引言:理解游戏渲染的本质

游戏渲染是将3D场景转换为2D屏幕图像的过程,这个过程充满了数学、物理和艺术的结合。对于初学者来说,渲染管线(Rendering Pipeline)是一个抽象但至关重要的概念。简单来说,渲染管线就像一条工厂流水线,顶点数据(Vertex Data)作为原材料进入,经过一系列处理步骤,最终输出为屏幕上的像素颜色。

现代游戏渲染技术主要依赖于光栅化(Rasterization)光线追踪(Ray Tracing)两种核心方法。光栅化速度快,适合实时渲染,而光线追踪能产生极其逼真的效果,但计算成本高昂。目前主流游戏引擎如Unreal Engine和Unity都以光栅化为基础,通过各种技巧模拟光线追踪的效果。

要掌握光影材质与着色器,我们需要理解三个核心支柱:

  1. 光照模型(Lighting Models):计算物体表面如何反射光线
  2. 材质系统(Material Systems):定义物体表面的视觉属性
  3. 着色器编程(Shader Programming):实现上述计算的代码逻辑

第一部分:光照模型基础

1.1 光照的物理基础

真实世界的光照遵循能量守恒定律——入射光的能量等于反射光的能量加上吸收的能量。在计算机图形学中,我们通常使用BRDF(Bidirectional Reflectance Distribution Function,双向反射分布函数)来描述表面如何反射光线。

最常用的BRDF模型是Cook-Torrance模型,它将反射分为漫反射(Diffuse)镜面反射(Specular)两部分:

最终颜色 = 漫反射 + 镜面反射

1.2 经典光照模型

1.2.1 Phong光照模型

Phong模型是早期但非常经典的模型,包含三个主要分量:

// GLSL伪代码示例
vec3 calculatePhongLighting(vec3 normal, vec3 viewDir, vec3 lightDir, vec3 lightColor) {
    // 环境光分量 (Ambient)
    vec3 ambient = 0.1 * lightColor;
    
    // 漫反射分量 (Diffuse) - 使用兰伯特余弦定律
    float diff = max(dot(normal, lightDir), 0.0);
    vec3 diffuse = diff * lightColor;
    
    // 镜面反射分量 (Specular) - 使用Phong反射模型
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
    vec3 specular = spec * lightColor;
    
    return ambient + diffuse + specular;
}

代码解析

  • ambient:模拟间接光照,即使没有直接光照的区域也有微弱亮度
  • diffuse:计算光线与表面法线夹角的余弦值,遵循兰伯特余弦定律
  • specular:计算反射光与视线方向的夹角,产生高光效果

1.2.2 Blinn-Phong改进模型

Blinn-Phong通过引入半角向量(Half-vector)优化了镜面反射计算:

vec3 calculateBlinnPhongLighting(vec3 normal, vec3 viewDir, vec3 lightDir, vec3 lightColor) {
    // 漫反射相同
    float diff = max(dot(normal, lightDir), 0.0);
    vec3 diffuse = diff * lightColor;
    
    // 使用半角向量计算镜面反射
    vec3 halfwayDir = normalize(lightDir + viewDir);
    float spec = pow(max(dot(normal, halfwayDir), 0.0), 32.0);
    vec3 specular = spec * lightColor;
    
    return diffuse + specular;
}

优势:计算效率更高,且能产生更自然的高光形状。

1.3 现代PBR光照模型

PBR(Physically Based Rendering,基于物理的渲染)是现代游戏的标准。它基于物理定律,确保材质在不同光照条件下表现一致。

1.3.1 PBR核心参数

PBR材质通常由以下参数定义:

  • Base Color:基础颜色(反照率)
  • Metallic:金属度(0=绝缘体,1=金属)
  • Roughness:粗糙度(0=完美镜面,1=完全漫反射)
  • Normal:法线贴图
  • AO:环境光遮蔽

1.3.2 PBR光照计算示例

// 简化的PBR光照计算
vec3 calculatePBRLighting(vec3 N, vec3 V, vec3 L, vec3 baseColor, float metallic, float roughness) {
    vec3 H = normalize(V + L);
    
    // 计算几何遮蔽和法线分布
    float NdotV = max(dot(N, V), 0.0001);
    float NdotL = max(dot(N, L), 0.0001);
    float NdotH = max(dot(N, H), 0.0);
    float VdotH = max(dot(V, H), 0.0);
    
    // 菲涅尔项(Fresnel)
    vec3 F0 = mix(vec3(0.04), baseColor, metallic);
    vec3 F = fresnelSchlick(VdotH, F0);
    
    // 法线分布函数(D)
    float D = distributionGGX(NdotH, roughness);
    
    // 几何遮蔽函数(G)
    float G = geometrySmith(NdotV, NdotL, roughness);
    
    // BRDF计算
    vec3 numerator = D * G * F;
    float denominator = 4.0 * NdotV * NdotL;
    vec3 specular = numerator / max(denominator, 0.001);
    
    // 漫反射和镜面反射能量守恒
    vec3 kD = (vec3(1.0) - F) * (1.0 - metallic);
    
    // 最终结果
    return (kD * baseColor / PI + specular) * NdotL;
}

// 辅助函数
vec3 fresnelSchlick(float cosTheta, vec3 F0) {
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}

float distributionGGX(float NdotH, float roughness) {
    float a = roughness * roughness;
    float a2 = a * a;
    float denom = NdotH * NdotH * (a2 - 1.0) + 1.0;
    return a2 / (PI * denom * denom);
}

float geometrySmith(float NdotV, float NdotL, float roughness) {
    float r = roughness + 1.0;
    float k = (r * r) / 8.0;
    float G1 = NdotV / (NdotV * (1.0 - k) + k);
    float G2 = NdotL / (NdotL * (1.0 - k) + k);
    return G1 * G2;
}

代码解析

  • Fresnel(菲涅尔):描述光线在不同角度下的反射率,掠射角反射更强
  • D项(法线分布):描述微观表面法线分布,决定高光形状
  • G项(几何遮蔽):描述表面微自阴影,防止高光过亮
  • 能量守恒:确保漫反射和镜面反射的总和不超过入射光能量

第二部分:材质系统详解

2.1 材质的物理属性

材质本质上是描述表面如何与光交互的属性集合。在PBR系统中,材质分为两大类:

2.1.1 绝缘体(Dielectrics)

  • 非金属材质:塑料、木材、石材、油漆
  • 反射率低(约4%),无金属颜色
  • 高光颜色为白色或淡色

2.1.2 导体(Conductors)

  • 金属材质:金、银、铜、铁
  • 反射率高(70-100%),有金属颜色
  • 高光颜色等于金属颜色

2.2 材质贴图系统

现代游戏使用多种贴图来定义材质属性:

2.2.1 基础颜色贴图(Albedo/Color Map)

