feat: attempt to implement math library

Adds many files, but the next thing is to implement the MeshRegistry properly.
This commit is contained in:
alphafetus committed 2026-05-08 08:02:38 -04:00
1 parent 9e4f531bd4
commit e7659ba387
18 files changed
+1232 -136

No files matched your search

+55
View File
@@ -0,0 +1,55 @@
use nalgebra::{Matrix4, Vector3, Vector4};
use crate::common::scene::SceneInstance;
/// Contains all information about an instance of a mesh, specifically its translation, rotation,
/// and scale. Note that the rotation is a [`Vector4`], and not a `Quaternion`.
///
/// This may be taken from a scene instance by calling the `Instance::from` method on the
/// corresponding [`SceneInstance`]. This is important, as the `Vector4` is not serializable.
///
/// Before passing this instance into the shaders, construct a transformation matrix by calling
/// [`MeshInstance::transform_matrix`].
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>,
/// The rotation of the instance. Note that since this is a [`Vector4`], or Euler angle, it
/// suffers from gimbal lock. If this becomes a serious issue, an [`nalgebra::UnitQuaternion`]
/// should be used instead.
pub rotation: Vector4<f32>,
/// The scale of the instance, represented as the scaling in XYZ coordinates.
pub scale: Vector3<f32>,
}
impl MeshInstance {
/// Constructs a transformation matrix from the current translation, rotation, and scale.
pub fn transform_matrix(&self) -> Matrix4<f32> {
Matrix4::new_translation(&self.translation.xyz()) *
// There's a possibility that these can be altered. Sometimes they're multiplied in the
// order ZYX. They are technically applied in the opposite order written here, so in
// this case it's actually applying Z, then Y, then X. They *do* produce different results.
Matrix4::from_axis_angle(&Vector3::x_axis(), self.rotation.x) *
Matrix4::from_axis_angle(&Vector3::y_axis(), self.rotation.y) *
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 {
Self {
mesh_id,
translation: Vector3::from(translation).to_homogeneous(),
rotation: Vector3::from(rotation).to_homogeneous(),
scale: Vector3::from(scale),
}
}
}
+155
View File
@@ -0,0 +1,155 @@
use std::collections::HashMap;
use std::error::Error;
use std::path::Path;
use std::sync::Arc;
use nalgebra::Vector3;
use uuid::Uuid;
use vulkano::buffer::{Buffer, BufferCreateInfo, BufferUsage, Subbuffer};
use vulkano::memory::allocator::{AllocationCreateInfo, MemoryAllocator, MemoryTypeFilter};
use wavefront::Obj;
use crate::common::scene::Scene;
use crate::gpu::vertex3::Vertex3;
/// This struct is slightly misleading - it doesn't actually contain the mesh information, but
/// the locations of the mesh's information in the overall application's vertex buffer, index buffer,
/// and other such structures.
///
/// Once loaded, the mesh should be accessible via ID through a registry-type struct, which simply
/// maintains the list of meshes that have been loaded by the application so far and which assigns
/// the correct indices for buffer-to-render logic.
///
/// That means that this `Mesh` struct is actually just an entry within that registry. They should
/// rarely be used directly, and should never be edited.
#[derive(Debug)]
pub struct Mesh {
pub id: String,
pub vertex_loc: usize,
pub index_loc: usize,
}
impl Mesh {
pub fn new(
id: impl Into<String>,
vertex_loc: usize,
index_loc: usize,
) -> Self {
Self {
id: id.into(),
vertex_loc,
index_loc,
}
}
}
/// # Mesh Registry
/// A wrapper around a series of [`Mesh`] structs, keyed by a unique ID, and the buffers that are
/// used by the rendering pipeline.
#[derive(Debug)]
pub struct MeshRegistry {
meshes: HashMap<String, Mesh>,
allocator: Arc<dyn MemoryAllocator>,
pub vertex_buffer: Arc<Subbuffer<[Vertex3]>>,
pub index_buffer: Arc<Subbuffer<[u32]>>,
}
impl MeshRegistry {
pub fn new(
allocator: Arc<dyn MemoryAllocator>,
) -> Result<Self, Box<dyn Error>> {
let vertex_buffer = Arc::new(Buffer::new_slice::<Vertex3>(
allocator.clone(),
BufferCreateInfo {
usage: BufferUsage::VERTEX_BUFFER,
..Default::default()
},
AllocationCreateInfo {
memory_type_filter: MemoryTypeFilter::PREFER_HOST |
MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
..Default::default()
},
1_000_000,
)?);
let index_buffer = Arc::new(Buffer::new_slice::<u32>(
allocator.clone(),
BufferCreateInfo {
usage: BufferUsage::INDEX_BUFFER,
..Default::default()
},
AllocationCreateInfo {
memory_type_filter: MemoryTypeFilter::PREFER_HOST |
MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
..Default::default()
},
3_000_000,
)?);
Ok(Self {
meshes: HashMap::new(),
allocator,
vertex_buffer,
index_buffer,
})
}
/// Creates a `MeshRegistry` from the provided [`Scene`].
///
/// This method will load all the meshes indicated by the scene and allocate the required
/// buffers. This significantly simplifies the allocation process, as the buffers only need to
/// be allocated once.
pub fn from_scene(scene: &Scene, allocator: Arc<dyn MemoryAllocator>) -> Self {
// First, load all the meshes into a single map.
println!("Loading meshes from scene");
let meshes = scene.mesh_paths.iter().map(|(id, path)|
(id.clone(), Obj::from_file(path).expect("Couldn't read wavefront file"))
).collect::<HashMap<String, Obj>>();
// Second, build two more maps that contain the vertex and index information.
let verts = meshes.iter().map(|(id, obj)|
(id.clone(), obj.vertices().map(|v| v.position().into()).collect())
).collect::<HashMap<String, Vec<Vector3<f32>>>>();
let indices = meshes.iter().map(|(id, obj)|
(id.clone(), obj.vertices().map(|v| v.position_index()).collect())
).collect::<HashMap<String, Vec<usize>>>();
// Apply static transformations here. The general idea is that we don't want to have to read
// any information from the buffer without the correct information. We do want to keep track
// of what mesh is at what location in the list though.
println!("Applying static mesh transformations");
// Get Vector3's as a list of Vertex3's (CPU to GPU)
// Third, allocate and fill buffers with this information.
// let vert_buffer = Self::alloc_vert_buffer(allocator, verts.values().collect());
// Fourth, return the struct.
todo!()
}
fn alloc_vert_buffer(
alloc: Arc<dyn MemoryAllocator>,
verts: Vec<Vertex3>
) -> Arc<Subbuffer<[Vertex3]>> {
println!("Allocating vertex buffer");
Arc::new(Buffer::from_iter(
alloc.clone(),
BufferCreateInfo {
usage: BufferUsage::VERTEX_BUFFER,
..Default::default()
},
AllocationCreateInfo {
memory_type_filter: MemoryTypeFilter::PREFER_HOST |
MemoryTypeFilter::HOST_SEQUENTIAL_WRITE,
..Default::default()
},
verts
).expect("Couldn't allocate vertex buffer"))
}
/// Attempts to find a [`Mesh`] with the given ID, returning `None` if it's not found.
pub fn get(&self, uuid: &str) -> Option<&Mesh> {
self.meshes.get(uuid)
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod mesh;
pub mod scene;
pub mod instance;
+74
View File
@@ -0,0 +1,74 @@
use std::collections::HashMap;
use std::io::Read;
use std::path::Path;
use serde::{Deserialize, Serialize};
/// A scene, which holds all information required to render a world.
///
/// This includes all mesh information, and all the information that is required to place
/// instances of these meshes in the world.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[repr(C)]
pub struct Scene {
/// The name of the scene.
pub name: String,
/// The collection of paths to the meshes, relative to the current scene file.
/// This collection is keyed on the ID of the mesh, which can be used later on to place
/// the meshes in the scenes with specific instances. This ID will also be used to register
/// the meshes with the application's registries, which will allow the program to associate
/// specific behavior with the mesh.
pub mesh_paths: HashMap<String, String>,
/// A collection of instances that are keyed by the mesh's IDs. Each instance can be used to
/// place a [`Mesh`] in the rendered scene.
pub instances: HashMap<String, SceneInstance>,
}
impl Scene {
/// Converts this `Scene` into its byte representation.
pub fn bytes(&self) -> Vec<u8> {
postcard::to_allocvec(self)
.expect("Couldn't serialize scene")
}
/// Reads a `scene` from the given path. Automatically resolves all paths for the meshes that
/// are referenced in the struct.
///
/// Reading a scene file allows for all meshes to be loaded at once, which therefore allows
/// the program to allocate pipeline buffers just once during the load of this. It would likely
/// be reasonable to create a `MeshRegistry` and `InstanceRegistry` at the same time, with
/// respect to a single loaded `Scene`.
pub fn from_file<P: AsRef<Path>>(path: P) -> Self {
let path = path.as_ref();
let mut file = std::fs::File::open(path).unwrap();
let mut contents = Vec::new();
let _ = file.read_to_end(&mut contents).unwrap();
let mut scene: Self = postcard::from_bytes(contents.as_slice())
.expect("Couldn't deserialize scene");
let base = path.parent().unwrap_or(&Path::new("."));
scene.mesh_paths = scene.mesh_paths
.into_iter()
.map(|(id, mesh_path)| {
let res = base.join(mesh_path);
let str = res.to_str().unwrap_or_default();
(id, str.to_string())
}).collect::<HashMap<String, String>>();
scene
}
}
/// Contains all information about an instance in the scene, specifically the name of the mesh it
/// should use and the translation, rotation, and scale of it.
///
/// After deserialization, this struct should be converted into an [`Instance`], which contains
/// defined vectors that can be used to work with the object before sending it to the shaders.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[repr(C)]
pub struct SceneInstance {
pub translation: [f32; 3],
pub rotation: [f32; 3],
pub scale: [f32; 3],
}