cub3D

DaewoongJeon·2021년 1월 4일
3

42seoul Subject

목록 보기
2/8

1. 구현 과정

cub3D 과제는 3D 게임의 초석인 Wolfenstein의 게임을 구현하는 과제이다.
과제와 함께 minilibx 라이브러리가 제공되는데, 구현을 시작하기 전에 minilibx에 대한 이해를 도모하였다.

A. MiniLibX

MiniLibX는 42 학생들을 위한 window interface library이다. 작성한 코드를 토대로 window를 표현하기 위해 MiniLibX 라이브러리의 함수를 적재적소에 잘 사용해야 한다.

1) 컴파일 방법

  • minilibx 라이브러리로 opengl과 mms 두 가지 버전이 존재한다.
    • mms : MiniLibX 라이브러리의 가장 최신 버전이다. 모니터의 최대 해상도를 받아오는 함수인 mlx_get_screen_size를 사용하려면 mms라이브러리를 컴파일 해야한다. (내 맥북에선 동작이 안되더라... mac 업데이트가 필요할 듯.)
    • opengl : MiniLibX 라이브러리 구 버전이다.
  • compile
    : gcc -L./minilibx_opengl_20191021 -lmlx - framework OpenGL -framework AppKit main.c
    -L : 라이브러리가 위치한 디렉토리 지정
    -lmlx : MiniLibX 라이브러리 호출

2) 함수

A) void *mlx_init();

  • 프로그램과 Graphical System간 연결을 확립한다.
  • MLX instance의 위치를 void*로 리턴 받는다.

B) void mlx_new_window(void mlx_ptr, int size_x, int size_y, char *title);

  • window 창을 만들어 주는 함수
  • *mlx_ptr에 mlx_init() 함수로 리턴 받은 MLX instance의 위치를 입력한다.
  • width, height 매개 변수로 window의 크기를 결정한다.
  • title에 window의 이름을 지정한다.

C) int mlx_destroy_window(void mlx_ptr, void win_ptr);

  • 만들어진 window를 종료시킨다.
  • *mlx_ptr에 mlx_init() 함수로 리턴 받은 MLX instance의 위치를 입력한다.
  • *win_ptr에 mlx_new_window 함수로 리턴 받은 값을 입력한다.

D) int mlx_pixel_put(void mlx_ptr, void win_ptr, int x, int y, int color);

  • window의 (x, y) 위치에 int 형의 color 데이터를 입력한다.
  • 해당 함수는 굉장히 느림. (window 전체에 렌더링되는 frame을 기다리는 것이 없음. window의 pixel 하나하나에 직접 값을 입력함) -> 대안으로 image 함수를 활용함.
  • *mlx_ptr에 mlx_init() 함수로 리턴 받은 MLX instance의 위치를 입력한다.
  • *win_ptr에 mlx_new_window 함수로 리턴 받은 값을 입력한다.

E) void mlx_new_image(void mlx_ptr, int width, int height);

  • *mlx_ptr에 mlx_init() 함수로 리턴 받은 MLX instance의 위치를 입력한다.
  • width, height에 입력할 image의 크기를 넣는다.
  • image의 모든 pixel에 대한 buffer를 만들고, 해당 buffer를 통해 전체 pixel 데이터를 window에 넣는다.

F) char mlx_get_data_addr(void img_ptr, int bits_per_pixel, int size_line, int *endian);

  • image를 입력할 메모리의 주소를 받는다. (pixel 데이터를 넣을 공간 확보)
  • *img_ptr에 생성한 image에 대한 리턴 값을 입력한다.
  • *endian을 통해 데이터 저장방식을 알 수 있다. (일반적으로 사용하는 cpu는 리틀 endian 방식이라고 함, 리틀 endian 방식이란? 4바이트의 숫자를 1바이트씩 쪼개서 메모리에 저장할 때 반대로 저장하는 방식)
  • *bits_per_pixel은 픽셀 하나를 표현하기 위해 필요한 비트 수이다.
  • *size_line은 image에서 line간 간격이라고 보면 됨.
  • 참고 : https://github.com/p-eye/cub3d_texturing/blob/master/1.mlx_get_data_addr.md

G) int mlx_put_image_to_window(void mlx_ptr, void win_ptr, void *img_ptr, int x, int y);

  • window의 (x, y)위치부터 image 공간에 받은 pixel 데이터를 window에 표현함. pixel 데이터는 int 형인 color로 받는다.
  • *mlx_ptr에 mlx_init() 함수로 리턴 받은 MLX instance의 위치를 입력한다.
  • *win_ptr에 mlx_new_window 함수로 리턴 받은 값을 입력한다.
  • *img_ptr에 생성한 image에 대한 리턴 값을 입력한다.

H) void mlx_xpm_file_to_image(void mlx_ptr, char filename, int width, int *height);

  • texture로 입힐 xpm 파일을 image data로 만들어 주는 함수이다.
  • *mlx_ptr에 mlx_init() 함수로 리턴 받은 MLX instance의 위치를 입력한다.
  • width, height변수를 통해 xpm file의 크기를 받는다.
  • *filename 변수에 image로 만들 xpm 파일 이름을 넣는다.

