Threejs扫盲

写在前面

什么是 WebGL?WebGL 基于 OpenGL ES 2.0 提供 3d 图形接口,是在浏览器环境下进行 3d/2d 图像渲染的技术,而 threejs 则是基于 webGL 的 3d 图形库,基于 threejs,我们可以用 js 做以下这些事:

  • 创建 3d 几何图形
  • 给对象应用材质和纹理
  • 在 3d 场景中移动对象、实现动画
  • 加载 3d 模型

基本概念

坐标系。Three.js 默认使用右手坐标系,因为这是 OpenGL 默认的坐标系

矩阵。用于坐标变换

相机(camera)。有多种相机,比如基于透视投影的镜头(PerspectiveCamera ),会模拟人的视觉效果(近大远小),从某个投射中心将物体投射到单一投影面上,是最常使用的投影模式

场景(scene)。存储并跟踪所有待渲染对象的容器

渲染器(renderer)。在指定的 camera 下绘制 scene。在 web 应用里,场景会被渲染器渲染到一个 canvas 上

视野(FOV)。表示可视范围,常用角度来表示(非弧度)。

视锥体(Viewing frustum)

视锥体也就是中间那个椎体,也就是被渲染的物体所在的区域。视锥剔除(View frustum culling) 是指从渲染过程中移除完全位于视截锥之外的对象的处理步骤

渲染管线

参考 https://www.cnblogs.com/wanbo/p/6754066.html

threejs

一个典型的 Three.js 应用至少包括渲染器、场景、相机、以及在场景中的物体

场景

场景是光源、相机和所有物体的父容器。场景的中心是点(0,0,0),也称为坐标系的原点

const scene = new THREE.Scene();
// 访问所有物体
// scene.children
// 通过指定name访问物体
// scene.getChildByMName(name)
// 遍历物体
// scene.traverse((obj) => {})

相机

有多种投影类型,分为 透视投影正交投影

const fov = 35; // AKA Field of View
const aspect = container.clientWidth / container.clientHeight;
const near = 0.1; // the near clipping plane
const far = 100; // the far clipping plane
// 透视投影
const camera = new PerspectiveCamera(fov, aspect, near, far);
// 定位相机
camera.position.set(0, 0, 5);
scene.add(camera);

渲染器

const renderer = new THREE.WebGLRenderer({
  canvas: document.getElementById("#mainCanvas"),
  // antialias: true, // 开启抗锯齿
});
renderer.render(scene, camera);
// 设置颜色及其透明度
renderer.setClearColor(0xffcc00);
// 设置设备像素比,防止 HiDPI 显示器模糊(视网膜显示器)
renderer.setPixelRatio(window.devicePixelRatio);
// 调整输出canvas的宽高并考虑设备像素比
renderer.setSize(window.innerWidth, window.innerHeight);

场景中的其他物体

基类图形只有点、线、三角形,其余图形都是在此基础上通过顶点着色算法组合而成。添加到场景中的对象如下图:

比如我们创建一个长方体

const cube = new THREE.Mesh(
  new THREE.CubeGeometry(1, 2, 3),
  new THREE.MeshBasicMaterial({
    color: 0xff0000,
  })
);
scene.add(cube);
// 拷贝长方体
// const clonedCube = cube.clone();
// 注意:自定义属性不会克隆

网格对象 Mesh 是 3D 计算机图形学中最常见的可见对象,包含几何体和材质。其他对象有 Point/Line/Group

这里涉及到物体的基本转换,分为平移translation、旋转rotation和缩放scale

// 平移
cube.translateX(100); // 沿着x轴正方向平移100个单位
const axis = new THREE.Vector3(0, 1, 0);
cube.translateOnAxis(axis, 100); // 沿着axis轴表示方向平移100
// 旋转
mesh.rotateX(Math.PI / 2); // 绕x轴旋转π/2
// 缩放
cube.scale.x = 2; // x轴方向放大2倍
cube.scale.set(0.5, 0.5, 0.5); // 缩小为原来的0.5倍

另外还有组 Group 的概念

const group = new Group();
group.add(cube);
group.add(light);
group.remove(light);
scene.add(group);

几何体(Geometry)

  • BoxGeometry 盒状几何体
  • CircleGeometry 圆形几何体
  • ConeGeometry 圆锥形几何体
  • CylinderGeometry 圆筒几何体
  • PlaneGeometry 平面几何体
  • SphereGeometry 球形几何体