vec3 baseColor = texture(albedoMap, uv).rgb;
  • 包含物体表面的颜色和图案
  • 对于金属,这是金属颜色;对于绝缘体,这是表面颜色

2.2.2 法线贴图(Normal Map)

vec3 normalMap = texture(normalMap, uv).rgb * 2.0 - 1.0;
vec3 N = normalize(TBN * normalMap); // TBN是切线空间矩阵
  • 通过RGB通道存储表面法线偏移
  • 创造表面凹凸细节而不增加几何复杂度

2.2.3 高度/视差贴图(Height/Parallax Map)

float height = texture(heightMap, uv).r;
vec2 parallaxUV = uv - viewDir.xy * height * scale;
  • 模拟深度感,通过UV偏移创造真实凹凸
  • 比法线贴图更消耗性能

2.2.4 粗糙度/光泽度贴图(Roughness/Glossiness Map)

float roughness = texture(roughnessMap, uv).r;
  • 控制表面微观粗糙度
  • 决定高光的锐利程度

2.2.5 金属度贴图(Metallic Map)

float metallic = texture(metallicMap, uv).r;
  • 区分金属和非金属区域
  • 通常与粗糙度合并到同一张贴图的RG通道

2.2.6 环境光遮蔽贴图(AO Map)

float ao = texture(aoMap, uv).r;
  • 预计算的静态阴影
  • 增强角落和缝隙的深度感

2.3 材质工作流

2.3.1 Metallic-Roughness工作流(主流)

  • Base Color:RGB颜色
  • Metallic:单通道(通常存储在RGB中)
  • Roughness:单通道(通常存储在RGB中)
  • AO:单通道

典型纹理打包

Metallic(R) Roughness(G) AO(B) - 一张纹理

2.3.2 Specular-Glossiness工作流

  • Diffuse:RGB颜色
  • Specular:RGB高光颜色
  • Glossiness:光泽度(1=锐利高光)

2.4 材质实例化与参数化

在游戏引擎中,材质通常通过实例化(Instancing)来复用:

// Unity Shader Graph示例(概念代码)
Material metalMaterial = new Material(basePBRShader);
metalMaterial.SetTexture("_Albedo", metalTexture);
metalMaterial.SetFloat("_Metallic", 1.0f);
metalMaterial.SetFloat("_Roughness", 0.2f);

Material plasticMaterial = new Material(basePBRShader);
plasticMaterial.SetTexture("_Albedo", plasticTexture);
plasticMaterial.SetFloat("_Metallic", 0.0f);
plasticMaterial.SetFloat("_Roughness", 0.3f);

第三部分:着色器编程核心技巧

3.1 着色器语言基础

3.1.1 GLSL(OpenGL Shading Language)

#version 330 core

// 输入变量(从顶点着色器传递)
in vec3 FragPos;
in vec3 Normal;
in vec2 UV;

// 输出变量
out vec4 FragColor;

// Uniform变量(CPU传递)
uniform vec3 lightPos;
uniform vec3 viewPos;
uniform sampler2D albedoMap;

void main() {
    // 主要渲染逻辑
    vec3 color = texture(albedoMap, UV).rgb;
    FragColor = vec4(color, 1.0);
}

3.1.2 HLSL(High-Level Shading Language)

// Unity ShaderLab中的HLSL
Shader "Custom/PBRShader" {
    Properties {
        _Albedo ("Albedo", 2D) = "white" {}
        _Metallic ("Metallic", Range(0,1)) = 0.0
        _Roughness ("Roughness", Range(0,1)) = 0.5
    }
    
    SubShader {
        Tags { "RenderType"="Opaque" }
        
        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows
        
        sampler2D _Albedo;
        float _Metallic;
        float _Roughness;
        
        struct Input {
            float2 uv_Albedo;
        };
        
        void surf (Input IN, inout SurfaceOutputStandard o) {
            fixed4 c = tex2D(_Albedo, IN.uv_Albedo);
            o.Albedo = c.rgb;
            o.Metallic = _Metallic;
            o.Smoothness = 1.0 - _Roughness;
        }
        ENDCG
    }
}

3.2 顶点着色器与片元着色器

3.2.1 顶点着色器(Vertex Shader)

#version 330 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 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 * model * vec4(aPos, 1.0);
}

3.2.2 片元着色器(Fragment Shader)

#version 330 core
out vec4 FragColor;

in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;

uniform vec3 lightPos;
uniform vec3 viewPos;
uniform vec3 lightColor;
uniform sampler2D albedoMap;
uniform float metallic;
uniform float roughness;

void main() {
    // 获取基础颜色
    vec3 albedo = texture(albedoMap, TexCoord).rgb;
    
    // 计算光照
    vec3 N = normalize(Normal);
    vec3 V = normalize(viewPos - FragPos);
    vec3 L = normalize(lightPos - FragPos);
    
    // 简化的PBR计算
    vec3 color = calculatePBRLighting(N, V, L, albedo, metallic, roughness);
    
    FragColor = vec4(color, 1.0);
}

3.3 高级着色器技巧

3.3.1 多通道渲染(Multi-pass Rendering)

// 第一次通过:计算基础光照
// 第二次通过:添加镜面反射
// 第三次通过:添加后期处理效果

3.3.2 延迟着色(Deferred Shading)

// 几何缓冲区(G-Buffer)填充
#version 330 core
layout (location = 0) out vec3 gPosition;
layout (location = 1) out vec3 gNormal;
layout (location = 2) out vec4 gAlbedoSpec;

void main() {
    gPosition = FragPos;
    gNormal = normalize(Normal);
    gAlbedoSpec.rgb = texture(albedoMap, TexCoord).rgb;
    gAlbedoSpec.a = texture(metallicMap, TexCoord).r;
}

3.3.3 计算着色器(Compute Shader)用于光照计算

#version 430 core
layout(local_size_x = 16, local_size_y = 16) in;

layout(rgba32f, binding = 0) uniform image2D outputImage;

void main() {
    ivec2 pixel = ivec2(gl_GlobalInvocationID.xy);
    // 并行计算每个像素的光照
    // 适合GPU加速的全局光照计算
}

3.4 着色器优化技巧

3.4.1 避免分支发散

// 不好的做法:分支导致性能下降
if (metallic > 0.5) {
    // 复杂计算
} else {
    // 另一种计算
}

// 好的做法:使用lerp或step函数
float metallicThreshold = step(0.5, metallic);
vec3 result = mix(nonMetalResult, metalResult, metallicThreshold);

3.4.2 使用预计算和查找表

// 预计算复杂的数学函数
uniform sampler2D brdfLUT; // BRDF查找表

// 在片段着色器中使用
vec2 brdf = texture(brdfLUT, vec2(NdotV, roughness)).xy;

3.4.3 减少纹理采样

// 合并纹理通道
// 将Metallic、Roughness、AO打包到一张纹理的RGB通道
vec3 packedData = texture(packedMap, uv).rgb;
float metallic = packedData.r;
float roughness = packedData.g;
float ao = packedData.b;

