组合2D中的有符号距离场

在上一教程中,我们学习了如何使用带符号的距离函数创建和移动简单形状。 在本文中,我们将学习如何组合多个形状以创建更复杂的距离场。 我从距离函数的glsl-sign库中学习了这里描述的大多数技术,可以在这里找到。 还有几种组合形状的方法,在此不做讨论。


准备工作


为了可视化带符号距离字段(带符号距离字段,SDF),我们将使用一种简单的配置,然后将运算符应用于该配置。 为了显示距离场,它将使用第一个教程中的距离线可视化。 为了简单起见,我们将在代码中设置除可视化参数以外的所有参数,但是您可以用属性替换任何值以使其可自定义。

我们将从以下内容开始的主要着色器如下:

Shader "Tutorial/035_2D_SDF_Combinations/Champfer Union"{ Properties{ _InsideColor("Inside Color", Color) = (.5, 0, 0, 1) _OutsideColor("Outside Color", Color) = (0, .5, 0, 1) _LineDistance("Mayor Line Distance", Range(0, 2)) = 1 _LineThickness("Mayor Line Thickness", Range(0, 0.1)) = 0.05 [IntRange]_SubLines("Lines between major lines", Range(1, 10)) = 4 _SubLineThickness("Thickness of inbetween lines", Range(0, 0.05)) = 0.01 } SubShader{ //the material is completely non-transparent and is rendered at the same time as the other opaque geometry Tags{ "RenderType"="Opaque" "Queue"="Geometry"} Pass{ CGPROGRAM #include "UnityCG.cginc" #include "2D_SDF.cginc" #pragma vertex vert #pragma fragment frag struct appdata{ float4 vertex : POSITION; }; struct v2f{ float4 position : SV_POSITION; float4 worldPos : TEXCOORD0; }; v2f vert(appdata v){ v2f o; //calculate the position in clip space to render the object o.position = UnityObjectToClipPos(v.vertex); //calculate world position of vertex o.worldPos = mul(unity_ObjectToWorld, v.vertex); return o; } float scene(float2 position) { const float PI = 3.14159; float2 squarePosition = position; squarePosition = translate(squarePosition, float2(1, 0)); squarePosition = rotate(squarePosition, .125); float squareShape = rectangle(squarePosition, float2(2, 2)); float2 circlePosition = position; circlePosition = translate(circlePosition, float2(-1.5, 0)); float circleShape = circle(circlePosition, 2.5); float combination = combination_function(circleShape, squareShape); return combination; } float4 _InsideColor; float4 _OutsideColor; float _LineDistance; float _LineThickness; float _SubLines; float _SubLineThickness; fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); fixed4 col = lerp(_InsideColor, _OutsideColor, step(0, dist)); float distanceChange = fwidth(dist) * 0.5; float majorLineDistance = abs(frac(dist / _LineDistance + 0.5) - 0.5) * _LineDistance; float majorLines = smoothstep(_LineThickness - distanceChange, _LineThickness + distanceChange, majorLineDistance); float distanceBetweenSubLines = _LineDistance / _SubLines; float subLineDistance = abs(frac(dist / distanceBetweenSubLines + 0.5) - 0.5) * distanceBetweenSubLines; float subLines = smoothstep(_SubLineThickness - distanceChange, _SubLineThickness + distanceChange, subLineDistance); return col * majorLines * subLines; } ENDCG } } FallBack "Standard" //fallback adds a shadow pass so we get shadows on other objects } 

和2D_SDF.cginc函数位于着色器所在的文件夹中,我们将对其进行扩展,首先看起来像这样:

 #ifndef SDF_2D #define SDF_2D //transforms float2 rotate(float2 samplePosition, float rotation){ const float PI = 3.14159; float angle = rotation * PI * 2 * -1; float sine, cosine; sincos(angle, sine, cosine); return float2(cosine * samplePosition.x + sine * samplePosition.y, cosine * samplePosition.y - sine * samplePosition.x); } float2 translate(float2 samplePosition, float2 offset){ //move samplepoint in the opposite direction that we want to move shapes in return samplePosition - offset; } float2 scale(float2 samplePosition, float scale){ return samplePosition / scale; } //shapes float circle(float2 samplePosition, float radius){ //get distance from center and grow it according to radius return length(samplePosition) - radius; } float rectangle(float2 samplePosition, float2 halfSize){ float2 componentWiseEdgeDistance = abs(samplePosition) - halfSize; float outsideDistance = length(max(componentWiseEdgeDistance, 0)); float insideDistance = min(max(componentWiseEdgeDistance.x, componentWiseEdgeDistance.y), 0); return outsideDistance + insideDistance; } #endif 

简单组合


我们将从几种简单的方法开始,将两个形状结合起来以创建一个大形状,共轭,交点和减法,以及将一个形状转换为另一种形状的方法。

配对


最简单的运算符是配对。 有了它,我们可以将两个图形放在一起,并获得具有连接图形符号的距离。 当我们有两个数字的符号的距离时,可以使用min函数将两个数字中的较小者合并起来。

由于选择了两个值中较小的一个,因此最终图形将低于0(可见),其中两个输入图形之一与边缘的距离小于0; 同样适用于所有其他距离值,显示两个数字的组合。

在这里,我将命名用于创建共轭“合并”的函数,部分是因为我们正在合并它们,部分是因为hlsl中的union关键字是保留的,因此它不能用作函数的名称。

 //in 2D_SDF.cginc include file float merge(float shape1, float shape2){ return min(shape1, shape2); } 

 //in scene function in shader float combination = merge(circleShape, squareShape); 




交叉口


连接形状的另一种常用方法是使用两个形状重叠的区域。 为此,我们取要合并的两个图形的距离的最大值。 当使用两个值中的最大值时,如果到两个图形的任何距离都在图形外部,并且其他距离也以相同的方式对齐,则得到的值大于0(在图形外部)。

 //in 2D_SDF.cginc include file float intersect(float shape1, float shape2){ return max(shape1, shape2); } 

 //in scene function in shader float combination = intersect(circleShape, squareShape); 


减法


但是,通常我们不想以相同的方式处理这两个形状,而需要从一个形状中减去另一个形状。 通过在我们要更改的形状与除我们要减去的形状之外的所有形状之间相交,这很容易做到。 对于图的内部和外部,我们得到相反的值,并用符号反转距离。 该图外面的1个单位现在变成了里面的1个单位。

 //in 2D_SDF.cginc include file float subtract(float base, float subtraction){ return intersect(base, -subtraction); } 

 //in scene function in shader float combination = subtract(squareShape, circleShape); 


插补


组合两个图形的一种明显方法是在它们之间进行插值。 具有混合形状的多边形网格在某种程度上也是可能的,但是比我们对有符号距离场的限制要大得多。 通过在两个图形的距离之间进行简单的插值,我们可以使一个图形平稳地流入另一个图形。 对于插值,您可以简单地使用lerp方法。

 //in 2D_SDF.cginc include file float interpolate(float shape1, float shape2, float amount){ return lerp(shape1, shape2, amount); } 

 //in scene function in shader float pulse = sin(_Time.y) * 0.5 + 0.5; float combination = interpolate(circleShape, pulse); 


其他化合物


收到简单的连接后,我们已经具备了简单地组合图形所需的一切,但是距离符号字段的惊人特性是我们不仅限于此,可以通过多种不同的方式组合图形并在其连接处执行有趣的操作。 在这里,我将仅再次解释其中的一些技术,但是您可以在http://mercury.sexy/hg_sdf库中找到许多其他技术(如果您知道其他有用的SDF库,请写信给我)。

四舍五入


我们可以将两个组合图形的表面解释为坐标系中该位置的x轴和y轴,然后计算到该位置的坐标原点的距离。 如果这样做,将得到一个非常奇怪的图形,但是如果将轴限制为小于0的值,则将得到类似于两个图形的内部距离的平滑共轭的东西。

 float round_merge(float shape1, float shape2, float radius){ float2 intersectionSpace = float2(shape1, shape2); intersectionSpace = min(intersectionSpace, 0); return length(intersectionSpace); } 


这很漂亮,但是我们不能用它来更改距离为0的行,因此此操作没有比普通配对更有价值。 但是在将两个数字联系起来之前,我们可以将它们稍微增加一点。 以与创建圆相同的方式,要放大图形,请从其距离中减去它,以便进一步推出一条线,其中带有符号的距离为0。

 float radius = max(sin(_Time.y * 5) * 0.5 + 0.4, 0); float combination = round_intersect(squareShape, circleShape, radius); 

 float round_merge(float shape1, float shape2, float radius){ float2 intersectionSpace = float2(shape1 - radius, shape2 - radius); intersectionSpace = min(intersectionSpace, 0); return length(intersectionSpace); } 


它只是放大图形并确保内部平滑过渡,但我们不想增加图形,我们只需要平滑过渡即可。 解决方案是在计算长度后再次减去半径。 除了图形之间的过渡(根据半径进行了平滑的平滑处理)外,大多数零件的外观与以前相同。 现在我们将忽略该图的外部。

 float round_merge(float shape1, float shape2, float radius){ float2 intersectionSpace = float2(shape1 - radius, shape2 - radius); intersectionSpace = min(intersectionSpace, 0); return length(intersectionSpace) - radius; } 


最后一步是对图的外部部分的校正。 另外,图中的内部为绿色,而外部则使用该颜色。 第一步是简单地通过用符号反转它们的距离来交换外部和内部零件。 然后我们替换减去半径的部分。 首先,我们将其从减法更改为加法。 这是必需的,因为在与半径组合之前,我们已绘制了矢量的距离,因此,根据此,我们需要反转所使用的数学运算。 然后,我们将使用常规配合替换半径,这将为我们提供图形外部的正确值,但不会靠近图形的边缘和内部。 为了避免这种情况,我们在值和半径之间取最大值,从而在图形外部获得正确值的正值,并在图形内部获得所需的半径。

 float round_merge(float shape1, float shape2, float radius){ float2 intersectionSpace = float2(shape1 - radius, shape2 - radius); intersectionSpace = min(intersectionSpace, 0); float insideDistance = -length(intersectionSpace); float simpleUnion = merge(shape1, shape2); float outsideDistance = max(simpleUnion, radius); return insideDistance + outsideDistance; } 


要创建相交,我们需要做相反的事情-将图形减小半径,确保矢量的所有分量都大于0,采用长度且不更改其符号。 因此,我们将创建图的外部。 然后,要创建内部零件,我们采用通常的相交并确保其不小于半径。 然后,像以前一样,我们添加内部和外部值。

 float round_intersect(float shape1, float shape2, float radius){ float2 intersectionSpace = float2(shape1 + radius, shape2 + radius); intersectionSpace = max(intersectionSpace, 0); float outsideDistance = length(intersectionSpace); float simpleIntersection = intersect(shape1, shape2); float insideDistance = min(simpleIntersection, -radius); return outsideDistance + insideDistance; } 


最后一点,减法可以再次描述为基本图形与除我们要减去的图形以外的所有图形之间的交集。

 float round_subtract(float base, float subtraction, float radius){ round_intersect(base, -subtraction, radius); } 


在这里,尤其是在进行减法时,您可以看到由于我们可以使用两个数字作为坐标而产生的假象,但是对于大多数应用而言,距离场仍然足够好。

斜角


我们也可以修剪过渡以使其像倒角一样倾斜。 为了达到这种效果,我们首先通过添加现有形状来创建一个新形状。 如果我们再次假设两个图形相交的点是正交的,那么此操作将给我们一条穿过两个曲面相交点的对角线。


由于我们只是简单地添加了这两个分量,所以带有新线符号的距离的比例尺不正确,但是我们可以通过将其除以单位平方的对角线即2的平方根来固定它,将其除以2的根等于乘以0.5的平方根,我们可以简单地将此值写入代码,以免每次都不计算相同的根。

现在我们有了一个具有所需斜角形状的形状,我们将对其进行扩展,以使斜角延伸到图形的边界之外。 与以前一样,我们减去增加数值所需的值。 然后,将斜角形状与常规合并的输出结合起来,从而产生斜角过渡。

 float champferSize = sin(_Time.y * 5) * 0.3 + 0.3; float combination = champfer_merge(circleShape, squareShape, champferSize); 

 float champfer_merge(float shape1, float shape2, float champferSize){ const float SQRT_05 = 0.70710678118; float simpleMerge = merge(shape1, shape2); float champfer = (shape1 + shape2) * SQRT_05; champfer = champfer - champferSize; return merge(simpleMerge, champfer); } 


要获得一个斜切的斜角,我们像以前一样添加两个图形,但是随后我们通过添加斜切的值来缩小图形,并与规则的斜交形相交。

 float champfer_intersect(float shape1, float shape2, float champferSize){ const float SQRT_05 = 0.70710678118; float simpleIntersect = intersect(shape1, shape2); float champfer = (shape1 + shape2) * SQRT_05; champfer = champfer + champferSize; return intersect(simpleIntersect, champfer); } 


与前面的减法相似,我们也可以在此处与倒数第二个图形进行交点。

 float champfer_subtract(float base, float subtraction, float champferSize){ return champfer_intersect(base, -subtraction, champferSize); } 


圆交集


到目前为止,我们仅使用布尔运算符(插值除外)。 但是我们可以用其他方式组合形状,例如,在两个形状的边界重叠的地方创建新的圆形形状。

为此,我们再次需要将两个数字解释为该点的x轴和y轴。 然后,我们仅计算该点到原点的距离。 如果两个图形的边界重叠,则到两个图形的距离将为0,这使我们到虚构坐标系的原点的距离为0。 然后,如果到原点有一段距离,则可以用它执行与圆相同的操作,然后减去半径。

 float round_border(float shape1, float shape2, float radius){ float2 position = float2(shape1, shape2); float distanceFromBorderIntersection = length(position); return distanceFromBorderIntersection - radius; } 


边框缺口


我要解释的最后一件事是在另一个形状的边界位置在一个形状中创建缺口的方法。

我们首先计算圆的边界形状。 这可以通过获取第一个图形的距离的绝对值来完成,而内部和外部都将被视为图形的内部,但边界仍为0。如果我们通过减去切口宽度来增加此图形,则将沿上一个图形的边界获得图形。

 float depth = max(sin(_Time.y * 5) * 0.5 + 0.4, 0); float combination = groove_border(squareShape, circleShape, .3, depth); 

 float groove_border(float base, float groove, float width, float depth){ float circleBorder = abs(groove) - width; return circleBorder; } 


现在,我们只需要按指定的值使圆的边界更深即可。 为此,我们从中减去基本图形的简化版本。 基本形状的减小量就是切口的深度。

 float groove_border(float base, float groove, float width, float depth){ float circleBorder = abs(groove) - width; float grooveShape = subtract(circleBorder, base + depth); return grooveShape; } 


最后一步是从基本形状中减去缺口,然后返回结果。

 float groove_border(float base, float groove, float width, float depth){ float circleBorder = abs(groove) - width; float grooveShape = subtract(circleBorder, base + depth); return subtract(base, grooveShape); } 


源代码


图书馆



 #ifndef SDF_2D #define SDF_2D //transforms float2 rotate(float2 samplePosition, float rotation){ const float PI = 3.14159; float angle = rotation * PI * 2 * -1; float sine, cosine; sincos(angle, sine, cosine); return float2(cosine * samplePosition.x + sine * samplePosition.y, cosine * samplePosition.y - sine * samplePosition.x); } float2 translate(float2 samplePosition, float2 offset){ //move samplepoint in the opposite direction that we want to move shapes in return samplePosition - offset; } float2 scale(float2 samplePosition, float scale){ return samplePosition / scale; } //combinations ///basic float merge(float shape1, float shape2){ return min(shape1, shape2); } float intersect(float shape1, float shape2){ return max(shape1, shape2); } float subtract(float base, float subtraction){ return intersect(base, -subtraction); } float interpolate(float shape1, float shape2, float amount){ return lerp(shape1, shape2, amount); } /// round float round_merge(float shape1, float shape2, float radius){ float2 intersectionSpace = float2(shape1 - radius, shape2 - radius); intersectionSpace = min(intersectionSpace, 0); float insideDistance = -length(intersectionSpace); float simpleUnion = merge(shape1, shape2); float outsideDistance = max(simpleUnion, radius); return insideDistance + outsideDistance; } float round_intersect(float shape1, float shape2, float radius){ float2 intersectionSpace = float2(shape1 + radius, shape2 + radius); intersectionSpace = max(intersectionSpace, 0); float outsideDistance = length(intersectionSpace); float simpleIntersection = intersect(shape1, shape2); float insideDistance = min(simpleIntersection, -radius); return outsideDistance + insideDistance; } float round_subtract(float base, float subtraction, float radius){ return round_intersect(base, -subtraction, radius); } ///champfer float champfer_merge(float shape1, float shape2, float champferSize){ const float SQRT_05 = 0.70710678118; float simpleMerge = merge(shape1, shape2); float champfer = (shape1 + shape2) * SQRT_05; champfer = champfer - champferSize; return merge(simpleMerge, champfer); } float champfer_intersect(float shape1, float shape2, float champferSize){ const float SQRT_05 = 0.70710678118; float simpleIntersect = intersect(shape1, shape2); float champfer = (shape1 + shape2) * SQRT_05; champfer = champfer + champferSize; return intersect(simpleIntersect, champfer); } float champfer_subtract(float base, float subtraction, float champferSize){ return champfer_intersect(base, -subtraction, champferSize); } /// round border intersection float round_border(float shape1, float shape2, float radius){ float2 position = float2(shape1, shape2); float distanceFromBorderIntersection = length(position); return distanceFromBorderIntersection - radius; } float groove_border(float base, float groove, float width, float depth){ float circleBorder = abs(groove) - width; float grooveShape = subtract(circleBorder, base + depth); return subtract(base, grooveShape); } //shapes float circle(float2 samplePosition, float radius){ //get distance from center and grow it according to radius return length(samplePosition) - radius; } float rectangle(float2 samplePosition, float2 halfSize){ float2 componentWiseEdgeDistance = abs(samplePosition) - halfSize; float outsideDistance = length(max(componentWiseEdgeDistance, 0)); float insideDistance = min(max(componentWiseEdgeDistance.x, componentWiseEdgeDistance.y), 0); return outsideDistance + insideDistance; } #endif 

着色器底座



 Shader "Tutorial/035_2D_SDF_Combinations/Round"{ Properties{ _InsideColor("Inside Color", Color) = (.5, 0, 0, 1) _OutsideColor("Outside Color", Color) = (0, .5, 0, 1) _LineDistance("Mayor Line Distance", Range(0, 2)) = 1 _LineThickness("Mayor Line Thickness", Range(0, 0.1)) = 0.05 [IntRange]_SubLines("Lines between major lines", Range(1, 10)) = 4 _SubLineThickness("Thickness of inbetween lines", Range(0, 0.05)) = 0.01 } SubShader{ //the material is completely non-transparent and is rendered at the same time as the other opaque geometry Tags{ "RenderType"="Opaque" "Queue"="Geometry"} Pass{ CGPROGRAM #include "UnityCG.cginc" #include "2D_SDF.cginc" #pragma vertex vert #pragma fragment frag struct appdata{ float4 vertex : POSITION; }; struct v2f{ float4 position : SV_POSITION; float4 worldPos : TEXCOORD0; }; v2f vert(appdata v){ v2f o; //calculate the position in clip space to render the object o.position = UnityObjectToClipPos(v.vertex); //calculate world position of vertex o.worldPos = mul(unity_ObjectToWorld, v.vertex); return o; } float scene(float2 position) { const float PI = 3.14159; float2 squarePosition = position; squarePosition = translate(squarePosition, float2(1, 0)); squarePosition = rotate(squarePosition, .125); float squareShape = rectangle(squarePosition, float2(2, 2)); float2 circlePosition = position; circlePosition = translate(circlePosition, float2(-1.5, 0)); float circleShape = circle(circlePosition, 2.5); float combination = /* combination calculation here */; return combination; } float4 _InsideColor; float4 _OutsideColor; float _LineDistance; float _LineThickness; float _SubLines; float _SubLineThickness; fixed4 frag(v2f i) : SV_TARGET{ float dist = scene(i.worldPos.xz); fixed4 col = lerp(_InsideColor, _OutsideColor, step(0, dist)); float distanceChange = fwidth(dist) * 0.5; float majorLineDistance = abs(frac(dist / _LineDistance + 0.5) - 0.5) * _LineDistance; float majorLines = smoothstep(_LineThickness - distanceChange, _LineThickness + distanceChange, majorLineDistance); float distanceBetweenSubLines = _LineDistance / _SubLines; float subLineDistance = abs(frac(dist / distanceBetweenSubLines + 0.5) - 0.5) * distanceBetweenSubLines; float subLines = smoothstep(_SubLineThickness - distanceChange, _SubLineThickness + distanceChange, subLineDistance); return col * majorLines * subLines; } ENDCG } } FallBack "Standard" //fallback adds a shadow pass so we get shadows on other objects } 

Source: https://habr.com/ru/post/zh-CN438954/


All Articles