浅析用粒子系统打造特效

1、从最简单的粒子系统说起

  粒子系统在做特效方面颇有一技之长,常常短短的几行代码便能带来意想不到的效果。接下来就从一个最简单的例子分析怎样用粒子系统打造特效。

  直接盗用了06wj的代码,短短的20行代码便打造了一个喷泉效果。看代码:

 1 var ctx = document.body.appendChild(document.createElement("canvas")).getContext('2d');
 2 var i, j, k, a = [], w = ctx.canvas.width = 550, h = ctx.canvas.height = 240, r = Math.random, p = Math.PI;
 3 setInterval(function(){
 4     ctx.fillStyle = "rgba(0, 0, 0, .5)";
 5     ctx.fillRect(0, 0, w, h);
 6     i = 10;
 7     while(i--){a.push({x:w/2,y:h/6,r:r()*3,c:"#fff",t:0,vx:r()*10-5,vy:r()*-5})}
 8     for(i = a.length-1;i >= 0;i--){
 9         k = a[i];
10         ctx.fillStyle=k.c;
11         ctx.beginPath();
12         ctx.arc(k.x, k.y, k.r, 0, p*2)
13         ctx.fill();
14         k.x+=k.vx;
15         k.y+=k.vy;
16         k.vy+=.2;
17         k.r -= .01;
18         if(k.y>h){k.y=h;k.vy*=-.5;k.r+=.005;}
19         k.r < 0 && a.splice(i, 1);
20     }
21 }, 1000/60);

  这是一个典型的面向过程的js编程,简单地说就是构造n个粒子,然后设置定时器每1000/60毫秒对n个粒子的参数进行变化,然后再在画布上画出来。

  我们来看粒子的属性,有坐标、半径、颜色,因为要符合牛顿运动定律,当然还定义了速度。代码第7行是粒子的初始化,8~20行就是重绘以及属性值变化的过程,其中的第16行就是加速度的体现。代码不难,我相信稍微研究下应该都能懂,的确,粒子系统简单的说就是重复画圆,你的粒子系统入门了。

 

2、运动学基础

  提到粒子系统,不得不提到运动学,虽然很多粒子系统的运动和运动学无关(或许h5的游戏和运动学更有关系),但是上面的例子告诉我们想要玩转粒子系统,稍微学点运动学还是很有必要的。因为有篇神文已经作了详细的介绍,这里就不加多说,具体请check用JavaScript玩转游戏物理(一)运动学模拟与粒子系统

  稍微提两句,其实也没必要像搞那么复杂,其实就是速度分解到x和y轴,x轴作的是匀速直线运动,直接在粒子的x坐标上每帧加减相应的数量即可;而y轴上作的是加速度一定的运动,总过程就是一个高中常见的抛物运动。还是看上述代码,14行就是x轴匀速运动的体现,而15行和16行则是y轴上加速度一定的直线运动,只是06wj把加速度加到了速度上,这样方便,这也是神文说的最优解。

 

3、矢量类

  从面向过程到面向对象,矢量类必不可少。坐标和速度都可以用到矢量类。

  1 function Vector(x, y) {
  2   this.x = x || 0;
  3   this.y = y || 0;
  4 }
  5 
  6 Vector.prototype.reset = function(x, y) {
  7   this.x = x;
  8   this.y = y;
  9 }
 10 
 11 Vector.prototype.getClone = function() {
 12   return new Vector(this.x, this.y);
 13 }
 14 
 15 //截断
 16 Vector.prototype.cut = function(max) {
 17   var r = Math.min(max, this.getLength());
 18   this.setLength(r);  
 19 }
 20 
 21 //截断
 22 Vector.prototype.cutNew = function(max) {
 23   var r = Math.min(max, this.getLength());
 24   var v = this.getClone();
 25   v.setLength(r);
 26   return v; 
 27 }
 28 
 29 //  比较是否相等
 30 Vector.prototype.equals = function(v) {
 31   return (this.x == v.x && this.y == v.y);
 32 }
 33 
 34 //  加法,改变当前对象
 35 Vector.prototype.plus = function(v) {
 36   this.x += v.x;
 37   this.y += v.y;  
 38 }
 39 
 40 //  求和,返回新对象
 41 Vector.prototype.plusNew = function(v) {
 42   return new Vector(this.x + v.x,this.y + v.y);
 43 }
 44 
 45 //  减法,改变当前对象
 46 Vector.prototype.minus = function(v) {
 47   this.x -= v.x;
 48   this.y -= v.y;
 49 }
 50 
 51 //  求差,返回新对象
 52 Vector.prototype.minusNew = function(v) {
 53   return new Vector(this.x - v.x, this.y - v.y);
 54 }
 55 ----
 56 //  求逆,改变当前对象
 57 Vector.prototype.negate = function() {
 58   this.x = -this.x;
 59   this.y = -this.y;
 60 }
 61 
 62 //  求逆,返回新对象
 63 Vector.prototype.negateNew = function() {
 64   return new Vector(-this.x, -this.y);
 65 }
 66 
 67 //  缩放,改变当前对象
 68 Vector.prototype.scale = function(s) {
 69   this.x *= s;
 70   this.y *= s;
 71 }
 72 
 73 //  缩放,返回新对象
 74 Vector.prototype.scaleNew = function(s) {
 75   return new Vector(this.x * s, this.y * s);
 76 }
 77 
 78 Vector.prototype.getLength = function() {
 79   return Math.sqrt(this.x * this.x + this.y * this.y);
 80 }
 81  
 82 //  设置向量长度
 83 Vector.prototype.setLength = function(len) {
 84   var r = this.getLength();
 85   if (r) this.scale (len / r);
 86   else this.x = len;
 87 }
 88 
 89 //  获取向量角度
 90 Vector.prototype.getAngle = function() {
 91   return Math.atan2(this.y, this.x);
 92 }
 93 
 94 //  设置向量角度
 95 Vector.prototype.setAngle = function(ang) {
 96   var r = this.getLength();
 97   this.x = r * Math.cos (ang);
 98   this.y = r * Math.sin (ang);
 99 }
