unlit3d_examples/scenes/mod.rs
1//! The example's selectable scenes.
2//!
3//! A scene is a [`SceneDef`]: the fixed size and frame count its snapshots
4//! were captured at, what offscreen target it draws into, and a
5//! [`build`](SceneDef::build) that populates an ECS world. The world it builds is the ordinary one every
6//! frame loop drives — the frame's context and [`Renderer`] are already in it
7//! — so the windowed run and the snapshot tests differ only in what they bind
8//! they do with the result.
9//!
10//! A scene's [`SceneControl`] carries the per-frame behaviour (an animated
11//! sequence, the orbit of the camera) and the snapshot each frame verifies
12//! against in the snapshot tests. Scenes without snapshots — the windowed
13//! example's own — report none.
14//!
15//! Every scene here is ported from a GPU snapshot test that used to live in
16//! `crates/unlit3d/tests`, which is why its size, frame count and content are
17//! exactly what that test froze: the stored snapshots in the
18//! `unlit3d_asset_files` submodule still verify them.
19
20pub mod animated;
21pub mod custom_pipeline;
22pub mod gltf;
23pub mod instanced_skinned_morph;
24pub mod lit;
25pub mod lit_clusters;
26pub mod mesh_and_ui;
27pub mod mesh_topologies;
28pub mod morphed;
29pub mod outline;
30pub mod skinned;
31pub mod spin_cube;
32pub mod transparent;
33pub mod ui_only;
34
35use unlit_ecs::{Bundle, DynamicBundle, bundle};
36use unlit_wgpu::pipeline::{DepthStencilPolicy, LitOptions};
37use unlit_wgpu::scene::ViewportRect;
38use unlit3d::prelude::*;
39
40/// Advances a scene's behaviour by one frame.
41///
42/// `frame` counts the frames the scene has drawn; the scenes that animate
43/// through a fixed sequence loop it, so a windowed scene keeps moving after
44/// the sequence that produced its snapshots ends. `delta` is the seconds since
45/// the previous frame. `size` is the render target's size in physical pixels,
46/// which a scene reads here rather than capturing the size it was built at: a
47/// camera whose projection has to follow the target's aspect is re-aimed from
48/// this every frame, so a window that changes shape never stretches the scene.
49pub type Advance = Box<dyn FnMut(&mut World, u32, f32, (u32, u32))>;
50
51/// Names the snapshot a frame verifies against, if it has one.
52pub type Snapshot = Box<dyn Fn(u32) -> Option<String>>;
53
54/// Builds a scene's world content and returns its per-frame behaviour.
55///
56/// The pass is the frame pass the scene draws through: its sources hang in the
57/// pass's scope, so what they own is what the pass records.
58pub type Build =
59 fn(&mut World, RenderContext, Entity, Option<Entity>, (u32, u32), SceneOptions) -> SceneControl;
60
61/// How the example's runner drives one scene.
62///
63/// The two closures are what remains of the scene once its world is built: the
64/// behaviour that advances every frame, and the snapshot the scene's test
65/// compares each frame against.
66pub struct SceneControl {
67 /// Advance the scene's behaviour by `delta` seconds.
68 pub advance: Advance,
69 /// The snapshot the scene's test compares frame `frame` against, if any.
70 ///
71 /// The name is relative to the snapshot directory, exactly as the test the
72 /// scene was ported from stored it.
73 pub snapshot: Snapshot,
74}
75
76/// One selectable scene.
77///
78/// The fields the snapshot tests draw the scene at — [`Self::size`],
79/// [`Self::frames`], [`Self::samples`] and [`Self::depth`] — reproduce the
80/// test the scene came from, so a captured frame is comparable with the
81/// snapshot it was stored against. The windowed path draws at the window's own
82/// size instead, and only [`Self::build`] is shared.
83pub struct SceneDef {
84 /// The scene's stable id, used by `--scene <ID>`.
85 pub id: &'static str,
86 /// A short title for the GUI and the scene list.
87 pub title: &'static str,
88 /// What the scene shows, one line for the GUI.
89 pub description: &'static str,
90 /// The render target size the scene's snapshots were captured at.
91 pub size: (u32, u32),
92 /// The aspect the windowed path keeps the scene's 3D content at, as a
93 /// reference resolution, or `None` for a scene that draws no 3D content and
94 /// so has an aspect to keep.
95 ///
96 /// A scene that declares one is drawn through the largest rectangle of that
97 /// aspect that fits the window, so a wide window and a narrow one show the
98 /// same picture — the wide one larger — instead of the wide one showing
99 /// more. The snapshot tests ignore this and draw at [`Self::size`], which
100 /// the snapshots were captured at.
101 ///
102 /// A UI-only scene declares none: an interface belongs on the whole window,
103 /// and letterboxing it would put bars around a panel that has no aspect of
104 /// its own.
105 pub baseline: Option<(u32, u32)>,
106 /// The colour the scene's pass clears to, or `None` to clear to the default.
107 ///
108 /// A scene whose picture is painted over a particular backdrop declares
109 /// one; every other scene leaves its pass at the default clear.
110 pub clear: Option<wgpu::Color>,
111 /// How many frames the snapshot tests draw before finishing.
112 pub frames: u32,
113 /// How long each frame stays on screen in the windowed loop, in seconds,
114 /// or `None` for a scene that animates continuously from the frame delta.
115 ///
116 /// The snapshot tests ignore this and draw the frames back to back, so a
117 /// capture stays reproducible; the windowed path uses it so a scene whose
118 /// frames were frozen as a sequence plays at a watchable pace rather than
119 /// as fast as the display refreshes.
120 pub step_seconds: Option<f32>,
121 /// The sample count of the offscreen target the snapshot tests bind.
122 pub samples: u32,
123 /// Whether that offscreen target carries a depth-stencil attachment.
124 pub depth: bool,
125 /// Whether the scene draws UI panels of its own.
126 ///
127 /// The snapshot tests mount a UI source to drive them; the windowed path
128 /// always mounts one, for the scene selector.
129 pub ui: bool,
130 /// Whether the scene's UI animates against egui's clock.
131 ///
132 /// A scene whose snapshots were captured with the clock suppressed — the
133 /// example's own, whose panels live in egui windows — declares `true`, so
134 /// the snapshot tests zero egui's animation time for it. The ported test
135 /// scenes were captured with the clock live, and declare `false`.
136 pub reproducible_ui: bool,
137 /// Build the scene's world content and return its per-frame behaviour.
138 ///
139 /// The world already holds the frame's context, the renderer resource
140 /// entity and the pass the scene draws through; the scene spawns its
141 /// sources, meshes, camera and entities, sources inside the pass.
142 ///
143 /// The entity is the UI source the shell mounted for the scene, when the
144 /// run has one: a panel the scene draws hangs in that source's scope, so
145 /// the scene spawns it there rather than at the root — a source draws the
146 /// subtree it owns and nothing beside it.
147 pub build: Build,
148}
149
150/// Every selectable scene, in the order the GUI lists and `--scene all` runs
151/// them.
152pub static SCENES: &[&SceneDef] = &[
153 &spin_cube::SCENE,
154 &ui_only::SCENE,
155 &mesh_and_ui::SCENE,
156 &animated::SCENE,
157 &skinned::SCENE,
158 &morphed::SCENE,
159 &instanced_skinned_morph::SCENE,
160 &lit::SCENE,
161 &lit_clusters::CLUSTER_SCENE,
162 &gltf::SCENE,
163 &mesh_topologies::SCENE,
164 &transparent::SCENE,
165 &custom_pipeline::SCENE,
166 &outline::SCENE,
167 &outline::MSAA_SCENE,
168];
169
170/// The scene whose id is `id`, if any.
171pub fn by_id(id: &str) -> Option<&'static SceneDef> {
172 SCENES.iter().copied().find(|scene| scene.id == id)
173}
174
175/// The scene the example runs when none is asked for.
176pub fn default() -> &'static SceneDef {
177 &spin_cube::SCENE
178}
179
180/// A table of every scene, for `--list-scenes` and `--help`.
181pub fn list_text() -> String {
182 let mut text = String::from("Scenes:\n");
183 for scene in SCENES {
184 text.push_str(&format!(" {:<22} {}\n", scene.id, scene.description));
185 }
186 text
187}
188
189/// What a scene is built with.
190#[derive(Clone, Copy)]
191pub struct SceneOptions {
192 /// Mount the scene's own UI panels.
193 pub ui: bool,
194 /// Mount the windowed shell's scene-selector panel.
195 pub selector: bool,
196 /// Suppress everything egui animates against the clock, so a captured
197 /// frame does not depend on when it was drawn.
198 pub reproducible: bool,
199 /// Keep the scene's declared [`SceneDef::baseline`] aspect by drawing its
200 /// content into the largest region of that aspect the target fits.
201 ///
202 /// The windowed path sets this so a window of any shape shows the same
203 /// picture, larger or smaller. The snapshot tests clear it: a capture draws
204 /// at the scene's own size, which its snapshots were taken at.
205 pub letterbox: bool,
206 /// Seconds each frame of a fixed-sequence scene stays on screen, or `None`
207 /// to advance the sequence once per drawn frame.
208 ///
209 /// A test passes `None` so it draws exactly the frames the snapshots
210 /// froze; a windowed run passes the scene's own [`SceneDef::step_seconds`]
211 /// so the sequence plays at a watchable pace.
212 pub sequence_step: Option<f32>,
213}
214
215// ---------------------------------------------------------------------------
216// Shared geometry and camera helpers
217//
218// These are the fixtures the ported tests shared, restated here because the
219// example cannot reach into a test crate.
220// ---------------------------------------------------------------------------
221
222/// The size the ported test scenes draw at, matching their snapshots.
223pub const TEST_SIZE: (u32, u32) = (256, 192);
224
225/// The aspect the windowed example keeps its 3D content to, as a reference
226/// resolution.
227///
228/// Whatever the window's shape, a scene that declares this baseline is drawn
229/// through the largest rectangle of this aspect that fits the target, so a wide
230/// screen and a narrow one show the same picture — the wide one larger — rather
231/// than the wide one showing more and the narrow one cropping it.
232///
233/// The resolution itself is only the reference the aspect comes from; the
234/// content is scaled to whatever the target allows. It matches the default
235/// window's size and the 4:3 ratio every scene's snapshots were captured at.
236pub const BASELINE_SIZE: (u32, u32) = (960, 720);
237
238/// The format the test scenes render into: sRGB, like the example's window.
239pub const TEST_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
240
241/// Seconds each frame of a fixed-sequence scene stays on screen in the
242/// windowed loop.
243///
244/// The ported test scenes froze an animation as a handful of frames, so
245/// playing them one per displayed frame would flash past in a fraction of a
246/// second. The snapshot tests ignore this and draw the frames back to back.
247pub const SEQUENCE_STEP: f32 = 0.5;
248
249/// The raw channels of one mesh: `(positions, uvs, colors, indices)`.
250pub type RawMesh = (Vec<[f32; 3]>, Vec<[f32; 2]>, Vec<[u8; 4]>, Vec<u32>);
251
252/// A unit cube centred at the origin, returned as
253/// `(positions, uvs, colors, indices)`.
254///
255/// The colors are quantized to the `Unorm8x4` width the vertex stream stores,
256/// exactly as the tests uploaded them.
257pub fn cube() -> RawMesh {
258 cube_with_normals().0
259}
260
261/// The same cube, beside one outward normal per vertex.
262///
263/// A lit pipeline shades with the normal, and the unlit one derives the
264/// normal it draws with from the fragment's derivatives, so only the scenes
265/// that shade need to carry one.
266pub fn cube_with_normals() -> (RawMesh, Vec<[f32; 3]>) {
267 let faces = [
268 ([-1.0f32, 0.0, 0.0], [0.0f32, 0.0, 1.0]),
269 ([1.0, 0.0, 0.0], [0.0, 0.0, -1.0]),
270 ([0.0, -1.0, 0.0], [1.0, 0.0, 0.0]),
271 ([0.0, 1.0, 0.0], [1.0, 0.0, 0.0]),
272 ([0.0, 0.0, -1.0], [-1.0, 0.0, 0.0]),
273 ([0.0, 0.0, 1.0], [1.0, 0.0, 0.0]),
274 ];
275
276 let mut positions = Vec::new();
277 let mut uvs = Vec::new();
278 let mut colors = Vec::new();
279 let mut normals = Vec::new();
280 let mut indices = Vec::new();
281
282 for (face_normal, tangent) in faces {
283 let base = positions.len() as u32;
284 let face_normal = glam::Vec3::from(face_normal);
285 let tangent = glam::Vec3::from(tangent);
286 let bitangent = face_normal.cross(tangent);
287 for (u, v) in [(-1.0f32, -1.0f32), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)] {
288 let position = face_normal + tangent * u + bitangent * v;
289 positions.push(position.to_array());
290 normals.push(face_normal.to_array());
291 uvs.push([(u + 1.0) * 0.5, (v + 1.0) * 0.5]);
292 let color = (position + 1.0) * 0.5;
293 colors.push([color.x, color.y, color.z, 1.0]);
294 }
295 indices.extend([base, base + 1, base + 2, base, base + 2, base + 3]);
296 }
297
298 // The stream stores colors as `Unorm8x4`, so quantize once here rather
299 // than carrying a float copy through the scene.
300 let colors = unlit_wgpu::mesh::quantize_colors(&colors).collect();
301 ((positions, uvs, colors, indices), normals)
302}
303
304/// A rectangle as the mesh allocator takes it.
305pub struct Grid {
306 /// One position per vertex.
307 pub positions: Vec<[f32; 3]>,
308 /// Per-vertex normals, or `None` for a surface the fragment shades from the
309 /// normals its own positions give it.
310 pub normals: Option<Vec<[f32; 3]>>,
311 /// One texture coordinate per vertex.
312 pub uvs: Vec<[f32; 2]>,
313 /// The one color every vertex of the surface carries.
314 pub albedo: unlit_wgpu::mesh::CompressedColor,
315 /// Triangles over the vertices, three indices each.
316 pub indices: Vec<u32>,
317}
318
319/// A rectangle centred on `origin`, `right` wide and `up` tall, split into
320/// `segments` quads along each of them, wound so its front face is the side
321/// the cross product of `right` and `up` points at.
322pub fn grid(
323 origin: glam::Vec3,
324 right: glam::Vec3,
325 up: glam::Vec3,
326 size: (f32, f32),
327 segments: (u32, u32),
328) -> Grid {
329 let (columns, rows) = (segments.0.max(1), segments.1.max(1));
330 let mut positions = Vec::with_capacity(((columns + 1) * (rows + 1)) as usize);
331 let mut uvs = Vec::with_capacity(positions.capacity());
332 for row in 0..=rows {
333 let v = row as f32 / rows as f32;
334 for column in 0..=columns {
335 let u = column as f32 / columns as f32;
336 positions.push(
337 (origin + right * (size.0 * (u - 0.5)) + up * (size.1 * (v - 0.5))).to_array(),
338 );
339 uvs.push([u, 1.0 - v]);
340 }
341 }
342 let mut indices = Vec::with_capacity((columns * rows * 6) as usize);
343 let stride = columns + 1;
344 for row in 0..rows {
345 for column in 0..columns {
346 let corner = row * stride + column;
347 // Wound right then up, which is the order the cross product of
348 // `right` and `up` puts on the front face.
349 indices.extend([
350 corner,
351 corner + 1,
352 corner + stride + 1,
353 corner,
354 corner + stride + 1,
355 corner + stride,
356 ]);
357 }
358 }
359 // White rather than a tint: these surfaces are coloured by the light that
360 // reaches them, and a tinted albedo would be read as the lighting itself.
361 Grid {
362 positions,
363 normals: None,
364 uvs,
365 albedo: unlit_wgpu::mesh::quantize_colors(&[[1.0f32, 1.0, 1.0, 1.0]])
366 .next()
367 .expect("one color is quantized"),
368 indices,
369 }
370}
371
372/// Upload a rectangle and return its mesh.
373pub fn allocate_surface(
374 world: &mut World,
375 source_entity: Entity,
376 key: &unlit3d::lit::LitPipelineKey,
377 grid: &Grid,
378) -> GpuMesh {
379 let colors = vec![grid.albedo; grid.positions.len()];
380 with_mesh_source(world, source_entity, |source, world| {
381 source.allocate_lit_mesh(
382 world,
383 &key.options,
384 LitMeshDesc {
385 positions: &grid.positions,
386 uvs: Some(&grid.uvs),
387 colors: Some(&colors),
388 normals: grid.normals.as_deref(),
389 indices: Some(&grid.indices),
390 ..Default::default()
391 },
392 )
393 })
394}
395
396/// One surface of the scene: the mesh and the pipeline it is drawn with.
397pub struct LitSurface {
398 /// The mesh this surface draws.
399 pub mesh: GpuMesh,
400 /// The pipeline its instances are drawn with.
401 pub pipeline: GpuRenderPipeline,
402}
403
404impl Bundle for LitSurface {
405 fn into_bundle(self) -> DynamicBundle {
406 bundle![Transform::default(), self.mesh, self.pipeline]
407 }
408}
409
410/// A light and the transform it shines from.
411///
412/// The light reads nothing of the transform but its own packing, so the two are
413/// spawned together: a light without one shines from the origin along the
414/// world's negative z.
415pub struct PlacedLight {
416 /// The light itself, of whichever kind it is.
417 pub light: LightSource,
418 /// Where the light sits, which a directional light ignores.
419 pub translation: glam::Vec3,
420 /// The orientation the light shines along.
421 pub rotation: glam::Quat,
422}
423
424impl PlacedLight {
425 /// A light at `translation` aimed at `target`.
426 ///
427 /// Every built-in light shines along its transform's negative z, so aiming
428 /// one is turning that axis onto the direction it has to shine in.
429 pub fn aiming(light: LightSource, translation: glam::Vec3, target: glam::Vec3) -> Self {
430 Self {
431 light,
432 translation,
433 rotation: glam::Quat::from_rotation_arc(
434 glam::Vec3::NEG_Z,
435 (target - translation).normalize(),
436 ),
437 }
438 }
439}
440
441impl Bundle for PlacedLight {
442 fn into_bundle(self) -> DynamicBundle {
443 bundle![
444 Transform {
445 translation: self.translation,
446 rotation: self.rotation,
447 scale: glam::Vec3::ONE,
448 },
449 self.light,
450 ]
451 }
452}
453/// The aspect ratio of a `size`-pixel render target.
454///
455/// Every camera in the example takes its aspect from here rather than from the
456/// size its scene was built at, so a target whose shape changes — a browser
457/// window being stretched — re-aims instead of stretching.
458pub fn aspect_of(size: (u32, u32)) -> f32 {
459 size.0 as f32 / size.1.max(1) as f32
460}
461
462/// The region of a `target`-pixel render target a `baseline`-aspect scene draws
463/// into: the largest rectangle of that aspect that fits, centred.
464///
465/// The content is scaled to this region instead of to the target, so the target
466/// only ever adds bars around the picture — a wide window shows the same view a
467/// narrow one does, larger, rather than more of it. A scene whose camera is
468/// built for the region's aspect draws undistorted inside it.
469pub fn baseline_viewport(baseline: (u32, u32), target: (u32, u32)) -> ViewportRect {
470 ViewportRect::fit_aspect(aspect_of(baseline), target.0.max(1), target.1.max(1))
471}
472
473/// The size a scene's camera should be built for when its content is drawn into
474/// the `baseline`-aspect part of a `target`-pixel render target.
475///
476/// This is the letterboxed region's own size, in whole pixels, so a camera
477/// taking its aspect from it matches what the viewport scales into place: the
478/// two agree exactly rather than approximately, which is what keeps a cube
479/// cubic. A camera built for the target's own size would shear the picture
480/// back out to the target's aspect.
481pub fn content_size(baseline: (u32, u32), target: (u32, u32)) -> (u32, u32) {
482 let viewport = baseline_viewport(baseline, target);
483 (viewport.width as u32, viewport.height as u32)
484}
485
486/// The eye [`camera_view`] looks from.
487pub const DEFAULT_EYE: glam::Vec3 = glam::Vec3::new(0.0, 1.2, 3.2);
488/// The point [`camera_view`] looks at.
489pub const DEFAULT_TARGET: glam::Vec3 = glam::Vec3::new(0.0, 0.2, 0.0);
490
491/// Camera looking at [`DEFAULT_TARGET`] from [`DEFAULT_EYE`].
492///
493/// Uses reverse-z infinite perspective matching the built-in pipeline's
494/// `CompareFunction::Greater` and `depth_clear = 0.0`.
495pub fn camera_view(size: (u32, u32)) -> Camera {
496 camera_looking_at(DEFAULT_EYE, DEFAULT_TARGET, size)
497}
498
499/// A camera at `eye` looking at `target`, with the same reverse-z infinite
500/// perspective as [`camera_view`].
501pub fn camera_looking_at(eye: glam::Vec3, target: glam::Vec3, size: (u32, u32)) -> Camera {
502 let projection = glam::camera::rh::proj::directx::perspective_infinite_reverse(
503 60f32.to_radians(),
504 aspect_of(size),
505 0.1,
506 );
507 let view = glam::camera::rh::view::look_at_mat4(eye, target, glam::Vec3::Y);
508 Camera {
509 view_from_world: view,
510 clip_from_view: projection,
511 active: true,
512 }
513}
514
515/// Re-aim `entity`'s camera at `target` from `eye` for a frame of `size`.
516///
517/// The camera hangs in `scope`, the source that draws it, so the view is its
518/// own. The scenes whose camera never moves still call this every frame,
519/// so their projection keeps the frame's own aspect. It is what a scene does in
520/// [`SceneControl::advance`](SceneControl) instead of capturing the size it was
521/// built at.
522pub fn aim_camera(
523 world: &mut World,
524 scope: Entity,
525 entity: Entity,
526 eye: glam::Vec3,
527 target: glam::Vec3,
528 size: (u32, u32),
529) {
530 let mut camera = camera_looking_at(eye, target, size);
531 world
532 .scope(scope)
533 .expect("the scope is alive")
534 .query_one::<&mut Camera>(entity)
535 .map(|mut current| {
536 camera.active = current.get().active;
537 *current.get() = camera;
538 })
539 .expect("the entity carries a camera");
540}
541
542/// Default options for the ported ECS scenes: per-instance transform and tint,
543/// no texture, no MSAA, with reverse-z depth.
544///
545/// The options are pure policy, so they say nothing about the geometry a draw
546/// carries: which vertex channels a mesh packs follows the slices its caller
547/// uploads, and which of them a draw reads follows the mesh it draws. The tint
548/// rides the same per-instance record as the transform, so a draw that reads
549/// instances reads the tint too.
550pub fn default_options() -> LitOptions {
551 LitOptions {
552 primitive: wgpu::PrimitiveState {
553 cull_mode: Some(wgpu::Face::Back),
554 ..Default::default()
555 },
556 depth_stencil: DepthStencilPolicy::default(),
557 blend: None,
558 write_mask: wgpu::ColorWrites::ALL,
559 // These scenes state no lights, so their variants draw their base
560 // colour unchanged.
561 unlit: true,
562 ..LitOptions::standard()
563 }
564}
565
566/// A rig of `joint_count` joints, all at rest, returned as joint matrices.
567///
568/// The identity pose deforms nothing: a vertex's weights sum to one, so the
569/// weighted sum of identity matrices is the vertex itself.
570pub fn rest_pose(joint_count: usize) -> Vec<JointMatrix> {
571 vec![glam::Mat4::IDENTITY; joint_count]
572}
573
574/// A two-joint skin that bends a mesh about its own base.
575///
576/// The lower joint is the base and stays at rest; the upper one rotates about
577/// the mesh's lowest point, and every vertex is weighted between them by how
578/// high it sits, so the mesh bends rather than shears. The weights exercise the
579/// four-lane weighted sum the shader computes.
580///
581/// The joint stream and weights are what a mesh is uploaded with; the matrices
582/// are the pose an entity carries, which [`Self::pose`] builds.
583pub struct BendSkin {
584 /// Four joint indices per vertex: the base and the bending joint.
585 pub joints: Vec<[u16; 4]>,
586 /// Four weights per vertex, summing to one.
587 pub weights: Vec<[f32; 4]>,
588 /// The angle the bending joint is rotated by.
589 angle: f32,
590 /// The height the bending joint pivots about, in the mesh's own space.
591 pivot: f32,
592}
593
594impl BendSkin {
595 /// Skin `positions` to two joints, splitting each vertex by its height.
596 pub fn new(positions: &[[f32; 3]], angle: f32) -> Self {
597 let (min, max) = positions.iter().fold(
598 (f32::INFINITY, f32::NEG_INFINITY),
599 |(min, max), position| (min.min(position[1]), max.max(position[1])),
600 );
601 let height = (max - min).max(f32::EPSILON);
602 let (joints, weights) = positions
603 .iter()
604 .map(|position| {
605 let t = ((position[1] - min) / height).clamp(0.0, 1.0);
606 ([0u16, 1, 0, 0], [1.0 - t, t, 0.0, 0.0])
607 })
608 .unzip();
609 Self {
610 joints,
611 weights,
612 angle,
613 pivot: min,
614 }
615 }
616
617 /// Replace the bending joint's rotation, which pivots about the mesh's
618 /// lowest point rather than its origin.
619 pub fn set_angle(&mut self, angle: f32) {
620 self.angle = angle;
621 }
622
623 /// The pose the two joints are at, ready to attach to an entity.
624 pub fn pose(&self) -> SkinPose {
625 let mut matrices = rest_pose(2);
626 matrices[1] = glam::Mat4::from_translation(glam::Vec3::Y * self.pivot)
627 * glam::Mat4::from_rotation_z(self.angle)
628 * glam::Mat4::from_translation(glam::Vec3::Y * -self.pivot);
629 SkinPose { matrices }
630 }
631}
632
633/// The displacement of the two morph targets the deformed scenes use, packed
634/// the way a mesh stores it: per vertex, one target after the other, three
635/// components each.
636///
637/// The first target tapers the cube's top to a point, the second widens its
638/// base.
639pub fn morph_deltas(positions: &[[f32; 3]]) -> MorphDeltas {
640 let mut deltas = Vec::with_capacity(positions.len() * 2 * 3);
641 for position in positions {
642 let height = (position[1] + 1.0) * 0.5;
643 deltas.extend_from_slice(&[
644 -position[0] * height * 0.7,
645 0.0,
646 -position[2] * height * 0.7,
647 ]);
648 let height = (1.0 - position[1]) * 0.5;
649 deltas.extend_from_slice(&[position[0] * height * 0.6, 0.0, position[2] * height * 0.6]);
650 }
651 MorphDeltas {
652 deltas,
653 target_count: 2,
654 }
655}
656
657/// Spawn `bundle` into `scope`'s subtree.
658///
659/// A scene's content hangs under the source that draws it: a source renders
660/// the subtree it owns, so an entity spawned beside the source is one no frame
661/// of it shows.
662pub fn spawn_in<B: unlit_ecs::Bundle>(world: &mut World, scope: Entity, bundle: B) -> Entity {
663 world
664 .scope_mut(scope)
665 .expect("the scope is alive")
666 .spawn(bundle)
667}
668
669/// Spawn `bundle` into the world's shared subtree.
670///
671/// State every source of a scene reads — the input batches — belongs here: a
672/// source's own scope holds what it draws, and one beside it would hold what
673/// the other cannot see.
674pub fn spawn_shared<B: unlit_ecs::Bundle>(world: &mut World, bundle: B) -> Entity {
675 let shared = world.shared();
676 world
677 .scope_mut(shared)
678 .expect("the shared scope exists")
679 .spawn(bundle)
680}
681
682/// Despawn `entity` and its subtree, through the scope that owns it.
683pub fn despawn_in(world: &mut World, scope: Entity, entity: Entity) {
684 world
685 .scope_mut(scope)
686 .expect("the scope named in the scene")
687 .despawn(entity)
688 .expect("the scene owns the entity it despawns");
689}
690
691/// Run `f` on the world's `MeshSource`, which lives on the source entity.
692///
693/// A shared-world entry point, so the scene's own `&mut World` can
694/// reborrow it next to the mutable borrow of the source.
695pub fn with_mesh_source<R>(
696 world: &World,
697 source_entity: Entity,
698 f: impl FnOnce(&mut MeshSource, &Scope<'_>) -> R,
699) -> R {
700 // The scope is the source's own, so what its closure reads is the subtree
701 // the source draws.
702 let scope = world
703 .scope(source_entity)
704 .expect("the source entity is alive");
705 let mut source = scope
706 .query_one::<&mut Source>(source_entity)
707 .expect("the source entity exists");
708 let mesh = source
709 .get()
710 .as_mut::<MeshSource>()
711 .expect("the source is a MeshSource");
712 f(mesh, &scope)
713}
714
715/// Allocate the unit cube through the source, its vertices offset by `offset`
716/// in mesh space.
717///
718/// The offset is baked into the vertices, so two cubes allocated from
719/// different offsets draw differently even at the same transform — which is
720/// what tells a mesh apart from the one whose pool range it sits next to.
721pub fn allocate_offset_cube_mesh(
722 world: &World,
723 source_entity: Entity,
724 key: &LitPipelineKey,
725 offset: glam::Vec3,
726) -> GpuMesh {
727 let (positions, uvs, colors, indices) = cube();
728 let positions = positions
729 .into_iter()
730 .map(|position| {
731 [
732 position[0] + offset.x,
733 position[1] + offset.y,
734 position[2] + offset.z,
735 ]
736 })
737 .collect::<Vec<_>>();
738 with_mesh_source(world, source_entity, |source, world| {
739 source.allocate_lit_mesh(
740 world,
741 &key.options,
742 LitMeshDesc {
743 positions: &positions,
744 uvs: Some(&uvs),
745 colors: Some(&colors),
746 indices: Some(&indices),
747 ..Default::default()
748 },
749 )
750 })
751}
752
753/// Free `mesh` through the source, releasing its pool ranges.
754pub fn remove_mesh(world: &World, source_entity: Entity, mesh: GpuMesh) {
755 with_mesh_source(world, source_entity, |source, _| source.remove_mesh(mesh));
756}