WebGL 工作原理和代码执行流程

405 阅读5分钟

WebGL 在 GPU 上的工作可以总结:

  1. 第一步是将顶点(或数据流)转换到裁剪空间坐标;
  2. 第二步是基于第一部分的结果绘制像素点。
  3. 然后每一帧运行都会重复第1、2步

一、WebGL 工作原理

WebGL 绘图主要是发生在顶点着色器和片元着色器中,每一帧都会把所有的顶点和片元重新计算运行一遍,当许多帧连贯运行起来,就会看到顶点和颜色的动态变化效果。

顶点着色器运行原理

顶点着色器的作用是计算没一个点的位置。当执行gl.drawArrays(gl.TRIANGLES, 0, 9),GPU 会执行顶点着色器代码,依次处理每一个顶点,gl_Position是顶点着色器内置的一个变量,处理之后的坐标必须要赋值给它。

WebGL 会从 Buffer 中读取,读取之后可以进行一系列加减乘除数学运算,实现平移、旋转、缩放等变换操作,然后赋值给gl_Position,进而实现顶点位置的定制。

下面这个 gif 动图示意了顶点着色器对每一个顶点的处理结果:每一个顶点的原始坐标乘以一个矩阵后得到新的坐标,赋值给gl_Position,原顶点就被转换到一个新的坐标位置。

片元着色器运行原理

片元着色器的任务是计算每个像素片元的颜色。在顶点着色器执行完成之后,会进行光栅化操作,然后执行片元着色器代码,片元着色器通过给gl_FragColor赋值,处理每一个片元。

gl_FragColor的值可以是顶点着色器使用varying传过来的,也可以是一个固定值。通过对gl_FragColor进行一系列加减乘除运算得到不同的颜色值,来绘制不同的片元颜色。

下面的 gif 动图示意了片元着色器运行的时候对每一个片元的处理结果:其中v_color是在顶点着色器里处传进来赋值给gl_FragColor,在顶点着色器里根据顶点位置进行了加减乘除运算,所以看起来每个位置的颜色都是不一样的。

需要注意的是:

WebGL 里的空间坐标默认是在 -1~1 之间,超出的会被裁剪;

颜色 RGBA 值每一个分量的范围是 0~1 之间;

二、WebGL 应用的整体流程

上面是大致的运行原理,具体到代码上,WebGL 会首先从 HTMLCanvasElement 对象中获取到执行环境(也叫上下文),之后设置上下文的状态和属性,然后创建着色器、绑定数据和渲染,代码结构如下图:

1、创建两个着色器:createShader —> shaderSource —> compileShader

2、创建着色器程序:createProgram —> attachShader —> linkProgram —> useProgram

3、创建 Buffer 数据:createBuffer —> bindBuffer —> bufferData

4、读取数据并渲染:getAttribLocation —> enableVertexAttribArray —> vertexAttribPointer —> drawArrays

看个画三角形的🌰

function render() {
  // -------------------------------------------准备环境------------------------------------------------
  let canvas = document.getElementById('canvas') as HTMLCanvasElement;
  let gl = canvas.getContext('webgl2') as WebGLRenderingContext;
  canvas.width = canvas.clientWidth;
  canvas.height = canvas.clientHeight;
  gl.viewport(0, 0, canvas.width, canvas.height);

  // -------------------------------------------准备数据------------------------------------------------
  // 三个二维点坐标
  let positions = [-0.5, -0.5, 0, 0, 0.5, -0.5];
  // 创建一个缓冲区对象
  let positionBuffer = gl.createBuffer();
  // 把刚才创建的缓冲区对象绑定到gl的 ARRAY_BUFFER,设置为当前默认的缓冲区,后面的所有的数据都会都会被放入当前缓冲区,直到bindBuffer绑定另一个当前缓冲区。
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  // 将数据存放放到缓冲区,gl.STATIC_DRAW 指定数据存储区的使用方法:缓存区的内容可能会经常使用,但是不会更改。
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);

  // -------------------------------------------绘制准备------------------------------------------------
  // 编写 GLSL shaders 代码,是个字符串
  let vertexShaderSource = `
      attribute vec4 a_position;// 一个属性变量,将会从缓冲中获取数据

      // 所有着色器都有一个main方法
      void main() {
        gl_Position = a_position; //  gl_Position 是顶点着色器内置的默认变量,将buffer取到的数据赋值进去
      }
	`;
  let fragmentShaderSource = `
      // 片元着色器没有默认精度,所以我们需要设置一个精度
      precision mediump float; // mediump 是一个不错的默认值,代表 中等精度 

      // 所有着色器都有一个main方法
      void main() {
        gl_FragColor = vec4(1, 0, 0.5, 1); // gl_FragColor 是片元着色器内置的变量,赋值粉色
      }
  `;
  
  // 创建着色器方法,输入参数:渲染上下文,着色器类型,数据源
  let vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource)!;
  let fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource)!;

  // 创建着色器程序
  let program = createProgram(gl, vertexShader, fragmentShader)!;
  // 告诉它用我们之前写好的着色程序(一个着色器对)
  gl.useProgram(program);
  
  // 获取给定 program 对象中某属性的下标指向位置。
  let positionAttributeLocation = gl.getAttribLocation(program, 'a_position');
  // 激活每一个属性以便使用
  gl.enableVertexAttribArray(positionAttributeLocation);

  let size = 2; // 每次迭代运行提取两个单位数据
  let type = gl.FLOAT; // 每个单位的数据类型是32位浮点型
  let normalize = false; // 不需要归一化数据
  let stride = 0; // 0 = 移动单位数量 * 每个单位占用内存(sizeof(type)),每次迭代运行运动多少内存到下一个数据开始点
  let offset = 0; // 从缓冲起始位置开始读取
  // 告诉显卡从当前绑定的缓冲区(bindBuffer()指定的缓冲区)中读取数据
  gl.vertexAttribPointer(positionAttributeLocation, size, type, normalize, stride, offset);

  // -------------------------------------------渲染------------------------------------------------
  let primitiveType = gl.TRIANGLES;
  let offSet = 0;
  let count = 3;
  gl.drawArrays(primitiveType, offSet, count);
}

// 创建着色器
function createShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | undefined {
  let shader: WebGLShader = gl.createShader(type)!; // 创建着色器对象
  gl.shaderSource(shader, source); // 提供数据源
  gl.compileShader(shader); // 编译 -> 生成着色器
  let success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
  if (success) {
    return shader;
  }

  console.log(gl.getShaderInfoLog(shader));
  gl.deleteShader(shader);
}

// 创建着色器程序
function createProgram(gl: WebGLRenderingContext, vertexShader: WebGLShader, fragmentShader: WebGLShader): WebGLProgram | undefined {
  let program: WebGLProgram = gl.createProgram()!;
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);
  let success = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (success) {
    return program;
  }

  console.log(gl.getProgramInfoLog(program));
  gl.deleteProgram(program);
}

需要注意的是,寻找属性值位置(和全局属性位置)应该在初始化的时候完成,而不是在渲染循环中。

WebGL会在光栅化的时候把点位从裁剪空间转换到屏幕空间,最终在屏幕空间绘制一个三角形, 如果画布大小是 400×300 我们会得到类似以下的转换。

这是WebGL 系列的入门文章,免费订阅,如有帮助请点赞收藏,纰漏之处欢迎指正!

参考文档:

zhuanlan.zhihu.com/p/409017167

qrcode_for_gh_3695c3ae18f4_258.jpg

如有帮助请点赞收藏,纰漏之处欢迎指正! 也欢迎关注公众号交流知识哇😄