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 <noreply@anthropic.com>
This commit is contained in:
mauwpixandClaude Opus 5.5 committed 2026-10-25 05:27:48 +01:00
1 parent 788eb98352
commit 8fe5d01942
11 files changed
+431 -11

No files matched your search

+6
View File
@@ -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)
+45 -2
View File
@@ -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());
+56 -3
View File
@@ -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<int>(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<std::uint16_t> joints;
std::vector<int> 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<std::size_t> order;
if (const auto why = pose_place::order(joints, relative_to, order); !why.empty()) luaL_error(L, "%s", why.c_str());
std::vector<Game::JointOffset> 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");
+4
View File
@@ -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 <array>
#include <cstdint>
@@ -729,7 +730,10 @@ public:
std::array<float, 4> rotation{0, 0, 0, 1};
float scale{1};
Vec3 offset{}; // added to its position, in its parent's frame
std::optional<pose_place::Place> 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<JointOffset>&, float /*weight*/, Shown /*shown*/) {
return unavailable;
}
+1 -1
View File
@@ -1023,7 +1023,7 @@ public:
poser_ = plugin;
pose_at_ = now;
std::vector<skater_animation::PoseOffset> 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 {};
}
+136
View File
@@ -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 <algorithm>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>
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<Joint> made_up_pose() {
std::vector<Joint> pose(anim_studio::skeleton_joints);
for (std::size_t i = 0; i < pose.size(); ++i) {
pose[i].position = {0.01f * static_cast<float>(i % 7), 0.1f, 0.02f};
pose[i].rotation = anim_studio::axis_angle({0, 0, 1}, 0.05f * static_cast<float>(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<Joint>& 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> 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<std::size_t> 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<std::size_t> 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";
}
+118
View File
@@ -0,0 +1,118 @@
#include "pose_place.h"
#include <algorithm>
#include <array>
#include <format>
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<std::uint16_t>(at)))
if (at == ancestor) return true;
return false;
}
Joint world_of(std::span<const Joint> pose, int joint) noexcept {
return joint >= 0 && joint < static_cast<int>(pose.size()) ? anim_studio::world(pose, static_cast<std::uint16_t>(joint)) : Joint{};
}
} // namespace
std::string order(std::span<const std::uint16_t> joints, std::span<const int> relative_to, std::vector<std::size_t>& out) {
out.clear();
const auto n = joints.size();
std::vector<std::vector<std::size_t>> 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<std::uint16_t>(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<std::uint16_t>(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<bool> 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<Joint> 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<const Joint> pose, std::uint16_t joint, std::vector<Kept>& out) {
out.clear();
for (std::size_t c = 0; c < pose.size(); ++c)
if (parent(static_cast<std::uint16_t>(c)) == joint) out.push_back({static_cast<std::uint16_t>(c), world_of(pose, static_cast<int>(c))});
}
void put_back(std::span<Joint> pose, std::uint16_t joint, std::span<const Kept> 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
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include "Extension/AnimationStudio/anim_core.h"
#include <cstdint>
#include <optional>
#include <span>
#include <string>
#include <vector>
// 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<Vec3> position; // left out: where the animation has it, in the same frame
std::optional<Quat> 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<const std::uint16_t> joints, std::span<const int> relative_to, std::vector<std::size_t>& 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<Joint> 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<const Joint> pose, std::uint16_t joint, std::vector<Kept>& out);
void put_back(std::span<Joint> pose, std::uint16_t joint, std::span<const Kept> kept) noexcept;
} // namespace dingosdk::pose_place
+10 -2
View File
@@ -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<Joint>& 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<pose_place::Kept> 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);
}
};
+7 -3
View File
@@ -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 <cstdint>
#include <memory>
#include <optional>
@@ -66,14 +67,17 @@ void reach(std::optional<arm_reach::Goal> 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<carry::Goal> 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<pose_place::Place> place; // set instead of added, after the above
bool keep_children{}; // its children stay where they were in the world
};
void pose(std::vector<PoseOffset> 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;
+1
View File
@@ -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)