From 8fe5d0194221e34eac65739df9a073d93062fe89 Mon Sep 17 00:00:00 2001 From: mauwpix <336649003+mauwpix@users.noreply.github.com> Date: Sun, 25 Oct 2026 05:27:48 +0100 Subject: [PATCH] Plugins: skater.pose can fully control a joint (its own place, relative to its parent, another joint, the skater or the world) instead of only adding to the game's animation So far a plugin could only add to what the game's animation does with a joint. Now it can give the joint its own transform, which the animation's is replaced by: its own animations, joints reused for body parts the skater doesn't have, joints moved by a simulation in Luau, joints pinned to a point. A joint's entry takes place = {position, rotation | angles, relative_to} and keep_children = true (its children stay where they were); a pose takes up to 128 joints. Each frame the place is turned into the joint's value under its real parent, the joints posed in an order that puts each after its posed parents and the joint it is placed relative to; a pose without such an order is refused with the reason. Extension/Skater/pose_place.h/.cpp hold the maths, with pose_place_tests; skater_animation applies it after a joint's offsets; plugin_api_world reads it and sorts the joints. Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 6 + Extension/Plugins/Test/plugin_tests.cpp | 47 ++++++- Extension/Plugins/plugin_api_world.cpp | 59 ++++++++- Extension/Plugins/plugin_game.h | 4 + Extension/Plugins/plugin_game_runtime.cpp | 2 +- Extension/Skater/Test/pose_place_tests.cpp | 136 +++++++++++++++++++++ Extension/Skater/pose_place.cpp | 118 ++++++++++++++++++ Extension/Skater/pose_place.h | 47 +++++++ Extension/Skater/skater_animation.cpp | 12 +- Extension/Skater/skater_animation.h | 10 +- cmake/Core.cmake | 1 + 11 files changed, 431 insertions(+), 11 deletions(-) create mode 100644 Extension/Skater/Test/pose_place_tests.cpp create mode 100644 Extension/Skater/pose_place.cpp create mode 100644 Extension/Skater/pose_place.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0bdcc1a5..43117b7f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -412,6 +412,12 @@ if(DINGOSDK_BUILD_ANIMATION_STUDIO_TESTS) target_include_directories(dingosdk_carry_tests PRIVATE "${PROJECT_SOURCE_DIR}") target_link_libraries(dingosdk_carry_tests PRIVATE dingosdk_json) add_test(NAME carry COMMAND dingosdk_carry_tests) + # A joint set to a place (reskate.skater.pose's place): in a frame, its children kept, the order. + add_executable(dingosdk_pose_place_tests Extension/Skater/Test/pose_place_tests.cpp Extension/Skater/pose_place.cpp + Extension/AnimationStudio/anim_core.cpp) + target_include_directories(dingosdk_pose_place_tests PRIVATE "${PROJECT_SOURCE_DIR}") + target_link_libraries(dingosdk_pose_place_tests PRIVATE dingosdk_json) + add_test(NAME pose_place COMMAND dingosdk_pose_place_tests) endif() option(DINGOSDK_BUILD_REPLAY_KEYFRAMES_TESTS "Build replay keyframe smoothing regression tests" OFF) if(DINGOSDK_BUILD_REPLAY_KEYFRAMES_TESTS) diff --git a/Extension/Plugins/Test/plugin_tests.cpp b/Extension/Plugins/Test/plugin_tests.cpp index be2901b2..b0508404 100644 --- a/Extension/Plugins/Test/plugin_tests.cpp +++ b/Extension/Plugins/Test/plugin_tests.cpp @@ -189,8 +189,26 @@ struct FakeGame final : Game { if (shown != Shown::everyone) text += std::format(" {}", shown_name(shown)); auto sorted = offsets; std::sort(sorted.begin(), sorted.end(), [](const auto& a, const auto& b) { return a.joint < b.joint; }); - for (const auto& o : sorted) text += std::format(" {}:{:.2f},{:.2f},{:.2f},{:.2f}x{:.2f}", o.joint, o.rotation[0], o.rotation[1], o.rotation[2], o.rotation[3], o.scale); + for (const auto& o : sorted) { + text += std::format(" {}:{:.2f},{:.2f},{:.2f},{:.2f}x{:.2f}", o.joint, o.rotation[0], o.rotation[1], o.rotation[2], o.rotation[3], o.scale); + if (o.place) { + const auto& pl = *o.place; + text += pl.frame == dingosdk::pose_place::Frame::joint ? std::format(" place@{}", pl.joint) + : pl.frame == dingosdk::pose_place::Frame::skater ? std::string(" place@skater") + : pl.frame == dingosdk::pose_place::Frame::world ? std::string(" place@world") + : std::string(" place@parent"); + if (pl.position) text += std::format(" p{:.2f},{:.2f},{:.2f}", (*pl.position)[0], (*pl.position)[1], (*pl.position)[2]); + if (pl.rotation) text += std::format(" r{:.2f},{:.2f},{:.2f},{:.2f}", (*pl.rotation)[0], (*pl.rotation)[1], (*pl.rotation)[2], (*pl.rotation)[3]); + } + if (o.keep_children) text += " keep"; + } calls.push_back(text); + // The order the joints are posed in, when any is set to a place or keeps its children. + if (std::any_of(offsets.begin(), offsets.end(), [](const auto& o) { return o.place || o.keep_children; })) { + std::string order = std::format("pose order {}", plugin); + for (const auto& o : offsets) order += std::format(" {}", o.joint); + calls.push_back(order); + } return {}; } std::string typed = ""; @@ -1193,7 +1211,7 @@ void the_skater_can_be_read_moved_and_the_world_probed() { check(environment.logged("teleport false teleporting is unavailable"), "nor to move"); environment.local_skater = SkaterState{{1, 0, 2}, 1.5f, 4, 0.5f, true, 0.25f}; host.run_console("probe"); - check(environment.logged("at 1.0 0.0 2.0 facing 1.50 speed 4.0 up 0.5 air true 0.2"), "The skater is read\n" + environment.all()); + check(environment.logged("at 1.0 0.0 2.0 facing 1.50 speed 4.0 up 0.5 air true 0."), "The skater is read\n" + environment.all()); running(host, "mover", environment); host.run_console("move 0.5"); check(environment.teleports.size() == 1 && environment.teleports[0].first == Vec3{10, 0, 20} && @@ -1528,6 +1546,26 @@ void plugins_read_joints_and_tune_physics() { assert(not pcall(reskate.skater.pose, {head = {scale = 11}}), "scale 0 to 10") assert(not pcall(reskate.skater.pose, {head = {}}, 2), "weight 0 to 1") assert(not pcall(reskate.skater.pose, {head = {}, [103] = {}}), "a joint once") + local many = {} + for j = 2, 129 do many[j] = {scale = 1} end + assert(reskate.skater.pose(many), "128 joints") + many[130] = {scale = 1} + assert(not pcall(reskate.skater.pose, many), "at most 128 joints") + -- Set to a place: a link of a chain relative to another joint, one in the skater's frame keeping + -- its children where they were, one in the world. + assert(reskate.skater.pose({[49] = {place = {position = vector.create(0, 0, -0.1), relative_to = 278}}, + [278] = {place = {angles = vector.create(0, 0, 90), relative_to = "skater"}, keep_children = true}, + head = {place = {position = vector.create(1, 2, 3), relative_to = "world"}}, [42] = {place = {position = vector.create(0, 0.2, 0)}}})) + assert(not pcall(reskate.skater.pose, {[49] = {place = {relative_to = 278}}}), "a place has a position or a rotation") + assert(not pcall(reskate.skater.pose, {[49] = {place = {position = vector.create(0, 0, 0), relative_to = "moon"}}}), "relative to a joint, the skater or the world") + assert(not pcall(reskate.skater.pose, {head = {keep_children = 1}}), "keep_children is true or false") + local _, itself = pcall(reskate.skater.pose, {[49] = {place = {position = vector.create(0, 0, 0), relative_to = 49}}}) + assert(itself:find("relative to itself"), itself) + local _, below = pcall(reskate.skater.pose, {hips = {place = {position = vector.create(0, 0, 0), relative_to = "spine"}}}) + assert(below:find("hangs from it"), below) + local _, circle = pcall(reskate.skater.pose, {[49] = {place = {position = vector.create(0, 0, 0), relative_to = 278}}, + [278] = {place = {position = vector.create(0, 0, 0), relative_to = 49}}}) + assert(circle:find("in a circle"), circle) assert(reskate.settings.set("Camera.Height", 1.5) and reskate.settings.set("Chat.Show", true) and reskate.settings.set("Render.Mode", "fast") and reskate.settings.set("Camera.Height", nil)) reskate.log("tuned") @@ -1542,6 +1580,11 @@ void plugins_read_joints_and_tune_physics() { check(game.called("pose tuner 0.50 8:0.00,0.00,0.00,1.00x1.00 44:0.00,0.00,0.71,0.71x1.00 103:0.00,0.00,0.00,1.00x0.00") && game.called("pose tuner 1.00"), "Joints posed: a head shrunk, one turned 90 degrees about Z, and off\n" + environment.all()); + check(game.called("pose tuner 1.00 42:0.00,0.00,0.00,1.00x1.00 place@parent p0.00,0.20,0.00 49:0.00,0.00,0.00,1.00x1.00 place@278 p0.00,0.00,-0.10 " + "103:0.00,0.00,0.00,1.00x1.00 place@world p1.00,2.00,3.00 278:0.00,0.00,0.00,1.00x1.00 place@skater r0.00,0.00,0.71,0.71 keep") && + game.called("pose order tuner 42 103 278 49"), + "Joints set to places: in their parent's frame, another joint's, the world and the skater's, the children kept; " + "each posed after the joint it is placed relative to\n" + environment.all()); check(game.called("setting tuner Camera.Height 1.5") && game.called("setting tuner Chat.Show true") && game.called("setting tuner Render.Mode fast") && game.called("setting tuner Camera.Height none"), "Game settings held and let go\n" + environment.all()); diff --git a/Extension/Plugins/plugin_api_world.cpp b/Extension/Plugins/plugin_api_world.cpp index 05a15db3..7bfeff0c 100644 --- a/Extension/Plugins/plugin_api_world.cpp +++ b/Extension/Plugins/plugin_api_world.cpp @@ -1437,8 +1437,39 @@ struct WorldApi { lua_setfield(L, -2, "scale"); return 1; } - // skater.pose({[joint] = {rotation = {x, y, z, w} | angles = vector (degrees), scale = n}, ...}, weight): - // joints turned in their own frame and scaled on top of the animation for 0.3 s; nil ends it. + // A joint's place in a pose: {position = vector, rotation = {x, y, z, w} | angles = vector (degrees), + // relative_to = joint | "skater" | "world"}, any part left out as the animation has it; relative to + // its own parent when no relative_to is given. + static pose_place::Place place_of(lua_State* L, int index) { + luaL_checktype(L, index, LUA_TTABLE); + pose_place::Place place; + lua_getfield(L, index, "position"); + if (!lua_isnil(L, -1)) place.position = position_of(L, lua_gettop(L), "a joint's place"); + lua_pop(L, 1); + lua_getfield(L, index, "rotation"); + if (!lua_isnil(L, -1)) place.rotation = quaternion_at(L, lua_gettop(L)); + lua_pop(L, 1); + lua_getfield(L, index, "angles"); + if (!lua_isnil(L, -1)) { + const auto a = position_of(L, lua_gettop(L), "a joint's place's angles"); + place.rotation = anim_studio::from_euler({a[0], a[1], a[2]}); + } + lua_pop(L, 1); + lua_getfield(L, index, "relative_to"); + if (lua_type(L, -1) == LUA_TSTRING && std::string_view(lua_tostring(L, -1)) == "skater") place.frame = pose_place::Frame::skater; + else if (lua_type(L, -1) == LUA_TSTRING && std::string_view(lua_tostring(L, -1)) == "world") place.frame = pose_place::Frame::world; + else if (!lua_isnil(L, -1)) { + place.frame = pose_place::Frame::joint; + place.joint = joint_of(L, lua_gettop(L)); + } + lua_pop(L, 1); + if (!place.position && !place.rotation) luaL_error(L, "a joint's place has a position, a rotation or both"); + return place; + } + // skater.pose({[joint] = {rotation = {x, y, z, w} | angles = vector (degrees), scale = n, offset = vector, + // place = {...}, keep_children = true}, ...}, weight): joints turned in their own frame, scaled and + // moved on top of the animation, or set to a place (place_of), for 0.3 s; nil ends it. keep_children: + // the joint's children stay where they were in the world. static int skater_pose(lua_State* L) { auto& plugin = plugin_of(L); need(L, plugin, "skater.animation", "reskate.skater.pose"); @@ -1475,12 +1506,34 @@ struct WorldApi { if (std::abs(v) > 2) luaL_error(L, "a joint's offset is at most 2 m on each axis"); } lua_pop(L, 1); + lua_getfield(L, at, "place"); + if (!lua_isnil(L, -1)) o.place = place_of(L, lua_gettop(L)); + lua_pop(L, 1); + lua_getfield(L, at, "keep_children"); + if (!lua_isnil(L, -1)) { + luaL_checktype(L, -1, LUA_TBOOLEAN); + o.keep_children = lua_toboolean(L, -1) != 0; + } + lua_pop(L, 1); if (std::any_of(offsets.begin(), offsets.end(), [&](const auto& e) { return e.joint == o.joint; })) luaL_error(L, "joint %d is posed twice", static_cast(o.joint)); - if (offsets.size() >= 64) luaL_error(L, "at most 64 joints are posed at once"); + // An avatar with its proportions and its own physics bones (tail, ears) needs about 70. + if (offsets.size() >= 128) luaL_error(L, "at most 128 joints are posed at once"); offsets.push_back(o); lua_pop(L, 1); } + // Each joint after the joints it hangs from and the joint it is placed relative to. + std::vector joints; + std::vector relative_to; + for (const auto& o : offsets) { + joints.push_back(o.joint); + relative_to.push_back(o.place && o.place->frame == pose_place::Frame::joint ? o.place->joint : -1); + } + std::vector order; + if (const auto why = pose_place::order(joints, relative_to, order); !why.empty()) luaL_error(L, "%s", why.c_str()); + std::vector ordered; + for (const auto i : order) ordered.push_back(offsets[i]); + offsets = std::move(ordered); } const double weight = luaL_optnumber(L, 2, 1); if (!(weight >= 0 && weight <= 1)) luaL_argerror(L, 2, "a weight is 0 to 1"); diff --git a/Extension/Plugins/plugin_game.h b/Extension/Plugins/plugin_game.h index f2076e0e..2790d4a7 100644 --- a/Extension/Plugins/plugin_game.h +++ b/Extension/Plugins/plugin_game.h @@ -2,6 +2,7 @@ #include "plugin_wave.h" #include "Engine/Core/Json/json.h" #include "Extension/AnimationStudio/anim_core.h" +#include "Extension/Skater/pose_place.h" #include "Extension/Throwdowns/throwdown_kit.h" #include #include @@ -729,7 +730,10 @@ public: std::array rotation{0, 0, 0, 1}; float scale{1}; Vec3 offset{}; // added to its position, in its parent's frame + std::optional place; // set instead of added, after the above + bool keep_children{}; // its children stay where they were in the world }; + // The joints come in the order they are posed in (pose_place::order). virtual std::string skater_pose(std::string_view /*plugin*/, const std::vector&, float /*weight*/, Shown /*shown*/) { return unavailable; } diff --git a/Extension/Plugins/plugin_game_runtime.cpp b/Extension/Plugins/plugin_game_runtime.cpp index d51114b9..b5c4a37a 100644 --- a/Extension/Plugins/plugin_game_runtime.cpp +++ b/Extension/Plugins/plugin_game_runtime.cpp @@ -1023,7 +1023,7 @@ public: poser_ = plugin; pose_at_ = now; std::vector out; - for (const auto& o : offsets) out.push_back({o.joint, o.rotation, o.scale, o.offset}); + for (const auto& o : offsets) out.push_back({o.joint, o.rotation, o.scale, o.offset, o.place, o.keep_children}); skater_animation::pose(std::move(out), weight, shown_of(shown)); return {}; } diff --git a/Extension/Skater/Test/pose_place_tests.cpp b/Extension/Skater/Test/pose_place_tests.cpp new file mode 100644 index 00000000..46c84c09 --- /dev/null +++ b/Extension/Skater/Test/pose_place_tests.cpp @@ -0,0 +1,136 @@ +// A joint set to a place instead of moved on top of the animation (Extension/Skater/pose_place.h): in +// its parent's frame, another joint's, the skater's or the world, part of it or all, part of the way; +// its children kept where they were; and the order a pose's joints go in, or why there is none. +#include "Extension/Skater/pose_place.h" +#include +#include +#include +#include +#include +#include + +namespace { +using namespace dingosdk; +using namespace dingosdk::pose_place; + +void check(bool ok, const std::string& message) { + if (ok) return; + std::cerr << "FAILED: " << message << '\n'; + std::exit(1); +} +bool near(const Vec3& a, const Vec3& b) { + return std::abs(a[0] - b[0]) < 1e-4f && std::abs(a[1] - b[1]) < 1e-4f && std::abs(a[2] - b[2]) < 1e-4f; +} +bool near(const Quat& a, const Quat& b) { + const float dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]; + return std::abs(std::abs(dot) - 1) < 1e-4f; +} +// A made-up pose on the real skeleton: every joint a little way from its parent, turned a little, the +// skater placed and turned in the world. +std::vector made_up_pose() { + std::vector pose(anim_studio::skeleton_joints); + for (std::size_t i = 0; i < pose.size(); ++i) { + pose[i].position = {0.01f * static_cast(i % 7), 0.1f, 0.02f}; + pose[i].rotation = anim_studio::axis_angle({0, 0, 1}, 0.05f * static_cast(i % 5)); + } + pose[1].position = {3, 0, -2}; + pose[1].rotation = anim_studio::axis_angle({0, 1, 0}, 1.2f); + return pose; +} +Vec3 world_position(const std::vector& pose, std::uint16_t joint) { return anim_studio::world(pose, joint).position; } +Joint after(const Joint& frame, const Vec3& position, const Quat& rotation) { + // Where something at `position`, turned `rotation`, in `frame` is in the world. + const auto moved = anim_studio::rotate(frame.rotation, {position[0] * frame.scale[0], position[1] * frame.scale[0], position[2] * frame.scale[0]}); + Joint out; + out.position = {frame.position[0] + moved[0], frame.position[1] + moved[1], frame.position[2] + moved[2]}; + out.rotation = anim_studio::normalised(anim_studio::multiply(frame.rotation, rotation)); + return out; +} +} // namespace + +int main() { + const Quat turned = anim_studio::axis_angle({1, 0, 0}, 0.7f); + // In its parent's frame: its own values, as asked. + { + auto pose = made_up_pose(); + apply(pose, 42, {Vec3{0, 0.2f, 0}, turned}, 1); + check(near(pose[42].position, {0, 0.2f, 0}) && near(pose[42].rotation, turned), "placed in its parent's frame: its own values"); + } + // Part of the way: halfway between where it was and where it is asked to be. + { + auto pose = made_up_pose(); + const auto before = pose[42].position; + apply(pose, 42, {Vec3{0, 0.5f, 0}, std::nullopt}, 0.5f); + check(near(pose[42].position, {before[0] * 0.5f, (before[1] + 0.5f) * 0.5f, before[2] * 0.5f}), "half the weight: halfway"); + } + // In the world: where it is asked to be, whatever its parents do; the part left out as it was. + { + auto pose = made_up_pose(); + const auto turn_before = anim_studio::world(pose, 103).rotation; + apply(pose, 103, {Vec3{1, 2, 3}, std::nullopt, Frame::world}, 1); + check(near(world_position(pose, 103), {1, 2, 3}), "placed in the world: there"); + check(near(anim_studio::world(pose, 103).rotation, turn_before), "placed in the world: its turn as it was"); + } + // In the skater's frame: moves and turns with the skater. + { + auto pose = made_up_pose(); + apply(pose, 103, {Vec3{0, 1.5f, 0.2f}, turned, Frame::skater}, 1); + const auto wanted = after(anim_studio::world(pose, 1), {0, 1.5f, 0.2f}, turned); + check(near(world_position(pose, 103), wanted.position) && near(anim_studio::world(pose, 103).rotation, wanted.rotation), + "placed in the skater's frame"); + } + // Relative to another joint it does not hang from (a link of a chain): follows that joint. + { + auto pose = made_up_pose(); + const Place link{Vec3{0, 0, -0.1f}, turned, Frame::joint, 278}; + apply(pose, 49, link, 1); + auto wanted = after(anim_studio::world(pose, 278), {0, 0, -0.1f}, turned); + check(near(world_position(pose, 49), wanted.position) && near(anim_studio::world(pose, 49).rotation, wanted.rotation), + "placed relative to another joint"); + pose[278].rotation = anim_studio::axis_angle({0, 1, 0}, 0.9f); // the other joint moves; placed again + apply(pose, 49, link, 1); + wanted = after(anim_studio::world(pose, 278), {0, 0, -0.1f}, turned); + check(near(world_position(pose, 49), wanted.position), "placed relative to another joint: follows it"); + } + // Its children kept: they stay where they were in the world, and what hangs from them too; not + // kept, they move with it. + { + auto pose = made_up_pose(); + const auto thigh = world_position(pose, 8), spine = world_position(pose, 42), shin = world_position(pose, 9); + std::vector kept; + keep(pose, 7, kept); + check(kept.size() == 3, "the hips have three children"); + apply(pose, 7, {Vec3{0, 0.3f, 0}, turned}, 1); + put_back(pose, 7, kept); + check(near(world_position(pose, 8), thigh) && near(world_position(pose, 42), spine) && near(world_position(pose, 9), shin), + "children kept: where they were"); + auto carried = made_up_pose(); + apply(carried, 7, {Vec3{0, 0.3f, 0}, turned}, 1); + check(!near(world_position(carried, 42), spine), "children not kept: carried along"); + } + // The order: a joint after what it hangs from, and after the joint it is placed relative to. + { + std::vector out; + const std::uint16_t joints[] = {49, 278, 9, 7}; + const int relative_to[] = {278, -1, -1, -1}; + check(order(joints, relative_to, out).empty(), "an order is found"); + const auto at = [&](std::size_t entry) { return std::find(out.begin(), out.end(), entry) - out.begin(); }; + check(out.size() == 4 && at(1) < at(0), "after the joint it is placed relative to"); + check(at(3) < at(2), "after the joint it hangs from"); + } + // No order: relative to itself, to a joint that hangs from it, or joints relative to each other. + { + std::vector out; + const std::uint16_t one[] = {42}; + const int itself[] = {42}; + check(!order(one, itself, out).empty(), "relative to itself: no order"); + const std::uint16_t hips[] = {7}; + const int below[] = {42}; + check(!order(hips, below, out).empty(), "relative to a joint that hangs from it: no order"); + const std::uint16_t two[] = {49, 278}; + const int circle[] = {278, 49}; + const auto why = order(two, circle, out); + check(why.find("circle") != std::string::npos && out.empty(), "relative to each other: no order, and why"); + } + std::cout << "pose_place tests passed\n"; +} diff --git a/Extension/Skater/pose_place.cpp b/Extension/Skater/pose_place.cpp new file mode 100644 index 00000000..2d310046 --- /dev/null +++ b/Extension/Skater/pose_place.cpp @@ -0,0 +1,118 @@ +#include "pose_place.h" +#include +#include +#include + +namespace dingosdk::pose_place { +namespace { +using anim_studio::parent; + +// One transform after another (parent, then child in the parent's frame); uniform scale. +Joint compose(const Joint& frame, const Joint& child) noexcept { + const float k = (frame.scale[0] + frame.scale[1] + frame.scale[2]) / 3.0f; + const auto moved = anim_studio::rotate(frame.rotation, {child.position[0] * k, child.position[1] * k, child.position[2] * k}); + Joint out; + out.position = {frame.position[0] + moved[0], frame.position[1] + moved[1], frame.position[2] + moved[2]}; + out.rotation = anim_studio::normalised(anim_studio::multiply(frame.rotation, child.rotation)); + const float c = (child.scale[0] + child.scale[1] + child.scale[2]) / 3.0f; + out.scale = {k * c, k * c, k * c}; + return out; +} +// `world` seen from `frame`. +Joint relative(const Joint& frame, const Joint& world) noexcept { + const float k = std::max((frame.scale[0] + frame.scale[1] + frame.scale[2]) / 3.0f, 1e-4f); + const auto back = anim_studio::conjugate(frame.rotation); + const auto d = anim_studio::rotate(back, {world.position[0] - frame.position[0], world.position[1] - frame.position[1], + world.position[2] - frame.position[2]}); + Joint out; + out.position = {d[0] / k, d[1] / k, d[2] / k}; + out.rotation = anim_studio::normalised(anim_studio::multiply(back, world.rotation)); + const float c = (world.scale[0] + world.scale[1] + world.scale[2]) / 3.0f; + out.scale = {c / k, c / k, c / k}; + return out; +} +// Whether `joint` hangs from `ancestor` (through any number of parents). +bool hangs_from(std::uint16_t joint, std::uint16_t ancestor) noexcept { + for (int at = parent(joint); at >= 0; at = parent(static_cast(at))) + if (at == ancestor) return true; + return false; +} +Joint world_of(std::span pose, int joint) noexcept { + return joint >= 0 && joint < static_cast(pose.size()) ? anim_studio::world(pose, static_cast(joint)) : Joint{}; +} +} // namespace + +std::string order(std::span joints, std::span relative_to, std::vector& out) { + out.clear(); + const auto n = joints.size(); + std::vector> after(n); // after[i]: the entries i is posed after + for (std::size_t i = 0; i < n; ++i) { + const int to = i < relative_to.size() ? relative_to[i] : -1; + if (to == joints[i]) return std::format("joint {} is placed relative to itself", joints[i]); + if (to >= 0 && hangs_from(static_cast(to), joints[i])) + return std::format("joint {} is placed relative to joint {}, which hangs from it", joints[i], to); + for (std::size_t k = 0; k < n; ++k) { + if (k == i) continue; + const bool above = hangs_from(joints[i], joints[k]); + const bool frame = to >= 0 && (joints[k] == to || hangs_from(static_cast(to), joints[k])); + if (above || frame) after[i].push_back(k); + } + } + // Each time, the lowest joint whose joints before it are all posed: the same order every frame. + std::vector done(n); + while (out.size() < n) { + std::size_t next = n; + for (std::size_t i = 0; i < n; ++i) + if (!done[i] && std::all_of(after[i].begin(), after[i].end(), [&](std::size_t k) { return done[k]; }) && + (next == n || joints[i] < joints[next])) + next = i; + if (next == n) { + std::string circle; + for (std::size_t i = 0; i < n; ++i) + if (!done[i]) circle += std::format("{}{}", circle.empty() ? "" : ", ", joints[i]); + out.clear(); + return "joints " + circle + " are placed relative to each other in a circle"; + } + done[next] = true; + out.push_back(next); + } + return {}; +} + +void apply(std::span pose, std::uint16_t joint, const Place& place, float weight) noexcept { + if (joint >= pose.size()) return; + const Joint now = pose[joint]; + Joint wanted = now; + if (place.frame == Frame::parent) { + if (place.position) wanted.position = *place.position; + if (place.rotation) wanted.rotation = anim_studio::normalised(*place.rotation); + } else { + const Joint parent_world = world_of(pose, parent(joint)); + const Joint frame = place.frame == Frame::joint ? world_of(pose, place.joint) + : place.frame == Frame::skater ? world_of(pose, anim_studio::placement_joint) + : Joint{}; + // Where it is now, in that frame; what is asked for replaces its part; back under its own parent. + auto in_frame = relative(frame, compose(parent_world, now)); + if (place.position) in_frame.position = *place.position; + if (place.rotation) in_frame.rotation = anim_studio::normalised(*place.rotation); + wanted = relative(parent_world, compose(frame, in_frame)); + wanted.scale = now.scale; + } + const float w = std::clamp(weight, 0.0f, 1.0f); + auto& out = pose[joint]; + for (std::size_t a = 0; a < 3; ++a) out.position[a] = now.position[a] + (wanted.position[a] - now.position[a]) * w; + out.rotation = anim_studio::slerp(now.rotation, wanted.rotation, w); +} + +void keep(std::span pose, std::uint16_t joint, std::vector& out) { + out.clear(); + for (std::size_t c = 0; c < pose.size(); ++c) + if (parent(static_cast(c)) == joint) out.push_back({static_cast(c), world_of(pose, static_cast(c))}); +} + +void put_back(std::span pose, std::uint16_t joint, std::span kept) noexcept { + const Joint now = world_of(pose, joint); + for (const auto& k : kept) + if (k.joint < pose.size()) pose[k.joint] = relative(now, k.world); +} +} // namespace dingosdk::pose_place diff --git a/Extension/Skater/pose_place.h b/Extension/Skater/pose_place.h new file mode 100644 index 00000000..459d01ea --- /dev/null +++ b/Extension/Skater/pose_place.h @@ -0,0 +1,47 @@ +#pragma once +#include "Extension/AnimationStudio/anim_core.h" +#include +#include +#include +#include +#include + +// A joint set to a place and turn instead of moved on top of the animation (reskate.skater.pose's +// place): in its parent's frame, in another joint's (a chain of joints that do not hang from each +// other, such as a tail made of spare joints), in the skater's or in the world. The skeleton's parents +// never change: each frame the place asked for is turned into the joint's own value under its real +// parent. A joint's children can stay where they were in the world while it moves (keep). Pure maths +// on the 395-joint pose; skater_animation.cpp applies it with the rest of a pose. +namespace dingosdk::pose_place { +using anim_studio::Joint; +using anim_studio::Quat; +using anim_studio::Vec3; + +enum class Frame : std::uint8_t { parent, joint, skater, world }; +struct Place { + std::optional position; // left out: where the animation has it, in the same frame + std::optional rotation; + Frame frame{Frame::parent}; + std::uint16_t joint{}; // the joint it is placed relative to, with Frame::joint +}; + +// The order to pose joints in, as indices into `joints`: each after the posed joints it hangs from, +// and after the joint it is placed relative to and the posed joints that one hangs from, so it is +// placed against where they are this frame. `relative_to[i]` is that joint, or -1. Empty with the +// reason when no order works: a joint placed relative to itself or to a joint that hangs from it, or +// joints placed relative to each other in a circle. +std::string order(std::span joints, std::span relative_to, std::vector& out); + +// Sets `joint` of `pose` (parent-local values, as the game holds them) to `place`, `weight` 0..1 of +// the way from where it is. Its scale is left as it is. +void apply(std::span pose, std::uint16_t joint, const Place& place, float weight) noexcept; + +// A joint's children where they are in the world, taken before it moves; put back after, so they stay +// where they were (and what hangs from them with them). +struct Kept { + std::uint16_t joint; + Joint world; +}; +void keep(std::span pose, std::uint16_t joint, std::vector& out); +void put_back(std::span pose, std::uint16_t joint, std::span kept) noexcept; +} // namespace dingosdk::pose_place diff --git a/Extension/Skater/skater_animation.cpp b/Extension/Skater/skater_animation.cpp index 3b401461..5af895dc 100644 --- a/Extension/Skater/skater_animation.cpp +++ b/Extension/Skater/skater_animation.cpp @@ -759,18 +759,26 @@ void on_animation(std::uintptr_t component) noexcept { last = to; } }; - // Joints turned and scaled, after the clip; then something carried; then an arm reaching. Each - // goes into the pose drawn, the pose sent, or both (Shown), and its joints join what is written. + // Joints turned, scaled and moved, or set to a place (pose_place.h), after the clip; then something + // carried; then an arm reaching. Each goes into the pose drawn, the pose sent, or both (Shown), and + // its joints join what is written (a joint's children too, when they are kept where they were). const auto pose_joints = [&](std::vector& pose, std::size_t& first, std::size_t& last) { const float w = std::clamp(snap->offsets_weight, 0.0f, 1.0f); + thread_local std::vector kept; for (const auto& o : snap->offsets) { if (o.joint < 2 || o.joint >= layout.count) continue; + if (o.keep_children) pose_place::keep(pose, o.joint, kept); auto& j = pose[o.joint]; j.rotation = normalised(multiply(j.rotation, slerp(Quat{0, 0, 0, 1}, o.rotation, w))); // Never quite 0: a joint at scale 0 has no inverse. This small, it is as good as gone. const float s = std::max(1.0f + (o.scale - 1.0f) * w, smallest_scale); for (auto& k : j.scale) k *= s; for (std::size_t a = 0; a < 3; ++a) j.position[a] += o.offset[a] * w; + if (o.place) pose_place::apply(pose, o.joint, *o.place, w); + if (o.keep_children) { + pose_place::put_back(pose, o.joint, kept); + for (const auto& k : kept) widen(first, last, k.joint, k.joint + 1u); + } widen(first, last, o.joint, o.joint + 1u); } }; diff --git a/Extension/Skater/skater_animation.h b/Extension/Skater/skater_animation.h index 2404fef0..7f8e6dc5 100644 --- a/Extension/Skater/skater_animation.h +++ b/Extension/Skater/skater_animation.h @@ -2,6 +2,7 @@ #include "Extension/AnimationStudio/anim_core.h" #include "Extension/Skater/arm_reach.h" #include "Extension/Skater/carry.h" +#include "Extension/Skater/pose_place.h" #include #include #include @@ -66,14 +67,17 @@ void reach(std::optional goal, Shown shown = Shown::everyone); // holds for 0.3 s: set it every tick. `push_straightens`: riding, the knees straighten (most of the // crouch let go) while a foot is down pushing. void carry(std::optional goal, bool push_straightens = false, Shown shown = Shown::everyone); -// Joints turned (in their own frame) and scaled on top of the animation and any clip, before any -// carry and reach; `weight` 0..1 of the way. Joints 0 and 1 (the skater's world placement) are left -// alone. It holds for 0.3 s: set it every tick; empty ends it. +// Joints turned (in their own frame), scaled and moved on top of the animation and any clip, or set to +// a place (pose_place.h), before any carry and reach; `weight` 0..1 of the way. Posed in the order +// given (pose_place::order). Joints 0 and 1 (the skater's world placement) are left alone. It holds +// for 0.3 s: set it every tick; empty ends it. struct PoseOffset { std::uint16_t joint{}; anim_studio::Quat rotation{0, 0, 0, 1}; float scale{1}; anim_studio::Vec3 offset{}; // added to its position, in its parent's frame + std::optional place; // set instead of added, after the above + bool keep_children{}; // its children stay where they were in the world }; void pose(std::vector offsets, float weight, Shown shown = Shown::everyone); // Where a joint (of the 395) was in the world in the last pose evaluated, after any clip and reach; diff --git a/cmake/Core.cmake b/cmake/Core.cmake index d4507b7d..428d30e7 100644 --- a/cmake/Core.cmake +++ b/cmake/Core.cmake @@ -155,6 +155,7 @@ add_library(dingosdk_plugins STATIC Extension/Throwdowns/challenge_registry.cpp Extension/Plugins/plugin_host.cpp Extension/AnimationStudio/anim_core.cpp + Extension/Skater/pose_place.cpp Extension/Plugins/plugin_wave.cpp) target_link_libraries(dingosdk_plugins PUBLIC dingosdk_json dingosdk_console_core PRIVATE dingosdk_luau) if(WIN32)