feat: xpbd 구현
This commit is contained in:
parent
bf44ea8ae7
commit
0d3b84cc28
4 changed files with 67 additions and 18 deletions
|
@ -86,6 +86,24 @@ export function drawWorld(ctx: CanvasRenderingContext2D,
|
|||
const b1 = world.balls[constraint.p1Idx];
|
||||
const b2 = world.balls[constraint.p2Idx];
|
||||
drawConstraint(ctx, b1, b2);
|
||||
// Draw force vectors for debugging
|
||||
const scale = 100; // Scale for visibility
|
||||
if (constraint.p1ConstraintForce) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(b1.pos[0], b1.pos[1]);
|
||||
ctx.lineTo(b1.pos[0] + constraint.p1ConstraintForce[0] * scale, b1.pos[1] + constraint.p1ConstraintForce[1] * scale);
|
||||
ctx.strokeStyle = "green";
|
||||
ctx.lineWidth = 3;
|
||||
ctx.stroke();
|
||||
}
|
||||
if (constraint.p2ConstraintForce) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(b2.pos[0], b2.pos[1]);
|
||||
ctx.lineTo(b2.pos[0] + constraint.p2ConstraintForce[0] * scale, b2.pos[1] + constraint.p2ConstraintForce[1] * scale);
|
||||
ctx.strokeStyle = "green";
|
||||
ctx.lineWidth = 3;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < world.balls.length; i++) {
|
||||
const ball = world.balls[i];
|
||||
|
|
|
@ -124,6 +124,11 @@ export default function Pendulum() {
|
|||
// reset handler to re-create pendulums and avoid large dt on next frame
|
||||
const reset = () => {
|
||||
worldRef.current = getDefaultMyWorld();
|
||||
if (worldRef.current) {
|
||||
for (const constraint of worldRef.current.constraints) {
|
||||
constraint.lagrangeMultiplier = 0;
|
||||
}
|
||||
}
|
||||
prevTimeRef.current = performance.now();
|
||||
};
|
||||
|
||||
|
|
|
@ -13,7 +13,11 @@ export type ConstraintState = {
|
|||
p1Idx: number;
|
||||
p2Idx: number;
|
||||
restLength: number;
|
||||
stiffness: number;
|
||||
compliance: number;
|
||||
lagrangeMultiplier?: number;
|
||||
|
||||
p1ConstraintForce?: [number,number]; // For debugging or visualization
|
||||
p2ConstraintForce?: [number,number]; // For debugging or visualization
|
||||
};
|
||||
|
||||
export type PhysicalWorldState = {
|
||||
|
@ -21,11 +25,12 @@ export type PhysicalWorldState = {
|
|||
constraints: ConstraintState[];
|
||||
};
|
||||
|
||||
export const SCENE_GRAVITY: Point = [0, 9.81 * 100];
|
||||
export const SCENE_GRAVITY: Point = [0, 9.81 * 10];
|
||||
|
||||
export function updateUnconstrainedBall(ball: BallState, dt: number = 0.016) {
|
||||
if (ball.isFixed) return;
|
||||
const acc: Point = [SCENE_GRAVITY[0], SCENE_GRAVITY[1]];
|
||||
// A more exact derivation uses the Taylor series (to second order)
|
||||
const nextPos: Point = [
|
||||
ball.pos[0] + (ball.pos[0] - ball.prevPos[0]) * (dt / ball.prevDt) + acc[0] * (dt + ball.prevDt) / 2 * dt,
|
||||
ball.pos[1] + (ball.pos[1] - ball.prevPos[1]) * (dt / ball.prevDt) + acc[1] * (dt + ball.prevDt) / 2 * dt,
|
||||
|
@ -35,26 +40,47 @@ export function updateUnconstrainedBall(ball: BallState, dt: number = 0.016) {
|
|||
ball.prevDt = dt;
|
||||
}
|
||||
|
||||
export function satisfyConstraint(ball1: BallState, ball2: BallState, constraint: ConstraintState) {
|
||||
// XPBD (Extended Position Based Dynamics) constraint solver
|
||||
export function satisfyConstraint(ball1: BallState, ball2: BallState,
|
||||
constraint: ConstraintState,
|
||||
dt: number = 0.016
|
||||
) {
|
||||
const dx = ball2.pos[0] - ball1.pos[0];
|
||||
const dy = ball2.pos[1] - ball1.pos[1];
|
||||
const length = Math.sqrt(dx * dx + dy * dy);
|
||||
if (length == 0) return;
|
||||
const diff = length - constraint.restLength;
|
||||
if (length < 1e-9) return;
|
||||
const C = (constraint.restLength - length);
|
||||
const [nx, ny] = [dx / length, dy / length];
|
||||
const invMass1 = 1 / ball1.mass;
|
||||
const invMass2 = 1 / ball2.mass;
|
||||
const sumInvMass = invMass1 + invMass2;
|
||||
if (sumInvMass == 0) return;
|
||||
const p1Prop = invMass1 / sumInvMass;
|
||||
const p2Prop = invMass2 / sumInvMass;
|
||||
const correction = diff * constraint.stiffness / length;
|
||||
ball1.pos[0] += dx * correction * p1Prop;
|
||||
ball1.pos[1] += dy * correction * p1Prop;
|
||||
ball2.pos[0] -= dx * correction * p2Prop;
|
||||
ball2.pos[1] -= dy * correction * p2Prop;
|
||||
|
||||
if (sumInvMass < 1e-9) return; // Both balls are fixed
|
||||
|
||||
const compliance = constraint.compliance ?? 0;
|
||||
const alpha = compliance / (dt * dt);
|
||||
|
||||
constraint.lagrangeMultiplier = (constraint.lagrangeMultiplier ?? 0);
|
||||
|
||||
const deltaLambda = (-C - alpha * constraint.lagrangeMultiplier) / (sumInvMass + alpha);
|
||||
constraint.lagrangeMultiplier += deltaLambda;
|
||||
|
||||
const correction = deltaLambda;
|
||||
|
||||
ball1.pos[0] += nx * correction * invMass1;
|
||||
ball1.pos[1] += ny * correction * invMass1;
|
||||
ball2.pos[0] -= nx * correction * invMass2;
|
||||
ball2.pos[1] -= ny * correction * invMass2;
|
||||
|
||||
constraint.p1ConstraintForce = [nx * correction * invMass1 / dt, ny * correction * invMass1 / dt];
|
||||
constraint.p2ConstraintForce = [-nx * correction * invMass2 / dt, -ny * correction * invMass2 / dt];
|
||||
}
|
||||
|
||||
export function updateWorld(world: PhysicalWorldState, dt: number = 0.016, constraintIterations: number = 50) {
|
||||
for (const constraint of world.constraints) {
|
||||
constraint.lagrangeMultiplier = 0;
|
||||
}
|
||||
|
||||
for (const ball of world.balls) {
|
||||
updateUnconstrainedBall(ball, dt);
|
||||
}
|
||||
|
@ -62,7 +88,7 @@ export function updateWorld(world: PhysicalWorldState, dt: number = 0.016, const
|
|||
for (const constraint of world.constraints) {
|
||||
const b1 = world.balls[constraint.p1Idx];
|
||||
const b2 = world.balls[constraint.p2Idx];
|
||||
satisfyConstraint(b1, b2, constraint);
|
||||
satisfyConstraint(b1, b2, constraint, dt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,10 +13,10 @@ export function getDefaultMyWorld(): WorldState {
|
|||
{ pos: [400, 0], prevPos: [400, 0], mass: 1, prevDt: 0.016 },
|
||||
];
|
||||
const constraints: ConstraintState[] = [
|
||||
{ p1Idx: 0, p2Idx: 1, restLength: 100, stiffness: 1 },
|
||||
{ p1Idx: 1, p2Idx: 2, restLength: 50, stiffness: 1 },
|
||||
{ p1Idx: 0, p2Idx: 3, restLength: 200, stiffness: 1 },
|
||||
{ p1Idx: 3, p2Idx: 4, restLength: 50, stiffness: 1 },
|
||||
{ p1Idx: 0, p2Idx: 1, restLength: 100, compliance: 0 },
|
||||
{ p1Idx: 1, p2Idx: 2, restLength: 50, compliance: 0.01 },
|
||||
{ p1Idx: 0, p2Idx: 3, restLength: 200, compliance: 0 },
|
||||
{ p1Idx: 3, p2Idx: 4, restLength: 50, compliance: 0 },
|
||||
];
|
||||
const ballDrawers = [
|
||||
new BallDrawer({ radius: 10 }),
|
||||
|
|
Loading…
Add table
Reference in a new issue