WebGL教程第二章:神奇的人物粒子系统

第二章 神奇的人物粒子系统


概要:

  1. 粒子系统中的粒子可以分布在模型的表面,形成惊叹的效果。

  2. 粒子可以按照非规律的方式运动,不一定是直线,斜线。

  3. 学会由模型转换为粒子。


这一节要做什么?创意何在?

这一节课,我们要做什么,我们的创意是什么?随便做一个粒子系统,那是非常小儿科的一件事情,但在这里,我们不随便做一件。青春可以挥霍,但是不能挥霍在一些无意义的事情上,那等于浪费生命。

我们现在要做一个人物粒子系统,什么叫人物粒子系统,这也是我给起的一个名字。就是在一个人物模型上,显示粒子,让粒子的集合看起来像一个人物。这很神奇,因为我们在编程中并不常见,所以比较难用文字解释,直接来看一下图吧:

Image

仔细看一下里面的人物都是由粒子组成的哦。暗红色的地板也是粒子哦。除了显示粒子人物,我们还能让人物瞬间坍塌,效果非常迷人。你可以在【中级教程\chapter1\1-1.html】中看到这节课的代码


实现思路

要完成上面精美的案例,仔细思考,大概有如下的步骤:

  1. 构件人形粒子:传统的方式是将粒子固定在某一个初始点,然后给每个粒子一个不同的速度,让粒子动起来形成不同的效果。显然,要手动去设置粒子的初始位置,让它看起来像个人,这是很困难的。所以这里,我们从一个人物3D模型中,取得点,由这些点构成粒子系统,所以最开始看到的粒子,构成了人物的形状。
  2. 为粒子染色:就是为粒子系统中每一个粒子赋一种颜色,为了方便,我们将一个人物赋予一种颜色。当然,你也可以为每个粒子赋予不同的色彩,这样看起来更cool。
  3. 让粒子动起来:为了实现例子中的效果,我们使用了缓动技术,让每一个粒子随着时间运动起来,最终实现了案例中的效果。(请先看一下案例中的效果)
  4. 让场景旋转起来。为了从多个方向观看场景,让场景看上去更酷,这里,我们让场景旋转了起来。

好了,这就是实现本例的所有重点了。下面的一些小节,我们就来详细的讲解这些鲜为人知的技术。


加载器

要完成第一步构件人形粒子,就必须要先加载模型,然后从模型中提取出点来,构成粒子。那么,我们先来讲一讲加载器。 在 Threejs 中,加载的工作由一系列的类来完成,最基本的加载类是 THREE.Loader(),它的源码在 src/Loader/Load.js 中。这是大多数加载类的基类。 Loader 类主要抽象了一些加载的共性,这些共性包括:

  1. 加载过程中是否显示加载状态
  2. 定义了由谁来显示加载信息,一般是一个div来显示
  3. 加载时执行的函数onLoadStart,表示开始加载。
  4. 加载过程中,不断调用的函数onLoadProgress,例如加载了30%调用一次,加载了40%调用一次。在这个函数中,可以监视加载的进度。
  5. 加载完成后调用的函数onLoadComplete 。

接下来,我们简单的分析一下构造函数,请打开 src/Loader/Load.js 文件参考学习。


构造函数

加载的构造函数如下:

THREE.Loader = function ( showStatus )

这个函数只有一个参数 showStatus,是一个 boolean 变量,表示在加载的过程中,是否显示加载进度信息。如果 showStatus为true,那么 THREE.Loader 会自己创建一个 div ,并在这个 div 上显示加载过程中的状态信息。 另外,也可以为这个函数传递一个 null,或者什么都不传递。我们可以这样调用这个方法来构件一个加载类:

Var loader = new THREE.Loader( );

添加显示加载信息的div

上面如果 showStatus为true,那么会调用 addStatusElement 函数来创建一个 div。 我们来分析一下这个函数,它的函数原型是:

addStatusElement: function ()

这个函数没有参数,它定义了一个 div,并且使用绝对定位到页面的右上角,默认情况下会显示“ Loading ...”字样。它的源码如下:

addStatusElement: function () {
		var e = document.createElement( "div" );	// 创建一个div
		e.style.position = "absolute";		// 将这个div使用绝对定位
		e.style.right = "0px";	// 将其放在右上角
		e.style.top = "0px";
		e.style.fontSize = "0.8em";	// 字体为0.8em
		e.style.textAlign = "left";	// 文本左对齐
		e.style.background = "rgba(0,0,0,0.25)";	// 背景色为透明的黑色
		e.style.color = "#fff";	// 字体颜色为白色
		e.style.width = "120px";	// 整个div的宽度为120像素
		e.style.padding = "0.5em 0.5em 0.5em 0.5em";
		e.style.zIndex = 1000;	// 显示在第1000层,基本上是最上层了。
		e.innerHTML = "Loading ...";	// 默认显示的文字为Loading
		return e;
}

Ok,这个函数的解释,在注释中已经很清楚了,这里就不再讲了。Loader 中的其他的函数我们暂时用不到,这里先不讲了。


Json加载器

前面讲了加载模型的基类,这一节我们讲加载具体一个模型文件的类。我们这里需要一个加载 Json 文件格式的加载器,因为我们打算把模型数据以Json格式存储在服务器上,为什么用json,而不是用其他格式呢,例如xml文件格式呢。这是因为json文件格式比较小的原因,而且javascript解析起来,也比较方便。 (如果不知道 Json 是什么格式,那么可以参考一下这个网址的文章之后再回来继续学习。http://zh.wikipedia.org/zh-cn/JSON)

本课用到了两个加载器,他们都派生于 Loader 类,如图:

Image


JSON加载基类JSONLoader

在本案例中,json 文件中存放了一个模型的顶点、颜色、法向量等信息。 加载Json的加载器叫 THREE.JSONLoader,它派生于 THREE.Loader,所以具有 Loader 的所有属性和方法。这个类你可以在 src/loaders/JSONLoader.js 文件中找到。 THREE.JSONLoader 最主要也是最方便的一个方法是 load 函数,主要是从服务器加载数据,它的原型如下所示:

THREE.JSONLoader.prototype.load = function ( url, callback, texturePath )
  1. 第一个参数是url地址:它是json文件的地址,这个参数是必须的,例如 http://hewebgl.com/json/test.js
  2. 第二个参数callback:一看就知道它是一个回调函数,当异步加载完后执行。注意是异步加载,不是同步加载。
  3. 第三个参数texturePath:这是纹理的路径,可以没有这个参数,那么函数会在当前路径下寻找一个默认的纹理。

在讲 JSONLoader 的内部原理之前,我们先看看 json 模型的文件格式。


json模型的文件格式

打开 【中级教程\chapter1\obj\ terrain.js】 这个 js 文件,这是一个地形的文件。它表示的是我们的效果图中的红色的由一处处红点组成的地板。terrain.js 文件的主要部分如下所示:

  {
	    "metadata": { "formatVersion" : 3 },
	    "scale" : 10.000000,
	    "materials": [	{
		"DbgColor" : 15658734,
		"DbgIndex" : 0,
		"DbgName" : "default",
		"vertexColors" : false
		}],
	"vertices": 
 [-2812,165,3000,-2812,283,2812,-3000,241,2812,-3000,164,3000,-26
	25,219,3000,-2625,299,2812,-2437,310,3000,-2437,315,2812,...]
	}
  • 第02行,表示该文件的元数据,就是描述文件本身信息的数据。"formatVersion" : 3表示该json模型文件的格式版本是3。
  • 第03行,表示模型应该放大10倍。
  • 第05行,表示模型的颜色。
  • 第08行,表示模型的每一个顶点时候使用顶点的颜色,这里是false,表示不使用顶点的颜色。那么每个顶点的颜色就应该取DbgColor的颜色了。
  • 第10到12行,这里是模型的顶点的一个数组,这里其实有很多行,因为篇幅的原因,我用最后的省略号代替了若干行。Vertices中每三个数字表示一个顶点,这些顶点都是有建模工具做出来的。

json模型的解析

JSONLoader派生于Loader,所以其加载模型的过程是一样的。这里,我们主要讲一讲当 json 格式的文件从服务器中读取到本地浏览器之后,转为本地的 json 数据后,怎么从 json 数据转换为 geometry 对象。这个过程由 createModel 函数实现,请定位到这个函数,我们一起来,学习一下。

THREE.JSONLoader.prototype.createModel = function ( json, callback, 
texturePath ) {
	var scope = this,
	geometry = new THREE.Geometry(),
	scale = ( json.scale !== undefined ) ? 1.0 / json.scale : 1.0;
	parseModel( scale );
	parseSkin();
	parseMorphing( scale );
	geometry.computeCentroids();
	geometry.computeFaceNormals();
}

下面解释一下上面的代码:

  • 第01,02行,是构造函数,其接受3个参数,第一个json是从服务器传回来的json数据。第二个是解析为geometry之后调用的回调函数。第三个表示该模型的纹理路径。
  • 第05行,scale表示缩放,如果在json模型文件中,没有定义scale,那么默认为1.0。如果定义了scale,那么scale为1.0 / json.scale,这就是说json模型的数据被放大了 json.scale 倍,存放在文件中的。
  • 第06行,就是解析模型,构造geometry的过程,后面小结,我们详细讲一下。
  • 第07行,如果
  • 第08行,如果json文件表示一个动画,那么我们构建一个动画,这样模型就能够运动了。

parseModel函数

parseModel 函数主要讲 json 模型文件,转换为 geometry,大家边看代码边仔细理解吧。在分析代码之前,请打开一个比较完整的 json 模型文件,它在 【three.js-master\examples\obj\buffalo\ buffalo.js】 中,请用 notepad++ 打开它,然后对照下面的源代码的注释分析一下。

function parseModel( scale ) {
        // isBitSet判断value某一位的值是否为1,这是“位图”的表示方式,其实是将一组boolean变量放在一个整形中表示的方法。
		function isBitSet( value, position ) {
			return value & ( 1 << position );
		}
		// 一些临时变量
	var i, j, fi,
		// offset是一个临时变量,zLength 表示顶点的个数。nVertices用来表示一个face
	面由3还是4个顶点组成。这些变量都在后面赋值。
		offset, zLength, nVertices,
		colorIndex, normalIndex, uvIndex, materialIndex,
		type,	// 临时变量
		isQuad,	//
		hasMaterial,
		hasFaceUv, hasFaceVertexUv,
		hasFaceNormal, hasFaceVertexNormal,
		hasFaceColor, hasFaceVertexColor,
		vertex, face, color, normal,
		// uvLayer表示纹理的层数
		uvLayer, uvs, u, v,
		faces = json.faces,	
		vertices = json.vertices,	// 获取json文件中的vertices数组
		normals = json.normals,
		colors = json.colors,
		nUvLayers = 0;
		// 计算模型有多少层纹理,nUvLayers最后表示有多少层纹理。
		for ( i = 0; i < json.uvs.length; i++ ) {
			if ( json.uvs[ i ].length ) nUvLayers ++;
		}
		// 有多少面就初始化多少个面纹理坐标数组和面顶点纹理坐标数组。
		for ( i = 0; i < nUvLayers; i++ ) {
			geometry.faceUvs[ i ] = [];
			geometry.faceVertexUvs[ i ] = [];
		}
		offset = 0;
		zLength = vertices.length;
		// 读取所有的顶点放到geometry.vertices数组中,注意每一个顶点应该乘以它的缩放系数scale
		while ( offset < zLength ) {
			vertex = new THREE.Vector3();
			vertex.x = vertices[ offset ++ ] * scale;
			vertex.y = vertices[ offset ++ ] * scale;
			vertex.z = vertices[ offset ++ ] * scale;
			geometry.vertices.push( vertex );
		}
		offset = 0;
		zLength = faces.length;
		// 如果有面信息,那么下面的代码将解析面信息的属性。
		while ( offset < zLength ) {
		// 面的信息我们的教程会讲几次,这里简单介绍一下,在faces中,有很多组面信息,面信息的第一个元素是面的元数据,后面3个或者4个是面的顶点在顶点数组中的索引信息。
			type = faces[ offset ++ ];
			isQuad          	= isBitSet( type, 0 );	// 是否由4个顶点组成。
			hasMaterial         = isBitSet( type, 1 );	// 是否有材质
			hasFaceUv           = isBitSet( type, 2 );	// 是否有面纹理
			hasFaceVertexUv     = isBitSet( type, 3 );	// 是否有顶点纹理
			hasFaceNormal       = isBitSet( type, 4 );	// 是否有面法线
			hasFaceVertexNormal = isBitSet( type, 5 );	// 是否有面顶点法线
			hasFaceColor	    = isBitSet( type, 6 );	// 是否有面颜色
			hasFaceVertexColor  = isBitSet( type, 7 );	// 是否面的每一个顶点都有颜色
			//console.log("type", type, "bits", isQuad, hasMaterial, hasFaceUv, 
	hasFaceVertexUv, hasFaceNormal, hasFaceVertexNormal, hasFaceColor, 
	hasFaceVertexColor);
			if ( isQuad ) {
				// 一个面由4个顶点组成,注意,这里存放的都是顶点数组中的索引,在几何体中,同一个顶点可能属性不同的面。
				face = new THREE.Face4();
				face.a = faces[ offset ++ ];
				face.b = faces[ offset ++ ];
				face.c = faces[ offset ++ ];
				face.d = faces[ offset ++ ];
				nVertices = 4;	// 一个面有4个顶点
			} else {
				face = new THREE.Face3();
				face.a = faces[ offset ++ ];
				face.b = faces[ offset ++ ];
				face.c = faces[ offset ++ ];
				nVertices = 3;	// 一个面有3个顶点的情况
	
			}
			if ( hasMaterial ) {
			// 该面是否有材质,如果有,将这个面的材质索引赋值给face.materialIndex
				materialIndex = faces[ offset ++ ];
				face.materialIndex = materialIndex;
			}
			// 这里是如果有面纹理,面纹理就是如果一个面使用的是一种纯色,那么就可以用一个纹理坐标来填充这个面的每一个像素。
			fi = geometry.faces.length;	// fi随着面的增多而增多,第一个值为0
			if ( hasFaceUv ) {
				// 有多层纹理的情况
				for ( i = 0; i < nUvLayers; i++ ) {
					uvLayer = json.uvs[ i ];
					uvIndex = faces[ offset ++ ];
					u = uvLayer[ uvIndex * 2 ];
					v = uvLayer[ uvIndex * 2 + 1 ];
					geometry.faceUvs[ i ][ fi ] = new THREE.Vector2( u, v );
				}
			}
			// 为每一个顶点指定一个纹理坐标
			if ( hasFaceVertexUv ) {
				for ( i = 0; i < nUvLayers; i++ ) {
					uvLayer = json.uvs[ i ];
					uvs = [];
					for ( j = 0; j < nVertices; j ++ ) {
						uvIndex = faces[ offset ++ ];
						u = uvLayer[ uvIndex * 2 ];
						v = uvLayer[ uvIndex * 2 + 1 ];
						uvs[ j ] = new THREE.Vector2( u, v );
					}
					geometry.faceVertexUvs[ i ][ fi ] = uvs;
				}
			}
			// 如果整个面的发线相同,那么为一个面赋值一个法线
			if ( hasFaceNormal ) {
	
				normalIndex = faces[ offset ++ ] * 3;
				normal = new THREE.Vector3();
				normal.x = normals[ normalIndex ++ ];
				normal.y = normals[ normalIndex ++ ];
				normal.z = normals[ normalIndex ];
				face.normal = normal;
			}
			// 如果面上的法线不相同,就需要为面的每一个顶点赋一个法线。
			if ( hasFaceVertexNormal ) {
				for ( i = 0; i < nVertices; i++ ) {
					normalIndex = faces[ offset ++ ] * 3;
					normal = new THREE.Vector3();
					normal.x = normals[ normalIndex ++ ];
					normal.y = normals[ normalIndex ++ ];
					normal.z = normals[ normalIndex ];
					face.vertexNormals.push( normal );
				}
			}
			// 如果一个面是纯色,那么给一个面赋值一种颜色
			if ( hasFaceColor ) {
				colorIndex = faces[ offset ++ ];
				color = new THREE.Color( colors[ colorIndex ] );
				face.color = color;
			}
			// 如果一个面是有多种颜色,面中间的颜色可以通过顶点颜色算出来,那么就需要为每一个顶点指定颜色了。
27			if ( hasFaceVertexColor ) {
				for ( i = 0; i < nVertices; i++ ) {
					colorIndex = faces[ offset ++ ];
					color = new THREE.Color( colors[ colorIndex ] );
					face.vertexColors.push( color );
				}
	
			}
			geometry.faces.push( face );
		}
	};

