top of page

(UnityShader) Koch Snowflake Part 1

  • 작성자 사진: Nobody
    Nobody
  • 2020년 12월 6일
  • 5분 분량

최종 수정일: 2022년 12월 25일


ree

이번 포스트에서는 코흐의 눈송이 곡선(Koch Snowflake)을 만들어 볼 것입니다.

스웨덴수학자 헬게 폰 코흐(Helge von Koch)가 고안한 프랙탈 도형이라서 이런 이름이 붙었습니다.


프랙탈 속성에 대해서는 이전 포스트(링크)에서 한 번 다룬 적이 있었습니다. 이 도형의 특징은 정삼각형에서 시작해서 각 변을 삼등분 한 길이의 삼각형을 바깥에 그립니다. 똑같은 과정을 무한히 반복 할 수 있습니다. 도형의 모양이 눈송이 같아서 이런 이름이 붙었습니다.


ree

선분 하나로 시작해서 문제를 단순하게 풀어 나가 봅시다. uv 데이터를 가지고 선분을 그릴 것 입니다.


ree
float4 frag (v2f i) : SV_Target
{
 float4 col;
 float4 MainTex = tex2D(_MainTex, i.uv);

 col.rgb = step( 0.01 , abs(i.uv.y - _var) );
 col.a = 1;
 
 return col;
}

uv에서 절대값 함수로 음수 부분을 없앤 뒤 step 함수로 두께 0.01 크기의 선분을 그렸습니다. 하지만 이 상태면 선분의 길이를 조절 할 수 없으니 uv의 x축으로 조절 해봅시다.



ree
float4 frag (v2f i) : SV_Target
{
 float4 col;
 float4 MainTex = tex2D(_MainTex, i.uv);

 float m = 1-min( step(_var1, i.uv.x) , step(_var1, 1-i.uv.x) );

 col.rgb = saturate( step( 0.01 , abs(i.uv.y - _var) ) + m );
 col.a = 1;

 return col;
}

이제 길이 조절이 되긴 하지만, x 축으로 길이 값을 더하고 saturate 함수를 써서 0~1 사이로 자르는 과정에서 코드가 번잡해지고 비효율적입니다. 가로와 세로를 한 번에 조절 할 수 있는 좀 더 직관적인 함수를 사용해서 처리 해봅시다.



length(x);


Returns the length of the vector x.



ree
col.rgb = length(i.uv);
col.rgb = distance(i.uv, float2(0,0));

length 함수는 원점(0,0)에서 부터 벡터의 길이를 표현합니다. OpenGl을 사용하는 유니티의 uv는 좌측 하단이 (0,0)이니 거리 값 0을 반환하고 방사형으로 원점에서 멀어질 수록 1에 가까워지는 것을 볼 수 있습니다.


distance() 함수도 똑같이 벡터의 거리를 반환합니다. 대신 원점이 아닌 다른 벡터를 집어넣을 수 있습니다.



ree
col.rgb = length(i.uv - _var);

따라서 원점(0,0)을 오프셋으로 이동시키면 위 처럼 움직이게 됩니다. 타일링도 당연히 적용이 됩니다.


ree
col.rgb = i.uv.y - 0.5;

원점과의 거리 값을 색으로 표현한 것이니 이렇게 0.5만큼 오프셋 이동한 uv의 x축 만을 length() 함수로 처리할 수 있을 것입니다.


ree
col.rgb = length(i.uv.y - 0.5);
col.rgb = length(i.uv - float2(i.uv.x, 0.5) );

뺄셈으로 x축 값을 제거 해버리면 항상 벡터(x,y)의 x값은 0이고 y값에 의해 거리가 결정됩니다.


거리 값은 '절대값'이기 때문에 uv에서 음수로 넘어간 부분의 절댓값을 반환합니다. 예를 들어 float2(0,-0.5)와 원점 과의 거리는 0.5입니다. 따라서 y축 자체가 원점이 되어 길쭉한 모양이 됩니다.


그렇다면 축의 길이는 어떻게 조절해야 할까요?


생각해봅시다. y축은 0.5로 고정이고, float2(x,0.5) 좌표에 있는 x 값은 항상 0인 동시에 0~1 그라데이션(gradation)이 유지되어야 합니다.



clamp(x, min, max);


Clamps x to the range [min, max].

ree
col.rgb = clamp(i.uv.x, _var, 1 - _var); 

clamp 함수는 최솟값 min과 최댓값 max 사이로 값을 잘라냅니다. saturate 함수가 0과 1 사이의 값으로 잘라내는 것과 같습니다.


그래서 위 이미지 처럼 min값이 max 값의 반전이면 그라데이션이 유지되면서 x축의 양쪽 끝 값만 영향을 받습니다.


ree

(i.uv.x)


그렇다면 clamp한 값을 uv.x값에서 뺄셈 한다면 어떻게 될까요?


