컴퓨터 그래픽스 프로젝트

nomis·2023년 1월 12일
1

computer graphics

목록 보기
1/1
post-thumbnail

opengl의 각 요소를 활용해서 3차원 제품 카탈로그를 만드는 프로젝트를 진행했다.

STEP 0. introduction

내가 선정한 제품은 애플워치이다. 애플워치는 애플에서 제작한 스마트 워치로 제품의 구조는 크게 케이스(본체), 밴드로 구성되어있다. 케이스와 밴드는 쉽게 탈부착이 가능하고 애플에서 공식으로 제작한 밴드 외에도 커스텀해서 여러 밴드가 유통되고 있기 때문에 시계줄 컬렉션을 만들기에 매우 좋은 구조로 되어있다.

애플 공식홈페이지에서는 고객이 제품을 구매하기 전 자신이 구매할 밴드와 케이스(본체)가 잘 어울리는지 미리 볼 수 있도록 시각화해주는 기능이 있다. 이에 영감을 받아 애플 공식홈페이지처럼 case와 strap의 색상을 선택해보면서 그 조합을 확인하고 카메라 zoom in/out, 시점변경 등 제품을 다각도에서 자세히 볼 수 있도록 구현하고자 한다. 또한 opengl의 다양한 요소를 활용해서 case와 strap을 분리, 제품 외 배경 추가, siri를 부르면 응답하는 모드를 구현해보도록 하겠다.

개발 환경
opengl, blender, opencv

STEP 1. blender

blender를 이용하여 애플워치를 모델링했다.

디테일

참고한 영상 : https://www.youtube.com/watch?v=TSP089hXMSs

STEP 2. blender to opengl

blender에서 모델링한 object를 opengl로 불러와야한다.
blender에서 모델링한 object를 .obj 파일로 export할 수 있는데 내보내진 obj 파일은 다음과 같은 형식이다. (메모장으로 열었을 경우 다음과 같은 형식이고 다른 프로그램으로 열 경우 3D 형태로 object가 보여지는 경우도 있다.)

obj 파일은 크게 o, v, vt, vn, f로 이루어져있다. 순서대로 각각 object 이름, vertex 좌표, texture 좌표, 법선 벡터, 면을 의미한다.
해당하는 요소들을 담을 class를 만들고 getline함수로 한 줄씩 읽어와서 저장한다.
이후 display() 함수에서 이 정보들을 가지고 obj를 출력한다.
이 과정을 거치면 blender에서 모델링한 object를 opengl에서도 출력할 수 있다.
자세한 방법은 아래의 참고한 영상에서 확인할 수 있다.

참고한 영상 : https://www.youtube.com/watch?v=HrRAjQQiKAQ

STEP 3. texture mapping

blender에서 모델링한 object가 opengl로 넘어올 때 형태만 넘어오고 texture는 넘어오지 않는다.
그러므로 따로 texture를 입혀줘야 하는데 이는 opencv를 이용해서 texture를 불러왔다.
불러온 texture는 case에서 silver, black, rose gold, gold가 있으며 strap에서 red, yellow, blue, ivory가 있다. 무료 texture 이미지는 여기에서 다운받을 수 있다.

main 함수 내부에서 obj 파일과 texture 이미지를 읽어오는 코드는 아래와 같다.
obj 파일을 불러올 때는 이후에 스트랩을 본체와 분리시키는 기능을 구현할 때 다른 객체로 불러와야 분리가 가능하기 때문에 본체와 스트랩을 나누어서 불러왔다.

image[0] = imread("/*이미지 경로*/", IMREAD_COLOR);
cvtColor(image[0], image[0], COLOR_BGR2RGB);
// ... (생략 : 아래 image[1], image[2]도 같은 방법으로 불러오기)

string filepath1 = "/*obj 파일 경로(본체)*/";
ifstream fin1(filepath1);
body.loadObj(fin1);
string filepath2 = "/*obj 파일 경로(스트랩)*/";
ifstream fin2(filepath2);
strap.loadObj(fin2);
fin1.close();
fin2.close();

