1. 项目概述为什么我们需要“拒接节点”在Unity ShaderGraph的世界里我们每天都在和向量打交道。无论是计算光照方向、混合纹理坐标还是实现复杂的物理模拟向量运算都是构建视觉效果的基石。然而当我们需要将一个向量“投影”到另一个向量上或者更关键地需要得到那个“被拒绝”的部分时很多开发者会感到一丝困惑。这个“被拒绝”的部分在数学上被称为向量的拒接Rejection而在ShaderGraph中就由一个名为“拒接节点”Rejection Node的数学工具来精确计算。简单来说如果你想象一个向量A斜着飞向一个平面这个平面由向量B的法线方向定义那么向量A在这个平面上的“影子”就是它在向量B方向上的投影而向量A本身与这个“影子”之间的差值也就是那个垂直于投影、真正“落在”平面上的分量就是拒接。这个操作在图形学中至关重要比如在实现卡通渲染的描边时我们常常需要计算顶点沿法线方向拒接了视线方向后的偏移量又或者在模拟布料与物体表面滑动时需要将运动速度拒接到接触面的法线上以获得切向速度。尽管概念清晰但在实际使用ShaderGraph时我发现不少朋友会绕远路他们可能会用点积、乘法、减法等一系列基础节点来手动拼凑出拒接运算过程繁琐且容易出错。这正是“拒接节点”存在的价值——它将一个常见的数学操作封装成一个清晰、高效的单一节点让Shader开发者的意图表达得更直接让Shader图更加简洁易读。今天我们就来彻底拆解这个节点从它的数学原理、在ShaderGraph中的具体表现到几个实战案例让你不仅能会用更能理解何时该用它以及如何避开使用中的那些“坑”。2. 核心原理与数学拆解拒接到底是什么要玩转拒接节点死记硬背它的连接口是不够的我们必须深入它的数学内核。只有理解了原理你才能在任何需要它的场景中毫不犹豫地将其从节点库中拖出来。2.1 向量投影与拒接的数学定义我们首先明确两个关键向量向量A和向量B。拒接运算的目标是得到向量A中“垂直于向量B”的那个分量。投影向量A在向量B上的投影记作Proj_B(A)。它的计算公式是(dot(A, B) / dot(B, B)) * B。这里dot是点积。这个公式的直观理解是先计算A在B方向上的“长度比例”点积除以B的模长平方再用这个比例去缩放向量B本身得到的就是A在B方向上的“影子”。拒接向量A相对于向量B的拒接记作Rej_B(A)。它就是向量A减去其自身在B上的投影。公式为Rej_B(A) A - Proj_B(A)。根据向量减法的几何意义拒接向量 Rej_B(A)必然与向量 B垂直。你可以这样想象向量A被分解为两个互相垂直的分量一个沿着B的方向投影另一个则完全垂直于B拒接。拒接节点输出的正是这个垂直分量。2.2 ShaderGraph节点的内部逻辑映射在Unity ShaderGraph的节点面板中拒接节点通常位于Math-Vector分类下。它有两个输入端口A和B以及一个输出端口Out。其内部实现与我们上面推导的数学公式完全一致。当你连接好A和B这个节点在背后默默地执行了以下操作计算dot(A, B)和dot(B, B)。计算投影向量Proj (dot(A, B) / dot(B, B)) * B。这里需要注意ShaderGraph的向量运算是逐分量的所以这个乘法是标量(dot(A, B) / dot(B, B))与向量B的标量乘法。计算最终输出Out A - Proj。注意一个关键的细节。当输入向量B是零向量时dot(B, B)为零会导致除以零的错误。在Shader中这通常会产生NaN非数字结果导致渲染错误如全黑或全粉。幸运的是ShaderGraph的拒接节点内部应该已经做了安全处理。但根据我的经验在编写自定义HLSL代码实现类似功能时必须手动判断并处理这种情况例如在除之前加上一个极小的值如1e-10来避免除零。在节点层面我们通常可以相信Unity已经做好了防护但了解这个潜在风险对于调试复杂Shader至关重要。2.3 与相关数学节点的对比理解一个节点常常需要把它放在上下文中对比。拒接节点最容易和两个节点混淆点积节点输出一个标量表示两个向量的方向相似度和长度乘积。它是计算拒接过程中的一个中间步骤但本身不产生向量结果。投影节点是的ShaderGraph也有一个专门的Projection节点。它直接输出我们上面提到的Proj_B(A)。拒接节点和投影节点是一对“互补”的节点A Proj_B(A) Rej_B(A)。如果你同时需要投影和拒接两个分量那么分别使用这两个节点会比用数学节点手动组装更清晰。选择使用拒接节点而非手动组装的最大优势在于可读性和维护性。一张充斥着各种乘加节点的图远不如一个明确的“Rejection”节点更能向其他开发者或未来的你传达你的设计意图。3. ShaderGraph中的实操详解从节点配置到视觉验证理论说得再多不如在ShaderGraph里实际连一下。我们通过一个简单的可视化实验来直观感受拒接节点的作用。3.1 节点参数与接口说明在ShaderGraph中创建拒接节点非常简单。在画布空白处右键选择Create Node然后在搜索框中输入“Rejection”即可找到Rejection节点。它的界面非常简洁输入端口A动态向量。这是我们需要被分解的“原始向量”。支持FloatVector 2,Vector 3,Vector 4。B动态向量。这是作为基准的“参考向量”。维度必须与A相同。输出端口Out动态向量。输出拒接结果即A - Proj_B(A)。维度与输入相同。节点属性面板通常没有需要配置的参数它是一个纯函数节点。3.2 构建一个可视化测试场景为了“看见”拒接的效果我们构建一个简单的Unlit Shader图。设置基础坐标使用Position节点空间选择World和Normal Vector节点作为我们的输入向量来源。为了演示我们假设A是物体表面点到世界原点的方向向量近似用世界位置B是表面法线。计算拒接创建Rejection节点。将Position连接至A将Normal Vector连接至B。可视化输出拒接结果是一个向量包含方向和长度。为了在颜色上可视化我们可以方案A显示方向。将Out向量通过Normalize节点归一化然后通过Vector3 to Float3拆分后直接连接到Base Color的RGB通道。这样不同的拒接方向会显示为不同的颜色。方案B显示长度。使用Length节点计算Out向量的模长得到一个标量。这个标量代表了原始向量中“偏离”法线方向的程度。可以将这个标量通过Saturate钳制到[0,1]范围然后作为灰度值或输入到一个Gradient节点生成彩色再连接到Base Color。对比验证为了加深理解可以并行创建另一套流程使用Dot Product、Multiply、Subtract等节点手动实现拒接公式并将两者的输出结果通过Subtract节点相减最后连接到Base Color。如果我们的理解正确这个差值应该始终是(0,0,0)在屏幕上显示为纯黑。通过这个测试你可以旋转场景中的物体观察颜色如何随着表面朝向的变化而变化。当表面某点法线与该点位置向量方向完全一致时比如球体顶点拒接结果为零向量模长为0对应的可视化颜色会显示为黑色如果显示长度或一个固定值如果显示方向。这正是我们期望的当向量A完全平行于向量B时其垂直于B的分量自然为零。3.3 常见连接错误与排查在实际连接中新手可能会遇到以下几个问题维度不匹配错误这是最常见的问题。如果你试图将一个Vector 3连接到只接受Float的端口或者A和B的维度不同ShaderGraph会报错。确保你理解上下游节点的数据类型。可以使用Split和Combine节点来转换维度。结果不符合预期方向错误请再次检查公式Rej_B(A) A - Proj_B(A)。减法的顺序是固定的。如果你不小心调换了A和B计算的就是Rej_A(B)即B在垂直于A方向上的分量这完全是另一个结果了。输出结果为NaN或异常值如前所述检查向量B是否可能为零向量。虽然节点内部可能已处理但如果你的B向量来自某些可能输出零值的计算例如对某些差值进行归一化失败就需要在上游逻辑中增加保护。一个简单的办法是在将B输入拒接节点前先通过Length节点判断其模长是否大于一个极小阈值如0.001如果小于则用一个安全的默认值如(0,1,0)替代。4. 核心应用场景案例解析理解了“是什么”和“怎么用”接下来就是最重要的“何时用”。拒接节点在Shader开发中应用广泛下面我分享几个最经典也最实用的案例。4.1 案例一卡通渲染Cel-Shading中的轮廓描边卡通渲染的轮廓边一种经典的实现方法是“法线外扩”。但单纯沿顶点法线方向挤出模型在平面区域会显得很粗。更高级的方法是让挤出方向拒接摄像机视线方向。原理我们希望在屏幕空间里让轮廓均匀。顶点在眼空间View Space的挤出方向如果完全使用法线那么在模型侧面法线近乎垂直于视线挤出效果明显而在模型正面法线与视线近乎平行挤出几乎无效。为了在正面也能产生轮廓我们需要一个始终垂直于视线方向的挤出向量。实现在顶点着色器阶段获取顶点法线转换到眼空间N和 从顶点指向摄像机的方向即眼空间下的顶点位置向量因为眼空间原点就是摄像机V。计算Rej_V(N)。这个结果就是法线N中垂直于视线V的那个分量。它正好是我们想要的挤出方向在模型侧面它接近原法线在模型正面它接近法线在屏幕平面上的投影从而产生挤出。将这个拒接向量归一化乘以一个描边宽度系数加到顶点眼空间位置上。将处理后的位置转换到齐次裁剪空间输出。这样实现的描边在各个视角下都能保持相对均匀的宽度效果更加稳定和专业。4.2 案例二物理模拟中的切向速度分解如布料、流体在模拟物体表面滑动如雨水沿车窗滑落、布料在角色身上滑动时我们需要将速度分解为法线方向和切向方向。垂直于表面的速度分量会导致穿透或反弹而切向分量才是滑动的动力。原理物体某点的速度向量Velocity接触面的法线Normal。速度在法线方向上的投影Proj_N(Velocity)决定了碰撞响应而拒接部分Rej_N(Velocity)就是切向速度用于计算摩擦力或驱动滑动动画。实现在Shader中例如在顶点着色器或片段着色器中处理顶点动画我们已有当前点的速度V可能来自纹理采样或参数动画和该点的表面法线N。直接使用拒接节点计算切向速度TangentialVelocity Rejection Node(V, N)。利用这个TangentialVelocity来偏移纹理坐标模拟流动、混合纹理模拟湿润痕迹或驱动顶点位置模拟布料滑动。计算摩擦力时也可以根据此切向速度的大小来动态混合表面材质如从干燥到湿润。这种方法比手动计算点积和投影更加直观Shader图逻辑一目了然速度减去其在法线上的分量剩下的就是用于滑动的部分。4.3 案例三高级UV扭曲与贴花投影有时我们希望一张贴花或纹理能沿着一个复杂表面的“趋势”方向扭曲而不是简单平铺。例如让藤蔓纹理沿着岩石的沟壑蔓延。原理我们可以定义一个主导方向向量Direction比如世界空间的向上向量(0,1,0)。对于表面每个点我们计算其法线N。然后计算Rej_N(Direction)。这个结果向量位于切平面内指示了“向上”方向在该点切平面上的投影。它可以用来扭曲UV将原始的UV坐标沿着这个拒接向量的方向进行偏移偏移量可以与法线强度或其他遮罩结合。实现获取表面法线N和定义的主导方向D如float3(0,1,0)。计算Rej_N(D)并归一化得到切平面内的方向向量FlowDir。使用FlowDir的 XZ 分量假设在水平面上来对纹理坐标进行扰动。例如float2 distortedUV originalUV FlowDir.xz * strength * mask。用distortedUV采样藤蔓纹理。这样纹理就会沿着物体表面“向上”的方向自然流动更好地贴合曲面几何。这个技巧在制作雪地堆积、污泥流向、苔藓生长等需要方向性适配表面的效果时非常有用。5. 性能考量与最佳实践虽然拒接节点非常方便但在性能敏感的场合如移动平台或全屏后处理我们也需要了解其开销并遵循最佳实践。5.1 节点开销分析一个拒接节点的内部计算包含两次点积、一次除法、一次标量乘向量、一次向量减法。在Shader中点积和基础算术运算在现代GPU上都是非常廉价的指令。因此单个拒接节点的性能开销通常可以忽略不计。性能问题的关键往往不在于节点本身而在于使用场景和频率在片段着色器中使用如果每个像素都执行一次拒接计算且计算依赖于纹理采样或复杂的上游节点那么累积的开销可能会变得显著。特别是当它被用在复杂的BRDF光照模型或密集循环中时。在顶点着色器中使用顶点数量远少于像素数量在这里进行计算通常是非常高效的。像前面提到的轮廓描边、顶点动画等应用将拒接计算放在顶点着色器是首选方案。冗余计算避免对相同的输入向量A和B重复计算拒接。如果下游多个节点都需要同一个拒接结果务必使用一个Branch节点或将其计算提前存储在一个临时变量中供复用。5.2 优化策略与替代方案预计算与烘焙对于静态或变化缓慢的向量例如基于世界位置和静态法线计算出的某个方向可以考虑在CPU端或离线工具中预计算然后通过顶点颜色、UV通道或额外的纹理贴图将结果传入Shader。这能将GPU的计算开销降至零。简化近似在某些对精度要求不高的视觉特效中如果向量B是单位向量归一化的那么拒接公式可以简化为A - dot(A, B) * B。因为dot(B, B) 1。省去了一次除法和一次额外的点积。在ShaderGraph中这意味着你可以用Dot Product、Multiply和Subtract三个节点来实现但前提是你必须确保B是归一化的。使用标准拒接节点的好处在于它总是正确的无需你手动保证输入条件。精度选择ShaderGraph允许你设置图的精度Precision。对于移动平台或不需要高精度的效果如某些颜色扭曲、简单的顶点动画将图设置为Half精度可以提升性能。这会影响图中所有计算包括拒接节点。你可以在Graph Inspector中设置整个图的精度也可以为个别节点设置覆盖精度。5.3 调试技巧与常见问题排查实录即使理解了原理和应用在实际开发中依然会遇到各种问题。下面是我在项目中总结的一些排查经验。问题一效果在特定角度下突然消失或闪烁。排查思路这极有可能是由向量B长度为零或接近零导致的除零或数值不稳定。首先检查生成向量B的逻辑。例如B是否是两个位置相减的结果这两个位置在特定情况下是否会重合添加一个调试输出将Length(B)的结果可视化到颜色上观察闪烁时该值是否接近0。解决方案在B输入拒接节点前进行安全处理。可以使用一个Maximum节点SafeB B (1e-10)但这可能改变方向。更好的做法是使用条件节点或函数SafeB Select(Length(B) 1e-5, DefaultVector, B)其中DefaultVector是一个安全的非零向量如(0,0,1)。问题二使用拒接节点实现的描边在模型内部背面也可见。排查思路这是卡通描边的经典问题。你的拒接计算可能没有考虑背面剔除。在顶点着色器中我们通常只处理正面或通过Cull Off关闭剔除。但挤出方向可能错误地指向了模型内部。解决方案在计算挤出方向时需要根据顶点是正面还是背面来调整。一种常见方法是使用dot(N, V)法线与视线点积。在眼空间对于正面三角形此值通常大于0因为法线指向摄像机。你可以根据此值的正负来反转挤出方向或者更简单地在计算拒接前确保使用的法线方向是面向摄像机的通过faceforward函数或手动判断。问题三拒接计算的结果向量长度远大于预期。排查思路检查输入向量A和B的量级模长。拒接结果的模长不仅取决于A和B之间的夹角也取决于A本身的模长。如果A是世界位置值可能非常大那么拒接结果也会非常大直接用于偏移UV或顶点位置会导致剧烈变形。解决方案在大多数应用场景中我们更关心拒接的方向而非其原始长度。因此在得到拒接结果R后通常紧接着会进行Normalize(R)操作得到一个单位方向向量。如果需要控制强度再乘以一个手动定义的标量系数。这是一个非常重要的经验拒接节点输出的是带有原始长度的向量而我们在90%的情况下真正需要的是它的方向。最后分享一个我个人的调试习惯在开发复杂的ShaderGraph时我会专门创建一个“调试输出”分支。在这个分支里我会把拒接节点的输入A、B中间变量如点积结果以及最终输出Out分别通过不同的映射方式如直接显示为颜色、显示长度、只显示单个通道连接到Base Color或Emission。通过切换不同的调试视图我可以快速定位是哪个输入出了问题或者计算在哪一步偏离了预期。磨刀不误砍柴工构建一个好的调试流程能极大提升Shader开发的效率。