[翻译] 物理引擎javascript实现
转自:
高中物理全还给老师了啊啊啊啊啊啊
牛顿第二定律
物体加速度的大小跟物体受到的作用力成正比,跟物体的质量成反比,加速度的方向跟合外力的方向相同。 而以物理学的观点来看,牛顿运动第二定律亦可以表述为“物体随时间变化之动量变化率和所受外力之和成正比”,即动量对时间的一阶导数等 于外力之和。牛顿第二定律说明了在宏观低速下,a∝F/m,F∝ma,用数学表达式可以写成F=kma,其中的k为比例系数,是一个常数。但由于当时没有 规定多大的力作为力的单位,比例系数k的选取就有一定的任意性,如果取k=1,就有F=ma,这就是今天我们熟知的牛顿第二定律的数学表达式。
The process for simulating an object’s motion goes something like this:
- Figure out what the forces are on an object
- Add those forces up to get a single “resultant” or “net” force 更新F
- Use F = ma to calculate the object’s acceleration due to those forces 计算加速度a
- Use the object’s acceleration to calculate the object’s velocity 计算速度v
- Use the object’s velocity to calculate the object’s position 计算位置
- Since the forces on the object may change from moment to moment, repeat this process from #1, forever 迭代
last_acceleration = acceleration position += velocity * time_step + ( 0.5 * last_acceleration * time_step^2 ) new_acceleration = force / mass avg_acceleration = ( last_acceleration + new_acceleration ) / 2 velocity += avg_acceleration * time_step
通常涉及到的简单的力:
- weight force 重力
- spring force 弹簧
- viscous damping force 阻尼
- air drag 空气阻力 $F_D=\frac{1}{2}\rho v^2C_DA$
示例:一维, 重力+空气阻力+阻尼
/* The following is not free software. You may use it for educational purposes, but you may not redistribute or use it commercially. (C) Burak Kanber 2012 */ var canvas = document.getElementById('canvas'), ctx = canvas.getContext('2d'), height = 400, width = 400, x = 200, y = 0, vy = 0, ay = 0, m = 0.1, // Ball mass in kg r = 10, // Ball radius in cm, or pixels. dt = 0.02, // Time step. e = -0.5, // Coefficient of restitution ("bounciness") rho = 1.2, // Density of air. Try 1000 for water. C_d = 0.47, // Coeffecient of drag for a ball A = Math.PI * r * r / 10000 // Frontal area of the ball; divided by 10000 to compensate for the 1px = 1cm relation ; ctx.fillStyle = 'red'; function loop() { var fy = 0; /* Weight force, which only affects the y-direction (because that's the direction gravity points). */ fy += m * 9.81; //重力 /* Air resistance force; this would affect both x- and y-directions, but we're only looking at the y-axis in this example. */ fy += -1 * 0.5 * rho * C_d * A * vy * vy; //空气阻力 /* Verlet integration for the y-direction */ dy = vy * dt + (0.5 * ay * dt * dt); //计算位置 /* The following line is because the math assumes meters but we're assuming 1 cm per pixel, so we need to scale the results */ y += dy * 100; new_ay = fy / m; avg_ay = 0.5 * (new_ay + ay); vy += avg_ay * dt; /* Let's do very simple collision detection */ if (y + r > height && vy > 0) { /* This is a simplification of impulse-momentum collision response. e should be a negative number, which will change the velocity's direction. */ vy *= e; //碰撞,反向,速度减小 /* Move the ball back a little bit so it's not still "stuck" in the wall. */ y = height - r; //避免重复判断 } draw(); } function draw() { ctx.clearRect(0, 0, width, height); ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2, true); ctx.fill(); ctx.closePath(); } /* A real project should use requestAnimationFrame, and you should time the frame rate and pass a variable "dt" to your physics function. This is just a simple brute force method of getting it done. */ setInterval(loop, dt * 1000);
对于全三维的情况,需要6个维度来描述(x,y,z维度的位移和旋转):
translation in x, y, and z (called “sway”, “heave” and “surge”), and rotation about x, y, and z (called “pitch”, “yaw”, and “roll”).
旋转+弹簧
position, velocity, and acceleration change : 位置、速度、加速度
rotation angle, angular velocity, and angular acceleration : 角度、角速度、角加速度
看不下去了啊
/* The following is not free software. You may use it for educational purposes, but you may not redistribute or use it commercially. (C) Burak Kanber 2012 */ var canvas = document.getElementById('canvas'), ctx = canvas.getContext('2d'), height = 400, width = 400, stiffness = 0.5, b = -1, angularB = -7, dt = 0.02; /* A more generic vector class could take an arbitrary number of elements, rather than just x, y, or z. This vector class is strictly 2D */ var V = function(x, y) { this.x = x; this.y = y; }; V.prototype.add = function(v) { return new V(v.x + this.x, v.y + this.y); }; V.prototype.subtract = function(v) { return new V(this.x - v.x, this.y - v.y); }; V.prototype.scale = function(s) { return new V(this.x * s, this.y * s); }; V.prototype.dot = function(v) { return (this.x * v.x + this.y * v.y); }; /* Normally the vector cross product function returns a vector. But since we know that all our vectors will only be 2D (x and y only), any cross product we calculate will only have a z-component. Since we don't have a 3D vector class, let's just return the z-component as a scalar. We know that the x and y components will be zero. This is absolutely not the case for 3D. */ V.prototype.cross = function(v) { return (this.x * v.y - this.y * v.x); }; V.prototype.rotate = function(angle, vector) { var x = this.x - vector.x; var y = this.y - vector.y; var x_prime = vector.x + ((x * Math.cos(angle)) - (y * Math.sin(angle))); var y_prime = vector.y + ((x * Math.sin(angle)) + (y * Math.cos(angle))); return new V(x_prime, y_prime); }; /* Our simple rectangle class is defined by the coordinates of the top-left corner, the width, height, and mass. */ var Rect = function(x, y, w, h, m) { if (typeof(m) === 'undefined') { this.m = 1; } this.width = w; this.height = h; this.topLeft = new V(x, y); this.topRight = new V(x + w, y); this.bottomRight = new V(x + w, y + h); this.bottomLeft = new V(x, y + h); this.v = new V(0, 0); this.a = new V(0, 0); this.theta = 0; this.omega = 0; this.alpha = 0; this.J = this.m * (this.height*this.height + this.width*this.width) / 12000; }; /* The Rect class only defines the four corners of the rectangle, but sometimes we need the center point so we can calulate that by finding the diagonal and cutting it in half. */ Rect.prototype.center = function() { var diagonal = this.bottomRight.subtract(this.topLeft); var midpoint = this.topLeft.add(diagonal.scale(0.5)); return midpoint; }; /* To rotate a rectangle we'll update both its angle and rotate all four of the corners */ Rect.prototype.rotate = function(angle) { this.theta += angle; var center = this.center(); this.topLeft = this.topLeft.rotate(angle, center); this.topRight = this.topRight.rotate(angle, center); this.bottomRight = this.bottomRight.rotate(angle, center); this.bottomLeft = this.bottomLeft.rotate(angle, center); return this; }; /* Simply translate all four corners */ Rect.prototype.move = function(v) { this.topLeft = this.topLeft.add(v); this.topRight = this.topRight.add(v); this.bottomRight = this.bottomRight.add(v); this.bottomLeft = this.bottomLeft.add(v); return this; } var rect = new Rect(200, 0, 100, 50); rect.v = new V(0, 2); var spring = new V(200, 0); ctx.strokeStyle = 'black'; var loop = function() { var f = new V(0, 0); var torque = 0; /* Start Velocity Verlet by performing the translation */ var dr = rect.v.scale(dt).add(rect.a.scale(0.5 * dt * dt)); rect.move(dr.scale(100)); /* Add Gravity */ f = f.add(new V(0, rect.m * 9.81)); /* Add damping */ f = f.add( rect.v.scale(b) ); /* Add Spring; we calculate this separately so we can calculate a torque. */ var springForce = rect.topLeft.subtract(spring).scale(-1 * stiffness); /* This vector is the distance from the end of the spring to the box's center point */ var r = rect.center().subtract(rect.topLeft); /* The cross product informs us of the box's tendency to rotate. */ var rxf = r.cross(springForce); torque += -1 * rxf; f = f.add(springForce); /* Finish Velocity Verlet */ var new_a = f.scale(rect.m); var dv = rect.a.add(new_a).scale(0.5 * dt); rect.v = rect.v.add(dv); /* Do rotation; let's just use Euler for contrast */ torque += rect.omega * angularB; // Angular damping rect.alpha = torque / rect.J; rect.omega += rect.alpha * dt; var deltaTheta = rect.omega * dt; rect.rotate(deltaTheta); draw(); }; var draw = function() { ctx.strokeStyle = 'black'; ctx.clearRect(0, 0, width, height); ctx.save(); ctx.translate(rect.topLeft.x, rect.topLeft.y); ctx.rotate(rect.theta); ctx.strokeRect(0, 0, rect.width, rect.height); ctx.restore(); ctx.strokeStyle = '#cccccc'; ctx.beginPath(); ctx.moveTo(spring.x,spring.y); ctx.lineTo(rect.topLeft.x, rect.topLeft.y); ctx.stroke(); ctx.closePath(); }; setInterval(loop, dt*1000);
碰撞检测
/* The following is not free software. You may use it for educational purposes, but you may not redistribute or use it commercially. (C) Burak Kanber 2012 */ var canvas, ctx, height = 400, width = 400, stiffness = 0.5, b = -1, angularB = -1, dt = 0.02; Array.prototype.max = function() { return Math.max.apply(null, this); }; Array.prototype.min = function() { return Math.min.apply(null, this); }; var V = function(x, y) { this.x = x; this.y = y; }; V.prototype.length = function() { return Math.sqrt(this.x*this.x + this.y*this.y); }; V.prototype.add = function(v) { return new V(v.x + this.x, v.y + this.y); }; V.prototype.subtract = function(v) { return new V(this.x - v.x, this.y - v.y); }; V.prototype.scale = function(s) { return new V(this.x * s, this.y * s); }; V.prototype.dot = function(v) { return (this.x * v.x + this.y * v.y); }; V.prototype.cross = function(v) { return (this.x * v.y - this.y * v.x); }; V.prototype.toString = function() { return '[' + this.x + ',' + this.y + ']'; }; V.prototype.rotate = function(angle, vector) { var x = this.x - vector.x; var y = this.y - vector.y; var x_prime = vector.x + ((x * Math.cos(angle)) - (y * Math.sin(angle))); var y_prime = vector.y + ((x * Math.sin(angle)) + (y * Math.cos(angle))); return new V(x_prime, y_prime); }; var Rect = function(x, y, w, h, m) { if (typeof(m) === 'undefined') { this.m = 1; } this.width = w; this.height = h; this.active = true; this.topLeft = new V(x, y); this.topRight = new V(x + w, y); this.bottomRight = new V(x + w, y + h); this.bottomLeft = new V(x, y + h); this.v = new V(0, 0); this.a = new V(0, 0); this.theta = 0; this.omega = 0; this.alpha = 0; this.J = this.m * (this.height * this.height + this.width * this.width) / 12000; }; Rect.prototype.center = function() { var diagonal = this.bottomRight.subtract(this.topLeft); var midpoint = this.topLeft.add(diagonal.scale(0.5)); return midpoint; }; Rect.prototype.rotate = function(angle) { this.theta += angle; var center = this.center(); this.topLeft = this.topLeft.rotate(angle, center); this.topRight = this.topRight.rotate(angle, center); this.bottomRight = this.bottomRight.rotate(angle, center); this.bottomLeft = this.bottomLeft.rotate(angle, center); return this; }; Rect.prototype.move = function(v) { this.topLeft = this.topLeft.add(v); this.topRight = this.topRight.add(v); this.bottomRight = this.bottomRight.add(v); this.bottomLeft = this.bottomLeft.add(v); return this; }; Rect.prototype.draw = function(ctx) { ctx.strokeStyle = 'black'; ctx.save(); ctx.translate(this.topLeft.x, this.topLeft.y); ctx.rotate(this.theta); ctx.strokeRect(0, 0, this.width, this.height); ctx.restore(); }; Rect.prototype.vertex = function(id) { if (id == 0) { return this.topLeft; } else if (id == 1) { return this.topRight; } else if (id == 2) { return this.bottomRight; } else if (id == 3) { return this.bottomLeft; } }; function intersect_safe(a, b) { var result = new Array(); var as = a.map( function(x) { return x.toString(); }); var bs = b.map( function(x) { return x.toString(); }); for (var i in as) { if (bs.indexOf(as[i]) !== -1) { result.push( a[i] ); } } return result; } satTest = function(a, b) { var testVectors = [ a.topRight.subtract(a.topLeft), a.bottomRight.subtract(a.topRight), b.topRight.subtract(b.topLeft), b.bottomRight.subtract(b.topRight), ]; var ainvolvedVertices = []; var binvolvedVertices = []; /* * Look at each test vector (shadows) */ for (var i = 0; i < 4; i++) { ainvolvedVertices[i] = []; // Our container for involved vertces binvolvedVertices[i] = []; // Our container for involved vertces var myProjections = []; var foreignProjections = []; for (var j = 0; j < 4; j++) { myProjections.push(testVectors[i].dot(a.vertex(j))); foreignProjections.push(testVectors[i].dot(b.vertex(j))); } // Loop through foreignProjections, and test if each point is x lt my.min AND x gt m.max // If it's in the range, add this vertex to a list for (var j in foreignProjections) { if (foreignProjections[j] > myProjections.min() && foreignProjections[j] < myProjections.max()) { binvolvedVertices[i].push(b.vertex(j)); } } // Loop through myProjections and test if each point is x gt foreign.min and x lt foreign.max // If it's in the range, add the vertex to the list for (var j in myProjections) { if (myProjections[j] > foreignProjections.min() && myProjections[j] < foreignProjections.max()) { ainvolvedVertices[i].push(a.vertex(j)); } } } // console.log( intersect_safe ( intersect_safe( involvedVertices[0], involvedVertices[1] ), intersect_safe( involvedVertices[2], involvedVertices[3] ) ) ); ainvolvedVertices = intersect_safe(intersect_safe(ainvolvedVertices[0], ainvolvedVertices[1]), intersect_safe(ainvolvedVertices[2], ainvolvedVertices[3])); binvolvedVertices = intersect_safe(intersect_safe(binvolvedVertices[0], binvolvedVertices[1]), intersect_safe(binvolvedVertices[2], binvolvedVertices[3])); /* If we have two vertices from one rect and one vertex from the other, probably the single vertex is penetrating the segment return involvedVertices; */ if (ainvolvedVertices.length === 1 && binvolvedVertices.length === 2) { return ainvolvedVertices[0]; } else if (binvolvedVertices.length === 1 && ainvolvedVertices.length === 2) { return binvolvedVertices[0]; } else if (ainvolvedVertices.length === 1 && binvolvedVertices.length === 1) { return ainvolvedVertices[0]; } else if (ainvolvedVertices.length === 1 && binvolvedVertices.length === 0) { return ainvolvedVertices[0]; } else if (ainvolvedVertices.length === 0 && binvolvedVertices.length === 1) { return binvolvedVertices[0]; } else if (ainvolvedVertices.length === 0 && binvolvedVertices.length === 0) { return false; } else { console.log("Unknown collision profile"); console.log(ainvolvedVertices); console.log(binvolvedVertices); clearInterval(timer); } return true; } var rect = new Rect(200, 0, 100, 50); var wall = new Rect(125, 200, 100, 50); rect.omega = -10; var loop = function() { var f = new V(0, 0); var torque = 0; /* Start Velocity Verlet by performing the translation */ var dr = rect.v.scale(dt).add(rect.a.scale(0.5 * dt * dt)); rect.move(dr.scale(100)); /* Add Gravity */ f = f.add(new V(0, rect.m * 9.81)); /* Add damping */ f = f.add(rect.v.scale(b)); /* Handle collision */ var collision = satTest(rect, wall); if (collision) { var N = rect.center().subtract(collision); //.rotate(Math.PI , new V(0,0)); N = N.scale( 1 / N.length()); var Vr = rect.v; var I = N.scale( -1 * (1 + 0.3) * Vr.dot(N) ); rect.v = I rect.omega = -1 * 0.2 * (rect.omega / Math.abs(rect.omega)) * rect.center().subtract(collision).cross(Vr); } /* Finish Velocity Verlet */ var new_a = f.scale(rect.m); var dv = rect.a.add(new_a).scale(0.5 * dt); rect.v = rect.v.add(dv); /* Do rotation; let's just use Euler for contrast */ torque += rect.omega * angularB; // Angular damping rect.alpha = torque / rect.J; rect.omega += rect.alpha * dt; var deltaTheta = rect.omega * dt; rect.rotate(deltaTheta); draw(); }; var draw = function() { ctx.clearRect(0, 0, width, height); rect.draw(ctx); wall.draw(ctx); }; var timer; canvas = document.getElementById('canvas'), ctx = canvas.getContext('2d'), ctx.strokeStyle = 'black'; timer = setInterval(loop, dt * 1000);