·48 分钟
着色器(Shader)完全指南:GPU 编程基础
图形学ShaderGPU编程着色器tech
什么是着色器(Shader)?
着色器(Shader)是运行在 GPU(图形处理单元)上的小程序,用于控制图形渲染管线的各个阶段。它们负责计算顶点位置、像素颜色、光照效果等,是现代实时图形渲染的核心。
着色器使用专门的着色语言编写,如 GLSL(OpenGL Shading Language)、HLSL(High-Level Shading Language)或 SPIR-V(Standard Portable Intermediate Representation)。
着色器的类型
1. 顶点着色器(Vertex Shader)
顶点着色器处理每个顶点,进行变换和属性计算:
glsl
#version 460 core
// 输入属性
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoord;
// 输出变量
out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoord;
// Uniform 变量
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main() {
// 计算世界空间位置
FragPos = vec3(model * vec4(aPos, 1.0));
// 计算法线(考虑非均匀缩放)
Normal = mat3(transpose(inverse(model))) * aNormal;
// 传递纹理坐标
TexCoord = aTexCoord;
// 计算裁剪空间位置
gl_Position = projection * view * vec4(FragPos, 1.0);
}
2. 片段着色器(Fragment Shader)
片段着色器计算每个像素的最终颜色:
glsl
#version 460 core
// 输入变量
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;
// 输出颜色
out vec4 FragColor;
// Uniform 变量
uniform vec3 lightPos;
uniform vec3 viewPos;
uniform vec3 lightColor;
uniform vec3 objectColor;
// 纹理采样器
uniform sampler2D diffuseTexture;
void main() {
// 环境光
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
// 漫反射
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
// 镜面反射
float specularStrength = 0.5;
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;
// 采样纹理
vec3 texColor = texture(diffuseTexture, TexCoord).rgb;
// 最终颜色
vec3 result = (ambient + diffuse + specular) * texColor;
FragColor = vec4(result, 1.0);
}
3. 几何着色器(Geometry Shader)
几何着色器可以生成新的几何图元:
glsl
#version 460 core
layout (triangles) in;
layout (triangle_strip, max_vertices = 3) out;
in VS_OUT {
vec3 FragPos;
vec3 Normal;
vec2 TexCoord;
} gs_in[];
out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoord;
void main() {
for (int i = 0; i < 3; i++) {
gl_Position = gl_in[i].gl_Position;
FragPos = gs_in[i].FragPos;
Normal = gs_in[i].Normal;
TexCoord = gs_in[i].TexCoord;
EmitVertex();
}
EndPrimitive();
}
4. 计算着色器(Compute Shader)
计算着色器用于通用 GPU 计算:
glsl
#version 460 core
layout (local_size_x = 256, local_size_y = 1, local_size_z = 1) in;
layout (std430, binding = 0) buffer InputBuffer {
float inputData[];
};
layout (std430, binding = 1) buffer OutputBuffer {
float outputData[];
};
void main() {
uint index = gl_GlobalInvocationID.x;
// 执行计算
outputData[index] = inputData[index] * 2.0;
}
着色器语言
1. GLSL(OpenGL Shading Language)
OpenGL 和 Vulkan 使用的着色语言:
glsl
#version 460 core
// 数据类型
float scalar = 1.0;
vec2 vector2 = vec2(1.0, 2.0);
vec3 vector3 = vec3(1.0, 2.0, 3.0);
vec4 vector4 = vec4(1.0, 2.0, 3.0, 4.0);
mat2 matrix2 = mat2(1.0);
mat3 matrix3 = mat3(1.0);
mat4 matrix4 = mat4(1.0);
// 内置函数
float result = sin(scalar);
vec3 normalized = normalize(vector3);
float dotProduct = dot(vector3, vector3);
2. HLSL(High-Level Shading Language)
Direct3D 使用的着色语言:
hlsl
// 顶点着色器输入
struct VS_INPUT {
float3 Position : POSITION;
float3 Normal : NORMAL;
float2 TexCoord : TEXCOORD;
};
// 顶点着色器输出
struct VS_OUTPUT {
float4 Position : SV_POSITION;
float3 WorldPos : TEXCOORD0;
float3 Normal : TEXCOORD1;
float2 TexCoord : TEXCOORD2;
};
// 常量缓冲区
cbuffer ConstantBuffer : register(b0) {
float4x4 World;
float4x4 View;
float4x4 Projection;
};
// 顶点着色器
VS_OUTPUT VSMain(VS_INPUT input) {
VS_OUTPUT output;
output.WorldPos = mul(float4(input.Position, 1.0), World).xyz;
output.Normal = mul(float4(input.Normal, 0.0), World).xyz;
output.TexCoord = input.TexCoord;
output.Position = mul(float4(output.WorldPos, 1.0), mul(View, Projection));
return output;
}
// 片段着色器
float4 PSMain(VS_OUTPUT input) : SV_TARGET {
return float4(input.Normal * 0.5 + 0.5, 1.0);
}
3. SPIR-V
Vulkan 和 OpenCL 使用的中间表示:
spirv
; SPIR-V 模块示例
OpCapability Shader
%1 = OpExtInstImport "GLSL.std.450"
OpMemoryModel Logical GLSL450
OpEntryPoint Vertex %main "main" %_ %aPos %aTexCoord %TexCoord
; 类型定义
%void = OpTypeVoid
%uint = OpTypeInt 32 0
%float = OpTypeFloat 32
%v4float = OpTypeVector %float 4
%v2float = OpTypeVector %float 2
%mat4x4 = OpTypeMatrix %v4float 4
; 函数定义
%main = OpFunction %void None %void_func
%entry = OpLabel
; ... 指令 ...
OpReturn
OpFunctionEnd
着色器编程基础
1. 数据类型
glsl
// 标量类型
int i = 1;
uint u = 1u;
float f = 1.0;
bool b = true;
// 向量类型
vec2 v2 = vec2(1.0, 2.0);
vec3 v3 = vec3(1.0, 2.0, 3.0);
vec4 v4 = vec4(1.0, 2.0, 3.0, 4.0);
ivec2 iv2 = ivec2(1, 2);
uvec3 uv3 = uvec3(1u, 2u, 3u);
bvec4 bv4 = bvec4(true, false, true, false);
// 矩阵类型
mat2 m2 = mat2(1.0, 2.0, 3.0, 4.0);
mat3 m3 = mat3(1.0);
mat4 m4 = mat4(1.0);
// 向量访问
float x = v3.x; // 或 v3.r 或 v3.s
float y = v3.y; // 或 v3.g 或 v3.t
float z = v3.z; // 或 v3.b 或 v3.p
vec2 xy = v3.xy; // 或 v3.rg 或 v3.st
vec3 xyz = v4.xyz; // 或 v4.rgb 或 v4.stp
2. 控制流
glsl
// 条件语句
if (condition) {
// ...
} else {
// ...
}
// 循环
for (int i = 0; i < 10; i++) {
// ...
}
while (condition) {
// ...
}
do {
// ...
} while (condition);
// 开关语句
switch (value) {
case 0:
// ...
break;
case 1:
// ...
break;
default:
// ...
break;
}
3. 函数
glsl
// 函数声明
float add(float a, float b);
vec3 calculateLighting(vec3 normal, vec3 lightDir);
// 函数定义
float add(float a, float b) {
return a + b;
}
vec3 calculateLighting(vec3 normal, vec3 lightDir) {
float NdotL = max(dot(normal, lightDir), 0.0);
return vec3(NdotL);
}
// 输入输出参数
void modifyVector(in vec3 input, out vec3 output) {
output = input * 2.0;
}
void passThrough(inout vec3 vector) {
vector = vector;
}
着色器阶段
1. 顶点处理阶段
glsl
// 顶点着色器
#version 460 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
out VS_OUT {
vec3 FragPos;
vec3 Normal;
vec2 TexCoord;
} vs_out;
void main() {
vs_out.FragPos = vec3(model * vec4(aPos, 1.0));
vs_out.Normal = mat3(transpose(inverse(model))) * aNormal;
vs_out.TexCoord = aTexCoord;
gl_Position = projection * view * vec4(vs_out.FragPos, 1.0);
}
2. 曲面细分阶段
glsl
// 细分控制着色器
#version 460 core
layout (vertices = 3) out;
in VS_OUT {
vec3 FragPos;
vec3 Normal;
vec2 TexCoord;
} tcs_in[];
out TCS_OUT {
vec3 FragPos;
vec3 Normal;
vec2 TexCoord;
} tcs_out[];
void main() {
tcs_out[gl_InvocationID] = tcs_in[gl_InvocationID];
if (gl_InvocationID == 0) {
gl_TessLevelOuter[0] = 4.0;
gl_TessLevelOuter[1] = 4.0;
gl_TessLevelOuter[2] = 4.0;
gl_TessLevelInner[0] = 4.0;
}
}
// 细分求值着色器
#version 460 core
layout (triangles, equal_spacing, ccw) in;
in TCS_OUT {
vec3 FragPos;
vec3 Normal;
vec2 TexCoord;
} tes_in[];
out TES_OUT {
vec3 FragPos;
vec3 Normal;
vec2 TexCoord;
} tes_out;
void main() {
vec3 p0 = gl_TessCoord.x * tes_in[0].FragPos;
vec3 p1 = gl_TessCoord.y * tes_in[1].FragPos;
vec3 p2 = gl_TessCoord.z * tes_in[2].FragPos;
tes_out.FragPos = p0 + p1 + p2;
tes_out.Normal = normalize(p0 + p1 + p2);
tes_out.TexCoord = gl_TessCoord.xy;
gl_Position = vec4(tes_out.FragPos, 1.0);
}
3. 几何处理阶段
glsl
// 几何着色器
#version 460 core
layout (triangles) in;
layout (triangle_strip, max_vertices = 3) out;
in TES_OUT {
vec3 FragPos;
vec3 Normal;
vec2 TexCoord;
} gs_in[];
out GS_OUT {
vec3 FragPos;
vec3 Normal;
vec2 TexCoord;
} gs_out;
void main() {
for (int i = 0; i < 3; i++) {
gl_Position = gl_in[i].gl_Position;
gs_out.FragPos = gs_in[i].FragPos;
gs_out.Normal = gs_in[i].Normal;
gs_out.TexCoord = gs_in[i].TexCoord;
EmitVertex();
}
EndPrimitive();
}
4. 光栅化阶段
光栅化阶段是硬件自动完成的,将三角形转换为片段。
5. 片段处理阶段
glsl
// 片段着色器
#version 460 core
in GS_OUT {
vec3 FragPos;
vec3 Normal;
vec2 TexCoord;
} fs_in;
out vec4 FragColor;
uniform sampler2D diffuseMap;
uniform vec3 lightPos;
uniform vec3 viewPos;
void main() {
vec3 color = texture(diffuseMap, fs_in.TexCoord).rgb;
// Phong 光照
vec3 normal = normalize(fs_in.Normal);
vec3 lightDir = normalize(lightPos - fs_in.FragPos);
vec3 viewDir = normalize(viewPos - fs_in.FragPos);
vec3 reflectDir = reflect(-lightDir, normal);
// 环境光
vec3 ambient = 0.1 * color;
// 漫反射
float diff = max(dot(normal, lightDir), 0.0);
vec3 diffuse = diff * color;
// 镜面反射
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
vec3 specular = vec3(0.3) * spec;
FragColor = vec4(ambient + diffuse + specular, 1.0);
}
高级着色器技术
1. PBR(基于物理的渲染)
glsl
// PBR 片段着色器
#version 460 core
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;
out vec4 FragColor;
uniform vec3 albedo;
uniform float metallic;
uniform float roughness;
uniform float ao;
uniform vec3 lightPositions[4];
uniform vec3 lightColors[4];
uniform vec3 viewPos;
const float PI = 3.14159265359;
// 法线分布函数
float DistributionGGX(vec3 N, vec3 H, float roughness) {
float a = roughness * roughness;
float a2 = a * a;
float NdotH = max(dot(N, H), 0.0);
float NdotH2 = NdotH * NdotH;
float num = a2;
float denom = (NdotH2 * (a2 - 1.0) + 1.0);
denom = PI * denom * denom;
return num / denom;
}
// 几何遮蔽函数
float GeometrySchlickGGX(float NdotV, float roughness) {
float r = (roughness + 1.0);
float k = (r * r) / 8.0;
float num = NdotV;
float denom = NdotV * (1.0 - k) + k;
return num / denom;
}
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) {
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
float ggx2 = GeometrySchlickGGX(NdotV, roughness);
float ggx1 = GeometrySchlickGGX(NdotL, roughness);
return ggx1 * ggx2;
}
// 菲涅尔方程
vec3 fresnelSchlick(float cosTheta, vec3 F0) {
return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
void main() {
vec3 N = normalize(Normal);
vec3 V = normalize(viewPos - FragPos);
vec3 F0 = vec3(0.04);
F0 = mix(F0, albedo, metallic);
vec3 Lo = vec3(0.0);
for (int i = 0; i < 4; ++i) {
vec3 L = normalize(lightPositions[i] - FragPos);
vec3 H = normalize(V + L);
float distance = length(lightPositions[i] - FragPos);
float attenuation = 1.0 / (distance * distance);
vec3 radiance = lightColors[i] * attenuation;
float NDF = DistributionGGX(N, H, roughness);
float G = GeometrySmith(N, V, L, roughness);
vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);
vec3 numerator = NDF * G * F;
float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.0001;
vec3 specular = numerator / denominator;
vec3 kS = F;
vec3 kD = vec3(1.0) - kS;
kD *= 1.0 - metallic;
float NdotL = max(dot(N, L), 0.0);
Lo += (kD * albedo / PI + specular) * radiance * NdotL;
}
vec3 ambient = vec3(0.03) * albedo * ao;
vec3 color = ambient + Lo;
// HDR 色调映射
color = color / (color + vec3(1.0));
// Gamma 校正
color = pow(color, vec3(1.0 / 2.2));
FragColor = vec4(color, 1.0);
}
2. 阴影映射
glsl
// 深度着色器(从光源视角渲染)
#version 460 core
layout (location = 0) in vec3 aPos;
uniform mat4 lightSpaceMatrix;
uniform mat4 model;
void main() {
gl_Position = lightSpaceMatrix * model * vec4(aPos, 1.0);
}
// 主渲染着色器(采样阴影)
float shadowCalculation(vec4 fragPosLightSpace) {
vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
projCoords = projCoords * 0.5 + 0.5;
float closestDepth = texture(shadowMap, projCoords.xy).r;
float currentDepth = projCoords.z;
vec3 normal = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);
float shadow = 0.0;
vec2 texelSize = 1.0 / textureSize(shadowMap, 0);
for (int x = -1; x <= 1; ++x) {
for (int y = -1; y <= 1; ++y) {
float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;
shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0;
}
}
shadow /= 9.0;
if (projCoords.z > 1.0)
shadow = 0.0;
return shadow;
}
3. 后处理效果
glsl
// 高斯模糊
#version 460 core
in vec2 TexCoords;
out vec4 FragColor;
uniform sampler2D screenTexture;
uniform bool horizontal;
const float weight[5] = float[](0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);
void main() {
vec2 tex_offset = 1.0 / textureSize(screenTexture, 0);
vec3 result = texture(screenTexture, TexCoords).rgb * weight[0];
if (horizontal) {
for (int i = 1; i < 5; ++i) {
result += texture(screenTexture, TexCoords + vec2(tex_offset.x * i, 0.0)).rgb * weight[i];
result += texture(screenTexture, TexCoords - vec2(tex_offset.x * i, 0.0)).rgb * weight[i];
}
} else {
for (int i = 1; i < 5; ++i) {
result += texture(screenTexture, TexCoords + vec2(0.0, tex_offset.y * i)).rgb * weight[i];
result += texture(screenTexture, TexCoords - vec2(0.0, tex_offset.y * i)).rgb * weight[i];
}
}
FragColor = vec4(result, 1.0);
}
着色器编译和链接
1. 运行时编译
cpp
GLuint compileShader(GLenum type, const char* source) {
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
int success;
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success) {
char infoLog[512];
glGetShaderInfoLog(shader, 512, nullptr, infoLog);
std::cerr << "Shader compilation failed:\n" << infoLog << std::endl;
}
return shader;
}
GLuint createShaderProgram(const char* vertexSource, const char* fragmentSource) {
GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexSource);
GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentSource);
GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
int success;
glGetProgramiv(program, GL_LINK_STATUS, &success);
if (!success) {
char infoLog[512];
glGetProgramInfoLog(program, 512, nullptr, infoLog);
std::cerr << "Shader linking failed:\n" << infoLog << std::endl;
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return program;
}
2. SPIR-V 编译
bash
# 使用 glslangValidator 编译 GLSL 到 SPIR-V
glslangValidator -V shader.vert -o vert.spv
glslangValidator -V shader.frag -o frag.spv
# 使用 glslc(Google 的编译器)
glslc shader.vert -o vert.spv
glslc shader.frag -o frag.spv
性能优化
1. 减少分支
glsl
// 不好的写法
if (condition) {
result = expensiveFunction1();
} else {
result = expensiveFunction2();
}
// 好的写法
float result1 = expensiveFunction1();
float result2 = expensiveFunction2();
result = condition ? result1 : result2;
2. 使用内置函数
glsl
// 不好的写法
float length = sqrt(dot(v, v));
// 好的写法
float length = length(v);
3. 避免不必要的计算
glsl
// 不好的写法
void main() {
vec3 normal = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float NdotL = dot(normal, lightDir);
if (NdotL > 0.0) {
// 执行光照计算
}
}
// 好的写法
void main() {
vec3 normal = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float NdotL = max(dot(normal, lightDir), 0.0);
// 使用 NdotL 作为权重,避免分支
}
调试技巧
1. 使用调试工具
- RenderDoc:GPU 调试和帧分析
- NVIDIA Nsight:NVIDIA GPU 调试
- AMD Radeon GPU Profiler:AMD GPU 调试
2. 输出中间结果
glsl
// 输出法线作为颜色
FragColor = vec4(normal * 0.5 + 0.5, 1.0);
// 输出深度值
FragColor = vec4(vec3(gl_FragCoord.z), 1.0);
// 输出纹理坐标
FragColor = vec4(TexCoord, 0.0, 1.0);
实际应用
1. 游戏开发
- 角色渲染
- 环境效果
- 后处理
2. 电影制作
- 离线渲染
- 视觉特效
- 虚拟摄影
3. 科学可视化
- 体积渲染
- 流场可视化
- 医学图像
总结
着色器是现代图形渲染的核心,理解其工作原理对于图形编程至关重要:
- 着色器类型:顶点、片段、几何、计算
- 着色语言:GLSL、HLSL、SPIR-V
- 编程基础:数据类型、控制流、函数
- 高级技术:PBR、阴影、后处理
- 性能优化:减少分支、使用内置函数
参考资源
- Learn OpenGL - 优秀的 OpenGL 教程
- GLSL Specification - GLSL 规范
- HLSL Reference - HLSL 参考
- Khronos GLSL - GLSL 官方文档
- Shader Toy - 在线着色器实验