docs(architecture): add character asset organization proposal
Directory structure, naming conventions, segmentation spec, and import pipeline for the 11 body type / 21 segment character system. Covers Quaternius Source tier mapping, CharacterVisualDescriptor contract, Surface Deform batch clothing pipeline, and coverage.json format. Built from spike findings with all meshes starting fresh from Source .blend files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,747 @@
|
||||
# Character Asset Organization Proposal
|
||||
|
||||
**Author:** TYRE
|
||||
**Date:** 2026-03-19
|
||||
**Status:** Proposal (pending review)
|
||||
**Inputs:** Spike 3dpipeline, Spike quaternius-aesthetic (VERDICT.md), Quaternius Source tier, D-159, D-160, D-161, D-162, D-163, D-164
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
This document proposes the directory structure, naming conventions, shared/per-variant
|
||||
boundaries, and import pipeline for character assets in the production client. It covers
|
||||
everything from skeleton to accessories. It does NOT cover environment assets (furniture,
|
||||
props) -- those follow the existing Trellis pipeline conventions proven in the 3dpipeline spike.
|
||||
|
||||
---
|
||||
|
||||
## 1. Quaternius Source Tier Mapping
|
||||
|
||||
The Quaternius Source tier has been purchased. Source .blend files are available at
|
||||
`docs/assets/downloads/Universal Base Characters[Source]/`. Five of the 11 body types have
|
||||
direct Source .blend files; six are forks requiring Blender modification.
|
||||
|
||||
All body meshes start fresh from Source .blend files. No mesh work from the spikes carries
|
||||
over -- the spike's automated segmentation output is discarded. Only shaders, import settings,
|
||||
and architectural patterns (slot system, CharacterVisualDescriptor, BoneAttachment3D) carry
|
||||
forward from spikes.
|
||||
|
||||
| Body type key | Quaternius source | Work needed |
|
||||
|---|---|---|
|
||||
| `average_m` | Regular_Male_FullBody.blend | Fork + segment |
|
||||
| `average_f` | Regular_Female_FullBody.blend | Fork + segment |
|
||||
| `muscular_m` | Superhero_Male_FullBody.blend | Segment (usable as-is) |
|
||||
| `muscular_f` | Superhero_Female_FullBody.blend | Segment (as-is) |
|
||||
| `teen_m` | Teen_Male_FullBody.blend | Segment (as-is or minor tweaks) |
|
||||
| `teen_f` | Teen_Female_FullBody.blend | Segment (as-is or minor tweaks) |
|
||||
| `thin_m` | Regular_Male as base | Fork + slim down + segment |
|
||||
| `thin_f` | Regular_Female as base | Fork + slim down + segment |
|
||||
| `heavy_m` | Regular_Male as base | Fork + widen + segment |
|
||||
| `heavy_f` | Regular_Female as base | Fork + widen + segment |
|
||||
| `child` | Teen as base | Fork to gender-neutral + scale down + segment |
|
||||
|
||||
Key decisions:
|
||||
- **Superhero = muscular.** Rejected as default body, but correct for the muscular end of
|
||||
the spectrum. Use Superhero .blends as-is for muscular body type.
|
||||
- **Teen is its own body type**, not a child stand-in. Direct Source .blends available.
|
||||
- **Child is gender-neutral**, forked from Teen and scaled down.
|
||||
- **5 direct / 6 forked** -- NOT hand-authored from scratch.
|
||||
|
||||
### Source tier hairstyles
|
||||
|
||||
Source tier includes ~18 hairstyle .blend files with "Rigged to Head Bone" variants ready
|
||||
for BoneAttachment3D:
|
||||
|
||||
**Female:** Bob, Buns, BuzzedFemale, Long, LongDreads, Ponytail_2 (+ Teen variants)
|
||||
**Male:** Balding, Beard, Buzzed, Dreads, Mohawk, Moustache, MuttonChops, Ponytail,
|
||||
SimpleParted, SlickBack (+ Teen variants)
|
||||
**Eyebrows:** Female, Regular, Teen, Thick (+ Teen variants)
|
||||
|
||||
Hair is mostly available from Source tier, not authored from scratch.
|
||||
|
||||
### Source tier head templates (OnlyHead .blends)
|
||||
|
||||
Source tier provides 4 standalone head .blend files -- the starter set for the curated head
|
||||
template library:
|
||||
|
||||
- Regular_Female_OnlyHead.blend
|
||||
- Regular_Male_OnlyHead.blend
|
||||
- Teen_Female_OnlyHead.blend
|
||||
- Teen_Male_OnlyHead.blend
|
||||
|
||||
These are exported as `head_001.glb` through `head_004.glb` in the template library. More
|
||||
heads are added over time via the offline Trellis pipeline (see Section 5).
|
||||
|
||||
### Animation libraries
|
||||
|
||||
Two Universal Animation Libraries are available (both Standard tier):
|
||||
|
||||
- **UAL1** -- core animation set (idle, walk, run, etc.)
|
||||
- **UAL2** -- extended animation set, includes Female Mannequin .blend
|
||||
|
||||
Both share the same skeleton hierarchy and can be combined into the production animation
|
||||
library GLBs.
|
||||
|
||||
---
|
||||
|
||||
## 2. Directory Structure
|
||||
|
||||
```
|
||||
client/assets/characters/
|
||||
skeleton/
|
||||
armature.glb # 65-bone skeleton, no mesh -- shared by ALL body types
|
||||
animations/
|
||||
ual_standard.glb # UAL1 -- core animations, shared by all body types
|
||||
ual_extended.glb # UAL2 -- extended animations + female mannequin
|
||||
ual_combat.glb # (future) additional animation libraries
|
||||
bodies/
|
||||
thin_m/ # One directory per body type (11 total)
|
||||
seg_head.glb
|
||||
seg_neck.glb
|
||||
seg_torso.glb # Full torso (default)
|
||||
seg_torso_upper.glb # Upper chest only (for low-cut/tank top clothing)
|
||||
seg_arm_upper_l.glb
|
||||
seg_arm_upper_r.glb
|
||||
seg_arm_lower_l.glb
|
||||
seg_arm_lower_r.glb
|
||||
seg_hand_l.glb
|
||||
seg_hand_r.glb
|
||||
seg_leg_upper_l.glb # Hip to knee (left)
|
||||
seg_leg_upper_r.glb # Hip to knee (right)
|
||||
seg_leg_lower_l.glb # Knee to ankle (left)
|
||||
seg_leg_lower_r.glb # Knee to ankle (right)
|
||||
seg_foot_l.glb # Extends up past ankle bone
|
||||
seg_foot_r.glb # Extends up past ankle bone
|
||||
seg_eyes.glb
|
||||
seg_eyebrows.glb
|
||||
thin_f/
|
||||
... # Same 21 segment files
|
||||
average_m/
|
||||
...
|
||||
average_f/
|
||||
...
|
||||
muscular_m/
|
||||
...
|
||||
muscular_f/
|
||||
...
|
||||
teen_m/
|
||||
...
|
||||
teen_f/
|
||||
...
|
||||
heavy_m/
|
||||
...
|
||||
heavy_f/
|
||||
...
|
||||
child/ # Gender-neutral, forked from Teen
|
||||
... # Same 21 segments but different proportions
|
||||
skin_tones/
|
||||
pale_cool.png # Skin tone texture variants
|
||||
pale_warm.png # Shared across all body types --
|
||||
light_olive.png # the toon shader tints these,
|
||||
medium_golden.png # so one set of textures works
|
||||
olive_warm.png # for all 11 body types
|
||||
medium_brown.png
|
||||
deep_brown.png
|
||||
very_deep_warm.png
|
||||
very_deep_cool.png
|
||||
heads/
|
||||
templates/ # Curated head template library (all heads live here)
|
||||
head_001.glb # Numeric IDs -- heads are unique per character
|
||||
head_001_mask.png # Recolor mask sidecar
|
||||
head_002.glb
|
||||
head_002_mask.png
|
||||
...
|
||||
hair/
|
||||
bob.glb # Source tier hairstyle (female)
|
||||
bob_mask.png # Recolor mask -- hair color via tint_color
|
||||
buns.glb
|
||||
buns_mask.png
|
||||
buzzed.glb # Source tier (male)
|
||||
buzzed_mask.png
|
||||
buzzed_female.glb # Source tier (female)
|
||||
buzzed_female_mask.png
|
||||
long.glb
|
||||
long_mask.png
|
||||
long_dreads.glb
|
||||
long_dreads_mask.png
|
||||
ponytail.glb
|
||||
ponytail_mask.png
|
||||
ponytail_f.glb # Ponytail_2 (female variant)
|
||||
ponytail_f_mask.png
|
||||
balding.glb
|
||||
balding_mask.png
|
||||
dreads.glb
|
||||
dreads_mask.png
|
||||
mohawk.glb
|
||||
mohawk_mask.png
|
||||
simple_parted.glb
|
||||
simple_parted_mask.png
|
||||
slick_back.glb
|
||||
slick_back_mask.png
|
||||
bald.glb # Placeholder or empty -- explicit "no hair"
|
||||
...
|
||||
facial_hair/
|
||||
beard.glb # Source tier facial hair
|
||||
beard_mask.png
|
||||
moustache.glb
|
||||
moustache_mask.png
|
||||
mutton_chops.glb
|
||||
mutton_chops_mask.png
|
||||
...
|
||||
eyebrows/
|
||||
eyebrows_regular.glb # Source tier eyebrows
|
||||
eyebrows_female.glb
|
||||
eyebrows_teen.glb
|
||||
eyebrows_thick.glb
|
||||
...
|
||||
clothing/
|
||||
<item_id>/ # One directory per clothing item
|
||||
reference.glb # Authored on average_m (reference body)
|
||||
reference_mask.png # Recolor mask (shared across all variants)
|
||||
coverage.json # Segment coverage + torso variant selection
|
||||
thin_m.glb # Surface Deform fitted variant
|
||||
thin_f.glb
|
||||
average_m.glb # Copy of reference.glb
|
||||
average_f.glb
|
||||
muscular_m.glb
|
||||
muscular_f.glb
|
||||
teen_m.glb
|
||||
teen_f.glb
|
||||
heavy_m.glb
|
||||
heavy_f.glb
|
||||
child.glb
|
||||
coveralls_basic/ # Example: a basic work coverall
|
||||
reference.glb
|
||||
reference_mask.png
|
||||
coverage.json
|
||||
thin_m.glb
|
||||
thin_f.glb
|
||||
average_m.glb
|
||||
...
|
||||
jacket_utility/
|
||||
...
|
||||
pants_cargo/
|
||||
...
|
||||
accessories/
|
||||
hat_hardhat.glb # Single-mesh rigid items
|
||||
hat_hardhat_mask.png # Recolor mask sidecar
|
||||
goggles_welding.glb
|
||||
goggles_welding_mask.png
|
||||
backpack_small.glb
|
||||
backpack_small_mask.png
|
||||
...
|
||||
shaders/
|
||||
toon.gdshader # Flat color toon (body segments)
|
||||
toon_masked.gdshader # Texture + recolor mask + toon shadow
|
||||
outline.gdshader # Inverted hull outline pass
|
||||
```
|
||||
|
||||
Total: ~10 top-level directories under `characters/`. Flat where possible, one level of
|
||||
nesting where multiplicity demands it (bodies, clothing). Facial hair and eyebrows are
|
||||
split from hair into their own directories because they are separate equip slots.
|
||||
|
||||
### Why this structure
|
||||
|
||||
**Bodies are directories, not flat files.** Each body type produces 21 segment GLBs (17 base
|
||||
+ 2 swappable torso variants + 2 face detail). Putting them in flat `bodies/` with prefixes
|
||||
(`thin_m_seg_head.glb`) would create 231 files in one directory. Directories per body type
|
||||
keep segment files scannable and let the compositor construct paths programmatically:
|
||||
`bodies/{body_type}/seg_{segment}.glb`.
|
||||
|
||||
**Clothing items are directories.** Each item has 11 body-type variants plus the reference
|
||||
mesh, the recolor mask, and coverage.json. That is 14 files per item. A flat structure would
|
||||
be unreadable by the time you have 20 items. One directory per item keeps variant lookup
|
||||
trivial: `clothing/{item_id}/{body_type}.glb`.
|
||||
|
||||
**Skin tones are shared, not per-body-type.** The toon shader applies skin tone via texture
|
||||
tinting. The same base skin tone texture works across all 11 body types because the shader
|
||||
controls the color, not the texture. One set of 9 textures, not 99.
|
||||
|
||||
**Heads are a curated template library.** All heads live in `heads/templates/` -- there is no
|
||||
distinction between Quaternius OnlyHead .blends and Trellis-generated heads. They are all
|
||||
equal templates. The starter set is 4 Quaternius OnlyHead .blends (Regular_Female,
|
||||
Regular_Male, Teen_Female, Teen_Male). More are added over time via the offline Trellis
|
||||
pipeline: generate candidates, curate the good ones, land them in `templates/`. Trellis head
|
||||
generation is an offline asset production tool, not a runtime or worldgen process.
|
||||
|
||||
---
|
||||
|
||||
## 3. Naming Conventions
|
||||
|
||||
### Body Types (the 11-value enum)
|
||||
|
||||
D-159 defines: Thin, Average, Muscular, Heavy, Teen (x2 genders) + Child.
|
||||
|
||||
| D-159 Name | File key | Description |
|
||||
|---|---|---|
|
||||
| Thin Male | `thin_m` | Ectomorph, narrow shoulders, long limbs (forked from Regular) |
|
||||
| Thin Female | `thin_f` | Ectomorph, narrow frame (forked from Regular) |
|
||||
| Average Male | `average_m` | Regular proportions (canonical/default, reference body for clothing) |
|
||||
| Average Female | `average_f` | Regular proportions |
|
||||
| Muscular Male | `muscular_m` | Broad shoulders, thick limbs (from Superhero .blend) |
|
||||
| Muscular Female | `muscular_f` | Broad shoulders, thick limbs (from Superhero .blend) |
|
||||
| Teen Male | `teen_m` | Adolescent proportions (from Teen .blend) |
|
||||
| Teen Female | `teen_f` | Adolescent proportions (from Teen .blend) |
|
||||
| Heavy Male | `heavy_m` | Wide torso, thick midsection (forked from Regular) |
|
||||
| Heavy Female | `heavy_f` | Wide torso, thick midsection (forked from Regular) |
|
||||
| Child | `child` | Gender-neutral, forked from Teen, scaled down |
|
||||
|
||||
These keys are used everywhere: directory names, GLB filenames, enum values in code. Lowercase,
|
||||
underscore-separated, no abbreviations beyond `m`/`f`. The GDScript and Rust enum should use
|
||||
`ThinM`, `ThinF`, `AverageM`, `AverageF`, `MuscularM`, `MuscularF`, `TeenM`, `TeenF`,
|
||||
`HeavyM`, `HeavyF`, `Child`.
|
||||
|
||||
### Body Segments (the 17 base + 2 swappable torso variants + 2 face detail)
|
||||
|
||||
From the spike validation and D-160. Fine-grained segmentation is required for per-limb
|
||||
addressability: amputation (hide limb, show stump cap), prosthetics (replace limb with
|
||||
alternative mesh), injury overlays (per-segment visual state), and clothing coverage (hide
|
||||
segments under clothing).
|
||||
|
||||
These are the segment suffixes in filenames:
|
||||
|
||||
| Segment | File suffix | Bones covered | Notes |
|
||||
|---|---|---|---|
|
||||
| Head | `seg_head` | Head | |
|
||||
| Neck | `seg_neck` | Neck | |
|
||||
| Torso (full) | `seg_torso` | Spine, Spine1, Spine2 | Default; swappable |
|
||||
| Torso (upper only) | `seg_torso_upper` | Spine1, Spine2 | For low-cut shirts, tank tops |
|
||||
| Upper arm left | `seg_arm_upper_l` | LeftArm (shoulder to elbow) | |
|
||||
| Upper arm right | `seg_arm_upper_r` | RightArm | |
|
||||
| Lower arm left | `seg_arm_lower_l` | LeftForeArm | |
|
||||
| Lower arm right | `seg_arm_lower_r` | RightForeArm | |
|
||||
| Hand left | `seg_hand_l` | LeftHand + finger bones | |
|
||||
| Hand right | `seg_hand_r` | RightHand + finger bones | |
|
||||
| Upper leg left | `seg_leg_upper_l` | LeftUpLeg (hip to knee) | |
|
||||
| Upper leg right | `seg_leg_upper_r` | RightUpLeg (hip to knee) | |
|
||||
| Lower leg left | `seg_leg_lower_l` | LeftLeg (knee to ankle) | |
|
||||
| Lower leg right | `seg_leg_lower_r` | RightLeg (knee to ankle) | |
|
||||
| Foot left | `seg_foot_l` | LeftFoot + toe bones + ankle overlap | Extends past ankle bone; 1-ring vertex overlap with lower leg segment |
|
||||
| Foot right | `seg_foot_r` | RightFoot + toe bones + ankle overlap | Extends past ankle bone; 1-ring vertex overlap with lower leg segment |
|
||||
| Eyes | `seg_eyes` | Attached to Head bone (small mesh) | |
|
||||
| Eyebrows | `seg_eyebrows` | Attached to Head bone (small mesh) | |
|
||||
|
||||
**Swappable variants (torso only):** The torso has TWO mesh variants. The compositor loads
|
||||
one or the other based on clothing coverage.json:
|
||||
|
||||
- **Torso:** `seg_torso` (full, default) or `seg_torso_upper` (upper chest only). Full-coverage
|
||||
garments hide seg_torso entirely. Partial-coverage garments (low-cut shirts, tank tops)
|
||||
swap to seg_torso_upper, exposing lower chest/abdomen.
|
||||
|
||||
**Legs use per-leg L/R segments split at the knee**, matching the arm pattern. This provides
|
||||
all needed granularity without swappable variants:
|
||||
- **Shorts:** clothing hides upper leg segments, lower legs show skin.
|
||||
- **Amputation:** hide one leg's segments individually.
|
||||
- **Prosthetics:** replace one leg's segments individually.
|
||||
|
||||
**Foot segments extend past the ankle bone.** Boots and shoes have geometry to cover the
|
||||
ankle region. Bare feet look natural with no visible seam. A 1-ring vertex overlap at the
|
||||
boundary with the lower leg segment above ensures smooth deformation at the joint.
|
||||
|
||||
The `seg_` prefix is load-bearing: it distinguishes segment GLBs from the `armature.glb` in
|
||||
the same directory and makes glob patterns simple (`seg_*.glb`).
|
||||
|
||||
Total per body type: 17 base segments + 2 swappable torso variants + 2 face detail = **21 GLB files**.
|
||||
|
||||
### Clothing Items
|
||||
|
||||
Format: `<category>_<descriptor>`
|
||||
|
||||
Categories:
|
||||
- `coveralls` -- full-body work suits
|
||||
- `jacket` -- upper body outerwear
|
||||
- `shirt` -- upper body inner
|
||||
- `pants` -- lower body
|
||||
- `boots` -- foot slot
|
||||
- `gloves` -- hand slot
|
||||
- `vest` -- sleeveless upper body
|
||||
- `dress` -- full-body single garment
|
||||
- `uniform` -- occupation-specific full outfit
|
||||
|
||||
Examples: `coveralls_basic`, `jacket_utility`, `pants_cargo`, `shirt_henley`, `boots_work`,
|
||||
`uniform_medic`, `dress_formal`.
|
||||
|
||||
The category prefix enables filtering in the character editor UI (show all jackets) and in
|
||||
the clothing catalogue data.
|
||||
|
||||
### Clothing Coverage (coverage.json)
|
||||
|
||||
Each clothing item directory contains a `coverage.json` sidecar that tells the compositor
|
||||
which body segments to hide and which torso variant to use:
|
||||
|
||||
```json
|
||||
{
|
||||
"hides": ["torso", "arm_upper_l", "arm_upper_r", "leg_upper_l", "leg_upper_r", "leg_lower_l", "leg_lower_r"],
|
||||
"torso_variant": "full"
|
||||
}
|
||||
```
|
||||
|
||||
The `hides` array uses segment names without the `seg_` prefix. The compositor hides the
|
||||
corresponding `seg_{name}` mesh when loading. Leg segments are individually addressable
|
||||
(upper/lower, left/right), so clothing specifies exactly which leg segments to hide.
|
||||
|
||||
Torso variant values:
|
||||
- `"full"` -- use `seg_torso` (default), or hide entirely if `"torso"` is in `hides`
|
||||
- `"upper"` -- swap to `seg_torso_upper`, exposing lower chest/abdomen
|
||||
|
||||
Example for a tank top:
|
||||
```json
|
||||
{
|
||||
"hides": [],
|
||||
"torso_variant": "upper"
|
||||
}
|
||||
```
|
||||
|
||||
Example for cargo shorts:
|
||||
```json
|
||||
{
|
||||
"hides": ["leg_upper_l", "leg_upper_r"],
|
||||
"torso_variant": "full"
|
||||
}
|
||||
```
|
||||
|
||||
### Hair
|
||||
|
||||
Format: `<visual_descriptor>.glb`
|
||||
|
||||
Source tier hairstyles are renamed from their Quaternius names to visual descriptors:
|
||||
- `Bob` -> `bob`, `Buns` -> `buns`, `Long` -> `long`, `Ponytail` -> `ponytail`
|
||||
- `SimpleParted` -> `simple_parted`, `SlickBack` -> `slick_back`
|
||||
- `BuzzedFemale` -> `buzzed_female`, `LongDreads` -> `long_dreads`
|
||||
|
||||
No category prefix needed -- hair is a single slot. The descriptor should be visually
|
||||
unambiguous at a glance.
|
||||
|
||||
### Accessories
|
||||
|
||||
Format: `<slot>_<descriptor>.glb`
|
||||
|
||||
Slots: `hat`, `goggles`, `mask`, `backpack`, `belt`, `earring`, `necklace`, `held_l`, `held_r`.
|
||||
|
||||
Each slot maps to a specific bone for BoneAttachment3D:
|
||||
- `hat` -> Head bone
|
||||
- `goggles` -> Head bone
|
||||
- `mask` -> Head bone
|
||||
- `backpack` -> Spine2 bone
|
||||
- `belt` -> Hips bone
|
||||
- `earring` -> Head bone
|
||||
- `held_l` -> LeftHand bone
|
||||
- `held_r` -> RightHand bone
|
||||
|
||||
Examples: `hat_hardhat`, `goggles_welding`, `backpack_small`, `held_r_wrench`.
|
||||
|
||||
### Recolor Masks
|
||||
|
||||
Convention: `<asset_name>_mask.png` as a sidecar file next to the GLB.
|
||||
|
||||
- `baroque_table.glb` + `baroque_table_mask.png` (existing Trellis convention from 3dpipeline spike)
|
||||
- `bob.glb` + `bob_mask.png`
|
||||
- `hat_hardhat.glb` + `hat_hardhat_mask.png`
|
||||
- For clothing, the mask lives in the item directory: `coveralls_basic/reference_mask.png`
|
||||
|
||||
Masks are per-asset, not per-variant. The same mask applies to all 11 body-type variants of a
|
||||
clothing item because the mask regions (collar is region A, sleeves are region B) correspond
|
||||
to UV regions, and the UV layout is preserved through Surface Deform fitting.
|
||||
|
||||
### Head Templates
|
||||
|
||||
Format: `head_NNN.glb` with zero-padded 3-digit numeric ID.
|
||||
|
||||
The starter set is 4 Quaternius OnlyHead .blends (Regular_Female, Regular_Male, Teen_Female,
|
||||
Teen_Male). Additional templates are produced offline via the Trellis pipeline: generate
|
||||
candidates, curate the best, export to GLB, and add to `templates/`. All heads are equal
|
||||
members of the template library regardless of origin.
|
||||
|
||||
Heads are unique per character in the fiction, so descriptive names are wrong -- these are
|
||||
catalogue entries. The number is an index into the head template library. The compositor
|
||||
always resolves `head_id` to `characters/heads/templates/{head_id}.glb` -- a single path,
|
||||
no branching.
|
||||
|
||||
---
|
||||
|
||||
## 4. Shared vs Per-Variant Breakdown
|
||||
|
||||
| Asset | Shared or per-variant | Count | Notes |
|
||||
|---|---|---|---|
|
||||
| Skeleton (armature.glb) | **Shared** | 1 | All body types, all clothing, all animations reference this one skeleton |
|
||||
| Animation libraries | **Shared** | 2+ GLBs | UAL1 (core) + UAL2 (extended + female mannequin); animations work on all body types via shared skeleton |
|
||||
| Shaders (toon, toon_masked, outline) | **Shared** | 3 files | One copy in `characters/shaders/` |
|
||||
| Skin tone textures | **Shared** | 9 PNGs | Toon shader tints these -- same textures for all body types |
|
||||
| Body segments | **Per body type** | 21 GLBs x 11 types = 231 | Each body type has its own segments (incl. swappable torso variants) |
|
||||
| Clothing items | **Per body type** | 11 GLBs + 1 reference + 1 mask + 1 coverage per item | Authored once on reference, batch-fitted to 11 |
|
||||
| Recolor masks (clothing) | **Shared per item** | 1 PNG per item | UV layout preserved through Surface Deform |
|
||||
| Coverage metadata | **Shared per item** | 1 JSON per item | Declares which segments to hide and torso variant selection |
|
||||
| Heads (templates) | **Shared library** | N GLBs + N masks | Curated set shipped with game; starter set of 4 Quaternius OnlyHead .blends, expanded via offline Trellis pipeline |
|
||||
| Hair | **Shared** | 1 GLB + 1 mask per style | Same mesh for all body types (BoneAttachment3D to Head bone); ~18 from Source tier |
|
||||
| Facial hair | **Shared** | 1 GLB + 1 mask per style | BoneAttachment3D to Head bone; beard, moustache, mutton chops from Source tier |
|
||||
| Eyebrows | **Shared** | 1 GLB per style | BoneAttachment3D to Head bone; 4 styles from Source tier |
|
||||
| Accessories | **Shared** | 1 GLB + 1 mask per item | Rigid bone attachment, body type irrelevant |
|
||||
|
||||
### The important asymmetry
|
||||
|
||||
Hair, facial hair, eyebrows, and accessories are body-type-agnostic. They attach to a single
|
||||
bone and do not deform with the body. This is the architectural win of BoneAttachment3D: one
|
||||
mesh works for all 11 body types with zero fitting work. The Source tier hairstyles arrive
|
||||
pre-rigged to the Head bone, making them nearly production-ready.
|
||||
|
||||
Clothing is the expensive axis. Every clothing item requires 11 fitted variants. The Surface
|
||||
Deform batch pipeline amortizes this, but the disk cost is real: 20 clothing items = 220
|
||||
GLBs + 20 masks + 20 reference meshes + 20 coverage JSONs = 280 files. This is manageable
|
||||
but worth tracking. At 50 items it is 700 files -- still fine for Godot's import system but
|
||||
a reason to keep the per-item directory structure clean.
|
||||
|
||||
---
|
||||
|
||||
## 5. Import Pipeline
|
||||
|
||||
### Source .blend to production flow
|
||||
|
||||
```
|
||||
Quaternius Source .blend files (docs/assets/downloads/Universal Base Characters[Source]/)
|
||||
|
|
||||
v
|
||||
Blender: fork/modify body shape (for thin, heavy, child -- 6 of 11 types)
|
||||
|-- OR: use as-is (average, muscular, teen -- 5 of 11 types)
|
||||
|
|
||||
v
|
||||
Blender: fine-grained segmentation (17 base + 2 swappable torso + 2 face detail = 21 GLBs)
|
||||
|
|
||||
v
|
||||
Export GLB (per segment / per clothing variant / per accessory)
|
||||
|
|
||||
v
|
||||
Recolor mask generation (Blender script or manual in image editor)
|
||||
|
|
||||
v
|
||||
Place files in client/assets/characters/<appropriate directory>
|
||||
|
|
||||
v
|
||||
Godot auto-import (gltf/embedded_image_handling=3 in project.godot)
|
||||
|
|
||||
v
|
||||
Runtime: CharacterVisual.gd loads by convention paths
|
||||
```
|
||||
|
||||
### Offline Head Production (Trellis)
|
||||
|
||||
Trellis is used as an offline asset production tool to expand the head template library
|
||||
beyond the 4 Quaternius starter heads. The workflow is: generate candidate heads in batch,
|
||||
review and curate for quality and diversity, export accepted heads as GLB + mask PNG, and
|
||||
place them in `client/assets/characters/heads/templates/` with the next sequential ID. This
|
||||
is a manual curation step, not an automated pipeline -- quality control matters more than
|
||||
volume.
|
||||
|
||||
### GLB Export Conventions
|
||||
|
||||
1. **One GLB per segment.** Each body segment is a separate GLB containing a single skinned
|
||||
MeshInstance3D and the skeleton it references. The skeleton must have the same bone names
|
||||
and hierarchy as `armature.glb`.
|
||||
|
||||
2. **Embedded textures.** GLBs use embedded textures (not external references). The project
|
||||
setting `gltf/embedded_image_handling=3` (embed uncompressed) must be set in `project.godot`.
|
||||
This was the critical finding from the 3dpipeline spike: values 0-1 fail silently, value 2
|
||||
compresses, only value 3 works correctly.
|
||||
|
||||
3. **Y-up, -Z forward.** Standard glTF orientation. Godot handles the conversion.
|
||||
|
||||
4. **Scale: 1 unit = 1 meter.** Consistent with Godot's physics defaults and the existing
|
||||
environment asset scale from the 3dpipeline spike.
|
||||
|
||||
5. **No animation data in segment/clothing/accessory GLBs.** Animations are in the separate
|
||||
animation library GLBs only. Exporting animations into every segment would bloat file size
|
||||
for no gain.
|
||||
|
||||
6. **Foot segments include ankle overlap.** The foot segment mesh extends 1 ring of vertices
|
||||
past the ankle bone boundary into leg territory. This overlap ensures smooth deformation
|
||||
and eliminates visible seams at the ankle. Boots and shoes have their own geometry covering
|
||||
this region.
|
||||
|
||||
### Mask Sidecar Convention
|
||||
|
||||
For every GLB that supports recoloring, a `_mask.png` sidecar sits next to it:
|
||||
|
||||
- **Format:** PNG, same UV layout as the GLB's texture
|
||||
- **Channels:** Greyscale (R channel only). White = tintable region, black = preserve original
|
||||
- **For multi-region recoloring:** Use distinct greyscale values per region (e.g. 0.0, 0.33,
|
||||
0.66, 1.0 for up to 4 independent tint regions). The shader samples the mask and routes
|
||||
each region to a different `tint_color` uniform. This is not yet implemented in the shader
|
||||
but the mask format supports it.
|
||||
- **Default:** `hint_default_black` in the shader means "no mask loaded = no tinting."
|
||||
Assets without masks render with their original Trellis/authored texture.
|
||||
|
||||
### Surface Deform Batch Output
|
||||
|
||||
The Blender batch pipeline takes one reference clothing mesh and produces 11 fitted variants.
|
||||
The output on disk looks like:
|
||||
|
||||
```
|
||||
clothing/coveralls_basic/
|
||||
reference.glb # Input: authored on average_m
|
||||
reference_mask.png # Input: recolor mask
|
||||
coverage.json # Input: segment coverage metadata
|
||||
thin_m.glb # Output: fitted variant
|
||||
thin_f.glb # Output: fitted variant
|
||||
average_m.glb # Output: copy of reference
|
||||
average_f.glb # Output: fitted variant
|
||||
muscular_m.glb # Output: fitted variant
|
||||
muscular_f.glb # Output: fitted variant
|
||||
teen_m.glb # Output: fitted variant
|
||||
teen_f.glb # Output: fitted variant
|
||||
heavy_m.glb # Output: fitted variant
|
||||
heavy_f.glb # Output: fitted variant
|
||||
child.glb # Output: fitted variant
|
||||
```
|
||||
|
||||
The batch script lives in `tooling/blender/` (not in the client tree). It takes arguments:
|
||||
- Input: path to reference GLB
|
||||
- Input: path to the 11 body type meshes (for Surface Deform targets)
|
||||
- Output: directory to write fitted variants
|
||||
|
||||
The reference mesh is authored on `average_m` because Average is the canonical/default body
|
||||
type (D-159). The `average_m.glb` variant in the output is a direct copy of the reference
|
||||
(convention: copy, not symlink -- see Section 9).
|
||||
|
||||
### Godot Import Settings
|
||||
|
||||
Required in `project.godot` (already set from spikes):
|
||||
|
||||
```
|
||||
[gltf]
|
||||
embedded_image_handling=3
|
||||
```
|
||||
|
||||
No per-file `.import` overrides are needed if this project-wide setting is correct. All
|
||||
character GLBs use the same import configuration.
|
||||
|
||||
---
|
||||
|
||||
## 6. CharacterVisualDescriptor (server-to-client interface)
|
||||
|
||||
The server sends a descriptor per character. The client compositor uses it to assemble the
|
||||
visual. This is the data contract between simulation and rendering:
|
||||
|
||||
```
|
||||
CharacterVisualDescriptor:
|
||||
body_type: BodyType # enum: thin_m, thin_f, average_m, average_f, muscular_m,
|
||||
# muscular_f, teen_m, teen_f, heavy_m, heavy_f, child
|
||||
head_id: String # template ID ("head_042") -- resolves to templates/ library
|
||||
hair_id: String # hair style key ("bob", "ponytail", "bald")
|
||||
hair_tint: Color # hair color via shader tint
|
||||
facial_hair_id: String # facial hair key ("beard", "moustache", "") -- empty = none
|
||||
facial_hair_tint: Color # facial hair color via shader tint
|
||||
eyebrow_id: String # eyebrow style key ("regular", "thick", "female", "teen")
|
||||
eyebrow_tint: Color # eyebrow color via shader tint
|
||||
skin_tone: int # index into the 9-tone palette
|
||||
clothing_slots: Dictionary # slot -> item_id (e.g. "torso" -> "coveralls_basic")
|
||||
clothing_tints: Dictionary # item_id -> Array[Color] for multi-region recolor
|
||||
accessory_slots: Dictionary # slot -> item_id (e.g. "hat" -> "hat_hardhat")
|
||||
accessory_tints: Dictionary # item_id -> Color
|
||||
```
|
||||
|
||||
The compositor resolves paths from this descriptor:
|
||||
- Body: `characters/bodies/{body_type}/seg_{segment}.glb`
|
||||
- Body (torso variant): `characters/bodies/{body_type}/seg_torso_upper.glb` (when coverage.json requests it)
|
||||
- Head: `characters/heads/templates/{head_id}.glb`
|
||||
- Hair: `characters/hair/{hair_id}.glb`
|
||||
- Facial hair: `characters/facial_hair/{facial_hair_id}.glb`
|
||||
- Eyebrows: `characters/eyebrows/{eyebrow_id}.glb`
|
||||
- Clothing: `characters/clothing/{item_id}/{body_type}.glb`
|
||||
- Accessories: `characters/accessories/{accessory_id}.glb`
|
||||
|
||||
Every path is deterministic from the descriptor. No lookup tables, no indirection. The
|
||||
directory structure IS the lookup table. The only additional data the compositor reads at
|
||||
runtime is `coverage.json` per equipped clothing item, which determines segment visibility
|
||||
and torso variant selection.
|
||||
|
||||
---
|
||||
|
||||
## 7. Disk Budget Estimate
|
||||
|
||||
| Category | Count | Files per | Total files | Size estimate |
|
||||
|---|---|---|---|---|
|
||||
| Skeleton | 1 | 1 | 1 | ~10 KB |
|
||||
| Animations | 3 (UAL1 + UAL2 + future) | 1 | 3 | ~4 MB |
|
||||
| Body segments | 11 types | 21 segs | 231 | ~2.2 MB |
|
||||
| Skin tones | 1 set | 9 | 9 | ~500 KB |
|
||||
| Head templates | 50 (v0.2) | 2 (GLB+mask) | 100 | ~5 MB |
|
||||
| Hair styles | 18 (v0.2, Source tier) | 2 (GLB+mask) | 36 | ~1.8 MB |
|
||||
| Facial hair | 3 (v0.2, Source tier) | 2 (GLB+mask) | 6 | ~300 KB |
|
||||
| Eyebrows | 4 (v0.2, Source tier) | 1 | 4 | ~50 KB |
|
||||
| Clothing items | 20 (v0.2) | 14 (11 variants+ref+mask+coverage) | 280 | ~22 MB |
|
||||
| Accessories | 15 (v0.2) | 2 (GLB+mask) | 30 | ~1.5 MB |
|
||||
| Shaders | 3 | 1 | 3 | ~5 KB |
|
||||
| **Total (v0.2)** | | | **~703** | **~37.4 MB** |
|
||||
|
||||
This is well within Godot's comfort zone. The increase from the earlier estimate comes from
|
||||
the per-leg L/R split at the knee (4 leg segments + 2 feet = 6 leg-related files per body
|
||||
type vs the old 2 combined + 2 feet = 4), swappable torso variants, coverage.json per
|
||||
clothing item, and the facial hair / eyebrow split. The import cache
|
||||
(`.godot/imported/`) will be larger due to Basis Universal compression and resource
|
||||
conversion, but that is gitignored and rebuilt on each machine.
|
||||
|
||||
---
|
||||
|
||||
## 8. Migration Path from Spikes
|
||||
|
||||
All body meshes start fresh from the Quaternius Source tier .blend files. No mesh work from
|
||||
the spikes carries over -- the spike's automated segmentation output is discarded.
|
||||
|
||||
What carries forward from spikes:
|
||||
|
||||
1. **Shaders** -- copy `toon.gdshader`, `toon_masked.gdshader`, `outline.gdshader` from spike
|
||||
to `client/assets/characters/shaders/`. These are proven and unmodified from spike.
|
||||
|
||||
2. **Import settings** -- `gltf/embedded_image_handling=3` and the per-file import
|
||||
configuration patterns proven in the quaternius-aesthetic spike.
|
||||
|
||||
3. **Architectural patterns** -- the slot system, CharacterVisualDescriptor structure,
|
||||
BoneAttachment3D attachment patterns, and segment visibility toggling from the spike's
|
||||
`character_visual.gd`. Move to `client/scripts/rendering/character_visual.gd` and update
|
||||
`res://` paths to match the new directory structure.
|
||||
|
||||
4. **Skin tones** -- copy `T_Skin_*.png` files from the spike to `client/assets/characters/skin_tones/`,
|
||||
renaming to match the convention (`T_Skin_pale_cool.png` -> `pale_cool.png`).
|
||||
|
||||
5. **Skeleton** -- extract `armature.glb` from the Source tier .blends (not from spike
|
||||
segmentation output) to `client/assets/characters/skeleton/armature.glb`.
|
||||
|
||||
What does NOT carry forward:
|
||||
|
||||
- **Body segment meshes** -- all 11 body types are segmented fresh from Source .blends.
|
||||
- **Fitted clothing variants** -- re-fitted against the new body meshes.
|
||||
- **Spike hairstyles** -- Source tier hairstyles replace any spike hair work.
|
||||
|
||||
---
|
||||
|
||||
## 9. Resolved Conventions
|
||||
|
||||
**Copy, not symlink.** The `average_m.glb` in a clothing directory is a copy of
|
||||
`reference.glb`, not a symlink. Symlinks are fragile across platforms (Windows). A copy adds
|
||||
~50KB per clothing item but is reliable. The batch script produces it automatically.
|
||||
|
||||
## 10. Open Questions for Review
|
||||
|
||||
**Q: Multi-region recolor masks -- implement now or defer?**
|
||||
The current `toon_masked` shader supports single-region (binary mask). Multi-region requires
|
||||
a shader update to sample distinct greyscale bands and route them to separate tint uniforms.
|
||||
Recommendation: defer to when the first multi-region clothing item is authored. The mask PNG
|
||||
format supports it already (greyscale values), so no re-export is needed later.
|
||||
|
||||
**Q: Teen clothing variants -- separate catalogue or shared with adult?**
|
||||
Teen body types use the same clothing system (Surface Deform fitting) but teen proportions
|
||||
may make some adult garments look wrong. Options: (a) all clothing items get teen variants
|
||||
automatically, accept minor visual issues; (b) tag clothing items as adult-only or all-ages
|
||||
in coverage.json; (c) separate teen-specific clothing items. Recommendation: (a) for v0.2,
|
||||
revisit if visual quality is insufficient.
|
||||
|
||||
---
|
||||
|
||||
## Decisions Referenced
|
||||
|
||||
- **D-159:** 5 adult body types x 2 genders + 1 child = 11 base meshes (updated: added Teen)
|
||||
- **D-160:** Body meshes segmented into 17+ bone-group regions with swappable torso variants and per-leg L/R split
|
||||
- **D-161:** Head is always a separate mesh on the Head bone
|
||||
- **D-162:** Clothing is pre-baked per body type via Surface Deform
|
||||
- **D-163:** Trellis generates heads via BoneAttachment3D (offline asset production, curated into template library)
|
||||
- **D-164:** Fork Quaternius skeleton, replace all body meshes (Source tier .blends as starting point)
|
||||
Reference in New Issue
Block a user