Skip to main content

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}