面向 JavaScript 游戏动画仿真的物理学教程(三)

原文:Physics for JavaScript Games, Animation, and Simulations

协议:CC BY-NC-SA 4.0

六、重力、轨道和火箭

从这一章开始,你将探索许多不同的力和它们产生的运动类型。这里我们只关注重力。你将学习重力如何使地球和太空中的物体运动。在你知道之前,你将能够编码轨道和火箭!

本章涵盖的主题包括以下内容:

  • 重力:引力,或称万有引力,是地球对其附近所有物体施加的力。但是重力有比我们到目前为止讨论的更多的东西。
  • 轨道:重力效应的一个例子是让行星保持在围绕太阳的轨道上。了解如何轻松构建简单的轨道模拟。
  • 局部重力:在地球表面附近,重力在局部尺度上的表现。
  • 火箭:制造一个简单的火箭,发射它,并让它围绕一个行星运行。

重力

作为一个星球上的居民,我们可能比任何其他力量都更了解重力。重力,也称为万有引力,将在你在本书中构建的大多数模拟中发挥作用。事实上,你已经遇到了几个例子。然而,到目前为止,在你遇到的所有情况下,我们都是把地球表面附近存在的重力作为一个恒力来处理的。现在是时候深入了解重力了。

重力、重量和质量

让我们首先简要回顾一下在前面的章节中你已经学到的关于重力的知识。如果一个物体的质量为 m,那么地球会对它施加一个垂直向下的力。这个重力的大小等于 mg,其中 g 是一个常数,在地球表面附近大约等于 9.81 m/s 2 。这个力也叫做物体的重量。

常数 g 等于任何物体在重力的单独作用下运动时所受到的加速度。我们可以用牛顿第二定律 F = ma 来看这个。如果合力 F 只是重力,那么 F = mg。因此,我们有了这个:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

两边除以 m 得到这个:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这个结果告诉我们,所有在重力作用下运动的物体,不管它们的质量如何,或者说,不管它们有什么其他性质,都经历相同的加速度。所以,假设所有其他的力都可以忽略不计,如果你从相同的高度落下一把锤子和一栋房子,两者会以相同的速度加速,因此会同时落下!

当然,如果你掉了一根羽毛,要花很长时间才能掉下来。这是因为有一个向上的阻力,这是由于空气阻碍了它的下落。在石头或房子的情况下也有阻力,但在这些情况下,与它们的重量相比,这种力小得可以忽略不计,因此对它们的运动影响也可以忽略不计。如果你把一根羽毛和一把锤子扔到月球上,那里没有空气,因此没有阻力,它们会同时落下。事实上,宇航员大卫·斯科特在阿波罗 15 号任务中正是这样做的。你甚至可以在美国宇航局的网站或 YouTube 上看到这一壮举的视频(搜索“锤子和羽毛”)。

牛顿万有引力定律

地球不是唯一施加重力的物体。牛顿告诉我们,任何物体都会对其他物体产生引力。因此,不仅地球对我们施加引力,而且我们每个人也对地球施加引力,事实上对彼此也是如此!

牛顿给出了计算任意两个物体间引力的精确公式,这就是引力的力定律:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这个公式中,m 1 和 m 2 是所涉及物体的质量,r 是它们中心之间的距离。符号 G 是一个通用常数,其值约为 6.67×10-11Nm2/kg2。它被称为引力常数。我们所说的普适性是指,不管相互作用的物体的属性如何,它都具有相同的值。牛顿假设,同样的公式,同样的 G 值,适用于任何两个物体——从微小的粒子到行星和恒星。因此,我们称之为牛顿万有引力定律。

让我们试着理解这个公式告诉我们什么。首先,引力 F 与两个物体的质量 m 1 和 m 2 成正比。因此,质量越大,力就越大。第二,这个力与它们之间距离的平方成反比;这意味着我们除以 r 2 。因此,两个物体之间的距离越大,力就越小(因为我们将一个数除以一个非常大的数的平方)。第三,因为我们是乘以一个很小的数 G (6.67 × 10 -11 等于 0.0000000000667),所以力会很小,除非涉及非常大的质量。所以,重力是一种很弱的力,除非至少有一个物体质量很大。

为了让你了解我们的意思,让我们计算两个体重 80 公斤、相距 1 米的人(我们称他们为 Joe1 和 Joe2)之间的重力,并将其与地球施加在他们每个人身上的重力进行比较。

利用前面的公式,Joe1 和 Joe2 之间的作用力由 F = 6.67×10-11×80×80/12N = 4.3×10-7N 给出(近似)。那是 0.43 百万分之一牛顿。

地球对每一个施加的力,当然等于它们的重量:mg = 80 × 9.81 N = 785 N(近似值)。你也可以用前面的公式和地球的质量(5.97 × 10 24 kg)和半径(6.37 × 10 6 m)算出 F = 6.67×10-11×5.97×1024×80/(6.37×1062N = 785N .那

我们还没说力的方向。牛顿说力总是沿着连接两个物体中心的线作用(见图 6-1 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-1。

Newton’s law of universal gravitation

还要注意,Joe1 和 Joe2 上的力形成一个作用力-反作用力对。因此,Joe1 对 Joe2 施加 4.3 × 10 -7 N 的力,反之亦然,两个力大小相等方向相反——两个 Joe 中的每一个与地球之间的力也是如此。

创建重力函数

我们现在将创建一个Forces对象的静态方法,它将以牛顿引力定律的形式实现重力。该方法将把进入引力定律的变量作为输入,即 G,m 1 ,m 2 和 r。它必须返回一个矢量力。因此,我们需要一个矢量形式的牛顿引力定律,它实现了力沿着连接两个物体的直线指向的事实。我们先给出公式,然后再解释:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

与之前重力定律的公式相比,我们所做的是把力写成一个向量;然后我们把力的大小乘以–r/r,回想一下第三章 r/r 是 r 方向的单位向量,这里的 r 是其中一个物体相对于另一个物体位置的位置向量。

如图 6-2 所示,r 是两个物体的位置矢量之差,r 是其大小,简单来说就是两个物体之间的距离。回想一下,有两个力,而不是一个:物体 2 对物体 1 的力,反之亦然。这两个力大小相等,方向相反。我们总是把 r 定义为力所作用的物体相对于施加力的物体的位置矢量。我们需要负号,因为力的方向与 r 相反。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-2。

Vector form of Newton’s law of universal gravitation

下面是实现重力的静态方法:

Forces.gravity = function(G,m1,m2,r){

return r.multiply(-G*m1*m2/(r.lengthSquared()*r.length()));

}

我们只是使用Vector2D对象的multiply()方法返回一个新的向量,该向量将 r 乘以(-G m1m2/r3)。现在让我们在接下来的几个例子中使用这个gravity()方法。

在应用gravity函数之前,我们必须为 G、m 1 、m 2 和 r 选择合适的值。因此,我们必须适当地缩放物理值,以便在合理的时间内在屏幕上获得正确的运动量。例如,如果我们正在模拟地球围绕太阳的运动,我们不会想要使用所有变量的真实值,例如太阳的质量,等等。那样做意味着我们要等一年才能看到地球绕太阳转一周!

在第四部分,我们将向你展示如何正确地创建一个按比例缩小的计算机模型。现在,我们将简化这种方法,并做出一些合适的选择,只是为了使运动在屏幕上看起来大致正确。为此,我们将为模型的所有变量选择自己的单位。

让我们从距离 r 开始。这很简单,这里使用的自然单位是像素。这就给我们留下了质量和 G,我们可以给 G 任何我们喜欢的值,所以我们选 G = 1。做了这两个选择后,我们现在需要为质量选择合适的值,以便在屏幕上产生明显的适量运动。我们来看一个例子:轨道!

轨道

使用我们的重力功能,很容易创建一个逼真的轨道模拟。图 6-3 显示了我们将要创建的截图:一颗行星围绕着一个太阳旋转,背景是固定的恒星。为简单起见,我们假设太阳保持不变。事实上,正如我们之前看到的,太阳和行星都将经历大小相同但方向相反的引力(每个行星上的力都指向另一个)。所以行星和太阳都会移动。但是,如果太阳的质量比行星的质量大得多,太阳的运动将是如此之小,无论如何,它几乎是可感知的。那是因为 F = maagain,所以加速度 a = F/m,这样,如果质量 m 很大,加速度 a 就会很小。所以为了节省编码和 CPU 时间,我们可以完全忽略太阳的运动。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-3。

A planet orbiting a stationary sun

轨道代码

实现基本轨道模拟的代码比您想象的要简单。文件orbits.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 planet;

var sun;

var m = 1; // planet's mass

var M = 1000000; // sun's mass

var G = 1;

var t0,dt;

window.onload = init;

function init() {

// create 100 stars randomly positioned

for (var i=0; i<100; i++){

var star = new Ball(Math.random()*2,'#ffff00');

star.pos2D= new Vector2D(Math.random()*canvas_bg.width,Math.random()*canvas_bg.height);

star.draw(context_bg);

}

// create a stationary sun

sun = new Ball(70,'#ff9900',M,0,true);

sun.pos2D = new Vector2D(275,200);

sun.draw(context_bg);

// create a moving planet

planet = new Ball(10,'#0000ff',m,0,true);

planet.pos2D = new Vector2D(200,50);

planet.velo2D = new Vector2D(70,-40);

planet.draw(context);

// make the planet orbit the sun

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.1) {dt=0;};

move();

}

function move(){

moveObject(planet);

calcForce();

updateAccel();

updateVelo(planet);

}

function moveObject(obj){

obj.pos2D = obj.pos2D.addScaled(obj.velo2D,dt);

context.clearRect(0, 0, canvas.width, canvas.height);

obj.draw(context);

}

function calcForce(){

force = Forces.gravity(G,M,m,planet.pos2D.subtract(sun.pos2D));

}

function updateAccel(){

acc = force.multiply(1/m);

}

function updateVelo(obj){

obj.velo2D = obj.velo2D.addScaled(acc,dt);

}

代码非常简单。首先,我们创建一组 100 颗星星作为Ball实例,并将它们随机放在舞台上。当然,这纯粹是出于审美原因。如果你不喜欢明星,那就去除掉他们吧。接下来,我们创建一个太阳作为另一个Ball实例,设置其半径为 70 像素,质量为 1,000,000。我们把太阳定位在(275,200)度。默认情况下,太阳的速度为零。然后我们创建一个行星作为Ball实例,设置它的半径为 10,质量为 1。然后设定行星的初始位置和速度。

动画循环类似于前面的例子;唯一的区别是我们选择在moveObject()updateVelo()中将行星作为参数传递,只是为了一些变化。记住updateAccel()实现了牛顿第二定律,F = m a,让一个粒子在calcForce()方法指定的力的作用下运动。在calcForce()函数中,我们将太阳施加在行星上的重力指定为唯一的力。在Forces.gravity()方法中,我们传递 G 的值(设置为 1)、两个物体的质量以及行星相对于太阳的位置向量:planet.pos2D.subtract(sun.pos2D)

继续运行代码——它真的工作了!

观察行星试图以其初始速度离开,但被太阳引力拉回。你可能想知道为什么行星不落入太阳。事实上,它一直朝着太阳“下落”,但是由于它的初始位置和速度,它每次都“错过”太阳。还要注意,当它靠近太阳时,行星运行得更快。这是因为越靠近太阳,重力越大,因此加速度越大。

这个代码产生了一个真实的轨道模拟,因为它基本上包含了所有在重力作用下控制轨道运动的物理过程。物理学可以归结为四个方程:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这些方程中,M 是行星的质量,M 是太阳的质量。前两个方程是定律,后两个只是定义。概括一下,这就是我们的代码在每个时间步所做的:从第一定律开始,你计算重力。然后你用第二定律来计算加速度。知道了加速度,你可以积分第三个方程来计算速度。最后,知道了速度,你积分第四个方程,给出位移。这告诉你在每个时间步你必须移动你的粒子到哪里。

尝试更改代码中的参数以查看它们的效果。例如,你可以改变行星和太阳的质量,或者行星的初始位置和速度。我们选择太阳的质量是行星的 100 万倍。这是一个现实的比例;比如太阳的质量是地球的 33 万倍,水星的 610 万倍。

把太阳的质量改成 200 万。行星被拉得离太阳更近,因为引力与太阳的质量成正比。太阳质量增加一倍,引力也增加一倍,因此朝向太阳的加速度也增加一倍。相反,如果你将太阳的质量减少到 900,000,你会看到太阳对行星的吸引力变弱,因此行星漂移得更远。

现在改为改变行星的质量。先做 0.001 再做 1000。你会发现行星的质量对它的运动没有影响。这是怎么回事,你是通过增加行星的质量来增加行星上的力,但然后你必须用更大的力除以它更大的质量。看看上面的前两个方程。在第一个例子中,你乘以行星的质量来计算引力。在第二个例子中,你需要用这个力除以相同的质量来计算加速度。最终结果是加速度与行星的质量无关。这是理所应当的。这相当于我们在本章开始时提到的锤子和羽毛实验。

这是否意味着即使行星的质量与太阳相当,运动也不变?不,不是真的。还记得我们一开始做的近似吗?我们假设太阳的质量比行星的质量大得多,这样我们就可以忽略太阳在行星施加给它的引力作用下的运动。如果行星的质量是太阳质量的很大一部分,我们也需要模拟太阳的运动,即使我们只是对行星的运动感兴趣!这是因为随着太阳的运动,它与行星的距离将会改变,这将改变行星上的力。到目前为止,我们所做的是模拟单体运动。为了解决这个问题,我们需要模拟两体运动。这并没有多难,我们稍后会讲到。在下一章中,我们甚至会研究大量粒子在相互引力作用下的运动。

接下来,用不同的行星初速度做实验。例如,将速度向量更改为(70,–40)会产生一个更圆的轨道,类似于地球的轨道。(85,-40)的速度给出了一个像彗星一样的高度拉长的轨道。在这本书的后面,我们会告诉你如何计算出产生圆形轨道所需的精确速度。

如果行星的速度很小,它会被吸进太阳。例如,将速度更改为(0,0)。行星朝着太阳的中心加速,就像它应该做的那样,但是当它实际到达那里时,奇怪的事情发生了。例如,它可能会反弹回来或高速飞走。这是一种不符合物理的行为,它的发生是因为我们没有考虑代码中对象的有限大小。因为牛顿万有引力定律与 1/r 2 成正比,所以距离 r 越小,力越大。例如,当 r 为 10 个像素时,1/r 2 的值为 0.01;当 r 为 1 时,1/r 2 为 1;而当 r 为 0.1 时,1/r 2 为 100。所以当 r 接近零时(当两个粒子的中心几乎重合时),引力变得无限大。与此同时,向量 r,即从太阳指向行星的位置向量,变得如此之小,以至于数值误差使其方向不可预测。因此,行星在太阳中心受到某个明显随机方向的大加速度。

这是一个普遍的问题,你会遇到像牛顿万有引力定律这样的力定律。您处理它的方式取决于您想要建模的确切内容。如果有东西落入太阳,你很可能想让它消失——这很容易编写代码,你不需要任何特殊的物理知识。如果有东西与像地球这样的固体行星相撞,它将在地球的大气层中燃烧起来,形成一个陨石坑,或者粉碎地球,这取决于它的大小。尽管你可以想象出一些电影风格的特效,但是要精确地建模这些都是相当困难的。如果有东西落入像木星或土星这样的气态巨行星,你可能想要模拟行星内部重力的变化。在本章的后面,我们将会看到一个例子来说明你如何做到这一点。

逃逸速度

事实证明,如果行星的速度大小小于某个临界值,它将总是被太阳的引力“捕获”,要么绕太阳运行,要么以与太阳碰撞而告终。超过这个临界值,行星就会脱离太阳的引力,这个临界值被称为逃逸速度。逃逸速度的公式由下式给出,其中 M 是太阳的质量,r 是距其中心的距离:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

所以逃逸速度是离吸引体中心距离的函数。

请注意,我们所说的围绕太阳运行的行星更普遍地适用于任何在重力作用下围绕另一个物体运行的物体——例如,围绕行星运行的卫星(天然的或人造的)。因此,使用上述公式,地球表面任何物体的逃逸速度由下式给出,其中 M e 和 r e 分别是地球的质量和半径:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

将 G、M e 和 r e 的值代入上述公式,得到 v = 11.2 km/s。这是地球表面的逃逸速度:如果你能以至少等于这个速度的速度扔出一个抛射体,它将摆脱地球的引力。相比之下,从太阳表面逃逸的速度为 618 公里/秒。相比之下,从 Eros(一颗尺寸约为 34 公里× 11 公里× 11 公里的近地小行星)表面逃逸的速度仅为 10 米/秒。你可以开车离开小行星!

回到Orbits模拟,G = 1,太阳的质量是 1,000,000,行星最初距离太阳的距离是外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传个像素。所以行星初始位置的逃逸速度是每秒外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传像素。你可以通过模拟来验证这一点。首先把行星的初速度改成(80,0),这样它的星等就是 80。这颗行星将绕太阳运行。现在,通过将速度更改为(100,0),将初始速度值增加到 100。这颗行星将离太阳更远,但仍将围绕太阳运行。即使你将速度改为(105,0)并等待足够长的时间(大约五分钟左右),行星也会从画布的边缘消失,但最终会回来完成一个轨道。但是如果你把速度增加到 109.2 以上,它就再也不会回来了。我们不建议你等待!

两体运动

到目前为止,我们已经让一个物体在另一个物体的引力下运动。但是我们知道重力是一个作用力-反作用力对,所以另一个物体也一定会受到一个大小相等方向相反的力。结果是另一个物体也会移动。修改orbits.js代码来实现这一点相当简单。结果代码在文件two-masses.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 ball1;

var ball2;

var r1 = 10;

var r2 = 40;

var m1 = 1;

var m2 = 60;

var G = 100000;

var t0, dt;

window.onload = init;

function init() {

var ball1Init = new Ball(r1,'#9999ff',m1,0,true);

ball1Init.pos2D = new Vector2D(150,200);

ball1Init.draw(context_bg);

var ball2Init = new Ball(r2,'#ff9999',m2,0,true);

ball2Init.pos2D = new Vector2D(350,200);

ball2Init.draw(context_bg);

ball1 = new Ball(r1,'#0000ff',m1,0,true);

ball1.pos2D = ball1Init.pos2D;

ball1.velo2D = new Vector2D(0,150);

ball1.draw(context);

ball2 = new Ball(r2,'#ff0000',m2,0,true);

ball2.pos2D = ball2Init.pos2D;

ball2.velo2D = new Vector2D(0,0);

ball2.draw(context);

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.1) {dt=0;};

move();

}

function move(){

context.clearRect(0, 0, canvas.width, canvas.height);

moveObject(ball1);

moveObject(ball2);

calcForce(ball1,ball2); // calc force on ball1 due to ball2

update(ball1);

calcForce(ball2,ball1); // calc force on ball2 due to ball1

update(ball2);

}

function update(obj){

updateAccel(obj.mass);

updateVelo(obj);

}

function moveObject(obj){

obj.pos2D = obj.pos2D.addScaled(obj.velo2D,dt);

obj.draw(context);

}

function calcForce(obj1,obj2){

force = Forces.gravity(G,obj1.mass,obj2.mass,obj1.pos2D.subtract(obj2.pos2D));

}

function updateAccel(m){

acc = force.multiply(1/m);

}

function updateVelo(obj){

obj.velo2D = obj.velo2D.addScaled(acc,dt);

}

我们在这里所做的基本上很简单。我们创建了几个Ball实例,ball1ball2,然后让每个球在另一个球施加的重力下移动。根据之前指定的数值,ball2的半径比ball1大 4 倍,质量大 60 倍。另外,ball1的初始速度为(0,150),而ball2最初是静止的。我们还创建了另外两个Ball实例ball1Initball2Init,以较浅的颜色表示它们各自对应部分的初始位置。

比较two-masses.jsorbits.js,你会发现我们已经重组了一些动画循环和物理代码。首先,calcForce()方法现在有两个参数:第一个参数是受到力的球;第二个参数是施加力的球。updateAccel()方法现在还需要一个参数,即正在计算加速度的物体的质量。为了方便起见,我们还引入了一个新功能update(),它将对updateAccel()updateVelo()的调用组合在一起。move()方法为每个球连续调用moveObject()calcForce()update()。仔细注意这些函数调用的顺序——顺序很重要!我们把它作为一个练习,让您找出原因,并试验如果您改变函数调用的顺序会发生什么。

我们在这里做的另一件事是将 G 的值改为 100,000。如果通过使用适当的质量值进行补偿来创建所需的效果,则可以给 G 任何想要的值。

如果你运行代码,你会看到ball1围绕ball2旋转,而后者似乎在摆动。这是有道理的。ball1ball2在任何给定时间都经历相同的力,但是因为ball2ball1大 60 倍,所以它的加速度小 60 倍(应用 a = F/m)。所以ball2ball1移动的少很多,如图 6-4 截图所示。请注意,这两个球经历了垂直向下的净缓慢漂移。这是由于动量守恒(见第五章):总的初始动量不为零(ball1最初垂直向下移动,而ball2最初静止),因此这种垂直向下的动量必须一直守恒,即使ball2移动。通过赋予ball2一个初速度,使系统的总初始动量为零,即 m1 × u1 + m2 × u2 = 0,可以很容易地消除这种漂移。因此,ball2的初始垂直速度为 U2 =–m1×u1/m2 =–1×150/60 =–2.5 px/s。在ball2创建后,通过添加以下代码行来实现:

ball2.velo2D = new Vector2D(0,-2.5);

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-4。

Two-body motion (lighter images show initial positions of respective balls)

如果您现在重新运行代码,您会发现残余漂移已经消失,并且由于ball1施加的重力,ball2只经历了轻微的抖动。

现在,通过修改相应的线,使ball2的质量和大小与ball1相同,如下所示:

ball2 = new Ball(r1,'#ff0000',m1,0,true);

然后,通过对代码进行以下更改来更改两个球的初始速度:

ball1.velo2D = new Vector2D(0,10);

ball2.velo2D = new Vector2D(0,-10);

如果你现在运行这段代码,你会发现这两个球以一种奇怪的方式绕着对方旋转,交替地靠近然后远离。实际情况是,它们围绕着一个共同的中心旋转,这个中心就是它们的质量中心。事实上,当球的质量不同时,这种情况也会发生。但是那时质量的中心在大球里面,所以它看起来在摇晃。

局部重力

我们最熟悉的是重力特写。本地的近地重力与你目前所学的有什么关系?在本节中,我们将建立牛顿万有引力定律和我们之前用于引力的公式 F = mg 之间的联系。这将引导我们阐明重力加速度 g 的意义。然后,我们将讨论 g 如何随着与地球中心的距离而变化,以及它在其他天体上如何不同。

地球表面附近的重力

是时候回到地球了!本章开始时(以及前面几章)我们说过,地球附近重力对质量为 m 的物体的作用力由 F = mg 给出,g 约为 9.81 m/s 2 。然后,到目前为止这一章的大部分时间,我们一直在用牛顿万有引力定律,它说引力由 F = Gm1m2/r2给出。我们如何调和这两个不同的公式?

答案是他们都是正确的。如果 M 是地球(或我们正在考虑的任何其他行星或天体)的质量,M 是地球上或附近物体的质量,则第二个公式给出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这必须给出与 F = mg 相同的力。因此,这一定是真的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在我们可以用这个等式的两边除以 m 来去掉它。我们只剩下这个:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这个公式表明,g 与地球的质量 M 和距其中心的距离 r 有关。它告诉我们,g 其实是 GM/r 2 的“昵称”。

如果一个物体坐在地球表面,它离地心的距离等于地球的半径。地球的质量为 5.974 × 10 24 千克,其半径为 6.375 × 10 6

因此,使用最后一个公式,地球表面的 g 值由下式给出,这是之前引用的精确值:

g = 6.67×10-11×5.974×1024/(6.374×106)2= 9.81 米/秒 2

所以 F = mg 符合牛顿万有引力定律。

因为它总是指向地球的中心,所以无论我们在地球表面上还是附近,重力都垂直向下作用(见图 6-5 )。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-5。

Near-Earth gravity

重力随高度的变化

公式 g = GM/r 2 告诉我们,我们可以用 F = mg 来计算离地球任意距离的重力,前提是我们要考虑 g 随离地心距离的变化。

如果一个物体在地球表面上方的高度 h 处,它到地球中心的距离由下式给出,其中 R 是地球的半径:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

因此,地球表面上方高度 h 处的 g 值由下式给出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

因为 g 在地球表面的值是 9.81 m/s 2 ,稍微用点代数就给出了两个等价的公式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

利用这个公式,可以得出在地球表面 350 公里的国际空间站高度上,g 的值约为 8.8 米/秒 2 。在月球距离(384000 公里)的 g 值约为 0.0026 米/秒 2 。记住 g 给你一个物体在重力作用下的加速度,这个加速度和物体的质量无关。所以,如果把月亮换成一个足球,它会经历同样的加速度。

你可能很好奇 g 在地球内部是如何变化的。一个简单的模型表明,从地球表面到地心,g 近似线性地减小到零。公式如下,其中 R 是地球的半径:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-6 显示了 g 随距离地心的归一化距离 r/R 的变化。可以看到,g 从地心到地表线性增加(其中 r/R = 1,g = 9.81)。然后,它会随着与地心距离的平方成反比而迅速减小。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-6。

Variation of g with distance from the center of the Earth

其他天体上的引力

公式 g = GM/r 2 可以让你计算出行星、恒星等其他天体上的 g 值,前提是你知道它们的质量和半径。

表 6-1 给出了几个天体表面 g 的近似值。这些数值表明,太阳的引力大约是地球的 28 倍,而月球的引力大约是地球的六分之一。太阳系最大的行星木星表面的 g 值大约是地球的 2.5 倍。谷神星和爱神星都是小行星,引力极其微弱:地球引力大约是谷神星的 36 倍,是爱神星的 1660 多倍。

表 6-1。

Gravity on Selected Celestial Bodies

| 天体 | 以米/秒为单位的 g 值 2 | | --- | --- | | 地球 | Nine point eight one | | 太阳 | Two hundred and seventy-four | | 月球 | one point six | | 木星 | Twenty-five | | 谷神星 | Zero point two seven | | 厄洛斯 | 0.0059 |

火箭

火箭被用于各种目的,包括发射宇宙飞船和导弹或作为烟火。在所有类型的火箭中,火箭是由火箭内燃烧的推进剂产生的废气推动的。在接下来的内容中,我们给出了一个非常简单的关于火箭运动的物理描述,并向你展示了如何将一个简单的火箭模拟整合到你的游戏中。

这是火箭科学!

不涉及各种类型火箭发动机运行的工程细节,我们只说高压热气产生并以高速通过喷嘴。根据牛顿第三定律,废气反推火箭,推动它前进。产生的向前的力叫做推力。

除了推力,火箭上可能还有其他力,这取决于它的位置和运动方式。重力就是其中之一。如果火箭穿过地球的大气层,它也会受到一个减速阻力和一个升力(见下一章)。这里我们将忽略阻力和升力的影响。

在地球上,由于压力随高度的变化也会产生影响,这将影响推力、阻力和升力。我们也会忽略这些影响。

当火箭上升时,它所受到的重力强度会像本章前面所描述的那样减小。我们将考虑这种影响。

火箭通常携带相当大一部分质量作为燃料。因此,随着燃料的消耗,火箭的质量会有很大的变化。这种质量的减少将影响火箭的加速度,因此必须包括在建模中。

模拟火箭的推力

为了模拟火箭的推力,我们利用了第五章中给出的牛顿第二定律的另一种形式。

记住,以速度 v 以 dm/dt 的速率移动物质所需的力由公式 F = v dm/dt 给出。火箭排出的废气以有效排气速度 v e 和可控制的质量速率 dm/dt 排出。因此,气体上的力由下式给出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

根据牛顿第三定律,火箭的推力由下式给出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

前面矢量方程中的负号表示产生的推力与排气速度方向相反。

火箭上也可能有侧向推进器,根据需要提供侧向推力。这些工作原理与主机相同。

构建火箭模拟

好吧,让我们建立一个火箭模拟!但是在你兴高采烈地跳上跳下之前,请注意我们这里只关注物理;我们把它作为一个练习,让你想出漂亮的图形和特殊效果。

我们将再次从orbits.js文件开始,进行一些修改,并将新文件保存为rocket-test.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 rocket;

var massPlanet;

var centerPlanet;

var radiusPlanetSquared;

var G = 0.1;

var dmdt = 0.5;

var dmdtSide= 0.1;

var fuelMass = 3.5;

var fuelSideMass = 3.5;

var fuelUsed = 0;

var fuelSideUsed = 0;

var ve = new Vector2D(0,200);

var veSide = new Vector2D(-100,0);

var applySideThrust = false;

var showExhaust = true;

var orientation = 1;

var animId;

var t0, dt;

window.onload = init;

function init() {

// create 100 stars randomly positioned

for (var i=0; i<100; i++){

var star = new Ball(1,'#ffff00');

star.pos2D= new Vector2D(Math.random()*canvas_bg.width,Math.random()*canvas_bg.height);

star.draw(context_bg);

}

// create a stationary planet planet

planet = new Ball(100,'#0033ff',1000000);

planet.pos2D = new Vector2D(400,400);

planet.draw(context_bg);

massPlanet = planet.mass;

centerPlanet = planet.pos2D;

radiusPlanetSquared = planet.radius*planet.radius;

// create a rocket

rocket = new Rocket(12,12,'#cccccc',10);

rocket.pos2D = new Vector2D(400,300);

rocket.draw(context,showExhaust);

// set up event listeners

window.addEventListener('keydown',startSideThrust,false);

window.addEventListener('keyup',stopSideThrust,false);

// launch the rocket

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.1) {dt=0;};

move();

}

function move(){

moveObject();

calcForce();

updateAccel();

updateVelo();

updateMass();

monitor();

}

function moveObject(){

rocket.pos2D = rocket.pos2D.addScaled(rocket.velo2D,dt);

context.clearRect(0, 0, canvas.width, canvas.height);

rocket.draw(context,showExhaust);

}

function calcForce(){

var gravity = Forces.gravity(G,massPlanet,rocket.mass,rocket.pos2D.subtract(centerPlanet));

var thrust = new Vector2D(0,0);

var thrustSide = new Vector2D(0,0);

if (fuelUsed < fuelMass){

thrust = ve.multiply(-dmdt);

}

if (fuelSideUsed < fuelSideMass && applySideThrust){

thrustSide = veSide.multiply(-dmdtSide*orientation);

}

force = Forces.add([gravity, thrust, thrustSide]);

}

function updateAccel(){

acc = force.multiply(1/rocket.mass);

}

function updateVelo(){

rocket.velo2D = rocket.velo2D.addScaled(acc,dt);

}

function updateMass(){

if (fuelUsed < fuelMass){

fuelUsed += dmdt*dt;

rocket.mass += -dmdt*dt;

}

if (fuelSideUsed < fuelSideMass && applySideThrust){

fuelSideUsed += dmdtSide*dt;

rocket.mass += -dmdtSide*dt;

}

}

function monitor(){

if (showExhaust && fuelUsed >= fuelMass){

showExhaust = false;

}

if (rocket.pos2D.subtract(centerPlanet).lengthSquared() < radiusPlanetSquared){

stop();

}

}

function startSideThrust(evt){

if (evt.keyCode==39){ // right arrow

applySideThrust = true;

orientation = 1;

}

if (evt.keyCode==37){ // left arrow

applySideThrust = true;

orientation = -1;

}

}

function stopSideThrust(evt){

applySideThrust = false;

}

function stop(){

cancelAnimationFrame(animId);

}

init()中的初始设置与orbits.js中的非常相似,除了我们现在用一个静止的行星代替了太阳,用一个火箭代替了行星。请注意,火箭是Rocket对象的一个实例,稍后我们将会看到。我们在init()中引入了几个事件监听器。这些分别监听keydownkeyup事件。相应的事件处理器startSideThrust()stopSideThrust()设置Boolean参数applySideThrust的值。每当按下右箭头键或左箭头键时,applySideThrust Boolean变量被设置为true;释放按键时,applySideThrust恢复为false。这个变量将控制侧向推进器施加的推力。变量orientation被赋予一个+1 或–1 的整数值,以区分左右按键。这个变量决定了施加推力的方向。

calcForce()中,我们指定作用在火箭上的力,然后像往常一样把它们加起来。第一个力是行星对它施加的重力。然后是主推力和侧推力。只要有燃料,就施加主推力;燃料量在变量fuelMass中设定。如果有燃料并且applySideThrust为真,则施加侧向推力。在任一情况下,推力都是用公式 F =–veDM/dt 计算的。主推进器和侧推进器的燃料量、排气速度和质量率是独立设置的。(实际上,可以改变排气速度和质量率来控制推力,但为了简单起见,我们在这里把它们固定下来。)

move()方法中有几个新的函数调用。首先,它调用一个新方法updateMass()。它需要这样做,因为火箭的质量在变化。updateMass()中的代码检查主推进器和侧推进器中是否还有燃料剩余;如果是这样的话,它会增加所用燃料的质量,并以相同的量减少火箭的质量。对于侧推进器,它还检查applySideThrust是否为真。

move()中调用的第二个新函数是monitor()。该方法执行两项检查。首先,它检查主油箱中的所有燃油是否已经用完,如果是,它将把showExhaust布尔变量的值设置为false,这样在下一次绘图更新时,排气将不再显示。不管有没有排气,火箭的绘制都是通过moveObject()中的函数调用rocket.draw(context,showExhaust)来完成的。

第二件事monitor()做的是检查火箭是否与行星相撞;如果是这样,它简单地调用stop()方法,停止时间步进,冻结所有运动。

如果你运行模拟,你会看到火箭自动发射。一开始它移动得很慢;然后它的速度和加速度都增加,直到它的燃料耗尽。然后它在行星的引力作用下减速。如果你什么都不做,它就会落回星球。如果您按下右箭头键或左箭头键,将分别向右侧或左侧施加侧向推力。如果你施加过多或过少的侧向推力,火箭要么会脱离行星的引力,要么会被拉回与行星碰撞的轨道。看看能不能让火箭绕行星运行(图 6-7 )!这包括在正确的时间施加正确的侧向推力。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 6-7。

A rocket orbiting a planet!

火箭物体

最后,让我们快速看一下创建可见火箭的Rocket对象。令人尴尬的是,我们的“火箭”只是一个三角形,另一个(透明的)三角形附在末端,代表废气中的气体。但是,如果您此时迫切需要运用自己的艺术技巧,请继续创作一个更好看的版本。

以下是rocket.js中的完整源代码:

function Rocket(width,height,color,mass){

if(typeof(width)==='undefined') width = 20;

if(typeof(height)==='undefined') height = 40;

if(typeof(color)==='undefined') color = '#0000ff';

if(typeof(mass)==='undefined') mass = 1;

this.width = width;

this.height = height;

this.color = color;

this.mass = mass;

this.x = 0;

this.y = 0;

this.vx = 0;

this.vy = 0;

}

Rocket.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,firing) {  // firing is a Boolean

if (firing){

var exhaust=new Triangle(this.x,this.y+0.5*this.height,this.width,this.height,'#ffff00',0.8);

exhaust.draw(context);

}

var capsule = new Triangle(this.x,this.y,this.width,this.height,this.color);

capsule.draw(context);

}

};

使用宽度、高度和颜色的参数值绘制火箭,这些参数值在构造函数中作为参数提供。代码看起来非常类似于Ball对象的代码,但是使用了不同的draw()方法,该方法有两个参数:contextfiringBoolean参数firing决定是否绘制“排气”。火箭体和排气都用一个方便的Triangle物体绘制成三角形,你可以在我们的生长物体库中找到。可以随意看一下triangle.js中的代码。

很容易看出如何将这个简单的模拟开发成一个有趣的游戏。除了改进的图形和特殊效果,你可以添加额外的功能和控制,以获得更大的真实感。考虑到这一点,我们需要指出,在我们实现火箭的一些操作功能的方式上有一些限制。对于一个简单的在线游戏来说,这些限制可能没什么大不了的,但是如果你试图构建一个真实的模拟器,这些限制就很重要了。无论如何,了解他们是值得的。

我们已经提到,在这次演示中,我们已经固定了火箭的推力,尽管实际上它可以通过改变排气速度和喷射气体的质量率来控制。更重要的是,主推力和侧推力的方向已经以一种严格来说不正确的方式实现了。无论火箭的角度是多少,火箭的主推力都是沿着它的轴线作用的,而它的侧推力会相对于火箭的角度处于某个特定的角度。我们在这个简化的模拟中所做的是固定主推力和侧推力,使其在垂直(y)和水平(x)方向上起作用,而不管火箭的倾斜度如何。我们还忽略了火箭在大气中运动时气压、阻力和升力的影响。

我们做的另一个明显的简化是将整个火箭送入轨道。实际上,太空火箭包括一个或多个一次性燃料箱,当燃料耗尽时,这些燃料箱就会被丢弃。我们把这个留给你做练习。类也可以在功能上得到增强。例如,将燃料量、使用的燃料、排气速度和质量率作为属性包含在Rocket中,而不是将它们硬编码到rocket-test.js中,这可能是有意义的。

最后,我们在本章中编写的例子是真实的,因为它们包含正确的物理方程,但是为各种参数选择的值不一定是成比例的。例如,在轨道模拟中,行星和太阳的大小与它们的距离相比被严重夸大了。为了在计算机屏幕上看到物体及其运动,这是必要的。缩放问题将在本书的第四部分详细讨论。

摘要

这一章向你展示了重力可以展现的丰富性和多样性,以及它可以产生的不同类型的运动。在相当短的一章中,你已经能够编写轨道,两个物体在相互引力下以有趣的方式运动,以及一个从发射到轨道的火箭模拟。

令人惊讶的是,仅仅几个物理公式和一点点代码就能带来如此多的乐趣。你现在尝到了什么是可以做到的,即使是一个单一的力量,如重力。我们将在后面的章节中更多地讨论重力。同时,我们将在下一章介绍许多其他的力。

七、接触力和流体力

在前一章中,我们详细研究了重力,这是一种远距离作用的力。然而,除了重力之外,我们在日常生活中遇到的大多数力都来自于与其他物体的直接接触,无论是固体还是液体。因为在这个星球上,我们通常与固体地板接触,并被流体(空气或水)包围,所以知道它们对物体施加什么力肯定是一件好事。本章将着眼于这些力量。

本章涵盖的主题包括以下内容:

  • 接触力:接触力是两个固体直接接触时相互施加的力。例子包括法向力、摩擦力和张力。
  • 压力:流体施加压力。由于这种流体压力,流体中的任何物体都会受到力的作用。
  • 上推力或浮力:上推力,也称为浮力,是部分或全部浸没在流体中的物体所承受的合成压力。
  • 阻力:物体在流体中运动时会受到一种阻碍其运动的阻力。这就是阻力。
  • 升力:在流体中移动的物体可能会受到另一个垂直于运动方向的力,称为升力。升力是飞机飞行的动力。
  • 风:虽然风本身不是一种力,但它确实会对它吹过的任何物体施加一种力。风还会产生湍流,这会导致物体运动不稳定。

如该列表所示,这些力的变化包括固体物体之间的相互作用以及固体物体和流体之间的相互作用,这两种情况都是直接物理接触的结果。但从技术上讲,接触力这个术语是专指存在于两个固体之间的力。只有第一节将从技术的角度讨论接触力。本章的大部分将集中在流体对固体物体施加的力上。这部分是因为后者更复杂,但它们也表现出更丰富的多样性,具有有趣的功能和效果。在研究流体力时,我们将把注意力限制在固体在流体中所受的力上。我们不考虑作用在流体上的力以及这些力对流体本身的影响。流体运动是一个更复杂的问题,不在本书中讨论。

这里只是一些你在阅读本章后能够创建的模拟:摩擦运动、漂浮物体、气泡、气球、降落伞、升力、风和湍流效果。你也将知道建造潜艇模拟所需的大部分物理知识(见第十六章)。

接触力

简单地说,接触力是一个固体物体与另一个固体物体接触时受到的力。推和拉涉及接触力。跑步或者只是站着也是如此。碰撞涉及持续时间很短的大接触力。

法向接触力

当两个固体物体接触时,每个物体都会对另一个物体施加一个法向力,阻止它们融合在一起。法向力的大小取决于将两个物体推到一起的力。

Note

在我们做其他事情之前,我们必须解释一下,这里的正常这个词是指这个词的数学意义,它是垂直的,而不是不正常的对立面。

正常接触力的一个例子是地板对你的反作用力。事实上,这个力就是我们意识到自己体重的原因。如果没有地板的正常反作用力(因此没有正常的力),我们会从地板上摔下来。

现在,每当我们在这本书里引入一个新的力,我们都会给你一个计算它的方法。你就可以这么模拟了。如果你不能计算一个力,谈论它就没有意义了!

法向力怎么算?这要看情况。不像重力,它没有通用的公式来告诉你法向力是什么。通常,你可以从作用在物体上的其他力推导出来。

这里有一个例子。假设有一本书放在一张水平的桌子上(见图 7-1 )。作用在书上的力主要有两个:地球施加的垂直向下作用的重力 W = mg,桌子施加在书上垂直向上作用的法向接触力 N(法向接触力通常用 N 或 R 表示)。因为书不动,所以对它的合力一定为零(牛顿第一运动定律)。因此,我们可以推导出,这种情况下的法向力 N 在量级上等于书的重量 W。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-1。

The normal contact force acting on a book resting on a table

例如,如果桌子倾斜,法向力就会改变。你会在本章的第一个例子(“滑下斜坡”)中看到如何计算这种情况下的法向力。

拉伸和压缩

弦的张力是另一种接触力。如果你用一根绳子系住一个物体,然后握住绳子的另一端将物体悬挂起来(见图 7-2 ),绳子会对物体施加拉力,防止其下落。绳子只有绷紧时才能做到这一点。根据牛顿第三运动定律,物体对绳子施加一个大小相等方向相反的力。绳子上的力叫做张力。非正式地,我们也把绳子施加在物体上的力称为张力。张力对细绳本身的作用是将它稍稍拉长。如果物体停止运动,那么张力一定等于物体的重量。如果物体很重,张力会把绳子拉至断裂点。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-2。

Tension in a string

弹簧也会受到张力。弹簧的设计使得相对较小的张力可以产生较大的伸长。因为弹簧在拉伸时会恢复正常长度,所以它会拉回拉伸它的物体。张力的反义词是压缩。弹簧也可以被压缩;然后,它们对压缩它们的物体施加推力。

弹簧既有趣又非常有用,可以产生各种各样的效果。事实上,下一章专门讨论弹簧和类似弹簧的运动。

摩擦

摩擦力是阻止两个相接触的物体相对运动或阻止一个物体在流体中运动的力。例如,如果你沿着桌子推一本书,它会慢下来并停下来。那是因为桌面对它施加的摩擦力。两个固体之间的摩擦也称为干摩擦。空气或水等流体也会产生摩擦力,称为粘性流体阻力。

我们将在本章后面讨论流体阻力。在这一节,我们将看看两个固体之间的干摩擦,以及我们如何对它建模。

如前所述,摩擦力是阻止两个相互接触的物体相对运动的力。这意味着作用在物体上的摩擦力将与其运动方向相反。但是一个物体不一定要移动才能经历摩擦。如果一个物体受到一个使它向另一个物体移动的力,它也会受到摩擦,即使它实际上并没有移动。事实上,是摩擦力阻止了它的运动。所以实际上有两种类型的摩擦力:静止物体所受的摩擦力,和运动物体所受的摩擦力。我们分别称之为静摩擦力和动摩擦力。

模拟静摩擦和动摩擦

我们用摩擦系数的概念来模拟这两种类型的摩擦。让我们从动摩擦开始,因为它简单一点。

普通的经验告诉我们,如果我们将两个表面压在一起,同时试图使一个表面滑过另一个表面,摩擦会更大。那是因为两个物体如果压在一起会更“粘在一起”。同样清楚的是,摩擦力的大小取决于构成物体的材料。例如,橡胶会比玻璃产生更大的摩擦力。因此,摩擦力 F 的大小由下式给出就不足为奇了,其中 N 是两个物体之间的法向力,C k 是一个取决于两个表面的数,称为动摩擦系数:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

根据牛顿第三定律,大小相同的力作用在两个物体上;对于每个物体,力的作用方向与它相对于另一个物体的运动方向相反。动摩擦有时也被称为动态或滑动摩擦。

静态摩擦有点不同。对于给定的一对表面和法向接触力,动摩擦力只有一个值,而静摩擦力可以取最大值以下的任何值。最大值由下式给出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这在形式上类似于动摩擦公式,但有一个不同的系数,称为静摩擦系数。对于相同的两个表面,其值通常大于 C k 的相应值。因此,最大静摩擦力大于动摩擦力。

为了理解静摩擦和动摩擦,考虑下面的思维实验。假设你正在推动一个物体接触一个表面。如果你施加的力的大小小于前面公式给出的 F max ,摩擦力将等于作用力,因此合力为零,物体不会移动。如果你现在更用力,使施加的力大于 F max 的值,摩擦力将等于 F max (因为它不能超过那个最大值)。所以摩擦力不能完全平衡施加的力,物体会在合力的作用下开始加速。一旦它开始运动,摩擦力就会减小到与动摩擦力相等的值。因此,合力会突然增加,物体会加速。如果你曾经试图移动一件沉重的家具,你会很容易理解这个事实:一旦它已经在移动了,就更容易推动了。那是因为动摩擦力小于静摩擦力的最大值。

摩擦系数

表 7-1 中给出了一些摩擦系数的例子。关键是数值一般都小于 1。摩擦系数大于 1 是非常罕见的,因为这意味着摩擦力大于将两个物体固定在一起的正常接触力。

表 7-1。

Static and Kinetic Coefficients of Friction

| 材料 | C s | C k | | --- | --- | --- | | 木头上的木头 | 0.25–0.5 | Zero point two | | 钢对钢 | Zero point seven four | Zero point five seven | | 混凝土上的橡胶 | One | Zero point eight | | 玻璃对玻璃 | Zero point nine four | Zero point four | | 冰上的冰 | Zero point one | Zero point zero three |

请注意,尽管这些值代表了所引用的材料,但实际的摩擦系数可能会因实际物体表面的性质而有所不同。对于游戏编程来说,这不是一个大问题。

不可能给出一个完整的摩擦系数列表;但是如果你需要的东西没有列在这里,在网上应该很容易找到。

示例:滑下斜坡

举例子的时间到了!现在,您可以应用所学的法向接触力和摩擦力来模拟一个物体沿斜面下滑。首先,让我们将物理概念应用到这个特殊的例子中。

物理学

本次模拟的力图如图 7-3 所示。如图所示,有三个力作用在物体上:重力 m g,法向接触力 N,摩擦力 f,由于物体沿表面直线运动,合力沿表面作用。没有垂直于表面的合力。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-3。

Force diagram for an object sliding down an inclined plane

因此,分解沿表面的力,得到合力 F 的大小如下,其中θ是倾斜平面的角度:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

垂直于表面的分解力的合力必须为零,这样法向接触力 N 就被垂直于表面的重力分量所平衡。注意摩擦力 f 沿着表面作用,所以它垂直于表面的分量为零。因此,

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当物体运动时,摩擦力 f 的大小由下式给出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果它不动,沿表面的合力 F 必然为零,所以 F 必须平衡重力沿表面的分量。根据上式,设置 F = 0 得出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

达到以下最大值时为真:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果重力分量的值(mg sin (θ))超过这个临界值(C s N),f 将具有后者的值,至少暂时是这样。一旦物体开始移动,f 的值将是 C k N

现在你知道了物理,有了相关的公式,就可以开始编码了。这将是一个两步的过程,与你在上一章中所做的非常相似。首先,您将创建可视化设置;然后,您将编写驱动模拟的代码。

创建视觉设置

代码在一个名为sliding.js的文件中。在声明和初始化画布、上下文和其他变量之后,我们在init()函数中处理可视化设置:

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 m = 1;          // mass of ball

var g = 10;         // acceleration due to gravity

var ck = 0.2;       // coeff of kinetic friction

var cs = 0.25;      // coeff of static friction

var vtol = 0.000001 // tolerance

// coordinates of end-points of inclined plane

var xtop = 50; var ytop = 150;

var xbot = 450; var ybot = 250;

var angle = Math.atan2(ybot-ytop,xbot-xtop); // angle of inclined plane

var t0,dt;

window.onload = init;

function init() {

// create a ball

ball = new Ball(20,'#0000ff',m,0,true);

ball.pos2D = new Vector2D(50,130);

ball.velo2D = new Vector2D(0,0);

ball.draw(context);

// create an inclined plane

context_bg.strokeStyle = '#333333';

context_bg.beginPath();

context_bg.moveTo(xtop,ytop);

context_bg.lineTo(xbot,ybot);

context_bg.closePath();

context_bg.stroke();

// make the ball move

t0 = new Date().getTime();

animFrame();

};

init()中,我们创建一个球和一条线(后者在不同的画布元素上,背景),并将球放在线上。使用端点和Math.atan2()函数计算线条的角度,并存储在angle变量中。如果你不确定这里发生了什么,再看一下第三章中的“反向触发函数”小节。最后,我们通过调用animFrame()函数让球沿着斜线向下移动。

编码动画

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(ball);

calcForce();

updateAccel();

updateVelo(ball);

}

function moveObject(obj){

obj.pos2D = obj.pos2D.addScaled(obj.velo2D,dt);

context.clearRect(0, 0, canvas.width, canvas.height);

obj.draw(context);

}

function updateAccel(){

acc = force.multiply(1/m);

}

function updateVelo(obj){

obj.velo2D = obj.velo2D.addScaled(acc,dt);

}

这几乎是全部的动画代码,你可以看到到目前为止什么都没有改变。唯一的变化是在calcForce()函数中,它包含了新的物理特性:

function calcForce(){

var gravity = Forces.constantGravity(m,g);

var normal = Vector2D.vector2D(m*g*Math.cos(angle),0.5*Math.PI-angle,false);

var coeff;

if (ball.velo2D.length() < vtol){  // static friction

coeff = Math.min(cs*normal.length(),m*g*Math.sin(angle));

}else{  // kinetic friction

coeff = ck*normal.length();

}

var friction = normal.perp(coeff);

force = Forces.add([gravity, normal, friction]);

}

calcForce()方法计算每个时间步的三个相关力gravitynormalfriction,然后使用Forces.add()方法将它们相加得到合力。法向力和摩擦力的计算需要解释。对于法向力,我们使用一种叫做vector2D()Vector2D新方法,定义如下:

Vector2D.vector2D = function(mag,angle,clockwise){

if (typeof(clockwise)==='undefined') clockwise = true;

var vec = new Vector2D(0,0);

vec.x = mag*Math.cos(angle);

vec.y = mag*Math.sin(angle);

if (!clockwise){

vec.y *= -1;

}

return vec;

}

该方法采用两个必需的Number参数magangle(以弧度表示),以及一个可选的布尔参数clockwise,并返回一个Vector2D对象,该对象是一个具有指定大小和角度的 2D 向量。它通过将(幅度、角度)表示转换为矢量的分量表示来实现这一点(参见第三章中的“解析矢量”一节)。clockwise参数告诉vector2D()角度是顺时针方向(默认)还是逆时针方向。

使用前面的公式和图 7-3 ,我们知道法向力的大小等于 mg cos (θ),其角度等于逆时针方向上的π/2–θ(即 90–θ,单位为弧度)。因此,法向力由下式给出:

var normal = Vector2D.vector2D(m*g*Math.cos(angle),0.5*Math.PI-angle,false);

摩擦力垂直于法向力,大小为coeff*N,其中coeff为摩擦系数,N为法向力的大小。因此它由下面给出,其中perp()是我们在Vector2D类中创建的另一个新方法(我们将很快描述它),当对象开始移动时coeff等于 CkN;并且等于 mg sin (θ),当物体不运动时,最大值为 C s N:

var friction = normal.perp(coeff);

这是通过以下方式实现的:

if (ball.velo2D.length() < vtol){  // static friction

coeff = Math.min(cs*normal.length(),m*g*Math.sin(angle));

}else{  // kinetic friction

coeff = ck*normal.length();

}

注意,我们利用Math.min()函数来实现这样一个事实,即对于静摩擦力,coeff等于 mg sin (θ)或 C s N,以较小者为准。

Vector2Dperp()方法是这样定义的:

Function perp(u,anticlockwise){

if (typeof(anticlockwise)==='undefined') anticlockwise = true;

var length = this.length();

var vec = new Vector2D(this.y, -this.x);

if (length > 0) {

if (anticlockwise){ // anticlockwise with respect to canvas coordinate system

vec.scaleBy(u/length);

}else{

vec.scaleBy(-u/length);

}

}else{

vec = new Vector2D(0,0);

}

return vec;

}

这个定义意味着,如果vec是一个Vector2D对象,k是一个数字,vec.perp(k)返回一个垂直于vec且长度为kVector2D对象,逆时针方向指向vec(在画布坐标系中)。参考图 7-4 ,图中显示了该设置的截图,normal.perp(coeff)因此给出了一个指向斜坡上方的矢量coeff,正如摩擦力应该是。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-4。

An object sliding down a slope

当检查对象是否没有移动时,我们实际上并没有检查它的速度是否为零,而是检查它是否小于某个小值vtol(在代码中设置为 0.000001)。g的值设置为 10,假设滑动物体和表面都是由木头制成的,我们选择ck = 0.2cs = 0.25

如果您使用给定的参数值运行代码,您将看到什么也没有发生。物体只是停留在那里。这怎么可能呢?为了理解这一点,再看一下图 7-3 。很明显,只有当重力沿斜面的分量超过最大静摩擦力时,物体才能滑动。换句话说,如果以下条件成立,它将滑动:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

因为 N = mg cos (θ),所以这相当于:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

将这个不等式的两边除以 mg cos (θ)得出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

所以只有当倾斜角的正切大于静摩擦系数时,物体才会滑动。

现在,您可以根据xtopytopxbotybot的值计算 tan (θ),如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这正好等于代码中规定的 C s 的值。因此,重力不足以克服摩擦力,物体不会滑动。如果你稍微增加斜面的角度,物体就会滑动。比如把ybot的值改成 260,这就得出 tan (θ) = 110/400 = 0.275,大于 C s 的值。试一试!

在这个例子中,物体沿着表面滑动而不滚动。滚动包括刚体的旋转运动(将在第十三章中讨论)。

本章的其余部分将讨论源于压力的力,更具体地说是流体压力。从技术上讲,压力与固体、液体和气体都有联系。但是这个概念对于流体特别有用,所以它更倾向于与液体和气体联系在一起。

压力的意义

那么什么是压力呢?它是施加在物体表面的单位面积上的法向(垂直)力。在不同情况下,垂直力的来源可以不同。固体和液体产生的压力通常是由于重力。气体中的压力是由气体分子的碰撞产生的。

关键问题是:我们如何计算压力,从而计算流体中物体所受的力?一旦我们做到了这一点,我们就可以尝试模拟物体在流体中的运动。

根据压力的定义,如果力 F 垂直作用于面积为 A 的表面,则表面上的平均压力 P 由下式给出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

因为压力是力除以面积,所以它的单位是 N/m 2 。这个单位有一个特殊的名字:帕斯卡,或 Pa。

如果 F 与法线成一个角度呢?然后你需要求解 F 来找到法线方向的分量,这个法线分量就是你在前面的公式中使用的分量。这给了我们一种方法,如果我们知道力,就可以计算压力,反之亦然。

压力并不局限于流体(尽管流体压力将是我们在这一章中主要关心的),用一个简单的固体例子来介绍计算压力的方法实际上更容易些。

假设一个长 l、宽 w、高 h、质量 m 的长方体(盒子)坐在一张桌子上(见图 7-5 )。它对桌子施加的压力是多少?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-5。

A box sitting on a table

它施加在桌子上的力等于它的重量,这个力垂直于桌面,所以我们有 F = mg。面积 A = lw。因此,压力 P 由下式给出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

密度

通过引入密度的概念,我们可以取得更多的进展。

密度被定义为每单位体积的质量。密度给了你一种比较不同物质相对“重量”的方法——你可以取同样体积的每种物质,然后比较每种物质的重量。

我们用希腊符号ρ(ρ)表示密度。所以,基于这个定义,我们可以写成:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

因为密度是质量除以体积,所以单位是 kg/m 3 。因为物体受热时会膨胀,尤其是液体和气体,所以流体的密度会随着温度(以及压力)而变化。在 20°C 和标准大气压下,水的密度约为 1000 kg/m 3 ,空气的密度约为 1.2 kg/m 3 。这意味着一个 1 米×1 米×1 米的盒子(体积为 1 米)可以容纳 1000 千克(1 吨)水和 1.2 千克空气。

操纵上面的公式给出如下结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

它也给出了这个:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们将在本章的其余部分频繁使用这些公式。

就像压力一样,密度的概念不仅适用于液体,也适用于固体。回到前面的长方体示例,我们可以用 m = ρV = ρlwh 来代替压力公式中的质量 m,因为长方体的体积是 V = lwh:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这是一个非常有用的公式。它给出了一个规则物体(在这种情况下,是一个长方体)在一个表面上施加的压力,这三个已知的量是:物体的密度、重力加速度和物体的高度。请注意,我们可以使用不同于盒子的其他形状,只要它具有规则的横截面 A。面积 A 将以相同的方式抵消,留下相同的公式:P = ρgh。

在下一节,我们会发现这个公式实际上更普遍,也适用于流体施加的压力,只要我们重新解释 h。

流体中压力随深度的变化

现在考虑一个与上一节中的固体形状相同的液体柱,而不是固体。

应用完全相同的数学公式,我们再次得出以下结果,其中 P 是液柱下方的压力,ρ是流体的密度,h 是液柱的高度:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在在精神上把自己传送到海洋。

在海面下的深度 h 处,有一个高度为 h 的水柱。因此,表面下深度 h 处的压力由相同的公式给出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

请注意,这是仅由水产生的压力。由于水面上方的空气,还有一个附加压力。这是大气压力,我们可以用 A 来表示,那么假设水的密度随深度不变,海洋中深度 h 处的总压力是 A + ρgh。这个公式表明,流体中的压力随着深度的增加而增加,这很有道理,因为上面有更多的流体在“下沉”

大气层也是如此。我们在地球表面感受到的气压是由于我们头上的空气柱。标准大气压约为 100 千帕或 100,000 帕。这有时简称为 1 大气压(atm)。

因为水的密度是 1000 kg/m 3 ,g 大约是 10 m/s 2 ,用公式 P = ρgh 告诉我们,10 m 的水将会施加大约和 1 个大气压相同的压力。因此,在 10 米的海洋深度,压力将是 2 个大气压,包括上面空气的压力。

静态和动态压力

到目前为止,当我们谈到流体中的压强时,我们指的是静压。公式 P = ρgh 适用于静压。静压定义在流动中的任何一点,它是各向同性的;也就是说,它在任何方向都有相同的值。它甚至存在于静态流体(静止的流体)中。

在流动的流体中,还有另一种压力,称为动压。动态压力是由于流体的流动。例如,从水龙头流出的水流会产生动压力。当水碰到水槽时,会对水槽施加一个力。类似地,由于与之相关的动态压力,风施加一个力。流体中任一点的动态压力公式如下,其中 v 是流体在该点的速度:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于在流体中以速度 v 运动的物体也是如此;重要的是物体和流体之间的相对速度。

运动流体中的总压力是静压和动压之和。

上升推力(浮力)

在了解了压力和密度的背景之后,我们现在准备介绍流体产生的力。所以我们先从上推力说起,也就是浮力。

这里有一个简单的实验让你在洗澡的时候做。试着在水下推一个空心球。你有什么感觉?你应该感觉到有一股力量试图把球向上推回去。这是向上推力。现在放开球。会发生什么?在沉淀下来之前,它会重新弹起并在水面振荡。你很快就会创造出这样的效果。

上推也使我们在游泳池或浴缸里感觉更轻。它向上运动,对抗重力。

什么导致了上升?上冲的物理起源是流体中不同高度存在的压力差。为了理解我们的意思,看一下图 7-6a ,图中显示了一个物体部分浸没在诸如水的液体中。物体顶面的压力等于大气压 A 并向下推动物体,而底面的压力等于 A + ρgh,其中ρ是流体的密度,h 是物体被淹没的深度。这个压力向上作用,所以有一个等于ρgh 的净向上压力。因此,因为力=压力×表面面积,所以压力差会产生一个向上的合力 U,由下式给出(其中 S 是物体顶面和底面的面积,为简单起见,此处假设相等):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这就是上推力或浮力。

注意,因为压力是各向同性的,所以它也作用于物体的侧面。但是在每一个高度上,相对两侧的压力大小相等,方向相反,所以它们的影响被抵消了。如果物体完全浸没在液体中(见图 7-6b ),包括空气中的物体,类似的结论也成立。这基本上是因为流体中的压力随着深度的增加而增加,所以水下物体底部的压力总是大于顶部的压力。所以流体会对物体产生一个向上的净压力。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-6。

An object immersed in a fluid experiences upthrust due to a pressure difference

阿基米德原理

让我们重温一下上一节中导出的最后一个公式。发现上冲的幅度 U 由 U = ρghS 给出。现在 h 是物体浸没部分的高度,S 是它的横截面积(为简单起见,我们假设它是常数)。因此,hS 等于物体浸没部分的体积 V。所以我们可以这样写:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

但是ρV = m,液体的质量被物体的水下部分所取代,所以我们现在可以这样写:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这只是排出液体的重量。因此,我们推导出阿基米德原理:

  • 阿基米德原理:浸没在液体中的物体所受的上推力等于它所排开的液体重量。

阿基米德是古希腊科学家、数学家和全才。根据传说,当他发现这个原理时,他从浴缸里跳出来大喊“找到了!”,意思是“找到了!”

视重量

上冲导致视重现象。图 7-7 说明了这个概念。任何部分或全部浸在液体中的物体都会受到向上的推力 U,这个力与重力 W 作用在物体上的向下的力相反。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-7。

Upthrust reduces the apparent weight of an object to W – U

因此,物体的表观重量由下式给出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

表观体重现象是我们在浴缸或游泳池中感觉更轻的原因。利用明显的重量效应,宇航员在一个巨大的水槽中进行训练,以模拟失重或零重力。

水下物体

完全浸没在液体中的物体显然会排出其自身体积的液体。

由于物体和排出的流体具有相同的体积 V,因此完全浸没的物体的表观重量可以写为:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

因此,它可以写成以下形式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这是一个非常有用的公式,它根据物体的密度和体积给出了浸没在液体中的物体的向下合力或表观重量。

从这个公式中,我们可以推导出:

  • 如果物体的密度大于流体的密度,则表观重量将为正(即向下),但小于实际重量。你可以在浴室或游泳池中体验这种较低的表观重量。
  • 如果物体的密度比流体的密度大得多,那么表观重量就几乎等于实际重量,因为与物体的重量相比,上推力可以忽略不计。例如,一块石头在空气中的上推力通常可以忽略不计。
  • 如果物体的密度正好等于液体的密度,那么表观重量将为零,因此物体将漂浮在液体中。
  • 如果物体的密度小于流体的密度,则表观重量为负(它向上作用),因此物体会上升。例如,水中的气泡与它们的重量相比有很大的上升力,因为空气的密度比水的密度低得多,所以它们上升很快。

漂浮物体

阿基米德原理的一种特殊形式适用于浮体。这就是所谓的漂浮定律。

  • 漂浮定律:一个漂浮物排出其自身重量的液体。

这是逻辑。浮体上没有净力作用;否则,它要么上升,要么下沉。因为作用在它身上的仅有的两个力是它的重量和向上的推力,它们必须平衡。因此,浮体上的上推力必须等于浮体的重量。根据阿基米德原理,这意味着排出液体的重量等于物体的重量。所以一个漂浮物体的表观重量是零——它“感觉没有重量”

如前所述,当物体浸没在密度等于物体自身密度的流体中时,也是这种情况。事实上,漂浮在液体表面的物体,如水上的船只,其“有效密度”与水的密度相同,尽管它可能由密度比水高的金属制成。这是因为船在水面以下的部分也包括封闭在船内部的空气,这降低了它的平均密度。

示例:气球

我们已经有了相当多的理论,所以让我们看一个例子。这个例子(如图 7-8 所示)模拟了一个简单的热气球。热气球的工作原理是加热空气。热空气的密度小于周围空气的密度,所以它上升。通过控制气球中空气的温度,你可以控制它的密度,从而控制它的上升力。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-8。

Simulating a hot-air balloon

该模拟的代码可在balloon.js中找到。init()功能创建如图 7-8 所示的视觉背景,并创建一个气球作为Ball对象,最初静止在地面上。它还设置了keydownkeyup事件监听器,相应的keydown事件监听器响应UPDOWN箭头的按下,分别从气球的初始值 1.1 增加或减少气球的密度rhoP。空气密度rho保持恒定在 1.2。您可以查看代码以了解更多细节。

到目前为止,你可能不需要太多的帮助就可以编写动画代码了。这里唯一的实质性变化是calcForce()方法,它指定了两个力(gravityupthrust),然后将它们相加:

function calcForce(){

var gravity = Forces.constantGravity(m,g);

var V = m/rhoP; // volume of air displaced

var upthrust = Forces.upthrust(rho,V,g);

force = Forces.add([gravity, upthrust]);

}

使用新创建的Forces.upthrust()静态函数计算upthrust,该静态函数基本上使用公式 U = ρgV,其中ρ(代码中的变量rho)是流体的密度,V(代码中的变量V是置换的空气体积。这是气球的体积,因为它完全浸没在空气中。所以V是用气球的质量和它的有效密度rhoP计算出来的,公式是体积=质量/密度。有效密度rhoP是整个气球的密度,包括它所承载的任何载荷(不仅仅是其中空气的密度)。

模拟中的关键参数是气球密度与环境空气密度的比率rhoP/rho。如果有效气球密度rhoP小于空气密度rho(该比值小于 1),气球将上升。如果rhoPrho大,就会沉下去。

为了使模拟更具交互性,我们还在move()中包含了对函数changeBalloonDensity()的调用,该函数允许您通过分别按上下箭头键来增加和减少气球的密度rhoP。每当rhoP改变时,该代码向控制台输出比率值(rhoP/rho)。看看你能否让气球上升,然后在某个高度保持静止。

这个模拟中缺少了一些东西。气球升得太快;它基本上是在多余的上推力的作用下加速的,所以它向上的速度一直在上升。现实中会被拖慢。因此,让我们来看看我们如何可以包括拖动效果。

对于不熟悉流体动力学的人来说,尝试对阻力建模可能会有些困惑。一个原因是阻力的类型不止一种,而是几种。根据要模拟的对象和流,可能需要考虑不同类型的拖动。

困难的另一个来源是,我们对阻力的许多了解都是基于经验工作。阻力定律是通过大量的实验发现的,而不是像引力那样作为一个美丽的普遍理论的一部分。

底线是,你会碰到从实验结果中发展出来的公式,你必须毫无争议地接受这些公式。

对于本书中的大部分内容,我们将使用两个阻力定律中的一个:一个适用于低速(或所谓的层流),另一个(更常见)适用于高速(或所谓的湍流)。

低速阻力定律

对于以非常低的速度在流体中移动的物体,物体周围的流动是层流或流线型的。这产生相对较低的阻力。那么阻力就服从斯托克斯定律,即阻力与物体的速度成正比。对于球形物体,斯托克斯阻力公式如下,其中 r 是球体的半径,希腊字母η表示称为粘度(更准确地说是动态粘度)的流体属性:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

注意,动态粘度也用希腊字母μ表示。流体的粘度是它对通过它的物体所提供的阻力的量度。直观上,它代表了流体的“厚度”。所以水的粘度比空气高,油的粘度比水高。因此,在水中以相同速度运动的物体比在空气中受到更大的阻力。该公式还告诉我们,较大的物体受到的阻力更大,因为较大物体的参数 r 更大。

要在精确的模拟中使用该公式,您需要知道对象在其中运动的流体的粘度。像密度一样,粘度取决于温度。20°C 时,水的动力粘度为 1.0 × 10 -3 kg/(ms),空气的动力粘度为 1.8×10-5kg/(ms)——约为 1/55。

在对这种阻力进行编码时,我们将把所有的因素放在一起,把速度乘以一个由 k 表示的单一因素,这样,线性阻力定律就变成如下,其中 k = 6ρρr 对于一个球体:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

以这种形式写出线性阻力定律实际上更普遍,因为它也适用于其它非球形物体,对于这些物体,这个公式没有给出 k。通过选择合适的 k 值,我们可以模拟任何形状物体上的线性阻力。

这基本上是我们在第五章的中编码到Forces.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;

}

高速时的阻力定律

斯托克斯阻力定律只适用于低速。在较高的速度下,浸没物体周围的流动变得紊乱,产生扰乱流动并使其混乱的涡流。阻力定律不同于层流定律,它与物体速度的平方成正比。阻力公式由下式给出,其中ρ是流体的密度,A 是物体的正面面积(当物体通过时流体冲击的面积),C d 是称为阻力系数的参数,其取决于物体的特性,例如其形状、表面特性和流动特性:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

请注意,该公式涉及速度 v 与其大小 v 的乘积。这给出了在 v 方向上的大小为 v 2 的矢量。因此,阻力定律在速度上是二次的,大小由下式给出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在这种形式中,很明显二次阻力取决于动压 P = 1/2 ρv 2 ,实际上我们可以这样写:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如前所述,阻力系数 C d 可能取决于大量因素。它的值是通过对特定对象和设置的实验得到的。例如,球体的阻力系数范围可以从 0.07 到 0.5。同样,你可以在物理或工程教科书或网站上找到大量物体形状的阻力系数(2D 和 3D)。

至于层流阻力公式,我们将通过定义高速阻力常数 k 来简化前面的公式,如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

所以高速时的阻力定律可以写成这种形式:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们现在定义名为Forces. drag()的第二个阻力函数,如下所示:

Forces.drag = function(k,vel) {

var force;

var velMag = vel.length();

if (velMag > 0) {

force = vel.multiply(-k*velMag);

}

else {

force = new Vector2D(0,0);

}

return force;

}

我应该使用哪种阻力定律?

我们已经描述了两个阻力定律,线性的和二次的,说前者适用于低速,后者适用于高速。但是到目前为止,我们对“低”和“高”的含义相当模糊。区分高低的“临界流速”是什么?

为了回答这个问题,我们引入了流动雷诺数的概念。简单地说,雷诺数告诉我们流动是层流还是湍流。因此,它告诉我们,除了别的以外,是线性还是二次阻力定律适用。用符号 Re 表示的雷诺数由以下方程定义,其中 u 是与流动相关的特征速度,d 是特征长度标度,希腊符号ν (nu)是所谓的流体运动粘度:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

运动粘度定义为流体的动力粘度与密度之比:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

利用前面给出的动力粘度和密度值,我们可以推导出在 20°C 时,水的运动粘度为 1.0×10-6m2s,空气的运动粘度为 1.5×10-5m2s

选择什么样的速度 u 和什么样的长度尺度 d 来计算雷诺数取决于问题。对于在流体中运动的球体这样的物体,u 只是物体的速度,d 是线性维度(球体的直径)。

现在,实验发现层流占主导地位,因此,当雷诺数远小于 1 时,线性阻力的斯托克斯定律成立。使用雷诺数的公式,并设置 Re = 1,这意味着决定哪个阻力定律适用的临界速度由下式给出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如果物体的速度远小于这个临界值 v c ,则阻力定律是线性的;如果远大于 v c ,阻力定律为二次型。如果速度介于这两个极限之间,则可以使用两个定律的组合。

