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.
- checksums.yaml +7 -0
- data/.gitmodules +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +228 -0
- data/Rakefile +180 -0
- data/examples/character_virtual.rb +35 -0
- data/examples/falling_sphere.rb +25 -0
- data/examples/stagecraft_binding.rb +29 -0
- data/ext/jolt_ruby/CMakeLists.txt +42 -0
- data/ext/jolt_ruby/character_helper.cpp +44 -0
- data/ext/jolt_ruby/constraint_helper.cpp +154 -0
- data/ext/jolt_ruby/extconf.rb +56 -0
- data/ext/jolt_ruby/helper.cpp +225 -0
- data/ext/jolt_ruby/helper.h +103 -0
- data/ext/joltc/.github/FUNDING.yml +1 -0
- data/ext/joltc/.github/workflows/build.yml +298 -0
- data/ext/joltc/.gitignore +272 -0
- data/ext/joltc/CMakeLists.txt +431 -0
- data/ext/joltc/LICENSE +21 -0
- data/ext/joltc/README.md +10 -0
- data/ext/joltc/build/cmake_vs2026_arm64.bat +3 -0
- data/ext/joltc/build/cmake_vs2026_clang.bat +10 -0
- data/ext/joltc/build/cmake_vs2026_x64.bat +3 -0
- data/ext/joltc/build/cmake_vs2026_x64_double.bat +3 -0
- data/ext/joltc/include/joltc.h +3166 -0
- data/ext/joltc/samples/01_HelloWorld/main.cpp +170 -0
- data/ext/joltc/samples/CMakeLists.txt +35 -0
- data/ext/joltc/src/joltc.c +4 -0
- data/ext/joltc/src/joltc.cpp +11812 -0
- data/ext/joltc/src/joltc_assert.cpp +271 -0
- data/ext/joltc/tests/CMakeLists.txt +77 -0
- data/ext/joltc/tests/test_character.cpp +149 -0
- data/ext/joltc/tests/test_collision.cpp +146 -0
- data/ext/joltc/tests/test_constraints.cpp +353 -0
- data/ext/joltc/tests/test_core.cpp +94 -0
- data/ext/joltc/tests/test_math.cpp +585 -0
- data/ext/joltc/tests/test_physics_system.cpp +465 -0
- data/ext/joltc/tests/test_shapes.cpp +789 -0
- data/ext/joltc/tests/test_skeleton.cpp +370 -0
- data/ext/joltc/tests/test_vehicle.cpp +319 -0
- data/generator/generate.rb +237 -0
- data/generator/layout_probe.cpp +489 -0
- data/generator/verify_layout.rb +32 -0
- data/lib/jolt/body.rb +147 -0
- data/lib/jolt/body_collection.rb +115 -0
- data/lib/jolt/body_dynamics.rb +93 -0
- data/lib/jolt/character_virtual.rb +162 -0
- data/lib/jolt/constraint.rb +136 -0
- data/lib/jolt/constraint_collection.rb +123 -0
- data/lib/jolt/contact_events.rb +19 -0
- data/lib/jolt/conversions.rb +76 -0
- data/lib/jolt/errors.rb +12 -0
- data/lib/jolt/fixed_stepper.rb +37 -0
- data/lib/jolt/hit.rb +5 -0
- data/lib/jolt/layers.rb +182 -0
- data/lib/jolt/native/character_functions.rb +16 -0
- data/lib/jolt/native/constraint_functions.rb +27 -0
- data/lib/jolt/native/core_functions.rb +17 -0
- data/lib/jolt/native/generated.rb +1995 -0
- data/lib/jolt/native/platform.rb +51 -0
- data/lib/jolt/native/query_functions.rb +43 -0
- data/lib/jolt/native/types.rb +28 -0
- data/lib/jolt/native.rb +94 -0
- data/lib/jolt/shape.rb +90 -0
- data/lib/jolt/shape_builders.rb +155 -0
- data/lib/jolt/system.rb +238 -0
- data/lib/jolt/system_characters.rb +25 -0
- data/lib/jolt/system_constraints.rb +36 -0
- data/lib/jolt/system_contacts.rb +57 -0
- data/lib/jolt/system_queries.rb +111 -0
- data/lib/jolt/transform.rb +5 -0
- data/lib/jolt/version.rb +5 -0
- data/lib/jolt.rb +83 -0
- data/script/smoke_gem.rb +29 -0
- data/script/verify_release.rb +36 -0
- metadata +147 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
|
|
5
|
+
module Jolt
|
|
6
|
+
module Native
|
|
7
|
+
module Platform
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def tag(cpu: RbConfig::CONFIG.fetch("host_cpu"), os: RbConfig::CONFIG.fetch("host_os"))
|
|
11
|
+
case os
|
|
12
|
+
when /darwin/
|
|
13
|
+
"#{darwin_cpu(cpu)}-darwin"
|
|
14
|
+
when /linux/
|
|
15
|
+
"#{linux_cpu(cpu)}-linux"
|
|
16
|
+
when /mswin|mingw/
|
|
17
|
+
"#{windows_cpu(cpu)}-mingw-ucrt"
|
|
18
|
+
else
|
|
19
|
+
"#{cpu}-#{os}".downcase.gsub(/[^a-z0-9._-]+/, "-")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def darwin_cpu(cpu)
|
|
24
|
+
case cpu
|
|
25
|
+
when "arm64", "aarch64" then "arm64"
|
|
26
|
+
when "x86_64", "amd64" then "x86_64"
|
|
27
|
+
else cpu
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
private_class_method :darwin_cpu
|
|
31
|
+
|
|
32
|
+
def linux_cpu(cpu)
|
|
33
|
+
case cpu
|
|
34
|
+
when "arm64", "aarch64" then "aarch64"
|
|
35
|
+
when "x64", "amd64", "x86_64" then "x86_64"
|
|
36
|
+
else cpu
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
private_class_method :linux_cpu
|
|
40
|
+
|
|
41
|
+
def windows_cpu(cpu)
|
|
42
|
+
case cpu
|
|
43
|
+
when "x64", "amd64", "x86_64" then "x64"
|
|
44
|
+
when "arm64", "aarch64" then "arm64"
|
|
45
|
+
else cpu
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
private_class_method :windows_cpu
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jolt
|
|
4
|
+
module Native
|
|
5
|
+
module QueryFunctions
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def attach(native)
|
|
9
|
+
install_object_layer_filter(native)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def object_layer_filter(native, layer_ids, layer_count)
|
|
13
|
+
context = FFI::MemoryPointer.new(:uint8, layer_count + 4, true)
|
|
14
|
+
context.write_uint32(layer_count)
|
|
15
|
+
layer_ids.each { |id| context.put_uint8(id + 4, 1) }
|
|
16
|
+
filter = native.JPH_ObjectLayerFilter_Create(context)
|
|
17
|
+
raise InitializationError, "failed to create object layer filter" if filter.null?
|
|
18
|
+
|
|
19
|
+
[filter, context]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def install_object_layer_filter(native)
|
|
23
|
+
@object_layer_callback ||= FFI::Function.new(:bool, %i[pointer uint32]) do |context, layer|
|
|
24
|
+
count = context.read_uint32
|
|
25
|
+
layer < count && context.get_uint8(layer + 4) == 1
|
|
26
|
+
end
|
|
27
|
+
@object_layer_procs ||= ObjectLayerFilterProcs.new.tap do |procs|
|
|
28
|
+
procs[:should_collide] = @object_layer_callback
|
|
29
|
+
end
|
|
30
|
+
native.JPH_ObjectLayerFilter_SetProcs(@object_layer_procs.pointer)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def body_collector
|
|
34
|
+
hits = []
|
|
35
|
+
callback = FFI::Function.new(:float, %i[pointer uint32]) do |_context, body_id|
|
|
36
|
+
hits << body_id
|
|
37
|
+
1.0
|
|
38
|
+
end
|
|
39
|
+
[callback, hits]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "generated"
|
|
4
|
+
|
|
5
|
+
module Jolt
|
|
6
|
+
module Native
|
|
7
|
+
Vec3 = Generated::Vec3
|
|
8
|
+
Vec4 = Generated::Vec4
|
|
9
|
+
Quat = Generated::Quat
|
|
10
|
+
Mat4 = Generated::Mat4
|
|
11
|
+
MassProperties = Generated::MassProperties
|
|
12
|
+
PhysicsSystemSettings = Generated::PhysicsSystemSettings
|
|
13
|
+
RayCastResult = Generated::RayCastResult
|
|
14
|
+
IndexedTriangle = Generated::IndexedTriangle
|
|
15
|
+
ObjectLayerFilterProcs = Generated::ObjectLayerFilter_Procs
|
|
16
|
+
|
|
17
|
+
class ContactEvent < FFI::Struct
|
|
18
|
+
layout :type, :uint32,
|
|
19
|
+
:body_a, :uint32,
|
|
20
|
+
:body_b, :uint32,
|
|
21
|
+
:sub_shape_a, :uint32,
|
|
22
|
+
:sub_shape_b, :uint32,
|
|
23
|
+
:point, [:float, 3],
|
|
24
|
+
:normal, [:float, 3],
|
|
25
|
+
:penetration, :float
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/jolt/native.rb
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
require_relative "native/platform"
|
|
6
|
+
require_relative "native/types"
|
|
7
|
+
require_relative "native/core_functions"
|
|
8
|
+
require_relative "native/query_functions"
|
|
9
|
+
require_relative "native/constraint_functions"
|
|
10
|
+
require_relative "native/character_functions"
|
|
11
|
+
|
|
12
|
+
module Jolt
|
|
13
|
+
module Native
|
|
14
|
+
extend FFI::Library
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def load!
|
|
18
|
+
return self if @loaded
|
|
19
|
+
|
|
20
|
+
ffi_lib(*Loader.library_paths)
|
|
21
|
+
Generated.attach(self)
|
|
22
|
+
[
|
|
23
|
+
CoreFunctions,
|
|
24
|
+
QueryFunctions,
|
|
25
|
+
ConstraintFunctions,
|
|
26
|
+
CharacterFunctions
|
|
27
|
+
].each { |functions| functions.attach(self) }
|
|
28
|
+
@loaded = true
|
|
29
|
+
self
|
|
30
|
+
rescue LoadError => error
|
|
31
|
+
raise NativeLoadError, error.message
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def loaded?
|
|
35
|
+
@loaded == true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
module Loader
|
|
41
|
+
module_function
|
|
42
|
+
|
|
43
|
+
def library_paths
|
|
44
|
+
directory = candidate_directories.find { |path| libraries_exist?(path) }
|
|
45
|
+
return library_names unless directory
|
|
46
|
+
|
|
47
|
+
library_names.map { |name| File.join(directory, name) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def candidate_directories
|
|
51
|
+
[
|
|
52
|
+
ENV["JOLT_RUBY_NATIVE_DIR"],
|
|
53
|
+
File.join(__dir__, "native", platform),
|
|
54
|
+
installed_extension_directory,
|
|
55
|
+
File.join(project_root, "tmp", "native", platform, "lib"),
|
|
56
|
+
File.join(project_root, "tmp", "native", platform, "lib", "Release"),
|
|
57
|
+
File.join(project_root, "tmp", "native", platform, "Release")
|
|
58
|
+
].compact
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def installed_extension_directory
|
|
62
|
+
return unless defined?(Gem)
|
|
63
|
+
|
|
64
|
+
specification = Gem.loaded_specs["jolt-ruby"]
|
|
65
|
+
return unless specification
|
|
66
|
+
|
|
67
|
+
File.join(specification.extension_dir, "jolt", "native", platform)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def platform
|
|
71
|
+
Platform.tag
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def project_root
|
|
75
|
+
File.expand_path("../..", __dir__)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def library_names
|
|
79
|
+
case RbConfig::CONFIG.fetch("host_os")
|
|
80
|
+
when /darwin/
|
|
81
|
+
%w[libjoltc.dylib libjolt_ruby_helper.dylib]
|
|
82
|
+
when /mswin|mingw/
|
|
83
|
+
%w[joltc.dll jolt_ruby_helper.dll]
|
|
84
|
+
else
|
|
85
|
+
%w[libjoltc.so libjolt_ruby_helper.so]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def libraries_exist?(directory)
|
|
90
|
+
library_names.all? { |name| File.file?(File.join(directory, name)) }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/jolt/shape.rb
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jolt
|
|
4
|
+
class Shape
|
|
5
|
+
extend ShapeBuilders
|
|
6
|
+
|
|
7
|
+
DEFAULT_CONVEX_RADIUS = 0.05
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def box(half_extents, convex_radius: DEFAULT_CONVEX_RADIUS)
|
|
11
|
+
Jolt.init
|
|
12
|
+
extents = Conversions.native_vec3(half_extents, name: "half_extents")
|
|
13
|
+
%i[x y z].each do |component|
|
|
14
|
+
raise InvalidArgumentError, "half_extents must be positive" unless extents[component].positive?
|
|
15
|
+
end
|
|
16
|
+
radius = Conversions.non_negative_float(convex_radius, "convex_radius")
|
|
17
|
+
build(Native.JPH_BoxShape_Create(extents.pointer, radius), :box)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def sphere(radius)
|
|
21
|
+
Jolt.init
|
|
22
|
+
build(Native.JPH_SphereShape_Create(Conversions.positive_float(radius, "radius")), :sphere)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def capsule(half_height:, radius:)
|
|
26
|
+
Jolt.init
|
|
27
|
+
height = Conversions.non_negative_float(half_height, "half_height")
|
|
28
|
+
radius = Conversions.positive_float(radius, "radius")
|
|
29
|
+
build(Native.JPH_CapsuleShape_Create(height, radius), :capsule)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def cylinder(half_height:, radius:)
|
|
33
|
+
Jolt.init
|
|
34
|
+
height = Conversions.positive_float(half_height, "half_height")
|
|
35
|
+
radius = Conversions.positive_float(radius, "radius")
|
|
36
|
+
build(Native.JPH_CylinderShape_Create(height, radius), :cylinder)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def build(pointer, kind)
|
|
42
|
+
raise ShapeError, "failed to create #{kind} shape" if pointer.null?
|
|
43
|
+
|
|
44
|
+
new(pointer, kind)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
attr_reader :kind
|
|
49
|
+
|
|
50
|
+
def initialize(pointer, kind)
|
|
51
|
+
@pointer = pointer
|
|
52
|
+
@kind = kind
|
|
53
|
+
@released = false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def release
|
|
57
|
+
return if @released
|
|
58
|
+
|
|
59
|
+
Native.JPH_Shape_Destroy(@pointer)
|
|
60
|
+
@released = true
|
|
61
|
+
@pointer = nil
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def released?
|
|
66
|
+
@released
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def volume
|
|
70
|
+
check_alive!
|
|
71
|
+
Native.JPH_Shape_GetVolume(@pointer)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def must_be_static?
|
|
75
|
+
check_alive!
|
|
76
|
+
Native.JPH_Shape_MustBeStatic(@pointer)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def native_pointer
|
|
80
|
+
check_alive!
|
|
81
|
+
@pointer
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def check_alive!
|
|
87
|
+
raise UseAfterDestroyError, "shape has been released" if @released
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jolt
|
|
4
|
+
module ShapeBuilders
|
|
5
|
+
def convex_hull(points, max_convex_radius: Shape::DEFAULT_CONVEX_RADIUS)
|
|
6
|
+
Jolt.init
|
|
7
|
+
coordinates = packed_floats(points, 3, "points")
|
|
8
|
+
raise InvalidArgumentError, "convex hull requires at least 4 points" if coordinates.length < 12
|
|
9
|
+
|
|
10
|
+
point_memory = float_memory(coordinates)
|
|
11
|
+
settings = Native.JPH_ConvexHullShapeSettings_Create(
|
|
12
|
+
point_memory, coordinates.length / 3,
|
|
13
|
+
Conversions.non_negative_float(max_convex_radius, "max_convex_radius")
|
|
14
|
+
)
|
|
15
|
+
create_from_settings(settings, :convex_hull, :JPH_ConvexHullShapeSettings_CreateShape)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def mesh(vertices:, indices:)
|
|
19
|
+
Jolt.init
|
|
20
|
+
coordinates = packed_floats(vertices, 3, "vertices")
|
|
21
|
+
triangles = packed_indices(indices)
|
|
22
|
+
vertex_count = coordinates.length / 3
|
|
23
|
+
raise InvalidArgumentError, "mesh requires at least 3 vertices" if vertex_count < 3
|
|
24
|
+
raise InvalidArgumentError, "mesh requires at least 1 triangle" if triangles.empty?
|
|
25
|
+
raise InvalidArgumentError, "mesh index is out of bounds" if triangles.any? { |index| index >= vertex_count }
|
|
26
|
+
|
|
27
|
+
vertex_memory = float_memory(coordinates)
|
|
28
|
+
triangle_memory = indexed_triangle_memory(triangles)
|
|
29
|
+
settings = Native.JPH_MeshShapeSettings_Create2(
|
|
30
|
+
vertex_memory, vertex_count, triangle_memory, triangles.length / 3
|
|
31
|
+
)
|
|
32
|
+
create_from_settings(settings, :mesh, :JPH_MeshShapeSettings_CreateShape)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def heightfield(samples:, size:, offset: [0, 0, 0], scale: [1, 1, 1])
|
|
36
|
+
Jolt.init
|
|
37
|
+
unless size.is_a?(Integer) && size >= 4
|
|
38
|
+
raise InvalidArgumentError, "size must be an integer greater than or equal to 4"
|
|
39
|
+
end
|
|
40
|
+
values = packed_floats(samples, 1, "samples")
|
|
41
|
+
unless values.length == size * size
|
|
42
|
+
raise InvalidArgumentError, "samples must contain size * size values"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sample_memory = float_memory(values)
|
|
46
|
+
offset = Conversions.native_vec3(offset, name: "offset")
|
|
47
|
+
scale = Conversions.native_vec3(scale, name: "scale")
|
|
48
|
+
settings = Native.JPH_HeightFieldShapeSettings_Create(
|
|
49
|
+
sample_memory, offset.pointer, scale.pointer, size, nil
|
|
50
|
+
)
|
|
51
|
+
create_from_settings(settings, :heightfield, :JPH_HeightFieldShapeSettings_CreateShape)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def compound(children)
|
|
55
|
+
Jolt.init
|
|
56
|
+
children = children.to_a
|
|
57
|
+
raise InvalidArgumentError, "compound requires at least one child" if children.empty?
|
|
58
|
+
|
|
59
|
+
settings = Native.JPH_StaticCompoundShapeSettings_Create
|
|
60
|
+
raise ShapeError, "failed to create compound settings" if settings.null?
|
|
61
|
+
|
|
62
|
+
children.each_with_index do |child, index|
|
|
63
|
+
shape, position, rotation = child
|
|
64
|
+
raise InvalidArgumentError, "child #{index} shape must be a Jolt::Shape" unless shape.is_a?(Shape)
|
|
65
|
+
|
|
66
|
+
position = Conversions.native_vec3(position || [0, 0, 0], name: "child position")
|
|
67
|
+
rotation = Conversions.native_quat(rotation || [0, 0, 0, 1], name: "child rotation")
|
|
68
|
+
Native.JPH_CompoundShapeSettings_AddShape2(
|
|
69
|
+
settings, position.pointer, rotation.pointer, shape.native_pointer, index
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
build(Native.JPH_StaticCompoundShape_Create(settings), :compound)
|
|
73
|
+
ensure
|
|
74
|
+
Native.JPH_ShapeSettings_Destroy(settings) if settings && !settings.null?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def scaled(shape, scale)
|
|
78
|
+
validate_shape(shape)
|
|
79
|
+
scale = Conversions.native_vec3(scale, name: "scale")
|
|
80
|
+
build(Native.JPH_ScaledShape_Create(shape.native_pointer, scale.pointer), :scaled)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def offset(shape, position: [0, 0, 0], rotation: [0, 0, 0, 1])
|
|
84
|
+
validate_shape(shape)
|
|
85
|
+
position = Conversions.native_vec3(position, name: "position")
|
|
86
|
+
rotation = Conversions.native_quat(rotation)
|
|
87
|
+
pointer = Native.JPH_RotatedTranslatedShape_Create(
|
|
88
|
+
position.pointer, rotation.pointer, shape.native_pointer
|
|
89
|
+
)
|
|
90
|
+
build(pointer, :offset)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def create_from_settings(settings, kind, function)
|
|
96
|
+
raise ShapeError, "failed to create #{kind} settings" if settings.null?
|
|
97
|
+
|
|
98
|
+
build(Native.public_send(function, settings), kind)
|
|
99
|
+
ensure
|
|
100
|
+
Native.JPH_ShapeSettings_Destroy(settings) if settings && !settings.null?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def packed_floats(value, tuple_size, name)
|
|
104
|
+
if value.is_a?(String) && !(value.bytesize % FFI.type_size(:float)).zero?
|
|
105
|
+
raise InvalidArgumentError, "#{name} packed f32 buffer has trailing bytes"
|
|
106
|
+
end
|
|
107
|
+
values = value.is_a?(String) ? value.unpack("e*") : value.to_a.flatten
|
|
108
|
+
unless (values.length % tuple_size).zero?
|
|
109
|
+
raise InvalidArgumentError, "#{name} has an incomplete tuple"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
values.map.with_index { |item, index| Conversions.finite_float(item, "#{name}[#{index}]") }
|
|
113
|
+
rescue NoMethodError
|
|
114
|
+
raise InvalidArgumentError, "#{name} must be an array or packed f32 String"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def packed_indices(value)
|
|
118
|
+
if value.is_a?(String) && !(value.bytesize % FFI.type_size(:uint32)).zero?
|
|
119
|
+
raise InvalidArgumentError, "indices packed u32 buffer has trailing bytes"
|
|
120
|
+
end
|
|
121
|
+
values = value.is_a?(String) ? value.unpack("V*") : value.to_a.flatten
|
|
122
|
+
raise InvalidArgumentError, "indices has an incomplete triangle" unless (values.length % 3).zero?
|
|
123
|
+
unless values.all? { |index| index.is_a?(Integer) && index >= 0 }
|
|
124
|
+
raise InvalidArgumentError, "indices must contain non-negative integers"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
values
|
|
128
|
+
rescue NoMethodError
|
|
129
|
+
raise InvalidArgumentError, "indices must be an array or packed u32 String"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def float_memory(values)
|
|
133
|
+
FFI::MemoryPointer.new(:float, values.length).tap { |memory| memory.write_array_of_float(values) }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def indexed_triangle_memory(indices)
|
|
137
|
+
count = indices.length / 3
|
|
138
|
+
FFI::MemoryPointer.new(Native::IndexedTriangle, count).tap do |memory|
|
|
139
|
+
indices.each_slice(3).with_index do |(i1, i2, i3), index|
|
|
140
|
+
triangle = Native::IndexedTriangle.new(memory + index * Native::IndexedTriangle.size)
|
|
141
|
+
triangle[:i1], triangle[:i2], triangle[:i3] = i1, i2, i3
|
|
142
|
+
triangle[:material_index] = 0
|
|
143
|
+
triangle[:user_data] = 0
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def validate_shape(shape)
|
|
149
|
+
raise InvalidArgumentError, "shape must be a Jolt::Shape" unless shape.is_a?(Shape)
|
|
150
|
+
|
|
151
|
+
shape.native_pointer
|
|
152
|
+
Jolt.init
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
data/lib/jolt/system.rb
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jolt
|
|
4
|
+
class System
|
|
5
|
+
include SystemContacts
|
|
6
|
+
include SystemQueries
|
|
7
|
+
include SystemConstraints
|
|
8
|
+
include SystemCharacters
|
|
9
|
+
|
|
10
|
+
DEFAULT_LIMITS = {
|
|
11
|
+
max_bodies: 10_240,
|
|
12
|
+
max_body_pairs: 65_536,
|
|
13
|
+
max_contact_constraints: 10_240
|
|
14
|
+
}.freeze
|
|
15
|
+
MAX_UINT32 = 0xffff_ffff
|
|
16
|
+
MAX_INT32 = 0x7fff_ffff
|
|
17
|
+
|
|
18
|
+
attr_reader :layers, :bodies, :constraints, :contact_events
|
|
19
|
+
|
|
20
|
+
def initialize(max_bodies: DEFAULT_LIMITS[:max_bodies],
|
|
21
|
+
max_body_pairs: DEFAULT_LIMITS[:max_body_pairs],
|
|
22
|
+
max_contact_constraints: DEFAULT_LIMITS[:max_contact_constraints],
|
|
23
|
+
gravity: [0, -9.81, 0], layers: Layers.default,
|
|
24
|
+
contact_queue_capacity: 65_536)
|
|
25
|
+
Jolt.init
|
|
26
|
+
@destroyed = false
|
|
27
|
+
@active_thread = nil
|
|
28
|
+
@body_registry = {}
|
|
29
|
+
@constraint_registry = []
|
|
30
|
+
@character_registry = []
|
|
31
|
+
@user_data = {}
|
|
32
|
+
@shapes = {}
|
|
33
|
+
@layers = validate_layers(layers)
|
|
34
|
+
create_native_system(
|
|
35
|
+
max_bodies, max_body_pairs, max_contact_constraints, contact_queue_capacity
|
|
36
|
+
)
|
|
37
|
+
self.gravity = gravity
|
|
38
|
+
@bodies = BodyCollection.new(self)
|
|
39
|
+
@constraints = ConstraintCollection.new(self)
|
|
40
|
+
Jolt.__register_system(self)
|
|
41
|
+
rescue StandardError
|
|
42
|
+
destroy_native_resources
|
|
43
|
+
raise
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def update(delta_time, collision_steps: 1)
|
|
47
|
+
__check_alive!
|
|
48
|
+
delta_time = Conversions.positive_float(delta_time, "delta_time")
|
|
49
|
+
unless collision_steps.is_a?(Integer) && collision_steps.between?(1, MAX_INT32)
|
|
50
|
+
raise InvalidArgumentError, "collision_steps must be a positive 32-bit integer"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
__with_native_operation("recursive System#update is not allowed") do
|
|
54
|
+
@body_registry.each_value(&:__capture_before_step)
|
|
55
|
+
error = Native.JPH_PhysicsSystem_Update(@pointer, delta_time, collision_steps, @job_system)
|
|
56
|
+
@body_registry.each_value(&:__capture_after_step)
|
|
57
|
+
__drain_contact_events
|
|
58
|
+
raise_update_error!(error) unless error.zero?
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def gravity
|
|
64
|
+
__check_alive!
|
|
65
|
+
native = Native::Vec3.new
|
|
66
|
+
Native.JPH_PhysicsSystem_GetGravity(@pointer, native.pointer)
|
|
67
|
+
Conversions.vec3(native)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def gravity=(value)
|
|
71
|
+
__check_alive!
|
|
72
|
+
native = Conversions.native_vec3(value, name: "gravity")
|
|
73
|
+
Native.JPH_PhysicsSystem_SetGravity(@pointer, native.pointer)
|
|
74
|
+
value
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def optimize_broad_phase
|
|
78
|
+
__check_alive!
|
|
79
|
+
Native.JPH_PhysicsSystem_OptimizeBroadPhase(@pointer)
|
|
80
|
+
self
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def destroy
|
|
84
|
+
return if @destroyed
|
|
85
|
+
|
|
86
|
+
__check_alive!
|
|
87
|
+
if @active_thread
|
|
88
|
+
raise ConcurrentAccessError, "cannot destroy a system during a native operation"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
__destroy_all_constraints
|
|
92
|
+
__destroy_all_characters
|
|
93
|
+
@body_registry.each_value(&:__mark_destroyed)
|
|
94
|
+
@body_registry.clear
|
|
95
|
+
@user_data.clear
|
|
96
|
+
destroy_native_resources
|
|
97
|
+
@shapes.each_value { |shape| shape.release unless shape.released? }
|
|
98
|
+
@shapes.clear
|
|
99
|
+
@destroyed = true
|
|
100
|
+
Jolt.__unregister_system(self)
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def destroyed?
|
|
105
|
+
@destroyed
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def __body_interface
|
|
109
|
+
__check_alive!
|
|
110
|
+
@body_interface
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def __register_body(body, shape)
|
|
114
|
+
@body_registry[body.id] = body
|
|
115
|
+
@shapes[shape.object_id] = shape
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def __destroy_body(body)
|
|
119
|
+
return if body.destroyed?
|
|
120
|
+
|
|
121
|
+
__check_alive!
|
|
122
|
+
registered = @body_registry.delete(body.id)
|
|
123
|
+
raise InvalidArgumentError, "body does not belong to this system" unless registered.equal?(body)
|
|
124
|
+
|
|
125
|
+
__destroy_constraints_for(body)
|
|
126
|
+
Native.JPH_BodyInterface_RemoveAndDestroyBody(@body_interface, body.id)
|
|
127
|
+
@user_data.delete(body.id)
|
|
128
|
+
body.__mark_destroyed
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def __body(id)
|
|
133
|
+
__check_alive!
|
|
134
|
+
@body_registry[id]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def __native_pointer
|
|
138
|
+
__check_alive!
|
|
139
|
+
@pointer
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def __bodies_snapshot
|
|
143
|
+
__check_alive!
|
|
144
|
+
@body_registry.values
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def __user_data(id)
|
|
148
|
+
__check_alive!
|
|
149
|
+
@user_data[id]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def __set_user_data(id, value)
|
|
153
|
+
__check_alive!
|
|
154
|
+
@user_data[id] = value
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def __check_alive!
|
|
158
|
+
raise UseAfterDestroyError, "system has been destroyed" if @destroyed
|
|
159
|
+
return unless @active_thread && !@active_thread.equal?(Thread.current)
|
|
160
|
+
|
|
161
|
+
raise ConcurrentAccessError, "system is in use by another thread"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def __with_native_operation(recursive_message = "recursive native operation is not allowed")
|
|
165
|
+
acquired = false
|
|
166
|
+
__check_alive!
|
|
167
|
+
raise Error, recursive_message if @active_thread
|
|
168
|
+
|
|
169
|
+
@active_thread = Thread.current
|
|
170
|
+
acquired = true
|
|
171
|
+
yield
|
|
172
|
+
ensure
|
|
173
|
+
@active_thread = nil if acquired
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
private
|
|
177
|
+
|
|
178
|
+
def validate_layers(layers)
|
|
179
|
+
raise InvalidArgumentError, "layers must be a Jolt::Layers" unless layers.is_a?(Layers)
|
|
180
|
+
|
|
181
|
+
layers
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def create_native_system(max_bodies, max_body_pairs, max_contact_constraints, contact_queue_capacity)
|
|
185
|
+
limits = [max_bodies, max_body_pairs, max_contact_constraints]
|
|
186
|
+
unless limits.all? { |value| value.is_a?(Integer) && value.between?(1, MAX_UINT32) }
|
|
187
|
+
raise InvalidArgumentError, "system limits must be positive 32-bit integers"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
@layer_resources = @layers.native_resources
|
|
191
|
+
@job_system = Native.JPH_JobSystemThreadPool_Create(nil)
|
|
192
|
+
raise InitializationError, "failed to create Jolt job system" if @job_system.null?
|
|
193
|
+
|
|
194
|
+
settings = Native::PhysicsSystemSettings.new
|
|
195
|
+
settings[:max_bodies], settings[:max_body_pairs], settings[:max_contact_constraints] = limits
|
|
196
|
+
settings[:num_body_mutexes] = 0
|
|
197
|
+
settings[:padding] = 0
|
|
198
|
+
settings[:broad_phase_layer_interface] = @layer_resources.broad_phase_interface
|
|
199
|
+
settings[:object_layer_pair_filter] = @layer_resources.object_pair_filter
|
|
200
|
+
settings[:object_vs_broad_phase_layer_filter] = @layer_resources.object_vs_broad_phase_filter
|
|
201
|
+
@pointer = Native.JPH_PhysicsSystem_Create(settings.pointer)
|
|
202
|
+
raise InitializationError, "failed to create Jolt physics system" if @pointer.null?
|
|
203
|
+
|
|
204
|
+
@layer_resources.transfer_to_system!
|
|
205
|
+
@body_interface = Native.JPH_PhysicsSystem_GetBodyInterface(@pointer)
|
|
206
|
+
raise InitializationError, "failed to get Jolt body interface" if @body_interface.null?
|
|
207
|
+
|
|
208
|
+
@narrow_phase_query = Native.JPH_PhysicsSystem_GetNarrowPhaseQuery(@pointer)
|
|
209
|
+
raise InitializationError, "failed to get Jolt narrow-phase query" if @narrow_phase_query.null?
|
|
210
|
+
|
|
211
|
+
@broad_phase_query = Native.JPH_PhysicsSystem_GetBroadPhaseQuery(@pointer)
|
|
212
|
+
raise InitializationError, "failed to get Jolt broad-phase query" if @broad_phase_query.null?
|
|
213
|
+
|
|
214
|
+
__create_contact_queue(contact_queue_capacity)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def destroy_native_resources
|
|
218
|
+
__destroy_contact_queue
|
|
219
|
+
Native.JPH_PhysicsSystem_Destroy(@pointer) if @pointer && !@pointer.null?
|
|
220
|
+
Native.JPH_JobSystem_Destroy(@job_system) if @job_system && !@job_system.null?
|
|
221
|
+
@layer_resources&.destroy
|
|
222
|
+
@pointer = nil
|
|
223
|
+
@job_system = nil
|
|
224
|
+
@body_interface = nil
|
|
225
|
+
@broad_phase_query = nil
|
|
226
|
+
@narrow_phase_query = nil
|
|
227
|
+
@layer_resources = nil
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def raise_update_error!(error)
|
|
231
|
+
names = []
|
|
232
|
+
names << "manifold cache full" unless (error & 1).zero?
|
|
233
|
+
names << "body pair cache full" unless (error & 2).zero?
|
|
234
|
+
names << "contact constraints full" unless (error & 4).zero?
|
|
235
|
+
raise PhysicsUpdateError, "physics update failed: #{names.join(", ")}"
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|