2018년 7월 17일 화요일

OpenGL로 FPS 카메라 구현하기


대학교 그래픽스 강의에서 OpenGL로 간단한 FPS를 구현했었습니다.
세가 신입사원 책이랑 게임 프로그래밍 패턴 책을 참고했었습니다.
핵심 아이디어만 간단히 요약해서 정리해둡니다.

카메라

LearnOpenGL 사이트에 카메라에 대해서 정말 잘 설명되어있습니다.
https://learnopengl.com/Getting-started/Camera

수평으로만 회전하는 카메라를 구현하고 먼저 키보드 이동까지 구현해보면 좋습니다.
수평은 생각보다 쉽습니다. 수직으로 카메라를 돌리기가 막히면 먼저 해보세요.

GLFW를 썼습니다.
화면을 가운데로 마우스 커서를 옮기는 glfwSetCursorPos()는 glfwSetCursorPosCallback 안에서 했습니다.
다른 환경에서도 마우스 이벤트는 입력 받는 곳에서 같이 처리하세요.

쿼터니언을 썼습니다.
https://youtu.be/_nJLDmue0h4

1인칭 시점을 3인칭으로 바꾸려면 offset을 적절히 사용하면 됩니다.
https://youtu.be/PoxDDZmctnU

플레이어

손과 총 모델은 언리얼엔진4 기본 FPS 게임의 마네킹을 추출해서 썼습니다.
FPS 1인칭 시점에서 캐릭터 모델은 저렇게 총과 손만 있습니다.

이동하는 방향으로 캐릭터가 움직일 때 카메라는 캐릭터의 위치를 그대로 씁시다.

바닥에 붙어서 이동하는건 wasd에 카메라의 수평을 곱하면 됩니다.

간단한 반동은 캐릭터의 위치에서 모델을 위아래로 조금씩 흔들어주면 됩니다.

총알

파티클 예제를 만들어야 했습니다(?)

간단한 물리(중력)를 구현했습니다.

캐릭터 위치에서 총알이 발사되는 위치는 적절하게 offset 주면 됩니다.

제 경우 충돌처리를 AABB를 간단히 구현했습니다.
충돌처리에 대해 조금 더 자세한 설명은 아래 블로그를 참고해주세요.
http://blog.naver.com/PostView.nhn?blogId=36513535&logNo=10024329388

히트스캔으로 구현하고 싶다고 총알의 속도를 높여봐야 소용 없습니다.
일반적인 게임의 물리 구조 상 너무 빠른 물체는 충돌처리가 불가능합니다.
대신 총의 발사 위치에서 직선으로 벡터를 쏘세요, 레이 트레이싱이랑 비슷한 원리입니다.

기타

게임의 기본적인 구성에 대해서 알고 계시면 구현이 편합니다.
https://www.slideshare.net/sm9kr/6-20903192

2018년 7월 8일 일요일

OpenGL 3.3+에서 PMX, FBX 렌더링

OpenGL 3.3+에서 PMX와 FBX 렌더링




해당 프로젝트와 블로그는 OBJ, PMX, FBX 파일을 렌더링하는 방법에 대해서 설명합니다.

최근 반년동안 삽질한 내용을 정리하고, 새로 프로젝트를 해보려고 합니다. 그전에 일단 삽질했던 내용을 공유합니다. OBJ, MMD(PMX), FBX 파일을 직접 렌더링해보고 싶으신 분들이 저와 같은 시행착오를 겪지 않기를 바랍니다. 해당 코드는 https://github.com/hyunjun529/SECA/releases/tag/1.0에서 받아서 실행해보실 수 있습니다.

SubMesh(서브메시)

이런 종류의 렌더링 기법(?)을 어떻게 부르는지 몰라서 일단 SubMesh라고 적었습니다.
Unity에선 좀 쓰는 용어 인 것 같은데, 일단 각 텍스쳐나 마테리얼 단위로 메시를 렌더링 할 때 참고한 코드들이 이렇게 부르는 경우가 많아서 저도 이렇게 부르고 있습니다.

구현할 때 참고했거나 관련된 글은 아래와 같습니다.
마테리얼을 처리하는 메시 단위를 서브메시라고 부른다. https://m.blog.naver.com/PostView.nhn?blogId=nuberus&logNo=50183215686&proxyReferer=https%3A%2F%2Fwww.google.co.kr%2F
하지만 막상 유니티 공식문서에선 서브메시란 단어가 없다. 메시나 텍스쳐, 마테리얼 단위에 대해선 어떻게 부르는지 잘 모르겠다. https://docs.unity3d.com/kr/530/Manual/class-Mesh.html
오거 엔진의 경우 서브메시를 명시적으로 사용합니다. https://ogrecave.github.io/ogre/api/2.1/class_ogre_1_1_sub_mesh.html
언리얼 엔진의 경우 마테리얼을 각각 할당하는 구조인데, 서브메시의 개념도 이와 비슷한 것 같습니다. http://api.unrealengine.com/KOR/Engine/Rendering/Materials/index.html

https://github.com/hyunjun529/SECA/blob/master/Engine/render/Object.h#L28
indicies, drawElement를 사용하지 않고 SubMeshes로 idx를 저장하는 방식을 사용했습니다. OBJ, PMX, FBX를 불러올 때 각각의 단위를 이렇게 저장하는게 Index를 저장하는 방식보다 편리했습니다.

https://github.com/hyunjun529/SECA/blob/master/Engine/format/OBJ/OBJLoader.cpp#L182
OBJ를 렌더링하기 위해서 VBO에 서브메시를 저장하는 부분입니다. 사용한 tinyobj에서 obj를 로드할 때 마테리얼 단위로 정리해주기 때문에 쉽게 사용할 수 있습니다. 이 부분이 PMX나 FBX보다 간단하니, 먼저 살펴보면 좋습니다.

PMX 렌더링

saba라는 오픈소스를 많이 참고했습니다. 아래에 MMD 관련 내용을 참조해주세요.
Qiita : https://qiita.com/benikabocha/items/ae9d48e314f51746f453
GitHub : https://github.com/benikabocha/saba

제가 구현한 부분은 아래 링크에 있습니다. 포맷 등에 관한 부분은 아래 MMD 부분을 참조해주세요, 구조만 파악하면 OBJ 파일과 크게 다르지 않고, saba를 통해 로드해와서 어려운 점이 없었습니다.
https://github.com/hyunjun529/SECA/tree/release/v1.0/Engine/format/PMX
제 구현에서 기억해두실 부분은 강체나 본 등과 별도로 Mesh 부분만 불러왔다는 점입니다.

FBX 렌더링

해당 부분 구현 코드는 아래 링크에서 확인할 수 있습니다.
https://github.com/hyunjun529/SECA/tree/release/v1.0/Engine/format/FBX

공식 문서는 일단 아래 링크들을 참조했습니다. 먼저 해당 문서들을 간략히 보시는 것을 추천드립니다.
FBX Scene, FBX nodes, FBX Mesh


앞으로 사용할 용어, FBX SDK 내의 클래스들을 먼저 정리합니다.
FbxManager : FBX SDK를 사용하는 컨텍스트입니다.
FbxIOSettings : FBX SDK가 파일을 읽어올 때 사용합니다.
FbxScene : 한 Scene으로, 일반적인 Scene을 생각하시면 됩니다.
FbxAxisSystem : 좌표 시스템을 변환해준다고 합니다(실제로 동작이 잘 안됩니다)
FbxNode : FBX 파일을 이루고 있는 한 단위입니다.
FbxMesh : FBX Node의 일종으로 Mesh입니다.
FbxGeometryElement : Mesh 내에 Vertex Position, Normal, Indices 등의 타입을 지정합니다. 열거형입니다.
FbxGeometryElementUV : Texture, Coord나 각 메시의 마테리얼 관련된 부분이 여기 들어 있습니다. 이건 열거형이 아니라 클래스입니다.

