use crate::components::MaterialType;
use gizmo_math::{Mat4, Vec3, Vec4};
pub const BACKDROP_NDC_DEPTH: f32 = 0.99999;
#[inline]
pub fn is_camera_locked(material_type: MaterialType) -> bool {
matches!(material_type, MaterialType::Backdrop)
}
#[inline]
pub fn is_backdrop(material_type: MaterialType) -> bool {
matches!(material_type, MaterialType::Backdrop | MaterialType::BackdropPlaced)
}
#[inline]
pub fn instance_model(material_type: MaterialType, model: &Mat4, camera_pos: Vec3) -> Mat4 {
if matches!(material_type, MaterialType::BackdropPlaced) {
Mat4::from_translation(-camera_pos) * *model
} else {
*model
}
}
#[inline]
pub fn camera_locked_model(material_type: MaterialType, model: &Mat4, camera_pos: Vec3) -> Mat4 {
if is_camera_locked(material_type) {
Mat4::from_translation(camera_pos) * *model
} else {
*model
}
}
pub fn backdrop_clip_position(
view_proj: &Mat4,
camera_pos: Vec3,
model: &Mat4,
local: Vec3,
) -> Vec4 {
let world = model.transform_point3(local) + camera_pos;
let mut clip = *view_proj * world.extend(1.0);
clip.z = clip.w * BACKDROP_NDC_DEPTH;
clip
}
pub fn backdrop_rgba(vertex_colour: Vec4, instance_albedo: Vec4, texel: Vec4) -> Vec4 {
vertex_colour * instance_albedo * texel
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct BackdropState {
pub(crate) blend: Option<wgpu::BlendState>,
pub(crate) depth_write: bool,
pub(crate) cull: Option<wgpu::Face>,
}
pub(crate) fn backdrop_state() -> BackdropState {
BackdropState {
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
depth_write: false,
cull: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
const SRC: &str = include_str!("shaders/backdrop.wgsl");
fn view_proj(eye: Vec3, forward: Vec3) -> Mat4 {
let view = Mat4::look_at_rh(eye, eye + forward, Vec3::Y);
let proj = Mat4::perspective_rh(std::f32::consts::FRAC_PI_4, 16.0 / 9.0, 0.1, 2000.0);
proj * view
}
fn ndc(clip: Vec4) -> Vec3 {
(clip / clip.w).truncate()
}
fn unlocked_clip(vp: &Mat4, model: &Mat4, local: Vec3) -> Vec4 {
*vp * model.transform_point3(local).extend(1.0)
}
#[test]
fn a_placed_backdrop_lands_where_it_was_authored() {
let authored = Mat4::from_translation(Vec3::new(-1200.0, 40.0, 800.0));
let local = Vec3::new(3.0, -2.0, 11.0);
let want = authored.transform_point3(local);
for eye in [Vec3::ZERO, Vec3::new(800.0, 12.0, -450.0), Vec3::new(-3000.0, 0.0, 3000.0)] {
let upload = instance_model(MaterialType::BackdropPlaced, &authored, eye);
let world = upload.transform_point3(local) + eye;
assert!(
(world - want).length() < 1e-3,
"placed backdrop moved with the camera at {eye:?}: {world:?} != {want:?}"
);
}
}
#[test]
fn a_locked_backdrop_still_follows_the_camera_and_a_placed_one_does_not() {
let authored = Mat4::from_translation(Vec3::new(0.0, 0.0, -50.0));
let local = Vec3::ZERO;
let a = Vec3::ZERO;
let b = Vec3::new(500.0, 0.0, 0.0);
let locked = |eye: Vec3| {
instance_model(MaterialType::Backdrop, &authored, eye).transform_point3(local) + eye
};
let placed = |eye: Vec3| {
instance_model(MaterialType::BackdropPlaced, &authored, eye).transform_point3(local)
+ eye
};
assert!((locked(b) - locked(a)).length() > 499.0, "the lock stopped locking");
assert!((placed(b) - placed(a)).length() < 1e-3, "the placed variant is following");
assert_eq!(camera_locked_model(MaterialType::BackdropPlaced, &authored, b), authored);
assert_ne!(camera_locked_model(MaterialType::Backdrop, &authored, b), authored);
assert!(is_backdrop(MaterialType::BackdropPlaced));
assert!(!is_camera_locked(MaterialType::BackdropPlaced));
}
#[test]
fn camera_translation_does_not_move_the_backdrop() {
let fwd = Vec3::new(0.0, -0.1, -1.0).normalize();
let model = Mat4::from_translation(Vec3::new(0.0, 40.0, -300.0));
let local = Vec3::new(12.0, 3.0, -1.0);
let near_origin = view_proj(Vec3::new(0.0, 1.6, 0.0), fwd);
let far_away = view_proj(Vec3::new(-620.0, 9.0, 700.0), fwd);
let a = ndc(backdrop_clip_position(&near_origin, Vec3::new(0.0, 1.6, 0.0), &model, local));
let b = ndc(backdrop_clip_position(&far_away, Vec3::new(-620.0, 9.0, 700.0), &model, local));
assert!(
(a - b).length() < 1e-4,
"the backdrop moved when the camera translated: {a:?} vs {b:?}"
);
let ua = ndc(unlocked_clip(&near_origin, &model, local));
let ub = ndc(unlocked_clip(&far_away, &model, local));
assert!(
(ua - ub).length() > 0.5,
"premise broken: an unlocked panel is supposed to slide with the camera ({ua:?} vs {ub:?})"
);
}
#[test]
fn camera_rotation_still_moves_the_backdrop() {
let eye = Vec3::new(5.0, 2.0, -3.0);
let model = Mat4::IDENTITY;
let local = Vec3::new(0.0, 20.0, -200.0);
let ahead = ndc(backdrop_clip_position(
&view_proj(eye, Vec3::NEG_Z),
eye,
&model,
local,
));
let turned = ndc(backdrop_clip_position(
&view_proj(eye, Vec3::new(-0.6, 0.0, -0.8).normalize()),
eye,
&model,
local,
));
assert!(
(ahead - turned).length() > 0.1,
"turning the camera left the backdrop where it was — the lock swallowed the \
rotation too, which paints the sky onto the lens: {ahead:?} vs {turned:?}"
);
}
#[test]
fn the_lock_is_precisely_the_view_translation_removed() {
let eye = Vec3::new(133.0, 8.0, -77.0);
let fwd = Vec3::new(0.3, -0.2, -1.0).normalize();
let vp = view_proj(eye, fwd);
let d = Vec3::new(-30.0, 15.0, -250.0);
let locked = backdrop_clip_position(&vp, eye, &Mat4::IDENTITY, d);
let plain = unlocked_clip(&vp, &Mat4::IDENTITY, eye + d);
assert!(
(ndc(locked).truncate() - ndc(plain).truncate()).length() < 1e-4,
"camera-locked (x) and world-space (camera + x) must land on the same pixel"
);
assert!((locked.w - plain.w).abs() < 1e-3, "the lock perturbed clip w");
}
#[test]
fn a_near_backdrop_panel_loses_the_depth_test_to_a_far_wall() {
let eye = Vec3::new(0.0, 1.5, 0.0);
let vp = view_proj(eye, Vec3::NEG_Z);
let panel_local = Vec3::new(0.0, 0.0, -5.0);
let wall_world = Vec3::new(0.0, 1.5, -50.0);
let wall_z = ndc(unlocked_clip(&vp, &Mat4::IDENTITY, wall_world)).z;
let panel_z = ndc(backdrop_clip_position(&vp, eye, &Mat4::IDENTITY, panel_local)).z;
assert!(
wall_z < panel_z,
"the wall must beat the backdrop (wall {wall_z}, backdrop {panel_z})"
);
let unpinned_z = ndc(unlocked_clip(&vp, &Mat4::IDENTITY, eye + panel_local)).z;
assert!(
unpinned_z < wall_z,
"premise broken: an unpinned 5 m panel is supposed to occlude a 50 m wall \
({unpinned_z} vs {wall_z})"
);
}
#[test]
fn backdrop_depth_is_pinned_and_inside_the_clip_volume() {
let eye = Vec3::new(-4.0, 12.0, 60.0);
let vp = view_proj(eye, Vec3::NEG_Z);
for local in [
Vec3::new(0.0, 0.0, -0.5),
Vec3::new(0.0, 0.0, -900.0),
Vec3::new(400.0, 200.0, -1800.0),
] {
let clip = backdrop_clip_position(&vp, eye, &Mat4::IDENTITY, local);
let z = clip.z / clip.w;
assert!(
(z - BACKDROP_NDC_DEPTH).abs() < 1e-5,
"backdrop vertex {local:?} landed at depth {z}, not the pinned {BACKDROP_NDC_DEPTH}"
);
assert!(z < 1.0, "a pinned depth of {z} is on or past the far plane and may be clipped");
}
}
#[test]
fn backdrop_pipeline_state_holds_the_no_depth_write_property() {
let s = backdrop_state();
assert!(
!s.depth_write,
"a backdrop that writes depth occludes the world it is a backdrop FOR — this is \
the property the whole material type exists for"
);
assert_eq!(
s.blend,
Some(wgpu::BlendState::ALPHA_BLENDING),
"with `None` the fragment alpha is computed and then discarded, so a cut-out \
panorama edge draws as a hard rectangle"
);
assert_eq!(s.cull, None, "a backdrop panel must be visible from whichever side faces the camera");
}
#[test]
fn every_factor_of_the_mesh_reaches_the_pixel() {
let white = Vec4::ONE;
let tex = Vec4::new(0.8, 0.4, 0.2, 1.0);
let vcol = Vec4::new(0.5, 1.0, 1.0, 1.0);
let tint = Vec4::new(1.0, 1.0, 0.25, 1.0);
assert_eq!(backdrop_rgba(white, white, tex), tex);
assert_eq!(backdrop_rgba(vcol, white, white), vcol);
let all = backdrop_rgba(vcol, tint, tex);
assert_eq!(all, Vec4::new(0.5 * 0.8, 0.4, 0.25 * 0.2, 1.0));
let cutout = backdrop_rgba(white, white, Vec4::new(1.0, 1.0, 1.0, 0.0));
assert_eq!(cutout.w, 0.0, "texture alpha must reach the fragment's alpha");
let faded = backdrop_rgba(Vec4::new(1.0, 1.0, 1.0, 0.5), white, white);
assert_eq!(faded.w, 0.5, "vertex alpha must reach the fragment's alpha");
}
#[test]
fn a_black_vertex_colour_is_taken_at_face_value() {
let black = Vec4::new(0.0, 0.0, 0.0, 1.0);
let out = backdrop_rgba(black, Vec4::ONE, Vec4::ONE);
assert_eq!(out.truncate(), Vec3::ZERO, "a black-painted panel came out lit");
}
#[test]
fn the_shader_still_computes_the_mirrored_expressions() {
assert!(
SRC.contains("let world = local + scene.camera_pos.xyz;"),
"backdrop.wgsl no longer camera-locks the way `backdrop_clip_position` mirrors"
);
assert!(
SRC.contains("clip.z = clip.w * BACKDROP_NDC_DEPTH;"),
"backdrop.wgsl no longer pins depth to the far plane"
);
assert!(
SRC.contains(&format!("const BACKDROP_NDC_DEPTH: f32 = {BACKDROP_NDC_DEPTH};")),
"backdrop.wgsl's far-plane constant drifted from `BACKDROP_NDC_DEPTH`"
);
assert!(
SRC.contains("let rgb = in.color.rgb * in.inst_albedo.rgb * tex.rgb;")
&& SRC.contains("let alpha = in.color.a * in.inst_albedo.a * tex.a;"),
"backdrop.wgsl's fragment no longer matches `backdrop_rgba`"
);
}
#[test]
fn the_shader_samples_the_mesh_texture_and_vertex_colour() {
assert!(
SRC.contains("textureSample(t_diffuse, s_diffuse, in.tex_coords)"),
"a backdrop that does not sample its texture is just the skybox again"
);
assert!(
SRC.contains("@location(1) color: vec4<f32>"),
"backdrop.wgsl stopped taking the vertex colour (with its alpha) as an input"
);
assert!(
!SRC.contains("length(in.color") && !SRC.contains("length(v_color"),
"the vertex colour is being second-guessed again — see `baked_lit.wgsl`"
);
assert!(
!SRC.contains("scene.sun_color"),
"a painted backdrop must not tint itself from the sun — that is `sky.wgsl`"
);
}
#[test]
fn only_backdrops_are_camera_locked() {
let cam = Vec3::new(800.0, 5.0, -200.0);
let model = Mat4::from_translation(Vec3::new(0.0, 30.0, 0.0));
let locked = camera_locked_model(MaterialType::Backdrop, &model, cam);
assert!(
(locked.transform_point3(Vec3::ZERO) - (cam + Vec3::new(0.0, 30.0, 0.0))).length() < 1e-3,
"a camera-locked model must resolve to the camera plus the authored offset"
);
let vp = view_proj(cam, Vec3::NEG_Z);
let via_model = ndc(vp * locked.transform_point3(Vec3::new(1.0, 2.0, -60.0)).extend(1.0));
let via_shader = ndc(backdrop_clip_position(&vp, cam, &model, Vec3::new(1.0, 2.0, -60.0)));
assert!(
(via_model.truncate() - via_shader.truncate()).length() < 1e-4,
"`camera_locked_model` disagrees with the vertex shader about where the backdrop is"
);
for mt in [
MaterialType::Pbr,
MaterialType::Unlit,
MaterialType::BakedLit,
MaterialType::Skybox,
MaterialType::Water,
MaterialType::Grid,
] {
assert_eq!(
camera_locked_model(mt, &model, cam).to_cols_array(),
model.to_cols_array(),
"{mt:?} must not be camera-locked"
);
assert!(!is_camera_locked(mt), "{mt:?} must not be camera-locked");
}
assert!(is_camera_locked(MaterialType::Backdrop));
}
}