[cub3d](7)광선(ray) 구현하기
1️⃣ 목표
- 이번 목표는 미니맵상에서
플레이어(player)의 광선(ray) 을 구현하는 것이 목표입니다. - 이때
광선(ray) 은벽(wall) 을 통과하지 못합니다. - 즉, player부터 벽(wall)까지의
거리(distance) 를 구할 수도 있습니다. 이거리(distance) 것이 이번 목표에서 가장중요한 부분인데, 여기서 구한거리(distance) 는 향후에3D 로 이미지를 구현할 때 중요한 요소가 될 것입니다.
2️⃣ 임시 draw_ray함수 구현
/* cub3d.h */
#define RAY_RANGE (PI / 3.0)
#define RAY_COUNT 121 // It must be bigger than 2 and recommend odd numbers.
/* ray.c */
void draw_ray(t_god *god)
{
double angle;
double maxAngle;
angle = god->player.rotationAngle;
maxAngle = god->player.rotationAngle + (RAY_RANGE / 2.0);
while (angle <= maxAngle)
{
draw_one_ray(god, angle);
draw_one_ray(god, angle - (RAY_RANGE / 2.0));
angle += (RAY_RANGE / 2.0) / ((RAY_COUNT - 1) / 2.0);
}
}
RAY_RANGE
는 player의 시야각을 나타냅니다. 무난하게(PI / 3)
(60도)로 시야범위를 매크로로 정의했습니다.- player의
시야각도 는player.rotationAngle
입니다. 그렇기 때문에player.rotationAngle
을 기준으로 양옆으로시야(RAY_RANGE / 2)
까지가시야범위 가 됩니다. RAY_COUNT
는 광선의 갯수를 지정해줍니다. 하지만 만들어준while
문의 특성상 항상2개 이상의 의 광선이 그려집니다. (광선을 한개씩 그리도록 while문을 구성해도 되지만RAY_COUNT
를 의도적으로 2개이상 지정해주는 것은 어려운일이 아니기 때문에 그대로 나뒀습니다. + while문을 한번이라도 적게 돌리는 것이 성능향상에 도움이 될듯..)
2️⃣ draw_one_ray함수
(1) 임시 draw_one_ray함수 구현
void draw_one_ray(t_god *god, double angle)
{
t_dpable_ray horz;
t_dpable_ray vert;
ray_init(&god->ray, angle);
cal_horz_ray(god, &horz);
cal_vert_ray(god, &vert);
/* 코드 생략 */
}
- 위의
draw_one_ray
함수에는 다음과 같은 새로운 함수와 새로운 구조체 변수를 포함하고 있습니다.t_dpable_ray
구조체ray_init
함수cal_horz_ray
함수cal_vert_ray
함수draw_line
함수
(2) t_dpable_ray 구조체
typedef struct s_dpable_ray {
double xintercept;
double yintercept;
double xstep;
double ystep;
int found_wallHit;
double wall_hitX;
double wall_hitY;
double distance;
} t_dpable_ray;
- disposable은 “일회용”이라는 뜻으로 구조체이름을
t_dpable_ray
로 작명하였습니다. - 각각의 요소들은
광선을 한개 그릴 때만 유효한 요소들 입니다.
(3) ray_init함수
/* cub3d.h */
typedef struct s_ray {
double ray_angle;
double wall_hitX;
double wall_hitY;
double distance;
int wasHit_vertical;
int isRay_facingDown;
int isRay_facingUp;
int isRay_facingRight;
int isRay_facingLeft;
} t_ray;
typedef struct s_god {
/* 코드 생략 */
t_ray ray;
} t_god;
/* ray.c */
void ray_init(t_ray *ray, double rayAngle)
{
ray->ray_angle = rayAngle;
ray->wall_hitX = 0;
ray->wall_hitY = 0;
ray->distance = 0;
ray->wasHit_vertical = FALSE;
ray->isRay_facingDown = ray->ray_angle > 0 && ray->ray_angle < PI;
ray->isRay_facingUp = !ray->isRay_facingDown;
ray->isRay_facingRight = ray->ray_angle < 0.5 * PI || ray->ray_angle > 1.5 * PI;
ray->isRay_facingLeft = !ray->isRay_facingRight;
}
t_ray ray
는t_god
구조체에 포홤되어 있는 구조체로 광선이 벽과만나는좌표와 광선의 방향적인 요소를 담고 있는 구조체 입니다.- 2D 지도가 2차원 배열형식으로 만들어져 있기 때문에
상하가 기존에 생각하던 방향과 반대로 되어 있습니다. 삼각함수를 이용하여 올바르게 계산하기 위해서"0도 ~ 180도" 는 윗방향이아닌facingDown
(아래를 바라볼때)의 범위로 생각해야합니다. ray_init
함수는 이러한t_ray
구조체를 초기화해주는 함수입니다.
3️⃣ horizontal(수평), vertical(수직) 좌표 계산하기
광선(ray) 을 벽을 찾아주는 역할을 하도록 구현해야합니다.하지만
매 도트마다 벽인지 아닌지 확인하는 것 은 비효율적입니다. 대신에 각 Tile의 경계면을 기준으로 확인하는 것이 좀 더 효율적입니다.- 항상
꼭지점 에서 광선이 만난다는 보장이 없기 때문에horizontal(수평), vertical(수직) 방향의 경계선으로 나누어 각각의 경계좌표를 구합니다. - 그 중에서
거리가 짧은쪽 이최초로 벽을 만나는 지점 이 됩니다.
(1) horizontal(수평)계산함수 구현
< cal_horz_ray 함수 >
void cal_horz_ray(t_god *god, t_dpable_ray *horz)
{
horz->found_wallHit = FALSE;
horz->wall_hitX = 0;
horz->wall_hitY = 0;
horz->yintercept = floor(god->player.y / TILE_SIZE) * TILE_SIZE;
horz->yintercept += god->ray.isRay_facingDown ? TILE_SIZE : 0;
horz->xintercept = god->player.x + (horz->yintercept - god->player.y) / tan(god->ray.ray_angle);
horz->ystep = TILE_SIZE;
horz->ystep *= god->ray.isRay_facingUp ? -1 : 1;
horz->xstep = TILE_SIZE / tan(god->ray.ray_angle);
horz->xstep *= (god->ray.isRay_facingLeft && horz->xstep > 0) ? -1 : 1;
horz->xstep *= (god->ray.isRay_facingRight && horz->xstep < 0) ? -1 : 1;
cal_ray(god, horz);
}
yintercept
변수는player
가 최초로 horizontal(수평)경계면에 만나는 좌표 입니다.- 그 이후의 경계면좌표는
TILE_SIZE
만큼 일정하게 증가하게 됩니다. - 이때
바라보는 방향 에 따라 부호를 잘 조절해주어야합니다.
(2) vertical(수직)계산함수 구현
< cal_horz_ray 함수 >
void cal_vert_ray(t_god *god, t_dpable_ray *vert)
{
vert->found_wallHit = FALSE;
vert->wall_hitX = 0;
vert->wall_hitY = 0;
vert->xintercept = floor(god->player.x / TILE_SIZE) * TILE_SIZE;
vert->xintercept += god->ray.isRay_facingRight ? TILE_SIZE : 0;
vert->yintercept = god->player.y + (vert->xintercept - god->player.x) * tan(god->ray.ray_angle);
vert->xstep = TILE_SIZE;
vert->xstep *= god->ray.isRay_facingLeft ? -1 : 1;
vert->ystep = TILE_SIZE * tan(god->ray.ray_angle);
vert->ystep *= (god->ray.isRay_facingUp && vert->ystep > 0) ? -1 : 1;
vert->ystep *= (god->ray.isRay_facingDown && vert->ystep < 0) ? -1 : 1;
cal_ray(god, vert);
}
- horizontal(수평)경계좌표를 계산하는 함수와 거의 비슷한 로직입니다.
- 하지만 기준이되는 변수들이 다르기 때문에 따로 구성했습니다. (공통함수로 구현하면 매개변수가 너무 많아지기 때문에 따로 구현했습니다.)
(3) 수평, 수직 공통 계산함수 구현
< cal_ray 함수 >
void cal_ray(t_god *god, t_dpable_ray *hv)
{
double next_touchX;
double next_touchY;
next_touchX = hv->xintercept;
next_touchY = hv->yintercept;
while (next_touchX >= 0 && next_touchX <= WINDOW_WIDTH && next_touchY >= 0 && next_touchY <= WINDOW_HEIGHT) {
if (is_wall(next_touchX, next_touchY - (god->ray.isRay_facingUp ? 1 : 0))) {
hv->found_wallHit = TRUE;
hv->wall_hitX = next_touchX;
hv->wall_hitY = next_touchY;
break;
} else {
next_touchX += hv->xstep;
next_touchY += hv->ystep;
}
}
cal_distance(god, hv);
}
- 사실 실질적으로
벽의 위치를 찾는 하는 함수 입니다. cal_horz_ray함수
,cal_vert_ray함수
에서 xintercept, yintercept, xstep, ystep의 값이 정해지고cal_ray함수
에서 그 값들을 이용하여 계산을 합니다.cal_distance함수
를 이용하여 거리를 계산해줍니다.
(4) 거리구하는 함수
< cal_distance 함수 >
void cal_distance(t_god *god, t_dpable_ray *hv)
{
if (hv->found_wallHit == TRUE)
hv->distance = distance_between_points(god->player.x, god->player.y, hv->wall_hitX, hv->wall_hitY);
else
hv->distance = DBL_MAX;
}
< distance_between_points 함수 >
double distance_between_points(double x1, double y1, double x2, double y2)
{
return sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
}
- 벽을 찾았을 경우
distance_between_points함수
를 통해 거리를 계산하게됩니다. - 벽을 찾지 못할 경우
DBL_MAX
(double자료형의 최대값)을 지정해주었습니다. (수평경계좌표와 수직경계좌표를 비교할때 항상 선택받지 못하도록 하기 위해)
4️⃣ draw_one_ray함수 최종 구현
(1) draw_one_ray함수
void draw_one_ray(t_god *god, double angle)
{
t_dpable_ray horz;
t_dpable_ray vert;
ray_init(&god->ray, angle);
cal_horz_ray(god, &horz);
cal_vert_ray(god, &vert);
if (vert.distance < horz.distance) {
god->ray.wall_hitX = vert.wall_hitX;
god->ray.wall_hitY = vert.wall_hitY;
god->ray.distance = vert.distance;
god->ray.wasHit_vertical = TRUE;
} else {
god->ray.wall_hitX = horz.wall_hitX;
god->ray.wall_hitY = horz.wall_hitY;
god->ray.distance = horz.distance;
god->ray.wasHit_vertical = FALSE;
}
draw_line(god, god->player.x, god->player.y, god->ray.wall_hitX, god->ray.wall_hitY);
}
- 수평(horizontal) 경계좌표와 수직(vertical) 경계좌표를 모두 구한 뒤
if문 을 통해거리를 비교 하여 짧은쪽의 데이터들을ray구조체변수 에 저장해줍니다. - 이렇게 저장된 값들을
draw_line함수
을 이용하여최종적으로 한개의 광선을 그리게 됩니다.
(2) draw_line함수
void draw_line(t_god *god, double x1, double y1, double x2, double y2)
{
double ray_x;
double ray_y;
double dx;
double dy;
double max_value;
ray_x = god->player.x;
ray_y = god->player.y;
dx = x2 - x1;
dy = y2 - y1;
max_value = fmax(fabs(dx), fabs(dy));
dx /= max_value;
dy /= max_value;
while (1)
{
if (!is_wall(ray_x, ray_y))
god->img.data[WINDOW_WIDTH * (int)floor(ray_y) + (int)floor(ray_x)] = 0xFF0000;
else
break;
ray_x += dx;
ray_y += dy;
}
}
draw_line함수
는 좌표 2개를 인자로 받아 선을 그려주는 함수 입니다.도트단위 로 그려집니다. 하지만 기울기는 항상다르기때문에x좌표와 y좌표 중에 기준을 정해서 도트단위로 그려나가야 합니다. (기준이 되는쪽의 미분값이1
이 됩니다.)- 두 점의
x좌표와 y좌표 각각의 차이를 구하여 긴쪽을 기준으로 하는 것이광선(ray) 을 좀 더 세밀하게 그릴 수 있는 방법입니다. - while문을 통하여 벽을만날때까지
0xFF0000(빨간색)
으로 그려줍니다.
5️⃣ 광선(ray)가 최종구현 및 아쉬운점
(1) 문제발생
- 최종적으로 광선(ray)이 잘 구현되어 나타났습니다.
- 하지만 회전각이
360도
를 넘어가는 순간오류 가 생겼고 위와같이 광선이 발사 되었습니다.
(2) 문제해결(normalize_angle함수 구현)
-
각도 가
2PI 를 넘어가면 자동으로 케스팅되어 계산이될 줄알았지만 생각처럼되지 않았습니다.
double normalize_angle(double angle)
{
if (angle >= 0)
{
while (angle >= TWO_PI)
angle -= TWO_PI;
}
else
{
while (angle <= 0)
angle += TWO_PI;
}
return angle;
}
/* 수정 후 ray_init함수 */
void ray_init(t_ray *ray, double rayAngle)
{
ray->ray_angle = normalize_angle(rayAngle);
/* 코드 생략 */
}
- 위와같이
0 ~ 2PI
의 값만 가지도록 자동으로 케스팅해주는 함수를 만들어 줬습니다. - 이제 어떠한 각도를 주어도 유효하게 광선을 발사해 주었습니다.
< 정상적으로 광선이 발사되는 모습 >