three.js学习笔记(二十)——性能优化提示

初设

import './style.css'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'

/**
 * Base
 */
// Canvas
const canvas = document.querySelector('canvas.webgl')

// Scene
const scene = new THREE.Scene()

/**
 * Textures
 */
const textureLoader = new THREE.TextureLoader()
const displacementTexture = textureLoader.load('/textures/displacementMap.png')

/**
 * Sizes
 */
const sizes = {
    width: window.innerWidth,
    height: window.innerHeight
}

window.addEventListener('resize', () =>
{
    // Update sizes
    sizes.width = window.innerWidth
    sizes.height = window.innerHeight

    // Update camera
    camera.aspect = sizes.width / sizes.height
    camera.updateProjectionMatrix()

    // Update renderer
    renderer.setSize(sizes.width, sizes.height)
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})

/**
 * Camera
 */
// Base camera
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100)
camera.position.set(2, 2, 6)
scene.add(camera)

// Controls
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true

/**
 * Renderer
 */
const renderer = new THREE.WebGLRenderer({
    canvas: canvas,
    powerPreference: 'high-performance',
    antialias: true
})
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(window.devicePixelRatio)

/**
 * Test meshes
 */
const cube = new THREE.Mesh(
    new THREE.BoxBufferGeometry(2, 2, 2),
    new THREE.MeshStandardMaterial()
)
cube.castShadow = true
cube.receiveShadow = true
cube.position.set(- 5, 0, 0)
scene.add(cube)

const torusKnot = new THREE.Mesh(
    new THREE.TorusKnotBufferGeometry(1, 0.4, 128, 32),
    new THREE.MeshStandardMaterial()
)
torusKnot.castShadow = true
torusKnot.receiveShadow = true
scene.add(torusKnot)

const sphere = new THREE.Mesh(
    new THREE.SphereBufferGeometry(1, 32, 32),
    new THREE.MeshStandardMaterial()
)
sphere.position.set(5, 0, 0)
sphere.castShadow = true
sphere.receiveShadow = true
scene.add(sphere)

const floor = new THREE.Mesh(
    new THREE.PlaneBufferGeometry(10, 10),
    new THREE.MeshStandardMaterial()
)
floor.position.set(0, - 2, 0)
floor.rotation.x = - Math.PI * 0.5
floor.castShadow = true
floor.receiveShadow = true
scene.add(floor)

/**
 * Lights
 */
const directionalLight = new THREE.DirectionalLight('#ffffff', 1)
directionalLight.castShadow = true
directionalLight.shadow.mapSize.set(1024, 1024)
directionalLight.shadow.camera.far = 15
directionalLight.shadow.normalBias = 0.05
directionalLight.position.set(0.25, 3, 2.25)
scene.add(directionalLight)

/**
 * Animate
 */
const clock = new THREE.Clock()

const tick = () =>
{
 
    const elapsedTime = clock.getElapsedTime()

    // Update test mesh
    torusKnot.rotation.y = elapsedTime * 0.1

    // Update controls
    controls.update()

    // Render
    renderer.render(scene, camera)

    // Call tick again on the next frame
    window.requestAnimationFrame(tick)

}

tick()

监视

我们需要直观看到页面运行性能,而不能单单仅靠我们的眼睛去观察。

1-监视FPS

安装JavaScript性能监视器stats.js

npm install --save stats.js

引入并实例化:

import Stats from 'stats.js'

const stats = new Stats()
stats.showPanel(0) // 显示面板 0: fps, 1: ms, 2: mb, 3+: custom
document.body.appendChild(stats.dom)

在动画函数中调用begin()end()方法:

const tick = () =>
{
    stats.begin()

    // ...

    stats.end()
}

three.js学习笔记(二十)——性能优化提示_第1张图片

2-禁用浏览器FPS限制

3-监控Draw-calls

Draw-calls网格描绘调用是GPU绘制三角形的指令。当我们场景中的对象越复杂,Draw-calls次数也越多。

通常来讲,Draw-call次数越少,性能越好,因此我们希望对Draw-call进行监控。

有一个Chrome扩展叫Spector.js可以帮助我们做到这点。

4-渲染器信息

