diff --git a/.gitmodules b/.gitmodules index 18429a82e6..c1019fc939 100644 --- a/.gitmodules +++ b/.gitmodules @@ -75,3 +75,6 @@ [submodule "hidapi-src"] path = Externals/hidapi/hidapi-src url = https://github.com/libusb/hidapi +[submodule "Externals/tinygltf/tinygltf"] + path = Externals/tinygltf/tinygltf + url = https://github.com/syoyo/tinygltf.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 88e8f4a8f5..1ff8bd9db3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -638,6 +638,7 @@ add_subdirectory(Externals/glslang) if(WIN32 OR APPLE) add_subdirectory(Externals/spirv_cross) endif() +add_subdirectory(Externals/tinygltf) if(ENABLE_VULKAN) add_definitions(-DHAS_VULKAN) diff --git a/Externals/tinygltf/CMakeLists.txt b/Externals/tinygltf/CMakeLists.txt new file mode 100644 index 0000000000..a551d6def3 --- /dev/null +++ b/Externals/tinygltf/CMakeLists.txt @@ -0,0 +1,11 @@ +add_library(tinygltf STATIC) +target_compile_definitions(tinygltf PUBLIC TINYGLTF_NOEXCEPTION) +target_compile_definitions(tinygltf PUBLIC TINYGLTF_NO_EXTERNAL_IMAGE) +target_compile_definitions(tinygltf PUBLIC TINYGLTF_USE_CPP14) +if (NOT MSVC) + target_compile_features(tinygltf PRIVATE cxx_std_20) +endif() +target_sources(tinygltf PRIVATE + tinygltf/tiny_gltf.cc) +target_include_directories(tinygltf INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) +dolphin_disable_warnings_msvc(tinygltf) diff --git a/Externals/tinygltf/exports.props b/Externals/tinygltf/exports.props new file mode 100644 index 0000000000..7e2fde81cf --- /dev/null +++ b/Externals/tinygltf/exports.props @@ -0,0 +1,14 @@ + + + + + $(ExternalsDir)tinygltf;%(AdditionalIncludeDirectories) + TINYGLTF_NOEXCEPTION;TINYGLTF_NO_EXTERNAL_IMAGE;TINYGLTF_USE_CPP14;%(PreprocessorDefinitions) + + + + + {8bda3693-4999-4d11-9e52-8d08c30b643a} + + + diff --git a/Externals/tinygltf/tinygltf b/Externals/tinygltf/tinygltf new file mode 160000 index 0000000000..c5641f2c22 --- /dev/null +++ b/Externals/tinygltf/tinygltf @@ -0,0 +1 @@ +Subproject commit c5641f2c22d117da7971504591a8f6a41ece488b diff --git a/Externals/tinygltf/tinygltf.vcxproj b/Externals/tinygltf/tinygltf.vcxproj new file mode 100644 index 0000000000..e33d949e56 --- /dev/null +++ b/Externals/tinygltf/tinygltf.vcxproj @@ -0,0 +1,35 @@ + + + + + + {8bda3693-4999-4d11-9e52-8d08c30b643a} + + + + + + + + + + + + + + tinygltf;%(AdditionalIncludeDirectories) + + + + + + + + + + + + + + + diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 59cecdf5dc..74b3185753 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -656,6 +656,7 @@ + @@ -1293,6 +1294,7 @@ + diff --git a/Source/Core/DolphinLib.vcxproj b/Source/Core/DolphinLib.vcxproj index d2071d6618..da443ea45b 100644 --- a/Source/Core/DolphinLib.vcxproj +++ b/Source/Core/DolphinLib.vcxproj @@ -58,6 +58,7 @@ + diff --git a/Source/Core/VideoCommon/Assets/MeshAsset.cpp b/Source/Core/VideoCommon/Assets/MeshAsset.cpp new file mode 100644 index 0000000000..b8dd029375 --- /dev/null +++ b/Source/Core/VideoCommon/Assets/MeshAsset.cpp @@ -0,0 +1,648 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "VideoCommon/Assets/MeshAsset.h" + +#include +#include +#include + +#include + +#include "Common/IOFile.h" +#include "Common/Logging/Log.h" +#include "Common/StringUtil.h" + +namespace VideoCommon +{ +namespace +{ +Common::Matrix44 BuildMatrixFromNode(const tinygltf::Node& node) +{ + if (!node.matrix.empty()) + { + Common::Matrix44 matrix; + for (std::size_t i = 0; i < node.matrix.size(); i++) + { + matrix.data[i] = static_cast(node.matrix[i]); + } + return matrix; + } + + Common::Matrix44 matrix = Common::Matrix44::Identity(); + + // Check individual components + + if (!node.scale.empty()) + { + matrix *= Common::Matrix44::FromMatrix33(Common::Matrix33::Scale( + Common::Vec3{static_cast(node.scale[0]), static_cast(node.scale[1]), + static_cast(node.scale[2])})); + } + + if (!node.rotation.empty()) + { + matrix *= Common::Matrix44::FromQuaternion(Common::Quaternion( + static_cast(node.rotation[3]), static_cast(node.rotation[0]), + static_cast(node.rotation[1]), static_cast(node.rotation[2]))); + } + + if (!node.translation.empty()) + { + matrix *= Common::Matrix44::Translate(Common::Vec3{static_cast(node.translation[0]), + static_cast(node.translation[1]), + static_cast(node.translation[2])}); + } + + return matrix; +} + +bool GLTFComponentTypeToAttributeFormat(int component_type, AttributeFormat* format) +{ + switch (component_type) + { + case TINYGLTF_COMPONENT_TYPE_BYTE: + { + format->type = ComponentFormat::Byte; + format->integer = false; + } + break; + case TINYGLTF_COMPONENT_TYPE_DOUBLE: + { + return false; + } + break; + case TINYGLTF_COMPONENT_TYPE_FLOAT: + { + format->type = ComponentFormat::Float; + format->integer = false; + } + break; + case TINYGLTF_COMPONENT_TYPE_INT: + { + format->type = ComponentFormat::Float; + format->integer = true; + } + break; + case TINYGLTF_COMPONENT_TYPE_SHORT: + { + format->type = ComponentFormat::Short; + format->integer = false; + } + break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: + { + format->type = ComponentFormat::UByte; + format->integer = false; + } + break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: + { + return false; + } + break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: + { + format->type = ComponentFormat::UShort; + format->integer = false; + } + break; + }; + + return true; +} + +bool UpdateVertexStrideFromPrimitive(const tinygltf::Model& model, u32 accessor_index, + MeshDataChunk* chunk) +{ + const tinygltf::Accessor& accessor = model.accessors[accessor_index]; + + const int component_count = tinygltf::GetNumComponentsInType(accessor.type); + if (component_count == -1) + { + ERROR_LOG_FMT(VIDEO, "Failed to update vertex stride, component count was invalid"); + return false; + } + + const int component_size = + tinygltf::GetComponentSizeInBytes(static_cast(accessor.componentType)); + if (component_size == -1) + { + ERROR_LOG_FMT(VIDEO, "Failed to update vertex stride, component size was invalid"); + return false; + } + + chunk->vertex_stride += component_size * component_count; + return true; +} + +bool CopyBufferDataFromPrimitive(const tinygltf::Model& model, u32 accessor_index, + std::size_t* outbound_offset, MeshDataChunk* chunk) +{ + const tinygltf::Accessor& accessor = model.accessors[accessor_index]; + + const int component_count = tinygltf::GetNumComponentsInType(accessor.type); + if (component_count == -1) + { + ERROR_LOG_FMT(VIDEO, "Failed to copy buffer data from primitive, component count was invalid"); + return false; + } + + const int component_size = + tinygltf::GetComponentSizeInBytes(static_cast(accessor.componentType)); + if (component_size == -1) + { + ERROR_LOG_FMT(VIDEO, "Failed to copy buffer data from primitive, component size was invalid"); + return false; + } + + const tinygltf::BufferView& buffer_view = model.bufferViews[accessor.bufferView]; + const tinygltf::Buffer& buffer = model.buffers[buffer_view.buffer]; + + if (buffer_view.byteStride == 0) + { + // Data is tightly packed + const auto data = &buffer.data[accessor.byteOffset + buffer_view.byteOffset]; + for (std::size_t i = 0; i < accessor.count; i++) + { + const std::size_t vertex_data_offset = i * chunk->vertex_stride + *outbound_offset; + memcpy(&chunk->vertex_data[vertex_data_offset], &data[i * component_size * component_count], + component_size * component_count); + } + } + else + { + // Data is interleaved + const auto data = &buffer.data[accessor.byteOffset + buffer_view.byteOffset]; + for (std::size_t i = 0; i < accessor.count; i++) + { + const std::size_t vertex_data_offset = i * chunk->vertex_stride + *outbound_offset; + const std::size_t gltf_data_offset = i * buffer_view.byteStride; + + memcpy(&chunk->vertex_data[vertex_data_offset], &data[gltf_data_offset], + component_size * component_count); + } + } + + *outbound_offset += component_size * component_count; + + return true; +} + +bool ReadGLTFMesh(std::string_view mesh_file, const tinygltf::Model& model, + const tinygltf::Mesh& mesh, const Common::Matrix44& mat, MeshData* data) +{ + for (std::size_t primitive_index = 0; primitive_index < mesh.primitives.size(); ++primitive_index) + { + MeshDataChunk chunk; + chunk.transform = mat; + const tinygltf::Primitive& primitive = mesh.primitives[primitive_index]; + if (primitive.indices == -1) + { + ERROR_LOG_FMT(VIDEO, "Mesh '{}' is expected to have indices but doesn't have any", mesh_file); + return false; + } + chunk.material_name = model.materials[primitive.material].name; + const tinygltf::Accessor& index_accessor = model.accessors[primitive.indices]; + const tinygltf::BufferView& index_buffer_view = model.bufferViews[index_accessor.bufferView]; + const tinygltf::Buffer& index_buffer = model.buffers[index_buffer_view.buffer]; + const int index_stride = index_accessor.ByteStride(index_buffer_view); + if (index_stride == -1) + { + ERROR_LOG_FMT(VIDEO, "Mesh '{}' has invalid stride", mesh_file); + return false; + } + // TODO C++23: use make_unique_overwrite + chunk.indices = std::unique_ptr(new u16[index_accessor.count]); + auto index_src = &index_buffer.data[index_accessor.byteOffset + index_buffer_view.byteOffset]; + for (std::size_t i = 0; i < index_accessor.count; i++) + { + if (index_accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT) + { + std::memcpy(&chunk.indices[i], &index_src[i * index_stride], sizeof(u16)); + } + else if (index_accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE) + { + u8 unsigned_byte; + std::memcpy(&unsigned_byte, &index_src[i * index_stride], sizeof(u8)); + chunk.indices[i] = unsigned_byte; + } + else if (index_accessor.componentType == TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT) + { + // TODO: update Dolphin to support u32 indices + ERROR_LOG_FMT( + VIDEO, + "Mesh '{}' uses an indice format of unsigned int which is not currently supported", + mesh_file); + return false; + } + } + + chunk.num_indices = static_cast(index_accessor.count); + + if (primitive.mode == TINYGLTF_MODE_TRIANGLES) + { + chunk.primitive_type = PrimitiveType::Triangles; + } + else if (primitive.mode == TINYGLTF_MODE_TRIANGLE_STRIP) + { + chunk.primitive_type = PrimitiveType::TriangleStrip; + } + else if (primitive.mode == TINYGLTF_MODE_TRIANGLE_FAN) + { + ERROR_LOG_FMT(VIDEO, "Mesh '{}' requires triangle fan but that is not supported", mesh_file); + return false; + } + else if (primitive.mode == TINYGLTF_MODE_LINE) + { + chunk.primitive_type = PrimitiveType::Lines; + } + else if (primitive.mode == TINYGLTF_MODE_POINTS) + { + chunk.primitive_type = PrimitiveType::Points; + } + + chunk.vertex_stride = 0; + static constexpr std::array all_names = { + "POSITION", "NORMAL", "COLOR_0", "COLOR_1", "TEXCOORD_0", "TEXCOORD_1", + "TEXCOORD_2", "TEXCOORD_3", "TEXCOORD_4", "TEXCOORD_5", "TEXCOORD_6", "TEXCOORD_7", + }; + for (std::size_t i = 0; i < all_names.size(); i++) + { + const auto it = primitive.attributes.find(std::string{all_names[i]}); + if (it != primitive.attributes.end()) + { + if (!UpdateVertexStrideFromPrimitive(model, it->second, &chunk)) + return false; + } + } + chunk.vertex_declaration.stride = chunk.vertex_stride; + + const auto position_it = primitive.attributes.find("POSITION"); + if (position_it == primitive.attributes.end()) + { + ERROR_LOG_FMT(VIDEO, "Mesh '{}' does not provide a POSITION attribute, that is required", + mesh_file); + return false; + } + std::size_t outbound_offset = 0; + const tinygltf::Accessor& pos_accessor = model.accessors[position_it->second]; + chunk.num_vertices = static_cast(pos_accessor.count); + // TODO C++23: use make_unique_overwrite + chunk.vertex_data = std::unique_ptr(new u8[chunk.num_vertices * chunk.vertex_stride]); + if (!CopyBufferDataFromPrimitive(model, position_it->second, &outbound_offset, &chunk)) + return false; + chunk.components_available = 0; + chunk.vertex_declaration.position.enable = true; + chunk.vertex_declaration.position.components = 3; + chunk.vertex_declaration.position.offset = 0; + if (!GLTFComponentTypeToAttributeFormat(pos_accessor.componentType, + &chunk.vertex_declaration.position)) + { + ERROR_LOG_FMT(VIDEO, "Mesh '{}' has invalid attribute format for position", mesh_file); + return false; + } + + // Save off min and max position in case we want to compute bounds + // GLTF spec expects these values to exist but error if they don't + if (pos_accessor.minValues.size() != 3) + { + ERROR_LOG_FMT(VIDEO, "Mesh '{}' is expected to have a minimum value but it is missing", + mesh_file); + return false; + } + chunk.minimum_position.x = static_cast(pos_accessor.minValues[0]); + chunk.minimum_position.y = static_cast(pos_accessor.minValues[1]); + chunk.minimum_position.z = static_cast(pos_accessor.minValues[2]); + + if (pos_accessor.maxValues.size() != 3) + { + ERROR_LOG_FMT(VIDEO, "Mesh '{}' is expected to have a maximum value but it is missing", + mesh_file); + return false; + } + chunk.maximum_position.x = static_cast(pos_accessor.maxValues[0]); + chunk.maximum_position.y = static_cast(pos_accessor.maxValues[1]); + chunk.maximum_position.z = static_cast(pos_accessor.maxValues[2]); + + static constexpr std::array color_names = { + "COLOR_0", + "COLOR_1", + }; + for (std::size_t i = 0; i < color_names.size(); i++) + { + const auto color_it = primitive.attributes.find(std::string{color_names[i]}); + if (color_it != primitive.attributes.end()) + { + chunk.vertex_declaration.colors[i].offset = static_cast(outbound_offset); + if (!CopyBufferDataFromPrimitive(model, color_it->second, &outbound_offset, &chunk)) + return false; + chunk.components_available |= VB_HAS_COL0 << i; + + chunk.vertex_declaration.colors[i].enable = true; + chunk.vertex_declaration.colors[i].components = 3; + const tinygltf::Accessor& accessor = model.accessors[color_it->second]; + if (!GLTFComponentTypeToAttributeFormat(accessor.componentType, + &chunk.vertex_declaration.colors[i])) + { + ERROR_LOG_FMT(VIDEO, "Mesh '{}' has invalid attribute format for {}", mesh_file, + color_names[i]); + return false; + } + } + else + { + chunk.vertex_declaration.colors[i].enable = false; + } + } + + const auto normal_it = primitive.attributes.find("NORMAL"); + if (normal_it != primitive.attributes.end()) + { + chunk.vertex_declaration.normals[0].offset = static_cast(outbound_offset); + if (!CopyBufferDataFromPrimitive(model, normal_it->second, &outbound_offset, &chunk)) + return false; + chunk.components_available |= VB_HAS_NORMAL; + chunk.vertex_declaration.normals[0].enable = true; + chunk.vertex_declaration.normals[0].components = 3; + const tinygltf::Accessor& accessor = model.accessors[normal_it->second]; + if (!GLTFComponentTypeToAttributeFormat(accessor.componentType, + &chunk.vertex_declaration.normals[0])) + { + ERROR_LOG_FMT(VIDEO, "Mesh '{}' has invalid attribute format for NORMAL", mesh_file); + return false; + } + } + else + { + chunk.vertex_declaration.normals[0].enable = false; + } + + static constexpr std::array texcoord_names = { + "TEXCOORD_0", "TEXCOORD_1", "TEXCOORD_2", "TEXCOORD_3", + "TEXCOORD_4", "TEXCOORD_5", "TEXCOORD_6", "TEXCOORD_7", + }; + for (std::size_t i = 0; i < texcoord_names.size(); i++) + { + const auto texture_it = primitive.attributes.find(std::string{texcoord_names[i]}); + if (texture_it != primitive.attributes.end()) + { + chunk.vertex_declaration.texcoords[i].offset = static_cast(outbound_offset); + if (!CopyBufferDataFromPrimitive(model, texture_it->second, &outbound_offset, &chunk)) + return false; + chunk.components_available |= VB_HAS_UV0 << i; + chunk.vertex_declaration.texcoords[i].enable = true; + chunk.vertex_declaration.texcoords[i].components = 2; + const tinygltf::Accessor& accessor = model.accessors[texture_it->second]; + if (!GLTFComponentTypeToAttributeFormat(accessor.componentType, + &chunk.vertex_declaration.texcoords[i])) + { + ERROR_LOG_FMT(VIDEO, "Mesh '{}' has invalid attribute format for {}", mesh_file, + texcoord_names[i]); + return false; + } + } + else + { + chunk.vertex_declaration.texcoords[i].enable = false; + } + } + + // Position matrix can be enabled if the draw that is using + // this mesh needs it + chunk.vertex_declaration.posmtx.enable = false; + + data->m_mesh_chunks.push_back(std::move(chunk)); + } + + return true; +} + +bool ReadGLTFNodes(std::string_view mesh_file, const tinygltf::Model& model, + const tinygltf::Node& node, const Common::Matrix44& mat, MeshData* data) +{ + if (node.mesh != -1) + { + if (!ReadGLTFMesh(mesh_file, model, model.meshes[node.mesh], mat, data)) + return false; + } + + for (std::size_t i = 0; i < node.children.size(); i++) + { + const tinygltf::Node& child = model.nodes[node.children[i]]; + const auto child_mat = mat * BuildMatrixFromNode(child); + if (!ReadGLTFNodes(mesh_file, model, child, child_mat, data)) + return false; + } + + return true; +} + +bool ReadGLTFMaterials(std::string_view mesh_file, const tinygltf::Model& model, MeshData* data) +{ + for (std::size_t i = 0; i < model.materials.size(); i++) + { + const tinygltf::Material& material = model.materials[i]; + + // TODO: support converting material data into Dolphin material assets + data->m_mesh_material_to_material_asset_id.insert_or_assign(material.name, ""); + } + + return true; +} + +bool ReadGLTF(std::string_view mesh_file, const tinygltf::Model& model, MeshData* data) +{ + int scene_index = model.defaultScene; + if (scene_index == -1) + scene_index = 0; + + const auto& scene = model.scenes[scene_index]; + const auto scene_node_indices = scene.nodes; + for (std::size_t i = 0; i < scene_node_indices.size(); i++) + { + const tinygltf::Node& node = model.nodes[scene_node_indices[i]]; + const auto mat = BuildMatrixFromNode(node); + if (!ReadGLTFNodes(mesh_file, model, node, mat, data)) + return false; + } + + return ReadGLTFMaterials(mesh_file, model, data); +} +} // namespace +bool MeshData::FromJson(const VideoCommon::CustomAssetLibrary::AssetID& asset_id, + const picojson::object& json, MeshData* data) +{ + if (const auto iter = json.find("material_mapping"); iter != json.end()) + { + if (!iter->second.is()) + { + ERROR_LOG_FMT( + VIDEO, + "Asset '{}' failed to parse json, expected 'material_mapping' to be of type object", + asset_id); + return false; + } + + for (const auto& [material_name, asset_id_json] : iter->second.get()) + { + if (!asset_id_json.is()) + { + ERROR_LOG_FMT( + VIDEO, + "Asset '{}' failed to parse json, material name '{}' linked to a non-string value", + asset_id, material_name); + return false; + } + + data->m_mesh_material_to_material_asset_id[material_name] = asset_id_json.to_str(); + } + } + return true; +} + +void MeshData::ToJson(picojson::object& obj, const MeshData& data) +{ + picojson::object material_mapping; + for (const auto& [material_name, asset_id] : data.m_mesh_material_to_material_asset_id) + { + material_mapping.emplace(material_name, asset_id); + } + obj.emplace("material_mapping", std::move(material_mapping)); +} + +bool MeshData::FromDolphinMesh(std::span raw_data, MeshData* data) +{ + std::size_t offset = 0; + + std::size_t chunk_size = 0; + std::memcpy(&chunk_size, raw_data.data(), sizeof(std::size_t)); + offset += sizeof(std::size_t); + + data->m_mesh_chunks.reserve(chunk_size); + for (std::size_t i = 0; i < chunk_size; i++) + { + MeshDataChunk chunk; + + std::memcpy(&chunk.num_vertices, raw_data.data() + offset, sizeof(u32)); + offset += sizeof(u32); + + std::memcpy(&chunk.vertex_stride, raw_data.data() + offset, sizeof(u32)); + offset += sizeof(u32); + + // TODO C++23: use make_unique_overwrite + chunk.vertex_data = std::unique_ptr(new u8[chunk.num_vertices * chunk.vertex_stride]); + std::memcpy(chunk.vertex_data.get(), raw_data.data() + offset, + chunk.num_vertices * chunk.vertex_stride); + offset += chunk.num_vertices * chunk.vertex_stride; + + std::memcpy(&chunk.num_indices, raw_data.data() + offset, sizeof(u32)); + offset += sizeof(u32); + + // TODO C++23: use make_unique_overwrite + chunk.indices = std::unique_ptr(new u16[chunk.num_indices]); + std::memcpy(chunk.indices.get(), raw_data.data() + offset, chunk.num_indices * sizeof(u16)); + offset += chunk.num_indices * sizeof(u16); + + std::memcpy(&chunk.vertex_declaration, raw_data.data() + offset, + sizeof(PortableVertexDeclaration)); + offset += sizeof(PortableVertexDeclaration); + + std::memcpy(&chunk.primitive_type, raw_data.data() + offset, sizeof(PrimitiveType)); + offset += sizeof(PrimitiveType); + + std::memcpy(&chunk.components_available, raw_data.data() + offset, sizeof(u32)); + offset += sizeof(u32); + + std::memcpy(&chunk.minimum_position, raw_data.data() + offset, sizeof(Common::Vec3)); + offset += sizeof(Common::Vec3); + + std::memcpy(&chunk.maximum_position, raw_data.data() + offset, sizeof(Common::Vec3)); + offset += sizeof(Common::Vec3); + + std::memcpy(&chunk.transform.data[0], raw_data.data() + offset, + chunk.transform.data.size() * sizeof(float)); + offset += chunk.transform.data.size() * sizeof(float); + + std::size_t material_name_size = 0; + std::memcpy(&material_name_size, raw_data.data() + offset, sizeof(std::size_t)); + offset += sizeof(std::size_t); + + chunk.material_name.assign(raw_data.data() + offset, + raw_data.data() + offset + material_name_size); + offset += material_name_size * sizeof(char); + + data->m_mesh_chunks.push_back(std::move(chunk)); + } + + return true; +} + +bool MeshData::ToDolphinMesh(File::IOFile* file_data, const MeshData& data) +{ + const std::size_t chunk_size = data.m_mesh_chunks.size(); + file_data->WriteBytes(&chunk_size, sizeof(std::size_t)); + for (const auto& chunk : data.m_mesh_chunks) + { + if (!file_data->WriteBytes(&chunk.num_vertices, sizeof(u32))) + return false; + if (!file_data->WriteBytes(&chunk.vertex_stride, sizeof(u32))) + return false; + if (!file_data->WriteBytes(chunk.vertex_data.get(), chunk.num_vertices * chunk.vertex_stride)) + return false; + if (!file_data->WriteBytes(&chunk.num_indices, sizeof(u32))) + return false; + if (!file_data->WriteBytes(chunk.indices.get(), chunk.num_indices * sizeof(u16))) + return false; + if (!file_data->WriteBytes(&chunk.vertex_declaration, sizeof(PortableVertexDeclaration))) + return false; + if (!file_data->WriteBytes(&chunk.primitive_type, sizeof(PrimitiveType))) + return false; + if (!file_data->WriteBytes(&chunk.components_available, sizeof(u32))) + return false; + if (!file_data->WriteBytes(&chunk.minimum_position, sizeof(Common::Vec3))) + return false; + if (!file_data->WriteBytes(&chunk.maximum_position, sizeof(Common::Vec3))) + return false; + if (!file_data->WriteBytes(&chunk.transform.data[0], + chunk.transform.data.size() * sizeof(float))) + { + return false; + } + + const std::size_t material_name_size = chunk.material_name.size(); + if (!file_data->WriteBytes(&material_name_size, sizeof(std::size_t))) + return false; + if (!file_data->WriteBytes(&chunk.material_name[0], chunk.material_name.size() * sizeof(char))) + return false; + } + return true; +} + +bool MeshData::FromGLTF(std::string_view gltf_file, MeshData* data) +{ + if (gltf_file.ends_with(".glb")) + { + ERROR_LOG_FMT(VIDEO, "File '{}' with glb extension is not supported at this time", gltf_file); + return false; + } + + if (gltf_file.ends_with(".gltf")) + { + tinygltf::Model model; + tinygltf::TinyGLTF loader; + std::string model_errors; + std::string model_warnings; + if (!loader.LoadASCIIFromFile(&model, &model_errors, &model_warnings, std::string{gltf_file})) + { + ERROR_LOG_FMT(VIDEO, "File '{}' was invalid GLTF, error: {}, warning: {}", gltf_file, + model_errors, model_warnings); + return false; + } + return ReadGLTF(gltf_file, model, data); + } + + ERROR_LOG_FMT(VIDEO, "GLTF '{}' has invalid extension", gltf_file); + return false; +} +} // namespace VideoCommon diff --git a/Source/Core/VideoCommon/Assets/MeshAsset.h b/Source/Core/VideoCommon/Assets/MeshAsset.h new file mode 100644 index 0000000000..eb23f39783 --- /dev/null +++ b/Source/Core/VideoCommon/Assets/MeshAsset.h @@ -0,0 +1,60 @@ +// Copyright 2023 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include "Common/CommonTypes.h" +#include "Common/Matrix.h" + +#include "VideoCommon/Assets/CustomAsset.h" +#include "VideoCommon/NativeVertexFormat.h" +#include "VideoCommon/RenderState.h" + +namespace File +{ +class IOFile; +} + +namespace VideoCommon +{ +struct MeshDataChunk +{ + std::unique_ptr vertex_data; + u32 vertex_stride; + u32 num_vertices; + std::unique_ptr indices; + u32 num_indices; + PortableVertexDeclaration vertex_declaration; + PrimitiveType primitive_type; + u32 components_available; + Common::Vec3 minimum_position; + Common::Vec3 maximum_position; + Common::Matrix44 transform; + std::string material_name; +}; + +struct MeshData +{ + static bool FromJson(const CustomAssetLibrary::AssetID& asset_id, const picojson::object& json, + MeshData* data); + static void ToJson(picojson::object& obj, const MeshData& data); + + static bool FromDolphinMesh(std::span raw_data, MeshData* data); + + static bool ToDolphinMesh(File::IOFile* file_data, const MeshData& data); + + static bool FromGLTF(std::string_view gltf_file, MeshData* data); + + std::vector m_mesh_chunks; + std::map> + m_mesh_material_to_material_asset_id; +}; +} // namespace VideoCommon diff --git a/Source/Core/VideoCommon/CMakeLists.txt b/Source/Core/VideoCommon/CMakeLists.txt index 36cf30a9d1..89ec66d048 100644 --- a/Source/Core/VideoCommon/CMakeLists.txt +++ b/Source/Core/VideoCommon/CMakeLists.txt @@ -20,6 +20,8 @@ add_library(videocommon Assets/DirectFilesystemAssetLibrary.h Assets/MaterialAsset.cpp Assets/MaterialAsset.h + Assets/MeshAsset.cpp + Assets/MeshAsset.h Assets/ShaderAsset.cpp Assets/ShaderAsset.h Assets/TextureAsset.cpp @@ -216,6 +218,7 @@ PRIVATE imgui implot glslang + tinygltf ) if(_M_X86_64) diff --git a/Source/dolphin-emu.sln b/Source/dolphin-emu.sln index a123e396dd..d4a6de1913 100644 --- a/Source/dolphin-emu.sln +++ b/Source/dolphin-emu.sln @@ -91,6 +91,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "spng", "..\Externals\libspn EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "rcheevos", "..\Externals\rcheevos\rcheevos.vcxproj", "{CC99A910-3752-4465-95AA-7DC240D92A99}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "tinygltf", "..\Externals\tinygltf\tinygltf.vcxproj", "{8BDA3693-4999-4D11-9E52-8D08C30B643A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -439,6 +441,14 @@ Global {CC99A910-3752-4465-95AA-7DC240D92A99}.Release|ARM64.Build.0 = Release|ARM64 {CC99A910-3752-4465-95AA-7DC240D92A99}.Release|x64.ActiveCfg = Release|x64 {CC99A910-3752-4465-95AA-7DC240D92A99}.Release|x64.Build.0 = Release|x64 + {8BDA3693-4999-4D11-9E52-8D08C30B643A}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {8BDA3693-4999-4D11-9E52-8D08C30B643A}.Debug|ARM64.Build.0 = Debug|ARM64 + {8BDA3693-4999-4D11-9E52-8D08C30B643A}.Debug|x64.ActiveCfg = Debug|x64 + {8BDA3693-4999-4D11-9E52-8D08C30B643A}.Debug|x64.Build.0 = Debug|x64 + {8BDA3693-4999-4D11-9E52-8D08C30B643A}.Release|ARM64.ActiveCfg = Release|ARM64 + {8BDA3693-4999-4D11-9E52-8D08C30B643A}.Release|ARM64.Build.0 = Release|ARM64 + {8BDA3693-4999-4D11-9E52-8D08C30B643A}.Release|x64.ActiveCfg = Release|x64 + {8BDA3693-4999-4D11-9E52-8D08C30B643A}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -477,6 +487,7 @@ Global {3F17D282-A77D-4931-B844-903AD0809A5E} = {87ADDFF9-5768-4DA2-A33B-2477593D6677} {447B7B1E-1D74-4AEF-B2B9-6EB41C5D5313} = {87ADDFF9-5768-4DA2-A33B-2477593D6677} {CC99A910-3752-4465-95AA-7DC240D92A99} = {87ADDFF9-5768-4DA2-A33B-2477593D6677} + {8BDA3693-4999-4D11-9E52-8D08C30B643A} = {87ADDFF9-5768-4DA2-A33B-2477593D6677} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {64B0A343-3B94-4522-9C24-6937FE5EFB22}