Threejs实现穿越云层动效

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

image

在线 Demo

首先说下实现穿越云层动效的基本思路:

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

首先官方文档提供了一个创建一个场景的快速开始,阅读后可以对下面的内容更好的理解。

下面介绍下Three.js中的基本概念。仅限我这新手的理解。有讲的好的文档或者分享,欢迎帮忙指个路。

场景

场景就是一块空间,用来装下我们想要渲染的内容。最简单的用处就是,场景可以添加一个网格,然后渲染出来。

// 初始化场景
var scene = new THREE.Scene();

// 其他代码...
// 把物体添加进场景
scene.add(mesh);
// 渲染场景
renderer.render(scene, camera);

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

image

代码:

 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 找官方的例子去,找了半天只找到一个下图这样的:

image

后来经过各种搜索,终于在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),原来的着色器代码是写在<script>元素标签里的,这和我们的工程化项目不符合。

// 原来的
<script id="vs" type="x-shader/x-vertex">
  varying vec2 vUv;
  void main()
  {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
  }
</script>

<script id="fs" type="x-shader/x-fragment">
   uniform sampler2D map;
   uniform vec3 fogColor;
   uniform float fogNear;
   uniform float fogFar;
   varying vec2 vUv;
   void main()
   {
       float depth = gl_FragCoord.z / gl_FragCoord.w;
       float fogFactor = smoothstep( fogNear, fogFar, depth );
       gl_FragColor = texture2D(map, vUv );
       gl_FragColor.w *= pow( gl_FragCoord.z, 20.0 );
       gl_FragColor = mix( gl_FragColor, vec4( fogColor, gl_FragColor.w ), fogFactor );
  }
</script>

后来找了几个地方才知道可以时间使用字符串代替:

  const vShader = `
    varying vec2 vUv;
    void main()
    {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
    }
  `;

源码

最后放上源码,感兴趣的同学可以看一下,欢迎 Star 和提出建议。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容

  • Three.js 是一款运行在浏览器中的 3D 引擎,你可以用它在 web 中创建各种三维场景,包括了摄影机、光影...
    吞风咽雪阅读 7,229评论 3 3
  • Three.js 是一款运行在浏览器中的 3D 引擎,你可以用它在 web 中创建各种三维场景,包括了摄影机、光影...
    了无_数据科学阅读 1,560评论 0 0
  • 由于对WebGL的兴趣,初步接触Three.js,决定将学习过程进行记录,以便于后期复习。 初步以实现3D机房为目...
    Mr_ZhaiDK阅读 2,758评论 0 2
  • 我是黑夜里大雨纷飞的人啊 1 “又到一年六月,有人笑有人哭,有人欢乐有人忧愁,有人惊喜有人失落,有的觉得收获满满有...
    陌忘宇阅读 8,532评论 28 53
  • 人工智能是什么?什么是人工智能?人工智能是未来发展的必然趋势吗?以后人工智能技术真的能达到电影里机器人的智能水平吗...
    ZLLZ阅读 3,770评论 0 5