前端 / 游戏 / 计算机图形学

【前端er入门Shader系列】02—GLSL语言基础

csxiaoyao · 2月13日 · 2025年 · · 本文共7951个字 · 预计阅读27分钟156次已读

【前端er入门Shader系列】02—GLSL语言基础

Write By CS逍遥剑仙
我的主页: www.csxiaoyao.com
GitHub: github.com/csxiaoyaojianxian
Email: sunjianfeng@csxiaoyao.com

Shader 一般由顶点着色器和片段着色器成对使用,GLSL 则是编写 Shader 着色器的语言,而 GLSL ES 是在 OpenGL Shader 着色器语言的基础上针对移动端和嵌入式设备的简化版。本章介绍 GLSL 语言相关语法。

1. GLSL数据类型

变量类型 说明 Cocos Shader 中的默认值 Cocos Shader 中的可选项
bool 布尔型标量数据类型 false
int/ivec2/ivec3/ivec4 包含 1/2/3/4 个整型向量 0/[0, 0]/[0, 0, 0]/[0, 0, 0, 0]
float/vec2/vec3/vec4 包含 1,2,3,4 个浮点型向量 0/[0, 0]/[0, 0, 0]/[0, 0, 0, 0]
sampler2D 表示 2D 纹理 default black、grey、white、normal、default
samplerCube 表示立方体纹理 default-cube black-cube、white-cube、default-cube
mat[2..3] 表示 2x2 和 3x3 的矩阵 不可用
mat4 表示 4x4 的矩阵 [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]

Cocos Shader 是指 shader 在 cocos creator 引擎中的应用,在系列第五章中会介绍

glsl 语言的语法非常简单,数据类型分三大类,但不支持字符串类型:

1.1 标量(字面量)

(1) 数字类型 int / float

需要注意,和 js 不同,1 和 1.0 类型不同,不能一起运算

float floatValue = 1.0;

(2) 布尔类型 bool

包括 true / false

(3) 类型转换

float(int) / float(bool) / int(float) / int(bool) / bool(int) / bool(float)

(4) 计算

+-*/ 运算左右值类型需一致

支持 ++--+=-=*=/=

比较符 <><=>===!=

其他 !&&^^、三元选择

1.2 向量 & 矩阵

(1) 浮点型向量Vector vec2 / vec3 / vec4

整型向量 ivec2 / ivec3 / ivec4

布尔型向量 bvec2 / bvec3 / bvec4

vec4 myVec4 = vec4(1.0);              // myVec4 = {1.0, 1.0, 1.0, 1.0}
vec4 color = vec4(1.0, 1.0, myVec2);
// 向量取值方式
// color.x, color.y, color.z, color.w // xyzw
// color.r, color.g, color.b, color.a // rgba
// color.s, color.t, color.p, color.q // stpq
vec3 myVec3_0 = color.xyz;

(2) 矩阵Matrix mat2 / mat3 / mat4

// [构造方式1] 矩阵可以由单个标量从左到右进行构造
mat4 m4 = mat4(
  1.0, 2.0, 3.0, 4.0, // 第一列
  0.0, 1.0, 0.0, 0.0, // 第二列
  0.0, 0.0, 1.0, 0.0, // 第三列
  0.5, 0.0, 0.0, 1.0 // 末尾不能带,
);
// [构造方式2] 若只为矩阵构造器提供了一个标量,则该值会构造矩阵对角线上的值
mat4(1.0); // myMat4 = {1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0}
// [构造方式3] 矩阵可以由多个向量构造
vec2 col1 = vec2(1.0, 0.0);
vec2 col2 = vec2(1.0, 0.0);
mat2 matrix2x2 = mat2(coll1, col2);
// 注意,定义的矩阵和实际矩阵是行列转置的关系
// 取值方式
// 获取【第一列】 (1.0, 2.0, 3.0, 4.0)
vec4 v4 = m4[0];
// 获取【第一列第二个】2.0
float m01 = m4[0][1];
// 获取【第一列第二个】2.0
float m01_2 = m4[1].y;
// 注意:不能使用未经const修饰的变量作为索引值
int i = 0;
// 错误 vec4 v4c = m4[i]; // 可以使用 const int i = 0;

1.3 sampler(取样器)

纹理句柄包含 sampler2D / samplerCube 只能是 uniform 类型变量

uniform sampler2D u_Sampler;

2. 控制流程 & 函数

2.1 数组

float floatArray[2]; // 数组必须声明长度
vec4 vec4Array[2]; // 不支持多维数组
// 数组不能在声明时一次性初始化,只能显式地对每个元素初始化
vec4Array[0] = vec4(1.0, 2.0, 3.0, 4.0); // 必须由常量表达式初始化
vec4Array[1] = vec4(2.0, 3.0, 4.0, 5.0);

2.2 结构体

