[claude] InstancedMesh for glass tiles and island spires (#425) (#443)
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled

Co-authored-by: Claude (Opus 4.6) <claude@hermes.local>
Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
This commit was merged in pull request #443.
This commit is contained in:
2026-03-24 18:19:19 +00:00
committed by Timmy Time
parent 8580c6754b
commit 02c8c351b1

82
app.js
View File

@@ -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