diff --git a/b3d.lua b/b3d.lua index 4e3f61f..6787ae9 100644 --- a/b3d.lua +++ b/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) diff --git a/test.lua b/test.lua index f6bfc41..8ba60b7 100644 --- a/test.lua +++ b/test.lua @@ -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