1.质点运动学和Stun2DPhysics引擎

这篇文章将介绍运动学的基本规律,包括质点的位移、速度和加速度之间的关系和计算机程序的实现,也是后面的质点动力学的基础。

物理知识

质点是一个理想化物理模型,无需考虑它的大小形状,在高中物理中大多数情况中都可以把物体看成一个有质量的点,而且点也没有旋转的概念。运动学是对物体运动的研究,并不考虑物体上的作用力,我们只关心质点的位移、速度和加速度随时间变化的规律。

在初中物理中我们就已经学过匀速直线运动,加速度为0,位移与速度关系为:

st=s0+vt

式中st表示t秒末的位移,s0表示初始位移,v为速度,它是一个有方向的矢量,表征物体运动的快慢,从另一个角度理解,它也是物体单位时间中位移的改变量,例如v=5m/s向右,即表示物体每秒钟向右移动5m的距离。

到了高中,我们又学习了匀变速直线运动,此运动中速度与加速度的关系为:

vt=v0+at

加速度a也是矢量,表示速度变化的快慢,从另一个角度理解,它也是物体单位时间中速度的改变量,例如a=5m/s2向右,表示物体的速度每秒钟在向右方向增加5m/s。 而位移、速度和加速度之间的关系为:

st=s0+v0t+at2/2

以上公式只能处理a不变的情况,如果a也会变化,那么必须要借助高等数学中微积分的知识才能解决。简单地说,速度就是位移对时间的一阶导数,加速度就是速度对时间的一阶导数,或者说加速度是位移对时间的二阶导数,可以用以下公式表示:

v=sʹ,a=vʹ,a=s˝

但在计算机实现上,我们更关心的是已知加速度如何求速度,如果知道速度求位移,这时就要用到微分的逆运算—积分,因为a=vʹ,所以逆向分析,即速度是加速度对时间的累积,写成公式就是:

v=∫ adt

同样的道理位移是速度对时间的累积,写成公式就是:

s=∫ vdt

式中的dt指一个无穷小的时间间隔,这在数学上是可行的,但在计算机上不可能存在无穷小的时间间隔,但只要时间间隔取得足够小,积分运算误差就不会很大,这倒印证了计算机图形学上的一句名言:如果看上去是对的,那它就是对的。通常帧频为30帧/秒,时间间隔为1/30秒,这已经够小了,而且为了减少系统负担,在进行物理模拟运算时这个时间间隔还会取得大一些。

因此在计算机上要实现v=∫ adt,s=∫ vdt的伪代码如下:

void Update(dt)
{
    v+=a*dt;
    s+=v*dt;
}

以上内容只讨论了一维方向上的运动,如果扩展到二维一点也不难,只需在两个维度上进行同样的操作,即:

vx=∫ axdt,vy=∫ aydt

sx=∫ vxdt,sy=∫ vydt

这在物理学上叫做运动独立性原理,即:一个物体同时参与几种运动,各分运动都可看成独立进行的,互不影响,物体的合运动则视为几个相互独立分运动叠加的结果。分运动和合运动之间具有:独立性、等时性、矢量性、同体性。高中阶段如果你选修物理就会学到这个知识。

三维空间中的情况也是一样的。

有以上公式我们可以知道,物体的运动情况归根结底是由它的初始状态(初始位移和初速度)和加速度(严格地说,应该是由受力情况决定的,这会在后面的质点动力学中进行讨论)决定的,因此只需设置物体的初始位置、初速度和加速度,计算机程序就会自动算出任意时刻物体的位置了。

实现Stun2DPhysics

理解了背后的物理知识,下面我们就开始实现一个最简陋的2D物理引擎,我把它命名为Stun2DPhysics。主要参考了Farseer Physics Engine2.13,虽然截止到2011年5月26日,Farseer Physics Engine的版本已经更新到了3.3.1,但是因为3.0版本相对于2.0版本变化较大,而且我也不太看的懂,所以Stun2Dphysics的第一个版本还是以2.0版本为蓝本的。

首先创建一个名为Stun2Dphysics的新项目,项目类型为Windows Game Library。然后在项目中添加名称为Dynamics文件夹,在这个文件夹中添加一个名为Body的类,代码如下:

using Microsoft.Xna.Framework;

namespace Stun2DPhysics.Dynamics
{
    /// 
    /// Body处理物理属性: 位置、速度、加速度等...
    /// 
    public sealed class Body
    {
        /// 
        /// 上一次更新时的线速度
        /// 
        private Vector2 previousLinearVelocity = Vector2.Zero;

        /// 
        /// 上一次更新时的位置
        /// 
        private Vector2 previousPosition = Vector2.Zero;

        /// 
        /// Body的当前位置
        /// 
        internal Vector2 position = Vector2.Zero;

        /// 
        /// Body是否静止。
        /// 
        internal bool isStatic;

        /// 
        /// 获取或设置线速度。
        /// 
        public Vector2 LinearVelocity = Vector2.Zero;

        /// 
        /// 获取或设置加速度。
        /// 
        public Vector2 Acceleration = Vector2.Zero ;

        /// 
        /// 创建一个Body对象
        /// 
        public Body() {}        

        /// 
        /// 获取或设置Body的位置。
        /// 
        public Vector2 Position
        {
            get { return position; }
            set { position = value;}
        }

