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}