GLSL(OpenGL着色器语言)是用于编写GPU着色器程序的类C语言,专为图形计算设计。它提供丰富的向量、矩阵数据类型和运算函数,支持结构体、数组及流程控制。着色器通过限定符管理输入/输出和全局变量,并包含大量内置函数与变量。纹理映射是核心应用,涉及纹素、纹理坐标、环绕与过滤方式,通过多级渐远纹理优化性能。本教程系统讲解了GLSL语法与纹理技术,为创建高效图形效果奠定基础。

博主博客

1. GLSL 概述

GLSL(OpenGL Shading Language)是 OpenGL 的着色器语言,一种专门为图形处理器(GPU)设计的类 C 语言。它包含了许多针对向量和矩阵操作的内置特性,用于编写在 GPU 上执行的着色器程序。

1.1 着色器程序的基本结构

每个 GLSL 着色器程序都以版本声明开始,接着定义输入/输出变量、uniform 变量和 main 函数。main 函数是着色器的入口点,在这里处理所有输入变量并将结果赋值给输出变量。

#version 330 core  // 版本声明

// 输入变量(从顶点属性或上一个着色器阶段传递)
in vec3 position;
in vec2 texCoord;

// 输出变量(传递到下一个着色器阶段或最终输出)
out vec4 fragColor;

// Uniform 变量(由应用程序设置,全局可见)
uniform mat4 modelMatrix;
uniform sampler2D mainTexture;

void main()
{
    // 处理输入并进行图形计算
    vec4 worldPos = modelMatrix * vec4(position, 1.0);
    
    // 采样纹理
    vec4 texColor = texture(mainTexture, texCoord);
    
    // 输出处理结果
    fragColor = texColor;
}

2. GLSL 数据类型详解

2.1 基本数据类型

GLSL 提供了丰富的内置数据类型:

标量类型

  • float:单精度浮点数
  • int:有符号整数
  • uint:无符号整数
  • bool:布尔值

向量类型

  • vec2, vec3, vec4:2、3、4维浮点向量
  • ivec2, ivec3, ivec4:2、3、4维整数向量
  • uvec2, uvec3, uvec4:2、3、4维无符号整数向量
  • bvec2, bvec3, bvec4:2、3、4维布尔向量

矩阵类型

  • mat2, mat3, mat4:2×2、3×3、4×4浮点矩阵
  • 还有 mat2x3, mat3x4 等非方阵类型

采样器类型

  • sampler2D, sampler3D, samplerCube:纹理采样器
  • isampler2D, usampler2D:整数和无符号整数纹理采样器

2.2 类型转换与构造

GLSL 要求类型严格匹配,但提供了构造函数进行类型转换:

// 标量转换
float a = 1.5;
int b = int(a);      // 显式转换为 int,值为 1
float c = float(b);  // 显式转换为 float,值为 1.0

// 向量构造
vec3 color = vec3(1.0, 0.5, 0.0);  // RGB 颜色
vec4 position = vec4(color, 1.0);  // 添加 alpha 分量

// 矩阵构造(按列填充)
mat3 rotation = mat3(
    1.0, 0.0, 0.0,   // 第一列
    0.0, 1.0, 0.0,   // 第二列
    0.0, 0.0, 1.0    // 第三列
);

// 使用标量构造单位矩阵
mat4 identity = mat4(1.0);  // 4x4 单位矩阵

2.3 向量和矩阵操作

向量分量访问

GLSL 提供多种方式访问向量分量:

vec4 vertex = vec4(1.0, 2.0, 3.0, 4.0);

// 分量访问方式
float x = vertex.x;    // 1.0
float y = vertex.y;    // 2.0
float z = vertex.z;    // 3.0
float w = vertex.w;    // 4.0

// 颜色表示法(RGBA)
float r = vertex.r;    // 1.0
float g = vertex.g;    // 2.0
float a = vertex.a;    // 4.0

// 纹理坐标表示法(STPQ)
float s = vertex.s;    // 1.0
float t = vertex.t;    // 2.0

