【WebGL】详解vertexAttribPointer,属性如何从缓冲中读取数据

583 阅读13分钟

如果对WebGL感兴趣,可以点击easy-webgl关注

前言

在webgl中,我们调用WebGLRenderingContext.vertexAttribPointer()方法告诉显卡从当前绑定的缓冲区(bindBuffer() 指定的缓冲区)中读取顶点数据。

语法:

vertexAttribPointer(index, size, type, normalized, stride, offset)

可以用javascript伪代码表示如下:

// 伪代码
gl.vertexAttribPointer = function(location, size, type, normalize, stride, offset) {
  const attrib = gl.vertexArray.attributes[location];
  attrib.size = size;
  attrib.type = type;
  attrib.normalize = normalize;
  attrib.stride = stride ? stride : sizeof(type) * size;
  attrib.offset = offset;
  attrib.buffer = gl.arrayBuffer;  // !!!! <-----
};

image.png

本篇文章会详细解读vertexAttribPointer方法的各个参数字段的作用。

属性索引(Attribute index)

vertexAttribPointer方法的第一个参数就是属性的索引,即属性在顶点数组(vertex array)中的索引。顶点数组能支持多少个顶点取决于显卡。可以调用gl.getParameter(gl.MAX_VERTEX_ATTRIBS)获取顶点数组支持的顶点数量。每个属性都必须指定索引,有两种方法可以指定索引:

1.手动绑定顶点索引。

在调用gl.linkProgram()前,调用bindAttribLocation绑定顶点索引。比如:

const positionLocation = 6; // 必须小于gl.getParameter(gl.MAX_VERTEX_ATTRIBS)
gl.bindAttribLocation(program, positionLocation, 'position');

上面的代码将着色程序program中的position属性绑定在顶点数组中索引为6的位置上。后续可以直接将6传递给gl.vertexAttribPointer()

2.显卡自动分配

默认情况下,在编译顶点着色器时,显卡会自动为每个属性分配索引位置。每个属性的索引取决于显卡的分配,会有较大差异。这也就是为什么我们需要先调用gl.getAttribLocation()获取属性的索引,然后再赋值给gl.vertexAttribPointer()

SIZE

vertexAttribPointer方法的第二个参数就是size。size用来指定每个顶点属性的组成数量,必须是 1,2,3或4。实际上这就是我们在顶点着色器中声明属性时的vec[1234]的大小。所以size最大为4。

在继续之前,我们先回顾一下上一篇文章中【WebGL】深入理解属性和缓冲讲的属性默认值对象。每个属性都有默认值,存在gl.attributeValues数组中。属性的默认值为vec4(0.0, 0.0, 0.0, 1.0)。我们可以通过gl.vertexAttrib[1234]f[v]()修改默认值。然后通过gl.enableVertexAttribArray()告诉webgl从缓冲中读取数据。

以下面的代码为例,我们在顶点着色器中使用vec4告诉webgl,a_position期望的是4个数字。

const vertexShaderSource1 = `
    attribute vec4 a_position;
    void main(){
        gl_PointSize = 10.0;
        gl_Position = vec4(a_position);
    }
`

当我们调用gl.vertexAttribPointer并将size设为2时,webgl会先从缓冲中读取两个数字,然后第三和第四个数字会从默认值vec4(0.0, 0.0, 0.0, 1.0)中读取。 比如下面的代码,顶点着色器运行三次,属性a_position最终的值为:

  • vec(-0.5, 0.0, 0.0, 1.0)
  • vec(0.5, 0.0, 0.0, 1.0)
  • vec(0.0, 0.0, 0.0, 1.0)
 let positions = [
    -0.5, 0.0,
    0.5, 0.0,
    0.0, 0.0
  ]
 gl.vertexAttribPointer(
    positionLocation1,
    2, // size
    gl.FLOAT, // type, buffer的数据类型
    false,
    0, // 每个点的信息所占的bytes
    0
  )

