이제 OpenGL Pipeline 에 따라 이전에 실습한 RandomDot_GPU.cpp 코드를 해석해보자.
먼저 CPU에서 우리가 그리고자 하는 points 데이터를 생성한다.
#include <vgl.h>
#include <math.h>
#include <InitShader.h>
struct vec2 {
float x, y;
}
const int NUM_POINTS = 5000;
void init() {
// 1. Generate Data in CPU
vec2 points[5000];
for (int i = 0; i < NUM_POINTS; i++) {
points[i].x = (rand() % 1000) / 500.0f - 1;
points[i].y = (rand() % 1000) / 500.0f - 1;
}
}
이제 CPU에서 만든 points의 vertex data를 GPU에 넘겨야 한다. 이때, 우리는 vertex data를 VBOs의 형태로 넘겨야 하는데, VBO는 반드시 VAO에 bind 되어야 한다.
VAO를 생성한 후 bind 한다. 여기서 bind는 해당 VAO를 사용할 것을 임의로 알려주는 것이다.
// 2. Send Data(Array) to GPU
// 2.1. Create a Vertex Array and bind the buffer
GLuint vao; // OpenGL unsigned int
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
이때 우리는 handler를 사용해 VAO를 bind 한다. handler 는 프로그램 밖 메모리, 즉, 운영체제가 관리하는 메모리에 접근하기 위해 사용하는 unsigned int 값이다.
void glGenVertexArrays(GLsize n, GLuint &arrays)
는 Vertex Arrays Objects를 GPU에 n
개 생성하고 이들을 관리할 수 있도록 handler 값을 *arrays
에 담는다. 따라서 glGenVertexArrays(1, &vao)
를 실행하면 GPU에 VAO가 한 개 생성되고 이를 관리할 수 있는 handler 가 GLuint vao
에 담긴다.
void glBindVertexArray(GLuint array)
는 VAO를 bind 하는 것으로, bind 한 다는 것은 handler 값으로 전달 받은 VAO를 사용하겠다는 것을 의미한다. 따라서 glBindVertexArray(vao)
를 실행하면 앞서 우리가 GPU에 생성한 VAO를 handler를 통해 사용할 수 있다.
앞서 생성하고 bind 한 VAO에 VBO를 생성하고 bind 한다. 이때 VAO와 마찬가지로 handler를 이용하여 bind 한다.
// 2.2. Create a Vertex Buffer and bind the buffer
GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
void glGenBuffers(GLsize n, GLuint * buffers)
는 현재 bind 된 VAO 안에 VBO를 n
개 생성하고 이들을 관리할 수 있도록 handler 값을 * buffers
에 담는다. 따라서 glGenBuffers(1, &vbo)
를 실행하면 VAO 안에 VBO가 한 개 생성되고 이를 관리할 수 있는 handler 가 GLuint vbo
에 담긴다.
void glBindBuffer(GLenum target, GLuint buffer)
는 어떤 종류의 버퍼를 bind 할지를 지정한다. 이때 target
은 특정 버퍼 타입을 알려주고, 그에 맞게 해당 버퍼를 활성화 한다.
Buffer Binding Target | Purpose |
---|---|
GL_ARRAY_BUFFER | Vertex attributes |
GL_ATOMIC_COUNTER_BUFFER | Atomic counter storage |
GL_COPY_READ_BUFFER | Buffer copy source |
GL_COPY_WRITE_BUFFER | Buffer copy destination |
GL_DISPATCH_INDIRECT_BUFFER | Indirect compute dispatch commands |
GL_DRAW_INDIRECT_BUFFER | Indirect command arguments |
GL_ELEMENT_ARRAY_BUFFER | Vertex array indices |
GL_PIXEL_PACK_BUFFER | Pixel read target |
GL_PIXEL_UNPACK_BUFFER | Texture data source |
GL_QUERY_BUFFER | Query result buffer |
GL_SHADER_STORAGE_BUFFER | Read-write storage for shaders |
GL_TEXTURE_BUFFER | Texture data buffer |
GL_TRANSFORM_FEEDBACK_BUFFER | Transform feedback buffer |
GL_UNIFORM_BUFFER | Uniform block storage |
따라서 glBindBuffer(GL_ARRAY_BUFFER, vbo)
를 실행하면 handler 값으로 전달 받은 vbo를 GL_ARRAY_BUFFER
타입으로 사용한 다는 것을 의미한다. 이때 GL_ARRAY_BUFFER
는 버퍼에 vertex attributes를 넣을 것을 의미하며 배열 형태로 저장될 것을 의미한다.
이제 bind한 VBO에 데이터를 전송하여 저장한다.
// 2.3. copy my data to the buffer
glBufferData(GL_ARRAY_BUFFER, sizeof(points), points, GL_STATIC_DRAW);
void glBufferData(GLenum target, GLsizeiptr size, const void * data, GLenum usage)
는 앞서 bind 한 VBO에 CPU에 저장되어 있던 vertex data 를 GPU로 전송하여 저장한다.
GLenum target
는 어떤 종류의 버퍼에 데이터를 넣을 것인지 명시한다.GLsizeiptr size
는 데이터 크기를 바이트 단위로 지정한다.const void * data
는 실제 전송할 데이터에 대한 포인터 이다.GLenum usage
는 버퍼가 어떻게 사용될 것 인지 대한 정보(예상)를 전달한다.GL_STREAM_DRAW, GL_STREAM_READ, GL_STREAM_COPY, GL_STATIC_DRAW, GL_STATIC_READ, GL_STATIC_COPY, GL_DYNAMIC_DRAW, GL_DYNAMIC_READ, GL_DYNAMIC_COPY
이제 Shader 들을 load 하고 handler를 통해 사용을 한다.
// 3. Load Shaders
GLuint program;
program = InitShader("vshader.glsl", "fhsader.glsl");
glUseProgram(program);
현재 shaderprogram
은 앞서 bind 한 VAO를 사용한다.
Shader 는 별도의 프로그램을 GPU에서 모든 vertex data 에 대해 각각 실행된다. 이때 Shader 는 GPU에 전달된 VBO의 구조를 알지 못하기 때문에 data의 구조를 전달해 줘야 한다.
// 4. Connect Data with Shader
GLuint vPosition = glGetAttribLocation(program, "vPosition");
glEnableVertexAttribArray(vPosition);
glVertexAttribPointer(vPosition, 2, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
GLuint glGetAttribLocation(GLuint program, )const GLchar *name)
은 vertex shader 에서 사용하는(in 으로 넣은) data를 handler로 연결한다. 때문에 glGetAttribLocation(program, "vPosition")
을 실행하면 vshader.glsl
의 in vPosition
을 사용할 수 있는 handler를 반환한다.
void glEnableVertexAttribArray(GLuint index)
는 handler 를 받아 해당 특정 속성(attribute)을 활성화 한다.
void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void * pointer)
는 정점 속성 배열을 설정한다. 이를 통해 shader가 GLuint index
의 구조를 알 수 있도록 한다. 따라서 glVertexAttribPointer(vPosition, 2, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0))
을 실행하면 handler로 권한을 받은 shader의vPosition
의 구조가 GL_FLOAT
2개의 숫자로 이루어져 있고, stride 가 0이고 VBO의 0번 부터 시작함을 shader가 알 수 있도록 한다.
[[Retained Mode Graphics|이전]] 의 display
Callback 함수와 달리 GPU Array에 VBO 형태로 vertex data를 전송하였기 때문에 해당 VBO를 그려줄 것을 요청한다.
void display() {
glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_POINTS, 0, NUM_POINTS);
glFlush();
}
void glDrawArrays(GLenum mode, GLint first, GLsizei count)
는 VAO에 저장된 VBO(vertex data)를GLenum mode
형태로 GLint first
부터 GLsizei count 까지 렌더링 한다.
main
함수에서는 display
Callback 함수를 등록하기 전에 반드시 GPU에 vertex data를 먼저 보내줘야 한다. 따라서 init
함수를 display
Callback 함수를 등록하기 전에 실행해야 한다.
int main(int argc, char** argv) {
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA);
glutInitWindowSize(800, 800);
glutCreateWindow("Hello GL");
glewExperimental = true;
glewInit();
// Initialize vertex data and send to GPU, connect shaders
init();
// Sets display callback for the current window
glutDisplayFunc(display);
glutMainLoop();
return 0;
}
vshader.glsl
은 vertex shader로 vec4 gl_Position
을 out 하여 vertex의 position data 를 파이프라인에 전달한다.
#version 330
in vec4 vPosition;
out vec4 pos;
void main() {
gl_Position = vPosition;
gl_Position.x = vPosition.x / 2;
gl_Position.y = vPosition.y / 2;
pos = vPosition;
}
fshader.glsl
은 fragment shader로 vec4 fColor
를 out
하여 vertex의 color data를 파이프라인에 전달한다.
#version 330
in vec4 pos;
out vec4 fColor;
void main() {
float r = sqrt(pos.x * pos.x + pos.y * pos.y);
// R G B A(투명도)
fColor = vec4(r, r, r, 1.0);
}
Shader에 대한 자세한 설명은 다음 장을 보자.