第四部分:光影效果实现技巧

4.1 阴影技术

4.1.1 阴影贴图(Shadow Mapping)

// 阴影贴图生成(从光源视角渲染)
// 片元着色器中计算阴影
float calculateShadow(sampler2D shadowMap, vec4 fragPosLightSpace, vec3 normal, vec3 lightDir) {
    // 透视除法
    vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
    projCoords = projCoords * 0.5 + 0.5; // 转换到[0,1]范围
    
    // 深度比较
    float closestDepth = texture(shadowMap, projCoords.xy).r;
    float currentDepth = projCoords.z;
    
    // 阴影偏移(防止阴影痤疮)
    float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);
    
    // 软阴影采样(PCF)
    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;
    
    return shadow;
}

4.1.2 级联阴影贴图(CSM)

用于解决阴影贴图分辨率不足导致的锯齿问题:

  • 近处使用高分辨率阴影贴图
  • 远处使用低分辨率阴影贴图
  • 根据距离选择不同的阴影贴图

4.1.3 方差阴影贴图(VSM)

// VSM存储深度和深度平方
vec2 moments = texture(shadowMap, projCoords.xy).rg;
float p = (currentDepth <= moments.x) ? 1.0 : 0.0;
float variance = moments.y - moments.x * moments.x;
float p_max = variance / (variance + 0.00001);
return min(p, p_max);

4.2 全局光照(Global Illumination)

4.2.1 辐射度(Radiosity)

// 预计算光照贴图(Lightmap)
// 在运行时采样
vec3 indirectLight = texture(lightmap, uv).rgb;

4.2.2 屏幕空间环境光遮蔽(SSAO)

// SSAO计算
float calculateSSAO(vec3 fragPos, vec3 normal, vec2 uv) {
    float occlusion = 0.0;
    for(int i = 0; i < kernelSize; ++i) {
        // 采样周围点
        vec3 samplePos = fragPos + kernel[i] * radius;
        // 投影到屏幕空间
        vec4 offset = vec4(samplePos, 1.0);
        offset = projection * offset;
        offset.xy /= offset.w;
        offset.xy = offset.xy * 0.5 + 0.5;
        
        // 深度比较
        float sampleDepth = texture(gPosition, offset.xy).z;
        float rangeCheck = smoothstep(0.0, 1.0, radius / abs(fragPos.z - sampleDepth));
        occlusion += (sampleDepth >= samplePos.z + bias ? 1.0 : 0.0) * rangeCheck;
    }
    occlusion = 1.0 - (occlusion / kernelSize);
    return occlusion;
}

4.2.3 屏幕空间反射(SSR)

// 简化的SSR实现
vec3 screenSpaceReflection(vec3 viewPos, vec3 reflectionDir) {
    // 射线步进
    vec3 rayPos = viewPos;
    vec3 rayDir = reflectionDir;
    
    for(int i = 0; i < maxSteps; ++i) {
        rayPos += rayDir * stepSize;
        
        // 转换到屏幕空间
        vec4 projCoords = projection * vec4(rayPos, 1.0);
        projCoords.xy /= projCoords.w;
        projCoords.xy = projCoords.xy * 0.5 + 0.5;
        
        // 深度测试
        float sceneDepth = texture(depthTexture, projCoords.xy).r;
        if(projCoords.z > sceneDepth) {
            // 找到交点
            return texture(sceneTexture, projCoords.xy).rgb;
        }
    }
    return vec3(0.0); // 无反射
}

4.3 后期处理效果

4.3.1 泛光(Bloom)

// 提取亮部
vec3 extractBright(vec3 color) {
    return max(color - 0.8, 0.0) * (1.0 / (1.0 - 0.8));
}

// 高斯模糊
vec3 gaussianBlur(sampler2D tex, vec2 uv, vec2 direction) {
    vec3 color = vec3(0.0);
    float weights[5] = float[](0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);
    vec2 texelSize = 1.0 / textureSize(tex, 0);
    
    color += texture(tex, uv).rgb * weights[0];
    for(int i = 1; i < 5; ++i) {
        vec2 offset = vec2(float(i)) * texelSize * direction;
        color += texture(tex, uv + offset).rgb * weights[i];
        color += texture(tex, uv - offset).rgb * weights[i];
    }
    return color;
}

4.3.2 色调映射(Tone Mapping)

// ACES色调映射
vec3 ACESFilm(vec3 x) {
    const float a = 2.51;
    const float b = 0.03;
    const float c = 2.43;
    const float d = 0.59;
    const float e = 0.14;
    return clamp((x*(a*x+b))/(x*(c*x+d)+e), 0.0, 1.0);
}

// 应用
vec3 finalColor = ACESFilm(hdrColor);

4.3.3 色差(Chromatic Aberration)

vec3 chromaticAberration(sampler2D tex, vec2 uv, float amount) {
    vec2 direction = uv - 0.5;
    vec3 color;
    color.r = texture(tex, uv + direction * amount).r;
    color.g = texture(tex, uv).g;
    color.b = texture(tex, uv - direction * amount).b;
    return color;
}

第五部分:实战案例与完整代码

5.1 完整PBR着色器实现

下面是一个完整的、可运行的PBR着色器示例:

#version 330 core
out vec4 FragColor;

// 输入变量
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;

// Uniform变量
struct Light {
    vec3 position;
    vec3 color;
    float intensity;
};

uniform Light lights[4]; // 支持4个光源
uniform vec3 viewPos;
uniform sampler2D albedoMap;
uniform sampler2D normalMap;
uniform sampler2D metallicMap;
uniform sampler2D roughnessMap;
uniform sampler2D aoMap;
uniform sampler2D irradianceMap; // 预计算的环境光照
uniform sampler2D prefilterMap;  // 预计算的反射
uniform sampler2D brdfLUT;       // BRDF查找表

// 常量
const float PI = 3.14159265359;

// 辅助函数声明
vec3 fresnelSchlick(float cosTheta, vec3 F0);
float distributionGGX(vec3 N, vec3 H, float roughness);
float geometrySchlickGGX(float NdotV, float roughness);
float geometrySmith(vec3 N, vec3 V, vec3 L, float roughness);
vec3 getNormalFromMap();

