DirectX에서 3D공간을 표시하기위해서 행렬들을 이용 , 변환해서 화면으로 출력한 것처럼

화면에서 클릭했을때 어떤것을 클릭했는지 확인하기 위해서는 화면의 x,y좌표를 변환해야 한다.

이번엔 화면의 x, y좌표를 이용해서

1) 클릭한 곳과 구가 충돌하는지

2) 클릭한 곳이 충돌한 부분에서 어느 좌표인지

이 두가지를 확인해 보자.


먼저 "구"라는 물체를 구조체로 정의해보자.

vCenter : 구의 중점 (위치)

fRadius : 구의 반지름 (크기)

isPicked : 선택됬는지 안됬는지 검사

밑의 ST_SPHERE()는 생성했을때 바로 저 값으로 초기화해준다.

클래스 생성자에서 이니셜 라이즈하는거랑 같다고 생각하면 된다.

-> ST_SPHERE를 생성하고 아무값도 안넣어주면 fRadius에 0.0, vCenter에 0,0,0, isPicked에 false값이 들어가있음


다음은 광선(마우스로 클릭한 좌표에서 -> 클릭한 물체로 향하는)에 대한 정의이다.

m_vOriginal : 광선의 출발 지점 (마우스로 클릭한 좌표)

m_vDirection : 광선의 방향 (화면 안쪽으로 향하게 됨)

m_eRaySpace : 종류를 나타냄, 궂이 없어도 된다.


다음은 마우스 왼쪽클릭시 실행되는 코드이다. 어떤 흐름인지 보자

cRay r : 현재 클릭한 마우스의 x좌표 (LOWORD(lParam), y좌표 (HIWORD(lParam))을 이용해 계산에 사용할 광선을 구해온다.

for반복문 : 모든 구들을 확인한다.

r.IsPicked : 구와 현재 광선이 충돌하는지 확인하고 충돌하면 true 충돌하지 않으면 false를 반환한다.

1) : 광선을 구하고

2) : 모든 구에

3) : 광선이 충돌하는지 검사하고 값을 바꿈

이와같은 순서로 흘러가고 있다.


그러면 어떻게 구해지는지 살펴보자.

RayAtWorldSpace를 구하기 위해서 ViewSpace함수가 필요하기 때문에 처음인 ViewSpace부터 시작한다.


RayAtViewSpace함수

1) d3dDevice에서 뷰포트를 얻어온뒤 저장한다.

2) d3dDevice에서 투영행렬(Projection)을 얻어온뒤 저장한다.

3) 계산에 필요한 방향벡터 (m_vDirection)를 만든다.

4) X,Y좌표에 뷰포트 , 투영행렬을 이용해 계산한다.

4-1)    X = ((+2.0f * 마우스X좌표) / 뷰포트 넓이 - 1.0f) / 투영행렬중 1행 1열 (만약 배열, 벡터로 행렬이 되어있다면 MatProjection[0][0]에 해당한다)

         ->이는 투영행렬의 1행 1열이 X값이 변하는데 영향을 주기 때문이다.

4-2)    Y = ((-2.0f * 마우스Y좌표) / 뷰포트 높이 + 1.0f) / 투영행렬중 2행 2열 (배열 , 벡터라면 [1][1]에 해당)

         ->투영행렬의 2행 2열이 Y값 변화에 영향을 줌

4-3)    Z = 1.0f

         투영 윈도우 값이다. DirectX는 평면 z = 1 과 일치하도록 정의하고 있다.

RayAtWorldSpace함수

1) r에 뷰포트, 투영행렬을 이용해 변환한 방향벡터를 얻어온다.

2) 행렬을 담기위한 변수 2개를 생성한다 (matView, matInvView)

3) d3dDevice를 이용해 matView에 현재 View행렬을 담는다.

4) matInvView행렬에 matView의 역행렬을 담는다.

5) 뷰의 역행렬을 이용해서 광선의 출발점을 변환한다.

