kitgit

tirbofish/dropbear · commit

53ee18b19d936451c2cf7373745d2a8dd7f6197c

optimised model loading, but am using the gltf crate instead of russimp-ng. this lowers down the import speed for 2 models from 10 seconds to 3 seconds. I'm not exactly sure how to optimise it, but im sure as hell that i need to implement async/await into the project. i suppose thats for another time... this should resolve Optimise Model Loading Fixes #41 for the timebeing

Unverified

tk <4tkbytes@pm.me> · 2025-09-13 15:42

view full diff

diff --git a/Cargo.toml b/Cargo.toml
index 843b111..67d6556 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -51,6 +51,10 @@ winit = { version = "0.30", features = [] }
 zip = "5.1"
 walkdir = "2.3"
 wasmer = { version = "6.1.0-rc.3" }
+rayon = "1.11"
+
+gltf = "1"
+thiserror = "2.0"
 
 [workspace.dependencies.image]
 version = "0.25"
diff --git a/dropbear-engine/Cargo.toml b/dropbear-engine/Cargo.toml
index 9cb8c64..3e42370 100644
--- a/dropbear-engine/Cargo.toml
+++ b/dropbear-engine/Cargo.toml
@@ -23,7 +23,7 @@ gilrs.workspace = true
 glam.workspace = true
 log.workspace = true
 log-once.workspace = true
-russimp-ng.workspace = true
+# russimp-ng.workspace = true
 serde.workspace = true
 spin_sleep.workspace = true
 wgpu.workspace = true
@@ -32,6 +32,8 @@ hecs.workspace = true
 # once_cell.workspace = true
 parking_lot.workspace = true
 lazy_static.workspace = true
+gltf.workspace = true
+rayon.workspace = true
 
 [target.'cfg(not(target_os = "android"))'.dependencies]
 rfd.workspace = true
diff --git a/dropbear-engine/src/graphics.rs b/dropbear-engine/src/graphics.rs
index c8d3999..55f12ee 100644
--- a/dropbear-engine/src/graphics.rs
+++ b/dropbear-engine/src/graphics.rs
@@ -1,4 +1,4 @@
-use std::{fs, path::PathBuf};
+use std::{fs, path::PathBuf, time::Instant};
 
 use egui::Context;
 use glam::{DMat4, DQuat, DVec3, Mat3};
@@ -23,6 +23,7 @@ pub struct Graphics<'a> {
     pub view: &'a TextureView,
     pub encoder: &'a mut CommandEncoder,
     pub screen_size: (f32, f32),
+    pub diffuse_sampler: Sampler,
 }
 
 pub const NO_TEXTURE: &'static [u8] = include_bytes!("../../resources/no-texture.png");
@@ -35,11 +36,23 @@ impl<'a> Graphics<'a> {
         encoder: &'a mut CommandEncoder,
     ) -> Self {
         let screen_size = (state.config.width as f32, state.config.height as f32);
+        let diffuse_sampler = state
+            .device
+            .create_sampler(&wgpu::SamplerDescriptor {
+                address_mode_u: wgpu::AddressMode::ClampToEdge,
+                address_mode_v: wgpu::AddressMode::ClampToEdge,
+                address_mode_w: wgpu::AddressMode::ClampToEdge,
+                mag_filter: wgpu::FilterMode::Linear,
+                min_filter: wgpu::FilterMode::Nearest,
+                mipmap_filter: wgpu::FilterMode::Nearest,
+                ..Default::default()
+            });
         Self {
             state,
             view,
             encoder,
             screen_size,
+            diffuse_sampler,
         }
     }
 
@@ -309,9 +322,99 @@ impl Texture {
         self.bind_group.as_ref().unwrap()
     }
 
