use glam::{Mat4, Vec3, Vec4};
use super::Camera3D;
use crate::{Ctx, Texture};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TargetCamera {
pub camera: Camera3D,
pub target: Texture,
pub clip_plane: Option<Vec4>,
}
impl TargetCamera {
#[must_use]
pub fn new(camera: Camera3D, target: Texture) -> Self {
Self {
camera,
target,
clip_plane: None,
}
}
#[must_use]
pub fn reflection(camera: Camera3D, target: Texture, level: f32) -> Self {
let mut mirrored = camera;
mirrored.position.y = 2.0 * level - camera.position.y;
let forward = camera.forward() * Vec3::new(1.0, -1.0, 1.0);
mirrored.rotation = super::look_rotation(forward);
Self {
camera: mirrored,
target,
clip_plane: Some(Vec4::new(0.0, 1.0, 0.0, -level)),
}
}
}
pub(crate) fn oblique(view_proj: Mat4, view: Mat4, plane: Vec4) -> Mat4 {
let c = view.inverse().transpose() * plane;
let c = c.normalize_or_zero();
if c.w >= 0.0 || c == Vec4::ZERO {
return view_proj;
}
let projection = view_proj * view.inverse();
let clip_plane = projection.inverse().transpose() * c;
let q = projection.inverse() * Vec4::new(clip_plane.x.signum(), clip_plane.y.signum(), 1.0, 1.0);
let z = c / c.dot(q);
let mut rows = projection.transpose().to_cols_array_2d();
rows[2] = z.to_array();
Mat4::from_cols_array_2d(&rows).transpose() * view
}
#[cfg(test)]
fn kept(plane: Vec4, p: Vec3) -> bool {
use glam::Vec4Swizzles;
plane.xyz().dot(p) + plane.w >= 0.0
}
impl Ctx {
pub fn create_render_target(&mut self, width: u32, height: u32) -> Texture {
self.try_create_render_target(width, height)
.unwrap_or_else(|e| panic!("ponko: {e}"))
}
#[doc(hidden)]
pub fn try_create_render_target(&mut self, width: u32, height: u32) -> Result<Texture, String> {
if width == 0 || height == 0 {
return Err(format!("render target size must not be zero (got {width} x {height})"));
}
let texture = if self.headless {
self.headless_textures += 1;
Texture::new(self.headless_textures, width, height)
} else {
let renderer = self.renderer();
let max = renderer.max_texture_size();
if width > max || height > max {
return Err(format!(
"{width} x {height} is larger than this GPU's maximum texture size ({max})"
));
}
Texture::new(renderer.create_render_target(width, height), width, height)
};
self.assets3d.render_targets.push(texture);
Ok(texture)
}
#[must_use]
pub fn is_render_target(&self, texture: Texture) -> bool {
self.assets3d.render_targets.contains(&texture)
}
}
#[cfg(test)]
mod tests {
use super::*;
use glam::{Quat, Vec2};
fn camera() -> Camera3D {
let mut c = Camera3D::new(Vec2::new(800.0, 600.0));
c.position = Vec3::new(0.0, -4.0, 10.0);
c.rotation = super::super::look_rotation(Vec3::new(0.0, 0.5, -1.0));
c
}
fn view(c: &Camera3D) -> Mat4 {
Mat4::from_rotation_translation(c.rotation, c.position).inverse()
}
#[test]
fn the_oblique_near_plane_is_the_clip_plane() {
let c = camera();
let plane = Vec4::new(0.0, 1.0, 0.0, -1.0); let m = oblique(c.view_proj(), view(&c), plane);
let depth = |p: Vec3| {
let clip = m * p.extend(1.0);
clip.z / clip.w
};
for p in [
Vec3::new(0.0, 1.5, 0.0),
Vec3::new(3.0, 4.0, -20.0),
Vec3::new(-2.0, 2.0, 5.0),
] {
assert!(kept(plane, p));
let d = depth(p);
assert!((0.0..=1.0).contains(&d), "{p} kept, depth {d}");
}
for p in [Vec3::new(0.0, 0.5, 0.0), Vec3::new(2.0, -1.0, -10.0)] {
assert!(depth(p) < 0.0, "{p} is below the plane");
}
assert!(depth(Vec3::new(0.0, 1.0, -3.0)).abs() < 1e-4);
}
#[test]
fn a_plane_behind_the_camera_changes_nothing() {
let c = camera();
let plane = Vec4::new(0.0, 1.0, 0.0, 10.0); assert_eq!(oblique(c.view_proj(), view(&c), plane), c.view_proj());
}
#[test]
fn reflections_mirror_the_camera_in_the_level() {
let mut c = Camera3D::new(Vec2::new(800.0, 600.0));
c.position = Vec3::new(1.0, 6.0, 2.0);
c.rotation = Quat::from_rotation_y(0.7) * Quat::from_rotation_x(-0.4);
let target = Texture::new(1, 8, 8);
let r = TargetCamera::reflection(c, target, 2.0);
assert_eq!(r.camera.position, Vec3::new(1.0, -2.0, 2.0));
let (f, g) = (c.forward(), r.camera.forward());
assert!((g - Vec3::new(f.x, -f.y, f.z)).length() < 1e-5);
assert_eq!(r.clip_plane, Some(Vec4::new(0.0, 1.0, 0.0, -2.0)));
}
}