你会用的three.js

1,482 阅读23分钟
注:文中的demo在 这里面

简介

Three.js, WebGL 与 OpenGL

提到 Three.js,就必须说一下 OpenGL 和 WebGL。
OpenGL 大概许多人都有所听闻,它是最常用的跨平台图形处理开源库。
WebGL 就是基于 OpenGL 设计的面向 web 的 3D 图形标准,它提供了一系列 JavaScript API,通过这些 API 进行图形渲染,系统硬件会加速 3D 渲染,从而获得较高性能。
而 Three.js 是 JavaScript 编写的 WebGL 第三方库,通过对 WebGL 接口的封装与简化而形成的一个易用的图形库。

WebGL 与 Three.js 对比

通过上面的简介,我们知道 WebGL 和 Three.js 都可以进行 Web 端的 3D 图形开发。那问题来了,既然我们有了 WebGL,为什么还需要 Three.js?
这是因为前端工程师想要短时间上手 WebGL 还是挺有难度的。
WebGL 门槛相对较高,计算机图形学需要相对较多的数学知识。一个前端程序员或许还熟悉解析几何,但是还熟悉线性代数的应该寥寥无几了(比如求个逆转置矩阵试试?),更何况使用中强调矩阵运算中的物理意义,这在教学中也是比较缺失。
于是,Three.js 对 WebGL 提供的接口进行了非常好的封装,简化了很多细节,大大降低了学习成本。并且,几乎没有损失 WebGL 的灵活性。
因此,从 Three.js 入手是值得推荐的,这可以让你在较短的学习后就能面对大部分需求场景。

Three.js 中的一些概念

想在屏幕上展示 3D 物体,大体上的思路是这样的:\

  1. 创建一个三维空间,Three.js 称之为场景( Scene )
  2. 确定一个观察点,并设置观察的方向和角度,Three.js 称之为相机( Camera )
  3. 在场景中添加供观察的物体,Three.js 中有很多种物体,如 Mesh、Group、Line 等,他们都继承自 Object3D 类。
  4. 最后我们需要把所有的东西渲染到屏幕上,这就是 Three.js 中的 Renderer 的作用。 下面来仔细看看这些概念吧。

Scene

放置所有物体的空间容器,对应现实的三维空间。创建一个场景也很简单,只需直接 new 一个 Scene 类即可。

Camera

Camera 相机,这个很好理解。“所见即所得”。虽然我是一个唯物主义者,不过只有被看到才能被感知。相机就相当于我们的眼睛,为了观察这个世界,我们需要描述某个物体的位置。描述物体位置需要用到坐标系。常用的坐标系有左手坐标系和右手坐标系。

image.png Three.js 采用的是右手坐标系。

Three.js 中一共有四种相机,分别为 CubeCamera、OrthographicCamera、PerspectiveCamera、StereoCamera,它们都继承自 Camera 类。我们常用的有两种,正投影相机 THREE.OrthographicCamera 和透视投影相机 THREE.PerspectiveCamera。

三维投影

正投影相机 THREE.OrthographicCamera 和透视投影相机 THREE.PerspectiveCamera,相信学过绘画的人一下子就能明白,它们对应三维投影中的正交投影和透视投影。

image.png

上面左图是正交投影,物体反射的光平行投射到屏幕上,其大小始终不变,所以远近的物体大小一样。在渲染一些 2D 效果和 UI 元素的时候会用到。右图是透视投影,符合我们平时看东西的感觉,近大远小,经常用在 3D 场景中。

视景体

视景体是一个比较重要的概念。它是指成像景物所在空间的集合。简单点说,视景体是一个几何体,只有在视景体内的物体才会被我们看到,视景体之外的物体将被裁剪掉(所见即所得)。这是为了去除不必要的计算。通过变换视景体,我们就得到不同的相机。 image.png 正交投影相机 OrthographicCamera 的视景体是一个长方体,其构造函数为 OrthographicCamera( left, right, top, bottom, near, far )。把 Camera 看作一个点,left 则表示视景体左平面在左右方向上与 Camera 的距离,另外几个参数同理。于是六个参数分别定义了视景体六个面的位置。我们可以近似地认为,视景体里的物体平行投影到近平面上,然后近平面上的图像被渲染到屏幕上。 image.png 透视投影相机 PerspectiveCamera 的视景体是一个四棱台,其构造函数为 PerspectiveCamera( fov, aspect, near, far )。fov 即 field of view,即视野,对应着图中的视角,是上下两面的夹角。aspect 是近平面的宽高比。再加上近平面距离 near,远平面距离 far,就可以唯一确定这个视景体了。

