webgl顶点着色器

2023/8/22 webgl3D

在工作原理中我们提到,WebGL每次绘制需要两个着色器, 一个顶点着色器和一个片段着色器,每一个着色器都是一个方法。 一个顶点着色器和一个片段着色器链接在一起放入一个着色程序中(或者只叫程序)。 一个典型的WebGL应用会有多个着色程序。

🌙 顶点着色器 Vertex Shader

一个顶点着色器的工作是生成裁剪空间坐标值,通常是以下的形式:

void main() {
    // 坐标计算
   gl_Position = doMathToMakeClipspaceCoordinates;
}
1
2
3
4

每个顶点调用一次(顶点)着色器,每次调用都需要设置一个特殊的全局变量gl_Position, 该变量的值就是裁减空间坐标值。

顶点着色器需要的数据,可以通过以下三种方式获得:

🌙 Attributes 属性

最常用的方法是缓冲和属性,创建缓冲:

// 创建缓冲
var buf = gl.createBuffer();

// 将数据存入缓冲
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, someData, gl.STATIC_DRAW);
1
2
3
4
5
6

初始化时,在着色程序中找到属性所在地址, 比如:

var positionLoc = gl.getAttribLocation(someShaderProgram, "a_position");
1

在渲染的时候告诉WebGL怎么从缓冲中获取数据传递给属性:

// 开启从缓冲中获取数据
gl.enableVertexAttribArray(positionLoc);
 
var numComponents = 3;  // (x, y, z)
var type = gl.FLOAT;    // 32位浮点数据
var normalize = false;  // 不标准化
var offset = 0;         // 从缓冲起始位置开始获取
var stride = 0;         // 到下一个数据跳多少位内存
                        // 0 = 使用当前的单位个数和单位长度 ( 3 * Float32Array.BYTES_PER_ELEMENT )
 
gl.vertexAttribPointer(positionLoc, numComponents, type, false, stride, offset);
1
2
3
4
5
6
7
8
9
10
11

不做任何运算直接将数据传递给gl_Position:

// 属性可以用 float, vec2, vec3, vec4, mat2, mat3 和 mat4 数据类型。
attribute vec4 a_position;
 
void main() {
   gl_Position = a_position;
}
1
2
3
4
5
6

🌙 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如何获取颜色数据时将这样:

 // 告诉颜色属性如何从colorBuffer中提取数据 (ARRAY_BUFFER)
  var size = 4;                 // 每次迭代使用四个单位数据
  var type = gl.UNSIGNED_BYTE;  // 数据类型是8位的 UNSIGNED_BYTE 类型。
  var normalize = true;         // 标准化数据
  var stride = 0;               // 0 = 移动距离 * 单位距离长度sizeof(type) 
                                // 每次迭代跳多少距离到下一个数据
  var offset = 0;               // 从缓冲的起始处开始
  gl.vertexAttribPointer(
      colorLocation, size, type, normalize, stride, offset)
1
2
3
4
5
6
7
8
9

如下向缓冲添加数据:

// 给矩形的两个三角形
// 设置颜色值并发到缓冲
function setColors(gl) {
  // 设置两个随机颜色
  var r1 = Math.random() * 256; // 0 到 255.99999 之间
  var b1 = Math.random() * 256; // 这些数据
  var g1 = Math.random() * 256; // 在存入缓冲时
  var r2 = Math.random() * 256; // 将被截取成
  var b2 = Math.random() * 256; // Uint8Array 类型
  var g2 = Math.random() * 256;
 
  gl.bufferData(
      gl.ARRAY_BUFFER,
      new Uint8Array(   // Uint8Array
        [ r1, b1, g1, 255,
          r1, b1, g1, 255,
          r1, b1, g1, 255,
          r2, b2, g2, 255,
          r2, b2, g2, 255,
          r2, b2, g2, 255]),
      gl.STATIC_DRAW);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

结果如下:

See the Pen WebGL - Fundamentals by Keekuun (@keekuun) on CodePen.

🌙 Uniforms 全局变量

全局变量在一次绘制过程中传递给着色器的值都一样,在下面的一个简单的例子中, 用全局变量给顶点着色器添加了一个偏移量

attribute vec4 a_position;
uniform vec4 u_offset;

void main() {
    gl_position = a_position + u_offset;
}
1
2
3
4
5
6

在初始化时找到全局变量的地址:

var offsetLoc = gl.getUniformLocation(yourProgram, "u_offset");
// 设置全局变量
gl.uniform4fv(offsetLoc, [1, 0, 0, 0]); // 向右偏移一半屏幕宽度
1
2
3

要注意的是全局变量属于单个着色程序,如果多个着色程序有同名全局变量,需要找到每个全局变量并设置自己的值。

一个数组可以一次设置所有的全局变量:

// 着色器里
// uniform vec2 u_someVec2[3];
 
// JavaScript 初始化时
var someVec2Loc = gl.getUniformLocation(someProgram, "u_someVec2");
 
// 渲染的时候
gl.uniform2fv(someVec2Loc, [1, 2, 3, 4, 5, 6]);  // 设置数组 u_someVec2
1
2
3
4
5
6
7
8

如果你想单独设置数组中的某个值,就要单独找到该值的地址:

// JavaScript 初始化时
var someVec2Element0Loc = gl.getUniformLocation(someProgram, "u_someVec2[0]");
var someVec2Element1Loc = gl.getUniformLocation(someProgram, "u_someVec2[1]");
var someVec2Element2Loc = gl.getUniformLocation(someProgram, "u_someVec2[2]");
 
// 渲染的时候
gl.uniform2fv(someVec2Element0Loc, [1, 2]);  // set element 0
gl.uniform2fv(someVec2Element1Loc, [3, 4]);  // set element 1
gl.uniform2fv(someVec2Element2Loc, [5, 6]);  // set element 2
1
2
3
4
5
6
7
8
9

如果是结构体:

struct SomeStruct {
  bool active;
  vec2 someVec2;
};
uniform SomeStruct u_someThing;
1
2
3
4
5

那么:

var someThingActiveLoc = gl.getUniformLocation(someProgram, "u_someThing.active");
var someThingSomeVec2Loc = gl.getUniformLocation(someProgram, "u_someThing.someVec2");
1
2