ARTICLE DETAIL

资讯详情

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

Unity视频黑底抠像无黑边Shader完整方案

Unity视频黑底抠像无黑边Shader完整方案 做视频合成或者UI动效的时候总免不了跟“视频抠底”打交道。尤其是Unity工程里用VideoPlayer播一段带纯色背景最常见就是黑底的视频想让它跟界面融合直接丢上去肯定是一块黑块必须用Shader处理。这东西看着简单真正落地的时候却有一堆细节尤其是“无黑边”这三个字坑过不少人。这篇就把我从踩坑到稳定出图的完整方案连带平台差异、参数调优、常见故障全部分享出来给正要搞这个需求的同学一个直接能抄作业的参考。我默认你拿到的是这样一段视频画面主体在中间背景是纯黑色或者接近纯黑需要把黑色区域变透明让视频能够叠在UI图片、3D模型或者其他材质上。需求听起来就是“把黑的抠掉”而已但直接写个alpha rgb.r 0.1这种做法边缘一定会出现黑边和锯齿。真正要解决的是三件事第一暗部变透明的阈值怎么设才不误伤主体第二半透明过渡区域的边缘如何消除黑边第三VideoPlayer在不同平台上播放的纹理格式、色彩空间差异会不会影响抠像效果。1. 内容整体设计与思路拆解先把需求拆成两个核心点一个是“除黑底”另一个是“无黑边”。这两件事在技术实现上其实是两个层次。除黑底属于颜色键控Color Keying本质上是根据像素亮度或颜色分量做透明度映射把暗色区域的Alpha值压到0亮色区域保留Alpha为1中间区域做平滑过渡。无黑边则是在这个基础上进一步处理过渡带和混合模式的问题。为什么不直接用一个简单的clip或者threshold我用一个场景解释一下视频里主体边缘往往带有轻微的暗角、阴影或者半透明过渡比如一个发光的特效素材主体的辉光在黑色背景上会逐渐衰减边缘像素的亮度从亮慢慢过渡到黑。如果做硬切边缘像素要么被当背景删掉导致主体边缘被割掉一圈要么被当主体留下导致边缘一圈灰黑色的“脏边”。所以渐变阈值是必须的。再说黑边。实际项目里黑边出现的原因有三类这也是我在多个项目里反复确认过的第一类是过渡带颜色残留。当Alpha介于0到1之间时如果混合模式是传统的透明混合SrcAlpha, OneMinusSrcAlpha那么半透明像素的颜色值中RGB部分原本包含的黑色信息会以透明度权重叠加到背景上导致过渡区看起来黑乎乎的也就是俗称的“黑边”。第二类是纹理采样造成的硬边。视频作为一个动态纹理如果像素分辨率不够高或者VideoPlayer输出的纹理在GPU上被双线性过滤边缘一圈的采样会混入透明区域的黑色导致Alpha边缘发灰。第三类是UV贴图边缘溢出。如果视频是放在RawImage上拉伸显示RawImage的边缘UV是0和1一旦有压缩或采样偏差边缘会露出一条细缝正好透出后面背景的颜色看起来就是黑边或者亮边。所以整个方案的设计思路不是“写一个Shader把黑色挖掉”这么简单而是颜色键控负责Alpha的准确性过渡带处理负责边缘平滑混合模式负责消除颜色残留UV和采样设置负责消除物理层面的缝隙。四者缺一不可。方案选型上我推荐使用一个Unlit Shader配合Transparent渲染队列同时支持颜色键控模式和预乘Alpha模式。为什么要用Unlit因为视频本身是一张已经上色的纹理不需要任何光照计算用Unlit效率最高、也最容易控制输出颜色。渲染队列用Transparent3000是为了确保视频和UI、特效的混合顺序正确避免被其他不透明物体挡住或者半透排序出问题。为什么不用Unity官方UI的默认材质因为默认的UI/Default只支持普通透明混合不支持动态计算Alpha。你可以在C#端每帧把视频渲染到RenderTexture上然后GetPixels读回来逐像素处理Alpha再重新上传但那样CPU性能和内存都是灾难。Shader是唯一合理的路径。2. 核心细节解析与实操要点2.1 关键代码一个可用的黑底抠像Shader先给出我实际项目里在用的Shader核心代码。这段代码能同时处理Alpha过渡、去除黑边还支持调节抠像的阈值和柔边程度。Shader Custom/VideoKeyingTransparent { Properties { _MainTex (Video Texture, 2D) black {} _Threshold (Key Threshold, Range(0, 1)) 0.1 _Softness (Edge Softness, Range(0, 0.5)) 0.08 _Desaturate (Background Desaturation, Range(0, 1)) 0.3 _PreMultiply (Pre-Multiplied Alpha, Range(0, 1)) 1 } SubShader { Tags { QueueTransparent RenderTypeTransparent IgnoreProjectorTrue } Blend One 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; float4 _MainTex_TexelSize; float _Threshold; float _Softness; float _Desaturate; float _PreMultiply; 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越亮越不透明 float alpha smoothstep(_Threshold, _Threshold _Softness, luma); // 让保留区域的RGB尽量保持原样背景区域RGB置黑避免半透明处出现灰边 col.rgb * alpha; col.a alpha; // 可选对整体输出做预乘让混合更干净 if (_PreMultiply 0.5) { col.rgb col.rgb * alpha; } return col; } ENDCG } } Fallback Off }这段Shader最核心的就是smoothstep(_Threshold, _Threshold _Softness, luma)这一句。Threshold是亮度的临界值低于这个值的像素判定为背景透明高于_Threshold _Softness的像素判定为主体不透明两者之间的亮度区间做平滑渐变。这样边缘不会出现硬切割。关于混合模式Blend One OneMinusSrcAlpha这是预乘AlphaPremultiplied Alpha的标准混合方式。普通透明混合的公式是SrcAlpha, OneMinusSrcAlpha输出颜色是src.rgb * srcAlpha dst.rgb * (1 - srcAlpha)。而预乘混合的输出是src.rgb * 1 dst.rgb * (1 - srcAlpha)这里的src.rgb已经被我们在片段着色器里提前乘以Alpha了。两者在数学上对于完全不透明和完全透明的像素结果一致但对于半透明像素预乘混合不会在过渡区域叠加额外的黑色残留所以边缘更干净。2.2 无黑边的三个关键细节细节一Alpha过渡必须连续很多新手写键控Shader时会用clip(luma - _Threshold)或者if (luma _Threshold) discard这种方式。这种硬切的后果就是主体边缘出现一圈肉眼可辨的锯齿动态播放时还会闪烁。smoothstep本质上是一条从0到1的连续过渡曲线它让Alpha在过渡带上线性增加边缘视觉上就会柔和很多。细节二输出前要把RGB乘上Alpha这一条极其重要。想象一下边缘某个像素的Alpha是0.5如果直接输出RGB为原始值比如一个灰暗压黑的颜色(0.1, 0.1, 0.1)半透明混合到亮色背景上最终颜色是0.1 * 0.5 bg * 0.5无论如何都会在边缘压出一条暗色的线。这就是黑边。先把RGB乘以Alpha即col.rgb * alpha半透明区域的RGB被压暗然后配合预乘混合背景颜色占比相应增加边缘过渡就变成纯亮度渐变不会再出现灰黑色残留。细节三UV边缘处理RawImage在显示视频纹理时如果视频分辨率与UI尺寸不成整数倍关系GPU的双线性过滤会采样到纹理边缘之外的像素。默认情况下这些像素是黑或透明会导致边缘出现一条细黑线或者细缝。解决办法有两个一个是在VideoPlayer输出前保证视频和UI的宽高比一致另一个是在Shader里根据UV距离边缘做一次渐出。如果只是RawImage后者未必需要但如果你把视频渲染到RenderTexture再贴到任意网格上建议加一段边缘渐出代码。我这里给出的Shader在大多数UI场景下已经够用了。2.3 泛化绿幕素材怎么改实际业务里素材不一定是黑底也可能是绿幕、蓝幕。只需要把亮度键控改成颜色键控即可。判定的核心逻辑从“亮度阈值”变成“颜色距离阈值”。例如绿幕可以计算green col.g - max(col.r, col.b)绿色通道远大于红蓝时判定为背景再用smoothstep做过渡。这个通用思路和黑幕处理完全一致所以我一般会把Shader写成带颜色选择模式的通用版本用下拉开关切换黑幕、绿幕、蓝幕。3. 实操过程与核心环节实现3.1 VideoPlayer组件配置Shader只是处理显示视频能不能正常喂给材质取决于VideoPlayer的配置。我习惯的配置如下在场景里创建一张RawImage命名为VideoLayer作为视频显示载体。挂VideoPlayer组件Target Texture留空直接让VideoPlayer的targetMaterial指向RawImage的材质球。这样VideoPlayer会把视频帧直接写入材质球的_MainTex不需要经过RenderTexture中转性能最好。如果视频需要同时被多个物体共享或者要叠加额外特效则把Target Texture设为一张RenderTexture多个物体采样这张RT。这个方案多一次拷贝但灵活性高。直接设置targetMaterial的方式最容易出现的问题是材质实例被共享。VideoPlayer操作的是Render.material会自动创建材质实例但如果你在代码里通过image.material videoPlayer.targetMaterial这种赋值方式有可能把同一个材质球赋给多个RawImage导致顶点数据串了。最好在启动时用new Material(shader)创建一个独立实例赋给RawImage再设置VideoPlayer的targetMaterial为同一个实例。3.2 色彩空间陷阱Linear模式下的阈值漂移这个坑如果不是真实项目踩到很难提前意识到。Unity项目可以选择Gamma色彩空间或Linear色彩空间。Linear模式下纹理采样后进入Shader的颜色会经过一次从sRGB到Linear的转换导致整体亮度数值发生变化。你在Inspector里看到的黑色背景其实不是0而是接近0.02左右的非零值主体的亮点也会被压低。这带来的直接后果是一个在Gamma模式下设为0.1的阈值在Linear模式下可能需要调成0.2甚至更高否则主体边缘会被啃掉一圈或者背景抠不干净。我一般会做一个简单判断项目用Linear空间就把Threshold回调到0.2左右起步Softness调到0.12左右再根据素材实际微调。验证颜色空间最直接的方法是把Shader临时改成输出纯luma值return luma.xxxx然后在Game视图里对比视频原始画面的黑白灰度。如果背景区域显示为深灰而不是纯黑说明色彩空间确实有偏移。3.3 平台差异WebGL、Android、iOS分别怎么处理视频播放这个需求在不同平台的差异非常大尤其是在WebGL上。VideoPlayer在WebGL平台走的是浏览器Video标签存在几个限制第一视频必须由用户手势触发播放。Unity WebGL里如果不先加一个“点击开始”的交互视频调用Play()会静默失败。在PC编辑器里正常一发布WebGL就黑屏多半是这个原因。第二WebGL对视频编码格式有硬性要求基本上必须是H.264MP4容器部分浏览器对特定码率、分辨率也有兼容性差异。我见过同时兼容Chrome、Edge、Firefox却在Safari上播放不了的情况基本靠多备一份视频转码解决。第三VideoPlayer在WebGL平台输出纹理时纹理格式有时会被平台转换成YUVShader里采样后颜色会和编辑器里不一样RGB通道可能发生偏移偏绿或者偏紫。如果遇到这种情况可以在Shader里额外加一个颜色修正参数单独给WebGL发布版本手动调节。Android平台则要留意硬件解码器的行为差异。国产机型的解码器对视频分辨率、码率容忍度不同播放高码率4K视频时可能出现花屏、绿屏。我的习惯是Android上视频分辨率不超过1080p码率控制在10Mbps以内编码格式H.264 High Profile这样兼容性最好。iOS相对省心硬解能力强H.264和HEVC都能稳定支持。iOS上还有一个特别坑的点VideoPlayer不能直接播放StreamingAssets下的视频需要先拷贝到Application.persistentDataPath下再播。这个不处理iOS上直接报错“Cannot open file”。3.4 滑动条控制播放进度的配合既然热词里出现了“滑动条”我就多说一句进度条配合的问题。VideoPlayer本身没有UI组件你需要自己做Slider联动核心代码如下private VideoPlayer vp; public Slider progressSlider; private bool isSettingSlider; void Update() { if (vp ! null vp.isPlaying) { isSettingSlider true; progressSlider.value (float)(vp.frame / (double)vp.frameCount); isSettingSlider false; } } public void OnSliderValueChanged(float value) { if (isSettingSlider || vp null) return; vp.frame (long)(value * vp.frameCount); }这里要特别注意vp.isPlaying不一定每秒都更新用frame/frameCount计算进度在大视频上精度足够但如果视频较长比如10分钟以上frameCount可能超过int上限要用long或者double来算。我见过有人直接(float)vp.frame / vp.frameCount视频短还好视频一长进度条乱跳其实就是精度溢出了。另外进度条拖动时VideoPlayer的frame设置不是立即生效的有时需要帧延迟。平滑的做法是设置一个目标帧号在Update里循环判断当前帧和目标帧的差超过一定范围才继续set防止拖动时卡顿。4. 常见问题与排查技巧实录这套方案我从最早的颜色键控版本迭代到现在至少帮不同项目解决过几十次视频播放相关的疑难杂症。下面把最常见的问题和排查结论列成一个参照表按出现频率排序后面再补几个长篇说明。症状根本原因解决方案背景抠不干净视频四周泛灰Threshold偏低或色彩空间偏移调高ThresholdLinear模式适当加大Softness主体边缘一圈黑边混合模式用了普通透明混合或未做RGB预乘切换为Blend One OneMinusSrcAlphaShader输出前乘alpha边缘闪烁/锯齿键控过渡带太窄或视频分辨率不足加大Softness或让视频输出分辨率对齐RawImageWebGL发布后黑屏用户手势未触发播放 / 视频编码不被浏览器支持加点击播放交互转码为H.264 MP4iOS无法播放视频VideoPlayer不支持直接读StreamingAssets先拷贝到persistentDataPath再播放Android机型花屏硬件解码器兼容性问题降低分辨率/码率H.264 High Profile视频拉伸变形RenderMode或RawImage宽高比与视频不一致使用AspectRatioFitter或固定设计分辨率视频偏绿/偏紫平台纹理格式YUV转换增加颜色修正参数单独调平台4.1 黑边清除的终极兜底方案边缘内缩采样如果调整混合模式和Alpha过渡还是能看到边缘有一圈像素残留尤其是当视频素材本身质量不太好、压得比较狠时可以在Shader里对UV做一点点内缩。做法是在Fragment里取UV时往中心收缩几个像素float2 uv i.uv; float2 texelSize _MainTex_TexelSize.xy; float inset 1.5; uv (uv - 0.5) * (1 - inset * texelSize * 2) 0.5; fixed4 col tex2D(_MainTex, uv);这样做的原理很简单视频纹理的边缘一圈像素常常因为压缩编码而出现异常内缩采样直接绕过那一圈相当于把边缘像素裁掉。inset设置为1.5通常就能解决问题个别素材可能需要3。这个操作会轻微缩小画面的可见尺寸视觉上几乎看不出。4.2 性能优化Shader变体和平台兼容视频Shader本身的计算量很小瓶颈通常不在片段着色器而在纹理上传和绘制调用。但我还是要提一个容易被忽略的点这个Shader里smoothstep和dot是纯GPU运算没有任何循环和分支性能开销可以忽略不计但如果你在同一个RawImage上叠加了Outline或Shadow组件UGUI会动态生成额外的顶点流导致视频区域出现重绘实际性能负担是Shadow组件造成的不是视频Shader。另外建议给这个Shader预留Keyword切换。用#pragma multi_compile KEYING_BLACK KEYING_GREEN KEYING_BLUE三个关键字切黑幕、绿幕、蓝幕发布时Unity会自动剔除未使用的变体减少打包体积。这个习惯在移动平台上尤其重要。4.3 配合RenderTexture的注意事项如果视频最终要输出到RenderTexture再叠加到3D模型上有几个额外的地方要处理。RenderTexture的格式建议用RenderTextureFormat.ARGB32Depth留0即可不需要深度缓冲。另外VideoPlayer渲染到RT时要确保RT的分辨率和视频分辨率一致否则画面糊。每次视频分辨率变化时RT需要重新创建这一步要监听videoPlayer.prepareCompleted事件在回调里做。有一个很实际的坑当RT被另一个相机或RawImage使用时VideoPlayer先写RT如果写RT的时间晚于渲染管线采样的时间画面会闪一帧。症状是播放过程中偶尔出现白闪或黑闪解决方法是把VideoPlayer的renderMode设置成RenderTexture后再调用一次videoPlayer.Play()触发内部重新同步或者干脆在start时给RT赋一张纯黑纹理兜底。4.4 Shader参数动态调整的调试技巧我强烈建议在调试阶段把这几个参数暴露到Inspector里并且在运行模式下实时拖动Threshold调为一个滑块范围0到0.5。Softness调为一个滑块范围0到0.3。PreMultiply开关用于对比普通混合和预乘混合的差异。一个DebugMode开关Shader里#ifdef DEBUG_MODE输出原始视频、Alpha通道灰度图、键控后通道图辅助判断问题出在Alpha还是颜色。用Alpha通道灰度图调试时最直观——背景区域应该是纯黑主体边缘应该是一个从黑到白连续渐变的过渡带。如果过渡带出现断裂说明视频压缩得太厉害需要源视频重新导出如果过渡带太宽说明Softness过大主体边缘会有一圈半透明的“虚边”。4.5 一个容易被忽略的Shader试验方法在Inspector里直接挂材质预览有时候素材表现异常但不确定是Shader问题还是播放链路问题。我习惯在Unity编辑器里新建一个Quad挂上材质手动拖一帧静态视频画面进_MainTex预览。这样绕开VideoPlayer直接在Inspector里旋转视角观察边缘是否干净。如果静态图干净但播放闪边问题就在视频流或者VideoPlayer时间同步如果静态图就看到黑边问题就在Shader的混合或阈值设置。这个排查方法比在Game视图里反复调参数高效得多。5. 从工具到“能用”选型与工程化建议最后聊一下这个方案的工程落地。很多同学做完Shader和VideoPlayer调试通过后就结束了但真正上线要面对打包、多平台、多素材的批量管理问题。我在实际项目里会做这几件事情。第一把Shader资源放入专门的VideoKeying目录通过Resources.Load或者Addressables加载避免依赖打包漏掉。第二做一个视频素材配置表用ScriptableObject记录每一条视频的素材路径、是否黑幕、阈值、柔边参数、播放是否循环。这样美术换素材时只需要在Inspector里配参数不需要改Shader、不需要改C#代码。第三WebGL平台的视频播放可以进一步封装一个交互控制组件点击画面播放、暂停、重新播放内部统一处理浏览器的事件约束。这个组件在微信小游戏、WebGL运营活动里是必须的因为很多浏览器策略要求播放必须由用户手势触发。第四如果视频数量多、内存换得紧要考虑首帧黑屏问题。VideoPlayer在Prepare完成前输出的是黑色纹理如果视频加载快但首帧延迟用户会看到黑底闪烁。解决方法是准备期间不显示RawImage或者先用视频封面图替换材质。5.1 关于“黑边”在最终UI上是否可见取决于目标背景这个点很多帖子不会提。同样是黑边在深色UI背景上几乎看不到在白色背景上会很明显。所以验收标准一定要用真实UI背景去测。我见过美术在深色背景下说“没问题了”结果上线换到浅色皮肤后边缘一圈黑边全部显形。最稳妥的做法是拿黑白灰三张渐变背景分别测试。5.2 后续可以扩展的方向这套Shader方案其实是从“视频抠像”这个需求生长出来的。后续可以扩展的能力很多把键控颜色变成可动态配置的颜色拾取器支持视频中实时调整结合VFX Graph做粒子特效叠加在视频上或者把视频播放输出到Pico设备上作为背景环境。纹理本身没有变化真正改变的是Alpha和混合逻辑所以这套核心思路完全可以复用。我在实际项目的经验是视频抠像不能只靠Shader一根救命稻草而是“视频素材质量、VideoPlayer配置、Shader键控参数、混合模式、平台限制”五个环节的协同。任何一个环节出问题最终视觉上都会表现为“抠不干净”“有黑边”“闪屏”之类的现象。把上边的细节处理好绝大多数素材都能做到肉眼无黑边的效果。以后遇到类似需求直接把这套思路搬过去把Threshold和Softness按素材微调一下基本不用再踩我踩过的坑。
返回列表