Skip to main content

unlit3d_examples/scenes/
instanced_skinned_morph.rs

1//! The instanced skin-and-morph scene: three entities sharing one deformed
2//! mesh.
3//!
4//! Ported from the `ecs_instanced_skinned_morphed_cubes_match_snapshots`
5//! test. Three entities share one skinned-and-morphed mesh. They draw the
6//! same pipeline, bind the same mesh and material bind groups and read the
7//! same vertex and index buffers, so the source folds them into a single
8//! instanced draw; what keeps each instance distinct is the pose state the
9//! instance stream carries — every instance names its own [`SkinPose`] and
10//! [`MorphWeights`] entity. The frames animate those per instance, so the
11//! sequence is what proves a merged draw still reads each instance's own pose.
12
13use super::{
14    BendSkin, SEQUENCE_STEP, SceneControl, SceneDef, SceneOptions, TEST_SIZE, default_options,
15    morph_deltas, rest_pose,
16};
17use unlit_ecs::{Bundle, DynamicBundle, bundle};
18use unlit3d::prelude::*;
19
20/// The frames rendered, one snapshot each.
21const FRAMES: usize = 6;
22
23/// How many entities share the mesh.
24const INSTANCES: usize = 3;
25
26/// One instance of the crowd: the shared mesh drawn at its own place, with
27/// its own pose and its own morph weights.
28///
29/// The pose and the weights name entities of their own, so they are fields:
30/// an instance without them would draw the rest pose of the whole crowd.
31struct Instance {
32    transform: Transform,
33    tint: glam::Vec4,
34    mesh: GpuMesh,
35    pipeline: GpuRenderPipeline,
36    pose: Entity,
37    weights: Entity,
38}
39
40impl Bundle for Instance {
41    fn into_bundle(self) -> DynamicBundle {
42        bundle![
43            self.transform,
44            InstanceColor::new(self.tint),
45            self.mesh,
46            self.pipeline,
47            SkinBinding { pose: self.pose },
48            MorphBinding {
49                weights: self.weights
50            },
51        ]
52    }
53}
54
55/// The scene: a crowd of three instanced cubes, each skinned and morphed on
56/// its own, stored as one snapshot per frame.
57pub static SCENE: SceneDef = SceneDef {
58    id: "instanced_skinned_morph",
59    title: "Instanced skin + morph",
60    description: "three cubes sharing one mesh, deformed per instance",
61    size: TEST_SIZE,
62    baseline: Some(TEST_SIZE),
63    clear: None,
64    frames: FRAMES as u32,
65    step_seconds: Some(SEQUENCE_STEP),
66    samples: 1,
67    depth: true,
68    ui: false,
69    reproducible_ui: false,
70    build,
71};
72
73/// Build the instanced scene's world content.
74fn build(
75    world: &mut World,
76    context: RenderContext,
77    pass: Entity,
78    _ui: Option<Entity>,
79    size: (u32, u32),
80    _options: SceneOptions,
81) -> SceneControl {
82    let mut source = MeshSource::new(&world.root_scope(), context);
83    source.register_lit_family(&world.root_scope());
84    // The variant that reads both the joint stream and the morph bindings.
85    let key = LitPipelineKey::new(default_options());
86    let source_entity = world
87        .scope_mut(pass)
88        .expect("the pass is alive")
89        .spawn(bundle![Source::new(source)]);
90
91    let (positions, uvs, colors, indices) = super::cube();
92    let skin = BendSkin::new(&positions, 0.0);
93    let deltas = morph_deltas(&positions);
94    let mesh = super::with_mesh_source(world, source_entity, |source, world| {
95        source.allocate_lit_mesh(
96            world,
97            &key.options,
98            LitMeshDesc {
99                positions: &positions,
100                uvs: Some(&uvs),
101                colors: Some(&colors),
102                normals: None,
103                indices: Some(&indices),
104                joints: Some(&skin.joints),
105                weights: Some(&skin.weights),
106                morph_deltas: Some(deltas),
107            },
108        )
109    });
110
111    let camera_entity = super::spawn_in(world, source_entity, bundle![camera(size)]);
112
113    // One pose entity and one weights entity per instance, all naming the
114    // same shared mesh: the instances merge into a single draw whose instance
115    // records carry each entity's pose.
116    let mut pose_entities = Vec::new();
117    let mut weight_entities = Vec::new();
118    for instance in 0..INSTANCES {
119        let pose_entity = super::spawn_in(
120            world,
121            source_entity,
122            bundle![SkinPose {
123                matrices: rest_pose(2)
124            }],
125        );
126        let weight_entity = super::spawn_in(
127            world,
128            source_entity,
129            bundle![MorphWeights {
130                weights: instance_weights(0, instance).to_vec()
131            }],
132        );
133        let color = match instance {
134            0 => glam::Vec4::new(1.0, 0.75, 0.55, 1.0),
135            1 => glam::Vec4::new(0.55, 0.85, 1.0, 1.0),
136            _ => glam::Vec4::new(0.7, 1.0, 0.6, 1.0),
137        };
138        super::spawn_in(
139            world,
140            source_entity,
141            Instance {
142                transform: Transform {
143                    translation: glam::Vec3::new((instance as f32 - 1.0) * 1.3, 0.0, 0.0),
144                    scale: glam::Vec3::splat(0.8),
145                    ..Default::default()
146                },
147                tint: color,
148                mesh: mesh.clone(),
149                pipeline: GpuRenderPipeline::new(key.clone()),
150                pose: pose_entity,
151                weights: weight_entity,
152            },
153        );
154        pose_entities.push(pose_entity);
155        weight_entities.push(weight_entity);
156    }
157
158    // One skin per instance, reused every frame: the pose is a component
159    // write, and only the bend angle changes.
160    let mut skins: Vec<BendSkin> = (0..INSTANCES)
161        .map(|_| BendSkin::new(&positions, 0.0))
162        .collect();
163
164    SceneControl {
165        advance: Box::new(move |world, frame, _delta, frame_size| {
166            let frame = frame as usize % FRAMES;
167            // Update every instance's own pose: one component write per
168            // instance, and the source packs and uploads them once per frame.
169            {
170                let scope = world.scope(source_entity).expect("the source is alive");
171                for instance in 0..INSTANCES {
172                    skins[instance].set_angle(instance_angle(frame, instance));
173                    *scope
174                        .query_one::<&mut SkinPose>(pose_entities[instance])
175                        .expect("the pose entity carries a `SkinPose`")
176                        .get() = skins[instance].pose();
177                    scope
178                        .query_one::<&mut MorphWeights>(weight_entities[instance])
179                        .expect("the weight entity carries a `MorphWeights`")
180                        .get()
181                        .weights = instance_weights(frame, instance).to_vec();
182                }
183            }
184            // The camera the whole sequence shares, re-aimed at the frame's
185            // aspect so a resized window does not stretch the instances.
186            super::aim_camera(
187                world,
188                source_entity,
189                camera_entity,
190                CAMERA_EYE,
191                CAMERA_TARGET,
192                frame_size,
193            );
194        }),
195        snapshot: Box::new(|frame| {
196            (frame < FRAMES as u32)
197                .then(|| format!("instanced_skinned_morph/frame_{frame:02}.webp"))
198        }),
199    }
200}
201
202/// Where the camera sits, far enough back to see all instances, and what it
203/// looks at.
204const CAMERA_EYE: glam::Vec3 = glam::Vec3::new(0.0, 1.3, 3.6);
205/// The point the camera looks at.
206const CAMERA_TARGET: glam::Vec3 = glam::Vec3::new(0.0, 0.2, 0.0);
207
208/// The camera the whole sequence shares, wide enough to see all instances.
209fn camera(size: (u32, u32)) -> Camera {
210    super::camera_looking_at(CAMERA_EYE, CAMERA_TARGET, size)
211}
212
213/// The bending angle `instance` holds in `frame`.
214///
215/// Every instance bends on its own schedule, so the frames cannot be told
216/// apart unless each instance's pose record reaches the shader.
217fn instance_angle(frame: usize, instance: usize) -> f32 {
218    let phase = frame as f32 * 0.9 + instance as f32 * 0.7;
219    phase.sin() * 0.55
220}
221
222/// The morph weights `instance` holds in `frame`, in target order.
223fn instance_weights(frame: usize, instance: usize) -> [f32; 2] {
224    let phase = frame as f32 * 0.5 + instance as f32 * 1.1;
225    [phase.cos().abs(), phase.sin().abs()]
226}