web物理引擎p2.js入门手册

来源:互联网 发布:fz什么意思网络语言 编辑:程序博客网 时间:2024/05/08 04:23
最近在学egret游戏引擎,他们推荐的第三方物理引擎库是p2.js,瑞士的一个杀马特开发的,中文资料很少,于是我就把他github项目的wiki给翻译出来了,这里是项目地址:https://github.com/schteppe/p2.js/wiki
大家觉得错了或者不够好,就来github完善这个中文wiki吧 https://github.com/schteppe/p2.js/wiki/Chinese-wiki-%E4%B8%AD%E6%96%87%E7%BB%B4%E5%9F%BA
六个月前,我参加英语六级考试,388分 再见再见

p2.js内容导航

  • Core concepts核心概念

  • Hello p2.js

  • Math

  • Collision碰撞

  • Dynamics动力

  • Shapes形状

  • Bodies刚体

  • Constraints约束

  • Equations方程式

  • Events事件

  • Materials材料

  • World世界

  • Solvers求解器

  • The Demo framework demo的代码框架

  • Limitations限制

  • References 说明

欢迎使用p2.js手册。这篇手册的目的在于覆盖p2.js API文档中没有提及的知识点,点击进入API文档地址。

使用p2框架前,你必须懂得基础的物理学概念,比如质量、力、扭力和推力。否则,你只能先上谷歌和维基百科了。由于p2.js是基于javascript语言编写的,显然你还得有js的编程能力。

如果你有问题反馈的话,请发帖。

Core concepts核心概念

Shape(形状),一个几何形状,可以是矩形、圆形等等。

Body(刚体),它是一块无限坚硬的物体。因此,在这块物体上任何两点之间的距离都被认为是固定的。Body(刚体)有自己的参数用来规定位置、质量和速度等,刚体的形状是由Shape创建的形状确定的。

Constraint(约束),constraint 是一个物理连接件,用来控制刚体的自由度。在3d世界,物体有6个自由度(3个平移坐标和3个旋转坐标)。在2d世界,物体只有3个自由度(2个平移坐标和1个旋转坐标)。众所周知,人类世界是3d的,因此我们家里的门本来应该是有6个自由度的,但是由于门的一侧被门铰链固定在墙上,它失去了另外5个自由度,只能照着门铰链这个轴旋转了。门铰链就相当于一个constraint(约束)。

Contact constraint(接触约束),这是一个特别的约束,作用在于防止刚体之间的渗透重叠,并且它可以模拟摩擦和弹性。你无须创建这个约束,系统会自动创建它的。

World(世界),这就是一个模拟的物理世界,所有的刚体和约束创建后都要放进来。

Solver(求解器),物理世界的solver(求解器)专门用于处理约束情况。

Units(单位),就是用来测量长度、时间等等数量的单位。在p2.js中,我们用 meters-kilogram-second (MKS) 为单位,用弧度作为角度单位。不要用像素做单位哦。

Hello p2.js!

接下来我们开始建一个简单的物理世界,只用一个动态的圆形和一个静态的平面就好了。搞起来:

var world = new p2.World({   gravity:[0,-9.82] });

这就是一个简单的世界实例了,并且我们把这个世界的重力设置为了9.82,也就是在y轴的负半轴方向。

现在,我们就来创建一个圆形刚体吧。

var circleBody = new p2.Body({     mass:5,     position:[0,10] });

这样,我们就在 x=0, y=10 的位置上创建了一个5kg的刚体了,不过它暂时还没有形状,因此我们得给它添加一个形状。

var circleShape = new p2.Circle({ radius: 1 });circleBody.addShape(circleShape);

至此,我们已经创建了一个圆形的刚体了,如果我们现在就运行程序模拟世界的话,这个圆形刚体将会开始下坠永不停息,并且速度会越来越快,因为我们给它指定了一个9.82的重力加速度,如图真实世界。所以我们还得创建一个平面模拟地面来接住这个圆形刚体。

var groundShape = new p2.Plane();var groundBody = new p2.Body({mass:0});groundBody.addShape(groundShape);

我们通过设置mass为0,来告诉物理引擎这个刚体应该是静止的。这里我们没有设置它的position(位置),因此默认它会从坐标原点开始生产这个平面。

在我们开始运行这个world之前,必须先把制作好的圆形刚体添加到world中。

world.addBody(circleBody);world.addBody(groundBody);

现在,我们可以开始整合这个世界了

var timeStep = 1/60;setInterval(function(){world.step(timeStep);console.log("Circle x position: " + circleBody.position[0]);console.log("Circle y position: " + circleBody.position[1]);console.log("Circle angle: " + circleBody.angle);}, 1000 * timeStep);