그리고 변수 중에 이런 녀석들이 있는데, 이건 FBX가 단순히 게임 엔진에서만 사용되는게 아니라 다양한 곳에서 사용되어 왔기 때문으로 추정됩니다. 같은 메모리를 참조하면서도 접근하는 클래스나 인터페이스에 따라서 각기 다르게 불리는 경우가 있으니 주의해주세요.
제 경험상 Polygon과 ControlPoint의 Triangle Mesh는 동일한 Scene내 메모리 영역을 사용했습니다.

애니메이션(AnimStack)이 적용되지 않은 기본 자세는 Geometry Deformations이 적용되지 않은 기본 스켈레톤으로 부릅시다. 기본적으로 FBX는 애니메이션을 위한 포맷이기 때문에 Mesh 하나를 부를려고 해도 PMX와 달리 복잡한 구조를 갖습니다. 예를들어 Mesh를 렌더링하기 위해 Mesh, Texture, Material, Element Indices를 불러오는 부분이 전부 다릅니다.

Axis에 대해서는 아래 그림을 참조포기하시면 편합니다. 실제로 구현해보면 MMD, OBJ, FBX, OpenGL이 다 꼬여서 조금 힘들겠지만 중요한 부분은 FbxAxisSystem에 의존하지 않는게 편합니다. 특정 부분에서 적용되지 않는 버그가 존재합니다. 아마 Scene으로 재생을 해야 적용되는거 같습니다.



제가 구현한 부분에 대해서 정리하자면 Geometry Deformation이 적용되지 않은 기본 Mesh를 렌더링했다고 볼 수 있습니다.

MMD에 대해서

pmx의 라이센스에 관해서는 아래 정보들을 참조하세요.

공식 사이트 : https://www6.atwiki.jp/vpvpwiki/pages/13.html
MMD는 VPVP Wiki 기준으로 상업적 이용 및 재배포를 금지한다. 그리고 배포된 모델(pmx)나 애니메이션(vmd)은 창작자가 같이 동봉한 readme.txt에 따른다.

MMD, Unity 관련 저작권 사건 : http://creativefreaks.net/?p=1705
readme.txt를 읽기 힘들게 만든 점이 문제가 된 사례입니. 또 모델러나 애니메이터의 저작권이 파일 포맷을 변환하거나 개조할 경우 미묘해지는데, 이 부분은 아직 회색지대로 남아있다고 하니 조심하셔야 할 것 같습니다.

라이센스에 비교적 자유롭거나 가장 널리 사용되는 PMX는 아래와 같습니다.
먼저 츠미식과 Tda식이 가장 널리 쓰입니다. 대부분 개조 파일도 해당 식을 개조한 버전인 경우가 많습니다.
그리고 가장 가벼운 파일은 three.js의 LowPoly 미쿠입니다. : https://github.com/mrdoob/three.js/tree/master/examples/models/mmd#readme
해당 파일이 라이센스에 가장 자유롭습니다.
또 비상업적 목적, 개인 목적으로는 키즈나 아이가 퀄리티가 제일 높습니다. : https://kizunaai.com/ 그리고 Tda 본인이 감수를 한다는 이야기도 있고 구조가 워낙 깔금하고 "영어"로 되어있어서 매우 유용합니다.

pmd 포맷은 아래 링크에서 분석된 파일이 있습니다.
http://mikumikudance.wikia.com/wiki/MMD:Polygon_Model_Data

pmx 포맷은 아래 링크를 참조해주세요, 단 실제로 거의 사용되지 않는 내용들도 있습니다.
PMX 2.0 : https://gist.github.com/DeXP/16ccdd09841bdc1961e0
PMX 2.1 : https://gist.github.com/felixjones/f8a06bd48f9da9a4539f

PMX weight map은 아래 링크를 참조해주세요. 실제로 사용되지 않는 방식이 같이 설명되어 있습니다.
https://learnmmd.com/http:/learnmmd.com/pmxe-weight-painting/

VMD 포맷은 MMD에서 애니메이션을 담당하는 부분인데, FBX의 경우 한 파일내 AnimStack으로 관리되지만, MMD는 이 구조가 분리되어 있어서 더 사용하기 쉽습니다. 카메라도 여기에 포함되어 있음을 주의해주세요.
http://mikumikudance.wikia.com/wiki/VMD_file_format

C++, WebGL에서 실제로 구현하는 방법에 대해서는 saba나 three.js를 참조하시면 좋습니다. 그리고 MMD에선 강체와 유체가 사용되는데, 이는 Bullet Physics를 사용합니다. 하지만 강체만 사용하는데다가 Bullet의 버전 문제도 있으니 MMD 프로그램의 라이브러리 부분을 확인해보시면 됩니다. MMM의 경우 MMD와 다른 버전을 사용하고 있어 결과의 차이가 발생하기도 합니다. MMD Bridge와 IM4U를 사용한 UE4와 Unity Mechanim, 그리고 three.js 의 경우 방식이 제각각입니다.
UE4 : Bullet Phtysics를 백그라운드 재생, 머리카락 등의 강체를 FBX 애니메이션으로 변환.
Unity : 메카님으로 직접 변환, 강체와 유체를 제외한 스켈레탈만 전송. 단 Unity 물리를 Bullet으로 바꾸던가 후처리 가공 필요.
three.js : Bullet Physics의 강체와 유체를 간단하게 직접 구현해놓은 형태

MMD의 경우 좀 복잡한 느낌이지만, 이에 대한 역사를 조금 살펴보시면 감을 잡으실 수 있을 것 같습니다.

FBX SDK에 대해서

빌드 세팅이 복잡할 수 있지만, 제 프로젝트 설정을 확인하시면 됩니다. FBX SDK를 직접 빌드해보면 여러가지 문제가 발생할 수 있는데 이 부분은 따로 설명하지 않습니다.