Objects

Objects 就是三维空间里的物体。Three.js 中提供了很多类型的物体,它们都继承自 Object3D 类,稍后我们会介绍。

Mesh
有时当你察觉不到时,它就不在。这一点在计算机图形学中得到充分地体现。在计算机的世界里,一条弧线是由有限个点构成的有限条线段连接得到的。当线段数量越多,长度就越短,当达到你无法察觉这是线段时,一条平滑的弧线就出现了。
计算机的三维模型也是类似的。只不过线段变成了平面,普遍用三角形组成的网格来描述。我们把这种模型称之为 Mesh 模型。 image.png Geometry
Three.js 中有很多种形状 geometry,立方体、平面、球体、圆形、圆柱、圆台等许多基本形状。Geometry 通过存储模型中的点集和点间关系(哪些点构成一个三角形)来描述物体形状。因此我们也可以通过自己定义每个点的位置来构造形状。我们还可以通过导入外部的模型文件来构造更加复杂的形状。

Material
这里的材质不仅仅指物体纹理,而是物体表面除了形状以外所有可视属性的集合,例如色彩、纹理、光滑度、透明度、反射率、折射率、发光度。
讲到材质( Material ),就还需要再讲一下贴图( Map )、纹理( Texture )。
材质上面已经提到了,它包括了贴图以及其它。
贴图其实是“贴”和“图”,它包括了图片和图片应当贴到什么位置。
纹理嘛,其实就是“图”了。
Three.js 提供了多种材质可供选择,能够自由地选择漫反射 / 镜面反射等材质。

Light
神说:要有光!
光影效果可以让画面更丰富。
Three.js 提供了包括环境光 AmbientLight、点光源 PointLight、聚光灯 SpotLight、方向光 DirectionalLight、半球光 HemisphereLight 等多种光源。
只要在场景中添加需要的光源就好了。

实现一个demo

为了真正能够让你的场景借助three.js来进行显示,我们需要以下几个对象:场景、相机和渲染器,这样我们就能透过摄像机渲染出场景。

const scene = new THREE.Scene(); 
const camera = new THREE.PerspectiveCamera( 75, 
window.innerWidth/window.innerHeight, 0.1, 1000 );  
const renderer = new THREE.WebGLRenderer(); 
renderer.setSize( window.innerWidth, window.innerHeight ); 
document.body.appendChild( renderer.domElement );

three.js里有几种不同的相机,在这里,我们使用的是PerspectiveCamera(透视摄像机)。

第一个参数是视野角度(FOV) 。视野角度就是无论在什么时候,你所能在显示器上看到的场景的范围,它的单位是角度(与弧度区分开)。

第二个参数是长宽比(aspect ratio) 。 也就是你用一个物体的宽除以它的高的值。比如说,当你在一个宽屏电视上播放老电影时,可以看到图像仿佛是被压扁的。

接下来的两个参数是近截面(near)和远截面(far)。 当物体某些部分比摄像机的远截面远或者比近截面近的时候,该这些部分将不会被渲染到场景中。或许现在你不用担心这个值的影响,但未来为了获得更好的渲染性能,你将可以在你的应用程序里去设置它。

接下来是渲染器。这里是施展魔法的地方。除了我们在这里用到的WebGLRenderer渲染器之外,Three.js同时提供了其他几种渲染器,当用户所使用的浏览器过于老旧,或者由于其他原因不支持WebGL时,可以使用这几种渲染器进行降级。

除了创建一个渲染器的实例之外,我们还需要在我们的应用程序里设置一个渲染器的尺寸。比如说,我们可以使用所需要的渲染区域的宽高,来让渲染器渲染出的场景填充满我们的应用程序。因此,我们可以将渲染器宽高设置为浏览器窗口宽高。对于性能比较敏感的应用程序来说,你可以使用setSize传入一个较小的值,例如window.innerWidth/2window.innerHeight/2,这将使得应用程序在渲染时,以一半的长宽尺寸渲染场景。