需要特别注意的是,即使调用gl.vertexAttrib3f(positionLocation1, 0.0, 0.5, 0.6);修改了属性的默认值,webgl在读取缓冲时,默认值还是按照vec4(0.0, 0.0, 0.0, 1.0)读取。比如下面的代码:

const main = () => {
  const canvas = document.getElementById('webgl')
  const gl = canvas.getContext('webgl2')
  const vertexShaderSource1 = `
    attribute vec4 a_position;
    void main(){
        gl_PointSize = 10.0;
        gl_Position = vec4(a_position);
    }
  `
  const fragmentShaderSource1 = `
    precision mediump float;
    void main(){
        gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    }
  `
  const program1 = initShaders(gl, vertexShaderSource1, fragmentShaderSource1)

  const positionLocation1 = gl.getAttribLocation(program1, 'a_position')
  gl.vertexAttrib3f(positionLocation1, 0.0, 0.5, 0.6);

  let positions = [
    -0.5,
    0.5,
    0.0, 
  ]
  positions = new Float32Array(positions)



  const positionBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
  gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW)

  // 设置属性positionLocation1的一系列状态,告诉它应该怎么从缓冲中读取数据
  // 定义点的信息
  gl.vertexAttribPointer(
    positionLocation1,
    1, 
    gl.FLOAT, // type, buffer的数据类型
    false,
    0, // 每个点的信息所占的bytes
    0
  );
 

  gl.enableVertexAttribArray(positionLocation1);

  gl.clearColor(0, 0, 0, 0)
  gl.clear(gl.COLOR_BUFFER_BIT);

  gl.useProgram(program1)
  // 告诉webgl绘制3个点
  gl.drawArrays(gl.POINTS, 0, 3)

}

main();

上面的代码,我们先调用gl.vertexAttrib3f(positionLocation1, 0.0, 0.5, 0.6);修改了属性a_position的默认值。a_position的默认值变成vec4(0.0, 0.5, 0.6,1.0)。

然后我们通过缓冲传递了3个数字,并调用gl.vertexAttribPointer告诉webgl怎么从缓冲中读取数据,这里我们将size设为1,因此顶点着色器运行三次,a_position的最终取值:

  • vec(-0.5, 0.0, 0.0, 1.0)
  • vec(0.5, 0.0, 0.0, 1.0)
  • vec(0.0, 0.0, 0.0, 1.0)

而不是

  • vec(-0.5, 0.5, 0.6, 1.0)
  • vec(0.5, 0.5, 0.6, 1.0)
  • vec(0.0, 0.5, 0.6, 1.0)

效果如下,从图中可以看出三个点的y轴方向均为0.0,而不是0.5

image.png

MDN关于size从缓冲和默认值取值的描述应该是个陷阱,上面的实践结果表明即使改变了默认值,webgl读取默认值时还是会按照vec4(0.0, 0.0, 0.0, 1.0)读取

TYPE

vertexAttribPointer方法的第三个参数就是type。type用来指定数组中每个元素的数据类型,可以是下面的类型:

  • gl.BYTE:有符号的8位整数,范围[-128, 127]。一个数字占1个字节。可以使用new Int8Array创建
  • gl.SHORT:有符号的16位整数,范围[-32768, 32767]。一个数字占2个字节。可以使用new Int16Array创建
  • gl.UNSIGNED_BYTE:无符号的8位整数,范围 [0, 255]。一个数字占1个字节。可以使用new Uint8Array创建
  • gl.UNSIGNED_SHORT:无符号的16位整数,范围[0, 65535]。一个数字占2个字节。可以使用new Uint16Array创建
  • gl.FLOAT:32位IEEE标准的浮点。一个数字占4个字节。可以使用new Float32Array创建
  • 使用 WebGL2 版本的还可以使用以下值:gl.HALF_FLOAT

NORMALIZED(归一化)

