cub3d

박서연·2022년 11월 27일
0

1. Minilibx란?

: 42에서 쉽게 graphical software를 만들 수 있도록 제공하는 라이브러리
-> 간단한 윈도우 생성, draw tool, image, event 등 제공
-> makgefile 옵션: -I $(mlx 경로) -L $(mlx 경로) -lmlx -framework OpenGl -framework AppKit

a. void *mlx_init() : software와 display를 연결해 주는 함수. (프로그램과 screen 연결)

  • return : void *identifier (screen에 대한 식별자로 사용)

b. void mlx_new_window(void mlx_ptr, int size_x, int size_y, char *title) : screen에 새로운 창 하나를 생성.

  • mlx_ptr에는 mlx_init으로 생성한 identifier를 넘겨줌.
  • size_x : 창의 width, size_y : 창의 height.
  • title : bar에 적힐 이름
  • 실패시 NULL 반환

c. int mlx_clear_window(void mlx_ptr, void win_ptr) : 주어진 window를 검정 화면으로 clear.


d. int mlx_destroy_window(void mlx_ptr, void win_ptr) : 주어진 window를 destroy

  • win_ptr이 아닌 mlx_ptr은 별도의 destroy함수가 없는데, mlx_ptr에 할당되는 메모리구조는 상당히 복잡하기 때문에, 메모리 해제가 거의 불가능하다. 따라서 나머지 부분에 대해 메모리를 전부 해제하고, mlx_ptr은 free()없이 exit()하는 것이 더 좋다. (릭 안날 자신이 있다면 하구)

e. char mlx_get_data_addr(void img_ptr, int bits_per_pixel, int size_line, int *endian) : 생성된 image에 대한 정보를 알려줌.

  • get_data 함수 사용 이유 : 픽셀에 대한 정보를 얻기 위해.
  • img_ptr : 사용할 image에 대한 식별자
  • bits_per_pixel : pixel의 색(픽셀 하나)을 표현하기 위해 필요한 bit수에 대한 정보
  • size_line : 메모리 상 이미지의 한 줄이 몇 byte로 저장되어있는지
    • 각 픽셀에 대한 컬러 정보를 1차원으로 저장하고 있는데, 시각적으로 볼때는 2차원으로 읽어내야 함. => 1차원에서 2차원으로 변환하기 위해 size_line이 사용.
  • endian : pixel color가 little endian 방식으로 저장되어 있는지, big endian 방식으로 저장되어 있는지에 대한 정보를 저장하기 위한 변수.
    • big endian : 1 (가장 큰 데이터에서 작은 데이터 순으로 저장. 12 34 56 78)
    • little endian : 0 (가장 작은 데이터에서 가장 큰 데이터 순으로 저장. 78 56 34 12)
      • 보통 big endian이 편함.
  • get_data 이유 : 컴퓨터 마다 이미지를 저장하는 방식이 다를 수 있음.
  • return : 생성된 이미지에 대한 정보
    • 사용자가 나중에 이미지를 수정할 수 있도록 mlx_xpm_file_to_image에서 생성된 이미지에 대한 메모리 주소 시작 부분의 포인터를 반환.
    • 반환된 주소로부터, 첫 번째 bits_per_pixel 비트가 이미지의 제일 첫 줄의 첫 번째 픽셀의 색상을 나타냄.
    • 두 번째 그룹의 bits_per_pixel 비트는 첫째 줄의 두번째 픽셀을 나타냄.
    • 주소에 size_line을 추가해서 두 번째 줄 시작점을 얻으면서, 이미지의 모든 픽셀에 도달 가능.
  • char * 포인터에 연속으로 색상을 저장할 때 4칸 간격으로 저장하고 저장한 값을 뽑아올 때도 4칸 간격으로 가져오기 때문에 unsigned int형으로 형변환.
    • int형이 아닌 unsigned int로 하는 이유는 ARGB가 가질 수 있는 값은 0x00000000 ~ 0xFFFFFFFF 로 (unsigned int) 범위와 동일하기 때문


f. void mlx_xpm_file_to_image(void mlx_ptr, char filename, int width, int *height) : xpm_file의 파일명을 인자로 받아 이미지 데이터를 생성하고, 그 이미지를 해당 데이터를 바탕으로 채움.

  • return : 성공(해당 이미지에 대한 식별자), 실패 (NULL)

g. int mlx_hook(void win_ptr, int x_event, int x_mask, int (funct_ptr)(), void *param) : 이벤트 생성하기.

  • x_event와 x_mask 파라미터로 이벤트 상황 지정 (현재 x_mask는 내부에서 사용을 안해서 0으로 사용해도 됨.)

