Spaces:
Running
Running
| import * as THREE from 'three'; | |
| import { G } from './globals.js'; | |
| import { MOUNTAINS } from './config.js'; | |
| const COLOR_N = new THREE.Color(MOUNTAINS.colorNight); | |
| const COLOR_D = new THREE.Color(MOUNTAINS.colorDay); | |
| const TMP_COLOR = new THREE.Color(); | |
| function dayFactor(t) { | |
| return 0.5 - 0.5 * Math.cos(2 * Math.PI * t); | |
| } | |
| function buildMountainRing({ radius, segments, baseHeight, heightVar, yOffset }) { | |
| const vertCount = segments * 2; | |
| const positions = new Float32Array(vertCount * 3); | |
| const colors = new Float32Array(vertCount * 3); | |
| const aH = new Float32Array(vertCount); // 0 at bottom ring, 1 at peaks | |
| const indices = new Uint16Array(segments * 6); | |
| const colBottom = new THREE.Color(MOUNTAINS.colorBase); | |
| const colTop = new THREE.Color(MOUNTAINS.colorPeak); | |
| function ridge(a) { | |
| const s1 = Math.sin(a * 3.0) * 0.7 + Math.sin(a * 7.0) * 0.3; | |
| const s2 = Math.sin(a * 1.7 + 1.3) * 0.5 + Math.sin(a * 4.3 + 0.7) * 0.5; | |
| const v = 0.5 * s1 + 0.5 * s2; | |
| return baseHeight + heightVar * (0.5 + 0.5 * v); | |
| } | |
| for (let i = 0; i < segments; i++) { | |
| const a0 = (i / segments) * Math.PI * 2; | |
| const h = ridge(a0); | |
| const x = Math.cos(a0) * radius; | |
| const z = Math.sin(a0) * radius; | |
| const iBot = i * 3; | |
| positions[iBot + 0] = x; | |
| positions[iBot + 1] = yOffset; | |
| positions[iBot + 2] = z; | |
| const iTop = (segments + i) * 3; | |
| positions[iTop + 0] = x; | |
| positions[iTop + 1] = yOffset + h; | |
| positions[iTop + 2] = z; | |
| colors[iBot + 0] = colBottom.r; | |
| colors[iBot + 1] = colBottom.g; | |
| colors[iBot + 2] = colBottom.b; | |
| colors[iTop + 0] = colTop.r; | |
| colors[iTop + 1] = colTop.g; | |
| colors[iTop + 2] = colTop.b; | |
| aH[i] = 0.0; // bottom vertex ratio | |
| aH[segments + i] = 1.0; // top vertex ratio | |
| } | |
| let idx = 0; | |
| for (let i = 0; i < segments; i++) { | |
| const n = (i + 1) % segments; | |
| const b0 = i; | |
| const b1 = n; | |
| const t0 = segments + i; | |
| const t1 = segments + n; | |
| indices[idx++] = b0; | |
| indices[idx++] = t0; | |
| indices[idx++] = b1; | |
| indices[idx++] = b1; | |
| indices[idx++] = t0; | |
| indices[idx++] = t1; | |
| } | |
| const geo = new THREE.BufferGeometry(); | |
| geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); | |
| geo.setAttribute('color', new THREE.BufferAttribute(colors, 3)); | |
| geo.setIndex(new THREE.BufferAttribute(indices, 1)); | |
| geo.setAttribute('aH', new THREE.BufferAttribute(aH, 1)); | |
| geo.computeBoundingSphere(); | |
| return geo; | |
| } | |
| export function setupMountains() { | |
| if (!MOUNTAINS.enabled) return; | |
| const geo = buildMountainRing({ | |
| radius: MOUNTAINS.radius, | |
| segments: MOUNTAINS.segments, | |
| baseHeight: MOUNTAINS.baseHeight, | |
| heightVar: MOUNTAINS.heightVar, | |
| yOffset: MOUNTAINS.yOffset | |
| }); | |
| const mat = new THREE.MeshBasicMaterial({ | |
| color: new THREE.Color(MOUNTAINS.colorDay), | |
| vertexColors: true, | |
| fog: false, // keep visible even in heavy fog | |
| depthTest: true, | |
| depthWrite: false, | |
| side: THREE.DoubleSide, // ensure visible regardless of winding | |
| transparent: true | |
| }); | |
| mat.onBeforeCompile = (shader) => { | |
| shader.uniforms.uFadeEdge = { value: MOUNTAINS.fadeEdge ?? 0.35 }; | |
| shader.uniforms.uFadePow = { value: MOUNTAINS.fadePow ?? 1.5 }; | |
| shader.vertexShader = ( | |
| 'attribute float aH;\n' + | |
| 'varying float vH;\n' + | |
| shader.vertexShader | |
| ).replace( | |
| '#include <begin_vertex>', | |
| `#include <begin_vertex>\n vH = aH;` | |
| ); | |
| shader.fragmentShader = ( | |
| 'varying float vH;\n uniform float uFadeEdge; uniform float uFadePow;\n' + | |
| shader.fragmentShader | |
| ).replace( | |
| '#include <dithering_fragment>', | |
| `diffuseColor.a *= pow(smoothstep(uFadeEdge, 1.0, vH), uFadePow);\n#include <dithering_fragment>` | |
| ); | |
| }; | |
| const mesh = new THREE.Mesh(geo, mat); | |
| mesh.castShadow = false; | |
| mesh.receiveShadow = false; | |
| mesh.renderOrder = -10; | |
| G.scene.add(mesh); | |
| G.mountains = mesh; | |
| } | |
| export function updateMountains(delta) { | |
| if (!MOUNTAINS.enabled || !G.mountains) return; | |
| const p = G.player ? G.player.pos : G.camera.position; | |
| G.mountains.position.set(p.x, 0, p.z); | |
| const dayF = dayFactor(G.timeOfDay || 0); | |
| TMP_COLOR.copy(COLOR_N).lerp(COLOR_D, dayF); | |
| G.mountains.material.color.copy(TMP_COLOR); | |
| } | |