From 8217ebc0fb3c0f3464d22b333164d87d759f4be9 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Mon, 23 Feb 2026 20:01:43 +0100 Subject: [PATCH 1/4] =?UTF-8?q?feat(assets):=203D=20sprite=20render=20pipe?= =?UTF-8?q?line=20=E2=80=94=20camera,=20lighting,=20docs=20(#541)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update Camera3D to exact D-019 angle (-72.5° from horizontal). Replace single DirectionalLight with three-point studio rig (key 1.0, fill 0.4, rim 0.3) for clean silhouettes without baked shadows. Add generic NPC capsule model (24×32px footprint per D-044). Document full pipeline spec in renderer/README.md. Co-Authored-By: Claude Opus 4.6 --- renderer/README.md | 111 +++++++++++++++++++++++++++++++ renderer/models/npc_generic.tscn | 14 ++++ renderer/render_scene.tscn | 18 ++++- 3 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 renderer/README.md create mode 100644 renderer/models/npc_generic.tscn diff --git a/renderer/README.md b/renderer/README.md new file mode 100644 index 000000000..8002d9bfa --- /dev/null +++ b/renderer/README.md @@ -0,0 +1,111 @@ +# Settled Reach — Sprite Render Pipeline + +Offline Godot 4 renderer for the 3D-to-2D sprite pipeline. Produces entity and structural sprites at three resolutions from a fixed camera angle, with outlines applied at working resolution. + +## Camera Specification (D-019) + +- **Angle**: -72.5° from horizontal (= 17.5° from vertical, the midpoint of the 15-20° from vertical range) +- **Projection**: Orthographic (`size = 1.4`) +- **Position**: `(0, 3, 1)` — above and slightly in front of the model origin +- **Godot Transform3D**: `Transform3D(1, 0, 0, 0, 0.30071, 0.95372, 0, -0.95372, 0.30071, 0, 3, 1)` + +This is "the angle" per D-019 amendment (2026-02-12). All entity, object, and wall sprites for v0.1 are rendered at this angle. The Godot gameplay camera is purely orthographic — this tilt is an art convention expressed through the 3D render. + +## Lighting Rig + +Three-point studio rig. All lights have `shadow_enabled = false` — sprites are shape templates, no baked shadows or directional lighting. The Godot runtime PointLight2D pipeline provides all scene lighting at runtime (D-043). + +| Light | Energy | Direction | Purpose | +|-------|--------|-----------|---------| +| KeyLight | 1.0 | Matches camera angle (-72.5° from horizontal) | Main illumination; ensures front face is lit as the camera sees it | +| FillLight | 0.4 | 45° from right side (-45° pitch, +90° yaw) | Fills shadow opposite the key; no deep-black patches on right face | +| RimLight | 0.3 | 45° from behind (-45° pitch, +180° yaw) | Back-edge accent; defines silhouette boundary for outline processing | + +**Rationale**: Even 3-light coverage ensures no black patches on a convex mesh, keeping surface colors flat and readable for the outline processing step. + +## Resolution Chain (D-043, D-044) + +``` +1024×1024 — source PNG, full fidelity for outline processing + ↓ bilinear resize +256×256 — working PNG, outline applied at 4-8px, color #333340 + ↓ bilinear resize +64×64 — runtime PNG, deployed to client/assets/sprites/ +``` + +Outline implementation: alpha-mask dilation + flat color fill. Outline width in `render_export.gd`: `outline_width_px = 4` (at 256px = 1px effective at 64px). + +## Entity Footprint Spec (D-044, D-066) + +- Entity sprites occupy a **24×32px footprint** within the 64×64 visual tile +- Entities render across a **2×2 sim tile sprite footprint** (D-066) +- Visual tile = 64×64px at runtime; sim tile = 32×32px (0.5m) +- **NPC model scale**: `CapsuleMesh(radius=0.25, height=0.62)` at camera size 1.4 produces ~24×32px apparent size in the 64px output tile + +## Usage + +### Command Line + +```bash +# From the renderer/ directory +godot --path . --headless --quit-after 1200 res://render_scene.tscn -- +``` + +Or use the `/sprite-gen` skill which wraps this command and handles output placement. + +### Adding a New Model + +1. Create `renderer/models/.tscn` — root node is a `MeshInstance3D` (or `Node3D` with children) +2. Center the mesh at the origin +3. Use a flat `StandardMaterial3D` (no baked shadows — just albedo color + roughness) +4. Run: `/sprite-gen ` +5. Output: 12 PNGs in `renderer/output/` (4 directions × 3 resolutions) +6. Runtime sprites: copy 64px variants to `client/assets/sprites/` + +### Material Guidelines + +- **Entity sprites**: neutral flat material (albedo Color(0.5, 0.5, 0.5)). D-033 relationship color tinting applied at runtime. +- **Structural sprites**: use zone-appropriate texture (`textures/wall_institutional_era1.png`, etc.) +- No specularity: `metallic = 0.0`, `roughness = 0.9` +- No emission, no normal maps — shape is the signal + +## Models + +| Model | File | Type | Notes | +|-------|------|------|-------| +| `wall_structural` | `models/wall_structural.tscn` | Structure | 1.0×0.8×0.2 box, institutional era-1 texture | +| `wall_bar_green` | `models/wall_bar_green.tscn` | Structure | 1.0×0.8×0.2 box, green panel texture | +| `npc_generic` | `models/npc_generic.tscn` | Entity | Capsule silhouette, neutral grey, 24×32px footprint | + +## Output Naming Convention + +``` +__.png + +wall_structural_north_64.png +npc_generic_south_256.png +``` + +Directions: `north`, `east`, `south`, `west` (model rotated 0°, 90°, 180°, 270° around Y axis). + +## Runtime Sprite Path + +64px sprites are deployed to: `client/assets/sprites/` + +Naming convention at the client side is `_.png` (64px only — the 1024 and 256 variants stay in `renderer/output/` as pipeline intermediates). + +## Design Constraints + +- **No baked lighting**: sprites are shape templates. Lighting is applied at runtime by Godot's PointLight2D per D-046. +- **No baked shadows**: shadow direction would conflict with runtime point lights at arbitrary positions. +- **No baked mood**: flat materials, three-point neutral rig. Zone atmosphere comes from runtime CanvasModulate. +- **Outline applied at 256px, not 64px**: ensures outline is crisp after bilinear downscale. +- **Transparent background**: `SubViewport.transparent_bg = true` — sprites are PNG with alpha, composited at runtime. + +## Cross-References + +- D-019: Camera angle spec and amendment +- D-043: Art direction — "functional warmth," resolution chain, outline color +- D-044: Visual hierarchy, entity footprint spec +- D-049: Z-level rendering stack (sprites land on layers 2-3) +- D-066: Dual-scale grid, 2×2 sim tile entity footprint diff --git a/renderer/models/npc_generic.tscn b/renderer/models/npc_generic.tscn new file mode 100644 index 000000000..46217a605 --- /dev/null +++ b/renderer/models/npc_generic.tscn @@ -0,0 +1,14 @@ +[gd_scene load_steps=3 format=3 uid="uid://npcgeneric001"] + +[sub_resource type="CapsuleMesh" id="CapsuleMesh_npc"] +radius = 0.25 +height = 0.62 + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_npc"] +albedo_color = Color(0.5, 0.5, 0.5, 1.0) +metallic = 0.0 +roughness = 0.9 + +[node name="NpcGeneric" type="MeshInstance3D"] +mesh = SubResource("CapsuleMesh_npc") +surface_material_override/0 = SubResource("StandardMaterial3D_npc") diff --git a/renderer/render_scene.tscn b/renderer/render_scene.tscn index de53c654d..362553984 100644 --- a/renderer/render_scene.tscn +++ b/renderer/render_scene.tscn @@ -12,16 +12,28 @@ render_target_update_mode = 1 own_world_3d = true [node name="Camera3D" type="Camera3D" parent="SubViewport"] -transform = Transform3D(1, 0, 0, 0, 0.309017, 0.951057, 0, -0.951057, 0.309017, 0, 3, 1) +transform = Transform3D(1, 0, 0, 0, 0.30071, 0.95372, 0, -0.95372, 0.30071, 0, 3, 1) projection = 1 size = 1.4 near = 0.01 far = 10.0 -[node name="DirectionalLight3D" type="DirectionalLight3D" parent="SubViewport"] -transform = Transform3D(1, 0, 0, 0, 0.309017, 0.951057, 0, -0.951057, 0.309017, 0, 3, 1) +[node name="KeyLight" type="DirectionalLight3D" parent="SubViewport"] +transform = Transform3D(1, 0, 0, 0, 0.30071, 0.95372, 0, -0.95372, 0.30071, 0, 0, 0) light_color = Color(1, 1, 1, 1) light_energy = 1.0 shadow_enabled = false +[node name="FillLight" type="DirectionalLight3D" parent="SubViewport"] +transform = Transform3D(0, -0.70711, 0.70711, 0, 0.70711, 0.70711, -1, 0, 0, 0, 0, 0) +light_color = Color(1, 1, 1, 1) +light_energy = 0.4 +shadow_enabled = false + +[node name="RimLight" type="DirectionalLight3D" parent="SubViewport"] +transform = Transform3D(-1, 0, 0, 0, 0.70711, 0.70711, 0, 0.70711, -0.70711, 0, 0, 0) +light_color = Color(1, 1, 1, 1) +light_energy = 0.3 +shadow_enabled = false + [node name="ModelRoot" type="Node3D" parent="SubViewport"] -- 2.54.0 From 885ad2168835c4db72e20718a5a1798580b10c14 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Mon, 23 Feb 2026 20:01:49 +0100 Subject: [PATCH 2/4] feat(assets): add test sprites for NPC and wall at D-019 angle (#541) Runtime 64px sprites rendered through the new pipeline: generic NPC (4 directions) and structural wall (4 directions). These unblock client ticket #540 for sprite camera angle integration. Co-Authored-By: Claude Opus 4.6 --- client/assets/sprites/npc_generic_east_64.png | Bin 0 -> 1098 bytes client/assets/sprites/npc_generic_north_64.png | Bin 0 -> 1098 bytes client/assets/sprites/npc_generic_south_64.png | Bin 0 -> 1098 bytes client/assets/sprites/npc_generic_west_64.png | Bin 0 -> 1098 bytes .../assets/sprites/wall_structural_east_64.png | Bin 0 -> 1060 bytes .../assets/sprites/wall_structural_north_64.png | Bin 0 -> 1235 bytes .../assets/sprites/wall_structural_south_64.png | Bin 0 -> 1256 bytes .../assets/sprites/wall_structural_west_64.png | Bin 0 -> 1047 bytes 8 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 client/assets/sprites/npc_generic_east_64.png create mode 100644 client/assets/sprites/npc_generic_north_64.png create mode 100644 client/assets/sprites/npc_generic_south_64.png create mode 100644 client/assets/sprites/npc_generic_west_64.png create mode 100644 client/assets/sprites/wall_structural_east_64.png create mode 100644 client/assets/sprites/wall_structural_north_64.png create mode 100644 client/assets/sprites/wall_structural_south_64.png create mode 100644 client/assets/sprites/wall_structural_west_64.png diff --git a/client/assets/sprites/npc_generic_east_64.png b/client/assets/sprites/npc_generic_east_64.png new file mode 100644 index 0000000000000000000000000000000000000000..71693ef2275861258acc284497743a33d324ca1f GIT binary patch literal 1098 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 literal 0 HcmV?d00001 diff --git a/client/assets/sprites/wall_structural_south_64.png b/client/assets/sprites/wall_structural_south_64.png new file mode 100644 index 0000000000000000000000000000000000000000..75d93b9f7fcd92e727cbd80409bac6d9483aa555 GIT binary patch literal 1256 zcmVP)Px(qDe$SRCt{2m`#!tHxPxtr{+c+z(--_jZfjru)w$Q0k{LJVOZH-)1~J{W=iUT zpbMhyaD??BV!ErOROZWHHEe8bY;0_7Y;0_7Y;0_7Y;0`&ub_JiUcdgy|Ar#}6yCr8 z{hKzvIwR!GFFzwhcSkh9*hUhz0fPk!uDXGKV1(lfsO4I6Z#?dHdcpZWWH$-NV#)&! zG2qh0#9PHnrbmVN?d`j}ww)rN_nrzE(~(V2g(c;4gP^5ap;xSv!M#H%+&e~TypZaB z5Gp#BCc#OP1U|@ff-|{;gdH+b$Tj_pY3B&Ie!dd2r?Em{=sretU~??{>?0$l`0((! z<=p4kOG~fD>P8Dg1@t&Eylsx7(=_6S77}4N#>jWG`#b^eJ&es@G(bNS61$TPAstmqt(B6HCfX% zn6ISF?O*_Gu3o&>naYwzIb?e~3@IX|GOv}c4;liv&y@61w2GA`Ju~NNqsl`NHK_DbBU*Ho)-t49%SxW`i#(QY=7V%P#aNc0 z2*i`)+bIG*e)_aTK^bvdutLA`b}_ZsQH~WeB8HY#T<5pBk()JIw$(0=s+zK;DyMgG zihy9<)q(PGFB*`;h?m+>WxCd`Wjfner2!gIHH`ZzIbO(Ni58Euu&|Gl=AR^h4A-HE z%8r~1W?qc^Ufnd(WRNh`^NrDK-cFL%f?Kj(BeWX0XO~%ZejouhIpD88SVrD@x*oca zGD-%0?qdzt#evoT>60g!!A&K4e)iC-bHl(x38<>S7!@AP;k5hZhK%yMs+7s0{pbR@ z*SVqAuNf2amZZOVyrju22;XD>*$4>AV|sO7P+qR_i}g#3Vv=Y~Q_PQJZkO=UCrwg% ze9d`fB;-)VjF@rKG7it4_-(Xv1mp+JJP*5;P|m-~3g3vSOy!--*#@}xAgLm!rp0Qj zqmkr4N4*iu>5qBeVEsIFdEgunhAXcCi}zi*=b~kLWGUX#^cSvyWqP-+#YJR%?&E#P z(`cir57Z4so=kzmbRUb+^};!$$B4VsM6~3vix522bw)|tm6jGp-VCWJ zamp*>>c^OP(U6_>FCtEAJoNW~QRK894)sHXl=FVw=jX(e1CWq1Haw%DrpH#v(oCy+ zL$Y@fc#M2`0a6jAp-)Yn9v7s`;|TckkH41w4ok@$YibHyr@?a4*DCU8eM>ms%GC|w zH9Zy=`a;@|;VuDBPal4Ldir7c8yg!N8yg!N8yg!N8yg!N8yg!N8yg${9sCWUR#vt< Suk@$@0000Px&(Md!>RCt{2o4t-CRSbo{<6?P--2_s8Uyd`0BpR~EGyUvGKz7R96zsAS9wFb038Wvg~gzZ8o+=R_%8mz zs9Xb+{oR}ldw>xpL?gjc9#qzuy$%lufzntwZ>_w*CTVQYtP#f3prpb+38$ew0WUAO za0p1^P!tea5W-GuZy=n0C&M6Ul2TZ4;y`Jx4Th64%mdUw7yY?XpIyeeWsbAK@hngz zsD36%ZZOPx;>re)gb=ZS(4YuFXr;ibX91*5g`=&Iodmgh7AV$NUSKyIbo!GQJ)V_e z5Dc;vm7+O;upm1dOdDa=13Jy+t_VBz75tivcu6p1jd;Q*;$R-4uge|5sKxG>-$rL{PJs~e;%6QL@0R+Qgk zK{ZE?Bf3#KC&R_i0)YN)xbpn~=cvpv-VlXHF+s`$)Y%{{TwT#? z?+5Ie00LE0JbO^WKj`qBj5kFW|7Yz9QRA@_7ccGsvLI?bPCY4JZ17Bzb27gm5$DUk zIXO@*NJ-=F-`AOyt)}|$XiVt7z@d*=rHJ~Tv{@L1HEtYZ`7y*f82qeSSZ zAHL_$KYriaAxJ2Kh;vr>%WuC{*~tO^A9-CIu0(pP7_dDlCXNx*Z_m2t?eq z$hdev;6%T(DH3Nn{D^9B*aOyEuR0)Nk)cxh5$p1!28albICdGlut41&V2s~m3b<+CqnjMI0O;+)Ly2_wT( z+8Msx#y3QUJ)r+Hq%puYc`N0y$*YIJ&rk38`RU!{k Date: Mon, 23 Feb 2026 20:02:04 +0100 Subject: [PATCH 3/4] chore(meta): update changelog Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9a6fc688..589dd587c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ Format based on [Keep a Changelog](https://keepachangelog.com/). ## [Unreleased] +### Added +- 3D sprite render pipeline — Camera3D at D-019 angle (-72.5° from horizontal), three-point studio lighting rig, orthographic projection, resolution chain 1024→256→64 +- Generic NPC capsule model (24×32px footprint per D-044) and structural wall model for pipeline validation +- Test sprites: 8 runtime 64px sprites (NPC + wall × 4 directions) deployed to client/assets/sprites/ +- Pipeline documentation (renderer/README.md) — camera spec, lighting rig, resolution chain, model authoring guide + ## [v0.1.15] — 2026-02-23 ### Added -- 2.54.0 From 19f81d4887f69dad053708d551e37049c9cc9143 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Mon, 23 Feb 2026 20:17:47 +0100 Subject: [PATCH 4/4] =?UTF-8?q?fix(assets):=20PR=20#57=20review=20?= =?UTF-8?q?=E2=80=94=20docs,=20uid,=20citation=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix client sprite naming in README (files are _64.png, not .png) - Clarify --path working directory (renderer/ from repo root) - Remove /sprite-gen reference (skill not in branch yet) - Add D-043 citation on outline color in render_export.gd - Add uid to wall_bar_green.tscn for reproducible imports - Note wall_bar_green has no rendered sprites yet - Note npc_generic capsule symmetry is by design (D-044) - Add outline + D-033 tint compatibility guidance Co-Authored-By: Claude Opus 4.6 --- renderer/README.md | 16 ++++++++-------- renderer/models/wall_bar_green.tscn | 2 +- renderer/render_export.gd | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/renderer/README.md b/renderer/README.md index 8002d9bfa..1725f24ab 100644 --- a/renderer/README.md +++ b/renderer/README.md @@ -47,24 +47,24 @@ Outline implementation: alpha-mask dilation + flat color fill. Outline width in ### Command Line ```bash -# From the renderer/ directory -godot --path . --headless --quit-after 1200 res://render_scene.tscn -- +# From the repository root +godot --path renderer/ --headless --quit-after 1200 res://render_scene.tscn -- ``` -Or use the `/sprite-gen` skill which wraps this command and handles output placement. +The `/sprite-gen` skill wraps this command and handles output placement. ### Adding a New Model 1. Create `renderer/models/.tscn` — root node is a `MeshInstance3D` (or `Node3D` with children) 2. Center the mesh at the origin 3. Use a flat `StandardMaterial3D` (no baked shadows — just albedo color + roughness) -4. Run: `/sprite-gen ` +4. Run: `godot --path renderer/ --headless --quit-after 1200 res://render_scene.tscn -- ` 5. Output: 12 PNGs in `renderer/output/` (4 directions × 3 resolutions) 6. Runtime sprites: copy 64px variants to `client/assets/sprites/` ### Material Guidelines -- **Entity sprites**: neutral flat material (albedo Color(0.5, 0.5, 0.5)). D-033 relationship color tinting applied at runtime. +- **Entity sprites**: neutral flat material (albedo Color(0.5, 0.5, 0.5)). D-033 relationship color tinting applied at runtime by the client's entity renderer. Outline color (#333340 per D-043) is compatible with all D-033 tint colors — the dark blue-grey outline remains visible against teal, green, amber, and red entity tints. - **Structural sprites**: use zone-appropriate texture (`textures/wall_institutional_era1.png`, etc.) - No specularity: `metallic = 0.0`, `roughness = 0.9` - No emission, no normal maps — shape is the signal @@ -74,8 +74,8 @@ Or use the `/sprite-gen` skill which wraps this command and handles output place | Model | File | Type | Notes | |-------|------|------|-------| | `wall_structural` | `models/wall_structural.tscn` | Structure | 1.0×0.8×0.2 box, institutional era-1 texture | -| `wall_bar_green` | `models/wall_bar_green.tscn` | Structure | 1.0×0.8×0.2 box, green panel texture | -| `npc_generic` | `models/npc_generic.tscn` | Entity | Capsule silhouette, neutral grey, 24×32px footprint | +| `wall_bar_green` | `models/wall_bar_green.tscn` | Structure | 1.0×0.8×0.2 box, green panel texture (model only — no sprites rendered yet) | +| `npc_generic` | `models/npc_generic.tscn` | Entity | Capsule silhouette, neutral grey, 24×32px footprint. Rotationally symmetric — north/south and east/west pairs are near-identical by design (asymmetric silhouettes come from named NPC models with identifying features per D-044). | ## Output Naming Convention @@ -92,7 +92,7 @@ Directions: `north`, `east`, `south`, `west` (model rotated 0°, 90°, 180°, 27 64px sprites are deployed to: `client/assets/sprites/` -Naming convention at the client side is `_.png` (64px only — the 1024 and 256 variants stay in `renderer/output/` as pipeline intermediates). +Naming convention at the client side matches the pipeline output: `__64.png`. The 1024 and 256 variants stay in `renderer/output/` as pipeline intermediates (gitignored). ## Design Constraints diff --git a/renderer/models/wall_bar_green.tscn b/renderer/models/wall_bar_green.tscn index 403726ce5..66b61ffac 100644 --- a/renderer/models/wall_bar_green.tscn +++ b/renderer/models/wall_bar_green.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=3 format=3] +[gd_scene load_steps=3 format=3 uid="uid://wallbargreen001"] [ext_resource type="Texture2D" path="res://textures/wall_bar_green_panels.png" id="1_wall_texture"] diff --git a/renderer/render_export.gd b/renderer/render_export.gd index 489da64b2..544dc9e3a 100644 --- a/renderer/render_export.gd +++ b/renderer/render_export.gd @@ -10,7 +10,7 @@ const ROTATIONS: PackedFloat64Array = [0.0, 90.0, 180.0, 270.0] @export var model_scene: PackedScene @export var output_name: String = "wall_structural" @export var outline_width_px: int = 4 # at 256 = 1px at 64 -@export var outline_color: Color = Color(0.2, 0.2, 0.25, 1.0) +@export var outline_color: Color = Color(0.2, 0.2, 0.25, 1.0) # #333340 per D-043 @export var render_now: bool = false: set(value): if value and model_scene: -- 2.54.0