H073.ModelKit 2.0.0

Prefix Reserved
dotnet add package H073.ModelKit --version 2.0.0
                    
NuGet\Install-Package H073.ModelKit -Version 2.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="H073.ModelKit" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="H073.ModelKit" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="H073.ModelKit" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add H073.ModelKit --version 2.0.0
                    
#r "nuget: H073.ModelKit, 2.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package H073.ModelKit@2.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=H073.ModelKit&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=H073.ModelKit&version=2.0.0
                    
Install as a Cake Tool

ModelKit — 3D Model Abstraction Layer for MonoGame

The code in this library is human-made — no AI was used to write it. This README, however, was generated with a locally running AI model.

ModelKit is a format-agnostic 3D model framework for MonoGame. It provides a unified scene graph, PBR materials, skeletal animation, and an auto-discovery loader system that supports multiple file formats through plugins.

// Load any supported format — the right loader is found automatically
var scene = FormatRegistry.LoadScene(GraphicsDevice, "character.glb");
var instance = scene.CreateInstance();
instance.Play("Idle");

Installation

dotnet add package H073.ModelKit

Format-Specific Loaders

ModelKit itself doesn't parse any file format. Add loader plugins for the formats you need:

dotnet add package H073.HxGLTF.MonoGame   # glTF / GLB
dotnet add package H073.HxOBJ.MonoGame    # OBJ / MTL
dotnet add package H073.HxSTL.MonoGame    # STL / STLX

Quick Start

using ModelKit;

Scene scene;
SceneInstance instance;

protected override void LoadContent()
{
    scene = FormatRegistry.LoadScene(GraphicsDevice, "character.glb");
    instance = scene.CreateInstance();
    instance.Play("Idle");
}

protected override void Update(GameTime gameTime)
{
    instance.Update((float)gameTime.ElapsedGameTime.TotalSeconds);
}

protected override void Draw(GameTime gameTime)
{
    var commands = new List<RenderCommand>();
    instance.CollectRenderCommands(commands);

    foreach (var cmd in commands)
    {
        // cmd.Material    — Material (color, textures, PBR factors)
        // cmd.WorldTransform — Matrix
        // cmd.BoneMatrices   — Matrix[]? (for skinned meshes)
        // Use your own shader/effect to render
        cmd.Apply(GraphicsDevice);
        cmd.Draw(GraphicsDevice);
    }
}

Scene Graph

Scene is the root container for all 3D data:

Scene
├── SceneNode[]       — Hierarchical transform nodes
├── Mesh[]            — Geometry (MeshPrimitive[] with GPU buffers)
├── Material[]        — PBR / Phong / Unlit materials
├── Skeleton[]        — Bone hierarchies + inverse bind matrices
├── AnimationClip[]   — Keyframe animations
├── SceneCamera[]     — Cameras
├── SceneLight[]      — Light sources
├── SceneBounds       — Axis-aligned bounding box (lazy)
└── SceneSphere       — Bounding sphere (lazy)

Instances

var instance = scene.CreateInstance();
instance.WorldTransform = Matrix.CreateTranslation(5, 0, 0);
instance.Play("Walk", loop: true, blendDuration: 0.2f);
instance.Update(deltaTime);

Multiple instances share GPU resources (meshes, textures) while each has its own animation state and transform.


Materials

Material supports PBR, Phong, and Unlit workflows:

var mat = scene.Materials[0];

mat.Type            // MaterialType.Pbr, Phong, or Unlit
mat.Color           // Base/diffuse color (RGBA)
mat.Texture         // Base color texture
mat.EmissiveFactor  // Emissive color multiplier
mat.EmissiveMap     // Emissive texture

mat.AlphaMode       // Opaque, Mask, Blend
mat.AlphaCutoff     // Threshold for Mask mode
mat.DoubleSided     // Backface culling

// PBR
mat.MetallicFactor, mat.RoughnessFactor
mat.NormalMap, mat.MetallicRoughnessMap
mat.OcclusionMap, mat.OcclusionStrength

