前言
在网上经常看到Three.js的身影,却始终徘徊不前,临门不入。一方面感叹Three.js做出的炫酷效果,一方面看着大段的陌生代码,头皮略微有些发麻,加上实际项目用不上,学习意愿一直不强烈。最近突然心血来潮,想揭开Three.js的神秘面纱。本文的目标是实现一个文章顶部的旋转动效。围绕如何实现这个动效,我们把Three.js相关知识点了解一下。下面我们开始进入正题。
Three.js是什么?
Three.js是一个 3D JavaScript 库。要阐述清楚Three.js,就得先说说WebGL。它们之间关系密切。WebGL(Web Graphics Library )是一种 3D 绘图技术标准,这种绘图技术标准可以让 JavaScript 和 OpenGL ES 2.0 结合在一起,通过给OpenGL ES 2.0 增加JavaScript 绑定, WebGL 可以为 HTML5 Canvas 提供硬件 3D 加速渲染,这样 Web 开发人员就可以借助系统显卡来在浏览器里更流畅地展示 3D 场景和模型。 WebGL 技术被用于创建复杂的 3D 结构网页和 3D 网页游戏。
WebGL解决了现有Web交互式三维动画的两个痛点:
- 无需任何浏览器插件支持,通过JavaScript本身就能实现 Web 交互式三维动画制作;
- 通过统一、标准、跨平台的OpenGL接口,利用底层图形硬件加速,提高图形渲染效率和质量。
WebGL 是一个底层的标准, 这些标准被定义之后, Chrome、Firefox等现代主流浏览器实现了这些标准。程序员通过 JavaScript 代码, 就能在网页上实现三维图形的渲染了。原生的 WebGL 比较复杂,所以我们经常会使用一些三方的库,如 Three.js、Cesium.js、Babylon.js等。Three.js 封装了底层的图形接口,使得程序员能够在无需掌握繁冗的图形学知识的情况下,也能用简单的代码实现三维场景的渲染。一般情况下, 高度的封装和灵活性是相矛盾的, 可是 Three.js 却做到了鱼与熊掌兼得。几乎没有 WebGL 支持而 Three.js 实现不了的功能, 如果遇到这种情况,依然可以用 WebGL 去实现, 不会和 Three.js 库产生冲突。
Three.js 和 D3.js区别
D3 的全称是(Data-Driven Documents),是一个很流行,使用很广泛的 JavaScript 数据可视化库。D3 遵循现有的 Web 标准,使用 HTML, CSS, SVG 以及 Canvas 来展示数据。可以不需要其它任何框架独立运行在现代浏览器中。其核心是使用绘图指令对数据进行转换,在源数据的基础上创建新的可绘制数据,生成SVG路径以及通过数据和方法在DOM中创建数据可视化元素。因为 JavaScript 文件的后缀名通常是.js,故 D3 也常被称呼为 D3.js 。D3 提供了各种简单易用的函数,大大简化了 JavaScript 操作数据的难度。D3 将生成可视化的复杂步骤精简到了几个简单的函数,只需要输入简单数据,就能够转换为各种绚丽的图形。看完D3的介绍,你会发现从用途方面来说,D3.js和 ECharts, Highcharts是一类,和Three.js是两个不同的东西,底层依赖的技术并不相同。容易混淆的地方是它们的名字中都带有3,都可以绘制图形。
Three.js基础概念
WebGL 的渲染是需要 HTML5 Canvas 元素的, 可以手动在 HTML 的 部分中定义Canvas 元素, 也可以用 Three.js 生成,这两种方式均可。一个典型的 Three.js 程序至少要包括渲染器(Renderer)、场景(Scene)、照相机(Camera), 以及你在场景中创建的物体。首先介绍一些这些概念,不理解这些概念的话,你就看不懂Three.js的编写的代码功能。
渲染器(Renderer)
渲染器将和 Canvas 元素进行绑定, 渲染器的参数有:
// 创建渲染器对象
var renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('can3d'), //渲染器绘制其输出的画布,
alpha: false, // 画布是否包含alpha(透明度)缓冲区。默认值为false。
premultipliedAlpha: true, //渲染器是否会假设颜色具有 预乘alpha。默认为true。
antialias: true, //是否执行抗锯齿。默认值为false。
preserveDrawingBuffer: true, //是否保留缓冲区直到手动清除或覆盖。默认值为false。
depth: true, //绘图缓冲区是否具有至少16位的 深度缓冲区。默认为true。
autoClear: true, //定义渲染器是否应在渲染帧之前自动清除其输出。
//以下为高级进阶调渲染后期
gammaFactor: 0.5, //伽马基础值
gammaInput: true, //如果设置,那么它期望所有纹理和颜色都是预乘伽马。默认值为false。
gammaOutput: true, //如果设置,那么它期望所有纹理和颜色需要以预乘伽马输出。默认值为false。
shadowMap: null, //如果使用,它包含阴影贴图的引用。
physicalCorrectLights: true, //是否使用物理上正确的照明模式。默认值为false。
toneMapping: 0.5, //曝光值
toneMappingExposure: 1, //色调映射的曝光级别。默认值为1。
renderLists: [], //在内部用于处理场景对象渲染的排序
sortObjects: true //定义渲染器是否应对对象进行排序。默认为true。
})
如果再 HTML 中定义了 id 为 xx 的 Canvas 元素,那么 Renderer 可以这样写:
var renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('xx')
});
而如果想用 Three.js 生成 Canvas 元素,就不需要在 HTML 中定义 Canvas 元素,在 JavaScript 代码中可以这样写:
var renderer = new THREE.WebGLRenderer();
// canvas元素宽400px,高300px
renderer.setSize(400, 300);
// 将渲染器添加到body元素中
document.getElementsByTagName('body')[0].appendChild(renderer.domElement);
// 设置canvas元素的背景色
renderer.setClearColor(0x000000);
场景(Scene)
相当于一个大容器,场景没有复杂的操作。在 Three.js 中添加的物体都是添加到场景中的。在程序运行的开始处对场景进行实例化, 随后将照相机,物体添加到场景中即可。
var scene = new THREE.Scene();
// ...
scene.add('照相机');
scene.add('物体');
照相机(Camera)
照相机在这里是类比, 并非我们日常所说的照相机。Three.js 创建的场景是三维的, 而通常情况下显示屏是二维的,那么三维的场景如何显示到二维显示屏上呢? 照相机就是这样一个抽象, 它定义了三维空间到二维屏幕的投影方式。Three.js 中一共有四种相机,分别为PerspectiveCamera(透视相机)、 OrthographicCamera(正交相机)、CubeCamera(立方相机,可以创造反光效果)、StereoCamera(立体相机,用于创造3D立体影像),它们都继承自 Camera 类。我们常用的有两种,透视投影相机 THREE.PerspectiveCamera 和 正投影相机 THREE.OrthographicCamera , 在这里重点介绍一下。
-
透视投影 比较接近人眼看物体的直觉,近大远小。对于透视投影而言,投影的结果除了与几何体的角度有关,还和距离相关。
-
正交投影 对于正投影而言,一条直线放置的角度不同,投影在投影面上面的长短不同,在三维空间平行的线,投影到二维空间也是平行的,投影之后物体的比例大小不变。
两种投影的区别如下图所示:透视投影看物体近大远小;正交投影在三维空间内平行的线, 投影到二维空间中也是平行的。
一般说来, 制图、建模软件通常使用正交投影,这样不会因为投影而改变物体比例;而对于其它大多数应用, 通常使用透视投影, 因为这更接近人眼的观察效果。
// 创建透视投影镜头
// PerspectiveCamera() 中的 4 个参数分别为:
// 1、fov(field of view 的缩写),可选参数,默认值为 50,指垂直方向上的角度,注意该值是度数不是弧度
// 2、aspect,可选参数,默认值为 1,画布的宽高比(宽/高)
// 3、near,可选参数,默认值为 0.1,近平面,限制摄像机可绘制最近的距离,若小于该距离则不会绘制
// 4、far,可选参数,默认值为 2000,远平面,限制摄像机可绘制最远的距离,若超出该距离则不会绘制
const camera = new PerspectiveCamera(50, 1, 0.1, 2000);
// 创建正交投影镜头
// left — 摄像机视锥体左侧面。
// right — 摄像机视锥体右侧面。
// top — 摄像机视锥体上侧面。
// bottom — 摄像机视锥体下侧面。
// near — 摄像机视锥体近端面。 其值的有效范围介于0和far之间, 默认值是0.1
// far — 摄像机视锥体远端面。默认值为2000
const camera = THREE.OrthographicCamera(left, right, top, bottom, near, far);
three.js 采用的是右手坐标系,什么是右手坐标系?左右手坐标系的区别如下图所示。相机默认是由正z轴看向-z轴
我们现在定义一个透视投影的照相机, 照相机要添加到场景中。
var camera = new THREE.PerspectiveCamera(45, 4 / 3, 1, 1000);
// 设置相机初始位置, x=0,y=0,z=5
camera.position.set(0, 0, 5);
scene.add(camera);
物体
Three.js 中提供了很多类型的物体,它们都继承自 Object3D 类,我们逐一介绍一下。
Mesh(网格)
Mesh是由一系列多边形组成的,三角形或者四边形,网格一般由顶点来描绘,常见的三维开发模型就是由一系列的点组成的。它也是其它网格对象的基类,例如SkinnedMesh。
// geometry 几何体实例
// material 用来定义对象的外观。缺省是一个启用线框模式和随机颜色的 基础网孔材料(MeshBasicMaterial) 。
// 可以是一个材质(`material`)或多个材质(`material`),多个材质对应几何体的各个面。
new THREE.Mesh( geometry, material )
示例,创建一个正方体网格。
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
const mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );
Geometry(几何体)
Threejs 中几何体的基类是 BufferGeometry,而 BufferGeometry 是面片、线或点几何体的有效表述。包括顶点位置,面片索引、法相量、颜色值、UV 坐标和自定义缓存属性值。使用 BufferGeometry 可以有效减少向 GPU 传输图像数据开销。
Three.js 中有很多种内置的几何体,如下图所示。 将几何体与材质(Material)相结合,可以创建出形状丰富、颜色各异的三维物体。也可以自定义每个点的位置或者通过导入外部的模型文件来构造自己的几何体。
以长方体BoxGeometry为例,看看如何创建几何体。BoxGeometry有6个参数,含义如下:
// width — X轴上面的宽度,默认值为1。
// height — Y轴上面的高度,默认值为1。
// depth — Z轴上面的深度,默认值为1。
// widthSegments — (可选)宽度的分段数,默认值是1。
// heightSegments — (可选)高度的分段数,默认值是1。
// depthSegments — (可选)深度的分段数,默认值是1。
BoxGeometry(width : Float, height : Float, depth : Float, widthSegments : Integer, heightSegments : Integer, depthSegments : Integer)
什么是分段数 ?看看下面这幅图,你应该就理解了。把每个方向分成几段。
var geometry = new THREE.BoxGeometry(10, 10, 10, 3, 3, 3);
var material = new THREE.MeshBasicMaterial({
color: '#ff0000',
wireframe: true,
opacity: 1,
transparent: true
});
mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
Material(材质)
在three.js中,材质决定了几何图形中的表面是如何画的。如果几何图形是骨架,定义了形状,那么材质就是皮肤。材质不仅仅指物体纹理,而是物体表面除了形状以外所有可视属性的集合,例如色彩、纹理、光滑度、透明度、反射率、折射率、发光度。three.js 中有许多不同种类的材质,它们拥有不同的属性,像反光,纹理映射,调整透明度。
每种材质都有设置参数,以基础材质为例,它的设置参数有:
new THREE.MeshBasicMaterial({
// 是否设置为透明, 默认false
transparent: true,
// alpha地图是一种灰度纹理,它控制着表面的不透明度(黑色:完全透明;白:完全不透明)。默认为null。
alphaMap: null,
// 材料的颜色值,默认为白色
color: "0xfff",
//将材质表面颜色与环境贴图相结合,默认为THREE.Multiply,如果选择混合模式,则反射率是用来混合两种颜色的
combine: THREE.Multiply,
// 环境贴图,默认为null
envMap: null,
// 灯光贴图,默认为null
lightMap: null,
// 灯光贴图的强度,默认为1
lightMapIntensity: 1,
// 材料是否受到光线影响,默认为false
lights: false,
// 贴图,默认为null
map: null,
// 反射率,表面对环境的影响程度,有效范围在0 - 1之间,默认为1
reflectivity: 1,
// 是否以线框模式呈现,默认为false
wireframe: false,
});
Light(灯光)
光影效果可以让画面更丰富。在场景中添加灯光后,灯光照射在物体上产生明暗、光亮和阴影,从而让物体显得更加立体有光泽。在 Three.js 中,有 6 种基础类型的灯光,他们都继承于 Three.Light。如下表所示:
| 灯光类型(都继承于Light) | 灯光名称 | 是否支持阴影 | 是否作用于全局(无处不在) | 是否有照射目标 |
|---|---|---|---|---|
| AmbientLight | 环境光、氛围光 | 否 | 是 | 无 |
| DirectionalLight | 平行光 | 是 | 否 | 有 |
| HemisphereLight | 半球光源、户外光源 | 否 | 是 | 无 |
| PointLight | 点光源 | 是 | 否 | 有 |
| RectAreaLight | 矩形面光源 | 否 | 否 | 无 |
| SpotLight | 聚光灯光源 | 是 | 否 | 有 |
- 是否有照射目标是指什么?
就是这个光除了光源本身之外,还包含一个 target 属性,并且可以通过设置 target.position 的位置。对于有照射目标的灯光,在场景中不光要添加灯光本身,还可以添加灯光照射目标。
我们以环境光为例,创建一个光源。
// color Integer 颜色的rgb数值。缺省值为 0xffffff。 (可选)
// intensity Float 光照的强度。缺省值为 1。(可选)
AmbientLight( color : Integer, intensity : Float )
光源也要添加到场景中。
const ambientLight = new THREE.AmbientLight(0xffffff, 0.2);
scene.add(ambientLight);
渲染
在定义了场景中的物体,设置好照相机之后,渲染器就知道渲染什么内容了。只需调用渲染器的渲染函数,就能把渲染出来。
renderer.render(scene, camera);
入门实例
现在让我们来实现一个文章开头的效果。依次创建渲染器,场景,相机,物体(两个立方体+光源), 最后再加一个转动动画,效果就出来了。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
<title>Three.js 入门指南DEMO</title>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<!-- CDN Link to Three.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/87/three.js"></script>
<script src="./roateCube.js"></script>
</body>
</html>
./roateCube.js内容如下:
// 创建渲染器
const renderer = new THREE.WebGLRenderer({
//是否执行抗锯齿。默认值为false
antialias: true,
});
// 设置渲染器的尺寸,也就是生成canvas元素的宽高
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置canvas背景色(clearColor)和背景色透明度(clearAlpha),透明度默认为1, 1-不透明
renderer.setClearColor("green");
// 添加canvas元素到body中
document.body.appendChild(renderer.domElement);
// 创建场景
const scene = new THREE.Scene();
// 创建透视照相机
// 1、fov(field of view 的缩写),可选参数,默认值为 50,指垂直方向上的角度,注意该值是度数不是弧度
// 2、aspect,可选参数,默认值为 1,画布的宽高比(宽/高)
// 3、near,可选参数,默认值为 0.1,近平面,限制摄像机可绘制最近的距离,若小于该距离则不会绘制
// 4、far,可选参数,默认值为 2000,远平面,限制摄像机可绘制最远的距离,若超出该距离则不会绘制
const camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5;
// resize 事件
window.addEventListener("resize", () => {
let width = window.innerWidth;
let height = window.innerHeight;
renderer.setSize(width, height);
camera.aspect = width / height;
// 渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix
// 但是不会每渲染一帧,就通过相机的属性计算投影矩阵(节约计算资源)
// 如果相机的一些属性发生了变化,就需要手动执行updateProjectionMatrix ()方法更新相机的投影矩阵
camera.updateProjectionMatrix();
});
// 外部较大的正方体
// BoxGeometry用来创建长方体
const geometry2 = new THREE.BoxGeometry(3, 3, 3);
const material2 = new THREE.MeshBasicMaterial({
color: "#dadada",
wireframe: true,
transparent: true,
});
const wireframeCube = new THREE.Mesh(geometry2, material2);
scene.add(wireframeCube);
// 里面小的立体方
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({
color: 0xff0051,
flatShading: true,
metalness: 0,
roughness: 1,
});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.2);
scene.add(ambientLight);
// 点光源
const pointLight = new THREE.PointLight(0xffffff, 1);
pointLight.position.set(25, 50, 25);
scene.add(pointLight);
function animate() {
requestAnimationFrame(animate);
// 大的立方体旋转角度大
cube.rotation.x += 0.04;
cube.rotation.y += 0.04;
// 大的立方体旋转角度小
wireframeCube.rotation.x -= 0.01;
wireframeCube.rotation.y -= 0.01;
renderer.render(scene, camera);
}
animate();
结语
感觉学Three.js和学CSS中的动画感觉一样,即便语法你都会,你也做不出很炫酷的效果。真不知这些炫酷动效,是如何被设计出来以及实现的(听说是通过创建图形模型设计出来的)。对于我这样的门外汉,只能参考已有的动效示例,对其修修改改,为我所用。