OzzAnimation.NET
0.17.0
dotnet add package OzzAnimation.NET --version 0.17.0
NuGet\Install-Package OzzAnimation.NET -Version 0.17.0
<PackageReference Include="OzzAnimation.NET" Version="0.17.0" />
<PackageVersion Include="OzzAnimation.NET" Version="0.17.0" />
<PackageReference Include="OzzAnimation.NET" />
paket add OzzAnimation.NET --version 0.17.0
#r "nuget: OzzAnimation.NET, 0.17.0"
#:package OzzAnimation.NET@0.17.0
#addin nuget:?package=OzzAnimation.NET&version=0.17.0
#tool nuget:?package=OzzAnimation.NET&version=0.17.0
OzzAnimation.NET
A managed C# port of the ozz-animation runtime — Guillaume Blanc's open-source skeletal animation library. Same data layout, same compression, same archive format, byte for byte; no native code, no P/Invoke, no marshalling.
var skeleton = Skeleton.Load(File.ReadAllBytes("skeleton.ozz"));
var clip = AnimationClip.Load(File.ReadAllBytes("walk.ozz"));
var context = new SamplingContext(clip.TrackCount);
var locals = new SoaTransforms(skeleton.JointCount);
var models = new Matrix4x4[skeleton.JointCount];
// per frame — allocates nothing
context.Sample(clip, ratio, locals);
LocalToModel.Compute(skeleton, locals, models);
Thanks
This library exists because of ozz-animation and its author, Guillaume Blanc. The runtime data layout, the archive format, the keyframe compression, the cursor-cache sampler, the i-frame seeking, the blending model, the analytic IK solvers — all of it is his design. This repository reimplements them in C#; it invents nothing. If you find this useful, the credit belongs upstream:
ozz-animation — https://github.com/guillaumeblanc/ozz-animation © Guillaume Blanc, MIT licensed.
Thank you for building it, and for releasing it under a licence that made this port possible.
Compatibility
Byte-compatible with ozz-animation 0.17:
| Archive | Tag | Version |
|---|---|---|
| Skeleton | ozz-skeleton |
2 |
| Animation | ozz-animation |
7 |
| Tracks | ozz-float_track … ozz-quat_track |
1 |
- A file written by
gltf2ozzloads here. - A file written here loads in ozz's C++ runtime.
This is enforced, not asserted. The test suite embeds archives produced by ozz-animation 0.17's own C++ builders; loading one and saving it again reproduces the file byte for byte, which pins every header count, the SoA rest-pose groups, the keyframe streams and the group-varint i-frames. A separate test samples a clip baked from a closed-form curve and checks the decoded poses against that curve, so the whole decode path is measured against ground truth rather than against itself.
Little-endian only. A big-endian archive is refused rather than byte-swapped.
Versioning tracks the ozz release this ports: 0.17.x reads and writes ozz-animation 0.17's archives, and the patch number is this port's own. A future ozz format revision would land here as a matching minor bump, so the version answers "which ozz does this speak?" without a lookup table.
What is here
This is the runtime half of ozz. ozz ships its authoring code as a separate ozz_animation_offline
library, and this repository draws the same line — see Not ported.
| This library | ozz |
|---|---|
Skeleton |
ozz::animation::Skeleton |
AnimationClip |
ozz::animation::Animation |
SamplingContext.Sample |
SamplingJob + its Context |
SoaTransforms |
span<ozz::math::SoaTransform> |
LocalToModel.Compute |
LocalToModelJob (including from / to / from_excluded) |
BlendingJob |
BlendingJob (weighted, per-joint, additive and subtractive layers) |
IKTwoBoneJob |
IKTwoBoneJob |
IKAimJob |
IKAimJob |
FloatTrack … QuaternionTrack |
FloatTrack … QuaternionTrack |
Track<T>.Sample |
TrackSamplingJob |
TrackTriggering.Edges |
TrackTriggeringJob and its iterator |
MotionBlending.Blend |
MotionBlendingJob |
SkeletonUtils |
skeleton_utils.h |
AnimationUtils |
animation_utils.h |
Two deliberate differences
Row-vector matrices. ozz composes model[i] = parent × local[i] in a column-vector convention.
System.Numerics.Matrix4x4 is row-vector, so this library composes model[i] = local[i] × parent
and stores the transpose. The transforms are the same; only the convention differs, and it is the
one every other .NET library expects.
Exact instead of estimated math. Where ozz uses estimated reciprocal and inverse-square-root
instructions (RcpEst, RSqrtEstNR, NormalizeEst), this uses exact division and square root.
Poses differ from native ozz around the seventh decimal, never structurally.
Jobs are C# methods rather than structs with Validate()/Run() fields, but every ozz parameter is
present, under its ozz name.
Not ported
- The offline module —
RawSkeleton/RawAnimation,SkeletonBuilder,AnimationBuilder,AnimationOptimizer,TrackBuilder,AdditiveAnimationBuilder,MotionExtractor. Build your archives with ozz's own tools (gltf2ozz) and load them here. - The glTF importer and the Fbx toolchain.
ozz::geometryskinning jobs, and the sample framework.
Nothing above changes the archive format, so any of it could be added later without breaking files already written.
One behavioural note
A track with no keys samples to identity, not to the joint's rest pose — same as ozz. If you
want unanimated joints to hold their rest transform, that is the exporter's job, and gltf2ozz
does it.
Performance
Sampling is a port of ozz's SIMD SamplingJob: keys are decoded and interpolated four tracks at a
time in Vector128<float> lanes, straight into a structure-of-arrays pose set with no transpose.
The cursor walk stays scalar, as in ozz.
One frame of one 64-joint character — sample plus local-to-model — on an Apple M3 Max, .NET 10:
| This library | ozz 0.17 (C++) | |
|---|---|---|
| Playback (steps 1/100 of the clip per frame) | 1.128 µs | 1.197 µs |
| Scrubbing (jumps to a random ratio per frame) | 3.607 µs | 3.201 µs |
| Allocated per frame | 0 B | 0 B |
Playback is a dead heat; this run has it slightly ahead, but run-to-run drift on this machine is a few percent either way, so read the two as equal rather than as a win. Scrubbing is about 12% behind, and that gap is the deliberate trade named above: ozz seeks with estimated reciprocal and inverse-square-root instructions, this library uses exact ones.
Splitting the frame shows where the work is, and where it is worth looking:
| Sampling | Local-to-model | Frame | |
|---|---|---|---|
| Playback | 690 ns | 438 ns | 1128 ns |
| Scrubbing | 3175 ns | 432 ns | 3607 ns |
Scrubbing is 88% sampler. Both runtimes walk the same keys from the same i-frames — the cost is
per-key, so the lever that actually moves it is i-frame density at build time (gltf2ozz's
sampling rate), not anything the runtime can do.
The other runtime jobs on the same rig, for scale (ozz's C++ is not wired up for these — the shim covers sampling only — so these are absolute numbers, not a comparison):
| Job | Mean |
|---|---|
BlendingJob, two layers |
175 ns |
BlendingJob, two layers plus an additive one |
397 ns |
LocalToModel, whole skeleton |
435 ns |
IKTwoBoneJob |
155 ns |
IKAimJob |
92 ns |
Nothing allocates after construction, so a frame produces no GC pressure. The library is trimmable and NativeAOT-compatible.
Reproduce it:
dotnet run --project src/OzzAnimation.Benchmarks -c Release -- --filter '*'
The native rows need ozz's own C++ runtime, through the shim in bench/native:
cmake -B bench/native/build bench/native -DOZZ_ROOT=/path/to/ozz-animation -DCMAKE_BUILD_TYPE=Release
cmake --build bench/native/build --config Release
OZZ_NATIVE_LIB=$PWD/bench/native/build/libozz_shim.dylib \
dotnet run --project src/OzzAnimation.Benchmarks -c Release -- --filter '*'
Both sides are allocation-free per frame — the shim keeps its scratch buffers in its context for the same reason this library sizes everything up front, so what is timed is the sampling and the hierarchy walk rather than an allocator.
Building
dotnet build OzzAnimation.NET.slnx
dotnet run --project src/OzzAnimation.Tests -c Release
Targets net10.0. With coverage:
dotnet run --project src/OzzAnimation.Tests -c Release -- \
--coverage --coverage-output-format cobertura --coverage-output coverage.cobertura.xml
93 tests cover 99.7% of the library's lines. The four that remain are the byte-swapping fallbacks for a big-endian host, which no supported .NET platform is — they stay because deleting them would not make anything faster, only wrong somewhere the reinterpreting fast path does not hold. CI reports the number on every push but does not gate on it.
Releasing
The tag is the version — push one and the package ships:
git tag v0.3.0
git push origin v0.3.0
publish-nuget.yml runs the test suite, packs with the tag's version and pushes to nuget.org. The
<Version> in the csproj is only the default for local builds and does not have to be bumped in
lockstep; -p:Version from the tag wins.
Authentication is OIDC rather than a stored key: nuget.org must be configured to trust this
repository, and the NUGET_USER repository variable must name the nuget.org account when it is not
the repository owner. No API key lives in this repository.
Licence
MIT — see LICENSE, which also carries ozz-animation's own MIT notice.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.17.0 | 98 | 9/5/2026 |