Experimental Blitz3D .b3d file reader

This commit is contained in:
Lars Mueller 2021-01-31 21:41:42 +01:00
parent b9483fb919
commit f5a7d6b1fe
3 changed files with 583 additions and 1 deletions

321
b3d.lua Normal file

@ -0,0 +1,321 @@
local metatable = {__index = getfenv(1)}
--! 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)
local left = 8
local function byte()
left = left - 1
return assert(stream:read(1):byte())
end
local function int()
local value = byte() + byte() * 0x100 + byte() * 0x10000 + byte() * 0x1000000
if value >= 2^31 then
return value - 2^32
end
return value
end
local function id()
return int() + 1
end
local function optional_id()
local id = int()
if id == -1 then
return
end
return id + 1
end
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
local function float()
-- TODO properly truncate to single floating point
local byte_4, byte_3, byte_2, byte_1 = byte(), byte(), byte(), byte()
local sign = 1
if byte_1 >= 0x80 then
sign = -1
byte_1 = byte_1 - 0x80
end
local exponent = byte_1 * 2
if byte_2 >= 0x80 then
byte_2 = byte_2 - 0x80
exponent = exponent + 1
end
local mantissa = ((((byte_4 / 0x100) + byte_3) / 0x100) + byte_2) / 0x80
if exponent == 0xFF then
if mantissa == 0 then
return sign * math.huge
end
-- TODO differentiate quiet and signalling NaN as well as positive and negative
return 0/0
end
if exponent == 0 then
-- subnormal value
return sign * 2^-126 * mantissa
end
return sign * 2 ^ (exponent - 127) * (1 + mantissa)
end
local function float_array(length)
local list = {}
for index = 1, length do
list[index] = float()
end
return list
end
local function color()
return {
r = float(),
g = float(),
b = float(),
a = float()
}
end
local function vector3()
return float_array(3)
end
local function quaternion()
return {[4] = float(), [1] = float(), [2] = float(), [3] = float()}
end
local function content()
assert(left >= 0, stream:seek())
return left ~= 0
end
local chunk
local chunks = {
TEXS = function()
local textures = {}
while content() do
table.insert(textures, {
file = string(),
flags = int(),
blend = int(),
pos = float_array(2),
scale = float_array(2),
rotation = float()
})
end
return textures
end,
BRUS = function()
local brushes = {}
brushes.n_texs = int()
assert(brushes.n_texs <= 8)
while content() do
local brush = {
name = string(),
color = color(),
shininess = float(),
blend = float(),
fx = float(),
texture_id = {}
}
for index = 1, brushes.n_texs do
brush.texture_id[index] = optional_id()
end
table.insert(brushes, brush)
end
return brushes
end,
VRTS = function()
local vertices = {
flags = int(),
tex_coord_sets = int(),
tex_coord_set_size = int()
}
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
local vertex = {
pos = vector3(),
normal = has_normal and vector3(),
color = has_color and color(),
tex_coords = {}
}
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()
local tris = {
brush_id = id(),
vertex_ids = {}
}
while content() do
table.insert(tris.vertex_ids, {id(), id(), id()})
end
return tris
end,
MESH = function()
local mesh = {
brush_id = optional_id(),
vertices = chunk{VRTS = true}
}
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
table.insert(bone, {
vertex_id = id(),
weight = float()
})
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
table.insert(bone, {
frame = int(),
position = position and vector3() or nil,
scale = scale and vector3() or nil,
rotation = rotation and quaternion() or nil
})
end
-- Ensure frames are sorted ascending
table.sort(bone, function(a, b) return a.frame < b.frame end)
return bone
end,
ANIM = function()
return {
-- flags are unused
flags = int(),
frames = int(),
fps = float()
}
end,
NODE = function()
local node = {
name = string(),
position = vector3(),
scale = vector3(),
keys = {},
rotation = quaternion(),
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
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
table.insert(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
}
local function chunk_header()
left = left - 4
return stream:read(4), int()
end
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
local self = chunk{BB3D = true}
return setmetatable(self, metatable)
end
-- TODO function write(self, stream)

260
b3d_specification.txt Normal file

@ -0,0 +1,260 @@
************************************************************************************
* Blitz3d file format V0.01 *
************************************************************************************
This document and the information contained within is placed in the Public Domain.
Please visit http://www.blitzbasic.co.nz for the latest version of this document.
Please contact marksibly@blitzbasic.co.nz for more information and general inquiries.
************************************************************************************
* Introduction *
************************************************************************************
The Blitz3D file format specifies a format for storing texture, brush and entity descriptions for
use with the Blitz3D programming language.
The rationale behind the creation of this format is to allow for the generation of much richer and
more complex Blitz3D scenes than is possible using established file formats - many of which do not
support key features of Blitz3D, and all of which miss out on at least some features!
A Blitz3D (.b3d) file is split up into a sequence of 'chunks', each of which can contain data
and/or other chunks.
Each chunk is preceded by an eight byte header:
char tag[4] ;4 byte chunk 'tag'
int length ;4 byte chunk length (not including *this* header!)
If a chunk contains both data and other chunks, the data always appears first and is of a fixed
length.
A file parser should ignore unrecognized chunks.
Blitz3D files are stored little endian (intel) style.
Many aspects of the file format are not quite a 'perfect fit' for the way Blitz3D works. This has
been done mainly to keep the file format simple, and to make life easier for the authors of third
party importers/exporters.
************************************************************************************
* Chunk Types *
************************************************************************************
This lists the types of chunks that can appear in a b3d file, and the data they contain.
Color values are always in the range 0 to 1.
string (char[]) values are 'C' style null terminated strings.
Quaternions are used to specify general orientations. The first value is the quaternion 'w' value,
the next 3 are the quaternion 'vector'. A 'null' rotation should be specified as 1,0,0,0.
Anything that is referenced 'by index' always appears EARLIER in the file than anything that
references it.
brush_id references can be -1: no brush.
In the following descriptions, {} is used to signify 'repeating until end of chunk'. Also, a chunk
name enclosed in '[]' signifies the chunk is optional.
Here we go!
BB3D
int version ;file format version: default=1
[TEXS] ;optional textures chunk
[BRUS] ;optional brushes chunk
[NODE] ;optional node chunk
The BB3D chunk appears first in a b3d file, and its length contains the rest of the file.
Version is in major*100+minor format. To check the version, just divide by 100 and compare it with
the major version your software supports, eg:
if file_version/100>my_version/100
RuntimeError "Can't handle this file version!"
EndIf
if file_version Mod 100>my_version Mod 100
;file is a more recent version, but should still be backwardly compatbile with what we can
handle!
EndIf
TEXS
{
char file[] ;texture file name
int flags,blend ;blitz3D TextureFLags and TextureBlend: default=1,2
float x_pos,y_pos ;x and y position of texture: default=0,0
float x_scale,y_scale ;x and y scale of texture: default=1,1
float rotation ;rotation of texture (in radians): default=0
}
The TEXS chunk contains a list of all textures used in the file.
The flags field value can conditional an additional flag value of '65536'. This is used to indicate that the texture uses secondary UV values, ala the TextureCoords command. Yes, I forgot about this one.
BRUS
int n_texs
{
char name[] ;eg "WATER" - just use texture name by default
float red,green,blue,alpha ;Blitz3D Brushcolor and Brushalpha: default=1,1,1,1
float shininess ;Blitz3D BrushShininess: default=0
int blend,fx ;Blitz3D Brushblend and BrushFX: default=1,0
int texture_id[n_texs] ;textures used in brush
}
The BRUS chunk contains a list of all brushes used in the file.
VRTS:
int flags ;1=normal values present, 2=rgba values present
int tex_coord_sets ;texture coords per vertex (eg: 1 for simple U/V) max=8
int tex_coord_set_size ;components per set (eg: 2 for simple U/V) max=4
{
float x,y,z ;always present
float nx,ny,nz ;vertex normal: present if (flags&1)
float red,green,blue,alpha ;vertex color: present if (flags&2)
float tex_coords[tex_coord_sets][tex_coord_set_size] ;tex coords
}
The VRTS chunk contains a list of vertices. The 'flags' value is used to indicate how much extra
data (normal/color) is stored with each vertex, and the tex_coord_sets and tex_coord_set_size
values describe texture coordinate information stored with each vertex.
TRIS:
int brush_id ;brush applied to these TRIs: default=-1
{
int vertex_id[3] ;vertex indices
}
The TRIS chunk contains a list of triangles that all share a common brush.
MESH:
int brush_id ;'master' brush: default=-1
VRTS ;vertices
TRIS[,TRIS...] ;1 or more sets of triangles
The MESH chunk describes a mesh. A mesh only has one VRTS chunk, but potentially many TRIS chunks.
BONE:
{
int vertex_id ;vertex affected by this bone
float weight ;how much the vertex is affected
}
The BONE chunk describes a bone. Weights are applied to the mesh described in the enclosing ANIM -
in 99% of cases, this will simply be the MESH contained in the root NODE chunk.
KEYS:
int flags ;1=position, 2=scale, 4=rotation
{
int frame ;where key occurs
float position[3] ;present if (flags&1)
float scale[3] ;present if (flags&2)
float rotation[4] ;present if (flags&4)
}
The KEYS chunk is a list of animation keys. The 'flags' value describes what kind of animation
info is stored in the chunk - position, scale, rotation, or any combination of.
ANIM:
int flags ;unused: default=0
int frames ;how many frames in anim
float fps ;default=60
The ANIM chunk describes an animation.
NODE:
char name[] ;name of node
float position[3] ;local...
float scale[3] ;coord...
float rotation[4] ;system...
[MESH|BONE] ;what 'kind' of node this is - if unrecognized, just use a Blitz3D
pivot.
[KEYS[,KEYS...]] ;optional animation keys
[NODE[,NODE...]] ;optional child nodes
[ANIM] ;optional animation
The NODE chunk describes a Blitz3D Entity. The scene hierarchy is expressed by the nesting of NODE
chunks.
NODE kinds are currently mutually exclusive - ie: a node can be a MESH, or a BONE, but not both!
However, it can be neither...if no kind is specified, the node is just a 'null' node - in Blitz3D
speak, a pivot.
The presence of an ANIM chunk in a NODE indicates that an animation starts here in the hierarchy.
This allows animations of differing speeds/lengths to be potentially nested.
There are many more 'kind' chunks coming, including camera, light, sprite, plane etc. For now, the
use of a Pivot in cases where the node kind is unknown will allow for backward compatibility.
************************************************************************************
* Examples *
************************************************************************************
A typical b3d file will contain 1 TEXS chunk, 1 BRUS chunk and 1 NODE chunk, like this:
BB3D
1
TEXS
...list of textures...
BRUS
...list of brushes...
NODE
...stuff in the node...
A simple, non-animating, non-textured etc mesh might look like this:
BB3D
1 ;version
NODE
"root_node" ;node name
0,0,0 ;position
1,1,1 ;scale
1,0,0,0 ;rotation
MESH ;the mesh
-1 ;brush: no brush
VRTS ;vertices in the mesh
0 ;no normal/color info in verts
0,0 ;no texture coords in verts
{x,y,z...} ;vertex coordinates
TRIS ;triangles in the mesh
-1 ;no brush for this triangle
{v0,v1,v2...} ;vertices
A more complex 'skinned mesh' might look like this (only chunks shown):
BB3D
TEXS ;texture list
BRUS ;brush list
NODE ;root node
MESH ;mesh - the 'skin'
ANIM ;anim
NODE ;first child of root node - eg: "pelvis"
BONE ;vertex weights for pelvis
KEYS ;anim keys for pelvis
NODE ;first child of pelvis - eg: "left-thigh"
BONE ;bone
KEYS ;anim keys for left-thigh
NODE ;second child of pelvis - eg: "right-thigh"
BONE ;vertex weights for right-thigh
KEYS ;anim keys for right-thigh
...and so on.

@ -75,7 +75,8 @@ for _, component in ipairs{
"minetest",
"trie",
"heap",
"ranked_set"
"ranked_set",
"b3d"
} do
modlib[component] = loadfile_exports(get_resource(component .. ".lua"))
end