哈哈--- highlight: a11y-dark theme: lilsnake
入门
入门Three.js的第一步,就是认识场景Scene 、相机Camera 、渲染器Renderer三个基本概念

三维场景Scene
可以把三维场景Scene (opens new window)对象理解为虚拟的3D场景,用来表示模拟生活中的真实三维场景,或者说三维世界。
物体形状,创建一个物体放到场景里

物体有了,还需要给物体添加材质,外观是怎样的。材质Material

我们需要用网格模型将物体和材质关联起来,然后告诉场景,这个网格模型在什么位置,用来表示生活中的一个物品。
三维场景是XYZ,那么我们这个物品的位置是哪里呢,使用网格模型的set方法告诉位置。最后添加到场景中。
js
import { Scene, BoxGeometry, MeshBasicMaterial, Mesh } from 'three'
// 创建三维场景
const scene = new Scene()
// 给场景添加物品--长方形,宽度100,高度100,深度100
const geometry = new BoxGeometry(100, 100, 100)
// 创建一个红色材质
const material = new MeshBasicMaterial({ color: '#ff0000' })
// 网格模型,将材质和几何体连接起来
const mesh = new Mesh(geometry, material)
// 设置网格模型位置
mesh.position.set(0, 10, 0)
// 添加到场景中
scene.add(mesh)
走到这里看不到场景,这就是需要后续的相机Camera和渲染器Renderer
材质

MeshBasicMaterial & MeshLambertMaterial
构造器
js
// 创建一个不受光照材质
new MeshBasicMaterial(parameters)
// 创建一个受光照材质
new MeshLambertMaterial(parameters)
parameters 参数集合
- color:颜色,支持英文和十六进制,默认白色
- transparent:是否开启透明,默认不开启
- opacity:透明度,1 是完全不透明,0.0 是完全透明,默认不透明
- side: DoubleSide 双面可见
高光网格材质 MeshPhongMaterial
具有高光效果。模拟反射,形成刺眼的感觉。
- color:颜色
- shininess 高光强度
- specular 高光颜色
js
const material = new MeshPhongMaterial({
color: 'blue', shininess: 30, specular: 0x444444
})
其他材质
- PointsMaterial 点材质
- LineBasicMaterial 线材质
光源


点光源
模拟生活中的灯泡,向四周发射光源,还需要设置光源位置,向那个位置发射光源。
- color: 颜色,默认白色
- intensity:强度,默认 1
- distance:范围,默认是0,表示无限远。
- decay:随距离衰减,默认2
js
import { Scene, PointLight } from 'three'
// 创建一个点光源
const pointLight = new PointLight('fff', 1, 100, 2)
// 设置点光源位置
pointLight.position.set(400, 0, 0)
scene.add(pointLight)
可视化点光源
可以看到点光源具体的位置,在页面中体现出来
- light: 需要可视化的点光源
- sphereSize?: 球体大小(默认是 1)
- color?: 球体的颜色,默认使用光源的颜色
js
import { Scene, PointLight, PointLightHelper } from 'three'
// 可视化点光源
const pointLightHelper = new PointLightHelper(pointLight, 10)
scene.add(pointLightHelper)
环境光
环境光 AmbientLight 没有特定方向,只是整体改变场景的光照明暗。
js
import { Scene, AmbientLight } from 'three'
// 环境光
const ambientLight = new AmbientLight('ffffff', 0.4)
scene.add(ambientLight)
平行光 / 添加可视化
平行光 DirectionalLight 就是沿着特定方向发射。
平行光照射到网格模型 Mesh 表面,光线和模型表面构成一个入射角度,入射角度不同,对光照的反射能力不同。
光线照射到漫反射网格材质MeshLambertMaterial 对应Mesh表面,Mesh表面对光线反射程度与入射角大小有关。
如果照射在 Mesh 的 角上,那就反射出来的立体感就很弱,光线太强。

js
import { Scene, DirectionalLight, DirectionalLightHelper } from 'three'
// 平行光
const directionalLight = new DirectionalLight(0xffffff, 1)
directionalLight.position.set(50, 100, 60)
directionalLight.target = mesh // 默认是坐标原点
scene.add(directionalLight)
// 可视化平行光
const directionalLightHelper = new DirectionalLightHelper(directionalLight, 5, 'red')
scene.add(directionalLightHelper)
相机
Threejs如果想把三维场景Scene渲染到web网页上,还需要定义一个虚拟相机Camera,就像你生活中想获得一张照片,需要一台用来拍照的相机。
Threejs提供了正投影相机 OrthographicCamera 和透视投影相机 PerspectiveCamera
透视投影相机 PerspectiveCamera
透视投影相机PerspectiveCamera本质上就是在模拟人眼观察这个世界的规律。
注意相机位置不同,拍照的结果也是不同的,想要好的效果,需要设置对相机位置。
位置有了, 那么相机看向谁呢,我们要拍摄谁呢。

使用参数:
js
/**
* 创建新的 PerspectiveCamera
* fov -- 相机视野角度。默认 50. 越大看的视野越光
* aspect -- Canvas 画布的大小,默认 1
* near 最近能看到的距离 默认 0.1.
* far 最远能看到的距离 默认 2000
*/
constructor(fov?: number, aspect?: number, near?: number, far?: number);
js
// 创建一个透视投影相机,假设 Canvas 画布是800*500,视野角度30
const camera = new PerspectiveCamera(30, 800 / 500)
投影规律
轨道控制器 OrbitControls
OrbitControls实现旋转缩放预览效果,如果没有执行重写渲染,不会有操控,但是实际上相机的参数已经改变。
js
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
// 创建控制器
const orbitControls = new OrbitControls(camera, renderer.domElement)
// 监听控制器
orbitControls.addEventListener('change', () => {
// 重新渲染
renderer.render(scene, camera)
})
渲染器
生活中如果有了景物和相机,那么如果想获得一张照片,就需要你拿着相机,按一下,咔,完成拍照。对于threejs而言,如果完成 咔 这个拍照动作,就需要一个新的对象,也就是WebGL渲染器WebGLRenderer
我们要创建一个渲染器,并且设置Canvas画布大小,并调用渲染方法,用那个相机对那个场景进行拍照。
生成以后我们渲染到哪里呢?
插入画布到div
html
<script setup lang="ts">
import { Scene, BoxGeometry, MeshBasicMaterial, Mesh, PerspectiveCamera, WebGLRenderer } from 'three'
import { onMounted, ref } from 'vue'
const threeRef = ref<HTMLDivElement>()
// 创建三维场景
const scene = new Scene()
// 给场景添加物品--长方形,宽度100,高度100,深度100
const geometry = new BoxGeometry(100, 100, 100)
// 创建一个红色材质
const material = new MeshBasicMaterial({ color: '#ff0000' })
// 网格模型,将材质和几何体连接起来
const mesh = new Mesh(geometry, material)
// 设置网格模型位置
mesh.position.set(0, 10, 0)
// 添加到场景中
scene.add(mesh)
// 创建一个透视投影相机,假设 Canvas 画布是800*500
const camera = new PerspectiveCamera(30, 800 / 500)
// 设置相机位置
camera.position.set(200, 200, 200)
// 设置相机拍照方向
camera.lookAt(mesh.position)
// 创建渲染器
const renderer = new WebGLRenderer()
// 设置画布尺寸,宽度800,高度500
renderer.setSize(800, 500)
// 执行渲染,用那个相机对那个场景进行拍照
renderer.render(scene, camera)
// 添加到body中
// document.body.appendChild(renderer.domElement)
// 添加到 div 中
onMounted(() => {
if (threeRef.value) {
threeRef.value.appendChild(renderer.domElement)
}
})
</script>
<template>
<div ref="threeRef" style="width: 800px; height: 500px;"></div>
</template>
动画渲染循环
首先我们要知道一个 JS 函数,requestAnimationFrame 是一个定时器函数,专门用于做动画更新。调用频率大约是 每秒 60 次(60fps),取决于显示器刷新率。
原理:就是不断的拍照
js
function render2() {
// 周期性旋转,每次 0.1弧度
mesh.rotateY(0.01)
// 周期性渲染
renderer.render(scene, camera)
requestAnimationFrame(render2)
}
// 添加到body中
// document.body.appendChild(renderer.domElement)
// 添加到 div 中
onMounted(() => {
if (threeRef.value) {
threeRef.value.appendChild(renderer.domElement)
render2()
}
})
计算两帧渲染时间间隔和帧率
js
const clock = new Clock() // 时钟对象
// 获取执行多少次
function render2() {
const delta = clock.getDelta() * 1000 // 毫秒
console.log( delta)
// 渲染帧率
console.log(1000 / delta)
mesh.rotateY(0.01)
renderer.render(scene, camera)
requestAnimationFrame(render2)
}
渲染循环和相机控件OrbitControls
设置了渲染循环, 相机控件 OrbitControls 就不用再通过事件 change 执行renderer.render(scene, camera)
毕竟渲染循环一直在执行 renderer.render(scene, camera)
三维坐标轴
辅助观察坐标系
js
import { Scene, AxesHelper } from 'three'
// AxesHelper:辅助观察的坐标系,参数是线长
const axesHelper = new AxesHelper(150);
scene.add(axesHelper);
全屏展示 / 窗口变化
获取窗口的大小,就可以了,用样式调整一下。
css
overflow: hidden;
js
const width = window.innerWidth
const height = window.innerHeight
窗口改动之后还需要,重新设置画布大小,和相机位置
js
function handleResize() {
// 根据窗口重写设置宽高
renderer.setSize(window.innerWidth, window.innerHeight)
// 重新设置摄像机宽高比
camera.aspect = window.innerWidth / window.innerHeight
// 必须在设置参数后使用。更新相机矩阵
camera.updateProjectionMatrix()
}
window.addEventListener('resize', handleResize)
// 组件卸载时移除监听器
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
renderer.dispose()
})
性能监视器
three.js每执行WebGL渲染器.render()方法一次,就在canvas画布上得到一帧图像,不停地周期性执行.render()方法就可以更新canvas画布内容,一般场景越复杂往往渲染性能越低,也就是每秒钟执行.render()的次数越低。
通过stats.js库可以查看three.js当前的渲染性能,具体说就是计算three.js的渲染帧率(FPS),所谓渲染帧率(FPS),简单说就是three.js每秒钟完成的渲染次数,一般渲染达到每秒钟60次为最佳状态。
方法,stats.setMode,显示模式。
js
import Stats from 'three/examples/jsm/libs/stats.module.js'
const staRef = ref<HTMLDivElement>()
// 创建帧率信息
const stats = new Stats()
// stats.setMode(0);//默认模式
// 获取执行多少次
function render2() {
// 更新帧率信息
stats.update()
mesh.rotateY(0.01)
renderer.render(scene, camera)
requestAnimationFrame(render2)
}
onMounted(() => {
if (staRef.value) {
// 将帧率dom添加到div中
staRef.value.appendChild(stats.dom)
}
})
几何体
会发现有些看不见反面,比如圆柱和圆形,这个需要设置双面可见。
js
import { BoxGeometry, SphereGeometry, CylinderGeometry, PlaneGeometry, CircleGeometry } from 'three'
// 长方形,宽度100,高度100,深度100
const geometry = new BoxGeometry(100, 100, 100)
// 球体
const geometry = new SphereGeometry(50)
// 圆柱
const geometry = new CylinderGeometry(30, 50, 100)
// 矩形平面
const geometry = new PlaneGeometry(100, 50)
// 圆形
const geometry = new CircleGeometry(50)
双面可见
js
import { MeshLambertMaterial, DoubleSide } from 'three'
// 给材质加上双面可见
const material = new MeshLambertMaterial({
color: 'blue', transparent: true, opacity: 0.8,
side: DoubleSide // 双面可见
})
WEBGL 的基础设置
设置渲染器锯齿属性.antialias的值可以直接在参数中设置,也可通过渲染器对象属性设置。
js
import { WebGLRenderer } from 'three'
const renderer = new WebGLRenderer({
antialias: true, // 打开抗锯齿
alpha: true // 开启透明 // 也可用 renderer.setClearAlpha(0.0)
})
设备像素比 window.devicePixelRatio
js
// 不同硬件设备的屏幕的设备像素比window.devicePixelRatio值可能不同
console.log('查看当前屏幕设备像素比', window.devicePixelRatio)
如果是 1 设置不设置差别不大。可以有效避免模糊。背景颜色设置
设置设备像素比.setPixelRatio()
js
const renderer = new WebGLRenderer({
antialias: true
})
// 设置像素比
renderer.setPixelRatio(window.devicePixelRatio)
// 设置背景颜色
renderer.setClearColor(0x333333)
gui.js库
gui.js 说白了就是一个前端js库,对HTML、CSS和JavaScript进行了封装,学习开发的时候,借助 gui.js 可以快速创建控制三维场景的UI交互界面。
threejs 已经包含了GUI 无需再安装。
创建一个可视化控件。用于操作 3D 场景。
js
import GUI from 'three/examples/jsm/libs/lil-gui.module.min.js'
const gui = new GUI()
gui.domElement.style.right = '0px'
gui.domElement.style.width = '300px'
添加具体操作项,更改 X 轴。最小0,最大100,默认30
js
// 环境光
const ambientLight = new AmbientLight(0xffffff, 0.3)
scene.add(ambientLight)
// 添加 GUI, 环境光强度,从 0 - 3
gui.add(ambientLight, 'intensity', 0, 3)

命名
显示英语,很不友好,而且很容易重复
每次拖动,步长多少
js
const gui = new GUI()
// 添加 GUI, 环境光强度,从 0 - 3
gui.add(ambientLight, 'intensity', 0, 3)
.name('环境光强度') // 设置名称
.step(0.1) // 每次拖动步长 0.1
change方法,监测数据改变出发
js
// 添加 GUI, 环境光强度,从 0 - 3
gui.add(ambientLight, 'intensity', 0, 3).name('环境光强度')
.step(0.1).onChange(value => {
console.log(value)
})
修改颜色
js
const gui = new GUI()
gui.addColor(mesh.material, 'color').name('颜色')
下拉框 / 单选框
下拉框
显示数值

js
const gui = new GUI()
gui.add(mesh.position, 'x', [-100, 0, 100]).name('x 坐标')
显示文字

js
const gui = new GUI()
gui.add(mesh.position, 'y', {
top: -100,
center: 0,
bottom: 100
}).name('y 坐标')
单选框

js
const obj = {
flag: true
}
const gui = new GUI()
gui.add(obj, 'flag').name('转动')
function animate() {
// 添加拍摄控制
if(obj.flag) {
renderer.render(scene, camera)
}
requestAnimationFrame(animate)
}
// 添加到 div 中
onMounted(() => {
if (threeRef.value) {
threeRef.value.appendChild(renderer.domElement)
animate()
}
})
分组
new GUI() 实例化一个gui对象,默认创建一个总的菜单,通过gui对象的.addFolder()方法可以创建一个子菜单,当你通过GUI控制的属性比较多的时候,可以使用.addFolder()进行分组。
默认是开启状态,如何关闭呢
.addFolder()创建的对象,同样也具有.addFolder()属性,可以继续嵌套子菜单。
js
const gui1 = gui.addFolder('环境光')
gui1.close() // 默认关闭,对应 .open 默认的
gui1.add(ambientLight, 'intensity', 0, 3)

模型
js
// 网格模型,将材质和几何体连接起来
const mesh = new Mesh(geometry, material)
// 定义一个点模型
const points = new Points(geometry, material)
线模型
Line,非闭合的线
渲染效果是从第一个点开始到最后一个点,依次连成线。
js
// 线模型
const points = new Line(geometry, material)
LineLoop,闭合的线
js
// 闭合线条
const line = new THREE.LineLoop(geometry, material);
LineSegments:非连续的线
js
//非连续的线条
const line = new THREE.LineSegments(geometry, material);
意思是指,如果一个线已经被连接了,就不会再连接别的了
网格模型
空间中一个三角形有正反两面,那么Three.js的规则是如何区分正反面的?非常简单,你的眼睛(相机)对着三角形的一个面,如果三个顶点的顺序是逆时针方向,该面视为正面,如果三个顶点的顺序是顺时针方向,该面视为反面。
Three.js的材质默认正面可见,反面不可见。
js
const material = new THREE.MeshBasicMaterial({
color: 0x0000ff, //材质颜色
side: THREE.FrontSide, //默认只有正面可见
});
const material = new THREE.MeshBasicMaterial({
side: THREE.DoubleSide, //两面可见
});
const material = new THREE.MeshBasicMaterial({
side: THREE.BackSide, //设置只有背面可见
});
看下图,假设不涉及Z轴,z轴全是0,根据图片,构建一个矩形。

js
import { BufferAttribute, BufferGeometry, DoubleSide, Mesh, MeshBasicMaterial } from 'three'
//创建一个空的几何体对象
const geometry = new BufferGeometry()
// 添加顶点数据,位置数据
const vertices = new Float32Array([
0, 0, 0,
80, 0, 0,
80, 80, 0,
0, 0, 0,
80, 80, 0,
0, 80, 0,
])
// 创建顶点数据对象
const bufferAttributes = new BufferAttribute(vertices, 3)
// 设置几何体的顶点位置属性
geometry.attributes.position = bufferAttributes
// 创建点材质
const material = new MeshBasicMaterial({
color: 0xffff00,
side: DoubleSide
})
// 定义一个点模型
const points = new Mesh(geometry, material)
export { points }
几何体
缓冲类型几何体BufferGeometry
threejs 的长方体 BoxGeometry、球体 SphereGeometry 等几何体都是基于 BufferGeometry 类构建的,BufferGeometry 是一个没有任何形状的空几何体,你可以通过BufferGeometry自定义任何几何形状,具体一点说就是定义顶点数据。
案例,创建一个点模型
案例
生成点模型
js
import { BufferAttribute, BufferGeometry, Points, PointsMaterial } from 'three'
//创建一个空的几何体对象
const geometry = new BufferGeometry()
// 添加顶点数据,位置数据
const vertices = new Float32Array([
0, 0, 0,
50, 0, 0,
0, 100, 0,
0, 0, 10,
0, 0, 100,
50, 0, 10,
])
// 创建顶点数据对象,3 个为一组
const bufferAttributes = new BufferAttribute(vertices, 3)
// 设置几何体的顶点位置属性
geometry.attributes.position = bufferAttributes
// 创建点材质
const material = new PointsMaterial({
color: 0xffff00,
size: 10
})
// 定义一个点模型对象
const points = new Points(geometry, material)
export { points }
几何体顶点索引数据
网格模型 Mesh 对应的几何体 BufferGeometry 拆分多个三角后,很多三角形重合的顶点位置坐标是相同的,如果需要减少顶点坐标数据量,可以用 geometry.index 实现。
还是需要创建位置属性的,如果没有设置索引数据,那么会按照数据点顺序来,它描述的只是XYZ坐标数据,
索引数据描述了如何拼接,但是没有XYZ坐标数据,也是不能创建的。
js
import { BufferAttribute, BufferGeometry, DoubleSide, Mesh, MeshBasicMaterial } from 'three'
const geometry = new BufferGeometry()
// 删除重复项
const vertices = new Float32Array([ 0, 0, 0, 80, 0, 0, 80, 80, 0, 0, 80, 0 ])
// 三个数一组成一个顶点 (x, y, z)
const bufferAttributes = new BufferAttribute(vertices, 3)
geometry.attributes.position = bufferAttributes
const material = new MeshBasicMaterial({
color: 0xffff00, side: DoubleSide
})
// 定义索引,每三个索引组成一个三角形
// 这里表示 (0,1,2) 和 (0,2,3) 两个三角形
const uint16Array = new Uint16Array([0, 1, 2, 0, 2, 3])
// setIndex 需要一个 BufferAttribute,存储的是顶点索引
geometry.setIndex(new BufferAttribute(uint16Array, 1))
// 定义一个点模型
const points = new Mesh(geometry, material)
export { points }
顶点法线数据
上面的索引弄完之后,我们可以尝试把材质从不受光照影响改成受光照影响,几何体颜色会变成黑色,当然需要把背景色改成其他色才能看出来。
所以我们使用受光照影响的材质,几何体Geometry需要定义顶点法线数据
法线:法线是用来描述顶点的朝向,有了朝向才可以计算光的强度。
那法线数据就要按照顶点数据去定义。每个顶点数据都要有法线数据。
注意环境光不依赖于法线。
无索引,有索引实际也是一样的,但是顶点数据的数组数量和法线数据的数量一样即可。
因为使用的法线,z 都是 1,所以背面肯定是看不到的,背面是-1。
js
import { BufferAttribute, BufferGeometry, DoubleSide, Mesh, MeshLambertMaterial } from 'three'
const geometry = new BufferGeometry()
// 删除重复项
const vertices = new Float32Array([
0, 0, 0,
80, 0, 0,
80, 80, 0,
0, 0, 0,
80, 80, 0,
0, 80, 0
])
// 受光照材质
const material = new MeshLambertMaterial({
color: 0x00ffff, side: DoubleSide
})
const bufferAttributes = new BufferAttribute(vertices, 3)
geometry.attributes.position = bufferAttributes
// 矩形平面,无索引,两个三角形,6个顶点
// 每个顶点的法线数据和顶点位置数据一一对应
const normals = new Float32Array([
0, 0, 1, //顶点1法线( 法向量 )
0, 0, 1, //顶点2法线
0, 0, 1, //顶点3法线
0, 0, 1, //顶点4法线
0, 0, 1, //顶点5法线
0, 0, 1, //顶点6法线
]);
// 设置几何体的顶点法线属性.attributes.normal
geometry.attributes.normal = new BufferAttribute(normals, 3);
// const uint16Array = new Uint16Array([ 0, 1, 2, 0, 2, 3 ])
// geometry.setIndex(new BufferAttribute(uint16Array, 1))
// 定义一个点模型
const points = new Mesh(geometry, material)
export { points }
细分数
通过 wireframe 来查看平面绘制数据。

js
// 创建三维场景
const scene = new Scene()
const geometry = new PlaneGeometry(100,50,1,1);
const material = new MeshBasicMaterial({
color: 0x00ffff, side: DoubleSide,
wireframe: true // 线框模式,查看绘制数据
})
const mesh = new Mesh(geometry, material)
scene.add(mesh)
将 PlaneGeometry 的 widthSegments 和 heightSegments 都从1改成10,可以看到细分的更多。

如果是球体的细分数越多,那么表面越光滑。
三角形数量与性能
对于一个曲面而言,细分数越大,表面越光滑,但是三角形和顶点数量却越多。
几何体三角形数量或者说顶点数量直接影响Three.js的渲染性能,在不影响渲染效果的情况下,一般尽量越少越好。
常见方法

js
// 几何体xyz三个方向都放大2倍
geometry.scale(2, 2, 2);
// 几何体沿着x轴平移50
geometry.translate(50, 0, 0);
// 几何体绕着x轴旋转45度
geometry.rotateX(Math.PI / 4);
// 几何体旋转、缩放或平移之后,查看几何体顶点位置坐标的变化
// BufferGeometry的旋转、缩放、平移等方法本质上就是改变顶点的位置坐标
console.log('顶点位置数据', geometry.attributes.position);
模型对象 / 材质
三维向量 Vector3 是Object3D的方法的一个参数类。 与 模型位置
Vector3 是Object3D的方法的一个参数类。
模型Points、线模型Line、网格网格模型Mesh等模型对象的父类都是Object3D (opens new window),如果想对这些模型进行旋转、缩放、平移等操作。需要借助 三维向量来实现。
三维向量Vector3有xyz三个分量,threejs中会用三维向量Vector3表示很多种数据。
js
//new THREE.Vector3()实例化一个三维向量对象
const v3 = new THREE.Vector3(0,0,0);
console.log('v3', v3);
v3.set(10,0,0);//set方法设置向量的值
v3.x = 100;//访问x、y或z属性改变某个分量的值
// scale 和 position 都是三维向量。
const mesh = new Mesh(geometry, material)
// mesh.scale
// mesh.position
// 方法平移直接操作的三维向量
// mesh.translateY()
// mesh.translateX()
// mesh.translateZ()
沿着自定义的方向移动。
js
//向量Vector3对象表示方向
const axis = new THREE.Vector3(1, 1, 1);
axis.normalize(); //向量归一化
//沿着axis轴表示方向平移100
mesh.translateOnAxis(axis, 100);
欧拉 Euler 与角度属性 rotation
模型的角度属性.rotation和四元数属性.quaternion都是表示模型的角度状态,只是表示方法不同,.rotation属性值是欧拉对象Euler (opens new window),.quaternion属性值是是四元数对象Quaternion
js
// 创建一个欧拉对象,表示绕着xyz轴分别旋转45度,0度,90度
const Euler = new THREE.Euler( Math.PI/4,0, Math.PI/2);
通过属性设置欧拉对象的三个分量值。
js
const Euler = new THREE.Euler();
Euler.x = Math.PI/4;
Euler.y = Math.PI/2;
Euler.z = Math.PI/4;
改变角度属性.rotation
js
//绕y轴的角度设置为60度
mesh.rotation.y += Math.PI/3;
//绕y轴的角度增加60度
mesh.rotation.y += Math.PI/3;
//绕y轴的角度减去60度
mesh.rotation.y -= Math.PI/3;
旋转方法.rotateX()、.rotateY()、.rotateZ()
模型执行.rotateX()、.rotateY()等旋转方法,你会发现改变了模型的角度属性.rotation。
js
mesh.rotateX(Math.PI/4);//绕x轴旋转π/4
// 绕着Y轴旋转90度
mesh.rotateY(Math.PI / 2);
颜色
颜色对象Color
颜色对象有三个属性,分别为.r、.g、.b,表示颜色RGB的三个分量。
js
const color = new THREE.Color();//默认是纯白色0xffffff。
color.setRGB(0,1,0);//RGB方式设置颜色
color.setHex(0x00ff00);//十六进制方式设置颜色
color.setStyle('#00ff00');//前端CSS颜色值设置颜色
color.set(0x00ff00);//十六进制方式设置颜色
color.set('#00ff00');//前端CSS颜色值设置颜色
模型材质父类Material
MeshBasicMaterial、MeshLambertMaterial、MeshPhongMaterial等子类网格材质会从父类Material继承一些属性和方法,比如透明度属性.opacity、面属性.side、是否透明属性.transparent等等
js
material.transparent = true;//开启透明
material.opacity = 0.5;//设置透明度
material.side = THREE.BackSide;//背面可以看到
material.side = THREE.DoubleSide;//双面可见
克隆.clone()和复制.copy()
克隆.clone()、复制.copy()是threejs很多对象都具有的方法,比如三维向量对象Vector3、网格模型Mesh、几何体、材质。
克隆
js
const v1 = new THREE.Vector3(1, 2, 3);
console.log('v1',v1);
//v2是一个新的Vector3对象,和v1的.x、.y、.z属性值一样
const v2 = v1.clone();
console.log('v2',v2);
复制
js
const v1 = new THREE.Vector3(1, 2, 3);
const v3 = new THREE.Vector3(4, 5, 6);
//读取v1.x、v1.y、v1.z的赋值给v3.x、v3.y、v3.z
v3.copy(v1);
实操
js
// 渲染循环
function render() {
mesh.rotateY(0.01);// mesh旋转动画
// 同步mesh2和mesh的姿态角度一样,不管mesh姿态角度怎么变化,mesh2始终保持同步
mesh2.rotation.copy(mesh.rotation);
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
层级模型
Vector3与模型位置、缩放属性

js
const group = new Group()
group.add(mesh)
group.add(mesh1)
console.log('查看group的子对象',group.children);
scene.add(group)
// console.log('查看Scene的子对象',scene.children);
场景对象Scene、组对象Group的.add()方法都是继承自它们共同的基类(父类)Object3D。
特性
父对象旋转缩放平移变换,子对象跟着变化
js
//沿着Y轴平移mesh1和mesh2的父对象,mesh1和mesh2跟着平移
group.translateY(100);
//父对象缩放,子对象跟着缩放
group.scale.set(4,4,4);
//父对象旋转,子对象跟着旋转
group.rotateY(Math.PI/6)
实战
js
import { BoxGeometry, BufferAttribute, BufferGeometry, DoubleSide, Group, Mesh, MeshLambertMaterial } from 'three'
const group1 = new Group()
group1.name = '高层'
for (let i = 0; i < 5; i++) {
const boxGeometry = new BoxGeometry(20, 60, 10)
const boxMaterial = new MeshLambertMaterial({ color: 0xffffff })
const mesh = new Mesh(boxGeometry, boxMaterial)
mesh.position.x = i * 30
mesh.name = i + 1 + '号楼'
group1.add(mesh)
}
// 平移父对象,所有子对象都会移动
group1.position.y = 30
const group2 = new Group()
group2.name = '洋房'
for (let i = 0; i < 5; i++) {
const boxGeometry = new BoxGeometry(20, 30, 10)
const boxMaterial = new MeshLambertMaterial({ color: 0x00ffff })
const mesh = new Mesh(boxGeometry, boxMaterial)
mesh.position.x = i * 30
mesh.name = i + 6 + '号楼'
group2.add(mesh)
}
group2.position.y = 15
group2.position.z = 50
// 统一管理
const model = new Group()
model.name = '小区房子'
model.add(group1, group2)
model.position.set(-50, 0, -25)
export { model }
遍历模型树结构、查询模型节点
递归遍历方法.traverse()
js
const model = new Group()
group.traverse(obj => {
if(obj.type === 'Mesh') {
console.log(obj.name)
}
})
根据名称查找到某一个节点
js
const objectByName = model.getObjectByName('5号楼')
if (objectByName instanceof Mesh) {
const material: MeshLambertMaterial = objectByName.material
material.color.set(0xff0000)
}
本地坐标和世界坐标
本地坐标:相对于父对象的坐标,就是本地坐标。相对于场景的坐标就是世界坐标。 如 group 或者 mesh 就是本地坐标。
世界坐标:场景的,我们打开直接看到的,就是世界坐标。
js
import { BoxGeometry, Group, Mesh, MeshLambertMaterial } from 'three'
const geometry = new BoxGeometry(20, 20, 20)
const material = new MeshLambertMaterial({ color: 0x00ffff })
const mesh = new Mesh(geometry, material)
mesh.position.set(50, 0, 0) // 本地坐标
const group = new Group()
group.add(mesh)
group.position.set(50, 0, 0) // 本地坐标
export { group }
这个时候实际上,我们在场景里,看到的是走了一百。
世界坐标就是所有的本地坐标相加到一起。
如何获取世界坐标呢
js
// 获取世界坐标
const worldPosition = mesh.getWorldPosition(new Vector3())
console.log(worldPosition)
局部坐标系
js
const meshAxesHelper = new AxesHelper(50);
mesh.add(meshAxesHelper);

改变模型相对局部坐标原点位置
原理就是,平移一下
js
import { BoxGeometry, Mesh, MeshLambertMaterial } from 'three'
const geometry = new BoxGeometry(50, 50, 50)
const material = new MeshLambertMaterial({ color: 0x00ffff })
const mesh = new Mesh(geometry, material)
// 平移几何体的顶点坐标,改变几何体自身相对局部坐标原点的位置
geometry.translate(50/2,0,0,)
export { mesh }
那如果围绕着自身局部坐标旋转呢。
js
function animate() {
mesh.rotateY(0.01) // 旋转动画
renderer.render(scene, camera)
requestAnimationFrame(animate)
}
// 添加到 div 中
onMounted(() => {
animate()
})
移除对象.remove()
场景对象Scene、组对象Group、网格模型对象Mesh的.remove()方法都是继承自它们共同的基类(父类)Object3D
js
// 删除父对象group的子对象网格模型mesh1
group.remove(mesh1);
scene.remove(ambient);//移除场景中环境光
scene.remove(model);//移除场景中模型对象
console.log('查看group的子对象',group.children);
#一次移除多个子对象
group.remove(mesh1,mesh2);
模型隐藏或显示
有时候需要临时隐藏一个模型,或者一个模型处于隐藏状态,需要重新恢复显示。
js
mesh.visible =false;// 隐藏一个网格模型,visible的默认值是true
group.visible =false;// 隐藏一个包含多个模型的组对象group
顶点UV坐标、纹理贴图
创建纹理贴图
设置纹理贴图时,不可以使用 color,默认 color 是白色的。白色才会生效。
js
import { BoxGeometry, Mesh, MeshLambertMaterial, SRGBColorSpace, TextureLoader } from 'three'
const geometry = new BoxGeometry(50, 50, 50)
// 纹理贴图
const textureLoader = new TextureLoader()
const texture = textureLoader.load('/favicon.ico')
// 图片都是 sRGB 颜色空间,设置 sRGB,确保渲染时的颜色显示更接近真实效果
texture.colorSpace = SRGBColorSpace
const material = new MeshLambertMaterial({
// 设置纹理贴图:Texture对象作为材质map属性的属性值
map: texture //map表示材质的颜色贴图属性
})
const mesh = new Mesh(geometry, material)
export { mesh }
自定义顶点UV坐标
顶点UV坐标的作用是从纹理贴图上提取像素映射到网格模型Mesh的几何体表面上。
顶点UV坐标可以在0~1.0之间任意取值,纹理贴图左下角对应的UV坐标是(0,0),右上角对应的坐标(1,1)

顶点UV坐标geometry.attributes.uv和顶点位置坐标geometry.attributes.position是一一对应的
UV顶点坐标你可以根据需要在0~1之间任意设置,具体怎么设置,要看你想把图片的哪部分映射到Mesh的几何体表面上。
js
import {
BufferAttribute,
BufferGeometry,
Mesh, MeshBasicMaterial,
SRGBColorSpace,
TextureLoader
} from 'three'
const geometry = new BufferGeometry()
geometry.attributes.position = new BufferAttribute(new Float32Array([
0, 0, 0,
200, 0, 0,
200, 100, 0,
0, 100, 0
]), 3)
geometry.index = new BufferAttribute(new Uint16Array([ 0, 1, 2, 0, 2, 3 ]), 1)
// 纹理坐标
const float32Array = new Float32Array([
0, 0,
1, 0,
1, 1,
0, 1
])
geometry.attributes.uv = new BufferAttribute(float32Array, 2)
// 纹理贴图
const textureLoader = new TextureLoader()
const texture = textureLoader.load('/favicon.ico')
// 图片都是 sRGB 颜色空间,设置 sRGB,确保渲染时的颜色显示更接近真实效果
texture.colorSpace = SRGBColorSpace
const material = new MeshBasicMaterial({
// 设置纹理贴图:Texture对象作为材质map属性的属性值
map: texture
})
const mesh = new Mesh(geometry, material)
export { mesh }
获取纹理贴图四分之一
js
const uvs = new Float32Array([
0, 0,
0.5, 0,
0.5, 0.5,
0, 0.5,
]);
圆形平面设置纹理贴图
形有uv坐标,然后将圆形去贴上去,圆形之外的会舍弃。
js
//CircleGeometry的顶点UV坐标是按照圆形采样纹理贴图
const geometry = new THREE.CircleGeometry(60, 100)
//纹理贴图加载器TextureLoader
const texLoader = new THREE.TextureLoader()
const texture = texLoader.load('./texture.jpg')
const material = new THREE.MeshBasicMaterial({
map: texture,//map表示材质的颜色贴图属性
side:THREE.DoubleSide
});
const mesh = new THREE.Mesh(geometry, material)
纹理对象Texture阵列
js
import {
DoubleSide,
Mesh, MeshBasicMaterial, PlaneGeometry, RepeatWrapping,
SRGBColorSpace,
TextureLoader
} from 'three'
// CircleGeometry的顶点UV坐标是按照圆形采样纹理贴图
const geometry = new PlaneGeometry(2000, 2000)
const textureLoader = new TextureLoader()
const texture = textureLoader.load('/images/微信图片_20250912163033_2166_3.png')
texture.colorSpace = SRGBColorSpace
// 设置阵列模式
texture.wrapS = RepeatWrapping
texture.wrapT = RepeatWrapping
// uv两个方向纹理重复数量
texture.repeat.set(12, 12) //注意选择合适的阵列数量
const material = new MeshBasicMaterial({
map: texture,
side: DoubleSide,
// transparent: true // png 图片可以开启透明
})
const mesh = new Mesh(geometry, material)
export { mesh }

UV 动画
学习偏移属性
映射方式:
x 坐标的对应的是 wrapS,等于U方向
y 坐标的对应的是 wrapT,等于V方向
js
import { Mesh, MeshBasicMaterial, PlaneGeometry, TextureLoader } from 'three'
const geometry = new PlaneGeometry(200, 20)
const textureLoader = new TextureLoader()
const texture = textureLoader.load('/images/img.png')
const material = new MeshBasicMaterial({
map: texture
})
// 纹理对象的偏移属性(修改了UV坐标)
texture.offset.x = -0.5
texture.offset.y = 0.5
const mesh = new Mesh(geometry, material)
mesh.rotateX(-Math.PI / 2)
export { mesh }
实现轮播图
js
import { Mesh, MeshBasicMaterial, PlaneGeometry, RepeatWrapping, TextureLoader } from 'three'
const geometry = new PlaneGeometry(200, 20)
const textureLoader = new TextureLoader()
const texture = textureLoader.load('/images/img.png')
const material = new MeshBasicMaterial({
map: texture
})
// 纹理对象的偏移属性(修改了UV坐标)
texture.offset.x = -0.5
texture.wrapS = RepeatWrapping // 实现轮播
const mesh = new Mesh(geometry, material)
mesh.rotateX(-Math.PI / 2)
export { mesh, texture }
js
<script setup lang="ts">
import { Scene, PerspectiveCamera, WebGLRenderer, AxesHelper, PointLight } from 'three'
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import Stats from 'three/examples/jsm/libs/stats.module.js'
import { mesh, texture } from '@/views/pi1.js'
const threeRef = ref<HTMLDivElement>()
function handleResize() {
// 根据窗口重写设置宽高
renderer.setSize(window.innerWidth, window.innerHeight)
// 重新设置摄像机宽高比
camera.aspect = window.innerWidth / window.innerHeight
// 必须在设置参数后使用。更新相机矩阵
camera.updateProjectionMatrix()
}
window.addEventListener('resize', handleResize)
// 组件卸载时移除监听器
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
renderer.dispose()
})
// 创建三维场景
const scene = new Scene()
scene.add(mesh)
// 创建坐标轴
const axesHelper = new AxesHelper(100)
scene.add(axesHelper)
// 创建一个点光源
const pointLight = new PointLight(0xffffff, 1)
pointLight.decay = 0
// 设置点光源位置
pointLight.position.set(400, 200, 300)
scene.add(pointLight)
const width = window.innerWidth
const height = window.innerHeight
// 创建一个透视投影相机,假设 Canvas 画布是800*500
const camera = new PerspectiveCamera(30, width / height, 0.1, 8000)
// 设置相机位置
camera.position.set(450, 400, 100)
// 设置相机拍照方向
camera.lookAt(mesh.position)
// 创建渲染器
const renderer = new WebGLRenderer({
antialias: true
})
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setClearColor(0x333333)
// 设置画布尺寸,宽度800,高度500
renderer.setSize(width, height)
// 创建控制器
const orbitControls = new OrbitControls(camera, renderer.domElement)
const staRef = ref<HTMLDivElement>()
// 创建帧率信息
const stats = new Stats()
function animate() {
texture.offset.x += 0.01
renderer.render(scene, camera)
requestAnimationFrame(animate)
}
// 添加到 div 中
onMounted(() => {
if (staRef.value) {
const node = stats.dom
node.style.top = '20px'
node.style.left = '20px'
staRef.value.appendChild(node)
}
if (threeRef.value) {
threeRef.value.appendChild(renderer.domElement)
animate()
}
})
renderer.render(scene, camera)
</script>
辅助
辅助网格地面
js
const gridHelper = new GridHelper(600, 50)
scene.add(gridHelper)
gridHelper.position.y = -1 // 可以看到坐标系
加载外部三维模型
GLTF格式
GLTF格式是新2015发布的三维模型格式。
和其他格式而言,gltf 格式可以包含更多的模型信息,几乎是包含了所有的三维模型相关信息,如模型层级关系、PBR 材质、纹理贴图、骨骼动画、变形动画。
GLTF格式信息
GLTF文件就是通过JSON的键值对方式来表示模型信息,比如meshes表示网格模型信息,materials表示材质信息。
json
{
"asset": {
"version": "2.0",
},
...
// 模型材质信息
"materials": [
{
"pbrMetallicRoughness": {//PBR材质
"baseColorFactor": [1,1,0,1],
"metallicFactor": 0.5,//金属度
"roughnessFactor": 1//粗糙度
}
}
],
// 网格模型数据
"meshes": ...
// 纹理贴图
"images": [
{
// uri指向外部图像文件
"uri": "贴图名称.png"//图像数据也可以直接存储在.gltf文件中
}
],
"buffers": [
// 一个buffer对应一个二进制数据块,可能是顶点位置 、顶点索引等数据
{
"byteLength": 840,
//这里面的顶点数据,也快成单独以.bin文件的形式存在
"uri": "data:application/octet-stream;base64,AAAAPwAAAD8AAAA/AAAAPwAAAD8AAAC/.......
}
],
}
.bin文件
有些glTF文件会关联一个或多个.bin文件,.bin文件以二进制形式存储了模型的顶点数据等信息。 .bin文件中的信息其实就是对应gltf文件中的 buffers属性,buffers.bin中的模型数据,可以存储在.gltf文件中,也可以单独一个二进制.bin文件。
json
"buffers": [
{
"byteLength": 102040,
"uri": "文件名.bin"
}
]
二进制.glb
gltf格式文件不一定就是以扩展名.gltf结尾,.glb就是gltf格式的二进制文件。比如你可以把.gltf模型和贴图信息全部合成得到一个.glb文件中,.glb文件相对.gltf文件体积更小,网络传输自然更快
加载 GLTF
js
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { Group } from 'three'
// 实例化一个加载器对象
const loader = new GLTFLoader()
const model = new Group()
loader.load('/gltf/scene.gltf', gltf => {
model.add(gltf.scene)
})
export { model }
单位问题
项目开发的时候,程序员对一个模型或者说一个三维场景要有一个尺寸的概念,不用具体值,要有一个大概印象。
一般通过三维建模软件可以轻松测试测量模型尺寸,比如作为程序员你可以用三维建模软件blender打开gltf模型,测量尺寸。
obj、gltf格式的模型信息只有尺寸,并不含单位信息。
不过实际项目开发的时候,一般会定义一个单位,一方面甲方、前端、美术之间更好协调,甚至你自己写代码也要有一个尺寸标准。比如一个园区、工厂,可以m为单位建模,比如建筑、人、相机都用m为尺度去衡量,如果单位不统一,就需要你写代码,通过.scale属性去缩放。
更改 LookAt 观察点
在循环中打印 orbitControls.target 的值,此时平移试图,可以打印当前观察点的结果。
相机的Lookat和控件的target结果要一致。
js
// 渲染循环
const orbitControls = new OrbitControls(camera, renderer.domElement)
function render() {
if (scene) {
console.log('tar', orbitControls.target)
renderer.render(scene, camera)
}
requestAnimationFrame(render)
}
gltf不同文件形式(.glb)
.gltf格式模型文件,有不同的组织形式。
- 单独.gltf文件
- 单独.glb文件
- .gltf + .bin + 贴图文件
获取模型的子元素
js
function loadGltf(url) {
const loader = new GLTFLoader()
loader.load( url, gltf => {
// 获取模型子元素
console.log(gltf.scene.children)
} )
}
子元素的每一项,都有name属性

