174 lines
8.7 KiB
Lua
174 lines
8.7 KiB
Lua
--- allows you to get information about nodes (bones or meshes) within a b3d table (generated with `b3d_reader`)
|
|
--- located in `mtul.b3d_nodes`.
|
|
--- WARNING! mtul-cpml must be present for this module to run!
|
|
--@module b3d_nodes
|
|
--@warning for this module mtul_cpml is required, trying to use these functions without mtul_cpml ran will error.
|
|
|
|
--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 = {}
|
|
local mat4 = mtul.math.mat4
|
|
local quat = mtul.math.quat
|
|
|
|
--- get a node by it's name
|
|
-- @function get_node_by_name
|
|
-- @param self the b3d table (from b3d_reader)
|
|
-- @param node_name the name of the node to fine
|
|
-- @param is_bone (optional) bool to indicate wether the node is a bone or not (incase there's a mesh named the same thing). False will only return meshes and pivots, true will only return bones. Nil will return any.
|
|
-- @return node (from b3d table, documentation needed)
|
|
function b3d_nodes.get_node_by_name(self, node_name, is_bone)
|
|
for i, this_node in pairs(self.node_paths) do
|
|
if is_bone ~= nil then
|
|
if (this_node.name == node_name) and ( ((this_node.type == "bone") and is_bone) or (this_node.type ~= "bone" and not is_bone) ) then
|
|
return this_node
|
|
end
|
|
elseif (this_node.name == node_name) then
|
|
return this_node
|
|
end
|
|
end
|
|
--don't know why I'd ever just not return nil?
|
|
--error("MTUL-b3d, b3d_nodes: no node found by the name '"..tostring(node_name).."'")
|
|
end
|
|
|
|
--non-methods:
|
|
--keep in mind that this returns *raw* info, other then vectorizing quaternions (as slerp has to be performed to interpolate).
|
|
--further, quaternions need to have their w inverted.
|
|
|
|
--- get the local "TRS" (translation, rotation, scale) of a bone in animation. This is used for global transformation calculations.
|
|
--- quaternion is returned as a string indexed table because it needs to be a cpml object to be interpolated, also has to be usable anyway.
|
|
-- @function get_animated_local_trs
|
|
-- @param node table, the node from within a b3d table to read (as outputed by b3d_reader).
|
|
-- @param target_frame float, the frame to find the TRS in, can be inbetween frames/keyframes (of course).
|
|
-- @return `position` ordered table: {x, y, z}
|
|
-- @return `rotation` quat from `mtul_cpml`: (example) {w=0,x=0,y=0,z=1}
|
|
-- @return `scale` ordered table: {x, y, z}
|
|
--outputs need cleaning up.
|
|
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_trs(node, target_frame)
|
|
assert(target_frame, "no frame specified for TRS calculations")
|
|
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. Also allow it to pick itself if this is an option.
|
|
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.
|
|
--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),
|
|
quat.new(unpack(frame_before_tbl.rotation)):slerp(quat.new(unpack(frame_after_tbl.rotation)), ratio),
|
|
interpolate(frame_before_tbl.scale, frame_after_tbl.scale, ratio)
|
|
else
|
|
return
|
|
table.copy(frame_before_tbl.position),
|
|
quat.new(unpack(frame_before_tbl.rotation)),
|
|
table.copy(frame_before_tbl.scale)
|
|
end
|
|
end
|
|
--param 3 (outputs) is either "rotation" or "transform"- determines what's calculated. You can use this if you dont want uncessary calculations. If nil outputs both
|
|
|
|
--- get a node's global mat4 transform and rotation.
|
|
-- @function get_node_global_transform
|
|
-- @param node table, the node from within a b3d table to read (as outputed by `b3d_reader`).
|
|
-- @param frame float, the frame to find the transform and rotation in.
|
|
-- @param outputs (optional) string, either "rotation" or "transform". Set to nil to return both.
|
|
-- @return `global_transform`, a matrix 4x4, note that CPML's tranforms are column major (i.e. 1st column is 1, 2, 3, 4). (see `mtul_cpml` docs)
|
|
-- @return `rotation quat`, the quaternion rotation in global space. (cannot be assumed to be normalized, this uses raw interpolated data from the b3d reader)
|
|
function b3d_nodes.get_node_global_transform(node, frame, outputs)
|
|
local global_transform
|
|
local rotation
|
|
for i, current_node in pairs(node.path) do
|
|
local pos_vec, rot_vec, scl_vec = b3d_nodes.get_animated_local_trs(current_node, frame)
|
|
rot_vec.w = -rot_vec.w --b3d rotates the opposite way around the axis (I guess)
|
|
--find the transform
|
|
if not (outputs and outputs ~= "transform") then
|
|
--rot_vec = {rot_vec[2], rot_vec[3], rot_vec[4], rot_vec[1]}
|
|
local local_transform = mat4.identity()
|
|
local_transform = local_transform:translate(local_transform, pos_vec)
|
|
local_transform = local_transform*(mat4.from_quaternion(rot_vec:normalize())) --W has to be inverted
|
|
|
|
--for some reason the scaling has to be broken up, I can't be bothered to figure out why after the time I've spent trying.
|
|
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
|
|
|
|
--find the rotation
|
|
|
|
if not (outputs and outputs ~= "rotation") then
|
|
if not rotation then
|
|
rotation = rot_vec
|
|
else
|
|
rotation = rotation*rot_vec
|
|
end
|
|
end
|
|
end
|
|
return global_transform, rotation
|
|
end
|
|
|
|
--Returns X, Y, Z. is_bone is optional, if "node" is the name of a node (and not the node table), parameter 1 (self) and parameter 3 (is_bone) is used to find it.
|
|
|
|
--- find the position of a node in global model space.
|
|
--@function get_node_global_position
|
|
--@param self b3d table, (optional if node is a node table and not name)
|
|
--@param node string or table, either the node from b3d table or a the name of the node to find.
|
|
--@param is_bone (optional) if node is string, this is used to find it (see `get_node_by_name`)
|
|
--@param frame the frame to find the global position of the node at.
|
|
--@return `x`
|
|
--@return `y`
|
|
--@return `z`
|
|
function b3d_nodes.get_node_global_position(self, node, is_bone, frame)
|
|
assert(self or not type(node)=="string")
|
|
assert(node, "cannot get position of a nil node")
|
|
assert(frame, "no frame specified!")
|
|
if type(node) == "string" then
|
|
node = b3d_nodes.get_node_by_name(self, node, is_bone)
|
|
end
|
|
local transform = b3d_nodes.get_node_global_transform(node, frame, "transform")
|
|
return transform[13], transform[14], transform[15]
|
|
end
|
|
--- find the global rotation of a node in model space.
|
|
--@function get_node_rotation
|
|
--@param self b3d table, (optional if node is a node table and not name)
|
|
--@param node string or table, either the node from b3d table or a the name of the node to find.
|
|
--@param is_bone (optional) if node is string, this is used to find it (see `get_node_by_name`)
|
|
--@param frame the frame to find the global rotation of the node at.
|
|
--@return `rotation` quaternion rotation of the node (may not be normalized)
|
|
function b3d_nodes.get_node_rotation(self, node, is_bone, frame)
|
|
assert(self or not type(node)=="string")
|
|
assert(node, "cannot get rotation of a nil node")
|
|
assert(frame, "no frame specified!")
|
|
if type(node) == "string" then
|
|
node = b3d_nodes.get_node_by_name(self, node, is_bone)
|
|
end
|
|
local _, rotation = b3d_nodes.get_node_global_transform(node, frame, "rotation")
|
|
return rotation
|
|
end
|
|
return b3d_nodes |