[cub3d](9)3D벽 구현하기



1️⃣ 목표

  • 지금까지 2D미니맵을 구현하였습니다.
  • 이전 포스트에서도 언급하였지만 레이케스팅기법의 3D는 가짜 3D입니다.
  • 화면상 “한열(column)당 한가지의 정보”만 있으면 됩니다. 정확하게는 “열(column) x 타일사이즈(TILE_SIZE)당 한가지의 정보”만 있으면 3D화면으로 확장시킬 수 있습니다.
  • 그 정보들은 이미 2D미니맵을 구현하면서 설정되었고 그 정보들을 이용해서 구현만 하면됩니다.


2️⃣ 3D랜더링 순서 정하기

(1) 단순하게 생각할 수 있는 순서 및 방법

  • 이전에 구현한 “player광선(ray)“는 미니맵이 랜더링된 후에 덧칠하는 느낌으로 그려지도록 했습니다.
  • 이러한 덧칠하는 방법으로 구현하려고하면 3D랜더링이 가장 먼저 실행되어야합니다. 하지만 3D랜더링을 하기 위해서는 광선(ray)을 구현하면서 설정해줬던 정보들이 필요합니다. 그렇기 때문에 지금 생각할 수 있는 방법은 다음과 같이 2가지 입니다.
    1. “2D미니맵랜더링” -> “3D화면랜더링” -> “2D미니맵랜더링” 으로 마지막에 2D미니맵을 한번 더 덧칠해주는 방법
    2. “3D화면”을 기준으로 먼저 함수를 구현하고 그정보를 이용한 “2D미니맵랜더링”함수 구현하기
  • 1번의 방법은 가장 단순하게 해결할 수 있지만 성능상 가장 비효율적인 방법입니다.
  • 2번의 방법이 괜찮을 것 같지만 추가적으로 각각의 광선(ray)의 정보를 담고 있는 ray구조체 배열을 만들어 줘야합니다. 어쩔수 없이 광선(ray)을 하나씩 살피는 반복문도 필요합니다. (3D화면또한 한 열씩 그려지는데 그 열의 위치가 2D맵이 그리는 과 반드시 같지 않기 때문에 덮여 씌어지는일이 생길 수 있고 어쩔 수없이 배열과 추가적인 반복문이 필요합니다.) 결론적으로 코드를 고치는 것이 불편할 뿐더러 효율도 그다지 좋아보이지 않습니다.

(2) 이상적인 방법

  • 이 방법이 이상적인 방법이라고는 확신하지 못하지만 그래도 위의 방법보다는 이상적인 방법이라고 생각합니다.
    1. hook_loop함수가 호출될때마다 프로젝트 화면특정색으로 세팅해줍니다.
    2. 반복문이 돌면서 각각의 광선(ray)마다 “2D맵”과 “3D화면”을 동시에 그려줍니다.
    3. 단, “3D화면”은 특정색인 곳만 그려줍니다. (2D미니맵위에 덧칠하는 것을 막아줌)

(3) 특정색을 채우는 반복문

for (int y = 0; y < WINDOW_HEIGHT; y++)
{
    for (int x = 0; x < WINDOW_WIDTH; x++)
        god->img.data[WINDOW_WIDTH * y + x] = 0x111111;            
}
  • 0x111111색으로 채워줬습니다. 2D맵을 그려주기 이전에만 호출되면 됩니다.

(4) 3D랜더링함수 매개변수

  • 각각의 광선마다 2D맵과 동시에 그려줄 계획입니다. (ray구조체 배열을 만들기 싫다면 어쩔 수 없이 동시에 그려줘야 합니다.)
  • 3D랜더링함수화면상 열(column)의 위치를 매개변수로 받아야합니다.
/* 3D랜더링 함수의 함수원형 */
void render_3D_project_walls(t_god *god, int ray_num);
  • 화면상 열(column)광선(ray)의 순번을 매개변수로 받으면 3D함수내부적으로 처리하도록 구현했습니다.
  • 광선(ray)읜 순번을 적절히 넘겨주기 위해 기존의 ray(광선)을 그려주는 함수를 수정하였습니다.
/* 수정된 draw_ray함수 */
void    draw_ray(t_god *god)
{
    double angle;
    double maxAngle;
    int i;

    i = 0;
    angle = god->player.rotationAngle - (RAY_RANGE / 2.0);
    maxAngle = god->player.rotationAngle + (RAY_RANGE / 2.0);

    while (i < RAY_COUNT)
    {
        draw_one_ray(god, angle, i);
        angle += RAY_RANGE / RAY_COUNT;
        i++;
    }
}
  • 기존에 광선을 동시에 2개씩그려주는 반복문을 왼쪽부터 하나씩그려주는 반복문으로 수정해 주었습니다.
  • 3D랜더링함수draw_one_ray함수에 위치할 계획입니다.

(5) 3D랜더링함수 최종위치

void    draw_one_ray(t_god *god, double angle, int i)
{
    /* 코드 생략 */

    draw_line(god, god->ray.wall_hitX - god->player.x, god->ray.wall_hitY - god->player.y);
    render_3D_project_walls(god, i);
}
  • 각각의 광선(ray)에 대한 정보는 draw_one_ray함수에 와서야 결정이 됩니다. 그렇기 때문에 3D랜더링함수의 위치를 이 함수의 끝쪽으로 최종결정하였습니다.
  • draw_line함수(광선(ray)을 그리는 함수)와 render_3D_project_walls함수의 위치는 서로 바뀌어도 상관없습니다.


3️⃣ 3D랜더링함수 구현하기

