Summary
- Added a simple vec3 class to help with operations on 3D positions, direction vectors and RGB colors.
Motivation
In the first chapter I created a simple image by assigning RGB values to a number
of individual variables in a loop that iterated across the x- and y-coordinates.
This was ok there, but this won't work very well for scenes with multiple objects
in a 3D coordinate system.
The following example produces the same image, but it introduces the vec3 class
that will make it easier to perform calculations with 3-dimensional vectors later.
To keep it simple, it can be used for x, y and z-coordinates as well as RGB values.
Click to view C++ source code
// vec3.h
#ifndef VEC3H
#define VEC3H
#include <math.h>
#include <stdlib.h>
#include <iostream>
class vec3 {
public:
float e[3];
vec3() {}
vec3(float e0, float e1, float e2) {
e[0] = e0;
e[1] = e1;
e[2] = e2;
}
inline float x() const { return e[0]; }
inline float y() const { return e[1]; }
inline float z() const { return e[2]; }
inline float r() const { return e[0]; }
inline float g() const { return e[1]; }
inline float b() const { return e[2]; }
inline const vec3& operator+() const { return *this; }
inline vec3 operator-() const { return vec3(-e[0], -e[1], -e[2]); }
inline float operator[](int i) const { return e[i]; }
inline float& operator[](int i) { return e[i]; }
inline vec3& operator+=(const vec3 &v2);
inline vec3& operator-=(const vec3 &v2);
inline vec3& operator*=(const vec3 &v2);
inline vec3& operator/=(const vec3 &v2);
inline vec3& operator*=(const float t);
inline vec3& operator/=(const float t);
inline float length() const {
return sqrt(e[0]*e[0] + e[1]*e[1] + e[2]*e[2]);
}
inline float squared_length() const {
return e[0]*e[0] + e[1]*e[1] + e[2]*e[2];
}
inline void make_unit_vector();
};
inline std::istream& operator>>(std::istream &is, vec3 &t) {
is >> t.e[0] >> t.e[1] >> t.e[2];
return is;
}
inline std::ostream& operator<<(std::ostream &os, const vec3 &t) {
os << t.e[0] << " " << t.e[1] << " " << t.e[2];
return os;
}
inline void vec3::make_unit_vector() {
float k = 1.0 / sqrt(e[0]*e[0] + e[1]*e[1] + e[2]*e[2]);
e[0] *= k;
e[1] *= k;
e[2] *= k;
}
inline vec3 operator+(const vec3 &v1, const vec3 &v2) {
return vec3(v1.e[0] + v2.e[0], v1.e[1] + v2.e[1], v1.e[2] + v2.e[2]);
}
inline vec3 operator-(const vec3 &v1, const vec3 &v2) {
return vec3(v1.e[0] - v2.e[0], v1.e[1] - v2.e[1], v1.e[2] - v2.e[2]);
}
inline vec3 operator*(const vec3 &v1, const vec3 &v2) {
return vec3(v1.e[0] * v2.e[0], v1.e[1] * v2.e[1], v1.e[2] * v2.e[2]);
}
inline vec3 operator/(const vec3 &v1, const vec3 &v2) {
return vec3(v1.e[0] / v2.e[0], v1.e[1] / v2.e[1], v1.e[2] / v2.e[2]);
}
inline vec3 operator*(float t, const vec3 &v) {
return vec3(t * v.e[0], t * v.e[1], t * v.e[2]);
}
inline vec3 operator/(const vec3 &v, float t) {
return vec3(v.e[0] / t, v.e[1] / t, v.e[2] / t);
}
inline vec3 operator*(const vec3 &v, float t) {
return vec3(t * v.e[0], t * v.e[1], t * v.e[2]);
}
inline float dot(const vec3 &v1, const vec3 &v2) {
return v1.e[0] * v2.e[0] + v1.e[1] * v2.e[1] + v1.e[2] * v2.e[2];
}
inline vec3 cross(const vec3 &v1, const vec3 &v2) {
return vec3(
(v1.e[1] * v2.e[2] - v1.e[2] * v2.e[1]),
(-(v1.e[0] * v2.e[2] - v1.e[2] * v2.e[0])),
(v1.e[0] * v2.e[1] - v1.e[1] * v2.e[0])
);
}
inline vec3& vec3::operator+=(const vec3 &v) {
e[0] += v.e[0];
e[1] += v.e[1];
e[2] += v.e[2];
return *this;
}
inline vec3& vec3::operator*=(const vec3 &v) {
e[0] *= v.e[0];
e[1] *= v.e[1];
e[2] *= v.e[2];
return *this;
}
inline vec3& vec3::operator/=(const vec3 &v) {
e[0] /= v.e[0];
e[1] /= v.e[1];
e[2] /= v.e[2];
return *this;
}
inline vec3& vec3::operator-=(const vec3 &v) {
e[0] -= v.e[0];
e[1] -= v.e[1];
e[2] -= v.e[2];
return *this;
}
inline vec3& vec3::operator*=(const float t) {
e[0] *= t;
e[1] *= t;
e[2] *= t;
return *this;
}
inline vec3& vec3::operator/=(const float t) {
float k = 1.0/t;
e[0] *= k;
e[1] *= k;
e[2] *= k;
return *this;
}
inline vec3 unit_vector(vec3 v) {
return v / v.length();
}
#endif
// vec3-example.cpp
#include <iostream>
#include "vec3.h"
int main() {
int nx = 200;
int ny = 100;
std::cout << "P3\n" << nx << " " << ny << "\n255\n";
for (int j = ny-1; j >= 0; j--) {
for (int i = 0; i < nx; i++) {
vec3 col( float(i) / float(nx), float(j) / float(ny), 0.2 );
int ir = int(255.99 * col[0]);
int ig = int(255.99 * col[1]);
int ib = int(255.99 * col[2]);
std::cout << ir << " " << ig << " " << ib << "\n";
}
}
}
Vec3 with Javascript and Canvas
The beauty of C++ is that it supports operator overloading and allows me to add two
3D vectors with a standard '+' operator just as easily as adding two integer numbers.
I won't have to do anything special to add up two 3D vectors or even more complex
datatypes.
Unfortunately, Javascript doesn't support the same type of overloading. For each
operation, I defined a function add(), mul(), and div() instead.
This is not a huge problem, but it makes the code a bit less easy to read when multiple
add, multiply, or divide operations are chained to a longer expression.
There may be more elegant or more complete approaches. Vector3 is a basic datatype
that has been implemented in pretty much all Javascript graphics libraries, like
Three.JS for example. Since the goal was to avoid external dependencies, I'll add my
own version here.
A quick note about the coding style: Normally I would define all class names to begin
with an uppercase letter. For example "Vector3" which is also the style used in Three.JS.
A lowercase "vec3" is the norm in other areas, for example the OpenGL Shading
Language (GLSL). Maybe it's a historical thing, or simply because vec3 or
vec4 are data types that can be considered just as fundamental as float
or int. The book defined a lowercase "vec3" class as well, and I decided to
go with it for the time being. I might change it to an uppercase format in one of the
future versions.
Figure RT003: Simple canvas image rendered with Javascript
Click to view Javascript source code
Vec3 with Python
Like C++, Python supports overloading operators. This can be accomplished with a number
of magic functions. In my vec3 class I used the following:
__pos__
: +vec3
__neg__
: -vec3
__iadd__
: vec3 + vec3, updates value in instance
__isub__
: vec3 - vec3, updates value in instance
__imul__
: vec3 * vec3 or float, updates value in instance
__idiv__
: vec3 / vec3 or float, updates value in instance
__add__
: vec3 + vec3, returns new vec3
__sub__
: vec3 - vec3, returns new vec3
__mul__
: vec3 * vec3 or float, returns new vec3
__truediv__
: vec3 / vec3 or float, returns new vec3
__str__
: returns a string representation of this instance
With the "@property" directive, Python provides a
convenient way to access the vec3 components with x,
y, z or r, g, and b.
Click to view Python source code
class vec3:
def __init__(self, e0, e1, e2):
self.e = [e0, e1, e2]
@property
def x(self):
return self.e[0]
@property
def y(self):
return self.e[1]
@property
def z(self):
return self.e[2]
@property
def r(self):
return self.e[0]
@property
def g(self):
return self.e[1]
@property
def b(self):
return self.e[2]
def __pos__(self):
return vec3(self.x, self.y, self.z)
def __neg__(self):
return vec3(-self.x, -self.y, -self.z)
def __iadd__(self, v):
self.e[0] += v.x
self.e[1] += v.y
self.e[2] += v.z
return self
def __isub__(self, v):
self.e[0] -= v.x
self.e[1] -= v.y
self.e[2] -= v.z
return self
def __imul__(self, v):
if isinstance(v, vec3):
self.e[0] *= v.x
self.e[1] *= v.y
self.e[2] *= v.z
else:
self.e[0] *= v
self.e[1] *= v
self.e[2] *= v
return self
# ...
import sys
from vec3 import vec3
def main():
nx = 200
ny = 100
print(f'P3\n{nx} {ny}\n255')
for j in range(ny, 0, -1):
for i in range(0, nx, 1):
col = vec3(i/nx, j/ny, 0.2)
col *= 255.99
col = col.toInt()
print(f'{col.r} {col.g} {col.b}')
if __name__ == '__main__':
sys.exit(main())
Links