# Stylization — the cel-shaded look Two passes, applied at load in `SkinnedPlayerModel.load_model`: ```gdscript 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`.