01 WEBGL 工作原理

WebGL在GPU上究竟做了什么。 WebGL在GPU上的工作基本上分为两部分,第一部分是将顶点(或数据流)转换到裁剪空间坐标, 第二部分是基于第一部分的结果绘制像素点。
当你调用

  1. var primitiveType = gl.TRIANGLES;
  2. var offset = 0;
  3. var count = 9;
  4. gl.drawArrays(primitiveType, offset, count);
  5. // void gl.drawArrays(mode, first, count);
参数 mode
GLenum(en-US) 类型,指定绘制图元的方式,可能值如下。
- gl.POINTS: 绘制一系列点。
- gl.LINE_STRIP: 绘制一个线条。即,绘制一系列线段,上一点连接下一点。
- gl.LINE_LOOP: 绘制一个线圈。即,绘制一系列线段,上一点连接下一点,并且最后一点与第一个点相连。
- gl.LINES: 绘制一系列单独线段。每两个点作为端点,线段之间不连接。
- gl.TRIANGLE_STRIP:绘制一个三角带
- gl.TRIANGLE_FAN:绘制一个三角扇
- gl.TRIANGLES: 绘制一系列三角形。每三个点作为顶点。
first
GLint(en-US) 类型 ,指定从哪个点开始绘制。
new Float32Array([
0, -100,
150, 125,
-175, 100]
)
32位整数的浮点类型;
count
GLsizei(en-US) 类型,指定绘制需要使用到多少个点。
返回
异常
- 如果 mode 不是一个可接受值,将会抛出 gl.INVALID_ENUM 异常。
- 如果 first 或者 count 是负值,会抛出 gl.INVALID_VALUE 异常。
- 如果 gl.CURRENT_PROGRAM 为 null,会抛出 gl.INVALID_OPERATION 异常。

这里的9表示“处理9个顶点”,所以将会有9个顶点被转换。。。。

02 WebGL.drawArrays - 图1
左侧是你提供的数据。顶点着色器(Vertex Shader)是你写进GLSL 中的一个方法,每个顶点调用一次,在这个方法中做一些数学运算后设置了一个特殊的gl_Position变量, 这个变量就是该顶点转换到裁剪空间中的坐标值,GPU接收该值并将其保存起来。

使用它们可以做出非常有趣的东西,但如你所见,到目前为止的例子中, 处理每个像素时片断着色器可用信息很少,幸运的是我们可以给它传递更多信息。 想要从顶点着色器传值到片断着色器,我们可以定义“可变量(varyings)”。

  1. // 定义一个三角形填充到缓冲里
  2. function setGeometry(gl) {
  3. gl.bufferData(
  4. gl.ARRAY_BUFFER,
  5. new Float32Array([
  6. 0, -100,
  7. 150, 125,
  8. -175, 100]),
  9. gl.STATIC_DRAW);
  10. }
  11. // 绘制场景
  12. function drawScene() {
  13. ...
  14. // 绘制几何体
  15. var primitiveType = gl.TRIANGLES;
  16. var offset = 0;
  17. var count = 3;
  18. gl.drawArrays(primitiveType, offset, count);
  19. }
  20. varying vec4 v_color;
  21. ...
  22. void main() {
  23. // 将位置和矩阵相乘
  24. gl_Position = vec4((u_matrix * vec3(a_position, 1)).xy, 0, 1);
  25. // 从裁减空间转换到颜色空间
  26. // 裁减空间范围 -1.0 到 +1.0
  27. // 颜色空间范围 0.0 到 1.0
  28. v_color = gl_Position * 0.5 + 0.5;
  29. }
  30. // 在片断着色器中定义同名varying变量
  31. precision mediump float;
  32. varying vec4 v_color;
  33. void main() {
  34. gl_FragColor = v_color;
  35. }

