Ray casting is often used to find out what objects are in a certain part of the world. A ray is just a straight line, and we can use a function provided by Box2D to check if the line crosses a shape. We can also find out what the normal is at the point the line hits the shape.
In Box2D v3.0, ray casting is done through the world object using these functions:
// Cast a ray and get all intersections
b2World_CastRay(worldId, origin, translation, filter, resultCallback, context);
// Cast a ray and get only the closest intersection
b2World_CastRayClosest(worldId, origin, translation, filter);
The ray is defined by an origin point and a translation vector. The filter parameter lets you control which shapes are tested against.
If there is an intersection, the result contains the intersection point and normal.
Example
To try out ray casting, let's set up a scene with a fenced area and some shapes floating inside in a zero-gravity environment:
// Create world
const gravity = new b2Vec2(0, 0);
const worldDef = b2DefaultWorldDef();
worldDef.gravity = gravity;
const worldId = b2CreateWorld(worldDef);
// Create static body for walls
const bodyDef = b2DefaultBodyDef();
bodyDef.type = STATIC;
bodyDef.position = new b2Vec2(0, 0);
const staticBodyId = b2CreateBody(worldId, bodyDef);
// Create wall shapes
const shapeDef = b2DefaultShapeDef();
shapeDef.density = 0;
// Ground
const groundSegment = new b2Segment(new b2Vec2(-20, 0), new b2Vec2(20, 0));
b2CreateSegmentShape(staticBodyId, shapeDef, groundSegment);
// Ceiling
const ceilingSegment = new b2Segment(new b2Vec2(-20, 40), new b2Vec2(20, 40));
b2CreateSegmentShape(staticBodyId, shapeDef, ceilingSegment);
// Left wall
const leftWallSegment = new b2Segment(new b2Vec2(-20, 0), new b2Vec2(-20, 40));
b2CreateSegmentShape(staticBodyId, shapeDef, leftWallSegment);
// Right wall
const rightWallSegment = new b2Segment(new b2Vec2(20, 0), new b2Vec2(20, 40));
b2CreateSegmentShape(staticBodyId, shapeDef, rightWallSegment);
// Create dynamic bodies
bodyDef.type = DYNAMIC;
bodyDef.position = new b2Vec2(0, 20);
shapeDef.density = 1;
// Create boxes
const boxShape = b2MakeBox(2, 2);
for (let i = 0; i < 5; i++) {
const bodyId = b2CreateBody(worldId, bodyDef);
b2CreatePolygonShape(bodyId, shapeDef, boxShape);
}
// Create circles
const circleShape = { radius: 2 };
for (let i = 0; i < 5; i++) {
const bodyId = b2CreateBody(worldId, bodyDef);
b2CreateCircleShape(bodyId, shapeDef, circleShape);
}
Now let's create a rotating ray to cast through the scene:
let currentRayAngle = 0;
function update() {
// Update ray angle (one revolution every 20 seconds)
currentRayAngle += (Math.PI * 2) / (20 * 60);
// Calculate ray points
const rayLength = 25;
const origin = new b2Vec2(0, 20);
const translation = new b2Vec2(
rayLength * Math.sin(currentRayAngle),
rayLength * Math.cos(currentRayAngle)
);
// Set up query filter
const filter = b2DefaultQueryFilter();
// Cast ray and get closest hit
const result = b2World_CastRayClosest(worldId, origin, translation, filter);
if (result.hit) {
// Calculate intersection point
const hitFraction = result.fraction;
const intersectionPoint = new b2Vec2(
origin.x + hitFraction * translation.x,
origin.y + hitFraction * translation.y
);
// Draw ray up to intersection
drawLine(origin, intersectionPoint);
drawPoint(intersectionPoint);
// Draw normal at intersection point
const normalEnd = new b2Vec2(
intersectionPoint.x + result.normal.x,
intersectionPoint.y + result.normal.y
);
drawLine(intersectionPoint, normalEnd);
} else {
// No hit - draw full ray
const endpoint = new b2Vec2(
origin.x + translation.x,
origin.y + translation.y
);
drawLine(origin, endpoint);
}
}
To create reflected rays, we can use the normals at each intersection to bounce the ray repeatedly in this recursive function:
function drawReflectedRay(origin, translation, depth = 0) {
if (depth > 8) return; // Limit reflection depth
const filter = b2DefaultQueryFilter();
const result = b2World_CastRayClosest(worldId, origin, translation, filter);
if (result.hit) {
// Calculate intersection point
const hitFraction = result.fraction;
const intersectionPoint = new b2Vec2(
origin.x + hitFraction * translation.x,
origin.y + hitFraction * translation.y
);
// Draw ray to intersection
drawLine(origin, intersectionPoint);
if (hitFraction < 1.0) {
// Calculate reflection vector
const dot = 2 * (
translation.x * result.normal.x +
translation.y * result.normal.y
);
const remainingTranslation = new b2Vec2(
translation.x - dot * result.normal.x,
translation.y - dot * result.normal.y
);
// Recurse with reflected ray
drawReflectedRay(
intersectionPoint,
remainingTranslation,
depth + 1
);
}
} else {
// No hit - draw full ray
const endpoint = new b2Vec2(
origin.x + translation.x,
origin.y + translation.y
);
drawLine(origin, endpoint);
}
}
And use it in the update function:
function update() {
currentRayAngle += (Math.PI * 2) / (20 * 60);
const rayLength = 25;
const origin = new b2Vec2(0, 20);
const translation = new b2Vec2(
rayLength * Math.sin(currentRayAngle),
rayLength * Math.cos(currentRayAngle)
);
drawReflectedRay(origin, translation);
}
Credits
This tutorial is adapted from an original piece of work created by Chris Campbell and is used under license.