「译」WebGL 系列 02 着色器基础与点

码农天地 -
「译」WebGL 系列 02 着色器基础与点
原文地址:Day 2. Simple shader and triangle原文作者:Andrei Lesnitsky

这是 WebGL 系列的第2天教程,每天都有新文章发布。

订阅或者加入邮件列表以便及时获取更新内容。

源代码在这里

第1天我们初步了解 WebGL 的功能:计算可渲染区域内的每个像素颜色。但是它究竟如何做到的呢?

WebGL 是与 GPU 协同渲染内容的 API。JavaScript 是由 v8 在 CPU 上执行的,虽然 GPU 无法执行 JavaScript,但仍可以对其编程。

GPU 能够识别 GLSL 语言,我们不仅会熟悉 WebGL API,还会熟悉这种新语言。

GLSL 是一种类似于 C 的编程语言,因此对于 JavaScript 开发人员来说很容易学习和编写。

但是,我们在哪里编写 glsl 代码?如何将其传递给 GPU 以执行?

接下来,我们创建一个新的 js 文件,并获取对 WebGL 渲染上下文的引用

? index.html

    </head>
    <body>
      <canvas></canvas>
-     <script src="./src/canvas2d.js"></script>
+     <script src="./src/webgl-hello-world.js"></script>
    </body>
  </html>

? src/webgl-hello-world.js

const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');

GPU 可执行的程序是通过 WebGL 渲染上下文方法创建的。

? src/webgl-hello-world.js

  const canvas = document.querySelector('canvas');
  const gl = canvas.getContext('webgl');
+ 
+ const program = gl.createProgram();

GPU程序包含两个“功能”,
这些功能称为 shaders
WebGL,支持多种类型的着色器

在这个示例中,我们将使用 vertexfragment 着色器。
两者都可以使用 createShader 方法创建

? src/webgl-hello-world.js

  const gl = canvas.getContext('webgl');
  
  const program = gl.createProgram();
+ 
+ const vertexShader = gl.createShader(gl.VERTEX_SHADER);
+ const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);

现在让我们编写最简单的着色器:

? src/webgl-hello-world.js

  
  const vertexShader = gl.createShader(gl.VERTEX_SHADER);
  const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
+ 
+ const vShaderSource = `
+ void main() {
+     
+ }
+ `;

对于具有一定 C/C++ 经验的人来说,这应该看起来很熟悉。

不像 C 或 C++ 语言的 main 没有返回值,这里的 main 会分配一个值到全局变量 gl_Position

? src/webgl-hello-world.js

  
  const vShaderSource = `
  void main() {
-     
+     gl_Position = vec4(0, 0, 0, 1);
  }
  `;

现在,让我们仔细看一下分配的内容。

着色器中有很多功能。

vec4 函数创建一个由 4 个分量组成的向量。

gl_Position = vec4(0, 0, 0, 1);

看起来很奇怪,我们生活在三维世界中,第四部分到底是什么?是时间吗??

并不是的

引自 MDN

事实证明,这种添加允许使用许多不错的技术来处理 3D 数据。
在经典的笛卡尔坐标系中定义了三维坐标点,附加的第四维将此点更改为齐次坐标。它仍然代表三维空间中的一个点,并且可以通过一对简单的函数,轻松演示如何构造此类坐标。

现在,我们可以忽略第四部分组件,并将其设置为 1.0

好的,我们有一个着色器变量,另一个变量中有着色器源。我们如何连接这两个呢?

? src/webgl-hello-world.js

      gl_Position = vec4(0, 0, 0, 1);
  }
  `;
+ 
+ gl.shaderSource(vertexShader, vShaderSource);

GLSL 着色器应进行编译才能执行:

? src/webgl-hello-world.js

  `;
  
  gl.shaderSource(vertexShader, vShaderSource);
+ gl.compileShader(vertexShader);

可以用检索找到编译结果,此方法返回 compiler 并输出。如果是空字符串,仍然都很好。

? src/webgl-hello-world.js

  
  gl.shaderSource(vertexShader, vShaderSource);
  gl.compileShader(vertexShader);
+ 
+ console.log(gl.getShaderInfoLog(vertexShader));

我们需要对片段着色器执行相同的操作,同时我们实现一个辅助功能,该功能也将用于片段着色器。

