Skip to content

고급 데이터

지금까지 대부분의 장에서 우리는 OpenGL의 버퍼를 사용하여 GPU에 데이터를 저장하는 방법을 광범위하게 다루었습니다. 이번 장에서는 버퍼를 관리하는 몇 가지 다른 접근 방식에 대해 간략하게 살펴보겠습니다.

OpenGL에서 버퍼는 본질적으로 특정 GPU 메모리 영역을 관리하는 객체일 뿐입니다. 특정 버퍼 타겟에 바인딩할 때 버퍼에 의미가 부여됩니다. 버퍼는 GL_ARRAY_BUFFER에 바인딩할 때만 정점 배열 버퍼가 되지만, GL_ELEMENT_ARRAY_BUFFER에 바인딩해도 마찬가지입니다. OpenGL은 내부적으로 타겟별로 버퍼에 대한 참조를 저장하고, 타겟에 따라 버퍼를 다르게 처리합니다.

지금까지는 glBufferData 함수를 호출하여 버퍼의 메모리를 채워왔습니다. 이 함수는 GPU 메모리를 할당하고 해당 메모리에 데이터를 추가합니다. 만약 데이터 인수로 NULL을 전달하면, 함수는 메모리를 할당만 하고 채우지는 않습니다. 이는 특정 크기의 메모리를 먼저 예약해 두고 나중에 해당 버퍼를 다시 사용하려는 경우에 유용합니다.

함수 호출 한 번으로 버퍼 전체를 채우는 대신, glBufferSubData 함수를 호출하여 버퍼의 특정 영역만 채울 수도 있습니다. 이 함수는 버퍼 대상, 오프셋, 데이터 크기, 그리고 실제 데이터를 인수로 받습니다. 이 함수의 새로운 기능은 버퍼를 채울 시작 위치를 지정하는 오프셋을 제공할 수 있다는 점입니다. 이를 통해 버퍼 메모리의 특정 부분만 삽입/업데이트할 수 있습니다. 단, 버퍼에 충분한 메모리가 할당되어 있어야 하므로 glBufferSubData 함수를 호출하기 전에 glBufferData 함수를 먼저 호출해야 합니다.

glBufferSubData(GL_ARRAY_BUFFER, 24, sizeof(data), &data); // 범위: [24, 24 + sizeof(data)]

버퍼에 데이터를 넣는 또 다른 방법은 버퍼의 메모리에 대한 포인터를 요청하여 데이터를 메모리에 직접 복사하는 것입니다. OpenGL에서 glMapBuffer를 호출하면 현재 바인딩된 버퍼의 메모리에 대한 포인터가 반환되므로 이를 사용하여 작업을 수행할 수 있습니다.

float data[] = {
  0.5f, 1.0f, -0.35f
  [...]
};
glBindBuffer(GL_ARRAY_BUFFER, buffer);
// 포인터 얻기
void *ptr = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
// 이제 메모리에 데이터를 복사합니다.
memcpy(ptr, data, sizeof(data));
// 포인터 처리가 완료되었음을 OpenGL에 알려주는 것을 잊지 마세요.
glUnmapBuffer(GL_ARRAY_BUFFER);

glUnmapBuffer 함수를 통해 포인터 연산이 완료되었음을 OpenGL에 알리면, OpenGL은 작업이 끝났음을 인식합니다. 매핑 해제를 통해 포인터는 무효화되고, OpenGL이 데이터를 버퍼에 성공적으로 매핑했다면 함수는 GL_TRUE를 반환합니다.

glMapBuffer를 사용하면 데이터를 임시 메모리에 먼저 저장하지 않고 버퍼에 직접 매핑할 수 있습니다. 파일에서 데이터를 직접 읽어 버퍼 메모리에 복사하는 경우를 생각해 보세요.

정점 속성 일괄 처리

glVertexAttribPointer를 사용하여 정점 배열 버퍼의 내용에 대한 속성 레이아웃을 지정할 수 있었습니다. 정점 배열 버퍼 내에서는 속성들을 인터리브(interleave)1 방식으로 배치했는데, 즉 각 정점마다 위치, 법선, 텍스처 좌표를 메모리상에 서로 나란히 배치한 것입니다. 이제 버퍼에 대해 조금 더 알게 되었으므로, 다른 접근 방식을 취할 수 있습니다.

바로은 데이터를 인터리브 방식으로 저장하는 대신 속성 유형별로 큰 덩어리로 묶어 배치하는 것입니다. 123123123123과 같은 인터리브 방식 대신 111122223333과 같은 배치 방식을 사용하는 것입니다.

파일에서 정점 데이터를 불러올 때 일반적으로 위치 배열과 법선 배열, 필요에 따라 텍스처 좌표 배열도 함께 가져옵니다. 이러한 배열들을 하나의 큰 배열로 결합하는 데에는 상당한 노력이 필요할 수 있습니다. 따라서 배치 처리 방식을 사용하는 것이 더 쉬운 해결책이며, glBufferSubData를 사용하여 쉽게 구현할 수 있습니다.

