H073.ModelKit 1.2.0

Prefix Reserved
dotnet add package H073.ModelKit --version 1.2.0
                    
NuGet\Install-Package H073.ModelKit -Version 1.2.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="1.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="H073.ModelKit" Version="1.2.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 1.2.0
                    
#r "nuget: H073.ModelKit, 1.2.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@1.2.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=1.2.0
                    
Install as a Cake Addin
#tool nuget:?package=H073.ModelKit&version=1.2.0
                    
Install as a Cake Tool

ModelKit — 3D Model Abstraction Layer for MonoGame

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);

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

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)

using ModelKit.Data;

KbinFile.Save("scene.kbin", scene);
var scene = KbinFile.Load(GraphicsDevice, "scene.kbin");

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
1.2.0 33 3/19/2026
1.0.0 110 3/16/2026
0.1.0 114 3/15/2026