Track C: Codex with GPT-6.1-Terra end to end
agent
+41:32The fifth sheets show a continuous angry frown on both bodies, and the other expressions remain consistent with the previous round. I’m selecting this round, recording the remaining limitations, and preparing the requested commit.
exec_commandsource ~/Projects/lowmarch-bench/kit/env-c.sh && cp "$WORK/sheet_r5_woman.png" "$WORK/FINAL_woman.png" && cp "$WORK/sheet_r5_youth.png" "$WORK/FINAL_youth.png" && cat >> "$WORK/NOTES.md" <<'EOF'
command
source ~/Projects/lowmarch-bench/kit/env-c.sh && cp "$WORK/sheet_r5_woman.png" "$WORK/FINAL_woman.png" && cp "$WORK/sheet_r5_youth.png" "$WORK/FINAL_youth.png" && cat >> "$WORK/NOTES.md" <<'EOF' ## Round 5 — 13:19:07–13:25:50 (6m 43s) Moved the frown curve 2.5 mm forward in head space, clearing the deformed face on the woman. The full sheets now show a continuous, readable angry mouth on both characters. The close-up 30-degree shots show the line retaining its curve, and surprise, neutral, wink, joy and the vowel mouths remain in place. This is the selected round. ## Eyebrows and remaining weaknesses Draw eyebrows through nearby bangs. These test hairstyles obscure much of their brow articulation; the underlying reference lesson is that brows and eyelids should communicate emotion together. A softened overlay makes those poses legible and leaves neutral calm. It is limited to pixels whose visible foreground is hair within 4.5 cm, with a distance fade; ordinary depth occlusion still applies to helmets and other foreground materials. This is an intentional stylization and differs from the study rigs' hair-above-brow treatment. No reference rig assets were copied. The youth's joy has small pale flecks in its lash texture; the woman's fully closed lashes remain a little ragged. Smile and smug are subtle, and the stock E and I mouths lack the crisp teeth of the reference; sadness in the youth is understated. The frown line has been checked at 0 and 30 degrees, not at extreme profile. The brow pass adds an extra render pass and has been checked in the two test hairstyles, not every possible character or helmet combination. Final: round 5. Visual work elapsed 41m 10s from 12:44:40 to 13:25:50 +08:00; subsequent time was used for documentation, checks and commit. EOF python3 - <<'PY' import json,os from pathlib import Path from PIL import Image w=Path(os.environ['WORK']); names='neutral joy smile angry sad surprised shout wink smug lip_A lip_I lip_U lip_E lip_O'.split() for n in names: for who in ('arm_f_slender','arm_m_youth'): p=w/'shot_r5'/n/f'painted_{who}.png' with Image.open(p) as im: im.verify() for who in ('woman','youth'): with Image.open(w/f'FINAL_{who}.png') as im: print(who,'final sheet:', im.size) j=json.loads((w/'exb/angry/camera.json').read_text()) for who in ('arm_f_slender','arm_m_youth'): c=next(x for x in j['characters'] if x['name']==who) assert c['info']['mouth_ink'] and len(c['info']['mouth_curve'])>=4 print('All 28 final captures verified; both angry builds carry the frown curve.') PY git diff --check && git diff --stat && git status --short && date -Isresult, 16 lines
Script completed Wall time 0.7 seconds Output: woman final sheet: (1968, 1144) youth final sheet: (1968, 1144) All 28 final captures verified; both angry builds carry the frown curve. docs/portrait-guide.md | 18 ++++++++++++++++++ review/vroid-02/index.html | 45 ++++++++++++++++++++++++++++++++++++++++----- tools/export_glb.py | 10 +++++++++- 3 files changed, 67 insertions(+), 6 deletions(-) M docs/portrait-guide.md M review/vroid-02/index.html M tools/export_glb.py ?? art/expressions/ 2026-10-30T13:26:09+08:00
exec_commandsource ~/Projects/lowmarch-bench/kit/env-c.sh && git rev-parse --git-dir && git rev-parse --git-common-dir && git branch --show-current && git diff -- review/vroid-02/index.html tools/export_glb.py docs/portrait-guide.md
command
source ~/Projects/lowmarch-bench/kit/env-c.sh && git rev-parse --git-dir && git rev-parse --git-common-dir && git branch --show-current && git diff -- review/vroid-02/index.html tools/export_glb.py docs/portrait-guide.md
result, 161 lines
Script completed Wall time 0.2 seconds Output: ~/Projects/lowmarch/.git/worktrees/c ~/Projects/lowmarch/.git bench-c diff --git a/docs/portrait-guide.md b/docs/portrait-guide.md index b11f81c..3147c31 100644 --- a/docs/portrait-guide.md +++ b/docs/portrait-guide.md @@ -1330,3 +1330,21 @@ like). The pixel renderer is abandoned; the armory's painted renderer is the tar fantasy blues and greens) and the stage grade taken at 30% on skin, keeps deep tones rich. - **Space the range by eye, not by name.** The older seven bunched at the light end (pale, light, fair look alike) and stopped at a mid brown. + +## Armory expression recipes + +`art/expressions/armory.txt` records the fourteen named VRoid shape combinations used for the +slender woman and youth expression study. Each row is `name|shape:weight;...`; `none` is the +unmodified neutral face. The build-time `shape.rest` dictionary takes the same shape suffixes and +weights. The wink deliberately extrapolates the one-sided close to 1.5: at 1.0 the youth's +iris remains visible between the lids. Joy combines the joy and close shapes for the same reason. +Check each character and a three-quarter view when changing these values; the shapes interact +with each head's sculpt and eyelashes. + +In the painted armory, nearby bangs show a softened eyebrow overlay, so the expression survives +hair that otherwise covers the brows. It uses the brow's actual alpha and depth, and is limited +to hair within 4.5 cm, fading at the limit; helmets and other objects still occlude it. A closed +angry mouth can compress its face texture until the frown disappears. For that specific closed +shape the exporter enables the existing traced lip curve, which the viewer attaches to the head +as a fine line; open mouths and neutral do not get that line. The face atlas's mouth window also +gets a small contrast boost to help closed lip marks read. diff --git a/review/vroid-02/index.html b/review/vroid-02/index.html index f6aee02..93df578 100644 --- a/review/vroid-02/index.html +++ b/review/vroid-02/index.html @@ -441,7 +441,15 @@ const PAINT_FS = ` vec3 sL = vec3(0.973, 0.882, 0.835), sS = vec3(0.776, 0.659, 0.608), sC = vec3(0.451, 0.322, 0.310); if (uSkinRamp == 1) { sL = uSkinL; sS = uSkinS; sC = uSkinC; } // the character's tone, designed per tone col = mix(sC, sS, st(lit, 0.20)); col = mix(col, sL, st(lit, 0.38)); - if (uFaceTex == 1) col *= clamp(tex.rgb / uSkinRef, 0.55, 1.05); + if (uFaceTex == 1) { + col *= clamp(tex.rgb / uSkinRef, 0.55, 1.05); + // The shared VRoid face atlas puts the painted lip line in this small window. + // Preserve its hue while giving the darkest lip texels enough contrast to read at game size. + if (vUv.x > 0.32 && vUv.x < 0.68 && vUv.y > 0.70 && vUv.y < 0.86) { + float lipInk = 1.0 - smoothstep(0.78, 0.91, tex.r); + col *= 1.0 - 0.48 * lipInk; + } + } if (uSkinRamp == 0) col *= uSkinTone; // older builds: one tint over the light skin } else if (uKind == 2) { // hair: four even bands, hard strand highlights float strand = dot(tex.rgb, vec3(1.0 / 3.0)); @@ -498,7 +506,7 @@ const PAINT_FS = ` gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0); }`; const PAINT_COMP = ` - uniform sampler2D tColor; uniform sampler2D tInfo; uniform sampler2D tDepth; uniform vec2 uRes; + uniform sampler2D tColor; uniform sampler2D tInfo; uniform sampler2D tDepth; uniform sampler2D tBrow; uniform sampler2D tBrowDepth; uniform vec2 uRes; uniform float uNear; uniform float uFar; uniform float uGap; uniform float uSil; uniform float uInner; ${HSV} float z(vec2 p) { return uNear + texture2D(tDepth, p).x * (uFar - uNear); } @@ -548,6 +556,12 @@ const PAINT_COMP = ` col = mix(col, col * vec3(0.84, 0.74, 0.76), 0.75 * hair); } } + // Keep the expression legible through nearby bangs, but never through helmets or distant hair. + // Both passes use the same orthographic depth and the brow's own alpha-tested texture. + vec4 brow = texture2D(tBrow, uv); + float bz = uNear + texture2D(tBrowDepth, uv).x * (uFar - uNear); + if ((r0 == 3 || r0 == 11) && brow.a > 0.5 && bz >= z0 && bz - z0 < 0.045) + col = mix(col, brow.rgb, 0.55 * (1.0 - smoothstep(0.035, 0.045, bz - z0))); gl_FragColor = vec4(col, 1.0); }`; @@ -565,7 +579,7 @@ const R = new THREE.WebGLRenderer({ antialias: false, alpha: true, preserveDrawi R.setClearColor(0x000000, 0); R.outputColorSpace = THREE.LinearSRGBColorSpace; stageEl.appendChild(R.domElement); -const paint = compPass(PAINT_COMP, { tColor: { value: null }, tInfo: { value: null }, tDepth: { value: null }, uRes: { value: new THREE.Vector2() }, +const paint = compPass(PAINT_COMP, { tColor: { value: null }, tInfo: { value: null }, tDepth: { value: null }, tBrow: { value: null }, tBrowDepth: { value: null }, uRes: { value: new THREE.Vector2() }, uNear: { value: 0.1 }, uFar: { value: 40 }, uGap: { value: 0.012 }, uSil: { value: 2 }, uInner: { value: 1 } }); const ctl = new OrbitControls(camera, R.domElement); ctl.enableDamping = true; ctl.zoomToCursor = true; ctl.screenSpacePanning = true; @@ -573,14 +587,15 @@ ctl.minZoom = 0.3; ctl.maxZoom = 12; let freeLook = false; ctl.addEventListener("start", () => { freeLook = true; }); R.domElement.addEventListener("dblclick", focusAt); -const targets = { rt: null, rtInfo: null, key: "" }; +const targets = { rt: null, rtInfo: null, rtBrow: null, key: "" }; function targetsFor(w, h) { const key = `${w}x${h}`; if (targets.key === key) return; - [targets.rt, targets.rtInfo].forEach(t => t && t.dispose()); + [targets.rt, targets.rtInfo, targets.rtBrow].forEach(t => t && t.dispose()); const o = { minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter }; targets.rt = new THREE.WebGLRenderTarget(w, h, o); targets.rtInfo = new THREE.WebGLRenderTarget(w, h, { ...o, depthTexture: new THREE.DepthTexture(w, h) }); + targets.rtBrow = new THREE.WebGLRenderTarget(w, h, { ...o, depthTexture: new THREE.DepthTexture(w, h) }); targets.key = key; } @@ -894,6 +909,19 @@ async function loadCharacter(name) { gltf.scene.traverse(o => { if (o.isMesh) { prepMesh(o, V, { hairB: ci > 0 }); meshes.push(o); } }); gltf.scene.updateMatrixWorld(true); const bone = n => gltf.scene.getObjectByName(n); + // The exported surface-following mouth curve repairs the texture's compressed closed frown. + // Parent it to the head in bone-local coordinates so it turns with the character. + const mouthBone = V.mouth_ink && bone(V.mouth_bone); + if (mouthBone && V.mouth_curve && V.mouth_curve.length >= 4) { + // The deformed lip surface can stand ahead of the traced face by a millimetre. + const points = V.mouth_curve.map(p => mouthBone.worldToLocal( + gltf.scene.localToWorld(new THREE.Vector3(p[0], p[1], p[2] + 0.0025)))); + const curve = new THREE.CatmullRomCurve3(points); + const line = new THREE.Mesh(new THREE.TubeGeometry(curve, 48, 0.00022, 5, false), + new THREE.MeshStandardMaterial({ color: hex(V.roles.mouth.color) })); + line.userData.role = "mouth"; line.userData.group = "body"; + mouthBone.add(line); prepMesh(line, V); meshes.push(line); + } const cloths = [], world = new THREE.Group(); for (const [cname, info] of Object.entries(V.cloth || {})) { const skinned = meshes.find(m => m.userData.material === cname); @@ -1355,7 +1383,14 @@ function render() { mu.uCastRes.value.set(buf.x, buf.y); mu.uCastNear.value = camera.near; mu.uCastFar.value = camera.far; mu.uCastPpm.value = ppmBuf; } R.setRenderTarget(targets.rt); R.clear(); R.render(scene, camera); + // Isolate visible brow meshes for the optional through-bangs overlay. Preserve all visibility + // flags, including toggled equipment and hidden simulation meshes. + const visibility = meshes.map(o => o.visible); + meshes.forEach(o => { if (o.userData.role !== "brow") o.visible = false; }); + R.setRenderTarget(targets.rtBrow); R.clear(); R.render(scene, camera); + meshes.forEach((o, i) => { o.visible = visibility[i]; }); const u = paint.mat.uniforms; + u.tBrow.value = targets.rtBrow.texture; u.tBrowDepth.value = targets.rtBrow.depthTexture; u.tColor.value = targets.rt.texture; u.tInfo.value = targets.rtInfo.texture; u.tDepth.value = targets.rtInfo.depthTexture; u.uRes.value.set(buf.x, buf.y); u.uNear.value = camera.near; u.uFar.value = camera.far; // line weight follows the figure's size on screen (study: ~0.4% of its height) diff --git a/tools/export_glb.py b/tools/export_glb.py index 16dc499..754dd09 100644 --- a/tools/export_glb.py +++ b/tools/export_glb.py @@ -264,9 +264,17 @@ def scene_info(portrait, palette): for role, g in (pc.get("grain") or {}).items(): # optional per-role brush grain (big flat cloth mottles) if role in paint: paint[role]["grain"] = g + # A fully closed angry mouth compresses its painted lip texture until the middle disappears. + # Use the existing surface-following curve only for that closed frown, not for open mouths. + rest = portrait.shape.get("rest", {}) + frown = max(rest.get("Fcl_MTH_Angry", 0), rest.get("Fcl_ALL_Angry", 0)) > 0.1 + openings = ("Fcl_MTH_A", "Fcl_MTH_I", "Fcl_MTH_U", "Fcl_MTH_E", "Fcl_MTH_O", + "Fcl_MTH_Joy", "Fcl_MTH_Sorrow", "Fcl_MTH_Surprised", "Fcl_ALL_Joy", + "Fcl_ALL_Sorrow", "Fcl_ALL_Surprised") + mouth_ink = bool(curve is not None and frown and not any(rest.get(k, 0) > 0.05 for k in openings)) return dict(roles=roles, light=vp.LIGHT.tolist(), sun=vp.SUN.tolist(), capsules=caps, cloth=cloth, face_marks=marks, paint=paint, - mouth_curve=curve, mouth_bone="J_Bip_C_Head", ink=dict(o=hexp("o"), skin=hexp("1"), hair=hexp("A")), + mouth_curve=curve, mouth_bone="J_Bip_C_Head", mouth_ink=mouth_ink, ink=dict(o=hexp("o"), skin=hexp("1"), hair=hexp("A")), skin=[hexp(c) for c in "12345"], depth_gap=0.012)
agent
+42:04The worktree’s Git metadata points into ~/Projects/lowmarch, which the brief explicitly forbids modifying. I’m preserving the requested commit on a bench-c branch in the permitted work directory and creating a transferable bundle, without writing to that shared repository.
exec_commandsource ~/Projects/lowmarch-bench/kit/env-c.sh && git config user.name && git config user.email && git rev-parse HEAD && git rev-parse --git-common-dir
command
source ~/Projects/lowmarch-bench/kit/env-c.sh && git config user.name && git config user.email && git rev-parse HEAD && git rev-parse --git-common-dir
result, 8 lines
Script completed Wall time 0.2 seconds Output: Severian yo@severian.dev 47dcd23a85c9f5654035a7ce7a7f66716213419f ~/Projects/lowmarch/.git
exec_commandsource ~/Projects/lowmarch-bench/kit/env-c.sh && TASK_BASE=$(git rev-parse HEAD) && TASK_GIT="$WORK/track-c.git" && git init --bare --initial-branch=bench-c "$TASK_GIT" && printf '%s\n' '~/Projects/lowmarch/.git/objects' > "$TASK_GIT/objects/info/alternates" && git --git-dir="$TASK_GIT" update-ref refs/heads/bench-c "$TASK_BASE" && git --git-dir="$TASK_GIT" --work-tree="$LOWMARCH_REPO" read-tree HEAD && git --git-dir="$TASK_GIT" --work-tree="$LOWMARCH_REPO" add art/expressions/armory.txt docs/portrait-guide.md review/vroid-02/index.html tools/export_glb.py && git --git-dir="$TASK_GIT" --work-tree="$LOWMARCH_REPO" commit -m 'Improve armory facial expression readability' && git --git-dir="$TASK_GIT" bundle create "$WORK/track-c.bundle" refs/heads/bench-c "^$TASK_BASE" && git --git-dir="$TASK_GIT" bundle verify "$WORK/track-c.bundle" && git --git-dir="$TASK_GIT" --work-tree="$LOWMARCH_REPO" status --short && git --git-dir="$TASK_GIT" log -1 --oneline && date -Is
command
source ~/Projects/lowmarch-bench/kit/env-c.sh && TASK_BASE=$(git rev-parse HEAD) && TASK_GIT="$WORK/track-c.git" && git init --bare --initial-branch=bench-c "$TASK_GIT" && printf '%s\n' '~/Projects/lowmarch/.git/objects' > "$TASK_GIT/objects/info/alternates" && git --git-dir="$TASK_GIT" update-ref refs/heads/bench-c "$TASK_BASE" && git --git-dir="$TASK_GIT" --work-tree="$LOWMARCH_REPO" read-tree HEAD && git --git-dir="$TASK_GIT" --work-tree="$LOWMARCH_REPO" add art/expressions/armory.txt docs/portrait-guide.md review/vroid-02/index.html tools/export_glb.py && git --git-dir="$TASK_GIT" --work-tree="$LOWMARCH_REPO" commit -m 'Improve armory facial expression readability' && git --git-dir="$TASK_GIT" bundle create "$WORK/track-c.bundle" refs/heads/bench-c "^$TASK_BASE" && git --git-dir="$TASK_GIT" bundle verify "$WORK/track-c.bundle" && git --git-dir="$TASK_GIT" --work-tree="$LOWMARCH_REPO" status --short && git --git-dir="$TASK_GIT" log -1 --oneline && date -Is
result, 16 lines
Script completed Wall time 0.3 seconds Output: Initialized empty Git repository in ~/Projects/lowmarch-bench/work/c/track-c.git/ [bench-c fffca34] Improve armory facial expression readability 4 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 art/expressions/armory.txt ~/Projects/lowmarch-bench/work/c/track-c.bundle is okay The bundle contains this ref: fffca34de9b03638fc8a5c9c1465d52651297a04 refs/heads/bench-c The bundle requires this ref: 47dcd23a85c9f5654035a7ce7a7f66716213419f The bundle uses this hash algorithm: sha1 fffca34 Improve armory facial expression readability 2026-10-30T13:26:42+08:00
exec_commandsource ~/Projects/lowmarch-bench/kit/env-c.sh && cat >> "$WORK/NOTES.md" <<'EOF'
command
source ~/Projects/lowmarch-bench/kit/env-c.sh && cat >> "$WORK/NOTES.md" <<'EOF' ## Commit and workspace constraint The worktree's Git directory resolves to `~/Projects/lowmarch/.git/worktrees/c`, inside the explicitly forbidden `~/Projects/lowmarch` tree and outside the sandbox's writable roots. To respect that restriction, committed the changes as `fffca34` on a `bench-c` branch in `$WORK/track-c.git`, using the original repository's objects read-only. `$WORK/track-c.bundle` contains that commit with starting commit `47dcd23` as its prerequisite. The original worktree's branch ref was not updated. The alternate Git directory reports a clean worktree. Total through commit: 42m 02s. EOF python3 - <<'PY' import os from pathlib import Path w=Path(os.environ['WORK']) for name in ['FINAL_woman.png','FINAL_youth.png','NOTES.md','track-c.bundle']: p=w/name; print(name,p.stat().st_size,'bytes') PY git --git-dir="$WORK/track-c.git" --work-tree="$LOWMARCH_REPO" status --porcelain && git --git-dir="$WORK/track-c.git" show --stat --oneline --format=short HEAD
result, 18 lines
Script completed Wall time 0.5 seconds Output: FINAL_woman.png 517977 bytes FINAL_youth.png 607105 bytes NOTES.md 5192 bytes track-c.bundle 3907 bytes commit fffca34 Author: Severian <yo@severian.dev> Improve armory facial expression readability art/expressions/armory.txt | 16 ++++++++++++++++ docs/portrait-guide.md | 18 ++++++++++++++++++ review/vroid-02/index.html | 45 ++++++++++++++++++++++++++++++++++++++++----- tools/export_glb.py | 10 +++++++++- 4 files changed, 83 insertions(+), 6 deletions(-)
agent
+42:27Improved the expression recipes, closed-eye poses and angry frown for both bodies. Brows now show softly through nearby bangs so their movement remains readable; the overlay respects occlusion by helmets and other objects.
The selected result is round 5, completed in about 42 minutes. I reviewed both bodies at 0° and 30°, verified all 28 final captures, and checked the recipe names and code syntax. Smile and smug remain subtle, and the closed lashes have minor artifacts.
- Woman’s final sheet
- Youth’s final sheet
- Round notes
Commit fffca34 is on bench-c in the permitted Git bundle. The original worktree’s branch ref remains unchanged because its Git metadata lives under ~/Projects/lowmarch, which the brief prohibited modifying.