use std::rc::Rc;
use arrayvec::ArrayVec;
use unlit_ecs::Entity;
use unlit_wgpu::mesh::JointMatrix;
use unlit_wgpu::offset_allocator::Allocation;
use unlit_wgpu::render_attachments::{color_clear, depth_clear, stencil_clear};
use unlit_wgpu::resources::{ResHandle, Virtual};
use unlit_wgpu::scene::MAX_VERTEX_BUFFERS;
use unlit_wgpu::specialize::VertexLayout;
use crate::bounds::Aabb;
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "reflect", derive(facet::Facet))]
pub struct Transform {
#[cfg_attr(feature = "reflect", facet(opaque, proxy = crate::reflect::Vec3Proxy))]
pub translation: glam::Vec3,
#[cfg_attr(feature = "reflect", facet(opaque, proxy = crate::reflect::QuatProxy))]
pub rotation: glam::Quat,
#[cfg_attr(feature = "reflect", facet(opaque, proxy = crate::reflect::Vec3Proxy))]
pub scale: glam::Vec3,
}
impl Default for Transform {
fn default() -> Self {
Self {
translation: glam::Vec3::ZERO,
rotation: glam::Quat::IDENTITY,
scale: glam::Vec3::ONE,
}
}
}
impl Transform {
#[must_use]
pub fn compute_matrix(&self) -> glam::Affine3A {
glam::Affine3A::from_scale_rotation_translation(self.scale, self.rotation, self.translation)
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "reflect", derive(facet::Facet))]
pub struct Camera {
#[cfg_attr(feature = "reflect", facet(opaque, proxy = crate::reflect::Mat4Proxy))]
pub view_from_world: glam::Mat4,
#[cfg_attr(feature = "reflect", facet(opaque, proxy = crate::reflect::Mat4Proxy))]
pub clip_from_view: glam::Mat4,
pub active: bool,
}
impl Camera {
#[must_use]
pub fn clip_from_world(&self) -> glam::Mat4 {
self.clip_from_view * self.view_from_world
}
#[must_use]
pub fn position(&self) -> glam::Vec3 {
self.view_from_world.inverse().col(3).truncate()
}
#[must_use]
pub fn view_depth(&self, world_point: glam::Vec3) -> f32 {
-self.view_from_world.transform_point3(world_point).z
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "reflect", derive(facet::Facet))]
pub struct RenderLoadOps {
#[cfg_attr(
feature = "reflect",
facet(opaque, proxy = crate::reflect::ColorLoadOpProxy)
)]
pub color: wgpu::LoadOp<wgpu::Color>,
#[cfg_attr(
feature = "reflect",
facet(opaque, proxy = crate::reflect::DepthLoadOpProxy)
)]
pub depth: wgpu::LoadOp<f32>,
#[cfg_attr(
feature = "reflect",
facet(opaque, proxy = crate::reflect::StencilLoadOpProxy)
)]
pub stencil: wgpu::LoadOp<u32>,
}
impl Default for RenderLoadOps {
fn default() -> Self {
Self {
color: color_clear(),
depth: depth_clear(),
stencil: stencil_clear(),
}
}
}
#[derive(Clone, Debug)]
pub struct MeshParts {
pub root: ResHandle<Virtual>,
pub vertex_buffers: ArrayVec<(u32, ResHandle<wgpu::Buffer>), MAX_VERTEX_BUFFERS>,
pub index_buffer: Option<(ResHandle<wgpu::Buffer>, wgpu::IndexFormat)>,
pub bind_group_id: Option<ResHandle<wgpu::BindGroup>>,
pub(crate) vertex_allocation: Option<Allocation>,
pub(crate) index_allocation: Option<Allocation>,
pub(crate) morph_deltas_allocation: Option<Allocation>,
pub metadata_index: u32,
}
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "reflect",
derive(facet::Facet),
facet(opaque, proxy = crate::reflect::GpuMeshProxy)
)]
pub struct GpuMesh {
pub parts: Rc<MeshParts>,
pub vertex_layout: VertexLayout,
pub count: u32,
pub first: u32,
pub base_vertex: u32,
pub indexed: bool,
pub aabb: Aabb,
pub morph_targets: u32,
pub skinned: bool,
}
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "reflect", derive(facet::Facet))]
pub struct SkinPose {
#[cfg_attr(feature = "reflect", facet(opaque, proxy = crate::reflect::Mat4VecProxy))]
pub matrices: Vec<JointMatrix>,
}
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "reflect", derive(facet::Facet))]
pub struct MorphWeights {
pub weights: Vec<f32>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "reflect", derive(facet::Facet))]
pub struct SkinBinding {
#[cfg_attr(feature = "reflect", facet(opaque, proxy = unlit_ecs::EntityProxy))]
pub pose: Entity,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "reflect", derive(facet::Facet))]
pub struct MorphBinding {
#[cfg_attr(feature = "reflect", facet(opaque, proxy = unlit_ecs::EntityProxy))]
pub weights: Entity,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(
feature = "reflect",
derive(facet::Facet),
facet(opaque, proxy = crate::reflect::GpuRenderPipelineProxy)
)]
pub struct GpuRenderPipeline<Key> {
pub(crate) key: Key,
}
impl<Key> GpuRenderPipeline<Key> {
pub fn new(key: Key) -> Self {
Self { key }
}
pub(crate) fn key(&self) -> &Key {
&self.key
}
}
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "reflect",
derive(facet::Facet),
facet(opaque, proxy = crate::reflect::GpuMaterialProxy)
)]
pub struct GpuMaterial {
pub bind_group_id: ResHandle<wgpu::BindGroup>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "reflect", derive(facet::Facet))]
pub struct ZSortedDrawing;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "reflect", derive(facet::Facet))]
pub struct InstanceVisibility {
pub visible: bool,
}
impl Default for InstanceVisibility {
fn default() -> Self {
Self { visible: true }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_transform_is_identity() {
let t = Transform::default();
assert_eq!(t.compute_matrix(), glam::Affine3A::IDENTITY);
}
#[test]
fn transform_matrix_combines_components() {
let t = Transform {
translation: glam::Vec3::new(1.0, 2.0, 3.0),
rotation: glam::Quat::from_rotation_y(1.5),
scale: glam::Vec3::splat(2.0),
};
let expected = glam::Affine3A::from_scale_rotation_translation(
glam::Vec3::splat(2.0),
glam::Quat::from_rotation_y(1.5),
glam::Vec3::new(1.0, 2.0, 3.0),
);
assert_eq!(t.compute_matrix(), expected);
}
#[test]
fn camera_fields_round_trip() {
let eye = glam::Vec3::new(0.0, 5.0, 10.0);
let view = glam::camera::rh::view::look_at_mat4(eye, glam::Vec3::ZERO, glam::Vec3::Y);
let c = Camera {
view_from_world: view,
clip_from_view: glam::Mat4::IDENTITY,
active: true,
};
assert_eq!(c.position(), eye);
assert_eq!(c.clip_from_world(), view);
assert!(c.active);
}
fn test_camera() -> Camera {
let projection = glam::camera::rh::proj::opengl::perspective(1.0, 1.0, 0.1, 100.0);
let view = glam::camera::rh::view::look_at_mat4(
glam::Vec3::new(0.0, 0.0, 5.0),
glam::Vec3::ZERO,
glam::Vec3::Y,
);
Camera {
view_from_world: view,
clip_from_view: projection,
active: true,
}
}
#[test]
fn view_depth_grows_away_from_the_camera() {
let camera = test_camera();
assert!((camera.view_depth(glam::Vec3::ZERO) - 5.0).abs() < 1e-4);
assert!((camera.view_depth(glam::Vec3::new(0.0, 0.0, 4.0)) - 1.0).abs() < 1e-4);
}
#[test]
fn view_depth_ignores_lateral_offset() {
let camera = test_camera();
let ahead = camera.view_depth(glam::Vec3::new(0.0, 0.0, 1.0));
let sideways = camera.view_depth(glam::Vec3::new(3.0, 0.0, 1.0));
assert!(
(ahead - sideways).abs() < 1e-4,
"a lateral offset changed the depth: {ahead} against {sideways}",
);
}
fn test_orthographic_camera() -> Camera {
let projection =
glam::camera::rh::proj::directx::orthographic(-1.0, 1.0, -1.0, 1.0, 0.1, 100.0);
let view = glam::camera::rh::view::look_at_mat4(
glam::Vec3::new(0.0, 0.0, 5.0),
glam::Vec3::ZERO,
glam::Vec3::Y,
);
Camera {
view_from_world: view,
clip_from_view: projection,
active: true,
}
}
#[test]
fn orthographic_view_depth_grows_away_from_the_camera() {
let camera = test_orthographic_camera();
assert!((camera.view_depth(glam::Vec3::ZERO) - 5.0).abs() < 1e-4);
assert!((camera.view_depth(glam::Vec3::new(0.0, 0.0, 4.0)) - 1.0).abs() < 1e-4);
}
#[test]
fn orthographic_view_depth_ignores_lateral_offset() {
let camera = test_orthographic_camera();
let ahead = camera.view_depth(glam::Vec3::new(0.0, 0.0, 1.0));
let sideways = camera.view_depth(glam::Vec3::new(3.0, 0.0, 1.0));
assert!(
(ahead - sideways).abs() < 1e-4,
"a lateral offset changed the depth: {ahead} against {sideways}",
);
}
#[test]
fn view_depth_follows_an_oblique_camera() {
let eye = glam::Vec3::new(3.0, 4.0, 5.0);
let view = glam::camera::rh::view::look_at_mat4(eye, glam::Vec3::ZERO, glam::Vec3::Y);
let projection =
glam::camera::rh::proj::directx::perspective_infinite_reverse(1.0, 1.0, 0.1);
let camera = Camera {
view_from_world: view,
clip_from_view: projection,
active: true,
};
let expected = eye.length();
assert!(
(camera.view_depth(glam::Vec3::ZERO) - expected).abs() < 1e-3,
"the depth of the target is not the eye distance: {} against {expected}",
camera.view_depth(glam::Vec3::ZERO),
);
let forward = (-eye).normalize();
let one_deeper = camera.view_depth(forward) - camera.view_depth(glam::Vec3::ZERO);
assert!(
(one_deeper - 1.0).abs() < 1e-3,
"one unit along the view axis is not one unit deeper: {one_deeper}",
);
}
fn camera_with(projection: glam::Mat4, eye: glam::Vec3, target: glam::Vec3) -> Camera {
Camera {
view_from_world: glam::camera::rh::view::look_at_mat4(eye, target, glam::Vec3::Y),
clip_from_view: projection,
active: true,
}
}
#[test]
fn view_depth_matches_the_view_axis_for_every_projection() {
let eye = glam::Vec3::new(3.0, 4.0, 5.0);
let target = glam::Vec3::new(-1.0, 0.5, 1.0);
let view = glam::camera::rh::view::look_at_mat4(eye, target, glam::Vec3::Y);
let forward = -view.row(2).truncate().normalize();
let projections = [
(
"perspective",
glam::camera::rh::proj::opengl::perspective(1.0, 1.5, 0.1, 100.0),
),
(
"infinite reverse",
glam::camera::rh::proj::directx::perspective_infinite_reverse(1.0, 1.5, 0.1),
),
(
"orthographic",
glam::camera::rh::proj::directx::orthographic(-2.0, 2.0, -1.0, 1.0, 0.1, 100.0),
),
(
"off-axis frustum",
glam::camera::rh::proj::opengl::frustum(-0.4, 1.6, -0.3, 0.9, 0.1, 100.0),
),
(
"off-axis orthographic",
glam::camera::rh::proj::opengl::orthographic(-0.5, 2.0, -0.4, 1.2, 0.1, 100.0),
),
];
for (name, projection) in projections {
let camera = camera_with(projection, eye, target);
if name == "off-axis frustum" {
let combined = camera.clip_from_world();
let cross = combined
.row(0)
.truncate()
.cross(combined.row(1).truncate())
.normalize();
assert!(
cross.dot(forward).abs() < 1.0 - 1e-3,
"the cross product was on the view axis, so this case tests nothing",
);
}
for point in [
target,
eye + forward * 4.0,
eye + forward * 9.0 + glam::Vec3::new(2.0, -1.0, 0.0),
] {
let expected = forward.dot(point - eye);
let actual = camera.view_depth(point);
assert!(
(actual - expected).abs() < 1e-3,
"{name}: view_depth({point:?}) was {actual}, expected {expected}",
);
}
}
}
}
#[cfg(test)]
mod size_tests {
use super::*;
#[test]
fn a_mesh_cell_stays_small_beside_the_parts_it_excludes() {
let cell = size_of::<core::cell::RefCell<GpuMesh>>();
let parts = size_of::<MeshParts>();
assert!(
cell * 2 <= parts,
"a mesh cell is {cell} bytes against {parts} of parts; every per-entity \
walk pays for the cell, so move per-mesh fields into MeshParts",
);
}
}