通过上面的代码你可以在控制台看到刚体的位置和角度,就像下面这样:

Circle x position: 0Circle y position: 10Circle angle: 0Circle x position: 0Circle y position: 9Circle angle: 0...Circle x position: 0Circle y position: 0.5Circle angle: 0

Math

在定义矩阵和向量方面,p2.js 使用一个特别的轻量级的的数学库glMatrix。p2的向量方法记载于此。glMatrix库的数学函数都公开在这个库里面,所以你可以这样使用它们:

var force = p2.vec2.fromValues(1,0);p2.vec2.add(body.force, body.force, force); // body.force += force

Collision(碰撞)

TODO

Dynamics(力学)

TODO

Shapes(形状)

在p2中,我们可以创建矩形Box,胶囊形状Capsule,圆形Circle,凸形状Convex,起伏平面Heightfield,线形Line,粒子Particle 和 平面Plane。

Filtering shape collisions(过滤碰撞形状)

位掩饰bit masks遮罩你就可以开启或者是禁止各组形状间的碰撞,当然我们得先给形状分组(groups)。更多相关知识,请看这个教程。

// Setup bits for each available groupvar PLAYER = Math.pow(2,0),    ENEMY =  Math.pow(2,1),    GROUND = Math.pow(2,2)// Put shapes into their groupsplayer1Shape.collisionGroup = PLAYER;player2Shape.collisionGroup = PLAYER;enemyShape  .collisionGroup = ENEMY;groundShape .collisionGroup = GROUND;// Assign groups that each shape collide with.// Note that the players can collide with ground and enemies, but not with other players.player1Shape.collisionMask = ENEMY | GROUND;player2Shape.collisionMask = ENEMY | GROUND;enemyShape  .collisionMask = PLAYER | GROUND;groundShape .collisionMask = PLAYER | ENEMY;

下面的代码演示了两个形状间的碰撞过滤检测:

if(shapeA.collisionGroup & shapeB.collisionMask)!=0 && (shapeB.collisionGroup & shapeA.collisionMask)!=0){    // The shapes can collide}

在javascript中,你可以使用32个有效的组(Math.pow(2,0) 到 Math.pow(2,31))。然而,当组group的类型设置为Math.pow(2,0)时,该组中的形状是不能与其他组碰撞的,当组group的类型设置为Math.pow(2,31),该组中的形状可以与其他任何组中的形状碰撞。

Bodies(刚体)

Body types(刚体类型)

刚体有三种类型,Body.STATIC, Body.KINEMATIC, and Body.DYNAMIC.

Dynamic类型刚体可以与任何类型的刚体交互,可以移动。

Static类型刚体不可以移动,但是可以与 dynamic类型刚体交互。

Kinematic类型刚体通过设置速度来控制,其他方面则和Static刚体相同。

// By setting the mass of a body to a nonzero number, the body// will become dynamic and will move and interact with other bodies.   var dynamicBody = new Body({      mass : 1   });   console.log(dynamicBody.type == Body.DYNAMIC); // true// Bodies are static if mass is not specified or zero. Static bodies will never move.var staticBody = new Body();console.log(staticBody.type == Body.STATIC); // true// Kinematic bodies will only move if you change their velocity.var kinematicBody = new Body({    type: Body.KINEMATIC});

Mass properties(质量属性)

刚体有质量和惯性。在创建刚体时你可以设置它的质量,或者在模拟物理世界时动态的设置质量值。

var body = new p2.Body({  mass: 3});// Dynamicallybody.mass = 1;body.updateMassProperties();

Constraints(约束)

TODO

Equations(方程式)

TODO

Events(事件)

postStep

给刚体施加力的时候,你可能要用到步后(postStep)事件,在每一步后力都会逐渐变为0。

world.on("postStep", function(){    body.force[0] -= 10 * body.position[0];});

Events fired during step(在步中销毁事件)

某些事件在执行world.step()方法的时候会被销毁。这是极好的,这样你就可以改变步的行为了。千万小心:如果你像下面中的例子那样,在一个步期间就移除刚体,那么你的世界极有可能崩裂。因此:阅读清楚你所使用事件的文档要求。

别这样:

world.on("beginContact",function(evt){    world.removeBody(evt.bodyA); // BAD!});

相反,你要保存这个需移除的刚体直到过了这个步:

var removeBody;world.on("beginContact",function(evt){    removeBody = evt.bodyA;});// Simulation loopsetInterval(function(){    world.step(timeStep);    if(removeBody){        world.removeBody(removeBody); // GOOD!        removeBody = null;    }},1/60*1000);

Materials(材料)

两个圆形,一个是冰做的,一个是铁做的。

var iceMaterial = new p2.Material();var steelMaterial = new p2.Material();var iceCircleShape = new p2.Circle({ radius: 0.5 });var steelCircleShape = new p2.Circle({ radius: 0.5 });iceCircleShape.material = iceMaterial;steelCircleShape.material = steelMaterial;

两块材料接触时,摩擦和弹性就产生了。那我们如何知道冰和铁接触时的摩擦系数是多少呢?简单,维基百科一下就OK了。

为了定义两个物体间的摩擦系数和弹性系数,我们得实例化一个ContactMaterial(材料接触)。

var iceSteelContactMaterial = new p2.ContactMaterial(iceMaterial, steelMaterial, {    friction : 0.03});world.addContactMaterial(iceSteelContactMaterial);

ContactMaterial还可以规定材料间接触时的其他属性,比如弹性、表面速度。

如果两块材料间的接触(ContactMaterial)没有被设置,则会使用默认的ContactMaterial

World(世界)

Gravity

Gravity(重力)是全局的,它会被运用在所有刚体上,每一步都会。通过world.gravity,你可以设置和获得当前的重力向量。

world.gravity[0] = 0;     // xworld.gravity[1] = -9.82; // y// or:p2.vec2.set(world.gravity,0,-9.82);

有时我们不想给所有的刚体运用重力,这时我们应该turn off掉全局重力了,然后我们自己来给刚体施力。

// Turn off global gravityworld.applyGravity=false;// Keep track of which bodies you want to apply gravity on:var gravityBodies=[body1,body2,body3];// And just before running world.step(), do this:var gravity = p2.vec2.fromValues(0,-9.82),    gravityForce = p2.vec2.create();for(var i=0; i<gravityBodies.length; i++){    var b =  gravityBodies[i];    p2.vec2.scale(gravityForce,gravity,b.mass); // F_gravity = m*g    p2.vec2.add(b.force,b.force,gravityForce);  // F_body += F_gravity}

Stepping the world(步进世界)

当你只想按照时间向前模拟world,你可以运行方法world.step(fixedTimeStep),fixedTimeStep将会决定模拟的世界的“分辨率”。

// Framerate dependent - Fail!var fixedTimeStep = 1 / 60;requestAnimationFrame(function animloop(timeMilliseconds){        requestAnimationFrame(animloop);        world.step(fixedTimeStep);});

如果你在物理世界的渲染环节使用这种方法,你会发现刚体的速度会依赖于我们此时运行world的帧率。在移动设备上,帧率经常会被覆盖为30帧,而不是我们在pc端得到的60帧。

World.prototype.step的完整情况是这样的:

world.step(   fixedTimeStep,   timeSinceLastCall,   maxSubSteps);

如果你把这三个参数都忽略掉,p2.js会让你的world在每一帧上都以相同的步调运行。

// Frame rate independent! Success!var fixedTimeStep = 1 / 60, maxSubSteps = 10, lastTimeMilliseconds;requestAnimationFrame(function animloop(timeMilliseconds){    requestAnimationFrame(animloop);var timeSinceLastCall = 0;if(timeMilliseconds !== undefined && lastTimeMilliseconds !== undefined){    timeSinceLastCall = (timeMilliseconds - lastTimeMilliseconds) / 1000;}world.step(fixedTimeStep, timeSinceLastCall, maxSubSteps);lastTimeMilliseconds = timeMilliseconds;}

这是怎么工作的呢?

如果你使用第一个参数fixedTimeStep的话,那么每当p2.js让时间前进的时候,它都会推进它内部的物理时钟用这个参数值。

第二个参数timeSinceLastCall,规定每隔多长的时间唤醒 world.step()方法。p2.js有个内置的“挂钟”,会把每隔多长这个时间的累积量映射出来。

当你把三个参数都传给world.step()方法时,p2.js会执行fixed steps直到“物理时钟”和“挂钟”的时间同步了。这个花招是为了得到独立的帧速率。最后一个参数 maxSubSteps就不要解释了:这个就是每次使用world.step()方法时,规定最大的fixed steps。

切记,timeSinceLastCall 的值总是要小于 maxSubSteps * fixedTimeStep,否则的话你就是在流失时间了。

请记住这里的时间值是以秒来衡量的,不是毫秒。人们在使用requestAnimationFrameDate.now()和 performance.now()时经常犯这样的错误。这会导致一些莫名奇怪的bug出现,比如:无论如何都是帧速率独立。刚体不会动直到你给它施加一个巨大的力并且随之它会拥有巨大的加速度。在运行step()方法之前把时间除以1000,问题就解决了。

fixedTimeStep size(固定时间步的大小)

减少fixedTimeStep(固定时间步),可以增加模拟的世界的“分辨率”。如果你发现你的物体移动得非常快,并且不经碰撞直接脱离墙体,那么你可以通过减少时间步fixedTimeStep来纠正这个问题。并且要记得增加maxSubSteps的值以确保符合要求timeSinceLastCall < maxSubSteps * fixedTimeStep

Interpolation(插值)

p2.js是动态传值的。如果你传三个参数值给world.step()方法,那么你会得到一个为每个刚体插值的位置通过 body.interpolatedPosition 和 body.interpolatedAngle。比起body.position and body.angle ,使用body.interpolatedPosition and body.interpolatedAngle去渲染物理世界能让动作看起来更流畅。

Example

var maxSubSteps = 10;var fixedTimeStep = 1 / 60;var lastTimeSeconds;function animate(t){    requestAnimationFrame(animate);    var timeSeconds = t / 1000;    lastTimeSeconds = lastTimeSeconds || timeSeconds;    var timeSinceLastCall = timeSeconds - lastTimeSeconds;    world.step(fixedTimeStep, timeSinceLastCall, maxSubSteps);    renderBody(body.interpolatedPosition, body.interpolatedAngle);}requestAnimationFrame(animate);

Solvers(求解器)

一个solver就是一条解决一个线性方程组的运算法则。在p2.js,它处理着约束,接触,摩擦。

GSSolver(高斯求解器)

在p2.js中有两种求解器,高斯求解器GSSolver是最稳固的了。这个求解器是重复的,设定好重复次数iterations后它能得出一个较平均的解。通常,重复次数越多,解也就越精确。

world.solver = new GSSolver();world.solver.iterations =  5; // Fast, but contacts might look squishy...world.solver.iterations = 50; // Slow, but contacts look good!

Island solving(岛屿处理)

不同于一次性处理整个系统,Island solving(岛屿处理)会把整体分割成独立的部分(亦称“岛屿”),然后去分别处理它们。

岛屿并行处理,效果显然是最好的。而且有助于在单线程的状态下连续处理,特别是当solver tolerance求解器的容差大于0的时候。求解器能够很早的就保释一些岛屿,其他的岛屿则得到更多的重复次数

var world = new p2.World({    islandSplit: true});world.solver.tolerance = 0.01;

Solver parameters(求解器参数)

求解器的参数在Equation对象上设置。你要提供硬度和放松度,想这样:

equation.stiffness = 1e8;equation.relaxation = 4;equation.updateSpookParams(timeStep);

你可以把硬度stiffness想象成一个弹簧spring的硬度,这个弹簧给出一个F=-k*x 的力,X代表弹簧的位移。 放松度relaxation和时间步的数量一致,窝们用这个来稳定约束(Relaxation 值越大,接触越柔软)。

ContactEquation(接触方程) 和 FrictionEquation(摩擦方程)是最主要的方程类。这些方程是自动创建的。想改变接触的硬度和放松度,设置ContactMaterial的属性值:

contactMaterial.stiffness = 1e8;contactMaterial.relaxation = 3;contactMaterial.frictionStiffness = 1e8;contactMaterial.frictionRelaxation = 3;

你也可以设置硬度和放松度在Constraint equations。只需遍历它所有的的方程式即可。

The Demo framework(demo的代码框架)

为了能简便的调试p2.js的特性,我们自制了一个渲染库(文件p2.render.js,其实就是游戏引擎pixi.js的包装)。这个库和p2.js库完全独立,所以你可以用自己的渲染库来替换掉它。

也正因为此,墙裂推荐大家去看demo学习,demo在引擎部分的代码和渲染部分的代码是分离的,所以在这里你可以暂时不用管它怎么渲染世界的,只需专心了解p2的特性就能好了。

demo框架在world的右边做了一个交互菜单。你可以用它:

Interact with objects via the mouse (pick and pull)在(pick and pull)模式下,可以用鼠标和物体互动。Create new objects on the fly在非(pick and pull)模式下,可以飞速创建新物体。Pause and play simulation暂停和继续模拟。Manually stepping through the simulation手动地步进通过模拟。Control simulation parameters such as step size, max sub steps, etc.控制模拟参数,比如 step size, max sub steps等等。Change gravity改变重力。Tweak global solver parameters (iterations, stiffness, relaxation, tolerance)改变全局的求解器参数(iterations, stiffness, relaxation, tolerance)。

Limitations(限制)

TODO

References(说明)

p2.js发轫于默奥大学的视觉交互模拟课程成果。如果你想深入了解物理引擎是如何工作的,赶紧钻到课程资料和实验讯息里去吧!

0 0