1
0
Fork 0
mirror of https://github.com/Reuh/wirefame.git synced 2025-10-27 09:39:30 +00:00
This commit is contained in:
Étienne Fildadut 2021-02-18 17:22:21 +01:00
parent b4798c8c16
commit 77cfbaad52
24 changed files with 3033 additions and 1545 deletions

View file

@ -1 +1,33 @@
return require((...)..".wirefame") --- 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 },
--- Model loaders
loader = {
gltf = require((...)..".loader.gltf")
},
-- Math classes
m4 = require((...)..".math.m4"),
v3 = require((...)..".math.v3"),
bb3 = require((...)..".math.bb3"),
bs3 = require((...)..".math.bs3"),
-- Shader code
shader = {
render = require((...)..".shader.render"),
pick = require((...)..".shader.pick")
}
}
package.loaded[(...)] = wirefame
--- Load Wirefame functions.
require((...)..".wirefame.model")
require((...)..".wirefame.light")
require((...)..".wirefame.group")
require((...)..".wirefame.scene")
return wirefame

1
lib/json Submodule

@ -0,0 +1 @@
Subproject commit d1e3b0f5d0f3d3493c7dadd0bb54135507fcebd7

391
loader/gltf.can Normal file
View file

@ -0,0 +1,391 @@
let json = require((...):match("^(.*)loader%.gltf$").."lib.json")
let dunpack = string.unpack or love.data.unpack
let v3 = require((...):match("^(.*)loader%.gltf$").."math.v3")
let m4 = require((...):match("^(.*)loader%.gltf$").."math.m4")
let qt = require((...):match("^(.*)loader%.gltf$").."math.qt")
let bb3 = require((...):match("^(.*)loader%.gltf$").."math.bb3")
let bs3 = require((...):match("^(.*)loader%.gltf$").."math.bs3")
let lg = love.graphics
let drawNode = :(shader)
if @mesh then
shader:send("model_transform", @finalMatrix)
for _, primitive in ipairs(@mesh.primitives) do
if shader:hasUniform("baseColorFactor") then
shader:send("baseColorFactor", primitive.material.pbrMetallicRoughness.baseColorFactor)
end
lg.draw(primitive.mesh)
end
end
for _, child in ipairs(@children) do
drawNode(child, shader)
end
end
let setTransform = :(tr)
@finalMatrix = tr * @matrix
for _, child in ipairs(@children) do
setTransform(child, tr)
end
end
let boundingBox = :(bb)
if @mesh then
for _, primitive in ipairs(@mesh.primitives) do
if not bb then
bb = bb3.fromMesh(primitive.mesh)
else
bb:include(bb3.fromMesh(primitive.mesh))
end
end
end
for _, child in ipairs(@children) do
bb = boundingBox(child, bb)
end
return bb
end
let boundingSphere = :(bs)
if @mesh then
for _, primitive in ipairs(@mesh.primitives) do
if not bs then
bs = bs3.fromMesh(primitive.mesh)
else
bs:include(bs3.fromMesh(primitive.mesh))
end
end
end
for _, child in ipairs(@children) do
bs = boundingBox(child, bs)
end
return bs
end
let gltf_mt = {
data = nil,
draw = :(shader)
for _, node in ipairs(@data.scene.nodes) do
drawNode(node, shader)
end
end,
setTransform = :(tr)
for _, node in ipairs(@data.scene.nodes) do
setTransform(node, tr)
end
end,
boundingBox = :()
local bb
for _, node in ipairs(@data.scene.nodes) do
bb = boundingBox(node, bb)
end
return bb
end,
boundingSphere = :()
local bs
for _, node in ipairs(@data.scene.nodes) do
bs = boundingSphere(node, bs)
end
return bs
end
}
gltf_mt.__index = gltf_mt
let attributeName = {
POSITION = "VertexPosition",
NORMAL = "VertexNormal",
TANGENT = "VertexTangent",
TEXCOORD_0 = "VertexTexCoord",
TEXCOORD_1 = "VertexTexCoord1",
COLOR_0 = "VertexColor",
JOINTS_0 = "VertexJoints",
WEIGHTS_0 = "VertexWeights"
}
let componentType = {
[5120] = "byte",
[5121] = "unsigned byte",
[5122] = "short",
[5123] = "unsigned short",
[5125] = "int",
[5126] = "float"
}
let mode = {
[0] = "points",
[1] = "lines",
[2] = "line_loop",
[3] = "line_strip",
[4] = "triangles",
[5] = "strip",
[6] = "fan"
}
--- Load a glTF file and returns it.
-- The Lua table returned mirror the glTF structure, except:
-- * nodes, buffers, etc. referenced using indices are replaced by an actual reference to the object
-- * node.matrix are replaced by corresponding mat4 objects and is calculated from TRS when undefined
-- * node.rotation, node.translation, node.scale replaced by qt and v3
-- * optional fields are intialized whith their standard default value (if any)
-- * enums number are replaced by the corresponding string (accessor.componentType, primitive.mode)
-- new fields:
-- * camera.matrix: the projection matrix
-- * node.finalMatrix: the full transformation matrix for this node, including stuff done from wirefame
-- * set a data field in buffer with the decoded/loaded data as a string
-- * set a data field in accessors as a list of components (either list of scalar or list of list)
-- * accessor.components contains component size
-- * the default material is created at materials[0]
-- * objects with name are will have an associated field in list where they are present
-- This implementation will not perform data consistency checks and have absolute trust in the exporter.
let gltf = (file)
let f = assert(io.open(file, "r"))
let t = json.decode(f:read("*a"))
f:close()
-- asset
if t.asset.minVersion then
let maj, min = t.asset.minVersion:match("^(%d+)%.(%d+)$")
assert(maj == "2" and min == "0", "asset require at least glTF version %s.%s but we only support 2.0":format(maj, min))
else
let maj, min = t.asset.version:match("^(%d+)%.(%d+)$")
assert(maj == "2", "asset require glTF version %s.%s but we only support 2.x":format(maj, min))
end
-- empty lists
t.nodes or= {}
t.scenes or= {}
t.cameras or= {}
t.meshes or= {}
t.buffers or= {}
t.bufferViews or = {}
t.accessors or= {}
t.materials or= {}
t.textures or= {}
t.images or= {}
t.samplers or= {}
t.skins or= {}
t.animations or= {}
-- scenes
for _, scene in ipairs(t.scenes) do
if scene.name then t.scenes[scene.name] = scene end
for i, node in ipairs(scene.nodes) do
scene.nodes[i] = t.nodes[node+1]
if scene.nodes[i].name then scene.nodes[scene.nodes[i].name] = scene.nodes[i] end
end
end
-- scene
if t.scene then
t.scene = t.scenes[t.scene+1]
end
-- nodes
for _, node in ipairs(t.nodes) do
if node.name then t.nodes[node.name] = node end
node.children or= {}
for i, child in ipairs(node.children) do
node.children[i] = t.nodes[child+1]
end
if node.matrix then
node.matrix = m4.fromColumnMajor(node.matrix)
else
node.translation or= {0,0,0}
node.rotation or= {0,0,0,1}
node.scale or= {1,1,1}
node.translation = v3(node.translation)
node.rotation = qt(node.rotation)
node.scale = v3(node.scale)
node.matrix = m4.translate(node.translation) * node.rotation:toM4() * m4.scale(node.scale)
end
if node.mesh then
node.mesh = t.meshes[node.mesh+1]
end
if node.camera then
node.camera = t.cameras[node.camera+1]
end
end
-- buffers
for i, buffer in ipairs(t.buffers) do
if i == 1 and not buffer.uri then
error("no support for glb-stored buffer") -- TODO
end
if buffer.uri:match("data:") then
local data = buffer.uri:match("^data:.-,(.*)$")
if buffer.uri:match("^data:.-;base64,") then
buffer.data = love.data.decode("string", "base64", data):sub(1, buffer.byteLength+1)
else
buffer.data = data:gsub("%%(%x%x)", (hex)
return love.data.decode("string", "hex", hex)
end):sub(1, buffer.byteLength+1)
end
else
let f = assert(io.open(buffer.uri, "r"), "can't find ressource %s":format(buffer.uri))
let s = f:read("*a")
f:close()
buffer.data = s:sub(1, buffer.byteLength+1)
end
end
-- bufferViews
for _, view in ipairs(t.bufferViews) do
view.buffer = t.buffers[view.buffer+1]
view.byteOffset or= 0
-- TODO target
end
-- accessors
for _, accessor in ipairs(t.accessors) do
accessor.bufferView = t.bufferViews[accessor.bufferView+1]
accessor.byteOffset or= 0
let view = accessor.bufferView
let data = view.buffer.data
let fmt, size
accessor.componentType = componentType[accessor.componentType]
if accessor.componentType == "byte" then
fmt, size = "b", 1
elseif accessor.componentType == "unsigned byte" then
fmt, size = "B", 1
elseif accessor.componentType == "short" then
fmt, size = "h", 2
elseif accessor.componentType == "unsigned short" then
fmt, size = "H", 2
elseif accessor.componentType == "unsigned int" then
fmt, size = "I4", 4
elseif accessor.componentType == "float" then
fmt, size = "f", 4
end
if accessor.type == "SCALAR" then
accessor.components, fmt = 1, fmt
elseif accessor.type == "VEC2" then
accessor.components, fmt = 2, fmt:rep(2)
elseif accessor.type == "VEC3" then
accessor.components, fmt = 3, fmt:rep(3)
elseif accessor.type == "VEC4" then
accessor.components, fmt = 4, fmt:rep(4)
elseif accessor.type == "MAT2" then
accessor.components = 4
fmt = (fmt:rep(2) .. "x":rep(4 - (size*2)%4)):rep(2) -- padding at each column start
elseif accessor.type == "MAT3" then
accessor.components = 9
fmt = (fmt:rep(3) .. "x":rep(4 - (size*3)%4)):rep(3)
elseif accessor.type == "MAT4" then
accessor.components = 16
fmt = (fmt:rep(4) .. "x":rep(4 - (size*4)%4)):rep(4)
end
fmt =.. "<" -- little endian
accessor.data = {}
let i = view.byteOffset+1 + accessor.byteOffset
let stop = view.byteOffset+1 + view.byteLength
let count = 0
while i < stop and count < accessor.count do
local d = { dunpack(fmt, data, i) }
d[#d] = nil
if accessor.components > 1 then
table.insert(accessor.data, d)
else
table.insert(accessor.data, d[1])
end
count += 1
i += view.byteStride or (size * accessor.components)
end
-- TODO sparse accessor
end
-- default material
t.materials[0] = {
pbrMetallicRoughness = {
baseColorFactor = {1,1,1,1},
metallicFactor = 1,
roughnessFactor = 1
},
emissiveFactor = {0,0,0},
alphaMode = "OPAQUE",
alphaCutoff = .5,
doubleSided = false
}
for _, material in ipairs(t.materials) do
material.pbrMetallicRoughness or= {}
material.pbrMetallicRoughness.baseColorFactor or= {1,1,1,1}
material.pbrMetallicRoughness.metallicFactor or= 1
material.pbrMetallicRoughness.roughnessFactor or= 1
material.emissiveFactor or= {0,0,0}
material.alphaMode or= "OPAQUE"
material.alphaCutoff or= .5
material.doubleSided or= false
end
-- meshes
for _, mesh in ipairs(t.meshes) do
for _, primitive in ipairs(mesh.primitives) do
let vertexformat = {}
let vertices = {}
for n, v in pairs(primitive.attributes) do
let accessor = t.accessors[v+1]
primitive.attributes[n] = accessor
table.insert(vertexformat, { attributeName[n] or n, accessor.componentType, accessor.components })
for i, f in ipairs(accessor.data) do
let vertex = vertices[i]
if not vertex then
table.insert(vertices, i, {})
vertex = vertices[i]
end
for _, c in ipairs(f) do
table.insert(vertex, c)
end
end
end
if primitive.mode then
primitive.mode = mode[primitive.mode]
else
primitive.mode = "triangles"
end
primitive.mesh = lg.newMesh(vertexformat, vertices, primitive.mode)
if primitive.indices then
primitive.indices = [ for _, i in ipairs(t.accessors[primitive.indices+1].data) do i+1 end ]
primitive.mesh:setVertexMap(primitive.indices)
end
primitive.material = t.materials[(primitive.material or -1)+1]
-- TODO targets
end
end
-- TODO skins
-- TODO textures, images, samplers
-- cameras
for _, camera in ipairs(t.cameras) do
if camera.name then t.cameras[camera.name] = camera end
if camera.type == "perspective" then
camera.perspective.aspectRatio or= 16/9
camera.matrix = m4.perspective(camera.perspective.yfov, camera.perspective.aspectRatio, camera.perspective.znear, camera.perspective.zfar)
elseif camera.type == "orthographic" then
camera.matrix = m4.orthographic(camera.orthographic.xmag, camera.orthographic.ymag, camera.orthographic.znear, camera.orthographic.zfar)
end
end
-- TODO animations
-- TODO extensions
-- TODO glb
return setmetatable({
data = t
}, gltf_mt)
end
return gltf

146
math/bb3.lua Normal file
View file

@ -0,0 +1,146 @@
--- Axis aligned 3D bounding box
local bb3
local v3 = require((...):match("^(.*)bb3$").."v3")
-- bb3 methods
-- Unless specified otherwise, all operations are done in place and do not create a bounding box.
local bb3_mt = {
-- Clone the bounding box. Returns a new bounding box.
clone = function(self)
return bb3(self.min:clone(), self.max:clone())
end,
-- Set bounding box
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, bounding sphere or point
collide = function(self, other)
if other.min then -- bb3
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]
elseif other.radius then -- bs3
return other:collide(self)
else -- v3
return self.min[1] <= other[1] and self.max[1] >= other[1] and
self.min[2] <= other[2] and self.max[2] >= other[2] and
self.min[3] <= other[3] and self.max[3] >= other[3]
end
end,
-- Extend the bounding box by v:
-- * number: extend by a certain distance from the center
-- * v3: extend each axis by the associated vector coordinate
extend = function(self, v)
if type(v) == "number" then
self.min = self.min - { v, v, v }
self.max = self.max + { v, v, v }
else
self.min = self.min - v
self.max = self.max + v
end
return self
end,
-- Modify the bounding box so that it include v (v3 or bb3)
include = function(self, v)
if v.min then -- bounding box
self.min[1] = math.min(self.min[1], v.min[1])
self.min[2] = math.min(self.min[2], v.min[2])
self.min[3] = math.min(self.min[3], v.min[3])
self.max[1] = math.max(self.max[1], v.max[1])
self.max[2] = math.max(self.max[2], v.max[2])
self.max[3] = math.max(self.max[3], v.max[3])
else -- vector
self.min[1] = math.min(self.min[1], v[1])
self.min[2] = math.min(self.min[2], v[2])
self.min[3] = math.min(self.min[3], v[3])
self.max[1] = math.max(self.max[1], v[1])
self.max[2] = math.max(self.max[2], v[2])
self.max[3] = math.max(self.max[3], v[3])
end
return self
end,
-- Common operations with vectors. Returns a new bounding box.
__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,
__tostring = function(self)
return ("bb3(%s,%s)"):format(self.min, self.max)
end
}
bb3_mt.__index = bb3_mt
bb3 = setmetatable({
-- Calculate the mesh's bounding box. Returns a new bounding box.
fromMesh = function(mesh, tr)
local bb
-- Get VertexPosition attribute index in vertex data
local iposition
for i, t in ipairs(mesh: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
break
end
end
if not iposition then error("mesh doesn't have VertexPosition attributes") end
-- Retrieve vertices
local vmap = mesh:getVertexMap()
if vmap then
local min, max = mesh:getDrawRange()
if not min then
min, max = 1, #vmap
end
local v = v3(mesh:getVertexAttribute(vmap[min], iposition))
if tr then v:transform(tr) end
bb = bb3(v:clone(),v:clone())
for i=min+1, max do
v = v3(mesh:getVertexAttribute(vmap[i], iposition))
if tr then v:transform(tr) end
bb:include(v)
end
else
local min, max = mesh:getDrawRange()
if not min then
min, max = 1, mesh:getVertexCount()
end
local v = v3(mesh:getVertexAttribute(min, iposition))
if tr then v:transform(tr) end
bb = bb3(v:clone(),v:clone())
for i=min+1, max do
v = v3(mesh:getVertexAttribute(i, iposition))
if tr then v:transform(tr) end
bb:include(v)
end
end
return bb
end
}, {
-- min and max will not be copied.
__call = function(self, min, max)
return setmetatable({ min = v3(min), max = v3(max) }, bb3_mt)
end
})
return bb3

145
math/bs3.lua Normal file
View file

@ -0,0 +1,145 @@
--- 3D bounding sphere
local bs3
local v3 = require((...):match("^(.*)bs3$").."v3")
-- bs3 methods
-- Unless specified otherwise, all operations are done in place and do not create a bounding sphere.
local bs3_mt = {
-- Clone the bounding sphere. Returns a new bounding sphere.
clone = function(self)
return bs3(self.center:clone(), self.radius)
end,
-- Set bounding sphere
set = function(self, center, radius)
self.center:set(center)
self.radius = radius
return self
end,
-- Retrieve center vectors and radius
unpack = function(self)
return self.center, self.radius
end,
-- Test if the bounding sphere collide with another bounding sphere, bounding box or point
collide = function(self, other)
if other.radius then -- bs3
return (self.center[1] - other.center[1]) * (self.center[1] - other.center[1]) +
(self.center[2] - other.center[2]) * (self.center[2] - other.center[2]) +
(self.center[3] - other.center[3]) * (self.center[3] - other.center[3])
<= (self.radius + other.radius) * (self.radius + other.radius)
elseif other.min then -- bb3
local x = math.max(other.min[1], math.min(self.center[1], other.max[1]))
local y = math.max(other.min[2], math.min(self.center[2], other.max[2]))
local z = math.max(other.min[3], math.min(self.center[3], other.max[3]))
return (self.center[1] - x) * (self.center[1] - x) +
(self.center[2] - y) * (self.center[2] - y) +
(self.center[3] - z) * (self.center[3] - z)
<= self.radius * self.radius
else -- v3
return (self.center[1] - other[1]) * (self.center[1] - other[1]) +
(self.center[2] - other[2]) * (self.center[2] - other[2]) +
(self.center[3] - other[3]) * (self.center[3] - other[3])
<= self.radius * self.radius
end
end,
-- Extend the bounding sphere by v (number)
extend = function(self, v)
self.radius = self.radius + v
return self
end,
-- Modify the bounding sphere so that it include v (v3 or bs3)
include = function(self, v)
if v.min then -- bounding sphere
self.radius = (self.center:distance(v.center) + self.radius + v.radius) / 2
self.center = v.center + (self.center - v.center):normalize() * (self.radius - v.radius)
else -- vector
self.radius = (self.center:distance(v) + self.radius) / 2
self.center = v + (self.center - v):normalize() * self.radius
end
return self
end,
-- Common operations with vectors. Returns a new bounding box.
__sub = function(self, other)
return bs3(self.center - other, self.radius)
end,
__add = function(self, other)
return bs3(self.center + other, self.radius)
end,
__unm = function(self)
return bs3(-self.center, self.radius)
end,
__div = function(self, other)
return bs3(self.center, self.radius / other)
end,
__mul = function(self, other)
return bs3(self.center, self.radius * other)
end,
__tostring = function(self)
return ("bs3(%s,%s)"):format(self.center, self.radius)
end
}
bs3_mt.__index = bs3_mt
bs3 = setmetatable({
-- Calculate the mesh's bounding sphere. Returns a new bounding sphere.
fromMesh = function(mesh, tr)
local bs
-- Get VertexPosition attribute index in vertex data
local iposition
for i, t in ipairs(mesh: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
break
end
end
if not iposition then error("mesh doesn't have VertexPosition attributes") end
-- Retrieve vertices
local vmap = mesh:getVertexMap()
if vmap then
local min, max = mesh:getDrawRange()
if not min then
min, max = 1, #vmap
end
local v = v3(mesh:getVertexAttribute(vmap[min], iposition))
if tr then v:transform(tr) end
bs = bs3(v:clone(),0)
for i=min+1, max do
v = v3(mesh:getVertexAttribute(vmap[i], iposition))
if tr then v:transform(tr) end
bs:include(v)
end
else
local min, max = mesh:getDrawRange()
if not min then
min, max = 1, mesh:getVertexCount()
end
local v = v3(mesh:getVertexAttribute(min, iposition))
if tr then v:transform(tr) end
bs = bs3(v:clone(),0)
for i=min+1, max do
v = v3(mesh:getVertexAttribute(i, iposition))
if tr then v:transform(tr) end
bs:include(v)
end
end
return bs
end
}, {
-- center will not be copied.
__call = function(self, center, radius)
return setmetatable({ center = v3(center), radius = radius }, bs3_mt)
end
})
return bs3

246
math/m4.lua Normal file
View file

@ -0,0 +1,246 @@
--- 4x4 matrix.
-- Reminder: they are represented in row-major order, **unlike** GLM which is column-major.
local m4
local cos, sin, tan = math.cos, math.sin, math.tan
local v3 = require((...):match("^(.*)m4$").."v3")
local tmpv3 = v3(0,0,0) -- temporary vector used in our computations
-- m4 methods
-- Unless specified otherwise, all operations are done in place and do not create a new matrix.
local m4_mt = {
-- Clone the matrix. Returns a new matrix.
clone = 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,
-- Set matrix
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,
-- Retrieve values
unpack = function(self)
return 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,
-- 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. Returns a new matrix.
__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,
__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,
__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,
__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 to create new matrices from
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):normalize():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).
perspective = function(fovy, aspect, zNear, zFar) -- If zFar is not specified, will return an infinite perspective matrix.
local f = 1 / tan(fovy / 2)
if zFar then
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
}
else
return m4{
f / aspect, 0, 0, 0,
0, f, 0, 0,
0, 0, -1, -2 * zNear,
0, 0, -1, 0
}
end
end,
orthographic = function(xmag, ymag, znear, zfar)
return m4{
1 / xmag, 0, 0, 0,
0, 1 / ymag, 0, 0,
0, 0, 2 / (znear - zfar), (zfar + znear) / (znear - zfar),
0, 0, 0, 1
}
end,
lookAt = function(eye, center, up)
center, up = v3(center), v3(up)
local f = (center - eye):normalize()
local s = f:cross(up):normalize()
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,
-- Create a new matrix from column major list
fromColumnMajor = function(m)
return m4{
m[1], m[5], m[9], m[13],
m[2], m[6], m[10], m[14],
m[3], m[7], m[11], m[15],
m[4], m[8], m[12], m[16]
}
end
}, {
-- Will not copy the table.
__call = function(self, t)
return setmetatable(t, m4_mt)
end
})
return m4

