Created
June 8, 2025 01:23
-
-
Save MrChickenRocket/d1c2ca4e1469976b2f036424af5ce855 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--!strict | |
--!native | |
--Polychrome effect - tag MeshParts with "Polychrome" | |
--Rewrites an editable image with some precalculate buffers each frame (because raw editableimages are memory capped!) | |
--Default values use about 30mb, but you can easily reduce/increase this with these two variables | |
--Try and be sane :) | |
local size : number = 128 | |
local frameCount : number = 500 | |
local animationSpeed : number = 0.5 --1 is full speed | |
local editableImage = game.AssetService:CreateEditableImage({Size = Vector2.new(size,size)}) | |
local textureContent = Content.fromObject(editableImage) | |
local halfSize : number = size * 0.5 | |
local invSize : number = 1 / size | |
local currentFrame : number = 1 | |
local frameAccumulator : number = 0 | |
local lastFrame : number = -1 | |
-- Precalculated frame buffers | |
local frameBuffers : {buffer} = {} | |
-- Calculate phases that will loop perfectly over frames | |
local function calculateLoopingPhases(frameIndex : number) | |
local t = (frameIndex - 1) / frameCount -- 0 to 1 over the loop | |
return { | |
phase1 = t * math.pi * 2 * 1, | |
phase2 = t * math.pi * 2 * 2, | |
phase3 = t * math.pi * 2 * 3, | |
phase4 = t * math.pi * 2 * 2, | |
phase5 = t * math.pi * 2 * 1, | |
hueShift = t * 1 -- Reduced from 2 to 1 full hue cycle | |
} | |
end | |
-- Precalculate all frames | |
print("Precalculating", frameCount, "frames...") | |
local startTime = tick() | |
for frame = 1, frameCount do | |
local phases = calculateLoopingPhases(frame) | |
local pixelArray = buffer.create(size * size * 4) | |
local index = 0 | |
for y = 0, size - 1 do | |
local ny = (y - halfSize) * invSize | |
local wave1y = math.sin(ny * 8 + phases.phase1) * 0.5 | |
local wave2y = math.sin(ny * 12 + phases.phase2) * 0.3 | |
for x = 0, size - 1 do | |
local nx = (x - halfSize) * invSize | |
-- Multiple sine waves for complex plasma pattern | |
local wave1x = math.sin(nx * 10 + phases.phase3) * 0.5 | |
local wave2x = math.sin(nx * 6 + phases.phase4) * 0.4 | |
local wave3 = math.sin(math.sqrt(nx*nx + ny*ny) * 15 + phases.phase5) * 0.3 | |
-- Combine waves | |
local plasma = wave1x + wave1y + wave2x + wave2y + wave3 | |
-- Convert to hue (0-1 range) with phase shift | |
local hue = (plasma + phases.hueShift) % 1 | |
-- Convert HSV to RGB with high saturation and brightness | |
local col = Color3.fromHSV(hue, 0.4, 1) | |
-- Store as RGBA values | |
buffer.writeu8(pixelArray, index, col.R * 255) | |
buffer.writeu8(pixelArray, index + 1, col.G * 255) | |
buffer.writeu8(pixelArray, index + 2, col.B * 255) | |
buffer.writeu8(pixelArray, index + 3, 255) | |
index = index + 4 | |
end | |
end | |
frameBuffers[frame] = pixelArray | |
end | |
print("Precalculation complete! Took", (tick() - startTime) * 1000, "ms") | |
print("Memory usage:", (frameCount * size * size * 4) / (1024 * 1024), "MB for all frames") | |
-- Set up texture assignment for tagged objects | |
-- (Doesn't handle untagging!) | |
game.CollectionService:GetInstanceAddedSignal("Polychrome"):Connect(function(value) | |
if value:IsA("MeshPart") then | |
value.TextureContent = textureContent | |
end | |
end) | |
for key, value in game.CollectionService:GetTagged("Polychrome") do | |
if value:IsA("MeshPart") then | |
value.TextureContent = textureContent | |
end | |
end | |
game:GetService("RunService").Heartbeat:Connect(function(dt : number) | |
local start = tick() | |
-- Calculate target frame rate (60 FPS) | |
local targetFPS = 60 | |
local frameTime = 1 / targetFPS | |
-- Accumulate time based on delta time | |
frameAccumulator = frameAccumulator + dt * animationSpeed | |
-- Check if enough time has passed for the next frame | |
if frameAccumulator >= frameTime then | |
local framesToAdvance = math.floor(frameAccumulator / frameTime) | |
frameAccumulator = frameAccumulator - (framesToAdvance * frameTime) | |
currentFrame = currentFrame + framesToAdvance | |
currentFrame = ((currentFrame - 1) % frameCount) + 1 | |
end | |
-- Write the current frame buffer directly to the image | |
if (currentFrame ~= lastFrame) then | |
editableImage:WritePixelsBuffer(Vector2.new(0, 0), Vector2.new(size, size), frameBuffers[currentFrame]) | |
lastFrame = currentFrame | |
end | |
--print("Frame render:", (tick() - start) * 1000, "ms Current frame:", currentFrame) | |
end) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The --!native tag doesn't do anything - woops :)
If you have CPU to spare, you could just generate the tiny little image each frame instead and not cache anything.