[openGL] 버퍼

나우히즈·2024년 10월 3일

Graphics

목록 보기
5/17

서론

이번에는 버퍼에 대해 다루어 보자.
버퍼는 데이터를 저장하고 접근하기 위한 storage이다. 최신 그래픽스 프로세서는 스트리밍 프로세서로 설계되어서 많은 양의 데이터를 처리하고 생성할 수 있는 방식이다.

이러한 방식에 맞게 효율적으로 데이터를 관리하는 방식이 버퍼와 텍스처가 있다. 버퍼는 메모리 할당과 유사하게 데이터의 연속된 공간을 의미하며 타입이 정해져있지 않다.

좀 더 자세하게 버퍼에 대해 알아보도록 하자.


버퍼

glBufferData vs glBufferSubData

glBufferDataglBufferSubData는 OpenGL에서 버퍼의 데이터를 설정하는데 사용.

1. glBufferData

  • 역할: 버퍼 객체의 전체 크기를 설정하고 데이터를 업로드.

  • 사용 시점: 보통 버퍼를 처음 생성할 때 사용하거나, 기존 버퍼를 완전히 덮어쓰고 새 데이터를 채울 때 사용.

  • 메모리 할당: 새로운 메모리를 할당하고 이전에 존재하던 데이터는 모두 제거.

  • 함수 프로토타입:

    void glBufferData(GLenum target, GLsizeiptr size, const void* data, GLenum usage);
    • target: 버퍼의 타입 (예: GL_ARRAY_BUFFER, GL_ELEMENT_ARRAY_BUFFER 등)
    • size: 버퍼의 크기 (바이트 단위)
    • data: 데이터를 담은 포인터 (초기화할 데이터. 만약 NULL이면 빈 버퍼가 생성됨)
    • usage: 버퍼의 사용 용도 (GL_STATIC_DRAW, GL_DYNAMIC_DRAW 등)
  • 예시:

    glGenBuffer(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    이 코드는 버퍼 객체에 데이터를 초기화하고 GPU 메모리에 할당하는 역할을 함

2. glBufferSubData

  • 역할: 이미 할당된 버퍼의 일부 구간에 데이터를 업데이트.

  • 사용 시점: 기존에 할당된 버퍼의 특정 부분만 업데이트할 필요가 있을 때 사용.

  • 메모리 할당: 새로운 메모리를 할당하지 않으며, 이미 존재하는 버퍼의 일부 데이터만 수정.

  • 함수 프로토타입:

    void glBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, const void* data);
    • target: 버퍼의 타입 (예: GL_ARRAY_BUFFER, GL_ELEMENT_ARRAY_BUFFER 등)
    • offset: 버퍼의 데이터에서 업데이트를 시작할 위치 (바이트 단위)
    • size: 업데이트할 데이터의 크기 (바이트 단위)
    • data: 데이터를 담은 포인터 (업데이트할 새 데이터)
  • 예시:

    // VBO의 일부분을 업데이트
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferSubData(GL_ARRAY_BUFFER, offset, sizeof(new_data), new_data);

    이 코드는 기존에 할당된 버퍼 중 offset부터 시작하여 new_data로 데이터를 업데이트함.

주요 차이점

  • 메모리 할당:
    • glBufferData새로운 메모리를 할당하고 버퍼를 다시 초기화.
    • glBufferSubData이미 할당된 메모리를 수정할 때 사용되며, 추가적인 메모리 할당은 없음.

약간 다른 결이지만, 버퍼 객체에 데이터를 전달하는 다른 방법으로 버퍼 객체에 해당하는 메모리 포인터를 openGL에 요청하여 직접 데이터를 복사하는 방법이 있다.

glMapBuffer

glMapBuffer는 OpenGL에서 버퍼 객체의 메모리를 직접 접근할 수 있도록 포인터를 반환해주는 함수입니다. 즉, GPU 메모리에 있는 데이터를 CPU 쪽에서 수정하거나 읽을 수 있도록 매핑해주는 기능을 한다.

역할

glMapBuffer는 버퍼를 GPU 메모리에서 CPU 메모리처럼 직접 접근 가능한 포인터로 매핑하여, CPU가 GPU 메모리에 직접 읽거나 쓸 수 있도록 해준다. 주로 큰 데이터를 수정하거나 복잡한 연산을 수행해야 할 때 사용다.

함수 프로토타입

