diff options
Diffstat (limited to 'src/linear_algebra.rs')
| -rw-r--r-- | src/linear_algebra.rs | 279 |
1 files changed, 269 insertions, 10 deletions
diff --git a/src/linear_algebra.rs b/src/linear_algebra.rs index 9b3ef73..26ce295 100644 --- a/src/linear_algebra.rs +++ b/src/linear_algebra.rs @@ -1,35 +1,294 @@ #![forbid(unsafe_code)] -use std::mem::size_of; +use std::mem::{ size_of, MaybeUninit }; +use std::ops::{ Add, Sub, Mul, Div, Neg }; use vulkanalia::vk::{ self, HasBuilder }; +pub trait Real: Add<Output=Self> + Sub<Output=Self> + + Mul<Output=Self> + Div<Output=Self> + + Neg<Output=Self> + + Sized + + From<f32> +{ + fn sin_cos(self) -> (Self, Self); + fn tan(self) -> Self; + fn recip(self) -> Self; +} + + +impl Real for f32 { + fn sin_cos(self) -> (Self, Self) { f32::sin_cos(self) } + fn tan(self) -> Self { f32::tan(self) } + fn recip(self) -> Self { f32::recip(self) } +} + + pub static VERTICES: [Vertex<f32>; 4] = [ - Vertex::new(Vec2(-0.5, -0.5), Vec3(1.0, 0.0, 0.0)), - Vertex::new(Vec2( 0.5, -0.5), Vec3(0.0, 1.0, 0.0)), - Vertex::new(Vec2( 0.5, 0.5), Vec3(0.0, 0.0, 1.0)), - Vertex::new(Vec2(-0.5, 0.5), Vec3(1.0, 1.0, 1.0)), + Vertex::new(Vec2::new(-0.5, -0.5), Vec3::new(1.0, 0.0, 0.0)), + Vertex::new(Vec2::new( 0.5, -0.5), Vec3::new(0.0, 1.0, 0.0)), + Vertex::new(Vec2::new( 0.5, 0.5), Vec3::new(0.0, 0.0, 1.0)), + Vertex::new(Vec2::new(-0.5, 0.5), Vec3::new(1.0, 1.0, 1.0)), ]; pub const INDICES: &[u16] = &[0, 1, 2, 2, 3, 0]; +#[repr(C)] #[derive(Clone, Debug)] -pub struct Vec2<T>(T, T); +pub struct Vec2<T: Copy>([T; 2]); + +// The GLSL spec version 4.60.8, in section 4.4, defers to the OpenGL spec +// for the meaning of the "std140" qualifier. The OpenGL spec version 4.6 +// core profile, in section 7.6.2.2, states that the base alignment of a +// 3-vector is the same as that for a 4-vector. It further states that, when +// a structure contains another structure, the offset of the item +// immediately following the sub-structure will be coerced to a multiple of +// the alignment of the sub-structure. Nothing actually states that a vector +// counts as a sub-structure for this purpose, and other nearby wording +// makes a distinction between vectors and structures, but as-implemented, +// it appears to. +// +// So, we need padding. +// +// That padding needs to be of the same size as T, and we want to do this +// with the bare minimum restrictions on what T can be. Furthermore, our +// constructors are const, and that's important to us. This may seem +// impossible, but MaybeUninit comes to the rescue! It does require T to be +// Copy; this is the reason that Copy is threaded everywhere in all these +// types. +#[repr(C)] #[derive(Clone, Debug)] -pub struct Vec3<T>(T, T, T); +pub struct Vec3<T: Copy>([T; 3], MaybeUninit<T>); + +#[repr(C)] +#[derive(Clone, Debug)] +#[allow(unused)] +pub struct Vec4<T: Copy>([T; 4]); + +// All our matrices are column-major, which is what GLSL expects. +#[repr(C)] +#[derive(Clone, Debug)] +#[allow(unused)] +pub struct Mat2<T: Copy>([Vec2<T>; 2]); + +// Using Vec3 here means we get its padding behavior, which is what we want, +// per that same section of the OpenGL spec. +#[repr(C)] #[derive(Clone, Debug)] #[allow(unused)] -pub struct Vec4<T>(T, T, T, T); +pub struct Mat3<T: Copy>([Vec3<T>; 3]); + +#[repr(C)] +#[derive(Clone, Debug)] +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 + pub const fn new(a: T, b: T) -> Self { + Vec2([a, b]) + } +} + +impl<T: Copy> Vec3<T> { + pub const fn new(a: T, b: T, c: T) -> Self { + Vec3([a, b, c], MaybeUninit::zeroed()) + } +} + +impl<T: Copy> Vec4<T> { + pub const fn new(a: T, b: T, c: T, d: T) -> Self { + Vec4([a, b, c, d]) + } + + // This is kept carefully the same as quaternion_conjugate() in shader.vert. + #[allow(unused)] + pub fn quaternion_conjugate(&self) -> Self + where T: Real + { + Vec4::new(self.0[0], -self.0[1], -self.0[2], -self.0[3]) + } + + // This is kept carefully the same as quaternion_product() in shader.vert. + pub fn quaternion_product(&self, other: &Self) -> Self + where T: Real + { + let a = &self.0; + let b = &other.0; + + Vec4::new((a[0] * b[0]) - (a[1] * b[1]) - (a[2] * b[2]) - (a[3] * b[3]), + (a[0] * b[1]) + (a[1] * b[0]) + (a[2] * b[3]) - (a[3] * b[2]), + (a[0] * b[2]) - (a[1] * b[3]) + (a[2] * b[0]) + (a[3] * b[1]), + (a[0] * b[3]) + (a[1] * b[2]) - (a[2] * b[1]) + (a[3] * b[0])) + } + + // The input angle is in radians. The resulting rotation is clockwise when + // looking in the positive direction of the axis, which is sometimes + // intuitive and sometimes not. + pub fn rotation_quaternion(axis: &Vec3<T>, angle: T) -> Self + where T: Real + { + let (sine, cosine) = (angle / 2.0.into()).sin_cos(); + + Self::new(cosine, sine * axis.0[0], sine * axis.0[1], sine * axis.0[2]) + } + + // A convenience method. + pub fn rotate(&self, axis: &Vec3<T>, angle: T) -> Self + where T: Real + { + // Rotation quaternions are composed by multiplying them. The one on the + // left side of the product is applied second, and the one on the right + // side is applied first. So, we do this in the "reverse" order because + // we're notionally composing the new rotation after the one we already + // have. + Self::rotation_quaternion(axis, angle).quaternion_product(self) + } +} + +impl<T: Copy> Mat2<T> { + #[allow(unused)] + pub const fn new(a: Vec2<T>, b: Vec2<T>) -> Self { + Mat2([a, b]) + } +} + +// You may be tempted to reimplement Mat3<T> as a synonym for Vec3<Vec3<T>>. +// This would be incorrect, because the padding behavior only applies at the +// inner layer. +impl<T: Copy> Mat3<T> { + #[allow(unused)] + pub const fn new(a: Vec3<T>, b: Vec3<T>, c: Vec3<T>) -> Self { + Mat3([a, b, c]) + } +} + +impl<T: Copy> Mat4<T> { + pub const fn new(a: Vec4<T>, b: Vec4<T>, c: Vec4<T>, d: Vec4<T>) -> Self { + Mat4([a, b, c, d]) + } + + // 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 + // 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. + // + // To apply the matrix, right-multiply with it. That is, compute a dot + // product where the right input is the matrix, and the left input is a + // position vector. The position must be a 4-vector with coordinates x, y, + // and z followed by a fourth value that is always 1.0. This fixed final + // coordinate of 1.0 allows the matrix to perform translation in addition to + // scaling and rotation. + // + // The result of the multiplication is a 4-vector with coordinates x, y, + // z, and w, with the expectation that the other three coordinates will be + // divided by w as part of the final viewport transformation performed + // internally to the graphics API. This is necessary in order to produce the + // homogenous behavior of the z-coordinate; it also acts as a convenient way + // to scale the x and y coordinates by depth, implementing what artists call + // foreshortening. + // + // The field of view is in radians, and is used as the angle measured + // vertically from top to bottom of the screen. The computed y scale will be + // directly based on this field of view, while the x scale will be based on + // the field of view and the aspect ratio. Thus, in setups where the screen + // is wider than it is tall, the horizontal field of view will be larger + // than the one given. + // + // The final z value, after the matrix multiplication and w-division have + // both been applied, is homogenous rather than Cartesian: coordinate values + // designate points further apart in space the farther they get from the + // origin. This is important because floating-point numbers have limited + // precision, and we want to spend most of that precision on the most + // visually noticeable things, which are the closest ones. Anyway, graphics + // APIs don't actually give us a choice about that. :) + // + // The near and far planes are used as the clipping boundaries for z + // values. An input position with z equal to the near value given here will + // have a final (multiplied and w-divided) z-value of 0.0; an input with z + // equal to the far value will have a final z of 1.0. + pub fn perspective(vertical_field_of_view: T, aspect_ratio: T, + near: T, far: T) + -> Self + where T: Real + { + // We compute some coefficients at the top here, which are best + // understood in the context of the full matrix, below. + let y_scale = (vertical_field_of_view / 2.0.into()).tan().recip(); + let x_scale = y_scale / aspect_ratio; + let z_scale = far / (far - near); + let z_offset = -near * z_scale; + + // This matrix will be treated as being in column-major order. That is, + // each Vec4 becomes a column. In the shader, we will then right-multiply + // with it, meaning we'll compute the dot product of the point with the + // matrix in that order. The result of this is that the items in the + // first vector are summed to give the final "x", the second vector gives + // the final "y", and so on. It expects the input point to have a value of + // 1.0 for w (the final item), which allows using the final item of each + // Vec4 to represent a constant term. + Mat4::new(Vec4::new(x_scale, 0.0.into(), 0.0.into(), 0.0.into()), + Vec4::new(0.0.into(), y_scale, 0.0.into(), 0.0.into()), + Vec4::new(0.0.into(), 0.0.into(), z_scale, z_offset), + Vec4::new(0.0.into(), 0.0.into(), 1.0.into(), 0.0.into())) + + // These coefficients require significant explanation. + // Counterintuitively, z_offset is the one that ends up contributing the + // part that varies with the input z, though it is not multiplied by z + // during the matrix multiplication. Notice the 1.0 in the bottom row, + // which sets the output w equal to the input z. When the w-division later + // happens, the z factor that was multiplied with z_scale cancels out, + // while z_offset winds up being inversely proportionate to the input z. + // + // Writing the full formula for the final z after matrix multiplication + // and w-division in terms of the input z, we get: + // + // z_final = [far / (far-near)] - [ near*far / z*(far-near)] + // + // Notice how both the constant part (from z_scale) and the part that + // varies with z (from z_offset) have (far - near) in the denominator. + // This is the part responsible for scaling the coordinates to the range + // 0.0 to 1.0. To see why z = near maps to z_final = 0.0 and z = far maps + // to z_final = 1.0, try substituting and simplifying. + // + // Also consider the subexpression near*far / z. This is the way the + // non-linear behavior needed for a homogenous coordinate is produced. + // When z is at its minimum, this subexpression reduces to just "far"; + // when z is at its maximum, the subexpression reduces to just "near". + // In between, the values are clustered densely around the "near" end. In + // the context of the full expression, this becomes the zero-to-one range. + } +} #[repr(C)] #[derive(Clone, Debug)] -pub struct Vertex<T> { +pub struct Vertex<T: Copy> { pub position: Vec2<T>, pub color: Vec3<T>, } -impl<T> Vertex<T> { +#[repr(C)] +#[derive(Clone, Debug)] +pub struct Transformation<T: Copy> { + pub rotation: Vec4<T>, + pub translation: Vec3<T>, +} + +#[repr(C)] +#[derive(Clone, Debug)] +pub struct UniformBlock<T: Copy> { + pub scale: Vec3<T>, + pub model: Transformation<T>, + pub view: Transformation<T>, + pub projection: Mat4<T>, +} + +impl<T: Copy> Vertex<T> { const fn new(position: Vec2<T>, color: Vec3<T>) -> Self { Self { position, color } } |