feat: boulders
This commit is contained in:
parent
9aa0ec6a3f
commit
680e08aadc
7 changed files with 169 additions and 11 deletions
96
engine/scene/Boulder.ts
Normal file
96
engine/scene/Boulder.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import type { Vec3 } from "../math/Vec3"
|
||||
import type { Mesh } from "./Mesh"
|
||||
|
||||
const TAU = Math.PI * 2
|
||||
|
||||
/** One procedural boulder. `radius` is the overall size; `seed` drives the
|
||||
* per-rock lumpiness and squash so no two look alike. It sits partly sunk into
|
||||
* the ground at `position`, like a real rock. */
|
||||
export type Boulder = {
|
||||
/** Resting point on the ground (the rock is centered a bit above and buried). */
|
||||
position: Vec3
|
||||
radius: number
|
||||
seed: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Low-poly boulder geometry in the same faceted flat-shaded style as the rest of
|
||||
* the world. A squashed, per-vertex-jittered sphere reads as an angular chunk of
|
||||
* rock once flat shading gives each face its own tone. Radial jitter is kept
|
||||
* seam- and pole-safe (the longitude wrap and both poles reuse one value) so the
|
||||
* rock never cracks open. `build` appends into a caller-owned mesh, so a whole
|
||||
* field of boulders batches into a single draw call.
|
||||
*/
|
||||
export namespace Boulder {
|
||||
export function build(boulder: Boulder, mesh: Mesh): void {
|
||||
const rand = rng(boulder.seed)
|
||||
const seg = 5
|
||||
const rings = 4
|
||||
const r = boulder.radius
|
||||
// Squat and slightly oval, so it reads as a rock, not a ball.
|
||||
const sx = r * (0.8 + rand() * 0.5)
|
||||
const sy = r * (0.55 + rand() * 0.3)
|
||||
const sz = r * (0.8 + rand() * 0.5)
|
||||
const cx = boulder.position.x
|
||||
const cz = boulder.position.z
|
||||
// Center lifted less than the half-height, so the base sinks into the ground.
|
||||
const cy = boulder.position.y + sy * 0.55
|
||||
|
||||
const jitter = jitterGrid(seg, rings, rand)
|
||||
const start = mesh.vertices.length
|
||||
for (let ir = 0; ir <= rings; ir++) {
|
||||
const phi = (ir / rings) * Math.PI
|
||||
const cyv = Math.cos(phi)
|
||||
const crv = Math.sin(phi)
|
||||
for (let is = 0; is <= seg; is++) {
|
||||
const theta = (is / seg) * TAU
|
||||
const j = jitter[ir][is]
|
||||
mesh.vertices.push({
|
||||
pos: {
|
||||
x: cx + crv * Math.cos(theta) * sx * j,
|
||||
y: cy + cyv * sy * j,
|
||||
z: cz + crv * Math.sin(theta) * sz * j,
|
||||
},
|
||||
uv: { x: (is / seg) * 1.5, y: (ir / rings) * 1.5 },
|
||||
})
|
||||
}
|
||||
}
|
||||
const row = seg + 1
|
||||
for (let ir = 0; ir < rings; ir++) {
|
||||
for (let is = 0; is < seg; is++) {
|
||||
const p = start + ir * row + is
|
||||
mesh.indices.push(p, p + 1, p + row + 1, p, p + row + 1, p + row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Per-vertex radial scale in ~0.72..1.14 for a chunky, angular surface. The
|
||||
* longitude seam (last column == first) and each pole row (one shared value)
|
||||
* match so the mesh stays closed. */
|
||||
function jitterGrid(seg: number, rings: number, rand: () => number): number[][] {
|
||||
const grid: number[][] = []
|
||||
for (let ir = 0; ir <= rings; ir++) {
|
||||
const pole = ir === 0 || ir === rings
|
||||
grid[ir] = []
|
||||
for (let is = 0; is <= seg; is++) {
|
||||
if (is === seg || (pole && is > 0)) {
|
||||
grid[ir][is] = grid[ir][0]
|
||||
} else {
|
||||
grid[ir][is] = 0.72 + rand() * 0.42
|
||||
}
|
||||
}
|
||||
}
|
||||
return grid
|
||||
}
|
||||
|
||||
/** Deterministic 0..1 generator (mulberry32) seeded per boulder. */
|
||||
function rng(seed: number): () => number {
|
||||
let a = seed >>> 0
|
||||
return () => {
|
||||
a = (a + 0x6D2B79F5) | 0
|
||||
let t = Math.imul(a ^ (a >>> 15), 1 | a)
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), 61 | t)
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue