webGL入门-绘制三角形

182 阅读7分钟

在动态绘制一个点文章中juejin.cn/post/706776… 只能绘制一个点,而不能绘制多个顶点组成的图形,比如三角形、矩形和立方体

所以我们需要方法,一次性的将图形的顶点全部传入顶点着色器,然后把图形画出来。

webGL提供了一种很方便的机制,即缓冲区对象,它可以一次性的向着色器传入多个顶点的数据。

<canvas id="glcanvas" width="640" height="480"></canvas>
  // 顶点着色器程序
  const VSHADER_SOURCE = `attribute vec4 a_Position; // a1.声明attribute变量
  void main() {
    gl_Position = a_Position; // a2.将attribute变量赋值给gl_Position变量
    gl_PointSize = 10.0;
  }`;

  // 片元着色器程序
  const FSHADER_SOURCE = `void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
  }`;

  // 顶点缓冲对象(VBO)流程封装
  function initVertexBuffers(gl) {
    // b1.创建缓冲区对象
    let vertexBuffer = gl.createBuffer();
    if (!vertexBuffer) {
      console.log("创建buffer Object缓冲区对象失败!");
      return -1;
    }

    // b2.将缓冲区对象绑定到目标
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);

    // b3.向缓冲区对象中写入数据
    let vertices = new Float32Array([
      -0.5, 0.5, 
      -0.5, -0.5, 
      0.5, 0.5, 
      0.5, -0.5,
    ]); // 这里为了方便,只传入x,y,即这里有四个顶点
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

    // a3.获取 attribute 变量的存储位置
    const a_Position = gl.getAttribLocation(gl.program, "a_Position");
    if (a_Position < 0) {
      console.log("获取a_Position的存储位置失败!");
      return -1;
    }

    // b4.将缓冲区对象分配给 attribute (a_Position) 变量
    // 第二个参数表示缓冲区中每个顶点有几个分量值,因为在缓冲区中我只提供了x,y,所以我们传入2
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);

    // b5.连接 attribute (a_Position) 变量与分配给他的缓冲区对象
    gl.enableVertexAttribArray(a_Position);

    let n = vertices.length / 2; // 两个值表示一个顶点,这里计算出有4个点,供后面gl.drawArrays()使用
    return n;
  }

  function main() {
    // 1.获取canvas元素
    const canvas = document.getElementById("glcanvas");

    // 2.获取webgl绘图上下文
    const gl = canvas.getContext("webgl");

    if (!gl) {
      alert("不支持webgl");
      return;
    }

    // 初始化着色器(封装在另一个函数库中,具体请看 https://juejin.cn/post/7066335207510507534)
    if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
      console.log("着色器初始化失败");
      return;
    }

    // 3.设置顶点位置
    let n = initVertexBuffers(gl);
    if (n < 0) {
      console.log("设置顶点位置失败");
      return;
    }

    // 4.设置背景色(指定清空canvas的颜色)
    gl.clearColor(0.0, 1.0, 0.0, 1.0);

    // 5.清空canvas
    gl.clear(gl.COLOR_BUFFER_BIT);

    // 6.绘制
    // gl.POINTS: 绘制一系列点。
    // gl.LINE_STRIP: 绘制一个线条。即,绘制一系列线段,上一点连接下一点。
    // gl.LINE_LOOP: 绘制一个线圈。即,绘制一系列线段,上一点连接下一点,并且最后一点与第一个点相连。
    // gl.LINES: 绘制一系列单独线段。每两个点作为端点,线段之间不连接。
    // gl.TRIANGLE_STRIP: 绘制一个三角带。
    // gl.TRIANGLE_FAN: 绘制一个三角扇。
    // gl.TRIANGLES: 绘制一系列三角形。每三个点作为顶点。
    gl.drawArrays(gl.LINE_STRIP, 0, n);
  }

  main();

缓冲区对象

缓冲区对象是webGL系统中的一块内存区域,可以一次性地向缓冲区对象中填充大量的顶点数据,然后将这些数据保存在其中,供顶点着色器使用。

使用缓冲区对象流程

  • b1.创建缓冲区对象(gl.createBuffer())
  • b2.将缓冲区对象绑定到目标(gl.bindBuffer())
  • b3.向缓冲区对象中写入数据(gl.bufferData())
  • b4.将缓冲区对象分配给 attribute 变量(gl.vertexAttribPointer())
  • b5.开启 attribute 变量(gl.enableVertexAttribArray())