90
math/qt.lua Normal file
View file

@ -0,0 +1,90 @@
--- Quaternion
local qt
local sin, cos = math.sin, math.cos
local m4 = require((...):match("^(.*)qt$").."m4")
-- qt methods
-- Unless specified otherwise, all operations are done in place and do not create a new quaternion.
local qt_mt = {
-- Clone the quaternion. Returns a new quaternion.
clone = function(self)
return qt(self[1], self[2], self[3], self[4])
end,
-- Set quaternion
set = function(self, other)
self[1], self[2], self[3], self[4] = other[1], other[2], other[3], other[4]
return self
end,
-- Retrieve coordinates
unpack = function(self)
return self[1], self[2], self[3], self[4]
end,
-- Convert to m4 rotation matrix. Returns a new 4x4 matrix.
toM4 = function(self)
local x, y, z, w = self[1], self[2], self[3], self[4]
local xx, yy, zz = x*x, y*y, z*z
local xw, xy, xz = x*w, x*y, x*z
local yw, yz = y*w, y*z
local zw = z*w
return m4{
1-2*(yy+zz), 2*(xy-zw), 2*(xz+yw), 0,
2*(xy+zw), 1-2*(xx+zz), 2*(yz-xw), 0,
2*(xz-yw), 2*(yz+xw), 1-2*(xx+yy), 0,
0, 0, 0, 1
}
end,
-- Common operations. Returns a new quaternion.
__sub = function(self, other)
return qt(self[1] - other[1], self[2] - other[2], self[3] - other[3], self[4] - other[4])
end,
__add = function(self, other)
return qt(self[1] + other[1], self[2] + other[2], self[3] + other[3], self[4] - other[4])
end,
__unm = function(self)
return qt(-self[1], -self[2], -self[3], -self[4])
end,
__mul = function(self, other)
local sx, sy, sz, sw = self[1], self[2], self[3], self[4]
local ox, oy, oz, ow = other[1], other[2], other[3], other[4]
return qt(sw * ow - sx * ox - sy * oy - sz * oz, sx * ow + sw * ox + sy * oz - sz * oy, sy * ow + sw * oy + sz * ox - sx * oz, sz * ow + sw * oz + sx * oy - sy * ox)
end,
__eq = function(self, other)
return self[1] == other[1] and self[2] == other[2] and self[3] == other[3] and self[4] == other[4]
end,
__tostring = function(self)
return ("qt(%s,%s,%s,%s)"):format(self[1], self[2], self[3], self[4])
end
}
qt_mt.__index = qt_mt
qt = setmetatable({
--- Create a new identity quaternion
identity = function()
return qt(0, 0, 0, 1)
end,
--- Create a new quaternion from a rotation
fromAngleAxis = function(angle, axis)
local halfAngle = angle / 2
local s = sin(halfAngle)
return qt(axis[1] * s, axis[2] * s, axis[3] * s, cos(halfAngle))
end,
}, {
-- If x is a table, will reuse it without copying.
__call = function(self, x, y, z, w)
if type(x) == "number" then
return setmetatable({ x, y, z, w }, qt_mt)
else
return setmetatable(x, qt_mt)
end
end
})
return qt

