three.js 笔记六 InstancedMesh InstancedBufferGeometry

游戏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和内存都很低

1.构造函数

InstancedMesh( geometry : BufferGeometry, material : Material, count : Integer )
要创建一个InstancedMesh,需要三个参数,几何体(BufferGeometry类型),材质(Material类型)和要创建的总数(Integer 类型)

2.属性
  • count : Integer 实例的数量。被传入到构造函数中的count表示mesh实例数量的最大值。 可以在运行时改变这个数值到 [0, count] 区间的一个整数
  • instanceColor : InstancedBufferAttribute 代表所有实例的颜色。默认情况下null。 如果通过.setColorAt()修改实例化数据,则必须将它的needsUpdate标志设置为 true
  • instanceMatrix : InstancedBufferAttribute 表示所有实例的本地变换。 如果你要通过 .setMatrixAt() 来修改实例数据,你必须将它的 needsUpdate 标识为 true
  • isInstancedMesh : Boolean 只读属性,判断一个对象是否是InstancedMesh类型
3.方法
  • dispose () 释放实例的内部资源
  • getColorAt ( index : Integer, color : Color ) 获取实例的颜色,它有两个参数,
    index:实例索引,取值范围为0~count
    color:已定义的颜色对象
  • getMatrixAt ( index : Integer, matrix : Matrix4 )
    获得已定义实例的本地变换矩阵,它有两个参数
    index: 实例的索引。值必须在 [0, count] 区间
    matrix: 该4x4矩阵将会被设为已定义实例的本地变换矩阵
  • setColorAt ( index : Integer, color : Color )
    将给定的颜色设置为定义的实例,它包含两个参数
    index:实例索引,取值范围为0~count
    color:单个实例的颜色
    这里需要注意 确保在使用setColorAt 更新所有颜色后将.instanceColor.needsUpdate设置为true
  • setMatrixAt ( index : Integer, matrix : Matrix4 )
    设置给定的本地变换矩阵到已定义的实例,需要两个参数
    index: 实例的索引。值必须在 [0, count] 区间
    matrix: 一个4x4矩阵,表示单个实例本地变换
    这里需要注意 确保在使用setMatrixAt 更新所有矩阵后将 .instanceMatrix.needsUpdate 设置为true
三、点击进行单个控制

参考
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 ) );
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容