那么我们就可以根据模型名称查找模型
js
// 查找子元素
const name = gltf.scene.getObjectByName( 'Camera001_1' )
console.log( name )
材质共享问题
美术通过三维建模软件,比如Blender绘制好一个三维场景以后,一些外观一样的Mesh,可能会共享一个材质对象。
改变一个模型颜色其它模型跟着变化
解决问题方向
- 三维建模软件中设置,需要代码改变材质的Mesh不要共享材质,要独享材质。
- 代码批量更改:克隆材质对象,重新赋值给mesh的材质属性
代码方式解决多个mesh共享材质的问题
js
//用代码方式解决mesh共享材质问题
gltf.scene.getObjectByName("小区房子").traverse(function (obj) {
if (obj.isMesh) {
// .material.clone()返回一个新材质对象,和原来一样,重新赋值给.material属性
obj.material = obj.material.clone();
}
});
mesh1.material.color.set(0xffff00);
mesh2.material.color.set(0x00ff00);
纹理encoding和渲染器
| texture.colorSpace | 值 | 语义 |
|---|---|---|
| NoColorSpace | 不做任何处理(默认) | 无颜色空间信息 |
| SRGBColorSpace | sRGB → 线性 转换 | 线性空间纹理 |
| LinearSRGBColorSpace | 不做任何处理 | sRGB 纹理 |
js
// 加载草地纹理贴图
const texture = new TextureLoader().load('/images/1.png')
texture.colorSpace = SRGBColorSpace
循环遍历打印,纹理类型
js
gltf.scene.traverse( (object) => {
if (object.type === 'Mesh') {
if(object.material.map) {
// 获取纹理颜色空间
console.log('map', object.material.map.colorSpace)
}
}
} )
注意渲染器的颜色空间也要设置一样
js
const renderer = new WebGLRenderer({ antialias: true })
renderer.outputColorSpace = SRGBColorSpace
纹理反转
纹理对象Texture翻转属性.flipY
.flipY表示是否翻转纹理贴图在Mesh上的显示位置。
js
#gltf的贴图翻转属性.flipY默认值
loader.load("../手机模型.glb", function (gltf) {
const mesh = gltf.scene.children[0]; //获取Mesh
console.log('.flipY', mesh.material.map.flipY);
})
//是否翻转纹理贴图
texture.flipY = false;
PBR 材质
渲染占用资源和表现能力,整体上来看,就是渲染表现能力越强,占用的计算机硬件资源更多。
占用渲染资源
MeshBasicMaterial < MeshLambertMaterial < MeshPhongMaterial < MeshStandardMaterial < MeshPhysicalMaterial
渲染表现能力
MeshBasicMaterial < MeshLambertMaterial < MeshPhongMaterial < MeshStandardMaterial < MeshPhysicalMaterial
金属度和粗糙度 / 环境贴图
只能是 MeshStandardMaterial
js
object.material = new MeshStandardMaterial({
metalness: 0.9, // 材质的金属度
roughness: 0.5 // 材质的粗糙程度
})
环境贴图

