带符号距离场的2D空间操纵

使用多边形资产时,一次只能绘制一个对象(如果不考虑批处理和实例化等技术),但是如果您使用带符号的距离字段(带符号距离字段,SDF),那么我们不仅限于此。 如果两个位置的坐标相同,则带符号的距离函数将返回相同的值,并且在一次计算中,我们可以获得多个数字。 要了解如何转换用于生成带符号距离字段的空间,建议您弄清楚如何使用带符号距离函数创建形状组合sdf shape


构型


在本教程中,我修改了正方形和圆形之间的配对,但是您可以将其用于任何其他形状。 这类似于上一教程的配置。

在此重要的是,可修改部分应在使用位置生成图形之前。

Shader "Tutorial/036_SDF_Space_Manpulation/Type"{ 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) { // manipulate position with cool methods here! float2 squarePosition = position; squarePosition = translate(squarePosition, float2(2, 2)); squarePosition = rotate(squarePosition, .125); float squareShape = rectangle(squarePosition, float2(1, 1)); float2 circlePosition = position; circlePosition = translate(circlePosition, float2(1, 1.5)); float circleShape = circle(circlePosition, 1); float combination = merge(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" } 

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; } //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 


重复空间


镜面反射


最简单的操作之一是围绕轴镜像世界。 为了使其绕y轴镜像,我们获取位置x分量的绝对值。 因此,轴线左右的坐标将相同。 (-1, 1)变成(1, 1) ,并以(1, 1)为坐标原点并且半径大于0变成圆内。

通常,使用此功能的代码看起来像position = mirror(position); 所以我们可以简化一下。 我们将简单地将position参数声明为inout。 因此,在写入参数时,它还将更改我们传递给函数的变量。 然后,该返回值可以是void类型,因为我们仍然不使用该返回值。

 //in 2D_SDF.cginc void mirror(inout float2 position){ position.x = abs(position.x); } 

 //in shader function mirror(position); 


结果已经很好,但是通过这种方式,我们只获得了一个用于镜像的轴。 我们可以像旋转图形时那样旋转空间来扩展功能。 首先,您需要旋转空间,然后对其进行镜像,然后再将其旋转回去。 这样,我们可以针对任何角度执行镜像。 转移空间并在镜像后执行反向转移时,也可以这样做。 (如果执行这两个操作,则在镜像之前,请不要忘记先进行传输,然后再转弯,然后先转弯。)

 //in shader function float rotation = _Time.y * 0.25; position = rotate(position, rotation); mirror(position); position = rotate(position, -rotation); 


细胞


如果您知道噪声产生的工作原理,那么您就会知道,对于程序生成,我们经常重复该位置并获得基本相同的小单元,只是参数无关紧要。 我们可以对距离场做同样的事情。

由于fmod函数(以及使用%除以余数)为我们提供了余数,而不是余数的定义,因此我们将不得不使用技巧。 首先,我们将其余的整数除以fmod函数。 对于正数,这正是我们需要的;对于负数,这是我们需要的结果减去周期。 您可以通过添加句点并再次使用除法的其余部分来解决此问题。 添加一个周期将为负输入值提供理想的结果,而对于正输入值,该值高一个周期。 除法的第二个余数不会对负输入值的值执行任何操作,因为它们已经在0到周期之间的范围内,而对于正输入值,我们将实质上减去一个周期。

 //in 2D_SDF.cginc void cells(inout float2 position, float2 period){ position = fmod(position, period); //negative positions lead to negative modulo position += period; //negative positions now have correct cell coordinates, positive input positions too high position = fmod(position, period); //second mod doesn't change values between 0 and period, but brings down values that are above period. } 

 //in shader function cells(position, float2(3, 3)); 


单元的问题在于,我们失去了我们喜欢距离场的连续性。 如果形状仅在单元格的中间,这也不错,但是在上面显示的示例中,当将距离场用于通常可应用距离场的各种任务时,这可能会导致产生明显的伪影,应避免使用。

有一种解决方案并非在每种情况下都有效,但是当它可行时,这很妙:镜像所有其他单元。 为此,我们需要一个像素单元索引,但是函数中仍然没有返回值,因此我们可以使用它来返回单元索引。