gl.createBuffer()

WebGLRenderingContext.createBuffer() 方法可创建并初始化一个用于储存顶点数据或着色数据的 WebGLBuffer 对象

  /**
   * 返回值:一个用于储存顶点数据或着色数据的WebGLBuffer对象
   */
  WebGLBuffer gl.createBuffer();

image.png

gl.bindBuffer()

WebGLRenderingContext.bindBuffer() 方法将给定的 WebGLBuffer 绑定到目标。这个‘目标’表示缓冲区对象的用途。

/**
 * @param target GLenum 指定绑定点(target)。可能的值:
 * gl.ARRAY_BUFFER: 包含顶点属性的Buffer,如顶点坐标,纹理坐标数据或顶点颜色数据。
 * gl.ELEMENT_ARRAY_BUFFER: 用于元素索引的Buffer。
 * @param buffer 要绑定的 WebGLBuffer
 */
void gl.bindBuffer(target, buffer);

image.png

gl.bufferData()

WebGLRenderingContext.bufferData() 方法创建并初始化了 Buffer 对象的数据存储区

/**
 * @param target GLenum 指定Buffer绑定点(目标)。可取以下值:
 * gl.ARRAY_BUFFER: 包含顶点属性的Buffer,如顶点坐标,纹理坐标数据或顶点颜色数据。
 * gl.ELEMENT_ARRAY_BUFFER: 用于元素索引的Buffer。
 * @param srcData 一个ArrayBuffer,SharedArrayBuffer 或者 ArrayBufferView 类型的数组对象,将被复制到Buffer的数据存储区。 如果为null,数据存储区仍会被创建,但是不会进行初始化和定义。
 * @param usage GLenum 表示程序将如何使用存储在缓冲区对象中的数据。该参数将帮助webGL优化操作,但是就算传入了错误的值,也不会终止程序(仅仅是降低程序的效率):
 * gl.STATIC_DRAW: 只会向缓冲区对象中写入一次数据,但需要绘制很多次。
 * gl.DYNAMIC_DRAW: 会向缓冲区对象中多次写入数据,并绘制很多次。
 * gl.STREAM_DRAW: 只会向缓冲区对象中写入一次数据,然后绘制若干次。
 */
void gl.bufferData(target, srcData, usage);

image.png

ArrayBuffer类型化数组

