# 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 treats **any untextured surface on a character** as the model's own line-work and handles it flat and unshaded: - **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, or with black eyes that should have irises, this function and its name-matching are where to look — the naming conventions vary by source and this is the one place they are read. ## 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`.