为了计算单元格索引,我们将位置除以句点。 因此,0-1是第一个单元格,1-2是第二个单元格,依此类推...我们可以轻松地离散化它。 为了获得单元格的索引,我们然后简单地将值四舍五入并返回结果。 重要的是在除以其余部分以重复单元之前,我们先计算单元的索引; 否则,我们将在任何地方获得索引0,因为头寸不能超过句点。

 //in 2D_SDF.cginc float2 cells(inout float2 position, float2 period){ position = fmod(position, period); //negative positions lead to negative modulo position += period; //negative positions now have correct cell coordinates, positive input positions too high position = fmod(position, period); //second mod doesn't change values between 0 and period, but brings down values that are above period. float2 cellIndex = position / period; cellIndex = floor(cellIndex); return cellIndex; } 

有了这些信息,我们就可以翻转单元格。 要了解是否翻转,我们将单元索引取模2。此操作的结果是每隔2个单元交替显示0和1或-1。 为了使更改更持久,我们采用绝对值并获得一个介于0和1之间的值。

要使用此值在正常位置和翻转位置之间进行翻转,我们需要一个对值0不执行任何操作的函数,并从翻转为1的周期中减去该位置。即,我们使用flip变量从法向位置到翻转位置执行线性插值。 由于flip变量是2d向量,因此其分量会单独翻转。

 //in shader function float2 period = 3; float2 cell = cells(position, period); float2 flip = abs(fmod(cell, 2)); position = lerp(position, period - position, flip); 


细胞


另一个重要特征是径向模式中的空间重复。

为了获得这种效果,我们首先计算径向位置。 为此,我们对相对于x轴中心的角度以及与中心沿y轴的距离进行编码。

 float2 radialPosition = float2(atan2(position.x, position.y), length(position)); 

然后,我们重复角落。 由于传输重复次数比每个片段的角度容易得多,因此我们首先计算每个片段的大小。 整个圆是2 * pi,因此要得到正确的部分,我们将2 * pi除以像元大小。

 const float PI = 3.14159; float cellSize = PI * 2 / cells; 

有了这些信息,我们可以在每个cellSize单位上重复径向位置的x分量。 我们用除数除法进行重复,因此,像以前一样,我们会遇到负数的问题,这可以通过除数除法的两个函数来消除。

 radialPosition.x = fmod(fmod(radialPosition.x, cellSize) + cellSize, cellSize); 

然后,您需要将新位置移回通常的xy坐标。 在这里,我们将sincos函数与径向位置的x分量用作角度,以将正弦写入位置的x坐标,并将余弦写入y坐标。 通过这一步,我们得到一个标准化的位置。 要从中心获得正确的方向,请将其乘以径向位置的分量y,即长度。

 //in 2D_SDF.cginc void radial_cells(inout float2 position, float cells){ const float PI = 3.14159; float cellSize = PI * 2 / cells; float2 radialPosition = float2(atan2(position.x, position.y), length(position)); radialPosition.x = fmod(fmod(radialPosition.x, cellSize) + cellSize, cellSize); sincos(radialPosition.x, position.x, position.y); position = position * radialPosition.y; } 

 //in shader function float2 period = 6; radial_cells(position, period, false); 


然后,我们也可以像常规单元格一样添加单元格索引和镜像。

有必要在计算径向位置之后但在从除法运算接收余数之前,计算像元索引。 我们通过除以径向位置的x分量并将结果四舍五入得到它。 在这种情况下,索引也可以为负数,如果单元格数为奇数,则这是一个问题。 例如,对于3个像元,我们得到1个像元的索引为0、1个像元的索引为-1和2个半像元的索引为1和-2。 为了解决这个问题,我们将单元格的数量加到变量的四舍五入后的变量中,然后除以剩下的单元格的大小。

 //in 2D_SDF.cginc float cellIndex = fmod(floor(radialPosition.x / cellSize) + cells, cells); //at the end of the function: return cellIndex; 

为了反映这一点,我们需要以弧度指定坐标,为避免在函数外重新计算径向坐标,我们将使用bool参数为其添加一个选项。 通常在着色器中,不欢迎分支(如果构造),但是在这种情况下,屏幕上的所有像素都将沿着相同的路径前进,因此这是正常的。

镜像应该在径向坐标循环之后但在转换回其正常位置之前发生。 我们找出是否需要通过将单元格索引除以2来除以当前单元格,通常这应该给我们零和一,但是在我的情况下会出现几个二,这很奇怪,但是我们可以处理它。 为了消除推论,我们只需从flip变量中减去1,然后取绝对值即可。 因此,根据需要,零和减数将变为1,单位变为零,仅以相反的顺序。

