1. 项目概述什么是视差卡牌以及为什么要在URP里折腾它如果你玩过一些卡牌对战游戏或者关注过近几年独立游戏的美术风格大概率见过一种效果一张看似平平无奇的卡牌当你倾斜视角或者鼠标悬停时卡牌上的图案、边框甚至文字会产生一种微妙的、有层次的“浮动”感仿佛卡牌内部真的有立体结构。这种效果就是“视差卡牌”Parallax Card的核心魅力。它本质上是一种基于视差映射Parallax Mapping技术的Shader特效通过在2D平面上模拟出3D的深度错觉极大地提升了UI和道具的视觉质感。几年前要在Unity里实现一个像样的视差效果你可能得去啃Built-in渲染管线的标准表面着色器或者自己从头写一个复杂的顶点/片元着色器。但自从Universal Render PipelineURP成为Unity的主流和未来方向后一切都变了。URP提供了一套更现代、更模块化同时也更“黑盒”的着色器框架。对于习惯了Built-in管线“所见即所得”的开发者来说在URP里实现自定义的、特别是这种需要复杂纹理采样的视觉效果初期可能会觉得有点无从下手。这个项目就是一次从零开始的探索。我们不依赖任何现成的Asset Store资源完全基于URP的Shader Graph和少量HLSL代码一步步构建一个功能完整的视差卡牌Shader。从最基础的视差映射原理讲起到实现静态的深度效果最后加入动态的交互反馈比如鼠标悬停时的动态倾斜。我的目标很明确不仅要让你能“抄作业”做出效果更要让你彻底理解URP下自定义Shader的工作流、视差技术的“障眼法”原理以及如何规避那些我踩过的坑。无论你是想丰富自己的UI库还是为你的下一个卡牌游戏增添亮点这套方案都能提供一个扎实的起点。2. 核心原理拆解视差映射如何“欺骗”你的眼睛在深入代码之前我们必须先搞懂视差映射到底是怎么一回事。把它想成一种高级的“贴图把戏”。2.1 从法线贴图到高度图你可能熟悉法线贴图Normal Map它通过改变像素的法线方向来模拟凹凸细节但模型的轮廓Silhouette依然是平的。视差映射想要更进一步它试图在不增加几何顶点的前提下让凹凸处的纹理采样点也能根据视角“移动”从而模拟出遮挡关系让轮廓看起来也有起伏。实现这个把戏的关键是一张高度图Height Map有时也叫视差图。这张图通常是一张灰度图白色值接近1代表“凸起”的区域黑色值接近0代表“凹陷”的区域。Shader会根据当前像素的视线方向去高度图上查找一个“偏移量”然后用这个偏移量去修正我们采样漫反射贴图、法线贴图等纹理的UV坐标。2.2 视线与高度的游戏偏移计算核心算法可以简化为一个步骤获取视线向量在切线空间Tangent Space下计算出从摄像机到当前片元的视线方向。切线空间非常重要因为它是一个以像素法线为Z轴的局部坐标系在这里处理高度偏移最直观。计算UV偏移将视线向量在XY平面上的分量即切线和副切线方向乘以该像素点的高度图采样值再乘以一个可控制的“强度”系数。公式近似为UV偏移量 (视线向量.xy / 视线向量.z) * 高度值 * 强度。这里视线向量.xy / 视线向量.z是关键它意味着视角越倾斜视线向量与表面法线夹角越大偏移量就越大这完全符合我们观察现实凹凸表面的直觉。应用偏移用计算出的UV偏移量去采样我们真正的颜色贴图、法线贴图等。这样在“凸起”的区域纹理采样点就会沿着视线反方向移动一点模拟出凸起部分遮挡其后侧纹理的效果。在URP的Shader Graph里我们无法直接使用上述除法操作因为节点限制但原理相通。我们会通过一系列向量计算和参数调节来模拟这个过程。2.3 URP下的实现路径选择在URP中你有两个主要选择来实现这个效果纯Shader Graph方案完全使用可视化节点连接。优点是快速、直观、易于调试和迭代。对于标准的视差遮挡Shader Graph的节点能力基本足够。这也是本教程前半部分采用的方式。Shader Graph Custom Function (HLSL) 方案当需要更复杂的计算比如陡峭视差映射Steep Parallax Mapping或者动态效果需要一些特殊数学运算时可以在Shader Graph中插入自定义HLSL函数节点。这提供了极大的灵活性是我们实现高级动态效果的后备手段。注意URP的Shader Graph默认是基于片元着色器的这意味着每个像素都会执行完整的视差计算。对于全屏UI或大量卡牌需要考虑性能。通常一张高度图分辨率适中如512x512的视差卡牌在现代移动设备上也是可以承受的。3. 环境准备与Shader Graph基础搭建工欲善其事必先利其器。在URP项目里玩转自定义Shader正确的起步姿势能避免一大堆莫名其妙的错误。3.1 创建URP项目与配置首先确保你有一个使用URP模板创建的项目或者已经为现有项目安装了URP包通过Package Manager。接着检查或创建一个URP Asset通常叫UniversalRP-HighQuality或类似的名字。在这个Asset里有几个关键设置需要留意Renderer List确保里面添加了正确的Renderer如Universal Renderer Data。我们创建的Shader最终会挂载到使用这个Renderer的摄像机上。Shader Stripping对于开发阶段可以暂时关闭一些Shader变体剥离选项避免某些功能被意外优化掉但发布前记得优化。3.2 创建视差卡牌着色器与材质在Project窗口中右键选择Create - Shader Graph - URP - Lit Shader Graph。命名为ParallaxCard。选择Lit是因为我们的卡牌通常需要光照哪怕是简单的UI光照。双击打开这个Shader Graph。你会看到一个默认的PBR Master节点。我们的大部分工作都将围绕这个节点展开。创建一个新的材质球将它的Shader选择为我们刚刚创建的ParallaxCard。将这个材质赋给一个简单的Quad或Plane作为我们的测试卡牌模型。3.3 构建基础属性面板我们需要在Shader Graph的Blackboard黑板中定义一系列属性这些属性将暴露给材质面板进行调节。属性名称 (显示名)类型默认值作用_BaseMap(Base Texture)Texture2DNone卡牌的基础颜色/图案贴图_BaseColor(Base Color)Color(1,1,1,1)基础颜色叠加常用于色调调整_NormalMap(Normal Map)Texture2DNone切线空间法线贴图提供基础凹凸细节_NormalStrength(Normal Strength)Vector1 (Float)1.0法线强度系数_HeightMap(Height Map)Texture2DNone灰度高度图白色凸起黑色凹陷_ParallaxStrength(Parallax Strength)Vector1 (Float)0.05视差效果的整体强度控制深度感_HeightScale(Height Scale)Vector1 (Float)0.1高度图的缩放系数影响凹凸的“陡峭”程度_Smoothness(Smoothness)Vector1 (Float)0.5光滑度影响反射高光_Metallic(Metallic)Vector1 (Float)0.0金属度对于卡牌通常设为0非金属创建完这些属性后你的Blackboard应该看起来内容丰富。接下来我们将开始连接节点实现静态视差效果。4. 静态视差效果的核心实现这是最核心的一步我们要在Shader Graph中搭建出视差偏移的计算网络。4.1 构建切线空间视线向量获取屏幕位置与摄像机方向使用Scene Position节点获取世界空间下的片元位置。使用View Direction节点获取世界空间下从片元指向摄像机的方向向量。注意这个方向需要转换到切线空间。构造TBN矩阵我们需要一个从世界空间到切线空间的转换矩阵。这可以通过Tangent Vector节点获取副切线和Normal Vector节点获取法线来构建。在Shader Graph中通常使用Transform节点选择World To Tangent空间转换并直接使用View Direction世界空间作为输入即可得到切线空间的视线向量ViewDirTS。这是一个关键技巧避免了手动构造矩阵的复杂操作。标准化与提取XY分量将得到的ViewDirTS通过Normalize节点标准化。然后使用Split节点将其拆分为X, Y, Z三个分量。视线向量的X和Y分量即切线空间的切线和副切线方向将用于UV偏移。4.2 计算视差UV偏移采样高度图使用Sample Texture 2D节点采样我们输入的_HeightMap。将采样结果的R通道灰度图通常信息在R通道作为高度值Height。应用高度缩放将Height与_HeightScale属性相乘得到缩放后的高度值ScaledHeight。这个值决定了当前像素“凸起”的绝对高度。计算初步偏移将切线空间视线向量的XY分量ViewDirTS.xy与ScaledHeight相乘。但这里有个问题直接相乘没有考虑视角倾斜度的影响即之前原理中的除法步骤。在Shader Graph中一个常见的简化/替代方法是利用视线向量的Z分量进行近似修正。我们可以用公式ParallaxOffset (ViewDirTS.xy * _ParallaxStrength * ScaledHeight) / (ViewDirTS.z 0.42)。这里的0.42是一个经验性的偏置值用于防止分母为零。在节点图中我们需要用Multiply、Add和Divide节点来组合这个计算。应用强度控制将上一步计算出的偏移量与_ParallaxStrength属性再次相乘作为最终的整体强度控制。这样我们就得到了最终的UV偏移量FinalOffset。实操心得_ParallaxStrength和_HeightScale这两个参数需要配合调节。_HeightScale更像定义了地形本身的“海拔落差”而_ParallaxStrength则是这个落差在视觉效果上的“放大镜”。一开始可以把_ParallaxStrength设小点如0.02_HeightScale设大点如0.3然后慢慢调整找到平衡。过大的_ParallaxStrength会导致严重的纹理拉伸和失真。4.3 应用偏移并采样最终纹理偏移基础UV使用Tiling And Offset节点将模型原始的UV坐标来自UV节点与计算出的FinalOffset相加得到用于采样颜色和法线贴图的“已偏移UV”OffsetUV。采样基础贴图用OffsetUV去采样_BaseMap颜色贴图和_NormalMap法线贴图。对于法线贴图采样后通常需要连接一个Normal From Texture节点并将其Strength输入与_NormalStrength属性相连以正确解码并应用法线强度。连接至Master节点将采样得到的颜色连接到PBR Master节点的Base Color输入。将处理后的法线连接到Normal输入。将_Smoothness和_Metallic属性直接连接到对应的输入口。至此一个基础的静态视差卡牌Shader就完成了。在场景中旋转摄像机你应该能看到卡牌图案根据视角产生了有深度的偏移凹凸感立现。5. 动态交互效果让卡牌响应鼠标悬停静态视差已经不错但交互能带来质的飞跃。我们希望当鼠标悬停在卡牌上时卡牌能模拟出一种被“拿起”或“聚焦”的动态倾斜效果同时视差感可以进一步增强。5.1 获取并处理鼠标交互信息在Unity中实现这类UI交互效果通常需要脚本将信息传递到Shader。这里我们采用一个简洁的方案创建交互脚本在卡牌对象比如那个Quad上挂载一个C#脚本命名为ParallaxCardInteraction。using UnityEngine; public class ParallaxCardInteraction : MonoBehaviour { private Material _material; // 用于传递鼠标在卡牌局部空间的归一化位置 (-0.5到0.5) private static readonly int MousePosID Shader.PropertyToID(_MousePos); // 用于传递悬停强度 (0-1) private static readonly int HoverIntensityID Shader.PropertyToID(_HoverIntensity); void Start() { var renderer GetComponentRenderer(); if (renderer ! null) { // 确保材质实例化避免修改共享材质 _material renderer.material; } // 初始化Shader参数 _material.SetVector(MousePosID, Vector4.zero); _material.SetFloat(HoverIntensityID, 0f); } void OnMouseEnter() { // 鼠标进入可以开始渐变增加悬停强度 // 实际使用中可能需要用Update做插值这里简化为直接设置 _material.SetFloat(HoverIntensityID, 1f); } void OnMouseExit() { // 鼠标离开取消悬停 _material.SetFloat(HoverIntensityID, 0f); _material.SetVector(MousePosID, Vector4.zero); } void OnMouseOver() { // 鼠标在对象上悬停时计算局部坐标 Ray ray Camera.main.ScreenPointToRay(Input.mousePosition); if (Physics.Raycast(ray, out RaycastHit hit) hit.collider.gameObject gameObject) { // 将碰撞点的局部坐标从(-0.5, 0.5)范围归一化到(-1, 1)或保持原样取决于你想要的效果 Vector3 localHitPos transform.InverseTransformPoint(hit.point); Vector2 normalizedPos new Vector2(localHitPos.x * 2, localHitPos.y * 2); // 假设卡牌尺寸为1x1单位 _material.SetVector(MousePosID, new Vector4(normalizedPos.x, normalizedPos.y, 0, 0)); } } }这个脚本做了三件事在鼠标进入时设置悬停强度为1离开时归零在鼠标悬停期间通过射线检测获取鼠标在卡牌模型局部空间的位置并将其传递给Shader。在Shader Graph中添加属性回到Shader Graph在Blackboard中添加两个新属性_MousePos(Vector2): 用于接收鼠标的局部坐标。_HoverIntensity(Float): 用于接收悬停强度0到1。5.2 在Shader中实现动态倾斜与视差增强动态效果的核心思路是根据_MousePos鼠标位置来动态生成一个额外的“倾斜法线”或“倾斜视线”叠加到我们原有的计算中。生成动态倾斜向量我们可以把_MousePos当作一个2D向量其方向指向鼠标偏离卡牌中心的方向其长度代表偏离程度。我们希望卡牌朝着鼠标相反的方向“倾斜”就像你用手指按住卡牌一边另一边会翘起。计算一个目标法线偏移TiltNormalOffset -_MousePos * _HoverIntensity。这里取负号是为了让卡牌向鼠标反方向倾斜。这个偏移量需要被转换到合适的方向。一个简单的方法是将它作为切线空间法线的XY分量的附加偏移。我们可以创建一个向量(TiltNormalOffset.x, TiltNormalOffset.y, 1)然后将其标准化作为一个“目标”法线方向。混合倾斜法线与原法线在Shader Graph中我们可以用Lerp线性插值节点来混合原始的法线来自法线贴图和这个动态生成的目标倾斜法线。混合系数由_HoverIntensity控制。当没有悬停时使用原法线完全悬停时使用倾斜法线。这样就能得到平滑的过渡效果。动态增强视差强度为了让悬停时立体感更强我们可以让_ParallaxStrength随着_HoverIntensity增加而增加。使用Lerp节点在_ParallaxStrength基础强度和一个更大的值如_ParallaxStrength * 2之间进行插值插值系数同样是_HoverIntensity。将动态计算出的新强度值替换掉之前静态计算中的_ParallaxStrength常量。连接与调试将混合后的新法线连接到PBR Master节点的Normal输入。确保动态计算的视差强度被应用到偏移计算中。现在运行游戏将鼠标移到卡牌上你应该能看到卡牌不仅产生了视差偏移其表面的“朝向”也会根据鼠标位置动态变化仿佛真的被手指按压一样立体感和交互反馈大大增强。注意事项动态倾斜的效果强度需要谨慎调节。过强的倾斜会导致法线突变产生不自然的光照撕裂。建议将_MousePos在传递给Shader前就进行适当的缩放比如乘以0.2到0.5的系数并在Shader中限制倾斜向量的最大长度。同时确保你的卡牌模型有足够多的面数或使用平滑的法线否则在Quad这样的低模上倾斜效果可能会显得生硬。6. 高级优化与效果增强技巧基础效果跑通后我们可以从性能和视觉两个层面进行打磨。6.1 性能优化考量纹理压缩与尺寸高度图、法线贴图可以使用BC5/DXT5nm法线和BC4/ATSC R8高度等压缩格式在保证质量的同时减少带宽。对于卡牌这种小物件512x512的分辨率通常绰绰有余甚至可以尝试256x256。Shader变体与关键字如果你的Shader包含了多种可开关的效果比如是否开启动态倾斜可以考虑使用Shader Graph的Keyword节点来生成变体避免在运行时进行复杂的条件判断。不过对于小型项目简单的Branch节点或参数插值在GPU上开销也可以接受。LOD策略如果游戏中有大量卡牌可以考虑为远距离的卡牌使用一个简化版本的Shader关闭视差或使用更廉价的模拟通过LOD Group或自定义脚本来切换材质。6.2 视觉增强手法边缘光与轮廓光在URP中可以很容易地集成屏幕空间环境光遮蔽SSAO和后处理轮廓光。但如果你想在Shader内部实现独立的边缘光可以在片元着色器最后根据视角方向与法线的点积Fresnel效应来添加一层自发光Emission模拟卡牌边缘被照亮的效果。高光形状控制卡牌材质往往不是纯粹的塑料或金属。你可以使用一张额外的粗糙度贴图Roughness Map或高光贴图Specular Map来控制不同区域的光泽度。例如让卡牌的文字部分更光滑背景部分更粗糙。在Shader Graph中只需添加一个贴图属性采样后连接到PBR Master的Smoothness输入即可。多层视差与细节对于特别复杂的卡牌设计比如有浮雕花纹、镶边可以使用两张高度图一张用于大结构的深度另一张用于细微的纹理细节。分别计算偏移后叠加或者使用不同的强度可以创造出更丰富的层次感。但这会带来额外的纹理采样开销需权衡使用。7. 常见问题与排查实录在实现过程中你几乎一定会遇到下面这些问题。这里是我的排查笔记。问题现象可能原因解决方案视差效果完全没出现1. 高度图连接错误或为纯色。2._ParallaxStrength或_HeightScale值为0。3. 视线向量计算错误未转换到切线空间。1. 检查_HeightMap属性是否指定了正确的灰度图并用一个Sample Texture 2D节点直接预览其输出确保不是纯黑/白。2. 将这两个参数调大到可见范围如0.05和0.1。3. 使用Preview功能查看Transform节点输出的切线空间视线向量其XY分量应随视角变化而变化。视差效果方向反了视线向量方向搞反。View Direction节点默认是从片元指向摄像机这是正确的。如果用了相反的方向偏移就会反向。确保使用View Direction节点而不是自己用摄像机位置减片元位置计算除非你明确知道方向。在Shader Graph中直接使用Transform节点的World To Tangent转换View Direction是最稳妥的。纹理在边缘严重拉伸或撕裂_ParallaxStrength值过大导致UV偏移量超出了纹理的合理范围。显著降低_ParallaxStrength值。同时考虑使用“视差遮挡映射”的进阶技术它通过步进采样高度图来寻找更准确的交点能有效减少边缘失真。在Shader Graph中实现步进采样需要Custom Function。动态倾斜时光照闪烁或突变动态计算出的倾斜法线没有正确标准化或者倾斜幅度太大导致法线值超出合理范围长度不为1。在将倾斜向量与原始法线混合前务必使用Normalize节点。同时限制脚本传递给Shader的_MousePos向量的最大长度例如使用Vector2.ClampMagnitude。效果在移动端很卡1. 纹理分辨率过高。2. Shader计算复杂度高特别是用了多张纹理采样和复杂向量运算。3. 没有使用合适的纹理压缩格式。1. 将所有贴图降至512x512或更低。2. 简化Shader考虑关闭动态倾斜或使用更简单的视差近似算法。3. 在Unity导入设置中为法线贴图选择“Normal map”格式为高度图选择“单通道”的压缩格式。与URP的2D Renderer或UI不兼容URP的2D Renderer和UI系统如Canvas使用不同的渲染路径和坐标空间。直接将为3D物体写的视差Shader用在UI Image上可能无效。为UI实现视差需要专门针对屏幕空间UV进行设计。一个变通方法是将UI元素渲染到一个Render Texture上然后将这个纹理应用到一个3D Quad上再使用我们的Shader。或者寻找专门为URP 2D/UI设计的视差Shader模板。一个关键的避坑技巧在调试Shader Graph时善用节点的Preview窗口。你可以把任何中间计算的结果比如视线向量、高度值、偏移量通过Branch节点或临时连接到Base Color上来可视化查看。这是定位计算错误最快的方法。例如将FinalOffset直接作为颜色输出你就能直观地看到每个像素的偏移方向和大小是否符合预期。