feat(assets): 3D sprite render pipeline — camera, lighting, docs (#541)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 -- <model_name>
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use the `/sprite-gen` skill which wraps this command and handles output placement.
|
||||||
|
|
||||||
|
### Adding a New Model
|
||||||
|
|
||||||
|
1. Create `renderer/models/<model_name>.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 <model_name>`
|
||||||
|
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
|
||||||
|
|
||||||
|
```
|
||||||
|
<model_name>_<direction>_<resolution>.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 `<model_name>_<direction>.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
|
||||||
@@ -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")
|
||||||
@@ -12,16 +12,28 @@ render_target_update_mode = 1
|
|||||||
own_world_3d = true
|
own_world_3d = true
|
||||||
|
|
||||||
[node name="Camera3D" type="Camera3D" parent="SubViewport"]
|
[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
|
projection = 1
|
||||||
size = 1.4
|
size = 1.4
|
||||||
near = 0.01
|
near = 0.01
|
||||||
far = 10.0
|
far = 10.0
|
||||||
|
|
||||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="SubViewport"]
|
[node name="KeyLight" type="DirectionalLight3D" 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, 0, 0)
|
||||||
light_color = Color(1, 1, 1, 1)
|
light_color = Color(1, 1, 1, 1)
|
||||||
light_energy = 1.0
|
light_energy = 1.0
|
||||||
shadow_enabled = false
|
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"]
|
[node name="ModelRoot" type="Node3D" parent="SubViewport"]
|
||||||
|
|||||||
Reference in New Issue
Block a user