6) 뷰의 역행렬을 이용해서 방향벡터를 변환한다.

7) 방향벡터를 정규화(행렬안의 모든값이 1보다 작도록)한다.


여기서 왜 View의 역행렬을 구하는건가 ?

렌더링 파이프라인을 한번 살펴보자

여기에서 실제로 좌표에 영향을 주는 부분은

1 : 로컬 스페이스 -> 2 : 월드 스페이스 -> 3 : 뷰 스페이스 -> 4 : 투영 -> 5 : 뷰포트 이다.

뷰포트 , 투영과 관련된 부분은 이전함수에서 처리했지만 뷰 스페이스와 관련된 부분은 아직 처리되지 않았다.

우리는 현재 뷰스페이스가 적용되기 전의 좌표가 필요하다.

그래서 뷰스페이스의 역행렬을 이용해서 좌표를 변환하는 것이다.


지금까지의 과정을 모두 거치면 실제 3D좌표 내에서 클릭한 점 (벡터)가리키는 방향 (벡터)를 둘다 구한 것이다.

이제 구와 충돌을 어떻게 처리하는지 확인해보자.

r.IsPicked : 광선 R을 이용해서 구와 충돌하는지 확인하겠다는 뜻이다.

1) cRay r = (*this) r이라는 변수를 만들고 거기에 자신의 값을 집어넣는다.

여기에서는 자기와 같은 광선 r을 한게 더 만든거다.

2) matInvWorld : 월드의 역행렬

2-1)    역행렬의 4행 1열에 구의 -x좌표를 넣는다

2-2)    역행렬의 4행 2열에 구의 -y좌표를 넣는다

2-3)    역행렬의 4행 3열에 구의 -z좌표를 넣는다

3) 월드의 역행렬을 이용해 카메라의 좌표를 변환한다.

4) 월드의 역행렬을 이용해 방향벡터를 변환한다.

5) vv는 방향벡터와 방향벡터를 내적한 값이다.

6) qv는 위치좌표와 방향벡터를 내적한 값이다.

7) qq는 위치좌표와 위치좌표를 내적한 값이다.

8) rr은 구의 반지름 * 반지름한 값이다.

9) qv * qv - vv * (qq - rr) >= 0;을 반환한다.

9-1)    0이라면 1개의 점이 교차, 0보다 크다면 2개의 점이 교차, 0보다 작다면 교차하지 않는다.

사실 여기 너무 어려워서 나도잘 모르겠음 ;;; 좀더 찾아봐야 겠다


다음은 세개의 점 (면)과 선이 충돌하는지 확인하는 부분이다.

사용될 함수는 D3DXIntersectTri 이다.

D3DXintersectTri함수는 충돌했다면 True, 충돌하지 않았다면 false값을 반환한다.

여기서 충돌된 지점을 구하는 부분이 중요하다

1번째 식 : 광선의 출발점 + t(출발점에서 충돌된 지점까지의 거리) * 광선의 방향

2번째 식 : v0 + (u * (v1 - v0)) + (v * (v2 - v0));

-> 충돌에 사용된 3개의 점을 이용해서 구한다. v0의 점에서 v1방향으로 u만큼 , v2방향으로 v만큼 움직여서 구한다.

이렇게 하면 충돌된 여부 뿐만 아니라 충돌된 좌표가 어디인지 구해낼 수 있다.

여러 책들은 번거롭게 일일히 점을 입력해서 배열방식으로 삽입하는데

이 글에서는 이미 벡터에 점과 나머지 정보가 담겨있는 상태에서 메쉬에 집어 넣어보자.


시작하기전 LPD3DXMESH == ID3DXMesh* 이다. 이는 정의에서도 확인할 수 있다.


1. 모든 점(Point)들은 std::vector<ST_PNT_VERTEX> vecTotalVertex에 담겨있다.

