[Cub3d] 기초.(mlx)

kihkim·2021년 2월 17일
0

42seoul

목록 보기
4/5

참고 자료.

주의 사항.

  • 이 글은 각도기반으로 시도하다가 망한 글입니다.
  • 개인적인 생각으로 각도기반은 구현하기 엄청 어렵다고 생각합니다.
    (특히 스프라이트 부분이)
  • 본인도 각도기반으로 하려다가 2달동안 뻘짓하고 벡터기반으로 갈아탔습니다.
  • cub3d를 처음하시는 분이라면 mlx파트만 봐주시고, 레이캐스팅(각도기반) 파트는 무시해주시기 바랍니다.
  • 벡터기반으로 구현 : https://github.com/365kim/raycasting_tutorial (레이캐스팅 튜토리얼 번역)
  • 벡터기반으로 스프라이트 구현 : https://github.com/lyohai/cub3d/blob/master/mlx_example/sprite.md

도전과제.

• 창 관리는 매끄럽게 유지되어야합니다 : 다른 창으로 변경, 최소화 등.
• 벽에 따라 달라지는 다양한 벽 텍스처를 표시합니다 (선택은 귀하의 것입니다).
벽이 향하는 쪽 (북, 남, 동, 서).
• 프로그램은 벽 대신 항목 (스프라이트)을 표시 할 수 있어야합니다.
• 프로그램은 바닥 및 천장 색상을 서로 다른 두 가지 색상으로 설정할 수 있어야합니다.
• Deepthought가 언젠가 귀하의 프로젝트를 평가할 눈이 있다면 귀하의 프로그램은
두 번째 인수가 다음과 같을 때 첫 번째 렌더링 된 이미지를 bmp 형식으로 저장해야합니다.
"--저장".
• 두 번째 인수가 제공되지 않으면 프로그램은 창에 이미지를 표시하고
다음 규칙을 준수합니다.
◦ 키보드의 왼쪽 및 오른쪽 화살표 키를 사용하여 왼쪽과 오른쪽을 볼 수 있어야합니다.
미로에서 바로.
◦ W, A, S 및 D 키를 사용하여 시점을 이동할 수 있어야합니다.
미로.
◦ ESC 키를 누르면 창을 닫고 프로그램을 깨끗하게 종료해야합니다.
◦ 창의 프레임에있는 빨간색 십자가를 클릭하면 창을 닫고
프로그램을 깨끗하게 종료하십시오.
◦지도에 선언 된 화면 크기가 디스플레이 해상도보다 큰 경우
창 크기는 현재 디스플레이 해상도에 따라 설정됩니다.
◦ minilibX의 이미지 사용을 적극 권장합니다.
• 프로그램은 .cub가있는 장면 설명 파일을 첫 번째 인수로 취해야합니다.

보너스 파트
보너스는 필수 부분이 PERFECT 인 경우에만 평가됩니다.
PERFECT 란 당연히 완전해야 함을 의미합니다.
잘못된 사용과 같은 심한 실수의 경우에도 실패 할 수 없습니다.
기본적으로 필수 부분이 ALL을 얻지 못하면
점수를 매기는 동안 귀하의 보너스는 완전히 무시됩니다.
보너스 목록 :
• 벽 충돌.
• 스카이 박스.
• 바닥 및 / 또는 천장 텍스처.
• HUD.
• 위아래를 보는 능력.
• 점프하거나 웅 크리십시오.
• 거리 관련 그림자 효과.
• 라이프 바.
• 미로에 더 많은 항목.
• 개체 충돌.
• 물건 / 덫을 주워 포인트를 획득하거나 생명을 잃습니다.
• 열고 닫을 수있는 문.
• 비밀 문.
• 총알 애니메이션 또는 애니메이션 스프라이트.
• 여러 수준.
• 소리와 음악.
• 마우스로 시점 회전

  • 보너스 파트에서 만만해 보이는 건, 벽 충돌, 소리, 마우스 시점 전환.

현재환경.

  • 맥이 없는 윈도우ㅠㅠ
  • 우분투 20.04버전 사용.
  • WSL2 사용.

GUI 환경 설정.

xWindow

  • 윈도우의 우분투 환경에서 개발을 한다면 위와 같은 이름을 들어봤을 것이다.
  • 우분투 환경에서는 GUI를 불러올 수 없다.
  • 그렇기에 만들어진 것이 xWindow.
  • xWindow는 텍스트 기반이었던 유닉스/리눅스 시스템에서 그래픽을 사용하기 위해 개발되었다.
  • xming과 vcxsrv은 xWindow의 한 종류라고 보면 될 듯.

xming? vcxsrv?

  • 둘 중 어느 것을 쓰냐고 하면 WSL1은 xming을 WSL2는 vcxsrv를 사용하는 것을 추천한다.
  • 내 경우엔 WSL1을 썻을 때는 xming으로 잘 됐지만, WSL2로 업그레이드하고 사용했지만 xming과 vcxsrv 둘 다 되지 않았다.
  • xming 설정법은 인터넷에 자세히 나와있으니 참고바람.
DISPLAY 환경 변수 설정.
WSL1의 경우 : export DISPLAY=localhost:0.0
WSL2의 경우 : export DISPLAY="`grep nameserver /etc/resolv.conf | sed 's/nameserver // '`:0"
  • WSL1은 호스트와 이더넷을 공유했기에 로컬호스트로 접속할 수 있다.
  • 하지만 WSL2는 이더넷이 분리되었기 때문에 로컬호스트로 접속 불가능하기에 환경 변수 설정이 서로 다르다.
  • wsl2 환경 변수 설정 (추가).
  • 위 명령어는 /etc/resolve.conf의 nameserver 주소를 가져와 끝에 :0을 추가한 것이다. (cat /etc/resolve.conf)
  • 만약 nameserver 주소가 172.26.128.1이라면 export DISPLAY=172.26.128.1:0 명령어로 환경 변수를 설정할 수 있다.

(추가) wsl2는 vcxsrv를 사용하자.

  • 컴퓨터 환경은 윈도우 우분투, wsl2.
  • 사용한 프로그램은 vcxsrv. (xming으로도 시도해봤는데 전혀 되지 않았다.)
    (반대로 wsl1에서는 xming이 좋다.)
  • 이것만 주의하면 잘 할 수 있다.
  • 첫째, 윈도우 방화벽에 가서 앱 허용하기.
  • 윈도우 보안 -> 방화벽 및 네트워크 보호 -> 방화벽에서 앱 허용 -> VcXsrv windows xserver 선택 후 개인, 공용 둘 다 허용해준다. (참고로 나는 vcxsrv 항목이 두 개 여서 둘다 허용했다.)
  • 둘째, -ac 플러그 추가하기.
  • vcxsrv 실행 전 나오는 설정 창에서 Additional parameters for VcXsrv 항목에 -ac를 추가한다.
  • 이건 마찬가지로 공용 접근을 허용한다는 의미인 것 같다.
  • 실험 결과 위 두 가지를 지켜야만 성공적으로 화면이 나타났다.
  • 그래도 vcsxsrv로 접속이 안되면 아래 RDP를 이용해보자.
    둘 다 사용해본 결과, RDP가 더 불편하다.

RDP

  • RDP는 마이크로소프트에서 개발한 그래픽 공유 프로토콜이다.
  • VNC처럼 가상 원격 접속 가능.
  • VNC에 비해 그래픽과 안정성이 높다.
  • 이건 xming과 vcxsrv 둘 다 안되었을 때, 사용했다.
  • 다만 그만큼 불편하다.

RDP 설치

