Files
security-game/docs/adr/0013-blender-authored-npc-characters.md
zwitschi d872034388
Full build / Full desktop build + optional release (push) Successful in 3m43s
Full build / Import + headless tests (push) Successful in 29s
Bake library AO and lift environment textures
Phase 4 of the v0.6.0 graphics plan:

- tools/bake_ao.py bakes 256² AO maps for every Blender character/gear/prop
  (Cycles, existing UVs) next to each GLB; Materials.apply_ao() wires the red
  channel into character slots, gear, and props. Compatibility has no SSAO.
- Regenerated brick/concrete/asphalt at 1024² with OpenRouter, derived tangent
  normal maps via tools/make_normal_maps.py, and wired them through
  SurfaceMaterials.textured() without changing UV scales.
- Evaluated Forward+: boots on desktop Vulkan, but the renderer is project-wide
  and Web must stay GL Compatibility; v0.6.0 keeps Compatibility (decision in
  MEMORY/plan). A desktop-only override is deferred.
- Recaptured screenshots; compare 11 good / 0 bad. Fixed marquee text alignment.
- Tests: baked AO maps exist for all 46 assets; prop/character AO material
  checks; headless 222/222, Python 69/69, all smokes pass.
- Version 0.6.0; ADR-0013 accepted; README/architecture/blender-assets/plan and
  TODO/DONE updated.
2026-11-01 15:07:27 +01:00

4.4 KiB

ADR-0013: Blender-authored NPC characters and baked materials

  • Status: Accepted (v0.6.0)
  • Date: 2026-11-01

Context

NPCs are the procedural capsule-and-box rig from ADR-0010: articulated Node3D limbs, flat StandardMaterial3D colors, and sphere hair/eyes. ADR-0010 accepted this as a placeholder and recorded that a later skinned-mesh pass would replace the rig while keeping the Head contract. The props already moved to generated .glb assets (ADR-0012), but people remain the lowest-fidelity part of the scene: no UVs, no baked normal or ambient-occlusion maps, and silhouettes built from capsules.

The project renders with the GL Compatibility backend (no SSAO/SSIL), so visual quality has to come from the assets and baked maps rather than a renderer change. CI must stay offline: the Blender step is a developer tool, not a build dependency.

Decision

  1. Author the NPC body in Blender and export a skinned, UV-mapped base mesh as assets/models/generated/npc_base.glb, driven by a new tools/npc_pipeline.py (build / audit) invoked through the Blender flatpak in headless mode. The .blend source is committed under assets/models/npc/.
  2. Bake material maps in Blender — neutral albedo, normal, and a packed ORM (occlusion/roughness/metallic) map per material slot (skin, cloth, pants, accessory, hair). Ambient occlusion is baked into the albedo or the ORM R channel, since Compatibility has no screen-space AO.
  3. Keep a procedural fallback. NpcAvatar loads the imported PackedScene when it exists and falls back to the current _build_body() rig otherwise, mirroring WorldBuilder._add_prop_model(). Committed assets mean CI never needs Blender, and the headless suite runs against the fallback when the import is absent.
  4. Preserve the Head contract and seed determinism. The armature bones are named after the current nodes (Head, HipL/R, KneeL/R, ShoulderL/R, ElbowL/R, HandL/R); outfit_for and friends stay pure and drive the material slots. A thin _set_limb_rotation() wrapper lets _animate() / _idle_layer() target either Node3D pivots or Skeleton3D bones.
  5. AI tooling supplies art direction only. tools/asset_pipeline.py generates seamless clothing albedos, tools/model_pipeline.py's palette helpers (dominant_colors, assign_roles) derive per-role palettes from concept sheets, and tools/screen_designs.py judge closes the loop. All AI outputs are committed; generation is dev-time and never a runtime or CI dependency.

Consequences

Positive

  • NPCs gain real geometry, UVs, and baked normal/AO/roughness, so they read as people at cinematic distance without a renderer switch.
  • Skinned meshes collapse ~33 draw calls per avatar into one, which can offset the skinning cost at MAX_LIVE_NPCS = 16.
  • The asset contract stays behind the same .glb + audit discipline that props already use; the procedural fallback keeps offline CI deterministic.
  • AO baked into the maps compensates for the absence of SSAO in Compatibility.

Negative

  • A new toolchain (Blender flatpak + npc_pipeline.py) and its path/colour-space gotchas enter the repo; contributors must run audit after changing the .blend.
  • Skinning shifts cost from draw calls to CPU; the 16-NPC frame budget must be re-measured before raising the cap.
  • Two avatar backends (skinned + fallback) must stay behaviourally identical, or tests that assume the fallback will silently pass against one and not the other.

Alternatives considered

  • Keep the procedural rig and only add textures. Rejected: textures on capsules do not fix the silhouette; the rig is the fidelity ceiling.
  • Use Mixamo / a downloaded skinned character. Rejected: licensing and retargeting overhead exceed the value; a low-poly base authored in-house keeps the art direction and the Head/bone contract under our control.
  • Switch the renderer to Forward+ for SSAO/SSIL. Rejected as a prerequisite; it would break the Web export assumptions. A desktop-only Forward+ preset remains a follow-up, not a dependency of this decision.
  • A 3D-model (image-to-mesh) API instead of Blender. Rejected for characters; output topology is not controllable enough to satisfy the bone/UV contract. It remains a possible prop-only follow-up (ADR-0012).