[cub3d](15)sprite구현하기



1️⃣ 목표

  • 이번엔 sprite(스프라이트)를 구현할 예정입니다.
  • sprite(스프라이트)를 입체적으로 구현하는 것이 아니기 때문에 어느 방향에서 보든지 앞부분만 보이게 됩니다.


2️⃣ 원리생각해보기

  • 지금까지 구현한 랜더링은 다음과 같습니다.
    1. 지도그리기
    2. 지도상에 player 그리기
    3. 지도상에 광선(ray) 그리기
    4. 3D벽그리기
  • 위의 그리기과정 중에서도 sprite(스프라이트)는 특히 3D벽그리기와 조화롭게 그려져야 합니다.

(1) 광선으로 탐색할때마다 그리기?

  • 3D벽광선(ray)을 왼쪽부터 하나씩 그릴때마다 데이터를 얻어 그리도록 구현했습니다.
  • 광선이 sprite를 탐지하는 범위를 sprite의 너비로 지정하여 그린다고 생각해보겠습니다. 그렇다면 광선이 sprite를 모두 탐색한 직후 그려주는 것이 정확할 것 입니다.
  • 하지만 광선을 한번 발사해서 얻은데이터는 다음번의 광선때까지만 유효합니다. 그렇기 때문에 이런식이라면 sprite를 처음 발견하는 순간의 데이터들만 이용해서 그려야합니다.
  • 위와같은 방법은 바라보는 각도, 스프라이트까지의 거리를 이용하면 어느정도 계산이 가능합니다. 하지만 계산과정이 너무 복잡하고 성능저하와 오차가 매우 심해집니다.
  • 결국 다른 방법을 찾아봐야될 것 같습니다.

(2) sprite거리에 따라 그리기?

  • 물리적으로 완전히 정확하게 원근감을 표현할 수 있으면 좋겠지만 성능을 고려하면 어느정도 타협이 필요할 것 같습니다.
  • 대신에 "sprite와 player사이의 거리"만을 변수로 사용하여 그려주는 것이 좋을 것 같습니다.
  • 이렇게되면 한줄씩 그리는 3D벽한번에 그리는 sprite조화롭게 그려지지 않을 것 같지만 일단 진행해보도록 하겠습니다.


3️⃣ sprite(스프라이트) 파싱하기

  • sprite(스프라이트)에 쓰일 파일은 .cub파일에 다음과 같이 작성됩니다.
S  texture/zergling.xpm
  • sprite의 데이터들을 저장하기위해 t_sprite구조체를 만들어 줬습니다.
  • 위의 .cub파일은 기존의 파싱함수를 통해 정상적으로 텍스트구조체에 저장됩니다.
  • 또한 지도(map)에서 2부분이 스프라이트를 가리킵니다. 파싱체크함수에서 2가 발견될 시 다음과 같은 함수를 호출해 줍니다.
static int put_sprite(t_god *god, int row, int col, int symbol)
{
    int cnt;

    cnt = 0;
    while (god->sprite[cnt].exist == TRUE)
    {
        cnt++;
        if (cnt >= SPRITE_COUNT)
            return (ERROR);
    }
    god->sprite[cnt].x = col * TILE_SIZE + TILE_SIZE / 2;
    god->sprite[cnt].y = row * TILE_SIZE + TILE_SIZE / 2;
    god->sprite[cnt].exist = TRUE;
    god->sprite->symbol = symbol;

    return (SUCCESS);
}
  • sprite구조체t_god구조체에 배열형태로 저장되어 있습니다. 이 배열의 크기는 사용자가 직접 정해주도록 매크로형식SPRITE_COUNT으로 만들어줬습니다.
  • 기존의 광선(ray)을 그리면서 탐색하는 방식은 비효율 적이였습니다. 그렇기 때문에 파싱과 동시에 스프라이트 데이터들을 설정하도록 했습니다. (x, y, exist, symbol)
  • 스프라이트의 종류가 여러개일 수 있기 때문에 symbol변수를 통해 구분해주도록 했습니다.
  • exist변수는 단순히 배열의 해당인덱스가 사용중인지 아닌지를 나타냅니다.


4️⃣ sprite 랜더링하기

(1) sprite데이터 초기화하기

