feat: implement a more modular graphics pipeline

This commit is contained in:
alphafetus committed 2026-05-10 01:04:42 -04:00
1 parent 6fe8ff2af0
commit 8b76fdb6ae
15 files changed
+498 -85

No files matched your search

+160
View File
@@ -0,0 +1,160 @@
use std::f32::consts::PI;
use std::sync::Arc;
use nalgebra::{Matrix4, Point3, Vector2, Vector3};
use vulkano::buffer::{Buffer, BufferCreateInfo, BufferUsage, Subbuffer};
use vulkano::descriptor_set::allocator::DescriptorSetAllocator;
use vulkano::descriptor_set::{DescriptorSet, WriteDescriptorSet};
use vulkano::descriptor_set::layout::DescriptorSetLayout;
use vulkano::memory::allocator::{AllocationCreateInfo, MemoryAllocator, MemoryTypeFilter};
use crate::gpu::camera::CameraUniform;
/// A camera set within 3D space.
pub struct Camera {
/// The position of the camera.
position: Vector3<f32>,
/// The forward direction of the camera.
forward: Vector3<f32>,
/// The upward direction of the camera.
up: Vector3<f32>,
/// The rightward direction of the camera.
right: Vector3<f32>,
/// The field of view, in radians.
fov: f32,
/// The extents of the viewport.
extents: Vector2<u32>,
}
impl Camera {
/// Creates a new camera. This default camera is positioned at (1, 1, 1) and is looking at
/// (0, 0, 0), and a default FOV of 70 degrees.
pub fn new(
extents: impl Into<Vector2<u32>>
) -> Self {
let extents = extents.into();
let position = Vector3::new(1.0, 1.0, 1.0);
let target = Vector3::zeros();
// Traveling along this vector moves us upward.
let up = Vector3::new(0.0, 1.0, 0.0);
// Traveling along this vector moves us to the right.
let right = up.cross(&(position - target)).normalize();
// Traveling along this vector moves us toward the target.
let forward = target - position;
Self {
position,
forward,
up,
right,
fov: 70.0 * PI / 180.0,
extents,
}
}
/// Moves the camera forward, or backward if given negative speed.
pub fn move_forward(&mut self, speed: f32) {
self.forward += self.forward * speed;
}
/// Moves the camera to the right, or left if given negative speed.
pub fn move_right(&mut self, speed: f32) {
self.right += self.right * speed;
}
/// Moves the camera upwards, or downward if given negative speed.
pub fn move_up(&mut self, speed: f32) {
self.up += self.up * speed;
}
/// Sets the extents of the viewport.
pub fn extents(&mut self, extents: impl Into<Vector2<u32>>) {
let extents = extents.into();
self.extents = extents;
}
/// Gets the current view matrix of the camera.
pub fn get_view(&self) -> Matrix4<f32> {
Matrix4::look_at_rh(
&Point3::from(self.position),
&Point3::from(self.forward - self.position),
&self.up
)
}
/// Creates a perspective matrix based on the current viewport aspect ratio.
pub fn get_projection(&self) -> Matrix4<f32> {
let aspect = self.extents.x as f32 / self.extents.y as f32;
Matrix4::new_perspective(
aspect,
self.fov,
1.0,
10000.0
)
}
}
/// An intermediate struct that maintains the buffer and descriptor set for the [`Camera`] struct.
/// The reason this information is not stored as part of the `Camera` itself is because tying
/// the GPU or rendering resources to the camera itself is quite awkward.
pub struct CameraResources {
buffer: Arc<Subbuffer<CameraUniform>>,
descriptor_set: Arc<DescriptorSet>,
}
impl CameraResources {
/// Creates a new set of `CameraResources`. Automatically allocates a uniform buffer to store
/// [`CameraUniform`] information.
pub fn new(
alloc: Arc<dyn MemoryAllocator>,
desc_alloc: Arc<dyn DescriptorSetAllocator>,
desc_layout: Arc<DescriptorSetLayout>,
) -> Arc<Self> {
// Allocate the buffer
let buffer = Arc::new(Buffer::new_sized::<CameraUniform>(
alloc.clone(),
BufferCreateInfo {
usage: BufferUsage::UNIFORM_BUFFER,
..Default::default()
},
AllocationCreateInfo {
memory_type_filter: MemoryTypeFilter::PREFER_HOST |
MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
..Default::default()
}
).expect("Couldn't allocate the camera uniform buffer"));
// Create the descriptor set
let descriptor_set = DescriptorSet::new(
desc_alloc.clone(),
desc_layout,
[WriteDescriptorSet::buffer(0, buffer.as_ref().clone())],
[]
).expect("Couldn't create camera descriptor set");
Arc::new(Self {
buffer,
descriptor_set
})
}
/// Updates the buffer with the given [`Camera`] information.
pub fn update(&self, camera: &Camera) {
// Update the buffer with the new information from the camera.
let view = camera.get_view();
let proj = camera.get_projection();
let mut buffer_data = self.buffer.write()
.unwrap();
buffer_data.proj = proj.into();
buffer_data.view = view.into();
}
/// Get a reference to the descriptor set.
pub fn descriptor_set(&self) -> Arc<DescriptorSet> {
self.descriptor_set.clone()
}
}
+42 -16
View File
@@ -1,4 +1,5 @@
use crate::common::scene::SceneInstance;
use std::collections::HashMap;
use crate::common::scene::{Scene, SceneInstance};
use nalgebra::{Matrix4, Vector3, Vector4};
/// Contains all information about an instance of a mesh, specifically its translation, rotation,
@@ -9,9 +10,8 @@ use nalgebra::{Matrix4, Vector3, Vector4};
///
/// Before passing this instance into the shaders, construct a transformation matrix by calling
/// [`MeshInstance::transform_matrix`].
#[derive(Debug, Clone)]
pub struct MeshInstance {
/// The ID of the mesh corresponding to this instance.
pub mesh_id: String,
/// The translation of the instance from the center of the standard space. It is possible that
/// local translation will be required in the future.
pub translation: Vector4<f32>,
@@ -35,21 +35,47 @@ impl MeshInstance {
Matrix4::from_axis_angle(&Vector3::z_axis(), self.rotation.z) *
Matrix4::new_nonuniform_scaling(&self.scale)
}
}
/// Constructs a `MeshInstance` from the given [`SceneInstance`] and mesh ID.
fn from_scene_instance(
mesh_id: String,
SceneInstance {
translation,
rotation,
scale,
}: SceneInstance,
) -> Self {
impl From<SceneInstance> for MeshInstance {
fn from(instance: SceneInstance) -> Self {
Self::from(&instance)
}
}
impl From<&SceneInstance> for MeshInstance {
fn from(SceneInstance { translation, rotation, scale }: &SceneInstance) -> Self {
Self {
mesh_id,
translation: Vector3::from(translation).to_homogeneous(),
rotation: Vector3::from(rotation).to_homogeneous(),
scale: Vector3::from(scale),
translation: Vector3::from(*translation).to_homogeneous(),
rotation: Vector3::from(*rotation).to_homogeneous(),
scale: Vector3::from(*scale),
}
}
}
/// A registry containing a series of `MeshInstance` structs linked to a mesh ID.
pub struct InstanceRegistry {
instances: HashMap<String, Vec<MeshInstance>>,
}
impl InstanceRegistry {
pub fn from_scene(scene: &Scene) -> Self {
// Read information about the instances from the scene.
let instances: HashMap<String, Vec<MeshInstance>> = scene.instances.iter()
.fold(HashMap::new(), |mut map, (mesh_id, instance)| {
map.entry(mesh_id.clone())
.and_modify(|v| v.push(instance.into()))
.or_insert(vec![instance.into()]);
map
});
Self {
instances
}
}
pub fn get_instances_for(&self, mesh_id: &String) -> Vec<MeshInstance> {
// We can't move these instances because we'll want them again later.
self.instances.get(mesh_id).cloned().unwrap_or_default()
}
}
+3 -3
View File
@@ -32,7 +32,7 @@ impl Mesh {}
/// used by the rendering pipeline.
#[derive(Debug)]
pub struct MeshRegistry {
meshes: HashMap<String, Mesh>,
pub meshes: HashMap<String, Mesh>,
allocator: Arc<dyn MemoryAllocator>,
pub vertex_buffer: Arc<Subbuffer<[Vertex3]>>,
pub index_buffer: Arc<Subbuffer<[u32]>>,
@@ -67,7 +67,7 @@ impl MeshRegistry {
let vert_start = verts.len();
let all_verts = obj.vertices();
let vert_len = all_verts.len();
verts.extend(all_verts.map(|v| Vertex3::from(v.position())));
verts.extend(all_verts.map(|v| Vertex3::new(v.position(), v.normal().unwrap_or_default())));
let index_list = obj
.triangles()
@@ -156,7 +156,7 @@ impl MeshRegistry {
}
/// Attempts to find a [`Mesh`] with the given ID, returning `None` if it's not found.
pub fn get(&self, uuid: &str) -> Option<&Mesh> {
pub fn get(&self, uuid: &String) -> Option<&Mesh> {
self.meshes.get(uuid)
}
}
+2
View File
@@ -1,3 +1,5 @@
pub mod instance;
pub mod mesh;
pub mod scene;
pub mod render_batch;
pub mod camera;
+56
View File
@@ -0,0 +1,56 @@
use std::sync::Arc;
use vulkano::buffer::{Buffer, BufferCreateInfo, BufferUsage, Subbuffer};
use vulkano::memory::allocator::{AllocationCreateInfo, MemoryAllocator, MemoryTypeFilter};
use crate::common::instance::InstanceRegistry;
use crate::common::mesh::MeshRegistry;
use crate::gpu::instance::GpuInstance;
#[derive(Debug, Clone)]
pub struct RenderBatch {
pub mesh_id: String,
pub instance_buffer: Subbuffer<[GpuInstance]>,
pub instance_count: u32,
}
impl RenderBatch {
/// Builds a `Vec<RenderBatch>` from the given list of instances and meshes. This should be done
/// once per frame.
pub fn build_batches(
allocator: Arc<dyn MemoryAllocator>,
mesh_registry: Arc<MeshRegistry>,
instance_registry: Arc<InstanceRegistry>
) -> Vec<Self> {
mesh_registry.meshes.iter()
.map(|(id, mesh)| {
let instances = instance_registry.get_instances_for(id);
let gpu_instances = instances.iter()
.cloned()
.map(|instance| {
instance.into()
})
.collect::<Vec<GpuInstance>>();
// Build instances into a RenderBatch
let instance_buffer = Buffer::from_iter(
allocator.clone(),
BufferCreateInfo {
usage: BufferUsage::VERTEX_BUFFER,
..Default::default()
},
AllocationCreateInfo {
memory_type_filter: MemoryTypeFilter::PREFER_DEVICE |
MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
..Default::default()
},
gpu_instances
).expect("Couldn't allocate instance buffer");
Self {
mesh_id: id.clone(),
instance_buffer,
instance_count: instances.len() as u32,
}
}).collect()
}
}
+46 -2
View File
@@ -42,10 +42,23 @@ impl Scene {
let mut contents = Vec::new();
let _ = file.read_to_end(&mut contents).unwrap();
let mut scene: Self =
let scene: Self =
postcard::from_bytes(contents.as_slice()).expect("Couldn't deserialize scene");
let base = path.parent().unwrap_or(&Path::new("."));
Self::resolve_paths(path, scene)
}
pub fn from_file_json<P: AsRef<Path>>(path: P) -> Self {
let path = path.as_ref();
let file = std::fs::File::open(path).unwrap();
let scene: Self = serde_json::from_reader(file).unwrap();
Self::resolve_paths(path, scene)
}
fn resolve_paths(scene_path: &Path, mut scene: Self) -> Self {
let base = scene_path.parent().unwrap_or(&Path::new("."));
scene.mesh_paths = scene
.mesh_paths
.into_iter()
@@ -73,3 +86,34 @@ pub struct SceneInstance {
pub rotation: [f32; 3],
pub scale: [f32; 3],
}
#[cfg(test)]
pub mod create_files {
use std::error::Error;
use std::io::Write;
pub use super::*;
#[test]
pub fn create_test_scene() -> Result<(), Box<dyn Error>> {
let scene = Scene {
name: "Testing Scene".to_string(),
mesh_paths: HashMap::from([
("venator".to_string(), "Venator.obj".to_string())
]),
instances: HashMap::from([
("venator".to_string(), SceneInstance {
translation: [0.0, 0.0, 0.0],
rotation: [0.0, 0.0, 0.0],
scale: [1.0, 1.0, 1.0]
})
])
};
let json = serde_json::to_string_pretty(&scene)?;
let mut file = std::fs::File::create("test_scene.scene")?;
file.write_all(json.as_bytes())?;
Ok(())
}
}