尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Unity Shader实战:从Android shape标签到可编程渲染,手把手实现圆角边框

Unity Shader实战:从Android shape标签到可编程渲染,手把手实现圆角边框 1. 项目概述为什么要在Unity里重写Android的shape标签如果你是从Android原生开发转战Unity或者你的项目需要一套与现有Android应用UI风格高度统一的界面那么你大概率对Android XML里的shape标签又爱又恨。爱的是它声明式的简洁几行代码就能定义出圆角矩形、渐变背景、描边边框恨的是当这套UI需要无缝迁移到Unity的UGUI或GUI系统中时你会发现Unity并没有提供一个开箱即用的、功能对等的“Shape”组件。这个项目标题——“Unity Shader实战从Android的shape标签到可编程渲染手把手教你写圆角边框Shader”——直指的就是这个痛点。它的核心目标不是简单地用图片替代而是通过编写一个可编程的Shader在Unity中动态、高性能地复现甚至超越Androidshape标签的核心功能尤其是最常用的“圆角边框”效果。这意味着你的UI元素不再依赖纹理图集可以任意缩放而不失真颜色、圆角半径、边框粗细都能在运行时通过脚本动态调整极大地提升了UI的灵活性和开发效率。为什么非得用Shader因为这是图形渲染的底层语言。UGUI的Image组件默认只支持简单颜色和Sprite要实现复杂的、可动态变化的图形效果Shader是最高效、最直接的途径。它直接在GPU上运行避免了CPU绘制到纹理再提交的开销。通过这个项目你将不仅学会一个特效的写法更能深入理解片元着色器Fragment Shader如何通过数学计算来“画”出我们想要的形状这是从使用引擎工具到掌握渲染核心能力的关键一步。2. 核心思路拆解从声明式XML到过程式ShaderAndroid的shape是声明式的你定义属性如corners radiusstroke width系统帮你渲染。Unity Shader是过程式的你需要用代码HLSL/GLSL告诉GPU每一个像素该如何着色。因此我们的核心思路就是将shape的“属性描述”翻译成Shader中的“数学判断”。2.1 Android Shape标签的功能映射一个典型的实现圆角边框的Android XMLshape可能包含以下节点shape xmlns:androidhttp://schemas.android.com/apk/res/android solid android:color#FFFF0000 / !-- 填充色 -- corners android:radius20dp / !-- 圆角半径 -- stroke android:width4dp android:color#FF000000 / !-- 边框 -- /shape对应到我们的Shader需要暴露以下几个可调节的参数Properties_MainColor(Color): 对应solid的填充色。_BorderColor(Color): 对应stroke的边框颜色。_BorderWidth(Float): 对应stroke的边框宽度。这里需要注意单位转换在Shader中我们通常在归一化的UV空间0到1或基于实际像素来定义。_Radius(Float): 对应corners的圆角半径。同样我们需要确定其计算基准。2.2 核心算法距离场与阶梯函数这是实现圆角边框的数学核心。我们不在Shader里“画圆”而是通过计算每个像素点到图形边界的“距离”来判断它属于哪个区域。第一步定义圆角矩形区域。假设我们有一个从(0,0)到(1,1)的矩形UV空间。一个圆角矩形可以看作是在四个角被“切”掉了半径为R的圆形区域。对于UV空间中的任意一点p我们如何判断它是否在圆角矩形内部呢 一个经典技巧是我们不在整个矩形上做判断而是将UV空间“收缩”到每个角的象限进行独立判断。 更通用且高效的方法是使用符号距离函数SDF。对于中心在(0.5, 0.5)宽高为1的矩形其带圆角的SDF可以近似通过以下步骤计算将UV坐标p从(0,1)映射到(-0.5, 0.5)使得矩形中心在原点。计算点p到矩形无圆角时的最近距离d max(abs(p.x), abs(p.y)) - 0.5。这得到了一个到矩形边界的“有符号距离”内部为负外部为正。对圆角进行处理我们可以将矩形的边界“向内推”一个半径R然后判断p到这条新边界的距离。但更直观的是在四个角我们用圆形距离函数替代矩形距离函数。实现上我们可以先计算一个“偏移向量”q abs(p) - (0.5 - _Radius)。如果q的分量小于0说明该点不在角的区域内距离由另一轴决定如果q的分量都大于0说明在角的扇形区域内距离就是length(q) - _Radius。第二步利用SDF区分填充区、边框区和外部区。计算出当前像素的SDF值sdf后内部为负值外部为正值如果sdf -_BorderWidth则该点位于填充区应使用_MainColor。如果-BorderWidth sdf 0则该点位于边框区应使用_BorderColor。如果sdf 0则该点位于图形外部应被裁剪透明。这个判断可以通过smoothstep或step函数来实现smoothstep能产生边缘抗锯齿效果更柔和。实操心得抗锯齿是关键直接使用step进行硬切割会在图形边缘产生难看的锯齿尤其在移动设备上。务必使用smoothstep并引入一个很小的平滑范围如0.5个像素让颜色在边界附近有一个渐变过渡。这是高质量UI Shader与玩具级Demo的本质区别之一。计算时可以将距离值转换到像素空间来确定这个平滑范围这样抗锯齿效果在不同分辨率下都是一致的。3. Shader代码实现与逐行解析我们将创建一个Unlit Shader因为它不涉及光照最适合UI效果。下面是一个基于URPUniversal Render Pipeline的Shader代码框架并附上详细注释。3.1 Properties属性块与变量声明Shader UI/Shape/RoundedBorder { Properties { [MainColor] _MainColor (Fill Color, Color) (1,1,1,1) _BorderColor (Border Color, Color) (0,0,0,1) _BorderWidth (Border Width, Range(0, 0.5)) 0.05 _Radius (Corner Radius, Range(0, 0.5)) 0.1 // 添加一个参数来控制抗锯齿的宽度基于像素 _AAWidth (Anti-Alias Width (Pixels), Float) 1.0 }[MainColor]标签让_MainColor在Material面板中显示为主要颜色这是UGUI的惯例。_BorderWidth和_Radius的范围设为[0, 0.5]是因为在归一化的UV空间中最大值不能超过0.5从中心到边缘的距离。_AAWidth是一个重要的实战参数。它定义了抗锯齿过渡的像素宽度。将其暴露给美术或策划可以微调不同屏幕密度下的边缘柔和度。3.2 顶点着色器与UV传递SubShader { Tags { RenderTypeTransparent QueueTransparent RenderPipelineUniversalPipeline} Blend SrcAlpha OneMinusSrcAlpha ZWrite Off Cull Off Pass { HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionHCS : SV_POSITION; float2 uv : TEXCOORD0; }; Varyings vert (Attributes IN) { Varyings OUT; OUT.positionHCS TransformObjectToHClip(IN.positionOS.xyz); OUT.uv IN.uv; return OUT; }这部分是标准流程。顶点着色器将模型空间顶点位置转换到齐次裁剪空间并简单传递UV坐标。注意我们设置了透明的渲染队列Queue、混合模式Blend并关闭了深度写入ZWrite Off这是UI元素的常见设置。3.3 核心片元着色器与SDF计算// 在HLSLPROGRAM后声明与Properties对应的变量 CBUFFER_START(UnityPerMaterial) half4 _MainColor; half4 _BorderColor; half _BorderWidth; half _Radius; float _AAWidth; CBUFFER_END // 核心函数计算圆角矩形的有符号距离场SDF float sdRoundedBox(float2 p, float2 b, float r) { // p: 当前点坐标中心在原点 // b: 矩形半宽高即从中心到边的距离 // r: 圆角半径 float2 q abs(p) - b r; // 如果q.x和q.y都小于0说明点在内部矩形区域距离为max(q.x, q.y) // 否则点在角的区域距离为length(max(q,0)) - r return length(max(q, 0.0)) min(max(q.x, q.y), 0.0) - r; } half4 frag (Varyings IN) : SV_Target { // 1. 将UV从[0,1]映射到[-0.5, 0.5]中心在(0,0) float2 uv IN.uv - 0.5; // 矩形的半宽高。我们假设Mesh是一个从(0,0)到(1,1)的矩形所以半宽高是0.5。 // 但为了支持非正方形可以传入实际缩放这里简化处理。 float2 halfSize float2(0.5, 0.5); // 2. 计算当前像素到圆角矩形边界的SDF距离 // 注意_Radius和_BorderWidth是在归一化UV空间的值。 float distance sdRoundedBox(uv, halfSize, _Radius); // 3. 将抗锯齿宽度从像素空间转换到UV空间。 // _ScreenParams.xy是屏幕分辨率。这一步是抗锯齿效果一致性的关键。 float pixelToUV 1.0 / min(_ScreenParams.x, _ScreenParams.y); float aaRange _AAWidth * pixelToUV; // 4. 利用smoothstep进行平滑区域判断 // 判断填充区域distance -_BorderWidth float fillAlpha 1.0 - smoothstep(-_BorderWidth - aaRange, -_BorderWidth aaRange, distance); // 判断边框区域distance 0 float borderAlpha 1.0 - smoothstep(-aaRange, aaRange, distance); // 整体形状Alphadistance 0 float shapeAlpha 1.0 - smoothstep(-aaRange, aaRange, distance); // 5. 混合颜色 half4 fillColor _MainColor * fillAlpha; // 边框颜色只存在于边框区域且需要减去填充区域 half4 borderColor _BorderColor * (borderAlpha - fillAlpha); half4 finalColor fillColor borderColor; // 最终Alpha由形状Alpha决定 finalColor.a * shapeAlpha; return finalColor; } ENDHLSL } } }代码关键点解析sdRoundedBox函数这是从社区优化过的SDF函数非常简洁高效。它同时处理了矩形内部和圆角外部的距离计算。抗锯齿空间转换pixelToUV的计算至关重要。1.0 / _ScreenParams.y大致是UV空间中一个像素的高度。我们用min(_ScreenParams.x, _ScreenParams.y)来适应横竖屏确保抗锯齿宽度在不同方向上表现一致。这样无论屏幕分辨率是1080p还是4K_AAWidth1.0永远代表1个像素的平滑过渡。三层Alpha混合这是实现“填充色边框色”透明叠加的核心逻辑。我们分别计算了fillAlpha纯填充区域、borderAlpha整个边框填充区域和shapeAlpha整个图形区域。最终边框颜色是borderAlpha - fillAlpha这确保了填充区域不会被边框色覆盖。性能考量片元着色器中的计算非常轻量只有几次简单算术运算和一次length计算即使在低端移动设备上性能开销也可以忽略不计。注意事项UV与Mesh的匹配这个Shader假设输入的Mesh是一个标准的、UV完美映射从(0,0)到(1,1)的矩形。如果你在Unity中使用默认的Quad或UGUI的Image组件这一点是满足的。但是如果你使用了Image的Preserve Aspect保持宽高比功能或者对RectTransform进行了非均匀缩放可能会导致UV实际覆盖的区域与矩形视觉区域不匹配从而影响圆角效果。一种更健壮的方案是在顶点着色器中传递缩放后的尺寸信息或者在片元着色器中通过ddx/ddy估算像素空间的大小来动态计算半径。对于大多数UI场景保持Mesh为1:1缩放即可。4. 在Unity中的使用与优化技巧编写完Shader后我们需要在Unity中创建材质并使用它。4.1 创建材质与参数调节在Project视图中右键 - Create - Material命名为“UIRoundedBorder”。将新创建的Material的Shader选择为我们刚编写的“UI/Shape/RoundedBorder”。现在你可以在材质Inspector面板上调节所有参数Fill Color: 中心填充颜色。Border Color: 边框颜色。Border Width: 边框宽度归一化值0.05通常是个不错的起点。Corner Radius: 圆角半径归一化值0.1对应10%的圆角。Anti-Alias Width: 抗锯齿像素宽度设为1.0或1.5在大多数情况下效果很好。4.2 应用于UGUI有两种主要方式方式一RawImage组件创建一个RawImage游戏对象将“UIRoundedBorder”材质拖拽给它。这是最直接的方式但RawImage不支持Image的精灵类型和动画。方式二修改Image的材质推荐这是更集成化的方式。将材质赋给一个标准Image组件的Material属性。同时你需要确保Image的Source Image为空或者使用一个纯白色的Sprite用于提供Mesh。为了正确显示你可能需要将该材质的Shader的Render Queue调整到TransparentXXX以确保在UI层正确渲染。4.3 通过脚本动态控制Shader参数可以通过脚本动态修改这是实现交互效果如按钮高亮、进度条填充的威力所在。using UnityEngine; using UnityEngine.UI; public class DynamicShapeController : MonoBehaviour { public Material shapeMaterial; // 拖拽赋值 public float animationSpeed 1.0f; private float _currentRadius; private bool _expanding true; void Start() { if (shapeMaterial ! null) { // 获取初始半径 _currentRadius shapeMaterial.GetFloat(_Radius); } } void Update() { if (shapeMaterial null) return; // 示例动态改变圆角半径 if (_expanding) { _currentRadius Time.deltaTime * animationSpeed * 0.1f; if (_currentRadius 0.25f) _expanding false; } else { _currentRadius - Time.deltaTime * animationSpeed * 0.1f; if (_currentRadius 0.05f) _expanding true; } // 限制范围 _currentRadius Mathf.Clamp(_currentRadius, 0.05f, 0.25f); // 应用修改到材质实例 shapeMaterial.SetFloat(_Radius, _currentRadius); // 示例点击时改变边框颜色 // 这部分逻辑可以放在Button的OnClick事件中 } // 一个公共方法供UI按钮事件调用 public void SetBorderColor(Color newColor) { if (shapeMaterial ! null) { shapeMaterial.SetColor(_BorderColor, newColor); } } }重要提示材质实例化直接修改Material属性会影响所有使用该材质的对象。在运行时动态修改材质属性时务必先使用MaterialPropertyBlock或者调用renderer.material这会创建一个新的材质实例来避免“牵一发而动全身”。对于UGUI的Graphic组件如Image可以使用graphic.materialForRendering来获取当前渲染用的材质实例进行修改。4.4 性能优化与批处理合批Batching使用相同材质的UI元素更容易进行合批减少Draw Call。确保动态修改材质属性时如果参数相同尽量使用同一个材质实例。避免每帧修改虽然Shader本身很高效但通过脚本每帧调用SetFloat或SetColor仍会有一定的CPU开销。仅在值真正发生变化时更新。针对移动平台确保Shader中使用的精度half,float合适。对于颜色和0-1范围内的参数使用half通常足够且更高效。复杂的数学函数如pow、sin等要谨慎使用在我们的Shader中只用了最基本的运算所以没有问题。5. 常见问题排查与进阶扩展即使代码正确在实际使用中也可能遇到一些预期之外的情况。5.1 问题排查速查表问题现象可能原因解决方案圆角显示为直角_Radius值设置过小或Mesh UV范围不是[0,1]。检查材质面板的_Radius值尝试调大到0.1以上。检查3D模型或UI元素的UV。边框不显示或显示不全_BorderWidth值过大接近或超过0.5。抗锯齿过度平滑导致边框视觉变淡。确保_BorderWidth_Radius 0.5。适当减小_AAWidth如从1.0调到0.7。边缘有锯齿未开启抗锯齿或_AAWidth设置过小。确保_AAWidth大于0。通常设置为0.5-2.0像素之间。检查Unity项目质量设置中的抗锯齿是否开启虽然后处理AA不影响此Shader的内部计算。颜色显示不正确颜色空间问题。在Linear空间下Inspector中拾取的颜色与Gamma空间观感不同。在Shader中明确处理颜色空间。URP默认是Linear空间可以在颜色属性上加[Gamma]标签或确保美术在Linear空间下选色。简单测试将颜色值设为纯白(1,1,1,1)看是否正常。在Game视图正确Build后出错Shader编译目标级别不支持某些函数或语法。为Shader添加编译指令如#pragma target 3.0确保兼容性。检查Unity日志中的Shader编译错误。UI元素互相遮挡错误透明渲染排序问题。检查Shader中Tags的QueueTransparent以及ZWrite Off设置。确保UI Canvas的渲染顺序正确。5.2 功能进阶扩展基础圆角边框实现了但这个Shader的框架潜力巨大可以轻松扩展出Androidshape标签的其他特性渐变填充Gradient Android的gradient节点支持线性、径向、扫描渐变。我们可以在Shader中添加一个_GradientColor和_GradientType参数。在片元着色器中根据UV的x或y坐标或者到中心点的距离使用lerp函数在_MainColor和_GradientColor之间进行插值替代固定的_MainColor。虚线边框Dash 实现虚线边框需要更多的片段控制。思路是在计算出边框区域后根据像素点沿着边框路径的“长度”可以通过角度和半径计算使用frac或sin函数进行周期性的裁剪Alpha置0。这会增加一些计算量但效果非常炫酷。内阴影Inner Shadow Android的padding和size可以结合图层达成类似效果。在Shader中我们可以计算一个内部的SDF将半径和尺寸向内收缩然后根据内部SDF的值来混合一个阴影颜色模拟光从边缘照射的效果。与UI Mask/RectMask2D配合 我们的Shader生成的Alpha通道是规则的圆角矩形。当它与Unity的RectMask2D组件一起使用时能完美实现子UI元素的圆角裁剪性能远优于使用Mask组件和一张Alpha贴图。从Android XML的shape标签出发我们不仅成功在Unity中用Shader复现了其核心功能更获得了一个动态、高性能、可编程的超级UI图形工具。这个过程深刻揭示了声明式UI描述与过程式渲染程序之间的思维转换。掌握了这套方法你就能摆脱对预制纹理的依赖用代码实时生成任意复杂的UI图形无论是动态变化的进度条、呼吸式高亮的按钮还是风格化十足的游戏HUD都将信手拈来。这或许就是图形编程让人着迷的地方——用数学和逻辑直接与像素对话。
返回列表