Textures (6 walls + normals + wet matcap, $0, ~64s on the m3ultra MPS): - FLUX ignores "seamless tiling" (seam_error 3.7-17.9); a 4-way cosine partition-of-unity blend of the four half-rolls fixes it exactly -> 0.87-1.32 - levels-normalised the pack to one exposure (means were 0.18-0.32); these are detail maps multiplied into a biome tint, so per-prompt exposure read as a broken tint rather than as dark tissue - prompt-kit experiment rejected + recorded: PROCITY's "uniform coverage" clause in the STEM cured centred subjects but flattened the pack; framing words now live only in the two subject prompts that needed them Audio (1 bed + 4 sfx, 4.2s render, 0.61/10MB, ogg+m4a dual-ship): - bed-esophagus 24.000s, loop seam 0.32 (wrap step 3x smaller than inner step) - cascaded lowpass poles: one-pole at 900Hz left 16kHz only ~25dB down = hiss - rewrote the spectrogram sheet (per-clip peak, log freq) — the first one was a saturated rectangle, and evidence you can't read is not evidence assets.js: miss ledger + misses() — Lane A found that a drifted slug falls back procedurally forever with no error. Each distinct miss now announces itself. Also: shot_sink.py (canvas -> docs/shots/laneD, correctly named) and a dev texture viewer that imports the stub world read-only for the eyeball law. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
14 KiB
LANE D — Assets — NOTES
Round 1 (2026-07-16) — pipeline online, first texture pack, audio proof
Everything below was generated free and local on the m3ultra box (flux_local, MPS).
Total spend this round: $0.00. FAL_LIST.md does not exist yet — nothing needed it.
What landed
| thing | where | note |
|---|---|---|
| 6 wall textures + normals | web/assets/gen/wall_*.webp |
grayscale SEM, exact-tiling, 1024² |
| 1 wet-tissue matcap | web/assets/gen/matcap_tissue_wet.webp |
512², cropped to the ball |
| 1 bed + 4 sfx | web/assets/audio/*.{ogg,m4a} |
procedural numpy synth, dual-shipped |
| manifest + loader | web/assets/manifest.json, js/core/assets.js |
null-on-miss, ?localassets=0 |
| pipeline scripts | pipeline/*.py |
ported from PROCITY, --dry-run first |
| dev viewer | web/dev/laneD_texview.html |
in-engine eyeball harness (see §ownership) |
| shot sink | pipeline/shot_sink.py |
writes canvas frames into docs/shots/laneD/ |
Shipped asset weight: 2.3 MB (1.7 MB textures+normals, 0.61 MB audio). Audio budget 0.61/10 MB.
Measured (not estimated)
-
Generation: 9.0–9.6 s per 1024² image on the m3ultra MPS GPU (
flux2-klein-4b, 4 steps, guidance 3.5, quantize 8), one GPU job at a time. Whole 7-image pack ≈ 64 s. Audio: 4.2 s for the entire pack (numpy synth + ffmpeg opus/aac), measured withtime. -
Tiling —
seam_error= mean |Δ| across the wrap seam ÷ mean |Δ| inside the image. ~1.0 means the seam is indistinguishable from ordinary interior detail. FLUX is told "seamless tiling texture" and flatly ignores it — every raw image scored 3.7–17.9:texture seam before seam after mean before → after wall_esophagus_a 3.70 1.14 0.22 → 0.50 wall_esophagus_b 11.12 2.39 0.17 → 0.49 wall_smallint_a 8.60 1.04 0.18 → 0.49 wall_smallint_b 10.50 1.32 0.24 → 0.50 wall_stomach_a 12.16 0.87 0.22 → 0.50 wall_stomach_b 17.85 1.08 0.25 → 0.50 wall_esophagus_b(2.39) is the pack's weakest: it's near-pure parallel lines, so the blend has no noise to hide in. It reads clean in the 2×2 check and in engine — flagging it honestly rather than claiming 1.0. -
Loop seam (
bed-esophagus) = 0.32: the wrap step is 3× smaller than the average sample-to-sample step, i.e. inaudible. Numeric, inpipeline/batch_audio.json. -
Audio streams verified with ffprobe: every asset ships opus/ogg 48 kHz stereo and aac/m4a 44.1 kHz stereo. Final ear-check is John's (PIPELINE.md).
Evidence: docs/shots/laneD/ — contact_walls.png (source | 2×2 tile check | normal, per
texture), contact_matcaps.png, audio_spectrograms.png (log-freq, 80 dB, loop seam printed),
and 7 in-engine 1280×720 frames round1_wall_*.png + round1_fallback_localassets0.png.
Two things that were wrong and are now right
- The pack was two stops apart. FLUX gives every prompt its own exposure — source means
ranged 0.18–0.32. These are DETAIL maps multiplied into a biome tint, so a dark texture
doesn't read as "dark tissue", it reads as "this biome's tint is broken".
derive_maps.pynow normalises every wall to mean 0.50 / std 0.19 (soft-clipped). The tint, not the texture, decides brightness. - The prompt kit needed framing, not uniformity — see §prompt-kit below.
§prompt-kit — an ART_BIBLE amendment, tried and REJECTED
The ART_BIBLE stem nails the look but lets FLUX compose a single centred hero subject: the
first esophagus_b was one sphincter donut (tiled → polka dots) and smallint_a was one
urchin of villi (tiled → mush). PROCITY hit this exact problem and solved it by ending every
template with "filling the entire frame edge to edge, nothing else visible".
I ported that into the STEM — and it made the pack worse: esophagus_a lost its ribbed
drama for bland wood-grain, and stomach/smallint collapsed into near-identical honeycomb.
Uniformity is not the goal; variety across biomes is. Rejected and reverted.
What shipped instead: the stem stays ART_BIBLE-verbatim, and only the two prompts that
actually drew a hero subject carry framing words in their own subject text. Four winners were
kept untouched rather than churned. This is recorded in gen_textures.py so nobody re-runs the
experiment. No ART_BIBLE edit is requested — the file is right as written.
§tiling — convention (ratified with Lane A)
tile: [repeats_around_theta, units_of_s_per_repeat], read off the stub's
uv = (theta/2pi, s-in-world-units). Lane A converged on the identical semantics independently
(LANE_A_NOTES §→ Lane D) — ratified from my side too, it's settled. assets.texture(n)
returns repeat = [tile[0], 1/tile[1]] precomputed.
Per-texture values live in pipeline/batch_textures.json and flow into the manifest.
→ Lane A
Thanks for the review — answering each point:
- Naming (
wall_smallint_avs biome idsmall_intestine): agreed, your proposal, round 2. You're right that it's the dangerous kind of bug. I did not rename this round on purpose: yourindex.jsslug map is live and a unilateral rename would silently break it mid-round — exactly the failure you described. Round 2, in one step: I rename to biome ids (wall_small_intestine_a,wall_large_intestine_a), you delete the map. F to sequence. - Meanwhile the silent-miss class is fixed at my end.
assets.jsnow keeps a miss ledger: the first time any name misses it logs[assets] miss: textures/x — using fallback, andassets.misses()returns every name asked for that wasn't there. Verified against your exact case:assets.texture('wall_small_intestine_a')→ null + one log + listed inmisses(). So a drifted slug now announces itself instead of falling back forever in silence. texture()vsget(): agreed —get()is the documented floor (raw JSON, per TECH),texture()is what a shader can eat. Seconding your suggestion that F promotestexture()into TECH as the contract. I'll keep both, andget()keeps working exactly as specced.- "the
repeatyou pre-apply is inert in a raw ShaderMaterial" — correct, and it's deliberate.texture.repeatonly feedsuvTransformin three's built-in materials. I set it anyway so the value is right if anyone ever uses a built-in material, and I expose the same numbers as.repeat/.tilefor shader use. Multiply it in yourself:vec2 duv = vUv * uRepeat;.
The TBN gotcha — you said round 2, here's the answer now
You noted the _b variants and matcap "aren't wired yet (round 2, with the TBN work)". I hit
the normal-map integration in the viewer and it cost me a while, so take the result:
Do NOT add the normal-map sample to the vertex normal. I first wrote
n = normalize(vN + tn * 0.55). A tangent-space sample is ~(0,0,1), so adding it tilts every
normal toward the camera → dot(n, view) → 1 → the fresnel rim dies → the tube renders
near-black. The rim is the biome's main light; this mistake looks like "Lane D's textures are
too dark" and it is not. Diagnosis: flip to the procedural fallback — if that's bright and the
textured path is black, it's this.
The tube geometry carries no tangent attribute, so rebuild the TBN per-pixel from screen
derivatives (three's perturbNormal2Arb). Working, verified in
web/dev/laneD_texview.html — lift it:
// needs: new THREE.ShaderMaterial({ extensions: { derivatives: true }, ... })
vec3 perturb(vec3 N, vec3 viewPos, vec2 st, vec3 mapN) {
vec3 q0 = dFdx(viewPos), q1 = dFdy(viewPos);
vec2 st0 = dFdx(st), st1 = dFdy(st);
vec3 S = normalize(q0 * st1.t - q1 * st0.t);
vec3 T = normalize(-q0 * st1.s + q1 * st0.s);
return normalize(mat3(S, T, N) * mapN);
}
// usage, in the fragment shader:
vec2 duv = vUv * uRepeat; // uRepeat = assets.texture(n).repeat
vec3 tn = texture2D(uNormal, duv).xyz * 2.0 - 1.0;
tn.xy *= uNormalScale; // 0.6 looks right; 0 = off, 1 = full relief
vec3 n = perturb(normalize(vN), -vView, duv, normalize(tn));
float fres = pow(1.0 - abs(dot(n, normalize(vView))), 2.2);
With this, the rim light traces every fold edge and the walls finally look like the ART_BIBLE
line ("SEM tissue + artificial rim light"): round1_wall_esophagus_a.png. It's the single
biggest visual win of my round and it's yours to take.
- Detail sampling: the wall webp is a luminance/AO map.
float detail = texture2D(uDetail, duv).r;thenbase = uTint * (0.35 + detail * 0.95) * 0.9is what the shots use. Note three uploads these asSRGB8_ALPHA8(I setcolorSpace = SRGBColorSpace), so the GPU decodes to linear on sample — that's intended. Normals areNoColorSpace, don't change that. - Biome tints beyond esophagus: the viewer carries the ART_BIBLE palettes for stomach and small intestine, so each wall was judged under its own tint, not teal.
→ Lane F
1. Wiring assets into boot. assets.js is done and flags.localassets already parses
(thank you). Nothing constructs it yet. Exact snippet to paste into js/boot.js:
import { createAssets } from './core/assets.js'; // with the other core imports
const assets = await createAssets({ flags, renderer }); // after `renderer` exists,
// before pickWorld()
then pass it into the lane factories (createWorld(level, { rng, assets }), and B/E likewise),
and add to the DBG export if you like: assets (has .misses() — see → Lane A).
It never throws and never blocks: no manifest ⇒ empty ⇒ every lane's fallback path. Safe to
wire before any lane consumes it. Lane A currently reaches assets via its own path; once boot
passes it in, that can collapse.
2. Ownership: I created web/dev/ — laneD_texview.html, a dev-only harness (nothing
imports it, not shipped). The eyeball law needs my textures on the real stub tube, but boot
doesn't construct assets yet, so it imports world_stub.js read-only and puts a textured
material on its geometry. Claiming web/dev/laneD_texview.html as Lane D; please ratify or
relocate in ROUND2. It doubles as the reference implementation Lane A is welcome to lift.
3. TECH suggestion (seconding Lane A): promote assets.texture(name) into the asset
contract next to get(). get() returns raw JSON; texture() returns
{map, normalMap, tile, repeat} and is what consumers actually want. Also worth adding
?sink=<port> nowhere near the flags table — it's viewer-local, not a game flag.
4. tools/qa.sh gate 5 already calls validate_manifest.py and it's green: 6 textures,
1 matcap, 1 bed, 4 sfx, audio 0.61/10 MB. It enforces urls-exist, WebP ≤1024, matcap square,
sane tile, ogg+m4a dual-ship, and warns on orphans in gen/. An absent manifest is treated
as legal (asset-free boot), not an error.
→ Lane E
The audio pack is live and the loader is ready for your engine:
const url = assets.audioUrl('beds', 'esophagus'); // best format for THIS browser, or null
const e = assets.audio('sfx', 'pellet'); // {ogg, m4a, loop, gain, seconds} | null
- Keys (not filenames): beds →
esophagus. sfx →pellet,hit_squelch,boost,pickup.assets.list('textures')etc. enumerates what actually shipped. audioUrlprobes the browser and returns m4a where Opus-in-Ogg isn't playable (Safari). Both formats ship for every asset — always. Don't hardcode.ogg.- Each entry carries
gain(bed 0.5; sfx 0.5–0.7) andloop. Please respectgain— beds are loudnorm'd to −16 LUFS, sfx are peak-normalised to ≈−3.5 dBFS, so raw playback has sfx much hotter than the bed by design. bed-esophagusis exactly 24.000 s and loops seamlessly (tail crossfaded over the head, seam 0.32). Loop it withaudio.loop = true/ a looping AudioBufferSourceNode — no gap logic, no fade needed. The heartbeat is 50 bpm and divides the loop exactly.- Your procedural fallback beeps stay necessary:
?localassets=0must still make noise. - Round 2 = full pack (beds per biome, torpedo/gate/alarm, boss stingers). Tell me what names you want and I'll match your keys rather than you adapting to mine.
→ Lane C
Got your round-2 list (bolus_chunk, eosinophil_swarm, candida_bloom) and the boss specs —
they're enough to concept from, thank you. Your esophagus wall pack shipped this round.
Early flag: candida_bloom is the right call as a decal/plane, not a GLB — a flat colony
on a curved wall is the shape TRELLIS is worst at, and a wall-hugging plaque wants to follow
(s, theta) anyway. I'll concept it as a sprite/decal sheet unless you object.
The tapeworm (long, thin, segmented) is my expected fal.ai candidate — PIPELINE flags thin
geometry as TRELLIS's known weakness. It'd go on FAL_LIST.md for John's approval, ~30¢.
Open questions / round 2
- Slug rename (§→ Lane A) — needs F to sequence it with A's map deletion. One step, both files.
- Hero meshes — the round-2 headline: ENDO-1 ship + C's three enemies. concept → sf3d draft → trellis_mac → normalize → thumb. Blobby organics are TRELLIS's sweet spot; the tapeworm probably isn't.
- Colon + mouth walls (2 each) to finish the biome set, + acid/decal sprites.
- Full audio pack — pending E's key list.
normalize.py+glb_stat.pystill need porting from PROCITY (round 2, with the meshes) — the house GLB law (metres, +Y up, ≤5k tris, no Draco) is unenforced until they exist.validate_manifest.pyalready fails any model over 5k tris that declares its count.