momodudu.zip

#3 ShaderToy - 원을 이용한 간단한 예제(Smile) 본문

Graphics/ShaderToy

#3 ShaderToy - 원을 이용한 간단한 예제(Smile)

ally10 2019. 9. 16. 15:00

간단한 예제를 다뤄보던중에, 아래와 같은 유튜브 채널을 발견했다.

https://www.youtube.com/watch?v=GgGBR4z8C9o

 

ShaderToy Tutorial에 대해 처음부터 차근차근 잘 설명해주고 있다. 원을 그리고 나서는 뭘해야될까..라고 생각중이었는데, 기초부터 따라가기에 아주 좋은것 같아서 이 강좌를 따라서 step by step으로 포스팅 해보려고 한다.

 

 

float circle(vec2 uv, float r, float edgeWidth)
{
    return smoothstep(r,r-edgeWidth,length(uv));
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from 0 to 1)

    vec2 uv = fragCoord/iResolution.xy;
	uv.xy -= 0.5;
    uv.x *= iResolution.x / iResolution.y;

    
    float d = circle(uv, 0.3, 0.05);


    // Output to screen
    fragColor = vec4(vec3(d),1.0);
}

하나씩 따라가고자 함수형을 조금 바꾸었다.ㅎㅎ

 

원을 만드는 함수를 따로 뺐고, 첫번째는 uv coordinate, 두번째는 원의 반지름, 즉 원의 크기이고 마지막은

smooth step을 넣을 범위, the width of edge. 로 정의한다.

 

 

그리고 이제 원을 평행이동 할 수 있는 인자를 하나 더 끼워넣어야 한다.

앞서 설명한것처럼, 평행이동은 uv에서 +/-를 하면서 원의 중심을 이동시킬 수 있다.

그래서 위 함수의 Circle에 특정 offset을 주어서 원을 이동시켜본다.

float circle(vec2 uv, vec2 offset,float r, float edgeWidth)
{
    return smoothstep(r,r-edgeWidth,length(uv-offset));
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from 0 to 1)

    vec2 uv = fragCoord/iResolution.xy;
	uv.xy -= 0.5;
    uv.x *= iResolution.x / iResolution.y;

    
    vec2 offset = vec2(0.2,0.2);
    float d = circle(uv, offset, 0.3, 0.05);


    // Output to screen
    fragColor = vec4(vec3(d),1.0);
}

circle함수에 파라미터로 vec2 인자의 offset 값을 넣었다.

그리고 실제 사용부에서는 0.2,0.2만큼 주었다.

즉 우리가 사용했던 스크린 중심의 0,0에서 +0.2/+0.2만큼 평행이동 시킨 결과라고 보면 된다.

offset값에 cos(iTime)을 넣어서 원이 직접 움직이게 만들어볼 수도 있다. 이건 일단 나중에 다루고..

 

 

원을 하나 더 추가하고자 한다.

 

float circle(vec2 uv, vec2 offset,float r, float edgeWidth)
{
    return smoothstep(r,r-edgeWidth,length(uv-offset));
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from 0 to 1)

    vec2 uv = fragCoord/iResolution.xy;
	uv.xy -= 0.5;
    uv.x *= iResolution.x / iResolution.y;

    float d = circle(uv, vec2(0,0), 0.3, 0.05);
	d += circle(uv, vec2(-.3,.3), 0.1, 0.01);

    // Output to screen
    fragColor = vec4(vec3(d),1.0);
}

기존 circle(r=0.3)인 코드 밑에 보면 한줄을 더 추가했다.

r=0.1이고 중심이 -0.3,0.3인 원을 한개 더 추가했다.

 

음.. 이부분은 개인적으로는 처음 그래픽스, 혹은 쉐이더를 입문한 사람에게는 좀 혼란스러울 수 있을거라 생각한다.

왜냐면 내가 그랬거든.. "원을 하나 더 추가한다"라는 개념이 d라는 컬러값에다가 플러스 연산만 하면 된다니,

어떻게 보면 직관적이고 쉽지만 이 깊은뜻을 이해하는데는 나는 꽤나 오래걸렸다.

여담이지만 그래픽스가 프로그래밍 영역에서도 진입장벽이 높다고 생각하는게, 수학이론이 많이 들어가는 이유도 있지만 딱 이런 개념에서 어렵다고 느끼게 되는것 같다.