为了绘制三维图形,webGL通常需要同时处理大量相同类型的数据,例如顶点的坐标,为了优化性能,webGL为每种基本数据类型引入了一种特殊的数组(类型化数组

webgl使用的各种类型化数组

数组类型每个元素所占字节数描述
Int8Array18位整数型(signed char)
UInt8Array18位无符号整数型(unsigned char)
Int16Array216位整数型(signed short)
UInt16Array216位无符号整数型(unsigned char)
Int32Array432位整数型(signed int)
UInt32Array432位无符号整数型(unsigned char)
Float32Array4单精度32位浮点数(float)
Float64Array8双精度64位浮点数(double)

类型化数组的方法、属性和常量

方法、属性和常量描述
get(index)获取第index个元素值
set(inex,value)设置第index个元素的值为value
set(array,offset)从第offset个蒜素开始将数组array中的值填充进去数据的长度
length数组的长度
BYTES_PER_ELEMENT数组中每个元素所占的字节数

顶点数据一般使用Float32Array

let vertices = new Float32Array([
  -0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, -0.5,
]);

gl.vertexAttribPointer()

在动态绘制一个点文章中juejin.cn/post/706776… 使用vertexAttrib3f为attribute变量分配值,但是这个方法一次只能向attribute变量分配一个值,而vertexAttribPointer方法可以将整个缓冲区对象分配给attribute变量

WebGLRenderingContext.vertexAttribPointer() 将绑定到gl.ARRAY_BUFFER的缓冲区对象分配给由index指定的attribute变量,告诉显卡从当前绑定的缓冲区(bindBuffer()指定的缓冲区)中读取顶点数据。

/**
 * @param index 指定待分配attribute变量的存储位置。
 * @param size 指定每个顶点属性的组成数量,必须是1,2,3或4。
 * @param type 指定数组中每个元素的数据类型可能是:
 * gl.BYTE: 字节,Int8Array
 * gl.UNSIGNED_BYTE: 无符号字节,Uint8Array
 * gl.SHORT: 短整型,Int16Array
 * gl.UNSIGNED_SHORT: 无符号短整型,Uint16Array
 * gl.INT: 整型,Int32Array
 * gl.UNSIGNED_INT: 无符号整型,Uint32Array
 * gl.FLOAT: 浮点型,Float32Array
 * @param normalized 当转换为浮点数时是否应该将整数数值归一化到特定的范围。
 * 对于类型gl.BYTE和gl.SHORT,如果是true则将值归一化为[-1, 1]
 * 对于类型gl.UNSIGNED_BYTE和gl.UNSIGNED_SHORT,如果是true则将值归一化为[0, 1]
 * 对于类型gl.FLOAT此参数无效
 * @param stride 一个GLsizei,以字节为单位指定连续顶点属性开始之间的偏移量(即数组中一行长度)。不能大于255。如果stride为0,则假定该属性是紧密打包的,即不交错属性,每个属性在一个单独的块中,下一个顶点的属性紧跟当前顶点之后。
 * @param offset GLintptr指定顶点属性数组中第一部分的字节偏移量。必须是类型的字节长度的倍数。
 */
void gl.vertexAttribPointer(index, size, type, normalized, stride, offset);

image.png

gl.enableVertexAttribArray()

WebGLRenderingContext.enableVertexAttribArray() 方法使顶点着色器能够访问缓冲区内的数据(需要手动执行开启)。

在 WebGL 中,作用于顶点的数据会先储存在 attributes。这些数据仅对 JavaScript 代码和顶点着色器可用。属性由索引号引用到 GPU 维护的属性列表中。在不同的平台或 GPU 上,某些顶点属性索引可能具有预定义的值。创建属性时,WebGL 层会分配其他属性。

无论怎样,都需要你使用 enableVertexAttribArray()方法,来激活每一个属性以便使用,不被激活的属性是不会被使用的。一旦激活,以下其他方法就可以获取到属性的值了,包括 vertexAttribPointer(),vertexAttrib(),和 getVertexAttrib()。

/**
 * @param index 类型为GLuint 的索引,指向要激活的顶点属性。如果您只知道属性的名称,不知道索引,您可以使用以下方法来获取索引getAttribLocation().
 */
void gl.enableVertexAttribArray(index);

image.png

gl.drawArrays()

WebGLRenderingContext.drawArrays() 方法用于从向量数组中绘制图元。

  /**
   * @param mode GLenum 类型,指定绘制图元的方式,可能值如下:
   * gl.POINTS: 绘制一系列点。
   * gl.LINE_STRIP: 绘制一个线条。即,绘制一系列线段,上一点连接下一点。
   * gl.LINE_LOOP: 绘制一个线圈。即,绘制一系列线段,上一点连接下一点,并且最后一点与第一个点相连。
   * gl.LINES: 绘制一系列单独线段。每两个点作为端点,线段之间不连接。
   * gl.TRIANGLE_STRIP:绘制一个三角带。
   * gl.TRIANGLE_FAN:绘制一个三角扇。
   * gl.TRIANGLES: 绘制一系列三角形。每三个点作为顶点。
   * @param first GLint 类型,指定从哪个点开始绘制。
   * @param count GLsizei 类型,指定绘制需要使用到多少个点
   */
  void gl.drawArrays(mode, first, count);

图片装配

顶点着色器获取到顶点数据后,执行gl.drawArrays(gl.LINE_STRIP, 0, n)时,当n有几个,顶点着色器就执行几次

image.png

  • 1.修改first = 0,count = 1时,从第一个顶点开始,顶点着色器只会执行1次,所以只会绘制第一个点后就停止了。(只能看到一个点)
  • 2.修改first = 1,count = 1时,从第二个顶点开始,顶点着色器只会执行1次,所以绘制第二个点后就停止了。(只能看到一个点)
  • 3.修改first = 1,count = 2时,从第二个顶点开始,顶点着色器只会执行2次,所以绘制第二个,第三个点后就停止了。(能看到两个点)
  • 4.以此类推...

mode模式绘制的基本图形

image.png

image.png