Please review this small piece of code

Trying to follow this tutorial https://raytracing.github.io/books/RayTracingInOneWeekend.html but in Rust.
Basically on chapter 1, got stuck with trying to implement simple Ray.
Decided against reimplementing vector, and just use vec3 crate.
Wanted my Ray to be generic, spend way too much time on making the following code work.
And i'm still HATING it.

Please, if there is a better way to write a generic Ray class, to use generic vec3, let me know.
Also if you could explain to me, why it works, it would be awesome too.
Although i eventually wrote the following code, i don't exactly understand the where clause.
Thanks.

pub struct Ray<T> {
    origin: [T; 3],
    dir: [T; 3],
}

impl<T> Ray<T>
where
    for<'a, 'b> &'a T: std::ops::Mul<&'b T, Output = T>,
    for<'a> T: std::ops::AddAssign<&'a T>,
    T: num_traits::identities::Zero,
{
    pub fn at(&self, t: T) -> [T; 3] {
        let mut out: [T; 3] = vec3::new_zero();
        vec3::smul(&mut out, &self.dir, &t);
        vec3::add_mut(&mut out, &self.origin);
        out
    }

    pub fn new(origin: [T; 3], dir: [T; 3]) -> Ray<T> {
        return Ray { origin, dir };
    }
}

[SPOILER] If you get desperate you might look at how another Rustacean addressed the problem:
Peter Shirley's Ray Tracing In One Weekend implementation in Rust

1 Like

A few things.

It looks like the vec3 crate does all calculations by reference, which is why you need all the lifetimes specified. For something like [T; 3] where T is a f64 or f32, the calculation code cany be made simpler by taking everything by value and just make sure it implements the Copy trait (so that you don't need to worry about ownership). If done that way, you can get rid of all the references and lifetimes in the code.

A common trick with a bunch of trait bounds is to move them all into to one trait and then just use that trait. Here that would be. It doesn't save much trouble here, but its particularly useful to know about if you're doing math operations.

trait RayValue:
    for<'a, 'b> std::ops::Mul<&'b Self, Output = Self>
    + for<'a> std::ops::AddAssign<&'a Self>
    + num_traits::identities::Zero
{
}

impl<T> Ray<T> where T: RayValue,
{
 ...
}

Regarding the traits themselves, given the constraint of having all the calculations by reference, the definition looks fine. Basically what the for<'a,'b> is doing is making sure that the trait is valid for all combinations of lifetimes for the two arguments to the multiply function.

Since you're just learning rust, you might try just implementing your own vector operations as a learning experience rather than tying yourself to another implementation. Here's an example of just implementing the vec yourself. Obviously you need to keep adding implementations for the various math operations you need, but I found it useful in the past to understand how Rust traits work. The code below would be a starting point for that. You can see the implementation of at() gets a lot simpler with a more friendly version of Vec3

#[derive(Clone, Copy)]
pub struct Vec3<T> {
    x: T,
    y: T,
    z: T,
}

impl<T> std::ops::Add<Self> for Vec3<T> where T: num_traits::Float{
    type Output = Self;
    fn add(self, other: Self) -> Self {
        Vec3 {
            x: self.x + other.x,
            y: self.y + other.y,
            z: self.z + other.z,
        }
    }
}

impl<T> std::ops::Mul<T> for Vec3<T> where T: num_traits::Float {
    type Output = Self;
    fn mul(self, other: T) -> Self {
        Vec3 {
            x: self.x * other,
            y: self.y * other,
            z: self.z * other,
        }
    }
}

impl<T> num_traits::Zero for Vec3<T> where T: num_traits::Float {
    fn zero() -> Self {
        Vec3{x:T::zero(), y:T::zero(), z:T::zero()}
    }
    fn is_zero(&self) -> bool {
        self.x == T::zero() && self.y == T::zero() && self.z == T::zero()
    }
}

pub struct Ray<T> {
    origin: Vec3<T>,
    dir: Vec3<T>,
}

impl<T> Ray<T>
where
    T: num_traits::Float,
{
    pub fn at(&self, t: T) -> Vec3<T> {
        self.origin + self.dir * t
    }

    pub fn new(origin: Vec3<T>, dir: Vec3<T>) -> Ray<T> {
        return Ray { origin, dir };
    }
}
2 Likes

Wow, thanks, good to know there are so many rust implementations already. Now i definitely not going to look at them before i complete my own. It will be much more fun to compare results later.

Wow thanks, wasn't expecting such broad and elaborate answer. Learned a lot today.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.