update char anim
This commit is contained in:
parent
3f496cd76c
commit
504a3405da
7
mods/character_anim/.luacheckrc
Normal file
7
mods/character_anim/.luacheckrc
Normal file
@ -0,0 +1,7 @@
|
||||
globals = {"character_anim"}
|
||||
read_globals = {
|
||||
"modlib",
|
||||
-- Minetest
|
||||
math = {fields = {"sign"}},
|
||||
"vector", "minetest"
|
||||
}
|
@ -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`
|
||||
* >= -180
|
||||
* <= 180
|
||||
* >= `-180`
|
||||
* <= `180`
|
||||
|
||||
##### `speed`
|
||||
|
||||
@ -60,8 +51,8 @@ Right arm spin speed
|
||||
|
||||
* Type: number
|
||||
* Default: `1000`
|
||||
* > 0
|
||||
* <= 10000
|
||||
* > `0`
|
||||
* <= `10000`
|
||||
|
||||
##### `yaw`
|
||||
|
||||
@ -71,8 +62,8 @@ Right arm yaw (max)
|
||||
|
||||
* Type: number
|
||||
* Default: `160`
|
||||
* >= -180
|
||||
* <= 180
|
||||
* >= `-180`
|
||||
* <= `180`
|
||||
|
||||
###### `min`
|
||||
|
||||
@ -80,8 +71,8 @@ Right arm yaw (min)
|
||||
|
||||
* Type: number
|
||||
* Default: `-30`
|
||||
* >= -180
|
||||
* <= 180
|
||||
* >= `-180`
|
||||
* <= `180`
|
||||
|
||||
|
||||
|
||||
@ -93,8 +84,8 @@ Body turn speed
|
||||
|
||||
* Type: number
|
||||
* Default: `0.2`
|
||||
* > 0
|
||||
* <= 1000
|
||||
* > `0`
|
||||
* <= `1000`
|
||||
|
||||
|
||||
#### `head`
|
||||
@ -107,8 +98,8 @@ Head pitch (max)
|
||||
|
||||
* Type: number
|
||||
* Default: `80`
|
||||
* >= -180
|
||||
* <= 180
|
||||
* >= `-180`
|
||||
* <= `180`
|
||||
|
||||
###### `min`
|
||||
|
||||
@ -116,8 +107,8 @@ Head pitch (min)
|
||||
|
||||
* Type: number
|
||||
* Default: `-60`
|
||||
* >= -180
|
||||
* <= 180
|
||||
* >= `-180`
|
||||
* <= `180`
|
||||
|
||||
|
||||
##### `yaw`
|
||||
@ -128,8 +119,8 @@ Head yaw (max)
|
||||
|
||||
* Type: number
|
||||
* Default: `90`
|
||||
* >= -180
|
||||
* <= 180
|
||||
* >= `-180`
|
||||
* <= `180`
|
||||
|
||||
###### `min`
|
||||
|
||||
@ -137,8 +128,8 @@ Head yaw (min)
|
||||
|
||||
* Type: number
|
||||
* Default: `-90`
|
||||
* >= -180
|
||||
* <= 180
|
||||
* >= `-180`
|
||||
* <= `180`
|
||||
|
||||
|
||||
##### `yaw_restricted`
|
||||
@ -149,8 +140,8 @@ Head yaw restricted (max)
|
||||
|
||||
* Type: number
|
||||
* Default: `45`
|
||||
* >= -180
|
||||
* <= 180
|
||||
* >= `-180`
|
||||
* <= `180`
|
||||
|
||||
###### `min`
|
||||
|
||||
@ -158,8 +149,8 @@ Head yaw restricted (min)
|
||||
|
||||
* Type: number
|
||||
* Default: `0`
|
||||
* >= -180
|
||||
* <= 180
|
||||
* >= `-180`
|
||||
* <= `180`
|
||||
|
||||
|
||||
##### `yaw_restriction`
|
||||
@ -168,8 +159,8 @@ Head yaw restriction
|
||||
|
||||
* Type: number
|
||||
* Default: `60`
|
||||
* >= -180
|
||||
* <= 180
|
||||
* >= `-180`
|
||||
* <= `180`
|
||||
|
||||
|
||||
|
||||
|
@ -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
|
||||
})
|
@ -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)
|
||||
|
@ -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)
|
@ -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
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.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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]
|
Loading…
x
Reference in New Issue
Block a user