Skip to main content

unlit3dx_examples/scene/
floor.rs

1//! The ground the balls land on: one fixed body in physics, one instance in
2//! the render world, wearing a checker so motion reads against it.
3//!
4//! The floor is the scene's only fixed body, and the only instance that never
5//! moves, so it is built once and then left alone: the ground is queued before
6//! anything can drop on it, and afterwards there is nothing to do. The
7//! instance is built by one job in the render world, so the mesh, the checker
8//! and the instance that names them need no round trip between them.
9
10use glam::Vec3;
11use unlit_ecs::World;
12use unlit3d::components::Transform;
13use unlit3dx::prelude::*;
14
15use super::Scene;
16use crate::render::{
17    allocate_mesh, allocate_rgba_material, cuboid_mesh, instance_of, linear_sampler,
18};
19
20/// The ground box's half extents, in metres.
21pub const GROUND_HALF: Vec3 = Vec3::new(12.0, 0.5, 12.0);
22
23/// Where the ground box sits; its top face is the y = 0 plane.
24pub const GROUND_POSITION: Vec3 = Vec3::new(0.0, -0.5, 0.0);
25
26/// The ground's base color, as sRGB bytes.
27pub const GROUND_COLOR: [u8; 4] = [126, 130, 138, 255];
28
29/// The checker's darker square, as sRGB bytes.
30pub const GROUND_CHECKER_DARK: [u8; 4] = [88, 92, 100, 255];
31
32/// The ground texture's edge length in pixels; the checker draws eight
33/// squares per side, so each square is a block of
34/// `GROUND_CHECKER_SIZE / 8` pixels.
35pub const GROUND_CHECKER_SIZE: u32 = 256;
36
37/// The checker squares per texture side.
38const GROUND_CHECKER_SQUARES: u32 = 8;
39
40/// The friction the ground gives the balls.
41const GROUND_FRICTION: f32 = 0.9;
42
43/// The ground's checker texture: `GROUND_CHECKER_SQUARES` squares per
44/// side, the light ones in [`GROUND_COLOR`] and the dark ones in
45/// [`GROUND_CHECKER_DARK`].
46#[must_use]
47pub fn checker_ground() -> Vec<[u8; 4]> {
48    let block = GROUND_CHECKER_SIZE / GROUND_CHECKER_SQUARES;
49    let mut pixels = Vec::with_capacity((GROUND_CHECKER_SIZE * GROUND_CHECKER_SIZE) as usize);
50    for y in 0..GROUND_CHECKER_SIZE {
51        for x in 0..GROUND_CHECKER_SIZE {
52            let square = (x / block + y / block).is_multiple_of(2);
53            pixels.push(if square {
54                GROUND_COLOR
55            } else {
56                GROUND_CHECKER_DARK
57            });
58        }
59    }
60    pixels
61}
62
63/// Put the ground under the scene: one fixed body in physics, one instance in
64/// the render world.
65///
66/// Nothing waits for the instance: one that arrives a frame late is simply a
67/// frame without a floor.
68pub fn spawn_floor(scene: &Scene) {
69    if let Err(error) = scene.physics.send(Box::new(|world: &mut World| {
70        spawn_fixed_cuboid(world, GROUND_POSITION, GROUND_HALF, GROUND_FRICTION);
71    })) {
72        log::error!("the physics world refused the ground ({error:?})");
73    }
74    if let Err(error) = scene.render.send(ground_job()) {
75        log::error!("the render world refused the ground ({error:?})");
76    }
77}
78
79/// The job that builds the ground's instance in the render world.
80fn ground_job() -> Job {
81    Box::new(|world: &mut World| {
82        let mesh = allocate_mesh(world, &cuboid_mesh(GROUND_HALF));
83        let material = allocate_rgba_material(
84            world,
85            (GROUND_CHECKER_SIZE, GROUND_CHECKER_SIZE),
86            &checker_ground(),
87            linear_sampler(),
88        );
89        instance_of(
90            world,
91            mesh,
92            material,
93            Transform {
94                translation: GROUND_POSITION,
95                ..Default::default()
96            },
97        );
98    })
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use crate::render::cuboid_mesh;
105    use crate::scene::shapes::tests::assert_faces_outward;
106
107    /// The ground's box faces outward like every other mesh: the unlit
108    /// pipeline culls back faces, so an inward face would show the floor as a
109    /// hole.
110    #[test]
111    fn the_ground_mesh_faces_outward() {
112        assert_faces_outward("cuboid", &cuboid_mesh(GROUND_HALF));
113    }
114
115    /// The checker alternates both ways: a texture of one color would draw a
116    /// flat floor, and the tumbling balls would read as sliding.
117    #[test]
118    fn the_checker_alternates_both_ways() {
119        let block = GROUND_CHECKER_SIZE / GROUND_CHECKER_SQUARES;
120        let pixels = checker_ground();
121        let at = |x: u32, y: u32| pixels[(y * GROUND_CHECKER_SIZE + x) as usize];
122        assert_ne!(at(0, 0), at(block, 0));
123        assert_ne!(at(0, 0), at(0, block));
124        assert_eq!(at(0, 0), at(block, block));
125    }
126}