h. int mlx_loop (void *mlx_ptr) : 이벤트를 받기 위해 반드시 사용해야 하는 함수.

  • return을 하지 않고, event를 기다리는 무한 반복 loop가 실행.
  • event가 들어올 경우, 해당 event 관련 user-define 함수를 call.

i. int mlx_loop_hook (void win_ptr, int (funct_ptr)(), void *param) : 위와 비슷하지만, event가 발생하지 않아도 계속 호출됨.

  • 이벤트를 수신 후 연동 함수 호출하여 키 변경 작업을 마쳤더라도, 화면에는 반영이 되기 전이라, 해당 상황을 업데이트 해 주는 함수.
  • mlx_loop()이 동작하는 동안 funct_ptr로 받은 함수를 반복적으로 호출.
  • cub3D의 경우에는 자연스러운 움직임 구현이 강조되는 프로젝트이기 때문에 이벤트가 발생하지 않았더라도 계속해서 렌더링하는 구현

j. void mlx_new_image(void mlx_ptr, int width, int height) : 메모리에 새로운 img 생성 후 식별자 return

2. Raycasting

: raycasting이란? : 2차원 맵에서 3차원의 원근감을 만드는 렌더링 기술

  • 해당 부분은 수학식이 많기 때문에, 도와주는 튜토리얼이 있음.
  • 코드 이해를 돕는 그림들 첨부.

0. 변수

  • posX, posY : 플레이어 초기 위치벡터

  • dirX, dirY : 플레이어 초기 방향벡터

  • planX, planY : 플레이어에게 보이는 화면

  • 광선의 시작점 : 플레이어 위치 (posx, posy)

  • cameraX : x값(화면의 수직선? x=1, x=2 이런 느낌 기준인듯) 위치가 카메라 평면에서 차지하는 x좌표 (-1 ~ 1 사이에 x값이 어디에 해당하는지)

     r->cam_x = 2 * x / (double)WIN_X - 1;

    계산하기 쉽도록 cam_x를 정규화 시켜서 -1 ~ 1의 범위를 가지도록 함.

    • x / WIN_X는 0~1의 값을 가짐.
    • *2를 통해 0~2의 값을 가지도록 한 다음
    • -1을 해서 -1~1의 범위로 만들어 줌.
  • rayDirX, rayDirY : 광선의 방향벡터 (cam_x의 비율을 곱해서 정확한 광선이 쏘는 위치를 판단)

    r->raydir_x = v->dir_x + v->pln_x * r->cam_x;
    r->raydir_y = v->dir_y + v->pln_y * r->cam_x;
  • 광선의 방향 : 방향벡터 + 카메라평면 * 배수

3. DDA알고리즘

  • sideDistX: 시작점 ~ 첫번째 x면을 만나는 점까지 광선의 이동거리
  • sideDistY: 시작점 ~ 첫번째 y면을 만나는 점까지 광선의 이동거리
  • deltaDistX: 첫번째 x면 ~ 바로 다음 x면까지 광선의 이동거리 (x는 1만큼 이동)
  • deltaDistY: 첫번째 y면 ~ 바로 다음 y면까지 광선의 이동거리 (y는 1만큼 이동)

1. stepX, stepY 초기값 구하기

  • 만약 광선의 x방향 rayDirX 의 값이 양수라면 stepX 의 값은 +1 로, 음수라면 -1 로 설정
  • 만약 rayDirX 의 값이 0 라면, stepX 는 사용되지 않으므로 어떤 값을 갖든 상관없음.
  • stepY 의 값도 똑같이.

2. sideDistX, sideDistY의 초기값 구하기

  • sideDistX 초기값
    • rayDirX의 값이 양수 일 경우, 광선의 시작점부터 오른쪽 으로 이동하다 처음 만나는 x면까지의 거리 (mapX + 1 에서 실제위치 posX 를 빼주고 deltaDistX 값을 곱함)
    • rayDirX의 값이 음수일 경우, 광선의 시작점부터 왼쪽 으로 이동하다 처음 만나는 x면까지의 거리 (posX 에서 mapX 를 빼주고 deltaDistX 값을 곱함)
  • sideDistY도 동일 (시작점 기준 위쪽 또는 아래쪽)
 	init_ray(v, r, x);
	if (r->raydir_x < 0)
	{
		r->step_x = -1;
		r->sidedist_x = (v->pos_x - r->map_x) * r->deltadist_x;
	}
	else
	{
		r->step_x = 1;
		r->sidedist_x = (r->map_x + 1.0 - v->pos_x) * r->deltadist_x;
	}
	if (r->raydir_y < 0)
	{
		r->step_y = -1;
		r->sidedist_y = (v->pos_y - r->map_y) * r->deltadist_y;
	}
	else
	{
		r->step_y = 1;
		r->sidedist_y = (r->map_y + 1.0 - v->pos_y) * r->deltadist_y;
	}