上面的注释已经很详细的说明了模型解析函数parseModel的过程了,我相信,现在你也能写一个模型解析函数了。不过,我还是决定对上面一些代码的重要部分做一些解释,以回报各位大大们。

  • 第58到71行,是构造face3或者face4,一般来说,面只需要3个顶点,就能组成一个面,4个顶点比较多余,但是为了3D设计师在诸如3D-Max一样的软件中调整模型方便,所以4个顶点的面也比较常用。
  • 第77到86行,很多朋友都疑惑,为什么一个面会赋一个纹理坐标,而不是一个点一个纹理坐标,这是因为如果一个面是纯色,那么就可以在纹理贴图中找一个点来填充这个面,所以只需要一个纹理坐标就ok了。哈哈,这一点,应该是,如果你不看教程,会非常费解的地方。嘿嘿,感谢大家购买中级教程,另外,高级教程也很不错哦,欢迎购买。
  • 第88到99行,是一个面需要有映射到纹理上,就是面上不是纯色,所以每个面上的点都需要一个纹理坐标。
  • 后面的01到19行,其实是101到119行,我们这里省略了1,是讲的顶点法线和面法线。我们来讲一些基本概念,三角形是构成实体的基本单位,因为一个三角形正好是一个平面,以三角形面为单位进行渲染效率最高。一个三角形由三个点构成,习惯上把这些点称为顶点(Vertex)。三角形平面有正反面之分,由顶点的排序决定:顶点按顺时针排列的表面是正面,如下图:

其中与三角形平面垂直、且指向正面的矢量称为该平面的法线(Normal)。顶点法线(Vertex Normal)是过顶点的一个矢量,用于在高洛德着色(Gouraud Shading)中的计算光照和纹理效果。在生成曲面时,通常令顶点法线和相邻平面的法线保持等角,如左图,这样进行渲染时,会在平面接缝处产生一种平滑过渡的效果。如果是多边形,则令顶点法线等于该点所属平面(三角形)的法线,如右图,以便在接缝处产生突出的边缘。

ImageImageImage

最后,总结,如果将顶点的法线和面的法线设置为一样,则边线因为受光照强,所以就明显一些。注意,法线与入射光的角度越小,则该点光线越强。

接下来的 parseSkin()parseMorphing() 函数与骨骼动画有关,我们这里没有用来骨骼动画,所以暂时不讲了。


二进制加载器THREE.BinaryLoader

二进制加载类与 JsonLoader 的区别很简单,THREE.BinaryLoader 能除了加载一个js 文件之外,还会加载一个二进制文件。二进制文件中主要存放一些顶点数据、索引等数据,这些数据用二进制方式存储,会减少大量的存储空间,从而减少模型的大小。下面我们来详细介绍一下这个类,这个类的构造函数是:

THREE.BinaryLoader = function ( showStatus ) {
	THREE.Loader.call( this, showStatus );
};