// Phong
mat.SpecularColor, mat.Shininess, mat.SpecularMap

// Extensions
mat.ClearcoatFactor, mat.TransmissionFactor, mat.IOR

Animation

// Via SceneInstance (recommended)
instance.Play("Walk");
instance.Play(0);                    // by index
instance.AddLayer("UpperBody", 0.5f); // blending
instance.Stop(blendDuration: 0.3f);

// Low-level
var clip = scene.GetAnimation("Walk");
var player = new AnimationPlayer();
player.Play(clip, loop: true);
player.Update(deltaTime);

Interpolation Modes

  • Linear — standard interpolation
  • Step — instant value changes
  • CubicSpline — smooth curves with in/out tangents

Animation State Machine

Automate animation transitions with parameter-driven conditions. The state machine evaluates transitions each frame and blends between states with configurable duration.

using ModelKit.Animation;

var sm = new AnimationStateMachine();

// Define states
var idle = sm.AddState("Idle", scene.GetAnimation("Idle"));
var walk = sm.AddState("Walk", scene.GetAnimation("Walk"));
var run  = sm.AddState("Run",  scene.GetAnimation("Run"));

// Define transitions with conditions
sm.AddTransition(idle, walk, duration: 0.2f,
    new AnimCondition("Speed", ComparisonType.Greater, 0.1f));

sm.AddTransition(walk, run, duration: 0.3f,
    new AnimCondition("Speed", ComparisonType.Greater, 0.8f));

sm.AddTransition(run, walk, duration: 0.3f,
    new AnimCondition("Speed", ComparisonType.Smaller, 0.8f));

sm.AddTransition(walk, idle, duration: 0.2f,
    new AnimCondition("Speed", ComparisonType.Smaller, 0.1f));

// Drive with parameters
sm.SetParameter("Speed", currentSpeed);
sm.Update(deltaTime);

// Manual state switch (ignores conditions)
sm.GoToState("Idle", blendDuration: 0.2f);

Transitions use SmoothStep blending for smooth crossfades between states. The first added state becomes the initial state.


Blend Trees

Blend between multiple animation clips using two parameters (e.g. movement direction and speed). Uses the Freeform Cartesian Gradient algorithm for smooth 2D interpolation.

using ModelKit.Animation;

var tree = new BlendTree();

// Place clips at 2D positions
tree.Add(scene.GetAnimation("IdleAim"),       x:  0, y:  0);
tree.Add(scene.GetAnimation("WalkForward"),   x:  0, y:  1);
tree.Add(scene.GetAnimation("WalkBackward"),  x:  0, y: -1);
tree.Add(scene.GetAnimation("StrafeLeft"),    x: -1, y:  0);
tree.Add(scene.GetAnimation("StrafeRight"),   x:  1, y:  0);

// Set blend position from input
tree.SetParameters(moveX, moveY);
tree.Update(deltaTime, animationOutput);

Blend trees can also be used as states in the state machine:

var locomotion = new BlendTree();
locomotion.Add(scene.GetAnimation("Idle"),  x: 0, y: 0);
locomotion.Add(scene.GetAnimation("Walk"),  x: 0, y: 1);
locomotion.Add(scene.GetAnimation("Run"),   x: 0, y: 2);

var moveState = sm.AddState("Locomotion", locomotion);

Clip playback is time-synchronized — all clips in the tree advance at the same normalized rate, weighted by their blend contribution.


Bone Masks

Filter which bones are affected by an animation layer. Useful for playing different animations on different body parts (e.g. shooting on upper body while running on lower body).

using ModelKit.Animation;

// Create mask from bone names
var upperBody = BoneMask.FromNames(skeleton, "Spine", "LeftArm", "RightArm", "Head");

// Or from a root bone and all its descendants
var upperBody = BoneMask.FromBoneAndDescendants(skeleton, "Spine");