->ST_PNT_VERTEX는 구조체로 D3DXVECTOR3 p (점), D3DXVECTOR3 n (법선벡터), D3DXVECTOR2 t (텍스쳐)의 정보를 가지고 있다.

ST_PNT_VERTEX::FVF == D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1 이다.

이와 비슷한 구조체를 여러분들은 이미 만들어서 사용중일거다.

ST_PNT_VERTEX대신에 여러분들이 사용중인 구조체의 이름을 적어주면 된다.

2. 모든 인덱스(Index)는 std::vector<WORD> vecIndex에 담겨있다.

3. 모든 속성(Attribute)은 std::vector<DWORD> vecAttribute에 담겨있다.


자 그럼 이제 MESH를 생성해보자.


LPD3DXMESH pMesh = NULL;


D3DXCreateMeshFVF(vecTotalVertex.size() / 3,    Mesh가 가지고 있는 모든 면의 개수, 현재 여기서는 삼각형으로 모든 면이 구현되기 때문에 이렇게 계산

vecTotalVertex.size(),        Mesh가 가지고 있는 모든 점의 개수

D3DXMESH_MANAGED,    생성 옵션, 관리 메모리 풀 내에 보관되도록 설정했다.

ST_PNT_VERTEX::FVF,        구조체의 FVF값, 현재 여기서는 D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_TEX1 값이 들어가고 있다.

g_pD3DDevice,                DirectX Device를 넣어주면 된다. 여기서는 싱글톤으로 관리되는 디바이스를 넣어줬다.

&pMesh);                        방금 생성한 Mesh의 주소를 넣어주면 된다.


현재 모든 데이터가 파일에서 파싱되어 벡터에 저장되어 있기 때문에 벡터의 정보를 그대로 메쉬에 넣어보자


ST_PNT_VERTEX* vertex;                                            다른 구조체 이름으로 정의되었다면 해당 구조체 포인터를 만든다.

pMesh->LockVertexBuffer(0, (void**)&vertex);                Mesh에 점을 복사하기 위해서 메모리를 잠근다.

memcpy(vertex, &vecTotalVertex[0], vecTotalVertex.size() * sizeof(ST_PNT_VERTEX));

메모리에 벡터의 처음부터 끝까지 복사해서 집어 넣는다. ST_PNT_VERTEX대신에 여러분들의 구조체 이름을 적으면 된다.

pMesh->UnlockVertexBuffer();                                    점을 다 입력했다면 무조건 잠금을 해제한다.


WORD* index = 0;                                                          인덱스 버퍼에 접근하기 위한 WORD포인터

pMesh->LockIndexBuffer(0, (void**)&index);                         인덱스 버퍼를 잠근다.

memcpy(index, &vecIndex[0], vecIndex.size() * sizeof(WORD));   메모리에 벡터의 처음부터 끝까지 복사해서 집어넣는다. 인덱스는 무조건 WORD이다.

pMesh->UnlockIndexBuffer();                                            인덱스를 다 입력했다면 무조건 잠금을 해제한다.


DWORD* attributeBuffer = 0;                                속성버퍼에 접근하기위한 DWORD포인터

pMesh->LockAttributeBuffer(0, &attributeBuffer);      속성버퍼를 잠근다.

memcpy(attributeBuffer, &vecAttribute[0], vecAttribute.size() * sizeof(DWORD)); 

       메모리에 벡터의 처음부터 끝까지 복사해서 집어넣는다. 속성 인덱스는 무조건 DWORD 이다.

pMesh->UnlockAttributeBuffer                             속성을 다 입력했다면 무조건 잠금을 해제한다.


이와 같은 과정을 거치면 벡터에 저장된 모든 데이터를 Mesh에 삽입할 수 있다.

3D MAX로 만든 데이터를 DirectX로 그리기 위해서는 여러가지 과정이 필요하다.

그중 이번에는 ASE Animation 데이터를 읽어보자.

애니메이션의 종류는 여러가지인데 그 중 지금하는건 키프레임 방식의 애니메이션이다.


