Unity实时人像抠图:基于距离场的Zero Distance Shader实现与优化 1. 项目概述从“距离”到“轮廓”的魔法在Unity里做实时人像抠图听起来像是需要调用某个AI模型API或者依赖一套复杂的后处理管线。但今天要聊的这个“Zero Distance Shader”方案走的是一条截然不同的“物理”路径。它的核心思想非常直观利用摄像机到人物模型表面每个像素点的距离信息来动态地区分前景人像和背景。简单来说就是让离摄像机特定距离范围内比如人物所在的区域的像素显示为原色而之外的像素则被“抠掉”或替换。这听起来有点像绿幕抠像的原理但它完全在Shader层面实现不依赖任何外部硬件或颜色键纯粹是数学计算。我第一次接触这个想法是在为一个需要实时虚拟直播但预算有限的移动端项目寻找方案时。传统的基于颜色差异或深度学习的方案要么对光照和背景要求苛刻要么在移动设备上性能开销太大。而这个基于距离的Shader其计算量可控效果在特定场景下出奇地好尤其适合固定机位、人物活动范围相对确定的场景比如虚拟主播、在线教育老师的形象展示等。它不是一个“万能”的抠图方案但绝对是工具箱里一把锋利且轻便的“手术刀”。2. 核心原理拆解距离场与阈值化要理解Zero Distance Shader得先抛开“图像识别”的思维进入“空间几何”的领域。它的理论基础可以概括为构建一个以摄像机或某个空间点为原点的距离场并通过阈值函数对距离值进行二值化或平滑处理从而生成遮罩Alpha通道。2.1 距离信息的获取在Shader中我们如何知道一个像素点距离摄像机有多远最直接的信息来源就是深度纹理Depth Texture。在Unity的URP或内置管线中我们可以通过_CameraDepthTexture采样获取当前像素在视图空间下的深度值。这个深度值Z本质上就是像素点到摄像机近裁剪平面的距离经过非线性变换。但直接使用这个深度值进行抠图会有一个问题它对整个场景一视同仁无法单独区分“人物”这个物体。因此更常见的做法是针对特定的人物模型在Shader中计算其模型表面某点到某个“参考点”的距离。这个“参考点”通常是摄像机位置_WorldSpaceCameraPos计算模型表面世界坐标点到摄像机世界坐标的距离。这种方式下人物整体与摄像机的距离是动态变化的适合人物在场景中移动的情况。一个自定义的世界空间坐标点比如在人物脚底或中心设置一个空物体作为“锚点”。计算表面点到这个锚点的距离。这种方式下抠图范围是以锚点为中心的球形或圆柱形区域适合人物固定位置旋转、做动作。在Shader中这个距离d的计算就是简单的向量模长d length(worldPos - referencePoint)。2.2 阈值化与边缘平滑得到距离d后下一步就是根据它来决定这个像素的透明度Alpha。最粗暴的方法是设置一个距离阈值_Cutoff如果d _Cutoff Alpha 1 完全显示前景如果d _Cutoff Alpha 0 完全透明背景但这会产生非常生硬、锯齿状的边缘就像用剪刀粗暴地剪开一样毫无真实感。为了解决这个问题我们需要引入一个平滑过渡区间。通常使用smoothstep函数来实现。smoothstep(min, max, x)函数会在x处于[min, max]区间时返回一个在[0, 1]之间平滑插值的结果。我们可以这样定义我们的透明度float halfWidth _EdgeWidth * 0.5; // _EdgeWidth 是过渡区的总宽度 float minThreshold _Cutoff - halfWidth; float maxThreshold _Cutoff halfWidth; // 当 distance 小于 minThreshold 时alpha为1大于 maxThreshold 时alpha为0在中间则平滑过渡。 float alpha smoothstep(maxThreshold, minThreshold, distance);这里smoothstep的参数顺序是(edge0, edge1, x)当x edge0时返回0x edge1时返回1。所以我们用maxThreshold和minThreshold对调来实现距离越小、透明度越高的效果。通过调整_Cutoff和_EdgeWidth我们就可以控制抠图的范围和边缘的柔和度。这构成了Zero Distance Shader最核心的控制参数。2.3 与深度测试的配合一个关键的细节是这个Shader通常需要关闭深度写入ZWrite Off但开启深度测试ZTest LEqual。为什么关闭深度写入避免人物自身的像素在深度缓冲区中互相覆盖导致内部被错误剔除。我们只希望用距离信息来控制透明度而不是用深度来排序。开启深度测试确保人物能正确地被场景中其他位于其前方的物体遮挡。这是保证抠出的人像能正确融入新场景的关键否则人像会永远“浮”在所有物体之上。注意这种处理方式意味着人物模型本身不能有复杂的自遮挡比如手臂放在身体前。因为关闭深度写入后模型内部的深度关系会混乱。对于大多数站姿或坐姿的人像这通常不是问题。3. Shader完整实现与逐行解析下面我将结合一个完整的、适用于Unity URP管线的Shader代码来详细讲解每一部分的作用和实现细节。这个Shader被命名为ZeroDistanceHumanCutout。3.1 属性Properties与变量定义Shader Custom/ZeroDistanceHumanCutout { Properties { _BaseColor (Base Color, Color) (1,1,1,1) _BaseMap (Base Map, 2D) white {} _Cutoff (Distance Cutoff, Range(0, 10)) 2.0 _EdgeWidth (Edge Smooth Width, Range(0.01, 2)) 0.2 _ReferencePoint (Reference Point (World Space), Vector) (0,0,0,0) [Toggle(USE_CAMERA_AS_REFERENCE)] _UseCamera (Use Camera as Reference, Float) 1 }_BaseColor和_BaseMap定义人物的基础颜色和纹理这是最终显示的部分。_Cutoff核心的距离阈值。单位是世界空间单位米。例如设置为2.0意味着距离参考点2米以内的像素将被保留。_EdgeWidth边缘平滑过渡区的宽度。值越大从前景到背景的过渡越柔和、越宽。_ReferencePoint一个世界空间坐标作为距离计算的参考原点。当不使用摄像机作为参考时生效。[Toggle] _UseCamera一个Shader变体开关。勾选时使用摄像机位置作为参考点不勾选时使用_ReferencePoint属性指定的位置。这提供了灵活性方便在固定机位和跟随摄像机两种模式间切换。3.2 顶点着色器Vertex Shader顶点着色器的任务主要是完成标准的坐标变换并将必要的数据传递给片元着色器。struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float3 positionWS : TEXCOORD1; // 关键将世界空间坐标传递下去 float3 normalWS : TEXCOORD2; }; Varyings vert(Attributes IN) { Varyings OUT; VertexPositionInputs vertexInput GetVertexPositionInputs(IN.positionOS.xyz); VertexNormalInputs normalInput GetVertexNormalInputs(IN.normalOS); OUT.positionCS vertexInput.positionCS; OUT.uv TRANSFORM_TEX(IN.uv, _BaseMap); OUT.positionWS vertexInput.positionWS; // 计算并传递世界坐标 OUT.normalWS normalInput.normalWS; return OUT; }这里最关键的一行是OUT.positionWS vertexInput.positionWS;。我们在顶点阶段计算出每个顶点的世界空间坐标并通过TEXCOORD1语义传递给片元着色器。片元着色器接收到的会是经过光栅化插值后的、每个像素点精确的世界坐标。这是后续计算距离的基础。3.3 片元着色器Fragment Shader与核心算法片元着色器是魔法发生的地方。half4 frag(Varyings IN) : SV_Target { // 1. 采样基础纹理和颜色 half4 baseColor SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, IN.uv) * _BaseColor; // 2. 确定参考点世界空间 float3 referencePointWS _ReferencePoint.xyz; #ifdef USE_CAMERA_AS_REFERENCE referencePointWS GetCameraPositionWS(); // URP内置函数获取摄像机世界坐标 #endif // 3. 计算当前像素到参考点的距离 float distanceToRef distance(IN.positionWS, referencePointWS); // 4. 基于距离计算Alpha值核心算法 float halfWidth _EdgeWidth * 0.5; float minThreshold _Cutoff - halfWidth; float maxThreshold _Cutoff halfWidth; // 使用smoothstep创建平滑过渡 // 注意smoothstep(edge0, edge1, x) 当 x edge0 返回0 x edge1 返回1。 // 我们希望距离越小Alpha越大所以把maxThreshold和minThreshold对调。 float alpha smoothstep(maxThreshold, minThreshold, distanceToRef); // 5. 应用Alpha到最终颜色并进行Alpha裁剪可选用于极致性能 #ifdef _ALPHATEST_ON clip(alpha - 0.001); // 如果alpha极小直接丢弃该片元 #endif baseColor.a * alpha; // 将计算出的alpha乘到基础颜色的alpha通道上 // 6. 简单的光照可选根据需求添加 // 这里使用兰伯特光照模型为例 float3 lightDir normalize(_MainLightPosition.xyz); float NdotL saturate(dot(IN.normalWS, lightDir)); baseColor.rgb * NdotL * _MainLightColor.rgb; return baseColor; }逐段解析纹理采样获取人物本来的颜色。参考点选择通过预编译指令#ifdef根据是否启用USE_CAMERA_AS_REFERENCE关键字动态选择使用自定义参考点还是摄像机位置。GetCameraPositionWS()是URP提供的工具函数。距离计算使用distance函数计算当前像素世界坐标IN.positionWS到参考点referencePointWS的欧几里得距离。Alpha计算核心这是整个Shader的灵魂。通过_Cutoff和_EdgeWidth计算出过渡区的上下界再利用smoothstep函数生成一个平滑的、从1到0变化的Alpha值。这个值在距离阈值中心附近变化最快模拟了边缘的羽化效果。Alpha应用与测试将计算得到的alpha乘到baseColor.a上。#ifdef _ALPHATEST_ON块是性能优化选项启用后对于Alpha值几乎为0的像素如完全背景GPU会直接丢弃该片元不进行后续的混合操作可以提升渲染效率。简单光照为了让人物抠出来后不显得扁平可以添加基础光照。这里使用了最简单的兰伯特漫反射模型。在实际项目中你可能需要根据需求替换为更复杂的PBR光照模型。3.4 SubShader与渲染状态配置SubShader { Tags { Queue Transparent RenderType Transparent RenderPipeline UniversalPipeline IgnoreProjector True } LOD 100 Pass { Name ForwardLit Tags { LightMode UniversalForward } Blend SrcAlpha OneMinusSrcAlpha // 标准Alpha混合 ZWrite Off // 关闭深度写入 ZTest LEqual // 开启深度测试 Cull Back // 背面剔除 HLSLPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile _ _ALPHATEST_ON // 编译变体支持Alpha测试 #pragma multi_compile _ USE_CAMERA_AS_REFERENCE // 编译变体支持参考点切换 // URP核心库 #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl // ... 属性、变量、结构体、函数声明 ... ENDHLSL } }TagsQueue Transparent将渲染队列设为透明物体确保它在不透明物体之后渲染能进行正确的Alpha混合。IgnoreProjector True忽略投影器避免干扰。BlendSrcAlpha OneMinusSrcAlpha是最常用的透明混合模式让前景和背景根据Alpha值进行叠加。ZWrite Off如前所述关闭深度写入是关键。ZTest LEqual深度测试设为小于等于确保人物能被前方的物体正确遮挡。#pragma multi_compile这两行指令告诉Unity为_ALPHATEST_ON和USE_CAMERA_AS_REFERENCE这两个关键字编译不同的Shader变体。这样我们就可以在材质面板上通过勾选框来动态切换功能而无需编写多个Shader文件。4. 实战应用与参数调优指南写好Shader只是第一步把它用对、用好才是关键。下面结合一个典型的虚拟主播应用场景来讲解如何设置和调优。4.1 场景搭建与材质配置导入人物模型将你的3D人像模型FBX或其它格式导入Unity场景。创建材质在Project窗口右键创建 MaterialShader选择我们刚写的Custom/ZeroDistanceHumanCutout。配置材质参数Base Map拖入人物模型的贴图。Use Camera as Reference通常勾选。这样抠图范围会随着摄像机移动而动态变化适合主播在有限范围内活动。如果不勾选则需要手动在场景中摆放一个GameObject将其世界坐标赋值给Reference Point适合固定位置的展示。Distance Cutoff (_Cutoff)这是最主要的参数。将摄像机移动到人物正前方合适的位置比如主播的常用位置。在Scene视图中选中人物材质逐步调整_Cutoff值。你会看到一个球形的“透明区域”从摄像机或参考点向外扩张。将这个值调整到刚好包裹住整个人物模型并稍微留出一点余量用于动作伸展。初始值可以从1.5到3.0之间尝试。Edge Smooth Width (_EdgeWidth)这个参数控制边缘的柔和度。值太小如0.05边缘会非常锐利可能产生锯齿。值太大如0.5过渡区会非常宽导致人物边缘半透明的区域过多显得模糊。对于大多数1080p或2K的渲染0.15到0.25是一个不错的起点。你可以放大观察人物轮廓调整到一个既有羽化效果又不失清晰度的值。4.2 与后处理栈的配合单纯抠出人像往往不够为了更好的融入新背景我们通常需要一些后处理来“润色”。边缘抗锯齿虽然smoothstep提供了平滑过渡但在运动或特定角度下边缘仍可能出现闪烁或锯齿。可以在URP的后处理体积Volume中启用抗锯齿Anti-aliasing推荐使用FXAA或TAA。TAA效果更好但略有延迟FXAA更轻量。颜色校正抠出的人像与新背景可能存在色温、对比度的差异。可以使用后处理中的Color Adjustments效果微调人物的伽马、饱和度、对比度使其与背景更协调。环境光遮蔽AO在人物脚底与地面接触的部分添加一点点屏幕空间环境光遮蔽SSAO可以让人物看起来更像是“站”在背景上而不是浮在空中。注意调整SSAO的强度和半径避免过度。背景虚化景深如果新背景是3D场景可以使用景深效果将焦点对准人物让背景产生自然的虚化进一步增强立体感和融入感。4.3 性能考量与移动端适配Zero Distance Shader本身计算非常轻量主要开销是每个像素一次距离计算和一次smoothstep操作。但在移动端仍需注意Overdraw由于我们关闭了深度写入人物模型内部重叠的像素如衣服褶皱会进行多次混合计算导致Overdraw增加。对于面数较高的模型这可能成为性能瓶颈。优化建议尽量使用面数适中、拓扑合理的人物模型。如果模型是双面渲染的如薄片状的头发考虑将其拆分为单面模型或使用更简化的Shader。Alpha Test vs Alpha Blend在片元着色器中我们提到了_ALPHATEST_ON。Alpha Test使用clip()函数对于完全透明的像素直接丢弃不进行混合。这减少了片元着色器的写入操作和带宽占用在大多数移动GPU上性能更好但会产生硬边缘。Alpha Blend进行标准的透明混合能产生平滑的边缘但性能开销相对较大。选择策略在移动端如果边缘柔和度要求不高可以启用_ALPHATEST_ON并将_EdgeWidth设得较小以获得更好的性能。在PC端可以放心使用Alpha Blend以获得最佳视觉效果。Shader变体管理我们使用了#pragma multi_compile来生成USE_CAMERA_AS_REFERENCE等变体。在项目构建时要确保所有用到的变体都被正确包含否则在运行时切换功能可能会导致材质变紫Shader丢失。可以在Graphics Settings中查看和配置变体剥离策略。5. 进阶技巧与常见问题排查掌握了基础用法后下面分享一些能显著提升效果和解决棘手问题的进阶技巧。5.1 处理复杂轮廓与头发半透明标准方案对大体轮廓有效但对于头发这类需要表现发丝半透明的区域一刀切的Alpha值会显得很假。解决方案是结合一张额外的遮罩纹理。制作遮罩贴图在建模软件或PS中为人物的头发、薄纱等需要特殊透明处理的区域制作一张灰度遮罩贴图如头发区域为灰色~白色其他区域为黑色。修改Shader在片元着色器中采样这张遮罩贴图得到一个mask值范围0~1。混合Alpha将距离计算出的alpha_distance与遮罩值mask进行某种混合。最简单的是相乘float finalAlpha alpha_distance * mask;。这样头发区域的透明度会同时受距离和遮罩控制可以实现发梢更透明、发根更实的效果。更精细的控制甚至可以定义两个阈值和过渡宽度一个用于身体一个用于头发然后用遮罩贴图在两者之间线性插值实现不同部位不同的抠图强度。5.2 解决背景颜色溢出Color Bleeding当人物边缘半透明时如果背景是纯色或高对比度区域可能会看到背景颜色“渗入”到人物边缘称为颜色溢出。原因Alpha混合时前景的半透明像素与背景色叠加所致。缓解方案收缩遮罩在距离计算时可以稍微将_Cutoff值调小一点或者增加_EdgeWidth中靠近前景一侧的权重这需要修改smoothstep的参数使其过渡曲线不对称让人物实心区域稍微“膨胀”一点减少边缘的半透明像素。后处理边缘处理使用一个后处理Shader检测人物边缘例如通过深度或法线突变并对边缘像素的颜色进行微调偏向于前景色以中和背景色的影响。这属于比较高级的技巧。5.3 动态参考点与多人物处理当场景中有多个人物需要独立抠图时为每个人物单独设置一个参考点GameObject会非常麻烦。我们可以通过脚本动态传递参考点位置。创建C#脚本using UnityEngine; public class DistanceCutoutController : MonoBehaviour { public Transform referencePoint; // 拖入作为参考点的Transform public Renderer targetRenderer; // 拖入需要控制的人物Renderer private MaterialPropertyBlock _propBlock; void Start() { _propBlock new MaterialPropertyBlock(); } void Update() { if (referencePoint ! null targetRenderer ! null) { targetRenderer.GetPropertyBlock(_propBlock); _propBlock.SetVector(_ReferencePoint, referencePoint.position); targetRenderer.SetPropertyBlock(_propBlock); } } }配置将脚本挂载到任意GameObject上把参考点比如一个空物体和人物的Renderer组件拖拽赋值。Shader调整确保材质中Use Camera as Reference是不勾选的。这样Shader就会使用脚本通过MaterialPropertyBlock设置的_ReferencePoint值。你可以用这个脚本控制多个人物甚至让参考点随时间移动实现动态变化的抠图区域。5.4 常见问题速查表问题现象可能原因解决方案人物完全透明或完全不透明_Cutoff值设置极端或参考点位置错误。检查_Cutoff值是否合理如0.1~10。在Scene视图开启Gizmos显示参考点确认其位置。边缘锯齿严重_EdgeWidth值太小或渲染分辨率过低。增大_EdgeWidth(如0.2)。在URP Asset中提高渲染缩放Render Scale或启用抗锯齿后处理。人物内部出现空洞模型本身有洞或开启了_ALPHATEST_ON且阈值过高。检查模型完整性。禁用_ALPHATEST_ON或降低其clip阈值。人物无法被其他物体遮挡Shader中深度测试ZTest设置错误。确保Pass中ZTest设置为LEqual或Less。移动时边缘闪烁没有使用抗锯齿或_EdgeWidth过小导致子像素级别的变化剧烈。启用TAA或FXAA抗锯齿。适当增加_EdgeWidth。考虑在距离计算中增加一点点抖动dithering来平滑运动时的过渡。性能开销过大移动端模型面数过高导致Overdraw严重或使用了复杂的片元计算。优化模型面数。尝试启用_ALPHATEST_ON。检查是否在片元着色器中进行了不必要的复杂计算如多次纹理采样、循环。6. 方案对比与适用边界没有一种抠图方案是完美的。Zero Distance Shader有其鲜明的优缺点和最佳适用场景。优势极致性能计算简单几乎不增加额外的渲染负担非常适合移动端和性能敏感的场景。实时性完全在渲染管线中完成零延迟适合实时交互应用。无需特殊背景不依赖绿幕、蓝幕或特定的背景颜色/图案。易于理解和控制参数物理意义明确距离、宽度美术和策划人员也能快速上手调整。劣势与局限依赖三维模型必须有一个三维的人物模型无法直接用于二维图片或视频流。对模型轮廓要求高抠图精度直接取决于三维模型的几何精度。对于复杂的发型、飘带等需要高精度模型或配合遮罩纹理。固定空间关系抠图范围基于空间距离。如果人物动作幅度过大超出设定的距离阈值部分身体可能会被错误抠除。需要根据动作范围预留足够的阈值余量。无法处理遮挡如果人物用手臂抱住一个属于背景的物体该物体不会被抠掉因为Shader只作用于指定的人物材质。这需要更复杂的场景分层管理。适用场景推荐虚拟主播/视频会议人物坐姿或站姿相对固定活动范围有限。AR试衣/虚拟形象展示用户与固定位置的虚拟形象交互。游戏中的角色特写/UI展示将3D角色从游戏场景中分离出来展示在UI界面中。教育与培训模拟讲师或操作员的虚拟形象演示。不适用场景警告需要从复杂动态背景中抠出人物的应用如监控视频。人物动作幅度极大、频繁超出预设距离范围的场景。对发丝级抠图精度有极高要求的影视级应用。这个基于距离的抠图Shader它更像是一个精巧的物理工具而非万能的AI。它的价值在于在特定的约束条件下固定的空间关系、已知的三维模型能以最小的性能代价获得相当不错的实时抠图效果。在项目初期进行技术选型时清晰界定需求边界判断是否落入它的“甜蜜区”往往能省下大量后期优化和寻找替代方案的时间。对于快速原型、移动端应用、预算有限的独立开发者来说掌握并善用这类“小而美”的技术常常是让项目顺利推进的关键。