Skip to content

Commit

Permalink
Readd character_anim
Browse files Browse the repository at this point in the history
  • Loading branch information
appgurueu committed Sep 22, 2020
1 parent 981199e commit c18a5ad
Show file tree
Hide file tree
Showing 14 changed files with 1,333 additions and 0 deletions.
5 changes: 5 additions & 0 deletions mods/character_anim/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
models/*
!models/character.b3d
!models/character.b3d.gltf
!models/character.blend
modeldata.lua
38 changes: 38 additions & 0 deletions mods/character_anim/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# 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) and [`cmdlib`](https://github.com/appgurueu/cmdlib). 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.

## 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. Add `player_animations` to `secure.trusted_mods` (or disable mod security)
3. Export the model as `glTF` and save it under `models/modelname.extension.gltf`
4. Do `/ca import modelname.extension`
1. Install and use `character_anim` like any other mod
37 changes: 37 additions & 0 deletions mods/character_anim/conf.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
local angle = { type = "number", range = { -180, 180 } }
local range = {
type = "table",
children = { angle, angle },
func = function(range)
if range[2] < range[1] then return "First range value is not <= second range value" end
end
}
local model = {
type = "table",
children = {
body = {
type = "table",
children = { turn_speed = { type = "number", range = { 0, 1e3 } } }
},
head = {
type = "table",
children = {
pitch = range,
yaw = range,
yaw_restricted = range,
yaw_restriction = angle
}
},
arm_right = {
type = "table",
children = { radius = angle, speed = { type = "number", range = { 0, 1e4 } }, yaw = range }
}
}
}
conf = modlib.conf.import(minetest.get_current_modname(), {
type = "table",
children = {
default = model,
models = { type = "table", keys = { type = "string" }, values = model }
}
})
13 changes: 13 additions & 0 deletions mods/character_anim/default_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"default": {
"body": { "turn_speed": 0.2 },
"head": {
"pitch": [ -60, 80 ],
"yaw": [ -90, 90 ],
"yaw_restricted": [ 0, 45 ],
"yaw_restriction": 60
},
"arm_right": { "radius": 10, "speed": 1e3, "yaw": [ -30, 160 ] }
},
"models": {}
}
158 changes: 158 additions & 0 deletions mods/character_anim/importer.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
-- TODO use minetest.request_insecure_environment()
local BinaryStream = require("binarystream")

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))
buffers[index] = minetest.decode_base64(buffer:sub((data_uri_start):len()+1))
end
local accessors = gltf.accessors
local function read_accessor(accessor)
local buffer_view = gltf.bufferViews[accessor.bufferView + 1]
buffer = 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 math.max(binary_stream:readU8() / 255)
end,
[5122] = function()
return math.max(binary_stream:readS16() / 32767, -1)
end,
[5123] = function()
return math.max(binary_stream:readU16() / 255)
end,
[5125] = function()
return math.max(binary_stream:readU8() / 255)
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)
modlib.file.write(basepath .. "modeldata.lua", minetest.serialize(modeldata))
return true
end

cmdlib.register_chatcommand("ca import", {
params = "<filename>",
description = "Imports a model for use with character_anim",
privs = {server = true},
func = function(_, params)
local filename = params.filename
local success = import_model(filename)
return success, success and "Model " .. filename .. " imported successfully" or "File "..filename.." does not exist"
end
})
8 changes: 8 additions & 0 deletions mods/character_anim/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
setfenv(1, modlib.mod)
local namespace = create_namespace()
local quaternion = setmetatable({}, {__index = namespace})
include_env(get_resource"quaternion.lua", quaternion)
namespace.quaternion = quaternion
extend"conf"
extend"importer"
extend"main"
Loading

0 comments on commit c18a5ad

Please sign in to comment.