Chenyj Space
返回博客
·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. 科学可视化

  • 体积渲染
  • 流场可视化
  • 医学图像

总结

着色器是现代图形渲染的核心,理解其工作原理对于图形编程至关重要:

  1. 着色器类型:顶点、片段、几何、计算
  2. 着色语言:GLSL、HLSL、SPIR-V
  3. 编程基础:数据类型、控制流、函数
  4. 高级技术:PBR、阴影、后处理
  5. 性能优化:减少分支、使用内置函数

参考资源