obj를 불러왔다면 display_body()와 display_strap()에서 case와 strap을 구성하는 object들마다 각각 texture를 매핑해준다.
예를 들어 아래의 코드를 보면 case의 경우 obj 파일에서 o가 0인 경우, 애플워치의 옆 테두리 케이스이므로 silver metal 이미지를 매핑해준다. o가 1과 2인 경우엔 애플워치의 화면과 뒷 부분에 해당하므로 black glass 이미지를 매핑해준다.

int idx;
for (int o = 0; o < strap.objs.size(); o++) {
	if (o == 0) {
		idx = 0;
	}
	else {
		idx = 2;
	}
	glBindTexture(GL_TEXTURE_2D, tex_ids[idx]);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, image[idx].cols, image[idx].rows, 0, GL_RGB, GL_UNSIGNED_BYTE, image[idx].data);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

o case_Cube.002
... 이 object의 v, vt, vn, f 좌표들
o display_Cube.003
... 이 object의 v, vt, vn, f 좌표들
o back_Cube.005
... 이 object의 v, vt, vn, f 좌표들

STEP 4. 구현한 기능들

STEP 4-1. strap과 case 색상 변경

스트랩과 케이스의 색상을 바꿔주는 기능은 opengl의 submenu 기능을 활용했다. 전역변수로 선언했던 image 배열에 다른 이미지 경로를 넣어주어 선택 시 다른 texture가 매핑될 수 있도록 구현했다.

void sub_menu_function1(int option) {
	switch (option) {
	case 1:
		image[0] = imread("/*이미지 경로*/", IMREAD_COLOR);
		cvtColor(image[0], image[0], COLOR_BGR2RGB);
		break;
	case 2:
		image[0] = imread("/*이미지 경로*/", IMREAD_COLOR);
		cvtColor(image[0], image[0], COLOR_BGR2RGB);
		break;
	... 생략
	}
	glutPostRedisplay();   
}

실행 결과는 다음과 같다.

STEP 4-2. 시점 이동

실행화면에서 애플워치는 3차원 시점에서 표현된다.
이 시점을 다양한 시선 방향에서 볼 수 있고 zoom in/out이 가능하도록 gluLookAt() 함수의 인자를 넣어보자.

먼저 자연스러운 시점 변경을 위해 gluLookAt() 함수의 앞의 세 인자인 카메라 위치를 구 좌표계로 표현할 것이다. 우리가 통상적으로 알고있는 좌표계에서 3차원 좌표계에서 구 좌표계로 변환하는 공식은 아래와 같다.

하지만 opengl에서는 좌표계가 다음과 같이 설정되어있다.

이러한 차이 때문에 3차원 좌표계에서 구 좌표계로 변환하는 공식 또한 변화가 필요한데 코드 단에서 설명해보도록 하겠다.
먼저 gluLookAt() 함수의 인자에서 필요한 값들을 전역변수로 선언해준다.

double theta = 45, phi = 0;
int radius = 18;
double upVector = cos(phi * PI / 180);
double eyex, eyey, eyez;

sin, cos 함수를 사용할 때는 각도를 라디안으로 변경해주어야 한다. 전역변수로 PI를 선언해주고 각도를 라디안으로 변경해서 sin, cos 함수의 인자로 넣어준다. 이후 opengl 좌표계에서 구 좌표계로 변환하는 공식에 맞게 수식을 작성하고 이를 카메라 위치 변수에 넣어주었다.

void display() {
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	eyex = radius * sin(theta * PI / 180) * cos(phi * PI / 180);
	eyey = radius * sin(phi * PI / 180);
	eyez = radius * cos(theta * PI / 180) * cos(phi * PI / 180);
	upVector = cos(phi * PI / 180);
	gluLookAt(eyex, eyey, eyez, 0, 0, 0, 0, upVector, 0);
	
    ... 생략
}

이제 다양한 시점에서 물체를 바라볼 수 있도록 키보드 방향키 입력으로 시점 변경을 해보자. specialKeyboard()에서는 입력받은 key를 switch문으로 분류해 각 case마다 theta와 phi 값을 변경해주었다. 이 때 GLUTKEY@@@@는 opengl에서 키보드 방향키를 의미하는 변수이다. case문 내에서 if문은 각도가 360도가 넘을 경우 예외를 처리하기 위한 부분이다.

