app.js: 5416 → 528 lines (entry point, animation loop, event wiring) modules/state.js: shared mutable state object modules/constants.js: color palette modules/matrix-rain.js: matrix rain canvas effect modules/scene-setup.js: scene, camera, renderer, lighting, stars modules/platform.js: glass platform, perlin noise, floating island, clouds modules/heatmap.js: commit heatmap modules/sigil.js: Timmy sigil modules/controls.js: mouse, overview, zoom, photo mode modules/effects.js: energy beam, sovereignty meter, rune ring modules/earth.js: holographic earth modules/warp.js: warp tunnel, crystals, lightning modules/dual-brain.js: dual-brain holographic panel modules/audio.js: Web Audio, spatial, portal hums modules/debug.js: debug mode, websocket, session export modules/celebrations.js: easter egg, shockwave, fireworks modules/portals.js: portal loading modules/bookshelves.js: floating bookshelves, spine textures modules/oath.js: The Oath interactive SOUL.md modules/panels.js: agent status board, LoRA panel modules/weather.js: weather system, portal health modules/extras.js: gravity zones, speech, timelapse, bitcoin Largest file: 528 lines (app.js). No file exceeds 1000. All files pass node --check. No refactoring — mechanical split only.
448 lines
16 KiB
JavaScript
448 lines
16 KiB
JavaScript
// === 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);
|