Clean up B3D reader, add B3D writer

This commit is contained in:
Lars Mueller 2022-06-08 11:15:47 +02:00
parent c0b2957c67
commit c088057d56
2 changed files with 240 additions and 16 deletions

247
b3d.lua

@ -1,15 +1,17 @@
-- Localize globals
local assert, error, math, modlib, next, pairs, setmetatable, table = assert, error, math, modlib, next, pairs, setmetatable, table
local assert, error, math, modlib, next, ipairs, pairs, setmetatable, string_char, table
= assert, error, math, modlib, next, ipairs, pairs, setmetatable, string.char, table
local read_int, read_single = modlib.binary.read_int, modlib.binary.read_single
local write_int, write_single = modlib.binary.write_int, modlib.binary.write_single
-- Set environment
local _ENV = {}
setfenv(1, _ENV)
local metatable = {__index = _ENV}
--! 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
@ -84,7 +86,9 @@ function read(stream)
end
local function content()
assert(left >= 0, stream:seek())
if left < 0 then
error(("unexpected EOF at position %d"):format(stream:seek()))
end
return left ~= 0
end
@ -106,8 +110,8 @@ function read(stream)
end,
BRUS = function()
local brushes = {}
brushes.n_texs = int()
assert(brushes.n_texs <= 8)
local n_texs = int()
assert(n_texs <= 8)
while content() do
local brush = {}
brush.name = string()
@ -116,7 +120,7 @@ function read(stream)
brush.blend = int()
brush.fx = int()
brush.texture_id = {}
for index = 1, brushes.n_texs do
for index = 1, n_texs do
brush.texture_id[index] = optional_id()
end
table.insert(brushes, brush)
@ -214,8 +218,6 @@ function read(stream)
end
table.insert(bone, frame)
end
-- Ensure frames are sorted ascending
table.sort(bone, function(a, b) return a.frame < b.frame end)
return bone
end,
ANIM = function()
@ -235,7 +237,7 @@ function read(stream)
node.children = {}
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
-- Order is not validated; double occurrences of mutually exclusive node def are
while content() do
local elem, type = chunk()
if type == "MESH" then
@ -247,7 +249,6 @@ function read(stream)
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)
@ -258,7 +259,11 @@ function read(stream)
node_type = "pivot"
end
end
-- TODO somehow merge keys
-- Ensure frames are sorted ascendingly
table.sort(node.keys, function(a, b)
assert(a.frame ~= b.frame, "duplicate frame")
return a.frame < b.frame
end)
return node
end,
BB3D = function()
@ -267,7 +272,6 @@ function read(stream)
version = {
major = math.floor(version / 100),
minor = version % 100,
raw = version
},
textures = {},
brushes = {}
@ -309,7 +313,224 @@ function read(stream)
return setmetatable(self, metatable)
end
-- TODO function write(self, stream)
-- Writer
local function write_rope(self)
local rope = {}
local written_len = 0
local function write(str)
written_len = written_len + #str
table.insert(rope, str)
end
local function byte(val)
write(string_char(val))
end
local function int(val)
write_int(byte, val, 4)
end
local function id(val)
int(val - 1)
end
local function optional_id(val)
int(val and (val - 1) or -1)
end
local function string(val)
write(val)
write"\0"
end
local function float(val)
write_single(byte, val)
end
local function float_array(arr, len)
assert(#arr == len)
for i = 1, len do
float(arr[i])
end
end
local function color(val)
float(val.r)
float(val.g)
float(val.b)
float(val.a)
end
local function vector3(val)
float_array(val, 3)
end
local function quaternion(quat)
float(quat[4])
float(quat[1])
float(quat[2])
float(quat[3])
end
local function chunk(name, write_func)
write(name)
-- Insert placeholder for the 4-bit len
table.insert(rope, false)
written_len = written_len + 4
local len_idx = #rope -- save index of placeholder
local prev_written_len = written_len
write_func()
-- Write the length of this chunk
local chunk_len = written_len - prev_written_len
local len_binary = {}
write_int(function(byte)
table.insert(len_binary, string_char(byte))
end, chunk_len, 4)
rope[len_idx] = table.concat(len_binary)
end
local function NODE(node)
chunk("NODE", function()
string(node.name)
vector3(node.position)
vector3(node.scale)
quaternion(node.rotation)
local mesh = node.mesh
if mesh then
chunk("MESH", function()
optional_id(mesh.brush_id)
local vertices = mesh.vertices
chunk("VRTS", function()
int(vertices.flags)
int(vertices.tex_coord_sets)
int(vertices.tex_coord_set_size)
for _, vertex in ipairs(vertices) do
if vertex.pos then vector3(vertex.pos) end
if vertex.normal then vector3(vertex.normal) end
if vertex.color then color(vertex.color) end
for tex_coord_set = 1, vertices.tex_coord_sets do
local tex_coords = vertex.tex_coords[tex_coord_set]
for tex_coord = 1, vertices.tex_coord_set_size do
float(tex_coords[tex_coord])
end
end
end
end)
for _, triangle_set in ipairs(mesh.triangle_sets) do
chunk("TRIS", function()
id(triangle_set.brush_id)
for _, tri in ipairs(triangle_set.vertex_ids) do
id(tri[1])
id(tri[2])
id(tri[3])
end
end)
end
end)
end
if node.bone then
chunk("BONE", function()
for vertex_id, weight in pairs(node.bone) do
id(vertex_id)
float(weight)
end
end)
end
if node.keys then
local keys_by_flags = {}
for _, key in ipairs(node.keys) do
local flags = 0
flags = flags
+ (key.position and 1 or 0)
+ (key.scale and 2 or 0)
+ (key.rotation and 4 or 0)
keys_by_flags[flags] = keys_by_flags[flags] or {}
table.insert(keys_by_flags[flags], key)
end
for flags, keys in pairs(keys_by_flags) do
chunk("KEYS", function()
int(flags)
for _, frame in ipairs(keys) do
int(frame.frame)
if frame.position then vector3(frame.position) end
if frame.scale then vector3(frame.scale) end
if frame.rotation then quaternion(frame.rotation) end
end
end)
end
end
local anim = node.animation
if anim then
chunk("ANIM", function()
int(anim.flags)
int(anim.frames)
float(anim.fps)
end)
end
for _, child in ipairs(node.children) do
NODE(child)
end
end)
end
chunk("BB3D", function()
int(self.version.major * 100 + self.version.minor)
if self.textures[1] then
chunk("TEXS", function()
for _, tex in ipairs(self.textures) do
string(tex.file)
int(tex.flags)
int(tex.blend)
float_array(tex.pos, 2)
float_array(tex.scale, 2)
float(tex.rotation)
end
end)
end
if self.brushes[1] then
local max_n_texs = 0
for _, brush in ipairs(self.brushes) do
for n in pairs(brush.texture_id) do
if n > max_n_texs then
max_n_texs = n
end
end
end
chunk("BRUS", function()
int(max_n_texs)
for _, brush in ipairs(self.brushes) do
string(brush.name)
color(brush.color)
float(brush.shininess)
int(brush.blend)
int(brush.fx)
for index = 1, max_n_texs do
optional_id(brush.texture_id[index])
end
end
end)
end
if self.node then
NODE(self.node)
end
end)
return rope
end
function write_string(self)
return table.concat(write_rope(self))
end
function write(self, stream)
for _, str in ipairs(write_rope(self)) do
stream:write(str)
end
end
local binary_search_frame = modlib.table.binary_search_comparator(function(a, b)
return modlib.table.default_comparator(a, b.frame)

@ -480,9 +480,9 @@ local tests = {
}
if tests.b3d then
local stream = assert(io.open(mod.get_resource("player_api", "models", "character.b3d"), "r"))
local b3d = b3d.read(stream)
local character = assert(b3d.read(stream))
stream:close()
--! dirty helper method to create truncate tables with 10+ number keys
--! dirty helper method to truncate tables with 10+ number keys
local function _b3d_truncate(table)
local count = 1
for key, value in pairs(table) do
@ -503,7 +503,10 @@ if tests.b3d then
end
return table
end
file.write(mod.get_resource"character.b3d.lua", "return " .. dump(_b3d_truncate(table.copy(b3d))))
local str = character:write_string()
local read = b3d.read(text.inputstream(str))
assert(modlib.table.equals_noncircular(character, read))
file.write(mod.get_resource"character.b3d.lua", "return " .. dump(_b3d_truncate(table.copy(character))))
end
local vector, minetest, ml_mt = _G.vector, _G.minetest, minetest
if tests.liquid_dir then