1 apt install -y xrdp
2 apt install -y xfce4
3 apt install -y xfce4-goodies
4 sudo cp /etc/xrdp/xrdp.ini /etc/xrdp/xrdp.ini.bak
5 sudo sed -i 's/3389/3390/g' /etc/xrdp/xrdp.ini
6 sudo sed -i 's/max_bpp=32/#max_bpp=32\nmax=bpp=128/g' /etc/xrdp/xrdp.ini
7 sudo sed -i 's/xserverbpp=24/#xserverbpp=24\nxserverbpp=128/g' /etc/xrdp/xrdp.ini
8 sudo /etc/init.d/xrdp start
  • 이러면 포트 3390으로 서버가 열린다.(본래는 3389.)
  • 1.윈도우의 기본 프로그램인 원격 데스크톱 연결에 들어간다.
  • 2.주소에는 localhost:3390입력후 접속.
  • 3.들어가면 뜨는 로그인 창에는 우분투의 사용자명과 비밀번호를 입력한다.
  • 4.접속하면 가상 리눅스 환경이 나오는데, 여기서 명령 프롭프트에 들어간다.
  • 5.GUI가 성공적으로 나올 수 있는지 확인하기 위해 sudo apt-get install gedit으로 gedit을 설치하고 명령어 gedit으로 실행. (이미 설치되었으면 스킵.)
  • 6.잘 나오면 GUI 환경이 완성된 것이다.
  • [추가] 간혹 RDP창이 검은색으로 꺼질 떄가 있다.
    이때는 재접속을 해도 여전히 검은창일 수도 있는데, 그럴떄는 우분투에서 RDP서버를 재시작하면 된다.(이 현상이 자주 일어나서 불편...)
    sudo /etc/init.d/xrdp restart

컴파일

minilibx_opengl_20191021

  • 42프로젝트 창에서 다운 받아, Cub3D 폴더 내부에 넣는다.
  • 여기에는 다양한 mlx 함수들이 있는데, 이것들을 사용하려면
    # include "../minilibx_opengl_20191021/mlx.h"로 헤더 파일을 불러와 사용하면 된다.

기본적인 창 생성 코드

# include "../minilibx_opengl_20191021/mlx.h"

int             main(void)
{
    void    *mlx;

    mlx = mlx_init();
    mlx_win = mlx_new_window(mlx, 800, 600, "Hello 42!");
    mlx_loop(mlx);
    return (0);
}

필요한 추가 라이브러리 설치.

apt install libx11-dev
apt install libxext-dev
apt install libbsd-dev
apt install clang

결과 확인.

gcc main.c -lmlx -lX11 -lXext -lm -lbsd (win)
gcc main.c -lmlx -lm -framework OpenGL -framework AppKit (Mac)

  • -lmlx : libmlx.a 라이브러리를 사용하는 옵션입니다.
  • -lX11 : 위에서 받은 추가 라이브러리인 libx11-dev을 사용하는 옵션.
  • -lXext : 위에서 받은 추가 라이브러리인 libxext-dev을 사용하는 옵션.
  • -lm : math libarary 사용하는 옵션입니다.
  • -lbsd : 위에서 받은 추가 라이브러리인 libbsd-dev을 사용하는 옵션.
  • 우선 makefile이 아닌 맛보기 실행 명령어.

mlx함수 정리.

mlx_pixel_put

  • 화면을 1픽셀로 칠하는 함수.
    예시.
	for (x = 0; x < 220; x++)
	{
		for (y = 0; y < 220; y++)
		{
			mlx_pixel_put(mlx_ptr, win_ptr, x, y, 0xFFFFFF);
		}

	}
  • 이러면 검은색인 화면을 부피 220만큼 흰색으로 채울 수 있다.

mlx_key_hook

  • int mlx_key_hook ( void win_ptr, int (funct_ptr)(), void param );
  • 이벤트를 감지하는 함수.
  • 첫번쨰 매개변수: 화면
  • 두번쨰 매개변수: 실행 할 함수.
  • 세번째 매개변수: 전달할 변수.
void ft_putchar(char c)
{
    write(1, &c, 1);
}

int deal_key(int key, void *param)
{
    ft_putchar('X');
    return (0);
}

int main(void)
{
	void *mlx_ptr;
	void *win_ptr;


	mlx_ptr = mlx_init();
	win_ptr = mlx_new_window(mlx_ptr, 300, 300, "Hello, World!");
    mlx_key_hook(win_ptr, deal_key, (void *)0);
	mlx_loop(mlx_ptr);
	return (0);
}
  • 이렇게 하면 키보드의 아무 키나 입력하면 이벤트가 발생해, 쉘 창에 'X'가 출력된다.

mlx_hook

  • int mlx_hook(void win_ptr, int x_event, int x_mask, int (funct)(), void *param);
  • 위에 나온 mlx_key_hook 보다 더 보편적인 함수. (이벤트와 이벤트 마스크라는 매개변수가 추가 되었다.)
mlx_hook(win_ptr, KeyPress, KeyPressMask, deal_key, (void *)0);
  • 두번쨰와 세번째 매개변수는 이벤트와 이벤트 마스크를 의미한다.
  • 위의 함수는 이와 같이 쓸 수도 있다.
mlx_hook(win_ptr, 2, 1L<<0, deal_key, (void *)0);
  • 본래라면 KeyPress는 2로 KeyPressMask는 (1L<<0)로 입력해야 하지만, 내 경우엔 따로 매크로가 있는 헤더 파일을 사용했다.
  • #include "X11/X.h" (mlx 이벤트 변수가 정의된 헤더파일)
  • 다양한 mlx 이벤트 변수들: https://harm-smits.github.io/42docs/libs/minilibx/events.html

mlx_mouse_hook

  • int mlx_mouse_hook ( void *win_ptr, int (*funct_ptr)(), void *param );
  • mlx_key hook이 키보드의 입력을 감지한다면, 이것은 마우스 버튼의 클릭을 감지한다.
int deal_key(int key, void *param)
{
    ft_putchar('X');
    return (0);
}
.
.
.
mlx_mouse_hook(win_ptr, deal_key, (void *)0);;
  • 이러면 마우스를 클릭할 때마다 'X'가 출력된다.

mlx_xpm_file_to_image

  • void *mlx_xpm_file_to_image(void *mlx_ptr, char *filename, int *width, int *height)
  • 로컬에 있는 이미지를 불러오는 함수.
  • 단 이미지를 불러오기만 할 뿐, 아직 화면에 입력하진 않았다.
  • [중요]여기서 이미지는 무조건 xpm형식의 파일이어야 한다.

mlx_put_image_to_window

  • int mlx_put_image_to_window(void *mlx_ptr, void *win_ptr, void *img_ptr, int x, int y)
  • 이미지를 직접 화면에 출력하는 함수.

키보드 입력 받기.

int				key_press(int keycode, t_param *param)
{
	static int a = 0;

	if (keycode == KEY_W)//Action when W key pressed
		param->x++;
	else if (keycode == KEY_S) //Action when S key pressed
		param->x--;
	else if (keycode == KEY_ESC) //Quit the program when ESC key pressed
		exit(0);
	printf("x: %d\n", param->x);
	return (0);
}
.
.
mlx_hook(win, KeyPress, KeyPressMask, key_press, &param);

레이캐스팅

레이캐스팅 기본 원리.

  • 플레이어는 시야가 있다.
  • 플레이어의 시야에서 뻗어 나온 광선(Ray)가 벽에 닿는다.
  • 이때, 플레이어와 벽 사이의 거리를 구한다.
  • 이 거리를 이용해서 우리는 구현해야 할 벽의 높이를 구할 수 있다.
  • 벽의 높이를 구할 수 있다는 말은 플레이어가 움직이면서 바뀌는 원근법을 어느 정도 조절할 수 있다는 소리.

브레즌헴 알고리즘 vs DDA 알고리즘.

  • 둘 다 컴퓨터 그래픽에서 도형, 선분을 그리는데 필요한 계산 알고리즘이다.
  • 기본적으로 DDA 보다 브레즌헴이 더 정확도가 높고, 게산식 또한 더 간단하다.
  • 브레즌헴은 DDA 알고리즘과 다르게 소수점의 반올림하는 과정이 필요없어 속도저하가 없다.

브레즌헴 알고리즘 원리

  • https://playground10.tistory.com/62 (예제)
  • 위 참고 글의 마지막 부분에 예시가 잘 나와있다.
  • 기본적인 공식과 그것을 코드로 바꾸는 부분까지.

