jo16
좌충우돌 기록기
jo16
전체 방문자
오늘
어제
  • 분류 전체보기 (30)
    • NLP (1)
    • 일반 (0)
    • 취업 (1)
    • 42seoul (1)
    • 운영체제 (1)
    • 컨퍼런스 (1)
    • 데이터베이스시스템 (5)
    • 알고리즘 (10)
    • 회고 (0)
    • Deep Learning Specializatio.. (9)
      • Neural Networks and Deep Le.. (4)
      • Improving Deep Neural Netwo.. (3)
      • Convolutional Neural Networ.. (0)
      • Sequence Models (0)
      • Structing Machine Learning .. (2)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 컴퓨터공학
  • relational algebra
  • Ai
  • 42seoul
  • 개발자컨퍼런스
  • 머신러닝
  • NAVERDEVIEW2023
  • raycasting
  • mlx
  • 복습
  • Cub3D
  • 데이터베이스시스템
  • 삼성대학생인턴
  • 딥러닝
  • dbms
  • 첫 취준
  • 네이버 deview
  • Computer Graphics
  • 강의정리
  • 삼성SW역량테스트
  • relational model
  • cs
  • KEY

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
jo16

좌충우돌 기록기

[cub3D] 회고
42seoul

[cub3D] 회고

2023. 3. 11. 21:46
레이캐스팅을 이용한 3D 구현과제
과제 기간 + 구현 사항 

시작 날 : 2/17
통과 날: 3/10 
 
2/17 ~ 2.24 : 개념학습 및 맵 파싱
2/25 : 2d 맵 구현
2/26 ~ 3/1 : 3d 구현 완
3/4 ~ 3/6 : 트러블 슈팅
3/7 : 트러블슈팅, makefile 및 norm 수정
3/9 : 마지막 트러블 슈팅. 제출
3/10 : 평가 완료
 
맨데토리 : 전부 충족
보너스
 벽 충돌 ✔️
 미니맵 구현 ✔️
  스프라이트 구현
  마우스에 따른 회전 구현
  열리고 닫히는 문 구현 
메모리 릭 : 없음 확인 (에러 종료나, ESC 종료하기 전에 전부 free하고 exit함)


공부과정
레이캐스팅 개념 학습 (텍스처 이전) -> 맵 파싱 -> 2d 맵 그리기 -> 개념학습 (텍스처) -> 3d 구현 -> 각종 트러블 슈팅 

예전에 학교에서 그래픽스를 어느정도 배우기는 했지만, opengl을 사용하였으며 공식을 빈칸넣기식으로 하는 과제들이었기때문에 다시 새롭게 공부하는 느낌이었다. 레이캐스팅을 구현하는 데에는 크게 두 가지 방법이 있는데, 첫 번째는 벡터 기반 방법이고 두 번째는 각도 기반 방법이다. 두 방법 모두 장단점이 있을 수 있겠지만, 내 기준 각도 기반 방법이 조금 더 직관적이고 이해하기 쉬웠는데, 벡터 기반 방법에 비해 자료가 많지 않으며 텍스처 적용부터 난이도가 급상승한다고 한다. 처음에 팀원과 어느정도 날까지 개념학습을 하기로 했는데 팀원은 벡터 기반 방법으로 공부하고 나는 각도 기반 방법으로 공부해와서 완전히 다른 느낌으로 공부를 해왔다. 따라서 조금 더 많은 자료가 있는 벡터 기반 방법으로 통일하여 다시 학습을 하였다. 구현 방법은 다르지만 두 방법 모두 공부한 것이 얼추 과제 개념을 익히는 데에 도움이 되었던 것 같다. 보통 공부는 기존 자료나 코드에 주석을 다는 식으로 하고, 증명이 필요한 부분에 대해서는 직접 노트에 정리하면서 이해하였다. 쭉쭉 공부해 나가다가 텍스처 부분부터 한번에 이해하기가 버겁고, 학습도 점점 지겨워져서 일단 눈에 보이는 파싱과 파싱에 맞는 2d 맵을 생성하였다.
 
