separate out renderer crate (#13)

* add keyboard handler

* some cleanup

* add l key binding

* Fix WASM shared memory generation for Rust nightly 2025-10-20+

Add explicit linker flags for shared memory generation after Rust PR #147225
removed automatic flags with +atomics. This fixes DataCloneError when sharing
memory with workers.

Changes:
- Add --shared-memory, --max-memory, --import-memory flags
- Export TLS symbols: __wasm_init_tls, __tls_size, __tls_align, __tls_base
- Update both Cargo.toml and package.json build scripts

Amp-Thread-ID: https://ampcode.com/threads/T-41d99b69-5754-4fdf-b06e-a46343fa7485
Co-authored-by: Amp <amp@ampcode.com>

* add a level-editor crate

* remove redundant worker setup

* simplify app setup

* add SceneBuilder

* separate out default scene implementation in a crate

* define default method for MeshBuilder

---------

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Akash Shakdwipeea
2025-10-22 18:08:02 +05:30
committed by GitHub
co-authored by Amp
parent cb65a52e95
commit b295c0bf0b
33 changed files with 5516 additions and 995 deletions
+216
View File
@@ -0,0 +1,216 @@
use gltf::Gltf;
use ultraviolet::{Mat4, Vec3};
use wgpu::TextureFormat;
use crate::renderer::scene::{mesh_vertex_layout, MeshBuilder};
#[derive(Clone, Copy, Debug)]
pub struct ModelBounds {
pub min: [f32; 3],
pub max: [f32; 3],
}
impl ModelBounds {
fn new(min: [f32; 3], max: [f32; 3]) -> Self {
Self { min, max }
}
fn include_point(&mut self, point: [f32; 3]) {
for i in 0..3 {
self.min[i] = self.min[i].min(point[i]);
self.max[i] = self.max[i].max(point[i]);
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ImportError {
#[error("failed to fetch the model")]
Http(#[from] reqwest::Error),
#[error("failed to decode bytes")]
GltfParse(#[from] gltf::Error),
#[error("failed to load model")]
LoadError,
#[error("{0}")]
Other(String),
}
fn convert_tex_coords(tex_coords: gltf::mesh::util::ReadTexCoords<'_>) -> Vec<[f32; 2]> {
use gltf::mesh::util::ReadTexCoords;
match tex_coords {
ReadTexCoords::F32(iter) => iter.collect(),
ReadTexCoords::U16(iter) => iter
.map(|[u, v]| [u as f32 / u16::MAX as f32, v as f32 / u16::MAX as f32])
.collect(),
ReadTexCoords::U8(iter) => iter
.map(|[u, v]| [u as f32 / u8::MAX as f32, v as f32 / u8::MAX as f32])
.collect(),
}
}
fn convert_indices(indices: gltf::mesh::util::ReadIndices<'_>) -> Vec<u32> {
use gltf::mesh::util::ReadIndices;
match indices {
ReadIndices::U8(iter) => iter.map(|i| i as u32).collect(),
ReadIndices::U16(iter) => iter.map(|i| i as u32).collect(),
ReadIndices::U32(iter) => iter.collect(),
}
}
fn visit_node<'a>(
node: gltf::Node<'a>,
parent_transform: Mat4,
device: &wgpu::Device,
resources: &mut crate::renderer::GpuResources,
meshes: &mut Vec<crate::renderer::scene::Mesh>,
data_blob: &[u8],
pipeline_index: usize,
model_bounds: &mut Option<ModelBounds>,
) {
let local_transform = Mat4::from(node.transform().matrix());
let world_transform = parent_transform * local_transform;
let normal_matrix = world_transform.inversed().transposed();
if let Some(mesh) = node.mesh() {
for primitive in mesh.primitives() {
let reader = primitive.reader(|buffer| match buffer.source() {
gltf::buffer::Source::Bin => Some(&data_blob[..]),
_ => None,
});
let positions: Vec<[f32; 3]> = match reader.read_positions() {
Some(iter) => iter.collect(),
None => Vec::new(),
};
if positions.is_empty() {
continue;
}
let vertex_count = positions.len();
let default_normal_vec = normal_matrix.transform_vec3(Vec3::unit_y()).normalized();
let default_normal = [
default_normal_vec.x,
default_normal_vec.y,
default_normal_vec.z,
];
let mut normals: Vec<[f32; 3]> = reader
.read_normals()
.map(|iter| {
iter.map(|normal| {
let vec = Vec3::new(normal[0], normal[1], normal[2]);
let transformed = normal_matrix.transform_vec3(vec).normalized();
[transformed.x, transformed.y, transformed.z]
})
.collect()
})
.unwrap_or_else(|| vec![default_normal; vertex_count]);
if normals.len() != vertex_count {
normals.resize(vertex_count, default_normal);
}
let mut uvs: Vec<[f32; 2]> = reader
.read_tex_coords(0)
.map(convert_tex_coords)
.unwrap_or_else(|| vec![[0.0, 0.0]; vertex_count]);
if uvs.len() != vertex_count {
uvs.resize(vertex_count, [0.0, 0.0]);
}
for position in &positions {
let vec = Vec3::new(position[0], position[1], position[2]);
let transformed = world_transform.transform_point3(vec);
let world_point = [transformed.x, transformed.y, transformed.z];
if let Some(bounds) = model_bounds.as_mut() {
bounds.include_point(world_point);
} else {
*model_bounds = Some(ModelBounds::new(world_point, world_point));
}
}
let indices: Vec<u32> = reader
.read_indices()
.map(convert_indices)
.unwrap_or_else(|| (0..vertex_count as u32).collect());
if indices.is_empty() {
continue;
}
let mesh = MeshBuilder::default()
.with_vertices(device, resources, &positions, &normals, &uvs)
.with_indices(device, resources, &indices)
.with_pipeline(pipeline_index)
.with_model_matrix(device, resources, world_transform)
.build();
meshes.push(mesh);
}
}
for child in node.children() {
visit_node(
child,
world_transform,
device,
resources,
meshes,
data_blob,
pipeline_index,
model_bounds,
);
}
}
pub async fn load_gltf_model(
device: &wgpu::Device,
resources: &mut crate::renderer::GpuResources,
meshes: &mut Vec<crate::renderer::scene::Mesh>,
surface_format: TextureFormat,
) -> Result<Option<ModelBounds>, ImportError> {
let glb_data = reqwest::get("http://localhost:8080/themanor.glb")
.await?
.bytes()
.await?;
let model = Gltf::from_slice(&glb_data)?;
let data_blob = model.blob.as_ref().ok_or(ImportError::LoadError)?;
let vertex_layout = mesh_vertex_layout();
let pipeline_index = resources.get_or_create_pipeline(
device,
"gltf_standard",
&vertex_layout,
include_str!("./gltf.wgsl"),
surface_format,
);
let mut model_bounds: Option<ModelBounds> = None;
for scene in model.scenes() {
for node in scene.nodes() {
visit_node(
node,
Mat4::identity(),
device,
resources,
meshes,
data_blob,
pipeline_index,
&mut model_bounds,
);
}
}
Ok(model_bounds)
}