브레즌헴 알고리즘 예제.

  • 두 시작점(20, 10)과 끝점(30, 18)으로 부터 판별식(F)를 구한다.
  • W(길이) = 30 - 20 = 10
    H(높이) = 18 - 10 = 8
    F(판별식) = 2H - W = 16 - 10 = 6
    F = 6
이름x좌표y좌표판별식(F)
판별식(F) < 0x + 1yF + 2H
판별식(F) >= 0x + 1y + 1F + 2(H-W)

즉, F < 0 이면 F + 2H = F + 16
F >= 0 이면 F + 2(H-W) = F + 2(-2) = F - 4

단계 1 (21, 11), F = 2
단계 2 (22, 12), F = -2
단계 3 (23, 12), F = 14
단계 4 (24, 13), F = 10
단계 5 (25, 14), F = 6
단계 6 (26, 15), F = 2
단계 7 (27, 16), F = -2
단계 8 (28, 16), F = 14
단계 9 (29, 17), F = 10
단계 10 (30, 18), F = 6

  • 단계마다 적힌 좌표를 픽셀로 찍으면, 그게 선이 된다.
  • 와! 우리는 선을 구현했다.

위 알고리즘을 사용하는 이유.

  • 우리는 Cub3D에서 3차원 맵을 구현해야 한다.
  • 우리는 위에서 설명한 레이캐스팅 기본 원리단계에서 광선(Ray)의 필요성에 대해 설명했다.
  • 두 알고리즘은 컴퓨터에서 플레이어가 쏘아낸 광선이 벽에 닿는 것을 검사하는 알고리즘이다.
  • 컴퓨터는 사람과 다르게 광선이 벽에 닿더라도 그것이 닿았는지 인식할 수 없다.
  • 그렇기에 DDA와 브레즌헴 알고리즘을 통해 광선의 인식 기능을 추가하는 것이다.

2D 로드맵을 구현.

  • 시작부터 3D 화면을 구현하는 것은 버거우니까, 가볍고, 우리에게 익숙한 2D(평면)부터 구현한다.
  • https://www.youtube.com/watch?v=gYRrGTC7GtA&list=WL&index=1(Make your Raycaster)
  • 위 참고 영상에서는 코드 상에서 2D 플레이어, 광선(Ray)을 구현하는 방법을 알려주었다.(2:45부터 시작)

플레이어 만들기.

  • 우선 우리는 앞으로도 계속해서 사용할 유용한 구조체 하나를 만들 것이다.
  • 구조체를 쓰는 이유는 나중에 만들 루프 함수에 win변수나, mlx변수가 필요한데, 가끔 두 개 전부 파라미터(param)으로 쓰기도 하기 때문이다.
  • 파라미터는 한번에 하나밖에 못 보내므로 구조체로 묶어서 보내는 것이다.
  • [추가] 앞으로 자주 사용할 변수들은 info 구조체에 포함시킬 예정이다.
  • 현재 단계에서는 info 구조체의 모든 변수를 적을 필요는 없고, 간단하게 mlx와 win만 추가해두고 진행하다가 부족하다 싶으면 더 추가해보자.
typedef struct	s_info
{
	void	*mlx;
	void	*win;
	float	player_x;
	float	player_y;
	int		pixel_size;
	int		window_x;
	int		window_y;

	void        *img;
    char        *addr;
    int         bits_per_pixel;
    int         line_length;
    int         endian;
}				t_info;
  • 이제 플레이어를 화면에 그린다.
  • 다만 플레이어의 위치는 방향키를 누름에 따라 움직일 수 있다.
  • 즉, 실시간으로 업데이트가 되므로, 루프를 돌려야 한다.

mlx_loop_hook

  • int mlx_loop_hook(void *mlx_ptr, int (*funct_ptr)(), void *param)
  • 두번째 매개변수에 루프를 돌린 함수를 집어넣으면, 프로그램이 돌아가는 동안 계속해서 함수를 반복실행 한다.
  • [정보] mlx_loop와 다른 점은 mlx_loop는 윈도우를 출력하는 함수만 반복실행하는데, 이것은 자신이 원하는 함수를 선택할 수 있다는 것.
mlx_loop_hook(info.mlx, &main_loop, &info);
  • [추가] 앞으로 새로 만들어갈 함수들 대부분은 main_loop함수 안에서 작동될 것이다.
    (ex) drawmap2D(), drawPlayer(), bresenham()
nt		main_loop(t_info *info)
{
	//draw player
	for(int x = 0; x < info->pixel_size; x++)
	{
		for(int y = 0; y < info->pixel_size; y++)
		{
			mlx_pixel_put(info->mlx, info->win, info->player_x + x, info->player_y + y, 0x00FFFF00);
		}
	}
}
  • 다만 이렇게 할 경우 플레이어가 움직이기는 하지만, 그 경로가 그대로 남는다.
  • 그렇기에 우리는 루프 함수 시작부분에 맵 전체를 초기화하는 함수를 넣어 플레이어의 이동 흔적을 지운다.
int		main_loop(t_info *info)
{
    //맵 전체를 검은색으로 초기화
	for(int x = 0; x < info->window_x; x++)
	{
		for(int y = 0; y < info->window_y; y++)
		{
			my_mlx_pixel_put(info, x, y, 0x00000000);
		}
	}
	mlx_put_image_to_window(info->mlx, info->win, info->img, 0, 0);

    //플레이어를 픽셀로 찍어낸다.
	for(int x = 0; x < info->pixel_size; x++)
	{
		for(int y = 0; y < info->pixel_size; y++)
		{
			mlx_pixel_put(info->mlx, info->win, info->px + x, info->py + y, 0x00FFFF00);
		}
	}
}
  • 그리고 맵 전체를 검은색으로 칠할 때, my_ml_mlx_pixel_put이라는 새로 만든 함수를 사용했다. (window_x y는 윈도우 화면 크기를 의미, 내 경우엔 800, 600 사용)
  • 이건 mlx의 image관련 함수인데, mlx_puxel_put보다 속도가 빠르다고 해서 한 번 사용해봤다. (즉, mlx_pixel_put으로 맵을 초기화해도 된다.)

2차원 맵 만들기.

int mapX=8, mapY=8, mapSize=64;
int map[]=
{
	1, 1, 1, 1, 1, 1, 1, 1,
	1, 0, 1, 0, 0, 0, 0, 1,
	1, 0, 1, 0, 0, 1, 0, 1,
	1, 0, 1, 0, 0, 0, 0, 1,
	1, 0, 0, 0, 0, 0, 1, 1,
	1, 0, 0, 0, 0, 0, 0, 1,
	1, 0, 0, 0, 0, 0, 0, 1,
	1, 1, 1, 1, 1, 1, 1, 1,
};
void	drawMap2D(t_info *info)
{
	int	x, y, xo, yo, color;
	for(y = 0; y < mapY; y++) //0, 1, 2, 3, 4, 5, 6, 7, 8, 9
	{
		for (x = 0; x < mapX; x++) //0, 1, 2, 3, 4, 5, 6, 7, 8, 9
		{
			if (map[y*mapX + x] == 1)
				color = 0x00FFFFFF;
			else
				color = 0x00000000;
			xo = x * mapSize; //0, 64 ,128 ,192, 256, 320
			yo = y * mapSize; //0, 64 ,128 ,192, 256, 320

			for (int i = 0; i < mapSize; i++)
			{
				for (int j = 0; j < mapSize; j++)
				{
					my_mlx_pixel_put(info, xo + i, yo + j, color);
				}
			}
		}
	}
}

방향키에 따른 각도 계산

  • Cub3D는 ws로 전진, 후진을 하고, ad로 움직일 방향(각도)를 조절할 수 있다.
  • 각도를 계산하는 과정에서는 필연적으로 math.h라이브러리가 필요하다.
#include <math.h>

float pdx, pdy, pa;

