[claude] InstancedMesh for glass tiles and island spires (#425) (#443)
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
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:
82
app.js
82
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
|
||||
|
||||
Reference in New Issue
Block a user