생성하는 과정에서 이전에 so long에서 했던 mlx 복습도 하였다. 그때는 텍스처를 올리는 것은 해보았지만 직접 그림을 그린 적은 없어서 맵을 구현하면서 조금 헤맸었다. 그래도 눈에 보이는 결과가 나오니까 점차 재미를 붙였다. 얼추 로데브 자료를 보면서 학습이 끝났을 때, 직접 구현하기 시작하였다. 그리고 코드를 짜는 과정에서 엄청난 좌충우돌을 겪는데 .. 이미 cub3D 이번 게시물에서는 코드 자체보다는 과제의 큰 그림과 트러블 슈팅 기록에 초점을 맞출 것이다. 똑같은 실수를 반복하지 않기 위해


과제 개요

마지막으로 C언어를 사용하는 과제이다.
레이캐스팅이란 무엇일까? 이름 자체를 자세히 들여다 보면 알 수 있다. 말 그대로 플레이어 위치를 기준으로, 가상의 빛(ray)을 투사(casting)해 벽을 감지하는 기술이다. 벽을 감지하여 벽까지의 거리를 계산하고, 적절한 비례식에 맞게 실제 벽을 세워서 그려주면 우리 눈에 3d처럼 보이게 된다. 즉 플레이어에서 벽 까지의 거리가 길면 비례식에 의해 작은 크기의 벽이 세워질 것이고,  플레이어에서 벽 까지의 거리가 짧으면 큰 크기의 벽이 세워질 것이다. 이 기술은 실제 2d맵을 많은 수학적 계산이나 리소스를 들이지 않아도 3d 게임을 구현할 수 있다는 점에서 의의가 있다. 실제로, 90년대 초반에 개발된 올펜슈타인 게임은 당시 3D 게임을 돌릴 수 없었던 컴퓨터 성능에서도 이 기술을 사용하여 3D같은 사실감을 주어 큰 반향을 일으켰다. (subject에서도 이 게임에 대해 소개하고 있다.)


구현 방법 + flow chart

그렇다면 우리가 처음에 따져봐야 할 것은 '어떻게' 벽을 감지할 것인지이다. 단순히 생각하여 일정 값을 더하는 식으로 벽을 감지하는 것을 불완전한 방법이다. 왜냐하면 너무 작은 간격으로 탐색을 하면 속도 저하와 함꼐 많은 부하가 있을 것이고, 너무 큰 간격으로 탐색을 하면 벽을 놓칠 수 있기 때문이다. 여기서 우리는, 벽은 어떠한 경우에도 정수 위에 있다는 점을 이용한다. 2d맵을 인덱싱하는 방식이기 때문에 벽이 소수점 위에 있을 수는 없다. 따라서 x면과 y면을 탐색하여 조금 더 가까이에 광선이 지나는 지점을 기준으로 1칸씩 옮기며 벽을 탐지해나가는데, 이 알고리즘을 DDA(Digital Differential Analysis)알고리즘이라고 한다. 벽을 탐지해나가는 알고리즘은 브레젠헴 알고리즘도 있지만, 우리 팀은 더 직관적으로 이해가 쉽고 코드가 짧은 DDA를 사용하기로 했다. 이를 기반으로 전체적인 흐름도는 다음과 같다.
 

<파싱>
subject 요구사항 중 하나가, configuration file에서 맵은 항상 고정적으로 마지막으로 오지만, 그 전에 오는 정보들은 순서가 바뀌어도 제대로 처리 되어야 한다. 따라서 똑같이 읽어들이더라도 read info 와 read map 을 구분하였다. 그렇게 해서 파일의 정보를 모두 읽어들인 뒤, 맵이 제대로 된 맵인지 판단하는 과정을 거친다. 
맵이 적절한지 판단하는 기준은 다음과 같이 하였다. 
 
