ShaderGraph位置节点全解析:从坐标空间到屏幕特效实战

📅 2026/7/13 11:35:54 👁️ 阅读次数 📝 编程学习
ShaderGraph位置节点全解析:从坐标空间到屏幕特效实战

1. 项目概述:为什么我们需要深入理解Position节点?

在ShaderGraph的世界里,如果你问我哪个节点是连接“物体自身”与“渲染世界”的桥梁,我会毫不犹豫地说是各种Position节点。这不仅仅是技术上的定义,更是我踩过无数坑后的深刻体会。很多新手,甚至一些有经验的开发者,在制作屏幕特效、世界空间溶解或者简单的UV动画时,效果总是不对劲——物体移动时特效乱跑,或者在不同分辨率的屏幕上表现不一致。这些问题,十有八九都出在对“位置”数据的理解偏差上。

Position节点,或者说位置数据节点,是ShaderGraph中用于获取顶点或片元在特定坐标系下位置信息的一族节点。它们不像ColorTexture 2D节点那样直观,其输出值(一个Vector 3或Vector 4)背后隐藏着一整套图形学的空间变换逻辑。理解它们,就等于拿到了编写高级、稳定、可预测着色器的钥匙。无论是实现一个随着摄像机移动而变化的动态天空盒,还是一个精确附着在物体表面的污渍效果,都离不开对位置数据的精确操控。

简单来说,这篇文章就是为你彻底拆解ShaderGraph中的位置节点家族:PositionView DirectionScreen Position以及相关的ObjectWorldTangentNormal等空间向量节点。我会从最根本的空间坐标系讲起,用大量实际案例告诉你每个节点输出的是什么、应该在什么场景下用、以及最关键的——如何避开那些让人头疼的“坑”。如果你曾对Shader中复杂的位置变换感到困惑,那么这篇解析正是为你准备的。

2. 图形学基石:必须厘清的坐标空间

在直接操作节点之前,我们必须打好地基。Shader中的计算不是在真空中进行的,每一个位置数据都必须属于一个特定的坐标空间。用错空间,就像用地图导航时错把“本地商场”当成“地球经纬度”,结果必然南辕北辙。

2.1 核心坐标空间解析

在Unity(以及大多数图形渲染管线)中,我们主要与以下四个空间打交道,它们构成一个完整的变换链条:

  1. 模型空间(Model Space / Object Space)

    • 定义:这是顶点的“老家”,坐标值来源于3D建模软件(如Blender, Maya)。一个立方体的中心可能在(0,0,0),某个角点在(1,1,1)。
    • 特点:与物体本身绑定。无论你把模型放在世界何处,旋转缩放成什么样,其模型空间坐标在导入时就是固定的。
    • 在ShaderGraph中Position节点选择Object模式,或者使用Object节点,获取的就是这个空间的位置。
  2. 世界空间(World Space)

    • 定义:这是整个游戏场景的全局统一坐标系。所有物体都通过其Transform组件(位置、旋转、缩放)被放置到这个空间中。
    • 变换过程世界坐标 = 模型坐标 × 物体的模型矩阵(Model Matrix)。这个矩阵包含了物体的位移、旋转和缩放信息。
    • 在ShaderGraph中Position节点选择World模式,或者使用World节点。这是最常用、最直观的空间之一,常用于计算物体与场景中其他元素(如灯光、玩家)的距离和方向。
  3. 观察空间(View Space / Eye Space)

    • 定义:以摄像机为原点的坐标系。摄像机的前方通常是-Z轴或+Z轴(取决于API),其视野范围构成了视锥体。
    • 变换过程观察坐标 = 世界坐标 × 观察矩阵(View Matrix)。观察矩阵本质上是世界矩阵的逆矩阵,把世界变换到摄像机“眼前”。
    • 在ShaderGraph中:通常不直接提供,但它是计算View Direction(视线方向)和向裁剪空间变换的中间环节。
  4. 裁剪空间(Clip Space)与屏幕空间(Screen Space)

    • 裁剪空间:这是一个齐次坐标空间(x, y, z, w),经过投影矩阵变换后,用于定义视锥体。在此空间内,所有在视锥体内的顶点,其x, y, z分量都在[-w, w]范围内。这是GPU进行裁剪(Clipping)操作的空间。
    • 屏幕空间:这是我们将3D场景最终渲染到2D屏幕上的空间。通常,x和y坐标的范围被归一化到[0, 1](左下角为(0,0),右上角为(1,1))或[-1, 1]等。
    • 在ShaderGraph中Screen Position节点的输出,本质上就是经过这一系列变换后,在屏幕空间(或相关空间)下的位置信息。

重要提示:理解这些空间的关键在于记住它们是一个链式变换:模型空间 ->(模型矩阵)-> 世界空间 ->(观察矩阵)-> 观察空间 ->(投影矩阵)-> 裁剪空间 ->(透视除法)-> 标准化设备坐标 ->(视口变换)-> 屏幕空间。ShaderGraph的节点帮我们封装了这些复杂的矩阵运算,但我们心里必须清楚数据流向了哪里。

2.2 空间选择的核心原则

如何选择正确的空间?我的经验法则是:

  • 做物体自身的特效(如基于模型高度的渐变、模型表面的纹理滚动):优先考虑模型空间切线空间(用于法线贴图)。这样特效会牢牢“长”在物体上。
  • 做与场景交互的特效(如世界坐标投影纹理、根据世界高度混合材质):必须使用世界空间
  • 做屏幕空间特效(如全屏泛光、扭曲、UI遮罩):必须使用屏幕空间(通过Screen Position节点获取)。
  • 计算光照和视线:通常需要将方向转换到统一的世界空间切线空间进行计算。

3. 核心节点深度拆解与实战应用

掌握了坐标系,我们就可以开始“操作”这些节点了。ShaderGraph中的位置相关节点主要分为两大类:获取位置获取方向

3.1 Position节点:你的空间坐标提取器

Position节点是其中最基础、最核心的一个。它的模式(Mode)下拉菜单,直接决定了你获取的是哪个空间的位置。

节点接口

  • 输出端口(Out)Vector 3。输出指定空间下的位置坐标。
  • 属性(Mode):下拉选项,包括WorldObjectViewTangent

各模式详解与实战场景

  1. World(世界空间)

    • 输出值:顶点或片元在世界坐标系中的(x, y, z)位置。
    • 典型应用
      • 三平面投影(Triplanar Mapping):利用世界XZ坐标采样地面纹理,世界Y坐标采样侧面纹理,实现无缝的复杂地形材质,无需担心模型UV。
      // 概念性伪代码,在ShaderGraph中通过节点连接实现 float3 worldPos = Position (World); float4 texX = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, worldPos.yz); float4 texY = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, worldPos.xz); float4 texZ = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, worldPos.xy); // 根据世界法线权重混合
      • 世界空间噪声:使用世界位置作为输入,生成全局连续、不随物体移动而跳变的噪声效果,常用于云层、水面、体积雾。
      • 基于高度的混合:例如,实现雪线效果,当世界Y坐标高于某个值时,将材质混合为雪地材质。
  2. Object(模型空间)

    • 输出值:顶点在原始模型坐标系中的位置。
    • 典型应用
      • 模型内建渐变:比如创建一个从模型底部(Y最小)到顶部(Y最大)的颜色渐变。无论你如何旋转、缩放实例化的物体,这个渐变都相对于模型自身保持不变。
      • 顶点动画:在模型空间中对顶点位置进行偏移(如正弦波模拟旗帜飘动),计算更直观,因为动画是基于模型自身轴向的。
  3. View(观察空间)

    • 输出值:顶点相对于摄像机的位置。原点在摄像机,通常摄像机看向-Z方向。
    • 典型应用:相对较少直接使用,但它是计算深度、边缘检测等后处理效果的基础。更常用的是View Direction节点。
  4. Tangent(切线空间)

    • 输出值:这是一个特例。当Position节点设置为Tangent模式时,它输出的不是位置,而是顶点的切线向量(Tangent Vector)。这是一个Vector 3,通常与法线(Normal)和副切线(Bitangent)一起,构成描述模型表面方向的切线空间基向量。
    • 典型应用:这是法线贴图(Normal Map)解码和应用的绝对核心。法线贴图中存储的法线信息是相对于切线空间的。我们需要将灯光方向、视线方向从世界空间转换到切线空间,才能与法线贴图进行正确的计算。
      • 操作流程:通常使用Transform节点,将World Space下的Light DirectionView Direction,转换到Tangent Space。这个转换过程就需要用到Tangent向量(来自此节点)、Normal向量和副切线(可通过NormalTangent叉积得到)。