+    pub(crate) fn create_texture_from_rgba_data(
+        graphics: &Graphics,
+        rgba_data: &[u8],
+        dimensions: (u32, u32),
+    ) -> Texture {
+        let texture_size = wgpu::Extent3d {
+            width: dimensions.0,
+            height: dimensions.1,
+            depth_or_array_layers: 1,
+        };
+
+        let create_start = Instant::now();
+        let diffuse_texture = graphics.state.device.create_texture(&wgpu::TextureDescriptor {
+            label: Some("diffuse_texture"),
+            size: texture_size,
+            mip_level_count: 1,
+            sample_count: 1,
+            dimension: wgpu::TextureDimension::D2,
+            format: wgpu::TextureFormat::Rgba8Unorm,
+            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
+            view_formats: &[],
+        });
+        println!("Creating new diffuse texture took {:?}", create_start.elapsed());
+
+        let write_start = Instant::now();
+        graphics.state.queue.write_texture(
+            wgpu::TexelCopyTextureInfo {
+                texture: &diffuse_texture,
+                mip_level: 0,
+                origin: wgpu::Origin3d::ZERO,
+                aspect: wgpu::TextureAspect::All,
+            },
+            rgba_data,
+            wgpu::TexelCopyBufferLayout {
+                offset: 0,
+                bytes_per_row: Some(4 * dimensions.0),
+                rows_per_image: Some(dimensions.1),
+            },
+            texture_size,
+        );
+        println!("Writing texture to graphics queue took {:?}", write_start.elapsed());
+
+        let sampler_start = Instant::now();
+        let diffuse_sampler = graphics.state.device.create_sampler(&wgpu::SamplerDescriptor {
+            address_mode_u: wgpu::AddressMode::ClampToEdge,
+            address_mode_v: wgpu::AddressMode::ClampToEdge,
+            address_mode_w: wgpu::AddressMode::ClampToEdge,
+            mag_filter: wgpu::FilterMode::Linear,
+            min_filter: wgpu::FilterMode::Nearest,
+            mipmap_filter: wgpu::FilterMode::Nearest,
+            ..Default::default()
+        });
+        println!("Creating sampler took {:?}", sampler_start.elapsed());
+
+        let view = diffuse_texture.create_view(&wgpu::TextureViewDescriptor::default());
+
+        let bind_group_start = Instant::now();
+        let diffuse_bind_group = graphics.state.device.create_bind_group(&wgpu::BindGroupDescriptor {
+            layout: &graphics.state.texture_bind_layout,
+            entries: &[
+                wgpu::BindGroupEntry {
+                    binding: 0,
+                    resource: wgpu::BindingResource::TextureView(&view),
+                },
+                wgpu::BindGroupEntry {
+                    binding: 1,
+                    resource: wgpu::BindingResource::Sampler(&diffuse_sampler),
+                },
+            ],
+            label: Some("texture_bind_group"),
+        });
+        println!("Creating diffuse bind group took {:?}", bind_group_start.elapsed());
+
+        println!("Done creating texture");
+
+        Texture {
+            texture: diffuse_texture,
+            sampler: diffuse_sampler,
+            view,
+            size: texture_size,
+            bind_group: Some(diffuse_bind_group),
+            layout: Some(graphics.state.texture_bind_layout.clone()),
+        }
+    }
+
     pub fn new(graphics: &Graphics, diffuse_bytes: &[u8]) -> Self {
+        let start = Instant::now();
         let diffuse_image = image::load_from_memory(diffuse_bytes).unwrap();
+        println!("Loading image to memory: {:?}", start.elapsed());
+
+        let start = Instant::now();
         let diffuse_rgba = diffuse_image.to_rgba8();
+        println!("Converting diffuse image to rgba8 took {:?}", start.elapsed());
 
         let dimensions = diffuse_image.dimensions();
         let texture_size = wgpu::Extent3d {
@@ -320,6 +423,7 @@ impl Texture {
             depth_or_array_layers: 1,
         };
 
+        let start = Instant::now();
         let diffuse_texture = graphics
             .state
             .device
@@ -333,7 +437,9 @@ impl Texture {
                 usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
                 view_formats: &[],
             });
+        println!("Creating new diffuse texture took {:?}", start.elapsed());
 
+        let start = Instant::now();
         graphics.state.queue.write_texture(
             wgpu::TexelCopyTextureInfo {
                 texture: &diffuse_texture,
@@ -349,7 +455,9 @@ impl Texture {
             },
             texture_size,
         );
+        println!("Writing texture to graphics queue took {:?}", start.elapsed());
 
+        let start = Instant::now();
         let diffuse_texture_view = diffuse_texture.create_view(&TextureViewDescriptor::default());
         let diffuse_sampler = graphics
             .state
@@ -363,7 +471,9 @@ impl Texture {
                 mipmap_filter: wgpu::FilterMode::Nearest,
                 ..Default::default()
             });
+        println!("Creating sampler took {:?}", start.elapsed());
 
+        let start = Instant::now();
         let diffuse_bind_group =
             graphics
                 .state
@@ -382,7 +492,8 @@ impl Texture {
                     ],
                     label: Some("texture_bind_group"),
                 });
