Unifox 3D Viewer 프로젝트 보고서

팀원

  • 선린인터넷고등학교 정보보호과, Unifox 10기 조강준
  • 선린인터넷고등학교 소프트웨어과, Unifox 10기 나정휘

제 1장 프로그램 개요

1. 개발 환경
윈도우10 노트북
Visual Studio 2017
C언어

2. 실행 환경
윈도우10 노트북
CMD에서 명령어로 실행 & VS에서 빌드 후 실행

3. 프로그램의 기능
정육면체와 같은 입체 도형을 생성한 뒤, 평행이동 혹은 회전을 해서 보여주는 3D 도형 Viewer 와 같은 역할을 한다.

제 2장 제작 과정

1. 프로젝트 준비
2018년 4월 6일에 팀 결성을 한 뒤, 프로젝트 주제를 3D 출력으로 정했다.
2일 뒤, 각자 자료 조사를 하면서 조강준, 나정휘는 시야 구현과 기초적인 자료구조 2가지를 각각 하나씩 맡아서 제작하기 시작하였고, 그와 동시에 github에 private repository를 생성해 코드를 관리하였다.

2. 도형 출력과 이동
4월 9일에 조강준이 시야 구현과 도형의 평행 이동까지 구현한 후 깃허브에 업로드 하였고, 그 코드를 나정휘가 넘겨 받아 프로그램이 미리 구현한 자료구조를 이용하도록 수정, 최적화를 해서 깃허브에 업로드했다.

3. 개발 일시정지
4월 10일을 기점으로 개발자 두 명 모두 학교 중간고사 공부에 전념하기 위해 개발을 일시 중단을 했고, 시험이 끝난 후 바로 회전 구현에 돌입하였다. 그러나 회전 구현을 하면서 생각치 못했던 오류가 발생되어서 개발이 느려졌다.

4. 개발의 침체기
도형의 회전을 구현하면서 전혀 생각치도 못하고, 중요하다고 생각하지도 않았던 것 하나 때문에 회전 뿐만 아니라 프로젝트 전체 진행이 느려졌다. 우리가 적용한 공식은 회전의 중심이 (0, 0, 0) 이지만, 진행하던 프로젝트에서 도형의 중심은 (150, 150, 150)으로 지정이 되어 있어서 다시 수정하는데 꽤 많은 시간을 할애했던 것 같다. 결국, 도형 생성시에는 (0, 0, 0) 기준으로 생성을 한 뒤, map에 넣을 때 다시 (150, 150, 150)을 더해주는 방식으로 해결하였다.

5. 프로젝트 완료
계속 카카오톡과 깃허브 만을 이용하여 작업하다가 3일 간 방과 후에 만나서 같이 작업한 결과, 침체기를 극복해내고 회전 기능을 완성을 하게 된다. 그 후 코드 정리, 최적화를 거쳐 최종적으로 프로젝트를 완료했다.

제 3장 프로그램의 구조

1. 기초적인 자료구조의 구현

가. POS 자료형
가장 처음에 생각했던 방법은 각각의 점(픽셀)을 3차원 배열의 각 원소와 1:1 대응하는 방식으로 구현했었다. 그러나 메모리 점유율이 높아서 x, y, z 좌표를 저장할 수 있는 POS 자료형을 만들어서 각 픽셀을 관리한다.

1
2
3
typedef struct _pos {
    int x, y, z;
}POS;