实操心得Position节点的Space参数在片元着色器(Fragment Shader)和顶点着色器(Vertex Shader)中都可以使用,但意义略有不同。在顶点着色器中,它获取的是顶点的位置;在片元着色器中,它获取的是经过光栅化插值后的片元的位置。对于大多数基于位置的特效(如世界空间纹理),在片元着色器中使用能获得更平滑、精确的结果。

3.2 Screen Position节点:屏幕特效的万能钥匙

Screen Position节点是制作全屏或与屏幕坐标相关特效的基石。它的不同模式(Mode)决定了输出坐标的原点和范围,这是最容易混淆的地方。

节点接口

  • 输出端口(Out)Vector 4。注意是四维向量!其xy分量通常是我们需要的屏幕坐标,zw分量可能有其他用途(如原始深度)。
  • 属性(Mode)DefaultRawCenterTiled

各模式详解与“避坑”指南

  1. Default(默认)模式

    • 输出值:经过透视除法(Perspective Divide)和视口映射后的标准化屏幕坐标。对于最常见的透视摄像机,其xy分量范围通常是**[0, 1]**,左下角为(0,0),右上角为(1,1)。
    • 计算过程屏幕坐标.xy = 裁剪空间坐标.xy / 裁剪空间坐标.w。这个除以w(即齐次坐标的w分量)就是透视除法,它实现了近大远小的透视效果。
    • 应用场景绝大多数屏幕空间效果。例如:
      • 屏幕UV动画:直接用其xy分量作为UV去采样一个全屏纹理。
      • 径向模糊:以屏幕中心为原点,根据当前片元屏幕坐标计算方向向量。
      • 制作简易UI遮罩:结合StepSmoothstep节点,可以轻松实现圆形、矩形等屏幕区域的遮罩。
  2. Raw(原始)模式

    • 输出值裁剪空间坐标,未进行透视除法。其xy分量范围是**[-w, w]**,具体值取决于视锥体和投影矩阵。
    • “坑”点解析:这是新手最容易出错的地方。如果你用Raw模式的输出直接当作UV,会发现纹理被严重拉伸扭曲,因为远处的顶点w值很大,导致xy范围被压缩。这个模式通常不直接用于采样纹理
    • 正确应用场景
      • 深度重建:在自定义渲染管线或某些后处理中,需要利用裁剪空间坐标和投影矩阵参数来重建世界空间位置或线性深度。
      • 某些特定的投影计算:如精确的球面映射(Sphere Mapping),需要未经透视除法的坐标。
  3. Center(中心)模式

    • 输出值:在Default模式的基础上,将坐标原点平移到了屏幕中心。即,屏幕中心是(0,0),左下角大约是(-0.5, -0.5),右上角大约是(0.5, 0.5)。具体范围与屏幕宽高比有关。
    • 应用场景:所有需要以屏幕中心为对称点或原点的效果。
      • 镜头光晕(Lens Flare):光晕元素通常沿屏幕中心到光源屏幕坐标的连线分布。
      • 色差/色散效果(Chromatic Aberration):根据当前片元到屏幕中心的距离,对RGB通道进行不同程度的偏移。
      • 径向渐变的遮罩:计算length(屏幕坐标 - 0.5)来得到到屏幕中心的距离,比用Default模式再减去0.5更直接。
  4. Tiled(平铺)模式

    • 输出值:在Center模式的基础上,对xy分量取了小数部分(frac操作)。这使得坐标在[-0.5, 0.5]范围内循环。
    • 应用场景:创建无限重复的、以屏幕中心为原点的平铺图案。比如一些复古的扫描线效果、网格背景,或者测试用的棋盘格纹理。它确保了图案在屏幕中心连续,边缘可能有接缝(取决于平铺方式)。