void* glMapBuffer(GLenum target, GLenum access);
  • target: 매핑할 버퍼의 대상 (예: GL_ARRAY_BUFFER, GL_ELEMENT_ARRAY_BUFFER 등).

  • access: 메모리 접근 권한을 지정 (예: 읽기, 쓰기, 혹은 읽기-쓰기).

    주요 접근 모드:

    • GL_READ_ONLY: 버퍼 데이터를 읽기 전용으로 매핑.
    • GL_WRITE_ONLY: 버퍼 데이터를 쓰기 전용으로 매핑.
    • GL_READ_WRITE: 버퍼 데이터를 읽기와 쓰기 모두 가능하게 매핑.

사용 예시

// VBO를 GL_ARRAY_BUFFER로 바인드
glBindBuffer(GL_ARRAY_BUFFER, VBO);

// 버퍼를 쓰기 전용으로 매핑
void* bufferData = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);

// 매핑된 메모리에 데이터를 쓰기 (CPU 메모리처럼 사용)
if (bufferData) {
    // 데이터를 수정
    float* data = (float*)bufferData;
    data[0] = 1.0f;  // 예시로 첫 번째 값을 수정
}

// 버퍼 매핑 해제
glUnmapBuffer(GL_ARRAY_BUFFER);

주요 특징

  • 직접 접근: glMapBuffer로 버퍼를 매핑하면 GPU 메모리를 CPU에서 직접 접근할 수 있다. 이를 통해 연산을 최적화하거나 GPU로 보내야 할 데이터를 효율적으로 관리할 수 있다.
  • 효율성: 큰 데이터나 자주 변경되는 데이터에 대해 CPU와 GPU 간의 메모리 복사를 최소화할 수 있는 장점이 있다.
  • 매핑 후 해제: glMapBuffer로 매핑한 후에는 glUnmapBuffer로 매핑을 해제해야 한다. 매핑된 상태로 두면 OpenGL이 더 이상 버퍼를 사용할 수 없다.

이제 만들어진 버퍼에 값을 넣을 방법을 생각해보자. 이미 glBufferData 함수를 통해 값을 전달해주었을 수도 있지만, 버퍼에 새로운 데이터를 넣고 싶을 수 있으니까.

glClearBufferSubData를 사용하여 버퍼의 일부를 단일 값으로 채우는 작업을 수행할 수 있다. 이 함수는 버퍼의 일부분을 초기화하거나 특정 값을 채우기 위한 목적으로 사용된다.

glClearBufferSubData 함수

void glClearBufferSubData(GLenum target, GLenum internalformat,
					GLintptr offset, GLsizeiptr size, 
                    GLenum format, GLenum type, const void *data);

매개변수 설명

  • target: 버퍼의 종류를 지정합니다. (예: GL_ARRAY_BUFFER, GL_ELEMENT_ARRAY_BUFFER 등)
  • internalformat: 버퍼의 내부 저장 형식입니다. 예를 들어 GL_R32F, GL_RGBA8 같은 값으로, 채울 데이터의 형식을 정의합니다.
  • offset: 버퍼 내에서 데이터를 시작할 오프셋(바이트 단위).
  • size: 채울 데이터의 크기(바이트 단위).
  • format: 채울 데이터의 형식 (예: GL_RED, GL_RGBA 등).
  • type: 채울 데이터의 자료형 (예: GL_FLOAT, GL_UNSIGNED_BYTE 등).
  • data: 채울 값을 담고 있는 포인터입니다. 단일 값 또는 배열 형태로 전달할 수 있습니다.

예시 코드

// 1. 버퍼를 생성하고 바인딩
GLuint buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);

// 2. 버퍼에 임의의 크기를 할당
glBufferData(GL_ARRAY_BUFFER, 1024, nullptr, GL_DYNAMIC_DRAW);

// 3. 단일 값으로 버퍼의 일부를 채우기
float value = 1.0f;
glClearBufferSubData(GL_ARRAY_BUFFER, GL_R32F, 0, 512, GL_RED, GL_FLOAT, &value);

위 예시에서는 glClearBufferSubData를 사용하여 버퍼의 첫 512바이트를 1.0으로 채우는 과정.

주요 포인트

  1. 데이터 초기화: glClearBufferSubData는 특정 값을 사용해 버퍼의 일부를 초기화할 때 유용.
  2. 부분적인 버퍼 조작: 이 함수는 버퍼의 일부분만 특정 값으로 초기화할 수 있기 때문에, 전체 버퍼 데이터를 변경하지 않고도 원하는 영역만 수정할 수 있다.
  3. 포맷과 타입 매칭: formattype은 데이터를 어떻게 처리할지 결정하며, 이들이 internalformat과 잘 매칭되는지 확인해야 한다. 예를 들어, 위처럼GL_R32F와 같은 내부 형식에 맞게 GL_REDGL_FLOAT를 사용.

