Add the Blender flashlight asset with a 600 lm beam and reduced bloom, shared-mode self badge, and compact phone controls. Relocate the summit marker, blend terrain seams, widen walking routes, and align local server configuration on port 2027.
67 lines
3.9 KiB
TypeScript
67 lines
3.9 KiB
TypeScript
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(x<xs[0] || x>xs.at(-1)! || z<zs[0] || z>zs.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();
|
|
}
|