渲染器可以提供有关场景中的内容和正在绘制的内容的一些信息:

console.log(renderer.info)

一般性原则

5-良好的js代码

这一点不用多讲

6-清除不必要的东西

当你的场景中不再需要某个对象,就要将其废置掉。好比你开发一个带有不同场景的关卡游戏,当你进入下一关,便要将上一关场景中的对象给清除废置。
Three.js官网有对应文档专门讲这一问题:
How to dispose of objects
举个例子:

scene.remove(cube)
cube.geometry.dispose()
cube.material.dispose()

灯光Lights

7-避免使用

尽可能避免使用Three.js中的灯光,虽然它们简单易上手,但同时也可以轻易显著降低性能。
如果实在需要使用,则越少越好,并且尽可能使用最廉价的灯光像是环境光AmbientLight和平行光DirectionalLight

8-避免添加和移除灯光

在场景中添加或移除灯光时,必须重新编译所有支持灯光的材质。如果你的场景很复杂,这个行为可以直接让你屏幕卡死。

阴影Shadows

9-避免使用

道理同第七点,影响运行性能。尽可能使用烘焙阴影等替代方案,例如在贴图纹理中加入烘焙阴影。

10-优化阴影贴图

如果实在得使用阴影的话,则尽可能去优化阴影贴图。
使用相机助手CameraHelper查看将由阴影贴图相机渲染的区域,并将其尽可能缩小到最小范围。

directionalLight.shadow.camera.top = 3
directionalLight.shadow.camera.right = 6
directionalLight.shadow.camera.left = - 6
directionalLight.shadow.camera.bottom = - 3
directionalLight.shadow.camera.far = 10

const cameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera)
scene.add(cameraHelper)

同时尽量使用低分辨率贴图尺寸:

directionalLight.shadow.mapSize.set(1024, 1024)

three.js学习笔记(二十)——性能优化提示_第2张图片

11-明智使用castShadow和receiveShadow

有些对象可以投射阴影,有些对象可以接收阴影,有些可能两者兼有。尝试在尽可能少的对象上激活castShadow和receiveShadow:

cube.castShadow = true
cube.receiveShadow = false

torusKnot.castShadow = true
torusKnot.receiveShadow = false

sphere.castShadow = true
sphere.receiveShadow = false

floor.castShadow = false
floor.receiveShadow = true

12-停用阴影自动更新

现在我们的场景中,阴影贴图在每次渲染之前都会更新。
我们可以将shadowMap的autoUpdate属性设为false以此来停用阴影自动更新。
同时告诉Three.js只有必要时候才进行阴影贴图更新。
needsUpdate属性被设为true, 场景中的阴影贴图会在下次render调用时刷新:

renderer.shadowMap.autoUpdate = false
renderer.shadowMap.needsUpdate = true

可以看到阴影不再旋转了。

纹理贴图Textures

13-调整尺寸

纹理贴图非常吃GPU内存,mipmap甚至更糟糕。
纹理贴图文件的大小于此无关,分辨率才是相关的,因此尽可能降低分辨率。

14-保持分辨率为2的幂次方

当调整贴图纹理尺寸时,尽可能为2的幂次方,这对于mipmap非常重要。
如果不这样做,渲染时候进行mipmap时Three.js会尝试通过将图像调整到最接近2的幂次方的分辨率来修复此问题,但此过程将占用资源,并可能导致质量较差的纹理贴图效果。

15-使用正确格式

虽然说过文件格式不会改变GPU的内存使用情况,但使用正确的文件格式可以有效减少加载时间。
可以根据图像和压缩程度以及alpha通道,选择使用.jpg.png
可以使用TinyPNG等在线工具来进一步减轻文件大小。同时也可以尝试特殊格式,如basis

Basis是一种类似.jpg和.png的格式,但压缩功能更强,GPU更容易读取该格式。我们不会介绍它,因为它很难生成,但如果你愿意的话也可以尝试一下。
可以在下面这个链接找到创建.basis文件的信息和工具:https://github.com/BinomialLLC/basis_universal

几何体Geometries

16-使用缓冲几何体

始终使用缓冲区几何图形 buffer geometries而不是经典几何图形。使用 BufferGeometry 可以有效减少向 GPU 传输上述数据所需的开销。

