A production-oriented glTF, GLB, and OBJ runtime for Minecraft Forge 1.20.1.
GFBS: glTF loads animated models from Minecraft resources and exposes a reusable Java API for rendering, animation, synchronization, visibility control, custom importers, RenderType selection, culling, optional voxel collision, and dependency-aware plugins. It is the model runtime used by GFBS: Main, but it is designed to be integrated by other mods without depending on GFBS-specific content.
Team maintaining this project: GFBS Mod Series Maintainers
维护此项目的团队: GFBS Mod Series Maintainers
- Source code
- Releases
- Issue tracker
- Pull requests
- 1.5 API guide
- Plugin development guide
- 1.5 migration guide
- 1.5.1 migration guide
| Component | Version |
|---|---|
| GFBS: glTF | 1.5.1 |
| Public API | 1.5.1 |
| Minecraft | 1.20.1 |
| Minecraft Forge | 47.4.21 |
| Java | 17 |
| glTF | 2.0 |
GFBS: glTF does not require Embeddium, Oculus, or Iris. Oculus and Iris are detected at runtime when present.
- glTF 2.0, GLB, and OBJ/MTL loading through Minecraft's resource system.
- Asynchronous client-side loading with request deduplication, caching, invalidation, and resource-pack reload support.
- External files, data URIs, sparse accessors, multiple scenes, indexed geometry, and all seven glTF primitive modes.
- Immutable runtime assets with validated node, mesh, material, skin, animation, texture, and scene references.
- Complete per-instance node graph with indexed/name/path lookup, duplicate-name handling, subtree operations, self/subtree visibility, TRS/matrix/post-transform overrides, morph weights, render, shadow, collision, custom parameters, and per-primitive state.
- Translation, rotation, scale, skinning, morph targets, generated normals, tangents, vertex colors, and two UV sets.
- Complete glTF 2.0 metallic-roughness material ingestion: base color, metallic-roughness, normal, occlusion, emissive, alpha modes/cutoff, double-sided state, and sampler state.
- Independent UV selection for every material texture,
KHR_texture_transform,KHR_materials_unlit, andKHR_materials_emissive_strength. - A dedicated full-bright emissive pass that samples the actual emissive texture instead of making the entire base material full-bright.
- General runtime material switching per primitive: source-material replacement, named reusable
variants, complete sparse overrides for every glTF material property, and
PBR,UNLIT, or game-styleNEONshading without mutating shared assets. - Animation playback, seeking, pausing, transitions, layers, masks, additive blending, fades, and user-defined events.
- Latency-tolerant server-authoritative animation synchronization for entities, block entities, and custom targets, driven by a tick-free monotonic seconds timeline: RTT clock probes, exact
doubletimeline math, a frame-driven heartbeat with fixed logical steps, smooth speed correction, and sequence ordering. - Primitive frustum culling, maximum render distance, optional occlusion queries, and per-part filtering.
- Per-instance, per-node, and per-part RenderType selection with a validated custom RenderType builder.
- Native triangle submission for glTF triangle, strip, and fan primitives—no degenerate quad padding.
- Forge static block/item model loading through
gfbs_gltf:gltf. - Oculus/Iris shadow-map rendering with a dedicated depth-writing caster path.
- Optional bounds, cached voxel, and current-pose precise collision.
- Extensible model importer registry for third-party formats.
- Dependency-aware plugin host with multiple plugins per Forge mod, direct and IMC registration, deterministic ordering, lifecycle rollback, diagnostics, and runtime disable support.
- Type-safe custom extension points, ordered asset processors, client resource-reload hooks, and quarantined render hooks with custom passes that reuse GFBS skinning, morphing, culling, and resident GPU geometry.
- Defensive resource limits and namespace-local resource resolution.
Version 1.5 turns extension support into a first-class subsystem. A plugin has an ID, version, owning Forge mod, dependency list, lifecycle, and any number of typed extensions. One Forge mod can own multiple plugins, while a one-purpose plugin can still ship as its own normal mod JAR.
Plugins can currently integrate at the model-import, post-import asset-processing, client resource reload, and staged rendering boundaries. They may also define new typed extension points for other plugins. Render extensions can add scene-wide passes before, between, or after the built-in passes, or suppress and replace a built-in pass. Custom passes receive resolved model/material/part state and use the existing skin, morph, visibility, culling, lighting, and resident-geometry machinery.
See Plugin development for registration, dependencies, lifecycle rules, custom extension points, and a non-invasive glow-pass example.
GFBS: glTF 1.4 replaces the old per-frame streamed path for ordinary rigid animated meshes with
a resident-GPU path. Triangle geometry without active skinning or morph deformation is compiled
once into a shared VertexBuffer owned by the immutable asset and reused by every GltfInstance.
Node animation changes only the draw matrix; packed light, overlay, tint, alpha, and emissive
strength remain per draw and do not duplicate the resident geometry.
The 1.4 renderer also removes the former 1-to-16 full-mesh emissive repetition, uses one emissive draw, caches world matrices and skin palettes, eliminates defensive array copies from internal hot paths, reuses immediate buffers, and performs allocation-free primitive bounds transforms for culling. Existing defensive-copy public model getters retain their original ownership semantics.
Version 1.4.1 preserves this resident-GPU path on MobileGlues by supplying packed light and overlay through the GLES-compatible four-component integer vertex-attribute entry point. Its values are identical to desktop OpenGL's two-component call, including the implicit zero/one trailing components; no renderer detection or streamed-geometry fallback is required.
Skinning, active morph targets, translucent materials, and custom RenderTypes retain a compatibility streamed path. That fallback is also substantially cheaper than 1.3 because it uses internal read-only primitive views and only copies vertex arrays when deformation actually needs writable data. See Performance architecture for fast-path rules and profiling notes.
- Install Minecraft Forge for Minecraft
1.20.1. - Download a GFBS: glTF JAR from GitHub Releases, or build the project from source.
- Place the JAR in the
modsdirectory of each required client and server.
Mods that use GFBS: glTF as a library should declare gfbs_gltf as a dependency and package models and related textures under their own asset namespace.
Clone the official repository and run:
git clone https://github.com/LytharaLab/GFBS-glTF.git
cd GFBS-glTF
./gradlew buildOn Windows PowerShell:
git clone https://github.com/LytharaLab/GFBS-glTF.git
Set-Location GFBS-glTF
.\gradlew.bat buildThe built JAR is written to build/libs/.
Run the test suite with:
./gradlew testRun the complete Gradle verification lifecycle with:
./gradlew checkPlace a model in a namespaced client resource location such as:
src/main/resources/assets/example/models/reactor.glb
Load it through the client model manager:
ResourceLocation modelId = ResourceLocation.fromNamespaceAndPath(
"example",
"models/reactor.glb"
);
ClientGltfApi.models().load(modelId).thenAccept(asset -> {
GltfInstance instance = new GltfInstance(asset);
instance.animations().play("idle", PlaybackOptions.loop());
// Store the instance in the owning entity, block entity, renderer, or system.
});Advance the instance with elapsed seconds:
instance.update(deltaSeconds);Render it from an entity, block-entity, or other client renderer:
GltfRenderer.render(
instance,
poseStack,
buffers,
packedLight,
packedOverlay
);The caller owns model placement. Apply translation, rotation, and scale to the supplied PoseStack before calling the renderer.
Close an instance when it is no longer used, especially when collision has been enabled:
instance.close();Every GltfInstance owns a complete mutable state graph through instance.nodes(). Imported
GltfAsset, GltfNode, and GltfMaterial objects remain immutable and safe to share; all live
changes are isolated to that instance.
Nodes can be selected by index, exact name, or hierarchy path. A path is the unambiguous choice when an asset legitimately contains duplicate names:
GltfNodeState panel = instance.nodes().requirePath("/reactor/console/service_panel");
panel.selfVisible(false); // Hide only this node's meshes.
panel.subtreeVisible(true); // Keep traversal of children enabled.
panel.translation(0.0f, 0.25f, 0.0f);
panel.alpha(0.8f);
panel.castShadows(false);
panel.parameter("owner", blockEntityId);instance.nodes().snapshot() captures the complete node/primitive/variant state for transactional
changes, presets, rollback, or temporary effects; restore it with instance.nodes().restore(...).
Custom parameter values are copied shallowly because their application-defined object types are
unknown to GFBS: glTF.
Matrix-authored nodes use localMatrix(...) for complete replacement or
postTransform(...) for an additional transform. TRS-authored nodes support independent
translation, quaternion rotation, scale, and morph-weight overrides. The same resolved transforms
and morph weights are used by rendering and enabled collision.
Material switching is not tied to indicator lights. A GltfMaterialVariant can replace the
source material by index or name, override any combination of base color/texture,
metallic-roughness, normal, occlusion, emissive, alpha, double-sided, and shading properties, or
combine replacement and overrides. Variants can be applied to any primitive or an entire node
subtree:
GltfNodeManager nodes = instance.nodes();
nodes.defineVariant("plastic", GltfMaterialVariant.override(
GltfMaterialOverride.builder()
.shadingMode(GltfShadingMode.PBR)
.build()
));
nodes.defineVariant("neon", GltfMaterialVariant.override(
GltfMaterialOverride.builder()
.shadingMode(GltfShadingMode.NEON)
.neonStrength(4.0f)
.build()
));
GltfPrimitiveState surface = nodes.primitive(nodeIndex, meshIndex, primitiveIndex);
surface.variant(powered ? "neon" : "plastic");NEON makes the effective base surface full-bright and feeds the same base color/texture into the
dedicated emissive pass, allowing shader-pack bloom. It deliberately does not create block light
or a dynamic point light. Resolved variants are cached, so repeatedly switching between reusable
states does not rebuild runtime materials every frame.
Version 1.5.1 removes the two properties that made long-running synchronized animations stutter, and it removes the tick loop from the synchronization path entirely.
The timeline is exact. Every timestamp on the animation path is monotonic seconds in double:
SyncedAnimationState, the packet payloads, PlaybackOptions, the AnimationController playhead,
its seek API, and GltfInstance.update. 1.5.0 returned the authoritative time as a float, whose
resolution is relative: after roughly six days of logical animation time a float step is already
62.5 ms. The client controller has a 15 ms dead zone, so it could never settle — it kept chasing a
staircase with speed corrections, which the player sees as stuttering. The same timestamp in double
still carries ~0.1 ns of resolution, so the controller sits inside its dead zone and the pose simply
advances.
The timeline left the tick domain. States, packets and the client estimator carry seconds, not
game ticks, so Level#getGameTime() can no longer influence a running animation: world-time resets,
/time set, TPS drops and tick freezes neither move nor slow down a clip.
The lifecycle is ours. RenderLevelStageEvent.Stage.AFTER_ENTITIES drives
ClientHeartbeat; the heartbeat owns a pause-aware SessionClock and a frame-rate independent
FixedStepScheduler, and it calls ClientAnimationSync.step(...) at a fixed 20 Hz logical cadence
derived from real frame time. There is no TickEvent registration, a stalled or minimized frame
drops its catch-up debt instead of replaying it, and single-player pause freezes the logical clock so
a paused world resumes where it stopped. On the server, ServerClock is the authority and
ServerTimeEstimator solves for a single offset because both endpoints already share the same
real-time rate.
The controller still never seeks every frame: normal drift is absorbed by small live speed changes, a late packet is applied at the position the clip should have reached, and only a catastrophic error triggers one blended rebase, protected by a cooldown.
GFBS: glTF 1.2.0 keeps animation commands server-authoritative without streaming bones or quantizing rendering to the server tick rate. The server sends clip state and time anchors; each client reconstructs the same logical timeline and advances its instance at render-frame frequency.
The 1.2.0 clock inferred the server's logical TPS and expressed the timeline in fractional server ticks; 1.5.1 replaced both with the monotonic seconds timeline described above. Normal drift is still corrected by temporarily changing the live playback speed by a small amount instead of seeking every frame. A late packet is applied at the position the animation should have reached on the server, with a short pose blend to hide the unavoidable first visible jump. Only catastrophic desynchronization can trigger a one-time blended rebase, protected by a cooldown.
This design remains synchronized under high latency—including roughly 500 ms RTT—while avoiding the repetitive 20 Hz snapping present in earlier versions. Network latency can still delay the first moment at which a brand-new server command becomes knowable to the client; no genuine server-authoritative system can display an unseen command before its packet arrives.
After binding with SyncedGltfAnimations.bind(...), continue calling instance.update(deltaSeconds)
from the render-side owner. Do not independently restart or seek the same base animation from block
state or renderer code, because the synchronized target owns that base layer.
Use the gfbs_gltf:gltf geometry loader for glTF or GLB models that should be baked once at
resource-load time and do not need animation playback:
{
"loader": "gfbs_gltf:gltf",
"model": "example:models/block/console.glb",
"textures": {
"particle": "example:block/console",
"material_0": "example:block/console"
},
"material_textures": {
"0": "#material_0"
},
"render_type": "minecraft:cutout",
"scale": 1.0,
"translation": [0.0, 0.0, 0.0],
"flip_v": false,
"shade": true,
"automatic_culling": false
}Place this JSON at assets/example/models/block/console.json, the GLB at
assets/example/models/block/console.glb, and reference example:block/console from the
blockstate or item model. The loader bakes the selected scene's default pose, including node
transforms, default morph weights, and rest-pose skinning. Animation clips are intentionally not
executed on this path.
Every atlas texture used by the glTF material must be declared in the model JSON. Map textures by
material index or glTF material name through material_textures; material_0, material-name,
texture, and particle slots are used as fallbacks. The API guide documents every option.
See the GFBS: glTF 1.x API guide for loading, animation, rendering, synchronization, node state, material variants, importers, culling, RenderTypes, collision, and migration details.
GFBS: glTF 1.3 does not ship a separate no-shader PBR pipeline. Normal rendering uses Minecraft's DefaultVertexFormat.NEW_ENTITY, native triangle draw mode, and the original entity shaders.
- Without a shader pack, base textures retain Minecraft entity lighting while emissive factors and textures are rendered in an independent full-bright additive pass.
MASKmaterials honor their declaredalphaCutoff;BLEND,doubleSided, texture sampler,TEXCOORD_0/TEXCOORD_1, and texture-transform state are retained.- Unlit materials use full-bright base rendering without changing ordinary lit materials.
- With an active Oculus or Iris shader pack, the same entity rendering path remains in use, LabPBR normal/specular companions are created lazily when supported, and emissive textures remain an explicit color pass.
- During an Oculus/Iris shadow pass, GFBS switches to a depth-writing caster path and bypasses color-pass culling and custom RenderType overrides.
- The active shader pack still controls shadow resolution, distance, filtering, and whether block entities participate in its shadow pass.
This keeps the no-shader path compatible with Minecraft's renderer and avoids bundling a second PBR shader implementation.
src/main/java/org/lytharalab/gfbs/gltf/api/ Public integration API
src/main/java/org/lytharalab/gfbs/gltf/core/ Model decoding and animation core
src/main/java/org/lytharalab/gfbs/gltf/client/ Client loading and rendering
src/main/java/org/lytharalab/gfbs/gltf/network/ Animation synchronization
src/main/java/org/lytharalab/gfbs/gltf/collision/ Collision implementation
src/main/resources/ Forge metadata and mixin configuration
src/test/java/ Unit tests
GFBS: glTF draws on and incorporates portions of code from the ModelLoader mod by Bilibili creator 洛谔谔, formerly known as _二千.
Contributions are welcome. Read CONTRIBUTING.md before opening an issue or pull request.
GFBS: glTF is available under the MIT License.
Copyright © 2026 LytharaLab.
Minecraft is a trademark of Microsoft Corporation. This project is not affiliated with or endorsed by Microsoft or Mojang Studios.