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
<PackageReference Include="H073.ModelKit" Version="1.2.0" />
<PackageVersion Include="H073.ModelKit" Version="1.2.0" />
<PackageReference Include="H073.ModelKit" />
paket add H073.ModelKit --version 1.2.0
#r "nuget: H073.ModelKit, 1.2.0"
#:package H073.ModelKit@1.2.0
#addin nuget:?package=H073.ModelKit&version=1.2.0
#tool nuget:?package=H073.ModelKit&version=1.2.0
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:
- 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
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 | 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)
- K4os.Compression.LZ4 (>= 1.3.8)
- MonoGame.Framework.DesktopGL (>= 3.8.4)
- StbImageSharp (>= 2.27.14)
-
net8.0
- BCnEncoder.Net (>= 2.1.0)
- K4os.Compression.LZ4 (>= 1.3.8)
- MonoGame.Framework.DesktopGL (>= 3.8.4)
- StbImageSharp (>= 2.27.14)
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.