import type { BufferGeometry } from 'three'; import { SUMMIT_TERRAIN } from '../constants/summitTerrain.ts'; import { SUMMIT_PLATEAU } from '../constants/summitPlateau.ts'; import { TERRAIN } from '../constants/terrain.ts'; import { structurePosition } from './structures.ts'; import { terrainGeometryHeightAt } from './terrainGeometry.ts'; import type { TerrainMetadata } from './terrainGeometry.ts'; /** Apply only to the Gwanaksan mesh. Original height arrays and reference origin stay unchanged. */ export function applySummitTerrain(geometry: BufferGeometry, meta: TerrainMetadata) { const start = structurePosition(SUMMIT_TERRAIN.start.latitude, SUMMIT_TERRAIN.start.longitude, meta.origin); // Sample all residuals BEFORE editing any vertex, avoiding order-dependent corrections. const controls = SUMMIT_TERRAIN.controls.map(control => { const point = structurePosition(control.latitude, control.longitude, meta.origin); const original = terrainGeometryHeightAt(geometry, point.x, point.z) + meta.observerSurfaceElevation + (point.x * point.x + point.z * point.z) / (2 * TERRAIN.earthRadiusMeters); return { x: point.x, z: point.z, residual: control.elevation - original }; }); const positions = geometry.getAttribute('position'); const key = (x: number, z: number) => `${Math.round(x * 1000)},${Math.round(z * 1000)}`; const slab = new Map(SUMMIT_PLATEAU.heights.map(([x,z,height]) => [key(x,z),height])); const xs = [...new Set(SUMMIT_PLATEAU.heights.map(p=>p[0]))].sort((a,b)=>a-b); const zs = [...new Set(SUMMIT_PLATEAU.heights.map(p=>p[1]))].sort((a,b)=>a-b); const authoredHeight = (x:number,z:number) => { if(xxs.at(-1)! || zzs.at(-1)!)return undefined; const cell=(axis:number[],v:number)=>{let lo=0,hi=axis.length-1;while(hi-lo>1){const m=(lo+hi)>>1;if(axis[m]<=v)lo=m;else hi=m;}return lo;}; const i=cell(xs,x),j=cell(zs,z),u=(x-xs[i])/(xs[i+1]-xs[i]),v=(z-zs[j])/(zs[j+1]-zs[j]); const a=slab.get(key(xs[i],zs[j]))!,b=slab.get(key(xs[i+1],zs[j]))!; const c=slab.get(key(xs[i],zs[j+1]))!,d=slab.get(key(xs[i+1],zs[j+1]))!; return u+v<=1 ? a*(1-u-v)+b*u+c*v : b*(1-v)+c*(1-u)+d*(u+v-1); }; for (let i = 0; i < positions.count; i++) { const x = positions.getX(i), z = positions.getZ(i); const radius = Math.hypot(x - start.x, z - start.z); if (radius >= SUMMIT_TERRAIN.outerRadius) continue; let weighted = 0, weights = 0; for (const control of controls) { const distanceSquared = (x - control.x) ** 2 + (z - control.z) ** 2; if (distanceSquared < 1e-8) { weighted = control.residual; weights = 1; break; } const weight = 1 / (distanceSquared * distanceSquared); weighted += control.residual * weight; weights += weight; } const t = Math.max(0, (radius - SUMMIT_TERRAIN.coreRadius) / (SUMMIT_TERRAIN.outerRadius - SUMMIT_TERRAIN.coreRadius)); const fade = 1 - t * t * (3 - 2 * t); positions.setY(i, positions.getY(i) + weighted / weights * fade); const authored = authoredHeight(x - start.x, z - start.z); if (authored !== undefined) { // Fade the sculpt into the corrected terrain before its rectangular edge. // This also suppresses discontinuities in the exported outermost samples. const localX = x - start.x, localZ = z - start.z; const smooth = (distance: number) => { const t = Math.min(1, Math.max(0, distance / SUMMIT_TERRAIN.plateauBlendMeters)); return t * t * (3 - 2 * t); }; const blend = smooth(Math.min(localX - xs[0], xs.at(-1)! - localX)) * smooth(Math.min(localZ - zs[0], zs.at(-1)! - localZ)); const target = authored - meta.observerSurfaceElevation - (x * x + z * z) / (2 * TERRAIN.earthRadiusMeters); positions.setY(i, positions.getY(i) + (target - positions.getY(i)) * blend); } } positions.needsUpdate = true; geometry.computeVertexNormals(); geometry.computeBoundingSphere(); }