int	key_press(int key, t_info *info)
{
	if (key == KEY_W)
	{
		info->player_x += pdx;
		info->player_y += pdy;
	}
	if (key == KEY_S)
	{
		info->player_x -= pdx;
		info->player_y -= pdy;
	}
	if (key == KEY_A)
	{
		pa -= 0.1;
		if (pa < 0)
		pa += 2 * PI;
		pdx = cos(pa) * 5;
		pdy = sin(pa) * 5;
	}
	if (key == KEY_D)
	{
		pa += 0.1;
		if (pa > 2 * PI)
		pa -= 2 * PI;
		pdx = cos(pa) * 5;
		pdy = sin(pa) * 5;
	}
	return (0);
}
  • 이제 ad를 통한 각도 변경이 가능해졌다.

시야 만들기.

  • 각도라는 기능을 추가했지만, 그것이 직접적으로 보이지 않아서 플레이어가 어디를 바라보고 있는 것인지 모른다.
  • 그래서 우리는 화면에 플레이어가 바라보는 시야를 그려야한다.
  • 영상속에서는 glBegin(GL_LINES) 함수를 이용했다. (https://www.youtube.com/watch?v=gYRrGTC7GtA&t=313s) (6:21)
  • 이건 두 개의 좌표를 입력받으면 두 좌표 사이를 이어서 선분을 만들어주는 함수이다.
  • mlx에는 그런 함수가 없으므로 우리가 직접 만들어 사용해야 한다.

브리즈헴 알고리즘.

void	bresenham(t_info *info, int startX, int startY, int finishX, int finishY)
{

	int x = startX;
	int y = startY;
	int Xfactor = finishX < startX ? -1 : 1;
	int Yfactor = finishY < startY ? -1 : 1;

	int w = abs(finishX - startX); // -300
	int h = abs(finishY - startY); // -200

	int f;

	if (w > h)
	{
		f = (2 * h) - w;
		for (x = startX; x != finishX; x += Xfactor)
		{
			if (f < 0)
			{
				f += 2 * h;
			}
			else
			{
				y += Yfactor;
				f += 2 * (h - w);
			}
			my_mlx_pixel_put(info, x, y, 0x00FFFF00);
		}
	}
	else
	{
		f = (2 * w) - h;
		for (y = startY; y != finishY; y += Yfactor)
		{
			if (f < 0)
			{
				f += 2 * w;
			}
			else
			{
				x += Xfactor;
				f += 2 * (w - h);
			}
			my_mlx_pixel_put(info, x, y, 0x00FFFF00);
		}
	}
}
  • bresenham 함수는 시작 좌표 (x, y)와 도착 좌표(x, y)를 입력받으면 두 좌표 사이에 선을 그려주는 함수다.
  • 시작 좌표와 도착 좌표의 각 x, y가 누가 크냐에 따라 Xfactor와 Yfactor의 값도 바뀌어야 한다는 것을 깨달았다.
  • 이제 모든 방향에 시야가 잘 그려진다.

맵의 블럭의 수평선을 감지하는 함수.(horizontal)

  • 눈에 보이는 광선을 하나 더 만들었다.
  • 이 광선은 각 맵 블럭(벽)의 수평선(남쪽, 북쪽 벽)에 닿으면 진행을 멈춘다.
void	drawRays3D(t_info *info)
{
	int		r, mx, my, mp, dof;
	float	rx,ry, ra, xo, yo;
	int		color;
	color = 0x00FFFF00;

	ra=pa;
	// 수평선(horizontal)
	for(r = 0; r < 1; r++)
	{
		dof = 0;
		float aTan = -1/tan(ra);
		if (ra > PI)
		{
			ry = (((int)info->player_y >> 6)<<6) - 0.0001;
			rx = (info->player_y - ry) * aTan + info->player_x;
			yo = -64;
			xo = -yo * aTan;
		}
		if (ra < PI)
		{
			ry = (((int)info->player_y >> 6) << 6) + 64;
			rx = (info->player_y - ry) * aTan + info->player_x;
			yo = 64;
			xo = -yo * aTan;
		}
		while (dof < 8)
		{
			mx = (int)(rx) >> 6;
			my = (int) (ry)>>6;
			mp = my * mapX + mx;
			if (mp < mapX * mapY && map[mp] == 1)
				dof = 8;
			else
			{
				rx += xo;
				ry += yo;
				dof += 1;
			}

		}
		bresenham(info, info->player_x, info->player_y, rx, ry, color);
	}
}
  • 몬가 복잡한 계산식이 있지만, 이건 너무 복잡하니까 원리는 그냥 넘어가자.
  • 수직선(vertical) 부분은 위 코드에 넣지 않았지만 수평선(horizontal)과 원리는 같으므로 영상에 나온대로 하면 된다.

segmentation fault

  • 이렇게 시야를 만들었는데, 각도를 움직이다가 간혹 튕기며 segmentation fault가 뜨는 경우가 있다.
  • 이는 그려야 하는 픽셀 또는 이미지가 화면 밖에 있을 때 에러가 발생한다.
  • 예를들어 화면이 800x600이라면, 그릴 수 있는 범위는 (0,0) 부터 시작해서 (800, 600) 좌표 사이다.
  • 여기서 말하는 에러는 (-25, -25) 같이 화면에 안 보이는 곳을 그렸을 때 발생한다.
  • 현재 문제가 되는 곳은, 선을 그리는 함수인 bresenham이므로 이걸 수정하면 된다.
if (w > h)
	{
		f = (2 * h) - w;
		for (x = startX; x != finishX; x += Xfactor)
		{
			if (f < 0)
			{
				f += 2 * h;
			}
			else
			{
				y += Yfactor;
				f += 2 * (h - w);
			}
			if (x >= 0 && x < info->window_x)
				my_mlx_pixel_put(info, x, y, color);
		}
	}
  • my_mlx_pixel_put의 조건에 x좌표가 0보다 크고, 윈도우 화면보다 작을 때만 그리도록 조건을 넣었다.

동영상#2

동시 키 입력.

  • 기존에는 버튼을 누르면 누른 버튼의 키코드를 이용해 플레이어를 움직였다.
  • 이것은 옳은 방법이지만 본래 쓰던 코드는 두 개 이상의 키를 동시에 인식하지 못한다는 단점이 있다.
  • 왜냐하면 버튼을 여러개 눌러봐야 들어오는 키코드는 한개이기 때문이다.
int		key_check[65536];

if (info->key_check[KEY_W] == 1 && keycode != KEY_ESC)
	{
		info->player_x += pdx;
		info->player_y += pdy;
	}
if (info->key_check[KEY_S] == 1 && keycode != KEY_ESC)
	{
		info->player_x -= pdx;
		info->player_y -= pdy;
	}
  • 배열의 크기는 본래 300이었지만, KEY_ESC의 키코드가 65536이기에 그냥 크게 잡았다.
int ButtonDown(int keycode, t_info *info)
{
	if (keycode >= 300)
		return (-1);
	info->key_check[keycode] = 1;
	return (0);
}

int Button_Release(int keycode, t_info *info)
{
	if (keycode >= 300)
		return (-1);
	info->key_check[keycode] = 0;
	return (0);
}
  • 함수 이름을 ButtonRelease로 하면 기존 헤더 함수와 이름이 겹쳐 에러 발생.
mlx_loop_hook(info.mlx, &main_loop, &info);
mlx_hook(info.win, KeyPress, KeyPressMask, ButtonDown, &info);
mlx_hook(info.win, KeyRelease, KeyReleaseMask, Button_Release, &info);
  • 이외에도 mlx_hook을 하나 더 추가한다.
  • 기존에 쓰던 KeyPressMask말고도 KeyReleaseMask를 사용하는 mlx_hook을 새로 만든다
  • KeyPressMask는 버튼을 누르면 인식되고, KeyReleaseMask는 버튼을 때는 순간 인식된다.
  • 이제 여러개의 키를 동시에 입력받아도 keycode가 여러개이기에 모두 인식이 가능하고, 각 keycode의 활성화/비활성화 여부도 두 개의 mlx_hook이 관리한다.

플레이어가 벽 통과 못하게 하기.

  • 플레이어는 벽을 통과해서는 안 되고, 맵 밖으로 나가서도 안 된다.
  • 플레이어의 앞에 벽이 있으면 앞으로 나아갈 수 없다는 조건을 주면 된다.
	int xo=0;	if (pdx<0) { xo=-20; } else { xo=20;}
	int yo=0;	if (pdy<0) { yo=-20; } else { yo=20;}

	//ipx와 ipy, add, sub 변수 생성.
	int ipx=info->player_x/64.0, ipx_add_xo=(info->player_x+xo)/64.0, ipx_sub_xo=(info->player_x-xo)/64.0;
	int ipy=info->player_y/64.0, ipy_add_yo=(info->player_y+yo)/64.0, ipy_sub_yo=(info->player_y-yo)/64.0;
    
	if (info->key_check[KEY_W] == 1) //키를 감지하면.
	{
    		//플레이어의 앞(현재 위치)에 있는 게 벽이 아니면.
		if (map[ipy*mapX + ipx_add_xo] != 1) {info->player_x += pdx;}
		if (map[ipy_add_yo*mapX + ipx] != 1) {info->player_y += pdy;}
	}
	if (info->key_check[KEY_S] == 1)
	{
		if (map[ipy*mapX + ipx_sub_xo] != 1) {info->player_x -= pdx;}
		if (map[ipy_sub_yo*mapX + ipx] != 1) {info->player_y -= pdy;}
	}
  • pdx와 pdy변수(x, y의 기울기, 플레이어가 바라보는 각도)를 통해 xo, yo 값을 작성한다.
  • ipx, ipy는 플레이어의 현재 위치(x, y)를 월드맵의 크기(64)에 맞게 변환한 것이다.
  • 이제 플레이어가 바라보는 시야의 앞에 무엇이 있는지 알수 있다.
  • if문 조건에 플레이어의 앞에 벽이 없을 때에만 움직이도록 조건을 주었다.

바닥, 천장 색 칠하기.

  • 벽은 구현했지만, 바닥과 천장이 시꺼먼 검정색이라 칙칙한 느낌이 있다. 색칠을 해보자.
  • 처음에는 어떻게 색칠을 해야하나 고민했다.
  • 화면을 절반으로 나누고, 그 위는 천장, 그 아래는 바닥으로 구분지어 몽땅 색칠하는 방법을 사용했으나, 옳은 방법은 아니었다.
  • 코드를 만들기 전에 천장과 바닥의 범위가 정확히 어디인지 알 필요가 있었다.
  • 결국 천장과 바닥이라는 것은 벽을 기준으로 벽보다 위에 있으면 천장, 벽보다 아래에 있으면 바닥이다.
// 벽을 칠한다.
bresenham(info, (r * 8+530), lineO, (r * 8+530), lineH + lineO, color, 8);
  • 선분을 긋는 함수인 bresenham, r은 반복문 안에서 1씩 증가하므로 결과적으로 선분은 위에서 아래로 그어진다.
  • X좌표는 반복문마다 1씩 증가이기에 우리는 Y좌표만 신경쓰면 된다.
  • 즉, 천장은 시작 Y지점이 0(화면의 시작) 부분이고, 끝나는 y지점은 벽의 제일 윗 부분이다.
  • 즉, 바닥은 시작 Y지점이 벽의 제일 아래이고, 끝나는 y지점은 화면의 끝(window_y) 부분이다.
//천장
bresenham(info, (r * 8+530), 0, (r * 8+530), lineO, 0x0040E0D0, 8);

// 바닥
bresenham(info, (r * 8+530), lineH + lineO, (r * 8+530), 512, 0x00FF0000, 8);

지도에 따라서 플레이어의 위치, 각도 조정하기.

int mapX=10, mapY=10, mapSize=64;
int map[]=
{
	1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
	1, 0, 3, 0, 0, 0, 0, 0, 0, 1,  <=  (2, 1)위치에 3이 있다.
	1, 0, 0, 0, 0, 0, 0, 0, 0, 1,
	1, 0, 0, 0, 0, 0, 0, 0, 0, 1,
	1, 0, 0, 0, 1, 0, 0, 0, 0, 1,
	1, 0, 0, 0, 0, 0, 0, 0, 0, 1,
	1, 0, 0, 0, 0, 0, 0, 0, 0, 1,
	1, 0, 0, 0, 0, 0, 0, 0, 0, 1,
	1, 0, 0, 0, 0, 0, 0, 0, 0, 1,
	1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
};
  • 현재 10X10 크기의 맵에 (2, 1)위치에 '3' 값이 있다. (맨 처음 시작 지점이 (0,0)이다.)
  • 저게 플레이어를 뜻한다.
  • 과제에서는 0은 공간, 1은 벽, 2는 아이템(아직 미구현)이다.
  • 그리고 플레이어는 W,S,N,E라는 알파벳으로 표현한다.
  • 각 알파벳에 따라 플레이어가 어느 방향을 볼 지 결정하는데, 현재 내 Map은 자료형이 int라 알파벳은 안 된다.
  • 그래서 맵 파일을 받으면 저 알바펫 부분은 다른 걸로 치환해야 한다.(E = 3(동), W = 4(서), S = 5(남), N = 6(북))
  • 치환 파트는 나중에 피싱 파트를 할 때 뒤에서 따로 설명.

전체 코드

void	player_init(t_info *info)
{
	int x;
	int y;

	for (int i = 0; i < (mapX * mapY); i++)
	{
		if (map[i] == 3 || map[i] == 4 ||  map[i] == 5 ||  map[i] == 6)
		{
			//맵 기준 x, y 구하기.
			x = i % mapX;
			y = i / mapX;
			info->player_x = x * 64 + (mapSize / 2);
			info->player_y = y * 64 + (mapSize / 2);
			if (map[i] == 3) //동
				pa = 0 + 0.25;
			else if (map[i] == 4) //서
				pa = 3 + 0.25;
			else if (map[i] == 5) //남
				pa = 1.5 + 0.25;
			else if (map[i] == 6) //북
				pa = 4.5 + 0.25;
			break;
		}
	}
}

위치

x = [현재 배열 위치] % 배열의 x크기.
y = [현재 패열 위치] / 배열의 x크기.
  • 1차원 배열을 2차원 배열, 즉 x,y로 바꿀 수 있다.
  • 현재 내 map은 일차원 배열이므로 따로 x, y 값을 계산해 구했다.
	info->player_x = x * 64 + (mapSize / 2);
	info->player_y = y * 64 + (mapSize / 2);
  • 현재 맵 타일의 사이즈(mapSize)가 64이므로, 위에서 구한 x,y 값에 64를 곱하면, 그게 곧 플레이어의 위치다.
  • 여기서 플레이어를 타일의 정중앙에 생성시켜야 했기에, 각 x,y 값에 타일의 절반을 곱했다.

각도

  • pa의 값에 따라 플레이어가 바라보는 각도가 다르다. (0이면 동쪽, 1.5 남쪽, 3.0 서쪽, 4.5 북쪽) (미세조정 위해 +0.25)
			if (map[i] == 3) //동
				pa = 0 + 0.25;
			else if (map[i] == 4) //서
				pa = 3 + 0.25;
			else if (map[i] == 5) //남
				pa = 1.5 + 0.25;
			else if (map[i] == 6) //북
				pa = 4.5 + 0.25;

텍스쳐 구현하기.

  • 이제 벽에 텍스쳐를 입혀야 한다.
  • xpm파일이라는 이미지 파일을 받아와서 하지만 그전에 어떻게 벽에 텍스쳐를 입히는지 알아야 한다.
int All_Textures[]=
{
	0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
	0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
	0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
	0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,

	1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
	1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
	1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
	1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,

	0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
	0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
	0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,
	0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1,

	1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
	1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
	1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
	1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0, 1,1,1,1,1,1,1,1, 0,0,0,0,0,0,0,0,
};
  • 위는 대표적인 체크무늬 텍스쳐다.
  • 위 배열 하나하나 마다 정보를 가져와서 벽에다 픽셀 단위로 그리면 되는 것이다.
  • 여기선 픽셀 단위로 세밀하게 찍어야하기에 선분을 그리는 함수인 bresenham은 벽을 그릴 때 사용하지 않았다.
		int rayHitVertical = 0;
        
		//vertical wall hit
		if (disV < disH) { rx = vx; ry = vy; disT = disV; color = 0x00FFAA00; rayHitVertical = 1; }

		//horizontal wall hit
		if (disH < disV) { rx = hx; ry = hy; disT = disH; color = 0x00FFCC00;}

		bresenham(info, info->player_x, info->player_y, rx, ry, color, 1);

		//----3D 그리기-----
		float ca = pa - ra; if (ca < 0) { ca+= 2* PI; } if(ca > 2*PI) { ca-= 2 * PI; } disT = disT * cos(ca);
		float lineH = (mapSize * 320)/disT;
		if (lineH > 320) { lineH = 320; }
		float lineO = 160 - lineH/2;

		float ty_step = 32.0 / (float)lineH;
		float ty_off=0;
		if (lineH > 320) { ty_off=(lineH-320)/2.0; lineH = 320;}

		float ty = ty_off*ty_step;

		float tx;
		if (disV > disH) { tx=(int)(rx / 4.0) % 16;}
		else 			 { tx=(int)(ry / 4.0) % 16;}

		//천장
		bresenham(info, (r * 8+530), 0, (r * 8+530), lineO, 0x0040E0D0, 8);

		// 벽 (텍스쳐 적용)
		for (int y = 0; y < lineH; y++)
		{
			int c;
			if ((int)ty + (int)tx * 32 < 512)
				c = All_Textures[(int)ty + (int)tx * 32];
			ty += ty_step;
			if (rayHitVertical == 1) //동서
			{
				if (c == 0) //텍스쳐 배열의 0
				{
					if (info->player_x > rx) //동쪽
						color = 0x000000000; //검정
					else //서쪽
						color = 0x000FFFF00; //노랑
				}
				else if (c == 1)//텍스쳐 배열의 1
				{
					if (info->player_x > rx) //동쪽
						color = 0x000FFFFFF; //하양
					else
						color = 0x000FFFFFF; //하양
				}
			}
			else //남북
			{

				if (c == 0)
				{
					if (info->player_y > ry) //남쪽
						color = 0x00066ff33;//연두
					else //북쪽
						color = 0x00000FFFF;//하늘색
				}
				else if (c == 1)
				{
					color = 0x000ff99ff; //밝은 보라
				}
			}
            // 벽 그리기
			for(int aa = 0; aa < 8; aa++)
				my_mlx_pixel_put(info, r * 8 +530 + aa, y+lineO, color);
		}

		// 바닥
		bresenham(info, (r * 8+530), lineH + lineO, (r * 8+530), 512, 0x00FF0000, 8);

		ra += DR; if (ra < 0) { ra+= 2* PI; } if(ra > 2*PI) { ra-= 2 * PI; }
	}
}

