JOLT Physics integrated. Hopefully.
This commit is contained in:
parent
807e61a887
commit
ada7896a9b
11 changed files with 1465 additions and 6 deletions
12
CHANGELOG
12
CHANGELOG
|
|
@ -18,6 +18,18 @@ API Changes
|
||||||
database), and lfs.mkdir and lfs.rmdir act on its data overlay, so
|
database), and lfs.mkdir and lfs.rmdir act on its data overlay, so
|
||||||
frameworks that list or create their own directories work packed.
|
frameworks that list or create their own directories work packed.
|
||||||
|
|
||||||
|
- Physics. Any scene node can carry a rigid body on Jolt Physics:
|
||||||
|
static, kinematic (follows its node and pushes) or dynamic (drives its
|
||||||
|
node), shaped as a box, sphere, capsule, cylinder, a convex hull of the
|
||||||
|
node's mesh or the mesh's triangles; mass, friction, bounce,
|
||||||
|
velocities, forces and impulses; triggers; hinge, ball and slider
|
||||||
|
joints with limits; a raycast; onCollision and onTrigger callbacks.
|
||||||
|
The world steps at a fixed 60 Hz between animation and rendering and
|
||||||
|
pauses with the game. New calls: body*, joint*, physics*, and the
|
||||||
|
BODY_*, SHAPE_* and JOINT_* constants; see the Physics chapter. Needs
|
||||||
|
SSE4.1 on x86 (2008 and later); the engine's first C++ (one wrapper
|
||||||
|
file and the library), statically linked, no runtime to install.
|
||||||
|
|
||||||
- 3D scenes. A game can draw a 3D scene between the disc video and the
|
- 3D scenes. A game can draw a 3D scene between the disc video and the
|
||||||
overlay: primitive meshes and script-built geometry, materials with
|
overlay: primitive meshes and script-built geometry, materials with
|
||||||
colour, textures from sprites, the disc or a loaded video, metallic and
|
colour, textures from sprites, the disc or a loaded video, metallic and
|
||||||
|
|
|
||||||
519
docs/Manual.adoc
519
docs/Manual.adoc
|
|
@ -797,6 +797,95 @@ Put `.glb` files in the game directory like any other asset; `modelLoad`
|
||||||
takes the same game-relative names as `spriteLoad`, and a packed `.game`
|
takes the same game-relative names as `spriteLoad`, and a packed `.game`
|
||||||
carries them along.
|
carries them along.
|
||||||
|
|
||||||
|
[#physics]
|
||||||
|
=== Physics
|
||||||
|
|
||||||
|
Bodies, joints, triggers and raycasts live in the 3D scene and ride on
|
||||||
|
Jolt Physics. A body attaches to a node the way a mesh does; from then on
|
||||||
|
the node and the body are one thing:
|
||||||
|
|
||||||
|
* A *static* body (`BODY_STATIC`) stays where the node was when the body
|
||||||
|
was made: floors, walls, the cabinet.
|
||||||
|
* A *kinematic* body (`BODY_KINEMATIC`) follows its node wherever the script
|
||||||
|
or an animation moves it, and pushes dynamic bodies out of its way without
|
||||||
|
being pushed itself: a paddle, a moving platform, a character's hand.
|
||||||
|
* A *dynamic* body (`BODY_DYNAMIC`) is moved by the simulation and drives
|
||||||
|
its node: it falls, bounces, slides and gets pushed. Do not move its node
|
||||||
|
yourself; give it velocities, forces or impulses instead.
|
||||||
|
|
||||||
|
Every body has a collision shape sized in world units and scaled by the
|
||||||
|
node: a box, sphere, capsule or cylinder, a convex hull of the node's mesh
|
||||||
|
(`SHAPE_HULL`, everything under the node, so a model instance's root works),
|
||||||
|
or the mesh's exact triangles (`SHAPE_MESH`, static and kinematic bodies
|
||||||
|
only). Units are metres, kilograms and seconds; gravity defaults to
|
||||||
|
`0, -9.81, 0`.
|
||||||
|
|
||||||
|
.Bodies for a crate stack and a paddle
|
||||||
|
[source,lua]
|
||||||
|
----
|
||||||
|
floor = nodeNew()
|
||||||
|
nodeSetMesh(floor, meshBox(14, 0.2, 14), stone)
|
||||||
|
nodeSetPosition(floor, 0, -1.9, 0)
|
||||||
|
bodyNew(floor, BODY_STATIC, SHAPE_BOX, 14, 0.2, 14)
|
||||||
|
|
||||||
|
for i = 1, 6 do
|
||||||
|
local crate = nodeNew()
|
||||||
|
nodeSetMesh(crate, meshBox(0.6, 0.6, 0.6), wood)
|
||||||
|
nodeSetPosition(crate, -1, -1.5 + (i - 1) * 0.62, 0)
|
||||||
|
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.6, 0.6, 0.6)
|
||||||
|
bodySetMass(crate, 5)
|
||||||
|
end
|
||||||
|
|
||||||
|
paddle = nodeNew()
|
||||||
|
nodeSetMesh(paddle, meshBox(0.3, 1.2, 1.6), red)
|
||||||
|
nodeSetPosition(paddle, -4, -1.2, 0)
|
||||||
|
bodyNew(paddle, BODY_KINEMATIC, SHAPE_BOX, 0.3, 1.2, 1.6)
|
||||||
|
|
||||||
|
function onOverlayUpdate()
|
||||||
|
nodeMove(paddle, 0.06, 0, 0) -- The kinematic paddle sweeps through the stack.
|
||||||
|
return OVERLAY_UPDATED
|
||||||
|
end
|
||||||
|
----
|
||||||
|
|
||||||
|
.Mass matters
|
||||||
|
A dynamic body's mass comes from its shape's volume at the density of
|
||||||
|
water, so a box the size of a door weighs 140 kilograms and a nudge will
|
||||||
|
not move it. Set `bodySetMass` on anything you want to feel light, and size
|
||||||
|
impulses to the mass: an impulse of `mass * speed` sets that speed.
|
||||||
|
|
||||||
|
.Timing
|
||||||
|
The world steps at a fixed sixty times a second from the wall clock, after
|
||||||
|
animations have posed their nodes and before the scene renders, so a
|
||||||
|
kinematic body driven by an animation pushes with the right velocity.
|
||||||
|
Pausing the game pauses physics; `physicsSetEnabled(false)` pauses it on
|
||||||
|
its own.
|
||||||
|
|
||||||
|
.Events and queries
|
||||||
|
Define `onCollision(nodeA, nodeB, x, y, z, speed)` and Singe calls it once
|
||||||
|
for each new contact, with the point and the speed the two bodies met at.
|
||||||
|
A body made a trigger with `bodySetTrigger` pushes nothing but reports what
|
||||||
|
enters and leaves it through `onTrigger(trigger, other, entered)`: a finish
|
||||||
|
line, a doorway, a danger zone. `physicsRaycast` finds the first body along
|
||||||
|
a ray; with `sceneUnproject` for the ray, that is mouse picking.
|
||||||
|
|
||||||
|
.Joints
|
||||||
|
`jointHinge`, `jointBall` and `jointSlider` connect two bodies, or a body
|
||||||
|
and the world when the second node is `-1`: doors, pendulums, drawers.
|
||||||
|
Hinges and sliders take limits. A positive hinge angle is a right-hand turn
|
||||||
|
of the first node about the axis; a positive slider distance is along it.
|
||||||
|
|
||||||
|
.Models
|
||||||
|
Bodies on model instances work like any other: `bodyNew(instanceRoot,
|
||||||
|
BODY_DYNAMIC, SHAPE_HULL)` wraps the whole model in a convex hull. Skinned
|
||||||
|
characters are best driven kinematically with a capsule and left to push
|
||||||
|
things rather than be pushed.
|
||||||
|
|
||||||
|
.Where it runs
|
||||||
|
Physics runs on the CPU and needs no GPU, but the library is built for
|
||||||
|
SSE4.1 on x86 (2008 and later) and NEON on the Pi; a machine below that
|
||||||
|
gets an error from the first `bodyNew`. The simulation uses every core
|
||||||
|
but one, so a desktop steps in parallel and a Pi on a single thread.
|
||||||
|
|
||||||
[#migrating]
|
[#migrating]
|
||||||
=== Migrating from Singe 2.10
|
=== Migrating from Singe 2.10
|
||||||
|
|
||||||
|
|
@ -865,6 +954,9 @@ available to `controls.cfg` and to `Framework.singe` alike:
|
||||||
| `RENDER_PIXELATED`, `RENDER_SMOOTH` | Arguments for `spriteQuality` / `videoQuality`.
|
| `RENDER_PIXELATED`, `RENDER_SMOOTH` | Arguments for `spriteQuality` / `videoQuality`.
|
||||||
| `DISC_STOPPED`, `DISC_PLAYING`, `DISC_PAUSED`, `DISC_EJECTED` | Return values of `discGetState`.
|
| `DISC_STOPPED`, `DISC_PLAYING`, `DISC_PAUSED`, `DISC_EJECTED` | Return values of `discGetState`.
|
||||||
| `LIGHT_DIRECTIONAL`, `LIGHT_POINT`, `LIGHT_SPOT` | Arguments for `lightNew`.
|
| `LIGHT_DIRECTIONAL`, `LIGHT_POINT`, `LIGHT_SPOT` | Arguments for `lightNew`.
|
||||||
|
| `BODY_STATIC`, `BODY_DYNAMIC`, `BODY_KINEMATIC` | Body types for `bodyNew`.
|
||||||
|
| `SHAPE_BOX`, `SHAPE_SPHERE`, `SHAPE_CAPSULE`, `SHAPE_CYLINDER`, `SHAPE_HULL`, `SHAPE_MESH` | Collision shapes for `bodyNew`.
|
||||||
|
| `JOINT_HINGE`, `JOINT_BALL`, `JOINT_SLIDER` | Joint types (the `joint*` calls name them directly).
|
||||||
| `SOUND_ERROR_INVALID`, `SOUND_REMOVE_HANDLE` | `-1`, what `soundPlay` returns when no channel is free.
|
| `SOUND_ERROR_INVALID`, `SOUND_REMOVE_HANDLE` | `-1`, what `soundPlay` returns when no channel is free.
|
||||||
| `SINGE_VERSION_MAJOR`, `SINGE_VERSION_MINOR`, `SINGE_VERSION_STRING`, `SINGE_FRAMEWORK_VERSION` | The engine version, as integers, as a string (`"v3.00"`), and as the number `singeVersion()` returns.
|
| `SINGE_VERSION_MAJOR`, `SINGE_VERSION_MINOR`, `SINGE_VERSION_STRING`, `SINGE_FRAMEWORK_VERSION` | The engine version, as integers, as a string (`"v3.00"`), and as the number `singeVersion()` returns.
|
||||||
| `SINGE_DEAD_ZONE` | The `DEAD_ZONE` from `controls.cfg`.
|
| `SINGE_DEAD_ZONE` | The `DEAD_ZONE` from `controls.cfg`.
|
||||||
|
|
@ -1104,6 +1196,256 @@ Stops and forgets the animation. The nodes keep their last pose.
|
||||||
*Since:* 3.00.
|
*Since:* 3.00.
|
||||||
*See also:* <<animationplay,animationPlay>>
|
*See also:* <<animationplay,animationPlay>>
|
||||||
|
|
||||||
|
[#body]
|
||||||
|
=== Body
|
||||||
|
|
||||||
|
A body gives a scene node a place in the physics world. One body per node;
|
||||||
|
static bodies stay put, kinematic bodies follow their node, dynamic bodies
|
||||||
|
drive it. Every call here takes the node handle and raises an error when
|
||||||
|
the node has no body. See <<physics,Physics>> for the model.
|
||||||
|
|
||||||
|
[#bodynew]
|
||||||
|
==== bodyNew
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
bodyNew(node, type, shape, ...)
|
||||||
|
bodyNew(node, BODY_DYNAMIC, SHAPE_BOX, width, height, depth)
|
||||||
|
bodyNew(node, BODY_DYNAMIC, SHAPE_SPHERE, radius)
|
||||||
|
bodyNew(node, BODY_DYNAMIC, SHAPE_CAPSULE, radius, height)
|
||||||
|
bodyNew(node, BODY_DYNAMIC, SHAPE_CYLINDER, radius, height)
|
||||||
|
bodyNew(node, BODY_DYNAMIC, SHAPE_HULL)
|
||||||
|
bodyNew(node, BODY_STATIC, SHAPE_MESH)
|
||||||
|
----
|
||||||
|
|
||||||
|
Gives the node a body, placed where the node is now, with a shape sized in world units and scaled by the node's scale. A second call on the same node replaces the first.
|
||||||
|
|
||||||
|
* `type` -- `BODY_STATIC`, `BODY_DYNAMIC` or `BODY_KINEMATIC`.
|
||||||
|
* `shape` -- a primitive with its sizes (a capsule's or cylinder's height is the total, along Y), `SHAPE_HULL` for a convex hull of the node's mesh and everything under it, or `SHAPE_MESH` for the exact triangles (static and kinematic only).
|
||||||
|
|
||||||
|
*Notes:* Mass comes from the shape's volume at the density of water; set it with `bodySetMass`. Changing the node's scale later does not resize the body. A dynamic body ignores its parent moving.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<bodysetmass,bodySetMass>>, <<bodydelete,bodyDelete>>, <<physicssetgravity,physicsSetGravity>>
|
||||||
|
|
||||||
|
.Example
|
||||||
|
[source,lua]
|
||||||
|
----
|
||||||
|
-- A rubber ball dropped onto the scene.
|
||||||
|
ball = nodeNew()
|
||||||
|
nodeSetMesh(ball, meshSphere(0.3, 32), blue)
|
||||||
|
nodeSetPosition(ball, 0, 3, 0)
|
||||||
|
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.3)
|
||||||
|
bodySetMass(ball, 0.5)
|
||||||
|
bodySetBounce(ball, 0.7)
|
||||||
|
----
|
||||||
|
|
||||||
|
[#bodyapplyforce]
|
||||||
|
==== bodyApplyForce
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
bodyApplyForce(node, fx, fy, fz)
|
||||||
|
bodyApplyForce(node, fx, fy, fz, px, py, pz)
|
||||||
|
----
|
||||||
|
|
||||||
|
A force for this step, in newtons, at the centre of mass or at a world point (which also spins the body). Dynamic bodies only.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<bodyapplyimpulse,bodyApplyImpulse>>
|
||||||
|
|
||||||
|
[#bodyapplyimpulse]
|
||||||
|
==== bodyApplyImpulse
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
bodyApplyImpulse(node, ix, iy, iz)
|
||||||
|
bodyApplyImpulse(node, ix, iy, iz, px, py, pz)
|
||||||
|
----
|
||||||
|
|
||||||
|
An instant change of momentum (mass times speed), at the centre of mass or at a world point. What a hit, a kick or a launch wants. Dynamic bodies only.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<bodyapplyforce,bodyApplyForce>>, <<bodysetvelocity,bodySetVelocity>>
|
||||||
|
|
||||||
|
.Example
|
||||||
|
[source,lua]
|
||||||
|
----
|
||||||
|
-- Flick whatever the mouse is over.
|
||||||
|
local mx, my = mouseGetPosition(0)
|
||||||
|
local ox, oy, oz = sceneUnproject(mx, my, 0)
|
||||||
|
local fx, fy, fz = sceneUnproject(mx, my, 10)
|
||||||
|
local hit, hx, hy, hz = physicsRaycast(ox, oy, oz, fx - ox, fy - oy, fz - oz)
|
||||||
|
if hit then
|
||||||
|
bodyApplyImpulse(hit, 0, 3, 0, hx, hy, hz)
|
||||||
|
end
|
||||||
|
----
|
||||||
|
|
||||||
|
[#bodydelete]
|
||||||
|
==== bodyDelete
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
bodyDelete(node)
|
||||||
|
----
|
||||||
|
|
||||||
|
Removes the body; the node stays. Joints on the body go with it. Deleting the node removes its body too.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<bodynew,bodyNew>>
|
||||||
|
|
||||||
|
[#bodygetangularvelocity]
|
||||||
|
==== bodyGetAngularVelocity
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
x, y, z = bodyGetAngularVelocity(node)
|
||||||
|
----
|
||||||
|
|
||||||
|
How fast the body spins about each world axis, in radians per second.
|
||||||
|
|
||||||
|
*Returns:* Three numbers.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<bodysetangularvelocity,bodySetAngularVelocity>>
|
||||||
|
|
||||||
|
[#bodygetvelocity]
|
||||||
|
==== bodyGetVelocity
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
x, y, z = bodyGetVelocity(node)
|
||||||
|
----
|
||||||
|
|
||||||
|
How fast the body moves, in units per second.
|
||||||
|
|
||||||
|
*Returns:* Three numbers.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<bodysetvelocity,bodySetVelocity>>
|
||||||
|
|
||||||
|
[#bodyisresting]
|
||||||
|
==== bodyIsResting
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
resting = bodyIsResting(node)
|
||||||
|
----
|
||||||
|
|
||||||
|
Whether the simulation has put the body to sleep because it stopped moving. Static and disabled bodies rest.
|
||||||
|
|
||||||
|
*Returns:* Boolean.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<bodysetenabled,bodySetEnabled>>
|
||||||
|
|
||||||
|
[#bodysetangularvelocity]
|
||||||
|
==== bodySetAngularVelocity
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
bodySetAngularVelocity(node, x, y, z)
|
||||||
|
----
|
||||||
|
|
||||||
|
Sets the spin about each world axis, in radians per second, and wakes the body.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<bodygetangularvelocity,bodyGetAngularVelocity>>
|
||||||
|
|
||||||
|
[#bodysetbounce]
|
||||||
|
==== bodySetBounce
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
bodySetBounce(node, bounce)
|
||||||
|
----
|
||||||
|
|
||||||
|
Restitution from `0` (stops dead) to `1` (bounces back with everything it arrived with). Default `0.1`.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<bodysetfriction,bodySetFriction>>
|
||||||
|
|
||||||
|
[#bodysetenabled]
|
||||||
|
==== bodySetEnabled
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
bodySetEnabled(node, enabled)
|
||||||
|
----
|
||||||
|
|
||||||
|
Takes the body out of the world (it stops colliding and moving, its node stays put) and puts it back.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<bodyisresting,bodyIsResting>>
|
||||||
|
|
||||||
|
[#bodysetfriction]
|
||||||
|
==== bodySetFriction
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
bodySetFriction(node, friction)
|
||||||
|
----
|
||||||
|
|
||||||
|
Surface friction, `0` for ice upward; default `0.5`. The two touching bodies' values combine.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<bodysetbounce,bodySetBounce>>
|
||||||
|
|
||||||
|
[#bodysetmass]
|
||||||
|
==== bodySetMass
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
bodySetMass(node, kilograms)
|
||||||
|
----
|
||||||
|
|
||||||
|
Rescales a dynamic body's mass and inertia. Do this for anything meant to feel light: the default is the shape's volume at the density of water.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<bodynew,bodyNew>>, <<bodyapplyimpulse,bodyApplyImpulse>>
|
||||||
|
|
||||||
|
[#bodysettrigger]
|
||||||
|
==== bodySetTrigger
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
bodySetTrigger(node, trigger)
|
||||||
|
----
|
||||||
|
|
||||||
|
Makes the body a trigger: it pushes nothing and collides with nothing, but `onTrigger` reports what enters and leaves it. Usually a static box or sphere.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<ontrigger,ontrigger>>, <<bodynew,bodyNew>>
|
||||||
|
|
||||||
|
.Example
|
||||||
|
[source,lua]
|
||||||
|
----
|
||||||
|
-- A finish line across the track.
|
||||||
|
line = nodeNew()
|
||||||
|
nodeSetPosition(line, 0, 0, -20)
|
||||||
|
bodyNew(line, BODY_STATIC, SHAPE_BOX, 6, 3, 0.5)
|
||||||
|
bodySetTrigger(line, true)
|
||||||
|
|
||||||
|
function onTrigger(trigger, other, entered)
|
||||||
|
if trigger == line and other == car and entered then
|
||||||
|
lapDone()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
----
|
||||||
|
|
||||||
|
[#bodysetvelocity]
|
||||||
|
==== bodySetVelocity
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
bodySetVelocity(node, x, y, z)
|
||||||
|
----
|
||||||
|
|
||||||
|
Sets the velocity in units per second and wakes the body. Kinematic bodies take theirs from their node instead.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<bodygetvelocity,bodyGetVelocity>>, <<bodyapplyimpulse,bodyApplyImpulse>>
|
||||||
|
|
||||||
[#camera]
|
[#camera]
|
||||||
=== Camera
|
=== Camera
|
||||||
|
|
||||||
|
|
@ -1901,6 +2243,100 @@ If you unload the currently selected font, the engine does not automatically pic
|
||||||
*Since:* 2.00
|
*Since:* 2.00
|
||||||
*See also:* <<fontload,fontLoad>>
|
*See also:* <<fontload,fontLoad>>
|
||||||
|
|
||||||
|
[#joint]
|
||||||
|
=== Joint
|
||||||
|
|
||||||
|
Joints connect two bodies, or a body and the world, and hand back a handle.
|
||||||
|
Anchors and axes are given in world space at the moment the joint is made,
|
||||||
|
with the bodies where they are then. Deleting either body deletes the
|
||||||
|
joint.
|
||||||
|
|
||||||
|
[#jointhinge]
|
||||||
|
==== jointHinge
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
joint = jointHinge(nodeA, nodeB, ax, ay, az, dx, dy, dz)
|
||||||
|
----
|
||||||
|
|
||||||
|
A hinge through the world point `(ax, ay, az)` along the axis `(dx, dy, dz)`: a door, a wheel, a lever. `nodeB` may be `-1` for the world.
|
||||||
|
|
||||||
|
*Returns:* The joint handle.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<jointsetlimits,jointSetLimits>>, <<jointdelete,jointDelete>>
|
||||||
|
|
||||||
|
.Example
|
||||||
|
[source,lua]
|
||||||
|
----
|
||||||
|
-- A door hinged to the world along its left edge, opening 100 degrees one way.
|
||||||
|
door = nodeNew()
|
||||||
|
nodeSetMesh(door, meshBox(1.4, 2.2, 0.1), wood)
|
||||||
|
nodeSetPosition(door, 0, 0, 0)
|
||||||
|
bodyNew(door, BODY_DYNAMIC, SHAPE_BOX, 1.4, 2.2, 0.1)
|
||||||
|
bodySetMass(door, 8)
|
||||||
|
hinge = jointHinge(door, -1, -0.7, 0, 0, 0, 1, 0)
|
||||||
|
jointSetLimits(hinge, 0, 100)
|
||||||
|
----
|
||||||
|
|
||||||
|
[#jointball]
|
||||||
|
==== jointBall
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
joint = jointBall(nodeA, nodeB, ax, ay, az)
|
||||||
|
----
|
||||||
|
|
||||||
|
A ball-and-socket at the world point: the bodies may turn any way about it but not separate. A pendulum, a chain link, a ragdoll shoulder. `nodeB` may be `-1` for the world.
|
||||||
|
|
||||||
|
*Returns:* The joint handle.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<jointhinge,jointHinge>>
|
||||||
|
|
||||||
|
[#jointslider]
|
||||||
|
==== jointSlider
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
joint = jointSlider(nodeA, nodeB, ax, ay, az, dx, dy, dz)
|
||||||
|
----
|
||||||
|
|
||||||
|
Lets the bodies slide relative to each other only along the axis through the anchor, keeping their orientation: a drawer, a piston, a lift. `nodeB` may be `-1` for the world.
|
||||||
|
|
||||||
|
*Returns:* The joint handle.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<jointsetlimits,jointSetLimits>>
|
||||||
|
|
||||||
|
[#jointsetlimits]
|
||||||
|
==== jointSetLimits
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
jointSetLimits(joint, low, high)
|
||||||
|
----
|
||||||
|
|
||||||
|
Bounds a hinge's angle in degrees, or a slider's travel in world units, either side of where the joint started; `low` at or below `0`, `high` at or above. A positive hinge angle is a right-hand turn of `nodeA` about the axis; a positive slider distance is along the axis.
|
||||||
|
|
||||||
|
*Notes:* A ball joint has no limits.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<jointhinge,jointHinge>>, <<jointslider,jointSlider>>
|
||||||
|
|
||||||
|
[#jointdelete]
|
||||||
|
==== jointDelete
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
jointDelete(joint)
|
||||||
|
----
|
||||||
|
|
||||||
|
Removes the joint; the bodies are free of each other again.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<jointhinge,jointHinge>>
|
||||||
|
|
||||||
[#keyboard]
|
[#keyboard]
|
||||||
=== Keyboard
|
=== Keyboard
|
||||||
|
|
||||||
|
|
@ -3264,6 +3700,55 @@ Higher resolutions give finer control over sprite placement and sharper text at
|
||||||
overlaySetResolution(discGetWidth(), discGetHeight())
|
overlaySetResolution(discGetWidth(), discGetHeight())
|
||||||
----
|
----
|
||||||
|
|
||||||
|
[#physicsref]
|
||||||
|
=== Physics
|
||||||
|
|
||||||
|
World-level calls. Physics is always initialised with the engine; there is
|
||||||
|
nothing to enable before the first `bodyNew`. See <<physics,Physics>> in
|
||||||
|
Game Development for how bodies, nodes and the frame fit together.
|
||||||
|
|
||||||
|
[#physicsraycast]
|
||||||
|
==== physicsRaycast
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
node, hx, hy, hz, nx, ny, nz = physicsRaycast(x, y, z, dx, dy, dz)
|
||||||
|
node, hx, hy, hz, nx, ny, nz = physicsRaycast(x, y, z, dx, dy, dz, maxDistance)
|
||||||
|
----
|
||||||
|
|
||||||
|
The first body along a ray from `(x, y, z)` in the direction `(dx, dy, dz)` (any length), within `maxDistance` (default `1000`). Triggers are hit too.
|
||||||
|
|
||||||
|
*Returns:* The body's node, the hit point and the surface normal there, or `nil` when nothing is hit.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<sceneunproject,sceneUnproject>>, <<bodyapplyimpulse,bodyApplyImpulse>>
|
||||||
|
|
||||||
|
[#physicssetenabled]
|
||||||
|
==== physicsSetEnabled
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
physicsSetEnabled(enabled)
|
||||||
|
----
|
||||||
|
|
||||||
|
Pauses and resumes the simulation on its own; the game's pause does the same. Bodies hold still and keep their velocities.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<physicssetgravity,physicsSetGravity>>
|
||||||
|
|
||||||
|
[#physicssetgravity]
|
||||||
|
==== physicsSetGravity
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
physicsSetGravity(x, y, z)
|
||||||
|
----
|
||||||
|
|
||||||
|
The acceleration every dynamic body feels, in units per second squared. Default `0, -9.81, 0`; `0, 0, 0` for space.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<bodynew,bodyNew>>
|
||||||
|
|
||||||
[#scene]
|
[#scene]
|
||||||
=== Scene
|
=== Scene
|
||||||
|
|
||||||
|
|
@ -4917,6 +5402,40 @@ function onMouseMoved(x, y, xr, yr, which)
|
||||||
end
|
end
|
||||||
----
|
----
|
||||||
|
|
||||||
|
[#oncollision]
|
||||||
|
==== onCollision
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
function onCollision(nodeA, nodeB, x, y, z, speed)
|
||||||
|
-- nodeA, nodeB: the two bodies' nodes
|
||||||
|
-- x, y, z: where they touched, in world space
|
||||||
|
-- speed: how fast they met, in units per second
|
||||||
|
end
|
||||||
|
----
|
||||||
|
|
||||||
|
Called once for each new contact between two bodies after a physics step, never for a pair already touching. A crate landing, a ball hitting the cabinet, the player's car clipping a wall. Triggers report through `onTrigger` instead.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<ontrigger,onTrigger>>, <<bodynew,bodyNew>>
|
||||||
|
|
||||||
|
[#ontrigger]
|
||||||
|
==== onTrigger
|
||||||
|
|
||||||
|
[source,text]
|
||||||
|
----
|
||||||
|
function onTrigger(trigger, other, entered)
|
||||||
|
-- trigger: the trigger body's node
|
||||||
|
-- other: the node that entered or left it
|
||||||
|
-- entered: true on the way in, false on the way out
|
||||||
|
end
|
||||||
|
----
|
||||||
|
|
||||||
|
Called when a body enters or leaves a trigger made with `bodySetTrigger`.
|
||||||
|
|
||||||
|
*Since:* 3.00.
|
||||||
|
*See also:* <<bodysettrigger,bodySetTrigger>>, <<oncollision,onCollision>>
|
||||||
|
|
||||||
[#onoverlayupdate]
|
[#onoverlayupdate]
|
||||||
==== onOverlayUpdate
|
==== onOverlayUpdate
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,30 @@ typedef enum ShapeTypeE {
|
||||||
} ShapeTypeE;
|
} ShapeTypeE;
|
||||||
|
|
||||||
|
|
||||||
|
// What happened during a step, drained by the engine after it: a new contact between two bodies
|
||||||
|
// (with the point and how fast they met), or a trigger being entered or left.
|
||||||
|
typedef enum PhysicsEventTypeE {
|
||||||
|
PHYSICS_EVENT_COLLISION = 0,
|
||||||
|
PHYSICS_EVENT_ENTER = 1,
|
||||||
|
PHYSICS_EVENT_LEAVE = 2
|
||||||
|
} PhysicsEventTypeE;
|
||||||
|
|
||||||
|
typedef struct PhysicsEventS {
|
||||||
|
PhysicsEventTypeE type;
|
||||||
|
int32_t nodeA; // The trigger, for enter and leave
|
||||||
|
int32_t nodeB;
|
||||||
|
Vec3T point;
|
||||||
|
float speed;
|
||||||
|
} PhysicsEventT;
|
||||||
|
|
||||||
|
|
||||||
|
typedef enum JointTypeE {
|
||||||
|
JOINT_HINGE = 0,
|
||||||
|
JOINT_BALL = 1,
|
||||||
|
JOINT_SLIDER = 2
|
||||||
|
} JointTypeE;
|
||||||
|
|
||||||
|
|
||||||
bool bodyApplyForce(int32_t node, Vec3T force, const Vec3T *at);
|
bool bodyApplyForce(int32_t node, Vec3T force, const Vec3T *at);
|
||||||
bool bodyApplyImpulse(int32_t node, Vec3T impulse, const Vec3T *at);
|
bool bodyApplyImpulse(int32_t node, Vec3T impulse, const Vec3T *at);
|
||||||
bool bodyDelete(int32_t node);
|
bool bodyDelete(int32_t node);
|
||||||
|
|
@ -65,10 +89,17 @@ bool bodySetBounce(int32_t node, float bounce);
|
||||||
bool bodySetEnabled(int32_t node, bool enabled);
|
bool bodySetEnabled(int32_t node, bool enabled);
|
||||||
bool bodySetFriction(int32_t node, float friction);
|
bool bodySetFriction(int32_t node, float friction);
|
||||||
bool bodySetMass(int32_t node, float kilograms);
|
bool bodySetMass(int32_t node, float kilograms);
|
||||||
|
bool bodySetTrigger(int32_t node, bool trigger);
|
||||||
bool bodySetVelocity(int32_t node, Vec3T velocity);
|
bool bodySetVelocity(int32_t node, Vec3T velocity);
|
||||||
|
bool jointDelete(int32_t joint);
|
||||||
|
int32_t jointNew(JointTypeE type, int32_t nodeA, int32_t nodeB, Vec3T anchor, Vec3T axis);
|
||||||
|
bool jointSetLimits(int32_t joint, float low, float high);
|
||||||
|
bool jointValid(int32_t joint);
|
||||||
bool physicsAvailable(void);
|
bool physicsAvailable(void);
|
||||||
|
int32_t physicsGetEvents(PhysicsEventT *events, int32_t maximum);
|
||||||
bool physicsInit(void);
|
bool physicsInit(void);
|
||||||
void physicsQuit(void);
|
void physicsQuit(void);
|
||||||
|
bool physicsRaycast(Vec3T origin, Vec3T direction, float maxDistance, int32_t *node, Vec3T *point, Vec3T *normal);
|
||||||
void physicsSetEnabled(bool enabled);
|
void physicsSetEnabled(bool enabled);
|
||||||
void physicsSetGravity(Vec3T gravity);
|
void physicsSetGravity(Vec3T gravity);
|
||||||
void physicsUpdate(bool advance);
|
void physicsUpdate(bool advance);
|
||||||
|
|
|
||||||
|
|
@ -33,12 +33,23 @@
|
||||||
#include <Jolt/Physics/PhysicsSystem.h>
|
#include <Jolt/Physics/PhysicsSystem.h>
|
||||||
#include <Jolt/Physics/Body/BodyCreationSettings.h>
|
#include <Jolt/Physics/Body/BodyCreationSettings.h>
|
||||||
#include <Jolt/Physics/Body/BodyLock.h>
|
#include <Jolt/Physics/Body/BodyLock.h>
|
||||||
|
#include <Jolt/Physics/Body/BodyLockMulti.h>
|
||||||
#include <Jolt/Physics/Collision/BroadPhase/BroadPhaseLayer.h>
|
#include <Jolt/Physics/Collision/BroadPhase/BroadPhaseLayer.h>
|
||||||
#include <Jolt/Physics/Collision/ObjectLayer.h>
|
#include <Jolt/Physics/Collision/ObjectLayer.h>
|
||||||
#include <Jolt/Physics/Collision/Shape/BoxShape.h>
|
#include <Jolt/Physics/Collision/Shape/BoxShape.h>
|
||||||
#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
|
#include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
|
||||||
#include <Jolt/Physics/Collision/Shape/CylinderShape.h>
|
#include <Jolt/Physics/Collision/Shape/CylinderShape.h>
|
||||||
#include <Jolt/Physics/Collision/Shape/SphereShape.h>
|
#include <Jolt/Physics/Collision/Shape/SphereShape.h>
|
||||||
|
#include <Jolt/Physics/Collision/Shape/ConvexHullShape.h>
|
||||||
|
#include <Jolt/Physics/Collision/Shape/MeshShape.h>
|
||||||
|
#include <Jolt/Physics/Constraints/HingeConstraint.h>
|
||||||
|
#include <Jolt/Physics/Constraints/PointConstraint.h>
|
||||||
|
#include <Jolt/Physics/Constraints/SliderConstraint.h>
|
||||||
|
#include <Jolt/Physics/Collision/CastResult.h>
|
||||||
|
#include <Jolt/Physics/Collision/RayCast.h>
|
||||||
|
#include <Jolt/Physics/Collision/ContactListener.h>
|
||||||
|
#include <mutex>
|
||||||
|
#include <vector>
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
#include "scene.h"
|
#include "scene.h"
|
||||||
|
|
@ -57,6 +68,10 @@ extern "C" {
|
||||||
#define DEFAULT_FRICTION 0.5f
|
#define DEFAULT_FRICTION 0.5f
|
||||||
#define DEFAULT_BOUNCE 0.1f
|
#define DEFAULT_BOUNCE 0.1f
|
||||||
#define NO_HANDLE -1
|
#define NO_HANDLE -1
|
||||||
|
#define MAX_EVENTS 512 // Per frame; the rest of a busy step is dropped
|
||||||
|
#define DEFAULT_RAY_DISTANCE 1000.0f
|
||||||
|
#define WORLD_NODE -1 // A joint's other side fixed to the world
|
||||||
|
#define DEGREES_TO_RADIANS(d) ((d) * (3.14159265358979323846f / 180.0f))
|
||||||
|
|
||||||
|
|
||||||
// Two object layers: what never moves and what may. Static bodies never collide with each other.
|
// Two object layers: what never moves and what may. Static bodies never collide with each other.
|
||||||
|
|
@ -109,11 +124,70 @@ namespace {
|
||||||
uint32_t generation; // The node's, so a reused handle is not mistaken for this body
|
uint32_t generation; // The node's, so a reused handle is not mistaken for this body
|
||||||
JPH::BodyID id;
|
JPH::BodyID id;
|
||||||
BodyTypeE type;
|
BodyTypeE type;
|
||||||
|
bool trigger; // A sensor: reports overlaps, pushes nothing
|
||||||
bool enabled; // In the world (bodySetEnabled)
|
bool enabled; // In the world (bodySetEnabled)
|
||||||
bool used;
|
bool used;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
struct WorldT;
|
||||||
|
extern WorldT *_world;
|
||||||
|
bool _isTrigger(JPH::BodyID id);
|
||||||
|
|
||||||
|
|
||||||
|
// Collects contacts from Jolt's job threads; the engine drains it after the step.
|
||||||
|
class ContactListenerT final : public JPH::ContactListener {
|
||||||
|
public:
|
||||||
|
std::mutex lock;
|
||||||
|
std::vector<PhysicsEventT> events;
|
||||||
|
|
||||||
|
void OnContactAdded(const JPH::Body &a, const JPH::Body &b, const JPH::ContactManifold &manifold, JPH::ContactSettings &settings) override {
|
||||||
|
PhysicsEventT event;
|
||||||
|
JPH::Vec3 relative;
|
||||||
|
|
||||||
|
(void)settings;
|
||||||
|
event.nodeA = (int32_t)(uint32_t)a.GetUserData();
|
||||||
|
event.nodeB = (int32_t)(uint32_t)b.GetUserData();
|
||||||
|
event.point = vec3((float)manifold.GetWorldSpaceContactPointOn1(0).GetX(), (float)manifold.GetWorldSpaceContactPointOn1(0).GetY(), (float)manifold.GetWorldSpaceContactPointOn1(0).GetZ());
|
||||||
|
relative = a.GetLinearVelocity() - b.GetLinearVelocity();
|
||||||
|
event.speed = fabsf(relative.Dot(manifold.mWorldSpaceNormal));
|
||||||
|
if (a.IsSensor() || b.IsSensor()) {
|
||||||
|
event.type = PHYSICS_EVENT_ENTER;
|
||||||
|
if (b.IsSensor()) {
|
||||||
|
// The trigger comes first.
|
||||||
|
int32_t swap = event.nodeA;
|
||||||
|
|
||||||
|
event.nodeA = event.nodeB;
|
||||||
|
event.nodeB = swap;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
event.type = PHYSICS_EVENT_COLLISION;
|
||||||
|
}
|
||||||
|
push(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnContactRemoved(const JPH::SubShapeIDPair &pair) override;
|
||||||
|
|
||||||
|
void push(const PhysicsEventT &event) {
|
||||||
|
std::lock_guard<std::mutex> guard(lock);
|
||||||
|
|
||||||
|
if (events.size() < MAX_EVENTS) {
|
||||||
|
events.push_back(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// A constraint between two bodies (or one body and the world).
|
||||||
|
struct JointRecordT {
|
||||||
|
JPH::Ref<JPH::Constraint> constraint;
|
||||||
|
JointTypeE type;
|
||||||
|
int32_t nodeA;
|
||||||
|
int32_t nodeB;
|
||||||
|
bool used;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
struct WorldT {
|
struct WorldT {
|
||||||
JPH::TempAllocatorImpl *tempAllocator;
|
JPH::TempAllocatorImpl *tempAllocator;
|
||||||
JPH::JobSystemThreadPool *jobs;
|
JPH::JobSystemThreadPool *jobs;
|
||||||
|
|
@ -121,8 +195,11 @@ namespace {
|
||||||
ObjectVsBroadPhaseFilterT objectVsBroadPhase;
|
ObjectVsBroadPhaseFilterT objectVsBroadPhase;
|
||||||
ObjectPairFilterT objectPairs;
|
ObjectPairFilterT objectPairs;
|
||||||
JPH::PhysicsSystem *system;
|
JPH::PhysicsSystem *system;
|
||||||
|
ContactListenerT *contacts;
|
||||||
BodyRecordT *bodies;
|
BodyRecordT *bodies;
|
||||||
int32_t bodyCount;
|
int32_t bodyCount;
|
||||||
|
JointRecordT *joints;
|
||||||
|
int32_t jointCount;
|
||||||
double accumulator; // Seconds owed to the fixed step
|
double accumulator; // Seconds owed to the fixed step
|
||||||
uint64_t lastTick;
|
uint64_t lastTick;
|
||||||
bool enabled;
|
bool enabled;
|
||||||
|
|
@ -132,15 +209,121 @@ namespace {
|
||||||
WorldT *_world = nullptr;
|
WorldT *_world = nullptr;
|
||||||
|
|
||||||
|
|
||||||
|
// A contact ending only matters for triggers: the pair is reported as left. This runs inside
|
||||||
|
// Jolt's step on a job thread, where taking a body lock deadlocks, so the lock-free interface
|
||||||
|
// reads the user data; the trigger flag comes from our records.
|
||||||
|
void ContactListenerT::OnContactRemoved(const JPH::SubShapeIDPair &pair) {
|
||||||
|
PhysicsEventT event;
|
||||||
|
JPH::uint64 userA = _world->system->GetBodyInterfaceNoLock().GetUserData(pair.GetBody1ID());
|
||||||
|
JPH::uint64 userB = _world->system->GetBodyInterfaceNoLock().GetUserData(pair.GetBody2ID());
|
||||||
|
bool sensorA = _isTrigger(pair.GetBody1ID());
|
||||||
|
bool sensorB = _isTrigger(pair.GetBody2ID());
|
||||||
|
|
||||||
|
if (!sensorA && !sensorB) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.type = PHYSICS_EVENT_LEAVE;
|
||||||
|
event.nodeA = (int32_t)(uint32_t)(sensorA ? userA : userB);
|
||||||
|
event.nodeB = (int32_t)(uint32_t)(sensorA ? userB : userA);
|
||||||
|
event.point = vec3(0.0f, 0.0f, 0.0f);
|
||||||
|
event.speed = 0.0f;
|
||||||
|
push(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
JPH::RefConst<JPH::Shape> _buildMeshShape(int32_t node, ShapeTypeE shape, Vec3T position, QuatT rotation);
|
||||||
|
void _collectGeometry(int32_t node, const Mat4T *toBody, JPH::Array<JPH::Vec3> &points, JPH::IndexedTriangleList &triangles, JPH::VertexList &vertices);
|
||||||
BodyRecordT *_find(int32_t node);
|
BodyRecordT *_find(int32_t node);
|
||||||
JPH::Quat _fromQuat(QuatT q);
|
JPH::Quat _fromQuat(QuatT q);
|
||||||
JPH::Vec3 _fromVec3(Vec3T v);
|
JPH::Vec3 _fromVec3(Vec3T v);
|
||||||
|
bool _isTrigger(JPH::BodyID id);
|
||||||
void _release(BodyRecordT *record);
|
void _release(BodyRecordT *record);
|
||||||
QuatT _toQuat(JPH::Quat q);
|
QuatT _toQuat(JPH::Quat q);
|
||||||
Vec3T _toVec3(JPH::Vec3 v);
|
Vec3T _toVec3(JPH::Vec3 v);
|
||||||
void _trace(const char *fmt, ...);
|
void _trace(const char *fmt, ...);
|
||||||
|
|
||||||
|
|
||||||
|
// A convex hull or a triangle mesh from the geometry under a node (its own mesh and every
|
||||||
|
// descendant's, a model instance included), in the body's frame: the node's world scale is
|
||||||
|
// baked in, its position and rotation are the body's.
|
||||||
|
JPH::RefConst<JPH::Shape> _buildMeshShape(int32_t node, ShapeTypeE shape, Vec3T position, QuatT rotation) {
|
||||||
|
JPH::Array<JPH::Vec3> points;
|
||||||
|
JPH::VertexList vertices;
|
||||||
|
JPH::IndexedTriangleList triangles;
|
||||||
|
Mat4T bodyWorld = mat4Compose(position, rotation, vec3(1.0f, 1.0f, 1.0f));
|
||||||
|
Mat4T toBody;
|
||||||
|
|
||||||
|
if (!mat4Invert(bodyWorld, &toBody)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
_collectGeometry(node, &toBody, points, triangles, vertices);
|
||||||
|
if (shape == SHAPE_HULL) {
|
||||||
|
JPH::ConvexHullShapeSettings settings(points);
|
||||||
|
JPH::Shape::ShapeResult result;
|
||||||
|
|
||||||
|
if (points.size() < 4) {
|
||||||
|
utilTrace("Physics: node %d has too little geometry for a hull.", node);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
result = settings.Create();
|
||||||
|
if (result.HasError()) {
|
||||||
|
utilTrace("Physics: hull: %s", result.GetError().c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return result.Get();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
JPH::MeshShapeSettings settings(vertices, triangles);
|
||||||
|
JPH::Shape::ShapeResult result;
|
||||||
|
|
||||||
|
if (triangles.empty()) {
|
||||||
|
utilTrace("Physics: node %d has no triangles for a mesh shape.", node);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
result = settings.Create();
|
||||||
|
if (result.HasError()) {
|
||||||
|
utilTrace("Physics: mesh: %s", result.GetError().c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return result.Get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Gathers the node's mesh and its descendants' into the body's frame.
|
||||||
|
void _collectGeometry(int32_t node, const Mat4T *toBody, JPH::Array<JPH::Vec3> &points, JPH::IndexedTriangleList &triangles, JPH::VertexList &vertices) {
|
||||||
|
const float *positions;
|
||||||
|
const uint32_t *indices;
|
||||||
|
int32_t vertexCount;
|
||||||
|
int32_t indexCount;
|
||||||
|
int32_t mesh = nodeGetMesh(node);
|
||||||
|
int32_t x;
|
||||||
|
|
||||||
|
if ((mesh != NO_HANDLE) && meshGetGeometry(mesh, &positions, &vertexCount, &indices, &indexCount)) {
|
||||||
|
Vec3T position;
|
||||||
|
QuatT rotation;
|
||||||
|
Vec3T scale;
|
||||||
|
Mat4T local;
|
||||||
|
uint32_t base = (uint32_t)vertices.size();
|
||||||
|
|
||||||
|
nodeGetWorldTransform(node, &position, &rotation, &scale);
|
||||||
|
local = mat4Multiply(*toBody, mat4Compose(position, rotation, scale));
|
||||||
|
for (x = 0; x < vertexCount; x++) {
|
||||||
|
Vec3T v = mat4TransformPoint(local, vec3(positions[x * 3], positions[x * 3 + 1], positions[x * 3 + 2]));
|
||||||
|
|
||||||
|
points.push_back(JPH::Vec3(v.x, v.y, v.z));
|
||||||
|
vertices.push_back(JPH::Float3(v.x, v.y, v.z));
|
||||||
|
}
|
||||||
|
for (x = 0; x + 2 < indexCount; x += 3) {
|
||||||
|
triangles.push_back(JPH::IndexedTriangle(base + indices[x], base + indices[x + 1], base + indices[x + 2]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (x = 0; x < nodeGetChildCount(node); x++) {
|
||||||
|
_collectGeometry(nodeGetChild(node, x), toBody, points, triangles, vertices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// The record for a node's body, or NULL. A record whose node was deleted (or reused) is
|
// The record for a node's body, or NULL. A record whose node was deleted (or reused) is
|
||||||
// released on the way.
|
// released on the way.
|
||||||
BodyRecordT *_find(int32_t node) {
|
BodyRecordT *_find(int32_t node) {
|
||||||
|
|
@ -175,10 +358,29 @@ namespace {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Takes the body out of the world and frees its slot.
|
// Whether a Jolt body is one of our triggers (by record, so a body being destroyed is safe).
|
||||||
|
bool _isTrigger(JPH::BodyID id) {
|
||||||
|
int32_t x;
|
||||||
|
|
||||||
|
for (x = 0; x < _world->bodyCount; x++) {
|
||||||
|
if (_world->bodies[x].used && (_world->bodies[x].id == id)) {
|
||||||
|
return _world->bodies[x].trigger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Takes the body out of the world (its joints first) and frees its slot.
|
||||||
void _release(BodyRecordT *record) {
|
void _release(BodyRecordT *record) {
|
||||||
JPH::BodyInterface &bodies = _world->system->GetBodyInterface();
|
JPH::BodyInterface &bodies = _world->system->GetBodyInterface();
|
||||||
|
int32_t x;
|
||||||
|
|
||||||
|
for (x = 0; x < _world->jointCount; x++) {
|
||||||
|
if (_world->joints[x].used && ((_world->joints[x].nodeA == record->node) || (_world->joints[x].nodeB == record->node))) {
|
||||||
|
jointDelete(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (record->enabled) {
|
if (record->enabled) {
|
||||||
bodies.RemoveBody(record->id);
|
bodies.RemoveBody(record->id);
|
||||||
}
|
}
|
||||||
|
|
@ -329,8 +531,19 @@ bool bodyNew(int32_t node, BodyTypeE type, ShapeTypeE shape, float a, float b, f
|
||||||
height = SDL_max(b * scale.y, MIN_DIMENSION);
|
height = SDL_max(b * scale.y, MIN_DIMENSION);
|
||||||
joltShape = new JPH::CylinderShape(height / 2.0f, radius);
|
joltShape = new JPH::CylinderShape(height / 2.0f, radius);
|
||||||
break;
|
break;
|
||||||
|
case SHAPE_HULL:
|
||||||
|
case SHAPE_MESH:
|
||||||
|
if ((shape == SHAPE_MESH) && (type == BODY_DYNAMIC)) {
|
||||||
|
utilTrace("Physics: a mesh shape can only be static or kinematic; use a hull for node %d.", node);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
joltShape = _buildMeshShape(node, shape, position, rotation);
|
||||||
|
if (joltShape == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
utilTrace("Physics: shape %d is not available yet.", (int32_t)shape);
|
utilTrace("Physics: unknown shape %d.", (int32_t)shape);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (x = 0; x < _world->bodyCount; x++) {
|
for (x = 0; x < _world->bodyCount; x++) {
|
||||||
|
|
@ -440,6 +653,26 @@ bool bodySetMass(int32_t node, float kilograms) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A trigger (Jolt sensor) reports what enters and leaves it and pushes nothing.
|
||||||
|
bool bodySetTrigger(int32_t node, bool trigger) {
|
||||||
|
BodyRecordT *record = _find(node);
|
||||||
|
|
||||||
|
if (record == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
JPH::BodyLockWrite lock(_world->system->GetBodyLockInterface(), record->id);
|
||||||
|
|
||||||
|
if (!lock.Succeeded()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
lock.GetBody().SetIsSensor(trigger);
|
||||||
|
}
|
||||||
|
record->trigger = trigger;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
bool bodySetVelocity(int32_t node, Vec3T velocity) {
|
bool bodySetVelocity(int32_t node, Vec3T velocity) {
|
||||||
BodyRecordT *record = _find(node);
|
BodyRecordT *record = _find(node);
|
||||||
|
|
||||||
|
|
@ -451,6 +684,131 @@ bool bodySetVelocity(int32_t node, Vec3T velocity) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ===== Joints =====
|
||||||
|
|
||||||
|
bool jointDelete(int32_t joint) {
|
||||||
|
if (!jointValid(joint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_world->system->RemoveConstraint(_world->joints[joint].constraint);
|
||||||
|
_world->joints[joint].constraint = nullptr;
|
||||||
|
_world->joints[joint].used = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A hinge (anchor and axis), ball (anchor) or slider (axis) between two bodies, or between a body
|
||||||
|
// and the world when nodeB is -1. Anchor and axis are in world space. nodeA is Jolt's first
|
||||||
|
// body (creating them the other way round mirrors the motion); see jointSetLimits for the sign
|
||||||
|
// that implies.
|
||||||
|
int32_t jointNew(JointTypeE type, int32_t nodeA, int32_t nodeB, Vec3T anchor, Vec3T axis) {
|
||||||
|
BodyRecordT *a = _find(nodeA);
|
||||||
|
BodyRecordT *b = (nodeB == WORLD_NODE) ? nullptr : _find(nodeB);
|
||||||
|
JPH::Ref<JPH::Constraint> constraint;
|
||||||
|
JPH::Vec3 direction;
|
||||||
|
JPH::Vec3 normal;
|
||||||
|
int32_t x;
|
||||||
|
|
||||||
|
if ((_world == nullptr) || (a == nullptr) || ((nodeB != WORLD_NODE) && (b == nullptr))) {
|
||||||
|
return NO_HANDLE;
|
||||||
|
}
|
||||||
|
direction = _fromVec3(axis).NormalizedOr(JPH::Vec3::sAxisY());
|
||||||
|
normal = direction.GetNormalizedPerpendicular();
|
||||||
|
{
|
||||||
|
JPH::BodyID ids[2] = { a->id, (b != nullptr) ? b->id : JPH::BodyID() };
|
||||||
|
JPH::BodyLockMultiWrite lock(_world->system->GetBodyLockInterface(), ids, (b != nullptr) ? 2 : 1);
|
||||||
|
JPH::Body *bodyA = lock.GetBody(0);
|
||||||
|
JPH::Body *bodyB = (b != nullptr) ? lock.GetBody(1) : &JPH::Body::sFixedToWorld;
|
||||||
|
|
||||||
|
if ((bodyA == nullptr) || (bodyB == nullptr)) {
|
||||||
|
return NO_HANDLE;
|
||||||
|
}
|
||||||
|
if (type == JOINT_HINGE) {
|
||||||
|
JPH::HingeConstraintSettings settings;
|
||||||
|
|
||||||
|
settings.mSpace = JPH::EConstraintSpace::WorldSpace;
|
||||||
|
settings.mPoint1 = JPH::RVec3(anchor.x, anchor.y, anchor.z);
|
||||||
|
settings.mPoint2 = settings.mPoint1;
|
||||||
|
settings.mHingeAxis1 = direction;
|
||||||
|
settings.mHingeAxis2 = direction;
|
||||||
|
settings.mNormalAxis1 = normal;
|
||||||
|
settings.mNormalAxis2 = normal;
|
||||||
|
constraint = settings.Create(*bodyA, *bodyB);
|
||||||
|
} else if (type == JOINT_BALL) {
|
||||||
|
JPH::PointConstraintSettings settings;
|
||||||
|
|
||||||
|
settings.mSpace = JPH::EConstraintSpace::WorldSpace;
|
||||||
|
settings.mPoint1 = JPH::RVec3(anchor.x, anchor.y, anchor.z);
|
||||||
|
settings.mPoint2 = settings.mPoint1;
|
||||||
|
constraint = settings.Create(*bodyA, *bodyB);
|
||||||
|
} else {
|
||||||
|
JPH::SliderConstraintSettings settings;
|
||||||
|
|
||||||
|
settings.mSpace = JPH::EConstraintSpace::WorldSpace;
|
||||||
|
settings.mPoint1 = JPH::RVec3(anchor.x, anchor.y, anchor.z);
|
||||||
|
settings.mPoint2 = settings.mPoint1;
|
||||||
|
settings.mSliderAxis1 = direction;
|
||||||
|
settings.mSliderAxis2 = direction;
|
||||||
|
settings.mNormalAxis1 = normal;
|
||||||
|
settings.mNormalAxis2 = normal;
|
||||||
|
constraint = settings.Create(*bodyA, *bodyB);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (constraint == nullptr) {
|
||||||
|
return NO_HANDLE;
|
||||||
|
}
|
||||||
|
_world->system->AddConstraint(constraint);
|
||||||
|
for (x = 0; x < _world->jointCount; x++) {
|
||||||
|
if (!_world->joints[x].used) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (x == _world->jointCount) {
|
||||||
|
JointRecordT *grown = new JointRecordT[_world->jointCount + 1];
|
||||||
|
|
||||||
|
for (int32_t y = 0; y < _world->jointCount; y++) {
|
||||||
|
grown[y] = _world->joints[y];
|
||||||
|
}
|
||||||
|
delete[] _world->joints;
|
||||||
|
_world->joints = grown;
|
||||||
|
_world->jointCount++;
|
||||||
|
}
|
||||||
|
_world->joints[x].constraint = constraint;
|
||||||
|
_world->joints[x].type = type;
|
||||||
|
_world->joints[x].nodeA = nodeA;
|
||||||
|
_world->joints[x].nodeB = nodeB;
|
||||||
|
_world->joints[x].used = true;
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Limits: degrees either side of the starting angle for a hinge (positive is a right-hand turn of
|
||||||
|
// nodeA about the axis), distance along the axis for a slider (positive along it); low at or below
|
||||||
|
// 0, high at or above. Jolt measures both as its second body relative to its first, which is the
|
||||||
|
// opposite of nodeA's own motion, so the range is mirrored on the way in. A ball joint has none.
|
||||||
|
bool jointSetLimits(int32_t joint, float low, float high) {
|
||||||
|
if (!jointValid(joint)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
low = SDL_min(low, 0.0f);
|
||||||
|
high = SDL_max(high, 0.0f);
|
||||||
|
if (_world->joints[joint].type == JOINT_HINGE) {
|
||||||
|
((JPH::HingeConstraint *)_world->joints[joint].constraint.GetPtr())->SetLimits(DEGREES_TO_RADIANS(-high), DEGREES_TO_RADIANS(-low));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (_world->joints[joint].type == JOINT_SLIDER) {
|
||||||
|
((JPH::SliderConstraint *)_world->joints[joint].constraint.GetPtr())->SetLimits(-high, -low);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool jointValid(int32_t joint) {
|
||||||
|
return (_world != nullptr) && (joint >= 0) && (joint < _world->jointCount) && _world->joints[joint].used;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ===== World =====
|
// ===== World =====
|
||||||
|
|
||||||
bool physicsAvailable(void) {
|
bool physicsAvailable(void) {
|
||||||
|
|
@ -458,6 +816,26 @@ bool physicsAvailable(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Hands the engine the events the last step produced (up to maximum) and clears them.
|
||||||
|
int32_t physicsGetEvents(PhysicsEventT *events, int32_t maximum) {
|
||||||
|
int32_t count = 0;
|
||||||
|
|
||||||
|
if (_world == nullptr) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> guard(_world->contacts->lock);
|
||||||
|
|
||||||
|
while ((count < maximum) && (count < (int32_t)_world->contacts->events.size())) {
|
||||||
|
events[count] = _world->contacts->events[(size_t)count];
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
_world->contacts->events.clear();
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Brings Jolt up: allocators, the type factory, a job pool sized to the machine, and an empty
|
// Brings Jolt up: allocators, the type factory, a job pool sized to the machine, and an empty
|
||||||
// world. Refuses (returning false, 3D physics unavailable) on an x86 without SSE4.1 and 4.2, the
|
// world. Refuses (returning false, 3D physics unavailable) on an x86 without SSE4.1 and 4.2, the
|
||||||
// level the library was compiled for, rather than faulting on the first instruction.
|
// level the library was compiled for, rather than faulting on the first instruction.
|
||||||
|
|
@ -483,6 +861,8 @@ bool physicsInit(void) {
|
||||||
_world->jobs = new JPH::JobSystemThreadPool(JPH::cMaxPhysicsJobs, JPH::cMaxPhysicsBarriers, threads);
|
_world->jobs = new JPH::JobSystemThreadPool(JPH::cMaxPhysicsJobs, JPH::cMaxPhysicsBarriers, threads);
|
||||||
_world->system = new JPH::PhysicsSystem();
|
_world->system = new JPH::PhysicsSystem();
|
||||||
_world->system->Init(MAX_BODIES, 0, MAX_BODY_PAIRS, MAX_CONTACTS, _world->broadPhaseLayers, _world->objectVsBroadPhase, _world->objectPairs);
|
_world->system->Init(MAX_BODIES, 0, MAX_BODY_PAIRS, MAX_CONTACTS, _world->broadPhaseLayers, _world->objectVsBroadPhase, _world->objectPairs);
|
||||||
|
_world->contacts = new ContactListenerT();
|
||||||
|
_world->system->SetContactListener(_world->contacts);
|
||||||
_world->enabled = true;
|
_world->enabled = true;
|
||||||
utilTrace("Physics: Jolt %d.%d.%d ready, %d job thread%s", JPH_VERSION_MAJOR, JPH_VERSION_MINOR, JPH_VERSION_PATCH, threads, (threads == 1) ? "" : "s");
|
utilTrace("Physics: Jolt %d.%d.%d ready, %d job thread%s", JPH_VERSION_MAJOR, JPH_VERSION_MINOR, JPH_VERSION_PATCH, threads, (threads == 1) ? "" : "s");
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -501,7 +881,9 @@ void physicsQuit(void) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SDL_free(_world->bodies);
|
SDL_free(_world->bodies);
|
||||||
|
delete[] _world->joints;
|
||||||
delete _world->system;
|
delete _world->system;
|
||||||
|
delete _world->contacts;
|
||||||
delete _world->jobs;
|
delete _world->jobs;
|
||||||
delete _world->tempAllocator;
|
delete _world->tempAllocator;
|
||||||
delete _world;
|
delete _world;
|
||||||
|
|
@ -512,6 +894,41 @@ void physicsQuit(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The nearest body along a ray (triggers included), with the hit point and surface normal.
|
||||||
|
bool physicsRaycast(Vec3T origin, Vec3T direction, float maxDistance, int32_t *node, Vec3T *point, Vec3T *normal) {
|
||||||
|
JPH::RayCastResult result;
|
||||||
|
JPH::Vec3 dir;
|
||||||
|
JPH::RVec3 hit;
|
||||||
|
|
||||||
|
if (_world == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
dir = _fromVec3(direction).NormalizedOr(JPH::Vec3::sZero());
|
||||||
|
if (dir.IsNearZero()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (maxDistance <= 0.0f) {
|
||||||
|
maxDistance = DEFAULT_RAY_DISTANCE;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
JPH::RRayCast ray(JPH::RVec3(origin.x, origin.y, origin.z), dir * maxDistance);
|
||||||
|
|
||||||
|
if (!_world->system->GetNarrowPhaseQuery().CastRay(ray, result)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
hit = ray.GetPointOnRay(result.mFraction);
|
||||||
|
}
|
||||||
|
*node = (int32_t)(uint32_t)_world->system->GetBodyInterface().GetUserData(result.mBodyID);
|
||||||
|
*point = vec3((float)hit.GetX(), (float)hit.GetY(), (float)hit.GetZ());
|
||||||
|
{
|
||||||
|
JPH::BodyLockRead lock(_world->system->GetBodyLockInterface(), result.mBodyID);
|
||||||
|
|
||||||
|
*normal = lock.Succeeded() ? _toVec3(lock.GetBody().GetWorldSpaceSurfaceNormal(result.mSubShapeID2, hit)) : vec3(0.0f, 1.0f, 0.0f);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Pauses the simulation (bodies hold still) without losing it.
|
// Pauses the simulation (bodies hold still) without losing it.
|
||||||
void physicsSetEnabled(bool enabled) {
|
void physicsSetEnabled(bool enabled) {
|
||||||
if (_world != nullptr) {
|
if (_world != nullptr) {
|
||||||
|
|
|
||||||
39
src/scene.c
39
src/scene.c
|
|
@ -111,6 +111,9 @@ typedef struct MeshS {
|
||||||
uint32_t indexCount;
|
uint32_t indexCount;
|
||||||
Vec3T boundsMin; // Of the vertices, for fitting the shadow map
|
Vec3T boundsMin; // Of the vertices, for fitting the shadow map
|
||||||
Vec3T boundsMax;
|
Vec3T boundsMax;
|
||||||
|
float *positions; // A CPU copy of the geometry (x, y, z per vertex) for physics shapes
|
||||||
|
uint32_t *indices;
|
||||||
|
int32_t vertexCount;
|
||||||
bool skinned;
|
bool skinned;
|
||||||
bool used;
|
bool used;
|
||||||
} MeshT;
|
} MeshT;
|
||||||
|
|
@ -301,8 +304,20 @@ static int32_t _addMesh(const SceneVertexT *vertices, int32_t vertexCount, const
|
||||||
return NO_HANDLE;
|
return NO_HANDLE;
|
||||||
}
|
}
|
||||||
mesh->indexCount = (uint32_t)indexCount;
|
mesh->indexCount = (uint32_t)indexCount;
|
||||||
|
mesh->vertexCount = vertexCount;
|
||||||
mesh->skinned = skinned;
|
mesh->skinned = skinned;
|
||||||
mesh->used = true;
|
mesh->used = true;
|
||||||
|
mesh->positions = SDL_malloc(sizeof(float) * 3 * (size_t)vertexCount);
|
||||||
|
mesh->indices = SDL_malloc(sizeof(uint32_t) * (size_t)indexCount);
|
||||||
|
if ((mesh->positions == NULL) || (mesh->indices == NULL)) {
|
||||||
|
utilDie("Out of memory keeping a mesh's geometry.");
|
||||||
|
}
|
||||||
|
for (x = 0; x < vertexCount; x++) {
|
||||||
|
mesh->positions[x * 3] = vertices[x].position[0];
|
||||||
|
mesh->positions[x * 3 + 1] = vertices[x].position[1];
|
||||||
|
mesh->positions[x * 3 + 2] = vertices[x].position[2];
|
||||||
|
}
|
||||||
|
memcpy(mesh->indices, indices, sizeof(uint32_t) * (size_t)indexCount);
|
||||||
mesh->boundsMin = vec3(vertices[0].position[0], vertices[0].position[1], vertices[0].position[2]);
|
mesh->boundsMin = vec3(vertices[0].position[0], vertices[0].position[1], vertices[0].position[2]);
|
||||||
mesh->boundsMax = mesh->boundsMin;
|
mesh->boundsMax = mesh->boundsMin;
|
||||||
for (x = 1; x < vertexCount; x++) {
|
for (x = 1; x < vertexCount; x++) {
|
||||||
|
|
@ -1663,6 +1678,8 @@ bool meshDelete(int32_t mesh) {
|
||||||
if (_scene.meshes[mesh].indexBuffer != NULL) {
|
if (_scene.meshes[mesh].indexBuffer != NULL) {
|
||||||
SDL_ReleaseGPUBuffer(_scene.device, _scene.meshes[mesh].indexBuffer);
|
SDL_ReleaseGPUBuffer(_scene.device, _scene.meshes[mesh].indexBuffer);
|
||||||
}
|
}
|
||||||
|
SDL_free(_scene.meshes[mesh].positions);
|
||||||
|
SDL_free(_scene.meshes[mesh].indices);
|
||||||
memset(&_scene.meshes[mesh], 0, sizeof(MeshT));
|
memset(&_scene.meshes[mesh], 0, sizeof(MeshT));
|
||||||
for (x = 0; x < _scene.nodeCount; x++) {
|
for (x = 0; x < _scene.nodeCount; x++) {
|
||||||
if (_scene.nodes[x].used && (_scene.nodes[x].mesh == mesh)) {
|
if (_scene.nodes[x].used && (_scene.nodes[x].mesh == mesh)) {
|
||||||
|
|
@ -1673,6 +1690,19 @@ bool meshDelete(int32_t mesh) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The mesh's geometry as kept on the CPU: x, y, z per vertex and triangle indices.
|
||||||
|
bool meshGetGeometry(int32_t mesh, const float **positions, int32_t *vertexCount, const uint32_t **indices, int32_t *indexCount) {
|
||||||
|
if (!meshValid(mesh)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*positions = _scene.meshes[mesh].positions;
|
||||||
|
*vertexCount = _scene.meshes[mesh].vertexCount;
|
||||||
|
*indices = _scene.meshes[mesh].indices;
|
||||||
|
*indexCount = (int32_t)_scene.meshes[mesh].indexCount;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Raw geometry from a script: positions (3 per vertex), normals (3, may be NULL for flat
|
// Raw geometry from a script: positions (3 per vertex), normals (3, may be NULL for flat
|
||||||
// shading computed here), uvs (2, may be NULL), and triangle indices.
|
// shading computed here), uvs (2, may be NULL), and triangle indices.
|
||||||
int32_t meshNew(const float *positions, const float *normals, const float *uvs, int32_t vertexCount, const uint32_t *indices, int32_t indexCount) {
|
int32_t meshNew(const float *positions, const float *normals, const float *uvs, int32_t vertexCount, const uint32_t *indices, int32_t indexCount) {
|
||||||
|
|
@ -1940,6 +1970,15 @@ uint32_t nodeGetGeneration(int32_t node) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The node's mesh handle, or -1.
|
||||||
|
int32_t nodeGetMesh(int32_t node) {
|
||||||
|
if (!nodeValid(node)) {
|
||||||
|
return NO_HANDLE;
|
||||||
|
}
|
||||||
|
return _scene.nodes[node].mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const char *nodeGetName(int32_t node) {
|
const char *nodeGetName(int32_t node) {
|
||||||
if (!nodeValid(node) || (_scene.nodes[node].name == NULL)) {
|
if (!nodeValid(node) || (_scene.nodes[node].name == NULL)) {
|
||||||
return "";
|
return "";
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,7 @@ int32_t meshBox(float width, float height, float depth);
|
||||||
int32_t meshCone(float radius, float height, int32_t segments);
|
int32_t meshCone(float radius, float height, int32_t segments);
|
||||||
int32_t meshCylinder(float radius, float height, int32_t segments);
|
int32_t meshCylinder(float radius, float height, int32_t segments);
|
||||||
bool meshDelete(int32_t mesh);
|
bool meshDelete(int32_t mesh);
|
||||||
|
bool meshGetGeometry(int32_t mesh, const float **positions, int32_t *vertexCount, const uint32_t **indices, int32_t *indexCount);
|
||||||
int32_t meshNew(const float *positions, const float *normals, const float *uvs, int32_t vertexCount, const uint32_t *indices, int32_t indexCount);
|
int32_t meshNew(const float *positions, const float *normals, const float *uvs, int32_t vertexCount, const uint32_t *indices, int32_t indexCount);
|
||||||
int32_t meshNewVertices(const SceneVertexT *vertices, int32_t vertexCount, const uint32_t *indices, int32_t indexCount, bool skinned);
|
int32_t meshNewVertices(const SceneVertexT *vertices, int32_t vertexCount, const uint32_t *indices, int32_t indexCount, bool skinned);
|
||||||
int32_t meshPlane(float width, float depth);
|
int32_t meshPlane(float width, float depth);
|
||||||
|
|
@ -109,6 +110,7 @@ int32_t nodeFind(int32_t root, const char *name);
|
||||||
int32_t nodeGetChild(int32_t node, int32_t index);
|
int32_t nodeGetChild(int32_t node, int32_t index);
|
||||||
int32_t nodeGetChildCount(int32_t node);
|
int32_t nodeGetChildCount(int32_t node);
|
||||||
uint32_t nodeGetGeneration(int32_t node);
|
uint32_t nodeGetGeneration(int32_t node);
|
||||||
|
int32_t nodeGetMesh(int32_t node);
|
||||||
const char *nodeGetName(int32_t node);
|
const char *nodeGetName(int32_t node);
|
||||||
int32_t nodeGetParent(int32_t node);
|
int32_t nodeGetParent(int32_t node);
|
||||||
Vec3T nodeGetPosition(int32_t node);
|
Vec3T nodeGetPosition(int32_t node);
|
||||||
|
|
|
||||||
170
src/singe.c
170
src/singe.c
|
|
@ -496,6 +496,7 @@ static void _pauseAllVideos(bool pause);
|
||||||
static void _processKey(bool down, int32_t keysym, int32_t scancode);
|
static void _processKey(bool down, int32_t keysym, int32_t scancode);
|
||||||
static void _progTrace(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
static void _progTrace(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||||
static int32_t _pushVec3(lua_State *L, Vec3T v);
|
static int32_t _pushVec3(lua_State *L, Vec3T v);
|
||||||
|
static void _physicsCallbacks(void);
|
||||||
static SDL_Texture *_sceneVideoSource(int32_t player);
|
static SDL_Texture *_sceneVideoSource(int32_t player);
|
||||||
static void _pushConstants(lua_State *L);
|
static void _pushConstants(lua_State *L);
|
||||||
static void _putPixel(int32_t x, int32_t y, uint32_t pixel);
|
static void _putPixel(int32_t x, int32_t y, uint32_t pixel);
|
||||||
|
|
@ -540,6 +541,7 @@ static int32_t apiBodySetBounce(lua_State *L);
|
||||||
static int32_t apiBodySetEnabled(lua_State *L);
|
static int32_t apiBodySetEnabled(lua_State *L);
|
||||||
static int32_t apiBodySetFriction(lua_State *L);
|
static int32_t apiBodySetFriction(lua_State *L);
|
||||||
static int32_t apiBodySetMass(lua_State *L);
|
static int32_t apiBodySetMass(lua_State *L);
|
||||||
|
static int32_t apiBodySetTrigger(lua_State *L);
|
||||||
static int32_t apiBodySetVelocity(lua_State *L);
|
static int32_t apiBodySetVelocity(lua_State *L);
|
||||||
static int32_t apiCameraSet(lua_State *L);
|
static int32_t apiCameraSet(lua_State *L);
|
||||||
static int32_t apiCameraSetOrthographic(lua_State *L);
|
static int32_t apiCameraSetOrthographic(lua_State *L);
|
||||||
|
|
@ -577,6 +579,11 @@ static int32_t apiFontQuality(lua_State *L);
|
||||||
static int32_t apiFontSelect(lua_State *L);
|
static int32_t apiFontSelect(lua_State *L);
|
||||||
static int32_t apiFontToSprite(lua_State *L);
|
static int32_t apiFontToSprite(lua_State *L);
|
||||||
static int32_t apiFontUnload(lua_State *L);
|
static int32_t apiFontUnload(lua_State *L);
|
||||||
|
static int32_t apiJointBall(lua_State *L);
|
||||||
|
static int32_t apiJointDelete(lua_State *L);
|
||||||
|
static int32_t apiJointHinge(lua_State *L);
|
||||||
|
static int32_t apiJointSetLimits(lua_State *L);
|
||||||
|
static int32_t apiJointSlider(lua_State *L);
|
||||||
static int32_t apiKeyboardGetLastDown(lua_State *L);
|
static int32_t apiKeyboardGetLastDown(lua_State *L);
|
||||||
static int32_t apiKeyboardGetLastUp(lua_State *L);
|
static int32_t apiKeyboardGetLastUp(lua_State *L);
|
||||||
static int32_t apiKeyboardGetMode(lua_State *L);
|
static int32_t apiKeyboardGetMode(lua_State *L);
|
||||||
|
|
@ -650,6 +657,7 @@ static int32_t apiOverlayLine(lua_State *L);
|
||||||
static int32_t apiOverlayPlot(lua_State *L);
|
static int32_t apiOverlayPlot(lua_State *L);
|
||||||
static int32_t apiOverlayPrint(lua_State *L);
|
static int32_t apiOverlayPrint(lua_State *L);
|
||||||
static int32_t apiOverlaySetResolution(lua_State *L);
|
static int32_t apiOverlaySetResolution(lua_State *L);
|
||||||
|
static int32_t apiPhysicsRaycast(lua_State *L);
|
||||||
static int32_t apiPhysicsSetEnabled(lua_State *L);
|
static int32_t apiPhysicsSetEnabled(lua_State *L);
|
||||||
static int32_t apiPhysicsSetGravity(lua_State *L);
|
static int32_t apiPhysicsSetGravity(lua_State *L);
|
||||||
static int32_t apiSceneEnable(lua_State *L);
|
static int32_t apiSceneEnable(lua_State *L);
|
||||||
|
|
@ -1088,6 +1096,9 @@ static void _callLua(const char *func, const char *sig, ...) {
|
||||||
case 'i': // Int
|
case 'i': // Int
|
||||||
lua_pushinteger(_global.luaContext, va_arg(vl, int)); // Promoted type for varargs.
|
lua_pushinteger(_global.luaContext, va_arg(vl, int)); // Promoted type for varargs.
|
||||||
break;
|
break;
|
||||||
|
case 'b': // Boolean (passed as an int)
|
||||||
|
lua_pushboolean(_global.luaContext, va_arg(vl, int32_t));
|
||||||
|
break;
|
||||||
|
|
||||||
case 's': // String
|
case 's': // String
|
||||||
lua_pushstring(_global.luaContext, va_arg(vl, char *));
|
lua_pushstring(_global.luaContext, va_arg(vl, char *));
|
||||||
|
|
@ -2108,6 +2119,26 @@ static int32_t _pushVec3(lua_State *L, Vec3T v) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Hands the step's contacts and trigger overlaps to the script: onCollision(nodeA, nodeB, x, y, z,
|
||||||
|
// speed) and onTrigger(trigger, other, entered), when the script defines them.
|
||||||
|
static void _physicsCallbacks(void) {
|
||||||
|
PhysicsEventT events[64];
|
||||||
|
int32_t count;
|
||||||
|
int32_t x;
|
||||||
|
|
||||||
|
count = physicsGetEvents(events, (int32_t)SDL_arraysize(events));
|
||||||
|
for (x = 0; x < count; x++) {
|
||||||
|
PhysicsEventT *event = &events[x];
|
||||||
|
|
||||||
|
if (event->type == PHYSICS_EVENT_COLLISION) {
|
||||||
|
_callLua("onCollision", "iidddd", event->nodeA, event->nodeB, (double)event->point.x, (double)event->point.y, (double)event->point.z, (double)event->speed);
|
||||||
|
} else {
|
||||||
|
_callLua("onTrigger", "iib", event->nodeA, event->nodeB, (event->type == PHYSICS_EVENT_ENTER) ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// A player's current frame for the 3D scene's video materials: the disc's texture is already
|
// A player's current frame for the 3D scene's video materials: the disc's texture is already
|
||||||
// updated for this frame; a loaded video is advanced here (drawing it on the overlay too is harmless).
|
// updated for this frame; a loaded video is advanced here (drawing it on the overlay too is harmless).
|
||||||
static SDL_Texture *_sceneVideoSource(int32_t player) {
|
static SDL_Texture *_sceneVideoSource(int32_t player) {
|
||||||
|
|
@ -2200,6 +2231,12 @@ static void _pushConstants(lua_State *L) {
|
||||||
lua_setglobal(L, "SHAPE_HULL");
|
lua_setglobal(L, "SHAPE_HULL");
|
||||||
lua_pushinteger(L, SHAPE_MESH);
|
lua_pushinteger(L, SHAPE_MESH);
|
||||||
lua_setglobal(L, "SHAPE_MESH");
|
lua_setglobal(L, "SHAPE_MESH");
|
||||||
|
lua_pushinteger(L, JOINT_HINGE);
|
||||||
|
lua_setglobal(L, "JOINT_HINGE");
|
||||||
|
lua_pushinteger(L, JOINT_BALL);
|
||||||
|
lua_setglobal(L, "JOINT_BALL");
|
||||||
|
lua_pushinteger(L, JOINT_SLIDER);
|
||||||
|
lua_setglobal(L, "JOINT_SLIDER");
|
||||||
|
|
||||||
lua_pushinteger(L, -1);
|
lua_pushinteger(L, -1);
|
||||||
lua_setglobal(L, "SOUND_ERROR_INVALID");
|
lua_setglobal(L, "SOUND_ERROR_INVALID");
|
||||||
|
|
@ -2819,7 +2856,7 @@ static int32_t apiBodyIsResting(lua_State *L) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// bodyNew(node, type, shape, a [, b [, c]]): BODY_* and SHAPE_*; a, b, c size the shape
|
// bodyNew(node, type, shape [, a [, b [, c]]]): BODY_* and SHAPE_*; a, b, c size the primitive shapes
|
||||||
static int32_t apiBodyNew(lua_State *L) {
|
static int32_t apiBodyNew(lua_State *L) {
|
||||||
int32_t node;
|
int32_t node;
|
||||||
int32_t type;
|
int32_t type;
|
||||||
|
|
@ -2898,6 +2935,14 @@ static int32_t apiBodySetMass(lua_State *L) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// bodySetTrigger(node, bool): a trigger reports what enters and leaves it (onTrigger) and pushes nothing
|
||||||
|
static int32_t apiBodySetTrigger(lua_State *L) {
|
||||||
|
_argCheck(L, "bodySetTrigger", 2, 2);
|
||||||
|
bodySetTrigger(_argBody(L, "bodySetTrigger", 1), _argBoolean(L, "bodySetTrigger", 2));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// bodySetVelocity(node, x, y, z)
|
// bodySetVelocity(node, x, y, z)
|
||||||
static int32_t apiBodySetVelocity(lua_State *L) {
|
static int32_t apiBodySetVelocity(lua_State *L) {
|
||||||
_argCheck(L, "bodySetVelocity", 4, 4);
|
_argCheck(L, "bodySetVelocity", 4, 4);
|
||||||
|
|
@ -3457,6 +3502,95 @@ static int32_t apiFontUnload(lua_State *L) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// joint = jointBall(nodeA, nodeB, ax, ay, az): nodeB -1 fixes to the world; anchor in world space
|
||||||
|
static int32_t apiJointBall(lua_State *L) {
|
||||||
|
int32_t nodeA;
|
||||||
|
int32_t nodeB;
|
||||||
|
int32_t joint;
|
||||||
|
|
||||||
|
_argCheck(L, "jointBall", 5, 5);
|
||||||
|
nodeA = _argBody(L, "jointBall", 1);
|
||||||
|
nodeB = _argInteger(L, "jointBall", 2);
|
||||||
|
if ((nodeB != -1) && !bodyExists(nodeB)) {
|
||||||
|
_luaDie(L, "jointBall", "Node %d has no body.", nodeB);
|
||||||
|
}
|
||||||
|
joint = jointNew(JOINT_BALL, nodeA, nodeB, _argVec3(L, "jointBall", 3), vec3(0.0f, 1.0f, 0.0f));
|
||||||
|
if (joint < 0) {
|
||||||
|
_luaDie(L, "jointBall", "Unable to create the joint.");
|
||||||
|
}
|
||||||
|
lua_pushinteger(L, joint);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// jointDelete(joint)
|
||||||
|
static int32_t apiJointDelete(lua_State *L) {
|
||||||
|
int32_t joint;
|
||||||
|
|
||||||
|
_argCheck(L, "jointDelete", 1, 1);
|
||||||
|
joint = _argInteger(L, "jointDelete", 1);
|
||||||
|
if (!jointDelete(joint)) {
|
||||||
|
_luaDie(L, "jointDelete", "No joint %d.", joint);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// joint = jointHinge(nodeA, nodeB, ax, ay, az, dx, dy, dz): anchor and axis in world space; nodeB -1 is the world
|
||||||
|
static int32_t apiJointHinge(lua_State *L) {
|
||||||
|
int32_t nodeA;
|
||||||
|
int32_t nodeB;
|
||||||
|
int32_t joint;
|
||||||
|
|
||||||
|
_argCheck(L, "jointHinge", 8, 8);
|
||||||
|
nodeA = _argBody(L, "jointHinge", 1);
|
||||||
|
nodeB = _argInteger(L, "jointHinge", 2);
|
||||||
|
if ((nodeB != -1) && !bodyExists(nodeB)) {
|
||||||
|
_luaDie(L, "jointHinge", "Node %d has no body.", nodeB);
|
||||||
|
}
|
||||||
|
joint = jointNew(JOINT_HINGE, nodeA, nodeB, _argVec3(L, "jointHinge", 3), _argVec3(L, "jointHinge", 6));
|
||||||
|
if (joint < 0) {
|
||||||
|
_luaDie(L, "jointHinge", "Unable to create the joint.");
|
||||||
|
}
|
||||||
|
lua_pushinteger(L, joint);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// jointSetLimits(joint, low, high): degrees for a hinge, distance for a slider
|
||||||
|
static int32_t apiJointSetLimits(lua_State *L) {
|
||||||
|
int32_t joint;
|
||||||
|
|
||||||
|
_argCheck(L, "jointSetLimits", 3, 3);
|
||||||
|
joint = _argInteger(L, "jointSetLimits", 1);
|
||||||
|
if (!jointSetLimits(joint, (float)_argNumber(L, "jointSetLimits", 2), (float)_argNumber(L, "jointSetLimits", 3))) {
|
||||||
|
_luaDie(L, "jointSetLimits", "Joint %d has no limits to set.", joint);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// joint = jointSlider(nodeA, nodeB, ax, ay, az, dx, dy, dz): slides along the axis through the anchor; nodeB -1 is the world
|
||||||
|
static int32_t apiJointSlider(lua_State *L) {
|
||||||
|
int32_t nodeA;
|
||||||
|
int32_t nodeB;
|
||||||
|
int32_t joint;
|
||||||
|
|
||||||
|
_argCheck(L, "jointSlider", 8, 8);
|
||||||
|
nodeA = _argBody(L, "jointSlider", 1);
|
||||||
|
nodeB = _argInteger(L, "jointSlider", 2);
|
||||||
|
if ((nodeB != -1) && !bodyExists(nodeB)) {
|
||||||
|
_luaDie(L, "jointSlider", "Node %d has no body.", nodeB);
|
||||||
|
}
|
||||||
|
joint = jointNew(JOINT_SLIDER, nodeA, nodeB, _argVec3(L, "jointSlider", 3), _argVec3(L, "jointSlider", 6));
|
||||||
|
if (joint < 0) {
|
||||||
|
_luaDie(L, "jointSlider", "Unable to create the joint.");
|
||||||
|
}
|
||||||
|
lua_pushinteger(L, joint);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// scancode = keyboardGetLastDown() Cleared every frame.
|
// scancode = keyboardGetLastDown() Cleared every frame.
|
||||||
static int32_t apiKeyboardGetLastDown(lua_State *L) {
|
static int32_t apiKeyboardGetLastDown(lua_State *L) {
|
||||||
_luaTrace(L, "keyboardGetLastDown", "%d", _global.keyboardLastDown);
|
_luaTrace(L, "keyboardGetLastDown", "%d", _global.keyboardLastDown);
|
||||||
|
|
@ -4628,6 +4762,32 @@ static int32_t apiOverlaySetResolution(lua_State *L) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// node, hx, hy, hz, nx, ny, nz = physicsRaycast(x, y, z, dx, dy, dz [, maxDistance]): nil when nothing is hit
|
||||||
|
static int32_t apiPhysicsRaycast(lua_State *L) {
|
||||||
|
Vec3T origin;
|
||||||
|
Vec3T direction;
|
||||||
|
float maxDistance = 0.0f;
|
||||||
|
int32_t node;
|
||||||
|
Vec3T point;
|
||||||
|
Vec3T normal;
|
||||||
|
|
||||||
|
_argCheck(L, "physicsRaycast", 6, 7);
|
||||||
|
origin = _argVec3(L, "physicsRaycast", 1);
|
||||||
|
direction = _argVec3(L, "physicsRaycast", 4);
|
||||||
|
if (lua_gettop(L) >= 7) {
|
||||||
|
maxDistance = (float)_argNumber(L, "physicsRaycast", 7);
|
||||||
|
}
|
||||||
|
if (!physicsRaycast(origin, direction, maxDistance, &node, &point, &normal)) {
|
||||||
|
lua_pushnil(L);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
lua_pushinteger(L, node);
|
||||||
|
_pushVec3(L, point);
|
||||||
|
_pushVec3(L, normal);
|
||||||
|
return 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// physicsSetEnabled(bool): pauses the simulation without losing it
|
// physicsSetEnabled(bool): pauses the simulation without losing it
|
||||||
static int32_t apiPhysicsSetEnabled(lua_State *L) {
|
static int32_t apiPhysicsSetEnabled(lua_State *L) {
|
||||||
_argCheck(L, "physicsSetEnabled", 1, 1);
|
_argCheck(L, "physicsSetEnabled", 1, 1);
|
||||||
|
|
@ -6173,6 +6333,7 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
|
||||||
lua_register(_global.luaContext, "bodySetEnabled", apiBodySetEnabled); // 3.00
|
lua_register(_global.luaContext, "bodySetEnabled", apiBodySetEnabled); // 3.00
|
||||||
lua_register(_global.luaContext, "bodySetFriction", apiBodySetFriction); // 3.00
|
lua_register(_global.luaContext, "bodySetFriction", apiBodySetFriction); // 3.00
|
||||||
lua_register(_global.luaContext, "bodySetMass", apiBodySetMass); // 3.00
|
lua_register(_global.luaContext, "bodySetMass", apiBodySetMass); // 3.00
|
||||||
|
lua_register(_global.luaContext, "bodySetTrigger", apiBodySetTrigger); // 3.00
|
||||||
lua_register(_global.luaContext, "bodySetVelocity", apiBodySetVelocity); // 3.00
|
lua_register(_global.luaContext, "bodySetVelocity", apiBodySetVelocity); // 3.00
|
||||||
lua_register(_global.luaContext, "colorBackground", apiColorBackground); // 1.xx
|
lua_register(_global.luaContext, "colorBackground", apiColorBackground); // 1.xx
|
||||||
lua_register(_global.luaContext, "colorForeground", apiColorForeground); // 1.xx
|
lua_register(_global.luaContext, "colorForeground", apiColorForeground); // 1.xx
|
||||||
|
|
@ -6216,6 +6377,11 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
|
||||||
lua_register(_global.luaContext, "fontToSprite", apiFontToSprite); // 1.xx
|
lua_register(_global.luaContext, "fontToSprite", apiFontToSprite); // 1.xx
|
||||||
lua_register(_global.luaContext, "fontUnload", apiFontUnload); // 2.00
|
lua_register(_global.luaContext, "fontUnload", apiFontUnload); // 2.00
|
||||||
|
|
||||||
|
lua_register(_global.luaContext, "jointBall", apiJointBall); // 3.00
|
||||||
|
lua_register(_global.luaContext, "jointDelete", apiJointDelete); // 3.00
|
||||||
|
lua_register(_global.luaContext, "jointHinge", apiJointHinge); // 3.00
|
||||||
|
lua_register(_global.luaContext, "jointSetLimits", apiJointSetLimits); // 3.00
|
||||||
|
lua_register(_global.luaContext, "jointSlider", apiJointSlider); // 3.00
|
||||||
lua_register(_global.luaContext, "keyboardGetLastDown", apiKeyboardGetLastDown); // 2.10
|
lua_register(_global.luaContext, "keyboardGetLastDown", apiKeyboardGetLastDown); // 2.10
|
||||||
lua_register(_global.luaContext, "keyboardGetLastUp", apiKeyboardGetLastUp); // 2.10
|
lua_register(_global.luaContext, "keyboardGetLastUp", apiKeyboardGetLastUp); // 2.10
|
||||||
lua_register(_global.luaContext, "keyboardGetMode", apiKeyboardGetMode); // 1.xx RDG
|
lua_register(_global.luaContext, "keyboardGetMode", apiKeyboardGetMode); // 1.xx RDG
|
||||||
|
|
@ -6291,6 +6457,7 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
|
||||||
lua_register(_global.luaContext, "overlayPrint", apiOverlayPrint); // 1.xx
|
lua_register(_global.luaContext, "overlayPrint", apiOverlayPrint); // 1.xx
|
||||||
lua_register(_global.luaContext, "overlaySetResolution", apiOverlaySetResolution); // 2.00
|
lua_register(_global.luaContext, "overlaySetResolution", apiOverlaySetResolution); // 2.00
|
||||||
|
|
||||||
|
lua_register(_global.luaContext, "physicsRaycast", apiPhysicsRaycast); // 3.00
|
||||||
lua_register(_global.luaContext, "physicsSetEnabled", apiPhysicsSetEnabled); // 3.00
|
lua_register(_global.luaContext, "physicsSetEnabled", apiPhysicsSetEnabled); // 3.00
|
||||||
lua_register(_global.luaContext, "physicsSetGravity", apiPhysicsSetGravity); // 3.00
|
lua_register(_global.luaContext, "physicsSetGravity", apiPhysicsSetGravity); // 3.00
|
||||||
lua_register(_global.luaContext, "sceneEnable", apiSceneEnable); // 3.00
|
lua_register(_global.luaContext, "sceneEnable", apiSceneEnable); // 3.00
|
||||||
|
|
@ -6849,6 +7016,7 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, Co
|
||||||
// 3D scene
|
// 3D scene
|
||||||
modelUpdate(!_global.frozen);
|
modelUpdate(!_global.frozen);
|
||||||
physicsUpdate(!_global.frozen);
|
physicsUpdate(!_global.frozen);
|
||||||
|
_physicsCallbacks();
|
||||||
sceneUpdateVideo(_sceneVideoSource);
|
sceneUpdateVideo(_sceneVideoSource);
|
||||||
sceneTexture = sceneRender();
|
sceneTexture = sceneRender();
|
||||||
if (sceneTexture != NULL) {
|
if (sceneTexture != NULL) {
|
||||||
|
|
|
||||||
|
|
@ -24,3 +24,5 @@ screenshot or two and quits by itself; the results are in screenshots/.
|
||||||
| scene7.singe | 7 | 18.6b | Shadows: sun, sun and spot together, a point light inside |
|
| scene7.singe | 7 | 18.6b | Shadows: sun, sun and spot together, a point light inside |
|
||||||
| scene8.singe | 8 | 18 | An arcade cabinet from primitives with the disc on screen |
|
| scene8.singe | 8 | 18 | An arcade cabinet from primitives with the disc on screen |
|
||||||
| scene9.singe | 9 | 19.2 | Physics: a crate stack, a ball, a kinematic paddle |
|
| scene9.singe | 9 | 19.2 | Physics: a crate stack, a ball, a kinematic paddle |
|
||||||
|
| scene10.singe | 10 | 19.3 | Physics: a trigger volume, collision callbacks, a raycast pick |
|
||||||
|
| scene11.singe | 11 | 19.4 | Physics: a hull down a ramp, a mesh torus, hinge, ball and slider joints |
|
||||||
|
|
|
||||||
|
|
@ -98,4 +98,26 @@ GAMES = {
|
||||||
DEVELOPER = "Test",
|
DEVELOPER = "Test",
|
||||||
PUBLISHER = "Test",
|
PUBLISHER = "Test",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
TITLE = "Physics2",
|
||||||
|
SCRIPT = "testScripts/scene10.singe",
|
||||||
|
VIDEO = "Singe/menuBackground.mkv",
|
||||||
|
DESCRIPTION = "A trigger volume, collision callbacks and a raycast pick.",
|
||||||
|
YEAR = 2026,
|
||||||
|
GENRE = "Test",
|
||||||
|
PLATFORM = "Singe",
|
||||||
|
DEVELOPER = "Test",
|
||||||
|
PUBLISHER = "Test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TITLE = "Physics3",
|
||||||
|
SCRIPT = "testScripts/scene11.singe",
|
||||||
|
VIDEO = "Singe/menuBackground.mkv",
|
||||||
|
DESCRIPTION = "A hull rolling down a ramp, a mesh torus, a hinged door, a pendulum, a slider.",
|
||||||
|
YEAR = 2026,
|
||||||
|
GENRE = "Test",
|
||||||
|
PLATFORM = "Singe",
|
||||||
|
DEVELOPER = "Test",
|
||||||
|
PUBLISHER = "Test",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
122
testScripts/scene10.singe
Normal file
122
testScripts/scene10.singe
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
-- Physics stage 3: a trigger volume in the crates' path, collision callbacks, and a raycast
|
||||||
|
-- through the middle of the overlay that flicks whatever it hits.
|
||||||
|
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
|
||||||
|
local frames = 0
|
||||||
|
local entered, left, collisions, lastSpeed, picks = 0, 0, 0, 0, 0
|
||||||
|
|
||||||
|
fontSelect(font)
|
||||||
|
discPlay()
|
||||||
|
sceneEnable(true)
|
||||||
|
sceneSetBackground(18, 18, 30, 255)
|
||||||
|
sceneSetAmbient(70, 70, 85)
|
||||||
|
|
||||||
|
local floorLook = materialNew()
|
||||||
|
materialSetColor(floorLook, 120, 110, 100)
|
||||||
|
materialSetRoughness(floorLook, 0.9)
|
||||||
|
local crateLook = materialNew()
|
||||||
|
materialSetColor(crateLook, 200, 140, 60)
|
||||||
|
local ballLook = materialNew()
|
||||||
|
materialSetColor(ballLook, 60, 140, 230)
|
||||||
|
local paddleLook = materialNew()
|
||||||
|
materialSetColor(paddleLook, 230, 60, 40)
|
||||||
|
local zoneLook = materialNew()
|
||||||
|
materialSetColor(zoneLook, 80, 255, 120, 70)
|
||||||
|
materialSetBlend(zoneLook, true)
|
||||||
|
materialSetUnlit(zoneLook, true)
|
||||||
|
local markLook = materialNew()
|
||||||
|
materialSetColor(markLook, 255, 255, 80)
|
||||||
|
materialSetEmissive(markLook, 120, 120, 0)
|
||||||
|
|
||||||
|
local floor = nodeNew()
|
||||||
|
nodeSetMesh(floor, meshBox(14, 0.2, 14), floorLook)
|
||||||
|
nodeSetPosition(floor, 0, -1.9, 0)
|
||||||
|
bodyNew(floor, BODY_STATIC, SHAPE_BOX, 14, 0.2, 14)
|
||||||
|
|
||||||
|
local crates = {}
|
||||||
|
for i = 1, 4 do
|
||||||
|
local crate = nodeNew()
|
||||||
|
nodeSetMesh(crate, meshBox(0.6, 0.6, 0.6), crateLook)
|
||||||
|
nodeSetPosition(crate, -1.0, -1.5 + (i - 1) * 0.62, 0)
|
||||||
|
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.6, 0.6, 0.6)
|
||||||
|
crates[i] = crate
|
||||||
|
end
|
||||||
|
|
||||||
|
local ball = nodeNew()
|
||||||
|
nodeSetMesh(ball, meshSphere(0.35, 32), ballLook)
|
||||||
|
nodeSetPosition(ball, 0.2, 1.5, 0.3)
|
||||||
|
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.35)
|
||||||
|
bodySetBounce(ball, 0.5)
|
||||||
|
|
||||||
|
-- The trigger: a translucent green volume to the right of the stack.
|
||||||
|
local zone = nodeNew()
|
||||||
|
nodeSetMesh(zone, meshBox(1.6, 1.6, 2.4), zoneLook)
|
||||||
|
nodeSetPosition(zone, 1.8, -1.0, 0)
|
||||||
|
bodyNew(zone, BODY_STATIC, SHAPE_BOX, 1.6, 1.6, 2.4)
|
||||||
|
bodySetTrigger(zone, true)
|
||||||
|
|
||||||
|
local paddle = nodeNew()
|
||||||
|
nodeSetMesh(paddle, meshBox(0.3, 1.2, 1.6), paddleLook)
|
||||||
|
nodeSetPosition(paddle, -4.0, -1.2, 0)
|
||||||
|
bodyNew(paddle, BODY_KINEMATIC, SHAPE_BOX, 0.3, 1.2, 1.6)
|
||||||
|
|
||||||
|
-- A marker placed where the pick ray hits.
|
||||||
|
local mark = nodeNew()
|
||||||
|
nodeSetMesh(mark, meshSphere(0.08, 12), markLook)
|
||||||
|
nodeSetVisible(mark, false)
|
||||||
|
|
||||||
|
local sun = lightNew(LIGHT_DIRECTIONAL)
|
||||||
|
nodeSetPosition(sun, -3, 6, 4)
|
||||||
|
nodeLookAt(sun, 0, 0, 0)
|
||||||
|
lightSetIntensity(sun, 1.2)
|
||||||
|
lightSetShadow(sun, true)
|
||||||
|
|
||||||
|
local camera = nodeNew()
|
||||||
|
nodeSetPosition(camera, 1.0, 1.6, 6.5)
|
||||||
|
nodeLookAt(camera, 0.2, -0.6, 0)
|
||||||
|
cameraSet(camera)
|
||||||
|
|
||||||
|
function onCollision(a, b, x, y, z, speed)
|
||||||
|
collisions = collisions + 1
|
||||||
|
lastSpeed = speed
|
||||||
|
end
|
||||||
|
|
||||||
|
function onTrigger(trigger, other, isEntering)
|
||||||
|
if isEntering then
|
||||||
|
entered = entered + 1
|
||||||
|
else
|
||||||
|
left = left + 1
|
||||||
|
end
|
||||||
|
debugPrint(string.format("trigger %d %s node %d", trigger, isEntering and "entered by" or "left by", other))
|
||||||
|
end
|
||||||
|
|
||||||
|
function onOverlayUpdate()
|
||||||
|
frames = frames + 1
|
||||||
|
overlayClear()
|
||||||
|
fontPrint(20, 20, string.format("Physics %d hits %d (%.1f) in %d out %d picks %d", frames, collisions, lastSpeed, entered, left, picks))
|
||||||
|
if frames >= 60 and frames < 150 then
|
||||||
|
nodeMove(paddle, 0.06, 0, 0)
|
||||||
|
end
|
||||||
|
if frames % 30 == 0 and frames <= 240 then
|
||||||
|
-- A ray from the camera through the overlay's centre; flick whatever it hits upward.
|
||||||
|
local w, h = sceneGetSize()
|
||||||
|
local ox, oy, oz = sceneUnproject(w / 2, h / 2, 0)
|
||||||
|
local fx, fy, fz = sceneUnproject(w / 2, h / 2, 10)
|
||||||
|
local hit, hx, hy, hz = physicsRaycast(ox, oy, oz, fx - ox, fy - oy, fz - oz)
|
||||||
|
if hit then
|
||||||
|
picks = picks + 1
|
||||||
|
nodeSetVisible(mark, true)
|
||||||
|
nodeSetPosition(mark, hx, hy, hz)
|
||||||
|
if hit ~= floor and hit ~= zone and hit ~= paddle then
|
||||||
|
bodyApplyImpulse(hit, 0, 3, 0, hx, hy, hz)
|
||||||
|
end
|
||||||
|
debugPrint(string.format("pick node %d at %.2f %.2f %.2f", hit, hx, hy, hz))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if frames == 50 or frames == 130 or frames == 220 then
|
||||||
|
singeScreenshot()
|
||||||
|
end
|
||||||
|
if frames == 250 then
|
||||||
|
debugPrint(string.format("RESULT collisions=%d entered=%d left=%d picks=%d", collisions, entered, left, picks))
|
||||||
|
singeQuit()
|
||||||
|
end
|
||||||
|
end
|
||||||
125
testScripts/scene11.singe
Normal file
125
testScripts/scene11.singe
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
-- Physics stage 4: the Duck as a convex hull rolling down a ramp, a torus as a static mesh shape
|
||||||
|
-- the ball rolls around, a swinging door on a hinge with limits, a pendulum on a ball joint, and
|
||||||
|
-- a crate on a slider.
|
||||||
|
local font = fontLoad("Singe/FreeSansBold.ttf", 28)
|
||||||
|
local frames = 0
|
||||||
|
|
||||||
|
fontSelect(font)
|
||||||
|
discPlay()
|
||||||
|
sceneEnable(true)
|
||||||
|
sceneSetBackground(18, 18, 30, 255)
|
||||||
|
sceneSetAmbient(70, 70, 85)
|
||||||
|
|
||||||
|
local floorLook = materialNew()
|
||||||
|
materialSetColor(floorLook, 120, 110, 100)
|
||||||
|
materialSetRoughness(floorLook, 0.9)
|
||||||
|
local rampLook = materialNew()
|
||||||
|
materialSetColor(rampLook, 90, 100, 130)
|
||||||
|
local doorLook = materialNew()
|
||||||
|
materialSetColor(doorLook, 160, 90, 40)
|
||||||
|
local ballLook = materialNew()
|
||||||
|
materialSetColor(ballLook, 60, 140, 230)
|
||||||
|
local ringLook = materialNew()
|
||||||
|
materialSetColor(ringLook, 220, 200, 60)
|
||||||
|
materialSetMetallic(ringLook, 0.8)
|
||||||
|
materialSetRoughness(ringLook, 0.35)
|
||||||
|
local frameLook = materialNew()
|
||||||
|
materialSetColor(frameLook, 40, 40, 50)
|
||||||
|
local crateLook = materialNew()
|
||||||
|
materialSetColor(crateLook, 200, 140, 60)
|
||||||
|
|
||||||
|
local floor = nodeNew()
|
||||||
|
nodeSetMesh(floor, meshBox(16, 0.2, 16), floorLook)
|
||||||
|
nodeSetPosition(floor, 0, -1.9, 0)
|
||||||
|
bodyNew(floor, BODY_STATIC, SHAPE_BOX, 16, 0.2, 16)
|
||||||
|
|
||||||
|
-- A ramp, and the Duck as a convex hull rolling down it.
|
||||||
|
local ramp = nodeNew()
|
||||||
|
nodeSetMesh(ramp, meshBox(3.0, 0.2, 2.2), rampLook)
|
||||||
|
nodeSetPosition(ramp, -3.4, -1.0, -1.0)
|
||||||
|
nodeSetRotation(ramp, 0, 0, -28)
|
||||||
|
bodyNew(ramp, BODY_STATIC, SHAPE_BOX, 3.0, 0.2, 2.2)
|
||||||
|
|
||||||
|
local duck = modelInstance(modelLoad("testScripts/Models/Duck.glb"))
|
||||||
|
nodeSetPosition(duck, -4.4, 0.6, -1.0)
|
||||||
|
nodeSetRotation(duck, 0, 90, 0)
|
||||||
|
nodeSetScale(duck, 0.6)
|
||||||
|
bodyNew(duck, BODY_DYNAMIC, SHAPE_HULL)
|
||||||
|
bodySetFriction(duck, 0.4)
|
||||||
|
|
||||||
|
-- A torus lying on the floor as a static triangle mesh; the ball rolls around inside it.
|
||||||
|
local ring = nodeNew()
|
||||||
|
nodeSetMesh(ring, meshTorus(1.2, 0.25, 40), ringLook)
|
||||||
|
nodeSetPosition(ring, 2.6, -1.55, 0.6)
|
||||||
|
bodyNew(ring, BODY_STATIC, SHAPE_MESH)
|
||||||
|
|
||||||
|
local ball = nodeNew()
|
||||||
|
nodeSetMesh(ball, meshSphere(0.3, 32), ballLook)
|
||||||
|
nodeSetPosition(ball, 2.6, 1.2, 0.6)
|
||||||
|
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.3)
|
||||||
|
bodySetBounce(ball, 0.4)
|
||||||
|
|
||||||
|
-- A door hinged to the world along its left edge, limited to swing 100 degrees one way.
|
||||||
|
local post = nodeNew()
|
||||||
|
nodeSetMesh(post, meshBox(0.15, 2.4, 0.15), frameLook)
|
||||||
|
nodeSetPosition(post, -1.0, -0.6, 1.4)
|
||||||
|
bodyNew(post, BODY_STATIC, SHAPE_BOX, 0.15, 2.4, 0.15)
|
||||||
|
|
||||||
|
local door = nodeNew()
|
||||||
|
nodeSetMesh(door, meshBox(1.4, 2.2, 0.1), doorLook)
|
||||||
|
nodeSetPosition(door, -0.1, -0.62, 1.4)
|
||||||
|
bodyNew(door, BODY_DYNAMIC, SHAPE_BOX, 1.4, 2.2, 0.1)
|
||||||
|
bodySetMass(door, 8)
|
||||||
|
local hinge = jointHinge(door, -1, -0.85, -0.62, 1.4, 0, 1, 0)
|
||||||
|
jointSetLimits(hinge, 0, 100)
|
||||||
|
|
||||||
|
-- A pendulum: a ball hanging from a point in the air.
|
||||||
|
local bob = nodeNew()
|
||||||
|
nodeSetMesh(bob, meshSphere(0.25, 24), ballLook)
|
||||||
|
nodeSetPosition(bob, 1.4, 1.6, -1.6)
|
||||||
|
bodyNew(bob, BODY_DYNAMIC, SHAPE_SPHERE, 0.25)
|
||||||
|
jointBall(bob, -1, 0.2, 2.6, -1.6)
|
||||||
|
|
||||||
|
-- A crate on a slider along X, pushed sideways at the start.
|
||||||
|
local rail = nodeNew()
|
||||||
|
nodeSetMesh(rail, meshBox(3.0, 0.08, 0.08), frameLook)
|
||||||
|
nodeSetPosition(rail, 0.6, 0.9, -3.0)
|
||||||
|
local crate = nodeNew()
|
||||||
|
nodeSetMesh(crate, meshBox(0.5, 0.5, 0.5), crateLook)
|
||||||
|
nodeSetPosition(crate, -0.6, 0.9, -3.0)
|
||||||
|
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.5, 0.5, 0.5)
|
||||||
|
local slider = jointSlider(crate, -1, -0.6, 0.9, -3.0, 1, 0, 0)
|
||||||
|
jointSetLimits(slider, 0, 2.4)
|
||||||
|
bodySetVelocity(crate, 3, 0, 0)
|
||||||
|
|
||||||
|
local sun = lightNew(LIGHT_DIRECTIONAL)
|
||||||
|
nodeSetPosition(sun, -3, 6, 4)
|
||||||
|
nodeLookAt(sun, 0, 0, 0)
|
||||||
|
lightSetIntensity(sun, 1.2)
|
||||||
|
lightSetShadow(sun, true)
|
||||||
|
|
||||||
|
local camera = nodeNew()
|
||||||
|
nodeSetPosition(camera, 0.8, 2.4, 7.5)
|
||||||
|
nodeLookAt(camera, 0, -0.4, -0.5)
|
||||||
|
cameraSet(camera)
|
||||||
|
cameraSetPerspective(52, 0.1, 100)
|
||||||
|
|
||||||
|
function onOverlayUpdate()
|
||||||
|
frames = frames + 1
|
||||||
|
overlayClear()
|
||||||
|
fontPrint(20, 20, "Physics joints and hulls, frame " .. frames)
|
||||||
|
if frames == 40 then
|
||||||
|
-- Kick the door and the pendulum.
|
||||||
|
bodyApplyImpulse(door, 0, 0, -60, 0.5, -0.62, 1.45)
|
||||||
|
bodyApplyImpulse(bob, 6, 0, 0)
|
||||||
|
end
|
||||||
|
if frames == 30 or frames == 90 or frames == 180 then
|
||||||
|
singeScreenshot()
|
||||||
|
end
|
||||||
|
if frames == 200 then
|
||||||
|
local dx, dy, dz = nodeGetWorldPosition(duck)
|
||||||
|
local cx = nodeGetWorldPosition(crate)
|
||||||
|
debugPrint(string.format("RESULT duck %.2f %.2f %.2f crate x %.2f", dx, dy, dz, cx))
|
||||||
|
singeQuit()
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Add table
Reference in a new issue