# 3D torus in Lua

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.

-- 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

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
end
-- 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
end
-- 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
end

-- 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]
end
res[i][j] = s
end
end
setmetatable(res, metamatrix)
return res
end

-- 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},
{y},
{z}}
end
function undot(dot)
return dot[1][1],
dot[2][1],
dot[3][1]
end

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

-- 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)
end

-- 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)
end
end

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

-- 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)
end



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
end

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 = {};
Vector3.__index = Vector3; -- Set the index to the Vector3 Base Class so missing indexes on the child objects lookup back to Vector3
function Vector3:New(x,y,z)
local self = {};
setmetatable(self, Vector3);
self.x = x or 0;
self.y = y or 0;
self.z = z or 0;
return self;
end
function Vector3:ToString() -- SampleMethod
return "(X: " .. self.x .. " Y: " .. self.y .. " Z: " .. self.z ..")";
end
local x = self.x + other.x;
local y = self.y + other.y;
local z = self.z + other.z;
return Vector3:New(x,y,z);
end
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;
end

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(Sum:ToString());

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