02 数据渲染

  1. "use strict";
  2. function main() {
  3. // Get A WebGL context
  4. /** @type {HTMLCanvasElement} */
  5. var canvas = document.querySelector("#canvas");
  6. var gl = canvas.getContext("webgl");
  7. if (!gl) {
  8. return;
  9. }
  10. // setup GLSL program
  11. var program = webglUtils.createProgramFromScripts(gl, ["vertex-shader-2d", "fragment-shader-2d"]);
  12. // look up where the vertex data needs to go.
  13. var positionAttributeLocation = gl.getAttribLocation(program, "a_position");
  14. // lookup uniforms
  15. var matrixLocation = gl.getUniformLocation(program, "u_matrix");
  16. // Create a buffer.
  17. var positionBuffer = gl.createBuffer();
  18. gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  19. // Set Geometry.
  20. setGeometry(gl);
  21. var translation = [200, 150];
  22. var angleInRadians = 0;
  23. var scale = [1, 1];
  24. drawScene();
  25. // Setup a ui.
  26. webglLessonsUI.setupSlider("#x", {value: translation[0], slide: updatePosition(0), max: gl.canvas.width });
  27. webglLessonsUI.setupSlider("#y", {value: translation[1], slide: updatePosition(1), max: gl.canvas.height});
  28. webglLessonsUI.setupSlider("#angle", {slide: updateAngle, max: 360});
  29. webglLessonsUI.setupSlider("#scaleX", {value: scale[0], slide: updateScale(0), min: -5, max: 5, step: 0.01, precision: 2});
  30. webglLessonsUI.setupSlider("#scaleY", {value: scale[1], slide: updateScale(1), min: -5, max: 5, step: 0.01, precision: 2});
  31. function updatePosition(index) {
  32. return function(event, ui) {
  33. translation[index] = ui.value;
  34. drawScene();
  35. };
  36. }
  37. function updateAngle(event, ui) {
  38. var angleInDegrees = 360 - ui.value;
  39. angleInRadians = angleInDegrees * Math.PI / 180;
  40. drawScene();
  41. }
  42. function updateScale(index) {
  43. return function(event, ui) {
  44. scale[index] = ui.value;
  45. drawScene();
  46. };
  47. }
  48. // Draw the scene.
  49. function drawScene() {
  50. webglUtils.resizeCanvasToDisplaySize(gl.canvas);
  51. // Tell WebGL how to convert from clip space to pixels
  52. gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  53. // Clear the canvas.
  54. gl.clear(gl.COLOR_BUFFER_BIT);
  55. // Tell it to use our program (pair of shaders)
  56. gl.useProgram(program);
  57. // Turn on the attribute
  58. gl.enableVertexAttribArray(positionAttributeLocation);
  59. // Bind the position buffer.
  60. gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  61. // Tell the attribute how to get data out of positionBuffer (ARRAY_BUFFER)
  62. var size = 2; // 2 components per iteration
  63. var type = gl.FLOAT; // the data is 32bit floats
  64. var normalize = false; // don't normalize the data
  65. var stride = 0; // 0 = move forward size * sizeof(type) each iteration to get the next position
  66. var offset = 0; // start at the beginning of the buffer
  67. gl.vertexAttribPointer(
  68. positionAttributeLocation, size, type, normalize, stride, offset);
  69. // Compute the matrix
  70. var matrix = m3.projection(gl.canvas.clientWidth, gl.canvas.clientHeight);
  71. matrix = m3.translate(matrix, translation[0], translation[1]);
  72. matrix = m3.rotate(matrix, angleInRadians);
  73. matrix = m3.scale(matrix, scale[0], scale[1]);
  74. // Set the matrix.
  75. gl.uniformMatrix3fv(matrixLocation, false, matrix);
  76. // Draw the geometry.
  77. var primitiveType = gl.TRIANGLES;
  78. var offset = 0;
  79. var count = 3;
  80. gl.drawArrays(primitiveType, offset, count);
  81. }
  82. }
  83. // Fill the buffer with the values that define a triangle.
  84. // Note, will put the values in whatever buffer is currently
  85. // bound to the ARRAY_BUFFER bind point
  86. function setGeometry(gl) {
  87. gl.bufferData(
  88. gl.ARRAY_BUFFER,
  89. new Float32Array([
  90. 0, -100,
  91. 150, 125,
  92. -175, 100]),
  93. gl.STATIC_DRAW);
  94. }
  95. main();

回想一下,我们只计算了三个顶点,调用了三次顶点着色器,所以也只计算出了三个颜色值, 但是我们的三角形却有很多颜色,这就是称之为可变量的varying的原因

image.png
我们的给顶点着色器施加了一个包含平移,旋转和缩放的的矩阵,并将结果转换到裁剪空间。 默认平移,旋转和缩放值为:平移 = 200, 150,旋转 = 0,缩放 = 1,所以这里只进行了平移。 画布大小(背景缓冲)为 400×300,所以三个顶点在裁剪空间中为以下坐标值。

