update char anim

master
Elkien3 2022-05-13 22:29:41 -05:00
parent 3f496cd76c
commit 504a3405da
18 changed files with 432 additions and 3437 deletions

View File

@ -0,0 +1,7 @@
globals = {"character_anim"}
read_globals = {
"modlib",
-- Minetest
math = {fields = {"sign"}},
"vector", "minetest"
}

View File

@ -4,7 +4,7 @@ Animates the character. Resembles [`playeranim`](https://github.com/minetest-mod
## About
Depends on [`modlib`](https://github.com/appgurueu/modlib). Code written by Lars Mueller aka LMD or appguru(eu) and licensed under the MIT license. Media (player model) was created by [MTG contributors](https://github.com/minetest/minetest_game/blob/master/mods/player_api/README.txt) (MirceaKitsune, stujones11 and An0n3m0us) and is licensed under the CC BY-SA 3.0 license, as must be its derivatives (`skinsdb` and `3d_armor` variants).
Depends on [`modlib`](https://github.com/appgurueu/modlib). Code written by Lars Mueller aka LMD or appguru(eu) and licensed under the MIT license.
## Screenshot
@ -20,6 +20,7 @@ Depends on [`modlib`](https://github.com/appgurueu/modlib). Code written by Lars
## Features
* Animates head, right arm & body
* Also provides support for arbitrary player models, as long as `Head`, `Arm_Right` & `Body` bones exist
* Advantages over `playeranim`:
* Extracts exact animations and bone positions from glTF models
* Also animates attached players (with restrictions on angles)
@ -28,16 +29,6 @@ Depends on [`modlib`](https://github.com/appgurueu/modlib). Code written by Lars
* Head angles are clamped, head can tilt sideways
* Animates right arm & body as well
## Instructions
0. If you want to use a custom model, install [`binarystream`](https://luarocks.org/modules/Tarik02/binarystream) from LuaRocks:
1. `sudo luarocks install binarystream` on many UNIX-systems
2. `sudo luarocks install luabitop` if you're not using LuaJIT
3. Disable mod security. **Make sure you trust all your mods! Ideally import models with all other mods disabled.**
4. Export the model as `glTF` and save it under `models/modelname.extension.gltf`
5. Do `/ca_import modelname.extension`
1. Install and use `character_anim` like any other mod
## Configuration
<!--modlib:conf:2-->
@ -51,8 +42,8 @@ Right arm spin radius
* Type: number
* Default: `10`
* &gt;= -180
* &lt;= 180
* &gt;= `-180`
* &lt;= `180`
##### `speed`
@ -60,8 +51,8 @@ Right arm spin speed
* Type: number
* Default: `1000`
* &gt; 0
* &lt;= 10000
* &gt; `0`
* &lt;= `10000`
##### `yaw`
@ -71,8 +62,8 @@ Right arm yaw (max)
* Type: number
* Default: `160`
* &gt;= -180
* &lt;= 180
* &gt;= `-180`
* &lt;= `180`
###### `min`
@ -80,8 +71,8 @@ Right arm yaw (min)
* Type: number
* Default: `-30`
* &gt;= -180
* &lt;= 180
* &gt;= `-180`
* &lt;= `180`
@ -93,8 +84,8 @@ Body turn speed
* Type: number
* Default: `0.2`
* &gt; 0
* &lt;= 1000
* &gt; `0`
* &lt;= `1000`
#### `head`
@ -107,8 +98,8 @@ Head pitch (max)
* Type: number
* Default: `80`
* &gt;= -180
* &lt;= 180
* &gt;= `-180`
* &lt;= `180`
###### `min`
@ -116,8 +107,8 @@ Head pitch (min)
* Type: number
* Default: `-60`
* &gt;= -180
* &lt;= 180
* &gt;= `-180`
* &lt;= `180`
##### `yaw`
@ -128,8 +119,8 @@ Head yaw (max)
* Type: number
* Default: `90`
* &gt;= -180
* &lt;= 180
* &gt;= `-180`
* &lt;= `180`
###### `min`
@ -137,8 +128,8 @@ Head yaw (min)
* Type: number
* Default: `-90`
* &gt;= -180
* &lt;= 180
* &gt;= `-180`
* &lt;= `180`
##### `yaw_restricted`
@ -149,8 +140,8 @@ Head yaw restricted (max)
* Type: number
* Default: `45`
* &gt;= -180
* &lt;= 180
* &gt;= `-180`
* &lt;= `180`
###### `min`
@ -158,8 +149,8 @@ Head yaw restricted (min)
* Type: number
* Default: `0`
* &gt;= -180
* &lt;= 180
* &gt;= `-180`
* &lt;= `180`
##### `yaw_restriction`
@ -168,8 +159,8 @@ Head yaw restriction
* Type: number
* Default: `60`
* &gt;= -180
* &lt;= 180
* &gt;= `-180`
* &lt;= `180`

View File

@ -1,167 +0,0 @@
local BinaryStream
local previous_require = require
rawset(_G, "require", insecure_environment.require)
pcall(function()
-- Lua 5.1 compatibility
rawset(_G, "bit", rawget(_G, "bit") or require"bit")
BinaryStream = require"binarystream"
end)
rawset(_G, "require", previous_require)
local io = insecure_environment.io
insecure_environment = nil
if not BinaryStream then return end
local data_uri_start = "data:application/octet-stream;base64,"
function read_bonedata(path)
local gltf = minetest.parse_json(modlib.file.read(path))
local buffers = {}
for index, buffer in ipairs(gltf.buffers) do
buffer = buffer.uri
assert(modlib.text.starts_with(buffer, data_uri_start))
-- Trim padding characters, see https://github.com/minetest/minetest/commit/f34abaedd2b9277c1862cd9b82ca3338747f104e
buffers[index] = assert(minetest.decode_base64(modlib.text.trim_right(buffer:sub((data_uri_start):len()+1), "=")) or nil, "base64 decoding failed, upgrade to Minetest 5.4 or newer")
end
local accessors = gltf.accessors
local function read_accessor(accessor)
local buffer_view = gltf.bufferViews[accessor.bufferView + 1]
local buffer = assert(buffers[buffer_view.buffer + 1])
local binary_stream = BinaryStream(buffer, buffer:len())
-- See https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations
local component_readers = {
[5120] = function()
return math.max(binary_stream:readS8() / 127, -1)
end,
[5121] = function()
return binary_stream:readU8() / 255
end,
[5122] = function()
return math.max(binary_stream:readS16() / 32767, -1)
end,
[5123] = function()
return binary_stream:readU16() / 65535
end,
[5126] = function()
return binary_stream:readF32()
end
}
local accessor_type = accessor.type
local component_reader = component_readers[accessor.componentType]
binary_stream:skip(buffer_view.byteOffset)
local values = {}
for index = 1, accessor.count do
if accessor_type == "SCALAR" then
values[index] = component_reader()
elseif accessor_type == "VEC3" then
values[index] = {
x = component_reader(),
y = component_reader(),
z = component_reader()
}
elseif accessor_type == "VEC4" then
values[index] = {
component_reader(),
component_reader(),
component_reader(),
component_reader()
}
end
end
return values
end
local nodes = gltf.nodes
local animation = gltf.animations[1]
local channels, samplers = animation.channels, animation.samplers
local animations_by_nodename = {}
for _, node in pairs(nodes) do
animations_by_nodename[node.name] = {
default_translation = node.translation,
default_rotation = node.rotation
}
end
for _, channel in ipairs(channels) do
local path, node_index, sampler = channel.target.path, channel.target.node, samplers[channel.sampler + 1]
assert(sampler.interpolation == "LINEAR")
if path == "translation" or path == "rotation" then
local time_accessor = accessors[sampler.input + 1]
local time, transform = read_accessor(time_accessor), read_accessor(accessors[sampler.output + 1])
local min_time, max_time = time_accessor.min and time_accessor.min[1] or modlib.table.min(time), time_accessor.max and time_accessor.max[1] or modlib.table.max(time)
local nodename = nodes[node_index + 1].name
assert(not animations_by_nodename[nodename][path])
animations_by_nodename[nodename][path] = {
start_time = min_time,
end_time = max_time,
keyframes = time,
values = transform
}
end
end
-- HACK to remove unanimated bones (technically invalid, but only proper way to remove Armature / Player / Camera / Suns)
for bone, animation in pairs(animations_by_nodename) do
if not(animation.translation or animation.rotation) then
animations_by_nodename[bone] = nil
end
end
local is_root, is_child = {}, {}
for index, node in pairs(nodes) do
if animations_by_nodename[node.name] then
local children = node.children
if children and #children > 0 then
is_root[index] = node
for _, child_index in pairs(children) do
child_index = child_index + 1
assert(not is_child[child_index])
is_child[child_index] = true
is_root[child_index] = nil
end
end
end
end
local order = {}
local insert = modlib.func.curry(table.insert, order)
for node_index in pairs(is_root) do
local node = nodes[node_index]
insert(node.name)
local function insert_children(parent, children)
for _, child_index in ipairs(children) do
local child = nodes[child_index + 1]
local name = child.name
animations_by_nodename[name].parent = parent
insert(name)
if child.children then
insert_children(name, child.children)
end
end
end
insert_children(node.name, node.children)
end
for index, node in ipairs(nodes) do
if animations_by_nodename[node.name] and not(is_root[index] or is_child[index]) then
insert(node.name)
end
end
return {order = order, animations_by_nodename = animations_by_nodename}
end
local basepath = modlib.mod.get_resource""
function import_model(filename)
local path = basepath .. "models/".. filename .. ".gltf"
if not modlib.file.exists(path) then
return false
end
modeldata[filename] = read_bonedata(path)
local file = io.open(basepath .. "modeldata.lua", "w")
file:write(minetest.serialize(modeldata))
file:close()
return true
end
minetest.register_chatcommand("ca_import", {
params = "<filename>",
description = "Imports a model for use with character_anim",
privs = {server = true},
func = function(_, filename)
local success = import_model(filename)
return success, (success and "Model %s imported successfully" or "File %s does not exist"):format(filename)
end
})

View File

@ -1,7 +1,313 @@
local mod = modlib.mod
local namespace = mod.create_namespace()
namespace.quaternion = modlib.quaternion
namespace.conf = mod.configuration()
namespace.insecure_environment = minetest.request_insecure_environment() or _G
mod.extend"importer"
mod.extend"main"
assert(modlib.version >= 93, "character_anim requires at least version rolling-93 of modlib")
character_anim = {}
character_anim.conf = modlib.mod.configuration()
local quaternion = modlib.quaternion
-- TODO deduplicate code: move to modlib (see ghosts mod)
local media_paths = modlib.minetest.media.paths
local models = setmetatable({}, {__index = function(self, filename)
local _, ext = modlib.file.get_extension(filename)
if not ext or ext:lower() ~= "b3d" then
-- Only B3D support currently
return
end
local path = assert(media_paths[filename], filename)
local stream = io.open(path, "rb")
local model = assert(modlib.b3d.read(stream))
assert(stream:read(1) == nil, "EOF expected")
stream:close()
self[filename] = model
return model
end})
function character_anim.is_interacting(player)
local control = player:get_player_control()
return minetest.check_player_privs(player, "interact") and (control.RMB or control.LMB)
end
local function get_look_horizontal(player)
return -math.deg(player:get_look_horizontal())
end
local players = {}
character_anim.players = players
local function get_playerdata(player)
local name = player:get_player_name()
local data = players[name]
if data then return data end
-- Initialize playerdata if it doesn't already exist
data = {
interaction_time = 0,
animation_time = 0,
look_horizontal = get_look_horizontal(player),
bone_positions = {}
}
players[name] = data
return data
end
function character_anim.set_bone_override(player, bonename, position, rotation)
local value = {
position = position,
euler_rotation = rotation
}
get_playerdata(player).bone_positions[bonename] = next(value) and value
end
local function nil_default(value, default)
if value == nil then return default end
return value
end
-- Forward declaration
local handle_player_animations
-- Raw PlayerRef methods
local set_bone_position, set_animation, set_local_animation
minetest.register_on_joinplayer(function(player)
get_playerdata(player) -- Initalizes playerdata if it isn't already initialized
if not set_bone_position then
local PlayerRef = getmetatable(player)
set_bone_position = PlayerRef.set_bone_position
function PlayerRef:set_bone_position(bonename, position, rotation)
if self:is_player() then
character_anim.set_bone_override(self, bonename or "",
position or {x = 0, y = 0, z = 0},
rotation or {x = 0, y = 0, z = 0})
end
return set_bone_position(self, bonename, position, rotation)
end
set_animation = PlayerRef.set_animation
function PlayerRef:set_animation(frame_range, frame_speed, frame_blend, frame_loop)
if not self:is_player() then
return set_animation(self, frame_range, frame_speed, frame_blend, frame_loop)
end
local player_animation = get_playerdata(self)
if not player_animation then
return
end
local prev_anim = player_animation.animation
local new_anim = {
nil_default(frame_range, {x = 1, y = 1}),
nil_default(frame_speed, 15),
nil_default(frame_blend, 0),
nil_default(frame_loop, true)
}
player_animation.animation = new_anim
if not prev_anim or (prev_anim[1].x ~= new_anim[1].x or prev_anim[1].y ~= new_anim[1].y) then
-- Reset animation only if the animation changed
player_animation.animation_time = 0
handle_player_animations(0, player)
elseif prev_anim[2] ~= new_anim[2] then
-- Adapt time to new speed
player_animation.animation_time = player_animation.animation_time * prev_anim[2] / new_anim[2]
end
end
local set_animation_frame_speed = PlayerRef.set_animation_frame_speed
function PlayerRef:set_animation_frame_speed(frame_speed)
if not self:is_player() then
return set_animation_frame_speed(self, frame_speed)
end
frame_speed = nil_default(frame_speed, 15)
local player_animation = get_playerdata(self)
if not player_animation then
return
end
local prev_speed = player_animation.animation[2]
player_animation.animation[2] = frame_speed
-- Adapt time to new speed
player_animation.animation_time = player_animation.animation_time * prev_speed / frame_speed
end
local get_animation = PlayerRef.get_animation
function PlayerRef:get_animation()
if not self:is_player() then
return get_animation(self)
end
local anim = get_playerdata(self).animation
if anim then
return unpack(anim, 1, 4)
end
return get_animation(self)
end
set_local_animation = PlayerRef.set_local_animation
function PlayerRef:set_local_animation(idle, walk, dig, walk_while_dig, frame_speed)
if not self:is_player() then return set_local_animation(self) end
frame_speed = frame_speed or 30
get_playerdata(self).local_animation = {idle, walk, dig, walk_while_dig, frame_speed}
end
local get_local_animation = PlayerRef.get_local_animation
function PlayerRef:get_local_animation()
if not self:is_player() then return get_local_animation(self) end
local local_anim = get_playerdata(self).local_animation
if local_anim then
return unpack(local_anim, 1, 5)
end
return get_local_animation(self)
end
end
-- Disable animation & local animation
local no_anim = {x = 0, y = 0}
set_animation(player, no_anim, 0, 0, false)
set_local_animation(player, no_anim, no_anim, no_anim, no_anim, 1)
end)
minetest.register_on_leaveplayer(function(player) players[player:get_player_name()] = nil end)
local function clamp(value, range)
if value > range.max then
return range.max
end
if value < range.min then
return range.min
end
return value
end
local function normalize_angle(angle)
return ((angle + 180) % 360) - 180
end
local function normalize_rotation(euler_rotation)
return vector.apply(euler_rotation, normalize_angle)
end
function handle_player_animations(dtime, player)
local mesh = player:get_properties().mesh
local model = models[mesh]
if not model then
return
end
local conf = character_anim.conf.models[mesh] or character_anim.conf.default
local player_animation = get_playerdata(player)
local anim = player_animation.animation
if not anim then
return
end
local range, frame_speed, _, frame_loop = unpack(anim, 1, 4)
local animation_time = player_animation.animation_time
animation_time = animation_time + dtime
player_animation.animation_time = animation_time
local range_min, range_max = range.x + 1, range.y + 1
local keyframe
if range_min == range_max then
keyframe = range_min
elseif frame_loop then
keyframe = range_min + ((animation_time * frame_speed) % (range_max - range_min))
else
keyframe = math.min(range_max, range_min + animation_time * frame_speed)
end
local bones = {}
for _, props in ipairs(model:get_animated_bone_properties(keyframe, true)) do
local bone = props.bone_name
local position, rotation = modlib.vector.to_minetest(props.position), props.rotation
-- Invert quaternion to match Minetest's coordinate system
rotation = {-rotation[1], -rotation[2], -rotation[3], rotation[4]}
local euler_rotation = quaternion.to_euler_rotation(rotation)
bones[bone] = {position = position, rotation = rotation, euler_rotation = euler_rotation}
end
assert(bones.Body and bones.Head and bones.Arm_Right, "Player model is missing Body, Head or Arm_Right bones")
local Body, Head, Arm_Right = bones.Body.euler_rotation, bones.Head.euler_rotation, bones.Arm_Right.euler_rotation
local look_vertical = -math.deg(player:get_look_vertical())
Head.x = look_vertical
local interacting = character_anim.is_interacting(player)
if interacting then
local interaction_time = player_animation.interaction_time
-- Note: +90 instead of +Arm_Right.x because it looks better
Arm_Right.x = 90 + look_vertical - math.sin(-interaction_time) * conf.arm_right.radius
Arm_Right.y = Arm_Right.y + math.cos(-interaction_time) * conf.arm_right.radius
player_animation.interaction_time = interaction_time + dtime * math.rad(conf.arm_right.speed)
else
player_animation.interaction_time = 0
end
local look_horizontal = get_look_horizontal(player)
local diff = look_horizontal - player_animation.look_horizontal
if math.abs(diff) > 180 then
diff = math.sign(-diff) * 360 + diff
end
local moving_diff = math.sign(diff) * math.abs(diff) * math.min(1, dtime / conf.body.turn_speed)
player_animation.look_horizontal = player_animation.look_horizontal + moving_diff
if math.abs(moving_diff) < 1e-6 then
player_animation.look_horizontal = look_horizontal
end
local lag_behind = diff - moving_diff
local attach_parent, _, _, attach_rotation = player:get_attach()
if attach_parent then
local parent_rotation = attach_parent:get_rotation()
if attach_rotation and parent_rotation then
parent_rotation = vector.apply(parent_rotation, math.deg)
local total_rotation = normalize_rotation(vector.subtract(attach_rotation, parent_rotation))
local function rotate_relative(euler_rotation)
euler_rotation.y = euler_rotation.y + look_horizontal
local new_rotation = normalize_rotation(vector.subtract(euler_rotation, total_rotation))
modlib.table.add_all(euler_rotation, new_rotation)
end
rotate_relative(Head)
if interacting then rotate_relative(Arm_Right) end
end
elseif not modlib.table.nilget(rawget(_G, "player_api"), "player_attached", player:get_player_name()) then
Body.y = Body.y - lag_behind
Head.y = Head.y + lag_behind
if interacting then Arm_Right.y = Arm_Right.y + lag_behind end
end
if spriteguns and spriteguns.is_wielding_gun(player:get_player_name()) then
local tempvertlook = math.rad(look_vertical)
local Rightval = vector.multiply(vector.dir_to_rotation(vector.rotate({x=0,y=0,z=1}, {x=tempvertlook,y=0,z=0})), 180/math.pi)
Rightval.x = Rightval.x + 85
bones.Arm_Right.euler_rotation = Rightval
bones.Arm_Right.position.x = bones.Arm_Right.position.x + .9
local Leftval = vector.multiply(vector.dir_to_rotation(vector.rotate({x=-.8,y=0,z=1}, {x=tempvertlook,y=0,z=0})), 180/math.pi)
Leftval.x = Leftval.x + 85
bones.Arm_Left.euler_rotation = Leftval
bones.Arm_Left.position.x = bones.Arm_Left.position.x - .9
if attach_parent then
clamphead = false
local parent_rotation = attach_parent:get_rotation()
local total_rotation = normalize_rotation(vector.add(attach_rotation, vector.apply(parent_rotation, math.deg)))
local function rotate_relative(euler_rotation)
-- HACK +180
euler_rotation.y = euler_rotation.y + look_horizontal-- + 180
local new_rotation = normalize_rotation(vector.add(euler_rotation, total_rotation))
euler_rotation.x, euler_rotation.y, euler_rotation.z = new_rotation.x, new_rotation.y, new_rotation.z
end
rotate_relative(bones.Body.euler_rotation)
bones.Head.euler_rotation = vector.subtract(bones.Head.euler_rotation, bones.Body.euler_rotation)
bones.Leg_Left.euler_rotation = vector.subtract(bones.Leg_Left.euler_rotation, bones.Body.euler_rotation)
bones.Leg_Right.euler_rotation = vector.subtract(bones.Leg_Right.euler_rotation, bones.Body.euler_rotation)
end
else
-- HACK assumes that Body is root & parent bone of Head, only takes rotation around X-axis into consideration
Head.x = normalize_angle(Head.x + Body.x)
if interacting then Arm_Right.x = normalize_angle(Arm_Right.x - Body.x) end
Head.x = clamp(Head.x, conf.head.pitch)
Head.y = clamp(Head.y, conf.head.yaw)
if math.abs(Head.y) > conf.head.yaw_restriction then
Head.x = clamp(Head.x, conf.head.yaw_restricted)
end
Arm_Right.y = clamp(Arm_Right.y, conf.arm_right.yaw)
end
-- Replace animation with serverside bone animation
for bone, values in pairs(bones) do
local overridden_values = player_animation.bone_positions[bone]
overridden_values = overridden_values or {}
set_bone_position(player, bone,
overridden_values.position or values.position,
overridden_values.euler_rotation or values.euler_rotation)
end
end
minetest.register_globalstep(function(dtime)
for _, player in pairs(minetest.get_connected_players()) do
handle_player_animations(dtime, player)
end
end)

View File

@ -1,261 +0,0 @@
modeldata = minetest.deserialize(modlib.file.read(modlib.mod.get_resource"modeldata.lua"))
function get_animation_value(animation, keyframe_index, is_rotation)
local values = animation.values
assert(keyframe_index >= 1 and keyframe_index <= #values, keyframe_index)
local ratio = keyframe_index % 1
if ratio == 0 then
return values[keyframe_index]
end
assert(ratio > 0 and ratio < 1)
local prev_value, next_value = values[math.floor(keyframe_index)], values[math.ceil(keyframe_index)]
assert(next_value)
if is_rotation then
return quaternion.slerp(prev_value, next_value, ratio)
end
return modlib.vector.interpolate(prev_value, next_value, ratio)
end
function is_interacting(player)
local control = player:get_player_control()
return minetest.check_player_privs(player, "interact") and (control.RMB or control.LMB)
end
local function disable_local_animation(player)
return player:set_local_animation(nil, nil, nil, nil, 0)
end
local function get_look_horizontal(player)
return 180-math.deg(player:get_look_horizontal())
end
players = {}
function set_bone_override(player, bonename, position, rotation)
local name = player:get_player_name()
local value = {
position = position,
euler_rotation = rotation
}
-- TODO consider setting empty overrides to nil
players[name].bone_positions[bonename] = value
end
-- Raw PlayerRef.set_bone_position
local set_bone_position
minetest.register_on_joinplayer(function(player)
local name = player:get_player_name()
disable_local_animation(player)
players[name] = {
interaction_time = 0,
animation_time = 0,
animation = {},
look_horizontal = get_look_horizontal(player),
bone_positions = {}
}
if not set_bone_position then
local PlayerRef = getmetatable(player)
set_bone_position = PlayerRef.set_bone_position
function PlayerRef:set_bone_position(bonename, position, rotation)
if self:is_player() then
set_bone_override(self, bonename or "", position or {x = 0, y = 0, z = 0}, rotation or {x = 0, y = 0, z = 0})
end
return set_bone_position(self, bonename, position, rotation)
end
end
end)
minetest.register_on_leaveplayer(function(player) players[player:get_player_name()] = nil end)
local function disable_animation(player)
return player:set_animation({x = 0, y = 0}, 0, 0, false)
end
local function clamp(value, range)
if value > range.max then
return range.max
end
if value < range.min then
return range.min
end
return value
end
local function normalize_angle(angle)
return ((angle + 180) % 360) - 180
end
local function normalize_rotation(euler_rotation)
return vector.apply(euler_rotation, normalize_angle)
end
local function handle_player_animations(dtime, player)
local mesh = player:get_properties().mesh
local modeldata = modeldata[mesh]
if not modeldata then
return
end
local conf = conf.models[mesh] or conf.default
local name = player:get_player_name()
local range, frame_speed, frame_blend, frame_loop = player:get_animation()
disable_animation(player)
local player_animation = players[name]
local anim = {range, frame_speed, frame_blend, frame_loop}
local animation_time = player_animation.animation_time
if (range.x == 0 and range.y == 0 and frame_speed == 0 and frame_blend == 0 and frame_loop == false) or modlib.table.equals_noncircular(anim, player_animation.animation) then
range, frame_speed, frame_blend, frame_loop = unpack(player_animation.animation)
animation_time = animation_time + dtime
else
player_animation.animation = anim
animation_time = 0
end
player_animation.animation_time = animation_time
local range_min, range_max = range.x + 1, range.y + 1
local keyframe
if range_min == range_max then
keyframe = range_min
elseif frame_loop then
keyframe = range_min + ((animation_time * frame_speed) % (range_max - range_min))
else
keyframe = math.min(range_max, range_min + animation_time * frame_speed)
end
local bone_positions = {}
for _, bone in ipairs(modeldata.order) do
local animation = modeldata.animations_by_nodename[bone]
local position, rotation = animation.default_translation, animation.default_rotation
if animation.translation then
position = get_animation_value(animation.translation, keyframe)
end
position = {x = -position.x, y = position.y, z = -position.z}
if animation.rotation then
-- rotation override instead of additional rotation (quaternion.multiply(animated_rotation, rotation))
rotation = get_animation_value(animation.rotation, keyframe, true)
end
rotation = {unpack(rotation)}
rotation[1] = -rotation[1]
local euler_rotation
local parent = animation.parent
if parent then
rotation[4] = -rotation[4]
local values = bone_positions[parent]
local absolute_rotation = quaternion.multiply(values.rotation, rotation)
euler_rotation = vector.subtract(quaternion.to_euler_rotation(absolute_rotation), values.euler_rotation)
else
euler_rotation = quaternion.to_euler_rotation(rotation)
end
bone_positions[bone] = {position = position, rotation = rotation, euler_rotation = euler_rotation}
end
local Body, Head, Arm_Right = bone_positions.Body.euler_rotation, bone_positions.Head.euler_rotation, bone_positions.Arm_Right.euler_rotation
local look_vertical = -math.deg(player:get_look_vertical())
Head.x = look_vertical
local interacting = is_interacting(player)
if interacting then
local interaction_time = player_animation.interaction_time
-- note: +90 instead +Arm_Right.x because it looks better
Arm_Right.x = 90 + look_vertical - math.sin(-interaction_time) * conf.arm_right.radius
Arm_Right.y = Arm_Right.y + math.cos(-interaction_time) * conf.arm_right.radius
player_animation.interaction_time = interaction_time + dtime * math.rad(conf.arm_right.speed)
else
player_animation.interaction_time = 0
end
local look_horizontal = get_look_horizontal(player)
local diff = look_horizontal - player_animation.look_horizontal
if math.abs(diff) > 180 then
diff = math.sign(-diff) * 360 + diff
end
local moving_diff = math.sign(diff) * math.abs(diff) * math.min(1, dtime / conf.body.turn_speed)
player_animation.look_horizontal = player_animation.look_horizontal + moving_diff
if math.abs(moving_diff) < 1e-6 then
player_animation.look_horizontal = look_horizontal
end
local lag_behind = diff - moving_diff
local attach_parent, _, _, attach_rotation = player:get_attach()
-- TODO properly handle eye offset & height vs. actual head position
if attach_parent then
local parent_rotation = attach_parent:get_rotation()
if attach_rotation and parent_rotation then
parent_rotation = vector.apply(parent_rotation, math.deg)
local total_rotation = normalize_rotation(vector.subtract(parent_rotation, attach_rotation))
local function rotate_relative(euler_rotation)
-- HACK +180
euler_rotation.y = euler_rotation.y + look_horizontal + 180
local new_rotation = normalize_rotation(vector.add(euler_rotation, total_rotation))
modlib.table.add_all(euler_rotation, new_rotation)
end
rotate_relative(Head)
if interacting then rotate_relative(Arm_Right) end
end
elseif not player_api.player_attached[name] then
Body.y = Body.y - lag_behind
Head.y = Head.y + lag_behind
if interacting then Arm_Right.y = Arm_Right.y + lag_behind end
end
-- HACK assumes that Body is root & parent bone of Head, only takes rotation around X-axis into consideration
Head.x = normalize_angle(Head.x + Body.x)
if interacting then Arm_Right.x = normalize_angle(Arm_Right.x + Body.x) end
local clamphead = true
if spriteguns and spriteguns.is_wielding_gun(name) then
local tempvertlook = math.rad(look_vertical)
local Rightval = vector.multiply(vector.dir_to_rotation(vector.rotate({x=0,y=0,z=1}, {x=tempvertlook,y=0,z=0})), 180/math.pi)
Rightval.x = Rightval.x + 85
bone_positions.Arm_Right.euler_rotation = Rightval
bone_positions.Arm_Right.position.x = bone_positions.Arm_Right.position.x + .9
local Leftval = vector.multiply(vector.dir_to_rotation(vector.rotate({x=-.8,y=0,z=1}, {x=tempvertlook,y=0,z=0})), 180/math.pi)
Leftval.x = Leftval.x + 85
bone_positions.Arm_Left.euler_rotation = Leftval
bone_positions.Arm_Left.position.x = bone_positions.Arm_Left.position.x - .9
if attach_parent then
clamphead = false
local parent_rotation = attach_parent:get_rotation()
local total_rotation = normalize_rotation(vector.add(attach_rotation, vector.apply(parent_rotation, math.deg)))
local function rotate_relative(euler_rotation)
-- HACK +180
euler_rotation.y = euler_rotation.y + look_horizontal + 180
local new_rotation = normalize_rotation(vector.add(euler_rotation, total_rotation))
euler_rotation.x, euler_rotation.y, euler_rotation.z = new_rotation.x, new_rotation.y, new_rotation.z
end
rotate_relative(bone_positions.Body.euler_rotation)
bone_positions.Head.euler_rotation = vector.subtract(bone_positions.Head.euler_rotation, bone_positions.Body.euler_rotation)
bone_positions.Leg_Left.euler_rotation = vector.subtract(bone_positions.Leg_Left.euler_rotation, bone_positions.Body.euler_rotation)
bone_positions.Leg_Right.euler_rotation = vector.subtract(bone_positions.Leg_Right.euler_rotation, bone_positions.Body.euler_rotation)
end
end
if clamphead then
Head.x = clamp(Head.x, conf.head.pitch)
Head.y = clamp(Head.y, conf.head.yaw)
if math.abs(Head.y) > conf.head.yaw_restriction then
Head.x = clamp(Head.x, conf.head.yaw_restricted)
end
Arm_Right.y = clamp(Arm_Right.y, conf.arm_right.yaw)
end
for bone, values in pairs(bone_positions) do
local overridden_values = player_animation.bone_positions[bone]
overridden_values = overridden_values or {}
set_bone_position(player, bone, overridden_values.position or values.position, overridden_values.euler_rotation or values.euler_rotation)
end
end
if player_api then
-- TODO prevent player_api from using player:set_animation
local set_animation = player_api.set_animation
player_api.set_animation = function(player, ...)
local player_animation = players[player:get_player_name()]
if not player_animation then
return
end
local ret = {set_animation(player, ...)}
handle_player_animations(0, player)
return unpack(ret)
end
end
minetest.register_globalstep(function(dtime)
for _, player in pairs(minetest.get_connected_players()) do
handle_player_animations(dtime, player)
end
end)

View File

@ -3,4 +3,5 @@ title = Character Animations
description = Animates the character
author = LMD aka appguru(eu)
depends = modlib
# these mods are supported, but don't necessarily need to load before character_anim
optional_depends = player_api, 3d_armor, skinsdb

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,17 +1,17 @@
local function angle(description, default)
return { type = "number", range = { min = -180, max = 180 }, description = description, default = default }
return { type = "number", range = { min = -180, max = 180 }, description = description, default = default }
end
local function range(description, default_min, default_max)
return {
type = "table",
entries = {
min = angle(description .. " (min)", default_min),
max = angle(description .. " (max)", default_max)
},
func = function(range)
if range.max < range.min then return "Minimum range value is not <= maximum range value" end
end
}
local range = function(description, default_min, default_max)
return {
type = "table",
entries = {
min = angle(description .. " (min)", default_min),
max = angle(description .. " (max)", default_max)
},
func = function(range)
if range.max < range.min then return "Minimum range value is not <= maximum range value" end
end
}
end
local model = {
type = "table",
@ -19,13 +19,13 @@ local model = {
body = {
type = "table",
entries = {
turn_speed = {
type = "number",
range = { min_exclusive = 0, max = 1e3 },
description = "Body turn speed",
default = 0.2
}
}
turn_speed = {
type = "number",
range = { min_exclusive = 0, max = 1e3 },
description = "Body turn speed",
default = 0.2
}
}
},
head = {
type = "table",
@ -39,28 +39,27 @@ local model = {
arm_right = {
type = "table",
entries = {
radius = angle("Right arm spin radius", 10),
speed = {
type = "number",
range = { min_exclusive = 0, max = 1e4 },
description = "Right arm spin speed",
default = 1e3
},
yaw = range("Right arm yaw", -30, 160)
}
radius = angle("Right arm spin radius", 10),
speed = {
type = "number",
range = { min_exclusive = 0, max = 1e4 },
description = "Right arm spin speed",
default = 1e3
},
yaw = range("Right arm yaw", -30, 160)
}
}
}
}
return {
type = "table",
entries = {
default = model,
models = {
type = "table",
keys = { type = "string" },
values = model,
description = "Other models, same format as `default` model"
}
}
}
type = "table",
entries = {
default = model,
models = {
type = "table",
keys = { type = "string" },
description = "Other models, same format as `default` model"
}
}
}

View File

@ -1,33 +1,53 @@
[*character_anim.default]
[**character_anim.default.arm_right]
[character_anim.default]
[*character_anim.default.arm_right]
# Right arm spin radius
character_anim.default.arm_right.radius (Character anim Default Arm right Radius) float 10
character_anim.default.arm_right.radius (Character anim Default Arm right Radius) float 10 -180.000000 180.000000
# Right arm spin speed
character_anim.default.arm_right.speed (Character anim Default Arm right Speed) float 1000
[***character_anim.default.arm_right.yaw]
character_anim.default.arm_right.speed (Character anim Default Arm right Speed) float 1000 0.000000 10000.000000
[**character_anim.default.arm_right.yaw]
# Right arm yaw (max)
character_anim.default.arm_right.yaw.max (Character anim Default Arm right Yaw Max) float 160
character_anim.default.arm_right.yaw.max (Character anim Default Arm right Yaw Max) float 160 -180.000000 180.000000
# Right arm yaw (min)
character_anim.default.arm_right.yaw.min (Character anim Default Arm right Yaw Min) float -30
[**character_anim.default.body]
character_anim.default.arm_right.yaw.min (Character anim Default Arm right Yaw Min) float -30 -180.000000 180.000000
[*character_anim.default.body]
# Body turn speed
character_anim.default.body.turn_speed (Character anim Default Body Turn speed) float 0.2
[**character_anim.default.head]
[***character_anim.default.head.pitch]
# Head pitch (max)
character_anim.default.head.pitch.max (Character anim Default Head Pitch Max) float 80
# Head pitch (min)
character_anim.default.head.pitch.min (Character anim Default Head Pitch Min) float -60
[***character_anim.default.head.yaw]
# Head yaw (max)
character_anim.default.head.yaw.max (Character anim Default Head Yaw Max) float 90
# Head yaw (min)
character_anim.default.head.yaw.min (Character anim Default Head Yaw Min) float -90
[***character_anim.default.head.yaw_restricted]
# Head yaw restricted (max)
character_anim.default.head.yaw_restricted.max (Character anim Default Head Yaw restricted Max) float 45
# Head yaw restricted (min)
character_anim.default.head.yaw_restricted.min (Character anim Default Head Yaw restricted Min) float 0
character_anim.default.body.turn_speed (Character anim Default Body Turn speed) float 0.2 0.000000 1000.000000
[*character_anim.default.head]
# Head yaw restriction
character_anim.default.head.yaw_restriction (Character anim Default Head Yaw restriction) float 60
[*character_anim.models]
character_anim.default.head.yaw_restriction (Character anim Default Head Yaw restriction) float 60 -180.000000 180.000000
[**character_anim.default.head.pitch]
# Head pitch (max)
character_anim.default.head.pitch.max (Character anim Default Head Pitch Max) float 80 -180.000000 180.000000
# Head pitch (min)
character_anim.default.head.pitch.min (Character anim Default Head Pitch Min) float -60 -180.000000 180.000000
[**character_anim.default.head.yaw]
# Head yaw (max)
character_anim.default.head.yaw.max (Character anim Default Head Yaw Max) float 90 -180.000000 180.000000
# Head yaw (min)
character_anim.default.head.yaw.min (Character anim Default Head Yaw Min) float -90 -180.000000 180.000000
[**character_anim.default.head.yaw_restricted]
# Head yaw restricted (max)
character_anim.default.head.yaw_restricted.max (Character anim Default Head Yaw restricted Max) float 45 -180.000000 180.000000
# Head yaw restricted (min)
character_anim.default.head.yaw_restricted.min (Character anim Default Head Yaw restricted Min) float 0 -180.000000 180.000000
[character_anim.models]