第 80 期 - WebGL 中的属性与缓冲
logoFRONTALK AI/1月12日 16:32/阅读原文

摘要

文章围绕 WebGL 中的属性和缓冲展开,介绍了着色器获取数据的 4 种方式,重点阐述属性赋值、缓冲概念,还通过伪代码展示其原理并提及相关实践。

一、WebGL 中着色器的数据获取方式

WebGL 代码由顶点着色器和片段着色器组成,它们在 GPU 中运行,所需数据需发送到 GPU。着色器获取数据有 4 种方法:

  1. 属性(Attributes)和缓冲:属性仅适用于顶点着色器。
  2. 全局变量(Uniforms):在一次绘制中对所有顶点或者像素保持一致值,适用于顶点着色器和片段着色器。
  3. 纹理(Textures):从像素或纹理元素中获取的数据,适用于顶点着色器和片段着色器。
  4. 可变量(Varyings):顶点着色器给片段着色器传值的一种方式。

本文主要关注属性和缓冲。缓冲是发送到 GPU 的二进制数据序列,是 GPU 中的内存区域,可存储任何数据。属性用来指明从缓冲中获取所需数据并提供给顶点着色器的方式。

二、属性相关内容

  1. 属性赋值方式
    • 一种方式是WebGLRenderingContext.vertexAttrib[1234]f[v](),这种方式不常用。
    • 另一种常用方式是通过缓冲。例如以下代码通过gl.vertexAttrib2f给属性赋值绘制一个点:
    const canvas = document.getElementById('webgl');
    const gl = canvas.getContext('webgl');
    const vertexShaderSource = `
        attribute vec2 a_position1;
        void main() {
          gl_Position = vec4(a_position1, 0, 1);
          gl_PointSize = 10.0;
        }
    `;
    const fragmentShaderSource = `
        precision mediump float;
        void main() {
          gl_FragColor = vec4(1,0,0.5,1);
        }
    `;
    const program = initShaders(gl, vertexShaderSource, fragmentShaderSource);
    const positionLocation1 = gl.getAttribLocation(program, 'a_position1');
    gl.clearColor(0, 0, 0, 0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.useProgram(program);
    gl.vertexAttrib2f(positionLocation1, 0.5, 0.5);
    gl.drawArrays(gl.POINTS, 0, 1);
    
  2. 属性在 WebGL 内部的表示(伪代码)
    • gl.arrayBuffer是全局变量,用于绑定目标缓冲区以便传输数据。
    • gl.vertexArray是顶点数组对象(VAO),WebGL 全局只有一个,所有着色程序从这个对象读取属性。它至少支持 8 个属性。
    • vertexArray.attributes数组中的每一项表示一个属性,有自己的状态,默认从attributeValues中读取值。例如:
    // 伪代码
    const gl = {
      arrayBuffer: null,
      attributeValues: [
        [0, 0, 0, 1],
        [0, 0, 0, 1],
        [0, 0, 0, 1],
        [0, 0, 0, 1],
        [0, 0, 0, 1],
        [0, 0, 0, 1],
        [0, 0, 0, 1],
        [0, 0, 0, 1],
      ],
      vertexArray: {
        attributes: [
          { enable:?, type:?, size:?, normalize:?, stride:?, offset:?, buffer:?, divisor: 0 },
          { enable:?, type:?, size:?, normalize:?, stride:?, offset:?, buffer:?, divisor: 0 },
          { enable:?, type:?, size:?, normalize:?, stride:?, offset:?, buffer:?, divisor: 0 },
          { enable:?, type:?, size:?, normalize:?, stride:?, offset:?, buffer:?, divisor: 0 },
          { enable:?, type:?, size:?, normalize:?, stride:?, offset:?, buffer:?, divisor: 0 },
          { enable:?, type:?, size:?, normalize:?, stride:?, offset:?, buffer:?, divisor: 0 },
          { enable:?, type:?, size:?, normalize:?, stride:?, offset:?, buffer:?, divisor: 0 },
          { enable:?, type:?, size:?, normalize:?, stride:?, offset:?, buffer:?, divisor: 0 },
        ],
        elementArrayBuffer: null,
      },
    };
    
  3. 属性默认值对象(gl.attributeValues)
    • WebGL 内部只有一个gl.attributeValues对象,所有着色程序共用。可通过gl.vertexAttrib[1234]f[v]()修改其值。
    • 例如通过const positionLocation1 = gl.getAttribLocation(program, 'a_position1')获取属性索引,gl.vertexAttrib2f(positionLocation1, 0.5, 0.5)的伪代码如下:
    // 伪代码
    gl.vertexAttrib2f = function(location, a, b) {
      const attrib = gl.attributeValues[location];
      attrib[0] = a;
      attrib[1] = b;
    };
    
    • 还可以通过代码验证两个着色程序共用一个attributeValues对象。
  4. WebGL 顶点数组对象(VAO)
    • WebGL 内部只有一个顶点数组对象gl.vertexArray,但 WebGL 提供OES_vertex_array_object扩展允许创建、替换vertexArray。例如在 WebGL2 中:
    var vao = gl.createVertexArray();
    gl.bindVertexArray(vao);
    
    • 伪代码实现:
    gl.bindVertexArray = function(vao) {
       gl.vertexArray = vao? vao : defaultVAO;
    };
    
    • 通过实践替换 VAO 的代码示例,展示了在不同操作下的渲染结果差异,以及属性共用gl.attributeValues对象的情况。还提到可以通过gl.getParameter(gl.MAX_VERTEX_ATTRIBS)获取 WebGL 支持的属性数量。

三、缓冲相关内容

  1. 为什么需要缓冲
    • 如果通过gl.vertexAttrib[1234]f[v]()的方式绘制多个点,例如以下代码绘制三个点:
    const canvas = document.getElementById('webgl');
    const gl = canvas.getContext('webgl');
    const vertexShaderSource1 = `
        attribute vec2 a_position1;
        void main() {
          gl_Position = vec4(a_position1, 0, 1);
          gl_PointSize = 10.0;
        }
    `;
    const fragmentShaderSource1 = `
        precision mediump float;
        void main() {
          gl_FragColor = vec4(1,0,0.5,1);
        }
    `;
    const program1 = initShaders(gl, vertexShaderSource1, fragmentShaderSource1);
    const positionLocation1 = gl.getAttribLocation(program1, 'a_position1');
    const positions = [
      0.0, 0.0,
      0.5, 0.5,
      0.5, 0.0,
    ];
    gl.clearColor(0, 0, 0, 0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.useProgram(program1);
    for(let i = 0; i < 6;i=i + 2){
      const x = positions[i];
      const y = positions[i + 1];
      gl.vertexAttrib2f(positionLocation1, x, y);
      gl.drawArrays(gl.POINTS, 0, 1);
    }
    
    • 在这个过程中,CPU 和 GPU 至少有三次通信过程,如果绘制 n 个点就需要 n 次通信,效率低下,存在性能瓶颈。所以更好的做法是一次性往 GPU 发送顶点数据,并设置属性状态告诉 GPU 如何读取。
  2. 如何给缓冲传值
    • 使用缓冲区对象向顶点着色器传入多个顶点的数据需要遵循五个步骤:
      • gl.createBuffer:创建缓冲区对象。
      • gl.bindBuffer:绑定缓冲区对象。
      • gl.bufferData:将数据写入缓冲区对象。
      • gl.vertexAttribPointer:将缓冲区对象分配给一个属性变量,并告诉属性如何读取缓冲中的数据。
      • gl.enableVertexAttribArray:开启属性变量,告诉 WebGL 这个属性应该从缓冲中读取数据,如果关闭则从attributeValues中读取。
    • 通过代码示例展示如何使用缓冲绘制三个点:
    const canvas = document.getElementById('webgl');
    const gl = canvas.getContext('webgl');
    const vertexShaderSource1 = `
        attribute vec2 a_position1;
        void main() {
          gl_Position = vec4(a_position1, 0, 1);
          gl_PointSize = 10.0;
        }
    `;
    const fragmentShaderSource1 = `
        precision mediump float;
        void main() {
          gl_FragColor = vec4(1,0,0.5,1);
        }
    `;
    const program1 = initShaders(gl, vertexShaderSource1, fragmentShaderSource1);
    const positionLocation1 = gl.getAttribLocation(program1, 'a_position1');
    const positions = [
      0.0, 0.0,
      0.5, 0.5,
      0.5, 0.0,
    ];
    const positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
    gl.vertexAttribPointer(
      positionLocation1, 2, gl.FLOAT, false, 0, 0
    );
    gl.enableVertexAttribArray(positionLocation1);
    gl.clearColor(0, 0, 0, 0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.useProgram(program1);
    gl.drawArrays(gl.POINTS, 0, 3);
    
    • 解释了每个步骤的作用以及为什么需要这么繁琐的步骤,如gl.bindBuffer的伪代码:
    // 伪代码
    gl.bindBuffer = function(target, buffer) {
      switch (target) {
        case ARRAY_BUFFER:
          gl.arrayBuffer = buffer;
          break;
        case ELEMENT_ARRAY_BUFFER:
          gl.vertexArray.elementArrayBuffer = buffer;
          break;
     ...
    };
    gl.bufferData = function(target, data, type) {
      gl.arrayBuffer = data;
     ...
    };
    
    • 还给出了gl.vertexAttribPointergl.enableVertexAttribArraygl.disableVertexAttribArray的伪代码及解释。
    • 通过示例验证gl.enableVertexAttribArray的作用。
  3. 如何通过缓冲传递坐标和颜色值
    • 缓冲中不仅可存储坐标数据,还可存储颜色、法向量坐标等数据。通过代码示例展示如何利用缓冲存储一个点的坐标和颜色值:
    const canvas = document.getElementById('webgl');
    const gl = canvas.getContext('webgl');
    const vertexShaderSource1 = `
        attribute vec2 a_position;
        attribute vec3 a_color;
        varying vec3 v_color;
        void main(){
            v_color = a_color;
            gl_PointSize = 10.0;
            gl_Position = vec4(a_position, 0.0, 1.0);
        }
    `;
    const fragmentShaderSource1 = `
        precision mediump float;
        varying vec3 v_color;
        void main(){
            gl_FragColor = vec4(v_color, 1.0);
        }
    `;
    const program1 = initShaders(gl, vertexShaderSource1, fragmentShaderSource1);
    const positionLocation1 = gl.getAttribLocation(program1, 'a_position');
    const colorPosition = gl.getAttribLocation(program1, 'a_color');
    let positions = [
      -0.5, 0.0, 1.0, 0.0, 0.0,
      0.5, 0.0, 0.0, 1.0, 0.0,
      0.0, 0.8, 0.0, 0.0, 1.0
    ];
    positions = new Float32Array(positions);
    const FSIZE = positions.BYTES_PER_ELEMENT; // 4
    const positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
    gl.vertexAttribPointer(
      positionLocation1,
      2,
      gl.FLOAT,
      false,
      5 * FSIZE,
      0
    );
    gl.vertexAttribPointer(
      colorPosition,
      3,
      gl.FLOAT,
      false,
      5 * FSIZE,
      2 * FSIZE
    );
    gl.enableVertexAttribArray(positionLocation1);
    gl.enableVertexAttribArray(colorPosition);
    gl.clearColor(0, 0, 0, 0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.useProgram(program1);
    gl.drawArrays(gl.POINTS, 0, 3);
    gl.drawArrays(gl.TRIANGLES, 0, 3);
    

四、总结

文章介绍了属性和缓冲的基本概念和作用,深入剖析了顶点数组对象(VAO)和AttributeValues,学习了通过gl.vertexAttrib[1234]fv和缓冲给属性传值,还通过伪代码展示了 WebGL 内部的工作原理。

 

扩展阅读

Made by 捣鼓键盘的小麦 / © 2025 Front Talk 版权所有