乐趣区

关于前端:译WebGL-系列之着色器与三角形

  • 原文地址: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);

Compilation result could be retreived by . This method returns a “compiler” output. If it is an empty string – everyhting is good
能够用检索找到编译后果,此办法返回 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 的工作形式有了更好的理解,并且能够出现十分根本的内容。咱们今天将摸索更多内容!

退出移动版