如果你希望保持你的应用程序的尺寸,但是以较低的分辨率来渲染,你可以在调用setSize时,将updateStyle(第三个参数)设为false。例如,假设你的 标签现在已经具有了100%的宽和高,调用setSize(window.innerWidth/2, window.innerHeight/2, false) 将使得你的应用程序以一半的分辨率来进行渲染。

最后一步很重要,我们将renderer(渲染器)的dom元素(renderer.domElement)添加到我们的HTML文档中。这就是渲染器用来显示场景给我们看的元素。

const geometry = new THREE.BoxGeometry(); 
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } ); 
const cube = new THREE.Mesh( geometry, material ); 
scene.add( cube );  
camera.position.z = 5;

要创建一个立方体,我们需要一个BoxGeometry(立方体)对象. 这个对象包含了一个立方体中所有的顶点(vertices)和面(faces)。未来我们将在这方面进行更多的探索。

接下来,对于这个立方体,我们需要给它一个材质,来让它有颜色。Three.js自带了几种材质,在这里我们使用的是MeshBasicMaterial。所有的材质都存有应用于他们的属性的对象。在这里为了简单起见,我们只设置一个color属性,值为0xfc5603,也就是黄色。这里所做的事情,和在CSS或者Photoshop中使用十六进制(hex colors)颜色格式来设置颜色的方式一致。

第三步,我们需要一个Mesh(网格)。 网格包含一个几何体以及作用在此几何体上的材质,我们可以直接将网格对象放入到我们的场景中,并让它在场景中自由移动。

默认情况下,当我们调用scene.add() 的时候,物体将会被添加到 (0,0,0) 坐标。但将使得摄像机和立方体彼此在一起。为了防止这种情况的发生,我们只需要将摄像机稍微向外移动一些即可。

渲染场景

现在,如果将之前写好的代码复制到HTML文件中,你不会在页面中看到任何东西。这是因为我们还没有对它进行真正的渲染。为此,我们需要使用一个被叫做“渲染循环”(render loop)或者“动画循环”(animate loop)的东西。

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

在这里我们创建了一个使渲染器能够在每次屏幕刷新时对场景进行绘制的循环(在大多数屏幕上,刷新率一般是60次/秒)。如果你是一个浏览器游戏开发的新手,你或许会说 “为什么我们不直接用setInterval来实现刷新的功能呢?” 当然啦,我们的确可以用setInterval,但是,requestAnimationFrame有很多的优点。最重要的一点或许就是当用户切换到其它的标签页时,它会暂停,因此不会浪费用户宝贵的处理器资源,也不会损耗电池的使用寿命。

使立方体动起来

在开始之前,如果你已经将上面的代码写入到了你所创建的文件中,你可以看到一个绿色的方块。让我们来做一些更加有趣的事 —— 让它旋转起来。

将下列代码添加到animate()函数中renderer.render调用的上方:

cube.rotation.x += 0.01;
cube.rotation.y += 0.01;

这段代码每帧都会执行(正常情况下是60次/秒),这就让立方体有了一个看起来很不错的旋转动画。基本上来说,当应用程序运行时,如果你想要移动或者改变任何场景中的东西,都必须要经过这个动画循环。当然,你可以在这个动画循环里调用别的函数,这样你就不会写出有上百行代码的animate函数。

结果

下面是完整的代码

<html lang="en">
<head>
    <meta charset="utf-8">
    <title>My first three.js app</title>
    <style>
        body { margin: 0; }
    </style>
</head>
<body>
<script src="js/three.js"></script>
<script>
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize( window.innerWidth, window.innerHeight );
    document.body.appendChild( renderer.domElement );
    const geometry = new THREE.BoxGeometry();
    const material = new THREE.MeshBasicMaterial( { color: 0xfc5603 } );
    const cube = new THREE.Mesh( geometry, material );
    scene.add( cube );
    camera.position.z = 5;
    const animate = function () {
        requestAnimationFrame( animate );
        cube.rotation.x += 0.01;
        cube.rotation.y += 0.01;
        renderer.render( scene, camera );
    };
    animate();
</script>
</body>

起步

安装自 npm

npm install --save three

// 方式 1: 导入整个 three.js核心库 
import * as THREE from 'three'; 
const scene = new THREE.Scene(); 

// 方式 2: 仅导入你所需要的部分 
import { Scene } from 'three'; 
const scene = new Scene();

从CDN或静态主机安装

<script type="module"> 
// 通过访问 https://cdn.skypack.dev/three 来查找最新版本。 
import * as THREE from 'https://cdn.skypack.dev/three@<version>'; 
const scene = new THREE.Scene(); 
</script>

