본문 바로가기
Graphics/GLSL

06. Shapes

by Lein_ 2022. 1. 2.

Shapes

 

지금까지 GLSL 기초와 타입, 함수들을 배웠다. 이번에는 간단한 도형을 병렬 절차로 그리는 방법을 알아보자.

 

 

Rectangle

격자무늬 종이를 가지고 정사각형을 그린다고 생각해보자. 종이의 크기는 10x10이고 정사각형은 8x8로 그린다고 할 때 어떻게 그릴 것인가?

 

일반적으로 첫 번째 행과 마지막 행, 첫 번째 열과 마지막 열을 제외한 모든 것을 칠할 것이다.

 

이게 셰이더랑 무슨 상관이 있는가? 격자무늬 종이의 각 작은 사각형은 스레드(픽셀)이다. 각각의 작은 정사각형은 체스판의 좌표처럼 그 위치를 알고 있다. 이전 챕터에서는 x와 y를 각각 R, G채널로 매핑하고 0.0과 1.0 사이의 좁은 2차원 영역을 사용하는 방법을 배웠다. 이걸 어떻게 이용해서 화면 중앙에 정사각형을 그릴 수 있을까?

 

공간 필드(spatial field)에 if문을 사용하는 pseudocode를 스케치해보자. 이를 위한 원칙은 격자무늬 종이 시나리오와 유사하다.

 

if ( (X GREATER THAN 1) AND (Y GREATER THAN 1) )
    paint white
else
    paint black

 

이제 if문을 step()으로 대체하고, 10x10을 사용하는 대신 0.0에서 1.0 사이의 정규화된 값을 사용한다.

 

uniform vec2 u_resolution;

void main(){
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    vec3 color = vec3(0.0);

    // Each result will return 1.0 (white) or 0.0 (black).
    float left = step(0.1,st.x);   // Similar to ( X greater than 0.1 )
    float bottom = step(0.1,st.y); // Similar to ( Y greater than 0.1 )

    // The multiplication of left*bottom will be similar to the logical AND.
    color = vec3( left * bottom );

    gl_FragColor = vec4(color,1.0);
}

 

step() 함수는 0.1 미만의 모든 픽셀을 검은색(vec3(0.0))으로 바꾸고 나머지는 흰색(vec3(1.0))으로 바꾼다. left와 bottom의 곱은 논리적 AND 연산으로 작동하며, 1.0을 반환하려면 둘 다 1.0이어야 한다. 이 연산에서는 캔버스의 왼쪽과 아래에 각각 검은색 선이 그려진다.

 

 

위 코드에서는 각 축(left와 bottom)에 대해 구조를 반복한다. 하나의 값이 아닌 step()에 직접 두 개의 값을 전달하면 아래 코드처럼 줄일 수 있다.

 

vec2 borders = step(vec2(0.1),st);
float pct = borders.x * borders.y;

 

지금까지 직사각형에서 두 개의 테두리(bottom-left)만 그렸다. 나머지 두 개(top-right)도 그리면 다음 코드와 같다.

 

// Author @patriciogv - 2015
// http://patriciogonzalezvivo.com

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;

void main(){
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    vec3 color = vec3(0.0);

    // bottom-left
    vec2 bl = step(vec2(0.1),st);
    float pct = bl.x * bl.y;

    // top-right
    vec2 tr = step(vec2(0.1),1.0-st);
    pct *= tr.x * tr.y;

    color = vec3(pct);

    gl_FragColor = vec4(color,1.0);
}

21~22번째 줄에서 st 좌표를 반전하고 step() 함수를 사용하는 것에 주목하자. 이렇게 하면 vec2(0.0, 0.0)가 오른쪽 상단에 있다.

 

위 코드에서 pct를 다음과 같이 줄일 수 있다.

 

vec2 bl = step(vec2(0.1),st);       // bottom-left
vec2 tr = step(vec2(0.1),1.0-st);   // top-right
color = vec3(bl.x * bl.y * tr.x * tr.y);

 

이 기술은 step()과 곱을 이용해 논리 연산을 수행하고 좌표를 뒤집는 것이다.

 

연습 문제

 

1. 직사각형의 크기와 비율을 변경해보자.

 

2. step() 대신 smoothstep()을 사용해보자. 흐릿한 모서리를 우아하고 부드러운 테두리로 전환할 수 있다.

 

