Lua Ray Tracer

Chapter 2
Login

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