例如,足球的直径为 22 厘米,即 0.22 米。使用前面给出的水和空气的运动粘度值,我们推断出这种球的临界速度在水中为 0.0045 毫米/秒,在空气中为 0.068 毫米/秒(是的,这些都是以毫米/秒为单位)。这些都是很小的速度,所以实际上你可以假设这种大小的球在水中或空气中的运动总是遵循二次阻力定律。这也适用于大多数在空气或水中以正常速度运动的日常物体。

另一方面,直径为 1 mm 的滚珠轴承落入甘油中(在 20°C 时,其运动粘度为 1.2 × 10 -3 m 2 /s),其临界速度为 1.2 m/s。这是一个大得多的临界速度。滚珠轴承达到的最大速度(其终端速度)比这个要小得多。因此,线性阻力定律适用于这种情况。

向气球模拟添加阻力

如前一节所示,适用于大多数在空气或水中运动的日常物体的阻力定律是二次阻力定律,正如在Forces.drag()函数中编码的那样。给气球模拟的最后一个例子添加阻力是一件非常简单的事情。我们简单地更新calcForce()如下:

function calcForce(){

var gravity = Forces.constantGravity(m,g);

var V = m/rhoP; // volume of air displaced

var upthrust = Forces.upthrust(rho,V,g);

var drag = Forces.drag(k,balloon.velo2D);

force = Forces.add([gravity, upthrust, drag]);

}

新变量k是阻力常数。在提供的示例代码中,我们给它的值是 0.01。如果您使用这些修改运行模拟,您将会看到气球不再以加速度上升。阻力的增加减缓了它的上升速度,效果更加真实。

示例:浮球

现在让我们回到我们在第五章中介绍的浮球模拟来说明阻力、上推力和重力的影响。该代码模拟了一个球在水中被抛出、落下或释放的运动。

代码在文件floating-ball.js中。代码的视觉和交互方面将在第五章中讨论。在这里,我们将把重点放在物理学上,它本质上包含在calcForce()方法中:

function calcForce(){

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 gravity = Forces.constantGravity(ball.mass,g);

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]);

// bouncing off walls

if (xball < rball){

ball.xpos = rball;

ball.vx *= vfac;

}

if (xball > canvas.width - rball){

ball.xpos = canvas.width - rball;

ball.vx *= vfac;

}

}

由位于calcForce()末端的两个if块组成的代码段处理球从墙上弹回的情况。之前的四行代码应该是显而易见的:我们正在计算作用在球上的三个力(重力、上推力和阻力),然后将它们相加得到合力。这里物理学的新内容是包含了一个叫做ratio的因素,它是浸没在水中的球的体积分数。这用于计算水中的上推力(根据阿基米德原理)和水中的阻力(假设这也与浸没的体积分数成比例——这实际上是一个相当粗略的近似,但有助于使算法更加简单)。为了简单起见,我们忽略了球在空气中可能受到的上推力和阻力。不难看出,如果需要的话,也可以把它们包括在内。所有这些应该很容易理解;微妙之处在于计算ratio的代码。

要计算ratio,需要做一些几何思考。看一下图 7-9 ,它代表一个长方体和一个部分浸入水中的球体。在我们的动画中,假设相应对象的坐标在对象的中心,我们需要一个公式,根据对象的位置和水位的位置给出ratio

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-9。

Objects partially immersed in water

要做到这一点,首先定义一个参数dr是很方便的,它告诉你物体的中心在水面之上或之下多少,作为球的半径(或长方体的半高)的一部分。所以dr定义如下,其中 r 是物体的半高(等于球的半径):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

稍微思考一下应该就能让你相信,dr=–1 表示物体(不管是球还是长方体)刚好完全出水,dr = 1 表示刚好完全没入水中。因此,如果dr<=–1,ratio为零,如果dr > = 1,ratio为 1。更棘手的是当物体部分浸没在水中时。以长方体为例,简单的几何告诉我们(是的,你可以自己算出来):ratio = 0.5 + 0.5 dr

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于球体,计算起来要复杂一点(你需要做一点微积分),但公式如下:ratio = 0.5+0.25 dr(3dr 2)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这就是我们所需要的。

代码中,球的体积V设为 1,质量也是 1。所以它的密度(等于质量/体积)是 1。水的密度设定为 1.5。所以,因为球的密度小于水的密度,所以会浮起来。

floating-ball.js中,球的初始位置和速度给定如下:

ball.pos2D = new Vector2D(50,50);

ball.velo2D = new Vector2D(40,-20);

这使得球最初在水面上,并产生一个向上的速度分量和一个向右的分量。如果你运行这个代码,你会看到球在空中沿着一条曲线(抛物线)运动,直到它碰到水。然后,由于水中的阻力和上升推力,它突然减速,下沉一点,然后再次浮出水面,在水面上振荡,直到停止。仿真截图如图 7-10 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-10。

A floating ball

这个模拟是如此的真实,你可以用它来做实验以了解其中的物理过程!点击水面上方的任何地方,将球移动到那里。然后把它放在水面上。它将落入水中,减速,上升,并在停止前再次在水面上振荡几次。你把它丢得越高,它就会沉得越深。现在点击水下并释放它。它将上升到表面,并在停止前再次振荡几次。

您还可以通过改变初始条件或参数值来进行试验。比如把球的质量改成 2,或者体积改成 0.5,这样密度就是 2。然后球会下沉而不是漂浮,因为它的密度大于水的密度,水的密度是 1.5。

想尝试不同的东西吗?如果球很轻会发生什么?试试看。通过将体积增加到 2,将密度减少到 0.5。当你在水下释放球时会发生什么?它射向空中,然后落回水中。如果你曾经试着用一个真实的球来做这件事,你会知道这是一个真实的效果。我们的模拟非常逼真,可以用来进行“虚拟实验”。我们甚至还没有注意到数字的准确性(我们将在本书的第四部分讨论)。

这个例子特别有启发性,因为它显示了包含相关物理的模拟如何表现得非常像真实生活中的真实事物。模拟知道如何处理初始条件或参数的任何变化,而不需要你告诉它更多的东西!这就是真实物理模拟的威力。它让你物有所值,可能比你想象的还要多——而且比造假容易多了!

终端速度

正如你在前面的例子中发现的,阻力的存在意味着上升或下降物体的速度不可能无限增加。为了理解为什么,考虑一个物体从高处(远离地面)释放并在重力作用下下落的情况。这个例子在第四章的中讨论过,在第五章的中详细阐述过,但是现在你对阻力有了更多的了解,这里有必要回顾一下以获得更深的理解。特别是,前面的讨论是根据线性阻力公式进行的;了解结果如何推广到二次阻力,以及何时考虑上推力,是很有用的。

在图 7-11 中,有一个向下的重力 W = mg 作用在物体上,还有一个向上的拖曳力 d。拖曳力的大小在非常低的速度下由 kv 给出,但通常由 kv 2 给出。最初物体的速度是零,所以阻力也是零。当它在重力作用下加速时,它的速度增加,因此阻力也增加。这将向下的合力减小到 W–D,从而减小加速度。因为重力是不变的,随着速度的增加,会有一个点,在这个点上拖曳力会和重力相等;换句话说,合力 W–D 为零,所以物体的加速度也为零。换句话说,它最终会以一个恒定的速度运动:终极速度。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-11。

Force balance giving rise to terminal velocity

如第四章所示,很容易算出终端速度的大小。这是 W–D = 0 时的 v 值。因为 W = mg,如果我们用 D = kv,我们得到这个:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

以便

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这是层流的极限速度。它适用于在高粘度流体(如石油)中运动的物体。

如果我们对更高的速度使用阻力定律,D = kv 2 ,我们反而得到

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这就意味着

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们也可以通过用视重量 W = (ρ 物体–ρ流体 ) V g 的绝对值代替重量 W = mg,将这些公式推广到包括上推(见图 7-12 )

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-12。

Force balance with upthrust

如果你正在做一个模拟,其中物体达到终极速度非常快,你可以利用前面的公式,只给物体一个与终极速度相等的恒定速度。这将保持模拟的物理真实性(如果初始加速度不重要),同时显著减少计算时间。例如,在水中上升的气泡达到极限速度非常快,如果除了重力、上推力和阻力之外没有其他力,就可以用这种方式模拟。

正如在第五章中所讨论的,一个物体在重力、上推力和阻力下的运动可以用微积分来解析求解,不仅可以给出最终速度,还可以给出任意时刻的速度。例如,对于在流体中静止下落的物体,假设线性阻力并忽略上推力,解析解由下式给出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这给出了物体在 t 时刻下落后的速度。二次阻力和包括上升推力的解看起来有点复杂,你可以在物理教科书中找到。

示例:降落伞

降落伞的工作原理是利用阻力与物体面积成正比的事实。回到二次阻力 F =–kv v 的公式,用 k = 1/2 ρAC d 给出阻力常数 k,我们可以看到物体的面积越大,阻力就会越大。打开降落伞突然增加了暴露在空气中的表面积,从而增加了 k 值。这大大增加了阻力,因此使跳伞者减速。

我们目前的例子是说明这一原则的教育练习。看看parachute.js中的init()函数:

function init() {

ball = new Ball(20,'#0000ff',m);

ball.pos2D = new Vector2D(650,50);

ball.velo2D=new Vector2D(0,0);

ball.draw(context);

setupGraph();

window.addEventListener('mousedown',openParachute,false);

t0 = new Date().getTime();

t = 0;

animFrame();

};

这只是创建了一个Ball实例(代表一个跳伞者!)然后通过animFrame()和相关的运动代码以通常的方式制作它下落的动画。calcForce()方法增加了重力、上推力和阻力:

function calcForce(){

var gravity = Forces.constantGravity(m,g);

var drag = Forces.drag(k,ball.velo2D);

var upthrust = Forces.upthrust(rho,V,g);

force = Forces.add([gravity, upthrust, drag]);

}

此外,在init()中为mousedown事件设置了一个事件监听器。在相应的事件处理程序openParachute()中,降落伞的半径增加了一个等于linearFactor的因子(在代码中被赋予 3 的值),阻力常数k的值增加了一个等于linearFactor的平方的因子(增加了 9)。然后删除事件侦听器。

function openParachute(evt){

k *= linearFactor*linearFactor;

ball.radius *= linearFactor;

window.removeEventListener('mousedown',openParachute,false);

}

这意味着当用户第一次点击鼠标时,降落伞会增大三倍(以半径计),阻力常数会增大九倍。最后在setupGraph()中设置一个Graph对象,在init()函数中调用,通过调用move()方法中的plotGraph()方法来绘制降落伞在每一时刻的速度的vy分量。

如果你现在运行这个代码(不打开降落伞),你会发现降落伞开始加速很快,然后变得更慢,直到它在下落大约 10 秒后达到一个恒定的速度(极限速度)。终端速度的值大约是每秒 30 个像素。如果你点击“打开降落伞”,它将变得更大,并立即减速,在几秒钟内达到大约每秒 10 像素的新的更低的终端速度。无论何时打开降落伞,最终速度都是一样的:它与时间无关,只取决于降落伞的质量 m,重力加速度 g,以及阻力常数 k(通过公式外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传)。图 7-13 显示了速度-时间图的典型形状。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-13。

Velocity-time graph for a parachute

电梯

你看到了阻力取决于动压。还有一个力也依赖于动压,它叫做升力。更准确地说,是物体两侧的动态压力差造成了这些力。物体上的阻力与物体的速度方向相反,而升力则与速度方向垂直(见图 7-14 )。现在,因为运动物体的前后总有一个动压差,所以运动物体上总有一个拖曳力。然而,如果一个物体沿运动方向的轴线完全对称,则该物体两侧的流量(以及动态压力)将完全相同。在这种情况下,没有升力。但是气流中的任何不对称——例如,由不对称的机翼形状(对于飞机)或迎角引起的不对称——都会产生垂直于运动方向的升力。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-14。