float positions[] = { ... };
float normals[] = { ... };
float tex[] = { ... };
// 버퍼 채우기
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(positions), &positions);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions), sizeof(normals), &normals);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions) + sizeof(normals), sizeof(tex), &tex);

이렇게 하면 속성 배열 전체를 별도의 처리 과정 없이 버퍼로 직접 전송할 수 있습니다. glBufferData를 사용하여 하나의 큰 배열로 결합한 후 버퍼를 바로 채울 수도 있지만, glBufferSubData를 사용하면 이러한 작업에 훨씬 더 적합합니다.

이러한 변경 사항을 반영하도록 정점 속성 포인터도 업데이트해야 합니다.

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), 0);  
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)(sizeof(positions)));  
glVertexAttribPointer(
  2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)(sizeof(positions) + sizeof(normals)));  

참고로, 스트라이드 매개변수는 정점 속성의 크기와 같습니다. 다음 정점 속성 벡터는 해당 벡터의 3개(또는 2개) 구성 요소 바로 다음에 위치하기 때문입니다.

이로써 정점 속성을 설정하고 지정하는 또 다른 접근 방식을 얻을 수 있습니다. 두 접근 방식 모두 사용 가능하며, 일반적으로 정점 속성을 설정하는 데 있어 좀 더 체계적인 방법입니다. 하지만 각 정점 셰이더 실행에 대한 정점 속성이 메모리에서 밀접하게 정렬되므로 인터리브 방식이 여전히 권장됩니다.

버퍼 복사하기

버퍼에 데이터가 채워지면 해당 데이터를 다른 버퍼와 공유하거나 버퍼 내용을 다른 버퍼로 복사해야 할 수 있습니다. glCopyBufferSubData 함수를 사용하면 한 버퍼에서 다른 버퍼로 데이터를 비교적 쉽게 복사할 수 있습니다. 함수의 프로토타입은 다음과 같습니다.

void glCopyBufferSubData(GLenum readtarget, GLenum writetarget, GLintptr readoffset,
                         GLintptr writeoffset, GLsizeiptr size);

readtarget 및 writetarget 매개변수는 복사할 버퍼의 대상을 지정합니다. 예를 들어, VERTEX_ARRAY_BUFFER 버퍼에서 VERTEX_ELEMENT_ARRAY_BUFFER 버퍼로 복사하려면 각각 read 및 write 대상으로 해당 버퍼를 지정합니다. 그러면 해당 버퍼 대상에 현재 바인딩된 버퍼들이 영향을 받게 됩니다.

그런데 만약 우리가 두 개의 서로 다른 정점 배열 버퍼에 데이터를 읽고 쓰고 싶다면 어떻게 해야 할까요? 같은 버퍼 타겟에 두 버퍼를 동시에 바인딩할 수는 없습니다. 바로 이 이유 때문에 OpenGL은 데이터를 복사할 때 사용할 수 있는 읽기 전용 버퍼(GL_COPY_READ_BUFFER)와 쓰기 전용 버퍼(GL_COPY_WRITE_BUFFER)라는 두 개의 새로운 버퍼 타겟을 제공합니다. 이제 원하는 버퍼를 이 새로운 타겟에 바인딩하고, readtarget과 writetarget 인수로 설정하면 됩니다.

glCopyBufferSubData 함수는 지정된 읽기 오프셋에서 지정된 크기의 데이터를 읽어 쓰기 오프셋에 있는 쓰기 대상 버퍼에 씁니다. 두 개의 정점 배열 버퍼 내용을 복사하는 예는 아래와 같습니다.

glBindBuffer(GL_COPY_READ_BUFFER, vbo1);
glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2);
glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, 8 * sizeof(float));

writetarget 버퍼를 새로운 버퍼 대상 유형 중 하나에만 바인딩하는 방식으로도 이 작업을 수행할 수 있었습니다.

float vertexData[] = { ... };
glBindBuffer(GL_ARRAY_BUFFER, vbo1);
glBindBuffer(GL_COPY_WRITE_BUFFER, vbo2);
glCopyBufferSubData(GL_ARRAY_BUFFER, GL_COPY_WRITE_BUFFER, 0, 0, 8 * sizeof(float));  

버퍼 조작 방법에 대한 추가적인 지식을 갖추면 버퍼를 더욱 흥미로운 방식으로 활용할 수 있습니다. OpenGL을 더 깊이 공부할수록 이러한 새로운 버퍼 메서드의 유용성은 더욱 커집니다. 다음 장에서는 유니폼 버퍼 객체에 대해 다루면서 glBufferSubData를 효과적으로 활용할 것입니다.


  1. 인터리브 방식이란, 데이터를 섞어서 저장하는 것입니다.
    예를 들어서 위치, 법선, 텍스처 좌표 데이터를 각각 P, N, T라고 했을때,
    메모리 위에 PNTPNTPNTPNT 같은 형식으로 저장하는 것입니다. ↩

Comments