Complete character pipeline spike validating the Quaternius rig as foundation for The Settled Reach's 3D character system. Validated: - 65-bone skeleton + Universal Animation Library as rig foundation - Body segmentation into 15 bone-group regions with 1-ring vertex overlap - Trellis-generated heads via BoneAttachment3D - Skin tone texture generation pipeline (9 variants from source) - Toon shader + inverted hull outline at gameplay zoom - CharacterVisual class as compositor prototype Failed (documented): - Trellis clothing auto-rigging (sculptures, not garments) - Bone scaling for body type variants (catastrophic joint deformation) - Runtime per-segment clothing scaling (same failure as Blender-side) Includes: Blender pipeline scripts (segmentation, auto-rigging, Surface Deform fitting), Godot showcase with interactive controls, automated screenshot cycle, smoke tests, team reviews, and VERDICT.md. D-158 through D-164 locked. Q-060 through Q-062 opened. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
21 KiB
TYRE Review: Quaternius Aesthetic Spike -- Phase 1 & 2
Date: 2026-03-18 Reviewer: TYRE (Technical Architect) Status: Review complete, recommendations for Phase 3
1. CharacterVisual Class Architecture
cracks knuckles
Let me be honest about what this means technically. The CharacterVisual class is a solid spike prototype -- it proves the pipeline works. It is not the right architecture for production. Here is what needs to change and why.
What works well
- Single ownership model. One class owns the body, skeleton, outline state, and shader application. The caller never touches materials. This is the right boundary and should be preserved.
- Signal-based state notification.
outfit_changed,shader_mode_changed,outline_changed-- clean observer pattern, no polling. - Animation remap. The path substitution approach for cross-scene animation libraries is exactly the right technique. Lean, no runtime overhead beyond the initial copy.
- Meta-based original material cache. Using
set_meta("_orig_mats", ...)to preserve originals before shader override is pragmatic. For a spike, fine.
What needs to change for production
Tier 1 -- must fix before integration:
-
Outline via duplicate() is architecturally wrong. See section 2 below. This is the single biggest technical debt item.
-
No slot system. The current outfit model is "attach meshes, detach all." Production needs named slots:
head,torso,legs,feet,hands,accessory_1,accessory_2. Each slot holds at most one equipment piece. Detaching should be per-slot, not all-or-nothing. Without slots, there is no way to mix-and-match a hat from one outfit with boots from another. -
Body visibility toggle is wrong.
set_body_visible()currently uses an AABB size heuristic (< 0.1) to keep eyes visible. This is fragile. Once the head is separated (see section 5), this becomes a proper slot: body, head, hair, eyes. The heuristic goes away. -
No resource pooling. Every
load()call instantiates a new scene. With 30-80 active NPCs (D-026), you need aCharacterVisualPoolor at minimum sharedPackedScenereferences.load()returns cached resources in Godot, butinstantiate()is the expensive call -- scene tree construction. For crowd rendering, we will want instanced mesh approaches or LOD switching. -
Shader parameters are hardcoded.
tint_colorat(0.8, 0.7, 0.6),shadow_strengthat0.2-- these should come from a configuration resource (aCharacterStyleResource class) so skin tone, clothing color, and shadow intensity are data-driven. The recolor_mask pipeline in toon_masked.gdshader already supports this; the GDScript just needs to wire it up.
Tier 2 -- needed before ship, not urgent:
-
Animation state machine.
play_animation()with a raw string name will not scale. Production needs an AnimationTree with a state machine -- idle, walk, run, interact, combat, each with transitions and blend times. The animation remap logic stays, but the playback API needs to go through a state machine. -
No LOD. At crowd zoom (15.0 ortho size), the character is ~40 pixels tall. Rendering the full 65-bone skinned mesh with toon shader and outline at that scale is wasteful. Three levels: full mesh + outline (heavy/gameplay), simplified mesh (gameplay distant), billboard sprite or imposter (crowd).
-
No serialization. CharacterVisual takes paths as strings. For save/load and for the server-driven character compositor, we need a
CharacterVisualDescriptorresource/struct that fully describes the visual state: body type, skin tone, hair, each equipment slot, and shader mode. This is what the server sends and what gets serialized.
Tier 3 -- design for now, build later:
-
Accessory attachment points. Beyond clothing slots, characters need bone-attachment-point accessories: held items, backpacks, shoulder pets. The Quaternius rig has the bone structure for this. The API should be
attach_accessory("right_hand", item_scene)with bone-following. -
Dynamic skin/hair color. Per-character color variation via shader uniforms. The toon_masked shader already has
tint_color; we need a skin-region mask on the body texture so skin tone can be varied per-NPC without re-texturing.
Verdict
The class is a clean prototype. Production needs a CharacterCompositor (server-side descriptor) + CharacterVisual (client-side renderer) split. The compositor tells the visual what to show; the visual owns the rendering. The current class maps cleanly onto the visual half. Feasible refactor -- it is additive, not a rewrite.
2. Outline Approach
The current approach: mi.duplicate() to create a copy of every visible
MeshInstance3D, apply the outline shader as material_override, parent it
alongside the original.
Why duplicate() was necessary in the spike
Creating a new MeshInstance3D and assigning .mesh and .skin did not
preserve the skinning bind state. The duplicate copies the internal GPU
buffer references that make skinning work. This is a known Godot 4 behavior
-- skin bind data is part of the MeshInstance3D state, not the Skin resource
alone.
Why it is wrong for production
-
Doubles the node count for every visible mesh. With 8 body parts per character and 30-80 active NPCs, that is 240-640 extra scene tree nodes just for outlines.
-
Subtree pollution.
duplicate()copies child nodes (BoneAttachment3D, other MeshInstance3D children). The_apply_material_recursivefix papers over this but does not eliminate the wasted nodes. The null material errors come from these phantom children. -
Rebuild cost.
_rebuild_outlines()is called on every outfit change, visibility toggle, and shader mode change. It queue_free's all outlines and rebuilds from scratch. This is O(n) scene tree churn.
Production solution: next_pass material chain
Godot 4's Material.next_pass is the correct approach. Instead of a duplicate
mesh, you set the outline shader as a next_pass on the primary material:
primary_material.next_pass = outline_shader_material
This renders the outline in a second draw pass on the same mesh, using the same vertex buffer, same skinning state. No duplicate nodes. No subtree copies. No null material errors.
Difficulty: Tier 1 (straightforward). The outline shader already uses
cull_front and render_mode unshaded, which are exactly the settings needed
for a next_pass inverted hull. The shader itself does not change. Only the
GDScript wiring changes -- instead of creating duplicate MeshInstance3D nodes,
you set next_pass on each surface's ShaderMaterial.
The one caveat: if any mesh surface uses material_override (which this code
does), next_pass on the override material works. If using
set_surface_override_material, next_pass needs to be set on each surface
override individually. Since _apply_toon already creates per-surface
ShaderMaterials, adding .next_pass = outline_mat to each is trivial.
This single change eliminates:
- The duplicate() hack
- The _apply_material_recursive workaround
- The null material errors
- The _rebuild_outlines rebuild churn
- Half the mesh node count
That is the kind of architectural win I get excited about -- one change fixes five problems.
3. Quaternius Fork Proposal
Jeroen proposes forking the Quaternius meshes and authoring our own baseline body types. Let me be honest about what this means technically.
Quaternius as a starting point: viable
What you get by forking:
- 65-bone rig that is already proven compatible with the Universal Animation Library. This is the expensive part to create from scratch. The rig topology, bone naming, weight painting -- this is weeks of skilled rigging work that you get for free.
- CC0 license means no restrictions on modification or redistribution.
- Mesh density is appropriate for the toon shader pipeline (not too high-poly to lose the toon look, not too low to look jagged at heavy zoom).
What you must change in the fork:
- Separate head from body mesh (see section 5).
- Add body type variations (see below).
- Add proper UV regions for skin/clothing recolor masks.
- Potentially add face detail geometry (currently very simple).
Body type scope: 8 types x 2 genders + children + elderly = ~20 body meshes
Scope-wise, this means authoring 20 body meshes, all on the same 65-bone skeleton. The skeleton itself stays constant -- only the mesh and skin weights change per body type. This is the correct approach. All animations work on all body types because the rig is shared.
Difficulty: Tier 2 (significant Blender work, but well-understood technique).
The Quaternius standard tier gives 3 body types (Superhero, Regular, Tiny). That is 3 out of 20 for free, though all 3 will need head separation and UV rework.
For the remaining 17, the approach is:
- Start from the closest existing body type
- Sculpt/reshape in Blender (proportional editing on the mesh)
- Re-paint skin weights where proportions changed significantly
- Export with identical bone names and hierarchy
Weight painting is the bottleneck. Proportional mesh editing is fast; getting the skin weights right on a reshaped body so that animations do not deform strangely is the laborious part.
Alternative: look elsewhere?
I do not recommend looking elsewhere. The Quaternius rig's value is:
- Proven animation library compatibility (we just validated this)
- CC0 (no license complications when forking)
- The toon shader works unmodified on the mesh density
- 65 bones is a good balance (enough for fingers/face, not so many that performance suffers at crowd scale)
Starting from a different rig means re-validating all of this, and the animation library is a massive time saver. The rig is the anchor. Keep it.
4. Clothing: Per-Body-Type vs Mesh Stretching
This is the core production pipeline question. Let me break down both options.
Option A: Per-body-type clothing authoring
Every clothing item is authored (or generated) for each body type. 20 body types x N clothing items = 20N meshes.
Pros: Perfect fit, no runtime artifacts, no deformation edge cases. Cons: Content explosion. If you want 50 clothing items, that is 1000 meshes. Even with Trellis generation, each variant needs validation.
Option B: Mesh stretching / transfer weights
One clothing mesh per item, runtime-deformed to fit different body types.
What Godot 4 supports:
- Skeleton-driven deformation works inherently -- if a clothing mesh is rigged to the same skeleton, the bones deform it the same way they deform the body. A shirt rigged to the Superhero body that is played on a Regular body will deform with the Regular body's bone transforms. The mesh will not perfectly conform to the new body surface, but it will follow the skeleton motion.
- Blend shapes (shape keys in Blender) can encode body-type-to-body-type deltas. The clothing mesh stores blend shape targets for each body type. At runtime, set the blend shape weight to morph the clothing to match the current body. This is the approach used by games like The Sims.
What Godot 4 does NOT support:
- Automatic mesh projection / shrinkwrap at runtime. There is no built-in "conform this mesh to that body surface" operation.
- Surface deform modifiers (Blender feature, not exportable to runtime).
My recommendation: hybrid approach
- Author clothing on a "medium" reference body (the Regular body type).
- Use blend shapes for body type adaptation. In Blender, create shape keys that morph the clothing from Regular to each other body type. Export as GLTF with blend shapes. At runtime, set the blend shape value to match the character's body type.
- Accept imperfect fit at extremes. The Tiny body type will have some clipping with clothing designed for Regular+blend-shaped-to-Tiny. At dramatic camera angle and gameplay zoom, this is likely invisible.
- Per-body-type authoring only for problem cases. If a specific item clips badly on a specific body type, author a variant. Do not do this by default.
Difficulty: Tier 2 for the blend shape pipeline setup. Tier 1 per item after
the pipeline exists. Blend shapes export cleanly from Blender to GLTF to
Godot 4. The runtime API is mesh_instance.set_blend_shape_value(index, weight).
This approach scales: 50 items x 1 mesh each (with blend shape targets) vs 50 items x 20 body variants. The content pipeline stays sane.
What about Trellis-generated clothing?
Trellis generates a single mesh. It will not have blend shapes. For Trellis-generated clothing, the pipeline would be:
- Generate mesh on reference body
- Auto-rig to skeleton (separate problem, but solvable)
- Run a Blender script to generate blend shape targets by projecting the mesh onto each body type variant (shrinkwrap modifier, automated)
- Export
Step 3 is automatable in Blender Python. This is the correct place for the Trellis-to-production pipeline to invest effort.
5. Head Separation
Jeroen wants the head as a separate mesh for idle look-around animation (subtle head turns independent of body). This is the right call for character expressiveness.
What this requires
In Blender (one-time per body type):
- Select the head vertices on the body mesh (everything above the neck seam).
- Separate by selection -- creates two mesh objects: body and head.
- Both objects remain parented to the same armature with the same vertex groups.
- The head mesh is driven by the
Headbone and its children (jaw, eyes, eyebrows if they have bones). - Seal the neck seam on both meshes -- add a ring of vertices at the cut line with matching UVs so there is no visible gap.
- Export both as separate meshes in the same GLTF, or as separate GLTFs sharing the same skeleton definition.
Difficulty: Tier 1 per body type. This is a 15-30 minute Blender operation per body type for someone comfortable with mesh editing. The tricky part is the neck seam -- if done carelessly, there will be a visible line where head meets body. A small overlap collar (head mesh extends slightly into the body mesh below the neck) avoids seam gaps during animation.
In Godot (CharacterVisual changes):
The head becomes its own slot in the compositor. The head MeshInstance3D is a child of the Skeleton3D, same as body and outfit meshes. For look-around:
- Apply a
SkeletonModification3DLookAtor manual bone pose override on theHeadbone to rotate toward a look target. - The animation system handles this via an AnimationTree's additive layer -- base animation plays on all bones, then a look-at override is blended additively on the Head bone chain only.
Difficulty: Tier 1 for the mesh separation. Tier 2 for the look-at system integration with AnimationTree. The look-at is a "design for it now, build it later" item -- separate the head mesh now, add the look-at behavior when the animation system matures.
What about the eyes floating in the 180-degree screenshot?
Shot 18 (Peasant, rear view) shows the eye meshes floating visibly above the outfit body because the base body is hidden but small meshes (eyes) are kept visible via the AABB heuristic. Once the head is its own mesh, this problem disappears -- the head (with eyes) is shown or hidden as a unit, and the outfit either includes a head or it does not.
6. Null Material Errors
The proper fix, in order of preference:
Fix 1: Use next_pass (eliminates the problem entirely)
If outlines use next_pass instead of duplicate(), there are no duplicate
nodes, no children to worry about, no null materials. The problem simply does
not exist.
Fix 2 (if duplicate is kept for any reason): duplicate(0) and strip children
Node.duplicate() accepts a flags parameter. duplicate(0) copies only the
node itself, not its children. However, this does not preserve all internal
state (specifically, it may not copy the skin binding). Testing required.
If duplicate(0) works:
var outline_mi := mi.duplicate(0) as MeshInstance3D
outline_mi.skin = mi.skin # explicitly copy skin
If it does not (likely -- the skin binding issue is why duplicate() was used in the first place), then:
Fix 3: duplicate() + explicit child cleanup
var outline_mi := mi.duplicate() as MeshInstance3D
# Remove all children that are not needed for skinning
for child in outline_mi.get_children():
outline_mi.remove_child(child)
child.queue_free()
outline_mi.material_override = outline_mat
This is cleaner than _apply_material_recursive because it removes the
source of null materials rather than patching over them.
Recommendation: Fix 1 (next_pass). Do not invest more time in duplicate() workarounds -- the approach itself is wrong.
7. Phase 3 Recommendation: Trellis Clothing Generation
What Phase 3 should validate
The question is not "can Trellis generate a mesh?" -- we know it can. The question is: can a Trellis-generated mesh be auto-rigged to the Quaternius skeleton and deform correctly with animations?
Proposed Phase 3 steps
-
Generate 3-5 clothing items via Trellis -- varied types (shirt, coat, hat, boots, gloves). Use the Quaternius body as a reference image prompt so Trellis generates items at roughly the right proportions.
-
Auto-rigging test. Import each Trellis mesh into Blender. Use the existing Quaternius skeleton. Attempt:
- Automatic weights (Blender's "Armature Deform > With Automatic Weights"). This works on meshes that roughly wrap a bone structure.
- Transfer weights from the body mesh to the clothing mesh (Data Transfer modifier in Blender). This is the more reliable approach -- it projects weights from the body surface onto nearby clothing vertices.
-
Animation deformation test. Apply the idle animation. Does the auto-rigged clothing follow the body without tearing, clipping, or exploding? Screenshot at gameplay zoom and heavy zoom.
-
Toon shader compatibility. Apply toon_masked shader with the Trellis atlas texture. Does it read correctly?
-
Pipeline automation estimate. If steps 2-4 work manually, estimate the effort to automate via Blender Python (headless batch processing of Trellis outputs through the rigging pipeline).
What I expect to find
Let me be honest: I expect partial success. Specifically:
- Auto-rigging will likely work for tight-fitting clothing (shirts, pants, gloves) because the vertices are close to the body surface and automatic weight transfer will assign reasonable weights.
- Auto-rigging will likely fail for loose/flowing clothing (capes, robes, skirts) because the vertices are far from the skeleton and automatic weights will assign them to the wrong bones or give garbage weights.
- Accessories (hats, belts, pouches) are the easiest case because they attach to a single bone and do not need complex weight painting.
This means the generative clothing pipeline is viable for fitted garments and accessories, and needs manual weight painting intervention for loose garments. That is still a massive win -- fitted garments are the majority of a clothing catalogue.
Decision gates after Phase 3
- If auto-rigging works on >= 3 of 5 items: green light the Trellis clothing pipeline. Invest in Blender Python automation.
- If auto-rigging works on 1-2 of 5: conditional proceed. Viable for accessories and fitted items only. Loose garments hand-authored or constrained to fitted silhouettes in the Trellis prompt.
- If auto-rigging works on 0 of 5: route closed. Clothing is hand-authored or sourced from packs, Trellis used for environment only.
Summary: Difficulty Tiers
| Item | Tier | Notes |
|---|---|---|
| next_pass outline (replaces duplicate) | 1 | Single afternoon. Fixes 5 problems. |
| Head mesh separation (Blender) | 1 | 15-30 min per body type |
| Null material fix (via next_pass) | 1 | Free with outline fix |
| Slot-based equipment system | 2 | 2-3 days. Needed before multi-piece outfits |
| CharacterVisualDescriptor resource | 2 | 1-2 days. Needed for server integration |
| Blend shape body type adaptation | 2 | Pipeline setup ~3 days, then Tier 1 per item |
| Animation state machine (AnimationTree) | 2 | 3-5 days. Can defer past v0.2 |
| LOD system for crowd rendering | 2 | 2-3 days. Needed when NPC count grows |
| Body type mesh authoring (17 remaining) | 2 | Significant Blender work, well-understood |
| Look-at head animation | 2 | 1-2 days after AnimationTree exists |
| Trellis auto-rig pipeline | 3 | Phase 3 will determine scope |
| Accessory bone attachment API | 3 | Design now, build when needed |
| Dynamic skin/hair color masks | 3 | Design now, build when content demands |
Bottom line
The spike proved the pipeline works. The Quaternius rig is the right foundation to fork. The outline approach needs to switch to next_pass before any production integration -- this is the highest-priority fix and the easiest win. Head separation is a prerequisite for the body type fork work. The blend shape approach for clothing is the scalable solution. Phase 3 should validate the Trellis auto-rigging path before committing to it.
Feasible. Challenging but doable. The architecture has good bones -- pun intended.