정점부터 애니메이션까지 모든 정보를 가진 상태를 기준으로 진행된다.(파싱이 완료된 상황)


위의 사진과 같이 업데이트를 돌게된다.

nKeyFrame은 현재 프레임을 말한다. (ex: 시작 프레임이 4 , 마지막 프레임이 20이라면 4 ~ 20 사이의 값)

pMatParent는 부모 행렬을 가리킨다. (단 Root는 부모 행렬이 없다.)


Scale -> Rotate -> Translate (크기 , 회전 , 이동)의 과정중 현재 회전 , 이동만 처리되어있다.


1) CalcLocalR을 통해 Rotate행렬을 구한다.

2) CalcLocalT를 통해 Translate 행렬을 구한다.

3) 부모가 없다면 Rotate * Translate 행렬을 곱해 자신의 로컬 행렬을 구하고

부모가 있다면 Rotate * Translate 한 행렬에 부모 행렬을 곱해준다.

4)그다음 노드(자신)가 소유한 자식들에게 현재 가지고 있는 월드 매트릭스(행렬)를 보내준다.


먼저 위의 사진과 같이 업데이트를 하기 위해서는 시간이 일단 흘러가야한다.

시간을 어떻게 증가시키는지 확인해보자.


main.cpp에서 업데이트를 호출할땐 Root에서 nKeyFrame을 구하고 있다. 이함수를 통해 시간이 늘어나는것을 확인하고 있다.


다음 사진은 시간을 구하는 함수와 ASE파일에서 SCENE부분이다. 내용을 먼저 보자.


ASE파일에서 FIRSTFRAME은 4이다. 즉 0 ~ 4 사이의 프레임에 대한 정보가 없다.

LASTFRAME이 20이기 때문에 지금 이 애니메이션 4 ~ 20프레임 까지의 애니메이션 정보밖에 없다.

4보다 작은 프레임 , 20보다 큰 프레임에 대한 정보가 없다는 점에 주의해야 한다.

이제 GetKeyFrame함수를 보자.

GetTickCount는 컴퓨터(윈도우)가 켜진뒤 값이 계속 증가하는데 이 값을 얻어오는 함수이다.


1.First에 첫 프레임값 (4)과 틱에 따른 프레임 증가값 (160)을 곱해서 담아준다.

2.Last에 마지막 프레임 값(16)과 틱에 따른 프레임 증가값 (160)을 곱해서 담아준다.

3.GetTickCount를 하게되면 일단 컴퓨터로부터 어떤 값이 넘어오게 된다.

이 값을 (최대프레임 - 최소프레임) 차를 이용해 나머지 연산을 하게되면 프레임 차이인 16보다 작은 값이 나오게 된다.

4.하지만 지금 애니메이션 정보가 4 ~ 20 까지 있기 때문에 최소값인 4를 더해준다.

이렇게 최소값을 더해주게 되면 4 ~ 20 사이의 값이 저 함수를 통해 나오게 된다.


GetTickCount를 하게되면 컴퓨터가 켜져있는 이상 값이 계속 증가하기 때문에 시간이 증가하게 된다.

단 이함수는 행동이 이어지는것 뿐이지 처음부터 애니메이션을 하지는 않기 때문에

애니메이션이 처음부터 나오게 하길 원한다면 기준이 될 시간값 (여기서는 GetTickCount)을 대체할 다른 변수가 필요하다.


이함수를 통해서 시간이 지남에 따라 프레임이 증가하는 효과를 낼수 있다.

이제 시간은 흘러가게 됬으니 Rotate함수와 Translate함수를 보자.

Rotate 행렬을 구하는 함수를 먼저 보자.

1) matR이 초기화가 안되있기 때문에 항등행렬(Identity)로 초기화 해준다.

m_vecRotTrack은 ASE파일에서 읽어온 ROT_TRACK 데이터를 담아둔 곳이다.

