• 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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142

    监视

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

    1-监视FPS

    安装JavaScript性能监视器stats.js

    npm install --save stats.js
    
    • 1

    引入并实例化:

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    const tick = () =>
    {
        stats.begin()
    
        // ...
    
        stats.end()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述

    2-禁用浏览器FPS限制

    3-监控Draw-calls

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

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

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

    4-渲染器信息

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

    console.log(renderer.info)
    
    • 1

    一般性原则

    5-良好的js代码

    这一点不用多讲

    6-清除不必要的东西

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

    scene.remove(cube)
    cube.geometry.dispose()
    cube.material.dispose()
    
    • 1
    • 2
    • 3

    灯光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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

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

    在这里插入图片描述

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    12-停用阴影自动更新

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

    renderer.shadowMap.autoUpdate = false
    renderer.shadowMap.needsUpdate = true
    
    • 1
    • 2

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

    纹理贴图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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    19-合并几何体

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

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

    不需要实例化,可以直接使用它的方法。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)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    虽然过程复杂了些,但是我们只有一次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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

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

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

    mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage)
    
    • 1

    模型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))
    
    • 1

    29-配置偏好

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

    const renderer = new THREE.WebGLRenderer({
        canvas: canvas,
        powerPreference: 'high-performance'
    })
    
    • 1
    • 2
    • 3
    • 4

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

    30-抗锯齿

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

    后期处理Postprocessing

    31-限制通道

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

    着色器Shaders

    32-指定精度

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

    const shaderMaterial = new THREE.ShaderMaterial({
        precision: 'lowp',
        // ...
    })
    
    • 1
    • 2
    • 3
    • 4

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

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

    33-保持代码简单

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

    34-使用贴图纹理

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

    35-使用defines

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

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

    #define uDisplacementStrength 1.5
    
    • 1

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

    const shaderMaterial = new THREE.ShaderMaterial({
    
        // ...
    
        defines:
        {
            uDisplacementStrength: 1.5
        },
    
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

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

  • 相关阅读:
    T1099 第n小的质数(信息学一本通C++)
    Vue路由进阶--VueRouter声明式导航
    UNPV2 学习:Posix Message Queues
    【Linux修炼】1.常见指令(上)
    charles + postern 抓包教程
    基于javaweb心理咨询预约管理系统
    分享关于职场心态
    ios自动化-Xcode、WebDriverAgent环境部署
    【Java集合类面试三十】、BlockingQueue中有哪些方法,为什么这样设计?
    buuctf-[网鼎杯 2020 朱雀组]phpweb
  • 原文地址:https://blog.csdn.net/weixin_43990650/article/details/126267031