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]>
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_namestarts witheyes) → flat ink, except anything withHLin 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.