kitgit

tirbofish/dropbear · diff

25ba059 · Thribhu K

feature: memory was an issue when serializing to bytes, so i made a new model file type (eucmdl) and a new generic binary file type (eucbin).

Unverified

diff --git a/Cargo.toml b/Cargo.toml
index e53fdc7..839b008 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -31,7 +31,7 @@ env_logger = "0.11"
 futures = "0.3"
 gilrs = "0.11"
 git2 = { version = "0.20", features = ["vendored-openssl"] }
-glam = { version = "0.30", features = ["serde", "mint", "bytemuck"] }
+glam = { version = "0.30", features = ["serde", "mint", "bytemuck", "rkyv", "bytecheck"] }
 hecs = { version = "0.11", features = ["serde"] }
 log = "0.4"
 log-once = "0.4"
@@ -89,6 +89,7 @@ puffin_http = "0.16"
 dyn-clone = "1.0"
 downcast-rs = "2.0"
 float-derive = "0.1"
+rkyv = "0.8"
 
 [workspace.dependencies.image]
 version = "0.24"
diff --git a/crates/dropbear-engine/Cargo.toml b/crates/dropbear-engine/Cargo.toml
index a7df5a3..2fe5667 100644
--- a/crates/dropbear-engine/Cargo.toml
+++ b/crates/dropbear-engine/Cargo.toml
@@ -30,7 +30,7 @@ log.workspace = true
 log-once.workspace = true
 serde.workspace = true
 spin_sleep.workspace = true
-wgpu.workspace = true
+wgpu = { workspace = true, features = ["serde"] }
 winit.workspace = true
 hecs.workspace = true
 parking_lot.workspace = true
@@ -49,6 +49,7 @@ puffin.workspace = true
 bitflags.workspace = true
 puffin_http.workspace = true
 float-derive.workspace = true
+rkyv.workspace = true
 
 #yakui-wgpu.workspace = true
 #yakui.workspace = true
diff --git a/crates/dropbear-engine/src/model.rs b/crates/dropbear-engine/src/model.rs
index dfd3e4c..d08d88e 100644
--- a/crates/dropbear-engine/src/model.rs
+++ b/crates/dropbear-engine/src/model.rs
@@ -10,6 +10,7 @@ use gltf::texture::MinFilter;
 use parking_lot::RwLock;
 use puffin::profile_scope;
 use rayon::prelude::*;
+use rkyv::Archive;
 use serde::{Deserialize, Serialize};
 use std::hash::{DefaultHasher, Hash, Hasher};
 use std::sync::Arc;
@@ -68,7 +69,7 @@ pub struct Material {
     pub wrap_mode: TextureWrapMode,
 }
 
