Generate sprite sheets and export renders from 3D scenes β fast and simple.
Built with Vite + React + Tauri for a lightweight, native desktop experience.
Example.mov
Prebuilt desktop binaries are available on the Releases page.
| Platform | File |
|---|---|
| macOS | .dmg |
| Windows | .msi or .exe |
| Linux | .AppImage or .deb |
Download the appropriate file for your OS and run it directly β no setup required.
- [Github Pages] - https://kyonru.github.io/sprite-sheet-helper
| Format | Notes |
|---|---|
| GLB | Recommended β fully self-contained |
| GLTF | External .bin and textures must be co-located |
| FBX | Binary FBX recommended |
| OBJ | Geometry only; .mtl materials not loaded |
- Lights β Ambient, Directional, Point, Spot with adjustable intensity and color
- Camera β Orbit controls with preset angles: Top-down, Isometric (45Β° / 225Β°), ΒΎ RPG (55Β°), and more
- Transform controls β Reposition, rotate, and scale objects in the scene
- Post-processing effects β Pixelation, Bloom, Outline, Glitch, Depth of Field, ASCII, Dither, Palette, SSAO, and 20+ more
- Loads animation clips embedded in the model
- Timeline editor with play/pause, frame stepping, and per-clip trim/speed/loop settings
- Per-frame capture synced to animation playback
Record animation clips directly from a webcam or a photo β no 3D animation software needed.
- Live recording β pose detection runs in-browser via MediaPipe; record at up to 30 FPS
- Photo mode β upload a single image to capture a static pose
- Review step β scrub through every captured frame before saving:
- Trim start/end and delete individual frames
- Global corrections: tilt (X), turn (Y), lean (Z), mirror LβR, flip 180Β°
- Per-bone overrides: X/Y/Z euler sliders per bone per frame, with "apply to all frames" option
- Captured clips are saved to the model and available for export alongside embedded animations
| Format | Output |
|---|---|
spritesheet |
PNG + JSON metadata |
zip |
Raw frame PNGs in a ZIP |
gif |
Animated GIF |
bevy |
Rust structs + Cargo.toml snippet |
godot |
GDScript + resource file |
unity |
C# SpriteSheetAnimator class |
phaser |
Atlas JSON for Phaser 3 |
pygame |
Python module |
raylib |
C header |
love2d-lua |
Lua module for LΓVE 2D |
love2d-anim8 |
anim8 library format |
turbo |
Turbo engine format |
Save and load .sshProj files to preserve the full scene state β objects, lights, camera, animations, materials, spritesheet postprocess settings, and undo history included.
Manual camera framing is a guess: tuned so idle fills the frame, it clips run; tuned to be safe for everything, it wastes most of the sprite.
With --fit auto, the run measures every animation before capturing β posing each clip, taking world-space bounds at each sample, projecting them through every direction's camera β then solves a single distance and target that hold the widest case inside the margin you ask for. Every row captures at that framing, so one scale and one pivot cover the whole sheet, and --margin 6 really means 6px on every sprite.
Available in the app too: Fit camera to animation in the Export Workbench fits a single sequence the same way. Defaults to manual, so existing projects are unchanged.
The Export Workbench can apply 2D effects after capture and before atlas packing:
- Outer Outline, including crisp pixel outlines
- Drop Shadow and Glow
- Color Adjust
- Before/after preview with a draggable divider
Normal-map atlases stay clean: color frames are processed, while matching normal frames are only padded when needed so atlas rects stay aligned.
The CLI drives the app headlessly: it starts a local preview server, injects the model into a Puppeteer browser, captures frames, and writes the exported files to disk. No GUI required.
Build the app with the CLI bridge before running:
npm run build:clinpm run cli -- <input> [options]
# or after npm link / global install:
sprite-sheet-helper <input> [options]| Option | Default | Description |
|---|---|---|
--format |
spritesheet |
Export format (see table above) |
--frames |
8 |
Number of frames to capture per animation sequence |
--fps |
10 |
Playback frame rate |
--width |
64 |
Frame width in pixels |
--height |
64 |
Frame height in pixels |
--output |
./out |
Output directory |
--port |
4174 |
Port for the local preview server |
--workflow |
β | Workflow preset ID (see table below) |
--cameraDistance |
5 |
Camera distance from the model (used with --workflow) |
--cameraAngle / --phi |
β | Workflow camera elevation override in degrees |
--directionRotationOffset |
β | Rotate every workflow direction by this many degrees |
--target |
β | Camera target as x,y,z |
--directionOverride |
β | Per-direction camera override, e.g. N:phi=45,theta=0,distance=3 |
--normalMap |
false |
Capture and export a matching normal atlas |
--skipStepLabel |
β | Skip one workflow row label (repeatable). |
--skipStepLabels |
β | Comma-separated workflow row labels to skip. |
--forceAnimationsInPlace |
false |
Force workflow animation playback to stay at the same root location. |
--captureNormalMaps |
false |
Override normal-map capture for workflow runs. |
--atlasLayout |
rows |
Atlas layout: rows or packed |
--atlasPadding |
0 |
Empty pixels around each frame slot |
--atlasBleed |
0 |
Edge-pixel extrusion into padding |
--atlasSpriteMargin |
0 |
Transparent border inside each frame rect |
--fit |
manual |
auto solves one framing for the whole run |
--margin |
0 |
Guaranteed border around the sprite (used with --fit auto) |
--marginUnit |
px |
px or percent, per side |
--fitScope |
all |
What shares a framing: all, animation, direction |
--fitSamples |
12 |
Poses sampled per clip while measuring |
--atlasScale |
1 |
Scale atlas frame dimensions |
--maxAtlasSize |
2048 |
Maximum atlas page width and height |
--multiPage |
false |
Allow generic spritesheet page splitting |
--config |
β | Run one or more JSON-configured jobs |
--job |
β | Run a single job from a config file |
--dryRun |
false |
Print resolved jobs without launching the browser |
--json |
false |
Emit a machine-readable summary |
--writeSummary |
β | Write the JSON summary to a file |
--failOnWarnings |
false |
Exit with an error when warnings exist |
--quiet |
false |
Suppress progress logs |
--debug |
false |
Show browser console output |
--headful |
false |
Launch Chromium with a visible window |
--timeout |
β | Per-job timeout in milliseconds |
--exportTimeout |
60000 |
Export download timeout in milliseconds |
--workflowTimeout |
β | Workflow completion timeout in milliseconds |
Format aliases: bevy-rust β bevy, love2d β love2d-lua
Quick discovery commands:
sprite-sheet-helper --help
sprite-sheet-helper --list-formats
sprite-sheet-helper --list-workflows# 8-frame spritesheet at 64Γ64 (defaults)
sprite-sheet-helper character.glb
# LΓVE 2D export, 16 frames at 128Γ128
sprite-sheet-helper character.glb --format love2d --frames 16 --width 128 --height 128
# Animated GIF
sprite-sheet-helper character.fbx --format gif --frames 12 --fps 12 --output ./exports
# Packed generic atlas with page splitting
sprite-sheet-helper character.glb --format spritesheet --atlasLayout packed --multiPage true --maxAtlasSize 1024
# Workflow camera override with JSON output
sprite-sheet-helper character.glb --workflow topdown-4dir --cameraAngle 45 --target 0,0.8,0 --json
# Solve one framing for every animation and direction, with a 6px margin
sprite-sheet-helper character.glb --workflow platformer --fit auto --margin 6{
"defaults": { "format": "spritesheet", "frames": 4 },
"jobs": [
{
"id": "hero-topdown",
"input": "assets/hero.glb",
"output": "dist/hero",
"workflow": "topdown-4dir"
}
]
}Run all jobs, one job, or inspect the resolved settings:
sprite-sheet-helper --config sprites.json
sprite-sheet-helper --config sprites.json --job hero-topdown
sprite-sheet-helper --config sprites.json --dryRunUse the GHCR image when you want local or CI automation without installing Node or Chromium:
docker run --rm \
-v "$PWD:/work" \
ghcr.io/kyonru/sprite-sheet-helper:v0 \
/work/assets/hero.glb \
--workflow topdown-4dir \
--format godot \
--output /work/dist/spritesFor batch pipelines:
docker run --rm \
-v "$PWD:/work" \
ghcr.io/kyonru/sprite-sheet-helper:v0 \
--config /work/sprites.json \
--writeSummary /work/dist/sprites-summary.json- name: Generate sprites
uses: Kyonru/sprite-sheet-helper/action@main
with:
input: assets/hero.glb
output: dist/sprites
workflow: topdown-4dir
format: godotThe action also supports config-driven batches:
- name: Generate sprite batch
uses: Kyonru/sprite-sheet-helper/action@main
with:
config: sprites.json
fail-on-warnings: "true"On release tags like v0.4.0, CI publishes Docker image tags v0.4.0, v0.4, v0, and latest. Branch builds do not update published image tags.
See example/ for a standalone GitHub repository template that discovers every model in a models/ folder, runs the Docker Action on push to main, and uploads generated sprite artifacts.
out/
spritesheet.png
spritesheet.json
spritesheet.lua
main.lua
Workflows automate multi-angle sprite sheet generation. Instead of capturing a single sequence, a workflow iterates over every animation embedded in the model and every camera direction defined by the preset β producing one labeled sequence per combination.
Each row in the output is named {animationName}_{direction}, for example walk_N, idle_SE, run_Left.
| ID | Label | Directions | Camera elevation |
|---|---|---|---|
topdown-8dir |
Top Down 8-directional | N, NE, E, SE, S, SW, W, NW | ~overhead (phi=1Β°) |
topdown-4dir |
Top Down 4-directional | N, E, S, W | ~overhead (phi=1Β°) |
isometric |
Isometric | SE, NE, NW, SW | 45Β° elevation |
platformer |
Platformer / Side View | Right, Left | Horizon (phi=90Β°) |
sprite-sheet-helper character.glb --workflow <id> [options]| Option | Default | Description |
|---|---|---|
--workflow |
β | Workflow preset ID (required for workflow mode) |
--frames |
8 |
Frames captured per animation Γ direction combination |
--fps |
10 |
Playback frame rate for each capture |
--width |
64 |
Frame width in pixels |
--height |
64 |
Frame height in pixels |
--cameraDistance |
5 |
Distance of the camera from the model origin |
--cameraAngle / --phi |
preset | Camera elevation override in degrees |
--directionRotationOffset |
0 |
Rotate preset directions |
--target |
0,0,0 |
Camera target as x,y,z |
--skipStepLabels |
β | Comma-separated workflow step labels to skip. |
--forceAnimationsInPlace |
false |
Keep animations in place while rendering workflow steps. |
--captureNormalMaps |
false |
Override normal-map capture for the workflow. |
--format |
spritesheet |
Export format applied to the full multi-sequence output |
--output |
./out |
Output directory |
# Top-down 8-directional sheet β all animations, all 8 angles
sprite-sheet-helper character.glb --workflow topdown-8dir
# Isometric at 128Γ128, exported as a Godot resource
sprite-sheet-helper character.glb --workflow isometric --width 128 --height 128 --format godot
# Platformer workflow with closer camera and more frames
sprite-sheet-helper character.glb --workflow platformer --cameraDistance 3 --frames 16 --fps 12
# Top-down 4-dir, exported as a Bevy plugin
sprite-sheet-helper character.glb --workflow topdown-4dir --format bevy --output ./bevy-assets
# Reframe and rotate a workflow before capture
sprite-sheet-helper character.glb --workflow isometric --cameraAngle 40 --directionRotationOffset 15 --target 0,0.8,0out/
spritesheet.png β atlas image with all 8 sequences (2 anims Γ 4 dirs)
spritesheet.json β metadata: frame size, quads, animation names
The JSON metadata labels each animation strip:
{
"animations": [
{ "name": "idle_N", "fps": 10, "frameWidth": 64, "frameHeight": 64, "quads": [...] },
{ "name": "idle_E", "fps": 10, ... },
{ "name": "idle_S", "fps": 10, ... },
{ "name": "idle_W", "fps": 10, ... },
{ "name": "walk_N", "fps": 10, ... },
{ "name": "walk_E", "fps": 10, ... },
{ "name": "walk_S", "fps": 10, ... },
{ "name": "walk_W", "fps": 10, ... }
]
}- Node.js (LTS recommended)
- Rust toolchain
- Tauri platform dependencies β Official guide
git clone https://github.com/Kyonru/sprite-sheet-helper.git
cd sprite-sheet-helpernpm installnpm run tauri devThis starts Vite and launches the Tauri desktop app with hot reload.
npm run tauri build -- --debugnpm run tauri buildThe compiled application will be output to src-tauri/target/.
| Command | Description |
|---|---|
npm install |
Install all dependencies |
npm run tauri dev |
Run the app in development mode |
npm run tauri build |
Build the production desktop app |
npm run tauri build -- --debug |
Build a debug desktop app |
npm run build |
Build the web frontend only |
Sprite Sheet Helper is pwa + web-first. The app runs as a standard web app by default and is also packaged as a native desktop app via Tauri.
When a feature requires different implementations between web and native (Tauri), the following file naming convention is used:
| File | Used when |
|---|---|
feature.web.ts |
Default β runs in the browser / web build |
feature.tauri.ts |
Native override β runs in the Tauri desktop build |
At build time, a Vite plugin (WebTauriSwapPlugin) automatically swaps .web.* imports for their .tauri.* counterpart when building for desktop. If no .tauri.* file exists, the .web.* version is used as the fallback.
Example:
src/components/reload-prompt/
βββ prompt.web.ts β used in web builds (PWA service worker)
βββ prompt.tauri.ts β used in Tauri builds (no-op or native equivalent)When adding a feature that behaves differently on each platform:
- Implement the web version in
feature.web.ts - Implement the native version in
feature.tauri.ts - Import using the
.websuffix β the build system handles the rest:
import useMyFeature from "./feature.web";βΉοΈ Never import
.tauri.*files directly. Always import the.web.*version and let the plugin swap it at build time.
Contributions are welcome! Here's how to get started:
# 1. Fork and clone the repo, then:
git checkout -b feature/your-feature-name
# 2. Install dependencies
npm install
# 3. Make your changes, then commit
git commit -m "feat: describe your change"
# 4. Push and open a Pull Request
git push origin feature/your-feature-namePlease keep commits clear and scoped. Bug fixes, performance improvements, and new features are all appreciated.