-
[DirectX11] 모델 가져오기(Model Import)2024년 03월 09일
- 유니얼
-
작성자
-
2024.03.09.:52
728x90DirectX 11로 게임 엔진 아키텍처 만들기
3D 모델 가져오기는 게임 개발의 핵심적인 부분입니다. 복잡한 3D 씬을 구성하고 애니메이션을 적용하기 위해서는 다양한 모델 데이터를 게임 엔진에 통합해야 합니다. 이번 포스트에서는 DirectX11을 사용하여 게임 엔진으로 모델과 소재(Material) 정보를 가져오는 고급 기법을 살펴봅니다.
참고강의 링크:
모델 데이터 읽기
모델 데이터 읽기 과정은 3D 모델 파일을 해석하여 메쉬, 소재, 본(bone) 정보를 추출합니다. 이 정보는 게임 엔진 내에서 모델을 렌더링하는 데 필요한 기본 요소입니다.
예제: Asset 파일 읽기
Asset 파일에서 모델 데이터를 읽는 과정은 Assimp 라이브러리를 활용하여 진행됩니다. 아래 예제 코드는 Assimp를 사용하여 3D 모델 파일을 읽고 필요한 처리를 수행합니다.
// 주어진 파일 경로에서 에셋 파일을 읽어오는 함수 void Converter::ReadAssetFile(wstring file) { wstring fileStr = _assetPath + file; // 전체 파일 경로 생성 auto p = std::filesystem::path(fileStr); assert(std::filesystem::exists(p)); // 파일이 실제로 존재하는지 확인 // Assimp를 사용해 파일을 읽어오는 과정. 여러 가공 처리를 함께 수행 _scene = _importer->ReadFile( Utils::ToString(fileStr), aiProcess_ConvertToLeftHanded | // 좌표계 변환 aiProcess_Triangulate | // 모든 메시를 삼각형으로 변환 aiProcess_GenUVCoords | // UV 좌표 생성 aiProcess_GenNormals | // 법선 생성 aiProcess_CalcTangentSpace // 탄젠트 공간 계산 ); assert(_scene != nullptr); // 읽기 성공 확인 }
모델 데이터 내보내기
모델 데이터를 읽은 후, 게임 엔진에서 사용할 수 있는 형식으로 데이터를 내보냅니다. 이 과정에서는 읽어들인 메쉬, 소재, 본 정보를 게임 엔진의 데이터 구조에 맞게 변환하고 파일로 저장합니다.
예제: 모델 데이터 내보내기
모델 데이터를 .mesh 형식의 파일로 내보내는 과정은 다음과 같습니다.
// 모델 데이터를 내보내는 함수 void Converter::ExportModelData(wstring savePath) { // 최종 저장될 파일 경로를 생성합니다. wstring finalPath = _modelPath + savePath + L".mesh"; // Assimp 라이브러리를 사용하여 읽어온 3D 모델의 루트 노드부터 시작하여 // 모델 데이터를 순회하며 읽어들입니다. ReadModelData(_scene->mRootNode, -1, -1); // 메시에 적용될 스킨(본) 데이터를 읽어들입니다. ReadSkinData(); // 본과 메시 정보를 포함한 CSV 파일을 작성합니다. // 이 파일은 모델의 구조를 이해하거나 디버깅에 유용하게 사용될 수 있습니다. { FILE* file; // CSV 파일을 쓰기 모드로 엽니다. ::fopen_s(&file, "../Vertices.csv", "w"); // 모든 본 정보를 파일에 씁니다. for (shared_ptr<asBone>& bone : _bones) { string name = bone->name; // 본의 인덱스와 이름을 파일에 기록합니다. ::fprintf(file, "%d,%s\n", bone->index, bone->name.c_str()); } // 메시 데이터를 파일에 씁니다. ::fprintf(file, "\n"); for (shared_ptr<asMesh>& mesh : _meshes) { string name = mesh->name; // 메시 이름을 콘솔에 출력합니다(디버깅 용도). ::printf("%s\n", name.c_str()); // 메시의 각 정점에 대한 정보를 파일에 기록합니다. for (UINT i = 0; i < mesh->vertices.size(); i++) { Vec3 p = mesh->vertices[i].position; // 정점의 위치 Vec4 indices = mesh->vertices[i].blendIndices; // 본 인덱스 Vec4 weights = mesh->vertices[i].blendWeights; // 본 가중치 // 위치, 본 인덱스, 본 가중치를 파일에 기록합니다. ::fprintf(file, "%f,%f,%f,", p.x, p.y, p.z); ::fprintf(file, "%f,%f,%f,%f,", indices.x, indices.y, indices.z, indices.w); ::fprintf(file, "%f,%f,%f,%f\n", weights.x, weights.y, weights.z, weights.w); } } // 파일 작성을 마치고 파일을 닫습니다. ::fclose(file); } // 변환된 모델 데이터를 `.mesh` 파일 형식으로 최종 저장합니다. WriteModelFile(finalPath); } // 모델의 노드(본) 데이터를 읽고 처리하는 함수 void Converter::ReadModelData(aiNode* node, int32 index, int32 parent) { // 새로운 본 객체를 생성하고 기본 정보를 설정합니다. shared_ptr<asBone> bone = make_shared<asBone>(); bone->index = index; // 본의 고유 인덱스 bone->parent = parent; // 부모 본의 인덱스 bone->name = node->mName.C_Str(); // 본의 이름 // 본의 로컬 변환 행렬을 가져오고, 전치(transpose)하여 저장합니다. // Assimp는 열 기준(column-major) 행렬을 사용하지만, DirectX나 OpenGL은 행 기준(row-major) 행렬을 사용할 수 있으므로, 전치가 필요할 수 있습니다. Matrix transform(node->mTransformation[0]); bone->transform = transform.Transpose(); // 루트(혹은 부모) 본으로부터 상대적인 변환을 계산합니다. Matrix matParent = Matrix::Identity; // 기본값으로 단위 행렬을 사용 if (parent >= 0) { matParent = _bones[parent]->transform; // 부모 본의 변환 행렬을 가져옵니다. } // 최종적으로 본의 변환 행렬을 계산합니다. // 본 자신의 로컬 변환에 부모의 변환 행렬을 곱합니다. bone->transform = bone->transform * matParent; // 처리된 본 정보를 내부 리스트에 추가합니다. _bones.push_back(bone); // 현재 노드(본)에 연결된 메시 데이터를 읽어들입니다. ReadMeshData(node, index); // 현재 노드의 모든 자식 노드를 재귀적으로 탐색하여 같은 처리를 반복합니다. for (uint32 i = 0; i < node->mNumChildren; i++) { ReadModelData(node->mChildren[i], _bones.size(), index); } } // 노드에서 메시 데이터를 읽어와 내부 구조체에 저장하는 함수 void Converter::ReadMeshData(aiNode* node, int32 bone) { if (node->mNumMeshes < 1) return; // 메시가 없는 노드는 처리하지 않음 shared_ptr<asMesh> mesh = make_shared<asMesh>(); // 새 메시 객체 생성 mesh->name = node->mName.C_Str(); // 메시 이름 설정 mesh->boneIndex = bone; // 메시와 연결된 본 인덱스 설정 // 노드에 포함된 모든 메시에 대해 반복 for (uint32 i = 0; i < node->mNumMeshes; i++) { uint32 index = node->mMeshes[i]; const aiMesh* srcMesh = _scene->mMeshes[index]; // 소스 메시 참조 // 메시의 재질 이름을 가져옴 const aiMaterial* material = _scene->mMaterials[srcMesh->mMaterialIndex]; mesh->materialName = material->GetName().C_Str(); // 재질 이름 설정 const uint32 startVertex = mesh->vertices.size(); // 현재 메시의 시작 정점 인덱스 // 메시의 모든 정점에 대해 반복 for (uint32 v = 0; v < srcMesh->mNumVertices; v++) { VertexType vertex; // 새 정점 객체 // 정점 위치, UV 좌표, 법선 벡터 복사 ::memcpy(&vertex.position, &srcMesh->mVertices[v], sizeof(Vec3)); if (srcMesh->HasTextureCoords(0)) ::memcpy(&vertex.uv, &srcMesh->mTextureCoords[0][v], sizeof(Vec2)); if (srcMesh->HasNormals()) ::memcpy(&vertex.normal, &srcMesh->mNormals[v], sizeof(Vec3)); mesh->vertices.push_back(vertex); // 정점을 메시에 추가 } // 메시의 모든 면(페이스)에 대해 반복하여 인덱스 정보 추출 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); // 인덱스를 메시에 추가 } } _meshes.push_back(mesh); // 메시를 내부 메시 리스트에 추가 } // 모델의 본과 정점 가중치 정보를 읽어와 처리하는 함수 void Converter::ReadSkinData() { // 모든 메시에 대해 반복 for (uint32 i = 0; i < _scene->mNumMeshes; i++) { aiMesh* srcMesh = _scene->mMeshes[i]; if (!srcMesh->HasBones()) continue; // 본이 없으면 건너뜀 shared_ptr<asMesh> mesh = _meshes[i]; // 현재 메시 참조 vector<asBoneWeight> tempVertexBoneWeights; tempVertexBoneWeights.resize(mesh->vertices.size()); // 정점별 가중치 리스트 초기화 // 모든 본에 대해 반복하여 가중치 정보 추출 for (uint32 b = 0; b < srcMesh->mNumBones; b++) { aiBone* srcMeshBone = srcMesh->mBones[b]; uint32 boneIndex = GetBoneIndex(srcMeshBone->mName.C_Str()); // 본 인덱스 검색 for (uint32 w = 0; w < srcMeshBone->mNumWeights; w++) { uint32 index = srcMeshBone->mWeights[w].mVertexId; // 정점 인덱스 float weight = srcMeshBone->mWeights[w].mWeight; // 가중치 tempVertexBoneWeights[index].AddWeight(boneIndex, weight); // 가중치 정보 추가 } } // 최종 가중치 정보를 정점 데이터에 적용 for (uint32 v = 0; v < tempVertexBoneWeights.size(); v++) { tempVertexBoneWeights[v].Normalize(); // 가중치 정규화 asBlendWeight blendWeight = tempVertexBoneWeights[v].GetBlendWeights(); mesh->vertices[v].blendIndices = blendWeight.indeices; // 본 인덱스 설정 mesh->vertices[v].blendWeights = blendWeight.weights; // 본 가중치 설정 } } } // 모델 파일을 작성하는 함수 void Converter::WriteModelFile(wstring finalPath) { auto path = filesystem::path(finalPath); // 최종 파일이 위치할 디렉토리를 생성합니다. 이미 존재하면 건너뜁니다. filesystem::create_directory(path.parent_path()); shared_ptr<FileUtils> file = make_shared<FileUtils>(); file->Open(finalPath, FileMode::Write); // 파일을 쓰기 모드로 엽니다. // 본 데이터를 파일에 씁니다. file->Write<uint32>(_bones.size()); // 본의 개수를 기록합니다. for (shared_ptr<asBone>& bone : _bones) { // 각 본에 대한 정보를 파일에 기록합니다. file->Write<int32>(bone->index); // 본의 인덱스 file->Write<string>(bone->name); // 본의 이름 file->Write<int32>(bone->parent); // 부모 본의 인덱스 file->Write<Matrix>(bone->transform); // 본의 변환 행렬 } // 메시 데이터를 파일에 씁니다. file->Write<uint32>(_meshes.size()); // 메시의 개수를 기록합니다. for (shared_ptr<asMesh>& meshData : _meshes) { // 각 메시에 대한 정보를 파일에 기록합니다. file->Write<string>(meshData->name); // 메시 이름 file->Write<int32>(meshData->boneIndex); // 연결된 본의 인덱스 file->Write<string>(meshData->materialName); // 사용하는 재질의 이름 // 정점 데이터를 파일에 씁니다. file->Write<uint32>(meshData->vertices.size()); // 정점의 개수 file->Write(&meshData->vertices[0], sizeof(VertexType) * meshData->vertices.size()); // 정점 데이터 // 인덱스 데이터를 파일에 씁니다. file->Write<uint32>(meshData->indices.size()); // 인덱스의 개수 file->Write(&meshData->indices[0], sizeof(uint32) * meshData->indices.size()); // 인덱스 데이터 } }
소재 데이터 내보내기
소재 데이터를 읽고 .xml 형식의 파일로 내보냅니다. 이 과정은 소재의 이름, 텍스처 파일 경로, 색상 정보(Ambient, Diffuse, Specular, Emissive) 등을 포함합니다.
예제: 소재 데이터 내보내기
// 재질 데이터를 내보내는 함수 void Converter::ExportMaterialData(wstring savePath) { wstring finalPath = _texturePath + savePath + L".xml"; // 최종 파일 경로 ReadMaterialData(); // 재질 데이터 읽기 WriteMaterialData(finalPath); // 재질 파일 쓰기 } void Converter::ReadMaterialData() { for (uint32 i = 0; i < _scene->mNumMaterials; i++) { aiMaterial* srcMaterial = _scene->mMaterials[i]; shared_ptr<asMaterial> material = make_shared<asMaterial>(); material->name = srcMaterial->GetName().C_Str(); // 재질의 이름 // 주변광, 확산광, 반사광, 자체발광 속성을 읽어들입니다. aiColor3D color; //Ambient srcMaterial->Get(AI_MATKEY_COLOR_AMBIENT, color); material->ambient = Color(color.r, color.g, color.b, 1.f); //Diffuse srcMaterial->Get(AI_MATKEY_COLOR_DIFFUSE, color); material->diffuse = Color(color.r, color.g, color.b, 1.f); //Specualar srcMaterial->Get(AI_MATKEY_COLOR_SPECULAR, color); material->specular = Color(color.r, color.g, color.b, 1.f); srcMaterial->Get(AI_MATKEY_SHININESS, material->specular.w); //Emissive srcMaterial->Get(AI_MATKEY_COLOR_EMISSIVE, color); material->emissive = Color(color.r, color.g, color.b, 1.f); // 텍스처 파일 경로를 읽어들입니다. aiString file; //Diffuse Texture srcMaterial->GetTexture(aiTextureType_DIFFUSE, 0, &file); material->diffuseFile = file.C_Str(); //Specular Texture srcMaterial->GetTexture(aiTextureType_SPECULAR, 0, &file); material->specularFile = file.C_Str(); //Normal Texture srcMaterial->GetTexture(aiTextureType_NORMALS, 0, &file); material->normalFile = file.C_Str(); // 재질 정보를 내부 리스트에 추가합니다. _materials.push_back(material); } } // 재질 데이터를 XML 형식으로 저장하는 함수 void Converter::WriteMaterialData(wstring finalPath) { auto path = filesystem::path(finalPath); // 최종 저장 경로에 해당하는 폴더가 없으면 생성합니다. filesystem::create_directory(path.parent_path()); string folder = path.parent_path().string(); // XML 문서 객체를 생성합니다. shared_ptr<tinyxml2::XMLDocument> document = make_shared<tinyxml2::XMLDocument>(); // XML 선언부를 추가합니다. tinyxml2::XMLDeclaration* decl = document->NewDeclaration(); document->LinkEndChild(decl); // 재질 데이터의 루트 엘리먼트를 생성하고 문서에 추가합니다. tinyxml2::XMLElement* root = document->NewElement("Materials"); document->LinkEndChild(root); // 모든 재질에 대해 반복합니다. for (shared_ptr<asMaterial> material : _materials) { // 각 재질에 대한 정보를 XML 엘리먼트로 추가합니다. tinyxml2::XMLElement* node = document->NewElement("Material"); root->LinkEndChild(node); // 재질의 이름, 텍스처 파일 경로, 색상 정보 등을 엘리먼트의 속성으로 추가합니다. tinyxml2::XMLElement* element = nullptr; element = document->NewElement("Name"); element->SetText(material->name.c_str()); node->LinkEndChild(element); 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); // 각 색상 속성(ambient, diffuse, specular, emissive)에 대한 정보를 추가합니다. element = document->NewElement("Ambient"); element->SetAttribute("R", material->ambient.x); element->SetAttribute("G", material->ambient.y); element->SetAttribute("B", material->ambient.z); element->SetAttribute("A", material->ambient.w); node->LinkEndChild(element); element = document->NewElement("Diffuse"); element->SetAttribute("R", material->diffuse.x); element->SetAttribute("G", material->diffuse.y); element->SetAttribute("B", material->diffuse.z); element->SetAttribute("A", material->diffuse.w); node->LinkEndChild(element); element = document->NewElement("Specular"); element->SetAttribute("R", material->specular.x); element->SetAttribute("G", material->specular.y); element->SetAttribute("B", material->specular.z); element->SetAttribute("A", material->specular.w); node->LinkEndChild(element); element = document->NewElement("Emissive"); element->SetAttribute("R", material->emissive.x); element->SetAttribute("G", material->emissive.y); element->SetAttribute("B", material->emissive.z); element->SetAttribute("A", material->emissive.w); node->LinkEndChild(element); } // XML 문서를 파일로 저장합니다. document->SaveFile(Utils::ToString(finalPath).c_str()); } // 텍스처 파일을 저장하거나 기존 텍스처 파일을 새 위치로 복사하는 함수 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()); if (srcTexture) { // 텍스처 데이터가 임베디드되어 있으면 새 파일로 저장합니다. string pathStr = (filesystem::path(saveFolder) / fileName).string(); if (srcTexture->mHeight == 0) { shared_ptr<FileUtils> file = make_shared<FileUtils>(); file->Open(Utils::ToWString(pathStr), FileMode::Write); file->Write(srcTexture->pcData, srcTexture->mWidth); } else { 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 { // 외부 텍스처 파일이면 새 위치로 복사합니다. 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;// 처리된 텍스처 파일의 이름을 반환합니다. }
모델과 소재 정보 읽기
게임 개발에서 모델은 단순히 메쉬 정보만을 포함하지 않습니다. 모델에 적용될 텍스처, 소재 특성(ambient, diffuse, specular, emissive), 그리고 애니메이션을 위한 본(Bone) 정보 등도 함께 관리되어야 합니다.
예제: XML 기반 소재 정보 읽기
게임 엔진에서 모델의 소재 정보를 관리하기 위해 XML 파일 포맷을 사용할 수 있습니다. 아래의 코드 예제는 XML 파일에서 소재 정보를 읽어들여, 게임 엔진이 사용할 수 있는 형식으로 변환하는 과정을 보여줍니다.
// XML 파일에서 재질 데이터를 읽어오는 함수 void Model::ReadMaterial(wstring filename) { // 재질 파일의 전체 경로를 구성합니다. wstring fullPath = _texturePath + filename + L".xml"; auto parentPath = filesystem::path(fullPath).parent_path(); // XML 문서를 로드하기 위한 준비를 합니다. tinyxml2::XMLDocument* document = new tinyxml2::XMLDocument(); tinyxml2::XMLError error = document->LoadFile(Utils::ToString(fullPath).c_str()); assert(error == tinyxml2::XML_SUCCESS); // XML 문서의 루트 엘리먼트를 찾습니다. tinyxml2::XMLElement* root = document->FirstChildElement(); tinyxml2::XMLElement* materialNode = root->FirstChildElement(); // 모든 재질 노드를 순회합니다. while (materialNode) { shared_ptr<Material> material = make_shared<Material>(); // 재질의 이름을 설정합니다. tinyxml2::XMLElement* node = nullptr; node = materialNode->FirstChildElement(); material->SetName(Utils::ToWString(node->GetText())); // Diffuse Texture node = node->NextSiblingElement(); if (node->GetText()) { wstring textureStr = Utils::ToWString(node->GetText()); if (textureStr.length() > 0) { auto texture = RESOURCES->GetOrAddTexture(textureStr, (parentPath / textureStr).wstring()); material->SetDiffuseMap(texture); } } // Specular Texture node = node->NextSiblingElement(); if (node->GetText()) { wstring texture = Utils::ToWString(node->GetText()); if (texture.length() > 0) { wstring textureStr = Utils::ToWString(node->GetText()); if (textureStr.length() > 0) { auto texture = RESOURCES->GetOrAddTexture(textureStr, (parentPath / textureStr).wstring()); material->SetSpecularMap(texture); } } } // Normal Texture node = node->NextSiblingElement(); if (node->GetText()) { wstring textureStr = Utils::ToWString(node->GetText()); if (textureStr.length() > 0) { auto texture = RESOURCES->GetOrAddTexture(textureStr, (parentPath / textureStr).wstring()); material->SetNormalMap(texture); } } // Ambient { node = node->NextSiblingElement(); Color color; color.x = node->FloatAttribute("R"); color.y = node->FloatAttribute("G"); color.z = node->FloatAttribute("B"); color.w = node->FloatAttribute("A"); material->GetMaterialDesc().ambient = color; } // Diffuse { node = node->NextSiblingElement(); Color color; color.x = node->FloatAttribute("R"); color.y = node->FloatAttribute("G"); color.z = node->FloatAttribute("B"); color.w = node->FloatAttribute("A"); material->GetMaterialDesc().diffuse = color; } // Specular { node = node->NextSiblingElement(); Color color; color.x = node->FloatAttribute("R"); color.y = node->FloatAttribute("G"); color.z = node->FloatAttribute("B"); color.w = node->FloatAttribute("A"); material->GetMaterialDesc().specular = color; } // Emissive { node = node->NextSiblingElement(); Color color; color.x = node->FloatAttribute("R"); color.y = node->FloatAttribute("G"); color.z = node->FloatAttribute("B"); color.w = node->FloatAttribute("A"); material->GetMaterialDesc().emissive = color; } _materials.push_back(material); // Next Material materialNode = materialNode->NextSiblingElement(); } // 재질 정보를 기반으로 추가 처리를 수행합니다 (예: 캐시 정보 바인딩). BindCacheInfo(); }
예제: .mesh 파일에서 모델 정보 읽기
모델의 메쉬 정보, 본 정보, 소재 이름 등을 .mesh 파일 포맷으로부터 읽어들이는 과정을 아래의 코드에서 살펴볼 수 있습니다.
// 모델 파일을 읽어 모델의 본과 메시 데이터를 로드하는 함수 void Model::ReadModel(wstring filename) { // 모델 파일의 전체 경로를 생성합니다. wstring fullPath = _modelPath + filename + L".mesh"; // 파일 유틸리티 객체를 생성하고 파일을 읽기 모드로 엽니다. shared_ptr<FileUtils> file = make_shared<FileUtils>(); file->Open(fullPath, FileMode::Read); // 본 데이터를 읽어들입니다. { const uint32 count = file->Read<uint32>(); // 본의 개수를 읽어옵니다. for (uint32 i = 0; i < count; i++) // 각 본에 대하여 반복합니다. { shared_ptr<ModelBone> bone = make_shared<ModelBone>(); // 새로운 본 객체를 생성합니다. bone->index = file->Read<int32>(); // 본의 인덱스를 읽어옵니다. bone->name = Utils::ToWString(file->Read<string>()); // 본의 이름을 읽어옵니다. bone->parentIndex = file->Read<int32>(); // 부모 본의 인덱스를 읽어옵니다. bone->transform = file->Read<Matrix>(); // 본의 변환 행렬을 읽어옵니다. _bones.push_back(bone); // 처리된 본 객체를 모델의 본 목록에 추가합니다. } } // 메시 데이터를 읽어들입니다. { const uint32 count = file->Read<uint32>(); // 메시의 개수를 읽어옵니다. for (uint32 i = 0; i < count; i++) // 각 메시에 대하여 반복합니다. { shared_ptr<ModelMesh> mesh = make_shared<ModelMesh>(); // 새로운 메시 객체를 생성합니다. mesh->name = Utils::ToWString(file->Read<string>()); // 메시의 이름을 읽어옵니다. mesh->boneIndex = file->Read<int32>(); // 메시와 연관된 본의 인덱스를 읽어옵니다. // 메시가 사용하는 재질의 이름을 읽어옵니다. mesh->materialName = Utils::ToWString(file->Read<string>()); //VertexData 메시의 정점 데이터를 읽어옵니다. { const uint32 count = file->Read<uint32>(); // 정점의 개수를 읽어옵니다. vector<ModelVertexType> vertices;// 정점 데이터를 저장할 벡터를 생성합니다. vertices.resize(count); void* data = vertices.data();// 정점 데이터를 읽어옵니다. file->Read(&data, sizeof(ModelVertexType) * count); mesh->geometry->AddVertices(vertices);// 읽어온 정점 데이터를 메시에 추가합니다. } //IndexData 메시의 인덱스 데이터를 읽어옵니다. { const uint32 count = file->Read<uint32>();// 인덱스의 개수를 읽어옵니다. vector<uint32> indices; indices.resize(count);// 인덱스 데이터를 저장할 벡터를 생성합니다. void* data = indices.data();// 인덱스 데이터를 읽어옵니다. file->Read(&data, sizeof(uint32) * count); mesh->geometry->AddIndices(indices);// 읽어온 인덱스 데이터를 메시에 추가합니다. } mesh->CreateBuffers(); // 메시의 버퍼를 생성합니다. _meshes.push_back(mesh); // 처리된 메시 객체를 모델의 메시 목록에 추가합니다. } } // 모델 데이터와 관련된 캐시 정보를 바인딩합니다. BindCacheInfo(); } // 모델의 메시와 본에 대한 참조 정보를 바인딩하는 함수 void Model::BindCacheInfo() { // 메시에 재질 정보를 바인딩합니다. for (const auto& mesh : _meshes) { if (mesh->material != nullptr) // 메시에 이미 재질이 바인딩되어 있다면, 다음 메시로 넘어갑니다. continue; // 메시의 이름을 통해 해당하는 재질을 찾아 메시에 바인딩합니다. mesh->material = GetMaterialByName(mesh->materialName); } // 메시에 본 정보를 바인딩합니다. for (const auto& mesh : _meshes) { if (mesh->bone != nullptr) // 메시에 이미 본이 바인딩되어 있다면, 다음 메시로 넘어갑니다. continue; // 메시가 참조하는 본의 인덱스를 통해 해당 본을 찾아 메시에 바인딩합니다. mesh->bone = GetBoneByIndex(mesh->boneIndex); } // 본 구조의 계층 정보를 설정합니다. if (_root == nullptr && !_bones.empty()) // 루트 본이 설정되지 않았고, 본이 하나 이상 있는 경우 { _root = _bones[0]; // 첫 번째 본을 루트 본으로 설정합니다. for (const auto& bone : _bones) { if (bone->parentIndex >= 0) // 부모 본이 있는 경우 { // 부모 본의 인덱스를 통해 부모 본을 찾아 설정하고, 부모 본의 자식 목록에 현재 본을 추가합니다. bone->parent = _bones[bone->parentIndex]; bone->parent->children.push_back(bone); } else { // 부모 본이 없는 경우(루트 본) bone->parent = nullptr; } } } }
프로젝트 호출
StaticMeshDemo.h
#pragma once #include "IExecute.h" class StaticMeshDemo : public IExecute { public: void Init() override; void Update() override; void Render() override; void CreateTower(); void CreateTank(); private: shared_ptr<Shader> _shader; shared_ptr<GameObject> _obj; shared_ptr<GameObject> _camera; };
StaticMeshDemo.cpp
#include "pch.h" #include "StaticMeshDemo.h" #include "GeometryHelper.h" #include "Camera.h" #include "GameObject.h" #include "CameraScript.h" #include "MeshRenderer.h" #include "Mesh.h" #include "Material.h" #include "Model.h" #include "ModelRenderer.h" void StaticMeshDemo::Init() { RESOURCES->Init(); _shader = make_shared<Shader>(L"15. ModelDemo.fx"); // Camera _camera = make_shared<GameObject>(); _camera->GetOrAddTransform()->SetWorldPosition(Vec3{ 0.f, 0.f, -5.f }); _camera->AddComponent(make_shared<Camera>()); _camera->AddComponent(make_shared<CameraScript>()); //CreateTank(); RENDER->Init(_shader); } void StaticMeshDemo::Update() { _camera->Update(); RENDER->Update(); { LightDesc lightDesc; lightDesc.ambient = Vec4(0.4f); lightDesc.diffuse = Vec4(1.f); lightDesc.specular = Vec4(0.f); lightDesc.direction = Vec3(1.f, 0.f, 1.f); RENDER->PushLightData(lightDesc); } { _obj->Update(); } } void StaticMeshDemo::Render() { } void StaticMeshDemo::CreateTower() { // CustomData -> Memory shared_ptr<class Model> m1 = make_shared<Model>(); m1->ReadModel(L"Tower/Tower"); m1->ReadMaterial(L"Tower/Tower"); _obj = make_shared<GameObject>(); _obj->GetOrAddTransform()->SetWorldPosition(Vec3(0, 0, 50)); _obj->GetOrAddTransform()->SetWorldScale(Vec3(1.0f)); _obj->AddComponent(make_shared<ModelRenderer>(_shader)); { _obj->GetModelRenderer()->SetModel(m1); _obj->GetModelRenderer()->SetPass(1); } } 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()->SetWorldPosition(Vec3(0, 0, 50)); _obj->GetOrAddTransform()->SetWorldScale(Vec3(1.0f)); _obj->AddComponent(make_shared<ModelRenderer>(_shader)); { _obj->GetModelRenderer()->SetModel(m1); _obj->GetModelRenderer()->SetPass(1); } }
결론
DirectX11을 활용한 게임 엔진 개발에서는 모델 데이터의 가져오기가 중요한 역할을 합니다. 본문에서 설명한 기법을 사용하여 모델, 소재, 애니메이션 데이터를 게임 엔진에 통합하는 과정은 실제 게임 개발의 효율성과 품질을 크게 향상시킬 수 있습니다. XML 파일과 .mesh 파일 포맷을 활용한 데이터 관리 방법은 모델 데이터를 유연하게 관리하고 확장하는 데 도움이 됩니다.
반응형다음글이전글이전 글이 없습니다.댓글