// 定义结构体light,包含两个成员变量,同时声明变量l1(可选)
struct light {
  vec4 color;
  vec3 position;
} l1;
light l2, l3;
l1 = light(vec4(0.0, 0.0, 0.0, 1.0), vec3(8.0, 2.0, 0.0));
vec4 color = l1.color;
// 结构体支持赋值(=)和比较(==和!=),但不适用含数组或纹理成员的结构体

2.3 循环/条件判断/函数

  • if-else / switch-case
  • for / while / do-while
  • break / continue / return

关注下面代码中 for / if / else / continue / break / discard 用法,其中 discard 仅适用片元色器,表示放弃当前片元直接处理下一片元。

const vertexShader = /*glsl*/`
attribute vec3 a_position;
void main() {
  float deg = radians(45.0); // 内置函数radians
  mat4 rotateMatrix = mat4(
    cos(deg), sin(deg), 0.0, 0.0, // 内置函数sin/cos
    -sin(deg), cos(deg), 0.0, 0.0,
    0.0, 0.0, 1.0, 0.0,
    0.0, 0.0, 0.0, 1.0
  );
  gl_Position = rotateMatrix * vec4(a_position, 1.0);
}`;

const fragmentShader = /*glsl*/ `
precision mediump float;
// glsl函数,计算两个点的距离
float getDiatance(vec2 p1, vec2 p2) {
  return pow(pow(p1.x - p2.x, 2.0) + pow(p1.y - p2.y, 2.0), 0.5);
}
void main() {
  float x = (gl_FragCoord.x / 400.0 - 0.5) * 2.0;
  float y = (gl_FragCoord.y / 400.0 - 0.5) * 2.0;
  vec3 color1 = vec3(1.0, 1.0, 1.0);
  vec3 color2 = vec3(0.0, 0.0, 1.0);
  float dis = distance(vec2(x, y), vec2(0.0, 0.0));
  /* 条件判断 */
  if (dis > 0.4) {
    gl_FragColor = vec4(color1 - color2, 1.0);
    // 仅适用片元色器,表示放弃当前片元直接处理下一片元
    discard;
  } else {
    gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);
  }
  /* 循环 */
  for (int i = 0; i < 10; i++) {
    if (i == 1) {
        continue;
    }
    if (i == 8) {
        break;
    }
  }
}`;

3. 内置函数

【角度函数】
radians 角度制转孤度制
degrees 弧度制转角度制
【三角函数】
sin 正弦
cos 余弦
tan 正切
asin 反正弦
acos 反余弦
atan 反正切
【指数函数】
pow 开方
exp 自然指数
log 自然对数
exp2 2的x方
log2 以2为底对数
sqrt 开平方
inversesqrt 平开方的倒数
【通用函数】
abs 绝对值
min 最小值
max 最大值
mod 取余数
sign 取下负号
floor 向下取整
ceil 向上取整
clamp 限定范围
mix 线性内插
step 步进函数
smoothstep 艾米内插步进
fract 获取最小数部分
【几何函数】
length 矢量长度
distance 两点间距离
dot 内积
cross 外积
normalize 归一化
reflect 矢量反射
faceforward 使向量"朝前"
【矩阵函数】
matrixCmpMult 逐元素乘法
【矢量函数】
lessThan 逐元素小于
lessThanEqual 逐元素小于等于
greaterThan 逐元素大于
greaterThanEqual 逐元素等于
equal 逐元素等于
notEqual 逐元素不等
any 任一元素为true则为true
all 所有元素为true则为true
not 逐元素取补
【纹理查询函数】
texture2D 在二维纹理中获取纹素
textureCube 在立方体纹理中获取纹素
texture2DProj texture2D 的投影版本
texture2DLod texture2D的金字塔版本
textureCubeLod textureCube的金字塔版本
texture2DProjLod textureCubeLod的投影版本

4. 存储限定字

4.1 const

// 常量限定
const vec4 red = vec4(1.0, 0.0, 0.0, 1.0);

4.2 attribute / uniform / varying

对应三种数据传递的存储限定字,详情见后。

attributevarying csxiaoyao.com变量类型只能是:floatvec2vec3vec4mat2mat3mat4

uniform 变量类型可以是除结构体外的任意类型

5. Shader初始化函数封装

通过上述代码不难看出,Shader 的初始化过程需要编写较多固定的代码,通过函数封装可以简化调用逻辑,精力专注于两段 Shader 的编写,初始化函数封装于 initShaders.js,如下所示:

export default function initShaders(gl, vertexSource, fragmentSource) {
  const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource);
  const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource);
  // 将创建的顶点着色器和片元着色器绑定到着色程序上,顶点着色器和片段着色器需要成对提供
  const program = createProgram(gl, vertexShader, fragmentShader);
  if (program) {
    gl.useProgram(program);
    gl.program = program; // 挂载gl对象上方便获取
    return true;
  } else {
    console.log("create program error.");
    return false;
  }
}

/**
 * 创建着色器
 * @param gl WebGL上下文
 * @param type 着色器类型
 * @param source 着色器文本
 */