나. VECTOR 컨테이너
POS 구조체에 저장된 각 픽셀을 어떻게 관리할지 생각하다가 C++의 VECTOR 컨테이너가 떠올랐다. 그러나 오직 C 만을 이용해서 만들어야 했기에 STL을 쓰지 못하고 직접 구현하였다. 최대한 C++의 VECTOR컨테이너와 원리와 동작 방식을 비슷하게 만들기 위해 노력하였다.
VECTOR라는 구조체에는 3개의 멤버가 있다. 직접 저장이 되는 POS* 형 arr와 현재 저장이 되어 있는 원소의 개수(C++ STL에서는 vector.size()에 해당한다.) cnt, 그리고 현재 할당이 되어 있는 칸의 개수(C++ STL에서는 vector.capacity에 해당한다.)를 나타내는 size로 구성되어 있다.
간단한 작동 방법은 다음과 같다.
처음에 VECTOR를 생성할 때에는 초기화와 동시에 5칸을 할당해준다.
push를 할 때 cnt가 size보다 크거나 같으면 배열에서 남는 칸이 없기 때문에 배열 전체의 크기(size)를 2배로 늘려주고 push 작업을 수행한다.
이러한 작업을 수행할 때는 메모리를 동적으로 할당해주어야 한다.
VECTOR를 처음 생성할 때는 malloc함수를, 크기를 늘려줄 때는 realloc함수를 사용한다.
코드로 간단하게 구현하면 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
typedef struct _vector {
    POS *arr;
    int cnt, size;
} VECTOR;

VECTOR newVector() {
    VECTOR ret;
    ret.arr = (POS*)malloc(sizeof(POS) * 5);
    ret.cnt = 0;
    ret.size = 0;
    return ret;
}

void deleteVector(VECTOR* a) {
    free(a->arr);
}

void push(VECTOR *a, POS *value) {
    if (a->cnt >= a->size) {
        if (a->size == 0) a->size = 5;
        else a->size *= 2;
        a->arr = (POS*)realloc(a->arr, a->size * sizeof(POS));
    }
    a->arr[a->cnt].x = value->x;
    a->arr[a->cnt].y = value->y;
    a->arr[a->cnt].z = value->z;
    a->cnt++;
}

int length(VECTOR *a) {
    return a->cnt;
}

POS pop(VECTOR *a) {
    POS ret;
    ret.x = a->arr[length(a) - 1].x;
    ret.y = a->arr[length(a) - 1].y;
    ret.z = a->arr[length(a) - 1].z;
    a->cnt--;
    return ret;
}

POS* begin(VECTOR *a) {
    return a->arr;
}

POS* end(VECTOR *a) {
    return a->arr + a->cnt;
}

다. SHAPE 자료형
SHAPE 자료형은 여러 개의 도형을 동시에 다루기 위해 제작한 구조체이다.
SHAPE 구조체의 멤버는 VECTOR형 arr뿐이다. 단지 VECTOR형 arr에 저장된 것이 “도형 이다”라는 것을 명시하기 위해 만들어졌다.

1
2
3
4
5
6
7
8
9
10
typedef struct _shape {
    VECTOR arr;
}SHAPE;

SHAPE newShape() {
    SHAPE ret;
    ret.arr = newVector();
    return ret;
}

2. 시야의 구현

가. 기본 개념
시야 구현의 기본적인 개념은 관찰자(파란 원)과 물체(정육면체) 사이에 가상의 스크린을 만든 뒤, 관찰자와 물체를 선으로 잇고, 그 선과 스크린의 교점을 찾는 것이 기본적인 개념이다.


나. 계산 식
먼저 물체와 관찰자, 그리고 스크린을 위에서 바라본 상태를 보자.


물체가 있는 (a, b)와 관찰자가 있는 (0, 0)을 이은 선이 스크린과 (x, s)에서 교차한다. (단, 이 때 s는 관찰자와 스크린 사이의 거리이다.)
서로 닮은 두 직각삼각형을 이용하여 x를 구하자면,
a:b = x:s
bx = as
x = as/b
같은 방법을 이용해 y좌표를 구하면 최종적으로 3차원 상의 점이 우리가 눈으로 보는 것과 똑같이 출력하고, 그 점의 좌표는 (xs/z, ys/z) 라는 점까지 도출해낼 수 있다.

3. 더블 버퍼링

