Sprite atlas cutting (Crimsonland)¶
Status: In progress
This is based on the decompiled engine in analysis/ghidra/raw/crimsonland.exe_decompiled.c.
The engine does not load atlas metadata from disk; all slicing is hard‑coded.
UV grid tables¶
effect_uv_tables_init precomputes UV grids for 2×2, 4×4, 8×8, 16×16.
It fills tables with (u, v) pairs for each cell in row‑major order.
Step sizes:
- 2×2: 0.5
- 4×4: 0.25
- 8×8: 0.125
- 16×16: 0.0625
The renderer later uses these tables to build quads:
u0 = table[idx].u,v0 = table[idx].vu1 = u0 + step,v1 = v0 + step
Sprite table (engine‑hardcoded)¶
FUN_0042e0a0 reads a table at VA 0x004755F0.
Each entry is (cell_code, group_id); cell_code maps to grid size:
0x80 → 2,0x40 → 4,0x20 → 8,0x10 → 16.
Extracted table (effect_id → size code + frame index):
| effect_id | size code | grid | frame |
|---|---|---|---|
0x00 |
0x80 |
2 | 0x2 |
0x01 |
0x80 |
2 | 0x3 |
0x02 |
0x20 |
8 | 0x0 |
0x03 |
0x20 |
8 | 0x1 |
0x04 |
0x20 |
8 | 0x2 |
0x05 |
0x20 |
8 | 0x3 |
0x06 |
0x20 |
8 | 0x4 |
0x07 |
0x20 |
8 | 0x5 |
0x08 |
0x20 |
8 | 0x8 |
0x09 |
0x20 |
8 | 0x9 |
0x0a |
0x20 |
8 | 0xa |
0x0b |
0x20 |
8 | 0xb |
0x0c |
0x40 |
4 | 0x5 |
0x0d |
0x40 |
4 | 0x3 |
0x0e |
0x40 |
4 | 0x4 |
0x0f |
0x40 |
4 | 0x5 |
0x10 |
0x40 |
4 | 0x6 |
0x11 |
0x40 |
4 | 0x7 |
0x12 |
0x10 |
16 | 0x26 |
The frame index is passed to the renderer alongside the grid size; its semantics aren’t obvious from the decompile.
Visual note:
effect_id 0x12(grid 16, frame0x26) looks like a small brass shell/casing sprite rather than a classic muzzle flash.
Non-uniform sub-rects (grim_set_sub_rect)¶
The engine sometimes uses Grim2D vtable 0x108 (grim_set_sub_rect) to pick a
rectangle that spans multiple grid cells (not a single cell).
Known uses:
artifacts/assets/crimson/ui/ui_wicons.pnguses an 8×8 grid but selects 2×1 sub-rects. Theframeargument is derived from the weapon table (weapon_id * 2) and is reused across HUD + menu renders.- Some UI paths call
grim_set_sub_recttwice in a row to draw two adjacent slices from the same sheet (split-screen layouts).
Manual UV overrides (grim_set_uv_point)¶
Some effects bypass atlas slicing and write UVs directly.
- In
projectile_render, beam/chain effects (type_id0x15/0x16/0x17/0x2d) callgrim_set_uv_pointto force all U values to0.625and V to0..0.25, then draw a quad strip. This targets a thin vertical slice insideprojs.png, soprojs/grid2/frame001is further sub‑cut at runtime. - The same path later resets UVs with
grim_set_uv(0,0,1,1). grim_set_atlas_frameitself only takes(atlas_size, frame)ingrim.dll; any extra pointer args seen in the decompile are ignored.
UV debug (grid2 frame 1)¶
grim_set_uv_point uses u=0.625, v=0..0.25, which lands inside the
top‑right grid2 cell. The overlays below show the sub‑slice used for beam
effects.


