added bone transform reading, interpolation not fully tested

This commit is contained in:
FatalErr42O 2023-09-01 18:19:55 -07:00
parent 129385c55e
commit 15fff69e75
7 changed files with 503 additions and 56 deletions

15
LICENSE
View File

@ -1,15 +1,8 @@
WARNING, THIS LICENSE DOES NOT AND IS NOT INTENDED TO APPLY FOR FILES WHICH
HAVE A "License.md", "LICENSE", "License.txt" "LICENSE.txt" OR ANY FILE NAMED
AFTER THE WORD "license" IN THEIR SHARED DIRECTORY;
"/modlib/" sub directory: Copright (c) respective owner(s), see the directory
for it's respective license (License.txt). Once again note that the "/modlib/"
directory in this case is explicitly exempt from the following MIT License.
IF A FILE IS IN THE SAME DIRECTORY AS A FILE NAMED ANY FORM OF THE WORD LICENSE
IN ANY COMBINATION WITH OR WITHOUT AN EXTENSION, THEN SOLEY THAT LICENSE IS TO
APPLY TO THOSE FILES.
IF A DIRECTORY DOES NOT CONTAIN A LICENSE, THE FOLLOWING LICENSE AGREEMENT APPLIES.
TO BE EXPLICIT, THE "modlib" DIRECTORY HAS IT'S OWN DISTINCT LICENSE AND COPYRIGHT
HOLDER. Please see it's own "License.txt"
for all other files outside of "/modlib/" directory:
MIT License

0
api.md Normal file
View File

260
b3d_specification.txt Normal file
View 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.

View File

