URP 渲染实战 / 第 7 章 法线贴图与视差 / 案例 7.7
案例 7.7在自写 Shader 中接入 URP 的单步视差进阶
目标:写一个可以在“无视差 / 单步 / Steep / POM”之间切换的 Shader,先实现前两种。四种模式用 [KeywordEnum(None, Simple, Steep, POM)] 声明,生成 _MODE_NONE 等四个关键字,每种模式编译成独立变体,运行时没有分支开销。完整的片元着色器如下:
Assets/Cookbook/Ch07/Parallax.shader(节选:frag)
half4 frag(Varyings i) : SV_Target { float2 uv = i.uv; float2 dx = ddx(uv), dy = ddy(uv); // 视线转到切线空间。URP 的 GetViewDirectionTangentSpace 要求传入“插值后未归一化”的 N/T, // 它内部用同一个缩放因子处理三根轴,保持与 MikkTSpace 一致。 half3 viewDirWS = GetWorldSpaceNormalizeViewDir(i.positionWS); half3 viewDirTS = GetViewDirectionTangentSpace(i.tangentWS, i.normalWS, viewDirWS); float hitDepth = 0; #if defined(_MODE_SIMPLE) // URP Lit 的做法(ParallaxMapping.hlsl):采样一次高度,沿视线偏移一步 uv += ParallaxMapping(TEXTURE2D_ARGS(_ParallaxMap, sampler_ParallaxMap), viewDirTS, _Parallax, uv); #elif defined(_MODE_STEEP) uv = SteepParallax(TEXTURE2D_ARGS(_ParallaxMap, sampler_ParallaxMap), uv, normalize(viewDirTS), _Parallax, _MinLayers, _MaxLayers, hitDepth); #elif defined(_MODE_POM) uv = ParallaxOcclusion(TEXTURE2D_ARGS(_ParallaxMap, sampler_ParallaxMap), uv, normalize(viewDirTS), _Parallax, _MinLayers, _MaxLayers, hitDepth); #endif // 之后所有贴图都用偏移后的 UV 采样,并沿用原始 UV 的导数,避免台阶边缘的 mip 跳变 half3 albedo = SAMPLE_TEXTURE2D_GRAD(_BaseMap, sampler_BaseMap, uv, dx, dy).rgb; half3 normalTS = UnpackNormalScale(SAMPLE_TEXTURE2D_GRAD(_BumpMap, sampler_BumpMap, uv, dx, dy), _BumpScale); float3 bitangent = i.tangentWS.w * cross(i.normalWS, i.tangentWS.xyz); half3x3 tbn = half3x3(i.tangentWS.xyz, bitangent, i.normalWS); half3 normalWS = NormalizeNormalPerPixel(TransformTangentToWorld(normalTS, tbn)); half selfShadow = 1; #if defined(_PARALLAX_SHADOW) && (defined(_MODE_STEEP) || defined(_MODE_POM)) // 把主光方向也转到切线空间:TBN 是正交基,转置即逆,mul(tbn, v) = (T·v, B·v, N·v) half3 lightDirTS = normalize(mul(tbn, GetMainLight().direction)); selfShadow = ParallaxSelfShadow(TEXTURE2D_ARGS(_ParallaxMap, sampler_ParallaxMap), uv, dx, dy, hitDepth, lightDirTS, _Parallax, _ShadowSteps, _ShadowSoftness); #endif half3 color = Ch07Shade(i.positionWS, normalWS, albedo, 0.45, 0.12, selfShadow); return half4(color, 1); }
void main() { vec2 uv = uv0; vec2 dx = dFdx(uv), dy = dFdy(uv); // 视线转到切线空间。URP 的 GetViewDirectionTangentSpace 要求传入“插值后未归一化”的 N/T, // 它内部用同一个缩放因子处理三根轴,保持与 MikkTSpace 一致。 vec3 viewDirWS = GetWorldSpaceNormalizeViewDir(positionWS); vec3 viewDirTS = GetViewDirectionTangentSpace(tangentWS, normalWS, viewDirWS); float hitDepth = 0.0; #if defined(_MODE_SIMPLE) // URP Lit 的做法(ParallaxMapping.hlsl):采样一次高度,沿视线偏移一步 uv += ParallaxMapping(_ParallaxMap, viewDirTS, _Parallax, uv); #elif defined(_MODE_STEEP) uv = SteepParallax(_ParallaxMap, uv, normalize(viewDirTS), _Parallax, _MinLayers, _MaxLayers, hitDepth); #elif defined(_MODE_POM) uv = ParallaxOcclusion(_ParallaxMap, uv, normalize(viewDirTS), _Parallax, _MinLayers, _MaxLayers, hitDepth); #endif // 之后所有贴图都用偏移后的 UV 采样,并沿用原始 UV 的导数,避免台阶边缘的 mip 跳变 vec3 albedo = textureGrad(_BaseMap, uv, dx, dy).rgb; vec3 normalTS = UnpackNormalScale(textureGrad(_BumpMap, uv, dx, dy), _BumpScale); vec3 bitangent = tangentWS.w * cross(normalWS, tangentWS.xyz); mat3 tbn = mat3(tangentWS.xyz, bitangent, normalWS); vec3 nWS = NormalizeNormalPerPixel(TransformTangentToWorld(normalTS, tbn)); float selfShadow = 1.0; #if defined(_PARALLAX_SHADOW) && (defined(_MODE_STEEP) || defined(_MODE_POM)) // 把主光方向也转到切线空间:TBN 是正交基,转置即逆。 // HLSL 的 mul(tbn, v)(tbn 按行放 T/B/N)= (T·v, B·v, N·v),GLSL 里写成 v * tbn(tbn 按列放) vec3 lightDirTS = normalize(GetMainLight().direction * tbn); selfShadow = ParallaxSelfShadow(_ParallaxMap, uv, dx, dy, hitDepth, lightDirTS, _Parallax, _ShadowSteps, _ShadowSoftness); #endif vec3 color = Ch07Shade(positionWS, nWS, albedo, 0.45, 0.12, selfShadow); fragColor = vec4(color, 1.0); }
单步视差只有一行:uv += ParallaxMapping(...)。之后所有贴图——颜色、法线——都用偏移后的 uv 采样,这一点很重要:如果只偏移颜色贴图而法线贴图仍用原 uv,光照与图案会错位。另外,采样改用了 SAMPLE_TEXTURE2D_GRAD 并传入原始 uv 的导数。偏移量在砖缝边缘是突变的,若让 GPU 用偏移后 uv 自动求导,这些地方的导数会异常地大,选到极低的 mip,出现一圈模糊的“描边”。用原始 uv 的导数可以避免这个问题。单步视差的效果见下一个案例的对比图(图 7-15 第二格):砖缝看上去有了一点深度,但砖块侧面被拉成了条纹,掠射角下尤其明显。