3. dda알고리즘은 반복문을 실행할 때 마다, x방향 또는 y방향으로 딱 한 칸 점프

  • 광선의 방향에 따라 stepX, stepY에 +1 또는 -1로 저장
  • stepX를 사용하면 x방향으로 한 칸 점프
  • stepY를 사용하면 y방향으로 한 칸 점프

4. 광선이 점프할 때마다 sideDistX, sideDistY는 deltaDistX, deltaDistY 가 더해지면서 값이 업데이트

5. 광선이 점프할 때마다 mapX, mapY 는 stepX, stepY 가 더해지면서 값이 업데이트

6. 벽의 x면 또는 y면과 부딪혔는지 여부에 따라 루프를 종료할지 결정

  • hit은 벽과 부딪혔는지 여부(루프 종료조건)

  • 만약 동쪽면에 부딪히면 side = 0

  • 만약 서쪽면에 부딪히면 side = 1

  • 북 = 2, 남 = 3

  • mapX, mapY로 어떤 벽이랑 부딪혔는지 알 수 있음.

    while (r->hit == 0)
    		{
    			if (r->sidedist_x < r->sidedist_y)
    			{
    				r->sidedist_x += r->deltadist_x;
    				r->map_x += r->step_x;
    				if (r->step_x == 1)
    					r->side = 0;
    				else
    					r->side = 1;
    			}
    			...
    		}

!! 여기까지는 그 벽에서 정확히 어디에 부딪힌거 까지는 알 수 없음. 이따 추가 과정이 필요 !!

4. 광선의 시작점에서 벽까지의 이동 거리 (벽 높이 구하기)

  • 어안렌즈 효과 : 실제 거리 값을 사용했을 때, 모든 벽이 둥글게 보여 회전할 때 울렁거릴 수 있음.
  • 어안렌즈 효과를 피하기 위해 플레이어 위치까지의 거리 대신, 카메라 평면까지의 거리를 사용.
    if (r->side == 0 || r->side == 1)
    		{
    			r->wall_dist = (r->map_x - v->pos_x + \
    				(1 - r->step_x) / 2) / r->raydir_x;
    			if (r->wall_dist <= 0.000001)
    				r->wall_dist = 0.00001;
    			return (v->pos_y + r->wall_dist * r->raydir_y);
    		}
    		...
    		}
    • (r->map_x - v->pos_x + (1 - r->step_x) / 2) / r->raydir_x;
      • step_x == 1

      • step_x == -1
      	if (r->wall_dist <= 0.000001)
      		r->wall_dist = 0.00001;
         # wall_dist가 0이면 천장바닥만 보일 수 있기 때문에, 그걸 방지!
         # (0이 되는 이유 : 0.00~~01이 되어서 double이 표현할 수 있는 구간을 넘겨 0으로 찍히는 듯!)

5. 실제 ray값 구하기

  • w->line_h = (WIN_Y / r->wall_dist) : w_d와 실제 우리가 보는 벽의 높이는 반비례!

6. 시작, 끝 지점 구하기

7. y구하기

step : 벽 이미지(texture)를 화면에 표시하려면 얼마나 늘리거나 줄여야 할지
tex_pos : 이미지에서 어디에 해당하는지. w.draw_start는 화면에서 벗어나면 화면의 끝(0 or WIN_Y - 1)으로 지정해 뒀기 때문에, 짤린 부분이 발생. 이런 경우에는 w.draw_start가 0이 되지 않음. 벗어난 길이 만큼 step 비율을 곱해주며 큰 경우는 줄이고 화면보다 작은 경우는 여러번 해당 부분 이미지가 출력되도록 해줌(이미지 키우기).
tex_y : texpos가 이미지 픽셀 h 길이 안에 들어오도록 계산. (만약 16px * 16px인 경우, tex_pos & 15를 해서 16을 넘지 않도록 예방)
tex_pos += step : tex_pos에 step을 더해주며 다음 칠해야 하는 칸으로 넘어감
& : 0이 하나라도 있으면 0, 둘 다 1이어야 1.

8. 참고

profile
좋은 사람들과 좋은 시간을 보내기 위한 프론트엔드 개발자

0개의 댓글