본문으로 바로가기

2D Signed Distance Field Basics

category Technical Report/Unity Shader 2021. 2. 10. 19:08
반응형

 

 

 

원문링크 : http://www.ronja-tutorials.com/post/034-2d-sdf-basics/

원문 레거시 shader code를 URP용으로 바꿔서 정리한 내용.

 

기본 예제 코드.

 

Shader "URPTraining/2Ddistance"
{
   Properties {  
         _TintColor("Test Color", color) = (1, 1, 1, 1)
         _Intensity("Range Sample", Range(0, 1)) = 0.5
         _MainTex("Main Texture", 2D) = "white" {}           
           }  

    SubShader
    {    
        Pass
        {  
        
         Name  "UniversalForward"
         Tags {"RenderPipeline"="UniversalForward" "RenderType"="Opaque" "Queue"="Geometry"}

         HLSLPROGRAM
         #pragma prefer_hlslcc gles
         #pragma exclude_renderers d3d11_9x
         #pragma vertex vert
         #pragma fragment frag
           
          #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
                        
          half4 _TintColor;
          float _Intensity;

          struct VertexInput
               {
                float4 vertex : POSITION;
                };

            struct VertexOutput
                {
                float4 position : SV_POSITION;
                float3 worldPos : TEXCOORD0;
                };

          VertexOutput vert(VertexInput v)
              {
              VertexOutput o;     
              o.position  =  TransformObjectToHClip(v.vertex.xyz);
              o.worldPos = TransformObjectToWorld(v.vertex.xyz);

             return o;
              }

              float scene(float2 position)
              {              
                return 0;
              }
            
            float4 frag(VertexOutput i) : SV_Target
              {                      
                float dist = scene(i.worldPos.xz);
                float4 col = float4(dist, dist, dist, 1);
                return col;                         
               }           
            ENDHLSL  
          }
     }
}

 

위에 로컬을 월드로 변환하는 행렬은 SpaceTransforms.hlsl에 아래와 같이 정의되어 있다.

 

float3 TransformObjectToWorld(float3 positionOS)
{
    #if defined(SHADER_STAGE_RAY_TRACING)
    return mul(ObjectToWorld3x4(), float4(positionOS, 1.0)).xyz;
    #else
    return mul(GetObjectToWorldMatrix(), float4(positionOS, 1.0)).xyz;
    #endif
}

 

float3타입으로 반환해야 하기 때문에 위와 같이 interpolate를 float3타입으로 맞춰주던가 아래와 같이 사용할 수 있다.

o.worldPos = float4(TransformObjectToWorld(v.vertex.xyz), 1.0);

 

 

// in include file

// include guards that keep the functions from being included more than once
#ifndef SDF_2D
#define SDF_2D

// functions

#endif

 

 

이제 shader에서 아래와 같이 include 시켜준다.

            #include "2D_SDF.hlsl"

 

Circle

2D_SDF.hlsl에 아래와 같이 추가한다.

 

#ifndef SDF_2D
#define SDF_2D

float circle(float2 samplePosition, float radius)
{
    return length(samplePosition);
}
#endif

 

 

Scene 계산을 아래와 같이 바꿔준다.

 

   float scene(float2 position)
                {
                    float sceneDistance = circle(position, 2);
                    return sceneDistance;
                }

 

float dist = scene(i.worldPos.xz);
                float4 col = float4(dist, dist, dist, 1);

월드공간 (0, 0)[각주:1] 을 기준으로 거리값을 구한다.

length(x) : 벡터의 길이를 계산한다. 하나의 float를 리턴한다. 즉 월드공간의 포지션값(vector)을 단일 scalar 으로 변환하게 된다.

 

 

 

 
이렇게 나오게 된다. 즉 (0, 0, 0)에서 해당 포지션의 거리 값은 0 이므로 가장어두운 검정(0, 0, 0). 원의 중심으로 부터 멀어질수록 루트값으로 밝기가 증가하는 값을 가지게 된다.
 
이제 2D_SDF.hlsl에 반지름을 핸들링 할수 있는 변수를 추가한다.

 


float circle(float2 samplePosition, float radius)
{
    return length(samplePosition) - radius  ;
}



 

 

이제 원의 중심을 이동시켜 본다. 아래 함수를 선언해준다.

 

 


float2 translate(float2 samplePosition, float2 offset)
{
    return samplePosition - offset;

}


 

 

이제 픽셀 셰이더에서 3,2로 원의 중심을 기준으로 계산하도록 넣어준다.

 


float scene(float2 position)
                {                       
                    float2 circlePosition = translate(position, float2(3, 2));
                    float sceneDistance = circle(circlePosition, 2);
                    return sceneDistance; 
                }

 

 

 

 

 