// Use with animation layers
instance.AddLayer("Shoot", weight: 1f, loop: false);
// The mask ensures only upper-body bones are affected by this layer

FromBoneAndDescendants is the most common pattern — pass a single bone like "Spine" or "Hips" and it automatically includes the entire subtree.


Bone Processors

Modify individual bone transforms after animation blending but before hierarchy multiplication. Changes automatically propagate to all child bones.

Head Tracking (Look-At)

using ModelKit.Animation;

float headYaw = 0f;

// Register a processor for the head bone — runs once per frame, O(1)
instance.Animator.SetBoneProcessor("Bip01 Head", (int idx, ref Matrix m) =>
{
    m *= Matrix.CreateFromAxisAngle(Vector3.Up, headYaw);
});

// In your update loop: compute angle toward target
var toTarget = targetPos - npcPos;
toTarget.Y = 0;
var forward = Vector3.Transform(Vector3.Forward, Matrix.CreateRotationY(npcYaw));
headYaw = MathF.Atan2(
    Vector3.Dot(toTarget, Vector3.Cross(Vector3.Up, forward)),
    Vector3.Dot(toTarget, forward));
headYaw = Math.Clamp(headYaw, -1.2f, 1.2f); // limit rotation

Simple IK (Hand reaching for object)

instance.Animator.SetBoneProcessor("RightHand", (int idx, ref Matrix m) =>
{
    // Get current hand world position via GlobalTransforms
    var handWorld = instance.Animator.GlobalTransforms[idx] * instance.WorldTransform;
    var handPos = handWorld.Translation;

    // Compute offset toward target
    var toTarget = doorHandlePos - handPos;
    float dist = toTarget.Length();

    if (dist < reachDistance)
    {
        float t = 1f - (dist / reachDistance); // blend by proximity
        m *= Matrix.CreateTranslation(toTarget * t * 0.5f);
    }
});

// Remove when no longer needed
instance.Animator.RemoveBoneProcessor("RightHand");

General Event (all bones)

For effects that need to process every bone (full-body IK chains, ragdoll blending):

instance.Animator.OnBoneTransform += (int idx, ref Matrix m) =>
{
    // Apply wind sway to all bones
    float sway = MathF.Sin(time + idx * 0.3f) * 0.02f;
    m *= Matrix.CreateRotationZ(sway);
};

Animation Curves

Drive float parameters automatically from animation timelines. Curves evaluate at normalized time (0..1) and set state machine parameters each frame.

using ModelKit.Animation;

// Add curves to animation states (fluent API)
var walkState = stateMachine.AddState("Walk", walkClip);
walkState
    .AddCurve("footstep", (0f, 0), (0.24f, 0), (0.25f, 1), (0.26f, 0),
                           (0.74f, 0), (0.75f, 1), (0.76f, 0), (1f, 0))
    .AddCurve("lean", (0f, 0), (0.5f, 0.3f), (1f, 0));

// Read driven parameters in your update loop
float footstep = stateMachine.GetParameter("footstep");
if (footstep > 0.5f)
    PlayFootstepSound();

float lean = stateMachine.GetParameter("lean");
ApplyBodyLean(lean);

Curves support Linear, Step, and CubicSpline interpolation:

// CubicSpline for smooth parameter curves
var curve = new AnimationCurve("blend", keyframes, InterpolationMode.CubicSpline);

Inverse Kinematics (CCD)

The CCDSolver iteratively rotates a bone chain to reach a target position. Works with any skeleton — hands reaching for objects, feet snapping to terrain, tentacles, tails.

using ModelKit.Animation;

// Create solver: chain from RightHand up to RightUpperArm
var ik = new CCDSolver(skeleton, endEffector: "RightHand", root: "RightUpperArm");
ik.MaxIterations = 10;
ik.Tolerance = 0.01f;
ik.AngleLimit = MathF.PI / 4; // 45 degrees per bone per iteration

// Register with animator (hooks into the animation pipeline automatically)
ik.Register(instance.Animator);

