mirror of
https://github.com/Reuh/wirefame.git
synced 2025-10-27 09:39:30 +00:00
1533 lines
43 KiB
Lua
1533 lines
43 KiB
Lua
--- WireFame v0.4.0 by Reuh: initially a wireframe software rendering engine, now also a 3D framework for Löve (optional).
|
|
-- Should be compatible with Lua 5.1 to 5.3. There are 3 functions you will need to implement yourself to make
|
|
-- this works with your graphical backend.
|
|
-- Name found by Lenade Lamidedi.
|
|
-- Thanks to tinyrenderer (https://github.com/ssloy/tinyrenderer/wiki) for explaining how a 3D renderer works.
|
|
|
|
local wirefame = {
|
|
--- Default drawing color used when creating models.
|
|
defaultColor = { 1, 1, 1, 1 }
|
|
}
|
|
|
|
--------------------------------------
|
|
--## LÖVE-specific initialization ##--
|
|
--------------------------------------
|
|
|
|
local lg, lshaderCode
|
|
if love then
|
|
-- love.graphics
|
|
lg = love.graphics
|
|
|
|
-- shader code
|
|
lshaderCode = [[
|
|
varying vec3 normal_world; // vertex normal in world space
|
|
varying vec4 position_world; // fragment position in world space
|
|
|
|
# ifdef VERTEX
|
|
uniform mat4 scene_transform; // world -> view transform
|
|
uniform mat4 model_transform; // model -> world transform
|
|
|
|
attribute vec3 VertexNormal;
|
|
|
|
vec4 position(mat4 love_transform, vec4 vertex_position) {
|
|
normal_world = (model_transform * vec4(VertexNormal, 0)).xyz;
|
|
position_world = model_transform * vertex_position;
|
|
return scene_transform * position_world;
|
|
}
|
|
# endif
|
|
|
|
# ifdef PIXEL
|
|
# if LIGHT_COUNT > 0
|
|
struct Light {
|
|
int type; // 1 for ambient, 2 for directional, 3 for point
|
|
vec3 position; // position for point lights, or direction for directional lights
|
|
vec4 color; // diffuse color for point and directional lights (alpha is intensity), or ambient color for ambient light
|
|
};
|
|
|
|
uniform Light[LIGHT_COUNT] lights;
|
|
# endif
|
|
|
|
vec4 effect(vec4 color, sampler2D texture, vec2 texture_coords, vec2 screen_coords) {
|
|
// base color
|
|
vec4 material_color = texture2D(texture, texture_coords) * color;
|
|
if (material_color.a == 0) discard;
|
|
|
|
# if LIGHT_COUNT > 0
|
|
// shading
|
|
vec4 final_color = vec4(0.0, 0.0, 0.0, 0.0);
|
|
for (int i = 0; i < LIGHT_COUNT; i++) {
|
|
Light light = lights[i];
|
|
|
|
// ambient
|
|
if (light.type == 1) {
|
|
final_color += material_color * light.color;
|
|
|
|
// directional
|
|
} else if (light.type == 2) {
|
|
// diffuse lighting
|
|
vec3 n = normalize(normal_world);
|
|
vec3 l = normalize(-light.position);
|
|
final_color += material_color * vec4(light.color.rgb * light.color.a * max(dot(n, l), 0.0), 1);
|
|
|
|
// point
|
|
} else if (light.type == 3) {
|
|
// diffuse lighting
|
|
vec3 n = normalize(normal_world);
|
|
vec3 l = normalize(light.position - position_world.xyz);
|
|
float distance = length(light.position - position_world.xyz);
|
|
final_color += material_color * vec4(light.color.rgb * light.color.a * max(dot(n, l), 0.0) / (distance * distance), 1);
|
|
}
|
|
|
|
// no specular because i'm lazy (TODO)
|
|
}
|
|
|
|
// done
|
|
return final_color;
|
|
# else
|
|
return material_color;
|
|
# endif
|
|
}
|
|
# endif
|
|
]]
|
|
|
|
-- enable depth buffer
|
|
lg.setDepthMode("lequal", true)
|
|
|
|
-- culling
|
|
love.graphics.setFrontFaceWinding("ccw")
|
|
lg.setMeshCullMode("back")
|
|
end
|
|
|
|
------------------------------------------------------------------
|
|
--## Functions you need to implement fore wireframe rendering ##--
|
|
------------------------------------------------------------------
|
|
|
|
--- Draws a line from x0,y0 to x1,y1 (px).
|
|
-- function wirefame.drawLine(x0, y0, x1, y1)
|
|
function wirefame.drawLine(x0, y0, x1, y1)
|
|
lg.line(x0, y0, x1, y1)
|
|
end
|
|
--- Draws a point at x,y (px).
|
|
-- function wirefame.drawPoint(x, y)
|
|
function wirefame.drawPoint(x, y)
|
|
lg.setPointSize(2)
|
|
lg.points(x, y)
|
|
end
|
|
--- Sets the current drawing color (0-1).
|
|
-- function wirefame.setColor(r, g, b, a)
|
|
function wirefame.setColor(r, g, b, a)
|
|
lg.setColor(r, g, b, a)
|
|
end
|
|
|
|
-------------------------
|
|
--## Utility classes ##--
|
|
-------------------------
|
|
|
|
local unpack = table.unpack or unpack
|
|
local sqrt, cos, sin, tan, rad = math.sqrt, math.cos, math.sin, math.tan, math.rad
|
|
local m4, v3, bb3
|
|
local tmpv3 -- temporary vector used in our computations
|
|
|
|
--- 4x4 matrix. Reminder: they are represented in row-major order, **unlike** GLM which is column-major.
|
|
local m4_mt = {
|
|
-- Clone the matrix
|
|
clone = function(self)
|
|
return m4{unpack(self)}
|
|
end,
|
|
|
|
-- Set matrix in place
|
|
set = function(self, other)
|
|
self[1], self[2], self[3], self[4] = other[1], other[2], other[3], other[4]
|
|
self[5], self[6], self[7], self[8] = other[5], other[6], other[7], other[8]
|
|
self[9], self[10], self[11], self[12] = other[9], other[10], other[11], other[12]
|
|
self[13], self[14], self[15], self[16] = other[13], other[14], other[15], other[16]
|
|
return self
|
|
end,
|
|
|
|
-- Apply a transformation matrix to our matrix
|
|
transform = function(self, tr)
|
|
local s1, s2, s3, s4 = self[1], self[2], self[3], self[4]
|
|
local s5, s6, s7, s8 = self[5], self[6], self[7], self[8]
|
|
local s9, s10, s11, s12 = self[9], self[10], self[11], self[12]
|
|
local s13, s14, s15, s16 = self[13], self[14], self[15], self[16]
|
|
|
|
self[1] = tr[1] * s1 + tr[2] * s5 + tr[3] * s9 + tr[4] * s13
|
|
self[2] = tr[1] * s2 + tr[2] * s6 + tr[3] * s10 + tr[4] * s14
|
|
self[3] = tr[1] * s3 + tr[2] * s7 + tr[3] * s11 + tr[4] * s15
|
|
self[4] = tr[1] * s4 + tr[2] * s8 + tr[3] * s12 + tr[4] * s16
|
|
|
|
self[5] = tr[5] * s1 + tr[6] * s5 + tr[7] * s9 + tr[8] * s13
|
|
self[6] = tr[5] * s2 + tr[6] * s6 + tr[7] * s10 + tr[8] * s14
|
|
self[7] = tr[5] * s3 + tr[6] * s7 + tr[7] * s11 + tr[8] * s15
|
|
self[8] = tr[5] * s4 + tr[6] * s8 + tr[7] * s12 + tr[8] * s16
|
|
|
|
self[9] = tr[9] * s1 + tr[10] * s5 + tr[11] * s9 + tr[12] * s13
|
|
self[10] = tr[9] * s2 + tr[10] * s6 + tr[11] * s10 + tr[12] * s14
|
|
self[11] = tr[9] * s3 + tr[10] * s7 + tr[11] * s11 + tr[12] * s15
|
|
self[12] = tr[9] * s4 + tr[10] * s8 + tr[11] * s12 + tr[12] * s16
|
|
|
|
self[13] = tr[13] * s1 + tr[14] * s5 + tr[15] * s9 + tr[16] * s13
|
|
self[14] = tr[13] * s2 + tr[14] * s6 + tr[15] * s10 + tr[16] * s14
|
|
self[15] = tr[13] * s3 + tr[14] * s7 + tr[15] * s11 + tr[16] * s15
|
|
self[16] = tr[13] * s4 + tr[14] * s8 + tr[15] * s12 + tr[16] * s16
|
|
|
|
return self
|
|
end,
|
|
translate = function(self, v)
|
|
return self:transform(m4.translate(v))
|
|
end,
|
|
scale = function(self, v)
|
|
return self:transform(m4.scale(v))
|
|
end,
|
|
rotate = function(self, angle, v)
|
|
return self:transform(m4.rotate(angle, v))
|
|
end,
|
|
shear = function(self, vxy, vyz)
|
|
return self:transform(m4.shear(vxy, vyz))
|
|
end,
|
|
|
|
-- Common operations
|
|
__mul = function(self, other)
|
|
return m4{
|
|
self[1] * other[1] + self[2] * other[5] + self[3] * other[9] + self[4] * other[13],
|
|
self[1] * other[2] + self[2] * other[6] + self[3] * other[10] + self[4] * other[14],
|
|
self[1] * other[3] + self[2] * other[7] + self[3] * other[11] + self[4] * other[15],
|
|
self[1] * other[4] + self[2] * other[8] + self[3] * other[12] + self[4] * other[16],
|
|
|
|
self[5] * other[1] + self[6] * other[5] + self[7] * other[9] + self[8] * other[13],
|
|
self[5] * other[2] + self[6] * other[6] + self[7] * other[10] + self[8] * other[14],
|
|
self[5] * other[3] + self[6] * other[7] + self[7] * other[11] + self[8] * other[15],
|
|
self[5] * other[4] + self[6] * other[8] + self[7] * other[12] + self[8] * other[16],
|
|
|
|
self[9] * other[1] + self[10] * other[5] + self[11] * other[9] + self[12] * other[13],
|
|
self[9] * other[2] + self[10] * other[6] + self[11] * other[10] + self[12] * other[14],
|
|
self[9] * other[3] + self[10] * other[7] + self[11] * other[11] + self[12] * other[15],
|
|
self[9] * other[4] + self[10] * other[8] + self[11] * other[12] + self[12] * other[16],
|
|
|
|
self[13] * other[1] + self[14] * other[5] + self[15] * other[9] + self[16] * other[13],
|
|
self[13] * other[2] + self[14] * other[6] + self[15] * other[10] + self[16] * other[14],
|
|
self[13] * other[3] + self[14] * other[7] + self[15] * other[11] + self[16] * other[15],
|
|
self[13] * other[4] + self[14] * other[8] + self[15] * other[12] + self[16] * other[16]
|
|
}
|
|
end,
|
|
__sub = function(self, other)
|
|
return m4{
|
|
self[1] + other[1], self[2] + other[2], self[3] + other[3], self[4] + other[4],
|
|
self[5] + other[5], self[6] + other[6], self[7] + other[7], self[8] + other[8],
|
|
self[9] + other[9], self[10] + other[10], self[11] + other[11], self[12] + other[12],
|
|
self[13] + other[13], self[14] + other[14], self[15] + other[15], self[16] + other[16],
|
|
}
|
|
end,
|
|
__add = function(self, other)
|
|
return m4{
|
|
self[1] - other[1], self[2] - other[2], self[3] - other[3], self[4] - other[4],
|
|
self[5] - other[5], self[6] - other[6], self[7] - other[7], self[8] - other[8],
|
|
self[9] - other[9], self[10] - other[10], self[11] - other[11], self[12] - other[12],
|
|
self[13] - other[13], self[14] - other[14], self[15] - other[15], self[16] - other[16],
|
|
}
|
|
end,
|
|
__unm = function(self)
|
|
return m4{
|
|
-self[1], -self[2], -self[3], -self[4],
|
|
-self[5], -self[6], -self[7], -self[8],
|
|
-self[9], -self[10], -self[11], -self[12],
|
|
-self[13], -self[14], -self[15], -self[16]
|
|
}
|
|
end,
|
|
|
|
__tostring = function(self)
|
|
local mt = getmetatable(self)
|
|
setmetatable(self, nil)
|
|
local str = "m4: "..(tostring(self):gsub("^table: ", ""))
|
|
setmetatable(self, mt)
|
|
return str
|
|
end
|
|
}
|
|
m4_mt.__index = m4_mt
|
|
m4 = setmetatable({
|
|
-- Common transformation matrices constructors
|
|
identity = function()
|
|
return m4{
|
|
1, 0, 0, 0,
|
|
0, 1, 0, 0,
|
|
0, 0, 1, 0,
|
|
0, 0, 0, 1
|
|
}
|
|
end,
|
|
translate = function(v)
|
|
return m4{
|
|
1, 0, 0, v[1],
|
|
0, 1, 0, v[2],
|
|
0, 0, 1, v[3],
|
|
0, 0, 0, 1
|
|
}
|
|
end,
|
|
scale = function(v)
|
|
return m4{
|
|
v[1], 0, 0, 0,
|
|
0, v[2], 0, 0,
|
|
0, 0, v[3], 0,
|
|
0, 0, 0, 1
|
|
}
|
|
end,
|
|
rotate = function(angle, v)
|
|
local c = cos(angle)
|
|
local s = sin(angle)
|
|
local x, y, z = tmpv3:set(v):normalizeInPlace():unpack()
|
|
local tx, ty, tz = (1 - c) * x, (1 - c) * y, (1 - c) * z
|
|
return m4{
|
|
c + tx * x, ty * x - s * z, tz * x + s * y, 0,
|
|
tx * y + s * z, c + ty * y, tz * y - s * x, 0,
|
|
tx * z - s * y, ty * z + s * x, c + tz * z, 0,
|
|
0, 0, 0, 1
|
|
}
|
|
end,
|
|
-- vxy: shearing vector in the (xy) plane
|
|
-- vyz: shearing vector in the (yz) plane
|
|
shear = function(vxy, vyz)
|
|
return m4{
|
|
1, vxy[1], vyz[1], 0,
|
|
vxy[2], 1, vyz[2], 0,
|
|
vxy[3], vyz[3], 1, 0,
|
|
0, 0, 0, 1
|
|
}
|
|
end,
|
|
|
|
-- Projection matrix constructor (right-handed).
|
|
gnomonic = function(dist)
|
|
return m4{
|
|
1, 0, 0, 0,
|
|
0, 1, 0, 0,
|
|
0, 0, 1, 0,
|
|
0, 0, 1/dist, 1
|
|
}
|
|
end,
|
|
perspective = function(fovy, aspect, zNear, zFar)
|
|
local f = 1 / tan(rad(fovy) / 2)
|
|
return m4{
|
|
f / aspect, 0, 0, 0,
|
|
0, f, 0, 0,
|
|
0, 0, (zFar + zNear) / (zNear - zFar), (2 * zFar * zNear) / (zNear - zFar),
|
|
0, 0, -1, 0
|
|
}
|
|
end,
|
|
lookAt = function(eye, center, up)
|
|
center, up = v3(center), v3(up)
|
|
local f = (center - eye):normalizeInPlace()
|
|
local s = f:cross(up):normalizeInPlace()
|
|
local u = s:cross(f)
|
|
return m4{
|
|
s[1], s[2], s[3], -s:dot(eye),
|
|
u[1], u[2], u[3], -u:dot(eye),
|
|
-f[1], -f[2], -f[3], f:dot(eye),
|
|
0, 0, 0, 1
|
|
}
|
|
end
|
|
}, {
|
|
__call = function(self, t)
|
|
return setmetatable(t, m4_mt)
|
|
end
|
|
})
|
|
wirefame.m4 = m4
|
|
|
|
--- 3D vector
|
|
local v3_mt = {
|
|
-- Clone the vector
|
|
clone = function(self)
|
|
return v3(self:unpack())
|
|
end,
|
|
|
|
-- Set vector in place
|
|
set = function(self, other)
|
|
self[1], self[2], self[3] = other[1], other[2], other[3]
|
|
return self
|
|
end,
|
|
|
|
-- Retrieve coordinates
|
|
unpack = function(self)
|
|
return self[1], self[2], self[3]
|
|
end,
|
|
|
|
-- Normalize
|
|
normalize = function(self)
|
|
local x, y, z = self[1], self[2], self[3]
|
|
local l = sqrt(x*x + y*y + z*z)
|
|
return v3(x/l, y/l, z/l)
|
|
end,
|
|
normalizeInPlace = function(self)
|
|
local x, y, z = self[1], self[2], self[3]
|
|
local l = sqrt(x*x + y*y + z*z)
|
|
self[1], self[2], self[3] = x/l, y/l, z/l
|
|
return self
|
|
end,
|
|
|
|
-- Vector product
|
|
cross = function(self, other)
|
|
local x1, y1, z1 = self[1], self[2], self[3]
|
|
local x2, y2, z2 = other[1], other[2], other[3]
|
|
return v3(
|
|
y1*z2 - z1*y2,
|
|
z1*x2 - x1*z2,
|
|
x1*y2 - y1*x2
|
|
)
|
|
end,
|
|
dot = function(self, other)
|
|
return self[1] * other[1] + self[2] * other[2] + self[3] * other[3]
|
|
end,
|
|
|
|
-- Transform by a mat4
|
|
transform = function(self, matrix)
|
|
local x, y, z = self:unpack()
|
|
-- Transform matrix * Homogeneous coordinates
|
|
local mx = matrix[1] * x + matrix[2] * y + matrix[3] * z + matrix[4]
|
|
local my = matrix[5] * x + matrix[6] * y + matrix[7] * z + matrix[8]
|
|
local mz = matrix[9] * x + matrix[10] * y + matrix[11] * z + matrix[12]
|
|
local mw = matrix[13] * x + matrix[14] * y + matrix[15] * z + matrix[16]
|
|
-- Go back to euclidian coordinates
|
|
return v3(mx/mw, my/mw, mz/mw)
|
|
end,
|
|
|
|
-- Length
|
|
len2 = function(self)
|
|
local x, y, z = self[1], self[2], self[3]
|
|
return x*x + y*y + z*z
|
|
end,
|
|
len = function(self)
|
|
local x, y, z = self[1], self[2], self[3]
|
|
return sqrt(x*x + y*y + z*z)
|
|
end,
|
|
|
|
-- Distance
|
|
distance = function(self, other)
|
|
return (self - other):len()
|
|
end,
|
|
distance2 = function(self, other)
|
|
return (self - other):len2()
|
|
end,
|
|
|
|
-- Common operations
|
|
__sub = function(self, other)
|
|
return v3(self[1] - other[1], self[2] - other[2], self[3] - other[3])
|
|
end,
|
|
__add = function(self, other)
|
|
return v3(self[1] + other[1], self[2] + other[2], self[3] + other[3])
|
|
end,
|
|
__unm = function(self)
|
|
return v3(-self[1], -self[2], -self[3])
|
|
end,
|
|
__div = function(self, other)
|
|
return v3(self[1] / other, self[2] / other, self[3] / other)
|
|
end,
|
|
__mul = function(self, other)
|
|
return v3(self[1] * other, self[2] * other, self[3] * other)
|
|
end,
|
|
|
|
__eq = function(self, other)
|
|
return self[1] == other[1] and self[2] == other[2] and self[3] == other[3]
|
|
end,
|
|
|
|
__tostring = function(self)
|
|
return ("v3(%s,%s,%s)"):format(self:unpack())
|
|
end
|
|
}
|
|
v3_mt.__index = v3_mt
|
|
v3 = function(x, y, z)
|
|
if type(x) == "number" then
|
|
return setmetatable({ x, y, z }, v3_mt)
|
|
else
|
|
return setmetatable(x, v3_mt)
|
|
end
|
|
end
|
|
wirefame.v3 = v3
|
|
tmpv3 = v3(0, 0, 0)
|
|
|
|
--- 3D bounding box
|
|
local bb3_mt = {
|
|
-- Clone the bounding box
|
|
clone = function(self)
|
|
return bb3(self.min:clone(), self.max:clone())
|
|
end,
|
|
|
|
-- Set bounding box in place
|
|
set = function(self, min, max)
|
|
self.min, self.max = v3(min), v3(max)
|
|
return self
|
|
end,
|
|
|
|
-- Retrieve min and max vectors
|
|
unpack = function(self)
|
|
return self.min, self.max
|
|
end,
|
|
|
|
-- Test if the bounding box collide with another bounding box
|
|
collide = function(self, other)
|
|
return self.min[1] < other.max[1] and self.max[1] > other.min[1] and
|
|
self.min[2] < other.max[2] and self.max[2] > other.min[2] and
|
|
self.min[3] < other.max[3] and self.max[3] > other.min[3]
|
|
end,
|
|
|
|
-- Extend a bounding box by a certain distance
|
|
extend = function(self, d)
|
|
self.min = self.min - { d, d, d }
|
|
self.max = self.max + { d, d, d }
|
|
return self
|
|
end,
|
|
|
|
-- Transform by a mat4
|
|
transform = function(self, matrix)
|
|
return bb3(self.min:transform(matrix), self.max:transform(matrix))
|
|
end,
|
|
|
|
-- Common operations with vectors
|
|
__sub = function(self, other)
|
|
return bb3(self.min - other, self.max - other)
|
|
end,
|
|
__add = function(self, other)
|
|
return bb3(self.min + other, self.max + other)
|
|
end,
|
|
__unm = function(self)
|
|
return bb3(-self.min, -self.max)
|
|
end,
|
|
__div = function(self, other)
|
|
return bb3(self.min / other, self.max / other)
|
|
end,
|
|
__mul = function(self, other)
|
|
return bb3(self.min * other, self.max * other)
|
|
end,
|
|
|
|
__tostring = function(self)
|
|
return ("bb3(%s,%s)"):format(self.min, self.max)
|
|
end
|
|
}
|
|
bb3_mt.__index = bb3_mt
|
|
bb3 = function(min, max)
|
|
return setmetatable({ min = v3(min), max = v3(max) }, bb3_mt)
|
|
end
|
|
wirefame.bb3 = bb3
|
|
|
|
----------------------------
|
|
--## Wirefame functions ##--
|
|
----------------------------
|
|
|
|
--- Returns a list of vertices and faces from a .obj or .iqe file.
|
|
-- Supports geometric vertices (ignores w), point, lines and faces.
|
|
-- The parser will ignore textures, normals, free-form geometry, blends, meshes, materials, smoothing, poses, skeletons, animations.
|
|
-- Made using http://paulbourke.net/dataformats/obj/ and http://sauerbraten.org/iqm/iqe.txt
|
|
-- TODO: animations? Simplify? Rely on iqm-exm? etc
|
|
local function parseModel(path, color, args)
|
|
args = args or {}
|
|
|
|
local vertices = { n = 0 }
|
|
local faces = { n = 0 }
|
|
|
|
for line in io.lines(path) do
|
|
-- .obj --
|
|
|
|
-- Variable substitution
|
|
for i, a in ipairs(args) do
|
|
line = line:gsub("$"..tostring(i), tostring(a))
|
|
end
|
|
|
|
-- Read another .obj file: call filename arg1...
|
|
if line:match("^call%s") then
|
|
local file = line:match("^call%s+([^%s]+)")
|
|
local nargs = {}
|
|
for narg in (line:match("^call%s+[^%s]+(.*)")):gmatch("([^%s]+)") do
|
|
table.insert(nargs, narg)
|
|
end
|
|
local nvertices, nfaces = parseModel(file, color, nargs)
|
|
for _, v in ipairs(nvertices) do
|
|
table.insert(vertices, v)
|
|
end
|
|
for _, f in ipairs(nfaces) do
|
|
table.insert(faces, f)
|
|
end
|
|
-- Vertex: v x y z w
|
|
-- Ignores w.
|
|
elseif line:match("^v%s") then
|
|
local x, y, z = line:match("^v%s+([-%d.e]+)%s+([-%d.e]+)%s+([-%d.e]+)")
|
|
local vec = v3(tonumber(x), tonumber(y), tonumber(z))
|
|
vec.color = color
|
|
table.insert(vertices, vec)
|
|
-- Point: p v1 v2 v3 ...
|
|
-- Line: l v1/vt1 v2/vt2 v3/vt3 ...
|
|
-- Faces: f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3 ...
|
|
-- Ignores vt* and vn*.
|
|
elseif line:match("^[plf]%s") then
|
|
local face = {}
|
|
for vertex in line:gmatch("([-%d.e/]+)") do
|
|
table.insert(face, tonumber(vertex:match("^([-%d.e]+)"))) -- extract just the vertex number
|
|
end
|
|
face.n = #face
|
|
face.type = line:match("^([plf])%s")
|
|
table.insert(faces, face)
|
|
|
|
-- .iqe --
|
|
|
|
-- Vertex: vp x y z w
|
|
-- Ignores w.
|
|
elseif line:match("^vp%s") then
|
|
local x, y, z = line:match("^vp%s+([-%d.e]*)%s*([-%d.e]*)%s*([-%d.e]*)")
|
|
local vec = v3(tonumber(x) or 0, tonumber(y) or 0, tonumber(z) or 0)
|
|
vec.color = color
|
|
table.insert(vertices, vec)
|
|
-- Vertex color: vc r g b a
|
|
elseif line:match("^vc%s") then
|
|
local r, g, b, a = line:match("^vc%s+([-%d.e]*)%s*([-%d.e]*)%s*([-%d.e]*)%s*([-%d.e]*)")
|
|
vertices[#vertices].color = { tonumber(r) or 0, tonumber(g) or 0, tonumber(b) or 0, tonumber(a) or 1 }
|
|
-- Triangles: fa v1 v2 v3...
|
|
elseif line:match("^fa%s") then
|
|
local face = { type = "f" }
|
|
for vertex in line:gmatch("([-%d.e/]+)") do
|
|
table.insert(face, tonumber(vertex:match("^([-%d.e]+)"))) -- extract just the vertex number
|
|
end
|
|
face.n = #face
|
|
table.insert(faces, face)
|
|
end
|
|
end
|
|
|
|
-- Some post-processing on the parsed data
|
|
vertices.n = #vertices
|
|
faces.n = #faces
|
|
for i=1, faces.n do
|
|
local face = faces[i]
|
|
for j=1, face.n do
|
|
local vertex = face[j]
|
|
if vertex < 0 then -- Relative vertex number
|
|
face[j] = vertices.n - vertex + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
return vertices, faces
|
|
end
|
|
|
|
-- Model methods.
|
|
local model_mt = {
|
|
--- Called when the model is added to a scene.
|
|
onAddToScene = function(self, scene)
|
|
self.scene = scene
|
|
end,
|
|
|
|
--- Called when the model is removed from a scene.
|
|
onRemoveFromScene = function(self, scene)
|
|
self.scene = nil
|
|
end,
|
|
|
|
--- Clone the model.
|
|
clone = function(self)
|
|
return wirefame.model(self.lmesh, self.vertices, self.faces)
|
|
:setTransformStack(self.transformStack)
|
|
:setColor(self.color[1], self.color[2], self.color[3], self.color[4])
|
|
:tag(unpack(self:listTags()))
|
|
end,
|
|
|
|
--- Create a new group containing this entity
|
|
group = function(self)
|
|
return wirefame.group{self}
|
|
end,
|
|
|
|
-- Add tags on this model only.
|
|
tag = function(self, ...)
|
|
for _, t in ipairs({...}) do
|
|
self.tags[t] = true
|
|
end
|
|
return self
|
|
end,
|
|
-- Check tags on this model only.
|
|
is = function(self, ...)
|
|
local r = true
|
|
for _, t in ipairs({...}) do
|
|
r = r and self.tags[t]
|
|
end
|
|
return r
|
|
end,
|
|
-- List tags on this model only.
|
|
listTags = function(self)
|
|
local r = {}
|
|
for t, _ in pairs(self.tags) do
|
|
table.insert(r, t)
|
|
end
|
|
return r
|
|
end,
|
|
|
|
--- Get a group of all models recursively contained in this object with these tags.
|
|
get = function(self, ...)
|
|
if self:is(...) then
|
|
return wirefame.group{self}
|
|
else
|
|
return wirefame.group{}
|
|
end
|
|
end,
|
|
|
|
--- Apply a transformation matrix to the model vertices coordinates.
|
|
-- If no i is given, will transform the first matrix.
|
|
transform = function(self, i, tr)
|
|
if not tr then
|
|
i, tr = 1, i
|
|
elseif type(i) == "string" then
|
|
i = self.transformStack.names[i]
|
|
end
|
|
self.transformStack[i]:transform(tr)
|
|
self.transformStack.changed = true
|
|
return self
|
|
end,
|
|
--- Retrieve the ith transformation matrix.
|
|
-- If no i is given, will retrieve the first matrix.
|
|
getTransform = function(self, i)
|
|
if not i then
|
|
i = 1
|
|
elseif type(i) == "string" then
|
|
i = self.transformStack.names[i]
|
|
end
|
|
return self.transformStack[i]
|
|
end,
|
|
--- Set the ith transformation matrix.
|
|
-- If no i is given, will set the first matrix.
|
|
setTransform = function(self, i, tr)
|
|
if not tr then
|
|
i, tr = 1, i
|
|
elseif type(i) == "string" then
|
|
i = self.transformStack.names[i]
|
|
end
|
|
self.transformStack[i] = tr
|
|
self.transformStack.changed = true
|
|
return self
|
|
end,
|
|
--- Reset the ith transformation matrix.
|
|
-- If no i is given, will reset the first matrix.
|
|
resetTransform = function(self, i)
|
|
if not i then
|
|
i = 1
|
|
end
|
|
self.transformStack[i] = m4.identity()
|
|
self.transformStack.changed = true
|
|
return self
|
|
end,
|
|
|
|
--- Set the whole transformation stack.
|
|
setTransformStack = function(self, stack)
|
|
self.transformStack = { names = self.transformStack.names }
|
|
for i, tr in ipairs(stack) do
|
|
self.transformStack[i] = tr
|
|
end
|
|
self.transformStack.n = #self.transformStack
|
|
self.transformStack.changed = true
|
|
return self
|
|
end,
|
|
--- Retrieve the whole transformation stack.
|
|
getTransformStack = function(self)
|
|
return self.transformStack
|
|
end,
|
|
--- Set the size of the transformation stack, initiatializing new transformations matrices if needed.
|
|
setTransformStackSize = function(self, n)
|
|
self.transformStack.n = n
|
|
for i=1, self.transformStack.n, 1 do
|
|
if self.transformStack[i] == nil then
|
|
self.transformStack[i] = m4.identity()
|
|
end
|
|
end
|
|
self.transformStack.changed = true
|
|
return self
|
|
end,
|
|
--- Assign to each transform in the transformation stack a name and resize the transformation stack to match.
|
|
setTransformNames = function(self, list)
|
|
self:setTransformStackSize(#list)
|
|
local names = {}
|
|
for i, name in ipairs(list) do
|
|
names[name] = i
|
|
end
|
|
self.transformStack.names = names
|
|
end,
|
|
--- Assign a name to the transform i.
|
|
setTransformName = function(self, i, name)
|
|
self.transformStack.names[name] = i
|
|
end,
|
|
|
|
--- Calculate the final transformation matrix.
|
|
getFinalTransform = function(self)
|
|
if self.transformStack.changed then
|
|
self.finalTransform = m4.identity()
|
|
for i=1, self.transformStack.n, 1 do
|
|
self.finalTransform:transform(self.transformStack[i])
|
|
end
|
|
self.transformStack.changed = false
|
|
end
|
|
return self.finalTransform
|
|
end,
|
|
|
|
--- Common tranforms
|
|
translate = function(self, i, v)
|
|
if v then
|
|
return self:transform(i, m4.translate(v))
|
|
else
|
|
return self:transform(m4.translate(i))
|
|
end
|
|
end,
|
|
scale = function(self, i, v)
|
|
if v then
|
|
return self:transform(i, m4.scale(v))
|
|
else
|
|
return self:transform(m4.scale(i))
|
|
end
|
|
end,
|
|
rotate = function(self, i, a, v)
|
|
if v then
|
|
return self:transform(i, m4.rotate(a, v))
|
|
else
|
|
return self:transform(m4.rotate(i, a))
|
|
end
|
|
end,
|
|
shear = function(self, i, vxy, vyz)
|
|
if vyz then
|
|
return self:transform(i, m4.shear(vxy, vyz))
|
|
else
|
|
return self:transform(m4.shear(i, vxy))
|
|
end
|
|
end,
|
|
|
|
--- Returns the minimum bounding box.
|
|
boundingBox = function(self, tr)
|
|
local transform = self:getFinalTransform()
|
|
if tr then
|
|
transform = tr * transform
|
|
end
|
|
|
|
-- Create vertices if needed from lmesh
|
|
self:_meshToVertices()
|
|
|
|
-- Calculate bounding box
|
|
if self.vertices.n > 0 then
|
|
local v = self.vertices[1]:transform(transform)
|
|
local min, max = v:clone(), v:clone()
|
|
for i=2, self.vertices.n do
|
|
v = self.vertices[i]:transform(transform)
|
|
min[1] = math.min(min[1], v[1])
|
|
min[2] = math.min(min[2], v[2])
|
|
min[3] = math.min(min[3], v[3])
|
|
max[1] = math.max(max[1], v[1])
|
|
max[2] = math.max(max[2], v[2])
|
|
max[3] = math.max(max[3], v[3])
|
|
end
|
|
return bb3(min, max)
|
|
else
|
|
return bb3(v3(0,0,0), v3(0,0,0))
|
|
end
|
|
end,
|
|
|
|
--- Sets the color used to draw the model.
|
|
setColor = function(self, r, g, b, a)
|
|
self.color[1], self.color[2], self.color[3], self.color[4] = r, g, b, a or 1
|
|
return self
|
|
end,
|
|
getColor = function(self)
|
|
return self.color[1], self.color[2], self.color[3], self.color[4]
|
|
end,
|
|
|
|
--- Draw the 3D model, applying transformation tr, view distance viewDist and a camera sitting on z = trCamZ in screen coordinates.
|
|
drawWireframe = function(self, tr, viewDist, trCamZ)
|
|
local drawLine, drawPoint = wirefame.drawLine, wirefame.drawPoint
|
|
local setColor = wirefame.setColor
|
|
|
|
-- Create vertices if needed from lmesh
|
|
self:_meshToVertices()
|
|
|
|
-- Transformation matrix
|
|
local transform = tr * self:getFinalTransform()
|
|
|
|
-- Already transformed vertices cache
|
|
local trVertex = {}
|
|
|
|
-- Iterate faces and draw them
|
|
for i=1, self.faces.n do
|
|
local face = self.faces[i]
|
|
|
|
local n = face.n -- number of vertices in the face
|
|
local nV -- number of vertices to iterate
|
|
local draw -- draw function(v3(n).x, v3(n).y, v3(n+1).x, v3(n+1).y)
|
|
|
|
-- Face type
|
|
if face.type == "f" then
|
|
nV = n
|
|
draw = drawLine
|
|
elseif face.type == "l" then
|
|
nV = n -1 -- doesn't iterate on the last vertex, so no line between the last and first vertex
|
|
draw = drawLine
|
|
elseif face.type == "p" then
|
|
nV = n
|
|
draw = drawPoint
|
|
end
|
|
|
|
for j=1, nV do
|
|
local vertex1, vertex2 = face[j], face[j%n+1]
|
|
|
|
-- Transform
|
|
if trVertex[vertex1] == nil then
|
|
trVertex[vertex1] = self.vertices[vertex1]:transform(transform)
|
|
end
|
|
if trVertex[vertex2] == nil then
|
|
trVertex[vertex2] = self.vertices[vertex2]:transform(transform)
|
|
end
|
|
|
|
local tr1, tr2 = trVertex[vertex1], trVertex[vertex2]
|
|
local tr1Z, tr2Z = tr1[3], tr2[3]
|
|
local col = self.vertices[vertex1].color
|
|
|
|
-- Ignore if a vertex is behind camera
|
|
if tr1Z < trCamZ and tr2Z < trCamZ then
|
|
-- Distance between camera and middle of the segment
|
|
-- distCam = 0 if the distance is equal to viewDist, becoming closer to viewDist if the object is closer to the camera)
|
|
local distCam = viewDist - (trCamZ - (tr1Z+tr2Z)/2)
|
|
if distCam > 0 then
|
|
setColor(col[1], col[2], col[3], distCam * col[4]/viewDist)
|
|
draw(tr1[1], tr1[2], tr2[1], tr2[2])
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end,
|
|
|
|
--- Send all relevant uniforms to the shader
|
|
sendToShader = function(self, lshader)
|
|
end,
|
|
|
|
-- Draw the model using LÖVE. Use OpenGL/hardware acceleration, but it's not actually wireframe anymore.
|
|
drawLove = function(self, lshader, transp, tr, col)
|
|
-- Create mesh if needed from .obj data
|
|
self:_verticesToMesh()
|
|
|
|
-- Transformation matrix
|
|
if tr then
|
|
lshader:send("model_transform", tr * self:getFinalTransform())
|
|
else
|
|
lshader:send("model_transform", self:getFinalTransform())
|
|
end
|
|
|
|
-- Color
|
|
if col then
|
|
col = { self.color[1] * col[1], self.color[2] * col[2], self.color[3] * col[3], self.color[4] * col[4] }
|
|
else
|
|
col = self.color
|
|
end
|
|
|
|
-- Transparency sort
|
|
if transp then
|
|
if col[4] >= 1 then return end
|
|
else
|
|
if col[4] < 1 then return end
|
|
end
|
|
|
|
-- Draw
|
|
lg.setColor(col)
|
|
if type(self.lmesh) == "function" then
|
|
self.lmesh()
|
|
else
|
|
lg.draw(self.lmesh)
|
|
end
|
|
end,
|
|
|
|
--- Private ---
|
|
_meshToVertices = function(self)
|
|
if not self.vertices and not self.faces then
|
|
local lmesh = self.lmesh
|
|
|
|
local vertices = { n = 0 }
|
|
local faces = { n = 0 }
|
|
|
|
if type(lmesh) == "userdata" and lmesh:type() == "Mesh" then
|
|
-- Get format
|
|
local iposition, icolor
|
|
local i = 1
|
|
for _, t in ipairs(lmesh:getVertexFormat()) do
|
|
if t[1] == "VertexPosition" then
|
|
if t[3] ~= 3 then error("mesh vertices don't have a 3 dimensional position") end
|
|
iposition = i
|
|
elseif t[1] == "VertexColor" then
|
|
if t[3] ~= 4 then error("mesh color doesn't have 4 components") end
|
|
icolor = i
|
|
end
|
|
i = i + t[3]
|
|
end
|
|
if not iposition or not icolor then
|
|
error("mesh doesn't have VertexPosition and/or VertexColor attributes")
|
|
end
|
|
|
|
-- Retrieve vertices
|
|
local lmap = lmesh:getVertexMap()
|
|
for i=1, #lmap do
|
|
local attr = { lmesh:getVertex(lmap[i]) }
|
|
local x, y, z, r, g, b, a = attr[iposition], attr[iposition+1], attr[iposition+2], attr[icolor], attr[icolor+1], attr[icolor+2], attr[icolor+3]
|
|
local v = v3(x, y, z)
|
|
v.color = { r, g, b, a }
|
|
table.insert(vertices, v)
|
|
end
|
|
vertices.n = #vertices
|
|
|
|
-- Retrieve faces
|
|
if lmesh:getDrawMode() == "triangles" then
|
|
for i=1, #lmap-2, 3 do
|
|
table.insert(faces, { n = 3, type = "f", i, i+1, i+2 })
|
|
end
|
|
elseif lmesh:getDrawMode() == "fan" then
|
|
for i=1, #lmap-1, 2 do
|
|
table.insert(faces, { n = 3, type = "f", 1, i, i+1 })
|
|
end
|
|
else
|
|
error("unsuported mesh drawing mode "..lmesh:getDrawMode())
|
|
end
|
|
faces.n = #faces
|
|
else
|
|
error(("wirefame conversion of a %s is not supported"):format(type(lmesh) == "userdata" and lmesh:type() or tostring(lmesh)))
|
|
end
|
|
|
|
self.vertices, self.faces = vertices, faces
|
|
end
|
|
end,
|
|
_verticesToMesh = function(self)
|
|
if not self.lmesh then
|
|
local vertices, faces = self.vertices, self.faces
|
|
|
|
-- Get vertices list
|
|
local lvertices = {}
|
|
for i=1, vertices.n do
|
|
local v = vertices[i]
|
|
v.normal = v3(0, 0, 0)
|
|
table.insert(lvertices, { v[1], v[2], v[3], v.color[1], v.color[2], v.color[3] })
|
|
end
|
|
|
|
-- Get vertex map
|
|
local lmap = {}
|
|
for i=1, faces.n do
|
|
local f = faces[i]
|
|
if f.type == "f" then
|
|
for j=3, f.n do
|
|
table.insert(lmap, f[1])
|
|
table.insert(lmap, f[j-1])
|
|
table.insert(lmap, f[j])
|
|
-- add triangle normal to vertices
|
|
local triangleNormal = (vertices[f[j-1]]-vertices[f[1]]):cross(vertices[f[j]]-vertices[f[1]]):normalizeInPlace()
|
|
vertices[f[1]].normal = vertices[f[1]].normal + triangleNormal
|
|
vertices[f[j-1]].normal = vertices[f[j-1]].normal + triangleNormal
|
|
vertices[f[j]].normal = vertices[f[j]].normal + triangleNormal
|
|
end
|
|
elseif f.type == "l" then
|
|
for j=2, f.n do
|
|
table.insert(lmap, f[j-1])
|
|
table.insert(lmap, f[j-1])
|
|
table.insert(lmap, f[j])
|
|
end
|
|
elseif f.type == "p" then
|
|
for j=1, f.n do
|
|
table.insert(lmap, f[j])
|
|
table.insert(lmap, f[j])
|
|
table.insert(lmap, f[j])
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Get vertex normals
|
|
for i=1, vertices.n do
|
|
local v = vertices[i]
|
|
local x, y, z = v.normal:normalizeInPlace():unpack()
|
|
table.insert(lvertices[i], x)
|
|
table.insert(lvertices[i], y)
|
|
table.insert(lvertices[i], z)
|
|
end
|
|
|
|
-- Send vertices
|
|
local lmesh = lg.newMesh({
|
|
{ "VertexPosition", "float", 3 },
|
|
{ "VertexColor", "float", 3 },
|
|
{ "VertexNormal", "float", 3 }
|
|
}, #lvertices > 0 and lvertices or 1, "triangles")
|
|
|
|
-- Send vertex map
|
|
lmesh:setVertexMap(lmap)
|
|
|
|
self.lmesh = lmesh
|
|
end
|
|
end
|
|
}
|
|
model_mt.__index = model_mt
|
|
|
|
--- Loads an .obj/.iqe file, or import a LÖVE drawable, or import a drawing function, or create a empty model.
|
|
function wirefame.model(path, _vertices, _faces)
|
|
local model = {
|
|
-- Size
|
|
n = 1,
|
|
true,
|
|
|
|
-- Tags
|
|
tags = {},
|
|
|
|
-- Model transformation matrices
|
|
transformStack = { n = 1, changed = true, names = {}, m4.identity() },
|
|
finalTransform = nil,
|
|
|
|
-- Model color
|
|
color = { wirefame.defaultColor[1], wirefame.defaultColor[2], wirefame.defaultColor[3], wirefame.defaultColor[4] },
|
|
|
|
-- List of vertices, each vertices being a 3D vector with a color property.
|
|
vertices = nil,
|
|
-- List of faces, lines and points, each item being a table { type = "t", n = #face, vertex1index<integer>, vertex2index<integer>, ... }
|
|
faces = nil,
|
|
|
|
-- LÖVE-specific: drawable object (ideally a mesh with the correct vertex format)
|
|
lmesh = nil,
|
|
|
|
-- Hierarchy
|
|
parents = {},
|
|
scene = nil
|
|
}
|
|
|
|
-- Empty model
|
|
if path == nil then
|
|
model.vertices, model.faces = { n = 0 }, { n = 0 }
|
|
-- Load an .obj file
|
|
elseif type(path) == "string" then
|
|
model.vertices, model.faces = parseModel(path, model.color)
|
|
-- Function
|
|
elseif type(path) == "function" then
|
|
model.lmesh = path
|
|
model.vertices, model.faces = _vertices, _faces
|
|
-- Convertible to LÖVE meshes.
|
|
elseif type(path) == "userdata" and path:type() == "Image" or path:type() == "Canvas" then
|
|
model.vertices, model.faces = _vertices, _faces
|
|
model.lmesh = love.graphics.newMesh({
|
|
{ "VertexPosition", "float", 3 },
|
|
{ "VertexTexCoord", "float", 2 },
|
|
{ "VertexColor", "byte", 4 },
|
|
{ "VertexNormal", "float", 3 }
|
|
},
|
|
{
|
|
{
|
|
0, 0, 0,
|
|
0, 0,
|
|
1, 1, 1, 1,
|
|
0, 0, -1
|
|
},
|
|
{
|
|
0, path:getHeight(), 0,
|
|
0, 1,
|
|
1, 1, 1, 1,
|
|
0, 0, -1
|
|
},
|
|
{
|
|
path:getWidth(), path:getHeight(), 0,
|
|
1, 1,
|
|
1, 1, 1, 1,
|
|
0, 0, -1
|
|
},
|
|
{
|
|
path:getWidth(), 0, 0,
|
|
1, 0,
|
|
1, 1, 1, 1,
|
|
0, 0, -1
|
|
}
|
|
}, "fan")
|
|
model.lmesh:setVertexMap(1, 2, 3, 4)
|
|
model.lmesh:setTexture(path)
|
|
else
|
|
model.lmesh = path
|
|
model.vertices, model.faces = _vertices, _faces
|
|
end
|
|
|
|
-- Create & return object
|
|
return setmetatable(model, model_mt)
|
|
end
|
|
|
|
--- Light methods.
|
|
local lightType = {
|
|
none = 0,
|
|
ambient = 1,
|
|
directional = 2,
|
|
point = 3
|
|
}
|
|
local light_mt = {
|
|
-- Rewrite model methods
|
|
sendToShader = function(self, lshader)
|
|
lshader:send("lights["..(self.index-1).."].type", self.type)
|
|
lshader:send("lights["..(self.index-1).."].position", self.position:transform(self:getFinalTransform()))
|
|
lshader:send("lights["..(self.index-1).."].color", self.color)
|
|
end,
|
|
onAddToScene = function(self, scene)
|
|
self.scene = scene
|
|
-- create array if needed
|
|
if not scene.shader.define.LIGHT_COUNT then
|
|
scene.shader.define.LIGHT_COUNT = 0
|
|
scene.shader.lights = {}
|
|
end
|
|
-- find free spot
|
|
self.index = nil
|
|
for i=1, scene.shader.define.LIGHT_COUNT, 1 do
|
|
if not scene.shader.lights[i] then
|
|
self.index = i
|
|
break
|
|
end
|
|
end
|
|
if not self.index then -- create new spot
|
|
scene.shader.define.LIGHT_COUNT = scene.shader.define.LIGHT_COUNT + 1
|
|
self.index = scene.shader.define.LIGHT_COUNT
|
|
scene.shader.changed = true
|
|
else
|
|
self:sendToShader() -- update already existing array in shader
|
|
end
|
|
-- register oneself
|
|
scene.shader.lights[self.index] = true
|
|
end,
|
|
onRemoveFromScene = function(self, scene)
|
|
self.scene = nil
|
|
-- unregister oneself
|
|
scene.shader.lights[self.index] = nil
|
|
-- recalculate minimal array size
|
|
for i=scene.shader.define.LIGHT_COUNT, 1, -1 do
|
|
if scene.shader.lights[i] then
|
|
break
|
|
else
|
|
scene.shader.define.LIGHT_COUNT = scene.shader.define.LIGHT_COUNT - 1 -- will update on next recompile, nothing is urgent
|
|
end
|
|
end
|
|
-- update oneself in shader
|
|
scene.shader.lshader:send("lights["..(self.index-1).."].type", lightType.none)
|
|
end,
|
|
boundingBox = function(self)
|
|
return bb3(v3(0,0,0), v3(0,0,0))
|
|
end,
|
|
setColor = function(self, ...)
|
|
model_mt.setColor(self, ...)
|
|
self.resend = true
|
|
return self
|
|
end,
|
|
drawWireframe = function(self) end,
|
|
drawLove = function(self)
|
|
-- update position and stuff
|
|
if self.transformStack.changed or self.resend then
|
|
self:sendToShader(self.scene.shader.lshader)
|
|
self.resend = false
|
|
end
|
|
end
|
|
}
|
|
for k, v in pairs(model_mt) do
|
|
if light_mt[k] == nil then
|
|
light_mt[k] = v
|
|
end
|
|
end
|
|
light_mt.__index = light_mt
|
|
|
|
--- Create a light object
|
|
wirefame.light = function(type)
|
|
local light = {
|
|
-- Light
|
|
type = lightType[type],
|
|
position = v3(0, 0, 0),
|
|
color = { wirefame.defaultColor[1], wirefame.defaultColor[2], wirefame.defaultColor[3], wirefame.defaultColor[4] },
|
|
resend = false,
|
|
|
|
-- Size
|
|
n = 1,
|
|
true,
|
|
|
|
-- Tags
|
|
tags = {},
|
|
|
|
-- Group transformation matrices
|
|
transformStack = { n = 1, changed = true, names = {}, m4.identity() },
|
|
finalTransform = nil,
|
|
|
|
-- Hierarchy
|
|
parents = {},
|
|
scene = nil
|
|
}
|
|
|
|
-- Create & return object
|
|
return setmetatable(light, light_mt)
|
|
end
|
|
|
|
--- Group methods.
|
|
local group_mt = {
|
|
--- Add a model to the node.
|
|
add = function(self, ...)
|
|
for _, m in ipairs({...}) do
|
|
self.n = self.n + 1
|
|
table.insert(self, m)
|
|
table.insert(m.parents, self)
|
|
if self.scene and not m.scene then
|
|
m:onAddToScene(self.scene)
|
|
end
|
|
end
|
|
return self
|
|
end,
|
|
|
|
--- Remove a model from the node.
|
|
remove = function(self, ...)
|
|
for _, m in ipairs({...}) do
|
|
for i=1, self.n do
|
|
if self[i] == m then
|
|
self.n = self.n - 1
|
|
table.remove(self, i)
|
|
for j, p in ipairs(m.parents) do
|
|
if p == self then
|
|
table.remove(m.parents, j)
|
|
break
|
|
end
|
|
end
|
|
if m.scene and #m.parents == 0 then
|
|
m:onRemoveFromScene(m.scene)
|
|
end
|
|
return self
|
|
end
|
|
end
|
|
end
|
|
error("can't find model to remove")
|
|
end,
|
|
|
|
--- All these methods and properties of model are present, but operate on the whole group.
|
|
|
|
-- Overwrite some model methods:
|
|
onAddToScene = function(self, scene)
|
|
self.scene = scene
|
|
for i=1, self.n do
|
|
self[i]:onAddToScene(scene)
|
|
end
|
|
end,
|
|
onRemoveFromScene = function(self, scene)
|
|
self.scene = nil
|
|
for i=1, self.n do
|
|
self[i]:onRemoveFromScene(scene)
|
|
end
|
|
end,
|
|
clone = function(self)
|
|
local l = {}
|
|
for i=1, self.n do table.insert(l, self[i]:clone()) end
|
|
return wirefame.group(l)
|
|
:setTransformStack(self.transformStack)
|
|
:setColor(self.color[1], self.color[2], self.color[3], self.color[4])
|
|
:tag(unpack(self:listTags()))
|
|
end,
|
|
get = function(self, ...)
|
|
local l = {}
|
|
for i=1, self.n do
|
|
local il = self[i]:get(...)
|
|
if #il > 0 then
|
|
table.insert(l, il)
|
|
end
|
|
end
|
|
return wirefame.group(l)
|
|
:setTransformStack(self.transformStack)
|
|
:setColor(self.color[1], self.color[2], self.color[3], self.color[4])
|
|
:tag(unpack(self:listTags()))
|
|
end,
|
|
boundingBox = function(self, tr)
|
|
local trans = self:getFinalTransform()
|
|
if tr then trans = tr * trans end
|
|
if #self > 0 then
|
|
local r = self[1]:boundingBox(trans)
|
|
for i=2, self.n do
|
|
local b = self[i]:boundingBox(trans)
|
|
r.min[1] = math.min(r.min[1], b.min[1])
|
|
r.min[2] = math.min(r.min[2], b.min[2])
|
|
r.min[3] = math.min(r.min[3], b.min[3])
|
|
r.max[1] = math.max(r.max[1], b.max[1])
|
|
r.max[2] = math.max(r.max[2], b.max[2])
|
|
r.max[3] = math.max(r.max[3], b.max[3])
|
|
end
|
|
return r
|
|
else
|
|
return bb3(v3(0,0,0), v3(0,0,0))
|
|
end
|
|
end,
|
|
drawWireframe = function(self, tr, viewDist, trCamZ)
|
|
local trans = tr * self.transform
|
|
for i=1, self.n do
|
|
local r, g, b, a = self[i]:getColor()
|
|
self[i]:setColor(self.color[1] * r, self.color[2] * g, self.color[3] * b, self.color[4] * a)
|
|
self[i]:drawWireframe(trans, viewDist, trCamZ)
|
|
self[i]:setColor(r, g, b, a)
|
|
end
|
|
end,
|
|
sendToShader = function(self, lshader)
|
|
for i=1, self.n do
|
|
self[i]:sendToShader(lshader)
|
|
end
|
|
end,
|
|
drawLove = function(self, lshader, transp, tr, col)
|
|
local trans = self:getFinalTransform()
|
|
if tr then trans = tr * trans end
|
|
if col then col = { self.color[1] * col[1], self.color[2] * col[2], self.color[3] * col[3], self.color[4] * col[4] } end
|
|
for i=1, self.n do
|
|
self[i]:drawLove(lshader, transp, trans, col)
|
|
end
|
|
end
|
|
}
|
|
for k, v in pairs(model_mt) do
|
|
if group_mt[k] == nil then
|
|
group_mt[k] = v
|
|
end
|
|
end
|
|
group_mt.__index = group_mt
|
|
|
|
--- Create a group of models.
|
|
wirefame.group = function(t)
|
|
local group = {
|
|
-- Size
|
|
n = 0,
|
|
|
|
-- Tags
|
|
tags = {},
|
|
|
|
-- Group transformation matrices
|
|
transformStack = { n = 1, changed = true, names = {}, m4.identity() },
|
|
finalTransform = nil,
|
|
|
|
-- Group color
|
|
color = { wirefame.defaultColor[1], wirefame.defaultColor[2], wirefame.defaultColor[3], wirefame.defaultColor[4] },
|
|
|
|
-- Hierarchy
|
|
parents = {},
|
|
scene = nil
|
|
}
|
|
|
|
-- Create & return object
|
|
return setmetatable(group, group_mt):add(unpack(t))
|
|
end
|
|
|
|
--- Scene methods
|
|
local scene_mt = {
|
|
--- Sets the viewport matrix. Not used when rendering with LÖVE (handled by OpenGL).
|
|
setViewport = function(self, x, y, width, height)
|
|
return self:setTransform(4, m4.translate{x + width/2, y + height/2, 0} * m4.scale{width/2, height/2, 1})
|
|
end,
|
|
|
|
--- Set the projection matrix.
|
|
setPerspective = function(self, fovy, ratio, near, far)
|
|
return self:setTransform(3, m4.perspective(fovy, ratio, near, far))
|
|
end,
|
|
|
|
--- Sets the view and projection matrix.
|
|
lookAt = function(self, eye, center, up)
|
|
self.camera = v3(eye)
|
|
return self:setTransform(2, m4.lookAt(eye, center, up))
|
|
end,
|
|
|
|
--- Sets the view distance, in world units (distance from the camera).
|
|
setViewDistance = function(self, viewDist)
|
|
self.viewDistance = viewDist
|
|
return self
|
|
end,
|
|
|
|
--- Get model group with tags.
|
|
get = function(self, ...)
|
|
local r = {}
|
|
for _, m in ipairs(self) do
|
|
local il = m:get(...)
|
|
if #il > 0 then
|
|
table.insert(r, il)
|
|
end
|
|
end
|
|
return wirefame.group(r)
|
|
end,
|
|
|
|
--- Rebuild the shader
|
|
rebuildShader = function(self)
|
|
if self.shader.changed then
|
|
if self.shader.lshader then self.shader.lshader:release() end
|
|
local s = ""
|
|
for var, val in pairs(self.shader.define) do
|
|
s = s .. ("# define %s %s"):format(var, val) .. "\n"
|
|
end
|
|
self.shader.lshader = lg.newShader(s .. lshaderCode)
|
|
self.shader.lshader:send("scene_transform", m4.identity())
|
|
self.shader.lshader:send("model_transform", m4.identity())
|
|
for _, model in ipairs(self) do model:sendToShader(self.shader.lshader) end
|
|
self.shader.changed = false
|
|
end
|
|
return self.shader.lshader
|
|
end,
|
|
|
|
--- Draws the scene.
|
|
drawWireframe = function(self)
|
|
-- Calculate transformation matrix
|
|
local transform = self:getFinalTransform()
|
|
|
|
-- Transformed camera Z coordinates
|
|
local trCamZ = self.camera:transform(self.viewport * self.view)[3]
|
|
|
|
-- Draw models
|
|
for _, model in ipairs(self) do
|
|
model:drawWireframe(transform, self.viewDistance, trCamZ)
|
|
end
|
|
end,
|
|
|
|
--- Draw the scene using LÖVE. Use OpenGL/hardware acceleration, but it's not actually wireframe anymore.
|
|
drawLove = function(self)
|
|
-- Init shader
|
|
local lshader = self:rebuildShader()
|
|
|
|
-- Calculate transformation matrix
|
|
lshader:send("scene_transform", self:getFinalTransform())
|
|
|
|
-- Draw models
|
|
lg.setShader(lshader)
|
|
for _, model in ipairs(self) do
|
|
model:drawLove(lshader, false) -- draw opaque models
|
|
end
|
|
for _, model in ipairs(self) do
|
|
model:drawLove(lshader, true) -- draw transparent models
|
|
end
|
|
lg.setShader()
|
|
end,
|
|
|
|
-- And every group method
|
|
}
|
|
for k, v in pairs(group_mt) do
|
|
if scene_mt[k] == nil then
|
|
scene_mt[k] = v
|
|
end
|
|
end
|
|
scene_mt.__index = scene_mt
|
|
|
|
--- Create a scene.
|
|
wirefame.scene = function()
|
|
local scene = {
|
|
-- Other scene variables
|
|
camera = v3(0, 0, 0), -- camera position
|
|
viewDistance = 0, -- view distance, in world units
|
|
shader = {
|
|
changed = true, -- rebuild shader
|
|
define = {}, -- map of variables to define in the shader
|
|
lshader = nil, -- the shader
|
|
lights = {} -- list of used lights slots, see light_mt
|
|
},
|
|
|
|
-- Size
|
|
n = 0,
|
|
|
|
-- Tags
|
|
tags = {},
|
|
|
|
-- Scene transformation matrices
|
|
transformStack = {
|
|
n = 4,
|
|
changed = true,
|
|
names = {},
|
|
m4.identity(), -- Custom: user-defined transformation to the models world coordinates in the scene (excluding camera)
|
|
m4.identity(), -- View: world coordinates -> camera coordinates
|
|
m4.identity(), -- Projection: camera coordinates -> perspective camera coordinates
|
|
m4.identity(), -- Viewport: perspective camera coordinates -> screen coordinates
|
|
},
|
|
finalTransform = nil,
|
|
|
|
-- Group color
|
|
color = { wirefame.defaultColor[1], wirefame.defaultColor[2], wirefame.defaultColor[3], wirefame.defaultColor[4] },
|
|
|
|
-- Hierarchy
|
|
parents = {},
|
|
scene = nil
|
|
}
|
|
scene.scene = scene
|
|
|
|
-- Create & return object
|
|
return setmetatable(scene, scene_mt)
|
|
end
|
|
|
|
return wirefame
|