Threejs實現(xiàn)穿越云層動效

上文說到,我對《你的性格主導色》活動中最感興趣的部分就是通過 Three.js 實現(xiàn)穿越云層動效了,據(jù)作者說每朵云出現(xiàn)的位置都是隨機的,效果很好,下圖是我實現(xiàn)的版本。

image

在線 Demo

首先說下實現(xiàn)穿越云層動效的基本思路:

  1. 沿著Z軸均勻的放一堆64*64的平面圖形,這些平面的X坐標和Y坐標是隨機的(很像下圖的桶裝薯片)
  2. 把上面的所有圖形合并成一個大的圖形
  3. 把大的圖形和貼片材質(云)生成網(wǎng)格,網(wǎng)格放進場景中
  4. 動效就是將相機從遠處沿著Z軸緩慢移動,就會有了穿越云層的效果
image

首先官方文檔提供了一個創(chuàng)建一個場景的快速開始,閱讀后可以對下面的內容更好的理解。

下面介紹下Three.js中的基本概念。僅限我這新手的理解。有講的好的文檔或者分享,歡迎幫忙指個路。

場景

場景就是一塊空間,用來裝下我們想要渲染的內容。最簡單的用處就是,場景可以添加一個網(wǎng)格,然后渲染出來。

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

// 其他代碼...
// 把物體添加進場景
scene.add(mesh);
// 渲染場景
renderer.render(scene, camera);

這里說下場景中的坐標規(guī)則:原點是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);

相機

場景內的物體要想被我們看見,也就是渲染出來,需要相機去“看”,通過上面的坐標系圖,我們知道同一個物體,相機觀察的角度不同,肯定也會呈現(xiàn)出不一樣的畫面。最常用的就是這里用的透視相機,可以穿透物體,用在這里正好穿透云層,效果拔群。

// 初始化相機
camera = new THREE.PerspectiveCamera(70, pageWidth / pageHeight, 1, 1000);

// 最后,場景和相機一起渲染出來,我們就能夠看到場景中的物體了
renderer.render(scene, camera);

材質

材質很好理解,在最初的例子中,使用MeshBasicMaterial給立方體添加了顏色。材質的使用方式是,將材質和圖形共同生成一個網(wǎng)格,我們這里使用的是比較復雜的貼圖材質。

  // 貼圖材質
  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
  });

圖形和網(wǎng)格

Three.js默認提供了很多的幾何體圖形,也就是各種Geometry,他們的基類是BufferGeometry。

圖形可以進行合并,像這里就是clone了很多個一樣的平面圖形,通過修改各自的位置,生成合并后形成一大片云的效果。

最初我認為圖形和網(wǎng)格是一個概念,后來知道了,材質和圖形可以生成網(wǎng)格,網(wǎng)格可以放進場景中。

// 把上面合并出來的形狀和材質,生成一個網(wǎng)格
mesh = new THREE.Mesh(mergedGeometry, material);

渲染

將場景和相機渲染到目標元素上,會生成一個canvas,如果是一個靜態(tài)的場景,那么渲染完畢就可以了。但是如果是一個會動的場景,這里需要用到一個原生函數(shù)requestAnimationFrame

function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}

上面的代碼是一個渲染循環(huán),在一般屏幕上的頻率是60HZ,在高刷屏幕上會增長刷新頻率,也就是會給用戶良好的刷新體驗,不需要我們自己使用setInterval去控制。并且當用戶切換到其它的標簽頁時,它會暫停刷新,不會浪費用戶寶貴的處理器資源,也不會損耗電池的使用壽命。

揭秘過程

過程其實很有意思,也很曲折。

扒下來了《你的性格主導色》活動的前端代碼,但是云層動效相關有很多代碼壓縮過了,看不懂。

怎么辦?然后我就去 three.js 找官方的例子去,找了半天只找到一個下圖這樣的:

image

后來經(jīng)過各種搜索,終于在three.js討論區(qū)發(fā)現(xiàn)了這種穿越云層的特效,是three.js的作者很久之前寫的例子。

把云層動效源碼拿到手以后,我對比后感覺 imyzf 同學應該也是從這個例子中借鑒了一下。

我發(fā)現(xiàn)源碼中的three.js的版本有一些落后,源碼中的版本是55,最新的是131版本,版本差距有點大,已經(jīng)沒有了上面的一些類和API,下面介紹下不同的部分:

THREE.Geometry

首先就是這個類在最新版沒有了,這個類是用來將很多個平面圖形,合并為一個圖形。觀察下面的代碼,55的版本是先生成一個Geometry,然后生成一個平面網(wǎng)格,再把網(wǎng)格和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);
}

經(jīng)過對最新文檔的查詢后,發(fā)現(xiàn)所有圖形的基類BufferGeometry提供clone方法,平面圖形自然也可以被clone出來。

  // 一個平面形狀
  const geometry = new THREE.PlaneGeometry(64, 64);
  const geometries = [];

  for (var i = 0; i < CloudCount; i++) {
    const instanceGeometry = geometry.clone();

    // 把這個克隆出來的云,通過隨機參數(shù),做一些位移,達到一堆云彩的效果,每次渲染出來的云堆都不一樣
    // 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,目的就是產(chǎn)生這一大片的云,然后通過相機去看,最新版的three.js已經(jīng)沒有了。

// 合并所有的平面圖形到一個基礎圖形
THREE.GeometryUtils.merge(geometry, plane);

通過查詢最新版的文檔,發(fā)現(xiàn)了可以將一組圖形進行合并,個人覺得比上面的好一些,語義上好很多。上面的代碼是重復的把平面合并到一個基礎圖形上面,下面是把這一組平面合成為一個新的平面。

// 把這些形狀合并
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 和提出建議。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • Three.js 是一款運行在瀏覽器中的 3D 引擎,你可以用它在 web 中創(chuàng)建各種三維場景,包括了攝影機、光影...
    了無_數(shù)據(jù)科學閱讀 1,692評論 0 0
  • 由于對WebGL的興趣,初步接觸Three.js,決定將學習過程進行記錄,以便于后期復習。 初步以實現(xiàn)3D機房為目...
    Mr_ZhaiDK閱讀 2,935評論 0 2
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月,有人笑有人哭,有人歡樂有人憂愁,有人驚喜有人失落,有的覺得收獲滿滿有...
    陌忘宇閱讀 8,839評論 28 54
  • 人工智能是什么?什么是人工智能?人工智能是未來發(fā)展的必然趨勢嗎?以后人工智能技術真的能達到電影里機器人的智能水平嗎...
    ZLLZ閱讀 4,100評論 0 5
  • 首先介紹下自己的背景: 我11年左右入市到現(xiàn)在,也差不多有4年時間,看過一些關于股票投資的書籍,對于巴菲特等股神的...
    瞎投資閱讀 5,939評論 3 8

友情鏈接更多精彩內容