WebGL2的基本原理

作者:renzp94
时间:2023-11-30 13:10:12

此系列是对学习WebGL2理论基础的记录,想查看全部知识的可查看原文进行学习。

基础概念

WebGL仅仅是栅格化引擎,它会基于代码来画点、线条和三角形。通过复杂组合最终形成复杂的3D效果。WebGL是在GPU上运行的,其代码是一对函数的形式,分别是:点着色器(Vertex Shader)片段着色器(Fragment Shader)。他们是一种类似C++的强类型语言GLSL编写的,一对函数组合叫做程序(Program)

点着色器的任务是计算点的位置。基于函数输出的位置,WebGL能狗栅格化不同种类的基本元素,如:点、线条、三角形。在栅格化这些基本元素的同时也会调用片段着色器,它的任务主要是计算当前正在绘制图形的每个像素的颜色。所有的WebGL API都是为这些函数对的运行来设置状态。通过设置一堆状态然后再调用gl.drawArraysgl.drawElements在GPU上运行你的着色器。

着色器有如下四种方法接收数据:

  • 属性(Attributes)缓冲区(Buffers)顶点数组(Vertex Arrays)

    缓存区以二进制数据形式的数组传给GPU。缓存区可以放任意数据,通常有位置,归一化参数,纹理坐标,顶点颜色等等

    属性用来指定数据如何从缓冲区获取并提供给顶点着色器。比如你可能将位置信息以3个32位的浮点数据存在缓存区中, 一个特定的属性包含的信息有:它来自哪个缓存区,它的数据类型(3个32位浮点数据),在缓存区的起始偏移量,从一个位置到下一个位置有多少个字节等等。

    缓冲区并非随机访问的,而是将顶点着色器执行指定次数。每次执行时,都会从每个指定的缓冲区中提取下一个值并分配给一个属性。

    属性的状态收集到一个顶点数组对象(VAO)中,该状态作用在每个缓冲区,以及如何从这些缓冲区中提取数据。

  • Uniforms

    Uniforms是在执行着色器程序前设置的全局变量

  • 纹理(Textures)

    纹理是能够在着色器程序中随机访问的数组数据。大多数情况下纹理存储图片数据,但它也用于包含颜色以为的数据。

  • Varyings

    Varyings是一种从点着色器到片段着色器传递数据的方法。根据显示的内容如点,线或三角形, 顶点着色器在Varyings中设置的值,在运行片段着色器的时候会被解析。

代码列子

WebGL只关注两件事:剪辑空间坐标和颜色,所以只需要编写两个着色器代码:点着色器提供剪辑空间坐标,片段着色器提供颜色即可。需要注意的是,剪辑空间坐标的取值范围是-1 ~ 1。

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>WebGL</title>
</head>
<body>
  <canvas id="gl" height="400px" width="400px" style="background-color: #ccc;"></canvas>
  <script type="module" src="./index.js"></script>
</body>
</html>

index.js

const main = () => {
  // 获取canvas元素
  const canvas = document.querySelector('#gl')
  // 开启webgl2
  const gl = canvas.getContext("webgl2")
  // 需要注意的是:
  // #version 300 es 必须位于着色器代码的第一行。 它前面不允许有任何的注释或空行!
  //  #version 300 es 的意思是你想要使用WebGL2的着色器语法:GLSL ES 3.00。 如果你没有把它放到第一行,将默认设置为GLSL ES 1.00,即WebGL1.0的语法。相比WebGL2的语法,会少很多特性。

  // 顶点着色器GLSL代码
  const vertexShaderSource = `#version 300 es
  
  // an attribute is an input (in) to a vertex shader.
  // It will receive data from a buffer
  in vec4 a_position;
  
  // all shaders have a main function
  void main() {
  
    // gl_Position is a special variable a vertex shader
    // is responsible for setting
    gl_Position = a_position;
  }
  `
  // 片段着色器GLSL代码
  const fragmentShaderSource = `#version 300 es
  
  // fragment shaders don't have a default precision so we need
  // to pick one. highp is a good default. It means "high precision"
  precision highp float;
  
  // we need to declare an output for the fragment shader
  out vec4 outColor;
  
  void main() {
    // Just set the output to a constant reddish-purple
    outColor = vec4(1, 0, 0.5, 1);
  }
  `
  // 创建点着色器
  var vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
  // 创建片段着色器
  var fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
  // 创建程序并链接两个着色器
  var program = createProgram(gl, vertexShader, fragmentShader);
  // 查找a_position属性位置
  var positionAttributeLocation = gl.getAttribLocation(program, "a_position");
  // 创建缓存区,用于存放属性
  var positionBuffer = gl.createBuffer();
  // 绑定缓冲区
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  // 将点放入缓冲区
  var positions = [
    0, 0,
    0, 0.5,
    0.7, 0,
  ];
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
  // 创建点属性集合
  var vao = gl.createVertexArray();
  // 绑定集合
  gl.bindVertexArray(vao);
  // 启用属性
  gl.enableVertexAttribArray(positionAttributeLocation);

  // 从缓冲期中取出数据
  var size = 2;          // 2 components per iteration
  var type = gl.FLOAT;   // the data is 32bit floats
  var normalize = false; // don't normalize the data
  var stride = 0;        // 0 = move forward size * sizeof(type) each iteration to get the next position
  var offset = 0;        // start at the beginning of the buffer
  gl.vertexAttribPointer(
      positionAttributeLocation, size, type, normalize, stride, offset)
  // 设置视窗大小
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  // 运行着色器程序
  gl.useProgram(program);
  // 绑定点属性集合
  gl.bindVertexArray(vao);
  // 运行GLSL程序
  var primitiveType = gl.TRIANGLES;
  var offset = 0;
  var count = 3;
  gl.drawArrays(primitiveType, offset, count);
}

