본문 바로가기
Opengles 3.0 with Android

Chapter 1.2 Vertex Buffer 사용해보기

by SimonLee 2024. 10. 12.

이전 챕터에서 셰이더는 빌드 후 프로그램에 어태치를 했고,

프로그램을 링킹하여 검증까지 완료했다.

 

셰이더에  in 키워드로 정의되어 있는 변수를 어트리뷰트라고 한다.

셰이더 어트리뷰트는 GPU에 존재 하기 때문에,

CPU에 있는 버텍스 데이터를 GPU로 전달해 주어야 한다.

uniform의 경우는 보간이 필요없는 const 데이터 값을 사용할때 사용한다.

 

어트리뷰트 바인딩 과정

1. glGetAttribLocation()

glGetAttribLocation은 셰이더 프로그램에서 속성(attribute) 변수의 위치(인덱스)를 얻는 함수입니다. 버텍스 데이터를 셰이더의 특정 속성에 연결하기 위해 해당 속성의 위치를 알아야 하며, 그 위치는 컴파일된 셰이더 프로그램에서만 결정됩니다.

GLint glGetAttribLocation(GLuint program, const GLchar *name);
  • program: 셰이더 프로그램의 ID입니다. (glCreateProgram으로 생성한 프로그램)
  • name: 셰이더 속성(attribute) 변수의 이름입니다. 이 이름은 셰이더 소스 코드에서 정의한 in 변수를 가리킵니다.
  • 셰이더 프로그램 내에서 속성의 인덱스 위치를 반환합니다. 이 값은 glVertexAttribPointer나 glEnableVertexAttribArray에서 속성의 인덱스로 사용됩니다.
  • 반환값이 -1이면 해당 이름을 가진 속성이 셰이더에서 존재하지 않거나 최적화로 인해 제거된 것입니다.

 

2. glVertexAttribPointer()

glVertexAttribPointer는 버텍스 데이터를 셰이더 속성에 연결하는 함수입니다.

이 함수는 OpenGL에 버텍스 버퍼에 저장된 데이터의 형식과 구조를 알려줍니다.

즉, 메모리에서 데이터를 어떻게 읽어야 할지 정의합니다.

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

 

  • index: 셰이더에서 속성의 인덱스. (glGetAttribLocation으로 얻은 값)
  • size: 속성의 요소 개수. 예를 들어, vec3는 3, vec4는 4가 됩니다.
  • type: 버텍스 데이터의 타입. 보통 GL_FLOAT를 사용합니다.
  • normalized: 정수형 데이터를 정규화할지 여부를 결정합니다. (GL_TRUE 또는 GL_FALSE)
  • stride: 버텍스 사이의 간격(바이트 단위). 같은 속성의 데이터가 연속되어 있으면 0으로 설정합니다.
  • pointer: 버퍼 내에서 속성 데이터의 시작 위치입니다. 보통 NULL 또는 0으로 설정해 첫 번째 데이터를 가리키게 합니다.

 

이 함수는 중요한 특징이 있다.

glVertexAttribPointer는 GPU에서 버퍼를 읽어 올 수도 있고, CPU에서 읽어올 수 도 있다.

 

> VBO를 사용하는 경우:

glBindBuffer(GL_ARRAY_BUFFER, vbo);

VBO를 바인딩한 상태에서 pointer에 들어가는 값은 버퍼에서 해당 속성의 데이터가 시작하는 위치를 가리킵니다.

이 값은 버퍼에서의 오프셋이 됩니다.

일반적으로는 NULL 또는 0으로 첫 번째 데이터 요소부터 시작하게 합니다.

glBindBuffer(GL_ARRAY_BUFFER, vbo);
glVertexAttribPointer(positionAttribLocation, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);

 

 

> VBO를 사용하지 않는 경우:

만약 버퍼가 아닌 CPU 메모리에 있는 데이터를 직접 참조할 경우(예: 배열),

pointer는 그 배열의 메모리 주소가 됩니다.

이때는 GPU에 데이터가 전송되지 않고, CPU 메모리에서 직접 버텍스 데이터를 읽습니다.