WebGL兼容性检查

github.com/mrdoob/thre…引入到你的文件,并在尝试开始渲染之前先运行该文件。

if (WEBGL.isWebGLAvailable()) { 
// Initiate function or other initializations here
animate();
} else {
const warning = WEBGL.getWebGLErrorMessage(); 
document.getElementById('container').appendChild(warning);
}

本地运行Three.js

Node.js server:

Node.js 具有一个简单的HTTP服务器包,如需安装,请执行:

npm install http-server -g

若要从本地目录下运行,请执行:

http-server . -p 8000

推荐使用react脚手架或者使用本文的demo

yarn

yarn start

注:引用外部字体或者模型资源时报 Unexpected string in JSON at position 1 / 0 的错误,我们vue将资源放到static中,react放到public直接引用就能解决 image.png

核心loader的使用

画线 (LineBasicMaterial)

假设你将要画一个圆或者画一条线,而不是一个线框模型,或者说不是一个Mesh(网格)。 第一步我们要做的,是设置好renderer(渲染器)、Scene(场景)和Camera(相机)

这是我们将要用到的代码:

const renderer = new THREE.WebGLRenderer(); 
renderer.setSize( window.innerWidth, 
window.innerHeight ); 
document.body.appendChild( renderer.domElement ); 
const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 500 ); 
camera.position.set( 0, 0, 100 ); 
camera.lookAt( 0, 0, 0 ); const scene = new THREE.Scene();

接下来我们要做的事情是定义一个材质。对于线条来说,我们能使用的材质只有LineBasicMaterial 或者 LineDashedMaterial

//create a blue LineBasicMaterial 
const material = new THREE.LineBasicMaterial( { color: 0x0000ff } );

定义好材质之后,我们需要一个带有一些顶点的geometry(几何体)。

const points = [];
points.push( new THREE.Vector3( - 10, 0, 0 ) ); 
points.push( new THREE.Vector3( 0, 10, 0 ) );
points.push( new THREE.Vector3( 10, 0, 0 ) ); 
const geometry = new THREE.BufferGeometry().setFromPoints( points );

注意,线是画在每一对连续的顶点之间的,而不是在第一个顶点和最后一个顶点之间绘制线条(线条并未闭合)。

既然我们已经有了能够画两条线的点和一个材质,那我们现在就可以将他们组合在一起,形成一条线。

const line = new THREE.Line( geometry, material );

剩下的事情就是把它添加到场景中,并调用render(渲染)函数。

scene.add( line ); renderer.render( scene, camera );

你现在应当已经看到了一个由两条蓝线组成的、指向上的箭头。

image.png

创建文字

构造函数 Font(文字) 并布置到场景中

FileLoader

使用XMLHttpRequest来加载资源的低级类,并由大多数加载器内部使用。 它也可以直接用于加载任何没有对应加载器的文件类型。

文本缓冲几何体 (TextGeometry)

一个用于将文本生成为单一的几何体的类。 它是由一串给定的文本,以及由加载的 Font(字体)和该几何体ExtrudeGeometry父类中的设置所组成的参数来构造的。 请参阅FontFontLoader页面来查看更多详细信息。

// loader
const loader = new THREE.FontLoader();
loader.load('/helvetiker_bold.typeface.json', function (font) {
    const color = 0x006699;
    // 阴影粗体
    const matDark = new THREE.LineBasicMaterial({
        color: color,
        side: THREE.DoubleSide
    });
    // 字体
    const matLite = new THREE.MeshBasicMaterial({
        color: color,
        transparent: true,
        opacity: 0.4,
        side: THREE.DoubleSide
    });
    const message = "   Text What\nYou Want to text.";
    const shapes = font.generateShapes(message, 100);
    const geometry = new THREE.ShapeGeometry(shapes);
    geometry.computeBoundingBox();
    const xMid = -0.5 * (geometry.boundingBox.max.x - geometry.boundingBox.min.x);
    geometry.translate(xMid, 0, 0);

    const text = new THREE.Mesh(geometry, matLite);
    text.position.z = -150;
    scene.add(text);
    const holeShapes = [];
    for (let i = 0; i < shapes.length; i++) {
        const shape = shapes[i];
        if (shape.holes && shape.holes.length > 0) {
            for (let j = 0; j < shape.holes.length; j++) {
                const hole = shape.holes[j];
                holeShapes.push(hole);
            }
        }
    }
    shapes.push.apply(shapes, holeShapes);

    const lineText = new THREE.Object3D();
    for (let i = 0; i < shapes.length; i++) {
        const shape = shapes[i];
        const points = shape.getPoints();
        const geometry = new THREE.BufferGeometry().setFromPoints(points);
        geometry.translate(xMid, 0, 0);
        const lineMesh = new THREE.Line(geometry, matDark);
        lineText.add(lineMesh);
    }
    scene.add(lineText);
});