void main() {
    // 获取材质参数
    vec3 albedo = texture(albedoMap, TexCoord).rgb;
    float metallic = texture(metallicMap, TexCoord).r;
    float roughness = texture(roughnessMap, TexCoord).r;
    float ao = texture(aoMap, TexCoord).r;
    
    // 获取法线
    vec3 N = getNormalFromMap();
    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) {
        if(lights[i].intensity <= 0.0) continue;
        
        vec3 L = normalize(lights[i].position - FragPos);
        vec3 H = normalize(V + L);
        float distance = length(lights[i].position - FragPos);
        float attenuation = 1.0 / (distance * distance);
        vec3 radiance = lights[i].color * lights[i].intensity * attenuation;
        
        float NdotL = max(dot(N, L), 0.0);
        float NdotV = max(dot(N, V), 0.0);
        float NdotH = max(dot(N, H), 0.0);
        float VdotH = max(dot(V, H), 0.0);
        
        // 计算BRDF
        float D = distributionGGX(N, H, roughness);
        float G = geometrySmith(N, V, L, roughness);
        vec3 F = fresnelSchlick(VdotH, F0);
        
        vec3 numerator = D * G * F;
        float denominator = 4.0 * max(NdotV, 0.001) * max(NdotL, 0.001);
        vec3 specular = numerator / denominator;
        
        vec3 kD = (vec3(1.0) - F) * (1.0 - metallic);
        
        Lo += (kD * albedo / PI + specular) * radiance * NdotL;
    }
    
    // 环境光照(IBL)
    vec3 F = fresnelSchlick(max(dot(N, V), 0.0), F0);
    vec3 kD = (vec3(1.0) - F) * (1.0 - metallic);
    
    vec3 irradiance = texture(irradianceMap, N.xy).rgb; // 简化采样
    vec3 diffuse = irradiance * albedo;
    
    // 预计算环境反射
    vec3 R = reflect(-V, N);
    // 简化:实际应使用粗糙度选择mipmap
    vec3 prefilteredColor = texture(prefilterMap, R.xy).rgb;
    
    // BRDF积分
    vec2 brdf = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).xy;
    vec3 specular = prefilteredColor * (F * brdf.x + brdf.y);
    
    vec3 ambient = (kD * diffuse + specular) * ao;
    
    // 最终颜色
    vec3 color = ambient + Lo;
    
    // HDR色调映射
    color = color / (color + vec3(1.0));
    color = pow(color, vec3(1.0/2.2)); // Gamma校正
    
    FragColor = vec4(color, 1.0);
}

// 实现辅助函数
vec3 fresnelSchlick(float cosTheta, vec3 F0) {
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}

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 nom = a2;
    float denom = (NdotH2 * (a2 - 1.0) + 1.0);
    denom = PI * denom * denom;
    
    return nom / denom;
}

float geometrySchlickGGX(float NdotV, float roughness) {
    float r = roughness + 1.0;
    float k = (r * r) / 8.0;
    return NdotV / (NdotV * (1.0 - k) + k);
}

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 getNormalFromMap() {
    vec3 tangentNormal = texture(normalMap, TexCoord).xyz * 2.0 - 1.0;
    
    // 构建TBN矩阵(简化版)
    vec3 Q1 = dFdx(FragPos);
    vec3 Q2 = dFdy(FragPos);
    vec2 st1 = dFdx(TexCoord);
    vec2 st2 = dFdy(TexCoord);
    
    vec3 T = normalize(Q1 * st2.t - Q2 * st1.t);
    vec3 N = normalize(Normal);
    vec3 B = normalize(cross(N, T));
    
    mat3 TBN = mat3(T, B, N);
    return normalize(TBN * tangentNormal);
}

5.2 Unity Shader Graph 实现

在Unity中,可以使用Shader Graph可视化工具:

  1. 创建PBR Graph

    • 创建Lit Master Stack
    • 添加Texture2D节点用于Albedo、Normal、Metallic、Roughness
    • 连接到Master Stack的对应输入
  2. 自定义光照节点

    • 使用Custom Function节点编写HLSL代码
    • 或者使用节点组合实现复杂效果
  3. 示例节点连接

    [Albedo Texture] -> [Sample Texture 2D] -> [Base Color]
    [Normal Texture] -> [Sample Texture 2D] -> [Normal]
    [Metallic Texture] -> [Sample Texture 2D] -> [Metallic]
    [Roughness Texture] -> [Sample Texture 2D] -> [Roughness]
    

5.3 性能优化建议

5.3.1 着色器优化

  • 减少纹理采样:合并纹理通道
  • 减少计算复杂度:使用预计算值
  • 避免动态分支:使用lerp/step函数
  • 使用LOD:根据距离选择不同复杂度的着色器

5.3.2 渲染管线优化

  • 实例化渲染:相同材质物体批量渲染
  • GPU Instancing:减少Draw Call
  • 剔除(Culling):移除不可见物体
  • 批处理(Batching):合并相同材质的物体

5.3.3 内存优化

  • 纹理压缩:使用ASTC/ETC2/DXT格式
  • 纹理图集:合并小纹理
  • Mipmap:根据距离使用不同分辨率

第六部分:学习路径与资源推荐

6.1 学习路径建议

阶段1:基础(2-4周)

  • 学习线性代数(矩阵、向量、变换)
  • 理解渲染管线基本概念
  • 掌握GLSL/HLSL基础语法
  • 实现简单的Phong光照

阶段2:中级(4-8周)

  • 深入理解PBR理论
  • 实现完整的PBR着色器
  • 学习法线贴图、视差贴图
  • 掌握阴影技术

阶段3:高级(8-12周)

  • 实现SSAO、SSR等后期效果
  • 学习延迟着色
  • 掌握计算着色器
  • 优化渲染性能

阶段4:专家(持续)

  • 研究光线追踪
  • 实现体积渲染
  • 探索实时GI技术
  • 参与开源项目

6.2 推荐资源

6.2.1 在线教程

6.2.2 书籍

  • 《Real-Time Rendering》:渲染领域的圣经
  • 《Physically Based Rendering》:PBR理论详解
  • 《OpenGL编程指南》:OpenGL权威参考

6.2.3 开源项目

6.2.4 工具

  • RenderDoc:GPU调试工具
  • NVIDIA Nsight:性能分析工具
  • Shader Playground:在线着色器编辑器

6.3 实践项目建议

项目1:简单PBR渲染器

  • 实现基础PBR光照
  • 支持1-2个光源
  • 加载模型和纹理

项目2:材质编辑器

  • 可视化材质参数调整
  • 实时预览效果
  • 导出材质配置

项目3:完整渲染引擎

  • 实现延迟着色管线
  • 包含阴影、SSAO、Bloom
  • 支持多种光源类型

结语

掌握游戏渲染技术是一个持续学习和实践的过程。从基础的Phong模型到复杂的PBR系统,再到现代的实时渲染技术,每一步都需要扎实的理论基础和大量的实践。

记住以下关键点:

  1. 理论与实践结合:理解物理原理,但也要知道如何近似
  2. 性能与质量平衡:实时渲染需要在两者间找到平衡
  3. 持续学习:渲染技术发展迅速,保持学习新技术
  4. 调试与分析:使用专业工具分析和优化渲染效果

通过系统学习和不断实践,你将能够创建出令人惊叹的渲染效果,为游戏带来视觉上的震撼体验。祝你学习顺利!# 游戏渲染学习从零开始掌握光影材质与着色器核心技巧

引言:理解游戏渲染的本质

