Reorganize docs into category folders and drop the retired head-review page
Group the top-level docs into docs/project, docs/design, docs/assets and docs/process, each with its own README index. Delete the superseded docs/screens/head-review page and sheets. Rewrite every markdown link, absolute docs path and tool/test reference to the new locations, fix the pre-existing broken anchors and remove the dead Joke Shop photo-board link. check_docs OK, Python tooling 167/167.
This commit is contained in:
1 parent
045ed2a58f
commit
2ba40f3578
57 files changed
+406
-414
No files matched your search
@@ -0,0 +1,730 @@
|
||||
# Asset Pipeline and Tooling
|
||||
|
||||
Everything the game renders is produced or gated by the scripts below. Architecture-level contracts,
|
||||
data flow and failure fallbacks are summarized in
|
||||
[`architecture.md`](../project/architecture.md#content-and-asset-pipeline-architecture); this page is the
|
||||
operational reference. Development-only outputs (AI concept sheets, Blender sources, raw AI fabrics)
|
||||
are gitignored; the audited game assets are committed.
|
||||
|
||||
## Image Asset Tools
|
||||
|
||||
Install the Pillow dependency used by the existing texture generator and the asset tools:
|
||||
|
||||
```bash
|
||||
python3 -m pip install -r requirements-tools.txt
|
||||
```
|
||||
|
||||
Generate a role-sized image (generation requires `OPENROUTER_API_KEY`):
|
||||
|
||||
```bash
|
||||
python3 tools/asset_pipeline.py generate --prompt "weathered red brick" --role tile --output assets/textures/brick_extra.png
|
||||
```
|
||||
|
||||
Generated images are resized to the selected role profile. Non-square requests use
|
||||
the validated `openai/gpt-5-image-mini` source ratio and crop to the final role dimensions.
|
||||
Transparent glyphs can be recolored while preserving their alpha mask with
|
||||
`--foreground '#ffffff'`.
|
||||
|
||||
Crop uses the image center by default; `--x` and `--y` select an explicit crop origin. Resize defaults to aspect-preserving `contain`; use `--fit cover` to crop or `--fit stretch` to distort to the exact dimensions.
|
||||
|
||||
```bash
|
||||
python3 tools/asset_pipeline.py crop assets/textures/source.png assets/textures/crop.png --width 256 --height 128
|
||||
python3 tools/asset_pipeline.py resize assets/textures/source.png assets/images/scenes/scene.png --width 1920 --height 1080 --fit cover
|
||||
python3 tools/asset_pipeline.py seamless assets/textures/rough_tile.png assets/textures/tile.png
|
||||
```
|
||||
|
||||
Audit accepts image files or directories. `--role auto` infers common roles from paths and filenames; explicit roles include `tile`, `texture`, `icon`, `card`, `banner`, `backdrop`, `background`, `overlay`, `app_icon`, and `screenshot`.
|
||||
|
||||
```bash
|
||||
python3 tools/asset_pipeline.py audit assets/textures
|
||||
python3 tools/asset_pipeline.py audit assets/images/scenes --role background --json
|
||||
python3 -m unittest discover -s tools/tests -t tools -p 'test_*.py'
|
||||
```
|
||||
|
||||
The audit classifies measurable fit issues as `good` or `bad` based on role-specific dimensions, aspect ratio, alpha requirements, and tile-edge continuity. It lists perspective/projection as a visual review item because image semantics cannot be judged reliably by these deterministic checks.
|
||||
|
||||
## Prop models
|
||||
|
||||
Street furniture (bollards, benches, bins, planters, newspaper boxes, a hydrant)
|
||||
is real 3D geometry generated by `tools/model_pipeline.py` and written to
|
||||
`assets/models/generated/*.glb`. Godot imports each file as a `PackedScene`, and
|
||||
`WorldBuilder` instantiates it with a procedural fallback when the model or its
|
||||
import is missing, so the world still builds on a fresh checkout.
|
||||
|
||||
```bash
|
||||
# Print the catalogue (parts, triangle count, whether a model/concept exists)
|
||||
python3 tools/model_pipeline.py list
|
||||
|
||||
# Rebuild the models (deterministic, offline) and validate them
|
||||
python3 tools/model_pipeline.py build
|
||||
python3 tools/model_pipeline.py audit
|
||||
|
||||
# Optional: an image model supplies the colour palette per prop
|
||||
# (requires OPENROUTER_API_KEY; the repo .env is read automatically)
|
||||
python3 tools/model_pipeline.py concept
|
||||
python3 tools/model_pipeline.py concept --only bench --force
|
||||
```
|
||||
|
||||
Each prop is a small list of boxes and cylinders, so `build` needs no network and
|
||||
the output is byte-identical between runs. `concept` asks an image model for a
|
||||
studio-style reference; `dominant_colors` and `assign_roles` then map that image's
|
||||
colours onto material roles (darkest → metal, warm saturated → wood, light grey →
|
||||
stone, green → foliage). glTF stores colours in linear space, so the tool converts
|
||||
the sRGB palettes before writing them. Re-run `build` and `godot --headless
|
||||
--path . --import` after changing a prop. See
|
||||
[ADR-0012](../adr/0012-surface-materials-and-generated-props.md).
|
||||
|
||||
Surface materials share one definition in `src/game/materials.gd`: named
|
||||
roughness/metallic profiles per surface, plus the environment tune-up (ACES
|
||||
tonemapping, glow, fog, colour grading) that the phase lighting applies.
|
||||
|
||||
## NPC character model
|
||||
|
||||
NPCs are moving from the procedural capsule rig to a skinned, UV-mapped base
|
||||
character authored in Blender ([ADR-0013](../adr/0013-blender-authored-npc-characters.md)).
|
||||
The active base is the CC0 Quaternius modular character, baked to the runtime contract
|
||||
([ADR-0017](../adr/0017-quaternius-npc-base.md)).
|
||||
`tools/npc_pipeline.py` builds `assets/models/generated/npc_base.glb` plus the
|
||||
per-slot material maps. `NpcAvatar` instantiates that skinned model, tints its
|
||||
five material slots from the seed-driven outfit, and drives the bones from the
|
||||
existing animation code; the procedural rig remains the offline fallback (see
|
||||
[ADR-0013](../adr/0013-blender-authored-npc-characters.md) and
|
||||
[`blender-assets.md`](blender-assets.md)).
|
||||
|
||||
```bash
|
||||
# Print the frozen contract (slots, bone names, triangle budget)
|
||||
python3 tools/npc_pipeline.py list
|
||||
|
||||
# Rebuild the skinned base mesh + neutral maps in Blender and validate it
|
||||
flatpak run org.blender.Blender --background --python "$PWD/tools/npc_pipeline.py" -- build
|
||||
python3 tools/npc_pipeline.py audit
|
||||
|
||||
# Layer the AI clothing albedos (shirt/trousers) over the procedural weave, and
|
||||
# derive per-fabric outfit colours for the avatar palette
|
||||
python3 tools/npc_pipeline.py prepare
|
||||
python3 tools/npc_pipeline.py palette
|
||||
```
|
||||
|
||||
`prepare` resizes the AI fabric tiles (kept locally in `assets/textures/npc/ai/`,
|
||||
which is gitignored) onto the 256² `cloth`/`pants` albedos; `build` bakes them into
|
||||
the `.glb` and regenerates the procedural normal + packed ORM maps at the same
|
||||
256² size. Runtime fabric materials repeat the weave 4× (`NPC_FABRIC_UV_SCALE`)
|
||||
and skip the per-character baked AO, which otherwise magnified into large
|
||||
blotches; skin, hair and fixed gear keep AO. `palette` writes
|
||||
`assets/textures/npc/palette.json` using `model_pipeline.dominant_colors`.
|
||||
|
||||
## Blender asset library (loaded by the game)
|
||||
|
||||
`tools/blender_assets.py` models a larger library procedurally in Blender for the
|
||||
v0.6.0 integration: **8 skinned NPC characters** (casual, jacket, coat, hi-vis vest,
|
||||
dress, hoodie, firefighter, guard), each a drop-in for `npc_base.glb` (same bones,
|
||||
joint positions and tint slots); **15 gear pieces** authored at head/hand/back/chest
|
||||
attach points (caps, hood, helmet, backpack, SCBA tank, lanyard, phone, cup,
|
||||
clipboard, umbrella, camera, sign, flashlight, wand, radio, …); and **20 props**
|
||||
(octagonal bollard and bin, slatted bench, hydrant, news box, planters, street lamp,
|
||||
street and palm trees, A-frame sign, wall lantern, cone, crowd barrier, stanchion,
|
||||
handrail, podium, double door, marquee, sedan).
|
||||
|
||||
`NpcAvatar` selects a character by role/seed, tints the five contract slots, and
|
||||
attaches gear at the documented points; `WorldBuilder` loads the library props
|
||||
with colliders/avoidance and drives their emissive parts from the day/night
|
||||
cycle. The single `npc_base.glb` and primitive props remain offline fallbacks.
|
||||
Contract and integration notes: [`blender-assets.md`](blender-assets.md).
|
||||
|
||||
Sourcing and migration plan: [`npc-model-sourcing.md`](npc-model-sourcing.md) records the
|
||||
2026-11-05 direction (ADR-0017). Quaternius Ultimate Modular (CC0) is the primary NPC mesh
|
||||
and animation source, baked to the existing 13-core contract; the procedural generator is
|
||||
the fallback. Kenney/Khronos/MakeHuman remain evaluated references, not active bases.
|
||||
|
||||
| Characters | Props |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
```bash
|
||||
flatpak run org.blender.Blender --background --python "$PWD/tools/blender_assets.py" -- build
|
||||
flatpak run org.blender.Blender --background --python "$PWD/tools/blender_assets.py" -- build --head mh
|
||||
flatpak run org.blender.Blender --background --python "$PWD/tools/blender_assets.py" -- render
|
||||
python3 tools/blender_assets.py sheet # docs/blender_previews/sheet_*.jpg
|
||||
python3 tools/blender_assets.py audit # no Blender needed
|
||||
python3 tools/blender_assets.py fit # gear-vs-body fit report
|
||||
python3 tools/blender_assets.py texels # per-slot texel density (px/m) of the characters
|
||||
# `build --head mh` selects the retained MakeHuman fallback head; the default is still `face`.
|
||||
godot --headless --path . --import
|
||||
```
|
||||
|
||||
### Intake inspection (`inspect`)
|
||||
|
||||
`audit` reads accessor-local bounds and ignores node transforms, so it is the right
|
||||
release gate but a poor first look at a downloaded or newly authored GLB. `inspect`
|
||||
imports the file into Blender, clears parents and applies object transforms, then
|
||||
reports what the scene actually contains:
|
||||
|
||||
```bash
|
||||
# one asset, a batch, or an external download; repeat --file as needed
|
||||
flatpak run org.blender.Blender --background \
|
||||
--python "$PWD/tools/blender_assets.py" -- inspect \
|
||||
--file "$PWD/assets/models/blender/npc/npc_casual.glb" \
|
||||
--file "$PWD/tmp/downloads/SomeCharacter.glb"
|
||||
# machine-readable, for intake scripts (write to a file: Blender prints a banner first)
|
||||
... -- inspect --file "$PWD/tmp/x.glb" --json --out "$PWD/tmp/x.json"
|
||||
# force a contract family instead of inferring it from the path
|
||||
... -- inspect --file "$PWD/tmp/x.glb" --category npc
|
||||
```
|
||||
|
||||
It prints triangles (with the category budget), raw and seam-split vertex counts,
|
||||
Godot-space bounds with height/feet for characters, whether transforms had to be
|
||||
applied, mesh nodes and armatures, bone names, materials, UV layers, skinning and
|
||||
animation count. Hard contract failures (`issues`) fail the command; advisory
|
||||
findings (`warnings`) do not. The category is inferred from the path:
|
||||
`blender/npc` → `npc`, `blender/npc_gear` → `npc_gear`, `blender/props` and
|
||||
`generated` → `props`, anything else → `external` (measured but with no project
|
||||
budget or rig contract).
|
||||
|
||||
Caveats:
|
||||
|
||||
- The glTF importer adds a bone-display mesh (`Icosphere`, used as
|
||||
`pose_bone.custom_shape`). `inspect` excludes custom-shape helpers so they never
|
||||
pollute bounds or the unskinned list.
|
||||
- Bounds and bone rests are converted from Blender Z-up to Godot Y-up (`x, z, -y`),
|
||||
matching the contract bands and the audit's coordinate space.
|
||||
|
||||
### Intake adaptation (`adapt`)
|
||||
|
||||
`adapt` is the mechanical first half of an external-mesh intake (Quaternius,
|
||||
MakeHuman fallback, downloaded GLB): import, apply transforms, ground the feet,
|
||||
weld seam splits, decimate to the character budget, and export a GLB. It does **not** map
|
||||
the five tint slots or rebuild the 13-core-bone `npc_base` armature (optional extra bones are allowed) — those stay a
|
||||
manual `retopology`/`rigging` step with a recorded second-rig contract if the
|
||||
source rig cannot be retargeted:
|
||||
|
||||
```bash
|
||||
flatpak run org.blender.Blender --background \
|
||||
--python "$PWD/tools/blender_assets.py" -- adapt \
|
||||
--file "$PWD/tmp/npc-research/quaternius/Casual.glb" \
|
||||
--out "$PWD/tmp/npc-research/quaternius_adapted.glb" \
|
||||
--target-tris 24000
|
||||
# prints: adapted quaternius_adapted.glb: <source> -> 24000 tris (bones=N, meshes=1)
|
||||
```
|
||||
|
||||
Run `inspect` on the adapted GLB before and after, and keep the procedural
|
||||
generator as the fallback until a Quaternius variant passes `audit`/`fit` and the
|
||||
24-NPC budget.
|
||||
- Numbers are Blender-side measurements. Triangles match `audit` on project assets; the seam-split
|
||||
vertex count can differ by ±1 because the importer rounds normals. `audit` stays the single final
|
||||
gate: it is stdlib-only, runs in CI, and matches what the engine imports.
|
||||
- `inspect` never writes or adapts the file; scale/grounding/weld/retarget work is the
|
||||
separate (not yet built) `adapt` step.
|
||||
- `--json` also captures the importer's INFO log in `stats.import_log` instead of printing it.
|
||||
Blender still prints its own version banner before the script runs, so intake scripts should use
|
||||
`--json --out <file>` rather than parsing stdout.
|
||||
|
||||
### Reference-anchored concept generation
|
||||
|
||||
`tools/scene_concepts.py generate --reference <photo>` anchors a concept frame on a real
|
||||
photograph: the photo is sent to the OpenRouter multimodal chat endpoint as a base64 data URL
|
||||
(`generate_textures.generate_image(reference_path=...)`) with a prompt suffix that pins the
|
||||
photographed geometry (column rhythm, band heights, signage placement, camera angle) while the
|
||||
prompt restages lighting, weather and people.
|
||||
|
||||
Licence gate: the reference must be listed in its `docs/screens/refs/<venue>/SIDECAR.md` row
|
||||
(which also supplies the licence and Commons page for provenance). A photo from anywhere else
|
||||
requires `--reference-unchecked`, which records `unchecked` in the provenance. Each anchored
|
||||
generation writes a `.refgen.json` sidecar next to the frame (`model`, reference file/licence/page,
|
||||
geometry suffix) so a concept frame can always be traced back to its evidence photo. Where a
|
||||
generated frame and its reference photo disagree, the photo wins for shape.
|
||||
|
||||
### Photo-anchored variant pass
|
||||
|
||||
`tools/scene_concepts.py anchor --reference <photo> --context "..." --treatments day,night,weathered
|
||||
--out-dir docs/screens/concepts/<block>` generates several restaged conditions (day, night,
|
||||
weathered) of one reference photo's framing, each with its `.refgen.json` provenance sidecar, so a
|
||||
concept board stays comparable with its photo board. The reference passes the same SIDECAR licence
|
||||
gate as `--reference` on `generate`.
|
||||
|
||||
### Local-only reference collection (`refcollect`)
|
||||
|
||||
`tools/refcollect.py` downloads images from DuckDuckGo image search into `tmp/research/<slug>/` for
|
||||
development reference only. It applies **no licence gate** (rows are marked `local_only` /
|
||||
`unchecked-local`); everything it fetches must never be committed or promoted into
|
||||
[`screens/refs/`](../screens/refs/README.md) without the normal sidecar licence + privacy gate. This
|
||||
is the owner-authorised local-only override (TODO.md §3D model re-evaluation → A).
|
||||
|
||||
```bash
|
||||
python3 tools/refcollect.py search "Comedy Store Sunset Boulevard" --slug comedy-store --limit 20
|
||||
python3 tools/refcollect.py sheet --slug comedy-store # local contact sheet (never committed)
|
||||
```
|
||||
|
||||
Requires `pip install duckduckgo-search` (renamed `ddgs` upstream). DuckDuckGo rate-limits by IP
|
||||
(HTTP 202), so every request — the search itself and each image download — passes through a shared
|
||||
token bucket (`--rate`, default 0.5 req/s, and `--burst`, default 3) and a polite retry routine:
|
||||
transient statuses (408/425/429/5xx/52x) and network errors back off exponentially with full
|
||||
jitter, `Retry-After` is honoured, permanent failures fail fast, and the loop gives up after six
|
||||
attempts. Requests carry a full, coherent Firefox header set (`--user-agent` overrides the UA
|
||||
string); the UA alone is no longer enough to look like a browser.
|
||||
|
||||
### Reference-board research (`refboard`)
|
||||
|
||||
`tools/refboard.py` searches, fetches and documents Wikimedia Commons photos of a real location for
|
||||
the [`screens/refs/`](../screens/refs/README.md) boards. It is stdlib-only and never commits anything
|
||||
itself: downloads land in `tmp/`, and moving a file into a board folder is a deliberate manual step
|
||||
that goes through the sidecar licence gate.
|
||||
|
||||
```bash
|
||||
# search: keyword, geosearch (the reliable "photos of this exact place"), or category
|
||||
python3 tools/refboard.py search --source geo "34.10118|-118.34009" --limit 60 --months 12
|
||||
python3 tools/refboard.py search "El Capitan Entertainment Centre" --source search
|
||||
python3 tools/refboard.py search "Hollywood Masonic Temple" --source category --json
|
||||
# fetch the usable rows into tmp/ (review-only rows need --include-review)
|
||||
python3 tools/refboard.py search ... --json > /tmp/manifest.json
|
||||
python3 tools/refboard.py fetch --from /tmp/manifest.json --dir tmp/research/<block> --max 12
|
||||
# emit SIDECAR + README table rows from the fetched manifest
|
||||
python3 tools/refboard.py rows --from tmp/research/<block>/meta.json
|
||||
```
|
||||
|
||||
Licence gate: `Public domain`, `CC0` and `CC BY` are committable with attribution; `CC BY-SA` rows
|
||||
are flagged `review` and stay research-only until the share-alike decision is written into the
|
||||
sidecar (`--allow-ccbysa` marks them committable after that review); anything else is skipped.
|
||||
`--months 12` flags recent captures so a "current reality" photo can be distinguished from the
|
||||
older geometry evidence. Network gotchas baked in: a descriptive User-Agent, batched `titles=`
|
||||
metadata queries and ~3 s download spacing to avoid Commons HTTP 429s.
|
||||
|
||||
### Texel density (`texels`)
|
||||
|
||||
`texel_density()` and the `texels` command compute `resolution * sqrt(UV area / world area)` per
|
||||
material slot straight from the GLB, so the number does not depend on island packing. The NPC slot
|
||||
maps are 256², and cloth/pants are reported twice: authored density and effective density with the
|
||||
runtime 4× repeat from `Materials.NPC_FABRIC_UV_SCALE`. Current values and interpretation are in
|
||||
[`3d-model-review.md`](3d-model-review.md).
|
||||
|
||||
```bash
|
||||
python3 tools/blender_assets.py texels # all eight characters
|
||||
python3 tools/blender_assets.py texels --json # machine-readable
|
||||
python3 tools/blender_assets.py texels --only npc_coat
|
||||
```
|
||||
|
||||
|
||||
### Quaternius modular parts (outfit variety)
|
||||
|
||||
`tools/quaternius_parts.py` is the intake for the upstream modular pack, which ships as a Google
|
||||
Drive folder and is therefore downloaded once by hand into `tmp/quaternius-intake/` (gitignored):
|
||||
|
||||
| Step | Command | Runs in |
|
||||
| --- | --- | --- |
|
||||
| list what the download contains | `python3 tools/quaternius_parts.py inventory --dir tmp/quaternius-intake` | plain Python (parses the GLB container) |
|
||||
| cut one mesh out as a part | `... extract --file <glb> --mesh <name> --part <name>` | Blender (rewrites the skin) |
|
||||
| hash and validate the kept parts | `... verify --dir assets/models/blender_src/quaternius/parts` | plain Python |
|
||||
|
||||
A part is one skinned mesh on the 62-joint rig; the contract and the join behaviour are in
|
||||
`assets/models/blender_src/quaternius/parts/README.md`. `build` joins a role's
|
||||
`CHARACTERS[...]["parts"]` onto its base body before the slot map, keeping the part's weights and
|
||||
dropping its redundant armature. The pack is never committed — only the parts that are used, with
|
||||
their SHA-256 in `CREDITS.md`.
|
||||
|
||||
## Local map development (planned)
|
||||
|
||||
The local map workstreams (H–M) in [`TODO.md`](../../TODO.md#3d-model-and-local-map-re-evaluation) are
|
||||
planned, not implemented; there are no map-generation commands yet. The runtime
|
||||
contract, provenance rules, budget/navigation gates and fallback are in
|
||||
[`architecture.md#venue-implementation-contract`](../project/architecture.md#venue-implementation-contract).
|
||||
The detailed pipeline/task list lives in `TODO.md`.
|
||||
|
||||
## Confirmed game assets
|
||||
|
||||
Everything below is audited and shipped in the game; development-only outputs
|
||||
(AI concept sheets, Blender sources, raw AI fabrics) are gitignored.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
- **Surface textures:** the v2 picks above (brick, sidewalk pavers, dry and wet asphalt, awning canvas) plus their normal maps, loaded by `src/game/materials.gd` and `WorldBuilder`; the three skyline panoramas are switched per shift phase. The first-generation tiles and `assets/textures/skyline.png` are no longer loaded.
|
||||
- **UI art:** `assets/textures/ai/game_ready/icons/` (flashlight, door lock, wand, REC) and `assets/textures/ai/game_ready/cards/` (ID, wristband, ticket, credential, guest list), loaded by `src/game/ui/hud.gd`.
|
||||
- **Generated models:** `assets/models/generated/*.glb` — bollard, bench, trash bin, planter, newspaper box, hydrant, and the NPC base — plus their colour atlases.
|
||||
- **NPC material maps:** `assets/textures/npc/*_{albedo,normal,orm}.png` and `palette.json`.
|
||||
|
||||
Audit them all:
|
||||
|
||||
```bash
|
||||
python3 tools/asset_pipeline.py audit assets/textures/ai/game_ready
|
||||
python3 tools/model_pipeline.py audit
|
||||
python3 tools/npc_pipeline.py audit
|
||||
```
|
||||
|
||||
## Optional interactive Blender MCP
|
||||
|
||||
Status: **optional interactive tooling, not a build dependency.** Blender MCP is
|
||||
useful for exploring a local part scene, moving the viewport and iterating on a
|
||||
head/hand mesh without a manual UI walkthrough. The authoritative asset
|
||||
pipeline remains the headless CLI plus `audit`/`fit`/`inspect`; CI, tests and
|
||||
release builds must continue to work with no Blender process and no MCP server.
|
||||
|
||||
This project does not vend the third-party addon or pin a runtime MCP
|
||||
dependency. Install it outside the repository and keep generated scenes under
|
||||
`tmp/`.
|
||||
|
||||
### What it adds
|
||||
|
||||
With Blender MCP connected, an agent can:
|
||||
|
||||
- query the current scene and object data;
|
||||
- run small `bpy` snippets through the MCP bridge;
|
||||
- capture a viewport screenshot for visual checks;
|
||||
- inspect or modify a local part scene before exporting a GLB.
|
||||
|
||||
It does **not** replace:
|
||||
|
||||
- `python3 tools/blender_assets.py audit/fit/inspect`;
|
||||
- `python3 tools/npc_proportions.py`;
|
||||
- the Godot capture and smoke gates;
|
||||
- the project's licence/provenance review.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Blender 5.2 LTS, installed as the `org.blender.Blender` flatpak (the project
|
||||
standard).
|
||||
- An MCP-capable client.
|
||||
- The upstream [BlenderMCP addon](https://github.com/ahujasid/blender-mcp)
|
||||
(`addon.py`) and its MCP server.
|
||||
- `uv`/`uvx` if you use the server's `uvx blender-mcp` launcher; a local install
|
||||
of the server package is equally valid.
|
||||
|
||||
Check the upstream README for the current package/addon version and any command
|
||||
changes before installing. Do not copy the addon into this repository.
|
||||
|
||||
### Setup
|
||||
|
||||
#### 1. Install the Blender addon
|
||||
|
||||
1. Download or clone the upstream BlenderMCP project.
|
||||
2. In Blender: **Edit → Preferences → Add-ons → Install from Disk**, select
|
||||
`addon.py` from that checkout.
|
||||
3. Enable **Interface: Blender MCP**.
|
||||
4. Open the 3D View sidebar (N panel), find the **BlenderMCP** tab, and click
|
||||
**Connect to MCP server**. The default server listens on
|
||||
`localhost:9876`.
|
||||
|
||||
The addon must be running inside the Blender process that has the scene open.
|
||||
Starting a second Blender instance does not connect the first one.
|
||||
|
||||
#### 2. Configure the MCP client
|
||||
|
||||
For Pi, add the server at user scope so it is not committed into the project:
|
||||
|
||||
```bash
|
||||
pi mcp add blender -- uvx blender-mcp
|
||||
pi mcp list
|
||||
```
|
||||
|
||||
Then start/reload Pi and use `/mcp` to inspect the connection. Other clients
|
||||
use their normal MCP server configuration. A generic JSON example:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"blender": {
|
||||
"command": "uvx",
|
||||
"args": ["blender-mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For a client with a different config schema, use the equivalent server command
|
||||
and keep the server name `blender`. Restart/reload the client after adding it,
|
||||
then request the tool list and run a harmless `get_scene_info` before editing.
|
||||
|
||||
#### 3. Connect through the flatpak
|
||||
|
||||
Start Blender through the project's normal flatpak command, open the local part
|
||||
scene, and connect from the sidebar. If the client cannot reach the addon:
|
||||
|
||||
- confirm the addon panel says connected;
|
||||
- confirm the client is launching the MCP server, not just a shell;
|
||||
- check port `9876` is not occupied;
|
||||
- if the flatpak sandbox is blocking localhost networking, grant network
|
||||
permission only for this interactive use, for example:
|
||||
`flatpak override --user --share=network org.blender.Blender`
|
||||
(review the permission change afterwards; do not add it to CI or build
|
||||
scripts).
|
||||
- restart both the MCP client and Blender after changing the addon or config.
|
||||
|
||||
### Recommended part-workflow
|
||||
|
||||
The headless part pipeline is `tools/npc_parts.py` (see
|
||||
[`blender-assets.md#local-part-pipeline`](blender-assets.md#local-part-pipeline)). It builds,
|
||||
extracts, assembles, inspects and sheets the reusable head/face and hand parts and reuses the
|
||||
`blender_assets.py` builders, so there is no second face/hand implementation. The MCP steps below
|
||||
remain optional interactive exploration; the headless commands stay the build and audit path.
|
||||
|
||||
1. Create a local scene, for example
|
||||
`tmp/npc-parts/npc_casual/head.blend`, or open a copy of an exported part GLB.
|
||||
Never save over the tracked `assets/models/npc/npc_base.blend`.
|
||||
2. Ask the MCP client for `get_scene_info`, then inspect the object/bone/mesh
|
||||
names it reports.
|
||||
3. Use `execute_blender_code` for small, reversible edits. Prefer adding a new
|
||||
object or modifier over deleting existing geometry; keep a backup copy.
|
||||
4. Use `get_viewport_screenshot` after camera/material changes to check the
|
||||
head/face/hand before exporting.
|
||||
5. Export a **local** GLB under `tmp/npc-parts/`, not directly into
|
||||
`assets/models/`.
|
||||
6. Run the normal gates on the exported file:
|
||||
|
||||
```bash
|
||||
flatpak run org.blender.Blender --background \
|
||||
--python "$PWD/tools/blender_assets.py" -- \
|
||||
inspect --file "$PWD/tmp/npc-parts/head.glb" --category npc --json
|
||||
python3 tools/blender_assets.py audit
|
||||
python3 tools/blender_assets.py fit
|
||||
python3 tools/npc_proportions.py report
|
||||
```
|
||||
|
||||
7. Only after the part is reviewed and the contract passes should it be
|
||||
assembled into a character and rebuilt through the headless pipeline.
|
||||
|
||||
### Safety and project rules
|
||||
|
||||
- MCP is **not** part of `project.godot`, the build, CI, tests, release packages
|
||||
or runtime.
|
||||
- Do not commit `.blend` part scenes, MCP configuration, API keys or generated
|
||||
previews. The working set stays in `tmp/`; a part GLB is not a shipped asset.
|
||||
- Do not use MCP to bypass the contract, budgets, AO/UV rules or licence gate.
|
||||
- The 13 core bones must remain at their rest positions; extra optional bones
|
||||
are allowed only if `NpcAvatar` and the audit/tests still pass.
|
||||
- Prefer scripted headless changes for reproducible work; MCP is for the
|
||||
interactive exploration and review that is awkward in a one-shot script.
|
||||
- If MCP is unavailable, use the headless CLI and document the manual step; never
|
||||
make the project depend on MCP being present.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
| Symptom | Check |
|
||||
| --- | --- |
|
||||
| Client cannot find `blender` | MCP client config path/schema; restart client; confirm server command exists |
|
||||
| `Connection refused` | Addon **Connect to MCP server** pressed; Blender still running; port 9876 |
|
||||
| Tools return stale scene data | Refresh scene/object query after edits; restart the connection if needed |
|
||||
| Flatpak cannot reach localhost | Check flatpak network permission; grant only for interactive use |
|
||||
| Changes look wrong in export | Re-import the GLB with `inspect`; check transforms, scale, UVs and materials |
|
||||
| Contract audit fails | Fix the mesh/bones first; do not edit the audit expectations around a broken asset |
|
||||
|
||||
## Realism pass: scene concepts, skylines and textures
|
||||
|
||||
The skyline, surface tiles and phase lighting were rebuilt against AI concept frames. The
|
||||
concepts live in [`docs/screens/concepts/`](../screens/concepts/README.md); the validated
|
||||
picks the game loads are in `assets/textures/ai/game_ready/v2/picks/` (day/dusk/night
|
||||
panoramas, brick, sidewalk pavers, dry and wet asphalt, awning canvas, each with a normal
|
||||
map).
|
||||
|
||||
`tools/scene_concepts.py` generates and processes new candidates, promotes the validated ones
|
||||
to stable names, audits them, and scores captures against the concepts. It never overwrites an
|
||||
image: new files get a `-vN` suffix, and `process` skips existing candidates unless `--new`.
|
||||
|
||||
```bash
|
||||
python3 tools/scene_concepts.py generate --group scene # or backdrop / texture, --variants N
|
||||
python3 tools/scene_concepts.py process && python3 tools/scene_concepts.py audit
|
||||
python3 tools/scene_concepts.py promote # validated picks -> v2/picks/
|
||||
godot --headless --path . --import
|
||||
|
||||
# A/B a candidate without touching game code: the same cameras, textures swapped at runtime.
|
||||
godot --path . --script res://src/test/capture_realism.gd -- --out=res://docs/screens/realism/<run>/after
|
||||
godot --path . --script res://src/test/capture_realism.gd -- --out=res://docs/screens/realism/<run>/try \
|
||||
--swap=res://assets/textures/ai/game_ready/v2/picks/brick_facade.png=res://assets/textures/ai/game_ready/v2/brick_facade-v2.png
|
||||
|
||||
# Score before/after against the concepts (Pillow detail metrics + vision judge).
|
||||
python3 tools/scene_concepts.py realism --run docs/screens/realism/<run> --judge \
|
||||
--model google/gemini-2.5-flash --repeats 3
|
||||
```
|
||||
|
||||
Judges default to `--model auto`: `tools/openrouter_judge.py` reads the live
|
||||
OpenRouter catalogue, keeps image-input models that support `response_format`,
|
||||
and ranks them by advertised token/image price. The run falls through the list
|
||||
on provider/model errors and aborts on an invalid key. Use
|
||||
`GIG_LINE_JUDGE_MODEL` or `--model <id>` for a pinned comparison.
|
||||
|
||||
For an open-ended review of a single capture/reference (no structured verdict),
|
||||
`tools/describe_image.py` sends the image plus a question to a vision model and
|
||||
prints the answer. It is how the 2026-11-03 re-evaluation read the local-only
|
||||
reference frames and the prop/venue captures:
|
||||
|
||||
```bash
|
||||
python3 tools/describe_image.py docs/screenshots/12-joke-shop.png \
|
||||
--question "Do the red curtain bays read as tied-back fabric or flat slabs?"
|
||||
```
|
||||
|
||||
`capture_npc_review.gd` is the pre-build NPC gate: it stages all eight
|
||||
library character models at portrait, conversation and crowd distance on both
|
||||
promoted maps in day and night lighting, with the normal crowd hidden. It uses
|
||||
the review-only `NpcAvatar.character_override` to include the guard model, which
|
||||
has no spawned NPC role:
|
||||
|
||||
```bash
|
||||
godot --path . --script res://src/test/capture_npc_review.gd -- \
|
||||
--out=res://tmp/npc-review
|
||||
```
|
||||
|
||||
Contact sheets can be assembled from the files (the output is local QA and is
|
||||
not committed). The runtime applies small attach-space offsets to the lanyard
|
||||
and firefighter helmet so the badge clears the jacket and the brim does not sit
|
||||
over the eyes; these are `NpcAvatar.GEAR_OFFSETS`, not changes to the gear
|
||||
contract.
|
||||
|
||||
`tools/npc_proportions.py` checks the eight character GLBs against the shared
|
||||
height/head/shoulder/hip/torso/leg/arm/foot guide; the current results are in
|
||||
[`3d-model-review.md`](3d-model-review.md#character-measurements). It reads the exported GLBs
|
||||
and does not require Blender.
|
||||
|
||||
`capture_npc_pose.gd` is the pose/deformation harness for the 3D re-evaluation: idle,
|
||||
walk-contact, walk-pass and seated poses of all eight variants, full-body plus shoulder
|
||||
close-ups. **It is currently blocked**: the applied pose is visible in the rig and bone values
|
||||
but not in the captured frames, so do not use its sheets as pose evidence. The reproduction and
|
||||
what was ruled out are in [`3d-model-review.md`](3d-model-review.md) §"Pose evidence is blocked".
|
||||
|
||||
The baseline sheet tool accepts a custom view list, which is also how pose sheets are tiled:
|
||||
|
||||
```bash
|
||||
# baseline set (six views, eight variants)
|
||||
python3 tools/npc_baseline.py sheet --dir tmp/npc-baseline
|
||||
# pose set produced by capture_npc_pose.gd
|
||||
python3 tools/npc_baseline.py sheet --dir tmp/npc-pose \
|
||||
--views idle,walk_contact,walk_pass,sit --out tmp/npc-pose/sheet_pose.jpg
|
||||
```
|
||||
|
||||
`capture_npc_baseline.gd` is the 0.9.4 neutral baseline: eight characters ×
|
||||
front/three-quarter/profile/back/full-body/crowd = 48 frames with a fixed
|
||||
camera, studio lighting and no venue/HUD/animation. The raw PNGs stay in
|
||||
`tmp/`; `tools/npc_baseline.py sheet` builds the committed contact sheet, and
|
||||
`tools/npc_baseline.py compare --before DIR --after DIR` builds the review
|
||||
comparison after a model change. See
|
||||
[`3d-model-review.md`](3d-model-review.md).
|
||||
|
||||
```bash
|
||||
godot --path . --script res://src/test/capture_npc_baseline.gd -- \
|
||||
--out=res://tmp/npc-baseline
|
||||
python3 tools/npc_baseline.py sheet --dir tmp/npc-baseline
|
||||
```
|
||||
|
||||
`capture_realism.gd` runs nine stages in this order: day, Hollywood surface close-ups,
|
||||
portrait, evening, late, Joke Shop day, Joke Shop surface close-ups, Joke Shop night,
|
||||
studio; `--stages=day,late` limits it while tuning; `--flashlight` turns on the guard
|
||||
lamp for night stages (used by the Desktop HQ A/B). The `portrait` shot freezes one
|
||||
NPC on open sidewalk and frames the face, which is the only capture that can judge
|
||||
avatar detail (`--portrait-type=N` picks a role). For the 0.9.5 texture audit, run
|
||||
the surface stages directly; the HUD is hidden for them:
|
||||
|
||||
```bash
|
||||
godot --path . --script res://src/test/capture_realism.gd \
|
||||
-- --out=res://tmp/surfaces --stages=surfaces,surfaces_joke
|
||||
```
|
||||
|
||||
The generated realism runs under
|
||||
`docs/screens/realism/` are **local, gitignored QA artifacts**; commit only the
|
||||
resulting decisions/docs, not the run folders. The 0.9.5 texture/UI audit checked actual screen sizes: all screen art is
|
||||
1280×720, access cards are 256×128 and HUD icons are 64×64; no runtime texture
|
||||
was oversized or unused. The 24-NPC smoke now reports 168 meshes / 131,616
|
||||
triangles / 6.94 ms. Compressed payloads are 24.55 MB of surface picks, 22.67 MB
|
||||
of screen art and 0.48 MB of NPC maps.
|
||||
|
||||
The 0.9.5 realism judge ran once per shot with `z-ai/glm-5.3-flash`
|
||||
(`python3 tools/scene_concepts.py realism --run docs/screens/realism/2026-11-02-tex
|
||||
--judge`). Its mean `after` scores were overall 1.8, lighting 2.3, materials 1.8,
|
||||
texture detail 1.2, backdrop 2.0 and characters 1.2; the Pillow metric gap was
|
||||
0.306. The judge still ranks the painted concept targets above the stylized build,
|
||||
particularly lit night facades and dense backdrop detail. That is expected: the
|
||||
pass validated close-range surfaces and promoted the texture repairs, not a
|
||||
photoreal renderer. The 0.9.5 before/after close-up sheet is
|
||||
[`blender_previews/sheet_texture_refinement_0.9.5.jpg`](../blender_previews/sheet_texture_refinement_0.9.5.jpg);
|
||||
the texture-adoption run itself was the earlier 0.8-style candidate comparison,
|
||||
and its report remains local/gitignored.
|
||||
|
||||
For the 0.9.6 integration checkpoint the same judge was pinned to
|
||||
`z-ai/glm-5.3-flash` and run once per shot on the 10-shot set: mean overall 2.1,
|
||||
lighting 2.4, materials 2.1, texture 1.7, backdrop 2.3, characters 1.8, Pillow
|
||||
gap 0.291. No `review` verdicts were produced by the screen judge, so there was
|
||||
no manual layout verdict to convert into a fix; the remaining realism comments
|
||||
are the accepted stylized gaps.
|
||||
|
||||
Build release artifacts from a clean checkout or detached worktree. A normal
|
||||
working tree may contain ignored `tmp/` captures and old `build/` artifacts, and
|
||||
Godot can pack enough of those into exports to inflate a 0.9.6 package from
|
||||
~160 MB to 1.6–3.1 GB. The 0.9.6 clean worktree produced 161.5 MB Linux,
|
||||
197.1 MB Windows, 148.0 MB macOS, 97.8 MB Web and 241.5 MB Desktop HQ.
|
||||
|
||||
The Forward+ A/B compared the Compatibility entrance with the optional Desktop HQ
|
||||
preset. The gear-fit run checked the sunglasses, bag and helmet against measured
|
||||
head/skirt bounds with `python3 tools/blender_assets.py fit`; it also fixed the
|
||||
portrait yaw and hid the focus overlay so the close-up shows the avatar.
|
||||
|
||||
Regenerate a run locally with the capture and `realism --run ...` commands above;
|
||||
the reports and sheets are intentionally not part of the release repository.
|
||||
|
||||
## Screen design references
|
||||
|
||||
Screen mockups and screenshot comparison use `tools/screen_designs.py`. It turns the
|
||||
screen descriptions in `docs/` (README captions, ADRs 0007–0010, `PLAN.md`, and
|
||||
`architecture.md`) into runtime reference art under `assets/images/ui/` (with a
|
||||
reference-only snapshot in `docs/screens/designs/`), then uses those images as
|
||||
the expected state to check the game's captures.
|
||||
|
||||
Each screen is tagged `scene` (a realistic first-person 3D world view) or `ui`
|
||||
(an in-game panel overlaid on a softened scene), and the generator appends a
|
||||
shared art-direction suffix so the frames read as one coherent game. Requests use
|
||||
a 3:2 landscape source and crop to the 16:9 capture dimensions.
|
||||
|
||||
```bash
|
||||
# Export the built-in scenarios to editor-authorable .tres resources
|
||||
godot --headless --path . --script res://tools/export_venues.gd
|
||||
|
||||
# List the catalogue (name, kind, whether a reference design exists)
|
||||
python3 tools/screen_designs.py list
|
||||
|
||||
# Deterministic wireframes (offline, no API key); existing designs are kept
|
||||
# unless --force is passed, so an offline run never replaces an AI mockup
|
||||
python3 tools/screen_designs.py generate --draft
|
||||
python3 tools/screen_designs.py generate --draft --only wand --force
|
||||
|
||||
# Realistic 3D scenes and in-game screen designs via OpenRouter
|
||||
# (requires OPENROUTER_API_KEY)
|
||||
python3 tools/screen_designs.py generate
|
||||
|
||||
# UI-state gallery (menu, pause, settings, review, radio, wheel, crowd, responder)
|
||||
godot --path . --script res://src/test/capture_ui_states.gd -- --out=res://docs/screenshots
|
||||
|
||||
# Prop gallery (studio line-up) for close-range prop checks
|
||||
godot --path . --script res://src/test/capture_prop_gallery.gd
|
||||
|
||||
# Compare docs/screenshots/*.png against the designs
|
||||
python3 tools/screen_designs.py compare
|
||||
python3 tools/screen_designs.py compare --json
|
||||
|
||||
# Ask a vision model whether each capture realises its design's screen and layout
|
||||
# (requires OPENROUTER_API_KEY; the repo .env is read automatically)
|
||||
python3 tools/screen_designs.py judge
|
||||
python3 tools/screen_designs.py judge --model openai/gpt-4o-mini --json
|
||||
python3 tools/screen_designs.py judge --only 03- # one capture
|
||||
python3 tools/screen_designs.py judge --dry-run # build requests, no network
|
||||
```
|
||||
|
||||
The catalogue covers 21 screens: the thirteen scene captures plus the start menu,
|
||||
pause overlay, settings, shift review, radial menu, emergency radio, crowd panic,
|
||||
and responder arrival. All 21 have an AI reference in `assets/images/ui/`
|
||||
(snapshot in `docs/screens/designs/`) and a committed capture, so every entry is
|
||||
compared or judged against the running game.
|
||||
|
||||
`compare` flags gross drift: wrong aspect ratio, undersized frame, near-blank
|
||||
frame, and large layout/palette/detail distance. It is a regression gate, not a
|
||||
semantic judge — pixel and perceptual metrics cannot decide whether a frame's
|
||||
composition is *right*.
|
||||
|
||||
`judge` covers that gap: it sends each capture together with its concept mockup to
|
||||
a vision model and asks whether the capture realises the same screen and layout
|
||||
intent. Art style, fidelity, fonts, and placeholder text are explicitly ignored.
|
||||
Verdicts are classed as `match`, `review` (a mismatch below `--fail-confidence`,
|
||||
default 0.6 — expected when a concept mockup is more stylised than the game), or
|
||||
`diff` (a confident mismatch, which exits non-zero). Regenerate the designs after changing screen layout, then re-run
|
||||
`compare` after regenerating the game screenshots with
|
||||
`src/test/capture_screenshots.gd` and `src/test/capture_ui_states.gd`.
|
||||
|
||||
For a one-command drift check, run `tools/check_screenshots.sh`. It renders the
|
||||
scene captures, UI-state captures and prop gallery into `docs/screenshots/` (using
|
||||
`xvfb-run` when no display is available) and then runs `screen_designs.py compare`.
|
||||
Reference in new issue
Block a user