114
math/v3.lua Normal file
View file

@ -0,0 +1,114 @@
--- 3D vector
local v3
local sqrt = math.sqrt
-- v3 methods
-- Unless specified otherwise, all operations are done in place and do not create a new vector.
local v3_mt = {
-- Clone the vector. Returns a new vector.
clone = function(self)
return v3(self[1], self[2], self[3])
end,
-- Set vector
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)
self[1], self[2], self[3] = x/l, y/l, z/l
return self
end,
-- Vector product. Returns a new vector.
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[1], self[2], self[3]
-- 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
self[1], self[2], self[3] = mx/mw, my/mw, mz/mw
return self
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. Returns a new vector.
__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[1], self[2], self[3])
end
}
v3_mt.__index = v3_mt
-- If x is a table, will reuse it without copying.
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
return v3

21
shader/pick.lua Normal file
View file

@ -0,0 +1,21 @@
return [[
# ifdef VERTEX
uniform mat4 scene_transform; // world -> view transform
uniform mat4 model_transform; // model -> world transform
vec4 position(mat4 love_transform, vec4 vertex_position) {
return scene_transform * model_transform * vertex_position;
}
# endif
# ifdef PIXEL
uniform vec3 pick_color;
vec4 effect(vec4 color, sampler2D texture, vec2 texture_coords, vec2 screen_coords) {
if (texture2D(texture, texture_coords).a < 0.1) discard;
return vec4(pick_color, 1);
}
# endif
]]