        /// 
        /// 表示Body是否是固定的。
        /// 地面和边界通常就是固定的。
        /// 
        public bool IsStatic
        {
            get { return isStatic; }
            set
            {
                isStatic = value;
            }
        }

        #region 积分运算临时变量

        private Vector2 ds;
        private Vector2 dv = Vector2.Zero;

        #endregion

        /// 
        /// 由引擎在内部调用,对速度进行积分运算。
        /// 
        /// 时间间隔。
        internal void IntegrateVelocity(float dt)
        {
            // 速度的该变量即加速度与时间的乘积
            dv.X = Acceleration.X * dt;
            dv.Y = Acceleration.Y * dt;

            // 保存上一次更新时的速度
            previousLinearVelocity = LinearVelocity;

            // 计算当前速度
            LinearVelocity.X = previousLinearVelocity.X + dv.X;
            LinearVelocity.Y = previousLinearVelocity.Y + dv.Y;            
        }

        /// 
        /// 由引擎在内部调用,对位置进行积分运算。
        /// 
        /// 时间间隔
        internal void IntegratePosition(float dt)
        {
            // 位置的改变量即速度乘以时间
            ds.X = LinearVelocity.X * dt;
            ds.Y = LinearVelocity.Y * dt;

            if (ds != Vector2.Zero)
            {
                // 保存上一次更新时的位置
                previousPosition = position;

                // 计算当前位置
                position.X = previousPosition.X + ds.X;
                position.Y = previousPosition.Y + ds.Y;
            }
        } 
    }
}

Body表示一个物体,目前表示一个质点,在很多其他程序中把处理质点的类命名为Particle,但是以后这个类还要处理刚体,因此没有把它命名为Particle。

这个类其实很简单,保存了物体的位置、速度和加速度,而速度和加速度可以直接进行设置。

最关键的代码位于积分运算的IntegrateVelocity()和IntegratePosition()中,不是很难。其实可以写成更简单的代码:

internal void IntegrateVelocity(float dt)
{ 
    LinearVelocity.X+=Acceleration.X * dt; 
    LinearVelocity.Y+=Acceleration.Y * dt;
}

internal void IntegratePosition(float dt)
{
    position.X+=LinearVelocity.X * dt; 
    position.Y+=LinearVelocity.Y * dt;
}

还可以更简单:

internal void IntegrateVelocity(float dt)
{
    LinearVelocity +=Acceleration * dt;
}

internal void IntegratePosition(float dt) 
{
    position+=LinearVelocity * dt;
}

LinearVelocity +=Acceleration * dt就是相当于LinearVelocity.X+=Acceleration.X * dt和LinearVelocity.Y+=Acceleration.Y * dt两行代码的组合,它调用了Vector2的+=方法,需要额外的函数调用,对于对速度比较敏感的物理引擎来说,还是分别写了两行代码比较快一些,这就相当于C++中内联函数。

但在以后的应用中还需用到上一次更新的速度和位置,所以代码中进行了额外的工作。有了Body类,下面需要创建一个新的类管理它,它就是引擎类PhysicsSimulator,代码如下:

using System;
using System.Collections.Generic;
using Stun2DPhysics.Dynamics;

namespace Stun2DPhysics
{
    public class PhysicsSimulator
    {
        /// 
        /// 保存所有Body的集合
        /// 
        private List bodyAddList;
        
        /// 
        /// 如果设置为false,物理系统会停止工作。
        /// 
        public bool Enabled = true; 
        
        /// 
        /// 创建一个类对象。
        /// 
        public PhysicsSimulator()
        {
            ConstructPhysicsSimulator(); 
        }
        
        private void ConstructPhysicsSimulator()
        {
            bodyAddList = new List(32); 
        }
        
        /// 
        /// 在集合中添加一个Body
        /// 
        /// 
        public void Add(Body body)
        {
            if (body == null)
                 throw new ArgumentNullException("body","body不能为null");
             if (!bodyAddList.Contains(body))
             {
                 bodyAddList.Add(body); 
             }
         }
         
         ///         
         // 更新物理系统。
         /// 
         /// 时间间隔。
         public void Update(float dt)
         {
             // 如果时间间隔为0或者物理系统没有激活则不进行任何操作。 
             if (dt == 0 || !Enabled)
                 return; UpdatePositions(dt); 
         }
         
         ///
        /// 更新所有Body的位置。
        /// 
        /// 时间间隔。
        private void UpdatePositions(float dt)
        {
            for (int i = 0; i < bodyAddList.Count; i++)
            {
                if (bodyAddList[i].isStatic)
                    continue;

                bodyAddList[i].IntegrateVelocity(dt);
                bodyAddList[i].IntegratePosition(dt);
            }
        }      
    }
}

这个类管理一个Body的集合,在它的Update方法中更新集合中所有Body的速度和位移。

好了,至此,史上最简陋的物理引擎制作完毕,下一篇文章我们将讨论如何使用这个引擎。

文件下载(已下载 1121 次)

发布时间:2011/5/26 上午11:02:44  阅读次数:6326

2006 - 2024,推荐分辨率 1024*768 以上,推荐浏览器 Chrome、Edge 等现代浏览器,截止 2021 年 12 月 5 日的访问次数:1872 万 9823 站长邮箱

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号