Fixed transformations

master
Mark 2023-12-22 19:18:03 -08:00
parent 6e13c91d37
commit 13f74b9d85
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
6 changed files with 88 additions and 49 deletions

View File

@ -8,12 +8,13 @@ pub struct Doodad {
} }
impl Spriteable for Doodad { impl Spriteable for Doodad {
fn sprite(&self, camera: &Camera) -> Sprite { fn sprite(&self, camera: Camera) -> Sprite {
return Sprite { return Sprite {
position: self.pos, position: self.pos,
camera: camera.pos, camera: camera,
name: self.sprite.clone(), name: self.sprite.clone(),
angle: Deg { 0: 0.0 }, angle: Deg { 0: 0.0 },
scale: 1.0,
}; };
} }
} }

View File

@ -1,7 +1,10 @@
use anyhow::Result; use anyhow::Result;
use cgmath::{Deg, Point2}; use cgmath::{Deg, Point2};
use winit::{ use winit::{
event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, event::{
ElementState, Event, KeyboardInput, MouseButton, MouseScrollDelta, TouchPhase,
VirtualKeyCode, WindowEvent,
},
event_loop::{ControlFlow, EventLoop}, event_loop::{ControlFlow, EventLoop},
window::WindowBuilder, window::WindowBuilder,
}; };
@ -22,12 +25,18 @@ use crate::{
system::System, system::System,
}; };
#[derive(Debug, Clone, Copy)]
struct Camera { struct Camera {
// Camera center
pos: Point2<Pfloat>, pos: Point2<Pfloat>,
// Camera zoom
// (How many game units tall is the viewport?)
zoom: Pfloat,
} }
trait Spriteable { trait Spriteable {
fn sprite(&self, camera: &Camera) -> Sprite; fn sprite(&self, camera: Camera) -> Sprite;
} }
struct Sprite { struct Sprite {
@ -37,12 +46,14 @@ struct Sprite {
// This object's position, in world coordinates. // This object's position, in world coordinates.
position: Point2<Pfloat>, position: Point2<Pfloat>,
scale: Pfloat,
// This sprite's rotation // This sprite's rotation
// (relative to north, measured ccw) // (relative to north, measured ccw)
angle: Deg<Pfloat>, angle: Deg<Pfloat>,
// The camera we want to draw this sprite from, in world coordinates // The camera we want to draw this sprite from
camera: Point2<Pfloat>, camera: Camera,
} }
struct Game { struct Game {
@ -61,6 +72,7 @@ impl Game {
player: Ship::new(ShipKind::Gypsum, (0.0, 0.0).into()), player: Ship::new(ShipKind::Gypsum, (0.0, 0.0).into()),
camera: Camera { camera: Camera {
pos: (0.0, 0.0).into(), pos: (0.0, 0.0).into(),
zoom: 500.0,
}, },
system: System::new(), system: System::new(),
} }
@ -95,8 +107,8 @@ impl Game {
fn sprites(&self) -> Vec<Sprite> { fn sprites(&self) -> Vec<Sprite> {
let mut sprites: Vec<Sprite> = Vec::new(); let mut sprites: Vec<Sprite> = Vec::new();
sprites.append(&mut self.system.sprites(&self.camera)); sprites.append(&mut self.system.sprites(self.camera));
sprites.push(self.player.sprite(&self.camera)); sprites.push(self.player.sprite(self.camera));
return sprites; return sprites;
} }

View File

@ -25,14 +25,6 @@ pub struct GPUState {
instance_buffer: wgpu::Buffer, instance_buffer: wgpu::Buffer,
} }
#[rustfmt::skip]
const OPENGL_TO_WGPU_MATRIX: Matrix4<f32> = Matrix4::new(
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 0.5, 0.5,
0.0, 0.0, 0.0, 1.0,
);
struct Instance { struct Instance {
transform: Transform, transform: Transform,
texture_index: u32, texture_index: u32,
@ -40,7 +32,7 @@ struct Instance {
impl Instance { impl Instance {
fn to_raw(&self) -> InstanceRaw { fn to_raw(&self) -> InstanceRaw {
InstanceRaw { InstanceRaw {
model: (self.transform.build_view_projection_matrix()).into(), model: (self.transform.to_matrix()).into(),
texture_index: self.texture_index, texture_index: self.texture_index,
} }
} }
@ -100,23 +92,49 @@ impl InstanceRaw {
} }
} }
/// API correction matrix.
/// cgmath uses OpenGL's matrix format, which
/// needs to be converted to wgpu's matrix format.
#[rustfmt::skip]
const OPENGL_TO_WGPU_MATRIX: Matrix4<f32> = Matrix4::new(
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 0.5, 0.5,
0.0, 0.0, 0.0, 1.0,
);
struct Transform { struct Transform {
pos: Point2<f32>, pos: Point2<f32>, // position on screen
aspect: f32, // width / height screen_aspect: f32, // width / height. Screen aspect ratio.
scale: f32, aspect: f32, // width / height. Sprite aspect ratio.
scale: f32, // if scale = 1, this sprite will be as tall as the screen.
rotate: Deg<f32>, // Around this object's center, in degrees measured ccw from vertical rotate: Deg<f32>, // Around this object's center, in degrees measured ccw from vertical
} }
impl Transform { impl Transform {
fn build_view_projection_matrix(&self) -> Matrix4<f32> { /// Build a matrix that corresponds to this transformation.
// Apply aspect ratio and scale fn to_matrix(&self) -> Matrix4<f32> {
let mut scale = Matrix4::from_nonuniform_scale(1.0, 1.0 / self.aspect, 1.0); // Note that our mesh starts centered at (0, 0).
scale = scale * Matrix4::from_scale(self.scale); // This is essential---we do not want scale and rotation
// changing our sprite's position!
// Our mesh starts at (0, 0), so this will rotate around the object's center. // Apply sprite aspect ratio, preserving height.
// Note that we translate AFTER scaling. // This must be done *before* rotation.
let sprite_aspect = Matrix4::from_nonuniform_scale(self.aspect, 1.0, 1.0);
// Apply provided scale
let scale = Matrix4::from_scale(self.scale);
// Apply rotation
let rotate = Matrix4::from_angle_z(self.rotate); let rotate = Matrix4::from_angle_z(self.rotate);
// Apply screen aspect ratio, again preserving height.
// This must be done AFTER rotation... think about it!
let screen_aspect = Matrix4::from_nonuniform_scale(1.0 / self.screen_aspect, 1.0, 1.0);
// After finishing all op, translate.
// This must be done last, all other operations
// require us to be at (0, 0).
let translate = Matrix4::from_translation(Vector3 { let translate = Matrix4::from_translation(Vector3 {
x: self.pos.x, x: self.pos.x,
y: self.pos.y, y: self.pos.y,
@ -124,8 +142,8 @@ impl Transform {
}); });
// Order matters! // Order matters!
// These are applied right-to-left // The rightmost matrix is applied first.
return OPENGL_TO_WGPU_MATRIX * translate * rotate * scale; return OPENGL_TO_WGPU_MATRIX * translate * screen_aspect * rotate * scale * sprite_aspect;
} }
} }
@ -158,23 +176,29 @@ impl Vertex {
} }
} }
// This is centered at 0,0 intentionally, // These vertices form a rectangle that covers the whole screen.
// so scaling works properly. // Two facts are important to note:
// - This is centered at (0, 0), so scaling doesn't change a sprite's position
// - At scale = 1, this covers the whole screen. Makes scale calculation easier.
//
// Screen coordinates range from -1 to 1, with the origin at the center.
// Texture coordinates range from 0 to 1, with the origin at the top-left
// and (1,1) at the bottom-right.
const VERTICES: &[Vertex] = &[ const VERTICES: &[Vertex] = &[
Vertex { Vertex {
position: [-0.5, 0.5, 0.0], position: [-1.0, 1.0, 0.0],
tex_coords: [0.0, 0.0], tex_coords: [0.0, 0.0],
}, },
Vertex { Vertex {
position: [0.5, 0.5, 0.0], position: [1.0, 1.0, 0.0],
tex_coords: [1.0, 0.0], tex_coords: [1.0, 0.0],
}, },
Vertex { Vertex {
position: [0.5, -0.5, 0.0], position: [1.0, -1.0, 0.0],
tex_coords: [1.0, 1.0], tex_coords: [1.0, 1.0],
}, },
Vertex { Vertex {
position: [-0.5, -0.5, 0.0], position: [-1.0, -1.0, 0.0],
tex_coords: [0.0, 1.0], tex_coords: [0.0, 1.0],
}, },
]; ];
@ -402,16 +426,17 @@ impl GPUState {
// TODO: warning when too many sprites are drawn. // TODO: warning when too many sprites are drawn.
let mut instances: Vec<Instance> = Vec::new(); let mut instances: Vec<Instance> = Vec::new();
for s in sprites { for s in sprites {
// Compute position on screen // Compute position on screen,
let screen_pos: Point2<f32> = (s.position - s.camera.to_vec()) / 400.0; // using logical pixels
let screen_pos: Point2<f32> = (s.position - s.camera.pos.to_vec()) / s.camera.zoom;
let texture = self.texture_array.get_texture(&s.name[..]); let texture = self.texture_array.get_texture(&s.name[..]);
instances.push(Instance { instances.push(Instance {
transform: Transform { transform: Transform {
pos: screen_pos, pos: screen_pos,
aspect: texture.aspect / screen_aspect, aspect: texture.aspect,
scale: 0.25, screen_aspect,
scale: s.scale * (texture.height / s.camera.zoom),
rotate: s.angle, rotate: s.angle,
}, },
texture_index: texture.index, texture_index: texture.index,

View File

@ -11,12 +11,12 @@ pub struct TextureArray {
texture_indices: HashMap<String, u32>, texture_indices: HashMap<String, u32>,
} }
const TEX: &[&str] = &["error", "gypsum", "earth", "a0"]; const TEX: &[&str] = &["error", "red", "gypsum", "earth", "a0"];
pub struct Texture { pub struct Texture {
pub index: u32, pub index: u32, // Index in texture array
pub dimensions: (u32, u32), pub aspect: f32, // width / height
pub aspect: f32, pub height: f32, // Height in game units
} }
impl TextureArray { impl TextureArray {
@ -30,7 +30,7 @@ impl TextureArray {
return Texture { return Texture {
index, index,
dimensions, height: 100.0,
aspect: dimensions.0 as f32 / dimensions.1 as f32, aspect: dimensions.0 as f32 / dimensions.1 as f32,
}; };
} }

View File

@ -33,12 +33,13 @@ impl Ship {
} }
impl Spriteable for Ship { impl Spriteable for Ship {
fn sprite(&self, camera: &Camera) -> Sprite { fn sprite(&self, camera: Camera) -> Sprite {
return Sprite { return Sprite {
position: self.body.pos, position: self.body.pos,
camera: camera.pos, camera: camera,
name: self.kind.sprite().to_owned(), name: self.kind.sprite().to_owned(),
angle: self.body.angle, angle: self.body.angle,
scale: 1.0,
}; };
} }
} }

View File

@ -17,7 +17,7 @@ impl System {
s.bodies.push(Doodad { s.bodies.push(Doodad {
pos: Polar { pos: Polar {
center: (0.0, 0.0).into(), center: (0.0, 0.0).into(),
radius: 300.0, radius: 100.0,
angle: Deg { 0: 31.0 }, angle: Deg { 0: 31.0 },
} }
.to_cartesian(), .to_cartesian(),
@ -32,7 +32,7 @@ impl System {
return s; return s;
} }
pub fn sprites(&self, camera: &Camera) -> Vec<Sprite> { pub fn sprites(&self, camera: Camera) -> Vec<Sprite> {
return self.bodies.iter().map(|x| x.sprite(camera)).collect(); return self.bodies.iter().map(|x| x.sprite(camera)).collect();
} }
} }