셰이더 드로잉: 기초(하)

이전 글에서 이어집니다.

격자

HTML Canvas 2D 등의 그래픽 API에서는 주로 반복문을 이용해 격자를 그린다.

p.setup = () => {
  p.createCanvas(window.innerWidth, window.innerHeight);

  const span = 40;
  for (let j = 0; j < p.height / span; j++) {
    for (let i = 0; i < p.width / span; i++) {
      p.rect(i * span, j * span, span, span);
    }
  }
};

이는 매우 직관적이지만, 도형을 반복해 그릴수록 성능 문제를 일으킬 수 있다. 그러나 프래그먼트 셰이더는 그러한 한계에서 벗어날 수 있게 해 준다. 결국 모든 픽셀에서 한 번씩 실행되기에 도형을 얼마나 반복하든 연산 횟수 자체는 동일하기 때문이다. 이를 이용해 패턴을 그리는 방법을 알아보자.

이전 글에서 난수를 만들 때 fract 함수를 이용했다. fract는 입력받는 값에서 정수 부분을 버리고 소수점 아래 부분만 반환한다. 이를테면 fract(2.3534)의 반환 값은 0.3534다. 그렇다면 fract의 인자로 uv를 넣으면 어떻게 되는가. uv는 픽셀의 위치를 화면 해상도로 나눠 정규화한 값이다. 기본적으로 0부터 1 사이 값이기에 fract(uv)는 무의미하다. 하지만 uv를 스케일링한다면 의미가 생겨난다. 예를 들어 fract(uv*3.0)은 0부터 1 사이로 3번 반복되는 uv좌표를 반환하는 것이다. 해당 좌표계를 이용하면 연산 횟수를 고정하며 형태를 반복할 수 있게 된다.

이를 이용해 격자를 그리는 코드다. 한 가지 문제가 있다면 선의 굵기는 격자 내에서 고정된 값이라 uv의 스케일에 따라 얇아 보이기도, 굵어 보이기도 한다는 것이다. 위 코드에서는 0.01의 값인데, uv가 커질 때 격자 내 공간 속 0.01의 값은 같은 비율로 커지지 않기에 발생하는 현상이다. 이를 해결하는 방법은 여러가지가 있다.

당연하게도 가장 간단한 방법은 uv의 스케일 비율만큼 선의 굵기를 키우는 것이다. 그렇게 하면 개념적으로는 동일한 굵기로 그려질 것이다. 그러나 위 결과에서 드러나듯 항상 일정한 굵기로 보이지는 않는다. 이는 앨리어싱(Aliasing)의 문제다. 결국 모든 형태는 픽셀이라는 격자로 표현되어야 하기에, 선이 그려지는 위치에 따라 1픽셀씩 더 혹은 덜 포함되어 그려질 수 있기 때문이다.

이를 해결하기 위해서는 보다 난해한 방법이 필요하다. 화면의 기울기를 구하는 것이다. 여기서 기울기라는 건 x가 1 이동했을 때의 y값의 변화량이다. GLSL에는 dFdxdFdy라는 함수가 존재한다. dFdx(v)는 화면 좌표 x값이 커지는 방향으로, 즉 왼쪽에서 오른쪽으로 한 칸 이동될 때의 v의 변화량(기울기)을 반환한다. x좌표가 반전되어있어도 화면 왼쪽 끝에서 오른쪽 끝 방향이며, 반환되는 기울기 값의 부호가 반전될 뿐이다. dFdy(v)는 화면 좌표 y값이 커지는 방향, 즉 아래에서 위로 약 한 픽셀 이동할 때의 v의 변화량을 반환한다.

fwidth라는 함수도 존재한다. fwidth(v)는 화면의 x방향과 y방향으로 한 픽셀 이동했을 때의 v의 변화 크기를 반환한다. 즉 abs(dFdx(v)) + abs(dFdy(v))와 동일한 셈이다. 이를 smoothstep의 인자로 사용하면 값의 변화가 급격할수록, 즉 형태의 경계가 분명한 만큼 현재 값의 변화율에 맞춰 보간 폭을 자동으로 정해 픽셀 단위로 칼같이 색이 잘리는 현상을 방지한다. uv가 커진 만큼 굵기를 키운다면, fwidth로 형태 경계를 보간해 주는 것이다. 이를 안티앨리어싱(Antialiasing)이라고 부른다.

fwidth함수는 흥미롭다. 해당 함수에 화면 좌표계를 입력하면 화면에서 약 1픽셀 이동했을 때 좌표계가 변하는 정도를 구할 수 있는데, 이는 사실상 입력된 좌표계에서 한 픽셀의 폭으로 볼 수 있다. uv를 스케일해도 좌표계의 한 픽셀 비율과 다름없기에 이는 유효하다. 이를 이용하면 좌표계에 따라 정규화된 거리값이 실제로 몇 픽셀 정도를 차지하는지 구할 수 있다. 예를 들어 축별 거리값이라면 d / fwidth(grid)처럼 나누어 픽셀 단위 거리로 바꿀 수 있고, 스칼라 거리장이라면 dst / fwidth(dst)처럼 사용할 수 있는 것이다. 이를 통해 선의 굵기를 픽셀 단위로 고정하며 굵기가 안정적인 격자를 그릴 수도 있다.

