목차

WebGLFundamentals.org

Fix, Fork, Contribute

WebGL 3D 원근 교정 텍스처 매핑

이 포스트는 WebGL 관련 시리즈에서 이어집니다. 첫 번째는 기초로 시작했습니다. 이 글은 원근 교정 텍스처 매핑을 다룹니다. 이걸 이해하기 위해서는 원근 투영텍스처에 대해 읽어야 할 겁니다. 또한 베링과 그 기능에 대해 알아야 하지만 여기서 간략하게 설명하겠습니다.

"동작 원리"에서 베링이 어떻게 작동하는지 다뤘는데요. 정점 셰이더는 베링을 선언하고 어떤 값으로 설정할 수 있습니다. 정점 셰이더가 3번 호출되면 WebGL은 삼각형을 그립니다. 해당 삼각형을 그리는 동안 모든 픽셀에 대해 프래그먼트 셰이더를 호출하고 해당 픽셀을 어떤 색상으로 만들지 묻습니다. 삼각형의 정점 3개 사이에서 3개의 값 사이를 보간한 베링을 전달할 겁니다.

v_color는 v0, v1, v2 사이에서 보간

첫 번째 글로 돌아가보면 우리는 클립 공간에서 삼각형을 그렸는데요. 다음과 같이 간단한 정점 셰이더에 클립 공간 좌표를 전달했습니다.

  // 속성은 버퍼에서 데이터를 받음
  attribute vec4 a_position;

  // 모든 셰이더는 main 함수를 가짐
  void main() {

    // gl_Position은 정점 셰이더가 설정을 담당하는 특수 변수
    gl_Position = a_position;
  }

일정한 색상으로 그리는 간단한 프래그먼트 셰이더가 있습니다.

  // 프래그먼트 셰이더는 기본 정밀도를 가지고 있지 않으므로 하나를 선택해야 합니다.
  // mediump은 좋은 기본값으로 "중간 정밀도"를 의미합니다.
  precision mediump float;

  void main() {
    // gl_FragColor는 프래그먼트 셰이더가 설정을 담당하는 특수 변수
    gl_FragColor = vec4(1, 0, 0.5, 1); // 자주색 반환
  }

클립 공간에 2개의 사각형을 그리도록 만들어봅시다. 각 정점의 X, Y, Z, W인 데이터를 전달할 겁니다.

var positions = [
  -.8, -.8, 0, 1,  // 1번째 사각형의 1번째 삼각형
   .8, -.8, 0, 1,
  -.8, -.2, 0, 1,
  -.8, -.2, 0, 1,  // 1번째 사각형의 2번째 삼각형
   .8, -.8, 0, 1,
   .8, -.2, 0, 1,

  -.8,  .2, 0, 1,  // 2번째 사각형의 1번째 삼각형
   .8,  .2, 0, 1,
  -.8,  .8, 0, 1,
  -.8,  .8, 0, 1,  // 2번째 사각형의 2번째 삼각형
   .8,  .2, 0, 1,
   .8,  .8, 0, 1,
];

여기 결과입니다.

베링 플로트 하나를 추가해봅시다. 해당 베링을 정점 셰이더에서 프래그먼트 셰이더로 전달할 겁니다.

  attribute vec4 a_position;
+  attribute float a_brightness;

+  varying float v_brightness;

  void main() {
    gl_Position = a_position;

+    // 프래그먼트 셰이더로 밝기 전달
+    v_brightness = a_brightness;
  }

프래그먼트 셰이더에서는 해당 베링을 사용하여 색상을 설정할 겁니다.

  precision mediump float;

+  // 정점 셰이더에서 전달받아 보간
+  varying float v_brightness;  

  void main() {
*    gl_FragColor = vec4(v_brightness, 0, 0, 1);  // 빨강
  }

