什么是 OpenGL 和 WebGL2 中的顶点数组?
What are Vertex Arrays in OpenGL & WebGL2?
我使用 WebGL1 已经有一段时间了,但现在我正在学习更多关于 WebGL2 的知识,我对 Vertex Array
的实际作用感到困惑。例如,在下面的 example 中,我可以删除对它们的所有引用(例如创建、绑定、删除)并且示例继续工作。
这已在别处解释,但您可以考虑 WebGL1 和 WebGL2 都有一个顶点数组。只是默认情况下 WebGL1 只有一个 WebGL2 可以创建多个顶点数组(尽管 99.9% 的 WebGL1 实现都支持它们作为扩展)
顶点数组是所有属性状态加上 ELEMENT_ARRAY_BUFFER
绑定的集合。
你可以这样想 WebGL 状态
class WebGLRenderingContext {
constructor() {
// internal WebGL state
this.lastError: gl.NONE,
this.arrayBuffer = null;
this.vertexArray = {
elementArrayBuffer: null,
attributes: [
{ enabled: false, type: gl.FLOAT, size: 3, normalized: false,
stride: 0, offset: 0, value: [0, 0, 0, 1], buffer: null },
{ enabled: false, type: gl.FLOAT, size: 3, normalized: false,
stride: 0, offset: 0, value: [0, 0, 0, 1], buffer: null },
{ enabled: false, type: gl.FLOAT, size: 3, normalized: false,
stride: 0, offset: 0, value: [0, 0, 0, 1], buffer: null },
{ enabled: false, type: gl.FLOAT, size: 3, normalized: false,
stride: 0, offset: 0, value: [0, 0, 0, 1], buffer: null },
{ enabled: false, type: gl.FLOAT, size: 3, normalized: false,
stride: 0, offset: 0, value: [0, 0, 0, 1], buffer: null },
...
],
}
...
你可以认为 gl.bindBuffer
是这样实现的
// Implementation of gl.bindBuffer.
// note this function is doing nothing but setting 2 internal variables.
this.bindBuffer = function(bindPoint, buffer) {
switch(bindPoint) {
case gl.ARRAY_BUFFER;
this.arrayBuffer = buffer;
break;
case gl.ELEMENT_ARRAY_BUFFER;
this.vertexArray.elementArrayBuffer = buffer;
break;
default:
this.lastError = gl.INVALID_ENUM;
break;
}
};
所以你可以在上面看到,用 gl.ELEMENT_ARRAY_BUFFER
调用 gl.bindBuffer
设置当前 vertexArray
的 elementArray
部分
您还可以看到 vertexArray
具有许多属性。它们定义了如何从缓冲区中提取数据以提供给顶点着色器。调用 gl.getAttribLocation(someProgram, "nameOfAttribute")
会告诉您顶点着色器将查看哪个属性以从缓冲区中获取数据。
有 4 个函数可用于配置属性如何从缓冲区获取数据。 gl.enableVertexAttribArray
、gl.disableVertexAttribArray
、gl.vertexAttribPointer
和 gl.vertexAttrib??
。
他们有效地实施了这样的事情
this.enableVertexAttribArray = function(location) {
const attribute = this.vertexArray.attributes[location];
attribute.enabled = true; // true means get data from attribute.buffer
};
this.disableVertexAttribArray = function(location) {
const attribute = this.vertexArray.attributes[location];
attribute.enabled = false; // false means get data from attribute.value
};
this.vertexAttribPointer = function(location, size, type, normalized, stride, offset) {
const attribute = this.vertexArray.attributes[location];
attribute.size = size; // num values to pull from buffer per vertex shader iteration
attribute.type = type; // type of values to pull from buffer
attribute.normalized = normalized; // whether or not to normalize
attribute.stride = stride; // number of bytes to advance for each iteration of the vertex shader. 0 = compute from type, size
attribute.offset = offset; // where to start in buffer.
// IMPORTANT!!! Associates whatever buffer is currently *bound* to
// "arrayBuffer" to this attribute
attribute.buffer = this.arrayBuffer;
};
this.vertexAttrib4f = function(location, x, y, z, w) {
const attribute = this.vertexArray.attributes[location];
attribute.value[0] = x;
attribute.value[1] = y;
attribute.value[2] = z;
attribute.value[3] = w;
};
现在,当您调用 gl.drawArrays
或 gl.drawElements
时,系统知道您希望如何从您为顶点着色器创建的缓冲区中提取数据。 See here for how that works.
然后有 3 个函数将管理连接到 this.vertexArray
的所有状态。它们是 gl.createVertexArray
、gl.bindVertexArray
和 gl.deleteVertexArray
。在 WebGL1 中,它们在稍微重命名的 OES_vertex_array_object
扩展中可用。在 WebGL2 上,它们只是默认可用
这也是 WebGL 2.0 的一个特性。
调用gl.createVertexArray
生成新的顶点数组。调用 gl.bindVertexArray
设置 this.vertexArray
指向你传入的那个。你可以想象它是这样实现的
this.bindVertexArray = function(vao) {
this.vertexArray = vao ? vao : defaultVertexArray;
}
好处应该是显而易见的。在你想要绘制的每一个东西之前,你需要设置所有的属性。设置每个属性需要每个使用的属性至少调用一次。更常见的是每个属性调用 3 次。一次调用 gl.bindBuffer
将缓冲区绑定到 ARRAY_BUFFER
,一次调用 gl.vertexAttribPointer
将该缓冲区绑定到特定属性并设置如何提取数据,一次调用 gl.enableVertexAttribArray
以打开从缓冲区获取属性数据的功能。
对于具有位置、法线和纹理坐标的典型模型,调用 9 次,如果您使用索引并需要将缓冲区绑定到 ELEMENT_ARRAY_BUFFER
,则再调用 +1 次。
对于顶点数组,所有这些调用都发生在初始时间。您为每个要绘制的事物创建一个顶点数组,然后为该事物设置属性。在绘制时,只需调用一次 gl.bindVertexArray
即可设置所有属性和 ELEMENT_ARRAY_BUFFER
.
如果您只想始终使用顶点数组,您可以在 WebGL1 中使用 this polyfill。如果扩展存在或者模拟它,它使用内置的。当然仿真速度较慢,但任何需要仿真的 GPU 可能已经太慢了。
请注意,如果您正在寻找示例,可以比较 https://webglfundamentals.org to https://webgl2fundamentals.org 上的相应示例。 WebGL2 站点到处都使用顶点数组。您会注意到在 WebGL1 示例中,就在绘制之前,对于每个顶点数据,绑定该数据的缓冲区,然后设置该数据的属性。在 WebGL2 示例中,它发生在初始化时间而不是绘制时间。在平局时,所有发生的事情都是调用 gl.bindVertexArray
关于顶点数组需要注意的另一件事是它们通常需要更多的组织。如果您打算使用不同的着色器程序多次绘制同一个对象,那么一个着色器程序可能会对相同的数据使用不同的属性。换句话说,如果没有额外的组织,shaderprogram1 可能会使用属性 3 作为位置,而 shaderprogram2 可能会使用属性 2 作为位置。在那种情况下,相同的顶点数组将无法用于相同数据的两个程序。
解决方法是手动分配位置。您可以在 WebGL2 的着色器中执行此操作。您也可以通过在为 WebGL1 和 WebGL2 中的每个着色器程序链接着色器之前调用 gl.bindAttribLocation
来实现。我倾向于认为使用 gl.bindAttribLocation
比在 GLSL 中更好,因为它更 D.R.Y.
我使用 WebGL1 已经有一段时间了,但现在我正在学习更多关于 WebGL2 的知识,我对 Vertex Array
的实际作用感到困惑。例如,在下面的 example 中,我可以删除对它们的所有引用(例如创建、绑定、删除)并且示例继续工作。
这已在别处解释,但您可以考虑 WebGL1 和 WebGL2 都有一个顶点数组。只是默认情况下 WebGL1 只有一个 WebGL2 可以创建多个顶点数组(尽管 99.9% 的 WebGL1 实现都支持它们作为扩展)
顶点数组是所有属性状态加上 ELEMENT_ARRAY_BUFFER
绑定的集合。
你可以这样想 WebGL 状态
class WebGLRenderingContext {
constructor() {
// internal WebGL state
this.lastError: gl.NONE,
this.arrayBuffer = null;
this.vertexArray = {
elementArrayBuffer: null,
attributes: [
{ enabled: false, type: gl.FLOAT, size: 3, normalized: false,
stride: 0, offset: 0, value: [0, 0, 0, 1], buffer: null },
{ enabled: false, type: gl.FLOAT, size: 3, normalized: false,
stride: 0, offset: 0, value: [0, 0, 0, 1], buffer: null },
{ enabled: false, type: gl.FLOAT, size: 3, normalized: false,
stride: 0, offset: 0, value: [0, 0, 0, 1], buffer: null },
{ enabled: false, type: gl.FLOAT, size: 3, normalized: false,
stride: 0, offset: 0, value: [0, 0, 0, 1], buffer: null },
{ enabled: false, type: gl.FLOAT, size: 3, normalized: false,
stride: 0, offset: 0, value: [0, 0, 0, 1], buffer: null },
...
],
}
...
你可以认为 gl.bindBuffer
是这样实现的
// Implementation of gl.bindBuffer.
// note this function is doing nothing but setting 2 internal variables.
this.bindBuffer = function(bindPoint, buffer) {
switch(bindPoint) {
case gl.ARRAY_BUFFER;
this.arrayBuffer = buffer;
break;
case gl.ELEMENT_ARRAY_BUFFER;
this.vertexArray.elementArrayBuffer = buffer;
break;
default:
this.lastError = gl.INVALID_ENUM;
break;
}
};
所以你可以在上面看到,用 gl.ELEMENT_ARRAY_BUFFER
调用 gl.bindBuffer
设置当前 vertexArray
elementArray
部分
您还可以看到 vertexArray
具有许多属性。它们定义了如何从缓冲区中提取数据以提供给顶点着色器。调用 gl.getAttribLocation(someProgram, "nameOfAttribute")
会告诉您顶点着色器将查看哪个属性以从缓冲区中获取数据。
有 4 个函数可用于配置属性如何从缓冲区获取数据。 gl.enableVertexAttribArray
、gl.disableVertexAttribArray
、gl.vertexAttribPointer
和 gl.vertexAttrib??
。
他们有效地实施了这样的事情
this.enableVertexAttribArray = function(location) {
const attribute = this.vertexArray.attributes[location];
attribute.enabled = true; // true means get data from attribute.buffer
};
this.disableVertexAttribArray = function(location) {
const attribute = this.vertexArray.attributes[location];
attribute.enabled = false; // false means get data from attribute.value
};
this.vertexAttribPointer = function(location, size, type, normalized, stride, offset) {
const attribute = this.vertexArray.attributes[location];
attribute.size = size; // num values to pull from buffer per vertex shader iteration
attribute.type = type; // type of values to pull from buffer
attribute.normalized = normalized; // whether or not to normalize
attribute.stride = stride; // number of bytes to advance for each iteration of the vertex shader. 0 = compute from type, size
attribute.offset = offset; // where to start in buffer.
// IMPORTANT!!! Associates whatever buffer is currently *bound* to
// "arrayBuffer" to this attribute
attribute.buffer = this.arrayBuffer;
};
this.vertexAttrib4f = function(location, x, y, z, w) {
const attribute = this.vertexArray.attributes[location];
attribute.value[0] = x;
attribute.value[1] = y;
attribute.value[2] = z;
attribute.value[3] = w;
};
现在,当您调用 gl.drawArrays
或 gl.drawElements
时,系统知道您希望如何从您为顶点着色器创建的缓冲区中提取数据。 See here for how that works.
然后有 3 个函数将管理连接到 this.vertexArray
的所有状态。它们是 gl.createVertexArray
、gl.bindVertexArray
和 gl.deleteVertexArray
。在 WebGL1 中,它们在稍微重命名的 OES_vertex_array_object
扩展中可用。在 WebGL2 上,它们只是默认可用
这也是 WebGL 2.0 的一个特性。
调用gl.createVertexArray
生成新的顶点数组。调用 gl.bindVertexArray
设置 this.vertexArray
指向你传入的那个。你可以想象它是这样实现的
this.bindVertexArray = function(vao) {
this.vertexArray = vao ? vao : defaultVertexArray;
}
好处应该是显而易见的。在你想要绘制的每一个东西之前,你需要设置所有的属性。设置每个属性需要每个使用的属性至少调用一次。更常见的是每个属性调用 3 次。一次调用 gl.bindBuffer
将缓冲区绑定到 ARRAY_BUFFER
,一次调用 gl.vertexAttribPointer
将该缓冲区绑定到特定属性并设置如何提取数据,一次调用 gl.enableVertexAttribArray
以打开从缓冲区获取属性数据的功能。
对于具有位置、法线和纹理坐标的典型模型,调用 9 次,如果您使用索引并需要将缓冲区绑定到 ELEMENT_ARRAY_BUFFER
,则再调用 +1 次。
对于顶点数组,所有这些调用都发生在初始时间。您为每个要绘制的事物创建一个顶点数组,然后为该事物设置属性。在绘制时,只需调用一次 gl.bindVertexArray
即可设置所有属性和 ELEMENT_ARRAY_BUFFER
.
如果您只想始终使用顶点数组,您可以在 WebGL1 中使用 this polyfill。如果扩展存在或者模拟它,它使用内置的。当然仿真速度较慢,但任何需要仿真的 GPU 可能已经太慢了。
请注意,如果您正在寻找示例,可以比较 https://webglfundamentals.org to https://webgl2fundamentals.org 上的相应示例。 WebGL2 站点到处都使用顶点数组。您会注意到在 WebGL1 示例中,就在绘制之前,对于每个顶点数据,绑定该数据的缓冲区,然后设置该数据的属性。在 WebGL2 示例中,它发生在初始化时间而不是绘制时间。在平局时,所有发生的事情都是调用 gl.bindVertexArray
关于顶点数组需要注意的另一件事是它们通常需要更多的组织。如果您打算使用不同的着色器程序多次绘制同一个对象,那么一个着色器程序可能会对相同的数据使用不同的属性。换句话说,如果没有额外的组织,shaderprogram1 可能会使用属性 3 作为位置,而 shaderprogram2 可能会使用属性 2 作为位置。在那种情况下,相同的顶点数组将无法用于相同数据的两个程序。
解决方法是手动分配位置。您可以在 WebGL2 的着色器中执行此操作。您也可以通过在为 WebGL1 和 WebGL2 中的每个着色器程序链接着色器之前调用 gl.bindAttribLocation
来实现。我倾向于认为使用 gl.bindAttribLocation
比在 GLSL 中更好,因为它更 D.R.Y.