游戏渲染是将3D场景转换为2D屏幕图像的过程,这个过程充满了数学、物理和艺术的结合。对于初学者来说,渲染管线(Rendering Pipeline)是一个抽象但至关重要的概念。简单来说,渲染管线就像一条工厂流水线,顶点数据(Vertex Data)作为原材料进入,经过一系列处理步骤,最终输出为屏幕上的像素颜色。

现代游戏渲染技术主要依赖于光栅化(Rasterization)光线追踪(Ray Tracing)两种核心方法。光栅化速度快,适合实时渲染,而光线追踪能产生极其逼真的效果,但计算成本高昂。目前主流游戏引擎如Unreal Engine和Unity都以光栅化为基础,通过各种技巧模拟光线追踪的效果。

要掌握光影材质与着色器,我们需要理解三个核心支柱:

  1. 光照模型(Lighting Models):计算物体表面如何反射光线
  2. 材质系统(Material Systems):定义物体表面的视觉属性
  3. 着色器编程(Shader Programming):实现上述计算的代码逻辑

第一部分:光照模型基础

1.1 光照的物理基础

真实世界的光照遵循能量守恒定律——入射光的能量等于反射光的能量加上吸收的能量。在计算机图形学中,我们通常使用BRDF(Bidirectional Reflectance Distribution Function,双向反射分布函数)来描述表面如何反射光线。

最常用的BRDF模型是Cook-Torrance模型,它将反射分为漫反射(Diffuse)镜面反射(Specular)两部分:

最终颜色 = 漫反射 + 镜面反射

1.2 经典光照模型

1.2.1 Phong光照模型

Phong模型是早期但非常经典的模型,包含三个主要分量:

// GLSL伪代码示例
vec3 calculatePhongLighting(vec3 normal, vec3 viewDir, vec3 lightDir, vec3 lightColor) {
    // 环境光分量 (Ambient)
    vec3 ambient = 0.1 * lightColor;
    
    // 漫反射分量 (Diffuse) - 使用兰伯特余弦定律
    float diff = max(dot(normal, lightDir), 0.0);
    vec3 diffuse = diff * lightColor;
    
    // 镜面反射分量 (Specular) - 使用Phong反射模型
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
    vec3 specular = spec * lightColor;
    
    return ambient + diffuse + specular;
}

代码解析

  • ambient:模拟间接光照,即使没有直接光照的区域也有微弱亮度
  • diffuse:计算光线与表面法线夹角的余弦值,遵循兰伯特余弦定律
  • specular:计算反射光与视线方向的夹角,产生高光效果

1.2.2 Blinn-Phong改进模型

Blinn-Phong通过引入半角向量(Half-vector)优化了镜面反射计算:

vec3 calculateBlinnPhongLighting(vec3 normal, vec3 viewDir, vec3 lightDir, vec3 lightColor) {
    // 漫反射相同
    float diff = max(dot(normal, lightDir), 0.0);
    vec3 diffuse = diff * lightColor;
    
    // 使用半角向量计算镜面反射
    vec3 halfwayDir = normalize(lightDir + viewDir);
    float spec = pow(max(dot(normal, halfwayDir), 0.0), 32.0);
    vec3 specular = spec * lightColor;
    
    return diffuse + specular;
}

优势:计算效率更高,且能产生更自然的高光形状。

1.3 现代PBR光照模型

PBR(Physically Based Rendering,基于物理的渲染)是现代游戏的标准。它基于物理定律,确保材质在不同光照条件下表现一致。

1.3.1 PBR核心参数

PBR材质通常由以下参数定义:

  • Base Color:基础颜色(反照率)
  • Metallic:金属度(0=绝缘体,1=金属)
  • Roughness:粗糙度(0=完美镜面,1=完全漫反射)
  • Normal:法线贴图
  • AO:环境光遮蔽

1.3.2 PBR光照计算示例

// 简化的PBR光照计算
vec3 calculatePBRLighting(vec3 N, vec3 V, vec3 L, vec3 baseColor, float metallic, float roughness) {
    vec3 H = normalize(V + L);
    
    // 计算几何遮蔽和法线分布
    float NdotV = max(dot(N, V), 0.0001);
    float NdotL = max(dot(N, L), 0.0001);
    float NdotH = max(dot(N, H), 0.0);
    float VdotH = max(dot(V, H), 0.0);
    
    // 菲涅尔项(Fresnel)
    vec3 F0 = mix(vec3(0.04), baseColor, metallic);
    vec3 F = fresnelSchlick(VdotH, F0);
    
    // 法线分布函数(D)
    float D = distributionGGX(NdotH, roughness);
    
    // 几何遮蔽函数(G)
    float G = geometrySmith(NdotV, NdotL, roughness);
    
    // BRDF计算
    vec3 numerator = D * G * F;
    float denominator = 4.0 * NdotV * NdotL;
    vec3 specular = numerator / max(denominator, 0.001);
    
    // 漫反射和镜面反射能量守恒
    vec3 kD = (vec3(1.0) - F) * (1.0 - metallic);
    
    // 最终结果
    return (kD * baseColor / PI + specular) * NdotL;
}

// 辅助函数
vec3 fresnelSchlick(float cosTheta, vec3 F0) {
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}

float distributionGGX(float NdotH, float roughness) {
    float a = roughness * roughness;
    float a2 = a * a;
    float denom = NdotH * NdotH * (a2 - 1.0) + 1.0;
    return a2 / (PI * denom * denom);
}

float geometrySmith(float NdotV, float NdotL, float roughness) {
    float r = roughness + 1.0;
    float k = (r * r) / 8.0;
    float G1 = NdotV / (NdotV * (1.0 - k) + k);
    float G2 = NdotL / (NdotL * (1.0 - k) + k);
    return G1 * G2;
}

代码解析

  • Fresnel(菲涅尔):描述光线在不同角度下的反射率,掠射角反射更强
  • D项(法线分布):描述微观表面法线分布,决定高光形状
  • G项(几何遮蔽):描述表面微自阴影,防止高光过亮
  • 能量守恒:确保漫反射和镜面反射的总和不超过入射光能量

第二部分:材质系统详解

2.1 材质的物理属性

材质本质上是描述表面如何与光交互的属性集合。在PBR系统中,材质分为两大类:

2.1.1 绝缘体(Dielectrics)

  • 非金属材质:塑料、木材、石材、油漆
  • 反射率低(约4%),无金属颜色
  • 高光颜色为白色或淡色

2.1.2 导体(Conductors)

  • 金属材质:金、银、铜、铁
  • 反射率高(70-100%),有金属颜色
  • 高光颜色等于金属颜色

2.2 材质贴图系统

现代游戏使用多种贴图来定义材质属性:

2.2.1 基础颜色贴图(Albedo/Color Map)

vec3 baseColor = texture(albedoMap, uv).rgb;
  • 包含物体表面的颜色和图案
  • 对于金属,这是金属颜色;对于绝缘体,这是表面颜色

