drawcall-ai/skills

physics

Add BVH-based physics for static/kinematic geometry with @pmndrs/viverse — physics worlds, character controllers, ground detection, and sensor volumes. Use when a game needs collision, a character controller, or enter/exit trigger volumes.

First seen Jun 17, 2026

Installation

$ npx skills add drawcall-ai/skills --skill physics

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from drawcall-ai/skills · top by installs.

npx skills add drawcall-ai/skills

Browse all from drawcall-ai/skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Also listed on

Alternate registries and mirrors of this skill.

Repository health

Default branch main
Open issues 0
Status Active

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 5,369 B
  • docs SUMMARY.md 256 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 152 installs

SKILL.md

Physics

Physics using BVH (Bounding Volume Hierarchy) for static/kinematic geometry. Dynamic bodies cannot interact with each other yet.

Physics World

Creating a physics world

import { BvhPhysicsWorld } from '@pmndrs/viverse'

const world = new BvhPhysicsWorld()

addBody(object, kinematic) — the second argument is whether the body is kinematic (script-moved, like a platform or train), not whether it is static. Pass false for static world geometry (ground, walls), true for things you move yourself.

world.addBody(ground, false) // static
world.addBody(train, true)   // kinematic (you move it each frame)

Watch the inversion: addBody's flag is kinematic, but addSensor's second flag below is isStatic — the booleans mean opposite things.

Character Controller

BvhCharacterPhysics is the character controller — it moves and collides the character body. It does not animate the character and is not a camera. For a full controllable humanoid, compose it with CharacterCameraBehavior (camera) and Acta for all animation and the motion it produces (see the acta skill). Do not use SimpleCharacter — it bundles its own animation that conflicts with Acta.

Creating a character controller

import { BvhCharacterPhysics, type BvhCharacterPhysicsOptions } from '@pmndrs/viverse'

const characterPhysics = new BvhCharacterPhysics(world)

The controller assumes the character object's origin sits at its feet (the bottom of the capsule). If your model's origin is at its center or head, the character floats or sinks — wrap it in a Group whose origin is at the feet. Tuning lives in the optional BvhCharacterPhysicsOptions (third arg to update): capsuleRadius, capsuleHeight, gravity, maxGroundSlope, updatesPerSecond, linearDamping.

Driving it

characterPhysics.inputVelocity.copy(velocity) // continuous (walking) — set each frame
characterPhysics.applyVelocity(jumpImpulse) // one-off impulse (e.g. jumping)

For an Acta character the path of information is input → Acta → physics: you pass Acta a movement intent (a camera-relative moveDirection, length 0..1), Acta turns the active animation into a desired velocity and hands it to you in its motion callback, and you set inputVelocity from that velocity and step the controller there. Do not set inputVelocity from raw input, and do not move the character anywhere else. See the acta skill.

Updating the controller

Call every frame after setting velocity:

characterPhysics.update(character, delta) // pass a BvhCharacterPhysicsOptions third arg to tune

Ground State

characterPhysics.isGrounded // true when character is on the ground

What the controller does not do — build these yourself

The controller gives you exactly two primitives — inputVelocity (continuous) and applyVelocity (impulse) — plus isGrounded. The rules a specific game layers on top are yours to write:

  • Multi-jump / double-jump, coyote-time, jump buffering: there is no jump counter — gate the first applyVelocity on isGrounded, track your own air-jump count, and reset it when grounded.
  • Wall-jump / wall-slide: isGrounded reports ground only, not walls. Detect a wall with a horizontal world.raycast(ray, far) from the capsule — the returned intersection gives the hit distance and surface normal — then applyVelocity away-from-wall + up.
  • Moving-platform ride-along: standing on a kinematic platform does not carry the character. Add the platform's per-frame delta to the character yourself while it stands on it.
  • Falling off the world / out of bounds: isGrounded is true only when the capsule's downward shapecast hits collision geometry — past the edge of a finite ground mesh there is nothing to hit, so the character falls forever and stays stuck in its airborne/fall pose (which often reads as an unintended "flying" or glide state). The controller will not stop this. Keep the player on solid ground yourself: clamp position to the collidable area, add an out-of-bounds floor plane or a kill-barrier that respawns, rather than assuming the world is infinite. Bound the player to the physics mesh, which may extend past the visible play area.

These are gameplay rules, not missing features — the controller stays deliberately minimal so each game tunes its own feel.

Sensors

Sensors detect when the character enters or exits a volume.

Adding a sensor

world.addSensor(object, isStatic, (intersected) => {
  if (intersected) {
    // character entered the sensor
  } else {
    // character exited the sensor
  }
})

Removing a sensor

world.removeSensor(object)

If you prefer a single callback that provides the sensor object that the character entered, do the following

function onEnter(object: Object3D) {}

world.addSensor(obj1, isStatic, (intersected) => intersected && onEnter(obj1))
world.addSensor(obj2, isStatic, (intersected) => intersected && onEnter(obj2))