1. 程式人生 > >web物理引擎p2.js入門手冊

web物理引擎p2.js入門手冊

最近在學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內容導航

歡迎使用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: 0
Circle y position: 10
Circle angle: 0
Circle x position: 0
Circle y position: 9
Circle angle: 0
...
Circle x position: 0
Circle y position: 0.5
Circle 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 group
var PLAYER = Math.pow(2,0),
    ENEMY =  Math.pow(2,1),
    GROUND = Math.pow(2,2)

// Put shapes into their groups
player1Shape.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
});
// Dynamically
body.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 loop
setInterval(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;     // x
world.gravity[1] = -9.82; // y
// or:
p2.vec2.set(world.gravity,0,-9.82);

有時我們不想給所有的剛體運用重力,這時我們應該turn off掉全域性重力了,然後我們自己來給剛體施力。

// Turn off global gravity
world.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發軔于默奧大學的視覺互動模擬課程成果。如果你想深入瞭解物理引擎是如何工作的,趕緊鑽到課程資料和實驗訊息裡去吧!