Combinando campos de distância assinada em 2D

No tutorial anterior, aprendemos como criar e mover formas simples usando funções de distância assinadas. Neste artigo, aprenderemos como combinar várias formas para criar campos de distância mais complexos. Aprendi a maioria das técnicas descritas aqui na biblioteca glsl-sign de funções de distância, que podem ser encontradas aqui . Existem também várias maneiras de combinar formas, as quais não discuto aqui.


Preparação


Para visualização de campos de distância assinados (campos de distância assinados, SDF), usaremos uma configuração simples e aplicaremos os operadores a ela. Para exibir campos de distância, ele usará a visualização de linhas de distância desde o primeiro tutorial. Por uma questão de simplicidade, definiremos todos os parâmetros, exceto os parâmetros de visualização no código, mas você pode substituir qualquer valor por uma propriedade para torná-lo personalizável.

O shader principal com o qual começaremos se parece com isso:

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 } 

E a função 2D_SDF.cginc na mesma pasta do shader, que expandiremos, a princípio se parece com isso:

 #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 

Combinações simples


Começaremos com algumas maneiras simples de combinar duas formas para criar uma grande forma, conjugações, interseções e subtrações, bem como uma maneira de transformar uma forma em outra.

Emparelhamento


O operador mais simples está emparelhando. Com ele, podemos juntar as duas figuras e obter a distância com o sinal da figura conectada. Quando temos uma distância com o sinal de duas figuras, podemos combiná-las tomando a menor das duas usando a função min .

Devido à escolha do menor dos dois valores, o número final estará abaixo de 0 (visível), onde um dos dois números recebidos tem uma distância até a borda menor que 0; o mesmo se aplica a todos os outros valores de distância, mostrando uma combinação de duas figuras.

Aqui vou nomear a função para criar a conjugação “mesclar”, em parte porque os estamos mesclando, em parte porque a palavra-chave union em hlsl é reservada, portanto, não pode ser usada como o nome da função.

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




Interseção


Outra maneira comum de conectar formas é usar áreas nas quais duas formas se sobrepõem. Para fazer isso, usamos o valor máximo das distâncias das duas figuras que queremos combinar. Ao usar o maior dos dois valores, obtemos um valor maior que 0 (fora da figura), quando qualquer uma das distâncias até duas figuras está fora da figura e outras distâncias também são alinhadas da mesma maneira.

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


Subtração


No entanto, geralmente não queremos processar as duas formas da mesma maneira e precisamos subtrair a outra de uma forma. Isso é bastante fácil de fazer, cruzando a forma que queremos mudar e a forma que queremos subtrair. Obtemos os valores opostos para as partes interna e externa da figura, invertendo a distância com o sinal. O que era 1 unidade fora da figura agora é 1 unidade dentro.

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


Interpolação


Uma maneira óbvia de combinar duas figuras é interpolar entre elas. Também é possível, até certo ponto, para malhas poligonais com formas de mistura, mas é muito mais limitado do que o que podemos fazer com campos de distância sinalizados. Pela simples interpolação entre as distâncias de duas figuras, conseguimos um fluxo suave de uma para a outra. Para interpolação, você pode simplesmente usar o método 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); 


Outros compostos


Tendo recebido conexões simples, já temos tudo o necessário para uma combinação simples de figuras, mas a incrível propriedade dos campos dos sinais de distância é que não podemos nos limitar a isso, existem muitas maneiras diferentes de combinar figuras e executar ações interessantes nos locais de suas conexões. Aqui explicarei apenas algumas dessas técnicas novamente, mas você pode encontrar muitas outras na biblioteca http://mercury.sexy/hg_sdf (escreva-me se conhecer outras bibliotecas SDF úteis).

Arredondamento


Podemos interpretar a superfície de duas figuras combinadas como o eixo xe o eixo y da posição no sistema de coordenadas e, em seguida, calcular a distância até a origem da coordenada nessa posição. Se fizermos isso, obteremos uma figura muito estranha, mas se limitarmos o eixo a valores abaixo de 0, obteremos algo que se assemelha à conjugação suave das distâncias internas de duas figuras.

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


Isso é lindo, mas não podemos usar isso para alterar a linha em que a distância é 0; portanto, essa operação não é mais valiosa que o emparelhamento comum. Mas antes de conectar as duas figuras, podemos aumentá-las um pouco. Da mesma maneira que criamos um círculo, para aumentar uma figura, subtraímos a distância, a fim de empurrar uma linha mais adiante, na qual a distância com um sinal é 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); } 


Ele apenas aumenta a figura e garante transições suaves por dentro, mas não queremos aumentar as figuras, precisamos apenas de uma transição suave. A solução é subtrair o raio novamente após calcular o comprimento. A maioria das partes terá a mesma aparência de antes, exceto pela transição entre as figuras, que é suavemente suavizada de acordo com o raio. Vamos ignorar a parte externa da figura por enquanto.

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


