GLSL 着色器语言与纹理应用完全教程
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. 性能优化建议
- 最小化 uniform 更新:只在必要时更新 uniform 变量
- 使用适当的精度:在片段着色器中为浮点数指定合适的精度
- 合理使用 Mipmap:为所有纹理生成 Mipmap 以提高缓存效率
- 批处理纹理:使用纹理数组或图集减少状态切换
- 避免动态分支:在着色器中尽量避免 if-else 语句,特别是在循环中
- 使用内置函数:GLSL 内置函数通常经过高度优化
10. 调试技巧
-
可视化输出:将中间结果输出为颜色进行调试
// 调试法线:将法线映射到颜色 FragColor = vec4(normal * 0.5 + 0.5, 1.0); // 调试深度 float depth = gl_FragCoord.z; FragColor = vec4(vec3(depth), 1.0); -
使用调试工具:
- RenderDoc
- NVIDIA Nsight Graphics
- AMD GPU PerfStudio
-
检查着色器编译错误:
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 的深入理解,你将能够创建更加复杂和高效的图形效果。
评论