0

I'm trying to learn Rust and have been building a raytracer as a beginner project to get my hands dirty with the language. I've built my raytracer following Peter Shirley's Ray Tracing in One Weekend The book is written in C++, I've tried my best to port it to Rust, and admittedly the code is probably not written in a Rust-like manner.

I've got the raytracer working, and I'm currently trying to implement a scene-saving system, where you save a scene as a JSON file, with various information about the scene's camera, objects, etc.

I'm running into a problem when it comes to using serde to deserialize JSON of a scene so it can be loaded into the program. I serealize in a simple Python script, where the output looks like this, saved to scene.json:

{
    "aspect_ratio": 1.5,
    "image_width": 1200,
    "image_height": 800,
    "samples_per_pixel": 500,
    "camera": {
        "origin": {
            "vec": [
                13.0,
                2.0,
                3.0
            ]
        },
        "lower_left_corner": {
            "vec": [
                3.0112371,
                -1.1992972,
                2.6938112
            ]
        },
        "horizontal": {
            "vec": [
                1.5556582,
                0,
                -5.055889
            ]
        },
        "vertical": {
            "vec": [
                -0.49034908,
                3.4890225,
                -0.15087664
            ]
        },
        "u": {
            "vec": [
                0.29408586,
                0,
                -0.955779
            ]
        },
        "v": {
            "vec": [
                -0.13904539,
                0.9893614,
                -0.042783197
            ]
        },
        "w": {
            "vec": [
                0.9456108,
                0.14547859,
                0.29095718
            ]
        },
        "radius": 0.5
    },
    "world": [
        {
            "center": {
                "vec": [
                    0,
                    -1000,
                    0
                ]
            },
            "r": 1000,
            "material": {
                "type": "lambertian",
                "albedo": [
                    0.5,
                    0.5,
                    0.5
                ]
            }
        },
        {
            "center": {
                "vec": [
                    0.3712137,
                    0.2,
                    1.5109301
                ]
            },
            "r": 0.2,
            "material": {
                "type": "metal",
                "albedo": [
                    0.66978514,
                    0.9735459,
                    0.52093863
                ],
                "fuzziness": 0.045185298
            }
        },
        .
        .
        .
    ]
}

Currently there's only sphere's in my raytracer, so the only thing that differs between two Renderables in "world" is "material", which I have LambertianMaterial, Metal, and Dielectric defined as so:

pub struct LambertianMaterial { 
    albedo: Color
}

pub struct Metal {
    albedo: Color,
    fuzziness: f32
}

pub struct Dielectric {
    // index of refraction (')
    ir: f32
}

Deseralizing metadata like the "camera" and "aspect_ratio", "image_width, etc, is perfectly fine. My issue is with the "world" field, as its a list that contains various spheres that all use the Renderable trait, which I have defined as:

pub trait Renderable: fmt::Display {
    fn hit (&self, ray: &Ray, t_min: f32, t_max: f32) -> (bool, HitRecord);
}

Within the raytracer these spheres are rendered via looping through another struct that contains a vec of Renderables, called RenderableList, defined as:

pub struct RenderableList {
    objects: Vec<Rc<dyn Renderable>>
}

However, trying to deserialize a RenderableList from JSON using serde is proving difficult. Simply slapping a #[derive(Debug, Deserialize)] on top of the struct definition obviously doesn't solve it.

I tried reading through this post on serializing Arc<Mutex>, but it's more on the serialization side, and not the deserialization.

I tried reading through the docs for serde about implementing a deserializer, but I think that's more complicated and overkill than what I actually need.

My initial stab at this was to do the following:

#[derive(Deserialize, Debug)]
struct Scene {
    aspect_ratio: f32,
    image_width: i32,
    image_height: i32,
    samples_per_pixel: i32,
    camera: Camera,
    world: RenderableList,
}

fn load_scene<P: AsRef<Path>>(path: P) -> Result<Scene, Box<dyn Error>> {
    // Open the file in read-only mode with buffer.
    let file = File::open(path)?;
    let reader = BufReader::new(file);

    // Read the JSON contents of the file as an instance of `Scene`.
    let s = serde_json::from_reader(reader)?;

    // Return the `Scene`.
    Ok(s)
}

But as stated previously, this raises errors when trying to make RenderableList deserializable with #[derive(Debug, Deserialize)]. Specifically, I get:

the trait bound `Rc<dyn Renderable>: Deserialize<'_>` is not satisfied
the following other types implement trait `Deserialize<'de>`:
  &'a Path
  &'a [u8]
  &'a str
  ()
  (T0, T1)
  (T0, T1, T2)
  (T0, T1, T2, T3)
  (T0, T1, T2, T3, T4)
and 131 others
required for `Vec<Rc<dyn Renderable>>` to implement `Deserialize<'_>`

Would anyone happen to know how to work around this?

lmseper
  • 1
  • 1
  • Have you tried [typetag](https://docs.rs/typetag/latest/typetag/) yet? This seems like the exact use case it's designed for. – apetranzilla Jun 16 '23 at 19:47

1 Answers1

1

The most straightforward way to solve this is to use an enum instead of a trait object.

#[derive(Debug, Deserialize)]
#[serde(untagged)] // since your current JSON is untagged
pub enum Object {
    Circle(Circle),
}

#[derive(Debug, Deserialize)]
pub struct RenderableList {
    objects: Vec<Rc<Object>>
}

impl Renderable for Object {
    fn hit (&self, ray: &Ray, t_min: f32, t_max: f32) -> (bool, HitRecord) {
        match self {
            Object::Circle(c) => c.hit(ray, t_min, t_max)
        }
    }
}

Now when you add a new object you would:

  • Make a type for that object.
  • Add a variant to Object
  • Implement Renderable for the object.
  • Add a branch to the match expression.

You can do the same for the materials.

There isn't really a way to retrieve a list of all implementers of a trait, which you would need to do to deserialize a trait object. There isn't a built-in way to retrieve a list of enum variants either, but the serde derive macro handles that. Enums are also usually faster than trait objects.

I did find this crate that can do it for trait objects, but I haven't tried it: typetag.

Also look at the serde guide chapter on enums.

drewtato
  • 6,783
  • 1
  • 12
  • 17
  • Oh interesting! I didn’t know you could implement traits for enums in Rust. That’s so cool! Typetag looks like my exact use case as well, so I’ll try both of these solutions out. Thank you so much, I’lll report back my results! – lmseper Jun 18 '23 at 01:42