First attempt at switching to b3d
parent
3117bfb779
commit
803d31fa66
10
Readme.md
10
Readme.md
|
@ -28,16 +28,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-->
|
||||
|
|
167
importer.lua
167
importer.lua
|
@ -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
|
||||
})
|
2
init.lua
2
init.lua
|
@ -2,6 +2,4 @@ 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"
|
40
main.lua
40
main.lua
|
@ -1,4 +1,11 @@
|
|||
modeldata = minetest.deserialize(modlib.file.read(modlib.mod.get_resource"modeldata.lua"))
|
||||
local models = {}
|
||||
for _, model in ipairs(minetest.get_dir_list(modlib.mod.get_resource"models", false)) do
|
||||
if modlib.text.ends_with(model, ".b3d") then
|
||||
local stream = io.open(modlib.mod.get_resource(minetest.get_current_modname(), "models", model), "r")
|
||||
models[model] = modlib.b3d.read(stream)
|
||||
assert(stream:read() == nil)
|
||||
end
|
||||
end
|
||||
|
||||
function get_animation_value(animation, keyframe_index, is_rotation)
|
||||
local values = animation.values
|
||||
|
@ -69,8 +76,8 @@ end
|
|||
|
||||
local function handle_player_animations(dtime, player)
|
||||
local mesh = player:get_properties().mesh
|
||||
local modeldata = modeldata[mesh]
|
||||
if not modeldata then
|
||||
local model = models[mesh]
|
||||
if not model then
|
||||
return
|
||||
end
|
||||
local conf = conf.models[mesh] or conf.default
|
||||
|
@ -98,23 +105,14 @@ local function handle_player_animations(dtime, player)
|
|||
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]
|
||||
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
|
||||
-- HACK
|
||||
rotation = {rotation[3], rotation[4], -rotation[1], rotation[2]}
|
||||
local euler_rotation
|
||||
local parent = animation.parent
|
||||
local parent = props.parent_bone_name
|
||||
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)
|
||||
|
@ -130,7 +128,7 @@ local function handle_player_animations(dtime, 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.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
|
||||
|
@ -172,10 +170,12 @@ local function handle_player_animations(dtime, player)
|
|||
|
||||
-- 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
|
||||
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)
|
||||
-- HACK
|
||||
Head.z = Head.z + 180
|
||||
if math.abs(Head.y) > conf.head.yaw_restriction then
|
||||
Head.x = clamp(Head.x, conf.head.yaw_restricted)
|
||||
end
|
||||
|
|
File diff suppressed because one or more lines are too long
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.
File diff suppressed because one or more lines are too long
Binary file not shown.
Loading…
Reference in New Issue