베링의 데이터를 제공해야 하므로 버퍼를 만들어 데이터를 넣을 겁니다. 정점 당 하나의 값을 가집니다. 왼쪽을 0으로 오른쪽은 1로 정점에 대한 밝기 값을 설정합니다.

  // 버퍼를 생성하고 12개의 밝기 값 넣기
  var brightnessBuffer = gl.createBuffer();

  // ARRAY_BUFFER에 바인딩 (ARRAY_BUFFER = brightnessBuffer)
  gl.bindBuffer(gl.ARRAY_BUFFER, brightnessBuffer);

  var brightness = [
    0,  // 1번째 사각형의 1번째 삼각형
    1, 
    0, 
    0,  // 1번째 사각형의 2번째 삼각형
    1, 
    1, 

    0,  // 2번째 사각형의 1번째 삼각형
    1, 
    0, 
    0,  // 2번째 사각형의 2번째 삼각형
    1, 
    1, 
  ];

  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(brightness), gl.STATIC_DRAW);

또한 초기화할 때 a_brightness 속성의 위치를 찾아야 합니다.

  // 정점 데이터가 어디로 가야하는지 탐색
  var positionAttributeLocation = gl.getAttribLocation(program, "a_position");
+  var brightnessAttributeLocation = gl.getAttribLocation(program, "a_brightness");

그리고 렌더링할 때 해당 속성을 설정합니다.

  // 속성 활성화
  gl.enableVertexAttribArray(brightnessAttributeLocation);

  // 위치 버퍼 바인딩
  gl.bindBuffer(gl.ARRAY_BUFFER, brightnessBuffer);

  // brightnessBuffer의 데이터를 가져오는 방법을 속성에 지시 (ARRAY_BUFFER)
  var size = 1;          // 반복마다 1개의 컴포넌트
  var type = gl.FLOAT;   // 데이터는 32비트 부동 소수점
  var normalize = false; // 데이터 정규화 안 함
  var stride = 0;        // 0 = 다음 위치를 가져오기 위해 반복마다 size * sizeof(type) 만큼 앞으로 이동
  var offset = 0;        // 버퍼의 처음부터 시작
  gl.vertexAttribPointer(
    brightnessAttributeLocation,
    size,
    type,
    normalize,
    stride,
    offset
  );

그리고 이제 렌더링할 때 brightness가 0인 왼쪽은 검은색이고 brightness가 1인 오른쪽은 빨간색인 사각형 2개를 얻으며, brightness 사이의 영역은 삼각형을 가로질러 보간됩니다.

원근에 대한 글에서 WebGL은 우리가 입력한 gl_Position을 가져와 gl_Position.w로 나눕니다.

위의 정점들에는 W1을 제공했지만 WebGL이 W로 나누는 걸 알고 있기 때문에 다음과 같이 할 수 있으며 동일한 결과를 얻습니다.

  var mult = 20;
  var positions = [
      -.8,  .8, 0, 1,  // 1번째 사각형의 1번째 삼각형
       .8,  .8, 0, 1,
      -.8,  .2, 0, 1,
      -.8,  .2, 0, 1,  // 1번째 사각형의 2번째 삼각형
       .8,  .8, 0, 1,
       .8,  .2, 0, 1,

      -.8       , -.2       , 0,    1,  // 2번째 사각형의 1번째 삼각형
       .8 * mult, -.2 * mult, 0, mult,
      -.8       , -.8       , 0,    1,
      -.8       , -.8       , 0,    1,  // 2번째 사각형의 2번째 삼각형
       .8 * mult, -.2 * mult, 0, mult,
       .8 * mult, -.8 * mult, 0, mult,
  ];

위에서 두 번째 사각형의 오른쪽에 있는 모든 점에 대해 XYmult를 곱한 것을 알 수 있지만, Wmult로 설정하는 것도 볼 수 있습니다. WebGL이 W로 나누기 때문에 똑같은 결과를 얻을 수 있겠죠?

음 여기 결과입니다.

두 사각형은 이전과 같은 곳에 그려졌습니다. 이건 X * MULT / MULT(W)가 여전히 X이고 Y에 대해 동일하다는 걸 증명합니다. 하지만 색상이 다릅니다. 무슨 일이 일어난 걸까요?

WebGL은 W를 사용하여 원근 교정 텍스처 매핑을 구현하거나 베링의 원근 교정 보간을 수행합니다.

실제로 이걸 더 쉽게 볼 수 있도록 프래그먼트 셰이더를 해킹해봅시다.

gl_FragColor = vec4(fract(v_brightness * 10.), 0, 0, 1);  // 빨강