100 
101 //  向量旋转,改变当前对象
102 Vector.prototype.rotate = function() {  
103   var cos, sin;
104   var a = arguments;
105   if(a.length == 1) {
106     cos = Math.cos(a[0]);
107     sin = Math.sin(a[0]);
108   } else {
109     cos = a[0];
110     sin = a[1];
111   }
112   var rx = this.x * cos - this.y * sin;
113   var ry = this.x * sin + this.y * cos;
114   this.x = rx;
115   this.y = ry;
116 } 
117 
118 //  向量旋转,返回新对象
119 Vector.prototype.rotateNew = function(ang) {
120   var v=new Vector(this.x,this.y);
121   v.rotate(ang);
122   return v;
123 }
124 
125 //  点积
126 Vector.prototype.dot = function(v) {
127   return this.x * v.x + this.y * v.y;
128 }
129 
130 //  法向量
131 Vector.prototype.getNormal = function() {
132   return new Vector(-this.y, this.x);
133 }
134 
135 //  垂直验证
136 Vector.prototype.isPerpTo = function(v) {
137   return (this.dot (v) == 0);
138 }
139 
140 //  向量的夹角
141 Vector.prototype.angleBetween = function(v) {
142   var dp = this.dot (v); 
143   var cosAngle = dp / (this.getLength() * v.getLength());
144   return Math.acos (cosAngle); 
145 }
vector.js

    这个矢量类还是蛮全的,操作也算是应有尽有了,不过基本上用不着,常用的也就那么几个,初始化、重置、缩放,就那么几个而已。

  我们还得实现一个粒子类,这个类的实现因人而异,属性变量随意设置,但是xy坐标是必不可少的。

  这是我在另一份代码中实现的粒子类,这是一个起点任意,但是有明确终点的粒子:

 1 function Particle(x, y, garden) {
 2   this.garden = garden;
 3   var xx = (Math.random() * this.garden.canvas.width);
 4   var yy = (Math.random() * this.garden.canvas.height);
 5   this.target = new Vector(x, y);
 6   this.pos = new Vector(x, y);
 7   this.pos.reset(xx, yy)
 8   this.angle = 0;
 9   this.angleV = -0.5 + Math.random();
10   this.a = new Vector();
11   this.color = 1; 
12 }

 

4、效果渲染

  有了一大堆的粒子,如何能产生效果?产生了一个效果,又如何能更加高效和逼真?

  以上述例子为例,我们发现粒子有阴影变化。透明度的调节在粒子的效果渲染中起到了很大的作用。通常我们通过背景色的rgba或者直接调整globalAlpha,使背景半透明,从而达到粒子阴影的效果,而透明度的大小直接影响粒子阴影的效果。这实际上是个trick,因为如果时间不是很短,背景就是灰色。

  另外一方面,粒子系统的效果渲染除了控制坐标速度半径等因素,还有一个关键的因素是颜色。

  js简单取色代码:

