Skip to content

Instantly share code, notes, and snippets.

@MrChickenRocket
Created June 8, 2025 01:23
Show Gist options
  • Save MrChickenRocket/d1c2ca4e1469976b2f036424af5ce855 to your computer and use it in GitHub Desktop.
Save MrChickenRocket/d1c2ca4e1469976b2f036424af5ce855 to your computer and use it in GitHub Desktop.
--!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)
@MrChickenRocket
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment