1. 탱크 모델을 띄우고 문제점 발견하기
여러개가 뭉처져서 하나를 만드는 경우가 있다.
만약 이걸 만들어야 한다면 분할 된 거 마다 메시를 만들어 합칠 수 있다.
지금 상황에서는 뼈대 정보를 이용해서 계층 구조를 잘 활용하지 않으면 물체가 정상적으로 뜨지 않는다는 문제가 생길 수 있다.
AssimpTool/Game/AssimpTool.cpp에서
Tower말고도 Tank도 같이 넣어준다.
void AssimpTool::Init()
{
{
shared_ptr<Converter> converter = make_shared<Converter>();
// FBX -> Memory
converter->ReadAssetFile(L"Tank/Tank.fbx");
// Memory -> CustomData (File)
// 1차 목표
converter->ExportMaterialData(L"Tank/Tank"); // xml 안붙이는 이유는 통일이 되어 있기 때문
// Material이 필요 없으면 이 코드는 주석 처리하면 된다. Material만 하고 싶으면 얘만 놔두면 된다.
// 읽은 데이터를 추출해서 우리만의 커스텀 데이터 파일로 만들어 주는게 목적이다.
converter->ExportModelData(L"Tank/Tank");
// CustomData (File) -> Memory
}
그리고 AssimpTool/Main/Main.cpp에서
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
GameDesc desc;
desc.appName = L"GameCoding";
desc.hInstance = hInstance;
desc.vsync = false;
desc.hWnd = NULL;
desc.width = 800;
desc.height = 600;
desc.clearColor = Color(0.f, 0.f, 0.f, 0.f);
desc.app = make_shared<StaticMeshDemo>(); // 실행 단위
//desc.app = make_shared<AssimpTool>();
GAME->Run(desc);
return 0;
}
필요에 따라 AssimpTool로 교체해서 사용할 수 있게 한다.
나중에 가면 따로 실행파일만 bat 파일로 해서 툴만 따로 돌린다거나 해주면 된다.
중요한 건 이 아이를 실행 시켜서 Tank를 로드해볼 것이다.
지금은 일단 WinMain에서 실행단위를
desc.app = make_shared<StaticMeshDemo>(); // 실행 단위
//desc.app = make_shared<AssimpTool>();
StaticMeshDemo로 다시 바꾼 다음에
StaticMeshDemo.h로 가서
void CreateTank();
를 추가해서 Tank를 로딩해서 살펴본다.
StaticMeshDemo::Init()에서 CreateTank를 호출한다.
//CreateTower();
CreateTank();
CreateTank는 StaticMeshDemo.cpp에서 구현한다.
Tower는 간단한 애였고, 단일로 되어 있었기 때문에 지금까지 아무런 문제가 없었지만 Tank는 여러 메쉬들이 뭉쳐져 있는 형태이기 때문에 같은 방식으로 만들어서 작업을 한다면 다른 부분이 있을 것이다.
void StaticMeshDemo::CreateTank()
{
// CustomData -> Memory
shared_ptr<class Model> m1 = make_shared<Model>();
m1->ReadModel(L"Tank/Tank");
m1->ReadMaterial(L"Tank/Tank");
_obj = make_shared<GameObject>();
_obj->GetOrAddTransform()->SetPosition(Vec3(0, 0, 50));
_obj->GetOrAddTransform()->SetScale(Vec3(1.f));
_obj->AddComponent(make_shared<ModelRenderer>(_shader));
{
_obj->GetModelRenderer()->SetModel(m1);
// _obj->GetModelRenderer()->SetPass(1);
}
}
이렇게 CreateTank를 구현하고 실행을 해보면
AssimpTool::Init의 converter->ExportModelData(L"Tower/Tower"); 이 코드가 실행 되면서
Resources/Models/Tank에 Tank.mesh가 생성이 되어 있다.
15. ModelDemo.fx에서
technique11 T0
{
PASS_VP(P0, VS, PS)
PASS_RS_VP(P1, FillModeWireFrame, VS, PS_RED)
};
이렇게 pass 옵션이 2개가 되게 PASS_VP(P0, VS, PS)에 걸려있던 주석을 해제하고
void StaticMeshDemo::CreateTank() 에서도
_obj->GetModelRenderer()->SetPass(1);
이렇게 pass를 1로 세팅을 한다.
실행을 하면

탱크가 보이긴 한데 너무 크니
StaticMeshDemo::CreateTank()에서
_obj->GetOrAddTransform()->SetScale(Vec3(0.1f));
이렇게 크기를 조절한.

탱크가 만들어 진게 보이긴 하다. 모양이 이상하다.
뭔가가 중앙에 밀집되어 있는것을 볼 수 있다.
이게 왜 그런걸까?
Bone에 대한 정보를 활용하지 않았기 때문이다.
Model에서 들고 있는 많은 정보들 중에서
vector<shared_ptr<ModelBone>> _bones;
이 변수는 뼈대와 관련된 정보들을 들고 있다. Mesh가 있어도 무조건 중앙에 배치해달라는게 아니라 뭔가 나름대로 포지션이 있었다.
그 포지션을 지켜주지 않았기 때문에 지금 이리저리 위치해 있다.
자기 부모를 기준으로 해서 위로위로 타고 올라가서 최종 루트를 기준으로 했을 때의 좌표를 구한 건 맞지만, 최종적으로 그런 애가 여러개 있어서 메쉬가 여러 개 있을 때의 처리가 안되고 있으니까 모든 물체들이 중앙에 일단 위치해 있게 배치가 되었다고 결론을 낼 수 있다.
이 부분을 간단하게 고칠 수가 있는데 오늘 할 필요는 없다. StaticMesh랑은 경계선이 있는 거기 때문이다. 그럼에도 하는 건 애니메이션을 다룰 때의 예습이라고 볼 수 있다.
2. 쉐이더 15. ModelDemo.fx에 각 Bone에 따른 루트 좌표계로 변환하는 매트릭스 행렬과 BoneIndex를 작성하고 input의 position에 BoneIndex로 찾은 행렬을 곱해서 보정해 탱크를 그리기
1) 15. ModelDemo.fx에 Bone들의 Trasnsform 매트릭스를 담은 행렬과 인덱스를 담은 변수 선언하기
15. ModelDemo.fx에서 50개짜리의 뼈대의 정보를 받아줄 수 있다고 가정을 해보자.
#define MAX_MODEL_TRANSFORMS 50
cbuffer BoneBuffer
{
matrix BoneTransforms[MAX_MODEL_TRANSFORMS];
};
uint BoneIndex;
지금까지 뼈대에 대한 transform들을 BoneTransforms에 넣어주고, BoneIndex를 따로 받아주는 개념이라고 볼 수 있다.
셰이더에 이걸 넣어줘서 사용할 건데
ModelRenderer에 가서 이 부분을 연동을 시켜줄 준비를 해야 한다.
많은 부분이 달라지진 않는다.
ModelRenderer.cpp의 Update를 복제 하고 원래 버전은 주석 처리를 하고, 두 번째 버전을 하나 더 만들 것이다. 히스토리 삼아서 첫번째 버전을 남겨둔 것이다.
메시에 물체에 대한 정보를 만들어 줬고, 메시에 대한 정보는 물체가 이루어지는 기하학적인 도형을 표현하고 있는데 최종 루트를 기준으로 하고 있는게 아니다. 내가 어떻게 생겼는지가 사실상 Mesh라는 개념이다. 그렇기 때문에 0, 0, 0 이라는 포지션에 위치해 있는데 계층 구조가 있기 때문에 parent를 기반으로 계속 옮겨 다니고 싶은 것이다.
2) RenderManager에서 Bone을 Shader에 넘겨주기 위해 버퍼들을 만들어 주고, Shader의 BoneBuffer와 파싱하고, BoneDesc 구조체를 만들고, PushBoneData 함수를 선언하는 등 준비하기
먼저 Bone이란 개념을 추가를 해서 이거를 쉐이더에다 넘겨줄 준비를 해줄 것인데
셰이더에 넘겨줄 때는 항상 RenderManager에서 작업을 했었다.
RenderManager.h에서
// Bone
#define MAX_BONE_TRANSFORMS 50
struct BoneDesc
{
Matrix transform[MAX_BONE_TRANSFORMS];
};
이렇게 추가하고
class RenderManager 안에
BoneDesc _boneDesc;
shared_ptr<ConstantBuffer<BoneDesc>> _boneBuffer;
ComPtr<ID3DX11EffectConstantBuffer> _boneEffectBuffer;
이렇게 추가로 BoneDesc와 Buffer 관련 변수를 넣어준다.
그리고
void PushBoneData(const BoneDesc& desc);
를 추가한다.
RenderManager::Init에서 다른 버퍼들을 만들어 주는 작업을 해줬었다.
_boneBuffer = make_shared<ConstantBuffer<BoneDesc>>();
_boneBuffer->Create();
_boneEffectBuffer = _shader->GetConstantBuffer("BoneBuffer");
이렇게 BoneBuffer를 만들어 주는 코드를 추가한다.
3) Shader에 Bone데이터 넣어주는 함수 PushBoneData 구현하기
void RenderManager::PushBoneData(const BoneDesc& desc)
{
_boneDesc = desc;
_boneBuffer->CopyData(_boneDesc);
_boneEffectBuffer->SetConstantBuffer(_boneBuffer->GetComPtr().Get());
}
BoneDesc를 저장한 다음에 CopyData를 통해 Shader, GPU쪽에다가 방금 만들어준 정보들을 밀어 넣는 작업을 하게 한다.
(_boneEffectBuffer는 cbuffer BoneBuffer와 RenderManager::Init에서 파싱했다.
_boneEffectBuffer = _shader->GetConstantBuffer("BoneBuffer"); )
애니메이션을 틀어야 할 때는 물체마다 뼈대에 대한 정보를 들고 있을 터인데
노드에서 파싱했던 계층 정보가 15. ModelDemo.fx의 cbuffer BoneBuffer 에 들어갈 수 있게, 순차적으로 쭉 넣어주게 될 것이다. 그게 PushBoneData에서 해주는 역할이라 할 수 있다.
4) ModelRenderer::Update에서 BoneDesc를 세팅해서 PushBoneData를 호출해 쉐이더에 전달하기
ModelRenderer::Update에서 Model을 렌더링 할 때는
설정했던 BoneDesc을 설정해서 그거를 밀어 넣어주도록 할 것이다.
나중엔 모델의 종류가 많아질 테니까 새로운 물체를 그릴 때마다 이런 식으로 관련된 뼈대 정보들을 싸그리 모아가지고 전달을 해주도록 한다.
void ModelRenderer::Update()
{
if (_model == nullptr)
return;
// Bones
BoneDesc boneDesc;
const uint32 boneCount = _model->GetBoneCount();
for (uint32 i = 0; i < boneCount; i++)
{
shared_ptr<ModelBone> bone = _model->GetBoneByIndex(i);
boneDesc.transform[i] = bone->transform;
// 안채워 준 값은 쓰레기값이 들어가겠지만 쉐이더에서 사용 안할 예정이라서 일단은 냅둔다.
// 깔끔하게 하려면 identity 행렬을 밀어 준다거나 하는 식으로 작업을 하면 된다.
}
RENDER->PushBoneData(boneDesc);
5) ModelRenderer::Update에서 쉐이더의 BoneIndex를 파싱하기
여기서 한 건 Bone 정보를 쭉 밀어준 것에 불과하다.
그럼 그 Bone 정보를 밀어 넣어 줬으면
물체마다 Mesh마다 출력을 함과 동시에 내가 몇 번째 Bone인 것을 나타내는 BoneIndex를 전달해 줄 것이다.
Shader에서 uint BoneIndex;를 밖으로 빼서 글로벌로 만들어 놨기에 임시적으로 다음과 같이 세팅한다.
const auto& meshes = _model->GetMeshes();
for (auto& mesh : meshes)
{
if (mesh->material)
mesh->material->Update();
// BoneIndex
_shader->GetScalar("BoneIndex")->SetInt(mesh->boneIndex);
Update 코드 전체를 보면
void ModelRenderer::Update()
{
if (_model == nullptr)
return;
// Bones
BoneDesc boneDesc;
const uint32 boneCount = _model->GetBoneCount();
for (uint32 i = 0; i < boneCount; i++)
{
shared_ptr<ModelBone> bone = _model->GetBoneByIndex(i);
boneDesc.transform[i] = bone->transform;
// 안채워 준 값은 쓰레기값이 들어가겠지만 쉐이더에서 사용 안할 예정이라서 일단은 냅둔다.
// 깔끔하게 하려면 identity 행렬을 밀어 준다거나 하는 식으로 작업을 하면 된다.
}
RENDER->PushBoneData(boneDesc);
// Transform
auto world = GetTransform()->GetWorldMatrix();
RENDER->PushTransformData(TransformDesc{ world });
const auto& meshes = _model->GetMeshes();
for (auto& mesh : meshes)
{
if (mesh->material)
mesh->material->Update();
// BoneIndex
_shader->GetScalar("BoneIndex")->SetInt(mesh->boneIndex);
uint32 stride = mesh->vertexBuffer->GetStride();
uint32 offset = mesh->vertexBuffer->GetOffset();
DC->IASetVertexBuffers(0, 1, mesh->vertexBuffer->GetComPtr().GetAddressOf(), &stride, &offset);
DC->IASetIndexBuffer(mesh->indexBuffer->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0);
_shader->DrawIndexed(0, _pass, mesh->indexBuffer->GetCount(), 0, 0);
}
}
이렇게 된다.
Engine을 rebuild한다.
결국 열심히 파싱했던 transform 정보들을
cbuffer BoneBuffer
{
matrix BoneTransforms[MAX_MODEL_TRANSFORMS];
};
골격 구조와 관련된 부분들이랑
uint BoneIndex;
그리고 uint BoneIndex라고 해서 내가 현재 그리고 있는 물체가 몇번째 뼈대에 연결이 되어 있었던 mesh인지를 BoneTransforms에 연결해줬다고 보면 된다.
6) 쉐이더에서 BoneTransforms를 이용해 input.position을 로컬(루트) 좌표계 기준으로 보정해주기
이 BoneTransforms이 실질적으로 파싱을 할 때 곰곰히 생각을 해보면
어떤 물체가 물체의 상대적인 relative한 좌표를 기준으로 하는 나의 부모님을 기준으로 하는 좌표계에서의 좌표를 들고 있었을텐데
그거를 타고 타고 가서 루트를 기준으로 하는 사실상 로컬좌표계로 변환을 할 수 있다.
지금은 물체가 로컬(루트)로 가지 않은 상태라고 볼 수 있다. 물체의 생김새는 알 수 있지만 아직 로컬(루트)로 가지 않은 상태이기 때문에 그걸 쉐이더 쪽에서 한 번 더 보정을 해줄 것이다.
VS에
MeshOutput VS(VertexTextureNormalTangent input)
{
MeshOutput output;
output.position = mul(input.position, BoneTransforms[BoneIndex]);
output.position = mul(output.position, W);
output.worldPosition = output.position.xyz;
output.position = mul(output.position, VP);
output.uv = input.uv;
output.normal = mul(input.normal, (float3x3)W);
output.tangent = mul(input.tangent, (float3x3)W);
return output;
}
output.position = mul(input.position, BoneTransforms[BoneIndex]);
코드를 한 줄 추가해줬다.
StaticMeshDemo::CreateTank에서
_obj->GetOrAddTransform()->SetScale(Vec3(1.f));
다시 Scale을 1.f으로 되돌린 뒤 실행을 하면