更多几何体内容参考 https://github.com/zyj1022/awesome-threejs/blob/master/docs/hello-geometry.md

Geometry 和 BufferGeometry 的区别

如果你只是简单地创建和改变几何体,可以使用 Geometry。如果追求更高的性能和内存利用率,或者处理大规模复杂的场景,可以选择使用 BufferGeometry,它将几何体的数据存储在一组连续的内存缓冲区中,数据存储更加紧凑,可以直接传输给 GPU 进行绘制。但数据是静态的,不容易修改。适用于大规模复杂的场景或静态的几何体

材质(Material)

  • LineBasicMaterial 基础线条材质 — 可以用于 THREE.Line 几何体,从而创建着色的直线
  • LineDashedMaterial 虚线材质 — 类似与基础材质,但可以创建虚线效果
  • MeshBasicMaterial 基础网孔材质 - 为几何体赋予一种简单的颜色,或者显示几何体的线框
  • MeshDepthMaterial 深度网孔材质 - 根据网格到相机的距离,该材质决定如何给网格染色
  • MeshLambertMaterial 兰伯特网孔材质 - 考虑光照的影响,可以创建颜色暗淡,不光亮的物体
  • MeshNormalMaterial 法向量网孔材质 - 根据物体表面的法向量计算颜色
  • MeshPhongMaterial Phong 网孔材质 - 考虑光照的影响,可以创建光亮的物体

更多材质内容参考 https://github.com/zyj1022/awesome-threejs/blob/master/docs/hello-material.md

纹理(Texture)

通常用于给物体表面添加贴图,可以理解成物体的皮肤。可以是图像文件,比如 JPEG、PNG 或 GIF,也可以是视频或其他来源生成的图像。可以通过创建 THREE.Texture 对象来加载和使用纹理,使用示例如下:

// 创建纹理对象
const texture = new THREE.TextureLoader().load("texture.jpg");
// 也可以指定一个image节点
// const image = document.getElementById('myImage');
// const texture = new THREE.Texture(image);
// 应用到材质上
const material = new THREE.MeshBasicMaterial({ map: texture });
const geometry = new THREE.BoxGeometry(1, 1, 1);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

光源

光源照射物体,会产生光影效果。物体的材质在渲染的时候,和光源有很重要的关系。比如物体的纹理、色彩、透明度、光滑度、折射率、反光率等

基础光源:

  • AmbientLight 环境光 - 它的颜色会添加到整个场景和所有对象的当前颜色上
  • DirectionalLight 平行光 — 例如:太阳光
  • PointLight 点光源 — 空间中的一点,朝所有的方向发射光线
  • SpotLight 聚光源 - 聚光灯是由点光源发出,这种类型的光也可以产生投影,有聚光的效果

更多光源内容参考 https://github.com/zyj1022/awesome-threejs/blob/master/docs/hello-light.md

阴影

主要有三种阴影

  • DirectionalLightShadow 平行光阴影,对应 DirectionalLight 光源
  • PointLightShadow 点光源阴影,对应 PointLight 光源
  • SpotLightShadow 聚光灯阴影,对应 SpotLight 光源

阴影的性能负担较大,一般不建议使用,或者用纹理贴图模拟。使用方式可以参考 Three.js 基础之阴影

载入模型

有以下几种分类:

  • gltf。现阶段主流的模型类型,可以包含模型、动画、几何图形、材质、灯光、相机,甚至整个场景。glb是它的二进制形式,体积小很多,所以实际项目中尽量使用这种,不过gltf可读性更好
  • obj。一种简单的文本格式,可以包含顶点位置、纹理坐标、法线等信息。缺点是文件体积相对较大,不支持二进制数据存储,无法存储动画数据,不支持材质和纹理的自定义属性等
  • mtl。是一种与 obj 配套使用的材质文件格式。mtl 文件包含了 obj 模型所需的材质属性信息,如颜色、纹理、法线贴图等。在加载 obj 模型时,可以通过 OBJLoader 来解析 obj 文件,并通过 MTLLoader 来加载对应的 mtl 文件。MTLLoader 会解析 mtl 文件中的信息,并将材质属性应用于对应的物体
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { OBJLoader } from "three/addons/loaders/OBJLoader.js";
import { MTLLoader } from "three/addons/loaders/MTLLoader.js";