var color = '#' + ('00000' + parseInt(Math.random() * 0xffffff).toString(16)).slice(-6);

  这是最简单的取色代码,显然,满足不了粒子系统中所需的炫酷的颜色系统。

  粒子系统中常用的颜色系统是rgb或者rgba,通过设置各个的值来组成颜色。通过rgb,可以确定两个值,通过对第三个值的渐变,从而达到颜色的渐变;而通过rgba,更可以通过对a的设置,使得粒子“消失”。

  另外一种hls,可以通过对第一个值的设置,达到颜色渐变的效果。霓虹灯效果...

1 this.color += 4;
2 this.color = this.color % 360;
3 this.garden.ctx.fillStyle = "hsl(" + this.color +",50%,50%)"

  hls第二个第三个参数分别是饱和度和亮度,不加多述,可以查阅相关资料。

  另外,在粒子数组中删除粒子:

1 function kill(index) {
2   // 效率低
3   particles.splice(index, 1);
4 
5   // 效率高
6   particles[index] = particles[particles.length - 1];
7   particles.pop();
8 }

 

5、创意

  很多时候,缺少的不是技术,而是创意。

  粒子系统有很多普遍的创意,就是大家都在玩,但却可以玩出不同的花样。比如构造“字”。

  花丛文字效果也是用“字”来构造的创意,把字先“写”在画布上,然后通过颜色的渐变画线,出现效果;而粒子同样可以做这种事情,而且不逞多让。

  构造的字是有讲究的,必须跟背景一个色,不然就显示出来了你懂的...通过字的alpha值找到字所在的像素点,接着就可以打造特效。很多时候,如果字太大,那么像素点就太多了,字太小,看不见,我们需要将字“放大”,一个像素点对应n个像素点:

var data = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height).data;
var length = data.length;
for (var i = 0, wl = this.canvas.width * 4; i < length; i += 4) {
  if (data[i + 3]) {
    var x = (i % wl) / 4;
    var y = parseInt(i / wl)
    this.textPoints.push([this.offsetX + x * this.textSize, this.offsetY + y * this.textSize]);
  }
}

  接下去的构造就需要自己的匠心独具了...

  我们还可以构造爱心。

  Our Love Story 花丛爱心效果其实是可以看做粒子特效的,可以把一朵朵花看成粒子;用爱心函数可以构造很多有趣的效果,这里简单介绍下两个爱心函数。

(x^2 + y^2 - 1)^3 - x^2*y^3 = 0

  一般这个函数用来判断爱心内的点,而不是动态构造爱心。而这个表达式的解所构成的图如下:

  一个标准的以(0,0)为心以1为半径的爱心。

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
ctx.fillStyle = 'red';
for(var i = 0; i <= 1000; i++)
for(var j = 0; j <= 500; j++) {
  // (x^2 + y^2 - 1)^3 - x^2*y^3 < 0
  // 以(200,200)为心,以100为半径
  var x = i - 200;
  var y = 200 - j;
  var r = 100;
  var ans = Math.pow((x/r*x/r+y/r*y/r-1),3)-x*x*y*y*y/r/r/r/r/r;
  if(ans < 0) {
    ctx.beginPath();
    // 注意是i&j 不是x&y
    ctx.arc(i, j, 1, 0, Math.PI * 2, true);
    ctx.fill();
  }
}

  效果图:

  另一个函数则重点在于绘制:

x = (16 * Math.pow(Math.sin(t), 3));
y = (13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t));

  原始的函数所绘的图和上面那个差不多,关键在于调整参数。

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var offsetX = 200;
var offsetY = 200;
ctx.fillStyle = 'red';

for(var i = 10; i <= 30; i += 0.2) {
  var ans = getHeartPoint(i);
  ctx.beginPath();
  ctx.arc(ans[0], ans[1], 1, 0, Math.PI * 2, true);
  ctx.fill();
}

function getHeartPoint(angle) {
   var t = angle / Math.PI;
   var sx = 10;
   var sy = -10;
   var x = sx * (16 * Math.pow(Math.sin(t), 3));
   var y = sy * (13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t));
   return new Array(offsetX + x, offsetY + y);
}

  同样的以(200,200)为心,通过offsetX和offsetY调整中心位置坐标;通过sx和sy参数调整爱心的“大小“。例子的图:

  如果需要改变爱心的角度,可以用rotate函数。

  试想,如果粒子在经过一番变化后最终汇成爱心型,是不是也会有种惊喜呢?

 

6、总结

  对于粒子系统特效的初步介绍就到这里了,总的来说,粒子是小事,创意是大事~

  

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值