glCopyBufferSubData 함수

void glCopyBufferSubData(GLenum readTarget, 
						GLenum writeTarget, 
						GLintptr readOffset, 
                        GLintptr writeOffset, 
                        GLsizeiptr size);

glCopyBufferSubData이미 존재하는 두 버퍼 간의 데이터를 복사하는 데 사용. 두 버퍼 간의 데이터를 메모리 복사 방식으로 효율적으로 전송할 수 있다.

매개변수 설명

  • readTarget: 복사할 원본 버퍼의 바인딩 포인트. 예를 들어 GL_ARRAY_BUFFER, GL_ELEMENT_ARRAY_BUFFER 등.
  • writeTarget: 복사할 대상 버퍼의 바인딩 포인트. 원본과 동일할 수도 있고 다른 버퍼일 수도 있습니다.
  • readOffset: 원본 버퍼에서 데이터를 읽기 시작할 오프셋(바이트 단위).
  • writeOffset: 대상 버퍼에 데이터를 쓰기 시작할 오프셋(바이트 단위).
  • size: 복사할 데이터의 크기(바이트 단위).

예시 코드

// 1. 두 개의 버퍼 생성 및 바인딩
GLuint bufferA, bufferB;
glGenBuffers(1, &bufferA);
glGenBuffers(1, &bufferB);

// 2. 원본 버퍼와 대상 버퍼에 각각 메모리 할당
glBindBuffer(GL_ARRAY_BUFFER, bufferA);
glBufferData(GL_ARRAY_BUFFER, 1024, nullptr, GL_STATIC_DRAW);

glBindBuffer(GL_ARRAY_BUFFER, bufferB);
glBufferData(GL_ARRAY_BUFFER, 1024, nullptr, GL_STATIC_DRAW);

// 3. 버퍼 A에서 버퍼 B로 데이터 복사 (512바이트)
glCopyBufferSubData(GL_ARRAY_BUFFER, GL_ARRAY_BUFFER, 0, 0, 512);

위 코드에서 bufferA의 첫 512바이트를 bufferB의 첫 512바이트로 복사합니다.

주요 특징

  1. 효율적인 버퍼 간 복사: glCopyBufferSubData는 CPU 메모리를 거치지 않고 GPU에서 직접 버퍼 간 데이터 이동을 처리하여 빠르고 효율적이다.
  2. 부분 복사 가능: 원하는 범위(readOffset에서 size만큼)를 복사할 수 있습니다. 즉, 버퍼 전체를 복사하지 않고 필요한 부분만 복사 가능
  3. 서로 다른 버퍼 간 복사: 복사 대상이 되는 두 버퍼는 서로 다를 수도 있으며, 동일한 버퍼 내에서 특정 범위를 복사할 수도 있다.

주의사항

  • 복사 범위가 겹치지 않도록 해야 합니다. 동일한 버퍼에서 복사할 때, readOffsetwriteOffset이 겹치지 않게 설정해야 합니다.
  • 버퍼가 충분히 크기 때문에 readOffsetwriteOffset이 유효한 범위 내에 있어야 하며, 복사할 데이터 크기가 버퍼 크기를 초과하지 않도록 해야 합니다.

이제 세팅한 버퍼 데이터를 GPU 가 해석할 수 있게 속성 정보를 알려주고 활성화시켜줘야한다.

glVertexAttribPointerglEnableVertexAttribArray는 OpenGL에서 버텍스 데이터와 그 데이터를 처리하는 쉐이더 간의 연결을 설정하는 중요한 함수들이다.

이 두 함수는 함께 사용되어 버텍스 데이터의 포맷과 사용 방식을 지정한다.

1. glVertexAttribPointer

이 함수는 버텍스 속성의 데이터 형식위치를 지정하는 함수입니다. 즉, VBO(Vertex Buffer Object)에서 특정 버텍스 속성(예: 위치, 색상, 텍스처 좌표 등)을 쉐이더로 전달하는 방법을 정의합니다.

함수 시그니처

void glVertexAttribPointer(GLuint index, 
						GLint size,
                        GLenum type,
                        GLboolean normalized,
                        GLsizei stride,
                        const void* pointer);

