Threejs实现穿越云层动效

上文说到,我对《你的性格主导色》活动中最感兴趣的部分就是通过 Three.js 实现穿越云层动效了,据作者说每朵云出现的位置都是随机的,效果很好,下图是我实现的版本。

在线 Demo
【Threejs实现穿越云层动效】首先说下实现穿越云层动效的基本思路:

  1. 沿着 Z 轴均匀的放一堆 64*64 的平面图形,这些平面的 X 坐标和 Y 坐标是随机的(很像下图的桶装薯片)
  2. 把上面的所有图形合并成一个大的图形
  3. 把大的图形和贴片材质(云)生成网格,网格放进场景中
  4. 动效就是将相机从远处沿着 Z 轴缓慢移动,就会有了穿越云层的效果
Threejs实现穿越云层动效
文章图片

首先官方文档提供了一个创建一个场景的快速开始,阅读后可以对下面的内容更好的理解。
下面介绍下Three.js中的基本概念。仅限我这新手的理解。有讲的好的文档或者分享,欢迎帮忙指个路。
场景 场景就是一块空间,用来装下我们想要渲染的内容。最简单的用处就是,场景可以添加一个网格,然后渲染出来。
// 初始化场景 var scene = new THREE.Scene(); // 其他代码... // 把物体添加进场景 scene.add(mesh); // 渲染场景 renderer.render(scene, camera);

这里说下场景中的坐标规则:原点是 canvas 的平面中心,Z 轴垂直于 X、Y 轴,正向是冲着我们的,我这里把 Z 轴的线做了些旋转,不然我们看不到,如下图:
Threejs实现穿越云层动效
文章图片

代码:
const scene = new THREE.Scene(); var camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000); camera.position.set(0, 0, 100); const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // 线段1,红色的,从原点到X轴40 const points = []; points.push(new THREE.Vector3(0, 0, 0)); points.push(new THREE.Vector3(40, 0, 0)); const geometry1 = new THREE.BufferGeometry().setFromPoints(points); var material1 = new THREE.LineBasicMaterial({ color: 'red' }); var line1 = new THREE.Line(geometry1, material1); // 线段2,蓝色的,从原点到Y轴40 points.length = 0; points.push(new THREE.Vector3(0, 0, 0)); points.push(new THREE.Vector3(0, 40, 0)); const geometry2 = new THREE.BufferGeometry().setFromPoints(points); var material2 = new THREE.LineBasicMaterial({ color: 'blue' }); var line2 = new THREE.Line(geometry2, material2); // 线段3,绿色的,从原点到Z轴40 points.length = 0; points.push(new THREE.Vector3(0, 0, 0)); points.push(new THREE.Vector3(0, 0, 40)); const geometry3 = new THREE.BufferGeometry().setFromPoints(points); var material3 = new THREE.LineBasicMaterial({ color: 'green' }); var line3 = new THREE.Line(geometry3, material3); // 做了个旋转,不然看不到Z轴上的线 line3.rotateX(Math.PI / 8); line3.rotateY(-Math.PI / 8); scene.add(line1, line2, line3); renderer.render(scene, camera);

相机 场景内的物体要想被我们看见,也就是渲染出来,需要相机去“看”,通过上面的坐标系图,我们知道同一个物体,相机观察的角度不同,肯定也会呈现出不一样的画面。最常用的就是这里用的透视相机,可以穿透物体,用在这里正好穿透云层,效果拔群。
// 初始化相机 camera = new THREE.PerspectiveCamera(70, pageWidth / pageHeight, 1, 1000); // 最后,场景和相机一起渲染出来,我们就能够看到场景中的物体了 renderer.render(scene, camera);

材质 材质很好理解,在最初的例子中,使用MeshBasicMaterial给立方体添加了颜色。材质的使用方式是,将材质和图形共同生成一个网格,我们这里使用的是比较复杂的贴图材质。
// 贴图材质 const material = new THREE.ShaderMaterial({ // 这里的值是给着色器传递的 uniforms: { map: { type: 't', value: texture }, fogColor: { type: 'c', value: fog.color }, fogNear: { type: 'f', value: fog.near }, fogFar: { type: 'f', value: fog.far } }, vertexShader: vShader, fragmentShader: fShader, transparent: true });

图形和网格 Three.js默认提供了很多的几何体图形,也就是各种Geometry,他们的基类是BufferGeometry
图形可以进行合并,像这里就是 clone 了很多个一样的平面图形,通过修改各自的位置,生成合并后形成一大片云的效果。
最初我认为图形和网格是一个概念,后来知道了,材质和图形可以生成网格,网格可以放进场景中。
// 把上面合并出来的形状和材质,生成一个网格 mesh = new THREE.Mesh(mergedGeometry, material);