// 数组索引访问
float first = vertex[0];  // 1.0
float last = vertex[3];   // 4.0

// 分量重组(swizzling)
vec2 xy = vertex.xy;      // (1.0, 2.0)
vec3 yzx = vertex.yzx;    // (2.0, 3.0, 1.0)
vec4 xxxx = vertex.xxxx;  // (1.0, 1.0, 1.0, 1.0)

矩阵操作

mat4 modelView = mat4(1.0);
mat4 projection = mat4(1.0);

// 访问矩阵的列(返回向量)
vec4 firstColumn = modelView[0];

// 矩阵乘法
mat4 mvp = projection * modelView;

// 矩阵与向量乘法
vec4 clipPos = mvp * vec4(position, 1.0);

2.4 结构体和数组

结构体定义

// 定义结构体类型
struct Material {
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
    float shininess;
};

// 声明结构体变量
Material gold;
gold.ambient = vec3(0.24725, 0.1995, 0.0745);
gold.diffuse = vec3(0.75164, 0.60648, 0.22648);
gold.specular = vec3(0.628281, 0.555802, 0.366065);
gold.shininess = 0.4;

// 也可以在声明时初始化
Material bronze = Material(
    vec3(0.2125, 0.1275, 0.054),
    vec3(0.714, 0.4284, 0.18144),
    vec3(0.393548, 0.271906, 0.166721),
    0.2
);

数组使用

// 创建数组
float weights[4];
vec3 positions[10];

// 初始化数组
float values[3] = float[3](0.0, 0.5, 1.0);
float sameValues[3] = float[](0.0, 0.5, 1.0);  // 简化写法

// 使用常量定义数组大小
const int MAX_LIGHTS = 4;
uniform vec3 lightPositions[MAX_LIGHTS];

// 数组作为函数参数(必须指定大小)
float calculateSum(float values[4]) {
    float sum = 0.0;
    for (int i = 0; i < 4; i++) {
        sum += values[i];
    }
    return sum;
}

3. GLSL 运算符与控制流

3.1 运算符优先级表

优先级 运算符类别 运算符 结合方向
1(最高) 成组操作 () 从左向右
2 数组下标、函数调用、成员访问、后置自增/自减 [] () . ++ -- 从左向右
3 一元运算符、前置自增/自减 ++ -- + - ! 从右向左
4 乘除运算 * / 从左向右
5 加减运算 + - 从左向右
6 关系比较 < > <= >= 从左向右
7 相等判断 == != 从左向右
8 逻辑与 && 从左向右
9 逻辑异或 ^^ 从左向右
10 逻辑或 || 从左向右
11 三元条件 ?: 从右向左
12 赋值运算 = += -= *= /= 从右向左
13(最低) 逗号运算符 , 从左向右

注意:GLSL 要求运算符两侧的操作数类型必须严格匹配。

3.2 流程控制语句

条件语句

// if-else 语句
if (intensity > 0.5) {
    color = vec4(1.0, 1.0, 1.0, 1.0);  // 白色
} else if (intensity > 0.2) {
    color = vec4(0.5, 0.5, 0.5, 1.0);  // 灰色
} else {
    color = vec4(0.0, 0.0, 0.0, 1.0);  // 黑色
}

// 三元运算符
vec3 finalColor = useTexture ? texColor.rgb : baseColor;

循环语句

// for 循环
float sum = 0.0;
for (int i = 0; i < 10; i++) {
    sum += float(i) * 0.1;
}

// while 循环
int count = 0;
while (count < 5) {
    // 执行操作
    count++;
}

// do-while 循环
int attempts = 0;
do {
    // 至少执行一次
    attempts++;
} while (attempts < 3 && !success);

跳转语句

// break - 跳出循环
for (int i = 0; i < 100; i++) {
    if (i * i > 50) break;
    // 当 i*i > 50 时跳出循环
}

// continue - 跳过本次循环
for (int i = 0; i < 10; i++) {
    if (i % 2 == 0) continue;  // 跳过偶数
    // 只处理奇数
}