I) int mlx_key_hook(void win_ptr, int (funct_ptr)(), void param);

  • *win_ptr : mlx_new_window함수에서 return된 값을 입력 (특정 window를 가리키는 값)
  • (*funct_ptr)() : 사용할 함수의 포인터 값을 입력. (사용자가 누른 키 int 값이 입력된 함수의 첫번째 매개변수로서 사용됨)
  • param : 함수 내에서 쓰일 변수들을 입력 (쓰일 변수가 많을 경우, 구조체로 묶어서 입력함)
  • mlx_key_hook함수가 연속으로 작성되어 있을 경우, 맨 마지막에 작성된 함수만 실행이 됨.
  • mlx_loop 함수가 맨 마지막에 같이 쓰여야 한다.

J) int mlx_loop(void *mlx_ptr);

  • 무한 루프를 돌려서 프로그램에서 임의로 정의한 이벤트가 발생하는 것을 기다린다.
  • *mlx_ptr에 mlx_init() 함수로 리턴 받은 MLX instance의 위치를 입력한다.

K) int mlx_hook(void win_ptr, int x_event, int x_mask, int (funct)(), void *param);

  • X11은 간단히 말해서 MiniLibX 라이브러리 사용자가 쉽게 정의할 수 있는 event들을 활용하는 라이브러리이다.
  • x_event : X11 라이브러리에서 정의된 event이다. 발생시키고 싶은 이벤트를 mlx_hook 함수의 매개변수로서 int형으로 입력하면 된다.
- event list
02: KeyPress
03: KeyRelease
04: ButtonPress
05: ButtonRelease
06: MotionNotify
07: EnterNotify
08: LeaveNotify
09: FocusIn
10: FocusOut
11: KeymapNotify
12: Expose
13: GraphicsExpose
14: NoExpose
15: VisibilityNotify
16: CreateNotify
17: DestroyNotify
18: UnmapNotify
19: MapNotify
20: MapRequest
21: ReparentNotify
22: ConfigureNotify
23: ConfigureRequest
24: GravityNotify
25: ResizeRequest
26: CirculateNotify
27: CirculateRequest
28: PropertyNotify
29: SelectionClear
30: SelectionRequest
31: SelectionNotify
32: ColormapNotify
33: ClientMessage
34: MappingNotify
35: GenericEvent
36: LASTEvent
  • x_mask : 보통 0을 입력함. (요건 이해가 안됨. 더 서칭 해봐야 할 듯)
- mask list
NoEventMask 0L
KeyPressMask (1L<<0)
KeyReleaseMask (1L<<1)
ButtonPressMask (1L<<2)
ButtonReleaseMask (1L<<3)
EnterWindowMask (1L<<4)
LeaveWindowMask (1L<<5)
PointerMotionMask (1L<<6)
PointerMotionHintMask (1L<<7)
Button1MotionMask (1L<<8)
Button2MotionMask (1L<<9)
Button3MotionMask (1L<<10)
Button4MotionMask (1L<<11)
Button5MotionMask (1L<<12)
ButtonMotionMask (1L<<13)
KeymapStateMask (1L<<14)
ExposureMask (1L<<15)
VisibilityChangeMask (1L<<16)
StructureNotifyMask (1L<<17)
ResizeRedirectMask (1L<<18)
SubstructureNotifyMask (1L<<19)
SubstructureRedirectMask (1L<<20)
FocusChangeMask (1L<<21)
PropertyChangeMask (1L<<22)
ColormapChangeMask (1L<<23)
OwnerGrabButtonMask (1L<<24)
  • mlx_key_hook과의 차이 : mlx_key_hook는 key를 누를때마다 이벤트가 일시적으로 발생하지만, mlx_hook는 누르고있는 상태에서 이벤트가 무한히 반복된다.
  • *win_ptr에 mlx_new_window 함수로 리턴 받은 값을 입력한다.
  • param : 함수 내에서 쓰일 변수들을 입력 (쓰일 변수가 많을 경우, 구조체로 묶어서 입력함)

L) int mlx_loop_hook(void mlx_ptr, int (funct_ptr)(), void *param);

  • 이벤트 발생 조건 없이 (*funct_ptr)()에 매개변수로 입력된 함수를 무한대로 실행함.
  • *mlx_ptr에 mlx_init() 함수로 리턴 받은 MLX instance의 위치를 입력한다.
  • param : 함수 내에서 쓰일 변수들을 입력 (쓰일 변수가 많을 경우, 구조체로 묶어서 입력함)

3) 참고 자료

B. map parsing

mlx함수들에 적응하기 위해서 2D 그래픽을 구현하는 실습을 진행하였다. 실습을 진행 하는 도중에 간단한 map 파싱을 해보는게 좋다고 판단 했고, 5x5형태의 간단한 map을 파싱하는 알고리즘을 생각해 보았다.

