From f7e6dec079dcbbc26a9e047cf2b0acf23c091836 Mon Sep 17 00:00:00 2001 From: Irene Knapp Date: Fri, 17 Jul 2026 14:25:30 -0700 Subject: add Transformation::look_at() now we have real camera behavior. yay! Force-Push: yes Change-Id: I291d0cce0f2624e9bff8356c8dbe2aabc49599e2 --- src/linear_algebra.rs | 363 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 353 insertions(+), 10 deletions(-) (limited to 'src/linear_algebra.rs') diff --git a/src/linear_algebra.rs b/src/linear_algebra.rs index 26ce295..eb9aca2 100644 --- a/src/linear_algebra.rs +++ b/src/linear_algebra.rs @@ -12,14 +12,20 @@ pub trait Real: Add + Sub { fn sin_cos(self) -> (Self, Self); fn tan(self) -> Self; + fn atan2(self, other: Self) -> Self; fn recip(self) -> Self; + fn powi(self, power: i32) -> Self; + fn sqrt(self) -> Self; } impl Real for f32 { fn sin_cos(self) -> (Self, Self) { f32::sin_cos(self) } fn tan(self) -> Self { f32::tan(self) } + fn atan2(self, other: Self) -> Self { f32::atan2(self, other) } fn recip(self) -> Self { f32::recip(self) } + fn powi(self, power: i32) -> Self { f32::powi(self, power) } + fn sqrt(self) -> Self { f32::sqrt(self) } } @@ -85,16 +91,175 @@ pub struct Mat4([Vec4; 4]); impl Vec2 { // We use letters from the beginning of the alphabet for all of these, so - // as not to take a stance on any of the TODO finish this sentence + // as not to preference any specific use of these types to represent points, + // colors, rotations etc over others. There's a lot of variations in which + // coordinate is which, and picking "proper" names for them would inevitably + // wind up disagreeing with how other code does it. pub const fn new(a: T, b: T) -> Self { Vec2([a, b]) } + + #[allow(unused)] + pub fn negate(&self) -> Self + where T: Real + { + Vec2::new(-self.0[0], -self.0[1]) + } + + #[allow(unused)] + pub fn add(&self, other: &Self) -> Self + where T: Real + { + Vec2::new(self.0[0] + other.0[0], + self.0[1] + other.0[1]) + } + + #[allow(unused)] + pub fn subtract(&self, other: &Self) -> Self + where T: Real + { + Vec2::new(self.0[0] - other.0[0], + self.0[1] - other.0[1]) + } + + #[allow(unused)] + pub fn scalar_product(&self, scalar: T) -> Self + where T: Real + { + Vec2::new(self.0[0] * scalar, + self.0[1] * scalar) + } + + #[allow(unused)] + pub fn scalar_quotient(&self, scalar: T) -> Self + where T: Real + { + Vec2::new(self.0[0] / scalar, + self.0[1] / scalar) + } + + #[allow(unused)] + pub fn magnitude(&self) -> T + where T: Real + { + (self.0[0].powi(2) + self.0[1].powi(2)).sqrt() + } + + #[allow(unused)] + pub fn normalize(&self) -> Self + where T: Real + { + self.scalar_quotient(self.magnitude()) + } + + #[allow(unused)] + pub fn dot_product(&self, other: &Self) -> T + where T: Real + { + let a = self.0; + let b = other.0; + + (a[0]*b[0]) + (a[1]*b[1]) + } + + #[allow(unused)] + pub fn distance(&self, other: &Self) -> T + where T: Real + { + self.subtract(other).magnitude() + } } impl Vec3 { pub const fn new(a: T, b: T, c: T) -> Self { Vec3([a, b, c], MaybeUninit::zeroed()) } + + pub fn negate(&self) -> Self + where T: Real + { + Vec3::new(-self.0[0], -self.0[1], -self.0[2]) + } + + #[allow(unused)] + pub fn add(&self, other: &Self) -> Self + where T: Real + { + Vec3::new(self.0[0] + other.0[0], + self.0[1] + other.0[1], + self.0[2] + other.0[2]) + } + + pub fn subtract(&self, other: &Self) -> Self + where T: Real + { + Vec3::new(self.0[0] - other.0[0], + self.0[1] - other.0[1], + self.0[2] - other.0[2]) + } + + pub fn scalar_product(&self, scalar: T) -> Self + where T: Real + { + Vec3::new(self.0[0] * scalar, + self.0[1] * scalar, + self.0[2] * scalar) + } + + pub fn scalar_quotient(&self, scalar: T) -> Self + where T: Real + { + Vec3::new(self.0[0] / scalar, + self.0[1] / scalar, + self.0[2] / scalar) + } + + pub fn magnitude(&self) -> T + where T: Real + { + (self.0[0].powi(2) + self.0[1].powi(2) + self.0[2].powi(2)).sqrt() + } + + pub fn normalize(&self) -> Self + where T: Real + { + self.scalar_quotient(self.magnitude()) + } + + pub fn dot_product(&self, other: &Self) -> T + where T: Real + { + let a = self.0; + let b = other.0; + + (a[0]*b[0]) + (a[1]*b[1]) + (a[2]*b[2]) + } + + pub fn cross_product(&self, other: &Self) -> Self + where T: Real + { + let a = self.0; + let b = other.0; + + Vec3::new(a[1]*b[2] - a[2]*b[1], + a[2]*b[0] - a[0]*b[2], + a[0]*b[1] - a[1]*b[0]) + } + + pub fn distance(&self, other: &Self) -> T + where T: Real + { + self.subtract(other).magnitude() + } + + pub fn rotate_position_by_quaternion(&self, rotation: &Vec4) -> Self + where T: Real + { + let position = Vec4::new(0.0.into(), self.0[0], self.0[1], self.0[2]); + let result = rotation.quaternion_product(&position) + .quaternion_product(&rotation.quaternion_conjugate()); + Vec3::new(result.0[1], result.0[2], result.0[3]) + } } impl Vec4 { @@ -102,10 +267,89 @@ impl Vec4 { Vec4([a, b, c, d]) } + #[allow(unused)] + pub fn negate(&self) -> Self + where T: Real + { + Vec4::new(-self.0[0], -self.0[1], -self.0[2], -self.0[3]) + } + + #[allow(unused)] + pub fn add(&self, other: &Self) -> Self + where T: Real + { + Vec4::new(self.0[0] + other.0[0], + self.0[1] + other.0[1], + self.0[2] + other.0[2], + self.0[3] + other.0[3]) + } + + #[allow(unused)] + pub fn subtract(&self, other: &Self) -> Self + where T: Real + { + Vec4::new(self.0[0] - other.0[0], + self.0[1] - other.0[1], + self.0[2] - other.0[2], + self.0[3] - other.0[3]) + } + + #[allow(unused)] + pub fn scalar_product(&self, scalar: T) -> Self + where T: Real + { + Vec4::new(self.0[0] * scalar, + self.0[1] * scalar, + self.0[2] * scalar, + self.0[3] * scalar) + } + + #[allow(unused)] + pub fn scalar_quotient(&self, scalar: T) -> Self + where T: Real + { + Vec4::new(self.0[0] / scalar, + self.0[1] / scalar, + self.0[2] / scalar, + self.0[3] / scalar) + } + + #[allow(unused)] + pub fn magnitude(&self) -> T + where T: Real + { + (self.0[0].powi(2) + self.0[1].powi(2) + self.0[2].powi(2) + + self.0[3].powi(2)).sqrt() + } + + #[allow(unused)] + pub fn normalize(&self) -> Self + where T: Real + { + self.scalar_quotient(self.magnitude()) + } + + #[allow(unused)] + pub fn dot_product(&self, other: &Self) -> T + where T: Real + { + let a = self.0; + let b = other.0; + + (a[0]*b[0]) + (a[1]*b[1]) + (a[2]*b[2]) + (a[3]*b[3]) + } + + #[allow(unused)] + pub fn distance(&self, other: &Self) -> T + where T: Real + { + self.subtract(other).magnitude() + } + // This is kept carefully the same as quaternion_conjugate() in shader.vert. #[allow(unused)] pub fn quaternion_conjugate(&self) -> Self - where T: Real + where T: Real { Vec4::new(self.0[0], -self.0[1], -self.0[2], -self.0[3]) } @@ -171,8 +415,8 @@ impl Mat4 { // This produces a 4x4 matrix suitable for use in a shader to transform // coordinates. The matrix expects input in a Cartesian coordinate system - // where positive x is right, positive y is up, and positive z is deeper - // into the screen. Such a system is left-handed. The matrix's output + // where positive x is right, positive y is down, and positive z is deeper + // into the screen. Such a system is right-handed. The matrix's output // coordinate system ranges from -1 to 1 in the x and y axes, and from 0 to // 1 in the z axis, with the x and y axes being Euclidean and the z axis // being homogenous - see below. @@ -267,16 +511,115 @@ impl Mat4 { #[repr(C)] #[derive(Clone, Debug)] -pub struct Vertex { - pub position: Vec2, - pub color: Vec3, +pub struct Transformation { + pub rotation: Vec4, + pub translation: Vec3, } +impl Transformation { + // This computes a Transformation suitable for use as a "view" transform, + // describing a camera situated at the position given by "vantage" and + // pointed at the position given by "target". Because this would be + // under-constrained on its own, we also require an "up" vector which says + // which axis, and which direction along it, should be oriented towards the + // top of the screen. + // + // We make the assumption that the transform we return will be applied to + // a starting situation in which the camera is looking along the positive z + // axis, with the top of the screen towards the negative y axis. + // + // TODO we should really add one final rotation which fixes the camera to + // actually have the up vector as its up-axis. Right now we use it for + // pitching the camera but not for anything else, and that feels incorrect. + pub fn look_at(vantage: &Vec3, target: &Vec3, up: &Vec3) -> Self + where T: Real + std::fmt::Debug + { + // We're given an up vector as absolute orientation. We start by using + // dot product to separate our target and vantage positions into scalar + // values along that axis, and vectors representing everything BUT that + // axis. You can think of these "planar" vectors as representing the + // shadows of the two positions on a notional ground plane. + let target_up = target.dot_product(&up); + let vantage_up = vantage.dot_product(&up); + let target_planar = target.subtract(&up.scalar_product(target_up)); + let vantage_planar = vantage.subtract(&up.scalar_product(vantage_up)); + + // Now we can use the "planar" positions to compute a vector measured + // along that plane. We'll use that vector as an offset in the angle + // computations below, and we'll use its normalized version right here as + // an axis for a cross-product that will give us a "right" vector that + // is used in one of our rotations. More on its meaning below. + let planar_offset = target_planar.subtract(&vantage_planar); + let forward = planar_offset.normalize(); + let right = forward.cross_product(&up).normalize(); + + // We do two rotations. Here, we figure out the first one, which is the + // "yaw" to orient ourselves left-right. Our caller has given us a camera + // pointing in the positive-z direction, and we need to rotate from that + // global reference frame into one pointing directly at the object. We + // compute the legs of a right triangle using the x and z ades in global + // coordinates, and use the arctangent to get the angle at its center. + // + // Notice that this doesn't take the "up" vector into account. It's not + // clear that doing so is a sensible thing for this step. + // + // By the way, atan2() has less precision loss than doing the division + // ourselves and using plain atan(). If you didn't know that, now you + // know. It's a very thoughtful function in that way. + let right_distance = planar_offset.0[0]; + let forward_distance = planar_offset.0[2]; + let horizontal_angle = T::atan2(right_distance, forward_distance); + + // Now the second rotation, the "pitch" to orient up-down. Again we + // compute two legs of a triangle and use the arctangent to get an angle + // from it. + let vertical_distance = vantage_up - target_up; + let planar_distance = vantage_planar.distance(&target_planar); + let vertical_angle = T::atan2(vertical_distance, planar_distance); + + // Here, we compute the two quaternions for our rotations and compose + // them into a single one. Doing this here on the CPU on a per-frame basis + // will mean the GPU has less per-vertex work to do. + // + // The axis for the "yaw" rotation is the up vector. The axis for the + // "pitch" rotation is the "right" vector we computed. We promised to + // explain that "right" vector: It's pointing directly out to the right of + // the camera, in the position it NOW has, which is already yawed. Thus, + // rotating about it now will only affect the up-down direction. + let rotation = Vec4::rotation_quaternion(&up, horizontal_angle) + .rotate(&right, vertical_angle); + + // The semantics of a Transformation are that the rotation is always + // applied first, followed by the translation. That's natural and + // intuitive for model transformations, since the transformation is + // applied to the coordinates of the thing being drawn, and the model's + // local origin will presumably be at some logical point that corresponds + // to people's expectations. + // + // It is not intuitive for view transformations, such as the one we're + // returning, since the transformation is STILL applied to the thing being + // drawn, though notionally what we care about is instead the camera. We + // resolve this by applying the rotation we compute to the translation, + // so that the combined Transformation will move the model in such a way + // that it's as if the camera has moved. + // + // Notice that, since this translation describes the camera's position + // in world space, it's the only calculation we do that uses the vantage + // by itself, rather than the offset between the vantage and the target. + let translation = vantage.negate() + .rotate_position_by_quaternion(&rotation); + + Transformation { rotation, translation } + } +} + + + #[repr(C)] #[derive(Clone, Debug)] -pub struct Transformation { - pub rotation: Vec4, - pub translation: Vec3, +pub struct Vertex { + pub position: Vec2, + pub color: Vec3, } #[repr(C)] -- cgit 1.4.1