diff --git a/b3d.lua b/b3d.lua new file mode 100644 index 0000000..daec7fd --- /dev/null +++ b/b3d.lua @@ -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) \ No newline at end of file diff --git a/b3d_specification.txt b/b3d_specification.txt new file mode 100644 index 0000000..81c8625 --- /dev/null +++ b/b3d_specification.txt @@ -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. diff --git a/init.lua b/init.lua index ca45a89..a4fca65 100644 --- a/init.lua +++ b/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