|
|
|
|
|
|
|
|
|
|
|
|
|
|
| use blade_graphics as gpu;
|
|
|
| use crate::pca::COMPONENTS;
|
|
|
|
|
| pub const MAX_EYES: usize = 2;
|
|
|
| #[repr(C)]
|
| #[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
|
| struct Params {
|
| scale: [f32; 2],
|
| offset: [f32; 2],
|
| grid: u32,
|
| pad: [u32; 3],
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
| #[derive(Clone, Copy, Debug)]
|
| pub struct EyeTransform {
|
| pub scale: [f32; 2],
|
| pub offset: [f32; 2],
|
| }
|
|
|
| impl EyeTransform {
|
|
|
|
|
|
|
| pub const FULLSCREEN: Self = Self {
|
| scale: [1.0, 1.0],
|
| offset: [0.0, 0.0],
|
| };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub fn filling_at(fov: [f32; 4], centre_tan: [f32; 2]) -> Self {
|
| let [left, right, up, down] = fov;
|
| let (tan_l, tan_r) = (left.tan(), right.tan());
|
| let (tan_u, tan_d) = (up.tan(), down.tan());
|
| let (span_x, span_y) = (tan_r - tan_l, tan_u - tan_d);
|
| Self {
|
|
|
| scale: [1.0, 1.0],
|
| offset: [
|
| (2.0 * centre_tan[0] - (tan_r + tan_l)) / span_x,
|
| (2.0 * centre_tan[1] - (tan_u + tan_d)) / span_y,
|
| ],
|
| }
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub fn filling(fov: [f32; 4]) -> Self {
|
| let [left, right, up, down] = fov;
|
| let half_tan = [
|
| (right.tan() - left.tan()) * 0.5,
|
| (up.tan() - down.tan()) * 0.5,
|
| ];
|
| Self::for_eye(fov, half_tan)
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub fn for_eye(fov: [f32; 4], half_tan: [f32; 2]) -> Self {
|
| let [left, right, up, down] = fov;
|
| let (tan_l, tan_r) = (left.tan(), right.tan());
|
| let (tan_u, tan_d) = (up.tan(), down.tan());
|
| let (span_x, span_y) = (tan_r - tan_l, tan_u - tan_d);
|
| Self {
|
| scale: [
|
| 2.0 * half_tan[0] / span_x,
|
| 2.0 * half_tan[1] / span_y,
|
| ],
|
| offset: [-(tan_r + tan_l) / span_x, -(tan_u + tan_d) / span_y],
|
| }
|
| }
|
| }
|
|
|
| #[derive(blade_macros::ShaderData)]
|
| struct ViewData {
|
| params: gpu::BufferPiece,
|
| cells: gpu::BufferPiece,
|
| }
|
|
|
|
|
| pub struct GridView {
|
| pipeline: gpu::RenderPipeline,
|
| params_buf: gpu::Buffer,
|
| cells_buf: gpu::Buffer,
|
| grid: usize,
|
| }
|
|
|
| impl GridView {
|
|
|
|
|
| pub fn new(context: &gpu::Context, color_format: gpu::TextureFormat, grid: usize) -> Self {
|
| let shader = context.create_shader(gpu::ShaderDesc {
|
| source: include_str!("shaders/grid_view.wgsl"),
|
| naga_module: None,
|
| });
|
| let data_layout = <ViewData as gpu::ShaderData>::layout();
|
| let pipeline = context.create_render_pipeline(gpu::RenderPipelineDesc {
|
| name: "grid-view",
|
| data_layouts: &[&data_layout],
|
| vertex: shader.at("vs_main"),
|
| vertex_fetches: &[],
|
| primitive: gpu::PrimitiveState::default(),
|
|
|
|
|
| depth_stencil: None,
|
| fragment: Some(shader.at("fs_main")),
|
| color_targets: &[gpu::ColorTargetState::from(color_format)],
|
| multisample_state: gpu::MultisampleState::default(),
|
| });
|
|
|
|
|
|
|
| let params_buf = context.create_buffer(gpu::BufferDesc {
|
| name: "grid-view-params",
|
| size: (std::mem::size_of::<Params>() * MAX_EYES) as u64,
|
| memory: gpu::Memory::Shared,
|
| });
|
| let cells_buf = context.create_buffer(gpu::BufferDesc {
|
| name: "grid-view-cells",
|
| size: (grid * grid * COMPONENTS * std::mem::size_of::<f32>()) as u64,
|
| memory: gpu::Memory::Shared,
|
| });
|
|
|
| let mut view = Self {
|
| pipeline,
|
| params_buf,
|
| cells_buf,
|
| grid,
|
| };
|
|
|
|
|
| for eye in 0..MAX_EYES {
|
| view.set_transform(context, eye, EyeTransform::FULLSCREEN);
|
| }
|
|
|
|
|
| view.upload(context, &vec![0.5; grid * grid * COMPONENTS]);
|
| view
|
| }
|
|
|
|
|
| pub fn upload(&mut self, context: &gpu::Context, rgb: &[f32]) {
|
| let expected = self.grid * self.grid * COMPONENTS;
|
| debug_assert_eq!(rgb.len(), expected, "grid payload is the wrong size");
|
| let n = rgb.len().min(expected);
|
| unsafe {
|
| std::ptr::copy_nonoverlapping(rgb.as_ptr(), self.cells_buf.data() as *mut f32, n);
|
| }
|
| context.sync_buffer(self.cells_buf);
|
| }
|
|
|
|
|
| pub fn set_transform(&mut self, context: &gpu::Context, eye: usize, t: EyeTransform) {
|
| debug_assert!(eye < MAX_EYES);
|
| unsafe {
|
| let slot = (self.params_buf.data() as *mut Params).add(eye);
|
| *slot = Params {
|
| scale: t.scale,
|
| offset: t.offset,
|
| grid: self.grid as u32,
|
| pad: [0; 3],
|
| };
|
| }
|
| context.sync_buffer(self.params_buf);
|
| }
|
|
|
|
|
| pub fn draw(&self, pass: &mut gpu::RenderCommandEncoder, eye: usize) {
|
| let offset = (eye.min(MAX_EYES - 1) * std::mem::size_of::<Params>()) as u64;
|
| let mut encoder = pass.with(&self.pipeline);
|
| encoder.bind(
|
| 0,
|
| &ViewData {
|
| params: self.params_buf.at(offset),
|
| cells: self.cells_buf.into(),
|
| },
|
| );
|
| encoder.draw(0, 6, 0, 1);
|
| }
|
|
|
| pub fn destroy(mut self, context: &gpu::Context) {
|
| context.destroy_buffer(self.cells_buf);
|
| context.destroy_buffer(self.params_buf);
|
| context.destroy_render_pipeline(&mut self.pipeline);
|
| }
|
| }
|
|
|
|
|
| fn rotate(q: [f32; 4], v: [f32; 3]) -> [f32; 3] {
|
| let (qx, qy, qz, qw) = (q[0], q[1], q[2], q[3]);
|
|
|
| let tx = 2.0 * (qy * v[2] - qz * v[1]);
|
| let ty = 2.0 * (qz * v[0] - qx * v[2]);
|
| let tz = 2.0 * (qx * v[1] - qy * v[0]);
|
| [
|
| v[0] + qw * tx + qy * tz - qz * ty,
|
| v[1] + qw * ty + qz * tx - qx * tz,
|
| v[2] + qw * tz + qx * ty - qy * tx,
|
| ]
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| pub fn reprojection_offset(then: [f32; 4], now: [f32; 4]) -> Option<[f32; 2]> {
|
|
|
|
|
| let forward = rotate(then, [0.0, 0.0, -1.0]);
|
| let inverse_now = [-now[0], -now[1], -now[2], now[3]];
|
| let v = rotate(inverse_now, forward);
|
| if v[2] >= -1e-3 {
|
| return None;
|
| }
|
| Some([v[0] / -v[2], v[1] / -v[2]])
|
| }
|
|
|
| #[cfg(test)]
|
| mod tests {
|
| use super::*;
|
|
|
|
|
| #[test]
|
| fn no_motion_means_no_shift() {
|
| let identity = [0.0, 0.0, 0.0, 1.0];
|
| let offset = reprojection_offset(identity, identity).unwrap();
|
| assert!(offset[0].abs() < 1e-6 && offset[1].abs() < 1e-6);
|
| }
|
|
|
|
|
|
|
| #[test]
|
| fn turning_right_pushes_content_left() {
|
| let identity = [0.0, 0.0, 0.0, 1.0];
|
|
|
|
|
| let half = (-15.0f32).to_radians() * 0.5;
|
| let turned_right = [0.0, half.sin(), 0.0, half.cos()];
|
| let offset = reprojection_offset(identity, turned_right).unwrap();
|
| assert!(
|
| offset[0] < -0.1,
|
| "expected the image to move left, got {offset:?}"
|
| );
|
|
|
| assert!((offset[0] + 15.0f32.to_radians().tan()).abs() < 1e-3);
|
| assert!(offset[1].abs() < 1e-6, "yaw should not shift vertically");
|
| }
|
|
|
|
|
| #[test]
|
| fn looking_up_pushes_content_down() {
|
| let identity = [0.0, 0.0, 0.0, 1.0];
|
| let half = 10.0f32.to_radians() * 0.5;
|
| let looked_up = [half.sin(), 0.0, 0.0, half.cos()];
|
| let offset = reprojection_offset(identity, looked_up).unwrap();
|
| assert!(offset[1] < -0.1, "expected downward shift, got {offset:?}");
|
| assert!(offset[0].abs() < 1e-6);
|
| }
|
|
|
|
|
| #[test]
|
| fn facing_away_has_no_offset() {
|
| let identity = [0.0, 0.0, 0.0, 1.0];
|
| let behind = [0.0, 1.0, 0.0, 0.0];
|
| assert!(reprojection_offset(identity, behind).is_none());
|
| }
|
| }
|
|
|