Appearance
3D 项目性能优化
加载优化
1.缓存
强缓存和协商缓存
html
<meta http-equiv="Cache-Control" content="max-age=7200" />
<meta http-equiv="Expires" content="Mon, 20 Jul 2013 23:00:00 GMT" />
强缓存:直接从缓存中获取资源,不会再与服务端发生通信
expires
jsexpires: Wed, 11 Sep 2019 16:12:18 GMT
cache-control
jscache-control: max-age=31536000
协商缓存:浏览器需要向服务器去询问缓存的相关信息,进而判断是否重新发起请求
jsLast-Modified: Fri, 27 Oct 2017 06:35:57 GMT If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT
jsETag: W/"2a3b-1602480f459" If-None-Match: W/"2a3b-1602480f459"
如果你将3D模型文件放在前端,并且想要在部署时设置协商缓存
- 1.在服务器上启用mod_expires模块:
首先,确保你的Apache服务器上启用了mod_expires模块,以便你可以使用.htaccess文件来配置缓存。
- 2.在项目目录中创建.htaccess文件:
在存放3D模型文件的目录中创建一个名为.htaccess的文件。
- 3.在.htaccess文件中添加以下内容:
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType model/gltf+json "access plus 1 hour"
ExpiresByType application/octet-stream "access plus 1 hour"
</IfModule>
<IfModule mod_headers.c>
Header append Cache-Control "public, max-age=3600"
</IfModule>
- 上述配置的含义如下:
ExpiresActive On 启用Expires模块。 ExpiresByType 根据MIME类型设置缓存时间。在示例中,model/gltf+json 和 application/octet-stream 是3D模型文件的MIME类型,缓存有效期设置为1小时。 Header append Cache-Control "public, max-age=3600" 设置Cache-Control头字段,也将缓存有效期设置为1小时。
- 4.重新启动或重载Apache服务器:
保存.htaccess文件后,确保重新启动或重载Apache服务器,以使配置生效。
2.路由懒加载
- 未用懒加载,vue中路由代码如下
js
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component:HelloWorld
}
]
})
```
- vue异步组件实现懒加载
```js
import Vue from 'vue'
import Router from 'vue-router'
/* 此处省去之前导入的HelloWorld模块 */
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: resolve=>(require(["@/components/HelloWorld"], resolve))
}
]
})
- ES 提出的
import
方法实现懒加载
js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
const HelloWorld = ()=>import("@/components/HelloWorld")
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component:HelloWorld
}
]
})
```
### 3.Web Worker
> glb 模型并发加载工作原理
> web worker 可以解析 glb 模型 但是 postMessage 发送的数据类型只是普通的对象,
> 并且不能存在方法,方法无法传递,带方法会导致发送失败,并且不会触发 onerror
> THREE 构建物体所需的 bufferGeometry,还是 BufferAttribute 或者 Material 等原型对象无法被传递
> 传递到主线程的只是一个普通对象和上面的属性(对象中不能有函数)
> 可以通过生成一个 THREE 所需的类型 把传递过来的对象上的参数复制给 THREE 需要的对象上
> 这样在主线程生成一个同样的模型,但是省去了解析模型时间(模型解析在 web worker 中与 js 主线程并发执行)实现并发加载
- 创建 webworker
> 普通 worker 无法使用导入的库: 添加第二个参数 { type: 'module' }
```js
const worker = new Worker(new URL('../composables/task.ts', import.meta.url), { type: 'module' })
let model: any = null
worker.onmessage = (event) => {
console.log('webwork', event.data)
model = parseModel(event.data)
console.log({ model })
// scene.add(model)
}
worker.postMessage('floor')
- webworker 子线程任务
js
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
import setFloorMaterial from './setFloorMaterial'
import { genAnimations, genGroupStruct } from './parse'
const loader = new GLTFLoader()
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('./3D/draco/')
loader.setDRACOLoader(dracoLoader)
onmessage = (e) => {
const num = e.data
console.log({ num })
loader.load(
'http://localhost:5173/3D/gltf/floor-1.glb',
(gltf: any) => {
// 材质重置
setFloorMaterial(gltf.scene)
return postMessage({
...genGroupStruct(gltf.scene),
sceneAnimations: genAnimations(gltf.animations)
})
},
(xhr: any) => {
// console.log({ xhr })
},
(err: any) => {
console.log({ err })
postMessage({ msg: 'Worker 模型加载错误!' + num })
}
)
}
- 模型解析器
js
/**
* 生成基本参数 旋转 位移 缩放等属性
*/
const genBaseStruct = (obj: THREE.Object3D) => {
const { type, name, quaternion: q, position: p, rotation: r, scale: s, up: u, userData, visible, matrix } = obj
const quaternion = [q.x, q.y, q.z, q.w]
const position = [p.x, p.y, p.z]
const rotation = [r.x, r.y, r.z, r.order]
const scale = [s.x, s.y, s.z]
const up = [u.x, u.y, u.z]
return {
type,
name,
quaternion,
position,
rotation,
scale,
up,
matrix,
userData,
visible,
children: genObject3DChildren(obj.children),
animations: genAnimations(obj.animations)
}
}
/**
* 生成动画结构
*/
export const genAnimations = (animations: THREE.AnimationClip[]) =>
animations.map((animation) => {
animation['tracks'].forEach((t) => delete t['createInterpolant'])
return animation
})
/**
* 生成物体参数
*/
const genMeshStruct = (mesh: THREE.Mesh) => {
const { geometry, material } = mesh
return {
geometry,
material,
...genBaseStruct(mesh)
}
}
/**
* 生成物体参数
*/
const genLineSegmentsStruct = (lineSegments: THREE.LineSegments) => {
const { geometry, material } = lineSegments
return {
geometry,
material,
...genBaseStruct(lineSegments)
}
}
const genPointLightStruct = (pointLight: THREE.PointLight) => {
return {
power: pointLight.power,
color: pointLight.color,
decay: pointLight.decay,
castShadow: pointLight.castShadow,
distance: pointLight.distance,
frustumCulled: pointLight.frustumCulled,
intensity: pointLight.intensity,
layers: pointLight.layers,
...genBaseStruct(pointLight)
}
}
const genObject3DStruct = (object: THREE.Object3D) => {
return {
...genBaseStruct(object)
}
}
/**
* 生成子元素结构
*/
const genObject3DChildren = (children: THREE.Object3D[]) => {
const childStruct = []
for (const child of children) {
const { type } = child
if (type === 'Mesh') {
childStruct.push(genMeshStruct(child as THREE.Mesh))
} else if (type === 'LineSegments') {
childStruct.push(genLineSegmentsStruct(child as THREE.LineSegments))
} else if (type === 'Group') {
childStruct.push(genGroupStruct(child as THREE.Group))
} else if (type === 'PointLight') {
childStruct.push(genPointLightStruct(child as THREE.PointLight))
} else if (type === 'Object3D') {
childStruct.push(genObject3DStruct(child))
}
}
return childStruct
}
/**
* 生成物体组结构
*/
export const genGroupStruct = (group: THREE.Group) => {
const struct = { ...genBaseStruct(group) }
return struct
}
- 模型加载器
js
import * as THREE from 'three'
import { THREEMaterialType, Vector3Arr, IBaseProps, IGroupParams, IMeshParams, IPointLight } from './types'
/**
* 通过设置attributes index来复刻一个集合体
*/
const genGeometry = (geometry: IMeshParams['geometry']) => {
const geom = new THREE.BufferGeometry()
// const geom = new THREE.InstancedBufferGeometry()
const {
attributes: { position, uv, normal },
index
} = geometry
console.log({ position, uv, normal })
//处理几何坐标
const vertexBuffer = new THREE.InterleavedBuffer(position.data.array, 8)
const positions = new THREE.InterleavedBufferAttribute(vertexBuffer, position.itemSize, position.offset, position.normalized)
geom.setAttribute('position', positions)
const uvs = new THREE.InterleavedBufferAttribute(vertexBuffer, uv.itemSize, uv.offset, uv.normalized)
geom.setAttribute('uv', uvs)
const normals = new THREE.InterleavedBufferAttribute(vertexBuffer, normal.itemSize, normal.offset, normal.normalized)
geom.setAttribute('normal', normals)
geom.setIndex(index ? new THREE.BufferAttribute(index.array, index.itemSize, index.normalized) : null)
// const attributes = {
// position: new THREE.BufferAttribute(position.data.array, position.itemSize, position.normalized),
// uv: new THREE.BufferAttribute(uv.data.array, uv.itemSize, uv.normalized),
// normal: new THREE.BufferAttribute(normal.data.array, normal.itemSize, normal.normalized)
// }
// geom.attributes = attributes
// geom.index = index ? new THREE.BufferAttribute(index.array, index.itemSize, index.normalized) : null
return geom
}
/**
* 通过设置attributes index来复刻一个 LineSegments 集合体
*/
const genLineSegmentsGeometry = (geometry: IMeshParams['geometry']) => {
const geom = new THREE.BufferGeometry()
// const geom = new THREE.InstancedBufferGeometry()
const {
attributes: { position }
} = geometry
// 处理几何坐标
geom.setAttribute('position', new THREE.BufferAttribute(position.array, position.itemSize, position.normalized))
return geom
}
/**
* 根据传入纹理的参数生成真正有效的Material类型数据
*/
const genMaterial = (mate: IMeshParams['material']) => {
if (!mate) return undefined
const multipleMaterial = Array.isArray(mate)
const material = multipleMaterial ? ([] as THREE.Material[]) : new THREE[mate.type as THREEMaterialType]()
//处理材质
//多个材质
if (multipleMaterial && Array.isArray(material)) {
for (const m of mate) {
const im = new THREE[m.type as THREEMaterialType]()
material.push(im)
}
} else if (mate) {
//单个材质
Object.assign(material, mate)
}
return material
}
/**
* 处理基本属性转换(Object3D基类上的属性) matrix scale rotate translate position children animations
*/
const parseBaseParams = (params: IBaseProps, object: THREE.Object3D) => {
const matrix = new THREE.Matrix4()
matrix.elements = params.matrix.elements
object.name = params.name
object.matrix = matrix
object.rotation.set(...params.rotation)
object.position.set(...params.position)
object.scale.set(...params.scale)
object.quaternion.set(...params.quaternion)
object.up.set(...params.up)
object.userData = params.userData
object.visible = params.visible
parseChildren(object, params.children)
genAnimations(object, params.animations)
}
const parseMesh = (IMeshParams: IMeshParams) => {
const geometry = genGeometry(IMeshParams.geometry)
const material = genMaterial(IMeshParams.material)
const mesh = new THREE.Mesh(geometry, material)
parseBaseParams(IMeshParams, mesh)
return mesh
}
const parseLineSegments = (ILineSegments: IMeshParams) => {
const geometry = genLineSegmentsGeometry(ILineSegments.geometry)
const material = genMaterial(ILineSegments.material)
const line = new THREE.LineSegments(geometry, material)
parseBaseParams(ILineSegments, line)
return line
}
const parseGroup = (params: IGroupParams) => {
const group = new THREE.Group()
parseBaseParams(params, group)
return group
}
const parsePointLight = (params: IPointLight) => {
const color = new THREE.Color()
// 色彩空间
// export type ColorSpace = NoColorSpace | SRGBColorSpace | LinearSRGBColorSpace
// export type NoColorSpace = ''
// export type SRGBColorSpace = 'srgb'
// export type LinearSRGBColorSpace = 'srgb-linear'
//glb模型为了亮度恢复 使用srgb格式 所以颜色也使用同样格式 使其颜色模式一致
color.setRGB(params.color.r, params.color.g, params.color.b, 'srgb-linear')
const pointLight = new THREE.PointLight(color, params.intensity, params.distance, params.decay)
parseBaseParams(params, pointLight)
return pointLight
}
const parseObject3D = (params: IBaseProps) => {
const object = new THREE.Object3D()
parseBaseParams(params, object)
return object
}
const parseChildren = (object3D: THREE.Object3D, children: IBaseProps[]) => {
if (!children.length) return
const objectList: THREE.Object3D[] = []
for (const child of children) {
const { type } = child
if (type === 'Mesh') {
objectList.push(parseMesh(child as IMeshParams))
} else if (type === 'LineSegments') {
objectList.push(parseLineSegments(child as IMeshParams))
} else if (type === 'Group') {
objectList.push(parseGroup(child))
} else if (type === 'PointLight') {
objectList.push(parsePointLight(child as IPointLight))
} else if (type === 'Object3D') {
objectList.push(parseObject3D(child))
} else {
throw new Error('出现了未处理的类型:' + type)
}
}
object3D.add(...objectList)
}
/**
* 生成动画
*/
const genAnimations = (object3D: THREE.Object3D, sceneAnimations: IGroupParams['sceneAnimations']) => {
if (!sceneAnimations) return
const animations: THREE.AnimationClip[] = []
for (const animation of sceneAnimations!) {
const clip = new THREE.AnimationClip(animation.name, animation.duration, [], animation.blendMode)
for (const { name, times, values } of animation.tracks) {
const nreTrack = new THREE.QuaternionKeyframeTrack(name, times as any, values as any)
clip.tracks.push(nreTrack)
}
animations.push(clip)
}
object3D.animations = animations
}
/**
* 解析传入的模型参数生成有效的three.js物体
*/
export const parseModel = (params: IGroupParams) => {
const model = parseGroup(params)
// model.position.x += 10
genAnimations(model, params.sceneAnimations)
// console.log('解析完:', model)
return model
}
渲染优化
1.drawcall
draw call 是指向图形处理器发出的绘制命令。每次调用 draw call,都会向 GPU 发送一组顶点和一组纹理,然后 GPU 根据这些数据绘制出一个三维物体。在绘制复杂的场景时,需要发出大量的 draw call,这可能会对性能产生负面影响
减少材质的数量:每个材质都需要单独的 draw call,因此减少材质的数量可以减少 draw call 的数量。
使用纹理图集:将多个纹理合并成一个纹理图集可以减少 draw call 的数量。
合并网格:如果多个网格具有相同的材质和纹理,则可以将它们合并成一个网格,以减少 draw call 的数量。
减少灯光的数量:每个灯光都需要单独的 draw call,因此减少灯光的数量可以减少 draw call 的数量。
减少透明度物体的数量:透明度物体需要进行混合和排序,这会导致更多的 draw call。因此,减少透明度物体的数量可以减少 draw call 的数量。
使用 LOD(层级细节):使用 LOD 可以根据距离自动调整物体的细节级别。这可以减少细节不必要的物体的 draw call 数量。
使用批处理:批处理是将多个物体合并为一个 draw call 的技术。这可以大大减少 draw call 的数量,提高性能。
使用 GPU 实例化:GPU 实例化是在 GPU 上绘制多个相同的物体的一种技术,可以通过单个 draw call 来绘制多个实例,从而减少 draw call 的数量。
2.帧数优化
帧数(FPS)是衡量性能的重要指标。通常,较高的帧数意味着更流畅的体验和更好的游戏感觉。
内存优化
减少场景中的多边形数量:减少场景中的多边形数量可以降低内存使用量和渲染时间。可以使用多边形化工具来将复杂的几何体转换为简单的几何体,从而减少多边形数量。
使用纹理合集:使用纹理合集可以将多个纹理合并为一个,从而减少内存使用量。这可以通过使用纹理集来实现。
优化纹理大小:使用适当大小的纹理可以减少内存使用量。如果纹理大小过大,可以使用压缩技术将其压缩为较小的文件。
释放不需要的资源:在场景中使用的资源可以随着时间释放。可以根据需要在场景中加载和卸载资源,以避免浪费内存。
优化脚本:如果脚本在场景中消耗大量内存,则可以优化它们以减少内存使用量。可以尝试使用更少的变量、更少的递归和更好的内存管理技术。
基于 LOD 级别减少模型的细节:当模型距离相机越远时,可以逐步减少其细节级别,从而减少内存使用量。
js
// 创建场景、摄像机和渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 创建LOD对象
const lod = new THREE.LOD();
// 创建两个不同级别的模型
const highDetailModel = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshBasicMaterial({ color: 0xff0000 })
);
const lowDetailModel = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshBasicMaterial({ color: 0x00ff00 })
);
// 设置LOD级别和距离阈值
lod.addLevel(highDetailModel, 0); // 添加高细节模型,0表示最低阈值
lod.addLevel(lowDetailModel, 500); // 添加低细节模型,500表示高阈值
// 将LOD对象添加到场景中
scene.add(lod);
// 设置摄像机位置
camera.position.z = 5;
// 创建一个简单的渲染循环
const animate = function () {
requestAnimationFrame(animate);
// 更新LOD对象的距离
lod.update(camera);
// 渲染场景
renderer.render(scene, camera);
};
animate();
使用模型复用:对于多个具有相似几何体的模型,可以使用模型复用,这样只需要存储一份几何体数据,可以在多个模型中共享。
禁用不必要的特性:如果某些特性不必要,则可以禁用它们以减少内存使用量。例如,可以禁用阴影、反射和抗锯齿等效果。
1.内存释放
js
onBeforeUnmount(() => {
try {
scene.clear()
renderer.dispose()
group.remove(buildingModel)
group.remove(buildingModelWire)
scene.remove(group)
scene.remove.apply(scene, scene.children)
renderer.forceContextLoss()
renderer.content = null
cancelAnimationFrame(animationID.value)
const gl = renderer.domElement.getContext('Model')
gl && gl.getExtension('WEBGL_lose_context').loseContext()
} catch (e) {
console.log(e)
}
})