JOLT Physics integrated. Hopefully.

This commit is contained in:
Scott Duensing 2026-09-05 20:44:58 -05:00
parent 807e61a887
commit ada7896a9b
11 changed files with 1465 additions and 6 deletions

View file

@ -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

View file

@ -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

View file

@ -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);

View file

@ -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) {

View file

@ -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;
@ -300,9 +303,21 @@ static int32_t _addMesh(const SceneVertexT *vertices, int32_t vertexCount, const
meshDelete(x); meshDelete(x);
return NO_HANDLE; return NO_HANDLE;
} }
mesh->indexCount = (uint32_t)indexCount; mesh->indexCount = (uint32_t)indexCount;
mesh->skinned = skinned; mesh->vertexCount = vertexCount;
mesh->used = true; mesh->skinned = skinned;
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 "";

View file

@ -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);

View file

@ -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) {

View file

@ -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 |

View file

@ -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
View 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
View 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