前言
来上点难度 💪
想不清楚就写写画画,没有什么比写下来更让人思路清晰了
效果
圆角边框效果,支持改变边框的颜色、粗细、曲度
👉展示备份(国内节点)
👇效果展示(科学上网)
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| Shader "Unlit/AloeaShader06" { Properties { _Radius ("BorderRadius", Range(0.01, 1)) = 0.1 _Border ("BorderWidth", Range(0, 1)) = 0.1 _Color ("BorderColor", Color) = (1,1,1,1)
} SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" float _Radius; float _Border; float4 _Color; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o; } fixed4 frag (v2f i) : SV_Target { float split = 1 / _Radius; float2 coords = i.uv; coords.x *= split; float2 pointOnLineSeg = float2( clamp(coords.x, 0.5, split - 0.5), 0.5 ); float sdf = distance(coords, pointOnLineSeg) * 2 ; clip(- (sdf - 1)); float borderSdf = sdf + _Border; float pd = fwidth(borderSdf); return saturate((borderSdf - 1) / pd )* _Color; } ENDCG } } }
|
效果调试
整体思路
- 裁剪掉半径外的部分
- 找到平面的一条中线,按照所有点到中线距离超过半径r1来算出需要裁掉的部分
- 在上一步的基础上,再减去一个内层半径r2的图形,得到圆角边框
步骤一:
画出中线,假设矩形均分为 6 段,第一段和最后一段是画圆角的位置
1 2 3 4 5 6 7 8
| fixed4 frag (v2f i) : SV_Target { float2 coords = i.uv; coords.x *= 6; float2 pointOnLineSeg = float2( clamp(coords.x, 0.5, 5.5), 0.5 ); }
|
步骤二:
算出 uv 上所有点到中线的距离
1 2 3 4 5 6 7 8 9
| fixed4 frag (v2f i) : SV_Target { float2 coords = i.uv; coords.x *= 6; float2 pointOnLineSeg = float2( clamp(coords.x, 0.5, 5.5), 0.5); float sdf = distance(coords, pointOnLineSeg); return float4(sdf.xxx, 1); }
|
这个时候圆角内的点到中线的距离数值范围在 [0, 0.5], 超出的部分最大达到 √ (0.5^2 + 0.5^2)。我们的目标就是裁剪掉这个部分。
1 2
| float sdf = distance(coords, pointOnLineSeg) * 2;
|
如下图,更清楚地观察我们要裁剪的部分(纯白色,大于1的部分)
步骤三:
裁剪掉圆角外侧
1 2 3 4 5 6 7 8 9 10
| fixed4 frag (v2f i) : SV_Target { float2 coords = i.uv; coords.x *= 6; float2 pointOnLineSeg = float2( clamp(coords.x, 0.5, 5.5), 0.5); float sdf = distance(coords, pointOnLineSeg) * 2; clip(sdf - 1); return float4(sdf.xxx, 1); }
|
ok,能看到准确得裁剪出来了。sdf - 1
让圆角内的点都因为小于 0 被丢弃了,我们把它翻过来就是了。
步骤四:
画内圈,边框的宽度先取 0.2 测试
1 2 3 4 5 6 7 8 9 10 11
| fixed4 frag (v2f i) : SV_Target { float2 coords = i.uv; coords.x *= 6; float2 pointOnLineSeg = float2( clamp(coords.x, 0.5, 5.5), 0.5); float sdf = distance(coords, pointOnLineSeg) * 2; clip(- (sdf - 1)); float borderSdf = sdf + 0.2; return float4(borderSdf.xxx, 1); }
|
现在所有点到中线的距离范围在 [0,1.2],可以观察到其中 [1,1.2] 的部分就是我们的目标边框
步骤五:
得到边框
1 2 3 4 5 6 7 8 9 10 11
| fixed4 frag (v2f i) : SV_Target { float2 coords = i.uv; coords.x *= 6; float2 pointOnLineSeg = float2( clamp(coords.x, 0.5, 5.5), 0.5); float sdf = distance(coords, pointOnLineSeg) * 2; clip(- (sdf - 1)); float borderSdf = sdf + 0.2; return float4(step(1, borderSdf).xxx, 1); }
|
步骤六:
接入参数作为变量,包括圆角角度、边框宽度、边框颜色
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| fixed4 frag (v2f i) : SV_Target { float split = 1 / _Radius; float2 coords = i.uv; coords.x *= split; float2 pointOnLineSeg = float2( clamp(coords.x, 0.5, split - 0.5), 0.5); float sdf = distance(coords, pointOnLineSeg) * 2; clip(- (sdf - 1)); float borderSdf = sdf + _Border; return float4(step(1, borderSdf).xxx, 1) * _Color; }
|
步骤七:
优化边缘锯齿
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| fixed4 frag (v2f i) : SV_Target { float split = 1 / _Radius; float2 coords = i.uv; coords.x *= split; float2 pointOnLineSeg = float2( clamp(coords.x, 0.5, split - 0.5), 0.5); float sdf = distance(coords, pointOnLineSeg) * 2; clip(- (sdf - 1)); float borderSdf = sdf + _Border; float pd = fwidth(borderSdf); return saturate((borderSdf - 1) / pd )* _Color; }
|
知识点
函数 clamp
clamp 函数用于将一个值限制在一个指定的范围内。它接受三个参数,第一个是要限制的值,第二个是范围的下限,第三个是范围的上限。如果值小于下限,函数返回下限,如果值大于上限,函数返回上限,否则返回原始值。例如,clamp(x,0.5,1.5)
将 x 限制在 0.5 到 1.5 之间。
1 2 3 4
| void Unity_Clamp_float4(float4 In, float4 Min, float4 Max, out float4 Out) { Out = clamp(In, Min, Max); }
|
函数 saturate
简化版的 clamp,指定限制范围为 [0, 1] 。
saturate 函数用于将一个值限制在 0 到 1 之间。它接受一个参数,即要限制的值。如果该值小于 0,则返回 0,如果该值大于 1,则返回 1,否则返回原始值。
1 2 3 4
| void Unity_Saturate_float4(float4 In, out float4 Out) { Out = saturate(In); }
|
函数 distance
distance 函数用于计算两个点之间的距离。它接受两个参数,即两个点的坐标。distance 函数实际上是计算两个点之间的欧几里得距离,即勾股定理。
1 2 3 4
| void Unity_Distance_float4(float4 A, float4 B, out float Out) { Out = distance(A, B); }
|
注意这个函数和 length
的区别,length 的传入是一个向量,计算其长度;distance
则是传入两个向量,计算它们之间的距离。在使用中有些场景可以相互替换用。
1 2
| float dist = length(vec1 - vec2); float len = distance(vec, vec3(0, 0, 0));
|
函数 clip
clip 函数用于在特定条件下舍弃像素片段。它接受一个参数,即一个浮点数,如果该参数小于等于 0,则该像素片段将被舍弃。
函数 step
相当于一个判断赋值的缩写/语法糖,step(edge, in)
等于 !!(in >= edge)
,返回结果为 0 或1。
step 函数用于根据一个阈值将一个值转换为 0 或 1 。它接受两个参数,第一个是要比较的值,第二个是阈值。如果值小于等于阈值,则函数返回 0 ,否则返回1。
1 2 3 4
| void Unity_Step_float4(float4 Edge, float4 In, out float4 Out) { Out = step(Edge, In); }
|
函数 fwidth
相当于每个用户的屏幕空间中当前像素的变化率,数值越大相邻像素间差距越大,表现为数学上的变化率。返回的数值范围上限取决于物理屏幕的分辨率,通常为 1,下限则为0 (比如严格纯色的场景)。下次可以合着抗锯齿一起单独讲一篇,通常也是用在各种边缘处理中。
fwidth 函数用于计算函数在屏幕空间中的变化率。它接受一个参数,即要计算的函数值。fwidth 函数实际上是对 dx 和 dy 函数的绝对值之和的计算,其中 dx 函数计算函数在 x 方向上的变化率,dy 函数计算函数在 y 方向上的变化率。
fwidth(x) = abs(ddx(x)) + abs(ddy(x))。
总结
- 还是回到核心思路,找到中线按照距离算出要裁剪掉的部分,后面就比较顺了。
- 涉及到的函数比较多,包括处理数值范围的
clamp
、saturate
、clip
,判断语法糖 step
,处理边缘锯齿的变化率导数 fwidth
。
- 整个效果还有改进的空间,包括抗锯齿的细节上,也包括圆角的设计策略上。