美文网首页
three.js 笔记六 InstancedMesh Inst

three.js 笔记六 InstancedMesh Inst

作者: 合肥黑 | 来源:发表于2023-01-04 09:02 被阅读0次

    游戏DrawCall参考UnityShader精要笔记四 渲染流水线
    游戏合批参考Cocos 3.x 由浅到浅入门批量渲染

    THREEJS参考官方文档
    https://threejs.org/docs/index.html#api/zh/objects/InstancedMesh
    https://threejs.org/examples/webgl_instancing_performance.html

    参考
    Threejs性能优化:Instance实例化几何体 和 Merge合并几何体

    当我们有大量的相同的几何体形状和相同的材质时,比如我有一千个立方几何体要渲染,他们的材质时相同的,但是坐标、大小矩阵变换这些不相同。如果按照常规的一个个Mesh的渲染,要生成一千个geometry,一千个material,一千个Mesh,占用太多内存和性能。

    我们可以使用合并几何体的方式,但这样合并后变为一个个体 ,失去了对单个小模型的控制。three.js还提供了InstanceMesh实例化模型可以实现。

    image.png
    一、静态合批

    官方示例相关代码如下

    const geometries = [];
    const matrix = new THREE.Matrix4();
    
    for ( let i = 0; i < api.count; i ++ ) {
    
        randomizeMatrix( matrix );
    
        const instanceGeometry = geometry.clone();
        instanceGeometry.applyMatrix4( matrix );
    
        geometries.push( instanceGeometry );
    
    }
    
    const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries( geometries );
    
    scene.add( new THREE.Mesh( mergedGeometry, material ) );
    
    
    

    从Native切换过来,可以看到DrawCall变成1,但是占用内存暴增。

    二、实例化渲染InstancedMesh

    官方示例相关代码如下

    const matrix = new THREE.Matrix4();
    const mesh = new THREE.InstancedMesh( geometry, material, api.count );
    
    for ( let i = 0; i < api.count; i ++ ) {
    
        randomizeMatrix( matrix );
        mesh.setMatrixAt( i, matrix );
    
    }
    
    scene.add( mesh );
    
    const randomizeMatrix = function () {
    
        const position = new THREE.Vector3();
        const rotation = new THREE.Euler();
        const quaternion = new THREE.Quaternion();
        const scale = new THREE.Vector3();
    
        return function ( matrix ) {
    
            position.x = Math.random() * 40 - 20;
            position.y = Math.random() * 40 - 20;
            position.z = Math.random() * 40 - 20;
    
            rotation.x = Math.random() * 2 * Math.PI;
            rotation.y = Math.random() * 2 * Math.PI;
            rotation.z = Math.random() * 2 * Math.PI;
    
            quaternion.setFromEuler( rotation );
    
            scale.x = scale.y = scale.z = Math.random() * 1;
    
            matrix.compose( position, quaternion, scale );
    
        };
    
    }();
    

    效果很好,DrawCall和内存都很低

    构造方法如下:

        constructor(
            geometry: TGeometry,
            material: TMaterial,
            count: number
        );
    

    使用setMatrixAt即可控制位置

    三、点击进行单个控制

    参考
    three.js 性能优化 实例化网格模型InstancedMesh

    //this.clickObjects是我存放可点击模型的数组
    var intersects = this.raycaster.intersectObjects(this.clickObjects);
    if (intersects.length) {
        var mesh = intersects[0].object;//这里就是需要操作的网格模型了
        var instanceId = intersects[0].instanceId;//这里的instanceId就是该实例的索引,对应我们之前初始化时的index
        //判断点击得到的是不是isInstancedMesh
        if (mesh.isInstancedMesh && instanceId>= 0) {
            //如果要更改颜色
            mesh.setColorAt(instanceId, 0x424242);
            mesh.instanceColor.needsUpdate = true;
            //如果要更改矩阵,matrix是要改成的矩阵,可以参考初始化时的那样得到矩阵
            mesh.setMatrixAt(instanceId, matrix);
            mesh.instanceMatrix.needsUpdate = true;
        }
    }
    
    四、案例视频中的boxes.instanceMatrix.setUsage(THREE.DynamicDrawUsage)

    搜索一下:

    new THREE.Float32BufferAttribute(positions, 3).setUsage(THREE.StreamCopyUsage)
    

    相应的d.ts文件,setUsage有如下类型:

    // usage types
    export enum Usage {}
    export const StaticDrawUsage: Usage;
    export const DynamicDrawUsage: Usage;
    export const StreamDrawUsage: Usage;
    export const StaticReadUsage: Usage;
    export const DynamicReadUsage: Usage;
    export const StreamReadUsage: Usage;
    export const StaticCopyUsage: Usage;
    export const DynamicCopyUsage: Usage;
    export const StreamCopyUsage: Usage;
    

    WebGL编程指南笔记二 第三章第四章 绘制和变换中,有如下介绍:

    usage:表示程序将如何使用存储在缓冲区对象中的数据。该参数将帮助WebGL优化操作,但是就算你传入了错误的值,也不会终止程序(仅仅是降低程序的效率)

    • gl.STATIC_DRAW 只会向缓冲区对象中写入一次数据,但需要绘制很多次(many times)
    • gl.STREAM_DRAW 只会向缓冲区对象中写入一次数据,然后绘制若干次(at most a few times)
    • gl.DYNAMIC_DRAW 会向缓冲区对象中多次写入数据,并绘制很多次(many times)
    五、InstancedBufferGeometry

    参考
    https://threejs.org/examples/?q=instanc#webgl_buffergeometry_instancing
    https://www.bilibili.com/video/BV1pW4y177yt P62

    1.在官方示例中,使用方式如下:
    const geometry = new THREE.InstancedBufferGeometry();
    
    // set so its initalized for dat.GUI, will be set in first draw otherwise
    geometry.instanceCount = instances; 
    
    geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
    
    geometry.setAttribute( 'offset', 
    new THREE.InstancedBufferAttribute( new Float32Array( offsets ), 3 ) );
    
    geometry.setAttribute( 'color', 
    new THREE.InstancedBufferAttribute( new Float32Array( colors ), 4 ) );
    
    geometry.setAttribute( 'orientationStart', 
    new THREE.InstancedBufferAttribute( new Float32Array( orientationsStart ), 4 ) );
    
    geometry.setAttribute( 'orientationEnd', 
    new THREE.InstancedBufferAttribute( new Float32Array( orientationsEnd ), 4 ) );
    
    const material = new THREE.RawShaderMaterial( {
    
        uniforms: {
            'time': { value: 1.0 },
            'sineTime': { value: 1.0 }
        },
        vertexShader: document.getElementById( 'vertexShader' ).textContent,
        fragmentShader: document.getElementById( 'fragmentShader' ).textContent,
        side: THREE.DoubleSide,
        transparent: true
    
    } );
    
    //
    
    const mesh = new THREE.Mesh( geometry, material );
    scene.add( mesh );
    
    2.回顾一下普通的BufferGeometry:
    function drawPlaneByBufferGeometryUV() {
      //创建BufferGeometry实例
      const bufferGeom = new THREE.BufferGeometry();
    
      //初始化存放顶点信息的序列化数组
      const positions = new Float32Array([
        -5.0, 3.0, 0.0, //point0
        5.0, 3.0, 0.0, //point1
        6.0, -3.0, 0.0, //point2
        -6.0, -3.0, 0.0, //point3
    
      ]);
    
      //设置顶点信息
      bufferGeom.setAttribute('position', new THREE.BufferAttribute(positions, 3));
    
      //初始化存放颜色信息的序列化数组
      const colors = new Float32Array([
        0.5, 0.3, 0.6,
        0.5, 0.3, 0.6,
        0.5, 0.3, 0.6,
        0.5, 0.3, 0.6,
    
      ]);
      //设置颜色信息
      bufferGeom.setAttribute('color', new THREE.BufferAttribute(colors, 3));
    
    
      const indexs = new Uint16Array([
        0, 1, 2,
        0, 2, 3,
        4, 5, 6,
        4, 6, 7
      ]);
    
    
      //设置画面的索引
      bufferGeom.index = new THREE.BufferAttribute(indexs, 1);
    
      const uvs = new Uint16Array([
        0, 1,
        1, 1,
        1, 0,
        0, 0,
    
      ]);
      //设置UV
      bufferGeom.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
    
      const planetTexture = new THREE.TextureLoader().load("../assets/textures/test.png");
    
      //创建材质
      const material = new THREE.MeshBasicMaterial({
        map: planetTexture,
        vertexColors: THREE.VertexColors, //使用缓存中的颜色
        side: THREE.DoubleSide
      });
    
      const mesh = new THREE.Mesh(bufferGeom, material);
      scene.add(mesh);
    }
    
    3.对比

    对比继承关系,可以看出使用API区别不大:

    InstancedBufferGeometry extends BufferGeometry
    InstancedBufferAttribute extends BufferAttribute
    

    最大的区别在于,RawShaderMaterial对传入的attribute进行控制

        <script id="vertexShader" type="x-shader/x-vertex">
            precision highp float;
    
            uniform float sineTime;
    
            uniform mat4 modelViewMatrix;
            uniform mat4 projectionMatrix;
    
            attribute vec3 position;
            attribute vec3 offset;
            attribute vec4 color;
            attribute vec4 orientationStart;
            attribute vec4 orientationEnd;
                    ...
    
    4. What is the difference between InstancedBufferGeometry and InstancedMesh in threeJS?

    机翻版本在这里:
    https://www.coder.work/article/7602085

    节选部分内容如下:

    If some or all of your instances will change often, and if you have fewer than ~50K instances, and the differences between the instances can be described by a TRS transform (or color), then InstancedMesh should be a good fit for you.

    如果您的部分或全部实例会经常更改,并且您的实例少于 ~50K,并且实例之间的差异可以通过 TRS 变换(或颜色)来描述,那么 InstancedMesh 应该非常适合您。

    注:TRS 变换即transform,rotate,sacle变换,对应的API是setMatrixAt;颜色变换对应setColorAt

    If most of your instances are going to be static and you'll only update a few at a time, then your number of instances could be potentially unlimited (until you reach GPU rendering bottlenecks or VRAM size).

    如果您的大多数实例都是静态的,并且您一次只能更新几个,那么您的实例数量可能是无限的(直到您达到 GPU 渲染瓶颈或 VRAM 大小)。

    If you really need to transform each instance each frame, you may want to consider if you can offload your transformation computations to the GPU itself, the standard way to do that is with vertex shaders. The amount of additional computation that can be done with vertex shaders compared to one cpu thread is staggering.

    如果您确实需要在每一帧中转换每个实例,您可能需要考虑是否可以将转换计算卸载到 GPU 本身,执行此操作的标准方法是使用顶点着色器。与一个 cpu 线程相比,使用顶点着色器可以完成的额外计算量是 惊人的 .

    So when either the volume of data involved with the transform for instancing is too much or when the computational overhead of manipulating that data is too much, you'll have to fall back to the more low-level InstancedBufferGeometry approach and get down and dirty with the shaders.

    This is the other thing about InstancedMesh. It allows you to avoid touching shaders.

    因此,当与用于实例化的转换相关的数据量过多或操作该数据的计算开销过多时,您将不得不回退到更底层的 InstancedBufferGeometry 方法并开始使用着色器。
    这是关于 InstancedMesh 的另一件事。它允许您避免接触着色器。

    六、InterleavedBuffer

    参考
    Difference and uses of InstancedMesh and InterleavedBuffer in ThreeJS

    InterleavedBuffer provides the possibility to manage your vertex data in an interleaved fashion. The motivation of doing this is to improve the amount of cache hits on the GPU. If you are more interested in the theory behind this approach, I suggest you google "structure of arrays vs. array of structures". The latter one applies to InterleavedBuffer.

    InterleavedBuffer提供了以交错方式管理顶点数据的可能性。这样做的动机是提高GPU上的缓存命中量。如果你对这种方法背后的理论更感兴趣,我建议你谷歌一下"structure of array vs. of structures“。

    In general, the performance benefits of both techniques depends on the specific use case. According to my personal experiences, the benefits of interleaved buffers is hard to measure since the performance improvements depend on the respective GPU. In many cases, I've seen no difference in FPS when using interleaved buffers. However, it's much more easier to see a performance improvement if the amount of draw calls is high and you lower it by using instanced rendering.

    three.js provides examples for both techniques. webgl_buffergeometry_instancing_interleaved demonstrates a combination.

    通常,这两种技术的性能优势取决于特定的用例。根据我的个人经验,交叉缓冲的好处很难衡量,因为性能的提高取决于各自的GPU。在许多情况下,在使用交错缓冲区时,我没有看到FPS有什么不同。但是,如果绘制调用量很高,而您使用实例化渲染来降低调用量,则更容易看到性能的提高。

    1.采用 array of structure (AOS) 还是 structure of arrays (SOA)

    参考
    优化数据排布,让你的程序加速 4 倍!

    我的程序中需要用到结构体的一维数组。 比如说,我有一个粒子系统,每个粒子有 x, y, z, w 四个属性。我应该采用 array of structure (AOS) 还是 structure of arrays (SOA) 的方式性能会更高?

    具体来看,在 C++ 中,我应该用

    // Array of structures (AOS)
    struct Particle {float x, y, z, w};
    Particle particles[1000];
    

    还是

    // Structure of arrays (SOA)
    struct Particles {
        float x[1000];
        float y[1000];
        float z[1000];
        float w[1000];
    };
    
    2.webgl_buffergeometry_points_interleaved.html

    https://threejs.org/examples/?q=interleaved#webgl_buffergeometry_points_interleaved

    将position 以及color 存放在一个ArrayBuffer 中,通过 THREE.InterleavedBufferAttribute 设置偏移等属性,读取对应的字段。

    export class InterleavedBufferAttribute {
    
        constructor(
            interleavedBuffer: InterleavedBuffer,
            itemSize: number,
            offset: number,
            normalized?: boolean
        );
    

    示例使用方式:

    const interleavedBuffer32 = new THREE.InterleavedBuffer( interleavedFloat32Buffer, 4 );
    const interleavedBuffer8 = new THREE.InterleavedBuffer( interleavedUint8Buffer, 16 );
    
    geometry.setAttribute( 'position', new THREE.InterleavedBufferAttribute( interleavedBuffer32, 3, 0, false ) );
    geometry.setAttribute( 'color', new THREE.InterleavedBufferAttribute( interleavedBuffer8, 3, 12, true ) );
    

    相关文章

      网友评论

          本文标题:three.js 笔记六 InstancedMesh Inst

          本文链接:https://www.haomeiwen.com/subject/nrfvfdtx.html