레이캐스팅으로 2.5d 맵 만들기

2025. 4. 14. 23:28개발자 능력치 물약/그래픽스

 

https://github.com/365kim/raycasting_tutorial/blob/master/2_basics.md

 

raycasting_tutorial/2_basics.md at master · 365kim/raycasting_tutorial

(한글) 레이캐스팅 튜토리얼 번역. Contribute to 365kim/raycasting_tutorial development by creating an account on GitHub.

github.com

레이캐스팅 튜토리얼을 보며 학습했습니다.

 

레이캐스팅은 2차원맵에서 3차원의 원근감을 보여주는 2.5d 기법입니다.

울펜슈타인3d, 둠같은 예전 게임들은 컴퓨터 성능이 좋지않아서 3d엔진을 실시간으로 실행할 수 없었습니다. 

그래서 3d인척하는 레이캐스팅 기법을 사용했습니다. 

 

 

원근감은 나타냈지만 계단, 점프등은 구현할 수 없는 한계가 있습니다. 

 

레이캐스팅 vs 레이트레이싱

 

 게이머들은 레이트레이싱이란 말을 아마 한번쯤은 들어본 적이 있을 것이다.

레이트레이싱 : 빛의 경로를 추적하여 사실적인 반사, 굴절, 그림자 등을 시뮬레이션하는 그래픽 기술

요즘에는 리얼타임 레이트레이싱을 할정도로 그래픽이 엄청나게 발달되었고 엔비디아gpu , 언리얼엔진에서도 제공한다. 

 

반면 레이캐스팅은 카메라에서 각 픽셀을 통과하는 가상의 광선을 쏘아 무엇이 보이는지 결정하는 기법이다.

 

플레이어의 위치는 항상 벡터이고, 그 방향벡터와 카메라평면을 나타내는 벡터의 합으로 플레이어의 시야를(FOV)를 정한다. 

 

 

FOV(Field Of View)는 방향벡터와 평면벡터의 길이의 비율로 정한다. 

 

플레이어가 회전할때는 회전행렬을 곱한다. 

 

int RayCasting::map[MAP_ROW * MAP_COLUME] =
{
  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
  1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,0,0,0,0,0,2,2,2,2,2,0,0,0,0,3,0,3,0,3,0,0,0,1,
  1,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,0,0,0,0,0,2,0,0,0,2,0,0,0,0,3,0,0,0,3,0,0,0,1,
  1,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,0,0,0,0,0,2,2,0,2,2,0,0,0,0,3,0,3,0,3,0,0,0,1,
  1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,4,4,4,4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,4,0,4,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,4,0,0,0,0,5,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,4,0,4,0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,4,0,4,4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,4,4,4,4,4,4,4,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
};

 

맵정보를 보면 0은 빈공간, 1은 벽처럼 2,3,4,5 모두 다른 타입의 타일로 그려준다. 

이 맵은 2차원공간이지만 해당 좌표에 있는 공간을 가까울수록 높이가 높게, 멀어질수록 높이가 낮게 그린다면 입체감이 생긴다. 

 

 

레이캐스터는 보통 광선의 위치에 일정한 값을 더해주며 반복하는 방식으로 벽에 부딪혔는지 검사한다. 하지만 무한한 정밀도를 얻기 위해서는 검사간격이 무한히 작아져야하며 계산량도 무한히 늘어난다. 

 

따라서 광선이 닿는 벽의 모든 면을 검사하는 알고리즘을 사용한다 .

 

DDA(Digital Differential Analysis)

  • DDA 알고리즘은 2차원 그리드를 지나가는 선(line)이 어떤 네모칸과 부딪히는지 찾을 때 일반적으로 사용되는, 속도가 빠른 알고리즘입니다. 그래서 이 알고리즘을 사용해서 광선이 맵에서 어떤 네모칸이랑 부딪히는지 찾아낼 수 있고, 벽에 부딪힌 것이 확인되면 이 알고리즘은 중단됩니다.

 

튜토리얼대로 따라하니 해상도가 높고 렌더되는 물체들이 여러개일때 프레임드랍이 심해졌다.

  1. 렌더링 성능 향상 • 동적 해상도 조정: FPS에 따라 자동으로 렌더링 해상도 조절 • 15 FPS 미만: 1/8 해상도 • 25 FPS 미만: 1/4 해상도 • 40 FPS 미만: 1/2 해상도 • 정상 FPS: 최대 해상도 • 픽셀 렌더링 최적화: SetPixel 대신 LineTo 사용하여 한 번에 선 그리기
  1. 뷰 컬링 (View Culling) • 카메라의 시야각 밖 객체는 업데이트/렌더링 안함 • 최대 렌더링 거리 밖 객체도 무시 • 객체마다 가시성 플래그를 활용

두가지 조건을 추가했더니 프레임드랍이 거의 사라졌다. 최소 80fps는 유지한다. 

 

int RayCasting::GetRenderScaleBasedOnFPS()
{
    // FPS에 따라 렌더링 스케일 결정
    if (currentFPS < 15) return 8;      // 매우 낮은 FPS: 1/8 해상도
    else if (currentFPS < 25) return 4; // 낮은 FPS: 1/4 해상도
    else if (currentFPS < 40) return 2; // 중간 FPS: 1/2 해상도
    else return 1;                      // 높은 FPS: 최대 해상도
}
void RayCasting::Render(HDC hdc)
{
    // 천장 그리기
    RECT ceiling = { 0, 0, WINSIZE_X, WINSIZE_Y / 2 };
    HBRUSH ceilingBrush = CreateSolidBrush(ceilingColor);
    FillRect(hdc, &ceiling, ceilingBrush);
    DeleteObject(ceilingBrush);

    // 바닥 그리기
    RECT floor = { 0, WINSIZE_Y / 2, WINSIZE_X, WINSIZE_Y };
    HBRUSH floorBrush = CreateSolidBrush(floorColor);
    FillRect(hdc, &floor, floorBrush);
    DeleteObject(floorBrush);

    // 렌더링 스케일에 따라 레이캐스팅 수행
    for (int i = 0; i < WINSIZE_X; i += renderScale)
    {
        Ray ray = RayCast(i);
        depth[i] = ray.distance;
        ray.height = fabs(FLOAT(WINSIZE_Y) / ray.distance);

        // 충돌 지점의 정확한 x 좌표 계산
        ray.wall_x = CalculateWallX(ray);

        // 텍스처 x 좌표 계산 (0~TEXTURE_SIZE-1 범위로 변환)
        ray.tex_x = (int)(ray.wall_x * TEXTURE_SIZE) % TEXTURE_SIZE;
        if ((ray.side == 0 && ray.ray_dir.x < 0) || (ray.side == 1 && ray.ray_dir.y > 0))
            ray.tex_x = TEXTURE_SIZE - ray.tex_x - 1;

        // i부터 i+renderScale-1까지 같은 값으로 채움
        for (int j = 0; j < renderScale && i + j < WINSIZE_X; j++) {
            if (useTextures)
                RenderWallWithTexture(hdc, ray, i + j);
            else
                RenderWall(hdc, ray, i + j);
        }
    }

    // 현재 FPS와 렌더링 스케일 표시 (디버깅용)
    wchar_t szText[128];
    wsprintf(szText, TEXT("FPS: %d, Scale: %d, Texture: %s"), currentFPS, renderScale,
        useTextures ? TEXT("On") : TEXT("Off"));
    SetTextColor(hdc, RGB(255, 255, 255));
    SetBkMode(hdc, TRANSPARENT);
    TextOut(hdc, 10, 10, szText, wcslen(szText));
}