立方体纹理加载器CubeTextureLoader
- TextureLoader返回Texture
- CubeTextureLoader返回CubeTexture
通过纹理贴图加载器TextureLoader的.load()方法加载一张图片可以返回一个纹理对象Texture。
立方体纹理加载器CubeTextureLoader的.load()方法是加载6张图片,返回一个立方体纹理对象CubeTexture。
立方体纹理对象 CubeTexture 的父类是纹理对象 Texture。
- envMap 会实现反光效果,比如金属上,反光的大小。随着物品会变化。
- envMapIntensity:反射率
- roughness:粗糙度。
js
const cubeTexture = new CubeTextureLoader()
// 加载路径,要下面有图片
.setPath('/images/环境贴图0/')
.load(
// 路径下的所有图片
[ 'nx.jpg', 'ny.jpg', 'nz.jpg', 'px.jpg', 'py.jpg', 'pz.jpg'
])
// 加载器
const loader = new FBXLoader()
loader.load(url, scene => {
scene.traverse(( object ) => {
if (object.type === 'Mesh') {
object.material = new MeshStandardMaterial({
metalness: 1.0, // 材质的金属度
roughness: 0.0, // 材质的粗糙程度
envMap: cubeTexture, // 设置
envMapIntensity: 1.0 //
})
}
})
console.log(scene)
resolve(scene)
})
更换不同明暗的环境贴图,场景中模型的明暗也有变化。
如果要给场景的环境属性加,所有受影响的材质都会贴图
场景环境属性.environment
希望环境贴图影响场景中scene所有Mesh,可以通过Scene的场景环境属性.environment实现,把环境贴图对应纹理对象设置为.environment的属性值即可。
js
scene.environment = textureCube
环境贴图色彩空间编码.encoding
js
//如果renderer.outputEncoding=THREE.sRGBEncoding;环境贴图需要保持一致
textureCube.encoding = THREE.sRGBEncoding;
MeshPhysicalMaterial清漆层
MeshPhysicalMaterial和 MeshStandardMaterial 都是拥有金属度 metalness、粗糙度 roughness 属性的PBR材质,MeshPhysicalMaterial 是在 MeshStandardMaterial 基础上扩展出来的子类,除了继承了 MeshStandardMaterial 的金属度、粗糙度等属性,还新增了清漆.clearcoat、透光率.transmission、反射率.reflectivity、光泽.sheen、折射率.ior等等各种用于模拟生活中不同材质的属性。
清漆层属性.clearcoat
清漆层属性 .clearcoat 可以用来模拟物体表面一层透明图层,就好比你在物体表面刷了一层透明清漆,喷了点水。.clearcoat的范围 0 到 1,默认 0。
清漆层粗糙度.clearcoatRoughness
清漆层粗糙度 .clearcoatRoughness 属性表示物体表面透明涂层 .clearcoat 对应的的粗糙度,.clearcoatRoughness 的范围是为 0.0 至 1.0。默认值为 0.0。
js
const mesh = gltf.scene.getObjectByName('外壳01');
mesh.material = new THREE.MeshPhysicalMaterial({
color: mesh.material.color, //默认颜色
metalness: 0.9,// 金属度
roughness: 0.5,// 粗糙度
envMap: textureCube, // 环境贴图
envMapIntensity: 2.5, // 环境贴图对Mesh表面影响程度
// clearcoat: 1.0, // 物体表面清漆层或者说透明涂层的厚度
// clearcoatRoughness: 0.1, // 透明涂层表面的粗糙度
})
物理材质透光率.transmission
MeshPhysicalMaterial 的透光率属性 .transmission 和 折射率属性 .ior
透光率(透射度).transmission
为了更好的模拟玻璃、半透明塑料一类的视觉效果,可以使用物理透明度.transmission属性代替Mesh普通透明度属性.opacity。
使用 .transmission 属性设置 Mesh 透明度, 即便完全透射的情况下仍可保持高反射率。
物理光学透明度 .transmission 的值范围是从 0.0 到 1.0。默认值为 0.0。
js
const mesh = gltf.scene.getObjectByName('玻璃01')
mesh.material = new THREE.MeshPhysicalMaterial({
transmission: 1.0, //玻璃材质透光率,transmission替代opacity
})
折射率.ior
非金属材料的折射率从 1.0 到 2.333。默认值为 1.5。
不同材质的折射率,你可以百度搜索。
js
new THREE.MeshPhysicalMaterial({
ior:1.5,//折射率
})
js
const mesh = gltf.scene.getObjectByName('玻璃01')
mesh.material = new THREE.MeshPhysicalMaterial({
metalness: 0.0,//玻璃非金属
roughness: 0.0,//玻璃表面光滑
envMap:textureCube,//环境贴图
envMapIntensity: 1.0, //环境贴图对Mesh表面影响程度
transmission: 1.0, //玻璃材质透光率,transmission替代opacity
ior:1.5, //折射率
})
Three.js 与建模软件 PBR 材质
在 三维建模软件(如 Blender、3dsMax、Maya)中设置好 → 导出为 glTF → Three.js 自动解析。
glTF 能导出的材质属性,可导出的常见 PBR 属性:
- 金属度 metalness
- 粗糙度 roughness
- 清漆 clearcoat、清漆粗糙度
- 透光率(透射度)transmission
⚠️ 限制:有些建模软件的特有材质属性,glTF 并不支持,Three.js 也无法解析。
注意:环境贴图依旧需要前端来配置。
渲染器问题
保存为图片
js
const renderer = new WebGLRenderer(
{
antialias: true,
preserveDrawingBuffer: true // 开启可以保存
})
function down() {
const link = document.createElement('a')
const canvas = renderer.domElement // 获取canvas对象
link.href = canvas.toDataURL('image/png') // 转换为dataURL
link.download = 'canvas.png'
link.click()
}
深度冲突
模型闪烁的原因简单地说就是深度冲突
主要是两个Mesh重合,电脑GPU分不清谁在前谁在后
html
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { camera, renderer } from '@/views/three3W/RendererCamera.ts'
import { AmbientLight, DoubleSide, Mesh, MeshLambertMaterial, PlaneGeometry, Scene } from 'three'
defineOptions({
name: 'AboutView2'
})
const scene = new Scene()
const cvRef = ref<HTMLDivElement>()
const geometry = new PlaneGeometry(250, 250)
const material = new MeshLambertMaterial({
color: 0x00ffff,
side: DoubleSide
})
const mesh = new Mesh(geometry, material)
const geometry1 = new PlaneGeometry(300, 300)
const material1 = new MeshLambertMaterial({
color: 0xff6666,
side: DoubleSide
})
const mesh1 = new Mesh(geometry1, material1)
// 适当偏移,解决深度冲突
mesh1.position.z = 1
scene.add(mesh, mesh1)
// 环境光
const ambientLight = new AmbientLight(0xffffff, 0.5)
scene.add(ambientLight)
// 渲染循环
function render() {
renderer.render(scene, camera) //执行渲染操作
requestAnimationFrame(render)
}
onMounted(() => {
if (cvRef.value) {
cvRef.value.appendChild(renderer.domElement)
render()
}
})
</script>
<template>
<div ref="cvRef" class="cv"></div>
</template>
透视投影相机对距离影响(深度冲突)
第1步:设置两个Mesh平面的距离相差0.1,课件中案例源码你可以看到,没有深度冲突导致的模型闪烁问题
js
mesh2.position.z = 0
mesh2.position.z = 0.1
camera.position.set(292, 223, 185)
第2步:改变相机.position属性,你会发现当相机距离三维模型较远的时候,两个面也可能出现深度冲突,当然你也可以通过相机控件OrbitControls缩放功能,改变相机与模型的距离,进行观察。
js
camera.position.set(292*5, 223*5, 185*5)
透视投影相机的投影规律是远小近大,和人眼观察世界一样,模型距离相机越远,模型渲染的效果越小,两个mesh之间的间距同样也会变小。当两个Mesh和相机距离远到一定程度,两个模型的距离也会无限接近0。
也可以用 logarithmicDepthBuffer 优化深度冲突问题
js
// WebGL渲染器设置
const renderer = new THREE.WebGLRenderer({
// 设置对数深度缓冲区,优化深度冲突问题
logarithmicDepthBuffer: true
})
模型加载进度条
js
const loader = new GLTFLoader()
loader.load(url, gltf => {
console.log(gltf.scene)
}, xhr => {
const data = xhr.loaded / xhr.total * 100
const progress = Math.floor(data)+ '%' // 进度
})