const gltfLoader = new GLTFLoader();
const objLoader = new OBJLoader();
const mtlLoader = new MTLLoader();
// 异步加载
const loadedData = await gltfLoader.loadAsync("yourModel.glb");
// load方法是回调的方式
// gltfLoader.load('yourModel.glb', (data) => { // handle data })

创建文字

webGL 中绘制文字会比较耗性能,建议用其他方法模拟,可以参考 https://threejs.org/docs/index.html#manual/zh/introduction/Creating-text

控制器

threejs 的控制器是一种用于交互式地控制场景中相机位置和视角的工具,可以帮助用户通过鼠标、触摸或其他输入设备来旋转、缩放和平移相机,以便浏览和操作场景。常见的控制器有

  • OrbitControls。最常见和流行的控制器,它允许用户通过鼠标左键拖动来旋转相机,鼠标右键拖动来平移相机,鼠标滚轮来缩放相机
  • TrackballControls。除了支持旋转、平移和缩放相机外,还可以通过鼠标拖动控制球(trackball)来自定义旋转方向和速度
  • FlyControls。通过键盘或其他输入设备来控制相机的位置和朝向,以便在场景中自由移动和浏览

除了上述控制器外,threejs 还提供了其他一些控制器,如 PointerLockControlsFirstPersonControlsDeviceOrientationControls 等,用于支持更特殊的交互需求或特殊设备的输入

动画

  • requestAnimationFrame
function animate() {
  mesh.rotation.x += 0.01;
  mesh.rotation.y += 0.02;
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
animate();
  • tweenjs。包含了各种经典动画算法

demo

下面是一个 react Hook 中使用 threejs 的组件 demo

import React, { useEffect, useRef } from "react";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";

function MyThreeDemo() {
  const canvasRef = useRef(null);
  const statsRef = useRef(null);
  function initStat() {
    statsRef.current = new Stats();
    container.appendChild(statsRef.current.dom);
  }

  useEffect(() => {
    // 创建场景、相机和渲染器
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    );
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    // 添加光源
    let ambi = new THREE.AmbientLight(0x686868);
    scene.add(ambi);
    // 初始化帧率组件
    initStat();

    // 添加一个立方体到场景中
    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;
    // 创建 OrbitControls 控制器并附加到相机上
    const controls = new OrbitControls(camera, renderer.domElement);
    // 渲染场景
    const animate = () => {
      requestAnimationFrame(animate);
      cube.rotation.x += 0.01;
      cube.rotation.y += 0.01;
      renderer.render(scene, camera);
      // 更新控制器
      controls.update();
      // 更新帧率
      statsRef.current.update();
    };
    canvasRef.current.appendChild(renderer.domElement);
    animate();

    return () => {
      // 销毁时清除渲染器占用的资源
      renderer.dispose();
    };
  }, []);

  return <div ref={canvasRef} />;
}

export default MyThreeDemo;

除了直接撸 threejs,也可以用react-three-fiber,能减少代码量,但有一定的上手成本

性能优化

做优化前肯定要有指标衡量性能,比如延迟和卡顿,延迟主要是针对实时应用,比如车端的 hmi(参考特斯拉的 hmi),数据或绘制处理不过来,其中因素还是比较多的,可能受硬件设备、网络影响,前端的话主要是尽可能减少数据解析和处理耗时吧。卡顿的话主要是看帧率,可以用 stats.js 这个三方库来协助监控帧率。下面给一些基础的优化建议

  • 绘制优化
    • 使用 BufferGeometry 创建物体
    • dispose 及时释放内存
    • 减少没必要执行的代码在周期渲染函数中的执行,必要时再执行
    • 尽量重用几何体和材质
    • 合理使用网格合并
  • 模型优化,比如模型压缩等
  • 数据优化。比如减小数据体积,减少传输耗时等
  • react 组件优化,比如减少组件不必要的重绘等
  • 借助 web worker 并行计算
  • 借助 wasm 加速计算

其他库

threejs 算是比较通用的 3d 库,提供的 api 都比较好用,可以轻松创建各种类型的 3d 场景和效果,社区也相对比较大,但在游戏处理和性能上并不占优

  • Babylon.js 专注于游戏开发,并提供了各种高级功能,如物理引擎、碰撞检测、粒子系统等。Babylon.js 的 API 简单易用,并具有高性能和跨平台支持

参考