function createShader(gl, type, source) {
  var shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
  if (success) {
    return shader;
  }
 
  console.log(gl.getShaderInfoLog(shader));
  gl.deleteShader(shader);
}

function createProgram(gl, vertexShader, fragmentShader) {
  var program = gl.createProgram();
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);
  var success = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (success) {
    return program;
  }
 
  console.log(gl.getProgramInfoLog(program));
  gl.deleteProgram(program);
}

main()

代码运行如下:

demo

上述代码是使用坐标指定的,如果要根据像素指定的话可以参考下面的代码

  // 获取canvas元素
  const canvas = document.querySelector('#gl')
  // 开启webgl2
  const gl = canvas.getContext("webgl2")
  // 顶点着色器GLSL代码
  var vertexShaderSource = `#version 300 es
  // an attribute is an input (in) to a vertex shader.
  // It will receive data from a buffer
  in vec2 a_position;
  
  // Used to pass in the resolution of the canvas
  uniform vec2 u_resolution;
  
  // all shaders have a main function
  void main() {
  
    // convert the position from pixels to 0.0 to 1.0
    vec2 zeroToOne = a_position / u_resolution;
  
    // convert from 0->1 to 0->2
    vec2 zeroToTwo = zeroToOne * 2.0;
  
    // convert from 0->2 to -1->+1 (clipspace)
    vec2 clipSpace = zeroToTwo - 1.0;
  
    gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
  }
  `;

  // 片段着色器GLSL代码
  const fragmentShaderSource = `#version 300 es
  
  // fragment shaders don't have a default precision so we need
  // to pick one. highp is a good default. It means "high precision"
  precision highp float;
  
  // we need to declare an output for the fragment shader
  out vec4 outColor;
  
  void main() {
    // Just set the output to a constant reddish-purple
    outColor = vec4(1, 0, 0.5, 1);
  }
  `
  // 创建点着色器
  var vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
  // 创建片段着色器
  var fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
  // 创建程序并链接两个着色器
  var program = createProgram(gl, vertexShader, fragmentShader);
  // 查找a_position属性位置
  var positionAttributeLocation = gl.getAttribLocation(program, "a_position");
  // 查找u_resolution属性位置
  var resolutionUniformLocation = gl.getUniformLocation(program, "u_resolution");
  // 创建缓存区,用于存放属性
  var positionBuffer = gl.createBuffer();
  // 绑定缓冲区
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  // 将点放入缓冲区
  var positions = [
   10, 20,
   80, 20,
   10, 30,
   10, 30,
   80, 20,
   80, 30,
  ;
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
  // 创建点属性集合
  var vao = gl.createVertexArray();
  // 绑定集合
  gl.bindVertexArray(vao);
  // 启用属性
  gl.enableVertexAttribArray(positionAttributeLocation);

  // 从缓冲期中取出数据
  var size = 2;          // 2 components per iteration
  var type = gl.FLOAT;   // the data is 32bit floats
  var normalize = false; // don't normalize the data
  var stride = 0;        // 0 = move forward size * sizeof(type) each iteration to get the next position
  var offset = 0;        // start at the beginning of the buffer
  gl.vertexAttribPointer(
      positionAttributeLocation, size, type, normalize, stride, offset)
  // 设置视窗大小
  gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  // 运行着色器程序
  gl.useProgram(program);
  // 绑定点属性集合
  gl.bindVertexArray(vao);
  gl.uniform2f(resolutionUniformLocation, gl.canvas.width, gl.canvas.height)
  // 运行GLSL程序
  var primitiveType = gl.TRIANGLES;
  var offset = 0;
  var count = 6;
  gl.drawArrays(primitiveType, offset, count);

demo

总结

  • WebGL是一个栅格化引擎,只负责绘制点、线条、三角形。复杂的图形需要自己去组合绘制
  • WebGL代码是以一对函数的形式运行。分别是点着色器(Vertex Shader)片段着色器(Fragment Shader)
  • 点着色器(Vertex Shader)是计算点的位置,从而绘制出图形
  • 片段着色器(Fragment Shader)是绘制图形颜色
  • 着色器接收数据的方法有:属性(Attributes),缓冲区(Buffers)和顶点数组(Vertex Arrays)Uniforms纹理(Textures)Varyings
  • WebGL的剪辑空间坐标范围是-1 ~ 1