# 着色器

JS

  • getAttribLocation enableVertexAttribArray vertexAttribPointer
  • getUniformLocation
  • uniform1i uniform2f
  • uniform1fv uniform2fv uniform3fv uniform4fv
  • createBuffer bindBuffer bufferData
  • createTexture bindTexture texImage2D texParameteri activeTexture

GLSL

  • attribute uniform varying
  • vec4 vec2
  • texture2D

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

一个典型的WebGL应用会有多个着色程序。

# 顶点着色器

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

void main() {
   gl_Position = doMathToMakeClipspaceCoordinates
}

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

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

  • Attributes 属性 (从缓冲中获取的数据)
  • Uniforms 全局变量 (在一次绘制中对所有顶点保持一致值)
  • Textures 纹理 (从像素或纹理元素中获取的数据)

# Attributes

attribute vec4 a_position;
 
void main() {
   gl_Position = a_position;
}
const buf = gl.createBuffer();
const someData = []
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, someData, gl.STATIC_DRAW);
const positionLoc = gl.getAttribLocation(someShaderProgram, "a_position");
// 开启从缓冲中获取数据
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);

# Uniforms

attribute vec4 a_position;
uniform vec4 u_offset;
 
void main() {
   gl_Position = a_position + u_offset;
}
// 首先在初始化时找到全局变量的地址
const offsetLoc = gl.getUniformLocation(someProgram, "u_offset");
// 然后在绘制前设置全局变量
gl.uniform4fv(offsetLoc, [1, 0, 0, 0]);  // 向右偏移一半屏幕宽度

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

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

单独设置数组中的某个值

const someVec2Element0Loc = gl.getUniformLocation(someProgram, "u_someVec2[0]");

单独设置数组中的某个值

struct SomeStruct {
  bool active;
  vec2 someVec2;
};
uniform SomeStruct u_someThing;
const someThingActiveLoc = gl.getUniformLocation(someProgram, "u_someThing.active");

# Textures

见下文--片段着色器--Textures

# 片段着色器

一个片断着色器的工作是为当前光栅化的像素提供颜色值,通常是以下的形式

precision mediump float;
 
void main() {
   gl_FragColor = doMathToMakeAColor;
}

每个像素都将调用一次片断着色器,每次调用需要从你设置的特殊全局变量 gl_FragColor 中获取颜色信息。

片断着色器所需的数据,可以通过以下三种方式获取

  • Varyings 可变量 (从顶点着色器传递并插值的数据)
  • Uniforms 全局变量 (对于单个绘图调用的每个像素保持相同的值)
  • Textures 纹理 (从像素或纹理元素中获取的数据)

# Varyings

在工作原理提到过,可变量是一种顶点着色器给片断着色器传值的方式。

为了使用可变量,要在两个着色器中定义同名的可变量。 给顶点着色器中可变量设置的值,会作为参考值进行内插,在绘制像素时传给片断着色器的可变量。

attribute vec4 a_position;
 
uniform vec4 u_offset;
 
varying vec4 v_positionWithOffset;
 
void main() {
  gl_Position = a_position + u_offset;
  v_positionWithOffset = a_position + u_offset; // set
}
precision mediump float;
 
varying vec4 v_positionWithOffset; // get
 
void main() {
  // 从裁剪空间 (-1 <-> +1) 转换到颜色空间 (0 -> 1).
  vec4 color = v_positionWithOffset * 0.5 + 0.5
  gl_FragColor = color;
}

# Uniforms

同上文--顶点着色器--Uniforms

# Textures

在着色器中获取纹理信息,可以先创建一个sampler2D类型全局变量,然后用GLSL方法texture2D 从纹理中提取信息。

precision mediump float;
 
uniform sampler2D u_texture; // 纹理
 
void main() {
   vec2 texcoord = vec2(0.5, 0.5)  // 获取纹理中心的值
   gl_FragColor = texture2D(u_texture, texcoord);
}

从纹理中获取的数据取决于很多设置。 至少要创建并给纹理填充数据,例如

const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
const level = 0;
const width = 2;
const height = 1;
const data = new Uint8Array([
   255, 0, 0, 255,   // 一个红色的像素
   0, 255, 0, 255,   // 一个绿色的像素
]);
gl.texImage2D(gl.TEXTURE_2D, level, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

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

const someSamplerLoc = gl.getUniformLocation(someProgram, "u_texture");

在渲染的时候WebGL要求纹理必须绑定到一个纹理单元上

const unit = 5;  // 挑选一个纹理单元
gl.activeTexture(gl.TEXTURE0 + unit);
gl.bindTexture(gl.TEXTURE_2D, tex);

然后告诉着色器你要使用的纹理在那个纹理单元

gl.uniform1i(someSamplerLoc, unit);