Basic animation playback shader

This is a basic shader I wrote to cycle through a series of animation frames packed into a single LUT image. I made variants to load a 1D image that’s basically zoetrope-style and another that loads a 2D array. They’re both intended for very low-framerate animations and it gets to be a real hassle to make (and the filesize can get out of hand quickly) with very long animations and/or high framerates. I found this site at least streamlines the tedious process of appending the images (I’m hoping that someone else knows of a programmatic way of handling it). The shader crossfades between the frames to make the transitions less jarring.

I had originally intended to reduce the size of the LUTs by reducing the resolution, but that actually makes them bigger for some reason unless/until you do pretty drastic reductions in quality. I guess it has to do with png compression :man_shrugging: The 2D images seem to handle it better than the 1D FWIW.

Before anyone asks, this shader isn’t useful for general purposes and was intended for, e.g., adding an animated screen to the arcade cabinets in the background of borders and so forth. @HyperspaceMadness

1D version:

#version 450

layout(push_constant) uniform Push
{
	vec4 SourceSize;
	vec4 OriginalSize;
	vec4 OutputSize;
	uint FrameCount;
} params;

layout(std140, set = 0, binding = 0) uniform UBO
{
	mat4 MVP;
} global;

// set up our constants based on the characteristics of our frame sheet
const float tick = 100.;        // how many real frames between each transition; animation speed
int itick = int(tick);          // go ahead and cast this once because we'll need it
const float frames = 25.;       // the total number of frames in the animation

#pragma stage vertex
layout(location = 0) in vec4 Position;
layout(location = 1) in vec2 TexCoord;
layout(location = 0) out vec2 vTexCoord;
layout(location = 1) out vec2 counter;
layout(location = 2) out float mixer;

void main()
{
   gl_Position = global.MVP * Position;
   vTexCoord = TexCoord;
   
   // precalculate these counters in the vertex for speed
   counter.x = float(int(mod(float(params.FrameCount / itick), frames)));
   // store the 'next frame' counters in the same vec2 why not
   counter.y = float(int(mod(float((params.FrameCount + itick) / itick), frames)));
   
   // blend between frames to smooth the transition across the tick period
   mixer = mod(float(params.FrameCount), itick) / itick;
}

#pragma stage fragment
layout(location = 0) in vec2 vTexCoord;
layout(location = 1) in vec2 counter;
layout(location = 2) in float mixer;
layout(location = 0) out vec4 FragColor;
layout(set = 0, binding = 2) uniform sampler2D Source;
layout(set = 0, binding = 3) uniform sampler2D tiles;

void main()
{
   // iterate through the frames of animation on each tick
   float x_count = counter.x / frames;
   vec2 animCoord = vTexCoord.xy / vec2(frames, 1.) + vec2(x_count, 0.);
   
   // step ahead 1 tick to get the next animation frame
   float next_x = counter.y / frames;
   vec2 nextCoord = vTexCoord.xy / vec2(frames, 1.) + vec2(next_x, 0.);
   
   vec3 curr = texture(tiles, animCoord).rgb;
   vec3 next = texture(tiles, nextCoord).rgb;
   
   FragColor = vec4(mix(curr, next, mixer), 1.0);
}

and the 2D version:

#version 450

layout(push_constant) uniform Push
{
	vec4 SourceSize;
	vec4 OriginalSize;
	vec4 OutputSize;
	uint FrameCount;
} params;

layout(std140, set = 0, binding = 0) uniform UBO
{
	mat4 MVP;
} global;

// set up our constants based on the characteristics of our frame sheet
const float tick = 24.;          // how many real frames between each transition; animation speed
int itick = int(tick);           // go ahead and cast this once because we'll need it
const vec2 grid = vec2(5.,5.);   // the dimensions of our animation sheet matrix

#pragma stage vertex
layout(location = 0) in vec4 Position;
layout(location = 1) in vec2 TexCoord;
layout(location = 0) out vec2 vTexCoord;
layout(location = 1) out vec2 frameDims;
layout(location = 2) out float frameNum;
layout(location = 3) out vec4 counter;
layout(location = 4) out float mixer;

void main()
{
   gl_Position = global.MVP * Position;
   vTexCoord = TexCoord;
   
   frameDims = 1. / grid.xy;   // the size of each frame of animation
   frameNum = grid.x * grid.y; // total number of frames in our animation
   
   // precalculate these counters in the vertex for speed
   counter.x = float(int(mod(float(params.FrameCount / itick), frameNum) / grid.x));
   counter.y = float(int(mod(float(params.FrameCount / int(tick * grid.y)), frameNum) / grid.y));
   // store the 'next frame' counters in the same vec4 why not
   counter.z = float(int(mod(float((params.FrameCount + itick) / itick), frameNum) / grid.x));
   counter.w = float(int(mod(float((params.FrameCount + itick) / int(tick * grid.y)), frameNum) / grid.y));
   
   // blend between frames to smooth the transition a little
   mixer = mod(float(params.FrameCount), itick) / itick;
}

#pragma stage fragment
layout(location = 0) in vec2 vTexCoord;
layout(location = 1) in vec2 frameDims;
layout(location = 2) in float frameNum;
layout(location = 3) in vec4 counter;
layout(location = 4) in float mixer;
layout(location = 0) out vec4 FragColor;
layout(set = 0, binding = 2) uniform sampler2D Source;
layout(set = 0, binding = 3) uniform sampler2D tiles;

void main()
{   
   // iterate through the frames of animation in the grid on each tick
   float x_count = frameDims.x * counter.x;
   float y_count = frameDims.y * counter.y;
   vec2 animCoord = vTexCoord.xy * frameDims + vec2(x_count, y_count);
   
   // step ahead 1 tick to get the next animation frame
   float next_x = frameDims.x * counter.z;
   float next_y = frameDims.y * counter.w;
   vec2 nextCoord = vTexCoord.xy * frameDims + vec2(next_x, next_y);
   
   vec3 curr = texture(tiles, animCoord).rgb;
   vec3 next = texture(tiles, nextCoord).rgb;
   
   FragColor = vec4(mix(curr, next, mixer), 1.0);
}

I tried to make a single version that responded intelligently to setting a Y size of 1 but wasn’t able to make it happen (though I admittedly didn’t spend a ton of time on it, so maybe I just missed something obvious).

Here are the 1D and 2D images for it, respectively:

EDIT: whoops, wrong image for the 2D. Here’s the right one:

6 Likes

I do this for videos I have all the time, to get thumbnails, and use ffmpeg for it. A pratical way to do it can be followed at https://superuser.com/questions/625189/combine-multiple-images-to-form-a-strip-of-images-ffmpeg. The tile=5x1 is the columns x rows, for those who don’t know.

2 Likes

ohhhhh, that’s perfect. You just made my day. Cheers!

5 Likes