17-不要去更新顶点

更新几何体的顶点会影响性能。创建几何图形时可以执行一次,但避免在动画函数中执行。
如果需要为顶点设置动画,请使用顶点着色器。

18-几何体同质化

如果你有非常多网格使用了相同的几何体图形,那就只创建一个geometry,然后在所有网格中去使用它:

const geometry = new THREE.BoxBufferGeometry(0.5, 0.5, 0.5)

for(let i = 0; i < 50; i++)
{
    const material = new THREE.MeshNormalMaterial()

    const mesh = new THREE.Mesh(geometry, material)
    mesh.position.x = (Math.random() - 0.5) * 10
    mesh.position.y = (Math.random() - 0.5) * 10
    mesh.position.z = (Math.random() - 0.5) * 10
    mesh.rotation.y = (Math.random() - 0.5) * Math.PI * 2
    mesh.rotation.z = (Math.random() - 0.5) * Math.PI * 2

    scene.add(mesh)
}

19-合并几何体

如果几何体不需要进行移动等操作,可以使用BufferGeometryUtils合并它们。

import { BufferGeometryUtils } from 'three/examples/jsm/utils/BufferGeometryUtils.js'

不需要实例化,可以直接使用它的方法。mergeBufferGeometries(...)将一组几何体作为参数,以获得一个合并后的几何体。然后,我们可以将该几何体与单个网格一起使用:

const geometries = []
for(let i = 0; i < 50; i++)
{
    const geometry = new THREE.BoxBufferGeometry(0.5, 0.5, 0.5)

    geometry.rotateX((Math.random() - 0.5) * Math.PI * 2)
    geometry.rotateY((Math.random() - 0.5) * Math.PI * 2)

    geometry.translate(
        (Math.random() - 0.5) * 10,
        (Math.random() - 0.5) * 10,
        (Math.random() - 0.5) * 10
    )

    geometries.push(geometry)
}

const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(geometries)
console.log(mergedGeometry)

const material = new THREE.MeshNormalMaterial()

const mesh = new THREE.Mesh(mergedGeometry, material)
scene.add(mesh)

虽然过程复杂了些,但是我们只有一次Draw-call

材质Materials

20-材质同质化

同理,如果有多个网格使用相同的材质,那就只创建一次material,在第18点上进行优化:

const geometry = new THREE.BoxBufferGeometry(0.5, 0.5, 0.5)

const material = new THREE.MeshNormalMaterial()

for(let i = 0; i < 50; i++)
{
    const mesh = new THREE.Mesh(geometry, material)
    mesh.position.x = (Math.random() - 0.5) * 10
    mesh.position.y = (Math.random() - 0.5) * 10
    mesh.position.z = (Math.random() - 0.5) * 10
    mesh.rotation.x = (Math.random() - 0.5) * Math.PI * 2
    mesh.rotation.y = (Math.random() - 0.5) * Math.PI * 2

    scene.add(mesh)
}

21-使用廉价的材质

一些像 MeshStandardMaterialMeshPhysicalMaterial 的材质需要比像MeshBasicMaterialMeshLambertMaterialMeshPhongMaterial的材质消耗更多的资源。

网格Meshes

22-实例化网格InstancedMesh

如果需要独立控制各个网格而无法合并几何体,但它们却使用相同的几何体和材质,这时候可以使用实例化网格InstancedMesh。

它就像是一个网格Mesh,但是你只能创建一个实例化网格InstancedMesh,然后可以为该网格的每个“实例”提供变换矩阵。

矩阵必须是四维矩阵Matrix4,可以使用各种方法来进行变换:

const geometry = new THREE.BoxBufferGeometry(0.5, 0.5, 0.5)

const material = new THREE.MeshNormalMaterial()

const mesh = new THREE.InstancedMesh(geometry, material, 50)
scene.add(mesh)

for(let i = 0; i < 50; i++)
{
    const position = new THREE.Vector3(
        (Math.random() - 0.5) * 10,
        (Math.random() - 0.5) * 10,
        (Math.random() - 0.5) * 10
    )

    const quaternion = new THREE.Quaternion()
    quaternion.setFromEuler(new THREE.Euler((Math.random() - 0.5) * Math.PI * 2, (Math.random() - 0.5) * Math.PI * 2, 0))

    const matrix = new THREE.Matrix4()
    matrix.makeRotationFromQuaternion(quaternion)
    matrix.setPosition(position)

    mesh.setMatrixAt(i, matrix)
}

