1️⃣ 목표
- 이번엔
sprite(스프라이트)
를 구현할 예정입니다. sprite(스프라이트)
를 입체적으로 구현하는 것이 아니기 때문에 어느 방향에서 보든지 앞부분만 보이게 됩니다.
2️⃣ 원리생각해보기
- 지금까지 구현한 랜더링은 다음과 같습니다.
지도
그리기- 지도상에
player
그리기 - 지도상에
광선(ray)
그리기 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랜더링
에서는 추가적으로 너비까지 지정했는데 단순히 높이와 같은 값을 갖습니다.- 추가적으로
각도
를 구해줘야합니다. 각도
는 다음에 사용됩니다.어안렌즈효과
를 보정해주기위해 사용FOV_ANGLE
(시야각: 60도)에 들었는지를 판별하여 그려줄지말지를 결정하는데 사용
각도
는 플레이어가 바라보는 각도(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) 스프라이트가 겹쳐있을 때 문제
< 나란히 있는 스프라이트 >
< 겹치게 바라봤을 때 >
- 위의 그림과 같이 스프라이트를 겹치게 바라봤을 때 뒤에 있는 스프라이트가 앞에 그려지는 경우가 생겼습니다.
- 해결방법은 간단합니다.
스프라이트 거리
를 이용하여 정렬
을 해주면 됩니다.
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함수
를 구현하여 거리가 짧은 함수가 앞쪽 인덱스에 위치하도록 해주었습니다.
(2) 벽을 뚫고 나오는 스프라이트
- 위와 같이 벽에 가려지지 않고 그대로 스프라이트가 그려졌습니다.
- 이러한 문제는 당연했습니다.
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);
- 이제 스프라이트가 벽 뒤에 있을 때 정상적으로 가려져서 출력됩니다.
- 애초에 배열로 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함수안에 위치했었다..)