Chapter 2 - The Vector3 Class
This chapter defines the Vector3 class, which I understand will be
used for all sorts of vectors in later chapters. I thought a bit about
whether to change to a more functional approach since I'm in Lua, and
decided that I'd learn a bit more and have a bit of fun by using Lua's
metatables to retain the object-oriented approach in the original C++.
So I did a bit of research about how objects are approached in Lua put
together Vector3, a Lua interpretation of the C++ version's vec3.
Vectors are simply Lua tables, and since they are generic, they have accessors supporting both Cartesian and RGB interpretations. This is unconventional, but follows the pattern in the book.
Vector3 = {}
function Vector3:new(o)
   o = o or {}
   self.__index = self
   setmetatable(o, self)
   return o
end
-- Accessors for cartesian coords
function Vector3:x() return self[1] end
function Vector3:y() return self[2] end
function Vector3:z() return self[3] end
-- Accessors for RGB values
function Vector3:r() return self[1] end
function Vector3:g() return self[2] end
function Vector3:b() return self[3] end
Since I'm trying out all my code using the Lua REPL, I also want a nice
string representation for vectors. It's easy to attach the tostring
method to the metamethod __tostring, which is used by the REPL when
printing out results.
-- REPL usability
function Vector3:tostring()
   return string.format("Vector3{%s, %s, %s}", self[1], self[2], self[3])
end
Vector3.__tostring = Vector3.tostring
Next, I implement a straight port of the C++ mathematical functions for vectors, attaching them to Lua's metamethods where available.
-- Mathematical operations
function Vector3:add(vec)
   return Vector3:new{self[1] + vec[1], self[2] + vec[2], self[3] + vec[3]}
end
Vector3.__add = Vector3.add
function Vector3:sub(vec)
   return Vector3:new{self[1] - vec[1], self[2] - vec[2], self[3] - vec[3]}
end
Vector3.__sub = Vector3.sub
function Vector3:mul(val)
   local t = type(val)
   if t == "number" then return self:nmul(val)
   elseif t == "table" then return self:vmul(val)
   end
end
Vector3.__mul = Vector3.mul
function Vector3:div(val)
   local t = type(val)
   if t == "number" then return self:ndiv(val)
   elseif t == "table" then return self:vdiv(val)
   end
end
Vector3.__div = Vector3.div
function Vector3:negate()
   return Vector3:new{-self[1], -self[2], -self[3]}
end
Vector3.__unm = Vector3.negate
function Vector3:length()
   return math.sqrt(self:squared_length())
end
function Vector3:squared_length()
   return self[1]*self[1] +
      self[2]*self[2] +
      self[3]*self[3]
end
function Vector3:dot(vec)
   return self[1] * vec[1] + self[2] * vec[2] + self[3] * vec[3]
end
function Vector3:cross(vec)
   return Vector3:new{
      self[2] * vec[3] - self[3] * vec[2],
      self[3] * vec[1] - self[1] * vec[3],
      self[1] * vec[2] - self[2] * vec[1]
   }
end
function Vector3:unit_vector()
   local l = self:length()
   return Vector3:new{self[1]/l, self[2]/l, self[3]/l}
end
-- Destructive functions
function Vector3:make_unit_vector()
   k = 1 / self:length()
   self[1] = self[1]*k
   self[2] = self[2]*k
   self[3] = self[3]*k
end
Many of the functions above are defined in the C++ code as inline and
const. I'm not well-versed in C++, but after reading up on these
keywords, I think they are both compiler hints that improve performance:
inline inlines the function so we avoid dispatch overhead, and const
allows the compiler to assume the value won't change, allowing further
optimizations. Neither is present in Lua, so I've ignored them above.
The C++ implementation also leverages static dispatch for performance,
but Lua isn't statically typed, so my implementation will be
dynamically dispatched. In the case of mul and div, which can accept
either a number (scalar context) or a table (vector context), I use
reflection via the type method to determine context and then dispatch
to the appropriate implementation. The actual implementations are below.
-- Internal implementations
function Vector3:nmul(num)
   return Vector3:new{self[1]*num, self[2]*num, self[3]*num}
end
function Vector3:vmul(vec)
   return Vector3:new{self[1]*vec[1], self[2]*vec[2], self[3]*vec[3]}
end
function Vector3:ndiv(num)
   return Vector3:new{self[1]/num, self[2]/num, self[3]/num}
end
function Vector3:vdiv(vec)
   return Vector3:new{self[1]/vec[1], self[2]/vec[2], self[3]/vec[3]}
end
This completes our Lua implementation of Vector3. A notable omission
from the C++ implementation are the +=, -=, *=, and /=
operators, which have no associated metamethods in Lua due to its
single-pass parse model. As a result, I'm leaving them out for now, but
I might add them later if the ray tracing code becomes to unwieldy
without them.
The code at the end of the chapter uses the new vec3 abstraction. We
can use our Vector3 class instead.
nx = 600
ny = 300
print(string.format("P3\n%s %s\n255\n", nx, ny))
for j = ny-1, 0, -1 do
   for i = 0, nx-1 do
      v = Vector3:new{i/nx, j/ny, 0.2}
      ir = math.floor(256*v[1])
      ig = math.floor(256*v[2])
      ib = math.floor(256*v[3])
      print(string.format("%s %s %s", ir, ig, ib))-
   end
end
With this new library to handle vectors, we can take a look at modeling how light moves!
Next up: Chapter 3 - Rays, Camera, Background