(mlx함수 적응을 위한 실습(taelee's github) : https://github.com/taelee42/mlx_example)

  • 구조 : main -> parse_map -> get_next_line
  • 설명
  1. map 파일을 main 함수의 인자로 받음.
  2. map 파일을 get_next_line 함수를 활용하여 한 줄씩 line 변수에 파싱함.
  3. window_size_y 크기를 가진 map_tmp 포인터배열 매개변수에 파싱한 line 변수를 차곡차곡 입력함.
  4. map 파일 파싱이 끝나면 최종적으로 map_tmp의 데이터들을 result 이중포인터 변수에 입력하여 parse_map 함수를 return 함.
  5. parse_map 함수에서 return 된 이중포인터 값을 구조체 변수 map에 입력함.
  • parse_map 함수 내부에서 map 구조체변수에 파싱된 데이터를 바로 넣으려 했으나, 이중포인터는 malloc함수를 써서 메모리 할당 후에 값을 넣어야 한다는 단점이 있었음. (map의 크기를 모르는 상태) 그래서 *map_tmp[WINDOW_SIZE_Y]; 형태의 포인터배열 크기를 미리 WINDOW_SIZE_Y로 선언한 후에 파싱된 데이터를 넣고, 모든 데이터가 map_tmp에 입력이 되면 result 이중포인터 변수에 넣어서 parse_map 함수를 return 함. (return된 값은 구조체 변수 map에 입력)
  • WINDOW_SIZE_Y는 window의 y크기로 map크기를 정할 수 있는 y최댓값이라 생각하였음.
  • map_tmp함수를 static으로 선언해야 main문 안에서 다른 함수로 맵 데이터가 입력이 될 때, 데이터를 안잃고 그대로 다른 함수에 전달할 수 있음. (strdup함수를 사용하여 이중포인터 변수로 옮긴 것이 아니기 때문에 parse_map 함수가 끝나면 데이터의 값을 모두 잃어버릴 우려가 있음.) -> 이 문제때문에 몇시간 고생함.

C. draw map and pos

첫번 째 구현 목표로 파싱한 map 데이터를 기반으로 2D 형태의 map을 window에 구현하기로 하였다.

  • 구조 : main -> draw_user, draw_map -> draw_block, draw_grid
  • 설명
  1. main 함수 내에서 draw_map, draw_user 함수에 구조체를 입력하여 실행함.
  2. 일정한 간격으로 격자와 block을 구현하기 위해 draw_map함수 내부에서 block 하나가 차지하는 pixel 수를 계산함. (window 크기에서 파싱한 map 크기를 나눠서 계산함)
  3. map 데이터가 1일 경우, draw_block 함수를, map 값이 0이거나 N일 경우, draw_grid 함수를 실행함. (draw_block 함수와 draw_grid 함수의 차이점은 격자 내부에 white color로 채웠느냐임)
  4. draw_user함수는 맵 데이터 중 N이 위치한 블록 가운데로 pos 위치를 옮긴 후에, pos를 둘러싸는 8개의 pixel에 color를 넣어주는 함수임.
  5. pixel 하나하나 정성스럽게 while문을 돌려가며 color를 넣어줬음.(block : white, grid : grey, mlx_pixel_put 함수 활용)
	i = x * tmp->block_x;
	while (i < (x + 1) * tmp->block_x)
	{
		j = y * tmp->block_y;
		if (i == ((x + 1) * tmp->block_x) - 1)
			tmp->color_tmp = GREY_COLOR;
		else
			tmp->color_tmp = WHITE_COLOR;
		while (j < (y + 1) * tmp->block_y)
		{
			if (j == ((y + 1) * tmp->block_y) - 1)
				tmp->color_tmp = GREY_COLOR;
			mlx_pixel_put(tmp->mlx, tmp->win, i, j, tmp->color_tmp);
			j++;
		}
		i++;
	}

D. user move

w, s key는 앞, 뒤, a, d key는 좌, 우 이동을 pos가 하게 해보았다. pos 이동 알고리즘의 핵심은 w, s, a, d key를 누를때마다 pos의 pixel 색깔을 전후좌우로 생성하고 삭제시켜서 pos가 움직이는 것처럼 보이게 하는 것이다.

  • 구조 : main -> mlx_hook -> user_move -> dont_move, erase_user, recovery_grid, draw_user
  • 설명
    - 원래 mlx_key_hook 함수를 활용하려 했으나, pos를 움직일 때마다 키를 눌러야 하는 단점이 있었음. 이를 보완하기 위해 key를 누르고 있으면 무한루프로 pos를 이동시켜주는 mlx_hook 함수를 활용함.
  1. user_move 함수에서 dont_move함수를 통해 다음 이동이 가능한지 판단함. 입력된 key가 w, s, a, d에 따라서 다음 이동이 벽일 경우, dont 값에 1를 입력하여 pos의 이동을 생략할 것임. (dont_move 함수에서 입력된 key에 따라 현재 pos가 위치한 pixel에 2를 더하거나 뺌. 더하거나 뺀 값을 map배열의 인덱스로 참조하여 맵 데이터가 1일 경우, 1을 return함)
  2. pos의 이동이 허가될 경우, erase_user, recovery_grid, draw_user함수를 차례대로 실행함. (recovery_grid 함수는 pos가 격자를 지나가는 경우, 격자가 지워지므로 다시 그려주기 위해 넣은 함수)
  3. erase_user함수를 활용하여 현재 pos pixel에 black color를 넣음. (draw_user함수 응용)
  4. draw_user함수를 활용하여 다음 pos pixel에 red color를 넣음.

E. rotate vector

a, d key를 눌렀을 때, 좌 or 우로 pos의 방향을 전환시켜 보았음.

  • 구조 : user_move함수에서 A_KEY, D_KEY 이벤트 발생 -> rotate_vector
  • 설명
    - 좌, 우 회전 알고리즘을 설계하기 전에 pos의 방향벡터(dir_x, dir_y)를 설정함.
    - A_KEY나 D_KEY를 눌렀을 때, 기존의 좌우 이동 동작을 좌우 회전으로 바꿈. (W_KEY 입력 시, 회전된 벡터가 바라보는 방향으로 전진해야 함)

    사진 출처 : https://ghebook.blogspot.com/2020/08/blog-post.html

    - A_KEY나 D_KEY를 눌렀을 때, rotate_vector함수 내부에서 일정 각도만큼 좌우로 회전하는 데 회전행렬 공식을 활용하여 이전 벡터좌표에서 일정 각도만큼 회전한 후의 벡터좌표를 계산함.
    - W_KEY를 눌렀을 때, 벡터방향으로 전진해야하므로 기존에 pos위치에서 1씩 증감하여 이동시키는 방식에서 pos위치를 위치벡터 값만큼 증감하는 방식으로 바꿈. (dont_move함수도 해당 방식대로 바꿈
user_move 함수

		if (key == A_KEY)
			rotate_vector(tmp, -PI / 36);
		else if (key == D_KEY)
			rotate_vector(tmp, PI / 36);
		else if (key == S_KEY)
		{
			tmp->user_x -= tmp->dir_x;
			tmp->user_y -= tmp->dir_y;
		}
		else if (key == W_KEY)
		{
			tmp->user_x += tmp->dir_x;
			tmp->user_y += tmp->dir_y;
		}
rotate_vector 함수

	tmp_x = (tmp->dir_x * cos(angle)) - (tmp->dir_y * sin(angle));
	tmp->dir_y = (tmp->dir_x * sin(angle)) + (tmp->dir_y * cos(angle));
	tmp->dir_x = tmp_x;

F. draw ray

기본적으로 cub3D를 구현할 때 raycasting 방식을 활용하여 구현하는 것이 일반적이다. 그리고 본격적으로 3D의 cub3D를 구현하기에 앞서 2D에서 raycasting 방식을 활용하여 광선을 쏘는 그래픽을 구현해볼 것이다.

(참고 : https://github.com/365kim/raycasting_tutorial/blob/master/3_untextured_raycaster.md
https://malbongcode.tistory.com/149)

  • 구조 : main -> user_move -> draw_ray -> draw_raycast
  • 설명
    - 임의로 정한 FOV(최외각 광선간의 각도) 안에서 일정 RAY_GAP(인접 광선간의 각도)만큼 띄워서 RAY를 뿌려줌
    - RAY가 벽을 만나면 RAY는 더이상 나아가지 않음
    - 회전할 때 RAY도 같이 회전함(draw_user함수와 같은 방식, key가 눌려질때마다 ray가 삭제되고 새로운 ray를 그려주는 방식으로 이동을 표현함)
  1. draw_ray함수에서 while문을 돌려 여러개의 ray를 draw_raycast함수를 통해 그려준다.
    (while문 범위는 FOV를 2만큼 나눠준 범위이고, while문 안에는 두 개의 draw_raycast함수가 있다(중앙의 광선을 기준으로 최외각 광선까지 퍼져나가면서 그려주는 방식))
  2. 한 개의 광선을 그려주는 draw_raycast함수 내에서 초기 광선의 위치를 pos위치로 정해준다.
  3. 광선이 나아가면서 벽을 만날 때까지 광선의 위치(ray_x, ray_y)에 더해질 dx, dy 값을 초기 방향벡터에서 회전행렬을 이용하여 구한다.(회전행렬에서 사용될 각도는 draw_raycast에 입력된 RAY_GAP)
  4. 무한히 돌아가는 while문 안에 map 데이터 값이 1일때까지 광선의 위치(ray_x, ray_y)에 빨간색의 ray pixel를 그려주는 알고리즘을 실행하여 광선을 표현함.
draw_ray 함수

	while (angle < FOV / 2)
	{
		draw_raycast(tmp, angle);
		draw_raycast(tmp, -angle);
		angle += RAY_GAP;
	}
draw_raycast 함수

	while (1)
	{
		if (tmp->map[(int)floor(ray_y) / tmp->block_y][(int)floor(ray_x) / tmp->block_x] == '1')
			break;
		else
			mlx_pixel_put(tmp->mlx, tmp->win, (int)floor(ray_x), (int)floor(ray_y), tmp->color_tmp);
		ray_x += tmp->dx;
		ray_y += tmp->dy;
	}

G. untextured raycast

본격적으로 3D의 cub3D를 구현하기 위한 기반을 다질 것이다. 광선을 2D 그래픽으로 구현하는 데 활용했던 raycasting 방식을 응용할 것이다.

참고 : https://github.com/l-yohai/cub3d/blob/master/mlx_example/01_untextured_raycast.c
참고 : https://github.com/365kim/raycasting_tutorial/blob/master/3_untextured_raycaster.md
참고 : https://malbongcode.tistory.com/149


남겨둔 코드가 없어서 당시에 휴대폰으로 찍은 사진을 올림..

  • 구조 : main -> mlx_loop_hook -> main_loop -> cal_pill
  • 설명
    - 2D를 3D로 구현하여 그려야 했기 때문에 기존의 draw관련 함수는 모두 필요없었음. 코드를 싹다 갈아엎음.
    - 기존의 draw_map, draw_ray함수에 활용된 데이터들은 temp함수를 만들어 이를 통해 받게하였고, draw관련 함수들(draw_ray, erase_ray, draw_raycast, recovery_grid, draw_map, draw_block, draw_grid, draw_user, erase_user)을 모두 지움.
    - 3D로 표현하기 위해 wall의 높이를 계산해야 했고, wall과 pos간 거리를 활용하기로 한다.

  1. while문의 x값에 따라 바뀌는 camera_x값을 구했음. camera_x는 간단하게 window에 pixel의 x값마다 그래픽을 표현하기 위해 -1과 1사이로 표준화한 x좌표라고 생각하면 된다.(while문이 돌아가면서 각 camera_x 값마다 wall과 pos간 거리를 계산할 것이다.)

    사진 출처 : https://github.com/365kim/raycasting_tutorial/blob/master/3_untextured_raycaster.md
  2. raydir 벡터는 각 camera_x 좌표에서 뿌려지는 광선의 방향이다. (이 광선은 벽에 부딫힐 때까지 직진함, 벽에 부딫힐 때의 광선의 길이를 계산하여 wall과 pos간 거리를 계산할 예정, pos를 기준으로 여러 광선을 뿌려줄 경우 어안렌즈 효과가 발생함. 이를 방지하기 위해 camera_x변수를 활용한 raydir 벡터를 사용함)
  3. map_x, map_y는 광선 위치의 좌표다. pos의 위치로 초기화 시켜줌
  4. deltadist는 인접 격자 사이에서의 광선 거리이다. (인접 x격자 사이면 deltadist_x, 인접 y격자 사이면 deltadist_y 변수를 활용할 예정)

    사진 출처 : https://github.com/365kim/raycasting_tutorial/blob/master/3_untextured_raycaster.md
  5. 광선이 벽에 부딫힐 때까지 광선의 위치 map_x, map_y를 증가시킬 stepX, stepY를 광선의 방향에 따라 초기화 시킨다. 또한, 광선의 방향(raydir)에 따라 광선의 길이 sidedist 변수를 pos와 인접 격자 사이의 광선 길이로 초기화 한다.(sidedist 값을 광선의 방향에 따라 초기화 하는 이유는 광선의 방향(raydir)이 음수일 때에는 int 값인 광선의 위치가 맞게 입력이 되어 있지만, 양수 일때에는 광선의 위치에 해당하는 격자의 +1 격자에 광선이 부딫히므로 map + 1된 값에서 pos위치를 빼줘야 함.)
  6. 벽에 광선이 부딫힐 때까지 map변수에는 step변수를, sidedist변수에는 deltadist를 더해줌. side 변수를 활용하여 x, y중 어떤 격자에 부딫혔는지 판단할 것이다. 그리고 side 값에 따라 최종 pos와 wall간 거리인 perpWallDist에 해당 map 값을 활용하여 거리를 계산하고 입력한다.(어안렌즈효과를 방지하기 위해 pos-wall간 거리인 sidedist 변수를 사용하지 않고 cameraplane-wall간 거리인 perpwalldist 변수를 사용한다.)

    사진 출처 : https://github.com/365kim/raycasting_tutorial/blob/master/3_untextured_raycaster.md
  7. perpWallDist 값을 활용하여 벽의 높이를 계산하고, 구한 벽의 높이를 활용하여 벽이 그려질 pixel시작점과 끝점을 구한다. (시작점과 끝점 사이의 pixel에서만 wall을 그릴 것임)
  8. perpWallDist 값을 활용하여 광선이 부딫힌 벽의 x좌표(wallX)를 구한다. 그리고 구한 wallX를 활용하여 texX 값을 구한다.(texX 값은 벽에 텍스쳐를 표현할 때, 텍스쳐 image의 x좌표를 참조하는 데 사용될 것이다.)
  9. texPos는 그려질 wall의 y좌표이고 texY는 texX와 마찬가지로 image의 y좌표를 참조하는 데 사용된다. 그리고 while문이 돌아갈 때마다 texPos 값이 증가하고 텍스쳐 image를 참조하여 wall을 그릴 것이다.
input_wall_texture 함수

	st_cal->step = 1.0 * tmp->image_height / st_cal->lineHeight;
	st_cal->texPos = (st_cal->drawStart
			- tmp->window_size_y / 2 + st_cal->lineHeight / 2) * st_cal->step;
	while (++y < tmp->window_size_y)
	{
		if (y >= st_cal->drawStart && y <= st_cal->drawEnd)
		{
			st_cal->texY = (int)st_cal->texPos & (tmp->image_height - 1);
			st_cal->texPos += st_cal->step;
			color = tmp->texture_arr[st_cal->texNum][tmp->image_height
				* st_cal->texY + st_cal->texX];
			if (st_cal->side == 1)
				color = (color >> 1) & 8355711;
			tmp->buf[y][x] = color;
		}
		else if (y < st_cal->drawStart)
			tmp->buf[y][x] = tmp->ceil_color;
		else
			tmp->buf[y][x] = tmp->floor_color;
	}
  • 회전 시, wall이 제대로 안그려지는 오류가 발생했는데, 이는 rotate_vector함수에서 방향벡터만 회전시키고 plane 벡터는 회전시키지 않았기 때문이었음. plane벡터를 회전시키는 코드를 추가하여 문제 해결.

H. textured raycast

그려진 3D 벽 위에 texture를 입혀볼 것이다. 이를 위해 xpm(X11 Pixmap Graphic)파일을 파싱해야 하고, 조건에 맞게 벽에 그려 넣어야 한다.

  • 구조 : main -> mlx_loop_hook -> main_loop -> cal_pill -> draw
  • 설명
    - untextured raycast에서 wall을 그릴 때 쓰였던 함수는 pixel마다 color를 직접 넣어주는 mlx_pixel_put 함수를 썼는데, 이 함수를 써서 window를 표현하게 되면 pos가 한번 움직일 때마다 mlx_pixel_put함수가 몇백~몇천번 동작하므로 동작이 매우 느렸음. 이를 보완할 방법으로 mlx_image관련 함수를 이용하기로 하였다.
  1. parse_cub 함수를 통해 cub 파일의 texture 경로를 파싱한다.
  2. mlx_xpm_file_to_image 함수를 활용하여 받은 texture 경로의 xpm파일을 image data로 받는다.
  3. 받은 여러 image data들을 texture 이중포인터 변수에 모아 담는다.
  4. input_wall_texture 함수에서 벽에 맞게 texture pixel 데이터를 변환하여 image data 매개변수인 buf 변수에 넣는다.
  5. draw 함수에서 새로 생성한 window image 공간에 buf 변수에 입력된 pixel data들을 넣는다.
  • y인덱스가 drawStart보다 작을때 하늘을 표현하는 sky색을 대입하고 y인덱스가 drawEnd보다 클때 바닥을 표현하는 grey색을 대입한다.

I. sprite

wall 뿐만 아니라 pos가 통과 가능하고 어느 방향에서 보든 일정한 모양을 유지하는 아이템(sprite)를 구현할 것이다. wall을 구현하는 방식과 유사하나, 일정한 모양을 유지해야하고 사각이 아닌 다양한 모양을 구현해야 해서 다른 부분이 있을 것이다.

  • 구조 : main -> mlx_loop_hook -> main_loop -> cal -> cal_sprite
  • 설명
  1. wall-pos간 거리인 perpWallDist들을 zbuffer 배열에 넣는다.
  2. sprite-pos 간 거리를 spriteDistance 배열에 넣음(제곱근 계산은 생략->상대적인 부분을 고려)
  3. sortSprites함수를 이용하여 spriteDistance 거리 먼 순으로 sprite를 정렬(sprite-pos간 거리가 먼 sprite를 먼저 그려야하기 때문)
  4. x, y 각 좌표의 sprite-pos간 거리 값을 spriteX, spriteY에 나누어 입력
  5. plane 벡터와 dir 벡터로 이루어진 행렬의 역행렬을 sprite변수에 곱하여 transform 변수에 입력(transform 변수의 용도는 차차 알아갈 예정)
  6. transform 변수를 이용하여 spriteScreenX 값을 구함(spriteScreenX 값은 실제 window에 표현하기 위한 camera plane 길이로 추정, 차차 알아갈 예정)
  7. 선언한 vMove 상수와 transformY 변수를 활용하여 vMoveScreen 값을 구함(vMoveScreen 변수는 어느 방향에서 sprite를 보던 간에 똑같은 모양이 나와야 하므로 사용하는 것으로 추정)
  8. wall을 그릴 때처럼 spriteHeight 값을 구하여 draw의 Y시작, 끝점과 X시작, 끝점을 구함
  9. wall을 그릴 때처럼 광선이 부딫힌 지점을 텍스쳐 위치로 변환하기 위해 texX와 texY 변수를 두어 어떤 텍스쳐 픽셀을 참조할 지 결정함.
  10. transformY가 0보다 클 때, stripe를 그리기 위한 while 반복문 변수인 stripe가 0보다 클 때, stripe가 window의 width보다 작을 때, transformY가 wall-pos간 거리(perpWallDist)보다 작을 때, sprite 텍스쳐를 buf에 입력함.

J. new parsing

본격적인 .cub파일 파싱 알고리즘을 설계할 것이다. .cub파일 파싱을 진행하기 전에 .cub파일의 포맷을 검사하여 유효한 .cub파일인지 판단해야한다. 유효한 .cub파일 포맷이 아닐 경우, 에러메세지를 출력해야한다.

  • 구조 : main -> parse_cub -> load_texture, parse_color, parse_map
  • 설명
    - 기존의 parsing 알고리즘은 유효성 검사가 없었고, map정보 없이 map data만 parsing하였음. 하지만 해상도나 텍스쳐 정보, ceil과 floor의 color 정보가 함께 내장되어있는 cub파일을 parsing 해야 하므로 새로운 parsing 방법이 필요함.
  1. parse_cub 함수에서 get_next_line 함수를 활용하여 .cub파일의 데이터를 한 줄씩 가져온다.
  2. 줄 단위로 각 정보가 들어있는 데, 각 줄의 첫 알파벳 정보를 통해 파싱된 정보가 어떤 정보인지 판단하고 어떤 함수를 실행시킬 지 결정함.
  3. ft_strncmp 함수를 활용하여 get_next_line함수로 받은 line 변수의 머리부분이 "R ", "NO ", "SO ", "S ", "WE ", "EA "와 일치할 때, load_texture 함수를 실행함, "F ", "C "와 일치할 때 parse_color 함수를 실행함.
  4. load_texture 함수가 실행되면 line을 ft_split 함수로 공백을 기준으로 값을 나누고, 조건에 맞게 필요한 변수에 입력한다. 해상도를 파싱할 때, ft_split으로 나눈 값이 모니터 최대 해상도를 초과할 경우, window_size로 최대 해상도 값을 입력한다. (mlx_get_screen_size 함수를 활용하여 최대 해상도 값을 가져오려고 했으나, mac 업데이트 문제로 코드에 값을 직접 입력함)
  5. texture의 경로가 line변수를 통해 load_texture에 입력되었을 경우, load_image 함수를 실행하여 texture 경로에 해당하는 xpm 파일 데이터를 texture_arr 배열에 넣는다.
  6. parse_color 함수가 실행되면 받은 line의 2번째 인덱스 값부터 ','를 기준으로 split 한다. split된 값은 순서대로 to_hexa 함수를 활용하여 16진수 값으로 변환시키고 strlcpy 함수를 활용하여 변환된 세 개의 16진수 값을 하나의 포인터 변수에 차곡차곡 쌓는다. 마지막으로 쌓여진 16진수 값을 hexa_to_int 함수를 활용하여 모두 합친 후 10진수로 변환하여 최종적으로 ret_result 변수에 입력한다. 그 후 line의 첫번째 인덱스 값에 따라서 floor_color or ceil_color 둘 중 어느 변수에 넣을 지 결정한 후에 넣는다.
  7. map 이차원 배열에 메모리 할당을 하기 위해 map의 y size를 미리 알아야하지만 parsing과 map의 y size를 동시에 파악하므로 처음에 딱 맞게 메모리를 할당하지 못함 -> 파싱을 진행하면서 할당과 해제를 반복하는 알고리즘을 고안함.
  8. 이를 위해 2차원 배열을 복사할 strdup함수가 필요했고 임의로 strdup_2함수를 만듦.
  9. first_last_line함수와 middle_line 함수를 이용하여 추출된 line이 유효한지 판단하였음.
  10. first_last_line 함수는 map의 첫줄과 마지막줄의 유효성을 검사하는 데, line 값이 white space가 아니고 1이 아닐 때 에러, 마지막줄에서 line 값이 white space일 때, 윗 줄이 0이면 에러로 판단함.
  11. middle_line 함수는 map의 첫줄과 마지막줄을 제외한 중간줄의 유효셩을 검사하는 데, line의 첫번째와 마지막 값이 0일 때 에러, line 값이 0이고 양 옆과 윗줄의 값이 white space일 때 에러, line 값이 white space이고 윗 줄이 0일 때 에러로 판단함.(유효성 검사를 진행하면서 sprite의 갯수 계산을 병행함)
  • parse_tmp 변수를 두어 .cub 파일에 내장되어 있는 map 정보가 순서대로 parsing될 수 있게 하였고, 순서가 어긋날 시에 오류를 출력하도록 하였음.

K. capture to BMP file

capture 기능을 추가하려고 한다. 프로그램 실행 시에 두 번째 인자로 --save 문자열이 입력될 경우, 프로그램의 window 초기 화면이 bmp 파일로 저장된다.

참고 : https://dojang.io/mod/page/view.php?id=702

  • 구조 : main -> main_loop -> draw -> save_bmp
  • 설명
  1. check_arg함수에서 두 번째 인자로 --save가 입력되었다고 판단할 경우, 구조체 변수 tmp.bmp_flag에 1이 입력됨.
  2. draw 함수에서 구조체 변수 tmp.bmp_flag가 1일 경우, 첫 화면을 캡쳐하는 기능의 함수인 save_bmp함수가 실행되고 mlx_dextroy_window와 exit함수를 실행시켜서 프로그램을 종료함.
  3. save_bmp 함수에서 bmp 파일 포맷에 맞게 순서대로 필요한 정보를 bmp 파일에 입력함.
  4. bmp파일의 처음 14바이트는 bmp 파일의 헤더가 저장됨.
  5. 그 다음 40바이트는 비트맵 정보의 헤더가 저장됨.
  6. 그 다음 바이트부터 픽셀 데이터가 가변적으로 저장됨.
  • exit 함수에 0을 입력하여 실행하면 정상종료, -1을 입력하여 실행하면 오류발생 종료임.
  • exit 함수 실행 시, 할당된 메모리를 모두 해제시켜 줌.
  • bmp 헤더에 값을 넣을 때, 비트연산자인 >>을 활용하여 8번 이동하게 하여 값을 순서대로 입력함.

L. Makefile

ft_printf를 구현할 때 사용했던 Makefile을 응용하였다.

  • 설명
    - 맨 상단에 복잡한 파일명이나 flag들을 간단한 변수로 치환
    - OBJ = $(SRC:.c=.o) : c파일에서 컴파일된 obj파일
    - all: $(NAME) : makefile 실행 시, 가장 먼저 실행되는 명령어
    - $(NAME): $(OBJ) : NAME을 만들기 위해 OBJ가 필요하고, NAME생성을 위한 컴파일 명령어를 수행함.
    - clean : OBJ 삭제
    - fclean : NAME과 OBJ 삭제
    - re : fclean실행 후 makefile 재실행

2. 오류 수정 및 코드 보완 과정

  1. pos의 위치에 N, S, W, E입력에 따라 pos가 스폰될 때 바라보는 방향이 결정된다.
  2. map이 좌우반전되는 문제를 해결했다.(plane 문제, 1번 과정을 거치면서 오류의 원인을 파악함)
  3. texture가 좌우반전되는 문제를 해결했다.(texX변수를 texture의 width값에서 빼준 후에 다시 texX변수에 넣어줬음)
  4. 프로그램의 초기화 부분과 파싱하는 부분에서 에러처리 기능을 추가했다.(tmp 구조체의 error 변수를 활용 하거나 일부 함수는 기존의 void 형에서 int형으로 바꾸어 함수가 끝날 때, 오류를 나타내는 int를 return할 수 있도록 함)
  5. load_texture함수에서 해상도를 파싱할 때, 조건문을 추가하여 최대해상도를 제한함.
  6. 임의의 텍스쳐를 직접 만들어서 입혔다.(facebook 로고를 png파일로 저장한 후에 인터넷에서 xpm 파일로 변환함. 변환한 xpm파일을 그림판으로 실행시켜서 해상도를 64x64로 설정)
  7. 같은 함수를 써서 다른 두 변수에 malloc후 return하면 오류발생(이유는 아직 파악못함)
  8. 텍스쳐의 빈공간을 채워야 오류가 없다.
  9. 프로그램 실행 시, 간헐적으로 malloc 에러 발생. (cub3D(20013,0x7fffb57dd380) malloc: error for object 0x7fdeb3021600: incorrect checksum for freed object - object was probably modified after being freed. set a breakpoint in malloc_error_break to debug Abort trap: 6) -> -g -fsanitize=address 플래그를 사용하여 컴파일하면 문제가 있는 부분을 찾을 수 있다.(어느 함수, 어느 변수에 문제가 있는지 직접적으로 알려줌, 오타가 있었음)
  10. leak을 잡음. 대부분 get_next_line 함수의 line을 free를 안해줘서 발생한 leak이었다.
  11. map 사이의 개행 예외처리는 get_next_line함수로 받아온 line 변수의 길이가 0일 때, 오류를 판단하는 것으로 처리함.
  12. cub 파일 첫줄에서 “R “ 다음에 값이 안들어 올 경우, split된 값이 null인지를 보고 정상 및 error로 판단하였다.
  13. 프로그램을 실행시킬 시에 인자가 안들어올 때, 에러처리가 안되고 seg fault 오류가 발생하였다.(인자가 없을 때, open 함수를 사용하여 인자를 참조하니까 seg fault 오류가 발생함, 인자를 체크하는 함수 뒤로 open 함수를 옮김)
  14. map의 첫 줄이나 마지막줄에 0이 입력될 경우, 에러처리가 안되고 seg fault 오류가 발생하였다.(error일 때, map_tmp 변수에 메모리를 할당하지 않는데, 이 변수를 참조하여 seg fault 오류가 발생하였음, error가 아닐 때에만 map_tmp 변수에 메모리를 할당하고 값을 넣는 조건문을 삭제함)
  15. left arrow or right arrow 키를 눌렀을 때, 좌우로 회전되고 a or d 키를 눌렀을 때, 좌우로 이동하는 코드로 수정하였다.(동작 방식은 기존 방식과 동일함, 함수만 몇개 추가하였음)

0개의 댓글