add back edited character_anim

This commit is contained in:
Elkien3 2022-04-19 11:55:44 -05:00
parent 2535c14394
commit ea5bfccf6f
18 changed files with 3615 additions and 0 deletions

View File

@ -0,0 +1,192 @@
# Character Animations (`character_anim`)
Animates the character. Resembles [`playeranim`](https://github.com/minetest-mods/playeranim) and [`headanim`](https://github.com/LoneWolfHT/headanim).
## 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).
## Screenshot
![Image](screenshot.png)
## Links
* [GitHub](https://github.com/appgurueu/character_anim) - sources, issue tracking, contributing
* [Discord](https://discordapp.com/invite/ysP74by) - discussion, chatting
* [Minetest Forum](https://forum.minetest.net/viewtopic.php?f=9&t=25385) - (more organized) discussion
* [ContentDB](https://content.minetest.net/packages/LMD/character_anim) - releases (cloning from GitHub is recommended)
## Features
* Animates head, right arm & body
* Advantages over `playeranim`:
* Extracts exact animations and bone positions from glTF models
* Also animates attached players (with restrictions on angles)
* Advantages over `headanim`:
* Provides compatibility for Minetest 5.2.0 and lower
* 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-->
### `default`
#### `arm_right`
##### `radius`
Right arm spin radius
* Type: number
* Default: `10`
* &gt;= -180
* &lt;= 180
##### `speed`
Right arm spin speed
* Type: number
* Default: `1000`
* &gt; 0
* &lt;= 10000
##### `yaw`
###### `max`
Right arm yaw (max)
* Type: number
* Default: `160`
* &gt;= -180
* &lt;= 180
###### `min`
Right arm yaw (min)
* Type: number
* Default: `-30`
* &gt;= -180
* &lt;= 180
#### `body`
##### `turn_speed`
Body turn speed
* Type: number
* Default: `0.2`
* &gt; 0
* &lt;= 1000
#### `head`
##### `pitch`
###### `max`
Head pitch (max)
* Type: number
* Default: `80`
* &gt;= -180
* &lt;= 180
###### `min`
Head pitch (min)
* Type: number
* Default: `-60`
* &gt;= -180
* &lt;= 180
##### `yaw`
###### `max`
Head yaw (max)
* Type: number
* Default: `90`
* &gt;= -180
* &lt;= 180
###### `min`
Head yaw (min)
* Type: number
* Default: `-90`
* &gt;= -180
* &lt;= 180
##### `yaw_restricted`
###### `max`
Head yaw restricted (max)
* Type: number
* Default: `45`
* &gt;= -180
* &lt;= 180
###### `min`
Head yaw restricted (min)
* Type: number
* Default: `0`
* &gt;= -180
* &lt;= 180
##### `yaw_restriction`
Head yaw restriction
* Type: number
* Default: `60`
* &gt;= -180
* &lt;= 180
### `models`
Other models, same format as `default` model
<!--modlib:conf-->
## API
Minetest's `player:set_bone_position` is overridden so that it still works as expected.
### `character_anim.set_bone_override(player, bonename, position, rotation)`
The signature resembles that of `set_bone_position`. `bonename` must be a string. The following additional features are provided:
* Using it like `set_bone_position` by setting `rotation` and `position` to non-`nil` values and using `""` to set the root bone
* *Setting only the bone position* by setting `rotation` to `nil` - bone rotation will then be model-animation-determined
* *Setting only the bone rotation* by setting `position` to `nil` - bone position will then be model-animation-determined
* *Clearing the override* by setting both `rotation` and `position` to `nil` ("unset_bone_position")

View File

@ -0,0 +1,167 @@
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

@ -0,0 +1,7 @@
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"

View File

@ -0,0 +1,243 @@
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
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)
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
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

@ -0,0 +1,6 @@
name = character_anim
title = Character Animations
description = Animates the character
author = LMD aka appguru(eu)
depends = modlib
optional_depends = player_api, 3d_armor, skinsdb

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,66 @@
local function angle(description, 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
}
end
local model = {
type = "table",
entries = {
body = {
type = "table",
entries = {
turn_speed = {
type = "number",
range = { min_exclusive = 0, max = 1e3 },
description = "Body turn speed",
default = 0.2
}
}
},
head = {
type = "table",
entries = {
pitch = range("Head pitch", -60, 80),
yaw = range("Head yaw", -90, 90),
yaw_restricted = range("Head yaw restricted", 0, 45),
yaw_restriction = angle("Head yaw restriction", 60)
}
},
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)
}
}
}
}
return {
type = "table",
entries = {
default = model,
models = {
type = "table",
keys = { type = "string" },
values = model,
description = "Other models, same format as `default` model"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -0,0 +1,33 @@
[*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
# Right arm spin speed
character_anim.default.arm_right.speed (Character anim Default Arm right Speed) float 1000
[***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
# Right arm yaw (min)
character_anim.default.arm_right.yaw.min (Character anim Default Arm right Yaw Min) float -30
[**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
# Head yaw restriction
character_anim.default.head.yaw_restriction (Character anim Default Head Yaw restriction) float 60
[*character_anim.models]