three.js粒子过度效果制作(一)
粒子过度效果在很多网页中经常简单,可以实现从物体A过度到物体B。其原理是改变顶点的位置,按照预先设计好的路径移动。
一种简单的实现方式是,给出在A位置的所有顶点坐标,给出B位置的所有顶点坐标,然后通过过度的方式实现。下面演示一下从一个平面过度到一个球体。A是一个平面,通过PlaneGeometry得到所以粒子的坐标,B是一个球体通过SphereGeometry的生成所有点的坐标。我们也可以通过外部加载的方式来加载一个模型,采用模型的顶点坐标,就可以实现从猿--人
的变化。
这里要求A、B两组顶点的数量一致,如果不一致的话,想办法处理一下,比如把数量少的一组顶点复制一部分添加到数组后面。这样得到了重复的顶点,在视觉上不影响。我这里A、B直接生成了462个顶点,不需要处理顶点数量问题。
planeP是数组对象,但是元素缺是{x:0,y:0,x:0}
Objectd对象,属于值引用类型。所以planeGeo、planeP的顶点数据都是一样的,改变一个其中一个也会改变。因此在创建
var mesh = new THREE.Points(new THREE.PlaneGeometry(40, 40, 20, 21), pointMaterial)的时候就不要再用planeGeo了,否则动画执行一次就停止了,因为A、B的值都变成了一样。
粒子位置和颜色的改变可以在GPU或者CPU中完成,二者在性能方面有些差异。GPU在处理顶点数据,片元方面比cpu快速很多,cpu在逻辑判断等编程方便比较有优势。所以在处理大量数据的时候最好交给gpu来处理,代码中粒子的数量很少,在性能上没有什么差异。粒子数量多的时候在移动设备上就会有明显的差别。
下面分别实现CPU 和GPU版本。
CPU版本
cpu版本中粒子位置的变化都是在js代码中实现,过度效果采用tween.js插件。
更新粒子位置后需要设置mesh.geometry.verticesNeedUpdate = true。
步骤
- 创建planegeometry–
A
和spheregeometry–B
分别获取它们的顶点坐标。为了方便,创建的两组数据的顶点数量相同。 - 将他们的顶点坐标拷贝(clone)到两个数组中。clone()是three.js 中vec3的内置方法,不是JavaScript的。
- 创建一个点材质。
- 再创建一个THREE.Points–
C
对象。这个对象和上面的planegeometry是一样的顶点坐标,但不是同一个数据源。坑:js的原始类型和值引用类型。 - 创建tween循环动画。让val的值在1-0之间来回变化。
- 创建tween循环体中的回调函数。改变
C
的顶点坐标从A
-B
直接变化。 - 更新geometry.verticesNeedUpdate ,这个很重要。
- 更新TWEEN.update()。
var planeGeo = new THREE.PlaneGeometry(40, 40, 20, 21);
planeGeo.translate(-30, 0, 0);
var sphereGeo = new THREE.SphereGeometry(20, 23, 21);
sphereGeo.translate(30, 0, 0);
var planeP = [], sphereP = [];
planeGeo.vertices.forEach(function (p) {
planeP.push(p); // clone()
});
sphereGeo.vertices.forEach(function (p) {
sphereP.push(p);
});
var pointMaterial = new THREE.PointsMaterial({
size: 2.0,
color: 0xffffff,
map: new THREE.TextureLoader().load("../img/disc.png"),
side: THREE.DoubleSide,
alphaTest: 0.5,
transparent: true,
});
var mesh = new THREE.Points(new THREE.PlaneGeometry(40, 40, 20, 21), pointMaterial); //planeGeo-->clone()-->new THREE.PlaneGeometry(40, 40, 20, 21)
var pos = {val: 1};
var tween = new TWEEN.Tween(pos).to({val: 0}, 2000).easing(TWEEN.Easing.Quadratic.InOut).delay(1000).onUpdate(callback);
var tweenBack = new TWEEN.Tween(pos).to({val: 1}, 2000).easing(TWEEN.Easing.Quadratic.InOut).delay(1000).onUpdate(callback);
tween.chain(tweenBack);
tweenBack.chain(tween);
tween.start();
function callback() {
var val = this.val;
for (var i = 0; i < mesh.geometry.vertices.length; i++) {
var pos = {};
pos.x = planeP[i].x * val + sphereP[i].x * (1 - val);
pos.y = planeP[i].y * val + sphereP[i].y * (1 - val);
pos.z = planeP[i].z * val + sphereP[i].z * (1 - val);
mesh.geometry.vertices[i].set(pos.x, pos.y, pos.z);
}
mesh.geometry.verticesNeedUpdate = true;
}
scene.add(mesh);
// 循环更新
TWEEN.update();
GPU版本
彩色的部分是GPU版本,白色的是CPU版本,两者的效果一样。
步骤
- 编写顶点着色器
- 编写片元着色器
- 创建一个planegeometry–
P
。获取他的顶点坐标。 - 创建points,sizes,colors Float32Array数组,长度是
P
的三倍。 - 向三个数组中写入数组,因为
P
的顶点坐标书符合数组(数组的每个元素是一个点的三维向量),我们要将它转换成纯数字的数组。vertex.toArray(positions, i * 3)也是three.js vec3内置的方法。是将vertex(x,y,z)的值存放在positions数组中,3是偏移量。 - 同理创建一个SphereGeometry –
S
,主要是获取它的顶点数组,并转化到一个新的一维数组中去。 - 创建一个BufferGeometry。并把顶点数据、颜色、粒子大小、待变化的顶点数据写到Attribute中。
- 创建ShaderMaterial,采用自定义的着色器。
- 创建bufferPoints,使用 BufferGeometry,ShaderMaterial。
- 创建tween动画,同上。这次回调函数中只改变material.uniforms.val.value的值。
<script type="x-shader/x-vertex" id="vertexshader">
attribute float size;
attribute vec3 customColor;
attribute vec3 positionB;
uniform float val;
varying vec3 vColor;
void main() {
vec3 vPos;
vPos.x = position.x * val + positionB.x * (1.-val);
vPos.y = position.y * val + positionB.y * (1.-val);
vPos.z = position.z * val + positionB.z * (1.-val);
vColor = customColor;
vec4 mvPosition = modelViewMatrix * vec4( vPos, 1.0 );
gl_PointSize = size * ( 300.0 / -mvPosition.z )*(abs(val-0.5)+0.2);
gl_Position = projectionMatrix * mvPosition;
}
</script>
<script type="x-shader/x-fragment" id="fragmentshader">
uniform vec3 color;
uniform sampler2D texture;
varying vec3 vColor;
void main() {
gl_FragColor = vec4( color * vColor, 1.0 );
gl_FragColor = gl_FragColor * texture2D( texture, gl_PointCoord );
if ( gl_FragColor.a < ALPHATEST ) discard;
}
</script>
var vertices = new THREE.PlaneGeometry(40, 40, 20, 21).translate(-30,0,5).vertices;
var positions = new Float32Array(vertices.length * 3);
var colors = new Float32Array(vertices.length * 3);
var sizes = new Float32Array(vertices.length);
var vertex;
var color = new THREE.Color();
for (var i = 0, l = vertices.length; i < l; i++) {
vertex = vertices[i];
vertex.toArray(positions, i * 3);
color.setHSL(0.01 + 0.5 * (i / l), 1.0, 0.5);
color.toArray(colors, i * 3);
sizes[i] = 5 * 0.5;
}
var verticesB =new THREE.SphereGeometry(20, 23, 21).translate(30,0,10).vertices;
var positionsB = new Float32Array(verticesB.length*3);
for (var i = 0, l = verticesB.length; i < l; i++) {
vertex = verticesB[i];
vertex.toArray(positionsB, i * 3);
}
var geometry = new THREE.BufferGeometry();
geometry.addAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.addAttribute('customColor', new THREE.BufferAttribute(colors, 3));
geometry.addAttribute('size', new THREE.BufferAttribute(sizes, 1));
geometry.addAttribute('positionB', new THREE.BufferAttribute(positionsB, 3));
var material = new THREE.ShaderMaterial({
uniforms: {
color: {value: new THREE.Color(0xffffff)},
texture: {value: new THREE.TextureLoader().load("../img/disc.png")},
val: {value: 1.0}
},
vertexShader: document.getElementById('vertexshader').textContent,
fragmentShader: document.getElementById('fragmentshader').textContent,
alphaTest: 0.5,
});
bufferPoints = new THREE.Points(geometry, material);
scene.add(bufferPoints);
var pos = {val: 1};
var tween = new TWEEN.Tween(pos).to({val: 0}, 2000).easing(TWEEN.Easing.Quadratic.InOut).delay(1000).onUpdate(callback);
var tweenBack = new TWEEN.Tween(pos).to({val: 1}, 2000).easing(TWEEN.Easing.Quadratic.InOut).delay(1000).onUpdate(callback);
tween.chain(tweenBack);
tweenBack.chain(tween);
tween.start();
function callback() {
bufferPoints.material.uniforms.val.value = this.val;
}