feat(ludic.anim): ozz-animation 0.17.0 underneath - a skin's skeleton and each clip built at load, sampled to 1e-4 of anim_mix (phase 19.1)

No bake and no new file: the skeleton comes from the skin's parents and rest pose (its own joints
and their ancestors - a kit holds several rigs), a clip from anim_read_doc's channels. Built by
native/build.sh from the pinned release; the Windows DLL imports KERNEL32 alone and passes there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 12:10:41 +03:00
parent 22d1e2d66a
commit 9a56ee9b9d
14 changed files with 419 additions and 1 deletions

View file

@ -0,0 +1,23 @@
ozz-animation is hosted at http://github.com/guillaumeblanc/ozz-animation and
distributed under the MIT License (MIT).
---
Copyright (c) 2020 Guillaume Blanc
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,29 @@
#!/bin/sh
# builds lib/<target>/ for ludic.anim: ozz-animation at a pinned tag - its base, its runtime jobs and
# the offline builders that make a skeleton and a clip at load - and the shim over them. No CMake,
# no glTF importer: the game reads the glTF and hands the shim its numbers. Objects go to build/native
set -eu
PKG="$(cd "$(dirname "$0")/.." && pwd)"
. "$PKG/../../tools/native/lib.sh"
OZZ_TAG=0.17.0
OZZ_SHA=22b4401c77a2be68acb0a8aa2e857f94dbaccfed3ab25e304a1ddea4c546e86b
SRC="$PKG/build/src/ozz-$OZZ_TAG"
native_fetch "$SRC" "https://github.com/guillaumeblanc/ozz-animation/archive/refs/tags/$OZZ_TAG.tar.gz" "$OZZ_SHA"
OBJ="$PKG/build/native/$(native_target)"
mkdir -p "$OBJ"
CXX="$(native_cxx)"
FLAGS="$(native_cflags) -std=c++17 -DNDEBUG -fno-exceptions -fno-rtti -I$SRC/include -I$SRC/src"
case "$(native_target)" in windows-*) FLAGS="$FLAGS -D_CRT_SECURE_NO_WARNINGS" ;; esac
JOBS="$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)"
# the base and the runtime whole, and of the offline library only what builds a skeleton and a clip
( cd "$SRC" && find src/base src/animation/runtime -name '*.cc'
for f in raw_skeleton skeleton_builder raw_animation raw_animation_utils animation_builder; do echo "src/animation/offline/$f.cc"; done
) | while read -r f; do
o="$OBJ/$(echo "$f" | tr '/' '_' | sed 's/\.cc$/.o/')"
[ "$o" -nt "$SRC/$f" ] || echo "$f $o"
done | xargs -P "$JOBS" -n 2 sh -c '"$0" '"$FLAGS"' -c "'"$SRC"'/$1" -o "$2"' "$CXX"
"$CXX" $FLAGS -I"$PKG/native/shim" -c "$PKG/native/shim/ozz_shim.cpp" -o "$OBJ/ozz_shim.o"
native_link "$PKG" ludicozz "$OBJ"/*.o

View file

@ -0,0 +1,186 @@
// ozz_shim.cpp - ludic.anim's door into ozz-animation (phase 19). It keeps the shim rules of
// packages/README.md: int, float and opaque handles cross; no struct by value, no callback into
// Ludic; errors are a null handle or a 0. A skeleton and a clip are built at load from the numbers
// the package already holds (a skin's parents and rest pose, a clip's flattened channels); what a
// frame needs is made once per actor (a context) and reused.
#include "ozz/animation/offline/raw_skeleton.h"
#include "ozz/animation/offline/skeleton_builder.h"
#include "ozz/animation/offline/raw_animation.h"
#include "ozz/animation/offline/animation_builder.h"
#include "ozz/animation/runtime/skeleton.h"
#include "ozz/animation/runtime/animation.h"
#include "ozz/animation/runtime/sampling_job.h"
#include "ozz/base/maths/soa_transform.h"
#include "ozz/base/maths/simd_math.h"
#include <cstdlib>
#include <string>
#include <vector>
#if defined(_WIN32)
#define OZZ_SHIM extern "C" __declspec(dllexport)
#else
#define OZZ_SHIM extern "C" __attribute__((visibility("default")))
#endif
namespace oa = ozz::animation;
namespace off = ozz::animation::offline;
// a skeleton, and the map between its joints (ozz orders them depth first) and the file's nodes
struct Skel {
ozz::unique_ptr<oa::Skeleton> s;
std::vector<int> node_of; // per joint: the file's node
std::vector<int> joint_of; // per node: its joint, -1 for none
std::vector<float> rest; // per node: t3 r4 s3, what a joint no channel moves holds
};
struct Clip {
ozz::unique_ptr<oa::Animation> a;
float dur;
};
struct Ctx {
const Skel *sk;
oa::SamplingJob::Context ctx;
std::vector<ozz::math::SoaTransform> locals;
};
static void add_children(off::RawSkeleton::Joint &j, int node, int n, const int *par, const float *rest) {
j.name = std::to_string(node).c_str();
const float *r = rest + node * 10;
j.transform.translation = ozz::math::Float3(r[0], r[1], r[2]);
j.transform.rotation = ozz::math::Quaternion(r[3], r[4], r[5], r[6]);
j.transform.scale = ozz::math::Float3(r[7], r[8], r[9]);
for (int c = 0; c < n; c++) {
if (par[c] != node) continue;
j.children.emplace_back();
add_children(j.children.back(), c, n, par, rest);
}
}
// n nodes, each's parent (-1 a root, -2 not in this skeleton) and rest pose (10 floats: translation, rotation x y z w, scale)
OZZ_SHIM void *ozz_skel_new(int n, const int *par, const float *rest) {
if (n <= 0 || n > oa::Skeleton::kMaxJoints) return nullptr;
off::RawSkeleton raw;
for (int i = 0; i < n; i++) {
if (par[i] != -1) continue;
raw.roots.emplace_back();
add_children(raw.roots.back(), i, n, par, rest);
}
if (!raw.Validate()) return nullptr;
off::SkeletonBuilder build;
Skel *sk = new Skel();
sk->s = build(raw);
if (!sk->s) { delete sk; return nullptr; }
int nj = sk->s->num_joints();
sk->node_of.assign(nj, -1);
sk->joint_of.assign(n, -1);
auto names = sk->s->joint_names();
for (int j = 0; j < nj; j++) {
int node = std::atoi(names[j]);
sk->node_of[j] = node;
sk->joint_of[node] = j;
}
sk->rest.assign(rest, rest + n * 10);
return sk;
}
OZZ_SHIM void ozz_skel_free(void *p) { delete static_cast<Skel *>(p); }
OZZ_SHIM int ozz_skel_joints(void *p) { return p ? static_cast<Skel *>(p)->s->num_joints() : 0; }
OZZ_SHIM int ozz_skel_joint_of(void *p, int node) {
Skel *sk = static_cast<Skel *>(p);
if (!sk || node < 0 || node >= (int)sk->joint_of.size()) return -1;
return sk->joint_of[node];
}
static float clampt(float t, float dur) { return t < 0.f ? 0.f : (t > dur ? dur : t); }
// a clip from ludic.anim's flattened channels (clip.ludic): a channel's keys are
// times[coff .. coff + ckeys], its values 4 (rotation, path 0) or 3 (translation, path 1) from
// cvoff; translations only when `positions`. A joint no channel moves holds its rest pose.
OZZ_SHIM void *ozz_clip_new(void *skp, float dur, int nch, const int *cnode, const int *cpath, const int *ckeys,
const int *coff, const int *cvoff, const float *times, const float *vals, int positions) {
Skel *sk = static_cast<Skel *>(skp);
if (!sk) return nullptr;
off::RawAnimation raw;
raw.duration = dur > 0.0001f ? dur : 0.0001f;
int nj = sk->s->num_joints();
raw.tracks.resize(nj);
for (int ch = 0; ch < nch; ch++) {
int node = cnode[ch];
if (node < 0 || node >= (int)sk->joint_of.size() || sk->joint_of[node] < 0) continue;
auto &tr = raw.tracks[sk->joint_of[node]];
const float *v = vals + cvoff[ch];
for (int k = 0; k < ckeys[ch]; k++) {
float t = clampt(times[coff[ch] + k], raw.duration);
if (k > 0 && t <= times[coff[ch] + k - 1] && t < raw.duration) continue;
if (cpath[ch] == 0) {
const float *q = v + k * 4;
tr.rotations.push_back({t, ozz::math::Quaternion(q[0], q[1], q[2], q[3])});
} else if (positions) {
const float *p = v + k * 3;
tr.translations.push_back({t, ozz::math::Float3(p[0], p[1], p[2])});
}
}
}
for (int j = 0; j < nj; j++) {
auto &tr = raw.tracks[j];
const float *r = &sk->rest[sk->node_of[j] * 10];
if (tr.translations.empty()) tr.translations.push_back({0.f, ozz::math::Float3(r[0], r[1], r[2])});
if (tr.rotations.empty()) tr.rotations.push_back({0.f, ozz::math::Quaternion(r[3], r[4], r[5], r[6])});
if (tr.scales.empty()) tr.scales.push_back({0.f, ozz::math::Float3(r[7], r[8], r[9])});
}
if (!raw.Validate()) return nullptr;
off::AnimationBuilder build;
Clip *c = new Clip();
c->a = build(raw);
if (!c->a) { delete c; return nullptr; }
c->dur = raw.duration;
return c;
}
OZZ_SHIM void ozz_clip_free(void *p) { delete static_cast<Clip *>(p); }
OZZ_SHIM int ozz_clip_bytes(void *p) { return p ? (int)static_cast<Clip *>(p)->a->size() : 0; }
// what one actor samples with, made once
OZZ_SHIM void *ozz_ctx_new(void *skp) {
Skel *sk = static_cast<Skel *>(skp);
if (!sk) return nullptr;
Ctx *c = new Ctx();
c->sk = sk;
c->ctx.Resize(sk->s->num_joints());
c->locals.resize(sk->s->num_soa_joints());
return c;
}
OZZ_SHIM void ozz_ctx_free(void *p) { delete static_cast<Ctx *>(p); }
// clip at t seconds (wrapped on its duration), each joint's local rotation (4 per node) and
// translation (3 per node) written at its FILE node, into the caller's buffers of n nodes
OZZ_SHIM int ozz_sample(void *cp, void *clp, float t, float *rot, float *pos, int n) {
Ctx *c = static_cast<Ctx *>(cp);
Clip *cl = static_cast<Clip *>(clp);
if (!c || !cl) return 0;
float tt = t;
if (cl->dur > 0.f) { tt = std::fmod(t, cl->dur); if (tt < 0.f) tt += cl->dur; }
oa::SamplingJob job;
job.animation = cl->a.get();
job.context = &c->ctx;
job.ratio = tt / cl->dur;
job.output = ozz::make_span(c->locals);
if (!job.Run()) return 0;
const Skel *sk = c->sk;
int nj = sk->s->num_joints();
float lane[4][10];
for (int s = 0; s * 4 < nj; s++) {
const ozz::math::SoaTransform &x = c->locals[s];
ozz::math::SimdFloat4 comp[7] = {x.translation.x, x.translation.y, x.translation.z,
x.rotation.x, x.rotation.y, x.rotation.z, x.rotation.w};
for (int k = 0; k < 7; k++) {
float v[4];
ozz::math::StorePtrU(comp[k], v);
for (int l = 0; l < 4; l++) lane[l][k] = v[l];
}
for (int l = 0; l < 4 && s * 4 + l < nj; l++) {
int node = sk->node_of[s * 4 + l];
if (node < 0 || node >= n) continue;
for (int k = 0; k < 3; k++) pos[node * 3 + k] = lane[l][k];
for (int k = 0; k < 4; k++) rot[node * 4 + k] = lane[l][3 + k];
}
}
return 1;
}