Three.js 学习记录

哈哈--- 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,可能会共享一个材质对象。

改变一个模型颜色其它模型跟着变化

解决问题方向

  1. 三维建模软件中设置,需要代码改变材质的Mesh不要共享材质,要独享材质。
  2. 代码批量更改:克隆材质对象,重新赋值给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清漆层

MeshPhysicalMaterialMeshStandardMaterial 都是拥有金属度 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)+ '%' // 进度
})
相关推荐
Mintopia3 天前
🌌 Three.js 几何变化动画配合噪声粒子教程:让你的代码也会“呼吸”
前端·javascript·three.js
爱看书的小沐7 天前
【小沐杂货铺】基于Three.js渲染三维风力发电机(WebGL、vue、react、WindTurbine)
javascript·vue.js·webgl·three.js·opengl·风力发电机·windturbine
Dragonir8 天前
React+Three.js 实现 Apple 2025 热成像 logo
前端·javascript·html·three.js·页面特效
CAD老兵11 天前
打造高性能二维图纸渲染引擎系列(一):Batched Geometry 助你轻松渲染百万实体
前端·webgl·three.js
CAD老兵11 天前
打造高性能二维图纸渲染引擎系列(三):高性能 CAD 文本渲染背后的隐藏工程
前端·webgl·three.js
CAD老兵11 天前
打造高性能二维图纸渲染引擎系列(二):创建结构化和可扩展的渲染场景
前端·webgl·three.js
勤奋菲菲12 天前
Vue3+Three.js:requestAnimationFrame的详细介绍
开发语言·javascript·three.js·前端可视化
小兔崽子去哪了16 天前
Three.js 曲线
three.js
Keepreal49617 天前
使用 Three.js 和 GSAP 动画库实现3D 名字抽奖
javascript·vue.js·three.js