vertexAttribPointer方法的第四个参数就是normalized。在WebGL中,归一化是指将数据按比例缩放到一个特定范围的过程,通常是 [0, 1] 或 [-1, 1]。这个过程在图形编程中非常重要,因为它可以帮助我们更好地控制颜色、纹理坐标和法向量等数据,从而实现更加精确和高效的渲染。归一化适用于所有非浮点型数据。

如果传递false解读原数据类型。 BYTE类型的范围是从-128到127,UNSIGNED_BYTE类型的范围是从0到255,SHORT类型的范围是从-32768到32767,等等...

如果设为true,BYTE数据的值(-128 to 127)将会转换到-1.0到+1.0之间,UNSIGNED_BYTE (0 to 255) 变为 0.0 到 +1.0 之间,SHORT 也是转换到 -1.0 到 +1.0 之间,但比BYTE精确度高。

  • 对于类型gl.BYTE和gl.SHORT,如果是 true 则将值归一化为 [-1, 1]
  • 对于类型gl.UNSIGNED_BYTE和gl.UNSIGNED_SHORT,如果是 true 则将值归一化为 [0, 1]
  • 对于类型gl.FLOAT和gl.HALF_FLOAT,此参数无效

最常用的是标准化颜色数据,将颜色值从[0, 255]范围缩放到[0, 1]范围,这样可以简化计算并提高渲染性能。用javascript实现就是:

function normalizeColor(color) {
  return color.map(c => c / 255);
}

在webgl中,颜色值范围为0.0到+1.0。使用4个浮点型数据存储红,绿,蓝和阿尔法通道数据时,每个顶点的颜色将会占用16字节空间(一个gl.FLOAT类型数据占用4个字节),如果你有复杂的几何体将会占用很多内存。代替的做法是将颜色数据转换为四个UNSIGNED_BYTE,其中0表示0.0,255表示 1.0。现在每个顶点只需要四个字节存储颜色值,省了75% 空间。

比如下面使用UNSIGNED_BYTE传送颜色数据:

import initShaders from "./initShaders.js";


const main = () => {
  const canvas = document.getElementById('webgl')
  const gl = canvas.getContext('webgl')
  const vertexShaderSource1 = `
    attribute vec3 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, 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,
    0.5, 0.0, 
    0.0, 0.5,
  ]
  let colors = [ // 这些数据在存入缓冲时将被截取成Uint8Array类型
    255, 0.1, 0.1111,// 会被截取成255,0,0
    0.222, 255, 0,// 会被截取成0,255,0
    0, 0, 255.888, // 会被截取成0, 0, 255
  ]
  colors = new Uint8Array(colors)
  positions = new Float32Array(positions)



  const FSIZE = positions.BYTES_PER_ELEMENT // 4
  const ISIZE = colors.BYTES_PER_ELEMENT // 1

  console.log('positions...',FSIZE, positions)

  const positionBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
  gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW)

  const colorBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer)
  gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW)

  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer)
  gl.vertexAttribPointer(
    positionLocation1,
    2, // size,
    gl.FLOAT, // type, buffer的数据类型
    false,
    2 * FSIZE, // 每个点的信息所占的bytes
    0
  );

  gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer)
  gl.vertexAttribPointer(
    colorPosition,
    3, // size,
    gl.UNSIGNED_BYTE,
    true,// 需要归一化
    3 * ISIZE,
    0
  );

  // 告诉webgl,属性positionLocation1应该从缓冲中读取数据,而不是从attributeValues中读取数据
  gl.enableVertexAttribArray(positionLocation1);
  gl.enableVertexAttribArray(colorPosition);

  gl.clearColor(0, 0, 0, 0)
  gl.clear(gl.COLOR_BUFFER_BIT);

  gl.useProgram(program1)
  // 告诉webgl绘制3个点
  gl.drawArrays(gl.POINTS, 0, 3)

}

main();

效果如下:

image.png

STRIDE 和 OFFSET

stride:以字节为单位指定连续顶点属性开始之间的偏移(即数组中一行长度)。不能大于255。如果 stride为0,则假定该属性是紧密打包的,即不交错属性,每个属性在一个单独的块中,下一个顶点的属性紧跟当前顶点之后。具体可看这里

offset:指定顶点属性数组中第一部分的字节偏移量。必须是类型的字节长度的倍数。

webglfundamentals中提到,如果stride和offset使用 0 以外的值时会复杂得多,虽然这样会取得一些性能能上的优势, 但是一般情况下并不值得,除非你想充分压榨WebGL的性能。

具体点,stride表示顶点着色器每执行一次,从缓冲中读取的一个片段,或者说顶点着色器每次执行,都需要前进的字节数。

交错属性:即使用同一个缓冲存储两个或多个属性需要的数据

假设我们有下面的顶点着色器:

const vertexShaderSource1 = `
    attribute vec3 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, 1.0);
    }