void render_3D_project_walls(t_god *god, int ray_num)
{
    double distance_project_plane = (WINDOW_WIDTH / 2) / tan(FOV_ANGLE / 2);
    double projected_wall_height = (TILE_SIZE / god->ray.distance) * distance_project_plane;

    int wallStripHeight = (int)projected_wall_height;

    int wall_top_pixel = (WINDOW_HEIGHT / 2) - (wallStripHeight / 2);
    wall_top_pixel = wall_top_pixel < 0 ? 0 : wall_top_pixel;

    int wall_bottom_pixel = (WINDOW_HEIGHT / 2) + (wallStripHeight / 2);
    wall_bottom_pixel = wall_bottom_pixel > WINDOW_HEIGHT ? WINDOW_HEIGHT : wall_bottom_pixel;

    int color = god->ray.wasHit_vertical ? 0xFFFFFF : 0xAAAAAA;
    for (int y = wall_top_pixel; y < wall_bottom_pixel; y++)
        for (int x = 0; x < WALL_STRIP_WIDTH; x++)
            if (god->img.data[WINDOW_WIDTH * y + (x + ray_num * WALL_STRIP_WIDTH)] == 0x111111)
                god->img.data[WINDOW_WIDTH * y + (x + ray_num * WALL_STRIP_WIDTH)] = color;
    
}
  • god->ray.wasHit_vertical ? 0xFFFFFF : 0xAAAAAA;코드를 이용하여 수직면과 수평면의 색을 다르게 하였습니다.
  • if (god->img.data[ ... ] == 0x111111)의 조건문을 이용하여 현재위치의 픽셀의 색이 초기에 설정해준 색이면 색을 덧입히도록 구현하였습니다.
  • FOV_ANGEL시야각을 나타내며 60 * (PI / 180.0)으로 정의하였습니다. distance_project_plane

  • distance_project_plane화면(모니터)과 사용자사이의 거리입니다. WINDOW_WIDTHFOV_ANGEL을 알면 간단하게 구할 수 있습니다. wallStripHeight

  • wallStripHeight화면(모니터)상에 보이는 벽의 높이입니다. 거리(god->ray.distance)반비례하며 distance_project_plane을 곱하여 적절한 높이를 가지게 만들었습니다. (실제 거리가 1일때 높이와 2 : 루트3비율을 갖게 되어 적당한 비율이라고 생각)


4️⃣ 문제점 발견 및 수정

(1) 왼쪽, 위쪽 출력문제

< 왼쪽방향을 바라볼때 출력 >

watch_left_direction_3D

< 위쪽방향 바라볼때 출력 >

watch_up_direction_3D
  • 왼쪽방향으로 바라볼 때 벽의 위치가 한칸 밀려서 생성됨을 확인할 수 있었고 위쪽방향으로 바라볼 때는 미세하지만 약간 뒤로 밀려서 출력됨을 확인할 수 있습니다.
  • 바라보는 방향에 따라 보정을 잘못해준 것이 원인이였습니다.

< 수정 전 >

void    cal_ray(t_god *god, t_dpable_ray *hv)
{
    /* 코드 생략 */
    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)))
        {
            /* 코드 생략
        }
    /* 코드 생략 */
}

< 수정 후 >

void    cal_ray(t_god *god, t_dpable_ray *hv, int a, int b)
{
    /* 코드 생략 */
    while (next_touchX >= 0 && next_touchX <= WINDOW_WIDTH && next_touchY >= 0 && next_touchY <= WINDOW_HEIGHT) {
        if (is_wall(next_touchX + a, next_touchY + b))
        {
            /* 코드 생략 */
        }
    }
    /* 코드 생략 */
}
  • horizontal, vertical의 각각의 수치를 계산하는 함수의 공통된 부분을 묶어만든 함수에서 바라보는 방향에 따른 보정horizontal일 경우로 맞췄었습니다.
  • 수정 후 cal_ray함수a(x좌표 보정값), b(y좌표 보정값)을 매개변수로 받아서 적용시킬 수 있도록 변경하였습니다.

(2) 면이 굴곡이 생기는 문제

  • 예전에 자바스크립트로 구현했을때도 언급했던 문제입니다. (어안렌즈효과?)
  • 지금까지 레이케스팅사용자(꼭지점)기준으로 60도의 시야각을 가지고 바라봤을때의 물체가 출력되도록 구현했습니다.
  • 그렇기 때문에 같은 모니터화면이라도 모니터안에서의 좌우의 위치에 따라서 거리가 달랐습니다. (당연히 부채꼴 모양으로 광선이 나가므로)
  • 하지만 이렇게 보이는 물체의 모습은 우리에게 익숙하지 않습니다. (사람은 두개의 눈으로 들어온 각각의 빛을 뇌에서 보정(?))
  • 이것을 같은 모니터상이라면 좌우 어느지점이라도 가운데에서의 거리에서 볼때와 비율이 같아지도록 보정해주었습니다.

< 수정 전 >

double distance_project_plane = (WINDOW_WIDTH / 2) / tan(FOV_ANGLE / 2);
double projected_wall_height = (TILE_SIZE / god->ray.distance) * distance_project_plane;

< 수정 후 >

double correct_distance = god->ray.distance * cos(god->ray.ray_angle - god->player.rotationAngle);
double distance_project_plane = (WINDOW_WIDTH / 2) / tan(FOV_ANGLE / 2);
double projected_wall_height = (TILE_SIZE / correct_distance) * distance_project_plane;

< 보정 전 >

watch_left_direction_3D

< 보정 후 >

watch_up_direction_3D




© 2021.02. by kirim

Powered by kkrim