#include "vkcv/asset/asset_loader.hpp" #include <iostream> #include <string.h> // memcpy(3) #include <set> #include <stdlib.h> // calloc(3) #include <vulkan/vulkan.hpp> #include <fx/gltf.h> #include <stb_image.h> #include <vkcv/Logger.hpp> #include <algorithm> namespace vkcv::asset { /** * This function unrolls nested exceptions via recursion and prints them * @param e The exception being thrown * @param path The path to the file that was responsible for the exception */ static void recurseExceptionPrint(const std::exception& e, const std::string &path) { vkcv_log(LogLevel::ERROR, "Loading file %s: %s", path.c_str(), e.what()); try { std::rethrow_if_nested(e); } catch (const std::exception& nested) { recurseExceptionPrint(nested, path); } } /** * Returns the component count for an accessor type of the fx-gltf library. * @param type The accessor type * @return An unsigned integer count */ static uint32_t getComponentCount(const fx::gltf::Accessor::Type type) { switch (type) { case fx::gltf::Accessor::Type::Scalar: return 1; case fx::gltf::Accessor::Type::Vec2: return 2; case fx::gltf::Accessor::Type::Vec3: return 3; case fx::gltf::Accessor::Type::Vec4: case fx::gltf::Accessor::Type::Mat2: return 4; case fx::gltf::Accessor::Type::Mat3: return 9; case fx::gltf::Accessor::Type::Mat4: return 16; case fx::gltf::Accessor::Type::None: default: return 0; } } static uint32_t getComponentSize(ComponentType type) { switch (type) { case ComponentType::INT8: case ComponentType::UINT8: return 1; case ComponentType::INT16: case ComponentType::UINT16: return 2; case ComponentType::UINT32: case ComponentType::FLOAT32: return 4; default: return 0; } } /** * Translate the component type used in the index accessor of fx-gltf to our * enum for index type. The reason we have defined an incompatible enum that * needs translation is that only a subset of component types is valid for * indices and we want to catch these incompatibilities here. * @param t The component type * @return The vkcv::IndexType enum representation */ enum IndexType getIndexType(const enum fx::gltf::Accessor::ComponentType &type) { switch (type) { case fx::gltf::Accessor::ComponentType::UnsignedByte: return IndexType::UINT8; case fx::gltf::Accessor::ComponentType::UnsignedShort: return IndexType::UINT16; case fx::gltf::Accessor::ComponentType::UnsignedInt: return IndexType::UINT32; default: vkcv_log(LogLevel::ERROR, "Index type not supported: %u", static_cast<uint16_t>(type)); return IndexType::UNDEFINED; } } /** * This function fills the array of vertex attributes of a VertexGroup (usually * part of a vkcv::asset::Mesh) object based on the description of attributes * for a fx::gltf::Primitive. * * @param src The description of attribute objects from the fx-gltf library * @param gltf The main glTF document * @param dst The array of vertex attributes stored in an asset::Mesh object * @return ASSET_ERROR when at least one VertexAttribute could not be * constructed properly, otherwise ASSET_SUCCESS */ static int loadVertexAttributes(const fx::gltf::Attributes &src, const std::vector<fx::gltf::Accessor> &accessors, const std::vector<fx::gltf::BufferView> &bufferViews, std::vector<VertexAttribute> &dst) { for (const auto &attrib : src) { VertexAttribute att {}; if (attrib.first == "POSITION") { att.type = PrimitiveType::POSITION; } else if (attrib.first == "NORMAL") { att.type = PrimitiveType::NORMAL; } else if (attrib.first == "TANGENT") { att.type = PrimitiveType::TANGENT; } else if (attrib.first == "TEXCOORD_0") { att.type = PrimitiveType::TEXCOORD_0; } else if (attrib.first == "TEXCOORD_1") { att.type = PrimitiveType::TEXCOORD_1; } else if (attrib.first == "COLOR_0") { att.type = PrimitiveType::COLOR_0; } else if (attrib.first == "COLOR_1") { att.type = PrimitiveType::COLOR_1; } else if (attrib.first == "JOINTS_0") { att.type = PrimitiveType::JOINTS_0; } else if (attrib.first == "WEIGHTS_0") { att.type = PrimitiveType::WEIGHTS_0; } else { att.type = PrimitiveType::UNDEFINED; } if (att.type != PrimitiveType::UNDEFINED) { const fx::gltf::Accessor &accessor = accessors[attrib.second]; const fx::gltf::BufferView &buf = bufferViews[accessor.bufferView]; att.offset = buf.byteOffset; att.length = buf.byteLength; att.stride = buf.byteStride; att.componentType = static_cast<ComponentType>(accessor.componentType); att.componentCount = getComponentCount(accessor.type); /* Assume tightly packed stride as not explicitly provided */ if (att.stride == 0) { att.stride = att.componentCount * getComponentSize(att.componentType); } } if ((att.type == PrimitiveType::UNDEFINED) || (att.componentCount == 0)) { return ASSET_ERROR; } dst.push_back(att); } return ASSET_SUCCESS; } /** * This function calculates the modelMatrix out of the data given in the gltf file. * It also checks, whether a modelMatrix was given. * * @param translation possible translation vector (default 0,0,0) * @param scale possible scale vector (default 1,1,1) * @param rotation possible rotation, given in quaternion (default 0,0,0,1) * @param matrix possible modelmatrix (default identity) * @return model Matrix as an array of floats */ static std::array<float, 16> calculateModelMatrix(const std::array<float, 3>& translation, const std::array<float, 3>& scale, const std::array<float, 4>& rotation, const std::array<float, 16>& matrix){ std::array<float, 16> modelMatrix = { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 }; if (matrix != modelMatrix){ return matrix; } else { // translation modelMatrix[3] = translation[0]; modelMatrix[7] = translation[1]; modelMatrix[11] = translation[2]; // rotation and scale auto a = rotation[0]; auto q1 = rotation[1]; auto q2 = rotation[2]; auto q3 = rotation[3]; modelMatrix[0] = (2 * (a * a + q1 * q1) - 1) * scale[0]; modelMatrix[1] = (2 * (q1 * q2 - a * q3)) * scale[1]; modelMatrix[2] = (2 * (q1 * q3 + a * q2)) * scale[2]; modelMatrix[4] = (2 * (q1 * q2 + a * q3)) * scale[0]; modelMatrix[5] = (2 * (a * a + q2 * q2) - 1) * scale[1]; modelMatrix[6] = (2 * (q2 * q3 - a * q1)) * scale[2]; modelMatrix[8] = (2 * (q1 * q3 - a * q2)) * scale[0]; modelMatrix[9] = (2 * (q2 * q3 + a * q1)) * scale[1]; modelMatrix[10] = (2 * (a * a + q3 * q3) - 1) * scale[2]; // flip y, because GLTF uses y up, but vulkan -y up modelMatrix[5] *= -1; return modelMatrix; } } bool Material::hasTexture(const PBRTextureTarget target) const { return textureMask & bitflag(target); } /** * This function translates a given fx-gltf-sampler-wrapping-mode-enum to its vulkan sampler-adress-mode counterpart. * @param mode: wrapping mode of a sampler given as fx-gltf-enum * @return int vulkan-enum representing the same wrapping mode */ static int translateSamplerMode(const fx::gltf::Sampler::WrappingMode mode) { switch (mode) { case fx::gltf::Sampler::WrappingMode::ClampToEdge: return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; case fx::gltf::Sampler::WrappingMode::MirroredRepeat: return VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT; case fx::gltf::Sampler::WrappingMode::Repeat: default: return VK_SAMPLER_ADDRESS_MODE_REPEAT; } } /** * If the glTF doesn't define samplers, we use the defaults defined by fx-gltf. * The following are details about the glTF/OpenGL to Vulkan translation. * magFilter (VkFilter?): * GL_NEAREST -> VK_FILTER_NEAREST * GL_LINEAR -> VK_FILTER_LINEAR * minFilter (VkFilter?): * mipmapMode (VkSamplerMipmapMode?): * Vulkans minFilter and mipmapMode combined correspond to OpenGLs * GL_minFilter_MIPMAP_mipmapMode: * GL_NEAREST_MIPMAP_NEAREST: * minFilter=VK_FILTER_NEAREST * mipmapMode=VK_SAMPLER_MIPMAP_MODE_NEAREST * GL_LINEAR_MIPMAP_NEAREST: * minFilter=VK_FILTER_LINEAR * mipmapMode=VK_SAMPLER_MIPMAP_MODE_NEAREST * GL_NEAREST_MIPMAP_LINEAR: * minFilter=VK_FILTER_NEAREST * mipmapMode=VK_SAMPLER_MIPMAP_MODE_LINEAR * GL_LINEAR_MIPMAP_LINEAR: * minFilter=VK_FILTER_LINEAR * mipmapMode=VK_SAMPLER_MIPMAP_MODE_LINEAR * The modes of GL_LINEAR and GL_NEAREST have to be emulated using * mipmapMode=VK_SAMPLER_MIPMAP_MODE_NEAREST with specific minLOD and maxLOD: * GL_LINEAR: * minFilter=VK_FILTER_LINEAR * mipmapMode=VK_SAMPLER_MIPMAP_MODE_NEAREST * minLOD=0, maxLOD=0.25 * GL_NEAREST: * minFilter=VK_FILTER_NEAREST * mipmapMode=VK_SAMPLER_MIPMAP_MODE_NEAREST * minLOD=0, maxLOD=0.25 * Setting maxLOD=0 causes magnification to always be performed (using * the defined magFilter), this may be valid if the min- and magFilter * are equal, otherwise it won't be the expected behaviour from OpenGL * and glTF; instead using maxLod=0.25 allows the minFilter to be * performed while still always rounding to the base level. * With other modes, minLOD and maxLOD default to: * minLOD=0 * maxLOD=VK_LOD_CLAMP_NONE * wrapping: * gltf has wrapS, wrapT with {clampToEdge, MirroredRepeat, Repeat} while * Vulkan has addressModeU, addressModeV, addressModeW with values * VK_SAMPLER_ADDRESS_MODE_{REPEAT,MIRRORED_REPEAT,CLAMP_TO_EDGE, * CAMP_TO_BORDER,MIRROR_CLAMP_TO_EDGE} * Translation from glTF to Vulkan is straight forward for the 3 existing * modes, default is repeat, the other modes aren't available. */ static vkcv::asset::Sampler loadSampler(const fx::gltf::Sampler &src) { Sampler dst {}; dst.minLOD = 0; dst.maxLOD = VK_LOD_CLAMP_NONE; switch (src.minFilter) { case fx::gltf::Sampler::MinFilter::None: case fx::gltf::Sampler::MinFilter::Nearest: dst.minFilter = VK_FILTER_NEAREST; dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; dst.maxLOD = 0.25; break; case fx::gltf::Sampler::MinFilter::Linear: dst.minFilter = VK_FILTER_LINEAR; dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; dst.maxLOD = 0.25; break; case fx::gltf::Sampler::MinFilter::NearestMipMapNearest: dst.minFilter = VK_FILTER_NEAREST; dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; break; case fx::gltf::Sampler::MinFilter::LinearMipMapNearest: dst.minFilter = VK_FILTER_LINEAR; dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; break; case fx::gltf::Sampler::MinFilter::NearestMipMapLinear: dst.minFilter = VK_FILTER_NEAREST; dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; break; case fx::gltf::Sampler::MinFilter::LinearMipMapLinear: dst.minFilter = VK_FILTER_LINEAR; dst.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; break; default: break; } switch (src.magFilter) { case fx::gltf::Sampler::MagFilter::None: case fx::gltf::Sampler::MagFilter::Nearest: dst.magFilter = VK_FILTER_NEAREST; break; case fx::gltf::Sampler::MagFilter::Linear: dst.magFilter = VK_FILTER_LINEAR; break; default: break; } dst.addressModeU = translateSamplerMode(src.wrapS); dst.addressModeV = translateSamplerMode(src.wrapT); // There is no information about wrapping for a third axis in glTF and // we have to hardcode this value. dst.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; return dst; } /** * Initializes vertex groups of a Mesh, including copying the data to * index- and vertex-buffers. */ static int loadVertexGroups(const fx::gltf::Mesh &objectMesh, const fx::gltf::Document &sceneObjects, Scene &scene, Mesh &mesh) { mesh.vertexGroups.reserve(objectMesh.primitives.size()); for (const auto &objectPrimitive : objectMesh.primitives) { VertexGroup vertexGroup; vertexGroup.vertexBuffer.attributes.reserve( objectPrimitive.attributes.size() ); if (ASSET_SUCCESS != loadVertexAttributes( objectPrimitive.attributes, sceneObjects.accessors, sceneObjects.bufferViews, vertexGroup.vertexBuffer.attributes)) { vkcv_log(LogLevel::ERROR, "Failed to get vertex attributes of '%s'", mesh.name.c_str()); return ASSET_ERROR; } // The accessor for the position attribute is used for // 1) getting the vertex buffer view which is only needed to get // the vertex buffer // 2) getting the vertex count for the VertexGroup // 3) getting the min/max of the bounding box for the VertexGroup fx::gltf::Accessor posAccessor; bool noPosition = true; for (auto const& attrib : objectPrimitive.attributes) { if (attrib.first == "POSITION") { posAccessor = sceneObjects.accessors[attrib.second]; noPosition = false; break; } } if (noPosition) { vkcv_log(LogLevel::ERROR, "Position attribute not found from '%s'", mesh.name.c_str()); return ASSET_ERROR; } const fx::gltf::Accessor& indexAccessor = sceneObjects.accessors[objectPrimitive.indices]; int indexBufferURI; if (objectPrimitive.indices >= 0) { // if there is no index buffer, -1 is returned from fx-gltf const fx::gltf::BufferView& indexBufferView = sceneObjects.bufferViews[indexAccessor.bufferView]; const fx::gltf::Buffer& indexBuffer = sceneObjects.buffers[indexBufferView.buffer]; // Because the buffers are already preloaded into the memory by the gltf-library, // it makes no sense to load them later on manually again into memory. vertexGroup.indexBuffer.data.resize(indexBufferView.byteLength); memcpy(vertexGroup.indexBuffer.data.data(), indexBuffer.data.data() + indexBufferView.byteOffset, indexBufferView.byteLength); } else { indexBufferURI = -1; } vertexGroup.indexBuffer.type = getIndexType(indexAccessor.componentType); if (IndexType::UNDEFINED == vertexGroup.indexBuffer.type) { vkcv_log(LogLevel::ERROR, "Index Type undefined or not supported."); return ASSET_ERROR; } if (posAccessor.bufferView >= sceneObjects.bufferViews.size()) { vkcv_log(LogLevel::ERROR, "Access to bufferView out of bounds: %d", posAccessor.bufferView); return ASSET_ERROR; } const fx::gltf::BufferView& vertexBufferView = sceneObjects.bufferViews[posAccessor.bufferView]; if (vertexBufferView.buffer >= sceneObjects.buffers.size()) { vkcv_log(LogLevel::ERROR, "Access to buffer out of bounds: %d", vertexBufferView.buffer); return ASSET_ERROR; } const fx::gltf::Buffer& vertexBuffer = sceneObjects.buffers[vertexBufferView.buffer]; // only copy relevant part of vertex data uint32_t relevantBufferOffset = std::numeric_limits<uint32_t>::max(); uint32_t relevantBufferEnd = 0; for (const auto& attribute : vertexGroup.vertexBuffer.attributes) { relevantBufferOffset = std::min(attribute.offset, relevantBufferOffset); relevantBufferEnd = std::max(relevantBufferEnd, attribute.offset + attribute.length); } const uint32_t relevantBufferSize = relevantBufferEnd - relevantBufferOffset; vertexGroup.vertexBuffer.data.resize(relevantBufferSize); memcpy(vertexGroup.vertexBuffer.data.data(), vertexBuffer.data.data() + relevantBufferOffset, relevantBufferSize); // make vertex attributes relative to copied section for (auto& attribute : vertexGroup.vertexBuffer.attributes) { attribute.offset -= relevantBufferOffset; } vertexGroup.mode = static_cast<PrimitiveMode>(objectPrimitive.mode); vertexGroup.numIndices = sceneObjects.accessors[objectPrimitive.indices].count; vertexGroup.numVertices = posAccessor.count; memcpy(&(vertexGroup.min), posAccessor.min.data(), sizeof(vertexGroup.min)); memcpy(&(vertexGroup.max), posAccessor.max.data(), sizeof(vertexGroup.max)); vertexGroup.materialIndex = static_cast<uint8_t>(objectPrimitive.material); mesh.vertexGroups.push_back(static_cast<int>(scene.vertexGroups.size())); scene.vertexGroups.push_back(vertexGroup); } return ASSET_SUCCESS; } /** * Returns an integer with specific bits set corresponding to the * textures that appear in the given material. This mask is used in the * vkcv::asset::Material struct and can be tested via the hasTexture * method. */ static uint16_t generateTextureMask(fx::gltf::Material &material) { uint16_t textureMask = 0; if (material.pbrMetallicRoughness.baseColorTexture.index >= 0) { textureMask |= bitflag(asset::PBRTextureTarget::baseColor); } if (material.pbrMetallicRoughness.metallicRoughnessTexture.index >= 0) { textureMask |= bitflag(asset::PBRTextureTarget::metalRough); } if (material.normalTexture.index >= 0) { textureMask |= bitflag(asset::PBRTextureTarget::normal); } if (material.occlusionTexture.index >= 0) { textureMask |= bitflag(asset::PBRTextureTarget::occlusion); } if (material.emissiveTexture.index >= 0) { textureMask |= bitflag(asset::PBRTextureTarget::emissive); } return textureMask; } int probeScene(const std::filesystem::path& path, Scene& scene) { fx::gltf::Document sceneObjects; try { if (path.extension() == ".glb") { sceneObjects = fx::gltf::LoadFromBinary(path.string()); } else { sceneObjects = fx::gltf::LoadFromText(path.string()); } } catch (const std::system_error& err) { recurseExceptionPrint(err, path.string()); return ASSET_ERROR; } catch (const std::exception& e) { recurseExceptionPrint(e, path.string()); return ASSET_ERROR; } const auto directory = path.parent_path(); scene.meshes.clear(); scene.vertexGroups.clear(); scene.materials.clear(); scene.textures.clear(); scene.samplers.clear(); // file has to contain at least one mesh if (sceneObjects.meshes.empty()) { vkcv_log(LogLevel::ERROR, "No meshes found! (%s)", path.c_str()); return ASSET_ERROR; } else { scene.meshes.reserve(sceneObjects.meshes.size()); for (size_t i = 0; i < sceneObjects.meshes.size(); i++) { Mesh mesh; mesh.name = sceneObjects.meshes[i].name; if (loadVertexGroups(sceneObjects.meshes[i], sceneObjects, scene, mesh) != ASSET_SUCCESS) { vkcv_log(LogLevel::ERROR, "Failed to load vertex groups of '%s'! (%s)", mesh.name.c_str(), path.c_str()); return ASSET_ERROR; } scene.meshes.push_back(mesh); } // This only works if the node has a mesh and it only loads the meshes and ignores cameras and lights for (const auto& node : sceneObjects.nodes) { if ((node.mesh >= 0) && (node.mesh < scene.meshes.size())) { scene.meshes[node.mesh].modelMatrix = calculateModelMatrix( node.translation, node.scale, node.rotation, node.matrix ); } } } if (sceneObjects.samplers.empty()) { vkcv_log(LogLevel::WARNING, "No samplers found! (%s)", path.c_str()); } else { scene.samplers.reserve(sceneObjects.samplers.size()); for (const auto &samplerObject : sceneObjects.samplers) { scene.samplers.push_back(loadSampler(samplerObject)); } } if (sceneObjects.textures.empty()) { vkcv_log(LogLevel::WARNING, "No textures found! (%s)", path.c_str()); } else { scene.textures.reserve(sceneObjects.textures.size()); for (const auto& textureObject : sceneObjects.textures) { Texture texture; if (textureObject.sampler < 0) { texture.sampler = -1; } else if (static_cast<size_t>(textureObject.sampler) >= scene.samplers.size()) { vkcv_log(LogLevel::ERROR, "Sampler of texture '%s' missing (%s)", textureObject.name.c_str(), path.c_str()); return ASSET_ERROR; } else { texture.sampler = textureObject.sampler; } if ((textureObject.source < 0) || (static_cast<size_t>(textureObject.source) >= sceneObjects.images.size())) { vkcv_log(LogLevel::ERROR, "Failed to load texture '%s' (%s)", textureObject.name.c_str(), path.c_str()); return ASSET_ERROR; } const auto& image = sceneObjects.images[textureObject.source]; if (image.uri.empty()) { const fx::gltf::BufferView bufferView = sceneObjects.bufferViews[image.bufferView]; texture.path.clear(); texture.data.resize(bufferView.byteLength); memcpy(texture.data.data(), sceneObjects.buffers[bufferView.buffer].data.data() + bufferView.byteOffset, bufferView.byteLength); } else { texture.path = directory / image.uri; } scene.textures.push_back(texture); } } if (sceneObjects.materials.empty()) { vkcv_log(LogLevel::WARNING, "No materials found! (%s)", path.c_str()); } else { scene.materials.reserve(sceneObjects.materials.size()); for (auto material : sceneObjects.materials) { scene.materials.push_back({ generateTextureMask(material), material.pbrMetallicRoughness.baseColorTexture.index, material.pbrMetallicRoughness.metallicRoughnessTexture.index, material.normalTexture.index, material.occlusionTexture.index, material.emissiveTexture.index, { material.pbrMetallicRoughness.baseColorFactor[0], material.pbrMetallicRoughness.baseColorFactor[1], material.pbrMetallicRoughness.baseColorFactor[2], material.pbrMetallicRoughness.baseColorFactor[3] }, material.pbrMetallicRoughness.metallicFactor, material.pbrMetallicRoughness.roughnessFactor, material.normalTexture.scale, material.occlusionTexture.strength, { material.emissiveFactor[0], material.emissiveFactor[1], material.emissiveFactor[2] } }); } } return ASSET_SUCCESS; } /** * Loads and decodes the textures data based on the textures file path. * The path member is the only one that has to be initialized before * calling this function, the others (width, height, channels, data) * are set by this function and the sampler is of no concern here. */ static int loadTextureData(Texture& texture) { if ((texture.width > 0) && (texture.height > 0) && (texture.channels > 0) && (!texture.data.empty())) { return ASSET_SUCCESS; // Texture data was loaded already! } uint8_t* data; if (texture.path.empty()) { data = stbi_load_from_memory( reinterpret_cast<uint8_t*>(texture.data.data()), static_cast<int>(texture.data.size()), &texture.width, &texture.height, &texture.channels, 4 ); } else { data = stbi_load( texture.path.string().c_str(), &texture.width, &texture.height, &texture.channels, 4 ); } if (!data) { vkcv_log(LogLevel::ERROR, "Texture could not be loaded from '%s'", texture.path.c_str()); texture.width = 0; texture.height = 0; texture.channels = 0; return ASSET_ERROR; } texture.data.resize(texture.width * texture.height * 4); memcpy(texture.data.data(), data, texture.data.size()); stbi_image_free(data); return ASSET_SUCCESS; } int loadMesh(Scene &scene, int index) { if ((index < 0) || (static_cast<size_t>(index) >= scene.meshes.size())) { vkcv_log(LogLevel::ERROR, "Mesh index out of range: %d", index); return ASSET_ERROR; } const Mesh &mesh = scene.meshes[index]; for (const auto& vg : mesh.vertexGroups) { const VertexGroup &vertexGroup = scene.vertexGroups[vg]; const Material& material = scene.materials[vertexGroup.materialIndex]; if (material.hasTexture(PBRTextureTarget::baseColor)) { const int result = loadTextureData(scene.textures[material.baseColor]); if (ASSET_SUCCESS != result) { vkcv_log(LogLevel::ERROR, "Failed loading baseColor texture of mesh '%s'", mesh.name.c_str()) return result; } } if (material.hasTexture(PBRTextureTarget::metalRough)) { const int result = loadTextureData(scene.textures[material.metalRough]); if (ASSET_SUCCESS != result) { vkcv_log(LogLevel::ERROR, "Failed loading metalRough texture of mesh '%s'", mesh.name.c_str()) return result; } } if (material.hasTexture(PBRTextureTarget::normal)) { const int result = loadTextureData(scene.textures[material.normal]); if (ASSET_SUCCESS != result) { vkcv_log(LogLevel::ERROR, "Failed loading normal texture of mesh '%s'", mesh.name.c_str()) return result; } } if (material.hasTexture(PBRTextureTarget::occlusion)) { const int result = loadTextureData(scene.textures[material.occlusion]); if (ASSET_SUCCESS != result) { vkcv_log(LogLevel::ERROR, "Failed loading occlusion texture of mesh '%s'", mesh.name.c_str()) return result; } } if (material.hasTexture(PBRTextureTarget::emissive)) { const int result = loadTextureData(scene.textures[material.emissive]); if (ASSET_SUCCESS != result) { vkcv_log(LogLevel::ERROR, "Failed loading emissive texture of mesh '%s'", mesh.name.c_str()) return result; } } } return ASSET_SUCCESS; } int loadScene(const std::filesystem::path &path, Scene &scene) { int result = probeScene(path, scene); if (result != ASSET_SUCCESS) { vkcv_log(LogLevel::ERROR, "Loading scene failed '%s'", path.c_str()); return result; } for (size_t i = 0; i < scene.meshes.size(); i++) { result = loadMesh(scene, static_cast<int>(i)); if (result != ASSET_SUCCESS) { vkcv_log(LogLevel::ERROR, "Loading mesh with index %d failed '%s'", static_cast<int>(i), path.c_str()); return result; } } return ASSET_SUCCESS; } Texture loadTexture(const std::filesystem::path& path) { Texture texture; texture.path = path; texture.sampler = -1; if (loadTextureData(texture) != ASSET_SUCCESS) { texture.path.clear(); texture.w = texture.h = texture.channels = 0; texture.data.clear(); } return texture; } }