92 lines
2.7 KiB
TypeScript
92 lines
2.7 KiB
TypeScript
import { vec2 } from 'gl-matrix';
|
|
import { PhysicsBody, Sdf } from './sdf';
|
|
import { evaluateSdf } from './evaluate-sdf';
|
|
import { sdfNormal } from './sdf-normal';
|
|
|
|
export interface MarchResult {
|
|
hitSurface: boolean;
|
|
normal?: vec2;
|
|
hitObject?: Sdf;
|
|
}
|
|
|
|
const minimumStep = 0.5;
|
|
const maximumSteps = 256;
|
|
|
|
// Raymarch a circle by `delta`, stopping at the first surface it would overlap.
|
|
// Shared so server and client resolve motion identically. The collision
|
|
// *reaction* is not dispatched here: on a real (non-ignored) hit,
|
|
// `onHit(intersecting)` is invoked at the point the backend used to dispatch its
|
|
// ReactToCollisionCommands, and the backend wrapper supplies that callback. The
|
|
// client passes none.
|
|
//
|
|
// The march advances by the *free gap* (distance minus the circle's radius),
|
|
// which is what sphere-tracing a circle of radius r requires.
|
|
export const marchCircle = (
|
|
body: PhysicsBody,
|
|
delta: vec2,
|
|
possibleIntersectors: Array<Sdf>,
|
|
ignoreCollision = false,
|
|
onHit?: (intersecting: Sdf) => void,
|
|
): MarchResult => {
|
|
const deltaLength = vec2.length(delta);
|
|
|
|
// A zero-length move cannot resolve a contact by advancing, so report no hit
|
|
// rather than letting a caller that loops on `hitSurface` spin forever.
|
|
if (!(deltaLength > 0)) {
|
|
return { hitSurface: false };
|
|
}
|
|
|
|
const direction = vec2.normalize(vec2.create(), delta);
|
|
|
|
const rayEnd = vec2.create();
|
|
let travelled = 0;
|
|
// Furthest point along the ray known to be overlap-free; a hit rewinds here.
|
|
let lastFreeDistance = 0;
|
|
|
|
for (let step = 0; step < maximumSteps; step++) {
|
|
vec2.scaleAndAdd(rayEnd, body.center, direction, travelled);
|
|
|
|
const minDistance = evaluateSdf(rayEnd, possibleIntersectors);
|
|
|
|
if (minDistance < body.radius) {
|
|
const intersecting = possibleIntersectors.find(
|
|
(i) => i.distance(rayEnd) <= body.radius,
|
|
)!;
|
|
|
|
if (ignoreCollision) {
|
|
// Pass straight through, but still report the contact so callers that
|
|
// march repeatedly to escape geometry keep making progress.
|
|
vec2.scaleAndAdd(body.center, body.center, direction, deltaLength);
|
|
return { hitSurface: true, hitObject: intersecting };
|
|
}
|
|
|
|
onHit?.(intersecting);
|
|
|
|
vec2.scaleAndAdd(rayEnd, body.center, direction, lastFreeDistance);
|
|
vec2.copy(body.center, rayEnd);
|
|
|
|
return {
|
|
hitSurface: true,
|
|
normal: sdfNormal(rayEnd, [intersecting]),
|
|
hitObject: intersecting,
|
|
};
|
|
}
|
|
|
|
lastFreeDistance = travelled;
|
|
|
|
if (travelled >= deltaLength) {
|
|
break;
|
|
}
|
|
|
|
travelled = Math.min(
|
|
travelled + Math.max(minDistance - body.radius, minimumStep),
|
|
deltaLength,
|
|
);
|
|
}
|
|
|
|
vec2.scaleAndAdd(body.center, body.center, direction, deltaLength);
|
|
|
|
return {
|
|
hitSurface: false,
|
|
};
|
|
};
|