2) 만약 아무 정보도 없다면 자신의 좌표인 LocalTM으로 초기화 해준다.


3)프레임이 RotTrack에 맨처음에 담긴 n보다 작거나 같다면 Rotation행렬을 RotTrack[0]에 담긴 쿼터니언 값만큼 회전한뒤 함수를 끝낸다.

-> 0 ~ 4 프레임까지의 정보가 없기 때문에 nKeyFrame이 4보다 작은 값이 들어오면 최소프레임인 4의 데이터를 넣어준다.

4)프레임이 RotTrack에 맨마지막에 담긴 n보다 크거나 같다면 RotTrack[Size - 1]에 담긴 쿼터니언 값만큼 회전한뒤 함수를 끝내버린다.

-> 20프레임 까지의 데이터밖에 없기 때문에 nKeyFrame이 20보다 큰값이 들어온다면 마지막 값이 20의 데이터를 넣어준다.


5) 만약 여기까지 왔는데도 아무 조건에 해당하지 않는다면 이제 RotTrack에 담긴 모든값들을 확인한다

-> 이 과정은 현재 프레임이 어디인지를 찾는 과정이다.

6) 이렇게 해서 현재 프레임을 찾은뒤 t를 구하게 된다 (t는 이전 프레임 , 다음 프레임 에서 어느정도만큼 움직였는지 확인하는 거다)

EX : 1프레임일때 X좌표가 0, 2프레임일때 X좌표가 10, 지금 X좌표가 5라면 현재 내 t값은 0.5다. (0 ~ 10 에서 5는 절반만큼 왔으니까

비율로 나타낸다면 0 ~ 1 의 절반인 0.5로 나타내 진것)


7) 이런식으로 이전 프레임 , 다음 프레임 사이에서 내가 얼마나 움직였는지 t값을 구하고 그값만큼 쿼터니언 함수를 이용해서 행렬을 이동한다.

->쿼터니언을 이용해서 회전하지 않으면 짐벌락이라는 문제가 생길수 있기 때문에 쿼터니언을 이용한다.


이단계를 마무리 하면 Rotation행렬이 모두 구해진다.

이제 Translate행렬을 구해보자.

1) 먼저 MatT를 항등행렬(Identity)로 초기화 해준다.

2) 만약 POS_TRACK의 값이 없다면 자신의 Local좌표 (LocalTM의 4행 1열 , 4행 2열, 4행 3열의값 코드기준시 -1씩)를 넣어준다.

3) 현재 프레임이 맨처음 프레임보다 적거나 같다면 x,y,z를 맨처음 프레임 애니메이션 값으로 초기화한다.

   ->0 ~ 4프레임 까지의 정보가 없기 때문에 nKeyFrame이 최소프레임보다 적게 들어온 상황을 위한 예외처리

4) 현재 프레임이 맨 마지막 프레임보다 크거나 같다면 x,y,z를 마지막 프레임 애니메이션 값으로 초기화한다.

   ->20프레임 이후의 정보가 없기때문에 nKeyFrame이 20프레임보다 크게 들어온 상황을 위한 예외처리


5) 두 예외상황이 모두 아니라면 이제 현재 프레임이 어디에 있는지 벡터를 모두 돌면서 찾는다.

벡터를 모두 돌아서 현재 프레임이 어느 프레임 사이에 있는지 찾아냈다면

6) 이전 프레임 , 다음 프레임 사이에서 어느정도에 위치하고 있는지 T값을 찾는다.

7) 그다음 출발점 [nPrevIndex]에서 T값만큼 쿼터니언 회전을 해서 위치를 찾아낸다.

그뒤 그위치를 적용시킨다.


이렇게 R , T를 적용시킨뒤 부모 행렬이 만약 존재한다면 부모행렬을 곱해주면 된다. (main의 cpp에서 처리되고 있다)

이와같은 흐름을 통해 애니메이션을 재생할 수 있다.

+ Recent posts