2.2.2 法线贴图(Normal Map)

vec3 normalMap = texture(normalMap, uv).rgb * 2.0 - 1.0;
vec3 N = normalize(TBN * normalMap); // TBN是切线空间矩阵
  • 通过RGB通道存储表面法线偏移
  • 创造表面凹凸细节而不增加几何复杂度

2.2.3 高度/视差贴图(Height/Parallax Map)

float height = texture(heightMap, uv).r;
vec2 parallaxUV = uv - viewDir.xy * height * scale;
  • 模拟深度感,通过UV偏移创造真实凹凸
  • 比法线贴图更消耗性能

2.2.4 粗糙度/光泽度贴图(Roughness/Glossiness Map)

float roughness = texture(roughnessMap, uv).r;
  • 控制表面微观粗糙度
  • 决定高光的锐利程度

2.2.5 金属度贴图(Metallic Map)

float metallic = texture(metallicMap, uv).r;
  • 区分金属和非金属区域
  • 通常与粗糙度合并到同一张贴图的RG通道

2.2.6 环境光遮蔽贴图(AO Map)

float ao = texture(aoMap, uv).r;
  • 预计算的静态阴影
  • 增强角落和缝隙的深度感

2.3 材质工作流

2.3.1 Metallic-Roughness工作流(主流)

  • Base Color:RGB颜色
  • Metallic:单通道(通常存储在RGB中)
  • Roughness:单通道(通常存储在RGB中)
  • AO:单通道

典型纹理打包

Metallic(R) Roughness(G) AO(B) - 一张纹理

2.3.2 Specular-Glossiness工作流

  • Diffuse:RGB颜色
  • Specular:RGB高光颜色
  • Glossiness:光泽度(1=锐利高光)

2.4 材质实例化与参数化

在游戏引擎中,材质通常通过实例化(Instancing)来复用:

// Unity Shader Graph示例(概念代码)
Material metalMaterial = new Material(basePBRShader);
metalMaterial.SetTexture("_Albedo", metalTexture);
metalMaterial.SetFloat("_Metallic", 1.0f);
metalMaterial.SetFloat("_Roughness", 0.2f);

Material plasticMaterial = new Material(basePBRShader);
plasticMaterial.SetTexture("_Albedo", plasticTexture);
plasticMaterial.SetFloat("_Metallic", 0.0f);
plasticMaterial.SetFloat("_Roughness", 0.3f);

第三部分:着色器编程核心技巧

3.1 着色器语言基础

3.1.1 GLSL(OpenGL Shading Language)

#version 330 core

// 输入变量(从顶点着色器传递)
in vec3 FragPos;
in vec3 Normal;
in vec2 UV;

// 输出变量
out vec4 FragColor;

// Uniform变量(CPU传递)
uniform vec3 lightPos;
uniform vec3 viewPos;
uniform sampler2D albedoMap;

void main() {
    // 主要渲染逻辑
    vec3 color = texture(albedoMap, UV).rgb;
    FragColor = vec4(color, 1.0);
}

3.1.2 HLSL(High-Level Shading Language)

// Unity ShaderLab中的HLSL
Shader "Custom/PBRShader" {
    Properties {
        _Albedo ("Albedo", 2D) = "white" {}
        _Metallic ("Metallic", Range(0,1)) = 0.0
        _Roughness ("Roughness", Range(0,1)) = 0.5
    }
    
    SubShader {
        Tags { "RenderType"="Opaque" }
        
        CGPROGRAM
        #pragma surface surf Standard fullforwardshadows
        
        sampler2D _Albedo;
        float _Metallic;
        float _Roughness;
        
        struct Input {
            float2 uv_Albedo;
        };
        
        void surf (Input IN, inout SurfaceOutputStandard o) {
            fixed4 c = tex2D(_Albedo, IN.uv_Albedo);
            o.Albedo = c.rgb;
            o.Metallic = _Metallic;
            o.Smoothness = 1.0 - _Roughness;
        }
        ENDCG
    }
}

3.2 顶点着色器与片元着色器

3.2.1 顶点着色器(Vertex Shader)

#version 330 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 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 * model * vec4(aPos, 1.0);
}

3.2.2 片元着色器(Fragment Shader)

#version 330 core
out vec4 FragColor;

in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;

uniform vec3 lightPos;
uniform vec3 viewPos;
uniform vec3 lightColor;
uniform sampler2D albedoMap;
uniform float metallic;
uniform float roughness;

void main() {
    // 获取基础颜色
    vec3 albedo = texture(albedoMap, TexCoord).rgb;
    
    // 计算光照
    vec3 N = normalize(Normal);
    vec3 V = normalize(viewPos - FragPos);
    vec3 L = normalize(lightPos - FragPos);
    
    // 简化的PBR计算
    vec3 color = calculatePBRLighting(N, V, L, albedo, metallic, roughness);
    
    FragColor = vec4(color, 1.0);
}

3.3 高级着色器技巧

3.3.1 多通道渲染(Multi-pass Rendering)

// 第一次通过:计算基础光照
// 第二次通过:添加镜面反射
// 第三次通过:添加后期处理效果

3.3.2 延迟着色(Deferred Shading)

// 几何缓冲区(G-Buffer)填充
#version 330 core
layout (location = 0) out vec3 gPosition;
layout (location = 1) out vec3 gNormal;
layout (location = 2) out vec4 gAlbedoSpec;

void main() {
    gPosition = FragPos;
    gNormal = normalize(Normal);
    gAlbedoSpec.rgb = texture(albedoMap, TexCoord).rgb;
    gAlbedoSpec.a = texture(metallicMap, TexCoord).r;
}

3.3.3 计算着色器(Compute Shader)用于光照计算

#version 430 core
layout(local_size_x = 16, local_size_y = 16) in;

layout(rgba32f, binding = 0) uniform image2D outputImage;

void main() {
    ivec2 pixel = ivec2(gl_GlobalInvocationID.xy);
    // 并行计算每个像素的光照
    // 适合GPU加速的全局光照计算
}

3.4 着色器优化技巧

3.4.1 避免分支发散

// 不好的做法:分支导致性能下降
if (metallic > 0.5) {
    // 复杂计算
} else {
    // 另一种计算
}

// 好的做法:使用lerp或step函数
float metallicThreshold = step(0.5, metallic);
vec3 result = mix(nonMetalResult, metalResult, metallicThreshold);

3.4.2 使用预计算和查找表

// 预计算复杂的数学函数
uniform sampler2D brdfLUT; // BRDF查找表

// 在片段着色器中使用
vec2 brdf = texture(brdfLUT, vec2(NdotV, roughness)).xy;

3.4.3 减少纹理采样

// 合并纹理通道
// 将Metallic、Roughness、AO打包到一张纹理的RGB通道
vec3 packedData = texture(packedMap, uv).rgb;
float metallic = packedData.r;
float roughness = packedData.g;
float ao = packedData.b;