// Per frame: just update the target
ik.Target = doorHandleWorldPosition;

// When done reaching:
ik.Unregister(instance.Animator);

The solver integrates as a post-processor — it runs after animation blending and bone processors, before hierarchy multiplication. The animation still plays normally; IK is layered on top.

Multiple solvers can be active simultaneously (e.g. left hand + right hand + foot placement):

var leftHand = new CCDSolver(skeleton, "LeftHand", "LeftUpperArm");
var rightHand = new CCDSolver(skeleton, "RightHand", "RightUpperArm");
var leftFoot = new CCDSolver(skeleton, "LeftFoot", "LeftUpperLeg");

leftHand.Register(animator);
rightHand.Register(animator);
leftFoot.Register(animator);

Morph Targets (Blend Shapes)

Morph targets store per-vertex position/normal/tangent deltas that are blended onto the base mesh by weight — the standard technique for facial expressions, muscle flexing, and shape keys.

using ModelKit;

// A morph target is a set of vertex deltas
var target = new MorphTarget
{
    Name = "Smile",
    PositionDeltas = positionDeltas,   // Vector3[] — one per vertex
    NormalDeltas   = normalDeltas,     // optional
    TangentDeltas  = tangentDeltas,    // optional
};

Targets live on MeshPrimitive.MorphTargets, and the current blend weights on Mesh.MorphTargets / Mesh.MorphWeights. When a loaded animation drives morph weights (glTF exposes these as MorphWeights tracks), SceneInstance applies them automatically every frame — no extra code required:

var instance = scene.CreateInstance();
instance.Play("Talk");     // if the clip animates morph weights, the face deforms automatically
instance.Update(dt);

For manual control (e.g. driving expressions from game logic rather than animation), use MorphApplier directly. It captures the base vertex data once and rewrites the vertex buffer on the CPU whenever weights change:

var applier = new MorphApplier(mesh);

// weights[i] corresponds to mesh.Primitives[*].MorphTargets[i]
float[] weights = { smile, blink, 0f };
applier.Apply(weights);    // no-op if weights are unchanged; restores base data when all zero

Programmatic Scenes

Build a Scene at runtime without a file, e.g. for procedural geometry:

using ModelKit;

// One mesh + one material
var scene = SceneFactory.BuildSingle("cube", meshPrimitive, material);

// A flat scene: one root node per mesh
var scene = SceneFactory.BuildFlat("level", meshes, materials);

Both produce a fully valid Scene (with empty skeletons/animations/cameras/lights) that works everywhere a loaded scene does — instancing, rendering, KBIN saving.


Primitive Splitting

Skinned meshes that exceed the GPU bone limit are automatically split into smaller batches. This happens during loading when LoadOptions.MaxBonesPerPrimitive is set, or you can do it manually:

using ModelKit;

// Auto-split a primitive if it uses more than 128 bones
var parts = PrimitiveSplitter.SplitGpu(primitive, GraphicsDevice, maxBones: 128);

// parts contains the original if no split was needed,
// or multiple new primitives with remapped bone indices

The splitter uses greedy best-fit bin-packing to group triangles into bone-compatible batches:

  1. For each triangle, collect the global bone indices it references
  2. Find the existing batch whose bone set has the smallest union with the triangle's bones
  3. If no batch can fit within the limit, create a new one
  4. Remap vertex bone indices to the new local bone set

Bounds are recomputed per split primitive, and 16-bit indices are used when the vertex count allows it.


Loading

Auto-Discovery

ModelKit scans loaded assemblies for [assembly: FormatLoader(typeof(...))] attributes. Just reference a loader package.

var scene = FormatRegistry.LoadScene(GraphicsDevice, "model.glb");
var scene = await FormatRegistry.LoadSceneAsync(GraphicsDevice, "model.glb");

bool canLoad = FormatRegistry.CanLoad(".glb");
string[] exts = FormatRegistry.SupportedExtensions;

Manual Registration

FormatRegistry.Register(new MyCustomLoader());

