一只脚踏入 Three.js

前言

正所谓:无折腾,不前端。不搞 WebGL,和咸鱼有啥区别!javascript

用官方的说法:Three.js - Javascript 3D library。css

咱们今天就来一块儿熟悉一下 Three.js 的设计理念与思想。html

笛卡尔右手坐标系

在作 3D,咱们首先得要了解其基本准则:三维坐标系。前端

咱们都知道在 CSS3 的三维空间中是左手坐标系。(若是不了解的能够阅读我以前写的一篇文章《CSS3 之 3D 变换》java

可是在 Three.js 中,咱们的空间是基于右手笛卡尔坐标系的而展示的。以下:git

了解了坐标系以后,咱们就能在这片三维空间中建立咱们想要的场景了。github

建立场景

想要使用三维空间,首先就必须开辟一个三维空间这一容器。而开辟一个三维空间只须要实例化 THREE.Scene 这一对象就能够了。canvas

var scene = new THREE.Scene();
复制代码

场景是你能够放置物体、相机和灯光的三维空间,如同宇宙通常,没有边界,也没有光亮,有的是无尽的黑暗。设计模式

一个场景中的组件能够的大体分为三类:摄像机、光源、对象。数组

咱们在了解 Thee.js 中的组件以前,先看一张照片:

这是一张拍摄商品的工做室照片。这张照片就基本能够说明咱们 Three.js 的 3D 设计模式:咱们在有了一个空间以后,咱们须要将咱们是拍摄对象放进去。有了对象以后咱们还须要设置至少一个光源,这样咱们才能看到咱们的拍摄对象。最后,咱们呈如今客户眼前的是一系列由相机拍摄出的照片连续播放产生的动画,相机的参数、位置和角度直接影响着咱们所拍到的图片。

拍摄对象

在使用拍摄对象以前咱们先说明一下用 Three.js 建立拍摄对象的设计模式:

首先 Three.js 将任何拍摄对象解构为一个个小三角形。不管是二维图形仍是三维图形,均可以用三角形做为结构最小单位。而结构出来的就是咱们拍摄对象的一个网格。

以下呈现的是二维平面的网格结构:

以下展现的是三维球体网格结构:

能够看到在 Three.js 中三角形是最小分割单位。这就是网格结构。

固然有网格结构仍是不够的。就像人体同样,由于网格结构就像是骨架,在其外表还须要材质。材质就是物体的皮肤,决定着几何体的外表。

几何体模型(Geometry)

在 Three.js 中,为咱们预设了不少几何体的网格结构:

  • 二维:

    • THREE.PlaneGeometry(平面)

      这个几何体在前文已经展现过了。

    • THREE.CircleGeometry(圆)

    • THREE.RingGeometry(环)

  • 三维

    • THREE.BoxGeometry(长方体)

    • THREE.SphereGeometry(球体)

      这个几何体在前文已经展现过了。

    • THREE.CylinderGeometry(圆柱体)

    • THREE.Torus(圆环)

以上所举的只是内置几何体的一部分。咱们在使用这些集合体的时候,咱们只须要实例化相应几何体对象便可。

具体咱们以实例化一个正方体为例:

var geometry = new THREE.BoxGeometry(4, 4, 4);
复制代码

这里咱们先声明而且实例化了一个 BoxGeometry(长方体)对象。在建立对象的时候咱们分别设置了长、宽、高各为 4。

这样一个正方体就建立好了。可是有了这么一个网格框架是远远不够的。下一步就是给他添加材质。

材质(Material)

在材质组件中,Three.js 也为咱们预设了几种材质对象,咱们这里简单的介绍两种最经常使用的:

  1. MeshBasicMaterial

    这一材质,是 Three.js 的基础材质。用于给几何体网格赋予一种简单的颜色或是显示几何体的网格结构。(即使在没有光源的状况下也能够显示。)

  2. MeshLambertMaterial

    这是一种考虑光照影响的材质。用于建立暗淡的,不光亮的物体。

值得注意的是,在同一个网格结构中咱们能够多种材质进行叠加。

这里咱们前后使用 MeshBasicMaterial 和 MeshLambertMaterial 为咱们前文所创造的正方体准备两个不一样的材质:

var geometryMeshBasicMaterial = new THREE.MeshBasicMaterial({
  color: 0xff0000,
  wireframe: true
});
var geometryMeshLambertMaterial = new THREE.MeshLambertMaterial({
  color: 0x242424
});
复制代码

其中 wireframe 属性当设置为 true 的时候,将会将材质渲染为线宽。相框能够变相的理解为网格线。好比说一个正方体的线框以下:

网格(Mesh)

当咱们拥有了几何体网格模型和材质以后咱们就须要将二者结合起来建立咱们正在的拍摄对象。

这里咱们介绍两个不一样的拍摄对象构造方法:

  • new THREE.Mesh(geometry, material)
  • THREE.SceneUtils.createMultiMaterialObject(geometry,[materials...])

这两种都是建立拍摄对象的方法,且第一个参数都是几何体模型(Geometry),惟一不一样在于第二个参数。前者只能用一种材质建立拍摄对象,后者可使用多种材质进行建立(传入一个包含多种材质的数组)。

这里咱们将建立一个多材质拍摄对象。

var cube = THREE.SceneUtils.createMultiMaterialObject(geometry, [
  geometryMeshBasicMaterial,
  geometryMeshLambertMaterial
]);
复制代码

如今咱们已经有一个拍摄对象了,这时候咱们须要将咱们的对象添加到场景中,就像咱们在拍摄商品同样,得要把咱们的商品放在拍摄空间之中。

在 Three.js 中,向场景中添加对象能够直接经过场景对象调用 add 方法实现。具体实现以下:

scene.add(cube);
复制代码

咱们向 add()方法内传入咱们要添加的对象,能够是一个,也能够多个,用逗号隔开。

光源

和现实生活中的逻辑是同样的,物体自己是不会发光的。若是没有太阳这一光源,地球将陷入无尽的黑暗,啥也瞅不着。因此咱们也要向咱们的场景中添加光源对象。

在 Three.js 中,光源分为了好几种,接下来将简单的介绍其中用的比较多的几种。

  1. THREE.AmbientLight

    这是一种基本光源,该光源将会叠加到场景现有物体的颜色上。

    该光源没有特定的来源方向,且不会产生阴影。

    咱们常常在使用了其余光源的同时使用它,是为了弱化阴影或给场景添加一些额外的颜色。

  2. THREE.SpotLight

    这种光源有聚光的效果,相似台灯、手电筒、舞台聚光灯。

    这种光源能够投射阴影。

  3. THREE.DirectionalLight

    这种光源也称为无限光,相似太阳光。

    这种光源发出的光线能够看做是平行的。

    这种光源也可投射阴影。

在咱们的例子中,咱们将用 SpotLight 来建立咱们的灯光。

首先咱们要和以前常见拍摄对象同样,先实例化一个 SpotLight 对象,而且以一个十六进制的颜色值做为传参,做为咱们灯光的颜色。

var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(0, 20, 20);
spotLight.intensity = 5;
scene.add(spotLight);
复制代码

在拥有光源对象以后,咱们将调用 position.set()方法设置在三维空间中的位置。

intensity 属性用于设置光源照射的强度,默认值为 1。

最后咱们也得将光源放进咱们的场景空间之中。这样咱们的场景就有了一个 SpotLight 光源了。

摄像机

在 THREE.js 中有两种相机:

  • THREE.PerspectiveCamera(透视相机)

    符合近大远小的常理。用接近真实世界的视角来渲染场景。

  • THREE.OrthographicCamera(正交相机)

    提供了一个伪三维效果。

能够看的出来:透视相机更贴近咱们现实生活中人眼所观察到的世界,而正交相机渲染的结果和对象相距相机距离的远近没有影响。

这里我将着重介绍一下 PerspectiveCamera:

咱们先来看一张图:

对于一个透视相机来讲,咱们须要设定如下几个参数:

  • fov(视场)是竖直方向上的张角(是角度制而非弧度制)
  • aspect(长宽比)是照相机水平方向和竖直方向长度的比值
  • near(近面距离)相机到视景体最近的距离
  • far(远面距离)相机到视景体最远的距离
  • zoom(变焦)

这里咱们也将建立一个咱们本身的透视相机。

var camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.position.x = 5;
camera.position.y = 10;
camera.position.z = 10;
camera.lookAt(cube.position);
复制代码

首先咱们在实例化透视相机对象的时候,向其内部传递了几个参数:竖直方向上的张角为 75 度,长宽比与窗口相同,相机到视景体最近、最远的距离分别为 0.1 和 1000。

最后咱们让相机经过调用 lookAt()方法,看向咱们以前建立的拍摄对象 cube 的位置上。(默认状态下,相机将指向三维坐标系的原点。)

渲染器(Renderer)

在有了以上的这些对象以后,咱们离成功之差区区几步了。

在看到这一部分的标题的时候,你可能会问:什么是渲染器?

通俗地说:咱们用相机拍到的是底片,还不是真正的相片。若是你还对老式相机有所印象,这一点将不难理解。

当咱们拿着一台老式相机(还须要胶卷的那种)咱们每拍一张都将获得一张底片。咱们想要拿到正真的相片还须要带着底片,前往照相馆去洗出来。这时候老板会问你你要洗多大的相片,而后依据你的需求洗出你想要的相片。

能够说这就是渲染器的做用——洗相片。还记得咱们以前在设置相机的参数的时候,咱们并无设定相机的宽高,而是只指定了相机的长宽比。这就像咱们的底片同样,虽然小,可是却显示了咱们相片的基本长宽比。

咱们建立渲染器的方法和建立 THREE 中的其余对象同样,都须要先将对象实例化。

Three.js 为咱们提供了好几种不一样的渲染器这里咱们将使用 THREE.WebGLRenderer 渲染器做为例子。

var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
renderer.render(scene, camera);
复制代码
  • 咱们经过调用 setSize() 方法设置渲染的长宽。
  • 渲染器 renderer 的 domElement 元素,表示渲染器中的画布,全部的渲染都是画在 domElement 上的,因此这里的 appendChild 表示将这个 domElement 挂接在 body 下面,这样渲染的结果就可以在页面中显示了。
  • render()方法中传递咱们的场景和相机,至关于传递了一张由相机拍摄场景获得的一张底片,它将将图像渲染到咱们的画布中。

这时候你将获得一个以下形状:

这里咱们为了方便观察,添加了坐标系对象。

与通常对象同样,咱们经过实例化该对象,并向其内传递一个轴长参数,最后添加进咱们的场景之中。

var axes = new THREE.AxisHelper(7);
scene.add(axes);
复制代码

这里咱们的坐标系轴长设置为 7。

这时候你会发现这张图片仍是静态的,3D 的特性尚未彻底发挥出来。

动画(Animation)

在讲解动画以前咱们须要科普几个知识点,实际上扯远了一点,不过会有助于咱们去理解动画的渲染,提升性能。

理解 Event Loop

异步执行的运行机制以下:

  1. 全部同步任务都在主线程上执行,造成一个执行栈(execution context stack)。
  2. 主线程以外,还存在一个“任务队列”(task queue)。只要知足异步任务的执行条件,就在“任务队列”之中放置一个事件
  3. 一旦“执行栈”中的全部同步任务执行完毕,系统就会读取“任务队列”,看看里面有哪些事件。那些对应的异步任务,因而结束等待状态,进入执行栈,开始执行。

主线程不断重复上面的第三步。主线程从“任务队列”中读取事件,这个过程是循环不断的,因此整个的这种运行机制又称为 Event Loop(事件循环)。只要主线程空了,就会去读取“任务队列”,这就是 JavaScript 的运行机制。这个过程会循环反复。

动画原理

动画其实是由一些列的图片在必定时间内,以必定的频率播放而产生的错觉。

眼睛的一个重要特性是视觉惰性,即光象一旦在视网膜上造成,视觉将会对这个光象的感受维持一个有限的时间,这种生理现象叫作视觉暂留性。对于中等亮度的光刺激,视觉暂留时间约为 0.1 至 0.4 秒。

为了让动画连贯的、平滑的方式进行过渡,通常咱们以 60 帧每秒甚至更高的速率渲染动画。

为何不用 setInterval() 实现动画?

  • setInterval()的执行时间并非肯定的。在 Javascript 中, setInterval()任务被放进了异步队列中,只有当主线程上的任务执行完之后,才会去检查该队列里的任务是否须要开始执行,所以 setInterval()的实际执行时间通常要比其设定的时间晚一些。
  • setInterval()只能设置一个固定的时间间隔,这个时间不必定和屏幕的刷新时间相同。

以上两种状况都会致使 setInterval()的执行步调和屏幕的刷新步调不一致,从而引发丢帧现象。 那为何步调不一致就会引发丢帧呢?

首先要明白,setInterval()的执行只是在内存中对图像属性进行改变,这个变化必需要等到屏幕下次刷新时才会被更新到屏幕上。若是二者的步调不一致,就可能会致使中间某一帧的操做被跨越过去,而直接更新下一帧的图像。假设屏幕每隔 16.7ms 刷新一次(60 帧),而 setInterval()每隔 10ms 设置图像向左移动 1px, 就会出现以下绘制过程:

  • 第 0ms: 屏幕未刷新,等待中,setInterval()也未执行,等待中;
  • 第 10ms: 屏幕未刷新,等待中,setInterval()开始执行并设置图像属性 left=1px;
  • 第 16.7ms: 屏幕开始刷新,屏幕上的图像向左移动了1px, setInterval()未执行,继续等待中;
  • 第 20ms: 屏幕未刷新,等待中,setInterval()开始执行并设置 left=2px;
  • 第 30ms: 屏幕未刷新,等待中,setInterval()开始执行并设置 left=3px;
  • 第 33.4ms:屏幕开始刷新,屏幕上的图像向左移动了3px, setInterval()未执行,继续等待中;

从上面的绘制过程当中能够看出,屏幕没有更新 left=2px 的那一帧画面,图像直接从 1px 的位置跳到了 3px 的的位置,这就是丢帧现象,这种现象就会引发动画卡顿。

requestAnimationFrame()

requestAnimationFrame()的优点

与 setInterval()相比,requestAnimationFrame()最大的优点是**由系统来决定回调函数的执行时机。**具体一点讲,若是屏幕刷新率是 60 帧,那么回调函数就每 16.7ms 被执行一次,若是刷新率是 75Hz,那么这个时间间隔就变成了 1000/75=13.3ms,换句话说就是,requestAnimationFrame()的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引发丢帧现象,也不会致使动画出现卡顿的问题。

除此以外,requestAnimationFrame()还有如下两个优点:

  • CPU 节能:使用 setInterval()实现的动画,当页面被隐藏或最小化时,setInterval()仍然在后台执行动画任务,因为此时页面处于不可见或不可用状态,刷新动画是没有意义的,彻底是浪费 CPU 资源。而 requestAnimationFrame()则彻底不一样,当页面处理未激活的状态下,该页面的屏幕刷新任务也会被系统暂停,所以跟着系统步伐走的 requestAnimationFrame()也会中止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了 CPU 开销。

  • 函数节流:在高频率事件(resize,scroll 等)中,为了防止在一个刷新间隔内发生屡次函数执行,使用 requestAnimationFrame()可保证每一个刷新间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销。一个刷新间隔内函数执行屡次时没有意义的,由于显示器每 16.7ms 刷新一次,屡次绘制并不会在屏幕上体现出来。

requestAnimationFrame()的工做原理:

先来看看 Chrome 源码:

int Document::requestAnimationFrame(PassRefPtr<RequestAnimationFrameCallback> callback)
{
  if (!m_scriptedAnimationController) {
    m_scriptedAnimationController = ScriptedAnimationController::create(this);
    // We need to make sure that we don't start up the animation controller on a background tab, for example.
      if (!page())
        m_scriptedAnimationController->suspend();
  }

  return m_scriptedAnimationController->registerCallback(callback);
}
复制代码

仔细看看就以为底层实现意外地简单,生成一个 ScriptedAnimationController 的实例用于存放注册事件,而后注册这个 callback。

requestAnimationFrame 的实现原理就很明显了:

  • 注册回调函数
  • 浏览器按必定帧率更新时会触发 触发全部注册过的 callback

这里的工做机制能够理解为全部权的转移,把触发帧更新的时间全部权交给浏览器内核,与浏览器的更新保持同步。这样作既能够避免浏览器更新与动画帧更新的不一样步,又能够给予浏览器足够大的优化空间。

用 requestAnimationFrame()建立动画

咱们须要建立一个循环渲染函数,而且进行调用:

// a render loop
function render() {
  requestAnimationFrame(render);

  // Update Properties

  // render the scene
  renderer.render(scene, camera);
}
复制代码

咱们在函数体内部进行相应的属性更新并渲染,而且让浏览器来控制动画帧的更新。

制做动画

这里咱们将经过 requestAnimationFrame() 来建立咱们的动画效果。让浏览器来控制动画帧的更新最大的提升咱们的性能。

var animate = function() {
  requestAnimationFrame(animate);
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;
  renderer.render(scene, camera);
};
animate();
复制代码

咱们在 animate()方法中,经过 requestAnimationFrame(animate)来使浏览器在每次更新页面的时候调用 animate 方法。且每调用一次,正方体的属性就做出相应的改变:每一次调用都比上一次 X 轴、Y 轴各旋转 0.01 弧度,而且将其渲染到画布上。

这样咱们的动画就产生了:

THREE.Color 对象

这里我在补充说明一下 Three.js 内置的颜色对象。

一般状况下,咱们可使用十六进制的字符串("#000000")或十六进制值(0x000000)来建立指定颜色对象。咱们也能够用 RGB 颜色值来建立(0.2, 0.3, 0.4),但值得注意的是其每一个值的范围为 0 到 1。

例如:

var color = new THREE.Color(0x000000);
复制代码

在建立颜色对象以后,咱们能够对用其自身的一些方法,这里就不详细介绍了:

函数名 描述
set(value) 将当前颜色设置为指定的十六进制值。这个值能够是字符串、数值或是已有的 THREE.Color 实例。
setHex(value) 将当前颜色设置为指定的十六进制数字值。
setRGB(r,g,b) 根据提供的 RGB 值设置颜色。参数范围从 0 到 1。
setHSL(h,s,l) 根据提供的 HSL 值设置颜色。参数范围从 0 到 1。
setStyle(style) 根据 css 设置颜色的方式来设置颜色。例如:可使用 "rgb(25, 0, 0)"、"#ff0000"、"#ff" 或 "red"。
copy(color) 从提供的颜色对象复制颜色值到当前对象。
getHex() 以十六进制值形式从颜色对象中获取颜色值:435241。
getHexString() 以十六进制字符串形式从颜色对象中获取颜色值:"0c0c0c"。
getStyle() 以 css 值的形式从颜色对象中获取颜色值:"rgb(112, 0, 0)"
getHSL(optionalTarget) 以 HSL 值的形式从颜色对象中获取颜色值。若是提供了 optionTarget 对象, Three.js 将把 h、s 和 l 属性设置到该对象。
toArray 返回三个元素的数组:[r,g,b]。
clone() 复制当前颜色。

总结

能够这么说:

Three.js 的一切都创建在 Scene 对象之上。有了场景这一空间以后,咱们就能够往里面添加咱们要展现的拍摄对象了。固然有了拍摄对象以后咱们还须要一个光源,让咱们看的见咱们的对象。这时候咱们还须要一个相机,用以拍摄咱们的拍摄对象。固然咱们实际还须要靠咱们的渲染器将实际图像绘制在画布上。

经过不断变换对象的属性,而且不断地绘制咱们的场景,这就产生了动画!

附源码

<html>
  <head>
    <title>Cube</title>
    <style> body { margin: 0; overflow: hidden; } canvas { width: 100%; height: 100%; } </style>
  </head>

  <body>
    <script src="https://cdn.bootcss.com/three.js/r83/three.min.js"></script>
    <script> var scene = new THREE.Scene(); var axes = new THREE.AxisHelper(7); scene.add(axes); var geometry = new THREE.BoxGeometry(4, 4, 4); var geometryMeshBasicMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true }); var geometryMeshLambertMaterial = new THREE.MeshLambertMaterial({ color: 0x242424 }); var cube = THREE.SceneUtils.createMultiMaterialObject(geometry, [ geometryMeshBasicMaterial, geometryMeshLambertMaterial ]); scene.add(cube); var spotLight = new THREE.SpotLight(0xffffff); spotLight.position.set(0, 20, 20); spotLight.intensity = 5; scene.add(spotLight); var camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); camera.position.x = 5; camera.position.y = 10; camera.position.z = 10; camera.lookAt(cube.position); var renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); var animate = function() { requestAnimationFrame(animate); cube.rotation.x += 0.01; cube.rotation.y += 0.01; renderer.render(scene, camera); }; animate(); </script>
  </body>
</html>
复制代码

-EFO-


笔者专门在 github 上建立了一个仓库,用于记录平时学习全栈开发中的技巧、难点、易错点,欢迎你们点击下方连接浏览。若是以为还不错,就请给个小星星吧!👍


2019/04/14

AJie

相关文章
相关标签/搜索