목차

WebGL2Fundamentals.org

Fix, Fork, Contribute

WebGL2 3D - 데이터 텍스처

이 글은 WebGL2 시리즈에서 이어지는 글입니다. 첫 번째 글은 WebGL2 기초였고, 이전 글은 텍스처입니다.

지난 글에서 텍스처가 작동하는 방법과 이를 적용하는 방법에 대해 살펴봤습니다. 다운로드된 이미지로 텍스처를 생성했는데요, 이번 글에서는 이미지 대신 자바스크립트에서 직접 데이터를 생성할 겁니다.

자바스크립트로 텍스처로 사용할 데이터를 만드는 것은 텍스처 포맷을 보시면 대부분 명확합니다. WebGL2는 아주 많은 텍스처 포맷을 지원합니다. WebGL2에서는 WebGL1의 모든 un-sized 포맷을 지원합니다.

FormatTypeChannelsBytes per pixel
RGBAUNSIGNED_BYTE44
RGBUNSIGNED_BYTE33
RGBAUNSIGNED_SHORT_4_4_4_442
RGBAUNSIGNED_SHORT_5_5_5_142
RGBUNSIGNED_SHORT_5_6_532
LUMINANCE_ALPHAUNSIGNED_BYTE22
LUMINANCEUNSIGNED_BYTE11
ALPHAUNSIGNED_BYTE11

un-sized라고 부르는 이유는 WebGL1에서는 이들이 내부적으로 어떻게 표현될지가 정해져 있지 않기 때문입니다. WebGL2에서는 정해져 있습니다. 이러한 un-sized포맷 이외에도 다수의 sized포맷들이 있습니다.

Sized
Format
Base
Format
R
bits
G
bits
B
bits
A
bits
Shared
bits
Color
renderable
Texture
filterable
R8 RED 8
R8_SNORM RED s8
RG8 RG 8 8
RG8_SNORM RG s8 s8
RGB8 RGB 8 8 8
RGB8_SNORM RGB s8 s8 s8
RGB565 RGB 5 6 5
RGBA4 RGBA 4 4 4 4
RGB5_A1 RGBA 5 5 5 1
RGBA8 RGBA 8 8 8 8
RGBA8_SNORM RGBA s8 s8 s8 s8
RGB10_A2 RGBA 10 10 10 2
RGB10_A2UI RGBA ui10 ui10 ui10ui2
SRGB8 RGB 8 8 8
SRGB8_ALPHA8 RGBA 8 8 8 8
R16F RED f16
RG16F RG f16 f16
RGB16F RGB f16 f16 f16
RGBA16F RGBA f16 f16 f16 f16
R32F RED f32
RG32F RG f32 f32
RGB32F RGB f32 f32 f32
RGBA32F RGBA f32 f32 f32 f32
R11F_G11F_B10F RGB f11 f11 f10
RGB9_E5 RGB 9 9 9 5
R8I RED i8
R8UI RED ui8
R16I RED i16
R16UI RED ui16
R32I RED i32
R32UI RED ui32
RG8I RG i8 i8
RG8UI RG ui8 ui8
RG16I RG i16 i16
RG16UI RG ui16 ui16
RG32I RG i32 i32
RG32UI RG ui32 ui32
RGB8I RGB i8 i8 i8
RGB8UI RGB ui8 ui8 ui8
RGB16I RGB i16 i16 i16
RGB16UI RGB ui16 ui16 ui16
RGB32I RGB i32 i32 i32
RGB32UI RGB ui32 ui32 ui32
RGBA8I RGBA i8 i8 i8 i8
RGBA8UI RGBA ui8 ui8 ui8 ui8
RGBA16I RGBA i16 i16 i16 i16
RGBA16UI RGBA ui16 ui16 ui16ui16
RGBA32I RGBA i32 i32 i32 i32
RGBA32UI RGBA ui32 ui32 ui32ui32

아래와 같은 깊이와 스텐실을 위한 포맷도 있습니다.

Sized
Format
Base
Format
Depth
bits
Stencil
bits
DEPTH_COMPONENT16 DEPTH_COMPONENT 16
DEPTH_COMPONENT24 DEPTH_COMPONENT 24
DEPTH_COMPONENT32F DEPTH_COMPONENT f32
DEPTH24_STENCIL8 DEPTH_STENCIL 24 ui8
DEPTH32F_STENCIL8 DEPTH_STENCIL f32 ui8

범례:

  • 8과 같은 숫자는 0과 1사이로 정규화되는 8비트를 의미합니다.
  • s8과 같이 숫자 앞에 s가 붙으면 부호가 있는(signed) 8비트 숫자로 -1에서 1 사이로 정규화됩니다.
  • f16과 같이 숫자 앞에 f가 붙으면 부동소수점 수를 의미합니다.
  • i8과 같이 숫자 앞에 i가 붙으면 정수형 수를 의미합니다.
  • ui8과 같이 숫자 앞에 ui가 붙으면 부호가 없는(unsigned) 정수형 수를 의미합니다.

