1. 项目概述:为什么我们需要一个专门的圆角矩形节点?
在ShaderGraph的世界里,绘制一个矩形是基础中的基础,但当你想要一个带圆角的矩形时,事情就变得微妙起来。很多刚接触Shader开发的朋友可能会想:我直接用SDF(有向距离场)公式手写一个不就行了?确实,一个基础的圆角矩形SDF公式并不复杂。但Unity在ShaderGraph中内置这个Rounded Rectangle Node,其意义远不止是提供一个“快捷方式”。它封装了一套经过优化、考虑周全的算法,特别是在处理抗锯齿(Anti- Aliasing)和UV空间变换时,能帮你避开无数新手甚至老手都容易踩的坑。
这个节点的核心价值在于“标准化”和“可组合性”。它接受明确的宽度(Width)、**高度(Height)和圆角半径(Radius)**参数,输出一个在[0,1]范围内的灰度图(Alpha遮罩)。这意味着你可以像搭积木一样,将它与其他节点(如Tiling And Offset、Gradient、Color Blend)快速组合,创造出按钮、头像框、进度条、平滑的光晕遮罩等UI元素或游戏内特效,而无需从零开始推导和调试SDF公式。对于需要快速迭代视觉效果的TA(技术美术)或图形程序员来说,这能节省大量时间。
2. 节点核心原理与数学拆解
要真正用好一个节点,不能只停留在“连接输入输出”的层面。理解其内部的数学原理,能让你在参数调节、问题排查和效果扩展时游刃有余。
2.1 输入参数的空间定义
首先,我们必须明确节点所有计算都发生在标准的UV空间(即[0,1]范围)内。这是理解其行为的关键。
- UV:默认是Shader的纹理坐标。节点期望的输入UV范围通常是[0,1],其原点(0,0)对应矩形的左下角。
- Width 与 Height:这两个参数定义的并非像素宽度,而是在UV空间中的尺寸比例。例如,Width=0.5, Height=0.3,意味着这个圆角矩形在水平方向上占据UV空间的一半宽度,在垂直方向上占据30%的高度。它们的值通常应小于1,且
Width + 2*Radius和Height + 2*Radius理论上不应超过1,否则形状会超出UV边界。 - Radius:同样是在UV空间中定义的圆角半径。它控制着四个角的弯曲程度。这里有一个极易被忽略的关键点:在内部代码中,半径会被乘以2进行处理(
Radius * 2)。这意味着你输入的Radius参数,其视觉效果上接近直径的感知。这是为了数学计算的便利性,但我们在调节时需要心中有数。
2.2 内部算法逐步解析
让我们结合官方文档中给出的那个简洁而强大的函数,一步步拆解:
void Unity_RoundedRectangle_float(float2 UV, float Width, float Height, float Radius, out float Out) { // 步骤1:半径的安全限制 Radius = max(min(min(abs(Radius * 2), abs(Width)), abs(Height)), 1e-5); // 步骤2:坐标变换与边框距离计算 float2 uv = abs(UV * 2 - 1) - float2(Width, Height) + Radius; // 步骤3:计算到边框的符号距离 float d = length(max(0, uv)) / Radius; // 步骤4:应用抗锯齿平滑 Out = saturate((1 - d) / fwidth(d)); }步骤1:半径的安全限制这行代码是算法的“安全阀”。它做了三件事:
abs(Radius * 2):取半径绝对值的两倍。min(min(..., abs(Width)), abs(Height)):确保这个值不超过Width和Height的绝对值。这是为了防止圆角半径大于矩形短边的一半,导致形状变得不可预测(例如,变成一个圆形或出现奇怪的交叠)。max(..., 1e-5):最后,确保半径不小于一个极小的正值(1e-5)。这是为了避免除零错误,因为后续计算中Radius会作为分母。这就是为什么有时候你把Radius调到0,依然能看到微小圆角的原因。
步骤2:坐标变换与边框距离计算float2 uv = abs(UV * 2 - 1) - float2(Width, Height) + Radius;这是整个算法的精髓,它巧妙地将UV空间从[0,1]映射到了以中心为原点的坐标系,并计算到矩形内框的距离。
UV * 2 - 1:将UV从[0,1]映射到[-1, 1]。此时,UV空间中心点(0.5, 0.5)变成了新坐标系的原点(0, 0)。abs(...):取绝对值。这个操作非常关键,它利用了矩形的对称性。经过这一步,我们只需要处理第一象限(即x, y都为正)的情况,因为其他三个象限可以通过对称性得到。此时,UV坐标表示的是当前点到中心轴(X轴和Y轴)的距离。- float2(Width, Height):减去矩形的半宽和半高。想象一下,在第一象限,矩形的右上角顶点坐标原本应该是(Width, Height)。减去之后,如果点(x, y)在矩形内部(非圆角区域),则uv.x和uv.y至少有一个会变为负数。+ Radius:加上半径。经过这个操作,uv向量现在表示的是:从当前点指向矩形直角边框(向内收缩了Radius距离后形成的更小矩形)的向量。如果这个向量的分量有负值,说明该点在“更小矩形”的内部。
步骤3:计算到边框的符号距离float d = length(max(0, uv)) / Radius;
max(0, uv):将uv向量的负分量钳制为0。这意味着,对于在“更小矩形”内部的点,我们只关心其到原点的距离(此时uv的某个分量为负,被置0,length计算的是到坐标轴的距离);对于在“更小矩形”外部的点,uv的两个分量都为正,length计算的就是到“更小矩形”顶点的向量长度。length(...) / Radius:将这个距离除以半径进行归一化。当d < 1时,点在圆角范围内;d = 1时,点在圆角边界上;d > 1时,点在圆角范围外或矩形平坦边上。
步骤4:应用抗锯齿平滑Out = saturate((1 - d) / fwidth(d));这是生成平滑边缘(抗锯齿)的标准SDF处理技巧。
1 - d:将距离场反转。现在,值越接近1表示越靠近形状内部,越接近0表示越外部。fwidth(d):这是一个非常重要的函数,它估算当前像素在屏幕空间(或纹理空间)中d值的变化率(近似于偏导数的绝对值之和)。简单理解,它给出了在边缘区域d值变化的“快慢程度”。(1 - d) / fwidth(d):这个除法操作,在边缘处(d在1附近)会生成一个从大于1到小于0的过渡值。saturate函数将其钳制在[0,1]范围内。最终效果是:在边缘处,输出值(Alpha)会在几个像素的宽度内从1平滑过渡到0,实现了完美的抗锯齿,无论形状放大还是缩小。
实操心得:很多自写的SDF形状边缘有锯齿,就是因为缺少了
fwidth这一步抗锯齿处理。Unity的这个节点帮你完美封装了,这也是推荐使用内置节点而非手写简单公式的重要原因之一。
3. 节点参数详解与实战配置
理解了原理,我们再来看看每个输入端口应该如何连接和配置,以及一些常见的组合技巧。
3.1 基础参数配置与视觉关系
创建一个Rounded Rectangle节点,你会看到四个输入口。最基础的用法是直接赋予常量。
- UV:通常连接
UV节点或经过处理的UV。保持默认[0,1]范围即可绘制一个居中的形状。 - Width/Height:控制矩形的核心形状。它们的值相对于UV空间。
- 视觉比例:如果你想要一个屏幕空间内保持固定宽高比的矩形(比如16:9),需要根据屏幕宽高比来动态计算其中一个值。例如,在UI场景中,你可能需要用一个
Aspect Ratio节点来修正。 - 常见误区:认为Width=1, Height=1会填满整个UV空间。这是错误的。因为算法中
UV * 2 - 1的变换,当Width=1时,矩形的边缘会刚好触及UV空间的边界(左/右或上/下)。要填满,需要结合Offset。
- 视觉比例:如果你想要一个屏幕空间内保持固定宽高比的矩形(比如16:9),需要根据屏幕宽高比来动态计算其中一个值。例如,在UI场景中,你可能需要用一个
- Radius:控制圆角大小。它的最大值受限于
min(Width, Height)/2。当Radius等于这个最大值时,如果Width等于Height,你会得到一个完美的圆形;如果不等,你会得到一个“体育场”形状(两端是半圆的胶囊形)。
一个快速验证的配置:将Width设为0.6, Height设为0.4, Radius设为0.1。连接到Unlit Master节点的Color,你会看到一个灰色的圆角矩形。将输出连接到Alpha,并将表面类型(Surface Type)改为Transparent,你就能看到一个透明的圆角矩形遮罩。
3.2 动态控制与节点组合
静态参数只是开始,动态控制才能发挥Shader的威力。
- 动态圆角:将
Radius端口连接一个Time节点或Sine Time节点,你可以创建圆角周期性脉动的效果,非常适合用于科幻UI或能量护盾的指示器。 - 形状变形:将
Width和Height连接到同一个控制器,可以实现等比例缩放。分别控制则可以实现拉伸动画。 - 与Tiling And Offset节点的组合:这是官方文档特别强调的一点。如果你想平铺(重复)多个圆角矩形,不能直接将Rounded Rectangle节点的输出连接到
Tiling And Offset。正确的做法是:将Tiling And Offset节点连接到Rounded Rectangle节点的UV输入口。这样,UV坐标本身被平铺和偏移,每个UV区间内都会独立生成一个圆角矩形。- 错误做法:
UV->Rounded Rectangle->Tiling And Offset-> 输出。这会导致整个生成的形状被拉伸平铺,边缘会断裂。 - 正确做法:
UV->Tiling And Offset->Rounded Rectangle-> 输出。这样每个平铺格子内都有一个完整的圆角矩形。
- 错误做法:
- 创建边框:利用
Step或Smoothstep节点可以轻松创建边框。例如,将Rounded Rectangle的输出连接到一个Smoothstep节点的Edge1和Edge2,通过调节这两个阈值,可以提取出形状的边缘区域,然后赋予不同的颜色。
同时,用另一个[Rounded Rectangle Out] --> [Smoothstep.Edge1] [Smoothstep.Edge2] (设为Edge1 + 一个小的偏移量,如0.05) [Smoothstep.Out] --> [边框颜色]Smoothstep提取内部填充(Edge1为0,Edge2为略小于1的值),两者相加或叠加,就得到了一个带边框的圆角矩形。
4. 高级应用与效果延伸
掌握了基础,我们可以探索一些更高级、视觉效果更丰富的应用。
4.1 实现渐变与纹理填充
圆角矩形节点输出的是一个单通道的遮罩(Alpha),这使其成为完美的“蒙版”。
- 线性渐变填充:创建一个
Linear Gradient节点,将其Out端口连接到一个Multiply节点的一个输入,将Rounded Rectangle的Out连接到Multiply的另一个输入。这样,渐变只会在圆角矩形区域内显示。通过旋转Linear Gradient的Rotation角度,可以改变渐变方向。 - 径向渐变填充:使用
Radial Gradient节点代替线性渐变,可以创建从中心向四周发散的光晕效果,常用于按钮高光。 - 纹理采样填充:使用
Sample Texture 2D节点采样一张纹理(如金属拉丝、布料),将其颜色输出与Rounded Rectangle的遮罩输出进行Multiply操作。这样,纹理就被精确地裁剪在圆角矩形框内。结合UV的缩放和偏移,可以实现纹理在框内的滚动动画。
4.2 构建复杂UI元素
结合多个图形节点,可以构建出游戏中的常见UI。
- 进度条:使用两个Rounded Rectangle节点。一个作为背景条(灰色,较大圆角),一个作为前景填充条(蓝色,相同或稍小圆角)。将前景条的
Width输入连接到一个Remap节点,该节点将你的进度值(如0到1)映射到合适的宽度范围(如从0到背景条的Width值)。对前景条应用一个鲜艳的Linear Gradient,就能做出动态填充的进度条。 - 头像框:将Rounded Rectangle节点作为遮罩,与一个
Circle节点(或另一个Radius更大的Rounded Rectangle)进行Subtract(相减)操作,可以挖空中间部分,只留下边框。然后在边框上叠加纹理或发光效果。 - 平滑的对话气泡:对话气泡通常有一个矩形主体和一个三角形尾巴。你可以用Rounded Rectangle做主体,再用一个
Polygon节点或自定义SDF绘制三角形,最后用Maximum(并集)或Add节点将两者平滑地融合在一起。调节融合区域的抗锯齿参数是关键。
4.3 融入3D物体表面
Rounded Rectangle不仅用于UI,也可以用于3D物体表面的图案绘制。
- 物体表面贴花:将3D物体的世界空间或物体空间坐标,通过一系列变换(通常包括投影到某个平面、缩放和偏移)映射到UV坐标,再输入给Rounded Rectangle节点。这样就可以在物体的特定区域(如盔甲的胸甲、地板的中心)绘制一个圆角矩形徽章或标记。
- 边缘发光(Rim Light)的遮罩:将Rounded Rectangle的遮罩与基于法线和视角计算的边缘光因子相乘,可以限制边缘光只出现在特定的圆角矩形区域内,创造出一种“能量从面板缝隙中渗出”的科技感效果。
5. 常见问题、性能分析与优化技巧
在实际项目中使用这个节点,你可能会遇到一些典型问题。
5.1 常见问题排查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 形状不显示或显示不全 | Width/Height参数过大(接近或超过1),导致形状超出UV的[0,1]范围。 | 检查Width和Height值,确保它们远小于1(如0.8以内),为UV变换留出空间。使用Fraction节点处理UV。 |
| 圆角不圆或形状怪异 | Radius值超过了min(Width, Height)/2,被内部算法钳制。 | 减小Radius值,确保其小于矩形短边的一半。 |
| 边缘有锯齿(Aliasing) | 在极少数情况下,如果UV输入本身有剧烈变化(如来自屏幕坐标),fwidth计算可能不够精确。或者是在移动设备上,没有启用合适的抗锯齿。 | 确保UV输入是平滑连续的。对于屏幕后处理效果,可以考虑使用ddx/ddy手动计算更精确的导数。在Unity项目设置中开启MSAA或FXAA。 |
| 平铺(Tiling)时形状断裂 | 错误地将节点输出进行平铺,而不是将UV输入进行平铺。 | 严格按照“正确做法”:将Tiling And Offset节点连接在Rounded Rectangle节点的UV输入之前。 |
| 在粒子系统或Line Renderer中无效 | Rounded Rectangle节点设计用于片元着色器阶段,且依赖UV。某些渲染器可能不提供标准UV。 | 检查渲染组件是否提供了合适的纹理坐标(如TEXCOORD0)。可能需要自定义顶点着色器传递UV。 |
| 性能开销突然增大 | 在循环或非常复杂的节点网络中过度使用该节点,或者将其用于全屏幕后处理。 | 该节点本身计算不重,但应避免每帧每像素进行多次调用。对于静态UI,考虑将结果烘焙到纹理中。对于动态效果,评估其必要性。 |
5.2 性能考量与优化建议
Rounded Rectangle节点在Shader中属于计算成本较低的一类。它的计算量主要在于几次abs、max、min、length和一次fwidth调用,这些都是现代GPU擅长处理的简单算术运算。
- 优化建议一:复用计算结果。如果你的Shader中需要多个相同尺寸但不同位置的圆角矩形(比如网格布局的按钮),可以考虑只计算一次基础形状,然后通过变换UV坐标来“移动”这个形状,而不是实例化多个节点。
- 优化建议二:权衡精度与性能。在移动平台或VR项目中,如果对边缘质量要求不是极端苛刻,可以尝试用一个更简单的、没有
fwidth抗锯齿的版本(比如直接用step(0.5, 1-d))来替代,但这会带来明显的锯齿。通常不建议这样做,因为内置的fwidth开销很小,带来的视觉提升却很大。 - 优化建议三:降采样处理。对于作为全屏遮罩或大幅面背景的圆角矩形,如果性能吃紧,可以考虑在较低分辨率下渲染这个遮罩,然后上采样(upscale),利用其本身的平滑特性,视觉损失可能很小。
5.3 调试技巧
当效果不符合预期时,可以尝试以下调试方法:
- 可视化中间变量:创建一个临时的Unlit Shader Graph,将Rounded Rectangle节点内部的中间变量(如变换后的
uv向量、距离d)通过Remap节点映射到颜色上输出。这能帮你直观地看到距离场是如何分布的。 - 分离测试:将复杂的节点网络拆开,先单独测试Rounded Rectangle节点,确保其基础功能正常。然后再逐步添加其他节点(如Tiling、Gradient),每次添加一个,观察变化。
- 检查UV流向:在Shader Graph中,使用
Custom Function节点或编写简单的HLSL代码,将输入的UV坐标直接作为颜色输出(return float4(UV, 0, 1);)。这样可以确认传入节点的UV坐标是否是你期望的范围和流向。
我个人在大量UI和VFX项目中频繁使用这个节点。它最让我欣赏的一点是可靠性和一致性。无论屏幕分辨率如何变化,它生成的抗锯齿边缘总是那么平滑。比起在网上找到的各种SDF代码片段,这个经过Unity官方测试和优化的节点,在跨平台兼容性和性能上更让人放心。记住,图形编程不仅是创造炫酷效果,更是关于在性能、质量和开发效率之间找到最佳平衡。Rounded Rectangle节点正是这样一个优秀的平衡点。