1use super::{SEQUENCE_STEP, SceneControl, SceneDef, SceneOptions, TEST_SIZE};
12use unlit_ecs::{Bundle, DynamicBundle, bundle};
13use unlit3d::prelude::*;
14
15const COLUMNS: usize = 3;
17const ROWS: usize = 3;
18const CELLS: usize = COLUMNS * ROWS;
19const CELL_SPACING: f32 = 1.5;
20
21const ORBIT_RADIUS: f32 = 4.3;
23const ORBIT_HEIGHT: f32 = 2.9;
24const ORBIT_STEP: f32 = 0.55;
26
27const TINTS: [glam::Vec4; 6] = [
29 glam::Vec4::new(1.0, 0.25, 0.2, 1.0),
30 glam::Vec4::new(1.0, 0.75, 0.2, 1.0),
31 glam::Vec4::new(0.35, 0.9, 0.3, 1.0),
32 glam::Vec4::new(0.25, 0.75, 1.0, 1.0),
33 glam::Vec4::new(0.55, 0.35, 1.0, 1.0),
34 glam::Vec4::new(1.0, 0.4, 0.8, 1.0),
35];
36
37struct Step {
39 allocate: &'static [usize],
41 free: &'static [usize],
43}
44
45const STEPS: [Step; 8] = [
47 Step {
48 allocate: &[4, 1, 6],
49 free: &[],
50 },
51 Step {
52 allocate: &[0, 7],
53 free: &[],
54 },
55 Step {
56 allocate: &[],
57 free: &[],
58 },
59 Step {
60 allocate: &[2, 5, 8],
61 free: &[],
62 },
63 Step {
64 allocate: &[],
65 free: &[1],
66 },
67 Step {
68 allocate: &[3],
69 free: &[],
70 },
71 Step {
72 allocate: &[],
73 free: &[4, 0],
74 },
75 Step {
76 allocate: &[1, 4],
77 free: &[],
78 },
79];
80
81pub static SCENE: SceneDef = SceneDef {
84 id: "animated",
85 title: "Animated grid",
86 description: "a grid of cubes filling, moving and recycling over eight frames",
87 size: TEST_SIZE,
88 baseline: Some(TEST_SIZE),
89 clear: None,
90 frames: STEPS.len() as u32,
91 step_seconds: Some(SEQUENCE_STEP),
92 samples: 1,
93 depth: true,
94 ui: false,
95 reproducible_ui: false,
96 build,
97};
98
99struct CellCube {
104 transform: Transform,
105 tint: glam::Vec4,
106 mesh: GpuMesh,
107 pipeline: GpuRenderPipeline,
108}
109
110impl Bundle for CellCube {
111 fn into_bundle(self) -> DynamicBundle {
112 bundle![
113 self.transform,
114 InstanceColor::new(self.tint),
115 self.mesh,
116 self.pipeline,
117 ]
118 }
119}
120
121fn build(
123 world: &mut World,
124 context: RenderContext,
125 pass: Entity,
126 _ui: Option<Entity>,
127 size: (u32, u32),
128 _options: SceneOptions,
129) -> SceneControl {
130 let mut source = MeshSource::new(&world.root_scope(), context);
131 source.register_lit_family(&world.root_scope());
132 let key = LitPipelineKey::new(super::default_options());
134 let source_entity = world
135 .scope_mut(pass)
136 .expect("the pass is alive")
137 .spawn(bundle![Source::new(source)]);
138
139 let camera_entity = super::spawn_in(world, source_entity, bundle![orbit_camera(0, size)]);
140
141 let mut cells: Vec<Option<(Entity, GpuMesh)>> = (0..CELLS).map(|_| None).collect();
145 let mut allocated = 0usize;
146
147 SceneControl {
148 advance: Box::new(move |world, frame, _delta, frame_size| {
149 let step_index = frame as usize % STEPS.len();
157 if step_index == 0 {
158 for entry in cells.iter_mut() {
159 if let Some((entity, mesh)) = entry.take() {
160 super::despawn_in(world, source_entity, entity);
161 super::remove_mesh(world, source_entity, mesh);
162 }
163 }
164 allocated = 0;
165 }
166 let step = &STEPS[step_index];
167
168 for &cell in step.free {
171 let (entity, mesh) = cells[cell].take().expect("a freed cell holds a mesh");
172 super::despawn_in(world, source_entity, entity);
173 super::remove_mesh(world, source_entity, mesh);
174 }
175
176 for &cell in step.allocate {
177 if let Some((entity, mesh)) = cells[cell].take() {
181 super::despawn_in(world, source_entity, entity);
182 super::remove_mesh(world, source_entity, mesh);
183 }
184 let offset = glam::Vec3::splat(0.06 * allocated as f32);
187 let mesh = super::allocate_offset_cube_mesh(world, source_entity, &key, offset);
188 let entity = super::spawn_in(
189 world,
190 source_entity,
191 CellCube {
192 transform: cell_transform(cell, step_index),
193 tint: TINTS[allocated % TINTS.len()],
194 mesh: mesh.clone(),
195 pipeline: GpuRenderPipeline::new(key.clone()),
196 },
197 );
198 cells[cell] = Some((entity, mesh));
199 allocated += 1;
200 }
201
202 for (cell, entry) in cells.iter().enumerate() {
205 if let Some((entity, _)) = entry {
206 let transform = cell_transform(cell, step_index);
207 world
208 .scope(source_entity)
209 .expect("the source is alive")
210 .query_one::<&mut Transform>(*entity)
211 .map(|mut current| *current.get() = transform)
212 .expect("a live mesh carries a transform");
213 }
214 }
215 super::aim_camera(
218 world,
219 source_entity,
220 camera_entity,
221 orbit_eye(step_index),
222 GRID_TARGET,
223 frame_size,
224 );
225 }),
226 snapshot: Box::new(|frame| {
227 let live = STEPS[..=frame as usize].iter().fold(0usize, |live, step| {
231 live - step.free.len() + step.allocate.len()
232 });
233 let step = &STEPS[frame as usize];
234 Some(format!(
235 "animated/frame_{frame:02}_alloc{}_free{}_live{live}.webp",
236 step.allocate.len(),
237 step.free.len(),
238 ))
239 }),
240 }
241}
242
243fn cell_center(cell: usize) -> glam::Vec3 {
245 let column = (cell % COLUMNS) as f32 - (COLUMNS as f32 - 1.0) * 0.5;
246 let row = (cell / COLUMNS) as f32 - (ROWS as f32 - 1.0) * 0.5;
247 glam::Vec3::new(column * CELL_SPACING, 0.2, row * CELL_SPACING)
248}
249
250fn cell_transform(cell: usize, frame: usize) -> Transform {
255 let moving = cell.is_multiple_of(2);
256 let phase = frame as f32 * 0.9 + cell as f32;
257 Transform {
258 translation: cell_center(cell)
259 + if moving {
260 glam::Vec3::Y * 0.35 * phase.sin()
261 } else {
262 glam::Vec3::ZERO
263 },
264 rotation: if moving {
265 glam::Quat::from_rotation_y(frame as f32 * 0.4 + cell as f32)
266 } else {
267 glam::Quat::from_rotation_y(cell as f32 * 0.7)
268 },
269 scale: glam::Vec3::splat(0.34 + 0.03 * (cell % 3) as f32),
270 }
271}
272
273const GRID_TARGET: glam::Vec3 = glam::Vec3::new(0.0, 0.2, 0.0);
275
276fn orbit_camera(frame: usize, size: (u32, u32)) -> Camera {
278 super::camera_looking_at(orbit_eye(frame), GRID_TARGET, size)
279}
280
281fn orbit_eye(frame: usize) -> glam::Vec3 {
283 let angle = frame as f32 * ORBIT_STEP;
284 glam::Vec3::new(
285 ORBIT_RADIUS * angle.sin(),
286 ORBIT_HEIGHT,
287 ORBIT_RADIUS * angle.cos(),
288 )
289}