74
shader/render.lua Normal file
View file

@ -0,0 +1,74 @@
return [[
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, or ambient color for ambient light (alpha is intensity)
};
uniform Light[LIGHT_COUNT] lights;
# endif
uniform vec4 baseColorFactor;
vec4 effect(vec4 color, sampler2D texture, vec2 texture_coords, vec2 screen_coords) {
// base color
vec4 material_color = texture2D(texture, texture_coords) * baseColorFactor * color;
if (material_color.a < 0.1) discard;
# if LIGHT_COUNT > 0
// shading
vec4 final_color = vec4(0.0, 0.0, 0.0, material_color.a);
for (int i = 0; i < LIGHT_COUNT; i++) {
Light light = lights[i];
// ambient
if (light.type == 1) {
final_color.rgb += material_color.rgb * light.color.rgb * light.color.a;
// directional
} else if (light.type == 2) {
// diffuse lighting
vec3 n = normalize(normal_world);
vec3 l = normalize(-light.position);
final_color.rgb += material_color.rgb * light.color.rgb * light.color.a * max(dot(n, l), 0.0);
// 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.rgb += material_color.rgb * light.color.rgb * light.color.a * max(dot(n, l), 0.0) / (distance * distance);
}
// no specular because i'm lazy (TODO)
}
// done
return final_color;
# else
return material_color;
# endif
}
# endif
]]

118
test/AnimatedTriangle.gltf Normal file
View file

