cub3d 진행과정
조금 느리지만 vnc환경에서 진행해 볼 생각이다.
맥북을 사용할 수 있어 빠르게 끝낼 생각이다.
프로젝트 개요
이 프로젝트는 레이캐스팅을 이용한다
90년대 첫번째 FPS게임에서 영감을 받은 프로젝트이다.
미로를 다양하게 바라볼 수 있는게 목표이다.
프로젝트 내용
- 이 프로젝트는 엄격함(완벽히 구현), C언어의 사용, 기본 알고리즘 이용, 정보 검색이 목표이다.
- 그래픽 설계 프로젝트로, windows, colors, events, fill shapes, etc등의 실력을 기를 수 있을 것이다.
- cub3d를 끝내는 것은 구체적인 내용의 이해 없이 수학의 응용을 탐구해 볼 수 있는 좋은 기회이다.
- 인터넷에 있는 수많은 자료와 수학을 도구로써, 멋지고 효율적인 알고리즘을 만들 수 있을것이다.
프로젝트 진행 과정
환경세팅
cp minilibx_opengl_20191021/*.h /usr/local/include/
cp minilibx_opengl_20191021/libmlx.a /usr/local/lib/
빌드 명령어
gcc main.c -lmlx -framework OpenGL -framework AppKit
색 코드는
https://encycolorpedia.kr/
이곳에서 변환하자
mlx_pixel_put()함수가 있는데
왜 mlx_new_image로 메모리에 이미지를 저장해야 할까?
이미지를 창에 띄우는 작업은 많은 연산을 요한다, 하나의 점씩 일일히 윈도우에 띄우는것보다 이미지 자체를 메모리에 저장하고, 이것을 한번에 윈도우에 띄우는게 시간적으로 효율적이다.
이미지 띄우는 과정
- 윈도우 창을 만든다 (mlx_init() -> mlx_new_window())
- 이미지를 만든다.(마치 mlx_init처럼) (mlx_new_image())
- 이미지의 데이터의 포인터를 받아온다.(데이터 접근 및 수정, 이미지를 채운다)(mlx_get_data_addr())
- 이미지를 띄운다(mlx_put_image_to_window)
- 창을 돌린다.(mlx_loop)
#include <mlx.h>
#ifndef EXAMPLE_H
# define EXAMPLE_H
# include <math.h>
/*
Defines for the width and height of your window. I suggest you to do the same so
you can change easily the size of your window later if needed.
*/
# define WIN_WIDTH 800
# define WIN_HEIGHT 600
/*
Here I built a struct of the MLX image :
It contains everything you need.
- img_ptr to store the return value of mlx_new_image
- data to store the return value of mlx_get_data_addr
- the 3 other variables are pretty much useless, but you'll need
them in the prototype of mlx_get_data_addr (see the man page for that)
*/
typedef struct s_img
{
void *img_ptr;
int *data; //Here you got an int * even though mlx_get_data_addr returns
//a char *, i'll talk more about this in the .c file.
//Here are the 3 "useless" variables. You can find more informations about these in the man page.
int size_l;
int bpp;
int endian;
} t_img;
/*
Here is my main struct containing every variables needed by the MLX.
- mlx_ptr stores the return value of mlx_init
- win stores the return value of mlx_new_window
- img will store everything we need for the image part, the struct is described above.
*/
typedef struct s_mlx
{
void *mlx_ptr;
void *win;
t_img img;
} t_mlx;
#endif
int main()
{
t_mlx mlx;
int count_w;
int count_h;
count_h = - 1;
mlx.mlx_ptr = mlx_init();
mlx.win = mlx_new_window(mlx.mlx_ptr, 800, 600, "test");
mlx.img.img_ptr = mlx_new_image(mlx.mlx_ptr, 400, 300);
mlx.img.data = (int*)mlx_get_data_addr(mlx.img.img_ptr, &mlx.img.bpp, &mlx.img.size_l, &mlx.img.endian);
while (++count_h < WIN_HEIGHT / 2)
{
count_w = -1;
while (++count_w < WIN_WIDTH / 2)
{
if (1)
/*
As you can see here instead of using the mlx_put_pixel function
I just assign a color to each pixel one by one in the image,
and the image will be printed in one time at the end of the loop.
Now one thing to understand here is that you're working on a 1-dimensional
array, while your window is (obviously) 2-dimensional.
So, instead of having data[height][width] here you'll have the following
formula : [current height * max width + current width] (as you can see below)
*/
mlx.img.data[count_h * (WIN_WIDTH / 2) + count_w] = 0xFF7F00;//0xFFFFFF;
else
mlx.img.data[count_h * (WIN_WIDTH / 2) + count_w] = 0;
}
}
//Now you just have to print the image using mlx_put_image_to_window !
mlx_put_image_to_window(mlx.mlx_ptr, mlx.win, mlx.img.img_ptr, 0, 0);
mlx_loop(mlx.mlx_ptr);
}
윈도우의 1/4를 주황색으로 채워보았다.
여기서
bpp는 32이다. RGBA->char 4개 -> 8 *4비트
size_line은 3200이다. 800(width) * 4(RGBA)
endian은 컴퓨터 구조에 따라결정된다. 0 or 1
레이캐스팅의 이해
레이캐스팅은 2D 맵을 3D처럼 보이게 렌더링 하는 기술이다.
컴퓨터 성능이 좋지 않던 시절, 연산을 최소화하기 위해 만들어졌다.
모든 벽은 같은 높이를 가진다.
계단, 점프 , 높이 차이는 이 엔진으로 만들 수 없다.(보너스는 과감히 포기하겠다..)
기본 아이디어
https://github.com/365kim/raycasting_tutorial
미혜님이 번역해 주신 글을 참고했다.
2D 사각형 맵, 0은 빈공간, 다른값은 벽으로 이용한다. (2차원 배열)
화면의 모든 x(모든 수직 선)에 대해 유저의 위치에서 시작되는 유저의 방향과 화면의 x 좌표에 따라 레이저를 쏜다.
그리고 광선이 벽에 닿으면 플레이어와 지점간 거리를 계산하고 벽을 얼만큼 크게 그려야 하는지 계산한다.
플레이어가 몸을 돌리는것은 벡터를 이용하여 계산한다.(회전행렬을 곱해서)
레이저를 쏘는 방식 -> DDA 알고리즘,
직선 방정식의 기울기를 구해서
기울기의 절대값이 1보다 작으면 x는 1씩 증가시키면서 y에는 기울기를 더하고, y값은 실수이므로 반올림을한다. 반대로 기울기의 절대값이 1보다 크면 y를 1씩 증가시키면서 x를 기울기의 역수만큼 더하고, x를 반올림한다.
구현 과정
우선 키보드 입력을 받아 상하좌우와 방향회전을 구현해 보았다.
int key_press_event_func(int keycode, t_mlx *mlx)
{
if (keycode >= 300)
return (-1);
mlx->key_check[keycode] = 1;
return (0);
}
int key_release_event_func(int keycode, t_mlx *mlx)
{
if (keycode >= 300)
return (-1);
mlx->key_check[keycode] = 0;
return (0);
}
이런식으로 함수를 짜서 동시 키입력을 받아준다. 300은 키코드 값이 배열 범위를 넘어섰을때 에러처리이다.
가상의 맵을 그려보았다.
저 글과 다르게 유클리드 각도를 이용해서 구현해보았다.
각 맵의 벽은 실제에선 64x64크기로 변환하였다.
벽의 색깔을 수직, 수평선 마다 다르게 해보았다.
이제 벽에 텍스쳐를 그리는 알고리즘을 넣어보자
보간하는 알고리즘을 멋대로 짰더니 텍스쳐가 중간중간 제대로 나오지 않는 모습이 보인다.
에초에 보간을 할 필요가 없이 가로 픽셀 수 만큼 레이져를 쏴야 맵이 정상적으로 보인다
텍스쳐 알고리즘을 수정하였다. y축, x축 모두 보정하였다.
하늘과 바닥은 텍스쳐를 넣기가 까다로워서 단일 색상으로 정했다.(보너스 하기를 포기하였다.)
xpm 파일을 불러와서 벽으로 생성시키는데 성공하였다.
이제 sprite를 표시하고 맵을 파싱하고 함수만 모듈화 시키면 완료될거 같다.
sprite 생각 구현방법 -> sprite 이미지도 64x64로 받아와서 완전한 검은색, 즉 값이 0인 곳은 이미지를 그리지 않는 방식으로 구현해보겠다.
우선 어느 각도로 보아도 항아리이게 바꿔야겠다..
조금 복잡할 것 같다.
나의 생각
- 스프라이트의 중심점 좌표와 플레이어간의 거리를 구한다.
- 레이저와 스프라이트의 중심점 좌표 거리를 구한다.
- 두 거리를 비교해서 스프라이트를 그려야 하는지 판별한다.
- 현재 플레이어에서 스프라이트까지의 각도와 레이저에서 스프라이트 까지의 각도를 비교하여 스프라이트에서 어느 픽셀을 그릴지 정한다.
2일간의 고생 끝에 스프라이트를 그릴 수 있게 되었다…
맵을 파싱 한 후 벽을 구현하였다. 생각해 보니까 보간을 할 필요가 없이 가로 픽셀 수 만큼 레이져를 쏴야 맵이 정상적으로 보인다 ..
천장과 바닥을 구현하였다.
이제 맵 유효성 검사를 구현하자
모든 0에서 검사 > 배열의 가장자리에 접근되거나 중간에 -1값을 만나면 유효하지 않음
–완료 –
문제가 생겼다 스프라이트가 중첩되었을시 마지막에 레이져에 맞은것만 표시된다
레이져가 스프라이트에 닿을때마다 리스트에 추가한후, 거리별로 소트하고 벽보다 거리가 적을때만 순서대로 표시한다
이제 보너스를 하고, 내가 그린 그림을 표시해보자
애니메이션, 체력바, 거리에 따른 명암조절, 점프, 등을 구현해보았다
-
최종 구현 방법
- 레이캐스팅
-
mlx라이브러리를 익힌다.
-
DDA알고리즘으로 레이저를 쏜다.
-
수평선과 수직선중의 distance가 더 가까운 점을 맞은 걸로 한다.
-
distance에 비례해서 이미지의 height를 결정한다.
- 텍스쳐
- 레이저가 맞은 좌표와 벽 좌표를 비교해서 텍스쳐의 어떤 부분을 출력할지 정한다. 레이져는 전방 60도에 흩뿌리며 레이져 개수는 화면의 width이다.
- 스프라이트
- 플레이어와 스프라이트까지의 각도와 플레이어와 스프라이트에 맞은 레이저 좌표의 각도를 비교해서 텍스쳐를 정한다.
- 리스트에 레이져의 맞은 스프라이트를 구조체로 저장하여 넣어논다.(필요한 변수들을 넣어서)
- 리스트를 거리순으로 sort하여 덮어서 그린다.
- 그릴때 스프라이트 이미지 값이 0인곳은 그리지 않는다.
-
보너스
죽었을땐 세상이 반전된다.
🖌혹시나 cub3D 각도기반으로 구현하시다가 알고리즘이 이해가 되지 않으시는 경우 @dochoi에게 DM을 주세요
++++
아이디어를 그림으로 그려봤습니다.