Rectangle

사각형은 수식은 비슷한데 특정값에서 값을 반올림하거나 버림하는 함수를 사용하게 된다.

 

float rectangle(float2 samplePosition, float2 halfSize)
{
    float2 EdgeDistance = abs(samplePosition) - halfSize;
    float   Distance = length(max(EdgeDistance, 0));
    return Distance;
}

 

max(a, b) : a와 b중 큰 수를 반환한다.

 

 float scene(float2 position)
                {                       
                    float2 circlePosition = translate(position, float2(0, 0));
                    float  sceneDistance = rectangle(circlePosition, float2(1, 1));
 
                    return sceneDistance;           
                }

 

 

 

Rotate

로테이션은 사각형 그리는것과 크게 다르지 않다.

 

 

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

 
PI 선언은 원문에는 선언해서 사용하지만  URP는 CoreRP/ShaderLibrary/Macro.hlsl에 선언되어 있어 주석처리로 빼줘야 한다.
 

 

Rotate 앵글 값을 파이를 2배 곱해서 회전 속도를 계산하며 -1을 곱해서 시계방향으로 회전하는 값을 얻는다
 

 

 float scene(float2 position)
                {                       
                    float2 circlePosition = position;
                    circlePosition = rotate(circlePosition, _Time.y);
                    circlePosition = translate(circlePosition, float2(2, 0));
                    float sceneDistance = rectangle(circlePosition, float2(1, 2));
                    return sceneDistance;       
                }

 

translate의 x,y값은 x, y  축 회전 하는 중심 축을 설정하게 된다.

 

 

 

 

 

 

float scene(float2 position)
                {                       
                        float2 circlePosition = position;
                        circlePosition = translate(circlePosition, float2(2, 0));
                        circlePosition = rotate(circlePosition, _Time.y);
                        float sceneDistance = rectangle(circlePosition, float2(1, 2));
                        return sceneDistance; 
                }

 

 

 

이제 UV 값을 기준으로 값을 편집하는 것을 해본다.


struct VertexInput
            {
             float4 vertex : POSITION;
             float2 uv    : TEXCOORD0;
             };

         struct VertexOutput
             {
            float4 position : SV_POSITION;
            float2 uv       : TEXCOORD0;
             };

VertexOutput vert(VertexInput v)
           {
           VertexOutput o;
           o.position = TransformObjectToHClip(v.vertex.xyz);
           o.uv = v.uv;
           return o;
           }

 

픽셀 셰이더에서 아래와 같이 출력해본다.


float4 frag(VertexOutput i) : SV_Target
           { 
           float4 col = i.uv.x;
           return col;    
            }

 

UV의 X(U)좌표를 메시에 그린다.

위 이미지와 같은 그라데이션이 출력된다. Y를 대신 넣으면 이제 Y(V) 값이 화면에 그려진다.
 

 

아래와 같이 픽셀셰이더에서 계산하면 shader graph에서 볼수 있는 UV 노드처럼 출력된다.


float4 frag(VertexOutput i) : SV_Target
           { 
           float4 col = float4(i.uv.x, i.uv.y, 0, 1);
           return col;    
            }

 

 

왼쪽아래가 (0,0). 왼쪽 위가 (0,1), 오른쪽 아래가 (1,0), 오른쪽 위가 (1,1)이다.

 
그라데이션이 눈으로 보는것과 차이가 다른이유는 감마보정 때문이다. 이를 우리가 익히 아는 그라데이션처럼 처리하려면 아래와 같이 해준다.
 


float4 col = pow(i.uv.x, 2.2);

 

 

 

자세한 내용은 감마가 어디감마 참조~[각주:1]


float4 frag(VertexOutput i) : SV_Target
           { 
                float dist = scene(i.uv.xy);
                float4 col = float4(dist, dist, dist, 1);
                return col;    
            }

 
world postion으로 생성했던것을 메시의 uv값을 읽어들이고 이를 버텍스 셰이더를 통해 픽셀셰이더로 전달해서 화면에 그리게 된다.
UV는 0,0부터 시작하므로 월드 공간과 달리 메시의 가장 끝점을 기준으로 계산하게 된다. 가운데가 0.5이므로 포지션을 0.5로 옮겨주면
 

와 같이 화면 중심을 기준으로 그리게 된다. 이제 함수를 사용해 다양한 도형을 만들어 본다.

 

 

#ifndef SDF_2D



 

#ifndef SDF_2D

 

#ifndef SDF_2D

 



 

 

 

 

 

 

  1. 높이값인 Y는 제외한 x,z만을 계산한다. [본문으로]
반응형