동서남북 구분.

  • disV < disH이면 동서, disV > disH이면 남북으로 구분 지을 수 있다. (disV는 플레이어와 세로벽 사이의 거리)
  • 우리는 이렇게 동서와 남북으로 구분지었다.
  • 그러면 동서는 어떻게 동과 서로 구분지을 수 있을까?
  • 벽과 플레이어의 위치를 이용하면 된다.
  • 플레이어의 x위치가 벽보다 작으면 동쪽, 크면 서쪽으로 구분짓는다.
  • 플레이어의 y위치가 벽보다 작으면 남쪽, 크면 북쪽으로 구분짓는다.
if (disV < disH) //동서
{
	if (info->player_x < rx)
		texNum = 0;  //동
	else
		texNum = 1;  //서
}
else
{
	if (info->player_y < ry)
		texNum = 2; //남
	else
		texNum = 3; //북
}

xpm 텍스쳐 파일을 저장.

void    load_image(t_info *info, int *texture, char *path)
{
    info->img = mlx_xpm_file_to_image(info->mlx, path, &info->img_width, &info->img_height);
    info->addr = mlx_get_data_addr(info->img, &info->bits_per_pixel, &info->size_l, &info->endian);
    for (int y = 0; y < info->img_height; y++)
    {
        for (int x = 0; x < info->img_width; x++)
        {
            texture[info->img_width * y + x] = info->addr[info->img_width * y + x];
        }
    }
    mlx_destroy_image(info->mlx, info->img);
}