1. 맵에 플레이어는 한 명인가?
2. 1, 0, ' '(공백) 외에 다른 문자가 있는가?
3. 맵이 1로 둘러싸여있는가? 
 
so long 과제의 경우 맵이 사각형으로 고정이었기때문에 판단이 비교적 쉽지만, cub3D의 경우 벽으로만 둘러싸여있으면 어떤 형태여도 상관없기때문에 조금 더 판단 과정이 까다롭다. 우리 팀의 경우 0을 기준으로 상하좌우를 탐색하여, 공백이 있으면 벽이 뚫렸다고 판단하는 방식을 사용하였다. 상하좌우를 판단할 때 세그가 안뜨기 위해서는 첫 번째 행, 첫 번째 열, 마지막 행, 마지막 열인 경우는 따로 판단해주어야 할 것이다.  
 
<실행>
main loop의 큰 흐름은 단순하다. 먼저 천장과 바닥 색상을 버퍼에 넣고, ray casting을 통해 그릴 벽의 색을 버퍼에 넣은 뒤, draw 함수로 칠해준다. ray casting의 자세한 흐름도는 다음과 같다.

즉, x가 전체 화면 너비만큼 픽셀 단위로 돌면서, 각 x마다 적절한 세로선의 벽을 그려주는 작업을 수행한다. 

보통 과제를 하게 되면 임의 색으로 벽을 칠하는 것을 먼저하고 무사히 돌아갔을 때 텍스처를 올리게 된다. 우리 팀의 경우는 한번에 텍스처를 올리려다가 많은 에러를 겪었지만.. 처음부터 차근차근 하기를 권장한다. 벽과 플레이어 사이 거리를 구하는 데에 중요한 점이 있는데, 만약 나이브하게 거리를 구하게 되면 벽이 우리의 의도와 달리 어안렌즈처럼 보이게 될 것이다.

왜냐하면, 플레이어 입장에서 길게 늘어진 벽이 있다고 가정해보자. 광선을 쏠 때 수선의 발을 내린 길이의 광선과 대각선으로 벽을 보았을 때의 광선의 길이는 모두 다를 것이다. 따라서 광선의 길이에 맞게 벽의 크기도 모두 달라질 것이다. 하지만 우리의 직관과 맞기 위해서는 모두 같은 크기의 벽이 있어야 한다. 

따라서 플레이어 위치에서 길게 선을 긋게 되면 모두 같은 길이의 광선의 길이를 가질 것이다. 따라서 나이브하게 그린 길이를 수선의 발을 내린 길이로 보정해주는 과정이 필요하다. 이 과정을 수행하는 작업을 calculate: wall dist에서 수행한다. 참고 자료에 이에 대한 증명이 자세히 적혀 있어 증명은 생략한다. 간단히 말해 삼각형의 닮은 비를 이용하여 비례식을 구할 수 있을 것이다. 
 
여기까지하면 벽에 임의의 색을 칠하는 것 까지 할 수 있다. 플로우 차트에 파란색으로 표시한 부분이 텍스처를 올리는 부분인데, subject의 요구사항에 의하면 동서남북에 따라 다른 텍스처를 올릴 수 있어야 한다. 따라서 텍스처를 벽으로 올리기 위해서는
1. 동서남북 중 어느 방향인지 벽의 방향 결정 (wall direction)
2. 충돌 지점에 대한 벽의 상대적 위치 ( texture 거리)
이 두가지를 알아야 특정 텍스처의 색을 알아낼 수 있을 것이다. 벽의 방향은 광선의 기울기 부호와 부딪힌 면이 X면인지 Y면인지에 따라 결정된다. 그리고 벽의 상대적 위치는 삼각형의 닮음비를 이용하여 구한다. 