void specialKeyboard(int key, int X, int Y)
{
	switch (key) {
	case GLUT_KEY_LEFT:     
		theta -= 3;
		if (theta <= -360) theta = 0.0;
		break;
	case GLUT_KEY_RIGHT:     
		theta += 3;
		if (theta >= 360) theta = 0.0;
		break;
	case GLUT_KEY_UP:      
		phi += 3;
		if (abs((int)phi) % 360 == 0) phi = 0.0;
		break;
	case GLUT_KEY_DOWN:  
		phi -= 3;
		if (abs((int)phi) % 360 == 0) phi = 0.0;
		break;
	default:
		printf("%d is pressed\n", key);
		break;
	}
	glutPostRedisplay();
}

또한 mouseWheel() 함수에서는 마우스 휠의 입력을 받아 전역변수 radius의 값을 변경해준다. 이를 통해 camera zoom in/out과 같은 효과를 만들 수 있다.

void mouseWheel(int but, int dir, int x, int y) {
	if (dir > 0) {
		if (radius > 2) {
			radius--;
		}
	}
	else {
		if (radius < 100) {
			radius++;
		}
	}
	glutPostRedisplay();
}

위에서 정의한 specialkeyboard()와 mouseWheel() 함수는 모두 main 함수에서 콜백 함수의 인자로 사용된다.

STEP 4-3. 스트랩 이동

스트랩을 이동할 수 있는 방법은 2가지이다. 두 방법 모두 전역변수 strap_cord를 선언하고 display() 함수에서 display_strap()을 호출하기 전에 glTranslatef(strap_cord, 0, 0) 으로 스트랩의 위치를 바꿔준다.
첫 번째 방법은 키보드 입력 'r'과 'l'로 각각 오른쪽, 왼쪽으로 스트랩을 이동할 수 있다. 이는 키보드 콜백 함수인 glutKeyboardFunc()의 인자로 들어가는 keyboard() 함수에서 case문으로 strap_cord의 값을 변경해주었다.

void keyboard(unsigned char key, int x, int y) {
	switch (key) {
	... 생략
	case 'r' :
		strap_cord += 1;
		break;
	case 'l':
		strap_cord -= 1;
		break;
	... 생략
	}
	glutPostRedisplay();
}

두 번째 방법은 마우스 입력으로 스트랩을 이동하는 것이다. 마우스 좌클릭 후 이동한 만큼의 x 좌표 차이를 전역변수 strap_cord에 저장해 스트랩의 위치를 변경하도록 했다.

void mouse(int button, int state, int x, int y) {
	if (button == GLUT_LEFT_BUTTON && state == GLUT_DOWN) 	  {
		g_nX = x;   
		strap_cord = (x / (double)WINDOW_WIDTH) * 4 - 1;
	}
}


void motion(int x, int y) {
	g_nX = x;  
	strap_cord = (x / (double)WINDOW_WIDTH) * 4 - 1;
}

STEP 4-4. environment mapping

모델링한 애플워치 외에도 배경 texture도 mapping 해서 좀 더 사실적인 결과를 만들어보자.
배경을 만드는 방법은 여러가지가 있지만 그 중에서도 대표적이고 간단한 방법인 skybox를 사용해서 배경을 만들어보겠다. skybox란 전체 scene을 둘러싸고 주변 환경 6개의 이미지를 가지고 있는 큰 큐브이다. 즉, 큰 정육면체를 그리고 그 안쪽에 texture 이미지를 매핑해주면 되는 것이다. skybox 이미지들은 일반적으로 다음과 같은 패턴을 취하고 이러한 이미지들은 구글링을 통해 쉽게 구할 수 있다.

위에서 보여준 실행결과들에서 배경이 모든 방향에서 잘 매핑된 모습을 볼 수 있다.

STEP 4-5. 화면에 선택할 수 있는 색상 나열하기

