vpyphys — rigid bodies
vpyphys provides rigid bodies under gravity, collisions between them, joints and ray casts.
It is integer-only and deterministic: the same calls give the same positions on every target, bit for bit, so replays and host test harnesses stay valid.
It draws nothing. The game reads the positions back and draws them, with vpy3d meshes or in 2D with z = 0.
To use it, add $(VPY_C_SDK)/vpyphys.c to UVM2_SRCS.
void vpyp_reset(void);
void vpyp_set_gravity(int32_t gx,int32_t gy,int32_t gz); /* units/s²: 9800 mm/s² */
void vpyp_set_floor(int on,int32_t y,int restitution_q8,int friction_q8);
int vpyp_add_sphere(int32_t x,int32_t y,int32_t z,int32_t r,int32_t mass); /* mass 0 = static */
int vpyp_add_box(int32_t x,int32_t y,int32_t z,int32_t hx,int32_t hy,int32_t hz,int32_t mass);
int vpyp_hull_shape(const int16_t *xyz,int nverts,const uint8_t *faces); /* checked; or -1 */
int vpyp_add_hull(int32_t x,int32_t y,int32_t z,int shape,int32_t mass);
int vpyp_hull_error(void); /* why a shape was refused */
int vpyp_ball_joint(int a,int b,int32_t px,int32_t py,int32_t pz); /* b -1: the world */
int vpyp_hinge(int a,int b,int32_t px,int32_t py,int32_t pz,int32_t ax,int32_t ay,int32_t az);
void vpyp_joint_remove(int joint);
void vpyp_set_material(int id,int restitution_q8,int friction_q8);
void vpyp_set_mask(int id,uint8_t mask); /* who collides with whom */
void vpyp_set_velocity(int id,int32_t vx,int32_t vy,int32_t vz);
void vpyp_apply_impulse(int id,int32_t ix,int32_t iy,int32_t iz);
void vpyp_apply_impulse_at(int id,int32_t ix,int32_t iy,int32_t iz, int32_t px,int32_t py,int32_t pz);
void vpyp_set_rotation(int id,int32_t ax,int32_t ay,int32_t az,int angle); /* 4096 per turn */
void vpyp_set_spin(int id,int32_t wx,int32_t wy,int32_t wz); /* 4096ths of a turn per second */
void vpyp_lock_rotation(int id,int on); /* stay upright */
void vpyp_step(void); /* once per frame at 50 Hz */
void vpyp_position(int id,int32_t *x,int32_t *y,int32_t *z);
void vpyp_rotation(int id,int32_t m[9]); /* Q14, straight into vpy_xf.m */
int vpyp_contact_count(void);
const vpyp_contact *vpyp_contact_get(int i); /* .impulse: how hard */
int vpyp_raycast(int32_t ox,int32_t oy,int32_t oz, int32_t dx,int32_t dy,int32_t dz,
int32_t max_dist,uint8_t mask,vpyp_hit *out);
int vpyp_blast(int32_t cx,int32_t cy,int32_t cz,int32_t radius,int32_t speed,uint8_t mask);
const vpyp_stats_t *vpyp_stats(void); /* awake, contacts, refused */What it simulates
- Shapes: spheres, boxes and convex hulls, plus a floor with restitution and friction.
- Sleeping bodies: a pile at rest costs almost nothing.
- A contact list with the impulse of each hit. That impulse is what a dent, a spark or an impact sound reads.
- Rotation. An off-centre hit spins a body, boxes tip over and tumble, and balls roll.
vpyp_rotation()returns the rotation as a Q14 matrix that goes straight into avpy_xf. Inertia is a single number, which is exact for spheres and cubes. - Exact box collisions: box against box tests all fifteen separating axes, including edge against edge.
- Stable stacks, no tunnelling. The solver uses sequential impulses with warm starting and speculative contacts, so a stack holds and a fast body does not pass through a thin wall.
Cost: about 42 KB of code and about 99 KB of RAM, most of it the contact table.
Hulls
A hull is any convex solid:
- Register the shape once. Give its corners around the body's centre, then its faces as a count followed by that many corner indices. A mesh's own faces will do.
- Add as many bodies of that shape as you like.
The shape is checked when you register it: flat faces, convex, centre inside, tables big enough. If it fails, it is refused with a reason (vpyp_hull_error()) rather than simulated wrongly.
Keep hulls small. A pair of hulls tests every face of both and every pair of edge directions.
Joints
- A ball joint holds two bodies together at a point, or a body to the world.
- A hinge also keeps an axis lined up: a door, a wheel, a flail.
Two joined bodies do not collide with each other, and removing a body removes its joints. There are no limits and no motors. vpyp_stats()->joint_stretch reports how far apart the worst joint has been pulled.
Limits
The body table holds VPYP_MAX_BODIES (64). A full table is a limit the game handles itself: compare vpyp_stats()->bodies with the maximum before adding, as physics_demo does. A refusal is counted either way.
sdk/vpy-c/tools/phys_check.c checks the engine against formulas (free fall, braking distance, momentum) and against expected behaviour.
vpyimpact — impact sounds
vpyimpact makes the sound of things hitting each other, synthesised from the hit itself, with no samples.
Each impact is a short envelope of noise and a falling tone. It plays through libvpy's SFX player on channel C, so it shares the console's sound chip with music like any other effect.
To use it, add $(VPY_C_SDK)/vpyimpact.c to UVM2_SRCS.
enum { VPYI_SOFT, VPYI_WOOD, VPYI_METAL, VPYI_NONE = 255 };
void vpyimpact_set_range(int32_t quiet,int32_t loud); /* impulses: silent below, full from */
int vpyimpact_hit(int32_t impulse,int material); /* one hit; 1 if it sounds */
int vpyimpact_hit_at(int32_t impulse,int material,int32_t x,int32_t y,int32_t z); /* placed */
void vpyimpact_set_listener(int32_t x,int32_t y,int32_t z,int32_t right_x,int32_t right_z,
int32_t near_dist,int32_t far_dist); /* the camera, usually */
int vpyimpact_contacts(const uint8_t *material_of,int floor_material); /* after vpyp_step */
void vpyimpact_step(void); /* once per frame */
const vpyimpact_stats_t *vpyimpact_stats(void); /* played, skipped, quiet */How a hit sounds
- Volume follows the contact's impulse, on a square-root curve so that a middling knock is still heard. A resting body reports a small impulse every step, which stays under
quiet. - Material decides the voice: wood knocks, soft bumps, metal rings. When two materials meet, the one that rings more decides.
- One channel. A new hit replaces the one playing only if it is at least as loud as what that one has left. The others are counted as
skipped.
The simplest wiring is to call vpyimpact_contacts() after each vpyp_step(), with a table that gives each body's material. It turns every contact into a hit.
Where it happened
With a listener set (usually the camera), a hit gets quieter with distance: full volume up to near, silent from far. This works on every cartridge, and each contact is placed at the point where the bodies touch.
Stereo on the UVMC2's jack
On a cartridge with a 16-bit DAC (currently only the UVMC2, through its audio jack), the same voices can also be rendered as 16-bit stereo:
void vpyimpact_set_pcm(vpyimpact_sink sink,int rate,int psg_too);
void vpyimpact_pcm(int n); /* every frame: n = what the DAC takes */
int vpyimpact_loop(int slot,int32_t x,int32_t y,int32_t z,int32_t vx,int32_t vy,int32_t vz,
int hz,int volume); /* an engine: Doppler-shifted */Pass uvm2_jack_write_lr as the sink, and call vpyimpact_pcm(uvm2_jack_space()) every frame. You then get:
- hits panned to their side, up to four at once;
- loops: continuous sources such as an engine, with a pitch that follows the Doppler shift relative to the listener. Measured on the host, a 400 Hz loop approaching at 40 m/s sounds at 452 Hz (theory: 453).
physics_demo and scene_demo use all of this. sdk/vpy-c/tools/impact_check.c checks it through the real SFX player. The voices are starting values and have not been tuned further. For the jack itself, see Sound.