여기서 사용할 것은 아니지만 half와 float 텍스처 포맷에 하이라이트를 해 두었습니다. WebGL1과 달리 WebGL2에서는 이러한 포맷이 항상 사용은 가능하지만 기본적으로는 color renderable이나 texture filterable이 아니라는 것을 강조하기 위함입니다. color renderable이 아니라는 뜻은 렌더링 대상이 되지 않는다는 뜻입니다. 텍스처에 렌더링하는 법은 다른 글에 설명되어 있습니다. texture filterable이 아니라는 것은 gl.NEAREST 옵션으로만 사용해야 한다는 뜻입니다. 이 기능들은 WebGL2에서는 선택적 확장 기능(optional extension)으로 제공됩니다.

이러한 포맷들에 대해서 여러분은 내부(internal) 포맷 (GPU 내부에서 사용할 포맷)과 포맷 그리고 데이터의 타입을 명시해 주어야 합니다. 아래 표는 내부 포맷에 대해 어떠한 포맷과 타입을 제공해야 하는지를 나타낸 표 입니다.

Internal
Format
Format Type Source
Bytes
Per Pixel
RGBA8
RGB5_A1
RGBA4
SRGB8_ALPHA8
RGBA UNSIGNED_BYTE 4
RGBA8_SNORM RGBA BYTE 4
RGBA4 RGBA UNSIGNED_SHORT_4_4_4_4 2
RGB5_A1 RGBA UNSIGNED_SHORT_5_5_5_1 2
RGB10_A2
RGB5_A1
RGBA UNSIGNED_INT_2_10_10_10_REV 4
RGBA16F RGBA HALF_FLOAT 8
RGBA32F
RGBA16F
RGBA FLOAT 16
RGBA8UI RGBA_INTEGER UNSIGNED_BYTE 4
RGBA8I RGBA_INTEGER BYTE 4
RGBA16UI RGBA_INTEGER UNSIGNED_SHORT 8
RGBA16I RGBA_INTEGER SHORT 8
RGBA32UI RGBA_INTEGER UNSIGNED_INT 16
RGBA32I RGBA_INTEGER INT 16
RGB10_A2UI RGBA_INTEGER UNSIGNED_INT_2_10_10_10_REV 4
RGB8
RGB565
SRGB8
RGB UNSIGNED_BYTE 3
RGB8_SNORM RGB BYTE 3
RGB565 RGB UNSIGNED_SHORT_5_6_5 2
R11F_G11F_B10F RGB UNSIGNED_INT_10F_11F_11F_REV 4
RGB9_E5 RGB UNSIGNED_INT_5_9_9_9_REV 4
RGB16F
R11F_G11F_B10F
RGB9_E5
RGB HALF_FLOAT 6
RGB32F
RGB16F
R11F_G11F_B10F
RGB9_E5
RGB FLOAT 12
RGB8UI RGB_INTEGER UNSIGNED_BYTE 3
RGB8I RGB_INTEGER BYTE 3
RGB16UI RGB_INTEGER UNSIGNED_SHORT 6
RGB16I RGB_INTEGER SHORT 6
RGB32UI RGB_INTEGER UNSIGNED_INT 12
RGB32I RGB_INTEGER INT 12
RG8 RG UNSIGNED_BYTE 2
RG8_SNORM RG BYTE 2
RG16F RG HALF_FLOAT 4
RG32F
RG16F
RG FLOAT 8
RG8UI RG_INTEGER UNSIGNED_BYTE 2
RG8I RG_INTEGER BYTE 2
RG16UI RG_INTEGER UNSIGNED_SHORT 4
RG16I RG_INTEGER SHORT 4
RG32UI RG_INTEGER UNSIGNED_INT 8
RG32I RG_INTEGER INT 8
R8 RED UNSIGNED_BYTE 1
R8_SNORM RED BYTE 1
R16F RED HALF_FLOAT 2
R32F
R16F
RED FLOAT 4
R8UI RED_INTEGER UNSIGNED_BYTE 1
R8I RED_INTEGER BYTE 1
R16UI RED_INTEGER UNSIGNED_SHORT 2
R16I RED_INTEGER SHORT 2
R32UI RED_INTEGER UNSIGNED_INT 4
R32I RED_INTEGER INT 4
DEPTH_COMPONENT16 DEPTH_COMPONENT UNSIGNED_SHORT 2
DEPTH_COMPONENT24
DEPTH_COMPONENT16
DEPTH_COMPONENT UNSIGNED_INT 4
DEPTH_COMPONENT32F DEPTH_COMPONENT FLOAT 4
DEPTH24_STENCIL8 DEPTH_STENCIL UNSIGNED_INT_24_8 4
DEPTH32F_STENCIL8 DEPTH_STENCIL FLOAT_32_UNSIGNED_INT_24_8_REV 8
RGBA RGBA UNSIGNED_BYTE 4
RGBA RGBA UNSIGNED_SHORT_4_4_4_4 2
RGBA RGBA UNSIGNED_SHORT_5_5_5_1 2
RGB RGB UNSIGNED_BYTE 3
RGB RGB UNSIGNED_SHORT_5_6_5 2
LUMINANCE_ALPHA LUMINANCE_ALPHA UNSIGNED_BYTE 2
LUMINANCE LUMINANCE UNSIGNED_BYTE 1
ALPHA ALPHA UNSIGNED_BYTE 1

