
1. 项目概述为什么屏幕位置节点是Shader的“眼睛”在ShaderGraph的世界里如果说各种数学运算节点是“大脑”纹理采样节点是“触觉”那么屏幕位置节点Screen Position Node无疑就是Shader的“眼睛”。它让着色器能够“看见”自己当前正被绘制在屏幕的哪个位置这个看似简单的信息却是实现屏幕空间特效、动态遮罩、UV扭曲以及各种高级视觉反馈的基石。无论是制作一个随着鼠标移动而泛起涟漪的水面还是一个从屏幕边缘向内淡出的雾气效果都离不开对屏幕坐标的精确掌控。很多刚接触ShaderGraph的朋友可能会被UV坐标和屏幕坐标搞得有点晕。简单来说UV坐标描述的是模型表面一个3D物体的纹理映射关系它的范围通常在(0,0)到(1,1)之间是相对于模型自身的。而屏幕坐标描述的是片元最终屏幕上每一个像素点在整个渲染画面中的位置它的坐标系是相对于摄像机视口Viewport的。理解这个根本区别是玩转屏幕位置节点的第一步。这个节点之所以值得用一整篇来详解是因为它提供的四种模式Default, Raw, Center, Tiled各有其精妙的用途和背后的数学原理。用对了你的特效丝滑流畅、性能高效用错了可能就会出现奇怪的接缝、非预期的拉伸或者性能上的浪费。接下来我们就深入这颗Shader的“眼睛”看看它到底能看到什么以及我们如何利用它看到的信息来创造令人惊叹的画面。2. 核心原理与四种模式深度拆解屏幕位置节点的核心工作是输出当前正在处理的片元在屏幕空间中的坐标。但“屏幕空间”本身就有不同的定义方式这也就是其四种模式的由来。理解它们的输出范围和数学变换是精准使用的关键。2.1 坐标空间基础从裁剪空间到屏幕空间在深入模式之前必须搞明白一个前置概念顶点着色器输出的位置通常是裁剪空间Clip Space坐标。这是一个中心在摄像机、范围在(-w, w)的齐次坐标。GPU的固定管线会进行“透视除法”除以w分量将其转换为归一化设备坐标NDC范围在[-1, 1]DirectX风格或[-1, 1] (OpenGL风格)的立方体内。最后视口变换将NDC映射到具体的屏幕像素坐标上。屏幕位置节点的工作就是在这个流水线的末端为我们提供映射后的坐标信息但提供了不同阶段的“快照”。2.2 Default模式最常用的“标准化”坐标这是最常用、也最符合直觉的模式。输出范围每个分量的值域为 [0, 1]。数学本质它输出的是经过完整视口变换后的坐标并且进行了归一化。屏幕左下角是(0,0)右上角是(1,1)。工作原理可以理解为(屏幕像素坐标) / (屏幕分辨率)。例如在1920x1080的屏幕上正中央的像素点其Screen Position (Default) 输出大约是 (0.5, 0.5, ...)。典型应用场景绝大多数需要基于屏幕位置进行采样或计算的效果。例如制作一个全屏的渐变色背景、模拟镜头晕影Vignette、或者根据屏幕Y坐标混合两种雾气。// 一个简单的自上而下的线性渐变 float4 screenPos Screen Position (Default); float gradient screenPos.y; // 底部为0顶部为1 return lerp(_ColorBottom, _ColorTop, gradient);注意Default模式的输出是Vector4其z分量通常代表深度非线性深度即经过透视投影变换后的深度w分量通常为1在透视除法后。在制作屏幕空间遮罩时我们主要使用其xy分量。2.3 Raw模式投影矩阵的“原始”输出Raw模式是理解屏幕位置来源的关键。输出范围xy分量值域大致在[-1, 1]或[0, 1]取决于图形API且未进行透视除法。其w分量非常重要是顶点在观察空间中的深度即距离摄像机的距离。数学本质它输出的是顶点/片元在裁剪空间的坐标。即(x, y, z, w) mul(UNITY_MATRIX_VP, worldPos)的结果。核心价值Raw模式最重要的用途是重建观察空间或世界空间位置。这是许多高级屏幕空间效果如SSR、SSAO的第一步。因为有了裁剪空间坐标和w分量深度我们可以通过逆投影变换反推出该像素在观察空间中的真实3D位置。// 伪代码使用Raw屏幕位置和深度纹理重建观察空间位置 float4 rawPos Screen Position (Raw); float depth SampleDepthTexture(rawPos.xy); float4 viewPos mul(unity_CameraInvProjection, float4(rawPos.xy * 2 - 1, depth * 2 - 1, 1)); viewPos.xyz / viewPos.w;实操心得除非你在手动进行空间变换或深度计算否则在常规的UV动画、遮罩制作中应优先使用Default或Center模式避免直接使用Raw模式的xy分量因为它们未经归一化直接使用会导致坐标随分辨率剧烈变化。2.4 Center模式以屏幕中心为原点的坐标Center模式是对Default模式的一个便捷变换。输出范围xy分量值域为 [-0.5, 0.5]假设屏幕宽高比非1:1则范围不对称例如[-0.5, 0.5*宽高比]。屏幕正中心为(0,0)。数学本质ScreenPos(Center).xy ScreenPos(Default).xy - 0.5典型应用场景所有需要围绕屏幕中心对称的效果。例如制作一个圆形的雷达扫描图、一个从中心向外扩散的冲击波、或者一个模拟鱼眼镜头的扭曲效果。使用Center模式可以省去手动减去0.5的步骤让表达式更清晰。// 创建一个从屏幕中心向外衰减的圆形遮罩 float2 centerPos Screen Position (Center).xy; float distanceFromCenter length(centerPos); // 计算到中心的距离 float circleMask 1 - smoothstep(0.1, 0.3, distanceFromCenter);避坑指南当屏幕宽高比不是1:1时Center模式输出的坐标范围在x和y方向上是不一致的。如果你想要一个完美的圆形需要先对坐标进行宽高比校正否则你会得到一个椭圆。校正方法通常是centerPos.x * _ScreenParams.y / _ScreenParams.x假设你想要适应高度。2.5 Tiled模式自动平铺的坐标Tiled模式是制作无缝平铺屏幕空间图案的神器。输出范围xy分量值域为 [0, 1]但它是基于小数部分frac的。无论屏幕坐标多大它都会在[0,1]区间内循环。数学本质ScreenPos(Tiled).xy frac(ScreenPos(Default).xy * _Tiling)。实际上节点内部已经处理了平铺逻辑输出直接就是循环后的[0,1]坐标。典型应用场景创建无限重复的屏幕空间网格、噪波、或纹理而无需担心接缝问题。例如模拟老式电视的扫描线、创建雨滴划过屏幕的效果、或者制作一个动态的科技感网格背景。// 创建一个平铺的网格效果 float2 tiledUV Screen Position (Tiled).xy; float gridSize 10.0; float2 grid floor(tiledUV * gridSize) / gridSize; float gridLine step(0.98, frac(tiledUV.x * gridSize)) step(0.98, frac(tiledUV.y * gridSize));性能提示Tiled模式非常高效因为它省去了你在片段着色器中使用frac指令的成本。对于需要大量平铺采样的全屏后处理效果使用Tiled模式结合一个小的平铺纹理比采样一张全尺寸屏幕纹理性能要好得多。3. 核心应用场景与实战案例解析理解了原理我们来看看如何用这颗“眼睛”去真正地创造视觉价值。屏幕位置节点的应用几乎贯穿了所有非物体本体的屏幕空间特效。3.1 动态遮罩与区域效果这是最直接的应用。利用屏幕坐标我们可以定义屏幕上任意区域的遮罩。案例1屏幕边缘暗角Vignette暗角效果能引导玩家视线聚焦于屏幕中心。使用Center模式最为方便。float2 centerUV Screen Position (Center).xy; // 校正宽高比确保圆形暗角 centerUV.x * _ScreenParams.y / _ScreenParams.x; float dist length(centerUV); float vignette 1.0 - smoothstep(_VignetteInnerRadius, _VignetteOuterRadius, dist); return baseColor * vignette;通过调整_VignetteInnerRadius和_VignetteOuterRadius可以控制暗角的起始范围和过渡柔和度。案例2自定义形状的屏幕区域高亮比如实现一个在屏幕右下角矩形区域内显示额外信息或增强对比度的效果。float2 screenUV Screen Position (Default).xy; float2 bottomRightCorner float2(0.7, 0.3); // 区域起始点 float2 areaSize float2(0.25, 0.25); // 区域大小 float2 inArea step(bottomRightCorner, screenUV) * step(screenUV, bottomRightCorner areaSize); float areaMask inArea.x * inArea.y; // 仅在区域内为1 // 将区域内的颜色做提亮处理 return lerp(baseColor, baseColor * _HighlightIntensity, areaMask);3.2 屏幕空间UV动画与扭曲通过将屏幕坐标作为UV并对它进行随时间或外部参数变化的变换可以实现全屏的动态效果。案例3全屏水面波纹扰动模拟热量扭曲或水下视线效果。通常需要结合一个噪声纹理和时间节点。float2 screenUV Screen Position (Default).xy; float2 distortion float2(0,0); // 采样一个流动的噪声图产生扰动向量 distortion.x (SampleNoise(screenUV _Time.x).r - 0.5) * 2.0 * _DistortionStrength; distortion.y (SampleNoise(screenUV _Time.x 0.5).g - 0.5) * 2.0 * _DistortionStrength; // 用扰动后的UV去采样场景颜色纹理通常在后处理中 float2 distortedUV screenUV distortion; return SampleSceneColor(distortedUV);注意事项这种直接扰动屏幕UV的方式在物体边缘可能会产生不自然的拉伸。更高级的做法是使用深度或法线信息来限制扰动的强度和方向。案例4无限滚动的背景使用Tiled模式可以轻松创建无缝滚动的星空、云层或科幻数据流背景。float2 tiledUV Screen Position (Tiled).xy; // 添加时间偏移形成滚动 tiledUV.y _Time.y * _ScrollSpeed; // 采样平铺的噪声或纹理 float4 background SampleTexture(_ScrollingTex, tiledUV); return background;3.3 高级应用与深度、法线纹理结合这是屏幕位置节点真正发挥威力的地方用于实现屏幕空间环境光遮蔽SSAO、屏幕空间反射SSR、雾效等。案例5简易的屏幕空间距离雾结合深度信息我们可以根据像素到摄像机的距离来混合雾效颜色。这里需要用到Raw模式的深度信息或单独采样深度纹理。// 方法使用深度纹理更精确 float2 screenUV Screen Position (Default).xy; float depth SampleDepthTexture(screenUV); // 将深度值转换为线性观察空间深度或世界空间距离 float linearDepth LinearEyeDepth(depth); // 根据距离计算雾效因子 float fogFactor saturate((linearDepth - _FogStart) / (_FogEnd - _FogStart)); float4 sceneColor SampleSceneColor(screenUV); return lerp(sceneColor, _FogColor, fogFactor);深度重建原理这里的关键是LinearEyeDepth函数它负责将采样到的非线性深度值转换回线性的、距离摄像机的实际单位距离。Screen Position (Default)提供了查找深度纹理所需的标准化UV坐标。案例6基于屏幕位置的边缘检测轮廓一种非传统的轮廓描边方法通过检测屏幕空间相邻像素的深度或法线差异来实现。float2 screenUV Screen Position (Default).xy; float2 texelSize 1.0 / _ScreenParams.xy; // 单个像素的UV大小 // 采样当前像素及上下左右四个邻居的深度 float depthCenter SampleDepthTexture(screenUV); float depthUp SampleDepthTexture(screenUV float2(0, texelSize.y)); float depthDown SampleDepthTexture(screenUV - float2(0, texelSize.y)); float depthLeft SampleDepthTexture(screenUV - float2(texelSize.x, 0)); float depthRight SampleDepthTexture(screenUV float2(texelSize.x, 0)); // 计算深度差使用绝对值或平方差 float depthEdge abs(depthUp - depthCenter) abs(depthDown - depthCenter) abs(depthLeft - depthCenter) abs(depthRight - depthCenter); // 如果深度差超过阈值则认为是边缘 float outline step(_DepthThreshold, depthEdge); return baseColor outline * _OutlineColor;性能与质量权衡这种屏幕空间边缘检测开销较大多次纹理采样且容易受噪声影响。在移动平台或性能敏感场景更推荐使用基于法线/深度的Roberts或Sobel算子并考虑降采样Downsample处理。4. 性能优化、常见问题与调试技巧即使知道了怎么用在实际项目中屏幕位置节点相关的特效也容易踩坑。这部分分享一些实战中积累的经验和排查方法。4.1 性能优化要点精度选择在ShaderGraph中屏幕位置节点输出的是Vector4即float4。对于大多数仅使用xy分量的遮罩或UV动画后续计算完全可以降级到float2精度。确保连接线后级的运算节点如乘法、加法也使用适当的精度避免不必要的全精度计算。平铺Tiling与重复采样当使用Tiled模式配合纹理采样时尽量使用尺寸较小、可无缝平铺的纹理。一张1024x1024的纹理平铺覆盖整个屏幕比采样一张4096x4096的全屏纹理要高效得多且能利用GPU的纹理缓存。避免全屏复杂计算基于屏幕位置的全屏效果尤其是后处理是性能消耗大户。务必将其放在独立的后处理渲染管线中并利用Unity的RenderScale或直接对低分辨率缓冲区如Half/Quarter Res进行计算再上采样这是业界常见的优化手段。分支优化尽量避免在片段着色器中使用基于屏幕位置的动态分支如if (screenUV.x 0.5)。GPU是并行架构动态分支可能导致同一波束Warp内的线程产生分歧严重降低性能。应尽量使用step、smoothstep或lerp等函数来替代条件判断。4.2 常见问题与解决方案下面是一个快速排查问题指南问题现象可能原因解决方案效果在屏幕边缘出现硬边或截断使用了Raw模式的xy分量且未正确处理或者计算时超出了[0,1]范围。检查是否误用了Raw模式。对于需要[0,1]范围的计算坚持使用Default或Tiled模式。使用saturate()函数将值钳制在[0,1]内。圆形效果变成了椭圆形使用了Center模式但未考虑屏幕宽高比Aspect Ratio。对Center模式的xy分量进行宽高比校正。公式correctedPos.x * _ScreenParams.y / _ScreenParams.x;假设以Y轴为基准归一化。平铺纹理出现接缝虽然用了Tiled模式但采样的纹理本身不是无缝纹理。确保使用的纹理在美术制作时就是可无缝平铺的。或者在Shader中使用frac或Tiled模式前对UV进行小幅度的偏移或旋转打破规律的接缝。效果随摄像机移动而“滑动”错误地将屏幕空间坐标用于世界空间或物体空间的计算。牢记屏幕坐标是相对于摄像机的。如果你想要一个固定在场景世界中的效果如地面上的血迹应该使用世界位置或物体位置节点而不是屏幕位置。后处理效果在VR中异常VR是双目渲染有两个视口Viewport。屏幕位置默认可能只对应其中一个。在Unity的URP或HDRP中处理VR时需要使用GetStereoAdjustedScreenPos等函数来校正屏幕坐标或者使用ComputeScreenPos手动计算。在ShaderGraph中可能需要使用自定义函数节点引入相关HLSL代码。深度重建效果错误深度纹理的采样方式或深度值到线性距离的转换错误。确认你采样的是正确的深度纹理可能是_CameraDepthTexture。使用正确的线性化函数如LinearEyeDepth或Linear01Depth。检查你的投影矩阵是透视投影还是正交投影两者的深度处理方式不同。4.3 调试技巧可视化你的屏幕坐标当效果不符合预期时最有效的调试方法就是将中间变量可视化。直接输出颜色最简单的方法将Screen Position节点的输出通常是.xy分量直接连接到主纹理的Base Color上。你会看到从黑到红X轴和从黑到绿Y轴的渐变。这能立刻告诉你坐标范围是否正确。分离通道检查使用Split节点将Vector4拆开然后单独将R或G通道输出为灰度图检查其分布是否符合预期例如Center模式中屏幕中央是否真的是0.5的灰度。使用Step函数绘制边界线用step函数可以清晰地看到坐标阈值。float2 uv Screen Position (Default).xy; float lineX step(0.495, uv.x) * step(uv.x, 0.505); // 绘制一条垂直中线 float lineY step(0.495, uv.y) * step(uv.y, 0.505); // 绘制一条水平中线 return float4(lineX, lineY, 0, 1); // 红线是竖线绿线是横线这能帮你精确定位坐标计算中的偏差。利用Custom Function节点打印数值对于复杂计算可以在关键步骤后连接一个自定义函数节点将向量转换为一个标量如取平均值或最大值然后通过将其输出到一个纯色材质上观察其明暗变化来推断数值范围。在开发中也可以考虑使用Debug.Log通过脚本传递参数或RenderDoc等GPU调试器来捕获中间值。掌握屏幕位置节点就等于为你的Shader打开了感知屏幕空间的大门。从简单的遮罩到复杂的屏幕空间反射其应用只受限于你的想象力。核心在于理解四种模式背后的坐标空间并在性能和效果之间做出明智的权衡。多动手实验用上面提到的调试方法观察中间结果是掌握它的最快路径。