GLSL着色器踩坑与实践经验

GLSL着色器踩坑与实践经验

搞游戏开发不可避免要接触Shader,GLSL是OpenGL的着色器语言。记录一下基础语法和踩过的坑。

基础类型

类型 说明 示例
void 空类型 void main()
bool 布尔 true, false
int 整数 42, -7
float 浮点数 3.14, -0.5
vec2/3/4 浮点向量 vec3(1.0, 2.0, 3.0)
mat2/3/4 浮点矩阵 mat3(1.0)
sampler2D 2D纹理 uniform sampler2D texture

注意:GLSL没有double类型,只有float。

向量分量访问

GLSL向量可以用多种方式访问分量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vec4 v = vec4(1.0, 2.0, 3.0, 1.0);

// 空间坐标 (x, y, z, w)
float x = v.x; // 1.0

// 颜色 (r, g, b, a)
float r = v.r; // 1.0

// 纹理坐标 (s, t, p, q)
float s = v.s; // 1.0

// 数组访问
float x2 = v[0]; // 1.0

// 向量重组
vec3 xyz = v.xyz; // vec3(1.0, 2.0, 3.0)
vec3 rgb = v.rgb; // vec3(1.0, 2.0, 3.0)
vec3 v3 = v.zyx; // vec3(3.0, 2.0, 1.0) 倒序

向量重组很灵活,可以随意组合:

1
2
vec4 v2 = v.xyzx;           // vec4(1.0, 2.0, 3.0, 1.0)
vec2 xy = v.xy; // vec2(1.0, 2.0)

结构体和数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 结构体
struct Light {
float intensity;
vec3 position;
vec4 color;
};

Light light1 = Light(3.0, vec3(1.0, 2.0, 3.0), vec4(1.0));

// 数组(只支持一维)
float weights[3] = float[3](0.5, 0.3, 0.2);
vec3 positions[4];

// 结构体数组
Light lights[4];

运算符

GLSL运算符和C语言类似,支持逐分量运算:

优先级 运算符 说明
1 () 括号
2 [] . 数组、属性
4 * / 乘除
5 + - 加减
6 < > <= >= 关系运算
7 == != 相等判断
8-10 && ^^ || 逻辑运算
12 = += -= 赋值

逐分量运算

1
2
3
4
5
6
7
8
9
10
11
vec3 a = vec3(1.0, 2.0, 3.0);
float s = 10.0;
vec3 b = s * a; // vec3(10.0, 20.0, 30.0)

vec3 c = a + b; // vec3(11.0, 22.0, 33.0)
vec3 d = a * b; // vec3(10.0, 40.0, 90.0) 逐分量相乘

// 矩阵与向量运算(线性代数规则)
vec2 v = vec2(10.0, 20.0);
mat2 m = mat2(1.0, 2.0, 3.0, 4.0);
vec2 w = m * v; // vec2(70.0, 100.0)

类型转换

GLSL不支持隐式转换,必须显式转换:

1
2
3
4
5
6
7
8
9
10
11
float f = 2.0;
int i = int(f); // 2

int i2 = 3;
float f2 = float(i2); // 3.0

bool t = true;
float f3 = float(t); // 1.0

vec3 v = vec3(1.0, 2.0, 3.0);
ivec3 iv = ivec3(v); // ivec3(1, 2, 3)

变量限定符

限定符 说明 位置
(默认) 局部变量 函数内
const 常量 任意
attribute 顶点属性 顶点着色器
uniform 统一变量 全局
varying 插值变量 顶点/片元着色器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// attribute:每个顶点不同的数据
attribute vec4 a_position;
attribute vec2 a_texCoord;
attribute vec3 a_normal;

// uniform:所有顶点/片元相同的数据
uniform mat4 u_mvpMatrix;
uniform vec4 u_color;
uniform sampler2D u_texture;

// varying:顶点输出,片元输入
varying vec2 v_texCoord;
varying vec3 v_normal;
varying float v_diffuse;

参数限定符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// in:值传递(默认)
void func(in float value) {
value = 10.0; // 不影响实参
}

// out:输出参数
void func(out float value) {
value = 10.0; // 修改会反映到实参
}

// inout:可输入可输出
void func(inout float value) {
value = value * 2.0;
}

精度限定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 高精度(顶点着色器常用)
highp float position;
highp vec4 vertex;

// 中精度(片元着色器常用)
mediump vec4 color;
mediump vec2 texCoord;

// 低精度(性能最好)
lowp float alpha;
lowp vec3 normal;

// 默认精度
precision mediump float;

精度选择建议:

类型 精度 说明
位置、矩阵 highp 需要精确计算
颜色、纹理坐标 mediump 平衡精度和性能
法线、光照强度 lowp 对精度要求低

:精度不够会导致渲染出现条纹或闪烁。

内置函数

通用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
float abs(float x);           // 绝对值
float sign(float x); // 符号函数
float floor(float x); // 向下取整
float ceil(float x); // 向上取整
float fract(float x); // 取小数部分
float mod(float x, float y); // 取模

