Project

jolt-ruby

0.0
The project is in a healthy, maintained state
A Ruby-friendly 3D physics API backed by Jolt Physics through joltc and FFI.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

~> 1.17
~> 1.0
 Project Readme

jolt-ruby

Ruby bindings for Jolt Physics, built on the joltc C API, ffi, and larb.

jolt-ruby uses SI units in a right-handed, Y-up coordinate system: metres, kilograms, seconds, and radians. Passing centimetres or degrees without conversion will produce incorrect simulation results.

Installation

gem "jolt-ruby"
bundle install

Release builds use platform gems for x86-64/aarch64 Linux, arm64/x86-64 macOS, and x64-mingw-ucrt. On another platform RubyGems falls back to the source gem, which requires CMake 3.16 or newer and a C++17 compiler.

The 1.x public API follows semantic versioning. Native declarations mirror the vendored joltc header; higher-level Ruby APIs are added deliberately rather than exposing unsafe ownership or callback behavior.

First simulation

require "jolt"

system = Jolt::System.new(gravity: [0, -9.81, 0])
floor = system.bodies.create(
  shape: Jolt::Shape.box([50, 0.5, 50]),
  position: [0, -0.5, 0],
  motion: :static,
  layer: :non_moving
)
ball = system.bodies.create(
  shape: Jolt::Shape.sphere(0.5),
  position: [0, 5, 0],
  mass: 2,
  restitution: 0.6,
  ccd: true
)

system.update(1.0 / 60)
puts ball.position

system.destroy
Jolt.shutdown

Jolt.init is called automatically. A System owns its bodies, constraints, characters, and retained shapes. Destroying the system invalidates their Ruby handles; subsequent access raises Jolt::UseAfterDestroyError. Explicit destroy/release is supported, and shutdown is also registered with at_exit.

Layers

Layers.default provides :non_moving and :moving. Custom collision matrices remain entirely native during simulation:

layers = Jolt::Layers.define do |layer|
  layer.broad_phase :static_bp, :dynamic_bp
  layer.object :ground, broad_phase: :static_bp
  layer.object :player, :enemy, broad_phase: :dynamic_bp
  layer.collide :player, with: %i[ground enemy]
  layer.collide :enemy, with: %i[ground player enemy]
end

system = Jolt::System.new(layers:)

Shapes and bodies

Primitive shapes:

Jolt::Shape.box([1, 0.5, 1])
Jolt::Shape.sphere(0.5)
Jolt::Shape.capsule(half_height: 0.8, radius: 0.3)
Jolt::Shape.cylinder(half_height: 1, radius: 0.5)

The advanced builders are convex_hull, mesh, heightfield, compound, scaled, and offset. Hull vertices and mesh vertices accept nested arrays or packed little-endian f32 strings; mesh indices also accept packed u32 strings. Mesh and heightfield shapes are static-only.

Bodies support :static, :kinematic, and :dynamic motion. Common operations include:

body.linear_velocity = [1, 0, 0]
body.angular_velocity = [0, 1, 0]
body.apply_impulse([0, 10, 0])
body.add_force([5, 0, 0], point: [0, 1, 0])
body.friction = 0.4
body.sensor = true
body.kinematic_move_to([2, 0, 0], [0, 0, 0, 1], 1.0 / 60)

Arbitrary Ruby objects can be stored in body.user_data. They never cross the native boundary.

Contacts and queries

Jolt invokes contact listeners from worker threads. jolt-ruby records those calls into a bounded native lock-free queue, then creates immutable Ruby snapshots after System#update:

system.update(1.0 / 60)
system.contact_events.added.each do |contact|
  puts [contact.body_a, contact.body_b, contact.point, contact.normal].inspect
end

warn "contact queue overflow" if system.contact_events.dropped_count.positive?

Synchronous queries return registered Jolt::Body handles:

hit = system.raycast(
  origin: [0, 10, 0],
  direction: [0, -20, 0],
  layer_mask: %i[non_moving moving]
)

nearby = system.overlap_sphere([0, 0, 0], 2, layer_mask: :moving)
system.overlap_point([0, 0, 0]) { |body| puts body.user_data }

Overlap queries use Jolt's broad phase and therefore report bodies whose bounds overlap the query.

Constraints

The collection supports fixed, point, distance, hinge, and slider constraints:

hinge = system.constraints.hinge(
  body_a: door,
  body_b: frame,
  anchor: [0, 1, 0],
  axis: [0, 1, 0],
  limits: (-Math::PI / 2)..(Math::PI / 2)
)
hinge.motor_speed = 1.5
hinge.disable_motor
hinge.destroy

Destroying either body destroys its attached constraints first.

Virtual characters

CharacterVirtual#update uses Jolt's extended update with stair walking and stick-to-floor enabled:

character = Jolt::CharacterVirtual.new(
  system,
  shape: Jolt::Shape.capsule(half_height: 0.8, radius: 0.3),
  position: [0, 2, 0],
  max_slope: Math::PI / 4,
  mass: 70
)

character.velocity = [3, character.velocity.y - 9.81 / 60, 0]
character.update(1.0 / 60)
puts character.ground_state # :on_ground, :sliding, or :in_air

Fixed stepping and rendering

System#update releases Ruby's GVL while Jolt runs. Use FixedStepper to keep simulation stable and interpolate rendering transforms:

stepper = Jolt::FixedStepper.new(hz: 60, max_substeps: 5)

alpha = stepper.advance(frame_delta) { |step| system.update(step) }
render_transform = ball.interpolated(alpha)

Each system permits one native operation at a time. Do not read, mutate, or destroy a system or one of its handles from another thread while System#update or CharacterVirtual#update is running. Such access raises Jolt::ConcurrentAccessError.

The returned transform exposes position and rotation as Larb::Vec3 and Larb::Quat, matching the stagecraft binding contract. See examples/stagecraft_binding.rb.

Development

Initialize the submodule and run the complete build:

git submodule update --init
bundle install
bundle exec rake

Useful tasks:

bundle exec rake native:compile       # build joltc and the helper library
bundle exec rake generator:generate   # regenerate all joltc FFI declarations
bundle exec rake native:verify_layout # compare Clang, C++, and FFI layouts
bundle exec rake native:gem           # build a precompiled platform gem
bundle exec rake native:smoke_gem     # install and run the platform gem
bundle exec rake package:smoke_source # compile, install, and run the source gem
JOLT_STRESS=1 bundle exec rspec spec/stress

The generator reads joltc.h through ffi-clang. CI rejects stale output and compares all generated struct sizes and field offsets against a C++ probe.

License

The gem is available under the MIT License. Jolt Physics and joltc retain their respective upstream licenses.