Pre-rendering 3D geometry into 2D isometric sprites is the production route classic and modern isometric games actually use — Age of Empires, Factorio, and Hades all bake high-fidelity 3D models to 2D sprite sheets rather than rendering isometric scenes in real time.[^grid-dynamics] Blender is the free, scriptable tool of choice for this: model once, render every camera direction, ship flat PNGs to any 2D engine.
This file owns the Blender-specific delta: camera rigs, batch rendering, the
depth/normal-pass export used to condition AI generation, and projecting AI output back
onto geometry. For the web-native alternative (rendering isometric sprites straight out
of a three.js scene, no Blender required), see
threejs-orthographic.md. For the underlying angle math, see
projection-math.md. For post-render QA, see
../scripts/tile-validate.py.
The projection decision comes first, per projection-math.md:
decide true isometric (81.65% axonometric foreshortening, all three cube faces
render equal) or 2:1 dimetric (commonly called isometric in games; game tile
rendering, cube top is exactly 2× as wide as tall) before touching the camera. The two
rigs below are not interchangeable and mixing them across a set is the single most
common isometric-pre-render mistake.
Most tutorials present only one rig and treat it as "the" isometric camera. It isn't — there are two correct rigs for two different projections, and conflating them produces tiles that don't tile or illustrations that read as subtly "off." Both rigs below must be considered whenever an isometric Blender pipeline is discussed; picking the wrong one for the job is the signature footgun this reference exists to prevent.
| Target | Camera rotation (XYZ, degrees) | Projection type | Verification test |
|---|---|---|---|
| 2:1 dimetric game tiles | RotX 60°, RotY 0°, RotZ 45° | 2:1 dimetric (not true isometric) | Rendered cube top face is exactly 2× as wide as tall — elevation angle is 30° above the horizon, and sin(30°) = 0.5, which is the 2:1 pixel ratio the whole "game isometric" convention is built on. |
| True isometric | RotX 54.736°, RotY 0°, RotZ 45° | True isometric (all axes at 120°, cube tilt 35.264°) | All three visible cube faces render as equal parallelograms — no face looks "flatter" than another. 54.736° = 90° − 35.264° (arctan(√2)); this is the camera-elevation form of the same true-iso angle used everywhere else in this skill. |
Both rigs use an orthographic camera — never a perspective camera; a perspective lens introduces vanishing points, which is the #1 giveaway that breaks the isometric illusion no matter how the rotation is set.
Why both exist in the wild, unlabeled: Clint Bellanger's widely-cited Blender tutorial ("Isometric Tiles in Blender") uses the 60/0/45 rig and explicitly verifies the "cube top 2× as wide as tall" check[^bellanger] — that is the 2:1 dimetric rig, correct for pixel game tiles, and is what most "Blender isometric" tutorials teach because most people asking are making game tiles. The ControlNet ortho-to-AI workflow (§3 below) instead uses 54.736/0/45 — true isometric — because it targets illustrative renders (architecture, cutaways, hero art) where equal-face fidelity matters more than 2:1 pixel-grid tiling. Neither tutorial is wrong; they are solving different problems. State which one you need before opening Blender. A Blender Artists forum thread on "Creating an Isometric Camera" independently confirms the historical footgun: ortho angles that merely "look isometric" by eye are usually trimetric, not true isometric or 2:1 dimetric, and a commenter there calls this out explicitly — always dial in the exact rotation numbers above rather than eyeballing the viewport.[^blenderartists]
Position is far less exacting than rotation for an orthographic camera — since ortho projection has no perspective falloff, where the camera sits along its view vector doesn't change the render, only how far back it needs to be to fit the subject inside the ortho scale. Bellanger's tutorial places the camera at a sample offset such as LocX=10, LocY=−10, LocZ=10 (a diagonal position consistent with a 45° Z-rotation looking back at the origin) purely to keep it clear of geometry — treat the exact coordinates as illustrative, not load-bearing; only the rotation and orthographic scale are load-bearing.[^bellanger]
Clip Start low and Clip End high enough that geometry
never gets clipped as the rig rotates around it (the same failure mode documented for
Unity's near-clip in engine-integration.md — an orthographic
camera with a tight clip plane will silently clip corners of tall geometry that a
perspective camera's wider frustum tolerated).The production pattern for "one asset, every direction" is a parented pivot empty: the camera is parented to an Empty placed at the world origin (or the asset's pivot point); rotating the empty around its local Z axis rotates the camera around the subject while preserving the camera's own tilt (RotX / the 60° or 54.736° elevation). This gives clean, exact rotation increments instead of re-deriving camera position by hand for each direction.
Ctrl+P → Object, keep transform) so the camera's
local rotation (its RotX from the table above, RotZ 45°) is preserved relative to
the empty.360° / N steps and
render at each step:
assets/blender-iso-rig.py --directions N (§5) which
automates exactly this loop from the CLI.Render each direction to its own numbered PNG (name_dir00.png … name_dir07.png for
8-direction), transparent background, identical orthographic scale and resolution across
all frames. Do not try to composite the sheet inside Blender — hand the per-direction
PNGs to scripts/sheet-pack.py, which packs a directory of
tiles into one spritesheet PNG plus a JSON atlas (frame {x,y,w,h,trimmed,sourceW,
sourceH} per name) — Blender's compositor is the wrong tool for atlas packing; use the
purpose-built script downstream of the render.
For pixel-art-styled games, render at a comfortably high resolution (e.g. 512² or 1024²)
with anti-aliasing on for clean edges, then downsample and posterize/pixelate as a
post-process rather than trying to render native low-res — this is the workflow
demonstrated in community tutorials on rendering and pixelating isometric assets in
Blender, and it produces cleaner results than forcing Blender's renderer to output
native low-res pixel art directly.[^pixelate-tutorial] Downsampling with a box/nearest
filter to the target tile resolution (e.g. 512² → 64²) after rendering at high
resolution avoids the jagged, uncontrolled aliasing that comes from rendering natively
small. Feed the downsampled output through
scripts/tile-validate.py --max-colors to confirm the
palette stayed within your target budget after any smoothing/dithering step.
When the goal is AI-generated final art that is perspective-locked to a 3D blockout
(see ai-generation.md for the full decision ladder and ControlNet
theory), Blender's job shifts from "renderer of final sprites" to "renderer of
conditioning maps" — a depth pass and a camera-space normal pass, both exported as
flat images and fed into Stable Diffusion's ControlNet as structural guides. This is
steps 1–3 (of a 5-step pipeline) of the Blender→ControlNet workflow; step 4
(ControlNet + Stable Diffusion generation) belongs to
ai-generation.md and is only summarized here for continuity.
Build a simple 3D blockout of the target scene using primitive geometry — walls, support beams, crates, barrels; massing and silhouette matter far more than surface detail at this stage, since the blockout only needs to condition depth/normal maps, not appear in the final render. Set the camera to Orthographic and rotate it to the true isometric rig: RotX 54.736°, RotZ 45° (the true-iso row of the rig table in §1 — this workflow specifically wants true isometric, not the 2:1 dimetric game rig, because the target output is illustrative cutaway/scene art, not tiling game sprites). Isolate and scale up any complex assets (barrels, crates) within the camera frame so their geometric detail is clearly captured in the depth/normal passes — a barrel that occupies 4 pixels of the frame will bake to a useless depth blob.[^src-b-step1]
View Layer Properties → Passes → Data → enable the Z pass.Geometry node → Normal output → Vector Transform node, set
to convert World Space → Camera Space. This step is what makes the normal map
camera-space rather than world/object-space — camera-space normals are what most
ControlNet "normal" preprocessors/models expect, since they encode which way each
surface faces relative to the view, not relative to the world.Multiply-Add math node to remap the
[−1.0, 1.0] vector-component range into the standard [0.0, 1.0] RGB image range
(multiply by 0.5, add 0.5 — the conventional tangent-to-RGB normal-map encoding).Both exported maps (depth + normal) must come from the exact same camera transform used for step 1 — any camera nudge between the two passes desynchronizes them and the dual-ControlNet conditioning in step 4 will fight itself.
Covered in full in ai-generation.md: load the depth map into a
Depth ControlNet unit and the normal map into a Normal ControlNet unit in
AUTOMATIC1111/ComfyUI, generate with the documented parameters (Euler, ~15 steps, 768²,
CFG 7), using the prompt/negative-prompt doctrine described there. Do not duplicate that
material here — this file's job ends at "export two clean conditioning images from
Blender."
Once the AI has synthesized high-resolution, perspective-locked texture art (step 4, elsewhere), the workflow closes the loop by projecting that art back onto the original 3D blockout:
This is the highest-effort tier of the isometric AI pipeline — reserve it for hero
assets or asset families that need lighting variation; one-off sprites are usually
better served by generating directly at the target angle (see the decision ladder in
ai-generation.md) without the round-trip through Blender geometry.
assets/blender-iso-rig.pyThe skill ships assets/blender-iso-rig.py to automate §1 (rig construction) and §2
(N-direction batch rendering) so a team doesn't hand-build the empty/camera parenting
every time. Documented here per the Resource Protocol contract; see the script's own
--help and first-comment-block for the authoritative, current interface.
Invocation (headless, inside Blender's bundled Python):
blender -b -P assets/blender-iso-rig.py -- --projection dimetric21|true --directions 8 --out DIR [--resolution N]
-- is consumed by Blender itself (-b = background/
headless, -P = run this Python file); everything after -- is the script's own
argv — this split is a Blender convention, not a choice this script makes, and it's
the reason the CLI can't be blender -b -P script.py --projection true (Blender would
swallow --projection as one of its own flags).--projection dimetric21 selects the RotX 60°, RotZ 45° rig (§1); --projection
true selects RotX 54.736°, RotZ 45°. The script's argparse default is
dimetric21 (confirm with --help) — but treat that as a convenience for the common
game-tile case, not license to skip the decision: the projection choice is still the
first decision per this skill's doctrine (§1), and passing --projection explicitly
every time is the way to avoid silently shipping the wrong rig for an illustrative
(true-isometric) job.--directions N builds the parented-empty rig (§2) with N evenly-spaced rotations
(360/N per step) — 4 and 8 are the common values; the empty's rotation step and
the camera's own rig rotation (RotX/RotZ) are independent and the script must not
conflate them.--out DIR is the output directory for the numbered per-direction renders
(name_dir00.png … name_dir{N-1}.png), transparent film, one orthographic-scale
value held constant across all frames per §2.--resolution N sets the square render resolution in pixels (applies to both width
and height — isometric sprite renders are conventionally square-canvas even when the
final sprite content isn't square, so downstream trimming in
scripts/sheet-pack.py --trim has clean margin to work
with).Two run modes, by design:
.py in Blender's Text Editor and
pressing Run, or via Blender's normal windowed startup): builds the camera + empty
rig only — no render, no --out required. This lets an artist inspect the rig in the
3D viewport, nudge the orthographic scale to frame their asset, and render manually
or resume the headless batch path once satisfied.blender -b -P … -- --out DIR): builds the rig and renders every
direction to --out, suitable for CI/batch pipelines with no display.Degrading gracefully outside Blender. This file is plain Python but only runs
correctly inside Blender's bundled interpreter, which provides the bpy module. Running
it under a system Python (python assets/blender-iso-rig.py) must not crash with a raw
ModuleNotFoundError traceback — per the Resource Protocol's agent-safety rule, the
script catches the bpy import, prints a helpful stderr message naming the correct
invocation (blender -b -P assets/blender-iso-rig.py -- --help), and exits with the
protocol's 5 (PRECONDITION — environment issue, wrong interpreter) rather than an
uncaught exception. --help (run under either interpreter, since argument parsing
should not itself require bpy) prints the usage above plus worked EXAMPLES per the
first-comment-block contract in
SKILL-RESOURCE-PROTOCOL.md.
[^grid-dynamics]: The pre-render pipeline pattern (Age of Empires, Factorio, Hades
baking 3D-to-2D sprite sheets) — SRC-B, "3D-to-2D Pre-Rendering Pipeline" section.
[^bellanger]: Clint Bellanger, "Isometric Tiles in Blender" — the canonical tutorial for
the 60/0/45 dimetric rig and the "cube top 2× as wide as tall" verification test;
cited via SRC-A's Blender-for-isometric-rendering catalog.
[^blenderartists]: Blender Artists forum, "Creating an Isometric Camera" — documents the
true-iso rig and the historical orthographic-scale bug (fixed in Blender 2.49+); a
commenter's correction that eyeballed "nice-looking" ortho angles are typically
trimetric, not true isometric — cited via SRC-A.
[^qweb]: QWeb, "Creating an isometric rig in Blender" — the parented-empty, 8-frame
animated-rotation batch-render pattern with a downloadable pre-built rig — cited via
SRC-A.
[^pixelate-tutorial]: Community tutorial, "Rendering Isometric Assets & Pixelating
Renders [In Blender]" — the render-high/downsample-to-pixelate post-process pattern —
cited via SRC-A.
[^src-b-step1]: SRC-B, "3D-to-2D ControlNet Rendering Workflow in Blender," Step 1
(Modeling and Camera Configuration).
[^src-b-step2]: SRC-B, ibid., Step 2 (Rendering and Exporting the Depth Map / Z-Pass). [^src-b-step3]: SRC-B, ibid., Step 3 (Baking and Exporting the Normal Map). [^src-b-step5]: SRC-B, ibid., Step 5 (Texture Projection Mapping in Blender).
Source documents (paths as supplied to the isometric-ops build):
SRC-A = compass_artifact_wf-75a0e032-3465-48c7-84ea-e104bae213c2_text_markdown.md
(Blender-for-isometric-rendering catalog); SRC-B = "Engineering and Aesthetic Standards
for Isometric Design" (3D-to-2D ControlNet Rendering Workflow section, steps 1–3 and 5).