尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Unity黑底视频去边抠像Shader:从原理到多平台实战

Unity黑底视频去边抠像Shader:从原理到多平台实战 把黑底视频变成透明背景这个需求在Unity项目里太常见了。UI上想放一段带炫光特效的视频美术给的却是一段黑色背景的H.264直接铺上去就是一块黑方块用Blend模式强行滤色吧画面又整体发灰发亮浅色界面上特别难看。最后我决定自己写Shader来处理把黑色区域按亮度抠成透明同时解决边缘那圈“脏边”。这套方案用VideoPlayer输出RenderTexture再交给自定义Shader渲染实测下来在PC、Android和iOS上都能稳定跑今天把完整的实现思路和踩过的坑都写出来。1. 为什么不能用透明视频或直接滤色了事先说结论去黑底这件事很多人的第一反应是“直接找美术要透明背景视频”或者“用Additive混合去黑”但这两条路在实际项目中都走不通。透明背景视频的问题在编码格式上。Uniyt的VideoPlayer底层依赖各平台的系统解码器对带Alpha通道的视频格式支持非常有限。VP9、AV1这些编码虽然能带Alpha但Unity移动端的VideoPlayer基本不认PC上想播还得额外装解码插件。就算格式支持带Alpha的ProRes或者无压缩AVI体积大到离谱一个几秒钟的特效素材动辄几百MB用户下载一次包体直接劝退。所以美术资源管线里黑底H.264才是主流因为它编码效率高、兼容性好、体积也正常。滤色混合Blend One One的坑更隐蔽。这种模式下黑色像素因为RGB接近0加色混合后几乎不改变背景颜色看起来确实“消失”了但问题在于视频里所有亮色都会和背景做加法混色。比如一段火焰特效火焰周围的橙色烟雾原本饱和度就不高叠加到浅色UI上会直接变成一片亮白色暗部细节也会被冲淡整体画面像蒙了一层半透明的白纱。在深色背景上还能勉强用一旦切到浅色界面效果几乎是灾难级的。所以最终可行的路线只有一条用Shader在片段着色器里把黑色像素的Alpha压到0把非黑色区域的Alpha保持为1再通过Blend SrcAlpha OneMinusSrcAlpha正常混合。思路听起来很简单但真正实现起来第一版Shader通常会出现两个让人头疼的问题画面边缘有明显的锯齿感以及一圈去不掉的黑边。这两个问题的根源完全不在一处得分开解决。2. 黑边从哪来色度采样、alpha过渡和边缘映射的逻辑第一版去黑底Shader很多人会写成这样在片段着色器里取像素亮度如果亮度小于某个固定阈值直接discard或者把Alpha设为0。试完会发现视频边缘全是狗牙而且物体轮廓上总包着一圈黑灰色的边。2.1 罪魁祸首之一是视频压缩的色度子采样H.264编码为了压缩体积会把图像从RGB转到YUV然后对色度通道U和V做大幅度降采样最常见的是4:2:0也就是每四个亮度像素才共享一组色度信息。这就导致原本应该在亮色边缘外侧的纯黑色像素解码之后色度通道混入了周围亮色的信息显示出来的颜色不是(0,0,0)而是类似(8,12,20)这种暗灰蓝色。如果你在用固定阈值判断比如亮度小于0.05才判为透明那这些暗灰像素会被算作“不够黑”被当成不透明显示出来在视频里的人物或特效边缘形成一圈脏兮兮的黑边。这种边在原始素材里其实并不明显但屏幕上一旦放大两倍三倍或者UI背景比较亮时立刻就能看出来。2.2 罪魁祸首之二是半透明像素的颜色残留就算你把阈值范围调大把那些暗灰像素也扣成透明还会遇到另一个问题边缘处很多像素本身就不是纯黑底而是黑色背景和亮色前景的混合过渡。这类像素的RGB值很低但又不是0Alpha被算出来后是一个半透明值。问题在于半透明混合时这些像素的RGB颜色仍然会参与到最终绘制中如果RGB里还残留着黑色成分混合到背景上就是一条深色描边。换句话说只控制Alpha是不够的必须同时处理半透明区域内的颜色数值把残留的暗色成分提亮让边缘像素看起来是“干净的半透明发光”而不是“半透明的黑边”。2.3 正确的解决思路亮度平滑过渡加边缘提亮要同时解决锯齿和黑边Shader需要做的事分成两步第一步用smoothstep取代if硬切。给像素亮度设置一个低阈值和高阈值亮度低于低阈值时Alpha为0高于高阈值时Alpha为1中间用平滑曲线过渡。这样边缘的透明度变化是渐进的不会出现生硬的锯齿。第二步对半透明区域的RGB做“提亮”处理。原理很简单如果这个像素的Alpha是0.6说明它只有60%被前景占据剩下40%本来是黑底。混合到背景上时那40%的黑色会被真正画出来形成暗边。所以我们要把这40%的黑色补偿掉让RGB整体向亮的方向偏移偏移量大约等于(1 - Alpha)乘上一个可控系数。这样即使半透明像素盖在浅色背景上看起来也只是轻微泛白而不是发黑。这两个点搞明白之后Shader写起来就很清晰了下面是完整可用的实现。3. 可直接复用的去黑底Shader完整实现与挂载流程这个方案的完整链路是VideoPlayer输出到RenderTextureRenderTexture喂给自定义材质材质挂在RawImage或者3D物体的MeshRenderer上。下面把Shader和C#操作分开讲。3.1 完整Shader代码适用于RawImage和MeshRendererShader Custom/VideoCutoutTransparent { Properties { _MainTex (Video Texture, 2D) black {} _Cutoff (Alpha Cutoff, Range(0, 0.5)) 0.18 _Smoothness (Edge Smoothness, Range(0.001, 1)) 0.25 _Boost (Edge Bright Boost, Range(0, 1)) 0.4 } SubShader { Tags { QueueTransparent RenderTypeTransparent IgnoreProjectorTrue } Blend SrcAlpha OneMinusSrcAlpha ZWrite Off Cull Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; float _Cutoff; float _Smoothness; float _Boost; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col tex2D(_MainTex, i.uv); // 亮度计算这里用标准感知亮度公式对绿色通道权重大 float luma dot(col.rgb, fixed3(0.299, 0.587, 0.114)); // 平滑过渡黑底区域Alpha趋近0前景区域Alpha趋近1 float alpha smoothstep(_Cutoff, _Cutoff _Smoothness, luma); // 边缘提亮半透明像素残留的黑色分量被补偿掉 // 越接近透明的像素提亮幅度越大完全透明的像素颜色无所谓 col.rgb (1.0 - alpha) * _Boost; // 防止提亮后亮度高于1导致过曝可以做一个简单clamp col.rgb saturate(col.rgb); col.a alpha; return col; } ENDCG } } Fallback Off }这段Shader有一个很实用的设计没有用if判断所有过渡都由smoothstep完成边缘会有非常柔和的渐变。_Boost参数在后续调优中非常重要它直接影响黑边能不能彻底消除。如果你的视频素材是烟雾、火焰这类加色混合的特效可以把亮度公式改成取RGB三个通道的最大值也就是float luma max(col.r, max(col.g, col.b));。这样对纯黑背景的判断会更准确因为这类素材的暗部往往带一点蓝紫色用感知亮度公式可能会把一些不该抠掉的暗色误判成透明。3.2 C#端VideoPlayer结合RenderTexture的播放控制Shader只是第一步还要把VideoPlayer和材质正确串起来。操作步骤如下在场景中创建一个RawImage把它的Material替换成上面这个Shader创建的材质。创建RenderTexture分辨率建议和视频源一致比如1920x1080。如果RT比视频分辨率小很多输出会模糊比视频大则浪费性能。在VideoPlayer组件上把Target Texture指向这张RenderTexture。创建材质把_MainTex赋值为这张RenderTexture。调用VideoPlayer.Prepare()在prepareCompleted的回调里再调用Play()这样可以避免视频画面还没准备好就黑屏闪一下。C#代码很简单我贴关键部分using UnityEngine; using UnityEngine.Video; public class VideoWithCutout : MonoBehaviour { public VideoPlayer videoPlayer; public RenderTexture renderTexture; public Material targetMaterial; private void Start() { if (videoPlayer null) videoPlayer GetComponentVideoPlayer(); videoPlayer.targetTexture renderTexture; videoPlayer.isLooping true; videoPlayer.playOnAwake false; videoPlayer.prepareCompleted OnVideoPrepared; videoPlayer.Prepare(); } private void OnVideoPrepared(VideoPlayer vp) { targetMaterial.SetTexture(_MainTex, renderTexture); vp.Play(); } private void OnDestroy() { videoPlayer.prepareCompleted - OnVideoPrepared; } }有一点要注意RenderTexture的宽高比必须和视频一致否则视频画面会被拉伸变形。建议从VideoPlayer获取视频原始分辨率后动态调整RT尺寸而不是在Inspector里写死。如果是3D场景里的面板播放视频把RawImage换成带MeshRenderer的Quad也是同样的流程Shader不需要修改VideoPlayer的输出链路完全一样。我实际项目里就这么干过把这段视频放在一个ParticleSystem旁边当全息投影效果比UI显示更灵活还能配合后处理做发光。4. 参数调优实战从灰蒙蒙边缘到干净透明Shader挂上去之后素材质量不同参数需要针对性调整。我第一次拿到的视频素材是黑色背景上有一团蓝色火焰和白色Logo直接用默认参数跑效果是Logo边缘有轻微锯齿火焰外围泛着一层灰蒙蒙的暗色。接下来是我怎么调参数的经验。4.1 _Cutoff决定“多黑才算透明”_Cutoff的值代表透明判定的起点通常设在0.05到0.25之间。值设得太低比如0.05那些被压缩过的暗灰像素亮度在0.1左右不会被扣掉黑底残留值设得太高比如0.3一些暗部细节比如深色头发、黑色衣服的边缘会被误伤画面像被啃过一样。一个比较实用的调试方法是把Shader暂时改成“显示Alpha”模式在片段着色器最后返回fixed4(alpha, alpha, alpha, 1)。这样画面会变成黑白灰度图能很直观地看到哪些区域被识别为透明哪些不是。然后一边播放视频的暗色场景一边调_Cutoff直到背景区域变成纯黑、人物或Logo区域保持纯白。4.2 _Smoothness边缘硬不硬就看它_Smoothness控制从透明到不透明的过渡范围。值越小过渡越陡峭边缘越锐利但AI换脸式的锯齿就越明显值越大边缘越柔和但也容易让细小的线条变淡甚至完全消失。针对粒子和火焰这类没有清晰轮廓的素材我会把_Smoothness调得稍大一些比如0.3到0.5让颗粒的边缘有自然的柔化过渡。针对Logo、字幕这类需要清晰轮廓的内容_Smoothness建议控制在0.1到0.2否则笔画细节会被削掉。4.3 _Boost黑边能不能根除就看这一项_Boost的取值范围我在0.3到0.6之间来回试过。当素材边缘有比较明显的压缩脏边时_Boost拉到0.5左右能有效把边缘提亮但当_Boost过高比如超过0.7视频中的半透明烟雾区域会被整体冲得太亮烟雾失去了原本的浓淡层次。一个判断标准播放到包含“烟雾高亮Logo”的画面如果烟雾边缘看起来泛白但内部层次还在_Boost就是合适的如果整个烟雾区域变成了一片均匀的光团说明_Boost太高。注意_Boost的本质是牺牲暗部饱和度换取边缘干净所以它只作用于半透明区域完全透明区域不受影响。4.4 不同素材类型的调参参考我整理了三种常见素材类型的起始参数你可以直接拿来当基准再微调。素材类型_Cutoff_Smoothness_Boost备注Logo/文字/UI图标0.120.100.30边缘锐利优先火焰/烟雾/粒子特效0.200.350.50柔化过渡补偿暗部人物/实物视频黑棚拍摄0.080.200.45需要保留更多暗部细节上面只是参考值实际素材千差万别我最推荐的做法仍然是“播放暂停在静帧再调参数对比差异”。视频播放中调参很难看清效果暂停在问题最明显的画面帧上慢慢调效率高得多。5. 平台差异与VideoPlayer的连环坑这套方案在编辑器里跑通之后真机上往往还会出各种幺蛾子。不同平台对视频解码、RenderTexture和Shader编译的支持都不一样下面是我实测过的几个坑。5.1 Android和iOS上的视频编码兼容问题VideoPlayer在移动端走的是系统硬解Android端最稳妥的编码组合是H.264 Baseline Profile加AAC音频部分低端机型对High Profile支持不好会导致花屏或黑屏。iOS对H.264的支持普遍较好但如果你用了HEVC(H.265)旧款iPhone可能解不了。另一个容易忽略的点是H.264解码后YUV到RGB的转换矩阵不同平台实现有细微差别同一个视频在PC上看着黑色背景偏灰绿在Android上可能偏紫红。如果发现移动端黑底抠不干净先别怀疑Shader先在PC上把同一帧画面截下来对比颜色确认是不是平台解码差异导致的。应对办法是给Shader加一个色偏修正参数或者针对特定平台微调_Cutoff。5.2 WebGL和微信小游戏的特殊限制WebGL平台下VideoPlayer的targetTexture支持非常有限Unity 2021 LTS及以前版本基本不可用2022版本也一直有各种兼容问题。如果你要发WebGL版本常规做法不适用。微信小游戏环境更是另一套逻辑VideoPlayer组件基本没戏通常要走同层渲染、把视频节点放到最上层显示或者用插件把视频画面转成纹理上传。这类方案实现成本高而且不同版本的小游戏基础库行为还有差异。所以如果你的项目明确要发WebGL或微信小游戏建议在技术选型阶段就提前确认视频播放方案而不是等到UI模块做完了再回头换实现否则返工量非常大。5.3 Shader变体编译与RenderTexture尺寸如果项目用了Shader变体比如通过ShaderVariantCollection预编译记得把自定义的VideoShader加进变体收集器否则打AssetBundle后运行时可能因为Shader没被打进包导致整个UI变紫。我遇到过两次这种问题都是在真机上材质突然变品红色查了半天最后发现是变体丢失。RenderTexture的尺寸也要控制。有些同学习惯把RT建得特别大比如4K但UI上实际只占屏幕一小块白白增加带宽压力。移动端建议RT分辨率不要超过视频实际分辨率配合RawImage的尺寸缩放保证清晰度就够了。另外如果你在频繁创建和销毁RenderTexture一定要记得调用Release释放否则Unity会报“Temp RenderTexture already allocated”的警告持续泄漏最终会导致真机黑屏。5.4 性能实测数据参考最后给一份我在中端Android机骁龙778G上实测的数据节选1920x1080视频渲染到1080p的RenderTextureRawImage全屏显示GPU频率稳定在低频段整体帧耗在2ms以内Shader本身的计算量很低性能瓶颈主要在视频解码和RT拷回。如果同一屏挂了多个视频实例性能影响会线性增加。对于性能敏感的团队我的建议是优先控制视频分辨率和同时播放的实例数量Shader的优化反而是次要的它本身足够轻量。6. 最后再分享一个小技巧实际项目中我还会在Shader里加一个可选的_CutoffMask贴图通道允许程序运行时通过一张可拖拽的遮罩贴图把视频的某部分区域强制抠掉。这个功能不是必须买的但在一些特定的运营活动页面上有奇效——比如视频素材边缘明明背景不是纯黑而是渐变的深灰色这种画面如果只靠亮度阈值抠会把边缘所有灰色渐变都抠掉导致画面缩水一圈给一张基于美术原始出图抠好的遮罩能省下大量调参时间。另外如果视频素材本身自带Alpha通道有些特效素材其实是RGBA的A通道才是透明信息Shader判断逻辑可以加一行如果素材带Alpha通道优先用A通道做透明而不是用亮度。这一点能兼容更多美术素材通用性会大很多。这套方案的完整链路我已经在项目里跑了两三个月从最初的全屏UI特效到后来的3D全息投影都在持续用这套Shader处理视频。如果你也卡在黑底视频的边缘问题上按这篇文章的思路把参数逐项调一遍多数情况下都能解决。实在遇到调不通的素材别死磕Shader先确认是不是视频编码导致的偏色问题。
返回列表