Voxel-ball bugs - ghost collisions and non-collisions
- 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
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