第四部分:光影效果实现技巧

4.1 阴影技术

4.1.1 阴影贴图(Shadow Mapping)

// 阴影贴图生成(从光源视角渲染)
// 片元着色器中计算阴影
float calculateShadow(sampler2D shadowMap, vec4 fragPosLightSpace, vec3 normal, vec3 lightDir) {
    // 透视除法
    vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
    projCoords = projCoords * 0.5 + 0.5; // 转换到[0,1]范围
    
    // 深度比较
    float closestDepth = texture(shadowMap, projCoords.xy).r;
    float currentDepth = projCoords.z;
    
    // 阴影偏移(防止阴影痤疮)
    float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);
    
    // 软阴影采样(PCF)
    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;
    
    return shadow;
}

4.1.2 级联阴影贴图(CSM)

用于解决阴影贴图分辨率不足导致的锯齿问题:

  • 近处使用高分辨率阴影贴图
  • 远处使用低分辨率阴影贴图
  • 根据距离选择不同的阴影贴图

4.1.3 方差阴影贴图(VSM)

// VSM存储深度和深度平方
vec2 moments = texture(shadowMap, projCoords.xy).rg;
float p = (currentDepth <= moments.x) ? 1.0 : 0.0;
float variance = moments.y - moments.x * moments.x;
float p_max = variance / (variance + 0.00001);
return min(p, p_max);

4.2 全局光照(Global Illumination)

4.2.1 辐射度(Radiosity)

// 预计算光照贴图(Lightmap)
// 在运行时采样
vec3 indirectLight = texture(lightmap, uv).rgb;

4.2.2 屏幕空间环境光遮蔽(SSAO)

// SSAO计算
float calculateSSAO(vec3 fragPos, vec3 normal, vec2 uv) {
    float occlusion = 0.0;
    for(int i = 0; i < kernelSize; ++i) {
        // 采样周围点
        vec3 samplePos = fragPos + kernel[i] * radius;
        // 投影到屏幕空间
        vec4 offset = vec4(samplePos, 1.0);
        offset = projection * offset;
        offset.xy /= offset.w;
        offset.xy = offset.xy * 0.5 + 0.5;
        
        // 深度比较
        float sampleDepth = texture(gPosition, offset.xy).z;
        float rangeCheck = smoothstep(0.0, 1.0, radius / abs(fragPos.z - sampleDepth));
        occlusion += (sampleDepth >= samplePos.z + bias ? 1.0 : 0.0) * rangeCheck;
    }
    occlusion = 1.0 - (occlusion / kernelSize);
    return occlusion;
}

4.2.3 屏幕空间反射(SSR)

// 简化的SSR实现
vec3 screenSpaceReflection(vec3 viewPos, vec3 reflectionDir) {
    // 射线步进
    vec3 rayPos = viewPos;
    vec3 rayDir = reflectionDir;
    
    for(int i = 0; i < maxSteps; ++i) {
        rayPos += rayDir * stepSize;
        
        // 转换到屏幕空间
        vec4 projCoords = projection * vec4(rayPos, 1.0);
        projCoords.xy /= projCoords.w;
        projCoords.xy = projCoords.xy * 0.5 + 0.5;
        
        // 深度测试
        float sceneDepth = texture(depthTexture, projCoords.xy).r;
        if(projCoords.z > sceneDepth) {
            // 找到交点
            return texture(sceneTexture, projCoords.xy).rgb;
        }
    }
    return vec3(0.0); // 无反射
}

4.3 后期处理效果

4.3.1 泛光(Bloom)

// 提取亮部
vec3 extractBright(vec3 color) {
    return max(color - 0.8, 0.0) * (1.0 / (1.0 - 0.8));
}

// 高斯模糊
vec3 gaussianBlur(sampler2D tex, vec2 uv, vec2 direction) {
    vec3 color = vec3(0.0);
    float weights[5] = float[](0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216);
    vec2 texelSize = 1.0 / textureSize(tex, 0);
    
    color += texture(tex, uv).rgb * weights[0];
    for(int i = 1; i < 5; ++i) {
        vec2 offset = vec2(float(i)) * texelSize * direction;
        color += texture(tex, uv + offset).rgb * weights[i];
        color += texture(tex, uv - offset).rgb * weights[i];
    }
    return color;
}

4.3.2 色调映射(Tone Mapping)

// ACES色调映射
vec3 ACESFilm(vec3 x) {
    const float a = 2.51;
    const float b = 0.03;
    const float c = 2.43;
    const float d = 0.59;
    const float e = 0.14;
    return clamp((x*(a*x+b))/(x*(c*x+d)+e), 0.0, 1.0);
}

// 应用
vec3 finalColor = ACESFilm(hdrColor);

4.3.3 色差(Chromatic Aberration)

vec3 chromaticAberration(sampler2D tex, vec2 uv, float amount) {
    vec2 direction = uv - 0.5;
    vec3 color;
    color.r = texture(tex, uv + direction * amount).r;
    color.g = texture(tex, uv).g;
    color.b = texture(tex, uv - direction * amount).b;
    return color;
}

第五部分:实战案例与完整代码

5.1 完整PBR着色器实现

下面是一个完整的、可运行的PBR着色器示例:

#version 330 core
out vec4 FragColor;

// 输入变量
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;

// Uniform变量
struct Light {
    vec3 position;
    vec3 color;
    float intensity;
};

uniform Light lights[4]; // 支持4个光源
uniform vec3 viewPos;
uniform sampler2D albedoMap;
uniform sampler2D normalMap;
uniform sampler2D metallicMap;
uniform sampler2D roughnessMap;
uniform sampler2D aoMap;
uniform sampler2D irradianceMap; // 预计算的环境光照
uniform sampler2D prefilterMap;  // 预计算的反射
uniform sampler2D brdfLUT;       // BRDF查找表

// 常量
const float PI = 3.14159265359;

// 辅助函数声明
vec3 fresnelSchlick(float cosTheta, vec3 F0);
float distributionGGX(vec3 N, vec3 H, float roughness);
float geometrySchlickGGX(float NdotV, float roughness);
float geometrySmith(vec3 N, vec3 V, vec3 L, float roughness);
vec3 getNormalFromMap();

