[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_RANGEplayer의 시야각을 나타냅니다. 무난하게 (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 rayt_god구조체에 포홤되어 있는 구조체로 광선이 벽과만나는좌표광선의 방향적인 요소를 담고 있는 구조체 입니다. 2nd_array_direction

  • 2D 지도2차원 배열형식으로 만들어져 있기 때문에 상하가 기존에 생각하던 방향과 반대로되어 있습니다. 삼각함수를 이용하여 올바르게 계산하기 위해서 "0도 ~ 180도"윗방향이아닌 facingDown(아래를 바라볼때)의 범위로 생각해야합니다.
  • ray_init함수는 이러한 t_ray구조체를 초기화해주는 함수입니다.


3️⃣ horizontal(수평), vertical(수직) 좌표 계산하기

  • 광선(ray)벽을 찾아주는 역할을 하도록 구현해야합니다.
  • 하지만 매 도트마다 벽인지 아닌지 확인하는 것은 비효율적입니다. 대신에 각 Tile의 경계면을 기준으로 확인하는 것이 좀 더 효율적입니다. wall_interface

  • 항상 꼭지점에서 광선이 만난다는 보장이 없기 때문에 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_in_map1

  • 최종적으로 광선(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의 값만 가지도록 자동으로 케스팅해주는 함수를 만들어 줬습니다.
  • 이제 어떠한 각도를 주어도 유효하게 광선을 발사해 주었습니다.

< 정상적으로 광선이 발사되는 모습 >

ray_in_map2




© 2021.02. by kirim

Powered by kkrim