参数 showStatus 是否显示加载进度的意思。True 为显示,false 为不显示。 与 JSONLoader 一样,它也有一个加载函数,这个函数的原型如下所示:

THREE.BinaryLoader.prototype.load = function( url, callback, texturePath, binaryPath )

各个参数的意思如下所示:

  • url:js文件的url路径,可以是一个相对路径,也可以是一个绝对路径。例如obj/female02/Female02_bin.js。如果对相对路径和绝对路径不明白,请访问这里:http://blog.csdn.net/wgy2006/article/details/3236602。注意,这个函数是必须要有的。

  • callback:这个函数是一个回调函数,当url中的数据被加载完成后,这个函数被调用。

  • texturePath:这个是纹理的路径,如果这个没有指定,那么纹理放在url同一个文件夹下,或者没有纹理。
  • binaryPath:这个是二进制文件的路径,不指定的话,根据url地址中的文件来加载。

明白了这些参数,我们现在来看一个例子:

bloader.load( "obj/female02/Female02_bin.js", function( geometry ) {
	createMesh( geometry, scene, 4.05, -1000, -350,    0, 0xffdd44, true );
	createMesh( geometry, scene, 4.05,     0, -350,    0, 0xffffff, true );
	createMesh( geometry, scene, 4.05,  1000, -350,  400, 0xff4422, true );
	createMesh( geometry, scene, 4.05,   250, -350, 1500, 0xff9955, true );
	createMesh( geometry, scene, 4.05,   250, -350, 2500, 0xff77dd, true );
} );

