首页 / 资讯中心 / 文章详情

Unity Shader全息效果实现:透明渲染与菲涅尔边缘光详解

Unity Shader全息效果实现:透明渲染与菲涅尔边缘光详解 ★ FEATURED ARTICLE
全息投影这个词这几年被用得有点泛滥展会上随便一块半透明玻璃后面放个显示器就敢叫全息但真落到实时渲染里我们做 Shader 的人心里都清楚所谓“全息效果”本质上就是一套透明渲染 边缘光 扫描线 菲涅尔的组合拳。这个项目标题里的“全息效果透明”抓得很准括号里那两个字才是重点——全息感能不能出来八成取决于透明混合这一层有没有处理干净。我打算把这次拆解写成一份可以直接照着复现的记录从思路选型到参数计算再到我踩过的那些坑尽量讲透。适合已经写过几个基础 Shader、想搞明白透明物体渲染顺序和菲涅尔边缘光怎么配合的读者纯新手也能看懂因为我会把每个参数为什么这么设都掰开说。1. 全息效果的整体设计与思路拆解1.1 为什么全息效果绕不开透明渲染先把概念理清楚。全息效果在实时渲染里追求的是“物体像由光构成、有体积感但不实体”的视觉印象。它和普通不透明材质的根本区别在于光线要能穿透物体同时物体边缘要比中心更亮还要有一层若有若无的扫描纹理在流动。这三条需求里第一条直接把我们推进了透明渲染的领域。Unity 里透明渲染的核心是Blend 指令和渲染队列。不透明物体走 Geometry 队列2000从前往后画靠深度缓冲Z-Buffer解决遮挡透明物体走 Transparent 队列3000从后往前画靠混合公式把颜色叠上去。全息效果必须放在 Transparent 队列因为我们要让背景透过物体显示出来。这里有个很多人第一次做会忽略的点一旦你写了Blend SrcAlpha OneMinusSrcAlpha深度写入ZWrite通常要关掉否则透明物体之间会互相遮挡出现“后面的全息板被前面的挡住一块”的诡异现象。我试过保留 ZWrite On 的做法结果是两个全息物体交叠时先画的那个会把后画的直接裁掉视觉上像被刀切了一块。关掉 ZWrite 之后遮挡关系交给渲染顺序和混合来表现虽然会带来排序问题后面第 4 节细讲但对全息这种“本来就不该有实体遮挡感”的效果来说反而是对的。1.2 菲涅尔边缘光是全息感的骨架全息效果最抓眼球的特征是边缘亮、中心透。这个视觉规律在物理上对应菲涅尔效应视线越接近掠射角视线与表面法线接近垂直反射越强视线越正对表面透射越多。我们不需要真的算菲涅尔方程用近似公式就够了fresnel pow(1.0 - saturate(dot(normalDir, viewDir)), fresnelPower)这里的dot(normalDir, viewDir)是法线和视线方向的点积正对时接近 1掠射时接近 0。用 1 减去它就得到“越边缘越大”的系数再取幂次fresnelPower控制边缘的锐利程度。fresnelPower越大亮边越窄越锐越小亮边越宽越柔。我做全息效果一般从 2.0 到 4.0 之间调2.0 偏柔和像能量场4.0 偏硬像科幻界面。为什么用这个近似而不是完整菲涅尔因为全息效果本来就是风格化的物理精确反而会让边缘光不够夸张。而且这个公式只依赖法线和视线计算量极小移动端也扛得住。这是典型的“够用就好”的工程取舍。1.3 扫描线和噪波让静态模型“活”起来光有边缘光物体看起来像个玻璃壳子缺少“正在投影”的动态感。全息效果的第二层是扫描线——沿某个轴向周期性变化的亮暗条纹模拟投影设备逐行扫描的痕迹。实现上就是用模型空间或世界空间的 Y 坐标或者 UV 的某个分量乘以一个频率取frac或sin得到周期函数再叠加到自发光颜色上。我一般用世界空间 Y 坐标因为这样扫描线是水平且均匀的不会因为模型 UV 展开方式不同而扭曲。频率参数scanlineDensity控制条纹疏密配合_Time.y让条纹缓慢移动就有了“扫描”的动感。这里有个细节扫描线不要做成纯黑纯白的硬条纹用smoothstep或者sin的平滑过渡否则在低分辨率下会出现严重的摩尔纹闪烁。噪波Noise是可选项用来给全息体加一点“信号不稳定”的颗粒感。我通常用一张小的噪声贴图采样或者用简单的哈希函数在片元里生成叠加到透明度或自发光上强度控制在 0.05 到 0.15 之间太高会显得脏。1.4 方案选型内置管线还是 URP这个项目标题没限定管线但我得说清楚。内置管线Built-in写透明 Shader 最直接Tags{QueueTransparent}加 Blend 就完事。URP 下要注意两点一是 Shader 要放在正确的 Renderer 里二是 URP 的透明物体默认不写深度混合模式也略有差异。如果你在 URP 下发现全息效果“叠不上去”或者“颜色发灰”八成是 Blend 模式或者 Color Mask 的问题。我这次以内置管线为主线讲因为它的透明渲染行为最“教科书”理解了它再迁移到 URP 只是改几个标签的事。下面所有代码和参数都基于内置管线的 Surface Shader 或手写顶点/片元 Shader我会用后者因为控制力更强。2. 核心细节解析与实操要点2.1 透明混合的公式选择与参数含义Blend 指令决定了源颜色当前 Shader 输出的颜色和目标颜色已经画在屏幕上的颜色怎么混合。全息效果最常用的是标准 Alpha 混合Blend SrcAlpha OneMinusSrcAlpha它的含义是最终颜色 源颜色 × 源Alpha 目标颜色 × (1 - 源Alpha)。源 Alpha 为 1 时完全不透明为 0 时完全透明。对全息效果来说我们希望中心区域 Alpha 低透出背景边缘 Alpha 高亮边实一点所以 Alpha 值本身就要和菲涅尔系数挂钩。还有一种加法混合Blend SrcAlpha One它会让颜色叠加变亮适合做纯发光体但全息效果如果全用加法混合物体叠在一起会过曝成一片白失去层次。我的做法是主颜色用标准 Alpha 混合边缘光和高光部分用加法混合叠加也就是写两个 Pass第一个 Pass 做基础透明体第二个 Pass 只输出边缘光并用Blend One One叠加。这样既有透明感边缘又够亮。注意两个 Pass 都要设ZWrite Off且第二个 Pass 的Tags里 Queue 要和第一个一致否则渲染顺序会乱。2.2 菲涅尔系数的计算与调试技巧前面给了公式这里补上完整的片元计算逻辑手写 HLSL 风格float3 normalDir normalize(i.worldNormal); float3 viewDir normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); float fresnel pow(1.0 - saturate(dot(normalDir, viewDir)), _FresnelPower); float3 emission _HoloColor.rgb * fresnel * _FresnelIntensity;_FresnelIntensity控制边缘光整体亮度我一般设 1.5 到 3.0。调试时有个技巧先把_FresnelPower设成 1.0_FresnelIntensity设成 1.0看边缘光的基础形态对不对再逐步调锐度和强度。很多人一上来就调一堆参数结果分不清是哪个参数导致效果不对。还有一个容易翻车的点法线方向。如果模型法线是平滑的比如球体边缘光会很均匀如果模型是硬边比如立方体每个面的边缘光会在面与面交界处断开看起来像一块块玻璃拼的。全息效果通常希望边缘连续所以要么用平滑法线要么在 Shader 里对法线做一点平滑处理比如用顶点法线插值后的结果而不是面法线。这个细节决定了全息体看起来是“一个整体”还是“一堆碎片”。2.3 扫描线的频率、方向与抗闪烁扫描线的核心代码float scanline sin((i.worldPos.y _Time.y * _ScanSpeed) * _ScanlineDensity); scanline smoothstep(-0.2, 0.2, scanline) * 0.5 0.5; emission _HoloColor.rgb * scanline * _ScanlineIntensity;_ScanlineDensity的单位是“每单位长度的周期数”世界空间下模型高度 2 米的话密度设 20 大概有 40 条条纹。_ScanSpeed控制移动速度0.5 到 2.0 比较自然。smoothstep(-0.2, 0.2, ...)是为了把正弦波变成带一点软边的条纹避免硬切导致的锯齿。抗闪烁的关键是条纹宽度要大于一个像素在世界空间的投影尺寸。如果模型离相机很远条纹密度又高就会出现摩尔纹。解决办法有两个一是根据相机距离动态降低密度在顶点着色器里算距离传过来二是给条纹加一点模糊用更宽的 smoothstep 区间。我一般用后者简单有效。2.4 透明度与深度的配合别让全息体“穿帮”全息效果虽然透明但完全透明就看不见了所以基础 Alpha 要有一个下限。我的做法是finalAlpha max(_BaseAlpha, fresnel * _FresnelAlphaScale);_BaseAlpha设 0.1 到 0.2保证中心区域还能看到一点颜色_FresnelAlphaScale让边缘 Alpha 升到 0.6 到 0.9。这样中心透、边缘实符合全息的视觉预期。深度方面ZWrite Off之后全息体不会遮挡后面的不透明物体这是对的。但如果场景里有多个全息体它们之间的排序就完全依赖渲染队列和物体中心到相机的距离。Unity 对 Transparent 队列的物体按包围盒中心到相机距离排序所以两个大小差异很大的全息体交叠时排序可能不符合直觉。我的经验是全息体之间尽量不要大面积交叠如果必须交叠给它们设不同的Queue值比如 3001、3002手动控制顺序比依赖自动排序可靠。3. 实操过程与核心环节实现3.1 从零搭建 Shader 文件的完整步骤第一步在项目里新建一个 Shader 文件命名随意但建议带 Holo 前缀方便查找。用内置管线的手写 Shader 结构Properties 块里把我们要调的参数都暴露出来Properties { _HoloColor (Holo Color, Color) (0.2, 0.8, 1.0, 1.0) _BaseAlpha (Base Alpha, Range(0, 1)) 0.15 _FresnelPower (Fresnel Power, Range(0.5, 8)) 3.0 _FresnelIntensity (Fresnel Intensity, Range(0, 5)) 2.0 _FresnelAlphaScale (Fresnel Alpha Scale, Range(0, 1)) 0.8 _ScanlineDensity (Scanline Density, Range(1, 100)) 25 _ScanSpeed (Scan Speed, Range(-5, 5)) 1.0 _ScanlineIntensity (Scanline Intensity, Range(0, 2)) 0.6 _NoiseIntensity (Noise Intensity, Range(0, 0.5)) 0.1 }参数命名要见名知意别用_Param1这种过两周自己都忘了是干嘛的。Range 的范围也要合理比如_FresnelPower给到 8 上限再大边缘就细得看不见了。第二步SubShader 的 Tags 和状态设置Tags { QueueTransparent RenderTypeTransparent IgnoreProjectorTrue } ZWrite Off Cull Back Blend SrcAlpha OneMinusSrcAlphaIgnoreProjector设 True 是因为全息体不该被投影器影响否则会出现奇怪的阴影贴图。Cull Back剔背面如果想让全息体双面可见比如一个薄片改成Cull Off但要注意双面渲染时法线方向问题背面法线要翻转否则菲涅尔会算错。第三步顶点着色器里把世界空间位置和法线传出去v2f vert (appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.worldPos mul(unity_ObjectToWorld, v.vertex).xyz; o.worldNormal UnityObjectToWorldNormal(v.normal); return o; }第四步片元着色器按第 2 节的逻辑组装算法线、视线、菲涅尔、扫描线、噪波最后输出颜色和 Alpha。完整片元大概三十行核心就是前面几段代码的组合。3.2 参数调试的实操记录我把这套 Shader 挂到一个胶囊体上实测记录一下调参过程。初始参数用默认值出来的效果边缘光偏弱、扫描线太密。先把_FresnelPower从 3.0 降到 2.2边缘光变宽全息感立刻出来了再把_ScanlineDensity从 25 降到 15条纹疏密合适_ScanSpeed设 1.2条纹缓慢上移有“扫描”的感觉。然后发现一个问题胶囊体顶部和底部的边缘光特别亮侧面反而暗。原因是胶囊体顶部是半球法线变化剧烈菲涅尔在掠射角区域占比大。这不是 bug是几何形状导致的。如果想让亮度均匀可以给菲涅尔系数乘一个基于世界 Y 的渐变遮罩或者接受这个效果——很多科幻作品里全息体顶部确实更亮像投影源在下方。_BaseAlpha我最终定在 0.12再低就几乎看不见中心颜色再高就失去透明感。_FresnelAlphaScale定 0.85边缘接近不透明但还留一点透。_NoiseIntensity定 0.08噪波只在暗部隐约可见不抢戏。3.3 双 Pass 叠加边缘光的实现前面提到主颜色和边缘光分两个 Pass。第一个 Pass 输出基础透明色含扫描线和噪波第二个 Pass 只输出菲涅尔边缘光用加法混合Pass { Blend One One ZWrite Off // 片元里只算 fresnel输出 emission * fresnel }第二个 Pass 的 Alpha 输出可以忽略加法混合不看 Alpha但为了规范还是输出 1。这样叠加后边缘光会在基础色之上再亮一层视觉上更“发光”。实测下来单 Pass 的边缘光在深色背景上够用但在亮色背景上会被冲淡双 Pass 叠加后亮背景上边缘依然清晰这是它值得多写一个 Pass 的理由。注意双 Pass 会让 draw call 翻倍移动端如果全息体很多要权衡。我的经验是主角级全息体用双 Pass场景里大量装饰性全息体用单 Pass。3.4 让全息体“呼吸”加入时间驱动的整体脉动静态全息体看久了会腻加一个整体透明度或亮度的低频脉动模拟能量不稳定float pulse 0.9 0.1 * sin(_Time.y * _PulseSpeed); emission * pulse;_PulseSpeed设 1.0 到 3.0幅度 0.1 左右太大像故障闪烁太小感觉不到。这个细节成本极低但效果提升明显属于“一行代码换质感”的典型。我一般还会让脉动和扫描线速度稍微错开比如脉动 1.3、扫描 1.0避免两者同步产生机械感。4. 常见问题与排查技巧实录4.1 全息体看不见或全黑最常见的原因是Alpha 输出为 0或者Blend 模式写错。排查顺序先确认片元返回的 Alpha 不是 0把_BaseAlpha临时设成 1 看有没有东西再确认 Blend 指令拼写正确SrcAlpha OneMinusSrcAlpha别写成SrcAlpha OneMinusSrcColor最后确认渲染队列是 Transparent如果误写成 Geometry透明混合不会生效物体要么全不透明要么被裁掉。还有一种隐蔽情况模型用了自定义法线贴图或者法线被缩放成 0导致dot(normalDir, viewDir)恒为 0菲涅尔恒为 1整个物体全亮。检查模型导入设置里的 Normals 选项。4.2 边缘光在物体内部出现亮斑这通常是法线方向错误或双面渲染时背面法线没翻转。如果用了Cull Off背面片元的法线指向物体内部dot(normalDir, viewDir)算出来是负的saturate后变 0菲涅尔变 1于是背面全亮透过正面看就是内部亮斑。解决办法是在片元里判断FRONT_FACINGUnity 提供SV_IsFrontFace背面时翻转法线。4.3 扫描线闪烁或摩尔纹前面提过条纹太密 距离太远 摩尔纹。除了降低密度和加宽 smoothstep还有一个办法是在顶点着色器里根据距离衰减扫描线强度float dist distance(_WorldSpaceCameraPos.xyz, worldPos); float scanFade saturate(1.0 - dist / _ScanFadeDistance);距离超过_ScanFadeDistance后扫描线淡出远处只保留边缘光。这样既避免闪烁又符合“远处看不清细节”的视觉逻辑。4.4 多个全息体交叠时排序错乱这是透明渲染的老大难。Unity 按物体中心距离排序两个全息体如果中心距离接近但实际交叠区域大排序会跳变。我的处理原则能不交叠就不交叠必须交叠时给它们设不同 Queue3001、3002……手动定序如果交叠关系随相机变化那就接受它——全息效果本身带一点“不真实感”轻微排序错乱反而不容易被察觉别为了完美排序把性能搭进去。4.5 常见问题速查表现象可能原因排查动作物体完全不可见Alpha 为 0 / Blend 写错 / 队列错误临时设 BaseAlpha1检查 Blend 和 Queue物体全黑无边缘光法线为零 / 视线方向算错检查模型法线打印 dot 结果内部出现亮斑双面渲染背面法线未翻转用 SV_IsFrontFace 翻转背面法线扫描线闪烁条纹过密 / 距离过远降密度、加宽 smoothstep、加距离衰减交叠排序跳变透明队列自动排序局限手动设不同 Queue或避免交叠亮背景下边缘光被冲淡单 Pass 亮度不足改用双 Pass 加法叠加边缘光4.6 几个我踩过的坑和私房技巧第一个坑忘了关阴影投射。全息体如果还往场景里投实心阴影违和感极强。在 Shader 里加FallBack Off或者显式写一个空的 ShadowCaster Pass让全息体不投影。这个细节很多人漏掉结果全息体在地上拖一条黑影子瞬间出戏。第二个技巧用顶点色控制局部强度。如果想让全息体的某个部位更亮比如“核心”区域可以在建模时刷顶点色Shader 里读顶点色乘到 emission 上。比用贴图省资源而且美术调起来直观。第三个坑HDR 颜色过曝。如果项目开了 HDR 和 Bloom全息体的边缘光很容易过曝成纯白失去颜色。解决办法是把_HoloColor的亮度控制在 1.0 以下或者用HDR颜色但配合 Bloom 阈值调高。我一般把边缘光强度峰值压在 1.5 左右给 Bloom 留余量。第四个技巧用_Time的多个分量做去同步。如果场景里有多个全息体用同一个材质它们的扫描线会完全同步看起来像复制粘贴。可以在材质属性里加一个_PhaseOffset每个实例设不同值扫描线相位错开立刻有了“各自独立运行”的感觉。用 MaterialPropertyBlock 批量设这个偏移性能开销可以忽略。这套全息 Shader 我从第一次写到现在改过七八版最大的体会是透明效果的成败在细节不在公式。菲涅尔公式就那一行但法线方向、Alpha 下限、扫描线抗闪烁、双 Pass 叠加这些“周边工作”才决定最终看起来是廉价玻璃还是像样的全息投影。参数没有标准答案同一个 Shader 挂在不同模型上表现差异很大多调、多对比、多在不同背景下看比背公式有用得多。
阅读完成 · 觉得有帮助?
咨询建站