初探神秘的Three.js

初探神秘的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交互式三维动画的两个问题:

  1. 通过HTML脚本本身实现 Web 交互式三维动画的制作,无需任何浏览器插件支持 ;
  2. 利用底层的图形硬件加速功能进行的图形渲染,是通过统一、标准、跨平台的OpenGL接口实现的。

WebGL 是一个底层的标准, 这些标准被定义之后,Chrome、Firefox等主流现代浏览器实现了这些标准。程序员通过 JavaScript 代码, 就能在网页上实现三维图形的渲染了。原生的 WebGL 比较复杂,所以我们经常会使用一些三方的库,如 Three.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 来展示数据。可以不需要其它任何框架独立运行在现代浏览器中,它结合强大的可视化组件来驱动 DOM 操作。因为 JavaScript 文件的后缀名通常是.js,故 D3 也常被称呼为 D3.js 。D3 提供了各种简单易用的函数,大大简化了 JavaScript 操作数据的难度。D3 将生成可视化的复杂步骤精简到了几个简单的函数,只需要输入简单数据,就能够转换为各种绚丽的图形。看完D3的介绍,你会发现,D3.js和Three.js是两个不同的东西,底层依赖的技术并不相同。容易混淆的地方是它们的名字中都带有3,都是绘制图形的。

Three.js基础概念

WebGL 的渲染是需要 HTML5 Canvas 元素的,你可以手动在 HTML 的 部分中定义Canvas 元素,或者让 Three.js 帮你生成。这两种方式均可。一个典型的 Three.js 程序至少要包括渲染器(Renderer)场景(Scene)照相机(Camera),以及你在场景中创建的物体。

渲染器(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);
// 设置背景色
renderer.setClearColor(0x000000);

场景(Scene)

相当于一个大容器,场景没有复杂的操作(相对于照相机和物体而言)。在 Three.js 中添加的物体都是添加到场景中的。在程序运行的开始处对场景进行实例化, 随后将物体添加到场景中即可。

var scene = new THREE.Scene();   

照相机(Camera)

“照相机”在这里是类比, 并非我们日常所说的照相机。Three.js 创建的场景是三维的,而通常情况下显示屏是二维的,那么三维的场景如何显示到二维的显示屏上呢? 照相机就是这样一个抽象,它定义了三维空间到二维屏幕的投影方式。Three.js 中一共有四种相机,分别为 CubeCamera(立方相机,可以创造反光效果)、OrthographicCamera(正交)、PerspectiveCamera(透视)、StereoCamera(立体相机,用于创造3D立体影像),它们都继承自 Camera 类。我们常用的有两种,正投影相机 THREE.OrthographicCamera 和透视投影相机 THREE.PerspectiveCamera。

  • 透视投影 比较接近人眼看物体的直觉,近大远小。

初探神秘的Three.js

  • 正交投影
    物体反射的光平行投射到上,其大小始终不变,远近的物体大小一样。视点作为观察点,视椎体由前后左右上下 6 个面包裹而成,物体在视椎体内部,最后投影到 near 近平面,视椎体范围之外将无法显示到屏幕上来。

初探神秘的Three.js

两种投影的区别如下图所示:透视投影看物体近大远小;正交投影在三维空间内平行的线, 投影到二维空间中也是平行的。

初探神秘的Three.js

一般说来, 制图、建模软件通常使用正交投影,这样不会因为投影而改变物体比例;而对于其它大多数应用, 通常使用透视投影, 因为这更接近人眼的观察效果。

// 创建透视投影镜头
// 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轴

初探神秘的Three.js

我们现在定义一个透视投影的照相机, 照相机也需要添加到场景中。

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(网格)

表示基于以三角形为polygon mesh(多边形网格)的物体的类。 同时也作为其它类的基类,例如SkinnedMesh。

// geometry 几何体实例
// material一个材质(`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)相结合,可以创建出形状丰富、颜色各异的三维物体。也可以自定义每个点的位置或者通过导入外部的模型文件来构造自己的几何体。

初探神秘的Three.js

// 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);   

初探神秘的Three.js

Material(材质)

在three.js中,材质决定了几何图形中的表面是如何画的。如果几何图形是骨架,定义了形状,那么材质就是皮肤。材质不仅仅指物体纹理,而是物体表面除了形状以外所有可视属性的集合,例如色彩、纹理、光滑度、透明度、反射率、折射率、发光度。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 聚光灯光源
  • 作用于全局是指什么?

只有环境光(AmbientLight、HemisphereLight)作用于全局,其它光则照耀范围都是有限的。

  • 是否有照射目标是指什么?

就是这个光除了光源本身之外,还包含一个 target 属性,并且可以通过设置 target.position 的位置。对于有照射目标的灯光,在场景中不光要添加灯光本身,还可以添加灯光照射目标。

渲染

在定义了场景中的物体,设置好照相机之后,渲染器就知道渲染什么内容了。只需调用渲染器的渲染函数,就能把渲染出来。

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中的动画感觉一样,即便语法你都会,你也做不出很炫酷的效果。真不知这些炫酷动效,是如何被设计出来以及实现的。对于我这样的门外汉,只能参考已有的动效示例,对其修修改改,为我所用。

参考文献

  1. Three.js 入门指南pdf文档
  2. Three.js 知识梳理三:Three.js几何体Geometry

原文链接:https://juejin.cn/post/7237051958993076280 作者:去伪存真

(0)
上一篇 2023年5月26日 上午10:10
下一篇 2023年5月26日 上午10:20

相关推荐

发表评论

登录后才能评论