122 lines
3.5 KiB
TypeScript
122 lines
3.5 KiB
TypeScript
|
|
import { STRIDE, type Mesh } from "../scene/Mesh"
|
||
|
|
|
||
|
|
/** A bounded ground surface. Implementations may be procedural, sampled, or
|
||
|
|
* loaded; callers only depend on world-space height sampling. */
|
||
|
|
export type Terrain = {
|
||
|
|
minX: number
|
||
|
|
minZ: number
|
||
|
|
maxX: number
|
||
|
|
maxZ: number
|
||
|
|
heightAt: (x: number, z: number) => number
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Parameters for the built-in rolling terrain generator. Values describe the
|
||
|
|
* surface only; level-specific holes and materials belong to level data. */
|
||
|
|
export type RollingTerrainConfig = {
|
||
|
|
inner: number
|
||
|
|
outer: number
|
||
|
|
blend: number
|
||
|
|
amplitude: number
|
||
|
|
frequency: number
|
||
|
|
peakHeight: number
|
||
|
|
peakFrequency: number
|
||
|
|
peakStart: number
|
||
|
|
}
|
||
|
|
|
||
|
|
export namespace Terrain {
|
||
|
|
/** Built-in square world with a flat center, rolling hills, and edge ridges. */
|
||
|
|
export function rolling(config: RollingTerrainConfig): Terrain {
|
||
|
|
return {
|
||
|
|
minX: -config.outer,
|
||
|
|
minZ: -config.outer,
|
||
|
|
maxX: config.outer,
|
||
|
|
maxZ: config.outer,
|
||
|
|
heightAt(x, z) {
|
||
|
|
const r = Math.max(Math.abs(x), Math.abs(z))
|
||
|
|
if (r <= config.inner) {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
const rise = smoothstep(config.inner, config.inner + config.blend, r)
|
||
|
|
const hills = config.amplitude * bumps(x, z, config.frequency)
|
||
|
|
const k = Math.min(
|
||
|
|
1,
|
||
|
|
(r - config.inner) / (config.outer - config.inner),
|
||
|
|
)
|
||
|
|
const peaks =
|
||
|
|
config.peakHeight *
|
||
|
|
ridges(x, z, config.peakFrequency) *
|
||
|
|
smoothstep(config.peakStart, 1, k)
|
||
|
|
return rise * (hills + peaks)
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function height(terrain: Terrain, x: number, z: number): number {
|
||
|
|
return terrain.heightAt(x, z)
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Append one sampled heightfield patch. `include` is level policy evaluated at
|
||
|
|
* each quad center, allowing arbitrary holes without teaching terrain what
|
||
|
|
* occupies them. */
|
||
|
|
export function patch(
|
||
|
|
terrain: Terrain,
|
||
|
|
mesh: Mesh,
|
||
|
|
x0: number,
|
||
|
|
z0: number,
|
||
|
|
x1: number,
|
||
|
|
z1: number,
|
||
|
|
cols: number,
|
||
|
|
rows: number,
|
||
|
|
uvScale: number,
|
||
|
|
include?: (x: number, z: number) => boolean,
|
||
|
|
): void {
|
||
|
|
const base = mesh.verts.length / STRIDE
|
||
|
|
const dx = (x1 - x0) / cols
|
||
|
|
const dz = (z1 - z0) / rows
|
||
|
|
const rowLength = cols + 1
|
||
|
|
for (let row = 0; row <= rows; row++) {
|
||
|
|
const z = z0 + row * dz
|
||
|
|
for (let col = 0; col <= cols; col++) {
|
||
|
|
const x = x0 + col * dx
|
||
|
|
mesh.verts.push(x, terrain.heightAt(x, z), z, x * uvScale, z * uvScale)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for (let row = 0; row < rows; row++) {
|
||
|
|
for (let col = 0; col < cols; col++) {
|
||
|
|
const cx = x0 + (col + 0.5) * dx
|
||
|
|
const cz = z0 + (row + 0.5) * dz
|
||
|
|
if (include !== undefined && !include(cx, cz)) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
const p = base + row * rowLength + col
|
||
|
|
mesh.indices.push(
|
||
|
|
p,
|
||
|
|
p + rowLength + 1,
|
||
|
|
p + 1,
|
||
|
|
p,
|
||
|
|
p + rowLength,
|
||
|
|
p + rowLength + 1,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function bumps(x: number, z: number, frequency: number): number {
|
||
|
|
const a = Math.sin(x * frequency) * Math.cos(z * frequency)
|
||
|
|
const b = Math.sin((x + z) * frequency * 0.5 + 1.7) * 0.5
|
||
|
|
return (a + b + 1.5) / 3
|
||
|
|
}
|
||
|
|
|
||
|
|
function ridges(x: number, z: number, frequency: number): number {
|
||
|
|
const n =
|
||
|
|
Math.sin(x * frequency + 1.3) * Math.cos(z * frequency - 0.7) * 0.7 +
|
||
|
|
Math.sin((x + z) * frequency * 0.6 + 2.5) * 0.3
|
||
|
|
return 1 - Math.abs(n)
|
||
|
|
}
|
||
|
|
|
||
|
|
function smoothstep(a: number, b: number, x: number): number {
|
||
|
|
const t = Math.max(0, Math.min(1, (x - a) / (b - a || 1e-4)))
|
||
|
|
return t * t * (3 - 2 * t)
|
||
|
|
}
|
||
|
|
}
|