Round 22 workshop output: character-visuals-spec.md (color mesh regions, LOD tiers, layered composition) and compositor-api-spec.md (Node3D architecture, CharacterColors data structure, set_color API). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
15 KiB
title, description, type, status, sprint, ticket, created
| title | description | type | status | sprint | ticket | created |
|---|---|---|---|---|---|---|
| Character Visuals Specification | Complete spec for character rendering: layer stack, color regions, body/face/hair types, direction system, outline, LOD | spec | active | 28 | 684 | 2026-03-17 |
Character Visuals Specification
Produced by: Sprint 28 Character Visuals Workshop (ticket #684)
Date: 2026-03-17
Art direction reference: Invisible Inc — clean, adult, high contrast. Not a candy store.
Downstream: docs/design/compositor-api-spec.md, tickets #686–#692 (visual team), #693 (compositor)
1. Camera and Display Context
All art must be authored and tested at the confirmed gameplay camera angle:
- Camera tilt: 30° from vertical (60° from horizontal) — orthographic
- Map rotation: 45° (diamond grid)
- Projection: Orthographic — no perspective foreshortening
- Reference games: Hades, Divinity: Original Sin (not Diablo — 45° is NOT the default)
Characters are rendered as live 3D models in the Godot scene (D-149). The camera is a real Camera3D — perspective is not faked in art. Artwork is authored as 3D assets, not pre-rendered sprites.
Art direction notes at this angle:
- Front faces are significantly visible at 30° — face and clothing detail matters more than at 45°
- The top of characters' heads is less prominent — hair silhouette reads from the side, not top-down
- Wall depth and character shadow are both visible — spatial grounding is strong
- Characters must read clearly at tile scale (~32–64px display height)
2. Rendering Architecture Overview
Characters use a CharacterCompositor (Node3D subtree). See docs/design/compositor-api-spec.md for the full API.
- Not sprites: No pre-rendered sprite sheets. All composition is runtime in Godot.
- Outline: Inverted hull method — uniform dark gray (#1a1a1a) for all characters. Not a relationship indicator (D-154). See §10.
- Relationship data: Lives on minimap, name bubbles, and insert/perception mode overlay (D-033 amended). Not on character body in normal gameplay.
- Player character: Visually identical to NPCs — same models, same variety (D-153).
3. Direction System
Server (authoritative): 8 facing directions — N, NE, E, SE, S, SW, W, NW.
Client Sprint 28: 4 visual groups. The 3D model rotates to the true 8-direction angle; only the visual mesh group snaps to 4:
| Visual Group | Covers Server Facings | Mirror |
|---|---|---|
| North | N, NW | — |
| East | NE, E | — (source mesh) |
| South | SE, S | — |
| West | SW, W | Mirror of East |
The exact mapping of server facings to visual groups depends on camera orientation relative to the diamond grid — Tyre to confirm in compositor implementation. West is a horizontal mirror of East; no additional art required.
Character editor: Four cardinal direction buttons. Button order: S → E → N → W. Default on open: South (face-forward at 30° camera — maximum cosmetic utility, highest clothing/face visibility). No free-spin (D-155). The four buttons correspond directly to the four visual groups.
Critical distinction — 8 facings vs 4 visual groups:
- 8 server-side facings = perception system input. Used by fog-of-war, vision cone direction, and all simulation logic. The server always tracks the true 8-direction facing.
- 4 visual groups = character rendering output. What the player actually sees. Characters snap to the nearest cardinal visual group. Diagonal facings do not exist as visible character states — a character facing NE renders as the East visual group.
- Smooth rotation interpolation between directions is applied at the model root level (ModelRoot rotation), so the body physically points the correct way even while the visual mesh group is a snap.
Post-Sprint 28: Diagonal mesh variants (NE, NW, SE, SW as separate visual groups) can be added without protocol changes — the 8 server facings are already tracked.
4. Layer Stack
Layers are rendered bottom-to-top. Each layer is a separate mesh in the CharacterCompositor subtree.
| # | Layer | Notes |
|---|---|---|
| 1 | Body base | Body type variant (slim / average / stocky). Skin tone mesh regions. |
| 2 | Leg clothing | Trousers, shorts, skirt. Occludes body base from waist down. |
| 3 | Footwear | Shoes, boots. Occludes leg clothing at ankle/foot. |
| 4 | Torso clothing (back) | Rear of jacket/shirt. Behind arms. |
| 5 | Torso clothing (front) | Front of jacket/shirt. Over arms. |
| 6 | Accessories | Belt, bag, holster, jewelry. Multiple items possible. |
| 7 | Head / face | Face mesh with skin regions. Neck connects to body base. |
| 8 | Hair (back) | Hair behind the head/shoulders. |
| 9 | Hair (front) | Hair in front of face / over forehead. |
| — | Outline | Inverted hull pass (not a layer — shader effect on all meshes). |
| 10 | Scar overlays | Placed on exposed skin regions. Additive blend on skin. |
| 11 | Tattoo overlays | Placed on exposed skin regions. Multiply blend on skin. |
| 12 | Injury overlays | Applied over clothing (tears, stains) and face (bruises, cuts). |
| 13 | Expression overlay | Replaces/modifies face mesh for expression state. |
Occlusion rule: Each layer should include an occlusion mesh (invisible, write-only depth) matching the body shape, so underlying layers do not bleed through clothing at edges.
5. Color Mesh Regions
Each layer exposes named UV-mapped color regions that can be overridden by the compositor. Regions that are not overridden use the layer's authored default color.
Body base
| Region | Description |
|---|---|
skin_primary |
Face, hands, neck, visible forearms |
skin_secondary |
Inner arm, sole of hand — subtle variation (may auto-derive from skin_primary) |
Hair
| Region | Description |
|---|---|
hair_primary |
Main hair color |
hair_highlight |
Secondary sheen / highlight (may auto-derive from hair_primary as lighter tint) |
Clothing (all clothing items)
| Region | Description |
|---|---|
cloth_primary |
Dominant fabric — the main color the player picks |
cloth_secondary |
Trim, seams, buttons, collar, cuffs |
cloth_accent |
Optional third zone — logo patch, lining, contrast panel. May be absent on simpler items. |
Accessories
| Region | Description |
|---|---|
accessory_primary |
Main material color |
accessory_secondary |
Optional secondary (e.g. stitching on a bag, gem on jewelry) |
Design note: Not all clothing recolors uniformly. A jacket's body is cloth_primary; its piping is cloth_secondary. Per-item region definition is the artist's responsibility. Invisible Inc reference: colored bodysuits with trim lines — crisp and readable at small scale.
6. Body Types (Sprint 28: 2–3)
Sprint 28 ships with 2–3 body type variants. Minimum viable: slim + average. Stocky as stretch if time allows.
| Type | Visual description | Notes |
|---|---|---|
| Slim | Narrow shoulders, slight frame, elongated limbs | |
| Average | Medium proportions, balanced silhouette | |
| Stocky | Broad shoulders, thicker torso and limbs | Stretch goal, Sprint 28 |
Constraints:
- All body types share the same layer interface (
skin_primary,skin_secondaryregion names) - Clothing mesh layers must fit all body types without visible clipping or floating
- Clothing artists author per–body-type mesh variants OR the compositor scales the clothing mesh to the body type (implementation TBD — Tyre to specify in compositor-api-spec)
- Silhouette must differ meaningfully at 32px display height — not just subtle (otherwise pointless at tile scale)
Combination validation: Sprint 28 goal is to confirm that switching body types with any clothing combination produces no visual artifacts. This is the primary QA target.
7. Face Types
Face is a compositable region on the Head/Face layer.
Range: Broad variety. Asymmetric features valued — these are adults, not game-industry default faces. Invisible Inc reference for readability at small scale.
Mesh regions on face:
skin_primary— face, forehead, cheeks, chinskin_lips— lips (may auto-derive fromskin_primary)
Compositable elements within the face region (each a separate overlay or mesh swap):
- Eye shape (mesh swap — left/right eye as separate swappable meshes or UV regions)
- Nose shape (mesh swap)
- Mouth/lip shape (mesh swap)
- Eyebrow shape (mesh swap or UV overlay)
Expression states (minimum Sprint 28):
| State | Description | Trigger |
|---|---|---|
neutral |
Default resting face | No active emotional state |
alert |
Eyes slightly widened, posture tense | Danger or person-of-interest nearby |
stressed |
Furrowed brow, tight mouth | High stress value from simulation |
distressed |
Open distress, visible tension | Extreme stress or injury |
Expression overlay replaces or modifies the base face mesh. The expression does not affect the body posture in Sprint 28.
8. Hair Style Spec
Hair silhouette must read legibly at tile scale. Overly detailed styles will read as noise at small size — err on the side of clean, strong silhouettes.
Minimum Sprint 28 styles:
- Short A (close-cropped)
- Short B (textured/spiky)
- Medium A (smooth, parted)
- Medium B (wavy/voluminous)
- Long A (flowing, shoulder-length or longer)
- Bald / shaved
Color regions: hair_primary, hair_highlight (see §5).
Layer split: Every style must have a Hair (back) and Hair (front) mesh — these straddle the head/face layer so face is visible between them at certain angles.
9. Scar and Tattoo Overlays
Overlays are placed on exposed skin regions only (face, hands, forearms — not on covered areas).
Placement zones:
- Face zone
- Left arm / hand zone
- Right arm / hand zone
- Torso zone (only visible when wearing open/sleeveless clothing)
Blending:
- Tattoos: Multiply blend on
skin_primaryregion — color mixes with skin tone - Scars: Additive blend on
skin_primary— lighter, raised-texture appearance
Optional: Not all characters have scars or tattoos. Zero overlays is valid and common.
10. Injury Overlays
Injury state represents physical damage visible on the character's appearance.
States (minimum Sprint 28):
| State | Clothing effect | Skin effect |
|---|---|---|
undamaged |
None | None |
light_injury |
Minor tear/dirt on torso clothing | Minor bruising on face/exposed skin |
heavy_injury |
Visible tears, blood staining | Heavy bruising, cuts visible |
Injury overlays are applied on top of clothing (not inside it) and on top of exposed skin. Injury tint auto-derives from the layer's base color (darker, desaturated).
11. Outline Specification
- Method: Inverted hull — GPU vertex extrusion, back-face-only render pass (D-150)
- Color:
#1e1e24(very dark blue-grey) for all characters, always — not pure black - Width: 1px at gameplay zoom (consistent regardless of zoom level — use shader-based fixed screen-space width)
- Semantic content: None — the outline does not encode relationship, status, or any game state
- LOD behavior: At Tier 2 (billboard impostor), the outline is baked into the impostor sprite. The inverted hull is disabled at Tier 2 — no separate draw call.
12. LOD Strategy
Three tiers, degraded based on GPU frame budget (not distance, not fixed count) (D-152):
| Tier | Label | Description |
|---|---|---|
| 0 | Full | All compositor layers active. Full 3D model. Inverted hull outline. |
| 1 | Simplified mesh | Reduced-poly model. Clothing layers merged into combined mesh. Outline active. |
| 2 | Billboard impostor | Flat sprite impostor with outline baked in. One draw call. |
Degradation order: Characters furthest from the player's character degrade first. Nearest characters stay at Tier 0 longest. Threshold is a frame budget check — when the GPU is under pressure, demote the outermost tier-eligible characters.
LOD trigger: Proactive on projected character count, not reactive on frame drop. The LOD manager anticipates load based on how many characters will be in the scene this frame and pre-demotes before GPU pressure occurs. Reactive triggering (demoting after a frame drops) produces visible hitches. Proactive triggering is invisible.
Billboard LOD requirement (Tier 2 preservation): At billboard tier, each impostor sprite must visually encode two signals per character:
- Body size tier (slim / average / stocky) — silhouette must differ meaningfully at billboard scale
- Dominant clothing color — the primary
cloth_primaryof the most visible clothing item must be readable
These signals must not be optimized away during impostor baking. The impostor is not a generic character silhouette — it is a per-character snapshot preserving identity.
Pause behavior: When the game is paused, the render budget is fully freed. All characters restore to Tier 0. The pause button is a deliberate affordance — player can inspect any character at full detail.
Chaos feel: In a very large crowd (400+ characters), peripheral degradation is intentional and cognitively appropriate — real crowds have visual noise at the edges. The player naturally focuses center-screen where Tier 0 characters are.
13. Art Direction Reference Summary
| Principle | Notes |
|---|---|
| Reference: Invisible Inc | Clean, sleek, adult. High contrast, strong silhouettes. Not Rimworld-chunky, not anime. |
| Scale readability | Every design decision must be validated at 32–64px. Detail that doesn't read at tile scale should be removed. |
| Uniform outlines | Dark gray for everyone. No auras, no glows, no player distinction. |
| Per-item color variety | Clothing colors are per-item, not per-faction, not per-team. Life-sim, not RTS. |
| Adult faces | Asymmetric features, variety across ages and types. Not default RPG faces. |
14. Open Questions / Flags
| Item | Status | Notes |
|---|---|---|
| Exact N/E/S/W → server facing mapping | Open | Tyre to confirm in compositor-api-spec based on camera orientation |
| Stocky body type (Sprint 28) | Conditional | Stretch goal — drop if time pressure |
| Clothing mesh per body type vs. scale | Open | Tyre to specify — separate mesh variants or compositor-level scaling |
| Torso-zone tattoo visibility | Open | Depends on clothing occlusion rules — artist + Tyre to confirm |
| Expression overlay = mesh swap or morph target | Open | Tyre to decide in compositor-api-spec |
| D-146 interaction with 3D rendering | Flag | D-146 says "tile-scale sprite with heavy zoom" for editor preview; D-149 says live 3D rendering. The editor preview may show the live 3D model at zoom scale — compatible if interpreted as "tile-scale representation" not "2D sprite". Do not reopen D-146 without explicit direction from Jeroen. |