FBX SDK의 히스토리를 간략히 살펴보면 아래와 같습니다. (https://en.wikipedia.org/wiki/FBX)
1996년 1.0버전, kaydara's Filmbox로 시작,
1999년 2.0버전
2001년 3.0버전
2002년 Filmbox에서 MotionBuilder로 이름이 바뀜
2003년 4.0버전
2004년 Apple's QuickTime Viewer
2005년 SDK 표준 모델 발표
2006년 Autodesk가 관리 시작
이후 Autodesk의 fbx sdk로 표준을 관리하고 있습니다.
fbx는 현재 game을 위한 기술로 분류되지만 최초엔 모션 캡처 데이터에 사용되었었습니다.
Autodesk 사의 자세한 버전 정보는 공식 링크를 참조하세요(http://help.autodesk.com/view/FBX/2019/ENU/)

그리고 FBX SDK 라이센스는 GPL이며 이를 사용한 엔진들은 모두 코드를 공개하고 있습니다. 그래서 UE4나 Unity에서도 해당 내용을 확인할 수 있죠. 이에 대해서는 다시 설명합니다. 하지만 FBX 포맷을 활용한 저작물(모델링, 애니메이션 파일)은 생산자가 GPL과 별도로 저작권을 가집니다. 

오픈소스 FBX들은 Assimp, Ozz, Blender에서 사용되고 있습니다. 하지만 현시점에선 전부 2015~2014년 버전을 기준으로 하고 있으며 2018에 도입할 경우 조금씩 깨지거나 의도치 않은 결과를 가져옵니다. Assimp의 경우 PMX와 FBX를 지원하지만 실제로 작업을 하다보면 꽤 많은 오류가 발생하고, OZZ는 2017년을 마지막으로 지원이 끊겼는데 Assimp보단 나은 결과를 보여줍니다. 마지막으로 Blender는 FBX 7.3과 7.4를 기준으로 하는데 제일 잘 동작하지만 UE4나 Unity에 적용하기 위해서는 Skeleton이나 Export 옵션에 많은 수정을 필요로 합니다. 대신 Import 부분은 매우 훌륭하게 동작합니다. 

FBX SDK는 FBX 포맷을 다루기 위한 툴인데, FBX 포맷에 관해서는 설명되어 있지 않습니다. 부분적이고 추상적으로 설명되어 있어서 바이너리 구조는 직접 알아봐야하는데 아래 링크들은 FBX File format에 관한 Blender의 문서들이 도움이 됩니다. 하지만 구버전을 기준으로 함을 주의해주세요.



2018년 7월 8일을 기준으로 Unreal Engine 4(4.19)와 Unity(2018.1.4) 최신 버전은 FBX SDK 2018.1을 사용하고 있습니다. 즉 2018.1로 변환만 성공하면 각 엔진이나 마야 등의 편집 툴에서 범용적으로 쓸 수 있습니다. 꽤 매력적이지만 함정이라면 FBX SDK가 쓰기 어렵습니다.

FBX의 애니메이션 관련 공식 문서는 예제 코드부터 오류가 존재합니다, doxygen과 실제 코드를 intellisence로 찍어본 결과도 제각각입니다. 제가 이 프로젝트에서 애니메이션을 포기한 이유기도 한데요[..] 이 부분은 나중에 필요해지면 다시 해볼 예정입니다.

차라리 공식 문서보다 How to work with FBX SDK 글이 큰 도움이 됬습니다. 해당 글의 코드들을 살펴보면 꽤 많은 리비전이 존재합니다. 13, 14 버전이 혼용되어 있으니 참고하세요. 2018.1로 돌릴려면 조금 손봐줘야 합니다.

번외로 MMD에서 FBX로 변환하는 C++ 툴, 오픈소스로 이미 pmx2fbx라는 툴이 있습니다. 당시 FBX SDK 2013인가 2015버전 기준인데 2018로 변경해서 빌드하고 실행시켜보니 돌아갑니다. 다만 UE4나 Unity에 넣을 때 루트 본이 하나가 아니라던가 pmx 파일에 따라서 오류가 발생합니다. 하지만 이 코드는 FBX SDK와 PMX 간 스켈레톤 구조를 공유하는 방식에 대해서 참고할 때 도움이 됬습니다.

후기, 다음 작업

원래는 MMD(PMX)에서 FBX로 변환하는 프로그램을 개발하고 있었는데 처음 생각보다 너무 일이 커졌습니다. 아마 잘 모르는 상황에서 일의 크기를 몰라서 견적을 잘 못 잡은거 같습니다.
아마 다음에 할 일은 애니메이션을 포함한 추출이 아니라 먼저 MMD(PMX)에서 FBX나 COLLADA로 스켈레톤 구조, 강체 등만 먼저 추출 해봐야 할 것 같습니다.

그리고 원래 목표였던 IK를 이용해서 애니메이션을 재생하는 예제는 따로 작성해봐야 할 것 같습니다. 지금 관심있게 보면서 참고 중인 프로젝트는 아래와 같습니다.
- WebGL로 MMD 재생 : three.js, MMD 예제
- C++에서 MMD 재생 : saba
- Assimp
- Ozz

제 경우 캐릭터 애니메이션 포맷이나 렌더링, 애니메이션 재생을 공부하는건 수단이고 최종 목표는 AR이나 RL 등에 적용해보는 것 입니다. 취미로 계속 하는 중이고 아마 다음에는 좀 더 단기간에 할 수 있는, 작은 규모로 프로젝트를 해볼 것 같습니다.

2018년 4월 27일 금요일

비결정론적인 폴리브릿지

폴리브릿지는 비결정론적(non-deterministic)이다

예전에 폴리브릿지를 하다가 운빨로 스테이지를 넘기고 뿌듯하게 리플레이를 돌려보는데 가끔 실패하는 경우를 발견해서 저장해놨었다가 이에 대해서 정리하려고 방치해뒀던 주제입니다.
쓰다보니 좀 애매하고 범위를 정하기가 어려웠습니다. 물리엔진에서 발생할 수 있는 문제와 원인들에 대해서 대략적으로 알아봅니다.

상황


좌표는 폴리브릿지 4-7

이렇게 성공할 때도 있지만

이렇게 실패할 때도 있습니다.

저렇게 다리가 흔들거리게 만들어두면 어떻게 재생할 때마다 결과가 달라집니다.
이런 상황에 대한 버그리포트는 이미 있지만 좀 더 자세히 알아봅시다.
특히 리더보드에서 순위를 따지는데 저런 상황이 발생하면 엄밀히 말해서 점수가 의미 없는 상황이 되죠(스코어링 곤란해짐)

Bug? Non-deterministic behaviour in jump, jump again (2015.09.28)
https://steamcommunity.com/app/367450/discussions/0/517142892064744193/
Q : 점프를 계속 반복하면 실패할 때가 발생한다, 이거 버그 아니냐?
A : 버그 맞다, 부동소수점 문제일 수도 있지만 근본적인 원인은 런타임에 바디들을 만들어서 발생하는 문제다. Unity, Box2D 구현에선 동일

The physics engine is not deterministic. (2016.06.24)
https://steamcommunity.com/app/367450/discussions/0/351660338688467230/
Q : 나 리더보드에서 순위작 하는데 같은 조건에서도 점수가 계속 다르게 나온다.
A : Box2D는 결정론적 시뮬레이션을 제공한다. 하지만 Unity에 사용할 때 문제가 생겼다. 시뮬레이션(bodies, constraints) 을 만들고 파괴하는 과정에서 객체가 해싱 테이블에 추가되는 순서가 무작위이며 이에 따라서 다른 시뮬레이션 결과가 발생한다. 대신 온라인 갤러리(https://gallery.drycactus.com/)에선 같은 순서를 보장하므로 업로드해서 확인하면 된다.

결정론적, 비결정론적 알고리즘


물리엔진에서 결정론적, 비결정론적은 시뮬레이션 결과를 예측할 수 있냐, 재현성이 있느냐라고 이야기할 수 있습니다. 결정론적이라면 동일한 상황에선 언제나 같은 결과가 반복될 것이고, 비결정론적이면 위에 올린 폴리브릿지처럼 같은 상황 같은 조건에서도 다른 결과가 생길 수 있죠.

정보통신기술용어해설 : Deterministic, Nondeterministic   결정론적, 비결정론적
http://www.ktword.co.kr/m/abbr_view.php?m_temp1=4086

Deterministic Algorithm
https://en.wikipedia.org/wiki/Deterministic_algorithm

Nondeterministic Algorithm
https://en.wikipedia.org/wiki/Nondeterministic_algorithm

Stochastic Optimization

비결정론적은 확률적과 엄밀히 따지면 다르게 정의됩니다. 이는 맥락에 따라서 조금씩 다르게 사용되는 것에 주의합시다.
https://cstheory.stackexchange.com/questions/632/what-is-the-difference-between-non-determinism-and-randomness

물리엔진

2D와 3D에서 물리엔진

2D에서는 Box2D(https://github.com/erincatto/Box2D)가 대표적입니다. 폴리브릿지 뿐만 아니라 수많은 게임이 Box2D를 물리 시뮬레이션에 사용하고 있죠.
하지만 3D로 넘어가면 차원이 증가함에 따라서 훨씬 더 많은 연산을 필요로 합니다. 그리고 무엇에 중점을 두느냐에 따라, 어떤 환경에서 개발하냐에 따라서 사용하는 물리 엔진의 종류가 달라집니다. 대표적으로 로켓리그라는 게임은 UE4로 개발했지만 물리엔진은 Bullet Phyics를 섞어서 썼다고 합니다.

물리엔진 강체(Rigidbody)와 제약(Constraints)




체인을 만들어봤습니다. 우측이 강체로 엮은 사슬, 좌측은 제약으로 엮은 사슬입니다.
강체(Rigidbody)로 엮은 경우 충돌(Collision)을 바탕으로 각 강체들의 다음 위치를 결정합니다. 반면 제약(Constraints)로 엮은 경우 보이지 않지만 각각 설정된 제약조건에 따라서 다음 위치를 결정합니다. 위치를 결정한다는 의미는 한 스텝(step)에서 다음 스텝으로 넘어갈 때 각 강체에 적용될 힘(F=ma)을 어떻게 주냐로 따질 수 있습니다.

사실 처음 폴리브릿지에서 버그를 봤을 때 여기서 에러가 발생한거 아닌가 의심했었습니다. 다리가 출렁이는 움직임이 매번 달리지기 때문이죠. Bullet Physics와 UE4에선 최적화 조건에 따라서 제약조건을 처리하는 Solver에서 속도 높일려고 랜덤을 박아놓는 경우가 있었습니다. 그래서 폴리브릿지도 마찬가지 아닐까? 생각했지만 2D니까 굳이 이럴 필요가 없었내요.

하지만 강체와 제약조건을 등록하는 해싱 테이블에서 등록하는 순서가 바뀌면 처리 순서가 바뀌고 이게 결국 비결정론적인 물리 시뮬레이션을 만든다는 점에서 결국 랜덤신의 가호가 맞았습니다(?)

조금 더 설명된 자료는 아래 슬라이드, 왠만하면 책을 읽어보시길 바랍니다. 저도 아직 공부를 하는 중이라 어디까지, 어떻게 설명해야할지 모르곘습니다ㅠㅠ
[NDC12] 게임 물리 엔진의 내부 동작 원리 이해


정리하자면...

비결정론적인 이유?

물리엔진은 왜 비결정론적일까? 여러 사례를 살펴보면 주로 처리순서에 따른 문제와 부동소수점에 관한 문제가 있습니다. 특히 게임에 쓰이는 물리엔진의 경우 시뮬레이션 정확도보다 속도를 중요시하기 때문에 꼭 필요한 곳이 아니면 비결정론적으로 구현하는게 도움되는 경우가 많습니다.

대표적으로 오버워치에서 주변 사물과 상호작용을 할 때 공은 모든 사람들이 동일하게 인식하죠, 루시우볼이 특히 이런 경우였습니다. 하지만 단순히 파괴되면 끝나는 물체들은 모든 사람들이 동일한 화면을 볼 필요가 없으니 보는 사람마다 조금씩 다르게 파괴되는 것을 볼 수 있습니다.

결정론적일 때는?

물론 가능합니다. 특히 로보틱스나 강화학습, 게임이 아닌 CG를 만들 땐 속도보다 정확도, 품질이 중요하기 때문에 결정론적인 물리 시뮬레이션을 사용합니다. 어떻게보면 게임 밖에 이렇게 물리시뮬레이션을 하는 사례가 없내요.

2018년 4월 13일 금요일

파이썬 웹 크롤러 만들기 (Python, Scrapy)

파이썬 웹 크롤러 만들기
(Python, Scrapy, Web Crawler, Crawling)


웹 크롤러는 여러가지 용도로 쓰일 수 있습니다.
자연어 처리를 위한 말뭉치를 모을 때부터 인터넷에 올라온 짤방을 저장할 때 등 여러가지 용도로 사용합니다.
하지만 크롤링을 하기 위해서 여러 URL과 게시판의 페이지를 자동으로 탐색하는건 꽤 귀찮은 작업이죠.

이 글은 Scrapy를 사용해 단 몇 줄로 파이썬 웹 크롤러를 만드는 방법과 필요한 배경지식을 소개합니다.
Beautiful Soup과 request를 쓸 경우 한 게시판에서 대한 URL을 찾고, 여기서 다음 페이지(목록) 찾는 방법을 구하고, 너무 많은 요청을 한 번에 보내지 않게 조절 하면서, 각 페이지에 직접 들어가 파일로 저장하는 것을 동시에 처리하도록 구현해야하는 등, 생각보다 귀찮은 작업이 많습니다.
이에 비해서 Scrapy 프레임워크의 원리만 알고 이용하면 대부분의 귀찮은 작업을 하지 않아도 됩니다.



위 코드는 scrapy runspider <filename> 한 줄로 실행가능합니다.
크롤링하는 대상은 네이버 지식인(http://kin.naver.com/qna/list.nhn) 입니다.

웹 크롤링을 위한 배경 지식

네이버 지식인을 크롤링하는 예제를 바탕으로 필요한 지식으 순서대로 설명합니다.

1. 웹 크롤링이란?

크롤링; (검색 엔진에서) 분산 저장되어 있는 정보를 수집하여 검색 대상으로 복제·보존하는 기술.(네이버 사전)
웹 스파이더(Spider)라는 용어는 인터넷(Internet) 즉 Net을 그물처럼 돌아다니면서 찾느 거미(..)가 어원입니다.
정리하자면 스파이더는 필요한 정보를 자동으로 찾는 것, 크롤링은 데이터를 긁어서(?) 저장하는 것을 의미합니다.

보통 웹 크롤링은 웹사이트에서 우리가 원하는 부분만 자동으로 가공해서 저장하는 작업을 의미합니다.
이 예제에선 네이버 지식인의 질문 목록을 받아오고 자동으로 이동하며(스파이더) 각 질문에 들어가서 본문을 파일로 저장(크롤링)합니다.

2. 웹사이트의 URL 구조





보통 웹 사이트의 블로그나 게시판들의 구조를 생각해봅시다.
지식인은 각 카테고리를 URL(인터넷 주소창) 끝에 ?dirId=(숫자)에 따라서 구분합니다.
즉 이 숫자에 따라서 원하는 카테고리를 선택할 수 있습니다.

보통 다른 게시판도 비슷한 구조로 되어있습니다. 우리가 원하는 게시판과 분류가 어디인지 알아낼 땐 여기를 먼저 조사하면됩니다.
인터넷 주소창에 대한 자세한 내용은 제타위키 GET 요청, POST 요청에 대해서 알아보시면 됩니다.
https://zetawiki.com/wiki/GET_%EB%B0%A9%EC%8B%9D,_POST_%EB%B0%A9%EC%8B%9D

3. robots.txt

robots.txt는 크롤링을 금지한다는 알림입니다. 자세한 내용은 따로 검색해보시면 됩니다. 알아둘 점은 긁지 말라는걸 긁을 경우 저작권에 문제가 생길 수 있습니다.
https://en.wikipedia.org/wiki/Robots_exclusion_standard

아마 Scrapy를 예제처럼 스파이더 따로가 아니라 프레임워크로 구동하면 robots.txt로 금지되어 있다고 경고가 뜰겁니다. 강제성은 없어서 무시할 수 있지만 나중에 문제의 소지가 될 수도 있으니 한 번 찾아보시길 바랍니다. 저는 공부 목적으로 긁었습니다.

4. HTTP

인터넷 통신 프로토콜입니다. 웹 크롤링을 하다보면 200, 301, 302, 404 등 몇 가지 숫자를 볼 텐데 이게 HTTP와 관계있습니다.


자세한 내용은 HTTP에 대해서 따로 찾아보시길 권장합니다. 만약 3XX가 뜨거나 5XX가 뜰 겨우 크롤링을 막아놓은 사이트일 수도 있습니다. 이 경우 웹 브라우저에선 보이는데 왜 크롤링이 안 될까? 궁금하신 분은 이걸 공부하시면 됩니다.

5. HTML & CSS Selector


HTML은 크롬 개발자도구로 보이는 마크업 언어입니다. 크롤링하고자 하는 대상을 적당히 지정해주면 됩니다.


파이썬 코드에서 뽑기 위해서는 CSS Selector로 해당 위치를 지정해줘야 합니다.
CSS Selector에 대해서는 아래 링크.
https://www.w3schools.com/cssref/css_selectors.asp
직접 테스트해보고 싶다면 아래 링크를 참조해주세요.
https://www.w3schools.com/cssref/trysel.asp

6. Encoding

보통 글씨 깨졌다고 표현하는 현상은  글씨가 깨지는 현상이 발생하면 파이썬 인코딩 등을 찾아보면 됩니다. 반드시 Python 3, UTF-8을 씁시다.

7. Python

프로그래밍 언어입니다. 이제 위에 내용들을 바탕으로 파이썬으로 테스트 해보면 됩니다.
윈도우라면 Anaconda를 사용하면 좋습니다. 아래와 같이 테스트하면 됩니다.


여기서 반드시 알아둘 점이 있습니다. 크롤러를 돌리는 행위는 웹 서비스 제공자에 영향을 줍니다. 짧은 시간내에 많은 요청을 보내기 때문인데, 테스트를 할 때는 크롤링하는 사이트에 부담을 줄 수 있음을 명심하고 너무 무리한 요청을 계속 보내지 않도록 조심합시다. 또 크롤링을 막 돌리면 서버에서 접근이 막힐 수도 있으니 조심합시다.

Scrapy에 관해서

공식 홈페이지 : https://scrapy.org/
홈페이지 첫 페이지에 나와있는 예제를 조금 바꿨을 뿐인데 충분히 훌륭하게 동작합니다.
저건 간단한 예제일 뿐 Scrapy는 프레임워크로써 좀 더 복잡한 사용법이 존재합니다.

제가 공유한 방식은 간단한 하나의 Spider를 수동으로 동작시키는 예제입니다. 목록을 탐색하고 각 게시물을 긁어오는 코드는 공식 튜토리얼을 참조해서 조금 변형했습니다.
https://docs.scrapy.org/en/latest/intro/tutorial.html

일정 시간마다 자동으로 긁어오거나, 만약 트래픽 과다로 차단을 먹거나 일정 시간 지연 기능등을 사용하려면 create a project를 통해서 제대로 만들길 추천드립니다.
https://docs.scrapy.org/en/latest/intro/tutorial.html#creating-a-project

수동으로 돌리실 땐 time()함수나 console창에 출력하는 내용을 잔뜩 만들어서 딜레이를 주는 등으로 하는 방법도 있습니다.


2018년 2월 23일 금요일

obj viewer 구현 및 정리

obj viewer 구현


구현한 프로그램 동작 예

obj loader로 파일을 불러온 뒤 텍스쳐(마테리얼?) 여러장을 Modern OpenGL로 출력하는 깔끔한 예제가 없어서 만들어 봤습니다. 구현하면서 공부한 내용들을 정리합니다.

전체 소스 코드는 아래 GitHub저장소에서 받을 수 있습니다.
Win10, VS2017에서 따로 설치하지 않고 바로 돌려볼 수 있습니다.
https://github.com/hyunjun529/WIN_VS_GL_Setups/tree/master/6_obj_viewer

쉐이더(Shader)와 VAO, VBO를 효율적으로 사용하는 방법과 관심사의 분리(SoC)를 최대한 고려했습니다. 저장소에 올린 예제 코드 중 이벤트 처리와 파일 입출력 부분은 더 손댔다가 나중에 알아보기 힘들어질 것 같고 양적인 작업이라 미뤄뒀습니다.

MMD의 .pmx 파일에서 .obj 파일로 변경

MMD는 MikuMikuDance의 약어입니다. 유튜브에 찾아보면 많습니다.
http://www.geocities.jp/higuchuu4/index.htm

MMD에서 캐릭터 모델링에 .pmx 파일을 사용합니다.
나중에 다시 살펴보겠지만 pmx 형식은 캐릭터 모델과 강체, 관절을 포함하고 있습니다.
https://gist.github.com/felixjones/f8a06bd48f9da9a4539f

버츄얼 유튜버 키즈나 아이로 테스트해보고 싶습니다.
https://kizunaai.com/download-page/

Blender에 pmx 파일을 불러오기 위해선 아래 글을 참고합니다.
http://mmdguide.tistory.com/698

Blender mmd_tools 최신 버전은 여기서 받을 수 있습니다.
https://github.com/sugiany/blender_mmd_tools


위 과정을 통해서 블랜더에서 불러올 수 있게 됬습니다.
이걸 그대로 obj 파일로 추출(Import)해보면...


윈도우 10의 3D 그림판이나 뷰어가 있으니 그걸로 열어볼 수 있습니다.
이렇게 하면 위와 같이 좀 괴상하게 나옵니다.

.pmx 파일은 블랜더에서 위와 같이 보입니다,
joints는 관절 부분, rigidbodies는 강체를 나타냅니다.
obj로 바로 추출할 경우 강체(rigid body)가 시각화되어 같이 출력됩니다.

관절과 강체를 제거하고 다시 추출하면 위와 같이 깔끔한 obj 파일을 얻을 수 있습니다.

그리고 obj 파일을 생성하면 mtl 파일과 텍스쳐들이 같이 생성되는데 일단 텍스쳐를 obj로 생성된 물체에 입히기 위한 것이라고 생각하고 넘어갑니다.

윈도우10 그림판3D로 열어보면 위와 같이 obj / mtl 파일이 생성됬음을 확인할 수 있습니다.

이제 이 키즈나 아이의 obj 파일을 직접 코딩한 프로그램으로 출력해봅니다.

Wavefront .obj / .mtl 파일 분석

obj는 object, mtl은 material의 약어입니다.

.obj 파일의 포맷
http://paulbourke.net/dataformats/obj/

.mtl 파일의 포맷
http://paulbourke.net/dataformats/mtl/


위 두 포맷을 설명하는 간단한 캡슐 예제가 같이 있습니다.
http://paulbourke.net/dataformats/obj/minobj.html

이런 표준을 읽으려면 각 단어가 무엇을 의미하는지 정확히 파악하는게 중요합니다.
https://en.wikipedia.org/wiki/3D_computer_graphics

여기서 키즈나 아이 모델과 캡슐에서 사용되는 statement call 들을 위주로 살펴봅니다.

.obj 관련

mtllib (filename1 filename2 . . .)
mtl + lib의 약어
mtl 파일을 불러오는데 사용한다.
이후 usemtl (name) 형식으로 f와 함께 사용한다.
버텍스 표면의 마테리얼(material)과 텍스쳐(Texture)를 정의할 때 사용한다.
마테리얼은 버텍스로 표현된 물체 표면에 빛이 부딪힐 때 처리하는 방법을 표시한다.
텍스쳐는 마테리얼의 색상이나 반사율(albedo) 표시하거나 bump 또는 normal map의 표면을 제공하는데 사용한다.

o (object_name)
object의 약어.
default도 아니고 자주 사용되는 문법도 아니지만 Blender로 obj파일을 생성하면 해당 mesh object 이름을 명명할 때 사용되기도 한다.

v (x y z w)
vertex의 약어
버텍스 각 정점의 좌표.
이 글의 예제에선 x y z 만 사용한다.
w를 지정하지 않은 경우 default 1.0으로 지정된다.

vt (u v w)
vertex texture의 약어
texture vertex의 좌표를 나타낸다.
u v w는 각각 다음과 같은 의미를 갖고 있다.
u 텍스쳐의 평행 방향(horizontal direction)
v 텍스쳐의 수직 방향(vertical direction), 기본 값은 0.
w 텍스쳐의 깊이(depth of the texture), 범위는 0-1.
깊이는 이 예제에서 사용되지 않았다.

vn (i , j, k)
vertex nomals의 약어
좌표가 아니라 3차원 벡터의 형식(i, j, k)으로 표현된다.
Vertex Normal에 대한 개념은 위키피디아에 잘 설명되어있다.
https://en.wikipedia.org/wiki/Normal_mapping

usemtl (material_name)
use + mtl의 약어
이제부터 나오는 f는 해당 mtl 객체를 사용한다고 명시하는 용도다.

f (v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3 . . .)
face의 약어
v1/vt1/vn1에서 1은 각 버텍스의 레퍼런스(reference) 번호인데, 이 번호는 각 v, vt, vn이 선언된 순서에 따른다.
f는 표면에 텍스쳐를 어떻게 입혀야 하는가 좌표를 담고 있다. 자세한 원리는 .mtl에서 다룬다.
f의 arg1, 2, 3은 vertex indices를 의미한다.

그리고 counterclockwise라는 용어가 등장하는데 이는 역시계방향을 의미한다.

참고) 왼손/오른손 좌표계, 시계/반시계 방향, xyz/xzy 와 신난다!

capsule은 f의 v1/vt1/vn1이 모두 동일한 숫자를 사용한다.
하지만 키즈나아이의 경우 다른 숫자를 사용하기도 한다.
키즈나아이의 obj 파일에서 v, vt, vn의 reference의 수는 다음과 같다.
v 5 - 47555 = 47550
vt 47556 - 95106 = 47550
vn 95107 - 142349 = 47242
vn의 경우 obj 파일의 용량을 줄이기 위해서 중복되는 벡터를 생략한 결과 수가 줄어든 것이다.
v와 vt가 다른건 나와 비슷한 의문을 가진 사람이 StackOverflow에 있었다.
vertex와 vertex texture의 reference call 순서가 동일할 필요가 없기 때문이다.
https://stackoverflow.com/questions/29867926/why-does-the-number-of-vt-and-v-elements-in-a-blender-obj-file-differ

.mtl 관련

newmtl
new + mtl의 약어,
새 마테리얼을 정의한다.

Ns (exponent)
specular의 초점(exponent)를 나타낸다.

Ka (r g b)
ambient를 나타낸다.
여기서 접두어 K는 RGB로 이뤄진 값을 의미한다.
mtl 파일은 RGB를 0-1의 실수로 정의한다.

Kd (r g b)
diffuse를 나타낸다.

Ks (r g b)
specular를 나타낸다.

Ke (r g b)
coeficient를 나타낸다.
주의) 1995년 표준에는 적혀있지 않는 이후 추가된 feature다.
http://paulbourke.net/dataformats/mtl/

Ni (optical_density)
광학적 밀도, 굴절(refraction)을 의미하기도 한다.

d (factor)
해당 물체의 흩어짐 정도(dissolve)를 표현한다.

illum
illumination model, 마테리얼에 적용될 조명 모델을 결정한다.
 0 Color on and Ambient off
 1 Color on and Ambient on
 2 Highlight on
 3 Reflection on and Ray trace on
 4 Transparency: Glass on
  Reflection: Ray trace on
 5 Reflection: Fresnel on and Ray trace on
 6 Transparency: Refraction on
  Reflection: Fresnel off and Ray trace on
 7 Transparency: Refraction on
  Reflection: Fresnel on and Ray trace on
 8 Reflection on and Ray trace off
 9 Transparency: Glass on
  Reflection: Ray trace off
 10 Casts shadows onto invisible surfaces

map_kd (-options args filename)
color texture file이나 color procedural texture file을 나타낸다.
이미지 파일이라고 생각하면 편하다.
이 파일은 렌더링 될 때 kd 값을 곱해서 사용한다.

map_d (-options args filename)
scalar texture file, scalar procedural texture file을 나타낸다.
이 파일은 렌더링 될 때 d 값을 곱해서 사용한다.

Tr
투명도(Transparency)이긴한데,
d의 역할과 중복된다.
해당 이슈는 다른 GitHub 저장소 이슈에서도 발생했었다.
https://github.com/syoyo/tinyobjloader/issues/43

obj loader 분석

obj, mtl 파일은 text/plain로 저장됩니다. 이런 파일을 읽기 위해서 loader는 사실상 parser와 비슷하게 구현됩니다. parser는 구문 분석인데.. PL이나 파싱을 검색해보는게 빠릅니다.

obj loader로는 tinyobjloader를 썼습니다. 이런 라이브러리를 선택할 때는 GtiHub에 star가 많은 순서로 정하는데, 일단 쓴 사람이 많으면 버그가 적고 예제가 많아서 좋습니다.

loader가 해야되는 가장 중요한 작업은 obj에서 f로 indicies를 재구성하는 작업입니다.
https://github.com/syoyo/tinyobjloader#usage

tinyobjloader는 vertex data를 attrib로, material을 material 클래스로 만들어줍니다.
그리고 각 obj의 o단위로 shape를 구성합니다. 사용 예제는 아래 코드를 참고합니다.
https://github.com/syoyo/tinyobjloader/blob/master/examples/viewer/viewer.cc

텍스쳐에 쓰이는 이미지 파일은 stb lib 중 stbi로 불러옵니다.
https://github.com/nothings/stb


DrawObject.h

DrawObjec를 위와 같이 구현합니다.
OpenGL Buffer에 넣기 위한 데이터들을 직접 들고 있습니다.
나중에 DrawObject는 VAO로 사용하며 VBO에 numTriangles를 통째로 넣으며
Texture, Material을 적용하는 단위를 SubMesh로 정의해서 순차적으로 glDrawArray합니다.
이에 대해서 자세한 설명은 뒤에서 다시하고 obj 파일을 tinyobjloader를 통해 DrawObject로 넣기 위한 방법을 살펴봅니다.


위는 tinyobjloader를 통해서 obj, mtl, texture image 파일을 읽고 저장하는 설명을 위한 이미지 입니다.
1. obj file을 tinyobjloader에 넣습니다. shape, attrib, material이 이에 맞춰 생성됩니다.
2. obj file에서 mtl 파일을 자동으로 읽습니다. mtl이 없을 경우 예외처리가 필요합니다.
3. shape 단위로 attrib에서 Vertex Position, UV, Normal을 Buffer 하나에 저장합니다.
4. Texture/Material 정보를 저장합니다. Vertex Buffer와는 별개로 저장합니다.
5. obj의 f가 어느 usemtl에 속했는지 체크한 후, 이 단위로 SubMesh를 저장합니다.
6. 이미지 파일은 파일 입출력 작업이니 경로만 갖고 몰아서 처리하는 편이 빠릅니다.

obj파일을 읽는 부분의 코드는 아래 링크에 있습니다.
https://github.com/hyunjun529/WIN_VS_GL_Setups/blob/master/6_obj_viewer/Engine/render/OBJLoader.h#L27

구현 개요

위와 같이 솔루션을 구성합니다.
Engine은 동적 라이브러리(lib)으로 만들어서 사용합니다.
실제 exe파일을 생성하는 프로젝트는 SingleWindowScene입니다.

Engine/component는 크게 input, physics, graphics로 나눠서 메인 루프를 순차적으로 처리하고 메시징할 때 각 컴포넌트가 서로의 객체를 참조하는 방식을 사용합니다. 컴포넌트 묶음의 포인터는 Scene이 관리합니다.

Engine/render는 OpenGL, GLFW 등과 관련있는 기능들 입니다.

SceneViewer는 예전 블로그 글에 설명했습니다.
https://3dshovel.blogspot.kr/2018/02/opengl.html

SingleWindowScene은 Scene 하나만 띄워서
이 프로그램에서 Scene은 여러 Componenet를 갖고 있는 클래스입니다.

입력과 렌더링을 분리하자

Input Component와 Graphics(Render) Component를 분리한 구조는 여러가지 이점을 가질 수 있습니다. 특히 기능을 조금씩 구현해나갈 때 유용합니다.

막상 적으려니 코딩하는 팁 밖에 없습니다.
1. 처음에 구조를 잘 잡으면 나중에 편합니다.
2. 여러 기능을 동시에 테스트 하지 맙시다.
2. Vertex Position이나 glEnable을 테스트할 때 color를 position으로 비례시키면 편합니다.

자세한 작업 과정은 GitHub 저장소의 Commit Log로 확인할 수 있습니다.
Git을 사용할 때 Commit은 기능 단위로, 반드시 빌드가 가능하도록 하면 좋습니다.

ImGui로 디버깅


ImGui를 사용하면 console이나 GLFW callback 등을 사용하지 않고 더 손쉽게, 실시간으로 테스트할 수 있습니다. 그리고 사용 또한 직접 콜백을 구현하는 것 보다 더 간단합니다.
https://github.com/ocornut/imgui

이 프로젝트에선 Camera와 ImGui Component를 Input Component로 만들어서 사용합니다. 이 경우 각 컴포넌트가 서로에 대해서 의존성을 갖지만 이것까지 분리하려고 하면 ECS라는 Entity-Components-System을 구현해야합니다.
https://en.wikipedia.org/wiki/Entity%E2%80%93component%E2%80%93system

OpenGL 바인딩 순서(ShaderProgram, VAO/VBO Buffer)

이 부분의 코드는 아래 링크에 있습니다.
https://github.com/hyunjun529/WIN_VS_GL_Setups/blob/master/6_obj_viewer/Engine/component/RenderComponenet/OBJRenderComponent.h#L27

VAO와 VBO를 언제 어떻게 쓰는지에 대해서 간략히 설명합니다.
VBO(VertexBufferObject)는 직접 Buffer를 bind합니다.
그리고 VAO(VertexArrayObject)는 bind한 VBO들의 현재 상태를 저장합니다.
이로써 여러 객체를 그릴 때 VAO만 바꿔주면 VBO 집합을 다시 Bind할 필요가 없어집니다.

DrawObject는 obj 파일 하나당 하나씩 생성하며 VAO를 하나씩 할당합니다.
그리고 한 DrawObject에서 VBO에 Position, UV, Normal을 통째로 Buffer에 bind합니다.
glDrawArray를 할 때는 각 SubMesh의 시작 index와 size로 그립니다.
이 때 SubMesh 순서에 따라서 Texture Uniform을 바꿔서 bind합니다.

VAO와 VBO에 크로노스 그룹의 OpenGL의 Vertex 명세는 아래 링크에 있습니다.
https://www.khronos.org/opengl/wiki/Vertex_Specification

하지만 실제 사용법은 아래 스택오버플로 질답이 더 도움됬습니다.
https://stackoverflow.com/questions/23314787/use-of-vertex-array-objects-and-vertex-buffer-objects

Texture는 Fragment Shader의 Uniform으로 넘기는 부분이 Vertex와 다릅니다.
Uniform으로 넘기기 위한 TextureId를 SubMesh의 textureId와 별개로 관리합니다.

엄밀히 따지면 이 예제는 여러 텍스쳐를 사용할 뿐 다중 텍스쳐(Multi Texture)는 아닙니다.
다중 텍스쳐는 TextureUnit으로 glActiveTexture와 관련있습니다. 이 경우는 diffuse, ambient, specular, normal, alpha 등을 각각 따로 unit 별로 저장하고 호출하면 됩니다.

Setup 단계에서 DrawObject의 Texture와 Vertex 정보들을 Buffer에 등록합니다.

Render 단계에서는 각 DrawObjects의 SubMesh 순서에 따라서 glDrawArray를 하면 됩니다.

이를 통해서 VAO, VBO, Vertex Shader, Fragment Shader를 나쁘지 않게 사용하는 obj viewer 예제를 만들 수 있었습니다.

---

혹시 더 나은 방법이나 의견, 코드에 관해서 질문이 있으면 댓글 부탁드립니다.

2018년 2월 2일 금요일

Cpp11+에서 std::call_once를 활용한 Singleton logger 구현 (spdlog 사용)

Cpp11+에서 std::call_once를 활용한 Singleton logger 구현




Logger를 구현하다가 싱글턴으로 짜고 싶어져서 만든 코드 일부입니다.
이론적인 설명은 제가 하는 것보다 다른 블로그에 잘 설명된 글이 많으니 그 쪽을 참조해주시기 바랍니다.

이 코드에서 8줄부터 싱글턴 구현이 있고 37줄부터는 전역으로 선언된 싱글턴 클래스를 쉽게 쓰기 위해서 Cpp11+에 추가된 기능들을 이것 저것 사용해놨습니다. 최종적으로 55줄처럼 편하게 사용할 수 있는 형태입니다.

이 코드는 성태의 닷넷 이야기 블로그에 올라온 std::call_once를 보고 cpp11+를 보고 꽂혀서 공부하면서 만들기 시작했습니다.
http://www.sysnet.pe.kr/Default.aspx?mode=2&sub=0&detail=1&pageno=0&wid=11091&rssMode=1&wtype=0

가변 인자 매크로, Parameter pack 활용

Cpp11+에선 가변 인자 매크로를 통해서 여러 인자를 편하게 매크로로 사용할 수 있습니다.
가변 인자 매크로에 관해서는 mycoboco (Jun Woong)님 블로그에 잘 정리되어 있습니다.
http://blog.woong.org/v/59aa5b8cd845cbff6d76d418

Parameter pack은 Cpp11에서 추가된 템플릿 형식입니다.
http://en.cppreference.com/w/cpp/language/parameter_pack
typename... Args로 여러 인자를 편리하게 받을 수 있습니다.
가변 인자 매크로와 함께 사용하면 위 코드와 같이 사용할 수 있습니다.

31줄, 39줄을 쉽게 설명하면 Args&는 입력 받은 매개변수를 call by reference하는 겁니다.
디버그 모드로 찍어서 확인만 해봤지 자세한 동작 원리는 솔직히 잘 모르고 썼습니다.
이론적인 내용은 아래 StackOverFlow에서 읽어볼 수 있습니다.

가변 인자 매크로와 Parameter을 사용한 비슷한 구현 방식으로는 날쑤의 연습장 블로그에도 있습니다.
http://narss.tistory.com/entry/5%EC%9D%BC%EC%B0%A8-%EA%B0%80%EB%B3%80%EC%9D%B8%EC%9E%90-%ED%85%9C%ED%94%8C%EB%A6%BF-variadic-template

위 Gist를 구현할 때 Saba라는 프로젝트를 참조했습니다.
https://github.com/benikabocha/saba/blob/master/src/Saba/Base/Log.h

logger

위 코드의 Log클래스는 전역 싱글턴 객체입니다.
가변 인자 매크로와 템플릿으로 사용하기 편하게 만들어서 55줄처럼 사용합니다.

이 예제에서 사용한 spdlog는 C++ Logging library입니다.
https://github.com/gabime/spdlog

C++에서 Logger에 대한 종류나 개념은 김연우님 블로그에 잘 설명되어있습니다.
http://ozt88.tistory.com/49

2018년 1월 31일 수요일

imgui image 함수로 scene 만들기

imgui에서 Image를 통해 3D element를 렌더링 하는 방법



imgui UI 내에 scene을 그리는 예제를 만들어봤다.
이를 통해서 GLFW Window(Context)를 동시에 처리 하는 방법을 익힐 수 있었다.
또 그래픽스 라이브러리를 사용할 때 주의점이나 팁, 그리고 멀티스레드로 OpenGL을 렌더링하는 방법 등에 대해서 다룬다.

저거 하나 만드는데 알아야 되는게 생각보다 너무 많았다

중간중간 이미지로 삽입된 코드는 GitHub 저장소에 올려놨다.
VS2017 환경에선 다운 받아서 바로 돌릴 수 있게 정리해뒀다.
https://github.com/hyunjun529/WIN_VS_GL_Setups/blob/master/4_imp_scene_in_imgui/main.cpp

imgui 

Bloat-free Immediate Mode Graphical User interface for C++ with minimal dependencies
쉽게 설명하면 UI를 운영체제가 아니라 그래픽스 라이브러리를 통해서 그린다는 소리다.

아래는 imgui를 예시 목록이다.
그래픽스 에디터, 게임 툴, 엔진, 유체 시뮬레이션, Mod 제작 등에 사용되고 있다.

사전 조사

처음 이미지처럼 imgui window 내에 scene을 만들기 위한 예제를 찾아봤다.
사실 imgui window의 image를 통해서 렌더링하는 3D Element, widget 등에 정해진 명칭은 없다. 이 글에서는 해당 요소가 Unity 같은 게임 엔진에서 비슷한 역할을 한다는 의미로 scene이라 부른다.

근데 원리를 설명해둔 부분이나 깔끔한 예제 코드가 없어서 직접 공부해야 했다.
보통 내가 잘 모르겠으면 남들도 잘 모르는 거다. imgui 이슈트래커에 가장 비슷한 이슈로는 아래 두 가지가 있었다.

How to render 3D scene within UI Element ?
https://github.com/ocornut/imgui/issues/475

Rendering am image
https://github.com/ocornut/imgui/issues/561


기본 원리

핵심은 2가지다.
1. GL Context를 별도로 생성하고 관리해야한다.
2. imgui Image()는 Image를 Texture로 그리는거다

Image()는 이름만 같고 운영체제에서 GUI 코딩이나 웹 프론트엔드의 개념과 전혀 다름을 명심하자.

scene을 생성하기 위해서는 다음과 같은 과정을 거친다.
1. scene을 렌더링할 OpenGL Context를 하나 더 생성한다.
2. 이를 위해서 GLFWwindow를 하나 더 생성한다(hidden).
3. hidden은 실제 화면에 그리지 않고 백그라운드에서 렌더링만 수행한다.
4. hidden에서 렌더링된 화면을 픽셀로 저장한다.
5. 4번에서 저장한 픽셀을 main에 전달한다.
6. main에서 imgui Image()로 hidden의 픽셀을 텍스쳐로 렌더링한다.

구현

이미지가 아닌 실제 코드는 아래 링크해둔 저장소에 있고 WIN10, VS2017로 바로 실행할 수 있도록 정리해놨다.
https://github.com/hyunjun529/WIN_VS_GL_Setups/blob/master/4_imp_scene_in_imgui/main.cpp

위와 같이 GLFWwindow를 관리하는 클래스를 따로 만들어둔다.
70줄에 있는 *window는 저렇게 쓰면 좋지 않지만 이후 설명을 위해서 저렇게 해뒀다.

setup()과 render()를 기본적으로 따로 관리해두자.
setup은 렌더링할 객체를 세팅하는 부분, render는 렌더링 루프에서 매번 실행할 함수다.

main 함수에서에서 GLWindow 클래스를 2개 생성한다.
main은 imgui가 있는 화면, hidden은 scene에 삽입할 화면이다.

window_main과 window_hidden을 각각 setup()해준다.
setup() 함수는 vertex binding, shader program을 세팅한다.

ImGui를 초기화 할 때 window_main를 사용하도록 해준다.

window_hidden은 glfwHideWindow를 설정해서 백그라운드에서 동작하도록 한다.
다른 옵션도 찾아보면 더 있는거 같지만 나중에 show()로 확인하기 위해서 사용.

그리고 glfwMakeContextCurrent가 핵심이다.
http://www.glfw.org/docs/latest/group__context.html#ga1c04dc242268f827290fe40aa1c91157
이 함수를 호출하는게 번거롭지만 멀티스레드 환경의 렌더링을 편하게 지원해준다.
가장큰 문제는 스코프를 명시하지 못하니 나중에 클래스로 묶을 때 주의해야한다.
glfwGetCurrentContext로 현재 Context를 확인하는 것도 방법이다.
http://www.glfw.org/docs/latest/group__context.html#gac84759b1f6c2d271a4fea8ae89ec980d

렌더링을 수행하는 루프.
보다시피 window_hidden과 window_main을 따로 렌더링한다.

window_hidden을 렌더링 한 후 glReadPixels로 렌더링된 화면을 저장한다.

하지만 이 함수로 얻은 이미지를 바로 사용하면 상하 반전(flip, flipped)되어 있다.
https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glReadPixels.xhtml
lower left corner에서 0.0으로 잡았기 때문인데 이는 아래 그림과 같다.

만약 영상처리나 파일 저장의 경우 일반적으로 역순으로 출력하면 된다.
하지만 우리는 이를 텍스쳐로 바로 옮겨야한다.

가장 쉬운 접근은 for문으로 역으로 뒤집는 방법을 떠올릴 수 있겠지만 우리는 이 픽셀 데이터를 텍스쳐로 다룬다는 사실을 명심하자.
http://dogfoottech.tistory.com/59

듀토리얼에도 따로 문서화도 없는 이유는 이 라이브러리를 쓰는 사람이 기본적으로 이런 지식을 알고 있으리라 기대하기 때문이다.
사전 조사에서 봤던 2가지 이슈도 이래서 해맸을 가능성이 높다.

ImGui::Image 함수를 살펴보자.
https://github.com/ocornut/imgui/blob/master/imgui.cpp#L6502
함수 인자에 uv0, uv1이 있다. 그리고 우리가 저장한 픽셀 데이터는 Texture2D로 처리된다.

즉 imgui image는 평면 GUI로 보이지만 Image를 삽입하기 위해서, 또 Plot, Font를 처리하기 위해서 Texture를 사용한다.

상하 반전을 하기 위해서 Texture를 거꾸로 씌우면 된다. 이에 관한 내용은 uv를 참조
http://www.opengl-tutorial.org/kr/beginners-tutorials/tutorial-5-a-textured-cube/

위 과정을 통해서 scene 기능을 구현할 수 있었다.

---

여담

Bullet Physics 2.87 ExampleBrowser

이 블로그에서 계속 OpenGL이나 GLFW, imgui 등을 다루고 있는 이유는 최종적으로 Bullet Physics 같은 엔진(?)을 구현하기 위함이다.
18년 2월내로 기본적인 기능은 완성하는게 목표고 중간중간 구현했던 내용을 블로그에 남기는 중이다.

OpenGL로 FPS 카메라 구현하기

https://youtu.be/ikhlwGKKqZk 대학교 그래픽스 강의에서 OpenGL로 간단한 FPS를 구현했었습니다. 세가 신입사원 책이랑 게임 프로그래밍 패턴 책을 참고했었습니다. 핵심 아이디어만 간단히 요약해서 정리해둡니다. ...