원리 1 - 세팅
벡터
벡터는 구조체를 이용해서 생성하는게 좋다.
typedef struct s_vec_i
{
int x;
int y;
} t_vec_i
typedef struct s_vec_f
{
float x;
float y;
} t_vec_f
기본 벡터
다음 벡터는 맵에서의 위치에 따라 다르게 설정해야 한다.
- pos 벡터 : 지금 캐릭터의 위치.
- dir 벡터 : 캐릭터가 바라보고 있는 방향
- camera 벡터 : 카메라평면이라고도 하며, 스크린을 의미한다. 해당 벡터를 상수배해서 오른쪽과 왼쪽 전체를 표현한다
광선의 방향
광선의 방향 = (dir 벡터) + (camera 벡터) * n
이때 생기는 각도를 FOV(field of view)라고 한다.
FOV는 아무렇게나 해도 되지만, 보통 60도로 가는 경우가 많다.
따라서 이를 다음과 같은 코드로 작성할 수 있다.
- raydir 벡터 : 광선의 방향을 의미
raydir.x = dir.x + camera.x * n
raydir.y = dir.y + camera.y * n
원리 2 - 광선
광선 쏘기
우선 실제 광선의 위치를 나타내는 벡터를 만들어야 한다.
- sidedist 벡터 : 광선의 위치, 실수
- deltadist 벡터 : 광선의 변화량, 실수
sidedist에 계속 deltadist를 더해서 그것이 벽에 있는지 확인을 해야 한다.
벽에 부딫혔는지 판별하기 위해서는, 정수기반의 광선도 필요하다.
왜? 우리의 맵은 정수로 된 2차원 배열이기 때문에.
- map 벡터 : 광선의 위치, 정수
- step 벡터 : 광선의 변화량, 정수
그런데 벽에 부딫힌 경우, 실제로는 벽에 해당하는 배열의 끝에 도달했다고 할 수 있다.
따라서 진짜 광선의 거리는 (다 구한 sidedist) - (deltadist)를 해야한다.
광선 공식
광선의 위치는 다음과 같이 초기화 해준다.
map = pos; # 정수위치
sidedist = (pos - map) * deltadist # 실수위치
광선의 변화량은 광선의 방향을 이용해 다음과 같이 초기화한다.
어떻게 보면 raydir 벡터의 norm으로 해석할 수도 있다.
deltadist.x = abs(1 / raydir.x);
deltadist.y = abs(1 / raydir.y);
# 왜 1인가? 실제 raydir 벡터의 norm을 그대로 쓰면 어안렌즈 효과가 나타날 수 밖에 없다.
다음 공식을 통해 광선의 위치에 계속 변화량을 더한다.
sidedist가 x에 닿을지, y에 닿을지를 계속 보면서 더 가까운 쪽을 더한다고 생각하면 된다.
(이는 결국 DDA 알고리즘의 응용이라고 볼 수 있다.)
side 값은 내가 부딫힌 위치가 동/서 인지 남/북인지를 보여준다. 나중에 그릴 때 쓰게 된다.
while hit == 0:
if sidedist.x < sidedist.y:
sidedist.x += deltadist.x
map.x += step.x
side = 0;
else
sidedist.y += deltadist.y
map.y += step.y
side = 1;
if (map[map.y][map.x] > 0)
hit = 1
그렇다면 이 while문을 빠져나간 상황에서, 과연 광선은 제대로 된 위치에 있는 것인가?
그렇지 않다. 왜냐하면 이 거리는 말 그대로 벽의 끝에 있기 때문이다.
따라서 벽의 앞 쪽으로 거리를 땡겨와야 한다.
perpedist = sidedist - deltadist
- perpedist : 실제 광선의 길이. (perpendicular distance의 약자)
원리 3 - 벽 그리기
지금까지 원리1, 원리2에서 했던 부분을 다음과 같이 함수로 넣어야 한다.
우리는 벽의 위치를 정하기 위해 광선을 쏜 것이고, 이는 y값만 정한 것이라고 할 수 있다.
x값은 해당 함수 밖에서 while 문으로 돌리면서 조정해야 한다.
또한 카메라벡터에 씌울 이미지와 텍스쳐의 이미지를 구분하는 것이 좋다.
float camera_x;
t_data camera_image;
x = 0;
camera_x = 0;
drawing_init(tools, &camera_image);
while (x < 전체 창의 세로길이)
{
camera_x = 2 * x / (float)WIDTH - 1;
raycast_vector_init(tools, camera_x); // 원리1
raycast_sidedist_init(tools); // 원리2
raycast_shoot_light(tools, tools->arr_map); // 원리2
raycast_draw_line(tools, &camera_image, x); // 원리3
x++;
}
mlx_clear_window(tools->mlx_ptr, tools->win_ptr);
mlx_put_image_to_window(tools->mlx_ptr, tools->win_ptr, \
camera_image.img, 0, 0);
mlx_destroy_image(tools->mlx_ptr, camera_image.img);
벽 그리기
raycast_draw_line()으로 들어와서 해야 할 일이다.
다음 값들은 전부 스칼라이다.
- line_height : 벽의 높이. 전체 창의 크기를 perpedist로 나눈다.
- draw_start : 벽을 그리는 처음 위치.
- draw_end : 벽을 그리는 마지막 위치
line_height = (전체 창의 세로길이 / perpedist) / 2
# 2로 나눈 값을 쓴다면 창이 좀 넓게 보일 것이다.
draw_start = (전체 창 세로길이 / 2) - (line_height / 2)
draw_end = (전체 창 세로길이 / 2) + (line_height / 2)
이제 진짜로 벽을 그려보자.
단, 주의할 점은 side의 값과 raydir 벡터 속 값에 따라 동서남북이 결정된다는 것이다.
그래서 다음과 같이 설정을 해준다.
if (tools->side == 0 && tools->raydir.x < 0)
ft_draw_line(tools, camera_image, &tools->img_east, x);
else if (tools->side == 0 && tools->raydir.x > 0)
ft_draw_line(tools, camera_image, &tools->img_west, x);
if (tools->side == 1 && tools->raydir.y < 0)
ft_draw_line(tools, camera_image, &tools->img_north, x);
else if (tools->side == 1 && tools->raydir.y > 0)
ft_draw_line(tools, camera_image, &tools->img_south, x);
벽을 그리려면, 우선적으로 텍스쳐의 위치를 찾는 벡터를 만들어야 한다.
- texture : 텍스쳐의 위치를 담는 벡터
일단 texture 벡터의 x값은 다음과 같이 설정한다. 물론, side에 따라 다르다.
- 나의 위치 + 광선의 길이 * 광선의 방향
- 1에서 구한 값에서 (int)로 캐스팅한 자기 자신을 뺀다.
- 여기에 실제 텍스쳐의 width 값을 곱한다.
void set_texture_vector(t_box *tools, t_data *image, t_vec_f *texture)
{
if (tools->side == 0)
{
texture->x = tools->pos.y + tools->perpwalldist * tools->raydir.y;
texture->x -= (int)texture->x;
texture->x *= (float)image->width;
}
if (tools->side == 1)
{
texture->x = tools->pos.x + tools->perpwalldist * tools->raydir.x;
texture->x -= (int)texture->x;
texture->x *= (float)image->width;
}
}
texture 벡터의 y값은 다음과 같이 구한다.
texture->dy = (float)image->height / (float)tools->line_height;
tools->texture.y = (tools->draw_start - HEIGHT / 2 + \
tools->line_height / 2) * tools->texture.dy;
- y의 변화량을 먼저 구한다. 이는 텍스쳐의 실제 세로 길이를 line_height 값으로 나눈 값이다.
- ( draw_start - 전체 세로 길이 / 2 + line_height / 2 ) 에 y의 변화량을 곱한다.
인제 while문을 돌려가며 그리기를 한다.
while (tools->draw_start <= tools->draw_end)
{
color = get_color_of_texture(wall_image, &tools->texture);
my_mlx_pixel_put(camera_image, x, tools->draw_start, color);
(tools->texture.y) += (tools->texture.dy);
if (tools->texture.y >= wall_image->height)
tools->texture.y = wall_image->height - 1;
(tools->draw_start)++;
}
// get_color와 my_mlx_pixel_put은 mlx 문서를 참고.