案例 18.1噪声溶解:用 clip 挖洞入门
目标:给物体一个 0~1 的进度参数,进度增加时物体表面按噪声图案一块块消失,并且它投在地上的影子同步“溶掉”。
原理:溶解就是带阈值的 Alpha Test
给表面每个点分配一个“溶解值” v(最简单的就是从噪声贴图采样),再给一个全局进度 p。规则只有一条:v < p 的像素丢掉。进度从 0 涨到 1,越来越多的像素满足条件,物体就从“噪声值最小的地方”开始破洞,最后完全消失。HLSL 里丢弃像素用 clip(x):参数小于 0 时,这个片元不写颜色、不写深度、也不参与后续混合。
溶解后的物体仍然是“不透明”的——每个像素要么完整存在,要么完全不存在,没有半透明。所以它不需要透明队列,也不需要排序,用 "Queue"="AlphaTest"(2450)就行。把 Alpha Test 物体排在普通不透明物体之后,是为了让完全不透明的物体先把深度写好,更多被遮挡的溶解像素能被 Early-Z 提前剔除(带 clip 的 Shader 自己无法享受 Early-Z 的写深度优化,见第 29 章)。
这里有个容易忽略的细节:噪声贴图的取值很少真正达到 0 或 1。如果直接 clip(v - p),进度为 0 时可能已经有几个像素被裁掉,进度为 1 时可能还剩下几个“顽固像素”。解决办法是把进度区间稍微外扩:
// 把进度从 [0,1] 稍微外扩,保证 0 时完全可见、1 时完全消失(噪声图很少真正到 0 或 1) half DissolveClip(half value, out half edge) { half p = GetProgress() * (1.0h + _EdgeWidth + 0.02h) - _EdgeWidth - 0.01h; half d = value - p; // d < 0:已经溶掉 clip(d); // 刚好在消失前沿附近的一条带子:d 从 0 到 EdgeWidth,edge 从 1 降到 0 edge = 1.0h - saturate(d / max(_EdgeWidth, 1e-4h)); return d; }
#ifdef FRAGMENT // discard 只能出现在片元着色器里(HLSL 的 clip 也是) // 把进度从 [0,1] 稍微外扩,保证 0 时完全可见、1 时完全消失(噪声图很少真正到 0 或 1) float DissolveClip(float value, out float edge) { float p = GetProgress() * (1.0 + _EdgeWidth + 0.02) - _EdgeWidth - 0.01; float d = value - p; // d < 0:已经溶掉 if (d < 0.0) discard; // HLSL 的 clip(d) // 刚好在消失前沿附近的一条带子:d 从 0 到 EdgeWidth,edge 从 1 降到 0 edge = 1.0 - saturate(d / max(_EdgeWidth, 1e-4)); return d; } #endif
外扩量里还包含了下一个案例要用的边带宽度 _EdgeWidth:保证进度为 0 时连边带都不出现。函数同时输出 edge,它是“离溶解前沿有多近”的 0~1 值,案例 18.2 用它发光。
完整代码
溶解的材质参数与计算放在一个公共 HLSL 里,三个 Pass 都包含它:
// 溶解效果的核心:材质参数 + “溶解值”计算。Forward、ShadowCaster、DepthOnly 三个 Pass 共用, // 保证屏幕上的镂空、阴影里的镂空、深度图里的镂空完全一致。 #ifndef COOKBOOK_CH18_DISSOLVE_CORE_INCLUDED #define COOKBOOK_CH18_DISSOLVE_CORE_INCLUDED #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap); TEXTURE2D(_NoiseMap); SAMPLER(sampler_NoiseMap); TEXTURE2D(_EdgeRamp); SAMPLER(sampler_EdgeRamp); // 所有 Pass 的 UnityPerMaterial 必须一模一样,SRP Batcher 才认 CBUFFER_START(UnityPerMaterial) float4 _BaseMap_ST; half4 _BaseColor; half _Smoothness; float4 _NoiseMap_ST; half _Progress; half _AutoPlay; half _EdgeWidth; half4 _EdgeColor; half _UseRamp; half _NoiseStrength; float4 _DissolveDir; // xyz:对象空间方向 float4 _DirRange; // x/y:沿方向的最小/最大投影(对象空间) float4 _SphereCenter; // xyz:世界空间球心,w:最大半径 CBUFFER_END half GetProgress() { // 运行时可以勾选 AutoPlay 让它自己循环;书中拍图时 _Time 不可控,用 _Progress 手动给进度 return _AutoPlay > 0.5 ? frac(_Time.y * 0.25) : _Progress; } // 返回 0~1 的“溶解值”:值越小越早消失。进度 p 时,值 < p 的像素被裁掉。 half DissolveValue(float2 uv, float3 positionOS, float3 positionWS) { half noise = SAMPLE_TEXTURE2D(_NoiseMap, sampler_NoiseMap, uv * _NoiseMap_ST.xy + _NoiseMap_ST.zw).r; #if defined(_MODE_DIRECTION) // 方向溶解:沿对象空间某方向的投影,归一化到 0~1,再掺一点噪声让边缘不规则 float h = dot(positionOS, normalize(_DissolveDir.xyz)); half t = saturate((h - _DirRange.x) / (_DirRange.y - _DirRange.x)); return lerp(t, noise, _NoiseStrength); #elif defined(_MODE_SPHERE) // 球形溶解:离球心越近越早消失,像被一个不断变大的球“吞掉” half t = saturate(distance(positionWS, _SphereCenter.xyz) / _SphereCenter.w); return lerp(t, noise, _NoiseStrength); #else return noise; #endif } // 把进度从 [0,1] 稍微外扩,保证 0 时完全可见、1 时完全消失(噪声图很少真正到 0 或 1) half DissolveClip(half value, out half edge) { half p = GetProgress() * (1.0h + _EdgeWidth + 0.02h) - _EdgeWidth - 0.01h; half d = value - p; // d < 0:已经溶掉 clip(d); // 刚好在消失前沿附近的一条带子:d 从 0 到 EdgeWidth,edge 从 1 降到 0 edge = 1.0h - saturate(d / max(_EdgeWidth, 1e-4h)); return d; } #endif
// 溶解效果的核心:材质参数 + “溶解值”计算。Forward、ShadowCaster、DepthOnly 三个 Pass 共用, // 保证屏幕上的镂空、阴影里的镂空、深度图里的镂空完全一致。(GLSL 版,对应 DissolveCore.hlsl) #ifndef COOKBOOK_CH18_DISSOLVE_CORE_INCLUDED #define COOKBOOK_CH18_DISSOLVE_CORE_INCLUDED uniform sampler2D _BaseMap; uniform sampler2D _NoiseMap; uniform sampler2D _EdgeRamp; // HLSL 版要求所有 Pass 的 UnityPerMaterial 一模一样(SRP Batcher);GLSL 里就是普通 uniform uniform vec4 _BaseMap_ST; uniform vec4 _BaseColor; uniform float _Smoothness; uniform vec4 _NoiseMap_ST; uniform float _Progress; uniform float _AutoPlay; uniform float _EdgeWidth; uniform vec4 _EdgeColor; uniform float _UseRamp; uniform float _NoiseStrength; uniform vec4 _DissolveDir; // xyz:对象空间方向 uniform vec4 _DirRange; // x/y:沿方向的最小/最大投影(对象空间) uniform vec4 _SphereCenter; // xyz:世界空间球心,w:最大半径 float GetProgress() { // 运行时可以勾选 AutoPlay 让它自己循环;书中拍图时 _Time 不可控,用 _Progress 手动给进度 return _AutoPlay > 0.5 ? fract(_Time.y * 0.25) : _Progress; } // 返回 0~1 的“溶解值”:值越小越早消失。进度 p 时,值 < p 的像素被裁掉。 float DissolveValue(vec2 uv, vec3 positionOS, vec3 positionWS) { float noise = texture(_NoiseMap, uv * _NoiseMap_ST.xy + _NoiseMap_ST.zw).r; #if defined(_MODE_DIRECTION) // 方向溶解:沿对象空间某方向的投影,归一化到 0~1,再掺一点噪声让边缘不规则 float h = dot(positionOS, normalize(_DissolveDir.xyz)); float t = saturate((h - _DirRange.x) / (_DirRange.y - _DirRange.x)); return mix(t, noise, _NoiseStrength); #elif defined(_MODE_SPHERE) // 球形溶解:离球心越近越早消失,像被一个不断变大的球“吞掉” float t = saturate(distance(positionWS, _SphereCenter.xyz) / _SphereCenter.w); return mix(t, noise, _NoiseStrength); #else return noise; #endif } #ifdef FRAGMENT // discard 只能出现在片元着色器里(HLSL 的 clip 也是) // 把进度从 [0,1] 稍微外扩,保证 0 时完全可见、1 时完全消失(噪声图很少真正到 0 或 1) float DissolveClip(float value, out float edge) { float p = GetProgress() * (1.0 + _EdgeWidth + 0.02) - _EdgeWidth - 0.01; float d = value - p; // d < 0:已经溶掉 if (d < 0.0) discard; // HLSL 的 clip(d) // 刚好在消失前沿附近的一条带子:d 从 0 到 EdgeWidth,edge 从 1 降到 0 edge = 1.0 - saturate(d / max(_EdgeWidth, 1e-4)); return d; } #endif #endif
Shader 主体如下。Forward Pass 负责着色;ShadowCaster 与 DepthOnly Pass 只做一件事——用同一个函数算溶解值然后 clip:
// 案例 18.1~18.3:溶解。噪声 clip + 边缘发光 + 三种溶解形状(噪声 / 方向 / 球形)。 Shader "Cookbook/Ch18/Dissolve" { Properties { [MainTexture] _BaseMap ("Base Map", 2D) = "white" {} [MainColor] _BaseColor ("Base Color", Color) = (1,1,1,1) _Smoothness ("Smoothness", Range(0,1)) = 0.4 [Header(Dissolve)] [NoScaleOffset] _NoiseMap ("Noise (R)", 2D) = "gray" {} _NoiseMap_ST ("Noise Tiling/Offset", Vector) = (1,1,0,0) _Progress ("Progress", Range(0,1)) = 0.3 [Toggle] _AutoPlay ("Auto Play (_Time)", Float) = 0 [KeywordEnum(Noise, Direction, Sphere)] _Mode ("Mode", Float) = 0 _NoiseStrength ("Noise Strength (Dir/Sphere)", Range(0,1)) = 0.25 _DissolveDir ("Direction (Object)", Vector) = (0,1,0,0) _DirRange ("Direction Range (min,max)", Vector) = (-0.5,0.5,0,0) _SphereCenter ("Sphere Center(xyz) MaxRadius(w)", Vector) = (0,0,0,2) [Header(Edge)] _EdgeWidth ("Edge Width", Range(0,0.3)) = 0.08 [HDR] _EdgeColor ("Edge Color", Color) = (4,1.6,0.4,1) [Toggle] _UseRamp ("Use Ramp", Float) = 1 [NoScaleOffset] _EdgeRamp ("Edge Ramp", 2D) = "white" {} [Enum(UnityEngine.Rendering.CullMode)] _Cull ("Cull", Float) = 0 } SubShader { // 溶解本质是 Alpha Test:用 AlphaTest 队列,让 Early-Z 友好的不透明物体先画 Tags { "RenderType"="TransparentCutout" "Queue"="AlphaTest" "RenderPipeline"="UniversalPipeline" } Pass { Name "ForwardLit" Tags { "LightMode"="UniversalForward" } Cull [_Cull] HLSLPROGRAM #pragma vertex vert #pragma fragment frag #pragma shader_feature_local _MODE_NOISE _MODE_DIRECTION _MODE_SPHERE #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN #pragma multi_compile_fragment _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH #pragma multi_compile _ _FORWARD_PLUS #pragma multi_compile_fog #include "Assets/Cookbook/Ch18/DissolveCore.hlsl" #include "Assets/Cookbook/Ch18/Ch18Common.hlsl" struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float3 positionWS : TEXCOORD1; float3 positionOS : TEXCOORD2; half3 normalWS : TEXCOORD3; half fogFactor : TEXCOORD4; }; Varyings vert(Attributes v) { Varyings o; VertexPositionInputs p = GetVertexPositionInputs(v.positionOS.xyz); o.positionCS = p.positionCS; o.positionWS = p.positionWS; o.positionOS = v.positionOS.xyz; o.normalWS = TransformObjectToWorldNormal(v.normalOS); o.uv = v.uv; o.fogFactor = ComputeFogFactor(p.positionCS.z); return o; } half4 frag(Varyings i, FRONT_FACE_TYPE face : FRONT_FACE_SEMANTIC) : SV_Target { half edge; DissolveClip(DissolveValue(i.uv, i.positionOS, i.positionWS), edge); half3 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv * _BaseMap_ST.xy + _BaseMap_ST.zw).rgb * _BaseColor.rgb; // 双面渲染时,背面(物体内壁)把法线翻过来,再压暗一点,看起来像“空心的内部” half3 normalWS = normalize(i.normalWS) * IS_FRONT_VFACE(face, 1.0h, -1.0h); albedo *= IS_FRONT_VFACE(face, 1.0h, 0.35h); // 前沿附近先“烧焦”变暗,再在最前沿发光 albedo *= 1.0h - 0.85h * smoothstep(0.0h, 0.6h, edge); half3 color = SimpleLighting(albedo, i.positionWS, normalWS, _Smoothness); half3 glow = _EdgeColor.rgb; if (_UseRamp > 0.5) glow *= SAMPLE_TEXTURE2D(_EdgeRamp, sampler_EdgeRamp, float2(edge, 0.5)).rgb; // edge^2:让发光集中在最前沿,向内快速衰减 color += glow * edge * edge; color = MixFog(color, i.fogFactor); return half4(color, 1); } ENDHLSL } Pass { Name "ShadowCaster" Tags { "LightMode"="ShadowCaster" } ZWrite On ZTest LEqual ColorMask 0 Cull [_Cull] HLSLPROGRAM #pragma vertex vertShadow #pragma fragment fragClip #pragma shader_feature_local _MODE_NOISE _MODE_DIRECTION _MODE_SPHERE #pragma multi_compile_vertex _ _CASTING_PUNCTUAL_LIGHT_SHADOW #include "Assets/Cookbook/Ch18/DissolveCore.hlsl" #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/CommonMaterial.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl" float3 _LightDirection; // 由 URP 的 ShadowUtils 在画阴影前设置 float3 _LightPosition; struct Attributes { float4 positionOS : POSITION; float3 normalOS : NORMAL; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float3 positionOS : TEXCOORD1; float3 positionWS : TEXCOORD2; }; Varyings vertShadow(Attributes v) { Varyings o; float3 positionWS = TransformObjectToWorld(v.positionOS.xyz); float3 normalWS = TransformObjectToWorldNormal(v.normalOS); #if _CASTING_PUNCTUAL_LIGHT_SHADOW float3 lightDirWS = normalize(_LightPosition - positionWS); #else float3 lightDirWS = _LightDirection; #endif o.positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, lightDirWS)); #if UNITY_REVERSED_Z o.positionCS.z = min(o.positionCS.z, UNITY_NEAR_CLIP_VALUE); #else o.positionCS.z = max(o.positionCS.z, UNITY_NEAR_CLIP_VALUE); #endif o.uv = v.uv; o.positionOS = v.positionOS.xyz; o.positionWS = positionWS; return o; } half4 fragClip(Varyings i) : SV_Target { half edge; DissolveClip(DissolveValue(i.uv, i.positionOS, i.positionWS), edge); return 0; } ENDHLSL } Pass { Name "DepthOnly" Tags { "LightMode"="DepthOnly" } ZWrite On ColorMask R Cull [_Cull] HLSLPROGRAM #pragma vertex vertDepth #pragma fragment fragClip #pragma shader_feature_local _MODE_NOISE _MODE_DIRECTION _MODE_SPHERE #include "Assets/Cookbook/Ch18/DissolveCore.hlsl" struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; float3 positionOS : TEXCOORD1; float3 positionWS : TEXCOORD2; }; Varyings vertDepth(Attributes v) { Varyings o; o.positionWS = TransformObjectToWorld(v.positionOS.xyz); o.positionCS = TransformWorldToHClip(o.positionWS); o.uv = v.uv; o.positionOS = v.positionOS.xyz; return o; } half4 fragClip(Varyings i) : SV_Target { half edge; DissolveClip(DissolveValue(i.uv, i.positionOS, i.positionWS), edge); return i.positionCS.z; } ENDHLSL } } }
// 案例 18.1~18.3:溶解。噪声 clip + 边缘发光 + 三种溶解形状(噪声 / 方向 / 球形)。(GLSL 版,对应 Ch18/Dissolve.shader) Shader "Cookbook/Ch18/Dissolve" { SubShader { // 溶解本质是 Alpha Test:用 AlphaTest 队列,让 Early-Z 友好的不透明物体先画 Tags { "RenderType"="TransparentCutout" "Queue"="AlphaTest" "RenderPipeline"="UniversalPipeline" } Pass { Name "ForwardLit" Tags { "LightMode"="UniversalForward" } Cull [_Cull] GLSLPROGRAM #pragma shader_feature_local _MODE_NOISE _MODE_DIRECTION _MODE_SPHERE #pragma multi_compile_fog #include "Ch18/DissolveCore.glsl" #include "Ch18/Ch18Common.glsl" VARYING vec2 uv; VARYING vec3 positionWS; VARYING vec3 posOS; // 对象空间位置(HLSL 版的 positionOS 字段;GLSL 里这个名字已被顶点输入占用) VARYING vec3 normalWS; VARYING float fogFactor; #ifdef VERTEX void main() { VertexPositionInputs p = GetVertexPositionInputs(positionOS.xyz); gl_Position = p.positionCS; positionWS = p.positionWS; posOS = positionOS.xyz; normalWS = TransformObjectToWorldNormal(normalOS); uv = texcoord0.xy; fogFactor = ComputeFogFactor(p.positionCS.z); } #endif #ifdef FRAGMENT void main() { float edge; DissolveClip(DissolveValue(uv, posOS, positionWS), edge); vec3 albedo = texture(_BaseMap, uv * _BaseMap_ST.xy + _BaseMap_ST.zw).rgb * _BaseColor.rgb; // 双面渲染时,背面(物体内壁)把法线翻过来,再压暗一点,看起来像“空心的内部” vec3 N = normalize(normalWS) * (gl_FrontFacing ? 1.0 : -1.0); albedo *= gl_FrontFacing ? 1.0 : 0.35; // 前沿附近先“烧焦”变暗,再在最前沿发光 albedo *= 1.0 - 0.85 * smoothstep(0.0, 0.6, edge); vec3 color = SimpleLighting(albedo, positionWS, N, _Smoothness); vec3 glow = _EdgeColor.rgb; if (_UseRamp > 0.5) glow *= texture(_EdgeRamp, vec2(edge, 0.5)).rgb; // edge^2:让发光集中在最前沿,向内快速衰减 color += glow * edge * edge; color = MixFog(color, fogFactor); fragColor = vec4(color, 1.0); } #endif ENDGLSL } Pass { Name "ShadowCaster" Tags { "LightMode"="ShadowCaster" } ZWrite On ZTest LEqual ColorMask 0 Cull [_Cull] GLSLPROGRAM #pragma shader_feature_local _MODE_NOISE _MODE_DIRECTION _MODE_SPHERE #include "Ch18/DissolveCore.glsl" // _LightDirection / _LightPosition 由引擎在画阴影前设置(Lighting.glsl 里已声明) VARYING vec2 uv; VARYING vec3 posOS; VARYING vec3 positionWS; #ifdef VERTEX void main() { positionWS = TransformObjectToWorld(positionOS.xyz); vec3 normalWS = TransformObjectToWorldNormal(normalOS); vec3 lightDirWS = _CastingPunctualLightShadow > 0.5 ? normalize(_LightPosition - positionWS) : _LightDirection; vec4 positionCS = TransformWorldToHClip(ApplyShadowBias(positionWS, normalWS, lightDirWS)); positionCS.z = max(positionCS.z, UNITY_NEAR_CLIP_VALUE * positionCS.w); gl_Position = positionCS; uv = texcoord0.xy; posOS = positionOS.xyz; } #endif #ifdef FRAGMENT void main() { float edge; DissolveClip(DissolveValue(uv, posOS, positionWS), edge); fragColor = vec4(0.0); } #endif ENDGLSL } Pass { Name "DepthOnly" Tags { "LightMode"="DepthOnly" } ZWrite On ColorMask R Cull [_Cull] GLSLPROGRAM #pragma shader_feature_local _MODE_NOISE _MODE_DIRECTION _MODE_SPHERE #include "Ch18/DissolveCore.glsl" VARYING vec2 uv; VARYING vec3 posOS; VARYING vec3 positionWS; #ifdef VERTEX void main() { positionWS = TransformObjectToWorld(positionOS.xyz); gl_Position = TransformWorldToHClip(positionWS); uv = texcoord0.xy; posOS = positionOS.xyz; } #endif #ifdef FRAGMENT void main() { float edge; DissolveClip(DissolveValue(uv, posOS, positionWS), edge); fragColor = vec4(gl_FragCoord.z); } #endif ENDGLSL } } }
关键代码讲解
① 三个 Pass 必须一起裁剪。只在 Forward Pass 里 clip,屏幕上的物体会破洞,但地上的影子还是完整的——因为阴影贴图是 ShadowCaster Pass 画的;如果项目开了 SSAO 或者用到了深度图(比如本章后面的护盾),深度图里也会留下一个完整的“幽灵”。所以溶解值的计算必须写成公共函数,所有写深度的 Pass 都调用它。这也是为什么 CBUFFER 要放在公共文件里:同一个 Shader 所有 Pass 的 UnityPerMaterial 布局必须完全一致,否则材质会被 SRP Batcher 判为不兼容。
② ShadowCaster 的写法。阴影 Pass 的顶点函数和 URP 的 ShadowCasterPass.hlsl 一样:用 ApplyShadowBias 沿法线和光方向偏移,_LightDirection/_LightPosition 由 URP 在绘制阴影前设置,_ 区分方向光和点光/聚光(第 6 章)。有一个坑:URP 的 Shadows.hlsl 用到了 CommonMaterial.hlsl 里的 LerpWhiteTo,而 Lit 是通过 LitInput.hlsl 间接包含它的。自己写阴影 Pass 只包含 Shadows.hlsl 会报 “undeclared identifier 'LerpWhiteTo'”,需要先包含 Packages/com.。
③ 双面渲染。物体被挖出洞后,你会透过洞看到它的内壁。默认 Cull Back 会把内壁剔掉,看起来像一张纸壳;本例把 _Cull 做成属性,配图里设为 Off,并在片元里用 FRONT_FACE_SEMANTIC 判断正反面:背面翻转法线、压暗颜色,看起来像实心物体被掏空。

常见坑与调参
- 噪声 UV 拉伸:溶解图案跟着模型 UV 走。UV 拉伸的地方(比如本例纽结的长条方向)斑块也会被拉长。想要均匀的斑块,可以改用三平面采样(案例 18.9)或者对象空间的 3D 噪声。
- 噪声分布决定节奏:fBm 噪声的值大多集中在中间,所以进度在 0.3~0.7 之间变化最快、两头最慢。想要匀速的视觉效果,可以对噪声做直方图均衡,或者干脆在 C# 里用曲线(AnimationCurve)驱动进度。
- Mipmap 让边缘变糊:远处采样到低级 mip,噪声被平均成一片灰,溶解边缘会突然整体消失。噪声贴图可以关闭 mipmap,或者在 Shader 里用
SAMPLE_TEXTURE2D_LOD限制 mip 级别。 - 锯齿:
clip是硬边,MSAA 也救不了它(MSAA 只对几何边缘多采样)。开 MSAA 的项目可以用 Alpha to Coverage(第 16 章)来柔化,或者靠后处理 AA。