use core::marker::PhantomData;
use core::time::Duration;
use crate::animation::{AnimationStates, Animator, Running};
use crate::math::{Mat3, Mat4, Vec3, Vec4};
use crate::mesh::{Animation, Clip, Frame, Mesh, Part, Posing};
use crate::surface_style::{Styled, SurfaceStyle, SurfaceStyleId, SurfaceStyles};
use crate::{Holds, Material, Transform, View};
const OPAQUE: f32 = 1.0;
#[derive(Clone, Debug)]
pub(crate) struct Draw<M> {
mesh: M,
transform: Transform,
facing: Facing,
roll: f32,
frame: Frame,
style: Option<Styled>,
fade: f32,
posed: Option<Running>,
paints: Paints,
}
impl<M> Draw<M> {
pub(crate) fn into_set<T: From<M>>(self) -> Draw<T> {
let Self {
mesh,
transform,
facing,
roll,
frame,
style,
fade,
posed,
paints,
} = self;
Draw {
mesh: mesh.into(),
transform,
facing,
roll,
frame,
style,
fade,
posed,
paints,
}
}
pub(crate) fn mesh(&self) -> &M {
&self.mesh
}
pub(crate) fn placement(&self, view: View) -> Placement {
Placement {
transform: self.facing.applied(self.transform, view, self.roll),
faced: self.faced(),
}
}
pub(crate) fn anchor(&self) -> Vec3 {
self.transform.matrix().w_axis.truncate()
}
pub(crate) fn window(&self) -> Frame {
self.frame
}
pub(crate) fn faced(&self) -> bool {
self.facing != Facing::AsPlaced
}
pub(crate) fn styled(&self) -> Option<Styled> {
self.style
}
pub(crate) fn posing(&self, now: Duration, clips: &[Animation]) -> Option<Posing> {
Some(self.posed?.posing(now, clips))
}
pub(crate) fn resolved(&self, part: Option<u32>, default: Material) -> Material {
part.and_then(|part| self.paints.of(part))
.or(self.paints.every)
.unwrap_or(default)
.faded(self.fade)
}
}
#[must_use = "an instance is only drawn once FrameContext::draw takes it"]
#[derive(Debug)]
pub struct Instance<M, S: SurfaceStyles = ()> {
draw: Draw<M>,
styles: PhantomData<S>,
}
impl<M, S: SurfaceStyles> Instance<M, S> {
pub(crate) fn new(mesh: M, transform: Transform) -> Self {
Self {
draw: Draw {
mesh,
transform,
facing: Facing::AsPlaced,
roll: 0.0,
frame: Frame::default(),
style: None,
fade: OPAQUE,
posed: None,
paints: Paints::default(),
},
styles: PhantomData,
}
}
pub fn at(mut self, transform: impl Into<Transform>) -> Self {
self.draw.transform = transform.into();
self
}
pub fn billboard(mut self) -> Self {
self.draw.facing = Facing::Billboard;
self
}
pub fn upright(mut self) -> Self {
self.draw.facing = Facing::Upright;
self
}
pub fn roll(mut self, radians: f32) -> Self {
self.draw.roll = radians;
self
}
pub fn surface_style<T: SurfaceStyle>(mut self) -> Self
where
S: Holds<T> + From<T>,
{
let seat = SurfaceStyleId(S::from(T::default()).seat());
self.draw.style = Some(Styled::at::<T>(seat));
self
}
pub fn posed<P: Part, A: AnimationStates>(mut self, animator: &Animator<M, A>) -> Self
where
M: Mesh<P, A::Clip>,
{
self.draw.posed = Some(animator.running());
self
}
#[cfg(all(test, feature = "offscreen"))]
pub(crate) fn posed_by(mut self, posing: Posing) -> Self {
self.draw.posed = Some(Running::stopped(posing));
self
}
pub fn frame(mut self, frame: Frame) -> Self {
self.draw.frame = frame;
self
}
pub fn material(mut self, material: Material) -> Self {
self.draw.paints.every(material);
self
}
pub fn material_of<P: Part, C: Clip>(mut self, part: P, material: Material) -> Self
where
M: Mesh<P, C>,
{
self.draw.paints.one(part.index(), material);
self
}
pub fn faded(mut self, alpha: f32) -> Self {
self.draw.fade = alpha.clamp(0.0, OPAQUE);
self
}
pub fn into_set<T: From<M>>(self) -> Instance<T, S> {
Instance {
draw: self.draw.into_set(),
styles: PhantomData,
}
}
pub(crate) fn record(self) -> Draw<M> {
self.draw
}
}
impl<M: Clone, S: SurfaceStyles> Clone for Instance<M, S> {
fn clone(&self) -> Self {
Self {
draw: self.draw.clone(),
styles: PhantomData,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Facing {
AsPlaced,
Billboard,
Upright,
}
impl Facing {
fn applied(self, transform: Transform, view: View, roll: f32) -> Transform {
let Some(turn) = self.turn(view, roll) else {
return transform;
};
let model = transform.matrix();
let sized = |axis: Vec3, column: Vec4| (axis * column.truncate().length()).extend(0.0);
Transform::from(Mat4::from_cols(
sized(turn.x_axis, model.x_axis),
sized(turn.y_axis, model.y_axis),
sized(turn.z_axis, model.z_axis),
model.w_axis,
))
}
fn turn(self, view: View, roll: f32) -> Option<Mat3> {
match self {
Self::AsPlaced => None,
Self::Billboard => {
Some(view_plane(looking(view)?, view.up()) * Mat3::from_rotation_z(roll))
}
Self::Upright => Some(standing(looking(view)?)),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Placement {
transform: Transform,
faced: bool,
}
impl Placement {
pub(crate) fn transform(self) -> Transform {
self.transform
}
pub(crate) fn faced(self) -> bool {
self.faced
}
}
fn looking(view: View) -> Option<Vec3> {
(view.target() - view.eye()).try_normalize()
}
fn view_plane(looking: Vec3, up: Vec3) -> Mat3 {
let across = looking
.cross(up)
.try_normalize()
.unwrap_or_else(|| looking.cross(aside(looking)).normalize());
Mat3::from_cols(across, across.cross(looking), -looking)
}
fn standing(looking: Vec3) -> Mat3 {
let back = Vec3::new(-looking.x, 0.0, -looking.z)
.try_normalize()
.unwrap_or(Vec3::Z);
Mat3::from_cols(Vec3::Y.cross(back), Vec3::Y, back)
}
fn aside(looking: Vec3) -> Vec3 {
if looking.y.abs() > 0.99 {
Vec3::Z
} else {
Vec3::Y
}
}
#[derive(Clone, Debug, Default)]
struct Paints {
every: Option<Material>,
parts: Vec<Option<Material>>,
}
impl Paints {
fn every(&mut self, material: Material) {
self.every = Some(material);
self.parts.clear();
}
fn one(&mut self, part: u32, material: Material) {
let at = part as usize;
if at >= self.parts.len() {
self.parts.resize(at + 1, None);
}
self.parts[at] = Some(material);
}
fn of(&self, part: u32) -> Option<Material> {
self.parts.get(part as usize).copied().flatten()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::Quat;
use crate::mesh::{Cube, MeshData, Slot};
use crate::{Assets, Catalog, Color};
const DIVING: View = View::look_at(Vec3::new(0.0, 5.0, 5.0), Vec3::ZERO);
const STILL: f32 = 0.0;
const QUARTER: f32 = core::f32::consts::FRAC_PI_2;
const GOLD: Material = Material::lit(Color::rgb(1.0, 0.8, 0.2));
const RED: Material = Material::lit(Color::rgb(1.0, 0.0, 0.0));
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
struct Lantern;
impl Catalog for Lantern {
fn catalog() -> Vec<Self> {
vec![Self]
}
}
impl Mesh<LanternPart> for Lantern {
fn build(&self, assets: &Assets) -> MeshData<LanternPart> {
let cube = Cube.build(assets);
let half = cube.indices().len() as u32 / 2;
MeshData::in_parts(cube.vertices().to_vec(), cube.indices().to_vec(), |_| {
Slot::new(half, Material::default())
})
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum LanternPart {
Frame,
Glass,
}
impl Part for LanternPart {
fn from_name(_name: &str) -> Option<Self> {
None
}
fn all() -> Vec<Self> {
vec![Self::Frame, Self::Glass]
}
fn index(&self) -> u32 {
*self as u32
}
}
#[test]
fn the_last_write_to_a_part_is_the_one_a_slot_resolves_to() {
let refined = Lantern
.at::<()>(Vec3::ZERO)
.material(GOLD)
.material_of(LanternPart::Glass, RED)
.record();
let replaced = Lantern
.at::<()>(Vec3::ZERO)
.material_of(LanternPart::Glass, RED)
.material(GOLD)
.record();
let glass = Some(LanternPart::Glass.index());
let frame = Some(LanternPart::Frame.index());
assert_eq!(refined.resolved(glass, Material::default()), RED);
assert_eq!(refined.resolved(frame, Material::default()), GOLD);
assert_eq!(replaced.resolved(glass, Material::default()), GOLD);
assert_eq!(replaced.resolved(frame, Material::default()), GOLD);
}
#[test]
fn an_anonymous_slot_takes_the_write_to_every_slot_and_no_write_to_a_part() {
let draw = Lantern
.at::<()>(Vec3::ZERO)
.material(GOLD)
.material_of(LanternPart::Glass, RED)
.record();
assert_eq!(draw.resolved(None, Material::default()), GOLD);
assert_eq!(
Cube.at::<()>(Vec3::ZERO).record().resolved(None, RED),
RED,
"and a slot no draw wrote to keeps its default"
);
}
fn turned() -> Transform {
Transform::from_scale_rotation_translation(
Vec3::new(1.0, 2.0, 3.0),
Quat::from_rotation_x(0.7) * Quat::from_rotation_y(1.1),
Vec3::new(4.0, 5.0, 6.0),
)
}
fn columns(transform: Transform) -> [Vec3; 3] {
let model = transform.matrix();
[model.x_axis, model.y_axis, model.z_axis].map(Vec4::truncate)
}
#[test]
fn a_billboarded_draw_stands_across_the_direction_the_camera_looks() {
for eye in [Vec3::new(0.0, 0.0, 3.0), Vec3::new(3.0, 4.0, -5.0)] {
let view = View::look_at(eye, Vec3::ZERO);
let ahead = (view.target() - view.eye()).normalize();
let [across, up, out] = columns(Facing::Billboard.applied(turned(), view, STILL));
assert!(across.dot(ahead).abs() < 1e-5, "{across} leans out of view");
assert!(up.dot(ahead).abs() < 1e-5, "{up} leans out of view");
assert!(
out.normalize().abs_diff_eq(-ahead, 1e-5),
"{out} faces away"
);
}
}
#[test]
fn an_upright_draw_keeps_the_way_up_and_turns_about_it_alone() {
let view = View::look_at(Vec3::new(3.0, 9.0, 3.0), Vec3::ZERO);
let [across, up, out] = columns(Facing::Upright.applied(turned(), view, STILL));
assert!(up.abs_diff_eq(Vec3::Y * 2.0, 1e-5), "{up} left the way up");
assert!(
across.y.abs() < 1e-5 && out.y.abs() < 1e-5,
"and stood level"
);
assert!(
out.normalize()
.abs_diff_eq(Vec3::new(3.0, 0.0, 3.0).normalize(), 1e-5),
"{out} does not face the camera"
);
}
#[test]
fn facing_keeps_the_sizes_and_the_position_the_transform_gave_a_draw() {
for (facing, roll) in [
(Facing::Billboard, STILL),
(Facing::Billboard, QUARTER),
(Facing::Upright, STILL),
] {
let faced = facing.applied(turned(), DIVING, roll);
let sizes = columns(faced).map(|column| column.length());
assert!(
sizes
.iter()
.zip(columns(turned()))
.all(|(kept, column)| (kept - column.length()).abs() < 1e-5),
"{sizes:?} are not the sizes the transform carried"
);
assert_eq!(faced.matrix().w_axis, turned().matrix().w_axis);
assert_ne!(columns(faced), columns(turned()), "and the turn is gone");
}
}
#[test]
fn a_camera_straight_overhead_leaves_an_upright_draw_standing() {
let view = View::look_at(Vec3::Y * 5.0, Vec3::ZERO).with_up(Vec3::NEG_Z);
let [across, up, out] = columns(Facing::Upright.applied(Transform::IDENTITY, view, STILL));
assert_eq!(up, Vec3::Y);
assert!(across.is_finite() && out.is_finite(), "{across} {out}");
assert!(out.y.abs() < 1e-5, "so it is seen edge-on from up there");
}
#[test]
fn a_billboard_stands_even_where_the_camera_looks_along_its_own_way_up() {
let view = View::look_at(Vec3::Y * 5.0, Vec3::ZERO);
let [across, up, out] =
columns(Facing::Billboard.applied(Transform::IDENTITY, view, STILL));
assert!(across.is_finite() && up.is_finite(), "{across} {up}");
assert!(out.abs_diff_eq(Vec3::Y, 1e-5), "{out} does not face back");
}
#[test]
fn a_quarter_of_a_roll_takes_a_billboards_across_onto_the_way_up() {
let view = View::look_at(Vec3::Z * 4.0, Vec3::ZERO);
let [across, up, out] =
columns(Facing::Billboard.applied(Transform::IDENTITY, view, QUARTER));
assert!(
across.abs_diff_eq(Vec3::Y, 1e-5),
"{across} is not the way the camera is up"
);
assert!(up.abs_diff_eq(Vec3::NEG_X, 1e-5), "{up} followed it around");
assert!(out.abs_diff_eq(Vec3::Z, 1e-5), "{out} left the view plane");
}
#[test]
fn a_rolled_billboard_stands_in_the_view_plane_however_far_it_is_turned() {
let view = View::look_at(Vec3::new(3.0, 4.0, -5.0), Vec3::ZERO);
let ahead = (view.target() - view.eye()).normalize();
for roll in [0.3, 2.0, -1.7, 100.0] {
let [across, up, out] =
columns(Facing::Billboard.applied(turned(), view, roll)).map(Vec3::normalize);
assert!(across.dot(up).abs() < 1e-5, "{across} leans onto {up}");
assert!(
across.dot(ahead).abs() < 1e-5 && up.dot(ahead).abs() < 1e-5,
"{across} or {up} leans out of view"
);
assert!(out.abs_diff_eq(-ahead, 1e-5), "{out} faces away");
}
}
#[test]
fn only_a_billboarded_draw_is_turned_by_the_roll_it_asks_for() {
for facing in [Facing::AsPlaced, Facing::Upright] {
assert_eq!(
facing.applied(turned(), DIVING, QUARTER),
facing.applied(turned(), DIVING, STILL),
"a turn of its own is a turn roll has no say in"
);
}
assert_ne!(
Facing::Billboard.applied(turned(), DIVING, QUARTER),
Facing::Billboard.applied(turned(), DIVING, STILL),
"where a billboarded draw leaves it free"
);
}
fn placed(instance: Instance<Cube>) -> Placement {
instance.record().placement(DIVING)
}
#[test]
fn a_draw_is_rolled_whichever_way_round_it_asked_to_be_billboarded() {
let cube = Cube.at::<()>(turned());
assert_eq!(
placed(cube.clone().roll(QUARTER).billboard()),
placed(cube.clone().billboard().roll(QUARTER))
);
assert_eq!(
placed(cube.clone()),
placed(cube.roll(QUARTER)),
"and a draw the camera never turned is left where it was"
);
}
#[test]
fn a_camera_that_looks_nowhere_leaves_a_faced_draw_where_it_was() {
let view = View::look_at(Vec3::Y, Vec3::Y);
for facing in [Facing::Billboard, Facing::Upright] {
assert_eq!(facing.applied(turned(), view, STILL), turned());
}
}
#[test]
fn the_last_facing_a_draw_asks_for_is_the_one_it_is_turned_by() {
let cube = Cube.at::<()>(turned());
assert_eq!(
placed(cube.clone().billboard().upright()),
placed(cube.clone().upright())
);
assert_eq!(
placed(cube.clone().upright().billboard()),
placed(cube.clone().billboard())
);
assert_ne!(
placed(cube.clone().upright()),
placed(cube.clone().billboard())
);
assert!(
!cube.record().faced(),
"and a draw asks for neither by default"
);
}
}