Track C: Codex with GPT-6.1-Terra end to end

agent
Codex 0.166 with GPT-6.1-Terra (xhigh)
model
gpt-6.1-terra
harness
Codex
started
2026-10-30 04:44 UTC
took
42 min · 5 rounds · 304 turns
verdict
3rd. Judged blind as X; third. Closest to the reference on the youth's joy.

user

+0:03

You are track C of a benchmark. Read ~/Projects/lowmarch-bench/brief/BRIEF.md and ~/Projects/lowmarch-bench/brief/track-c.md, then do the task they describe end to end, without asking questions. The materials are in ~/Projects/lowmarch-bench/brief/materials. Your commands run in a sandbox: you can write only to your worktree, $WORK and the body cache, which is all the task needs.

agent

+0:05

I’ll read the benchmark brief and track instructions, then carry out the requested work and verify the result within the provided workspace.

  • exec_commandcat ~/Projects/lowmarch-bench/brief/BRIEF.md (+2 more)

    command

    cat ~/Projects/lowmarch-bench/brief/BRIEF.md
    
    cat ~/Projects/lowmarch-bench/brief/track-c.md
    
    pwd && rg --files -g 'AGENTS.md' -g '!node_modules' -g '!vendor' ~/Projects/lowmarch-bench/c ~/Projects/lowmarch-bench/brief | head -100

    result, 99 lines

    Script completed
    Wall time 0.8 seconds
    Output:
    # Benchmark brief: improve our characters' expressions
    
    You are one of three tracks given this same task from the same starting point. Work alone; do not look at the other
    tracks' folders (`~/Projects/lowmarch-bench/{a,b,c}` and `work/{a,b,c}` other than your own).
    
    ## The project
    
    Lowmarch is a code-built anime character pipeline for a tactics game. Characters are VRoid models reshaped in code,
    exported as GLBs and drawn in the browser by our own "painted" renderer (a three.js page). Everything is drawn with an
    orthographic camera. The owner reviews characters in that page, the "armory".
    
    ## The task
    
    Make our two test bodies' facial expressions read better, in the armory's painted renderer:
    - the slender woman (`arm_f_slender`);
    - the youth (`arm_m_youth`).
    
    The expressions are baked from VRoid's 57 expression shapes at build time.
    
    **The reference** is two commercial anime rigs, Yin and Yang. They're for study only, and nothing of theirs may be
    copied into the project.
    - In `brief/materials/`:
      - `expressions-vs-yin-yang.png`: their expressions beside ours at the starting point, 9 expressions and 5 lip-sync
        mouths at 0 and 30 to 35 degrees;
      - their individual frontal renders (`yin-expressions/`, `yang-expressions/`);
      - frames from their product page;
      - `yin-yang-rig-review.md`: notes on how their rig and shading work;
      - `vroid-shapes.txt`: the VRoid shape list.
    - Study what makes theirs read, then improve ours with our own code and recipes.
    
    **The 14 expressions** are in `$WORK/expr_list.txt`: neutral, joy, smile, angry, sad, surprised, shout, wink, smug,
    and the lip shapes A, I, U, E, O.
    - You may change the recipes, the build code and the renderer.
    - Keep the names.
    - Neutral is every character's default face in the armory, so it must still look right.
    
    **Decide one open question and justify it:** should eyebrows be drawn on top of the hair (bangs) when the hair covers
    them? Some anime styles do this and many don't, and the reference rigs keep their bangs above the brows instead.
    Decide for our characters, implement your decision, and explain it.
    
    ## Your setup
    
    - **Before every shell command,** run `source ~/Projects/lowmarch-bench/kit/env-<track>.sh` (or chain it with `&&`).
      It sets:
      - `LOWMARCH_REPO`: your git worktree. Edit code only here; it's on branch `bench-<track>`.
      - `WORK`: your builds, shots, sheets and notes.
      - `KIT`: the shared tools. Don't edit them.
    - **Where the code is, in your worktree:**
      - `review/vroid-02/index.html`: the painted renderer. The shaders are `PAINT_VS`, the material fragment shader and
        `PAINT_COMP` (the composite pass with lines). Roles: face=1, body=2, hair=3, line=4, iris=5, white=6, highlight=7,
        brow=8, mouth=9.
      - `tools/vroid_portrait.py`: the `Portrait` class builds a face. It applies the expression shapes, then
        `reshape`, `face_normals`, `shadow_cards` and more.
      - `tools/vrm.py`: the VRM loader. Shapes are applied in `VRM.primitives`.
      - `tools/export_glb.py`: the GLB export and the page's material data (`scene_info`).
      - `docs/portrait-guide.md`: our accumulated lessons.
    - **A round:**
      1. Change code or recipes.
      2. If you edited `tools/vroid_portrait.py` (or `cage.py`, `fit.py`, `proportions.py`, `makehuman.py`, `anthro.py`,
         `heads.py`, `collide.py`), run `cd $LOWMARCH_REPO && python3 -I tools/cage.py arm_f_slender arm_m_youth`
         (about 2 minutes). Those files key the body cache, and the bodies come out identical.
      3. Build: `$KIT/build_expr.sh $WORK/expr_list.txt [names...]` (about 25 seconds per expression). Page-only changes
         need no rebuild.
      4. Shoot: `$KIT/shoot_expr.sh $LOWMARCH_REPO/review/vroid-02/index.html r<N>` (about 4 minutes for all 14). Shots
         land in `$WORK/shot_r<N>/<expr>/painted_<body>.png`: a frontal tile, then a 30 degree tile.
      5. Sheets: `$KIT/sheets.sh r<N> "Round N: what changed"` writes `$WORK/sheet_r<N>_woman.png` and
         `sheet_r<N>_youth.png` (before vs this round, frontal).
      6. Look at the sheets, and at the 30 degree tiles too: an expression must hold off-front.
    - **Rules for running things:**
      - Run long commands in the foreground with generous timeouts, not as background jobs.
      - Work only in your worktree and `$WORK`.
      - Don't touch `~/Projects/lowmarch`, the other tracks, the shared kit or anything published.
      - Don't install anything. Headless only: no GUI windows.
      - You may commit to your branch.
    
    ## Budget
    
    At most 6 rounds and 90 minutes of wall-clock time, whichever comes first. Note your start time in `$WORK/NOTES.md`.
    
    ## Deliverables
    
    - `$WORK/FINAL_woman.png` and `$WORK/FINAL_youth.png`: copies of your best round's sheets.
    - `$WORK/NOTES.md`:
      - each round: what you changed, why, what the sheets showed, and how long it took;
      - your eyebrow decision and why;
      - what still reads weakly.
    - Your code committed on your branch.
    - A final message under 200 words: what you changed, and the rounds and time you used.
    # Track C
    
    Your track letter is `c`: `source ~/Projects/lowmarch-bench/kit/env-c.sh`.
    
    Look at your sheets with your image viewing tool after each round and decide the next round from what you see. No
    outside reviewer.
    ~/Projects/lowmarch-bench/c
    
  • exec_commandsource ~/Projects/lowmarch-bench/kit/env-c.sh && date -Is && printf 'WORK=%s\nKIT=%s\n' "$WORK" "$KIT" && ls -la "$WORK" && cat "$WORK/expr_list.txt" (+2 more)

    command

    source ~/Projects/lowmarch-bench/kit/env-c.sh && date -Is && printf 'WORK=%s\nKIT=%s\n' "$WORK" "$KIT" && ls -la "$WORK" && cat "$WORK/expr_list.txt"
    
    source ~/Projects/lowmarch-bench/kit/env-c.sh && git status --short && rg -n 'expression|EXPRESSION|brow|wink|smug|surpris' tools/vroid_portrait.py tools/vrm.py tools/export_glb.py review/vroid-02/index.html | head -180
    
    source ~/Projects/lowmarch-bench/kit/env-c.sh && cat ~/Projects/lowmarch-bench/brief/materials/yin-yang-rig-review.md && cat ~/Projects/lowmarch-bench/brief/materials/vroid-shapes.txt

    result, 255 lines

    Script completed
    Wall time 1.3 seconds
    Output:
    2026-10-30T12:44:40+08:00
    WORK=~/Projects/lowmarch-bench/work/c
    KIT=~/Projects/lowmarch-bench/kit
    total 48
    drwx------ 10 severian severian 4096 Oct 30 12:44 .
    drwx------  6 severian severian 4096 Oct 30 12:42 ..
    dr-xr-xr-x  2 severian severian   40 Oct 30 12:44 .agents
    dr-xr-xr-x  2 severian severian   40 Oct 30 12:44 .aws
    drwx------ 37 severian severian 4096 Oct 30 12:44 .chrome-profile
    dr-xr-xr-x  2 severian severian   40 Oct 30 12:44 .codex
    dr-xr-xr-x  2 severian severian   40 Oct 30 12:44 .git
    drwx------  2 severian severian 4096 Oct 30 12:44 .tmp
    -rw-------  1 severian severian   21 Oct 30 12:44 START
    -rw-------  1 severian severian 8293 Oct 30 12:44 codex_events.jsonl
    -rw-------  1 severian severian   39 Oct 30 12:44 codex_stderr.txt
    -rw-------  1 severian severian  544 Oct 30 12:42 expr_list.txt
    drwx------ 16 severian severian 4096 Oct 30 12:42 shot_before
    drwx------  2 severian severian 4096 Oct 30 12:42 site
    # expression recipes: name|VRoid shapes baked at build (Fcl_* names, weight; ';' between; 'none' = neutral)
    # the 14 the sheets show; keep these names (front_sheet.py reads them). Edit the recipes freely.
    neutral|none
    joy|Fcl_ALL_Joy:1
    smile|Fcl_ALL_Fun:1
    angry|Fcl_ALL_Angry:1
    sad|Fcl_ALL_Sorrow:1
    surprised|Fcl_ALL_Surprised:1
    shout|Fcl_BRW_Angry:1;Fcl_EYE_Angry:1;Fcl_MTH_A:1
    wink|Fcl_EYE_Close_R:1;Fcl_MTH_Fun:1
    smug|Fcl_EYE_Close:0.3;Fcl_MTH_Up:0.7
    lip_A|Fcl_MTH_A:1
    lip_I|Fcl_MTH_I:1
    lip_U|Fcl_MTH_U:1
    lip_E|Fcl_MTH_E:1
    lip_O|Fcl_MTH_O:1
    review/vroid-02/index.html:52:.brand .eyebrow { font: 10px/1.3 var(--f-label); letter-spacing: 0.1em; text-transform: uppercase; color: var(--screen-dim); }
    review/vroid-02/index.html:126:      <div class="brand"><span class="eyebrow">Lowmarch · Model viewer</span><h1>Lowmarch cast</h1></div>
    review/vroid-02/index.html:174:        <li><b>Test: MakeHuman bodies with VRoid heads</b> (the first three in <b>Lineup</b>, labelled "MH", next to the VRoid soldier on the far left). Same soldier kit. The bodies come from MakeHuman's CC0 shape controls (a slim young soldier, a stocky 50-year-old, a huge brute); the head is VRoid's CC0 male head, with its anime eyes, expressions and hair, scaled to each body and set on its neck the way VRoid does it (the neck runs up inside the head). Faces vary through our face presets. Sleeves: the puffs are now sized to the arm's real thickness (they were buried in MakeHuman's thicker arms, which is what looked like muscle), and on these bodies cloth spans the hollows between muscles.</li>
    review/vroid-02/index.html:183:        <li><b>Builds and faces vary.</b> The soldier's kit is also worn by a stocky grey veteran and a slim woman. Builds run from petite (the thief, 1.4 m) to huge (the berserker), with a new overall height control and oversized hands, as the study's units have. Faces differ in shape (jaw, chin, brow, nose, cheeks), expression, hair and eye colour, skin tone, and beards and moustaches. The lineup's check sheet has rows of faces with helmets off and on.</li>
    review/vroid-02/index.html:243:const ROLE_ID = { face: 1, body: 2, hair: 3, line: 4, iris: 5, white: 6, highlight: 7, brow: 8, mouth: 9, cloth: 10, hair_b: 11 };
    review/vroid-02/index.html:256:  document.querySelector(".brand .eyebrow").textContent = PAGE.eyebrow || "Lowmarch";
    review/vroid-02/index.html:1371:// drawn into one image, which lands in #sheetOut as a PNG data URL (a headless browser reads it).
    tools/export_glb.py:2:browser 3D viewer.
    tools/export_glb.py:5:the viewer can toggle and x-ray them. Face, eye, line, brow, mouth and hair keep VRoid's textures
    tools/export_glb.py:18:TEXTURED = ("face", "iris", "white", "highlight", "line", "brow", "mouth", "hair")
    tools/export_glb.py:19:ROLE_TONE = {"face": "5", "body": "4", "shadow": "3", "hair": "D", "brow": "B", "coat": "M", "cape": "W", "metal": "8",
    tools/export_glb.py:31:    if role in ("face", "body", "shadow", "iris", "white", "highlight", "line", "brow", "mouth"):
    tools/export_glb.py:136:            if role in ("hair", "line", "brow", "iris", "highlight"):
    tools/export_glb.py:184:    """Everything the browser's pixel renderer and cloth solver need besides the meshes: palette
    tools/export_glb.py:196:             "line": dict(mode="flat", color=hexp("o")), "brow": dict(mode="flat", color=hexp("A")),
    tools/export_glb.py:418:                if role in ("hair", "line", "brow", "iris", "highlight"):
    tools/vrm.py:6:game except through those buffers (eye/brow/lash shapes come from alpha-tested texture masks).
    tools/vroid_portrait.py:4:palette tones (skin/hair/cloth bands from normals; eyes, brows, lashes from alpha-tested masks)
    tools/vroid_portrait.py:39:         ("FaceEyeline", "line"), ("FaceEyelash", "line"), ("FaceMouthLine", "line"), ("FaceBrow", "brow"), ("FaceMouth", "mouth"),
    tools/vroid_portrait.py:161:#  3. resting expression: VRoid's own shapes (hooded eyes, stern brows) held at a low weight
    tools/vroid_portrait.py:164:               face_len=0.0, jaw=0.0, chin_w=0.0, chin_len=0.0, cheek=0.0, brow_ridge=0.0,
    tools/vroid_portrait.py:170:SCULPT_ROLES = ("face", "body", "mouth", "brow", "line", "white", "iris", "highlight", "hair")
    tools/vroid_portrait.py:341:        move("ridge" + sd, np.array([0, -0.0015, -0.004]) * sh["brow_ridge"])
    tools/vroid_portrait.py:397:    instead of patches that jump as the head turns. The face, its brows, lines and mouth and the skull take it in
    tools/vroid_portrait.py:417:        if r in ("face", "brow", "line", "mouth"):
    tools/vroid_portrait.py:565:    (from the nose's wing round the mouth, under the eyes, crow's feet, a frown between the brows,
    tools/vroid_portrait.py:655:    # a frown between the brows
    tools/vroid_portrait.py:696:    """Face length, sculpting, eye size/tilt and mouth width (skeleton and expression are applied
    tools/vroid_portrait.py:825:            if self.roles[k] in ("line", "brow", "iris", "highlight", "white", "hair", "mouth"):
    tools/vroid_portrait.py:922:               if k in self.tex and self.roles[k] in ("line", "brow", "iris", "highlight", "white", "hair", "mouth")}
    tools/vroid_portrait.py:993:            elif r == "brow":
    tools/vroid_portrait.py:1154:            "line": hexrgb("o"), "brow": hexrgb("B"), "mouth": hexrgb("2"), "white": hexrgb("w"),
    # Yin & Yang rigs (DillonGoo Studios): review for lowmarch
    
    2026-10-30. EEVEE builds (`_Yin_EV_Rig_Master.blend`, `_Yang_EV_Rig_Master.blend`, saved by Blender 4.3), opened in Blender 5.2.3 headless with `--disable-autoexec`.
    
    ## Short answer
    
    **Not a base.** The licence is personal/educational only: no commercial use of the characters, no redistribution "in original or modified form" (links below). A game build would redistribute them. Technically they are also a poor fit: two fixed characters whose clothes are the body (Yin has no skin under the shirt and pants), a Rigify A-pose rig split over 3 armatures (~1,640 bones), and expressions done with bones, not shape keys.
    
    **The lessons are worth a lot, and the ideas are free to reimplement.** Ranked for "too sharp from some angles" and "hold at every angle":
    
    1. **Proxy shading normals ("Vertex Skeleton Normals").** The face is shaded from normals that point away from a simple line inside the head, blended in by a painted mask and blurred. They are not the mesh's own normals. The contact sheet shows the difference: rows 2 vs 3 and 5 vs 6, same lowmarch 4-band shader, rig normals vs mesh normals. With mesh normals the nose, mouth and cheeks break into noisy patches that change with yaw. With the proxy they make clean ovals at every yaw.
    2. **One proxy for the whole head.** Hair, brows, lashes, mouth bag, teeth and tongue all take the same head proxy normal, so the hair's shade edge carries on into the face's.
    3. **Few tones, low threshold.** Skin is 2 tones split at N·L > 0.1: only surfaces turned more than about 84° from the light go to shade. Everything else is authored: a painted shade mask and small shadow cards.
    4. **Authored shadow shapes.** A painted per-corner `Shadows` attribute forces shade where the artist wants it. Nose, lip and eyelid shadows are tiny separate meshes in the shade colour. Yang's nose card has two flat normals, one per side, so the shadow shows only on the side away from the light.
    5. **Line-only geometry.** `Eyelids` is 10 vertices and no faces. It exists only so Line Art draws the lid creases as 3D lines that turn with the head. Shading-only meshes are excluded from Line Art.
    
    ## Files and safety
    
    - Embedded texts, dumped and read: `scratchpad/rigs/texts/{Yin,Yang}/`.
      - `*_ui.py` (BODY 109/121 KB, FACE, PHYSICS) are stock Rigify-generated rig UI (Blender 4.3 template): panels, IK/FK snapping and bake operators. I diffed them line by line against Rigify's own sources. The only lines not in Rigify are layout calls, the rig_id and the 4.3-era fcurve API. The one `eval` is Rigify's standard `parse_bone_names`. There is no os/subprocess/socket/urllib/file I/O.
      - `DO_NOT_DELETE_META_DATA` is pure JSON of bone property defaults (no licence text).
      - `PUBLISH_SHOT_LOG` is the studio's publish checklist (W:\StudioGooThing paths, "Pack files").
      - `Text` is a manual cleanup script that clears ID props and drops empty vertex groups. It is not a module, so it never runs on load.
    - Drivers: about 800 per rig. Only 6 are Python expressions, all arithmetic: `1-((var-N)**2)` for the eye-look parent switch, and `(var + .1125) % 0.225 - .1125` for the hand macro wrap. The sun_vec/rim_vec/light_fx drivers on Teeth/Tongue are simple expressions, and their targets are empty in the EEVEE build (leftovers from the Goo Engine build).
    - I judged the texts safe for autoexec, but the permission classifier refused `--enable-autoexec`. All renders therefore ran with autoexec off. That skips only the 6 drivers above, which have no visible effect at rest pose.
    
    ## 1. Techniques, how each works
    
    ### Vertex Skeleton Normals (GN, render-only)
    Head stack: `Armature > Subdivision (2, custom normals) > Warp x2 > GN "Blur Threshold" > GN "Vertex Skeleton Normals" > Lattice (Flattice)`. Both GN modifiers are off in the viewport and on in render.
    
    The node group `Head Vertex Skeleton Normals` (input Blur Iterations, default 5; hands use 48) works like this:
    1. Store the mesh normal as point attribute `vs_nrm`.
    2. Node `Neck_VS` (group `Edge Skeleton Projection`) runs with object `Yin_Neck_Pos`, factor 0.95, influence attribute `NRM_Neck`. Node `Head_VS` runs the same group with object `Yin_Head_Pos`, factor 0.5, influence `NRM_Head`. Each skeleton object is a few vertices skinned to the body rig. `Yin_Head_Pos` is a single vertical edge from z 1.42 to 1.755 at y -0.069, inside the head and forward of the skull's middle.
    3. `Edge Skeleton Projection` takes the nearest point on the skeleton's edges Q (Geometry Proximity, EDGES) and the skeleton's mean point M (`Mean_Pos`). It forms `target = mix(M, Q, factor)` and computes `n = normalize(P - target)` (`Edge_Pos`). Then `vs_nrm = mix(vs_nrm, n, weight of the influence group)`.
       - Factor 1 gives capsule normals around the edge, factor 0 gives sphere normals around its centre.
       - The head uses 0.5: cylinder-like across the face, rounded over the crown and chin.
       - The neck uses 0.95: a cylinder.
    4. Blur `vs_nrm` over mesh neighbours N times, normalize, store.
    
    Every material reads `vs_nrm` (Attribute node) into the shared toon group. Because the GN runs after Armature and before the Flattice lattice, the normals follow the pose but ignore the camera-facing flattening.
    
    Who carries the influence weights:
    - NRM_Head: Head 1,436 of 1,671 verts at weight 1. Hair, Eyebrows, Eyelashes, Mouthbag, Teeth and Tongue carry NRM_Head on all their verts, so they shade from the same head proxy.
    - NRM_Neck: 263 verts.
    - Hands use `Hand Vertex Skeleton Normals` (finger skeleton, 20 edges) plus a `sphere_nrm` about the mesh centroid.
    - Clothes use per-garment skeletons (`Vertex Skeleton Shirt/Pants/Shoe*`). In the material they mix 70-90% `vs_nrm` with an object-space Normal Map fed by `sphere_nrm` (frame "Mix Normals Types").
    
    ### Toon shader (`Yin_Shader` / `Yang_Shader`, used by 16-18 materials)
    ```
    N = mix(TexCoord.Normal, vs_nrm, Normals=1)
    v = ShaderToRGB(Diffuse(N, roughness 0)) * (ShadeMap - attr "Shadows" - Backfacing)
    colour = v > 0.1 ? Lit : Shaded
    ```
    - There are two colours per material, set on the group node (Skin Lit 0.85/0.65/0.53, Shaded 0.59/0.30/0.21, linear).
    - `Shadows` is a painted corner colour attribute. 21% of Yin's and 26% of Yang's face corners are non-zero.
    - `thresh`, also painted, is blurred by GN `Blur Threshold` but isn't read in the EEVEE materials. It is presumably used in the Goo Engine build (unverified).
    - The EEVEE file still holds the Goo Engine groups `BASE_SunFace_Group` etc. They contain an undefined node "Shader Info" (half-lambert per light, multiplied by a ShadeMap, then a threshold), so the Goo Engine build shades per named sun with half-lambert. I didn't diff the GE files further.
    
    ### Lights
    - `Sun_Body`, `Sun_Face` and `Sun_Hair`: energy π, no shadows, all pointing +Y, i.e. lit from the character's front.
    - Each sun is parented to a rig bone (`Body_Sunvector`, `Head_Sunvector`, `Hair_Sunvector`), and `Hair_Sunvector` copies `Head_Sunvector`'s rotation.
    - The bones carry "Head space", neck_follow and head_follow switches, so a light can follow the head.
    - Light linking gives each sun its own receivers: Face takes head, teeth, eyes and shadow cards; Hair takes hair; Body takes clothes and hands.
    
    ### Shadow cards
    - Yin `Nose_Shadow` (9 v), `Lip_Shadow` (10 v), `Eyelid_Shadow` (30 v) use material `Skin_Shadow_Caster`: a constant Emission in the skin-shade colour. They are driven by face bones (`DEF-eyelid-shadow-*`) and excluded from Line Art.
    - Yang `Nose_Shadow` (38 v) is shrinkwrapped onto `Male_Face` (TARGET_PROJECT, 1 mm). GN `Geometry Nodes.005` sets `vs_nrm` to the mean normal of the "Left" group on the left half and of the "Right" group on the right. Material `NoseShaded` shows the shade colour where N·L < 0.1 and is transparent elsewhere, so the shadow is a flat shape that flips sides with the light.
    - Yang `Shorts_Shadows` is a constant shade band under the shorts.
    
    ### Warps and lattices
    - Head and both teeth meshes have `Warp`/`PG - Cheek Warp.L/R`: bone pair `cheek-warp-from.L` to `cheek-warp-to.L` (to is a child of from, same rest position), 3.5 cm smooth falloff. Moving the "to" bone pushes the cheek and the teeth follow, with no weights painted.
    - Tongue has three of the same.
    - Yang adds `Head_Squash_Lattice` with Hook_Top/Hook_Bottom for head squash and stretch.
    - Flattice:
      - 4x2x4 B-spline lattice whose `Flatten` key moves the two depth layers ±0.5 together.
      - Driven by bone prop `General Properties["Flattice"]` (default 0).
      - `Flattice_Empty` copies location from DEF-spine.006 and damped-tracks `Camera_Reference_Flattice_Empty`, whose location is driven to the scene camera's.
      - It sits last in the stack, after the normals.
      - Under our orthographic camera, squashing along the view axis changes only depth order, not the silhouette.
    
    ### Eyes
    `Eye_Reflection` is a 358-vert shell over the eyes (BLENDED, excluded from Line Art). It draws procedural highlights from SDF circles in UV space (`white_highlights`, `purple_highlights`) with an animated Wave-texture "wibble". Iris colour comes from one packed `Eye_Texture.png`.
    
    ### Line Art
    Three Grease Pencil objects, each with Line Art modifiers, `Screen Space Thickness` GN, Taper and Overall Thickness:
    - `LineArt.Body`: collection `Yin_Main_Body`, contour+crease+edge marks+intersection+loose.
    - `LineArt.Character`: separate modifiers for Eyelashes, Head, Head gap-fill, Teeth, Mouthbag, Eyelids and Eyebrows. Each has contour, edge marks and loose edges; crease is off on the face.
    - `LineArt.Hair`.
    
    Shading meshes are set to EXCLUDE. I couldn't evaluate Line Art headless (0 strokes), so the outlines on the sheet are my approximation from an ID/depth pass.
    
    ### Face rig (`*_Rig_FACE`, 648 bones, 141 deform)
    - Lips: lip MASTER/top/bot/corner plus tweak rings, `lip-zipper-smoother`, `jaw-yapper` and `jaw-influence`.
    - Eyes: 12 eyelid bones per eye, `MCH_Eye_Tilt_upper/lower`, eye-socket bones named after the orbital bones, `eye-look-MASTER` with 4-way parent switch, `eye-look-blink-macro`.
    - Brows: 5 brow bones and 5 brow-skin bones per side, plus `brow-shape-control`.
    - Also cheek warps, nose bones, `eyelid-shadow-*`, and `sync-control`/`sync-frame`.
    - Correctives: Eyelashes `COR - Blink.L/R` (driver `var / -0.025` on blink target Z), shirt elbows, pants knees/butt.
    
    ### Body and hair
    - `*_Rig_BODY` is Rigify (590/584 bones, 176/172 deform) with knee/elbow helper bones, finger "Fatten"/"Knuckle" deform bones and `Palm_squish`. A hand-macro control curls the fist from one slider.
    - `*_Rig_PHYSICS` holds the hair chains (Yin 68 deform, Yang 50): FK + Tweak + `-PHYSICS` bones joined by Copy Rotation/Transforms. `wiggle_enable` props suggest the Wiggle 2 add-on (unverified, not installed).
    
    ## 2. What transfers to lowmarch, ranked
    
    lowmarch shades `lit = n @ LIGHT` from interpolated vertex normals (`tools/vroid_portrait.py` render(), LIGHT about (0.08, 0.06, 1) in camera space) into 4 skin bands (0.80/0.62/0.38).
    
    1. **Proxy face normals (highest value).** After posing, for face, hair, brow, line and mouth vertices:
       - Take the head segment from the head bone (neck-top to crown), set about 0.25 of the head's depth forward of the skull centre.
       - `target = lerp(centroid, closest_point_on_segment(P), 0.5)`, `n_proxy = normalize(P - target)`.
       - `n = normalize(lerp(n_mesh, n_proxy, w))`, then 5 rounds of neighbour averaging over the mesh's edges and normalize. Pass `n` to `interpolate(prims, buf, "nrm")` in place of the stored normals.
       - The weight `w` is per template vertex. All 26 VRoid faces share the skin triangles, so the mask is authored once by index (doc lesson 29): 1 on the face and hair, falling to 0 at the ears and the seam to the skull.
       - Add a neck capsule (factor 0.95) so the jaw-over-neck edge stays clean.
       - The sheet's rows 2/3 and 5/6 are this exact A/B under our bands and headlight.
    2. **Same proxy for hair, brows, lashes and mouth.** It is free once (1) exists. The hair's band edges then line up with the face's, and the forehead hair-shadow rule in `ink()` stops fighting the face bands.
    3. **Fewer skin tones.** Try 2 skin tones (maybe 3), with the split at a low N·L (0.1 to 0.3) on proxy normals, and keep 4 bands for cloth and metal. With a near-frontal light this leaves faces mostly flat lit with a shade rim, the look these rigs get. Four bands on a near-sphere make concentric rings (row 2), which read "3D", not anime.
    4. **Authored shade.**
       - A per-template-vertex "forced shade" weight (under the chin, eye sockets, below the lower lip), subtracted before banding as in `Yin_Shader`.
       - Nose and lip shadows as small fixed shapes in UV space, drawn like `MARK_WINDOW` marks, with a two-sided nose shadow chosen by the sign of the light's x in head space (Yang's `NoseShaded`).
    5. **3D line geometry for face lines.** Lid creases and the nose line as polylines on the template, projected each frame (as `draw_curve_mouth` already does for the mouth) rather than texture marks. They keep their place at every yaw.
    6. **Per-part light.** A face light defined in head space (or camera space but clamped so the face never goes mostly dark at yaw 90), separate from the scenery SUN and the body light. The rigs do this with bones and light linking.
    7. **Shading before shape tricks.** Compute normals before any view-dependent squash (Flattice), so shape fixes don't move shade edges. Flattice itself does nothing to an orthographic silhouette.
    8. **Warp-style face controls.** A bone-pair Warp (3.5 cm smooth falloff) is the same idea as our sculpt field. There's nothing new for us beyond confirming the approach.
    
    ## 3. As a base
    
    | | Yin / Yang | lowmarch VRoid |
    |---|---|---|
    | Mesh | Quad cages + Catmull-Clark level 2 at render. Head 1,671 v / 1,750 f (Yang 1,703 / 1,752), hair 1,018 (Yang 289+557+597), shirt 2,734. About 28k faces on the head after subdivision. | Triangulated game mesh. Face about 3.2k-4.2k tris on one shared template, body about 11.6k tris. |
    | Body | Clothes are the body. Yin: shirt, pants, hands, socks, shoes, no skin under them. Yang: arm and leg skin meshes masked under the clothes. | Nude base, costumes in code. |
    | Height | Yin 7.3 heads (crown 1.675, chin 1.444). Yang about 7.8 heads by the face mesh crown (1.632, chin 1.423). | arm_f_slender 7.3 (crown 1.661, chin 1.434); arm_m_youth 6.5 (1.623 / 1.375). |
    | Eye line, down from crown | Yin 0.58, Yang 0.55 (eyeball centres). | 0.56-0.64 (docs/vroid.md); 0.61 on both armory heads. |
    | Expressions | Bones (141 face deform bones) plus a few correctives. | 57 VRoid blendshapes. |
    | Rig | Rigify, 3 armatures, A-pose, many MCH/helper bones. | VRM humanoid, T-pose, spring bones + spheres. |
    
    - **Pose mapping:** feasible but not drop-in.
      - DEF-spine to `.006` map to hips, spine, chest, upperChest, neck and head.
      - DEF-shoulder, DEF-upper_arm (+.001 twist), DEF-forearm (+.001), DEF-hand, 3-bone fingers and thumb map to the arms and hands. DEF-thigh/shin/foot/toe map to the legs.
      - The A-pose rest needs a retarget to VRM's T-pose. The face and hair armatures must be merged.
    - **Spring bones:** hair spring bones could hang on the `DEF-Hair_*` chains (3 to 13 bones each).
    - **Adapting to archetypes:** poor. Two characters only, clothing fused to the silhouette, and the face rig and its warps and shadow cards are tuned to these two faces.
    - **Face design:** Yang sets the eyes higher than any VRoid sample (an older look, which our set lacks per lesson 30). The shapes are a reference to study, not to ship.
    
    **Licence** (verified 2026-10-30 on the listings):
    - [Gumroad bundle](https://dillongoo.gumroad.com/l/yin-yang-anime-twins-rig-pack), [Yin](https://dillongoo.gumroad.com/l/yin-anime-girl-rig), [Yang](https://dillongoo.gumroad.com/l/yang-anime-boy-rig): "non-exclusive right to use the assets on a royalty-free basis for personal or educational use". "You may not sell, share or otherwise redistribute the assets in original or modified form". "you cannot use the character 'Yin' or 'Yang' for your own commercial projects". Personal projects such as demo reels are fine with credit to DillonGoo Studios.
    - [Superhive](https://superhivemarket.com/products/yin-yang-twins-anime-rig-pack) lists License: **Editorial** and the same text. Its [FAQ](https://superhivemarket.com/products/yin-yang-twins-anime-rig-pack/faq): "If the game or movie is a commercial project (intended to make money), then the answer is no". Modifying for VRChat is fine, but "you are not allowed to redistribute the model".
    - So: modifying is allowed for personal use only. Shipping in a commercial game is not allowed. Redistributing in a build is not allowed, even modified.
    - Unverified: whether a character "built on" the mesh but substantially changed escapes this. Treat it as a derivative (no). Reimplementing the techniques (normals from a skeleton, two-tone thresholds, shadow cards) in our own code copies no asset. I am not giving legal advice.
    - The FAQ text mentions "Emily" in places, so it's a shared FAQ across their rigs.
    
    ## 4. Renders
    
    - No EGL or Vulkan here, so EEVEE can't run headless. Renders are Cycles CPU via `scripts/cycles_turn.py`:
      - Each `Diffuse > Shader to RGB` is replaced by `max(0, dot(N, L))`. That is equal to EEVEE for these shadowless suns of strength π on white diffuse.
      - Grease Pencil is removed and outlines are drawn from an object-ID and depth pass (`scripts/sheet.py`).
      - Ortho camera, scale 0.56 m, 640 px, light as noted per row.
    - lowmarch heads: `scripts/lm_turn.py` calls `vroid_portrait.Portrait.render` and `smooth_view` on the newest `arm_f_slender` and `arm_m_youth` cage VRMs, read-only. lowmarch's positive yaw turns the other way from mine.
    - **Contact sheet:** `/tmp/claude-1000/-home-severian-Projects/d2fdcea1-0c48-4814-81a2-89cf837a9b4f/scratchpad/rigs/out/contact_sheet.png` (and `.jpg`). Per-yaw PNGs are `out/{yin,yang}_{ship,bands,bands_novs}_yNN_lined.png` and `out/lm_*`.
    - Rows on the sheet:
      - 1 and 4: as shipped (2 tones, sun from the front, fixed in the world).
      - 2 and 5: our 4 bands with our headlight on the rig's normals.
      - 3 and 6: the same on the mesh's normals.
      - 7 to 10: lowmarch.
    
    ## Not verified
    
    - The look in EEVEE and in Goo Engine. Line Art strokes (they didn't evaluate headless).
    - The 6 Python drivers (autoexec refused).
    - Whether GN modifier inputs differ from the group defaults (Blur Iterations 5 head, 48 hands). The 5.2 API wouldn't read them.
    - The Wiggle 2 dependency. Goo Engine-only shading (`thresh`, Shader Info half-lambert).
    
    ## Addendum, 2026-10-30
    
    The owner ran `cycles_turn.py ... ship` on both rigs with `--enable-autoexec` (the embedded Rigify UI scripts loaded). All 8 renders (Yin and Yang at yaw 0, 35, 60, 90) are pixel-identical to the autoexec-off renders: the 6 Python drivers have no effect at rest pose. The Teeth/Tongue shader-vector drivers and the CORRECTIVE_Blink drivers stay invalid either way, since their targets are empty in the EEVEE build.
    The 57 VRoid expression shapes on our faces (morph targets, applied by tools/vrm.py VRM.primitives; a recipe key
    matches a target whose name ends with it):
    Fcl_ALL_Angry Fcl_ALL_Fun Fcl_ALL_Joy Fcl_ALL_Neutral Fcl_ALL_Sorrow Fcl_ALL_Surprised
    Fcl_BRW_Angry Fcl_BRW_Fun Fcl_BRW_Joy Fcl_BRW_Sorrow Fcl_BRW_Surprised
    Fcl_EYE_Angry Fcl_EYE_Close Fcl_EYE_Close_L Fcl_EYE_Close_R Fcl_EYE_Fun Fcl_EYE_Highlight_Hide Fcl_EYE_Iris_Hide
    Fcl_EYE_Joy Fcl_EYE_Joy_L Fcl_EYE_Joy_R Fcl_EYE_Natural Fcl_EYE_Sorrow Fcl_EYE_Spread Fcl_EYE_Surprised
    Fcl_HA_Fung1 Fcl_HA_Fung1_Low Fcl_HA_Fung1_Up Fcl_HA_Fung2 Fcl_HA_Fung2_Low Fcl_HA_Fung2_Up Fcl_HA_Fung3
    Fcl_HA_Fung3_Low Fcl_HA_Fung3_Up Fcl_HA_Hide Fcl_HA_Short Fcl_HA_Short_Low Fcl_HA_Short_Up
    Fcl_MTH_A Fcl_MTH_Angry Fcl_MTH_Close Fcl_MTH_Down Fcl_MTH_E Fcl_MTH_Fun Fcl_MTH_I Fcl_MTH_Joy Fcl_MTH_Large
    Fcl_MTH_Neutral Fcl_MTH_O Fcl_MTH_SkinFung Fcl_MTH_SkinFung_L Fcl_MTH_SkinFung_R Fcl_MTH_Small Fcl_MTH_Sorrow
    Fcl_MTH_Surprised Fcl_MTH_U Fcl_MTH_Up
    
  • exec_commandsource ~/Projects/lowmarch-bench/kit/env-c.sh && printf '# Track C expression study\n\nStart: 2026-10-30 12:44:40 +08:00\n' > "$WORK/NOTES.md" && cat "$WORK/START" && ls ~/Projects/lowmarch-bench/brief/materials && ls "$KIT" (+2 more)

    command

    source ~/Projects/lowmarch-bench/kit/env-c.sh && printf '# Track C expression study\n\nStart: 2026-10-30 12:44:40 +08:00\n' > "$WORK/NOTES.md" && cat "$WORK/START" && ls ~/Projects/lowmarch-bench/brief/materials && ls "$KIT"
    
    source ~/Projects/lowmarch-bench/kit/env-c.sh && cat "$KIT/build_expr.sh" && cat "$KIT/shoot_expr.sh" && cat "$KIT/sheets.sh"
    
    source ~/Projects/lowmarch-bench/kit/env-c.sh && rg -n 'expression|expr|morph|shape' tools/vroid_portrait.py tools/vrm.py tools/cage.py | head -160 && rg -n 'PAINT_VS|PAINT_COMP|role ==|roleId|vRole|brow|depth' review/vroid-02/index.html | head -160

    result, 154 lines

    Script completed
    Wall time 1.2 seconds
    Output:
    2026-10-30T04:44:25Z
    expressions-vs-yin-yang.png
    vroid-shapes.txt
    yang-expressions
    yin-expressions
    yin-yang-product-gif-frames.png
    yin-yang-rig-review.md
    arm_shoot.py
    build_expr.sh
    env-a.sh
    env-b.sh
    env-c.sh
    expr_list.txt
    front_sheet.py
    setup_base.sh
    sheets.sh
    shoot_expr.sh
    terra_review.sh
    variant_build.py
    #!/bin/bash
    # Build both bodies (arm_f_slender, arm_m_youth) in each expression of LIST from your worktree's code, into
    # $WORK/exb/<name>/. Run tools/cage.py first if you edited tools/vroid_portrait.py (setup.sh does it).
    #   build_expr.sh LIST [only these names...]
    set -u
    LIST=$1; shift
    while IFS='|' read -r name rest; do
      [ -z "$name" ] && continue
      case "$name" in \#*) continue;; esac
      if [ $# -gt 0 ] && [[ ! " $* " =~ " $name " ]]; then continue; fi
      d=$WORK/exb/$name; rm -rf "$d"; mkdir -p "$d"; cp "$WORK/site/camera.json" "$d/"
      timeout 900 python3 "$KIT/variant_build.py" "$d" "rest=$rest" arm_f_slender arm_m_youth > "$WORK/log_build_$name.txt" 2>&1 \
        && echo "built $name" || { echo "BUILD FAILED $name"; tail -5 "$WORK/log_build_$name.txt"; }
    done < "$LIST"
    #!/bin/bash
    # Shoot every built expression ($WORK/exb/*) through PAGE (normally $LOWMARCH_REPO/review/vroid-02/index.html):
    # a frontal face tile and a 30 degree tile per body, into $WORK/shot_<TAG>/<name>/painted_<body>.png
    #   shoot_expr.sh PAGE TAG
    set -u
    PAGE=$1; TAG=$2
    site=$WORK/shootsite_$TAG; rm -rf "$site"; cp -r "$WORK/site" "$site"
    python3 - "$PAGE" "$site/index.html" <<'PY'
    import sys
    s = open(sys.argv[1], encoding="utf-8").read()
    i = s.index("const ARMORY_ROWS = ["); j = s.index("const DUEL_YAWS")
    s = s[:i] + '''const ARMORY_ROWS = [
      { label: "Face", tw: 480, th: 480, tiles: [{ clip: "ref", t: 0, yaw: 0, pitch: 2, focus: "J_Bip_C_Head", dy: 0.045,
          size: 0.22, label: "0°" }, { clip: "ref", t: 0, yaw: 30, pitch: 2, focus: "J_Bip_C_Head", dy: 0.045,
          size: 0.22, label: "30°" }] },
    ];
    ''' + s[j:]
    open(sys.argv[2], "w", encoding="utf-8").write(s)
    PY
    for d in "$WORK"/exb/*/; do
      name=$(basename "$d")
      cp "$d"/*.glb.json "$d/camera.json" "$site/"
      rm -rf "$WORK/shot_$TAG/$name"
      ROWS=0 timeout 900 python3 "$KIT/arm_shoot.py" "$site" "$WORK/shot_$TAG/$name" arm_f_slender arm_m_youth > /dev/null || echo "SHOOT FAILED $name"
    done
    rm -rf "$site"
    echo "shot $TAG"
    #!/bin/bash
    # Frontal sheets for both bodies: before vs a round. sheets.sh TAG "caption"   -> $WORK/sheet_<TAG>_{woman,youth}.png
    set -u
    TAG=$1; CAP=$2
    python3 "$KIT/front_sheet.py" "$WORK/sheet_${TAG}_woman.png" arm_f_slender "Woman, slender: expressions, front" "before:Before" "$TAG:$CAP" | tail -1
    python3 "$KIT/front_sheet.py" "$WORK/sheet_${TAG}_youth.png" arm_m_youth "Man, youth: expressions, front" "before:Before" "$TAG:$CAP" | tail -1
    tools/cage.py:1:"""VRoid bodies reshaped whole, their own head kept (spike, 2026-10-10).
    tools/cage.py:28:Without a reference the spec's own controls shape it (the first try; on their own they made fat or boxy
    tools/cage.py:184:    tri = np.concatenate([v.accessor(p["indices"]).reshape(-1, 3).astype(int) for p in mesh["primitives"]])
    tools/cage.py:202:    A = sp.coo_matrix((np.ones(len(i)), (i, j)), shape=(n, n)).tocsr()
    tools/cage.py:330:    """The model a cage body takes its shape from: a cast entry's MakeHuman body, or (name, MakeHuman spec) for
    tools/cage.py:520:    """(VRM, {mesh index: new positions, normals, morph deltas}, new joint positions)."""
    tools/cage.py:648:    if spec.get("bust_shape"):
    tools/cage.py:649:        Pw2 = _shape_bust(Pw2, md, wid, n, names, spec["bust_shape"], nw)
    tools/cage.py:682:def _shape_bust(P, md, wid, n, names, bs, nw=None):
    tools/cage.py:683:    """VRoid's own breasts reshaped after the wrap (which pulls them back to the reference's skin when shaped
    tools/cage.py:691:              shape and widens its base: the push fades over `base` metres across and above (a long upper
    tools/cage.py:733:    vertices' moves since the rig placed them (wrap, forms, bust_shape), weighted by its own weights: the springs
    tools/cage.py:746:    nc = arr.shape[1]
    tools/cage.py:754:    """The base again with every mesh's positions, normals and morph deltas, the joints and the skins'
    tools/cage.py:820:    "cup_p": [(_c("bust_shape.inflate"), (0.0, 0.04), 0.008)],  # VRoid's breasts inflated (tools/anthro.py's cup)
    tools/vrm.py:6:game except through those buffers (eye/brow/lash shapes come from alpha-tested texture masks).
    tools/vrm.py:44:            arr = np.frombuffer(self.bin, dt, n * nc, start).reshape(n, nc)
    tools/vrm.py:79:            m = np.array(n["matrix"], float).reshape(4, 4).T
    tools/vrm.py:92:            if adj.shape == (3, 3):
    tools/vrm.py:129:            ibm = self.accessor(sk["inverseBindMatrices"]).reshape(-1, 4, 4).transpose(0, 2, 1)
    tools/vrm.py:133:    def primitives(self, morphs=None, pose=None):
    tools/vrm.py:135:        morphs: {target name substring: weight} applied to meshes that have targets.
    tools/vrm.py:148:                if morphs and pr.get("targets"):
    tools/vrm.py:149:                    for key, wgt in morphs.items():
    tools/vrm.py:162:                tri = self.accessor(pr["indices"]).reshape(-1, 3).astype(np.int64)
    tools/vrm.py:221:                th, tw = tex.shape[:2]
    tools/vrm.py:237:    H, W = buf["prim"].shape
    tools/vrm.py:238:    dim = prims[0][attr].shape[1]
    tools/vroid_portrait.py:86:    out = np.full(v.shape, table[-1][1], "<U1")
    tools/vroid_portrait.py:122:    blk = lambda a: a.reshape(h, SS, w, SS).transpose(0, 2, 1, 3).reshape(h * w, SS * SS)
    tools/vroid_portrait.py:125:    ci = ci.reshape(cb.shape)
    tools/vroid_portrait.py:127:    counts = np.bincount((np.arange(h * w)[:, None] * nc + ci).ravel(), minlength=h * w * nc).reshape(h * w, nc)
    tools/vroid_portrait.py:140:    return out.reshape(h, w), dep.reshape(h, w), prim.reshape(h, w), rol.reshape(h, w)
    tools/vroid_portrait.py:157:# Archetype shape controls, in three layers (0 / 1 / {} = unchanged):
    tools/vroid_portrait.py:161:#  3. resting expression: VRoid's own shapes (hooded eyes, stern brows) held at a low weight
    tools/vroid_portrait.py:168:# hair is reshaped with the face: beards (built on the face before the face is sculpted) and side locks
    tools/vroid_portrait.py:213:        if r.shape == (4, 4):
    tools/vroid_portrait.py:396:    halfway toward the line's middle, so cheeks, nose and jaw take clean shadow shapes that hold at every angle
    tools/vroid_portrait.py:478:    U, V = P.shape[:2]
    tools/vroid_portrait.py:479:    pos = P.reshape(-1, 3)
    tools/vroid_portrait.py:672:    H, W = tex.shape[:2]
    tools/vroid_portrait.py:695:def reshape(prims, roles, lm, sh):
    tools/vroid_portrait.py:696:    """Face length, sculpting, eye size/tilt and mouth width (skeleton and expression are applied
    tools/vroid_portrait.py:731:    def __init__(self, path, morphs=None, pose=BUST_POSE, shape=None, hair_from=None, costume=None,
    tools/vroid_portrait.py:738:        sh = dict(NEUTRAL, **(shape or {}))
    tools/vroid_portrait.py:740:        for k_, w_ in (morphs or {}).items():
    tools/vroid_portrait.py:747:        self.shape = sh
    tools/vroid_portrait.py:803:        # framing uses the unshaped face at the base head size, so head-size changes still show
    tools/vroid_portrait.py:805:        reshape(self.prims, self.roles, self.lm, sh)
    tools/vroid_portrait.py:845:            th, tw = t.shape[:2]
    tools/vroid_portrait.py:859:                    th, tw = raw.shape
    tools/vroid_portrait.py:964:                    th, tw = mark.shape
    tools/vroid_portrait.py:1005:                    th, tw = t.shape[:2]
    tools/vroid_portrait.py:1036:        h, w = c.shape
    tools/vroid_portrait.py:1113:    h, w = c.shape
    tools/vroid_portrait.py:1127:    h, w = c.shape
    tools/vroid_portrait.py:1150:    render()), for reviewing shape. Uses the role colours, not VRoid's textures."""
    tools/vroid_portrait.py:1165:    h, w = char.shape
    52:.brand .eyebrow { font: 10px/1.3 var(--f-label); letter-spacing: 0.1em; text-transform: uppercase; color: var(--screen-dim); }
    126:      <div class="brand"><span class="eyebrow">Lowmarch · Model viewer</span><h1>Lowmarch cast</h1></div>
    183:        <li><b>Builds and faces vary.</b> The soldier's kit is also worn by a stocky grey veteran and a slim woman. Builds run from petite (the thief, 1.4 m) to huge (the berserker), with a new overall height control and oversized hands, as the study's units have. Faces differ in shape (jaw, chin, brow, nose, cheeks), expression, hair and eye colour, skin tone, and beards and moustaches. The lineup's check sheet has rows of faces with helmets off and on.</li>
    243:const ROLE_ID = { face: 1, body: 2, hair: 3, line: 4, iris: 5, white: 6, highlight: 7, brow: 8, mouth: 9, cloth: 10, hair_b: 11 };
    245:const roleId = r => (ROLE_ID[r] ??= nextRole++);
    256:  document.querySelector(".brand .eyebrow").textContent = PAGE.eyebrow || "Lowmarch";
    316:const PAINT_VS = `${SKIN_HEAD}
    349:  // Cast shadows on skin, in screen space (the "depth offset" shadows of anime toon shaders): walk from this pixel
    500:const PAINT_COMP = `
    568:const paint = compPass(PAINT_COMP, { tColor: { value: null }, tInfo: { value: null }, tDepth: { value: null }, uRes: { value: new THREE.Vector2() },
    583:  targets.rtInfo = new THREE.WebGLRenderTarget(w, h, { ...o, depthTexture: new THREE.DepthTexture(w, h) });
    806:    uInfo: { value: new THREE.Vector4(roleId(infoRole) / 255, (index % 256) / 255, Math.floor(index / 256) / 255, 1) },
    826:  if (role === "face" && map) u.uFaceTex.value = 1;
    827:  if (role === "hair" && map) u.uAlpha.value = 1;
    828:  if (role === "iris") { u.uKind.value = 7; u.uAlpha.value = 1; }
    829:  else if (role === "white" || role === "highlight") { u.uKind.value = map ? 8 : 9; if (map) u.uAlpha.value = 1; u.uFlat.value = hex(Rl.color || "#ffffff"); }
    830:  else if (!P && Rl.mode === "flat") { u.uKind.value = 9; u.uFlat.value = hex(Rl.color); if (map) u.uAlpha.value = role === "line" ? 2 : 1; }
    832:  const m = new THREE.ShaderMaterial({ uniforms: u, vertexShader: PAINT_VS, fragmentShader: PAINT_FS, side: THREE.DoubleSide,
    861:  const infoRole = role === "hair" && opts.hairB ? "hair_b" : role === "shadow" ? "body" : role;
    866:  if (role === "hair" && m0.map && V.paint && V.paint.hair) {
    939:    mat.transparent = x; mat.opacity = x ? 0.28 : 1; mat.depthWrite = !x; mat.needsUpdate = true;
    1285:    paint.mat.uniforms.uGap.value = lead.depth_gap;
    1346:  // the info pass (roles, ids, depth) first: the colour pass reads it for cast shadows
    1354:    mu.uPass.value = 0; mu.uCastOn.value = 1; mu.tCastInfo.value = targets.rtInfo.texture; mu.tCastDepth.value = targets.rtInfo.depthTexture;
    1359:  u.tColor.value = targets.rt.texture; u.tInfo.value = targets.rtInfo.texture; u.tDepth.value = targets.rtInfo.depthTexture;
    1371:// drawn into one image, which lands in #sheetOut as a PNG data URL (a headless browser reads it).
    
  • view_image~/Projects/lowmarch-bench/brief/materials/expressions-vs-yin-yang.png

    input

    const im=await tools.view_image({path:"~/Projects/lowmarch-bench/brief/materials/expressions-vs-yin-yang.png",detail:"original"});image(im.image_url);
    

    result, 4 lines

    Script completed
    Wall time 0.2 seconds
    Output:
    [redacted: reference rig image (Yin/Yang by DillonGoo, personal-use licence)]

agent

+0:25

The reference makes emotion legible through distinct eye shapes and brows, while several starting expressions here differ mainly at the mouth. I’m checking how recipes enter the build and how the renderer orders face features so the improvements remain usable in the project.