`

我们使用同一个positions缓冲存储每个顶点的坐标和颜色数据,下面数组中,每一行前两个表示顶点的坐标,后面三个表示顶点的颜色值。

  let positions = [
    // x y  r g b 前面两个代表坐标,后面三个代表颜色rgb的值
    -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)

然后调用gl.vertexAttribPointer告诉webgl怎么从缓冲中读取数据:

  gl.vertexAttribPointer(
    positionLocation1,
    2, // size,
    gl.FLOAT, // type, buffer的数据类型
    false,
    5 * FSIZE, // 每个点的信息所占的bytes
    0
  );
  gl.vertexAttribPointer(
    colorPosition,
    3, // size,attribute变量的长度(vec3)
    gl.FLOAT,
    false,
    5 * FSIZE,
    2 * FSIZE
  );

positions缓冲中,每个数字占用4个字节,我们用每5个数字存储一个顶点的坐标和颜色数据,因此一个顶点需要的坐标和颜色数据占用5 * 4 = 20个字节。比如下图所示,每一行(4个字节)表示一个数字,比如00 00 00 BF表示0.5。00 00 00 00表示0.0。

顶点着色器每次运行读取stride(即传给gl.vertexAttribPointer方法的stride参数的值)个字节。然后这stride个字节中,又有坐标数据,又有颜色数据,那怎么读取每个属性的数据?这就是offet的作用

image.png

image.png

不同数据类型的交错属性

到目前为止,我们基本都是用buffer传递类型固定的数据给GPU,那能不能用同一个buffer传递包含不同数据类型的数据到GPU?本节我们就来实践一下。

我们使用下面的数据结构表示一个顶点的数据,数组中每一项代表一个顶点,每个顶点包含坐标(position)和颜色(color)数据。坐标使用gl.FLOAT存储,需要8个字节。颜色使用gl.UNSIGNED_BYTE存储,需要4个字节,所以一个顶点需要12个字节

  // position需要8个字节,color需要4个字节,所以一个顶点需要12个字节
  const verticesInfo = [
    {
      position: [-0.5, 0.0], // 需要8个字节
      color: [255, 0, 0, 255], // 需要4个字节
    },
    {
      position: [0.5, 0.0],
      color: [0, 255, 0, 255],
    },
    {
      position: [0.0, 0.8],
      color: [0, 0, 255, 255]
    }
  ]

根据以上数据创建缓冲:


  // position需要8个字节,color需要4个字节,所以一个顶点需要12个字节
  const totalSizePerVertex = 12;
  const buffer = new ArrayBuffer(totalSizePerVertex * verticesInfo.length)
  const dv = new DataView(buffer);
  for (let i = 0; i < verticesInfo.length; i++) {
    const vertex = verticesInfo[i]
    dv.setFloat32(totalSizePerVertex * i, vertex.position[0], true)
    dv.setFloat32(totalSizePerVertex * i + 4, vertex.position[1], true)
    dv.setUint8(totalSizePerVertex * i + 8, vertex.color[0], true)
    dv.setUint8(totalSizePerVertex * i + 9, vertex.color[1], true)
    dv.setUint8(totalSizePerVertex * i + 10, vertex.color[2], true)
    dv.setUint8(totalSizePerVertex * i + 11, vertex.color[3], true)

  }
  console.log('buffer...', buffer)

然后告诉WebGL怎么读取缓冲:

  const vertexBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
  gl.bufferData(gl.ARRAY_BUFFER, buffer, gl.STATIC_DRAW)

  // 设置属性positionLocation1的一系列状态,告诉它应该怎么从缓冲中读取数据
  // 定义点的信息
  gl.vertexAttribPointer(
    positionLocation1,
    2, 
    gl.FLOAT, 
    false,
    totalSizePerVertex, 
    0
  );
  gl.vertexAttribPointer(
    colorPosition,
    4, 
    gl.UNSIGNED_BYTE,
    true,
    totalSizePerVertex,
    2 * Float32Array.BYTES_PER_ELEMENT
  );

绘制结果如下:

image.png 完整代码如下:

import initShaders from "./initShaders.js";


const main = () => {
  const canvas = document.getElementById('webgl')
  const gl = canvas.getContext('webgl')
  const vertexShaderSource1 = `
    attribute vec3 a_position;
    attribute vec4 a_color;
    varying vec4 v_color;
    void main(){
        v_color = a_color;
        gl_PointSize = 10.0;
        gl_Position = vec4(a_position, 1.0);
    }
  `
  const fragmentShaderSource1 = `
    precision mediump float;
    varying vec4 v_color;
    void main(){
        gl_FragColor = vec4(v_color);
    }
  `
  const program1 = initShaders(gl, vertexShaderSource1, fragmentShaderSource1)

  const positionLocation1 = gl.getAttribLocation(program1, 'a_position')
  const colorPosition = gl.getAttribLocation(program1, 'a_color')

  // position需要8个字节,color需要4个字节,所以一个顶点需要12个字节
  const verticesInfo = [
    {
      position: [-0.5, 0.0], // 需要8个字节
      color: [255, 0, 0, 255], // 需要4个字节
    },
    {
      position: [0.5, 0.0],
      color: [0, 255, 0, 255],
    },
    {
      position: [0.0, 0.8],
      color: [0, 0, 255, 255]
    }
  ]

  // position需要8个字节,color需要4个字节,所以一个顶点需要12个字节
  const totalSizePerVertex = 12;
  const buffer = new ArrayBuffer(totalSizePerVertex * verticesInfo.length)
  const dv = new DataView(buffer);
  for (let i = 0; i < verticesInfo.length; i++) {
    const vertex = verticesInfo[i]
    dv.setFloat32(totalSizePerVertex * i, vertex.position[0], true)
    dv.setFloat32(totalSizePerVertex * i + 4, vertex.position[1], true)
    dv.setUint8(totalSizePerVertex * i + 8, vertex.color[0], true)
    dv.setUint8(totalSizePerVertex * i + 9, vertex.color[1], true)
    dv.setUint8(totalSizePerVertex * i + 10, vertex.color[2], true)
    dv.setUint8(totalSizePerVertex * i + 11, vertex.color[3], true)

  }
  console.log('buffer...', buffer)
  const vertexBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
  gl.bufferData(gl.ARRAY_BUFFER, buffer, gl.STATIC_DRAW)

  // 设置属性positionLocation1的一系列状态,告诉它应该怎么从缓冲中读取数据
  // 定义点的信息
  gl.vertexAttribPointer(
    positionLocation1,
    2, 
    gl.FLOAT, 
    false,
    totalSizePerVertex, 
    0
  );
  gl.vertexAttribPointer(
    colorPosition,
    4, 
    gl.UNSIGNED_BYTE,
    true,
    totalSizePerVertex,
    2 * Float32Array.BYTES_PER_ELEMENT
  );


  gl.enableVertexAttribArray(positionLocation1);
  gl.enableVertexAttribArray(colorPosition);

  gl.clearColor(0, 0, 0, 0)
  gl.clear(gl.COLOR_BUFFER_BIT);

  gl.useProgram(program1)
  // 告诉webgl绘制3个点
  gl.drawArrays(gl.POINTS, 0, 3)

}

main();

交错属性需要特别注意的点

回顾一下,传递给vertexAttribPointer的stride参数的值必须是传递的数据类型的大小的整数倍。 比如传递给vertexAttribPointer的type是gl.FLOAT,那么stride必须是4的整数倍

vertexAttribPointer(index, size, type, normalized, stride, offset)

这在使用缓冲存储不同数据类型的交错属性时尤其需要注意,比如下面的数据:

  // position需要8个字节,color需要3个字节,所以一个顶点需要11个字节
  const verticesInfo = [
    {
      position: [-0.5, 0.0], // 需要8个字节
      color: [255, 0, 0], // 需要3个字节
    },
    {
      position: [0.5, 0.0],
      color: [0, 255, 0],
    },
    {
      position: [0.0, 0.8],
      color: [0, 0, 255]
    }
  ]

这里一个顶点需要11个字节,当我们调用vertexAttribPointer给position属性指定如何读取缓冲时:

  gl.vertexAttribPointer(
    positionLocation1,
    2, 
    gl.FLOAT, 
    false,
    totalSizePerVertex, 
    0
  );

由于type是gl.FLOAT,即4个字节,但是stride并不是4的倍数,此时是会报错的,如下面所示:

image.png

可以通过下面的代码验证:

import initShaders from "./initShaders.js";


const main = () => {
  const canvas = document.getElementById('webgl')
  const gl = canvas.getContext('webgl')
  const vertexShaderSource1 = `
    attribute vec3 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, 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')

  // position需要8个字节,color需要3个字节,所以一个顶点需要11个字节
  const verticesInfo = [
    {
      position: [-0.5, 0.0], // 需要8个字节
      color: [255, 0, 0], // 需要3个字节
    },
    {
      position: [0.5, 0.0],
      color: [0, 255, 0],
    },
    {
      position: [0.0, 0.8],
      color: [0, 0, 255]
    }
  ]

  // position需要8个字节,color需要3个字节,所以一个顶点需要11个字节
  const totalSizePerVertex = 11;
  const buffer = new ArrayBuffer(totalSizePerVertex * verticesInfo.length)
  const dv = new DataView(buffer);
  for (let i = 0; i < verticesInfo.length; i++) {
    const vertex = verticesInfo[i]
    dv.setFloat32(totalSizePerVertex * i, vertex.position[0], true)
    dv.setFloat32(totalSizePerVertex * i + 4, vertex.position[1], true)
    dv.setUint8(totalSizePerVertex * i + 8, vertex.color[0], true)
    dv.setUint8(totalSizePerVertex * i + 9, vertex.color[1], true)
    dv.setUint8(totalSizePerVertex * i + 10, vertex.color[2], true)

  }
  console.log('buffer...', buffer)
  const vertexBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
  gl.bufferData(gl.ARRAY_BUFFER, buffer, gl.STATIC_DRAW)


  gl.vertexAttribPointer(
    positionLocation1,
    2, 
    gl.FLOAT, 
    false,
    totalSizePerVertex, 
    0
  );
  gl.vertexAttribPointer(
    colorPosition,
    3, 
    gl.UNSIGNED_BYTE,
    true,
    totalSizePerVertex,
    2 * Float32Array.BYTES_PER_ELEMENT
  );


  gl.enableVertexAttribArray(positionLocation1);
  gl.enableVertexAttribArray(colorPosition);

  gl.clearColor(0, 0, 0, 0)
  gl.clear(gl.COLOR_BUFFER_BIT);

  gl.useProgram(program1)
  // 告诉webgl绘制3个点
  gl.drawArrays(gl.POINTS, 0, 3)

}

main();

参考