再来一遍 renderer(渲染器)、scene(场景)和camera(相机)三件套 -->后面场景将会忽略这步

let camera, scene, renderer;
init();
animate();

function init() {
    // 相机
    camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 10000);
    camera.position.set(100, -1600, 1800);

    // 场景
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0xf0f0f0);

    // ...loader

    renderer = new THREE.WebGLRenderer({antialias: true});
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderHelper(renderer.domElement)

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.target.set(0, 0, 0);
    controls.update();

    window.addEventListener('resize', onWindowResize);
}

function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();

    renderer.setSize(window.innerWidth, window.innerHeight);
}

function animate() {
    requestAnimationFrame(animate);
    render();
}

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

image.png

载入3D模型

目前,3D模型的格式有成千上万种可供选择,但每一种格式都具有不同的目的、用途以及复杂性。 虽然 three.js已经提供了多种导入工具, 但是选择正确的文件格式以及工作流程将可以节省很多时间,以及避免遭受很多挫折。某些格式难以使用,或者实时体验效率低下,或者目前尚未得到完全支持。

推荐的工作流程

推荐使用glTF(gl传输格式)。.GLB和.GLTF是这种格式的这两种不同版本, 都可以被很好地支持。由于glTF这种格式是专注于在程序运行时呈现三维物体的,所以它的传输效率非常高,且加载速度非常快。 功能方面则包括了网格、材质、纹理、皮肤、骨骼、变形目标、动画、灯光和摄像机。

公共领域的glTF文件可以在网上找到,例如 Sketchfab,或者很多工具包含了glTF的导出功能:

Texture loader 俗称贴图 loader,一个图片对象,通常由TextureLoader.load方法创建。 该对象可以是被three.js所支持的任意图片(例如PNG、JPG、GIF、DDS)或视频(例如MP4、OGG/OGV)格式。

const texture = new THREE.TextureLoader().load( "textures/water.jpg" ); 
texture.wrapS = THREE.RepeatWrapping; 
texture.wrapT = THREE.RepeatWrapping; 
texture.repeat.set( 4, 4);

GLTF loader glTF(gl传输格式)是一种开放格式的规范 (open format specification), 用于更高效地传输、加载3D内容。该类文件以JSON(.glft)格式或二进制(.glb)格式提供, 外部文件存储贴图(.jpg、.png)和额外的二进制数据(.bin)。一个glTF组件可传输一个或多个场景, 包括网格、材质、贴图、蒙皮、骨架、变形目标、动画、灯光以及摄像机

参阅GLTFLoader documentation来深入了解详细信息。 一旦你引入了一个加载器,你就已经准备好为场景添加模型了。不同加载器之间可能具有不同的语法 —— 当使用其它格式的时候请参阅该格式加载器的示例以及文档。对于glTF,使用全局script的用法类似:

const loader = new GLTFLoader(); 
loader.load( 'path/to/model.glb', function ( gltf ) { 
scene.add( gltf.scene );
}, undefined, function ( error ) { 
console.error( error );
} );

Object Loader 此加载器内部使用FileLoader进行加载文件。

const loader = new THREE.ObjectLoader();
loader.load( 
// 资源的URL 
"models/json/example.json", 
// onLoad回调 
// Here the loaded data is assumed to be an object 
function ( obj ) { // Add the loaded object to the scene scene.add( obj );
}, 
// onProgress回调 
function ( xhr ) { console.log( (xhr.loaded / xhr.total * 100) + '% loaded' );
}, 
// onError回调 
function ( err ) { console.error( 'An error happened' ); 
} );

如何更新场景

默认情况下,所有对象都会自动更新它们的矩阵(如果它们已添加到场景中)

const object = new THREE.Object3D();
scene.add( object );

或者它们是已添加到场景中的另一个对象的子节点:

const object1 = new THREE.Object3D();
const object2 = new THREE.Object3D(); 
object1.add( object2 );
scene.add( object1 ); //object1 和 object2 会自动更新它们的矩阵

但是,如果你知道对象将是静态的,则可以禁用此选项并在需要时手动更新转换矩阵。

object.matrixAutoUpdate = false; 
object.updateMatrix();

BufferGeometry

是面片、线或点几何体的有效表述。包括顶点位置,面片索引、法相量、颜色值、UV 坐标和自定义缓存属性值。使用 BufferGeometry 可以有效减少向 GPU 传输上述数据所需的开销。

读取或编辑 BufferGeometry 中的数据,见 BufferAttribute 文档。

const geometry = new THREE.BufferGeometry(); 
// 创建一个简单的矩形. 在这里我们左上和右下顶点被复制了两次。
// 因为在两个三角面片里,这两个顶点都需要被用到。 
const vertices = new Float32Array( [ -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0 ] ); 
// itemSize = 3 因为每个顶点都是一个三元组。 
geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
const material = new THREE.MeshBasicMaterial( { color: 0xff0000 } ); 
const mesh = new THREE.Mesh( geometry, material );

例子Mesh with non-indexed faces

如何废置对象

为了提高性能,并避免应用程序中的内存泄露,一个重要的方面是废置未使用的类库实体。 每当你创建一个three.js中的实例时,都会分配一定数量的内存。然而,three.js会创建在渲染中所必需的特定对象, 例如几何体或材质,以及与WebGL相关的实体,例如buffers或着色器程序。 非常值得注意的是,这些对象并不会被自动释放;相反,应用程序必须使用特殊的API来释放这些资源。 本指南简要概述了这一API是如何使用的,以及哪些对象是和这一环境相关的。

几何体

几何体常用来表示定义为属性集合的顶点信息,three.js在内部为每一个属性创建一个WebGLBuffer类型的对象。 这些实体仅有在调用BufferGeometry.dispose()的时候才会被删除。 如果应用程序中的几何体已废弃,请执行该方法以释放所有相关资源。

材质

材质定义了物体将如何被渲染。three.js使用材质所定义的信息来构造一个着色器程序,以用于渲染。 着色器程序只有在相应材质被废置后才能被删除。由于性能的原因,three.js尽可能尝试复用已存在的着色器程序。 因此,着色器程序只有在所有相关材质被废置后才被删除。 你可以通过执行Material.dispose()方法来废置材质。

纹理

对材质的废置不会对纹理造成影响。它们是分离的,因此一个纹理可以同时被多个材质所使用。 每当你创建一个Texture实例的时候,three.js在内部会创建一个WebGLTexture实例。 和buffer相似,该对象只能通过调用Texture.dispose()来删除。

渲染目标

WebGLRenderTarget类型的对象不仅分配了WebGLTexture的实例, 还分配了WebGLFramebufferWebGLRenderbuffer来实现自定义渲染目标。 这些对象仅能通过执行WebGLRenderTarget.dispose()来解除分配。

杂项

有一些来自examples目录的类,例如控制器或者后期处理过程,提供了dispose() 方法以用于移除内部事件监听器或渲染目标。 通常来讲,非常建议查阅类的API或者文档,并注意dispose() 函数。如果该函数存在的话,你应当在清理时使用它。

如何使用后期处理

很多three.js应用程序是直接将三维物体渲染到屏幕上的。 有时,你或许希望应用一个或多个图形效果,例如景深、发光、胶片微粒或是各种类型的抗锯齿。 后期处理是一种被广泛使用、用于来实现这些效果的方式。 首先,场景被渲染到一个渲染目标上,渲染目标表示的是一块在显存中的缓冲区。 接下来,在图像最终被渲染到屏幕之前,一个或多个后期处理过程将滤镜和效果应用到图像缓冲区。

three.js通过EffectComposer(效果合成器),提供了一个完整的后期处理解决方案来实现这样的工作流程。

工作流程

首先,我们要做的是从示例(examples)文件夹导入所有必需的文件。本指南假设你正在使用three.js官方npm包(npm package)。 在本指南的基础示例中,我们需要下列文件。

import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'; 
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'; 
import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass.js';

当这些文件被成功导入后,我们便可以通过传入一个WebGLRenderer的实例,来创建我们的合成器了

