mirror of
https://github.com/appgurueu/modlib.git
synced 2024-11-22 07:13:45 +01:00
Experimental Blitz3D .b3d file reader
This commit is contained in:
parent
b9483fb919
commit
f5a7d6b1fe
321
b3d.lua
Normal file
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
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.
|
3
init.lua
3
init.lua
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user