@ -0,0 +1,118 @@
{
"scene" : 0,
"scenes" : [
{
"nodes" : [ 0 ]
}
],
"nodes" : [
{
"mesh" : 0,
"rotation" : [ 0.0, 0.0, 0.0, 1.0 ]
}
],
"meshes" : [
{
"primitives" : [ {
"attributes" : {
"POSITION" : 1
},
"indices" : 0
} ]
}
],
"animations": [
{
"samplers" : [
{
"input" : 2,
"interpolation" : "LINEAR",
"output" : 3
}
],
"channels" : [ {
"sampler" : 0,
"target" : {
"node" : 0,
"path" : "rotation"
}
} ]
}
],
"buffers" : [
{
"uri" : "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=",
"byteLength" : 44
},
{
"uri" : "data:application/octet-stream;base64,AAAAAAAAgD4AAAA/AABAPwAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAD0/TQ/9P00PwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAPT9ND/0/TS/AAAAAAAAAAAAAAAAAACAPw==",
"byteLength" : 100
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteOffset" : 0,
"byteLength" : 6,
"target" : 34963
},
{
"buffer" : 0,
"byteOffset" : 8,
"byteLength" : 36,
"target" : 34962
},
{
"buffer" : 1,
"byteOffset" : 0,
"byteLength" : 100
}
],
"accessors" : [
{
"bufferView" : 0,
"byteOffset" : 0,
"componentType" : 5123,
"count" : 3,
"type" : "SCALAR",
"max" : [ 2 ],
"min" : [ 0 ]
},
{
"bufferView" : 1,
"byteOffset" : 0,
"componentType" : 5126,
"count" : 3,
"type" : "VEC3",
"max" : [ 1.0, 1.0, 0.0 ],
"min" : [ 0.0, 0.0, 0.0 ]
},
{
"bufferView" : 2,
"byteOffset" : 0,
"componentType" : 5126,
"count" : 5,
"type" : "SCALAR",
"max" : [ 1.0 ],
"min" : [ 0.0 ]
},
{
"bufferView" : 2,
"byteOffset" : 20,
"componentType" : 5126,
"count" : 5,
"type" : "VEC4",
"max" : [ 0.0, 0.0, 1.0, 1.0 ],
"min" : [ 0.0, 0.0, 0.0, -0.707 ]
}
],
"asset" : {
"version" : "2.0"
}
}

142
test/Box.gltf Normal file
View file

@ -0,0 +1,142 @@
{
"asset": {
"generator": "COLLADA2GLTF",
"version": "2.0"
},
"scene": 0,
"scenes": [
{
"nodes": [
0
]
}
],
"nodes": [
{
"children": [
1
],
"matrix": [
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
-1.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
1.0
]
},
{
"mesh": 0
}
],
"meshes": [
{
"primitives": [
{
"attributes": {
"NORMAL": 1,
"POSITION": 2
},
"indices": 0,
"mode": 4,
"material": 0
}
],
"name": "Mesh"
}
],
"accessors": [
{
"bufferView": 0,
"byteOffset": 0,
"componentType": 5123,
"count": 36,
"max": [
23
],
"min": [
0
],
"type": "SCALAR"
},
{
"bufferView": 1,
"byteOffset": 0,
"componentType": 5126,
"count": 24,
"max": [
1.0,
1.0,
1.0
],
"min": [
-1.0,
-1.0,
-1.0
],
"type": "VEC3"
},
{
"bufferView": 1,
"byteOffset": 288,
"componentType": 5126,
"count": 24,
"max": [
0.5,
0.5,
0.5
],
"min": [
-0.5,
-0.5,
-0.5
],
"type": "VEC3"
}
],
"materials": [
{
"pbrMetallicRoughness": {
"baseColorFactor": [
0.800000011920929,
0.0,
0.0,
1.0
],
"metallicFactor": 0.0
},
"name": "Red"
}
],
"bufferViews": [
{
"buffer": 0,
"byteOffset": 576,
"byteLength": 72,
"target": 34963
},
{
"buffer": 0,
"byteOffset": 0,
"byteLength": 576,
"byteStride": 12,
"target": 34962
}
],
"buffers": [
{
"byteLength": 648,
"uri": "data:application/octet-stream;base64,AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAvwAAAL8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAvwAAAL8AAAA/AAAAPwAAAL8AAAC/AAAAvwAAAL8AAAC/AAAAPwAAAD8AAAA/AAAAPwAAAL8AAAA/AAAAPwAAAD8AAAC/AAAAPwAAAL8AAAC/AAAAvwAAAD8AAAA/AAAAPwAAAD8AAAA/AAAAvwAAAD8AAAC/AAAAPwAAAD8AAAC/AAAAvwAAAL8AAAA/AAAAvwAAAD8AAAA/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAvwAAAL8AAAC/AAAAvwAAAD8AAAC/AAAAPwAAAL8AAAC/AAAAPwAAAD8AAAC/AAABAAIAAwACAAEABAAFAAYABwAGAAUACAAJAAoACwAKAAkADAANAA4ADwAOAA0AEAARABIAEwASABEAFAAVABYAFwAWABUA"
}
]
}

219
test/Duck.gltf Normal file

File diff suppressed because one or more lines are too long

85
test/SimpleMeshes.gltf Normal file
View file

@ -0,0 +1,85 @@
{
"scene" : 0,
"scenes" : [
{
"nodes" : [ 0, 1]
}
],
"nodes" : [
{
"mesh" : 0
},
{
"mesh" : 0,
"translation" : [ 1.0, 0.0, 0.0 ]
}
],
"meshes" : [
{
"primitives" : [ {
"attributes" : {
"POSITION" : 1,
"NORMAL" : 2
},
"indices" : 0
} ]
}
],
"buffers" : [
{
"uri" : "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8=",
"byteLength" : 80
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteOffset" : 0,
"byteLength" : 6,
"target" : 34963
},
{
"buffer" : 0,
"byteOffset" : 8,
"byteLength" : 72,
"byteStride" : 12,
"target" : 34962
}
],
"accessors" : [
{
"bufferView" : 0,
"byteOffset" : 0,
"componentType" : 5123,
"count" : 3,
"type" : "SCALAR",
"max" : [ 2 ],
"min" : [ 0 ]
},
{
"bufferView" : 1,
"byteOffset" : 0,
"componentType" : 5126,
"count" : 3,
"type" : "VEC3",
"max" : [ 1.0, 1.0, 0.0 ],
"min" : [ 0.0, 0.0, 0.0 ]
},
{
"bufferView" : 1,
"byteOffset" : 36,
"componentType" : 5126,
"count" : 3,
"type" : "VEC3",
"max" : [ 0.0, 0.0, 1.0 ],
"min" : [ 0.0, 0.0, 1.0 ]
}
],
"asset" : {
"version" : "2.0"
}
}

70
test/Triangle.gltf Normal file
View file

@ -0,0 +1,70 @@
{
"scene" : 0,
"scenes" : [
{
"nodes" : [ 0 ]
}
],
"nodes" : [
{
"mesh" : 0
}
],
"meshes" : [
{
"primitives" : [ {
"attributes" : {
"POSITION" : 1
},
"indices" : 0
} ]
}
],
"buffers" : [
{
"uri" : "data:application/octet-stream;base64,AAABAAIAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAACAPwAAAAA=",
"byteLength" : 44
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteOffset" : 0,
"byteLength" : 6,
"target" : 34963
},
{
"buffer" : 0,
"byteOffset" : 8,
"byteLength" : 36,
"target" : 34962
}
],
"accessors" : [
{
"bufferView" : 0,
"byteOffset" : 0,
"componentType" : 5123,
"count" : 3,
"type" : "SCALAR",
"max" : [ 2 ],
"min" : [ 0 ]
},
{
"bufferView" : 1,
"byteOffset" : 0,
"componentType" : 5126,
"count" : 3,
"type" : "VEC3",
"max" : [ 1.0, 1.0, 0.0 ],
"min" : [ 0.0, 0.0, 0.0 ]
}
],
"asset" : {
"version" : "2.0"
}
}

