1. 延迟渲染的原理
延迟渲染主要包含了两个Pass。在第一个Pass中,我们不进行任何光照计算,而是仅仅计算哪些片元是可见的,这主要是通过深度缓冲技术来实现,当发现一个片元是可见的,我们就把它的相关信息存储到G缓冲区中。然后,在第二个Pass中,我们利用G缓冲区的各个片元信息,例如表面法线、视角方向、漫反射系数等,进行真正的光照计算。
延迟渲染的过程大致可以用下面的伪代码来描述:
// 第一个 Pass 不进行真正的光照计算
// 仅仅把光照计算需要的信息存储到 G 缓冲中
for (each primitive in this model) {
for (each fragment covered by this primitive) {
if (failed in depth test) {
// 如果没有通过深度测试,说明该片元是不可见的
discard;
} else {
// 如果该片元可见
// 就把需要的信息存储到 G 缓冲中
writeGBuffer(materialInfo, pos, normal, lightDir, viewDir);
}
}
}
// 利用 G 缓冲中的信息进行真正的光照计算
for (each pixel in the screen) {
if (the pixel is valid) {
// 如果该像素是有效的
// 读取它对应的 G 缓冲中的信息
readGBuffer(pixel, materialInfo, pos, normal, lightDir, viewDir);
// 根据读取到的信息进行光照计算
float4 color = Shading(materialInfo, pos, normal, lightDir, viewDir);
// 更新帧缓冲
writeFrameBuffer(pixel, color);
}
}
Unity 里面的延迟渲染
一、代码结构
Shader "Hidden/Internal-DeferredShading" {
Properties {
// 光照贴图(用于点光源/聚光灯衰减)
_LightTexture0 ("Light Texture", any) = "" {}
// 光照方向贴图(用于聚光灯方向衰减)
_LightTextureB0 ("Light Direction Texture", 2D) = "" {}
// 阴影贴图(来自深度缓冲)
_ShadowMapTexture ("Shadow Map", any) = "" {}
// 混合模式参数(源/目标混合因子)
_SrcBlend ("Source Blend", Float) = 1
_DstBlend ("Destination Blend", Float) = 1
}
SubShader {
Pass {
// Pass 1: 延迟光照计算(叠加或减法混合)
}
Pass {
// Pass 2: LDR模式最终解码(将对数值转为线性)
}
Fallback Off // 无备用着色器
}
二、第一个Pass
Pass {
ZWrite Off // 关闭深度写入
Blend [_SrcBlend] [_DstBlend] // 动态混合模式(HDR时为加法,LDR时为减法)
CGPROGRAM
#pragma target 3.0 // 要求SM3.0以上
#pragma vertex vert_deferred // 使用Unity内置延迟渲染顶点着色器
#pragma fragment frag // 片段着色器
#pragma multi_compile_lightpass // 多编译光照传递模式
#pragma multi_compile ___ UNITY_HDR_ON // 根据是否启用HDR编译不同路径
#pragma exclude_renderers nomrt // 排除不支持多渲染目标的平台
#include "UnityCG.cginc" // Unity基础CG库
#include "UnityDeferredLibrary.cginc" // 延迟渲染专用库
#include "UnityPBSLighting.cginc" // 物理渲染光照计算
#include "UnityStandardUtils.cginc" // 标准着色器工具
#include "UnityGBuffer.cginc" // GBuffer解包
#include "UnityStandardBRDF.cginc" // BRDF计算
// GBuffer纹理采样器(包含材质信息)
sampler2D _CameraGBufferTexture0; // Albedo + 金属度
sampler2D _CameraGBufferTexture1; // 法线 + 粗糙度
sampler2D _CameraGBufferTexture2; // 前照光 + 漫反射
// 计算单个光源的光照贡献
half4 CalculateLight (unity_v2f_deferred i) {
float3 wpos; // 世界空间坐标
float2 uv; // GBuffer纹理坐标
float atten, fadeDist;// 光照衰减和雾化距离
UnityLight light; // 光照结构体
UNITY_INITIALIZE_OUTPUT(UnityLight, light); // 初始化光照结构
UnityDeferredCalculateLightParams (
i, wpos, uv, light.dir, atten, fadeDist
); // 计算光照参数(方向、衰减等)
// 设置光照颜色(光源颜色 * 衰减)
light.color = _LightColor.rgb * atten;
// 从GBuffer解包材质属性
//“Unity Standard Shader 解析(六)之Deferred”里面,可以看到Gbuffer的数据来源
half4 gbuffer0 = tex2D(_CameraGBufferTexture0, uv);
half4 gbuffer1 = tex2D(_CameraGBufferTexture1, uv);
half4 gbuffer2 = tex2D(_CameraGBufferTexture2, uv);
UnityStandardData data = UnityStandardDataFromGbuffer(
gbuffer0, gbuffer1, gbuffer2
); // 转换为标准材质数据结构
// 计算视角方向(世界空间)
float3 eyeVec = normalize(wpos - _WorldSpaceCameraPos);
// 计算非高光部分(1 - 反射率)
half oneMinusReflectivity = 1 - SpecularStrength(data.specularColor.rgb);
// 环境光(此处设为0,因为延迟渲染通常单独处理)
UnityIndirect ind;
UNITY_INITIALIZE_OUTPUT(UnityIndirect, ind);
ind.diffuse = 0;
ind.specular = 0;
// 使用标准BRDF计算光照结果
return UNITY_BRDF_PBS(
data.diffuseColor, // 漫反射颜色
data.specularColor, // 高光颜色
oneMinusReflectivity, // 非高光部分
data.smoothness, // 粗糙度
data.normalWorld, // 法线方向
-eyeVec, // 视角方向(取反)
light, // 光照参数
ind // 环境光(未使用)
);
}
// 主片段着色器入口
#ifdef UNITY_HDR_ON
half4 frag (unity_v2f_deferred i) : SV_Target {
#else
fixed4 frag (unity_v2f_deferred i) : SV_Target {
#endif
half4 c = CalculateLight(i); // 计算光照
#ifdef UNITY_HDR_ON
return c; // HDR模式直接返回线性值
#else
return exp2(-c); // LDR模式对数编码
#endif
}
ENDCG
}
1.顶点着色器
vert_deferred
定义在UnityDeferredLibrary.cginc
里面
// 顶点着色器部分
// 延迟渲染顶点着色器输出结构体
struct unity_v2f_deferred {
float4 pos : SV_POSITION; // 裁剪空间顶点坐标
float4 uv : TEXCOORD0; // 屏幕空间UV坐标(用于采样GBuffer)
float3 ray : TEXCOORD1; // 视线方向向量(用于光照计算)
};
// 控制是否使用全屏四边形模式的Shader变量
float _LightAsQuad;
// 延迟渲染顶点着色器入口函数
unity_v2f_deferred vert_deferred (float4 vertex : POSITION, float3 normal : NORMAL)
{
unity_v2f_deferred o;
// 1. 将顶点从物体空间转换为裁剪空间
o.pos = UnityObjectToClipPos(vertex);
// 2. 计算屏幕空间UV坐标(范围[0,1],保留深度信息)
o.uv = ComputeScreenPos(o.pos);
// 3. 计算视线方向向量(视图空间下的相机到顶点方向)
// UnityObjectToViewPos将顶点转换为视图空间坐标
// 乘以float3(-1,-1,1)调整坐标方向(可能与坐标系转换有关)
o.ray = UnityObjectToViewPos(vertex) * float3(-1, -1, 1);
// 4. 根据_LightAsQuad选择ray的来源:
// - 当_LightAsQuad=1时:使用传入的normal参数(实际是相机到近平面角落的向量)
// - 当_LightAsQuad=0时:使用顶点计算的ray
o.ray = lerp(o.ray, normal, _LightAsQuad);
return o;
}
2.UnityDeferredCalculateLightParams
// --------------------------------------------------------
// 核心光照参数计算函数
void UnityDeferredCalculateLightParams (
unity_v2f_deferred i,
out float3 outWorldPos, // 输出世界空间坐标
out float2 outUV, // 输出屏幕UV坐标
out half3 outLightDir, // 输出光照方向
out float outAtten, // 输出光照衰减
out float outFadeDist) // 输出阴影衰减距离
{
// 1. 计算视线方向(归一化到近裁剪面)
i.ray = i.ray * (_ProjectionParams.z / i.ray.z);
float2 uv = i.uv.xy / i.uv.w;
// 2. 从深度缓冲区重建世界坐标
float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);
depth = Linear01Depth(depth); // 将深度转换为线性0-1范围
float4 vpos = float4(i.ray * depth, 1); // 视图空间坐标
float3 wpos = mul(unity_CameraToWorld, vpos).xyz; // 世界空间坐标
// 3. 计算阴影衰减距离
float fadeDist = UnityComputeShadowFadeDistance(wpos, vpos.z);
// 4. 根据光照类型计算光照参数
#if defined (SPOT) // 聚光灯
float3 tolight = _LightPos.xyz - wpos; // 光源到顶点向量
half3 lightDir = normalize(tolight); // 光照方向
float4 uvCookie = mul(unity_WorldToLight, float4(wpos,1)); // 光源空间坐标
// 聚光灯衰减(带贴图)
float atten = tex2Dbias(_LightTexture0, float4(uvCookie.xy / uvCookie.w, 0, -8)).w;
atten *= uvCookie.w < 0; // 防止背面采样
float att = dot(tolight, tolight) * _LightPos.w; // 距离衰减
atten *= tex2D(_LightTextureB0, att.rr).r; // 衰减贴图采样
// 计算阴影
atten *= UnityDeferredComputeShadow(wpos, fadeDist, uv);
#elif defined (DIRECTIONAL) || defined (DIRECTIONAL_COOKIE) // 方向光
half3 lightDir = -_LightDir.xyz; // 光方向(反向)
float atten = 1.0;
// 计算阴影
atten *= UnityDeferredComputeShadow(wpos, fadeDist, uv);
#if defined (DIRECTIONAL_COOKIE) // 带贴图的方向光
// 采样方向光贴图(如天空盒投影)
atten *= tex2Dbias(_LightTexture0, float4(mul(unity_WorldToLight, half4(wpos,1)).xy, 0, -8)).w;
#endif
#elif defined (POINT) || defined (POINT_COOKIE) // 点光源
float3 tolight = wpos - _LightPos.xyz; // 顶点到光源向量
half3 lightDir = -normalize(tolight); // 光照方向(反向)
float att = dot(tolight, tolight) * _LightPos.w; // 距离衰减
float atten = tex2D(_LightTextureB0, att.rr).r; // 衰减贴图
// 计算阴影
atten *= UnityDeferredComputeShadow(tolight, fadeDist, uv);
#if defined (POINT_COOKIE) // 带贴图的点光源
// 采样立方体阴影贴图
atten *= texCUBEbias(_LightTexture0, float4(mul(unity_WorldToLight, half4(wpos,1)).xyz, -8)).w;
#endif
#else // 无效光照类型
half3 lightDir = 0;
float atten = 0;
#endif
// 输出结果
outWorldPos = wpos;
outUV = uv;
outLightDir = lightDir;
outAtten = atten;
outFadeDist = fadeDist;
}
3,UnityComputeShadowFadeDistance
// 计算阴影衰减距离(用于混合实时阴影和烘焙阴影)
//float3 wpos:顶点的世界空间坐标(通过深度缓冲区重建)。
//float z:顶点的归一化设备坐标(NDC)的Z值(范围 [0,1]),表示顶点到相机的距离。
float UnityComputeShadowFadeDistance(float3 wpos, float z)
{
// 1. 计算顶点到阴影衰减中心点的线性距离
// unity_ShadowFadeCenterAndType.xyz 是阴影衰减球体的中心点坐标(世界空间)
float sphereDist = distance(wpos, unity_ShadowFadeCenterAndType.xyz);
// 2. 根据混合因子 lerp 在 z(深度值)和 sphereDist(球体距离)之间插值
// unity_ShadowFadeCenterAndType.w 是混合权重(0~1):
// - 当为0时,完全使用 z(深度值)作为衰减距离
// - 当为1时,完全使用 sphereDist(球体距离)作为衰减距离
// - 中间值则线性插值
return lerp(z, sphereDist, unity_ShadowFadeCenterAndType.w);
}
4.UnityDeferredComputeShadow
// 计算混合阴影(实时+烘焙)
half UnityDeferredComputeShadow(float3 vec, float fadeDist, float2 uv)
{
half fade = UnityComputeShadowFade(fadeDist); // 计算阴影衰减因子
half shadowMaskAttenuation = UnityDeferredSampleShadowMask(uv); // 烘焙阴影
half realtimeShadowAttenuation = UnityDeferredSampleRealtimeShadow(fade, vec, uv); // 实时阴影
// 混合烘焙和实时阴影
return UnityMixRealtimeAndBakedShadows(realtimeShadowAttenuation, shadowMaskAttenuation, fade);
}
5.UnityComputeShadowFade
half UnityComputeShadowFade(float fadeDist)
{
// 计算衰减因子:将 fadeDist 乘以比例因子(scale),加上偏移因子(offset)
// 然后通过 saturate() 将结果限制在 [0, 1] 范围内
return saturate(fadeDist * _LightShadowData.z + _LightShadowData.w);
}
6.UnityDeferredSampleShadowMask
// 采样阴影掩码(烘焙阴影)
half UnityDeferredSampleShadowMask(float2 uv)
{
half shadowMaskAttenuation = 1.0f;
#if defined (SHADOWS_SHADOWMASK)
// 从GBuffer的第四通道(_CameraGBufferTexture4)读取阴影掩码
half4 shadowMask = tex2D(_CameraGBufferTexture4, uv);
// 使用unity_OcclusionMaskSelector计算最终的阴影衰减
shadowMaskAttenuation = saturate(dot(shadowMask, unity_OcclusionMaskSelector));
#endif
return shadowMaskAttenuation;
}
7.UnityDeferredSampleRealtimeShadow
// 采样实时阴影(动态阴影)
half UnityDeferredSampleRealtimeShadow(half fade, float3 vec, float2 uv)
{
half shadowAttenuation = 1.0f;
// 方向光/带贴图的方向光的阴影计算(屏幕空间阴影)
#if defined (DIRECTIONAL) || defined (DIRECTIONAL_COOKIE)
#if defined(SHADOWS_SCREEN)
// 从_ShadowMapTexture采样阴影值
shadowAttenuation = tex2D(_ShadowMapTexture, uv).r;
#endif
#endif
// 快速分支优化(Unity 5.6+)
#if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING) && defined(SHADOWS_SOFT) && !defined(LIGHTMAP_SHADOW_MIXING)
// 当衰减小于阈值时,跳过计算(提升性能)
UNITY_BRANCH
if (fade < (1.0f - 1e-2f))
{
#endif
// 聚光灯阴影(深度阴影)
#if defined(SPOT)
#if defined(SHADOWS_DEPTH)
// 转换到阴影坐标系并采样
float4 shadowCoord = mul(unity_WorldToShadow[0], float4(vec, 1));
shadowAttenuation = UnitySampleShadowmap(shadowCoord);
#endif
#endif
// 点光源/带贴图的点光源阴影(立方体贴图阴影)
#if defined (POINT) || defined (POINT_COOKIE)
#if defined(SHADOWS_CUBE)
// 直接采样立方体阴影贴图
shadowAttenuation = UnitySampleShadowmap(vec);
#endif
#endif
#if defined(UNITY_FAST_COHERENT_DYNAMIC_BRANCHING) && defined(SHADOWS_SOFT) && !defined(LIGHTMAP_SHADOW_MIXING)
}
#endif
return shadowAttenuation;
}
8.UnitySampleShadowmap
// ------------------------------------------------------------------
// Spot 光源阴影采样逻辑
// ------------------------------------------------------------------
// 仅在启用深度阴影(SHADOWS_DEPTH)且光源类型为 Spot(SPOT)时编译此部分
#if defined(SHADOWS_DEPTH) && defined(SPOT)
// 阴影采样函数(核心逻辑)
inline fixed UnitySampleShadowmap (float4 shadowCoord)
{
#if defined(SHADOWS_SOFT)
half shadow = 1; // 默认全亮(无阴影)
// 情况1:不支持硬件比较采样器(如部分移动设备或 Xbox360)
#if !defined(SHADOWS_NATIVE)
float3 coord = shadowCoord.xyz / shadowCoord.w; // 将齐次坐标转换为归一化坐标 [0,1]
// 使用四个偏移量进行软阴影采样(PCF)
float4 shadowVals;
shadowVals.x = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, coord + _ShadowOffsets[0].xy);
shadowVals.y = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, coord + _ShadowOffsets[1].xy);
shadowVals.z = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, coord + _ShadowOffsets[2].xy);
shadowVals.w = SAMPLE_DEPTH_TEXTURE(_ShadowMapTexture, coord + _ShadowOffsets[3].xy);
// 比较采样深度与当前坐标深度,计算阴影权重(0为全黑,1为全亮)
half4 shadows = (shadowVals < coord.zzzz) ? _LightShadowData.rrrr : 1.0f;
shadow = dot(shadows, 0.25f); // 四个采样的平均值
#else // 情况2:支持硬件比较采样器(如移动设备)
// 使用硬件比较采样器进行高效采样
float3 coord = shadowCoord.xyz / shadowCoord.w;
half4 shadows;
shadows.x = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[0]);
shadows.y = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[1]);
shadows.z = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[2]);
shadows.w = UNITY_SAMPLE_SHADOW(_ShadowMapTexture, coord + _ShadowOffsets[3]);
shadow = dot(shadows, 0.25f); // 四个采样的平均值
#else // 情况3:其他平台(如PC)
// 使用更复杂的3x3采样(PCF3x3)提高阴影质量
float3 coord = shadowCoord.xyz / shadowCoord.w;
float3 receiverPlaneDepthBias = UnityGetReceiverPlaneDepthBias(coord, 1.0f);
shadow = UnitySampleShadowmap_PCF3x3(float4(coord, 1), receiverPlaneDepthBias);
#endif
// 根据光照阴影强度混合阴影效果
shadow = lerp(_LightShadowData.r, 1.0f, shadow);
#else // 情况4:硬阴影(非软阴影)
// 硬阴影直接采样(1-tap)
#if defined(SHADOWS_NATIVE)
// 使用硬件比较采样器
half shadow = UNITY_SAMPLE_SHADOW_PROJ(_ShadowMapTexture, shadowCoord);
shadow = lerp(_LightShadowData.r, 1.0f, shadow);
#else
// 手动比较深度
half shadow = SAMPLE_DEPTH_TEXTURE_PROJ(_ShadowMapTexture, UNITY_PROJ_COORD(shadowCoord))
< (shadowCoord.z / shadowCoord.w) ? _LightShadowData.r : 1.0;
#endif
#endif
return shadow;
}
#endif // #if defined(SHADOWS_DEPTH) && defined(SPOT)
三、第二个Pass
Pass {
ZTest Always // 总是通过深度测试
Cull Off // 不剔除面
ZWrite Off // 关闭深度写入
Stencil {
// 只渲染非背景区域(通过Stencil缓冲区)
ref [_StencilNonBackground] // 参考值来自Unity的Stencil设置
readmask [_StencilNonBackground] // 读取掩码
compback equal // 后面面的比较函数
compfront equal // 前面面的比较函数(修复bug,见注释)
}
CGPROGRAM
#pragma target 3.0
#pragma vertex vert
#pragma fragment frag
#pragma exclude_renderers nomrt
#include "UnityCG.cginc"
sampler2D _LightBuffer; // 光照缓冲区纹理
struct v2f {
float4 vertex : SV_POSITION;
float2 texcoord : TEXCOORD0;
};
// 顶点着色器(传递屏幕坐标)
v2f vert (float4 vertex : POSITION, float2 texcoord : TEXCOORD0) {
v2f o;
o.vertex = UnityObjectToClipPos(vertex);
o.texcoord = texcoord.xy;
#ifdef UNITY_SINGLE_PASS_STEREO
o.texcoord = TransformStereoScreenSpaceTex(o.texcoord, 1.0f);
#endif
return o;
}
// 片段着色器(解码对数值)
fixed4 frag (v2f i) : SV_Target {
return -log2(tex2D(_LightBuffer, i.texcoord)); // 对数解码
}
ENDCG
}
总结Unity中关于内部延迟渲染(Deferred Shading)的实现,主要包括了原理介绍、代码结构和关键函数的作用。
延迟渲染的原理
延迟渲染通过两个Pass完成:第一个Pass只计算哪些片元是可见的,并将这些片元的相关信息存储到G缓冲区中;第二个Pass利用G缓冲区的信息进行光照计算。这种方法允许在不增加额外复杂度的情况下处理大量光源。
Unity里面的延迟渲染
一、代码结构
- Shader “Hidden/Internal-DeferredShading”:定义了一个隐藏的着色器,用于内部延迟渲染。
- Properties块:定义了各种纹理属性,如光纹理、阴影贴图等。
- SubShader块:包含了具体的渲染过程,分为多个Pass来执行不同的渲染任务。
二、第一个Pass
- ZWrite Off 和 Blend:关闭深度写入并设置混合模式。
- CGPROGRAM块:包含了顶点和片段着色器的实现,使用多编译指令以适应不同平台和配置。
- CalculateLight函数:根据从GBuffer读取的信息计算单个光源对像素颜色的贡献。
- frag函数:主片段着色器入口,根据是否启用高动态范围(HDR)选择不同的返回值处理方式。
关键函数
- vert_deferred:延迟渲染使用的顶点着色器,负责转换坐标系并将必要信息传递给片段着色器。
- UnityDeferredCalculateLightParams:核心光照参数计算函数,根据当前片元的位置和其他信息计算出光照方向、衰减等因素。
- UnityComputeShadowFadeDistance:计算阴影衰减距离,用于混合实时阴影和烘焙阴影。
- UnityDeferredComputeShadow:计算混合阴影效果,结合实时阴影和烘焙阴影。
- UnityComputeShadowFade:计算阴影衰减因子。
- UnityDeferredSampleShadowMask:采样阴影掩码,即烘焙阴影。
- UnityDeferredSampleRealtimeShadow:采样实时阴影,即动态生成的阴影。
- UnitySampleShadowmap:阴影贴图采样逻辑,支持多种情况下的阴影采样,包括软阴影和硬阴影。
第二个Pass
- ZTest Always, Cull Off, ZWrite Off:总是通过深度测试,不剔除面,关闭深度写入。
- Stencil块:仅渲染非背景区域,通过模板缓冲区实现。
- CGPROGRAM块:解码对数值,主要是通过对数运算将HDR颜色值转换为线性空间颜色值。