For context: I'm writing a ray tracer in Rust but I'm struggling with finding a good way to load the scene in a filesystem-agnostic way. I'm using serde so that I don't have to invent my own file format (yet). The assets (image textures and mesh data) are stored separately to the scene file. The scene file only stores the paths of these files. Because the ray tracer itself is supposed to be a platform-agnostic library (I want to be able to compile it to WebAssembly for the Browser) the ray tracer itself has no idea about the file system. I intend to load the assets when deserializing the scene but this is causing me real problems now:
I need to pass an implementation of the file system interfacing code to serde that I can use in Deserialize::deserialize()
but there doesn't seem to be any easy way to do that. I came up with a way to do it with generics, but I'm not happy about it.
Here's the way I'm doing it at the moment, stripped down as an MCVE (packages used are serde
and serde_json
):
The library code (lib.rs):
use std::marker::PhantomData;
use serde::{Serialize, Serializer, Deserialize, Deserializer};
pub struct Image {}
pub struct Texture<L: AssetLoader> {
path: String,
image: Image,
phantom: PhantomData<L>,
}
impl<L: AssetLoader> Serialize for Texture<L> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.path.serialize(serializer)
}
}
impl<'de, L: AssetLoader> Deserialize<'de> for Texture<L> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Texture<L>, D::Error> {
let path = String::deserialize(deserializer)?;
// This is where I'd much rather have an instance of AssetLoader
let image = L::load_image(&path);
Ok(Texture {
path,
image,
phantom: PhantomData,
})
}
}
pub trait AssetLoader {
fn load_image(path: &str) -> Image;
// load_mesh(), load_hdr(), ...
}
#[derive(Serialize, Deserialize)]
pub struct Scene<L: AssetLoader> {
textures: Vec<Texture<L>>,
// meshes, materials, lights, ...
}
The platform-specific code (main.rs):
use serde::{Serialize, Deserialize};
use assetloader_mcve::{AssetLoader, Image, Scene};
#[derive(Serialize, Deserialize)]
struct AssetLoaderImpl {}
impl AssetLoader for AssetLoaderImpl {
fn load_image(path: &str) -> Image {
println!("Loading image: {}", path);
// Load the file from disk, the web, ...
Image {}
}
}
fn main() {
let scene_str = r#"
{
"textures": [
"texture1.jpg",
"texture2.jpg"
]
}
"#;
let scene: Scene<AssetLoaderImpl> = serde_json::from_str(scene_str).unwrap();
// ...
}
What I don't like about this approach:
AssetLoaderImpl
has to implementSerialize
andDeserialize
even though it's never (de-)serialized- I'm also using typetag which causes a compilation error because "deserialization of generic impls is not supported yet"
- Caching assets will be very difficult because I don't have an instance of
AssetLoaderImpl
which could cache them in a member variable - Passing the
AssetLoader
type parameter around is getting unwieldy whenTexture
(or other assets) are nested deeper - It just doesn't feel right, mostly because of the
PhantomData
and the abuse of generics
This makes me think that I'm not going about this the right way but I'm struggling to come up with a better solution. I thought about using a mutable global variable in the library holding an instance of AssetLoader
(maybe with lazy_static
) but that also doesn't seem right. Ideally I'd pass an instance of AssetLoader
(Box<dyn AssetLoader>
probably) to serde when deserializing that I can access in the impl Deserialize for Texture
. I haven't found any way to do that and I'd really appreciate if anybody could point me in the right direction.