由于零和一的顺序不正确,因此我们像以前一样执行从上向下版本到上向下版本的线性插值,反之亦然。 要翻转坐标,我们只需从像元大小中减去位置即可。

 //in 2D_SDF.cginc float radial_cells(inout float2 position, float cells, bool mirrorEverySecondCell = false){ const float PI = 3.14159; float cellSize = PI * 2 / cells; float2 radialPosition = float2(atan2(position.x, position.y), length(position)); float cellIndex = fmod(floor(radialPosition.x / cellSize) + cells, cells); radialPosition.x = fmod(fmod(radialPosition.x, cellSize) + cellSize, cellSize); if(mirrorEverySecondCell){ float flip = fmod(cellIndex, 2); flip = abs(flip-1); radialPosition.x = lerp(cellSize - radialPosition.x, radialPosition.x, flip); } sincos(radialPosition.x, position.x, position.y); position = position * radialPosition.y; return cellIndex; } 

 //in shader function float2 period = 6; radial_cells(position, period, true); 


摇摆空间


但是改变空间是没有必要重复的。 例如,在基础教程中,我们对其进行了旋转,移动和缩放。 您还可以执行以下操作:使用正弦波在每个轴的基础上移动每个轴。 这会使带符号的距离函数的距离不太准确,但是除非它们摆动太大,否则一切都会好起来的。

首先,我们通过翻转x和y分量,然后将它们乘以摆频来计算位置变化的幅度。 然后,我们从该值中获取正弦值,然后将其乘以要添加的摆动量。 之后,我们只需将此摆动因子添加到位置,然后再次将结果应用于位置。

 //in 2D_SDF.cginc void wobble(inout float2 position, float2 frequency, float2 amount){ float2 wobble = sin(position.yx * frequency) * amount; position = position + wobble; } 

 //in shader function wobble(position, 5, .05); 


我们还可以对此挥舞进行动画处理,更改其位置,在偏移位置应用挥舞并返回该空间。 为了使浮点数不会太大,我用摆动频率除以pi * 2的余数,这与摆动相关(正弦波每pi * 2个单位重复一次),因此我们避免了跳跃和太大的偏移量。

 //in shader function const float PI = 3.14159; float frequency = 5; float offset = _Time.y; offset = fmod(offset, PI * 2 / frequency); position = translate(position, offset); wobble(position, 5, .05); position = translate(position, -offset); 


源代码


2D SDF库



 #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); } // space repetition void mirror(inout float2 position){ position.x = abs(position.x); } float2 cells(inout float2 position, float2 period){ //find cell index float2 cellIndex = position / period; cellIndex = floor(cellIndex); //negative positions lead to negative modulo position = fmod(position, period); //negative positions now have correct cell coordinates, positive input positions too high position += period; //second mod doesn't change values between 0 and period, but brings down values that are above period. position = fmod(position, period); return cellIndex; } float radial_cells(inout float2 position, float cells, bool mirrorEverySecondCell = false){ const float PI = 3.14159; float cellSize = PI * 2 / cells; float2 radialPosition = float2(atan2(position.x, position.y), length(position)); float cellIndex = fmod(floor(radialPosition.x / cellSize) + cells, cells); radialPosition.x = fmod(fmod(radialPosition.x, cellSize) + cellSize, cellSize); if(mirrorEverySecondCell){ float flip = fmod(cellIndex, 2); flip = abs(flip-1); radialPosition.x = lerp(cellSize - radialPosition.x, radialPosition.x, flip); } sincos(radialPosition.x, position.x, position.y); position = position * radialPosition.y; return cellIndex; } void wobble(inout float2 position, float2 frequency, float2 amount){ float2 wobble = sin(position.yx * frequency) * amount; position = position + wobble; } //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/036_SDF_Space_Manpulation/Mirror"{ 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) { // modify position here! float2 squarePosition = position; squarePosition = translate(squarePosition, float2(2, 2)); squarePosition = rotate(squarePosition, .125); float squareShape = rectangle(squarePosition, float2(1, 1)); float2 circlePosition = position; circlePosition = translate(circlePosition, float2(1, 1.5)); float circleShape = circle(circlePosition, 1); float combination = merge(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 } 

现在您已经知道了记号距离功能的所有基础知识。 在下一个教程中,我将尝试对它们做一些有趣的事情。

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


All Articles