1use core::marker::PhantomData;
2use core::time::Duration;
3
4use crate::animation::{AnimationStates, Animator, Running};
5use crate::math::{Mat3, Mat4, Vec3, Vec4};
6use crate::mesh::{Animation, Clip, Frame, Mesh, Part, Posing};
7use crate::surface_style::{Styled, SurfaceStyle, SurfaceStyleId, SurfaceStyles};
8use crate::{Holds, Material, Transform, View};
9
10const OPAQUE: f32 = 1.0;
13
14#[derive(Clone, Debug)]
19pub(crate) struct Draw<M> {
20 mesh: M,
21 transform: Transform,
22 facing: Facing,
23 roll: f32,
24 frame: Frame,
25 style: Option<Styled>,
26 fade: f32,
27 posed: Option<Running>,
28 paints: Paints,
29}
30
31impl<M> Draw<M> {
32 pub(crate) fn into_set<T: From<M>>(self) -> Draw<T> {
34 let Self {
35 mesh,
36 transform,
37 facing,
38 roll,
39 frame,
40 style,
41 fade,
42 posed,
43 paints,
44 } = self;
45 Draw {
46 mesh: mesh.into(),
47 transform,
48 facing,
49 roll,
50 frame,
51 style,
52 fade,
53 posed,
54 paints,
55 }
56 }
57
58 pub(crate) fn mesh(&self) -> &M {
59 &self.mesh
60 }
61
62 pub(crate) fn placement(&self, view: View) -> Placement {
64 Placement {
65 transform: self.facing.applied(self.transform, view, self.roll),
66 faced: self.faced(),
67 }
68 }
69
70 pub(crate) fn anchor(&self) -> Vec3 {
72 self.transform.matrix().w_axis.truncate()
73 }
74
75 pub(crate) fn window(&self) -> Frame {
77 self.frame
78 }
79
80 pub(crate) fn faced(&self) -> bool {
83 self.facing != Facing::AsPlaced
84 }
85
86 pub(crate) fn styled(&self) -> Option<Styled> {
88 self.style
89 }
90
91 pub(crate) fn posing(&self, now: Duration, clips: &[Animation]) -> Option<Posing> {
94 Some(self.posed?.posing(now, clips))
95 }
96
97 pub(crate) fn resolved(&self, part: Option<u32>, default: Material) -> Material {
101 part.and_then(|part| self.paints.of(part))
102 .or(self.paints.every)
103 .unwrap_or(default)
104 .faded(self.fade)
105 }
106}
107
108#[must_use = "an instance is only drawn once FrameContext::draw takes it"]
118#[derive(Debug)]
119pub struct Instance<M, S: SurfaceStyles = ()> {
120 draw: Draw<M>,
121 styles: PhantomData<S>,
122}
123
124impl<M, S: SurfaceStyles> Instance<M, S> {
125 pub(crate) fn new(mesh: M, transform: Transform) -> Self {
127 Self {
128 draw: Draw {
129 mesh,
130 transform,
131 facing: Facing::AsPlaced,
132 roll: 0.0,
133 frame: Frame::default(),
134 style: None,
135 fade: OPAQUE,
136 posed: None,
137 paints: Paints::default(),
138 },
139 styles: PhantomData,
140 }
141 }
142
143 pub fn at(mut self, transform: impl Into<Transform>) -> Self {
145 self.draw.transform = transform.into();
146 self
147 }
148
149 pub fn billboard(mut self) -> Self {
155 self.draw.facing = Facing::Billboard;
156 self
157 }
158
159 pub fn upright(mut self) -> Self {
166 self.draw.facing = Facing::Upright;
167 self
168 }
169
170 pub fn roll(mut self, radians: f32) -> Self {
177 self.draw.roll = radians;
178 self
179 }
180
181 pub fn surface_style<T: SurfaceStyle>(mut self) -> Self
190 where
191 S: Holds<T> + From<T>,
192 {
193 let seat = SurfaceStyleId(S::from(T::default()).seat());
194 self.draw.style = Some(Styled::at::<T>(seat));
195 self
196 }
197
198 pub fn posed<P: Part, A: AnimationStates>(mut self, animator: &Animator<M, A>) -> Self
205 where
206 M: Mesh<P, A::Clip>,
207 {
208 self.draw.posed = Some(animator.running());
209 self
210 }
211
212 #[cfg(all(test, feature = "offscreen"))]
219 pub(crate) fn posed_by(mut self, posing: Posing) -> Self {
220 self.draw.posed = Some(Running::stopped(posing));
221 self
222 }
223
224 pub fn frame(mut self, frame: Frame) -> Self {
227 self.draw.frame = frame;
228 self
229 }
230
231 pub fn material(mut self, material: Material) -> Self {
240 self.draw.paints.every(material);
241 self
242 }
243
244 pub fn material_of<P: Part, C: Clip>(mut self, part: P, material: Material) -> Self
250 where
251 M: Mesh<P, C>,
252 {
253 self.draw.paints.one(part.index(), material);
254 self
255 }
256
257 pub fn faded(mut self, alpha: f32) -> Self {
272 self.draw.fade = alpha.clamp(0.0, OPAQUE);
273 self
274 }
275
276 pub fn into_set<T: From<M>>(self) -> Instance<T, S> {
283 Instance {
284 draw: self.draw.into_set(),
285 styles: PhantomData,
286 }
287 }
288
289 pub(crate) fn record(self) -> Draw<M> {
293 self.draw
294 }
295}
296
297impl<M: Clone, S: SurfaceStyles> Clone for Instance<M, S> {
298 fn clone(&self) -> Self {
299 Self {
300 draw: self.draw.clone(),
301 styles: PhantomData,
302 }
303 }
304}
305
306#[derive(Clone, Copy, Debug, Eq, PartialEq)]
308enum Facing {
309 AsPlaced,
310 Billboard,
311 Upright,
312}
313
314impl Facing {
315 fn applied(self, transform: Transform, view: View, roll: f32) -> Transform {
319 let Some(turn) = self.turn(view, roll) else {
320 return transform;
321 };
322
323 let model = transform.matrix();
324 let sized = |axis: Vec3, column: Vec4| (axis * column.truncate().length()).extend(0.0);
325 Transform::from(Mat4::from_cols(
326 sized(turn.x_axis, model.x_axis),
327 sized(turn.y_axis, model.y_axis),
328 sized(turn.z_axis, model.z_axis),
329 model.w_axis,
330 ))
331 }
332
333 fn turn(self, view: View, roll: f32) -> Option<Mat3> {
336 match self {
337 Self::AsPlaced => None,
338 Self::Billboard => {
341 Some(view_plane(looking(view)?, view.up()) * Mat3::from_rotation_z(roll))
342 }
343 Self::Upright => Some(standing(looking(view)?)),
344 }
345 }
346}
347
348#[derive(Clone, Copy, Debug, PartialEq)]
354pub(crate) struct Placement {
355 transform: Transform,
356 faced: bool,
357}
358
359impl Placement {
360 pub(crate) fn transform(self) -> Transform {
362 self.transform
363 }
364
365 pub(crate) fn faced(self) -> bool {
367 self.faced
368 }
369}
370
371fn looking(view: View) -> Option<Vec3> {
374 (view.target() - view.eye()).try_normalize()
375}
376
377fn view_plane(looking: Vec3, up: Vec3) -> Mat3 {
379 let across = looking
380 .cross(up)
381 .try_normalize()
382 .unwrap_or_else(|| looking.cross(aside(looking)).normalize());
383
384 Mat3::from_cols(across, across.cross(looking), -looking)
385}
386
387fn standing(looking: Vec3) -> Mat3 {
389 let back = Vec3::new(-looking.x, 0.0, -looking.z)
390 .try_normalize()
391 .unwrap_or(Vec3::Z);
392
393 Mat3::from_cols(Vec3::Y.cross(back), Vec3::Y, back)
394}
395
396fn aside(looking: Vec3) -> Vec3 {
398 if looking.y.abs() > 0.99 {
399 Vec3::Z
400 } else {
401 Vec3::Y
402 }
403}
404
405#[derive(Clone, Debug, Default)]
408struct Paints {
409 every: Option<Material>,
410 parts: Vec<Option<Material>>,
411}
412
413impl Paints {
414 fn every(&mut self, material: Material) {
416 self.every = Some(material);
417 self.parts.clear();
418 }
419
420 fn one(&mut self, part: u32, material: Material) {
421 let at = part as usize;
422 if at >= self.parts.len() {
423 self.parts.resize(at + 1, None);
424 }
425 self.parts[at] = Some(material);
426 }
427
428 fn of(&self, part: u32) -> Option<Material> {
431 self.parts.get(part as usize).copied().flatten()
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use crate::math::Quat;
439 use crate::mesh::{Cube, MeshData, Slot};
440 use crate::{Assets, Catalog, Color};
441
442 const DIVING: View = View::look_at(Vec3::new(0.0, 5.0, 5.0), Vec3::ZERO);
445
446 const STILL: f32 = 0.0;
448
449 const QUARTER: f32 = core::f32::consts::FRAC_PI_2;
452
453 const GOLD: Material = Material::lit(Color::rgb(1.0, 0.8, 0.2));
454 const RED: Material = Material::lit(Color::rgb(1.0, 0.0, 0.0));
455
456 #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
458 struct Lantern;
459
460 impl Catalog for Lantern {
461 fn catalog() -> Vec<Self> {
462 vec![Self]
463 }
464 }
465
466 impl Mesh<LanternPart> for Lantern {
467 fn build(&self, assets: &Assets) -> MeshData<LanternPart> {
468 let cube = Cube.build(assets);
469 let half = cube.indices().len() as u32 / 2;
470 MeshData::in_parts(cube.vertices().to_vec(), cube.indices().to_vec(), |_| {
471 Slot::new(half, Material::default())
472 })
473 }
474 }
475
476 #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
477 enum LanternPart {
478 Frame,
479 Glass,
480 }
481
482 impl Part for LanternPart {
483 fn from_name(_name: &str) -> Option<Self> {
484 None
485 }
486
487 fn all() -> Vec<Self> {
488 vec![Self::Frame, Self::Glass]
489 }
490
491 fn index(&self) -> u32 {
492 *self as u32
493 }
494 }
495
496 #[test]
497 fn the_last_write_to_a_part_is_the_one_a_slot_resolves_to() {
498 let refined = Lantern
499 .at::<()>(Vec3::ZERO)
500 .material(GOLD)
501 .material_of(LanternPart::Glass, RED)
502 .record();
503 let replaced = Lantern
504 .at::<()>(Vec3::ZERO)
505 .material_of(LanternPart::Glass, RED)
506 .material(GOLD)
507 .record();
508 let glass = Some(LanternPart::Glass.index());
509 let frame = Some(LanternPart::Frame.index());
510
511 assert_eq!(refined.resolved(glass, Material::default()), RED);
512 assert_eq!(refined.resolved(frame, Material::default()), GOLD);
513 assert_eq!(replaced.resolved(glass, Material::default()), GOLD);
514 assert_eq!(replaced.resolved(frame, Material::default()), GOLD);
515 }
516
517 #[test]
518 fn an_anonymous_slot_takes_the_write_to_every_slot_and_no_write_to_a_part() {
519 let draw = Lantern
520 .at::<()>(Vec3::ZERO)
521 .material(GOLD)
522 .material_of(LanternPart::Glass, RED)
523 .record();
524
525 assert_eq!(draw.resolved(None, Material::default()), GOLD);
526 assert_eq!(
527 Cube.at::<()>(Vec3::ZERO).record().resolved(None, RED),
528 RED,
529 "and a slot no draw wrote to keeps its default"
530 );
531 }
532
533 fn turned() -> Transform {
536 Transform::from_scale_rotation_translation(
537 Vec3::new(1.0, 2.0, 3.0),
538 Quat::from_rotation_x(0.7) * Quat::from_rotation_y(1.1),
539 Vec3::new(4.0, 5.0, 6.0),
540 )
541 }
542
543 fn columns(transform: Transform) -> [Vec3; 3] {
545 let model = transform.matrix();
546 [model.x_axis, model.y_axis, model.z_axis].map(Vec4::truncate)
547 }
548
549 #[test]
550 fn a_billboarded_draw_stands_across_the_direction_the_camera_looks() {
551 for eye in [Vec3::new(0.0, 0.0, 3.0), Vec3::new(3.0, 4.0, -5.0)] {
552 let view = View::look_at(eye, Vec3::ZERO);
553 let ahead = (view.target() - view.eye()).normalize();
554 let [across, up, out] = columns(Facing::Billboard.applied(turned(), view, STILL));
555
556 assert!(across.dot(ahead).abs() < 1e-5, "{across} leans out of view");
557 assert!(up.dot(ahead).abs() < 1e-5, "{up} leans out of view");
558 assert!(
559 out.normalize().abs_diff_eq(-ahead, 1e-5),
560 "{out} faces away"
561 );
562 }
563 }
564
565 #[test]
566 fn an_upright_draw_keeps_the_way_up_and_turns_about_it_alone() {
567 let view = View::look_at(Vec3::new(3.0, 9.0, 3.0), Vec3::ZERO);
568 let [across, up, out] = columns(Facing::Upright.applied(turned(), view, STILL));
569
570 assert!(up.abs_diff_eq(Vec3::Y * 2.0, 1e-5), "{up} left the way up");
571 assert!(
572 across.y.abs() < 1e-5 && out.y.abs() < 1e-5,
573 "and stood level"
574 );
575 assert!(
576 out.normalize()
577 .abs_diff_eq(Vec3::new(3.0, 0.0, 3.0).normalize(), 1e-5),
578 "{out} does not face the camera"
579 );
580 }
581
582 #[test]
583 fn facing_keeps_the_sizes_and_the_position_the_transform_gave_a_draw() {
584 for (facing, roll) in [
585 (Facing::Billboard, STILL),
586 (Facing::Billboard, QUARTER),
587 (Facing::Upright, STILL),
588 ] {
589 let faced = facing.applied(turned(), DIVING, roll);
590 let sizes = columns(faced).map(|column| column.length());
591
592 assert!(
593 sizes
594 .iter()
595 .zip(columns(turned()))
596 .all(|(kept, column)| (kept - column.length()).abs() < 1e-5),
597 "{sizes:?} are not the sizes the transform carried"
598 );
599 assert_eq!(faced.matrix().w_axis, turned().matrix().w_axis);
600 assert_ne!(columns(faced), columns(turned()), "and the turn is gone");
601 }
602 }
603
604 #[test]
605 fn a_camera_straight_overhead_leaves_an_upright_draw_standing() {
606 let view = View::look_at(Vec3::Y * 5.0, Vec3::ZERO).with_up(Vec3::NEG_Z);
607 let [across, up, out] = columns(Facing::Upright.applied(Transform::IDENTITY, view, STILL));
608
609 assert_eq!(up, Vec3::Y);
610 assert!(across.is_finite() && out.is_finite(), "{across} {out}");
611 assert!(out.y.abs() < 1e-5, "so it is seen edge-on from up there");
612 }
613
614 #[test]
615 fn a_billboard_stands_even_where_the_camera_looks_along_its_own_way_up() {
616 let view = View::look_at(Vec3::Y * 5.0, Vec3::ZERO);
617 let [across, up, out] =
618 columns(Facing::Billboard.applied(Transform::IDENTITY, view, STILL));
619
620 assert!(across.is_finite() && up.is_finite(), "{across} {up}");
621 assert!(out.abs_diff_eq(Vec3::Y, 1e-5), "{out} does not face back");
622 }
623
624 #[test]
625 fn a_quarter_of_a_roll_takes_a_billboards_across_onto_the_way_up() {
626 let view = View::look_at(Vec3::Z * 4.0, Vec3::ZERO);
627 let [across, up, out] =
628 columns(Facing::Billboard.applied(Transform::IDENTITY, view, QUARTER));
629
630 assert!(
631 across.abs_diff_eq(Vec3::Y, 1e-5),
632 "{across} is not the way the camera is up"
633 );
634 assert!(up.abs_diff_eq(Vec3::NEG_X, 1e-5), "{up} followed it around");
635 assert!(out.abs_diff_eq(Vec3::Z, 1e-5), "{out} left the view plane");
636 }
637
638 #[test]
639 fn a_rolled_billboard_stands_in_the_view_plane_however_far_it_is_turned() {
640 let view = View::look_at(Vec3::new(3.0, 4.0, -5.0), Vec3::ZERO);
641 let ahead = (view.target() - view.eye()).normalize();
642
643 for roll in [0.3, 2.0, -1.7, 100.0] {
644 let [across, up, out] =
645 columns(Facing::Billboard.applied(turned(), view, roll)).map(Vec3::normalize);
646
647 assert!(across.dot(up).abs() < 1e-5, "{across} leans onto {up}");
648 assert!(
649 across.dot(ahead).abs() < 1e-5 && up.dot(ahead).abs() < 1e-5,
650 "{across} or {up} leans out of view"
651 );
652 assert!(out.abs_diff_eq(-ahead, 1e-5), "{out} faces away");
653 }
654 }
655
656 #[test]
657 fn only_a_billboarded_draw_is_turned_by_the_roll_it_asks_for() {
658 for facing in [Facing::AsPlaced, Facing::Upright] {
659 assert_eq!(
660 facing.applied(turned(), DIVING, QUARTER),
661 facing.applied(turned(), DIVING, STILL),
662 "a turn of its own is a turn roll has no say in"
663 );
664 }
665 assert_ne!(
666 Facing::Billboard.applied(turned(), DIVING, QUARTER),
667 Facing::Billboard.applied(turned(), DIVING, STILL),
668 "where a billboarded draw leaves it free"
669 );
670 }
671
672 fn placed(instance: Instance<Cube>) -> Placement {
674 instance.record().placement(DIVING)
675 }
676
677 #[test]
678 fn a_draw_is_rolled_whichever_way_round_it_asked_to_be_billboarded() {
679 let cube = Cube.at::<()>(turned());
680
681 assert_eq!(
682 placed(cube.clone().roll(QUARTER).billboard()),
683 placed(cube.clone().billboard().roll(QUARTER))
684 );
685 assert_eq!(
686 placed(cube.clone()),
687 placed(cube.roll(QUARTER)),
688 "and a draw the camera never turned is left where it was"
689 );
690 }
691
692 #[test]
693 fn a_camera_that_looks_nowhere_leaves_a_faced_draw_where_it_was() {
694 let view = View::look_at(Vec3::Y, Vec3::Y);
695
696 for facing in [Facing::Billboard, Facing::Upright] {
697 assert_eq!(facing.applied(turned(), view, STILL), turned());
698 }
699 }
700
701 #[test]
702 fn the_last_facing_a_draw_asks_for_is_the_one_it_is_turned_by() {
703 let cube = Cube.at::<()>(turned());
704
705 assert_eq!(
706 placed(cube.clone().billboard().upright()),
707 placed(cube.clone().upright())
708 );
709 assert_eq!(
710 placed(cube.clone().upright().billboard()),
711 placed(cube.clone().billboard())
712 );
713 assert_ne!(
714 placed(cube.clone().upright()),
715 placed(cube.clone().billboard())
716 );
717 assert!(
718 !cube.record().faced(),
719 "and a draw asks for neither by default"
720 );
721 }
722}