想要给片断着色器传值,我们可以先把值传递给顶点着色器然后再传给片断着色器。 让我们来画一个由两个不同颜色三角形组成的矩形。我们需要给顶点着色器添加一个属性值, 把值通过属性传递给它后它再直接传递给片断着色器。

  1. attribute vec2 a_position;
  2. attribute vec4 a_color;
  3. ...
  4. varying vec4 v_color;
  5. void main() {
  6. ...
  7. // 直接把属性值中的数据赋给可变量
  8. v_color = a_color;
  9. }
  10. // 寻找顶点着色器中需要的数据
  11. var positionLocation = gl.getAttribLocation(program, "a_position");
  12. var colorLocation = gl.getAttribLocation(program, "a_color");
  13. ...
  14. // 给颜色数据创建一个缓冲
  15. var colorBuffer = gl.createBuffer();
  16. gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
  17. // 设置颜色
  18. setColors(gl);
  19. ...
  20. // 给矩形的两个三角形
  21. // 设置颜色值并发到缓冲
  22. function setColors(gl) {
  23. // 生成两个随机颜色
  24. var r1 = Math.random();
  25. var b1 = Math.random();
  26. var g1 = Math.random();
  27. var r2 = Math.random();
  28. var b2 = Math.random();
  29. var g2 = Math.random();
  30. gl.bufferData(
  31. gl.ARRAY_BUFFER,
  32. new Float32Array(
  33. [ r1, b1, g1, 1,
  34. r1, b1, g1, 1,
  35. r1, b1, g1, 1,
  36. r2, b2, g2, 1,
  37. r2, b2, g2, 1,
  38. r2, b2, g2, 1]),
  39. gl.STATIC_DRAW);
  40. }
  41. gl.enableVertexAttribArray(colorLocation);
  42. // 绑定颜色缓冲
  43. gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer);
  44. // 告诉颜色属性怎么从 colorBuffer (ARRAY_BUFFER) 中读取颜色值
  45. var size = 4; // 每次迭代使用4个单位的数据
  46. var type = gl.FLOAT; // 单位数据类型是32位的浮点型
  47. var normalize = false; // 不需要归一化数据
  48. var stride = 0; // 0 = 移动距离 * 单位距离长度sizeof(type)
  49. // 每次迭代跳多少距离到下一个数据
  50. var offset = 0; // 从绑定缓冲的起始处开始
  51. gl.vertexAttribPointer(
  52. colorLocation, size, type, normalize, stride, offset)
  53. // 画几何体
  54. var primitiveType = gl.TRIANGLES;
  55. var offset = 0;
  56. var count = 6;
  57. gl.drawArrays(primitiveType, offset, count);

03 vertexAttribPointer 中的 normalizeFlag

标准化标记(normalizeFlag)适用于所有非浮点型数据。如果传递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 精确度高。
最常用的是标准化颜色数据。大多数情况颜色值范围为 0.0 到 +1.0。 使用4个浮点型数据存储红,绿,蓝和阿尔法通道数据时,每个顶点的颜色将会占用16字节空间, 如果你有复杂的几何体将会占用很多内存。代替的做法是将颜色数据转换为四个 UNSIGNED_BYTE , 其中 0 表示 0.0,255 表示 1.0。现在每个顶点只需要四个字节存储颜色值,省了 75% 空间。
我们来修改之前代码实现。当我们告诉WebGL如何获取颜色数据时将这样

  1. // 告诉颜色属性如何从colorBuffer中提取数据 (ARRAY_BUFFER)
  2. var size = 4; // 每次迭代使用四个单位数据
  3. var type = gl.UNSIGNED_BYTE; // 数据类型是8位的 UNSIGNED_BYTE 类型。***************
  4. var normalize = true; // 标准化数据
  5. var stride = 0; // 0 = 移动距离 * 单位距离长度sizeof(type)
  6. // 每次迭代跳多少距离到下一个数据
  7. var offset = 0; // 从缓冲的起始处开始
  8. gl.vertexAttribPointer(
  9. colorLocation, size, type, normalize, stride, offset)
  10. // 给矩形的两个三角形
  11. // 设置颜色值并发到缓冲
  12. function setColors(gl) {
  13. // 设置两个随机颜色
  14. var r1 = Math.random() * 256; // 0 到 255.99999 之间
  15. var b1 = Math.random() * 256; // 这些数据
  16. var g1 = Math.random() * 256; // 在存入缓冲时
  17. var r2 = Math.random() * 256; // 将被截取成
  18. var b2 = Math.random() * 256; // Uint8Array 类型
  19. var g2 = Math.random() * 256;
  20. gl.bufferData(
  21. gl.ARRAY_BUFFER,
  22. new Uint8Array( // Uint8Array
  23. [ r1, b1, g1, 255,
  24. r1, b1, g1, 255,
  25. r1, b1, g1, 255,
  26. r2, b2, g2, 255,
  27. r2, b2, g2, 255,
  28. r2, b2, g2, 255]),
  29. gl.STATIC_DRAW);
  30. }