解释一下这段代码

首先 load 函数是异步加载的,异步的意思是 load 函数已经执行完成了,程序正在执行 load 函数之后的函数了,而加载的内容还正在加载的过程中。当加载完成之后,会回调 loader 的第二个参数指定的一个函数。

在这里,这个函数是 function( geometry ){}。这是一个没有名字的匿名函数。这个回调函数将返回一个 geometry 对象。


创建粒子系统

创建粒子系统,我们需要使用 THREE.ParticleSystem 这个类来实现,这个类需要接受一个几何体 geometry 参数,其实接受这个参数的目的就是得到 geometry 中的顶点信息,THREE.ParticleSystem 就可以利用这些顶点来绘制粒子了。

我们将从 geometry 变换为 THREE.ParticleSystem 的过程封装到了 CreateMesh 这个函数中,CreateMesh 用来创建我们需要的粒子系统,这个函数有8个参数。他们是:

  • originalGeometry:表示从模型中加载的几何体。
  • scene:表示场景。
  • scale:表示模型缩放的参数。
  • x,y,z:表示需要把模型放到什么位置。
  • color:表示模型的颜色。
  • dynamic:表示粒子是否运动起来,这里的意思是人物是否瞬间坍塌。另外,这个如果为true,还有一个意思,就是在其周围复制7个人物粒子,颜色很淡。这样让场景中的人物看起来多一些。

整个函数的代码如下:

function createMesh( originalGeometry, scene, scale, x, y, z, color, dynamic ) {
	var i, c;
	// 取得顶点
	var vertices = originalGeometry.vertices;
	var vl = vertices.length;
	// 重新生成一个几何体
	var geometry = new THREE.Geometry();
	var vertices_tmp = [];
	// 复制顶点,重新生成一个几何体对象
	for ( i = 0; i < vl; i ++ ) {
		p = vertices[ i ];
// 一定要用克隆的方法,不然当vertices的内存消失后,会出现空指针异常。
		geometry.vertices[ i ] = p.clone();	
		// 这里是很重要,同时也是非常令人费解的地方。第0,1,2个元素表示的顶点的位置,第3个元素表示down,就是顶点是否应该向下运动,0为暂时不向下运动,1为向下运动。第4个元素是up,也可以取0和1。
		vertices_tmp[ i ] = [ p.x, p.y, p.z, 0, 0 ];
	}
// 这里表示几个克隆的人物的位置,仔细观察场景,你可以在一个人物周围看到几个若隐若现的人物,这是我们创建的人物的影子,这样场景看起来人物更丰富。Clones数组中包含了几个影子的位置。它们都是围绕了某一个人物的周围。
	var clones = [
		[  6000, 0, -4000 ],
		[  5000, 0, 0 ],
		[  1000, 0, 5000 ],
		[  1000, 0, -5000 ],
		[  4000, 0, 2000 ],
		[ -4000, 0, 1000 ],
		[ -5000, 0, -5000 ],
		[ 0, 0, 0 ]
	];

	if ( dynamic ) {
		// 生成7个人物影子
		for ( i = 0; i < clones.length; i ++ ) {
			c = ( i < clones.length -1 ) ? 0x252525 : color;
			mesh = new THREE.ParticleSystem( geometry, new THREE.ParticleBasicMaterial( { size: 3, color: c } ) );
			mesh.scale.x = mesh.scale.y = mesh.scale.z = scale;
			mesh.position.x = x + clones[ i ][ 0 ];
			mesh.position.y = y + clones[ i ][ 1 ];
			mesh.position.z = z + clones[ i ][ 2 ];
// parent是所有粒子的父亲,将所有日志放在parent下,方便管理。
			parent.add( mesh ); 
			// 保存所有人物影子的数组,在渲染Mesh的时候会用到它。
			clonemeshes.push( { mesh: mesh, speed: 0.5 + Math.random() } );
		}
		// 将新增加的影子加到总数totaln中。
		totaln += clones.length;
		// 计算顶点的总数
		total += clones.length * vl;
	} else {
		// 无动画时候的粒子系统,一个模型一个粒子系统
		mesh = new THREE.ParticleSystem( geometry, new THREE.ParticleBasicMaterial( { size: 3, color: color } ) );
		mesh.scale.x = mesh.scale.y = mesh.scale.z = scale;	//  设置缩放参数
		// 设置模型的位置
		mesh.position.x = x;
		mesh.position.y = y;
		mesh.position.z = z;
		// 将改mesh加入一个统一的parent中,parent是一个object对象,已经被加入到了场景中了。
		parent.add( mesh );
		// 模型的总数增加1个
		totaln += 1;
		// 顶点的个数增加v1个
		total += vl;
	}
	// 将加载进度div隐藏
	bloader.statusDomElement.style.display = "none";
	// 将粒子系统的信息放入meshes中,为以后使用
	meshes.push( {
		mesh: mesh, vertices: geometry.vertices, vertices_tmp: vertices_tmp, vl: vl,
		down: 0, up: 0, direction: 0, speed: 35, delay: Math.floor( 200 + 200 * Math.random() ),
		started: false, start: Math.floor( 100 + 200 * Math.random() ),
		dynamic: dynamic
	} );
}

上面的大多数代码,我们都在注释中讲解了其意思。唯有最后几段代码 meshes.push,没有讲清楚。

meshes 是一个数组,用来存放有关粒子系统的信息,例如顶点,速度,运动方向,时间,顶点的个数等。这些信息在渲染的时候是会用到的,这里集中起来,一方面我了方便管理,另一方面为了阅读方便。我们会在后面用到这些数据。

在我们的例子中,人物有运动有2个方向,要么从上到下坍塌,要么从下到上重组成人形。方向用direction来表示,1表示向上,-1表示向下。

另外,用 started 来表示,动画是否开始,人物禁止时 startedfalse


渲染

就像男人离不开女人,webgl 离不开渲染。渲染的工作主要是将粒子系统完美的表现在场景中。这个伟大艰巨的工作由render来完成。