注意事项Screen Position节点在URP(通用渲染管线)和HDRP(高清渲染管线)中的行为是高度一致的,但在Built-in管线中,需要注意一些平台差异(如DirectX和OpenGL的屏幕空间Y轴方向可能不同)。ShaderGraph在很大程度上屏蔽了这些差异,但如果你发现上下颠倒的问题,可以尝试使用Flip节点对Y分量进行处理。

3.3 View Direction与Normal节点:光照与视角的奥秘

位置决定了“在哪”,而方向决定了“怎么看”和“怎么被照亮”。View DirectionNormal节点虽然不直接输出位置,但与位置计算密不可分。

View Direction(视线方向)节点

  • 输出Vector 3。从当前片元指向摄像机的单位向量(归一化向量)。
  • 空间:取决于其Space属性(World, Object, View, Tangent)。
  • 核心用途:计算高光反射(Specular)菲涅尔效应(Fresnel Effect)边缘光(Rim Light)
    • 菲涅尔效应:物体在视线与表面法线夹角越大的地方(边缘)反射越强。计算公式通常涉及1 - dot(Normal, ViewDir)
    • 边缘光:类似菲涅尔,在物体轮廓边缘添加发光效果。
  • 实操技巧:通常使用世界空间下的View Direction,并与世界空间下的Normal进行点积计算。确保两者在同一空间下!

Normal(法线)节点

  • 输出Vector 3。顶点的法线向量。
  • 空间:取决于其Space属性。同样有World, Object, View, Tangent可选。
  • 核心用途:所有与光照、视角相关的计算基础。漫反射光照 =max(0, dot(Normal, LightDirection))
  • “坑”点:从模型顶点插值得到的法线(Vertex Normal)是平滑的。如果你想要表现硬表面效果,需要使用Normal Vector节点并选择Face模式来获取面法线,或者依赖法线贴图来提供细节法线。法线贴图中的法线信息是存储在切线空间的,因此你需要用Transform节点将世界空间的光照和视线转换到切线空间,再与法线贴图采样结果进行计算。

4. 综合实战:构建一个高级边缘光与屏幕空间扭曲特效

理论说得再多,不如动手做一个。我们来组合运用上述节点,创建一个复合特效:一个具有世界空间边缘光的物体,其表面的扭曲效果会影响到屏幕空间的背景。

目标效果:一个物体,其边缘会根据视角发出自发光(边缘光)。同时,以该物体在屏幕上的位置为中心,对屏幕背景进行一个类似热浪的扭曲扰动。

