Rgltf
Rgltf is a dependency-free Ruby library for loading, inspecting, building, and writing glTF 2.0 JSON and GLB files. Its primary accessor API returns packed, little-endian binary strings suitable for direct GPU upload, while Ruby arrays remain available for inspection and CPU-side processing.
Features
- Load glTF 2.0 JSON and GLB from paths, IO objects, or strings.
- Resolve external files and data URIs with path-traversal protection.
- Read interleaved, sparse, normalized, and matrix accessors.
- Return GPU-ready accessor data without converting it to Ruby objects.
- Traverse scenes and calculate node world transforms.
- Build and write GLB, external-resource glTF, and embedded glTF.
- Validate documents and preserve unknown optional extensions.
- Parse four commonly used Khronos extensions out of the box.
Ruby 3.1 or newer is required.
Installation
Add Rgltf to your bundle:
bundle add rgltf
Alternatively, add it to your Gemfile and run bundle install:
gem "rgltf"
For a standalone installation:
gem install rgltf
Usage
Require the gem before using its public API:
require "rgltf"
Loading documents
document = Rgltf.load("assets/model.glb")
document = Rgltf.load("assets/model.gltf")
document = Rgltf.load_glb(binary_string)
document = Rgltf.load_json(json_string, base_dir: "assets")
External resources are resolved relative to the glTF file. Paths that escape
the base directory are rejected. Buffers are loaded lazily by default; pass
lazy: false to load them immediately.
Pass validate: true to validate while loading:
document = Rgltf.load("assets/model.glb", validate: true)
All public failures inherit from Rgltf::Error. Invalid documents raise
Rgltf::ValidationError. Unknown required extensions raise
Rgltf::UnsupportedExtensionError; pass strict_extensions: false to record
a warning instead.
Reading GPU-ready accessor data
primitive = document.meshes.first.primitives.first
positions = primitive.attributes.fetch(:POSITION)
gpu_bytes = positions.packed # frozen ASCII-8BIT String
format = positions.vertex_format # for example, :float32x3
values = positions.to_a # [[x, y, z], ...]
indices = primitive.indices.packed_as_u32
packed resolves interleaved buffer views, sparse accessors, and matrix column
padding. It preserves normalized integer components; to_a converts normalized
values to floats. Call document.release_buffers! after uploading the data when
the source buffers are no longer needed.
References in the source JSON are resolved to objects while loading:
document.default_scene.nodes.each(&:traverse)
document.each_mesh_instance do |node, mesh, world_matrix|
# world_matrix is a 16-element, column-major Ruby Array
end
Building and writing documents
document = Rgltf::Builder.build do |builder|
builder.asset(generator: "my exporter")
positions = builder.accessor(:VEC3, :f32, positions_binary, min_max: true)
indices = builder.accessor(:SCALAR, :u16, indices_binary, target: :element_array)
texture = builder.texture(
image: File.binread("albedo.png"),
mime_type: "image/png",
sampler: {wrap_s: :repeat, mag_filter: :linear}
)
material = builder.material(
name: "body",
base_color_texture: texture,
metallic_factor: 0.0,
roughness_factor: 0.8
)
mesh = builder.mesh(name: "hull") do |mesh_builder|
mesh_builder.primitive(
attributes: {POSITION: positions},
indices: indices,
material: material
)
end
root = builder.node(name: "root", mesh: mesh)
builder.scene(nodes: [root], default: true)
end
document.write_glb("model.glb")
document.write_gltf("model.gltf")
document.write_gltf("embedded.gltf", embed: true)
Builder buffer views are aligned to four bytes. Writer validation is enabled by
default and can be disabled with validate: false.
Use explicit buffer views for interleaved records and create sparse accessors without constructing dense base buffers:
view = builder.buffer_view(vertex_records, byte_stride: 24, target: :array_buffer)
positions = builder.accessor_from_view(
:VEC3, :f32, buffer_view: view, count: vertex_count
)
normals = builder.accessor_from_view(
:VEC3, :f32, buffer_view: view, count: vertex_count, byte_offset: 12
)
weights = builder.sparse_accessor(
:SCALAR, :f32,
count: vertex_count,
indices: changed_vertex_indices,
values: changed_weights_binary,
index_component_type: :u16
)
Texture-info handles retain per-use coordinates, transforms, and extras:
info = builder.texture_info(
texture,
tex_coord: 1,
extensions: {"KHR_texture_transform" => {"offset" => [0.5, 0.0]}}
)
material = builder.material(base_color_texture: info, double_sided: true)
builder.extensions_used("KHR_texture_transform")
builder.extension("VENDOR_scene", {"revision" => 2}, required: true)
builder.extras("exporter" => "example")
Handles belong to the builder that created them. Passing a handle to another
builder raises ArgumentError before an invalid reference can be emitted.
Extensions
The default extension registry supports:
KHR_materials_unlitKHR_texture_transform, includingTextureInfo#transform.uv_matrixKHR_lights_punctualKHR_materials_emissive_strength
Register custom parsers on a separate registry when isolation is needed:
registry = Rgltf::ExtensionRegistry.new
registry.add("VENDOR_example", MyExtension, on: :material)
document = Rgltf.load(path, extensions: registry)
Unknown optional extension payloads remain Hash objects, so documents can be
read and written without losing them. A handler's serialize method receives
the parsed object when writing. Handlers may also implement decode_primitive
to provide external Draco or meshopt decoding.
Handlers that omit defaults should declare the properties they handle, leaving newer unknown properties untouched:
def self.serialize(object, doc:)
value = object.factor == 1.0 ? {} : {"factor" => object.factor}
serialize_properties(value, handled_keys: %w[factor])
end
Examples
Runnable examples for building, inspecting, and converting assets are available
in examples/.
Development
Clone the repository and install its dependencies:
git clone https://github.com/ydah/rgltf.git
cd rgltf
bundle install
Run the unit and property suites:
bundle exec rake
Property checks use prop_check with shrinking and cover every accessor
component/type combination plus 100 generated Builder-to-GLB round trips. Replay
a reported case with:
PROP_CHECK_SEED=<seed> bundle exec rspec spec/accessor_property_spec.rb
The Khronos glTF-Sample-Assets corpus is pinned to
2bac6f8c57bf471df0d2a1e8a8ec023c7801dddf:
bundle exec rake sample_assets:fetch
GLTF_SAMPLE_ASSETS="$PWD/tmp/gltf-sample-assets" bundle exec rake sample_assets:verify
Corpus verification covers 334 representations, including 310 fully materialized representations and 24 containers that require an external Draco or meshopt decoder. It also runs a 30-model feature manifest.
The repository additionally provides:
benchmark/sample_assets.rbfor repeatable load and accessor benchmarks.- A performance workflow that rejects median regressions greater than 30%.
- A manual official glTF Validator workflow for GLB and glTF writer output.
spec/fixtures/public_api.jsonas the machine-readable public API contract.
Contributing
Bug reports and pull requests are welcome on
GitHub. Please include a focused test for
behavior changes and ensure bundle exec rake passes before submitting a pull
request.
License
Rgltf is available as open source under the terms of the MIT License.