WebGL 튜토리얼 목록

튜토리얼 2-1편에서는 VAO, VBO라는 개념과 이를 이용하여 화면에 직사각형을 그리는 방법에 대해 알아보았고, 튜토리얼 2-2편에서는 WebGL을 감싸는 레이어를 설계해 보았습니다.

전 튜토리얼에 이어, 이번에는 인덱스 버퍼라는 개념에 대해 알아보도록 하겠습니다.

인덱스 버퍼 (Index Buffer)

직사각형은 4개의 vertex를 가지고 있는데, 지금의 코드에서는 6개의 vertex를 전달합니다. 모든 도형을 삼각형으로 분할하여 고려하기 때문에, 사각형을 2개의 삼각형으로 나누어 6개의 vertex가 생기는 것입니다.

하지만, 6개의 vertex 중 실제로 2개는 중복되는 데이터입니다. 지금은 vertex마다 차지하는 데이터가 작아서 (2개의 float이니, 8 byte) 체감이 잘 안 됩니다. 그러나 vertex attribute가 많아져 데이터 크기가 커지고 모델도 복잡해져 수십만 개의 vertex가 필요하다면, 이렇게 겹치는 데이터로 인해 낭비되는 공간이 커질 것입니다. 이 때문에 인덱스 버퍼를 사용합니다.

인덱스 버퍼는 vertex의 인덱스만 나열한 배열을 담은 버퍼를 말합니다. Vertex의 attribute 데이터를 담은 VBO와 서로 다른 버퍼입니다. 즉, 기존에 사각형을 다음과 같이 저장했다면,

  • VBO: [(-0.5, 0.5), (-0.5, -0.5), (0.5, -0.5), (-0.5, 0.5), (0.5, -0.5), (0.5, 0.5)]

인덱스 버퍼를 사용하면 다음과 같습니다.

  • VBO: [(-0.5, 0.5), (-0.5, -0.5), (0.5, 0.5), (0.5, -0.5)]
  • IBO: [0, 1, 3, 0, 3, 2]

VBO가 실제 vertex의 개수인 4개 vertex 데이터만 담고 있고, 그 vertex들이 어떻게 삼각형을 이루는지 IBO가 담고 있습니다.

WebGL에서 index buffer 사용하기

IBO도 Buffer Object의 종류 중 하나이기 때문에 BO와 같은 WebGL API 함수들로 관리합니다. 단, 전에 VBO를 만질 때는 target 인자로 gl.ARRAY_BUFFER를 지정했다면, IBO는 gl.ELEMENT_ARRAY_BUFFER를 지정합니다.

IBO를 VAO와 연결하는 것은 좀 tricky합니다. VAO는 여러 개의 VBO를 가질 수 있지만, IBO는 단 하나만 가질 수 있습니다. 따라서 VBO처럼 gl.vertexAttribPointer로 명시적으로 연결하는 것이 아니라, VAO가 bind된 상태에서 gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo)를 호출하면 별도의 작업 없이 bind된 VAO가 ibo의 reference를 저장합니다.

VAO에서 VBO와 IBO를 모두 사용하여 렌더링하는 경우, 마찬가지로 사용할 VAO를 bind 한후 gl.drawArrays API 대신 gl.drawElements를 사용합니다.

  • void gl.drawElements(GLenum mode, GLsizei count, GLenum type, GLintptr offset)

    mode, countgl.drawArrays와 동일합니다. typeoffset은 VBO를 glVertexAttribPointer에서 설정하는 것과 비슷하게, IBO에 담긴 데이터를 어떻게 해석할지 설정하는 인자들입니다. type는 IBO 데이터의 타입을, offset은 어디서부터 데이터를 사용할지 byte 단위를 설정합니다.

엔진에 IBO 적용하기

Mesh 클래스에서 IBO를 지원하는 코드를 작성하겠습니다. 먼저 클래스애 IBO를 저장할 변수를 추가합니다.

export default class Mesh {
    ...
    _ibo: WebGLBuffer;
    ...
}

Constructor에서 IBO를 생성하고, delete에서 삭제하는 부분을 작성해 줍니다.

constructor() {
    ...
    this._ibo = this._gl.createBuffer();
    ...
}

delete() {
    ...
    this._gl.deleteBuffer(this._ibo);
    ...
}

어플리케이션에서 IBO에 데이터를 전송하는 메소드도 추가합니다.

updateIndexBuffer(buffer: ArrayBuffer): void {
    if (this._deleted)
        return;
    this._gl.bindBuffer(this._gl.ELEMENT_ARRAY_BUFFER, this._ibo);
    this._gl.bufferData(this._gl.ELEMENT_ARRAY_BUFFER, buffer, this._gl.STATIC_DRAW);
    this._gl.bindBuffer(this._gl.ELEMENT_ARRAY_BUFFER, null);
}

다음으로, configure 함수에서 VAO과 IBO를 연결해야 합니다. VAO가 bind된 상태인 this._gl.bindVertexArray(this._vao)this._gl.bindVertexArray(null) 사이에 IBO를 bind하면 됩니다.

configure(attributes: Array<[GLenum, number]>): void {
    ...
    this._gl.bindVertexArray(this._vao);
    ...
    this._gl.bindBuffer(this._gl.ELEMENT_ARRAY_BUFFER, this._ibo);
    ...
    this._gl.bindVertexArray(null);
    ...
}

마지막으로 render 메소드가 gl.drawArrays 대신 gl.drawElements를 사용하도록 바꿉니다.

render(): void {
    if (this._deleted)
        return;
    this._gl.drawElements(this._gl.TRIANGLES, this._count, this._gl.UNSIGNED_INT, 0);
}

어플리케이션에 IBO 적용하기

어플리케이션에서는 다른 부분 바꿀 필요 없이, 기존에 updateVertexBuffer에서 6개 vertex를 전송하는 대신 4개 vertex만 전송하고, 대신 updateIndexBuffer로 두 개 삼각형이 차지하는 인덱스를 전송하면 됩니다.

...
mesh.updateVertexBuffer(new Float32Array([
    -0.5,  0.5, -0.5, -0.5,  0.5, 0.5, 0.5, -0.5
]));
mesh.updateIndexBuffer(new Uint32Array([
    0, 1, 3, 0, 3, 2
]));
...

결과는 2번 튜토리얼과 동일하지만, GPU 내부에서는 중복되는 데이터가 줄어들었기 때문에 더 적은 메모리를 사용할 수 있었습니다.

링크

GitHub Repository