dimforge / dimforge/rapier

Voxel-ball bugs - ghost collisions and non-collisions

Open
#993 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
5.7k
Forks
387
Avg merge
4d 23h
Merged PRs (30d)
6

Description

Hi! I'm experimenting with Rapier for physics in a voxel world - mostly things work great, but I'm seeing a lot of non-physical behavior when ball primitives collide with edges or corners of adjacent voxels. I'm using Typescript but at a guess it's probably not TS related.

Visually, here's a repro of an egregious example - a ball object shoots into a 3x3x3 block of voxels, and depending on the positioning will pass through the chunk, only bouncing around the inside.

https://github.com/user-attachments/assets/a8838de6-47e3-4b40-aa3e-864fb1a22ef2

Code reproduction is below.

I noticed [this TODO comment](https://github.com/dimforge/parry/blob/v0.25.1/src/query/contact_manifolds/contact_manifolds_voxels_ball.rs#L46) in the voxels-ball collision code, so I guess maybe this results from a known issue? If so, I thought maybe the repro steps might make it easier to fix.

But I should note that the thing in the video isn't the only bug I saw - generally whenever a ball primitive collides with the edge of two adjacent voxels, depending on the angle/velocity/etc the collision often sends the ball off in a nonphysical direction.

Thanks for checking!

----

Minimal repro of what's in the video (in an empty world except for the 3x3x3 span of solid voxels). If you check the console logs, after 15-20 ticks the ball projectile is fully within the solid voxels.

```ts
import RAPIER from "@dimforge/rapier3d-compat"

const timestep = 1 / 60
const ticks = 20
const projectileRadius = 0.25

const blockMinimum = 5
const blockMaximum = 7

const voxelData = new Int32Array(3 * 3 * 3 * 3)
let index = 0
for (let y = blockMinimum; y <= blockMaximum; y += 1) {
for (let z = blockMinimum; z <= blockMaximum; z += 1) {
for (let x = blockMinimum; x <= blockMaximum; x += 1) {
voxelData[index] = x
voxelData[index + 1] = y
voxelData[index + 2] = z
index += 3
}
}
}

/**
* Reproduces a sphere shooting into the top edge of a 3×3×3 voxel cube,
* then passing into the interior and bouncing around inside.
*/
export const runReproduction = async () => {
await RAPIER.init()

const world = new RAPIER.World({ x: 0, y: -10, z: 0 })
const events = new RAPIER.EventQueue(true)
try {
const terrain = RAPIER.ColliderDesc.voxels(voxelData, { x: 1, y: 1, z: 1 })
.setFriction(0.8)
.setRestitution(0)
world.createCollider(terrain)

const projectile = world.createRigidBody(
RAPIER.RigidBodyDesc.dynamic()
.setTranslation(5.068835807168076, 10.666715670475918, 10.803566521990922)
.setLinvel(5.966327745035736, -12.77696104762374, -14.182813529984893)
.setCcdEnabled(true),
)
const projectileCollider = RAPIER.ColliderDesc.ball(projectileRadius)
.setDensity(1)
.setFriction(0.8)
.setRestitution(0.45)
.setActiveEvents(RAPIER.ActiveEvents.COLLISION_EVENTS)
world.createCollider(projectileCollider, projectile)

let collisionStartCount = 0
for (let tick = 0; tick < ticks; tick += 1) {
world.timestep = timestep
world.step(events)
events.drainCollisionEvents((_first, _second, started) => {
if (started) collisionStartCount += 1
if (started) console.log(`tick ${tick + 1}: collision start event`)
})
console.log(`tick ${tick + 1}: position=${JSON.stringify(projectile.translation())}`)
}

return {
position: projectile.translation(),
velocity: projectile.linvel(),
collisionStartCount,
}
} finally {
events.free()
world.free()
}
}

runReproduction().then((result) => {
console.info("Rapier voxel-ball repro result", result)
})
```

Log output:
```
$ node repro.ts
using deprecated parameters for the initialization function; pass a single object instead
tick 1: position={"x":5.168275356292725,"y":10.452030181884766,"z":10.567185401916504}
tick 2: position={"x":5.267714977264404,"y":10.234565734863281,"z":10.330803871154785}
tick 3: position={"x":5.367154598236084,"y":10.014324188232422,"z":10.094422340393066}
tick 4: position={"x":5.466594219207764,"y":9.791305541992188,"z":9.858040809631348}
tick 5: position={"x":5.566033840179443,"y":9.565509796142578,"z":9.621659278869629}
tick 6: position={"x":5.665473461151123,"y":9.336936950683594,"z":9.38527774810791}
tick 7: position={"x":5.764913082122803,"y":9.105583190917969,"z":9.148896217346191}
tick 8: position={"x":5.864352703094482,"y":8.871452331542969,"z":8.912514686584473}
tick 9: position={"x":5.963792324066162,"y":8.634544372558594,"z":8.676133155822754}
tick 10: position={"x":6.063231945037842,"y":8.394859313964844,"z":8.439751625061035}
tick 11: position={"x":6.1626715660095215,"y":8.152397155761719,"z":8.203370094299316}
tick 12: position={"x":6.262111186981201,"y":7.907155990600586,"z":7.966989040374756}
tick 13: collision start event
tick 13: position={"x":6.361550807952881,"y":7.65913724899292,"z":7.73060941696167}
tick 14: position={"x":6.4609904289245605,"y":7.408339977264404,"z":7.494229793548584}
tick 15: position={"x":6.56043004989624,"y":7.154765605926514,"z":7.257850170135498}
tick 16: position={"x":6.65986967086792,"y":6.898413181304932,"z":7.021470546722412}
tick 17: position={"x":6.7593092918396,"y":6.639282703399658,"z":6.785090923309326}
tick 18: position={"x":6.858748912811279,"y":6.37737512588501,"z":6.54871129989624}
tick 19: position={"x":6.958188533782959,"y":6.112689018249512,"z":6.312331676483154}
tick 20: position={"x":7.057628154754639,"y":5.8452253341674805,"z":6.075952053070068}
Rapier voxel-ball repro result {
position: fA {
x: 7.057628154754639,
y: 5.8452253341674805,
z: 6.075952053070068
},
velocity: fA {
x: 5.966327667236328,
y: -16.11031723022461,
z: -14.18281364440918
},
collisionStartCount: 1
}
```

Contributor guide

Open the contributing guide

Research direction

Start by running the TypeScript reproduction and comparing its logged positions, velocity, and collision events with the expected behavior. Then read parry/src/query/contact_manifolds/contact_manifolds_voxels_ball.rs, especially the TODO near line 46, and trace the voxel-ball contact handling. Done means balls remain outside solid voxel blocks and edge or corner collisions produce physical responses.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust, typescript
Domain
computer-graphics, game-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.