일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 데이터베이스
- 아이패드다이어리
- Xcode
- sql
- 프로그래밍언어론
- DBMS
- 스프링부트 웹 소켓
- 오라클
- swift
- AI
- 스프링부트
- JPA
- 오블완
- springboot
- MySQL
- CD
- 소켓
- libasm
- 다이어리
- 스프링
- 42seoul
- IOS
- Spring
- 인공지능
- 티스토리챌린지
- jenkins
- javascript
- 리눅스
- CI
- 네트워크
- Today
- Total
Hi yoahn 개발블로그
[42Seoul/Cub3d] #2 cub3D 벡터를 이용해 화면 그리기 본문
먼저, 코드를 짜면서 대충 감을 익히고 짠 코드와 설명을 비교하면서 공부하려고 한다.
참고한 자료 >
github.com/365kim/raycasting_tutorial/blob/master/3_untextured_raycaster.md
365kim/raycasting_tutorial
(한글) 레이캐스팅 튜토리얼 번역. Contribute to 365kim/raycasting_tutorial development by creating an account on GitHub.
github.com
typedef struct s_img
{
void *img;
int *data;
int img_width;
int img_height;
int size_l;
int bpp;
int endian;
} t_img;
typedef struct s_game
{
void *mlx;
void *win;
t_img img;
int buf[screenHeight][screenWidth];
int **texture;
double posX;
double posY;
double dirX;
double dirY;
double planeX;
double planeY;
double moveSpeed;
double rotSpeed;
} t_game;
int main(int argc, char *argv[])
{
t_game game;
game.posX = 22.0; // x and y start position
game.posY= 11.5; // player start position vector
game.dirX = -1.0;
game.dirY = 0.0; // player 초기 direction vector
game.planeX = 0; //the 2d raycaster version of camera plane
game.planeY = 0.66; // player 카메라 평면
game.moveSpeed = 0.05; // 이동 속도
game.rotSpeed = 0.05; // 회전 속도 rotate speed
먼저, 변수들을 선언하고 초기화한다.
posX, posY : 플레이어의 초기 위치를 나타내는 변수
dirX, dirY : 플레이어의 방향벡터
planeX, planeY : 플레이어의 카메라 평면
- 카메라 평면은 플레이어의 방향벡터와 수직이어야 한다.
moveSpeed : 플레이어 이동 속도
rotSpeed : 플레이어 회전 속도 (rotate)
time, oldTime 은 프레임 계산하는데 사용 -> 아직 안씀
window_init(); // window 생성
// 변수 초기화
for (int i = 0;i < screenHeight;i++)
{
for (int j = 0;j < screenWidth;j++)
game.buf[i][j] = 0;
}
// 변수 할당
if (!(game.texture = (int**)malloc(sizeof(int*) * 8)))
return -1;
for (int i = 0;i < 8;i++)
{
if (!(game.texture[i] = (int*)malloc(sizeof(int)*(texHeight * texWidth))))
return -1;
}
for (int i = 0;i < 8;i++)
{
for (int j = 0;j < texHeight * texWidth;j++)
game.texture[i][j] = 0;
}
load_texture(&game);
game.img.img = mlx_new_image(game.mlx. screenWidth, screenHeight);
game.img.data = (int*)mlx_get_data_addr(game.img.img, &game.img.bpp, &game.img.size_l, &game.img.endian);
mlx_hook(game.win, X_EVENT_KEY_PRESS, &key_press, &game);
mlx_loop_hook(game.mlx, &main_loop, &game);
mlx_loop(game.mlx);
}
load_texture(&game);
game.texture[] 배열 변수에 2차원 이미지 정보를 1차원 배열에 저장
mlx_new_image(game.mlx, screenWidth, screeenHeight);
새로운 이미지를 메모리에 생성
mlx_get_data_addr(game.img.img, &game.img.bpp, &game.img.size_l, &game.img.endian);
새로운 이미지를 생성하여 정보를 반환
user가 이미지 정보를 가지고 수정할 수 있다.
int main_loop(t_game *game)
{
for (int x = 0; x < screenWidth; x++)
{
double cameraX = 2 * x / (double)screenWidth - 1;
double rayDirX = game->dirX + game->planeX * cameraX;
double rayDirY = game->dirY + game->planeY * cameraX;
int mapX = (int)game->posX; // 현재 광선의 위치
int mapY = (int)game->posY;
double sideDistX, sideDistY;
double deltaDistX = fabs(1 / rayDirX);
double deltaDistY = fabs(1 / rayDirY);
double perpWallDist;
int stepX;
int stepY;
int hit = 0;
int side; // NS or EW hit?
for 문을 돌면서 화면에 세로줄을 왼쪽 끝 부터 한줄씩 그린다.
cameraX : for문의 x 값 (화면의 수직선)이 카메라 평면에서 차지하는 x 좌표
( 범위: -1 ~ 1 )
rayDirX, rayDirY : 광선의 방향벡터
광선의 방향 = ( 방향벡터 ) + ( 카메라 평면 x 배수 )
sideDistX : 시작점 ~ 첫번째 x 축 면을 만나는 점까지의 거리
sideDistY : 시작점 ~ 첫번째 y 축 면을 만나는 점까지의 거리
deltaDistX : 첫번째 x 축 면 ~ 바로 다음 x 면까지의 광선의 이동 거리 ( x += 1 )
deltaDistY : 첫번째 y 축 면 ~ 바로 다음 y 면까지의 광선의 이동 거리 ( y += 1 )
perpWallDist : 나중에 광선의 이동거리를 계산하는데 사용
DDA 알고리즘은 반복문을 실행할때마다 x / y 방향으로 딱 한칸씩 점프한다.
(광선의 방향에 따라 건너뛰는 방향이 달라진다 -> stepX, stepY 에 +1 또는 -1 로 저장됨)
if (rayDirX < 0)
{
stepX = -1;
sideDistX = (game->posX - mapX) * deltaDistX;
}
else
{
stepX = 1;
sideDistX = (mapX + 1.0 - game->posX) * deltaDistX;
}
if (rayDirY < 0)
{
stepY = -1;
sideDistY = (game->posY - mapY) * deltaDistY;
}
else
{
stepY = 1;
sideDistY = (mapY + 1.0 - game->posY) * deltaDistY;
}
step의 값은 광선의 방향이 음수이면 -1, 양수이면 +1로 설정
rayDirX가 0이면 stepX는 사용되지 않음
while (hit == 0)
{
if (sideDistX < sideDistY)
{
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
}
else
{
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
if (worldMap[mapX][mapY] > 0)
hit = 1;
}
벽에 부딪힐 때까지 매번 한 칸씩 광선을 이동시키는 루프
hit 이 1이 되면 벽에 부딪혔음을 의미, 반복문을 종료
stepX를 사용하면 x방향으로 한칸, stepY를 사용하면 y 방향으로 한칸 점프
광선의 방향이 x 축 방향과 완전히 일치하면, 반목문을 돌 때 x 방향으로만 한 칸씩 점프
광선의 y축 방향과 일치하면 x방향으로는 점프하지 않음
광선이 점프할 때마다 sideDistX, sideDistY에 deltaDistX, deltaDistY가 더해지면서 값이 업데이트된다.
광선이 점프할 때마다 mapX, mapY는 stepX, Y 값이 더해지면서 값이 업데이트 된다.
광선이 벽에 부딪히면 루프가 종료된다.
이 때 side 값이 0이면 x면 벽에 부딪힌 것이고, 1이면 y면 벽에 부딪힌 것을 알 수 있다.
mapX, mapY 값으로 어떤 벽과 부딪힌 건지 알 수 있다.
벽을 만나 dda가 완료되었으니 광선의 시작점에서 벽까지의 이동거리 계산
- 그릴 벽의 높이를 알아내는데 사용된다.
- 실제 거리 값을 사용하면 벽이 둥글게 보이는 어안렌즈 효과가 생겨서, 이를 피하기 위해 카메라 평면까지의 거리를 사용한다.
if (side == 0)
perpWallDist = (mapX - game->posX + (1 - stepX) / 2) / rayDirX;
else
perpWallDist = (mapY - game->posY + (1 - stepY) / 2) / rayDirY;
if (side == 0)
// 광선이 처음으로 부딪힌 면이 x 면 이라면, mapX - posX + (1 - stepX) / 2)는 광선이 x 방향으로 몇 칸이나 지나갔는지를 나타내는 수이다. (정수일 필요 없음)
광선의 방향이 x 면에 수직이면 이미 정확한 수직 거리의 값이다. 하지만 대부분 광선의 방향이 있고 이때 구해진 값은 실제 수직거리보다 큰 값이므로 rayDirX로 나누어준다.
else
// side == 1 , y 면에 부딪힌 경우에도 같은 방식으로 계산
// screen 에 그릴 선의 높이
int lineHeight = (int)(screenHeight / perpWallDist);
// 수직선의 높이에서 시작과 끝 위치 계산
int drawStart = -lineHeight / 2 + screenHeight / 2;
if (drawStart < 0)
drawStart = 0;
int drawEnd = lineHeight / 2 + screenHeight / 2;
if (drawEnd >= screenHeight)
drawEnd = screenHeight - 1;
계산한 거리 perpWallDist로 화면에 그려야하는 선의 높이를 구할 수 있다.
-> perpWallDist를 역수로 취하고, 픽셀 단위로 맞추기 위해 픽셀단위의 화면 높이를 곱해서 구할 수 있다.
- 벽의 높이를 조절하고 싶으면 곱하는 화면 높이의 값을 늘리거나 줄인다.
- screenHeight 값은 일정한 벽의 높이, 너비 및 깊이를 가진 박스처럼 보이게 해준다. 값이 클수록 높은 박스를 만들어준다.
-> 구해진 lineHeight 값에서 (화면에 그릴 수직선의 높이), 실제로 선을 그릴 위치의 시작 및 끝 위치를 알 수 있다.
- 벽의 중심은 화면의 중심에 있어야 하고, 이 중심점이 화면 범위 아래에 놓여있다면 0으로, 화면 범위 위에 놓여있다면 h - 1 로 덮어 씌운다.
1 > 특정 색으로 벽을 칠하는 경우
int color;
if (worldMap[mapY][mapX] == 1)
color = 0xff0000;
else if (worldMap[mapY][mapX] == 2)
color = 0x00ff00;
else if (worldMap[mapY][mapX] == 3)
color = 0x0000ff;
else if (worldMap[mapY][mapX] == 4)
color = 0xffffff;
else
color = 0xffff00;
if (side == 1)
color /= 2;
verLine(game, x, drawStart, drawEnd, color);
}
return (0);
}
광선이 부딪힌 벽 == worldMap[mapY][mapX]
광선이 부딪힌 벽의 값에 따라 색상값을 정하고, 그 색상값을 저장해둔다.
color /= 2 는
색상 숫자를 2로 나눠 더 어둡게 보이도록 설정한다.
if (side == 1)
y면에 부딪힌 경우 색상을 어둡게 보이도록 설정한 것
void verLine(t_game *game, int x, int y1, int y2, int color)
{
int y;
y = y1;
while (y <= y2)
{
mlx_pixel_put(game->mlx, game->win, x, y, color);
y++;
}
}
세로줄을 한줄씩 그리는 것이기 때문에 세로줄을 그릴 x축 좌표, 세로줄을 그릴 y축의 시작과 끝 좌표만 주어지면 된다.
2 > 텍스쳐를 표현한 레이캐스터
수직선을 그리는 방식은 사용할 수 없다.
대신 픽셀을 하나하나 그려주어야 한다.
-> 스크린버퍼를 사용하여 정보를 모아 한번에 화면에 출력한다.
int texNum = worldMap[mapX][mapY] - 1; // - 1
double wallX;
if (side == 0)
wallX = game->posY + perpWallDist * rayDirY;
else
wallX = game->posX + perpWallDist * rayDirX;
wallX -= floor(wallX);
int texX = (int)(wallX * (double)texWidth);
if (side == 0 && rayDirX > 0)
texX = texWidth - texX - 1;
if (side == 1 && rayDirY < 0)
texX = texWidth - texX - 1;
texNum은 map에 저장된 값의 변수에 따라 다른 텍스처 종류를 의미한다.
( -1 을 해서 0번째 텍스처와 벽이 없는 것을 구분한다. 값이 1 일 때 0번째 텍스처이미지를 사용할 수 있게 함 )
wallX 값은 벽의 정확히 어디에 부딪혔는지 double 형 좌표로 나타낸다.
이 값은 텍스처를 적용할 때 어떤 x 좌표를 사용해야 하는지 판단할 때 사용함
-> x 면과 부딪힌 경우 (side == 0) wallX 는 x 좌표가 되지만,
y 면과 부딪힌 경우 (side == 1)에는 벽의 y 좌표가 된다.
벽면 어디에 부딪혔는지 double 값으로 정확히 구해( posY + perpWallDist * rayDirY = 현재 위치 + 부딪힌 벽의 거리 * 광선 방향 ) int값을 빼서 판단한다. ( 0 - 1 범위 )
하지만 텍스처를 적용할 때의 wallX 값은 텍스처의 x 좌표에 사용된다.
wallX의 값을 이용해서 텍스처의 x 좌표를 나타내는 texX 를 계산 ( wallX * texWidth )
-> 이 x 좌표는 해당 수직선 상에서 그대로 유지된다.
double step = 1.0 * texHeight / lineHeight;
double texPos = (drawStart - screenHeight / 2 + lineHeight / 2) * step;
for (int y = drawStart; y < drawEnd; y++)
{
int texY = (int)texPos & (texHeight - 1);
texPos += step;
int color = game->texture[texNum][texHeight * texY + texX];
if (side == 1)
color = (color >> 1) & 8355711;
game->buf[y][x] = color;
}
}
이제 수직선 상의 각 픽셀이 텍스처의 어떤 y 좌표 (= texY ) 값을 갖게 할건지 정하기 위해 y방향 반복문을 돌 차례이다.
texY의 값은 첫 줄에서 계산한 step 의 크기만큼 증가하면서 계산된다.
step의 크기는 텍스처의 좌표를 수직선 상에 있는 좌표에 대해 얼마나 늘려야 하는지에 따라 결정된다. double 형에서 int 형으로 타입캐스팅하여 텍스처 픽셀값을 선택하도록 한다.
texY = (int)texPos & (texHeight - 1); == texPos % texHeight 와 같은 결과
buffer[y][x]에 넣을 픽셀의 색상 color = texture[texNum][texX][texY]
만약 side가 1이면 벽의 y면에 부딪힌 것이다. -> 더 어둡게 표현하기 위해 2로 나눔
1. 입력 키에 따라 플레이어 이동하기
int key_press(int keycode, t_game *game)
{
if (keycode == KEY_ESC)
exit(0);
if (keycode == KEY_W || keycode == 126)
{
if (!worldMap[(int)(game->posX + game->dirX * game->moveSpeed)][(int)(game->posY + game->dirY * game->moveSpeed)])
{
game->posX += game->dirX * game->moveSpeed;
game->posY += game->dirY * game->moveSpeed;
}
}
if (keycode == KEY_S || keycode == 125)
{
if (!worldMap[(int)(game->posX - game->dirX * game->moveSpeed)][(int)(game->posY - game->dirY * game->moveSpeed)])
{
game->posX -= game->dirX * game->moveSpeed;
game->posY -= game->dirY * game->moveSpeed;
}
}
if (keycode == KEY_D || keycode == 124)
{
double oldDirX = game->dirX;
game->dirX = game->dirX * cos(-game->rotSpeed) - game->dirY * sin(-game->rotSpeed);
game->dirY = oldDirX * sin(-game->rotSpeed) + game->dirY * cos(-game->rotSpeed);
double oldPlaneX = game->planeX;
game->planeX = game->planeX * cos(-game->rotSpeed) - game->planeY * sin(-game->rotSpeed);
game->planeY = oldPlaneX * sin(-game->rotSpeed) + game->planeY * cos(-game->rotSpeed);
}
if (keycode == KEY_A || keycode == 123)
{
double oldDirX = game->dirX;
game->dirX = game->dirX * cos(game->rotSpeed) - game->dirY * sin(game->rotSpeed);
game->dirY = oldDirX * sin(game->rotSpeed) + game->dirY * cos(game->rotSpeed);
double oldPlaneX = game->planeX;
game->planeX = game->planeX * cos(game->rotSpeed) - game->planeY * sin(game->rotSpeed);
game->planeY = oldPlaneX * sin(game->rotSpeed) + game->planeY * cos(game->rotSpeed);
}
return (0);
}
위쪽 화살표 - 앞으로 이동
worldMap[posX + dirX * moveSpeed][posY + dirY * moveSpeed] == 0 이면
움직이려고 하는 위치에 벽이 없으므로
posX += dirX * moveSpeed
posY += dirY * moveSpeed
연산을 한다
-> 충돌 감지 기능
플레이어가 움직이려고 하는 최종 위치는 x, y 이므로 움직이려고 하는 위치에 벽이 없으면 posX, posY 값을 변화시키고, 벽이 있으면 아무런 작업을 하지 않는다.
원래 코드는 x 위치 y위치를 나눠서 비교했는데, 그랬더니 앞에 벽이 있으면 한쪽으로 밀리는 현상이 발생하여 변경했다.
'42 SEOUL > 배운 것들 정리' 카테고리의 다른 글
[42Seoul] push_swap 풀이 과정 (0) | 2021.05.24 |
---|---|
[42Seoul/cub3D] #3 벽과 바닥 raycasting (0) | 2021.03.31 |
[42Seoul/exam] exam02 공부 (0) | 2021.03.18 |
[42Seoul/Cub3d] #1 cub3d 시작하기 (0) | 2021.03.12 |
[42Seoul/ft_server] nginx 설정파일 (autoindex / ssl / redirect 설정) (0) | 2021.02.16 |