이번 실습에선 객체 지향을 이용해 MyColorCube Class
를 만들고 회전시켜보자.
MyColorCube Class
에 필요한 멤버 변수는 아래와 같다.
#include <vgl.h>
#include <vec.h>
class MyColorCube {
public:
int NUM_VERTEX;
vec4* points;
vec4* colors;
GLuint vao;
GLuint vbo;
}
생성자에서 멤버 변수를 다음과 같이 초기화 한다. NUM_VERTEX
는 6면체의 모든 면을 삼각형으로 쪼개었을 때 삼각형의 꼭짓점 수 이다.
모든 면을 삼각형으로 쪼개는 이유는 삼각형이 평면을 만들 수 있는 가장 최소 단위이자 세 점이 한 직선 위에 있지 않는 경우 확실하게 만들 수 있는 유일한 경우이기 때문이다. 때문에 OpenGL에서는 기본적으로 vertex를 이용하여 면을 만들 때 삼각형으로 나누어 저장한다.
MyColorCube() {
NUM_VERTEX = 6 * 2 * 3; // 6면체의 면을 삼각형으로 나누었을 때 꼭짓점의 수
points = new vec4[NUM_VERTEX];
colors = new vec4[NUM_VERTEX];
}
void init
init
함수에서는 6면체의 초기 데이터를 생성하고 이를 GPU에 저장한다. 또한 이미 GPU로 vertex data를 넘겼기 때문에 CPU에는 더 이상 해당 데이터가 필요하지 않다. 때문에 delete
로 메모리를 할당 해제 한다.
여기선 glBufferData
함수로 2 * sizeof(vec4) * NUM_VERTEX
만큼의 메모리 공간을 GPU에 할당 한 후 glBufferSubData
를 이용해 points
데이터와 colors
데이터를 각각 보냈다. 따라서 메모리에는 points
배열 뒤에 바로 colors
배열이 붙는 식으로 저장된다.
void init() {
// 1. Create data in CPU
generate_cube_data();
// 2. Send data to GPU
// 2.1. Create a Vertex Array and bind the buffer
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
// 2.2. Create a Vertex Buffer and bind the buffer
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
// 2.3. Copy my data to the buffer
glBufferData(GL_ARRAY_BUFFER, 2 * sizeof(vec4) * NUM_VERTEX, NULL, GL_STATIC_DRAW);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vec4) * NUM_VERTEX, points);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(vec4) * NUM_VERTEX, sizeof(vec4) * NUM_VERTEX, colors);
// 3. delete data in CPU
delete points;
delete cololrs;
}
void draw
draw
함수는 handler 인program
을 받아 vao
에 저장된 데이터를 렌더링 하도록 지시한다. 이때 connectShader
는
void draw(GLuint program) {
glBindVertexArray(vao);
connectShader(program);
glDrawArrays(GL_TRIANGLES, 0, NUM_VERTEX);
}
void connectShader
connectShader
함수는 shader program의 handler 인 program
을 받아 정점 속성(vPosition, vColor
) 을 설정한다. 이때 shader program은 반드시 vPosition
과 vColor
을 갖고 있어야 한다.
glVertexAttribPointer
를 이용해 각 정점 속성을 설정하는데,vPosition
과 vColor
는 각각 4개의 GL_FLOAT
으로 이루어져 있다.
vPosition
은 vbo
의 0번(BUFFER_OFFSET(0)
) 부터 stride 없이(0
) 데이터를 정규화하지 않은(GL_FALSE
) 상태로 저장되어 있음을 의미한다.
vColor
는 vbo
에 vPosition
다음(BUFFER_OFFSET(sizeof(vec4) * NUM_VERTEX)
) 부터 stride 없이(0
) 데이터를 정규화하지 않은(GL_FALSE
) 상태로 저장되어 있음을 의미한다.
void connectShader(GLuint program) {
GLuint vPosition = glGetAttribLocation(program, "vPosition");
glEnableVertexArrtibArray(vPosition);
glVertexAttribPointer(vPosition, 4, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
GLuint vColor = glGetAttribLocation(program, "vColor");
glEnableVertexAttribArray(vColor);
glVertexAttribPointer(vColor, 4, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(sizeof(vec4) * NUM_VERTEX));
}
void set_rectangle
set_rectangle
함수는 6면체의 6면 중 한 면을 입력 받아 두 개의 삼각형으로 나누어 각각 points
와 colors
에 vertex data 를 저장한다.
이때 두 삼각형으로 나눌 때 vertex data 는 시계 반대 방향으로 저장하는 것이 관례이다. 그 이유는 앞면과 뒷면을 구분하기 위해서로, 이를 통해 면 판별(Face Culling) 이라는 최적화 기법을 사용할 수 있다.
void set_rectangle(int a, int b, int c, int d, vec4 vertex_pos[], vec4 vertex_color[]) {
static int vindex = 0;
points[vindex] = vertex_pos[a]; colors[vindex] = vertex_color[a]; vindex++;
points[vindex] = vertex_pos[b]; colors[vindex] = vertex_color[b]; vindex++;
points[vindex] = vertex_pos[c]; colors[vindex] = vertex_color[c]; vindex++;
points[vindex] = vertex_pos[c]; colors[vindex] = vertex_color[c]; vindex++;
points[vindex] = vertex_pos[d]; colors[vindex] = vertex_color[d]; vindex++;
points[vindex] = vertex_pos[a]; colors[vindex] = vertex_color[a]; vindex++;
}
void generate_cube_data
generate_cube_data
함수에선 6면체를 만들기 위해 6면체의 각 꼭짓점의 데이터의 position과 color 값을 각각 vertex_pos[], vertex_color[]
에 저장한다. 그 후 set_rectangle
함수를 이용해 모든 면들을 삼각형으로 쪼갠다.
void generate_cube_data() {
vec4 vertex_pos[] = {
vec4(-0.5, -0.5, 0.5, 1),
vec4(0.5, -0.5, 0.5, 1),
vec4(0.5, 0.5, 0.5, 1),
vec4(-0.5, 0.5, 0.5, 1),
vec4(-0.5, -0.5, -0.5, 1),
vec4(0.5, -0.5, -0.5, 1),
vec4(0.5, 0.5, -0.5, 1),
vec4(-0.5, 0.5, -0.5, 1),
};
vec4 vertex_color[] = {
vec4(1, 0, 0, 1),
vec4(0, 1, 0, 1),
vec4(0, 0, 1, 1),
vec4(1, 1, 0, 1),
vec4(0, 1, 1, 1),
vec4(1, 0, 1, 1),
vec4(1, 1, 1, 1),
vec4(0, 0, 0, 1),
};
set_rectangle(0, 3, 2, 1, vertex_pos, vertex_color);
set_rectangle(0, 4, 7, 3, vertex_pos, vertex_color);
set_rectangle(3, 7, 6, 2, vertex_pos, vertex_color);
set_rectangle(2, 6, 5, 1, vertex_pos, vertex_color);
set_rectangle(1, 5, 4, 0, vertex_pos, vertex_color);
set_rectangle(4, 5, 6, 7, vertex_pos, vertex_color);
}
Main.cpp
에서 사용할 전역 변수는 아래와 같다.
#include <vgl.h>
#include <vec.h>
#include <InitShader.h>
#include "MyColorCube.h"
GLuint program;
MyColorCube cube;
float myTime = 0;
void myInit
myInit
함수는 cube
를 초기화(vertex data 를 GPU 에 저장)를 하고 shader를 로드한다.
void myInit() {
cube.init();
// 4. load shaders
program = InitShader("vshader.glsl", "fshader.glsl");
glUseProgram(program);
}
void display
display
함수는 Callback 함수로 main
함수에서 glutDisplayFunc
함수로 등록된다.
display
함수는 shader에 uniform
으로 입력되는 uTime
을 myTime
과 연결하고 cube.draw(program)
으로 cube
를 렌더링 한다.
void display() {
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(program);
printf("Time: %f \n", myTime);
GLuint uTime = glGetUniformLocation(program, "uTime");
glUniform1f(uTime, myTime);
cube.draw(program);
glFlush();
}
void idle
idle
함수는 Callback 함수로 main
함수에서 glutIdleFunc
로 등록된다.
idle
함수는 초당 약 30 프레임으로 화면을 다시 렌더링(새로고침) 한다.
Callback에 대한 자세한 설명은 이 장을 확인하자.
void idle() {
myTime += 0.0333f;
Sleep(33);
glutPostRedisplay();
}
int main
main
함수에선 myInit
을 display
함수를 Callback 함수로 등록하기 전에 먼저 호출하여 MyColorCube cube
를 초기화 한다. 이후 display
함수와 idle
함수를 Callback 함수로 등록한다.
int main(int argc, char** argv) {
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA);
glutInitWindowSize(800, 800);
glutCreateWindow("A Color Cube");
glewExperimental = true;
glewInit();
myInit();
glutDisplayFunc(display);
glutIdleFunc(idle);
glutMainLoop();
return 0;
}
vshader.glsl
는 vertex shader 로 그래픽 파이프라인의 첫 번째 단계이다. 이 단계에서는 3D 객체의 각 정점(vertex)을 처리한다.
vshader.glsl
에선 uniform
으로 입력 받는 uTime
에 따라 x-rotation 과 y-rotation 을 한다.
#version 330
uniform float uTime;
in vec4 vPosition;
in vec4 vColor;
out vec4 color;
out vec4 position;
void main() {
float scale = 1 + sin(uTime);
float ang = uTime*90/180.0f*3.141592f;
mat4 m1 = mat4(1.0f);
// x-rotation
m1[1][1] = cos(ang);
m1[2][1] = -sin(ang);
m1[1][2] = sin(ang);
m1[2][2] = cos(ang);
mat4 m2 = mat4(1.0f);
// z-rotation
/*m[0][0] = cos(ang);
m[1][0] = -sin(ang);
m[0][1] = sin(ang);
m[1][1] = cos(ang);*/
// x-rotation
/*m[1][1] = cos(ang);
m[2][1] = -sin(ang);
m[1][2] = sin(ang);
m[2][2] = cos(ang);*/
// y-rotation
m2[0][0] = cos(ang);
m2[2][0] = sin(ang);
m2[0][2] = -sin(ang);
m2[2][2] = cos(ang);
gl_Position = m2*m1*vPosition;
gl_Position.w = 1.0f;
color = vColor;
position = vPosition;
}
fshader.glsl
은 fragment shader 로 그래픽 파이프라인에서 각 fragment의 색상, 명암, 텍스처링, 조명 등을 계산하는 단계이다.
fshader.glsl
에선 in
으로 받은 color
와 position
에 따라 6면체의 색상을 정한다. 그리고 모서리 부분의 색상은 버린다(discard
).
#version 330
in vec4 color;
in vec4 position;
out vec4 fColor;
void main() {
fColor = color;
if (abs(position.x) > 0.45 &&
abs(position.y) > 0.45)
discard;
if (abs(position.x) > 0.45 &&
abs(position.z) > 0.45)
discard;
if (abs(position.y) > 0.45 &&
abs(position.z) > 0.45)
discard;
}
Shader에 대한 자세한 설명은 다음 장을 보자.
출력 결과를 보면 육면체가 제대로 표현되지 않는 것을 알 수 있다. 이는 Z-Buffer(Depth-Buffer)의 부재로 인해 발생한 결과로 이 장을 확인하자.