void    init_sprite(t_god *god, t_3d *v, t_sprite *sprite)
{
    sprite->angle = cal_degree(god, *sprite);

    sprite.distance = distance_between_points(god->player.x, god->player.y, sprite.x, sprite.y);
    v->correct_distance = sprite->distance * cos(sprite->angle);
    v->distance_plane = (god->map.window_width / 2) / tan(FOV_ANGLE / 2);
    v->projected_height = (int)((TILE_SIZE / v->correct_distance) * v->distance_plane);
    v->width = v->projected_height;
    
    v->top = (god->map.window_height / 2) - (v->projected_height / 2) - god->player.updown_sight;
    v->correct_top = v->top < 0 ? 0 : v->top;

    v->bottom = (god->map.window_height / 2) + (v->projected_height / 2) - god->player.updown_sight;
    v->correct_bottom = v->bottom > god->map.window_height ? god->map.window_height : v->bottom;

    double spriteAngle = atan2(sprite->y - god->player.y, sprite->x - god->player.x) - normalize_angle(god->player.rotationAngle);
    sprite->start = tan(spriteAngle) * v->distance_plane;

    sprite->left_x = (god->map.window_width / 2) + sprite->start - (god->parse.tex[T_S].width / 2);
    sprite->right_x = sprite->left_x + v->width;
}
  • 먼저 데이터들을 알맞게 초기화해주는 함수입니다.
  • 3D벽 랜더링에서 초기화해주는 함수의 원리를 그대로 이용하였습니다. 3D벽을 그릴때는 거리에 따라서 높이를 지정해주었습니다.sprite랜더링에서는 추가적으로 너비까지 지정했는데 단순히 높이와 같은 값을 갖습니다.
  • 추가적으로 각도를 구해줘야합니다. 각도는 다음에 사용됩니다.
    1. 어안렌즈효과를 보정해주기위해 사용
    2. FOV_ANGLE(시야각: 60도)에 들었는지를 판별하여 그려줄지말지를 결정하는데 사용 sprite_angle
  • 각도플레이어가 바라보는 각도(rotationAngle), sprite좌표, player좌표삼각함수를 이용하여 계산하면 간단하게 구할 수 있습니다.
  • 하지만 플레이어가 바라보는 각도(rotationAngle)를 잘 보정해주어야 합니다. 다음과 같은 함수를 통해 보정하여 계산해 주었습니다.
double  norm_angle(double angle)
{
    if (angle > PI)
            angle -= TWO_PI;
    else if (angle < -1 * PI)
            angle += TWO_PI;
    return (fabs(angle));
}
double    cal_degree(t_god *god, t_sprite sprite)
{
    double angle;
    angle = normalize_angle(god->player.rotationAngle) - atan2(sprite.y - god->player.y, sprite.x - god->player.x);
    return (norm_angle(angle));
}

(2) sprite그리기

void render_sprite(t_god *god)
{
    int     i;

    for (int j = 0; god->sprite[j].exist == TRUE && j < SPRITE_COUNT ; j++)
    {
        init_sprite(god, &god->sprite[j].v, &god->sprite[j]);
        if (god->sprite[j].angle < FOV_ANGLE / 2 + 1)
        {
            i = 0;
            for (int y = god->sprite[j].v.correct_top; y < god->sprite[j].v.correct_bottom; y++)
            {
                for (int x = god->sprite[j].left_x; x < god->sprite[j].right_x; x++)
                    if (x > 0 && x < god->map.window_width && y > 0 && y < god->map.window_height)
                        if (god->img.data[god->map.window_width * y + x] == NO_COLOR)
                            god->img.data[god->map.window_width * y + x] = set_sprite_color(god, &god->sprite[j], i, x - god->sprite[j].left_x);
                i++;
            }
        }
    }
}
  • 반복문을 이용하여 x축, y축을 한번에 그려줍니다. 단, FOV_ANGLE / 2보다 스프라이트 각도가 작을때만 그려줍니다. 화면 끝쪽에서 갑자기 사라지는 현상이 있어서 1의 보정값을 주어 FOV_ANGLE / 2 + 1보다 작을때 그려주도록 해주었습니다.
  • 추가로 모든sprite인덱스를 탐색하여 이 과정을 반복해 줍니다.