? src/webgl-hello-world.js

  }
  `;
  
- gl.shaderSource(vertexShader, vShaderSource);
- gl.compileShader(vertexShader);
+ function compileShader(shader, source) {
+     gl.shaderSource(shader, source);
+     gl.compileShader(shader);
  
- console.log(gl.getShaderInfoLog(vertexShader));
+     const log = gl.getShaderInfoLog(shader);
+ 
+     if (log) {
+         throw new Error(log);
+     }
+ }
+ 
+ compileShader(vertexShader, vShaderSource);

最简单的片段着色器的外观如何?完全相同。

? src/webgl-hello-world.js

  }
  `;
  
+ const fShaderSource = `
+     void main() {
+         
+     }
+ `;
+ 
  function compileShader(shader, source) {
      gl.shaderSource(shader, source);
      gl.compileShader(shader);

片段着色器的计算结果是一种颜色,它也是 4 个分量 (r, g, b, a) 的向量。与 CSS 不同,值在[0..1]范围内,而不是[0..255]。片段着色器的计算结果应分配给变量gl_FragColor

? src/webgl-hello-world.js

  
  const fShaderSource = `
      void main() {
-         
+         gl_FragColor = vec4(1, 0, 0, 1);
      }
  `;
  
  }
  
  compileShader(vertexShader, vShaderSource);
+ compileShader(fragmentShader, fShaderSource);

现在我们应该使用着色器连接 program

? src/webgl-hello-world.js

  
  compileShader(vertexShader, vShaderSource);
  compileShader(fragmentShader, fShaderSource);
+ 
+ gl.attachShader(program, vertexShader);
+ gl.attachShader(program, fragmentShader);

接下来是链接程序。需要在这一阶段来验证顶点着色器和片段着色器是否相互兼容(我们将在后面详细介绍)。

? src/webgl-hello-world.js

  
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
+ 
+ gl.linkProgram(program);

我们的应用程序可能有几个程序,所以我们应该在发出绘图调用之前告诉 gpu 我们要使用哪个程序。

? src/webgl-hello-world.js

  gl.attachShader(program, fragmentShader);
  
  gl.linkProgram(program);
+ 
+ gl.useProgram(program);

好了,我们准备画点东西。

? src/webgl-hello-world.js

  gl.linkProgram(program);
  
  gl.useProgram(program);
+ 
+ gl.drawArrays();

WebGL 可以渲染几种类型的"原语"

点直线三角形

接着我们传递要渲染的原始类型

? src/webgl-hello-world.js

  
  gl.useProgram(program);
  
- gl.drawArrays();
+ gl.drawArrays(gl.POINTS);

有一种方法可以将包含有关图元位置信息的输入数据传递到顶点着色器,因此我们需要将要渲染的第一个原语的索引进行传递。

? src/webgl-hello-world.js

  
  gl.useProgram(program);
  
- gl.drawArrays(gl.POINTS);
+ gl.drawArrays(gl.POINTS, 0);

还有原语的计数

? src/webgl-hello-world.js

  
  gl.useProgram(program);
  
- gl.drawArrays(gl.POINTS, 0);
+ gl.drawArrays(gl.POINTS, 0, 1);

什么都没呈现?
出现什么问题了?

实际上,要渲染点,我们还应该在顶点着色器中指定点大小。

? src/webgl-hello-world.js

  
  const vShaderSource = `
  void main() {
+     gl_PointSize = 20.0;
      gl_Position = vec4(0, 0, 0, 1);
  }
  `;

哇?我们有一个点!

它是在画布的中心渲染的, 因为它的 gl_Positionvec4(0, 0, 0, 1) => x == 0 并且 y == 0
WebGL 坐标系不同于 canvas2d

canvas2d

0.0
-----------------------→ width (px)
|
|
|
↓
height (px)

webgl

                    (0, 1)
                      ↑
                      |
                      |
                      |
(-1, 0) ------ (0, 0)-·---------> (1, 0)
                      |
                      |
                      |
                      |
                    (0, -1)

现在让我们从 JS 传递点坐标,而不是在着色器中对其进行硬编码

顶点着色器的输入数据称为 attribute
让我们定义 position 属性

? src/webgl-hello-world.js

  const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
  
  const vShaderSource = `
+ attribute vec2 position;
+ 
  void main() {
      gl_PointSize = 20.0;
-     gl_Position = vec4(0, 0, 0, 1);
+     gl_Position = vec4(position.x, position.y, 0, 1);
  }
  `;
  

为了用数据填充属性,我们需要获取属性位置。把它当作是 JavaScript 世界中的唯一属性标识符。