일반적으로는 이 방법보다 VBO를 사용하는 것이 더 효율적입니다.

아래 예제 에서도 VBO를 사용하지 않는다.

float vertices[] = { ... };  // CPU 메모리에 있는 버텍스 데이터
glBindBuffer(GL_ARRAY_BUFFER, 0);  // VBO 사용 안 함
glVertexAttribPointer(positionAttribLocation, 3, GL_FLOAT, GL_FALSE, 0, vertices);

 

3. glEnableVertexAttribArray()

glEnableVertexAttribArray는 **셰이더에서 특정 속성(attribute)**을 활성화하는 함수입니다.

이 함수가 호출되면 해당 속성의 데이터를 읽을 수 있게 됩니다.

이 함수가 호출되지 않으면, OpenGL은 해당 속성 데이터를 읽어들이지 않습니다.

void glEnableVertexAttribArray(GLuint index);

index: 활성화하려는 속성의 인덱스. (glGetAttribLocation으로 얻은 값)

 

정리하면, 어레이 데이터를 GPU 셰이더 어트리뷰트(변수)를 연결하기 위해서는 다음과 같다.

glGetAttribLocation() 함수를 통해서 GPU에서 사용하는 어트리뷰트의 인덱스를 가져오고,

glVertexAttribPointer() 함수를 통해서 연결할 버퍼의 정보를 입력하고,

glEnableVertexAttribArray() 함수를 통해서 어트리 뷰트 인덱스 값을 사용하여 어트리뷰트 활성화 여부를 설정한다.

 

유니폼 바인딩 과정

1. glGetUniformLocation

glGetUniformLocation은 특정 셰이더 프로그램에서 uniform 변수의 위치(인덱스)를 가져오는 함수입니다.

이 위치를 알면 나중에 해당 변수에 값을 설정할 수 있습니다.

 

GLint glGetUniformLocation(GLuint program, const GLchar *name);
  • program: uniform 변수를 가져올 셰이더 프로그램의 ID입니다. (glCreateProgram으로 생성한 프로그램)
  • name: uniform 변수의 이름입니다. 이 이름은 셰이더 소스 코드에서 정의된 uniform 변수의 이름과 일치해야 합니다.
  • 지정한 uniform 변수의 인덱스 위치를 반환합니다. 이 값은 나중에 glUniform* 함수를 호출할 때 사용됩니다.
  • 반환값이 -1이면 해당 이름을 가진 uniform 변수가 셰이더에서 존재하지 않거나 최적화로 인해 제거된 것입니다.

2. glUniform1f

glUniform1f는 float 타입의 uniform 변수를 설정하는 함수입니다.

이 함수를 사용하여 셰이더에서 정의된 uniform 변수의 값을 업데이트할 수 있습니다.

void glUniform1f(GLint location, GLfloat v0);
  • location: 설정할 uniform 변수의 위치. (glGetUniformLocation으로 얻은 값)
  • v0: uniform 변수에 설정할 float 값입니다.

정리하면, 유니폼 위치를 가져와서 유니폼 값을 설정합니다.

 

 

셰이더 코드를 분석해보면

CPU에서 선언한 배열과 VertexPosition, VertexColor 어트리뷰트를 연결할 것이다.

CPU에서 정의한 float값을 uniform 변수 RadianAngle을 연결할 것이다.

static const char vertexShader[] =
        "#version 300 es                                          \n"
        "in vec4        VertexPosition;                           \n"
        "in vec4        VertexColor;                              \n"
        "uniform float  RadianAngle;                              \n"
        "out vec4       TriangleColor;                            \n"

        "void main() {                                            \n"
        "  mat2 rotation = mat2(cos(RadianAngle), sin(RadianAngle), \n"
        "                       -sin(RadianAngle), cos(RadianAngle)); \n"
        "  vec2 position = rotation * VertexPosition.xy;            \n"
        "  gl_Position   = vec4(position, VertexPosition.zw); \n"
        "  TriangleColor = VertexColor;                           \n"
        "}\n";

