3D Model Reading
모델 파일 읽기
대부분의 3D 모델은 디자이너 분들이 ‘손’으로 직접 만든 것!
대표적인 모델링 소프트웨어
- blender, maya, 3d max 등등
Blender 사용법
블렌더는 ‘오픈 소스’이기에 무료로 사용이 가능
(https://www.blender.org/)
블렌더를 킨 ‘첫 화면’
(박스는 지워주었다)
- Modeling-> Add 를 통하여 3D 모델 추가가 가능
‘원숭이’…? 도 있다
(여러 사용법이 있지만 튜토리얼은 공식 페이지 등에서 찾을 수 있다)
(https://studio.blender.org/training/)
우리가 할 것은
3D 모델을 이러한 모델링 소프트웨어를 통하여
게임에서 사용할 수 있도록 편집하는 것!
Export(모델 저장 - 내보내기)
-
File->Export를 통해 3D 모델을 다양한 확장자로 저장시킬 수 있다
(언리얼 엔진 등에서는 주로 .fbx 등을 사용)
(.gltf 는 ‘공개된’ 확장자 이기에 퀄리티가 좋아지는 중이라 카더라…)
(.bvh : 애니메이션 데이터) - 여기서는 .obj를 사용
- Wave Front?
원래 .obj를 포맷을 만들어 제공하였던 회사
(현재는 인수되어 해당 회사는 존재하지 않음)
- Wave Front?
- 3D 파일이지만 ‘문서 편집기’로 열수도 있음
예시
# Blender v2.82 (sub 7) OBJ File: ''
# www.blender.org
mtllib Monkey.mtl
o Suzanne
v 0.437500 0.164062 0.765625
v -0.437500 0.164062 0.765625
v 0.500000 0.093750 0.687500
...
-
좌표 (v), 노멀(vn), 텍스쳐 좌표(vt)
삼각형 인덱스(f) 등이
들어있음을 확인 가능 -
- MTL 파일?
- 머테리얼 파일
Blender 에서 ‘재질’을 설정한 경우
내용이 있음
- MTL 파일?
근데 MTL 파일이 없다면…?
현재 예제의 Diff.png 파일들과
.obj 모델만 존재하는 상황…이라면?
- 직접 머테리얼 매핑을 해주어
MTL 파일을 만들어주자!
-
fbx를 import로 가져오기
- 머테리얼(재질) 확인하기
- Material Properties를 확인(빨간 구체 아이콘)
- FBX에 기본 머테리얼이 있는지 확인하고 없으면 New로 새로운 머테리얼 생성
- Material Properties를 확인(빨간 구체 아이콘)
- Shader Editor로 이동
- 상단 Shading 탭에서 Pricipled BSDF 노드를 찾자
- 상단 Shading 탭에서 Pricipled BSDF 노드를 찾자
- Diffuse 텍스쳐를 연결
- Shift + A -> Texture > Image Texture 추가
(3D viewport에 마우스를 대고 하면 안됨) - open을 통해 diff 텍스쳐 파일 선택
- Principled BSDF의 BaseColor에 연결
- Shift + A -> Texture > Image Texture 추가
- 뷰포트에서 확인하기
- Material Preview 모드에서 확인가능!
- export로 내보내기!
- .blender 로 저장하여 작업 재사용 가능
- .obj 등으로 다시 내보내기
- .blender 로 저장하여 작업 재사용 가능
MTL 파일은 .obj 전용이다
-
obj에 지오메트리를 담고
mtl 파일에 재질을 따로 저장하는 방식임 -
fbx는 자체적으로 Mesh,Material, Texture, 애니메이션 등을 전부 포함가능함
모델 로딩
-
여기서는 assimp 라이브러리를 사용함
-
역시 vcpkg 로 쉽게 다운 가능
-
.\vcpkg 를 통하여 assimp:x64를 다운
-
이후 프로젝트에서 ‘추가 #using 라이브러리’에서 vcpkg의 include 부분을 선택하면 된다
모델 로딩 예제
struct MeshData {
std::vector<Vertex> vertices;
std::vector<uint32_t> indices; // uint32로 변경
std::string textureFilename;
};
-
- indices
- uint32 로 변경?
복잡한 모델링의 경우는 int16의 범위를 넘어서는 경우가
종종 발생할 수 있다고 하기에 안전하게 32로 사용
- indices
void ModelLoader::ProcessNode(aiNode *node, const aiScene *scene, Matrix tr) {
Matrix m;
ai_real *temp = &node->mTransformation.a1;
float *mTemp = &m._11;
for (int t = 0; t < 16; t++) {
mTemp[t] = float(temp[t]);
}
m = m.Transpose() * tr;
for (UINT i = 0; i < node->mNumMeshes; i++) {
aiMesh *mesh = scene->mMeshes[node->mMeshes[i]];
auto newMesh = this->ProcessMesh(mesh, scene);
for (auto &v : newMesh.vertices) {
v.position = DirectX::SimpleMath::Vector3::Transform(v.position, m);
}
meshes.push_back(newMesh);
}
for (UINT i = 0; i < node->mNumChildren; i++) {
this->ProcessNode(node->mChildren[i], scene, m);
}
}
MeshData ModelLoader::ProcessMesh(aiMesh *mesh, const aiScene *scene) {
// Data to fill
std::vector<Vertex> vertices;
std::vector<uint32_t> indices;
// Walk through each of the mesh's vertices
for (UINT i = 0; i < mesh->mNumVertices; i++) {
Vertex vertex;
vertex.position.x = mesh->mVertices[i].x;
vertex.position.y = mesh->mVertices[i].y;
vertex.position.z = mesh->mVertices[i].z;
vertex.normal.x = mesh->mNormals[i].x;
vertex.normal.y = mesh->mNormals[i].y;
vertex.normal.z = mesh->mNormals[i].z;
vertex.normal.Normalize();
if (mesh->mTextureCoords[0]) {
vertex.texcoord.x = (float)mesh->mTextureCoords[0][i].x;
vertex.texcoord.y = (float)mesh->mTextureCoords[0][i].y;
}
vertices.push_back(vertex);
}
for (UINT i = 0; i < mesh->mNumFaces; i++) {
aiFace face = mesh->mFaces[i];
for (UINT j = 0; j < face.mNumIndices; j++)
indices.push_back(face.mIndices[j]);
}
MeshData newMesh;
newMesh.vertices = vertices;
newMesh.indices = indices;
// http://assimp.sourceforge.net/lib_html/materials.html
if (mesh->mMaterialIndex >= 0) {
aiMaterial *material = scene->mMaterials[mesh->mMaterialIndex];
if (material->GetTextureCount(aiTextureType_DIFFUSE) > 0) {
aiString filepath;
material->GetTexture(aiTextureType_DIFFUSE, 0, &filepath);
std::string fullPath =
this->basePath +
std::string(std::filesystem::path(filepath.C_Str())
.filename()
.string());
newMesh.textureFilename = fullPath;
}
}
return newMesh;
}
-
모델 구조는 보통 ‘트리’ 구조이기에
‘Node’나 ‘root’ 같은 표현이 사용 -
재귀를 통하여 mesh를 읽어들인다
for (const auto &meshData : meshes) {
auto newMesh = std::make_shared<Mesh>();
AppBase::CreateVertexBuffer(meshData.vertices, newMesh->vertexBuffer);
newMesh->m_indexCount = UINT(meshData.indices.size());
AppBase::CreateIndexBuffer(meshData.indices, newMesh->indexBuffer);
if (!meshData.textureFilename.empty()) {
cout << meshData.textureFilename << endl;
AppBase::CreateTexture(meshData.textureFilename, newMesh->texture,
newMesh->textureResourceView);
}
newMesh->vertexConstantBuffer = vertexConstantBuffer;
newMesh->pixelConstantBuffer = pixelConstantBuffer;
this->m_meshes.push_back(newMesh);
}
- 읽어들인 데이터 중 ‘텍스쳐 파일 이름’을 이용하여
텍스쳐 파일을 만들어준다
(그리고 그것을 textureResourceView라는
SRV에 저장하여 ‘텍스쳐’로 사용하겠다는 용도)
//Render()
// 버텍스/인덱스 버퍼 설정
for (const auto &mesh : m_meshes) {
m_context->VSSetConstantBuffers(
0, 1, mesh->vertexConstantBuffer.GetAddressOf());
m_context->PSSetShaderResources(
0, 1, mesh->textureResourceView.GetAddressOf());
m_context->PSSetConstantBuffers(
0, 1, mesh->pixelConstantBuffer.GetAddressOf());
m_context->IASetInputLayout(m_basicInputLayout.Get());
m_context->IASetVertexBuffers(0, 1, mesh->vertexBuffer.GetAddressOf(),
&stride, &offset);
m_context->IASetIndexBuffer(mesh->indexBuffer.Get(),
DXGI_FORMAT_R32_UINT, 0);
m_context->IASetPrimitiveTopology(
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
m_context->DrawIndexed(mesh->m_indexCount, 0, 0);
}
- 여러 Mesh에 대하여
각각 draw를 해주는 모습
노멀이 없는 모델…??
// 노멀 벡터가 없는 경우를 대비하여 다시 계산
// 한 위치에는 한 버텍스만 있어야 연결 관계를 찾을 수 있음
for (auto &m : this->meshes) {
vector<Vector3> normalsTemp(m.vertices.size(), Vector3(0.0f));
vector<float> weightsTemp(m.vertices.size(), 0.0f);
for (int i = 0; i < m.indices.size(); i += 3) {
int idx0 = m.indices[i];
int idx1 = m.indices[i + 1];
int idx2 = m.indices[i + 2];
auto v0 = m.vertices[idx0];
auto v1 = m.vertices[idx1];
auto v2 = m.vertices[idx2];
auto faceNormal =
(v1.position - v0.position).Cross(v2.position - v0.position);
normalsTemp[idx0] += faceNormal;
normalsTemp[idx1] += faceNormal;
normalsTemp[idx2] += faceNormal;
weightsTemp[idx0] += 1.0f;
weightsTemp[idx1] += 1.0f;
weightsTemp[idx2] += 1.0f;
}
for (int i = 0; i < m.vertices.size(); i++) {
if (weightsTemp[i] > 0.0f) {
m.vertices[i].normal = normalsTemp[i] / weightsTemp[i];
m.vertices[i].normal.Normalize();
}
}
}
-
faceNormal을 통하여 Normal을 직접 계산하여 저장
(각 점들간의 외적을 이용) -
다만 요새는 모델링 소프트웨어가 대부분 normal 값을 계산해준다
gltf 샘플 모델들
git clone 한 후,
원하는 모델을 읽을 수 있음!
ps) obj 파일은 vs로 열 수 있음!
(시간은 조금 걸리지만…)
예제에서 .fbx 파일이 ‘텍스쳐를 못 찾는 경우?’
현 예제는
fbx 파일이 있는 폴더 안에 ‘텍스쳐 이미지’ 파일들이
‘들어있다 가정하고 구현’한 것이기 때문
정리
-
지금껏 해온 다양한 DX 예제들이
디자이너들이 열심히 만든 모델과 결합할때
더 훌륭한 결과를 낼 수 있다는 점을 확인 가능 -
모델링 엔진을 만들어 보려 한다면
해당 부분의 ‘Model Loader’ 클래스를 조금 더 자세히 보는 것도 방법 -
실제로 게임 엔진 등을 사용할때는
이러한 과정이 많이 스킵되지만
내부에서 ‘어떠한 일’이 일어나는지 안다면
구조의 이해, 응용, 문제의 원인과 해결책 등에 대한
다양한 이해도가 생기게 된다
댓글남기기