summaryrefslogtreecommitdiffhomepage
path: root/mrbgems/jolt/mrblib/jolt.rb
blob: 95852bbca6dcd88912171e53be97d06fc540437a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# Idiomatic Ruby surface for Jolt Physics (3D), layered on the low-level
# Jolt::World#_* primitives (see src/jolt_bindings.c). Vectors accept Arrays or
# Rl::Vector3/Vector4 and are returned as Rl::Vector3/Vector4 when raylib is
# present (else plain Arrays).
#
#   world = Jolt::World.new(gravity: [0, -9.81, 0])
#   floor = world.body(shape: Jolt.box(100, 1, 100), position: [0, -0.5, 0], motion: Jolt::STATIC)
#   ball  = world.body(shape: Jolt.sphere(0.5), position: [0, 10, 0], velocity: [0, 0, 0])
#   loop { world.step(1.0/60); puts ball.position.y }

module Jolt
  class << self
    # --- shape factories ---
    def box(width, height, depth)        # full dimensions (converted to half-extents)
      _box(width * 0.5, height * 0.5, depth * 0.5)
    end
    def sphere(radius)                    = _sphere(radius)
    def capsule(half_height, radius)      = _capsule(half_height, radius) # cylinder half-height
    def cylinder(half_height, radius)     = _cylinder(half_height, radius)
    # convex hull from points (Array of [x,y,z] or a flat float Array)
    def convex_hull(points) = _convex_hull(points.first.is_a?(Array) ? points.flatten : points)
    # triangle mesh for STATIC bodies (Array of [x,y,z] triples, or flat; 3 verts/tri)
    def mesh(vertices)      = _mesh(vertices.first.is_a?(Array) ? vertices.flatten : vertices)

    # --- vector coercion (Array | Rl::Vector3/4 -> [floats]); out -> Rl type ---
    def v3(v) = v.is_a?(Array) ? [v[0].to_f, v[1].to_f, v[2].to_f] : [v.x.to_f, v.y.to_f, v.z.to_f]
    def v4(v) = v.is_a?(Array) ? [v[0].to_f, v[1].to_f, v[2].to_f, v[3].to_f] : [v.x.to_f, v.y.to_f, v.z.to_f, v.w.to_f]
    # Return Rl::Vector3/4 when raylib is present, else a plain Array. The check
    # is memoized lazily so gem load order doesn't matter.
    def rl?
      @rl = (Object.const_defined?(:Rl) && Rl.const_defined?(:Vector3)) if @rl.nil?
      @rl
    end
    def out3(a) = rl? ? Rl::Vector3.new(a[0], a[1], a[2]) : a
    def out4(a) = rl? ? Rl::Vector4.new(a[0], a[1], a[2], a[3]) : a
  end

  class World
    def initialize(gravity: [0.0, -9.81, 0.0], max_bodies: 10240)
      g = Jolt.v3(gravity)
      # The world OWNS its constraints/ragdolls: keep Ruby refs so they aren't
      # garbage-collected (a Constraint/Ragdoll finalizer detaches it from the
      # simulation, so a dropped handle would silently break the joint). They are
      # released on #remove or when the world itself is collected.
      @joints   = []
      @ragdolls = []
      _setup(g[0], g[1], g[2], max_bodies)
    end

    def _retain_joint(c);   @joints   << c; c; end   # internal
    def _forget_joint(c);   @joints.delete(c);  end   # internal (called by #remove)
    def _forget_ragdoll(r); @ragdolls.delete(r); end  # internal

    def gravity=(v); g = Jolt.v3(v); _set_gravity(g[0], g[1], g[2]); v; end
    def step(dt = 1.0 / 60.0, collision_steps: 1); _step(dt, collision_steps); self; end
    alias update step
    def optimize_broad_phase; _optimize; self; end

    # Create + add a body. shape: a Jolt::Shape; motion: Jolt::STATIC/DYNAMIC/KINEMATIC.
    def body(shape:, position: [0, 0, 0], rotation: [0, 0, 0, 1], motion: Jolt::DYNAMIC,
             restitution: 0.0, friction: 0.2, activate: true, velocity: nil, user_data: nil,
             linear_damping: 0.05, angular_damping: 0.05, mass: nil, ccd: false, sensor: false)
      p = Jolt.v3(position); q = Jolt.v4(rotation)
      id = _add_body(shape, p[0], p[1], p[2], q[0], q[1], q[2], q[3],
                     motion.to_i, restitution.to_f, friction.to_f, activate,
                     linear_damping.to_f, angular_damping.to_f, (mass || 0.0).to_f,
                     ccd ? true : false, sensor ? true : false)
      b = Body.new(self, id)
      b.user_data = user_data if user_data
      b.linear_velocity = velocity if velocity
      b
    end
    alias add_body body

    # Bodies whose shape contains `point` -> Array<Jolt::Body> (overlap query).
    def overlap_point(point)
      p = Jolt.v3(point)
      _overlap_point(p[0], p[1], p[2]).map { |id| Body.new(self, id) }
    end

    # Contacts that ENDED (stopped touching) this step -> Array<Jolt::ContactEnd>.
    # Pair with sensor bodies for trigger enter (contacts) / leave (contacts_ended).
    def contacts_ended
      _contacts_ended.map { |a, b| ContactEnd.new(self, a, b) }
    end

    # --- constraints / joints (return Jolt::Constraint; call #remove to delete) ---
    # The world retains each one (see initialize) so it survives GC; #remove drops it.
    # weld two bodies rigidly at their current relative transform
    def weld(a, b) = _retain_joint(_fixed(a.to_i, b.to_i))
    # ball / point joint at a world-space point (free rotation, fixed point)
    def ball_joint(a, b, point)
      p = Jolt.v3(point); _retain_joint(_point(a.to_i, b.to_i, p[0], p[1], p[2]))
    end
    # keep two world-space attach points within [min, max] metres (rope/rod)
    def distance_joint(a, b, point_a, point_b, min: 0.0, max: nil)
      pa = Jolt.v3(point_a); pb = Jolt.v3(point_b)
      d = max || Math.sqrt((pa[0]-pb[0])**2 + (pa[1]-pb[1])**2 + (pa[2]-pb[2])**2)
      _retain_joint(_distance(a.to_i, b.to_i, pa[0], pa[1], pa[2], pb[0], pb[1], pb[2], min.to_f, d.to_f))
    end
    # hinge (door) about `axis` through world `point`; angle limits in DEGREES
    def hinge(a, b, point, axis, min_deg: -180.0, max_deg: 180.0)
      p = Jolt.v3(point); ax = Jolt.v3(axis)
      _retain_joint(_hinge(a.to_i, b.to_i, p[0], p[1], p[2], ax[0], ax[1], ax[2],
             min_deg * Math::PI / 180.0, max_deg * Math::PI / 180.0))
    end
    # slider (piston) along `axis` through world `point`; limits in METRES
    def slider(a, b, point, axis, min: -1.0e10, max: 1.0e10)
      p = Jolt.v3(point); ax = Jolt.v3(axis)
      _retain_joint(_slider(a.to_i, b.to_i, p[0], p[1], p[2], ax[0], ax[1], ax[2], min.to_f, max.to_f))
    end
    # cone / swing limit about `axis` through world `point`; half-angle in DEGREES
    def cone(a, b, point, axis, half_angle_deg: 45.0)
      p = Jolt.v3(point); ax = Jolt.v3(axis)
      _retain_joint(_cone(a.to_i, b.to_i, p[0], p[1], p[2], ax[0], ax[1], ax[2],
            half_angle_deg * Math::PI / 180.0))
    end

    # Contacts that BEGAN during the last step -> Array<Jolt::Contact>.
    # (Use a body's user_data to map ids back to your game objects.)
    def contacts
      _contacts.map do |a, b, px, py, pz, nx, ny, nz|
        Contact.new(self, a, b, Jolt.out3([px, py, pz]), Jolt.out3([nx, ny, nz]))
      end
    end

    # Create a kinematic character controller (player capsule with stair/slope
    # handling) -> Jolt::Character. `shape` is typically a Jolt.capsule.
    def character(shape:, position: [0, 0, 0], max_slope_deg: 45.0, mass: 70.0)
      p = Jolt.v3(position)
      _character(shape, p[0], p[1], p[2], max_slope_deg.to_f, mass.to_f)
    end

    # Build a ragdoll: a tree of dynamic bodies joined by swing-twist (cone +
    # twist) constraints -> Jolt::Ragdoll. `parts` is an Array of Hashes, listed
    # PARENTS BEFORE CHILDREN (skeleton order). Each part:
    #   name:        unique String/Symbol (referenced by children's :parent)
    #   shape:       a Jolt.capsule/box/sphere
    #   position:, rotation:  world transform of the body (rotation default identity)
    #   parent:      name of the parent part (omit/nil for the single root)
    #   joint:       world-space pivot connecting to the parent (default: position)
    #   twist_axis:  bone axis (default [0,1,0]); plane_axis: perpendicular ([1,0,0])
    #   cone_deg:, plane_deg:  swing limits; twist_min_deg:, twist_max_deg:  twist range
    #   mass:        kg (default: derived from shape); motion: (default DYNAMIC)
    def ragdoll(parts:, user_data: 0)
      names = parts.map { |p| (p[:name] || p["name"]).to_s }
      packed = parts.map do |p|
        pname  = (p[:name] || p["name"]).to_s
        parent = p[:parent] ? names.index(p[:parent].to_s) : -1
        raise ArgumentError, "ragdoll part #{pname.inspect} has unknown parent #{p[:parent].inspect}" \
          if p[:parent] && parent.nil?
        pos   = Jolt.v3(p[:position] || [0, 0, 0])
        rot   = Jolt.v4(p[:rotation] || [0, 0, 0, 1])
        joint = Jolt.v3(p[:joint] || p[:position] || [0, 0, 0])
        twist = Jolt.v3(p[:twist_axis] || [0, 1, 0])
        plane = Jolt.v3(p[:plane_axis] || [1, 0, 0])
        [pname, parent.to_i, p[:shape],
         pos[0], pos[1], pos[2], rot[0], rot[1], rot[2], rot[3],
         (p[:motion] || Jolt::DYNAMIC).to_i, (p[:mass] || 0.0).to_f,
         joint[0], joint[1], joint[2], twist[0], twist[1], twist[2], plane[0], plane[1], plane[2],
         (p[:cone_deg]  || 45.0) * Math::PI / 180.0, (p[:plane_deg] || 45.0) * Math::PI / 180.0,
         (p[:twist_min_deg] || -45.0) * Math::PI / 180.0, (p[:twist_max_deg] || 45.0) * Math::PI / 180.0]
      end
      r = _ragdoll(packed, user_data.to_i)
      @ragdolls << r   # retain so it isn't GC'd out of the world (see initialize)
      r
    end

    # Cast a ray (direction is the full ray vector). -> Jolt::RayHit | nil.
    def raycast(origin, direction)
      o = Jolt.v3(origin); d = Jolt.v3(direction)
      r = _raycast(o[0], o[1], o[2], d[0], d[1], d[2])
      r && RayHit.new(self, r[0], r[1], Jolt.out3(r[2..4]), Jolt.out3(r[5..7]))
    end
  end

  # A rigid body: a body id bound to its world.
  class Body
    attr_reader :id, :world
    def initialize(world, id); @world = world; @id = id; end
    def to_i; @id; end
    def to_int; @id; end

    def position;        Jolt.out3(@world._position(@id)); end
    def center_of_mass;  Jolt.out3(@world._com_position(@id)); end
    def rotation;        Jolt.out4(@world._rotation(@id)); end
    def position=(v); set_transform(position: v); v; end

    def set_transform(position:, rotation: nil, activate: true)
      p = Jolt.v3(position)
      q = rotation ? Jolt.v4(rotation) : @world._rotation(@id)
      @world._set_transform(@id, p[0], p[1], p[2], q[0], q[1], q[2], q[3], activate); self
    end

    def linear_velocity;  Jolt.out3(@world._linear_velocity(@id)); end
    def angular_velocity; Jolt.out3(@world._angular_velocity(@id)); end
    def linear_velocity=(v);  a = Jolt.v3(v); @world._set_linear_velocity(@id, a[0], a[1], a[2]); v; end
    def angular_velocity=(v); a = Jolt.v3(v); @world._set_angular_velocity(@id, a[0], a[1], a[2]); v; end

    def apply_force(v);   a = Jolt.v3(v); @world._add_force(@id, a[0], a[1], a[2]); self; end
    def apply_impulse(v); a = Jolt.v3(v); @world._add_impulse(@id, a[0], a[1], a[2]); self; end
    def apply_torque(v);  a = Jolt.v3(v); @world._add_torque(@id, a[0], a[1], a[2]); self; end

    def active?;     @world._active?(@id); end
    def activate;    @world._activate(@id); self; end
    def deactivate;  @world._deactivate(@id); self; end
    def remove;      @world._remove_body(@id); end

    # arbitrary 64-bit tag (e.g. a flecs entity id or object id) for collision lookup
    def user_data;     @world._user_data(@id); end
    def user_data=(v); @world._set_user_data(@id, v.to_i); v; end

    def motion_type;     @world._motion_type(@id); end
    def motion_type=(mt); @world._set_motion_type(@id, mt.to_i, true); mt; end
    def set_motion_type(mt, activate: true); @world._set_motion_type(@id, mt.to_i, activate); self; end

    def friction;        @world._friction(@id); end
    def friction=(v);    @world._set_friction(@id, v.to_f); v; end
    def restitution;     @world._restitution(@id); end
    def restitution=(v); @world._set_restitution(@id, v.to_f); v; end
    def gravity_factor;     @world._gravity_factor(@id); end
    def gravity_factor=(v); @world._set_gravity_factor(@id, v.to_f); v; end
    # sensor: detects overlaps (contacts/contacts_ended) without a physical response
    def sensor=(v); @world._set_sensor(@id, v ? true : false); v; end
    # continuous collision detection (linear cast) — for fast bodies vs thin walls
    def ccd=(v);    @world._set_ccd(@id, v ? true : false); v; end

    def ==(other); other.respond_to?(:to_i) && other.to_i == @id; end
    def inspect; "#<Jolt::Body #{@id}>"; end
  end

  # Result of World#raycast.
  class RayHit
    attr_reader :body_id, :fraction, :point, :normal
    def initialize(world, body_id, fraction, point, normal = nil)
      @world = world; @body_id = body_id; @fraction = fraction
      @point = point; @normal = normal
    end
    def body; Body.new(@world, @body_id); end
  end

  # A constraint/joint (World#weld/ball_joint/distance_joint/hinge/slider/cone).
  # The world retains it; you don't need to hold the handle to keep the joint alive.
  class Constraint
    def remove                       # detach + destroy now (also done on GC)
      _remove
      w = instance_variable_get(:@world)
      w._forget_joint(self) if w
      self
    end
  end

  # A collision that ENDED this step (from World#contacts_ended). No point/normal.
  class ContactEnd
    attr_reader :body_a_id, :body_b_id
    def initialize(world, a, b); @world = world; @body_a_id = a; @body_b_id = b; end
    def body_a; Body.new(@world, @body_a_id); end
    def body_b; Body.new(@world, @body_b_id); end
    def involves?(x); i = x.to_i; @body_a_id == i || @body_b_id == i; end
    def other(x); i = x.to_i; @body_a_id == i ? body_b : body_a; end
  end

  # Kinematic character controller (Jolt CharacterVirtual). You set its velocity
  # each frame (applying gravity/jump yourself) and call update(dt); it moves and
  # slides along the world, stepping stairs and handling slopes.
  #
  #   ch = world.character(shape: Jolt.capsule(0.6, 0.3), position: [0, 2, 0])
  #   loop do
  #     v = ch.velocity
  #     vy = ch.on_ground? ? (jump? ? 6.0 : 0.0) : v.y - 20.0 * dt
  #     ch.velocity = [input_x * 5, vy, input_z * 5]
  #     ch.update(dt)
  #     world.step(dt)
  #   end
  class Character
    GROUND = { 0 => :on_ground, 1 => :on_steep, 2 => :not_supported, 3 => :in_air }.freeze

    def update(dt = 1.0 / 60.0); _update(dt); self; end
    def position;     Jolt.out3(_position); end
    def position=(v); a = Jolt.v3(v); _set_position(a[0], a[1], a[2]); v; end
    def velocity;     Jolt.out3(_velocity); end
    def velocity=(v); a = Jolt.v3(v); _set_velocity(a[0], a[1], a[2]); v; end

    def ground_state;  GROUND[_ground_state]; end   # :on_ground|:on_steep|:not_supported|:in_air
    def on_ground?;    _ground_state == 0; end
    def supported?;    _supported?; end
    def ground_normal; Jolt.out3(_ground_normal); end

    # Velocity of the surface under the character (moving platform / elevator);
    # zero when airborne. Add it to your movement so the character rides along.
    def ground_velocity; Jolt.out3(_ground_velocity); end
    # The body the character is standing on, or nil when airborne.
    def ground_body
      return nil unless supported?
      Body.new(@world, _ground_body_id)
    end

    # Move with the platform under your feet, then update. Pass your own desired
    # horizontal/vertical velocity (gravity/jump applied by you); the platform's
    # velocity is added on top so the character isn't left behind.
    #   ch.velocity = [input_x*5, vy, input_z*5]
    #   ch.ride(dt)
    #
    # Only a STATIC/KINEMATIC platform's velocity is inherited. A DYNAMIC ground
    # body (a ball you stand on, a constrained pendulum) reports its REACTION to
    # your own weight (and its own bouncing/swinging) as ground_velocity —
    # inheriting that launches the character — so dynamic ground is ignored here
    # and you just stand/collide on it normally.
    def ride(dt = 1.0 / 60.0)
      gb = ground_body
      if gb && gb.motion_type != Jolt::DYNAMIC
        v = velocity; gv = ground_velocity
        self.velocity = [v.x + gv.x, v.y + gv.y, v.z + gv.z]
      end
      _update(dt)
      self
    end

    # Max force (N) the character exerts on dynamic bodies it walks into. Raise
    # it above the Jolt default (100 N) to push heavier props around.
    def max_strength;      _max_strength; end
    def max_strength=(v);  _set_max_strength(v.to_f); v; end
    # Effective mass when dynamic bodies collide with the character (still
    # kinematic to gravity); higher = harder for props to shove the player.
    def mass=(v);          _set_mass(v.to_f); v; end
  end

  # A ragdoll: a tree of dynamic bodies wired with swing-twist joints (from
  # World#ragdoll). Each body is a normal Jolt::Body — read position/rotation to
  # render, apply impulses to fling it around.
  #
  #   rd = world.ragdoll(parts: [
  #     { name: :torso, shape: Jolt.capsule(0.25, 0.18), position: [0, 4.0, 0] },
  #     { name: :head,  shape: Jolt.sphere(0.16), position: [0, 4.5, 0], parent: :torso,
  #       joint: [0, 4.32, 0], twist_axis: [0,1,0], cone_deg: 25, twist_min_deg: -20, twist_max_deg: 20 },
  #   ])
  #   rd.bodies.each { |b| draw_capsule(b.position, b.rotation) }
  class Ragdoll
    # Array<Jolt::Body>, one per part, in skeleton order (memoized).
    def bodies; @bodies ||= (0...body_count).map { |i| Body.new(@world, _body_id(i)) }; end
    def body_count; _body_count; end
    def [](i); bodies[i]; end
    def activate; _activate; self; end   # wake all parts
    def remove                           # take out of the world (also on GC)
      _remove
      w = instance_variable_get(:@world)
      w._forget_ragdoll(self) if w
      self
    end
  end

  # A collision that began this step (from World#contacts).
  class Contact
    attr_reader :body_a_id, :body_b_id, :point, :normal
    def initialize(world, a, b, point, normal)
      @world = world; @body_a_id = a; @body_b_id = b; @point = point; @normal = normal
    end
    def body_a; Body.new(@world, @body_a_id); end
    def body_b; Body.new(@world, @body_b_id); end
    def involves?(x); i = x.to_i; @body_a_id == i || @body_b_id == i; end
    def other(x); i = x.to_i; @body_a_id == i ? body_b : body_a; end
  end
end