Threejs
基础概念
- 3D 场景
- 相机
- 渲染器
- 几何体
- 材质
- 光源
学习路径:
基础知识掌握
- 掌握 JavaScript、HTML 和 CSS 等基础知识
- WebGL:3D 绘图标准,使用该技术可以在浏览器中渲染 3D 图形。学习时间:几个月+
学习方式
- 阅读 Threejs 官方文档,(这应该算是最佳途径)。熟悉 API 和示例代码
学习目标
- 熟悉 3D 图形的基本概念,如顶点、纹理、材料、光照等
- 掌握 Threejs 中最常用的 API,包括场景、相机、渲染器、几何体、材质和光源等
- 对 Threejs 的优化。性能提升:比如使用缓存、避免过多重绘、使用纹理合并等
- 调试:浏览器内置的调试工具或第三方工具
学习实践
- 创建自己的 3D 场景,并修改 Threejs 示例代码以满足自己的需求。
概念澄清
相机类型
透视相机
正交相机
材质
材质类型
- IBL: IBL(image base lighting)材质计算镜面反射会考虑环境光,会比单纯的环境贴图更真实。但同时它需要更高的计算能力和显存,可能会导致性能下降。环境贴图生成 + 光照计算。
- MeshPhongMaterial: MeshPhongMaterial 材质在计算镜面反射时,只考虑局部光照。镜面反射,比较刺眼
- MeshLambertMaterial: 对应的 Mesh 受到光线照射,没有镜面反射的效果,只是一个漫反射,也就是光线向四周反射。
材质参数
- specularStrength: 镜面反射强度,默认为 1。
世界坐标 & 局部(本地)坐标
本地坐标
- 本地坐标是相对于父级物体的坐标。就只是自己的坐标。
可以为网格创建一个局部坐标系,可视化
const axis = new THREE.AxesHelper(50);
mesh.add(axis);
世界坐标
- 自身的坐标 + 所有父对象的坐标。
const vector = new THREE.Vector3(); // 创建一个三维向量来表示坐标
mesh.getWorldPosition(vector); //getWorldPosition会将结果存储在vector中
Startup
要想用 threejs 中展示任何东西,那有 three 样(哈哈哈哈哈,这是 threejs 命名的缘由吗?)东西是必不可少的。
- scene
- camera
- renderer
render the scene with camera
installation
npx crate-react-app threejs-demo --template typescript
yarn add three
yarn add @types/three
yarn add sass
Overview
- Scene
- Camera
- render
- create Scene
const scene = new THREE.Scene();
- create Camera(multiple type: perspective)
const camera = new THREE.PerspectiveCamera(
75, // fov ?
window.innerWidth / window.innerHeight,
0.1,
1000
);
- change the camera position
camera.position.set(0, 0, 0);
- add Camera into scene
scene.add(camera);
add objective into scene
- create geometry: cube
BoxGeometry
- set the material(color: xf)
MeshBasicMaterial
- create mesh base on geometry and material
Mesh(BoxGeometry, MeshBasicMaterial)
- add the mesh into scene
scene add mesh
- create geometry: cube
const geometry = new THREE.BoxGeometry(1, 1, 1);
const materaial = new THREE.MeshBasicMaterial({ color: 0xeeeeee });
const cube = new THREE.Mesh(geometry, materaial);
scene.add(cube);
- init render & config render size
// 抗锯齿
const renderer = new THREE.WebGLRenderer({ antialias: true }); // 锯齿严重的话,可以传入antialias: true参数来解决
// || renderer.antialias = true;
// || renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置背景色
renderer.setClearColor(0xeeeeee);
renderer.setClearColor(0xffffff);
- render renderer.domElement into document
document.body.appendChild(renderer.domElement);
- render scene by camera
renderer.render(scene, camera);
use the controller to view the 3d model
- use the orbitControl(import & entity)
- render when requestAnimationFrame triggered
const controls = new OrbitControls(camera, renderer.domElement);
const render = () => {
renderer.render(scene, camera);
window.requestAnimationFrame(render);
};
render();
summary
<script type="importmap">
{
"imports": {
"three": "../../three.js/build/three.module.js",
"three/addon/": "../../three.js/examples/jsm/"
}
}
</script>
<script src="./index.js" type="module"></script>
import * as THREE from 'three';
import { OrbitControls } from 'three/addon/controls/OrbitControls.js';
// 1. scene create
const scene = new THREE.Scene();
// 2. geometry create
const geometry = new THREE.BoxGeometry(1, 1, 1);
// 3. material create
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
// 4. mesh create
const mesh = new THREE.Mesh(geometry, material);
// 5. set mesh positon
mesh.position.set(0, 0, 0);
// 6. add mesh into the scene
scene.add(mesh);
// 7. create perspective camera
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 3000);
// 8. set the camera position
camera.position.set(100, 100, 100);
// 9. set the camera lookAt
camera.lookAt(mesh.position);
// 10. create the renderer
const renderer = new THREE.WebGLRenderer();
// 11. set the renderer size
renderer.setSize(window.innerWidth, window.innerHeight);
// 12. render the scene and camera
renderer.render(scene, camera);
// 13. show the renderer content
document.body.append(renderer.domElement);
// 14. control the camera
const controller = new OrbitControls();
// 14. 1: use event listener
controller.addEventListener('change', function () {
renderer.render(camera, renderer.domElement);
});
// 14.2: use window.requestAnimation
const render = window.requestAnimationFrame(() => {
renderer.render(scene, camera);
render();
});
render();
// 添加光源
// 1. 创建光源
const spotLight = new THREE.Spotlight(0xffffff, 1.0);
// 设置光源是否衰减
spotLight.decay = 0;
// 设置光源位置
spotLight.position.set(700, 500, 800);
// 将光源添加至场景中
scene.add(spotLight);
// Tips: 不能使用MeshBasicMaterial了,因为它不受光照影响。
画布
画布缩放,模型大小的 scale demo
光源与材质
threejs 提供的网格材质,有的受光照影响,有的不受光照影响。 当添加光源,发现没有效果时,可以排查材质是否正确。
材质
- 不受光照影响
- 基础材质: MeshBasicMaterial
- 受光照影响
- 漫反射: MeshLambertMaterial
- 高光: MeshPhongMaterial
- 光照效果相对来说会更逼真。
- 依照实际情况来使用对应的材质
- 物理:
- MeshStandardMaterial
- metalness(通常没有中间值,0 为非金属材质,eg: 木材,石材,1 为金属材质。0 到 1 之间的值可用于生锈金属的外观。如果有 metalnessMap,则两个值相乘)
- roughness(0 表示平滑的镜面反射,1 表示完全漫反射)
- MeshPhysicalMaterial
- MeshStandardMaterial
真实度和所耗费的性能由上到下,依次递增。
金属材质
metalness
粗糙度
roughness
环境贴图
const textureCube = new THREE.CubeTextureLoader().setPath().load([
/*数组长度为6,表明周围6个面 x轴的正负, y轴的正负, z轴的正负*/
]);
material.envMap = textureCubel;
material.envMapIntensity = 1.0; // 0表示没有影响
或者也可以直接把 texture 设置给 scene 的 environment。它不覆盖已有的 material 贴图属性。
scene.environment = textureCube;
光源
- 环境光:AmbientLight - 没有方向
- 点光源 PointLight - 向四周发射
- 聚光灯光源 SpotLight
- 平行光 DirectionLight - 沿着一个方向
- 半球光 HemisphereLight
PointLightHelper
const lightHelper = new THREE.PointLightHelper(spotLight, 10);
scene.add(lightHelper);
阴影的投射
需要的步骤:
- 材质要对光照有反应,比如 basicMaterial 就不行
- renderer.shadowMap.enabled = true
- object.castShadow = true
- light.castShadow = true
- plane.receiveShadow = true
几何体
缓冲类型的几何体
BufferGeometry
: 没有任何形状的空几何体,可以通过BufferGeometry
自定义任何几何形状。具体一点说,就是定义顶点数据。threejs 中的类似BoxGeometry
,SphereGeometry
等几何都是基于BufferGeometry
类构建的。
点模型
- 定义几何体顶点数据
const geometry = new THREE.BufferGeometry();
// 类型化数组Float32Array创作一组xyz坐标数据用来表示几何体的顶点坐标
const vertices = new Float32Array([
0,
0,
0, // 顶点1坐标
50,
0,
0, // 顶点2坐标
0,
100,
0, // 顶点3坐标
0,
0,
10, // 顶点4坐标
0,
0,
100, // 顶点5坐标
50,
0,
10 // 顶点6坐标
]);
const attribute = new THREE.BufferAttribute(vertices, 3); // 3个为一组,表示一个顶点的xyz坐标)
geometry.attributes.position = attribute;
// 网格模型是渲染成一个面,点模型是把点展示出来,它有它自己的专属材质
const material = new THREE.PointsMaterial({
color: 0xffff00,
size: 10
});
const pointsModel = new THREE.Points(geometry, material);
export default pointsModel;
线模型
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([]); // 定点数据,用上面的就行
const bufferAttribute = new THREE.BufferAttribute(vertices, 3);
geometry.attributes.position = bufferAttribute;
const material = new THREE.LineBasicMaterial({
color: 0xffff00
});
const lineModel = new THREE.Line(geometry, material);
export default lineModel;
网格模型
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([]); // 定点数据,用上面的就行
const bufferAttribute = new THREE.BufferAttribute(vertices, 3);
geometry.attributes.position = bufferAttribute;
const material = new THREE.MeshBasicMaterial({
color: 0xffff00,
side: THREE.DoubleSide
});
const meshModel = new THREE.Mesh(geometry, material);
export default meshModel;
// 三角面分为正面和反面。
// 正面: 逆时针
// 反面:顺时针
// 可以在材质中设置side属性来控制哪一面可见
// side:
// 1. THREE.DoubleSide
// 2. THREE.BackSide
// 3. THREE.FrontSide
因为网格模型的绘制中,顶点是可以共用的,所以我们可以创建顶点 BufferArray,并且将对应的属性设置给 geometry,就可以共用顶点来实现网格模型的绘制。
const indexes = new THREE.BufferAttribute(new Uint16Array([0, 1, 2, 2, 3, 0]), 1);
const geometry = new THREE.BufferGeometry();
geometry.index = indexes;
常用的几何体操作
scale();
translate();
rotateX(); // 正负数表示顺逆时针
rotateY();
rotateZ();
center();
translateX();
translateY();
translateZ();
// translateOnAxis(axis: Vector, distance: float)
normalize();
group.add();
模型的一些属性
位置:position 缩放: scale 角度:rotation & quaternion
rotation - Euler
const euler = new THREE.Euler(0, Math.PI, 0);
mesh.rotation.set(euler);
mesh.rotation.y = Math.PI / 4;
quaternion - Quaternion
材质
基础代码
一些基础代码
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { useRef, useEffect } from 'react';
export const UVComponent = () => {
const container = useRef(null);
const scene = new THREE.Scene();
const geometry = new THREE.SphereGeometry(50);
const texture = new THREE.TextureLoader().load('https://gd-hbimg.huaban.com/55a9bf701ec3c9bf7a780e01bf79ef5721cc29f9c9bff-kcQlZy_fw1200webp');
const material = new THREE.MeshPhongMaterial({ map: texture });
const mesh = new THREE.Mesh(geometry, material);
const axesHelper = new THREE.AxesHelper(200);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
const width = 800;
const height = 500;
const camera = new THREE.PerspectiveCamera(30, width / height, 0.1, 3000);
camera.position.set(200, 200, 200);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
scene.add(mesh, axesHelper, ambientLight);
const controls = new OrbitControls(camera, renderer.domElement);
useEffect(() => {
if (container.current) {
const hasCanvas = container.current?.getElementsByTagName('canvas').length;
if (!hasCanvas) {
container.current.appendChild(renderer.domElement);
}
}
}, [container.current]);
useEffect(() => {
const render = () => {
renderer.render(scene, camera);
window.requestAnimationFrame(render);
};
render();
}, []);
return <div id="uv-scene" ref={container}></div>;
};
常用 API
不做特殊说明都是 threejs 独有的,js 中的通用 api 前面会加一个 🏐
交互
save as Image
- preserveDrawingBuffer: true
- canvas.toDataURL("image/png");
add axesHelper
x: red, y: green, z: blue
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
gridHelper
网格
position | scale | rotation
- cube.[type].set(x, y, z)
- cube.[type].x = XXX
clock
- 渲染帧相关的时间追踪 eg: 动画总时长,间隔等~ 可以参考具体 api
Texture
TextureLoader loader.load(url) texture: rotation, offset, center set。 & wrap method. texture[minFilter | magFilter] alpha(蒙版,黑隐白现), transparent, side
CubeTextureLoader
const cubeTextureLoader = new THREE.CubeTextureLoader();
const envMapTexture = cubeTextureLoader.load([
'image-url-px', // positive-x
'image-url-nx', // negative-x
'image-url-py', // positive-y
'image-url-ny', // negative-y
'image-url-pz', // positive-z
'image-url-nz' // negative-z
]);
const sphereGeometry = new THREE.SphereGeometry(1, 20, 20);
const material = new THREE.MeshStandardMaterial({
metalness: 0.7,
roughness: 0.1,
envMap: envMapTexture
});
const sphere = new THREE.Mesh(sphereGeometry, material);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
scene.add(ambientLight);
scene.add(directionalLight);
scene.add(sphere);
RGBLoader
加载 HDR 图
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader';
const rgbeLoader = new RGBELoader();
rgbeLoader.loadAsync('XXX.hdr').then((texture) => {
// 一定要hdr格式的图。图形学介绍页有分享hdr格式图的下载地址
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.background = texture;
scene.environment = texture;
});
🏐document.fullscreenElement
可以切换全屏以及退出全屏。
renderer.domElement.requestFullscreen();
document.exitFullscreen();
此处根据 document.fullscreenElement 的有无来判断全屏状态,进行对应的操作
🏐window.resize
window.onresize = () => {
// ……
};
// 1. 更新camera aspect
camera.aspect = window.innerWidth / window.innerHeight;
// 2. 更新camera projectionMatrix
camera.updateProjectionMatrix();
// 3. 重新设置大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 4. 更新画布ratio
renderer.setPixelRatio(window.devicePixelRatio);
三方库
gsap
注意: 该库商用需要授权。
- 动画的暂停,重播,间隔等
- 动画的效果
gui
yarn add dat.gui
import * as dat from 'dat.gui';
// 或者
import { GUI } from 'three/addon/libs/lil-gui.module.min.js';
const gui = new dat.GUI() || new GUI();
const ambientLight = new THREE.AmbientLight(0xffffff, 1.0);
gui.add(ambientLight, 'intensity', 0, 2)
.name('环境光强度')
.step(1)
.onChange((value) => {
console.log(value);
});
const obj = {
color: 0xffffff,
arr: 0,
bool: true,
objValue: 10
};
gui.add(obj, 'arr', [-100, 0, 100]).onChange((value) => {
mesh.position.x = value;
});
gui.add(obj, 'objValue', {
left: 0,
right: 100,
center: 50
}).onChange((value) => {
mesh.position.y = value;
});
gui.add(obj, 'bool');
gui.addColor(obj, 'color').onChange((value) => {
console.log(value);
mesh.material.color.set(value);
});
gui.domElement.style.top = 500;