function createShader(gl, type, source) {
  // 根据 type 创建着色器
  const shader = gl.createShader(type);
  // 绑定内容文本 source
  gl.shaderSource(shader, source);
  // 编译着色器(将文本内容转换成着色器程序)
  gl.compileShader(shader);
  // 获取编译后的状态
  const state = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
  if (state) {
    return shader;
  } else {
    // 获取当前着色器csxiaoyao.com相关信息
    const error = gl.getShaderInfoLog(shader);
    console.log("compile shaders error: " + error);
    // 删除失败的着色器
    gl.deleteShader(shader);
    return null;
  }
}

/**
 * 创建着色程序
 * @param gl WebGL上下文
 * @param vertexShader 顶点着色器对象
 * @param fragmentShader 片元着色器对象
 */
function createProgram(gl, vertexShader, fragmentShader) {
  // 创建着色程序
  const program = gl.createProgram();
  if (!program) return null;
  // 使着色程序获取顶点/片段着色器
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  // 将两个着色器与着色程序绑定
  gl.linkProgram(program);
  const state = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (state) {
    return program;
  } else {
    const error = gl.getProgramInfoLog(program);
    console.log("link program error: " + error);
    gl.deleteProgram(program);
    gl.deleteShader(vertexShader);
    gl.deleteShader(fragmentShader);
    return null;
  }
}

后面的开发都可以直接调用 initShaders.js,第一节的代码结构可以简化如下:

import initShaders from "./initShaders.js";
const gl = document.getElementById("webgl").getContext("webgl");
const vertexShader = `
void main() {
  gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
  gl_PointSize = 10.0;
}
`;
const fragmentShader = `
void main() {
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`;
initShaders(gl, vertexShader, fragmentShader);
gl.clearColor(0.5, 0.5, 0.5, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);

6. Shader数据传递 attribute / uniform / varying

【前端er入门Shader系列】02—GLSL语言基础

GLSL中可以使用三种存储限定符实现数据传递:

  • attribute: [属性和缓冲] 用于从外部应用程序(如 js)向 vertexShader 中传递逐顶点数据
  • uniform: [全局只读变量] 用于从外部应用程序(如 js)向 vertexShader 或 fragmentShader 中传递数据,着色程序运行前赋值,全局有效,Shader 内不可修改声明的 uniform 常量。常量的传递使用了 GPU 中的常量寄存器
  • varying: [全局可变量] 支持 vertexShader 和 fragmentShader 间使用同名变量传递【插值】数据
import initShaders from "./initShaders.js";
const gl = document.getElementById("webgl").getContext("webgl");
const vertexShader = `
attribute vec2 a_position; // 从js接收顶点位置数据
uniform float u_size; // 从js接收顶点尺寸数据
varying vec2 v_ab; // 向片段着色器提供 vec2 两个数据

void main() {
    v_ab = a_position;
    gl_Position = vec4(a_position, 0.0, 1.0); // 顶点坐标需要接收vec4
    gl_PointSize = u_size;
}
`;
const fragmentShader = `
precision mediump float; // 精度设置
uniform vec3 u_color; // 从js接收片段颜色数据
varying vec2 v_ab; // 从顶点着色器接收 vec2 两个数据

void main() {
    gl_FragColor = vec4(u_color, 1.0);
    // gl_FragColor = vec4(v_ab, 0.0, 1.0);
}
`;
initShaders(gl, vertexShader, fragmentShader);

// 【1】attribute (js=>vertexShader)
const a_position = gl.getAttribLocation(gl.program, "a_position");
gl.vertexAttrib2f(a_position, 0.5, 0.5);

// 【2】uniform (js=>vertexShader/fragmentShader)
const u_color = gl.getUniformLocation(gl.program, "u_color");
gl.uniform3f(u_color, 1.0, 0.0, 0.0);
const u_size = gl.getUniformLocation(gl.program, "u_size");
gl.uniform1f(u_size, 10.0);

// 【3】varying (vertexShader=>fragmentShader)

gl.clearColor(0.5, 0.5, 0.5, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);

7. 精度限定符

符号 描述
highp 高精度,整型-2^16~2^16,浮点型-2^62~2^62
mediump 中精度,整型 -2^10~2^10,浮点型-2^14~2^14
lowp 低精度,整型 -2^8~2^8,浮点型-2~2

在着色器第一行可以声明着色器内所有浮点数的精度,如: precision highp float;

8. uniform后缀

前面的案例中使用了 uniform3funiform1f 设置 uniform 全局变量。

后缀 含义
f 一个 float
i 一个 int
ui 一个 unsigned int
3f 一个 float
fv 一个 float 向量/数组

例如分别使用 uniform4funiform4fv 传递颜色 rgba 值:

gl.uniform4f(vertexColorLocation, Math.random(), Math.random(), Math.random(), 1.0);
gl.uniform4fv(vertexColorLocation, [Math.random(), Math.random(), Math.random(), 1.0]);

【前端er入门Shader系列】02—GLSL语言基础

0 条回应