void main() {
    // 获取材质参数
    vec3 albedo = texture(albedoMap, TexCoord).rgb;
    float metallic = texture(metallicMap, TexCoord).r;
    float roughness = texture(roughnessMap, TexCoord).r;
    float ao = texture(aoMap, TexCoord).r;
    
    // 获取法线
    vec3 N = getNormalFromMap();
    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) {
        if(lights[i].intensity <= 0.0) continue;
        
        vec3 L = normalize(lights[i].position - FragPos);
        vec3 H = normalize(V + L);
        float distance = length(lights[i].position - FragPos);
        float attenuation = 1.0 / (distance * distance);
        vec3 radiance = lights[i].color * lights[i].intensity * attenuation;
        
        float NdotL = max(dot(N, L), 0.0);
        float NdotV = max(dot(N, V), 0.0);
        float NdotH = max(dot(N, H), 0.0);
        float VdotH = max(dot(V, H), 0.0);
        
        // 计算BRDF
        float D = distributionGGX(N, H, roughness);
        float G = geometrySmith(N, V, L, roughness);
        vec3 F = fresnelSchlick(VdotH, F0);
        
        vec3 numerator = D * G * F;
        float denominator = 4.0 * max(NdotV, 0.001) * max(NdotL, 0.001);
        vec3 specular = numerator / denominator;
        
        vec3 kD = (vec3(1.0) - F) * (1.0 - metallic);
        
        Lo += (kD * albedo / PI + specular) * radiance * NdotL;
    }
    
    // 环境光照(IBL)
    vec3 F = fresnelSchlick(max(dot(N, V), 0.0), F0);
    vec3 kD = (vec3(1.0) - F) * (1.0 - metallic);
    
    vec3 irradiance = texture(irradianceMap, N.xy).rgb; // 简化采样
    vec3 diffuse = irradiance * albedo;
    
    // 预计算环境反射
    vec3 R = reflect(-V, N);
    // 简化:实际应使用粗糙度选择mipmap
    vec3 prefilteredColor = texture(prefilterMap, R.xy).rgb;
    
    // BRDF积分
    vec2 brdf = texture(brdfLUT, vec2(max(dot(N, V), 0.0), roughness)).xy;
    vec3 specular = prefilteredColor * (F * brdf.x + brdf.y);
    
    vec3 ambient = (kD * diffuse + specular) * ao;
    
    // 最终颜色
    vec3 color = ambient + Lo;
    
    // HDR色调映射
    color = color / (color + vec3(1.0));
    color = pow(color, vec3(1.0/2.2)); // Gamma校正
    
    FragColor = vec4(color, 1.0);
}

// 实现辅助函数
vec3 fresnelSchlick(float cosTheta, vec3 F0) {
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}

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 nom = a2;
    float denom = (NdotH2 * (a2 - 1.0) + 1.0);
    denom = PI * denom * denom;
    
    return nom / denom;
}

float geometrySchlickGGX(float NdotV, float roughness) {
    float r = roughness + 1.0;
    float k = (r * r) / 8.0;
    return NdotV / (NdotV * (1.0 - k) + k);
}

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 getNormalFromMap() {
    vec3 tangentNormal = texture(normalMap, TexCoord).xyz * 2.0 - 1.0;
    
    // 构建TBN矩阵(简化版)
    vec3 Q1 = dFdx(FragPos);
    vec3 Q2 = dFdy(FragPos);
    vec2 st1 = dFdx(TexCoord);
    vec2 st2 = dFdy(TexCoord);
    
    vec3 T = normalize(Q1 * st2.t - Q2 * st1.t);
    vec3 N = normalize(Normal);
    vec3 B = normalize(cross(N, T));
    
    mat3 TBN = mat3(T, B, N);
    return normalize(TBN * tangentNormal);
}

5.2 Unity Shader Graph 实现

在Unity中,可以使用Shader Graph可视化工具:

  1. 创建PBR Graph

    • 创建Lit Master Stack
    • 添加Texture2D节点用于Albedo、Normal、Metallic、Roughness
    • 连接到Master Stack的对应输入
  2. 自定义光照节点

    • 使用Custom Function节点编写HLSL代码
    • 或者使用节点组合实现复杂效果
  3. 示例节点连接

    [Albedo Texture] -> [Sample Texture 2D] -> [Base Color]
    [Normal Texture] -> [Sample Texture 2D] -> [Normal]
    [Metallic Texture] -> [Sample Texture 2D] -> [Metallic]
    [Roughness Texture] -> [Sample Texture 2D] -> [Roughness]
    

5.3 性能优化建议

5.3.1 着色器优化

  • 减少纹理采样:合并纹理通道
  • 减少计算复杂度:使用预计算值
  • 避免动态分支:使用lerp/step函数
  • 使用LOD:根据距离选择不同复杂度的着色器

5.3.2 渲染管线优化

  • 实例化渲染:相同材质物体批量渲染
  • GPU Instancing:减少Draw Call
  • 剔除(Culling):移除不可见物体
  • 批处理(Batching):合并相同材质的物体

5.3.3 内存优化

  • 纹理压缩:使用ASTC/ETC2/DXT格式
  • 纹理图集:合并小纹理
  • Mipmap:根据距离使用不同分辨率

第六部分:学习路径与资源推荐

6.1 学习路径建议

阶段1:基础(2-4周)

  • 学习线性代数(矩阵、向量、变换)
  • 理解渲染管线基本概念
  • 掌握GLSL/HLSL基础语法
  • 实现简单的Phong光照

阶段2:中级(4-8周)

  • 深入理解PBR理论
  • 实现完整的PBR着色器
  • 学习法线贴图、视差贴图
  • 掌握阴影技术

阶段3:高级(8-12周)

  • 实现SSAO、SSR等后期效果
  • 学习延迟着色
  • 掌握计算着色器
  • 优化渲染性能

阶段4:专家(持续)

  • 研究光线追踪
  • 实现体积渲染
  • 探索实时GI技术
  • 参与开源项目

6.2 推荐资源

6.2.1 在线教程

6.2.2 书籍

  • 《Real-Time Rendering》:渲染领域的圣经
  • 《Physically Based Rendering》:PBR理论详解
  • 《OpenGL编程指南》:OpenGL权威参考

6.2.3 开源项目

6.2.4 工具

  • RenderDoc:GPU调试工具
  • NVIDIA Nsight:性能分析工具
  • Shader Playground:在线着色器编辑器

6.3 实践项目建议

项目1:简单PBR渲染器

  • 实现基础PBR光照
  • 支持1-2个光源
  • 加载模型和纹理

项目2:材质编辑器

  • 可视化材质参数调整
  • 实时预览效果
  • 导出材质配置

项目3:完整渲染引擎

  • 实现延迟着色管线
  • 包含阴影、SSAO、Bloom
  • 支持多种光源类型

结语

掌握游戏渲染技术是一个持续学习和实践的过程。从基础的Phong模型到复杂的PBR系统,再到现代的实时渲染技术,每一步都需要扎实的理论基础和大量的实践。

记住以下关键点:

  1. 理论与实践结合:理解物理原理,但也要知道如何近似
  2. 性能与质量平衡:实时渲染需要在两者间找到平衡
  3. 持续学习:渲染技术发展迅速,保持学习新技术
  4. 调试与分析:使用专业工具分析和优化渲染效果

通过系统学习和不断实践,你将能够创建出令人惊叹的渲染效果,为游戏带来视觉上的震撼体验。祝你学习顺利!