3. floor()를 사용해 다른 방식으로 구현해보자.

 

4. 가장 마음에 드는 구현을 정하고 나중에 재사용할 수 있도록 함수화해보자.

 

5. 직사각형의 외곽선(outline)만 그리는 함수를 만들어 보자.

 

6. 스크린에 또다른 직사각형을 배치할 수 있는가? 만약 가능하다면 몬드리안 그림과 같은 직사각형과 색상의 구성을 만들어 보자.

 

Piet Mondrian - Tableau (1921)


Circles

 

격자 종이에 정사각형을 그리고 데카르트 좌표에 직사각형을 그리는 것은 쉽지만, 원은 "픽셀 단위" 알고리즘이 필요하기 때문에 다른 접근법이 필요하다. 한 가지 방법은 step() 함수를 사용하여 원을 그릴 수 있도록 공간 좌표를 다시 매핑하는 것이다.

 

원 반지름까지 컴퍼스를 벌리고 원의 중심을 정해서 컴퍼스를 고정하고 회전해 원의 가장자리를 추적해 보자.

 

 

격자무늬 종이의 각 정사각형이 픽셀인 셰이더로 변환하는 것은 각 픽셀이 원의 영역 안에 있는지를 판단하는 것이다. 우리는 픽셀에서 원의 중심까지의 거리를 계산하여 이를 수행할 것이다.

 

 

그 거리를 계산하는 방법은 여러 가지가 있다. 가장 쉬운 방법은 두 점 사이의 차의 length()를 내부적으로 계산하는 distance() 함수를 사용한다. length() 함수는 내부적으로 제곱근(sqrt())을 사용하는 빗변방정식(hypotenuse equation)과 다를게 없으며 2차원 기준 중심이 항상 (0, 0)이라고 가정한다.

 

distance(), length(), sqrt()를 사용하여 화면 중심까지의 거리를 계산할 수 있다. 아래 코드는 세 함수가 동일한 결과를 반환한다는 것을 보여준다. 각 주석을 풀면서 결과를 확인해보자.

 

// Author @patriciogv - 2015
// http://patriciogonzalezvivo.com

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;

void main(){
	vec2 st = gl_FragCoord.xy/u_resolution;
    float pct = 0.0;

    // a. The DISTANCE from the pixel to the center
    pct = distance(st,vec2(0.5));

    // b. The LENGTH of the vector
    //    from the pixel to the center
    // vec2 toCenter = vec2(0.5)-st;
    // pct = length(toCenter);

    // c. The SQUARE ROOT of the vector
    //    from the pixel to the center
    // vec2 tC = vec2(0.5)-st;
    // pct = sqrt(tC.x*tC.x+tC.y*tC.y);

    vec3 color = vec3(pct);

	gl_FragColor = vec4( color, 1.0 );
}

앞의 예제에서는 화면 중앙까지의 거리를 픽셀의 색상에 매핑하고 있다. 픽셀이 중앙에 가까울수록 더 낮은(어두운) 값을 가진다. 중심으로부터의 최대 거리(vec2(0.5, 0.5))는 0.5를 0.5를 간신히 초과하기 때문에 전체적으로 어둡다.


Distance field

 

또한 위의 예는 어두운 것이 더 크다는 것을 의미하는 고도(altitude) 지도라고 할 수 있다. 그라디언트(gradient)는 원뿔에 의해 만들어진 패턴과 비슷하단 것을 보여준다. 원뿔 꼭대기에 있다고 상상해보자. 원뿔의 가장자리까지의 수평거리는 0.5이다. 이것은 모든 방향에서 일정할 것이다. 원뿔을 "절단"할 위치를 선택함으로써 더 크거나 더 작은 원형 표면을 얻을 수 있다.

기본적으로 우리는 중심까지의 거리를 바탕으로 공간을 재해석하여 형태를 만들고 있다. 이 기술은 "distance field"로 알려져 있으며 폰트 윤곽에서부터 3D 그래픽에 이르기까지 다양한 방식으로 사용된다.

 

연습 문제

 

1. step()을 사용하여 0.5 이상의 모든 값을 흰색으로, 그 아래의 모든 값은 0.0으로 변경한다.

 

2. 배경(background)과 전경(foreground)의 색을 반대로 해보자.

 

3. smoothstep()을 사용해 여러 값을 실험하여 원의 테두리를 부드럽게 만들어 보자.

 