void    load_texture(t_info *info)
{
    load_image(info, info->texture[0], "textures/wall_e.xpm");
    load_image(info, info->texture[1], "textures/wall_n.xpm");
    load_image(info, info->texture[2], "textures/wall_s.xpm");
    load_image(info, info->texture[3], "textures/wall_w.xpm");
}
  • 추가로 int **texture; 형식의 변수를 구조체에 추가해야 한다.
  • 여기에 필요한 텍스쳐들을 따로 저장할 것이다.
int	main(void)
{
	t_info info;

	info.mlx = mlx_init();
	info.player_x = 300;
	info.player_y = 300;
	info.pixel_size = 8;
	info.window_x = 1024;
	info.window_y = 512;

	player_init();

	//텍스쳐 초기화.
	if (!(info.texture = (int **)malloc(sizeof(int *) * 8)))
        return (-1);
    for (int i = 0; i < 8; i++)
        if (!(info.texture[i] = (int *)malloc(sizeof(int) * (texHeight * texWidth))))
			return (-1);
    for (int i = 0; i < 8; i++)
        for (int j = 0; j < texHeight * texWidth; j++)
            info.texture[i][j] = 0;

	info.key_check[KEY_ESC] = 0;
	//텍스쳐 초기화시 keycode가 멋대로 지정되는 오류가 있어서 넣음.

	load_texture(&info);

	info.win = mlx_new_window(info.mlx, info.window_x, info.window_y, "Hello world!");
    info.img = mlx_new_image(info.mlx, info.window_x, info.window_y);
    info.addr = mlx_get_data_addr(info.img, &info.bits_per_pixel, &info.size_l,
                                  &info.endian);

	mlx_loop_hook(info.mlx, &main_loop, &info);
	mlx_hook(info.win, KeyPress, KeyPressMask, Button_down, &info);
	mlx_hook(info.win, KeyRelease, KeyReleaseMask, Button_release, &info);

	mlx_loop(info.mlx);
}
  • main부분에 텍스쳐 초기화 코드를 추가.
  • texture 변수를 사용하기 전에 알맞은 크기의 xpm파일인지 검사하고, 메모리 할당, 초기화 한다.
  • load_image라는 텍스쳐의 색 정보를 추출하는 함수를 제작.
  • load_texture라는 함수를 이용해 우리가 가져온 텍스트 파일의 색 정보를 Texture 변수에 저장.

xpm 텍스쳐 파일을 사용.

  • 색 정보를 추출했으니 이것을 벽에 입혀야 한다.
  • 그런데 우리는 텍스처의 어느 위치에 있는 픽셀을 추출해야 할지 알 수 없다.
  • 예를 들어보자.
  • 만약 벽의 크기가 30X30이고, 텍스쳐 파일의 크기도 30X30이라면 그냥 텍스쳐의 위치에 벽의 위치를 대입해서 색깔을 입히면 되니 문제가 없다.
  • 하지만 만약 벽의 크기가 30X30이고, 텍스쳐 파일의 크기는 기본 64X64라면 크기가 서로 달라서 충돌이 일어난다.
  • 그렇기에 우리는 텍스쳐 크기의 맞춰서 벽에 색을 입힐 위치 또한 조정해야 한다.