ree
col.rgb = i.uv.x - clamp(i.uv.x, _var, 1 - _var); 

var 값이 0이라면 clamp(x,0,1) 이고 그러면 원래 uv.x 값과 똑같으니까 전체에서 전체를 뺄셈한 것이니 모든 픽셀의 값은 0입니다.



ree
col.rgb = abs(i.uv.x - clamp(i.uv.x, _var, 1 - _var) ); 

var 값이 0.5이라면 clamp(x,0.5,0.5) 라서 전체에서 0.5 만큼 오프셋 이동하게 됩니다. 그리고 검은색 부분에는 -0.5의 값을 가지게 됩니다. (알기 쉽게 절대값 abs 함수를 사용했습니다.)


ree
col.rgb = length(i.uv - float2( clamp(i.uv.x, _var, 1 - _var), 0.5) );

코드가 깔끔하게 한 줄로 정리됩니다. 이것으로 우리는 원하는 길이와 두께의 선분을 그릴 수 있게 되었습니다.


ree
float4 frag (v2f i) : SV_Target
{
 float4 col;
 float4 MainTex = tex2D(_MainTex, i.uv);

 float segment = length(i.uv - float2( clamp(i.uv.x, _var, 1 - _var), 0.5) );
 
 col.rgb = step(0.05, segment);
 col.a = 1;

 return col;
}

여기까지해서 선분 하나를 그렸습니다. 다음은 선분을 삼등분 하는 과정인데, UV의 특성을 생각해봅시다.


ree
float2 uv = i.uv;
float segment = length(uv - float2( clamp(uv.x, _var, 1 - _var), 0.5) );
col.rgb = step(0.05, segment) * float3(uv,0);

UV를 출력 해서 선분과 곱하여 uv가 보이게 하는 것이 이해에 도움이 될 것입니다.


ree

선분을 삼등분 하고 같은 길이로 삼각형을 그린다는 것은 다르게 생각하면 대칭되는 꺾인 선분입니다.


지금까지 uv의 양수 부분에서 선분을 그렸지만 음수 부분에서 똑같이 선분을 그리고 원점을 기준으로 가로 대칭 시키면 될 것 같습니다.


ree
float2 uv = float2(i.uv - 0.5) * 1;
uv.x = abs(uv.x);
col.rgb = float3(uv,0);

0.5를 뺄셈 해서 원점(0,0)을 중앙으로 옮기고, 절대값 abs 함수를 사용해서 uv.x를 가로로 대칭 했습니다.


ree
float segment = length(uv - float2( clamp(uv.x, _var, 1-_var), 0) );
col.rgb = step(0.05, segment) * float3(uv,0);

짜잔. 선분이 두개가 됬습니다. 다만 지금 선분을 그리는 공식으로는 위 이미지 처럼 0.5 값에서 오프셋이 되기 때문에 수정해줍시다.


ree
float segment = length(uv - float2( clamp(uv.x, -_var, _var), 0) );
col.rgb = step(0.05, segment) * float3(uv,0);

선분은 여전히 하나인 것 처럼 보입니다. 사실 두 개의 선분이 그려지는 영역이 완전히 겹쳐 있어서 그렇습니다. x축 값이 0이 되는 부분을 늘려서 선분을 양쪽으로 분리시켜 봅시다.


ree
float2 uv = float2(i.uv - 0.5) * 2;
uv.x = abs(uv.x) - 0.5;
float segment = length(uv - float2( clamp(uv.x, -0.25, 0.25), 0) );
col.rgb = 1-step(0.05, segment) + float3(uv,0);

간단히 x축에서 0.5 길이 만큼 0이 들어가는 영역을 늘렸습니다. 그리고 선분이 잘 보이도록 흰색으로 변경했습니다.


이제 선분을 회전시키는 함수를 만듭시다.


앞서 우리는 uv로 선분을 만들었습니다. 따라서 선분을 회전시킨다는 것은 uv를 회전시키는 것과 같습니다. 이전 포스트에서 회전 행렬(링크)에 대해 다룬 적이 있었습니다.


ree
ree
col.rgb = float3(RotateUV(i.uv, _var1), 0);

하지만 저희가 원하는 것은 uv 전체가 회전 하는 것이 아닌 특정 위치에서 uv를 꺾어서 원하는 각도를 만드는 것입니다. 회전 행렬을 그대로 쓰지 않고 응용 해봅시다.



float ReflectUV(float2 uv, float degrees)
            {
 float Degrees2Rad = UNITY_PI * 2 / 360;
 float Radians = degrees * Degrees2Rad;
 float c = cos(Radians);
 float s = sin(Radians);
 float2 rotateMatrix = float2(s,c);
 float result = dot(uv - 0.5, rotateMatrix);

 return result;
            }

먼저 각도 변수를 사용하기 쉽게 라디안으로 변환하는 것 까지는 기존 코드와 같지만 회전 행렬을 간소화 했습니다.


