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;