#extension GL_OES_standard_derivatives : enable

참고로 dFdx, dFdy, fwidth 등의 함수는 WebGL 1.0 환경에서 기본적으로 내장되어있지 않다. 이를 사용하기 위해서는 OES_standard_derivatives 확장을 켜 줘야 한다. WebGL 2.0 이상의 환경에서는 기본적으로 지원한다.

fract 함수를 사용하지 않아도 격자를 그릴 수 있다. mod 함수는 두 가지 인자를 받아 첫 번째 인자를 두 번째 인자로 나눈 나머지값을 반환한다. 예를 들어 mod(3.345, 2.0)1.345를 반환한다. fract 함수는 사실상 mod(v, 1.0)인 셈이다.

vec2 cell = vec2(20.0);
vec2 iuv = vec2(uv * cell); //id
vec2 fuv = vec2(uv * cell); //격자 공간

vec3 color = vec3(fuv, 0.5);
color *= mod(iuv.x - iuv.y, 2.0); //체커패턴

격자를 그린다고 하지만, 결국 핵심은 그림이 그려지는 공간을 반복하는 것이다. 반복되는 격자 공간의 각 셀(Cell)에 차이를 주기 위해선 현재 픽셀이 어느 셀 공간에 속해 있는지 알아야 한다. 이 때 floor 함수를 사용한다. floor(v)는 fract와 반대로 입력된 값의 소수점 아래 부분을 버리고 정수 부분만 반환한다. 격자를 그리기 위해 공간을 4배 키웠다고 하자. floor 함수를 이용해 정수 부분만 남긴다면 (2.535, 3.184)의 값을 가진 픽셀은 (2.0, 3.0)을 반환한다. 일종의 셀 ID를 반환하는 것이다. 그렇다면 공간은 사실상 소수점으로 쪼개진 부분공간과 정수로 이루어진 셀 위치의 합, 즉 fract(uv) + floor(uv)가 되는 셈이다. 이를 이용해 각 공간에 다른 그림을 그릴 수 있다.

각 셀마다 다른 형태를 그리는 코드

앞서 살펴본 난수 생성 함수의 인자로 셀의 위치 정보를 입력하면 각 셀 위치에 해당하는 난수를 반환한다. 각 셀마다 다른 난수를 가지게 될 테고, 이를 그림을 그리는 데 사용한다면 부분공간별로 다른 그림을 그릴 수 있게 된다. 어느 셀인지, 그 셀 안에서 어느 위치인지 나눠 그림을 그리는 것이다.

반복문으로 화면을 400×400 격자로 분할하고 각 공간에 그림을 그리려면 160000번 이상의 연산이 필요하다. CPU는 순차적으로 연산을 처리하는 반면, 프래그먼트 셰이더는 아주 작은 연산을 각 픽셀마다 독립적으로 실행한다. 즉 모든 셀을 순회하는 대신, 픽셀 단위의 계산을 병렬적으로 수행하기에 많은 형태를 그려야 하는 등의 몇몇 상황에서 매우 강력하다.

노이즈

이전까지 살펴본 랜덤 함수는 마치 TV의 백색 잡음처럼 종잡을 수 없는 값의 반환을 목표로 했다. 하지만 구름, 파도, 언덕 등 자연의 무작위함은 이와 사뭇 다르다. 컴퓨터 과학자 Ken Perlin은 1980년대 영화 「트론(Tron)」에서 사용될 텍스처의 제작을 위해 “자연스러운” 난수 생성 함수를 개발하게 된다. 그것이 펄린 노이즈(Perlin Noise)다.

노이즈를 알아보기 전에 mix 함수를 살펴봐야 한다. mix(v1, v2, t)v1v2t만큼 보간한 값을 반환한다. 예를 들어 v11.0, v20.0이고 t0.3이라면 0.7을 반환한다. 이는 사실상 아래와 같다.

$$v = v1 + (v2 - v1) \times t$$

색상(3채널 값)의 혼합
uv.x의 sin 값을 t로 사용

『The Book of Shaders』에서는 펄린 노이즈에 앞서 다른 방식으로 생성하는 노이즈를 먼저 살펴본다. 이를 값 노이즈(Value Noise)라고 칭하는데, 개념적으로 펄린 노이즈에 선행하기에 단계적 학습을 위해 먼저 살펴보는 듯하다.

prev next
I'm looking for an English tutor (⸝⸝◜▿◝⸝⸝ゝ)