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行就是加速度的体现。代码不难,我相信稍微研究下应该都能懂,的确,粒子系统简单的说就是重复画圆,你的粒子系统入门了。
提到粒子系统,不得不提到运动学,虽然很多粒子系统的运动和运动学无关(或许h5的游戏和运动学更有关系),但是上面的例子告诉我们想要玩转粒子系统,稍微学点运动学还是很有必要的。因为有篇神文已经作了详细的介绍,这里就不加多说,具体请check用JavaScript玩转游戏物理(一)运动学模拟与粒子系统
稍微提两句,其实也没必要像搞那么复杂,其实就是速度分解到x和y轴,x轴作的是匀速直线运动,直接在粒子的x坐标上每帧加减相应的数量即可;而y轴上作的是加速度一定的运动,总过程就是一个高中常见的抛物运动。还是看上述代码,14行就是x轴匀速运动的体现,而15行和16行则是y轴上加速度一定的直线运动,只是06wj把加速度加到了速度上,这样方便,这也是神文说的最优解。
从面向过程到面向对象,矢量类必不可少。坐标和速度都可以用到矢量类。
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 }
这个矢量类还是蛮全的,操作也算是应有尽有了,不过基本上用不着,常用的也就那么几个,初始化、重置、缩放,就那么几个而已。
我们还得实现一个粒子类,这个类的实现因人而异,属性变量随意设置,但是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 }
有了一大堆的粒子,如何能产生效果?产生了一个效果,又如何能更加高效和逼真?
以上述例子为例,我们发现粒子有阴影变化。透明度的调节在粒子的效果渲染中起到了很大的作用。通常我们通过背景色的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 }
很多时候,缺少的不是技术,而是创意。
粒子系统有很多普遍的创意,就是大家都在玩,但却可以玩出不同的花样。比如构造“字”。
前面有篇花丛效果文字代码重写也是用“字”来构造的创意,把字先“写”在画布上,然后通过颜色的渐变画线,出现效果;而粒子同样可以做这种事情,而且不逞多让。
构造的字是有讲究的,必须跟背景一个色,不然就显示出来了你懂的...通过字的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函数。
试想,如果粒子在经过一番变化后最终汇成爱心型,是不是也会有种惊喜呢?
对于粒子系统特效的初步介绍就到这里了,总的来说,粒子是小事,创意是大事~