[컴퓨터그래픽스] 실습: ColorCube

Serun1017·2024년 10월 23일
0

컴퓨터그래픽스

목록 보기
15/31

이번 실습에선 객체 지향을 이용해 MyColorCube Class 를 만들고 회전시켜보자.

1. MyColorCube.h

Member Variables

MyColorCube Class 에 필요한 멤버 변수는 아래와 같다.

#include <vgl.h>
#include <vec.h>

class MyColorCube {
public:
	int NUM_VERTEX;

	vec4* points;
	vec4* colors;
	GLuint vao;
	GLuint vbo;
}

Constructor

생성자에서 멤버 변수를 다음과 같이 초기화 한다. NUM_VERTEX 는 6면체의 모든 면을 삼각형으로 쪼개었을 때 삼각형의 꼭짓점 수 이다.

모든 면을 삼각형으로 쪼개는 이유는 삼각형이 평면을 만들 수 있는 가장 최소 단위이자 세 점이 한 직선 위에 있지 않는 경우 확실하게 만들 수 있는 유일한 경우이기 때문이다. 때문에 OpenGL에서는 기본적으로 vertex를 이용하여 면을 만들 때 삼각형으로 나누어 저장한다.

MyColorCube() {
	NUM_VERTEX = 6 * 2 * 3; // 6면체의 면을 삼각형으로 나누었을 때 꼭짓점의 수

	points = new vec4[NUM_VERTEX];
	colors = new vec4[NUM_VERTEX];
}

Member Functions

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은 반드시 vPositionvColor 을 갖고 있어야 한다.

glVertexAttribPointer 를 이용해 각 정점 속성을 설정하는데,vPositionvColor 는 각각 4개의 GL_FLOAT 으로 이루어져 있다.

vPositionvbo 의 0번(BUFFER_OFFSET(0)) 부터 stride 없이(0) 데이터를 정규화하지 않은(GL_FALSE) 상태로 저장되어 있음을 의미한다.

vColorvbovPosition 다음(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면 중 한 면을 입력 받아 두 개의 삼각형으로 나누어 각각 pointscolors 에 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++;
	}

ColorCube_set_rectangle.png

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);
}

ColorCube_generate_cube_data.png

2. Main.cpp

Global Variables

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 으로 입력되는 uTimemyTime 과 연결하고 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 함수에선 myInitdisplay 함수를 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;
}

3. vshader.glsl

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;
}

4. fshader.glsl

fshader.glsl 은 fragment shader 로 그래픽 파이프라인에서 각 fragment의 색상, 명암, 텍스처링, 조명 등을 계산하는 단계이다.

fshader.glsl 에선 in 으로 받은 colorposition 에 따라 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에 대한 자세한 설명은 다음 장을 보자.

결과

ColorCube_result.png

출력 결과를 보면 육면체가 제대로 표현되지 않는 것을 알 수 있다. 이는 Z-Buffer(Depth-Buffer)의 부재로 인해 발생한 결과로 이 장을 확인하자.

0개의 댓글