? src/webgl-hello-world.js

  
  gl.useProgram(program);
  
+ const positionPointer = gl.getAttribLocation(program, 'position');
+ 
  gl.drawArrays(gl.POINTS, 0, 1);

GPU 仅接受数组类型作为输入,因此我们将定义 Float32Array 作为点位置的存储。

? src/webgl-hello-world.js

  
  const positionPointer = gl.getAttribLocation(program, 'position');
  
+ const positionData = new Float32Array([0, 0]);
+ 
  gl.drawArrays(gl.POINTS, 0, 1);

但是此数组无法按原样传递给 GPU,GPU 应该具有自己的缓冲区。
GPU 世界中存在不同类型的“缓冲区”,在这种情况下,我们需要 ARRAY_BUFFER

? src/webgl-hello-world.js

  
  const positionData = new Float32Array([0, 0]);
  
+ const positionBuffer = gl.createBuffer(gl.ARRAY_BUFFER);
+ 
  gl.drawArrays(gl.POINTS, 0, 1);

要对 GPU 缓冲区进行任何更改,我们需要对其进行“绑定”。绑定缓冲区后,将其视为“current”,并且将对“current”缓冲区执行任何缓冲区修改操作。

? src/webgl-hello-world.js

  
  const positionBuffer = gl.createBuffer(gl.ARRAY_BUFFER);
  
+ gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
+ 
  gl.drawArrays(gl.POINTS, 0, 1);

为了填充一些数据,我们需要调用 bufferData 方法

? src/webgl-hello-world.js

  const positionBuffer = gl.createBuffer(gl.ARRAY_BUFFER);
  
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
+ gl.bufferData(gl.ARRAY_BUFFER, positionData);
  
  gl.drawArrays(gl.POINTS, 0, 1);

为了优化 GPU 端的缓冲区操作(内存管理),我们应该向 GPU 传递“提示”,指示如何使用此缓冲区。有几种使用缓冲区的方法

gl.STATIC_DRAW: 缓冲区的内容很可能经常使用,并且不会经常更改。内容被写入缓冲区,但未被读取。gl.DYNAMIC_DRAW: 缓冲区的内容很可能经常使用并且经常更改。内容被写入缓冲区,但未被读取。gl.STREAM_DRAW: 缓冲区的内容可能不经常使用。内容被写入缓冲区,但未被读取。

使用 WebGL2 上下文时,还可以使用以下值:

gl.STATIC_READ: 缓冲区的内容很可能经常使用,并且不会经常更改。从缓冲区读取内容,但不写入内容。gl.DYNAMIC_READ: 缓冲区的内容很可能经常使用并且经常更改。从缓冲区读取内容,但不写入内容。gl.STREAM_READ: 缓冲区的内容可能不经常使用。从缓冲区读取内容,但不写入内容。gl.STATIC_COPY: 缓冲区的内容很可能经常使用,并且不会经常更改。内容既不由用户写入也不由用户读取。gl.DYNAMIC_COPY: 缓冲区的内容很可能经常使用并且经常更改。内容既不由用户写入也不由用户读取。gl.STREAM_COPY: 缓冲区的内容很可能经常使用,并且不会经常更改。内容既不由用户写入也不由用户读取。

? src/webgl-hello-world.js

  const positionBuffer = gl.createBuffer(gl.ARRAY_BUFFER);
  
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
- gl.bufferData(gl.ARRAY_BUFFER, positionData);
+ gl.bufferData(gl.ARRAY_BUFFER, positionData, gl.STATIC_DRAW);
  
  gl.drawArrays(gl.POINTS, 0, 1);

现在我们需要告诉GPU如何从缓冲区读取数据。

必填信息:

属性大小(如果是 2,则为 vec2,如果是 3,则为 vec3,以此类推)

? src/webgl-hello-world.js

  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, positionData, gl.STATIC_DRAW);
  
+ const attributeSize = 2;
+ 
  gl.drawArrays(gl.POINTS, 0, 1);

缓冲区中的数据类型

? src/webgl-hello-world.js

  gl.bufferData(gl.ARRAY_BUFFER, positionData, gl.STATIC_DRAW);
  
  const attributeSize = 2;
+ const type = gl.FLOAT;
  
  gl.drawArrays(gl.POINTS, 0, 1);

标准化:表明是否应将数据值限制在某个范围内

