// === GLASS PLATFORM + PERLIN NOISE + FLOATING ISLAND + CLOUDS === import * as THREE from 'three'; import { NEXUS } from './constants.js'; import { scene } from './scene-setup.js'; // === GLASS PLATFORM === const glassPlatformGroup = new THREE.Group(); const platformFrameMat = new THREE.MeshStandardMaterial({ color: 0x0a1828, metalness: 0.9, roughness: 0.1, emissive: new THREE.Color(NEXUS.colors.accent).multiplyScalar(0.06), }); const platformRimGeo = new THREE.RingGeometry(4.7, 5.3, 64); const platformRim = new THREE.Mesh(platformRimGeo, platformFrameMat); platformRim.rotation.x = -Math.PI / 2; platformRim.castShadow = true; platformRim.receiveShadow = true; glassPlatformGroup.add(platformRim); const borderTorusGeo = new THREE.TorusGeometry(5.0, 0.1, 6, 64); const borderTorus = new THREE.Mesh(borderTorusGeo, platformFrameMat); borderTorus.rotation.x = Math.PI / 2; borderTorus.castShadow = true; borderTorus.receiveShadow = true; glassPlatformGroup.add(borderTorus); const glassTileMat = new THREE.MeshPhysicalMaterial({ color: new THREE.Color(NEXUS.colors.accent), transparent: true, opacity: 0.09, roughness: 0.0, metalness: 0.0, transmission: 0.92, thickness: 0.06, side: THREE.DoubleSide, depthWrite: false, }); const glassEdgeBaseMat = new THREE.LineBasicMaterial({ color: NEXUS.colors.accent, transparent: true, opacity: 0.55, }); export const GLASS_TILE_SIZE = 0.85; const GLASS_TILE_GAP = 0.14; const GLASS_TILE_STEP = GLASS_TILE_SIZE + GLASS_TILE_GAP; export const GLASS_RADIUS = 4.55; const tileGeo = new THREE.PlaneGeometry(GLASS_TILE_SIZE, GLASS_TILE_SIZE); const tileEdgeGeo = new THREE.EdgesGeometry(tileGeo); /** @type {Array<{mat: THREE.LineBasicMaterial, distFromCenter: number}>} */ export const glassEdgeMaterials = []; 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; _tileSlots.push({ x, z, distFromCenter }); } } 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); 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 }); } export const voidLight = new THREE.PointLight(NEXUS.colors.accent, 0.5, 14); voidLight.position.set(0, -3.5, 0); glassPlatformGroup.add(voidLight); scene.add(glassPlatformGroup); glassPlatformGroup.traverse(obj => { if (obj.isMesh) obj.userData.zoomLabel = 'Glass Platform'; }); // === PERLIN NOISE === function createPerlinNoise() { const p = new Uint8Array(256); for (let i = 0; i < 256; i++) p[i] = i; 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 === (function buildFloatingIsland() { const ISLAND_RADIUS = 9.5; const SEGMENTS = 96; const SIZE = ISLAND_RADIUS * 2; function islandFBm(nx, nz) { const wx = perlin(nx * 0.5 + 3.7, nz * 0.5 + 1.2) * 0.55; const wz = perlin(nx * 0.5 + 8.3, nz * 0.5 + 5.9) * 0.55; const px = nx + wx, pz = nz + wz; let h = 0; h += perlin(px, pz ) * 1.000; h += perlin(px * 2, pz * 2 ) * 0.500; h += perlin(px * 4, pz * 4 ) * 0.250; h += perlin(px * 8, pz * 8 ) * 0.125; h += perlin(px * 16, pz * 16 ) * 0.063; h /= 1.938; const ridge = 1.0 - Math.abs(perlin(px * 3.1 + 5.0, pz * 3.1 + 7.0)); return h * 0.78 + ridge * 0.22; } const geo = new THREE.PlaneGeometry(SIZE, SIZE, SEGMENTS, SEGMENTS); geo.rotateX(-Math.PI / 2); const pos = geo.attributes.position; const vCount = pos.count; const rawHeights = new Float32Array(vCount); for (let i = 0; i < vCount; i++) { const x = pos.getX(i); const z = pos.getZ(i); const dist = Math.sqrt(x * x + z * z) / ISLAND_RADIUS; const rimNoise = perlin(x * 0.38 + 10, z * 0.38 + 10) * 0.10; const edgeFactor = Math.max(0, 1 - Math.pow(Math.max(0, dist - rimNoise), 2.4)); const h = islandFBm(x * 0.15, z * 0.15); const height = ((h + 1) * 0.5) * edgeFactor * 3.2; pos.setY(i, height); rawHeights[i] = height; } geo.computeVertexNormals(); const colBuf = new Float32Array(vCount * 3); for (let i = 0; i < vCount; i++) { const h = rawHeights[i]; let r, g, b; if (h < 0.25) { r = 0.11; g = 0.09; b = 0.07; } else if (h < 0.75) { const t = (h - 0.25) / 0.50; r = 0.11 + t * 0.13; g = 0.09 + t * 0.09; b = 0.07 + t * 0.06; } else if (h < 1.4) { const t = (h - 0.75) / 0.65; r = 0.24 + t * 0.12; g = 0.18 + t * 0.10; b = 0.13 + t * 0.10; } else if (h < 2.2) { const t = (h - 1.4) / 0.80; r = 0.36 + t * 0.14; g = 0.28 + t * 0.11; b = 0.23 + t * 0.13; } else { const t = Math.min(1, (h - 2.2) / 0.9); r = 0.50 + t * 0.05; g = 0.39 + t * 0.10; b = 0.36 + t * 0.28; } colBuf[i * 3] = r; colBuf[i * 3 + 1] = g; colBuf[i * 3 + 2] = b; } geo.setAttribute('color', new THREE.BufferAttribute(colBuf, 3)); const topMat = new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.86, metalness: 0.05, }); const topMesh = new THREE.Mesh(geo, topMat); topMesh.castShadow = true; topMesh.receiveShadow = true; const crystalMat = new THREE.MeshStandardMaterial({ color: new THREE.Color(NEXUS.colors.accent).multiplyScalar(0.55), emissive: new THREE.Color(NEXUS.colors.accent), emissiveIntensity: 0.5, roughness: 0.08, metalness: 0.25, transparent: true, opacity: 0.80, }); const CRYSTAL_MIN_H = 2.05; /** @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; if (Math.sqrt(bx * bx + bz * bz) > ISLAND_RADIUS * 0.72) continue; const edF = Math.max(0, 1 - Math.pow(Math.sqrt(bx * bx + bz * bz) / ISLAND_RADIUS, 2.4)); const candidateH = ((islandFBm(bx * 0.15, bz * 0.15) + 1) * 0.5) * edF * 3.2; if (candidateH < CRYSTAL_MIN_H) continue; const jx = bx + perlin(bx * 0.7 + 20, bz * 0.7 + 20) * 0.55; const jz = bz + perlin(bx * 0.7 + 30, bz * 0.7 + 30) * 0.55; if (Math.sqrt(jx * jx + jz * jz) > ISLAND_RADIUS * 0.68) continue; const clusterSize = 2 + Math.floor(Math.abs(perlin(bx * 0.5 + 40, bz * 0.5 + 40)) * 3); for (let c = 0; c < clusterSize; c++) { const angle = (c / clusterSize) * Math.PI * 2 + perlin(bx + c, bz + c) * 1.4; const spread = 0.08 + Math.abs(perlin(bx + c * 5, bz + c * 5)) * 0.22; const sx = jx + Math.cos(angle) * spread; const sz = jz + Math.sin(angle) * spread; 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; _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, }); } } } 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); const BOTTOM_SEGS_R = 52; const BOTTOM_SEGS_V = 10; const BOTTOM_HEIGHT = 2.6; const bottomGeo = new THREE.CylinderGeometry( ISLAND_RADIUS * 0.80, ISLAND_RADIUS * 0.28, BOTTOM_HEIGHT, BOTTOM_SEGS_R, BOTTOM_SEGS_V, true ); const bPos = bottomGeo.attributes.position; for (let i = 0; i < bPos.count; i++) { const bx = bPos.getX(i); const bz = bPos.getZ(i); const by = bPos.getY(i); const angle = Math.atan2(bz, bx); const r = Math.sqrt(bx * bx + bz * bz); const radDisp = perlin(Math.cos(angle) * 1.6 + 50, Math.sin(angle) * 1.6 + 50) * 0.65; const vNorm = (by + BOTTOM_HEIGHT * 0.5) / BOTTOM_HEIGHT; const stalDisp = (1 - vNorm) * Math.abs(perlin(bx * 0.35 + 70, by * 0.7 + bz * 0.35)) * 0.9; const newR = r + radDisp; bPos.setX(i, (bx / r) * newR); bPos.setZ(i, (bz / r) * newR); bPos.setY(i, by - stalDisp); } bottomGeo.computeVertexNormals(); const bottomMat = new THREE.MeshStandardMaterial({ color: 0x0c0a08, roughness: 0.93, metalness: 0.02 }); const bottomMesh = new THREE.Mesh(bottomGeo, bottomMat); bottomMesh.position.y = -BOTTOM_HEIGHT * 0.5; bottomMesh.castShadow = true; const capGeo = new THREE.CircleGeometry(ISLAND_RADIUS * 0.28, 48); capGeo.rotateX(Math.PI / 2); const capMesh = new THREE.Mesh(capGeo, bottomMat); capMesh.position.y = -(BOTTOM_HEIGHT + 0.1); const islandGroup = new THREE.Group(); islandGroup.add(topMesh); islandGroup.add(crystalGroup); islandGroup.add(bottomMesh); islandGroup.add(capMesh); islandGroup.position.y = -2.8; scene.add(islandGroup); })(); // === PROCEDURAL CLOUD LAYER === const CLOUD_LAYER_Y = -6.0; const CLOUD_DIMENSIONS = 120; const CLOUD_THICKNESS = 15; const CLOUD_OPACITY = 0.6; const cloudGeometry = new THREE.BoxGeometry(CLOUD_DIMENSIONS, CLOUD_THICKNESS, CLOUD_DIMENSIONS, 8, 4, 8); const CloudShader = { uniforms: { 'uTime': { value: 0.0 }, 'uCloudColor': { value: new THREE.Color(0x88bbff) }, 'uNoiseScale': { value: new THREE.Vector3(0.015, 0.015, 0.015) }, 'uDensity': { value: 0.8 }, }, vertexShader: ` varying vec3 vWorldPosition; void main() { vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } `, fragmentShader: ` uniform float uTime; uniform vec3 uCloudColor; uniform vec3 uNoiseScale; uniform float uDensity; varying vec3 vWorldPosition; vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } vec4 mod289(vec4 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; } vec4 permute(vec4 x) { return mod289(((x * 34.0) + 1.0) * x); } vec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; } float snoise(vec3 v) { const vec2 C = vec2(1.0/6.0, 1.0/3.0); const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); vec3 i = floor(v + dot(v, C.yyy)); vec3 x0 = v - i + dot(i, C.xxx); vec3 g = step(x0.yzx, x0.xyz); vec3 l = 1.0 - g; vec3 i1 = min(g.xyz, l.zxy); vec3 i2 = max(g.xyz, l.zxy); vec3 x1 = x0 - i1 + C.xxx; vec3 x2 = x0 - i2 + C.yyy; vec3 x3 = x0 - D.yyy; i = mod289(i); vec4 p = permute(permute(permute( i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0)); float n_ = 0.142857142857; vec3 ns = n_ * D.wyz - D.xzx; vec4 j = p - 49.0 * floor(p * ns.z * ns.z); vec4 x_ = floor(j * ns.z); vec4 y_ = floor(j - 7.0 * x_); vec4 x = x_ * ns.x + ns.yyyy; vec4 y = y_ * ns.x + ns.yyyy; vec4 h = 1.0 - abs(x) - abs(y); vec4 b0 = vec4(x.xy, y.xy); vec4 b1 = vec4(x.zw, y.zw); vec4 s0 = floor(b0) * 2.0 + 1.0; vec4 s1 = floor(b1) * 2.0 + 1.0; vec4 sh = -step(h, vec4(0.0)); vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy; vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww; vec3 p0 = vec3(a0.xy, h.x); vec3 p1 = vec3(a0.zw, h.y); vec3 p2 = vec3(a1.xy, h.z); vec3 p3 = vec3(a1.zw, h.w); vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3))); p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w; vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0); m = m * m; return 42.0 * dot(m*m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3))); } void main() { vec3 noiseCoord = vWorldPosition * uNoiseScale + vec3(uTime * 0.003, 0.0, uTime * 0.002); float noiseVal = snoise(noiseCoord) * 0.500; noiseVal += snoise(noiseCoord * 2.0) * 0.250; noiseVal += snoise(noiseCoord * 4.0) * 0.125; noiseVal /= 0.875; float density = smoothstep(0.25, 0.85, noiseVal * 0.5 + 0.5); density *= uDensity; float layerBottom = ${(CLOUD_LAYER_Y - CLOUD_THICKNESS * 0.5).toFixed(1)}; float yNorm = (vWorldPosition.y - layerBottom) / ${CLOUD_THICKNESS.toFixed(1)}; float fadeFactor = smoothstep(0.0, 0.15, yNorm) * smoothstep(1.0, 0.85, yNorm); gl_FragColor = vec4(uCloudColor, density * fadeFactor * ${CLOUD_OPACITY.toFixed(1)}); if (gl_FragColor.a < 0.04) discard; } `, }; export const cloudMaterial = new THREE.ShaderMaterial({ uniforms: CloudShader.uniforms, vertexShader: CloudShader.vertexShader, fragmentShader: CloudShader.fragmentShader, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending, side: THREE.DoubleSide, }); const clouds = new THREE.Mesh(cloudGeometry, cloudMaterial); clouds.position.y = CLOUD_LAYER_Y; scene.add(clouds);