2

I have been writing a Ray Caster in Rust following "The Ray Tracer Challenge", and I've been having a hard time figuring out the proper way to implement polymorphism in Rust. My priorities are that the object can be used in a multi-threaded program, and that seems to be the main problem.

I have two cases on this, but I'll focus on one: a shape. There are different kinds of shapes (sticking with the able suffix I originally called my trait Intersectable). Here was a working trait object implementation, but it didn't work with multi-threading:

#[derive(Debug)]
pub struct Shape {
    pub parent: Option<Arc<Shape>>,
    pub transform: Matrix4,
    pub material: Material,
    pub intersectable: Box<Intersectable>,
}
pub trait Intersectable: Debug + IntersectableClone {
    fn local_intersect(&self, ray: &Ray, object: Arc<Shape>) -> Vec<Intersection>;
}

pub trait IntersectableClone {
    fn clone_box(&self) -> Box<Intersectable>;
}

impl<T> IntersectableClone for T
where
    T: 'static + Intersectable + Clone,
{
    fn clone_box(&self) -> Box<Intersectable> {
        Box::new(self.clone())
    }
}

impl Clone for Box<Intersectable> {
    fn clone(&self) -> Box<Intersectable> {
        self.clone_box()
    }
}

#[derive(Clone, Debug)]
pub struct Sphere {}

impl Intersectable for Sphere {
    fn local_intersect(&self, ray: &Ray, object: Arc<Shape>) -> Vec<Intersection> {
        ...sphere specific code
    }
}

#[derive(Clone, Debug)]
pub struct Plane {}

impl Intersectable for Plane {
    fn local_intersect(&self, ray: &Ray, object: Arc<Shape>) -> Vec<Intersection> {
        ...plane specific code
    }
}

As a pure struct, with no official polymorphism, I've written a kind of static dispatch that looks like this:

#[derive(Debug, Clone)]
pub enum IntersectableType {
    Sphere,
    Plane,
}

#[derive(Debug, Clone)]
pub struct Intersectable {
    intersectable_type: IntersectableType,
}

impl Intersectable {
    pub fn local_intersect(&self, ray: &Ray, object: Arc<Shape>) -> Vec<Intersection> {
        match self.intersectable_type {
            IntersectableType::Sphere => self.local_intersect_sphere(ray, object),
            IntersectableType::Plane => self.local_intersect_plane(ray, object),
            _ => Vec::new(),
        }
    }

    fn local_intersect_sphere(&self, ray: &Ray, object: Arc<Shape>) -> Vec<Intersection> {
        ...sphere specific code
    }

    fn local_intersect_plane(&self, ray: &Ray, object: Arc<Shape>) -> Vec<Intersection> {
        ...plane specific implementation
    }
}

This works great, but it feels very non-Rusty. I've hit a few problems with using other implementations: - Using Box<Intersectable> (when it was a Trait, not a struct), is difficult to clone (I copied How to clone a struct storing a boxed trait object? but didn't love having to use 'static, since that made concurrency impossible). - Using Arc<Intersectable> seemed to have the same problems as Box, though maybe there is a way to make that work.

Is there a way to do this in Rust that allows me to take advantage of concurrency and not write manual static dispatch like this?

Josh
  • 95
  • 10
  • Would you be able to show functional examples of the trait approach you've tried so far, so we can understand the issues you had? It'll be a lot easier than us trying to start from scratch. – loganfsmyth Mar 09 '19 at 01:25
  • 1
    Yes, I have added the main implementation I had before this one! – Josh Mar 09 '19 at 01:46
  • Perhaps you could just store a function pointer `fn(&Ray, Arc) -> Vec` in your Shape. I believe `fn` is `Clone + Send`. It's essentially what you're already doing with dynamic dispatch on empty structs. – JayDepp Mar 09 '19 at 02:52
  • That is a very interesting concept that I hadn't thought about, I will give it a shot and report back! – Josh Mar 09 '19 at 06:17
  • Ha, I'm there as well now! But a bit less advanced. Struggling with how to extract a shape out of my spheres. – schmijos Dec 20 '20 at 20:33

1 Answers1

0

I'm also working on "The Ray Tracer Challenge", a bit further behind than you though. I have no concurrency in mind. I now try to follow the idea of the A-Frame ECS:

pub trait Primitive {}

pub struct Shape<T: Primitive> {
    pub transformation: M4x4,
    pub material: Material,
    pub primitive: T,
}

pub struct Sphere;
impl Primitive for Sphere {}
schmijos
  • 8,114
  • 3
  • 50
  • 58