int     set_sprite_color(t_god *god, t_sprite *sprite, int i, int k)
{
    int row;
    int x;
    t_3d v;
    int symbol;

    symbol = sprite->symbol;
    v = sprite->v;
    x = ((k * god->parse.tex[symbol].width)) / v.width;
    row = (((v.correct_top - v.top + i) * god->parse.tex[symbol].height) / v.projected_height);
    return god->parse.tex[symbol].texture[(int)god->parse.tex[symbol].width * row + x];
}
  • 색은 3D벽때와 같이 텍스트에서 적절히 색을 추출하도록 했습니다.
  • 기존에 각각의 스프라이트마다 지정했던 symbol을 이용하여 알맞은 텍스쳐를 사용하도록 했습니다.


5️⃣ 문제점 발견

(1) 스프라이트가 겹쳐있을 때 문제

< 나란히 있는 스프라이트 >

two_sprite

< 겹치게 바라봤을 때 >

bug_sprite
  • 위의 그림과 같이 스프라이트겹치게 바라봤을 때 뒤에 있는 스프라이트가 앞에 그려지는 경우가 생겼습니다.
  • 해결방법은 간단합니다. 스프라이트 거리를 이용하여 정렬을 해주면 됩니다.
void sort_sprite(t_sprite *sprite)
{
    int i;
    int j;

    i = 0;
    while (i < SPRITE_COUNT - 1)
    {
        j = i + 1;
        while (j < SPRITE_COUNT)
        {
            if (sprite[j].distance < sprite[i].distance)
                change_value(&sprite[i], &sprite[j]);
            j++;
        }
        i++;
    }
}
  • sort_sprite함수를 구현하여 거리가 짧은 함수가 앞쪽 인덱스에 위치하도록 해주었습니다.

correct_two_sprite

  • 정상적으로 스프라이트들이 출력되었습니다.


(2) 벽을 뚫고 나오는 스프라이트

correct_two_sprite

  • 위와 같이 벽에 가려지지 않고 그대로 스프라이트가 그려졌습니다.
  • 이러한 문제는 당연했습니다. sprite는 한번에 그려주지만 3D벽은 광선당 새로한줄씩 그려줍니다. 또한 데이터가 일회성으로 sprite를 그릴때 벽의 위치를 파악하기가 쉽지않습니다.
  • 결국 여태껏 고수했던 "광선(ray)을 쏨과 동시에 랜더링"하는 방식을 버리기로 결정하였습니다.
  • t_rray구조체를 만들어 t_god구조체에 포인터로 저장했습니다.
void	set_ray(t_god *god)
{
	god->rray = (t_rray *)malloc(sizeof(t_rray) * (god->map.ray_count));
}
  • ray_count.cub파일에 저장된 의 형태에 따라 달라지는 것이기 때문에 t_rray구조체 변수를 실행중에 크기를 정해주도록 만들어 줬습니다.
  • 이제 광선(ray)을 쏠때마다 랜더링을 하는대신 t_rray구조체에 데이터를 저장하게 되며 각각의 인덱스값은 광선의 순번을 나타내게 됩니다.
  • 이제 이렇게 저장된 데이터를 이용하여 (스프라이트 거리 < 벽까지 거리)일때만 그려주도록 조건문을 만들어줬습니다.
if (god->img.data[god->map.window_width * y + x] == NO_COLOR && god->sprite[j].distance < god->rray[x].distance)
    god->img.data[god->map.window_width * y + x] = set_sprite_color(god, &god->sprite[j], i, x - god->sprite[j].left_x);

correct_sprite_behind_wall

  • 이제 스프라이트가 벽 뒤에 있을 때 정상적으로 가려져서 출력됩니다.
  • 애초에 배열로 ray값을 저장했더라면 간단하게 수정이 가능했을 것 입니다.
  • 이제 모든 랜더링의 과정이 독립적으로 진행되며 앞으로 새로운 랜더링이 추가되더라도 좀 더 유연하게 처리가 가능해질 것 같습니다.
int     ft_loop(t_god *god)
{
    
    setting_value(god, &(god->player), &(god->img));
    render_map(god);
    render_ray(god);
    render_sprite(god);
    render_3d(god);
    draw_player(god);
    mlx_put_image_to_window(god->mlx, god->win, god->img.ptr, 0, 0);
    return (0);
}
  • 위와같이 랜더링의 과정들이 훨씬 더 가시적으로 구성되었습니다. (이전에는 랜더링 함수들이 이곳 저곳에 위치, 심지어 3D벽 랜더링함수는 ray함수안에 위치했었다..)




© 2021.02. by kirim

Powered by kkrim