static const char fragmentShader[] =
        "#version 300 es            \n"
        "precision mediump float;   \n"
        "in vec4 TriangleColor;     \n"
        "out vec4 FragColor;        \n"
        "void main() {              \n"
        "  FragColor = TriangleColor;\n"
        "}";

 

void GraphicsRender()
{
    // Which buffer to clear? – color buffer
    glClear( GL_COLOR_BUFFER_BIT );

    // Clear color with black color
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

    // Use shader program to apply on current primitives
    glUseProgram( programID );
    radian = degree++/57.2957795;

    // Query and send the uniform variable.
    radianAngle          = glGetUniformLocation(programID, "RadianAngle");
    glUniform1f(radianAngle, radian);

    // Query ‘VertexPosition’ from vertex shader
    positionAttribHandle = glGetAttribLocation(programID, "VertexPosition");
    colorAttribHandle    = glGetAttribLocation(programID, "VertexColor");

    // Send data to shader using queried attrib location
    glVertexAttribPointer(positionAttribHandle, 2, GL_FLOAT, GL_FALSE, 0, gTriangleVertices);
    glVertexAttribPointer(colorAttribHandle, 3, GL_FLOAT, GL_FALSE, 0, gTriangleColors);

    // Enable vertex position attribute
    glEnableVertexAttribArray(positionAttribHandle);
    glEnableVertexAttribArray(colorAttribHandle);

    // Draw 3 triangle vertices from 0th index
    glDrawArrays(GL_TRIANGLES, 0, 3);
}

 

위 코드는 쉽게 이해 갈 것이다.

GraphicsRender() 함수는 매 프레임마다 불린다.

버텍스 셰이더 코드를 보면 유니폼으로 값을 전달 받고, 로테이션 매트릭스를 GPU에서 생성한다.

GPU 최적화를 생각해보면 로테이션 매트릭스를 CPU에서 만들고 전달하는 것이 좀 더 낫다. 

 

아래 렌더 함수내에서 attribute 세팅하는 부분이 들어가 있으면 매 프레임마다 실행되는데, 이게 맞나 ?

매 각도가 달라지기 때문에 glUniform1f는 매 프레임마다 실행이 되야 하는 것은 맞다.

하지만 glGetUniformLocation 경우에는 최초 한번만 실행하여 위치를 알면 된다.

 

그래서 최적화 한 코드는 다음과 같다.

SettingAttributeAndUniform() 최초 1회만 호출하고, 

프레임 렌더링 함수의 대부분은 Setting으로 옮겨졌다.

 

당연히 멀티렌더링 같이 Program이 여러개거나 버텍스 어레이가 여러개라서 바인딩을 여러번 해야하는 경우에는

Setting에 있는 내용이 렌더링 함수로 들어간 것이 맞다 ^^~

void SettingAttributeAndUniform() {
    // Use shader program to apply on current primitives
    glUseProgram( programID );

    // Query and send the uniform variable.
    radianAngle          = glGetUniformLocation(programID, "RadianAngle");

    // Query ‘VertexPosition’ from vertex shader
    positionAttribHandle = glGetAttribLocation(programID, "VertexPosition");
    colorAttribHandle    = glGetAttribLocation(programID, "VertexColor");

    // Send data to shader using queried attrib location
    glVertexAttribPointer(positionAttribHandle, 2, GL_FLOAT, GL_FALSE, 0, gTriangleVertices);
    glVertexAttribPointer(colorAttribHandle, 3, GL_FLOAT, GL_FALSE, 0, gTriangleColors);

    // Enable vertex position attribute
    glEnableVertexAttribArray(positionAttribHandle);
    glEnableVertexAttribArray(colorAttribHandle);
}

void GraphicsRender()
{
    // Which buffer to clear? – color buffer
    glClear( GL_COLOR_BUFFER_BIT );

    // Clear color with black color
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

    radian = degree++/57.2957795;
    glUniform1f(radianAngle, radian);

    // Draw 3 triangle vertices from 0th index
    glDrawArrays(GL_TRIANGLES, 0, 3);
}