Pure Lua 3D lib for norns.
Provides classes for storing & manipulating 3D objects, with similar APIs but different internal structures:
Polyhedron
has a notion of faces composed of vertices. It is more suited for importing 3D models.
Wireframe
, on the other hand, only has a notion of edges (line between points). It is more suited for representing models with internal edges (such as an hypercube).
Both support importing .OBJ
models (even though Polyhedron
is more naturally suited for this use-case).
Sphere
is just a basic sphere.
Importing a 3D model and displaying it.
local Polyhedron = include('lib/3d/polyhedron')
local draw_mode = include('lib/3d/enums/draw_mode')
local model = Polyhedron.new_from_obj("/home/we/dust/code/3d/model/teapot.obj")
local level = 15
model:draw(level, draw_mode.FACES)
Rotating it:
local axis = include('lib/3d/enums/axis')
model:rotate(axis.Y, 0.02)
Drawing can take a multiplication coefficient and a camera position:
local mult = 64 -- scale up model by 640%
local cam = {0, 0, -4} -- camera coordinates (x, y, z)
model:draw(level, draw_mode.FACES, mult, cam)
This is important as .OBJ
models vary greatly in scale and are not necessarily origin-centered.
See the teapot (Polyhedron
) and wireframe_cube (Wireframe
) examples for this basic use-case.
Several draw mode are supported:
model:draw(nil, draw_mode.FACES) -- faces (not supported by `Wireframe`)
model:draw(level, draw_mode.WIREFRAME) -- edges
model:draw(level, draw_mode.POINTS) -- vertices
And can be combined:
model:draw(level, draw_mode.FACES | draw_mode.WIREFRAME) -- faces + edges
In this case, independent screen levels can be specified:
model:draw(level, draw_mode.WIREFRAME | draw_mode.POINTS, nil, nil,
{line_level = 10,
point_level = 5})
See the octagon example to illustrate this use-case.
A custom drawing function can be configured:
function draw_v_as_circle(x, y, l)
if l then
screen.level(l)
end
local radius = 2
screen.move(x + radius, y)
screen.circle(x, y, radius)
screen.fill()
end
model:draw(level, draw_mode.POINTS, mult, cam,
{point_draw_fn = draw_v_as_circle})
Custom drawing function parameter depends of draw_mode
:
object \ draw_mode | POINTS |
WIREFRAME |
FACES |
---|---|---|---|
Wireframe |
point_draw_fn(x, y, l) |
lines_draw_fn(x0, y0, x1, y1, l) |
n/a |
Polyhedron |
point_draw_fn(x, y, l) |
face_edges_draw_fn(f_edges, l) |
face_draw_fn(f_edges, l) |
See the octagon example to illustrate this use-case.
Drawing of vertices/edges/faces can be conditional thanks to these props:
prop | description |
---|---|
draw_pct |
% of chance that element get drawn |
min_z |
elements w/ at least 1 vertex bellow value are skipped |
maw_z |
elements w/ at least 1 vertex above value are skipped |
When tuned appropriately, this can lead to a nice glitchy effect.
See the octaglitch example.
Wireframe
, when in draw_mode.WIREFRAME
, supports drawing random lines between vertices.
prop | description |
---|---|
glitch_edge_pct |
% of chance that element get drawn |
glitch_edge_amount_pct |
% of total vertices that attempts getting linked |
See the glitchpercube example.
No clean masking support, elements (faces / edges / vertices) are drawn in no specific order.
Basic masking could be enabled tuning the min_z
drawing property, even though that'll only work properly for highly symmetrical models.
No support for materials.
Currently, glitches (conditional drawing, random elements) refresh at the same rate as the element is being drawn. Ideally we should split this "glicthing" logic from the drawing logic for it to be less aggressive.
90% of the 3D vertex calculation code is based on an example by @Ivoah for PICO-8.
It has been modified to rely on a mutating state for better performance and preventing memory consumption to grow out of control (I assume GC tuning is a bit conservative on norns).
Lowpoly 3D fish models used in obj_fish.lua
example by @rkuhlf (source).