2021-06-11 20:47:29 +02:00
|
|
|
-- Localize globals
|
2021-06-17 19:45:08 +02:00
|
|
|
local assert, error, math, modlib, next, pairs, setmetatable, table = assert, error, math, modlib, next, pairs, setmetatable, table
|
2021-06-11 20:47:29 +02:00
|
|
|
|
2021-10-10 18:14:35 +02:00
|
|
|
local read_int, read_single = modlib.binary.read_int, modlib.binary.read_single
|
|
|
|
|
2021-06-17 19:45:08 +02:00
|
|
|
-- Set environment
|
|
|
|
local _ENV = {}
|
|
|
|
setfenv(1, _ENV)
|
|
|
|
|
|
|
|
local metatable = {__index = _ENV}
|
2021-01-31 21:41:42 +01:00
|
|
|
|
|
|
|
--! experimental
|
|
|
|
--+ Reads a single BB3D chunk from a stream
|
|
|
|
--+ Doing `assert(stream:read(1) == nil)` afterwards is recommended
|
|
|
|
--+ See `b3d_specification.txt` as well as https://github.com/blitz-research/blitz3d/blob/master/blitz3d/loader_b3d.cpp
|
|
|
|
--> B3D model
|
|
|
|
function read(stream)
|
2021-03-27 20:10:49 +01:00
|
|
|
local left = 8
|
2021-01-31 21:41:42 +01:00
|
|
|
|
2021-03-27 20:10:49 +01:00
|
|
|
local function byte()
|
|
|
|
left = left - 1
|
|
|
|
return assert(stream:read(1):byte())
|
|
|
|
end
|
2021-01-31 21:41:42 +01:00
|
|
|
|
2021-03-27 20:10:49 +01:00
|
|
|
local function int()
|
2021-10-10 18:14:35 +02:00
|
|
|
return read_int(byte, 4)
|
2021-03-27 20:10:49 +01:00
|
|
|
end
|
2021-01-31 21:41:42 +01:00
|
|
|
|
2021-03-27 20:10:49 +01:00
|
|
|
local function id()
|
|
|
|
return int() + 1
|
|
|
|
end
|
2021-01-31 21:41:42 +01:00
|
|
|
|
2021-03-27 20:10:49 +01:00
|
|
|
local function optional_id()
|
|
|
|
local id = int()
|
|
|
|
if id == -1 then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
return id + 1
|
|
|
|
end
|
2021-01-31 21:41:42 +01:00
|
|
|
|
2021-03-27 20:10:49 +01:00
|
|
|
local function string()
|
|
|
|
local rope = {}
|
|
|
|
while true do
|
|
|
|
left = left - 1
|
|
|
|
local char = assert(stream:read(1))
|
|
|
|
if char == "\0" then
|
|
|
|
return table.concat(rope)
|
|
|
|
end
|
|
|
|
table.insert(rope, char)
|
|
|
|
end
|
|
|
|
end
|
2021-01-31 21:41:42 +01:00
|
|
|
|
2021-03-27 20:10:49 +01:00
|
|
|
local function float()
|
|
|
|
return read_single(byte)
|
|
|
|
end
|
2021-01-31 21:41:42 +01:00
|
|
|
|
2021-03-27 20:10:49 +01:00
|
|
|
local function float_array(length)
|
|
|
|
local list = {}
|
|
|
|
for index = 1, length do
|
|
|
|
list[index] = float()
|
|
|
|
end
|
|
|
|
return list
|
|
|
|
end
|
2021-01-31 21:41:42 +01:00
|
|
|
|
2021-03-27 20:10:49 +01:00
|
|
|
local function color()
|
2022-01-07 18:50:36 +01:00
|
|
|
local ret = {}
|
|
|
|
ret.r = float()
|
|
|
|
ret.g = float()
|
|
|
|
ret.b = float()
|
|
|
|
ret.a = float()
|
|
|
|
return ret
|
2021-03-27 20:10:49 +01:00
|
|
|
end
|
2021-01-31 21:41:42 +01:00
|
|
|
|
2021-03-27 20:10:49 +01:00
|
|
|
local function vector3()
|
|
|
|
return float_array(3)
|
|
|
|
end
|
2021-01-31 21:41:42 +01:00
|
|
|
|
2021-03-27 20:10:49 +01:00
|
|
|
local function quaternion()
|
2022-01-07 18:50:36 +01:00
|
|
|
local w = float()
|
|
|
|
local x = float()
|
|
|
|
local y = float()
|
|
|
|
local z = float()
|
|
|
|
return {x, y, z, w}
|
2021-03-27 20:10:49 +01:00
|
|
|
end
|
2021-01-31 21:41:42 +01:00
|
|
|
|
2021-03-27 20:10:49 +01:00
|
|
|
local function content()
|
|
|
|
assert(left >= 0, stream:seek())
|
|
|
|
return left ~= 0
|
|
|
|
end
|
2021-01-31 21:41:42 +01:00
|
|
|
|
2021-03-27 20:10:49 +01:00
|
|
|
local chunk
|
|
|
|
local chunks = {
|
|
|
|
TEXS = function()
|
|
|
|
local textures = {}
|
|
|
|
while content() do
|
2022-01-07 18:50:36 +01:00
|
|
|
local tex = {}
|
|
|
|
tex.file = string()
|
|
|
|
tex.flags = int()
|
|
|
|
tex.blend = int()
|
|
|
|
tex.pos = float_array(2)
|
|
|
|
tex.scale = float_array(2)
|
|
|
|
tex.rotation = float()
|
|
|
|
table.insert(textures, tex)
|
2021-03-27 20:10:49 +01:00
|
|
|
end
|
|
|
|
return textures
|
|
|
|
end,
|
|
|
|
BRUS = function()
|
|
|
|
local brushes = {}
|
|
|
|
brushes.n_texs = int()
|
|
|
|
assert(brushes.n_texs <= 8)
|
|
|
|
while content() do
|
2022-01-07 18:50:36 +01:00
|
|
|
local brush = {}
|
|
|
|
brush.name = string()
|
|
|
|
brush.color = color()
|
|
|
|
brush.shininess = float()
|
2022-04-21 11:08:43 +02:00
|
|
|
brush.blend = int()
|
|
|
|
brush.fx = int()
|
2022-01-07 18:50:36 +01:00
|
|
|
brush.texture_id = {}
|
2021-03-27 20:10:49 +01:00
|
|
|
for index = 1, brushes.n_texs do
|
|
|
|
brush.texture_id[index] = optional_id()
|
|
|
|
end
|
|
|
|
table.insert(brushes, brush)
|
|
|
|
end
|
|
|
|
return brushes
|
|
|
|
end,
|
|
|
|
VRTS = function()
|
2022-01-07 18:50:36 +01:00
|
|
|
local vertices = {}
|
|
|
|
vertices.flags = int()
|
|
|
|
vertices.tex_coord_sets = int()
|
|
|
|
vertices.tex_coord_set_size = int()
|
2021-03-27 20:10:49 +01:00
|
|
|
assert(vertices.tex_coord_sets <= 8 and vertices.tex_coord_set_size <= 4)
|
|
|
|
local has_normal = (vertices.flags % 2 == 1) or nil
|
|
|
|
local has_color = (math.floor(vertices.flags / 2) % 2 == 1) or nil
|
|
|
|
while content() do
|
2022-01-07 18:50:36 +01:00
|
|
|
local vertex = {}
|
|
|
|
vertex.pos = vector3()
|
|
|
|
vertex.normal = has_normal and vector3()
|
|
|
|
vertex.color = has_color and color()
|
|
|
|
vertex.tex_coords = {}
|
2021-03-27 20:10:49 +01:00
|
|
|
for tex_coord_set = 1, vertices.tex_coord_sets do
|
|
|
|
local tex_coords = {}
|
|
|
|
for tex_coord = 1, vertices.tex_coord_set_size do
|
|
|
|
tex_coords[tex_coord] = float()
|
|
|
|
end
|
|
|
|
vertex.tex_coords[tex_coord_set] = tex_coords
|
|
|
|
end
|
|
|
|
table.insert(vertices, vertex)
|
|
|
|
end
|
|
|
|
return vertices
|
|
|
|
end,
|
|
|
|
TRIS = function()
|
2022-01-07 18:50:36 +01:00
|
|
|
local tris = {}
|
|
|
|
tris.brush_id = id()
|
|
|
|
tris.vertex_ids = {}
|
2021-03-27 20:10:49 +01:00
|
|
|
while content() do
|
2022-01-07 18:50:36 +01:00
|
|
|
local i = id()
|
|
|
|
local j = id()
|
|
|
|
local k = id()
|
|
|
|
table.insert(tris.vertex_ids, {i, j, k})
|
2021-03-27 20:10:49 +01:00
|
|
|
end
|
|
|
|
return tris
|
|
|
|
end,
|
|
|
|
MESH = function()
|
2022-01-07 18:50:36 +01:00
|
|
|
local mesh = {}
|
|
|
|
mesh.brush_id = optional_id()
|
|
|
|
mesh.vertices = chunk{VRTS = true}
|
2021-03-27 20:10:49 +01:00
|
|
|
mesh.triangle_sets = {}
|
|
|
|
repeat
|
|
|
|
local tris = chunk{TRIS = true}
|
|
|
|
table.insert(mesh.triangle_sets, tris)
|
|
|
|
until not content()
|
|
|
|
return mesh
|
|
|
|
end,
|
|
|
|
BONE = function()
|
|
|
|
local bone = {}
|
|
|
|
while content() do
|
|
|
|
local vertex_id = id()
|
|
|
|
assert(not bone[vertex_id], "duplicate vertex weight")
|
|
|
|
local weight = float()
|
|
|
|
if weight > 0 then
|
|
|
|
-- Many exporters include unneeded zero weights
|
|
|
|
bone[vertex_id] = weight
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return bone
|
|
|
|
end,
|
|
|
|
KEYS = function()
|
|
|
|
local flags = int()
|
|
|
|
local _flags = flags % 8
|
|
|
|
local rotation, scale, position
|
|
|
|
if _flags >= 4 then
|
|
|
|
rotation = true
|
|
|
|
_flags = _flags - 4
|
|
|
|
end
|
|
|
|
if _flags >= 2 then
|
|
|
|
scale = true
|
|
|
|
_flags = _flags - 2
|
|
|
|
end
|
|
|
|
position = _flags >= 1
|
|
|
|
local bone = {
|
|
|
|
flags = flags
|
|
|
|
}
|
|
|
|
while content() do
|
2022-01-07 18:50:36 +01:00
|
|
|
local frame = {}
|
|
|
|
frame.frame = int()
|
2021-04-22 21:04:07 +02:00
|
|
|
if position then
|
|
|
|
frame.position = vector3()
|
|
|
|
end
|
|
|
|
if scale then
|
|
|
|
frame.scale = vector3()
|
|
|
|
end
|
|
|
|
if rotation then
|
|
|
|
frame.rotation = quaternion()
|
|
|
|
end
|
|
|
|
table.insert(bone, frame)
|
2021-03-27 20:10:49 +01:00
|
|
|
end
|
|
|
|
-- Ensure frames are sorted ascending
|
|
|
|
table.sort(bone, function(a, b) return a.frame < b.frame end)
|
|
|
|
return bone
|
|
|
|
end,
|
|
|
|
ANIM = function()
|
2022-01-07 18:50:36 +01:00
|
|
|
local ret = {}
|
|
|
|
ret.flags = int() -- flags are unused
|
|
|
|
ret.frames = int()
|
|
|
|
ret.fps = float()
|
|
|
|
return ret
|
2021-03-27 20:10:49 +01:00
|
|
|
end,
|
|
|
|
NODE = function()
|
2022-01-07 18:50:36 +01:00
|
|
|
local node = {}
|
|
|
|
node.name = string()
|
|
|
|
node.position = vector3()
|
|
|
|
node.scale = vector3()
|
|
|
|
node.keys = {}
|
|
|
|
node.rotation = quaternion()
|
|
|
|
node.children = {}
|
2021-03-27 20:10:49 +01:00
|
|
|
local node_type
|
|
|
|
-- See https://github.com/blitz-research/blitz3d/blob/master/blitz3d/loader_b3d.cpp#L263
|
|
|
|
-- Order is not validated; double occurences of mutually exclusive node def are
|
|
|
|
while content() do
|
|
|
|
local elem, type = chunk()
|
|
|
|
if type == "MESH" then
|
|
|
|
assert(not node_type)
|
|
|
|
node_type = "mesh"
|
|
|
|
node.mesh = elem
|
|
|
|
elseif type == "BONE" then
|
|
|
|
assert(not node_type)
|
|
|
|
node_type = "bone"
|
|
|
|
node.bone = elem
|
|
|
|
elseif type == "KEYS" then
|
|
|
|
assert((node.keys[#node.keys] or {}).frame ~= (elem[1] or {}).frame, "duplicate frame")
|
|
|
|
modlib.table.append(node.keys, elem)
|
|
|
|
elseif type == "NODE" then
|
|
|
|
table.insert(node.children, elem)
|
|
|
|
elseif type == "ANIM" then
|
|
|
|
node.animation = elem
|
|
|
|
else
|
|
|
|
assert(not node_type)
|
|
|
|
node_type = "pivot"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
-- TODO somehow merge keys
|
|
|
|
return node
|
|
|
|
end,
|
|
|
|
BB3D = function()
|
|
|
|
local version = int()
|
|
|
|
local self = {
|
|
|
|
version = {
|
|
|
|
major = math.floor(version / 100),
|
|
|
|
minor = version % 100,
|
|
|
|
raw = version
|
|
|
|
},
|
|
|
|
textures = {},
|
|
|
|
brushes = {}
|
|
|
|
}
|
|
|
|
assert(self.version.major <= 2, "unsupported version: " .. self.version.major)
|
|
|
|
while content() do
|
|
|
|
local field, type = chunk{TEXS = true, BRUS = true, NODE = true}
|
|
|
|
if type == "TEXS" then
|
|
|
|
modlib.table.append(self.textures, field)
|
|
|
|
elseif type == "BRUS" then
|
|
|
|
modlib.table.append(self.brushes, field)
|
|
|
|
else
|
|
|
|
self.node = field
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return self
|
|
|
|
end
|
|
|
|
}
|
2021-01-31 21:41:42 +01:00
|
|
|
|
2021-03-27 20:10:49 +01:00
|
|
|
local function chunk_header()
|
|
|
|
left = left - 4
|
|
|
|
return stream:read(4), int()
|
|
|
|
end
|
2021-01-31 21:41:42 +01:00
|
|
|
|
2021-03-27 20:10:49 +01:00
|
|
|
function chunk(possible_chunks)
|
|
|
|
local type, new_left = chunk_header()
|
|
|
|
local parent_left
|
|
|
|
left, parent_left = new_left, left
|
|
|
|
if possible_chunks and not possible_chunks[type] then
|
|
|
|
error("expected one of " .. table.concat(modlib.table.keys(possible_chunks), ", ") .. ", found " .. type)
|
|
|
|
end
|
|
|
|
local res = assert(chunks[type])()
|
|
|
|
assert(left == 0)
|
|
|
|
left = parent_left - new_left
|
|
|
|
return res, type
|
|
|
|
end
|
2021-01-31 21:41:42 +01:00
|
|
|
|
2021-03-27 20:10:49 +01:00
|
|
|
local self = chunk{BB3D = true}
|
|
|
|
return setmetatable(self, metatable)
|
2021-01-31 21:41:42 +01:00
|
|
|
end
|
|
|
|
|
2021-02-02 23:15:24 +01:00
|
|
|
-- TODO function write(self, stream)
|
|
|
|
|
|
|
|
local binary_search_frame = modlib.table.binary_search_comparator(function(a, b)
|
2021-03-27 20:10:49 +01:00
|
|
|
return modlib.table.default_comparator(a, b.frame)
|
2021-02-02 23:15:24 +01:00
|
|
|
end)
|
2021-02-06 11:55:43 +01:00
|
|
|
|
2021-04-01 17:18:39 +02:00
|
|
|
--> list of { bone_name = string, parent_bone_name = string, position = vector, rotation = quaternion, scale = vector }
|
2021-02-06 11:55:43 +01:00
|
|
|
function get_animated_bone_properties(self, keyframe, interpolate)
|
2021-03-27 20:10:49 +01:00
|
|
|
local function get_frame_values(keys)
|
|
|
|
local values = keys[keyframe]
|
|
|
|
if values and values.frame == keyframe then
|
|
|
|
return {
|
|
|
|
position = values.position,
|
|
|
|
rotation = values.rotation,
|
|
|
|
scale = values.scale
|
|
|
|
}
|
|
|
|
end
|
|
|
|
local index = binary_search_frame(keys, keyframe)
|
|
|
|
if index > 0 then
|
|
|
|
return keys[index]
|
|
|
|
end
|
|
|
|
index = -index
|
|
|
|
assert(index > 1 and index <= #keys)
|
|
|
|
local a, b = keys[index - 1], keys[index]
|
|
|
|
if not interpolate then
|
|
|
|
return a
|
|
|
|
end
|
|
|
|
local ratio = (keyframe - a.frame) / (b.frame - a.frame)
|
|
|
|
return {
|
|
|
|
position = (a.position and b.position and modlib.vector.interpolate(a.position, b.position, ratio)) or a.position or b.position,
|
2021-04-01 18:45:58 +02:00
|
|
|
rotation = (a.rotation and b.rotation and modlib.quaternion.slerp(a.rotation, b.rotation, ratio)) or a.rotation or b.rotation,
|
2021-03-27 20:10:49 +01:00
|
|
|
scale = (a.scale and b.scale and modlib.vector.interpolate(a.scale, b.scale, ratio)) or a.scale or b.scale,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
local bone_properties = {}
|
2021-04-01 17:18:39 +02:00
|
|
|
local function get_props(node, parent_bone_name)
|
|
|
|
local properties = {parent_bone_name = parent_bone_name}
|
2021-03-27 20:10:49 +01:00
|
|
|
if node.keys and next(node.keys) ~= nil then
|
|
|
|
properties = modlib.table.add_all(properties, get_frame_values(node.keys))
|
|
|
|
end
|
|
|
|
for _, property in pairs{"position", "rotation", "scale"} do
|
|
|
|
properties[property] = properties[property] or modlib.table.copy(node[property])
|
|
|
|
end
|
2022-05-11 19:32:38 +02:00
|
|
|
properties.rotation = modlib.quaternion.compose(node.rotation, properties.rotation)
|
2021-03-27 20:10:49 +01:00
|
|
|
if node.bone then
|
2021-04-01 17:18:39 +02:00
|
|
|
properties.bone_name = node.name
|
2021-04-01 17:12:00 +02:00
|
|
|
table.insert(bone_properties, properties)
|
2021-03-27 20:10:49 +01:00
|
|
|
end
|
|
|
|
for _, child in pairs(node.children or {}) do
|
2021-04-01 17:18:39 +02:00
|
|
|
get_props(child, properties.bone_name)
|
2021-03-27 20:10:49 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
get_props(self.node)
|
|
|
|
return bone_properties
|
2021-06-17 19:45:08 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
-- Export environment
|
|
|
|
return _ENV
|