서론에서 보여준 애플 공식홈페이지에서는 선택할 수 있는 색상을 화면에 나열하고 컬러를 선택 시 해당 색상으로 변하는 기능이 있다. 이와 유사하게 만들기 위해 먼저 화면에 선택할 수 있는 색상을 나열하는 기능을 구현해보았다.
먼저 색상을 나타내는 구를 그리기 위한 DrawSphere()를 정의해주었다. 구는 2D로 나타내기 위해 gluOrtho2D() 함수를 사용했고 화면 우측 상단에 색상들의 RGB 값을 입력해 구를 그려주었다.
또한 스트랩과 케이스의 색상을 모두 나열할 것이기 때문에 이를 구분하기 위한 text도 출력해야했다. text 역시 2D로 나타내기 위해 gluOrtho2D() 함수를 사용했고 위치와 출력하고자 하는 text를 인자로 받아서 화면에 출력한다.

void draw_string(void* font, const char* str, float x_position, float y_position, float red, float green, float blue) {
	glPushAttrib(GL_LIGHTING_BIT);  
	glDisable(GL_LIGHTING);
	glMatrixMode(GL_PROJECTION); 
	glPushMatrix();
	glLoadIdentity();
	gluOrtho2D(-10, 10, -10, 10);
	glMatrixMode(GL_MODELVIEW);
	glPushMatrix();
	glLoadIdentity();
	glColor3f(red, green, blue);
	glRasterPos3f(x_position, y_position, 0);
	for (unsigned int i = 0; i < strlen(str); i++) {
		glutBitmapCharacter(font, str[i]);
	}
	glPopMatrix();
	glMatrixMode(GL_PROJECTION);
	glPopMatrix();
	glMatrixMode(GL_MODELVIEW);
	glPopAttrib();
}


void draw_text(const char* str, float x_position, float y_position) {
	draw_string(GLUT_BITMAP_TIMES_ROMAN_24, str, x_position, y_position, 1, 1, 1);
}

STEP 4-6. siri mode

키보드 's' 입력으로 siri mode로 진입할 수 있다.
siri mode에서는 시점이 애플워치의 화면 앞으로 이동하고 strap은 화면에서 보이지 않는다. 또한 화면에는 "hey siri"라는 문장과 함께 siri logo 이미지가 출력된다. text는 위에서 설명한 draw_string() 함수와 draw_text() 함수를 사용했고 siri logo 이미지는 사각형에 texture를 입혀 애플워치 디스플레이 위치에 올려주었다.
메뉴에서 clear를 누르면 다시 초기화면으로 돌아갈 수 있고 text와 siri logo 이미지는 화면에서 출력되지 않는다.

프로젝트를 마치며..

보완할 점들

  1. 화면에 선택할 수 있는 색상을 나열하는 것까지는 구현했으나 마우스 클릭으로 색상을 변경하는 부분은 시간 관계상 구현하지 못했다.
  2. siri mode에서 좀 더 사용자와 interactive한 기능을 보완하고자 한다. 생각해본 기능은 다음과 같다.
  • text를 직접 입력 받아 시간을 출력한다.
  • siri mode로 선택했을 경우 display object만 불러와 좀 더 현실적인 출력화면을 나타낸다.
  • siri logo를 애니메이션으로 출력한다.
  1. Apple watch 뿐만 아니라 책상, 거치대 등을 blender로 모델링하고 environment map도 apple store의 이미지를 직접 취득해 직접 apple store에서 제품을 구경하는 것과 같은 환경을 구성한다.

느낀점

한 학기동안 컴퓨터 그래픽스를 수강하면서 단편적으로 opengl의 여러 기능을 배웠었는데 이 기능들을 집대성해서 하나의 프로젝트로 만드는 과정에서 배운 것들을 다시 정리하고 내 것 으로 만드는 좋은 경험이었다.
또 원하는 기능을 구현하기 위해 배우지 않았던 다른 부분들도 있었지만 이를 찾아가면서 프로젝트의 완성도를 높이는 과정이 즐거웠다.
그럼에도 역량 부족과 시간의 제한 때문에 원하는 모든 기능을 구현하지는 못했지만 이후에도 더 보완하고 기능을 추가해 더 좋은 결과물을 만들어내고 싶다.

profile
computer vision & machine learning blog

0개의 댓글