实现步骤拆解

  1. 创建Unlit Graph并设置基础颜色:新建一个Unlit Graph,用于我们的特效材质。首先用一个Color节点定义物体基础色。

  2. 实现世界空间边缘光

    • a. 获取World Space下的NormalView Direction
    • b. 使用Dot Product节点计算法线与视线方向的点积。dot(N, V)。当视线与表面平行时(看边缘),点积接近0;当视线垂直表面时(看正面),点积接近1。
    • c. 使用One Minus节点计算1 - dot(N, V)。这样边缘的值接近1,正面的值接近0。
    • d. 使用Power节点对结果进行幂运算,例如Power(0.5),来控制边缘光的“硬度”或“软度”。幂值越小,边缘光范围越宽、越柔和。
    • e. 使用Color节点定义一个发光的颜色(如亮蓝色),并用Multiply节点与上一步的结果相乘。
    • f. 最后用Add节点将边缘光颜色与基础色相加。至此,一个基础的菲涅尔边缘光完成。
  3. 实现屏幕空间扭曲(这是难点和重点):

    • a. 我们需要获取当前片元在屏幕空间的位置。拖入Screen Position节点,模式选择Default。其输出的xy分量就是标准的[0,1]屏幕UV。
    • b. 我们需要一个扭曲的中心点。我们希望这个中心是该物体在屏幕上的投影位置。如何获取?我们需要将物体的世界空间位置转换到屏幕空间
      • 获取物体的世界位置:对于物体表面的每个点,其世界位置就是Position (World)。但我们需要一个代表物体的“中心点”,简单起见,我们可以使用模型原点在世界空间的位置。更精确的做法是使用Object节点的Position输出,但它在片元着色器中是插值后的。一个常见的技巧是,在顶点着色器中计算模型原点的屏幕位置,然后传递给片元。在ShaderGraph中,我们可以通过Custom Function或利用Transform节点矩阵手动计算。为了简化演示,我们假设使用顶点着色器传递的物体中心屏幕坐标(可通过脚本传递或简化处理)。
      • 假设我们通过一个Vector 2属性_ObjectScreenCenter传入物体中心的屏幕UV坐标(范围0,1)。
    • c.计算扭曲:用当前屏幕UV减去物体中心UV,得到相对于中心的向量。计算这个向量的长度(到中心的距离)。
      float2 uv = ScreenPosition.xy; float2 center = _ObjectScreenCenter; float2 dir = uv - center; float distance = length(dir);
    • d. 基于距离创建一个扰动值。可以使用Noise节点,以uv + _Time.y为输入,生成动态噪声。然后让扰动的强度随着距离增加而衰减(例如乘以1/(1 + distance*10))。
      float2 noiseUV = uv + _Time.y * 0.1; // 让噪声动起来 float2 noise = SimpleNoise(noiseUV).rg; // 假设是二维噪声,取值在[-0.5, 0.5] float attenuation = 1.0 / (1.0 + distance * 10.0); noise *= attenuation * _DistortionStrength; // _DistortionStrength是控制强度的属性
    • e. 将扰动加到原始的屏幕UV上,得到扭曲后的UV。
      float2 distortedUV = uv + noise;
    • f. 使用这个扭曲后的UV去采样屏幕背景纹理。在URP中,你可以通过Scene Color节点获取当前渲染的屏幕图像。将采样到的颜色作为背景。
    • g.关键合成步骤:我们需要将物体的颜色(含边缘光)与扭曲后的背景合成。这里需要透明度混合。我们之前计算的边缘光强度(步骤2.d的结果)可以作为物体“主体”的Alpha遮罩的一个来源。越靠近边缘,物体越“透明”,越能显示背后的扭曲背景。我们可以:
      • 将边缘光强度值重映射,作为物体颜色的Alpha值。
      • 使用Lerp(线性插值)节点,以这个Alpha值为系数,在扭曲背景色物体色(含边缘光)之间进行插值。这样在物体边缘,就能看到背景被扭曲的效果,而物体中心则保持实体感。
  4. 最终输出:将Lerp的结果连接到主纹理的Color端口,并处理好Alpha通道连接到Alpha端口,确保渲染管线能正确进行混合。

这个案例综合运用了World PositionWorld NormalView DirectionScreen Position,并涉及了空间转换的思想。它清晰地展示了:物体自身的特效(边缘光)基于世界/观察空间,而与屏幕交互的特效(扭曲)必须基于屏幕空间,并通过巧妙的Alpha混合将两者结合。

5. 性能优化、常见问题与调试技巧

即使理解了原理,在实际开发中还是会遇到各种问题。下面是我总结的一些实战经验和排查清单。

5.1 性能考量

  • 空间转换的成本:在片元着色器中进行复杂的空间转换(特别是矩阵乘法)是昂贵的。PositionNormal等节点在背后可能隐含着矩阵运算。
    • 优化建议:如果可能,尽量在顶点着色器中计算位置和方向,然后传递给片元着色器插值。ShaderGraph中可以通过节点设置选择是在Vertex还是Fragment阶段采样。对于View Direction这种每个片元都不同的计算,在片元中计算是必要的;但对于Object空间的位置,如果不需要每像素精度,可以考虑在顶点阶段计算。
  • 屏幕空间节点的开销Screen Position和采样Scene Color(如果需要)通常意味着你的着色器无法被批处理(Batching),并且可能增加显存带宽消耗。
    • 优化建议:将屏幕后处理效果放在专门的后处理堆栈(Post Processing Stack)中实现,而非物体材质本身。对于物体局部的屏幕交互(如本例),严格控制其影响范围和渲染队列,避免全屏使用。

