javascript
用JavaScript玩转游戏物理(一)运动学模拟与粒子系统
系列簡介
也許,三百年前的艾薩克·牛頓爵士(Sir Issac Newton, 1643-1727)并沒幻想過,物理學(xué)廣泛地應(yīng)用在今天許多游戲、動畫中。為什么在這些應(yīng)用中要使用物理學(xué)?筆者認(rèn)為,自我們出生以來,一直感受著物理世界的規(guī)律,意識到物體在這世界是如何"正常移動",例如射球時球?yàn)閽佄锞€(自旋的球可能會做成弧線球) 、石子系在一根線的末端會以固定頻率擺動等等。要讓游戲或動畫中的物體有真實(shí)感,其移動方式就要符合我們對"正常移動"的預(yù)期。
今天的游戲動畫應(yīng)用了多種物理模擬技術(shù),例如運(yùn)動學(xué)模擬(kinematics simulation)、剛體動力學(xué)模擬(rigid body dynamics simulation)、繩子/布料模擬(string/cloth simulation)、柔體動力學(xué)模擬(soft body dynamics simulation)、流體動力學(xué)模擬(fluid dynamics simulation)等等。另外碰撞偵測(collision detection)是許多模擬系統(tǒng)里所需的。
本系列希望能介紹一些這方面最基礎(chǔ)的知識,繼續(xù)使用JavaScript做例子,以即時互動方式體驗(yàn)。
本文簡介
作為系列第一篇,本文介紹最簡單的運(yùn)動學(xué)模擬,只有兩條非常簡單的公式。運(yùn)動學(xué)模擬可以用來模擬很多物體運(yùn)動(例如馬里奧的跳躍、炮彈等),本文將會配合粒子系統(tǒng)做出一些視覺特效(粒子系統(tǒng)其實(shí)也可以用來做游戲的玩法,而不單是視覺特效)。
運(yùn)動學(xué)模擬
運(yùn)動學(xué)(kinematics)研究物體的移動,和動力學(xué)(dynamics)不同之處,在于運(yùn)動學(xué)不考慮物體的質(zhì)量(mass)/轉(zhuǎn)動慣量(moment of inertia),以及不考慮加之于物體的力(force )和力矩(torque)。
我們先回憶牛頓第一運(yùn)動定律:
當(dāng)物體不受外力作用,或所受合力為零時,原先靜止者恒靜止,原先運(yùn)動者恒沿著直線作等速度運(yùn)動。該定律又稱為「慣性定律」。此定律指出,每個物體除了其位置(position)外,還有一個線性速度(linear velocity)的狀態(tài)。然而,只模擬不受力影響的物體并不有趣。撇開力的概念,我們可以用線性加速度(linear acceleration)去影響物體的運(yùn)動。例如,要計(jì)算一個自由落體在任意時間t的y軸座標(biāo),可以使用以下的分析解(analytical solution):
當(dāng)中,和分別是t=0時的y軸起始座標(biāo)和速度,而g則是重力加速度(gravitational acceleration)。
這分析解雖然簡單,但是有一些缺點(diǎn),例如g是常數(shù),在模擬過程中不能改變;另外,當(dāng)物體遇到障礙物,產(chǎn)生碰撞時,這公式也很難處理這種不連續(xù)性(discontinuity) 。
在計(jì)算機(jī)模擬中,通常需要計(jì)算連續(xù)的物體狀態(tài)。用游戲的用語,就是計(jì)算第一幀的狀態(tài)、第二幀的狀態(tài)等等。設(shè)物體在任意時間t的狀態(tài):位置矢量為、速度矢量為、加速度矢量為。我們希望從時間的狀態(tài),計(jì)算下一個模擬時間的狀態(tài)。最簡單的方法,是采用歐拉方法(Euler method)作數(shù)值積分(numerical integration):
歐拉方法非常簡單,但有準(zhǔn)確度和穩(wěn)定性問題,本文會先忽略這些問題。本文的例子采用二維空間,我們先實(shí)現(xiàn)一個JavaScript二維矢量類:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // Vector2.js Vector2 = function(x, y) { this.x = x; this.y = y; }; Vector2.prototype = { ????copy : function() { return?new?Vector2(this.x, this.y); }, ????length : function() { return?Math.sqrt(this.x * this.x + this.y * this.y); }, ????sqrLength : function() { return?this.x * this.x + this.y * this.y; }, ????normalize : function() { var?inv = 1/this.length(); return?new?Vector2(this.x * inv, this.y * inv); }, ????negate : function() { return?new?Vector2(-this.x, -this.y); }, ????add : function(v) { return?new?Vector2(this.x + v.x, this.y + v.y); }, ????subtract : function(v) { return?new?Vector2(this.x - v.x, this.y - v.y); }, ????multiply : function(f) { return?new?Vector2(this.x * f, this.y * f); }, ????divide : function(f) { var?invf = 1/f; return?new?Vector2(this.x * invf, this.y * invf); }, ????dot : function(v) { return?this.x * v.x + this.y * v.y; } }; Vector2.zero = new?Vector2(0, 0); |
然后,就可以用HTML5 Canvas去描繪模擬的過程:
Run ? Stop ? Clear ?
| ? | 修改代碼試試看 |
這程序的核心就是step()函數(shù)頭兩行代碼。很簡單吧?
粒子系統(tǒng)
粒子系統(tǒng)(particle system)是圖形里常用的特效。粒子系統(tǒng)可應(yīng)用運(yùn)動學(xué)模擬來做到很多不同的效果。粒子系統(tǒng)在游戲和動畫中,常常會用來做雨點(diǎn)、火花、煙、爆炸等等不同的視覺效果。有時候,也會做出一些游戲性相關(guān)的功能,例如敵人被打敗后會發(fā)出一些閃光,主角可以把它們吸收。
粒子的定義
粒子系統(tǒng)模擬大量的粒子,并通常用某些方法把粒子渲染。粒子通常有以下特性:
以下是本文例子里實(shí)現(xiàn)的粒子類:
| 12345678910 | // Particle.jsParticle = function(position, velocity, life, color, size) {????this.position = position;????this.velocity = velocity;????this.acceleration = Vector2.zero;????this.age = 0;????this.life = life;????this.color = color;????this.size = size;}; |
游戲循環(huán)
粒子系統(tǒng)通常可分為三個周期:
在游戲循環(huán)(game loop)中,需要對每個粒子系統(tǒng)執(zhí)行以上的三個步驟。
生與死
在本文的例子里,用一個JavaScript數(shù)組particles儲存所有活的粒子。產(chǎn)生一個粒子只是把它加到數(shù)組末端。代碼片段如下:
| 123456789101112131415161718 | //ParticleSystem.jsfunction?ParticleSystem() {????// Private fields????var?that = this;????var?particles = new?Array();????// Public fields????this.gravity = new?Vector2(0, 100);????this.effectors = new?Array();????// Public methods?????????????this.emit = function(particle) {????????particles.push(particle);????};????// ...} |
粒子在初始化時,年齡(age)設(shè)為零,生命(life)則是固定的。年齡和生命的單位都是秒。每個模擬步,都會把粒子老化,即是把年齡增加,年齡超過生命,就會死亡。代碼片段如下:
| 12345678910111213141516171819202122232425262728293031 | function?ParticleSystem() {????// ... ????this.simulate = function(dt) {????????aging(dt);????????applyGravity();????????applyEffectors();????????kinematics(dt);????};?????????// ...????// Private methods?????????function?aging(dt) {????????for?(var?i = 0; i < particles.length; ) {????????????var?p = particles[i];????????????p.age += dt;????????????if?(p.age >= p.life)????????????????kill(i);????????????else????????????????i++;????????}????}????function?kill(index) {????????if?(particles.length > 1)????????????particles[index] = particles[particles.length - 1];????????particles.pop();????}????// ...} |
在函數(shù)kill()里,用了一個技巧。因?yàn)榱W釉跀?shù)組里的次序并不重要,要刪除中間一個粒子,只需要復(fù)制最末的粒子到那個元素,并用pop()移除最末的粒子就可以。這通常比直接刪除數(shù)組中間的元素快(在C++中使用數(shù)組或std::vector亦是)。
運(yùn)動學(xué)模擬
把本文最重要的兩句運(yùn)動學(xué)模擬代碼套用至所有粒子就可以。另外,每次模擬會先把引力加速度寫入粒子的加速度。這樣做是為了將來可以每次改變加速度(續(xù)篇會談這方面)。
| 12345678910111213141516 | function?ParticleSystem() {????// ...????function?applyGravity() {????????for?(var?i in?particles)????????????particles[i].acceleration = that.gravity;????}????function?kinematics(dt) {????????for?(var?i in?particles) {????????????var?p = particles[i];????????????p.position = p.position.add(p.velocity.multiply(dt));????????????p.velocity = p.velocity.add(p.acceleration.multiply(dt));????????}????}????// ...} |
渲染
粒子可以用很多不同方式渲染,例如用圓形、線段(當(dāng)前位置和之前位置)、影像、精靈等等。本文采用圓形,并按年齡生命比來控制圓形的透明度,代碼片段如下:
| 12345678910111213141516171819 | function?ParticleSystem() {????// ...????this.render = function(ctx) {????????for?(var?i in?particles) {????????????var?p = particles[i];????????????var?alpha = 1 - p.age / p.life;????????????ctx.fillStyle = "rgba("????????????????+ Math.floor(p.color.r * 255) + ","????????????????+ Math.floor(p.color.g * 255) + ","????????????????+ Math.floor(p.color.b * 255) + ","????????????????+ alpha.toFixed(2) + ")";????????????ctx.beginPath();????????????ctx.arc(p.position.x, p.position.y, p.size, 0, Math.PI * 2, true);????????????ctx.closePath();????????????ctx.fill();????????}????}????// ...} |
基本粒子系統(tǒng)完成
以下的例子里,每幀會發(fā)射一個粒子,其位置在畫布中間(200,200),發(fā)射方向是360度,速率為100,生命為1秒,紅色、半徑為5象素。
Run ? Stop ?
| ? | 修改代碼試試看 |
簡單碰撞
為了說明用數(shù)值積分相對于分析解的優(yōu)點(diǎn),本文在粒子系統(tǒng)上加簡單的碰撞。我們想加入一個需求,當(dāng)粒子碰到長方形室(可設(shè)為整個Canvas大小)的內(nèi)壁,就會碰撞反彈,碰撞是完全彈性的(perfectly elastic collision)。
在程序設(shè)計(jì)上,我把這功能用回調(diào)方式進(jìn)行。 ParticleSystem類有一個effectors數(shù)組,在進(jìn)行運(yùn)動學(xué)模擬之前,先執(zhí)行每個effectors對象的apply()函數(shù):
而長方形室就這樣實(shí)現(xiàn):
| 1 2 3 4 5 6 7 8 9 10 | // ChamberBox.js function?ChamberBox(x1, y1, x2, y2) { ????this.apply = function(particle) { ????????if?(particle.position.x - particle.size < x1 || particle.position.x + particle.size > x2) ????????????particle.velocity.x = -particle.velocity.x; ????????if?(particle.position.y - particle.size < y1 || particle.position.y + particle.size > y2) ????????????particle.velocity.y = -particle.velocity.y; ????}; } |
這其實(shí)就是當(dāng)偵測到粒子超出內(nèi)壁的范圍,就反轉(zhuǎn)該方向的速度分量。
此外,這例子的主循環(huán)不再每次把整個Canvas清空,而是每幀畫一個半透明的黑色長方形,就可以模擬動態(tài)模糊(motion blur)的效果。粒子的顏色也是隨機(jī)從兩個顏色中取樣。
Run ? Stop ?
互動發(fā)射
最后一個例子加入互動功能,在鼠標(biāo)位置發(fā)射粒子,粒子方向是按鼠標(biāo)移動速度再加上一點(diǎn)噪音(noise)。粒子的大小和生命都加入了隨機(jī)性。
Run ? Stop ?
總結(jié)
本文介紹了最簡單的運(yùn)動學(xué)模擬,使用歐拉方法作數(shù)值積分,并以此法去實(shí)現(xiàn)一個有簡單碰撞的粒子系統(tǒng)。本文的精華其實(shí)只有兩條簡單公式(只有兩個加數(shù)和兩個乘數(shù)),希望讓讀者明白,其實(shí)物理模擬可以很簡單。雖然本文的例子是在二維空間,但這例子能擴(kuò)展至三維空間,只須把Vector2換成Vector3。本文完整源代碼可下載。
續(xù)篇會談及在此基礎(chǔ)上加入其他物理現(xiàn)象,有機(jī)會再加入其他物理模擬課題。希望各位支持,并給本人更多意見。
from:?http://www.cnblogs.com/miloyip/archive/2010/06/14/Kinematics_ParticleSystem.html
總結(jié)
以上是生活随笔為你收集整理的用JavaScript玩转游戏物理(一)运动学模拟与粒子系统的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C# vs C++ 全局照明渲染性能比试
- 下一篇: 那些计算机界的伟大女性