트러블 슈팅

 이 게시물을 쓰는 가장 큰 이유이다. 레이캐스팅은 굉장히 자료가 많아서 솔직히 따라하기만 해도 어느정도 결과가 나올 수 있다. 그럼에도 불구하고 우리 팀은 상당한 삽질을 했다.. 개념을 확실히 몰라서 생긴 삽질도 있지만 정말 사소한 것인데 발견하지 못해서 하루를 날린 삽질도 있기때문에 다시는 이런 실수를 하지 말자고 다짐하는 의미로 적어본다.
 
1. mlx 함수에서 최종적으로 윈도우 창에 image를 올리기 위해서는 반드시 mlx_put_image_to_window()를 호출해주어야 한다.
 
2. 플레이어가 부드럽게 이동하지 않고 정수 단위로 끊겨서?이동하는 현상이 있는데, 이것은 좌표를 double로 받으면 부드럽게 이동하도록 할 수 있다.
 
3. 레이캐스팅하여 버퍼에 그림을 그렸을 때, 한 줄만 그려진 것 같은 현상이 있었다. 어디서 이런 문제가 나오는지 알지 못하여 엄청나게 삽질하였는데, 너무나 황당한 이유였다. 이차원 배열인 버퍼를 이미지에 띄우기 위한 함수에서 이중 반복문을 썼는데, 내부 반복문에서 초기화를 안해줬었다.. 그러니까 한줄만 그려지고 그 이후로 초기화가 이루어지지 않으므로 그려지지 않은 것이다. 이상한 삽질이었지만, 이유를 알아내려고 코드를 몇 번씩이나 읽으면서 제대로 출력되는지 확인하였는데, 그 과정에서 레이캐스팅에 대해 더 이해할 수 있었던 것 같다. 맵에서 동서남북을 쳤을 때 알맞게 선의 각도가 변하는 것을 보고, 공식 자체에는 문제가 없음을 알 수 있었다.  
 
3. 드디어 3D 화면이 그려졌다. 그런데 회전을 구현하기 위해 회전행렬을 곱해주었는데 정상적으로 돌지 않고 마치 좌우반전이 일어나는 것 처럼 작동하였다. 이 이유에 대해서도 한참 헤맸는데, 단순히 회전행렬을 곱할 때 부호 하나를 오타로 인해 실수 했었다. 단순히 철자 실수라고 볼 수도 있지만, 내가 회전행렬을 코드화한 식을 확실히 알았더라면 이러한 실수를 할 수 있을지 반성도 하였다. 
 
4. 그렇게 해서 3D 화면을 구현하고, 기존에 만들었던 2D 맵과 합쳐보았는데, 그렇게 두 맵을 비교하니까 의도와 다른 점들을 발견하였다. 
 4-1. 먼저, 3D맵은 윗쪽(북쪽)으로 움직이는데, 이와 달리 2D맵은 아랫쪽으로 움직인다.
 4-2 따라서 직관과 맞게 2D맵에서 움직이도록 하고 싶어서 똑같이 북쪽으로 움직이게 수정하였더니 맵이 좌우반전되는 현상이 일어났다. 
  만약 2d맵 모양이 이라면
1111
 1001
1111
 
 1111
1001
 1111
  이런 식으로 좌우반전해서 맵이 나오는데. 텍스처는 똑바로 나왔기 때문에 멘붕상태에 빠졌디.
  결론적으로는 처음에 벡터 초기화 설정에 문제가 있음을 발견하였다. 초기 벡터값을 설정할 때 제대로 생각하지 않고, 제대로 나올 때까지 값을 바꿔가며 넣었기때문에, 잘못된 형태의 값이 나와있었다.

예를 들어 N을 간다고 치면 초기 벡터가 dirX는 0, dirY는 -1, plane_y는 0, plane_x벡터는 0.66 (값은 자유. 양수) 가 나와야 할 것이다. 역으로  S라면 dirY는 1, plane_x는 -0.66이어야 할 것이다. 벡터 개념이 익숙하지 않기도 했고, 텍스처를 바로 보이게 하려고 벡터를 이리저리 계산없이 수정하다가 나온 결과였다. 다시 계산하고 초기값을 넣으니 맵은 똑바로 보이되 텍스처가 좌우반전이 되어 나왔다. 따라서 텍스처 그리는 함수에서 보정해주는 조건을 반대로 하니 텍스처가 반대로 나오는 현상은 해결되었다.