View file

@ -0,0 +1,54 @@
{
"scene" : 0,
"scenes" : [
{
"nodes" : [ 0 ]
}
],
"nodes" : [
{
"mesh" : 0
}
],
"meshes" : [
{
"primitives" : [ {
"attributes" : {
"POSITION" : 0
}
} ]
}
],
"buffers" : [
{
"uri" : "data:application/octet-stream;base64,AAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAgD8AAAAA",
"byteLength" : 36
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteOffset" : 0,
"byteLength" : 36,
"target" : 34962
}
],
"accessors" : [
{
"bufferView" : 0,
"byteOffset" : 0,
"componentType" : 5126,
"count" : 3,
"type" : "VEC3",
"max" : [ 1.0, 1.0, 0.0 ],
"min" : [ 0.0, 0.0, 0.0 ]
}
],
"asset" : {
"version" : "2.0"
}
}

BIN
test/hyke.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View file

@ -1,3 +1,4 @@
require("candran").setup()
local wirefame = require("wirefame") local wirefame = require("wirefame")
love.graphics.setLineStyle("rough") love.graphics.setLineStyle("rough")
@ -6,23 +7,22 @@ love.window.setMode(1000, 1000, { vsync = false })
wirefame.defaultColor = { 1, 0, 0, 1 } wirefame.defaultColor = { 1, 0, 0, 1 }
local camera = { 0, 1, 3 } local model = wirefame.model("test/Duck.gltf")
local frame = wirefame.model("frame.obj")
:setColor(1, 1, 1) :setColor(1, 1, 1)
local model = wirefame.model("metaknight.obj") local img = wirefame.model(love.graphics.newImage("hyke.png")):scale{1/100, 1/100, 1/100}
:scale(1/15, 1/15, 1/15)
:translate(0, -0.7, 0) -- local model = wirefame.model("test/metaknight.obj")
-- :scale{1/15, 1/15, 1/15}
-- :translate{0, -0.7, 0}
local scene = wirefame.scene() local scene = wirefame.scene()
:setViewport(0, 0, 1000, 1000) :setPerspective(math.rad(60), 1, .1, 100)
:lookAt(camera, {0,0,0}, {0,1,0}) :lookAt({0,0,3}, {0,0,0}, {0,1,0})
:setViewDistance(5) :add(model, img)
:add(frame, model)
function love.update(dt) function love.update(dt)
model:rotate(dt/2, {0, 1, 0}) scene:rotate(dt/2, {0, 1, 0})
end end
function love.draw() function love.draw()

222
test/untitled.gltf Normal file
View file

@ -0,0 +1,222 @@
{
"asset" : {
"generator" : "Khronos glTF Blender I/O v1.0.5",
"version" : "2.0"
},
"extensionsUsed" : [
"KHR_lights_punctual"
],
"extensionsRequired" : [
"KHR_lights_punctual"
],
"extensions" : {
"KHR_lights_punctual" : {
"lights" : [
{
"color" : [
1,
1,
1
],
"intensity" : 1000,
"type" : "point",
"name" : "Light"
}
]
}
},
"scene" : 0,
"scenes" : [
{
"name" : "Scene",
"nodes" : [
0,
2,
4,
5
]
}
],
"nodes" : [
{
"mesh" : 0,
"name" : "Cube"
},
{
"extensions" : {
"KHR_lights_punctual" : {
"light" : 0
}
},
"name" : "Light_Orientation",
"rotation" : [
-0.7071067690849304,
0,
0,
0.7071067690849304
]
},
{
"children" : [
1
],
"name" : "Light",
"rotation" : [
0.16907575726509094,
0.7558803558349609,
-0.27217137813568115,
0.570947527885437
],
"translation" : [
4.076245307922363,
5.903861999511719,
-1.0054539442062378
]
},
{
"camera" : 0,
"name" : "Camera_Orientation",
"rotation" : [
-0.7071067690849304,
0,
0,
0.7071067690849304
]
},
{
"children" : [
3
],
"name" : "Camera",
"rotation" : [
0.483536034822464,
0.33687159419059753,
-0.20870360732078552,
0.7804827094078064
],
"translation" : [
7.358891487121582,
4.958309173583984,
6.925790786743164
]
},
{
"extras" : {
"prop" : 1.7699999809265137
},
"name" : "Empty",
"translation" : [
0,
0,
-4.324187755584717
]
}
],
"cameras" : [
{
"name" : "Camera",
"perspective" : {
"yfov" : 0.39959652046304894,
"zfar" : 100,
"znear" : 0.10000000149011612
},
"type" : "perspective"
}
],
"materials" : [
{
"doubleSided" : true,
"name" : "Material",
"pbrMetallicRoughness" : {
"baseColorFactor" : [
0.800000011920929,
0.800000011920929,
0.800000011920929,
1
],
"metallicFactor" : 0,
"roughnessFactor" : 0.4000000059604645
}
}
],
"meshes" : [
{
"name" : "Cube",
"primitives" : [
{
"attributes" : {
"POSITION" : 0,
"NORMAL" : 1,
"TEXCOORD_0" : 2
},
"indices" : 3,
"material" : 0
}
]
}
],
"accessors" : [
{
"bufferView" : 0,
"componentType" : 5126,
"count" : 24,
"max" : [
1,
1,
1
],
"min" : [
-1,
-1,
-1
],
"type" : "VEC3"
},
{
"bufferView" : 1,
"componentType" : 5126,
"count" : 24,
"type" : "VEC3"
},
{
"bufferView" : 2,
"componentType" : 5126,
"count" : 24,
"type" : "VEC2"
},
{
"bufferView" : 3,
"componentType" : 5123,
"count" : 36,
"type" : "SCALAR"
}
],
"bufferViews" : [
{
"buffer" : 0,
"byteLength" : 288,
"byteOffset" : 0
},
{
"buffer" : 0,
"byteLength" : 288,
"byteOffset" : 288
},
{
"buffer" : 0,
"byteLength" : 192,
"byteOffset" : 576
},
{
"buffer" : 0,
"byteLength" : 72,
"byteOffset" : 768
}
],
"buffers" : [
{
"byteLength" : 840,
"uri" : "data:application/octet-stream;base64,AACAPwAAgD8AAIA/AACAPwAAgD8AAIC/AACAvwAAgD8AAIC/AACAvwAAgD8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIA/AACAPwAAgL8AAIA/AACAPwAAgD8AAIA/AACAvwAAgD8AAIC/AACAvwAAgL8AAIC/AACAvwAAgL8AAIA/AACAvwAAgD8AAIA/AACAvwAAgL8AAIA/AACAvwAAgL8AAIC/AACAPwAAgL8AAIC/AACAPwAAgL8AAIA/AACAPwAAgD8AAIA/AACAPwAAgL8AAIA/AACAPwAAgL8AAIC/AACAPwAAgD8AAIC/AACAPwAAgD8AAIC/AACAPwAAgL8AAIC/AACAvwAAgL8AAIC/AACAvwAAgD8AAIC/AAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgL8AAACAAAAAAAAAgL8AAACAAAAAAAAAgL8AAACAAAAAAAAAgL8AAACAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAgPwAAgD4AACA/AAAAPwAAYD8AAAA/AABgPwAAgD4AACA/AAAAAAAAwD4AAAAAAADAPgAAgD4AACA/AACAPgAAID8AAEA/AADAPgAAQD8AAMA+AACAPwAAID8AAIA/AAAAPgAAgD4AAAA+AAAAPwAAwD4AAAA/AADAPgAAgD4AACA/AACAPgAAwD4AAIA+AADAPgAAAD8AACA/AAAAPwAAID8AAAA/AADAPgAAAD8AAMA+AABAPwAAID8AAEA/AAABAAIAAAACAAMABAAFAAYABAAGAAcACAAJAAoACAAKAAsADAANAA4ADAAOAA8AEAARABIAEAASABMAFAAVABYAFAAWABcA"
}
]
}

