고전게임 <Wolfenstein 3D>에서 영감을 받은 과제로 42 교육과정에서 제공하는 그래픽 라이브러리 mlx를 이용해 해당 게임의 일부를 구현하는 과제이다.
2D 엔진인 mlx를 사용해 3D 처럼 보이도록 화면을 출력하는 것을 목표로 한다.
2D 화면을 3D 처럼 보이도록 하는 것이 목표로 하지만 사실 평가지의 절반 정도를 맵 유효성 검사에 할당하고 있다.
subject에서 요구하는 유효한 맵의 기준은
1. 맵 파일의 확장자가 .cub 일 것
2. 벽의 텍스쳐 파일이 올바른 주소를 가리킬 것
3. 천장과 바닥의 색을 RGB 값으로 나타낼 것
4. 유효하지 않은 문자를 포함하지 말것
5. 맵은 벽으로 둘러싸여 있을 것
6. 캐릭터의 스폰 위치가 한 개만 존재할 것
그래서.. 이상적인 맵 파일은 다음과 같다
뭐 다른 항목들은 구현하기 귀찮을 뿐 그렇게 어렵지는 않다고 생각하지만 5번. 맵은 벽으로 둘러싸여 있어야 한다는 저 조건이 난관이었다.
앞서 했던 과제인 so_long처럼 맵이 사각형이라면 쉽겠지만 cub3d의 맵은 행마다 길이가 다 다르고, 벽과 벽 사이에 공백이 들어갈 수도 있다.
내가 생각한 방법은 strtok 함수를 사용해 맵 한 줄을 공백을 기준으로 자르고, 잘라낸 문자열의 첫 번째와 마지막 문자가 1인 경우 해당 문자열은 유효하다고 판단했다.
y축 검사는 맵의 줄 수만큼 문자열 배열을 할당해서 세로로 한 줄씩 복사해 검사하였다.
위의 예시 맵처럼 7번째 줄이후 처럼 맵이 짧은 경우에는 널 문자 대신 공백으로 넣어주었다.
위 빨간 사각형으로 표시한 부분을 예로 든다면 검사하기 위한 문자열은
"100001^^^^^1^^" 이 된다.
typedef struct s_game_info
{
int fd;
void *mlx;
void *win;
int **texture;
t_key key;
t_img img;
t_ray *ray;
t_img_info *imginfo;
t_map_info *mapinfo;
int buf[SCREEN_HEIGHT][SCREEN_WIDTH];
} t_game_info;
mlx에 대한 정보와 이미지 정보를 저장하는 구조체들이 존재하지만 이를 모두 설명하기에 너무 지엽적인것 같고, t_map_info 에 대한 설명만 하고, t_ray 구조체는 ray casting 파트에서 설명하도록 하겠다.
typedef struct s_map_info
{
int row;
int col;
double pos_x;
double pos_y;
double dir_x;
double dir_y;
double plane_x;
double plane_y;
char **map;
} t_map_info;
map 변수는 맵의 정보를 저장하고 있고, row는 해당 맵의 줄 수, col은 해당 맵에서 가장 긴 줄의 길이를 저장하고 있다.
pos_x와 pos_y는 캐릭터의 x, y 좌표이다.
dir_x와 dir_y는 캐릭터가 바라보고 있는 방향벡터를 의미한다.
plane_x와 plane_y는 카메라 평면의 방향벡터를 의미한다.
카메라 평면이란
위 이미지처럼 dir_x = 0 , dir_y = 1 을 가진다면 캐릭터는 남쪽을 바라보게 된다. 이 경우 캐릭터의 시야에 비치는 벽은 <- 방향으로 존재하고, 이를 방향벡터로 나타내면 x는 음수, y는 0이 된다.
플레이어의 시야각은 dir 벡터와 plane 벡터의 비율에 따라 결정되는데, cub3d의 모티브인 울펜슈타인 3D의 시야각은 66°이므로 plane 벡터의 값은 ±0.66으로 정해주었다.
레이 캐스팅에 대해 설명하기 위해 위키백과를 인용하자면
광선 투사(Ray casting)는 컴퓨터 그래픽스와 계산기하학의 다양한 문제를 해결하기 위해 광선과 표면의 교차검사를 사용하는 기법을 말한다.
이러한 레이 캐스팅을 우리는 플레이어 시점에서 광선을 발사하고, 맵의 벽과 충돌여부를 검사하여 충돌 지점에 맞춰 벽을 그리는데 사용할 것이다.
DDA는 Digital Diffferential Analyzer의 약자로 직선의 방정식을 이용한 선 긋기 알고리즘이다.
우리가 생각하는 직선은 붉은선이지만 디지털로 표현하려면 어쩔수 없이 근사값으로 픽셀을 찍을수 밖에 없는데, 어떠한 픽셀을 출력할 것인지 계산하는 것이 DDA 알고리즘이다.
우리는 저 붉은선을 광선, 각 픽셀을 맵에 대입하여 노란색으로 칠해진 사각형이 벽인지 검사할 것이다.
sideDist는 시작점에서 첫 번째 면을 만나는 점까지의 거리를 의미하고, deltaDist는 다음 점까지의 거리를 의미한다.
deltaDist는 광선의 방향벡터를 이용해 구할 수 있다.
deltaDistX = abs(|v| / rayDirX)
deltaDistY = abs(|v| / rayDirY)
그런데 우리는 정확한 값이 아닌 deltaDistX와 deltaDistY의 비율만 필요로 하기 때문에 v 값 대신 1을 넣어서 식을 간단히 할 수 있다.
화면에 벽을 출력하기 위해 화면 좌표가 벽 텍스쳐의 어떤 좌표에 대응되는지에 대한 계산이 필요하다.
먼저 광선이 벽의 어떤 위치에 부딛혔는지 알아내는 코드가 다음과 같고,
if (ray->side == 0)
wall_x = info->mapinfo->pos_y + ray->dist * ray->dir_y;
else
wall_x = info->mapinfo->pos_x + ray->dist * ray->dir_x;
wall_x -= floor(wall_x);
ray->tex_x = (int)(wall_x * (double)TEXT_WIDTH);
if (ray->img == 1 || ray->img == 2)
ray->tex_x = TEXT_WIDTH - ray->tex_x - 1;
ray->step = (float) TEXT_HEIGHT / ray->line_height;
ray->tex_pos = (ray->draw_start - SCREEN_HEIGHT / 2 + ray->line_height / 2) * ray->step;
벽의 1픽셀이 화면으로 나타낼 때는 몇 픽셀로 그려지는지에 대한 공식은 다음과 같다.
ray->step = (float) TEXT_HEIGHT / ray->line_height
위 방식으로 구해진 값들을 이용해 색을 추출해낸다.
ray->tex_pos = (ray->draw_start - SCREEN_HEIGHT / 2 + ray->line_height / 2) * ray->step;
ray->tex_y = (int)ray->tex_pos & (TEXT_HEIGHT - 1);
ray->tex_pos += ray->step;
color = info->texture[ray->img][TEXT_HEIGHT * ray->tex_y + ray->tex_x];
이렇게 캐릭터와 벽의 거리를 알게 되면 해당 벽을 어느정도 크기로 그려야하는지를 알 수 있는데, 해당 공식은 다음과 같다.
ray.line_height = (int)(SCREEN_HEIGHT / ray.dist);
ray.draw_start = -ray.line_height / 2 + SCREEN_HEIGHT / 2;
if (ray.draw_start < 0)
ray.draw_start = 0;
ray.draw_end = ray.line_height / 2 + SCREEN_HEIGHT / 2;
if (ray.draw_end >= SCREEN_HEIGHT)
ray.draw_end = SCREEN_HEIGHT - 1;
그려야 하는 벽의 높이를 알게되면 화면 중간지점을 기준으로 절반은 위에, 나머지 절반은 아래에 그려주어야 하므로 draw_start와 draw_end를 계산하는 공식이 위와 같다.
0부터 draw_start를 천장, draw_start부터 draw_end를 벽, draw_end부터 SCREEN_HEIGHT까지가 바닥에 해당한다.
while (++i < ray->draw_start)
info->buf[i][x] = info->imginfo->celling;
while (i < ray->draw_end)
{
ray->tex_y = (int)ray->tex_pos & (TEXT_HEIGHT - 1);
ray->tex_pos += ray->step;
color = info->texture[ray->img][TEXT_HEIGHT * ray->tex_y + ray->tex_x];
info->buf[i++][x] = color;
}
while (i < SCREEN_HEIGHT)
info->buf[i++][x] = info->imginfo->floor;