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(); 
} 
} 
// ... 
} 
基本粒子系统完成
以下的例子里,每帧会发射一个粒子,其位置在画布中间(200,200),发射方向是360度,速率为100,生命为1秒,红色、半径为5象素。
复制代码 代码如下:
 
var ps = new ParticleSystem(); 
var dt = 0.01; 
function sampleDirection() { 
var theta = Math.random() * 2 * Math.PI; 
return new Vector2(Math.cos(theta), Math.sin(theta)); 
} 
function step() { 
ps.emit(new Particle(new Vector2(200, 200), sampleDirection().multiply(100), 1, Color.red, 5)); 
ps.simulate(dt); 
clearCanvas(); 
ps.render(ctx); 
} 
start("basicParticleSystemCanvas", step); 
<button type="button">Run</button> 
<button type="button">Stop</button> 
<table> 
<tbody> 
<tr> 
<td><canvas></canvas></td> 
<td> </td> 
<td valign="top"> 
<h4>修改代码试试看</h4> 
<li>改变发射位置</li> 
<li>向上发射,发射范围在90度内</li> 
<li>改变生命</li> 
<li>改变半径</li> 
<li>每帧发射5个粒子</li> 
</td> 
</tr> 
</tbody> 
</table> 
简单碰撞
为了说明用数值积分相对于分析解的优点,本文在粒子系统上加简单的碰撞。我们想加入一个需求,当粒子碰到长方形室(可设为整个Canvas大小)的内壁,就会碰撞反弹,碰撞是完全弹性的(perfectly elastic collision)。
在程序设计上,我把这功能用回调方式进行。 ParticleSystem类有一个effectors数组,在进行运动学模拟之前,先执行每个effectors对象的apply()函数:
而长方形室就这样实现:
复制代码 代码如下:
// 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; 
}; 
} 
这其实就是当侦测到粒子超出内壁的范围,就反转该方向的速度分量。
此外,这例子的主循环不再每次把整个Canvas清空,而是每帧画一个半透明的黑色长方形,就可以模拟动态模糊(motion blur)的效果。粒子的颜色也是随机从两个颜色中取样。
复制代码 代码如下:
 
var ps = new ParticleSystem(); 
ps.effectors.push(new ChamberBox(0, 0, 400, 400)); // 最重要是多了这语句 
var dt = 0.01; 
function sampleDirection(angle1, angle2) { 
var t = Math.random(); 
var theta = angle1 * t + angle2 * (1 - t); 
return new Vector2(Math.cos(theta), Math.sin(theta)); 
} 
function sampleColor(color1, color2) { 
var t = Math.random(); 
return color1.multiply(t).add(color2.multiply(1 - t)); 
} 
function step() { 
ps.emit(new Particle(new Vector2(200, 200), sampleDirection(Math.PI * 1.75, Math.PI * 2).multiply(250), 3, sampleColor(Color.blue, Color.purple), 5)); 
ps.simulate(dt); 
ctx.fillStyle="rgba(0, 0, 0, 0.1)"; 
ctx.fillRect(0,0,canvas.width,canvas.height); 
ps.render(ctx); 
} 
start("collisionChamberCanvas", step); 
<button type="button">Run</button> 
<button type="button">Stop</button> 
<canvas></canvas> 
互动发射
最后一个例子加入互动功能,在鼠标位置发射粒子,粒子方向是按鼠标移动速度再加上一点噪音(noise)。粒子的大小和生命都加入了随机性。
复制代码 代码如下:
var ps = new ParticleSystem(); 
ps.effectors.push(new ChamberBox(0, 0, 400, 400)); 
var dt = 0.01; 
var oldMousePosition = Vector2.zero, newMousePosition = Vector2.zero; 
function sampleDirection(angle1, angle2) { 
var t = Math.random(); 
var theta = angle1 * t + angle2 * (1 - t); 
return new Vector2(Math.cos(theta), Math.sin(theta)); 
} 
function sampleColor(color1, color2) { 
var t = Math.random(); 
return color1.multiply(t).add(color2.multiply(1 - t)); 
} 
function sampleNumber(value1, value2) { 
var t = Math.random(); 
return value1 * t + value2 * (1 - t); 
} 
function step() { 
var velocity = newMousePosition.subtract(oldMousePosition).multiply(10); 
velocity = velocity.add(sampleDirection(0, Math.PI * 2).multiply(20)); 
var color = sampleColor(Color.red, Color.yellow); 
var life = sampleNumber(1, 2); 
var size = sampleNumber(2, 4); 
ps.emit(new Particle(newMousePosition, velocity, life, color, size)); 
oldMousePosition = newMousePosition; 
ps.simulate(dt); 
ctx.fillStyle="rgba(0, 0, 0, 0.1)"; 
ctx.fillRect(0,0,canvas.width,canvas.height); 
ps.render(ctx); 
} 
start("interactiveEmitCanvas", step); 
canvas.onmousemove = function(e) { 
if (e.layerX || e.layerX == 0) { // Firefox 
e.target.style.position='relative'; 
newMousePosition = new Vector2(e.layerX, e.layerY); 
} 
else 
newMousePosition = new Vector2(e.offsetX, e.offsetY); 
}; 
<button type="button">Run</button> 
<button type="button">Stop</button> 
<canvas></canvas> 
总结
本文介绍了最简单的运动学模拟,使用欧拉方法作数值积分,并以此法去实现一个有简单碰撞的粒子系统。本文的精华其实只有两条简单公式(只有两个加数和两个乘数),希望让读者明白,其实物理模拟可以很简单。虽然本文的例子是在二维空间,但这例子能扩展至三维空间,只须把Vector2换成Vector3。本文完整源代码可下载。
续篇会谈及在此基础上加入其他物理现象,有机会再加入其他物理模拟课题。希望各位支持,并给本人更多意见。
您可能感兴趣的文章:
