First attempt at switching to b3d

master
Lars Mueller 2021-04-02 21:03:12 +02:00
parent 3117bfb779
commit 803d31fa66
11 changed files with 20 additions and 3100 deletions

View File

@ -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-->

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

@ -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"

View File

@ -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