ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Unity全息投影着色器实战:动态扫描线与边缘光效实现

Unity全息投影着色器实战:动态扫描线与边缘光效实现 你有过这种时刻吗在游戏里看到角色从地面缓缓投影上升或是在过场动画里看见一台科幻设备正在半空中构造出全息地图那种半透明的、边缘泛光的、还带着扫描线闪烁的“全息感”一下就能把氛围拉满。做这个效果核心就是一套全息投影着色器Holographic Shader。这篇文章我会从零开始把“Unity 实现全息投影着色器”这件事拆开揉碎视觉上到底需要哪些元素、Shader代码怎么组织、每个参数背后的数学原理是什么、实际工程里怎么调出好看的风格化效果。标题里提到的“动态着色”四个字其实是这套效果的灵魂全息影像不是静止的一张透明贴图它的颜色会随时间流动、边缘辉光会呼吸、扫描线会不断刷新甚至整体明暗会像信号不稳定那样抖动。这些全部依赖Shader里的时间变量_Time和一组信号处理式的数学函数共同驱动。这篇文章就是要把这个过程从“看起来很酷”变成“你也能写出来”。我尽量写得像我在自己项目里调Shader时的手记既照顾到想抄作业的新手也照顾到想看底层原理的进阶读者。场景是Unity内置渲染管线模型任意不需要额外插件一个Shader文件加一个材质球就能跑起来。1. 全息投影效果拆解先把视觉目标翻译成Shader功能清单1.1 全息影像的视觉特征是什么在做任何效果之前我习惯先看参考图把视觉特征列成清单。全息投影不是简单地把模型变透明它显著看起来像“光做出来的立体影像”所以它至少包含下面几个特征整体半透明能看到背景穿过模型模型本身像一层光幕而不是实心物体。边缘辉光物体轮廓边缘有一圈更亮的泛光尤其是轮廓朝向观察者时越接近轮廓越亮这是“光感”最重要的来源。扫描线从上到下或从下到上移动的横向条纹像电视机的扫描线或显示器的刷新栅格让人一眼识别出“这是屏幕上的东西”。动态颜色颜色不会固定在一种蓝或一种绿而是会流动、渐变、扰动像光谱在表面流动。闪烁和噪点全息影像不是完美稳定的会有低频的明暗呼吸、随机颗粒甚至偶尔丢帧这些“瑕疵”恰恰是真实感的关键。双面可见全息影像是半透明的从背面看也要能看到不能因为法线朝向就直接“单面消失”。把这些列出来之后Shader的代码结构就已经在脑子里面了一个双面渲染的半透明Pass里面同时做透明度混合、扫描线、菲涅尔边缘光、动态颜色和噪声扰动。部分效果放在顶点着色器部分放在片元着色器后面我会详细解释拆分逻辑。1.2 功能拆解与方案选型有了特征清单下一步是决定用Shader的哪种写法。Unity里有Surface Shader、Vertex/Fragment Shader还有Shader Graph可视化方案。我的选择是Vertex/Fragment Shader顶点/片元着色器原因有几点第一全息投影需要的透明度混合、双面渲染、顶点扰动都是布莱克箱式的操作V/F Shader写起来最直接Shader Graph也能做但节点连线在这种多效果叠加的场景中反而难维护。第二Surface Shader虽然是Unity的推荐封装但它更适合处理复杂光照。全息效果基本上不需要PBR光照用的更多是“自发光边缘光叠加混合”用Surface Shader的Alpha参数写虽然可以但很多底层的混合控制不如手动写Pass来得精准而且自定义的噪声扰动写在Surface函数里也容易走弯路。第三V/F Shader是Unity Shader的基础功学一遍之后不管之后去玩Shader Graph还是去写URP都能无缝迁移。对于渲染管线这篇文章默认使用内置渲染管线Built-in Render Pipeline最后单开一节讲怎么改到URP下使用。光效部分不依赖额外贴图完全靠程序化生成噪声和波形这样不管是谁拿到这套代码不用去额外找噪声贴图就能直接跑。2. Shader框架与渲染状态配置2.1 SubShader Tags 与 Blend 设置的逻辑Shader的Unity属性结构和普通Pass不一样的地方都在开头这句话。第一个坑RenderType和Queue必须同时设为Transparent并且Shader中的渲染队列直接决定了物体绘制顺序。全息物体是半透明的必须队列排在不透明物体之后才不会被完全遮挡。如果队列设置错误比如默认的Geometry透明物体就可能变成“能看见后面的物体但被自己前面不透明的东西挡住”的奇怪状态甚至某些半透明模型会直接渲染不出来。第二个重要设置是Blend。全息投影本质上是“光叠加”所以混合模式我用的是加法混合为主Blend SrcAlpha One参数含义是源颜色片元颜色乘以其Alpha值目标颜色背景颜色直接加到自己身上最终结果就是“发光并且照亮周围”。全息投影的图像在看的时候很亮加色混合能产生那种明亮、发光的感觉。如果想让全息物体像毛玻璃一样整体较淡并保留背景细节也可以换成Blend SrcAlpha OneMinusSrcAlpha这对半透明效果更通用。我会在第4节给你两种方案的调节思路。第三个设置是Cull Off。全息效果必须是双面可见的物体的背面也要显示这样在摆姿势或从背面看时才不会穿帮。但要注意Cull Off之后背面法线的方向和正面是相反的如果直接做菲涅尔边缘光背面的算法结果可能会异常。我的处理方案是在计算菲涅尔时用点积的绝对值这样正面背面都能得到正确的边缘宽度这个技巧后面会重点说。第四个设置ZWrite Off也是半透明的铁律。如果保持深度写入开启透明物体之间叠加会出现很奇怪的遮挡某些地方看起来像是被切了一刀。关闭深度写入之后半透明物体不会影响彼此的深度重叠区域会进行混合表现出统一的发光感。代价是半透明物体的前后排序可能不准确但在全息效果的“发光体”场景里这个代价可以接受。2.2 Properties参数设计把效果参数化刚开始写这套Shader的时候我犯过一个错误把所有数值都写死结果每次调效果都要改代码重进Play模式非常痛苦。后来我养成了好习惯把所有需要微调的量全部暴露成Properties。这个习惯对于风格化Shader尤其重要同一个Shader参数调得不同就能做出“军用全息”偏绿、条纹锐利和“民用科幻”偏蓝、辉光大两种风格。Properties并不是简单地把一堆Range堆在那里参数设计背后有三个原则每个参数必须对应一个明确的视觉特征。比如_ScanLineCount对应扫描线的粗细_FresnelPower对应边缘光的衰减速度而不是让用户改一个Shader内部不知道干什么的浮点数。每个参数必须有合理的调节范围。扫描线数量在1到200之间如果上限给到10000数值一调大就会变成密集噪点没有实际意义。颜色类参数用Color强度类参数用Range这样在Inspector面板上调节直观。这套参数我设计了足了16个包括主颜色、动态颜色深度、颜色变化速度、扫描线数量、扫描速度、扫描线强度、刷新带宽度、菲涅尔强度、菲涅尔范围、噪声强度、闪烁强度、闪烁速度、顶点扰动幅度、透明度等等。这些参数拆开来看都很好理解组合起来就是无限种全息风格。2.3 顶点着色器位置扰动与坐标传递很多初学者忽略顶点着色器把所有逻辑堆在片元着色器但全息效果在顶点阶段做点位置扰动性价比极高。为什么这么说因为片元着色器是逐像素执行的执行频率远高于顶点任何计算放在片元阶段都会成倍消耗性能。而顶点扰动只需要在每个顶点上计算一次就能模拟出“全息影像不稳定”的晃动感尤其适合在低端手机和移动平台的项目里用。顶点扰动的核心思路是让顶点的位置沿着法线方向偏移一小段距离偏移量随时间变化让模型表面像水面一样呼吸。具体做法是先用顶点的本地位置和_Time.y构造一个噪声值然后把法线方向乘以噪声和扰动幅度再加到顶点位置上最后做MVP矩阵变换。这里有个小坑在做顶点扰动后模型的法线方向如果用来计算菲涅尔可能会和后处理阶段的光效不太协调。我的对策是在片元阶段传入的顶点法线使用原始法线而不是扰动后的法线因为全息效果的菲涅尔只是“视觉边缘”不需要物理精确的法线参与。顶点着色器除了扰动位置还要把世界坐标、法线方向、视线方向都传给片元。常用的方法是用一个结构体往里面的语义变量塞数据。这个数据传递过程是Shader从顶点到片元的桥梁后续的扫描线坐标计算、菲涅尔边缘光计算全部依赖这些变量我在第4节完整代码里会仔仔细细标出每个变量的作用。3. 动态着色核心从算法到代码3.1 动态颜色计算让全息影像“活”起来“动态着色”这个词最核心的实现就是让颜色随时间变化。最直接的做法是三个正弦函数分别叠加RGB通道然后互相错开相位。为什么用正弦因为正弦波是周期函数且在一段时间内的值连续光滑非常适合做颜色过渡不会出现突然跳变。代码可以写成这样float3 DynamicColor(float t) { float r 0.5 0.5 * sin(t * 1.0); float g 0.5 0.5 * sin(t * 1.0 2.094); float b 0.5 0.5 * sin(t * 1.0 4.188); return float3(r, g, b); }这里2.094就是2 * PI / 3让三个通道的相位差刚好是120度。这样RGB会分别交替升降产生彩虹色瀑布一样的感觉。但在实际全息效果中纯RGB流动可能会太花哨所以我把这个动态颜色当作“目标颜色”用_MainColor和_SecondaryColor作为两个基准色把动态颜色当作插值系数用lerp把两个基准色混合起来。这样调参时想得到蓝紫色全息就把两个基准色都选蓝紫色系想让颜色流光溢彩就把两个基准色选互补色非常灵活。另外要不要在颜色上加上HSV转换呢我不建议这里做因为动态颜色已经能覆盖绝大多数需求引入HSV会增加几个sin/cos的运算对移动端GPU没那么友好。移动端Shader里宁可多做几次lerp也要少做几次sin。3.2 扫描线与刷新带效果扫描线是全息效果的“身份证”加了扫描线一眼就能认出来是电子影像。扫描线的实现方式很多我最常用的是基于世界坐标Y轴做周期条纹。原理是把Y坐标映射成一个周期性函数然后通过系数控制条纹的疏密和移动速度。细暗条纹的写法float scanLine sin(worldPos.y * _ScanLineCount * 3.14159 _Time.y * _ScanLineSpeed); scanLine scanLine * 0.5 0.5;worldPos.y乘以一个较大的频率系数再叠加时间就能得到一条条横向条纹。sin结果在-1到1之间映射到0到1之后条纹就会在亮暗之间循环。这里频率可以理解为条纹数量时间系数控制移动方向速度为正时条纹向上移动速度为负时向下移动。真正好看的扫描线不是只有暗条纹还要有一条明亮的高亮度刷新带。全息影像里的显示设备往往有一条从上到下扫描的亮线像老电视刷屏一样。我实现的方式是取frac(worldPos.y - _Time.y * speed)这样会得到一个在0到1之间循环的值然后通过smoothstep(0.0, 0.15, v) * (1.0 - smoothstep(0.15, 0.35, v))形成一个只有一小段亮起的“脉冲”再叠加到主颜色上。这条亮带不仅让画面更有层次也强化了“动态刷新”的颗粒。切记worldPos.y是从世界空间取的而不是模型本地坐标。如果你的模型很小世界Y轴跨度过大按世界坐标取会导致扫描线间距不均匀。此时可以改用TransformObjectToWorld把模型顶点坐标转换后再计算或者直接在顶点着色器里提前做好worldPos的传递。如果模型缩放严重或使用了不标准轴向可能出现扫描线倾斜的问题后来我干脆改成了只取Y轴这样做对大多数标准摆放的模型都没问题。3.3 菲涅尔边缘光与透明度菲涅尔Fresnel效果是模拟“半透明物质边缘更亮”的核心。简单的理解是你正对着玻璃看中间很透明但当你斜着看玻璃边缘时投影反射明显变多。全息投影也一样物体的轮廓边缘如果有一圈亮光会显得更像“一个立体光幕”。CG里标准菲涅尔公式是pow(1 - dot(N, V), power)其中N是法线方向V是视线方向dot(N, V)表示法线和视线的夹角余弦。当法线指向视线方向时点积接近1边缘系数就是0当法线垂直于视线时点积接近0边缘系数就是1亮度最高。这就是“边缘泛光”的数学解释。但这里有个双面渲染的坑。Cull Off开启后背面的法线是反的直接和视线点积会产生错误结果甚至可能让整个背面都是亮的。我的技巧是取绝对值float fresnel pow(1.0 - abs(dot(normalize(i.normal), normalize(i.viewDir))), _FresnelPower);abs的作用是把法线朝向造成的“一边亮一边暗”变成“两边对称”因为绝对值把点积的结果映射到0到1之间无论是正面看还是背面看都能得到一圈合理的边缘高光。虽然物理上不完全正确但对于全息这种视觉幻觉谁在乎物理呢只要好看就行。透明度也要和菲涅尔联动。如果整片全息物体的透明度是均匀的看起来像贴纸。更好的做法是中间的透明度低一点边缘透明度高一点但颜色要亮。因为边缘亮的区域叠加到自己身上再用加法混合就能形成类似半透明发光体的体积感。透明度公式我给的是_Opacity * (1.0 - _TransparentCore * fresnel)就是在物体正面朝着玩家时让中心更透让边缘保持一种光裹感。3.4 噪声、闪烁与信号不稳定感全息投影的魅力在于“既真实又虚幻”而“虚幻”很大程度上来自不稳定性。我在这个Shader里加入了三层信号干扰第一层是随机颗粒噪声。我用程序化哈希函数生成float HashNoise(float2 p) { return frac(sin(dot(p, float2(12.9898, 78.233))) * 43758.5453); }原理是先用两个坐标分量与一个固定向量做点积得到一个标量再用sin把它映射到-1到1的波动区间最后乘以一个大数取小数部分。这样只要输入坐标稍微变一点输出值就会剧烈变化看起来就像随机颗粒。把它叠加到颜色和透明度上就制造出了“全息影像表面有信号杂质”。第二层是低频闪烁。全息投影的显示亮度会有一种周期性呼吸感用一个大周期的正弦乘上强度值就行float flicker 1.0 _FlickerIntensity * sin(_Time.y * _FlickerSpeed noiseValue * 6.2831);我会把噪声值也融进闪烁公式里让闪烁频率在不同区域不完全一致这样比单一直通的正弦更自然。如果你在项目里发现整个模型都在一起闪那可能是因为闪烁函数里没有加入位置相关项这种“全屏同时闪”比较假尽量在计算里混入一点世界坐标或噪声坐标参数来破周期。第三层是顶点位置扰动。这一层在第2.3节讲过本质上是对“影像漂移”的模拟让模型的边缘轮廓有一点点不稳定的晃动。相比于闪烁顶点扰动的频率应该更低幅度也要很小一般0.001到0.01之间就够了太大会让模型破面穿帮。4. 完整Shader代码与Unity实操步骤4.1 完整Shader代码把上面所有算法拼在一起给一份能直接用的Shader文件。我按CG格式写适配内置渲染管线、Unity 2019及之后的版本都没问题。Shader Custom/HolographicShader { Properties { _MainColor (Main Color, Color) (0.3, 0.8, 1.0, 1.0) _SecondaryColor (Secondary Color, Color) (1.0, 0.3, 0.7, 1.0) _ColorSpeed (Color Speed, Range(0.0, 5.0)) 1.5 _Opacity (Opacity, Range(0.0, 1.0)) 0.65 _ScanLineCount (Scan Line Count, Range(10, 200)) 80 _ScanLineSpeed (Scan Line Speed, Range(-5.0, 5.0)) 2.0 _ScanLineIntensity (Scan Line Intensity, Range(0.0, 1.0)) 0.35 _RefreshBandSize (Refresh Band Size, Range(0.0, 0.5)) 0.12 _FresnelPower (Fresnel Power, Range(0.5, 8.0)) 2.5 _FresnelIntensity (Fresnel Intensity, Range(0.0, 3.0)) 1.25 _NoiseAmount (Noise Amount, Range(0.0, 1.0)) 0.18 _FlickerSpeed (Flicker Speed, Range(0.0, 10.0)) 3.0 _FlickerIntensity (Flicker Intensity, Range(0.0, 1.0)) 0.12 _GlitchOffset (Glitch Offset, Range(0.0, 0.05)) 0.005 } SubShader { Tags { Queue Transparent RenderType Transparent IgnoreProjector True } Blend SrcAlpha One Cull Off ZWrite Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc fixed4 _MainColor; fixed4 _SecondaryColor; float _ColorSpeed; float _Opacity; float _ScanLineCount; float _ScanLineSpeed; float _ScanLineIntensity; float _RefreshBandSize; float _FresnelPower; float _FresnelIntensity; float _NoiseAmount; float _FlickerSpeed; float _FlickerIntensity; float _GlitchOffset; struct appdata { float4 vertex : POSITION; float3 normal : NORMAL; float2 uv : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float3 worldPos : TEXCOORD0; float3 normal : TEXCOORD1; float3 viewDir : TEXCOORD2; float3 localPos : TEXCOORD3; }; // 哈希噪声 float HashNoise(float2 p) { return frac(sin(dot(p, float2(12.9898, 78.233))) * 43758.5453); } // 动态颜色 float3 DynamicColor(float t) { float r 0.5 0.5 * sin(t); float g 0.5 0.5 * sin(t 2.094); float b 0.5 0.5 * sin(t 4.188); return float3(r, g, b); } v2f vert(appdata v) { v2f o; // 顶点扰动沿法线方向加入噪声偏移 float noiseVal HashNoise(v.vertex.xz * 10.0 _Time.y * 0.15); float offset (noiseVal - 0.5) * _GlitchOffset * 0.1; float3 pos v.vertex.xyz v.normal * offset; o.pos UnityObjectToClipPos(float4(pos, 1.0)); o.worldPos mul(unity_ObjectToWorld, v.vertex).xyz; o.normal UnityObjectToWorldNormal(v.normal); o.viewDir normalize(_WorldSpaceCameraPos.xyz - o.worldPos); o.localPos v.vertex.xyz; return o; } fixed4 frag(v2f i) : SV_Target { float3 normalDir normalize(i.normal); float3 viewDir normalize(i.viewDir); // 1. 动态颜色 float3 baseColor lerp(_MainColor.rgb, _SecondaryColor.rgb, DynamicColor(_Time.y * _ColorSpeed).r); float3 dynamicColor DynamicColor(_Time.y * _ColorSpeed); float3 finalColor lerp(baseColor, dynamicColor, 0.35); // 2. 扫描线 float scanY i.localPos.y * _ScanLineCount _Time.y * _ScanLineSpeed; float scanLine sin(scanY * 3.14159) * 0.5 0.5; float darkMask lerp(1.0, 0.3, scanLine * _ScanLineIntensity); // 3. 刷新亮带 float refreshValue frac(i.localPos.y - _Time.y * _ScanLineSpeed * 0.4); float refreshBand smoothstep(0.0, _RefreshBandSize, refreshValue) * (1.0 - smoothstep(_RefreshBandSize, _RefreshBandSize * 2.0, refreshValue)); // 4. 菲涅尔边缘光 float fresnel pow(1.0 - abs(dot(normalDir, viewDir)), _FresnelPower); float edgeGlow fresnel * _FresnelIntensity; // 5. 噪声颗粒 float noise HashNoise(i.worldPos.xy * 0.2 _Time.y * 4.0); float noiseMask lerp(1.0, noise, _NoiseAmount); // 6. 闪烁 float flickerNoise HashNoise(i.worldPos.xy * 1.3 _Time.y * 0.1); float flicker 1.0 _FlickerIntensity * sin(_Time.y * _FlickerSpeed flickerNoise * 6.2831); // 组合输出 finalColor * darkMask; finalColor lerp(finalColor, finalColor * refreshBand * 1.5, saturate(refreshBand * 2.0)); finalColor edgeGlow * _MainColor.rgb; finalColor * noiseMask * flicker; // 透明度 float alpha _Opacity * (0.4 0.6 * edgeGlow); return fixed4(finalColor, alpha); } ENDCG } } FallBack Off }4.2 Unity中创建材质并调参在Unity里实验十分简单。右键Project窗口选择Create - Shader - Custom - HolographicShader系统会自动创建Shader文件。如果你是从文本编辑器粘贴直接在Project窗口里Create - Shader - Standard Surface Shader然后把内容替换成上面的代码文件名改成Shader内部的名称一致就行。创建完后再在Project窗口右键Create - Material把Shader下拉菜单切到Custom/HolographicShader。接着把材质拖到场景模型上。这时模型应该就能看到半透明全息效果了。但是注意如果你直接把材质拖到默认地板上效果会被深度缓冲区盖住所以要保证材质所附的模型本身是网格模型比如Unity内置的Capsule、Cube或者你导入的FBX角色。场景背景最好是纯色。如果你用的是Standard Asset或URP下的模型需要先确认渲染管线下面第5节有解决方法。调参的时候我的习惯是先把_FresnelIntensity拉高到2.0让边缘先出来再调_FresnelPower控制边缘宽度然后调_ScanLineIntensity控制暗条纹的深浅最后再回来调_Opacity。颜色部分先选中两个基准色再把_ColorSpeed调到1到2观察流动是否自然。4.3 参数调节技巧与风格化预设同一个Shader通过参数可以做成好几种风格。我在实际项目中摸索出了三套比较常用的预设参数分享出来供大家参考科幻蓝调风格主色(0.2, 0.6, 1.0)副色(0.0, 0.9, 1.0)菲涅尔强度1.5菲涅尔Power 2.5扫描线速度2.0噪声0.15闪烁0.08。适合用在UI全息地图或者角色投影上整体干净明亮。信号紊乱风格主色(0.0, 1.0, 0.3)副色(0.8, 1.0, 0.0)闪烁强度0.35闪烁速度7.0噪声0.4顶点扰动0.02扫描线速度-3。这种风格强调信号不稳定感适合任务提示、即将消失的幻影、入侵特效。暗黑低语风格主色(0.4, 0.0, 0.8)副色(0.1, 0.0, 0.3)菲涅尔强度0.8菲涅尔Power 4.0透明度0.5扫描线强度0.15闪烁强度0.2。这种比较阴森适合游戏里的幽灵或封印效果。调参的经验法则如果效果太“塑料感”一般是菲涅尔Power太高或边缘光太弱如果看起来太亮白把加法混合改成普通混合或调低_FresnelIntensity扫描线太生硬就调低_ScanLineIntensity或者把sin函数换成smoothstep软化边缘。5. 常见问题与排错实录5.1 全息物体渲染不出或全透明这是最常遇到的问题我踩过几次坑之后总结下来无非三个原因一是渲染队列或混合模式不对。先在Inspector里检查材质的Shader是否真的已经切到Custom/HolographicShader然后确认Shader里Queue确实是TransparentBlend不是Off。如果连默认都不显示看看物体的Mesh Renderer组件是不是把材质隐藏了。二是透明度计算成了0。_Opacity默认是0.65但如果你调节了缩放或传入了不合理的参数可能让某个通道结果为0导致完全透明。简单调试方法把return fixed4(finalColor, 1.0);临时改成完全不透看物体是否出现。如果出现了就说明问题出在透明度计算回头检查_Opacity是否太小或edgeGlow是否被压到0。三是深度和遮挡问题。当前场景如果有很多不透明物体半透明物体默认不会被深度写入保护可能会出现被遮挡就完全看不见的情况。这其实是正确的半透明行为如果你想让它始终可见可以把ZWrite Off改成ZWrite On但要注意这样会破坏透明物体之间正确的前后混合排序。更好的做法是把渲染队列继续保持Transparent并手动调整模型的位置让全息物体尽量放在不透明物体前面。5.2 颜色过亮或过暗颜色过亮是加法混合的常见副作用尤其当_FresnelIntensity设置得过大时物体在白色背景下会完全过曝。解决办法有三个方向把Blend SrcAlpha One改成Blend SrcAlpha OneMinusSrcAlpha。这样不再是增加发光而是普通的半透明混合物体就不会过曝。调低_FresnelIntensity和_Opacity不要让边缘光超过1.0。在片元着色器的输出前加saturate()确保颜色值在0到1之间。正常说CG输出会自动截断但手动加上更保险。颜色过暗则大概率是_Opacity太低或乘法过多。检查你的finalColor * noiseMask * flicker如果_NoiseAmount过高噪声的lerp(1.0, noise, value)会不断把颜色乘以小于1的系数整体就会偏暗。可以把噪声模式从乘法改成加法叠加但乘法更符合“遮挡杂质”的效果所以我更推荐把_NoiseAmount控制在0.2以内。5.3 在URP或HDRP下失效这个问题主要是渲染管线导致的。这篇文章里的Shader是内置渲染管线的写法如果你把Unity项目切换成URP或HDRP打开Shader会呈现粉红色或者编译报错。原因是URP下看不到内置管线的UnityCG.cginc标签也要换成UniversalPipeline。解决方案有几个一个是在URP下改兼容版本。主要改动是把#include UnityCG.cginc换成#include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl把UnityObjectToClipPos换成TransformObjectToHClip法线变换换成TransformObjectToWorldNormal。Tags里加RenderPipeline UniversalPipeline。其他的混合、队列、Cull这些核心逻辑完全一样。另一种方案是直接用Shader Graph搭一套等价节点视觉思路完全一致只是从代码变成连线。如果你非得在URP下用代码建议按前者改。平时请在Project Settings - Graphics里确认项目渲染管线默认是Built-in用这套Shader最省心。5.4 扫描线方向、闪烁与顶点扰动不生效扫描线不移动或者方向不对先看_ScanLineSpeed的正负号和具体数值。如果数值接近0速度自然很慢看起来像静止。如果方向反了把数值取负即可。扫描线方向的视觉判断是从下往上还是从上往下取决于你的模型坐标Y轴向。闪烁不生效可能是因为_FlickerIntensity太小。这个参数默认0.12如果屏幕太小或者光线太亮肉眼可能感知不到。我排查的时候会把强度临时调到0.8确认有效后再慢慢调回来。顶点扰动不生效大概率是_GlitchOffset数值太小。它影响的是顶点沿法线方向的位置偏移在模型顶点很少比如Cube的情况下本身就很难看出效果建议用带较多顶点的模型来对比测试。另一个原因是扰动公式中乘了0.1把整体幅度缩小了10倍如果你想要明显一些可以直接删掉这个系数或者把参数范围调大。6. 进阶拓展一套全息投影展示流程既然Shader已经实现不妨想得更远一点。全息投影效果在项目中通常不只是给一个模型赋材质就完事了还需要配套的布景和相机设置。我常用的一套展示流程使用效果非常好分享给大家。首先准备一个黑色或深蓝色的背景。全息效果本质是“发光”在纯黑背景下最亮眼。如果你在天花板很高的场景里可以把相机背景颜色设为深色或放一块深色幕布。将模型放在一个平台上平台用半反半透材质比如透明玻璃或深色半透明塑料让模型的光能透过平台反射到下方。更极致的做法是放一个底面镜面反射Plane用长条响应反射但那样性能高一点简单的半透明平台也不错。相机设置上开启Allow HDR后期加Bloom泛光效果。全息投影最需要的后处理其实是Bloom它能让边缘辉光扩散到周围空气中形成真正的“光感”而不是死板的贴图边缘。在Unity的Post Processing Stack里加一个Bloom效果Threshold调到0.6左右Intensity根据场景亮度来调整大约1到3之间。绝对的万能组合全息Shader 黑色背景 Bloom 效果下线。模型方面网格不要过度密集全息投影是一个“粗糙中带细节”的视觉如果你的模型面数太高扫描线和噪声会变得非常细碎反而不好看。我会在SkinnedMeshRenderer模型上使用动画让角色做一个缓慢的旋转或待机动作全息影像本身带有不稳定晃动配合轻量动画会比纯静态模型更显真实。如果要做“从地面浮出”的效果可以在代码里给模型加一个从底部到顶部的裁切面动画动态调整裁剪范围随着_Time.y逐渐把下端往上收看起来就像是影像从设备中一层层扫描生成出来的。最后别忘了性能。这套Shader包含一个Pass、两个sin、一个哈希噪声和一个点积运算量在移动平台属于中低负载。但如果你场景里挂着十个全息物体每个都启用双面渲染加上后处理Bloom帧率还是会被拖慢。建议把扫描线和噪声函数里的浮点精度从highp改成mediump在移动端能省下不少性能。或者你也可以考虑让距离较远的全息物体使用LOD版本剪掉精度部分。我个人最大的体会是全息效果这类“风格化Shader”最重要的不是技术本身而是对视觉语言的拆解。只要你能把“全息感”拆解成半透明、边缘光、扫描线、动态颜色、闪烁噪点这几个信号然后用Shader里的函数把每个信号表达出来就不愁做出别人眼中的“酷炫特效”。参数化做得越细后面复用时的爽感越强。你可以把这篇代码当成起点往里加自己的特征比如扫描线变成对角波形、动态颜色改成特定逻辑控制、边缘光换成随距离衰减的形态都会碰撞出完全不同的效果。项目里跑起来之后记得多截图对比找到一套适合自己项目的参数方案存下来。后面换项目的时候直接把这套Shader和参数预设丢过去简单调调颜色就能用这才是“从零写一次受用无数回”的正确打开方式。
返回列表