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::{NoSurfaceStyles, 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 keyed<T>(self, mesh: T) -> Draw<T> {
61 let Self {
62 mesh: _,
63 transform,
64 facing,
65 roll,
66 frame,
67 style,
68 fade,
69 posed,
70 paints,
71 } = self;
72 Draw {
73 mesh,
74 transform,
75 facing,
76 roll,
77 frame,
78 style,
79 fade,
80 posed,
81 paints,
82 }
83 }
84
85 pub(crate) fn mesh(&self) -> &M {
86 &self.mesh
87 }
88
89 pub(crate) fn placement(&self, view: View) -> Placement {
91 Placement {
92 transform: self.facing.applied(self.transform, view, self.roll),
93 faced: self.faced(),
94 }
95 }
96
97 pub(crate) fn anchor(&self) -> Vec3 {
99 self.transform.matrix().w_axis.truncate()
100 }
101
102 pub(crate) fn window(&self) -> Frame {
104 self.frame
105 }
106
107 pub(crate) fn faced(&self) -> bool {
110 self.facing != Facing::AsPlaced
111 }
112
113 pub(crate) fn styled(&self) -> Option<Styled> {
115 self.style
116 }
117
118 pub(crate) fn posing(&self, now: Duration, clips: &[Animation]) -> Option<Posing> {
121 Some(self.posed?.posing(now, clips))
122 }
123
124 pub(crate) fn resolved(&self, part: Option<u32>, default: Material) -> Material {
128 part.and_then(|part| self.paints.of(part))
129 .or(self.paints.every)
130 .unwrap_or(default)
131 .faded(self.fade)
132 }
133}
134
135#[must_use = "an instance is only drawn once FrameContext::draw takes it"]
145#[derive(Debug)]
146pub struct Instance<M, S: SurfaceStyles = NoSurfaceStyles> {
147 draw: Draw<M>,
148 styles: PhantomData<S>,
149}
150
151impl<M, S: SurfaceStyles> Instance<M, S> {
152 pub(crate) fn new(mesh: M, transform: Transform) -> Self {
154 Self {
155 draw: Draw {
156 mesh,
157 transform,
158 facing: Facing::AsPlaced,
159 roll: 0.0,
160 frame: Frame::default(),
161 style: None,
162 fade: OPAQUE,
163 posed: None,
164 paints: Paints::default(),
165 },
166 styles: PhantomData,
167 }
168 }
169
170 pub fn at(mut self, transform: impl Into<Transform>) -> Self {
172 self.draw.transform = transform.into();
173 self
174 }
175
176 pub fn billboard(mut self) -> Self {
182 self.draw.facing = Facing::Billboard;
183 self
184 }
185
186 pub fn upright(mut self) -> Self {
193 self.draw.facing = Facing::Upright;
194 self
195 }
196
197 pub fn roll(mut self, radians: f32) -> Self {
204 self.draw.roll = radians;
205 self
206 }
207
208 pub fn surface_style<T: SurfaceStyle>(mut self) -> Self
217 where
218 S: Holds<T>,
219 {
220 let seat = SurfaceStyleId(S::from(T::default()).seat());
221 self.draw.style = Some(Styled::at::<T>(seat));
222 self
223 }
224
225 pub fn posed<P: Part, A: AnimationStates>(mut self, animator: &Animator<M, A>) -> Self
232 where
233 M: Mesh<P, A::Clip>,
234 {
235 self.draw.posed = Some(animator.running());
236 self
237 }
238
239 #[cfg(all(test, feature = "offscreen"))]
246 pub(crate) fn posed_by(mut self, posing: Posing) -> Self {
247 self.draw.posed = Some(Running::stopped(posing));
248 self
249 }
250
251 pub fn frame(mut self, frame: Frame) -> Self {
254 self.draw.frame = frame;
255 self
256 }
257
258 pub fn material(mut self, material: Material) -> Self {
267 self.draw.paints.every(material);
268 self
269 }
270
271 pub fn material_of<P: Part, C: Clip>(mut self, part: P, material: Material) -> Self
277 where
278 M: Mesh<P, C>,
279 {
280 self.draw.paints.one(part.index(), material);
281 self
282 }
283
284 pub fn faded(mut self, alpha: f32) -> Self {
299 self.draw.fade = alpha.clamp(0.0, OPAQUE);
300 self
301 }
302
303 pub fn into_set<T: From<M>>(self) -> Instance<T, S> {
310 Instance {
311 draw: self.draw.into_set(),
312 styles: PhantomData,
313 }
314 }
315
316 pub(crate) fn record(self) -> Draw<M> {
320 self.draw
321 }
322}
323
324impl<M: Clone, S: SurfaceStyles> Clone for Instance<M, S> {
325 fn clone(&self) -> Self {
326 Self {
327 draw: self.draw.clone(),
328 styles: PhantomData,
329 }
330 }
331}
332
333#[derive(Clone, Copy, Debug, Eq, PartialEq)]
335enum Facing {
336 AsPlaced,
337 Billboard,
338 Upright,
339}
340
341impl Facing {
342 fn applied(self, transform: Transform, view: View, roll: f32) -> Transform {
346 let Some(turn) = self.turn(view, roll) else {
347 return transform;
348 };
349
350 let model = transform.matrix();
351 let sized = |axis: Vec3, column: Vec4| (axis * column.truncate().length()).extend(0.0);
352 Transform::from(Mat4::from_cols(
353 sized(turn.x_axis, model.x_axis),
354 sized(turn.y_axis, model.y_axis),
355 sized(turn.z_axis, model.z_axis),
356 model.w_axis,
357 ))
358 }
359
360 fn turn(self, view: View, roll: f32) -> Option<Mat3> {
363 match self {
364 Self::AsPlaced => None,
365 Self::Billboard => {
368 Some(view_plane(looking(view)?, view.up()) * Mat3::from_rotation_z(roll))
369 }
370 Self::Upright => Some(standing(looking(view)?)),
371 }
372 }
373}
374
375#[derive(Clone, Copy, Debug, PartialEq)]
381pub(crate) struct Placement {
382 transform: Transform,
383 faced: bool,
384}
385
386impl Placement {
387 pub(crate) fn transform(self) -> Transform {
389 self.transform
390 }
391
392 pub(crate) fn faced(self) -> bool {
394 self.faced
395 }
396}
397
398fn looking(view: View) -> Option<Vec3> {
401 (view.target() - view.eye()).try_normalize()
402}
403
404fn view_plane(looking: Vec3, up: Vec3) -> Mat3 {
406 let across = looking
407 .cross(up)
408 .try_normalize()
409 .unwrap_or_else(|| looking.cross(aside(looking)).normalize());
410
411 Mat3::from_cols(across, across.cross(looking), -looking)
412}
413
414fn standing(looking: Vec3) -> Mat3 {
416 let back = Vec3::new(-looking.x, 0.0, -looking.z)
417 .try_normalize()
418 .unwrap_or(Vec3::Z);
419
420 Mat3::from_cols(Vec3::Y.cross(back), Vec3::Y, back)
421}
422
423fn aside(looking: Vec3) -> Vec3 {
425 if looking.y.abs() > 0.99 {
426 Vec3::Z
427 } else {
428 Vec3::Y
429 }
430}
431
432#[derive(Clone, Debug, Default)]
435struct Paints {
436 every: Option<Material>,
437 parts: Vec<Option<Material>>,
438}
439
440impl Paints {
441 fn every(&mut self, material: Material) {
443 self.every = Some(material);
444 self.parts.clear();
445 }
446
447 fn one(&mut self, part: u32, material: Material) {
448 let at = part as usize;
449 if at >= self.parts.len() {
450 self.parts.resize(at + 1, None);
451 }
452 self.parts[at] = Some(material);
453 }
454
455 fn of(&self, part: u32) -> Option<Material> {
458 self.parts.get(part as usize).copied().flatten()
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465 use crate::math::Quat;
466 use crate::mesh::{Cube, MeshData, Slot};
467 use crate::{Assets, Catalog, Color};
468
469 const DIVING: View = View::look_at(Vec3::new(0.0, 5.0, 5.0), Vec3::ZERO);
472
473 const STILL: f32 = 0.0;
475
476 const QUARTER: f32 = core::f32::consts::FRAC_PI_2;
479
480 const GOLD: Material = Material::lit(Color::rgb(1.0, 0.8, 0.2));
481 const RED: Material = Material::lit(Color::rgb(1.0, 0.0, 0.0));
482
483 #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
485 struct Lantern;
486
487 impl Catalog for Lantern {
488 fn catalog() -> Vec<Self> {
489 vec![Self]
490 }
491 }
492
493 impl Mesh<LanternPart> for Lantern {
494 fn build(&self, assets: &Assets) -> MeshData<LanternPart> {
495 let cube = Cube.build(assets);
496 let half = cube.indices().len() as u32 / 2;
497 MeshData::in_parts(cube.vertices().to_vec(), cube.indices().to_vec(), |_| {
498 Slot::new(half, Material::default())
499 })
500 }
501 }
502
503 #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
504 enum LanternPart {
505 Frame,
506 Glass,
507 }
508
509 impl Part for LanternPart {
510 fn from_name(_name: &str) -> Option<Self> {
511 None
512 }
513
514 fn all() -> Vec<Self> {
515 vec![Self::Frame, Self::Glass]
516 }
517
518 fn index(&self) -> u32 {
519 *self as u32
520 }
521 }
522
523 #[test]
524 fn the_last_write_to_a_part_is_the_one_a_slot_resolves_to() {
525 let refined = Lantern
526 .at::<NoSurfaceStyles>(Vec3::ZERO)
527 .material(GOLD)
528 .material_of(LanternPart::Glass, RED)
529 .record();
530 let replaced = Lantern
531 .at::<NoSurfaceStyles>(Vec3::ZERO)
532 .material_of(LanternPart::Glass, RED)
533 .material(GOLD)
534 .record();
535 let glass = Some(LanternPart::Glass.index());
536 let frame = Some(LanternPart::Frame.index());
537
538 assert_eq!(refined.resolved(glass, Material::default()), RED);
539 assert_eq!(refined.resolved(frame, Material::default()), GOLD);
540 assert_eq!(replaced.resolved(glass, Material::default()), GOLD);
541 assert_eq!(replaced.resolved(frame, Material::default()), GOLD);
542 }
543
544 #[test]
545 fn an_anonymous_slot_takes_the_write_to_every_slot_and_no_write_to_a_part() {
546 let draw = Lantern
547 .at::<NoSurfaceStyles>(Vec3::ZERO)
548 .material(GOLD)
549 .material_of(LanternPart::Glass, RED)
550 .record();
551
552 assert_eq!(draw.resolved(None, Material::default()), GOLD);
553 assert_eq!(
554 Cube.at::<NoSurfaceStyles>(Vec3::ZERO)
555 .record()
556 .resolved(None, RED),
557 RED,
558 "and a slot no draw wrote to keeps its default"
559 );
560 }
561
562 fn turned() -> Transform {
565 Transform::from_scale_rotation_translation(
566 Vec3::new(1.0, 2.0, 3.0),
567 Quat::from_rotation_x(0.7) * Quat::from_rotation_y(1.1),
568 Vec3::new(4.0, 5.0, 6.0),
569 )
570 }
571
572 fn columns(transform: Transform) -> [Vec3; 3] {
574 let model = transform.matrix();
575 [model.x_axis, model.y_axis, model.z_axis].map(Vec4::truncate)
576 }
577
578 #[test]
579 fn a_billboarded_draw_stands_across_the_direction_the_camera_looks() {
580 for eye in [Vec3::new(0.0, 0.0, 3.0), Vec3::new(3.0, 4.0, -5.0)] {
581 let view = View::look_at(eye, Vec3::ZERO);
582 let ahead = (view.target() - view.eye()).normalize();
583 let [across, up, out] = columns(Facing::Billboard.applied(turned(), view, STILL));
584
585 assert!(across.dot(ahead).abs() < 1e-5, "{across} leans out of view");
586 assert!(up.dot(ahead).abs() < 1e-5, "{up} leans out of view");
587 assert!(
588 out.normalize().abs_diff_eq(-ahead, 1e-5),
589 "{out} faces away"
590 );
591 }
592 }
593
594 #[test]
595 fn an_upright_draw_keeps_the_way_up_and_turns_about_it_alone() {
596 let view = View::look_at(Vec3::new(3.0, 9.0, 3.0), Vec3::ZERO);
597 let [across, up, out] = columns(Facing::Upright.applied(turned(), view, STILL));
598
599 assert!(up.abs_diff_eq(Vec3::Y * 2.0, 1e-5), "{up} left the way up");
600 assert!(
601 across.y.abs() < 1e-5 && out.y.abs() < 1e-5,
602 "and stood level"
603 );
604 assert!(
605 out.normalize()
606 .abs_diff_eq(Vec3::new(3.0, 0.0, 3.0).normalize(), 1e-5),
607 "{out} does not face the camera"
608 );
609 }
610
611 #[test]
612 fn facing_keeps_the_sizes_and_the_position_the_transform_gave_a_draw() {
613 for (facing, roll) in [
614 (Facing::Billboard, STILL),
615 (Facing::Billboard, QUARTER),
616 (Facing::Upright, STILL),
617 ] {
618 let faced = facing.applied(turned(), DIVING, roll);
619 let sizes = columns(faced).map(|column| column.length());
620
621 assert!(
622 sizes
623 .iter()
624 .zip(columns(turned()))
625 .all(|(kept, column)| (kept - column.length()).abs() < 1e-5),
626 "{sizes:?} are not the sizes the transform carried"
627 );
628 assert_eq!(faced.matrix().w_axis, turned().matrix().w_axis);
629 assert_ne!(columns(faced), columns(turned()), "and the turn is gone");
630 }
631 }
632
633 #[test]
634 fn a_camera_straight_overhead_leaves_an_upright_draw_standing() {
635 let view = View::look_at(Vec3::Y * 5.0, Vec3::ZERO).with_up(Vec3::NEG_Z);
636 let [across, up, out] = columns(Facing::Upright.applied(Transform::IDENTITY, view, STILL));
637
638 assert_eq!(up, Vec3::Y);
639 assert!(across.is_finite() && out.is_finite(), "{across} {out}");
640 assert!(out.y.abs() < 1e-5, "so it is seen edge-on from up there");
641 }
642
643 #[test]
644 fn a_billboard_stands_even_where_the_camera_looks_along_its_own_way_up() {
645 let view = View::look_at(Vec3::Y * 5.0, Vec3::ZERO);
646 let [across, up, out] =
647 columns(Facing::Billboard.applied(Transform::IDENTITY, view, STILL));
648
649 assert!(across.is_finite() && up.is_finite(), "{across} {up}");
650 assert!(out.abs_diff_eq(Vec3::Y, 1e-5), "{out} does not face back");
651 }
652
653 #[test]
654 fn a_quarter_of_a_roll_takes_a_billboards_across_onto_the_way_up() {
655 let view = View::look_at(Vec3::Z * 4.0, Vec3::ZERO);
656 let [across, up, out] =
657 columns(Facing::Billboard.applied(Transform::IDENTITY, view, QUARTER));
658
659 assert!(
660 across.abs_diff_eq(Vec3::Y, 1e-5),
661 "{across} is not the way the camera is up"
662 );
663 assert!(up.abs_diff_eq(Vec3::NEG_X, 1e-5), "{up} followed it around");
664 assert!(out.abs_diff_eq(Vec3::Z, 1e-5), "{out} left the view plane");
665 }
666
667 #[test]
668 fn a_rolled_billboard_stands_in_the_view_plane_however_far_it_is_turned() {
669 let view = View::look_at(Vec3::new(3.0, 4.0, -5.0), Vec3::ZERO);
670 let ahead = (view.target() - view.eye()).normalize();
671
672 for roll in [0.3, 2.0, -1.7, 100.0] {
673 let [across, up, out] =
674 columns(Facing::Billboard.applied(turned(), view, roll)).map(Vec3::normalize);
675
676 assert!(across.dot(up).abs() < 1e-5, "{across} leans onto {up}");
677 assert!(
678 across.dot(ahead).abs() < 1e-5 && up.dot(ahead).abs() < 1e-5,
679 "{across} or {up} leans out of view"
680 );
681 assert!(out.abs_diff_eq(-ahead, 1e-5), "{out} faces away");
682 }
683 }
684
685 #[test]
686 fn only_a_billboarded_draw_is_turned_by_the_roll_it_asks_for() {
687 for facing in [Facing::AsPlaced, Facing::Upright] {
688 assert_eq!(
689 facing.applied(turned(), DIVING, QUARTER),
690 facing.applied(turned(), DIVING, STILL),
691 "a turn of its own is a turn roll has no say in"
692 );
693 }
694 assert_ne!(
695 Facing::Billboard.applied(turned(), DIVING, QUARTER),
696 Facing::Billboard.applied(turned(), DIVING, STILL),
697 "where a billboarded draw leaves it free"
698 );
699 }
700
701 fn placed(instance: Instance<Cube>) -> Placement {
703 instance.record().placement(DIVING)
704 }
705
706 #[test]
707 fn a_draw_is_rolled_whichever_way_round_it_asked_to_be_billboarded() {
708 let cube = Cube.at::<NoSurfaceStyles>(turned());
709
710 assert_eq!(
711 placed(cube.clone().roll(QUARTER).billboard()),
712 placed(cube.clone().billboard().roll(QUARTER))
713 );
714 assert_eq!(
715 placed(cube.clone()),
716 placed(cube.roll(QUARTER)),
717 "and a draw the camera never turned is left where it was"
718 );
719 }
720
721 #[test]
722 fn a_camera_that_looks_nowhere_leaves_a_faced_draw_where_it_was() {
723 let view = View::look_at(Vec3::Y, Vec3::Y);
724
725 for facing in [Facing::Billboard, Facing::Upright] {
726 assert_eq!(facing.applied(turned(), view, STILL), turned());
727 }
728 }
729
730 #[test]
731 fn the_last_facing_a_draw_asks_for_is_the_one_it_is_turned_by() {
732 let cube = Cube.at::<NoSurfaceStyles>(turned());
733
734 assert_eq!(
735 placed(cube.clone().billboard().upright()),
736 placed(cube.clone().upright())
737 );
738 assert_eq!(
739 placed(cube.clone().upright().billboard()),
740 placed(cube.clone().billboard())
741 );
742 assert_ne!(
743 placed(cube.clone().upright()),
744 placed(cube.clone().billboard())
745 );
746 assert!(
747 !cube.record().faced(),
748 "and a draw asks for neither by default"
749 );
750 }
751}