이제까지 내가 생각해온 더하기 연산자와, 쉐이더상의 더하기 연산자는 느낌이 다르다. 뭔가 딱 정의할수 없지만.. 지금 이 fragment shader를 이해하고 있는 프로그래머라면 공감할거라고 생각한다.

왜냐면 얘는 CPU가 아니라 GPU이고, 픽셀단위로 모두 저 함수를 실행하고 있기 때문이다.

 

음... 예를들자면, CPU로 어떤 특정 n*n의 배열 안에 1,0으로 원을 채운다고 생각을 해보면

for문을 돌면서 (n/2,n/2) 중심으로 부터의 거리값이 r이내인 배열 array에다가 1을 채워넣을것이고 아니면 0을 넣을것이다.

그런데 쉐이더는 다르다. 이 하나의 function 자체가 전체 동작을 정의하는게 아니고, 프래그먼트 하나의 동작을 정의하는것이기때문에 그래픽스 병렬체계를 처음 접하는 프로그래머에겐 좀 낯선 개념이라고 생각한다.

물론 내가 멍청해서 그럴수도 있다...

하지만 5년동안 CPU처럼 사고하다가, GPU처럼 사고하려니까 이런 간단한 예제들조차도 굉장히 애를 먹었다.(물론 지금도 삽질은 엄청나게 함)

 

 

 

어찌됐던간에, 다시 이 d의 value에 +를 했는데, 어떻게 원이 생기는지에 대해 설명을 해보자면,

없던 원이 생긴 영역의 fragment 입장에서 생각해보면 쉽다.

 

새로 하나 더 만든 원은 원점이 (-0.3,0.3)이다. 이 원점에 해당되는 빨간색 fragment입장에서 생각해보면,

    float d = circle(uv, vec2(0,0), 0.3, 0.05); /// 1st
	d += circle(uv, vec2(-.3,.3), 0.1, 0.01);	/// 2nd

 

위 컬러값을 지정하는 코드에서, 1st의 코드만 존재했다면 이 fragment는 d가 0으로 리턴이 되었을것이다.

왜냐면 circle함수는 (0,0)으로부터 거리가 0.3인 픽셀들만 1로 리턴을 해주니까.

그래서 (-0.3,0.3)의 fragment가 해당 함수로 들어갔을 땐 0으로 리턴이 되었고, 이걸 RGB채널에 넣으니까

해당 픽셀은 검은색으로 출력될 수 있었던것이다.

 

그런데 2nd라인이 추가되면, (-0.2,0.2) fragment가 첫번째 함수를 통과했을때는 0으로 나왔지만,

두번째 circle함수에서는 1로 리턴되었다. 왜냐면 -0.3,0.3으로부터 거리 0.1 이내인 fragment에 속하기 때문이다.

그래서 결국 이 픽셀은 RGB채널 (1,1,1)을 가질 수 있는 것이다.

 

원 안의 다른 픽셀들도 같은 원리다.

 

어떻게 보면 쉽지만, 이걸 몸에 체득하기까지는 내 경우에는 좀 오래걸렸다. 머리가 싱싱할때 배우지 않아서 그런가-_-..

그러면 이제 이 원리를 이용해서 중앙의 큰 흰색의 원 안에 검은색 원을 넣어보자.

 

 

 

 

    float d = circle(uv, vec2(0,0), 0.3, 0.05); /// 1st
	d -= circle(uv, vec2(-.1,.1), 0.1, 0.01);	/// 2nd

이번엔 작은 원의 중심을 큰 흰 원안의 범위에 들도록 옮겼고, d에서 더한게 아니라 빼주었다.

만약 위에서 2nd라인의 더한 의미가 와닿았다면, 이 의미도 와닿을거라 생각한다.

다시 검은 원의 중심인 -0.1,0.1 fragment의 입장에서 생각해보면, 1st라인을 통과했을 때 이 프래그먼트는 1이었다.

왜냐면 첫번째 원 반경안에 들었으니까.

 

그런데 두번째 라인을 통과하면서 두번째 원 반경 안에도 들게 되어서 circle 함수 내에서 1을 리턴했을것이다.

즉, 1st -> d = 1, 2nd = d-1 이 되어서 0이 된것이다. 그래서 (-0.2,0.2) fragment는 0으로 처리된다.

 

너무 직관적이라서 사실 좀 와닿기 힘든 개념이라고 생각한다..