@ -1,12 +1,32 @@
--[[
this library provides two modules:
b3d
b3d_nodes
b3d_nodes is for reading and interperetting b3d objects from the b3d module.
the b3d module is a heavily modified version of Modlib's b3d reader, and as such
has it's own respective directory for licensing purposes.
]]
mtul.b3d = {}
mtul.loaded_modules.b3d = true
local modpath = minetest.get_modpath("mtul_b3d")
--placed in a seperate directory for the license
dofile(modpath.."/modlib/read_b3d.lua")
dofile(modpath.."/modlib/write_b3d.lua") --this is untested, could be very broken.
--these modules are disabled, refactoring is needed.
if mtul.math.cpml_loaded then
dofile(modpath.."/read_b3d_bone")
--prevent accidental access of unavailable features:
if mtul.loaded_modules.cpml then
mtul.b3d_nodes = dofile(modpath.."/nodes.lua")
mtul.loaded_modules.b3d_nodes = true
else
mtul.b3d_nodes = {}
setmetatable(mtul.b3d_nodes, {
__index = function()
error("MTUL-CPML not present, b3d_nodes module inaccessible.")
end
})
end
--dofile(modpath.."/read_b3d_bone.lua"

View File

@ -1,12 +1,61 @@
--this reader has been heavily modified to implement additional needed features.
--implementations include:
--mtul.b3d.read_model()
--ignore_chunks parameter,
--node.parent,
--node.path
--b3d.node_paths
-- Localize globals
local read_int, read_single = mtul.binary.read_int, mtul.binary.read_single
--+ 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 mtul.b3d.read(stream)
--reads a model directly (based on name). Note that "node_only" abstracts chunks not necessary to finding the position/transform of a bone/node.
function mtul.b3d.read_model(modelname, node_only)
local path = mtul.media_paths[modelname]
local out
if path then
local ignored
if node_only then
ignored = {"TEXS", "BRUS", "BONE", "MESH"}
end
local stream = io.open(path, "rb")
if not stream then return end --if the file wasn't found we probably shouldnt just assert.
out = mtul.b3d.read_from_stream(stream, ignored)
assert(stream:read(1)==nil, "MTUL B3D: unknown error, EOF not reached")
stream:close()
end
return out
end
--"ignore_chunks" is a list of chunks to ignore when reading- as for various applications it may be uncessary or otherwise redundant
--note that this does not increase runtime spead as chunks still must be read before we know what they are (currently)
--chunk types: "BB3D", "TEXS", "BRUS", "TRIS", "MESH", "BONE", "ANIM", "KEYS", "VRTS", "NODE"
--this specifies what chunks can be found inside eachother
--MESH subtypes: VRTS, TRIS
--BB3D subtypes: TEXS, NODE
--NODE subtypes: KEYS, ANIM, NODE, BONE, MESH
--node_paths is a table of nodes indexed by a table containing a hierarchal list of nodes to get to that node (including itself).
--this is ideal if you need to, say, solve for the transform of a node- instead of iterating 100s of times to get every parent node
--it's all provided for you. Note that it's from highest to lowest, where lowest of course is the current node, the last element.
function mtul.b3d.read_from_stream(stream, ignore_chunks)
local left = 8
local ignored = {}
if ignore_chunks then
for i, v in pairs(ignore_chunks) do
ignored[v] = true
end
assert(not ignored.BB3D, "reader cannot not ignore entire model. (ignore_chunks contained BB3D)")
assert(not ignored.NODE, "reader cannot not ignore entire model. (ignore_chunks contained NODE)")
end
local function byte()
left = left - 1
return assert(stream:read(1):byte())
@ -80,6 +129,10 @@ function mtul.b3d.read(stream)
return left ~= 0
end
local node_chunk_types = {
}
local chunk
local chunks = {
TEXS = function()
@ -220,7 +273,9 @@ function mtul.b3d.read(stream)
node.name = string()
node.position = vector3()
node.scale = vector3()
node.keys = {}
if not ignored.KEYS then
node.keys = {}
end
node.rotation = quaternion()
node.children = {}
local node_type
@ -228,25 +283,30 @@ function mtul.b3d.read(stream)
-- Order is not validated; double occurrences 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
mtul.tbl.append(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"
if not ignored[type] then
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
mtul.tbl.append(node.keys, elem)
elseif type == "NODE" then
elem.parent = node
table.insert(node.children, elem)
elseif type == "ANIM" then
node.animation = elem
else
assert(not node_type)
node_type = "pivot"
end
end
end
--added because ignored nodes may unintentionally obfuscate the type of node- which could be necessary for finding bone "paths"
node.type = node_type
-- Ensure frames are sorted ascendingly
table.sort(node.keys, function(a, b)
assert(a.frame ~= b.frame, "duplicate frame")
@ -261,18 +321,20 @@ function mtul.b3d.read(stream)
major = math.floor(version / 100),
minor = version % 100,
},
textures = {},
brushes = {}
}
if not ignored.TEXS then self.textures = {} end
if not ignored.BRUS then self.brushes = {} end
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
mtul.tbl.append(self.textures, field)
elseif type == "BRUS" then
mtul.tbl.append(self.brushes, field)
else
self.node = field
if not ignored[type] then
if type == "TEXS" then
mtul.tbl.append(self.textures, field)
elseif type == "BRUS" then
mtul.tbl.append(self.brushes, field)
else
self.node = field
end
end
end
return self
@ -297,6 +359,32 @@ function mtul.b3d.read(stream)
return res, type
end
--due to the nature of how the b3d is read, paths have to be built by recursively iterating the table in post.
--luckily most of the ground work is layed out for us already.
--also, Fatal here: for the sake of my reputation (which is nonexistent), typically I wouldn't nest these functions
--because I am not a physcopath and or a german named Lars, but for the sake of consistency it has to happen. (Not that its *always* a bad idea, but unless you're baking in parameters it's sort of awful)
local copy_path = mtul.table and mtul.table.shallow_copy or function(tbl)
local new_table = {}
for i, v in pairs(tbl) do
new_table[i] = v
end
return new_table
end
local function make_paths(node, path, node_paths)
local new_path = copy_path(path)
table.insert(new_path, node)
node_paths[new_path] = node --this will create a list of paths
for i, next_node in pairs(node.children) do
make_paths(next_node, new_path, node_paths)
end
node.path = new_path
end
local self = chunk{BB3D = true}
return setmetatable(self, mtul.b3d)
self.node_paths = {}
make_paths(self.node, {}, self.node_paths)
--b3d metatable unimplemented
return setmetatable(self, mtul._b3d_metatable or {})
end

100
nodes.lua Normal file
View File

@ -0,0 +1,100 @@
--gets node by name
--this breaks if you have multiple nodes with the same name.
--if there are meshes that go by the same name, you can set "bone" param to true.
local b3d_nodes = {}
function b3d_nodes.get_node_by_name(self, node_name, is_bone)
for i, this_node in pairs(self.node_paths) do
if ( (not is_bone) or (this_node.type=="bone") ) and (this_node.name == node_name) then
return this_node
end
end
error("MTUL-b3d, b3d_nodes: no node found by the name '"..node_name.."'")
end
--non-methods:
local interpolate = function(a, b, ratio)
local out = {}
for i, v in pairs(a) do
out[i] = a[i]-((a[i]-b[i])*ratio)
end
return out
end
function b3d_nodes.get_animated_local_transform(node, target_frame)
local frames = node.keys
local key_index_before = 0 --index of the key before the target_frame.
for i, key in ipairs(frames) do
--pick the closest frame we find that's less then the target
if key.frame < target_frame then
key_index_before = i
else
break --we've reached the end of our possible frames to use.
end
end
--need this so we can replace it if before doesnt exist
local frame_before_tbl = frames[key_index_before]
local frame_after_tbl = frames[key_index_before+1] --frame to interpolate will be out immediate neighbor since we know its either the frame or after the frame.
print(target_frame)
--it may still be zero, indicating that the frame before doesnt exist.
if not frame_before_tbl then
frame_before_tbl = node --set it to the node so it pulls from PRS directly as that's it's default state.
end
--no point in interpolating if it's all the same...
if frame_after_tbl then
local f1 = frame_before_tbl.frame or -1
local f2 = frame_after_tbl.frame --if there's no frame after that then
local ratio = (f1-target_frame)/(f1-f2) --find the interpolation ratio
return
interpolate(frame_before_tbl.position, frame_after_tbl.position, ratio),
interpolate(frame_before_tbl.rotation, frame_after_tbl.rotation, ratio),
interpolate(frame_before_tbl.scale, frame_after_tbl.scale, ratio)
else
return
table.copy(frame_before_tbl.position),
table.copy(frame_before_tbl.rotation),
table.copy(frame_before_tbl.scale)
end
end
local mat4 = mtul.math.mat4
local quat = mtul.math.quat
function b3d_nodes.get_node_global_transform(node, frame)
local global_transform
for i, current_node in pairs(node.path) do
local pos_vec, rot_vec, scl_vec = b3d_nodes.get_animated_local_transform(current_node, frame)
--rot_vec = {rot_vec[2], rot_vec[3], rot_vec[4], rot_vec[1]}
local local_transform = mat4.identity()
--translate rotation by position
local_transform = local_transform:translate(local_transform, {-pos_vec[1], pos_vec[2], pos_vec[3]})
local_transform = local_transform*(mat4.from_quaternion(quat.new(-rot_vec[1], rot_vec[2], rot_vec[3], rot_vec[4]):normalize()))
--scale the mat4.
--local_transform = local_transform:scale(local_transform, {scl_vec[1], scl_vec[2], scl_vec[3]})
--I dont really know why this works and the above doesn't, but at this point I'm done trying to figure it out...
local identity = mat4.identity()
local_transform = local_transform*identity:scale(identity, {scl_vec[1], scl_vec[2], scl_vec[3]})
--get new global trasnform with the local.
if global_transform then
global_transform=global_transform*local_transform
else
global_transform=local_transform
end
end
--pos = global_transform:apply({pos[1], pos[2], pos[3], 1})
--print(dump(global_transform))
--return vector.new(pos[1], pos[2], pos[3])
return global_transform
end
--Returns X, Y, Z. is_bone is optional, if "node" is the name of a node (and not the node table), this is used to find it.
function b3d_nodes.get_node_position(self, node, is_bone, frame)
if type(node) == "string" then
node = b3d_nodes.get_node_by_name(self, node)
end
local transform = b3d_nodes.get_node_global_transform(node, frame)
return transform[13], transform[14], transform[15]
end
--since it's impossible to determine the difference between rotation
--and non-uniform scaling, we have to use a different method for this.
function b3d_nodes.get_node_rotation()
end
return b3d_nodes

View File

@ -1,14 +0,0 @@
--INACTIVE FILE
local binary_search_frame = modlib.table.binary_search_comparator(function(a, b)
return modlib.table.default_comparator(a, b.frame)
end)
--> list of { bone_name = string, parent_bone_name = string, position = vector, rotation = quaternion, scale = vector }
function mtul.b3d:get_bone_global_transform(self, node_name)
end
function mtul.b3d:get_bone