From 5c409f04470e319f0a57e8791bc96cd724ee601c Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Wed, 17 Feb 2021 21:06:20 -0500 Subject: Proper collisions happening in 2 dimensions --- frontend/_rigidbody/circle.js | 53 +++++++++++- frontend/_rigidbody/program_common.js | 15 ++-- frontend/_rigidbody/rigidbody_2.js | 18 ++-- frontend/_rigidbody/rigidbody_3.js | 158 ++++++++++++++++++++++++++++++++++ frontend/_rigidbody/vec2.js | 14 +++ 5 files changed, 245 insertions(+), 13 deletions(-) create mode 100644 frontend/_rigidbody/rigidbody_3.js (limited to 'frontend/_rigidbody') diff --git a/frontend/_rigidbody/circle.js b/frontend/_rigidbody/circle.js index 194a772..35044e3 100644 --- a/frontend/_rigidbody/circle.js +++ b/frontend/_rigidbody/circle.js @@ -1,6 +1,16 @@ const BYTES_PER_FLOAT = 4; -function circle(pGl, pRadius, pSegments, pColorList, pInitialPosition) { +/** + * Initializes a new circle object for the WebGL context. + * + * @param {WebGLRenderingContext} pGl + * @param {number} pRadius + * @param {number} pSegments + * @param {Array} pColorList + * @param {vec2} pInitialPosition + * @param {number} pMass + */ +function circle(pGl, pRadius, pSegments, pColorList, pInitialPosition, pMass) { const lBuffer = pGl.createBuffer(); pGl.bindBuffer(pGl.ARRAY_BUFFER, lBuffer); @@ -30,11 +40,12 @@ function circle(pGl, pRadius, pSegments, pColorList, pInitialPosition) { return { buffer: lBuffer, vertexCount: vertexCount, + prevPos: vec2(), position: pInitialPosition || vec2(), velocity: vec2(), force: vec2(), torque: 0, - mass: 1, + mass: pMass || 1, rotationVelocity: 0, rotationRadians: 0, model: mat4(), @@ -43,6 +54,7 @@ function circle(pGl, pRadius, pSegments, pColorList, pInitialPosition) { } function renderCircle(pGl, pProgramInfo, pCircle) { + pGl.uniformMatrix4fv(pProgramInfo.uniformLocations.model, false, pCircle.model); pGl.bindBuffer(pGl.ARRAY_BUFFER, pCircle.buffer); { pGl.enableVertexAttribArray(pProgramInfo.attributeLocations.position); @@ -57,4 +69,41 @@ function renderCircle(pGl, pProgramInfo, pCircle) { function getMomentOfInertia(pCircle) { return (Math.PI * Math.pow(pCircle.radius, 4)) / 4; +} + +function doCirclesIntersect(pFirst, pSecond) { + const lDistanceBetween = Math.pow(pFirst.position.x - pSecond.position.x, 2) + + Math.pow(pFirst.position.y - pSecond.position.y, 2) + return lDistanceBetween <= Math.pow(pFirst.radius + pSecond.radius, 2); +} + +/** + * Returns intersection information about the intersecting circles. + * + * Warning! Only use this if doCirclesIntersect returned true for these circles. + * + * @param {circle} pFirst + * @param {circle} pSecond + */ +function getIntersectionDataForCircles(pFirst, pSecond) { + // The collision normal is simply the difference between the two current positions + const lCollisionNormal = normalize2(subVec2(pFirst.position, pSecond.position)); + + // TODO: Might be useful in the future? + const lCollisionPoint = vec2( + ((pFirst.position.x * pSecond.radius) + (pSecond.position.x * pFirst.radius)) / (pFirst.radius + pSecond.radius), + ((pFirst.position.y * pSecond.radius) + (pSecond.position.y * pFirst.radius)) / (pFirst.radius + pSecond.radius) + ); + + const lSecondDeepestPointInFirst = (subVec2(pSecond.position, scaleVec2(lCollisionNormal, pSecond.radius))); + const lFirstDeepestPointInSecond = (addVec2(pFirst.position, scaleVec2(lCollisionNormal, pFirst.radius))); + const lLengthOfOverlapByTwo = length2(subVec2(lFirstDeepestPointInSecond, lSecondDeepestPointInFirst)) / 2.0; + const lMedianIntersectingPoint = addVec2(lSecondDeepestPointInFirst, scaleVec2(lCollisionNormal, lLengthOfOverlapByTwo)); + + return { + relativeVelocity: subVec2(pFirst.velocity, pSecond.velocity), + collisionNormal: lCollisionNormal, + firstPointOfApplication: subVec2(lMedianIntersectingPoint, pFirst.position), + secondPointOfApplication: subVec2(lMedianIntersectingPoint, pSecond.position) + } } \ No newline at end of file diff --git a/frontend/_rigidbody/program_common.js b/frontend/_rigidbody/program_common.js index 9d097e2..988056f 100644 --- a/frontend/_rigidbody/program_common.js +++ b/frontend/_rigidbody/program_common.js @@ -36,7 +36,7 @@ function getContext(pId, pOnRun) { function requestUpdateLoop(pFunction, pOnExit) { let lDeltaTimeSeconds = undefined, - lStartTimeSeconds = undefined, + lLastTimeSeconds = undefined, lIsRunning = true; function update(pTimeStamp) { @@ -50,15 +50,18 @@ function requestUpdateLoop(pFunction, pOnExit) { pTimeStamp = pTimeStamp / 1000.0; // Convert to seconds // Time calculation - if (lStartTimeSeconds === undefined) { - lStartTimeSeconds = pTimeStamp; + if (lLastTimeSeconds === undefined) { + lLastTimeSeconds = pTimeStamp; lDeltaTimeSeconds = 0; } else { - lDeltaTimeSeconds = lStartTimeSeconds - pTimeStamp; - lStartTimeSeconds = pTimeStamp; + lDeltaTimeSeconds = pTimeStamp - lLastTimeSeconds; + lLastTimeSeconds = pTimeStamp; } - pFunction(lDeltaTimeSeconds); + while (lDeltaTimeSeconds > 0) { + pFunction(lDeltaTimeSeconds); + lDeltaTimeSeconds -= 0.16; + } requestAnimationFrame(update); } diff --git a/frontend/_rigidbody/rigidbody_2.js b/frontend/_rigidbody/rigidbody_2.js index c41461e..1c6b79f 100644 --- a/frontend/_rigidbody/rigidbody_2.js +++ b/frontend/_rigidbody/rigidbody_2.js @@ -31,22 +31,29 @@ function main() { ], vec2(lProgramContext.width / 2.0, lProgramContext.height / 2.0)); function update(pDeltaTimeSeconds) { - // Same physics updates from previously + // Same physics updates from part 1 applyForce(vec2(0, -1.0 * (lCircle.mass * GRAVITY))); const lCurrentAcceleration = scaleVec2(lCircle.force, 1.0 / lCircle.mass); lCircle.velocity = addVec2(lCircle.velocity, scaleVec2(lCurrentAcceleration, pDeltaTimeSeconds)); lCircle.position = addVec2(lCircle.position, scaleVec2(lCircle.velocity, pDeltaTimeSeconds)); lCircle.force = vec2(); - // Calculate rotation + // Angular code starts here + + // Retrieve the moment of inertia for our shape (Ours is a circle by default) const lMomentOfInertia = getMomentOfInertia(lCircle); + + // Calculate the angular acceperation (omega = T / I) const lAngularAcceleration = lCircle.torque / lMomentOfInertia; + // Calculate the rotation in radians lCircle.rotationVelocity += lAngularAcceleration * pDeltaTimeSeconds; lCircle.rotationRadians += lCircle.rotationVelocity * pDeltaTimeSeconds; - lCircle.model = rotateMatrix2d(translateMatrix(mat4(), lCircle.position.x, lCircle.position.y, 0), lCircle.rotationRadians); lCircle.torque = 0; + // Calculate the model as previously, but this time, also rotate it + lCircle.model = rotateMatrix2d(translateMatrix(mat4(), lCircle.position.x, lCircle.position.y, 0), lCircle.rotationRadians); + // Render Code only lProgramContext.gl.clearColor(0.0, 0.0, 0.0, 1.0); lProgramContext.gl.clearDepth(1.0); @@ -65,9 +72,10 @@ function main() { function applyForce(pForceVector, pPointOfApplication) { if (pPointOfApplication !== undefined) { - const lOriginToPointOfApp = subVec2(vec2(), pPointOfApplication), // The point of application is relative to the model (i.e. the center of the circle, not the scene) - lPerpVec = vec2(-lOriginToPointOfApp.y, lOriginToPointOfApp.x); + const lOriginToPointOfApp = subVec2(vec2(), pPointOfApplication), // The point of application is relative to the model (i.e. the center of the circle, not the scene) + lPerpVec = vec2(-lOriginToPointOfApp.y, lOriginToPointOfApp.x); // Retrieve the perpendicular vector + // Calculate the torque from the perp dot (T = r_perp . F) lCircle.torque += TORQUE_MULTIPLIER * dot2(lPerpVec, pForceVector); } diff --git a/frontend/_rigidbody/rigidbody_3.js b/frontend/_rigidbody/rigidbody_3.js new file mode 100644 index 0000000..0316268 --- /dev/null +++ b/frontend/_rigidbody/rigidbody_3.js @@ -0,0 +1,158 @@ +/// +/// +/// +/// +/// + +function main() { + // Define Constants + const CIRCLE_RADIUS = 16; + const GRAVITY = 9.8; + const COF_OF_RESTITUITION = 0.7; + + // Retrieve context + const lProgramContext = getContext('#rigidbody_3'); + + if (lProgramContext.gl === null) { + console.error('Unable to initialize WebGL. Your browser or machine may not support it.'); + return; + } + + lProgramContext.gl.clearColor(0.0, 0.0, 0.0, 1.0); + lProgramContext.gl.clear(lProgramContext.gl.COLOR_BUFFER_BIT); + + function run() { + console.log('Running Rigid Body 3'); + lProgramContext.load().then(function(pProgramInfo) { + // Circile initialization + const horizontalCircle = circle(lProgramContext.gl, CIRCLE_RADIUS, 30, [ + { x: 1, y: 1, z: 0, w: 1 }, + { x: 1, y: 0, z: 1, w: 1 }, + { x: 0, y: 1, z: 1, w: 1 }, + { x: 0, y: 1, z: 0, w: 1 } + ], vec2(400, lProgramContext.height / 2.0)); + + const verticalCircle = circle(lProgramContext.gl, CIRCLE_RADIUS, 30, [ + { x: 1, y: 0, z: 0, w: 1 }, + { x: 0, y: 1, z: 0, w: 1 }, + { x: 0, y: 0, z: 1, w: 1 } + ], vec2(lProgramContext.width / 2.0, lProgramContext.height / 2.0 + 100)); + + horizontalCircle.velocity = vec2(-100, 0); + verticalCircle.velocity = vec2(0, -100); + + lTimeStepScale = $('#time_step_slider').val(); + + /** + * Run the update method of a single circle + * + * @param {circle} pCircle + * @param {number} pDeltaTimeSeconds + */ + function updateCircle(pCircle, pDeltaTimeSeconds) { + // Same physics updates from part 1 + applyForce(pCircle, vec2(0, -1.0 * (pCircle.mass * GRAVITY))); + const lCurrentAcceleration = scaleVec2(pCircle.force, 1.0 / pCircle.mass); + pCircle.velocity = addVec2(pCircle.velocity, scaleVec2(lCurrentAcceleration, pDeltaTimeSeconds)); + pCircle.prevPos = { ...pCircle.position }; + pCircle.position = addVec2(pCircle.position, scaleVec2(pCircle.velocity, pDeltaTimeSeconds)); + pCircle.force = vec2(); + + // Same physics updates from part 2 + const lMomentOfInertia = getMomentOfInertia(pCircle); + const lAngularAcceleration = pCircle.torque / lMomentOfInertia; + pCircle.rotationVelocity += lAngularAcceleration * pDeltaTimeSeconds; + pCircle.rotationRadians += pCircle.rotationVelocity * pDeltaTimeSeconds; + pCircle.torque = 0; + + pCircle.model = rotateMatrix2d(translateMatrix(mat4(), pCircle.position.x, pCircle.position.y, 0), pCircle.rotationRadians); + } + + function update(pDeltaTimeSeconds) { + pDeltaTimeSeconds = pDeltaTimeSeconds * lTimeStepScale; + updateCircle(horizontalCircle, pDeltaTimeSeconds); + updateCircle(verticalCircle, pDeltaTimeSeconds); + collision(); + render(); + } + + function collision() { + if (!doCirclesIntersect(horizontalCircle, verticalCircle)) { + return false; + } + + const lIntersectionResult = getIntersectionDataForCircles(horizontalCircle, verticalCircle); + + console.log('We have a collision'); + const lRelativeVelocity = lIntersectionResult.relativeVelocity, + lCollisionNormal = lIntersectionResult.collisionNormal, + lFirstPerp = getPerp2(lIntersectionResult.firstPointOfApplication), + lSecondPerp = getPerp2(lIntersectionResult.secondPointOfApplication); + + console.log(lIntersectionResult); + + const lNumerator = dot2(scaleVec2(lRelativeVelocity, -(1.0 + COF_OF_RESTITUITION)), lCollisionNormal); + const lLinearDenomPart = dot2(lCollisionNormal, (scaleVec2(lCollisionNormal, 1 / horizontalCircle.mass + 1 / verticalCircle.mass))); + const lRotationalDenomPart = (Math.pow(dot2(lFirstPerp, lCollisionNormal), 2) / getMomentOfInertia(horizontalCircle)) + + (Math.pow(dot2(lSecondPerp, lCollisionNormal), 2) / getMomentOfInertia(verticalCircle)) + + const lImpulseMagnitude = lNumerator / (lLinearDenomPart + lRotationalDenomPart); + + horizontalCircle.velocity = addVec2(horizontalCircle.velocity, scaleVec2(lCollisionNormal, lImpulseMagnitude / horizontalCircle.mass)); + verticalCircle.velocity = subVec2(verticalCircle.velocity, scaleVec2(lCollisionNormal, lImpulseMagnitude / verticalCircle.mass)); + + horizontalCircle.rotationVelocity = horizontalCircle.rotationVelocity + dot2(lFirstPerp, scaleVec2(lCollisionNormal, lImpulseMagnitude)) / getMomentOfInertia(horizontalCircle); + verticalCircle.rotationVelocity = verticalCircle.rotationVelocity - dot2(lSecondPerp, scaleVec2(lCollisionNormal, lImpulseMagnitude)) / getMomentOfInertia(verticalCircle); + + return true; + } + + function render() { + lProgramContext.gl.clearColor(0.0, 0.0, 0.0, 1.0); + lProgramContext.gl.clearDepth(1.0); + lProgramContext.gl.enable(lProgramContext.gl.DEPTH_TEST); + lProgramContext.gl.depthFunc(lProgramContext.gl.LEQUAL); + lProgramContext.gl.clear(lProgramContext.gl.COLOR_BUFFER_BIT | lProgramContext.gl.DEPTH_BUFFER_BIT); + lProgramContext.gl.useProgram(pProgramInfo.program); + lProgramContext.gl.uniformMatrix4fv(pProgramInfo.uniformLocations.projection, false, lProgramContext.perspective); + + renderCircle(lProgramContext.gl, pProgramInfo, horizontalCircle); + renderCircle(lProgramContext.gl, pProgramInfo, verticalCircle); + } + + const TORQUE_MULTIPLIER = 100.0; // TODO: This may be unncessary + function applyForce(pCircle, pForceVector, pPointOfApplication) { + if (pPointOfApplication !== undefined) { + const lOriginToPointOfApp = subVec2(vec2(), pPointOfApplication), + lPerpVec = vec2(-lOriginToPointOfApp.y, lOriginToPointOfApp.x); + + pCircle.torque += TORQUE_MULTIPLIER * dot2(lPerpVec, pForceVector); + } + + pCircle.force = addVec2(pCircle.force, pForceVector); + } + + function cleanup() { + lProgramContext.gl.deleteBuffer(horizontalCircle.buffer); + lProgramContext.gl.deleteBuffer(verticalCircle.buffer); + lProgramContext.gl.deleteProgram(pProgramInfo.program); + lProgramContext.gl.clearColor(0.0, 0.0, 0.0, 1.0); + lProgramContext.gl.clear(lProgramContext.gl.COLOR_BUFFER_BIT); + } + + function reset() { + lExitRequestFunc(); + lProgramContext.reset(); + $('#time_step_slider').unbind('change'); + } + + const lExitRequestFunc = requestUpdateLoop(update, cleanup); + lProgramContext.stopButton.on('click', reset); + $('#time_step_slider').on('change', function() { lTimeStepScale = $(this).val(); }); + }); + } + + lProgramContext.playButton.on('click', run); +} + +$(document).ready(main); \ No newline at end of file diff --git a/frontend/_rigidbody/vec2.js b/frontend/_rigidbody/vec2.js index 3b24371..2db42e0 100644 --- a/frontend/_rigidbody/vec2.js +++ b/frontend/_rigidbody/vec2.js @@ -38,4 +38,18 @@ function normalize2(v) { function vec2str(v) { return `(${v.x.toFixed(2)}, ${v.y.toFixed(2)})`; +} + +function getPerp2(v) { + return { + x: -v.y, + y: v.x + }; +} + +function negate2(v) { + return { + x: -v.x, + y: -v.y + }; } \ No newline at end of file -- cgit v1.2.1