1use super::{
9 BendSkin, SEQUENCE_STEP, SceneControl, SceneDef, SceneOptions, TEST_SIZE, default_options,
10};
11use unlit_ecs::{Bundle, DynamicBundle, bundle};
12use unlit3d::prelude::*;
13
14const FRAMES: usize = 6;
16
17const CAMERA_EYE: glam::Vec3 = glam::Vec3::new(0.9, 1.1, 2.6);
19
20pub static SCENE: SceneDef = SceneDef {
22 id: "skinned",
23 title: "Skinned cube",
24 description: "a cube bent by a two-joint skin over six frames",
25 size: TEST_SIZE,
26 baseline: Some(TEST_SIZE),
27 clear: None,
28 frames: FRAMES as u32,
29 step_seconds: Some(SEQUENCE_STEP),
30 samples: 1,
31 depth: true,
32 ui: false,
33 reproducible_ui: false,
34 build,
35};
36
37struct SkinnedCube {
43 mesh: GpuMesh,
44 pipeline: GpuRenderPipeline,
45 pose: Entity,
46}
47
48impl Bundle for SkinnedCube {
49 fn into_bundle(self) -> DynamicBundle {
50 bundle![
51 Transform {
52 scale: glam::Vec3::splat(0.9),
53 ..Default::default()
54 },
55 InstanceColor::new(glam::Vec4::new(1.0, 0.85, 0.6, 1.0)),
56 self.mesh,
57 self.pipeline,
58 SkinBinding { pose: self.pose },
59 ]
60 }
61}
62
63fn build(
65 world: &mut World,
66 context: RenderContext,
67 pass: Entity,
68 _ui: Option<Entity>,
69 size: (u32, u32),
70 _options: SceneOptions,
71) -> SceneControl {
72 let mut source = MeshSource::new(&world.root_scope(), context);
73 source.register_lit_family(&world.root_scope());
74 let key = LitPipelineKey::new(default_options());
77 let source_entity = world
78 .scope_mut(pass)
79 .expect("the pass is alive")
80 .spawn(bundle![Source::new(source)]);
81
82 let (positions, uvs, colors, indices) = super::cube();
83 let skin = BendSkin::new(&positions, bend_angle(0));
84 let mesh = super::with_mesh_source(world, source_entity, |source, world| {
85 source.allocate_lit_mesh(
86 world,
87 &key.options,
88 LitMeshDesc {
89 positions: &positions,
90 uvs: Some(&uvs),
91 colors: Some(&colors),
92 normals: None,
93 indices: Some(&indices),
94 joints: Some(&skin.joints),
95 weights: Some(&skin.weights),
96 morph_deltas: None,
97 },
98 )
99 });
100
101 let camera_entity = super::spawn_in(world, source_entity, bundle![camera(size)]);
102 let pose_entity = super::spawn_in(world, source_entity, bundle![skin.pose()]);
106 super::spawn_in(
107 world,
108 source_entity,
109 SkinnedCube {
110 mesh,
111 pipeline: GpuRenderPipeline::new(key),
112 pose: pose_entity,
113 },
114 );
115
116 let mut skin = skin;
117 SceneControl {
118 advance: Box::new(move |world, frame, _delta, frame_size| {
119 let frame = frame as usize % FRAMES;
120 skin.set_angle(bend_angle(frame));
123 *world
124 .scope(source_entity)
125 .expect("the source is alive")
126 .query_one::<&mut SkinPose>(pose_entity)
127 .expect("the pose entity carries a `SkinPose`")
128 .get() = skin.pose();
129 super::aim_camera(
132 world,
133 source_entity,
134 camera_entity,
135 CAMERA_EYE,
136 CAMERA_TARGET,
137 frame_size,
138 );
139 }),
140 snapshot: Box::new(|frame| {
141 (frame < FRAMES as u32).then(|| format!("skinned/frame_{frame:02}.webp"))
142 }),
143 }
144}
145
146const CAMERA_TARGET: glam::Vec3 = glam::Vec3::new(0.0, 0.2, 0.0);
148
149fn camera(size: (u32, u32)) -> Camera {
151 super::camera_looking_at(CAMERA_EYE, CAMERA_TARGET, size)
152}
153
154fn bend_angle(frame: usize) -> f32 {
160 let half = FRAMES / 2;
161 let step = if frame < half {
162 frame as isize
163 } else {
164 half as isize - (frame - half) as isize
165 };
166 step as f32 * 0.42
167}