Masks: “All bodies on layer X can collide with all bodies on layer Y.”
Per-pair: “But body #7 on layer X should ignore body #12 on layer Y right now.”
For reference, Unreal solves this with UPrimitiveComponent::IgnoreComponentWhenMoving().
As far as I can tell, there’s no way to do per-pair collision filtering with the current Havok WASM API. Bitmasks are per-layer and collision events are post-solve only.
Masks operate on layers. Per-pair operates on specific body instances.
Is there any mechanism I’m missing to ignore collision between a specific body pair at runtime?
What I have already looked at:
filterCollideMask and filterMembershipMask → bitmask only, per-layer, not per-body
collisionGroup → same limitation, group-wide not per-body
I am using a similar approach to yours for standard layer filtering. You are correct that there is no native IgnoreComponent method in the WASM API, but you can achieve per-pair exclusion by using spare bits in your masks.
Your logic is the right starting point:
private applyShapeMasks(shapes: PhysicsShape[]): void {
for (const shape of shapes) {
shape.filterMembershipMask = ShapeMask.Player;
// Standard setup: collides with everything
shape.filterCollideMask = ShapeMask.Wall | ShapeMask.Player | ShapeMask.Ground | ShapeMask.Item;
}
}
How to achieve Per-Pair filtering:
Since you have 32 bits available, you can reserve one (e.g., 1 << 31) as a “SpecificIgnore” bit. To make Body #7 ignore Body #12 at runtime:
Tag Body #12: Add the bit to its membership. shape12.filterMembershipMask |= (1 << 31);
Filter Body #7: Remove that specific bit from its collision mask. shape7.filterCollideMask &= ~(1 << 31);
Now, Body #7 will collide with all other objects on Layer Y, but it will specifically ignore Body #12. Since these properties are reactive in the Babylon Havok plugin, the physics engine will pick up the change in the next step.
I see. This is a nice workaround. I will have some difficulty to scale with it but it tackles the immediate problem for when I have only a phew entities.
I was wondering if I was doing something wrong, since I have to remove and re-add the body after updating its filter mask/group for the change to take effect. Has the filter update worked for you without removing and re-adding the body?
In my experience, the masks should be reactive, but Havok sometimes needs a ‘nudge’ to re-evaluate collisions for bodies that are already settled.
Instead of removing/re-adding the body, try calling body.setMotionType(PhysicsMotionType.DYNAMIC) or simply waking it up with body.applyImpulse(new Vector3(0,0,0), body.transformNode.position) after the mask change. This forces the engine to refresh the broadphase pairs for that body without the overhead of re-creating it in the world.
Recreating a shape is the hard part, which I get around by creating a container and all the shapes in the pool at once. Then, I swap out the shapes during the process to ensure all the forces acting on them are preserved. If you swap out shapes, you risk losing momentum and inertia.
In my game, here are the pieces of code that control the wolf’s shapes. When the wolf is crawling, it’s a lying capsule, when it’s standing, it’s a standing capsule, and when it’s crouching, it’s a short capsule. Each wolf mode has its own shape.
I use applyShapeMasks only once, when I create the wolf and apply physics to it.
/**
* Creates shape pool and container. Uses container with addChild/removeChild to swap shapes on mode change
* without recreating PhysicsBody - preserves velocity and angularVelocity, avoids GC pressure.
*/
private createShapePool(mesh: AbstractMesh): void {
const scene = mesh.getScene()
this.shapeContainer = new PhysicsShapeContainer(scene)
this.standShape = createStandShape(scene)
this.moveShape = createMoveShape(scene)
this.crouchShape = createCrouchShape(scene)
this.layShape = createLayShape(scene)
this.attackShape = createAttackShape(scene)
this.stunShape = createStunShape(scene)
this.applyShapeMasks([this.standShape, this.moveShape, this.crouchShape, this.layShape, this.attackShape, this.stunShape])
}
private applyShapeMasks(shapes: PhysicsShape[]): void {
for (const shape of shapes) {
shape.filterMembershipMask = ShapeMask.Player
shape.filterCollideMask = ShapeMask.Wall | ShapeMask.Player | ShapeMask.Ground | ShapeMask.Item
}
}
private getShapeForMode(mode: string): PhysicsShapeCapsule | undefined {
if (mode === WolfMode.Walk || mode === WolfMode.Run) return this.moveShape
if (mode === WolfMode.Attack) return this.attackShape
if (mode === WolfMode.Stun) return this.stunShape
if (mode === WolfMode.CrouchIdle || mode === WolfMode.CrouchMove) return this.crouchShape
if (mode === WolfMode.LayIdle || mode === WolfMode.LayMove) return this.layShape
return this.standShape
}
/**
* Swaps active shape in container when mode changes. Preserves velocity/angularVelocity.
*/
private updateActiveShape(): void {
const mesh = this.wolf.mesh
const container = this.shapeContainer
if (!mesh || !container || !this.aggregate) return
const mode = this.wolf.mode
if (mode === this.lastPhysicsMode) return
if (mode === WolfMode.Dead) return
const newShape = this.getShapeForMode(mode)
if (!newShape) return
if (container.getNumChildren() > 0) {
container.removeChild(0)
}
container.addChild(newShape)
this.lastPhysicsMode = mode
}
/**
* Wake up the physics body (e.g. after loading is complete).
* Wolf physics is applied with startAsleep; call this when the loading screen is unlocked.
*/
wakeUp(): void {
const body = this.wolf.mesh?.physicsBody
if (!body) return
body.startAsleep = false
const pos = body.getObjectCenterWorld()
body.applyImpulse(new Vector3(1e-10, 0, 0), pos)
}
Shapes in separate file: WolfPhysicsShapes.ts
import { AbstractMesh, PhysicsShapeBox, PhysicsShapeCapsule, Quaternion, Scene, Vector3 } from 'babylonjs'
import WolfMode from '@game/entities/actors/wolf/WolfModeInterface'
/**
* Creates the physics shape for the given wolf mode.
*/
export function createShapeForMode(mesh: AbstractMesh, mode: string): PhysicsShapeCapsule | PhysicsShapeBox {
const scene = mesh.getScene()
if (mode === WolfMode.Walk || mode === WolfMode.Run) {
return createMoveShape(scene)
}
if (mode === WolfMode.Attack) {
return createAttackShape(scene)
}
if (mode === WolfMode.Stun) {
return createStunShape(scene)
}
if (mode === WolfMode.Dead) {
return createDeadShape(scene)
}
if (mode === WolfMode.CrouchIdle || mode === WolfMode.CrouchMove) {
return createCrouchShape(scene)
}
if (mode === WolfMode.LayIdle || mode === WolfMode.LayMove) {
return createLayShape(scene)
}
// Default: StandIdle or StandMove
return createStandShape(scene)
}
export function createStandShape(scene: Scene): PhysicsShapeCapsule {
const radius = 0.6
const pointA = new Vector3(0, 0.6, 0)
const pointB = new Vector3(0, 2.5, 0)
return new PhysicsShapeCapsule(pointA, pointB, radius, scene)
}
export function createLayShape(scene: Scene): PhysicsShapeCapsule {
const radius = 0.5
const heightY = 0.5
const pointA = new Vector3(0, heightY, -0.5)
const pointB = new Vector3(0, heightY, 1.2)
return new PhysicsShapeCapsule(pointA, pointB, radius, scene)
}
export function createCrouchShape(scene: Scene): PhysicsShapeCapsule {
const radius = 1.2
const pointA = new Vector3(0, 1.3, 0)
const pointB = new Vector3(0, 1.6, 0)
return new PhysicsShapeCapsule(pointA, pointB, radius, scene)
}
export function createMoveShape(scene: Scene): PhysicsShapeCapsule {
const radius = 0.65
const pointA = new Vector3(0, 0.6, 0)
const pointB = new Vector3(0, 2.4, 0)
return new PhysicsShapeCapsule(pointA, pointB, radius, scene)
}
export function createAttackShape(scene: Scene): PhysicsShapeCapsule {
const radius = 0.7
const pointA = new Vector3(0, 0.7, 0)
const pointB = new Vector3(0, 2.3, 0)
return new PhysicsShapeCapsule(pointA, pointB, radius, scene)
}
export function createStunShape(scene: Scene): PhysicsShapeCapsule {
const radius = 0.4
const pointA = new Vector3(0, 0.4, -1.5)
const pointB = new Vector3(0, 0, -0.5)
return new PhysicsShapeCapsule(pointA, pointB, radius, scene)
}
function createDeadShape(scene: Scene): PhysicsShapeBox {
const center = new Vector3(-0.2, 0.4, 2.5)
const rotation = Quaternion.Identity()
const extents = new Vector3(0.8, 0.5, 2.5)
return new PhysicsShapeBox(center, rotation, extents, scene)
}
Thank you so much, onekit, for all of your help and detailed explanation
This is amazingly helpful, and I’ll try to figure out why I have to remove/re-add bodies for filter mask/group updates to take effect in my project
@eoin Sorry for the ping, I would be grateful for any insights you could share
Would you know if it’s expected that a body should be removed and re-added to the world for an update to its collision filter mask and group to take effect?
And for the original question by Stargazer, would onekit’s solution be the best one available in Havok?
@regna Actually, about two months ago, I was constantly recreating shapes, manually grabbing the Velocity and reassigning movement directions to each new one.
Then it finally clicked that I could just use a container.
Now I apply all physical impacts to the outer wrapper the container, so I don’t have to recreate the shapes at all.
I just create them all once and swap them inside.
The external system doesn’t even notice the change; it interacts with the container and doesn’t care what’s happening inside. That was a huge insight for me.
P.S.: Glad I could be helpful for once (I still remember you helping me with the skeleton sync many months ago).