I'm trying to follow the Raytracing in One Weekend book, and I found that he uses the same Vec3
class for everything - colors, coordinates, direction, and more:
struct Vec3 { float x, y, z };
He also overloads every possible operator of the Vec3
class to make it convenient to use, and adds plenty of member functions (length
, distance_to
, etc). While this definitely reduces the amount of code one has to write, it isn't terribly safe, and allows for things doesn't make sense (why would you want a distance_to
between two Color
s?). I was trying to fix that.
I defined a base Vector type:
struct Vector {
Vector(float, float, float);
float x;
float y;
float z;
float length() const;
};
According to the Wikipedia page, there can be 2 types of vectors - bound vectors (goes from point A to point B) and free vectors (the particular point is of no significance, only the magnitude and direction are). I defined these types too, but with very limited operators.
Notice how you can add a FreeVector
to a BoundVector
to give a BoundVector
, but you cannot add 2 BoundVectors
:
struct BoundVector : public Vector {
BoundVector(float, float, float);
BoundVector& operator+=(const FreeVector&);
BoundVector& operator-=(const FreeVector&);
FreeVector operator-(const BoundVector&) const;
};
struct FreeVector : public Vector {
FreeVector(float, float, float);
explicit FreeVector(const UnitVector&);
FreeVector& operator+=(const FreeVector&);
FreeVector& operator-=(const FreeVector&);
FreeVector& operator*=(float);
FreeVector& operator/=(float);
UnitVector unit() const;
float dot(const FreeVector&) const;
};
BoundVector operator+(BoundVector, const FreeVector&);
BoundVector operator-(BoundVector, const FreeVector&);
FreeVector operator+(FreeVector, const FreeVector&);
FreeVector operator-(FreeVector, const FreeVector&);
FreeVector operator*(FreeVector, float);
FreeVector operator/(FreeVector, float);
I would have liked the UnitVector
class to extend the FreeVector
(or even the Vector
) class, but I can't, since the implementation is completely different. For starters, to ensure that it is guaranteed to be a unit vector (x*x + y*y + z*z == 1
) I have to make all members const
:
struct UnitVector {
UnitVector(float, float, float);
explicit UnitVector(const FreeVector&);
const float x;
const float y;
const float z;
FreeVector operator*(float) const;
FreeVector operator/(float) const;
private:
UnitVector(float, float, float, float);
};
UnitVector::UnitVector(const float x, const float y, const float z)
: UnitVector(x, y, z, std::sqrt(x * x + y * y + z * z)) {}
UnitVector::UnitVector(const FreeVector& v) : UnitVector(v.x, v.y, v.z) {}
FreeVector UnitVector::operator*(const float k) const {
return FreeVector(x * k, y * k, z * k);
}
FreeVector UnitVector::operator/(const float k) const {
return FreeVector(x / k, y / k, z / k);
}
UnitVector::UnitVector(const float x, const float y, const float z, const float r)
: x{x / r}, y{y / r}, z{z / r} {}
UnitVectors
allow you to define directions in a much better manner:
struct Ray {
BoundVector source;
UnitVector direction;
BoundVector parametric_eq(float) const;
};
BoundVector Ray::parametric_eq(const float t) const {
return source + direction * t;
}
However, this is not all sunshine and roses, as it sometimes results in very ugly-looking static_cast
s:
struct Lambertian : public Material {
FreeVector albedo;
std::optional<Scatter> scatter(const Ray&, const Strike&) const override;
};
FreeVector random_in_unit_sphere() {
std::random_device r;
std::default_random_engine gen(r());
std::uniform_real_distribution<float> distribution(0, 1);
while (true) {
const FreeVector v(distribution(gen), distribution(gen), distribution(gen));
if (v.length() < 1) return v;
}
}
std::optional<Scatter> Lambertian::scatter(const Ray& ray,
const Strike& strike) const {
return Scatter{.attenuation = albedo,
.scattered = Ray{.source = strike.point,
.direction = static_cast<UnitVector>(
static_cast<FreeVector>(strike.normal) +
random_in_unit_sphere())}};
}
Note that the std::optional
is added here because the material may choose to absorb the Ray
completely with some probability, and hence not scatter it at all.
Is there a way to reduce the number of static_cast
s in the last example (or at least the overhead due to them)?
Any other feedback, comments and nitpickings are also welcomed.
set(CMAKE_CXX_STANDARD 2a)
, but GCC stopped complaining once I addedset(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++2a)
to the CMakeLists.txt file. \$\endgroup\$