3x2 픽셀 크기의 R8 텍스처를 만들어 봅시다. R8 텍스처리므로 각 픽셀마다 Red 채널에 한 개의 값만 가집니다.

지난 글에서 샘플을 가져올 겁니다. 먼저 육면체의 각 면에 전체 텍스처를 사용하기 위해 텍스처 좌표를 수정합시다.

// 육면체의 텍스처 좌표로 버퍼 채우기
function setTexcoords(gl) {
  gl.bufferData(
      gl.ARRAY_BUFFER,
      new Float32Array([
        // 앞면
        0, 0,
        0, 1,
        1, 0,
        1, 0,
        0, 1,
        1, 1,
        ...

그런 다음 텍스처 생성 코드를 수정합니다.

// 텍스처 생성
var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);

-// 1x1 파란색 픽셀로 텍스처 채우기
-gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
-              new Uint8Array([0, 0, 255, 255]));

// 3x2 픽셀로 텍스처 채우기
const level = 0;
const internalFormat = gl.R8;
const width = 3;
const height = 2;
const border = 0;
const format = gl.RED;
const type = gl.UNSIGNED_BYTE;
const data = new Uint8Array([
  128,  64, 128,
    0, 192,   0,
]);
gl.texImage2D(gl.TEXTURE_2D, level, internalFormat, width, height, border,
              format, type, data);

// 밉맵을 사용하지 않고 필터링을 수행하지 않도록 설정합니다.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

-// 비동기적으로 이미지 로드
-...

그리고 여기 결과입니다.

이런! 왜 작동하지 않는걸까요?!?!?

자바스크립트 콘솔을 확인하면 이런 오류가 표시됩니다.

WebGL: INVALID_OPERATION: texImage2D: ArrayBufferView not big enough for request

WebGL에는 OpenGL이 처음 만들어졌을 때의 모호한 설정들이 남아있습니다. 컴퓨터는 데이터가 특정 크기일 때 더 빠르게 동작합니다. 예를 들어 한 번에 1 바이트가 아닌 2, 4, 8 바이트를 복사하는 것이 더 빠를 수 있습니다. WebGL은 기본적으로 한 번에 4바이트를 사용하므로 각 데이터 행을 4바이트의 배수로 가정합니다. (마지막 행을 제외하고)

위의 데이터는 행마다 3 바이트로, 총 6 바이트에 불과하지만, WebGL은 첫 번째 행에 대해 4 바이트, 두 번째 행에 대해 3 바이트로, 총 7 바이트를 읽으려 시도합니다. 그래서 오류가 발생하는 것입니다.

다음과 같이 한 번에 1 바이트씩 처리하도록 WebGL에 지시할 수 있습니다.

const alignment = 1;
gl.pixelStorei(gl.UNPACK_ALIGNMENT, alignment);

alignment로 사용할 수 있는 값은 1, 2, 4, 8입니다.

WebGL에서 정렬된 데이터와 비정렬 데이터 사이의 속도 차이는 측정할 수 없을 정도로 작을 것으로 생각됩니다. 이 문제가 초보자들을 헷갈리게 만들지 않으려면 4가 아닌 1을 기본값으로 하면 좋겠지만, OpenGL과의 호환성을 유지하기 위해 기본값은 동일해야만 했습니다. 이렇게 하면 포팅된 앱이 패딩된 행을 제공하는 경우 변경없이 작동될 겁니다. 아니면 새 앱에서 항상 alignment를 1로 설정하고 신경쓰지 않아도 됩니다.

이렇게 설정해 주었으니 잘 동작할겁니다.

이제 텍스처에 렌더링하기로 넘어갑시다.

픽셀 vs 텍셀(Texel)

텍스처의 픽셀을 텍셀이라고 부르는 경우가 있습니다. 픽셀은 Picture Element의 줄임말입니다. 텍셀은 Texture Element의 줄임말이죠.

그래픽 전문가들은 뭐라고 하겠지만, 제가 생각에 "텍셀"은 그냥 복잡한 용어일 뿐입니다. 개인적으로 저는 texture element를 언급할 때 특별히 고민하지 않고 그냥 "픽셀"이라고 합니다. 😇

이슈나 버그가 있나요? 깃헙에서 이슈 만들기.
comments powered by Disqus