// discard - 丢弃当前片段(仅限片段着色器)
void main() {
    vec4 texColor = texture(texSampler, texCoord);
    if (texColor.a < 0.1) {
        discard;  // 丢弃透明片段
    }
    fragColor = texColor;
}

// return - 从函数返回
float calculateBrightness(vec3 color) {
    if (length(color) == 0.0) {
        return 0.0;  // 提前返回
    }
    return dot(color, vec3(0.299, 0.587, 0.114));
}

4. GLSL 函数

4.1 函数定义与声明

// 函数声明(可选的,用于前置声明)
float calculateLighting(vec3 normal, vec3 lightDir);

// 函数定义
float calculateLighting(vec3 normal, vec3 lightDir) {
    // 计算漫反射分量
    float diffuse = max(dot(normal, lightDir), 0.0);
    
    // 返回结果
    return diffuse;
}

// 无返回值的函数
void applyGammaCorrection(inout vec3 color, float gamma) {
    color = pow(color, vec3(1.0 / gamma));
}

4.2 函数重载

GLSL 支持函数重载,只要参数列表不同即可:

// 重载函数示例
float multiply(float a, float b) {
    return a * b;
}

vec2 multiply(vec2 a, float b) {
    return a * b;
}

vec2 multiply(float a, vec2 b) {
    return a * b;
}

vec2 multiply(vec2 a, vec2 b) {
    return a * b;
}

5. 限定符 (Qualifiers)

5.1 存储限定符

限定符 描述
const 定义编译时常量,声明时必须初始化
in 指定变量为着色器输入
out 指定变量为着色器输出
uniform 全局变量,由应用程序设置
layout 指定顶点属性位置等布局信息

Uniform 示例

// 顶点着色器
#version 330 core
uniform mat4 modelViewProjection;
layout(location = 0) in vec3 position;

void main() {
    gl_Position = modelViewProjection * vec4(position, 1.0);
}
// C++/OpenGL 代码中设置 uniform
GLint mvpLocation = glGetUniformLocation(program, "modelViewProjection");
glUseProgram(program);
glUniformMatrix4fv(mvpLocation, 1, GL_FALSE, &mvpMatrix[0][0]);

5.2 参数限定符

限定符 描述
in (默认) 值传递,函数内修改不影响原值
out 输出参数,函数内修改会传递出去
inout 引用传递,既可读又可写
// 参数限定符示例
void transformPoint(in vec3 inputPoint, 
                    out vec3 outputPoint,
                    inout mat4 transformation) {
    // inputPoint 是只读的
    // transformation 是可读写的
    // outputPoint 是只写的
    
    outputPoint = (transformation * vec4(inputPoint, 1.0)).xyz;
    transformation[3][3] = 1.0;  // 修改传入的矩阵
}

5.3 精度限定符 (主要用于 OpenGL ES)

限定符 描述
highp 高精度,满足顶点着色器要求
mediump 中精度,满足片段着色器最低要求
lowp 低精度,仍可表示所有颜色值
// 设置默认精度
precision highp float;    // 所有浮点数使用高精度
precision mediump int;    // 所有整数使用中精度

// 为特定变量指定精度
lowp vec3 color;
mediump float specularIntensity;
highp mat4 viewMatrix;

5.4 不变性限定符

// 保证相同计算得到相同结果
invariant gl_Position;        // 内置变量
invariant out vec3 Color;     // 自定义输出变量

// 声明时直接指定
invariant centroid out vec3 Normal;

6. 内置变量与函数

6.1 内置变量

顶点着色器内置变量

  • gl_Position:输出,顶点位置(裁剪空间)
  • gl_PointSize:输出,点精灵大小
  • gl_VertexID:输入,顶点索引
  • gl_InstanceID:输入,实例索引

片段着色器内置变量

  • gl_FragCoord:输入,片段窗口坐标
  • gl_FrontFacing:输入,是否为正面
  • gl_FragDepth:输出,深度值(可写)
  • gl_PointCoord:输入,点精灵纹理坐标

