I'm a total beginoob in Lua. The aim is to display a 3D Torus in dots (see Figure1), or at least compute the positions, privileging clarity over performance. Pseudocode link.

Don't hesitate to be picky, I'd like to learn proper Lua idioms. For instance, I was pleased to find operator overloading, but couldn't find a neat way to bind it to my "matrix type" by default.

enter image description here

-- Bake dot-donut. Based on pseudo-code here:
-- https://64nops.wordpress.com/2021/01/21/rosetta-sugar-japprends-a-coder-avec-mon-cpc/

-- Main parameters to play with.
r, r2 = 100, 20  -- Major and minor radius
dist = 200       -- Distance from observator. Should be greater than major radius
zoom = 300       
dots_per_circle = 100
nbcircles = 200   

-- pi is wrong!
local tau = 2*math.pi
local cos = math.cos
local sin = math.sin

-- For operator overloading
metamatrix = {}
-- Rotation matrice around x axis
function rotx(a) 
  local c, s = cos(a), sin(a)
  res = {{1,  0,  0},
         {0,  c, -s},
         {0,  s,  c}}
  setmetatable(res, metamatrix)
  return res
-- Rotation matrice around y axis
function roty(a) 
  local c, s = cos(a), sin(a)
  res = {{c,  0, -s},
         {0,  1,  0},
         {s,  0, c}}
  setmetatable(res, metamatrix)
  return res
-- Rotation matrice around z axis
function rotz(a) 
  local c, s = cos(a), sin(a)
  res = {{c, -s,  0},
         {s,  c,  0},
         {0,  0,  1}}
  setmetatable(res, metamatrix)
  return res

-- Multiplication of matrices (m rows n cols) * (n rows p cols) 
function metamatrix.__mul(A, B) 
   local res = {}
   for i = 1, #A do
     res[i] = {}
     for j = 1, #B[1] do   -- TODO? Handle empty matrix.
       local s = 0
       for k = 1, #B do
          s = s + A[i][k] * B[k][j]
       res[i][j] = s
   setmetatable(res, metamatrix)
   return res

-- Abstract the encoding of position (x y z).
-- Here, we choose column vector convention,
-- to be compatible with left multiplication by rotation matrices.
function dot(x, y, z)
  return {{x},
function undot(dot)
  return dot[1][1],

-- 3d to 2d
function proj(x, y, z)
  local z2 = z + dist
  return x/z2 * zoom, y/z2 * zoom

-- List of regularly spaced dots from a circle of radius r' at distance x = r in the plane XOY.
circle = {}
for i = 1, dots_per_circle do
  local a = (i-1) * tau / dots_per_circle
  circle[i] = dot(cos(a)*r2+r, sin(a)*r2, 0)

-- Now the torus is a surface of revolution.
torus = {}
for i = 1, nbcircles do
  local a = (i-1) * tau / nbcircles
  for _, dot in pairs(circle) do
    table.insert(torus, roty(a) * dot)

-- Let's tilt it.
tilt = rotx(0.5) * rotz(0.3)
torus2 = {} 
for _, dot in pairs(torus) do
  table.insert(torus2, tilt * dot)

-- Project to 2D and serialize.
for _, dot in pairs(torus2) do
   local x, y, z = undot(dot)
   xp, yp = proj(x, y, z)
   print(xp, yp)


2 Answers 2

  1. You've named the radii as r and r2, which to me would read as \$ r \$ and \$ r ^ 2 \$; if they are different radii values, you should use names like r1 and r2 instead. This is exactly what the author does in their BASIC implementation.

  2. Use locals until you don't. This makes importing your project easier, without dirtying the namespace.

  3. Split matrix code as a separate file. Not really necessary for a single file script.

  4. You can reduce some calculation in the proj function:

    function proj(x, y, z)
      local multiplier = zoom / (z + dist)
      return x * multiplier, y * multiplier
  5. Where do the magic numbers \$ 0.5 \$ and \$ 0.3 \$ come from? The pseudo code uses \$ 0.7 \$ for the 2nd one, how does it affect the rotation? The constants themselves should be defined at the beginning, with a little comment/description.

  6. You can take some hints on managing metatable and matrix itself by taking a look at the source code of lua-matrix project, or some other lua packages.


You could organize it by using Lua's object oriented tricks to make a class, methods etc. I need to learn more math to cleanup the code you posted but here is a sample Vector3 class I wrote with operator overloading and a sample method. Additionally all the methods get packed into one "namespace" in the Vector3 table so there is less global pollution.

As far as your code goes if its just a one time run in an isolated environment its fine for its purpose, but otherwise you should encapsulate all your vars/functions into one namespace or make sure they are all local to prevent pollution as in Lua all scripts share the environment and the default is global unless local is specified. The other reason is garbage collection.

any object stored in a global variable is not garbage for Lua, even if your program will never use it again — https://www.lua.org/pil/17.html

Vector3 = {};
function Vector3:New(x,y,z)
  local obj = {}; -- create a new table (object)
  -- self already exists as a local because of the : syntactic sugar it points to the table Vector3 here, but would point to a child table if Vector3 is inherited.
  setmetatable(obj, self); -- Set the metatable of the new object to self
  self.__index = self -- set the __index metamethod to self, so missing indexes on objects lookup back to self(Vector3 or inherited) instead of returning nil.
  obj.x = x or 0;
  obj.y = y or 0;
  obj.z = z or 0;
  return obj;
function Vector3:ToString() -- SampleMethod
  return "(X: " .. self.x .. " Y: " .. self.y .. " Z: " .. self.z ..")";
function Vector3:__add(other) -- Sample Overload
  local x = self.x + other.x;
  local y = self.y + other.y;
  local z = self.z + other.z;
  return Vector3:New(x,y,z);
function Vector3:__div(other)
  local distance = math.sqrt((other.x - self.x)^2 + (other.y - self.y)^2 + (other.z - self.z)^2);
  return distance;

local Origin = Vector3:New(25,25,25); -- Object1
local Destin = Vector3:New(50,50,50); -- Object2

print("Vector Addition: " .. Origin:ToString() .. " + " .. Destin:ToString());
local Sum = Origin + Destin;

print("Distance Between: " .. Origin:ToString() .. " and: " .. Destin:ToString());
local Distance = (Destin / Origin);



Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.