서로 다른 벽과 텍스쳐의 위치 구하기

  • https://yechoi.tistory.com/60 (yechoi)
  • 다행히 yechoi님의 설명에 나와있다.
  • 즉 텍스쳐상의 좌표는 벽의 좌표에 TEX_SIZE / wall_height 만큼 곱해주면 된다고 한다.
  • EX) 벽의 크기는 30이고, 텍스쳐의 크기는 64이다. 이때 벽의 (15, 15)의 위치에 해당하는 텍스쳐의 위치는 어디인가?
  • TEX_SIZE = 64
  • wall_height = 30
  • 벽의 위치(15, 15)
  • 텍스쳐의 위치(15 x (64/30), 15 x (64/30) => (32, 32)
  • 근데 난 https://www.youtube.com/watch?v=PC1RaETIx3Y&t=381s (Make Your Own Raycaster Part 2) 보고 해서 좌표가 이미 구해져있었다. (tx와 ty)
  • 그래서 내가 한 일이라곤 영상에 나온 32X16 기준으로 된 코드를 64X64 코드로 수정한 게 전부다.
float ca = pa - ra;
		if (ca < 0) { ca+= 2* PI; }
		if (ca > 2*PI) { ca-= 2 * PI; }
		disT = disT * cos(ca);

		float lineH = (mapSize * 320)/disT;
		if (lineH > 320) { lineH = 320; }
		float lineO = 160 - lineH/2;

		float ty_step = 64.0 / (float)lineH;  //원래 32, 지금은 64X64라 64.0으로 변경
		float ty_off=0;
		if (lineH > 320) { ty_off=(lineH-320)/2.0; lineH = 320;}

		float ty = ty_off*ty_step;

		float tx;
        

		//32X16사이즈의 경우: (4, 16)
        	//32X32사이즈의 경우: (2, 32)
       		//64X64사이즈의 경우: (1, 64)
		if (disV > disH) { tx=(int)(rx / 1.0) % 64;} 
		else 		 { tx=(int)(ry / 1.0) % 64;}

		//천장
		bresenham(info, (r * 8+530), 0, (r * 8+530), lineO, 0x0040E0D0, 8);

		int texNum;
		//벽 (텍스쳐 적용)
		for (int y = 0; y < lineH; y++)
		{
			if (rayHitVertical == 1) //동서
			{ 
				if (info->player_x < rx)
					texNum = 0;  //동
				else
					texNum = 1;  //서
			}
			else
			{
				if (info->player_y < ry)
					texNum = 2; //남
				else
					texNum = 3; //북
			}



			color = info->texture[texNum][(int)ty * 64 + (int)tx];
			ty += ty_step;
			for(int aa = 0; aa < 8; aa++)
				my_mlx_pixel_put(info, r * 8 +530 + aa, y+lineO, color);
		}
  • tx는 64X64 배열의 가로줄을 담당한다.
  • ty는 64X64 배열의 세로줄을 담당한다.
  • tx와 ty값에 따라 배열의 값도 달라지는 것.
  • 배열 공식: (ty * Texture_size) + tx)
  • 예를 들어 tx가 4이고 ty가 1이면 배열의 68번째 색을 불러온다는 의미.

특정 방향의 벽이 회전되서 칠해지는 경우.

  • xpm 파일을 가져와 색의 정보를 받고 벽에 입히는 것까지 성공했다.
  • 다만 서쪽과 남쪽의 경우 좌우가 뒤바뀌어 출력되었다.
	if (rayHitVertical == 1) //동서
	{	// 여기 중괄호(Braces)로 묶어야 한다. 안 묶으면 else를 인식 못함.
		if (info->player_x > rx) //서 (플레이어가 벽보다 x좌표가 클때.)
			tx= 63-tx;
	}
	else
		if (info->player_y < ry) //남 (플레이어가 벽보다 y좌표가 작을 때.)
			tx= 63-tx;
  • 배열 공식: (ty * Texture_size) + tx)
  • 서쪽과 남쪽을 따로 지정하고 이것들만 x배열을 뒤집었다.
  • 이제 벽의 네 방면 모두가 정상적으로 출력된다.

다 접고 다시 하는 중.

함수.

bresenham

void	bresenham(t_info *info, int startX, int startY, int finishX, int finishY, int color, int width)
{
	int x = startX;
	int y = startY;
	int Xfactor = finishX < startX ? -1 : 1;
	int Yfactor = finishY < startY ? -1 : 1;

	int w = abs(finishX - startX); // -300
	int h = abs(finishY - startY); // -200

	int f;

	for (int i = 0; i < width; i++)
	{
	if (w > h)
	{
		f = (2 * h) - w;
		for (x = startX; x != finishX; x += Xfactor)
		{
			if (f < 0)
			{
				f += 2 * h;
			}
			else
			{
				y += Yfactor;
				f += 2 * (h - w);
			}
			if (x >= 0 && x < info->window_x)
				my_mlx_pixel_put(info, x + i, y, color);
		}
	}
	else
	{
		f = (2 * w) - h;
		for (y = startY; y != finishY; y += Yfactor)
		{
			if (f < 0)
			{
				f += 2 * w;
			}
			else
			{
				x += Xfactor;
				f += 2 * (w - h);
			}
			if (y >= 0 && y < info->window_y)
				my_mlx_pixel_put(info, x + i, y, color);
		}
	}
	}
}
  • 시작 지점 (x, y)와 도착 지점 (x, y)를 입력하면 두 좌표를 잇는 선분을 그려준다. (색, 굵기 지정 가능)

bresen

t_pos	bresen(t_info *info, float startX, float startY, float pdx, float pdy)
{
	t_pos pos;

	float x = startX;
	float y = startY;
	float finishX = startX + pdx * 100;
	float finishY = startY + pdy * 100;
	float Xfactor = finishX < startX ? -1 : 1;
	float Yfactor = finishY < startY ? -1 : 1;

	float w = abs(finishX - startX); // -300
	float h = abs(finishY - startY); // -200

	float f;

	if (w > h)
	{
		f = (2 * h) - w;
		for (x = startX; x != finishX; x += Xfactor)
		{
			if (f < 0)
			{
				f += 2 * h;
			}
			else
			{
				y += Yfactor;
				f += 2 * (h - w);
			}
			if (map[((int)y / mapSize)*mapX + ((int)x / mapSize)] == 1)
			{
				pos.x = x;
				pos.y = y;
				return (pos);
			}
		}
	}
	else
	{
		f = (2 * w) - h;
		for (y = startY; y != finishY; y += Yfactor)
		{
			if (f < 0)
			{
				f += 2 * w;
			}
			else
			{
				x += Xfactor;
				f += 2 * (w - h);
			}
			if (map[((int)y / mapSize)*mapX + ((int)x / mapSize)] == 1)
			{
				pos.x = x;
				pos.y = y;
				return (pos);
			}
		}
	}
}
  • (x, y)시작 지점과 x증가량(pdx), y증가량(pdy)을 입력 받는다.
  • 이를 이용하면 쭉 뻗어나가는 광선을 구현할 수 있는데, 그 광선이 벽에 닿았을 때, 그 위치 정보(x, y)값을 리턴한다.
  • 참고로 pdy(y값의 증가량)/pdx(x값의 증가량)이 우리가 잘 아는 기울기이다.
	pdx = cos(pa) * 5;  // cos각도 * 변의 길이 = x증가량.
	pdy = sin(pa) * 5; // sin각도 * 변의 길이 = y증가량.
  • pdx, pdy의 변의 값은 임시로 5라고 지정했다. 즉 두 변수의 범위는 (-5 ~ 5) 사이라는 것.

각도의 개념.

  • 우리는 플레이어의 현재 위치(x, y)를 안다.
  • 우리는 플레이어가 바라보는 각도를 안다.
  • 여기서 말하는 각도(Angle)는 두 가지 개념이 있다.
  • 각도(Degrees), 라디안(Radians).
  • 파이는 3.141592....로 파이 하나당 180도를 담당한다.
  • 플레이어가 사방을 둘러보려면 360도가 되야하고, 즉 2파이가 필요하다.
  • 각도(Degrees)는 0 ~ 360도, 라디안(Radians)는 0 ~ 6.28이 있고, 우리는 코드에서 라디안을 사용한다.

시야 구현.

