资讯动态

Unity ForwardBase光照Pass核心原理与实战解析

发布时间:2026/10/15 4:02:29 来源:尧图企业网站定制
1. 项目概述为什么一个ForwardBase函数值得花一整篇来拆解在Unity Shader开发的实战现场我见过太多人卡在同一个地方改了几十遍光照计算场景里物体就是不亮或者明明写了法线贴图模型表面却像一块没打磨的水泥板更常见的是换了个灯光类型整个材质就彻底失色——不是过曝就是死黑。这些问题背后十有八九不是贴图错了、法线反了而是你根本没真正看懂Unity底层是怎么把“光”和“物”捏在一起的。而这个链条上最核心、最不可绕过的那个齿轮就是ForwardBase Pass里的那个看似平淡无奇的frag函数。“Unity StandardShader系列1——ForwardBase源码解析”这个标题里没有炫技的词没有新版本号甚至没提URP或HDRP但它直指Unity前向渲染Forward Rendering的命门。Standard Shader不是黑盒它是一套被千锤百炼、经受过无数项目验证的工业级光照范式。而ForwardBase正是这套范式中负责处理主方向光Main Directional Light 环境光Ambient Light 自发光Emission 全局光照探针Light Probe 反射探针Reflection Probe的第一道也是最重要的一道计算入口。它不是“其中一段代码”它是整个光照管线的总控台——所有后续Pass比如ForwardAdd处理点光/聚光都建立在它的输出基础之上。如果你正在用Built-in Render Pipeline做项目无论是独立游戏、工业仿真还是建筑可视化只要你的材质依赖Standard或基于它的自定义Shader那么ForwardBase就是你必须亲手摸透的“地基”。它决定了你的模型在默认光照下是否可信决定了美术给的PBR参数能否真实还原也决定了你后续加自定义效果时是事半功倍还是处处补丁。我带过的几个团队新人上手Shader第一个月的任务从来不是写新效果而是把ForwardBase从头到尾手敲一遍、逐行注释、关掉每一项光源单独验证——三个月后他们调试光照问题的速度比之前快了至少五倍。这不是玄学是当你知道光子从哪里来、怎么打、打完怎么算自然就不再靠“多调几个值试试看”。这篇解析不讲抽象理论不堆数学公式只带你钻进Unity官方发布的Standard.shader源码里一行行看它怎么把_WorldSpaceLightPos0、_LightColor0、_LightDir这些宏和变量变成屏幕上那一片真实的明暗过渡。你会看到所谓“标准”其实是无数个精妙取舍的结果为什么环境光要分SH和直接光两路为什么反射探针采样要用UNITY_SAMPLE_TEXCUBE_LOD而不是普通采样为什么half3 diffuse LightingLambert(...)这行调用背后藏着整整一个独立的光照函数文件这些答案不在文档里而在你打开编辑器、定位到那几百行HLSL代码的瞬间。2. 整体设计与思路拆解ForwardBase不是“一个Pass”而是一套协同作战的系统2.1 它为何必须是第一个Pass——渲染顺序即逻辑顺序在Unity的Built-in管线中ForwardBase Pass被强制标记为Tags { LightMode ForwardBase }并且其RenderType被设为Opaque。这不是随意定的而是由渲染队列Render Queue和光照Pass调度机制共同决定的硬性规则。简单说ForwardBase必须是所有Opaque物体渲染流程中的第一个光照Pass原因有三第一数据初始化依赖。ForwardBase计算出的基础漫反射diffuse、高光specular和环境光ambient结果会作为后续Pass如ForwardAdd的输入基础。比如ForwardAdd处理额外点光源时它叠加的不是原始颜色而是叠加在ForwardBase输出的o.Albedo * o.Alpha之上的增量光照。如果ForwardBase没跑后续Pass拿到的就是未定义的垃圾值。第二全局光照数据绑定。Unity的Light Probe和Reflection Probe数据是在ForwardBase Pass中通过UnityGI结构体一次性打包传入的。这些数据是静态预计算的占用显存且更新成本高不可能每个Pass都重复绑定。ForwardBase作为首个光照Pass承担了“一次加载、全局复用”的职责。你如果强行把Probe采样挪到ForwardAdd里不仅性能暴跌还会因采样坐标不一致导致光照闪烁。第三阴影投射锚点。主方向光通常是太阳光的阴影贴图Shadow Map采样也是在ForwardBase中完成的。Unity通过SHADOW_COORDS(2)宏在顶点着色器中计算阴影坐标并在片元着色器中用UNITY_TRANSFER_SHADOW和UNITY_APPLY_SHADOW完成深度比较。这个流程高度耦合于ForwardBase的顶点变换空间从对象空间→世界空间→阴影裁剪空间其他Pass若想用同一份阴影必须复用这套坐标体系否则阴影位置完全错乱。提示你可以用Frame Debugger帧调试器直观验证这一点。在任意Standard材质物体上触发渲染展开其Draw Call列表你会发现ForwardBase总是排在最前面且其Shader Tag明确显示为“ForwardBase”。点开它再点开“Shader Variables”就能看到_WorldSpaceLightPos0、_LightColor0等关键变量已被正确注入——而ForwardAdd Pass里这些变量要么为空要么是另一组值。2.2 它为何不叫“MainLight”而叫“ForwardBase”——语义即架构名字本身泄露了设计哲学。“ForwardBase”中的“Base”二字精准概括了它的双重角色既是基础光照Basic Lighting的提供者也是前向渲染管线Forward Pipeline的基石。它不像Custom Pass那样可以随意增删而是Unity Built-in管线协议的一部分是引擎与Shader之间的一份契约。这份契约规定了ForwardBase必须完成以下最小功能集计算主方向光的Lambert漫反射 Blinn-Phong高光混合环境光含Light Probe的球谐函数SH系数加入自发光Emission和基础Alpha混合采样并混合Reflection Probe反射探针输出最终的fixed4 frag(...) : SV_Target颜色任何试图绕过ForwardBase、自己另起炉灶写“主光计算”的做法在Built-in管线中都是危险的。因为Unity的光照系统Lighting Window、GI烘焙器、Probe更新器全部默认ForwardBase是那个“权威计算者”。你写一个MyCustomBase引擎不会自动把Probe数据塞给你也不会帮你处理阴影更不会在Lighting窗口里同步你的参数。这就是为什么哪怕你要做一个极简的非PBR效果最佳实践也是继承Standard Shader然后重写ForwardBase里的LightingXXX函数而不是另起一套。2.3 它为何要拆成顶点片元两段——GPU并行计算的物理限制ForwardBase的完整实现横跨vert顶点着色器和frag片元着色器两个阶段这不是为了代码好看而是GPU硬件特性的刚性约束。顶点着色器运行在每个顶点上频率低一个三角形3个顶点但可执行复杂变换。ForwardBase的vert函数主要干三件事空间变换将顶点从对象空间Object Space变换到世界空间World Space为后续光照计算提供统一坐标系法线变换用UnityObjectToWorldNormal(v.normal)将法线从对象空间转到世界空间并归一化normalize这是光照计算的前提插值准备计算并输出供片元着色器插值使用的变量如世界空间顶点位置o.worldPos、世界空间法线o.worldNormal、阴影坐标o.shadowCoord、以及Light Probe/Reflection Probe的采样坐标o.probeHDR、o.reflUVW。而片元着色器运行在每个像素上频率极高一个三角形可能覆盖成百上千像素但计算资源受限。ForwardBase的frag函数则专注在“每像素决策”根据插值得到的世界法线和世界顶点位置计算光照方向向量执行Lambert点积dot(worldNormal, lightDir)和Blinn-Phong高光计算pow(dot(halfDir, worldNormal), _Gloss)采样Light Probe的球谐系数ShadeSH9和Reflection Probe的立方体贴图UNITY_SAMPLE_TEXCUBE_LOD将所有光照项主光diffuse/specular 环境光 自发光 反射按物理意义加权混合。这种分工本质是把“一次计算、多次复用”的任务如空间变换放在顶点端把“多次计算、每次不同”的任务如像素级光照放在片元端最大限度榨取GPU并行能力。我曾见过有开发者把ShadeSH9调用直接写在vert里结果发现模型边缘出现严重色块——因为球谐函数需要在片元级进行平滑插值顶点级计算后线性插值会破坏其频域特性。3. 核心细节解析与实操要点从宏定义到数学本质3.1#include UnityCG.cginc与#include Lighting.cginc——两座不可逾越的桥梁ForwardBase的源码开头必然有这两行#include。它们不是装饰而是整个光照计算的基础设施。UnityCG.cginc提供了Unity引擎层的基础工具集UnityObjectToWorldNormal(float3 v)将法线从对象空间转换到世界空间。它内部使用unity_WorldToObject的逆矩阵转置即unity_WorldToObject的前三行前三列的转置这是法线变换的正确方式普通顶点用unity_ObjectToWorld法线必须用其转置逆矩阵否则缩放会导致法线长度失真。UnityObjectToWorldDir(float3 v)将方向向量如切线、副切线从对象空间转到世界空间用的是纯旋转部分忽略位移避免方向被错误平移。UnityWorldToClipPos(float4 v)世界空间到裁剪空间的最终变换是vert函数里o.pos UnityWorldToClipPos(o.worldPos)的核心。而Lighting.cginc则是光照逻辑的中枢LightingLambert标准Lambert漫反射函数输入世界法线、光照方向、光照颜色输出漫反射颜色。其内部实现就是saturate(dot(worldNormal, lightDir)) * _LightColor0.rgb但封装后支持多光源叠加和Gamma校正开关。LightingBlinnPhongBlinn-Phong高光模型输入世界法线、视角方向、光照方向、高光颜色、高光强度_SpecColor, _Shininess输出高光颜色。关键在于halfDir normalize(lightDir viewDir)这比Phong模型的reflect更高效且视觉更自然。ShadeSH99阶球谐函数Spherical Harmonics环境光计算函数。它接收世界空间法线和一组12个float的SH系数unity_SHAr,unity_SHAg,unity_SHAb等输出一个half3环境光颜色。SH9能以极低存储12个float近似全方向环境光是Light Probe的核心技术。注意ShadeSH9的参数顺序极易出错。它要求法线是单位向量且SH系数必须是Unity GI系统烘焙生成的unity_SH*变量。如果你手动传入一组随机float结果会是完全不可预测的噪点。实测中我曾因忘记对o.worldNormal做normalize导致ShadeSH9输出一片惨白——因为未归一化的法线点积结果超出[-1,1]范围SH函数内部饱和处理失效。3.2_WorldSpaceLightPos0与_LightColor0——主光的“身份证”与“能量包”在ForwardBase的frag函数里这两个内置变量是光照计算的起点_WorldSpaceLightPos0这是一个float4。当它是方向光时.xyz是世界空间中指向光源的方向向量注意是从物体指向光源所以实际光照方向是-lightPos.xyz.w为0.0当它是点光或聚光时.xyz是光源在世界空间的位置.w为1.0。ForwardBase默认按方向光处理所以代码中常见float3 lightDir normalize(_WorldSpaceLightPos0.xyz)。_LightColor0这是一个half4包含主光源的颜色rgb和强度a。它不是美术在Inspector里调的Light.color原始值而是经过Unity光照系统处理后的最终值已乘以光源强度Light.intensity、已应用颜色温度Light.colorTemperature、已考虑衰减对点光/聚光并被裁剪到合理范围。这意味着你不需要、也不应该在Shader里再乘一遍_LightIntensity——它已经在那里了。这里有个经典陷阱很多新手以为_LightColor0.a是Alpha通道想用来控制透明度。其实.a是光源的强度缩放因子intensity multiplier在方向光下恒为1.0在点光下会随距离衰减而变小。把它用在Alpha混合上会导致物体在强光下变透明、弱光下变不透明完全违背设计意图。3.3UNITY_TRANSFER_SHADOW与UNITY_APPLY_SHADOW——阴影不是“画上去”的而是“比出来”的ForwardBase的阴影实现是Unity封装最深、也最容易误解的部分。它不涉及任何纹理采样代码而是通过两个宏完成UNITY_TRANSFER_SHADOW(o)这是一个顶点着色器宏作用是将顶点的世界空间位置o.worldPos通过阴影投影矩阵_LightMatrix0变换到阴影贴图的UV坐标系并存储在o.shadowCoord中。这个坐标是float4其xy是标准化设备坐标NDC的uvz是深度值w是齐次除法的w分量。UNITY_APPLY_SHADOW(i)这是一个片元着色器宏作用是用i.shadowCoord对阴影贴图_ShadowMapTexture进行采样并将采样得到的深度值与i.shadowCoord.z / i.shadowCoord.w即当前像素的深度进行比较。如果当前深度大于贴图深度说明该像素被遮挡返回0.0全阴影否则返回1.0全亮。最终结果是一个float即阴影遮罩shadow mask。关键点在于阴影不是一张“黑色图片”贴在物体上而是一个实时的深度比较过程。这也是为什么阴影边缘可以有柔和过渡PCF滤波——UNITY_APPLY_SHADOW内部会根据_ShadowBias参数在采样时进行多点偏移和平均模拟软阴影效果。如果你在frag里直接tex2D(_ShadowMapTexture, i.shadowCoord.xy)你得到的只是深度图本身不是阴影。实操心得调试阴影时不要先看最终颜色而是把UNITY_APPLY_SHADOW(i)的结果直接输出为return fixed4(shadow, shadow, shadow, 1);。如果看到的是一片纯白说明shadowCoord没传对检查vert里是否漏了UNITY_TRANSFER_SHADOW如果是一片纯黑说明深度比较永远失败检查_LightMatrix0是否为空或光源是否开启了Cast Shadows如果边缘锯齿严重说明_ShadowBias太小需要在Light组件里调大Bias值。3.4UnityGI与UnityGIInput——全局光照的“数据快递员”ForwardBase之所以能同时处理Direct Light直接光、Light Probe光照探针和Reflection Probe反射探针全靠UnityGI这个结构体。它不是一个函数而是一个数据容器在frag函数开始时被构造UnityGIInput d; d.light light; d.worldPos worldPos; d.worldViewDir worldViewDir; d.atten atten; d.halfDir halfDir; UnityGI gI; UNITY_INITIALIZE_OUTPUT(UnityGI, gI); gI UnityGlobalIllumination(d, o.Occlusion, o.Normal);这段代码的含义是d是一个输入结构体打包了当前像素的所有必要信息光照数据light来自_LightColor0等、世界位置、视线方向、衰减系数、半角向量o.Occlusion是环境光遮蔽AO值通常来自Occlusion贴图或Shader Graph节点o.Normal是世界法线UnityGlobalIllumination是一个宏它会根据编译宏如ENABLE_LIGHT_PROBE_SH、ENABLE_REFLECTION_PROBES自动选择调用UnityGI_Direct仅直接光、UnityGI_Indirect间接光或UnityGI_All全部。UnityGI结构体最终输出三个关键字段gI.light: 直接光照主方向光的贡献已包含阴影gI.indirect.diffuse: 间接漫反射来自Light Probe的SH9计算gI.indirect.specular: 间接高光来自Reflection Probe的立方体贴图采样。这才是ForwardBase真正“全局”的地方它把Direct、Indirect、Diffuse、Specular全部揉在一个结构体里让你在后续混合时可以清晰地看到每一部分的权重。比如half3 lighting gI.light.diffuse gI.indirect.diffuse emission;这行代码一眼就能看出最终漫反射直接光漫反射间接光漫反射自发光。4. 实操过程与核心环节实现手把手还原ForwardBase的每一行4.1 从零构建一个最小可行ForwardBase Pass我们不直接修改Standard.shader它太庞大而是新建一个SimpleForwardBase.shader只保留ForwardBase的核心骨架。以下是完整、可运行的代码已去除所有无关分支仅保留方向光路径Shader Custom/SimpleForwardBase { Properties { _Color (Color, Color) (1,1,1,1) _MainTex (Albedo (RGB), 2D) white {} _Glossiness (Smoothness, Range(0,1)) 0.5 _Metallic (Metallic, Range(0,1)) 0.0 _EmissionColor (Emission Color, Color) (0,0,0,1) } SubShader { Tags { RenderTypeOpaque LightModeForwardBase } LOD 200 Pass { Name FORWARD Tags { LightModeForwardBase } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fwdbase #pragma multi_compile_fog #include UnityCG.cginc #include Lighting.cginc #include AutoLight.cginc struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; float2 uv : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; float3 worldNormal : TEXCOORD1; float3 worldPos : TEXCOORD2; SHADOW_COORDS(3) // 为阴影坐标预留TEXCOORD3 UNITY_FOG_COORDS(4) }; sampler2D _MainTex; float4 _MainTex_ST; fixed4 _Color; half _Glossiness; half _Metallic; fixed4 _EmissionColor; v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); o.worldNormal UnityObjectToWorldNormal(v.normal); o.worldPos mul(unity_ObjectToWorld, v.vertex).xyz; // 为阴影传递坐标 TRANSFER_SHADOW(o); // 为雾效传递坐标 UNITY_TRANSFER_FOG(o,o.pos); return o; } fixed4 frag (v2f i) : SV_Target { // 1. 基础采样 fixed4 albedo tex2D(_MainTex, i.uv) * _Color; half3 worldNormal normalize(i.worldNormal); half3 worldPos i.worldPos; half3 viewDir normalize(_WorldSpaceCameraPos.xyz - worldPos); // 2. 主方向光计算 half3 lightDir normalize(_WorldSpaceLightPos0.xyz); half3 lightColor _LightColor0.rgb; // 3. 漫反射 (Lambert) half diff max(0.0, dot(worldNormal, lightDir)); half3 diffuse diff * lightColor; // 4. 高光 (Blinn-Phong) half3 halfDir normalize(lightDir viewDir); half spec pow(max(0.0, dot(worldNormal, halfDir)), _Glossiness * 128.0); half3 specular spec * _LightColor0.rgb * _Color.rgb; // 5. 环境光 (简化版仅直接环境光) half3 ambient ShadeSH9(half4(worldNormal, 1.0)); // 6. 自发光 half3 emission _EmissionColor.rgb; // 7. 阴影 half shadow SHADOW_ATTENUATION(i); // 8. 最终混合 half3 color albedo.rgb * (diffuse ambient) specular emission; color * shadow; // 应用阴影 // 9. 雾效 UNITY_APPLY_FOG(i.fogCoord, color); return fixed4(color, albedo.a); } ENDCG } } FallBack Diffuse }这段代码的关键实操点#pragma multi_compile_fwdbase这是启用ForwardBase所有特性的开关。没有它_WorldSpaceLightPos0等变量不会被注入SHADOW_ATTENUATION宏也无法工作。SHADOW_COORDS(3)与TRANSFER_SHADOW(o)SHADOW_COORDS(3)声明o.shadowCoord将占用TEXCOORD3TRANSFER_SHADOW则负责填充它。数字3必须严格匹配否则坐标错位。ShadeSH9(half4(worldNormal, 1.0))注意ShadeSH9的输入是half4第四个分量必须是1.0这是SH函数的约定。shadow变量的应用位置它必须在所有光照计算之后、最终color赋值之前乘上。如果提前乘在diffuse上会导致环境光也被错误遮挡。4.2 参数计算的物理意义与美术协作逻辑ForwardBase里的每一个参数都不是孤立的而是与Unity编辑器中的美术设置一一映射_Glossiness光滑度对应Inspector中Material的Smoothness滑块。它的值范围是[0,1]但在Blinn-Phong计算中pow(..., _Glossiness * 128.0)将其映射到[0,128]的指数范围。128.0是经验值能让0.5的滑块对应中等锐利的高光。美术调0.2意味着高光非常弥散类似哑光塑料调0.8则高光尖锐集中类似抛光金属。_Metallic金属性在Standard Shader中它控制材质是介电质Dielectric如塑料、陶瓷还是导体Conductor如金属。金属性为0时高光颜色_LightColor0白色高光金属性为1时高光颜色albedo金属本色高光。但在我们的简化版中暂未引入金属流所以_Metallic未被使用。如果你想加入需要替换specular计算为specular spec * lerp(_LightColor0.rgb, albedo.rgb, _Metallic)。_EmissionColor自发光它不参与任何光照计算而是直接加到最终颜色上。美术用它来表现屏幕、霓虹灯、熔岩等自身发光的表面。注意它不受阴影影响——一个自发光的物体在阴影里依然会发亮这符合物理直觉。Occlusion环境光遮蔽虽然我们的简化版没用到但在完整Standard中o.Occlusion来自Occlusion贴图R通道值为0~1。它被乘在ambient上让模型凹陷处如缝隙、孔洞环境光更暗极大增强立体感。美术制作AO贴图时就是在告诉Shader“这里不容易被环境光照到”。4.3 关键步骤的逐行调试技巧调试ForwardBase不能只看最终画面要像外科医生一样一层层剥离Step 1验证基础几何与UV注释掉所有光照代码只留return tex2D(_MainTex, i.uv);确保贴图正确拉伸无扭曲、无翻转。如果出错检查TRANSFORM_TEX(v.uv, _MainTex)和_MainTex_ST是否被正确传入。Step 2验证法线方向注释掉光照只输出return fixed4(worldNormal * 0.5 0.5, 1);归一化法线映射到[0,1]范围正常应看到蓝Z轴朝前、红X轴朝右、绿Y轴朝上的渐变。如果全是黑说明worldNormal为0检查UnityObjectToWorldNormal是否被正确调用。Step 3验证主光方向注释掉环境光、阴影、高光只留return fixed4(diff * lightColor, 1);旋转场景主方向光观察模型明暗面是否随灯光移动。如果不动检查_WorldSpaceLightPos0是否为0可能是Light未开启或未设为主光源。Step 4验证阴影如前所述输出shadow值。正常应是0~1的灰度图阴影区为黑非阴影区为白。Step 5验证环境光关闭所有实时光源把Directional Light的Intensity设为0只留return fixed4(ambient, 1);此时模型应呈现均匀的、柔和的底色由Light Probe决定。如果一片死黑说明Probe未烘焙或ShadeSH9输入错误。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 “我的模型在ForwardBase里是黑的但ForwardAdd里却有光”——光照模式错配现象场景里只有一个Directional Light模型完全不亮纯黑但当你添加一个Point Light后模型上突然出现了点光源的高光而主光区域依然死黑。根因LightMode标签错配。你的Shader Pass被错误地标记为LightMode ForwardAdd而非ForwardBase。ForwardAdd只处理额外光源它不接收_WorldSpaceLightPos0和_LightColor0这两个变量在ForwardAdd里是未定义的值为0所以主光计算结果为0。排查在Shader Inspector里点击“Compile and show code”搜索LightMode确认Pass的Tag确实是ForwardBase。在Frame Debugger中确认该Draw Call的Shader Tag显示为ForwardBase而不是ForwardAdd或Always。修复确保Pass的Tags块中LightMode ForwardBase是唯一且正确的。5.2 “法线贴图一开模型就闪烁、撕裂”——TBN矩阵未归一化现象启用法线贴图后模型表面出现高频噪点、条纹或随视角剧烈闪烁。根因法线贴图采样后得到的切线空间法线tex2D(_BumpMap, i.uv).rgb * 2 - 1在变换到世界空间时其长度未被归一化。由于插值和纹理压缩采样出的法线向量长度往往偏离1.0当它被用于dot(worldNormal, lightDir)时点积结果会超出[-1,1]范围saturate函数将其钳制为0或1造成块状色阶。排查临时将worldNormal输出为颜色return fixed4(normalize(worldNormal) * 0.5 0.5, 1);。如果看到的不是平滑渐变而是明显色块说明法线向量长度不一。修复在frag函数中对任何来自纹理的法线必须立即归一化half3 tangentNormal UnpackNormal(tex2D(_BumpMap, i.uv)); half3 worldNormal normalize( tangentNormal.x * i.tangentWorld tangentNormal.y * i.bitangentWorld tangentNormal.z * i.normalWorld );注意这里i.tangentWorld等必须在vert中正确计算并插值。5.3 “Light Probe的环境光颜色怪怪的像蒙了一层灰”——Gamma与Linear色彩空间混淆现象在Linear色彩空间项目中Light Probe烘焙出的环境光显得过暗、发灰缺乏活力。根因ShadeSH9函数返回的是Linear空间的颜色但如果你的项目设置为Gamma空间Unity会在最终输出时进行Gamma校正。然而ShadeSH9的系数是为Linear空间烘焙的直接在校正后的Gamma空间使用会导致颜色失真。排查检查Edit Project Settings Player Other Settings Color Space确认是Linear还是Gamma。如果是Linear确保所有贴图尤其是Light Probe的unity_SH*变量都以Linear方式读取。修复在frag函数中对ShadeSH9的结果进行手动Gamma校正仅在Gamma空间项目中需要#if !defined(UNITY_COLORSPACE_GAMMA) half3 ambient ShadeSH9(half4(worldNormal, 1.0)); #else half3 ambient GammaToLinearSpace(ShadeSH9(half4(worldNormal, 1.0))); #endif不过强烈建议项目全程使用Linear色彩空间这是现代渲染的标准。5.4 “Reflection Probe采样结果是纯黑”——探针未激活或坐标错误现象启用了Reflection Probe也设置了Blend Distance但模型反射始终是黑色。根因两个常见原因。一是Probe本身未被激活Enable Box Projection未勾选或Box Size为0二是frag中采样坐标i.reflUVW未被正确计算和传递。排查在Scene视图中选中Reflection Probe确认其Box Size不为0且Mode为Baked或Realtime。在vert函数中检查是否调用了UNITY_TRANSFER_REFLECTION(i, worldPos)并在v2f结构体中声明了float3 reflUVW : TEXCOORD5;。修复确保v2f结构体中有float3 reflUVW : TEXCOORD5;且vert中o.reflUVW reflect(-viewDir, worldNormal); o.reflUVW mul(unity_WorldToObject, float4(o.reflUVW, 0.0)).xyz;然后在frag中half3 reflection UNITY_SAMPLE_TEXCUBE_LOD(unity_ReflectionTex, i.reflUVW, 0).rgb;5.5 “为什么我的自定义ForwardBase比Standard Shader还慢”——过度计算与冗余分支现象替换了Standard Shader的ForwardBase性能反而下降GPU耗时增加。根因Standard Shader经过极致优化而自定义版本常犯两个错误一是做了不必要的计算如在vert里计算viewDir二是开启了大量编译分支#pragma multi_compile过多。优化技巧延迟计算viewDir只需在frag中计算一次不要在vert里算好再插值插值会损失精度且浪费带宽。精简宏#pragma multi_compile_fwdbase已足够不要额外加#pragma multi_compile __ LIGHTMAP_ON DYNAMICLIGHTMAP_ON除非你真需要这些功能。合并操作diffuse和specular的lightColor乘法可以合并为lightColor * (diff spec * _SpecColor.rgb)减少一次纹理采样。我踩过的最大坑在frag里反复调用UnityObjectToWorldNormal(v.normal)。这个函数内部有矩阵乘法每帧每像素执行一次开销巨大。正确做法是在vert里计算一次插值到fragfrag里只做normalize。6.

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