Files
Papay-Shooter/.claude/skills/character-pipeline/references/stylization.md
T
Nicholas ButzkeandClaude Opus 5 cd0d1b2d99 feat(characters): import the Quaternius mannequin as a selectable skin
A third playable character, built with the pipeline skill from a source
that was already in the repo: the animation library ships a rigged
Mannequin mesh on the exact 53-joint reference skeleton, CC0, so it needed
no download and retargets perfectly. 18 clips, 0.3% cross-leg bleed, 7% of
verts at four influences — a clean authored-weight import. Licence
recorded in mannequin.license.json as the other skins do.

It has no cloth chains, correctly: it is a mannequin and has neither hair
nor clothes.

Importing it turned up two real bugs, both of which would have hit any
flat-coloured or single-piece model:

- LevelMaterials.apply_character_look treated ANY untextured surface on a
  character as the model's own outline shell and hid it, so the mannequin
  rendered as a solid black silhouette — its body and joint materials are
  untextured flat colours, not ink. _is_line_work() now asks whether the
  surface is named eyes*, is drawn front-face-culled (the inverted-hull
  setup), or is near-black. Taila and Miku are unaffected: their materials
  are textured and never reach that branch. Verified by render.

- verify_character.py failed the build for having one mesh. That check
  cannot tell "the pipeline joined them" from "the artist authored one
  mesh" — Quaternius' mannequin is one piece on purpose. It is advisory
  now; the join path's two unambiguous signatures, cross-leg bleed and the
  4-influences-everywhere spread, are still hard checks.

Also restored Miku's description, which the re-import had blanked.

3 GLB skins selectable (6 with the built-in colour skins). Smoke 0
failures, 11/11 movement tests, cloth idle 0.024-0.078 deg/frame.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-26 13:25:26 -04:00

4.2 KiB

Stylization — the cel-shaded look

Two passes, applied at load in SkinnedPlayerModel.load_model:

LevelMaterials.apply_toon_recursive(scene)   # world-wide toon shading
LevelMaterials.apply_character_look(scene)   # character-only corrections

The trap: imported models bring their own line-work

Anime models exported from MMD/VRoid/Blender toon setups very often ship the outline as geometry — an inverted-hull shell of the mesh with a flat black, UNTEXTURED material, plus separate flat cards for the eye whites, irises and the pupil highlight. The mesh you import is not just the character; part of it is already the drawing.

Toon-lighting that shell is what put a white rim on every hair strand. It is an inverted hull whose normals face away from you; a lighting model that adds a rim term lights it brightly exactly where it is supposed to read as ink.

apply_character_look therefore looks for the model's own line-work and handles it flat and unshaded. "Untextured" alone is NOT the test — that made every flat-coloured model render as a black silhouette, because Quaternius' mannequin has two untextured materials (a yellow body, lilac joints) and both were hidden as though they were an outline shell. _is_line_work() asks three things instead: is it named eyes*, is it drawn front-face-culled (the classic inverted-hull setup), or is its albedo near-black. An ink shell is black; a flat-coloured character is any colour at all.

What it then does:

  • Outline hull → made fully transparent rather than deleted. Deleting a surface would renumber the rest and break the mesh's own skin bindings. The game draws its own outline.
  • Eye cards (resource_name starts with eyes) → flat ink, except anything with HL in the name, which is the glint in the pupil and really is white.

If a newly imported character comes out with a white halo, a black silhouette, or black eyes that should have irises, _is_line_work() and the name-matching below it are where to look. The conventions vary by source and this is the one place they are read.

Taila's eyes still render as black cards rather than amber irises. Her eye surfaces are untextured, and the glTF import hands every untextured surface a default near-white albedo, so colour cannot tell an iris card from a lash card on her — the name is all there is, and eyes* currently means "ink". Unfixed.

Materials on import: the unlit problem

Anime glTFs are very often exported "unlit": KHR_materials_unlit, a black baseColorFactor, and the real texture wired to emissiveTexture. Renderers honouring the unlit extension use base colour and ignore emission — so Blender reads black, never references the images, and imports with bpy.data.images empty. The character comes out a silhouette, and there is no node graph left to patch afterwards.

tools/gltf_fix.py rewrites the container before import: emissive becomes base colour, the unlit flag is dropped. It must run first — this is the first thing retarget.py::main does, before import_any.

fix_unlit_materials(meshes) then repairs anything left inside Blender.

What the toon pass does

apply_toon_recursive gives everything the game's banded ramp. apply_character_look then softens the banding on characters, because re-banding an already-shaded anime texture reads as gloss — the texture already contains its own shading and the second pass fights it.

Convention alignment

The Hoyoverse-class look is, broadly: a ramp texture indexed by NdotL for the body, a separate ramp and often a dedicated shader for the face, an inverted-hull outline whose width is vertex-colour-modulated, and specific handling for eyes and hair highlights. This project does the simplified version — one banded ramp plus a screen-space-ish ink treatment, and the model's own outline shell hidden in favour of the game's. The face is NOT specially shaded here; if a character comes out with harsh shadow shapes across the nose, that is the missing piece.

Outline thickness

Lives with the toon material in scenes/maps/level_materials.gd (CHARACTER_INK and the outline settings). This is the branch it was last touched on — feat/outline-thickness-and-tp-weapon-hold.