Skip to main content

unlit3d_examples/scenes/
mesh_topologies.rs

1//! The mesh-topology scene: every primitive topology, drawn indexed and
2//! non-indexed.
3//!
4//! The other scenes draw triangle lists, so the primitive a pipeline
5//! assembles — and the draw call that feeds it — is the one thing none of them
6//! pins. This scene draws one patch in each of the five topologies, once from
7//! an index buffer and once straight from the vertex buffer, in a 5x2 grid:
8//! the top row is indexed, the bottom row draws its vertices in order.
9//!
10//! The indexed row stores its vertices **shuffled** and restores the draw
11//! order through the index buffer, so the two rows draw the same picture only
12//! if that buffer is really read: a draw that ignored it would assemble the
13//! patch from the storage order instead and show a differently-shaped patch.
14//! The rows are therefore compared against each other, and the pair is what
15//! carries the evidence.
16//!
17//! Strips carry a second trap. A strip pipeline has to declare the index width
18//! its draw binds, and here that width is the mesh source's own choice — the
19//! narrowest format the mesh's vertex count fits, widened while baking in a
20//! pool offset on a device without `base_vertex`. The top row's strip cells
21//! are what exercise the specializer that derives it from the mesh.
22
23use super::{SceneControl, SceneDef, SceneOptions, TEST_SIZE, camera_looking_at, default_options};
24use unlit_ecs::{Bundle, DynamicBundle, bundle};
25use unlit_wgpu::pipeline::LitOptions;
26use unlit3d::prelude::*;
27
28/// The topologies drawn, left to right.
29///
30/// The top row indexes each one; the bottom row draws its vertices in order.
31const TOPOLOGIES: [wgpu::PrimitiveTopology; 5] = [
32    wgpu::PrimitiveTopology::PointList,
33    wgpu::PrimitiveTopology::LineList,
34    wgpu::PrimitiveTopology::LineStrip,
35    wgpu::PrimitiveTopology::TriangleList,
36    wgpu::PrimitiveTopology::TriangleStrip,
37];
38
39/// How many vertices one patch has.
40///
41/// Six is the shortest count every topology above draws something with: a
42/// triangle list needs a multiple of three, a triangle strip needs at least
43/// three, and a line list needs an even count. It is also what makes the patch
44/// a zigzag with a readable shape in each topology.
45const PATCH_VERTICES: usize = 6;
46
47/// The patch in its own space, in draw order.
48///
49/// The shape is chosen so every topology draws something a reader can tell
50/// apart: a point list draws six dots down it, a line list three separate
51/// strokes, a line strip one connected zigzag, a triangle list two detached
52/// triangles and a triangle strip a filled band.
53///
54/// Every consecutive triple winds the same way, because the triangle list
55/// culls back faces: two triangles of opposite winding would draw as one, and
56/// the snapshot would no longer show what the topology does.
57const PATCH: [[f32; 2]; PATCH_VERTICES] = [
58    [-1.0, -1.0],
59    [0.0, -1.0],
60    [-1.0, 1.0],
61    [0.0, 1.0],
62    [1.0, -1.0],
63    [1.0, 1.0],
64];
65
66/// The colour of each patch vertex, in draw order.
67///
68/// Distinct per vertex so a shuffle reads as one: a colour rides on its
69/// vertex, so a draw that ignored the index buffer would tie each colour to
70/// the wrong corner as well.
71const VERTEX_COLORS: [[u8; 4]; PATCH_VERTICES] = [
72    [255, 80, 80, 255],
73    [255, 200, 60, 255],
74    [120, 230, 90, 255],
75    [70, 200, 240, 255],
76    [110, 130, 255, 255],
77    [230, 100, 220, 255],
78];
79
80/// The draw vertex each storage slot holds in the indexed row.
81///
82/// A permutation of `0..PATCH_VERTICES`: the indexed row uploads vertex
83/// `INDEX_ORDER[slot]` of [`PATCH`] into `slot`, so its vertex buffer is in a
84/// different order from the one it draws in.
85const INDEX_ORDER: [usize; PATCH_VERTICES] = [3, 0, 5, 1, 4, 2];
86
87/// The storage slots the non-indexed row draws, which is the draw order
88/// itself: a draw with no index buffer reads its vertices in the order they
89/// are stored.
90const DRAW_ORDER: [usize; PATCH_VERTICES] = [0, 1, 2, 3, 4, 5];
91
92/// The distance between neighbouring columns, in world units.
93const COLUMN_SPACING: f32 = 0.9;
94
95/// How far each row's patches sit above and below the centre, in world units.
96const ROW_OFFSET: f32 = 0.78;
97
98/// Half of a patch's width and height, in world units.
99///
100/// Small enough that neighbouring patches stay clear of each other: the
101/// columns are [`COLUMN_SPACING`] apart and the rows twice [`ROW_OFFSET`],
102/// while a patch is only this wide.
103const PATCH_HALF_WIDTH: f32 = 0.38;
104
105/// Half of a patch's height, in world units.
106///
107/// Taller than it is wide because a cell is: five columns across the frame and
108/// two rows down it leave each cell about twice as tall as it is wide, and a
109/// patch fills its cell without touching its neighbours.
110const PATCH_HALF_HEIGHT: f32 = 0.62;
111
112/// How far in front of the patches the camera sits, in world units.
113///
114/// A perspective camera looking straight down `-Z` from here: every patch lies
115/// in the `z = 0` plane, so they all project at the same scale and the grid
116/// stays even on screen.
117const CAMERA_DISTANCE: f32 = 3.0;
118
119/// One patch of the grid: a mesh and the pipeline that assembles it.
120///
121/// A patch drawn by another topology's pipeline is not the patch its vertices
122/// describe, so the two only mean anything together.
123struct Patch {
124    mesh: GpuMesh,
125    pipeline: GpuRenderPipeline,
126}
127
128impl Bundle for Patch {
129    fn into_bundle(self) -> DynamicBundle {
130        bundle![self.mesh, self.pipeline]
131    }
132}
133
134/// The scene: one patch per topology, indexed and non-indexed.
135pub static SCENE: SceneDef = SceneDef {
136    id: "mesh_topologies",
137    title: "Mesh topologies",
138    description: "every primitive topology, indexed and non-indexed",
139    size: TEST_SIZE,
140    baseline: Some(TEST_SIZE),
141    clear: None,
142    frames: 1,
143    step_seconds: None,
144    samples: 1,
145    depth: true,
146    ui: false,
147    reproducible_ui: false,
148    build,
149};
150
151/// The options one topology draws with.
152///
153/// The topology is part of the options, so it is part of the pipeline key: a
154/// family variant is compiled per topology, which is why an entity cannot
155/// share a key with a different one.
156///
157/// Culling is off, unlike the other scenes. What this scene pins is the
158/// primitive each pipeline assembles, and culling would hide half of it: a
159/// strip alternates the winding of the triangles it composes, so a back-face
160/// test would drop every other one and the snapshot could no longer show the
161/// topology whole. Pinning culling is the opaque scenes' job.
162fn options_for(topology: wgpu::PrimitiveTopology) -> LitOptions {
163    let mut options = default_options();
164    options.primitive.topology = topology;
165    options.primitive.cull_mode = None;
166    options
167}
168
169/// The world position of the cell at `column` in `row`, in world units.
170fn cell_center(column: usize, row: usize) -> [f32; 2] {
171    let last = TOPOLOGIES.len() as f32 - 1.0;
172    let x = (column as f32 - last * 0.5) * COLUMN_SPACING;
173    let y = if row == 0 { ROW_OFFSET } else { -ROW_OFFSET };
174    [x, y]
175}
176
177/// Place patch vertex `vertex` into the cell whose centre is `center`.
178fn place(vertex: usize, center: [f32; 2]) -> [f32; 3] {
179    let [x, y] = PATCH[vertex];
180    [
181        center[0] + x * PATCH_HALF_WIDTH,
182        center[1] + y * PATCH_HALF_HEIGHT,
183        0.0,
184    ]
185}
186
187/// The index buffer the indexed row binds.
188///
189/// Draw vertex `i` is stored in slot `slot` where `INDEX_ORDER[slot] == i`, so
190/// the buffer names that slot in draw order and nothing else. It is an
191/// `u32` list like any other caller's; the mesh source picks the width it
192/// uploads them as.
193fn index_buffer() -> Vec<u32> {
194    (0..PATCH_VERTICES)
195        .map(|draw_vertex| {
196            INDEX_ORDER
197                .iter()
198                .position(|&stored| stored == draw_vertex)
199                .expect("the storage order is a permutation of the draw order") as u32
200        })
201        .collect()
202}
203
204/// Build the topology scene's world content.
205fn build(
206    world: &mut World,
207    context: RenderContext,
208    pass: Entity,
209    _ui: Option<Entity>,
210    size: (u32, u32),
211    _options: SceneOptions,
212) -> SceneControl {
213    let mut source = MeshSource::new(&world.root_scope(), context);
214    source.register_lit_family(&world.root_scope());
215    let source_entity = world
216        .scope_mut(pass)
217        .expect("the pass is alive")
218        .spawn(bundle![Source::new(source)]);
219
220    // One key per topology, reused by that topology's two cells: an indexed
221    // and a non-indexed draw share a pipeline wherever the topology cannot
222    // tell them apart, and a strip is the one that can, through the index
223    // width the specializer reads off the mesh.
224    let keys: Vec<LitPipelineKey> = TOPOLOGIES
225        .iter()
226        .map(|&topology| LitPipelineKey::new(options_for(topology)))
227        .collect();
228    let indices = index_buffer();
229
230    let camera_entity = super::spawn_in(
231        world,
232        source_entity,
233        bundle![camera_looking_at(
234            glam::Vec3::new(0.0, 0.0, CAMERA_DISTANCE),
235            glam::Vec3::ZERO,
236            size,
237        ),],
238    );
239
240    let mut entities = Vec::new();
241    for (column, key) in keys.iter().enumerate() {
242        for row in 0..2 {
243            // The indexed row stores the shuffled order; the bottom row has no
244            // index buffer to reorder with, so it stores what it draws.
245            let stored: &[usize] = if row == 0 { &INDEX_ORDER } else { &DRAW_ORDER };
246            let center = cell_center(column, row);
247            let positions: Vec<[f32; 3]> =
248                stored.iter().map(|&vertex| place(vertex, center)).collect();
249            let colors: Vec<[u8; 4]> = stored.iter().map(|&vertex| VERTEX_COLORS[vertex]).collect();
250            // The variant reads no UVs, but every other scene hands the mesh
251            // one per vertex, and a slice of the right length keeps the
252            // channels describing the same vertices.
253            let uvs: Vec<[f32; 2]> = (0..PATCH_VERTICES).map(|_| [0.0, 0.0]).collect();
254            let mesh = super::with_mesh_source(world, source_entity, |source, world| {
255                source.allocate_lit_mesh(
256                    world,
257                    &key.options,
258                    LitMeshDesc {
259                        positions: &positions,
260                        uvs: Some(&uvs),
261                        colors: Some(&colors),
262                        indices: (row == 0).then_some(indices.as_slice()),
263                        ..Default::default()
264                    },
265                )
266            });
267            entities.push((column, mesh));
268        }
269    }
270
271    for (column, mesh) in entities {
272        super::spawn_in(
273            world,
274            source_entity,
275            Patch {
276                mesh,
277                pipeline: GpuRenderPipeline::new(keys[column].clone()),
278            },
279        );
280    }
281
282    SceneControl {
283        // Static: nothing about the frame animates, but the camera keeps the
284        // frame's aspect so a resized window does not stretch the patches.
285        advance: Box::new(move |world, _frame, _delta, frame_size| {
286            super::aim_camera(
287                world,
288                source_entity,
289                camera_entity,
290                glam::Vec3::new(0.0, 0.0, CAMERA_DISTANCE),
291                glam::Vec3::ZERO,
292                frame_size,
293            );
294        }),
295        snapshot: Box::new(|frame| (frame == 0).then(|| "mesh_topologies.webp".to_owned())),
296    }
297}