Threejs
Three.js 是基于 canvas
的一个开源应用级依赖,它屏蔽了 WebGL
底层的调用细节,可以使我们快速在网页创建 3D 效果。此文章是在学习 @react-three/fiber 是 three.js 之前,我需要了解 three.js 的一些基本概念,这些概念可以帮助我更好的理解 threejs 的设计理念和编程艺术。
概念
核心元素名词
开发一个 Three.js
应用,一般会涉及以下元素:
scene
场景:类似于一个容器(container),可以将物体,包括物体的模型、例子、光源等加入其中object
物体:物体即我们看到的实际元素,可以是原始的集合体,也可以是导入的模型、粒子、光源等camera
相机:相机类似于我们的视角,相机也可以做scene
的一部分,但是在页面是不可见的renderer
渲染器:从camera
的角度渲染,结果绘制到 canvas 中
通用的模板
import * as THREE from 'three'
// 1. 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true }) // 抗锯齿
renderer.setSize(window.innerWidth, window.innerHeight) // 设置大小
rebderer.pixelRatio = window.devicePixelRatio // 设置像素密度
document.body.appendChild(renerer.domElement) // 将渲染器 dom 加入 body 节点下
// 2. 创建场景
const scene = new THREE.Scene()
// 3. 创建物体
const geomerty = new THREE.BoxGeometry(4, 4, 4) // 物体的几何形状
const material = new THREE.MeshStandardMaterial({ color: 0xff0000 }) // 物体的材质
const object = new THREE.Mesh(geometry, material) // 物体
scene.add(object) // 将物体加入场景
// 4. 创建相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000,
)
camera.position.set(15, 15, 15) // 设置相机的位置
camera.lookAt(0, 0, 0) // 相机看向远点
// 5. 调用渲染器 rendr 方法渲染
renderer.render(scene, camera)
Threejs 的结构:
透视相机 Perspective Camera
透视相机模拟的是人的视角,人观察物体时,物体近大远小:
const camera = new PerspectiveCamera(fovy, aspect, zNear, zFar)
参数含义可以看图:
坐标系统
Three.js 使用的是右手坐标系(伸出右手,大拇指指向 Z 轴,另外四指指向 X 轴,弯曲 90 度就是 Y 轴),Z 轴方向就是屏幕朝向我们的方向。
所有物体都会摆在坐标原点位置,也就是 (0, 0, 0) 这个位置
这也是为什么相机需要设置位置
camera.position.set(x, y, z)
,并“看向原点”:camera.lookAt(0, 0, 0)
光源
Three.js 中三种基本光源:
- 环境光(Ambient Light):均匀光照,会均匀照亮场景中所有物体,不考虑光照源的位置和方向
- 方向光(Directional Light):方向光是一种平行光源,具有确定的方向和强度,类似太阳光
- 点光源(Point Light):点光源是一种位于特定位置的光源,向所有方向发射光线,类似于灯泡
材质 material
材质定义了对象在场景中的外形。
越精细的材质,构建速度往往更加慢,但是场景会更加逼真。因为在移动设备这种低功率设备上,要选择合适的材质。
const material = new THREE.MeshPhongMaterial({
color: 0xff0000,
flatShading: true, // 定义材质是否使用平面着色进行渲染
})
几种材质:
MeshBasicMaterial
:不受光照影响MeshLamertMaterial
:只在顶点计算光照MeshPhongMaterial
:每个像素计算光照,还支持镜面高光
另外 MeshPhongMaterial
的 shininess
属性决定了镜面高光的光泽度,默认 30。
另外一种材质 MeshToonMaterial
,它与 MeshPhongMaterial
,但是它不是平滑地着色,而是使用一种渐变图(一个 X 乘 1 的纹理)来决定如何着色。
上面的材质是使用简单的数学来制作,看起来是 3D 的,但并不是现实世界存在的,下面 2 中是基于物理引擎(Physically Based Rendering,简称 PBR)的材质:
MeshStandardMaterial
:有两个参数设置材质,分别是roughness
和metalness
属性,代表粗糙度和金属度MeshPhysicalMaterial
:与MeshStandardMaterial
相同,但是增加了一个clearcoat
参数,表示清漆光亮层的成都,和另一个clearCoatRoughness
参数,指定光泽层的粗糙程度
例子,搜索关键字MeshPhysicalMaterial,这个例子自己写的不太好,就不展示了。
另外几种特殊用处的材质:
ShadowMaterial
:获取阴影创建的数据MeshdepthMeterial
:渲染每个像素的深度MeshNormalMaterial
:显示几何体的法线ShaderMaterial
:通过 three.js 制作的自定义材质RawShaderMaterial
:用来制作完全自定义的着色器,不需要 three.js 的帮助(这个涉及的东西太多了,这里不细讲)
最后,大多数材质都是共享一堆由 Material
定义的设置,所有设置可参考文档
纹理 texture
纹理可以理解为图片“贴”在我们的物体上,只需要在 material
上设置 map
属性即可:
const loader = new THREE.TextureLoader()
loader.load('/threejs/images/wall.jpg', (texture) => {
texture.colorSpace = THREE.SRGBColorSpace
const material = new THREE.MeshBasicMaterial({
color: 0xff0000,
map: texture,
})
})
例如:
另外,我们可以指定多种纹理,例如每个立方体面都有不同纹理:
只需要定义 materials
数组即可
import * as THREE from 'three'
const loader = new THREE.TextureLoader()
function loadTexture(path) {
return new Promise((resolve) => {
loader.load(path, rsolve)
})
}
async function loaMaterials(paths: string[]) {
const textures = await Promise.all(
paths.map(async (path) => {
const texture = await loadTexture(path)
texture.colorSpace = THREE.SRGBColorSpace
return texture
}),
)
}
const materials = await loadMaterials([
// ...paths
])
const geometry = new THREE.BoxGeometry(4, 4, 4)
const mesh = new Mesh(geometry, materials)
BoxGeometry
每个面都可以有一种材质,但是像GoneGeometry
能用两种材料(底部和侧面),CylinderGeometry
可以使用三种(底部、顶部和侧面)。
内存管理
纹理在 threejs 中占的内存往往非常多,一般计算公式,纹理会占 宽度 * 高度 * 4 * 1.33
字节的内存。
有个有意思的现象,虽然有些图片大小可能很小,但是它的分辨率会很高,例如 threejs
官网中的这个例子:
为了让 three.js 使用成本,必须把纹理交给 GPU 处理,但 GPU 一般要求纹理数据不被压缩,因此要合理处理纹理的文件:
文件大小小,网络下载快;分辨率小,占用的内存少。
光照 Light
lil-gui
学习光照之前,我们可以尝试一下 lil-gui
,它是一个悬浮的可以控制 threejs 参数的面板,方便我们观察不同参数的 3D 效果,例如:
核心代码:
export export function buildGUI(ref: RefObject<HTMLDivElement>, width = 250) {
const gui = new GUI({
autoPlace: false,
container: ref.current!,
width,
})
return gui
}
const gui = buildGUI(ref)
function render() {
renderer.render(scene, camera)
}
const controls = new OrbitControls(camera, renderer.domElement)
controls.addEventListener('change', render)
controls.update()
gui.add(mesh.scale, 'x', 0, 4, 0.1).name('x').onChange(render)
gui.add(mesh.scale, 'y', 0, 4, 0.1).name('y').onChange(render)
gui.add(mesh.scale, 'z', 0, 4, 0.1).name('z').onChange(render)
环境光 AmbientLight
环境光没有反向,也无法产生阴影,场景内任何一点受到的光照强度都是相通的。环境光看起来并不是真正意义上的光照,通常用于提高亮度。
import { useEffect, useRef } from 'react'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/Addons.js'
import { buildCamera, buildGUI, buildRenderer } from './utils'
export default function ThreePureAmbientLight() {
const ref = useRef(null)
useEffect(() => {
const renderer = buildRenderer(ref)
const camera = buildCamera(10, 10, 10)
const scene = new THREE.Scene()
const material = new THREE.MeshPhongMaterial({ color: 0x5080ef })
const geometry = new THREE.BoxGeometry(4, 4, 4)
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)
const light = new THREE.AmbientLight(0xffffff, 1)
scene.add(light)
const gui = buildGUI(ref)
gui.add(light, 'intensity', 0, 5, 0.01).onChange(render)
gui.addColor(light, 'color').onChange(render)
function render() {
renderer.render(scene, camera)
}
const controls = new OrbitControls(camera, renderer.domElement)
controls.update()
controls.addEventListener('change', render)
render()
return () => {
controls.removeEventListener('change', render)
}
}, [])
return <div className="fcc" ref={ref} />
}
半球光 HemisphereLight
Before
虽然目前大多数设备以及浏览器都支持 WebGL2,但是依旧少部分不支持,我们需要给用户提示信息。
import WebGL from 'three/addons/capabilities/WebGL.js'
if (WebGL.isWebGL2Available()) {
// Initiate function or other initializations here
animate()
} else {
const warning = WebGL.getWebGL2ErrorMessage()
document.getElementById('container').appendChild(warning)
}
Examples
move to three-js