열렬히.뛰기

2. 고정된 이미지 출력

école 42 > cub3D > 진행상황 > 2. 고정된 이미지 출력

원리 1 - 세팅

벡터

벡터는 구조체를 이용해서 생성하는게 좋다.

c
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 벡터 : 광선의 방향을 의미
javascript
raydir.x = dir.x + camera.x * n
raydir.y = dir.y + camera.y * n

원리 2 - 광선

광선 쏘기

우선 실제 광선의 위치를 나타내는 벡터를 만들어야 한다.

  • sidedist 벡터 : 광선의 위치, 실수
  • deltadist 벡터 : 광선의 변화량, 실수

sidedist에 계속 deltadist를 더해서 그것이 벽에 있는지 확인을 해야 한다.

벽에 부딫혔는지 판별하기 위해서는, 정수기반의 광선도 필요하다.

왜? 우리의 맵은 정수로 된 2차원 배열이기 때문에.

  • map 벡터 : 광선의 위치, 정수
  • step 벡터 : 광선의 변화량, 정수

그런데 벽에 부딫힌 경우, 실제로는 벽에 해당하는 배열의 끝에 도달했다고 할 수 있다.

따라서 진짜 광선의 거리는 (다 구한 sidedist) - (deltadist)를 해야한다.

광선 공식

광선의 위치는 다음과 같이 초기화 해준다.

python
map = pos; # 정수위치
sidedist = (pos - map) * deltadist # 실수위치

광선의 변화량은 광선의 방향을 이용해 다음과 같이 초기화한다.

어떻게 보면 raydir 벡터의 norm으로 해석할 수도 있다.

python
deltadist.x = abs(1 / raydir.x);
deltadist.y = abs(1 / raydir.y);

# 왜 1인가? 실제 raydir 벡터의 norm을 그대로 쓰면 어안렌즈 효과가 나타날 수 밖에 없다.

다음 공식을 통해 광선의 위치에 계속 변화량을 더한다.

sidedist가 x에 닿을지, y에 닿을지를 계속 보면서 더 가까운 쪽을 더한다고 생각하면 된다.

(이는 결국 DDA 알고리즘의 응용이라고 볼 수 있다.)

side 값은 내가 부딫힌 위치가 동/서 인지 남/북인지를 보여준다. 나중에 그릴 때 쓰게 된다.

python
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문을 빠져나간 상황에서, 과연 광선은 제대로 된 위치에 있는 것인가?

그렇지 않다. 왜냐하면 이 거리는 말 그대로 벽의 끝에 있기 때문이다.

따라서 벽의 앞 쪽으로 거리를 땡겨와야 한다.

python
perpedist = sidedist - deltadist
  • perpedist : 실제 광선의 길이. (perpendicular distance의 약자)

원리 3 - 벽 그리기

지금까지 원리1, 원리2에서 했던 부분을 다음과 같이 함수로 넣어야 한다.

우리는 벽의 위치를 정하기 위해 광선을 쏜 것이고, 이는 y값만 정한 것이라고 할 수 있다.

x값은 해당 함수 밖에서 while 문으로 돌리면서 조정해야 한다.

또한 카메라벡터에 씌울 이미지와 텍스쳐의 이미지를 구분하는 것이 좋다.

c
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 : 벽을 그리는 마지막 위치
python
line_height = (전체 창의 세로길이 / perpedist) / 2 
# 2로 나눈 값을 쓴다면 창이 좀 넓게 보일 것이다.

draw_start = (전체 창 세로길이 / 2) - (line_height / 2)
draw_end = (전체 창 세로길이 / 2) + (line_height / 2)

이제 진짜로 벽을 그려보자.

단, 주의할 점은 side의 값과 raydir 벡터 속 값에 따라 동서남북이 결정된다는 것이다.

그래서 다음과 같이 설정을 해준다.

c
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. 나의 위치 + 광선의 길이 * 광선의 방향
  2. 1에서 구한 값에서 (int)로 캐스팅한 자기 자신을 뺀다.
  3. 여기에 실제 텍스쳐의 width 값을 곱한다.
c
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값은 다음과 같이 구한다.

c
texture->dy = (float)image->height / (float)tools->line_height;
tools->texture.y = (tools->draw_start - HEIGHT / 2 + \
				tools->line_height / 2) * tools->texture.dy;
  1. y의 변화량을 먼저 구한다. 이는 텍스쳐의 실제 세로 길이를 line_height 값으로 나눈 값이다.
  2. ( draw_start - 전체 세로 길이 / 2 + line_height / 2 ) 에 y의 변화량을 곱한다.

인제 while문을 돌려가며 그리기를 한다.

c
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 문서를 참고.