무튼 이런식으로 유튜브 샘플대로 눈 비슷한걸 두개 그려넣어준다.

    float d = circle(uv, vec2(0,0), 0.3, 0.05); /// 1st
	d -= circle(uv, vec2(-.1,.1), 0.05, 0.01);	/// 2nd
	d -= circle(uv, vec2(.1,.1), 0.05, 0.01);	/// 3rd

 

비슷한 원리로, 원 두개를 겹쳐서 입 모양을 만들어본다.

 

    float d = circle(uv, vec2(0,0), 0.3, 0.05); /// 1st
	d -= circle(uv, vec2(-.1,.1), 0.05, 0.01);	/// 2nd
	d -= circle(uv, vec2(.1,.1), 0.05, 0.01);	/// 3rd
    
    float mouth = circle(uv, vec2(0,0), 0.2, 0.01); //4th
    mouth -= circle(uv, vec2(0,0.05), 0.2, 0.01); //5th
    
    d -= mouth;

 

첫번째 눈을 그렸던 원리랑 정확히 같다고 보면 된다. 우리는 입모양 주변의 fragemnt의 RGB채널에 0 을 채워넣는게 목적이다. 

    float mouth = circle(uv, vec2(0,0), 0.2, 0.01); //4th
    mouth -= circle(uv, vec2(0,0.05), 0.2, 0.01); //5th
    
    // Output to screen
    fragColor = vec4(vec3(mouth),1.0);

 

일단 mouth에 대해서만 설명하자면, 원점에 반지름이 조금 더 작은 원을 그려넣고,

그 원에서 y축으로 조금 평행이동 한 똑같은 크기의 원만큼 빼준것이다. 위처럼 초승달 모양이 나오고,

이 초승달을 이루고 있는 fragment의 컬러값은 1이라는걸 기억해두자.

 

 

    float d = circle(uv, vec2(0,0), 0.3, 0.05); /// 1st
	d -= circle(uv, vec2(-.1,.1), 0.05, 0.01);	/// 2nd
	d -= circle(uv, vec2(.1,.1), 0.05, 0.01);	/// 3rd
    
    float mouth = circle(uv, vec2(0,0), 0.2, 0.01); //4th
    mouth -= circle(uv, vec2(0,0.05), 0.2, 0.01); //5th
    
    d -= mouth;

즉, 위의 코드로 다시 돌아와서, d에서는 눈부분을 제외한 원 안의 모든 프래그먼트 RGB가 vec3(1)인데,

여기서 mouth 부분의 fragment로 리턴된 입모양 주변 fragment역시 vec3(1)을 리턴하므로

입모양부분은 다시 검은색이 되는것이다.

 

 

이런 원리로 아래와 같은 Smile을 그릴 수 있다!

얼굴형을 이루고 있는 흰색 fragment값에다가 원하는 컬러값의 벡터를 곱하면 원하는 색으로 바꿀 수 있다.

 

 

 

float circle(vec2 uv, vec2 offset,float r, float edgeWidth)
{
    return smoothstep(r,r-edgeWidth,length(uv-offset));
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates (from 0 to 1)

    vec2 uv = fragCoord/iResolution.xy;
	uv.xy -= 0.5;
    uv.x *= iResolution.x / iResolution.y;

    float d = circle(uv, vec2(0,0), 0.3, 0.05); /// 1st
	d -= circle(uv, vec2(-.1,.1), 0.05, 0.01);	/// 2nd
	d -= circle(uv, vec2(.1,.1), 0.05, 0.01);	/// 3rd
    
    float mouth = circle(uv, vec2(0,0), 0.2, 0.01); //4th
    mouth -= circle(uv, vec2(0,0.05), 0.2, 0.01); //5th
    
    d -= mouth;
    
    vec3 col = vec3(1.0,1.0,0)*d;
    
    // Output to screen
    fragColor = vec4(col,1.0);
}

 

기존에는 d값을 RGB채널에 다 넣어서 0아니면 1의 흑백이었지만, 노란색 벡터를 곱해서 노란색과 검은색으로 표현할 수 있다.

 

언뜻 보면 간단하지만, 쉐이더를 처음 접한다면 이해하는데 꽤 오래 걸릴수도 있겠다.

 

'Graphics > ShaderToy' 카테고리의 다른 글

#2 ShaderToy - 원 그리기  (0) 2019.09.16
#1 ShaderToy - 기본 예제  (0) 2019.09.16