四、基本物理概念
前两章涵盖了大量关于 JavaScript 编码和数学工具的背景材料。这一章将提供一个基本物理学的概述,以建立你开始基于物理学的编程所需要的最后一组关键概念。在此过程中,我们将使用 JavaScript 示例来说明其中的一些概念,并将构建将在整本书中使用的有用的物理对象。
本章涵盖的主题包括以下内容:
- 普通物理学概念和符号:物理学处理可测量的量。这个简短的部分回顾了一些关于物理量及其单位的性质的基本事实,并解释了使用科学符号来表示物理量的值。
- 事物——物理学中的粒子和其他物体:物理学中的事物使用理想化的概念建模,例如粒子。我们构建一个 JavaScript
Particle
对象来实现适当的粒子属性和方法,并扩展它来创建一个可见的Ball
对象。 - 描述运动——运动学:本节解释描述和分析运动所需的概念和方法。它包含任何准物理程序员必须知道的一些基本定义和公式。
- 预测运动——力和动力学:要预测运动,你需要了解运动的原因:力。动力学是对力和运动的研究。通过指定作用在物体上的力,你可以计算出它的运动。
- 能量概念:能量是物理学中一个强有力的概念,它使我们能够以简单的方式解决一些棘手的问题。
普通物理概念和符号
这一小段回顾了一些一般概念,并建立了一些适用于所有物理学的基本术语和符号。
物理量和单位
在物理学中,当你谈论长度或质量或能量时,你希望尽可能精确。理想情况下,你希望能够测量或计算你所谈论的事情。例如,物体的大小不仅仅是一种质量;这是一个数量。物理量是可测量的属性。它们可以被赋予一个数值或数量级。
物理量还必须有另外一个东西:单位。比如我们不只是说某个特定表的长度是 2;我们说(在世界上大多数地方)是 2 米。米(m)是单位。为什么我们需要一个单位?一个单位做两件事。首先,它告诉我们正在谈论什么样的事情。米告诉我们,我们正在处理一个长度,而不是温度或其他东西。第二,单位建立了一个参照物,我们用它来比较量的大小。当我们说一张桌子的长度是 2 米时,我们实际上是说它的长度是我们称之为米的参照物的两倍。所有的测量值只有在我们与之比较的参考值下才有意义。
如你所想,有不同的单位选择。例如,要测量长度或距离,可以使用米或英尺。科学文献中通常的长度单位是米(m)及其细分数和倍数,如厘米(cm)和千米(km)。关键是使用一致的单位,而不是混合不同类型的单位。否则,我们可以使用任何合适的单位制。例如,在计算机图形和动画工作中,我们通常以像素为单位来测量距离。
在前一章中,你遇到了标量和向量。简单回顾一下,向量是一个有大小和方向的数学对象,而标量只有大小。物理量可以是标量或矢量。前一章提到的矢量的一个例子是位移。在这一章的后面你会遇到更多的例子。
科学符号
在物理学中,你经常会遇到非常大和非常小的数字。例如,光速大约为 300 000 000 m/s。我们通常将这写成 3×108m/s。108(“10 的 8 次方”)是 1 后跟 8 个 0,等于 1 亿。这就是所谓的科学记数法。想法是用这种形式写大或小的数,其中 A 大于等于 1 但小于 10,B 是正整数或负整数:
如示例所示,如果 B 为正,10 B 为 1,后跟 B 个零。如果 B 是负整数呢?在这种情况下,我们将 1 放在小数位后的第 B 第位,在它之前有零:例如,10 -4 与 0.0001 相同。
举个例子,一个电子(围绕原子中心的微小粒子)的质量大约是 9.1 × 10 -31 kg。
在 JavaScript 中,你把这个写成 9.1e-31,光速写成 3e8。这里的“e”一定不能和上一章介绍的数字 e 混淆,在 JavaScript 中是Math.E
。这里,e 用来表示 10 的幂,我们用它乘以它前面的数。
事物:物理学中的粒子和其他物体
为了描述和模拟真实世界,物理学必须有一些物理事物的表示。物理学的理论是现实的概念和数学模型。它们由现实生活中存在的事物的理想化概念组成。一个这样的理想化是粒子的概念。虽然在这一节我们将专门关注粒子,但为了将它们放在上下文中,这里列出了您将在本书中遇到的各种“事物”:
- 粒子:从这一节开始,本书的大部分内容将涉及粒子。我们很快会谈到更多的粒子,但简单地说,它们可以被认为是存在于空间离散点的不可分割的单元。因此,它们不同于可以扩展或由多个部分组成的更复杂的对象。粒子以一种简单的方式运动:它们基本上可以改变它们的位置。这也被称为翻译。
- 刚体:刚体是一种扩展的物体,其大小和形状不容易改变,如桌子或汽车。刚体可以像粒子一样通过平移运动,但除此之外,它们还可以进行旋转。刚体运动将在第十三章中介绍。
- 可变形物体:像刚体一样,可变形物体可以平移和旋转,但除此之外,它们的形状和/或大小也可以改变。例子包括一个橡皮球、一个布娃娃或一块布。建模可变形物体的方法将在第十三章中讨论。
- 流体:流体不同于之前描述的物体,因为它们没有明确的大小或形状,但具有从空间的一部分流向另一部分的能力。因为流体的任何部分都可以移动,所以精确模拟流体运动要困难得多。但有时也有可能通过用粒子模拟流体来“作弊”。这是我们在第十二章中用来创造一些有趣的流体视觉效果的方法。
- 字段:字段是更抽象的实体。广义上讲,场是在空间上连续存在的某种物理量。例如,我们将在第十章中探讨力场的概念。
什么是粒子?
因为我们要用粒子做很多事情,所以让我们多讨论一下。什么是粒子?可能首先想到的是微小的实体,如电子。在物理学中,这些被称为基本粒子。那不一定是我们在这里谈论的。
我们使用“粒子”这个词的意义在于,它是对任何物理对象的数学理想化,这些物理对象的内部结构或成分对我们所考虑的任何问题都不重要。这可能包括原子,或台球,甚至是星系中的恒星!从这个意义上来说,粒子基本上只是一组表征物体个体的属性。
粒子属性
让我们更准确地解释一下上一节中最后一句话的意思。我们在谈论什么属性?粒子具有以下特性:
- 位置:粒子一定在某个地方!这意味着它必须具有坐标 x 和 y(以及 3D 中的 z)。
- 速度:粒子运动,所以有速度。我们将在下一节正式定义速度;现在只需要注意它是一个有分量的向量,就像位置一样。
- 质量:作为一个物理事物,粒子也一定有质量。
- 电荷:一些基本粒子,如电子,也有一种称为电荷的属性,这使它们经历一种有趣的力,称为电磁力。因为我们将在本书中讨论这个力,我们也想给我们的粒子一个电荷属性。
- 其他(自旋,寿命,等等):我们可以包括其他属性,从物理学的基本粒子中获得灵感。但是现在,我们将坚持使用这里列出的属性。
这个背景给了我们足够的素材来开始构建一些 JavaScript 对象来表示粒子和它们的行为,尽管在我们这样做时引入的一些粒子属性和运动概念将在本章的后续章节中更深入地讨论。
首先,我们将创建一个实现刚才描述的粒子属性的Particle
对象。然后,我们将创建一个扩展了Particle
的Ball
对象,绘制图形以便能够看到Ball
实例,同时表现为粒子。最后,我们将展示如何使粒子运动。
构建粒子对象
我们需要创建基于物理属性的对象属性,如质量、电荷、位置和速度,如前一节所述。我们应该能够读取和修改这些属性。表 4-1 显示了我们想要创建的属性。
表 4-1。
Properties of the Particle object
| 财产 | 类型 | 可能的价值和意义 | | --- | --- | --- | | `mass` | `Number` | 任何正值(默认值为 1) | | `charge` | `Number` | 任何值:正数、负数或零(默认值) | | `x` | `Number` | 任何值;位置的 x 分量 | | `y` | `Number` | 任何值;位置的 y 分量 | | `vx` | `Number` | 任意值(默认为 0);速度的 x 分量 | | `vy` | `Number` | 任意值(默认为 0);速度的 y 分量 | | `pos2D` | `Vector2D` | 任何`Vector2D`值;2D 位置向量 | | `velo2D` | `Vector2D` | 任何`Vector2D`值;2D 速度矢量 |这些属性的选择和它们可能的值需要一些解释。很明显mass
不可能是负数或者零,因为每个粒子都有质量。正如你将在第十章中看到的,charge
可以是正的、负的或零。电荷为零的粒子会表现得好像根本没有电荷一样。
在前一章赞美了向量的优点后,我们不使用它们就是伪君子了。因此,我们将位置和速度向量pos2D
和velo2D
创建为来自它们各自组件的Vector2D
对象。
这些属性的实现过程如下。首先,我们在其构造函数中定义了Particle mass
、charge
、x
、y
、vx
和vy
属性:
function Particle(mass,charge){
if(typeof(mass)==='undefined') mass = 1;
if(typeof(charge)==='undefined') charge = 0;
this.mass = mass;
this.charge = charge;
this.x = 0;
this.y = 0;
this.vx = 0;
this.vy = 0;
}
我们希望能够在创建粒子时设置它们的质量和电荷,因此使用构造函数并将其作为参数输入构造函数是有意义的。请注意,Particle
实例的质量和电荷值分别默认为 1 和 0,而位置和速度分量最初都被赋值为 0。
通过 getters 和 setters 将pos2D
和velo2D
属性添加到Particle
原型中:
Particle.prototype = {
get pos2D (){
return new Vector2D(this.x,this.y);
},
set pos2D (pos){
this.x = pos.x;
this.y = pos.y;
},
get velo2D (){
return new Vector2D(this.vx,this.vy);
},
set velo2D (velo){
this.vx = velo.x;
this.vy = velo.y;
}
};
(请注意,如果您使用 Internet Explorer,尤其是 IE8 或更早版本,使用这些 getter/setter 可能会出错。)通过这些访问器,您可以简单地通过键入particle.pos2D
来获取或设置名为particle
的Particle
实例的pos2D
属性,对于velo2D
属性也是如此。自然,您可以使用组件x
和y
的pos2D
向量读取或分配Particle
实例的位置坐标,对于velo2D
也是如此。这就是Particle
对象的全部内容。
文件particle-example.js
包含了Particle
对象的用法示例。您需要启动 JavaScript 控制台来查看代码的输出。
为了简化更新粒子的pos2D
或velo2D
值的代码,我们向Vector2D
对象添加了两个方法multiply(k)
和addScaled(vec, k)
(其中vec
是一个Vector2D
,k
是一个Number
)。vec1.multiply(k)
方法将向量vec1
乘以标量k
,vec1.addScaled(vec, k)
将k
乘以vec
加到vec1
。
例如,要更新名为particle
的Particle
实例的位置,请编写以下代码:
particle.pos2D = particle.pos2D.add(particle.velo2D.multiply(dt));
或者这样做,它包含一个方法调用addScaled()
,而不是两个方法调用multiply()
和add()
:
particle.pos2D = particle.pos2D.addScaled(particle.velo2D, dt);
这些相当于组件形式:
particle.x += particle.vx * dt;
particle.y += particle.vy * dt;
扩展粒子对象
我们在前面部分所做的是很棒的东西,但是你还没有看到任何粒子。那是因为我们的粒子目前是不可见的。对象被有意地保持在最低的复杂程度。现在是时候扩展它,加入一些我们能看到的东西了。为此,我们将通过对我们在前面章节中使用的旧版本做一些修改来重新发明Ball
对象。
球对象
与其从前面章节中已有的Ball
对象开始,不如从Particle
对象开始,并添加一些额外的属性和方法,将它变成可见的东西。首先我们添加一个radius
和一个color
属性,类似于第三章中的Ball
对象。然后我们引入一个额外的属性gradient
,一个Boolean
,它指定Ball
对象是否要用渐变来绘制。最后,我们将draw()
方法添加到Ball
的原型中,增加了绘制带有或不带渐变的球的选项。Ball
对象的完整代码如下所示:
function Ball(radius,color,mass,charge,gradient){
if(typeof(radius)==='undefined') radius = 20;
if(typeof(color)==='undefined') color = '#0000ff';
if(typeof(mass)==='undefined') mass = 1;
if(typeof(charge)==='undefined') charge = 0;
if(typeof(gradient)==='undefined') gradient = false;
this.radius = radius;
this.color = color;
this.mass = mass;
this.charge = charge;
this.gradient = gradient;
this.x = 0;
this.y = 0;
this.vx = 0;
this.vy = 0;
}
Ball.prototype = {
get pos2D (){
return new Vector2D(this.x,this.y);
},
set pos2D (pos){
this.x = pos.x;
this.y = pos.y;
},
get velo2D (){
return new Vector2D(this.vx,this.vy);
},
set velo2D (velo){
this.vx = velo.x;
this.vy = velo.y;
},
draw: function (context) {
if (this.gradient){
grad = context.createRadialGradient(this.x,this.y,0,this.x,this.y,this.radius);
grad.addColorStop(0,'#ffffff');
grad.addColorStop(1,this.color);
context.fillStyle = grad;
}else{
context.fillStyle = this.color;
}
context.beginPath();
context.arc(this.x, this.y, this.radius, 0, 2*Math.PI, true);
context.closePath();
context.fill();
}
};
可以看到,构造函数有五个可选参数:radius
、color
、mass
、charge
和gradient
,默认值分别为 20、“#0000ff”、1、0 和false
。Boolean gradient
选项决定球是用渐变填充还是简单填充绘制。和以前一样,实际的绘制发生在draw()
方法中。
这里,我们通过简单地复制Particle
的属性和方法,从头开始创建了Ball
对象。一个更聪明的方法是使用Object.create()
方法模拟经典继承,用Particle
原型创建Ball
对象:
Ball.prototype = Object.create(Particle.prototype);
Ball.prototype.constructor = Ball;
然后可以用附加的draw()
方法来扩充Ball
对象,因此:
Ball.prototype.draw = function (context) {
//
code as before
}
我们演示了在文件ball2.js
中创建Ball
对象的方法。要使用它,只需在相关的 HTML 文件中包含ball2.js
文件,而不是ball.js
。记得也包括particle.js
文件。在以后的例子中使用ball.js
还是ball2.js
纯粹是个人喜好问题,不会以任何方式影响结果。
使用球对象
使用Ball
对象非常简单。这段代码片段创建一个ball
对象,初始化它的位置,并绘制它:
var ball = new Ball(20,'#ff0000',1,0,true);
ball.pos2D = new Vector2D(150,50);
ball.draw(context);
这段代码生成一堆大小和位置随机的球,并将它们放入一个数组中,以便在后面的代码中引用它们(见图 4-1 ):
var balls = new Array();
var numBalls = 10;
for (var i=1; i<=numBalls; i++){
var ball;
var radius = (Math.random()+0.5)*20;
var color = '#0000ff';
ball = new Ball(radius,color,1,0,true);
ball.pos2D = new Vector2D(Math.random()*canvas.width,Math.random()*canvas.height);
ball.draw(context);
balls.push(ball);
}
图 4-1。
A random bunch of Ball
objects
这些例子的代码在ball-test.js
中。为了运行这段代码,ball-test.html
文件使用了ball.js
文件,而ball-particle-inheritance-test.html
使用了ball2.js
(参见上一节)。
移动粒子
图 4-1 中描述的Ball
实例看起来很酷,但是它们只是坐在那里,没有做太多事情。我们需要让他们动起来。原则上,让Particle
(和Ball
)实例移动并不困难:我们只需要建立一个动画循环,更新粒子的位置,并在每个时间步重画它。为此,我们将使用基于requestAnimationFrame()
方法的动画循环,以及Date.getTime()
方法来精确计算时间间隔。我们建议你看一下第二章、getTime-example.js
的最后一个例子,刷新一下你对基本逻辑和代码结构的记忆。ball-move.js
中的示例代码建立在getTime-example.js
(第二章)中的代码之上,在此完整展示:
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var ball;
var t;
var t0;
var dt;
var animId;
var animTime = 5; // duration of animation
window.onload = init;
function init() {
ball = new Ball(20,'#ff0000',1,0,true);
ball.pos2D = new Vector2D(150,50);
ball.velo2D=new Vector2D(30,20);
ball.draw(context);
t0 = new Date().getTime();
t = 0;
animFrame();
};
function animFrame(){
animId = requestAnimationFrame(animFrame,canvas);
onTimer();
}
function onTimer(){
var t1 = new Date().getTime();
dt = 0.001*(t1-t0);
t0 = t1;
t += dt;
if (t < animTime){
move();
}else{
stop();
}
}
function move(){
ball.pos2D = ball.pos2D.addScaled(ball.velo2D,dt);
context.clearRect(0, 0, canvas.width, canvas.height);
ball.draw(context);
}
function stop(){
cancelAnimationFrame(animId);
}
大部分代码处理设置计时,如第二章中的所述,因此应该很熟悉。init()
函数创建一个Ball
实例,初始化它的位置和速度,并在画布上绘制它。然后它初始化时间变量并调用animFrame()
函数,该函数设置动画循环。在每个时间步长触发的onTimer()
函数计算从第一次调用dt
起经过的时间间隔(以秒为单位),然后更新从模拟开始的持续时间t
(以秒为单位)。后者是通过将所有经过的时间间隔dt
相加到当前时间间隔来计算的。onTimer()
函数还包含一个if
循环,如果模拟的总运行时间小于指定的持续时间animTime
,则调用move()
函数,否则调用stop()
函数,从而终止动画循环。move()
函数根据球的速度向量更新球的位置向量,擦除画布上的所有内容,并重新绘制球对象。在网络浏览器中打开文件ball-move.html
,您将看到一个球在画布上匀速运动。
这段代码中计时的基本设置将在以后的例子中反复使用。下一个例子balls-move.js
,扩展代码使几个球同时移动。唯一的实质性变化发生在init()
和move()
方法中:
function init() {
balls = new Array();
for (var i=0; i<numBalls; i++){
var radius = (Math.random()+0.5)*20;
var ball = new Ball(radius,'#0000ff',1,0,true);
ball.pos2D = new Vector2D(canvas.width/2,canvas.height/2);
ball.velo2D = new Vector2D((Math.random()-0.5)*20,(Math.random()-0.5)*20);
ball.draw(context);
balls.push(ball);
}
t0 = new Date().getTime();
t = 0;
animFrame();
};
在init()
中,现在创建了许多球,并初始化了它们的位置和速度。然后将它们放入一个名为balls
的数组中,就像上一节中的例子一样。这一次,这个数组实际上是用在了move()
函数中:
function move(){
context.clearRect(0, 0, canvas.width, canvas.height);
for (var i=0; i<numBalls; i++){
var ball = balls[i];
ball.pos2D = ball.pos2D.addScaled(ball.velo2D,dt);
ball.draw(context);
}
}
正如你所看到的,我们遍历了所有的balls
元素来更新每个球的位置,并在画布元素上重新绘制它。通过在浏览器中打开文件balls-move.html
运行代码,您将看到不同大小的球从相同的初始位置移出。
在所有这些例子中,球以恒定的速度运动。真正有趣的情况是当速度随时间变化时。我们将在本章中很快介绍这一点。但首先我们需要介绍一些运动概念。
描述运动:运动学
在这一节中,我们将开始用精确的概念、方程和图形来更正式地描述运动。对运动的描述叫做运动学。在运动学中,我们不关心是什么引起运动,而只关心如何描述它。下一节,“预测运动:力和动力学”,将着眼于运动的原因:力。
概念:位移,速度,速度,加速度
到目前为止,我们已经相当松散地讨论了速度和加速度之类的东西。现在是时候准确地说出这些量的含义了。
以下是运动学中需要定义的主要概念:
- 排水量
- 速度(以及相关的速度概念)
- 加速
这些物理量依赖于更基本的概念——位置、距离和时间——这些概念不言而喻,不需要定义。对于矢量来说,还有第四个基本概念:角度,它定义了空间中的一个方向。
排水量
位移是一个与物体运动相关的矢量。更准确地说,它是连接物体初始位置和最终位置的向量。还记得第三章的 Bug 的例子吗?Bug 从原点沿 y 轴移动 10 个单位的距离,这就是位移。从那里开始,它再向右移动 10 个单位(在 x 轴的正方向)。
在这个例子中,Bug 的净位移或合成位移是,或者在 45 度角上大约 14.1 个单位。但是它移动的净距离当然是 20 个单位。这个例子说明了位移和距离的区别。一般来说,一个物体可能会沿着任何复杂的轨迹移动:移动的距离就是沿着轨迹的长度,而位移总是连接物体初始位置和最终位置的向量(见图 4-2 )。
图 4-2。
Displacement vector (arrow) contrasted with distance along trajectory
位移通常用符号 s 或 x 表示,其大小用 s 或 x 表示。位移(和距离)的常用单位是米及其倍数(例如 km)和分数(cm、mm 等)。在画布上,你在电脑屏幕上看到一个舞台,而不是真实的空间,你会用像素来考虑距离和位移,我们通常缩写为 px。
速度
速度被定义为位移随时间的变化率。它告诉你某物移动的速度,以及它移动的方向。因此,速度是一个矢量。
速度的通常符号是 u 或 v;对于它的大小,u 或 v。
还记得微积分里变化率是什么意思吗?这是梯度函数,或者导数。因此,速度是位移对时间的导数。在微积分符号中,我们把它写成
同样,随着时间间隔δt(以及因此位移变化δs)变小,该导数是比率δs/δt 的极限。因此,对于有限的间隔,我们可以写
这给出了时间间隔δt 内的平均速度,而 v = d s/dt 给出了给定时间的瞬时速度。在速度恒定的特殊情况下,任意时刻的平均速度等于瞬时速度。
如前所述,物理学中通常的速度单位是米每秒(m/s),尽管在日常生活中通常使用公里/小时和英里/小时。在 canvas 上下文中,时间的度量仍然是秒,但是距离的度量是像素。所以,你通常会认为速度是以像素每秒(px/s)来表示的。
在计算机代码中,我们总是在处理离散区间,所以我们实际上是在处理平均速度。但是如果我们使用足够小的时间间隔,平均值就接近瞬时值。可以重新排列前面的等式,用 v 和δt 表示δs:
这个方程告诉我们,位移增量是速度和时间间隔的乘积。这正是我们在ball-move.js
示例中的move()
方法中所做的:
ball.pos2D = ball.pos2D.addScaled(ball.velo2D,dt);
你也可能认为这等同于使用正向方案进行时间积分(参见第三章)。这很有道理——因为速度是位移对时间的导数,位移是速度对时间的积分。我们将使用这个积分方案,具体地说就是欧拉方案,直到我们读到本书的第四部分。
速度
速度是速度的标量形式;它没有方向。速度被定义为距离随时间的变化率,换句话说,就是单位时间内移动的距离。速度的单位和速率的单位是一样的。
物体的平均速度是移动的总距离除以花费的总时间:
在这本书里,大部分时间我们会讨论速度和速度分量,而不是速度。
加速
加速度被定义为速度随时间的变化率。它告诉你物体的速度变化有多快。如果你以 20 米/秒(约 45 英里/小时)的匀速直线行驶,你没有加速——加速度为零。但如果你踩下油门,在 10 秒内将汽车速度从 20 米/秒提高到 30 米/秒,那么平均加速度就是(30–20)/10 米/秒,或者 1 米/秒 2 。这意味着速度每秒增加 1 米/秒。加速度也可以是负的;在这种情况下,速度值减小。
加速度的微积分定义是
离散版本是
这些方程类似于速度方程,只是用速度代替了位移。与相应的速度方程一样,a =δv/δt 给出时间间隔δt 内的平均加速度,而 a = d v/dt 给出给定时间的瞬时加速度。
Note
在加速度恒定的特殊情况下,平均加速度等于瞬时加速度。我们将在下一节说明这一事实。
颠倒前面的等式得出:
解释为一个离散方程,这给了我们一个积分方案,更新速度给定的加速度。这是您使用Vector2D
在 JavaScript 中编写代码的方式:
particle.velo2D = particle.velo2D.add(acc.multiply(dt));
或者
particle.velo2D = particle.velo2D.addScaled(acc, dt);
这里acc
是代表加速度的变量。它是一个Vector2D
对象。假设我们使用一个固定值:
var acc:Vector2D = new Vector2D(0,10);
这会让我们的粒子以 10 px/s 2 的速度向下加速,模拟重力的作用。
因为加速度是速度的变化率,如果运动方向改变,即使速度不变,加速度也不为零。一个常见的例子是物体匀速圆周运动。因为物体的运动方向改变了,它的速度也就改变了。因此,它的加速度不为零。我们将在第九章中详细讨论这种情况。
组合向量
因为位移、速度和加速度都是矢量,所以不言而喻,它们必须用矢量方法组合(加或减)。在前一章中你已经看到了一个例子,所以我们在这里不再重复。
为什么要把这些量结合起来?您可能经常想要添加位移和速度来计算它们的总和(也称为它们的合力)。同样,你在第三章中看到了一个合成位移的例子。一个你想要增加速度的例子是一艘船在流动的河流上。船只的合成速度由船只在水中的速度与水流速度的矢量和给出(见图 4-3 )。
图 4-3。
Resultant velocity
当计算相对速度时,你需要使用矢量减法。假设你有一艘宇宙飞船 A 在追逐另一艘宇宙飞船 B,在太空中以不同的速度 A 和 B 运动(见图 4-4 )。B 相对于 A 的速度是多少?那是从 A 的角度看 B 的速度,换句话说,是 B 在 A 不动(我们说它静止)的参照系中的速度。稍微静下心来想一想,你应该会相信它是由 B 减去 A 的速度给出的,在矢量意义上:B–A。图 4-4 向你展示了 B–A 在几何上的含义:它与 b+(–A)相同,因为(–A)以相同的大小指向 A 的相反方向,我们最终得到 B–A 指向所示的方向。这是从 A 的角度看 B 的速度。
图 4-4。
Relative velocity
用图形描述运动
物体的运动可以用不同的方式用图形表示。对于一个二维运动的粒子,你可以在不同的时间绘制它的 y 坐标和 x 坐标。当然,最终的图形,只会给你粒子的轨迹。
还可以绘制位移、速度或加速度随时间变化的曲线。这些图表能告诉我们关于粒子如何运动的有用信息。
你将很快看到一些例子,在这些例子中,我们使用了在前一章中介绍的Graph
对象来创建这样的图形。
匀加速运动方程
对于匀加速运动的特殊情况(也称为匀速运动),仅从速度和加速度的定义出发,就有可能得到一组非常有用的方程。这些运动方程可以用来分析加速度为常数的问题,包括重力作用下物体的下落运动和抛射体的运动。
我们可以只陈述运动方程,并要求你接受它们。但是如果你看到他们来自哪里,你可能会更好地理解他们。这并不困难。你只需要做少量的代数运算。起点是平均加速度的定义:
如果加速度不变,a av 也等于任意时刻 t 的瞬时加速度 a。
现在让我们建立初始条件,假设在时间 t = 0(初始),s = 0(零初始位移),v = u(初始速度用 u 表示)。
因此,在时间 t,我们有δv = v–u,和δt = t–0 = t,因此
现在很容易改变这个公式的主题:
这个公式给出了任意时刻 t 的速度 v,给定了初速度 u 和(恒定)加速度 a。
如果也有一个类似的位移方程就好了。能做到吗?是的,你只需要运用同样的技巧,这次从平均速度的定义开始:
现在我们有δs = s–0 = s,δt = t–0 = t,这就给出
因为速度线性增加,平均速度 v av 正好是 (u + v),初速度和终速度的平均值。代入前面的等式得出:
现在你可以用已经导出的公式(v = u + a t)来代替 v,这样做并简化,你应该得到这个:
现在坐下来,欣赏你努力工作的非凡成果。这里有一个公式,告诉你一个质点在任意时刻 t 的位移 s,用质点的初速度 u 和(恒定)加速度 a 来表示。
如果质点在时间 t = 0 时从原点出发,这将等于质点在时间 t 时的位置向量。一般来说,如果粒子在时间 t = 0 从位置 pos0 = (x0,y0,z0)开始,那么它在时间 t 的位置向量 pos = (x,y,z)将由以下伪代码给出:
pos = u * t + 0.5 * a * t * t + pos0
这给了你粒子的位置,作为一个精确的解析方程。在这种情况下,没有必要使用数值积分。
您可以直接以矢量形式编码这个解决方案(正如我们所做的那样),或者如果您愿意,也可以将它拆分成组件。在伪代码中,你得到的是 3D(在 2D 忽略 z 分量):
x = ux * t + 0.5 * ax * t * t + x0
y = uy * t + 0.5 * ay * t * t + y0
z = uz * t + 0.5 * az * t * t + z0
在我们离开这一部分之前,你应该知道有第三个公式,有时证明是有用的。前面推导的两个公式(v = u + a t,s = u t + a t 2 )给出了速度 v 和位移 s,它们都是时间 t 的函数。如果去掉这两个方程之间的时间 t,就可以得到关于 v 和 s 的方程:
注意,这个是标量方程,不是矢量方程。
Caution
如前所述,本节推导的运动方程只对恒定加速度有效。如果加速度随时间变化,他们会给出错误的答案。
示例:将方程式应用于抛体运动
是时候举个例子了。让我们应用前面的方程来模拟炮弹等抛射体的运动。为此,我们将忽略除重力之外的所有其他力。在那种情况下,射弹在飞行过程中的任何时候所受的力都是恒定的。这会给它一个恒定的加速度(下一章你会完全明白为什么)。
我们首先不模拟发射炮弹的爆炸力。这个力非常短暂,它的作用是给炮弹一个初速度 u。
在这种特殊情况下,分量形式的射弹位置方程简化为伪代码形式:
x = ux * t + x0
y = uy * t + 0.5 * g * t * t + y0
z = uz * t + z0
因为重力产生的加速度指向垂直下方,所以其 x 和 z 分量 ax 和 az 为零,ay = g,如果使用矢量,只需在方程 pos = u * t + 0.5 * a * t * t + pos0 中指定加速度矢量为 3D 中的(0,g,0)和 2D 中的(0,g)。
让我们用这些公式来编写一个简单的抛射体模拟程序。代码在projectile-test.js
中,在此复制:
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var ball1;
var ball2;
var t;
var t0;
var dt;
var animId;
var pos0 = new Vector2D(100,350);
var velo0 = new Vector2D(20,-80);
var acc = new Vector2D(0,10); // acceleration due to gravity
var animTime = 16;
window.onload = init;
function init() {
ball1 = new Ball(15,'#000000',1,0,true);
ball1.pos2D = pos0;
ball1.velo2D = velo0;
ball2 = new Ball(15,'#aaaaaa',1,0,true);
ball2.pos2D = pos0;
ball2.velo2D = velo0;
ball1.draw(context);
ball2.draw(context);
t0 = new Date().getTime();
t = 0;
animFrame();
};
function animFrame(){
animId = requestAnimationFrame(animFrame,canvas);
onTimer();
}
function onTimer(){
var t1 = new Date().getTime();
dt = 0.001*(t1-t0);
t0 = t1;
if (dt>0.2) {dt=0;}; // fix for bug if user switches tabs
t += dt;
if (t < animTime){
move();
}
}
function move(){
// numerical solution - Euler scheme
ball1.pos2D = ball1.pos2D.addScaled(ball1.velo2D,dt);
ball1.velo2D = ball1.velo2D.addScaled(acc,dt);
// analytical solution
ball2.pos2D = pos0.addScaled(velo0,t).addScaled(acc,0.5*t*t);
ball2.velo2D = velo0.addScaled(acc,t);
// display
context.clearRect(0, 0, canvas.width, canvas.height);
ball1.draw(context);
ball2.draw(context);
}
在init()
函数中,代码创建了两个球(一个灰球和一个黑球),它们最初位于相同的位置(100,350),具有相同的初速度(20,–80),并受到相同的向下加速度(0,10)来模拟重力。在move()
函数中,这两个球的位置和速度被不同地更新。对于第一个球,我们实现了欧拉积分方案(如速度一节中所讨论的),通过将加速度乘以当前时间间隔dt
来更新速度。球的位置也是如此。对于第二个球,我们根据精确的解析公式 pos = u * t + 0.5 * a * t * t + pos0 指定一个新的当前位置。不涉及数值近似。
在运行代码之前,请注意onTimer()
中的以下附加代码行:
if (dt>0.2) {dt=0;};
这是对用户切换浏览器标签然后返回时出现的错误的修复。当这种情况发生时,requestAnimationFrame()
停止活动,直到用户返回到当前选项卡,因此动画“冻结”在当前帧上。然而,当用户返回时,经过的时间dt
仍然被计算为对getTime()
的最后一次调用和当前时间之间的差值,导致该帧中dt
的人为大值。然后move()
函数中的代码在很长的时间间隔dt
内使用相同的速度更新粒子的位置。如果速度是恒定的,就像我们在本章前面看到的ball-move.js
例子一样,这是没问题的。但一般来说,如本例所示,速度实际上会因加速度而变化。人为的大值dt
会导致粒子位置的非物理偏差或“跳跃”。如果dt
的值恰好大于 0.2 秒的阈值,前面一行代码简单地通过将dt
的值设置为 0 来解决这个问题。0.2 的值基本上是任意的,您可以使用任何您喜欢的值,只要它比动画正常运行时的典型值dt
大得多(这里大约是 20 毫秒,比 0.2 秒小 10 倍),并且比切换到一个新选项卡并返回所需的时间小得多。
如果你运行这个代码,你会看到两个球一起开始,并像抛射体应该做的那样以抛物线轨迹运动。随着模拟的进行,你会看到它们开始轻微分离,如图 4-5 所示,这是由于欧拉方案造成的数值误差。
图 4-5。
Simulating the motion of a projectile with both numerical and analytical solutions
灰球就在它应该在的地方,因为它遵循运动方程的精确解析解。现在你可能通常不会关心这种差异。但是如果你正在建立一个精确的投射物模拟或者一个台球游戏,你会的。图 4-5 所示的位置差异仅发生在 16 秒的运行时间之后。如果你要运行几分钟,并包括反弹或与其他粒子的碰撞,你的粒子很快就会在它不应该在的地方。
那么,如果你的模拟或游戏要求很高的准确性,对此可以做些什么呢?如果您的模拟足够简单,可以获得解析解(如本例所示),那么使用解析解就可以解决问题。然而,在绝大多数情况下,解析解并不存在。在这些情况下,你需要使用比欧拉方法更精确的积分方法。
本书第四部分的第十四章涵盖了一些更精确的集成方案。但是在我们到达那里之前,我们将在大多数例子中使用欧拉方案,因为欧拉是最简单和最快的方案,并且我们在本书第四部分之前的大部分中的主要目的是演示物理效果,而不必太担心绝对精度。随着我们的继续,我们将指出缺乏准确性可能特别重要的例子,尽管我们将把任何解决方案推迟到第十四章。
如果担心数值精度,积分方案的选择不是唯一的考虑因素。处理突然变化(如弹跳和碰撞)的算法也会产生误差。我们将在本书的后面讨论这些错误的来源以及如何处理它们。
更多与运动相关的概念:惯性、质量和动量
到目前为止,我们被限制在几个运动概念上,而我们能取得的进展也相当有限。最大的力量来自于力的概念以及它们如何影响运动。我们将在下一节中从概念上介绍这种方法,然后在下一章中对其进行全面阐述。
在此之前,我们需要引入两个新概念,作为运动和产生运动的力之间的联系:质量和动量。
先说质量。在物理学中,质量有一个非常具体的含义:它是惯性的量度。惯性(字面意思是“懒惰”)是对运动的阻力。物体质量越大,运动阻力越大——推汽车比推自行车更难。所以我们说汽车的质量大于自行车的质量。物理学中通常的质量单位是千克(kg)。质量的通常符号是 m。
在物理学中,物体的质量和重量是不一样的。重量其实是一种力,地球施加在它身上的重力。质量是标量;重量是一个向量。我们将在下一节回到这个问题。
一个物体的动量,通常用符号 p 表示,大小为 p,定义为其质量和速度的乘积。它是一个矢量。
因为它等于标量(质量)乘以速度,所以它的方向与速度矢量的方向相同。
通俗地说,动量是一个物体拥有的“运动量”的度量——对于相同的速度,汽车比自行车“运动更多”,因为它更难停下来。动量之所以重要,有两个原因:它与力密切相关,有一个定律叫做动量守恒定律,对于解决粒子间的碰撞等问题非常有用。我们将在下一章仔细研究动量及其守恒。
预测运动:力和动力学
在前一节中,我们可以通过得到一个抛射体位置随时间变化的公式来“解决”抛射体问题。这是可能的,因为我们知道加速度,它是常数,等于重力加速度 g。
预测物体运动的问题基本上归结为计算每一时刻 a(t)的加速度,无论是通过某种解析公式还是通过某种数值方法。一旦我们知道了 a,我们就知道如何去做剩下的事情——我们可以进行数值积分(使用欧拉或其他方案),或者使用解析解(如果有的话)。
在地球表面附近有重力的情况下,你现在知道 a = g,这很简单。但是我们如何计算一个物体的加速度呢?在我们给出答案之前,我们需要再谈一谈力。
运动的原因:力
力是物理学中的一个抽象概念,表示使物体运动的“某物”。更准确地说,如果有点神秘的话,力改变了事物运动的方式。这句话的意思一会儿就清楚了。
直觉上,你可以把力想象成某种推力或拉力。重力是一种力;它把你拉向地球。摩擦力是另一种力;它推着移动的物体。
力是可以测量和计算的。力的单位是牛顿,用符号 n 表示。
力是有用的,因为知道作用在物体上的力就可以预测它的运动。接下来将解释这是如何进行的。
力、质量和加速度之间的关系
让我们回到我们在本节开始时提出的问题。我们怎样才能算出物体的加速度?答案出人意料地简单:
f 是作用在物体上的合力,m 是其质量,a 是力产生的加速度。就是这样。这是一个非凡的公式,可能是本书中最重要的。它之所以引人注目,是因为它以你能想象到的最简单的方式将运动(更准确地说是加速度)与其原因(力)联系起来。
方程 F = m a 是牛顿第二运动定律的特例。这是这条法律最常见的形式。在下一章中,我们将研究法律的一般形式。
你可以将公式改写如下,给定一个物体的质量 m 和作用在其上的力 F,你可以用它来计算加速度 a:
正如我们在第一章中所讨论的,物体的运动是作用在物体上的力的函数:
运动=功能{力}
嗯,a = F/m 就是这个意思。这是我们的职能。力引起加速度;它们会改变物体的速度。这就是我们如何计算运动中的变化。
这个公式与我们关于力和质量分别引起和阻止运动的概念是一致的。对于一个给定的力,质量越大,产生的加速度越小,因为我们将力除以一个更大的数。
下一个问题是:我们如何知道力 F?事实上,有许多不同类型的力。幸运的是,每种力都有公式。公式来自物理理论或实验。当然,对我们来说,它们来自哪里并不重要。重点是我们可以计算出任意时刻作用在物体上的所有力,然后把它们全部加起来,得到合力 F,我们再把这个合力 F 除以物体的质量 m;这给了我们物体的加速度 a。问题解决了。
力的类型
在这本书的第二部分,我们将会详细探讨不同类型的力。这里有一些简单的例子。
重力也许是力的最明显的例子。作用在物体上的重力也称为其重量。地球(或任何其他行星或恒星)对其附近的任何物体施加与该物体质量成比例的引力。所以质量为 m 的物体的重量由下式给出
其中 g 是一个垂直向下的向量,大小不变。
用 a = F/m,这就给出了 a = m g/m = g,换句话说,一个物体的重量(重力对它的作用力)产生加速度 g,如果它是唯一作用在物体上的力。这表明 g 实际上是一个加速度,称为重力加速度。在地球表面附近,其震级约为 9.81 米/秒 2 。注意,物体的质量 m“掉出来了”——所有物体不论质量大小都以相同的加速度下落(只要空气阻力等其他力与重力相比可以忽略不计)。
另一种常见的力是接触力,它是一个物体与另一个物体直接物理接触时所受到的力。例如,当你推动某物时,当两个物体碰撞时,或者当两个物体被另一个力压在一起时(例如,一本书因其重量而压在桌子上),就会发生这种情况。如果你正在读这本书的纸质版,把它放在桌子上。它不会从桌子上掉下来的原因是后者施加了一个向上的接触力,平衡了书上的重力。
另一种类型的接触力是摩擦力,当两个接触的物体相对运动时,摩擦力就会起作用。如果你沿着桌子推书,摩擦力就是阻止书运动的力。我们将在第七章中研究接触力。
还有几个由流体引起的力的例子,如压力、阻力(一种摩擦力)和上推力。我们将在第七章中详细讨论这些力。
然后还有电力和磁力,它们也可以共同作用成一个电磁力。这些力是由具有电荷这一物理属性的粒子和物体施加和经历的。事实上,除了重力之外,所有的日常作用力如接触力、流体力等等都来源于原子和分子之间相互施加的电磁力。我们将在第十章中研究这些力,看看它们如何被用来产生有趣的效果。
合力:力图和合力
在方程 F = m a 中,F 是合力。因为力是一个矢量,两个或两个以上的力产生的合力必须通过矢量相加得到,如第三章所述。
显示作用在物体上的力的矢量图叫做力图。力图是有用的工具,因为它能帮助我们算出合力。关键是作用在物体上的力是否不同并不重要。例如,只要使用矢量加法,就可以将重力添加到摩擦力和阻力中。
作为矢量,力可以被“分解”成垂直分量,如第三章中的解释。有时,通过将力分解成它们的分量,并将所有的水平和垂直分量分别组合起来,可能有助于分析问题。
图 4-6 显示了一个力图的例子。它显示了物体沿斜面下滑时所受的力。有三个力作用在这个物体上:它的重量 m g,向下作用;由表面施加的摩擦力 f,其作用方向与其运动方向相反;和表面施加在其上的垂直于表面作用的接触力 R。注意,你不关心物体施加在表面上的力。如果你正在模拟一个物体的运动,你只关心作用在它上面的力,而不是它施加的力。
图 4-6。
Force diagram for a body sliding down an inclined plane
如果你要模拟一个物体的运动,如图 4-6 所示(可能是一辆下坡的汽车),你会怎么做?一如既往,用一点常识逻辑,一些物理公式,和一些代码。我们不会详细讨论物理和编码(我们会把这些留到《??》第七章,当我们详细讨论摩擦力和接触力的时候),但是这里有一些常识性的部分。凭经验,你知道物体会沿着表面滑动,同时始终保持接触。所以沿着平面分解力是有意义的。嗯,f 已经沿着平面了;r 垂直于它,所以没有沿它的分量;重力的分量(重量)是 mg sin 30 沿斜面向下。因此,沿斜面向下的合力大小由下式给出
当然,你需要知道如何计算摩擦力 f,我们会在第七章告诉你。一旦你知道了 F,你就可以计算加速度 a = F/m,现在你肯定知道了。
平衡力
有时,有两个或两个以上的力作用在一个物体上,碰巧力的矢量和(合力)为零。在那种情况下,力被认为是平衡的。就物体的运动而言,就好像没有力作用在物体上一样。
让我们再一次回到等式 a = F/m,这个等式意味着如果合力 F = 0,那么 a = 0。因此,如果没有合力作用在一个物体上,这个物体就不会加速。换句话说,无论速度是多少,它都不会改变。这意味着两件事。首先,如果物体不运动(如果它是静止的),它将保持静止。但是,如果物体已经以某个速度运动,它将保持这个速度,既不加速也不减速(这实际上是一个负加速度)。后一个结论可能会让你大吃一惊;我们将在下一章讨论牛顿运动定律时继续讨论这个问题。
图 4-7 显示了一个物体在多个力的作用下处于平衡状态的两个例子。在第一个例子中,一本书静止不动地放在一个水平的桌子上,因此它必须经受至少两个加起来为零的力。你知道其中一个一定是向下作用的重力。还有另一个力,接触力 R,桌子施加在书上,向上作用,正好与重力相反。
图 4-7。
Examples of forces in equilibrium
第二个稍微复杂一点的例子再次展示了一个静止的物体。但这次有三种力量在起作用。这个物体是用两根绳子悬挂起来的。所以每根弦上都有张力,还有重力作用在物体上。如果你把这三个力加起来,它们的矢量和一定是零。如果你把每个力分解成水平分量和垂直分量,然后把所有的水平分量加在一起,所有的垂直分量加在一起,两者相加为零。
图 4-8 中的例子显示了匀速飞行的飞机的力图。从前面的讨论中,我们再次推导出,无论有多少个力,无论它们有多复杂,作用在它上面的合力一定为零。事实上,有四个主要的力作用在飞机上:一个向前的推力,一个相反的阻力,一个向下的重量和一个向上的升力。升力平衡飞机的重量,推力平衡飞机上的阻力。推力和阻力相等,这听起来可能有点违反直觉。事实上,只有当飞机加速时,推力才必须超过阻力,例如在起飞和上升时。但是一旦飞机达到匀速,只需要推力来克服阻力。
图 4-8。
Forces on a plane moving at constant velocity are also in equilibrium
示例:物体在重力和阻力作用下下落
所有这些关于力的讨论可能看起来有点抽象,你可能很想看看它是如何应用到实践中的。尽管在第二部分中我们会有大量的例子,但是现在让我们看一个简单的例子来吊起你的胃口。如果您不能立即理解所有内容,也不要担心:当您在第二部分中看到更多的例子后,就会完全理解了。
在这个例子中,一个球落入诸如空气的流体中,当它下落时,它受到向下的重力 W 和向上的阻力 D(见图 4-9 )。当然,重力是不变的,由下式给出:
图 4-9。
A ball falling in air, experiencing forces of gravity W and drag D
我们将在第七章中对阻力有更多的描述;现在我们只是借用下面的公式:
换句话说,阻力与球通过流体的速度成比例(负号表示阻力与速度方向相反,k 是比例常数)。利用这两个公式,我们可以作出如下推论。最初,球是静止的,所以它受到的阻力为零。当球下落时,它在重力的作用下加速,因此它的速度增加,阻力 d 也增加。最终(如果球下落足够长的时间而不落地),阻力将变得与重力相等,因此两者将平衡。在这一点上,我们会达到平衡,所以加速度为零,就像上一节讨论的那样。球将继续以恒定的速度下落,这就是所谓的极限速度。根据物理理论,这是我们预计会发生的情况,但我们能在模拟中再现它吗?
为了证明我们确实可以模拟球在这些力的作用下的运动,我们将制作球下落的动画,同时随着时间的推移绘制球的速度和加速度。看一下文件forces-example.js
中的代码和嵌入它的文件forces-example.html
中的标记。
首先,您会注意到我们在 HTML 文件中有两个 canvas 实例,id 分别为canvas
和canvas_bg
。canvas
实例被精确地放置在canvas_bg
之上,并被透明化(参见样式文件style1.css
了解这是如何完成的)。我们的想法是在canvas_bg
上放置一个静态的Graph
实例,在canvas
上制作球的动画。还要注意,除了 HTML 文件中的vector2D.js
和ball.js
之外,我们还必须包含graph.js
文件。
forces-example.js
中的代码建立在前面的例子之上,但也添加了一些重要的新元素。init()
函数看起来像这样:
function init() {
ball = new Ball(15,'#000000',1,0,true);
ball.pos2D = new Vector2D(75,20);
ball.velo2D=new Vector2D(0,0);
ball.draw(context);
setupGraphs();
t0 = new Date().getTime();
t = 0;
animFrame();
};
这是非常熟悉的:这里新增了setupGraphs()
方法,它在canvas_bg
上设置了几个Graph
实例,用于绘制球下落时的速度和加速度:
function setupGraphs(){
//graph = new Graph(context,xmin,xmax,ymin,ymax,xorig,yorig,xwidth,ywidth);
graphAcc = new Graph(context_bg,0,30,0,10,150,250,600,200);
graphAcc.drawgrid(5,1,5,1);
graphAcc.drawaxes('time (s)','acceleration (px/s/s)');
graphVelo = new Graph(context_bg,0,30,0,25,150,550,600,200);
graphVelo.drawgrid(5,1,5,1);
graphVelo.drawaxes('time (s)','velocity (px/s)');
}
在init()
中调用的用于设置动画的animFrame()
函数本质上与前面的抛射体示例相同,但是时间步进move()
方法现在看起来有所不同:
function move(){
moveObject();
calcForce();
updateAccel();
updateVelo();
plotGraphs();
}
主要出于教学的原因,我们将代码分成了独立的函数,从它们的名字中可以明显看出它们的任务。首先,moveObject()
方法更新球的位置并重新绘制它,它看起来像这样:
function moveObject(){
ball.pos2D = ball.pos2D.addScaled(ball.velo2D,dt);
context.clearRect(0, 0, canvas.width, canvas.height);
ball.draw(context);
}
这并不是我们以前没有见过的新东西。重要的补充是三个方法calcForce()
、updateAccel()
和updateVelo()
,如下所示:
function calcForce(){
force = new Vector2D(0,ball.mass*g-k*ball.vy);
}
function updateAccel(){
acc = force.multiply(1/ball.mass);
}
function updateVelo(){
ball.velo2D = ball.velo2D.addScaled(acc,dt);
}
如您所见,这些函数分别使用 F = mg–kv 计算合成向下力,使用 a = F /m 计算加速度,使用δv = aδt 计算新速度。因此,我们让 ball 实例受到重力和阻力这两个力的作用,计算它们的合力,然后计算加速度和速度。在moveObject()
方法的下一个时间步更新位置。
最后一种方法plotGraphs()
,在canvas_bg
的各个Graph
实例上绘制加速度和速度的垂直分量:
function plotGraphs(){
graphAcc.plot([t], [acc.y], '#ff0000', false, true);
graphVelo.plot([t], [ball.vy], '#ff0000', false, true);
}
运行代码,您将看到类似图 4-10 的内容。当球下落时,它最初以 g = 10 的值(如代码中设置的)加速,并且它的速度开始急剧增加。但是之后阻力开始增加。这减小了向下的合力,因此也减小了向下的加速度,导致速度增加得不太快。模拟进行到大约 10 秒钟时,加速度已经下降到零,因为阻力现在已经增长到足以完全平衡向下的重力。从那时起,球的速度恒定在 20 px/s。这都是如前所述,所以模拟真的工作!事实上,最终速度的值与物理理论预测的完全一样。
为了看到这一点,我们推论当达到最终速度时:
图 4-10。
Simulating a ball falling under gravity and drag
阻力=重力,
因此
这给出了 v = mg/k。在我们的模拟中,m = 1,g = 10,k = 0.5。这给出了 v = 20 px/s,与仿真计算的完全相同。对于力的第一个例子来说还不错,你不同意吗?
能源概念
我们已经谈了很多关于力的问题,但是还有一个物理概念可能同样重要:能量。从我们的角度来看,重要的是,能量概念有时提供了一种替代方法来解决问题,这些问题可能被证明是棘手的,甚至不可能用力的方法来解决。正如你看到的动量,这是因为有一个强大的能量守恒定律(我们很快会谈到)。事实上,能量守恒和动量守恒可以一起应用来解决涉及碰撞的问题,正如我们将在第五章中看到的,更详细的内容将在第一章 1 中看到。
与动量不同,能量的定义有点棘手。在此之前,我们需要引入一个密切相关的概念——工作。
物理学中的功的概念
我们常说力导致运动,但我们也看到一种力可以与其他力平衡存在,所以运动不会发生。为了区分一个力产生运动的效果和仅仅平衡另一个力的效果,我们引入了功的概念。
在物理学中,我们说,当一个力在它作用的方向上产生运动时,它就做功。所做的功 W 定义为力的大小 F 和力的方向上的位移 s 的乘积:
这听起来可能有点抽象,所以让我们看一个例子。假设一块石头从离地面一定高度 h 处落下(见图 4-11 )。
图 4-11。
An object dropped from rest at a height h above the ground
那么石头的重量(重力)通过向下移动它来做功。根据前面的定义,下落距离 h 到地面所做的功由下面的公式给出,因为这里 F = mg,s = h:
因为功是力和距离的乘积(记得力的单位是牛顿,N),所以它的单位是 Nm(牛顿乘以米)。这个单位也有一个特殊的名字,焦耳(J)。所以,J = Nm。功是一个标量。
如果位移和力的方向不一样呢?举个例子,如果石头是斜着扔的,而不是垂直向下掉的,会怎么样?那么所做的功就是力和位移在其方向上的投影的乘积(见图 4-12 )。
图 4-12。
Work done is the scalar product of force and displacement
你可能认为这是 F 和 s 的点积,其中θ是力 F 和位移 s 之间的角度:
因此,如果角度θ为零,则 W = F s(因为 cos (0) = 1)。但是如果θ是 90 度,那么 cos θ = 0,那么 W = 0。换句话说,如果位移垂直于力,力所做的功为零。
Note
合成加速度必须总是在合力的方向上。然而,合成运动(位移)可能与合力方向一致,也可能不一致。
做功的能力:能量
既然我们知道什么是功,能量的定义就很简单:能量是做功的能力。以一块从高处落下的石头为例:因为它能通过下落来做功,所以它只要在地面上就一定拥有能量。这个能量叫做势能(通常缩写为 PE,符号 E p )。同样,一个运动的物体可以通过与另一个物体碰撞,在碰撞过程中施加一个接触力,并使其在这个力的作用下运动来做功。因此,运动的物体也有能量,称为动能(通常缩写为 KE,符号为 E k )。
事实上,功引起能量的转移或转换。例如,施加一个力使一个物体运动,就给了它动能。这导致施加力的力的能量减少。这通常用功能定理来表示:
能量转移=做功
在符号中:
因为我们把能量和功等同起来,所以能量和功有相同的单位:焦耳(J)。像功一样,能量是一个标量。
能量转移、转换和守恒
正如力有多种类型一样,能量也有多种类型。例子包括光能、热能、核能等等。但是通常我们只关心两种形式的能量:动能和势能。在基本的微观层面上,所有形式的能量都是这两种基本形式的表现。
能量可以从一个身体转移到另一个身体。能量也可以从一种形式转换成另一种形式。无论能量是转移还是转化,总能量总是守恒的。
能量转移的一个例子是两个粒子的碰撞,其中一个粒子失去 KE,另一个粒子获得 KE。弹性碰撞被定义为总动能守恒的碰撞(KE 不会因碰撞而转化为其他形式的能量);否则,碰撞是非弹性的。因此,如果碰撞是弹性的,一方得到的 KE 的量正好等于另一方失去的量。
作为能量转换的一个例子,从一定高度释放的物体失去势能,但在向下加速时获得动能——其 PE 转换为 KE。如果物体只在重力的作用下下落,KE 中的增益正好等于 PE 中的损耗;动能加上势能的总能量保持不变。如果有其他的力,比如摩擦力(由于空气阻力),通过对摩擦力做功,将少量的能量转化为身体和周围空气中的热能。所有形式的总能量仍然保持不变。这就是众所周知的能量守恒原理。
这个原则如此重要,让我们用一种更通用的方式再陈述一次:
能量守恒原理:在相互作用过程中,能量可以从一种形式转化为另一种形式,或者从一个物体转移到另一个物体,但一个封闭系统的总能量是恒定的。
“封闭系统”是指除自身外没有其他相互作用的系统。在系统中,交互和交换可以是多种多样的,也可以是复杂的,这个原则仍然适用。例如,如果你有数百个粒子在一个封闭的系统中相互碰撞,该原理将成立。如果你有无数的分子在一个孤立的气体中相互作用,这个原理仍然成立。
势能和动能
因为势能和动能这么重要,我们来看看怎么计算。让我们从势能开始。当我们讨论功的概念时,回想一下,一个物体从离地高度 h 下落所做的功等于 mgh。因为做的功等于传递的能量,所以物体在下落之前,一定拥有这个能量。因为它在高度 h 从静止状态落下,它的速度最初为零,所以它没有动能。因此,它的能量一开始都是势能。因此,我们得出结论,一个质量为 m 的物体在离地高度 h 处,其势能的大小由下式给出
这是我们的势能公式。
现在让我们算出一个动能公式。考虑同样的落石的例子。忽略摩擦损失的能量,石头落地前的动能必须等于它最初的势能(因为它在地面上没有任何 PE,所以它必须全部转化为 KE)。所以最后的 KE 在数值上也等于 mgh。这里 h 是石头落下的距离。因为 KE 是由于运动而拥有的能量,我们真的想要一个公式,作为石头速度的函数,而不是它所经历的位移。还记得连接速度和位移的公式吗?使用 v 2 = u 2 + 2 a . s,并注意到 u = 0,a = g,s = h,我们得到 v 2 = 2gh。
使用这个,我们现在需要做的就是用 v 2 代替 gh,我们最终得到这个:
这是质量为 m 的物体以速度 v 运动时的动能公式。
力量
我们要介绍的最后一个概念是权力。在谈论能量时,我们似乎已经放弃了时间的概念。我们已经讨论了做功和能量交换或转移,但没有提到它发生的速度。在能量方法中,功率引入了时间。
功率被定义为做功的速率。因此,在微积分符号中,乘方 P 由下式给出:
或者,以离散形式:
换句话说,P 定义为所做的功除以所用的时间。根据这个定义,功率的单位是 J/s。这个单位有一个特殊的名称。它叫做瓦特。另一个常用的单位是马力(HP)。功率是一个标量,就像能量一样。
我们可以颠倒最后一个公式,以获得当使用功率 P 时,在时间间隔δt 内所做的功:
功率是一个非常有用的概念,用于分析机器的运行,例如车辆。假设一台机器通过施加一个力 F 并在 F 的方向上以速度 v 移动其施力点来做功,那么,所做的功就是δW = Fδs(注:这只是为小变化δW 和δs 写的公式 W = Fs)
根据功率是做功的速率的定义,机器的功率输出因此是这样的:
因为δs/δt = v,我们最终得到这个公式:
所以机器的功率等于它施加的力 F 和它产生的速度 v 的乘积。
如果你正在模拟由人类设计的机器,比如汽车和船,你通常需要应用功率概念。
示例:一个基本的“汽车”模拟
我们通过展示一个简单的例子来结束这一章,这个例子展示了如何在不明确使用力的情况下,应用功率和能量的概念来模拟运动。在本例中,您将对一个Ball
对象(汽车)施加动力,以加速其摩擦和其他动力损失,就像您踩下汽车的油门踏板以保持其移动一样。
该示例的代码在文件energy-example.js
中,在此完整显示:
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var canvas_bg = document.getElementById('canvas_bg');
var context_bg = canvas_bg.getContext('2d');
var car;
var t;
var t0;
var dt;
var animId;
var graph;
var force;
var acc;
var g = 10;
var k = 0.5;
var animTime = 60; // duration of animation
var powerLossFactor=0.1;
var powerApplied=50;
var ke;
var vmag;
var mass;
var applyThrust=false;
window.onload = init;
function init() {
car = new Ball(15,'#000000',1,0,true);
car.pos2D = new Vector2D(50,50);
car.velo2D=new Vector2D(20,0);
car.draw(context);
mass = car.mass;
vmag = car.velo2D.length();
ke = 0.5*mass*vmag*vmag;
window.addEventListener('keydown',startThrust,false);
window.addEventListener('keyup',stopThrust,false);
setupGraphs();
t0 = new Date().getTime();
t = 0;
animFrame();
};
function setupGraphs(){
//graph = new Graph(context,xmin,xmax,ymin,ymax,xorig,yorig,xwidth,ywidth);
graph= new Graph(context_bg,0,60,0,50,100,550,600,400);
graph.drawgrid(5,1,5,1);
graph.drawaxes('time (s)','velocity (px/s)');
}
function startThrust(evt){
if (evt.keyCode==38){
applyThrust = true;
}
}
function stopThrust(){
applyThrust = false;
}
function animFrame(){
animId = requestAnimationFrame(animFrame,canvas);
onTimer();
}
function onTimer(){
var t1 = new Date().getTime();
dt = 0.001*(t1-t0);
t0 = t1;
if (dt>0.2) {dt=0;}; // fix for bug if user switches tabs
t += dt;
//console.log(dt,t,t0,animTime);
if (t < animTime){
move();
}else{
stop();
}
}
function move(){
moveObject();
applyPower();
updateVelo();
plotGraphs();
}
function moveObject(){
car.pos2D = car.pos2D.addScaled(car.velo2D,dt);
context.clearRect(0, 0, canvas.width, canvas.height);
car.draw(context);
}
function applyPower(){
if (applyThrust){
ke += powerApplied*dt;
}
ke -= powerLossFactor*vmag*vmag*dt;
}
function updateVelo(){
vmag = Math.sqrt(2*ke/mass);
car.vx = vmag;
}
function plotGraphs(){
graph.plot([t], [car.vx], '#ff0000', false, true);
}
function stop(){
cancelAnimationFrame(animId);
}
从前面的例子来看,大部分代码应该是熟悉的。init()
方法包括设置 keydown 和 keyup 事件监听器的额外代码。如果按下向上箭头键,相应的事件处理程序将变量Boolean
的值applyThrust
设置为true
,否则设置为false
。
代码中使用了一些方便的变量:mass
(粒子的质量)vmag
(其速度大小)ke
(其动能)powerApplied
(施加的功率)和powerLossFactor
(我们用来计算由于摩擦和其他因素造成的功率损失的系数)。vmag
和ke
的值在init()
中初始化。我们给球在 x 方向的初速度是 20 px/s。球的质量是 1 个单位。
代码中最重要也是最新颖的部分是两个方法applyPower()
和updateVelo()
,它们在move()
方法中的每个时间步都会被调用。
在applyPower()
中,我们正在更新球的动能。布尔变量applyThrust
告诉我们向上箭头键是否被按下;如果是,则通过将功率乘以时间间隔dt
来更新 KE。我们现在做的是应用这个公式:
你可能不会马上认出这个公式,但它是通过将做功δW = pδt 与动能相等得到的,动能是根据功能定理得到的,如前所述。从概念上讲,所发生的是所施加的功率对球做机械功,这导致球的动能增加。
在applyPower()
中,你会注意到我们也减少了球的动能,减少量等于powerLossFactor*vmag*vmag*dt
。这相当于应用由下式给出的功率损耗,其中 k 是对应于powerLossFactor
的常数:
这个公式来源于应用我们在上一节看到的公式 P = F v,并用 F =–k v 模拟所有阻力(如摩擦力和阻力)的总和,作为与球的速度成比例的力(见第七章)。这是一个粗略的近似值,但是对于我们这个简单的例子来说已经足够了。
updateVelo()
方法首先通过该公式从更新的动能计算速度大小,该公式是通过反转动能 E k = m v 2 的公式获得的:
然后它相应地更新球的速度。
还有一些代码用于设置一个Graph
实例,并随着模拟的进行绘制球的水平速度分量的图形。这些代码很简单,所以我们不会在这里深入讨论。
如果你运行代码,你会发现球开始移动,但是由于能量损失的影响,它的速度逐渐降低。如果你接着按住向上箭头键,施加的力量将导致球的动能增加,使其加速。如果持续通电,最终速度会趋于一个恒定值,但如果松开上箭头键,速度会再次降低,如图 4-13 所示。这种行为以简单的方式反映了汽车加速器的操作。
图 4-13。
A rudimentary car simulation
摘要
这是另一个很长的概述章节,你现在已经学到了很多物理概念。这一章完成了这本书的第一部分。下一章将把你到目前为止所学的东西结合在一起,列出如何模拟各种各样的力和运动的原理。
准备好真正的行动吧!
五、支配运动的定律
第四章提供了一些描述和分析运动的一般概念背景。背景知识包括对运动相关概念的讨论,如速度、加速度和力。在这一章中,我们将使用这些概念来阐述物理定律,这将允许你计算粒子在任何类型的力下的运动。接下来的五章将把这些定律应用到一些不同的力上。我们将用简短的例子来说明这些原则的应用,目的是在接下来的几章中以它们为基础。
我们特意让这一章保持简短,并把重点放在你需要应用到本书这一部分和其他部分的例子中的基本法则和原则上。你可以根据需要回到这一章来更新你对基本物理原理的理解,就像你在这本书的其余部分研究它们的应用一样。
本章涵盖的主题包括以下内容:
- 牛顿运动定律:牛顿三定律提供了力和运动之间的联系。这些定律使我们能够预测物体在已知力的作用下的运动。
- 应用牛顿定律:我们向你展示了如何在代码中实现牛顿运动定律,并用几个例子来说明它们的用法。
- 牛顿第二定律作为微分方程:在物理教科书中,牛顿第二定律有时被表示为微分方程,并使用微积分进行解析求解。我们简要讨论了该公式和我们的数值方法之间的联系,并用一个例子来说明。
- 能量守恒原理:我们通过考虑运动粒子的势能和动能,将能量守恒原理应用于运动。
- 动量守恒原理:我们来看看动量守恒及其在一个简单碰撞例子中的应用。
- 支配旋转运动的定律:我们将简要地注意到前面的定律可以扩展到经历旋转运动的物体。我们在这里不讨论旋转,但将在后面的章节中讨论。
牛顿运动定律
牛顿的三大运动定律,在他 1687 年的经典著作《数学原理》中阐明,提供了力和运动之间的联系。你可能还记得,我们在前一章说过,公式 F = m a 提供了这种联系。这个公式实际上是牛顿三大运动定律中的第二个。事实上,它是法律一种特殊形式。在这一章中,我们将深入一点,看看牛顿第二定律以及其他两个定律的一般形式。
牛顿第一运动定律(N1)
运动第一定律告诉你如果没有力作用在物体上会发生什么(见图 5-1 )。想象一个静止不动的物体。如果你不对它施加任何力(换句话说,如果你不管它),你认为会发生什么?没错——什么都没有。事物不会突然自己开始运动。这基本上就是第一定律所说的。常识?但是等等,还有更多。假设物体已经在运动,没有力作用在它上面。它会怎么样?日常经验可能会诱使你认为物体会减速并最终停止。所以当你听到第一定律说只要没有外力作用,物体就会继续以初速度运动时,你可能会感到惊讶。
图 5-1。
Schematic illustration of Newton’s first law of motion
这个命题似乎与我们的日常经验有些背道而驰。例如,一个在地上滚动的球总是会停下来。事实上,它这样做是因为与地面的摩擦导致它减速。如果没有摩擦力,球会无限期地运动。这可以通过减少与底层表面的摩擦来证明——例如,在非常光滑的水平面上使用弹珠。然后,弹珠以直线移动很长一段距离,其速度不会显著降低。
另一种说法是,你不需要一个力来让一个物体继续匀速运动。你只需要一个力来改变它的速度——例如,开始移动,加速或减速。换句话说,你只需要一个力来使一个物体加速(或减速)。
有了这种认识,我们可以用下面的形式简洁地表述牛顿第一运动定律,它涵盖了物体静止和匀速运动的两种情况:
- N1:如果作用在物体上的合力为零,那么它的加速度也为零。F = 0 意味着 a = 0。
这条定律非常重要,所以我们可以用另一种方式来表述:如果没有合力作用在一个物体上,它的速度一定是恒定的(包括零)。反过来,如果一个物体有一个恒定的速度(可能是零),你肯定知道它受到的合力一定是零。
最后注意,我们说合力为零。我们并不是说没有力作用在物体上。正如你在第四章中看到的,作用在同一物体上的两个或更多的力可以处于平衡状态,因此它们的合力为零。在这种情况下,第一定律仍然成立。它没有区分没有合力和根本没有力。
为了将这些概念放入上下文中,第四章中的最后两个例子中的moveObject()
方法实现了第一定律;它带走一个粒子,并永远以它的速度运动。它对力一无所知。
牛顿第二运动定律(N2)
牛顿第二运动定律建立在第一定律的基础上(见图 5-2 )。第一定律告诉你当没有合力作用在物体上时会发生什么。第二定律告诉你当合力作用在物体上时会发生什么。它告诉你物体的运动改变了。但这还不是全部。它会准确地告诉你它改变了多少——它会给出一个精确的公式,将施加的力和产生的运动变化联系起来。
图 5-2。
Schematic illustration of Newton’s second law of motion
回想一下上一章,动量(用符号 p 表示,定义为 p = m v)代表一个物体所拥有的“运动量”。牛顿第二定律把施加的力和它产生的动量变化联系起来。我们不会去探究牛顿是如何得出那个公式的,但这里有:
- N2:如果一个合力 F 作用在一个物体上,它的动量会改变,所以动量的变化率等于所施加的力:F = d p/dt。
这是牛顿第二定律的一般形式,可能看起来有些不直观,至少在你习惯之前是这样的!用微积分的话来说,这告诉我们作用力等于动量对时间的导数。本质上,动量的时间导数告诉我们,动量在瞬间的变化有多快。这意味着在任何给定时间施加的力等于单位时间内动量的变化。
我们以前见过类似的公式。比如 v = d s/dt,a = d v/dt。牛顿第二定律的形式完全相同:F = d p/dt。请注意,前面的速度和加速度方程实际上是定义,而 F = d p/dt 是告诉我们现实世界中事物如何表现的定律。
在这种一般形式下,第二定律可能看起来与第一定律大不相同。在第一定律中,我们在讨论加速度,现在我们在讨论动量变化率。当我们将第二定律应用于质量不变的物体时,比如粒子,这两者之间实际上是有联系的。
要了解这种联系,请再次回忆动量是由 p = m v 定义的,对于一个粒子,质量 m 是常数。那样的话,微积分的规则告诉我们 d p/dt = m d v/dt。如果你对微积分感兴趣,我们在这里做的是计算 p 的(时间)导数,它必须等于 m v 的导数(因为这两件事是相等的:p = m v)。但是 m 是常数,所以 m v 的导数等于 m 乘以 v 的导数,你大概还记得 d v/dt = a(加速度的定义)。因此,我们得出结论,对于一个粒子,d p/dt = m a。因为牛顿向我们保证 F = d p/dt,我们已经恢复了我们的老朋友 F = m a。所以这里是牛顿第二运动定律的特殊形式:
N2(特殊):如果一个合力 F 作用在一个质量为 m 的物体上,它会产生一个加速度 a,公式为 F = m a
如果你现在将第二定律的这种形式与上一节给出的第一定律的陈述相比较,你会发现 N1 实际上只是 N2 的一个特例。从 F = m a 开始,放 F = 0,得到 m a = 0,因此 a = 0。换句话说,F = 0 给出 a = 0,这正是第一定律所说的。所以第一定律被“包含”在第二定律中。好;少了一条需要担心的法律!
大多数时候你会用到牛顿第二定律,以 F = m a 的形式。但是请记住,这不是定律最普遍的形式,它只适用于质量 m 不变的情况。有第三种形式的定律适用,当你有一股以一定速度运动的物质流,而不是一个固定质量的物体。例如,火箭的废气以相对于火箭恒定的速度 v 被推出。因为 v 现在是常数,微积分规则告诉我们 d p/dt = v dm/dt。因此,气体上的力由 F = v dm/dt 给出,其中 dm/dt 是质量变化率(每秒释放的气体质量)。我们将在第六章中使用这种形式的牛顿第二定律来模拟火箭:
N2(交替):以恒定速度 v 以 dm/dt 的速率移动任何物质所需的力 F 由 F = v dm/dt 给出。
尽管简单,牛顿第二定律可以说是牛顿力学中最重要的定律。它是如何将力和运动联系起来的,这几乎是不可思议的。但是,除非我们知道如何计算出作用在物体上的力,否则这也是毫无用处的。我们需要强制法。我们很快会看一些力定律的例子。但在这之前,你想知道牛顿第三定律,不是吗?
牛顿第三运动定律(N3)
在前一章中,我们说过力是运动变化的原因。正如你刚才看到的,牛顿第二定律使这个陈述更加精确,给了我们一个公式来计算一个给定力引起的运动变化。但是到目前为止,关于力从何而来,我们还没有说太多。许多力的产生是因为其他物体的存在。牛顿第一和第二定律告诉我们物体的运动是如何对外力做出反应的。牛顿第三定律告诉我们两个物体是如何通过相互施加力来相互作用的。这是这样的:
N3:如果一个物体 A 对另一个物体 B 施加一个力 F,物体 B 必然反过来对物体 A 施加一个大小相等方向相反的力 F
这意味着物体之间的力总是以作用力-反作用力对的形式存在。两种力量同时存在;他们之间没有延迟。即使它们的大小和方向随时间变化,它们始终保持大小相等,方向相反。
牛顿第三运动定律经常被误解和错误引用。一个常见的错误是认为作用力和反作用力相互“平衡”。事实上,在作用力-反作用力对中,作用力作用在不同的物体上。所以,认为它们互相“平衡”或“抵消”是错误的。换句话说,它们并不均衡。每个力作用在不同的物体上,单独影响其运动。诸如此类的细微之处使得牛顿第三定律在抽象层面上有些难以深入理解。但是看看这个定律在分析具体问题时是如何应用的,会有很大帮助。例如,在接下来的“动量守恒原理”一节中,牛顿第三定律被用来分析两个粒子之间的相互作用。你也会看到它在整本书不同例子的分析中的应用。
以下是力的作用力-反作用力对的一些例子:
-
Two colliding bodies exert equal and opposite forces on each other, even if they have different masses, as depicted in Figure 5-3.
图 5-3。
Schematic illustration of Newton’s third law of motion
-
地球对你施加的重力等于你的体重。这暗示着你也在对地球施加一个大小相等方向相反的重力!
-
在火箭中,发动机对废气施加一个向下的力,废气反过来对火箭施加一个大小相等方向相反的力,推动火箭前进。
运用牛顿定律
实际上,这本书的其余部分都是关于应用牛顿运动定律。在这里,我们将建立方法,创建一些力函数,并用一些简单的例子说明它们的应用。
应用 F = ma 的一般方法
要分析应用 F = m a 的问题,请使用以下步骤:
Draw a diagram representing the interacting objects in the problem. Choose the object whose motion is to be calculated and indicate using arrows all the forces acting on it due to other objects. Ignore the forces exerted by the object on other objects. This gives you a force diagram, as described in Chapter 4. Calculate the resultant force F on the object (using vector addition, as described in Chapter 3) and apply the second law F = m a to calculate the acceleration a.
原则上,这就是全部内容。你必须用纸和笔完成第一步和第二步。让我们使用 JavaScript 来帮助我们做第 3 步。
对任何力下的运动进行编码
为了实现上一节中的步骤 3,我们需要一段通用代码,它允许你计算一个粒子上的力,找到它们的合力,从而计算它们的加速度。然后,您可以使用加速度来更新粒子的速度和位置。回想一下我们使用术语“粒子”的意义,正如在第四章中所讨论的:从模拟的角度来看,任何内部结构不相关的物体。因此,为了我们的目的,一个粒子可以是一个球,也可以是一个行星。
这样做的一般代码结构看起来非常类似于上一章示例forces-example.js
中的move()
方法:
function move(){
moveObject();
calcForce();
updateAccel();
updateVelo();
}
如前所述,moveObject()
方法只是根据粒子的现有速度来移动粒子:
function moveObject(){
particle.pos2D = particle.pos2D.addScaled(particle.velo2D,dt);
context.clearRect(0, 0, canvas.width, canvas.height);
particle.draw(context);
}
剩下的代码计算力,计算加速度,并更新速度,准备在下一个时间步的moveObject()
方法中使用。calcForce()
的工作是计算作用在质点上的合力。我们将在最后讨论这个问题。一旦我们知道了力,?? 方法就会更新加速度。我们如何做到这一点?当然,使用 F = m a 给出 a = F/m。因此,将acc
和force
定义为Vector2D
对象,updateAccel()
只是一行代码:
function updateAccel(){
acc = force.multiply(1/particle.mass);
}
回顾第四章中的可知,δv = aδt,updateVelo()
方法同样简单:
function updateVelo(){
particle.velo2D = particle.velo2D.addScaled(acc,dt);
}
最后,calcForce()
方法应该计算出作用在粒子上的每一个力,然后将它们相加,得到它们的合力,即force
变量。进入calcForce()
的代码将取决于问题和涉及的力量。在第四章的forces-example.js
代码中,calcForce()
看起来是这样的:
function calcForce(){
force = new Vector2D(0,particle.mass*g-k*ball.vy);
}
在这个简单的例子中,我们直接指定了力。但是随着我们的例子变得越来越复杂,将我们将遇到的不同种类的力定律聚集到一个我们称之为Forces
的对象中的静态方法中是很有用的。为了调用特定力的作用,我们可以这样做:
function calcForce(){
force = Forces.zeroForce();
}
这种方法目前所做的就是将力设置为零。它使用Forces.zeroForce()
方法来实现这一点,该方法产生一个没有组件的Vector2D
对象。当然,关于Forces
对象我们还没有说太多,所以现在让我们开始吧。
力对象
确切地说,进入calcForce()
取决于手头的问题,但它总是涉及指定粒子上的力,然后将它们相加。因此,让我们构建一个新的对象来帮助我们完成这些任务。
我们将构建的对象基本上只包含不同类型力的静态方法。因此,我们将把这个对象命名为Forces
,这样就足够恰当了:
function Forces(){
}
一般来说,作用在粒子上的力可能取决于粒子的性质(大小、位置、速度、质量、电荷),以及它所处环境的性质,或其他物体的性质。这些属性需要被指定为相关方法中的参数。
让我们看一些例子。首先,让我们创建一个零力。这是一个大小为零的力,因此分量等于零。
下面是将创建一个的静态方法:
Forces.zeroForce = function() {
return (new Vector2D(0,0));
}
接下来,让我们创建一个重力方法:
Forces.constantGravity = function(m,g){
return new Vector2D(0,m*g);
}
质量为 m 的物体所受的重力由 mg 给出,并指向下方。因此,我们给出 m 和 g 的值作为自变量,并返回一个向量,其垂直(y)分量为 mg,水平(x)分量为零。你会注意到我们将函数命名为constantGravity
,而不是简单的gravity
。这是因为我们将重力这个名称保留给更一般形式的重力,你会在第六章的中了解到。mg 给出的引力形式是近地引力,就像地球表面附近的物体所经历的那样。在下一章你会学到更多。
作为另一个例子,让我们看看阻力,这是一个物体在流体(如空气或水)中运动时受到的阻力。我们将在第七章中更深入地研究阻力,但现在我们只能说在低速时,阻力由–k v 给出。这是一个常数 k 乘以物体的速度 v。负号表示阻力与速度方向相反。让我们为这种类型的阻力创建一个函数,我们称之为linearDrag
。
下面是静态方法linearDrag
:
Forces.linearDrag = function(k,vel){
var force;
var velMag = vel.length();
if (velMag > 0) {
force = vel.multiply(-k);
}else {
force = new Vector2D(0,0);
}
return force;
}
如你所见,linearDrag
函数有两个参数:物体的阻力常数k
(一个Number
)和速度vel
(一个Vector2D
)。
接下来,我们创建一个静态方法add()
来添加任意数量的力:
Forces.add = function(arr){
var forceSum = new Vector2D(0,0);
for (var i=0; i<arr.length; i++){
var force = arr[i];
forceSum.incrementBy(force);
}
return forceSum;
}
add()
方法将力的数组arr
作为参数。它遍历数组,依次添加力并返回最终的矢量和。这就是我们目前所需要的。在接下来的几章中,我们将添加更多的力函数作为Forces
对象的静态方法。要使用Forces
对象,不要忘记在你的 HTML 文件中添加文件forces.js
(可以在 http://apress.com
找到所有的源代码)。
一个简单的例子:有阻力的抛射体
为了演示如何使用Forces
类,让我们看一个简单的例子,它将我们在前面两节中讨论的内容结合在一起。假设我们想让一个粒子在重力作用下运动,同时经历阻力(比如一个物体在空气或水等流体中抛出或下落)。文件forces-test.js
显示了如何做:
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var ball;
var t;
var t0;
var dt;
var animId;
var force;
var acc;
var g = 10;
var k = 0.1;
var animTime = 10; // duration of animation
window.onload = init;
function init() {
ball = new Ball(15,'#0000ff',1,0,true);
ball.pos2D = new Vector2D(50,400);
ball.velo2D = new Vector2D(60,-60);
ball.draw(context);
t0 = new Date().getTime();
t = 0;
animFrame();
};
function animFrame(){
animId = requestAnimationFrame(animFrame,canvas);
onTimer();
}
function onTimer(){
var t1 = new Date().getTime();
dt = 0.001*(t1-t0);
t0 = t1;
if (dt>0.2) {dt=0;};
t += dt;
if (t < animTime){
move();
}else{
stop();
}
}
function move(){
moveObject();
calcForce();
updateAccel();
updateVelo();
}
function stop(){
cancelAnimationFrame(animId);
}
function moveObject(){
ball.pos2D = ball.pos2D.addScaled(ball.velo2D,dt);
context.clearRect(0, 0, canvas.width, canvas.height);
ball.draw(context);
}
function calcForce(){
var gravity = Forces.constantGravity(ball.mass,g);
var drag = Forces.linearDrag(k,ball.velo2D);
force = Forces.add([gravity, drag]);
}
function updateAccel(){
acc = force.multiply(1/ball.mass);
}
function updateVelo(){
ball.velo2D = ball.velo2D.addScaled(acc,dt);
}
这里的新物理发生在calcForce()
方法中,我们通过使用Forces
类的相关静态方法来包括重力和线性阻力。所以我们调用了Forces.constantGravity()
,使用 g = 10 和粒子质量。我们还使用 k = 0.1 调用了Forces.linearDrag()
。然后我们通过将它们作为数组参数传递给Forces.add()
方法来添加这两个力,并将结果赋给force
变量。
运行代码,你会看到一个球被向上抛;然后它被重力拉下,并因阻力而减速。
为了理解附加阻力对球运动的影响,用下面的行替换calcForce()
中的最后两行:
force = gravity;
这使得球只在重力作用下运动。如果你现在运行代码,你会看到球遵循抛物线轨迹,就像第四章中的抛射体模拟一样。
另一方面,如果你保持阻力并将阻力系数 k 增加到 0.5,阻力将产生更极端的影响,迅速扼杀球的水平运动,并使它在此后几乎垂直下落——类似于你向上撞击气球可能发生的情况。
这个简单的例子演示了利用Forces
对象构建模拟是多么容易,以及如何通过在calcForce()
方法中改变力及其参数来获得不同的效果。
为了让您了解这种方法有多灵活和强大,让我们看一个稍微复杂一些的例子。
一个更复杂的例子:浮球
我们要看的例子包括在空中或水中扔一个球,并让它像在现实生活中那样运动。这个例子使用了比我们所涵盖的更多的物理知识,所以我们不会深入到所涉及的物理或编码的细节中,留给第七章更完整的讨论。在这个阶段,我们只是想通过向您展示使用本节中概述的方法,通过相当少量的简单编码可以完成什么来吊起您的胃口。
这个例子的源代码在文件floating-ball.js
中。在我们看这个之前,在 HTML 设置上简单说一下,给出想要的视觉环境,如图 5-4 所示。正如你从这个截图中看到的,我们有一个代表水的矩形区域,和一个看起来部分浸入其中的球。为了实现这种视觉效果,水被绘制在前景中的透明画布实例canvas_fg
上,动画发生在另一个名为canvas
的画布实例上。看看文件floating-ball.html
和style2.css
,看看这是如何实现的。
图 5-4。
The floating ball simulation
以下是floating-ball.js
的完整代码:
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var canvas_fg = document.getElementById('canvas_fg');
var context_fg = canvas_fg.getContext('2d');
var ball;var t0;
var dt;
var animId;
var force;
var acc;
var g = 50;
var k = 0.01;
var rho = 1.5;
var V = 1;
var yLevel = 300;
var vfac = -0.8;
window.onload = init;
function init() {
// create a ball
ball = new Ball(40,'#0000ff',1,0,true);
ball.pos2D = new Vector2D(50,50);
ball.velo2D = new Vector2D(40,-20);
//ball.velo2D = new Vector2D(20,-60);
ball.draw(context);
// create water
context_fg.fillStyle = "rgba(0,255,255,0.5)";
context_fg.fillRect(0,yLevel,canvas.width,canvas.height);
// set up event listeners
addEventListener('mousedown',onDown,false);
addEventListener('mouseup',onUp,false);
// initialize time and animate
initAnim();
};
function onDown(evt) {
ball.velo2D = new Vector2D(0,0);
ball.pos2D = new Vector2D(evt.clientX,evt.clientY);
moveObject();
stop();
}
function onUp(evt) {
ball.velo2D = new Vector2D(evt.clientX-ball.x,evt.clientY-ball.y);
initAnim();
}
function initAnim(){
t0 = new Date().getTime();
animFrame();
}
function animFrame(){
animId = requestAnimationFrame(animFrame,canvas);
onTimer();
}
function onTimer(){
var t1 = new Date().getTime();
dt = 0.001*(t1-t0);
t0 = t1;
if (dt>0.2) {dt=0;};
move();
}
function move(){
moveObject();
calcForce();
updateAccel();
updateVelo();
}
function stop(){
cancelAnimationFrame(animId);
}
function moveObject(){
ball.pos2D = ball.pos2D.addScaled(ball.velo2D,dt);
context.clearRect(0, 0, canvas.width, canvas.height);
ball.draw(context);
}
function calcForce(){
//force = new Vector2D(0,ball.mass*g-k*ball.vy);
var gravity = Forces.constantGravity(ball.mass,g);
var rball = ball.radius;
var xball = ball.x;
var yball = ball.y;
var dr = (yball-yLevel)/rball;
var ratio; // volume fraction of object that is submerged
if (dr <= -1){ // object completely out of water
ratio = 0;
}else if (dr < 1){ // object partially in water
//ratio = 0.5 + 0.5*dr; // for cuboid
ratio = 0.5 + 0.25*dr*(3-dr*dr); // for sphere
}else{ // object completely in water
ratio = 1;
}
var upthrust = new Vector2D(0,-rho*V*ratio*g);
var drag = ball.velo2D.multiply(-ratio*k*ball.velo2D.length());
force = Forces.add([gravity, upthrust, drag]);
//force = Forces.add([gravity, upthrust]);
if (xball < rball){
ball.xpos = rball;
ball.vx *= vfac;
}
if (xball > canvas.width - rball){
ball.xpos = canvas.width - rball;
ball.vx *= vfac;
}
}
function updateAccel(){
acc = force.multiply(1/ball.mass);
}
function updateVelo(){
ball.velo2D = ball.velo2D.addScaled(acc,dt);
}
不用深入细节,你可以看到我们有一个更复杂的calcForce()
方法,包括三个力:重力、上推力和阻力。calcForce()
中也有一些逻辑,告诉代码如何根据球的位置计算出球上的力。此外,还有一些逻辑告诉代码在边界上做什么。最后,不同的力有许多参数。不过,总的来说,这肯定不是一段过于复杂的代码,您应该有可能了解它在做什么。
onDown()
和onUp()
方法允许用户通过点击鼠标与模拟交互。如果你点击画布上的任何地方,球会立即移动到那里。如果您按住鼠标按钮,拖动光标,然后释放鼠标,球将被赋予一个速度,该速度在数值上等于球与释放鼠标的点之间的距离。
运行模拟,看看它在多大程度上像真实的东西一样,所有这些都是用相对少量的代码完成的。很好玩!
作为微分方程的牛顿第二定律
这一节特别为那些想了解我们在这里所做的事情和你通常在物理教科书、物理网站或维基百科上找到的东西之间的联系的读者准备。它提供了对材料更深入的理解,但在本书的其余部分中并不是严格要求的。如果你愿意,你可以安全地跳过它。
如果你是一个严肃的物理程序员,你可能会在某个时候发现自己在钻研物理教科书或在线资源,也许是为了寻找一些公式或寻找特定问题的解决方案。现在,如果你在寻找任何涉及牛顿第二定律的问题的答案,你很可能会遇到微分方程,以及用数学来解析地解决它们。所有这些数学与我们在前面讨论的数值求解牛顿定律的方法有什么关系?在接下来的两个小节中,我们从概念上解释这种联系,然后用一个具体的例子来说明它。
深入了解 F = ma
微分方程包含量的导数。解微分方程一般比解普通代数方程更复杂,因为它涉及积分(参见第三章)。
F = m a 形式的牛顿第二定律表面上看起来可能像一个简单的代数方程。
然而,记住加速度实际上是速度的导数是有用的,a = d v/dt,这样我们也可以把牛顿第二定律写成
这是所谓的关于速度的一阶微分方程,因为它涉及速度的一阶导数。回想 v = d s/dt,我们可以将前面的等式写成
这现在是一个关于位移的二阶微分方程,因为它涉及到位移的二阶导数(参考第三章)。
一般来说,力 F 可以是位置和速度的函数。你很快就会看到这样一个力函数的例子。
如果你看物理教科书,你有时会看到牛顿第二定律以这些形式表达。原则上,前述微分方程可以通过解析或数值积分来求解,以产生作为时间函数的位移 s 和速度 v。大部分物理教材侧重于解析解。但是只有在特殊情况下解析解才是可能的,这需要应用微积分积分技术。另一方面,用数值方法求解微分方程总是可能的。事实上,这正是我们在我们所看到的例子中所做的。具体来说,我们在updateVelo()
方法中积分微分方程的第一种形式,然后在moveObject()
方法中积分速度以给出位移。
下一个例子将说明一种情况,在这种情况下,我们可以用解析方法和数值方法求解牛顿第二定律。我们将用这个例子向你展示这个微分方程的典型解析解。我们还将比较精确的解析解和数值积分解,看看后者有多好。
示例:重温重力和阻力下的坠落
这个例子建立在上一章所描述的例子的基础上,在这个例子中,我们模拟了一个球在重力和阻力的联合作用下下落,并表明它达到了简单物理理论所预测的极限速度。这里,我们将更新示例,以比较详细的解析解和模拟。
这种情况下的微分方程如下(注意这是 1D 情况;因此没有必要使用向量符号):
或者是速度的一阶形式:
这个方程的解析解在很多物理教材中都有给出(有兴趣的可以从 www.physicscodes.com
下载一个推导作为补充资料)。对于从静止状态掉落的物体,它是这样的:
当时间 t 较大时,指数项趋于零(回想一下第三章对指数函数的复习)。所以根据这个方程,v 趋向于一个极限值 mg/k,当然就是终速度。所以这个解决方案与我们在第四章中发现的一致。另外,它现在告诉我们在任意时刻 t 的速度,而不仅仅是终端速度。
这个解可以反过来积分,给出任意时刻 t 的位移,结果如下:
现在可以将位移 s 和速度 v 的这些解析解与模拟结果进行比较。在forces-example2.js
中,我们从第四章更新了forces-example.js
来做这个比较。我们也在calcForce()
中使用了Forces
方法;除此之外,物理学与forces-example.js
中的完全相同,因为涉及到相同的力(重力和阻力)。
相关代码是函数plotGraphs()
,它绘制图表来比较 s 和 v 的分析值和数值:
function plotGraphs(){
graphDisp.plot([t], [ball.y-y0], '#ff0000');
graphDisp.plot([t], [m*g/k*(t+m/k*Math.exp(-k/m*t)-m/k)], '#0000ff');
graphVelo.plot([t], [ball.vy], '#ff0000');
graphVelo.plot([t], [m*g/k*(1-Math.exp(-k/m*t))], '#0000ff');
}
在这段代码中,我们绘制了球的垂直位移,即ball.y
减去其初始值y0
,以及由Graph
实例graphDisp
的解析解给出的值。类似地,graphVelo
显示由代码和解析解计算的垂直速度。模拟如图 5-5 所示。两者之间的一致性如此之好,以至于在两个图中各自的曲线位于彼此的顶部。因此,我们很高兴在这种情况下,代码中实现的简单欧拉积分方案实际上做得相当不错。
图 5-5。
Comparing numerical and analytical solutions for a ball falling under gravity and drag
能量守恒原理
我们在前一章中介绍了能量及其守恒。这里要重申的关键点是,我们可以应用能量守恒原理来计算事物如何运动以及它们如何与其他事物相互作用。
同样重要的是要记住,该原理可以应用于不同形式之间的能量转换,以及不同物体之间的能量转移。以下是原则的陈述,形式略有不同:
- 能量守恒原理:在不同形式的能量转换中,或在不同物体间的能量转移中,转换或转移前后的能量总量总是相等的。
该原理的一个特别有用的形式是当能量转换或转移只涉及势能和动能时。我们把 PE 和 KE 统称为机械能。
机械能守恒
虽然能量守恒原理的一般形式是强有力的,而且总是正确的,但在实践中可能很难应用,因为计算一个相互作用中涉及的所有能量形式并不总是容易的。例如,想一想当一个球落下,穿过空气落下,从一个表面弹回时所涉及的能量转换。球最初有 PE,当它穿过空气下落时,由于重力的作用,当它加速时,PE 逐渐转化为 KE。由于空气中的摩擦(阻力),少量能量也转化为热量。当它撞击地表时,大量的能量可能会转移到地表。更多的能量通常在撞击时转化为热能,一些也可以转化为声音。现在,我们将避开试图计算像热能和声能这样的东西,因为那会变得相当复杂。但有时,如果它们可以被假设为很小,我们只需要处理 PE 和 KE 在那种情况下,我们说机械能守恒。
机械能守恒是一个特别有用的原理,因为它涉及到与运动(KE)和位置(PE)有关的能量形式。在这一章中,我们将看两个应用它的例子。一种是两个粒子的弹性碰撞,此时粒子的总动能守恒。我们将在后面的“动量守恒原理”一节中讨论这个问题另一个例子是抛射体的运动(忽略空气阻力产生的阻力)。现在我们来看看这个例子。
例子:射弹中的能量变化
为了简化这个例子,让我们假设抛射体以某个初速度 u 从地面水平垂直向上发射。然后我们有一个在重力作用下恒定加速度的 1D 问题,我们可以使用在第四章中介绍的运动分析方程的 1D 版本:
这里 s = h,即抛射体离地面的高度,a =–g,其中 g 是重力加速度。所以我们可以这样写:
现在可以很容易地用下面的公式计算出射弹在任何时候的 PE 和 KE:
以下代码使用 m = 1、g = 10 px/s 2 和 u = 50 px/s 的值计算并绘制 10 秒钟的 PE、KE 及其总和。源代码在projectile-energy.js
中。
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var canvas_bg = document.getElementById('canvas_bg');
var context_bg = canvas_bg.getContext('2d');
var ball;
var animId;
var graph;
var m = 1; // particle mass
var g = 10; // gravity
var u = 50; // initial velocity
var groundLevel = 350;
var n = 0;
var tA = new Array();
var hA = new Array();
var peA = new Array();
var keA = new Array();
var teA = new Array();
window.onload = init;
function init() {
ball = new Ball(10,'#000000',m,0,true);
ball.pos2D = new Vector2D(550,groundLevel);
ball.draw(context);
setupGraph();
setupArrays();
animFrame();
};
function setupGraph(){
//graph = new Graph(context,xmin,xmax,ymin,ymax,xorig,yorig,xwidth,ywidth);
graph = new Graph(context_bg,0,10,0,1500,50,350,450,300);
graph.drawgrid(1,0.5,500,100);
graph.drawaxes('t','p.e., k.e., total');
}
function setupArrays(){
var t;
var v;
for (var i=0; i<=100; i++){
tA[i] = i*0.1;
t = tA[i];
v = u - g*t;
hA[i] = u*t - 0.5*g*t*t;
peA[i] = m*g*hA[i];
keA[i] = 0.5*m*v*v;
teA[i] = peA[i] + keA[i];
}
}
function animFrame(){
setTimeout(function() {
animId = requestAnimationFrame(animFrame,canvas);
animate();
}, 1000/10);
}
function animate(){
moveObject();
plotGraphs();
n++;
if (n==hA.length){
stop();
}
}
function moveObject(){
ball.y = groundLevel-hA[n];
context.clearRect(0, 0, canvas.width, canvas.height);
ball.draw(context);
}
function plotGraphs(){
graph.plot([tA[n]], [peA[n]], '#ff0000', true, false);
graph.plot([tA[n]], [keA[n]], '#0000ff', true, false);
graph.plot([tA[n]], [teA[n]], '#000000', true, false);
function stop(){
cancelAnimationFrame(animId);
}
代码应该容易理解。注意,我们通过在setTimeout()
函数中嵌套对requestAnimationFrame()
和animate()
的调用,将帧速率限制为 10 fps。这将使动画变慢,足以使球的运动在视觉上与图上的相应位置相匹配。在图 5-6 中,我们显示了作为时间函数的三个曲线图。向上弯曲的一系列点是 PE,向下弯曲的一系列点是 KE,水平的(常数)是总能量(PE 和 KE 之和)。从这些图中,我们了解到,当抛射体最初在地面上发射时,它具有零 PE 和最大 KE,然后,在它上升的前 5 秒,它的 PE 以其 KE 为代价增加。恰好在 5 秒钟时,它的 KE 为零,表明它暂时处于静止状态。当它达到最高点时,就会发生这种情况。然后它又开始往下掉。当它这样做时,它的 PE 又开始下降,它的 KE 随着向地面加速而增加。在整个运动过程中,PE 和 KE 的总和是恒定的,如水平系列的点所示。这证明了能量守恒。注意,PE 和 KE 之和只是常数,因为我们忽略了阻力效应。如果你包括阻力,那么你会发现 PE 和 KE 之和会随着时间的推移而略微减小。如果你特别感兴趣,你可以通过增加一个阻力项,对运动方程进行数值积分,给出 v 和 h 作为时间的函数。
图 5-6。
Energy graphs for a projectile—downward curve: KE; upward curve: PE; constant line: total
动量守恒原理
如前一章所述,动量守恒定律和能量守恒定律一样。先说原理,再解释:
- 动量守恒原理:对于任何相互作用的粒子系统,只要没有外力作用于该系统,所有粒子的总动量保持不变。
“相互作用的粒子”是指粒子通过相互施加力来相互影响。力可以是任何类型的;例如,星系中恒星之间的引力。不管粒子间力的性质如何,这个原理都是正确的。一旦你定义了你的系统,该系统中的粒子可能会相互受到任何数量的内力——只要没有外力,该系统的总动量就会守恒。
动量守恒与牛顿运动定律密切相关。实际上,在一定条件下,可能是从牛顿定律推导出来的。让我们看看怎么做。
起点是牛顿第二定律 F = d p/dt,我们可以写成离散形式 F =δp/δt,两边乘以δt 得出:
当然,这只是牛顿第二定律的一种稍微不同的形式,对于一个小但有限的时间间隔δt。它告诉我们的是,如果一个力 F 作用在一个粒子上一个小的持续时间δt,用 F 乘以δt 就可以得到粒子动量的变化δp。我们称这个量 fδt 为力引起的冲量。前面的关系叫做冲量-动量定理。
接下来,想象两个粒子相互作用(相互施加力;例如通过碰撞),如图 5-7 所示。从牛顿第三运动定律来看,它们同时对彼此施加大小相等方向相反的力。如果它们在δt 的时间间隔内相互施加力 F 和–F,它们分别经历–Fδt 和 Fδt 的冲量。根据冲量-动量定理,这意味着它们交换动量,分别获得–δp 和δp。然而,两个粒子的总动量仍然相同,因为一个粒子获得的动量正好是另一个粒子获得的动量的负值;换句话说,一个粒子失去的动量被另一个粒子获得。这个论点延伸到任何数量的相互作用的粒子,所以我们有动量守恒原理。
图 5-7。
Two interacting particles exchanging momentum
动量守恒的例子包括:
- 一个苹果落到地上,每一瞬间都会获得向下的动量。考虑到苹果-地球系统,地球本身必须“倒向”苹果以进行补偿。但是因为地球的质量如此之大,它朝向苹果的速度非常小。
- 火箭排出的废气和火箭本身在相反的方向上获得相等的动量变化。
- 在爆炸中,所有碎片的总动量必须等于爆炸前整个物体的动量。如果物体最初是静止的,爆炸后的总动量(所有碎片动量的矢量和)仍然为零。
为了让你感觉到如何在实践中应用这个原则,这里有一个数值例子。假设你从一把质量为 1.6 kg 的步枪中射出一颗质量为 40 g 的子弹,子弹的出膛速度为 80 m/s,那么步枪的后坐速度是多少?
让我们分别用 m 和 v 来表示子弹的质量和速度。我们将用 M 和 V 分别表示步枪的质量和速度。最初,两者都是静止的,所以总动量为零。
因此,动量守恒原理意味着最终动量也应该为零:
这个等式可以很容易地重新排列为:
代入 M、v 和 M 的值,得到如下结果:
因此,步枪的后坐力速度是–2 米/秒。负号表示它与子弹的速度方向相反。
例子:两个粒子之间的 1D 弹性碰撞
动量守恒在处理粒子间的碰撞时特别有用。碰撞是一种特殊类型的相互作用,在这种相互作用中,粒子在很短的时间内相互施加很大的力。
第十一章的全部内容都致力于碰撞。在这里,我们将简要地看一下 1D 中两个粒子之间弹性碰撞的最简单情况。这里的弹性意味着动能守恒。换句话说,在碰撞过程中,没有能量转化为其他形式(如热量)。当然,动量总是守恒的。
假设粒子的质量为 m1 和 m2,碰撞前的初速度为 u1 和 u2,碰撞后的终速度为 v1 和 v2(见图 5-8 )。运用动量守恒告诉我们
应用动能守恒得出:
记住动能的公式是 1/2 m v 2 。
图 5-8。
Change in particle velocities caused by a collision
这里我们有两个方程,包含两个未知量:两个碰撞粒子的最终速度 v1 和 v2。其他的都知道了。可以求解这些方程,根据已知量 m1、m2、u1 和 u2 给出 v1 和 v2 的一般公式。但是我们会把它留到第十一章来讲。
现在,我们来讨论 m1 = m2 的情况;也就是说,这两个粒子具有相同的质量。在那种情况下,可以证明 v1 = u2,v2 = u1 换句话说,粒子 1 的最终速度等于粒子 2 的初始速度,反之亦然。相同质量的粒子弹性碰撞只是交换了它们的速度!
我们现在将构建一个实现这种特殊情况的示例。代码在collisions-test.js
里。由于它与您到目前为止遇到的其他示例有些不同,所以在讨论它之前,我们在这里复制完整的代码:
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var t0;
var dt;
var animId;
var radius = 15; // ball radius
var balls = new Array();
window.onload = init;
function init() {
makeBalls();
t0 = new Date().getTime();
animFrame();
};
function makeBalls(){
setupBall('#0000ff',new Vector2D(50,200),new Vector2D(30,0));
setupBall('#ff0000',new Vector2D(500,200),new Vector2D(-20,0));
setupBall('#00ff00',new Vector2D(300,200),new Vector2D(10,0));
}
function setupBall(color,pos,velo){
var ball = new Ball(radius,color,1,0,true);
ball.pos2D = pos;
ball.velo2D = velo;
ball.draw(context);
balls.push(ball);
}
function animFrame(){
animId = requestAnimationFrame(animFrame,canvas);
onTimer();
}
function onTimer(){
var t1 = new Date().getTime();
dt = 0.001*(t1-t0);
t0 = t1;
if (dt>0.2) {dt=0;}; checkCollision();
move();
}
function move(){
context.clearRect(0, 0, canvas.width, canvas.height);
for (var i=0; i<balls.length; i++){
var ball = balls[i];
ball.pos2D = ball.pos2D.addScaled(ball.velo2D,dt);
ball.draw(context);
}
}
function checkCollision(){
for (var i=0; i<balls.length; i++){
var ball1 = balls[i];
for (var j=i+1; j<balls.length; j++){
var ball2 = balls[j];
if (Vector2D.distance(ball1.pos2D,ball2.pos2D)<=ball1.radius+ball2.radius){
var vtemp = ball1.velo2D;
ball1.velo2D = ball2.velo2D;
ball2.velo2D = vtemp;
}
}
}
}
这里我们在函数makeBalls()
中创建和初始化三个球,使用一个特殊的函数setupBalls()
来最小化代码重复。为了让事情更有趣一点,我们创建了三个水平排列的球,并给它们不同的水平速度。动画循环代码看起来没什么特别的,但是触发每个时间步长的onTimer()
方法现在除了move()
方法之外还包含了一个额外的函数checkCollision()
。
在checkCollision()
中,我们测试了阵列中粒子对之间的碰撞。为此,我们使用Vector2D.distance(vec1,vec2)
静态方法,用位置向量vec1
和vec2
计算两点之间的距离。碰撞检测算法的逻辑很简单:如果两个粒子中心之间的距离小于或等于它们半径的总和,这意味着它们已经碰撞。然后我们交换两个粒子的速度。
顺便说一下,Vector2D.distance()
方法通过勾股定理计算两点之间的距离(参见第三章),如下面相关Vector2D
方法列表所示:
Vector2D.prototype = {
lengthSquared: function(){
return this.x*this.x + this.y*this.y;
},
length: function(){
return Math.sqrt(this.lengthSquared());
},
}
Vector2D.distance = function(vec1,vec2){
return (vec1.subtract(vec2)).length();
}
如果你运行代码,你会看到,最初,所有三个球靠得更近了。然后,它们经历三次连续的碰撞,交换每一对碰撞的速度。最后,他们都彼此远离。
支配旋转运动的定律
在这一章中,我们集中讨论了平移运动,在这种运动中,所考虑的物体改变了它的位置。但是如果一个物体围绕一个中心旋转或者(如果它是一个延伸的物体)围绕某个轴自转呢?
事实证明,旋转运动学、动力学和守恒定律也存在类似的原理。例如,牛顿运动定律的模拟可以写成旋转运动。类似于动量,有一个量叫做角动量,它也是守恒的。
我们将在第九章和第十三章中研究旋转力学。
摘要
这一章为模拟粒子在任何类型的力下的运动奠定了基础。这里讨论的运动定律和守恒原理可以应用于许多不同的场景。在第二部分的剩余章节中,我们将应用这些定律来模拟物体在各种力定律作用下的运动。