我们得到的结果几乎和合并几何体一样好,但是我们仍然可以通过改变矩阵来移动网格。

如果要在动画函数中更改这些矩阵,请将下列代码添加到实例化网格InstancedMesh

mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage)

模型Models

23-低多边形

使用低多边形模型,多边形越少,帧速率越好。如果需要更多细节,则尝试使用法线贴图,它们在性能消耗方面很优秀,同时在处理纹理时可以获得出色的细节。

24-Draco压缩

如果模型有很多细节和非常复杂的几何图案,请使用Draco压缩,它可以大大减少文件体积。
缺点在于解压几何体时可能会页面卡住,并且还必须加载Draco库。

25-Gzip

Gzip是发生在服务端的压缩。大多数服务器不支持gzip文件,如.glb、.gltf、.obj等。根据自身的服务器寻找合适方案。

相机Cameras

26-视野范围

当对象不在视野中时,它们将不会被渲染,这叫做视锥体剔除Frustum Culling。
虽然感觉有点low,但是缩小相机的视野,让屏幕中显示的对象越少个,我们要渲染的三角形个数也就越少。

27-近端面和远端面

像相机视野一样,可以减少相机的near近端面属性和far远端面属性。
比如有一个非常广阔的世界,有山有水,那我们可能会看不到远在山后的小房子,将far值降到合适的值,让这些房子甚至不会被渲染。

渲染器Renderer

28-像素比

一些设备有非常高的像素比,但要知道,渲染的像素越多,消耗的性能越巨大,帧率也越差。
因此最好尝试将渲染器的像素比限制为2:

renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

29-配置偏好

一些设备可能能够在不同GPU使用之间切换。我们可以通过指定powerPreference属性来提示用户代理怎样的配置更适用于当前WebGL环境:

const renderer = new THREE.WebGLRenderer({
    canvas: canvas,
    powerPreference: 'high-performance'
})

如果没有性能问题,则将此属性设置为default

30-抗锯齿

只有在有可见的锯齿且不会导致性能问题的时候才去添加抗锯齿。

后期处理Postprocessing

31-限制通道

每个后期处理过程都将使用与渲染器分辨率(包括像素比率)相同的像素进行渲染。
如果分辨率为1920x1080,有4个通道,像素比为2,则需要渲染19202108024=33177600像素。
合理一点,可以的话将其整合为一个通道。

着色器Shaders

32-指定精度

可以通过更改材质中着色器的精度precision属性来强制材质中着色器精度:

const shaderMaterial = new THREE.ShaderMaterial({
    precision: 'lowp',
    // ...
})

设置好后检查是否性能降低或者出现故障。

这对于RawShaderMaterial是无效的,必须自己添加精度。

33-保持代码简单

监控着色器代码的异常是非常困难的,因此尽量拆分写得简单写,不要写过于复杂的语句,避免使用if语句,应该充分使用各种内置函数。

34-使用贴图纹理

虽然使用柏林噪声函数很酷,但它会显著影响性能。有时,最好使用纹理来表示噪波。使用texture2D()比柏林噪声函数要廉价得多,并且可以使用photoshop等工具非常高效地生成这些纹理。

35-使用defines

uniform是很有作用的,因为我们可以设置它们的值并且在动画函数中去调整值,但是uniform有性能成本。如果某个值不会改变,则可以使用defines。

第一种,直接在着色器代码中:

#define uDisplacementStrength 1.5

第二种,在着色器材质ShaderMaterialdefines属性中,它们会自动加到GLSL代码里边:

const shaderMaterial = new THREE.ShaderMaterial({

    // ...

    defines:
    {
        uDisplacementStrength: 1.5
    },

    // ...
}

36-在顶点着色器中进行计算

尽可能在顶点着色器中进行计算,并将结果发送到片元着色器。

你可能感兴趣的:(three.js学习笔记,javascript,前端,Three.js)