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
-- 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
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
2 Answers 2
You've named the radii as
r
andr2
, which to me would read as \$ r \$ and \$ r ^ 2 \$; if they are different radii values, you should use names liker1
andr2
instead. This is exactly what the author does in theirBASIC
implementation.Use
local
s until you don't. This makes importing your project easier, without dirtying the namespace.Split matrix code as a separate file. Not really necessary for a single file script.
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
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.
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;
end
function Vector3:ToString() -- SampleMethod
return "(X: " .. self.x .. " Y: " .. self.y .. " Z: " .. self.z ..")";
end
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);
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);
Explore related questions
See similar questions with these tags.