-#[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize, Default)]
+#[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize, Default)]
 pub enum AlphaMode {
     #[default]
     Opaque = 1,
@@ -87,7 +88,7 @@ impl Into<AlphaMode> for gltf::material::AlphaMode {
 }
 
 /// Represents a node in the scene graph (can be a joint/bone or a mesh)
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)]
 pub struct Node {
     pub name: String,
     pub parent: Option<usize>,
@@ -96,7 +97,7 @@ pub struct Node {
 }
 
 /// Local transform of a node relative to its parent
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)]
 pub struct NodeTransform {
     pub translation: glam::Vec3,
     pub rotation: glam::Quat,
@@ -118,7 +119,7 @@ impl NodeTransform {
 }
 
 /// A skin defines how a mesh is bound to a skeleton
-#[derive(Clone)]
+#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)]
 pub struct Skin {
     pub name: String,
     /// Indices of joints (nodes) in the Model's nodes array
@@ -130,7 +131,7 @@ pub struct Skin {
 }
 
 /// An animation that can be played on a skeleton
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)]
 pub struct Animation {
     pub name: String,
     pub channels: Vec<AnimationChannel>,
@@ -138,7 +139,7 @@ pub struct Animation {
 }
 
 /// Describes how an animation affects a specific node
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)]
 pub struct AnimationChannel {
     /// Target node index in the Model's nodes array
     pub target_node: usize,
@@ -150,7 +151,7 @@ pub struct AnimationChannel {
     pub interpolation: AnimationInterpolation,
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)]
 pub enum ChannelValues {
     Translations(Vec<glam::Vec3>),
     Rotations(Vec<glam::Quat>),
@@ -342,7 +343,7 @@ impl Material {
     }
 }
 
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)]
 pub enum AnimationInterpolation {
     /// The animated values are linearly interpolated between keyframes
     Linear,
@@ -1524,7 +1525,7 @@ pub trait Vertex {
 /// };
 /// ```
 #[repr(C)]
-#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable, Serialize, Deserialize)]
+#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)]
 pub struct ModelVertex {
     pub position: [f32; 3],
     pub normal: [f32; 3],
diff --git a/crates/dropbear-engine/src/procedural/mod.rs b/crates/dropbear-engine/src/procedural/mod.rs
index da57d66..fe5d54c 100644
--- a/crates/dropbear-engine/src/procedural/mod.rs
+++ b/crates/dropbear-engine/src/procedural/mod.rs
@@ -7,6 +7,7 @@ use crate::model::ModelVertex;
 use crate::model::{Material, Mesh, Model};
 use crate::utils::ResourceReference;
 use parking_lot::RwLock;
+use rkyv::Archive;
 use serde::{Deserialize, Serialize};
 use std::hash::{DefaultHasher, Hasher};
 use std::sync::Arc;
@@ -14,13 +15,13 @@ use wgpu::util::DeviceExt;
 
 pub mod cube;
 
-#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)]
 pub enum ProcObjType {
     Cuboid,
 }
 
 /// An object that comes with a template, and is generated through parameter input.
-#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)]
 pub struct ProcedurallyGeneratedObject {
     pub vertices: Vec<ModelVertex>,
     pub indices: Vec<u32>,
diff --git a/crates/dropbear-engine/src/texture.rs b/crates/dropbear-engine/src/texture.rs
index a1ba27d..b532f7b 100644
--- a/crates/dropbear-engine/src/texture.rs
+++ b/crates/dropbear-engine/src/texture.rs
@@ -3,7 +3,8 @@ use std::sync::Arc;
 use crate::asset::AssetRegistry;
 use crate::graphics::SharedGraphicsContext;
 use crate::utils::{ResourceReference, ToPotentialString};
-use image::GenericImageView;
+use image::{DynamicImage, GenericImageView, RgbaImage};
+use rkyv::Archive;
 use serde::{Deserialize, Serialize};
 use crate::multisampling::{AntiAliasingMode};
 
@@ -41,22 +42,21 @@ pub struct TextureBuilder<'a> {
     lod_min_clamp: f32,
     lod_max_clamp: f32,
 
-    view_descriptor: Option<wgpu::TextureViewDescriptor<'a>>,
+    view_descriptor: Option<wgpu::TextureViewDescriptor<'a>>, // doesnt support serde
 
     label: Option<&'a str>,
     mime_type: Option<String>,
 
-    source: TextureSource<'a>,
+    source: TextureSource,
 }
 
-enum TextureSource<'a> {
+enum TextureSource {
     Empty,
-    Bytes(&'a [u8]),
-    RawPixels(&'a [u8]),
-    #[allow(dead_code)]
-    SurfaceConfig(&'a wgpu::SurfaceConfiguration),
-    #[allow(dead_code)]
-    Depth(&'a wgpu::SurfaceConfiguration),
+    Image {
+        image: DynamicImage,
+        hash: u64,
+        reference: ResourceReference,
+    },
 }
 
 impl<'a> TextureBuilder<'a> {
@@ -111,7 +111,7 @@ impl<'a> TextureBuilder<'a> {
 
     /// preset: depth_texture()
     pub fn depth(mut self, config: &'a wgpu::SurfaceConfiguration, antialiasing: AntiAliasingMode) -> Self {
-        self.source = TextureSource::Depth(config);
+        self.source = TextureSource::Empty;
         self.width = config.width.max(1);
         self.height = config.height.max(1);
         self.format = Texture::DEPTH_FORMAT;
@@ -128,7 +128,7 @@ impl<'a> TextureBuilder<'a> {
 
     /// preset: viewport()
     pub fn viewport(mut self, config: &'a wgpu::SurfaceConfiguration) -> Self {
-        self.source = TextureSource::SurfaceConfig(config);
+        self.source = TextureSource::Empty;
         self.width = config.width.max(1);
         self.height = config.height.max(1);
         self.format = config.format.add_srgb_suffix();
@@ -159,9 +159,71 @@ impl<'a> TextureBuilder<'a> {
         self
     }
 
-    pub fn from_bytes(mut self, graphics: Arc<SharedGraphicsContext>, bytes: &'a [u8]) -> Self {
+    pub fn with_bytes(mut self, graphics: Arc<SharedGraphicsContext>, bytes: &'a [u8]) -> Self {
         self.graphics = Some(graphics);
-        self.source = TextureSource::Bytes(bytes);
+        let hash = AssetRegistry::hash_bytes(bytes);
+        let requested_dimensions = Some((self.width, self.height)).filter(|&d| d != (1, 1));
+
+        let image = match image::load_from_memory(bytes) {
+            Ok(image) => image,
+            Err(err) => {
+                if let Some((width, height)) = requested_dimensions {
+                    let expected_len = (width as usize)
+                        .saturating_mul(height as usize)
+                        .saturating_mul(4);
+                    if bytes.len() == expected_len {
+                        if let Some(rgba) = RgbaImage::from_raw(width, height, bytes.to_vec()) {
+                            DynamicImage::ImageRgba8(rgba)
+                        } else {
+                            log::error!(
+                                "Texture [{:?}] decode failed ({:?}); raw RGBA reconstruction failed for dimensions {}x{}. Falling back.",
+                                self.label,
+                                err,
+                                width,
+                                height
+                            );
+                            DynamicImage::ImageRgba8(RgbaImage::from_pixel(
+                                1,
+                                1,
+                                image::Rgba([255, 0, 255, 255]),
+                            ))
+                        }
+                    } else {
+                        log::error!(
+                            "Texture [{:?}] decode failed ({:?}); expected {} bytes for raw RGBA ({}x{}), got {}. Falling back.",
+                            self.label,
+                            err,
+                            expected_len,
+                            width,
+                            height,
+                            bytes.len()
+                        );
+                        DynamicImage::ImageRgba8(RgbaImage::from_pixel(
+                            1,
+                            1,
+                            image::Rgba([255, 0, 255, 255]),
+                        ))
+                    }
+                } else {
+                    log::error!(
+                        "Texture [{:?}] decode failed ({:?}) and no dimensions were provided; falling back to 1x1 magenta.",
+                        self.label,
+                        err
+                    );
+                    DynamicImage::ImageRgba8(RgbaImage::from_pixel(
+                        1,
+                        1,
+                        image::Rgba([255, 0, 255, 255]),
+                    ))
+                }
+            }
+        };
+
+        self.source = TextureSource::Image {
+            image,
+            hash,
+            reference: ResourceReference::from_bytes(bytes),
+        };
         self.auto_mip = true;
         self.mipmap_filter = wgpu::FilterMode::Linear;
         self.usage = wgpu::TextureUsages::TEXTURE_BINDING
@@ -171,9 +233,56 @@ impl<'a> TextureBuilder<'a> {
         self
     }
 
+    pub fn from_bytes(self, graphics: Arc<SharedGraphicsContext>, bytes: &'a [u8]) -> Self {
+        self.with_bytes(graphics, bytes)
+    }
+
     pub fn from_raw_pixels(mut self, graphics: Arc<SharedGraphicsContext>, pixels: &'a [u8]) -> Self {
         self.graphics = Some(graphics);
-        self.source = TextureSource::RawPixels(pixels);
+        let hash = AssetRegistry::hash_bytes(pixels);
+
+        let dimensions = (self.width, self.height);
+        let expected_len = (dimensions.0 as usize)
+            .saturating_mul(dimensions.1 as usize)
+            .saturating_mul(4);
+
+        let image = if pixels.len() == expected_len {
+            if let Some(rgba) = RgbaImage::from_raw(dimensions.0, dimensions.1, pixels.to_vec()) {
+                DynamicImage::ImageRgba8(rgba)
+            } else {
+                log::error!(
+                    "Texture [{:?}] raw RGBA reconstruction failed for dimensions ({}x{}). Falling back to 1x1 magenta.",
+                    self.label,
+                    dimensions.0,
+                    dimensions.1
+                );
+                DynamicImage::ImageRgba8(RgbaImage::from_pixel(
+                    1,
+                    1,
+                    image::Rgba([255, 0, 255, 255]),
+                ))
+            }
+        } else {
+            log::error!(
+                "Texture [{:?}] raw pixel byte length {} does not match expected {} for RGBA8 ({}x{}). Falling back.",
+                self.label,
+                pixels.len(),
+                expected_len,
+                dimensions.0,
+                dimensions.1
+            );
+            DynamicImage::ImageRgba8(RgbaImage::from_pixel(
+                1,
+                1,
+                image::Rgba([255, 0, 255, 255]),
+            ))
+        };
+
+        self.source = TextureSource::Image {
+            image,
+            hash,
+            reference: ResourceReference::from_bytes(pixels),
+        };
         self.auto_mip = true;
         self.mipmap_filter = wgpu::FilterMode::Linear;
         self.usage = wgpu::TextureUsages::TEXTURE_BINDING
@@ -207,49 +316,22 @@ impl<'a> TextureBuilder<'a> {
         puffin::profile_function!(self.label.unwrap_or("TextureBuilder::build"));
 
         match &self.source {
-            TextureSource::Bytes(bytes) => {
+            TextureSource::Image { image, hash, reference } => {
                 let graphics = self
                     .graphics
                     .as_ref()
-                    .expect("from_bytes() requires graphics context");
-                let hash = AssetRegistry::hash_bytes(bytes);
+                    .expect("with_data() requires graphics context");
                 let requested_dimensions = Some((self.width, self.height)).filter(|&d| d != (1, 1));
 
-                let (rgba, dimensions) = match image::load_from_memory(bytes) {
-                    Ok(image) => {
-                        let rgba = image.to_rgba8().into_raw();
-                        let dims = requested_dimensions.unwrap_or_else(|| image.dimensions());
-                        (rgba, dims)
+                let mut image = image.clone();
+                if let Some((width, height)) = requested_dimensions {
+                    if image.width() != width || image.height() != height {
+                        image = image.resize_exact(width, height, image::imageops::FilterType::Triangle);
                     }
-                    Err(err) => {
-                        if let Some(dims) = requested_dimensions {
-                            let expected_len = (dims.0 as usize)
-                                .saturating_mul(dims.1 as usize)
-                                .saturating_mul(4);
-                            if bytes.len() == expected_len {
-                                (bytes.to_vec(), dims)
-                            } else {
-                                log::error!(
-                                    "Texture [{:?}] decode failed ({:?}); expected {} bytes for raw RGBA ({}x{}), got {}. Falling back.",
-                                    self.label,
-                                    err,
-                                    expected_len,
-                                    dims.0,
-                                    dims.1,
-                                    bytes.len()
-                                );
-                                (vec![255, 0, 255, 255], (1, 1))
-                            }
-                        } else {
-                            log::error!(
-                                "Texture [{:?}] decode failed ({:?}) and no dimensions were provided; falling back to 1x1 magenta.",
-                                self.label,
-                                err
-                            );
-                            (vec![255, 0, 255, 255], (1, 1))
-                        }
-                    }
-                };
+                }
+
+                let rgba = image.to_rgba8().into_raw();
+                let dimensions = image.dimensions();
 
                 let size = wgpu::Extent3d {
                     width: dimensions.0,
@@ -264,67 +346,8 @@ impl<'a> TextureBuilder<'a> {
                     &graphics,
                     texture,
                     size,
-                    hash,
-                    ResourceReference::from_bytes(bytes),
-                )
-            }
-            TextureSource::RawPixels(pixels) => {
-                let graphics = self
-                    .graphics
-                    .as_ref()
-                    .expect("from_raw_pixels() requires graphics context");
-                let hash = AssetRegistry::hash_bytes(pixels);
-
-                let (format, pixels, dimensions) = match self.format.block_copy_size(None) {
-                    Some(bytes_per_pixel) => {
-                        let dimensions = (self.width, self.height);
-                        let expected_len = (dimensions.0 as usize)
-                            .saturating_mul(dimensions.1 as usize)
-                            .saturating_mul(bytes_per_pixel as usize);
-
-                        if pixels.len() == expected_len {
-                            (self.format, pixels.to_vec(), dimensions)
-                        } else {
-                            log::error!(
-                                "Texture [{:?}] byte length {} does not match expected {} for {:?} ({}x{}). Falling back to 1x1 magenta.",
-                                self.label,
-                                pixels.len(),
-                                expected_len,
-                                self.format,
-                                dimensions.0,
-                                dimensions.1
-                            );
-                            (Texture::TEXTURE_FORMAT, vec![255, 0, 255, 255], (1, 1))
-                        }
-                    }
-                    None => {
-                        log::error!(
-                            "Texture [{:?}] has unsupported format {:?}; falling back to 1x1 magenta.",
-                            self.label,
-                            self.format
-                        );
-                        (Texture::TEXTURE_FORMAT, vec![255, 0, 255, 255], (1, 1))
-                    }
-                };
-
-                let size = wgpu::Extent3d {
-                    width: dimensions.0,
-                    height: dimensions.1,
-                    depth_or_array_layers: 1,
-                };
-
-                let bytes_per_pixel = format
-                    .block_copy_size(None)
-                    .expect("fallback format must have a valid block size");
-                let mip_level_count = self.compute_mip_level_count(size);
-                let texture = self.create_texture(&graphics.device, size, format, mip_level_count);
-                Self::upload_level0(&graphics.queue, &texture, size, &pixels, bytes_per_pixel);
-                self.finish_uploaded_texture(
-                    &graphics,
-                    texture,
-                    size,
-                    hash,
-                    ResourceReference::from_bytes(pixels.as_slice()),
+                    *hash,
+                    reference.clone(),
                 )
             }
             _ => {
@@ -865,7 +888,7 @@ impl Texture {
     }
 }
 
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)]
 pub enum TextureWrapMode {
     Repeat,
     Clamp,
diff --git a/crates/dropbear-engine/src/utils.rs b/crates/dropbear-engine/src/utils.rs
index fe94353..04bce82 100644
--- a/crates/dropbear-engine/src/utils.rs
+++ b/crates/dropbear-engine/src/utils.rs
@@ -2,6 +2,7 @@
 
 use crate::procedural::ProcedurallyGeneratedObject;
 use serde::{Deserialize, Serialize};
+use rkyv::Archive;
 use std::fmt::{Display, Formatter};
 use std::path::Path;
 use std::sync::Arc;
@@ -65,7 +66,7 @@ pub fn canonicalize_euca_uri(uri: &str) -> anyhow::Result<String> {
     Ok(format!("{EUCA_SCHEME}{clean}"))
 }
 
-pub fn relative_path_from_euca<'a>(uri: &'a str) -> anyhow::Result<&'a str> {
+pub fn relative_path_from_euca(uri: &str) -> anyhow::Result<&str> {
     let without_scheme = uri.strip_prefix(EUCA_SCHEME).unwrap_or(uri);
 
     let stripped = without_scheme.trim_start_matches('/');
@@ -87,7 +88,7 @@ pub fn relative_path_from_euca<'a>(uri: &'a str) -> anyhow::Result<&'a str> {
 /// );
 /// assert_eq!(resource_ref.as_path().unwrap(), "models/cube.obj");
 /// ```
-#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)]
 pub enum ResourceReferenceType {
     /// The default type; Specifies there being no resource reference type.
     /// Typically creates errors, so watch out!
@@ -124,7 +125,7 @@ impl Default for ResourceReferenceType {
 /// - In the runtime (with redback-runtime), it
 ///   translates to `/home/tk/Downloads/Maze/resources/models/cube.obj`
 ///   _(assuming the executable is at `/home/tk/Downloads/Maze/maze_runner.exe`)_.
-#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
+#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize, Archive, rkyv::Serialize, rkyv::Deserialize)]
 pub struct ResourceReference {
     pub ref_type: ResourceReferenceType,
 }
@@ -192,11 +193,6 @@ impl ResourceReference {
         }
     }
 
-    /// Converts an euca URI string into a path relative to the `resources/` directory.
-    pub fn relative_path_from_uri<'a>(uri: &'a str) -> anyhow::Result<&'a str> {
-        relative_path_from_euca(uri)
-    }
-
     /// Creates a `ResourceReference` from a full path by extracting the part after "resources/".
     ///
     /// # Examples
diff --git a/crates/eucalyptus-core/Cargo.toml b/crates/eucalyptus-core/Cargo.toml
index 60e2573..67c1951 100644
--- a/crates/eucalyptus-core/Cargo.toml
+++ b/crates/eucalyptus-core/Cargo.toml
@@ -48,6 +48,8 @@ thiserror.workspace = true
 combine.workspace = true
 dyn-clone.workspace = true
 downcast-rs.workspace = true
+wgpu = {workspace = true, features = ["serde"]}
+rkyv.workspace = true
 
 [features]
 default = []
diff --git a/crates/eucalyptus-core/src/component.rs b/crates/eucalyptus-core/src/component.rs
index 1752a6e..904946a 100644
--- a/crates/eucalyptus-core/src/component.rs
+++ b/crates/eucalyptus-core/src/component.rs
@@ -1,7 +1,8 @@
 use crate::hierarchy::EntityTransformExt;
 use crate::physics::PhysicsState;
-use crate::states::{SerializedMaterialCustomisation, SerializedMeshRenderer};
-use crate::utils::ResolveReference;
+use crate::ser::model::EucalyptusModel;
+use crate::states::{Label, SerializedMaterialCustomisation, SerializedMeshRenderer};
+use crate::utils::{AsFile, ResolveReference};
 use downcast_rs::{Downcast, impl_downcast};
 use dropbear_engine::asset::{ASSET_REGISTRY, Handle};
 use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform};
@@ -24,33 +25,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
 
 pub use typetag::*;
 
-// todo: fix up this ---------------
 pub const DRAGGED_ASSET_ID: &str = "dragged_asset_reference";
-fn sanitize_resource_reference_for_scene_save(
-    reference: ResourceReference,
-    context: &str,
-) -> ResourceReference {
-    match reference.ref_type {
-        ResourceReferenceType::Bytes(bytes) => {
-            log::warn!(
-                "Dropping in-memory byte resource for {} during scene save ({} bytes); use a file-backed asset for persistence",
-                context,
-                bytes.len()
-            );
-            ResourceReference::default()
-        }
-        _ => reference,
-    }
-}
-
-fn sanitize_optional_resource_reference_for_scene_save(
-    reference: Option<ResourceReference>,
-    context: &str,
-) -> Option<ResourceReference> {
-    reference.map(|resource| sanitize_resource_reference_for_scene_save(resource, context))
-}
-
-// ------------------------
 
 pub struct ComponentRegistry {
     /// Maps TypeId to ComponentDescriptor for quick lookups
@@ -553,6 +528,44 @@ impl Component for MeshRenderer {
         Box::pin(async move {
             let import_scale = ser.import_scale.unwrap_or(1.0);
 
+            async fn load_model_from_reference(
+                model_ref: ResourceReference,
+                source_label: String,
+                graphics: Arc<SharedGraphicsContext>,
+            ) -> anyhow::Result<Handle<Model>> {
+                let path = model_ref.resolve()?;
+                let extension = path
+                    .extension()
+                    .and_then(|ext| ext.to_str())
+                    .map(|ext| ext.to_ascii_lowercase());
+
+                match extension.as_deref() {
+                    Some("eucmdl") => {
+                        let bytes = std::fs::read(&path)?;
+                        let model = rkyv::from_bytes::<EucalyptusModel, rkyv::rancor::Error>(&bytes)
+                            .map_err(|e| anyhow::anyhow!(
+                                "Failed to deserialize .eucmdl '{}' into EucalyptusModel: {e}",
+                                path.display()
+                            ))?;
+
+                        let runtime_model = model.load(model_ref.clone(), graphics);
+                        let mut registry = ASSET_REGISTRY.write();
+                        Ok(registry.add_model_with_label(source_label, runtime_model))
+                    }
+                    _ => {
+                        let buffer = std::fs::read(&path)?;
+                        Model::load_from_memory_raw(
+                            graphics,
+                            buffer,
+                            Some(model_ref.clone()),
+                            None,
+                            ASSET_REGISTRY.clone(),
+                        )
+                        .await
+                    }
+                }
+            }
+
             let handle = match &ser.handle.ref_type {
                 ResourceReferenceType::None => {
                     log::debug!("ResourceReferenceType is None, setting to `Handle::NULL`");
@@ -560,19 +573,17 @@ impl Component for MeshRenderer {
                 }
                 ResourceReferenceType::File(reference) => {
                     log::debug!("Loading model from file: {:?}", ser.handle);
-                    let path = ser.handle.clone().resolve()?;
-                    let buffer = std::fs::read(&path)?;
-                    Model::load_from_memory_raw(
+                    load_model_from_reference(
+                        ser.handle.clone(),
+                        reference.clone(),
                         graphics.clone(),
-                        buffer,
-                        Some(ser.handle.clone()),
-                        Some(reference),
-                        ASSET_REGISTRY.clone(),
                     )
                     .await?
                 }
                 ResourceReferenceType::Bytes(bytes) => {
                     log::debug!("Loading model from bytes [Len: {}]", bytes.len());
+                    log::warn!("ResourceReferenceType::Bytes is unsupported and is highly NOT recommended to be used for serialization as it will explode the memory on save");
+                    log::warn!("Please serialize to a file when you get the chance to do so (could also be an editor bug...)");
                     Model::load_from_memory_raw(
                         graphics.clone(),
                         bytes,
@@ -583,6 +594,8 @@ impl Component for MeshRenderer {
                     .await?
                 }
                 ResourceReferenceType::ProcObj(obj) => {
+                    log::warn!("ResourceReferenceType::Bytes is unsupported and is highly NOT recommended to be used for serialization as it will explode the memory on save");
+                    log::warn!("Please serialize to a file when you get the chance to do so (could also be an editor bug...)");
                     obj.build_model(graphics.clone(), None, None, ASSET_REGISTRY.clone())
                 }
             };
@@ -685,16 +698,82 @@ impl Component for MeshRenderer {
         }
     }
 
-    fn save(&self, _world: &World, _entity: Entity) -> Box<dyn SerializedComponent> {
+    fn save(&self, world: &World, entity: Entity) -> Box<dyn SerializedComponent> {
+        let entity_label_raw = world
+            .query_one::<&Label>(entity)
+            .get()
+            .map(|label| label.as_str().to_string())
+            .unwrap_or_else(|_| "unnamed_entity".to_string());
+
+        let sanitize_segment = |segment: &str| -> String {
+            let mut result = String::with_capacity(segment.len());
+            for ch in segment.chars() {
+                if ch.is_ascii_alphanumeric() {
+                    result.push(ch.to_ascii_lowercase());
+                } else if ch == '_' || ch == '-' {
+                    result.push(ch);
+                } else {
+                    result.push('_');
+                }
+            }
+
+            let trimmed = result.trim_matches('_').to_string();
+            if trimmed.is_empty() {
+                "unnamed_entity".to_string()
+            } else {
+                trimmed
+            }
+        };
+
+        let proc_obj_type_name = |ty: &ProcObjType| -> &'static str {
+            match ty {
+                ProcObjType::Cuboid => "cuboid",
+            }
+        };
+
+        let entity_label = sanitize_segment(entity_label_raw.as_str());
+
+        let save_reference = |reference: ResourceReference, context: &str| -> ResourceReference {
+            match &reference.ref_type {
+                ResourceReferenceType::None | ResourceReferenceType::File(_) => reference,
+                ResourceReferenceType::Bytes(_) | ResourceReferenceType::ProcObj(_) => {
+                    let desired_ref = match &reference.ref_type {
+                        ResourceReferenceType::ProcObj(obj) => Some(format!(
+                            "gen/{}.{}.eucmdl",
+                            entity_label,
+                            proc_obj_type_name(&obj.ty)
+                        )),
+                        _ => None,
+                    };
+
+                    match reference
+                        .as_file(desired_ref)
+                        .and_then(|path| ResourceReference::from_path(path.as_path()))
+                    {
+                        Ok(file_ref) => file_ref,
+                        Err(err) => {
+                            log::warn!(
+                                "Failed to save {} as file-backed reference: {}",
+                                context,
+                                err
+                            );
+                            ResourceReference::default()
+                        }
+                    }
+                }
+            }
+        };
+
+        let save_optional_reference = |reference: Option<ResourceReference>, context: &str| {
+            reference.map(|resource| save_reference(resource, context))
+        };
+
         let asset = ASSET_REGISTRY.read();
         let model = asset.get_model(self.model());
         let (label, handle) = if let Some(model) = model.as_ref() {
             (
                 model.label.clone(),
-                sanitize_resource_reference_for_scene_save(
-                    model.path.clone(),
-                    "mesh renderer model handle",
-                ),
+                save_reference(model.path.clone(), "mesh renderer model handle"),
             )
         } else {
             if !self.model().is_null() {
@@ -722,7 +801,7 @@ impl Component for MeshRenderer {
                     .get_texture(mat.diffuse_texture)
                     .and_then(|t| t.reference.clone())
             };
-            let diffuse_texture = sanitize_optional_resource_reference_for_scene_save(
+            let diffuse_texture = save_optional_reference(
                 diffuse_texture,
                 "mesh renderer diffuse texture",
             );
@@ -735,7 +814,7 @@ impl Component for MeshRenderer {
                 mat.normal_texture
                     .and_then(|h| asset.get_texture(h).and_then(|t| t.reference.clone()))
             };
-            let normal_texture = sanitize_optional_resource_reference_for_scene_save(
+            let normal_texture = save_optional_reference(
                 normal_texture,
                 "mesh renderer normal texture",
             );
@@ -748,7 +827,7 @@ impl Component for MeshRenderer {
                 mat.emissive_texture
                     .and_then(|h| asset.get_texture(h).and_then(|t| t.reference.clone()))
             };
-            let emissive_texture = sanitize_optional_resource_reference_for_scene_save(
+            let emissive_texture = save_optional_reference(
                 emissive_texture,
                 "mesh renderer emissive texture",
             );
@@ -761,7 +840,7 @@ impl Component for MeshRenderer {
                 mat.occlusion_texture
                     .and_then(|h| asset.get_texture(h).and_then(|t| t.reference.clone()))
             };
-            let occlusion_texture = sanitize_optional_resource_reference_for_scene_save(
+            let occlusion_texture = save_optional_reference(
                 occlusion_texture,
                 "mesh renderer occlusion texture",
             );
@@ -775,7 +854,7 @@ impl Component for MeshRenderer {
                 mat.metallic_roughness_texture
                     .and_then(|h| asset.get_texture(h).and_then(|t| t.reference.clone()))
             };
-            let metallic_roughness_texture = sanitize_optional_resource_reference_for_scene_save(
+            let metallic_roughness_texture = save_optional_reference(
                 metallic_roughness_texture,
                 "mesh renderer metallic-roughness texture",
             );
diff --git a/crates/eucalyptus-core/src/lib.rs b/crates/eucalyptus-core/src/lib.rs
index 56fa85c..bec4fe1 100644
--- a/crates/eucalyptus-core/src/lib.rs
+++ b/crates/eucalyptus-core/src/lib.rs
@@ -1,3 +1,5 @@
+extern crate core;
+
 pub mod animation;
 pub mod asset;
 pub mod camera;
@@ -23,6 +25,7 @@ pub mod types;
 pub mod utils;
 pub mod billboard;
 pub mod ui;
+pub mod ser;
 
 pub use dropbear_macro as macros;
 
diff --git a/crates/eucalyptus-core/src/ser/mod.rs b/crates/eucalyptus-core/src/ser/mod.rs
new file mode 100644
index 0000000..81adc73
--- /dev/null
+++ b/crates/eucalyptus-core/src/ser/mod.rs
@@ -0,0 +1,31 @@
+use std::fmt::Display;
+use std::string::ToString;
+
+pub mod model;
+
+pub enum SerializedType {
+    /// This is a `*.eucbin` file type.
+    GenericBinary,
+
+    /// This is a `*.eucmdl` file type.
+    Model,
+}
+
+impl Display for SerializedType {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let str = match self {
+            SerializedType::GenericBinary => "eucbin".to_string(),
+            SerializedType::Model => "eucmdl".to_string(),
+        };
+        write!(f, "{}", str)
+    }
+}
+
+impl SerializedType {
+    pub fn iter_extensions() -> impl Iterator<Item = String> {
+        [
+            Self::GenericBinary.to_string(),
+            Self::Model.to_string()
+        ].into_iter()
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/ser/model.rs b/crates/eucalyptus-core/src/ser/model.rs
new file mode 100644
index 0000000..a4135ff
--- /dev/null
+++ b/crates/eucalyptus-core/src/ser/model.rs
@@ -0,0 +1,315 @@
+use std::sync::Arc;
+use std::collections::hash_map::DefaultHasher;
+use std::hash::{Hash, Hasher};
+use dropbear_engine::asset::{Handle, ASSET_REGISTRY};
+use dropbear_engine::graphics::SharedGraphicsContext;
+use dropbear_engine::model::{AlphaMode, Animation, Material, Mesh, Model, ModelVertex, Node, Skin};
+use dropbear_engine::texture::{Texture, TextureWrapMode};
+use dropbear_engine::utils::{ResourceReference, ResourceReferenceType};
+use dropbear_engine::wgpu::util::DeviceExt;
+use dropbear_engine::wgpu;
+
+use crate::utils::ResolveReference;
+
+/// The serialized format for a Model without all the buffers and stuff.
+///
+/// This is stored in the file system as `*.eucmdl`.
+#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Debug, serde::Serialize, serde::Deserialize)]
+pub struct EucalyptusModel {
+    pub label: String,
+    pub meshes: Vec<EucalyptusMesh>, // this needs to be custom type because of wgpu buffers
+    pub materials: Vec<EucalyptusMaterial>, // same here
+    pub skins: Vec<Skin>,
+    pub animations: Vec<Animation>,
+    pub nodes: Vec<Node>,
+    pub morph_deltas: Vec<f32>,
+}
+
+impl EucalyptusModel {
+    fn runtime_hash(&self, source: &ResourceReference) -> u64 {
+        let mut hasher = DefaultHasher::default();
+        source.hash(&mut hasher);
+        self.label.hash(&mut hasher);
+        self.meshes.len().hash(&mut hasher);
+        self.materials.len().hash(&mut hasher);
+        self.nodes.len().hash(&mut hasher);
+
+        for mesh in &self.meshes {
+            mesh.name.hash(&mut hasher);
+            mesh.num_elements.hash(&mut hasher);
+            mesh.vertices.len().hash(&mut hasher);
+            mesh.material.hash(&mut hasher);
+        }
+
+        hasher.finish()
+    }
+
+    /// Loads the [`EucalyptusModel`] as a [`Model`] by loading the buffers.
+    pub fn load(&self, source: ResourceReference, graphics: Arc<SharedGraphicsContext>) -> Model {
+        let materials = self
+            .materials
+            .iter()
+            .map(|material| material.load(graphics.clone()))
+            .collect::<Vec<_>>();
+
+        let meshes = self
+            .meshes
+            .iter()
+            .map(|mesh| mesh.load(graphics.clone()))
+            .collect::<Vec<_>>();
+
+        let morph_deltas_buffer = if self.morph_deltas.is_empty() {
+            None
+        } else {
+            Some(graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                label: Some("model morph deltas buffer"),
+                contents: bytemuck::cast_slice(&self.morph_deltas),
+                usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+            }))
+        };
+
+        Model {
+            hash: self.runtime_hash(&source),
+            label: self.label.clone(),
+            path: source,
+            meshes,
+            materials,
+            skins: self.skins.clone(),
+            animations: self.animations.clone(),
+            nodes: self.nodes.clone(),
+            morph_deltas_buffer,
+        }
+    }
+}
+
+impl From<Model> for EucalyptusModel {
+    fn from(value: Model) -> Self {
+        Self {
+            label: value.label.clone(),
+            meshes: value.meshes.into_iter().map(EucalyptusMesh::from).collect(),
+            materials: value.materials.into_iter().map(EucalyptusMaterial::from).collect(),
+            skins: value.skins,
+            animations: value.animations,
+            nodes: value.nodes,
+            morph_deltas: Vec::new(),
+        }
+    }
+}
+
+#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Debug, serde::Serialize, serde::Deserialize)]
+pub struct EucalyptusMesh {
+    pub name: String,
+    pub num_elements: u32,
+    pub material: usize,
+    pub vertices: Vec<ModelVertex>,
+    pub morph_deltas_offset: u32,
+    pub morph_target_count: u32,
+    pub morph_vertex_count: u32,
+    pub morph_default_weights: Vec<f32>,
+}
+
+impl From<Mesh> for EucalyptusMesh {
+    fn from(value: Mesh) -> Self {
+        Self {
+            name: value.name,
+            num_elements: value.num_elements,
+            material: value.material,
+            vertices: value.vertices,
+            morph_deltas_offset: value.morph_deltas_offset,
+            morph_target_count: value.morph_target_count,
+            morph_vertex_count: value.morph_vertex_count,
+            morph_default_weights: value.morph_default_weights,
+        }
+    }
+}
+
+impl EucalyptusMesh {
+    fn load(&self, graphics: Arc<SharedGraphicsContext>) -> Mesh {
+        let vertex_buffer =
+            graphics
+                .device
+                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                    label: Some(&format!("{} Vertex Buffer", self.name)),
+                    contents: bytemuck::cast_slice(&self.vertices),
+                    usage: wgpu::BufferUsages::VERTEX,
+                });
+
+        let index_count = self.num_elements.min(self.vertices.len() as u32);
+        let indices = (0..index_count).collect::<Vec<u32>>();
+        let index_buffer =
+            graphics
+                .device
+                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
+                    label: Some(&format!("{} Index Buffer", self.name)),
+                    contents: bytemuck::cast_slice(&indices),
+                    usage: wgpu::BufferUsages::INDEX,
+                });
+
+        Mesh {
+            name: self.name.clone(),
+            vertex_buffer,
+            index_buffer,
+            num_elements: index_count,
+            material: self.material,
+            vertices: self.vertices.clone(),
+            morph_deltas_offset: self.morph_deltas_offset,
+            morph_target_count: self.morph_target_count,
+            morph_vertex_count: self.morph_vertex_count,
+            morph_default_weights: self.morph_default_weights.clone(),
+        }
+    }
+}
+
+#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize, Debug, serde::Serialize, serde::Deserialize)]
+pub struct EucalyptusMaterial {
+    pub name: String,
+    pub diffuse_texture: ResourceReference, // the file can either be a eucalyptus texture or a standard file
+    pub normal_texture: Option<ResourceReference>, // same
+    pub emissive_texture: Option<ResourceReference>, // same
+    pub metallic_roughness_texture: Option<ResourceReference>, // same
+    pub occlusion_texture: Option<ResourceReference>, // same
+    pub tint: [f32; 4],
+    pub emissive_factor: [f32; 3],
+    pub metallic_factor: f32,
+    pub roughness_factor: f32,
+    pub alpha_mode: AlphaMode,
+    pub alpha_cutoff: Option<f32>,
+    pub double_sided: bool,
+    pub occlusion_strength: f32,
+    pub normal_scale: f32,
+    pub uv_tiling: [f32; 2],
+    pub texture_tag: Option<String>,
+    pub wrap_mode: TextureWrapMode,
+}
+
+impl From<Material> for EucalyptusMaterial {
+    fn from(value: Material) -> Self {
+        let get_texture = |tex: Option<Handle<Texture>>| -> Option<ResourceReference> {
+            if let Some(tex) = tex {
+                if let Some(t) = ASSET_REGISTRY.read().get_texture(tex) {
+                    return t.reference.clone();
+                }
+            }
+
+            None
+        };
+
+        Self {
+            name: value.name,
+            diffuse_texture: get_texture(Some(value.diffuse_texture)).unwrap_or_default(),
+            normal_texture: get_texture(value.normal_texture),
+            emissive_texture: get_texture(value.emissive_texture),
+            metallic_roughness_texture: get_texture(value.metallic_roughness_texture),
+            occlusion_texture: get_texture(value.occlusion_texture),
+            tint: value.tint,
+            emissive_factor: value.emissive_factor,
+            metallic_factor: value.metallic_factor,
+            roughness_factor: value.roughness_factor,
+            alpha_mode: value.alpha_mode,
+            alpha_cutoff: value.alpha_cutoff,
+            double_sided: value.double_sided,
+            occlusion_strength: value.occlusion_strength,
+            normal_scale: value.normal_scale,
+            uv_tiling: value.uv_tiling,
+            texture_tag: value.texture_tag,
+            wrap_mode: value.wrap_mode,
+        }
+    }
+}
+
+impl EucalyptusMaterial {
+    fn load_texture(
+        &self,
+        graphics: Arc<SharedGraphicsContext>,
+        reference: &ResourceReference,
+        suffix: &str,
+    ) -> Option<Handle<Texture>> {
+        match &reference.ref_type {
+            ResourceReferenceType::None => None,
+            ResourceReferenceType::File(_) => {
+                let path = reference.resolve().ok()?;
+                let bytes = std::fs::read(path).ok()?;
+                let label = format!("{}_{}", self.name, suffix);
+                let mut texture = dropbear_engine::texture::TextureBuilder::new(&graphics.device)
+                    .from_bytes(graphics.clone(), bytes.as_slice())
+                    .label(label.as_str())
+                    .build();
+                texture.reference = Some(reference.clone());
+
+                let mut registry = ASSET_REGISTRY.write();
+                Some(registry.add_texture(texture))
+            }
+            ResourceReferenceType::Bytes(bytes) => {
+                let label = format!("{}_{}", self.name, suffix);
+                let texture = dropbear_engine::texture::TextureBuilder::new(&graphics.device)
+                    .from_bytes(graphics.clone(), bytes)
+                    .label(label.as_str())
+                    .build();
+
+                let mut registry = ASSET_REGISTRY.write();
+                Some(registry.add_texture(texture))
+            }
+            ResourceReferenceType::ProcObj(_) => None,
+        }
+    }
+
+    fn load(&self, graphics: Arc<SharedGraphicsContext>) -> Material {
+        let diffuse_texture = {
+            let maybe = self.load_texture(graphics.clone(), &self.diffuse_texture, "diffuse");
+            if let Some(handle) = maybe {
+                handle
+            } else {
+                ASSET_REGISTRY.write().solid_texture_rgba8(
+                    graphics.clone(),
+                    [255, 255, 255, 255],
+                    Some(Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix()),
+                )
+            }
+        };
+
+        let normal_texture = self
+            .normal_texture
+            .as_ref()
+            .and_then(|reference| self.load_texture(graphics.clone(), reference, "normal"));
+        let emissive_texture = self
+            .emissive_texture
+            .as_ref()
+            .and_then(|reference| self.load_texture(graphics.clone(), reference, "emissive"));
+        let metallic_roughness_texture = self
+            .metallic_roughness_texture
+            .as_ref()
+            .and_then(|reference| self.load_texture(graphics.clone(), reference, "metallic_roughness"));
+        let occlusion_texture = self
+            .occlusion_texture
+            .as_ref()
+            .and_then(|reference| self.load_texture(graphics.clone(), reference, "occlusion"));
+
+        let mut registry = ASSET_REGISTRY.write();
+        let mut material = Material::new(
+            &mut registry,
+            graphics.clone(),
+            self.name.clone(),
+            diffuse_texture,
+            normal_texture,
+            emissive_texture,
+            metallic_roughness_texture,
+            occlusion_texture,
+            self.tint,
+            self.texture_tag.clone(),
+        );
+
+        material.emissive_factor = self.emissive_factor;
+        material.metallic_factor = self.metallic_factor;
+        material.roughness_factor = self.roughness_factor;
+        material.alpha_mode = self.alpha_mode;
+        material.alpha_cutoff = self.alpha_cutoff;
+        material.double_sided = self.double_sided;
+        material.occlusion_strength = self.occlusion_strength;
+        material.normal_scale = self.normal_scale;
+        material.uv_tiling = self.uv_tiling;
+        material.wrap_mode = self.wrap_mode;
+
+        material.sync_uniform(&graphics);
+        material
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/utils.rs b/crates/eucalyptus-core/src/utils.rs
index 70db39e..728a614 100644
--- a/crates/eucalyptus-core/src/utils.rs
+++ b/crates/eucalyptus-core/src/utils.rs
@@ -4,9 +4,13 @@ pub mod option;
 
 use crate::scripting::result::DropbearNativeResult;
 use crate::states::Node;
+use crate::ser::model::{EucalyptusMaterial, EucalyptusMesh, EucalyptusModel};
 use dropbear_engine::utils::{ResourceReference, ResourceReferenceType, relative_path_from_euca};
 use jni::JNIEnv;
 use jni::objects::{JObject, JValue};
+use std::collections::hash_map::DefaultHasher;
+use std::fs;
+use std::hash::{Hash, Hasher};
 use std::path::{Path, PathBuf};
 use std::time::Duration;
 use winit::keyboard::KeyCode;
@@ -301,6 +305,154 @@ impl ResolveReference for ResourceReference {
     }
 }
 
+/// Converts a [`ResourceReference`] into a file. 
+pub trait AsFile {
+    /// Converts a [`ResourceReference`] into a file. 
+    /// 
+    /// # Different type's behaviours
+    /// - [`ResourceReferenceType::None`] => This just returns an error as it's impossible. 
+    /// - [`ResourceReferenceType::File`] => Returns that file resolved. 
+    /// - [`ResourceReferenceType::Bytes`] => Converts the bytes into a `*.eucbin`, with the name derived from `new_ref` (arg). 
+    /// - [`ResourceReferenceType::ProcObj`] => Converts the vertices into a `*.eucmdl`, with the name derived from `new_ref` (arg). 
+    fn as_file(&self, new_ref: Option<String>) -> anyhow::Result<PathBuf>;
+}
+
+impl AsFile for ResourceReference {
+    fn as_file(&self, new_ref: Option<String>) -> anyhow::Result<PathBuf> {
+        match &self.ref_type {
+            ResourceReferenceType::None => {
+                anyhow::bail!("Cannot convert ResourceReferenceType::None to a file")
+            }
+            ResourceReferenceType::File(_) => {
+                let resolved = self.resolve()?;
+                if let Some(relative) = new_ref {
+                    let root = resources_root_for_write()?;
+                    let out_path = root.join(relative.trim_start_matches('/'));
+                    if let Some(parent) = out_path.parent() {
+                        fs::create_dir_all(parent)?;
+                    }
+                    Ok(out_path)
+                } else {
+                    Ok(resolved)
+                }
+            }
+            ResourceReferenceType::Bytes(bytes) => {
+                let hash = hash_value(bytes);
+                let root = resources_root_for_write()?;
+                let out_path = root
+                    .join("gen")
+                    .join(format!("{hash:016x}.eucbin"));
+
+                if let Some(parent) = out_path.parent() {
+                    fs::create_dir_all(parent)?;
+                }
+                fs::write(&out_path, bytes.as_ref())?;
+
+                Ok(out_path)
+            }
+            ResourceReferenceType::ProcObj(obj) => {
+                let hash = hash_value(obj);
+                let out_path = if let Some(relative) = new_ref.as_ref() {
+                    let root = resources_root_for_write()?;
+                    root.join(relative.trim_start_matches('/'))
+                } else {
+                    let root = resources_root_for_write()?;
+                    root.join("gen")
+                        .join(format!("{hash:016x}.eucmdl"))
+                };
+
+                let label_stem = out_path
+                    .file_stem()
+                    .and_then(|stem| stem.to_str())
+                    .unwrap_or("procedural_model")
+                    .to_string();
+
+                let mut expanded_vertices = Vec::with_capacity(obj.indices.len());
+                for &index in &obj.indices {
+                    let vertex = obj
+                        .vertices
+                        .get(index as usize)
+                        .ok_or_else(|| anyhow::anyhow!(
+                            "Procedural object index {} is out of bounds for {} vertices",
+                            index,
+                            obj.vertices.len()
+                        ))?
+                        .clone();
+                    expanded_vertices.push(vertex);
+                }
+
+                let model = EucalyptusModel {
+                    label: label_stem.clone(),
+                    meshes: vec![EucalyptusMesh {
+                        name: format!("{label_stem}_mesh"),
+                        num_elements: expanded_vertices.len() as u32,
+                        material: 0,
+                        vertices: expanded_vertices,
+                        morph_deltas_offset: 0,
+                        morph_target_count: 0,
+                        morph_vertex_count: obj.indices.len() as u32,
+                        morph_default_weights: Vec::new(),
+                    }],
+                    materials: vec![EucalyptusMaterial {
+                        name: "procedural_material".to_string(),
+                        diffuse_texture: ResourceReference::default(),
+                        normal_texture: None,
+                        emissive_texture: None,
+                        metallic_roughness_texture: None,
+                        occlusion_texture: None,
+                        tint: [1.0, 1.0, 1.0, 1.0],
+                        emissive_factor: [0.0, 0.0, 0.0],
+                        metallic_factor: 1.0,
+                        roughness_factor: 1.0,
+                        alpha_mode: dropbear_engine::model::AlphaMode::Opaque,
+                        alpha_cutoff: None,
+                        double_sided: false,
+                        occlusion_strength: 1.0,
+                        normal_scale: 1.0,
+                        uv_tiling: [1.0, 1.0],
+                        texture_tag: Some("procedural_material".to_string()),
+                        wrap_mode: dropbear_engine::texture::TextureWrapMode::Repeat,
+                    }],
+                    skins: Vec::new(),
+                    animations: Vec::new(),
+                    nodes: Vec::new(),
+                    morph_deltas: Vec::new(),
+                };
+
+                if let Some(parent) = out_path.parent() {
+                    fs::create_dir_all(parent)?;
+                }
+
+                let serialized = rkyv::to_bytes::<rkyv::rancor::Error>(&model)
+                    .map_err(|e| anyhow::anyhow!("Failed to serialize proc object model as rkyv bytes: {e}"))?;
+                fs::write(&out_path, serialized.as_ref())?;
+
+                Ok(out_path)
+            }
+        }
+    }
+}
+
+fn hash_value<T: Hash>(value: &T) -> u64 {
+    let mut hasher = DefaultHasher::new();
+    value.hash(&mut hasher);
+    hasher.finish()
+}
+
+fn resources_root_for_write() -> anyhow::Result<PathBuf> {
+    #[cfg(feature = "editor")]
+    {
+        use crate::states::PROJECT;
+
+        let project_path = PROJECT.read().project_path.clone();
+        if !project_path.as_os_str().is_empty() {
+            return Ok(project_path.join("resources"));
+        }
+    }
+
+    runtime_resources_dir()
+}
+
 fn try_resolve_resource_from_root(relative: &str, root: &Path) -> Option<PathBuf> {
     let resolved = root.join(relative);
     resolved.exists().then_some(resolved)
diff --git a/crates/eucalyptus-editor/Cargo.toml b/crates/eucalyptus-editor/Cargo.toml
index 8857b34..8fa25b2 100644
--- a/crates/eucalyptus-editor/Cargo.toml
+++ b/crates/eucalyptus-editor/Cargo.toml
@@ -57,6 +57,8 @@ serde.workspace = true
 bitflags.workspace = true
 downcast-rs.workspace = true
 puffin.workspace = true
+image.workspace = true
+rkyv.workspace = true
 
 [target.'cfg(unix)'.dependencies]
 daemonize = "0.5.0"
diff --git a/crates/eucalyptus-editor/src/editor/asset_viewer.rs b/crates/eucalyptus-editor/src/editor/asset_viewer.rs
index ce8fbb7..bd3069d 100644
--- a/crates/eucalyptus-editor/src/editor/asset_viewer.rs
+++ b/crates/eucalyptus-editor/src/editor/asset_viewer.rs
@@ -3,6 +3,7 @@ use dropbear_engine::model::Model;
 use dropbear_engine::texture::TextureBuilder;
 use dropbear_engine::{graphics::NO_TEXTURE, utils::ResourceReference};
 use egui_ltreeview::{Action, NodeBuilder, TreeViewBuilder};
+use eucalyptus_core::ser::model::EucalyptusModel;
 use eucalyptus_core::states::PROJECT;
 use eucalyptus_core::utils::ResolveReference;
 use hecs::Entity;
@@ -196,6 +197,13 @@ impl<'a> EditorTabViewer<'a> {
                 );
                 let is_model = Self::is_model_file(&entry.name);
                 let is_texture = Self::is_texture_file(&entry.name);
+                let ext = entry
+                    .path
+                    .extension()
+                    .and_then(|v| v.to_str())
+                    .map(|v| v.to_ascii_lowercase());
+                let is_eucbin = matches!(ext.as_deref(), Some("eucbin"));
+                let is_eucmdl = matches!(ext.as_deref(), Some("eucmdl"));
                 let entry_name = entry.name.clone();
                 let reference_for_menu = reference.clone();
                 let file_info = AssetNodeInfo {
@@ -213,8 +221,8 @@ impl<'a> EditorTabViewer<'a> {
                         self.asset_file_context_menu(cfg, ui, node_id, &file_info);
                         ui.separator();
 
-                        if is_model {
-                            if ui.button("Load to memory").clicked() {
+                        if is_model || is_eucmdl {
+                            if ui.button("Load model to memory").clicked() {
                                 ui.close();
                                 self.queue_model_load(
                                     reference_for_menu.clone(),
@@ -225,15 +233,37 @@ impl<'a> EditorTabViewer<'a> {
                         }
 
                         if is_texture {
-                            if ui.button("Load to memory").clicked() {
+                            if ui.button("Load texture to memory").clicked() {
                                 ui.close();
                                 self.queue_texture_load(
                                     reference_for_menu.clone(),
                                     entry_name.clone(),
+                                    false,
                                 );
                                 info!("Loading texture {}", entry_name);
                             }
                         }
+
+                        if is_eucbin {
+                            if ui.button("Load as model to memory").clicked() {
+                                ui.close();
+                                self.queue_model_load(
+                                    reference_for_menu.clone(),
+                                    entry_name.clone(),
+                                );
+                                info!("Loading eucbin as model {}", entry_name);
+                            }
+
+                            if ui.button("Load as texture to memory").clicked() {
+                                ui.close();
+                                self.queue_texture_load(
+                                    reference_for_menu.clone(),
+                                    entry_name.clone(),
+                                    true,
+                                );
+                                info!("Loading eucbin as texture {}", entry_name);
+                            }
+                        }
                     });
                 builder.node(menu);
             }
@@ -1208,20 +1238,39 @@ impl<'a> EditorTabViewer<'a> {
         queue.push(async move {
             let path = reference.resolve()?;
             let buffer = fs::read(&path)?;
-            let handle = match Model::load_from_memory_raw(
-                graphics.clone(),
-                buffer,
-                Some(reference.clone()),
-                Some(label.as_str()),
-                ASSET_REGISTRY.clone(),
-            )
-            .await
-            {
-                Ok(v) => v,
-                Err(e) => {
-                    eucalyptus_core::warn!("Unable to load model {}: {}", reference, e);
-                    return Err(e);
+            let extension = path
+                .extension()
+                .and_then(|ext| ext.to_str())
+                .map(|ext| ext.to_ascii_lowercase());
+
+            let handle = match extension.as_deref() {
+                Some("eucmdl") => {
+                    let model = rkyv::from_bytes::<EucalyptusModel, rkyv::rancor::Error>(&buffer)
+                        .map_err(|e| anyhow::anyhow!(
+                            "Unable to deserialize .eucmdl model '{}': {}",
+                            path.display(),
+                            e
+                        ))?;
+
+                    let runtime_model = model.load(reference.clone(), graphics.clone());
+                    let mut registry = ASSET_REGISTRY.write();
+                    registry.add_model_with_label(label.clone(), runtime_model)
                 }
+                _ => match Model::load_from_memory_raw(
+                    graphics.clone(),
+                    buffer,
+                    Some(reference.clone()),
+                    Some(label.as_str()),
+                    ASSET_REGISTRY.clone(),
+                )
+                .await
+                {
+                    Ok(v) => v,
+                    Err(e) => {
+                        eucalyptus_core::warn!("Unable to load model {}: {}", reference, e);
+                        return Err(e);
+                    }
+                },
             };
 
             let mut registry = ASSET_REGISTRY.write();
@@ -1231,7 +1280,7 @@ impl<'a> EditorTabViewer<'a> {
         });
     }
 
-    fn queue_texture_load(&self, reference: ResourceReference, label: String) {
+    fn queue_texture_load(&self, reference: ResourceReference, label: String, strict_image_decode: bool) {
         if ASSET_REGISTRY
             .read()
             .get_texture_handle_by_reference(&reference)
@@ -1246,6 +1295,19 @@ impl<'a> EditorTabViewer<'a> {
         queue.push(async move {
             let path = reference.resolve()?;
             let bytes = fs::read(&path)?;
+
+            if strict_image_decode
+                && let Err(err) = image::load_from_memory(bytes.as_slice())
+            {
+                let error = anyhow::anyhow!(
+                    "'{}' is not a texture-compatible eucbin payload: {}",
+                    path.display(),
+                    err
+                );
+                eucalyptus_core::warn!("{}", error);
+                return Err(error);
+            }
+
             let mut texture = TextureBuilder::new(&graphics.device)
                 .from_bytes(graphics.clone(), bytes.as_slice())
                 .label(label.as_str())