4. 구현이 만족스러우면 나중에 재사용할 수 있도록 함수화 시켜두자.

 

5. 원에 색상을 추가해 보자.

 

6. 뛰고 있는 심장처럼 원이 확대되었다가 축소되는 애니메이션을 만들어 보자.

 

7. 화면에 또다른 원을 그려보자.

 

8. 다른 함수들과 연산을 사용해 distance field를 결합(combine)하면 어떻게 되는가?

 

pct = distance(st,vec2(0.4)) + distance(st,vec2(0.6));
pct = distance(st,vec2(0.4)) * distance(st,vec2(0.6));
pct = min(distance(st,vec2(0.4)),distance(st,vec2(0.6)));
pct = max(distance(st,vec2(0.4)),distance(st,vec2(0.6)));
pct = pow(distance(st,vec2(0.4)),distance(st,vec2(0.6)));

 

퍼포먼스 측면에서 sqrt() 함수와 이에 의존하는 모든 함수들은 무겁다. 다음은 dot() product를 사용해 원형 distance field를 생성하는 또다른 방법이다.

 

// Author @patriciogv - 2015
// http://patriciogonzalezvivo.com

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;

float circle(in vec2 _st, in float _radius){
    vec2 dist = _st-vec2(0.5);
	return 1.-smoothstep(_radius-(_radius*0.01),
                         _radius+(_radius*0.01),
                         dot(dist,dist)*4.0);
}

void main(){
	vec2 st = gl_FragCoord.xy/u_resolution.xy;

	vec3 color = vec3(circle(st,0.9));

	gl_FragColor = vec4( color, 1.0 );
}

 


Useful properties of a Distance Field

Zen garden

Distance field는 거의 모든 것을 그리는 데 사용할 수 있다. 모양이 복잡할수록 방정식도 복잡해질 수 있지만, 특정 모양의 distance field를 만드는 공식을 갖게 되면 부드러운 모서리 및 다중 윤곽처럼 결합해 효과를 적용하기 매우 쉽다.

 

다음 코드를 살펴보자.

 

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;

void main(){
  vec2 st = gl_FragCoord.xy/u_resolution.xy;
  st.x *= u_resolution.x/u_resolution.y;
  vec3 color = vec3(0.0);
  float d = 0.0;

  // Remap the space to -1. to 1.
  st = st *2.-1.;

  // Make the distance field
  d = length( abs(st)-.3 );
  // d = length( min(abs(st)-.3,0.) );
  // d = length( max(abs(st)-.3,0.) );

  // Visualize the distance field
  gl_FragColor = vec4(vec3(fract(d*10.0)),1.0);

  // Drawing with the distance field
  // gl_FragColor = vec4(vec3( step(.3,d) ),1.0);
  // gl_FragColor = vec4(vec3( step(.3,d) * step(d,.4)),1.0);
  // gl_FragColor = vec4(vec3( smoothstep(.3,.4,d)* smoothstep(.6,.5,d)) ,1.0);
}

우리는 좌표계를 중심으로 옮긴 후 -1과 1 사이의 위치값을 다시 매핑하기 위해 반으로 축소하는 것부터 시작한다. 또한 24행에서는 fract() 함수를 사용하여 distance field 값을 시각화하여 패턴을 쉽게 확인할 수 있다. distance field 패턴이 Zen garden의 고리처럼 반복된다.

 

19행의 distance field formula를 보자. 여기서는 4개의 사분면(abs()의 역할)에서 (.3, .3)또는 vec3(.3)의 위치까지의 거리를 계싼하고 있다.

 

20행의 주석을 풀면 min() 함수에서 0과 비교하여 4개의 점까지의 거리를 합친다는 것을 알 수 있다. 그 결과는 흥미로운 새로운 패턴을 만들어낸다.

 

21행의 주석을 풀어보자. 여기서는 max() 함수를 사용한다. 결과는 모서리가 둥근 지갓각형이 된다. distance field의 링은 중심에서 멀어질수록 부드러워진다.

 

distance field 패턴의 다양한 사례를 이해하기 위해 27~29행의 주석을 하나씩 풀어보자.

 


Polar shapes

Robert Mangold - Untitled (2008)

Color 챕터에서 우리는 데카르트 좌표를 극좌표에 매핑해보았다.

 

vec2 pos = vec2(0.5)-st;
float r = length(pos)*2.0;
float a = atan(pos.y,pos.x);

 

