This article concludes a series of publications and is dedicated to real-time volumetric effects baked into sprite sheets.
A standard sprite sheet texture typically holds up to 64 frames (an 8x8 grid). With a resolution of 512x512 pixels per cell, the texture size reaches 4K, which is one of the most common sizes. However, when animation significantly exceeding 64 frames is required, it becomes necessary to increase resolution and add new frames. A more effective approach is retiming.
The Principle of Retiming
Retiming is based on the use of Optical Flow. This method allows for determining the difference between the current and the next frames, i.e., the pixel displacement needed to obtain an image that maximally approximates the appearance of smoke in the next frame.
Creating the Retiming Texture
The following method is used to store displacement data in a standard 8-bit texture:
- timeshift3 shifts to next frame (
$F+1
)
- The Houdini SOP node
texture optical flow
is used for precise displacement vector calculation, returning 3 scalar fields (flow.[x,y,z]
). - The first 2 fields (
flow.[x,y]
) are imported into a new COP context and converted fromgeometry
tolayer
type. - Two mono textures are combined into one standard RGBA texture.
- The resulting frames are written to disk and read as a sprite sheet.
float x = max(abs(@cmin.x),abs(@cmax.x));
float y = max(abs(@cmin.y),abs(@cmax.y));
@div = min(255,max(x,y));
@mult = @div/256;
To effectively store values in the standard PNG format, it’s necessary to compress the values into a range of 0 to 1. This way, even with slow movement, the 8-bit value range is effectively utilized.
@C = @C/2 + set(0.5,0.5,0,0);
To avoid losing the negative range, the normalized vector is divided by 2, and 0.5 is added to the X and Y coordinates.
Values needed to obtain a non-normalized version of the vector (multiplier) are written into the blue channel.
Using the Retiming Texture
Using the retiming texture is analogous to working with a regular sprite sheet, but with a key difference:
- Displacement vector values are always read from the previous frame.
- In intermediate frames, the previous frame number is rounded down (
floor
). - The transition to the next frame occurs in the next step.
The other textures used in the shader (lighting, color, etc.) need to be read twice:
- At the frame rounded down (
floor(frame)
. UV coordinates - displacement vector multiplied by the fractional part of the frame). - At the frame rounded up (
ceil(frame)
. UV coordinates + displacement vector multiplied by the inverse of the fractional part of the frame).This way, two images are obtained which are maximally similar in appearance. Then a crossfade is performed between them, which looks very smooth and is practically unnoticeable, creating the illusion of fluid motion while using significantly fewer frames in the sprite sheet.