type Verts = Vec<([f32; 3], [f32; 3], [f32; 3], [f32; 2])>;
pub(super) fn build_terrain(args: &serde_json::Value) -> Result<(Verts, Vec<u16>), String> {
let half_width = args
.get("half_width")
.and_then(|v| v.as_f64())
.unwrap_or(64.0) as f32;
let half_depth = args
.get("half_depth")
.and_then(|v| v.as_f64())
.unwrap_or(64.0) as f32;
let subdivisions = args
.get("subdivisions")
.and_then(|v| v.as_u64())
.unwrap_or(64)
.clamp(4, 255) as usize;
let amplitude = args
.get("amplitude")
.and_then(|v| v.as_f64())
.unwrap_or(4.0) as f32;
let cols = subdivisions + 1;
let rows = subdivisions + 1;
if cols * rows > 65536 {
return Err(format!(
"terrain subdivisions {} produces {} vertices, exceeding the u16 limit; use subdivisions ≤ 255",
subdivisions,
cols * rows
));
}
let color = [0.55f32, 0.62, 0.42];
let mut positions: Vec<[f32; 3]> = Vec::with_capacity(cols * rows);
for row in 0..rows {
for col in 0..cols {
let s = col as f32 / subdivisions as f32;
let t = row as f32 / subdivisions as f32;
let x = -half_width + s * half_width * 2.0;
let z = -half_depth + t * half_depth * 2.0;
let y = terrain_height(col as u32, row as u32, subdivisions as u32, amplitude);
positions.push([x, y, z]);
}
}
let mut normals: Vec<[f32; 3]> = vec![[0.0, 0.0, 0.0]; cols * rows];
for row in 0..subdivisions {
for col in 0..subdivisions {
let tl = row * cols + col;
let tr = tl + 1;
let bl = tl + cols;
let br = bl + 1;
let n1 = super::vec3_face_normal(positions[tl], positions[bl], positions[tr]);
super::vec3_add(&mut normals[tl], n1);
super::vec3_add(&mut normals[bl], n1);
super::vec3_add(&mut normals[tr], n1);
let n2 = super::vec3_face_normal(positions[tr], positions[bl], positions[br]);
super::vec3_add(&mut normals[tr], n2);
super::vec3_add(&mut normals[bl], n2);
super::vec3_add(&mut normals[br], n2);
}
}
let mut idxs: Vec<u16> = Vec::with_capacity(subdivisions * subdivisions * 6);
let mut verts: Verts = Vec::with_capacity(cols * rows);
for i in 0..cols * rows {
let [x, y, z] = positions[i];
let normal = super::vec3_normalise(normals[i]);
verts.push(([x, y, z], normal, color, [x, z]));
}
for row in 0..subdivisions {
for col in 0..subdivisions {
let tl = (row * cols + col) as u16;
let tr = tl + 1;
let bl = tl + cols as u16;
let br = bl + 1;
idxs.extend_from_slice(&[tl, bl, tr, tr, bl, br]);
}
}
Ok((verts, idxs))
}
fn terrain_height(col: u32, row: u32, subdivisions: u32, amplitude: f32) -> f32 {
let octaves: &[(u32, f32)] = &[(1, 1.00), (3, 0.40), (9, 0.15)];
let mut sum = 0.0f32;
let mut weight_sum = 0.0f32;
for &(divisor, weight) in octaves {
let scale = (subdivisions / divisor).max(1);
let gx = col / scale;
let gy = row / scale;
let fx = (col % scale) as f32 / scale as f32;
let fy = (row % scale) as f32 / scale as f32;
let h00 = lattice_val(gx, gy);
let h10 = lattice_val(gx + 1, gy);
let h01 = lattice_val(gx, gy + 1);
let h11 = lattice_val(gx + 1, gy + 1);
let top = h00 + (h10 - h00) * fx;
let bot = h01 + (h11 - h01) * fx;
sum += (top + (bot - top) * fy) * weight;
weight_sum += weight;
}
let normalised = sum / weight_sum;
(normalised - 0.05).max(0.0) * amplitude
}
fn lattice_val(x: u32, y: u32) -> f32 {
let h = lcg_hash(x.wrapping_mul(1619).wrapping_add(y.wrapping_mul(31337)));
(h & 0xFF) as f32 / 255.0
}
fn lcg_hash(mut v: u32) -> u32 {
v = v.wrapping_mul(1664525).wrapping_add(1013904223);
v ^= v >> 16;
v
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grid_counts_and_extents_follow_the_subdivision_count() {
let args = serde_json::json!({
"half_width": 10.0, "half_depth": 5.0, "subdivisions": 8, "amplitude": 3.0,
});
let (verts, idxs) = build_terrain(&args).unwrap();
assert_eq!(verts.len(), 9 * 9);
assert_eq!(idxs.len(), 8 * 8 * 6);
assert!(idxs.iter().all(|&i| (i as usize) < verts.len()));
let mut mn = [f32::INFINITY; 3];
let mut mx = [f32::NEG_INFINITY; 3];
for (pos, ..) in &verts {
for k in 0..3 {
mn[k] = mn[k].min(pos[k]);
mx[k] = mx[k].max(pos[k]);
}
}
assert_eq!((mn[0], mx[0]), (-10.0, 10.0));
assert_eq!((mn[2], mx[2]), (-5.0, 5.0));
assert!(mn[1] >= 0.0 && mx[1] <= 3.0);
assert!(mx[1] > mn[1], "expected height variation, got flat {mx:?}");
}
#[test]
fn every_vertex_normal_is_unit_length() {
let args = serde_json::json!({"subdivisions": 6, "amplitude": 5.0});
let (verts, _) = build_terrain(&args).unwrap();
for (_, normal, ..) in &verts {
let len =
(normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
assert!((len - 1.0).abs() < 1e-5, "normal {normal:?} is not unit");
assert!(normal[1] > 0.0, "terrain normal points down: {normal:?}");
}
}
#[test]
fn zero_amplitude_produces_a_flat_grid() {
let args = serde_json::json!({"subdivisions": 4, "amplitude": 0.0});
let (verts, _) = build_terrain(&args).unwrap();
assert!(verts.iter().all(|(pos, ..)| pos[1] == 0.0));
assert!(verts.iter().all(|(_, n, ..)| *n == [0.0, 1.0, 0.0]));
}
#[test]
fn subdivisions_clamp_to_the_supported_range() {
let (small, _) = build_terrain(&serde_json::json!({"subdivisions": 0})).unwrap();
assert_eq!(small.len(), 5 * 5);
let (large, idxs) = build_terrain(&serde_json::json!({"subdivisions": 4096})).unwrap();
assert_eq!(large.len(), 256 * 256);
assert_eq!(idxs.len(), 255 * 255 * 6);
}
#[test]
fn terrain_height_is_deterministic_and_scales_with_amplitude() {
let a = terrain_height(3, 7, 32, 4.0);
assert_eq!(a, terrain_height(3, 7, 32, 4.0));
assert!((terrain_height(3, 7, 32, 8.0) - a * 2.0).abs() < 1e-5);
for col in 0..32u32 {
for row in 0..32u32 {
assert!(terrain_height(col, row, 32, 4.0) >= 0.0);
}
}
}
#[test]
fn lattice_values_are_normalised_and_position_dependent() {
for x in 0..16u32 {
for y in 0..16u32 {
let v = lattice_val(x, y);
assert!((0.0..=1.0).contains(&v), "lattice_val({x},{y}) = {v}");
}
}
assert_ne!(lattice_val(0, 0), lattice_val(1, 0));
assert_ne!(lcg_hash(0), lcg_hash(1));
}
}