이제 탱크모양이 살아있는 것을 볼 수 있다.
원래와 모양이 달라지고 정상적으로 보이고 있다.
이렇게 탱크가 보이고 있다가 결론이다.
7) 복기
복기를 해보면
Shader에서 뼈대와 관련된 정보를 싸그리 넣어줬다.
현재 몇번째 뼈대인지를 이용해서
Bone 각각에 행렬이 있는데
Bone에 들어가 있는 행렬 중에 하나를 골라가지고 월드 행렬을 곱하기 전 첫번째 단계에서 본의 행렬을 곱한 다음에 월드행렬을 곱하는 것으로 넘어간 것이다.
이게 무엇을 한 것인지를 고민할 필요가 있다.
가장 중요한 정보는 Converter::ReadModelData의
// Relative Transform
Matrix transform(node->mTransformation[0]); // Matrix 생성자 중에서 주소 하나를 받는 버전
bone->transform = transform.Transpose();
// 2) Root(Local)
Matrix matParent = Matrix::Identity;
if (parent >= 0) // index의 부모가 있다면
matParent = _bones[parent]->transform; // 부모부터 root까지 한번에 계산한 거
// Local (Root) Transform
bone->transform = bone->transform * matParent;
이 정보라고 했다.
처음에 받았던 transform 자체는 relative한 좌표를 갖고 있는 것이고
relative한 transform에다가 부모 부모 부분을 곱해가지고 로컬로 넘어가는 변환 행렬을 구해 줬었다.
그게 bone→transform에 들어가 있는 것이고,
그게 실제로 전달전달 되어가지고
쉐이더 15. ModelDemo.fx의
cbuffer BoneBuffer
{
matrix BoneTransforms[MAX_MODEL_TRANSFORMS];
};
이 부분에 넣어준 부분이
Converter::ReadModelData에서 구해준
bone->transform = bone->transform * matParent;
이 아이를 얘기하고 있는 것이다.
cbuffer BoneBuffer
{
matrix BoneTransforms[MAX_MODEL_TRANSFORMS];
};
여기서 들어간 transorm이란 무엇이냐, 결국에는 나의 직속 상관으로 넘어가는 그 행렬이 아니라
쭉 뚫어서 맨 마지막까지 뚫고 넘어가는 그 로컬로 변환하는 그 행렬을 말하는 것이다.
그거를 만들기 위해서 여기까지 작업을 해줘서 저장을 하고 있는 것이다.
메쉬에서 저장이 된 형태라 함은 로컬을 기준으로 하는 좌표계에서 말하는게 아니라
어떠한 하나의 노드 기반으로 했을 때 그 공간에서의 물체의 좌표가 되는 것이다.
예를 들어 탱크의 포신에서 vertex 정보는 어디를 기준으로 하는 정점일까?
Convert::ReadMeshData에서
for (uint32 v = 0; v < srcMesh->mNumVertices; v++)
{
// Vertex
VertexType vertex; // VertexTextureNormalTangentBlendData에 필요한 내용을 채워준다
::memcpy(&vertex.position, &srcMesh->mVertices[v], sizeof(Vec3));
여기서 말하는 Vertex 포지션은 탱크의 포신에 해당하는 정보를 그린다고 했을 때 누구를 기준으로하는 좌표계 에서의 정점이냐가 중요한 것이다.
vertex란 물체의 기하학적인 모형을 표현하기 위한 것이다.
1번. 자신을 기준으로 하는 좌표다. 2번. 직속 상관을 기준으로 하는 좌표다. 3번. 루트를 기준으로 하는 좌표다.
이중에 무엇인지 이거를 잘 고민해 봐야 한다.
Vertex의 정보를 긁어 모아서 Mesh라는 걸 만들어서 그 다음에 VS의 input으로 넣어주는 것이다. 사실상 Convert::ReadMeshData에서 넣어준 정보들이 쉐이더의 VS의 인자로 들어가고 있는 것이다.
VertexShader에 넣어주는 좌표가 원래는 어떤 좌표였을까?
계층 구조가 없었을 때, 월드를 넣어준 것이 아니다. 로컬 좌표였다. 그걸 변환해서 월드 좌표로 변환 시킨 것이다. 그 다음에 vp를 곱한 것이다.
결국 input은 1번. 자신을 기준으로 한 좌표다.
나중에 어디에 배치가 될텐데 그거에 따라 World 행렬이 만들어 질 것이고
View행렬, Projection 행렬이 만들어진다.
좌표계와 어떤 좌표계를 중심으로 한 정점인지를 동시에 생각을 해야 하는데
처음에 넣어준 물체들의 좌표는 로컬을 기준으로 하는 좌표계에서의 좌표였기 때문에 그거를 월드 좌표계로 변환 시키기 위해서는 각각의 좌표에다가 World행렬을 곱해주면 이 물체를 기준으로 좌표가 아니라 월드를 기준으로 하는 좌표로 변환이 되는 것이다.
View 좌표계는 카메라가 어딘가에서 찍고 있으면 카메라를 기준으로 다시 변환을 하는게 View space 변환이었고,
Projetion은 이걸 투영 시켜가지고 원하는 가두리 양식장 모양의 네모에 들어가게 하는게 projection의 목적이었다.
이 작업을 계속 하고 있는 것이다. 이걸 헷갈리면 안된다.
다시 한번 반복해 보면
Convert::ReadMeshData의
for (uint32 v = 0; v < srcMesh->mNumVertices; v++)
{
// Vertex
VertexType vertex; // VertexTextureNormalTangentBlendData에 필요한 내용을 채워준다
::memcpy(&vertex.position, &srcMesh->mVertices[v], sizeof(Vec3));
여기서 정점들을 받고 있다.
여기서 받는 정점들이 무슨 좌표계를 기준으로 하는 정점들이냐를 다시 생각해 봐야 한다.
로컬은 로컬인데, 루트는 최상위 노드의 기준점이고, 그 다음에 하나의 물체에서의 좌표인지, 계층 구조에 있어서 루트를 기준으로 하는 좌표인지, 아니면 포신만 생각해서 자신을 기준으로 하는 좌표인지 생각해야 한다.
1번. 루트
2번. 직속상관
3번. 자신
결국 여기서 받고 있는 vertex 정보는 무엇을 기준으로 하는 것이냐를 묻고 있는 것이다.
정답은 3번 자신이다.
이게 중요하다.
그래서 Convert::ReadMeshData에서 정점을 받으면 모형이 나온다.
여기는 계층 구조를 생각할 필요가 없다. 자신만 생각했을 때 이 좌표계에서의 모형을 나타내는 게 목적이다.
이걸 이해하고 넘어가야 한다.
그래서 자신을 기준으로 하는 모형은 나올 것이다. 문제는 이런 게 여러개 뭉쳐있는게 탱크였다.
노드의 계층 구조에 따라 무엇은 상대적으로 어디에 위치해 있고가 정해진 것이다.
그럼 애니메이션 때 이해해야 하는 것은,
VS에 넘겨줄 때 그 좌표는 어디까지나 자신을 기준으로 하는 좌표라는 것이다.
계층 구조가 있다고 하면, 원위치에 그리는 게 아니라 얘네들끼리 상관 관계가 있어서 계층 구조가 표현이 된다고 하면, 자신을 기준으로 하는 좌표계의 좌표를 먼저 구해야 하는게 아니라 루트를 기준으로 하는 좌표로 먼저 변환을 해줘야 한다. 그거를 어떻게 해야 할까?
루트로 가는 행렬을 한번 곱해주거나, 루트까지 부모의 행렬을 계속 재귀적으로 곱하거나 둘 중 하나를 하면 된다.
Converter::ReadModelData에서
// Relative Transform
Matrix transform(node->mTransformation[0]); // Matrix 생성자 중에서 주소 하나를 받는 버전
bone->transform = transform.Transpose(); // fbx 포멧에서는 순서가 뒤바뀌어 있기 때문에 뒤집어 줘야 한다.
// 2) Root(Local)
Matrix matParent = Matrix::Identity;
if (parent >= 0) // index의 부모가 있다면
matParent = _bones[parent]->transform; // 부모부터 root까지 한번에 계산한 거
// Local (Root) Transform
bone->transform = bone->transform * matParent;
이 작업이 무엇을 구하는 것이었을까?
이것은 자신의 직속 부모로 가는 행렬일까, 루트까지 가는 행렬일까
루트까지 한번에 순간이동 하는 행렬을 구해 준 것이다.
이렇게 안해 놨으면 계층이 100개면 100번을 해야 한다.
이걸 들고만 있었지 아직 사용하지 않았다. 방금 VS에서 사용한 것이다.
왜 VS에서
output.position = mul(input.position, BoneTransforms[BoneIndex]);
이 코드가 빠져있었을 때 모든 파츠가 가운데 모여 있었느냐, 결국 이 계층 구조를 표현한 것을 무시했기 때문이었다.
모양 자체를 원점에 그대로 있는 거 마냥 표현해줬기 때문에 바퀴건 포신이건 다 동일하게 포개졌던 거였고, 원래 fbx 포멧에서 얘기했던 계층 구조를 잘 지키면 object 끼리 계층 구조가 있고, 상대적으로 직속상관과 부모를 기준으로 어디에 있어요 라는 모든 그런 것이 있었고, 그걸 정제해서 한번에 루트까지 가는 것을 행렬로 만들어서
// Local (Root) Transform
bone->transform = bone->transform * matParent;
이걸로 저장해 주고 있었기 때문에 몇 번째 아인지만 알면 그걸 인덱스로 접근을 해서 한 번에 루트로 가는 변환 행렬을 얻을 수 있는 거고, 그걸 VS에서 곱해준 것이다.
루트로 변하는 변환 행렬을 곱해주게 되면 나오는 포지션 좌표는 무슨계를 기준으로 하는 좌표가 될까?
output.position = mul(input.position, BoneTransforms[BoneIndex]);
요 아이를 곱해주게 되면
1번. 루트 좌표계를 기준으로 하는 좌표가 나오게 된다.
그 루트를 기준으로 한 좌표계를 기준으로 W, VP을 곱해주면, 모든 물체들이 원래 루트를 기준으로 한 좌표에 배치가 되니까, 탱크 모양이 나왔다고 볼 수 있는 것이다.
애니메이션을 할 때도 비슷한 문제가 일어날 것이다.
캐릭터의 경우도 보통은 허리를 기준으로 잡는데 그거에 따라 계층 구조가 만들어져가지고, 물체가 만들어 진다는 것이다.
그래서 VS에서
output.position = mul(input.position, BoneTransforms[BoneIndex]);
이렇게 곱해줬을 때 정상적으로 뜬 것이라 라고 볼 수 있는 것이다.
하나의 퀴즈를 더 내보자면,
쉐이더의
cbuffer BoneBuffer
{
matrix BoneTransforms[MAX_MODEL_TRANSFORMS];
};
여기다가 모든 뼈대의 정보를 넣어준 다음에
uint BoneIndex;
를 넘겨서
VS에서 구해줬는데
output.position = mul(input.position, BoneTransforms[BoneIndex]);
애당초,
matrix ToRootTransform;
이런걸 하나 만들어 줘가지고, 이걸 왜 막바로 하지 않았을까?
이건 아직 답을 모르는게 당연하다.
ModelRenderer::Update에서
BoneDesc boneDesc;
const uint32 boneCount = _model->GetBoneCount();
for (uint32 i = 0; i < boneCount; i++)
{
shared_ptr<ModelBone> bone = _model->GetBoneByIndex(i);
boneDesc.transform[i] = bone->transform;
}
RENDER->PushBoneData(boneDesc);
BoneData를 넣어주고,
// BoneIndex
_shader->GetScalar("BoneIndex")->SetInt(mesh->boneIndex);
그 본데이터에서 내가 몇번째 인덱스인지를 이렇게 두번에 걸쳐서 했는데
가만히 생각해보면 CPU쪽에서 몇 번째인지 알면, 애당초 matrix ToRootTransform; 이걸 그냥 구해서 바로 넘겨주면 되지 않을까 하는 생각이 들 수 있다.
하지만 왜 지금처럼 하고 있는 것이지 답을 모르는게 당연하다.
정답은 그렇게 해도 된다.
그럴 수 있지만,
지금처럼 온갖 정보를 다 밀어 넣고, BoneIndex를 골라서 하는 실습을 한 이유는 애니메이션에서 이런 작업을 해볼 것이기 떄문에 연습을 한 것이다.
애니메이션이 들어가게 되면, CPU에서 나는 누구고 Bone으로 할거에요 하는게 아니라,
cbuffer BoneBuffer
{
matrix BoneTransforms[MAX_MODEL_TRANSFORMS];
};
이 상태 중에서
여러개를 골라서 혼합해서 만들어 줘야 한다.
애니메이션은 결국 허리 움직이면 어깨도 움직이는 개념이기 때문에
여기서 온갖 변환과 관련된 정보를 들고 있는 상태에서 내가 누군지 알면 그거와 연관된 뼈대를 골라서 그거에 대한 블렌딩 연산을 해줘가지고, 좌표를 최종적으로 골라줘야 한다.
오늘 한 게 1단계고
나중에 가면 조금 더 복잡하게 만들어 줘야 하는 것이고,
그래서 VertexData를 만들 떄
struct VertexTextureNormalTangentBlendData
{
Vec3 position = { 0, 0, 0 };
Vec2 uv = { 0, 0 };
Vec3 normal = { 0, 0, 0 };
Vec3 tangent = { 0, 0, 0 };
Vec4 blendIndices = { 0, 0, 0, 0 };
Vec4 blendWeights = { 0, 0, 0, 0 };
};
blendIndices , blendWeights 정보가 들어가게 된 것이다.
몇 번째 뼈대랑 얼마만큼 비율로 섞어서 최종 결과를 그려줄 것인가를 여기에 저장해준다고 보면 된다.
그게 애니메이션의 원리다.
기본적으로 BoneTransforms에 뼈대와 관련된 온갖 정보를 밀어 넣고, BoneIndex로 골라서 쏙쏙 빼먹고 있다.
하지만, input이 월드 좌표라 이해하고 있었다고 하면 빨리 리뉴얼 해야 한다. 렌더링 기초가 안잡힌 것이다.
들어올 때는 물체의 로컬이고, 월드를 곱하고 VP를 곱해서 클립 스페이스가 적용이 되었고,
클립 스페이스가 적용이 되었을 때는 최종적으로 깊이 나누기가 안들어간 상태인데 그게 그걸 중간에 rasterizer 단계에서 해줘가지고, z나누기가 되고,
PS로 넘어가면 2D로 넘어간다.
그런걸 완벽하게 이해해야 한다.
VS의
output.position = mul(input.position, BoneTransforms[BoneIndex]);
이 부분이 중요한 것이고,
Converter::ReadModelData에서
// Relative Transform
Matrix transform(node->mTransformation[0]); // Matrix 생성자 중에서 주소 하나를 받는 버전
bone->transform = transform.Transpose(); // fbx 포멧에서는 순서가 뒤바뀌어 있기 때문에 뒤집어 줘야 한다.
// 2) Root(Local)
Matrix matParent = Matrix::Identity;
if (parent >= 0) // index의 부모가 있다면
matParent = _bones[parent]->transform; // 부모부터 root까지 한번에 계산한 거
// Local (Root) Transform
bone->transform = bone->transform * matParent;
여기서 하는게 좌표계 변환,
정확하게 말하면, 변환 행렬을 구하는 것이지 좌표를 구하는게 아니다.
실제로 어떠한 기준으로 봤을 때의 각각 정점들의 좌표는
Converter::ReadMeshData의
for (uint32 v = 0; v < srcMesh->mNumVertices; v++)
{
// Vertex
VertexType vertex; // VertexTextureNormalTangentBlendData에 필요한 내용을 채워준다
::memcpy(&vertex.position, &srcMesh->mVertices[v], sizeof(Vec3));
vertex와 관련된 부분에서 추출하고 있는 것이다.
이 부분을 이해해야 한다.
Vertex는 나를 기준으로 했을 떄 물체가 어떻게 생겼는지 기하학적 도형을 나타내는 것이고, 나를 기준으로 하는 좌표계인 것이다.
그리고 Converter::ReadModelData에
// Relative Transform
Matrix transform(node->mTransformation[0]); // Matrix 생성자 중에서 주소 하나를 받는 버전
bone->transform = transform.Transpose(); // fbx 포멧에서는 순서가 뒤바뀌어 있기 때문에 뒤집어 줘야 한다.
// 2) Root(Local)
Matrix matParent = Matrix::Identity;
if (parent >= 0) // index의 부모가 있다면
matParent = _bones[parent]->transform; // 부모부터 root까지 한번에 계산한 거
// Local (Root) Transform
bone->transform = bone->transform * matParent;
변환 행렬을 구해놨기 때문에
이걸 이용해서 언제든지 루트를 기준으로 하는 좌표계로 변환을 하는 것은 언제든지 가능하다.
하나하나 타고 올라가야 하는 것을 한번에 갈 수 있게 캐싱해주고 있다.
탱크를 출력하는 거 까지 해봤다.
여기까지 했으면 애니메이션을 적용할 준비가 끝났다.
'DirectX' 카테고리의 다른 글
| 58. 애니메이션_애니메이션 이론 (0) | 2024.03.02 |
|---|---|
| 57. 모델_ImGUI (0) | 2024.03.01 |
| 55. 모델_모델 띄우기 (0) | 2024.02.28 |
| 54. 모델_Bone, Mesh 로딩 (1) | 2024.02.23 |
| 53. 모델_Material 로딩 (0) | 2024.02.21 |
댓글