jolt-ruby 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitmodules +3 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +228 -0
  5. data/Rakefile +180 -0
  6. data/examples/character_virtual.rb +35 -0
  7. data/examples/falling_sphere.rb +25 -0
  8. data/examples/stagecraft_binding.rb +29 -0
  9. data/ext/jolt_ruby/CMakeLists.txt +42 -0
  10. data/ext/jolt_ruby/character_helper.cpp +44 -0
  11. data/ext/jolt_ruby/constraint_helper.cpp +154 -0
  12. data/ext/jolt_ruby/extconf.rb +56 -0
  13. data/ext/jolt_ruby/helper.cpp +225 -0
  14. data/ext/jolt_ruby/helper.h +103 -0
  15. data/ext/joltc/.github/FUNDING.yml +1 -0
  16. data/ext/joltc/.github/workflows/build.yml +298 -0
  17. data/ext/joltc/.gitignore +272 -0
  18. data/ext/joltc/CMakeLists.txt +431 -0
  19. data/ext/joltc/LICENSE +21 -0
  20. data/ext/joltc/README.md +10 -0
  21. data/ext/joltc/build/cmake_vs2026_arm64.bat +3 -0
  22. data/ext/joltc/build/cmake_vs2026_clang.bat +10 -0
  23. data/ext/joltc/build/cmake_vs2026_x64.bat +3 -0
  24. data/ext/joltc/build/cmake_vs2026_x64_double.bat +3 -0
  25. data/ext/joltc/include/joltc.h +3166 -0
  26. data/ext/joltc/samples/01_HelloWorld/main.cpp +170 -0
  27. data/ext/joltc/samples/CMakeLists.txt +35 -0
  28. data/ext/joltc/src/joltc.c +4 -0
  29. data/ext/joltc/src/joltc.cpp +11812 -0
  30. data/ext/joltc/src/joltc_assert.cpp +271 -0
  31. data/ext/joltc/tests/CMakeLists.txt +77 -0
  32. data/ext/joltc/tests/test_character.cpp +149 -0
  33. data/ext/joltc/tests/test_collision.cpp +146 -0
  34. data/ext/joltc/tests/test_constraints.cpp +353 -0
  35. data/ext/joltc/tests/test_core.cpp +94 -0
  36. data/ext/joltc/tests/test_math.cpp +585 -0
  37. data/ext/joltc/tests/test_physics_system.cpp +465 -0
  38. data/ext/joltc/tests/test_shapes.cpp +789 -0
  39. data/ext/joltc/tests/test_skeleton.cpp +370 -0
  40. data/ext/joltc/tests/test_vehicle.cpp +319 -0
  41. data/generator/generate.rb +237 -0
  42. data/generator/layout_probe.cpp +489 -0
  43. data/generator/verify_layout.rb +32 -0
  44. data/lib/jolt/body.rb +147 -0
  45. data/lib/jolt/body_collection.rb +115 -0
  46. data/lib/jolt/body_dynamics.rb +93 -0
  47. data/lib/jolt/character_virtual.rb +162 -0
  48. data/lib/jolt/constraint.rb +136 -0
  49. data/lib/jolt/constraint_collection.rb +123 -0
  50. data/lib/jolt/contact_events.rb +19 -0
  51. data/lib/jolt/conversions.rb +76 -0
  52. data/lib/jolt/errors.rb +12 -0
  53. data/lib/jolt/fixed_stepper.rb +37 -0
  54. data/lib/jolt/hit.rb +5 -0
  55. data/lib/jolt/layers.rb +182 -0
  56. data/lib/jolt/native/character_functions.rb +16 -0
  57. data/lib/jolt/native/constraint_functions.rb +27 -0
  58. data/lib/jolt/native/core_functions.rb +17 -0
  59. data/lib/jolt/native/generated.rb +1995 -0
  60. data/lib/jolt/native/platform.rb +51 -0
  61. data/lib/jolt/native/query_functions.rb +43 -0
  62. data/lib/jolt/native/types.rb +28 -0
  63. data/lib/jolt/native.rb +94 -0
  64. data/lib/jolt/shape.rb +90 -0
  65. data/lib/jolt/shape_builders.rb +155 -0
  66. data/lib/jolt/system.rb +238 -0
  67. data/lib/jolt/system_characters.rb +25 -0
  68. data/lib/jolt/system_constraints.rb +36 -0
  69. data/lib/jolt/system_contacts.rb +57 -0
  70. data/lib/jolt/system_queries.rb +111 -0
  71. data/lib/jolt/transform.rb +5 -0
  72. data/lib/jolt/version.rb +5 -0
  73. data/lib/jolt.rb +83 -0
  74. data/script/smoke_gem.rb +29 -0
  75. data/script/verify_release.rb +36 -0
  76. metadata +147 -0
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jolt
4
+ class ConstraintCollection
5
+ include Enumerable
6
+
7
+ class << self
8
+ def interval(value, name)
9
+ values = value.is_a?(Range) ? [value.begin, value.end] : value.to_a
10
+ raise InvalidArgumentError, "#{name} must contain two values" unless values.length == 2
11
+
12
+ minimum = Conversions.finite_float(values[0], "#{name} minimum")
13
+ maximum = Conversions.finite_float(values[1], "#{name} maximum")
14
+ raise InvalidArgumentError, "#{name} minimum must not exceed maximum" if minimum > maximum
15
+
16
+ [minimum, maximum]
17
+ rescue NoMethodError
18
+ raise InvalidArgumentError, "#{name} must be a Range or two-element array"
19
+ end
20
+ end
21
+
22
+ def initialize(system)
23
+ @system = system
24
+ end
25
+
26
+ def fixed(body_a:, body_b:, anchor: nil)
27
+ create(:fixed, :JR_Constraint_CreateFixed, body_a, body_b, optional_vec(anchor, "anchor"))
28
+ end
29
+
30
+ def point(body_a:, body_b:, anchor:)
31
+ create(:point, :JR_Constraint_CreatePoint, body_a, body_b, vec(anchor, "anchor"))
32
+ end
33
+
34
+ def distance(body_a:, body_b:, point_a: nil, point_b: nil, limits: nil)
35
+ validate_body_pair(body_a, body_b)
36
+ point_a ||= body_a.position
37
+ point_b ||= body_b.position
38
+ minimum, maximum = limits ? self.class.interval(limits, "limits") : [-1.0, -1.0]
39
+ create(
40
+ :distance, :JR_Constraint_CreateDistance, body_a, body_b,
41
+ vec(point_a, "point_a"), vec(point_b, "point_b"), minimum, maximum
42
+ )
43
+ end
44
+
45
+ def hinge(body_a:, body_b:, anchor:, axis: [0, 1, 0], normal: nil, limits: nil)
46
+ axis = unit_vec(axis, "axis")
47
+ normal = normal ? unit_vec(normal, "normal") : perpendicular(axis)
48
+ has_limits, minimum, maximum = optional_interval(limits)
49
+ create(
50
+ :hinge, :JR_Constraint_CreateHinge, body_a, body_b,
51
+ vec(anchor, "anchor"), axis, normal, has_limits, minimum, maximum
52
+ )
53
+ end
54
+
55
+ def slider(body_a:, body_b:, anchor:, axis:, limits: nil)
56
+ has_limits, minimum, maximum = optional_interval(limits)
57
+ create(
58
+ :slider, :JR_Constraint_CreateSlider, body_a, body_b,
59
+ vec(anchor, "anchor"), unit_vec(axis, "axis"), has_limits, minimum, maximum
60
+ )
61
+ end
62
+
63
+ def each(&block)
64
+ return enum_for(:each) unless block
65
+
66
+ @system.__constraints_snapshot.each(&block)
67
+ end
68
+
69
+ def size
70
+ @system.__constraints_snapshot.size
71
+ end
72
+ alias length size
73
+
74
+ private
75
+
76
+ def create(kind, function, body_a, body_b, *arguments)
77
+ validate_body_pair(body_a, body_b)
78
+ native_arguments = arguments.map { |argument| argument.is_a?(Native::Vec3) ? argument.pointer : argument }
79
+ pointer = Native.public_send(
80
+ function, @system.__native_pointer, body_a.id, body_b.id, *native_arguments
81
+ )
82
+ raise InitializationError, "failed to create #{kind} constraint" if pointer.null?
83
+
84
+ Constraint.new(@system, pointer, kind, body_a, body_b).tap do |constraint|
85
+ @system.__register_constraint(constraint)
86
+ end
87
+ end
88
+
89
+ def validate_body_pair(body_a, body_b)
90
+ [body_a, body_b].each do |body|
91
+ unless body.is_a?(Body) && body.system.equal?(@system) && !body.destroyed?
92
+ raise InvalidArgumentError, "constraint bodies must be live bodies from this system"
93
+ end
94
+ end
95
+ raise InvalidArgumentError, "constraint bodies must be different" if body_a.equal?(body_b)
96
+ end
97
+
98
+ def vec(value, name)
99
+ Conversions.native_vec3(value, name:)
100
+ end
101
+
102
+ def unit_vec(value, name)
103
+ Conversions.native_unit_vec3(value, name:)
104
+ end
105
+
106
+ def optional_vec(value, name)
107
+ value.nil? ? nil : vec(value, name)
108
+ end
109
+
110
+ def optional_interval(value)
111
+ return [false, 0.0, 0.0] if value.nil?
112
+
113
+ [true, *self.class.interval(value, "limits")]
114
+ end
115
+
116
+ def perpendicular(axis)
117
+ x, y, z = %i[x y z].map { |component| axis[component] }
118
+ reference = y.abs < 0.9 ? [0.0, 1.0, 0.0] : [1.0, 0.0, 0.0]
119
+ rx, ry, rz = reference
120
+ unit_vec([y * rz - z * ry, z * rx - x * rz, x * ry - y * rx], "normal")
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jolt
4
+ Contact = Data.define(
5
+ :body_a,
6
+ :body_b,
7
+ :sub_shape_a,
8
+ :sub_shape_b,
9
+ :point,
10
+ :normal,
11
+ :penetration
12
+ )
13
+
14
+ ContactEvents = Data.define(:added, :persisted, :removed, :dropped_count) do
15
+ def self.empty
16
+ new(added: [].freeze, persisted: [].freeze, removed: [].freeze, dropped_count: 0)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "larb"
4
+
5
+ module Jolt
6
+ module Conversions
7
+ module_function
8
+
9
+ def native_vec3(value, name: "vector")
10
+ values = components(value, 3, name)
11
+ Native::Vec3.new.tap do |native|
12
+ native[:x], native[:y], native[:z] = values
13
+ end
14
+ end
15
+
16
+ def native_quat(value, name: "rotation")
17
+ values = components(value, 4, name)
18
+ length = Math.sqrt(values.sum { |component| component * component })
19
+ raise InvalidArgumentError, "#{name} must not be a zero quaternion" if length <= Float::EPSILON
20
+
21
+ values.map! { |component| component / length }
22
+ Native::Quat.new.tap do |native|
23
+ native[:x], native[:y], native[:z], native[:w] = values
24
+ end
25
+ end
26
+
27
+ def native_unit_vec3(value, name:)
28
+ native = native_vec3(value, name:)
29
+ length = Math.sqrt(%i[x y z].sum { |component| native[component]**2 })
30
+ raise InvalidArgumentError, "#{name} must not be zero" if length <= Float::EPSILON
31
+
32
+ %i[x y z].each { |component| native[component] /= length }
33
+ native
34
+ end
35
+
36
+ def vec3(native)
37
+ Larb::Vec3.new(native[:x], native[:y], native[:z])
38
+ end
39
+
40
+ def quat(native)
41
+ Larb::Quat.new(native[:x], native[:y], native[:z], native[:w])
42
+ end
43
+
44
+ def positive_float(value, name)
45
+ number = finite_float(value, name)
46
+ raise InvalidArgumentError, "#{name} must be greater than zero" unless number.positive?
47
+
48
+ number
49
+ end
50
+
51
+ def non_negative_float(value, name)
52
+ number = finite_float(value, name)
53
+ raise InvalidArgumentError, "#{name} must be non-negative" if number.negative?
54
+
55
+ number
56
+ end
57
+
58
+ def finite_float(value, name)
59
+ number = Float(value)
60
+ raise InvalidArgumentError, "#{name} must be finite" unless number.finite?
61
+
62
+ number
63
+ rescue TypeError, ArgumentError
64
+ raise InvalidArgumentError, "#{name} must be a number"
65
+ end
66
+
67
+ def components(value, count, name)
68
+ values = value.respond_to?(:to_a) ? value.to_a : nil
69
+ unless values&.length == count
70
+ raise InvalidArgumentError, "#{name} must contain exactly #{count} components"
71
+ end
72
+
73
+ values.map.with_index { |component, index| finite_float(component, "#{name}[#{index}]") }
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jolt
4
+ class Error < StandardError; end
5
+ class NativeLoadError < Error; end
6
+ class InitializationError < Error; end
7
+ class InvalidArgumentError < Error; end
8
+ class UseAfterDestroyError < Error; end
9
+ class ConcurrentAccessError < Error; end
10
+ class ShapeError < Error; end
11
+ class PhysicsUpdateError < Error; end
12
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jolt
4
+ class FixedStepper
5
+ attr_reader :step, :max_substeps
6
+
7
+ def initialize(hz: 60, max_substeps: 5)
8
+ frequency = Conversions.positive_float(hz, "hz")
9
+ unless max_substeps.is_a?(Integer) && max_substeps.positive?
10
+ raise InvalidArgumentError, "max_substeps must be a positive integer"
11
+ end
12
+
13
+ @step = 1.0 / frequency
14
+ @max_substeps = max_substeps
15
+ @accumulator = 0.0
16
+ end
17
+
18
+ def advance(delta_time)
19
+ raise ArgumentError, "a block is required" unless block_given?
20
+
21
+ @accumulator += Conversions.non_negative_float(delta_time, "delta_time")
22
+ substeps = 0
23
+ while @accumulator >= @step && substeps < @max_substeps
24
+ yield @step
25
+ @accumulator -= @step
26
+ substeps += 1
27
+ end
28
+ @accumulator %= @step if @accumulator >= @step
29
+ @accumulator / @step
30
+ end
31
+
32
+ def reset
33
+ @accumulator = 0.0
34
+ self
35
+ end
36
+ end
37
+ end
data/lib/jolt/hit.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jolt
4
+ Hit = Data.define(:body, :fraction, :point, :normal, :sub_shape_id)
5
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Jolt
6
+ class Layers
7
+ class << self
8
+ def default
9
+ @default ||= define do |layers|
10
+ layers.broad_phase :static_bp, :dynamic_bp
11
+ layers.object :non_moving, broad_phase: :static_bp
12
+ layers.object :moving, broad_phase: :dynamic_bp
13
+ layers.collide :non_moving, with: :moving
14
+ layers.collide :moving, with: :moving
15
+ end
16
+ end
17
+
18
+ def define
19
+ builder = Builder.new
20
+ yield builder
21
+ builder.build
22
+ end
23
+ end
24
+
25
+ attr_reader :object_layers, :broad_phase_layers
26
+
27
+ def initialize(object_layers:, broad_phase_layers:, mappings:, collisions:)
28
+ @object_layers = object_layers.freeze
29
+ @broad_phase_layers = broad_phase_layers.freeze
30
+ @mappings = mappings.freeze
31
+ @collisions = collisions.freeze
32
+ freeze
33
+ end
34
+
35
+ def object_layer_id(name)
36
+ @object_layers.fetch(name.to_sym)
37
+ rescue KeyError
38
+ raise InvalidArgumentError, "unknown object layer: #{name.inspect}"
39
+ end
40
+
41
+ def native_resources
42
+ NativeResources.new(self, @mappings, @collisions)
43
+ end
44
+
45
+ class Builder
46
+ def initialize
47
+ @broad_phase_layers = {}
48
+ @object_layers = {}
49
+ @mappings = {}
50
+ @collisions = Set.new
51
+ end
52
+
53
+ def broad_phase(*names)
54
+ names.each { |name| add_named_layer(@broad_phase_layers, name, 256, "broad-phase") }
55
+ end
56
+
57
+ def object(*names, broad_phase:)
58
+ broad_name = normalize_name(broad_phase)
59
+ names.each do |name|
60
+ object_name = add_named_layer(@object_layers, name, nil, "object")
61
+ @mappings[object_name] = broad_name
62
+ end
63
+ end
64
+
65
+ def collide(layer, with:)
66
+ first = normalize_name(layer)
67
+ Array(with).each do |other|
68
+ second = normalize_name(other)
69
+ @collisions << [first, second].sort.freeze
70
+ end
71
+ end
72
+
73
+ def build
74
+ raise InvalidArgumentError, "at least one broad-phase layer is required" if @broad_phase_layers.empty?
75
+ raise InvalidArgumentError, "at least one object layer is required" if @object_layers.empty?
76
+
77
+ missing_mappings = @mappings.values.reject { |name| @broad_phase_layers.key?(name) }.uniq
78
+ unless missing_mappings.empty?
79
+ raise InvalidArgumentError, "unknown broad-phase layer(s): #{missing_mappings.join(", ")}"
80
+ end
81
+
82
+ collision_names = @collisions.to_a.flatten.uniq
83
+ missing_collisions = collision_names.reject { |name| @object_layers.key?(name) }
84
+ unless missing_collisions.empty?
85
+ raise InvalidArgumentError, "unknown collision layer(s): #{missing_collisions.join(", ")}"
86
+ end
87
+
88
+ Layers.new(
89
+ object_layers: @object_layers.dup,
90
+ broad_phase_layers: @broad_phase_layers.dup,
91
+ mappings: @mappings.dup,
92
+ collisions: @collisions.to_a
93
+ )
94
+ end
95
+
96
+ private
97
+
98
+ def add_named_layer(collection, name, limit, kind)
99
+ normalized = normalize_name(name)
100
+ raise InvalidArgumentError, "duplicate #{kind} layer: #{normalized}" if collection.key?(normalized)
101
+ raise InvalidArgumentError, "too many #{kind} layers" if limit && collection.length >= limit
102
+
103
+ collection[normalized] = collection.length
104
+ normalized
105
+ end
106
+
107
+ def normalize_name(name)
108
+ normalized = name.to_sym
109
+ raise InvalidArgumentError, "layer name must not be empty" if normalized.to_s.empty?
110
+
111
+ normalized
112
+ rescue NoMethodError
113
+ raise InvalidArgumentError, "layer name must be convertible to a symbol"
114
+ end
115
+ end
116
+
117
+ class NativeResources
118
+ attr_reader :broad_phase_interface, :object_pair_filter, :object_vs_broad_phase_filter
119
+
120
+ def initialize(layers, mappings, collisions)
121
+ @destroyed = false
122
+ create_filters(layers, mappings, collisions)
123
+ rescue StandardError
124
+ destroy
125
+ raise
126
+ end
127
+
128
+ def destroy
129
+ return if @destroyed
130
+
131
+ Native.JPH_ObjectVsBroadPhaseLayerFilter_Destroy(@object_vs_broad_phase_filter) if present?(@object_vs_broad_phase_filter)
132
+ Native.JPH_ObjectLayerPairFilter_Destroy(@object_pair_filter) if present?(@object_pair_filter)
133
+ Native.JPH_BroadPhaseLayerInterface_Destroy(@broad_phase_interface) if present?(@broad_phase_interface)
134
+ @destroyed = true
135
+ end
136
+
137
+ def transfer_to_system!
138
+ @destroyed = true
139
+ end
140
+
141
+ private
142
+
143
+ def create_filters(layers, mappings, collisions)
144
+ object_count = layers.object_layers.length
145
+ broad_count = layers.broad_phase_layers.length
146
+ @broad_phase_interface = Native.JPH_BroadPhaseLayerInterfaceTable_Create(object_count, broad_count)
147
+ check_pointer!(@broad_phase_interface, "broad-phase layer interface")
148
+
149
+ mappings.each do |object_name, broad_name|
150
+ Native.JPH_BroadPhaseLayerInterfaceTable_MapObjectToBroadPhaseLayer(
151
+ @broad_phase_interface,
152
+ layers.object_layers.fetch(object_name),
153
+ layers.broad_phase_layers.fetch(broad_name)
154
+ )
155
+ end
156
+
157
+ @object_pair_filter = Native.JPH_ObjectLayerPairFilterTable_Create(object_count)
158
+ check_pointer!(@object_pair_filter, "object layer pair filter")
159
+ collisions.each do |first, second|
160
+ Native.JPH_ObjectLayerPairFilterTable_EnableCollision(
161
+ @object_pair_filter,
162
+ layers.object_layers.fetch(first),
163
+ layers.object_layers.fetch(second)
164
+ )
165
+ end
166
+
167
+ @object_vs_broad_phase_filter = Native.JPH_ObjectVsBroadPhaseLayerFilterTable_Create(
168
+ @broad_phase_interface, broad_count, @object_pair_filter, object_count
169
+ )
170
+ check_pointer!(@object_vs_broad_phase_filter, "object-vs-broad-phase filter")
171
+ end
172
+
173
+ def check_pointer!(pointer, description)
174
+ raise InitializationError, "failed to create #{description}" unless present?(pointer)
175
+ end
176
+
177
+ def present?(pointer)
178
+ pointer && !pointer.null?
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jolt
4
+ module Native
5
+ module CharacterFunctions
6
+ module_function
7
+
8
+ def attach(native)
9
+ native.attach_function :JR_CharacterVirtual_Create,
10
+ %i[pointer pointer pointer pointer float float], :pointer
11
+ native.attach_function :JR_CharacterVirtual_ExtendedUpdate,
12
+ %i[pointer float uint32 pointer], :void, blocking: true
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jolt
4
+ module Native
5
+ module ConstraintFunctions
6
+ module_function
7
+
8
+ def attach(native)
9
+ attach_constructors(native)
10
+ end
11
+
12
+ def attach_constructors(native)
13
+ native.attach_function :JR_Constraint_CreateFixed,
14
+ %i[pointer uint32 uint32 pointer], :pointer
15
+ native.attach_function :JR_Constraint_CreatePoint,
16
+ %i[pointer uint32 uint32 pointer], :pointer
17
+ native.attach_function :JR_Constraint_CreateDistance,
18
+ %i[pointer uint32 uint32 pointer pointer float float], :pointer
19
+ native.attach_function :JR_Constraint_CreateHinge,
20
+ %i[pointer uint32 uint32 pointer pointer pointer bool float float], :pointer
21
+ native.attach_function :JR_Constraint_CreateSlider,
22
+ %i[pointer uint32 uint32 pointer pointer bool float float], :pointer
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jolt
4
+ module Native
5
+ module CoreFunctions
6
+ module_function
7
+
8
+ def attach(native)
9
+ native.attach_function :JR_ContactQueue_Create, [:uint32], :pointer
10
+ native.attach_function :JR_ContactQueue_Destroy, [:pointer], :void
11
+ native.attach_function :JR_ContactQueue_GetListener, [:pointer], :pointer
12
+ native.attach_function :JR_ContactQueue_Pop, %i[pointer pointer], :bool
13
+ native.attach_function :JR_ContactQueue_GetDroppedCount, [:pointer], :uint64
14
+ end
15
+ end
16
+ end
17
+ end