Creating a Loader Plugin

public class MyLoader : IFormatLoader
{
    public string[] SupportedExtensions => [".myf"];

    public IAsset Load(GraphicsDevice device, string path, LoadOptions? options = null)
    {
        // Parse file -> build Scene
        return new Scene { Name = Path.GetFileName(path), ... };
    }

    public Task<IAsset> LoadAsync(GraphicsDevice device, string path,
        LoadOptions? options = null, IProgress<float>? progress = null,
        CancellationToken ct = default)
        => Task.Run(() => Load(device, path, options), ct);
}

[assembly: FormatLoader(typeof(MyLoader))]

Load Options

LoadOptions.Default        // balanced defaults
LoadOptions.ForRendering   // compute bounds
LoadOptions.ForCollision   // keep CPU data, skip textures
LoadOptions.ForPreview     // skip animations

Two-Phase Loading (CPU parse → GPU upload)

For asynchronous content pipelines, split parsing from GPU upload. Prepare returns a CPU-side SceneSnapshot with no GraphicsDevice involved — safe to run on a background thread — which you later commit to the GPU on the main thread.

// Background thread: parse only, no GPU
SceneSnapshot snapshot = FormatRegistry.Prepare("model.glb");
SceneSnapshot snapshot = await FormatRegistry.PrepareAsync("model.glb");

// A SceneSnapshot is also the CPU-side representation used for KBIN serialization.

Render Sorting

Sort render commands for correct transparency and optimal draw order:

var commands = new List<RenderCommand>();
instance.CollectRenderCommands(commands);

// Frustum cull
var visible = new List<RenderCommand>();
RenderSorter.CollectVisible(commands, camera.Frustum, visible);

// Sort: opaque front-to-back, transparent back-to-front
var span = CollectionsMarshal.AsSpan(visible);
RenderSorter.Sort(span, camera.Position);

GPU Upload Queue

Upload textures and geometry asynchronously, processing a fixed number of operations per frame to avoid stalls:

var queue = new GpuUploadQueue { MaxOpsPerFrame = 4 };

queue.Enqueue(device => texture.SetData(pixels));
queue.Enqueue(device => vertexBuffer.SetData(vertices));

// In your update loop
queue.ProcessFrame(GraphicsDevice);

Binary Format (KBIN) — Legacy

⚠️ Deprecated. KBIN (KaiserBinary) is ModelKit's original compact binary scene format. It is kept only for backward compatibility with existing .kbin assets and is marked [Obsolete] in the API. The format is frozen and does not support the latest ModelKit scene features — do not use it for new content. Load source assets (e.g. glTF) directly, or build a caching pipeline on top of SceneSnapshot instead.

Existing .kbin files still load via the auto-discovery loader (FormatRegistry.LoadScene("old.kbin")). The direct API remains available but obsolete:

using ModelKit.Data;

#pragma warning disable CS0618  // KBIN is deprecated
KbinFile.Save("scene.kbin", scene);
var scene = KbinFile.Load(GraphicsDevice, "scene.kbin");
#pragma warning restore CS0618

Available Loader Plugins

Package Formats Description
H073.HxGLTF.MonoGame .glb, .gltf glTF 2.0 with PBR, animation, skinning
H073.HxOBJ.MonoGame .obj OBJ with MTL materials, vertex colors, tangents
H073.HxSTL.MonoGame .stl, .stlx, .stlc, .stlxc STL with UV and color support

License

MIT

Contact

Discord: sameplayer

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on H073.ModelKit:

Package Downloads
H073.HxGLTF.MonoGame

MonoGame bridge for HxGLTF – loads glTF/GLB into ModelKit scenes.

H073.HxOBJ.MonoGame

MonoGame bridge for HxOBJ — loads OBJ/MTL files into ModelKit scenes with PBR materials, vertex colors, and tangents.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0 94 7/1/2026
1.2.0 116 3/19/2026
1.0.0 187 3/16/2026
0.1.0 147 3/15/2026