WebGL2系列之实例数组(Instanced Arrays)
实例化数组
实例化是一种只调用一次渲染函数却能绘制出很多物体的技术,它节省渲染一个物体时从CPU到GPU的通信时间。
实例数组是这样的一个对象,使用它,可以把原来的的uniform变量转换成attribute变量,而且这个attribute变量对应的缓冲区可以被多个对象使用;这样在绘制的时候,可以减少webgl的调用次数。
背景
假设这样的一个场景:你需要绘制很多个形状相同的物体,但是每个物体的颜色、位置却不一样,通常的做法是这样的:
for(var i = 0; i < amount_of_models_to_draw; i++)
{
doSomePreparations(); // bind VAO, bind Textures, set uniforms etc.
gl.drawArrays(gl.TRIANGLES, 0, amount_of_vertices);
}
但是这种做法的一个缺点是:当绘制的对象的数量巨大之后,执行的效率就会变的很慢了;这是因为每一次绘制的时候,都需要调用很多webgl 的很多方法,比如绑定VAO对象,绑定贴图,设置uniform变量,告诉GPU从哪个缓冲区区读取顶点数据,以及从哪里找到顶点属性,所有这些都会是CPU和GPU的资源消耗过多。
实例化
如果能够讲数据一次性发送给GPU,然后告诉WebGL使用一个绘制函数,绘制多个物体,就会更方便。这种技术,便是实例化技术。这种技术的实现思路,就是把原本的uniform变量,比如变换矩阵,变成attribute变量,然后把多个对象的矩阵数据,写在一起,然后创建所有矩阵的VBO对象(顶点缓存区); 创建好缓冲区后,把所有对象的矩阵数据通过bufferData 上传到缓冲区中,这和普通的attribute变量的缓冲区没什么差别。
接下来,就是和普通的VBO差异的部分:该缓冲区可以在多个对象之间共享。每个对象 取该缓冲区的一部分数据,作为attribute变量的值,方法如下:
gl.vertexAttribDivisor(index, divisor)
通过gl.vertexAttribDivisor方法指定缓冲区中的每一个值,用于多少个对象,比如divisor = 1,表示每一个值用于一个对象;如果divisor=2,表示一个值用于两个对象。 index表示的attribute变量的地址。
然后,通过调用如下方法进行绘制:
gl.drawArraysInstanced(mode, first, count, instanceCount);
gl.drawElementsInstanced(mode, count, type, offset, instanceCount);
这两个方法和 gl.drawArrays与gl.drawElements类似,不同的是多了第四个参数 instanceCount,表示一次绘制多少个对象。
通过这个方法,便能实现一次调用绘制多个对象的目标。
案例说明
代码展示
本案例 将一次绘制多个四边形,代码如下:
var count = 3000;
var positions = new Float32Array([
-1/count, 1/count, 0.0,
-1/count, -1/count, 0.0,
1/count, 1/count, 0.0,
1/count, -1/count, 0.0,
]);
var positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(0);
var colors = new Float32Array([
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0,
1.0, 1.0, 1.0,
]);
var colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(1);
var indices = new Uint8Array([
0,1,2,
2,1,3
]);
var indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,indices,gl.STATIC_DRAW); //给缓冲区填充数据
var offsetArray = [];
for(var i = 0;i < count;i ++){
for(var j = 0; j < count; j ++){
var x = ((i + 1) - count/2) / count * 4;
var y = ((j + 1) - count/2) / count * 4;
var z = 0;
offsetArray.push(x,y,z);
}
}
var offsets = new Float32Array(offsetArray)
var offsetBuffer = gl.createBuffer();
var aOffsetLocation = 2;
gl.bindBuffer(gl.ARRAY_BUFFER, offsetBuffer);
gl.bufferData(gl.ARRAY_BUFFER, offsets, gl.STATIC_DRAW);
gl.enableVertexAttribArray(aOffsetLocation);
gl.vertexAttribPointer(aOffsetLocation, 3, gl.FLOAT, false, 12, 0);
gl.vertexAttribDivisor(aOffsetLocation, 1);
// ////////////////
// // DRAW
// ////////////////
gl.clear(gl.COLOR_BUFFER_BIT);// 清空颜色缓冲区
// // 绘制第一个三角形
gl.bindVertexArray(triangleArray);
gl.drawElementsInstanced(gl.TRIANGLES,indices.length,gl.UNSIGNED_BYTE,0,count * count);
定义四边形VBO、IBO数据
首先定义一个变量count,绘制四边形的个数为 count * count,也就是count 列 count行个四边形。 然后一下代码定义四边形的顶点坐标、颜色和索引相关数据,这在WebGL1中多次使用,不在赘述:
var positions = new Float32Array([
-1/count, 1/count, 0.0,
-1/count, -1/count, 0.0,
1/count, 1/count, 0.0,
1/count, -1/count, 0.0,
]);
var positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(0);
var colors = new Float32Array([
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0,
1.0, 1.0, 1.0,
]);
var colorBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(1);
var indices = new Uint8Array([
0,1,2,
2,1,3
]);
var indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,indices,gl.STATIC_DRAW); //给缓冲区填充数据
uniform变量改成attribute变量
接下来,为了把每个四边形分开,我们给每个四边形定义一个偏移量(此处的偏移量可以相当于变换矩阵),在WebGL1中,这个偏移量会以uniform变量的方式定义,但是在实例化的技术下,该偏移量定义为attribute变量, layout(location=2) in vec4 offset:
var vsSource = `#version 300 es
......
layout(location=2) in vec4 offset;
......
void main() {
vColor = color;
gl_Position = position + offset;
}
`;
定义偏移量的数据及VBO
然后定义每个对象的偏移量数据的数组:
for(var i = 0;i < count;i ++){
for(var j = 0; j < count; j ++){
var x = ((i + 1) - count/2) / count * 4 - 2/count;
var y = ((j + 1) - count/2) / count * 4 - 2/count;
var z = 0;
offsetArray.push(x,y,z);
}
}
这个偏移量,将会使所有的四边形,按照count 行 count 列排列。
定义了偏移量数组之后,创建相应的缓冲区和开启attribute变量:
var offsetBuffer = gl.createBuffer();
var aOffsetLocation = 2; // 偏移量attribute变量地址
gl.bindBuffer(gl.ARRAY_BUFFER, offsetBuffer);
gl.bufferData(gl.ARRAY_BUFFER, offsets, gl.STATIC_DRAW);
gl.enableVertexAttribArray(aOffsetLocation); // 启用偏移量attribute变量从缓冲区取数据
gl.vertexAttribPointer(aOffsetLocation, 3, gl.FLOAT, false, 12, 0); // 定义每个数据的长度为3个分量,长度为12 = 3 * 4(浮点数长度)。
gl.vertexAttribDivisor(aOffsetLocation, 1);
gl.vertexAttribDivisor
注意 gl.vertexAttribDivisor(aOffsetLocation, 1); 这一行,1表示指定每个数据(定义每个数据的长度为3个分量,长度为12 = 3 * 4(浮点数长度)) 被一个四边形所用,而每一个四边形的绘制期间,attribute变量offset保持不变,这个uniform变量类似。
gl.drawElementsInstanced 绘制多个实例
接下来,调用方法绘制多个实例,
// ////////////////
// // DRAW
// ////////////////
gl.clear(gl.COLOR_BUFFER_BIT);// 清空颜色缓冲区
// // 绘制第一个三角形
gl.bindVertexArray(triangleArray);
gl.drawElementsInstanced(gl.TRIANGLES,indices.length,gl.UNSIGNED_BYTE,0,count * count);
gl.drawElementsInstanced 将会绘制count * count个四边形的实例,需要注意的是,绘制实例的个数,不能多于attribute变量offset变量的对应的缓冲区的数据个数,前面代码offsetArray定义了count*count个数据(注意每个数据有3个分量,所以数据个数不等于offsetArray数组长度),因此绘制的示例个数不能超过count * count 个,但是可以少于。
案例效果说明
如果把count 指定为10,最终绘制的效果如下:
可以看出,一次绘制调用,绘制出了100个对象;
如果通过WebGL1的方式需要遍历100次绘制。因此可以看出减少了绘制的遍历。
当然如果只是绘制100个四边形,遍历方法也没什么不好,实例化的威力主要体现在,当数据量变到很大的时候,比如在笔者电脑上,把count值改为4000,那么会绘制4000 * 4000 = 一千六百万个四边形,如下:
可以看出,还是可以很好的绘制出来(虽然由于对象太多,已经看不清楚界限)
而采用WebGL1 循环遍历的方式,估计最多也就能够达到万级别的绘制循环数量,千万级别的数量简直不可想象。
当然这个数量 也是有限制的,比如在笔者的机器上,把count改成5000,也就是5000 * 5000 = 两千五百万的时候,机器就奔溃了。
WebGL1 扩展
在WebGL1中,可以通过扩展来ANGLE_instanced_arrays来实现,相关函数如下:
var ext = gl.getExtension('ANGLE_instanced_arrays');
ext.vertexAttribDivisorANGLE(index, divisor);
ext.drawArraysInstancedANGLE(mode, first, count, primcount);
ext.drawElementsInstancedANGLE(mode, count, type, offset, primcount);
更多精彩内容,请关注公众号:ITman彪叔
WebGL2系列之实例数组(Instanced Arrays)的更多相关文章
- WebGL2系列之顶点数组对象
使用了顶点缓冲技术后,绘制效率有了较大的提升.但是还有一点不尽如人意,那就是顶点的位置坐标.法向量.纹理坐标等不同方面的数据每次使用时需要单独指定,重复了一些不必要的工作.WebGL2提供了一种专门用 ...
- Java-Runoob-高级教程-实例-数组:16. Java 实例 - 数组并集
ylbtech-Java-Runoob-高级教程-实例-数组:16. Java 实例 - 数组并集 1.返回顶部 1. Java 实例 - 数组并集 Java 实例 以下实例演示了如何使用 unio ...
- ylbtech-Java-Runoob-高级教程-实例-数组:15. Java 实例 – 判断数组是否相等
ylbtech-Java-Runoob-高级教程-实例-数组:15. Java 实例 – 判断数组是否相等 1.返回顶部 1. Java 实例 - 判断数组是否相等 Java 实例 以下实例演示了如 ...
- Java-Runoob-高级教程-实例-数组:08. Java 实例 – 数组填充
ylbtech-Java-Runoob-高级教程-实例-数组:08. Java 实例 – 数组填充 1.返回顶部 1. Java 实例 - 数组填充 Java 实例 以下实例我们通过 Java Ut ...
- Java-Runoob-高级教程-实例-数组:07. Java 实例 – 数组合并
ylbtech-Java-Runoob-高级教程-实例-数组:07. Java 实例 – 数组合并 1.返回顶部 1. Java 实例 - 数组合并 Java 实例 以下实例演示了如何通过 List ...
- Java-Runoob-高级教程-实例-数组:06. Java 实例 – 数组获取最大和最小值
ylbtech-Java-Runoob-高级教程-实例-数组:06. Java 实例 – 数组获取最大和最小值 1.返回顶部 1. Java 实例 - 数组获取最大和最小值 Java 实例 以下实例 ...
- Java-Runoob-高级教程-实例-数组:02. Java 实例 – 数组添加元素
ylbtech-Java-Runoob-高级教程-实例-数组:02. Java 实例 – 数组添加元素 1.返回顶部 1. Java 实例 - 数组添加元素 Java 实例 以下实例演示了如何使用s ...
- Java-Runoob-高级教程-实例-数组:01. Java 实例 – 数组排序及元素查找
ylbtech-Java-Runoob-高级教程-实例-数组:01. Java 实例 – 数组排序及元素查找 1.返回顶部 1. Java 实例 - 数组排序及元素查找 Java 实例 以下实例演示 ...
- Java数组声明创建和使用以及多维数组、Arrays类、稀疏数组
目录 数组概述 数组声明创建 内存分析 java内存分析 堆 栈 方法区 三种初始化 静态初始化 动态初始化 数组的默认初始化 数组的四个基本特点 数组边界 小结: 数组使用 数组基础使用 For E ...
随机推荐
- MEMCACHE与REDIS
千万数据量的高并发,容灾. Redis 基于单线程, 持久性 多数据类型 内存中只存KEY Redis支持数据的备份,即master-slave模式的数据备份. Redis与Memcached的区别 ...
- ORA-28001: the password has expired (DBD ERROR: OCISessionBegin)解决办法
1.问题描述: 打开oracle在线管理页面发现这个错误:界面如下 2问题原因 造成这个问题的主要原因是因为DBSNMP .SYSMAN用户密码已经过期. 3解决办法 可以使用sys以管理员的身份登录 ...
- Azkaban时区问题导致调度差1天
设置了Azkaban调度是每日凌晨一次,如下: 但是调度历史上显示最近一次调度时间是 初步怀疑是因为时区问题导致,查看服务器时区如下 cat /etc/timezone 为Asia/Shanghai. ...
- php5.6+Apache2.4+MySQL
在配置php的时候可以直接使用集成环境XAMPP:https://bitnami.com/stack/xampp?utm_source=bitnami&utm_medium=installer ...
- Kafka设计解析(十八)Kafka与Flink集成
转载自 huxihx,原文链接 Kafka与Flink集成 Apache Flink是新一代的分布式流式数据处理框架,它统一的处理引擎既可以处理批数据(batch data)也可以处理流式数据(str ...
- [NOI2001]炮兵阵地 【状压DP】
#\(\color{red}{\mathcal{Description}}\) \(Link\) 司令部的将军们打算在\(N \times M\)的网格地图上部署他们的炮兵部队.一个\(N \time ...
- JS控制上传文件个数
<html><body> <h3>js控制文件上传数量</h3> <form action="" enctype= ...
- 第一次作业:基于Linux 4.5的进程模型与调度器分析
1.操作系统是怎么组织进程的? 1.1什么是线程,什么是进程: 刚接触时可能经常会将这两个东西搞混.简单一点的说,进程是一个大工程,线程则是这个大工程中每个小地方需要做的东西(在linux下看作&qu ...
- PCIE_DMA实例二:xapp1052的EDK仿真
一:前言 这篇博客是我应一位网友之约写的,他想要学习基于FPGA的PCIe DMA控制器设计,但是手上没有合适的Xilinx开发板,而且xapp1052又没有提供仿真代码,让他的学习陷入了困境.所以我 ...
- JavaWeb基础—EL表达式与JSTL标签库
EL表达式: EL 全名为Expression Language.EL主要作用 获取数据(访问对象,访问数据,遍历集合等) 执行运算 获取JavaWeb常用对象 调用Java方法(EL函数库) 给出一 ...