portal64-still-alive/skelatool64/lua/sk_animation.lua

329 lines
9.8 KiB
Lua

--- @module sk_animation
local sk_scene = require('sk_scene')
local sk_input = require('sk_input')
local sk_math = require('sk_math')
local sk_transform = require('sk_transform')
local sk_definition_writer = require('sk_definition_writer')
local node_order = {}
local current_node_index = 1
sk_scene.for_each_node(sk_scene.scene.root, function(node)
node_order[node] = current_node_index
current_node_index = current_node_index + 1
end)
local function find_interpolated_point(key_list, time)
for index, key in pairs(key_list) do
if key.time >= time then
if index == 1 then
return key.value, key.value, 0
else
local prev_key = key_list[index - 1]
local delta_time = key.time - prev_key.time
if delta_time == 0.0 then
return key.value, key.value, 0
else
return prev_key.value, key.value, (time - prev_key.time) / delta_time
end
end
end
end
if #key_list > 0 then
return key_list[#key_list].value, key_list[#key_list].value, 1
end
return nil, nil, 0
end
local function evaluate_channel_at(channel, time)
local prev_pos, next_pos, pos_lerp = find_interpolated_point(channel.position_keys, time)
local prev_rot, next_rot, rot_lerp = find_interpolated_point(channel.rotation_keys, time)
local prev_scale, next_scale, scale_lerp = find_interpolated_point(channel.scaling_keys, time)
return (prev_pos and prev_pos:lerp(next_pos, pos_lerp) or sk_math.vector3(0, 0, 0)),
(prev_rot and prev_rot:slerp(next_rot, rot_lerp) or sk_math.quaternion(0, 0, 0, 1)),
(prev_scale and prev_scale:lerp(next_scale, scale_lerp) or sk_math.vector3(1, 1, 1))
end
local function evaluate_animation_at(node_pose, animation, time)
for _, channel in pairs(animation.channels) do
local node = sk_scene.node_with_name(channel.node_name)
if node then
local pos, rot, scale = evaluate_channel_at(channel, time)
local local_transform = sk_transform.from_pos_rot_scale(pos, rot, scale)
if node.parent then
node_pose[node] = local_transform
else
node_pose[node] = sk_input.settings.fixed_point_transform * local_transform
end
end
end
end
local Armature = {}
--- @function build_armature
--- @tparam {sk_scene.Node,...} animation_nodes
--- @treturn Armature
local function build_armature(animation_nodes)
local nodes = {table.unpack(animation_nodes)}
table.sort(nodes, function(a, b)
return (node_order[a] or 0) < (node_order[b] or 0)
end)
local node_to_index = {}
for index, node in pairs(nodes) do
node_to_index[node] = index
end
return setmetatable({
nodes = nodes,
node_to_index = node_to_index,
}, Armature)
end
local function add_to_node_pose(node_pose, node)
if node_pose[node] then
return
end
if node.parent then
node_pose[node] = node.transformation
add_to_node_pose(node_pose, node.parent)
else
node_pose[node] = node.full_transformation
end
end
local function build_node_pose(armature, node, node_pose)
local result = nil
while node do
result = result and (node_pose[node] * result) or node_pose[node]
node = node.parent
-- only build the transform relative to the ancestor in the chain
if node and armature:has_node(node) then
return result
end
end
return result
end
local function build_quat_pose(quat)
if quat.w < 0 then
return {
math.floor((-quat.x * 32767) + 0.5),
math.floor((-quat.y * 32767) + 0.5),
math.floor((-quat.z * 32767) + 0.5)
}
else
return {
math.floor((quat.x * 32767) + 0.5),
math.floor((quat.y * 32767) + 0.5),
math.floor((quat.z * 32767) + 0.5)
}
end
end
local function build_armature_pose(armature, node_pose, result)
for _, node in pairs(armature.nodes) do
local pose = build_node_pose(armature, node, node_pose)
local pos, rot = pose:decompose()
pos = pos * sk_input.settings.fixed_point_scale
table.insert(result, {
sk_math.vector3(math.floor(pos.x + 0.5), math.floor(pos.y + 0.5), math.floor(pos.z + 0.5)),
build_quat_pose(rot)
})
end
end
local function build_animation(armature, animation)
-- Don't stop at the last frame, include it
local ticks_to_include = animation.duration + 1
local n_frames = math.ceil(ticks_to_include * sk_input.settings.ticks_per_second / animation.ticks_per_second)
local node_pose = {}
for _, node in pairs(armature.nodes) do
add_to_node_pose(node_pose, node)
end
local frames = {}
for frame_index = 1,n_frames do
local time = (frame_index - 1) * animation.ticks_per_second / sk_input.settings.ticks_per_second
-- populate node_pose from animation
evaluate_animation_at(node_pose, animation, time)
-- generate frame for armature
build_armature_pose(armature, node_pose, frames)
end
return frames, n_frames
end
--- @table Clip
--- @tfield number nFrames
--- @tfield number nBones
--- @tfield sk_definition_writer.RefType frames
--- @tfield number fps
--- @function build_animation_clip
--- @tparam sk_scene.Animation animation
--- @tparam Armature armature
--- @tparam string animation_file_suffix
--- @treturn Clip the exported clip object use sk_definition_writer.reference_to(clip) to reference in other data
local function build_animation_clip(animation, armature, animation_file_suffix)
local animation_frames, n_frames = build_animation(armature, animation)
sk_definition_writer.add_definition(animation.name .. '_frames', 'struct SKAnimationBoneFrame[]', animation_file_suffix, animation_frames)
local clip = {
nFrames = n_frames,
nBones = #armature.nodes,
frames = sk_definition_writer.reference_to(animation_frames, 1),
fps = sk_input.settings.ticks_per_second
}
return clip
end
local function build_armature_for_animations(animations)
local nodes_at_set = {}
for _, animation in pairs(sk_scene.scene.animations) do
for _, channel in pairs(animation.channels) do
nodes_at_set[sk_scene.node_with_name(channel.node_name)] = true
end
end
local all_nodes = {}
for node, _ in pairs(nodes_at_set) do
table.insert(all_nodes, node)
end
return build_armature(all_nodes)
end
--- @function filter_animations_for_armature
--- @tparam Armature armature
--- @tparam {sk_scene.Animation,...} animations
--- @treturn {sk_scene.Animation,...}
local function filter_animations_for_armature(armature, animations)
local result = {}
for _, animation in pairs(animations) do
for _, channel in pairs(animation.channels) do
if armature:has_node(sk_scene.node_with_name(channel.node_name)) then
table.insert(result, animation)
break
end
end
end
return result
end
--- @function build_armature_data
--- @tparam Armature armature
--- @tparam sk_definition_writer.RefType|nil gfx_reference_or_nil
--- @tparam string name_hint
--- @tparam string file_suffix
--- @treturn table
local function build_armature_data(armature, gfx_reference_or_nil, name_hint, file_suffix)
local node_pose = {}
local transforms = {}
local parent_mapping = {}
for _, node in pairs(armature.nodes) do
add_to_node_pose(node_pose, node)
-- calculate base pose
local relative_transform = build_node_pose(armature, node, node_pose)
local pos, rot, scale = relative_transform:decompose()
table.insert(transforms, {pos * sk_input.settings.fixed_point_scale, rot, scale})
-- calculate parent mapping
local _, parent_index = armature:get_parent_bone(node)
if parent_index then
table.insert(parent_mapping, parent_index - 1)
else
table.insert(parent_mapping, sk_definition_writer.raw('NO_BONE_PARENT'))
end
end
sk_definition_writer.add_definition(name_hint .. '_base_transform', 'struct Transform[]', file_suffix, transforms)
sk_definition_writer.add_definition(name_hint .. '_parent_mapping', 'unsigned short[]', file_suffix, parent_mapping)
return {
displayList = gfx_reference_or_nil or sk_definition_writer.null_value,
pose = sk_definition_writer.reference_to(transforms, 1),
numberOfBones = #armature.nodes,
numberOfAttachments = 0,
boneParentIndex = sk_definition_writer.reference_to(parent_mapping, 1),
}
end
--- @type Armature
--- @tfield {sk_scene.Node,...} nodes
Armature.__index = Armature;
--- @function has_node
--- @tparam sk_scene.Node Node
--- @treturn boolean
Armature.has_node = function(armature, node)
return armature.node_to_index[node] or false
end
--- @function get_parent_bone
--- @tparam sk_scene.Node Node
--- @treturn sk_scene.Node|nil
--- @treturn number bone_index|nil
Armature.get_parent_bone = function(armature, node)
local curr = node
while curr do
-- check if current parent is in the armature
local parent_index = armature.node_to_index[node.parent]
if parent_index then
-- return the parent bone if in armature
return armature.nodes[parent_index], parent_index
end
curr = curr.parent
end
-- no parent bone in armature so return nil
return nil, nil
end
return {
build_armature = build_armature,
build_armature_for_animations = build_armature_for_animations,
build_armature_data = build_armature_data,
filter_animations_for_armature = filter_animations_for_armature,
build_animation_clip = build_animation_clip,
Armature = Armature,
}