Project

rgltf

0.0
The project is in a healthy, maintained state
Loads and writes glTF 2.0 and GLB with GPU-ready packed binary accessors.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 Project Readme

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_unlit
  • KHR_texture_transform, including TextureInfo#transform.uv_matrix
  • KHR_lights_punctual
  • KHR_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.rb for 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.json as 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.