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 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 000000000..71693ef22 Binary files /dev/null and b/client/assets/sprites/npc_generic_east_64.png differ diff --git a/client/assets/sprites/npc_generic_north_64.png b/client/assets/sprites/npc_generic_north_64.png new file mode 100644 index 000000000..71693ef22 Binary files /dev/null and b/client/assets/sprites/npc_generic_north_64.png differ diff --git a/client/assets/sprites/npc_generic_south_64.png b/client/assets/sprites/npc_generic_south_64.png new file mode 100644 index 000000000..71693ef22 Binary files /dev/null and b/client/assets/sprites/npc_generic_south_64.png differ diff --git a/client/assets/sprites/npc_generic_west_64.png b/client/assets/sprites/npc_generic_west_64.png new file mode 100644 index 000000000..71693ef22 Binary files /dev/null and b/client/assets/sprites/npc_generic_west_64.png differ diff --git a/client/assets/sprites/wall_structural_east_64.png b/client/assets/sprites/wall_structural_east_64.png new file mode 100644 index 000000000..f0b097c39 Binary files /dev/null and b/client/assets/sprites/wall_structural_east_64.png differ diff --git a/client/assets/sprites/wall_structural_north_64.png b/client/assets/sprites/wall_structural_north_64.png new file mode 100644 index 000000000..e7de7a7fd Binary files /dev/null and b/client/assets/sprites/wall_structural_north_64.png differ 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 000000000..75d93b9f7 Binary files /dev/null and b/client/assets/sprites/wall_structural_south_64.png differ diff --git a/client/assets/sprites/wall_structural_west_64.png b/client/assets/sprites/wall_structural_west_64.png new file mode 100644 index 000000000..90c35dc50 Binary files /dev/null and b/client/assets/sprites/wall_structural_west_64.png differ diff --git a/renderer/README.md b/renderer/README.md new file mode 100644 index 000000000..1725f24ab --- /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 repository root +godot --path renderer/ --headless --quit-after 1200 res://render_scene.tscn -- +``` + +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: `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 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 + +## 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 (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 + +``` +__.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 matches the pipeline output: `__64.png`. The 1024 and 256 variants stay in `renderer/output/` as pipeline intermediates (gitignored). + +## 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/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: 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"]