引言:理解游戏渲染的本质
游戏渲染是将3D场景转换为2D屏幕图像的过程,这个过程充满了数学、物理和艺术的结合。对于初学者来说,渲染管线(Rendering Pipeline)是一个抽象但至关重要的概念。简单来说,渲染管线就像一条工厂流水线,顶点数据(Vertex Data)作为原材料进入,经过一系列处理步骤,最终输出为屏幕上的像素颜色。
现代游戏渲染技术主要依赖于光栅化(Rasterization)和光线追踪(Ray Tracing)两种核心方法。光栅化速度快,适合实时渲染,而光线追踪能产生极其逼真的效果,但计算成本高昂。目前主流游戏引擎如Unreal Engine和Unity都以光栅化为基础,通过各种技巧模拟光线追踪的效果。
要掌握光影材质与着色器,我们需要理解三个核心支柱:
- 光照模型(Lighting Models):计算物体表面如何反射光线
- 材质系统(Material Systems):定义物体表面的视觉属性
- 着色器编程(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可视化工具:
创建PBR Graph:
- 创建Lit Master Stack
- 添加Texture2D节点用于Albedo、Normal、Metallic、Roughness
- 连接到Master Stack的对应输入
自定义光照节点:
- 使用Custom Function节点编写HLSL代码
- 或者使用节点组合实现复杂效果
示例节点连接:
[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 在线教程
- LearnOpenGL:https://learnopengl.com/(经典入门教程)
- Catlike Coding:https://catlikecoding.com/(深入的Unity渲染教程)
- Real-Time Rendering:https://www.realtimerendering.com/(资源汇总)
6.2.2 书籍
- 《Real-Time Rendering》:渲染领域的圣经
- 《Physically Based Rendering》:PBR理论详解
- 《OpenGL编程指南》:OpenGL权威参考
6.2.3 开源项目
- Filament:Google的PBR渲染引擎
- Google’s Filament:https://github.com/google/filament
- Unity URP/HDRP:学习官方渲染管线实现
6.2.4 工具
- RenderDoc:GPU调试工具
- NVIDIA Nsight:性能分析工具
- Shader Playground:在线着色器编辑器
6.3 实践项目建议
项目1:简单PBR渲染器
- 实现基础PBR光照
- 支持1-2个光源
- 加载模型和纹理
项目2:材质编辑器
- 可视化材质参数调整
- 实时预览效果
- 导出材质配置
项目3:完整渲染引擎
- 实现延迟着色管线
- 包含阴影、SSAO、Bloom
- 支持多种光源类型
结语
掌握游戏渲染技术是一个持续学习和实践的过程。从基础的Phong模型到复杂的PBR系统,再到现代的实时渲染技术,每一步都需要扎实的理论基础和大量的实践。
记住以下关键点:
- 理论与实践结合:理解物理原理,但也要知道如何近似
- 性能与质量平衡:实时渲染需要在两者间找到平衡
- 持续学习:渲染技术发展迅速,保持学习新技术
- 调试与分析:使用专业工具分析和优化渲染效果
通过系统学习和不断实践,你将能够创建出令人惊叹的渲染效果,为游戏带来视觉上的震撼体验。祝你学习顺利!# 游戏渲染学习从零开始掌握光影材质与着色器核心技巧
引言:理解游戏渲染的本质
游戏渲染是将3D场景转换为2D屏幕图像的过程,这个过程充满了数学、物理和艺术的结合。对于初学者来说,渲染管线(Rendering Pipeline)是一个抽象但至关重要的概念。简单来说,渲染管线就像一条工厂流水线,顶点数据(Vertex Data)作为原材料进入,经过一系列处理步骤,最终输出为屏幕上的像素颜色。
现代游戏渲染技术主要依赖于光栅化(Rasterization)和光线追踪(Ray Tracing)两种核心方法。光栅化速度快,适合实时渲染,而光线追踪能产生极其逼真的效果,但计算成本高昂。目前主流游戏引擎如Unreal Engine和Unity都以光栅化为基础,通过各种技巧模拟光线追踪的效果。
要掌握光影材质与着色器,我们需要理解三个核心支柱:
- 光照模型(Lighting Models):计算物体表面如何反射光线
- 材质系统(Material Systems):定义物体表面的视觉属性
- 着色器编程(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可视化工具:
创建PBR Graph:
- 创建Lit Master Stack
- 添加Texture2D节点用于Albedo、Normal、Metallic、Roughness
- 连接到Master Stack的对应输入
自定义光照节点:
- 使用Custom Function节点编写HLSL代码
- 或者使用节点组合实现复杂效果
示例节点连接:
[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 在线教程
- LearnOpenGL:https://learnopengl.com/(经典入门教程)
- Catlike Coding:https://catlikecoding.com/(深入的Unity渲染教程)
- Real-Time Rendering:https://www.realtimerendering.com/(资源汇总)
6.2.2 书籍
- 《Real-Time Rendering》:渲染领域的圣经
- 《Physically Based Rendering》:PBR理论详解
- 《OpenGL编程指南》:OpenGL权威参考
6.2.3 开源项目
- Filament:Google的PBR渲染引擎
- Google’s Filament:https://github.com/google/filament
- Unity URP/HDRP:学习官方渲染管线实现
6.2.4 工具
- RenderDoc:GPU调试工具
- NVIDIA Nsight:性能分析工具
- Shader Playground:在线着色器编辑器
6.3 实践项目建议
项目1:简单PBR渲染器
- 实现基础PBR光照
- 支持1-2个光源
- 加载模型和纹理
项目2:材质编辑器
- 可视化材质参数调整
- 实时预览效果
- 导出材质配置
项目3:完整渲染引擎
- 实现延迟着色管线
- 包含阴影、SSAO、Bloom
- 支持多种光源类型
结语
掌握游戏渲染技术是一个持续学习和实践的过程。从基础的Phong模型到复杂的PBR系统,再到现代的实时渲染技术,每一步都需要扎实的理论基础和大量的实践。
记住以下关键点:
- 理论与实践结合:理解物理原理,但也要知道如何近似
- 性能与质量平衡:实时渲染需要在两者间找到平衡
- 持续学习:渲染技术发展迅速,保持学习新技术
- 调试与分析:使用专业工具分析和优化渲染效果
通过系统学习和不断实践,你将能够创建出令人惊叹的渲染效果,为游戏带来视觉上的震撼体验。祝你学习顺利!