function render () {
	// 距上一帧的时间
	delta = 10 * clock.getDelta();
	delta = delta < 2 ? delta : 2;
	// 将这个场景的粒子做一下旋转
	parent.rotation.y += -0.02 * delta;
	// 将所有人物影子原地旋转,旋转的速度是随机的
	for( j = 0, jl = clonemeshes.length; j < jl; j ++ ) {
		cm = clonemeshes[ j ];
		cm.mesh.rotation.y += -0.1 * delta * cm.speed;
	}
	
	for( j = 0, jl = meshes.length; j < jl; j ++ ) {
		// 取出关于模型的信息
		data = meshes[ j ];
		// 网格信息
		mesh = data.mesh;
		// 顶点信息
		vertices = data.vertices;
		vertices_tmp = data.vertices_tmp;
		vl = data.vl;
		// 如果粒子没有坍塌的效果,那么就变化这个粒子系统中粒子的位置等信息。
		if ( ! data.dynamic ) continue;
		if ( data.start > 0 ) {
			data.start -= 1;
		} else {
			// 动画没有开始,则设置方向
			if ( !data.started ) {
				// -1表示人物坍塌
				data.direction = -1;
				// 设置动画开始
				data.started = true;
			}
		}

		for ( i = 0; i < vl; i ++ ) {
			p = vertices[ i ];
			vt = vertices_tmp[ i ];
			// 设置坍塌效果
			if ( data.direction < 0 ) {
				// 如果组成人物的所有点,还没有到地平面上,那么继续下降
				if ( p.y > 0 ) {
					// 以一定的速度随机的移动点,x,z点增加减少的概率是一样的。而y点坐标最终会不断减少。
					p.x += 1.5 * ( 0.50 - Math.random() ) * data.speed * delta;
					p.y += 3.0 * ( 0.25 - Math.random() ) * data.speed * delta;
					p.z += 1.5 * ( 0.50 - Math.random() ) * data.speed * delta;
				} else {
					// 如果没有向下运动,那么设置成向下运动
					if ( ! vt[ 3 ] ) {
						// 表示donw=1,向下运动
						vt[ 3 ] = 1;
						data.down += 1;	// 每设置一个粒子就加1
					}
				}
			}

			// 人物升起的样子
			if ( data.direction > 0 ) {
				d = Math.abs( p.x - vt[ 0 ] ) + Math.abs( p.y - vt[ 1 ] ) + Math.abs( p.z - vt[ 2 ] );
				if ( d > 1 ) {
					p.x += - ( p.x - vt[ 0 ] ) / d * data.speed * delta * ( 0.85 - Math.random() );
					p.y += - ( p.y - vt[ 1 ] ) / d * data.speed * delta * ( 1 + Math.random() );
					p.z += - ( p.z - vt[ 2 ] ) / d * data.speed * delta * ( 0.85 - Math.random() );
				} else {

					if ( ! vt[ 4 ] ) {
						vt[ 4 ] = 1;
						data.up += 1;
					}
				}
			}
		}

		// all down
		// 向下的所有点都赋予了新坐标之后
		if ( data.down === vl ) {
			// delay表示向下掉落的时间,delay为0,表示已经全部掉落在地上了。
			if ( data.delay === 0 ) {
				// 重置方向,速度,是否向下,延时等变量。
				data.direction = 1;
				data.speed = 10;
				data.down = 0;
				data.delay = 320;
				// 将所有点向下的标志设为0,表示目前所有点不向下
				for ( i = 0; i < vl; i ++ ) {
					vertices_tmp[ i ][ 3 ] = 0;
				}
			} else {
				// 不断减少时间
				data.delay -= 1;
			}
		}

		// all up
		// 向上的所有点都赋予了新坐标之后
		if ( data.up === vl ) {

			if ( data.delay === 0 ) {
				data.direction = -1;
				data.speed = 35;
				data.up = 0;
				data.delay = 120;
				for ( i = 0; i < vl; i ++ ) {
					vertices_tmp[ i ][ 4 ] = 0;
				}
			} else {
				data.delay -= 1;
			}
		}
		// 这里表示geometry中的顶点需要刷新一下,如果不刷新,那么渲染出来的结果,顶点的位置是没有改变的。
		mesh.geometry.verticesNeedUpdate = true;
	}
	// 清除上次渲染的结果
	renderer.clear();
	composer.render( 0.01 );

}

除了 composer.render 函数之外,我们把大多数核心概念都已经讲了。现在就是 composer.render 有什么作用了。Compose 就像一个组合家,把各种需要渲染的效果都组合起来,形成一个最终的效果。

Compose 产生的效果复杂一点,我们会在后面仔细讲解一下。


效果组合器

上文的 composer指的就是 THREE.EffectComposer,效果组合器,就是将各种效果放在一个数组里面,待渲染的时候,统一渲染。当所有渲染完成后,就能够得到最终的效果。如对效果组合器感兴趣,请看高级课程的相关章节,它需要shader的相关知识,所以那里有非常详细的成套解释。 这一部分较为复杂,本课已经讲了很多知识,在继续讲下去,学习效果就不理想了。所以我们将在后面的课程中,来详细讲解效果组合器,敬请期待。 本课完。