각 프레임마다 cls명령을 쓰고 다시 printf로 출력하면 필연적으로 깜빡임 이 나타난다. 이러한 현상을 막기 위해서 더블 버퍼링을 쓴다. 말 그대로 버퍼를 2개 사용한다. 화면에 보여지는 프론트 버퍼와 실제로 그림이 찍히는 백 버퍼 총 2가지의 버퍼를 사용하는데, 일단 모든 그림을 백 버퍼에 저장시키고, 마지막에 프론트 버퍼로 복사하는 방식이다.

4. 도형의 이동

가. 대략적인 구현 방법
도형을 이동시키는 것은 간단하다. X축 방향으로 1만큼 가고 싶다면 모든 픽셀의 x좌표를 1씩 증가시키면 되는 것 이다. 이동 시키는 방법은 매우 간단하지만 이를 구현하기 위해 두 가지의 구현 방법을 도전해보았고, 미래에 기능을 더 추가할 때 사용하기 편한 것으로 선택을 했다.

나. 구현 방법의 종류
A. 원본 도형 이동
처음에는 처음에 생성한 도형의 각 점에 대해 모두 이동 연산을 하는 방식으로 하였다. 편할 뿐만 아니라 추가 메모리도 필요하지 않기 때문에 처음에는 이 방법을 사용하였다. 하지만, 도형의 회전과 같은 새로운 기능을 추가할 때 불편함을 느꼈고, 개선된 방법을 바로 다음에 설명할 것 이다.

B. 임시 도형 이동
원본 도형(SHAPE)에는 처음에 생성했을 때의 점의 상태만 기억하고 값은 바뀌지 않는다. 대신, 매 프레임마다 임시 도형을 만든 뒤, 원본 도형의 데이터를 임시 도형에 복사한 뒤에 임시 도형을 이동, 회전 시키는 방식을 사용해 원본 도형의 상태를 그대로 보존할 수 있게 되었다.

5. 도형의 회전

가. 육십분법과 호도법
도형을 회전시키는 데에는 삼각함수가 필요하다. math.h 에 삼각함수가 내장되어 있다. 하지만 math.h에 내장되어 있는 삼각함수들은 우리가 흔히 쓰는 육심분법이 아니라 호도법을 사용하기 때문에 육십분법으로 나타낸 값을 호도법으로 바꿔줄 필요가 있다. 그러므로 다음과 같은 함수를 선언해서 쓴다.

1
2
3
4
double deg2rad(double angle) {
    return angle / 180 * 3.14;
}

나. 회전 공식

원래 있던 지점을 $P(x, y)$로 하고, x축과의 각도를 $\alpha$라 하자.
회전된 지점은 $P’(x’, y’)$로 하고, 원점과 점p를 이은 선과의 각도를 $\theta$라 하자.
선분OP의 길이는 $\sqrt{x^2+ y^2}$이고, $\cos \alpha$는 $x/OP$ 이다.
$x = OP \cos \alpha \ x = \sqrt{x^2 + y^2}\cos \alpha \ \cos \alpha = x / \sqrt{x^2 + y^2}$
같은 방법으로,
$\sin \alpha = y/OP \ y = OP \sin \alpha \ y = \sqrt{x^2 + y^2} \sin \alpha \ \sin \alpha = y / \sqrt{x^2 + y^2}$
가 된다.

이제 $P(x, y)$를 $P’(x’, y’)$로 $+\theta$만큼 회전시키자.
$x’ = \sqrt{x^2 + y^2} \cos (\alpha + \theta) \ y’ = \sqrt{x^2 + y^2} \cos (\alpha + \theta)$

식을 전개해서 잘 정리해주면 $(x’, y’) = (x \cos \theta - y \sin \theta, x \sin \theta + y \cos \theta)$가 된다.

6. 도형 이동 처리의 흐름

도형 이동 처리의 전체적인 과정은 다음과 같다.