summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--shaders/shader.vert18
-rw-r--r--src/linear_algebra.rs363
-rw-r--r--src/main.rs7
3 files changed, 356 insertions, 32 deletions
diff --git a/shaders/shader.vert b/shaders/shader.vert
index ff1698d..d72c6a8 100644
--- a/shaders/shader.vert
+++ b/shaders/shader.vert
@@ -5,8 +5,6 @@ struct Transformation {
   vec3 translation;
 };
 
-// layout(binding = 0) uniform vec3 only_transform;
-
 layout(binding = 0) uniform UniformBlock {
    vec3 scale;
    Transformation model;
@@ -19,18 +17,6 @@ layout(location = 1) in vec3 inColor;
 
 layout(location = 0) out vec3 vertexColor;
 
-vec2 triangle[3] = vec2[](
-  vec2(0.0, -0.5),
-  vec2(0.5, 0.5),
-  vec2(-0.5, 0.5)
-);
-
-vec3 colors[3] = vec3[](
-  vec3(1.0, 0.0, 0.0),
-  vec3(0.0, 1.0, 0.0),
-  vec3(0.0, 0.0, 1.0)
-);
-
 
 // /////////////////
 // // Quaternions //
@@ -89,14 +75,10 @@ vec4 apply_all_transforms(vec3 position) {
   vec3 scaled = vec3(position.x * uniform_block.scale.x,
                      position.y * uniform_block.scale.y,
                      position.z * uniform_block.scale.z);
-  //return vec4(scaled, 1.0);
   vec3 model = transform(scaled, uniform_block.model);
   vec3 view = transform(model, uniform_block.view);
-  //vec4 projected = vec4(view, 1.0) * uniform_block.projection;
   vec4 projected = vec4(view, 1.0) * uniform_block.projection;
-  //return vec4(view, 1.000);
   return projected;
-  //return vec4(position, 1.0);
 }
 
 
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<Output=Self> + Sub<Output=Self>
 {
   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<T: Copy>([Vec4<T>; 4]);
 
 impl<T: Copy> Vec2<T> {
   //   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<T: Copy> Vec3<T> {
   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<T>) -> 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<T: Copy> Vec4<T> {
@@ -102,10 +267,89 @@ impl<T: Copy> Vec4<T> {
     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<T: Copy> Mat4<T> {
 
   //   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<T: Copy> Mat4<T> {
 
 #[repr(C)]
 #[derive(Clone, Debug)]
-pub struct Vertex<T: Copy> {
-  pub position: Vec2<T>,
-  pub color: Vec3<T>,
+pub struct Transformation<T: Copy> {
+  pub rotation: Vec4<T>,
+  pub translation: Vec3<T>,
 }
 
+impl<T: Copy> Transformation<T> {
+  //   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<T>, target: &Vec3<T>, up: &Vec3<T>) -> 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<T: Copy> {
-  pub rotation: Vec4<T>,
-  pub translation: Vec3<T>,
+pub struct Vertex<T: Copy> {
+  pub position: Vec2<T>,
+  pub color: Vec3<T>,
 }
 
 #[repr(C)]
diff --git a/src/main.rs b/src/main.rs
index ae56f2a..e80e2be 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -290,10 +290,9 @@ fn render_uniforms(device: &Device, device_memory: &vk::DeviceMemory,
                   .rotate(&Vec3::new(0.0, 1.0, 0.0), time % TAU),
     translation: Vec3::new(0.0, 0.0, 0.0),
   };
-  let view = Transformation {
-    rotation: Vec4::rotation_quaternion(&Vec3::new(1.0, 0.0, 0.0), 0.1),
-    translation: Vec3::new(0.0, 0.0, 2.0),
-  };
+  let view = Transformation::look_at(&Vec3::new(0.0, -1.0, -2.0),
+                                     &Vec3::new(0.0, 0.0, 0.0),
+                                     &Vec3::new(0.0, -1.0, 0.0));
   let aspect_ratio = extent.width as f32 / extent.height as f32;
   let projection = Mat4::perspective(FRAC_PI_4, aspect_ratio, 0.1, 10.0);
   let block = UniformBlock::<f32> { scale, model, view, projection };