在UE5的延迟渲染管线中DecodeGBufferData这个函数的存在核心原因可以概括为GBuffer里存储的都是精心压缩、编码过的数据不解码就无法直接用于光照计算。你可以把GBuffer想象成一份“速记稿”——为了节省显存和带宽几何和材质信息被高度压缩打包进几张纹理。渲染器在光照阶段必须先把这些速记稿还原成完整的句子这个过程就是解码。FGBufferData DecodeGBufferData( float4 InGBufferA, float4 InGBufferB, float4 InGBufferC, float4 InGBufferD, float4 InGBufferE, float4 InGBufferF, float4 InGBufferVelocity, float CustomNativeDepth, uint CustomStencil, float SceneDepth, bool bGetNormalizedNormal, bool bChecker) { FGBufferData GBuffer; GBuffer.WorldNormal DecodeNormal( InGBufferA.xyz ); if(bGetNormalizedNormal) { GBuffer.WorldNormal normalize(GBuffer.WorldNormal); } GBuffer.PerObjectGBufferData InGBufferA.a; GBuffer.Metallic InGBufferB.r; GBuffer.Specular InGBufferB.g; GBuffer.Roughness InGBufferB.b; // Note: must match GetShadingModelId standalone function logic // Also Note: SimpleElementPixelShader directly sets SV_Target2 ( GBufferB ) to indicate unlit. // An update there will be required if this layout changes. GBuffer.ShadingModelID DecodeShadingModelId(InGBufferB.a); GBuffer.SelectiveOutputMask DecodeSelectiveOutputMask(InGBufferB.a); GBuffer.BaseColor DecodeBaseColor(InGBufferC.rgb); #if GBUFFER_HAS_DIFFUSE_SAMPLE_OCCLUSION GBuffer.DiffuseIndirectSampleOcclusion 255 * InGBufferC.a; GBuffer.GBufferAO saturate(1.0 - float(countbits(GBuffer.DiffuseIndirectSampleOcclusion)) * rcp(float(INDIRECT_SAMPLE_COUNT))); GBuffer.IndirectIrradiance 1; #elif ALLOW_STATIC_LIGHTING GBuffer.GBufferAO 1; GBuffer.DiffuseIndirectSampleOcclusion 0x0; GBuffer.IndirectIrradiance DecodeIndirectIrradiance(InGBufferC.a); #else GBuffer.GBufferAO InGBufferC.a; GBuffer.DiffuseIndirectSampleOcclusion 0x0; GBuffer.IndirectIrradiance 1; #endif GBuffer.CustomData HasCustomGBufferData(GBuffer.ShadingModelID) ? InGBufferD : 0; GBuffer.CustomMask (GBuffer.ShadingModelID SHADINGMODELID_DEFAULT_LIT) ? InGBufferD.a : 0; // FirstPerson uses a bit in SelectiveOutputMask that is aliased with ZERO_PRECSHADOW_MASK when !ALLOW_STATIC_LIGHTING, so we explicitly skip this logic here. #if ALLOW_STATIC_LIGHTING GBuffer.PrecomputedShadowFactors HasPrecShadowMask(GBuffer) ? InGBufferE : (HasZeroPrecShadowMask(GBuffer) ? 0 : 1); #else GBuffer.PrecomputedShadowFactors half(1.0f); #endif GBuffer.CustomDepth ConvertFromDeviceZ(CustomNativeDepth); GBuffer.CustomStencil CustomStencil; GBuffer.Depth SceneDepth; GBuffer.StoredBaseColor GBuffer.BaseColor; GBuffer.StoredMetallic GBuffer.Metallic; GBuffer.StoredSpecular GBuffer.Specular;具体来说有以下几个决定性因素1. 极致的带宽与显存压缩延迟渲染的本质是把复杂的材质计算先跑一遍把结果法线、颜色、粗糙度等写入多张GBuffer纹理光照阶段再读取。如果有大量像素GBuffer的读写带宽就是性能瓶颈。因此UE5会对数据做非破坏性或近似无损的压缩编码例如法线通常使用八面体映射编码。原本需要float312字节的世界空间法线被映射到一个二维向量打包进RGBA8或R10G10B10A2纹理的两个通道。解码时需要调用类似DecodeNormal的函数反向展开回单位法线。基础颜色可能舍弃不必要的高精度使用sRGB/BC压缩或存储时去掉与金属度重复的信息解码时再还原。粗糙度、金属度、AO往往共享一个8位通道如GBufferB.A通过简单的乘法和范围映射解码。自定义深度/模板CustomNativeDepth和CustomStencil并不是直接存于GBuffer而是从深度缓冲和模板缓冲中取出解码函数会利用它们重构像素的世界位置或判断像素是否属于特定标记物体。如果不解码你拿到的是一个已经被投影、量化、打包成奇怪数值的“压缩包”直接当做法线或颜色会得到完全错误的结果。UE5通常使用八面体映射Octahedral Mapping将3D单位法线压缩成2D坐标再量化存入8位通道如RGBA8纹理的两个通道。这样每个像素的法线只占16位而直接存RGB法线要24甚至48位。编码过程假设我们有一个世界空间法线已经归一化n(0.267, 0.534, 0.802)n(0.267, 0.534, 0.802)容易验证 0.26720.53420.8022≈10.26720.53420.8022≈11. 投影到八面体先计算L1范数曼哈顿距离d∣0.267∣∣0.534∣∣0.802∣1.603d∣0.267∣∣0.534∣∣0.802∣1.603将法线分量除以 dd得到投影n′(0.1666, 0.3333, 0.5006)n′(0.1666, 0.3333, 0.5006)此时三个分量的绝对值之和等于1。2. 折叠到二维八面体展开因为 nz′≥0nz′≥0法线在上半球直接取前两个分量作为编码结果o(0.1666, 0.3333)o(0.1666, 0.3333)若 nz′0nz′0则需要绕中心折叠公式不同3. 映射到 [0,1][0,1] 存储空间将 [−1,1][−1,1] 范围的八面体坐标映射到 [0,1][0,1]cx0.1666×0.50.50.5833cx0.1666×0.50.50.5833cy0.3333×0.50.50.66665cy0.3333×0.50.50.666654. 量化为8位整数存入GBuffer假设GBuffer使用RGBA8格式A和B通道分别存入 cx,cycx,cyVxround(0.5833×255)149Vxround(0.5833×255)149Vyround(0.66665×255)170Vyround(0.66665×255)170所以GBuffer中这两个通道的最终存储值为149和170。总结basecolor压缩先平方根让数值变大再乘以255那么丢掉的小数更靠后精度损失越小因为人眼对暗部变化更敏感直接用线性值存储暗部如0.1只分配到25个灰度级0.1×255≈26很容易出现色带。而平方根编码会把暗部数值“拉高”分配更多码位亮部则被压缩完美匹配视觉特性。具体数值例子假设GBuffer使用RGBA8每通道0-255整数格式基础颜色需要存储三个通道。例子1中等暗度的灰色原始线性颜色C(0.30, 0.30, 0.30)C(0.30, 0.30, 0.30)① 编码平方根EC(0.30,0.30,0.30)≈(0.5477, 0.5477, 0.5477)EC(0.30,0.30,0.30)≈(0.5477, 0.5477, 0.5477)② 量化到0-255整数Vround(0.5477×255)round(139.66)140Vround(0.5477×255)round(139.66)140GBuffer中存储的三个通道值就是140, 140, 140。解码过程在DecodeGBufferData中① 归一化回[0,1]E′140/255≈0.5490E′140/255≈0.5490② 逆运算平方C′(0.5490)2≈(0.3014, 0.3014, 0.3014)C′(0.5490)2≈(0.3014, 0.3014, 0.3014)误差原始0.3000→ 解码0.3014误差0.0014相对误差仅0.47%。如果不编码线性存储0.010 × 255 2.55 → 存成整数 30.015 × 255 3.825 → 存成整数 4这两个暗色被强塞进了相邻的两个整数解码后只能还原成3/255≈0.0118和4/255≈0.0157中间的细腻变化全部丢失而且台阶感极重。用平方根编码√0.010 0.10.1 × 255 25.5 → 存成整数 26√0.015 ≈ 0.12250.1225 × 255 31.2 → 存成整数 31看这里原本挤在两个整数里的值现在跨了 5 个整数26→31。解码时平方回去(26/255)² ≈ 0.0102(31/255)² ≈ 0.0148不仅还原了原始数值中间还多出了 3 个可用整数27,28,29,30能表达0.010到0.015之间的其他暗部层次。乘以255就是为了把一个0到1的小数塞进一个“8位整数格子”里。1. 存储格式整数 vs 浮点GPU的纹理有不同格式。UE5的GBuffer在很多模式下用的是RGBA8 UNORM格式。UNORM的意思是纹理里存的真是整数0-255但采样器帮你自动除以255变回0-1的小数给着色器。所以如果你要把一个值写进这张纹理你必须先乘以255转成整数再存进去。如果你不乘255直接把0.5477这种小数写进去硬件会把它截断为0或1因为整数纹理只接受整数颜色就完全错了。因此“乘以255”是把着色器里的浮点数翻译成纹理能懂的整数语言的必要步骤。2. 为什么不能跳过这步直接存浮点可以直接存浮点但那就得用浮点纹理格式如R16G16B16A16_FLOAT。代价是带宽翻倍RGBA8是32位/像素RGBA16F是64位/像素。GBuffer有好几张每一张都翻倍整个延迟管线的读写带宽压力就会暴增帧率会大幅下降。显存占用翻倍同样分辨率显存多占一倍。在游戏主机和多数PC上带宽和显存是极其宝贵的资源。所以引擎选择用8位整数来存大部分数据然后用编码技巧平方根、法线八面体、位打包弥补整数精度不足的问题。
UE5 之 GBuffer - DecodeGBuffer
发布时间:2026/6/28 10:15:25
在UE5的延迟渲染管线中DecodeGBufferData这个函数的存在核心原因可以概括为GBuffer里存储的都是精心压缩、编码过的数据不解码就无法直接用于光照计算。你可以把GBuffer想象成一份“速记稿”——为了节省显存和带宽几何和材质信息被高度压缩打包进几张纹理。渲染器在光照阶段必须先把这些速记稿还原成完整的句子这个过程就是解码。FGBufferData DecodeGBufferData( float4 InGBufferA, float4 InGBufferB, float4 InGBufferC, float4 InGBufferD, float4 InGBufferE, float4 InGBufferF, float4 InGBufferVelocity, float CustomNativeDepth, uint CustomStencil, float SceneDepth, bool bGetNormalizedNormal, bool bChecker) { FGBufferData GBuffer; GBuffer.WorldNormal DecodeNormal( InGBufferA.xyz ); if(bGetNormalizedNormal) { GBuffer.WorldNormal normalize(GBuffer.WorldNormal); } GBuffer.PerObjectGBufferData InGBufferA.a; GBuffer.Metallic InGBufferB.r; GBuffer.Specular InGBufferB.g; GBuffer.Roughness InGBufferB.b; // Note: must match GetShadingModelId standalone function logic // Also Note: SimpleElementPixelShader directly sets SV_Target2 ( GBufferB ) to indicate unlit. // An update there will be required if this layout changes. GBuffer.ShadingModelID DecodeShadingModelId(InGBufferB.a); GBuffer.SelectiveOutputMask DecodeSelectiveOutputMask(InGBufferB.a); GBuffer.BaseColor DecodeBaseColor(InGBufferC.rgb); #if GBUFFER_HAS_DIFFUSE_SAMPLE_OCCLUSION GBuffer.DiffuseIndirectSampleOcclusion 255 * InGBufferC.a; GBuffer.GBufferAO saturate(1.0 - float(countbits(GBuffer.DiffuseIndirectSampleOcclusion)) * rcp(float(INDIRECT_SAMPLE_COUNT))); GBuffer.IndirectIrradiance 1; #elif ALLOW_STATIC_LIGHTING GBuffer.GBufferAO 1; GBuffer.DiffuseIndirectSampleOcclusion 0x0; GBuffer.IndirectIrradiance DecodeIndirectIrradiance(InGBufferC.a); #else GBuffer.GBufferAO InGBufferC.a; GBuffer.DiffuseIndirectSampleOcclusion 0x0; GBuffer.IndirectIrradiance 1; #endif GBuffer.CustomData HasCustomGBufferData(GBuffer.ShadingModelID) ? InGBufferD : 0; GBuffer.CustomMask (GBuffer.ShadingModelID SHADINGMODELID_DEFAULT_LIT) ? InGBufferD.a : 0; // FirstPerson uses a bit in SelectiveOutputMask that is aliased with ZERO_PRECSHADOW_MASK when !ALLOW_STATIC_LIGHTING, so we explicitly skip this logic here. #if ALLOW_STATIC_LIGHTING GBuffer.PrecomputedShadowFactors HasPrecShadowMask(GBuffer) ? InGBufferE : (HasZeroPrecShadowMask(GBuffer) ? 0 : 1); #else GBuffer.PrecomputedShadowFactors half(1.0f); #endif GBuffer.CustomDepth ConvertFromDeviceZ(CustomNativeDepth); GBuffer.CustomStencil CustomStencil; GBuffer.Depth SceneDepth; GBuffer.StoredBaseColor GBuffer.BaseColor; GBuffer.StoredMetallic GBuffer.Metallic; GBuffer.StoredSpecular GBuffer.Specular;具体来说有以下几个决定性因素1. 极致的带宽与显存压缩延迟渲染的本质是把复杂的材质计算先跑一遍把结果法线、颜色、粗糙度等写入多张GBuffer纹理光照阶段再读取。如果有大量像素GBuffer的读写带宽就是性能瓶颈。因此UE5会对数据做非破坏性或近似无损的压缩编码例如法线通常使用八面体映射编码。原本需要float312字节的世界空间法线被映射到一个二维向量打包进RGBA8或R10G10B10A2纹理的两个通道。解码时需要调用类似DecodeNormal的函数反向展开回单位法线。基础颜色可能舍弃不必要的高精度使用sRGB/BC压缩或存储时去掉与金属度重复的信息解码时再还原。粗糙度、金属度、AO往往共享一个8位通道如GBufferB.A通过简单的乘法和范围映射解码。自定义深度/模板CustomNativeDepth和CustomStencil并不是直接存于GBuffer而是从深度缓冲和模板缓冲中取出解码函数会利用它们重构像素的世界位置或判断像素是否属于特定标记物体。如果不解码你拿到的是一个已经被投影、量化、打包成奇怪数值的“压缩包”直接当做法线或颜色会得到完全错误的结果。UE5通常使用八面体映射Octahedral Mapping将3D单位法线压缩成2D坐标再量化存入8位通道如RGBA8纹理的两个通道。这样每个像素的法线只占16位而直接存RGB法线要24甚至48位。编码过程假设我们有一个世界空间法线已经归一化n(0.267, 0.534, 0.802)n(0.267, 0.534, 0.802)容易验证 0.26720.53420.8022≈10.26720.53420.8022≈11. 投影到八面体先计算L1范数曼哈顿距离d∣0.267∣∣0.534∣∣0.802∣1.603d∣0.267∣∣0.534∣∣0.802∣1.603将法线分量除以 dd得到投影n′(0.1666, 0.3333, 0.5006)n′(0.1666, 0.3333, 0.5006)此时三个分量的绝对值之和等于1。2. 折叠到二维八面体展开因为 nz′≥0nz′≥0法线在上半球直接取前两个分量作为编码结果o(0.1666, 0.3333)o(0.1666, 0.3333)若 nz′0nz′0则需要绕中心折叠公式不同3. 映射到 [0,1][0,1] 存储空间将 [−1,1][−1,1] 范围的八面体坐标映射到 [0,1][0,1]cx0.1666×0.50.50.5833cx0.1666×0.50.50.5833cy0.3333×0.50.50.66665cy0.3333×0.50.50.666654. 量化为8位整数存入GBuffer假设GBuffer使用RGBA8格式A和B通道分别存入 cx,cycx,cyVxround(0.5833×255)149Vxround(0.5833×255)149Vyround(0.66665×255)170Vyround(0.66665×255)170所以GBuffer中这两个通道的最终存储值为149和170。总结basecolor压缩先平方根让数值变大再乘以255那么丢掉的小数更靠后精度损失越小因为人眼对暗部变化更敏感直接用线性值存储暗部如0.1只分配到25个灰度级0.1×255≈26很容易出现色带。而平方根编码会把暗部数值“拉高”分配更多码位亮部则被压缩完美匹配视觉特性。具体数值例子假设GBuffer使用RGBA8每通道0-255整数格式基础颜色需要存储三个通道。例子1中等暗度的灰色原始线性颜色C(0.30, 0.30, 0.30)C(0.30, 0.30, 0.30)① 编码平方根EC(0.30,0.30,0.30)≈(0.5477, 0.5477, 0.5477)EC(0.30,0.30,0.30)≈(0.5477, 0.5477, 0.5477)② 量化到0-255整数Vround(0.5477×255)round(139.66)140Vround(0.5477×255)round(139.66)140GBuffer中存储的三个通道值就是140, 140, 140。解码过程在DecodeGBufferData中① 归一化回[0,1]E′140/255≈0.5490E′140/255≈0.5490② 逆运算平方C′(0.5490)2≈(0.3014, 0.3014, 0.3014)C′(0.5490)2≈(0.3014, 0.3014, 0.3014)误差原始0.3000→ 解码0.3014误差0.0014相对误差仅0.47%。如果不编码线性存储0.010 × 255 2.55 → 存成整数 30.015 × 255 3.825 → 存成整数 4这两个暗色被强塞进了相邻的两个整数解码后只能还原成3/255≈0.0118和4/255≈0.0157中间的细腻变化全部丢失而且台阶感极重。用平方根编码√0.010 0.10.1 × 255 25.5 → 存成整数 26√0.015 ≈ 0.12250.1225 × 255 31.2 → 存成整数 31看这里原本挤在两个整数里的值现在跨了 5 个整数26→31。解码时平方回去(26/255)² ≈ 0.0102(31/255)² ≈ 0.0148不仅还原了原始数值中间还多出了 3 个可用整数27,28,29,30能表达0.010到0.015之间的其他暗部层次。乘以255就是为了把一个0到1的小数塞进一个“8位整数格子”里。1. 存储格式整数 vs 浮点GPU的纹理有不同格式。UE5的GBuffer在很多模式下用的是RGBA8 UNORM格式。UNORM的意思是纹理里存的真是整数0-255但采样器帮你自动除以255变回0-1的小数给着色器。所以如果你要把一个值写进这张纹理你必须先乘以255转成整数再存进去。如果你不乘255直接把0.5477这种小数写进去硬件会把它截断为0或1因为整数纹理只接受整数颜色就完全错了。因此“乘以255”是把着色器里的浮点数翻译成纹理能懂的整数语言的必要步骤。2. 为什么不能跳过这步直接存浮点可以直接存浮点但那就得用浮点纹理格式如R16G16B16A16_FLOAT。代价是带宽翻倍RGBA8是32位/像素RGBA16F是64位/像素。GBuffer有好几张每一张都翻倍整个延迟管线的读写带宽压力就会暴增帧率会大幅下降。显存占用翻倍同样分辨率显存多占一倍。在游戏主机和多数PC上带宽和显存是极其宝贵的资源。所以引擎选择用8位整数来存大部分数据然后用编码技巧平方根、法线八面体、位打包弥补整数精度不足的问题。