File diff suppressed because it is too large Load diff

170
wirefame/group.lua Normal file
View file

@ -0,0 +1,170 @@
local wirefame = require((...):match("^(.*)%.wirefame%.group$"))
local model_mt = require((...):match("^(.*)group$").."model")
local m4, v3, bb3, bs3 = wirefame.m4, wirefame.v3, wirefame.bb3, wirefame.bs3
local unpack = table.unpack or unpack
--- Group methods.
local group_mt = {
--- Add a model to the node.
add = function(self, ...)
assert(not self.filtered, "can't add to a filtered group")
for _, m in ipairs({...}) do
self.n = self.n + 1
table.insert(self, m)
if m.parent == nil then
m.parent = self
if self.scene then
m:onAddToScene(self.scene)
end
m:setTransform(0, self:updateFinalTransform())
end
end
return self
end,
--- Remove a model from the node.
remove = function(self, ...)
assert(not self.filtered, "can't remove from a filtered group")
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)
m.parent = nil
if m.scene 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,
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
l.n = #l
l.filtered = true
return setmetatable(l, {__index = self, __tostring = self.__tostring})
end,
boundingBox = function(self)
if #self > 0 then
self:updateFinalTransform()
local r = self[1]:boundingBox()
for i=2, self.n do
r:include(self[i]:boundingBox())
end
return r
else
return bb3(v3(0,0,0), v3(0,0,0))
end
end,
boundingSphere = function(self)
if #self > 0 then
self:updateFinalTransform()
local r = self[1]:boundingSphere()
for i=2, self.n do
r:include(self[i]:boundingSphere())
end
return r
else
return bs3(v3(0,0,0), 0)
end
end,
initShader = function(self, shader)
for i=1, self.n do
self[i]:initShader(shader)
end
end,
sendFinalTransform = function(self, tr)
if self.filtered then self = getmetatable(self).__index end
for i=1, self.n do
self[i]:setTransform(0, tr)
end
end,
setColor = function(self, r, g, b, a) -- apply color to all children
self.color[1], self.color[2], self.color[3], self.color[4] = r, g, b, a or 1
for i=1, self.n do
self[i]:setColor(r, g, b, a)
end
return self
end,
draw = function(self, shader, transp)
-- Transparency sort
if transp then
if self.color[4] >= 1 then return end
else
if self.color[4] < 1 then return end
end
-- Transformation matrix
self:updateFinalTransform()
-- Draw
for i=1, self.n do
self[i]:draw(shader, transp)
end
end,
__tostring = function(self)
local mt = getmetatable(self)
setmetatable(self, nil)
local str = "group: "..(tostring(self):gsub("^table: ", ""))
setmetatable(self, mt)
return str
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.
function wirefame.group(t)
local group = {
-- Size
n = 0,
-- Tags
tags = {},
-- Group transformation matrices
transformStack = { n = 1, changed = true, names = {}, [0] = m4.identity(), m4.identity() },
finalTransform = nil,
-- Group color
color = { wirefame.defaultColor[1], wirefame.defaultColor[2], wirefame.defaultColor[3], wirefame.defaultColor[4] },
-- Hierarchy
parent = nil,
scene = nil
}
-- Create & return object
return setmetatable(group, group_mt):add(unpack(t))
end
return group_mt

117
wirefame/light.lua Normal file
View file