void	drawRays2D(t_info *info)
{
	float	ra = pa;
	int		color, color2;
	color = 0x00FF0000;  //빨간색
	color2 = 0x00FFFF00; //노랑색

	t_pos pos;

	//시야가 60이니까, 반시계 방향으로 30각도 만큼만 빼서 START!, 그래야 광선의 중심이 시야의 중심이 되니까.
	ra -= 30 * DR;
	for (int i = 0; i < 60; i++)
	{
		//px, py : 지도상에서 플레이어의 위치.
		//pdx pdy : x, y 증가값. (증가 위치는 임시로 5라고 했음. (딱히 상관 없다.))
        
        // ra(Radians)값이 6.28을 넘어가면 초기화.
		if (ra < 0) { ra+= 2* PI; } 
		if(ra > 2*PI) { ra-= 2 * PI; }
        
        //바뀐 ra에 따라서 pdx, pdy 값을 변경.
		float pdx_a = cos(ra) * 5;
		float pdy_a = sin(ra) * 5;

        //bresen 함수를 이용해, 광선이 벽에 닿은 위치를 계산, 밎 pos 변수에 저장.
		pos = bresen(info, info->player_x, info->player_y, pdx_a, pdy_a);
        
        //pos의 x, y 값을 이용해 선을 그린다.
		bresenham(info, info->player_x, info->player_y, pos.x, pos.y, color2, 1);
        
        //현재 Radians(파이각)이 6.28이므로 파이/360의 값인 DR을 1도로 가정하고 계산. DR = 0.017
		ra += DR; 
	}
}

현재 진행 중.

void	drawRays2D(t_info *info)
{
	float	ra = pa;
	int		color, color2, color3;
	color = 0x00FF0000;
	color2 = 0x00FFFF00;
	color3 = 0x00FFFFFF;


	t_pos pos;
	float tempX;
	float tempY;

	float disV;
	float disH;

	//시야가 60이니까, 반시계 방향으로 30각도 만큼만 빼서 START!
	ra -= 30 * DR;
	for (int i = 0; i < 60; i++)
	{
		//px, py : 지도상에서 플레이어의 위치.
		//pdx pdy : 플레이어 앵글의 각 x, y 증가값. (증가 위치는 임시로 +5라고 했음. (딱히 상관 없다.))

		//이거 초기화 해야함.
		float pdx_a = cos(ra) * 5;
		float pdy_a = sin(ra) * 5;

		pos = bresen(info, info->player_x, info->player_y, pdx_a, pdy_a);

		if (pos.y < info->player_y) //벽의 y위치가 플레이어보다 높이 있을 시.
			pos.y += 1;
		if (pos.x < info->player_x)
			pos.x += 1;

		if ((int)pos.x % mapSize == 0) //수직선 hit!
		{
			//bresenham(info, info->player_x, info->player_y, pos.x, pos.y, color, 1);
			disV = dist(info->player_x, info->player_y, pos.x, pos.y);
			disH = 0;
		}
		else if ((int)pos.y % mapSize == 0) //수평선 hit!
		{
			//bresenham(info, info->player_x, info->player_y, pos.x, pos.y, color2, 1);
			disH = dist(info->player_x, info->player_y, pos.x, pos.y);
			disV = 0;
		}

		//가끔 두 조건을 충족하는 경우 때문에 에러 발생.
		//그래서 이전에 사용한 좌표(tempX, tempY)와 비교해서 에러를 없앰.
		//다만 이 경우 i = 0, 즉 가장 첫번째는 비교할 temp가 존재하지 않아 에러 발생.
		//그래서 시야각을 61로 주고, 1부분의 시야각을 삭제하면 될 듯.
		if ((int)pos.x % mapSize == 0 && (int)pos.y % mapSize == 0)
		{
			if ((int)tempX % mapSize == 0)
			{
				//bresenham(info, info->player_x, info->player_y, pos.x, pos.y, color, 1);
				disV = dist(info->player_x, info->player_y, pos.x, pos.y);
				disH = 0;
			}
			else if((int)tempY % mapSize == 0)
			{
				//bresenham(info, info->player_x, info->player_y, pos.x, pos.y, color2, 1);
				disH = dist(info->player_x, info->player_y, pos.x, pos.y);
				disV = 0;
			}
		}
		tempX = pos.x;
		tempY = pos.y;


		//3D 구현.
		//위에서 구한 광선이 벽에 닿은 위치(pos.x, pos.y)를 이용해 3D 벽을 구현.

		//광선이 벽의 수직선(Vertical) 수평선(Horizontal)에 맞았는지 구분해야 한다.
		//disV, disH 중에 더 큰 것이 진짜 거리이므로 disT에 값을 입력.
		int		rayHitVertical = 0;
		float disT;
		if (disV > disH)
		{
			disT = disV;
			rayHitVertical = 1;
		}
		else if (disH > disV)
		{
			disT = disH;

		}


		// 벽의 뭉툭함을 없애줌.
		float ca = pa - ra;
		if (ca < 0) { ca+= 2* PI; }
		if (ca > 2*PI) { ca-= 2 * PI; }
		disT = disT * cos(ca);

		//벽의 높이, 오프셋 계산.
		float lineH = (mapSize * info->window_y)/disT;  //원래 640, 지금은 window_y

		//동서남북 구분하고 그에 따른 텍스트 넘버 부여.
		int texNum = 0;
		if (rayHitVertical == 1)
		{
			if (info->player_x < pos.x)
				texNum = 0;  //동
			else
				texNum = 1;  //서
		}
		else
		{
			if (info->player_y < pos.y)
				texNum = 2; //남
			else
				texNum = 3; //북
		}

		float tx, ty;
		float ty_off = 0;
		float ty_step = 64.0 / (float)lineH;

		//구현해야 할 벽의 크기가 윈도우 화면보다 클 시.
		//즉, 플레이어가 벽에 딱 붙었을 시.
		//너무 가까이 붙으면 벽의 윗부분과 아래는 시야에 보이지 않는다.
		//그래서 벽의 윗부분과 아랫부분을 버리는 작업.
		if (lineH >= info->window_y)
		{
			//lineH - info->window_y로 불필요한 벽의 윗부분을 버린다.
			//이후 2로 나눠서 불필요한 벽의 아래부분 마저 버린다.
			//결과적으로 필요한 벽의 중앙 부분만 남음.
			ty_off=(lineH-info->window_y) / 2;
            
			//구현해야 할 벽의 높이가 윈도우 화면보다 크면 안되므로 고정해준다.
			lineH = info->window_y;
		}
		float lineO = (info->window_y / 2) - lineH / 2;

		//ty는 텍스쳐 배열에서 y배열을 얼만큼 아래로 옳길지 결정.
		ty = ty_off*ty_step;

		//tx는 텍스쳐 배열에서 x배열을 얼만큼 아래로 옳길지 결정.
		if (rayHitVertical == 0) //광선이 수평선(Horizontal) 벽에 맞았으니, x좌표가 필요.
			tx = (int)pos.x % 64;
		else //광선이 수평선(Vertical) 벽에 맞았으니, y좌표가 필요.
			tx = (int)pos.y % 64;

		//벽을 구현.
		//i(시야 60) 만큼
		int xx = info->window_x / 60 - 1;
		for (int aa = 0; aa < lineH; aa++)
		{
			color = info->texture[texNum][(int)ty * texWidth + (int)tx];
			ty += ty_step;
			for (int bb = 0; bb < 16; bb++)
			{
				my_mlx_pixel_put(info, i * xx + bb , lineO + aa, color);
			}
		}

		//풀해상도 천장 바닥.
		bresenham(info, (i * 16), 0, (i * 16), lineO, 0x0040E0D0, 16);
		bresenham(info, (i * 16), lineH + lineO, (i * 16), info->window_y, 0x00FF0000, 16);

		ra += DR; //현재 Radians(파이각)이 6.28이므로 파이/360의 값인 DR을 1도로 가정하고 계산. DR = 0.017
		if (ra < 0) { ra+= 2* PI; } if(ra > 2*PI) { ra-= 2 * PI; }
	}
}

다 접고 lodev보고 벡터 기반으로 다시하는 중.

profile
신입

0개의 댓글