4

我使用 WebGL1 已经有一段时间了,但现在我正在学习更多关于 WebGL2 的知识,我很困惑Vertex Arrays 的实际作用。例如,在下面的示例中,我可以删除对它们的所有引用(例如创建、绑定、删除)并且示例继续工作。

4

1 回答 1

7

这已在其他地方解释过,但您可以认为 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.bindBufferwithgl.ELEMENT_ARRAY_BUFFER设置elementArray当前的部分vertexArray

您还可以看到vertexArray有许多属性。它们定义了如何从缓冲区中提取数据以提供给顶点着色器。调用gl.getAttribLocation(someProgram, "nameOfAttribute")会告诉您顶点着色器将查看哪个属性以从缓冲区中获取数据。

有 4 个函数可用于配置属性如何从缓冲区获取数据。gl.enableVertexAttribArray, gl.disableVertexAttribArray,gl.vertexAttribPointergl.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.drawArraysgl.drawElements系统知道您想如何从缓冲区中提取数据以提供顶点着色器时。请参阅此处了解其工作原理

然后有 3 个函数将管理连接到的所有状态this.vertexArray。它们gl.createVertexArraygl.bindVertexArraygl.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.

对于顶点数组,所有这些调用都发生在初始化时。您为要绘制的每个事物创建一个顶点数组,然后设置该事物的属性。在绘图时,只需调用一次gl.bindVertexArray即可设置所有属性和ELEMENT_ARRAY_BUFFER.

如果您只想始终使用顶点数组,您可以在 WebGL1 中使用这个polyfill。如果扩展存在或模拟它,它使用内置的。当然,仿真速度较慢,但​​任何需要仿真的 GPU 可能已经太慢了。

请注意,如果您正在寻找样本,可以将 https://webglfundamentals.org 上的相应示例与https://webgl2fundamentals.org进行比较。WebGL2 站点到处都使用顶点数组。您会在绘制之前的 WebGL1 示例中注意到,对于每个顶点数据,该数据的缓冲区被绑定,然后该数据的属性被设置。在 WebGL2 示例中,发生在初始化时间而不是绘制时间。在抽签的时候,所有发生的事情就是打电话gl.bindVertexArray

关于顶点数组需要注意的另一件事是它们通常需要更多的组织。如果您要使用不同的着色器程序多次绘制同一个对象,那么一个着色器程序可能会对相同的数据使用不同的属性。换句话说,在没有额外组织的情况下,shaderprogram1 可能将属性 3 用于位置,而 shaderprogram2 可能将属性 2 用于位置。在这种情况下,相同的顶点数组将无法与两个程序一起使用相同的数据。

解决方案是手动分配位置。您可以在 WebGL2 中的着色器本身中执行此操作。您也可以通过gl.bindAttribLocation在链接 WebGL1 和 WebGL2 中的每个着色器程序的着色器之前调用来实现。我倾向于认为使用gl.bindAttribLocation比在 GLSL 中更好,因为它更干燥

于 2018-05-09T16:06:15.873 回答