-
+        println!("Creating diffuse bind group took {:?}", start.elapsed());
+        println!("Done creating texture");
         Self {
             bind_group: Some(diffuse_bind_group),
             layout: Some(graphics.state.texture_bind_layout.clone()),
diff --git a/dropbear-engine/src/lighting.rs b/dropbear-engine/src/lighting.rs
index f33736a..cf82227 100644
--- a/dropbear-engine/src/lighting.rs
+++ b/dropbear-engine/src/lighting.rs
@@ -232,7 +232,7 @@ impl Light {
 
         let cube_model = Model::load_from_memory(
             graphics,
-            include_bytes!("../../resources/cube.obj").to_vec(),
+            include_bytes!("../../resources/cube.glb").to_vec(),
             label.clone(),
         )
         .unwrap();
diff --git a/dropbear-engine/src/model.rs b/dropbear-engine/src/model.rs
index 549a86a..f6b4ae3 100644
--- a/dropbear-engine/src/model.rs
+++ b/dropbear-engine/src/model.rs
@@ -1,15 +1,18 @@
-use crate::graphics::{Graphics, NO_MODEL, Texture};
+use crate::graphics::{Graphics, Texture};
 use crate::utils::ResourceReference;
+use image::GenericImageView;
 use lazy_static::lazy_static;
 use parking_lot::Mutex;
-use russimp_ng::{
-    Vector3D,
-    material::{DataContent, TextureType},
-    scene::{PostProcess, Scene},
-};
+// use russimp_ng::{
+//     Vector3D,
+//     material::{DataContent, TextureType},
+//     scene::{PostProcess, Scene},
+// };
 use std::collections::HashMap;
+use std::time::Instant;
 use std::{mem, ops::Range, path::PathBuf};
 use wgpu::{BufferAddress, VertexAttribute, VertexBufferLayout, util::DeviceExt};
+use rayon::prelude::*;
 
 pub const GREY_TEXTURE_BYTES: &'static [u8] = include_bytes!("../../resources/grey.png");
 
@@ -45,118 +48,132 @@ pub struct Mesh {
 impl Model {
     pub fn load_from_memory(
         graphics: &Graphics<'_>,
-        buffer: Vec<u8>,
-        label: Option<&str>,
+        buffer: impl AsRef<[u8]>,
+        label: Option<&str>
     ) -> anyhow::Result<Model> {
+        let start = Instant::now();
         let cache_key = label.unwrap_or("default").to_string();
 
-        // Check memory cache first
         if let Some(cached_model) = MEMORY_MODEL_CACHE.lock().get(&cache_key) {
             log::debug!("Model loaded from memory cache: {:?}", cache_key);
             return Ok(cached_model.clone());
         }
 
         log::debug!("Loading from memory");
-        let res_ref = ResourceReference::from_bytes(buffer.clone());
-
-        let scene = match Scene::from_buffer(
-            buffer.as_slice(),
-            vec![
-                PostProcess::Triangulate,
-                PostProcess::FlipUVs,
-                PostProcess::GenerateNormals,
-            ],
-            "obj",
-        ) {
-            Ok(v) => v,
-            Err(_) => {
-                log::error!("Unable to load {:?} from memory", label);
-                Scene::from_buffer(
-                    NO_MODEL,
-                    vec![
-                        PostProcess::Triangulate,
-                        PostProcess::FlipUVs,
-                        PostProcess::GenerateNormals,
-                    ],
-                    "glb",
-                )?
-            }
-        };
+        let res_ref = ResourceReference::from_bytes(buffer.as_ref());
 
-        let mut materials = Vec::new();
-        for m in &scene.materials {
-            let mut name = String::new();
-            let diffuse_bytes_opt = m
-                .textures
-                .iter()
-                .find(|(t_type, _)| **t_type == TextureType::Diffuse)
-                .and_then(|(_, tex)| {
-                    name = tex.borrow().filename.clone();
-                    match &tex.borrow().data {
-                        DataContent::Bytes(b) => Some(b.clone()),
-                        DataContent::Texel(_) => {
-                            log::warn!("Skipping texel-based texture for material '{}'", &name);
-                            None
-                        }
-                    }
-                });
+        let (gltf, buffers, _images) = gltf::import_slice(buffer.as_ref())?;
+        let mut meshes = Vec::new();
 
-            let diffuse_texture = if let Some(bytes) = diffuse_bytes_opt {
-                Texture::new(graphics, &bytes)
-            } else {
-                if !name.is_empty() {
-                    log::warn!(
-                        "Error loading material {}, using default missing texture",
-                        name
-                    );
-                } else {
-                    log::warn!("Error loading material, using default missing texture");
+        let mut texture_data = Vec::new();
+        for material in gltf.materials() {
+            log::debug!("Processing material: {:?}", material.name());
+            let material_name = material.name().unwrap_or("Unnamed Material").to_string();
+            
+            let image_data = if let Some(pbr) = material.pbr_metallic_roughness().base_color_texture() {
+                let texture_info = pbr.texture();
+                let image = texture_info.source();
+                match image.source() {
+                    gltf::image::Source::View { view, mime_type: _ } => {
+                        let buffer_data = &buffers[view.buffer().index()];
+                        let start = view.offset();
+                        let end = start + view.length();
+                        buffer_data[start..end].to_vec()
+                    }
+                    gltf::image::Source::Uri { uri, mime_type: _ } => {
+                        log::warn!("External URI textures not supported: {}", uri);
+                        GREY_TEXTURE_BYTES.to_vec()
+                    }
                 }
-                Texture::new(graphics, GREY_TEXTURE_BYTES)
+            } else {
+                GREY_TEXTURE_BYTES.to_vec()
             };
+            
+            texture_data.push((material_name, image_data));
+        }
+
+        if texture_data.is_empty() {
+            texture_data.push(("Default".to_string(), GREY_TEXTURE_BYTES.to_vec()));
+        }
 
+        let parallel_start = Instant::now();
+        let processed_textures: Vec<_> = texture_data
+            .into_par_iter()
+            .map(|(material_name, image_data)| {
+                let material_start = Instant::now();
+                
+                let load_start = Instant::now();
+                let diffuse_image = image::load_from_memory(&image_data).unwrap();
+                println!("Loading image to memory: {:?}", load_start.elapsed());
+                
+                let rgba_start = Instant::now();
+                let diffuse_rgba = diffuse_image.to_rgba8();
+                println!("Converting diffuse image to rgba8 took {:?}", rgba_start.elapsed());
+                
+                let dimensions = diffuse_image.dimensions();
+                
+                println!("Parallel processing of material '{}' took: {:?}", material_name, material_start.elapsed());
+                
+                (material_name, diffuse_rgba.into_raw(), dimensions)
+            })
+            .collect();
+
+        println!("Total parallel image processing took: {:?}", parallel_start.elapsed());
+
+        let mut materials = Vec::new();
+        for (material_name, rgba_data, dimensions) in processed_textures {
+            let start = Instant::now();
+            
+            let diffuse_texture = Texture::create_texture_from_rgba_data(graphics, &rgba_data, dimensions);
             let bind_group = diffuse_texture.bind_group().to_owned();
+            
             materials.push(Material {
-                name: name,
+                name: material_name,
                 diffuse_texture,
                 bind_group,
             });
+            
+            println!("Time to create GPU texture: {:?}", start.elapsed());
         }
 
-        let mut meshes = Vec::new();
-        for mesh in &scene.meshes {
-            let vertices: Vec<ModelVertex> = mesh
-                .vertices
-                .iter()
-                .enumerate()
-                .map(|(i, v)| {
-                    let normal = mesh.normals.get(i).copied().unwrap_or(Vector3D {
-                        x: 0.0,
-                        y: 1.0,
-                        z: 0.0,
-                    });
-                    let tex_coords = mesh
-                        .texture_coords
-                        .get(0)
-                        .and_then(|coords| coords.as_ref().and_then(|vec| vec.get(i)))
-                        .map(|tc| [tc.x, tc.y])
-                        .unwrap_or([0.0, 0.0]);
-                    ModelVertex {
-                        position: [v.x, v.y, v.z],
-                        tex_coords,
-                        normal: [normal.x, normal.y, normal.z],
-                    }
-                })
-                .collect();
-
-            let indices: Vec<u32> = mesh
-                .faces
-                .iter()
-                .flat_map(|f| f.0.iter().copied())
-                .collect();
-
-            let vertex_buffer =
-                graphics
+        for mesh in gltf.meshes() {
+            log::debug!("Processing mesh: {:?}", mesh.name());
+            for primitive in mesh.primitives() {
+                let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()]));
+                
+                let positions: Vec<[f32; 3]> = reader
+                    .read_positions()
+                    .ok_or_else(|| anyhow::anyhow!("Mesh missing positions"))?
+                    .collect();
+
+                let normals: Vec<[f32; 3]> = reader
+                    .read_normals()
+                    .map(|iter| iter.collect())
+                    .unwrap_or_else(|| vec![[0.0, 1.0, 0.0]; positions.len()]);
+
+                let tex_coords: Vec<[f32; 2]> = reader
+                    .read_tex_coords(0)
+                    .map(|iter| iter.into_f32().collect())
+                    .unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]);
+
+                let vertices: Vec<ModelVertex> = positions
+                    .iter()
+                    .zip(normals.iter())
+                    .zip(tex_coords.iter())
+                    .map(|((pos, norm), tex)| ModelVertex {
+                        position: *pos,
+                        normal: *norm,
+                        tex_coords: *tex,
+                    })
+                    .collect();
+
+                let indices: Vec<u32> = reader
+                    .read_indices()
+                    .ok_or_else(|| anyhow::anyhow!("Mesh missing indices"))?
+                    .into_u32()
+                    .collect();
+
+                let vertex_buffer = graphics
                     .state
                     .device
                     .create_buffer_init(&wgpu::util::BufferInitDescriptor {
@@ -164,8 +181,8 @@ impl Model {
                         contents: bytemuck::cast_slice(&vertices),
                         usage: wgpu::BufferUsages::VERTEX,
                     });
-            let index_buffer =
-                graphics
+
+                let index_buffer = graphics
                     .state
                     .device
                     .create_buffer_init(&wgpu::util::BufferInitDescriptor {
@@ -174,28 +191,29 @@ impl Model {
                         usage: wgpu::BufferUsages::INDEX,
                     });
 
-            meshes.push(Mesh {
-                name: mesh.name.clone(),
-                vertex_buffer,
-                index_buffer,
-                num_elements: indices.len() as u32,
-                material: mesh.material_index as usize,
-            });
+                let material_index = primitive.material().index().unwrap_or(0);
+
+                meshes.push(Mesh {
+                    name: mesh.name().unwrap_or("Unnamed Mesh").to_string(),
+                    vertex_buffer,
+                    index_buffer,
+                    num_elements: indices.len() as u32,
+                    material: material_index,
+                });
+            }
         }
+
         log::debug!("Successfully loaded model [{:?}]", label);
         let model = Model {
             meshes,
             materials,
-            label: if let Some(l) = label {
-                l.to_string()
-            } else {
-                String::from("No named model")
-            },
+            label: label.unwrap_or("No named model").to_string(),
             path: res_ref,
         };
 
         MEMORY_MODEL_CACHE.lock().insert(cache_key, model.clone());
         log::debug!("Model cached from memory: {:?}", label);
+        log::debug!("Took {:?} to load model: {:?}", start.elapsed(), label);
         Ok(model)
     }
 
@@ -214,148 +232,8 @@ impl Model {
             return Ok(cached_model.clone());
         }
 
-        let scene = match Scene::from_file(
-            path.to_str().unwrap(),
-            vec![
-                PostProcess::Triangulate,
-                PostProcess::FlipUVs,
-                PostProcess::GenerateNormals,
-            ],
-        ) {
-            Ok(v) => v,
-            Err(_) => {
-                log::error!("Unable to locate model [{}]", path.display());
-                Scene::from_buffer(
-                    NO_MODEL,
-                    vec![
-                        PostProcess::Triangulate,
-                        PostProcess::FlipUVs,
-                        PostProcess::GenerateNormals,
-                    ],
-                    "glb",
-                )?
-            }
-        };
-
-        let mut materials = Vec::new();
-        for m in &scene.materials {
-            let mut name = String::new();
-            let diffuse_bytes_opt = m
-                .textures
-                .iter()
-                .find(|(t_type, _)| **t_type == TextureType::Diffuse)
-                .and_then(|(_, tex)| {
-                    name = tex.borrow().filename.clone();
-                    match &tex.borrow().data {
-                        DataContent::Bytes(b) => Some(b.clone()),
-                        DataContent::Texel(_) => {
-                            log::warn!("Skipping texel-based texture for material '{}'", &name);
-                            None
-                        }
-                    }
-                });
-
-            let diffuse_texture = if let Some(bytes) = diffuse_bytes_opt {
-                Texture::new(graphics, &bytes)
-            } else {
-                if !name.is_empty() {
-                    log::warn!(
-                        "Error loading material {}, using default missing texture",
-                        name
-                    );
-                } else {
-                    log::warn!("Error loading material, using default missing texture");
-                }
-                Texture::new(graphics, GREY_TEXTURE_BYTES)
-            };
-
-            let bind_group = diffuse_texture.bind_group().to_owned();
-            materials.push(Material {
-                name: name,
-                diffuse_texture,
-                bind_group,
-            });
-        }
-
-        let mut meshes = Vec::new();
-        for mesh in &scene.meshes {
-            let vertices: Vec<ModelVertex> = mesh
-                .vertices
-                .iter()
-                .enumerate()
-                .map(|(i, v)| {
-                    let normal = mesh.normals.get(i).copied().unwrap_or(Vector3D {
-                        x: 0.0,
-                        y: 1.0,
-                        z: 0.0,
-                    });
-                    let tex_coords = mesh
-                        .texture_coords
-                        .get(0)
-                        .and_then(|coords| coords.as_ref().and_then(|vec| vec.get(i)))
-                        .map(|tc| [tc.x, tc.y])
-                        .unwrap_or([0.0, 0.0]);
-                    ModelVertex {
-                        position: [v.x, v.y, v.z],
-                        tex_coords,
-                        normal: [normal.x, normal.y, normal.z],
-                    }
-                })
-                .collect();
-
-            let indices: Vec<u32> = mesh
-                .faces
-                .iter()
-                .flat_map(|f| f.0.iter().copied())
-                .collect();
-
-            let vertex_buffer =
-                graphics
-                    .state
-                    .device
-                    .create_buffer_init(&wgpu::util::BufferInitDescriptor {
-                        label: Some(&format!("{:?} Vertex Buffer", file_name)),
-                        contents: bytemuck::cast_slice(&vertices),
-                        usage: wgpu::BufferUsages::VERTEX,
-                    });
-            let index_buffer =
-                graphics
-                    .state
-                    .device
-                    .create_buffer_init(&wgpu::util::BufferInitDescriptor {
-                        label: Some(&format!("{:?} Index Buffer", file_name)),
-                        contents: bytemuck::cast_slice(&indices),
-                        usage: wgpu::BufferUsages::INDEX,
-                    });
-
-            meshes.push(Mesh {
-                name: mesh.name.clone(),
-                vertex_buffer,
-                index_buffer,
-                num_elements: indices.len() as u32,
-                material: mesh.material_index as usize,
-            });
-        }
-        log::debug!("Successfully loaded model [{:?}]", file_name);
-        let model = Model {
-            meshes,
-            materials,
-            label: if let Some(l) = label {
-                l.to_string()
-            } else {
-                String::from(
-                    file_name
-                        .unwrap()
-                        .to_str()
-                        .unwrap()
-                        .split(".")
-                        .into_iter()
-                        .next()
-                        .unwrap(),
-                )
-            },
-            path: ResourceReference::from_path(path).unwrap(),
-        };
+        let buffer = std::fs::read(path)?;
+        let model = Self::load_from_memory(graphics, buffer, label)?;
 
         MODEL_CACHE.lock().insert(path_str, model.clone());
         log::debug!("Model cached and loaded: {:?}", file_name);
diff --git a/dropbear-engine/src/utils.rs b/dropbear-engine/src/utils.rs
index c6661b0..dd2693a 100644
--- a/dropbear-engine/src/utils.rs
+++ b/dropbear-engine/src/utils.rs
@@ -76,9 +76,9 @@ impl ResourceReference {
     }
 
     /// Creates a new `ResourceReference` from bytes
-    pub fn from_bytes(bytes: Vec<u8>) -> Self {
+    pub fn from_bytes(bytes: impl AsRef<[u8]>,) -> Self {
         Self {
-            ref_type: ResourceReferenceType::Bytes(bytes),
+            ref_type: ResourceReferenceType::Bytes(bytes.as_ref().to_vec()),
         }
     }
 
diff --git a/eucalyptus-core/src/input.rs b/eucalyptus-core/src/input.rs
index 1c83cd3..52aa21b 100644
--- a/eucalyptus-core/src/input.rs
+++ b/eucalyptus-core/src/input.rs
@@ -19,6 +19,7 @@ pub struct InputState {
     pub pressed_keys: HashSet<KeyCode>,
     pub mouse_delta: Option<(f64, f64)>,
     pub is_cursor_locked: bool,
+    pub last_mouse_pos: Option<(f64, f64)>,
 }
 
 impl Default for InputState {
@@ -37,6 +38,7 @@ impl InputState {
             double_press_threshold: Duration::from_millis(300),
             mouse_delta: None,
             is_cursor_locked: false,
+            last_mouse_pos: Default::default(),
         }
     }
 
diff --git a/eucalyptus-editor/src/editor/input.rs b/eucalyptus-editor/src/editor/input.rs
index d775dc5..4d285fd 100644
--- a/eucalyptus-editor/src/editor/input.rs
+++ b/eucalyptus-editor/src/editor/input.rs
@@ -236,47 +236,61 @@ impl Keyboard for Editor {
 }
 
 impl Mouse for Editor {
+    #[cfg(target_os = "linux")]
+    // this impl doesn't have the mouse being recentered back to the center (to be fixed), this works as a usable alternative
     fn mouse_move(&mut self, position: PhysicalPosition<f64>) {
         if (self.is_viewport_focused && matches!(self.viewport_mode, ViewportMode::CameraMove))
-            || (matches!(self.editor_state, EditorState::Playing)
-                && !self.input_state.is_cursor_locked)
+            || (matches!(self.editor_state, EditorState::Playing) && !self.input_state.is_cursor_locked)
         {
-            if let Some(window) = &self.window {
-                let size = window.inner_size();
-                let center =
-                    PhysicalPosition::new(size.width as f64 / 2.0, size.height as f64 / 2.0);
-
-                let dx = position.x - center.x;
-                let dy = position.y - center.y;
+            if let Some(last_pos) = self.input_state.last_mouse_pos {
+                let dx = position.x - last_pos.0;
+                let dy = position.y - last_pos.1;
+                
                 if let Some(active_camera) = self.active_camera {
-                    if let Ok(mut q) = self.world.query_one::<(
-                        &mut Camera,
-                        &CameraComponent,
-                        Option<&CameraFollowTarget>,
-                    )>(active_camera)
-                    {
+                    if let Ok(mut q) = self.world.query_one::<(&mut Camera, &CameraComponent, Option<&CameraFollowTarget>)>(active_camera) {
                         if let Some((camera, _, _)) = q.get() {
                             camera.track_mouse_delta(dx, dy);
-                        } else {
-                            log_once::warn_once!(
-                                "Unable to fetch the query result of camera: {:?}",
-                                active_camera
-                            )
                         }
-                    } else {
-                        log_once::warn_once!(
-                            "Unable to query camera, component and option<camerafollowtarget> for active camera: {:?}",
-                            active_camera
-                        );
                     }
-                } else {
-                    log_once::warn_once!("No active camera found");
                 }
+            }
+            self.input_state.last_mouse_pos = Some((position.x, position.y));
+        }
+        
+        self.input_state.mouse_pos = (position.x, position.y);
+    }
 
-                let _ = window.set_cursor_position(center);
+    #[cfg(not(target_os = "linux"))]
+    // for some reason, this doesn't work on linux but works on windows (not tested on anywhere else)
+    fn mouse_move(&mut self, position: PhysicalPosition<f64>) {
+        if (self.is_viewport_focused && matches!(self.viewport_mode, ViewportMode::CameraMove))
+            || (matches!(self.editor_state, EditorState::Playing) && !self.input_state.is_cursor_locked)
+        {
+            if let Some(window) = &self.window {
+                let size = window.inner_size();
+                let center = PhysicalPosition::new(size.width as f64 / 2.0, size.height as f64 / 2.0);
+                
+                let distance_from_center = ((position.x - center.x).powi(2) + (position.y - center.y).powi(2)).sqrt();
+                
+                if distance_from_center > 5.0 {
+                    let dx = position.x - center.x;
+                    let dy = position.y - center.y;
+                    
+                    if let Some(active_camera) = self.active_camera {
+                        if let Ok(mut q) = self.world.query_one::<(&mut Camera, &CameraComponent, Option<&CameraFollowTarget>)>(active_camera) {
+                            if let Some((camera, _, _)) = q.get() {
+                                camera.track_mouse_delta(dx, dy);
+                            }
+                        }
+                    }
+                    
+                    let _ = window.set_cursor_position(center);
+                }
+                
                 window.set_cursor_visible(false);
             }
         }
+        
         self.input_state.mouse_pos = (position.x, position.y);
     }
 
diff --git a/eucalyptus-editor/src/editor/mod.rs b/eucalyptus-editor/src/editor/mod.rs
index 987044b..211c92a 100644
--- a/eucalyptus-editor/src/editor/mod.rs
+++ b/eucalyptus-editor/src/editor/mod.rs
@@ -363,7 +363,10 @@ impl Editor {
                     ui.separator();
                     if ui.button("Quit").clicked() {
                         match self.save_project_config() {
-                            Ok(_) => {}
+                            Ok(_) => {
+                                log::info!("Saved, quitting...");
+                                std::process::exit(0);
+                            }
                             Err(e) => {
                                 fatal!("Error saving project: {}", e);
                             }
diff --git a/eucalyptus-editor/src/editor/scene.rs b/eucalyptus-editor/src/editor/scene.rs
index 5a43ad5..a5f88d8 100644
--- a/eucalyptus-editor/src/editor/scene.rs
+++ b/eucalyptus-editor/src/editor/scene.rs
@@ -270,15 +270,60 @@ impl Scene for Editor {
 
         match &self.signal {
             Signal::Paste(scene_entity) => {
-                match AdoptedEntity::new(
-                    graphics,
-                    &scene_entity
-                        .model_path
-                        .to_project_path(self.project_path.clone().unwrap())
-                        .unwrap(),
-                    Some(&scene_entity.label),
-                ) {
-                    Ok(adopted) => {
+                match &scene_entity.model_path.ref_type {
+                    dropbear_engine::utils::ResourceReferenceType::None => {
+                        warn!("Resource has a reference type of None");
+                        self.signal = Signal::None;
+                    },
+                    dropbear_engine::utils::ResourceReferenceType::File(reference) => {
+                        match &scene_entity.model_path.to_project_path(self.project_path.clone().unwrap()) {
+                            Some(v) => {
+                                match AdoptedEntity::new(
+                                    graphics,
+                                    v,
+                                    Some(&scene_entity.label),
+                                ) {
+                                    Ok(adopted) => {
+                                        let entity_id = Arc::get_mut(&mut self.world).unwrap().spawn((
+                                            adopted,
+                                            scene_entity.transform,
+                                            ModelProperties::default(),
+                                        ));
+                                        self.selected_entity = Some(entity_id);
+                                        log::debug!(
+                                            "Successfully paste-spawned {} with ID {:?}",
+                                            scene_entity.label,
+                                            entity_id
+                                        );
+
+                                        success_without_console!("Paste!");
+                                        self.signal = Signal::Copy(scene_entity.clone());
+                                    }
+                                    Err(e) => {
+                                        warn!("Failed to paste-spawn {}: {}", scene_entity.label, e);
+                                    }
+                                }
+                            },
+                            None => {
+                                fatal!("Unable to convert resource reference [{}] to project related path", reference);
+                                self.signal = Signal::None;
+                            },
+                        };
+                    },
+                    dropbear_engine::utils::ResourceReferenceType::Bytes(bytes) => {
+                        let model = match Model::load_from_memory(graphics, bytes, Some(&scene_entity.label)) {
+                            Ok(v) => v,
+                            Err(e) => {
+                                fatal!("Unable to load from memory: {}", e);
+                                self.signal = Signal::None;
+                                return;
+                            },
+                        };
+                        let adopted = AdoptedEntity::adopt(
+                            graphics,
+                            model,
+                            Some(&scene_entity.label),
+                        );
                         let entity_id = Arc::get_mut(&mut self.world).unwrap().spawn((
                             adopted,
                             scene_entity.transform,
@@ -293,9 +338,11 @@ impl Scene for Editor {
 
                         success_without_console!("Paste!");
                         self.signal = Signal::Copy(scene_entity.clone());
-                    }
-                    Err(e) => {
-                        warn!("Failed to paste-spawn {}: {}", scene_entity.label, e);
+                    },
+                    _ => {
+                        warn!("Unable to copy, not of bytes or path");
+                        self.signal = Signal::None;
+                        return;
                     }
                 }
             }
@@ -947,32 +994,7 @@ impl Scene for Editor {
                             log::debug!("Creating new cube");
                             let model = Model::load_from_memory(
                                 graphics,
-                                include_bytes!("../../../resources/cube.obj").to_vec(),
-                                Some("Cube")
-                            );
-                            match model {
-                                Ok(model) => {
-                                    let cube = AdoptedEntity::adopt(
-                                        graphics,
-                                        model,
-                                        Some("Cube")
-                                    );
-                                    Arc::get_mut(&mut self.world).unwrap().spawn((cube, Transform::new(), ModelProperties::new()));
-                                }
-                                Err(e) => {
-                                    fatal!("Failed to load cube model: {}", e);
-                                }
-                            }
-                            success!("Created new cube");
-
-                            self.signal = Signal::None;
-                        }
-
-                        if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Cube")).clicked() {
-                            log::debug!("Creating new cube");
-                            let model = Model::load_from_memory(
-                                graphics,
-                                include_bytes!("../../../resources/cube.obj").to_vec(),
+                                include_bytes!("../../../resources/cube.glb"),
                                 Some("Cube")
                             );
                             match model {
diff --git a/redback-runtime b/redback-runtime
index d0b8e17..5cb98c0 160000
--- a/redback-runtime
+++ b/redback-runtime
@@ -1 +1 @@
-Subproject commit d0b8e1730dc73b4ec869cc00e213d3de0f305735
+Subproject commit 5cb98c0d508c7f8fd7d6a268d101d6e0fde0f41f
diff --git a/resources/cube.glb b/resources/cube.glb
new file mode 100644
index 0000000..88c5ec8
Binary files /dev/null and b/resources/cube.glb differ
diff --git a/resources/cube.obj b/resources/cube.obj
deleted file mode 100644
index 54fa3fe..0000000
--- a/resources/cube.obj
+++ /dev/null
@@ -1,52 +0,0 @@
-# Blender v3.0.0 OBJ File: ''
-# www.blender.org
-mtllib Cube.mtl
-o Cube_Scene
-v -0.500000 -0.500000 0.500000
-v -0.500000 0.500000 0.500000
-v 0.500000 0.500000 0.500000
-v 0.500000 -0.500000 0.500000
-v -0.500000 0.500000 -0.500000
-v 0.500000 0.500000 -0.500000
-v -0.500000 -0.500000 -0.500000
-v 0.500000 -0.500000 -0.500000
-vt 0.000000 0.000000
-vt 1.000000 1.000000
-vt 0.000000 1.000000
-vt 1.000000 0.000000
-vt 0.000000 0.000000
-vt 1.000000 1.000000
-vt 0.000000 1.000000
-vt 1.000000 0.000000
-vt 0.000000 0.000000
-vt 1.000000 1.000000
-vt 0.000000 1.000000
-vt 1.000000 0.000000
-vt 0.000000 0.000000
-vt 1.000000 1.000000
-vt 0.000000 1.000000
-vt 1.000000 0.000000
-vt 0.000000 0.000000
-vt 0.000000 1.000000
-vt 1.000000 1.000000
-vt 1.000000 0.000000
-vn 0.0000 -0.0000 1.0000
-vn 0.0000 1.0000 0.0000
-vn 0.0000 0.0000 -1.0000
-vn 0.0000 -1.0000 -0.0000
-vn 1.0000 0.0000 0.0000
-vn -1.0000 0.0000 0.0000
-usemtl Gray
-s 1
-f 1/1/1 3/2/1 2/3/1
-f 1/1/1 4/4/1 3/2/1
-f 2/5/2 6/6/2 5/7/2
-f 2/5/2 3/8/2 6/6/2
-f 5/9/3 8/10/3 7/11/3
-f 5/9/3 6/12/3 8/10/3
-f 7/13/4 4/14/4 1/15/4
-f 7/13/4 8/16/4 4/14/4
-f 4/17/5 6/6/5 3/18/5
-f 4/17/5 8/16/5 6/6/5
-f 7/13/6 2/19/6 5/7/6
-f 7/13/6 1/20/6 2/19/6
\ No newline at end of file