Type-safe Euclidean vectors in C++
up vote
0
down vote
favorite
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.
c++ coordinate-system raytracing
New contributor
add a comment |
up vote
0
down vote
favorite
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.
c++ coordinate-system raytracing
New contributor
designated initializers are a very nice feature of C++20... but we're in 2018! I'm not even sure which compilers do support it already
– papagaga
25 mins ago
Strangely, CMake didn't support it so I couldn't useset(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.
– ajeetdsouza
20 mins ago
add a comment |
up vote
0
down vote
favorite
up vote
0
down vote
favorite
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.
c++ coordinate-system raytracing
New contributor
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.
c++ coordinate-system raytracing
c++ coordinate-system raytracing
New contributor
New contributor
New contributor
asked 30 mins ago
ajeetdsouza
11
11
New contributor
New contributor
designated initializers are a very nice feature of C++20... but we're in 2018! I'm not even sure which compilers do support it already
– papagaga
25 mins ago
Strangely, CMake didn't support it so I couldn't useset(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.
– ajeetdsouza
20 mins ago
add a comment |
designated initializers are a very nice feature of C++20... but we're in 2018! I'm not even sure which compilers do support it already
– papagaga
25 mins ago
Strangely, CMake didn't support it so I couldn't useset(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.
– ajeetdsouza
20 mins ago
designated initializers are a very nice feature of C++20... but we're in 2018! I'm not even sure which compilers do support it already
– papagaga
25 mins ago
designated initializers are a very nice feature of C++20... but we're in 2018! I'm not even sure which compilers do support it already
– papagaga
25 mins ago
Strangely, CMake didn't support it so I couldn't use
set(CMAKE_CXX_STANDARD 2a)
, but GCC stopped complaining once I added set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++2a)
to the CMakeLists.txt file.– ajeetdsouza
20 mins ago
Strangely, CMake didn't support it so I couldn't use
set(CMAKE_CXX_STANDARD 2a)
, but GCC stopped complaining once I added set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++2a)
to the CMakeLists.txt file.– ajeetdsouza
20 mins ago
add a comment |
active
oldest
votes
active
oldest
votes
active
oldest
votes
active
oldest
votes
active
oldest
votes
ajeetdsouza is a new contributor. Be nice, and check out our Code of Conduct.
ajeetdsouza is a new contributor. Be nice, and check out our Code of Conduct.
ajeetdsouza is a new contributor. Be nice, and check out our Code of Conduct.
ajeetdsouza is a new contributor. Be nice, and check out our Code of Conduct.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f208317%2ftype-safe-euclidean-vectors-in-c%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
designated initializers are a very nice feature of C++20... but we're in 2018! I'm not even sure which compilers do support it already
– papagaga
25 mins ago
Strangely, CMake didn't support it so I couldn't use
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.– ajeetdsouza
20 mins ago