When an AI sprite sheet from video is the right call
There are two kinds of AI sprite sheet generator. One draws grid cells directly, usually as pixel art, and struggles to keep the character identical from cell to cell. The other, covered here, renders a short transparent-background animation and turns its frames into a sheet. Every frame comes from one continuous performance, so motion is smooth and edges are soft. What you give up is control over individual frames and over the camera, and that trade-off decides where it fits:
| Use case | Fit | Why |
|---|---|---|
| Prototypes and vertical slices | Good | Placeholder art that already moves well, replaceable later without code changes. |
| Idle, victory and celebration loops | Good | Front-facing, on-the-spot motions are exactly what a rendered clip does best. |
| Menu, shop and tutorial mascots | Good | The character lives in UI, faces the player, and needs four or five moods, not a moveset. |
| Visual novels | Good | Breathing idles and reaction beats (surprise, laugh, sulk) for a front-facing cast. |
| Casual and mobile games | Good | Match-3, idle and puzzle games use characters as presenters, not as physics bodies. |
| Pixel art | Poor | Downscaled renders are anti-aliased and off-palette. Use Aseprite or a pixel-specific generator. |
| 4- or 8-direction locomotion | Poor | Each clip has a single camera angle; you cannot get matching back, side and diagonal walks. |
| Frame-precise combat | Poor | Startup, active and recovery frames are tuned one by one. You need authored frames. |
| Dozens of actions that must match | Poor | Each clip is a separate performance. Start poses are close, not identical, so blending is hard. |
The concrete limits of our own studio: clips are square, 720 × 720 pixels, 24 fps, 4 to 12 seconds long, from one fixed front-facing angle, with the character performing on the spot. There is no camera movement, no audio and no skeleton, so if you need runtime bone blending (Spine, Godot’s Skeleton2D, Unity’s 2D Animation package), a rendered sheet is the wrong tool. If you are still comparing generators, the roundup of AI character animation tools covers the alternatives.
Generating a matching idle, jump, wave and celebrate set
Consistency between actions comes from approving a design before any animation exists. In Character Animation AI you describe the character (or upload concept art as a reference), pick a style, and regenerate until it is right. Approval locks the design, so the idle, the jump and the celebration share the same proportions and palette (the how it works page shows both steps). Flat and Stylized keep a clean silhouette at small sizes; Soft 3D suits casual mobile games. Then animate once per action:
- Idle preset for anything that loops forever.
- Jump and Wave presets for one-shot reactions and greetings.
- Custom for the rest, for example “pumps both fists in a quick, proud celebration, then returns to the same relaxed standing pose”.
Two prompt habits matter most for games. Keep the motion on the spot: travel across the frame wastes cells and fights your game’s own movement code. And end every custom motion with a return to a neutral pose, so the last frame of “celebrate” resembles the first frame of “idle” and switching does not pop. The animation principles guide goes deeper on loops.
Before downloading, trim each clip to its single best cycle in the free editor (fewer frames, less memory), and optionally pick the 360 or 240 px output size. Skip the mirror option; flipping in the engine is free. Download the transparent WebM: the GIF is 480 px, 15 fps and 1-bit transparent (hard edges), and the MP4 has no alpha.
For a bigger cast, the MCP server lets Claude Code, Cursor or another MCP client run this batch for you: list your characters, start Idle, Jump, Wave and a custom celebrate one at a time with your approval, and save the WebMs into assets/raw/. The MCP automation guide has the prompts. Each design and each animation costs one credit, so a four-action set is five credits.
Video to sprite sheet with ffmpeg: frame rate, size and alpha
Do the texture-memory maths first
A PNG sheet that weighs a few megabytes on disk is decoded to raw pixels on the GPU, and raw RGBA costs 4 bytes per pixel. So the number that matters is frames × width × height × 4. Godot’s import docs make the same point: its default Lossless mode only compresses the file on disk, not the video memory. Here is what one six-second clip costs:
| Settings (6 s clip) | Frames | Memory (decimal MB) |
|---|---|---|
| 24 fps, 720 px (raw export) | 144 | 298.6 |
| 12 fps, 720 px | 72 | 149.3 |
| 12 fps, 360 px | 72 | 37.3 |
| 12 fps, 256 px | 72 | 18.9 |
| 8 fps, 256 px | 48 | 12.6 |
Multiply by the number of actions (four actions at 12 fps and 256 px is about 75 MB) and add roughly a third if you enable mipmaps. Three levers bring this down: halve the frame rate to 12 fps (animating “on twos”, which reads as deliberate rather than choppy), scale to the size the sprite is actually drawn at, and trim empty space around the character (next section). For mobile and web builds, keep each sheet at or under 4096 × 4096; Godot’s docs note that these platforms usually cannot display larger textures.
Check the alpha channel, then extract
First confirm the WebM really carries transparency. Do not look for yuva420p in the stream line: ffprobe reports these files as plain yuv420p and records the alpha as a tag.
ffprobe -v error -show_streams wave.webm | grep -i alpha
# TAG:alpha_mode=1 <- the alpha channel is there (some encoders write ALPHA_MODE)Now the gotcha behind most “my frames have a black background” questions: ffmpeg’s native VP9 decoder ignores the alpha channel. Request the libvpx decoder explicitly, and put it before -i, because options placed before an input apply to that input. We tested both: without it the PNGs are RGB; with it, RGBA.
mkdir -p frames
ffmpeg -c:v libvpx-vp9 -i wave.webm \
-vf "fps=12,scale=360:360:flags=lanczos,format=rgba" \
frames/wave_%03d.pngfps=12 keeps every other frame of the 24 fps source, scale resizes with a sharp Lanczos filter, and format=rgba guarantees four channels. We ran this on a real six-second export from our studio: 145 frames at 24 fps became 72 RGBA PNGs. To know the count before you pick a grid (trimmed clips will not match the table below), run ffmpeg -c:v libvpx-vp9 -i wave.webm -vf fps=12 -f null - 2>&1 | grep -o 'frame= *[0-9]*' | tail -1. “Unknown decoder” means your build lacks libvpx (ffmpeg -decoders | grep libvpx checks; Homebrew and most Linux builds include it). For a VP8 file, use -c:v libvpx.
Packing the sprite sheet: ffmpeg tile, montage or an atlas packer
You can skip the folder of PNGs entirely: ffmpeg’s tile filter lays frames into a grid, left to right and top to bottom, which is the order Godot, Unity and Phaser number them in.
ffmpeg -c:v libvpx-vp9 -i wave.webm \
-vf "fps=12,scale=360:360:flags=lanczos,format=rgba,tile=9x8:margin=2:padding=2:color=0x00000000" \
-frames:v 1 -update 1 wave_sheet.pngcolor=0x00000000 makes gutters and unused cells transparent (the default is opaque black). margin=2 puts 2 px around the outside and padding=2 puts 2 px between cells, so this sheet is 3260 × 2898 px. -update 1 tells ffmpeg you are writing one image, not a numbered sequence. Pick a grid with at least as many cells as frames; leftover cells stay transparent. At 12 fps and 360 px cells:
| Clip length | Frames | Grid | Sheet (before gutters) |
|---|---|---|---|
| 4 s | 48 | 8×6 | 2880 × 2160 |
| 6 s | 72 | 9×8 | 3240 × 2880 |
| 8 s | 96 | 10×10 | 3600 × 3600 |
| 10 s | 120 | 11×11 | 3960 × 3960 |
| 12 s | 144 | 12×12 | 4320 × 4320 (too big for mobile: drop to 8 fps or smaller cells) |
Trim the empty space, the same way for every action
ffmpeg can measure the bounding box of the opaque pixels in every frame, and a little awk turns that into one crop rectangle. Run it over all of a character’s clips at once so idle, jump and wave share the rectangle: the feet then sit on the same pixel row in every sheet, and the sprite does not shift when animations switch.
for f in idle.webm jump.webm wave.webm; do
ffmpeg -c:v libvpx-vp9 -i "$f" -vf "alphaextract,bbox=min_val=16" -f null - 2>&1
done | grep -o "x1:[0-9]* x2:[0-9]* y1:[0-9]* y2:[0-9]*" \
| awk -F'[: ]' 'NR==1{x1=$2;x2=$4;y1=$6;y2=$8}
{if($2<x1)x1=$2; if($4>x2)x2=$4; if($6<y1)y1=$6; if($8>y2)y2=$8}
END{printf "crop=%d:%d:%d:%d\n", x2-x1+1, y2-y1+1, x1, y1}'
# prints something like: crop=519:479:101:81Put the printed crop before the scale, and scale by width so the aspect ratio is kept:
ffmpeg -c:v libvpx-vp9 -i jump.webm \
-vf "fps=12,format=rgba,crop=519:479:101:81,scale=260:-1:flags=lanczos,tile=8x6:margin=2:padding=2:color=0x00000000" \
-frames:v 1 -update 1 jump_sheet.png
# 260 x 240 cells, sheet 2098 x 1454 pxmin_val=16 ignores faint noise in the alpha plane. How much trimming saves depends on the motion: in our tests it ranged from roughly half the pixels (a small subject in a short hop) to almost nothing (a big-eared fox whose tail and plate swing out to the frame edges). It is always worth measuring.
Padding, bleeding and power-of-two sizes
- Bleeding is filtering sampling a neighbouring cell, so a sliver of the next frame flickers at the edge. A transparent gutter of 2 px or more prevents it; TexturePacker’s docs give the same minimum for shape padding.
- Extrusion repeats a sprite’s border pixels outward. It matters when pixels touch the cell edge (tiles, tightly trimmed atlases), rarely for a character with a transparent gutter.
- Power of two is no longer required by current engines. Unity’s manual says non-power-of-two textures take slightly more memory and may sample slower, and are padded up where a platform cannot handle them. For GPU block compression, keep dimensions a multiple of 4. A UI mascot drawn at one size also does not need mipmaps.
ImageMagick and atlas packers
If you already have a folder of frames, ImageMagick 7 does the same grid:
magick montage frames/wave_*.png -tile 9x -geometry +2+2 -background none wave_sheet.pngAlways pass -geometry: montage’s default shrinks every image to fit 120 × 120. An offset with no size keeps frames at full size, and +2+2 adds 2 px around each tile, so the gap between cells is 4 px (margin 2, spacing 4 in your engine). -background none keeps it transparent.
For many actions, frames of different sizes, or several characters on one texture, use an atlas packer that writes a JSON file beside the PNG. TexturePacker is the polished commercial option, with Godot, Unity and Phaser exporters plus trim, extrude, padding and size constraints. free-tex-packer is MIT-licensed, runs in the browser or on the desktop, and exports Phaser, Pixi and Godot formats, though its author now fixes only critical bugs.
Godot 4: SpriteFrames and AnimatedSprite2D from a sprite sheet
These steps match the current Godot 4.7 editor and its 2D sprite animation docs.
- Copy the sheet into the project. In the Import dock, leave Compress › Mode on Lossless (the default; the docs warn VRAM Compressed shows artefacts in 2D) and Process › Fix Alpha Border on (also default), which prevents dark fringes.
- Add an AnimatedSprite2D node. In the Inspector, open Sprite Frames and choose New SpriteFrames, then click the resource to open the SpriteFrames panel at the bottom.
- Click Add frames from a Sprite Sheet (the grid icon) and pick the PNG. In the Select Frames dialog, set Size to your cell size (260 × 240 in the example), Separation to 2 × 2 and Offset to 2 × 2 to match the ffmpeg gutters. Set Frame Order to Left to Right, Top to Bottom, click Select All, then Add 48 Frame(s).
- Rename the
defaultanimation tojumpand set the FPS field to 12 (it starts at 5, which would play your sheet in slow motion). Use the Animation Looping button: off for one-shots like jump and celebrate, on for idle. In 4.7 the same button also cycles to ping-pong, which plays a loop forward and back without storing the reversed frames. - Add an animation per action (idle, wave, celebrate) in the same SpriteFrames resource, and turn on Autoplay on Load for idle.
extends Node2D
@onready var sprite: AnimatedSprite2D = $AnimatedSprite2D
func celebrate() -> void:
sprite.play("celebrate")
await sprite.animation_finished # not emitted for looping animations
sprite.play("idle")
func face_left(left: bool) -> void:
sprite.flip_h = leftTexture filtering is set on the node (CanvasItem › Texture › Filter) or as a project default. Linear suits rendered characters; Nearest is for pixel art.
Unity sprite sheet animation: Grid By Cell Size and a clip
- Drag the PNG into the Project window and select it. In the Inspector set Texture Type to Sprite (2D and UI) (automatic in a 2D project) and Sprite Mode to Multiple. Use Bilinear filtering for rendered art, tick Alpha Is Transparency (it dilates colour into transparent pixels to avoid edge artefacts), and check that Max Size is at least the sheet’s longest side, or Unity scales it down on import. Click Apply.
- Open the Sprite Editor. From the Slice dropdown choose Type: Grid By Cell Size, then set Pixel Size to 260 × 240, Offset to 2 × 2 and Padding to 2 × 2. Set Pivot to Bottom Center so the feet stay planted when animations change. Click Slice, then Apply. Each frame becomes a child sprite of the texture; empty cells are discarded unless you tick Keep Empty Rects.
- Select the GameObject that has a Sprite Renderer, open Window › Animation › Animation (Ctrl+6, or Cmd+6 on macOS), click Create and save the clip. Shift-select the sliced sprites in the Project window and drag them onto the Animation window’s timeline.
- In the Animation window’s More (⋮) menu, enable Show Sample Rate and type 12 into Samples. The default is 60, which plays a 12 fps sheet five times too fast.
- Select the clip asset and tick Loop Time for idle; untick it for jump and celebrate, then wire transitions in the Animator, or call
animator.Play("Celebrate")from a script.
Shortcut from Unity’s tutorials: drop all the sliced sprites into the Scene view and Unity creates the GameObject, clip and Animator in one go (you still set the sample rate). For facing direction, use the Sprite Renderer’s Flip X.
Phaser spritesheet: load.spritesheet, anims.create and generateFrameNumbers
The loader’s margin and spacing options map one-to-one to ffmpeg’s margin and padding, as listed in Phaser’s ImageFrameConfig reference. These calls are the same in Phaser 3 and the current Phaser 4.
// preload()
this.load.spritesheet('dino-wave', 'assets/dino-wave.png', {
frameWidth: 260,
frameHeight: 240,
margin: 2, // ffmpeg tile margin
spacing: 2, // ffmpeg tile padding
});
// create()
this.anims.create({
key: 'wave',
frames: this.anims.generateFrameNumbers('dino-wave', { start: 0, end: 71 }),
frameRate: 12, // the default is 24: twice too fast for a 12 fps sheet
repeat: -1, // loop forever; use 0 for one-shots
// yoyo: true, // ping-pong without doubling the frames
});
const dino = this.add.sprite(400, 300, 'dino-wave').play('wave');
dino.setFlipX(true); // face leftFor a JSON atlas, use this.load.atlas(key, png, json) and this.anims.generateFrameNames(key, { prefix: 'wave_', start: 1, end: 72, zeroPad: 3, suffix: '.png' }), matching the frame names your packer wrote. Keep each texture within 4096 px for mobile browsers; this.load.multiatlas spreads a large move set across several.
Can you play the transparent video instead of using sprites?
Sometimes, and it is worth knowing exactly where each engine stands:
- Unity’s VideoPlayer supports per-pixel alpha from two sources only, according to its manual: Apple ProRes 4444 and WebM with VP8. Apart from a few platform exceptions it does not import VP9, which is what our WebM uses. Either import the ProRes 4444 MOV export with Transcode and Keep Alpha enabled, or convert the WebM to VP8 (tested, alpha preserved). On Android, Unity says to enable Transcode and Keep Alpha for WebM too, because native VP8 transparency varies by device.
ffmpeg -c:v libvpx-vp9 -i wave.webm -c:v libvpx -pix_fmt yuva420p \ -auto-alt-ref 0 -b:v 2M -an wave-vp8.webm - Godot 4 supports only Ogg Theora in core; WebM support was removed in 4.0, and decoding runs on the CPU. The docs offer a chroma-key shader for cut-outs, not an alpha video path, so for a transparent character, sprites are the practical route.
- Phaser’s Video game object can show transparent WebM where the browser plays it, which rules out Safari for VP9 alpha. The transparent video formats guide explains the browser split.
Sprites usually win in a game: you can switch animations on an exact frame, draw ten copies from one texture, reverse or ping-pong for free, and get identical behaviour on every platform with no decoder involved. Video wins when the clip is too big to store as frames (a 12-second clip at 720 px and 24 fps is 288 frames, about 597 MB as raw RGBA), so a full-screen intro is a job for the VideoPlayer.
Your first sprite sheet this afternoon
- Approve one design, then animate Idle, Jump, Wave and a custom celebrate that ends in a neutral pose.
- Trim each clip to its best cycle in the free editor and download the transparent WebM.
- Confirm
alpha_mode=1with ffprobe, and always decode with-c:v libvpx-vp9before-i. - Budget memory as frames × width × height × 4; start at 12 fps and the size you actually draw.
- Measure one crop rectangle across all clips, then tile with a transparent 2 px gutter.
- Import with matching offset and spacing, a bottom-centre pivot, and the frame rate set to 12.
- Test on your lowest-end target device before you add a fifth action.
New accounts get three free credits at sign-up, no card needed: enough for a design plus an idle and a jump to push through this pipeline today. When the game heads for a store page, a client or ad revenue, move to the Professional plan for the commercial licence.