float min(float x, float y); // 最小值
float max(float x, float y); // 最大值
float clamp(float x, float min, float max); // 限制范围

float mix(float x, float y, float a); // 线性插值 x*(1-a) + y*a
float step(float edge, float x); // x < edge ? 0 : 1
float smoothstep(float e0, float e1, float x);

三角函数

1
2
3
4
5
6
7
8
9
10
float radians(float degrees);   // 角度转弧度
float degrees(float radians); // 弧度转角度

float sin(float angle); // angle为弧度
float cos(float angle);
float tan(float angle);

float asin(float x);
float acos(float x);
float atan(float y, float x);

指数函数

1
2
3
4
5
6
7
float pow(float x, float y);      // x^y
float exp(float x); // e^x
float log(float x); // ln(x)
float exp2(float x); // 2^x
float log2(float x); // log2(x)
float sqrt(float x); // sqrt(x)
float inversesqrt(float x); // 1/sqrt(x)

几何函数

1
2
3
4
5
6
7
8
float length(vec3 v);               // 向量长度 |v|
float distance(vec3 p0, vec3 p1); // 两点距离
float dot(vec3 v0, vec3 v1); // 点积
vec3 cross(vec3 v0, vec3 v1); // 叉积(仅vec3)
vec3 normalize(vec3 v); // 归一化 v/|v|

vec3 reflect(vec3 I, vec3 N); // 反射
vec3 refract(vec3 I, vec3 N, float eta); // 折射

纹理采样

1
2
3
4
5
6
// 2D纹理采样
vec4 texture2D(sampler2D sampler, vec2 coord);
vec4 texture2D(sampler2D sampler, vec2 coord, float bias);

// 立方体纹理
vec4 textureCube(samplerCube sampler, vec3 coord);

流控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// if-else
if (condition) {
// do something
} else if (otherCondition) {
// do something else
} else {
// default
}

// 三目运算符
float value = condition ? a : b;

// discard(片元着色器专用,丢弃当前片元)
if (alpha < 0.01) {
discard;
}

// for循环
for (int i = 0; i < 10; i++) {
// do something
}

// while循环
while (count < 10) {
count++;
}

注意:循环变量必须是编译时常量或已知值,有些GPU不支持动态循环。

完整着色器示例

顶点着色器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#version 100

// 顶点属性
attribute vec4 a_position;
attribute vec3 a_normal;
attribute vec2 a_texCoord;

// uniform变量
uniform mat4 u_mvpMatrix;
uniform mat3 u_normalMatrix;
uniform vec3 u_lightDir;

// varying变量(输出到片元着色器)
varying vec2 v_texCoord;
varying float v_diffuse;

void main() {
// 变换顶点位置
gl_Position = u_mvpMatrix * a_position;

// 变换法线并归一化
vec3 normal = normalize(u_normalMatrix * a_normal);

// 计算漫反射光照强度
v_diffuse = max(dot(u_lightDir, normal), 0.0);

// 传递纹理坐标
v_texCoord = a_texCoord;
}

片元着色器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#version 100

precision mediump float;

uniform sampler2D u_texture;
uniform vec4 u_ambient;

varying vec2 v_texCoord;
varying float v_diffuse;

void main() {
// 采样纹理
vec4 texColor = texture2D(u_texture, v_texCoord);

// 漫反射 + 环境光
vec3 color = texColor.rgb * (v_diffuse + u_ambient.rgb);

gl_FragColor = vec4(color, texColor.a);
}

流程:

1
2
3
顶点数据(attribute) -> 矩阵变换(uniform) -> 光照计算 -> varying
|
varying <- 纹理采样(uniform) <- 片元着色(gl_FragColor)

预编译指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 版本声明(文件开头)
#version 100

// 宏定义
#define PI 3.14159
#define MAX_LIGHTS 4

// 条件编译
#ifdef GL_ES
precision mediump float;
#endif

// 取消宏
#undef PI

内置宏

说明
__LINE__ 当前行号
__VERSION__ GLSL版本号
GL_ES OpenGL ES环境为1
GL_FRAGMENT_PRECISION_HIGH 片元着色器是否支持高精度
1
2
3
4
5
6
7
8
// 自动选择精度
#ifdef GL_ES
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
#endif

踩坑记录

问题 原因 解决
黑屏 编译错误 检查日志
颜色异常 精度问题 调整精度修饰符
纹理错乱 坐标问题 检查纹理坐标范围0-1
性能差 计算复杂 简化算法,避免动态分支
条纹/闪烁 精度不够 提高精度到highp

学习资源

网站 网址 特点
ShaderToy shadertoy.com 最知名的在线Shader编辑器
GLSLSandbox glslsandbox.com 简洁的GLSL沙盒
The Book of Shaders thebookofshaders.com 系统学习资料

GLSL入门不难,但要写出高效好看的Shader需要大量练习。建议多逛ShaderToy看别人怎么写,从模仿开始。注意精度问题和GPU差异,移动端和桌面端表现可能不一样。