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
<PackageReference Include="H073.ModelKit" Version="2.0.0" />
<PackageVersion Include="H073.ModelKit" Version="2.0.0" />
<PackageReference Include="H073.ModelKit" />
paket add H073.ModelKit --version 2.0.0
#r "nuget: H073.ModelKit, 2.0.0"
#:package H073.ModelKit@2.0.0
#addin nuget:?package=H073.ModelKit&version=2.0.0
#tool nuget:?package=H073.ModelKit&version=2.0.0
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:
- For each triangle, collect the global bone indices it references
- Find the existing batch whose bone set has the smallest union with the triangle's bones
- If no batch can fit within the limit, create a new one
- 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
.kbinassets 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 ofSceneSnapshotinstead.
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 | Versions 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. |
-
net10.0
- BCnEncoder.Net (>= 2.1.0)
- H073.VertexKit (>= 1.0.0)
- K4os.Compression.LZ4 (>= 1.3.8)
- MonoGame.Framework.DesktopGL (>= 3.8.4)
- StbImageSharp (>= 2.30.15)
-
net8.0
- BCnEncoder.Net (>= 2.1.0)
- H073.VertexKit (>= 1.0.0)
- K4os.Compression.LZ4 (>= 1.3.8)
- MonoGame.Framework.DesktopGL (>= 3.8.4)
- StbImageSharp (>= 2.30.15)
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.