아마 로데브 자료와는 조건이 다를 것이다.

만약 우리 팀과 같이 맵이 제대로 나오지 않거나, 텍스처가 반대로 나오는 현상이 있을 경우 위 함수들을 참고해보면 좋을 것이다. 미니맵 구현 자체가 어렵지 않은데, 이렇게 미니맵을 구현해야 3D 화면이 맵 의도대로 나오는지 제대로 확인할 수 있기 때문에 구현하는 것을 추천한다. 
5. 벽 뚫림 현상
 벽 충돌은 보너스 구현사항이다. 벽이 뚫고 가는 현상 자체는 현재 플레이어 위치를 int 형변환을 하여 index가 벽이면 못가는 식으로 조건문 하나로 간단하게 막을 수 있다. 또한 벽이 뚫리도록 둘 경우 언젠가는 세그가 뜰 수 있기 때문에 오히려 구현을 하는 쪽이 마음이 편하다.  그런데 플레이어가 움직이다가 정확히 벽의 인덱스에 도달할 경우, 일시적으로 벽이 뚫려보이는 문제가 있었다. (만약 플레이어의 인덱스가 4이고 벽이 5인데, 플레이어가 0.5씩 이동한다면? 4 -> 4.5 -> 5(더이상 가지 못함) 로 이동할 때 5 위치에서 문제가 되는 것이다.) 

 
우리는 이 문제를 해결하기 위해 처음에는  hit box를 두어, 앞으로 갈 방향에 살짝의 값을 더 더해서 벽이 감지되는 경우 더 가지 못하도록 설정하였다.
 그런데 그렇게 할 경우 벽이 화면의 중간에 있을 경우 벽을 감지하지 못하고 뚫고 가는 현상이 있었다. 따라서 방법을 유지하되, 플레이어가 갈 방향에서 hit방향을 x에 더하는 경우, 빼는 경우, y에 더하는 경우, 빼는 경우 4가지 조건을 두어 이 조건을 충족할 때에만 갈 수 있도록 하였다. 그렇게 하니 모든 경우에 대해 벽이 뚫려보이는듯한 현상을 방지할 수 있었다.

6. 플레이어 초기 위치 변동
 5번과 같이 문제를 해결할 경우, 플레이어를 벽 바로 옆에 위치시키면 동작키가 먹지 않는다. 그 이유에 대해 생각해보았는데, 맵 상의 인덱스와 3D에서 플레이어의 위치의 괴리감이었다. 따라서 X과 Y에 대해 0.49씩 더해주었더니 (0.5를 더하면 반올림때문에 세그가 뜰 수 있다) 해결되었다. 
 
어찌보면 사소한 이슈들이지만, 이슈가 발생한 위치나 이유를 알지못해 코드를 짠 시간보다 트러블 슈팅하는 데에 더 오랜 시간을 쓴 것 같다. 앞으로는 이러한 실수를 하지 않기를 바라며.. 
 

회고

 

에러를 고치면서 느낀 점은 정말 생각을 하고 코드를 짜야한다는 점이다. 일단 되는대로 코드를 짜면 운이 좋으면 그대로 끝난겠지만 그렇지 않으면 코드를 짜는 시간보다 배가 되는 시간을 에러를 고치는데 써야 한다.
 