@ -0,0 +1,117 @@
local wirefame = require((...):match("^(.*)%.wirefame%.light$"))
local model_mt = require((...):match("^(.*)light$").."model")
local m4, v3 = wirefame.m4, wirefame.v3
--- Light methods.
local lightType = {
none = 0,
ambient = 1,
directional = 2,
point = 3
}
local light_mt = {
-- Rewrite model methods
initShader = function(self, shader)
shader:send("lights["..(self.index-1).."].type", self.type)
shader:send("lights["..(self.index-1).."].position", self.position)
shader: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
self:initShader() -- update already existing array in shader
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
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.shader:send("lights["..(self.index-1).."].type", lightType.none)
end,
sendFinalTransform = function(self, tr)
self.position = v3(0,0,0):transform(tr)
end,
draw = function(self, shader, transp)
-- Draw
if shader:hasUniform("lights["..(self.index-1).."].position") then
self:updateFinalTransform()
shader:send("lights["..(self.index-1).."].position", self.position)
shader:send("lights["..(self.index-1).."].color", self.color)
end
end,
__tostring = function(self)
local mt = getmetatable(self)
setmetatable(self, nil)
local str = "light: "..(tostring(self):gsub("^table: ", ""))
setmetatable(self, mt)
return str
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
function wirefame.light(type)
local light = {
-- Light
type = lightType[type],
position = v3(0, 0, 0),
-- Color
color = { wirefame.defaultColor[1], wirefame.defaultColor[2], wirefame.defaultColor[3], wirefame.defaultColor[4] },
-- Size
n = 1,
true,
-- Tags
tags = {},
-- Group transformation matrices
transformStack = { n = 1, changed = true, names = {}, [0] = m4.identity(), m4.identity() },
finalTransform = nil,
-- Empty model object
object = {},
-- Hierarchy
parent = nil,
scene = nil
}
-- Create & return object
return setmetatable(light, light_mt)
end
return light_mt

375
wirefame/model.lua Normal file
View file

@ -0,0 +1,375 @@
local wirefame = require((...):match("^(.*)%.wirefame%.model$"))
local m4, v3, bb3, bs3 = wirefame.m4, wirefame.v3, wirefame.bb3, wirefame.bs3
local unpack = table.unpack or unpack
local lg = love.graphics
-- 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,
--- 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 new group of all models recursively contained in this object with these tags.
get = function(self, ...)
if self:is(...) then
return 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, [0] = self.transformStack[0] }
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
return self
end,
--- Retrieve the transformation names.
getTransformNames = function(self, list)
local names = {}
for name, i in pairs(self.transformStack.names) do
names[i] = name
end
return names
end,
--- Assign a name to the transform i.
setTransformName = function(self, i, name)
self.transformStack.names[name] = i
return self
end,
--- Calculate the final transformation matrix if needed, and send it to eventual children (using :sendFinalTransform).
-- Returns the transform.
updateFinalTransform = 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.finalTransform:transform(self.transformStack[0])
self.transformStack.changed = false
self:sendFinalTransform(self.finalTransform)
end
return self.finalTransform
end,
--- Send the final transformation matrix to the eventual children objects.
sendFinalTransform = function(self, tr)
if self.object.setTransform then
self.object:setTransform(tr)
end
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)
if self.object.boundingBox then
return self.object:boundingBox(self:updateFinalTransform())
else
return bb3(v3(0,0,0), v3(0,0,0))
end
end,
--- Returns the minimum bounding sphere.
boundingSphere = function(self)
if self.object.boundingSphere then
return self.object:boundingSphere(self:updateFinalTransform())
else
return bs3(v3(0,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,
--- Initialize all relevant uniforms in the shader (called after shader rebuild)
initShader = function(self, shader)
end,
-- Draw the model. Shader may be either render or pick.
draw = function(self, shader, transp)
if self.object.draw then
-- Transparency sort
if transp then
if self.color[4] >= 1 then return end
else
if self.color[4] < 1 then return end
end
-- Transformation matrix (will automatically update object that need to know the matrix)
shader:send("model_transform", self:updateFinalTransform())
-- Draw
lg.setColor(self.color)
self.object:draw(shader)
end
end,
__tostring = function(self)
local mt = getmetatable(self)
setmetatable(self, nil)
local str = "model: "..(tostring(self):gsub("^table: ", ""))
setmetatable(self, mt)
return str
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(object)
local model = {
-- Size
n = 1,
true,
-- Tags
tags = {},
-- Model transformation matrices. Matrix 0 is the transform inherited from parent chain.
transformStack = { n = 1, changed = true, names = {}, [0] = m4.identity(), m4.identity() },
finalTransform = nil,
-- Model color
color = { wirefame.defaultColor[1], wirefame.defaultColor[2], wirefame.defaultColor[3], wirefame.defaultColor[4] },
-- Model object
-- object:draw(shader)
-- object:setTransform(tr)
-- object:boundingBox(tr)
-- object:boundingSphere(tr)
object = nil,
-- Hierarchy
parent = nil,
scene = nil
}
-- Empty model
if object == nil then
model.object = {}
-- Load a model file
elseif type(object) == "string" then
local ext = object:match("%.(.-)$") or ""
if wirefame.loader[ext] then
model.object = wirefame.loader[ext](object)
else
error(("unknown model type %s"):format(ext))
end
-- Function
elseif type(object) == "function" then
model.object = { draw = object }
-- Convertible to LÖVE meshes.
elseif type(object) == "userdata" then
if object:type() == "Image" or object:type() == "Canvas" then
local t = lg.newMesh({
{ "VertexPosition", "float", 3 },
{ "VertexTexCoord", "float", 2 },
{ "VertexColor", "byte", 4 },
{ "VertexNormal", "float", 3 }
},
{
{
0, 0, 0,
0, 1,
1, 1, 1, 1,
0, 0, 1
},
{
object:getWidth(), 0, 0,
1, 1,
1, 1, 1, 1,
0, 0, 1
},
{
object:getWidth(), object:getHeight(), 0,
1, 0,
1, 1, 1, 1,
0, 0, 1
},
{
0, object:getHeight(), 0,
0, 0,
1, 1, 1, 1,
0, 0, 1
}
}, "fan")
t:setVertexMap(1, 2, 3, 4)
t:setTexture(object)
model.object = {
draw = function()
lg.draw(t)
end,
boundingBox = function(self, tr)
return bb3.fromMesh(t, tr)
end,
boundingSphere = function(self, tr)
return bs3.fromMesh(t, tr)
end
}
elseif object:type() == "Mesh" then
model.object = {
draw = function()
lg.draw(object)
end,
boundingBox = function(self, tr)
return bb3.fromMesh(object, tr)
end,
boundingSphere = function(self, tr)
return bs3.fromMesh(t, tr)
end
}
else
error("unknown userdata")
end
else
model.object = object
end
-- Create & return object
return setmetatable(model, model_mt)
end
return model_mt

189
wirefame/scene.lua Normal file
View file

@ -0,0 +1,189 @@
local wirefame = require((...):match("^(.*)%.wirefame%.scene$"))
local group_mt = require((...):match("^(.*)scene$").."group")
local m4, v3 = wirefame.m4, wirefame.v3
local lg = love.graphics
-- Global shaders
local pickShader = lg.newShader(wirefame.shader.pick)
--- Scene methods
local scene_mt = {
--- 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,
--- Rebuild the shader
rebuildShader = function(self)
if self.shader.changed then
if self.shader.shader then self.shader.shader:release() end
local s = ""
for var, val in pairs(self.shader.define) do
s = s .. ("# define %s %s"):format(var, val) .. "\n"
end
s = s .. wirefame.shader.render
self.shader.shader = lg.newShader(s)
self.shader.shader:send("scene_transform", m4.identity())
self.shader.shader:send("model_transform", m4.identity())
for _, model in ipairs(self) do model:initShader(self.shader.shader) end
self.shader.changed = false
end
return self.shader.shader
end,
--- Draw the scene.
draw = function(self)
-- Init shader
local shader = self:rebuildShader()
-- Calculate transformation matrix
shader:send("scene_transform", self:updateFinalTransform())
-- Draw models
lg.setShader(shader)
for _, model in ipairs(self) do
model:draw(shader, false) -- draw opaque models
end
for _, model in ipairs(self) do
model:draw(shader, true) -- draw transparent models
end
lg.setShader()
end,
--- Returns the object visible at x, y on the screen.
pick = function(self, x, y)
local canvas = lg.newCanvas()
-- Calculate transformation matrix
pickShader:send("scene_transform", m4.scale{1, -1, 1} * self:updateFinalTransform())
-- Draw models
lg.setCanvas{canvas, depth = true}
lg.setShader(pickShader)
local r, g, b = 0, 0, 0
for _, model in ipairs(self) do
if r < 255 then
r = r+1
elseif g < 255 then
g = g+1
elseif b < 255 then
b = b+1
else
error("too many object to pick from")
end
pickShader:send("pick_color", {r/255, g/255, b/255})
model:draw(pickShader, false)
end
r, g, b = 0, 0, 0
for _, model in ipairs(self) do
if r < 255 then
r = r+1
elseif g < 255 then
g = g+1
elseif b < 255 then
b = b+1
end
pickShader:send("pick_color", {r/255, g/255, b/255})
model:draw(pickShader, true)
end
lg.setShader()
love.graphics.setCanvas()
-- Pick object
r, g, b = canvas:newImageData():getPixel(x, y)
local i = math.floor(r*255) + math.floor(g*255)*255 + math.floor(b*255)*255*255
return self[i]
end,
-- Redefine some group methods (main change is not passing scene transform to children)
add = function(self, ...)
for _, m in ipairs({...}) do
self.n = self.n + 1
table.insert(self, m)
if m.parent == nil then
m.parent = self
if self.scene then
m:onAddToScene(self.scene)
end
end
end
return self
end,
sendFinalTransform = function(self, tr)
-- scene final transform is applied in shader
end,
__tostring = function(self)
local mt = getmetatable(self)
setmetatable(self, nil)
local str = "scene: "..(tostring(self):gsub("^table: ", ""))
setmetatable(self, mt)
return str
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
shader = {
changed = true, -- rebuild shader
define = {}, -- map of variables to define in the shader
shader = 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 = {},
[0] = m4.identity(),
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
parent = nil,
scene = nil
}
scene.scene = scene
-- enable depth buffer (lequal instead of less in order to allow regular 2D drawing (constant depth value))
lg.setDepthMode("lequal", true)
-- enable back face culling
lg.setFrontFaceWinding("ccw")
lg.setMeshCullMode("back")
-- Create & return object
return setmetatable(scene, scene_mt)
end
return scene_mt