https://github.com/365kim/raycasting_tutorial
상기 자료를 보고 다시 이해해서 작성한 글입니다.
기초 아주 기본적인 원리아주기본적인 원리를 다 읽어도 이해가 되지 않는다..ㅠ
pos 벡터 : 플레이어의 위치 (x, y 좌표) pos 벡터라고하지만 내 이해로는 벡터가 아니라 단순한 좌표값이다. 벡터라고 표현해버리면 0점에서 플레이어 좌표까지의 스칼라 값, 방향벡터를 가지는 어떤 힘이 되어버리기때문에 이해하기 더 어려워짐. pos 는 벡터가 아니라 플레이어의 단순 절대 좌표.
dir 벡터 : 방향벡터, 방향벡터라 함은 크기가 1이면서 방향을 나타내는데 쓰이는 벡터를 의미한다.
예를 들어 3, 4 라는 벡터가 있다면 이 벡터의 스칼라 값은 5이다. (피타고라스의 정리: 루트[3^2 + 4^2] = 5 이므로)
각 요소를 스칼라값 5로 나눈 (3/5, 4/5) 가 (3, 4) 벡터의 방향벡터가 된다.
중간정리 : pos 는 플레이어의 x, y 좌표, dir 벡터는 플레이어가 바라보는 방향을 가리키는 방향벡터라고 할 수 있다.
카메라평면 : 카메라평면은 컴퓨터 화면의 표면을 나타내고 방향벡터는 화면의 정 중앙을 가리킨다.
pos + dir 벡터(방향벡터) : 방향벡터의 끝점이라고 글에서는 정의한다. 내가 이해하기로는 플레이어의 위치에서 어떤방향을 바라보고있는지 알려주는 벡터(화면의 정 중앙).
plane벡터 : pos + dir 벡터에서 오른쪽 카메라평면의 끝점 까지. (왼쪽 끝은 -plane벡터)
즉, 컴퓨터화면의 정 중앙은 pos + dir, 컴퓨터 화면의 맨 오른쪽은 (pos + dir) + plane, 컴퓨터 화면의 맨 왼쪽은 (pos + dir) - plane 이 된다.
광선의 방향 : dir + plane * 1/3 이라고 정의하는데 가장 이해가 안되는 부분이였다.
plane 에 대한 절대값을 정해놓지 않았는데 배수를 곱하는게 이해가 안되었다.
아마 plane 을 1로 가정하고 해당 공식을 사용한듯 보인다.
광선의 방향 추가 팁 : 기본적인 원리 다음 페이지를 보면 1인칭 fps 에서 66.8(dgree) 가 가장 적합한 시야각 이라고 한다. 시야 각은 해당 자료에서 FOV(Field of View)라고 말하고있음.
dir 의 스칼라값이 1이고 plane 역시 1일때 플레이어의 위치에서 카메라평면 까지의 거리(방향벡터 dir) 가 1이고 정 중앙에서 오른쪽 카메라평면 끝까지 의 위치가 1이라는 소리고, 사잇각은 45dgree 가 된다.
왼쪽 카메라평면 끝까지의 각 역시 생각해야되므로 시야각은 총 90dgree 가 된다.
정리 : dir + plane 1/3 의 의미는 비율적으로 dir 이 1이고 plane 이 1일때 plane 에 1/3 을 곱함으로써 1 : 0.333... 의 비율을 가지는 FOV를 구하고자 하는것이다.
-> 이는 두 plane 방향(왼쪽 끝점 부터 오른쪽 끝점까지) 사이의 각도가 66.8도(33.33... 도 2)인 시야각을 의미한다.
회전행렬 : 플레이어가 방향을 회전시키면 모든 벡터 역시 회전해야하므로 벡터들에 회전행렬을 곱해서 각 벡터들을 재계산하는 과정이 필요하다. 공식자체는 어렵지않음.
링크의 내용을 가져와서 수정하는 방식으로 작성하는게 더 좋은 방법인것 같다. 글 원문 이외에 생각을 추가한 부분은
인용문구로 표현합니다.
문제가 되면 수정하겠습니다.
예제코드 전체보기 : raycaster_flat.cpp
레이캐스터의 기본이 되는 텍스쳐 없이 색상만 표현한 레이캐스터 (Untextured Raycaster) 부터 시작하겠습니다.
- __FPS 카운터__ (fps : frames per second, 초당 프레임)도 다룹니다.
- __이동/회전을 위한 충돌감지__ 기능이 있는 입력키 (input key)도 다룹니다.
#define mapWidth 24
#define mapHeight 24
#define screenWidth 640
#define screenHeight 480
int worldMap[mapWidth][mapHeight]=
{
{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}
};
지도는 2차원 배열 로 나타낼 수 있고, 배열의 각 요소는 지도의 한 칸을 나타냅니다.
- 위에 선언된 지도는 24칸 x 24칸 크기로 굉장히 작은 편이고, 코드 안에서 바로 정의되고 있습니다.
- Wolfenstein 3D와 같은 실제 게임에서는 더 큰지도를 사용하고 코드 안에서 바로 정의하지 않고 파일에서 지도를 불러옵니다.
- 만약 요소의 값이 0이라면, 그 칸은 비어있어서 플레이어가 지나갈 수 있는 칸이라는 뜻입니다. 만약 요소의 값이 0보다 크다면, 그 칸은 특정 색상이나 텍스쳐가 있는 벽이라는 뜻입니다.
- __배열 요소값 '0'__ : 비어있는 큰 공간
- __배열 요소값 '1'__ : 벽
- __배열 요소값 '2'__ : 내부의 작은 방
- __배열 요소값 '3'__ : 몇 개의 기둥
- __배열 요소값 '4'__ : 복도
- 이 코드는 아직 어떤 함수에도 포함되어 있지 않습니다. 메인함수 시작되기 전에 넣어주세요.
int main(int argc, char *argv[])
{
double posX = 22, posY = 12; //x and y start position
double dirX = -1, dirY = 0; //initial direction vector
double planeX = 0, planeY = 0.66; //the 2d raycaster version of camera plane
double time = 0; //time of current frame
double oldTime = 0; //time of previous frame
우선 메인함수의 변수를 선언 합니다.
여기까지 main함수의 선언부를 마치고 아래에서 본문을 이어서 설명합니다.
screen(screenWidth, screenHeight, 0, "Raycaster");
본문에서는 우선, screen()함수로 해상도를 지정해서 화면을 생성 합니다.
- 이 때 1280 * 1024 처럼 해상도를 높게 지정하면 광선추적 알고리즘이 빨라도 CPU에서 비디오카드로 전체화면을 불러오는게 너무 오래걸려서 렌더링이 느려지게 됩니다.
while(!done())
{
화면을 생성한 후 바로 게임루프가 시작 됩니다.
- 이 반복문은 계속 반복해서 전체 프레임을 그려내고 입력을 읽는 일을 합니다.
for(int x = 0; x < w; x++)
{
//calculate ray position and direction
double cameraX = 2 * x / double(w) - 1; //x-coordinate in camera space
double rayDirX = dirX + planeX * cameraX;
double rayDirY = dirY + planeY * cameraX;
이제 진짜 레이캐스팅을 시작 합니다. (for문)
- 광선의 시작점은 플레이어 위치로 합니다. __(posX, posY)__
- 레이캐스팅 반복문에 필요한 변수를 선언하고 값을 계산합니다.
- __cameraX__ 는 for문의 x값(화면의 수직선)이 위치가 카메라평면에서 차지하는 x좌표 입니다.
- for문의 x값이 0이면 (스크린의 왼쪽 끝이면) cameraX = -1
- for문의 x값이 w/2이면 (스크린의 중앙이면) cameraX = 0
- for문의 x값이 w이면 (스크린의 오른쪽 끝이면) cameraX = 1
- 이를 활용해서 광선의 방향을 계산할 것입니다.
- __rayDirX, rayDirY__ 는 광선의 방향벡터 입니다.
- [앞서](https://github.com/365kim/raycasting_tutorial/blob/master/2_basics.md) 설명한 것과 같이 광선의 방향은 __( 방향벡터 ) + ( 카메라평면 x 배수 )__ 로 구할 수 있습니다. 벡터 x, y에 대해 각각 이 계산을 해줍니다.
- 이 반복문은 스크린의 모든 x값(수직선)에 대해서 계산할 뿐, 모든 픽셀에 대해서 계산하는게 아니라 계산량이 얼마 안됩니다!
> 상기 코드에서 w의 값은
```cpp
screen(screenWidth, screenHeight, 0, "Raycaster");
```
screen 함수에서 내부적으로 w 라는 변수에 screenWidth 값을 대입시킨다.
즉, x = 0 부터 화면해상도 (현재 코드에선 640) 오른쪽 끝까지 반복문을 돌리는것.<span>
> - cameraX 의 값을 왜 저렇게 구하는지 의문이 들었다. 광선의 방향을 계산하기 위해서 사용한다는데,
광선의 방향은 플레이어의 위치에서 카메라 평면방향의 수평선으로 수많은 광선들을 쏘는 것이다.
때문에 각 광선들의 벡터값을 계산하기 위해서 640해상도를 반복문으로 1씩 돌려가며 각 광선들의 벡터를 구하는 과정이라고 볼 수 있다.
---
- rayDirX, rayDirY 의 의미가 직관적이지 않았다.
dirX 혹은 dirY가 방향벡터고, planeX * cameraX 의 값의 의미는 카메라 우측 혹은 좌측 끝점을 가리키는 벡터 곱하기 -1 ~ +1 까지 범위의 값을 곱하면서 수평선으로 (가로 해상도가 640일경우) 640개 광선의 벡터를 가리킨다.
//which box of the map we're in
int mapX = int(posX);
int mapY = int(posY);
//length of ray from current position to next x or y-side
double sideDistX;
double sideDistY;
//length of ray from one x or y-side to next x or y-side
double deltaDistX = std::abs(1 / rayDirX);
double deltaDistY = std::abs(1 / rayDirY);
double perpWallDist;
//what direction to step in x or y-direction (either +1 or -1)
int stepX;
int stepY;
int hit = 0; //was there a wall hit?
int side; //was a NS or a EW wall hit?
이제 DDA 알고리즘 과 관련된 변수를 선언하고 계산할 것입니다.
- __mapX, mapY__ 는 현재 광선의 위치, 광선이 있는 한 칸(square) 입니다.
- 광선의 위치 자체는 부동소수점수로 표현되서 광선이 맵상 어느 칸(square)에 있는지 그리고 그 한 칸안에서 어디쯤 있는지까지 알 수 있지만, __mapX, mapY__ 는 간단히 그 한 칸(square)의 좌표만 나타냅니다.
- __sideDistX__ 는 '시작점 ~ 첫번째 x면을 만나는 점'까지의 광선의 이동거리 이고,
- __sideDistY__ 는 '시작점 ~ 첫번째 y면을 만나는 점'까지의 광선의 이동거리 입니다.
- __sideDistX, sideDistY__ 의미는 나중에 지금과 다른 의미로 약간 변경될 예정입니다.
- __deltaDistX__ 는 '첫번째 x면 ~ 바로 다음 x면'까지의 광선의 이동거리 입니다. (이때 x는 1만큼 이동)
- __deltaDistY__ '첫번째 y면 ~ 바로 다음 y면'까지의 광선의 이동거리 입니다 (이때 y는 1만큼 이동)
- 피타고라스 공식을 이용해서 __deltaDistX, deltaDistY__ 산출식을 위의 예제코드와 같이 쓸 수 있습니다.
- 아래의 'deltaDist 공식유도' 참고
- __perpWallDist__ 는 나중에 광선의 이동거리를 계산하는 데 사용할 것입니다.
DDA 알고리즘은 반복문을 실행할 때마다 x방향 또는 y방향으로 딱 한 칸(square)씩 점프합니다.
마지막으로 벽의 x면 또는 y면과 부딪쳤는지 여부에 따라 루프를 종료할지 결정합니다.
- __hit__ 은 벽과 부딪쳤는지 여부 (루프 종료조건) 입니다. 만약에 벽과 부딪혔고 그게 x면에 부딪힌 거라면 __side__ 의 값은 0으로, y면에 부딪히면 1이 됩니다. __x면, y면__ 은 두개의 칸(square)의 경계가 되는 부분의 선을 의미합니다.
deltaDistX = sqrt(1 + (rayDirY * rayDirY) / (rayDirX * rayDirX))
deltaDistY = sqrt(1 + (rayDirX * rayDirX) / (rayDirY * rayDirY))
But this can be simplified to:
deltaDistX = abs(|v| / rayDirX)
deltaDistY = abs(|v| / rayDirY)
Where |v| is the length of the vector rayDirX, rayDirY (that is sqrt(rayDirX * rayDirX + rayDirY * rayDirY)).
However, we can use 1 instead of |v|,
because only the *ratio* between deltaDistX and deltaDistY matters for the DDA code that follows later below,
so we get:
deltaDistX = abs(1 / rayDirX)
deltaDistY = abs(1 / rayDirY)
[thanks to Artem for spotting this simplification]
참고 : 만약 rayDirX 또는 rayDirY 의 값이 0이면, 0으로 나누는 꼴이 되서 deltaDistX 또는 deltaDistY 의 값이 무한대가 됩니다.
// Alternative code for deltaDist in case division through zero is not supported
double deltaDistX = (rayDirY == 0) ? 0 : ((rayDirX == 0) ? 1 : abs(1 / rayDirX));
double deltaDistY = (rayDirX == 0) ? 0 : ((rayDirY == 0) ? 1 : abs(1 / rayDirY));
- 문서에 심각한 생략이 존재한다.
deltaDistX = sqrt(1 + (rayDirY rayDirY) / (rayDirX rayDirX))
도대체 deltaDistX는 어째서 sqrt(1 + (rayDirY rayDirY) / (rayDirX rayDirX))이 되는걸까.
- 위 그림 에서 deltaDistX 의 길이를 구하려면 제곱근 연산 sqrt(1 제곱 + y길이 미지수 x)가 된다(피타고라스의 정리), 즉, 1 : y길이 증가분인 미지수x 의 비례와 rayDirX : rayDirY 의 비례율과 동일하다. 수식으로 나타내면..
- 1(deltaDistX 의 x증가분) : 미지수 x(deltaDistX 의 y증가분) = rayDirX : rayDirY 가 된다.
- 이를 유도해보자.
1 : x = rayDirX : rayDirY -> 각 우변의 값을 나눈다. ->
-> 1/x : 1 = rayDirX/rayDirY : 1 -> 이렇게 되면 1/x = rayDirX/rayDirY 가 된다. 그리고 이를 역수 취하면.
즉, deltaDistX 의 y증가분 미지수 x = rayDirY/rayDirX 가 된다.
- 이를 통해서 deltaDistX 를 구하는데 피타고라스의 정리를 활용하면
x증가분 1과 y증가분 rayDirY/rayDirX을 각각 제곱하여 더하게되는공식이
deltaDistX = sqrt(1 + (rayDirY rayDirY) / (rayDirX rayDirX)) 이 되는것이다.
이후의 수식을 유도해보자
deltaDistX = abs(|v| / rayDirX)
v 는 rayDir의 길이를 나타낸다 즉, v = sqrt(rayDirX^2 + rayDirY^2)
deltaDistX = sqrt(1 + (rayDirY rayDirY) / (rayDirX rayDirX))
위 deltaDistX = sqrt(rayDirX^2 / rayDirX^2 + rayDirY^2 / rayDirX^2 이므로
분모는 제곱근을 풀 수 있다. 즉, sqrt(rayDirX^2 + rayDirY^2) / rayDirX 이 됨.
- 아까 v = sqrt(rayDirX^2 + rayDirY^2)라고 했으므로
deltaDistX = v / rayDirX 로 볼 수 있다.
여기에서 v는 rayDir 의 길이라고 했고 rayDir 은 방향벡터이므로 1 이다.
즉, deltaDistX = 1 / rayDirX 가 된다.
//calculate step and initial sideDist
if (rayDirX < 0)
{
stepX = -1;
sideDistX = (posX - mapX) * deltaDistX;
}
else
{
stepX = 1;
sideDistX = (mapX + 1.0 - posX) * deltaDistX;
}
if (rayDirY < 0)
{
stepY = -1;
sideDistY = (posY - mapY) * deltaDistY;
}
else
{
stepY = 1;
sideDistY = (mapY + 1.0 - posY) * deltaDistY;
}
DDA 알고리즘을 시작하기 전 stepX, stepY 의 초기값 을 구해줍니다.
그리고 sideDistX, sideDistY 의 초기값 을 구해줍니다.
- sideDistX 의 값은, rayDirX 의 값이 양수 일 경우, '광선의 시작점부터 오른쪽 으로 이동하다 처음 만나는 x면까지의 거리'입니다.
- 반대로 rayDirX 의 값이 음수 일 경우, '광선의 시작점부터 왼쪽 으로 이동하다 처음 만나는 x면까지의 거리'입니다.
- sideDistY 의 값도 마찬가지입니다. (시작점 기준 위쪽 또는 아래쪽 )
- sideDistX, sideDistY 를 산출하기 위해 정수값인 mapX 와 실제위치인 posX 가 사용하고, 광선의 방향에 따라 산출식을 알맞게 설정합니다.
- rayDirX 의 경우 양수 일 경우, sideDistX 는 mapX + 1 에서 실제위치 posX 를 빼주고 deltaDistX 값을 곱해서 구할 수 있습니다.
- rayDirX 가 음수 일 경우, sideDistX 는 posX 에서 mapX 를 빼주고 deltaDistX 값을 곱해서 구할 수 있습니다.
- (주. 아래의 이미지는 튜토리얼 원본과는 무관하게 이해를 돕기위해 rayDirX가 양수인 경우에 대해 추가한 이미지 입니다)
//perform DDA
while (hit == 0)
{
//jump to next map square, OR in x-direction, OR in y-direction
if (sideDistX < sideDistY)
{
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
}
else
{
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
//Check if ray has hit a wall
if (worldMap[mapX][mapY] > 0) hit = 1;
}
이제 DDA 알고리즘을 시작 합니다.
광선이 벽에 부딪히면 루프가 종료됩니다.
- 이 때, 변수 side 의 값이 0이면 벽의 x면에, 1이면 벽의 y면에 부딪혔다는 것을 알 수 있고, 또 mapX, mapY 로 어떤 벽이랑 부딪힌 건지도 알 수 있습니다.
- 우리는 그 벽에서 정확히 어느 지점에서 부딪힌 건지는 알 수 없는데, 지금은 텍스쳐 없이 색상만 표현하기 때문에 몰라도 괜찮습니다.
벽을 만나 DDA가 완료되었으니 이제 광선의 시작점에서 벽까지의 이동거리 를 계산하겠습니다.
//Calculate distance projected on camera direction (Euclidean distance will give fisheye effect!)
if (side == 0) perpWallDist = (mapX - posX + (1 - stepX) / 2) / rayDirX;
else perpWallDist = (mapY - posY + (1 - stepY) / 2) / rayDirY;
여기서 사용되는 레이캐스팅 방법 은 광선의 이동거리를 계산하면서, 어안렌즈 효과를 보정하는 코드를 따로 추가하지 않고도 간단히 방지할 수 있는 방법입니다.
수직거리를 계산하는 방법 은 다음과 같습니다.
- 만약 광선이 처음으로 부딪힌 면이 x면이라면, __mapX - posX + (1 - stepX) / 2)__ 는 광선이 x방향으로 몇 칸이나 지나갔는지를 나타내는 수입니다 (정수일 필요는 없음).
- 만약 광선의 방향이 x면에 수직이면 이미 정확한 수직거리의 값이지만 대부분의 경우 광선의 방향이 있고 이 때 구해진 값은 실제 수직거리보다 큰 값이므로 __rayDirX__ 로 나누어줍니다.
- y면에 부딪힌 경우에도 같은 방식으로 계산해줍니다.
- __mapX - posX__ 가 음수이더라도 역시 음수인 __rayDirX__ 로 나누어 계산된 값은 항상 양수가 됩니다.
The image shows:
P: position of the player
H: hitpoint of the ray on the wall
perpWallDist: the length of this line is the value to compute now, the distance perpenducilar from the wall hit point to the camera plane instead of Euclidean distance to the player point, to avoid making straight walls look rounded.
yDist matches what is "(mapY - posY + (1 - stepY) / 2)" in the code above, this is the y coordinate of the Euclidean distance vector, in world coordinates.
Euclidean is the Euclidean distance from the player P to the exact hit point H. Its direction is the rayDir, but its length is all the way to the wall.
rayDir: the direction of the ray marked "Euclidean", matching the rayDirX and rayDirY variables in the code. Note that its length |rayDir| is not 1 but slightly higher, due to how we added a value to (dirX,dirY) (the dir vector, which is normalized to 1) in the code.
rayDirX and rayDirY: the X and Y components of rayDir, matching the rayDirX and rayDirY variables in the code.
dir: the main player looking direction, given by dirX,dirY in the code. The length of this vector is always exactly 1. This matches the looking direction in the center of the screen, as opposed to the direction of the current ray. It is perpendicular to the camera plane, and perpWallDist is parallel to this.
orange dotted line (may be hard to see, use CTRL+scrollwheel or CTRL+plus to zoom in a desktop browser to see it better): the value that was added to dir to get rayDir. Importantly, this is parallel to the camera plane, perpendicular to dir.
camera plane: this is the camera plane, the line given by cameraX and cameraY, perpendicular to the main player's looking direction.
A: point of the camera plane closest to H, the point where perpWallDist intersects with camera plane
B: point of X-axis through player closest to H, point where yDist crosses X-axis through the player
C: point at player position + rayDirX
D: point at player position + rayDir.
E: This is point D with the dir vector subtracted, in other words, E + dir = D.
points A, B, C, D, E, H and P are used in the explanation below: they form triangles which are considered: BHP, CDP, AHP and DEP.
And the derivation of the perpWallDist computation above then is:
1: "(mapY - posY + (1 - stepY) / 2) / rayDirY" is "yDist / rayDirY" in the picture.
2: Triangles PBH and PCD have the same shape but different size, so same ratios of edges
3: Given step 2, the triangles show that the ratio yDist / rayDirY is equal to the ratio Euclidean / |rayDir|, so now we can derive perpWallDist = Euclidean / |rayDir| instead.
4: Triangles AHP and EDP have the same shape but different size, so same ratios of edges. Length of edge ED, that is |ED|, equals length of dir, |dir|, which is 1. Similarly, |DP| equals |rayDir|.
5: Given step 4, the triangles show that the ratio Euclidean / |rayDir| = perpWallDist / |dir| = perpWallDist / 1.
6: Combining steps 5 and 3 shows that perpWallDist = yDist / rayDirY, the computation used in the code above
[Thanks to Roux Morgan for helping to clarify the explanation of perpWallDist in 2020, the tutorial was lacking some information before this]
//Calculate height of line to draw on screen
int lineHeight = (int)(h / perpWallDist);
//calculate lowest and highest pixel to fill in current stripe
int drawStart = -lineHeight / 2 + h / 2;
if(drawStart < 0)drawStart = 0;
int drawEnd = lineHeight / 2 + h / 2;
if(drawEnd >= h)drawEnd = h - 1;
이제 계산한 거리 (perpWallDist)로, 화면에 그려야하는 선의 높이 를 구할 수 있습니다.
- __perpWallDist__ 를 역수로 취하고, 픽셀단위로 맞춰주기 위해 픽셀 단위의 화면높이 __h__ 를 곱해서 구할 수 있습니다.
- 벽을 더 높게 그리거나 낮게 그리거나 하고 싶으면 2 * h와 같은 다른 값을 넣을 수도 있습니다.
- h값은 일정한 벽의 높이, 너비 및 깊이를 가진 박스처럼 보이게 해주고, 값이 클수록 높이가 높은 박스를 만들어줍니다.
- 이렇게 구한 __lineHeight__ (화면에 그려야 할 수직선의 높이)에서, 실제로 선을 그릴 위치의 시작 및 끝 위치를 알 수 있습니다.
- 벽의 중심은 화면의 중심에 있어야 하고, 이 중심점이 화면 범위 아래에 놓여있다면 0 으로, 화면 범위 위에 놓여있다면 h-1으로 덮어씌웁니다.
//choose wall color
ColorRGB color;
switch(worldMap[mapX][mapY])
{
case 1: color = RGB_Red; break; //red
case 2: color = RGB_Green; break; //green
case 3: color = RGB_Blue; break; //blue
case 4: color = RGB_White; break; //white
default: color = RGB_Yellow; break; //yellow
}
//give x and y sides different brightness
if (side == 1) {color = color / 2;}
//draw the pixels of the stripe as a vertical line
verLine(x, drawStart, drawEnd, color);
}
마지막으로, 광선이 부딪힌 벽의 색상값에 따라 표현할 색상을 선택 해줍니다.
- y면에 부딪힌 경우에 색상을 더 어둡게 설정하면 더 그럴듯하게 표현해 줄 수 있습니다.
- 그리고 verLine() 함수로 수직선을 그려줍니다.
- 여기까지의 과정을 모든 x값에 대해 반복한 후 이것으로 raycasting loop가 종료됩니다.
//timing for input and FPS counter
oldTime = time;
time = getTicks();
double frameTime = (time - oldTime) / 1000.0; //frameTime is the time this frame has taken, in seconds
print(1.0 / frameTime); //FPS counter
redraw();
cls();
//speed modifiers
double moveSpeed = frameTime * 5.0; //the constant value is in squares/second
double rotSpeed = frameTime * 3.0; //the constant value is in radians/second
레이캐스팅 loop를 마친 후, 현재 프레임과 이전 프레임의 시간을 계산합니다.
속도조정자 (speed modifier) 는 frameTime 과 상수를 이용해서 입력키로 인한 이동속도 또는 회전속도를 결정합니다.
- frameTime을 사용하면 이동속도 또는 회전속도를 프로세서의 속도와는 독립적으로 설정할 수 있습니다.
readKeys();
//move forward if no wall in front of you
if (keyDown(SDLK_UP))
{
if(worldMap[int(posX + dirX * moveSpeed)][int(posY)] == false) posX += dirX * moveSpeed;
if(worldMap[int(posX)][int(posY + dirY * moveSpeed)] == false) posY += dirY * moveSpeed;
}
//move backwards if no wall behind you
if (keyDown(SDLK_DOWN))
{
if(worldMap[int(posX - dirX * moveSpeed)][int(posY)] == false) posX -= dirX * moveSpeed;
if(worldMap[int(posX)][int(posY - dirY * moveSpeed)] == false) posY -= dirY * moveSpeed;
}
//rotate to the right
if (keyDown(SDLK_RIGHT))
{
//both camera direction and camera plane must be rotated
double oldDirX = dirX;
dirX = dirX * cos(-rotSpeed) - dirY * sin(-rotSpeed);
dirY = oldDirX * sin(-rotSpeed) + dirY * cos(-rotSpeed);
double oldPlaneX = planeX;
planeX = planeX * cos(-rotSpeed) - planeY * sin(-rotSpeed);
planeY = oldPlaneX * sin(-rotSpeed) + planeY * cos(-rotSpeed);
}
//rotate to the left
if (keyDown(SDLK_LEFT))
{
//both camera direction and camera plane must be rotated
double oldDirX = dirX;
dirX = dirX * cos(rotSpeed) - dirY * sin(rotSpeed);
dirY = oldDirX * sin(rotSpeed) + dirY * cos(rotSpeed);
double oldPlaneX = planeX;
planeX = planeX * cos(rotSpeed) - planeY * sin(rotSpeed);
planeY = oldPlaneX * sin(rotSpeed) + planeY * cos(rotSpeed);
}
}
}
예제코드의 마지막 부분은 입력키를 다룹니다. 우선 readKeys()로 입력된 키값을 읽어옵니다.
원문링크
[전편으로] 기초 : 아주 기본적인 원리
[다음편으로] 고급 : 예제코드로 이해하는 레이캐스터 구현 (textured)