[claude] Procedural terrain generation for floating island (#251) #296
114
app.js
114
app.js
@@ -232,6 +232,120 @@ glassPlatformGroup.add(voidLight);
|
||||
|
||||
scene.add(glassPlatformGroup);
|
||||
|
||||
// === PERLIN NOISE ===
|
||||
// Classic Perlin noise used for procedural terrain generation.
|
||||
|
||||
function createPerlinNoise() {
|
||||
const p = new Uint8Array(256);
|
||||
for (let i = 0; i < 256; i++) p[i] = i;
|
||||
// Fisher-Yates shuffle with a fixed seed sequence for reproducibility
|
||||
let seed = 42;
|
||||
function seededRand() {
|
||||
seed = (seed * 1664525 + 1013904223) & 0xffffffff;
|
||||
return (seed >>> 0) / 0xffffffff;
|
||||
}
|
||||
for (let i = 255; i > 0; i--) {
|
||||
const j = Math.floor(seededRand() * (i + 1));
|
||||
const tmp = p[i]; p[i] = p[j]; p[j] = tmp;
|
||||
}
|
||||
const perm = new Uint8Array(512);
|
||||
for (let i = 0; i < 512; i++) perm[i] = p[i & 255];
|
||||
|
||||
function fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); }
|
||||
function lerp(a, b, t) { return a + t * (b - a); }
|
||||
function grad(hash, x, y, z) {
|
||||
const h = hash & 15;
|
||||
const u = h < 8 ? x : y;
|
||||
const v = h < 4 ? y : (h === 12 || h === 14) ? x : z;
|
||||
return ((h & 1) ? -u : u) + ((h & 2) ? -v : v);
|
||||
}
|
||||
|
||||
return function noise(x, y, z) {
|
||||
z = z || 0;
|
||||
const X = Math.floor(x) & 255, Y = Math.floor(y) & 255, Z = Math.floor(z) & 255;
|
||||
x -= Math.floor(x); y -= Math.floor(y); z -= Math.floor(z);
|
||||
const u = fade(x), v = fade(y), w = fade(z);
|
||||
const A = perm[X] + Y, AA = perm[A] + Z, AB = perm[A + 1] + Z;
|
||||
const B = perm[X + 1] + Y, BA = perm[B] + Z, BB = perm[B + 1] + Z;
|
||||
return lerp(
|
||||
lerp(lerp(grad(perm[AA], x, y, z ), grad(perm[BA], x-1, y, z ), u),
|
||||
lerp(grad(perm[AB], x, y-1, z ), grad(perm[BB], x-1, y-1, z ), u), v),
|
||||
lerp(lerp(grad(perm[AA + 1], x, y, z-1), grad(perm[BA + 1], x-1, y, z-1), u),
|
||||
lerp(grad(perm[AB + 1], x, y-1, z-1), grad(perm[BB + 1], x-1, y-1, z-1), u), v),
|
||||
w
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const perlin = createPerlinNoise();
|
||||
|
||||
// === FLOATING ISLAND TERRAIN ===
|
||||
// Procedural terrain below the glass platform, shaped like a floating rock island.
|
||||
// Heights generated via fBm (fractional Brownian motion) layered Perlin noise.
|
||||
|
||||
(function buildFloatingIsland() {
|
||||
const ISLAND_RADIUS = 9.5;
|
||||
const SEGMENTS = 90;
|
||||
const SIZE = ISLAND_RADIUS * 2;
|
||||
|
||||
const geo = new THREE.PlaneGeometry(SIZE, SIZE, SEGMENTS, SEGMENTS);
|
||||
geo.rotateX(-Math.PI / 2);
|
||||
const pos = geo.attributes.position;
|
||||
const count = pos.count;
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const x = pos.getX(i);
|
||||
const z = pos.getZ(i);
|
||||
const dist = Math.sqrt(x * x + z * z) / ISLAND_RADIUS;
|
||||
|
||||
// Island edge taper — smooth falloff toward rim
|
||||
const edgeFactor = Math.max(0, 1 - Math.pow(dist, 2.2));
|
||||
|
||||
// fBm: four octaves of Perlin noise
|
||||
const nx = x * 0.17, nz = z * 0.17;
|
||||
let h = 0;
|
||||
h += perlin(nx, nz ) * 1.000;
|
||||
h += perlin(nx * 2, nz * 2 ) * 0.500;
|
||||
h += perlin(nx * 4, nz * 4 ) * 0.250;
|
||||
h += perlin(nx * 8, nz * 8 ) * 0.125;
|
||||
h /= 1.875; // normalise to ~[-1, 1]
|
||||
|
||||
const height = ((h + 1) * 0.5) * edgeFactor * 2.6;
|
||||
pos.setY(i, height);
|
||||
}
|
||||
|
||||
geo.computeVertexNormals();
|
||||
|
||||
// Vertex colours: low=dark earth, mid=dusty stone, high=pale rock
|
||||
const colors = new Float32Array(count * 3);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const t = Math.min(1, pos.getY(i) / 2.0);
|
||||
colors[i * 3] = 0.18 + t * 0.22; // R
|
||||
colors[i * 3 + 1] = 0.14 + t * 0.16; // G
|
||||
colors[i * 3 + 2] = 0.10 + t * 0.15; // B
|
||||
}
|
||||
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
|
||||
|
||||
const topMat = new THREE.MeshStandardMaterial({
|
||||
vertexColors: true,
|
||||
roughness: 0.88,
|
||||
metalness: 0.04,
|
||||
});
|
||||
const topMesh = new THREE.Mesh(geo, topMat);
|
||||
|
||||
// Underside — tapered cylinder giving the island its rocky underbelly
|
||||
const bottomGeo = new THREE.CylinderGeometry(ISLAND_RADIUS * 0.82, ISLAND_RADIUS * 0.35, 2.0, 64, 1);
|
||||
const bottomMat = new THREE.MeshStandardMaterial({ color: 0x0c0a08, roughness: 0.92, metalness: 0.03 });
|
||||
const bottomMesh = new THREE.Mesh(bottomGeo, bottomMat);
|
||||
bottomMesh.position.y = -1.0;
|
||||
|
||||
const islandGroup = new THREE.Group();
|
||||
islandGroup.add(topMesh);
|
||||
islandGroup.add(bottomMesh);
|
||||
islandGroup.position.y = -2.8; // float below the glass platform
|
||||
scene.add(islandGroup);
|
||||
})();
|
||||
|
||||
// === COMMIT HEATMAP ===
|
||||
// Canvas-texture overlay on the floor. Each agent occupies a polar sector;
|
||||
// recent commits make that sector glow brighter. Activity decays over 24 h.
|
||||
|
||||
Reference in New Issue
Block a user