Skip to main content

concinnity_core/geometry/
mod.rs

1//! The geometry the engine can build without any source file: voxel-chunk
2//! streaming (`build_chunk_mesh` / `build_chunk_impostor_mesh` regenerate a
3//! chunk's mesh as it streams in), the glass/water quad generators the GPU
4//! backends call, the procedural mesh generators (room, box, cylinder, plane,
5//! sphere, terrain, skybox, extrude, heightfield-from-pixels), and the shared
6//! low-level mesh math (per-vertex tangents, face normals, the vertex tuple
7//! type).
8//!
9//! The generators produce `(Vec<Vert>, Vec<u16>)`; packing that into a payload
10//! is [`crate::bake::mesh`]. The cook crate's JSON compile front end parses
11//! world.jsonl args and calls down into both.
12
13// Procedural voxel-chunk generation, consumed by the backends' chunk-streaming
14// path.
15mod chunk_gen;
16mod extrude;
17pub mod glass_quad;
18mod heightfield;
19mod primitives;
20mod room;
21mod skybox;
22mod terrain;
23mod voxel;
24pub mod water_grid;
25
26pub use extrude::build_extrude;
27pub use heightfield::{HeightfieldField, build_heightfield_from_pixels};
28pub use primitives::{build_box, build_cylinder, build_plane, build_sphere};
29pub use room::build_room_geometry;
30pub use skybox::build_skybox;
31pub use terrain::build_terrain;
32
33use crate::math::vec3::{vec3_add, vec3_normalise};
34use alloc::format;
35use alloc::string::String;
36use alloc::vec;
37use alloc::vec::Vec;
38
39pub use chunk_gen::{ChunkBlockType, ChunkGenerator};
40// Shared with the cook crate's `compile_voxel_chunk_payload`, which builds the
41// same palette the runtime `build_chunk_mesh` consumes.
42pub use voxel::{PaletteSlot, build_voxel_mesh};
43
44/// Interleaved CPU vertex tuple the geometry generators produce before packing
45/// into the GPU `Vertex`: position, normal, color, uv. Public so the cook crate's
46/// generators and payload compilers can name the same shape the runtime tangent
47/// pass consumes.
48pub type Vert = ([f32; 3], [f32; 3], [f32; 3], [f32; 2]);
49
50// Convert a payload-form joint back into the args-form `SkeletonJoint`.
51fn payload_joint_to_def(
52    j: crate::gfx::mesh_payload::PayloadJoint,
53) -> crate::components::SkeletonJoint {
54    crate::components::SkeletonJoint {
55        name: j.name,
56        parent: j.parent,
57        translation: j.translation,
58        rotation_deg: j.rotation_deg,
59        scale: j.scale,
60    }
61}
62
63/// Convert a payload-joint vec to the args-form vec the runtime
64/// `build_skeleton_from_joint_defs` consumes. Public so the client runtime
65/// init path can call it without re-implementing the field mapping.
66pub fn payload_joints_to_defs(
67    joints: Vec<crate::gfx::mesh_payload::PayloadJoint>,
68) -> Vec<crate::components::SkeletonJoint> {
69    joints.into_iter().map(payload_joint_to_def).collect()
70}
71
72/// Build a renderable mesh for one procedurally generated chunk.
73///
74/// The runtime counterpart of cook's `compile_voxel_chunk_payload`: it takes a
75/// chunk's already-generated block array and resolved palette and returns
76/// interleaved `Vertex` geometry directly, with no on-disk payload in between.
77/// Chunk streaming (`app::chunk_stream`) calls this on its background thread.
78pub fn build_chunk_mesh(
79    dim: [u32; 3],
80    block_size: f32,
81    blocks: &[u32],
82    palette: &[ChunkBlockType],
83) -> Result<(Vec<crate::gfx::mesh_payload::Vertex>, Vec<u16>), String> {
84    let slots: Vec<Option<PaletteSlot>> = palette
85        .iter()
86        .map(|b| {
87            if b.solid {
88                Some(PaletteSlot {
89                    uv_top: b.uv_top,
90                    uv_bottom: b.uv_bottom,
91                    uv_side: b.uv_side,
92                })
93            } else {
94                None
95            }
96        })
97        .collect();
98    let (verts, indices) = build_voxel_mesh(dim, block_size, blocks, &slots)?;
99    let tangents = compute_tangents(&verts, &indices);
100    let vertices = verts
101        .into_iter()
102        .zip(tangents)
103        .map(
104            |((pos, normal, color, uv), tangent)| crate::gfx::mesh_payload::Vertex {
105                pos,
106                normal,
107                tangent,
108                color,
109                uv,
110            },
111        )
112        .collect();
113    Ok((vertices, indices))
114}
115
116/// Build a coarse "impostor" mesh for one distant chunk from its terrain
117/// surface heights.
118///
119/// Where [`build_chunk_mesh`] emits every visible voxel face, this stands a
120/// far-away chunk in for a fraction of the triangles: the surface height
121/// sampled on a coarse `step`-block grid becomes a low-poly top surface (one
122/// quad per coarse cell), wrapped by a perimeter skirt that drops to the chunk
123/// floor to hide the gap against a nearer full-detail neighbour or the world
124/// edge. Side and subsurface geometry are dropped: invisible at impostor
125/// distance.
126///
127/// `heights[gz * (nx + 1) + gx]` is the surface block index at coarse corner
128/// `(gx, gz)`, where `nx = ceil(dx / step)`, `nz = ceil(dz / step)`, and corner
129/// `gx`'s local block column is `min(gx * step, dx)` (the last corner lands on
130/// the chunk's far edge so adjacent impostors share it exactly). The caller
131/// samples those heights from [`ChunkGenerator::surface_height_world`] at the
132/// matching world columns, which keeps neighbouring impostors watertight.
133/// `top_uv` / `side_uv` are the surface block's atlas rects.
134pub fn build_chunk_impostor_mesh(
135    dim: [u32; 3],
136    block_size: f32,
137    step: u32,
138    heights: &[i32],
139    top_uv: [f32; 4],
140    side_uv: [f32; 4],
141) -> Result<(Vec<crate::gfx::mesh_payload::Vertex>, Vec<u16>), String> {
142    let step = step.max(1);
143    let [dx, _dy, dz] = dim;
144    let nx = dx.div_ceil(step);
145    let nz = dz.div_ceil(step);
146    let cols = (nx + 1) as usize;
147    let expected = ((nx + 1) * (nz + 1)) as usize;
148    if heights.len() != expected {
149        return Err(format!(
150            "impostor mesh: expected {} height samples for a {}x{} coarse grid, got {}",
151            expected,
152            nx + 1,
153            nz + 1,
154            heights.len()
155        ));
156    }
157    let bs = block_size;
158    // Local position of coarse corner gx / gz. The last corner clamps to the
159    // chunk's far edge (dx / dz) so a non-dividing `step` still closes the mesh
160    // exactly on the boundary shared with the next chunk.
161    let cx = |gx: u32| ((gx * step).min(dx) as f32) * bs;
162    let cz = |gz: u32| ((gz * step).min(dz) as f32) * bs;
163    // Top of the surface block at corner (gx, gz): the +1 matches the full
164    // mesher, whose top face of block `h` sits at `(h + 1) * block_size`.
165    let surf_y = |gx: u32, gz: u32| ((heights[gz as usize * cols + gx as usize] + 1) as f32) * bs;
166
167    type RawVerts = Vec<([f32; 3], [f32; 3], [f32; 3], [f32; 2])>;
168    let mut verts: RawVerts = Vec::new();
169    let mut indices: Vec<u16> = Vec::new();
170    let color = [0.75f32, 0.74, 0.72];
171
172    // CCW-from-outside quad, matching `build_voxel_mesh`'s winding + UV mapping.
173    let mut emit_quad = |corners: [[f32; 3]; 4], normal: [f32; 3], uv_rect: [f32; 4]| {
174        if verts.len() + 4 > u16::MAX as usize {
175            return;
176        }
177        let base = verts.len() as u16;
178        let [u0, v0, u1, v1] = uv_rect;
179        let uvs = [[u0, v0], [u1, v0], [u1, v1], [u0, v1]];
180        for (i, p) in corners.iter().enumerate() {
181            verts.push((*p, normal, color, uvs[i]));
182        }
183        indices.extend_from_slice(&[base, base + 1, base + 2, base + 2, base + 3, base]);
184    };
185
186    // Top surface: one up-facing quad per coarse cell. Each cell carries its
187    // own 4 vertices; adjacent cells sample identical corner heights, so the
188    // duplicated corner vertices coincide and the surface stays watertight.
189    let n_up = [0.0, 1.0, 0.0];
190    for gz in 0..nz {
191        for gx in 0..nx {
192            emit_quad(
193                [
194                    [cx(gx), surf_y(gx, gz + 1), cz(gz + 1)],
195                    [cx(gx + 1), surf_y(gx + 1, gz + 1), cz(gz + 1)],
196                    [cx(gx + 1), surf_y(gx + 1, gz), cz(gz)],
197                    [cx(gx), surf_y(gx, gz), cz(gz)],
198                ],
199                n_up,
200                top_uv,
201            );
202        }
203    }
204
205    // Perimeter skirt: vertical quads from the surface edge down to the chunk
206    // floor (y = 0), one per boundary segment, facing outward. Hides the seam
207    // where a coarse impostor abuts a nearer full chunk (or the world edge).
208    let x_max = (dx as f32) * bs;
209    let z_max = (dz as f32) * bs;
210    for gx in 0..nx {
211        // -Z edge (z = 0), outward normal -Z.
212        emit_quad(
213            [
214                [cx(gx + 1), 0.0, 0.0],
215                [cx(gx), 0.0, 0.0],
216                [cx(gx), surf_y(gx, 0), 0.0],
217                [cx(gx + 1), surf_y(gx + 1, 0), 0.0],
218            ],
219            [0.0, 0.0, -1.0],
220            side_uv,
221        );
222        // +Z edge (z = z_max), outward normal +Z.
223        emit_quad(
224            [
225                [cx(gx), 0.0, z_max],
226                [cx(gx + 1), 0.0, z_max],
227                [cx(gx + 1), surf_y(gx + 1, nz), z_max],
228                [cx(gx), surf_y(gx, nz), z_max],
229            ],
230            [0.0, 0.0, 1.0],
231            side_uv,
232        );
233    }
234    for gz in 0..nz {
235        // -X edge (x = 0), outward normal -X.
236        emit_quad(
237            [
238                [0.0, 0.0, cz(gz)],
239                [0.0, 0.0, cz(gz + 1)],
240                [0.0, surf_y(0, gz + 1), cz(gz + 1)],
241                [0.0, surf_y(0, gz), cz(gz)],
242            ],
243            [-1.0, 0.0, 0.0],
244            side_uv,
245        );
246        // +X edge (x = x_max), outward normal +X.
247        emit_quad(
248            [
249                [x_max, 0.0, cz(gz + 1)],
250                [x_max, 0.0, cz(gz)],
251                [x_max, surf_y(nx, gz), cz(gz)],
252                [x_max, surf_y(nx, gz + 1), cz(gz + 1)],
253            ],
254            [1.0, 0.0, 0.0],
255            side_uv,
256        );
257    }
258
259    let tangents = compute_tangents(&verts, &indices);
260    let vertices = verts
261        .into_iter()
262        .zip(tangents)
263        .map(
264            |((pos, normal, color, uv), tangent)| crate::gfx::mesh_payload::Vertex {
265                pos,
266                normal,
267                tangent,
268                color,
269                uv,
270            },
271        )
272        .collect();
273    Ok((vertices, indices))
274}
275
276/// Compute a per-vertex tangent vector for every vertex in the mesh.
277///
278/// For each triangle the tangent is derived from the UV gradient. Contributions
279/// are accumulated at each shared vertex and then Gram-Schmidt orthogonalized
280/// against the existing normal. Degenerate UV triangles fall back to an
281/// arbitrary perpendicular so the TBN matrix is always well-defined. Shared with
282/// the cook payload compilers so baked meshes and streamed chunks derive
283/// identical tangents.
284pub fn compute_tangents(vertices: &[Vert], indices: &[u16]) -> Vec<[f32; 3]> {
285    let n = vertices.len();
286    let mut accum: Vec<[f32; 3]> = vec![[0.0; 3]; n];
287
288    let tris = indices.len() / 3;
289    for t in 0..tris {
290        let ia = indices[t * 3] as usize;
291        let ib = indices[t * 3 + 1] as usize;
292        let ic = indices[t * 3 + 2] as usize;
293        if ia >= n || ib >= n || ic >= n {
294            continue;
295        }
296        let (pa, _, _, uva) = vertices[ia];
297        let (pb, _, _, uvb) = vertices[ib];
298        let (pc, _, _, uvc) = vertices[ic];
299
300        let e1 = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
301        let e2 = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
302        let du1 = uvb[0] - uva[0];
303        let dv1 = uvb[1] - uva[1];
304        let du2 = uvc[0] - uva[0];
305        let dv2 = uvc[1] - uva[1];
306
307        let denom = du1 * dv2 - du2 * dv1;
308        let tangent = if denom.abs() < 1e-8 {
309            arbitrary_tangent(vertices[ia].1)
310        } else {
311            let r = 1.0 / denom;
312            [
313                (e1[0] * dv2 - e2[0] * dv1) * r,
314                (e1[1] * dv2 - e2[1] * dv1) * r,
315                (e1[2] * dv2 - e2[2] * dv1) * r,
316            ]
317        };
318
319        vec3_add(&mut accum[ia], tangent);
320        vec3_add(&mut accum[ib], tangent);
321        vec3_add(&mut accum[ic], tangent);
322    }
323
324    vertices
325        .iter()
326        .zip(accum)
327        .map(|((_, normal, _, _), raw)| {
328            let dot = raw[0] * normal[0] + raw[1] * normal[1] + raw[2] * normal[2];
329            let t = [
330                raw[0] - dot * normal[0],
331                raw[1] - dot * normal[1],
332                raw[2] - dot * normal[2],
333            ];
334            vec3_normalise(t)
335        })
336        .collect()
337}
338
339// Returns an arbitrary unit vector perpendicular to `normal`.
340fn arbitrary_tangent(normal: [f32; 3]) -> [f32; 3] {
341    let up = if normal[0].abs() <= normal[1].abs() && normal[0].abs() <= normal[2].abs() {
342        [1.0f32, 0.0, 0.0]
343    } else if normal[1].abs() <= normal[2].abs() {
344        [0.0, 1.0, 0.0]
345    } else {
346        [0.0, 0.0, 1.0]
347    };
348    let t = [
349        up[1] * normal[2] - up[2] * normal[1],
350        up[2] * normal[0] - up[0] * normal[2],
351        up[0] * normal[1] - up[1] * normal[0],
352    ];
353    vec3_normalise(t)
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359    use alloc::string::ToString;
360
361    // A flat coarse height grid of `(nx+1)*(nz+1)` corners all at height `h`.
362    fn flat_heights(dim: [u32; 3], step: u32, h: i32) -> Vec<i32> {
363        let nx = dim[0].div_ceil(step);
364        let nz = dim[2].div_ceil(step);
365        vec![h; ((nx + 1) * (nz + 1)) as usize]
366    }
367
368    #[test]
369    fn impostor_mesh_counts_match_cells_plus_skirt() {
370        let dim = [8, 8, 8];
371        let step = 4;
372        let heights = flat_heights(dim, step, 3);
373        let uv = [0.0, 0.0, 1.0, 1.0];
374        let (v, i) = build_chunk_impostor_mesh(dim, 1.0, step, &heights, uv, uv).expect("impostor");
375        // 2x2 top cells + 2 skirt quads per edge * 4 edges = 4 + 8 = 12 quads.
376        assert_eq!(v.len(), 12 * 4);
377        assert_eq!(i.len(), 12 * 6);
378    }
379
380    #[test]
381    fn impostor_top_surface_sits_above_the_surface_block() {
382        let dim = [8, 4, 8];
383        let bs = 2.0;
384        let heights = flat_heights(dim, 4, 1);
385        let uv = [0.0, 0.0, 1.0, 1.0];
386        let (v, _) = build_chunk_impostor_mesh(dim, bs, 4, &heights, uv, uv).expect("impostor");
387        // Surface block index 1: its top face sits at (1 + 1) * block_size.
388        let want = 2.0 * bs;
389        assert!(v.iter().any(|vert| (vert.pos[1] - want).abs() < 1e-4));
390    }
391
392    #[test]
393    fn impostor_spans_the_full_chunk_footprint() {
394        let dim = [8, 4, 8];
395        let bs = 2.0;
396        let heights = flat_heights(dim, 4, 1);
397        let uv = [0.0, 0.0, 1.0, 1.0];
398        let (v, _) = build_chunk_impostor_mesh(dim, bs, 4, &heights, uv, uv).expect("impostor");
399        let max_x = v.iter().map(|vert| vert.pos[0]).fold(0.0f32, f32::max);
400        let max_z = v.iter().map(|vert| vert.pos[2]).fold(0.0f32, f32::max);
401        assert!((max_x - (dim[0] as f32 * bs)).abs() < 1e-4);
402        assert!((max_z - (dim[2] as f32 * bs)).abs() < 1e-4);
403    }
404
405    #[test]
406    fn impostor_rejects_a_mismatched_height_grid() {
407        let dim = [8, 8, 8];
408        let uv = [0.0, 0.0, 1.0, 1.0];
409        let bad = vec![0; 3];
410        assert!(build_chunk_impostor_mesh(dim, 1.0, 4, &bad, uv, uv).is_err());
411    }
412
413    #[test]
414    fn impostor_with_step_exceeding_chunk_collapses_to_one_cell() {
415        let dim = [8, 8, 8];
416        let uv = [0.0, 0.0, 1.0, 1.0];
417        let heights = flat_heights(dim, 32, 2);
418        let (v, i) = build_chunk_impostor_mesh(dim, 1.0, 32, &heights, uv, uv).expect("impostor");
419        // One coarse cell + 1 skirt quad per edge = 1 + 4 = 5 quads.
420        assert_eq!(v.len(), 5 * 4);
421        assert_eq!(i.len(), 5 * 6);
422    }
423
424    #[test]
425    fn degenerate_uvs_still_produce_unit_tangents() {
426        let verts: Vec<Vert> = vec![
427            ([0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [1.0; 3], [0.0, 0.0]),
428            ([1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [1.0; 3], [0.0, 0.0]),
429            ([0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0; 3], [0.0, 0.0]),
430        ];
431        let tangents = compute_tangents(&verts, &[0, 1, 2]);
432        for (t, v) in tangents.iter().zip(&verts) {
433            let len = (t[0] * t[0] + t[1] * t[1] + t[2] * t[2]).sqrt();
434            assert!((len - 1.0).abs() < 1e-5);
435            let dot = t[0] * v.1[0] + t[1] * v.1[1] + t[2] * v.1[2];
436            assert!(dot.abs() < 1e-5);
437        }
438    }
439
440    #[test]
441    fn out_of_range_indices_are_skipped_by_the_tangent_pass() {
442        let verts: Vec<Vert> = vec![([0.0; 3], [0.0, 0.0, 1.0], [1.0; 3], [0.0, 0.0])];
443        // No triangle survives, so the accumulated tangent falls back to +Y.
444        let tangents = compute_tangents(&verts, &[0, 1, 2]);
445        assert_eq!(tangents, vec![[0.0, 1.0, 0.0]]);
446    }
447
448    #[test]
449    fn arbitrary_tangent_is_unit_and_perpendicular() {
450        for normal in [
451            [1.0f32, 0.0, 0.0],
452            [0.0, 1.0, 0.0],
453            [0.0, 0.0, 1.0],
454            [0.6, 0.5, 0.3],
455        ] {
456            let t = arbitrary_tangent(normal);
457            let len = (t[0] * t[0] + t[1] * t[1] + t[2] * t[2]).sqrt();
458            assert!((len - 1.0).abs() < 1e-5, "normal {normal:?}");
459            let dot = t[0] * normal[0] + t[1] * normal[1] + t[2] * normal[2];
460            assert!(dot.abs() < 1e-5, "normal {normal:?}");
461        }
462    }
463
464    #[test]
465    fn chunk_mesh_respects_solid_flags() {
466        let bt = |solid: bool| ChunkBlockType {
467            solid,
468            uv_top: [0.0, 0.0, 1.0, 1.0],
469            uv_bottom: [0.0, 0.0, 1.0, 1.0],
470            uv_side: [0.0, 0.0, 1.0, 1.0],
471        };
472        let (verts, indices) = build_chunk_mesh([1, 1, 1], 1.0, &[0], &[bt(true)]).unwrap();
473        assert_eq!(verts.len(), 24);
474        assert_eq!(indices.len(), 36);
475        let (verts, indices) = build_chunk_mesh([1, 1, 1], 1.0, &[0], &[bt(false)]).unwrap();
476        assert!(verts.is_empty() && indices.is_empty());
477        // Two adjacent solid blocks cull the two shared interior faces.
478        let (verts, _) = build_chunk_mesh([2, 1, 1], 1.0, &[0, 0], &[bt(true)]).unwrap();
479        assert_eq!(verts.len(), 10 * 4);
480    }
481
482    #[test]
483    fn payload_joints_convert_back_to_joint_defs() {
484        let pj = crate::gfx::mesh_payload::PayloadJoint {
485            name: "hip".to_string(),
486            parent: 2,
487            translation: [1.0, 2.0, 3.0],
488            rotation_deg: [4.0, 5.0, 6.0],
489            scale: [7.0, 8.0, 9.0],
490        };
491        let defs = payload_joints_to_defs(vec![pj]);
492        assert_eq!(defs.len(), 1);
493        assert_eq!(defs[0].name, "hip");
494        assert_eq!(defs[0].parent, 2);
495        assert_eq!(defs[0].translation, [1.0, 2.0, 3.0]);
496        assert_eq!(defs[0].rotation_deg, [4.0, 5.0, 6.0]);
497        assert_eq!(defs[0].scale, [7.0, 8.0, 9.0]);
498    }
499}