const composer = new EffectComposer( renderer );

在使用合成器时,我们需要对应用程序的动画循环进行更改。 现在我们不再调用WebGLRenderer的render方法,而是使用EffectComposer中对应的render方法。

function animate() { 
requestAnimationFrame( animate ); 
composer.render(); 
}

我们的合成器已经准备好了,现在我们就可以来配置后期处理过程链了。 这些过程负责创建应用程序的最终视觉输出,它们按照添加/插入的顺序来进行处理。 在我们的示例中,首先执行的是RenderPass实例,然后是GlitchPass。在链中的最后一个过程将自动被渲染到屏幕上。 这些过程的设置类似这样:

const renderPass = new RenderPass( scene, camera ); 
composer.addPass( renderPass ); const glitchPass = new GlitchPass(); 
composer.addPass( glitchPass );

RenderPass通常位于过程链的开始,以便将渲染好的场景作为输入来提供给下一个后期处理步骤。 在我们的示例中,GlitchPass将会使用这些图像数据,来应用一个疯狂的故障效果。参见这个示例: live example来看一看它的实际效果。

内置过程

你可以使用由本引擎提供的各种预定义好的后期处理过程, 它们位于postprocessing目录中。

自定义过程

有时你或许想要自己写一个自定义后期处理着色器,并将其包含到后期处理过程链中。 对于这个需求,你可以使用ShaderPass。在引入该文件以及你的自定义着色器后,可以使用下列代码来设置该过程:

import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'; 
import { LuminosityShader } from 'three/examples/jsm/shaders/LuminosityShader.js'; 
// later in your init routine 
const luminosityPass = new ShaderPass( LuminosityShader ); 
composer.addPass( luminosityPass );

仓库中提供了一个名为CopyShader的文件, 这是你自定义自己的着色器的一个很好的起始代码。CopyShader仅仅是拷贝了读缓冲区中的图像内容到写缓冲区,不会应用任何效果。

矩阵变换(Matrix transformations)

Three.js使用matrix编码3D变换 —— 平移(位置),旋转和缩放。 Object3D的每个实例都有一个matrix,用于存储该对象的位置,旋转和比例。本页介绍如何更新对象的变换。

便利的属性和matrixAutoUpdate(Convenience properties and matrixAutoUpdate

有两种方法可以更新对象的转换:

  1. 修改对象的positionquaternionscale属性,让three.js重新计算来自这些属性的对象矩阵:
object.position.copy( start_position ); 
object.quaternion.copy( quaternion );

默认情况下,matrixAutoUpdate属性设置为true,并且将自动重新计算矩阵。 如果对象是静态的,或者您希望在重新计算时手动控制,则可以通过将属性设置为false来获得更好的性能:object.matrixAutoUpdate = false;更改任何属性后,手动更新矩阵:object.updateMatrix(); 2. 直接修改对象的矩阵。 Matrix4类有各种修改矩阵的方法:

object.matrix.setRotationFromQuaternion( quaternion ); 
object.matrix.setPosition( start_position );
object.matrixAutoUpdate = false;

请注意,在这种情况下,matrixAutoUpdate 必须 设置为false,并且您应该确保 调用updateMatrix。 调用updateMatrix将破坏对矩阵所做的手动更改,从positionscale重新计算矩阵,依此类推。

对象和世界矩阵(Object and world matrices)

一个对象的matrix存储了该对象 相对于Object3D.parent(父节点)的变换。要在 世界 坐标系中获取对象的转换,您必须访问该对象的Object3D.matrixWorld

当父对象或子对象的变换发生更改时,可以通过调用[page:Object3D.updateMatrixWorld updateMatrixWorld()]来请求更新子对象的matrixWorld

旋转和四元数(Rotation and Quaternion)

Three.js提供了两种表示3D旋转的方式:Euler angles(欧拉角)和Quaternions(四元数),以及两者之间的转换方法。 欧拉角有称为“万向节锁定”的问题,其中某些配置可能失去一定程度的自由度(防止物体绕一个轴旋转)。 因此,对象旋转 始终 存储在对象的quaternion中。

该库的早期版本包含useQuaternion属性,当设置为false时,将导致对象的matrix从欧拉角计算。这种做法已被弃用 - 作为代替,您应该使用setRotationFromEuler方法,该方法将更新四元数。

参考自three.js官网

下一章将会使用three.js做一个地球🌍