How slicing is used in practice¶
The engine uses two patterns:
1) Direct grid selection: calls the renderer with an explicit grid size
(+0x104 with first arg = 2/4/8) and a frame index.
2) Sprite table selection: calls FUN_0042e0a0(index) which looks up the
grid size from the table above and passes that to the renderer.
Known assets and grids¶
artifacts/assets/crimson/game/projs.png(projectile_texture/DAT_0048f7d4)- Uses grid=4 (e.g.
+0x104(4, …)aroundanalysis/ghidra/raw/crimsonland.exe_decompiled.c:18448). - Uses grid=2 for some effects (e.g.
+0x104(2, 0)around:16479). - Several projectile/beam effects draw repeated quads along a vector using a single frame (segment tiling instead of unique frames).
-
One beam path calls
set_atlas_frame(4,2,<dir>,<dir>)with extra vector pointers; this likely orients the UVs for directional beam segments. -
artifacts/assets/crimson/game/bonuses.png(DAT_0048f7f0) - Uses sprite table index 0x10 (call at
:18550), which maps to grid=4. -
Sheet is 128×128 → 32×32 cells.
-
artifacts/assets/crimson/game/particles.png(DAT_0048f7ec) - Uses grid=8 for the main particle system (see
+0x104(8, …)at:9704). - Uses sprite table indices 0x10, 0x0e, 0x0d, 0x0c for UI/overlay effects
(calls at
:996?,:16217,:16854,:18788,:18824). These indices map to grid=4. -
Effects pool binds
particles.pngand uses the effect_id table above (0x00..0x12). Muzzle flash useseffect_id 0x12→ grid 16, frame0x26(the sprite itself resembles a shell casing). -
artifacts/assets/crimson/ui/ui_wicons.png(ui_weapon_icons_texture/DAT_0048f7e4) - Uses grid=8, but rendered via
grim_set_sub_rect(8, 2, 1, frame). - This implies each weapon icon spans 2×1 cells (wider than a single cell).
Projectile frames (projs.png)¶
In projectile_render, the projectile type_id is stored as an int but shows
up as float constants in the decompile (e.g. 2.66247e-44 = 0x13).
Known projs.png frame selections:
| type_id | grid | frame | Source | Notes |
|---|---|---|---|---|
0x13 |
2 | 0 | Jackhammer | Draws a small glow/splash; size scales with life. |
0x1d |
4 | 3 | Gauss Shotgun | Beam/segment style when life is 0.4. |
0x19 |
4 | 6 | Spider Plasma | Beam/segment style when life is 0.4. |
0x15 |
4 | 2 | Ion Minigun | Repeated along a vector to build beam/trail segments; also used by Man Bomb (perk id 53). |
0x16 |
4 | 2 | Ion Cannon | Repeated along a vector to build beam/trail segments; also used by Man Bomb (perk id 53). |
0x17 |
4 | 2 | Shrinkifier 5k | Repeated along a vector to build beam/trail segments. |
0x2d |
4 | 2 | Fire Bullets bonus + Fire Cough perk | Used by the Fire Bullets bonus (bonus id 14) and the Fire Cough perk (perk id 54). The bonus path spawns weapon_projectile_pellet_count[weapon_id] pellets per shot. |
The same path also calls grim_set_atlas_frame(2, …) for these beam types, but
the exact grid2 index is still unclear from the decompile.
- Enemy sheets (
artifacts/assets/crimson/game/zombie.png,artifacts/assets/crimson/game/lizard.png,artifacts/assets/crimson/game/alien.png,artifacts/assets/crimson/game/spider_sp1.png,artifacts/assets/crimson/game/spider_sp2.png,artifacts/assets/crimson/game/trooper.png) - Drawn via grid=8 (
+0x104(8, …)in the enemy render path around:9704). - Per‑enemy base frame offsets are stored in the enemy data struct
(e.g.
_DAT_00482760 = 0x20,_DAT_004827a4 = 0x10, etc).
Replicating the atlas cutting¶
src/crimson/atlas.py provides the same slicing math used by the engine:
grid_size_from_code(code)grid_size_for_index(table_index)uv_for_index(grid, index)rect_for_index(width, height, grid, index)slice_index(image, grid, index)slice_grid(image, grid)
This is sufficient to reproduce the engine’s sprite cuts for any of the uniform grids (2/4/8/16).
Exporting frames and manifests¶
scripts/atlas_export.py slices a sprite sheet into per‑frame PNGs and writes
out a JSON manifest with rect/UV data.
Outputs:
- Frames under
artifacts/atlas/projs/grid4/(e.g.frame_000.png). manifest.jsonwithimage,grid,cell_size, andframesentries.
Use --indices 0-3,8-11 to export a subset, or --table-index 0x10 to use the
engine sprite table (grid 4).
Bulk export¶
To export all textures referenced by the static scan into artifacts/atlas/frames/,
run:
uv run python scripts/atlas_scan.py --output-json artifacts/atlas/atlas_usage.json
uv run python scripts/atlas_export.py --all
The output layout is:
artifacts/atlas/frames/<relative_path>/<texture_name>/grid<grid>/frame_###.pngartifacts/atlas/frames/<relative_path>/<texture_name>/grid<grid>/manifest.json
Manifests include used_indices when the static scan reported concrete frame
indices for a grid.
Atlas usage by texture (static scan)¶
The notes below come from scanning analysis/ghidra/raw/crimsonland.exe_decompiled.c
for texture binds (+0xc4) followed by atlas selection (+0x104 or
FUN_0042e0a0). Use:
The JSON output includes texture, direct (grid/index), and table_indices
per bound texture.
They list which grids and indices are used, but not the semantic meaning of each frame.
artifacts/assets/crimson/game/projs.png(projs)- Direct grid calls: 4×4 indices
2,3,6(plus one call with extra parameters usinggrid=4, index=2), and 2×2 index0. -
Table index
0x10→ 4×4. -
artifacts/assets/crimson/game/bonuses.png(bonuses) -
Direct grid calls: 4×4 index
0, plus dynamic indicesiVar6andiVar6 + 1(frame selection happens in code). -
artifacts/assets/crimson/game/bodyset.png(bodyset) -
Table index
0x10→ 4×4. -
artifacts/assets/crimson/game/particles.png - Table indices
0x10,0x0e,0x0d,0x0c→ 4×4. - Table index
0x02→ 8×8. -
Direct grid calls: 2×2, 4×4, 8×8, 16×16 with a dynamic index (
uVar2). -
groundtexture (terrain) - Direct grid call: 8×8 with dynamic index.
Enemy animation slices (grid 8)¶
Enemies are rendered from 8×8 sheets (+0x104(8, frame)) with two selection
paths in FUN_00418b60:
- 32‑frame strip:
frame = floor(anim_phase)(0..31), optionally mirrored if the type table flag(&DAT_00482768)[type * 0x44] & 1is set. If the per‑creature flags include0x10, the frame offset shifts by+0x20(indices 32..63). - 8‑frame ping‑pong strip:
frame = base + 0x10 + pingpong(floor(anim_phase))wherebase = *(int *)(&DAT_00482760 + type * 0x44)and ping‑pong folds a 0..15 phase into 0..7..0.
Examples from the type init table (FUN_00412dc0):
- Zombie: base
0x20 - Lizard: base
0x10 - Spider SP1/SP2: base
0x10 - Alien: base
0x20
The animation phase itself lives at creature offset 0x94 and is advanced in
FUN_00426220 using a per‑type rate (&DAT_0048275c + type * 0x44).
These sheets often pack multiple animations for the same type (long strip plus short ping‑pong strip), and in some cases multiple type variants share one sheet by selecting different base offsets.
Enemy type table (DAT_00482728)¶
The render helper indexes a 0x44‑byte type table starting at DAT_00482728.
Known entries from FUN_00412dc0:
| Type id | Texture | Base (short strip) | Anim rate (hex) | Mirror flag | Notes |
|---|---|---|---|---|---|
| 0 | zombie.png |
0x20 |
0x3f99999a |
0 | uses 32‑frame strip by default |
| 1 | lizard.png |
0x10 |
0x3fcccccd |
1 | mirror flag set (DAT_004827ac = 1) |
| 2 | alien.png |
0x20 |
0x3faccccd |
0 | alt strip via creature flag 0x10 |
| 3 | spider_sp1.png |
0x10 |
0x3fc00000 |
1 | mirror flag set (DAT_00482834 = 1) |
| 4 | spider_sp2.png |
0x10 |
0x3fc00000 |
1 | mirror flag set (DAT_00482878 = 1) |
| 5 | trooper.png |
unknown | unknown | unknown | not initialized in FUN_00412dc0 |
Creature flags that select animation strips¶
Creature flags are written in the spawn helper FUN_00430af0 and control which
strip is used in FUN_00418b60.
Examples:
param_1 == 0x3asetsDAT_0049bfc4 = 0x10(forces the+0x20strip).- Many IDs (e.g.
0x7,0x8,0x9,0x0b) setDAT_0049bfc4 = 0x4(short strip). param_1 == 0setsDAT_0049bfc4 = 0x44, which still uses the long strip because bit0x40overrides the short‑strip branch.