
1. 项目概述为什么IBL是高质量画面的基石如果你在Unity里调过材质肯定遇到过这种尴尬一个金属球在纯色天空盒下怎么看都像塑料怎么调高光都没用。或者一个室内场景明明打了天光但角落里的物体还是死黑一片缺乏真实世界那种光线“弹来弹去”的柔和感。问题的核心往往在于缺少了高质量的环境光照也就是我们常说的全局光照GI中的间接光部分。IBL全称基于图像的照明就是解决这个问题的“银弹”。它不是什么新潮概念而是现代游戏和影视级实时渲染的标配技术。简单说IBL的核心思想是“借光”——用一张预先计算好的、记录着真实世界或虚构环境全方位光照信息的HDRI图像来照亮你的场景模型。这张图不是拿来当背景贴图用的而是被当作一个包裹整个场景的、无限大的发光球体模型表面每一点的颜色和明暗都由这张图上对应方向射来的光线决定。为什么非得是HDRI普通JPG不行吗关键在于动态范围。现实世界的光照亮度差异巨大太阳直射的亮度可能是阴影处的几万倍。普通的8位低动态范围图像LDR记录不了这种跨度用它来计算光照高光部分会一片死白丢失所有细节。而HDRI通常使用32位浮点数存储每个像素能完美保留从最暗到最亮的光照信息这才是物理正确光照计算的基础。这次我们不谈空洞的理论直接上手。我会带你走完从获取一张HDRI环境贴图开始到在Unity中编写Shader最终让模型被这张图真实照亮的完整流程。你会得到一套可以直接用在项目里的、包含完整注释的Shader代码。无论你是想提升个人作品的画面质感还是面试时被问到PBR和IBL的实现细节这篇文章都能给你扎实的底气。2. IBL技术核心与Unity管线适配解析2.1 IBL的数学原理不止是“贴图采样”很多人以为IBL就是对着一个球面贴图采样这理解太浅了。IBL的数学核心是渲染方程的简化与预计算。完整的渲染方程计算量巨大无法实时求解。IBL的聪明之处在于它把方程中与场景物体无关、只与环境相关的部分——即来自所有方向的环境光辐射率——提前“烘焙”到了一张或多张贴图里。在基于物理的渲染中IBL主要贡献两部分光照漫反射辐照度和镜面反射高光。它们对应渲染方程中的不同部分需要分开处理。漫反射辐照度这模拟了光线在粗糙表面发生的多次散射。由于漫反射是低频信息变化平缓我们可以对它进行大幅度的预计算简化。通常的做法是对原始的HDRI环境贴图进行卷积滤波生成一张全新的、更模糊的辐照度图。卷积的过程可以理解为对于球面上的每一个输出像素即一个法线方向去平均原始环境贴图中该法线所在半球所有入射方向的光照贡献。在Shader中我们直接用法线向量去采样这张辐照度图就能得到该点的间接漫反射颜色。镜面反射高光这部分是高频信息与观察方向、粗糙度强相关不能简单用一张图搞定。主流方案是分割求和近似法。它需要一张特殊的预滤波环境贴图和一个BRDF积分查找表。预滤波环境贴图这是一张Mipmap链但不是用来做抗锯齿的。它的每一层Mip对应一个特定的材质粗糙度。越粗糙的表面其镜面反射 lobe 越宽越模糊因此我们使用更高层更模糊的Mip来采样。在Unity中我们可以利用TextureCube.SampleLevel函数根据粗糙度参数动态选择Mip层级。BRDF积分查找表这是一个2D纹理横坐标是N·V法线与视线夹角的余弦值纵坐标是粗糙度。它预计算了菲涅尔项与BRDF其他部分积分的值。在Shader中我们根据N·V和粗糙度采样这张LUT再与预滤波环境贴图的采样结果相乘就能得到精确的镜面反射贡献。2.2 Unity渲染管线下的实现选择Unity提供了URP和HDRP两种现代渲染管线它们都内置了IBL支持。但“手把手实现”的意义在于理解底层机制所以我们会从Built-in管线或URP的自定义Shader入手自己编写完整的IBL光照模型。Built-in 管线自由度最高我们需要自己处理所有纹理的采样、计算和混合。这包括获取Unity内置的天空盒CubeMap作为环境贴图或者使用我们自己的HDRI转换而来的CubeMap。本文将以此为基础进行讲解。URP 管线URP的Lit Shader已经集成了IBL。但如果你想深度定制或优化可以通过SampleSH函数获取漫反射球谐光照一种比辐照度图更轻量的近似并通过GlossyEnvironmentReflection函数获取镜面反射。理解了我们自实现的原理后你就能更好地驾驭这些内置函数。HDRP 管线拥有最完善的物理光照系统环境光照的配置非常直观。但HDRP的Shader框架更复杂自定义门槛较高。注意无论哪种管线核心思路是相通的。本文提供的Shader代码框架主要面向Built-in管线的Surface Shader但其算法和思想可以无缝迁移到URP的Unlit Shader或HLSL代码块中。关键在于理解如何组织纹理、编写光照函数。2.3 工具链准备从HDRI到引擎可用资源在写代码之前我们需要准备好“弹药”。IBL的实现强烈依赖于预处理好的纹理资源。HDRI源文件获取推荐去Poly Haven或HDRI Haven这类网站下载免费的、高质量的HDRI。选择时注意光照类型阴天、晴空、室内工作室、动态范围和分辨率。一张4K或8K的HDRI足以满足大多数项目需求。HDRI转CubeMapUnity不能直接使用.hdr或.exr格式的HDRI文件作为环境光照源。我们需要将其转换为六张面的CubeMap或者一个.cubemap资源。有两个常用方法使用渲染器或专业工具在Blender、3ds Max或独立的工具如CMFT Studio中导入HDRI渲染或导出为六张.png或.tga序列图。这是最标准、质量最高的方法。使用Unity插件Asset Store中有如“HDRP Sky”或“Cubemap Generator”等插件可以在编辑器内完成转换更为便捷。生成辐照度图与预滤波环境图这是最关键的预处理步骤。我们可以编写离线工具使用C或Python借助OpenCV或图像库实现球面卷积和预滤波卷积算法。这需要较强的图形学数学和编程能力。使用引擎或工具链Unity HDRP的烘焙系统可以生成这些图。更通用的方法是使用AMD的CubeMapGen工具已开源或Epic Games的官方IBL工具。这些工具通常提供GUI界面输入CubeMap选择输出辐照度图尺寸和预滤波Mip层级数即可一键生成。在Shader中实时近似对于移动端或性能敏感的场景我们有时会省去预滤波图而用一套简化的公式在Fragment Shader中实时计算镜面反射IBL虽然精度下降但速度更快。为了教程的完整性和可复现性我会假设你已经通过AMD CubeMapGen工具生成了一套纹理一张IrradianceMap128x128一张PrefilteredMap512x512带Mipmaps以及一张BRDF_LUT512x512。3. 完整Shader代码实现与逐行解析接下来是核心部分。我们将编写一个名为CustomPBR_IBL.shader的Surface Shader。这里我会先给出完整代码块然后分段进行深度解析。Shader Custom/CustomPBR_IBL { Properties { _Color (Albedo, Color) (1,1,1,1) _MainTex (Albedo (RGB), 2D) white {} _MetallicGlossMap (Metallic (R) Smoothness (A), 2D) white {} _Metallic (Metallic Scale, Range(0,1)) 0.0 _Glossiness (Smoothness Scale, Range(0,1)) 1.0 _BumpMap (Normal Map, 2D) bump {} _BumpScale (Normal Scale, Float) 1.0 _OcclusionMap (Occlusion Map, 2D) white {} _OcclusionStrength (Occlusion Strength, Range(0, 1)) 1.0 // IBL 纹理 _IrradianceMap (Irradiance CubeMap, Cube) black {} _PrefilteredMap (Prefiltered CubeMap, Cube) black {} _BRDFLUT (BRDF Lookup Texture, 2D) white {} } SubShader { Tags { RenderTypeOpaque } LOD 200 CGPROGRAM #pragma surface surf CustomLighting fullforwardshadows #pragma target 3.0 #include UnityPBSLighting.cginc sampler2D _MainTex, _MetallicGlossMap, _BumpMap, _OcclusionMap, _BRDFLUT; samplerCUBE _IrradianceMap, _PrefilteredMap; fixed4 _Color; half _Metallic, _Glossiness, _BumpScale, _OcclusionStrength; struct Input { float2 uv_MainTex; float2 uv_BumpMap; float2 uv_MetallicGlossMap; float2 uv_OcclusionMap; float3 worldPos; float3 worldNormal; INTERNAL_DATA }; // 自定义光照函数 half4 LightingCustomLighting (SurfaceOutputStandard s, float3 viewDir, UnityGI gi) { // 1. 解包表面参数 fixed4 albedo s.Albedo; half metallic s.Metallic; half smoothness s.Smoothness; half occlusion s.Occlusion; float3 normal s.Normal; // 2. 计算基础向量 float3 N normalize(normal); float3 V normalize(viewDir); float3 R reflect(-V, N); // 反射向量用于采样预滤波环境图 float NdotV max(dot(N, V), 0.0001); // 防止除零 // 3. 直接光照计算 (使用Unity GI数据) UnityLight light gi.light; float3 directDiffuse, directSpecular; directDiffuse light.color * albedo * (1.0 - metallic); // 金属度影响漫反射 float3 halfVec normalize(light.dir V); float NdotL max(dot(N, light.dir), 0.0); float NdotH max(dot(N, halfVec), 0.0); float VdotH max(dot(V, halfVec), 0.0); // 此处可调用UnityStandardBRDF.cginc中的BRDF函数为简化先使用近似 float D DistributionGGX(NdotH, smoothness); float G GeometrySmith(NdotV, NdotL, smoothness); float3 F FresnelSchlick(VdotH, albedo, metallic); directSpecular (D * G * F) / (4.0 * NdotV * NdotL 0.0001); directSpecular * light.color * NdotL; // 4. IBL 间接光照计算 // 4.1 漫反射 IBL (辐照度) float3 irradiance texCUBE(_IrradianceMap, N).rgb; float3 indirectDiffuse irradiance * albedo * (1.0 - metallic); // 4.2 镜面反射 IBL (分割求和近似) // a. 采样预滤波环境贴图 float roughness 1.0 - smoothness; // 注意Unity的Smoothness是粗糙度的反义 float mipLevel roughness * 6.0; // 假设预滤波图有7个Mip层级 (0-6) float3 prefilteredColor texCUBElod(_PrefilteredMap, float4(R, mipLevel)).rgb; // b. 采样BRDF LUT float2 brdfUV float2(NdotV, roughness); float2 brdf tex2D(_BRDFLUT, brdfUV).rg; // c. 组合镜面反射IBL float3 F_ibl FresnelSchlickRoughness(NdotV, albedo, metallic, roughness); float3 indirectSpecular prefilteredColor * (F_ibl * brdf.x brdf.y); // 5. 组合所有光照并应用环境光遮蔽 float3 diffuse directDiffuse indirectDiffuse; float3 specular directSpecular indirectSpecular; float3 finalColor (diffuse specular) * occlusion; // 加入Unity的全局环境光如果存在 finalColor gi.indirect.diffuse * albedo * (1.0 - metallic); return half4(finalColor, 1.0); } // 实现必要的工具函数 float DistributionGGX(float NdotH, float roughness) { float a roughness * roughness; float a2 a * a; float NdotH2 NdotH * NdotH; float denom (NdotH2 * (a2 - 1.0) 1.0); denom UNITY_PI * denom * denom; return a2 / max(denom, 0.0000001); } 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(float NdotV, float NdotL, float roughness) { return GeometrySchlickGGX(NdotV, roughness) * GeometrySchlickGGX(NdotL, roughness); } float3 FresnelSchlick(float VdotH, float3 albedo, float metallic) { float3 F0 lerp(0.04, albedo, metallic); // 非金属F0约0.04金属F0为albedo return F0 (1.0 - F0) * pow(1.0 - VdotH, 5.0); } float3 FresnelSchlickRoughness(float NdotV, float3 albedo, float metallic, float roughness) { float3 F0 lerp(0.04, albedo, metallic); return F0 (max(float3(1.0 - roughness, 1.0 - roughness, 1.0 - roughness), F0) - F0) * pow(1.0 - NdotV, 5.0); } void surf (Input IN, inout SurfaceOutputStandard o) { // 采样基础纹理 fixed4 albedoColor tex2D(_MainTex, IN.uv_MainTex) * _Color; fixed4 metallicGloss tex2D(_MetallicGlossMap, IN.uv_MetallicGlossMap); fixed4 occlusionTex tex2D(_OcclusionMap, IN.uv_OcclusionMap); // 赋值给表面输出结构 o.Albedo albedoColor.rgb; o.Metallic metallicGloss.r * _Metallic; o.Smoothness metallicGloss.a * _Glossiness; o.Occlusion lerp(1.0, occlusionTex.r, _OcclusionStrength); o.Normal UnpackScaleNormal(tex2D(_BumpMap, IN.uv_BumpMap), _BumpScale); o.Alpha albedoColor.a; } ENDCG } FallBack Diffuse }3.1 关键代码段深度剖析1. 光照函数入口LightingCustomLighting这是我们自定义光照模型的核心。Unity的Surface Shader会在生成最终代码时自动调用这个函数。它接收SurfaceOutputStandard结构体包含了surf函数中赋值的所有表面属性、观察方向viewDir和全局光照数据gi。所有复杂的光照计算都在这里发生。2. 直接光照与间接光照的分离计算这是PBR Shader的典型模式。直接光照directDiffuse,directSpecular来自场景中的实时光源如Directional Light, Point Light计算依赖于光源方向、颜色和强度。间接光照indirectDiffuse,indirectSpecular则完全来自环境贴图与具体光源无关。这种分离使得光照贡献清晰也便于性能优化例如静态物体可以烘焙间接光。3. 镜面反射IBL的Mipmap选择逻辑float mipLevel roughness * 6.0;这一行是精髓。它建立了材质粗糙度与预滤波环境贴图Mip层级之间的线性映射。为什么是6.0这取决于你生成预滤波图时指定的最大Mip层级。如果你用工具生成了7个Mip层级0到6那么粗糙度1.0完全粗糙就对应最模糊的第6层。你需要根据自己纹理的实际Mip数量来调整这个乘数。texCUBElod函数允许我们指定Mip层级进行采样这是实现粗糙度相关模糊的关键。4. BRDF积分查找表的使用float2 brdf tex2D(_BRDFLUT, brdfUV).rg;我们采样的是纹理的R和G两个通道。通常R通道存储了缩放后的菲涅尔项积分G通道存储了偏差值。这两个值与预滤波颜色相乘共同构成了准确的镜面反射积分近似。这张LUT是通用的与具体环境无关因此可以预先计算好并在所有材质中共享。5. 环境光遮蔽的巧妙应用float3 finalColor (diffuse specular) * occlusion;环境光遮蔽贴图AO Map记录了模型表面缝隙、凹陷处接收环境光更少的程度。我们在最后一步将其作为乘项应用到总光照结果上。注意它通常只影响间接光照但为了简化这里同时影响了直接和间接光。更精确的做法是只让AO影响indirectDiffuse和indirectSpecular。3.2 纹理采样与性能优化要点立方体贴图采样texCUBE用于辐照度图因为它不需要Mip。texCUBElod用于预滤波图因为我们需要指定Mip。在移动平台上texCUBElod可能开销较大可以考虑使用texCUBEbias或在顶点着色器中计算粗略的Mip。线性空间确保所有纹理导入设置中sRGB (Color Texture)选项正确。反照率贴图应勾选sRGB而金属度、粗糙度、法线、AO等数据贴图绝对不能勾选应取消勾选即Linear空间。同时在Player Settings中启用Linear Color Space这是PBR渲染正确的基石。纹理压缩辐照度图128x128可以使用低质量的压缩格式如ASTC 4x4。预滤波环境图由于有Mip且对颜色精度要求高建议使用BC7/ASTC 8x8等格式。BRDF LUT尺寸小且精度要求极高建议使用未压缩的RGBAFloat格式或高质量压缩。4. Unity场景配置与效果调试实战有了Shader我们还需要在Unity编辑器中正确设置场景才能看到IBL的效果。4.1 环境设置与材质配置设置环境光照源将你转换好的环境CubeMap例如Skybox.cubemap拖入项目。打开Window Rendering Lighting Settings。在Environment标签页下将Skybox Material指定为一个使用该CubeMap的天空盒材质。或者更直接地将CubeMap赋给Environment Reflections下的Source并选择Custom。确保Environment Lighting下的Source也设置为Skybox。这样Unity的内置全局光照和反射探针才会使用我们提供的HDRI信息。创建并配置材质在项目里创建一个新材质Shader选择Custom/CustomPBR_IBL。将准备好的纹理分别拖拽到对应属性槽_MainTex,_MetallicGlossMap,_BumpMap,_OcclusionMap。关键一步将预处理好的IrradianceMap、PrefilteredMap和BRDFLUT纹理分别赋给材质的对应属性。_IrradianceMap和_PrefilteredMap的类型必须是Cubemap。调整_Metallic和_Glossiness滑块观察材质变化。应用到模型将一个模型如Stanford Dragon、Sphere等拖入场景将材质赋给它。4.2 效果对比与参数调试现在旋转场景中的模型你会看到其表面的光照和反射会随着环境CubeMap的内容而动态变化。为了直观感受IBL的威力可以进行以下对比开关IBL在Shader代码中临时注释掉indirectDiffuse和indirectSpecular的计算只保留直接光照。对比两者你会发现没有IBL的模型看起来非常“平”且“假”缺乏环境光的融入感。更换HDRI准备两张光照氛围截然不同的HDRI如明亮的户外和昏暗的室内生成两套对应的IBL纹理并在材质中切换。观察同一材质在不同环境下的表现。这是验证你IBL系统是否正常工作的最好方法。调试参数金属度从0调到1观察物体如何从绝缘体如塑料过渡到导体如金属。金属度高的部分漫反射会消失颜色完全由镜面反射即环境反射决定。光滑度从0调到1观察反射从模糊、大范围的漫反射状逐渐变得清晰、锐利。注意观察预滤波环境图的不同Mip层级是如何被使用的。法线强度调整_BumpScale观察表面凹凸细节对光照和反射的细微影响。4.3 与Unity内置系统的协同与替代方案我们的自定义Shader是一个完整的、独立的光照模型。但在实际项目中我们可能需要与Unity的其他系统协作。反射探针对于动态物体Unity的反射探针是实时捕捉场景环境并用于IBL的绝佳方案。你可以让反射探针使用我们设置的HDRI天空盒作为捕捉源。在我们的Shader中可以通过Unity_GlossyEnvironment函数来获取引擎提供的反射数据从而替代我们手动采样的_PrefilteredMap。这能实现动态物体与静态场景的反射融合。光照贴图对于完全静态的物体其间接漫反射光照可以被烘焙到光照贴图中性能最优。此时我们的Shader中indirectDiffuse部分可以简化或移除直接从光照贴图采样。URP内置函数如果你在URP下工作强烈建议研究ShadeSHPerPixel球谐光照和GlossyEnvironmentReflection函数。它们封装了引擎优化的IBL计算比自己手写的版本通常更高效、功能更全如支持屏幕空间反射。我们的自定义实现是理解这些“黑盒”内部原理的钥匙。5. 常见问题、性能考量与进阶优化即使代码正确在实现和优化过程中你仍会遇到不少挑战。这里记录一些典型的“坑”和解决方案。5.1 问题排查清单问题现象可能原因排查步骤与解决方案模型全黑或极暗1. 环境贴图未正确设置或为纯黑。2. Shader中采样CubeMap的向量错误如用了世界位置而非法线。3. 线性空间未启用导致计算错误。1. 检查Lighting Settings中的天空盒和环境光源确保CubeMap已赋值且HDR属性正确。2. 在Shader中使用return float4(N, 1);等调试输出检查法线向量是否正确。3. 在Player Settings中切换到Linear Color Space。反射颜色错误如偏紫1. 预滤波环境图或BRDF LUT纹理的导入设置错误如sRGB错误开启。2. 菲涅尔项F0计算错误。1. 确认所有数据纹理预滤波图、BRDF LUT、金属/粗糙度贴图的导入设置中取消勾选“sRGB (Color Texture)”。2. 检查FresnelSchlick函数中非金属的基准反射率F0是否为0.04左右。反射边缘有锯齿或闪烁1. 预滤波环境图的Mipmaps未生成或生成不正确。2. 粗糙度到Mip层级的映射系数不对。3. 立方体贴图采样时未进行三线性过滤。1. 检查_PrefilteredMap纹理的导入设置确保“Generate Mip Maps”已勾选且Mipmap数量足够如7级。2. 调整mipLevel roughness * UNITY_SPECCUBE_LOD_STEPS;UNITY_SPECCUBE_LOD_STEPS是Unity内置变量代表Mip层级数。3. 确保Shader中使用了texCUBElod自动三线性而非texCUBE。移动设备上帧率骤降1. 立方体贴图分辨率过高。2. 每帧多次采样高分辨率CubeMap。3. BRDF LUT采样或复杂计算过多。1. 将辐照度图降至64x64预滤波图降至256x256。质量损失在移动端小屏幕上不明显。2. 考虑将漫反射IBL替换为球谐光照SH计算量极低。3. 确保BRDF LUT使用Nearest Point采样并预计算到常量或顶点着色器中。与场景中其他标准Shader效果不匹配1. 能量守恒处理不同。2. 色调映射或后处理影响不一致。1. 确保你的BRDF函数D、G、F是能量守恒的模型如Cook-Torrance。使用Unity官方UnityStandardBRDF.cginc中的函数可保证一致。2. 将你的材质和Standard材质放在同一场景、同一后处理体积下对比。5.2 性能优化实战心得在移动端或大型场景中全精度IBL可能是性能瓶颈。以下是一些行之有效的优化策略漫反射替代方案球谐光照对于低频的漫反射辐照度用9个系数的球谐函数来近似其计算成本仅为几次点乘远低于采样立方体贴图。Unity内置了ShadeSH9函数。你可以离线将辐照度图投影到SH系数运行时在Shader中使用。这是移动端PBR的标配优化。镜面反射优化LOD与混合对于远处或次要的物体可以降低IBL计算的精度。例如直接使用较低Mip层级的预滤波图或者完全关闭镜面反射IBL用一张纯色的环境反射代替。纹理压缩与合图如前所述 aggressive地压缩IBL纹理。甚至可以考虑将BRDF LUT的RG两个通道合并到一张纹理的不同颜色通道中减少一次纹理采样。将计算移至顶点着色器对于低模或静态物体可以将NdotV等计算在顶点着色器中完成然后插值到片元着色器虽然精度略有下降但能节省大量计算。5.3 从实现到精通进阶方向探讨当你成功运行起这个自定义IBL Shader后可以沿着以下几个方向深入这能让你从“会用”变成“高手”实现更真实的材质加入清漆层、各向异性、次表面散射等高级材质模型。IBL作为环境光照部分可以与这些特性很好地结合。动态环境我们的环境是静态的。如何实现动态的IBL这就需要结合反射探针系统让探针实时捕捉场景可能包含动态物体并更新到立方体贴图中然后供Shader采样。屏幕空间反射对于光滑表面IBL反射可能不够精确因为它反射的是天空盒而非场景中的实际物体。可以将SSR与IBL结合近处物体使用SSR远处和缺失信息用IBL环境贴图填补达到质量与性能的平衡。自定义BRDF尝试实现Disney BRDF、Kulla-Conty多重散射补偿等更现代、更物理准确的BRDF模型并为其适配IBL计算。理解如何为新的BRDF生成对应的BRDF LUT。这套完整的Shader代码和实现思路已经为你打通了从一张HDRI到最终游戏画面的核心路径。真正的掌握来自于不断的调试、优化和将其应用到复杂项目中去解决实际的光照问题。不妨现在就找一个你喜欢的HDRI按照这个流程走一遍亲眼见证一个平淡的模型如何被环境光照赋予灵魂。