From 5dddb64f6a2fd072f2a26d4400f9a2a84386dc4e Mon Sep 17 00:00:00 2001 From: Irene Knapp Date: Mon, 13 Jul 2026 13:25:16 -0700 Subject: it spins!!!! ahem. more formally: provide animated uniforms to the shader, and use them the tutorial just wants us to do three matrices, and to defer all the actual math to a library it recommends. that would certainly be simpler. anyway, we use quaterion+offset for everything but the projection frustum, and do all the math ourselves Change-Id: I399f432bebb1e7ca4d4342efa1d5aaf37b32f427 Force-Push: yes --- shaders/shader.vert | 86 ++++++++++++- src/graphics_permanent.rs | 40 +++++- src/graphics_window_dressing.rs | 177 ++++++++++++++++++++++--- src/linear_algebra.rs | 279 ++++++++++++++++++++++++++++++++++++++-- src/main.rs | 58 +++++++++ 5 files changed, 611 insertions(+), 29 deletions(-) diff --git a/shaders/shader.vert b/shaders/shader.vert index d53ef0a..ff1698d 100644 --- a/shaders/shader.vert +++ b/shaders/shader.vert @@ -1,7 +1,22 @@ #version 460 +struct Transformation { + vec4 rotation; + vec3 translation; +}; + +// layout(binding = 0) uniform vec3 only_transform; + +layout(binding = 0) uniform UniformBlock { + vec3 scale; + Transformation model; + Transformation view; + mat4 projection; +} uniform_block; + layout(location = 0) in vec2 inPosition; layout(location = 1) in vec3 inColor; + layout(location = 0) out vec3 vertexColor; vec2 triangle[3] = vec2[]( @@ -16,8 +31,77 @@ vec3 colors[3] = vec3[]( vec3(0.0, 0.0, 1.0) ); + +// ///////////////// +// // Quaternions // +// ///////////////// +// +// To mathematicians, the components of a quaternion are traditionally +// multiplied by 1, i, j, and k, where 1 is a real number and i, j, and k are +// unit values along the three imaginary axes. GLSL doesn't give us component +// names that map cleanly onto these, and xyzw are confusing in this context +// because it's unclear whether x or w is the real-valued component, so we +// reference the components by numeric index. Yes, that sucks, but everything +// else sucks worse. +// +// That is: 0 is the real value and 1, 2, and 3 are the i, j, and k values. +// +// When reading code samples elsewhere, keep in mind that this is not the +// only possible choice. Checking how components are labeled should be the +// very first thing you do in understanding any quaternion arithmetic, +// anywhere. Also keep in mind that there are many equivalent ways to write +// any algebraic expression, and check carefully whether things are identical +// in meaning. +// +// This same challenge arises with matrices, it's just that the designers +// of GLSL have chosen to encapsulate that complexity inside the language. + +// This is kept carefully the same as quaternion_conjugate() in +// linear_algebra.rs. +vec4 quaternion_conjugate(vec4 a) { + return vec4(a[0], -a[1], -a[2], -a[3]); +} + +// This is kept carefully the same as quaternion_product() in +// linear_algebra.rs. +vec4 quaternion_product(vec4 a, vec4 b) { + return vec4((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])); +} + +vec3 quaternion_rotate(vec3 position, vec4 rotation) { + vec4 result = quaternion_product(quaternion_product(rotation, + vec4(0.0, position)), + quaternion_conjugate(rotation)); + return vec3(result[1], result[2], result[3]); +} + +vec3 transform(vec3 a, Transformation transformation) { + vec3 rotated = quaternion_rotate(a, transformation.rotation); + vec3 translated = rotated + transformation.translation; + return translated; +} + + +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); +} + + void main() { - gl_Position = vec4(inPosition, 0.0, 1.0); + gl_Position = apply_all_transforms(vec3(inPosition, 0.0)); vertexColor = inColor; } diff --git a/src/graphics_permanent.rs b/src/graphics_permanent.rs index c2225a3..b58e8a4 100644 --- a/src/graphics_permanent.rs +++ b/src/graphics_permanent.rs @@ -127,11 +127,13 @@ impl PermanentGraphicsState { = init_vulkan_device(&instance, &surface, enable_validation, enable_portability)?; + let descriptor_set_layout = init_descriptor_set_layout(&device)?; + Ok((PermanentGraphicsState { window, entry, instance, debug_messager, surface, device, - graphics_queue, presentation_queue, + graphics_queue, presentation_queue }, GraphicsStateForReinit { - physical_device, indices + physical_device, indices, descriptor_set_layout }, enable_swapchain)) } @@ -214,6 +216,17 @@ impl PermanentGraphicsState { pub struct GraphicsStateForReinit { pub physical_device: vk::PhysicalDevice, pub indices: QueueFamilyIndices, + pub descriptor_set_layout: vk::DescriptorSetLayout, +} + + +impl GraphicsStateForReinit { + #[allow(unsafe_code)] + pub fn destroy(self, device: &Device) -> () { + unsafe { + device.destroy_descriptor_set_layout(self.descriptor_set_layout, None) + }; + } } @@ -517,6 +530,29 @@ fn init_vulkan_device(instance: &Instance, surface: &vk::SurfaceKHR, } +#[allow(unsafe_code)] +fn init_descriptor_set_layout(device: &Device) + -> Result +{ + let uniform_block_binding = vk::DescriptorSetLayoutBinding::builder() + .binding(0) + .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER) + .descriptor_count(1) + .stage_flags(vk::ShaderStageFlags::VERTEX); + + let bindings = [uniform_block_binding]; + let descriptor_set_layout_info + = vk::DescriptorSetLayoutCreateInfo::builder() + .bindings(&bindings); + let descriptor_set_layout = unsafe { + device.create_descriptor_set_layout(&descriptor_set_layout_info, None) + }?; + + Ok(descriptor_set_layout) +} + + + // To Vulkan, a "physical" device is the actual GPU, and a "logical" // device is per-process state that represents a connection to the GPU. // Before we can create a logical device, we must choose which physical diff --git a/src/graphics_window_dressing.rs b/src/graphics_window_dressing.rs index fb56fb3..38d4188 100644 --- a/src/graphics_window_dressing.rs +++ b/src/graphics_window_dressing.rs @@ -3,10 +3,10 @@ use crate::error::*; use crate::graphics_permanent::{ PermanentGraphicsState, GraphicsStateForReinit, QueueFamilyIndices }; -use crate::linear_algebra::{ Vertex, VERTICES, INDICES }; +use crate::linear_algebra::{ Vertex, VERTICES, INDICES, UniformBlock }; use std::collections::BTreeSet; -use std::ptr; +use std::ptr::copy_nonoverlapping; use vulkanalia::{ Device, Instance }; use vulkanalia::vk::{ self, Handle, HasBuilder, InstanceV1_0, DeviceV1_0, KhrSwapchainExtensionDeviceCommands }; @@ -43,6 +43,12 @@ pub struct WindowDressing { index_buffer: vk::Buffer, index_buffer_memory: vk::DeviceMemory, + uniform_buffers: Vec, + pub uniform_buffer_memory: Vec, + + descriptor_pool: vk::DescriptorPool, + descriptor_sets: Vec, + pub command_buffers: Vec, pub concurrency: Concurrency, @@ -58,7 +64,7 @@ pub struct Swapchain { images: Vec, image_views: Vec, format: vk::Format, - extent: vk::Extent2D, + pub extent: vk::Extent2D, } #[derive(Debug)] @@ -95,6 +101,7 @@ impl WindowDressing { let graphics_queue = &permanent.graphics_queue; let physical_device = &for_reinit.physical_device; let indices = &for_reinit.indices; + let descriptor_set_layout = &for_reinit.descriptor_set_layout; let swapchain = init_swapchain( window, instance, surface, &physical_device, device, &indices)?; @@ -102,7 +109,8 @@ impl WindowDressing { let render_pass = init_render_pass(device, &swapchain.format)?; let (pipeline_layout, pipeline) - = init_pipeline(device, &swapchain.extent, &render_pass)?; + = init_pipeline(device, descriptor_set_layout, &swapchain.extent, + &render_pass)?; let framebuffers = init_framebuffers( device, &swapchain.extent, &swapchain.image_views, &render_pass)?; @@ -116,12 +124,21 @@ impl WindowDressing { let (index_buffer, index_buffer_memory) = init_index_buffer(instance, physical_device, device, graphics_queue, &transient_command_pool)?; + let (uniform_buffers, uniform_buffer_memory) + = init_uniform_buffers(instance, physical_device, device, + swapchain.images.len())?; + + let descriptor_pool + = init_descriptor_pool(device, swapchain.images.len())?; + let descriptor_sets + = init_descriptor_sets(device, descriptor_set_layout, + &uniform_buffers, &descriptor_pool, + swapchain.images.len())?; - let command_buffers = init_commands(device, &swapchain.extent, - &framebuffers, &render_pass, - &pipeline, - &vertex_buffer, &index_buffer, - &primary_command_pool)?; + let command_buffers = init_commands( + device, &swapchain.extent, &framebuffers, &render_pass, + &pipeline_layout, &pipeline, &vertex_buffer, &index_buffer, + &descriptor_sets, &primary_command_pool)?; let concurrency = init_concurrency(device, &swapchain.images)?; @@ -135,6 +152,10 @@ impl WindowDressing { vertex_buffer_memory, index_buffer, index_buffer_memory, + uniform_buffers, + uniform_buffer_memory, + descriptor_pool, + descriptor_sets, primary_command_pool, transient_command_pool, command_buffers, @@ -154,6 +175,7 @@ impl WindowDressing { let device = &permanent.device; let physical_device = &for_reinit.physical_device; let indices = &for_reinit.indices; + let descriptor_set_layout = &for_reinit.descriptor_set_layout; unsafe { device.device_wait_idle() }.unwrap(); @@ -165,15 +187,30 @@ impl WindowDressing { let render_pass = init_render_pass(device, &swapchain.format)?; let (pipeline_layout, pipeline) - = init_pipeline(device, &swapchain.extent, &render_pass)?; + = init_pipeline(device, descriptor_set_layout, &swapchain.extent, + &render_pass)?; let framebuffers = init_framebuffers( device, &swapchain.extent, &swapchain.image_views, &render_pass)?; + let (uniform_buffers, uniform_buffer_memory) + = init_uniform_buffers(instance, physical_device, device, + swapchain.images.len())?; + + // Notice that we did NOT reuse the descriptor pool. + let descriptor_pool + = init_descriptor_pool(device, swapchain.images.len())?; + + let descriptor_sets + = init_descriptor_sets(device, descriptor_set_layout, + &uniform_buffers, &descriptor_pool, + swapchain.images.len())?; + // Notice that we reused the command pool. let command_buffers = init_commands( device, &swapchain.extent, &framebuffers, &render_pass, - &pipeline, &self.vertex_buffer, &self.index_buffer, + &pipeline_layout, &pipeline, &self.vertex_buffer, + &self.index_buffer, &descriptor_sets, &self.primary_command_pool)?; self.concurrency.image_fences.resize(swapchain.images.len(), @@ -184,6 +221,10 @@ impl WindowDressing { self.pipeline = pipeline; self.pipeline_layout = pipeline_layout; self.framebuffers = framebuffers; + self.uniform_buffers = uniform_buffers; + self.uniform_buffer_memory = uniform_buffer_memory; + self.descriptor_pool = descriptor_pool; + self.descriptor_sets = descriptor_sets; self.command_buffers = command_buffers; Ok(()) @@ -236,6 +277,20 @@ impl WindowDressing { &self.command_buffers) }; + // While the descriptor pool is also a pool, it has a preallocated size + // which will be different next time. So, we destroy it all the way. + unsafe { device.destroy_descriptor_pool(self.descriptor_pool, None) }; + + // Notice that, unlike the vertex and index buffers, we destroy and + // re-create these on every reinitialization. That's because the number of + // them depends on how many images the swapchain has. + for buffer in &self.uniform_buffers { + unsafe { device.destroy_buffer(*buffer, None) }; + } + for memory in &self.uniform_buffer_memory { + unsafe { device.free_memory(*memory, None) }; + } + unsafe { device.destroy_pipeline(self.pipeline, None) }; unsafe { device.destroy_pipeline_layout(self.pipeline_layout, None) }; unsafe { device.destroy_render_pass(self.render_pass, None) }; @@ -402,8 +457,9 @@ fn init_render_pass(device: &Device, format: &vk::Format) #[allow(unsafe_code)] -fn init_pipeline(device: &Device, extent: &vk::Extent2D, - render_pass: &vk::RenderPass) +fn init_pipeline(device: &Device, + descriptor_set_layout: &vk::DescriptorSetLayout, + extent: &vk::Extent2D, render_pass: &vk::RenderPass) -> Result<(vk::PipelineLayout, vk::Pipeline)> { let vertex_binary = include_bytes!( @@ -489,7 +545,9 @@ fn init_pipeline(device: &Device, extent: &vk::Extent2D, .attachments(&blend_attachments) .blend_constants([0.0, 0.0, 0.0, 0.0]); - let pipeline_layout_info = vk::PipelineLayoutCreateInfo::builder(); + let layouts = [*descriptor_set_layout]; + let pipeline_layout_info = vk::PipelineLayoutCreateInfo::builder() + .set_layouts(&layouts); let pipeline_layout = unsafe { device.create_pipeline_layout(&pipeline_layout_info, None) @@ -571,6 +629,28 @@ fn init_index_buffer(instance: &Instance, vk::BufferUsageFlags::INDEX_BUFFER, INDICES) } +fn init_uniform_buffers(instance: &Instance, + physical_device: &vk::PhysicalDevice, device: &Device, + count: usize) + -> Result<(Vec, Vec)> +{ + let mut buffers = Vec::new(); + let mut all_memory = Vec::new(); + + for _ in 0 .. count { + let (buffer, memory) = allocate_buffer( + instance, physical_device, device, + size_of::>() as u64, + vk::BufferUsageFlags::UNIFORM_BUFFER, + vk::MemoryPropertyFlags::HOST_COHERENT + | vk::MemoryPropertyFlags::HOST_VISIBLE)?; + buffers.push(buffer); + all_memory.push(memory); + } + + Ok((buffers, all_memory)) +} + #[allow(unsafe_code)] fn init_buffer(instance: &Instance, @@ -593,8 +673,7 @@ fn init_buffer(instance: &Instance, }?; unsafe { - ptr::copy_nonoverlapping( - contents.as_ptr(), host_memory.cast(), contents.len()) + copy_nonoverlapping(contents.as_ptr(), host_memory.cast(), contents.len()) }; unsafe { device.unmap_memory(staging_memory) }; @@ -615,6 +694,61 @@ fn init_buffer(instance: &Instance, } +#[allow(unsafe_code)] +fn init_descriptor_pool(device: &Device, count: usize) + -> Result +{ + let uniform_block_size = vk::DescriptorPoolSize::builder() + .type_(vk::DescriptorType::UNIFORM_BUFFER) + .descriptor_count(count as u32); + + let sizes = [uniform_block_size]; + let pool_info = vk::DescriptorPoolCreateInfo::builder() + .pool_sizes(&sizes) + .max_sets(count as u32); + let pool = unsafe { device.create_descriptor_pool(&pool_info, None) }?; + + Ok(pool) +} + + +#[allow(unsafe_code)] +fn init_descriptor_sets(device: &Device, layout: &vk::DescriptorSetLayout, + buffers: &Vec, pool: &vk::DescriptorPool, + count: usize) + -> Result> +{ + let layouts = vec![*layout; count]; + let set_info = vk::DescriptorSetAllocateInfo::builder() + .descriptor_pool(*pool) + .set_layouts(&layouts); + let sets = unsafe { device.allocate_descriptor_sets(&set_info) }?; + + for index in 0 .. count { + let buffer_info = vk::DescriptorBufferInfo::builder() + .buffer(buffers[index]) + .offset(0) + .range(size_of::>() as u64); + + let buffer_info_list = [buffer_info]; + let descriptor_write_info = vk::WriteDescriptorSet::builder() + .dst_set(sets[index]) + .dst_binding(0) + .dst_array_element(0) + .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER) + .buffer_info(&buffer_info_list); + let write_info_list = [descriptor_write_info]; + let copy_info_list: [vk::CopyDescriptorSet; 0] = []; + + unsafe { + device.update_descriptor_sets(&write_info_list, ©_info_list) + }; + } + + Ok(sets) +} + + #[allow(unsafe_code)] fn init_command_pools(device: &Device, indices: &QueueFamilyIndices) -> Result<(vk::CommandPool, vk::CommandPool)> @@ -641,9 +775,11 @@ fn init_commands(device: &Device, extent: &vk::Extent2D, framebuffers: &Vec, render_pass: &vk::RenderPass, + pipeline_layout: &vk::PipelineLayout, pipeline: &vk::Pipeline, vertex_buffer: &vk::Buffer, index_buffer: &vk::Buffer, + descriptor_sets: &Vec, command_pool: &vk::CommandPool) -> Result> { @@ -707,6 +843,15 @@ fn init_commands(device: &Device, vk::IndexType::UINT16) }; + unsafe { + device.cmd_bind_descriptor_sets(command_buffer, + vk::PipelineBindPoint::GRAPHICS, + *pipeline_layout, + 0, + &[descriptor_sets[index]], + &[]) + }; + unsafe { device.cmd_draw_indexed(command_buffer, INDICES.len() as u32, 1, 0, 0, 0) 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 + Sub + + Mul + Div + + Neg + + Sized + + From +{ + 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; 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); +pub struct Vec2([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); +pub struct Vec3([T; 3], MaybeUninit); + +#[repr(C)] +#[derive(Clone, Debug)] +#[allow(unused)] +pub struct Vec4([T; 4]); + +// All our matrices are column-major, which is what GLSL expects. +#[repr(C)] +#[derive(Clone, Debug)] +#[allow(unused)] +pub struct Mat2([Vec2; 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); +pub struct Mat3([Vec3; 3]); + +#[repr(C)] +#[derive(Clone, Debug)] +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 + pub const fn new(a: T, b: T) -> Self { + Vec2([a, b]) + } +} + +impl Vec3 { + pub const fn new(a: T, b: T, c: T) -> Self { + Vec3([a, b, c], MaybeUninit::zeroed()) + } +} + +impl Vec4 { + 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, 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, 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 Mat2 { + #[allow(unused)] + pub const fn new(a: Vec2, b: Vec2) -> Self { + Mat2([a, b]) + } +} + +// You may be tempted to reimplement Mat3 as a synonym for Vec3>. +// This would be incorrect, because the padding behavior only applies at the +// inner layer. +impl Mat3 { + #[allow(unused)] + pub const fn new(a: Vec3, b: Vec3, c: Vec3) -> Self { + Mat3([a, b, c]) + } +} + +impl Mat4 { + pub const fn new(a: Vec4, b: Vec4, c: Vec4, d: Vec4) -> 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 { +pub struct Vertex { pub position: Vec2, pub color: Vec3, } -impl Vertex { +#[repr(C)] +#[derive(Clone, Debug)] +pub struct Transformation { + pub rotation: Vec4, + pub translation: Vec3, +} + +#[repr(C)] +#[derive(Clone, Debug)] +pub struct UniformBlock { + pub scale: Vec3, + pub model: Transformation, + pub view: Transformation, + pub projection: Mat4, +} + +impl Vertex { const fn new(position: Vec2, color: Vec3) -> Self { Self { position, color } } diff --git a/src/main.rs b/src/main.rs index e6e3c0a..7372a24 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,8 +6,13 @@ use crate::graphics_permanent::{ use crate::graphics_window_dressing::{ WindowDressing, N_SIMULTANEOUS_FRAMES }; +use crate::linear_algebra::{ Vec3, Vec4, Mat4, Transformation, UniformBlock }; use std::cell::RefCell; +use std::f32::consts::{ FRAC_PI_2, FRAC_PI_4, TAU }; +use std::ptr::copy_nonoverlapping; +use std::time::Instant; +use vulkanalia::Device; use vulkanalia::vk::{ self, Handle, HasBuilder, DeviceV1_0, KhrSwapchainExtensionDeviceCommands }; use winit::application::ApplicationHandler; @@ -28,6 +33,7 @@ struct Surreality { is_minimized: bool, is_reinit_queued: bool, frame_index: usize, + simulation_start: Instant, } impl Surreality { @@ -39,6 +45,7 @@ impl Surreality { is_minimized: false, is_reinit_queued: false, frame_index: 0, + simulation_start: Instant::now(), } } @@ -120,6 +127,11 @@ impl Surreality { concurrency.image_fences[image_index] = *frame_fence; + render_uniforms(device, + &window_dressing.uniform_buffer_memory[image_index], + &window_dressing.swapchain.extent, + self.simulation_start)?; + let first_semaphores = [*image_available_semaphore]; let second_semaphores = [*rendering_finished_semaphore]; @@ -170,6 +182,10 @@ impl Drop for Surreality { window_dressing.destroy(&permanent.device); } + if let Some(for_reinit) = self.for_reinit.replace(None) { + for_reinit.destroy(&permanent.device); + } + permanent.destroy(); } } @@ -217,6 +233,12 @@ impl ApplicationHandler for Surreality { _ => { } } } + + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + if let Some(permanent) = self.permanent.borrow().as_ref() { + permanent.window.request_redraw(); + } + } } @@ -237,3 +259,39 @@ fn main() -> std::process::ExitCode { } } + +#[allow(unsafe_code)] +fn render_uniforms(device: &Device, device_memory: &vk::DeviceMemory, + extent: &vk::Extent2D, simulation_start: Instant) + -> Result<()> +{ + let time = simulation_start.elapsed().as_secs_f32(); + + let scale = Vec3::new(1.0, 1.0, 1.0); + let model = Transformation { + rotation: Vec4::rotation_quaternion(&Vec3::new(-1.0, 0.0, 0.0), FRAC_PI_2) + .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 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:: { scale, model, view, projection }; + + let size = size_of::>() as u64; + let host_memory = unsafe { + device.map_memory(*device_memory, 0, size, vk::MemoryMapFlags::empty()) + }?; + + unsafe { + copy_nonoverlapping(&block, host_memory.cast(), 1) + }; + + unsafe { device.unmap_memory(*device_memory) }; + + Ok(()) +} + -- cgit 1.4.1