ree
float c = cos(Radians);
float s = sin(Radians);
float2 rotateMatrix = float2(s,c);

저희같은 아티스트에게는 호도법 보다는 60분법으로 설명하는 것이 친화적이고 직관적이라 각도로 치환해서 써보겠습니다.

float result = dot(uv - 0.5, rotateMatrix);

uv와 내적 연산을 하면 x * s + y * c 입니다.


sin(0) = 0이고, cos(o) = 1입니다. float2(0, y)

sin(90) = 1이고, cos(90) = 0입니다. float2(x, 0)

sin(180) = 0이고, cos(180) = -1입니다. float2(0, -y)

sin(270) = -1이고, cos(270) = 0입니다. float2(-x, 0)


uv의 x와 y를 따로 보면 이해가 쉬울 것입니다.


ree

(i.uv.x -0.5)

0, 1, 0, -1, 0

ree

(i.uv.y -0.5)

1, 0, -1, 0, 1

ree
col.rgb = abs( ReflectUV(i.uv, _var1) );

내적 연산으로 x와 y의 비율을 결정하여 회전을 구현한 아이디어 입니다. 삼각함수가 -1~1의 값을 순환하기 때문에 회전 행렬과 똑같이 동작합니다.



ree
float Degrees2Rad = UNITY_PI * 2 / 360;
float Radians = _var1 * Degrees2Rad;
float c = cos(Radians);
float s = sin(Radians);
float2 rotateMatrix = float2(s,c);
float distance = dot(i.uv - 0.5, rotateMatrix);
col.rgb = distance;

저희는 굴절 되는 각도 값(rotateMatrix)도 알고 있고, 원점에서의 거리 값(distance)도 알고 있습니다. 절대값 함수를 빼면 한 쪽은 음수라서 전부 검은 색으로 출력됩니다.


ree

세타 값 만큼 선분이 기울어지려면 음수 혹은 양수 중 한 부분을 제외하고 회전해야합니다. 위 이미지 대로 동작하려면 음수 부분의 uv는 그대로여야 한다는 뜻입니다.


ree
float2 result = (rotateMatrix * distance);
col.rgb = 1-step(0.01, abs(distance) ) + float3(result, 0);

알아보기 쉽게 선분을 따로 출력해서 표현 했습니다. 현재는 전체 uv가 다 같이 회전합니다. 저희는 uv를 굴절 시켜야 하죠.


각도 값은 x * s + y * c 였습니다. 각도 값에 거리 값을 곱하고 출력하면 위 자료 처럼 되죠. 이 값을 uv에서 뺄셈하면 uv가 회전 할 것입니다.


ree

(i.uv - 0.5)


조금 혼란이 오는 부분이라 천천히 설명하겠습니다. uv가 회전하는 원리는 다음과 같습니다.


uv에서 0.5만큼 뺄셈 하면 x와 y 값이 모두 오프셋 되어 0,0이 중심이 됩니다.


ree
col.rgb = float3(rotateMatrix * distance * 1,0);

rotateMatrix * distance 을 봅시다. 중심이 (0,0)이기 때문에 -0.5 ~ +0.5 범위를 가지고 있습니다.


2를 곱하면 -0.5 ~ 0.5였던 값이 -1 ~ 1이 됩니다.


따라서 -0.5 ~ 0.5의 uv에서 (rotateMatrix * distance * 2)를 뺄셈하면?


ree

0.5에서 -1 하여 -0.5가 되고, -0.5에서 -(-1) 하여 0.5가 됩니다. 부호가 반전 되어 회전 합니다. 위에서 본 삼각함수와 똑같습니다.


ree
float2 result = (i.uv - 0.5) - (rotateMatrix * distance * 2);
col.rgb =  float3( result, 0);

이렇게 됩니다. 이제 음수와 양수 부분중 한 쪽만 회전시켜 봅시다.



min(x, y);


Selects the lesser of x and y.


max(x, y);


Selects the greater of x and y.


ree
col.rgb = float3( (rotateMatrix * min(0,distance) * 2) , 0);

min(x,y) 함수는 x와 y중 작은 값을 반환합니다. 따라서 거리 값에서 음수 부분인 -1 ~ 0만 남습니다.


max(x,y) 함수는 x와 y중 큰 값을 반환합니다. 따라서 거리 값에서 양수 부분인 +1 ~ 0 만 남습니다.


ree
ree
float2 result = (i.uv - 0.5) - (rotateMatrix * min(0,distance) * 2);
col.rgb = 1-step(0.01, abs(distance)) + float3( frac(result * 10) ,0);

이렇게 양수와 음수 부분중 원하는 부분을 굴절 할 수 있습니다.


과정이 길어지기 때문에 2부로 나눠서 작성하겠습니다.


다음 포스트에서는 for 구문으로 선분에 프랙탈을 적용하고, uv에 텍스쳐를 입혀 결과물을 완성 하는 것 까지 하겠습니다.




Comments


bottom of page