Drag and lift force on an object moving in a fluid

升力使飞机能够飞行。在那种情况下,运动的方向通常是水平的,所以升力垂直向上作用,平衡了飞机的重量。但是升力并不总是垂直向上的。根据运动的方向,它可以向任何方向运动。

升力有时与上推力相混淆。事实上,上冲有时被误认为是升力。像上升一样,上升是因为压力差。上冲是由物体顶部和底部的静压力差引起的,静压力差是由被驱替流体的重量(浮力)引起的;但是升力是由物体沿其运动方向相对两侧的动态压力差引起的。这就造成了前面提到的两侧静压的差异。因此,尽管上升总是向上的(正如它的名字所暗示的),但对升力来说却不一定如此。更重要的是,物理定律以及支配这两种力的公式完全不同。想想这个:升力是使飞机飞行的力量;上推力是使气球漂浮的力量;这两种情况下的机制是不同的。

升力系数

升力是用升力系数来模拟的,与二次阻力完全一样。因此,升力的大小可以写如下,其中 C L 是升力系数,A 是物体沿运动方向的面积(而不是像阻力一样垂直于运动方向):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

像阻力系数 C d 一样,升力系数 C L 取决于多种因素。对于飞机来说,重要的变量是机翼形状和迎角。

正如我们对阻力所做的那样,我们可以定义一个升力常数 k = 1/2 ρAC L ,这样我们就可以把升力的大小写成 F = kv 2

升力的方向垂直于速度的方向。考虑到这一点,我们可以定义一个Forces.lift()函数如下:

Forces.lift = function(k,vel) {

var force;

var velMag = vel.length();

if (velMag > 0) {

force = vel.perp(k*velMag);

}

else {

force = new Vector2D(0,0);

}

return force;

}

例如:一架飞机

现在让我们来演示升力是如何使飞机飞行的。文件airplane.js包含基本飞行演示的代码。我们将在这里复制代码的关键部分。首先,看看变量声明/初始化和init()函数:

var plane;

var m = 1;

var g = 10;

var kDrag = 0.01;

var kLift = 0.5;

var magThrust = 5;

var groundLevel = 550;

var t0,dt;

window.onload = init;

function init() {

makeBackground();

makePlane();

t0 = new Date().getTime();

animFrame();

};

变量名应该是不言自明的。makeBackground()makePlane()方法产生的视觉设置如图 7-16 所示。在makePlane()中,一架飞机被创建为Plane对象的实例,并被放置在初始速度为零的“跑道”上。欢迎您查看一下plane.js文件,看看Plane对象的代码是什么样子(如果您愿意,甚至可以对它进行改进!).但是视觉细节对我们目前的目的并不重要。真正重要的是如何让飞机飞起来。这是由animFrame()和相关方法发起的。这里唯一的新代码在calcForce()中。

如图 7-15 所示,飞机在飞行中主要受四种力:重力(W)、推力(T)、阻力(D)和升力(L)。当飞机在地面上时,还有地面产生的法向接触力。因此,calcForce()方法计算这些力并将它们相加:

function calcForce(){

var gravity = Forces.constantGravity(m,g);

var velX = new Vector2D(plane.vx,0);

var drag = Forces.drag(kDrag,velX);

var lift = Forces.lift(kLift,velX);

var thrust = new Vector2D(magThrust,0);

var normal;

if (plane.y >= groundLevel-plane.height){

normal = gravity.multiply(-1);

}else{

normal = new Vector2D(0,0);

}

force = Forces.add([gravity, drag, lift, thrust, normal]);

}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-15。

The four forces on a airplane in flight

引力和往常一样简单明了。阻力和升力的计算方法很简单,只考虑飞机速度的水平分量。同样,假设飞机在任何时候都是水平的,迎角为零。推力被模拟为一个恒定的力。最后,当飞机在地面上时,法向力被设置为与重力大小相等方向相反,否则为零。

当你运行模拟时,你会看到飞机只有在获得足够高的速度以产生足够的升力来克服其重量时才会起飞(见图 7-16 )。如果你把推力的大小减少到 3 或 4,飞机会沿着跑道移动,但永远不会起飞。在水平推力和阻力的作用下,它达到的最大水平速度(类似于极限速度)不足以产生足够的升力来克服其重量。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-16。

Plane flying thanks to the lift force

重申一下,我们在这个例子中做了很多简化。我们将在最后一章建立一个更加完整和真实的飞行模拟器,在这一章中,我们还将更详细地研究飞机上的阻力、升力和推力。

风和湍流

风本身不是一种力,但它能产生力。风是空气的运动,它在空间和时间的每一点都有一个相关的速度。这可能相当复杂,但即使使用简单的风模型也可以实现相当真实的效果。一股持续的风吹过一个静止的物体,可以被看作是一个物体在空气中运动,只是方向相反。它以类似的方式产生阻力和升力。

风产生的力

正如你在本章开始时看到的,流动的流体有一个相关的动态压力。风只不过是流动的空气。当风吹在一个物体或表面上时,由于动态压力,它会施加一个力。

我们如何计算这个力?根据我们所说的,只有相对速度是重要的。因此,风施加的力正好是反方向的阻力,其中 w 是风速:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

因此,我们可以使用阻力函数来模拟风,使用风速和负阻力常数 k。

风和阻力

当然,即使有风,在空气中运动的物体仍然会受到阻力。因此,由物体运动和空气(风)运动产生的净力将是阻力,以物体和空气之间的相对速度为参考。因此,风和阻力的合力由下式给出,其中| w–v |表示矢量(w–v)的大小:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

注意| w–v |通常不等于 w 和 v 的幅度差(w–v)。

我们可以使用阻力函数,通过物体和空气之间的相对速度来计算风和阻力的综合影响。

稳定和湍流

我们现在知道了在给定风速的情况下,如何计算有风时物体所受的力。但是我们如何模拟风速 w 本身呢?

答案是,这取决于我们正在建模的情况。风的行为相当复杂,因为它随时间和空间而变化。风取决于复杂的大气条件以及建筑物和树木等障碍物。然而,在大多数情况下,你可能希望事情尽可能简单。所以我们将只讨论一些简单的风速建模方法。

最简单的风模型是假设速度 w 在空间和时间上都是恒定的。这相当于所谓的均匀稳定流动。一个稍微复杂一点的模型是假设一个水平恒定但随高度变化的稳定流(在时间上仍然恒定)。这个模型可以解释这样一个事实,即近地面的风速较低,越往上风速越高。

在现实世界中,风通常不是稳定的,而是由明显随机吹动的阵风组成。阵风产生所谓的湍流,这种湍流可以使事物以复杂的方式运动。

示例:稳定风中的气泡

在这个例子中,我们将模拟一个稳定均匀的风在空气中吹一些气泡。文件bubbles-wind.js包含代码。由于它与前面的示例略有不同(因为它涉及多个粒子),我们在此完整复制代码:

var canvas = document.getElementById('canvas');

var context = canvas.getContext('2d');

var bubbles; var t0;

var dt;

var force;

var acc;

var numBubbles = 10;

var g = 10;

var rho = 1;

var rhoP = 0.99;

var kfac = 0.01;

var windvel = new Vector2D(40,0);

window.onload = init;

function init() {

bubbles = new Array();

var color = 'rgba(0,200,255,0.5)';

for (var i=0; i<numBubbles; i++){

var radius = Math.random()*20+5;

var V = 4*Math.PI*Math.pow(radius,3)/3;

var mass = rho*V;

var bubble = new Ball(radius,color,mass,0,true);

bubble.pos2D = new Vector2D(Math.random()*canvas.width,Math.random()*canvas.height);

bubble.velo2D = new Vector2D((Math.random()-0.5)*20,0);

bubble.draw(context);

bubbles.push(bubble);

}

t0 = new Date().getTime();

animFrame();

};

function animFrame(){

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(){

context.clearRect(0, 0, canvas.width, canvas.height);

for (var i=0; i<numBubbles; i++){

var bubble = bubbles[i];

moveObject(bubble);

calcForce(bubble);

updateAccel(bubble.mass);

updateVelo(bubble);

}

}

function moveObject(obj){

obj.pos2D = obj.pos2D.addScaled(obj.velo2D,dt);

obj.draw(context);

}

function updateAccel(mass){

acc = force.multiply(1/mass);

}

function updateVelo(obj){

obj.velo2D = obj.velo2D.addScaled(acc,dt);

}

function calcForce(particle){

var V = particle.mass/rhoP;

var k = Math.PI*particle.radius*particle.radius*kfac;

var gravity = Forces.constantGravity(particle.mass,g);

var upthrust = Forces.upthrust(rho,V,g);

var relwind = windvel.subtract(particle.velo2D);

var wind = Forces.drag(-k,relwind);

force = Forces.add([gravity, upthrust, wind]);

}

init()中,我们创建了许多气泡作为Ball物体,并给每个气泡一个随机的半径。(如果你有一台很慢的电脑,你可能想减少气泡的数量!)然后使用球体体积和气泡密度的公式计算它们的体积和质量,该公式指定为 0.99。气泡被赋予一个随机的位置和一个小的随机水平速度。然后它们被收集到一个数组中,然后在move()方法中使用该数组来依次将方法moveObject(), calcForce(), updateAccel()updateVelo()应用于每个气泡。

calcForce()方法中,三种力被计算并相加:重力、上推力和风(阻力)。上升推力是用每个气泡的体积来计算的,体积=质量/密度。为了计算风力,通过从风速windvel中减去气泡的当前速度来计算相对速度relwind,风速以恒定的水平向量给出。然后通过使用具有负阻力系数的Forces.drag()k和相对速度作为自变量来计算风/阻力。注意k包括一个等于气泡投影面积的因子,考虑到拖曳力与物体的面积成比例。

如果你运行模拟,你会看到气泡顺着风向漂移,同时缓慢上升,如你所料(见图 7-17 )。玩风速和其他参数,看看运动是如何修改的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 7-17。

Bubbles in a steady wind

模拟湍流

湍流是一种复杂的现象。事实上,它是如此复杂,以至于你需要带有数百个处理器的巨大超级计算机来精确计算湍流,即使是简单的配置。另一方面,有许多近似模拟湍流的方法。一个简单的方法是使用随机数。

通过在稳定的风场上叠加随机噪声,可以产生看起来像湍流的效果。这利用了湍流的统计特性,通常可以通过以下方式分解任何时间的风速来表示,其中 w 稳定是稳定部分,w 波动是捕捉湍流波动的时变部分:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

稳定部分 w 稳定部分只是一个恒定矢量,正如我们在之前的气泡模拟中建模的那样。我们可以用一个向量来模拟 w 波动,这个向量的分量是在每个时间步长更新的随机数。要在 bubbles 模拟中做到这一点,只需在计算relwind的代码行之前添加以下代码行:

windvel = new Vector2D(20 + (Math.random()-0.5)*1000,(Math.random()-0.5)*1000);

我们这样做了,并将新文件保存为bubbles-turbulence.js。我们还在从move()调用的函数showArrow()中添加了一些代码来指示风矢量 w 的变化。代码在舞台中间产生一条线,沿着瞬时风向,长度与风速的大小成比例。

前面一行代码将风速 w 指定为以每秒 20 个像素的速度向右吹的稳定风矢量(20,0)和具有每秒–500 到 500 个像素的 x 和 y 分量的随机风矢量(Math.random()-0.5)*1000,(Math.random()-0.5)*1000)之和。舞台上所有位置的风速都相同。

尝试将这些值更改为您喜欢的值。稳定风的大小(此处设置为值 20)使气泡稳定漂移。波动的幅度(这里由因子 1000 设定)控制风的阵风并使气泡波动。你可以用这种方式产生一个相当令人信服的湍流效果。

摘要

本章介绍了许多作用在与其他固体或流体接触的固体上的力。如果你正在模拟地球上的几乎任何东西,很可能你需要模拟这些力。你在这一章学到的东西给了你一个坚实的基础,在此基础上你可以创建更复杂的效果和模拟。在最后一章,你将有机会使用这里解释的物理学来开发更完整的模拟。与此同时,在下一章中,你将了解到一种产生振荡(类似弹簧)运动的新型力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值