/// /// /// /// /// /// (function() { // Define Constants const GRAVITY = 50.0; const COF_OF_RESTITUITION = 0.9; var lProgramContext = undefined; function main() { lProgramContext = getContext('#rigidbody_3b'); 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); lProgramContext.playButton.on('click', run); } function run() { console.log('Running Rigid Body 3b'); lProgramContext.load().then(function(pProgramInfo) { const lNumHorizontalPegs = lProgramContext.width / 80.0, lNumVerticalPegs = lProgramContext.height / 80.0, lBall = circle(lProgramContext.gl, 16.0, 16, [ { 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(38 * 8, lProgramContext.height - 24.0), 10.0), lPegList = []; lBall.velocity = vec2(0, -50.0); // Generate a peg. These pegs will NOT be updated, so that they // dont' fall due to gravity. for (let lRowIdx = 0; lRowIdx < lNumVerticalPegs - 1; lRowIdx++) { for (let lColIdx = 0; lColIdx <= lNumHorizontalPegs; lColIdx++) { lPegList.push(circle(lProgramContext.gl, 16.0, 16, [ { x: 165.0 / 255.0, y: 42.0 / 255.0, z: 42.0 / 255.0, w: 1.0 }], vec2((lRowIdx % 2 ? 40 : 0) + (lColIdx) * 80.0, lRowIdx * 80.0), 10000 // Real big so it's almost negligble )); } } 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.prevVelocity = pCircle.velocity; 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 = getCircleMomentOfInertia(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) { updateCircle(lBall, pDeltaTimeSeconds); collision(pDeltaTimeSeconds); render(); } function collision(pDeltaTimeSeconds) { for (let lPegIdx = 0; lPegIdx < lPegList.length; lPegIdx++) { const lPeg = lPegList[lPegIdx]; if (!doCirclesIntersect(lPeg, lBall)) { continue; } // We have an intersection! Let's try and figure out precisely when that happened. let lSubdividedDeltaTime = pDeltaTimeSeconds; // Create a ball and move it back to the balls previous position let lSubdividedBall = undefined; do { // Move the subdivided ball back to the previous position, and then // advance its position by increasingly smaller timestep. This could be pretty // slow in some circumstances, and it most definitely does not prevent tunneling, // but - for now - this is not so bad. lSubdividedBall = JSON.parse(JSON.stringify(lBall)); lSubdividedBall.position = {...lBall.prevPos}; lSubdividedBall.velocity = {...lBall.prevVelocity}; lSubdividedDeltaTime = lSubdividedDeltaTime / 2.0; updateCircle(lSubdividedBall, lSubdividedDeltaTime); if (lSubdividedDeltaTime === 0) { console.error('This should NOT be happening'); break; } } while (doCirclesIntersect(lPeg, lSubdividedBall)) // The ball is no longer intersecting at the time presented here. That means this is // (nearly) the precise point of intersection. const lIntersectionResult = getIntersectionDataForCircles(lPeg, lSubdividedBall); const lRelativeVelocity = lIntersectionResult.relativeVelocity, lCollisionNormal = lIntersectionResult.collisionNormal, lFirstPerp = getPerp2(lIntersectionResult.firstPointOfApplication), lSecondPerp = getPerp2(lIntersectionResult.secondPointOfApplication); const lNumerator = dot2(scaleVec2(lRelativeVelocity, -(1.0 + COF_OF_RESTITUITION)), lCollisionNormal); const lLinearDenomPart = dot2(lCollisionNormal, (scaleVec2(lCollisionNormal, 1 / lBall.mass))); const lRotationalDenomPart = (Math.pow(dot2(lFirstPerp, lCollisionNormal), 2) / getCircleMomentOfInertia(lPeg)) + (Math.pow(dot2(lSecondPerp, lCollisionNormal), 2) / getCircleMomentOfInertia(lBall)) const lImpulseMagnitude = lNumerator / (lLinearDenomPart + lRotationalDenomPart); lBall.position = lSubdividedBall.position; // Move the ball back to its proper subdivided position lBall.velocity = subVec2(lBall.velocity, scaleVec2(lCollisionNormal, lImpulseMagnitude / lBall.mass)); lBall.rotationVelocity = lBall.rotationVelocity - dot2(lSecondPerp, scaleVec2(lCollisionNormal, lImpulseMagnitude)) / getCircleMomentOfInertia(lBall); // Now we update in our new direction with the remaining time that we have left. updateCircle(lBall, pDeltaTimeSeconds - lSubdividedDeltaTime); break; } } 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); lPegList.forEach(function(pCircle) { renderCircle(lProgramContext.gl, pProgramInfo, pCircle); }); renderCircle(lProgramContext.gl, pProgramInfo, lBall); } 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() { lPegList.forEach(function(pCircle) { freeCircle(lProgramContext.gl, pCircle); }); freeCircle(lProgramContext.gl, lBall); 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(); }); }); } $(document).ready(main); })()