O último passo é consertar a parte externa da figura. Além disso, enquanto o interior da figura é verde, usamos essa cor para o exterior. O primeiro passo é trocar as partes externa e interna, simplesmente invertendo a distância com um sinal. Em seguida, substituímos a parte em que o raio é subtraído. Primeiro, mudamos de subtração para adição. Isso é necessário, porque antes de combinar com o raio, traçamos a distância do vetor, portanto, de acordo com isso, precisamos reverter a operação matemática usada. Em seguida, substituiremos o raio pelo posicionamento usual, o que fornecerá os valores corretos fora da figura, mas não próximos às bordas e dentro da figura. Para evitar isso, tomamos o máximo entre o valor e o raio, obtendo assim um valor positivo dos valores corretos fora da figura, bem como a adição do raio que precisamos dentro da figura.

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


Para criar uma interseção, precisamos fazer o oposto - reduzir as figuras pelo raio, garantir que todos os componentes do vetor sejam maiores que 0, pegar o comprimento e não alterar seu sinal. Então, criaremos a parte externa da figura. Então, para criar a parte interna, fazemos a interseção usual e garantimos que ela não seja menor que o raio. Então, como antes, adicionamos os valores internos e externos.

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


E, como último ponto, a subtração pode ser novamente descrita como a interseção entre a figura base e tudo, exceto a figura que estamos subtraindo.

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


Aqui, e especialmente ao subtrair, você pode ver artefatos decorrentes da suposição de que podemos usar duas figuras como coordenadas, mas para a maioria das aplicações, os campos de distância ainda são bons o suficiente.

Chanfro


Também podemos cortar a transição para obter um ângulo como um chanfro. Para alcançar esse efeito, primeiro criamos uma nova forma adicionando as duas existentes. Se assumirmos novamente que o ponto no qual as duas figuras se encontram é ortogonal, essa operação nos dará uma linha diagonal que passa pelo ponto de encontro das duas superfícies.


Como simplesmente adicionamos os dois componentes, a distância com o sinal dessa nova linha tem uma escala incorreta, mas podemos corrigi-la dividindo-a pela diagonal de uma unidade quadrada, ou seja, a raiz quadrada de 2. Divisão pela raiz de 2 é a mesma que multiplicando pela raiz quadrada de 0,5 e podemos simplesmente escrever esse valor no código para não calcular sempre a mesma raiz.

Agora que temos uma forma que tem a forma de um chanfro desejado, vamos expandi-la para que o chanfro se estenda além dos limites da figura. Da mesma maneira que antes, subtraímos o valor necessário para aumentar a figura. Em seguida, combinamos a forma do chanfro com a saída da mesclagem usual, resultando em uma transição chanfrada.

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


Para obter um chanfro cruzado, adicionamos duas figuras, como antes, mas reduzimos a figura adicionando o valor do chanfro e cruzamos com uma figura cruzada regular.

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


E, similarmente às subtrações anteriores, também podemos realizar a interseção com a segunda figura invertida aqui.

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


Interseção arredondada


Até agora, usamos apenas operadores booleanos (exceto a interpolação). Mas podemos combinar as formas de outras maneiras, por exemplo, criando novas formas arredondadas nos locais onde as bordas das duas formas se sobrepõem.

Para fazer isso, precisamos novamente interpretar as duas figuras como o eixo xe o eixo y do ponto. Então, simplesmente calculamos a distância desse ponto até a origem. Onde os limites das duas figuras se sobrepõem, a distância para ambas as figuras será 0, o que nos dá uma distância de 0 ao ponto de origem do nosso sistema de coordenadas imaginário. Então, se tivermos uma distância da origem, podemos realizar as mesmas operações que para os círculos e subtrair o raio.

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


Entalhe na borda


A última coisa que explicarei é a maneira de criar um entalhe em uma forma na posição da borda de outra forma.

Começamos calculando a forma do limite do círculo. Isso pode ser feito obtendo o valor absoluto da distância da primeira figura, enquanto as partes interna e externa serão consideradas a parte interna da figura, mas a borda ainda terá o valor 0. Se aumentarmos essa figura subtraindo a largura do entalhe, obtemos a figura ao longo da borda da figura anterior .

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


Agora precisamos que a borda do círculo seja mais profunda apenas pelo valor que especificamos. Para fazer isso, subtraímos uma versão reduzida da figura base. A quantidade de redução na forma da base é a profundidade do entalhe.

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


O último passo é subtrair o entalhe da forma base e retornar o resultado.

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


Código fonte


A biblioteca



 #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 

Base de sombreador



 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/pt438954/


All Articles