Unity Custom Interpolators与半透明阴影的原理与实战

张开发
2026/4/27 14:52:49 15 分钟阅读

分享文章

Unity Custom Interpolators与半透明阴影的原理与实战
深入剖析 URP 渲染管线中两个容易被忽略的关键问题 插值寄存器Interpolator的数量瓶颈与打包技巧以及半透明阴影的底层限制与三种可用的 workaround。 本文包含完整的 HLSL 代码示例与原理示意图。Part 01Custom Interpolators插值寄存器是什么在 GPU 渲染管线中顶点着色器Vertex Shader与片元着色器Fragment Shader 之间有一段固定宽度的数据通道——即插值寄存器Interpolator / Varying。 顶点阶段写入的每一个值GPU 在光栅化时会对三角形面片做重心插值 最终将插好的结果传递给每个片元。ℹ️硬件限制DirectX Shader Model 4/5对应 PC 桌面端定义了最多16 个 float4的插值语义TEXCOORD0~TEXCOORD15 移动端 OpenGL ES 3.0 通常只有8 个部分 Mali/PowerVR 芯片更少。 超出限制会直接导致编译报错或运行时黑屏。HLSL/GLSL 里插值语义通常写在结构体的字段上Part 02Pack / Unpack寄存器打包技术当需要传递的数据量接近或超过寄存器上限时 最常用的手段是将多个语义相近、精度要求低于 float4的数据 打包进同一个float4的xyzw分量 在片元着色器再按约定拆包。典型打包组合⚠️精度注意打包前请确认分量的值域。UV 通常在 [0, 1]法线分量在 [-1, 1] 顶点色在 [0, 1]——这些都可以安全共存于同一 float4不会相互干扰。 但若有数量级差距如世界坐标 vs. UV不建议强行打包。Part 03HLSL 代码完整 Pack / Unpack 实现① 顶点输出结构Varyings把原本需要 5 个语义的数据压缩到 3 个float4中// 精简后的顶点输出结构节省插值寄存器 struct Varyings { float4 positionCS : SV_POSITION; // 裁剪空间坐标系统语义不占 TEXCOORD float4 packed0 : TEXCOORD0; // .xyz normal(OS) .w uv1.x float4 packed1 : TEXCOORD1; // .xy uv1.y/uv2.x .zw uv2.y/tangentSign float4 packed2 : TEXCOORD2; // .xyzw vertexColor // 如需世界坐标再加一个 float3 positionWS : TEXCOORD3; // 世界坐标光照计算用 }; // 共 4 个 float4 SV_POSITION比原始方案节省约 3 个寄存器② 顶点着色器打包写入Varyings LitPassVertex(Attributes input) { Varyings output (Varyings)0; // ── 基础变换 ────────────────────────────── VertexPositionInputs posInput GetVertexPositionInputs(input.positionOS); output.positionCS posInput.positionCS; output.positionWS posInput.positionWS; // ── PACK: packed0 — 法线 xyz UV1.x ───── float3 normalOS TransformObjectToWorldNormal(input.normalOS); output.packed0.xyz normalOS; // 法线 x y z → .xyz output.packed0.w input.texcoord.x; // UV1.x → .w // ── PACK: packed1 — UV1.y / UV2 / 切线符号 ─ output.packed1.x input.texcoord.y; // UV1.y → .x output.packed1.yz input.texcoord2.xy; // UV2.xy → .yz output.packed1.w input.tangentOS.w; // 切线手性 → .w 值为 ±1 // ── PACK: packed2 — 顶点色 ──────────────── output.packed2 input.color; // rgba → xyzw直接赋值 return output; }③ 片元着色器解包读取half4 LitPassFragment(Varyings input) : SV_Target { // ── UNPACK packed0 ─────────────────────── float3 normalWS normalize(input.packed0.xyz); // 插值后重新归一化 float2 uv1 float2(input.packed0.w, // UV1.x 来自 packed0.w input.packed1.x); // UV1.y 来自 packed1.x // ── UNPACK packed1 ─────────────────────── float2 uv2 input.packed1.yz; float tangentSign input.packed1.w; // ±1用于重建副法线 // ── UNPACK packed2 ─────────────────────── half4 vertexColor half4(input.packed2); // xyzw → rgba // ── 后续正常使用 ────────────────────────── half4 albedo SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv1); albedo.rgb * vertexColor.rgb; // 与顶点色相乘 // ... 其余光照计算 return albedo; }插值后必须 normalize法线在光栅化阶段被线性插值结果不再是单位向量。 在片元着色器中读取后必须调用normalize()才能用于光照计算否则会出现亮度异常。Part 04半透明阴影URP 半透明阴影的底层限制Unity URP 的阴影系统基于Shadow Map技术 在主光源方向渲染一张深度贴图Shadow Map 随后在正常渲染通道中把当前像素的深度与 Shadow Map 对比判断是否在阴影中。URP 的ShadowCaster Pass要求写入深度ZWrite On 而半透明渲染通道通常关闭深度写入ZWrite Off并依赖 Alpha Blend。 两者的技术前提本质冲突属性不透明物体半透明物体兼容性ZWriteOnOff不兼容Blend ModeOff完全替换SrcAlpha OneMinusSrcAlpha不兼容ShadowCaster Pass自带正常工作缺失或禁用需手动添加渲染队列Geometry (2000)Transparent (3000)顺序依赖Workaround 1Alpha Test Alpha-to-CoverageAlpha Test Alpha-to-Coverage这是最常见也是效果最自然的方案。核心思路不走透明混合改走裁剪Clip 让物体仍属于不透明渲染队列可以正常写入深度与阴影。Alpha Test 工作原理在片元着色器中调用clip(alpha - _Cutoff) 当 alpha 低于阈值时丢弃当前片元相当于完全透明 高于阈值时当作完全不透明处理。 渲染队列设为AlphaTest2450仍走 ZWrite。Alpha-to-CoverageMSAA 模式Alpha Test 的硬边缘会产生明显的锯齿开启 MSAA 后可搭配[AlphaToMask On]利用 MSAA 的多重采样点来模拟平滑边缘效果接近半透明。URP 的 ShadowCaster Pass 要求写入深度ZWrite On 而半透明渲染通道通常关闭深度写入ZWrite Off并依赖 Alpha Blend。 两者的技术前提本质冲突 属性 不透明物体 半透明物体 兼容性 ZWrite On Off 不兼容 Blend Mode Off完全替换 SrcAlpha OneMinusSrcAlpha 不兼容 ShadowCaster Pass 自带正常工作 缺失或禁用 需手动添加 渲染队列 Geometry (2000) Transparent (3000) 顺序依赖 Workaround 1 Alpha Test Alpha-to-Coverage Alpha Test Alpha-to-Coverage 这是最常见也是效果最自然的方案。核心思路 不走透明混合改走裁剪Clip 让物体仍属于不透明渲染队列可以正常写入深度与阴影。 Alpha Test 工作原理 在片元着色器中调用 clip(alpha - _Cutoff) 当 alpha 低于阈值时丢弃当前片元相当于完全透明 高于阈值时当作完全不透明处理。 渲染队列设为 AlphaTest2450仍走 ZWrite。 Alpha-to-CoverageMSAA 模式 Alpha Test 的硬边缘会产生明显的锯齿开启 MSAA 后可搭配 [AlphaToMask On] 利用 MSAA 的多重采样点来模拟平滑边缘效果接近半透明。 AlphaTestShadow.shader ShaderLab Shader Custom/AlphaTestWithShadow { Properties { _BaseMap (Albedo, 2D) white {} _Cutoff (Alpha Cutoff, Range(0,1)) 0.5 } SubShader { // 关键渲染队列仍是 AlphaTest属于不透明队列 Tags { RenderTypeTransparentCutout QueueAlphaTest } Pass { AlphaToMask On // 需要 MSAA开启后边缘更平滑 ZWrite On // 写入深度 → ShadowMap 可用 // ... HLSLPROGRAM ... } // ShadowCaster Pass也要做 clip否则镂空处会投射实心阴影 Pass { Name ShadowCaster Tags { LightMode ShadowCaster } ZWrite On HLSLPROGRAM // 在 frag 里执行相同的 clip half alpha SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv).a; clip(alpha - _Cutoff); // 镂空区域不写深度 → 阴影形状正确 ENDHLSL } } }Workaround 2Dither 透明 阴影Dithering 抖动透明Dithering有序抖动的思路用空间上的像素开关来模拟视觉透明度——某个区域 50% 的像素被 clip 掉 远看就像 50% 透明同时每个未被裁剪的像素仍然是完全不透明的 可以正常写入深度和阴影。常用的抖动矩阵是4×4 Bayer 矩阵 将屏幕坐标对 4 取余得到矩阵索引再与 Alpha 比较决定是否 clipWorkaround 2 Dither 透明 阴影 Dithering 抖动透明 Dithering有序抖动的思路用空间上的像素开关 来模拟视觉透明度——某个区域 50% 的像素被 clip 掉 远看就像 50% 透明同时每个未被裁剪的像素仍然是完全不透明的 可以正常写入深度和阴影。 常用的抖动矩阵是 4×4 Bayer 矩阵 将屏幕坐标对 4 取余得到矩阵索引再与 Alpha 比较决定是否 clip DitherTransparent.hlsl HLSL // ── 4×4 Bayer 有序抖动矩阵 ───────────────────────────────────── static const float BayerMatrix4x4[4][4] { { 0.0/16.0, 8.0/16.0, 2.0/16.0, 10.0/16.0 }, { 12.0/16.0, 4.0/16.0, 14.0/16.0, 6.0/16.0 }, { 3.0/16.0, 11.0/16.0, 1.0/16.0, 9.0/16.0 }, { 15.0/16.0, 7.0/16.0, 13.0/16.0, 5.0/16.0 }, }; // ── 片元着色器中的抖动裁剪 ────────────────────────────────────── half4 DitherFrag(Varyings input) : SV_Target { half4 color SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.uv); half alpha color.a * _Color.a; // 取屏幕坐标像素整数坐标对 4 取余得矩阵索引 uint2 pixelCoord (uint2)input.positionCS.xy; float threshold BayerMatrix4x4[pixelCoord.x % 4][pixelCoord.y % 4]; // alpha 矩阵阈值 → clip 掉剩余像素完全不透明 clip(alpha - threshold); return half4(color.rgb, 1.0); // 输出不透明颜色 }ℹ️ShadowCaster Pass 同理在 ShadowCaster 的片元着色器里执行完全相同的抖动裁剪阴影边缘就会与物体本身的抖动图案一致 产生视觉上半透明阴影的效果。Dithering 效果的视觉示意Part 07方案选型方案对比与选型建议方案适用场景阴影质量性能依赖Alpha Test硬边裁剪树叶、铁丝网、布料镂空良好极低无Alpha Test A2C平滑边缘植被、草丛、头发MSAA 场景优秀低MSAA 开启Dithering渐变透明幽灵、全息、渐隐特效可接受低建议 TAA/高分辨率原生透明Alpha Blend玻璃、水面、UI 元素无阴影中—决策流程1确认是否需要阴影纯 UI 元素、粒子特效通常不需要投射阴影直接用 Alpha Blend 即可性能最佳。2判断透明类型如果边缘是硬裁剪型树叶、镂空图案→ Alpha Test 如果是渐变透明幽灵效果、消散动画→ Dithering。3检查渲染管线配置项目开启了 MSAA→ 在 Alpha Test 基础上加AlphaToMask On边缘质量大幅提升。 使用 TAA 或 DLSS→ Dithering 的噪点会被时域积累抑制效果更佳。4ShadowCaster Pass 别忘了同步无论选哪种方案ShadowCaster Pass 里必须执行相同的裁剪逻辑 否则会出现物体透明但阴影是实心的穿帮效果。

更多文章