5.2 常见问题排查表

问题现象可能原因排查与解决方案
特效在物体移动时“抖动”或“滑动”错误地使用了模型空间位置来做世界空间或屏幕空间的效果。检查Position节点的Space设置。如果是需要跟随世界移动的效果,确保使用World空间。屏幕空间特效则必须使用Screen Position
屏幕UV采样时纹理上下颠倒图形API差异(如OpenGL与DirectX的纹理坐标系)。使用Flip节点对Screen Position的Y分量进行取反操作(1 - Y)。或者在采样纹理前,检查并统一UV坐标系。
法线贴图看起来是错的,光照方向不对空间不匹配。法线贴图是切线空间法线,但光照方向是世界空间。确保使用Transform节点,将World SpaceLight DirectionView Direction转换到Tangent Space,再与法线贴图采样结果进行点积计算。
Screen PositionRaw模式导致纹理严重扭曲误将裁剪空间坐标直接当作[0,1]范围的UV使用。Raw模式输出的是裁剪空间坐标,需进行透视除法(除以w)才能得到标准化设备坐标/屏幕UV。直接使用Default模式即可,它已自动完成除法。
边缘光在物体背面也可见计算菲涅尔时没有考虑法线方向。当视线与法线点积为负时(即看背面),1-dot(N,V)会大于1。在计算点积后,使用Saturate节点将结果限制在[0,1]范围,或者使用Absolute节点取绝对值(如果希望双面都有边缘光)。更物理的方法是使用max(0, dot(N, V))
在不同分辨率或屏幕比例下特效变形屏幕空间计算没有考虑屏幕宽高比。Screen PositionCenter模式坐标范围受宽高比影响。在进行基于屏幕中心距离的计算时,将UV的y分量乘以屏幕宽高比(_ScreenParams.x / _ScreenParams.y),或将x分量除以宽高比,以校正为非正方形空间的距离计算。

5.3 调试技巧

当效果不符合预期时,不要盲目猜测。将中间变量可视化是最高效的调试手段。

  1. 颜色调试法:这是最直观的方法。将你怀疑有问题的向量(如PositionView Direction、某个计算中间值)直接连接到主输出的Color上。因为颜色分量范围是[0,1],你需要对向量值进行适当的重映射

    • 位置:世界位置通常值很大,直接输出会全白。可以尝试frac(worldPos)输出其小数部分,会得到彩色网格图案。或者worldPos * 0.01进行缩放。
    • 方向向量:方向向量分量范围在[-1,1]。需要将其映射到[0,1]:(vector + 1.0) * 0.5。这样你可以通过颜色判断方向是否正确(例如,法线朝上的部分应该是绿色的)。
    • 标量/强度:直接连接,白色代表1,黑色代表0。
  2. 分离计算链路:将复杂的节点网络拆分成几个独立的部分,分别检查输出。例如,先单独测试边缘光强度计算是否正确,再单独测试屏幕扭曲UV是否正确。

  3. 善用预览窗口:ShaderGraph的预览窗口可以设置为显示不同的中间变量。右键点击任意节点或连线,选择“Convert to Sub-graph”或直接创建Custom Function来隔离和测试特定功能模块。

理解ShaderGraph的位置节点,本质上是理解计算机图形学中顶点从模型到屏幕的旅程。每一个节点都是这个旅程中的一个快照或路标。Position告诉你现在在哪一站,Screen Position告诉你最终在屏幕地图上的哪个点,而View DirectionNormal则描述了你看待这个点的视角和它自身的朝向。掌握了它们,你就能自由地操控光影、空间与屏幕,将天马行空的视觉创意转化为精确可控的代码与连接线。记住,每当效果出现诡异的位移或扭曲时,第一个要问自己的问题就是:“我用的位置,到底是谁眼中的位置?”