v_brightness를 10으로 곱하면 값을 0에서 10사이로 만듭니다. fract는 소수 부분만 유지하므로 0에서 1사이, 0에서 1사이, 0에서 1사이, 10번이 됩니다.

이제 원근을 쉽게 볼 수 있습니다.

하나의 값에서 또 다른 값으로 선형 보간하는 것은 이런 공식이 됩니다.

 result = (1 - t) * a + t * b

여기서 tab사이의 위치를 나타내는 0에서 1사이의 값입니다. a가 0이고 b가 1입니다.

베링의 경우 WebGL은 이 공식을 사용합니다.

 result = (1 - t) * a / aW + t * b / bW
          -----------------------------
             (1 - t) / aW + t / bW

여기서 aW는 베링이 a로 설정된 경우 gl_Position.w에 설정된 W이며, bW 베링이 b로 설정된 경우 gl_Position.w에 설정된 W입니다.

그게 왜 중요할까요? 음 여기 텍스처에 대한 글에서 만들었던 간단한 텍스처 큐브가 있습니다. UV 좌표를 양쪽에 0에서 1로 조정했고 4x4 픽셀 텍스처를 사용하고 있습니다.

이제 해당 예제를 가져와 정점 셰이더를 변경하여 우리가 직접 W로 나눠봅시다. 한 줄만 추가하면 됩니다.

attribute vec4 a_position;
attribute vec2 a_texcoord;

uniform mat4 u_matrix;

varying vec2 v_texcoord;

void main() {
  // 위치에 행렬 곱하기
  gl_Position = u_matrix * a_position;

+  // 수동으로 W 나누기
+  gl_Position /= gl_Position.w;

  // 프래그먼트 셰이더로 텍스처 좌표 전달
  v_texcoord = a_texcoord;
}

W로 나누는 것은 gl_Position.w가 결국 1이 됨을 의미합니다. X, Y, Z는 WebGL이 자동으로 분할했던 것처럼 나올 겁니다. 음 여기 결과입니다.

여전히 3D 큐브를 얻게 되지만 텍스처가 뒤틀리고 있습니다. 이는 이전처럼 W를 전달하지 않으면 WebGL이 원근 교정 텍스처 매핑을 할 수 없기 때문입니다. 좀 더 정확하게는 WebGL이 베링의 원근 교정 보간을 수행할 수 없습니다.

원근 행렬에서 ZW였던 걸 떠올려보면, W1인 경우 WebGL은 선형 보간을 수행합니다. 실제로 위의 방정식을 가져와 봅시다.

 result = (1 - t) * a / aW + t * b / bW
          -----------------------------
             (1 - t) / aW + t / bW

그리고 모든 W를 1로 변경합니다.

 result = (1 - t) * a / 1 + t * b / 1
          ---------------------------
             (1 - t) / 1 + t / 1

1로 나누는 것은 아무 영향이 없으므로 이렇게 단순화할 수 있습니다.

 result = (1 - t) * a + t * b
          -------------------
             (1 - t) + t

(1 - t) + t1과 같습니다. 예를 들어 t.7이면, (1 - .7) + .7이 되고, 이건 .3 + .7이며, 이는 곧 1입니다. 즉 분모를 지울 수 있기 때문에 이렇게 남게 됩니다.

 result = (1 - t) * a + t * b

이는 위의 선형 보간 방정식과 동일합니다.

이제 왜 WebGL이 4x4 행렬과 X, Y, Z, W가 있는 4개의 벡터를 사용하는지 이해가 되셨으면 좋겠습니다. XYW로 나누어 클립 공간 좌표를 얻습니다. W로 나누는 Z도 클립 공간 좌표를 얻으며, W는 베링의 보간 중에 계속 사용되고 원근 교정 텍스처 매핑 기능을 제공합니다.

1990년대 중반 게임 콘솔

플레이스테이션 1과 같은 시대의 일부 게임 콘솔들은 원근 교정 텍스처 매핑을 하지 않았습니다. 위 결과를 보면 왜 그렇게 보였는지 알 수 있습니다.

이슈/버그는? Github에 이슈를 만들어주세요.
코드 블록은 <pre><code>여기에 코드 입력</code></pre>를 사용해주세요
comments powered by Disqus