파라미터 설명

  • index: 속성 인덱스. 쉐이더에서 사용할 변수의 위치(location)를 나타낸다. 쉐이더의 layout(location = n)을 이용해 인덱스를 매칭시킬 수 있다.
  • size: 속성의 구성 요소 수. 예를 들어, 2D 위치의 경우 (x, y)로 2개의 값이 필요하므로 size는 2가 된다. 색상은 4개의 값(R, G, B, A)이므로 size는 4이다.
  • type: 데이터 타입. 데이터가 어떻게 저장되어 있는지에 대한 정보. 예를 들어, GL_FLOAT(float형 데이터), GL_UNSIGNED_BYTE(unsigned byte형 데이터) 등을 사용.
  • normalized: 정규화 여부. 데이터가 정규화되어야 하는지 여부를 지정
  • stride: 연속된 버텍스 간의 거리. VBO에서 각 버텍스가 차지하는 크기.다음 버텍스까지의 거리라고 보면 됨.
  • pointer: 버텍스 데이터의 시작 주소. VBO에서 이 속성의 첫 번째 데이터가 시작되는 위치를 지정. 보통 0 또는 offset을 사용.

예시

// 위치 데이터 (3D 좌표)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);

// 색상 데이터 (RGB)
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, color));

// 텍스처 좌표 데이터 (2D)
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, texCoords));

이 예시에서 0, 1, 2는 쉐이더에서 선언한 layout(location = n)에 해당하는 인덱스.

2. glEnableVertexAttribArray

이 함수는 특정 속성 인덱스를 활성화시켜 해당 속성의 데이터를 쉐이더에 전달할 수 있도록 한다. glVertexAttribPointer로 데이터를 설정한 후, 이 속성 데이터를 쉐이더에서 실제로 사용하려면 해당 속성을 활성화해야 한다. 이렇게 활성화되어있는 attribute 정보는 VAO 에 저장된다.

함수 시그니처

void glEnableVertexAttribArray(GLuint index);

파라미터 설명

  • index: 활성화할 속성 인덱스. 이 인덱스는 glVertexAttribPointer에서 사용한 인덱스와 일치해야 한다.

예시

// 위치 속성 활성화
glEnableVertexAttribArray(0);

// 색상 속성 활성화
glEnableVertexAttribArray(1);

// 텍스처 좌표 속성 활성화
glEnableVertexAttribArray(2);

3. 함께 사용하는 방식

보통, glVertexAttribPointer속성 데이터의 포맷을 정의하고, glEnableVertexAttribArray해당 속성을 활성화하는 데 사용.

  1. VBO와 VAO를 설정한 후, glVertexAttribPointer로 속성 포맷을 정의
  2. glEnableVertexAttribArray를 호출하여 정의된 속성을 활성화
  3. 렌더링 시 VAO에 바인딩된 속성 데이터가 사용

glVertexAttribPointer -> glEnableVertexAttribArray를 통해 쉐이더에서 입력받은 버텍스 데이터를 사용하여 렌더링하고 나면, glDisableVertexAttribArray 함수를 통해 활성화되어있는 attribute를 비활성화해주는 것이 좋다.

glDisableVertexAttribArray

glDisableVertexAttribArray특정 버텍스 속성 인덱스를 비활성화하는 함수입니다. 이 함수는 보통 쉐이더에서 해당 속성을 더 이상 사용하지 않도록 할 때 사용됩니다.

언제 사용해야 할까?

  • 렌더링 호출 후: 여러 개의 렌더링 호출을 할 때, 각 호출이 끝나면 해당 속성을 비활성화하는 것이 좋습니다.
  • 다른 VAO 사용 시: 새로운 VAO가 바인딩되면 이전 VAO에 설정된 속성들이 그대로 남아 있을 수 있기 때문에, 속성을 사용하고 싶지 않다면 이를 비활성화한 뒤 새로운 VAO에 대한 속성을 활성화해야 합니다.

glDisableVertexAttribArrayglEnableVertexAttribArray의 관계

  • glEnableVertexAttribArray로 활성화한 속성은 해당 렌더링 동안 활성화 상태로 유지됩니다.
  • glDisableVertexAttribArray는 활성화된 속성을 비활성화합니다.
  • 한 번 활성화한 속성은, 명시적으로 비활성화하지 않는 한 계속 활성화 상태로 남게 됩니다.

이렇게 버텍스 버퍼를 생성하여 쉐이더에 넘겨 렌더링될수 있게 한다. 하지만 프로그래머가 일일히 버텍스 버퍼를 만들어 배열 객체를 설정하고, 모든 버텍스 속성 포인터를 설정하는건 조잡하고 실수하기 쉬운 부분이다. 따라서 모델 데이터를 파일로 저장하여 로딩하여 사용하는 방식이 일반적이다.

0개의 댓글