写在前面
什么是 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 还提供了其他一些控制器,如 PointerLockControls
、FirstPersonControls
、DeviceOrientationControls
等,用于支持更特殊的交互需求或特殊设备的输入
动画
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 简单易用,并具有高性能和跨平台支持