对于 gl.BYTEgl.SHORT,判断是否在 [-1, 1] ,是则正常。

对于 gl.UNSIGNED_BYTEgl.UNSIGNED_SHORT,判断是否在 [0, 1],是则正常。

对于 gl.FLOATgl.HALF_FLOAT,此参数无效。

? src/webgl-hello-world.js

  
  const attributeSize = 2;
  const type = gl.FLOAT;
+ const nomralized = false;
  
  gl.drawArrays(gl.POINTS, 0, 1);

我们稍后再讨论这两个?

? src/webgl-hello-world.js

  const attributeSize = 2;
  const type = gl.FLOAT;
  const nomralized = false;
+ const stride = 0;
+ const offset = 0;
  
  gl.drawArrays(gl.POINTS, 0, 1);

现在我们需要调用 vertexAttribPointer 以设置 position 属性。

? src/webgl-hello-world.js

  const stride = 0;
  const offset = 0;
  
+ gl.vertexAttribPointer(positionPointer, attributeSize, type, nomralized, stride, offset);
+ 
  gl.drawArrays(gl.POINTS, 0, 1);

让我们尝试更改该点的其它位置

? src/webgl-hello-world.js

  
  const positionPointer = gl.getAttribLocation(program, 'position');
  
- const positionData = new Float32Array([0, 0]);
+ const positionData = new Float32Array([1.0, 0.0]);
  
  const positionBuffer = gl.createBuffer(gl.ARRAY_BUFFER);
  

没什么改变?但是为什么呢?

事实是:默认情况下,所有属性都是禁用的(填充为0),我们需要启用( enable)位置属性。

? src/webgl-hello-world.js

  const stride = 0;
  const offset = 0;
  
+ gl.enableVertexAttribArray(positionPointer);
  gl.vertexAttribPointer(positionPointer, attributeSize, type, nomralized, stride, offset);
  
  gl.drawArrays(gl.POINTS, 0, 1);

现在我们可以渲染更多点!
让我们用点标记画布的每个角落

? src/webgl-hello-world.js

  
  const positionPointer = gl.getAttribLocation(program, 'position');
  
- const positionData = new Float32Array([1.0, 0.0]);
+ const positionData = new Float32Array([
+     -1.0, // point 1 x
+     -1.0, // point 1 y
+ 
+     1.0, // point 2 x
+     1.0, // point 2 y
+ 
+     -1.0, // point 3 x
+     1.0, // point 3 y
+ 
+     1.0, // point 4 x
+     -1.0, // point 4 y
+ ]);
  
  const positionBuffer = gl.createBuffer(gl.ARRAY_BUFFER);
  
  gl.enableVertexAttribArray(positionPointer);
  gl.vertexAttribPointer(positionPointer, attributeSize, type, nomralized, stride, offset);
  
- gl.drawArrays(gl.POINTS, 0, 1);
+ gl.drawArrays(gl.POINTS, 0, positionData.length / 2);

让我们回到着色器

我们不一定需要明确地传递 position.xposition.y 到一个 vec4 构造函数, 这里有一个 vec4(vec2, float, float) 会覆盖。

? src/webgl-hello-world.js

  
  void main() {
      gl_PointSize = 20.0;
-     gl_Position = vec4(position.x, position.y, 0, 1);
+     gl_Position = vec4(position, 0, 1);
  }
  `;
  
  const positionPointer = gl.getAttribLocation(program, 'position');
  
  const positionData = new Float32Array([
-     -1.0, // point 1 x
-     -1.0, // point 1 y
+     -1.0, // top left x
+     -1.0, // top left y
  
      1.0, // point 2 x
      1.0, // point 2 y

现在,通过将每个位置除以 2.0,将所有点移近中心。

? src/webgl-hello-world.js

  
  void main() {
      gl_PointSize = 20.0;
-     gl_Position = vec4(position, 0, 1);
+     gl_Position = vec4(position / 2.0, 0, 1);
  }
  `;
  

结果:

结论

现在,我们对 GPU 和 WebGL 的工作方式有了更好的了解,并且可以呈现比较基本的内容。我们明天将探索更多内容!

作业

使用 Math.cos 渲染带有点的图形
提示:您需要做的就是用有效数值填充 positionData

特别申明:本文内容来源网络,版权归原作者所有,如有侵权请立即与我们联系(cy198701067573@163.com),我们将及时处理。

Tags 标签

加个好友,技术交流

1628738909466805.jpg