渲染 将场景和相机渲染到目标元素上,会生成一个canvas,如果是一个静态的场景,那么渲染完毕就可以了。但是如果是一个会动的场景,这里需要用到一个原生函数requestAnimationFrame
function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); }

上面的代码是一个渲染循环,在一般屏幕上的频率是 60HZ,在高刷屏幕上会增长刷新频率,也就是会给用户良好的刷新体验,不需要我们自己使用setInterval去控制。并且当用户切换到其它的标签页时,它会暂停刷新,不会浪费用户宝贵的处理器资源,也不会损耗电池的使用寿命。
揭秘过程 过程其实很有意思,也很曲折。
扒下来了《你的性格主导色》活动的前端代码,但是云层动效相关有很多代码压缩过了,看不懂。
怎么办?然后我就去 three.js 找官方的例子去,找了半天只找到一个下图这样的:
Threejs实现穿越云层动效
文章图片

后来经过各种搜索,终于在three.js的讨论区发现了这种穿越云层的特效,是three.js的作者很久之前写的例子。
把云层动效源码拿到手以后,我对比后感觉 imyzf 同学应该也是从这个例子中借鉴了一下。
我发现源码中的three.js的版本有一些落后,源码中的版本是 55,最新的是 131 版本,版本差距有点大,已经没有了上面的一些类和 API,下面介绍下不同的部分:
THREE.Geometry
首先就是这个类在最新版没有了,这个类是用来将很多个平面图形,合并为一个图形。观察下面的代码,55 的版本是先生成一个Geometry,然后生成一个平面网格,调整网格的坐标后,把网格和Geometry合并(这里有点不懂了,图形怎么和网格合并,而且是同一个网格,我猜是在合并的时候新生成了一个网格)。
// 初始化一个基础的图形 geometry = new THREE.Geometry(); // 初始化一个64*64的平面 var plane = new THREE.Mesh(new THREE.PlaneGeometry(64, 64)); for (var i = 0; i < 8000; i++) { // 调整平面图案的位置和旋转角度等 plane.position.x = Math.random() * 1000 - 500; plane.position.y = -Math.random() * Math.random() * 200 - 15; plane.position.z = i; plane.rotation.z = Math.random() * Math.PI; plane.scale.x = plane.scale.y = Math.random() * Math.random() * 1.5 + 0.5; // 平面合并到基础图形 THREE.GeometryUtils.merge(geometry, plane); }

查询最新文档后,发现所有图形的基类BufferGeometry提供 clone 方法,平面图形自然也可以被 clone 出来。
// 一个平面形状 const geometry = new THREE.PlaneGeometry(64, 64); const geometries = []; for (var i = 0; i < CloudCount; i++) { const instanceGeometry = geometry.clone(); // 把这个克隆出来的云,通过随机参数,做一些位移,达到一堆云彩的效果,每次渲染出来的云堆都不一样 // X轴偏移后,通过调整相机位置达到平衡 // Y轴想把云彩放在场景的偏下位置,所以都是负值 // Z轴位移就是:当前第几个云*每个云所占的Z轴长度 instanceGeometry.translate(Math.random() * RandomPositionX, -Math.random() * RandomPositionY, i * perCloudZ); geometries.push(instanceGeometry); }// 把这些形状合并 const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(geometries);

GeometryUtils.merge
旧代码码中有一个这样的 API,这是一个很重要的 API,目的就是合并图形和网格,生成一片云,最新版的three.js已经没有了。
// 合并所有的平面图形到一个基础图形 THREE.GeometryUtils.merge(geometry, plane);

通过查询最新版的文档,发现了可以将一组图形进行合并,个人觉得比上面的好一些,语义上好很多。上面的代码是重复的把同一个平面合并到一个基础图形上面,下面是把这一组平面合成为一个新的平面。
// 把这些形状合并 const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(geometries);

着色器
着色器代码逻辑我是完全的没有修改,GLSL(OpenGL 着色语言 OpenGL Shading Language),原来的着色器代码是写在
后来找了几个地方才知道可以时间使用字符串代替:
const vShader = ` varying vec2 vUv; void main() { vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); } `;

顶点着色器和片元着色器代码,我目前是真的不懂,先抄为敬。
源码 最后放上源码,感兴趣的同学可以看一下,欢迎 Star 和提出建议。

    推荐阅读