From 02c8c351b168ef72ef980a629293cc2c6578fd17 Mon Sep 17 00:00:00 2001 From: "Claude (Opus 4.6)" Date: Tue, 24 Mar 2026 18:19:19 +0000 Subject: [PATCH] [claude] InstancedMesh for glass tiles and island spires (#425) (#443) Co-authored-by: Claude (Opus 4.6) Co-committed-by: Claude (Opus 4.6) --- app.js | 82 ++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 59 insertions(+), 23 deletions(-) diff --git a/app.js b/app.js index d20cb70..86afc00 100644 --- a/app.js +++ b/app.js @@ -295,29 +295,43 @@ const tileEdgeGeo = new THREE.EdgesGeometry(tileGeo); /** @type {Array<{mat: THREE.LineBasicMaterial, distFromCenter: number}>} */ const glassEdgeMaterials = []; +// Pre-collect valid tile positions so we know the InstancedMesh count upfront +const _tileDummy = new THREE.Object3D(); +/** @type {Array<{x: number, z: number, distFromCenter: number}>} */ +const _tileSlots = []; for (let row = -5; row <= 5; row++) { for (let col = -5; col <= 5; col++) { const x = col * GLASS_TILE_STEP; const z = row * GLASS_TILE_STEP; const distFromCenter = Math.sqrt(x * x + z * z); if (distFromCenter > GLASS_RADIUS) continue; - - // Transparent glass tile - const tile = new THREE.Mesh(tileGeo, glassTileMat.clone()); - tile.rotation.x = -Math.PI / 2; - tile.position.set(x, 0, z); - glassPlatformGroup.add(tile); - - // Glowing edge lines - const mat = glassEdgeBaseMat.clone(); - const edges = new THREE.LineSegments(tileEdgeGeo, mat); - edges.rotation.x = -Math.PI / 2; - edges.position.set(x, 0.002, z); - glassPlatformGroup.add(edges); - glassEdgeMaterials.push({ mat, distFromCenter }); + _tileSlots.push({ x, z, distFromCenter }); } } +// Transparent glass tiles — InstancedMesh: all tiles rendered in one draw call +const glassTileIM = new THREE.InstancedMesh(tileGeo, glassTileMat, _tileSlots.length); +glassTileIM.instanceMatrix.setUsage(THREE.StaticDrawUsage); +_tileDummy.rotation.x = -Math.PI / 2; +for (let i = 0; i < _tileSlots.length; i++) { + const { x, z } = _tileSlots[i]; + _tileDummy.position.set(x, 0, z); + _tileDummy.updateMatrix(); + glassTileIM.setMatrixAt(i, _tileDummy.matrix); +} +glassTileIM.instanceMatrix.needsUpdate = true; +glassPlatformGroup.add(glassTileIM); + +// Glowing edge lines — individual LineSegments retained for per-distance opacity animation +for (const { x, z, distFromCenter } of _tileSlots) { + const mat = glassEdgeBaseMat.clone(); + const edges = new THREE.LineSegments(tileEdgeGeo, mat); + edges.rotation.x = -Math.PI / 2; + edges.position.set(x, 0.002, z); + glassPlatformGroup.add(edges); + glassEdgeMaterials.push({ mat, distFromCenter }); +} + // Void shimmer — faint point light below the glass, emphasising the infinite depth const voidLight = new THREE.PointLight(NEXUS.colors.accent, 0.5, 14); voidLight.position.set(0, -3.5, 0); @@ -491,8 +505,12 @@ const perlin = createPerlinNoise(); }); const CRYSTAL_MIN_H = 2.05; // only spawn on high terrain - const crystalGroup = new THREE.Group(); + // --- Crystal spire formations (InstancedMesh) --- + // Two-pass: collect params first so we know the count, then build InstancedMesh. + // Unit cone (r=1, h=1) scaled per-instance replaces N unique ConeGeometry objects. + /** @type {Array<{sx:number,sz:number,posY:number,rotX:number,rotZ:number,scaleXZ:number,scaleY:number}>} */ + const _spireData = []; for (let row = -5; row <= 5; row++) { for (let col = -5; col <= 5; col++) { const bx = col * 1.75, bz = row * 1.75; @@ -518,18 +536,36 @@ const perlin = createPerlinNoise(); const spireScale = 0.14 + (candidateH - CRYSTAL_MIN_H) * 0.11; const spireH = spireScale * (0.8 + Math.abs(perlin(sx, sz)) * 0.45); const spireR = spireH * 0.17; - - const spireGeo = new THREE.ConeGeometry(spireR, spireH * 2.8, 5); - const spire = new THREE.Mesh(spireGeo, crystalMat); - spire.position.set(sx, candidateH + spireH * 0.5, sz); - spire.rotation.z = perlin(sx * 2, sz * 2) * 0.28; - spire.rotation.x = perlin(sx * 3 + 1, sz * 3 + 1) * 0.18; - spire.castShadow = true; - crystalGroup.add(spire); + _spireData.push({ + sx, sz, + posY: candidateH + spireH * 0.5, + rotX: perlin(sx * 3 + 1, sz * 3 + 1) * 0.18, + rotZ: perlin(sx * 2, sz * 2) * 0.28, + scaleXZ: spireR, + scaleY: spireH * 2.8, + }); } } } + // Single InstancedMesh: N spires rendered in one draw call + const _spireDummy = new THREE.Object3D(); + const spireBaseGeo = new THREE.ConeGeometry(1, 1, 5); + const crystalGroup = new THREE.Group(); + const spireIM = new THREE.InstancedMesh(spireBaseGeo, crystalMat, _spireData.length); + spireIM.castShadow = true; + spireIM.instanceMatrix.setUsage(THREE.StaticDrawUsage); + for (let i = 0; i < _spireData.length; i++) { + const { sx, sz, posY, rotX, rotZ, scaleXZ, scaleY } = _spireData[i]; + _spireDummy.position.set(sx, posY, sz); + _spireDummy.rotation.set(rotX, 0, rotZ); + _spireDummy.scale.set(scaleXZ, scaleY, scaleXZ); + _spireDummy.updateMatrix(); + spireIM.setMatrixAt(i, _spireDummy.matrix); + } + spireIM.instanceMatrix.needsUpdate = true; + crystalGroup.add(spireIM); + // --- Noise-displaced rocky underside with stalactite drips --- // CylinderGeometry with open top (openEnded=true) so only the tapered side // wall is visible. Vertices are radially and vertically displaced by Perlin