leef-b3d-cd2025/modlib/write_b3d.lua
2024-12-27 18:30:51 -08:00

239 lines
5.5 KiB
Lua

--- writes b3d models in the same format as outputted by the b3d reader modul
--
-- This is apart of the [LEEF-b3d](https://github.com/Luanti-Extended-Engine-Features/LEEF-b3d) module
--
--@module b3d_writer
--Writer
local write_int, write_single = leef.binary.write_int, leef.binary.write_single
local string_char = string.char
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, leef.binary.fround(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()
assert(node.scale, "a node is missing a name")
string(node.name)
assert(node.scale, "a node is missing position")
vector3(node.position)
assert(node.scale, "a node is missing scale")
vector3(node.scale)
assert(node.scale, "a node is missing rotation")
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
vector3(vertex.pos)
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
--- output a string of binary in the blitz 3d format
-- @function write_string
-- @param self @{b3d_reader.BB3D|BB3D}
-- @return string containing the binary file
function leef.b3d_writer.write_string(self)
return table.concat(write_rope(self))
end
--- output in the blitz3d format file reference
-- @function write_model_to_file
-- @param self @{b3d_reader.BB3D|BB3D}
-- @param stream io file object to write to
function leef.b3d_writer.write_model_to_file(self, stream)
for _, str in ipairs(write_rope(self)) do
stream:write(str)
end
end