Add assimp-based mesh import
This commit is contained in:
@@ -9,6 +9,10 @@
|
||||
#include <algorithm>
|
||||
#include <glad/glad.h>
|
||||
|
||||
#ifdef MemoryBarrier
|
||||
#undef MemoryBarrier
|
||||
#endif
|
||||
|
||||
namespace XCEngine {
|
||||
namespace RHI {
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@ void Mesh::Release() {
|
||||
m_vertexData.Clear();
|
||||
m_indexData.Clear();
|
||||
m_sections.Clear();
|
||||
m_vertexCount = 0;
|
||||
m_vertexStride = 0;
|
||||
m_attributes = VertexAttribute::Position;
|
||||
m_indexCount = 0;
|
||||
m_use32BitIndex = false;
|
||||
UpdateMemorySize();
|
||||
SetInvalid();
|
||||
}
|
||||
|
||||
@@ -21,9 +27,11 @@ void Mesh::SetVertexData(const void* data, size_t size, Core::uint32 vertexCount
|
||||
m_attributes = attributes;
|
||||
|
||||
m_vertexData.Resize(size);
|
||||
std::memcpy(m_vertexData.Data(), data, size);
|
||||
|
||||
m_memorySize += size;
|
||||
if (size > 0 && data != nullptr) {
|
||||
std::memcpy(m_vertexData.Data(), data, size);
|
||||
}
|
||||
|
||||
UpdateMemorySize();
|
||||
}
|
||||
|
||||
void Mesh::SetIndexData(const void* data, size_t size, Core::uint32 indexCount, bool use32Bit) {
|
||||
@@ -31,13 +39,22 @@ void Mesh::SetIndexData(const void* data, size_t size, Core::uint32 indexCount,
|
||||
m_use32BitIndex = use32Bit;
|
||||
|
||||
m_indexData.Resize(size);
|
||||
std::memcpy(m_indexData.Data(), data, size);
|
||||
|
||||
m_memorySize += size;
|
||||
if (size > 0 && data != nullptr) {
|
||||
std::memcpy(m_indexData.Data(), data, size);
|
||||
}
|
||||
|
||||
UpdateMemorySize();
|
||||
}
|
||||
|
||||
void Mesh::AddSection(const MeshSection& section) {
|
||||
m_sections.PushBack(section);
|
||||
UpdateMemorySize();
|
||||
}
|
||||
|
||||
void Mesh::UpdateMemorySize() {
|
||||
m_memorySize = m_vertexData.Size() +
|
||||
m_indexData.Size() +
|
||||
m_sections.Size() * sizeof(MeshSection);
|
||||
}
|
||||
|
||||
} // namespace Resources
|
||||
|
||||
@@ -1,8 +1,123 @@
|
||||
#include <XCEngine/Resources/Mesh/MeshImportSettings.h>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
namespace {
|
||||
|
||||
bool TryParseBool(const std::string& json, const char* key, bool& value) {
|
||||
const std::string token = std::string("\"") + key + "\"";
|
||||
const size_t keyPos = json.find(token);
|
||||
if (keyPos == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t colonPos = json.find(':', keyPos + token.length());
|
||||
if (colonPos == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t valuePos = json.find_first_not_of(" \t\r\n", colonPos + 1);
|
||||
if (valuePos == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (json.compare(valuePos, 4, "true") == 0) {
|
||||
value = true;
|
||||
return true;
|
||||
}
|
||||
if (json.compare(valuePos, 5, "false") == 0) {
|
||||
value = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TryParseUInt32(const std::string& json, const char* key, Core::uint32& value) {
|
||||
const std::string token = std::string("\"") + key + "\"";
|
||||
const size_t keyPos = json.find(token);
|
||||
if (keyPos == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t colonPos = json.find(':', keyPos + token.length());
|
||||
if (colonPos == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t valuePos = json.find_first_of("-0123456789", colonPos + 1);
|
||||
if (valuePos == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char* endPtr = nullptr;
|
||||
const unsigned long parsed = std::strtoul(json.c_str() + valuePos, &endPtr, 10);
|
||||
if (endPtr == json.c_str() + valuePos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
value = static_cast<Core::uint32>(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryParseFloat(const std::string& json, const char* key, float& value) {
|
||||
const std::string token = std::string("\"") + key + "\"";
|
||||
const size_t keyPos = json.find(token);
|
||||
if (keyPos == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t colonPos = json.find(':', keyPos + token.length());
|
||||
if (colonPos == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t valuePos = json.find_first_of("-0123456789.", colonPos + 1);
|
||||
if (valuePos == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char* endPtr = nullptr;
|
||||
const float parsed = std::strtof(json.c_str() + valuePos, &endPtr);
|
||||
if (endPtr == json.c_str() + valuePos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
value = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryParseVector3(const std::string& json, const char* key, Math::Vector3& value) {
|
||||
const std::string token = std::string("\"") + key + "\"";
|
||||
const size_t keyPos = json.find(token);
|
||||
if (keyPos == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t bracketPos = json.find('[', keyPos + token.length());
|
||||
const size_t endBracketPos = json.find(']', bracketPos);
|
||||
if (bracketPos == std::string::npos || endBracketPos == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float z = 0.0f;
|
||||
if (std::sscanf(json.c_str() + bracketPos, "[%f,%f,%f]", &x, &y, &z) != 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
value = Math::Vector3(x, y, z);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MeshImportSettings::MeshImportSettings() = default;
|
||||
|
||||
MeshImportSettings::~MeshImportSettings() = default;
|
||||
@@ -12,11 +127,42 @@ Core::UniqueRef<ImportSettings> MeshImportSettings::Clone() const {
|
||||
}
|
||||
|
||||
bool MeshImportSettings::LoadFromJSON(const Containers::String& json) {
|
||||
return false;
|
||||
if (json.Empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string jsonText(json.CStr());
|
||||
|
||||
Core::uint32 importFlags = 0;
|
||||
if (TryParseUInt32(jsonText, "importFlags", importFlags)) {
|
||||
m_importFlags = static_cast<MeshImportFlags>(importFlags);
|
||||
}
|
||||
|
||||
TryParseFloat(jsonText, "scale", m_scale);
|
||||
TryParseVector3(jsonText, "offset", m_offset);
|
||||
TryParseBool(jsonText, "axisConversion", m_axisConversion);
|
||||
TryParseBool(jsonText, "mergeMeshes", m_mergeMeshes);
|
||||
TryParseFloat(jsonText, "optimizeThreshold", m_optimizeThreshold);
|
||||
TryParseFloat(jsonText, "importScale", m_importScale);
|
||||
TryParseFloat(jsonText, "threshold", m_threshold);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Containers::String MeshImportSettings::SaveToJSON() const {
|
||||
return Containers::String();
|
||||
std::ostringstream stream;
|
||||
stream << "{"
|
||||
<< "\"importFlags\":" << static_cast<Core::uint32>(m_importFlags) << ","
|
||||
<< "\"scale\":" << m_scale << ","
|
||||
<< "\"offset\":[" << m_offset.x << "," << m_offset.y << "," << m_offset.z << "],"
|
||||
<< "\"axisConversion\":" << (m_axisConversion ? "true" : "false") << ","
|
||||
<< "\"mergeMeshes\":" << (m_mergeMeshes ? "true" : "false") << ","
|
||||
<< "\"optimizeThreshold\":" << m_optimizeThreshold << ","
|
||||
<< "\"importScale\":" << m_importScale << ","
|
||||
<< "\"threshold\":" << m_threshold
|
||||
<< "}";
|
||||
const std::string text = stream.str();
|
||||
return Containers::String(text.c_str());
|
||||
}
|
||||
|
||||
} // namespace Resources
|
||||
|
||||
@@ -1,9 +1,189 @@
|
||||
#include <XCEngine/Resources/Mesh/MeshLoader.h>
|
||||
#include <XCEngine/Resources/Mesh/MeshImportSettings.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Core/Math/Matrix4.h>
|
||||
#include <assimp/Importer.hpp>
|
||||
#include <assimp/postprocess.h>
|
||||
#include <assimp/scene.h>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Resources {
|
||||
|
||||
namespace {
|
||||
|
||||
struct ImportedMeshData {
|
||||
std::vector<StaticMeshVertex> vertices;
|
||||
std::vector<Core::uint32> indices;
|
||||
VertexAttribute attributes = VertexAttribute::Position;
|
||||
};
|
||||
|
||||
Math::Matrix4 ConvertAssimpMatrix(const aiMatrix4x4& matrix) {
|
||||
Math::Matrix4 result;
|
||||
result.m[0][0] = matrix.a1; result.m[0][1] = matrix.a2; result.m[0][2] = matrix.a3; result.m[0][3] = matrix.a4;
|
||||
result.m[1][0] = matrix.b1; result.m[1][1] = matrix.b2; result.m[1][2] = matrix.b3; result.m[1][3] = matrix.b4;
|
||||
result.m[2][0] = matrix.c1; result.m[2][1] = matrix.c2; result.m[2][2] = matrix.c3; result.m[2][3] = matrix.c4;
|
||||
result.m[3][0] = matrix.d1; result.m[3][1] = matrix.d2; result.m[3][2] = matrix.d3; result.m[3][3] = matrix.d4;
|
||||
return result;
|
||||
}
|
||||
|
||||
Core::uint32 BuildPostProcessFlags(const MeshImportSettings& settings) {
|
||||
Core::uint32 flags = aiProcess_Triangulate |
|
||||
aiProcess_JoinIdenticalVertices |
|
||||
aiProcess_SortByPType |
|
||||
aiProcess_ValidateDataStructure |
|
||||
aiProcess_FindInvalidData;
|
||||
|
||||
if (settings.GetAxisConversion()) {
|
||||
flags |= aiProcess_MakeLeftHanded;
|
||||
flags |= aiProcess_FlipWindingOrder;
|
||||
}
|
||||
|
||||
if (settings.HasImportFlag(MeshImportFlags::FlipUVs)) {
|
||||
flags |= aiProcess_FlipUVs;
|
||||
}
|
||||
|
||||
if (settings.HasImportFlag(MeshImportFlags::FlipWindingOrder)) {
|
||||
flags |= aiProcess_FlipWindingOrder;
|
||||
}
|
||||
|
||||
if (settings.HasImportFlag(MeshImportFlags::GenerateNormals)) {
|
||||
flags |= aiProcess_GenSmoothNormals;
|
||||
}
|
||||
|
||||
if (settings.HasImportFlag(MeshImportFlags::GenerateTangents)) {
|
||||
flags |= aiProcess_CalcTangentSpace;
|
||||
}
|
||||
|
||||
if (settings.HasImportFlag(MeshImportFlags::OptimizeMesh)) {
|
||||
flags |= aiProcess_ImproveCacheLocality;
|
||||
flags |= aiProcess_OptimizeMeshes;
|
||||
flags |= aiProcess_OptimizeGraph;
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
ImportedMeshData ImportSingleMesh(const aiMesh& mesh,
|
||||
const Math::Matrix4& worldTransform,
|
||||
float globalScale,
|
||||
const Math::Vector3& offset) {
|
||||
ImportedMeshData result;
|
||||
result.vertices.reserve(mesh.mNumVertices);
|
||||
result.indices.reserve(mesh.mNumFaces * 3);
|
||||
|
||||
VertexAttribute attributes = VertexAttribute::Position;
|
||||
if (mesh.HasNormals()) {
|
||||
attributes = attributes | VertexAttribute::Normal;
|
||||
}
|
||||
if (mesh.HasTangentsAndBitangents()) {
|
||||
attributes = attributes | VertexAttribute::Tangent | VertexAttribute::Bitangent;
|
||||
}
|
||||
if (mesh.HasTextureCoords(0)) {
|
||||
attributes = attributes | VertexAttribute::UV0;
|
||||
}
|
||||
|
||||
const Math::Matrix4 normalTransform = worldTransform.Inverse().Transpose();
|
||||
const float appliedScale = globalScale;
|
||||
|
||||
for (Core::uint32 vertexIndex = 0; vertexIndex < mesh.mNumVertices; ++vertexIndex) {
|
||||
StaticMeshVertex vertex;
|
||||
|
||||
const aiVector3D& position = mesh.mVertices[vertexIndex];
|
||||
const Math::Vector3 transformedPosition = worldTransform.MultiplyPoint(Math::Vector3(position.x, position.y, position.z));
|
||||
vertex.position = transformedPosition * appliedScale + offset;
|
||||
|
||||
if (mesh.HasNormals()) {
|
||||
const aiVector3D& normal = mesh.mNormals[vertexIndex];
|
||||
vertex.normal = normalTransform.MultiplyVector(Math::Vector3(normal.x, normal.y, normal.z)).Normalized();
|
||||
}
|
||||
|
||||
if (mesh.HasTangentsAndBitangents()) {
|
||||
const aiVector3D& tangent = mesh.mTangents[vertexIndex];
|
||||
const aiVector3D& bitangent = mesh.mBitangents[vertexIndex];
|
||||
vertex.tangent = normalTransform.MultiplyVector(Math::Vector3(tangent.x, tangent.y, tangent.z)).Normalized();
|
||||
vertex.bitangent = normalTransform.MultiplyVector(Math::Vector3(bitangent.x, bitangent.y, bitangent.z)).Normalized();
|
||||
}
|
||||
|
||||
if (mesh.HasTextureCoords(0)) {
|
||||
const aiVector3D& uv = mesh.mTextureCoords[0][vertexIndex];
|
||||
vertex.uv0 = Math::Vector2(uv.x, uv.y);
|
||||
}
|
||||
|
||||
result.vertices.push_back(vertex);
|
||||
}
|
||||
|
||||
for (Core::uint32 faceIndex = 0; faceIndex < mesh.mNumFaces; ++faceIndex) {
|
||||
const aiFace& face = mesh.mFaces[faceIndex];
|
||||
if (face.mNumIndices != 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.indices.push_back(face.mIndices[0]);
|
||||
result.indices.push_back(face.mIndices[1]);
|
||||
result.indices.push_back(face.mIndices[2]);
|
||||
}
|
||||
|
||||
result.attributes = attributes;
|
||||
return result;
|
||||
}
|
||||
|
||||
void ProcessNode(const aiNode& node,
|
||||
const aiScene& scene,
|
||||
const Math::Matrix4& parentTransform,
|
||||
const MeshImportSettings& settings,
|
||||
std::vector<StaticMeshVertex>& vertices,
|
||||
std::vector<Core::uint32>& indices,
|
||||
Containers::Array<MeshSection>& sections,
|
||||
VertexAttribute& attributes) {
|
||||
const Math::Matrix4 worldTransform = parentTransform * ConvertAssimpMatrix(node.mTransformation);
|
||||
const float globalScale = settings.GetScale() * settings.GetImportScale();
|
||||
|
||||
for (Core::uint32 meshIndex = 0; meshIndex < node.mNumMeshes; ++meshIndex) {
|
||||
const aiMesh* mesh = scene.mMeshes[node.mMeshes[meshIndex]];
|
||||
if (mesh == nullptr || mesh->mNumVertices == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ImportedMeshData meshData = ImportSingleMesh(*mesh, worldTransform, globalScale, settings.GetOffset());
|
||||
if (meshData.vertices.empty() || meshData.indices.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Core::uint32 baseVertex = static_cast<Core::uint32>(vertices.size());
|
||||
const Core::uint32 startIndex = static_cast<Core::uint32>(indices.size());
|
||||
|
||||
vertices.insert(vertices.end(), meshData.vertices.begin(), meshData.vertices.end());
|
||||
indices.reserve(indices.size() + meshData.indices.size());
|
||||
for (Core::uint32 index : meshData.indices) {
|
||||
indices.push_back(baseVertex + index);
|
||||
}
|
||||
|
||||
MeshSection section{};
|
||||
section.baseVertex = baseVertex;
|
||||
section.vertexCount = static_cast<Core::uint32>(meshData.vertices.size());
|
||||
section.startIndex = startIndex;
|
||||
section.indexCount = static_cast<Core::uint32>(meshData.indices.size());
|
||||
section.materialID = mesh->mMaterialIndex;
|
||||
sections.PushBack(section);
|
||||
|
||||
attributes = attributes | meshData.attributes;
|
||||
}
|
||||
|
||||
for (Core::uint32 childIndex = 0; childIndex < node.mNumChildren; ++childIndex) {
|
||||
const aiNode* child = node.mChildren[childIndex];
|
||||
if (child != nullptr) {
|
||||
ProcessNode(*child, scene, worldTransform, settings, vertices, indices, sections, attributes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MeshLoader::MeshLoader() = default;
|
||||
MeshLoader::~MeshLoader() = default;
|
||||
|
||||
@@ -19,43 +199,100 @@ Containers::Array<Containers::String> MeshLoader::GetSupportedExtensions() const
|
||||
}
|
||||
|
||||
bool MeshLoader::CanLoad(const Containers::String& path) const {
|
||||
Containers::String ext = GetExtension(path);
|
||||
ext.ToLower();
|
||||
Containers::String ext = GetExtension(path).ToLower();
|
||||
|
||||
return ext == "fbx" || ext == "obj" || ext == "gltf" ||
|
||||
ext == "glb" || ext == "dae" || ext == "stl";
|
||||
}
|
||||
|
||||
LoadResult MeshLoader::Load(const Containers::String& path, const ImportSettings* settings) {
|
||||
(void)settings;
|
||||
|
||||
Containers::String ext = GetExtension(path);
|
||||
ext.ToLower();
|
||||
const Containers::String ext = GetExtension(path).ToLower();
|
||||
|
||||
if (!CanLoad(path)) {
|
||||
return LoadResult(Containers::String("Unsupported mesh format: ") + ext);
|
||||
}
|
||||
|
||||
Containers::Array<Core::uint8> fileData = ReadFileData(path);
|
||||
if (fileData.Empty()) {
|
||||
|
||||
std::ifstream file(path.CStr(), std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
return LoadResult(Containers::String("Failed to read file: ") + path);
|
||||
}
|
||||
|
||||
|
||||
MeshImportSettings defaultSettings;
|
||||
const MeshImportSettings* resolvedSettings = dynamic_cast<const MeshImportSettings*>(settings);
|
||||
if (resolvedSettings == nullptr) {
|
||||
resolvedSettings = &defaultSettings;
|
||||
}
|
||||
|
||||
Assimp::Importer importer;
|
||||
const aiScene* scene = importer.ReadFile(path.CStr(), BuildPostProcessFlags(*resolvedSettings));
|
||||
if (scene == nullptr || scene->mRootNode == nullptr || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) != 0) {
|
||||
const char* errorText = importer.GetErrorString();
|
||||
return LoadResult(Containers::String("Assimp failed to load mesh: ") +
|
||||
Containers::String(errorText != nullptr ? errorText : "Unknown error"));
|
||||
}
|
||||
|
||||
std::vector<StaticMeshVertex> vertices;
|
||||
std::vector<Core::uint32> indices;
|
||||
Containers::Array<MeshSection> sections;
|
||||
VertexAttribute attributes = VertexAttribute::Position;
|
||||
|
||||
ProcessNode(*scene->mRootNode,
|
||||
*scene,
|
||||
Math::Matrix4::Identity(),
|
||||
*resolvedSettings,
|
||||
vertices,
|
||||
indices,
|
||||
sections,
|
||||
attributes);
|
||||
|
||||
if (vertices.empty() || indices.empty()) {
|
||||
return LoadResult(Containers::String("No triangle mesh data found in: ") + path);
|
||||
}
|
||||
|
||||
auto* mesh = new Mesh();
|
||||
|
||||
|
||||
IResource::ConstructParams params;
|
||||
params.name = path;
|
||||
const std::string fileName = std::filesystem::path(path.CStr()).filename().string();
|
||||
params.name = Containers::String(fileName.c_str());
|
||||
params.path = path;
|
||||
params.guid = ResourceGUID::Generate(path);
|
||||
params.memorySize = fileData.Size();
|
||||
params.memorySize = 0;
|
||||
|
||||
mesh->Initialize(params);
|
||||
|
||||
mesh->SetVertexData(vertices.data(),
|
||||
vertices.size() * sizeof(StaticMeshVertex),
|
||||
static_cast<Core::uint32>(vertices.size()),
|
||||
sizeof(StaticMeshVertex),
|
||||
attributes);
|
||||
|
||||
const bool use32BitIndex = vertices.size() > std::numeric_limits<Core::uint16>::max();
|
||||
if (use32BitIndex) {
|
||||
mesh->SetIndexData(indices.data(),
|
||||
indices.size() * sizeof(Core::uint32),
|
||||
static_cast<Core::uint32>(indices.size()),
|
||||
true);
|
||||
} else {
|
||||
std::vector<Core::uint16> compactIndices;
|
||||
compactIndices.reserve(indices.size());
|
||||
for (Core::uint32 index : indices) {
|
||||
compactIndices.push_back(static_cast<Core::uint16>(index));
|
||||
}
|
||||
mesh->SetIndexData(compactIndices.data(),
|
||||
compactIndices.size() * sizeof(Core::uint16),
|
||||
static_cast<Core::uint32>(compactIndices.size()),
|
||||
false);
|
||||
}
|
||||
|
||||
for (const MeshSection& section : sections) {
|
||||
mesh->AddSection(section);
|
||||
}
|
||||
|
||||
return LoadResult(mesh);
|
||||
}
|
||||
|
||||
ImportSettings* MeshLoader::GetDefaultSettings() const {
|
||||
return nullptr;
|
||||
return new MeshImportSettings();
|
||||
}
|
||||
|
||||
REGISTER_RESOURCE_LOADER(MeshLoader);
|
||||
|
||||
Reference in New Issue
Block a user