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)