6.2 常用内置函数

角度与三角函数

float angle = radians(45.0);      // 角度转弧度
float degrees = degrees(3.14159); // 弧度转角度
float s = sin(angle);             // 正弦
float c = cos(angle);             // 余弦
float t = tan(angle);             // 正切

指数函数

float power = pow(2.0, 3.0);      // 2的3次方
float expVal = exp(1.0);          // e的1次方
float logVal = log(2.71828);      // 自然对数
float sqrtVal = sqrt(4.0);        // 平方根

几何函数

float len = length(vec);          // 向量长度
float dist = distance(a, b);      // 两点距离
float dotProd = dot(a, b);        // 点积
vec3 crossProd = cross(a, b);     // 叉积
vec3 normalized = normalize(v);   // 归一化

纹理函数

// 获取纹理大小
ivec2 size = textureSize(sampler, 0);

// 采样纹理
vec4 color = texture(sampler, texCoord);

// 带偏置的采样
vec4 color = texture(sampler, texCoord, bias);

// 投影纹理采样
vec4 color = textureProj(sampler, projCoord);

7. 纹理详解

7.1 基本概念

纹素 (Texel):纹理的基本单元,类似于像素但用于纹理映射。
纹理坐标:归一化的坐标系统,范围 [0, 1],用于访问纹理。

7.2 纹理坐标系统

纹理坐标系 (左下角为原点)
   ↑ t
   |
   +----→ s
(0,0)

7.3 纹理环绕方式

环绕方式 描述 视觉效果
GL_REPEAT 重复纹理 平铺效果
GL_MIRRORED_REPEAT 镜像重复 镜像平铺
GL_CLAMP_TO_EDGE 边缘拉伸 边缘扩展
GL_CLAMP_TO_BORDER 指定边框色 自定义边框

7.4 纹理过滤

过滤方式 描述 适用场景
GL_NEAREST 最近邻过滤 像素艺术,需要锐利边缘
GL_LINEAR 线性过滤 平滑过渡,大多数情况

7.5 多级渐远纹理 (Mipmap)

Mipmap 是一系列逐渐缩小的纹理副本,用于优化远处物体的纹理渲染。

Mipmap 过滤 描述
GL_NEAREST_MIPMAP_NEAREST 最近Mipmap层 + 最近邻采样
GL_LINEAR_MIPMAP_NEAREST 最近Mipmap层 + 线性采样
GL_NEAREST_MIPMAP_LINEAR 线性Mipmap层间 + 最近邻采样
GL_LINEAR_MIPMAP_LINEAR 线性Mipmap层间 + 线性采样(三线性过滤)

8. 实战:OpenGL 纹理绘制

8.1 完整纹理绘制流程

步骤 1:顶点数据(包含纹理坐标)

float vertices[] = {
    // 位置              // 颜色              // 纹理坐标
     0.5f,  0.5f, 0.0f,  1.0f, 0.0f, 0.0f,  1.0f, 0.0f,  // 右上
     0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,  1.0f, 1.0f,  // 右下
    -0.5f, -0.5f, 0.0f,  0.0f, 0.0f, 1.0f,  0.0f, 1.0f,  // 左下
    -0.5f,  0.5f, 0.0f,  1.0f, 1.0f, 0.0f,  0.0f, 0.0f   // 左上
};

unsigned int indices[] = {
    0, 1, 3,  // 第一个三角形
    1, 2, 3   // 第二个三角形
};

步骤 2:顶点属性配置

// 位置属性 (location = 0)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

// 颜色属性 (location = 1)
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

// 纹理坐标属性 (location = 2)
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);

步骤 3:着色器程序

顶点着色器 (vertex_shader.glsl):

#version 330 core

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;

out vec3 ourColor;
out vec2 TexCoord;

void main() {
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor;
    TexCoord = aTexCoord;
}

片段着色器 (fragment_shader.glsl):

