From 9b8aabe0db4c683f0191008d0427e114f523988c Mon Sep 17 00:00:00 2001 From: m3ultra Date: Fri, 17 Jul 2026 01:29:22 +1000 Subject: [PATCH] Add sail UVs and Lane E's weave texture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E's recipe verbatim: grid (i,j) -> (u,v), repeat 6x6, sRGB. Without the uv attribute three defaults every vertex to (0,0), the map samples one texel and the membrane reads as flat colour — which looks like the texture failing rather than like a bug, so E flagged it ahead of time. Texture URL resolves against import.meta.url rather than the server root, same as weather.js's STORM_DIR and the same thing the integrator's /world/ -> relative pass was fixing. A missing texture warns and falls back to flat colour instead of throwing: the cloth is the game, the weave is a finish, and it must not be able to take the sail down. Added anisotropy 4 — the sail is mostly viewed at a raking angle from underneath, which is exactly where an unfiltered weave turns to moire. Co-Authored-By: Claude Opus 4.8 --- web/world/js/sail.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/web/world/js/sail.js b/web/world/js/sail.js index 8a0b972..becc223 100644 --- a/web/world/js/sail.js +++ b/web/world/js/sail.js @@ -755,9 +755,37 @@ export async function createSailView(rig, { color = 0xd8c48a } = {}) { geo.setAttribute('position', new THREE.BufferAttribute(verts, 3)); geo.setIndex(new THREE.BufferAttribute(new Uint16Array(rig.tris), 1)); + // UVs: the grid IS the UV space, so (i, j) maps straight to (u, v). Without + // this three defaults every vertex to (0,0), the map samples one texel, and + // the membrane reads as flat colour — which looks like the texture failing + // rather than like a bug. (Lane E's recipe, THREADS.) + const N = rig.N; + const uv = new Float32Array(N * N * 2); + for (let j = 0, k = 0; j < N; j++) { + for (let i = 0; i < N; i++, k += 2) { uv[k] = i / (N - 1); uv[k + 1] = j / (N - 1); } + } + geo.setAttribute('uv', new THREE.BufferAttribute(uv, 2)); + const mat = new THREE.MeshStandardMaterial({ color, side: THREE.DoubleSide, roughness: 0.92, metalness: 0.0, }); + + // Resolved against this module rather than the server root: the same reason + // weather.js builds STORM_DIR this way, and it's what the integrator's + // /world/ -> relative pass was fixing. A missing texture must not take the + // sail down — the cloth is the game, the weave is a finish. + try { + const tex = await new THREE.TextureLoader().loadAsync( + new URL('../models/textures/sail_weave.png', import.meta.url).href, + ); + tex.wrapS = tex.wrapT = THREE.RepeatWrapping; + tex.repeat.set(6, 6); // ~6 tiles across a 5 m sail (E's density) + tex.colorSpace = THREE.SRGBColorSpace; // r175 spelling — `encoding` is gone + tex.anisotropy = 4; // it's viewed at a raking angle from underneath + mat.map = tex; // keep mat.color: the weave multiplies it + } catch (err) { + console.warn('[sail] weave texture missing, falling back to flat colour:', err.message); + } const mesh = new THREE.Mesh(geo, mat); mesh.castShadow = true; // the shadow IS the product mesh.receiveShadow = true;