이 장의 시작 부분에 있는 이 공식의 일부를 사용하여 원을 그린다. length()를 이용해 중심까지의 거리를 계산했다. distance field가 무엇인지 알게 되었으므로 극좌표를 사용해 그리는 다른 방법을 배울 수 있다.

 

이 테크닉은 조금 제한적이지만 매우 간단하다. 원의 반지름을 각도에 따라 변경하여 다른 모양을 만드는 것이다. 변조(modulation)는 shaping function을 이용하면 된다.

 

아래에는 데카르트 그래프와 극좌표 셰이더 예제(21~25행)에서 동일한 함수를 볼 수 있다. 좌표계와 다른 좌표계 사이의 관계를 주목하면서 한 줄씩 주석을 풀어보자.

 

y = cos(x*3.);
//y = abs(cos(x*3.));
//y = abs(cos(x*2.5))*0.5+0.3;
//y = abs(cos(x*12.)*sin(x*3.))*.8+.1;
//y = smoothstep(-.5,1., cos(x*10.))*0.2+0.5;
// Author @patriciogv - 2015
// http://patriciogonzalezvivo.com

#ifdef GL_ES
precision mediump float;
#endif

uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;

void main(){
    vec2 st = gl_FragCoord.xy/u_resolution.xy;
    vec3 color = vec3(0.0);

    vec2 pos = vec2(0.5)-st;

    float r = length(pos)*2.0;
    float a = atan(pos.y,pos.x);

    float f = cos(a*3.);
    // f = abs(cos(a*3.));
    // f = abs(cos(a*2.5))*.5+.3;
    // f = abs(cos(a*12.)*sin(a*3.))*.8+.1;
    // f = smoothstep(-.5,1., cos(a*10.))*0.2+0.5;

    color = vec3( 1.-smoothstep(f,f+0.02,r) );

    gl_FragColor = vec4(color, 1.0);
}

연습 문제

 

1. 애니메이션을 적용해보자.

 

2. 다양한 shaping function을 결합해 모양에 구멍을 내 꽃, 눈송이, 기어 등을 만들어보자.

 

3. shaping function 챕터에서 사용하던 plot() 함수를 사용해 등고선(contour)만 그려보자.

 


Combining powers

 

지금까지 atan()을 이용해 각도에 따라 원의 반지름을 변조(modulation)하는 방법을 배웠으므로 distance field와 atan()을 사용해 모든 트릭과 효과를 적용할 수 있다.

 

트릭은 폴리곤의 모서리 수를 이용해 극좌표를 사용하여 distance field를 생성한다. Andrew Baldwin의 다음 코드를 확인해보자.

 

#ifdef GL_ES
precision mediump float;
#endif

#define PI 3.14159265359
#define TWO_PI 6.28318530718

uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;

// Reference to
// http://thndl.com/square-shaped-shaders.html

void main(){
  vec2 st = gl_FragCoord.xy/u_resolution.xy;
  st.x *= u_resolution.x/u_resolution.y;
  vec3 color = vec3(0.0);
  float d = 0.0;

  // Remap the space to -1. to 1.
  st = st *2.-1.;

  // Number of sides of your shape
  int N = 3;

  // Angle and radius from the current pixel
  float a = atan(st.x,st.y)+PI;
  float r = TWO_PI/float(N);

  // Shaping function that modulate the distance
  d = cos(floor(.5+a/r)*r-a)*length(st);

  color = vec3(1.0-smoothstep(.4,.41,d));
  // color = vec3(d);

  gl_FragColor = vec4(color,1.0);
}

1. 이 예제를 이용해 원하는 모양의 위치와 모서리 수를 입력하고 distance field 값을 반환하는 함수를 만들어 보자.

 

2. min() 및 max()를 사용하여 distance field를 혼합한다.

 

3. 기하학적인 로고를 정하고 distance field를 사용하여 클론해보자.

 

이로써 모양을 그리는 방법을 모두 알아보았다. 셰이더에서는 도형을 그리는 것조차 쉽지 않으므로 꾸준한 연습을 통해 적응할 필요가 있다.

 

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

05. Colors  (0) 2021.12.31
04. Shaping functions  (0) 2021.12.30
03. Uniforms, gl_FragCoord  (0) 2021.12.30
02. Hello world!  (0) 2021.12.29
01. Shader란 무엇인가?  (0) 2021.12.29

댓글