이어서 Material에 관련하여 WriteTexture를 마무리해본다.
1. Converter::WriteTexture 정의하기
필수까진 아니다. xml이 추출이 된 다음에 diffuse랑 specular 파일 넣어주고, 그거에 따라서 실질적으로 로딩을 하게 할 수 있다. 선생님이 포폴 만들 때 썼던 방법 중 하나는 대부분의 물체들이 텍스쳐가 있을 것이기 때문에 House.xml로 material을 만들었으면 House_diffuse랑 House_normal 같은 애들로 이름을 지정해서 컨벤션에 맞게끔 저장하는 방법도 사용했었다. 이렇게 하면 작업하는 시간을 벌 수 있다.
그게 아니라 넣어준 이름 그대로 사용하고 싶다면
<DiffuseFile></DiffuseFile>
<SpecularFile></SpecularFile>
<NormalFile></NormalFile>
이 부분을 채워주는 노가다가 필요하긴 하다.
노가다를 조금이라도 줄이기 위해 파일을 로드해서 fbx 파일 자체에서 텍스쳐를 어떤 텍스쳐를 사용할지 적어둔 경우가 꽤 있기 때문에 그걸 최대한 활용하기 위해 Converter::WriteMaterialData에서
element = document->NewElement("DiffuseFile");
element->SetText(WriteTexture(folder, material->diffuseFile).c_str());
node->LinkEndChild(element);
element = document->NewElement("SpecularFile");
element->SetText(WriteTexture(folder, material->specularFile).c_str());
node->LinkEndChild(element);
element = document->NewElement("NormalFile");
element->SetText(WriteTexture(folder, material->normalFile).c_str());
node->LinkEndChild(element)
string diffuseFile을 받아올 수 있으면 그 받아온 거를 이용해 가지고 WriteTexture에서 실제로 텍스쳐 경로를 설정하고 복사까지 하는 것을 첫 번째 목표로 하고 있다.
이제부터는 무엇이 편할지 선택을 하는 거다.
이 리소스 파일에서 예를 들면
Resources\Assets\House\cottage_textures에
이렇게 diffuse랑 normal이 들어가 있고,
경우에 따라 Tower의 경우도
Resources\Assets\Tower의 Tower.fbx를 로드하면
Resources\Assets\Tower\textures에
이 아이들이 있다고 fbx파일 자체에서 들고 있을 것이다.
Resources\Assets\Tower\textures 폴더에서 이 아이 둘을 끄집어내서
Resources\Textures에 새로운 폴더를 만들어 가지고 거기다가 복사하는 그런 과정들을 만들어 놓으면 그나마 편리하게 작업을 할 수가 있을 것이다.
그런 부분들을 채워두는 것을 권장을 드린다.
WriteTexture는 얼마든지 본인이 정책을 정해서 작업을 하면 되는 거라 크게 중요한 부분은 아니니 간단하게만 해본다.
// 경로에 모아 놓는 함수
string Converter::WriteTexture(string saveFolder, string file)
{
string fileName = filesystem::path(file).filename().string();
string folderName = filesystem::path(saveFolder).filename().string();
const aiTexture* srcTexture = _scene->GetEmbeddedTexture(file.c_str());
// 에셋 가끔 살펴보면 실제로 fbx파일에 포함이 되어서 텍스쳐가 들어가 있는 경우가 간혹 있다.
// 어지간해서는 텍스쳐 따로 있는 버전을 사용하는 걸 조금 더 권장한다. 이 편이 관리가 편하기 때문이다.
// 어떤 건 안에 들어있고 어떤건 안들어가 있고 하면 귀찮다.
// 경우에 따라서는 그런 경우에는 메모리 안에 있던 걸 끄집어 내서 별도의 파일로 만드는 경우도 더러 있다.
// 코드를 사용할지는 모르겠지만 일단 그렇게 하는 방식으로 넣어 놓고 분석은 해본다.
if (srcTexture) // fbx에 텍스쳐가 들어가 있는 경우
{
string pathStr = saveFolder + fileName;
if (srcTexture->mHeight == 0) // 데이터가 1차원 배열 형태로 저장되어 있다면 바이너리 모드로 만든다.
{
shared_ptr<FileUtils> file = make_shared<FileUtils>();
file->Open(Utils::ToWString(pathStr), FileMode::Write);
file->Write(srcTexture->pcData, srcTexture->mWidth);
}
else // fbx에 1차원 배열이 아닌 2차원 텍스쳐가 있다면 내부에 있는 걸 끄집어 내서 별도의 파일로 만드는 경우
{
D3D11_TEXTURE2D_DESC desc;
ZeroMemory(&desc, sizeof(D3D11_TEXTURE2D_DESC));
desc.Width = srcTexture->mWidth;
desc.Height = srcTexture->mHeight;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
desc.SampleDesc.Count = 1;
desc.SampleDesc.Quality = 0;
desc.Usage = D3D11_USAGE_IMMUTABLE;
D3D11_SUBRESOURCE_DATA subResource = { 0 };
subResource.pSysMem = srcTexture->pcData;
ComPtr<ID3D11Texture2D> texture;
HRESULT hr = DEVICE->CreateTexture2D(&desc, &subResource, texture.GetAddressOf());
CHECK(hr);
DirectX::ScratchImage img;
::CaptureTexture(DEVICE.Get(), DC.Get(), texture.Get(), img);
// Save To File
hr = DirectX::SaveToDDSFile(*img.GetImages(), DirectX::DDS_FLAGS_NONE, Utils::ToWString(fileName).c_str());
CHECK(hr);
}
}
else // fbx에 텍스쳐가 안들어가 있는 경우
{
string originStr = (filesystem::path(_assetPath) / folderName / file).string();
Utils::Replace(originStr, "\\", "/");
string pathStr = (filesystem::path(saveFolder) / fileName).string();
Utils::Replace(pathStr, "\\", "/");
::CopyFileA(originStr.c_str(), pathStr.c_str(), false);
}
return fileName;
}
FileUtils라는 게 생겼는데
소위 바이너리 파일을 만드는 유틸리티를 추가했다. 그게 먼저 진행이 되어야 하는데
나중에 진행할 것이니 일단은
shared_ptr<FileUtils> file = make_shared<FileUtils>();
file->Open(Utils::ToWString(pathStr), FileMode::Write);
file->Write(srcTexture->pcData, srcTexture->mWidth);
이 부분은 주석처리를 한다.
if(srcTexture) 이 부분 fbx에 텍스쳐가 들어간 경우의 사용을 권장하진 않는다.
선생님 만든 포폴에서는 대부분 외장으로 따로 텍스쳐들이 있었어서 그걸 사용하는 게 좋다.
그래도 이런 코드들이 있길래 보여드린 것이고
가장 핵심은 일반적으로 봤을 때 텍스쳐가 내장이 되어 있지 않고 빠져 있을 때는
else
{
string originStr = (filesystem::path(_assetPath) / folderName / file).string();
Utils::Replace(originStr, "\\\\", "/");
string pathStr = (filesystem::path(saveFolder) / fileName).string();
Utils::Replace(pathStr, "\\\\", "/");
::CopyFileA(originStr.c_str(), pathStr.c_str(), false);
}
return fileName;
}
원래 오리진에 있던 그 파일을 긁어 와서 그거를 다른 경로로 데리고 가는 게 두 번째 목표다.
예를 들어 Resources\Textures 산하에 최종적으로 만드는 게 목표이긴 한데
코드가 실행이 될 때 즈음에는 Resources/Asset/Tower에 Tower.fbx를 가지고 작업하고 있다면, 여기 있는 material을 갖고 오면서 Resources/Asset/Tower/textures에 texture가 있으니 이 texture 들을 복붙 해가지고,
Resources/Textures산하에 Tower라는 폴더를 만들어서 여기다 복사해 달라가 목적이다.
그래야 Resources/Textures/Tower에 wood라는 material이 만들어지게 되면 나머지 세트들도 여기에 들어가게 되고 노가다하는 작업을 그나마 줄일 수 있다.
그런 작업을 해주면 된다.
filesystem을 이용하면 file경로끼리 추가하는 걸 쉽게 할 수 있다. filesystem 상에서 /라는 오퍼레이터를 지원하는데 그걸 이용해서 덧붙일 수가 있다. 이건 문자열이 아니라 하나의 경로라고 보면 된다.
Utils::Replace 함수는 아직 안 만들었는데 여러 가지 Util 함수가 추가될 필요가 있다.
Utils.h에 가서
static void Replace(OUT string& str, string comp, string rep);
static void Replace(OUT wstring& str, wstring comp, wstring rep);
이렇게 두 개를 넣어준 다음에 cpp에 구현을 해 놓으면 편리하게 사용할 수 있다.
구현부는 인터넷에 찾아보면 나온다. C++문자열은 이상하게 되어 있어서 굳이 외울 필요는 없다.
void Utils::Replace(OUT string& str, string comp, string rep)
{
string temp = str;
size_t start_pos = 0;
while ((start_pos = temp.find(comp, start_pos)) != wstring::npos)
{
temp.replace(start_pos, comp.length(), rep);
start_pos += rep.length();
}
str = temp;
}
void Utils::Replace(OUT wstring& str, wstring comp, wstring rep)
{
wstring temp = str;
size_t start_pos = 0;
while ((start_pos = temp.find(comp, start_pos)) != wstring::npos)
{
temp.replace(start_pos, comp.length(), rep);
start_pos += rep.length();
}
str = temp;
}
Engine을 빌드한다.
이걸 이용해서 Converter를 계속 만들면 된다.
else
{
string originStr = (filesystem::path(_assetPath) / folderName / file).string();
Utils::Replace(originStr, "\\\\", "/");
string pathStr = (filesystem::path(saveFolder) / fileName).string();
Utils::Replace(pathStr, "\\\\", "/");
::CopyFileA(originStr.c_str(), pathStr.c_str(), false);
}
return fileName;
}
원본 경로와 넣어줄 경로를 넣고, WinApi의 CopyFile을 이용해서 원래 경로에 있던 파일을 원하는 경로로 복사해 주세요가 실행이 되면 된다.
리턴할 때는 결론으로 찾은 filename을 뱉어주면 된다.
난장판이긴 한데 포폴을 만들 때 정책에 따라가지고 여러 가지 코드를 넣어줘서 그거에 맞춰서 작업을 하면 된다.
AssimpTool::Init에서 House뿐만 아니라 Tower도 추가해 준다.
{
shared_ptr<Converter> converter = make_shared<Converter>();
// FBX -> Memory
converter->ReadAssetFile(L"Tower/Tower.fbx");
// Memory -> CustomData (File)
// 1차 목표
converter->ExportMaterialData(L"Tower/Tower");
converter->ExportModelData(L"Tower/Tower");
// CustomData (File) -> Memory
}
Tower도 이런 식으로 Material과 Model을 각각 추출해서 만들어줄 것이고,
아직 Material만 되어 있고, Model은 파싱이 되어 있지 않지만,
이렇게 해서 실행하면 여러 개를 나중에 툴로 만들어서 원하는 것만 집어서 하면 되게 만들어 줄 것이다.
AssimpTool을 빌드한다.
실행을 하면 정상적으로 완료가 됐다고 하면
Resources\Textures에 Tower 폴더가 있고, 그 안에
Wood_Tower_Col과 Wood_Tower_Nor파일이 자동으로 긁어 온 것을 볼 수 있다.
이것을 하기 위해 고생을 한 것이다.
경로가 마음에 안 든다면 다른 경로에 맞춰서 작업을 해주면 된다.
WriteTexture는 얼마든지 수정을 해서 작업을 하면 된다가 결론이다.
여기까지는 지난 시간에 이어서 Material에 관련된 내용이다.
Tower.xml을 열어보면
<?xml version="1.0" encoding="UTF-8"?>
<Materials>
<Material>
<Name>watchtower</Name>
<DiffuseFile>Wood_Tower_Col.jpg</DiffuseFile>
<SpecularFile>Wood_Tower_Col.jpg</SpecularFile>
<NormalFile>Wood_Tower_Nor.jpg</NormalFile>
<Ambient R="0" G="0" B="0" A="1"/>
<Diffuse R="0.39738566" G="0.39738566" B="0.39738566" A="1"/>
<Specular R="1" G="1" B="1" A="0"/>
<Emissive R="0" G="0" B="0" A="1"/>
</Material>
</Materials>
Wood_Tower_Col, Wood_Tower_Nor을 어떻게 만들어야 되는지에 대한 정보가 들어가 있다 보니까 그걸 이용해서 Diffuse, Specular, Normal 파일이 있으니까 Tower.xml과 같은 폴더에 있을 거라는 걸 가정을 해서 나중에 작업을 해주면 된다.
이제 mesh를 로드할 때도 수월할 것이다.
지금까지는 client 보면 image 따로 로드해서 그거를 diffuseMap에 넣어주는 식으로 작업을 했는데 그거를 조금 더 편리하게 작업할 수 있게 되었다.
2. ExportModelData
Material은 맛보기였다.
핵심은 ExportModelData를 했을 때
void Converter::ExportModelData(wstring savePath)
{
wstring finalPath = _modelPath + savePath + L".mesh"; // .data, .model 로 해도 상관 없다. 하나 골라서 사용하면 된다.
ReadModelData(_scene->mRootNode, -1, -1);
WriteModelFile(finalPath);
}
이 아이들이 핵심이다.
ReadModelData를 실행할 때
가장 중요한 건 첫 번째도 두 번째도 계층구조였다.
계층 구조를 파싱 하는 게 중요했고, 계층 구조를 파싱 하기 위해 맨 상위 -1, -1부터 작업을 한다고 볼 수 있고,
트리와 관련된 알고리즘 문제를 많이 풀어봤다면 익숙한 코드를 보게 될 것이다.
ReadModelData라 해서 노드들을 하나씩 순회를 하면서 읽어서 파싱 하는 준비를 끝내려고 한다.
여기에서 왼쪽을 분석을 하는데
분석을 하다가 추가적으로 정보가 있으면 파싱을 할 것이다.
중요한 건 계층 구조에 따라 각기의 transform이 상대적으로 내 부모를 기준으로 되어 있다는 게 중요하다.
겸사겸사 한 번에 만든다. 지나가는 김에 _bones와 관련된 부분을 주으면서 갈 것이다. Bone이라 함은 뼈대와 관련된 정보를 박아서 그 정보를 이용해 조립을 할 텐데 지금은 사실상 노드 하나하나가 뼈대라 가정을 하고 조립을 하게 될 것이다.
나중에 애니메이션을 적용하면 스켈레톤 구조가 맞지만 오늘 할 때는 뼈대가 아니라 모델의 계층 구조만 표현을 할 것이다. 이번 시간엔 스테틱 메쉬만 할 것이다. 나중에 가면 이게 사실상 스켈레톤 구조를 얘기하게 될 것이다.
1) ReadModelData 구현하기
ReadModelData에서
// Relative Transform
Matrix transform(node->mTransformation[0]); // Matrix 생성자 중에서 주소 하나를 받는 버전
// 4x4 16개 숫자를 알아서 복사해준다.
bone->transform = transform.Transpose(); // fbx 포멧에서는 순서가 뒤바뀌어 있기 때문에 뒤집어 줘야 한다.
// transform이 무엇을 기준으로 되어야 하는 것이냐? 최종 루트가 아닌 직속상관(부모)를 기준으로 하는 좌표를 나타낸다.
// 이거를 변환해서 최종루트를 기준으로 하는 것으로 변신이 하고 싶다면 정제를 해줄 필요가 있다.
// 여기서 matParent는 무엇을 기준으로 하는 transform일까?
// Root(Local) 까지 가야 할 경로를 미리 계산한 것이다
Matrix matParent = Matrix::Identity;
if (parent >= 0) // index의 부모가 있다면
matParent = _bones[parent]->transform; // 부모부터 root까지 한번에 계산한 거
// Local (Root) Transform
// 부모를 하나씩 타고 가면서 다 곱해주면 된다.
bone->transform = bone->transform * matParent;
이 부분이 가장 핵심을 담고 있다.
이걸 복습하면서 이해한다면 다음 주 애니메이션도 수월하게 할 수 있을 것이고 이해가 안 가면 애니메이션도 이해가 안 갈 것이다.
루트를 기준으로 하는 좌표계까지 넘어가고 싶다면 점프를 여러 번 해서 가야 한다.
왜 여기서는 한 번만 하면 됐을까? 루트부터 child로 거꾸로 타고 타고 내려가고 있었기 때문에 가능한 것이다.
그래서 이미 계산이 된 _bones[parent]에 들어가 있는 부모들은 자기가 들고 있는 transform 자체가 애당초 root를 기준으로 하는 트랜스폼으로 다 바꿔놓은 상태이기 때문에 새로 찾은 애만 다시 한번 변환을 해줘서 걔도 역시
bone->transform = bone->transform * matParent;
이렇게 넣어주면 된다는 거다.
void Converter::ReadModelData(aiNode* node, int32 index, int32 parent)
{
shared_ptr<asBone> bone = make_shared<asBone>();
bone->index = index; // index는 파싱한 순서라고 보면 된다. -1부터 child로 갈 때 마다 번호 증가
bone->parent = parent;
bone->name = node->mName.C_Str();
// Relative Transform
Matrix transform(node->mTransformation[0]); // Matrix 생성자 중에서 주소 하나를 받는 버전
// 4x4 16개 숫자를 알아서 복사해준다.
bone->transform = transform.Transpose(); // fbx 포멧에서는 순서가 뒤바뀌어 있기 때문에 뒤집어 줘야 한다.
// transform이 무엇을 기준으로 되어야 하는 것이냐? 최종 루트가 아닌 직속상관(부모)를 기준으로 하는 좌표를 나타낸다.
// 이거를 변환해서 최종루트를 기준으로 하는 것으로 변신이 하고 싶다면 정제를 해줄 필요가 있다.
// 여기서 matParent는 무엇을 기준으로 하는 transform일까?
// Root(Local) 까지 가야 할 경로를 미리 계산한 것이다
Matrix matParent = Matrix::Identity;
if (parent >= 0) // index의 부모가 있다면
matParent = _bones[parent]->transform; // 부모부터 root까지 한번에 계산한 거
// Local (Root) Transform
// 부모를 하나씩 타고 가면서 다 곱해주면 된다.
bone->transform = bone->transform * matParent;
_bones.push_back(bone);
// 새로 구한 신참이 들고 있는 transform은 relative로 들고 있는게 아니라
// 최상위 루트를 기준으로 하는 최종 물체의 transform을 구해 준 다음에 넣어준 것이다.
// 신참도 자식이 있었다고 하면 자식코드도 다시 실행이 되면서 자식 또한 Relative로 찾을 것인데
// 그걸 다시 이미 구해준 한번에 root로 가는 transform을 곱해줘서 걔도 한번에 빠져나갈 수 있게 한 것이다.
// 여기까지 하나의 노드에 대해 처리
// Mesh
ReadMeshData(node, index);
// 재귀적인 코드
for (uint32 i = 0; i < node->mNumChildren; i++)
ReadModelData(node->mChildren[i], _bones.size(), index);
// parent에 자신의 인덱스를 넣고,
// 새로운 아이의 번호는 _bones.size()로 말 그대로 bones가 늘어날 때 마다 맨 마지막에 들어갈 자리가 자신의 id라고 볼 수 있다.
}
이게 오늘의 핵심이라고 보면 된다.
이 계층 구조를 파싱 하는 부분과
그다음에 transform을 relative로 그대로 들고 있는 게 아니라 변신을 해서 최상위로 나갈 수 있는 것을 준비해 준다가 중요하다.
이렇게 해서 bone과 관련된 정보 다 추출이 될 것이다.
2) ReadModelData에서 호출되는 ReadMeshData 구현하기
mesh와 관련된 정보도 사실은 따로 ReadModelData와 독립적으로 ReadMeshData로 따로 호출해줘도 되는데 샘플 코드 중에서 괜찮았던 게 ReadMeshData를 ReadModelData안에서 호출하는 경우가 있었다.
// Mesh
ReadMeshData(node, index);
합리적인 이유가 있는데 실제로 노드라는 게 뼈대라곤 하지만 진짜로 스켈레톤에 들어가 있는 모든 뼈대들 용도로만 활용하는 게 아니라 유니티에서 봤다시피 어떤 뼈에는 라이팅 정보가 들어가 있다 거나 여러 용도로도 사용될 수 있다.
하나의 노드는 결국에는 그냥 데이터를 저장하는 단위이기 때문에 가끔가다가 그런 애들이 들어간 경우가 있고, 물론 엉뚱한 데이터는 뼈대 흐름에 넣는 게 아니라 최상위 루트에 꽂아서 메쉬를 표현하는 용도로 사용하긴 해야겠지만 어찌 됐던 노드를 파싱 하다 보면 그 아이까지 걸리게 될 것이다.
그러기 때문에 굳이 따로 하기보다는 노드를 파싱함과 동시에 Mesh도 같이 하기로 한다.
void Converter::ReadMeshData(aiNode* node, int32 bone)
{
if (node->mNumMeshes < 1)
return;
// mesh 카운트가 0이 아니라 하나라도 있다면 마지막 노드는 메쉬 정보를 나타내기 위해서 사용하는 것이다라는 걸 알 수 있다.
// 예를 들면 House를 추출하다 계층구조가 있는데 갑자기 한 노드에서는 Cube라는 Mesh를 들고 있으면 Mesh로 활용되거나 작동을 한다.
// 파싱을 하다 보면 Mesh와 관련된 정보가 하나 뜰텐데 그거를 파싱하는 코드를 여기에 넣어주면 된다.
shared_ptr<asMesh> mesh = make_shared<asMesh>();
mesh->name = node->mName.C_Str();
mesh->boneIndex = bone; // 나중에 따로 연결해주는 거 보다 여기서 흐름별로 가면서 여기서 끼워주는 것도 좋을 방법이다.
// mesh가 하나만 있는 경우도 있겠지만 경우에 따라서 여러개 있는 경우가 있다.
// dragon 에셋도 파싱할 때 보면 submeshes라고 메쉬가 3개짜리로 만들어져 있는 것을 볼 수 있다.
// 3개로 관리할지 묶어서 관리할지 선택을 해야 하는데 한번에 그려줘야 한다면 indexBuffer를 만들어서 해야 하는데
// 따로 관리하기 보다는 이걸 합쳐서 하나로 만들어서 관리하는 것도 방법이다.
// fbx가 상상이상으로 복잡하게 되어 있기 때문에 오늘 작성한 코드가 모든 fbx에서 돌아갈지도 검증이 안된 것이다.
// submesh가 여러개 있을 수 있으니 하나씩 순회를 하는 코드를 넣어준다.
for (uint32 i = 0; i < node->mNumMeshes; i++)
{
uint32 index = node->mMeshes[i]; // 인덱스 번호가 실제로 scene의 mesh번호가 된다.
// 설계도를 보면 child node가 index를 갖고 있을건데 그 index가 Scene이 들고 있었던 mMeshes의 원본의 번호라 써있다.
// child node에서 mMesh가 0, 1, 3 이면 처음에 Scene에서 mMeshes가 들고 있었던 0, 1, 3번째에 있었던 그 메쉬 정보를 추출해서 갖고오면 된다.
// index번호를 이용해서 scene에서 들고 있던 mesh에 접근하는 것이라 결론은 내릴 수 있다.
const aiMesh* srcMesh = _scene->mMeshes[index];
// 다음은 원래 하던 거처럼 이름을 꺼내주고 하는 식으로 하면 된다.
// aiMesh* srcMesh 가 mesh를 그릴 때 어떤 material로 그려줘야 되는지 정보도 들고 있다.
// 어딘가에서 수동으로 해줬던 정보를 여기서 들고 있다. fbx에 기능이 많다는 거.
// Material Name
const aiMaterial* material = _scene->mMaterials[srcMesh->mMaterialIndex];
mesh->materialName = material->GetName().C_Str();
// asMesh의 string materialName을 알아야 이 이름을 찾아서 struct asMaterial의 name과 매핑을 시켜줄 수 있을 것이다.
// 이걸 이용해야지 나중에 어떤 메쉬를 그릴 때는 어떤 마테리얼을 이용할지에 대한 관계도가 그려진다.
// 내용이 복잡해지는게 지금까지는 Mesh하나에 Material 하나 짝을 이뤄서 거기다 MeshRenderer로 그려줬지만
// 이제는 하나의 물체에 Mesh가 여러개 될 수 있다는 거다. 그 Mesh마다 동일한 Material을 사용한다는 보장도 없다.
// 용이 10개짜리 Mesh로 이루어져 있는데 10개짜리 mesh마다 material이 다 다르다고 하면
// material 10개, mesh가 10개가 될 수 있고, 그거를 관리할 수 있어야 한다.
// 그렇기 때문에 관계도가 있다고 보면 된다.
const uint32 startVertex = mesh->vertices.size();
// submeshes를 뭉쳐서 관리해줄 때 통합을 위해 따로 계산해준다.
// 지금까지 정점의 번호가 몇 번 이었는지를 여기에 적어 놓고
// 실시간으로 추가해주고,
// 그 다음에 index 번호를 계산할 때도 + startVertex를 해서 오프셋 만큼을 뒤로 당겨서 관리하고 있다.
for (uint32 v = 0; v < srcMesh->mNumVertices; v++)
{
// Vertex
VertexType vertex; // VertexTextureNormalTangentBlendData에 필요한 내용을 채워준다
::memcpy(&vertex.position, &srcMesh->mVertices[v], sizeof(Vec3));
// UV
if (srcMesh->HasTextureCoords(0))
::memcpy(&vertex.uv, &srcMesh->mTextureCoords[0][v], sizeof(Vec2));
// Normal
if (srcMesh->HasNormals())
::memcpy(&vertex.normal, &srcMesh->mNormals[v], sizeof(Vec3));
// 오늘은 애니메이션은 들어가지 않기 때문에 필요한 것만 넣어서 vertices에 넣어준다.
mesh->vertices.push_back(vertex);
}
// Index
// 주의할 점은 submeshes를 3개짜리를 뭉쳐서 관리하다 보면 인덱스 번호가 겹치면 안된다.
// 인덱스 번호를 구분하면서 늘리면서 관리를 해줄 필요가 있다.
for (uint32 f = 0; f < srcMesh->mNumFaces; f++)
{
aiFace& face = srcMesh->mFaces[f];
for (uint32 k = 0; k < face.mNumIndices; k++)
mesh->indices.push_back(face.mIndices[k] + startVertex);
// +startVertex해서 index번호를 관리하고 있다. submeshes를 통합 관리할 때 index번호가 겹치지 않게 관리해 주는 것이다.
}
}
_meshes.push_back(mesh);
}
이렇게 만들어 줄 수 있다.
Submeshes의 인덱스 번호가 겹치면 안 된다.
이걸 통합해서 관리하려면 이 모든 정점들을 다 합친 다음에
인덱스 번호도 그것에 맞게끔 하나에 통합되게끔 겹치지 않게 만들어 줘야 하는데
그 부분이 startVertex로 들어간다고 보면 된다.
요약하면
Converter.h에
vector<shared_ptr<asMesh>> _meshes;
이 asMesh가 여러 개 있을 수 있는 게 끔찍하다.
보면 심지어 하나의 Mesh에도 여러 개의 submesh로 파싱하고 있으니까 그걸 조립해서 우리만의 정보로 이렇게 만드는 부분이 들어간다고 보면 된다.
Converter.h를 보면
vector<shared_ptr<asBone>> _bones;
vector<shared_ptr<asMesh>> _meshes;
vector<shared_ptr<asMaterial>> _materials;
여기까지만 했으면 _bones 계층구조와 관련된 부분이고,
_meshes는 vertex, index, material과 관련된, 어떻게 그려야 될지, 어떻게 연동해야 될지에 관련된 부분이고,
_materials는 지금까지 사용하던 material 그 자체, 어떠한 텍스쳐로 어떠한 라이팅 수치로 그려야 될지 등등을 파싱 해가지고 이렇게 세 가지로 들고 있게 된다.라고 보면 된다.
코드를 분석할 때는 헷갈리겠지만
ExportModelData에서 Model 데이터를 파싱 할 때는
ReadModelData부터 시작을 해서
재귀함수 부분을 유심히 봐야 하고,
// 재귀적인 코드
for (uint32 i = 0; i < node->mNumChildren; i++)
ReadModelData(node->mChildren[i], _bones.size(), index);
// parent에 자신의 인덱스를 넣고,
// 새로운 아이의 번호는 _bones.size()로 말 그대로 bones가 늘어날 때 마다 맨 마지막에 들어갈 자리가 자신의 id라고 볼 수 있다.
그다음에 중요했던 부분은 transform을 계산하는 부분이 중요하다.
// Relative Transform
Matrix transform(node->mTransformation[0]); // Matrix 생성자 중에서 주소 하나를 받는 버전
// 4x4 16개 숫자를 알아서 복사해준다.
bone->transform = transform.Transpose(); // fbx 포멧에서는 순서가 뒤바뀌어 있기 때문에 뒤집어 줘야 한다.
// transform이 무엇을 기준으로 되어야 하는 것이냐? 최종 루트가 아닌 직속상관(부모)를 기준으로 하는 좌표를 나타낸다.
// 이거를 변환해서 최종루트를 기준으로 하는 것으로 변신이 하고 싶다면 정제를 해줄 필요가 있다.
// 여기서 matParent는 무엇을 기준으로 하는 transform일까?
// Root(Local) 까지 가야 할 경로를 미리 계산한 것이다
Matrix matParent = Matrix::Identity;
if (parent >= 0) // index의 부모가 있다면
matParent = _bones[parent]->transform; // 부모부터 root까지 한번에 계산한 거
// Local (Root) Transform
// 부모를 하나씩 타고 가면서 다 곱해주면 된다.
bone->transform = bone->transform * matParent;
_bones.push_back(bone);
나중에 애니메이션을 다룰 때는 왔다 갔다 해야 할 필요가 있다.
Converter::ReadModelData의
Local (Root) Transform을 구해준 부분을 이용해
Root를 기준으로 하는 최종 로컬 transform을 그렸다가
다시 Relative Transform으로 바꾸던가 왔다 갔다 할 필요가 있기 때문에 기억을 해 둘 필요가 있다.
ReadModelData를 했으면,
WriteModelFile로 넘어가서 하나의 포맷을 만들어서 작업을 해야 한다.
Material과 다르게 정보가 워낙 많고, 보면서 수정할 것이 아니기 때문에
어지간해서는 그냥 바이너리 파일로 만들어서 읽을 수는 없지만 효율적인 방식으로 쭉 포멧을 정해서 적혀있는 그런 파일로 일단은 만들어서 관리를 하게 될 것이다.
바이너리 파일로 만드는 건 나중에 해보도록 하고
3. 실행하기
일단 여기까지 한 게 잘 실행이 되는지 테스트하자.
일단 AssimpTool::Init에 House를 주석처리 한다.
//{
// shared_ptr<Converter> converter = make_shared<Converter>();
// // FBX -> Memory
// converter->ReadAssetFile(L"House/House.fbx");
// // Memory -> CustomData (File)
// // 1차 목표
// converter->ExportMaterialData(L"House/House"); // xml 안붙이는 이유는 통일이 되어 있기 때문
// // Material이 필요 없으면 이 코드는 주석 처리하면 된다. Material만 하고 싶으면 얘만 놔두면 된다.
// // 읽은 데이터를 추출해서 우리만의 커스텀 데이터 파일로 만들어 주는게 목적이다.
// converter->ExportModelData(L"House/House");
// // CustomData (File) -> Memory
//}
그리고
void Converter::ExportModelData(wstring savePath)
{
wstring finalPath = _modelPath + savePath + L".mesh"; // .data, .model 로 해도 상관 없다. 하나 골라서 사용하면 된다.
ReadModelData(_scene->mRootNode, -1, -1);
WriteModelFile(finalPath);
}
WriteModelFile에 break point를 걸어서 실행해 본다.
const aiScene* _scene;
이게 원래 들고 있던 모든 정보들이고,
간단하게 되어 있었는데 이걸 파싱 해서 _bone에 들고 오게 된 거다.
이렇게 2개짜리의 노드로 되어 있고, 사실상 bone이 없는 것이다.
RootNode는 모든 잡동사니를 모아 놓는 공간이기에 사실 뼈대가 없다고 볼 수 있는 거다.
그다음에 _mesh를 보면
하나만 만들어져 있다.
_material도 보면
하나만 만들어져 있다.
깔끔하게 되어 있다는 걸 볼 수 있다.
ReadMeshData도 break point를 찍어서 살펴보면 정점이 몇 개 들어가 있는지 등등과 관련된 정보들을 살펴볼 수가 있을 것이다.
_meshes.push_back(mesh);
여기서 멈춰서
잘 들어갔나 보면 vertices는 4084, indices는 6108개 들어간 것을 볼 수 있다.
타워 하나도 정점이 4000개가 있는 것이다.
작은 물체인데도 4000개가 있었다. 조금만 더 복잡해지면 10만 개 단위를 넘어가는 게 전혀 이상한 게 아니다.
컬링이 중요하다. 여러 컬링을 다루겠지만 한번 걸러서 표현할 때 최대한 걸러줄 수 있으면 몇 십만 번의 연산을 gpu가 안 할 수 있으니까 큰 이점이 있다.
4. 맺음말
타워는 기본적으로 만들어져서 잘 들어가 있는 걸 확인했고
이걸 최종적으로 편리하게 관리할 수 있는 하나의 포맷을 파가지고 그것을
WriteModelFile에서 관리해 주면 되는데
Material은 xml 파일로 만들어서 관리했지만
이것은 Binary 형태로 만들어 줄 것이다.
차이는 binary는 읽는 걸 고려하지 않고 들어가다 보니까 xml처럼 복잡한 규칙성 있게 만들 필요가 없다.
그냥 데이터를 밀어 넣는 것이다.
중요한 건 꺼낼 때도 같은 순서로 꺼내야 한다.
온라인 게임에서 패킷 통신 하는 것과 같다. 시리얼라이즈 해서 데이터를 납작하게 해서 파일로 만든 다음에 파일을 다시 불러 읽을 때도 똑같이 사용할 수 있게 만들어 줘야지 의미가 있다고 볼 수 있다.
그걸 위해 파일을 바이너리 형태로 쓰고 읽는 Helper 클래스를 만들어 주고 그걸 기반으로 작업할 것이다.
서버에서 한 것과 비슷한 내용이다.
'DirectX' 카테고리의 다른 글
56. 모델_계층 구조 (0) | 2024.02.29 |
---|---|
55. 모델_모델 띄우기 (0) | 2024.02.28 |
53. 모델_Material 로딩 (0) | 2024.02.21 |
52. 모델_Assimp (0) | 2024.02.18 |
51. Light, Material_버그수정(카메라 좌표) (0) | 2024.02.17 |
댓글