#version 330 core

in vec3 ourColor;
in vec2 TexCoord;

out vec4 FragColor;

uniform sampler2D texture1;

void main() {
    // 混合纹理颜色和顶点颜色
    FragColor = texture(texture1, TexCoord) * vec4(ourColor, 1.0);
}

步骤 4:纹理加载与配置

// 生成纹理对象
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);

// 设置纹理参数
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

// 加载图像
int width, height, nrChannels;
unsigned char* data = stbi_load("texture.jpg", &width, &height, &nrChannels, 0);

if (data) {
    // 根据通道数选择格式
    GLenum format = GL_RGB;
    if (nrChannels == 1)
        format = GL_RED;
    else if (nrChannels == 3)
        format = GL_RGB;
    else if (nrChannels == 4)
        format = GL_RGBA;
    
    // 上传纹理数据
    glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
    glGenerateMipmap(GL_TEXTURE_2D);
} else {
    std::cout << "Failed to load texture" << std::endl;
}

// 释放图像内存
stbi_image_free(data);

步骤 5:渲染循环

while (!glfwWindowShouldClose(window)) {
    // 清除缓冲
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    
    // 绑定纹理
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, texture);
    
    // 使用着色器程序
    glUseProgram(shaderProgram);
    
    // 设置纹理单元(如果着色器中有多个纹理)
    glUniform1i(glGetUniformLocation(shaderProgram, "texture1"), 0);
    
    // 绘制
    glBindVertexArray(VAO);
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
    
    // 交换缓冲并检查事件
    glfwSwapBuffers(window);
    glfwPollEvents();
}

8.2 多纹理示例

片段着色器支持多个纹理

#version 330 core

in vec2 TexCoord;

out vec4 FragColor;

uniform sampler2D texture1;
uniform sampler2D texture2;

void main() {
    // 混合两个纹理
    vec4 tex1 = texture(texture1, TexCoord);
    vec4 tex2 = texture(texture2, TexCoord);
    FragColor = mix(tex1, tex2, 0.5);
}

C++ 代码中绑定多个纹理

// 激活多个纹理单元
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture2);

// 设置着色器中的纹理单元
glUseProgram(shaderProgram);
glUniform1i(glGetUniformLocation(shaderProgram, "texture1"), 0);  // 对应 GL_TEXTURE0
glUniform1i(glGetUniformLocation(shaderProgram, "texture2"), 1);  // 对应 GL_TEXTURE1

9. 性能优化建议

  1. 最小化 uniform 更新:只在必要时更新 uniform 变量
  2. 使用适当的精度:在片段着色器中为浮点数指定合适的精度
  3. 合理使用 Mipmap:为所有纹理生成 Mipmap 以提高缓存效率
  4. 批处理纹理:使用纹理数组或图集减少状态切换
  5. 避免动态分支:在着色器中尽量避免 if-else 语句,特别是在循环中
  6. 使用内置函数:GLSL 内置函数通常经过高度优化

10. 调试技巧

  1. 可视化输出:将中间结果输出为颜色进行调试

    // 调试法线:将法线映射到颜色
    FragColor = vec4(normal * 0.5 + 0.5, 1.0);
    
    // 调试深度
    float depth = gl_FragCoord.z;
    FragColor = vec4(vec3(depth), 1.0);
    
  2. 使用调试工具

    • RenderDoc
    • NVIDIA Nsight Graphics
    • AMD GPU PerfStudio
  3. 检查着色器编译错误

    GLint success;
    GLchar infoLog[512];
    glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(shader, 512, NULL, infoLog);
        std::cout << "Shader compilation failed:\n" << infoLog << std::endl;
    }
    

总结

GLSL 是 OpenGL 编程的核心,掌握 GLSL 语言特性和纹理技术对于图形编程至关重要。本教程涵盖了从基础语法到高级用法的完整内容,建议通过实际项目练习来巩固这些知识。随着对 GLSL 的深入理解,你将能够创建更加复杂和高效的图形效果。