미니쉘에서는 코드를 짤 때 파트를 나누어서 진행하였는데, 파트를 진행한다고 꼭 속도가 빠르지도 않으며 (만날 때마다 서로의 코드를 이해시키느라 들이는 시간이 많다.) 그럼에도 불구하고 완전히 이해하지 못하는 지점이 생긴다는 점을 알게 되었다. 따라서 이번 팀 프로젝트에서는 vscode의 live share 기능을 이용하여 서로 코드를 같이 짜면서 진행을 하였다. 이 방법이 장점만 있는 것은 아니지만, 적어도 코드를 다 짰을 때 모든 코드를 세세히 알 수 있게 되었다. 
 
간만에 열심히, 재미있게 한 과제였다. 방학이기도 해서 십몇일 시간동안 이 과제에만 몰입하여 깰 수 있었다. 처음에 공부할 때는 수식의 양에 지레 겁을 먹었지만, 하나씩 증명해가며 이해를 해보니 꽤나 흥미로웠던 것 같다. 무엇보다 코드의 결과가 눈에 나온다는 점이 좋았다. 정성들여 한 과제이니만큼 노션에 아무렇게 정리해 놓은 것을 처음으로 블로그에 제대로 정리해보았는데, 생각보다 꽤나 시간이 걸리는 작업이라는 것을 알게 되었다. 과제를 열심히 문서화하시는 분들을 존경한다.. 이전에 끝냈던 과제들도 복습겸 이렇게 블로그에 적고 싶다. 
 
42seoul의 마지막 c과제이니만큼 감회가 새롭다. 그리고 드디어 norm의 해방이다! 언제까지 c만 하냐고 투덜거렸는데, 이제부터는 c++로 코드를 짜야 한다. 제대로 c++을 공부하여 c++ 스타일로 코드를 잘 짜보고 싶다.  
 

과제하며 참고자료

벡터 기반 방법은 아니지만, 처음에 과제 접하고 막막할 때 레이캐스팅이라는 개념을 이해하기 좋은 영상이다.

 
DDA 알고리즘을 이해하기 좋은 영상, 게시물

 

[알고리즘] DDA알고리즘

선분그리기를 할 때 이런저런알고리즘이 있는데요~ 그 중 간단하고 직선의 방정식에 원리에 가장 가까운 방법인(오차가 있긴하지만) DDA알고리즘에 대해 포스팅하겠습니다. Digital Differential Analyz

playground10.tistory.com

아마 큡디를 하는 모두가 참고할 로데브 자료의 한글 번역본 

GitHub - 365kim/raycasting_tutorial: (한글) 레이캐스팅 튜토리얼 번역

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

github.com

로데브 자료는 기본적으로 C++로 쓰였기 때문에, 이를 C와 mlx로 매핑해주신 카뎃 자료

GitHub - l-yohai/cub3d: Porting Lode's Computer Graphics Tutorial - Raycasting to C and Minilibx for 42 Subject Cub3D

Porting Lode's Computer Graphics Tutorial - Raycasting to C and Minilibx for 42 Subject Cub3D - GitHub - l-yohai/cub3d: Porting Lode's Computer Graphics Tutorial - Raycasting to C and Minil...

github.com

로데브 자료 보면서 공부할 때 정말 많이 참조한 자료. 변수와 증명 정리가 잘 되어 있다. 

[Rank 4] Cub3D - Raycasting 구현 ① 변수 설명

변수 설명 posX, posY 플레이어의 좌표를 나타내는 벡터 원점 (0, 0) 을 시작점으로, 플레이어의 좌표를 끝점으로 하는 벡터이다 단도직입적으로 플레이어의 위치 좌표라고 생각하는게 편하다 dirX, d

blog.chichoon.com

mlx 예제. 연습하기 좋다.

GitHub - terry-yes/mlx_example: Some examples for those who try to use MLX library for 42 subject CUB3D or miniRT. And some help

Some examples for those who try to use MLX library for 42 subject CUB3D or miniRT. And some helpful links. - GitHub - terry-yes/mlx_example: Some examples for those who try to use MLX library for 4...

github.com

 

    jo16
    jo16
    공부한 것들을 기록합니다.

    티스토리툴바