한양과학기술고등학교

에듀테크 프로젝트 대시보드

레이 캐스팅 (Ray Casting)

레이 캐스팅 — 광선 하나씩 쏘아 만드는 2.5D 미로

1992년 울펜슈타인 3D, 1993년 둠은 3D 그래픽 카드가 없던 시절에 3D처럼 보이는 화면을 초당 수십 장씩 그려냈다. 비밀은 레이 캐스팅이다. 맵은 사실 위에서 내려다본 2D 격자일 뿐이고, 플레이어의 눈에서 화면 가로 픽셀 한 줄마다 광선(ray) 하나를 쏴서 "이 방향으로 가면 벽까지 거리가 얼마인가"를 잰다. 가까운 벽은 길게, 먼 벽은 짧게 세로 막대로 그리면 3D 공간이 된다.

1. 직접 걸어 보기 — 왼쪽 격자에서 쏜 광선이 오른쪽 3D가 된다

탑다운 맵 — 초록 부채꼴이 광선들
1인칭 시점 — 광선마다 세로 막대 하나

키보드 WASD 또는 ←↑↓→, 맵 캔버스를 클릭하면 그 지점으로 순간이동합니다. 광선 개수를 20개로 줄이면 벽이 세로 줄무늬로 쪼개지는 걸 볼 수 있어요 — 원래 화면 가로 픽셀 수(예: 320개)만큼 쏘는 게 정석입니다.

2. 광선 하나가 벽을 찾는 법 — DDA 격자 행진

광선을 아주 작은 보폭으로 조금씩 전진시키며 벽인지 확인하면 느리고 부정확하다. 대신 DDA(Digital Differential Analyzer)는 세로 격자선과 가로 격자선을 만나는 지점만 콕콕 짚어 이동한다. 다음 교차점이 세로선이 더 가까우면 x칸을, 가로선이 더 가까우면 y칸을 밟는다. 새 칸이 벽이면 그 자리에서 멈춘다.

3. 핵심 루프 (한 프레임에 화면 폭만큼 반복)

# posX, posY = 플레이어 위치, dir = 바라보는 각도, FOV = 시야각 for col in range(SCREEN_W): # 세로 막대 하나 = 광선 하나 cameraX = 2 * col / SCREEN_W - 1 # 화면 좌 -1 ~ 우 +1 rayAng = dir + atan(cameraX * tan(FOV / 2)) mapX, mapY = int(posX), int(posY) # 격자선 하나 넘을 때 광선이 나아가는 거리 dDistX = abs(1 / cos(rayAng)) dDistY = abs(1 / sin(rayAng)) # ... 첫 격자선까지 sideDistX, sideDistY 계산, stepX/stepY = ±1 ... while not world[mapY][mapX]: # DDA: 벽 만날 때까지 if sideDistX < sideDistY: sideDistX += dDistX; mapX += stepX; side = 0 else: sideDistY += dDistY; mapY += stepY; side = 1 # 어안(fisheye) 왜곡 제거: 화면에 수직인 '카메라 평면' 거리 사용 perpDist = (sideDistX - dDistX) if side == 0 else (sideDistY - dDistY) lineH = SCREEN_H / perpDist # 가까울수록 길다 draw_vertical_bar(col, lineH, shade=1 / (1 + perpDist * 0.2))

4. 왜 이렇게 빠른가 · 무엇이 안 되는가

폴리곤이 없다

삼각형·정점·행렬 변환이 전혀 없다. 화면 폭이 320이면 루프 320번, 각 루프는 격자 몇 칸만 밟으면 끝. 486 PC로도 부드럽게 돌아간 이유다. 복잡도는 대략 O(화면폭 × 맵크기).

어안 왜곡

광선의 실제 거리를 그대로 쓰면 벽 가운데가 불룩해 보인다. 광선 거리에 cos(rayAng − dir)를 곱해 화면에 수직인 거리로 바꿔야 벽이 평평해진다. 위 데모에서 버튼으로 껐다 켜 보라.

높이 차이가 없다

순수 레이 캐스팅은 벽이 모두 같은 높이, 바닥·천장은 단색. 계단이나 경사, 위아래로 보기는 불가능하다. 둠은 여기에 높이 다른 섹터 개념을 더해 한계를 넓혔다.

레이 트레이싱과 다르다

레이 캐스팅은 한 픽셀 열마다 광선 1개로 벽 거리만 잰다. 레이 트레이싱은 픽셀마다 광선을 쏘고 반사·굴절·그림자까지 재귀 추적한다 — 이름은 비슷해도 계산량이 하늘과 땅 차이.