From 36f387a965c57411d7aec5c50b5c8103c7aa4429 Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Wed, 19 Jul 2023 19:12:20 +0200 Subject: [PATCH 01/27] [core] add gltf material asset management --- src/Core/Material/BaseGLTFMaterial.cpp | 17 ++ src/Core/Material/BaseGLTFMaterial.hpp | 165 ++++++++++++++++++ src/Core/Material/GLTFTextureParameters.hpp | 70 ++++++++ .../MetallicRoughnessMaterialData.cpp | 23 +++ .../MetallicRoughnessMaterialData.hpp | 38 ++++ .../SpecularGlossinessMaterialData.cpp | 13 ++ .../SpecularGlossinessMaterialData.hpp | 39 +++++ src/Core/filelist.cmake | 7 + 8 files changed, 372 insertions(+) create mode 100644 src/Core/Material/BaseGLTFMaterial.cpp create mode 100644 src/Core/Material/BaseGLTFMaterial.hpp create mode 100644 src/Core/Material/GLTFTextureParameters.hpp create mode 100644 src/Core/Material/MetallicRoughnessMaterialData.cpp create mode 100644 src/Core/Material/MetallicRoughnessMaterialData.hpp create mode 100644 src/Core/Material/SpecularGlossinessMaterialData.cpp create mode 100644 src/Core/Material/SpecularGlossinessMaterialData.hpp diff --git a/src/Core/Material/BaseGLTFMaterial.cpp b/src/Core/Material/BaseGLTFMaterial.cpp new file mode 100644 index 00000000000..2b694fd8803 --- /dev/null +++ b/src/Core/Material/BaseGLTFMaterial.cpp @@ -0,0 +1,17 @@ +#include +namespace Ra { +namespace Core { +namespace Material { + +BaseGLTFMaterial::BaseGLTFMaterial( const std::string& gltfType, const std::string& instanceName ) : + Ra::Core::Asset::MaterialData( instanceName, gltfType ) { + // extension supported by all gltf materials + // TODO : uncomment the extension supported by the implementation. + /* + allowExtension("KHR_materials_unlit"); + */ +} + +} // namespace Material +} // namespace Core +} // namespace Ra diff --git a/src/Core/Material/BaseGLTFMaterial.hpp b/src/Core/Material/BaseGLTFMaterial.hpp new file mode 100644 index 00000000000..8eb4bbf9a47 --- /dev/null +++ b/src/Core/Material/BaseGLTFMaterial.hpp @@ -0,0 +1,165 @@ +#pragma once +#include + +#include +#include +#include + +#include +namespace Ra { +namespace Core { +namespace Material { + +/// GLTF Alpha mode definition +enum AlphaMode : unsigned int { Opaque = 0, Mask, Blend }; + +/** + * \brief Base class for all official gltf extensions. + * + * Official gltf extensions are listed and specified at + * https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0 + */ +struct RA_CORE_API GLTFMaterialExtensionData : public Ra::Core::Asset::MaterialData { + explicit GLTFMaterialExtensionData( const std::string& gltfType, + const std::string& instanceName ) : + Ra::Core::Asset::MaterialData( instanceName, gltfType ) {} + GLTFMaterialExtensionData() = delete; +}; + +/** + * \brief Definition of the base gltf material representation + */ +class RA_CORE_API BaseGLTFMaterial : public Ra::Core::Asset::MaterialData +{ + public: + /// Attributes of a gltf material + /// Normal texture + std::string m_normalTexture {}; + float m_normalTextureScale { 1 }; + GLTFSampler m_normalSampler {}; + bool m_hasNormalTexture { false }; + mutable std::unique_ptr m_normalTextureTransform { nullptr }; + + /// Occlusion texture + std::string m_occlusionTexture {}; + float m_occlusionStrength { 1 }; + GLTFSampler m_occlusionSampler {}; + bool m_hasOcclusionTexture { false }; + mutable std::unique_ptr m_occlusionTextureTransform { nullptr }; + /// Emissive texture + std::string m_emissiveTexture {}; + Ra::Core::Utils::Color m_emissiveFactor { 0.0, 0.0, 0.0, 1.0 }; + GLTFSampler m_emissiveSampler {}; + bool m_hasEmissiveTexture { false }; + mutable std::unique_ptr m_emissiveTextureTransform { nullptr }; + + /// Transparency parameters + AlphaMode m_alphaMode { AlphaMode::Opaque }; + float m_alphaCutoff { 0.5 }; + + /// Face culling parameter + bool m_doubleSided { false }; + + // Extension data pass through the system + std::map> m_extensions {}; + + explicit BaseGLTFMaterial( const std::string& gltfType, const std::string& instanceName ); + ~BaseGLTFMaterial() override = default; + + virtual bool supportExtension( const std::string& extensionName ) { + auto it = m_allowedExtensions.find( extensionName ); + return ( it != m_allowedExtensions.end() ) && ( it->second ); + } + + void prohibitAllExtensions() { m_allowedExtensions.clear(); } + + void prohibitExtension( const std::string& extension ) { + m_allowedExtensions[extension] = false; + } + + void allowExtension( const std::string& extension ) { m_allowedExtensions[extension] = true; } + + private: + std::map m_allowedExtensions {}; +}; + +/** + * \brief Clearcoat layer extension + */ +struct RA_CORE_API GLTFClearcoatLayer : public GLTFMaterialExtensionData { + explicit GLTFClearcoatLayer( const std::string& name = std::string {} ) : + GLTFMaterialExtensionData( "Clearcoat", name ) {} + /// The clearcoat layer intensity. + float m_clearcoatFactor { 0 }; + bool m_hasClearcoatTexture { false }; + std::string m_clearcoatTexture {}; + GLTFSampler m_clearcoatSampler {}; + mutable std::unique_ptr m_clearcoatTextureTransform { nullptr }; + /// The clearcoat layer roughness. + float m_clearcoatRoughnessFactor { 0 }; + bool m_hasClearcoatRoughnessTexture { false }; + std::string m_clearcoatRoughnessTexture {}; + GLTFSampler m_clearcoatRoughnessSampler {}; + mutable std::unique_ptr m_clearcoatRoughnessTextureTransform { nullptr }; + /// The clearcoat normal map texture. + std::string m_clearcoatNormalTexture {}; + float m_clearcoatNormalTextureScale { 1 }; + GLTFSampler m_clearcoatNormalSampler {}; + bool m_hasClearcoatNormalTexture { false }; + mutable std::unique_ptr m_clearcoatNormalTextureTransform { nullptr }; +}; + +/** + * \brief Specular layer extension + */ +struct RA_CORE_API GLTFSpecularLayer : public GLTFMaterialExtensionData { + explicit GLTFSpecularLayer( const std::string& name = std::string {} ) : + GLTFMaterialExtensionData( "Specular", name ) {} + /// The specular layer strength. + float m_specularFactor { 1. }; + bool m_hasSpecularTexture { false }; + std::string m_specularTexture {}; + GLTFSampler m_specularSampler {}; + mutable std::unique_ptr m_specularTextureTransform { nullptr }; + /// The specular layer color. + Ra::Core::Utils::Color m_specularColorFactor { 1.0, 1.0, 1.0, 1.0 }; + bool m_hasSpecularColorTexture { false }; + std::string m_specularColorTexture {}; + GLTFSampler m_specularColorSampler {}; + mutable std::unique_ptr m_specularColorTextureTransform { nullptr }; +}; + +/** + * \brief Sheen layer extension + */ +struct RA_CORE_API GLTFSheenLayer : public GLTFMaterialExtensionData { + explicit GLTFSheenLayer( const std::string& name = std::string {} ) : + GLTFMaterialExtensionData( "Sheen", name ) {} + + /// The sheen color. + Ra::Core::Utils::Color m_sheenColorFactor { 0.0, 0.0, 0.0, 1.0 }; + bool m_hasSheenColorTexture { false }; + std::string m_sheenColorTexture {}; + GLTFSampler m_sheenColorTextureSampler {}; + mutable std::unique_ptr m_sheenColorTextureTransform { nullptr }; + + /// The sheen roughness. + float m_sheenRoughnessFactor { 0 }; + bool m_hasSheenRoughnessTexture { false }; + std::string m_sheenRoughnessTexture {}; + GLTFSampler m_sheenRoughnessTextureSampler {}; + mutable std::unique_ptr m_sheenRoughnessTextureTransform { nullptr }; +}; + +/** + * \brief IOR extension + */ +struct RA_CORE_API GLTFIor : public GLTFMaterialExtensionData { + explicit GLTFIor( const std::string& name = std::string {} ) : + GLTFMaterialExtensionData( "Ior", name ) {} + float m_ior { 1.5 }; +}; + +} // namespace Material +} // namespace Core +} // namespace Ra diff --git a/src/Core/Material/GLTFTextureParameters.hpp b/src/Core/Material/GLTFTextureParameters.hpp new file mode 100644 index 00000000000..edabd42b0c1 --- /dev/null +++ b/src/Core/Material/GLTFTextureParameters.hpp @@ -0,0 +1,70 @@ +#pragma once +#include + +#include +#include + +#include + +namespace Ra { +namespace Core { +namespace Material { + +/** + * \brief Implementation of the gltf_KHRTextureTransform extension. + */ +struct RA_CORE_API GLTFTextureTransform { + std::array offset { 0_ra, 0_ra }; + std::array scale { 1_ra, 1_ra }; + Scalar rotation { 0_ra }; + int texCoord { -1 }; + + /// Warning : this transformation take into account uv origin changes from gltf and Radium ... + Ra::Core::Matrix3 getTransformationAsMatrix() const { + Ra::Core::Matrix3 Mat_translation; + Mat_translation << Scalar( 1 ), Scalar( 0 ), offset[0], Scalar( 0 ), Scalar( 1 ), + -offset[1], Scalar( 0 ), Scalar( 0 ), Scalar( 1 ); + Ra::Core::Matrix3 Mat_rotation; + Mat_rotation << std::cos( rotation ), -std::sin( rotation ), std::sin( rotation ), + std::sin( rotation ), std::cos( rotation ), 1_ra - std::cos( rotation ), Scalar( 0 ), + Scalar( 0 ), Scalar( 1 ); + Ra::Core::Matrix3 Mat_scale; + Mat_scale << scale[0], Scalar( 0 ), Scalar( 0 ), Scalar( 0 ), scale[1], 1 - scale[1], + Scalar( 0 ), Scalar( 0 ), Scalar( 1 ); + Ra::Core::Matrix3 res = Mat_translation * Mat_rotation * Mat_scale; + return res; + } +}; + +/** + * \brief Sampler Data as defined by GlTF specification + * Enums correspond to OpenGL specification + */ +struct RA_CORE_API GLTFSampler { + enum class MagFilter : uint16_t { Nearest = 9728, Linear = 9729 }; + + enum class MinFilter : uint16_t { + Nearest = 9728, + Linear = 9729, + NearestMipMapNearest = 9984, + LinearMipMapNearest = 9985, + NearestMipMapLinear = 9986, + LinearMipMapLinear = 9987 + }; + + enum class WrappingMode : uint16_t { + ClampToEdge = 33071, + MirroredRepeat = 33648, + Repeat = 10497 + }; + + MagFilter magFilter { MagFilter::Nearest }; + MinFilter minFilter { MinFilter::Nearest }; + + WrappingMode wrapS { WrappingMode::Repeat }; + WrappingMode wrapT { WrappingMode::Repeat }; +}; + +} // namespace Material +} // namespace Core +} // namespace Ra diff --git a/src/Core/Material/MetallicRoughnessMaterialData.cpp b/src/Core/Material/MetallicRoughnessMaterialData.cpp new file mode 100644 index 00000000000..f0a7e6060f4 --- /dev/null +++ b/src/Core/Material/MetallicRoughnessMaterialData.cpp @@ -0,0 +1,23 @@ +#include + +namespace Ra { +namespace Core { +namespace Material { + +MetallicRoughnessData::MetallicRoughnessData( const std::string& name ) : + BaseGLTFMaterial( { "MetallicRoughness" }, name ) { + // extension supported by MetallicRoughness gltf materials + allowExtension( "KHR_materials_clearcoat" ); + allowExtension( "KHR_materials_ior" ); + allowExtension( "KHR_materials_specular" ); + allowExtension( "KHR_materials_sheen" ); + // TODO : uncomment the extension when supported by the implementation. + /* + allowExtension("KHR_materials_transmission"); + allowExtension("KHR_materials_volume"); + */ +} + +} // namespace Material +} // namespace Core +} // namespace Ra diff --git a/src/Core/Material/MetallicRoughnessMaterialData.hpp b/src/Core/Material/MetallicRoughnessMaterialData.hpp new file mode 100644 index 00000000000..17b9890046a --- /dev/null +++ b/src/Core/Material/MetallicRoughnessMaterialData.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include + +namespace Ra { +namespace Core { +namespace Material { + +/** + * \brief RadiumIO representation of the MetalicRoughness material + */ +class RA_CORE_API MetallicRoughnessData : public BaseGLTFMaterial +{ + public: + /// Base texture + std::string m_baseColorTexture {}; + Ra::Core::Utils::Color m_baseColorFactor { 1.0, 1.0, 1.0, 1.0 }; + GLTFSampler m_baseSampler {}; + bool m_hasBaseColorTexture { false }; + mutable std::unique_ptr m_baseTextureTransform { nullptr }; + + /// Metallic-Roughness texture + std::string m_metallicRoughnessTexture {}; + float m_metallicFactor { 1 }; + float m_roughnessFactor { 1 }; + GLTFSampler m_metallicRoughnessSampler {}; + bool m_hasMetallicRoughnessTexture { false }; + mutable std::unique_ptr m_metallicRoughnessTextureTransform { nullptr }; + + explicit MetallicRoughnessData( const std::string& name = std::string {} ); + ~MetallicRoughnessData() override = default; +}; + +} // namespace Material +} // namespace Core +} // namespace Ra diff --git a/src/Core/Material/SpecularGlossinessMaterialData.cpp b/src/Core/Material/SpecularGlossinessMaterialData.cpp new file mode 100644 index 00000000000..2c7405a0fe7 --- /dev/null +++ b/src/Core/Material/SpecularGlossinessMaterialData.cpp @@ -0,0 +1,13 @@ +#include +namespace Ra { +namespace Core { +namespace Material { + +SpecularGlossinessData::SpecularGlossinessData( const std::string& name ) : + BaseGLTFMaterial( { "SpecularGlossiness" }, name ) { + // extension supported by SpecularGlossiness gltf materials +} + +} // namespace Material +} // namespace Core +} // namespace Ra diff --git a/src/Core/Material/SpecularGlossinessMaterialData.hpp b/src/Core/Material/SpecularGlossinessMaterialData.hpp new file mode 100644 index 00000000000..25bd8fcbb44 --- /dev/null +++ b/src/Core/Material/SpecularGlossinessMaterialData.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include + +#include + +namespace Ra { +namespace Core { +namespace Material { + +/** + * \brief RadiumIO representation of Specular-Glossiness material + * \note Specular-Glossiness is an archived extension of the specification. Its use is discouraged + */ +class RA_CORE_API SpecularGlossinessData : public BaseGLTFMaterial +{ + public: + /// Diffuse texture + std::string m_diffuseTexture {}; + Ra::Core::Utils::Color m_diffuseFactor { 1.0, 1.0, 1.0, 1.0 }; + GLTFSampler m_diffuseSampler {}; + bool m_hasDiffuseTexture { false }; + mutable std::unique_ptr m_diffuseTextureTransform { nullptr }; + + /// Specular-Glossiness texture + std::string m_specularGlossinessTexture {}; + Ra::Core::Utils::Color m_specularFactor { 1.0, 1.0, 1.0, 1.0 }; + float m_glossinessFactor { 1 }; + GLTFSampler m_specularGlossinessSampler {}; + bool m_hasSpecularGlossinessTexture { false }; + mutable std::unique_ptr m_specularGlossinessTransform { nullptr }; + + explicit SpecularGlossinessData( const std::string& name = std::string {} ); + ~SpecularGlossinessData() override = default; +}; + +} // namespace Material +} // namespace Core +} // namespace Ra diff --git a/src/Core/filelist.cmake b/src/Core/filelist.cmake index 7aabbe1b640..4fa087a2193 100644 --- a/src/Core/filelist.cmake +++ b/src/Core/filelist.cmake @@ -35,6 +35,9 @@ set(core_sources Geometry/TriangleMesh.cpp Geometry/Volume.cpp Geometry/deprecated/TopologicalMesh.cpp + Material/BaseGLTFMaterial.cpp + Material/MetallicRoughnessMaterialData.cpp + Material/SpecularGlossinessMaterialData.cpp Resources/Resources.cpp Tasks/TaskQueue.cpp Utils/Attribs.cpp @@ -101,6 +104,10 @@ set(core_headers Geometry/TriangleMesh.hpp Geometry/Volume.hpp Geometry/deprecated/TopologicalMesh.hpp + Material/BaseGLTFMaterial.hpp + Material/GLTFTextureParameters.hpp + Material/MetallicRoughnessMaterialData.hpp + Material/SpecularGlossinessMaterialData.hpp Math/DualQuaternion.hpp Math/Interpolation.hpp Math/LinearAlgebra.hpp From f692e1a0f0832f6b8a96ed1af9b3797bdbb6c67e Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Wed, 19 Jul 2023 19:12:57 +0200 Subject: [PATCH 02/27] [engine] add gltf material support --- src/Engine/Data/GLTFMaterial.cpp | 645 ++++++++++++++++++ src/Engine/Data/GLTFMaterial.hpp | 543 +++++++++++++++ src/Engine/Data/MetallicRoughnessMaterial.cpp | 165 +++++ src/Engine/Data/MetallicRoughnessMaterial.hpp | 103 +++ .../MetallicRoughnessMaterialConverter.cpp | 38 ++ .../MetallicRoughnessMaterialConverter.hpp | 26 + .../Data/SpecularGlossinessMaterial.cpp | 179 +++++ .../Data/SpecularGlossinessMaterial.hpp | 117 ++++ .../SpecularGlossinessMaterialConverter.cpp | 36 + .../SpecularGlossinessMaterialConverter.hpp | 25 + src/Engine/RadiumEngine.cpp | 5 +- src/Engine/filelist.cmake | 17 + 12 files changed, 1898 insertions(+), 1 deletion(-) create mode 100644 src/Engine/Data/GLTFMaterial.cpp create mode 100644 src/Engine/Data/GLTFMaterial.hpp create mode 100644 src/Engine/Data/MetallicRoughnessMaterial.cpp create mode 100644 src/Engine/Data/MetallicRoughnessMaterial.hpp create mode 100644 src/Engine/Data/MetallicRoughnessMaterialConverter.cpp create mode 100644 src/Engine/Data/MetallicRoughnessMaterialConverter.hpp create mode 100644 src/Engine/Data/SpecularGlossinessMaterial.cpp create mode 100644 src/Engine/Data/SpecularGlossinessMaterial.hpp create mode 100644 src/Engine/Data/SpecularGlossinessMaterialConverter.cpp create mode 100644 src/Engine/Data/SpecularGlossinessMaterialConverter.hpp diff --git a/src/Engine/Data/GLTFMaterial.cpp b/src/Engine/Data/GLTFMaterial.cpp new file mode 100644 index 00000000000..72436ef3912 --- /dev/null +++ b/src/Engine/Data/GLTFMaterial.cpp @@ -0,0 +1,645 @@ +#include "nlohmann/json.hpp" +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include + +namespace Ra { +namespace Engine { +namespace Data { + +bool GLTFMaterial::s_bsdfLutsLoaded { false }; +Ra::Engine::Data::Texture* GLTFMaterial::s_ggxlut { nullptr }; +Ra::Engine::Data::Texture* GLTFMaterial::s_sheenElut { nullptr }; +Ra::Engine::Data::Texture* GLTFMaterial::s_charlielut { nullptr }; + +std::string GLTFMaterial::s_shaderBasePath {}; +nlohmann::json GLTFMaterial::m_parametersMetadata = {}; + +std::shared_ptr + GLTFMaterial::s_AlphaModeEnum( new GLTFMaterial::GltfAlphaModeEnumConverter( + { { Core::Material::AlphaMode::Opaque, "Opaque" }, + { Core::Material::AlphaMode::Mask, "Mask" }, + { Core::Material::AlphaMode::Blend, "Blend" } } ) ); + +GLTFMaterial::GLTFMaterial( const std::string& name, const std::string& materialName ) : + Material( name, materialName, Material::MaterialAspect::MAT_OPAQUE ) { + auto& renderParameters = Data::ShaderParameterProvider::getParameters(); + renderParameters.addEnumConverter( "material.baseMaterial.alphaMode", s_AlphaModeEnum ); +} + +GLTFMaterial::~GLTFMaterial() { + this->m_textures.clear(); +} + +const Core::Material::GLTFTextureTransform* +GLTFMaterial::getTextureTransform( const TextureSemantic& semantic ) const { + if ( semantic == "TEX_NORMAL" ) { return m_normalTextureTransform.get(); } + if ( semantic == "TEX_EMISSIVE" ) { return m_emissiveTextureTransform.get(); } + if ( semantic == "TEX_OCCLUSION" ) { return m_occlusionTextureTransform.get(); } + return nullptr; +} + +void GLTFMaterial::updateGL() { + m_isOpenGlConfigured = true; + // Load textures + auto texManager = RadiumEngine::getInstance()->getTextureManager(); + auto& renderParameters = getParameters(); + if ( GLTFMaterial::s_ggxlut == nullptr ) { + Ra::Engine::Data::TextureParameters ggxLut; + ggxLut.name = "GLTFMaterial::ggxLut"; + GLTFMaterial::s_ggxlut = texManager->getOrLoadTexture( ggxLut, false ); + } + renderParameters.addParameter( "material.baseMaterial.ggxLut", GLTFMaterial::s_ggxlut ); + + for ( const auto& tex : m_pendingTextures ) { + // only manage GLTFMaterial texture semantic. + if ( tex.first == "TEX_EMISSIVE" ) { + m_textures[tex.first] = texManager->getOrLoadTexture( tex.second, true ); + } + else if ( tex.first == "TEX_NORMAL" || tex.first == "TEX_OCCLUSION" ) { + m_textures[tex.first] = texManager->getOrLoadTexture( tex.second, false ); + } + } + + m_pendingTextures.erase( { "TEX_EMISSIVE" } ); + m_pendingTextures.erase( { "TEX_NORMAL" } ); + m_pendingTextures.erase( { "TEX_OCCLUSION" } ); + + renderParameters.addParameter( "material.baseMaterial.emissiveFactor", m_emissiveFactor ); + renderParameters.addParameter( "material.baseMaterial.alphaMode", m_alphaMode ); + renderParameters.addParameter( "material.baseMaterial.alphaCutoff", m_alphaCutoff ); + renderParameters.addParameter( "material.baseMaterial.doubleSided", m_doubleSided ); + renderParameters.addParameter( "material.baseMaterial.ior", m_indexOfRefraction ); + + auto tex = getTexture( { "TEX_NORMAL" } ); + if ( tex != nullptr ) { + renderParameters.addParameter( "material.baseMaterial.normalTextureScale", + m_normalTextureScale ); + renderParameters.addParameter( "material.baseMaterial.normal", tex ); + if ( m_normalTextureTransform ) { + auto tr = m_normalTextureTransform->getTransformationAsMatrix(); + renderParameters.addParameter( "material.baseMaterial.normalTransform", tr ); + } + } + + tex = getTexture( { "TEX_OCCLUSION" } ); + if ( tex != nullptr ) { + renderParameters.addParameter( "material.baseMaterial.occlusionStrength", + m_occlusionStrength ); + renderParameters.addParameter( "material.baseMaterial.occlusion", tex ); + if ( m_occlusionTextureTransform ) { + auto tr = m_occlusionTextureTransform->getTransformationAsMatrix(); + renderParameters.addParameter( "material.baseMaterial.occlusionTransform", tr ); + } + } + + tex = getTexture( { "TEX_EMISSIVE" } ); + if ( tex != nullptr ) { + renderParameters.addParameter( "material.baseMaterial.emissive", tex ); + if ( m_emissiveTextureTransform ) { + auto tr = m_emissiveTextureTransform->getTransformationAsMatrix(); + renderParameters.addParameter( "material.baseMaterial.emmissiveTransform", tr ); + } + } + + for ( const auto& l : m_layers ) { + l->updateGL(); + } +} + +void GLTFMaterial::updateFromParameters() { + if ( m_isOpenGlConfigured ) { + auto& renderParameters = getParameters(); + m_emissiveFactor = renderParameters.getParameter( + "material.baseMaterial.emissiveFactor" ); + m_alphaMode = renderParameters.getParameter( + "material.baseMaterial.alphaMode" ); + m_alphaCutoff = + renderParameters.getParameter( "material.baseMaterial.alphaCutoff" ); + m_doubleSided = renderParameters.getParameter( "material.baseMaterial.doubleSided" ); + m_indexOfRefraction = renderParameters.getParameter( "material.baseMaterial.ior" ); + + for ( const auto& l : m_layers ) { + l->updateFromParameters(); + } + } +} + +bool GLTFMaterial::isTransparent() const { + return m_alphaMode == 2 || std::any_of( m_layers.begin(), m_layers.end(), []( const auto& l ) { + return l->isTransparent(); + } ); +} + +void GLTFMaterial::registerMaterial() { + // gets the resource path + auto resourcesRootDir { RadiumEngine::getInstance()->getResourcesDir() }; + + s_shaderBasePath = resourcesRootDir + "Shaders/Materials/GLTF"; + auto shaderProgramManager = RadiumEngine::getInstance()->getShaderProgramManager(); + shaderProgramManager->addNamedString( { "/baseGLTFMaterial.glsl" }, + s_shaderBasePath + "/Materials/baseGLTFMaterial.glsl" ); + + if ( !GLTFMaterial::s_bsdfLutsLoaded ) { + auto* engine = Ra::Engine::RadiumEngine::getInstance(); + auto* texMngr = engine->getTextureManager(); + auto& ggxLut = texMngr->addTexture( "GLTFMaterial::ggxLut", 1024, 1024, nullptr ); + // load the texture image without OpenGL initialization + ggxLut.name = s_shaderBasePath + "/BSDF_LUTs/lut_ggx.png"; + texMngr->loadTextureImage( ggxLut ); + // Set the registered name again for further access to the texture + ggxLut.name = "GLTFMaterial::ggxLut"; + + auto& sheenELut = texMngr->addTexture( "GLTFMaterial::sheenELut", 1024, 1024, nullptr ); + sheenELut.name = s_shaderBasePath + "/BSDF_LUTs/lut_sheen_E.png"; + texMngr->loadTextureImage( sheenELut ); + // Set the registered name again for further access to the texture + sheenELut.name = "GLTFMaterial::sheenELut"; + + auto& charlieLut = texMngr->addTexture( "GLTFMaterial::charlieLut", 1024, 1024, nullptr ); + charlieLut.name = s_shaderBasePath + "/BSDF_LUTs/lut_charlie.png"; + texMngr->loadTextureImage( charlieLut ); + // Set the registered name again for further access to the texture + charlieLut.name = "GLTFMaterial::charlieLut"; + + GLTFMaterial::s_bsdfLutsLoaded = true; + } + // Registering parameters metadata + std::ifstream metadata( s_shaderBasePath + "/Metadata/GlTFMaterial.json" ); + metadata >> m_parametersMetadata; + + MetallicRoughness::registerMaterial(); + SpecularGlossiness::registerMaterial(); +} + +void GLTFMaterial::unregisterMaterial() { + MetallicRoughness::unregisterMaterial(); + SpecularGlossiness::unregisterMaterial(); +} + +std::map( GLTFMaterial& baseMaterial, + const std::string& instanceName, + const void* source )>> + extensionBuilder = { + { "KHR_materials_ior", + []( GLTFMaterial& baseMaterial, + const std::string& /*instanceName*/, + const auto* source ) { + auto iorProvider = reinterpret_cast( source ); + baseMaterial.setIndexOfRefraction( iorProvider->m_ior ); + return nullptr; + } }, + { "KHR_materials_clearcoat", + []( GLTFMaterial& baseMaterial, const std::string& instanceName, const auto* source ) { + return std::make_unique( + baseMaterial, + instanceName, + reinterpret_cast( source ) ); + } }, + { "KHR_materials_specular", + []( GLTFMaterial& baseMaterial, const std::string& instanceName, const auto* source ) { + return std::make_unique( + baseMaterial, + instanceName, + reinterpret_cast( source ) ); + } }, + { "KHR_materials_sheen", + []( GLTFMaterial& baseMaterial, const std::string& instanceName, const auto* source ) { + return std::make_unique( + baseMaterial, + instanceName, + reinterpret_cast( source ) ); + } } }; + +void GLTFMaterial::fillBaseFrom( const Core::Material::BaseGLTFMaterial* source ) { + + // Warning, must modify this if textures are embedded in the GLTF file + if ( source->m_hasNormalTexture ) { + addTexture( { "TEX_NORMAL" }, source->m_normalTexture, source->m_normalSampler ); + m_normalTextureTransform = std::move( source->m_normalTextureTransform ); + } + if ( source->m_hasOcclusionTexture ) { + addTexture( { "TEX_OCCLUSION" }, source->m_occlusionTexture, source->m_occlusionSampler ); + m_occlusionTextureTransform = std::move( source->m_occlusionTextureTransform ); + } + if ( source->m_hasEmissiveTexture ) { + addTexture( { "TEX_EMISSIVE" }, source->m_emissiveTexture, source->m_emissiveSampler ); + m_emissiveTextureTransform = std::move( source->m_emissiveTextureTransform ); + } + m_normalTextureScale = source->m_normalTextureScale; + m_occlusionStrength = source->m_occlusionStrength; + m_emissiveFactor = source->m_emissiveFactor; + m_alphaMode = source->m_alphaMode; + m_alphaCutoff = source->m_alphaCutoff; + m_doubleSided = source->m_doubleSided; + + for ( const auto& ext : source->m_extensions ) { + auto it = extensionBuilder.find( ext.first ); + if ( it != extensionBuilder.end() ) { + auto e = it->second( *this, ext.second->getName(), ext.second.get() ); + if ( e ) { m_layers.emplace_back( std::move( e ) ); } + } + else { + LOG( Ra::Core::Utils::logERROR ) + << "Unable to find translator for gltf extension " << ext.first << "!!"; + } + } +} + +std::list GLTFMaterial::getPropertyList() const { + std::list props = Ra::Engine::Data::Material::getPropertyList(); + // Expose the new GLTF__INTERFACE that will eveolve until it is submitted to Radium + // GLSL/Material interface + props.emplace_back( "GLTF_MATERIAL_INTERFACE" ); + // textures + if ( m_pendingTextures.find( { "TEX_NORMAL" } ) != m_pendingTextures.end() || + getTexture( { "TEX_NORMAL" } ) != nullptr ) { + props.emplace_back( "TEXTURE_NORMAL" ); + if ( m_normalTextureTransform ) { props.emplace_back( "TEXTURE_COORD_TRANSFORM_NORMAL" ); } + } + if ( m_pendingTextures.find( { "TEX_OCCLUSION" } ) != m_pendingTextures.end() || + getTexture( { "TEX_OCCLUSION" } ) != nullptr ) { + props.emplace_back( "TEXTURE_OCCLUSION" ); + if ( m_occlusionTextureTransform ) { + props.emplace_back( "TEXTURE_COORD_TRANSFORM_OCCLUSION" ); + } + } + if ( m_pendingTextures.find( { "TEX_EMISSIVE" } ) != m_pendingTextures.end() || + getTexture( { "TEX_EMISSIVE" } ) != nullptr ) { + props.emplace_back( "TEXTURE_EMISSIVE" ); + if ( m_emissiveTextureTransform ) { + props.emplace_back( "TEXTURE_COORD_TRANSFORM_EMISSIVE" ); + } + } + + for ( const auto& l : m_layers ) { + props.splice( props.end(), l->getPropertyList() ); + } + return props; +} + +nlohmann::json GLTFMaterial::getParametersMetadata() const { + return m_parametersMetadata; +} + +/* --- clearcoat layer --- */ + +GLTFClearcoat::GLTFClearcoat( GLTFMaterial& baseMaterial, + const std::string& instanceName, + const Core::Material::GLTFClearcoatLayer* source ) : + GLTFMaterialExtension( baseMaterial, instanceName, "GLTF_ClearcoatLayer" ) { + + // Warning, must modify this if textures are embedded in the GLTF file + + m_clearcoatFactor = source->m_clearcoatFactor; + if ( source->m_hasClearcoatTexture ) { + + addTexture( { "TEX_CLEARCOAT" }, source->m_clearcoatTexture, source->m_clearcoatSampler ); + if ( source->m_clearcoatTextureTransform ) { + m_textureTransform["TEX_CLEARCOAT"] = std::move( source->m_clearcoatTextureTransform ); + } + } + + m_clearcoatRoughnessFactor = source->m_clearcoatRoughnessFactor; + if ( source->m_hasClearcoatRoughnessTexture ) { + addTexture( { "TEX_CLEARCOATROUGHNESS" }, + source->m_clearcoatRoughnessTexture, + source->m_clearcoatRoughnessSampler ); + if ( source->m_clearcoatRoughnessTextureTransform ) { + m_textureTransform["TEX_CLEARCOATROUGHNESS"] = + std::move( source->m_clearcoatRoughnessTextureTransform ); + } + } + + if ( source->m_hasClearcoatNormalTexture ) { + addTexture( { "TEX_CLEARCOATNORMAL" }, + source->m_clearcoatNormalTexture, + source->m_clearcoatNormalSampler ); + m_clearcoatNormalTextureScale = source->m_clearcoatNormalTextureScale; + if ( source->m_clearcoatNormalTextureTransform ) { + m_textureTransform["TEX_CLEARCOATNORMAL"] = + std::move( source->m_clearcoatNormalTextureTransform ); + } + } +} + +std::list GLTFClearcoat::getPropertyList() const { + std::list props; + props.emplace_back( "CLEARCOAT_LAYER" ); + // textures + if ( m_pendingTextures.find( { "TEX_CLEARCOAT" } ) != m_pendingTextures.end() || + getTexture( { "TEX_CLEARCOAT" } ) != nullptr ) { + props.emplace_back( "TEXTURE_CLEARCOAT" ); + if ( m_textureTransform.find( "TEX_CLEARCOAT" ) != m_textureTransform.end() ) { + props.emplace_back( "TEXTURE_COORD_TRANSFORM_CLEARCOAT" ); + } + } + if ( m_pendingTextures.find( { "TEX_CLEARCOATROUGHNESS" } ) != m_pendingTextures.end() || + getTexture( { "TEX_CLEARCOATROUGHNESS" } ) != nullptr ) { + props.emplace_back( "TEXTURE_CLEARCOATROUGHNESS" ); + if ( m_textureTransform.find( "TEX_CLEARCOATROUGHNESS" ) != m_textureTransform.end() ) { + props.emplace_back( "TEXTURE_COORD_TRANSFORM_CLEARCOATROUGHNESS" ); + } + } + if ( m_pendingTextures.find( { "TEX_CLEARCOATNORMAL" } ) != m_pendingTextures.end() || + getTexture( { "TEX_CLEARCOATNORMAL" } ) != nullptr ) { + props.emplace_back( "TEXTURE_CLEARCOATNORMAL" ); + if ( m_textureTransform.find( "TEX_CLEARCOATNORMAL" ) != m_textureTransform.end() ) { + props.emplace_back( "TEXTURE_COORD_TRANSFORM_CLEARCOATNORMAL" ); + } + } + return props; +} + +void GLTFClearcoat::updateGL() { + m_baseMaterial.getParameters().addParameter( "material.baseMaterial.clearcoat.clearcoatFactor", + m_clearcoatFactor ); + m_baseMaterial.getParameters().addParameter( + "material.baseMaterial.clearcoat.clearcoatRoughnessFactor", m_clearcoatRoughnessFactor ); + + // Load textures + if ( !m_pendingTextures.empty() ) { + auto texManager = RadiumEngine::getInstance()->getTextureManager(); + for ( const auto& tex : m_pendingTextures ) { + // According to the clearcoat spec, all clearcoat textures are in RGB linear space + // https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_clearcoat + m_textures[tex.first] = texManager->getOrLoadTexture( tex.second, false ); + } + m_pendingTextures.clear(); + } + Ra::Engine::Data::Texture* tex = getTexture( { "TEX_CLEARCOAT" } ); + if ( tex != nullptr ) { + m_baseMaterial.getParameters().addParameter( + "material.baseMaterial.clearcoat.clearcoatTexture", tex ); + auto it = m_textureTransform.find( "TEX_CLEARCOAT" ); + if ( it != m_textureTransform.end() ) { + auto tr = it->second->getTransformationAsMatrix(); + m_baseMaterial.getParameters().addParameter( + "material.baseMaterial.clearcoat.clearcoatTextureTransform", tr ); + } + } + + tex = getTexture( { "TEX_CLEARCOATROUGHNESS" } ); + if ( tex != nullptr ) { + m_baseMaterial.getParameters().addParameter( + "material.baseMaterial.clearcoat.clearcoatRoughnessTexture", tex ); + auto it = m_textureTransform.find( "TEX_CLEARCOATROUGHNESS" ); + if ( it != m_textureTransform.end() ) { + auto tr = it->second->getTransformationAsMatrix(); + m_baseMaterial.getParameters().addParameter( + "material.baseMaterial.clearcoat.clearcoatRoughnessTextureTransform", tr ); + } + } + + tex = getTexture( { "TEX_CLEARCOATNORMAL" } ); + if ( tex != nullptr ) { + m_baseMaterial.getParameters().addParameter( + "material.baseMaterial.clearcoat.clearcoatNormalTextureScale", + m_clearcoatNormalTextureScale ); + m_baseMaterial.getParameters().addParameter( + "material.baseMaterial.clearcoat.clearcoatNormalTexture", tex ); + auto it = m_textureTransform.find( "TEX_CLEARCOATNORMAL" ); + if ( it != m_textureTransform.end() ) { + auto tr = it->second->getTransformationAsMatrix(); + m_baseMaterial.getParameters().addParameter( + "material.baseMaterial.clearcoat.clearcoatNormalTextureTransform", tr ); + } + } +} + +void GLTFClearcoat::updateFromParameters() { + m_clearcoatFactor = m_baseMaterial.getParameters().getParameter( + "material.baseMaterial.clearcoat.clearcoatFactor" ); + m_clearcoatRoughnessFactor = m_baseMaterial.getParameters().getParameter( + "material.baseMaterial.clearcoat.clearcoatRoughnessFactor" ); +} + +/* --- specular layer --- */ + +GLTFSpecular::GLTFSpecular( GLTFMaterial& baseMaterial, + const std::string& instanceName, + const Core::Material::GLTFSpecularLayer* source ) : + GLTFMaterialExtension( baseMaterial, instanceName, "GLTF_ClearcoatLayer" ) { + + // Warning, must modify this if textures are embedded in the GLTF file + + m_specularFactor = source->m_specularFactor; + if ( source->m_hasSpecularTexture ) { + + addTexture( + { "TEXTURE_SPECULAR_EXT" }, source->m_specularTexture, source->m_specularSampler ); + if ( source->m_specularTextureTransform ) { + m_textureTransform["TEXTURE_SPECULAR_EXT"] = + std::move( source->m_specularTextureTransform ); + } + } + + m_specularColorFactor = source->m_specularColorFactor; + if ( source->m_hasSpecularColorTexture ) { + addTexture( { "TEXTURE_SPECULARCOLOR_EXT" }, + source->m_specularColorTexture, + source->m_specularColorSampler ); + if ( source->m_specularColorTextureTransform ) { + m_textureTransform["TEXTURE_SPECULARCOLOR_EXT"] = + std::move( source->m_specularColorTextureTransform ); + } + } +} + +std::list GLTFSpecular::getPropertyList() const { + std::list props; + props.emplace_back( "SPECULAR_LAYER" ); + // textures + if ( m_pendingTextures.find( { "TEXTURE_SPECULAR_EXT" } ) != m_pendingTextures.end() || + getTexture( { "TEXTURE_SPECULAR_EXT" } ) != nullptr ) { + props.emplace_back( "TEXTURE_SPECULAR_EXT" ); + if ( m_textureTransform.find( "TEXTURE_SPECULAR_EXT" ) != m_textureTransform.end() ) { + props.emplace_back( "TEXTURE_COORD_TRANSFORM_SPECULAR_EXT" ); + } + } + if ( m_pendingTextures.find( { "TEXTURE_SPECULARCOLOR_EXT" } ) != m_pendingTextures.end() || + getTexture( { "TEXTURE_SPECULARCOLOR_EXT" } ) != nullptr ) { + props.emplace_back( "TEXTURE_SPECULARCOLOR_EXT" ); + if ( m_textureTransform.find( "TEXTURE_SPECULARCOLOR_EXT" ) != m_textureTransform.end() ) { + props.emplace_back( "TEXTURE_COORD_TRANSFORM_SPECULARCOLOR_EXT" ); + } + } + return props; +} + +void GLTFSpecular::updateGL() { + m_baseMaterial.getParameters().addParameter( "material.baseMaterial.specular.specularFactor", + m_specularFactor ); + m_baseMaterial.getParameters().addParameter( + "material.baseMaterial.specular.specularColorFactor", m_specularColorFactor ); + + // Load textures + if ( !m_pendingTextures.empty() ) { + auto texManager = RadiumEngine::getInstance()->getTextureManager(); + for ( const auto& tex : m_pendingTextures ) { + // textures are in sRGB, must be linearized + m_textures[tex.first] = texManager->getOrLoadTexture( tex.second, true ); + } + m_pendingTextures.clear(); + } + auto tex = getTexture( { "TEXTURE_SPECULAR_EXT" } ); + if ( tex != nullptr ) { + m_baseMaterial.getParameters().addParameter( + "material.baseMaterial.specular.specularTexture", tex ); + auto it = m_textureTransform.find( "TEXTURE_SPECULAR_EXT" ); + if ( it != m_textureTransform.end() ) { + auto tr = it->second->getTransformationAsMatrix(); + m_baseMaterial.getParameters().addParameter( + "material.baseMaterial.specular.specularTextureTransform", tr ); + } + } + + tex = getTexture( { "TEXTURE_SPECULARCOLOR_EXT" } ); + if ( tex != nullptr ) { + m_baseMaterial.getParameters().addParameter( + "material.baseMaterial.specular.specularColorTexture", tex ); + auto it = m_textureTransform.find( "TEXTURE_SPECULARCOLOR_EXT" ); + if ( it != m_textureTransform.end() ) { + auto tr = it->second->getTransformationAsMatrix(); + m_baseMaterial.getParameters().addParameter( + "material.baseMaterial.specular.specularColorTextureTransform", tr ); + } + } +} + +void GLTFSpecular::updateFromParameters() { + m_specularFactor = m_baseMaterial.getParameters().getParameter( + "material.baseMaterial.specular.specularFactor" ); + m_specularColorFactor = m_baseMaterial.getParameters().getParameter( + "material.baseMaterial.specular.specularColorFactor" ); +} + +/* --- sheen layer --- */ + +GLTFSheen::GLTFSheen( GLTFMaterial& baseMaterial, + const std::string& instanceName, + const Core::Material::GLTFSheenLayer* source ) : + GLTFMaterialExtension( baseMaterial, instanceName, "GLTF_SheenLayer" ) { + + // Warning, must modify this if textures are embedded in the GLTF file + + m_sheenRoughnessFactor = source->m_sheenRoughnessFactor; + if ( source->m_hasSheenRoughnessTexture ) { + + addTexture( { "TEXTURE_SHEEN_ROUGHNESS" }, + source->m_sheenRoughnessTexture, + source->m_sheenRoughnessTextureSampler ); + if ( source->m_sheenRoughnessTextureTransform ) { + m_textureTransform["TEXTURE_SHEEN_ROUGHNESS"] = + std::move( source->m_sheenRoughnessTextureTransform ); + } + } + + m_sheenColorFactor = source->m_sheenColorFactor; + if ( source->m_hasSheenColorTexture ) { + addTexture( { "TEXTURE_SHEEN_COLOR" }, + source->m_sheenColorTexture, + source->m_sheenColorTextureSampler ); + if ( source->m_sheenColorTextureTransform ) { + m_textureTransform["TEXTURE_SHEEN_COLOR"] = + std::move( source->m_sheenColorTextureTransform ); + } + } +} + +std::list GLTFSheen::getPropertyList() const { + std::list props; + props.emplace_back( "SHEEN_LAYER" ); + // textures + if ( m_pendingTextures.find( { "TEXTURE_SHEEN_COLOR" } ) != m_pendingTextures.end() || + getTexture( { "TEXTURE_SHEEN_COLOR" } ) != nullptr ) { + props.emplace_back( "TEXTURE_SHEEN_COLOR" ); + if ( m_textureTransform.find( "TEXTURE_SHEEN_COLOR" ) != m_textureTransform.end() ) { + props.emplace_back( "TEXTURE_COORD_TRANSFORM_SHEEN_COLOR" ); + } + } + if ( m_pendingTextures.find( { "TEXTURE_SHEEN_ROUGHNESS" } ) != m_pendingTextures.end() || + getTexture( { "TEXTURE_SHEEN_ROUGHNESS" } ) != nullptr ) { + props.emplace_back( "TEXTURE_SHEEN_ROUGHNESS" ); + if ( m_textureTransform.find( "TEXTURE_SHEEN_ROUGHNESS" ) != m_textureTransform.end() ) { + props.emplace_back( "TEXTURE_COORD_TRANSFORM_SHEEN_ROUGHNESS" ); + } + } + return props; +} + +void GLTFSheen::updateGL() { + if ( GLTFMaterial::s_sheenElut == nullptr ) { + auto texManager = RadiumEngine::getInstance()->getTextureManager(); + Ra::Engine::Data::TextureParameters lut; + lut.name = "GLTFMaterial::sheenELut"; + GLTFMaterial::s_sheenElut = texManager->getOrLoadTexture( lut, false ); + lut.name = "GLTFMaterial::charlieLut"; + GLTFMaterial::s_charlielut = texManager->getOrLoadTexture( lut, false ); + } + auto& renderParameters = getParameters(); + renderParameters.addParameter( "material.baseMaterial.sheen.sheenE_LUT", + GLTFMaterial::s_sheenElut ); + renderParameters.addParameter( "material.baseMaterial.sheen.charlieLUT", + GLTFMaterial::s_charlielut ); + + m_baseMaterial.getParameters().addParameter( "material.baseMaterial.sheen.sheenColorFactor", + m_sheenColorFactor ); + m_baseMaterial.getParameters().addParameter( "material.baseMaterial.sheen.sheenRoughnessFactor", + m_sheenRoughnessFactor ); + + // Load textures + if ( !m_pendingTextures.empty() ) { + auto texManager = RadiumEngine::getInstance()->getTextureManager(); + for ( const auto& tex : m_pendingTextures ) { + // textures are in sRGB, must be linearized + m_textures[tex.first] = texManager->getOrLoadTexture( tex.second, true ); + } + m_pendingTextures.clear(); + } + auto tex = getTexture( { "TEXTURE_SHEEN_COLOR" } ); + if ( tex != nullptr ) { + m_baseMaterial.getParameters().addParameter( + "material.baseMaterial.sheen.sheenColorTexture", tex ); + auto it = m_textureTransform.find( "TEXTURE_SHEEN_COLOR" ); + if ( it != m_textureTransform.end() ) { + auto tr = it->second->getTransformationAsMatrix(); + m_baseMaterial.getParameters().addParameter( + "material.baseMaterial.sheen.sheenColorTextureTransform", tr ); + } + } + + tex = getTexture( { "TEXTURE_SHEEN_ROUGHNESS" } ); + if ( tex != nullptr ) { + m_baseMaterial.getParameters().addParameter( + "material.baseMaterial.sheen.sheenRoughnessTexture", tex ); + auto it = m_textureTransform.find( "TEXTURE_SHEEN_ROUGHNESS" ); + if ( it != m_textureTransform.end() ) { + auto tr = it->second->getTransformationAsMatrix(); + m_baseMaterial.getParameters().addParameter( + "material.baseMaterial.sheen.sheenRoughnessTextureTransform", tr ); + } + } +} + +void GLTFSheen::updateFromParameters() { + m_sheenColorFactor = m_baseMaterial.getParameters().getParameter( + "material.baseMaterial.sheen.sheenColorFactor" ); + m_sheenRoughnessFactor = m_baseMaterial.getParameters().getParameter( + "material.baseMaterial.sheen.sheenRoughnessFactor" ); +} + +} // namespace Data +} // namespace Engine +} // namespace Ra diff --git a/src/Engine/Data/GLTFMaterial.hpp b/src/Engine/Data/GLTFMaterial.hpp new file mode 100644 index 00000000000..d8e53961cf4 --- /dev/null +++ b/src/Engine/Data/GLTFMaterial.hpp @@ -0,0 +1,543 @@ +#pragma once +#include + +#include + +#include +#include +#include + +namespace Ra { +namespace Engine { +namespace Data { + +class GLTFMaterial; +/** + * \brief Base class for Radium Engine representation and management of a material extension. + */ +class RA_ENGINE_API GLTFMaterialExtension : public Material +{ + public: + using TextureSemantic = std::string; + + GLTFMaterialExtension( GLTFMaterial& baseMaterial, + const std::string& instanceName, + const std::string& typeName ) : + Material( instanceName, typeName ), m_baseMaterial { baseMaterial } {} + GLTFMaterialExtension( const GLTFMaterialExtension& ) = delete; + GLTFMaterialExtension operator=( const GLTFMaterialExtension& ) = delete; + + void updateGL() override {} + void updateFromParameters() override {}; + + [[nodiscard]] bool isTransparent() const override { return false; } + [[nodiscard]] std::list getPropertyList() const override { return {}; } + + /** + * Add a texture to the material for the given semantic + * @param semantic + * @param texture + * @param sampler + * @return + */ + inline Ra::Engine::Data::TextureParameters& + addTexture( const TextureSemantic& semantic, + const std::string& texture, + const Core::Material::GLTFSampler& sampler ); + + /** + * Get the Radium texture associated with the given semantic + * @param semantic + * @return + */ + [[nodiscard]] inline Ra::Engine::Data::Texture* + getTexture( const TextureSemantic& semantic ) const; + + /** + * Get the Radium texture (parameter struct) associated with the given semantic + * @param semantic + * @return The TextureParameter description + */ + [[nodiscard]] inline std::shared_ptr + getTextureParameter( const TextureSemantic& semantic ) const; + + /** + * Get the texture transform associated with the given semantic + * @param semantic + * @return a raw pointer to the texture transform, nullptr if thereis no transformation. + * @note ownership is kept by the GLTFMaterial + */ + [[nodiscard]] inline const Core::Material::GLTFTextureTransform* + getTextureTransform( const TextureSemantic& semantic ) const; + + private: + inline Ra::Engine::Data::TextureParameters& + addTexture( const TextureSemantic& type, const Ra::Engine::Data::TextureParameters& texture ); + + protected: + GLTFMaterial& m_baseMaterial; + + std::map m_textures; + std::map m_pendingTextures; + std::map> + m_textureTransform; +}; + +/** + * \brief Radium Engine representation and management of the clearcoat layer + */ +class RA_ENGINE_API GLTFClearcoat : public GLTFMaterialExtension +{ + public: + GLTFClearcoat( GLTFMaterial& baseMaterial, + const std::string& instanceName, + const Core::Material::GLTFClearcoatLayer* source ); + + /** Texture semantics allowed for this material + * GLTFMaterial manage the following semantics : + * "TEX_CLEARCOAT" + * "TEX_CLEARCOATROUGHNESS" + * "TEX_CLEARCOATNORMAL" + */ + + void updateGL() override; + void updateFromParameters() override; + [[nodiscard]] std::list getPropertyList() const override; + + private: + float m_clearcoatFactor { 0. }; + float m_clearcoatRoughnessFactor { 0. }; + float m_clearcoatNormalTextureScale { 1 }; +}; + +/** + * \brief Radium Engine representation and management of the specular layer + */ +class RA_ENGINE_API GLTFSpecular : public GLTFMaterialExtension +{ + public: + GLTFSpecular( GLTFMaterial& baseMaterial, + const std::string& instanceName, + const Core::Material::GLTFSpecularLayer* source ); + + /** Texture semantics allowed for this material + * GLTFMaterial manage the following semantics : + * "TEXTURE_SPECULAR_EXT" + * "TEXTURE_SPECULARCOLOR_EXT" + */ + + void updateGL() override; + void updateFromParameters() override; + [[nodiscard]] std::list getPropertyList() const override; + + private: + float m_specularFactor { 1. }; + Ra::Core::Utils::Color m_specularColorFactor { 1.0, 1.0, 1.0, 1.0 }; +}; + +/** + * \brief Radium Engine representation and management of the sheen layer + */ +class RA_ENGINE_API GLTFSheen : public GLTFMaterialExtension +{ + public: + GLTFSheen( GLTFMaterial& baseMaterial, + const std::string& instanceName, + const Core::Material::GLTFSheenLayer* source ); + + /** Texture semantics allowed for this material + * GLTFMaterial manage the following semantics : + * "TEXTURE_SHEEN_COLOR" + * "TEXTURE_SHEEN_ROUGHNESS" + */ + + void updateGL() override; + void updateFromParameters() override; + [[nodiscard]] std::list getPropertyList() const override; + + private: + Ra::Core::Utils::Color m_sheenColorFactor { 0.0, 0.0, 0.0, 0.0 }; + float m_sheenRoughnessFactor { 0. }; +}; + +/** + * Radium Engine material representation of pbrMetallicRoughness + * + */ +class RA_ENGINE_API GLTFMaterial : public Material, public ParameterSetEditingInterface +{ + public: + /** Texture semantics allowed for this material + * GLTFMaterial manage the following semantics : + * "TEX_NORMAL" + * "TEX_OCCLUSION" + * "TEX_EMISSIVE" + */ + using TextureSemantic = std::string; + + public: + /** + * Constructor of a named material + * @param name + */ + explicit GLTFMaterial( const std::string& name, const std::string& materialName ); + + /** + * Destructor + */ + ~GLTFMaterial() override; + + /** + * Add a texture to the material for the given semantic + * @param semantic + * @param texture + * @param sampler + * @return + */ + inline Ra::Engine::Data::TextureParameters& + addTexture( const TextureSemantic& semantic, + const std::string& texture, + const Core::Material::GLTFSampler& sampler ); + + /** + * Get the Radium texture associated with the given semantic + * @param semantic + * @return + */ + [[nodiscard]] inline Ra::Engine::Data::Texture* + getTexture( const TextureSemantic& semantic ) const; + + /** + * Get the Radium texture (parameter struct) associated with the given semantic + * @param semantic + * @return The TextureParameter description + */ + [[nodiscard]] inline std::shared_ptr + getTextureParameter( const TextureSemantic& semantic ) const; + + /** + * Get the texture transform associated with the given semantic + * @param semantic + * @return a raw pointer to the texture transform, nullptr if thereis no transformation. + * @note ownership is kept by the GLTFMaterial + */ + [[nodiscard]] virtual const Core::Material::GLTFTextureTransform* + getTextureTransform( const TextureSemantic& semantic ) const; + + /** + * Update the OpenGL component of the material + */ + void updateGL() override; + + /** + * Update the state of the material from its render Parameters + */ + void updateFromParameters() override; + + /** + * + * @return true if the material is transperent. Depends on the material parameters + */ + [[nodiscard]] bool isTransparent() const override; + + /** + * Get the list of properties the material migh use in a shader. + */ + [[nodiscard]] std::list getPropertyList() const override; + + /** + * Get a json containing metadata about the parameters of the material. + */ + nlohmann::json getParametersMetadata() const override; + + /** + * Register the material to the Radium Material subsystem + */ + static void registerMaterial(); + + /** + * Remove the material from the Radium material subsystem + */ + static void unregisterMaterial(); + + /** + * Initialize from a BaseGLTFMaterial after reading + */ + void fillBaseFrom( const Core::Material::BaseGLTFMaterial* source ); + + /******************************************************************/ + /* Inline methods */ + /******************************************************************/ + + float getNormalTextureScale() const { return m_normalTextureScale; } + void setNormalTextureScale( float normalTextureScale ) { + m_normalTextureScale = normalTextureScale; + } + + float getOcclusionStrength() const { return m_occlusionStrength; } + void seOcclusionStrength( float occlusionStrength ) { m_occlusionStrength = occlusionStrength; } + + const Ra::Core::Utils::Color& getEmissiveFactor() const { return m_emissiveFactor; } + void setEmissiveFactor( const Ra::Core::Utils::Color& emissiveFactor ) { + m_emissiveFactor = emissiveFactor; + } + + Core::Material::AlphaMode getAlphaMode() const { return m_alphaMode; } + void setAlphaMode( Core::Material::AlphaMode alphaMode ) { m_alphaMode = alphaMode; } + + float getAlphaCutoff() const { return m_alphaCutoff; } + void setAlphaCutoff( float alphaCutoff ) { m_alphaCutoff = alphaCutoff; } + + bool isDoubleSided() const { return m_doubleSided; } + void setDoubleSided( bool doubleSided ) { m_doubleSided = doubleSided; } + + float getIndexOfRefraction() const { return m_indexOfRefraction; } + void setIndexOfRefraction( float ior ) { m_indexOfRefraction = ior; } + /******************************************************************/ + + protected: + inline Ra::Engine::Data::TextureParameters& + addTexture( const TextureSemantic& type, const Ra::Engine::Data::TextureParameters& texture ); + inline void addTexture( const TextureSemantic& type, Ra::Engine::Data::Texture* texture ); + + float m_normalTextureScale { 1 }; + float m_occlusionStrength { 1 }; + Ra::Core::Utils::Color m_emissiveFactor { 0.0, 0.0, 0.0, 1.0 }; + Core::Material::AlphaMode m_alphaMode { Core::Material::AlphaMode::Opaque }; + float m_alphaCutoff { 0.5 }; + bool m_doubleSided { false }; + + // attributes having default value in the spec with allowed modifications from extensions + float m_indexOfRefraction { 1.5 }; + + std::map m_textures; + std::map m_pendingTextures; + + std::unique_ptr m_normalTextureTransform { nullptr }; + std::unique_ptr m_occlusionTextureTransform { nullptr }; + std::unique_ptr m_emissiveTextureTransform { nullptr }; + + std::vector> m_layers {}; + + static std::string s_shaderBasePath; + + static nlohmann::json m_parametersMetadata; + + private: + friend class GLTFSheen; + + static bool s_bsdfLutsLoaded; + static Ra::Engine::Data::Texture* s_ggxlut; + static Ra::Engine::Data::Texture* s_sheenElut; + static Ra::Engine::Data::Texture* s_charlielut; + + using GltfAlphaModeEnumConverter = typename Ra::Core::Utils::EnumConverter< + typename std::underlying_type::type>; + static std::shared_ptr s_AlphaModeEnum; + + protected: + // todo : make this private with set/reset methods + bool m_isOpenGlConfigured { false }; +}; + +/* -------------------------------------------------------------------------------------------- */ + +inline void GLTFMaterial::addTexture( const TextureSemantic& type, + Ra::Engine::Data::Texture* texture ) { + m_textures[type] = texture; + m_pendingTextures.erase( type ); +} + +inline Ra::Engine::Data::TextureParameters& +GLTFMaterial::addTexture( const TextureSemantic& semantic, + const std::string& texture, + const Core::Material::GLTFSampler& sampler ) { + + Ra::Engine::Data::TextureParameters textureParams; + textureParams.name = texture; + switch ( sampler.wrapS ) { + case Core::Material::GLTFSampler::WrappingMode::Repeat: + textureParams.wrapS = gl::GL_REPEAT; + break; + case Core::Material::GLTFSampler::WrappingMode::MirroredRepeat: + textureParams.wrapS = gl::GL_MIRRORED_REPEAT; + break; + case Core::Material::GLTFSampler::WrappingMode::ClampToEdge: + textureParams.wrapS = gl::GL_CLAMP_TO_EDGE; + break; + } + switch ( sampler.wrapT ) { + case Core::Material::GLTFSampler::WrappingMode::Repeat: + textureParams.wrapT = gl::GL_REPEAT; + break; + case Core::Material::GLTFSampler::WrappingMode::MirroredRepeat: + textureParams.wrapT = gl::GL_MIRRORED_REPEAT; + break; + case Core::Material::GLTFSampler::WrappingMode::ClampToEdge: + textureParams.wrapT = gl::GL_CLAMP_TO_EDGE; + break; + } + switch ( sampler.magFilter ) { + case Core::Material::GLTFSampler::MagFilter::Nearest: + textureParams.magFilter = gl::GL_NEAREST; + break; + case Core::Material::GLTFSampler::MagFilter::Linear: + textureParams.magFilter = gl::GL_LINEAR; + break; + } + switch ( sampler.minFilter ) { + case Core::Material::GLTFSampler::MinFilter::Nearest: + textureParams.minFilter = gl::GL_NEAREST; + break; + case Core::Material::GLTFSampler::MinFilter::Linear: + textureParams.minFilter = gl::GL_LINEAR; + break; + case Core::Material::GLTFSampler::MinFilter::NearestMipMapNearest: + textureParams.minFilter = gl::GL_NEAREST_MIPMAP_NEAREST; + break; + case Core::Material::GLTFSampler::MinFilter::LinearMipMapNearest: + textureParams.minFilter = gl::GL_LINEAR_MIPMAP_NEAREST; + break; + case Core::Material::GLTFSampler::MinFilter::NearestMipMapLinear: + textureParams.minFilter = gl::GL_NEAREST_MIPMAP_LINEAR; + break; + case Core::Material::GLTFSampler::MinFilter::LinearMipMapLinear: + textureParams.minFilter = gl::GL_LINEAR_MIPMAP_LINEAR; + break; + } + + return addTexture( semantic, textureParams ); +} + +inline Ra::Engine::Data::TextureParameters& +GLTFMaterial::addTexture( const TextureSemantic& type, + const Ra::Engine::Data::TextureParameters& texture ) { + m_pendingTextures[type] = texture; + m_isDirty = true; + + return m_pendingTextures[type]; +} + +inline Ra::Engine::Data::Texture* +GLTFMaterial::getTexture( const TextureSemantic& semantic ) const { + Ra::Engine::Data::Texture* tex = nullptr; + + auto it = m_textures.find( semantic ); + if ( it != m_textures.end() ) { tex = it->second; } + + return tex; +} + +inline std::shared_ptr +GLTFMaterial::getTextureParameter( const TextureSemantic& semantic ) const { + Ra::Engine::Data::Texture* tex = getTexture( semantic ); + if ( tex == nullptr ) { + auto it = m_pendingTextures.find( semantic ); + if ( it != m_pendingTextures.end() ) { + return std::make_shared( it->second ); + } + } + else { return std::make_shared( tex->getParameters() ); } + return nullptr; +} + +inline Ra::Engine::Data::TextureParameters& +GLTFMaterialExtension::addTexture( const TextureSemantic& semantic, + const std::string& texture, + const Core::Material::GLTFSampler& sampler ) { + + Ra::Engine::Data::TextureParameters textureParams; + textureParams.name = texture; + switch ( sampler.wrapS ) { + case Core::Material::GLTFSampler::WrappingMode::Repeat: + textureParams.wrapS = gl::GL_REPEAT; + break; + case Core::Material::GLTFSampler::WrappingMode::MirroredRepeat: + textureParams.wrapS = gl::GL_MIRRORED_REPEAT; + break; + case Core::Material::GLTFSampler::WrappingMode::ClampToEdge: + textureParams.wrapS = gl::GL_CLAMP_TO_EDGE; + break; + } + switch ( sampler.wrapT ) { + case Core::Material::GLTFSampler::WrappingMode::Repeat: + textureParams.wrapT = gl::GL_REPEAT; + break; + case Core::Material::GLTFSampler::WrappingMode::MirroredRepeat: + textureParams.wrapT = gl::GL_MIRRORED_REPEAT; + break; + case Core::Material::GLTFSampler::WrappingMode::ClampToEdge: + textureParams.wrapT = gl::GL_CLAMP_TO_EDGE; + break; + } + switch ( sampler.magFilter ) { + case Core::Material::GLTFSampler::MagFilter::Nearest: + textureParams.magFilter = gl::GL_NEAREST; + break; + case Core::Material::GLTFSampler::MagFilter::Linear: + textureParams.magFilter = gl::GL_LINEAR; + break; + } + switch ( sampler.minFilter ) { + case Core::Material::GLTFSampler::MinFilter::Nearest: + textureParams.minFilter = gl::GL_NEAREST; + break; + case Core::Material::GLTFSampler::MinFilter::Linear: + textureParams.minFilter = gl::GL_LINEAR; + break; + case Core::Material::GLTFSampler::MinFilter::NearestMipMapNearest: + textureParams.minFilter = gl::GL_NEAREST_MIPMAP_NEAREST; + break; + case Core::Material::GLTFSampler::MinFilter::LinearMipMapNearest: + textureParams.minFilter = gl::GL_LINEAR_MIPMAP_NEAREST; + break; + case Core::Material::GLTFSampler::MinFilter::NearestMipMapLinear: + textureParams.minFilter = gl::GL_NEAREST_MIPMAP_LINEAR; + break; + case Core::Material::GLTFSampler::MinFilter::LinearMipMapLinear: + textureParams.minFilter = gl::GL_LINEAR_MIPMAP_LINEAR; + break; + } + + return addTexture( semantic, textureParams ); +} + +inline Ra::Engine::Data::TextureParameters& +GLTFMaterialExtension::addTexture( const TextureSemantic& type, + const Ra::Engine::Data::TextureParameters& texture ) { + m_pendingTextures[type] = texture; + m_isDirty = true; + + return m_pendingTextures[type]; +} + +inline Ra::Engine::Data::Texture* +GLTFMaterialExtension::getTexture( const TextureSemantic& semantic ) const { + auto it = m_textures.find( semantic ); + if ( it != m_textures.end() ) { return it->second; } + return nullptr; +} + +inline std::shared_ptr +GLTFMaterialExtension::getTextureParameter( const TextureSemantic& semantic ) const { + Ra::Engine::Data::Texture* tex = getTexture( semantic ); + if ( tex == nullptr ) { + auto it = m_pendingTextures.find( semantic ); + if ( it != m_pendingTextures.end() ) { + return std::make_shared( it->second ); + } + } + else { return std::make_shared( tex->getParameters() ); } + return nullptr; +} + +const Core::Material::GLTFTextureTransform* +GLTFMaterialExtension::getTextureTransform( const TextureSemantic& semantic ) const { + auto it = m_textureTransform.find( semantic ); + if ( it != m_textureTransform.end() ) { return it->second.get(); } + return nullptr; +} + +} // namespace Data +} // namespace Engine +} // namespace Ra diff --git a/src/Engine/Data/MetallicRoughnessMaterial.cpp b/src/Engine/Data/MetallicRoughnessMaterial.cpp new file mode 100644 index 00000000000..9af3fa7e6b4 --- /dev/null +++ b/src/Engine/Data/MetallicRoughnessMaterial.cpp @@ -0,0 +1,165 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Ra { +namespace Engine { +namespace Data { + +using namespace Ra::Engine::Rendering; + +const std::string MetallicRoughness::m_materialName { "MetallicRoughness" }; + +MetallicRoughness::MetallicRoughness( const std::string& instanceName ) : + GLTFMaterial( instanceName, m_materialName ) {} + +MetallicRoughness::~MetallicRoughness() = default; + +void MetallicRoughness::updateGL() { + if ( !m_isDirty ) { return; } + // manage inherited pending textures + GLTFMaterial::updateGL(); + + // manage specific textures + auto texManager = RadiumEngine::getInstance()->getTextureManager(); + for ( const auto& tex : m_pendingTextures ) { + bool tolinear = ( tex.first == "TEX_BASECOLOR" ); + m_textures[tex.first] = texManager->getOrLoadTexture( tex.second, tolinear ); + } + + m_pendingTextures.clear(); + m_isDirty = false; + + auto& renderParameters = getParameters(); + renderParameters.addParameter( "material.baseColorFactor", m_baseColorFactor ); + renderParameters.addParameter( "material.metallicFactor", m_metallicFactor ); + renderParameters.addParameter( "material.roughnessFactor", m_roughnessFactor ); + + auto tex = getTexture( { "TEX_BASECOLOR" } ); + if ( tex != nullptr ) { + renderParameters.addParameter( "material.baseColor", tex ); + if ( m_baseTextureTransform ) { + auto tr = m_baseTextureTransform->getTransformationAsMatrix(); + renderParameters.addParameter( "material.baseTransform", tr ); + } + } + + tex = getTexture( { "TEX_METALLICROUGHNESS" } ); + if ( tex != nullptr ) { + renderParameters.addParameter( "material.metallicRoughness", tex ); + if ( m_metallicRoughnessTextureTransform ) { + auto tr = m_metallicRoughnessTextureTransform->getTransformationAsMatrix(); + renderParameters.addParameter( "material.metallicRoughnessTransform", tr ); + } + } +} + +void MetallicRoughness::updateFromParameters() { + + GLTFMaterial::updateFromParameters(); + if ( m_isOpenGlConfigured ) { + auto& renderParameters = getParameters(); + m_baseColorFactor = + renderParameters.getParameter( "material.baseColorFactor" ); + m_metallicFactor = renderParameters.getParameter( "material.metallicFactor" ); + m_roughnessFactor = renderParameters.getParameter( "material.roughnessFactor" ); + } +} + +void MetallicRoughness::registerMaterial() { + // gets the resource path of the plugins + auto shaderPath = s_shaderBasePath; + + EngineMaterialConverters::registerMaterialConverter( m_materialName, + MetallicRoughnessMaterialConverter() ); + + auto shaderProgramManager = RadiumEngine::getInstance()->getShaderProgramManager(); + shaderProgramManager->addNamedString( { "/MetallicRoughness.glsl" }, + shaderPath + "/Materials/MetallicRoughness.glsl" ); + + // registering re-usable shaders + auto baseConfiguration = + ShaderConfiguration { m_materialName, + shaderPath + "/Materials/baseGLTFMaterial.vert.glsl", + shaderPath + "/Materials/baseGLTFMaterial_LitOpaque.frag.glsl" }; + baseConfiguration.addInclude( { "\"MetallicRoughness.glsl\"" }, + Ra::Engine::Data::ShaderType::ShaderType_FRAGMENT ); + ShaderConfigurationFactory::addConfiguration( baseConfiguration ); + + auto zprepassConfiguration = + ShaderConfiguration { "ZPrepass" + m_materialName, + shaderPath + "/Materials/baseGLTFMaterial.vert.glsl", + shaderPath + "/Materials/baseGLTFMaterial_Zprepass.frag.glsl" }; + zprepassConfiguration.addInclude( { "\"MetallicRoughness.glsl\"" }, + Ra::Engine::Data::ShaderType::ShaderType_FRAGMENT ); + ShaderConfigurationFactory::addConfiguration( zprepassConfiguration ); + + auto litoitConfiguration = + ShaderConfiguration { "LitOIT" + m_materialName, + shaderPath + "/Materials/baseGLTFMaterial.vert.glsl", + shaderPath + "/Materials/baseGLTFMaterial_LitOIT.frag.glsl" }; + litoitConfiguration.addInclude( { "\"MetallicRoughness.glsl\"" }, + Ra::Engine::Data::ShaderType::ShaderType_FRAGMENT ); + ShaderConfigurationFactory::addConfiguration( litoitConfiguration ); + + EngineRenderTechniques::registerDefaultTechnique( + m_materialName, + + []( RenderTechnique& rt, bool isTransparent ) { + // Configure the technique to render this object using forward Renderer or any + // compatible one Main pass (Mandatory) + auto lpconfig = ShaderConfigurationFactory::getConfiguration( m_materialName ); + rt.setConfiguration( *lpconfig, DefaultRenderingPasses::LIGHTING_OPAQUE ); + + // Z prepass (Recomanded) : DepthAmbiantPass + auto dpconfig = + ShaderConfigurationFactory::getConfiguration( "ZPrepass" + m_materialName ); + rt.setConfiguration( *dpconfig, DefaultRenderingPasses::Z_PREPASS ); + // Transparent pass (Optional) : If Transparent ... add LitOIT + if ( isTransparent ) { + auto tpconfig = + ShaderConfigurationFactory::getConfiguration( "LitOIT" + m_materialName ); + rt.setConfiguration( *tpconfig, DefaultRenderingPasses::LIGHTING_TRANSPARENT ); + } + } ); +} + +void MetallicRoughness::unregisterMaterial() { + // strange bug here, using m_materialName segfault at exit + EngineMaterialConverters::removeMaterialConverter( { "MetallicRoughness" } ); + EngineRenderTechniques::removeDefaultTechnique( { "MetallicRoughness" } ); +} + +std::list MetallicRoughness::getPropertyList() const { + std::list props = GLTFMaterial::getPropertyList(); + if ( m_pendingTextures.find( { "TEX_BASECOLOR" } ) != m_pendingTextures.end() || + getTexture( { "TEX_BASECOLOR" } ) != nullptr ) { + props.emplace_back( "TEXTURE_BASECOLOR" ); + if ( m_baseTextureTransform ) { props.emplace_back( "TEXTURE_COORD_TRANSFORM_BASECOLOR" ); } + } + if ( m_pendingTextures.find( { "TEX_METALLICROUGHNESS" } ) != m_pendingTextures.end() || + getTexture( { "TEX_METALLICROUGHNESS" } ) != nullptr ) { + props.emplace_back( "TEXTURE_METALLICROUGHNESS" ); + if ( m_metallicRoughnessTextureTransform ) { + props.emplace_back( "TEXTURE_COORD_TRANSFORM_METALLICROUGHNESS" ); + } + } + return props; +} + +const Core::Material::GLTFTextureTransform* +MetallicRoughness::getTextureTransform( const TextureSemantic& semantic ) const { + if ( semantic == "TEX_BASECOLOR" ) { return m_baseTextureTransform.get(); } + if ( semantic == "TEX_METALLICROUGHNESS" ) { return m_metallicRoughnessTextureTransform.get(); } + return GLTFMaterial::getTextureTransform( semantic ); +} + +} // namespace Data +} // namespace Engine +} // namespace Ra diff --git a/src/Engine/Data/MetallicRoughnessMaterial.hpp b/src/Engine/Data/MetallicRoughnessMaterial.hpp new file mode 100644 index 00000000000..e59e7feb828 --- /dev/null +++ b/src/Engine/Data/MetallicRoughnessMaterial.hpp @@ -0,0 +1,103 @@ +#pragma once + +#include + +#include + +namespace Ra::Engine { +class Texture; +} // namespace Ra::Engine + +namespace Ra { +namespace Engine { +namespace Data { + +class MetallicRoughnessMaterialConverter; + +/** + * Radium Engine material representation of pbrMetallicRoughness + * + * Texture semantics defined by this material : + * "TEX_BASECOLOR" + * "TEX_METALLICROUGHNESS" + * + */ +class RA_ENGINE_API MetallicRoughness final : public GLTFMaterial +{ + friend class MetallicRoughnessMaterialConverter; + + public: + /** + * Register the material to the Radium Material subsystem + */ + static void registerMaterial(); + + /** + * Remove the material from the Radium material subsystem + */ + static void unregisterMaterial(); + + /** + * Constructor of a named material + * @param instanceName + */ + explicit MetallicRoughness( const std::string& instanceName ); + + /** + * Destructor + */ + ~MetallicRoughness() override; + + /** + * Update the OpenGL component of the material + */ + void updateGL() override; + + /** + * Update the state of the material from its render Parameters + */ + void updateFromParameters() override; + + /** + * Get the list of properties the material migh use in a shader. + */ + [[nodiscard]] std::list getPropertyList() const override; + + /** + * Get the texture transform associated with the given semantic + * @param semantic + * @return a raw pointer to the texture transform, nullptr if thereis no transformation. + * @note ownership is kept by the GLTFMaterial + */ + [[nodiscard]] const Core::Material::GLTFTextureTransform* + getTextureTransform( const TextureSemantic& semantic ) const override; + + /******************************************************************/ + const Ra::Core::Utils::Color& getBaseColorFactor() const { return m_baseColorFactor; } + void setBaseColorFactor( const Ra::Core::Utils::Color& baseColorFactor ) { + m_baseColorFactor = baseColorFactor; + } + + float getMetallicFactor() const { return m_metallicFactor; } + void setMetallicFactor( float metallicFactor ) { m_metallicFactor = metallicFactor; } + + float getRoughnessFactor() const { return m_roughnessFactor; } + void setRoughnessFactor( float roughnessFactor ) { m_roughnessFactor = roughnessFactor; } + + /******************************************************************/ + + private: + // attributes of MetallicRoughness + Ra::Core::Utils::Color m_baseColorFactor { 1.0, 1.0, 1.0, 1.0 }; + float m_metallicFactor { 1 }; + float m_roughnessFactor { 1 }; + static const std::string m_materialName; + + std::unique_ptr m_baseTextureTransform { nullptr }; + std::unique_ptr m_metallicRoughnessTextureTransform { + nullptr }; +}; + +} // namespace Data +} // namespace Engine +} // namespace Ra diff --git a/src/Engine/Data/MetallicRoughnessMaterialConverter.cpp b/src/Engine/Data/MetallicRoughnessMaterialConverter.cpp new file mode 100644 index 00000000000..82b5c04728f --- /dev/null +++ b/src/Engine/Data/MetallicRoughnessMaterialConverter.cpp @@ -0,0 +1,38 @@ +#include +#include +#include + +namespace Ra { +namespace Engine { +namespace Data { +using namespace Ra::Core::Asset; + +Material* +MetallicRoughnessMaterialConverter::operator()( const Ra::Core::Asset::MaterialData* toconvert ) { + auto result = new MetallicRoughness( toconvert->getName() ); + auto source = static_cast( toconvert ); + + result->fillBaseFrom( source ); + + result->m_baseColorFactor = source->m_baseColorFactor; + if ( source->m_hasBaseColorTexture ) { + result->addTexture( + { "TEX_BASECOLOR" }, source->m_baseColorTexture, source->m_baseSampler ); + result->m_baseTextureTransform = std::move( source->m_baseTextureTransform ); + } + result->m_metallicFactor = source->m_metallicFactor; + result->m_roughnessFactor = source->m_roughnessFactor; + if ( source->m_hasMetallicRoughnessTexture ) { + result->addTexture( { "TEX_METALLICROUGHNESS" }, + source->m_metallicRoughnessTexture, + source->m_metallicRoughnessSampler ); + result->m_metallicRoughnessTextureTransform = + std::move( source->m_metallicRoughnessTextureTransform ); + } + + return result; +} + +} // namespace Data +} // namespace Engine +} // namespace Ra diff --git a/src/Engine/Data/MetallicRoughnessMaterialConverter.hpp b/src/Engine/Data/MetallicRoughnessMaterialConverter.hpp new file mode 100644 index 00000000000..c89c9557301 --- /dev/null +++ b/src/Engine/Data/MetallicRoughnessMaterialConverter.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include +#include + +namespace Ra { +namespace Engine { +namespace Data { +/** + * Radium IO to Engine conversion for pbrMetallicRoughness + */ +class RA_ENGINE_API MetallicRoughnessMaterialConverter +{ + public: + MetallicRoughnessMaterialConverter() = default; + + ~MetallicRoughnessMaterialConverter() = default; + + Ra::Engine::Data::Material* operator()( const Ra::Core::Asset::MaterialData* toconvert ); +}; + +} // namespace Data +} // namespace Engine +} // namespace Ra diff --git a/src/Engine/Data/SpecularGlossinessMaterial.cpp b/src/Engine/Data/SpecularGlossinessMaterial.cpp new file mode 100644 index 00000000000..3e9aabd8906 --- /dev/null +++ b/src/Engine/Data/SpecularGlossinessMaterial.cpp @@ -0,0 +1,179 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Ra { +namespace Engine { +namespace Data { + +using namespace Ra::Engine::Rendering; + +const std::string SpecularGlossiness::m_materialName { "SpecularGlossiness" }; + +SpecularGlossiness::SpecularGlossiness( const std::string& instanceName ) : + GLTFMaterial( instanceName, m_materialName ) {} + +SpecularGlossiness::~SpecularGlossiness() = default; + +void SpecularGlossiness::updateGL() { + if ( !m_isDirty ) { return; } + // manage inherited pending textures + GLTFMaterial::updateGL(); + // manage specific textures + auto texManager = RadiumEngine::getInstance()->getTextureManager(); + for ( const auto& tex : m_pendingTextures ) { + bool tolinear = ( tex.first == "TEX_DIFFUSE" || tex.first == "TEX_SPECULARGLOSSINESS" ); + m_textures[tex.first] = texManager->getOrLoadTexture( tex.second, tolinear ); + } + + m_pendingTextures.clear(); + m_isDirty = false; + auto& renderParameters = getParameters(); + renderParameters.addParameter( "material.diffuseFactor", m_diffuseFactor ); + renderParameters.addParameter( "material.specularFactor", m_specularFactor ); + renderParameters.addParameter( "material.glossinessFactor", m_glossinessFactor ); + + auto tex = getTexture( { "TEX_DIFFUSE" } ); + if ( tex != nullptr ) { + renderParameters.addParameter( "material.diffuse", tex ); + if ( m_diffuseTextureTransform ) { + auto ct = std::cos( m_diffuseTextureTransform->rotation ); + auto st = std::sin( m_diffuseTextureTransform->rotation ); + Ra::Core::Matrix3 tr; + tr << m_diffuseTextureTransform->scale[0] * ct, + -m_diffuseTextureTransform->scale[1] * st, m_diffuseTextureTransform->offset[0], + m_diffuseTextureTransform->scale[0] * st, m_diffuseTextureTransform->scale[1] * ct, + m_diffuseTextureTransform->offset[1], 0, 0, 1; + renderParameters.addParameter( "material.diffuseTransform", tr ); + } + } + + tex = getTexture( { "TEX_SPECULARGLOSSINESS" } ); + if ( tex != nullptr ) { + renderParameters.addParameter( "material.specularGlossiness", tex ); + if ( m_specularGlossinessTransform ) { + auto ct = std::cos( m_specularGlossinessTransform->rotation ); + auto st = std::sin( m_specularGlossinessTransform->rotation ); + Ra::Core::Matrix3 tr; + tr << m_specularGlossinessTransform->scale[0] * ct, + -m_specularGlossinessTransform->scale[1] * st, + m_specularGlossinessTransform->offset[0], + m_specularGlossinessTransform->scale[0] * st, + m_specularGlossinessTransform->scale[1] * ct, + m_specularGlossinessTransform->offset[1], 0, 0, 1; + renderParameters.addParameter( "material.specularGlossinessTransform", tr ); + } + } +} + +void SpecularGlossiness::updateFromParameters() { + GLTFMaterial::updateFromParameters(); + if ( m_isOpenGlConfigured ) { + auto& renderParameters = getParameters(); + m_diffuseFactor = + renderParameters.getParameter( "material.diffuseFactor" ); + m_specularFactor = + renderParameters.getParameter( "material.specularFactor" ); + m_glossinessFactor = renderParameters.getParameter( "material.glossinessFactor" ); + } +} + +void SpecularGlossiness::registerMaterial() { + // gets the resource path of the plugins + auto shaderPath = s_shaderBasePath; + + EngineMaterialConverters::registerMaterialConverter( m_materialName, + SpecularGlossinessMaterialConverter() ); + + auto shaderProgramManager = RadiumEngine::getInstance()->getShaderProgramManager(); + shaderProgramManager->addNamedString( { "/SpecularGlossiness.glsl" }, + shaderPath + "/Materials/SpecularGlossiness.glsl" ); + + // registering re-usable shaders + auto baseConfiguration = + ShaderConfiguration { m_materialName, + shaderPath + "/Materials/baseGLTFMaterial.vert.glsl", + shaderPath + "/Materials/baseGLTFMaterial_LitOpaque.frag.glsl" }; + baseConfiguration.addInclude( { "\"SpecularGlossiness.glsl\"" }, + Ra::Engine::Data::ShaderType::ShaderType_FRAGMENT ); + ShaderConfigurationFactory::addConfiguration( baseConfiguration ); + + auto zprepassConfiguration = + ShaderConfiguration { "ZPrepass" + m_materialName, + shaderPath + "/Materials/baseGLTFMaterial.vert.glsl", + shaderPath + "/Materials/baseGLTFMaterial_Zprepass.frag.glsl" }; + zprepassConfiguration.addInclude( { "\"SpecularGlossiness.glsl\"" }, + Ra::Engine::Data::ShaderType::ShaderType_FRAGMENT ); + ShaderConfigurationFactory::addConfiguration( zprepassConfiguration ); + + auto litoitConfiguration = + ShaderConfiguration { "LitOIT" + m_materialName, + shaderPath + "/Materials/baseGLTFMaterial.vert.glsl", + shaderPath + "/Materials/baseGLTFMaterial_LitOIT.frag.glsl" }; + litoitConfiguration.addInclude( { "\"SpecularGlossiness.glsl\"" }, + Ra::Engine::Data::ShaderType::ShaderType_FRAGMENT ); + ShaderConfigurationFactory::addConfiguration( litoitConfiguration ); + + EngineRenderTechniques::registerDefaultTechnique( + m_materialName, + + []( RenderTechnique& rt, bool isTransparent ) { + // Configure the technique to render this object using forward Renderer or any + // compatible one Main pass (Mandatory) : BlinnPhong + auto lpconfig = ShaderConfigurationFactory::getConfiguration( m_materialName ); + rt.setConfiguration( *lpconfig, DefaultRenderingPasses::LIGHTING_OPAQUE ); + + // Z prepass (Reccomanded) : DepthAmbiantPass + auto dpconfig = + ShaderConfigurationFactory::getConfiguration( "ZPrepass" + m_materialName ); + rt.setConfiguration( *dpconfig, DefaultRenderingPasses::Z_PREPASS ); + // Uber is sometimes transparent ... + // Transparent pass (Optional) : If Transparent ... add LitOIT + if ( isTransparent ) { + auto tpconfig = + ShaderConfigurationFactory::getConfiguration( "LitOIT" + m_materialName ); + rt.setConfiguration( *tpconfig, DefaultRenderingPasses::LIGHTING_TRANSPARENT ); + } + } ); +} + +void SpecularGlossiness::unregisterMaterial() { + // strange bug here, using m_materialName segfault at exit + EngineRenderTechniques::removeDefaultTechnique( { "SpecularGlossiness" } ); + EngineMaterialConverters::removeMaterialConverter( { "SpecularGlossiness" } ); +} + +std::list SpecularGlossiness::getPropertyList() const { + std::list props = GLTFMaterial::getPropertyList(); + if ( m_pendingTextures.find( { "TEX_DIFFUSE" } ) != m_pendingTextures.end() || + getTexture( { "TEX_DIFFUSE" } ) != nullptr ) { + props.emplace_back( "TEXTURE_DIFFUSE" ); + if ( m_diffuseTextureTransform ) { + props.emplace_back( "TEXTURE_COORD_TRANSFORM_DIFFUSE" ); + } + } + if ( m_pendingTextures.find( { "TEX_SPECULARGLOSSINESS" } ) != m_pendingTextures.end() || + getTexture( { "TEX_SPECULARGLOSSINESS" } ) != nullptr ) { + props.emplace_back( "TEXTURE_SPECULARGLOSSINESS" ); + if ( m_specularGlossinessTransform ) { + props.emplace_back( "TEXTURE_COORD_TRANSFORM_SPECULARGLOSSINESS" ); + } + } + return props; +} + +const Core::Material::GLTFTextureTransform* +SpecularGlossiness::getTextureTransform( const TextureSemantic& semantic ) const { + if ( semantic == "TEX_DIFFUSE" ) { return m_diffuseTextureTransform.get(); } + if ( semantic == "TEX_SPECULARGLOSSINESS" ) { return m_specularGlossinessTransform.get(); } + return GLTFMaterial::getTextureTransform( semantic ); +} + +} // namespace Data +} // namespace Engine +} // namespace Ra diff --git a/src/Engine/Data/SpecularGlossinessMaterial.hpp b/src/Engine/Data/SpecularGlossinessMaterial.hpp new file mode 100644 index 00000000000..26176847428 --- /dev/null +++ b/src/Engine/Data/SpecularGlossinessMaterial.hpp @@ -0,0 +1,117 @@ +#pragma once +#include + +#include + +namespace Ra::Engine { +class Texture; +} // namespace Ra::Engine + +namespace Ra { +namespace Engine { +namespace Data { + +class SpecularGlossinessMaterialConverter; +/** + * Radium Engine material representation of GLTF SpecularGlossiness Material + * Texture semantics defined by this material : + * "TEX_DIFFUSE" + * "TEX_SPECULARGLOSSINESS" + * + */ +class RA_ENGINE_API SpecularGlossiness final : public GLTFMaterial +{ + friend class SpecularGlossinessMaterialConverter; + + public: + public: + /** + * Register the material to the Radium Material subsystem + */ + static void registerMaterial(); + + /** + * Remove the material from the Radium material subsystem + */ + static void unregisterMaterial(); + + /** + * Constructor of a named material + * @param instanceName + */ + explicit SpecularGlossiness( const std::string& instanceName ); + + /** + * Destructor + */ + ~SpecularGlossiness() override; + + /** + * Update the OpenGL component of the material + */ + void updateGL() override; + + /** + * Update the state of the material from its render Parameters + */ + void updateFromParameters() override; + + /** + * Get the list of properties the material migh use in a shader. + */ + [[nodiscard]] std::list getPropertyList() const override; + + /** + * Get the texture transform associated with the given semantic + * @param semantic + * @return a raw pointer to the texture transform, nullptr if thereis no transformation. + * @note ownership is kept by the GLTFMaterial + */ + [[nodiscard]] const Core::Material::GLTFTextureTransform* + getTextureTransform( const TextureSemantic& semantic ) const override; + + /******************************************************************/ + /** + * @return the diffuse factor of the material + */ + const Ra::Core::Utils::Color& getDiffuseFactor() const { return m_diffuseFactor; } + /** + * @param diffuseFactor the diffuse factor to set + */ + void setDiffuseFactor( const Ra::Core::Utils::Color& diffuseFactor ) { + m_diffuseFactor = diffuseFactor; + } + /** + * @return the specular factor of the material + */ + const Ra::Core::Utils::Color& getSpecularFactor() const { return m_specularFactor; } + /** + * @param specularFactor the specular factor to set + */ + void setSpecularFactor( const Ra::Core::Utils::Color& specularFactor ) { + m_specularFactor = specularFactor; + } + /** + * @return the glossiness factor of the material + */ + float getGlossinessFactor() const { return m_glossinessFactor; } + /** + * @param glossinessFactor the glossiness factor to set + */ + void setGlossinessFactor( float glossinessFactor ) { m_glossinessFactor = glossinessFactor; } + /******************************************************************/ + private: + // attributes of SpecularGlossiness + Ra::Core::Utils::Color m_diffuseFactor { 1.0, 1.0, 1.0, 1.0 }; + Ra::Core::Utils::Color m_specularFactor { 1.0, 1.0, 1.0, 1.0 }; + float m_glossinessFactor { 1 }; + + static const std::string m_materialName; + + std::unique_ptr m_diffuseTextureTransform { nullptr }; + std::unique_ptr m_specularGlossinessTransform { nullptr }; +}; + +} // namespace Data +} // namespace Engine +} // namespace Ra diff --git a/src/Engine/Data/SpecularGlossinessMaterialConverter.cpp b/src/Engine/Data/SpecularGlossinessMaterialConverter.cpp new file mode 100644 index 00000000000..b76cdcd709d --- /dev/null +++ b/src/Engine/Data/SpecularGlossinessMaterialConverter.cpp @@ -0,0 +1,36 @@ +#include +#include +#include + +namespace Ra { +namespace Engine { +namespace Data { + +using namespace Ra::Core::Asset; + +Material* SpecularGlossinessMaterialConverter::operator()( const MaterialData* toconvert ) { + auto result = new SpecularGlossiness( toconvert->getName() ); + auto source = static_cast( toconvert ); + + result->fillBaseFrom( source ); + + result->m_diffuseFactor = source->m_diffuseFactor; + if ( source->m_hasDiffuseTexture ) { + result->addTexture( { "TEX_DIFFUSE" }, source->m_diffuseTexture, source->m_diffuseSampler ); + result->m_diffuseTextureTransform = std::move( source->m_diffuseTextureTransform ); + } + result->m_specularFactor = source->m_specularFactor; + result->m_glossinessFactor = source->m_glossinessFactor; + if ( source->m_hasSpecularGlossinessTexture ) { + result->addTexture( { "TEX_SPECULARGLOSSINESS" }, + source->m_specularGlossinessTexture, + source->m_specularGlossinessSampler ); + result->m_specularGlossinessTransform = std::move( source->m_specularGlossinessTransform ); + } + + return result; +} + +} // namespace Data +} // namespace Engine +} // namespace Ra diff --git a/src/Engine/Data/SpecularGlossinessMaterialConverter.hpp b/src/Engine/Data/SpecularGlossinessMaterialConverter.hpp new file mode 100644 index 00000000000..fe16bca886b --- /dev/null +++ b/src/Engine/Data/SpecularGlossinessMaterialConverter.hpp @@ -0,0 +1,25 @@ +#pragma once +#include + +#include +#include + +namespace Ra { +namespace Engine { +namespace Data { +/** + * Radium IO to Engine conversion for pbrSpecularGlossiness + */ +class RA_ENGINE_API SpecularGlossinessMaterialConverter +{ + public: + SpecularGlossinessMaterialConverter() = default; + + ~SpecularGlossinessMaterialConverter() = default; + + Ra::Engine::Data::Material* operator()( const Ra::Core::Asset::MaterialData* toconvert ); +}; + +} // namespace Data +} // namespace Engine +} // namespace Ra diff --git a/src/Engine/RadiumEngine.cpp b/src/Engine/RadiumEngine.cpp index be78eb36167..39040481cec 100644 --- a/src/Engine/RadiumEngine.cpp +++ b/src/Engine/RadiumEngine.cpp @@ -8,7 +8,8 @@ #include #include #include -#include +// #include +#include #include #include #include @@ -136,6 +137,8 @@ void RadiumEngine::registerDefaultPrograms() { Data::BlinnPhongMaterial::registerMaterial(); Data::LambertianMaterial::registerMaterial(); Data::VolumetricMaterial::registerMaterial(); + // Load gltf material resources + Data::GLTFMaterial::registerMaterial(); } void RadiumEngine::cleanup() { diff --git a/src/Engine/filelist.cmake b/src/Engine/filelist.cmake index 274f82178ba..0dcd036441e 100644 --- a/src/Engine/filelist.cmake +++ b/src/Engine/filelist.cmake @@ -8,10 +8,13 @@ set(engine_sources Data/BlinnPhongMaterial.cpp Data/DrawPrimitives.cpp Data/EnvironmentTexture.cpp + Data/GLTFMaterial.cpp Data/LambertianMaterial.cpp Data/Material.cpp Data/MaterialConverters.cpp Data/Mesh.cpp + Data/MetallicRoughnessMaterial.cpp + Data/MetallicRoughnessMaterialConverter.cpp Data/PlainMaterial.cpp Data/RawShaderMaterial.cpp Data/RenderParameters.cpp @@ -20,6 +23,8 @@ set(engine_sources Data/ShaderProgram.cpp Data/ShaderProgramManager.cpp Data/SimpleMaterial.cpp + Data/SpecularGlossinessMaterial.cpp + Data/SpecularGlossinessMaterialConverter.cpp Data/Texture.cpp Data/TextureManager.cpp Data/VolumeObject.cpp @@ -61,10 +66,13 @@ set(engine_headers Data/DisplayableObject.hpp Data/DrawPrimitives.hpp Data/EnvironmentTexture.hpp + Data/GLTFMaterial.hpp Data/LambertianMaterial.hpp Data/Material.hpp Data/MaterialConverters.hpp Data/Mesh.hpp + Data/MetallicRoughnessMaterial.hpp + Data/MetallicRoughnessMaterialConverter.hpp Data/PlainMaterial.hpp Data/RawShaderMaterial.hpp Data/RenderParameters.hpp @@ -73,6 +81,8 @@ set(engine_headers Data/ShaderProgram.hpp Data/ShaderProgramManager.hpp Data/SimpleMaterial.hpp + Data/SpecularGlossinessMaterial.hpp + Data/SpecularGlossinessMaterialConverter.hpp Data/Texture.hpp Data/TextureManager.hpp Data/ViewingParameters.hpp @@ -141,6 +151,13 @@ set(engine_shaders Materials/BlinnPhong/BlinnPhong.vert.glsl Materials/BlinnPhong/BlinnPhongZPrepass.frag.glsl Materials/BlinnPhong/LitOITBlinnPhong.frag.glsl + Materials/GLTF/Materials/MetallicRoughness.glsl + Materials/GLTF/Materials/SpecularGlossiness.glsl + Materials/GLTF/Materials/baseGLTFMaterial.glsl + Materials/GLTF/Materials/baseGLTFMaterial.vert.glsl + Materials/GLTF/Materials/baseGLTFMaterial_LitOIT.frag.glsl + Materials/GLTF/Materials/baseGLTFMaterial_LitOpaque.frag.glsl + Materials/GLTF/Materials/baseGLTFMaterial_Zprepass.frag.glsl Materials/Lambertian/Lambertian.frag.glsl Materials/Lambertian/Lambertian.glsl Materials/Lambertian/Lambertian.vert.glsl From fd8553fe52eba0d3e0d6ebec432fb657605e838b Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Wed, 19 Jul 2023 19:13:46 +0200 Subject: [PATCH 03/27] [shaders] add gltf material support --- .../Materials/GLTF/BSDF_LUTs/lut_charlie.png | Bin 0 -> 61735 bytes Shaders/Materials/GLTF/BSDF_LUTs/lut_ggx.png | Bin 0 -> 94738 bytes .../Materials/GLTF/BSDF_LUTs/lut_sheen_E.png | Bin 0 -> 416421 bytes .../GLTF/Materials/MetallicRoughness.glsl | 108 +++ .../GLTF/Materials/SpecularGlossiness.glsl | 104 +++ .../GLTF/Materials/baseGLTFMaterial.glsl | 690 ++++++++++++++++++ .../GLTF/Materials/baseGLTFMaterial.vert.glsl | 40 + .../baseGLTFMaterial_LitOIT.frag.glsl | 69 ++ .../baseGLTFMaterial_LitOpaque.frag.glsl | 43 ++ .../baseGLTFMaterial_Zprepass.frag.glsl | 32 + .../Materials/GLTF/Metadata/GlTFMaterial.json | 220 ++++++ 11 files changed, 1306 insertions(+) create mode 100644 Shaders/Materials/GLTF/BSDF_LUTs/lut_charlie.png create mode 100644 Shaders/Materials/GLTF/BSDF_LUTs/lut_ggx.png create mode 100644 Shaders/Materials/GLTF/BSDF_LUTs/lut_sheen_E.png create mode 100644 Shaders/Materials/GLTF/Materials/MetallicRoughness.glsl create mode 100644 Shaders/Materials/GLTF/Materials/SpecularGlossiness.glsl create mode 100644 Shaders/Materials/GLTF/Materials/baseGLTFMaterial.glsl create mode 100644 Shaders/Materials/GLTF/Materials/baseGLTFMaterial.vert.glsl create mode 100644 Shaders/Materials/GLTF/Materials/baseGLTFMaterial_LitOIT.frag.glsl create mode 100644 Shaders/Materials/GLTF/Materials/baseGLTFMaterial_LitOpaque.frag.glsl create mode 100644 Shaders/Materials/GLTF/Materials/baseGLTFMaterial_Zprepass.frag.glsl create mode 100644 Shaders/Materials/GLTF/Metadata/GlTFMaterial.json diff --git a/Shaders/Materials/GLTF/BSDF_LUTs/lut_charlie.png b/Shaders/Materials/GLTF/BSDF_LUTs/lut_charlie.png new file mode 100644 index 0000000000000000000000000000000000000000..7e6c0db3583045f77250312e7cdba20f35790edf GIT binary patch literal 61735 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

hzKs`a8HHI5bM%~U$>3)aIb)Rqyn4TG@0jpz0 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 Y?1Z0&W%5rI+7EW`-1|%RcAx+Le`k&xJOBUy literal 0 HcmV?d00001 diff --git a/Shaders/Materials/GLTF/BSDF_LUTs/lut_ggx.png b/Shaders/Materials/GLTF/BSDF_LUTs/lut_ggx.png new file mode 100644 index 0000000000000000000000000000000000000000..40dd648eaa2018ca65dfaae0f01ce0aa6dfc8171 GIT binary patch literal 94738 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�XE2zI>;U{>^={0?5Cj8Z4IXV=$Y2 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 literal 0 HcmV?d00001 diff --git a/Shaders/Materials/GLTF/BSDF_LUTs/lut_sheen_E.png b/Shaders/Materials/GLTF/BSDF_LUTs/lut_sheen_E.png new file mode 100644 index 0000000000000000000000000000000000000000..03ac3f22bde870c9f5e7a6ed3a97a17a3ee8bfce GIT binary patch literal 416421 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ⅇ+%qA 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 z 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

epqq&g+ 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 0.0 ) { return 0.5 / GGX; } + return 0.0; +} + +float D_GGX( float NdotH, float alphaRoughness ) { + float alphaRoughnessSq = alphaRoughness * alphaRoughness; + float f = ( NdotH * NdotH ) * ( alphaRoughnessSq - 1.0 ) + 1.0; + return alphaRoughnessSq / ( M_PI * f * f ); +} + +vec3 BRDF_lambertian( vec3 diffuseColor, float VdotH, vec3 f0, vec3 f90, float specularWeight ) { + // see https://seblagarde.wordpress.com/2012/01/08/pi-or-not-to-pi-in-game-lighting-equation/ + return ( 1.0 - specularWeight * F_Schlick( f0, f90, VdotH ) ) * ( diffuseColor / M_PI ); +} + +vec3 BRDF_specularGGX( vec3 f0, + vec3 f90, + float alphaRoughness, + float specularWeight, + float VdotH, + float NdotL, + float NdotV, + float NdotH ) { + vec3 F = F_Schlick( f0, f90, VdotH ); + float Vis = V_GGX( NdotL, NdotV, alphaRoughness ); + float D = D_GGX( NdotH, alphaRoughness ); + + return specularWeight * F * Vis * D; +} + +/* -- Layer extensions -- */ +#ifdef CLEARCOAT_LAYER +// definition of the clearcoat layer +struct Clearcoat { + float clearcoatFactor; + float clearcoatRoughnessFactor; +# ifdef TEXTURE_CLEARCOAT + sampler2D clearcoatTexture; +# ifdef TEXTURE_COORD_TRANSFORM_CLEARCOAT + mat3 clearcoatTextureTransform; +# endif +# endif +# ifdef TEXTURE_CLEARCOATROUGHNESS + sampler2D clearcoatRoughnessTexture; +# ifdef TEXTURE_COORD_TRANSFORM_CLEARCOATROUGHNESS + mat3 clearcoatRoughnessTextureTransform; +# endif +# endif +# ifdef TEXTURE_CLEARCOATNORMAL + float clearcoatNormalTextureScale; + sampler2D clearcoatNormalTexture; +# ifdef TEXTURE_COORD_TRANSFORM_CLEARCOATNORMAL + mat3 clearcoatNormalTextureTransform; +# endif +# endif +}; +#endif + +#ifdef SPECULAR_LAYER +struct Specular { + float specularFactor; + vec4 specularColorFactor; +# ifdef TEXTURE_SPECULAR_EXT + sampler2D specularTexture; +# ifdef TEXTURE_COORD_TRANSFORM_SPECULAR_EXT + mat3 specularTextureTransform; +# endif +# endif +# ifdef TEXTURE_SPECULARCOLOR_EXT + sampler2D specularColorTexture; +# ifdef TEXTURE_COORD_TRANSFORM_SPECULARCOLOR_EXT + mat3 specularColorTextureTransform; +# endif +# endif +}; +#endif + +#ifdef SHEEN_LAYER +struct Sheen { + sampler2D sheenE_LUT; + sampler2D charlieLUT; + + float sheenRoughnessFactor; + vec4 sheenColorFactor; + +# ifdef TEXTURE_SHEEN_COLOR + sampler2D sheenColorTexture; +# ifdef TEXTURE_COORD_TRANSFORM_SHEEN_COLOR + mat3 sheenColorTextureTransform; +# endif +# endif + +# ifdef TEXTURE_SHEEN_ROUGHNESS + sampler2D sheenRoughnessTexture; +# ifdef TEXTURE_COORD_TRANSFORM_SHEEN_ROUGHNESS + mat3 sheenRoughnessTextureTransform; +# endif +# endif +}; +#endif + +/* -- Base material -- */ +struct GLTFBaseMaterial { + vec4 emissiveFactor; + uint alphaMode; // 0 --> Opaque, 1 --> Mask, 2 --> Blend + int doubleSided; + float alphaCutoff; + float ior; +#ifdef TEXTURE_NORMAL + float normalTextureScale; + sampler2D normal; +# ifdef TEXTURE_COORD_TRANSFORM_NORMAL + mat3 normalTransform; +# endif +#endif +#ifdef TEXTURE_OCCLUSION + float occlusionStrength; + sampler2D occlusion; +# ifdef TEXTURE_COORD_TRANSFORM_OCCLUSION + mat3 occlusionTransform; +# endif +#endif +#ifdef TEXTURE_EMISSIVE + sampler2D emissive; +# ifdef TEXTURE_COORD_TRANSFORM_EMISSIVE + mat3 emmissiveTransform; +# endif +#endif +#ifdef CLEARCOAT_LAYER + Clearcoat clearcoat; +#endif +#ifdef SPECULAR_LAYER + Specular specular; +#endif +#ifdef SHEEN_LAYER + Sheen sheen; +#endif + // Global pre-computed data for BSDF evaluation + sampler2D ggxLut; +}; + +// Encapsulate the various inputs used by the various functions in the shading equation +// We store values in these struct to simplify the integration of alternative implementations +// of the shading terms, outlined in the Readme.MD Appendix. + +// Based on : +// https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/master/source/Renderer/shaders/material_info.glsl +// https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/master/source/Renderer/shaders/pbr.frag + +/* + * Definestructure and functions to access wormal and world-space to local space transformation + * mapping + */ +// the struct Normal info contains all vectors in world space +struct NormalInfo { + vec3 ng; // Geometry normal + vec3 t; // Geometry tangent + vec3 b; // Geometry bitangent + vec3 n; // Shading normal +}; + +NormalInfo getNormalInfo( GLTFBaseMaterial material, vec3 texCoord ) { + // if tangent are usable, just get the interpolated TBN matrix. + // For now, compute it. + NormalInfo res; + res.ng = getWorldSpaceNormal(); + res.t = getWorldSpaceTangent(); + res.b = cross( res.ng, res.t ); + // For a back-facing surface, the tangential basis vectors are negated. + if ( gl_FrontFacing == false ) + { + res.t *= -1.0; + res.b *= -1.0; + res.ng *= -1.0; + } +#ifdef TEXTURE_NORMAL + vec2 ct = texCoord.xy; +# ifdef TEXTURE_COORD_TRANSFORM_NORMAL + ct = ( material.normalTransform * vec3( ct, 1 ) ).xy; +# endif + res.n = texture( material.normal, ct ).rgb * 2.0 - 1.0; + res.n *= vec3( material.normalTextureScale, material.normalTextureScale, 1.0 ); + res.n = normalize( res.n ); +# ifdef SHADER_DEBUG + res.ntex = res.n; +# endif + res.n = normalize( mat3( res.t, res.b, res.ng ) * res.n ); +#else + res.n = res.ng; +#endif + return res; +} + +#ifdef CLEARCOAT_LAYER +struct ClearcoatInfo { + vec3 f0; // will be the same as the base layer f0 + vec3 f90; // set to 1 in the spec and ref implementation + vec2 intrough; // x contains the clearcoat intensity, y its roughness + vec3 normal; // contains the normal of the clearcoat layer +}; + +ClearcoatInfo getClearcoatInfo( Clearcoat u_clearcoat, NormalInfo nrm, vec3 f0, vec3 texCoord ) { + ClearcoatInfo res; + res.intrough = vec2( u_clearcoat.clearcoatFactor, u_clearcoat.clearcoatRoughnessFactor ); + res.f0 = f0; + res.f90 = vec3( 1. ); + + vec2 ct; +# ifdef TEXTURE_CLEARCOAT +# ifdef TEXTURE_COORD_TRANSFORM_CLEARCOAT + ct = ( u_clearcoat.clearcoatTextureTransform * texCoord ).xy; +# else + ct = texCoord.xy; +# endif + res.intrough.x *= texture( u_clearcoat.clearcoatTexture, ct ).r; +# endif + +# ifdef TEXTURE_CLEARCOATROUGHNESS +# ifdef TEXTURE_COORD_TRANSFORM_CLEARCOATROUGHNESS + ct = ( u_clearcoat.clearcoatRoughnessTextureTransform * texCoord ).xy; +# else + ct = texCoord.xy; +# endif + res.intrough.y *= texture( u_clearcoat.clearcoatRoughnessTexture, ct ).g; +# endif + +# ifdef TEXTURE_CLEARCOATNORMAL +# ifdef TEXTURE_COORD_TRANSFORM_CLEARCOATNORMAL + ct = ( u_clearcoat.clearcoatNormalTextureTransform * texCoord ).xy; +# else + ct = texCoord.xy; +# endif + res.normal = normalize( ( texture( u_clearcoat.clearcoatNormalTexture, ct ).rgb * 2 - 1 ) * + vec3( u_clearcoat.clearcoatNormalTextureScale, + u_clearcoat.clearcoatNormalTextureScale, + 1 ) ); + res.normal = mat3( nrm.t, nrm.b, nrm.ng ) * res.normal; +# else + res.normal = nrm.ng; +# endif + res.intrough.y = clamp( res.intrough.y, 0., 1. ); + return res; +} + +vec3 getPunctualRadianceClearCoat( ClearcoatInfo clearcoat, vec3 v, vec3 l, vec3 h, float vdoth ) { + float ndotl = clampedDot( clearcoat.normal, l ); + float ndotv = clampedDot( clearcoat.normal, v ); + float ndoth = clampedDot( clearcoat.normal, h ); + return ndotl * BRDF_specularGGX( clearcoat.f0, + clearcoat.f90, + clearcoat.intrough.y * clearcoat.intrough.y, + 1., + vdoth, + ndotl, + ndotv, + ndoth ); +} +#endif + +#ifdef SHEEN_LAYER +struct SheenInfo { + // rgb --> sheen color; a --> sheen roughness + vec4 sheenColorRough; +}; + +SheenInfo getSheenInfo( Sheen u_sheen, vec3 texCoord ) { + SheenInfo res; + res.sheenColorRough.rgb = u_sheen.sheenColorFactor.rgb; + res.sheenColorRough.a = u_sheen.sheenRoughnessFactor; + vec2 tc; +# ifdef TEXTURE_SHEEN_COLOR +# ifdef TEXTURE_COORD_TRANSFORM_SHEEN_COLOR + tc = ( u_sheen.sheenColorTextureTransform * texCoord ).xy; +# else + tc = texCoord.xy; +# endif + res.sheenColorRough.rgb *= texture( u_sheen.sheenColorTexture, tc ).rgb; +# endif + +# ifdef TEXTURE_SHEEN_ROUGHNESS +# ifdef TEXTURE_COORD_TRANSFORM_SHEEN_ROUGHNESS + tc = ( u_sheen.sheenRoughnessTextureTransform * texCoord ).xy; +# else + tc = texCoord.xy; +# endif + res.sheenColorRough.a *= texture( u_sheen.sheenRoughnessTexture, tc ).a; +# endif + res.sheenColorRough.a = clamp( res.sheenColorRough.a, 0., 1. ); + return res; +} + +float albedoSheenScalingLUT( Sheen u_sheen, float NdotV, float roughness ) { + return texture(u_sheen.sheenE_LUT, vec2(NdotV, roughness)).r; +} + +float D_Charlie( float roughness, float NdotH ) { + float alphaG = roughness * roughness; + float invR = 1.0 / alphaG; + float cos2h = NdotH * NdotH; + float sin2h = 1.0 - cos2h; + return ( 2.0 + invR ) * pow( sin2h, invR * 0.5 ) / ( 2.0 * M_PI ); +} + +float lambdaSheenNumericHelper( float x, float alphaG ) { + float oneMinusAlphaSq = ( 1.0 - alphaG ) * ( 1.0 - alphaG ); + float a = mix( 21.5473, 25.3245, oneMinusAlphaSq ); + float b = mix( 3.82987, 3.32435, oneMinusAlphaSq ); + float c = mix( 0.19823, 0.16801, oneMinusAlphaSq ); + float d = mix( -1.97760, -1.27393, oneMinusAlphaSq ); + float e = mix( -4.32054, -4.85967, oneMinusAlphaSq ); + return a / ( 1.0 + b * pow( x, c ) ) + d * x + e; +} + +float lambdaSheen( float cosTheta, float alphaG ) { + if ( abs( cosTheta ) < 0.5 ) { return exp( lambdaSheenNumericHelper( cosTheta, alphaG ) ); } + else + { + return exp( 2.0 * lambdaSheenNumericHelper( 0.5, alphaG ) - + lambdaSheenNumericHelper( 1.0 - cosTheta, alphaG ) ); + } +} + +// according to the spec, this could be optimized using the "Aschikmin approximation" +// see the following : +// https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_sheen +// https://dassaultsystemes-technology.github.io/EnterprisePBRShadingModel/spec-2021x.md.html#components/sheen +float V_Sheen( float NdotL, float NdotV, float roughness ) { + // This is the "Charlie", full precision visibility + roughness = max( roughness, 0.000001 ); // clamp (0,1] + float alphaG = roughness * roughness; + + return clamp( 1.0 / ( ( 1.0 + lambdaSheen( NdotV, alphaG ) + lambdaSheen( NdotL, alphaG ) ) * + ( 4.0 * NdotV * NdotL ) ), + 0.0, + 1.0 ); +} + +vec3 BRDF_specularSheen( SheenInfo params, float NdotL, float NdotV, float NdotH ) { + float sheenDistribution = D_Charlie( params.sheenColorRough.a, NdotH ); + float sheenVisibility = V_Sheen( NdotL, NdotV, params.sheenColorRough.a ); + return params.sheenColorRough.rgb * sheenDistribution * sheenVisibility; +} + +vec3 getPunctualRadianceSheen( SheenInfo params, float NdotL, float NdotV, float NdotH ) { + return NdotL * BRDF_specularSheen( params, NdotL, NdotV, NdotH ); +} + +float max3( vec3 v ) { + return max( max( v.x, v.y ), v.z ); +} + +#endif +struct MaterialInfo { + vec3 basecolor; + vec3 f0; // full reflectance color at normal incidence angle. Computed from ior for dielectric + vec3 f90; // full reflectance at grazing angle. Set to 1. in the spec. + vec3 diffusebase; // diffuse base coefficient + // r contains alphaRoughness, g contains perceptual roughness, b contains metallic coefficient + vec3 RoughnessMetalness; + // set to 1 by the spec, can bve modified by extension KHR_materials_specular + float specularWeight; +#ifdef SHEEN_LAYER + SheenInfo sheen; +#endif +#ifdef CLEARCOAT_LAYER + ClearcoatInfo clearcoat; +#endif + // add this when KHR_materials_specular extension will be supported + // float specularWeight +}; + +#ifdef SPECULAR_LAYER +void getSpecularInfo( inout MaterialInfo base, + Specular u_specular, + NormalInfo nrm, + vec3 texCoord ) { + vec4 specularTexture = vec4( 1.0 ); + vec2 ct = texCoord.xy; +# ifdef TEXTURE_SPECULAR_EXT +# ifdef TEXTURE_COORD_TRANSFORM_SPECULAR_EXT + ct = ( u_specular.specularTextureTransform * texCoord ).xy; +# endif + specularTexture.a = texture( u_specular.specularTexture, ct ).a; +# endif + +# ifdef TEXTURE_SPECULARCOLOR_EXT +# ifdef TEXTURE_COORD_TRANSFORM_SPECULARCOLOR_EXT + ct = ( u_specular.specularColorTextureTransform * texCoord ).xy; +# else + ct = texCoord.xy; +# endif + specularTexture.rgb = texture( u_specular.specularColorTexture, ct ).rgb; +# endif + + vec3 dielectricSpecularF0 = + min( base.f0 * u_specular.specularColorFactor.rgb * specularTexture.rgb, vec3( 1.0 ) ); + base.f0 = mix( dielectricSpecularF0, base.basecolor, base.RoughnessMetalness.b ); + base.specularWeight = u_specular.specularFactor * specularTexture.a; +} +#endif + +bool toDiscardBase( GLTFBaseMaterial material, vec4 color ) { + if ( material.alphaMode == 1 && color.a < material.alphaCutoff ) return true; + if ( material.alphaMode == 2 && color.a < 1 ) return true; + return false; +} + +float dielectricSpecular( float ior ) { // compute f0 + float rel_ior = ( ior - 1 ) / ( ior + 1 ); + return rel_ior * rel_ior; +} + +#define RESPECT_SPEC +// compute F90 coef according to material spec (GLTF says 1, Schlick says what is in the comment) +vec3 f90( vec3 specularColor ) { +#ifdef RESPECT_SPEC + return vec3( 1 ); +#else + float r = max( max( specularColor.r, specularColor.g ), specularColor.b ); + float r90 = clamp( r * 25.0, 0.0, 1.0 ); + return vec3( r90 ); +#endif +} + +vec3 getEmissiveColorBase( GLTFBaseMaterial material, vec3 textCoord ) { + vec3 e = material.emissiveFactor.rgb; +#ifdef TEXTURE_EMISSIVE +# ifdef TEXTURE_COORD_TRANSFORM_EMISSIVE + vec3 ct = material.emissiveTransform * vec3( textCoord.xy, 1 ); +# else + vec3 ct = vec3( textCoord.xy, 1 ); +# endif + e *= texture( material.emissive, ct.xy ).rgb; +#endif + return e; +} + +int extractBSDFParametersBase( GLTFBaseMaterial material, + vec3 tc, + NormalInfo N, + inout MaterialInfo params ) { + params.specularWeight = 1.0; +#ifdef SHEEN_LAYER + // prepare SHEEN material info + params.sheen = getSheenInfo( material.sheen, vec3( tc.xy, 1 ) ); +#endif +#ifdef CLEARCOAT_LAYER + params.clearcoat = getClearcoatInfo( material.clearcoat, N, params.f0, vec3( tc.xy, 1 ) ); +#endif +#ifdef SPECULAR_LAYER + getSpecularInfo( params, material.specular, N, vec3( tc.xy, 1 ) ); +#endif +#ifdef MATERIAL_TRANSMISSION + getTransmissionInfo( params ); +#endif +#ifdef MATERIAL_VOLUME + getVolumeInfo( params ); +#endif + return 1; +} +/// Implementation of separable BSDF interface +/// BSDF SEPARABLE INTERFACE + +struct BsdfInfo { + vec3 f_diffuse; + vec3 f_specular; +#ifdef CLEARCOAT_LAYER + vec3 f_clearcoat; +#endif +#ifdef SHEEN_LAYER + vec3 f_sheen; + float sheen_scaling; +#endif + // add other layers +}; + +// verify this ... why r^.5 ? +float GGXroughness( MaterialInfo params ) { + return pow( params.RoughnessMetalness.g, 0.5 ); +} + +BsdfInfo evaluateBSDFBase( GLTFBaseMaterial material, + MaterialInfo bsdf_params, + NormalInfo N, + vec3 wi, // L + vec3 wo, // N + vec3 light_intensity ) { + BsdfInfo result; + vec3 ns = N.n; + float cosTo = dot( wo, ns ); + /* If material is double side, disable culling in the renderer + if ( material.doubleSided == 1 ) + { + if ( cosTo < 0. ) + { + // back face fragment + ns *= -1; + cosTo = -cosTo; + } + } + else + { cosTo = clamp( cosTo, 0.0, 1.0 ); } + */ + + cosTo = clamp( cosTo, 0.0, 1.0 ); + + // Just in case of null lighting direction, consider normal incidence + if ( length( wi ) < local_epsilon ) wi = ns; + + vec3 h = wi + wo; + if ( length( h ) < local_epsilon ) + h = ns; // TODO --> this could be buggy + else + h = normalize( h ); + + float cosTi = clampedDot( ns, wi ); + + if ( cosTo > 0 || cosTi > 0 ) + { + float VdotH = clampedDot( wo, h ); + float NdotH = clampedDot( ns, h ); + + result.f_diffuse = light_intensity * cosTi * + BRDF_lambertian( bsdf_params.diffusebase, + VdotH, + bsdf_params.f0, + bsdf_params.f90, + bsdf_params.specularWeight ); + result.f_specular = light_intensity * cosTi * + BRDF_specularGGX( bsdf_params.f0, + bsdf_params.f90, + bsdf_params.RoughnessMetalness.r, + bsdf_params.specularWeight, + VdotH, + cosTi, + cosTo, + NdotH ); +#ifdef CLEARCOAT_LAYER + result.f_clearcoat = light_intensity * getPunctualRadianceClearCoat( + bsdf_params.clearcoat, wo, wi, h, VdotH ); +#endif + +#ifdef SHEEN_LAYER + result.f_sheen = + light_intensity * getPunctualRadianceSheen( bsdf_params.sheen, cosTi, cosTo, NdotH ); + float scalingV = + albedoSheenScalingLUT( material.sheen, cosTo, bsdf_params.sheen.sheenColorRough.a ); + float scalingL = + albedoSheenScalingLUT( material.sheen, cosTi, bsdf_params.sheen.sheenColorRough.a ); + result.sheen_scaling = + min( 1.0 - max3( bsdf_params.sheen.sheenColorRough.rgb ) * scalingV, + 1.0 - max3( bsdf_params.sheen.sheenColorRough.rgb ) * scalingL ); + // DEBUG + // result.sheen_scaling = bsdf_params.sheen.sheenColorRough.a; +#endif + } + + return result; +} + +// will not work if there is more than one light in the scene. clearcoat must be applied once and +// not after each light pass +vec3 combineLayers( MaterialInfo mat, BsdfInfo bsdf, NormalInfo N, vec3 v ) { +#ifdef CLEARCOAT_LAYER + vec3 ccfr = + F_Schlick( mat.clearcoat.f0, mat.clearcoat.f90, clampedDot( mat.clearcoat.normal, v ) ); + bsdf.f_clearcoat *= mat.clearcoat.intrough.x; +#endif + // modify the following each time a layer extension is added + vec3 color = bsdf.f_specular + bsdf.f_diffuse; +#ifdef SHEEN_LAYER + color = color * bsdf.sheen_scaling + bsdf.f_sheen; +#endif +#ifdef CLEARCOAT_LAYER + color = color * ( 1.0 - mat.clearcoat.intrough.r * ccfr ) + bsdf.f_clearcoat; +#endif + return color; +} + +// 496 +int getSeparateBSDFComponentBase( GLTFBaseMaterial material, + MaterialInfo params, + NormalInfo N, + vec3 wo, + out BsdfInfo layers ) { + vec3 ns = N.n; + float cosTo = dot( wo, ns ); + // Will be set to 1 if base layers (diffuse + specular) are computed + int result = 0; + + /* If material is double side, disable culling in the renderer + if ( material.doubleSided == 1 ) + { + if ( cosTo < 0. ) + { + // back face fragment + ns *= -1; + cosTo = -cosTo; + } + } + else + { cosTo = clamp( cosTo, 0.0, 1.0 ); } + */ + + cosTo = clamp( cosTo, 0.0, 1.0 ); + if ( cosTo > 0 ) + { + float r = GGXroughness( params ); + vec2 f_ab = texture( material.ggxLut, vec2( cosTo, r ) ).rg; + // see + // https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/master/source/Renderer/shaders/ibl.glsl + // see https://bruop.github.io/ibl/#single_scattering_results + vec3 Fr = max( vec3( 1. - r ), params.f0 ) - params.f0; + vec3 Ks = params.f0 + Fr * pow( 1. - cosTo, 5.0 ); + vec3 FssEss = params.specularWeight * (Ks * f_ab.x + f_ab.y); + layers.f_specular = FssEss; + + float Ems = ( 1.0 - ( f_ab.x + f_ab.y ) ); + vec3 Favg = params.specularWeight * ( params.f0 + ( 1. - params.f0 ) / 21. ); + vec3 FmsEms = Ems * FssEss * Favg / ( 1.0 - Favg * Ems ); + layers.f_diffuse = params.diffusebase * ( 1.0 - FssEss + FmsEms ) + FmsEms; + +#ifdef SHEEN_LAYER + r = params.sheen.sheenColorRough.a; + layers.f_sheen = params.sheen.sheenColorRough.rgb * texture(material.sheen.charlieLUT, vec2( cosTo, r ) ).b; + layers.sheen_scaling = 1.0 - max3(params.sheen.sheenColorRough.rgb) * albedoSheenScalingLUT(material.sheen, cosTo, r); +#endif + +#ifdef CLEARCOAT_LAYER + cosTo = clamp( dot(wo, params.clearcoat.normal), 0.0, 1.0 ); + r = params.clearcoat.intrough.y; + f_ab = texture( material.ggxLut, vec2( cosTo, r ) ).rg; + Fr = max( vec3( 1. - r ), params.clearcoat.f0 ) - params.clearcoat.f0; + Ks = params.clearcoat.f0 + Fr * pow( 1. - cosTo, 5.0 ); + layers.f_clearcoat = Ks * f_ab.x + f_ab.y; +#endif + result = 1; + } + else + { + layers.f_diffuse = vec3( 0 ); + layers.f_specular = vec3( 0 ); + result = 0; + } + + return result; +} + +#endif // GLTF_BASEMATERIAL_GLSL diff --git a/Shaders/Materials/GLTF/Materials/baseGLTFMaterial.vert.glsl b/Shaders/Materials/GLTF/Materials/baseGLTFMaterial.vert.glsl new file mode 100644 index 00000000000..ff3a11c4b0a --- /dev/null +++ b/Shaders/Materials/GLTF/Materials/baseGLTFMaterial.vert.glsl @@ -0,0 +1,40 @@ +// This is the basic vertexShader any PBR material can use +#include "TransformStructs.glsl" + +// This is for a preview of the shader composition, but in time we must use more specific Light +// Shader +#include "DefaultLight.glsl" + +layout( location = 0 ) in vec3 in_position; +layout( location = 1 ) in vec3 in_normal; +layout( location = 2 ) in vec3 in_tangent; +layout( location = 3 ) in vec3 in_bitangent; +layout( location = 4 ) in vec3 in_texcoord; +layout( location = 5 ) in vec4 in_color; + +layout( location = 0 ) out vec3 out_position; +layout( location = 1 ) out vec3 out_normal; +layout( location = 2 ) out vec3 out_texcoord; +layout( location = 3 ) out vec3 out_vertexcolor; +layout( location = 4 ) out vec3 out_tangent; +layout( location = 5 ) out vec3 out_viewVector; +layout( location = 6 ) out vec3 out_lightVector; + +uniform Transform transform; + +void main() { + mat4 mvp = transform.proj * transform.view * transform.model; + gl_Position = mvp * vec4( in_position, 1.0 ); + vec4 pos = transform.model * vec4( in_position, 1.0 ); + pos /= pos.w; + out_position = pos.xyz; + out_texcoord = in_texcoord; + out_normal = normalize( mat3( transform.worldNormal ) * in_normal ); + out_tangent = normalize( mat3( transform.model ) * in_tangent ); + vec3 eye = -transform.view[3].xyz * mat3( transform.view ); + out_viewVector = normalize( eye - pos.xyz ); + out_lightVector = getLightDirection( light, pos.xyz ); + out_vertexcolor = in_color.rgb; +} + +// pos, view, light, normal and tangent are in world space diff --git a/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_LitOIT.frag.glsl b/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_LitOIT.frag.glsl new file mode 100644 index 00000000000..f7add31c9d7 --- /dev/null +++ b/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_LitOIT.frag.glsl @@ -0,0 +1,69 @@ +layout( location = 0 ) out vec4 f_Accumulation; +layout( location = 1 ) out vec4 f_Revealage; + +#include "DefaultLight.glsl" + +#include "VertexAttribInterface.frag.glsl" +layout( location = 5 ) in vec3 in_viewVector; +layout( location = 6 ) in vec3 in_lightVector; + +// ----------------------------------------------------------- + +// implementation of weight functions of the paper +// Weighted Blended Order-Independent Transparency +// Morgan McGuire, Louis Bavoil - NVIDIA +// Journal of Computer Graphics Techniques (JCGT), vol. 2, no. 2, 122-141, 2013 +// http://jcgt.org/published/0002/02/09/ + +// remark : manage only non colored transmission. see the paper for : +// ... non-refractive colored transmission can be implemented as a simple extension by processing a +// separate coverage value per color channel + +// Note, z range from 0 at the camera to +infinity far away ... + +float weight( float z, float alpha ) { + + // pow(alpha, colorResistance) : increase colorResistance if foreground transparent are + // affecting background transparent color clamp(adjust / f(z), min, max) : + // adjust : Range adjustment to avoid saturating at the clamp bounds + // clamp bounds : to be tuned to avoid over or underflow of the reveleage texture. + // f(z) = 1e-5 + pow(z/depthRange, orederingStrength) + // defRange : Depth range over which significant ordering discrimination is required. Here, + // 10 camera space units. + // Decrease if high-opacity surfaces seem “too transparent”, + // increase if distant transparents are blending together too much. + // orderingStrength : Ordering strength. Increase if background is showing through + // foreground too much. + // 1e-5 + ... : avoid dividing by zero ! + + return pow( alpha, 0.5 ) * clamp( 10 / ( 1e-5 + pow( z / 10, 6 ) ), 1e-2, 3 * 1e3 ); +} + +void main() { + vec3 tc = getPerVertexTexCoord(); + // only render non opaque fragments and not fully transparent fragments + vec4 bc = getBaseColor( material, tc ); + // compute the transparency factor + float a = bc.a; + if ( !toDiscard( material, bc ) || a < 0.001 ) discard; + NormalInfo nrm_info = getNormalInfo( material.baseMaterial, tc ); + + MaterialInfo bsdf_params; + extractBSDFParameters( material, tc, nrm_info, bsdf_params ); + vec3 wo = normalize( in_viewVector ); // outgoing direction + + // the following could be done for each ligh source (in a loop) ... + + vec3 wi = normalize( in_lightVector ); // incident direction + BsdfInfo layers = evaluateBSDF( material, bsdf_params, + nrm_info, + wi, + wo, + lightContributionFrom( light, getWorldSpacePosition().xyz ) ); + + vec3 color = combineLayers( bsdf_params, layers, nrm_info, wo ); + + float w = weight( gl_FragCoord.z, a ); + f_Accumulation = vec4( color * a, a ) * w; + f_Revealage = vec4( a ); +} diff --git a/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_LitOpaque.frag.glsl b/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_LitOpaque.frag.glsl new file mode 100644 index 00000000000..ba8180b0bc9 --- /dev/null +++ b/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_LitOpaque.frag.glsl @@ -0,0 +1,43 @@ +// This is the basic fragmentShader any PBR material can use. +// A specific material fragment shader implements the material interface (computeMaterialInternal) +// and include this shader + +// This is for a preview of the shader composition, but in time we must use more specific Light +// Shader +#include "DefaultLight.glsl" + +out vec4 fragColor; + +#include "VertexAttribInterface.frag.glsl" +// ----------------- +layout( location = 5 ) in vec3 in_viewVector; +layout( location = 6 ) in vec3 in_lightVector; + +void main() { + vec3 tc = getPerVertexTexCoord(); + // discard non opaque fragment + vec4 bc = getBaseColor( material, tc ); + if ( toDiscard( material, bc ) ) discard; + + NormalInfo nrm_info = getNormalInfo( material.baseMaterial, tc ); + + MaterialInfo bsdf_params; + extractBSDFParameters( material, tc, nrm_info, bsdf_params ); + vec3 wo = normalize( in_viewVector ); // outgoing direction + + // the following could be done for each ligh source (in a loop) ... + + vec3 wi = normalize( in_lightVector ); // incident direction + BsdfInfo layers = evaluateBSDF( material, bsdf_params, + nrm_info, + wi, + wo, + lightContributionFrom( light, getWorldSpacePosition().xyz ) ); + + vec3 color = combineLayers( bsdf_params, layers, nrm_info, wo ); +#ifdef USE_IBL + color = modulateByAO( material.baseMaterial, color, getPerVertexTexCoord() ); +#endif + // color = color + getEmissiveColor(material.baseMaterial, getPerVertexTexCoord()); + fragColor = vec4( color, 1.0 ); +} diff --git a/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_Zprepass.frag.glsl b/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_Zprepass.frag.glsl new file mode 100644 index 00000000000..b48af264ea6 --- /dev/null +++ b/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_Zprepass.frag.glsl @@ -0,0 +1,32 @@ +layout(location = 0) out vec4 out_ambient; +layout(location = 1) out vec4 out_normal; +layout(location = 2) out vec4 out_diffuse; +layout(location = 3) out vec4 out_specular; + +#include "VertexAttribInterface.frag.glsl" +layout(location = 5) in vec3 in_viewVector; +layout(location = 6) in vec3 in_lightVector; + +//------------------- main --------------------- +void main() { + // discard non opaque fragment + vec4 bc = getBaseColor(material, in_texcoord); + + if (toDiscard(material, bc)) discard; + + NormalInfo nrm_info = getNormalInfo(material.baseMaterial, in_texcoord); + + MaterialInfo bsdf_params; + BsdfInfo layers; + + getSeparateBSDFComponent( material, + in_texcoord, + normalize(in_viewVector), + nrm_info, + bsdf_params, + layers ); + out_ambient = vec4(layers.f_diffuse * 0.01 + getEmissiveColor(material, in_texcoord), 1.0); + out_normal = vec4(nrm_info.n * 0.5 + 0.5, 1.0); + out_diffuse = vec4(layers.f_diffuse, 1.0); + out_specular = vec4(layers.f_specular, 1.0); +} diff --git a/Shaders/Materials/GLTF/Metadata/GlTFMaterial.json b/Shaders/Materials/GLTF/Metadata/GlTFMaterial.json new file mode 100644 index 00000000000..40d4ac40a6e --- /dev/null +++ b/Shaders/Materials/GLTF/Metadata/GlTFMaterial.json @@ -0,0 +1,220 @@ +{ + "material.baseMaterial.emissiveTexture": { + "name": "emissiveTexture", + "description": "The emissive texture. It controls the color and intensity of the light being emitted by the material. This texture contains RGB components encoded with the sRGB transfer function. If a fourth component (A) is present, it **MUST** be ignored. When undefined, the texture **MUST** be sampled as having `1.0` in RGB components.", + "type": "texture" + }, + "material.baseMaterial.emissiveFactor": { + "name": "emissiveFactor", + "description": "The factors for the emissive color of the material. This value defines linear multipliers for the sampled texels of the emissive texture.", + "type": "array", + "items": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "minItems": 3, + "maxItems": 3 + }, + "material.baseMaterial.alphaMode": { + "name": "alphaMode", + "description": "The material's alpha rendering mode enumeration specifying the interpretation of the alpha value of the base color.", + "type": "enum" + }, + "material.baseMaterial.alphaCutoff": { + "name": "alphaCutoff", + "description": "Specifies the cutoff threshold when in `MASK` alpha mode. If the alpha value is greater than or equal to this value then it is rendered as fully opaque, otherwise, it is rendered as fully transparent. A value greater than `1.0` will render the entire material as fully transparent. This value **MUST** be ignored for other alpha modes. When `alphaMode` is not defined, this value **MUST NOT** be defined.", + "type": "number", + "minimum": 0.0 + }, + "material.baseMaterial.doubleSided": { + "name": "doubleSided", + "description": "Specifies whether the material is double sided. When this value is false, back-face culling is enabled. When this value is true, back-face culling is disabled and double-sided lighting is enabled. The back-face **MUST** have its normals reversed before the lighting equation is evaluated.", + "type": "boolean", + "editable": true + }, + "material.baseMaterial.ior": { + "name": "ior", + "description": "The index of refraction (IOR) is a measured physical number usually in the range between 1 and 2 that determines how much the path of light is bent, or refracted, when entering a material. It also influences the ratio between reflected and transmitted light, calculated from the Fresnel equations.", + "type": "number", + "oneOf": [ + { + "minimum": 0.0, + "maximum": 0.0 + }, + { + "minimum": 1.0 + } + ] + }, + "material.baseColorFactor": { + "name": "baseColorFactor", + "description": "The factors for the base color of the material. This value defines linear multipliers for the sampled texels of the base color texture.", + "type": "array", + "items": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "minItems": 4, + "maxItems": 4 + }, + "material.baseColorTexture": { + "name": "baseColorTexture", + "description": "The base color texture. The first three components (RGB) **MUST** be encoded with the sRGB transfer function. They specify the base color of the material. If the fourth component (A) is present, it represents the linear alpha coverage of the material. Otherwise, the alpha coverage is equal to `1.0`. The `material.alphaMode` property specifies how alpha is interpreted. The stored texels **MUST NOT** be premultiplied. When undefined, the texture **MUST** be sampled as having `1.0` in all components.", + "type": "texture" + }, + "material.metallicFactor": { + "name": "metallicFactor", + "description": "The factor for the metalness of the material. This value defines a linear multiplier for the sampled metalness values of the metallic-roughness texture.", + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "material.roughnessFactor": { + "name": "roughnessFactor", + "description": "The factor for the roughness of the material. This value defines a linear multiplier for the sampled roughness values of the metallic-roughness texture.", + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "material.metallicRoughnessTexture": { + "name": "metallicRoughnessTexture", + "description": "The metallic-roughness texture. The metalness values are sampled from the B channel. The roughness values are sampled from the G channel. These values **MUST** be encoded with a linear transfer function. If other channels are present (R or A), they **MUST** be ignored for metallic-roughness calculations. When undefined, the texture **MUST** be sampled as having `1.0` in G and B components.", + "type": "texture" + }, + "material.diffuseFactor": { + "name": "diffuseFactor", + "description": "The RGBA components of the reflected diffuse color of the material. Metals have a diffuse value of `[0.0, 0.0, 0.0]`. The fourth component (A) is the alpha coverage of the material. The `alphaMode` property specifies how alpha is interpreted. The values are linear.", + "type": "array", + "items": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "minItems": 4, + "maxItems": 4 + }, + "material.diffuseTexture": { + "name": "diffuseTexture", + "description": "The diffuse texture. This texture contains RGB components of the reflected diffuse color of the material encoded with the sRGB transfer function. If the fourth component (A) is present, it represents the linear alpha coverage of the material. Otherwise, an alpha of 1.0 is assumed. The `alphaMode` property specifies how alpha is interpreted. The stored texels must not be premultiplied.", + "type": "texture" + }, + "material.specularFactor": { + "name": "specularFactor", + "description": "The specular RGB color of the material. This value is linear.", + "type": "array", + "items": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "minItems": 3, + "maxItems": 3 + }, + "material.glossinessFactor": { + "name": "glossinessFactor", + "description": "The glossiness or smoothness of the material. A value of 1.0 means the material has full glossiness or is perfectly smooth. A value of 0.0 means the material has no glossiness or is completely rough. This value is linear.", + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "material.specularGlossinessTexture": { + "name": "specularGlossinessTexture", + "description": "The specular-glossiness texture is an RGBA texture, containing the specular color (RGB) encoded with the sRGB transfer function and the linear glossiness value (A).", + "type": "texture" + }, + "material.baseMaterial.clearcoat.clearcoatFactor": { + "name": "clearcoatFactor", + "description": "The clearcoat layer intensity (aka opacity) of the material. A value of 0.0 means the material has no clearcoat layer enabled.", + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "material.baseMaterial.clearcoat.clearcoatTexture": { + "name": "clearcoatTexture", + "description": "The clearcoat layer intensity texture. These values are sampled from the R channel. The values are linear. Use value 1.0 if no texture is supplied.", + "type": "texture" + }, + "material.baseMaterial.clearcoat.clearcoatRoughnessFactor": { + "name": "clearcoatRoughnessFactor", + "description": "The clearcoat layer roughness of the material.", + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "material.baseMaterial.clearcoat.clearcoatRoughnessTexture": { + "name": "clearcoatRoughnessTexture", + "description": "The clearcoat layer roughness texture. These values are sampled from the G channel. The values are linear. Use value 1.0 if no texture is supplied.", + "type": "texture" + }, + "material.baseMaterial.sheen.sheenColorFactor": { + "name": "sheenColorFactor", + "description": "Color of the sheen layer (in linear space).", + "type": "array", + "items": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "minItems": 3, + "maxItems": 3 + }, + "material.baseMaterial.sheen.sheenColorTexture": { + "name": "sheenColorTexture", + "description": "The sheen color (RGB) texture. Stored in channel RGB, the sheen color is in sRGB transfer function.", + "type": "texture" + }, + "material.baseMaterial.sheen.sheenRoughnessFactor": { + "name": "sheenRoughnessFactor", + "description": "The sheen layer roughness of the material.", + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "material.baseMaterial.sheen.sheenRoughnessTexture": { + "name": "sheenRoughnessTexture", + "description": "The sheen roughness (Alpha) texture. Stored in alpha channel, the roughness value is in linear space.", + "type": "texture" + }, + "material.baseMaterial.specular.specularFactor": { + "name": "specularFactor", + "description": "This parameter scales the amount of specular reflection on non-metallic surfaces. It has no effect on metals.", + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "material.baseMaterial.specular.specularTexture": { + "name": "specularTexture", + "description": "A texture that defines the specular factor in the alpha channel. This will be multiplied by specularFactor.", + "type": "texture" + }, + "material.baseMaterial.specular.specularColorFactor": { + "name": "specularColorFactor", + "description": "This is an additional RGB color parameter that tints the specular reflection of non-metallic surfaces. At grazing angles, the reflection still blends to white, and the parameter has not effect on metals. The value is linear.", + "type": "array", + "items": { + "type": "number", + "minimum": 0.0 + }, + "minItems": 3, + "maxItems": 3 + }, + "material.baseMaterial.specular.specularColorTexture": { + "name": "specularColorTexture", + "description": "A texture that defines the specular color in the RGB channels (encoded in sRGB). This will be multiplied by specularColorFactor.", + "type": "texture" + }, + "material.baseMaterial.transmission.transmissionFactor": { + "name": "transmissionFactor", + "description": "The base percentage of non-specularly reflected light that is transmitted through the surface. i.e. of the light that penetrates a surface (isn't specularly reflected), this is the percentage that is transmitted and not diffusely re-emitted.", + "type": "number", + "minimum": 0.0, + "maximum": 1.0 + }, + "material.baseMaterial.transmission.transmissionTexture": { + "name": "transmissionTexture", + "description": "A texture that defines the transmission percentage of the surface, sampled from the R channel. These values are linear, and will be multiplied by transmissionFactor. This indicates the percentage of non-specularly reflected light that is transmitted through the surface. i.e. of the light that penetrates a surface (isn't specularly reflected), this is the percentage is transmitted and not diffusely re-emitted.", + "type": "texture" + } +} From 9d3cee7779d6550a58c40658551255cb9f1c9a93 Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Wed, 19 Jul 2023 19:14:21 +0200 Subject: [PATCH 04/27] [script] update IO filelist generator to manage gltf loader/writer extension --- scripts/generateFilelistForModule.sh | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/scripts/generateFilelistForModule.sh b/scripts/generateFilelistForModule.sh index 9448c419fac..b0b5ff7ffc7 100755 --- a/scripts/generateFilelistForModule.sh +++ b/scripts/generateFilelistForModule.sh @@ -45,7 +45,7 @@ function genListIo(){ if [ ! -z "$L" ] then echo "set(${LOWBASE}_${suffix}" >> "${OUTPUT}" - echo "${L}" | grep -v pch.hpp | grep -v deprecated | grep -v AssimpLoader | grep -v TinyPlyLoader | grep -v VolumesLoader | cut -f 4- -d/ | sort | xargs -n1 echo " " >> "${OUTPUT}" + echo "${L}" | grep -v pch.hpp | grep -v deprecated | grep -v AssimpLoader | grep -v Gltf | grep -v TinyPlyLoader | grep -v VolumesLoader | cut -f 4- -d/ | sort | xargs -n1 echo " " >> "${OUTPUT}" echo ")" >> "${OUTPUT}" echo "" >> "${OUTPUT}" fi @@ -121,5 +121,19 @@ if [ "$BASE" = "IO" ]; then genListIoAppendSubdir "cpp" "sources" "VolumesLoader" genListIoAppendSubdir "hpp" "headers" "VolumesLoader" echo "endif( RADIUM_IO_VOLUMES )" >> "${OUTPUT}" + echo "if( RADIUM_IO_GLTF )" >> "${OUTPUT}" + genListIoAppendSubdir "cpp" "sources" "Gltf/Loader" + genListIoAppendSubdir "cpp" "sources" "Gltf/internal/GLTFConverter" + genListIoAppendSubdir "c" "sources" "Gltf/internal/GLTFConverter" + genListIoAppendSubdir "hpp" "headers" "Gltf/Loader" + genListIoAppendSubdir "hpp" "private_headers" "Gltf/internal/GLTFConverter" + genListIoAppendSubdir "h" "private_headers" "Gltf/internal/GLTFConverter" + genListIoAppendSubdir "hpp" "private_headers" "Gltf/internal/Extensions" + genListIoAppendSubdir "h" "private_headers" "Gltf/internal/fx" + echo "if( RADIUM_IO_GLTF_WRITER )" >> "${OUTPUT}" + genListIoAppendSubdir "cpp" "sources" "Gltf/Writer" + genListIoAppendSubdir "hpp" "headers" "Gltf/Writer" + echo "endif( RADIUM_IO_GLTF_WRITER )" >> "${OUTPUT}" + echo "endif( RADIUM_IO_GLTF )" >> "${OUTPUT}" fi cmake-format -i "${OUTPUT}" From f1d3de3d5934e77ca72474e2bb79984fb6178c45 Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Wed, 19 Jul 2023 19:14:50 +0200 Subject: [PATCH 05/27] [IO] add gltf 2.0 custom file loader and writer --- src/IO/CMakeLists.txt | 18 +- src/IO/Config.cmake.in | 30 +- src/IO/Gltf/Loader/glTFFileLoader.cpp | 89 + src/IO/Gltf/Loader/glTFFileLoader.hpp | 53 + src/IO/Gltf/Writer/glTFFileWriter.cpp | 676 ++++++ src/IO/Gltf/Writer/glTFFileWriter.hpp | 49 + .../internal/Extensions/LightExtensions.hpp | 179 ++ .../Extensions/MaterialExtensions.hpp | 343 +++ .../internal/GLTFConverter/AccessorReader.cpp | 226 ++ .../internal/GLTFConverter/AccessorReader.hpp | 46 + .../Gltf/internal/GLTFConverter/Converter.cpp | 414 ++++ .../Gltf/internal/GLTFConverter/Converter.hpp | 45 + .../internal/GLTFConverter/HandleData.cpp | 273 +++ .../internal/GLTFConverter/HandleData.hpp | 81 + .../Gltf/internal/GLTFConverter/ImageData.hpp | 73 + .../GLTFConverter/MaterialConverter.cpp | 600 +++++ .../GLTFConverter/MaterialConverter.hpp | 77 + .../Gltf/internal/GLTFConverter/MeshData.cpp | 264 +++ .../Gltf/internal/GLTFConverter/MeshData.hpp | 307 +++ .../GLTFConverter/NormalCalculator.cpp | 50 + .../GLTFConverter/NormalCalculator.hpp | 36 + .../Gltf/internal/GLTFConverter/SceneNode.cpp | 29 + .../Gltf/internal/GLTFConverter/SceneNode.hpp | 26 + .../GLTFConverter/TangentCalculator.cpp | 176 ++ .../GLTFConverter/TangentCalculator.hpp | 137 ++ .../GLTFConverter/TransformationManager.cpp | 163 ++ .../GLTFConverter/TransformationManager.hpp | 46 + .../Gltf/internal/GLTFConverter/mikktspace.c | 1938 +++++++++++++++++ .../Gltf/internal/GLTFConverter/mikktspace.h | 160 ++ src/IO/Gltf/internal/fx/gltf.h | 1716 +++++++++++++++ src/IO/filelist.cmake | 51 + 31 files changed, 8368 insertions(+), 3 deletions(-) create mode 100644 src/IO/Gltf/Loader/glTFFileLoader.cpp create mode 100644 src/IO/Gltf/Loader/glTFFileLoader.hpp create mode 100644 src/IO/Gltf/Writer/glTFFileWriter.cpp create mode 100644 src/IO/Gltf/Writer/glTFFileWriter.hpp create mode 100644 src/IO/Gltf/internal/Extensions/LightExtensions.hpp create mode 100644 src/IO/Gltf/internal/Extensions/MaterialExtensions.hpp create mode 100644 src/IO/Gltf/internal/GLTFConverter/AccessorReader.cpp create mode 100644 src/IO/Gltf/internal/GLTFConverter/AccessorReader.hpp create mode 100644 src/IO/Gltf/internal/GLTFConverter/Converter.cpp create mode 100644 src/IO/Gltf/internal/GLTFConverter/Converter.hpp create mode 100644 src/IO/Gltf/internal/GLTFConverter/HandleData.cpp create mode 100644 src/IO/Gltf/internal/GLTFConverter/HandleData.hpp create mode 100644 src/IO/Gltf/internal/GLTFConverter/ImageData.hpp create mode 100644 src/IO/Gltf/internal/GLTFConverter/MaterialConverter.cpp create mode 100644 src/IO/Gltf/internal/GLTFConverter/MaterialConverter.hpp create mode 100644 src/IO/Gltf/internal/GLTFConverter/MeshData.cpp create mode 100644 src/IO/Gltf/internal/GLTFConverter/MeshData.hpp create mode 100644 src/IO/Gltf/internal/GLTFConverter/NormalCalculator.cpp create mode 100644 src/IO/Gltf/internal/GLTFConverter/NormalCalculator.hpp create mode 100644 src/IO/Gltf/internal/GLTFConverter/SceneNode.cpp create mode 100644 src/IO/Gltf/internal/GLTFConverter/SceneNode.hpp create mode 100644 src/IO/Gltf/internal/GLTFConverter/TangentCalculator.cpp create mode 100644 src/IO/Gltf/internal/GLTFConverter/TangentCalculator.hpp create mode 100644 src/IO/Gltf/internal/GLTFConverter/TransformationManager.cpp create mode 100644 src/IO/Gltf/internal/GLTFConverter/TransformationManager.hpp create mode 100644 src/IO/Gltf/internal/GLTFConverter/mikktspace.c create mode 100644 src/IO/Gltf/internal/GLTFConverter/mikktspace.h create mode 100644 src/IO/Gltf/internal/fx/gltf.h diff --git a/src/IO/CMakeLists.txt b/src/IO/CMakeLists.txt index 100f29c98c9..681b30062e6 100644 --- a/src/IO/CMakeLists.txt +++ b/src/IO/CMakeLists.txt @@ -7,10 +7,13 @@ option(RADIUM_IO_DEPRECATED "Provide deprecated loaders (to be removed without n option(RADIUM_IO_ASSIMP "Provide loaders based on Assimp library" ON) option(RADIUM_IO_TINYPLY "Provide loaders based on TinyPly library" ON) option(RADIUM_IO_VOLUMES "Provide loader for volume pvm file format" ON) - +option(RADIUM_IO_GLTF "Provide loader for gltf2.0 file format" ON) +cmake_dependent_option( + RADIUM_IO_GLTF_WRITER "Provide writer for gltf2.0 file format" ON RADIUM_IO_GLTF OFF +) include(filelist.cmake) -add_library(${ra_io_target} SHARED ${io_sources} ${io_headers}) +add_library(${ra_io_target} SHARED ${io_sources} ${io_headers} ${io_private_headers}) if(RADIUM_IO_ASSIMP) find_package(assimp 5.0 REQUIRED NO_DEFAULT_PATH) @@ -38,6 +41,17 @@ if(RADIUM_IO_ASSIMP) MAP_IMPORTED_CONFIG_RELWITHDEBINFO Release ) endif(RADIUM_IO_ASSIMP) + +if(RADIUM_IO_GLTF) + set_target_properties(${ra_io_target} PROPERTIES IO_HAS_GLTF ${RADIUM_IO_GLTF}) + if(RADIUM_IO_GLTF_WRITER) + target_link_libraries(${ra_io_target} PUBLIC Engine) + set_target_properties( + ${ra_io_target} PROPERTIES IO_HAS_GLTF_WRITER ${RADIUM_IO_GLTF_WRITER} + ) + endif(RADIUM_IO_GLTF_WRITER) +endif(RADIUM_IO_GLTF) + if(RADIUM_IO_TINYPLY) target_link_libraries(${ra_io_target} PUBLIC tinyply) endif(RADIUM_IO_TINYPLY) diff --git a/src/IO/Config.cmake.in b/src/IO/Config.cmake.in index 1c5358c2737..d07d212a630 100644 --- a/src/IO/Config.cmake.in +++ b/src/IO/Config.cmake.in @@ -15,7 +15,19 @@ if (IO_FOUND AND NOT TARGET IO) set(Configure_IO OFF) endif() endif() - + if(@IO_HAS_GLTF_WRITER@) + # verify dependencies + if(NOT Engine_FOUND) + if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../Engine/RadiumEngineConfig.cmake") + include(${CMAKE_CURRENT_LIST_DIR}/../Engine/RadiumEngineConfig.cmake) + set(Engine_FOUND TRUE) + else() + set(Radium_FOUND False) + set(Radium_NOT_FOUND_MESSAGE "Radium::IO: dependency Engine needed by GLTF_WRITER not found") + set(Configure_IO OFF) + endif() + endif() + endif() endif() if(Configure_IO) @@ -32,8 +44,24 @@ if(Configure_IO) BRIEF_DOCS "Radium::IO has volume loader support." FULL_DOCS "Identify if Radium::IO was compiled with volume loader support." ) + define_property( + TARGET PROPERTY IO_HAS_GLTF BRIEF_DOCS "Radium::IO has gltf loading support." + FULL_DOCS "Identify if Radium::IO was compiled with gltf loading support." + ) + define_property( + TARGET PROPERTY IO_HAS_GLTF_WRITER BRIEF_DOCS "Radium::IO has gltf writer support." + FULL_DOCS "Identify if Radium::IO was compiled with gltf writer support (add dependency of IO on Engine)." + ) include("${CMAKE_CURRENT_LIST_DIR}/IOTargets.cmake" ) + #Detect if library has been compiled with volumeIO support + if(@RADIUM_IO_GLTF@) + set_target_properties(Radium::IO PROPERTIES IO_HAS_GLTF TRUE) + if(@IO_HAS_GLTF_WRITER@) + set_target_properties(Radium::IO PROPERTIES IO_HAS_GLTF_WRITER TRUE) + endif() + endif() + #Detect if library has been compiled with volumeIO support if(@RADIUM_IO_VOLUMES@) set_target_properties(Radium::IO PROPERTIES IO_HAS_VOLUMES TRUE) diff --git a/src/IO/Gltf/Loader/glTFFileLoader.cpp b/src/IO/Gltf/Loader/glTFFileLoader.cpp new file mode 100644 index 00000000000..511cb7c0b36 --- /dev/null +++ b/src/IO/Gltf/Loader/glTFFileLoader.cpp @@ -0,0 +1,89 @@ +#include +#include +#include + +#include + +namespace Ra { +namespace IO { +namespace GLTF { +using namespace Ra::Core::Asset; +using namespace Ra::Core::Utils; + +glTFFileLoader::glTFFileLoader() = default; + +glTFFileLoader::~glTFFileLoader() = default; + +std::vector glTFFileLoader::getFileExtensions() const { + return { "*.gltf", "*.glb" }; +} + +bool glTFFileLoader::handleFileExtension( const std::string& extension ) const { + return ( extension == "gltf" ) || ( extension == "glb" ); +} + +FileData* glTFFileLoader::loadFile( const std::string& filename ) { + auto fileData = new FileData( filename ); + fileData->setVerbose( true ); + + if ( !fileData->isInitialized() ) { + delete fileData; + return nullptr; + } + + std::clock_t startTime; + startTime = std::clock(); + + fileData->m_geometryData.clear(); + fileData->m_animationData.clear(); + + // Load data + fx::gltf::Document gltfFile; + // Load at most 100 buffers in total, each as large as 80mb... + // additionally, place a quota on the file size as well + fx::gltf::ReadQuotas readQuotas {}; + readQuotas.MaxBufferCount = 100; // default: 8 + readQuotas.MaxBufferByteLength = 1500 * 1024 * 1024; // default: 32mb + readQuotas.MaxFileSize = 1500 * 1024 * 1024; // default: 32mb (applies to binary .glb only) + + try { + if ( filename.substr( filename.size() - 3 ) == "glb" ) { + gltfFile = fx::gltf::LoadFromBinary( filename, readQuotas ); + } + else { gltfFile = fx::gltf::LoadFromText( filename, readQuotas ); } + } + catch ( std::exception& e ) { + LOG( logERROR ) << "Catched std::exception exception : " << e.what(); + delete fileData; + return nullptr; + } + + // get the basedir of the document + std::string baseDir = filename.substr( 0, filename.rfind( '/' ) + 1 ); + if ( baseDir.empty() ) { baseDir = "./"; } + + // convert gltf scenegraph to Filedata ... + Converter convertGlTF( fileData, baseDir ); + + if ( !convertGlTF( gltfFile ) ) { + LOG( logERROR ) << "Unable to convert gltf scene " << filename << ". Aborting"; + delete fileData; + return nullptr; + } + + fileData->m_loadingTime = ( std::clock() - startTime ) / Scalar( CLOCKS_PER_SEC ); + + if ( fileData->isVerbose() ) { fileData->displayInfo(); } + + fileData->m_processed = true; + + return fileData; +} + +std::string glTFFileLoader::name() const { + return { "glTF 2.0" }; +} + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/Loader/glTFFileLoader.hpp b/src/IO/Gltf/Loader/glTFFileLoader.hpp new file mode 100644 index 00000000000..a1821f843e5 --- /dev/null +++ b/src/IO/Gltf/Loader/glTFFileLoader.hpp @@ -0,0 +1,53 @@ +#pragma once +#include + +#include + +namespace Ra::Core::Asset { +class FileData; +} // namespace Ra::Core::Asset + +namespace Ra { +namespace IO { +namespace GLTF { + +/** + * FileLoader for GLTF2.0 file format + */ +class RA_IO_API glTFFileLoader : public Ra::Core::Asset::FileLoaderInterface +{ + public: + glTFFileLoader(); + + ~glTFFileLoader() override; + + /** Radium Loader interface + * + * @return {"gltf", "glb"} + */ + [[nodiscard]] std::vector getFileExtensions() const override; + + /** + * check if an extension is managed by the loader + * @param extension + * @return true if extension is gltf or glb + */ + [[nodiscard]] bool handleFileExtension( const std::string& extension ) const override; + + /** Try to load file, returns nullptr in case of failure + * + * @param filename the file to load + * @return the File data representing the gltf scene, nullptr if loading failed + */ + Ra::Core::Asset::FileData* loadFile( const std::string& filename ) override; + + /** Unique name of the loader + * + * @return "glTF 2.0" + */ + [[nodiscard]] std::string name() const override; +}; + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/Writer/glTFFileWriter.cpp b/src/IO/Gltf/Writer/glTFFileWriter.cpp new file mode 100644 index 00000000000..a17dfaa1df8 --- /dev/null +++ b/src/IO/Gltf/Writer/glTFFileWriter.cpp @@ -0,0 +1,676 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +/// @todo allow to export standard Radium Materials +#ifdef EXPORT_BLINNPHONG +// To export Radium materials : +# include +#endif + +using namespace fx; + +namespace Ra { +namespace IO { +namespace GLTF { +using namespace Ra::Core::Utils; +using namespace Ra::Engine; + +/// The used and required GLTF extension saved by the writer +static std::set gltf_usedExtensions; +static std::set gltf_requiredExtensions; +/// The texture uri prefix +static std::string g_texturePrefix { "textures/" }; +/** + * FileWriter for GLTF2.0 file format + * TODO : a huge refactoring is expected so that a to_json method is defined on each element to + * export in GLTF. + * + */ +glTFFileWriter::glTFFileWriter( std::string filename, + std::string texturePrefix, + bool writeImages ) : + m_fileName { std::move( filename ) }, + m_texturePrefix { std::move( texturePrefix ) }, + m_writeImages { writeImages } { + LOG( logINFO ) << "GLTF2 Writer : saving to file " << m_fileName << " with texture prefix " + << m_texturePrefix; + // Constructing root node name + auto p = m_fileName.find_last_of( "/\\" ); + if ( p != std::string::npos ) { m_rootName = m_fileName.substr( p + 1 ); } + else { m_rootName = m_fileName; } + p = m_rootName.find_last_of( '.' ); + if ( p != std::string::npos ) { m_rootName = m_rootName.substr( 0, p ); } + m_bufferName = m_rootName + ".bin"; +} + +glTFFileWriter::~glTFFileWriter() {} + +void fillTransform( gltf::Node& node, const Ra::Core::Transform& transform ) { + // Decompose the current transform into T*R*S + Ra::Core::Matrix3 rotationMat; + Ra::Core::Matrix3 scaleMat; + Ra::Core::Vector3 translate = transform.translation(); + transform.computeRotationScaling( &rotationMat, &scaleMat ); + Ra::Core::Quaternion quat( rotationMat ); + node.rotation = { quat.x(), quat.y(), quat.z(), quat.w() }; + node.translation = { translate.x(), translate.y(), translate.z() }; + node.scale = { scaleMat( 0, 0 ), scaleMat( 1, 1 ), scaleMat( 2, 2 ) }; +} + +int addIndices( gltf::Document& document, + int buffer, + const Ra::Core::Geometry::TriangleMesh& geometry ) { + gltf::Buffer& theBuffer = document.buffers[buffer]; + + // 1 - Build a bufferview for the indices + document.bufferViews.push_back( gltf::BufferView {} ); + gltf::BufferView& bufferView = document.bufferViews.back(); + bufferView.buffer = buffer; + bufferView.target = gltf::BufferView::TargetType::ElementArrayBuffer; + bufferView.byteOffset = uint32_t( theBuffer.data.size() ); + bufferView.byteLength = 3 * geometry.getIndices().size() * sizeof( unsigned int ); + + // 2 - append indices to the binary buffer + theBuffer.data.reserve( bufferView.byteOffset + bufferView.byteLength ); + theBuffer.data.resize( bufferView.byteOffset + bufferView.byteLength ); + std::memcpy( theBuffer.data.data() + bufferView.byteOffset, + reinterpret_cast( geometry.getIndices().data() ), + bufferView.byteLength ); + theBuffer.byteLength = uint32_t( theBuffer.data.size() ); + + // 3 - Build an accessor for the indices. + document.accessors.push_back( gltf::Accessor {} ); + gltf::Accessor& accessor = document.accessors.back(); + // compute bounds on the indice values + size_t ix_min = geometry.getIndices()[0]( 0 ); + size_t ix_max = ix_min; + auto minmax = [&ix_min, &ix_max]( const Ra::Core::Vector3ui& t ) { + if ( t( 0 ) < ix_min ) { ix_min = t( 0 ); } + else if ( t( 0 ) > ix_max ) { ix_max = t( 0 ); } + + if ( t( 1 ) < ix_min ) { ix_min = t( 1 ); } + else if ( t( 1 ) > ix_max ) { ix_max = t( 1 ); } + + if ( t( 2 ) < ix_min ) { ix_min = t( 2 ); } + else if ( t( 2 ) > ix_max ) { ix_max = t( 2 ); } + }; + std::for_each( geometry.getIndices().cbegin(), geometry.getIndices().cend(), minmax ); + accessor.min.push_back( ix_min ); + accessor.max.push_back( ix_max ); + accessor.bufferView = uint32_t( document.bufferViews.size() - 1 ); + accessor.byteOffset = 0; + accessor.count = 3 * geometry.getIndices().size(); + accessor.componentType = gltf::Accessor::ComponentType::UnsignedInt; + accessor.type = gltf::Accessor::Type::Scalar; + return document.accessors.size() - 1; +} + +// Functor applied on mesh attributes to build gltf equivalent. +class VertexAttribWriter +{ + public: + VertexAttribWriter( gltf::Document& document, int buffer, gltf::Primitive& primitive ) : + m_document( document ), m_buffer( buffer ), m_primitive( primitive ) {} + ~VertexAttribWriter() = default; + void operator()( const Ra::Core::Utils::AttribBase* att ) const; + + private: + gltf::Document& m_document; + int m_buffer; + gltf::Primitive& m_primitive; + + static std::map translator; +}; + +std::map VertexAttribWriter::translator { + { "in_position", "POSITION" }, + { "in_normal", "NORMAL" }, + { "in_tangent", "TANGENT" }, + { "in_texcoord", "TEXCOORD_0" }, + { "in_texcoord_1", "TEXCOORD_1" }, + { "in_color", "COLOR_0" }, + { "in_joints", "JOINTS_0" }, + { "in_weights", "WEIGHTS_0" } }; + +void VertexAttribWriter::operator()( const Ra::Core::Utils::AttribBase* att ) const { + auto name = translator.find( att->getName() ); + if ( name == translator.end() ) { + LOG( logERROR ) << "Not exporting invalid vertex attribute for GLTF : " << att->getName(); + return; + } + + gltf::Buffer& theBuffer = m_document.buffers[m_buffer]; + gltf::Accessor accessor; + accessor.byteOffset = 0; + accessor.componentType = gltf::Accessor::ComponentType::Float; + accessor.count = att->getSize(); + + // TODO : only 1 bufferview per type attrib type per RenderObject ... + gltf::BufferView bufferView; + bufferView.buffer = m_buffer; + bufferView.target = gltf::BufferView::TargetType::ArrayBuffer; + bufferView.byteOffset = theBuffer.data.size(); + + // Fill the node or exit if gltf node is not valid + // see https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#meshes + const uint8_t* dataToCopy { reinterpret_cast( att->dataPtr() ) }; + uint32_t dataByteLength { uint32_t( att->getBufferSize() ) }; + + // One of these vector will be used to convert data type and will be delete automatically at the + // end of the function + std::vector vec2ConvertedData; + std::vector vec3ConvertedData; + std::vector vec4ConvertedData; + + if ( ( name->second == "POSITION" ) || ( name->second == "NORMAL" ) ) { + // only Vec3 is allowed for POSITION and NORMAL + // Chances that Radium manage these attribs as Vec3 + if ( att->isVector4() ) { + // Need to convert data :( + vec3ConvertedData.reserve( att->getSize() ); + auto& s = att->cast().data(); + std::transform( s.begin(), + s.end(), + std::back_inserter( vec3ConvertedData ), + []( const Ra::Core::Vector4& c ) { + return Ra::Core::Vector3 { c.x(), c.y(), c.z() }; + } ); + dataToCopy = reinterpret_cast( vec3ConvertedData.data() ); + dataByteLength = vec3ConvertedData.size() * sizeof( Ra::Core::Vector3 ); + } + else if ( !att->isVector3() ) { + LOG( logERROR ) + << "POSITION and NORMAL vertex attributes must be Vec3 or Vec4 for gltf export of " + << att->getName(); + return; + } + accessor.type = gltf::Accessor::Type::Vec3; + // Compute min and max on the attribute value + const Ra::Core::Vector3* arrayOfAttribs = + reinterpret_cast( dataToCopy ); + Ra::Core::Vector3 minAtt = arrayOfAttribs[0]; + Ra::Core::Vector3 maxAtt = arrayOfAttribs[0]; + for ( auto i = 0; i < att->getSize(); ++i ) { + if ( arrayOfAttribs[i].x() < minAtt.x() ) { minAtt.x() = arrayOfAttribs[i].x(); } + else if ( arrayOfAttribs[i].x() > maxAtt.x() ) { maxAtt.x() = arrayOfAttribs[i].x(); } + if ( arrayOfAttribs[i].y() < minAtt.y() ) { minAtt.y() = arrayOfAttribs[i].y(); } + else if ( arrayOfAttribs[i].y() > maxAtt.y() ) { maxAtt.y() = arrayOfAttribs[i].y(); } + if ( arrayOfAttribs[i].z() < minAtt.z() ) { minAtt.z() = arrayOfAttribs[i].z(); } + else if ( arrayOfAttribs[i].z() > maxAtt.z() ) { maxAtt.z() = arrayOfAttribs[i].z(); } + } + accessor.min.push_back( minAtt.x() ); + accessor.min.push_back( minAtt.y() ); + accessor.min.push_back( minAtt.z() ); + accessor.max.push_back( maxAtt.x() ); + accessor.max.push_back( maxAtt.y() ); + accessor.max.push_back( maxAtt.z() ); + } + else if ( ( name->second == "COLOR_0" ) ) { + // only Vec3 or Vec4 is allowed for COLOR_0 + if ( att->isVector3() ) { accessor.type = gltf::Accessor::Type::Vec3; } + else if ( att->isVector4() ) { accessor.type = gltf::Accessor::Type::Vec4; } + else { + LOG( logERROR ) << "COLOR_0 vertex attributes must be Vec3 or Vec4 for gltf export of " + << att->getName(); + return; + } + } + else if ( ( name->second == "TANGENT" ) ) { + // only Vec4 is allowed for Tangent + if ( att->isVector3() ) { + // Radium manage tangents as Vec3 + // Need to convert tangent :( + vec4ConvertedData.reserve( att->getSize() ); + auto& s = att->cast().data(); + // TODO : verify handedness here: 1_ra could be -1_ra ... + std::transform( s.begin(), + s.end(), + std::back_inserter( vec4ConvertedData ), + []( const Ra::Core::Vector3& c ) { + return Ra::Core::Vector4 { c.x(), c.y(), c.z(), 1_ra }; + } ); + dataToCopy = reinterpret_cast( vec4ConvertedData.data() ); + dataByteLength = vec4ConvertedData.size() * sizeof( Ra::Core::Vector4 ); + } + else if ( !att->isVector4() ) { + LOG( logERROR ) << "TANGENT vertex attributes must be Vec3 or Vec4 for gltf export of " + << att->getName(); + return; + } + accessor.type = gltf::Accessor::Type::Vec4; + } + else if ( ( name->second.substr( 0, 8 ) == "TEXCOORD" ) ) { + // only Vec2 is allowed for TexCoord + if ( att->isVector4() ) { + // Radium manage TexCoord as Vec4 + // Need to convert TexCoord :( + vec2ConvertedData.reserve( att->getSize() ); + auto& s = att->cast().data(); + // Warning : here, 1-v is done according to GLTF specification for image reference (see + // the way gltf file is loaded) + std::transform( s.begin(), + s.end(), + std::back_inserter( vec2ConvertedData ), + []( const Ra::Core::Vector4& c ) { + return Ra::Core::Vector2 { c.x(), 1 - c.y() }; + } ); + dataToCopy = reinterpret_cast( vec2ConvertedData.data() ); + dataByteLength = vec2ConvertedData.size() * sizeof( Ra::Core::Vector2 ); + } + else if ( att->isVector3() ) { + // Need to convert TexCoord :( + vec2ConvertedData.reserve( att->getSize() ); + auto& s = att->cast().data(); + // TODO : verify handedness here: 1_ra could be -1_ra ... + std::transform( s.begin(), + s.end(), + std::back_inserter( vec2ConvertedData ), + []( const Ra::Core::Vector3& c ) { + return Ra::Core::Vector2 { c.x(), 1 - c.y() }; + } ); + dataToCopy = reinterpret_cast( vec2ConvertedData.data() ); + dataByteLength = vec2ConvertedData.size() * sizeof( Ra::Core::Vector2 ); + } + else if ( !att->isVector2() ) { + LOG( logERROR ) << "TEXCOORD vertex attributes must be Vec2 (or vec3/vec4 converted) " + "for gltf export of " + << att->getName(); + return; + } + accessor.type = gltf::Accessor::Type::Vec2; + // Compute min and max on the attribute value + const Ra::Core::Vector2* arrayOfAttribs = + reinterpret_cast( dataToCopy ); + Ra::Core::Vector2 minAtt = arrayOfAttribs[0]; + Ra::Core::Vector2 maxAtt = arrayOfAttribs[0]; + for ( auto i = 0; i < att->getSize(); ++i ) { + if ( arrayOfAttribs[i].x() < minAtt.x() ) { minAtt.x() = arrayOfAttribs[i].x(); } + else if ( arrayOfAttribs[i].x() > maxAtt.x() ) { maxAtt.x() = arrayOfAttribs[i].x(); } + if ( arrayOfAttribs[i].y() < minAtt.y() ) { minAtt.y() = arrayOfAttribs[i].y(); } + else if ( arrayOfAttribs[i].y() > maxAtt.y() ) { maxAtt.y() = arrayOfAttribs[i].y(); } + } + accessor.min.push_back( minAtt.x() ); + accessor.min.push_back( minAtt.y() ); + accessor.max.push_back( maxAtt.x() ); + accessor.max.push_back( maxAtt.y() ); + } + else { + /// TODO : implement theGLTF export of vertex attribs JOINTS_0 and WEIGHTS_0 + LOG( logWARNING ) << "Attribute " << name->second << " (from " << att->getName() + << ") is not yet expoerted."; + return; + } + + bufferView.byteLength = dataByteLength; + theBuffer.data.resize( bufferView.byteOffset + bufferView.byteLength ); + std::memcpy( theBuffer.data.data() + bufferView.byteOffset, dataToCopy, dataByteLength ); + theBuffer.byteLength = theBuffer.data.size(); + + // Add the bufferview + m_document.bufferViews.push_back( bufferView ); + // update and add the accessor + accessor.bufferView = m_document.bufferViews.size() - 1; + m_document.accessors.push_back( accessor ); + + // 4 - Update the primitive + m_primitive.attributes[name->second] = m_document.accessors.size() - 1; +} + +int addSampler( gltf::Document& document, gltf::Sampler sampler ) { + auto s = std::find_if( + document.samplers.cbegin(), document.samplers.cend(), [&sampler]( const gltf::Sampler& s ) { + return ( ( s.magFilter == sampler.magFilter ) && ( s.minFilter == sampler.minFilter ) && + ( s.wrapS == sampler.wrapS ) && ( s.wrapT == sampler.wrapT ) ); + } ); + if ( s == document.samplers.cend() ) { + document.samplers.push_back( sampler ); + return document.samplers.size() - 1; + } + return std::distance( document.samplers.cbegin(), s ); +} + +int addImage( gltf::Document& document, gltf::Image img ) { + auto s = std::find_if( + document.images.cbegin(), document.images.cend(), [&img]( const gltf::Image& i ) { + return ( ( !img.uri.empty() ) && ( i.uri == img.uri ) ); + } ); + if ( s == document.images.cend() ) { + document.images.push_back( img ); + return document.images.size() - 1; + } + return std::distance( document.images.cbegin(), s ); +} + +int addTexture( gltf::Document& document, + int /* buffer // Use this to save embeded textures */, + const Ra::Engine::Data::TextureParameters& params ) { + gltf::Texture texture; + + gltf::Image image; + image.uri = g_texturePrefix + params.name.substr( params.name.find_last_of( "/\\" ) + 1 ); + texture.source = addImage( document, std::move( image ) ); + + gltf::Sampler sampler; + sampler.magFilter = gltf::Sampler::MagFilter( (unsigned int)( params.magFilter ) ); + sampler.minFilter = gltf::Sampler::MinFilter( (unsigned int)( params.minFilter ) ); + sampler.wrapS = gltf::Sampler::WrappingMode( (unsigned int)( params.wrapS ) ); + sampler.wrapT = gltf::Sampler::WrappingMode( (unsigned int)( params.wrapT ) ); + texture.sampler = addSampler( document, std::move( sampler ) ); + auto s = std::find_if( + document.textures.cbegin(), document.textures.cend(), [&texture]( const gltf::Texture& t ) { + return ( ( texture.sampler == t.sampler ) && ( texture.source == t.source ) ); + } ); + if ( s == document.textures.cend() ) { + document.textures.push_back( texture ); + return document.textures.size() - 1; + } + return std::distance( document.textures.cbegin(), s ); +} + +void addMaterialTextureExtension( gltf::Material::Texture& texNode, + Engine::Data::GLTFMaterial* material, + const std::string& texName ) { + auto transform = material->getTextureTransform( texName ); + if ( transform ) { + + gltf_KHRTextureTransform tt; + tt.offset = transform->offset; + tt.scale = transform->scale; + tt.rotation = transform->rotation; + tt.texCoord = transform->texCoord; + if ( !tt.isDefault() ) { + texNode.extensionsAndExtras["extensions"]["KHR_texture_transform"] = tt; + gltf_usedExtensions.insert( { "KHR_texture_transform" } ); + } + } +} +void addBaseMaterial( gltf::Document& document, + int buffer, + Engine::Data::GLTFMaterial* material, + gltf::Material& node ) { + + node.name = material->getInstanceName(); + // generates Texture, sampler and image node for each base texture : "TEX_NORMAL", + // "TEX_OCCLUSION", "TEX_EMISSIVE" + { + auto t = material->getTextureParameter( { "TEX_NORMAL" } ); + if ( t ) { + node.normalTexture.index = addTexture( document, buffer, *t ); + node.normalTexture.scale = material->getNormalTextureScale(); + addMaterialTextureExtension( + static_cast( node.normalTexture ), + material, + { "TEX_NORMAL" } ); + } + } + { + auto t = material->getTextureParameter( { "TEX_OCCLUSION" } ); + if ( t ) { + node.occlusionTexture.index = addTexture( document, buffer, *t ); + node.occlusionTexture.strength = material->getOcclusionStrength(); + addMaterialTextureExtension( + static_cast( node.occlusionTexture ), + material, + { "TEX_OCCLUSION" } ); + } + } + { + auto t = material->getTextureParameter( { "TEX_EMISSIVE" } ); + if ( t ) { + node.emissiveTexture.index = addTexture( document, buffer, *t ); + addMaterialTextureExtension( + static_cast( node.emissiveTexture ), + material, + { "TEX_EMISSIVE" } ); + } + } + // Get the base parameters + // Color + const auto& c = material->getEmissiveFactor(); + node.emissiveFactor = { c( 0 ), c( 1 ), c( 2 ) }; + // int paramaters + node.alphaMode = gltf::Material::AlphaMode( material->getAlphaMode() ); + node.doubleSided = material->isDoubleSided(); + // floats parameters + node.alphaCutoff = material->getAlphaCutoff(); +} + +int addMetallicRoughnessMaterial( gltf::Document& document, + int buffer, + Engine::Data::MetallicRoughness* material ) { + document.materials.push_back( gltf::Material {} ); + gltf::Material& node = document.materials.back(); + int idxMaterial = document.materials.size() - 1; + addBaseMaterial( document, buffer, material, node ); + + // Base color + { + auto t = material->getTextureParameter( { "TEX_BASECOLOR" } ); + if ( t ) { + node.pbrMetallicRoughness.baseColorTexture.index = addTexture( document, buffer, *t ); + addMaterialTextureExtension( static_cast( + node.pbrMetallicRoughness.baseColorTexture ), + material, + { "TEX_BASECOLOR" } ); + } + const auto& c = material->getBaseColorFactor(); + node.pbrMetallicRoughness.baseColorFactor = { c( 0 ), c( 1 ), c( 2 ), c( 3 ) }; + } + // MetallicRoughness color + { + auto t = material->getTextureParameter( { "TEX_METALLICROUGHNESS" } ); + if ( t ) { + node.pbrMetallicRoughness.metallicRoughnessTexture.index = + addTexture( document, buffer, *t ); + addMaterialTextureExtension( static_cast( + node.pbrMetallicRoughness.metallicRoughnessTexture ), + material, + { "TEX_METALLICROUGHNESS" } ); + } + node.pbrMetallicRoughness.metallicFactor = material->getMetallicFactor(); + node.pbrMetallicRoughness.roughnessFactor = material->getRoughnessFactor(); + } + return idxMaterial; +} + +int addSpecularGlossinessMaterial( gltf::Document& document, + int buffer, + Engine::Data::SpecularGlossiness* material ) { + gltf_usedExtensions.insert( { "KHR_materials_pbrSpecularGlossiness" } ); + gltf_PBRSpecularGlossiness outMaterial; + + document.materials.push_back( gltf::Material {} ); + gltf::Material& node = document.materials.back(); + int idxMaterial = document.materials.size() - 1; + addBaseMaterial( document, buffer, material, node ); + + // Diffuse color + { + auto t = material->getTextureParameter( { "TEX_DIFFUSE" } ); + if ( t ) { + outMaterial.diffuseTexture.index = addTexture( document, buffer, *t ); + addMaterialTextureExtension( + static_cast( outMaterial.diffuseTexture ), + material, + { "TEX_DIFFUSE" } ); + } + const auto& c = material->getDiffuseFactor(); + outMaterial.diffuseFactor = { c( 0 ), c( 1 ), c( 2 ), c( 3 ) }; + } + // SpecularGlossiness color + { + auto t = material->getTextureParameter( { "TEX_SPECULARGLOSSINESS" } ); + if ( t ) { + outMaterial.specularGlossinessTexture.index = addTexture( document, buffer, *t ); + addMaterialTextureExtension( + static_cast( outMaterial.specularGlossinessTexture ), + material, + { "TEX_SPECULARGLOSSINESS" } ); + } + outMaterial.glossinessFactor = material->getGlossinessFactor(); + const auto& c = material->getSpecularFactor(); + outMaterial.specularFactor = { c( 0 ), c( 1 ), c( 2 ) }; + } + node.extensionsAndExtras["extensions"]["KHR_materials_pbrSpecularGlossiness"] = outMaterial; + return idxMaterial; +} + +#ifdef EXPORT_BLINNPHONG +/* TODO + * convert blinn-phong to pbrSpecularGlossiness such as ... + * + * + */ +int transformBlinnPhongMaterial( gltf::Document& document, + int buffer, + Ra::Engine::Data::BlinnPhongMaterial* material ) { + return -1; +} +#endif + +int addMaterial( gltf::Document& document, int buffer, Ra::Engine::Data::Material* material ) { + int materialIndex { -1 }; + // Supported Materials : MetallicRoughness, SpecularGlossiness, + if ( material->getMaterialName() == "MetallicRoughness" ) { + materialIndex = addMetallicRoughnessMaterial( + document, buffer, dynamic_cast( material ) ); + } + else if ( material->getMaterialName() == "SpecularGlossiness" ) { + materialIndex = addSpecularGlossinessMaterial( + document, buffer, dynamic_cast( material ) ); + } +#ifdef EXPORT_BLINNPHONG + else if ( material->getMaterialName() == "BlinnPhong" ) { + materialIndex = transformBlinnPhongMaterial( + document, buffer, dynamic_cast( material ) ); + } +#endif + else { + LOG( logWARNING ) << "GLTF export : unsupported material " << material->getMaterialName(); + } + return materialIndex; +} + +int addMesh( gltf::Document& document, int buffer, Ra::Engine::Rendering::RenderObject* ro ) { + auto displayMesh = dynamic_cast( ro->getMesh().get() ); + if ( !displayMesh ) { return -1; } + const auto& geometry = displayMesh->getCoreGeometry(); + // geometry is a TriangleMesh (IndexedGeometry) that is an + // AttribArrayGeometry + document.meshes.push_back( gltf::Mesh {} ); + gltf::Mesh& mesh = document.meshes.back(); + mesh.name = displayMesh->getName(); + mesh.primitives.push_back( gltf::Primitive {} ); + gltf::Primitive& primitive = mesh.primitives.back(); + primitive.mode = gltf::Primitive::Mode::Triangles; + // 1 - Store the indices accessor index + primitive.indices = addIndices( document, buffer, geometry ); + // 2 - manage vertex attributes + const auto& vertexAttribs = geometry.vertexAttribs(); + VertexAttribWriter addAttrib { document, buffer, primitive }; + vertexAttribs.for_each_attrib( addAttrib ); + // 3 - manage material + auto material = ro->getMaterial().get(); + if ( material ) { primitive.material = addMaterial( document, buffer, material ); } + return document.meshes.size() - 1; +} + +void addNode( gltf::Document& document, int buffer, Ra::Engine::Rendering::RenderObject* ro ) { + // Create the node + document.nodes.push_back( gltf::Node {} ); + gltf::Node& node = document.nodes.back(); + // Fill the node + node.name = "node " + std::to_string( document.nodes.size() - 1 ); // ro->getName(); + fillTransform( node, ro->getLocalTransform() ); + node.mesh = addMesh( document, buffer, ro ); +} + +void glTFFileWriter::write( std::vector toExport ) { + if ( toExport.empty() ) { + LOG( logWARNING ) << "No entities selected : abort file save."; + return; + } + g_texturePrefix = m_texturePrefix; + auto roManager = RadiumEngine::getInstance()->getRenderObjectManager(); + gltf::Document radiumScene; + radiumScene.asset.generator = "Radium glTF Plugin"; + // Create the buffer and add it to the json + int currentBuffer = 0; + { + gltf::Buffer sceneBuffer; + sceneBuffer.uri = m_bufferName; + sceneBuffer.name = m_rootName; + sceneBuffer.byteLength = 0; + radiumScene.buffers.push_back( sceneBuffer ); + } + // Export scene + for ( const auto e : toExport ) { + // An entity define a scene with its root node + radiumScene.scenes.push_back( gltf::Scene {} ); + gltf::Scene& sceneRoot = radiumScene.scenes.back(); + sceneRoot.name = e->getName(); + sceneRoot.nodes.push_back( radiumScene.nodes.size() ); + + // create the root node associated to the entity + radiumScene.nodes.push_back( gltf::Node {} ); + int parentNode = radiumScene.nodes.size() - 1; + radiumScene.nodes[parentNode].name = e->getName() + " root node."; + // initialize the node : transformation + fillTransform( radiumScene.nodes[parentNode], e->getTransform() ); + for ( const auto& c : e->getComponents() ) { + // Nothing to do with the component, just loop over its renderObjects + for ( const auto& roIdx : c->m_renderObjects ) { + const auto& ro = roManager->getRenderObject( roIdx ); + // verify the type of the RO : do not save debug nor ui ro + if ( ro->getType() == Ra::Engine::Rendering::RenderObjectType::Geometry && + ro->isVisible() ) { + // Update the state of the materials + auto material = ro->getMaterial(); + material->updateFromParameters(); + // Add a new node and link to parent + radiumScene.nodes[parentNode].children.push_back( radiumScene.nodes.size() ); + addNode( radiumScene, currentBuffer, ro.get() ); + } + else { + LOG( logINFO ) << "\t\tRenderObject " << ro->getName() + << " is not a geometry RO. Not saved"; + } + } + } + } + // Finalize the document : set the scene attribute + radiumScene.scene = 0; + // Add extensions used and required : + for ( const auto& e : gltf_usedExtensions ) { + radiumScene.extensionsUsed.push_back( e ); + } + for ( const auto& e : gltf_requiredExtensions ) { + radiumScene.extensionsRequired.push_back( e ); + } + try { + gltf::Save( radiumScene, m_fileName, false ); + } + catch ( gltf::invalid_gltf_document& e ) { + LOG( logERROR ) << "Caught invalid_gltf_document exception : " << e.what(); + } + catch ( std::exception& e ) { + LOG( logERROR ) << "Caught std::exception exception : " << e.what(); + } +} + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/Writer/glTFFileWriter.hpp b/src/IO/Gltf/Writer/glTFFileWriter.hpp new file mode 100644 index 00000000000..5e0b45c7100 --- /dev/null +++ b/src/IO/Gltf/Writer/glTFFileWriter.hpp @@ -0,0 +1,49 @@ +#pragma once +#include + +#include +#include + +namespace Ra::Engine::Scene { +class Entity; +} + +namespace Ra { +namespace IO { +namespace GLTF { + +/** + * FileWriter for GLTF2.0 file format + * + * TODO : make the loader keep the names of elements and save elements with the same name + */ +class RA_IO_API glTFFileWriter +{ + public: + /** + * Create a GLTF file writer + * @param filename the file to save + * @param texturePrefix the texture uri prefix + * @param writeImages set it to true to also export texture images. + * + * @note Images are not yet exported. It is expected that they are in the texturePrefix relative + * path (uri) + */ + explicit glTFFileWriter( std::string filename, + std::string texturePrefix = { "textures/" }, + bool writeImages = false ); + ~glTFFileWriter(); + + void write( std::vector toExport ); + + private: + std::string m_fileName; + std::string m_texturePrefix; + bool m_writeImages; + std::string m_bufferName; + std::string m_rootName; +}; + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/Extensions/LightExtensions.hpp b/src/IO/Gltf/internal/Extensions/LightExtensions.hpp new file mode 100644 index 00000000000..7f9be765663 --- /dev/null +++ b/src/IO/Gltf/internal/Extensions/LightExtensions.hpp @@ -0,0 +1,179 @@ +#pragma once +#include + +namespace Ra { +namespace IO { +namespace GLTF { + +/** + * https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_lights_punctual + * + */ + +/// https://github.com/KhronosGroup/glTF/blob/master/extensions/2.0/Khronos/KHR_lights_punctual/schema/light.spot.schema.json +struct gltf_lightSpot { + /// Angle in radians from centre of spotlight where falloff begins. + /// min : 0 + /// max (exclusive) : pi/2 + float innerConeAngle { 0.0 }; + /// Angle in radians from centre of spotlight where falloff ends. + /// min : 0 + /// max (exclusive) : pi/2 + float outerConeAngle { 0.7853981633974483 }; + nlohmann::json extensionsAndExtras {}; + + [[nodiscard]] bool empty() const { + return innerConeAngle == 0.f && outerConeAngle == 0.7853981633974483f; + } +}; + +inline void from_json( nlohmann::json const& json, gltf_lightSpot& lightSpot ) { + fx::gltf::detail::ReadOptionalField( "innerConeAngle", json, lightSpot.innerConeAngle ); + fx::gltf::detail::ReadOptionalField( "outerConeAngle", json, lightSpot.outerConeAngle ); + + fx::gltf::detail::ReadExtensionsAndExtras( json, lightSpot.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, gltf_lightSpot const& lightSpot ) { + fx::gltf::detail::WriteField( { "innerConeAngle" }, json, lightSpot.innerConeAngle, 0.0f ); + fx::gltf::detail::WriteField( + { "outerConeAngle" }, json, lightSpot.outerConeAngle, 0.7853981633974483f ); + fx::gltf::detail::WriteExtensions( json, lightSpot.extensionsAndExtras ); +} + +/// https://github.com/KhronosGroup/glTF/blob/master/extensions/2.0/Khronos/KHR_lights_punctual/schema/light.schema.json +struct gltf_lightPunctual { + enum class Type { + /// Directional lights act as though they are infinitely far away and emit light in the + /// direction of the local -z axis. This light type inherits the orientation of the node + /// that it belongs to; position and scale are ignored except for their effect on the + /// inherited node orientation. Because it is at an infinite distance, the light is not + /// attenuated. Its intensity is defined in lumens per metre squared, or lux (lm/m^2). + directional, + /// Point lights emit light in all directions from their position in space; rotation and + /// scale are ignored except for their effect on the inherited node position. ` + /// The brightness of the light attenuates in a physically correct manner as distance + /// increases from the light's position (i.e. brightness goes like the inverse square of + /// the distance). Point light intensity is defined in candela, which is lumens per square + /// radian (lm/sr). + point, + /// Spot lights emit light in a cone in the direction of the local -z axis. The angle + /// and falloff of the cone is defined using two numbers, the innerConeAngle and + /// outerConeAngle. As with point lights, the brightness also attenuates in a physically + /// correct manner as distance increases from the light's position (i.e. brightness goes + /// like the inverse square of the distance). Spot light intensity refers to the brightness + /// inside the innerConeAngle (and at the location of the light) and is defined in candela, + /// which is lumens per square radian (lm/sr). Engines that don't support two angles + /// for spotlights should use outerConeAngle as the spotlight angle (leaving innerConeAngle + /// to implicitly be 0). + spot, + None + }; + /// The type of the light source + Type type { Type::None }; + /// Color of the light source. + std::array color = { fx::gltf::defaults::IdentityVec3 }; + /// Intensity of the light source. `point` and `spot` lights use luminous intensity in + /// candela (lm/sr) while `directional` lights use illuminance in lux (lm/m^2). + float intensity { 1.0 }; + /// spot properties + gltf_lightSpot spot; + /// A distance cutoff at which the light's intensity may be considered to have reached zero. + /// attenuation = max( min( 1.0 - ( current_distance / range )^4, 1 ), 0 ) / current_distance^2 + float range { std::numeric_limits::max() }; + /// Name of the light source + std::string name {}; + /// extensions and extra + nlohmann::json extensionsAndExtras {}; +}; + +inline void from_json( nlohmann::json const& json, gltf_lightPunctual::Type& lightType ) { + std::string type = json.get(); + if ( type == "directional" ) { lightType = gltf_lightPunctual::Type::directional; } + else if ( type == "point" ) { lightType = gltf_lightPunctual::Type::point; } + else if ( type == "spot" ) { lightType = gltf_lightPunctual::Type::spot; } + else { throw fx::gltf::invalid_gltf_document( "Unknown lights_punctual.type value", type ); } +} + +inline void from_json( nlohmann::json const& json, gltf_lightPunctual& lightPunctual ) { + fx::gltf::detail::ReadRequiredField( "type", json, lightPunctual.type ); + fx::gltf::detail::ReadOptionalField( "name", json, lightPunctual.name ); + fx::gltf::detail::ReadOptionalField( "color", json, lightPunctual.color ); + fx::gltf::detail::ReadOptionalField( "intensity", json, lightPunctual.intensity ); + fx::gltf::detail::ReadOptionalField( "range", json, lightPunctual.range ); + if ( lightPunctual.type == gltf_lightPunctual::Type::spot ) { + fx::gltf::detail::ReadOptionalField( "spot", json, lightPunctual.spot ); + } + fx::gltf::detail::ReadExtensionsAndExtras( json, lightPunctual.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, gltf_lightPunctual::Type& lightType ) { + switch ( lightType ) { + case gltf_lightPunctual::Type::directional: + json = "directional"; + break; + case gltf_lightPunctual::Type::point: + json = "point"; + break; + case gltf_lightPunctual::Type::spot: + json = "spot"; + break; + default: + throw fx::gltf::invalid_gltf_document( "Unknown lights_punctual.type value" ); + } +} + +inline void to_json( nlohmann::json& json, gltf_lightPunctual const& lightPunctual ) { + fx::gltf::detail::WriteField( "name", json, lightPunctual.name ); + fx::gltf::detail::WriteField( + "type", json, lightPunctual.type, gltf_lightPunctual::Type::None ); + fx::gltf::detail::WriteField( "color", json, lightPunctual.color ); + fx::gltf::detail::WriteField( "intensity", json, lightPunctual.intensity, 1.0f ); + fx::gltf::detail::WriteField( + "range", json, lightPunctual.range, std::numeric_limits::max() ); + if ( lightPunctual.type == gltf_lightPunctual::Type::spot ) { + fx::gltf::detail::WriteField( "spot", json, lightPunctual.spot ); + } + fx::gltf::detail::WriteExtensions( json, lightPunctual.extensionsAndExtras ); +} + +struct gltf_KHR_lights_punctual { + /// The vector of lights + std::vector lights {}; + /// extensions and extra + nlohmann::json extensionsAndExtras {}; +}; + +inline void from_json( nlohmann::json const& json, gltf_KHR_lights_punctual& lights_punctual ) { + fx::gltf::detail::ReadRequiredField( "lights", json, lights_punctual.lights ); + fx::gltf::detail::ReadExtensionsAndExtras( json, lights_punctual.extensionsAndExtras ); + if ( lights_punctual.lights.empty() ) { + throw fx::gltf::invalid_gltf_document( "KHR_lights_punctual must have a least 1 light!" ); + } +} + +inline void to_json( nlohmann::json& json, gltf_KHR_lights_punctual const& lights_punctual ) { + fx::gltf::detail::WriteField( "lights", json, lights_punctual.lights ); + fx::gltf::detail::WriteExtensions( json, lights_punctual.extensionsAndExtras ); +} + +struct gltf_node_KHR_lights_punctual { + /// The light index in the gltf_KHR_lights_punctual extension + int32_t light { -1 }; + /// extensions and extra + nlohmann::json extensionsAndExtras {}; +}; + +inline void from_json( nlohmann::json const& json, gltf_node_KHR_lights_punctual& light ) { + fx::gltf::detail::ReadRequiredField( "light", json, light.light ); + fx::gltf::detail::ReadExtensionsAndExtras( json, light.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, gltf_node_KHR_lights_punctual const& light ) { + fx::gltf::detail::WriteField( "light", json, light.light, -1 ); + fx::gltf::detail::WriteExtensions( json, light.extensionsAndExtras ); +} + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/Extensions/MaterialExtensions.hpp b/src/IO/Gltf/internal/Extensions/MaterialExtensions.hpp new file mode 100644 index 00000000000..22ddfeecef7 --- /dev/null +++ b/src/IO/Gltf/internal/Extensions/MaterialExtensions.hpp @@ -0,0 +1,343 @@ +#pragma once +#include + +/** + * Add default values to the namespace containing all default values. + * @note : it is not a good idea to add to external namespace + */ +namespace fx::gltf::defaults { +constexpr std::array IdentityVec2 { 1, 1 }; +constexpr std::array NullVec2 { 0, 0 }; +} // namespace fx::gltf::defaults + +namespace Ra { +namespace IO { +namespace GLTF { + +/** + * https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness + * + */ +struct gltf_PBRSpecularGlossiness { + std::array diffuseFactor { fx::gltf::defaults::IdentityVec4 }; + fx::gltf::Material::Texture diffuseTexture; + + float glossinessFactor { fx::gltf::defaults::IdentityScalar }; + std::array specularFactor { fx::gltf::defaults::IdentityVec3 }; + fx::gltf::Material::Texture specularGlossinessTexture; + + nlohmann::json extensionsAndExtras {}; + + [[nodiscard]] bool empty() const { + return diffuseTexture.empty() && diffuseFactor == fx::gltf::defaults::IdentityVec4 && + glossinessFactor == fx::gltf::defaults::IdentityScalar && + specularFactor == fx::gltf::defaults::IdentityVec3 && + specularGlossinessTexture.empty(); + } +}; + +inline void from_json( nlohmann::json const& json, + gltf_PBRSpecularGlossiness& pbrSpecularGlossiness ) { + fx::gltf::detail::ReadOptionalField( + "diffuseFactor", json, pbrSpecularGlossiness.diffuseFactor ); + fx::gltf::detail::ReadOptionalField( + "diffuseTexture", json, pbrSpecularGlossiness.diffuseTexture ); + fx::gltf::detail::ReadOptionalField( + "glossinessFactor", json, pbrSpecularGlossiness.glossinessFactor ); + fx::gltf::detail::ReadOptionalField( + "specularFactor", json, pbrSpecularGlossiness.specularFactor ); + fx::gltf::detail::ReadOptionalField( + "specularGlossinessTexture", json, pbrSpecularGlossiness.specularGlossinessTexture ); + + fx::gltf::detail::ReadExtensionsAndExtras( json, pbrSpecularGlossiness.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, + gltf_PBRSpecularGlossiness const& pbrSpecularGlossiness ) { + fx::gltf::detail::WriteField( { "diffuseFactor" }, + json, + pbrSpecularGlossiness.diffuseFactor, + fx::gltf::defaults::IdentityVec4 ); + fx::gltf::detail::WriteField( + { "diffuseTexture" }, json, pbrSpecularGlossiness.diffuseTexture ); + fx::gltf::detail::WriteField( { "glossinessFactor" }, + json, + pbrSpecularGlossiness.glossinessFactor, + fx::gltf::defaults::IdentityScalar ); + fx::gltf::detail::WriteField( { "specularFactor" }, + json, + pbrSpecularGlossiness.specularFactor, + fx::gltf::defaults::IdentityVec3 ); + fx::gltf::detail::WriteField( + { "specularGlossinessTexture" }, json, pbrSpecularGlossiness.specularGlossinessTexture ); + fx::gltf::detail::WriteExtensions( json, pbrSpecularGlossiness.extensionsAndExtras ); +} + +/** + * https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_transform + * KHR_texture_transform + */ +struct gltf_KHRTextureTransform { + std::array offset { fx::gltf::defaults::NullVec2 }; + std::array scale { fx::gltf::defaults::IdentityVec2 }; + float rotation { 0.0 }; + int texCoord { -1 }; + nlohmann::json extensionsAndExtras {}; + + bool isDefault() { + return ( rotation == 0.0 ) && ( texCoord == -1 ) && + ( offset == fx::gltf::defaults::NullVec2 ) && + ( scale == fx::gltf::defaults::IdentityVec2 ) && extensionsAndExtras.is_null(); + } +}; + +inline void from_json( nlohmann::json const& json, gltf_KHRTextureTransform& khrTextureTransform ) { + fx::gltf::detail::ReadOptionalField( "offset", json, khrTextureTransform.offset ); + fx::gltf::detail::ReadOptionalField( "scale", json, khrTextureTransform.scale ); + fx::gltf::detail::ReadOptionalField( "rotation", json, khrTextureTransform.rotation ); + fx::gltf::detail::ReadOptionalField( "texCoord", json, khrTextureTransform.texCoord ); + + fx::gltf::detail::ReadExtensionsAndExtras( json, khrTextureTransform.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, gltf_KHRTextureTransform const& khrTextureTransform ) { + fx::gltf::detail::WriteField( + { "offset" }, json, khrTextureTransform.offset, fx::gltf::defaults::NullVec2 ); + fx::gltf::detail::WriteField( + { "scale" }, json, khrTextureTransform.scale, fx::gltf::defaults::IdentityVec2 ); + fx::gltf::detail::WriteField( { "rotation" }, json, khrTextureTransform.rotation, 0.f ); + fx::gltf::detail::WriteField( { "texCoord" }, json, khrTextureTransform.texCoord, -1 ); + + fx::gltf::detail::WriteExtensions( json, khrTextureTransform.extensionsAndExtras ); +} + +/** + * KHR_materials_ior + * https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_ior + */ +struct gltf_KHRMaterialsIor { + float ior { 1.5 }; + nlohmann::json extensionsAndExtras {}; +}; + +inline void from_json( nlohmann::json const& json, gltf_KHRMaterialsIor& khrMaterialsIor ) { + fx::gltf::detail::ReadOptionalField( "ior", json, khrMaterialsIor.ior ); + fx::gltf::detail::ReadExtensionsAndExtras( json, khrMaterialsIor.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, gltf_KHRMaterialsIor const& khrMaterialsIor ) { + fx::gltf::detail::WriteField( { "ior" }, json, khrMaterialsIor.ior, 1.5f ); + + fx::gltf::detail::WriteExtensions( json, khrMaterialsIor.extensionsAndExtras ); +} + +/** + * KHR_materials_clearcoat + * https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_clearcoat + */ +struct gltf_KHRMaterialsClearcoat { + float clearcoatFactor { 0. }; + fx::gltf::Material::Texture clearcoatTexture; + + float clearcoatRoughnessFactor { 0. }; + fx::gltf::Material::Texture clearcoatRoughnessTexture; + + fx::gltf::Material::NormalTexture clearcoatNormalTexture; + + nlohmann::json extensionsAndExtras {}; + + bool isDefault() { + return ( clearcoatFactor == 0.0 ) && ( clearcoatRoughnessFactor == 0.0 ) && + clearcoatTexture.empty() && clearcoatRoughnessTexture.empty() && + clearcoatNormalTexture.empty() && extensionsAndExtras.is_null(); + } +}; + +inline void from_json( nlohmann::json const& json, + gltf_KHRMaterialsClearcoat& khrMaterialsClearcoat ) { + fx::gltf::detail::ReadOptionalField( + "clearcoatFactor", json, khrMaterialsClearcoat.clearcoatFactor ); + fx::gltf::detail::ReadOptionalField( + "clearcoatTexture", json, khrMaterialsClearcoat.clearcoatTexture ); + + fx::gltf::detail::ReadOptionalField( + "clearcoatRoughnessFactor", json, khrMaterialsClearcoat.clearcoatRoughnessFactor ); + fx::gltf::detail::ReadOptionalField( + "clearcoatRoughnessTexture", json, khrMaterialsClearcoat.clearcoatRoughnessTexture ); + + fx::gltf::detail::ReadOptionalField( + "clearcoatNormalTexture", json, khrMaterialsClearcoat.clearcoatNormalTexture ); + + fx::gltf::detail::ReadExtensionsAndExtras( json, khrMaterialsClearcoat.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, + gltf_KHRMaterialsClearcoat const& khrMaterialsClearcoat ) { + fx::gltf::detail::WriteField( + { "clearcoatFactor" }, json, khrMaterialsClearcoat.clearcoatFactor, 0.f ); + if ( !khrMaterialsClearcoat.clearcoatTexture.empty() ) { + fx::gltf::detail::WriteField( + { "clearcoatTexture" }, json, khrMaterialsClearcoat.clearcoatTexture ); + } + + fx::gltf::detail::WriteField( + { "clearcoatRoughnessFactor" }, json, khrMaterialsClearcoat.clearcoatRoughnessFactor, 0.f ); + if ( !khrMaterialsClearcoat.clearcoatRoughnessTexture.empty() ) { + fx::gltf::detail::WriteField( { "clearcoatRoughnessTexture" }, + json, + khrMaterialsClearcoat.clearcoatRoughnessTexture ); + } + + if ( !khrMaterialsClearcoat.clearcoatNormalTexture.empty() ) { + fx::gltf::detail::WriteField( + { "clearcoatNormalTexture" }, json, khrMaterialsClearcoat.clearcoatNormalTexture ); + } + + fx::gltf::detail::WriteExtensions( json, khrMaterialsClearcoat.extensionsAndExtras ); +} + +/** + * KHR_materials_specular + * https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_specular + */ +struct gltf_KHRMaterialsSpecular { + float specularFactor { 1. }; + fx::gltf::Material::Texture specularTexture; + + std::array specularColorFactor { fx::gltf::defaults::IdentityVec3 }; + fx::gltf::Material::Texture specularColorTexture; + + nlohmann::json extensionsAndExtras {}; + + bool isDefault() { + return ( specularFactor == 1.0 ) && + ( specularColorFactor == fx::gltf::defaults::IdentityVec3 ) && + specularTexture.empty() && specularColorTexture.empty() && + extensionsAndExtras.is_null(); + } +}; + +inline void from_json( nlohmann::json const& json, + gltf_KHRMaterialsSpecular& khrMaterialsSpecular ) { + fx::gltf::detail::ReadOptionalField( + "specularFactor", json, khrMaterialsSpecular.specularFactor ); + fx::gltf::detail::ReadOptionalField( + "specularTexture", json, khrMaterialsSpecular.specularTexture ); + + fx::gltf::detail::ReadOptionalField( + "specularColorFactor", json, khrMaterialsSpecular.specularColorFactor ); + fx::gltf::detail::ReadOptionalField( + "specularColorTexture", json, khrMaterialsSpecular.specularColorTexture ); + + fx::gltf::detail::ReadExtensionsAndExtras( json, khrMaterialsSpecular.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, gltf_KHRMaterialsSpecular const& khrMaterialsSpecular ) { + fx::gltf::detail::WriteField( + { "specularFactor" }, json, khrMaterialsSpecular.specularFactor, 1.f ); + if ( !khrMaterialsSpecular.specularTexture.empty() ) { + fx::gltf::detail::WriteField( + { "specularTexture" }, json, khrMaterialsSpecular.specularTexture ); + } + + fx::gltf::detail::WriteField( { "specularColorFactor" }, + json, + khrMaterialsSpecular.specularColorFactor, + fx::gltf::defaults::IdentityVec3 ); + if ( !khrMaterialsSpecular.specularColorTexture.empty() ) { + fx::gltf::detail::WriteField( + { "specularColorTexture" }, json, khrMaterialsSpecular.specularColorTexture ); + } + + fx::gltf::detail::WriteExtensions( json, khrMaterialsSpecular.extensionsAndExtras ); +} + +/** + * KHR_materials_sheen + * https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_sheen + */ +struct gltf_KHRMaterialsSheen { + std::array sheenColorFactor { fx::gltf::defaults::NullVec3 }; + fx::gltf::Material::Texture sheenColorTexture; + + float sheenRoughnessFactor { 0. }; + fx::gltf::Material::Texture sheenRoughnessTexture; + + nlohmann::json extensionsAndExtras {}; + + bool isDefault() { + return ( sheenRoughnessFactor == 0.0 ) && + ( sheenColorFactor == fx::gltf::defaults::NullVec3 ) && sheenColorTexture.empty() && + sheenRoughnessTexture.empty() && extensionsAndExtras.is_null(); + } +}; + +inline void from_json( nlohmann::json const& json, gltf_KHRMaterialsSheen& khrMaterialsSheen ) { + fx::gltf::detail::ReadOptionalField( + "sheenColorFactor", json, khrMaterialsSheen.sheenColorFactor ); + fx::gltf::detail::ReadOptionalField( + "sheenColorTexture", json, khrMaterialsSheen.sheenColorTexture ); + + fx::gltf::detail::ReadOptionalField( + "sheenRoughnessFactor", json, khrMaterialsSheen.sheenRoughnessFactor ); + fx::gltf::detail::ReadOptionalField( + "sheenRoughnessTexture", json, khrMaterialsSheen.sheenRoughnessTexture ); + + fx::gltf::detail::ReadExtensionsAndExtras( json, khrMaterialsSheen.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, gltf_KHRMaterialsSheen const& khrMaterialsSheen ) { + fx::gltf::detail::WriteField( { "sheenColorFactor" }, + json, + khrMaterialsSheen.sheenColorFactor, + fx::gltf::defaults::NullVec3 ); + if ( !khrMaterialsSheen.sheenColorTexture.empty() ) { + fx::gltf::detail::WriteField( + { "sheenColorTexture" }, json, khrMaterialsSheen.sheenColorTexture ); + } + + fx::gltf::detail::WriteField( + { "sheenRoughnessFactor" }, json, khrMaterialsSheen.sheenRoughnessFactor, 0.f ); + if ( !khrMaterialsSheen.sheenRoughnessTexture.empty() ) { + fx::gltf::detail::WriteField( + { "sheenRoughnessTexture" }, json, khrMaterialsSheen.sheenRoughnessTexture ); + } + + fx::gltf::detail::WriteExtensions( json, khrMaterialsSheen.extensionsAndExtras ); +} + +/** + * INN_material_atlas_V1 + * + */ +struct gltf_INNMaterialAtlas { + struct INN_AtlasTexture : fx::gltf::Material::Texture { + int nbMaterial { 0 }; + }; + + INN_AtlasTexture atlasTexture; +}; + +inline void from_json( nlohmann::json const& json, + gltf_INNMaterialAtlas::INN_AtlasTexture& atlasTexture ) { + from_json( json, static_cast( atlasTexture ) ); + fx::gltf::detail::ReadRequiredField( "nbMaterial", json, atlasTexture.nbMaterial ); +} + +inline void from_json( nlohmann::json const& json, gltf_INNMaterialAtlas& textureAtlas ) { + fx::gltf::detail::ReadRequiredField( "atlasTexture", json, textureAtlas.atlasTexture ); +} + +inline void to_json( nlohmann::json& json, + gltf_INNMaterialAtlas::INN_AtlasTexture const& atlasTexture ) { + to_json( json, static_cast( atlasTexture ) ); + fx::gltf::detail::WriteField( { "nbMaterial" }, json, atlasTexture.nbMaterial, 0 ); +} + +inline void to_json( nlohmann::json& json, gltf_INNMaterialAtlas const& textureAtlas ) { + fx::gltf::detail::WriteField( { "atlasTexture" }, json, textureAtlas.atlasTexture ); +} + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/GLTFConverter/AccessorReader.cpp b/src/IO/Gltf/internal/GLTFConverter/AccessorReader.cpp new file mode 100644 index 00000000000..88088d7d344 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/AccessorReader.cpp @@ -0,0 +1,226 @@ +#include + +#include + +namespace Ra { +namespace IO { +namespace GLTF { + +using namespace fx; + +using namespace Ra::Core::Utils; + +const std::map nbByteByValueMap { + { gltf::Accessor::ComponentType::Byte, 1 }, + { gltf::Accessor::ComponentType::UnsignedByte, 1 }, + { gltf::Accessor::ComponentType::Short, 2 }, + { gltf::Accessor::ComponentType::UnsignedShort, 2 }, + { gltf::Accessor::ComponentType::UnsignedByte, 4 }, + { gltf::Accessor::ComponentType::Float, 4 }, + { gltf::Accessor::ComponentType::None, 0 } }; + +const std::map nbValueByComponentMap { + { gltf::Accessor::Type::Scalar, 1 }, + { gltf::Accessor::Type::Vec2, 2 }, + { gltf::Accessor::Type::Vec3, 3 }, + { gltf::Accessor::Type::Vec4, 4 }, + { gltf::Accessor::Type::Mat2, 4 }, + { gltf::Accessor::Type::Mat3, 9 }, + { gltf::Accessor::Type::Mat4, 16 }, + { gltf::Accessor::Type::None, 0 } }; + +template +void minmax( uint8_t* data, const gltf::Accessor& accessor, int nbComponents ) { + std::vector min; + std::vector max; + T defaultMin = std::numeric_limits::min(); + T defaultMax = std::numeric_limits::max(); + auto* convertedData = (T*)data; + if ( accessor.min.empty() ) { min = std::vector( nbComponents, defaultMin ); } + else { + std::transform( accessor.min.begin(), + accessor.min.end(), + std::back_inserter( min ), + []( float i ) { return static_cast( i ); } ); + } + if ( accessor.max.empty() ) { max = std::vector( nbComponents, defaultMax ); } + else { + std::transform( accessor.max.begin(), + accessor.max.end(), + std::back_inserter( max ), + []( float i ) { return static_cast( i ); } ); + } + int k = 0; + for ( uint32_t i = 0; i < accessor.count; ++i ) { + for ( int j = 0; j < nbComponents; ++j ) { + if ( convertedData[k] > max[j] ) { convertedData[k] = max[j]; } + else if ( convertedData[k] < min[j] ) { convertedData[k] = min[j]; } + ++k; + } + } +} + +template +void sparseData( uint8_t* data, + const gltf::Accessor::Sparse& sparse, + int nbBytesByComponents, + const uint8_t* indices, + const uint8_t* values ) { + auto* indicesT = (T*)indices; + for ( int i = 0; i < sparse.count; ++i ) { + for ( int j = 0; j < nbBytesByComponents; ++j ) { + data[indicesT[i] * nbBytesByComponents + j] = values[i * nbBytesByComponents + j]; + } + } +} + +template +uint8_t* normalizeData( uint8_t* data, uint32_t nbComponents ) { + T max = std::numeric_limits::max(); + T* dataT = (T*)data; + auto* dataFloat = new float[nbComponents]; + for ( uint32_t i = 0; i < nbComponents; ++i ) { + dataFloat[i] = std::max( dataT[i] / (float)max, -1.0f ); + } + delete data; + return (uint8_t*)dataFloat; +} + +uint8_t* readBufferView( const gltf::Document& doc, + int bufferViewIndex, + int byteOffset, + int count, + int nbValueByComponents, + int nbByteByValue ) { + const auto& bufferView = doc.bufferViews[bufferViewIndex]; + const auto& buffer = doc.buffers[bufferView.buffer]; + auto byteStride = bufferView.byteStride; + auto rawBuffer = buffer.data.data(); + int nbBytesByComponents = nbValueByComponents * nbByteByValue; + byteStride = ( byteStride == 0 ) ? nbBytesByComponents : byteStride; + auto data = new uint8_t[count * nbBytesByComponents]; + rawBuffer = rawBuffer + byteOffset + bufferView.byteOffset; + + // the data is store in little endian + for ( int i = 0; i < count; ++i ) { + for ( int j = 0; j < nbValueByComponents; ++j ) { + for ( int k = 0; k < nbByteByValue; ++k ) { + data[i * nbBytesByComponents + j * nbByteByValue + k] = + rawBuffer[j * nbByteByValue + k]; + } + } + rawBuffer = rawBuffer + byteStride; + } + + return data; +} + +void sparseCapDataNormalize( uint8_t*& data, const gltf::Document& doc, int accessorsIndex ) { + gltf::Accessor accessor = doc.accessors[accessorsIndex]; + gltf::Accessor::Sparse sparse = accessor.sparse; + int nbValueByComponents = nbValueByComponentMap.at( accessor.type ); + int nbByteByValue = nbByteByValueMap.at( accessor.componentType ); + if ( !sparse.empty() ) { + uint8_t* indices = readBufferView( doc, + sparse.indices.bufferView, + sparse.indices.byteOffset, + sparse.count, + 1, + nbByteByValueMap.at( sparse.indices.componentType ) ); + uint8_t* values = readBufferView( doc, + sparse.values.bufferView, + sparse.values.byteOffset, + sparse.count, + nbValueByComponents, + nbByteByValue ); + switch ( sparse.indices.componentType ) { + case gltf::Accessor::ComponentType::UnsignedByte: + sparseData( + data, sparse, nbValueByComponents * nbByteByValue, indices, values ); + break; + case gltf::Accessor::ComponentType::UnsignedShort: + sparseData( + data, sparse, nbValueByComponents * nbByteByValue, indices, values ); + break; + case gltf::Accessor::ComponentType::UnsignedInt: + sparseData( + data, sparse, nbValueByComponents * nbByteByValue, indices, values ); + break; + default: + LOG( logINFO ) << "Illegal type : sparse.indices.componentType"; + exit( 1 ); + } + delete indices; + delete values; + } + bool normalized = accessor.normalized; + switch ( accessor.componentType ) { + case gltf::Accessor::ComponentType::Byte: + if ( !accessor.min.empty() || !accessor.max.empty() ) + minmax( data, accessor, nbValueByComponents ); + if ( normalized ) + data = normalizeData( data, accessor.count * nbValueByComponents ); + break; + case gltf::Accessor::ComponentType::UnsignedByte: + if ( !accessor.min.empty() || !accessor.max.empty() ) + minmax( data, accessor, nbValueByComponents ); + if ( normalized ) + data = normalizeData( data, accessor.count * nbValueByComponents ); + break; + case gltf::Accessor::ComponentType::Short: + if ( !accessor.min.empty() || !accessor.max.empty() ) + minmax( data, accessor, nbValueByComponents ); + if ( normalized ) + data = normalizeData( data, accessor.count * nbValueByComponents ); + break; + case gltf::Accessor::ComponentType::UnsignedShort: + if ( !accessor.min.empty() || !accessor.max.empty() ) + minmax( data, accessor, nbValueByComponents ); + if ( normalized ) + data = normalizeData( data, accessor.count * nbValueByComponents ); + break; + case gltf::Accessor::ComponentType::UnsignedInt: + if ( !accessor.min.empty() || !accessor.max.empty() ) + minmax( data, accessor, nbValueByComponents ); + if ( normalized ) + data = normalizeData( data, accessor.count * nbValueByComponents ); + break; + case gltf::Accessor::ComponentType::Float: + if ( !accessor.min.empty() || !accessor.max.empty() ) + minmax( data, accessor, nbValueByComponents ); + if ( normalized ) + data = normalizeData( data, accessor.count * nbValueByComponents ); + break; + default: + LOG( logINFO ) << "Illegal type : accessor.componentType"; + exit( 1 ); + } +} + +// Read the accessor and return a pointer where the data has been stored. +// The pointer can be cast to the accessor's type. If normalizes +uint8_t* AccessorReader::read( int32_t accessorIndex ) { + // if data already in the map + if ( m_accessors.find( accessorIndex ) != m_accessors.end() ) { + return m_accessors[accessorIndex]; + } + const gltf::Document& doc = m_doc; + gltf::Accessor accessor = doc.accessors[accessorIndex]; + int nbByteByValue = nbByteByValueMap.at( accessor.componentType ); + int nbValueByComponent = nbValueByComponentMap.at( accessor.type ); + uint8_t* data = readBufferView( doc, + accessor.bufferView, + accessor.byteOffset, + accessor.count, + nbValueByComponent, + nbByteByValue ); + // sparse and min-max + sparseCapDataNormalize( data, doc, accessorIndex ); + // add data to map + m_accessors.insert( std::pair( accessorIndex, data ) ); + return data; +} + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/GLTFConverter/AccessorReader.hpp b/src/IO/Gltf/internal/GLTFConverter/AccessorReader.hpp new file mode 100644 index 00000000000..81d0aca2a80 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/AccessorReader.hpp @@ -0,0 +1,46 @@ +#pragma once +#include + +#include + +namespace Ra { +namespace IO { +namespace GLTF { + +/** + * + */ +class AccessorReader +{ + public: + /** + * Constructor of the accessorReader + * @param doc the gltf's document + */ + explicit AccessorReader( const fx::gltf::Document& doc ) : m_doc( doc ) {}; + + /** + * Destructor of the accessorReader + */ + ~AccessorReader() { + for ( std::pair p : m_accessors ) { + delete p.second; + } + }; + + /** + * Read the accessor + * @param accessorIndex index of the gltf's accessor + * @return a pointer to the data. The pointer can be cast the the corresponding type. + * If the data should be normalized, the stored data's type is float + */ + uint8_t* read( int32_t accessorIndex ); + + private: + const fx::gltf::Document& m_doc; + std::map m_accessors; +}; + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/GLTFConverter/Converter.cpp b/src/IO/Gltf/internal/GLTFConverter/Converter.cpp new file mode 100644 index 00000000000..d47ff3b6968 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/Converter.cpp @@ -0,0 +1,414 @@ +#include +#include +#include +#include +#include +#include + +#include + +using namespace fx; + +namespace Ra { +namespace IO { +namespace GLTF { + +using namespace Ra::Core; +using namespace Ra::Core::Asset; +using namespace Ra::Core::Utils; + +static std::vector gltfSupportedExtensions { { "KHR_lights_punctual" }, + { "KHR_materials_pbrSpecularGlossiness" }, + { "KHR_texture_transform" }, + { "KHR_materials_ior" }, + { "KHR_materials_clearcoat" }, + { "KHR_materials_specular" }, + { "KHR_materials_sheen" }, + { "INN_material_atlas_V1" } }; + +// Check extensions used or required by this gltf scene +bool checkExtensions( const gltf::Document& gltfscene ) { + + if ( !gltfscene.extensionsRequired.empty() ) { + for ( const auto& ext : gltfscene.extensionsRequired ) { + if ( !std::any_of( gltfSupportedExtensions.begin(), + gltfSupportedExtensions.end(), + [&ext]( const auto& supported ) { return supported == ext; } ) ) { + LOG( logINFO ) << "Required extension " << ext + << " not supported by Radium glTF2 file loader."; + return false; + } + } + } + if ( !gltfscene.extensionsUsed.empty() ) { + for ( const auto& ext : gltfscene.extensionsUsed ) { + if ( !std::any_of( gltfSupportedExtensions.begin(), + gltfSupportedExtensions.end(), + [&ext]( const auto& supported ) { return supported == ext; } ) ) { + LOG( logINFO ) + << "Used extension " << ext + << " not supported by Radium glTF2 file loader, fallback to default."; + } + else { LOG( logINFO ) << "Using extension " << ext; } + } + } + return true; +} + +LightData* +getLight( const gltf_KHR_lights_punctual& lights, int32_t lightIndex, const Transform& transform ) { + if ( lightIndex < lights.lights.size() ) { + const auto gltfLight = lights.lights[lightIndex]; + std::string lightName = gltfLight.name; + if ( lightName.empty() ) { lightName = "light_" + std::to_string( lightIndex ); } + auto color = Ra::Core::Utils::Color { gltfLight.color[0] * gltfLight.intensity, + gltfLight.color[1] * gltfLight.intensity, + gltfLight.color[2] * gltfLight.intensity }; + + auto radiumLight = new LightData( lightName ); + // warning, as this property is not used by Radium LightSystem (lightManager), + // must pre-transform the data + radiumLight->setFrame( transform.matrix() ); + auto pos = Ra::Core::Vector3 { 0_ra, 0_ra, 0_ra }; + pos = transform * pos; + auto dir = Ra::Core::Vector3 { 0_ra, 0_ra, -1_ra }; + dir = transform.linear() * dir; + LightData::LightAttenuation attenuation { 0_ra, 0_ra, 1_ra }; + switch ( gltfLight.type ) { + case gltf_lightPunctual::Type::directional: + radiumLight->setLight( color, dir ); + break; + case gltf_lightPunctual::Type::point: + radiumLight->setLight( color, pos, attenuation ); + break; + case gltf_lightPunctual::Type::spot: + radiumLight->setLight( color, + pos, + dir, + gltfLight.spot.innerConeAngle, + gltfLight.spot.outerConeAngle, + attenuation ); + break; + default: + delete radiumLight; + radiumLight = nullptr; + break; + } + return radiumLight; + } + LOG( logWARNING ) << "Gltf loader : request for light " << lightIndex << " but only " + << lights.lights.size() << " lights are available."; + return nullptr; +} + +Camera* buildCamera( const gltf::Document& doc, + int32_t cameraIndex, + const Transform& parentTransform, + const std::string& filePath, + int32_t nodeNum ) { + // Radium Camera have problems if there is a scaling in the matrix : remove the scaling + // TODO : verify and check against the gltf specification + Ra::Core::Matrix4 cameraTransform = parentTransform.matrix(); + cameraTransform.block( 0, 0, 3, 1 ).normalize(); + cameraTransform.block( 0, 1, 3, 1 ).normalize(); + cameraTransform.block( 0, 2, 3, 1 ).normalize(); + + switch ( doc.cameras[cameraIndex].type ) { + case gltf::Camera::Type::Orthographic: { + auto cam = doc.cameras[cameraIndex].orthographic; + // todo test if cam is empty ? + auto name = doc.cameras[cameraIndex].name; + if ( name.empty() ) { name = std::string { "Cam_gltf_" } + std::to_string( nodeNum ); } + else { name += std::string { "Cam_" } + std::to_string( nodeNum ); } + auto radiumCam = new Camera(); + radiumCam->setType( Camera::ProjType::ORTHOGRAPHIC ); + radiumCam->setFrame( Transform { cameraTransform } ); + radiumCam->setZNear( cam.znear ); + radiumCam->setZFar( cam.zfar ); + radiumCam->setXYmag( cam.xmag, cam.ymag ); + radiumCam->setViewport( 1_ra, 1_ra ); // TODO check this + return radiumCam; + } + case gltf::Camera::Type::Perspective: { + auto cam = doc.cameras[cameraIndex].perspective; + // todo test if cam is empty ? + auto name = doc.cameras[cameraIndex].name; + if ( name.empty() ) { name = std::string { "Cam_gltf_" } + std::to_string( nodeNum ); } + else { name += std::string { "Cam_" } + std::to_string( nodeNum ); } + auto radiumCam = new Camera(); + radiumCam->setType( Camera::ProjType::PERSPECTIVE ); + radiumCam->setZNear( cam.znear ); + radiumCam->setZFar( cam.zfar ); + + if ( cam.aspectRatio > 0 ) { radiumCam->setViewport( radiumCam->getAspect(), 1_ra ); } + else { radiumCam->setViewport( 1_ra, 1_ra ); } + + // convert fovy to fovx + Scalar fovxDiv2 = std::atan( radiumCam->getAspect() * std::tan( cam.yfov / 2_ra ) ); + + // consider that fov < pi. if fovy/2 is more than pi/2 (i.e. atan return <0) + // let's clamp it to pi/2 + if ( fovxDiv2 < 0_ra ) { fovxDiv2 = Ra::Core::Math::PiDiv2; } + + radiumCam->setFOV( 2_ra * fovxDiv2 ); + + radiumCam->setFrame( Transform { cameraTransform } ); + return radiumCam; + } + default: + return nullptr; + } +} + +// Compute the combined matrix at each node +void glTfVisitor( const gltf::Document& scene, + int32_t nodeIndex, + const Transform& parentTransform, + std::vector& graphNodes, + std::set& visitedNodes ) { + SceneNode& graphNode = graphNodes[nodeIndex]; + graphNode.m_transform = parentTransform; + visitedNodes.insert( nodeIndex ); + gltf::Node const& node = scene.nodes[nodeIndex]; + if ( !node.name.empty() ) { graphNode.m_nodeName = node.name; } + else { graphNode.m_nodeName = "Unnamed node"; } + if ( node.matrix != gltf::defaults::IdentityMatrix ) { + auto tr = node.matrix.data(); + Matrix4f mat; + mat << tr[0], tr[4], tr[8], tr[12], tr[1], tr[5], tr[9], tr[13], tr[2], tr[6], tr[10], + tr[14], tr[3], tr[7], tr[11], tr[15]; + graphNode.m_transform = graphNode.m_transform * Transform( mat ); + } + else { + // gltf transform is T * R * S + if ( node.translation != gltf::defaults::NullVec3 ) { + auto tr = node.translation.data(); + graphNode.m_transform.translate( Vector3( tr[0], tr[1], tr[2] ) ); + } + if ( node.rotation != gltf::defaults::IdentityVec4 ) { + auto tr = node.rotation.data(); + Quaternionf quat( tr[3], tr[0], tr[1], tr[2] ); + graphNode.m_transform.rotate( quat ); + } + if ( node.scale != gltf::defaults::IdentityVec3 ) { + auto tr = node.scale.data(); + graphNode.m_transform.scale( Vector3( tr[0], tr[1], tr[2] ) ); + } + } + + if ( node.camera >= 0 ) { + graphNode.m_cameraIndex = node.camera; + /* + LOG( logINFO ) << "Camera node with transformation : \n" + << graphNode.m_transform.matrix() << std::endl; + */ + } + else { + if ( node.mesh >= 0 ) { + graphNode.m_meshIndex = node.mesh; + if ( node.skin >= 0 ) { graphNode.m_skinIndex = node.skin; } + } + else { graphNode.initPropsFromExtensionsAndExtra( node.extensionsAndExtras ); } + + graphNode.children = node.children; + for ( auto childIndex : node.children ) { + glTfVisitor( scene, childIndex, graphNode.m_transform, graphNodes, visitedNodes ); + } + } +} + +void buildAnimation( std::vector& animations, + HandleDataLoader::IntToString& map, + const fx::gltf::Document& doc, + int activeAnimation ) { + std::vector channels = doc.animations[activeAnimation].channels; + std::vector samplers = doc.animations[activeAnimation].samplers; + AccessorReader accessorReader( doc ); + TransformationManager transformationManager( map ); + for ( const gltf::Animation::Channel& channel : channels ) { + gltf::Animation::Channel::Target target = channel.target; + gltf::Animation::Sampler sampler = samplers[channel.sampler]; + // weights' and scale's animations not handle by radium => now it does, so what is that + // comment for? + auto* times = (float*)accessorReader.read( sampler.input ); + auto* transformation = (float*)accessorReader.read( sampler.output ); + transformationManager.insert( target.node, + target.path, + times, + transformation, + doc.accessors[sampler.input].count, + sampler.interpolation, + doc.nodes[channel.target.node].rotation, + doc.nodes[channel.target.node].scale, + doc.nodes[channel.target.node].translation ); + } + transformationManager.buildAnimation( animations ); +} + +Converter::Converter( FileData* fd, const std::string& baseDir ) : + fileData { fd }, filePath { baseDir } {} + +bool Converter::operator()( const gltf::Document& gltfscene ) { + + MeshNameCache::resetCache(); + + if ( !checkExtensions( gltfscene ) ) { return false; } + // pre-load supported scene extensions + // manage node extension + // KHR_lights_punctual; + gltf_KHR_lights_punctual gltfLights; + + if ( !gltfscene.extensionsAndExtras.empty() ) { + auto extensions = gltfscene.extensionsAndExtras.find( "extensions" ); + if ( extensions != gltfscene.extensionsAndExtras.end() ) { + auto iter = extensions->find( "KHR_lights_punctual" ); + if ( iter != extensions->end() ) { + from_json( *iter, gltfLights ); + LOG( logINFO ) << "Found KHR_lights_punctual extension with " + << gltfLights.lights.size() << " light sources."; + } + } + } + // cf https://github.com/MathiasPaulin/fx-gltf/blob/master/examples/viewer/DirectX/D3DEngine.cpp + // BuildScene + if ( !gltfscene.scenes.empty() ) { + std::vector graphNodes( gltfscene.nodes.size() ); + + Transform rootTransform; + rootTransform.setIdentity(); + + int activeScene = gltfscene.scene; + if ( activeScene == -1 ) activeScene = 0; + std::set visitedNodes; + for ( const uint32_t sceneNode : gltfscene.scenes[activeScene].nodes ) { + glTfVisitor( gltfscene, sceneNode, rootTransform, graphNodes, visitedNodes ); + } + + int32_t nodeNum = 0; + // For skeleton and animation data + HandleDataLoader::IntToString nodeNumToComponentName; + + for ( auto visited : visitedNodes ) { + auto& graphNode = graphNodes[visited]; + // Is the node a mesh ? + if ( graphNode.m_meshIndex >= 0 ) { + auto meshParts = buildMesh( gltfscene, graphNode.m_meshIndex, filePath, nodeNum ); + for ( auto& p : meshParts ) { + p->setFrame( graphNode.m_transform ); + fileData->m_geometryData.emplace_back( std::move( p ) ); + } + ++nodeNum; + } + // Is the node a Camera ? + if ( graphNode.m_cameraIndex >= 0 ) { + + fileData->m_cameraData.emplace_back( buildCamera( gltfscene, + graphNode.m_cameraIndex, + graphNode.m_transform, + filePath, + nodeNum ) ); + + ++nodeNum; + } + // Is the node a Light ? + if ( graphNode.m_lightIndex >= 0 ) { + auto radiumLight = + getLight( gltfLights, graphNode.m_lightIndex, graphNode.m_transform ); + if ( radiumLight ) { fileData->m_lightData.emplace_back( radiumLight ); } + } + } + + // Build Skeletons + if ( !gltfscene.skins.empty() ) { + size_t skinIndex = 0; + for ( const auto& skin : gltfscene.skins ) { + auto skeleton = HandleDataLoader::loadSkeleton( + gltfscene, graphNodes, visitedNodes, skin, skinIndex, nodeNumToComponentName ); + fileData->m_handleData.push_back( std::unique_ptr( skeleton ) ); + if ( fileData->isVerbose() ) { skeleton->displayInfo(); } + ++skinIndex; + } + LOG( logINFO ) << "Loaded " << skinIndex << " skeletons."; + } + + // BuildAnimation + if ( !gltfscene.animations.empty() ) { + int activeAnimation = 0; + // find the first animation that affect the scene + while ( activeAnimation < gltfscene.animations.size() && + ( visitedNodes.find( + gltfscene.animations[activeAnimation].channels[0].target.node ) == + visitedNodes.end() ) ) { + ++activeAnimation; + } + // if animation found + if ( activeAnimation < gltfscene.animations.size() ) { + auto animationData = new AnimationData(); + // set m_dt + animationData->setTimeStep( 1.0f / 60.0f ); + // set m_keyframe + std::vector animationPart; + buildAnimation( animationPart, nodeNumToComponentName, gltfscene, activeAnimation ); + animationData->setHandleData( animationPart ); + // set m_time + Asset::AnimationTime time; + time.setStart( 0.f ); + time.setEnd( 0.f ); + for ( const auto& handleAnimation : animationPart ) { + auto t = handleAnimation.m_animationTime; + time.extends( t ); + } + animationData->setTime( time ); + // set m_name + if ( !gltfscene.animations[activeAnimation].name.empty() ) { + animationData->setName( gltfscene.animations[activeAnimation].name ); + } + else { animationData->setName( { "Animation_defaultname" } ); } + fileData->m_animationData.push_back( + std::unique_ptr( animationData ) ); + } + } + } + else { + Transform rootTransform; + rootTransform.setIdentity(); + for ( uint32_t i = 0; i < gltfscene.meshes.size(); i++ ) { + auto meshParts = buildMesh( gltfscene, i, filePath, i ); + for ( auto& p : meshParts ) { + p->setFrame( rootTransform ); + fileData->m_geometryData.emplace_back( std::move( p ) ); + } + } + } + + MeshNameCache::resetCache(); + + if ( fileData->isVerbose() ) { + LOG( logINFO ) << "Loaded gltf file : \n\t" << gltfscene.asset.generator << "\n\t" + << gltfscene.asset.copyright << "\n\tVersion " << gltfscene.asset.version; + // move the following in the verbose part + LOG( logINFO ) << "Loaded file contains : " << std::endl; + LOG( logINFO ) << "\t" << gltfscene.accessors.size() << " accessors."; + LOG( logINFO ) << "\t" << gltfscene.animations.size() << " animations."; + LOG( logINFO ) << "\t" << gltfscene.buffers.size() << " buffers."; + LOG( logINFO ) << "\t" << gltfscene.bufferViews.size() << " bufferViews."; + LOG( logINFO ) << "\t" << gltfscene.cameras.size() << " cameras."; + LOG( logINFO ) << "\t" << gltfscene.images.size() << " images."; + LOG( logINFO ) << "\t" << gltfscene.materials.size() << " materials."; + LOG( logINFO ) << "\t" << gltfscene.meshes.size() << " meshes."; + LOG( logINFO ) << "\t" << gltfscene.nodes.size() << " nodes."; + LOG( logINFO ) << "\t" << gltfscene.samplers.size() << " samplers."; + LOG( logINFO ) << "\t" << gltfscene.scenes.size() << " scenes."; + LOG( logINFO ) << "\t" << gltfscene.skins.size() << " skins."; + LOG( logINFO ) << "\t" << gltfscene.textures.size() << " textures."; + LOG( logINFO ) << "Active scene is : " << gltfscene.scene; + if ( gltfscene.scene >= 0 ) { + LOG( logINFO ) << "\t" << gltfscene.scenes[gltfscene.scene].name << std::endl; + } + } + return true; +} + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/GLTFConverter/Converter.hpp b/src/IO/Gltf/internal/GLTFConverter/Converter.hpp new file mode 100644 index 00000000000..dde610075ae --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/Converter.hpp @@ -0,0 +1,45 @@ +#pragma once +#include + +// TODO : For the moment, the scene tree is flattened. Make Radium accept scene trees +namespace Ra::Core::Asset { +class FileData; +} // namespace Ra::Core::Asset + +namespace fx::gltf { +struct Document; +} + +namespace Ra { +namespace IO { +namespace GLTF { + +/** + * Functor that process a json tree that represents a gltf scene and convert it to Radium FileData + * object. + */ +class Converter +{ + public: + /** + * Constructor of the functor + * @param fd the FileData to fill + * @param baseDir The base directory of the loaded file to access external assets. + */ + explicit Converter( Ra::Core::Asset::FileData* fd, + const std::string& baseDir = std::string {} ); + /** + * Convert the given gltf json scene to Radium FileData + * @param gltfscene + * @return + */ + bool operator()( const fx::gltf::Document& gltfscene ); + + private: + Ra::Core::Asset::FileData* fileData; + std::string filePath; +}; + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/GLTFConverter/HandleData.cpp b/src/IO/Gltf/internal/GLTFConverter/HandleData.cpp new file mode 100644 index 00000000000..81619d453e4 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/HandleData.cpp @@ -0,0 +1,273 @@ +#include +#include +#include +#include +#include + +// #define BE_VERBOSE +#ifdef BE_VERBOSE +# include +#endif + +using namespace fx; + +namespace Ra { +namespace IO { +namespace GLTF { + +using namespace Ra::Core; +using namespace Ra::Core::Asset; +using namespace Ra::Core::Utils; + +Ra::Core::Asset::HandleData* +HandleDataLoader::loadSkeleton( const fx::gltf::Document& gltfScene, + const std::vector& graphNodes, + const std::set& visitedNodes, + const fx::gltf::Skin& skin, + size_t skinIndex, + IntToString& nodeNumToComponentName ) { + + // Create skeleton and initialize its frame + auto skeleton = new HandleData(); + skeleton->setType( HandleData::SKELETON ); + std::string skeletonName = "Skeleton_" + std::to_string( skinIndex ); + skeleton->setName( skeletonName ); + // Find root node and set its transform to the skeleton + if ( skin.skeleton >= 0 ) { +#ifdef BE_VERBOSE + LOG( logINFO ) << "GLTF Skeleton : set root node to node " << skin.skeleton << " (" + << graphNodes[skin.skeleton].m_nodeName << ")"; +#endif + skeleton->setFrame( graphNodes[skin.skeleton].m_transform ); + } + else { LOG( logWARNING ) << "No root node defined for skeleton !!! TODO : find it "; } + + // skeleton joint table and nodeNum <-> nodeName joint mappings + std::map skeletonNameTable; + HandleDataLoader::StringToInt componentNameToNodeNum; + + // fetch the joints + auto skeletonJoints = + buildJoints( gltfScene, graphNodes, skin, nodeNumToComponentName, componentNameToNodeNum ); + + // fetch the joints' bindMatrices + auto jointBindMatrix = getBindMatrices( gltfScene, skin ); + + // Map the mesh part name with its jointWeight vector; + // jointWeight[jointIndexInTheSkinJointsVector] contains the vector of + // (MeshVertexIndex, weight) + std::map>>> allJointWeights; + std::map> allJointBindMatrices; + + // load joint weights and bind matrices per skinned meshes + size_t nodeNum = 0; + for ( auto visited : visitedNodes ) { + auto& graphNode = graphNodes[visited]; + if ( graphNode.m_skinIndex == skinIndex ) { + addMeshesWeightsAndBindMatrices( gltfScene, + graphNode, + nodeNum, + jointBindMatrix, + allJointWeights, + allJointBindMatrices ); + ++nodeNum; + } +#ifdef BE_VERBOSE + else { + LOG( logINFO ) << "GLTF Skeleton : visited node " << visited << "(" + << graphNode.m_nodeName << ") is not skinned."; + } +#endif + if ( graphNode.m_meshIndex >= 0 || graphNode.m_cameraIndex >= 0 ) { ++nodeNum; } + } + + // Finalize the skeleton + std::set skinnedNodes; + + // fill all the joints data + for ( int32_t i = 0; i < skeletonJoints.size(); ++i ) { + // initialize the weighs and bind matrices + for ( auto it : allJointWeights ) { + if ( !it.second[i].empty() ) { skeletonJoints[i].m_weights[it.first] = it.second[i]; } + } + for ( auto it : allJointBindMatrices ) { + if ( !it.second.empty() ) { skeletonJoints[i].m_bindMatrices[it.first] = it.second[i]; } + } + skeletonNameTable[skeletonJoints[i].m_name] = i; + skinnedNodes.insert( componentNameToNodeNum[skeletonJoints[i].m_name] ); + } + + // Set the Radium Skeleton properties + skeleton->setComponents( skeletonJoints ); + skeleton->setNameTable( skeletonNameTable ); + for ( const auto& it : allJointWeights ) { + skeleton->addBindMesh( it.first ); + } + // build the Radium Skeleton topology + HandleDataLoader::buildSkeletonTopology( + graphNodes, nodeNumToComponentName, componentNameToNodeNum, skinnedNodes, skeleton ); + + return skeleton; +} + +// -------- +AlignedStdVector +HandleDataLoader::buildJoints( const gltf::Document& gltfScene, + const std::vector& graphNodes, + const fx::gltf::Skin& skin, + IntToString& nodeNumToComponentName, + StringToInt& componentNameToNodeNum ) { +#ifdef BE_VERBOSE + LOG( logINFO ) << "GLTF Skeleton : buildJoints begin: "; +#endif + AlignedStdVector skeletonJoints( skin.joints.size(), + HandleComponentData() ); + for ( size_t i = 0; i < skin.joints.size(); ++i ) { + skeletonJoints[i].m_name = + HandleDataLoader::getJointName( gltfScene, nodeNumToComponentName, skin.joints[i] ); + skeletonJoints[i].m_frame = graphNodes[skin.joints[i]].m_transform; + componentNameToNodeNum[skeletonJoints[i].m_name] = skin.joints[i]; +#ifdef BE_VERBOSE + LOG( logINFO ) << "\tGLTF Skeleton : add joint: " << i << "(" << skin.joints[i] << ", " + << skeletonJoints[i].m_name << ")"; +#endif + } +#ifdef BE_VERBOSE + LOG( logINFO ) << "GLTF Skeleton : buildJoints done: "; +#endif + return skeletonJoints; +} + +std::vector +HandleDataLoader::getBindMatrices( const fx::gltf::Document& gltfScene, + const fx::gltf::Skin& skin ) { + std::vector jointBindMatrix( skin.joints.size(), Transform::Identity() ); + float* invBindMatrices = (float*)AccessorReader( gltfScene ).read( skin.inverseBindMatrices ); + for ( uint i = 0; i < skin.joints.size(); ++i ) { + Matrix4 mat; + mat << invBindMatrices[16 * i], invBindMatrices[16 * i + 1], invBindMatrices[16 * i + 2], + invBindMatrices[16 * i + 3], invBindMatrices[16 * i + 4], invBindMatrices[16 * i + 5], + invBindMatrices[16 * i + 6], invBindMatrices[16 * i + 7], invBindMatrices[16 * i + 8], + invBindMatrices[16 * i + 9], invBindMatrices[16 * i + 10], invBindMatrices[16 * i + 11], + invBindMatrices[16 * i + 12], invBindMatrices[16 * i + 13], + invBindMatrices[16 * i + 14], invBindMatrices[16 * i + 15]; + + jointBindMatrix[i] = Transform( mat.transpose() ); + } + return jointBindMatrix; +} + +std::string HandleDataLoader::getJointName( const gltf::Document& gltfscene, + std::map& nodeNumToComponentName, + int32_t nodeNum ) { + auto it = nodeNumToComponentName.find( nodeNum ); + if ( it != nodeNumToComponentName.end() ) { return it->second; } + else { + std::string hcName = gltfscene.nodes[nodeNum].name; + if ( hcName.empty() ) { hcName = "bone_" + std::to_string( nodeNum ); } + nodeNumToComponentName[nodeNum] = hcName; + return hcName; + } +} + +void HandleDataLoader::addMeshesWeightsAndBindMatrices( + const fx::gltf::Document& gltfScene, + const SceneNode& graphNode, + int32_t nodeNum, + std::vector& bindMatrices, + std::map>>>& jointsWeights, + std::map>& jointsMatrices ) { + + VectorArray joints {}; + VectorArray weights {}; + uint numJoints; // Number of joints in the meshPart + const auto& meshParts = gltfScene.meshes[graphNode.m_meshIndex].primitives; + for ( uint partNum = 0; partNum < meshParts.size(); partNum++ ) { + // Ensure the name is the same thant for mesh loading --> make a naming + // function .... + std::string meshPartName = gltfScene.meshes[graphNode.m_meshIndex].name; + if ( meshPartName.empty() ) { + meshPartName = "mesh_"; + meshPartName += "_n_" + std::to_string( nodeNum ) + "_m_" + + std::to_string( graphNode.m_meshIndex ) + "_p_" + + std::to_string( partNum ); + } + + joints.clear(); + weights.clear(); + numJoints = 0; + // Fill joints and weights from accessors + for ( const auto& attrib : meshParts[partNum].attributes ) { + if ( attrib.first.substr( 0, 7 ) == "JOINTS_" ) { + MeshData::GetJoints( joints, gltfScene, gltfScene.accessors[attrib.second] ); + auto weightAccessor = gltfScene.accessors[meshParts[partNum].attributes.at( + "WEIGHTS_" + attrib.first.substr( 7 ) )]; + MeshData::GetWeights( weights, gltfScene, weightAccessor ); + numJoints = joints.size(); + } + } + // Change the way to access info to conform with FileData + // jointWeight[jointIndexInTheSkinJointsVector] contains the vector of + // (MeshVertexIndex, weight) + std::vector>> jointWeights( + bindMatrices.size(), std::vector>() ); + float w; + for ( uint i = 0; i < joints.size(); ++i ) { + for ( auto iJoint = 0; iJoint < 4; iJoint++ ) { + w = weights[i]( iJoint, 0 ); + if ( w != 0. ) { + jointWeights[joints[i]( iJoint, 0 )].emplace_back( + std::pair( i % numJoints, w ) ); + } + } + } + jointsWeights[meshPartName] = jointWeights; + jointsMatrices[meshPartName] = bindMatrices; + } +} + +void HandleDataLoader::buildSkeletonTopology( const std::vector& graphNodes, + const IntToString& nodeNumToComponentName, + const StringToInt& componentNameToNodeNum, + const std::set& skinnedNodes, + HandleData* handle ) { + std::vector> edgeList; + const auto& skeletonJoints = handle->getComponentData(); + for ( auto& component : skeletonJoints ) { + const auto& node = graphNodes[componentNameToNodeNum.at( component.m_name )]; + for ( auto j = 0; j < node.children.size(); ++j ) { + const auto itChild = skinnedNodes.find( node.children[j] ); + if ( itChild != skinnedNodes.end() ) { + edgeList.emplace_back( std::pair { + component.m_name, nodeNumToComponentName.at( node.children[j] ) } ); + } // TODO, if a non skinned subgraph is attached to the node, add an edge for it ? + else { +#ifdef BE_VERBOSE + LOG( logINFO ) << "GLTF Skeleton : buildSkeletonTopology found non skinned " + "subgraph between " + << component.m_name << " and " + << graphNodes[node.children[j]].m_nodeName << "!"; + + // edgeList.emplace_back( std::pair { + // component.m_name, graphNodes[node.children[j]].m_nodeName} ); + +#endif + } + } + } + + AlignedStdVector edge; + edge.reserve( edgeList.size() ); + std::transform( edgeList.cbegin(), + edgeList.cend(), + std::back_inserter( edge ), + [handle]( auto p ) -> Vector2ui { + return { handle->getIndexOf( p.first ), handle->getIndexOf( p.second ) }; + } ); + + handle->setEdges( edge ); +} + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/GLTFConverter/HandleData.hpp b/src/IO/Gltf/internal/GLTFConverter/HandleData.hpp new file mode 100644 index 00000000000..ece371abbe8 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/HandleData.hpp @@ -0,0 +1,81 @@ +#pragma once +#include +#include +#include +#include + +#include + +#include +#include + +namespace Ra::Core::Asset { +class FileData; +} + +namespace Ra { +namespace IO { +namespace GLTF { + +/** + * Functions to load handle data from a json GLTF file + * + */ +class HandleDataLoader +{ + public: + using IntToString = std::map; + using StringToInt = std::map; + + /** + * Load and build the skeleton and skinning weights for a given skin. + * @param gltfscene The json representation of the gltf scene + * @param graphNodes The array of active nodes + * @param visitedNodes The set of node index for active nodes + * @param skin The gltf skeleton definition + * @param nodeNumToComponentName the mapping between node numbers and component names + * @param skeleton_num The number of the loaded skeleton + * @return The Radium representation of the skeleton with nodes weight matrices + * @todo verify the bindmatrices of nodes and make attached subgraph to follow the skeleton + * animation + */ + static Ra::Core::Asset::HandleData* loadSkeleton( const fx::gltf::Document& gltfScene, + const std::vector& graphNodes, + const std::set& visitedNodes, + const fx::gltf::Skin& skin, + size_t skinIndex, + IntToString& nodeNumToComponentName ); + + private: + static Ra::Core::AlignedStdVector + buildJoints( const fx::gltf::Document& gltfScene, + const std::vector& graphNodes, + const fx::gltf::Skin& skin, + IntToString& nodeNumToComponentName, + StringToInt& componentNameToNodeNum ); + + static std::vector getBindMatrices( const fx::gltf::Document& gltfScene, + const fx::gltf::Skin& skin ); + + static std::string getJointName( const fx::gltf::Document& gltfScene, + IntToString& nodeNumToComponentName, + int32_t nodeNum ); + + static void addMeshesWeightsAndBindMatrices( + const fx::gltf::Document& gltfScene, + const SceneNode& graphNode, + int32_t nodeNum, + std::vector& bindMatrices, + std::map>>>& jointsWeights, + std::map>& jointsMatrices ); + + static void buildSkeletonTopology( const std::vector& graphNodes, + const IntToString& nodeNumToComponentName, + const StringToInt& componentNameToNodeNum, + const std::set& insertedNodes, + Ra::Core::Asset::HandleData* handle ); +}; + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/GLTFConverter/ImageData.hpp b/src/IO/Gltf/internal/GLTFConverter/ImageData.hpp new file mode 100644 index 00000000000..3ca256027be --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/ImageData.hpp @@ -0,0 +1,73 @@ +#pragma once +#include + +namespace Ra { +namespace IO { +namespace GLTF { + +/** + * Represent an image extracted from json file. + */ +class ImageData +{ + public: + /** + * Representation of an image. + * This representation is either the filename for external images of the image extracted from a + * binary stream. + * + * @todo, support binary stream + */ + struct ImageInfo { + std::string FileName {}; + + uint32_t BinarySize {}; + uint8_t const* BinaryData {}; + + [[nodiscard]] bool IsBinary() const noexcept { return BinaryData != nullptr; } + }; + + /** + * Constructor of the image data + * @param doc the json document the image must be extracted from + * @param textureIndex The index of the image in the json representation + * @param modelPath the base directory of the json scenes + */ + ImageData( fx::gltf::Document const& doc, int32_t textureIndex, std::string const& modelPath ) { + fx::gltf::Image const& image = doc.images[doc.textures[textureIndex].source]; + + bool isEmbedded = image.IsEmbeddedResource(); + if ( !image.uri.empty() && !isEmbedded ) { + m_info.FileName = fx::gltf::detail::GetDocumentRootPath( modelPath ) + "/" + image.uri; + } + else { + if ( isEmbedded ) { + image.MaterializeData( m_embeddedData ); + m_info.BinaryData = &m_embeddedData[0]; + m_info.BinarySize = static_cast( m_embeddedData.size() ); + } + else { + fx::gltf::BufferView const& bufferView = doc.bufferViews[image.bufferView]; + fx::gltf::Buffer const& buffer = doc.buffers[bufferView.buffer]; + + m_info.BinaryData = &buffer.data[bufferView.byteOffset]; + m_info.BinarySize = bufferView.byteLength; + } + } + } + + /** + * Access to the ImageInfo structure extracted from the json file + * @return + */ + [[nodiscard]] ImageInfo const& Info() const noexcept { return m_info; } + + private: + ImageInfo m_info {}; + + std::vector m_embeddedData {}; +}; + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/GLTFConverter/MaterialConverter.cpp b/src/IO/Gltf/internal/GLTFConverter/MaterialConverter.cpp new file mode 100644 index 00000000000..6f0f5677639 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/MaterialConverter.cpp @@ -0,0 +1,600 @@ +#include +#include +#include + +#include +#include + +#include + +using namespace fx; + +namespace Ra { +namespace IO { +namespace GLTF { +using namespace Ra::Core::Asset; +using namespace Ra::Core::Utils; +using namespace Ra::Core::Material; + +GLTFSampler convertSampler( const fx::gltf::Sampler& sampler ) { + GLTFSampler rasampler; + switch ( sampler.magFilter ) { + case fx::gltf::Sampler::MagFilter::Nearest: + rasampler.magFilter = GLTFSampler::MagFilter::Nearest; + break; + case fx::gltf::Sampler::MagFilter::Linear: + rasampler.magFilter = GLTFSampler::MagFilter::Linear; + break; + default: + rasampler.magFilter = GLTFSampler::MagFilter::Nearest; + break; + } + switch ( sampler.minFilter ) { + case fx::gltf::Sampler::MinFilter::Nearest: + rasampler.minFilter = GLTFSampler::MinFilter::Nearest; + break; + case fx::gltf::Sampler::MinFilter::Linear: + rasampler.minFilter = GLTFSampler::MinFilter::Linear; + break; + case fx::gltf::Sampler::MinFilter::NearestMipMapNearest: + rasampler.minFilter = GLTFSampler::MinFilter::NearestMipMapNearest; + break; + case fx::gltf::Sampler::MinFilter::LinearMipMapNearest: + rasampler.minFilter = GLTFSampler::MinFilter::LinearMipMapNearest; + break; + case fx::gltf::Sampler::MinFilter::NearestMipMapLinear: + rasampler.minFilter = GLTFSampler::MinFilter::NearestMipMapLinear; + break; + case fx::gltf::Sampler::MinFilter::LinearMipMapLinear: + rasampler.minFilter = GLTFSampler::MinFilter::LinearMipMapLinear; + break; + default: + rasampler.minFilter = GLTFSampler::MinFilter::Nearest; + break; + } + switch ( sampler.wrapS ) { + case fx::gltf::Sampler::WrappingMode::ClampToEdge: + rasampler.wrapS = GLTFSampler::WrappingMode ::ClampToEdge; + break; + case fx::gltf::Sampler::WrappingMode::MirroredRepeat: + rasampler.wrapS = GLTFSampler::WrappingMode ::MirroredRepeat; + break; + case fx::gltf::Sampler::WrappingMode::Repeat: + rasampler.wrapS = GLTFSampler::WrappingMode ::Repeat; + break; + } + switch ( sampler.wrapT ) { + case fx::gltf::Sampler::WrappingMode::ClampToEdge: + rasampler.wrapT = GLTFSampler::WrappingMode ::ClampToEdge; + break; + case fx::gltf::Sampler::WrappingMode::MirroredRepeat: + rasampler.wrapT = GLTFSampler::WrappingMode ::MirroredRepeat; + break; + case fx::gltf::Sampler::WrappingMode::Repeat: + rasampler.wrapT = GLTFSampler::WrappingMode ::Repeat; + break; + } + + return rasampler; +} + +void getMaterialExtensions( const nlohmann::json& extensionsAndExtras, BaseGLTFMaterial* mat ) { + // Manage non standard material extensions +#if 0 + if ( !extensionsAndExtras.empty() ) { + auto ext = extensionsAndExtras.find( "extensions" ); + if ( ext != extensionsAndExtras.end() ) { + auto extensions = *ext; + const nlohmann::json::const_iterator iter = extensions.find( "INN_material_atlas_V1" ); + if ( iter != extensions.end() ) { + mat->m_inn_materialAtlas = new gltf_INNMaterialAtlas; + from_json( *iter, *( mat->m_inn_materialAtlas ) ); + } + } + } +#endif +} + +void getMaterialTextureTransform( const nlohmann::json& extensionsAndExtras, + std::unique_ptr& dest ) { + if ( !extensionsAndExtras.empty() ) { + auto ext = extensionsAndExtras.find( "extensions" ); + if ( ext != extensionsAndExtras.end() ) { + nlohmann::json textureExtensions = *ext; + nlohmann::json::const_iterator iter = textureExtensions.find( "KHR_texture_transform" ); + if ( iter != textureExtensions.end() ) { + gltf_KHRTextureTransform textTransform; + from_json( *iter, textTransform ); + dest = std::make_unique(); + dest->offset[0] = textTransform.offset[0]; + dest->offset[1] = textTransform.offset[1]; + dest->scale[0] = textTransform.scale[0]; + dest->scale[1] = textTransform.scale[1]; + dest->rotation = textTransform.rotation; + dest->texCoord = textTransform.texCoord; + } + } + } +} + +void getCommonMaterialParameters( const gltf::Document& doc, + const std::string& filePath, + const fx::gltf::Material& gltfMaterial, + BaseGLTFMaterial* mat ) { + // Normal texture + if ( !gltfMaterial.normalTexture.empty() ) { + ImageData tex { doc, gltfMaterial.normalTexture.index, filePath }; + if ( !tex.Info().IsBinary() ) { + mat->m_normalTexture = tex.Info().FileName; + mat->m_normalTextureScale = gltfMaterial.normalTexture.scale; + mat->m_hasNormalTexture = true; + } + else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } + // get sampler information for this texture + int samplerIndex = doc.textures[gltfMaterial.normalTexture.index].sampler; + if ( samplerIndex >= 0 ) { + mat->m_normalSampler = convertSampler( doc.samplers[samplerIndex] ); + } + getMaterialTextureTransform( gltfMaterial.normalTexture.extensionsAndExtras, + mat->m_normalTextureTransform ); + } + // Occlusion texture + if ( !gltfMaterial.occlusionTexture.empty() ) { + ImageData tex { doc, gltfMaterial.occlusionTexture.index, filePath }; + if ( !tex.Info().IsBinary() ) { + mat->m_occlusionTexture = tex.Info().FileName; + mat->m_occlusionStrength = gltfMaterial.occlusionTexture.strength; + mat->m_hasOcclusionTexture = true; + } + else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } + // get sampler information for this texture + int samplerIndex = doc.textures[gltfMaterial.occlusionTexture.index].sampler; + if ( samplerIndex >= 0 ) { + mat->m_occlusionSampler = convertSampler( doc.samplers[samplerIndex] ); + } + getMaterialTextureTransform( gltfMaterial.occlusionTexture.extensionsAndExtras, + mat->m_occlusionTextureTransform ); + } + // Emmissive Component + mat->m_emissiveFactor = { gltfMaterial.emissiveFactor[0], + gltfMaterial.emissiveFactor[1], + gltfMaterial.emissiveFactor[2], + 1_ra }; + if ( !gltfMaterial.emissiveTexture.empty() ) { + ImageData tex { doc, gltfMaterial.emissiveTexture.index, filePath }; + if ( !tex.Info().IsBinary() ) { + mat->m_emissiveTexture = tex.Info().FileName; + mat->m_hasEmissiveTexture = true; + } + else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } + // get sampler information for this texture + int samplerIndex = doc.textures[gltfMaterial.emissiveTexture.index].sampler; + if ( samplerIndex >= 0 ) { + mat->m_emissiveSampler = convertSampler( doc.samplers[samplerIndex] ); + } + getMaterialTextureTransform( gltfMaterial.emissiveTexture.extensionsAndExtras, + mat->m_emissiveTextureTransform ); + } + mat->m_alphaMode = static_cast( gltfMaterial.alphaMode ); + mat->m_doubleSided = gltfMaterial.doubleSided; + mat->m_alphaCutoff = gltfMaterial.alphaCutoff; + + // load supported material extensions + getMaterialExtensions( gltfMaterial.extensionsAndExtras, mat ); +} + +std::map( const gltf::Document& doc, + const std::string& filePath, + const nlohmann::json& jsonData, + const std::string& basename )>> + instanciateExtension { + { "KHR_materials_ior", + []( const gltf::Document& doc, + const std::string& filePath, + const nlohmann::json& jsonData, + const std::string& basename ) { + gltf_KHRMaterialsIor data; + from_json( jsonData, data ); + auto built = std::make_unique( basename + " - IOR" ); + built->m_ior = data.ior; + return std::move( built ); + } }, + { "KHR_materials_clearcoat", + []( const gltf::Document& doc, + const std::string& filePath, + const nlohmann::json& jsonData, + const std::string& basename ) { + gltf_KHRMaterialsClearcoat data; + from_json( jsonData, data ); + auto built = std::make_unique( basename + " - Clearcoat layer" ); + // clearcoat layer + built->m_clearcoatFactor = data.clearcoatFactor; + if ( !data.clearcoatTexture.empty() ) { + ImageData tex { doc, data.clearcoatTexture.index, filePath }; + if ( !tex.Info().IsBinary() ) { + built->m_clearcoatTexture = tex.Info().FileName; + built->m_hasClearcoatTexture = true; + } + else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } + // get sampler information for this texture + int samplerIndex = doc.textures[data.clearcoatTexture.index].sampler; + if ( samplerIndex >= 0 ) { + built->m_clearcoatSampler = convertSampler( doc.samplers[samplerIndex] ); + } + getMaterialTextureTransform( data.clearcoatTexture.extensionsAndExtras, + built->m_clearcoatTextureTransform ); + } + // clearcoat roughness + built->m_clearcoatRoughnessFactor = data.clearcoatRoughnessFactor; + if ( !data.clearcoatRoughnessTexture.empty() ) { + ImageData tex { doc, data.clearcoatRoughnessTexture.index, filePath }; + if ( !tex.Info().IsBinary() ) { + built->m_clearcoatRoughnessTexture = tex.Info().FileName; + built->m_hasClearcoatRoughnessTexture = true; + } + else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } + // get sampler information for this texture + int samplerIndex = doc.textures[data.clearcoatRoughnessTexture.index].sampler; + if ( samplerIndex >= 0 ) { + built->m_clearcoatRoughnessSampler = + convertSampler( doc.samplers[samplerIndex] ); + } + getMaterialTextureTransform( data.clearcoatRoughnessTexture.extensionsAndExtras, + built->m_clearcoatRoughnessTextureTransform ); + } + // clearcoat Normal texture + if ( !data.clearcoatNormalTexture.empty() ) { + ImageData tex { doc, data.clearcoatNormalTexture.index, filePath }; + if ( !tex.Info().IsBinary() ) { + built->m_clearcoatNormalTexture = tex.Info().FileName; + built->m_clearcoatNormalTextureScale = data.clearcoatNormalTexture.scale; + built->m_hasClearcoatNormalTexture = true; + } + else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } + // get sampler information for this texture + int samplerIndex = doc.textures[data.clearcoatNormalTexture.index].sampler; + if ( samplerIndex >= 0 ) { + built->m_clearcoatNormalSampler = + convertSampler( doc.samplers[samplerIndex] ); + } + getMaterialTextureTransform( data.clearcoatNormalTexture.extensionsAndExtras, + built->m_clearcoatNormalTextureTransform ); + } + return std::move( built ); + } }, + { "KHR_materials_specular", + []( const gltf::Document& doc, + const std::string& filePath, + const nlohmann::json& jsonData, + const std::string& basename ) { + gltf_KHRMaterialsSpecular data; + from_json( jsonData, data ); + auto built = std::make_unique( basename + " - Specular layer" ); + // spacular strength + built->m_specularFactor = data.specularFactor; + if ( !data.specularTexture.empty() ) { + ImageData tex { doc, data.specularTexture.index, filePath }; + if ( !tex.Info().IsBinary() ) { + built->m_specularTexture = tex.Info().FileName; + built->m_hasSpecularTexture = true; + } + else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } + // get sampler information for this texture + int samplerIndex = doc.textures[data.specularTexture.index].sampler; + if ( samplerIndex >= 0 ) { + built->m_specularSampler = convertSampler( doc.samplers[samplerIndex] ); + } + getMaterialTextureTransform( data.specularTexture.extensionsAndExtras, + built->m_specularTextureTransform ); + } + // specular color + built->m_specularColorFactor = Ra::Core::Utils::Color { data.specularColorFactor[0], + data.specularColorFactor[1], + data.specularColorFactor[2], + 1_ra }; + if ( !data.specularColorTexture.empty() ) { + ImageData tex { doc, data.specularColorTexture.index, filePath }; + if ( !tex.Info().IsBinary() ) { + built->m_specularColorTexture = tex.Info().FileName; + built->m_hasSpecularColorTexture = true; + } + else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } + // get sampler information for this texture + int samplerIndex = doc.textures[data.specularColorTexture.index].sampler; + if ( samplerIndex >= 0 ) { + built->m_specularColorSampler = convertSampler( doc.samplers[samplerIndex] ); + } + getMaterialTextureTransform( data.specularColorTexture.extensionsAndExtras, + built->m_specularColorTextureTransform ); + } + return std::move( built ); + } }, + { "KHR_materials_sheen", + []( const gltf::Document& doc, + const std::string& filePath, + const nlohmann::json& jsonData, + const std::string& basename ) { + gltf_KHRMaterialsSheen data; + from_json( jsonData, data ); + auto built = std::make_unique( basename + " - Sheen layer" ); + // Sheen color. + built->m_sheenColorFactor = Ra::Core::Utils::Color { data.sheenColorFactor[0], + data.sheenColorFactor[1], + data.sheenColorFactor[2], + 1_ra }; + if ( !data.sheenColorTexture.empty() ) { + ImageData tex { doc, data.sheenColorTexture.index, filePath }; + if ( !tex.Info().IsBinary() ) { + built->m_sheenColorTexture = tex.Info().FileName; + built->m_hasSheenColorTexture = true; + } + else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } + // get sampler information for this texture + int samplerIndex = doc.textures[data.sheenColorTexture.index].sampler; + if ( samplerIndex >= 0 ) { + built->m_sheenColorTextureSampler = + convertSampler( doc.samplers[samplerIndex] ); + } + getMaterialTextureTransform( data.sheenColorTexture.extensionsAndExtras, + built->m_sheenColorTextureTransform ); + } + // Sheen roughness + built->m_sheenRoughnessFactor = data.sheenRoughnessFactor; + if ( !data.sheenRoughnessTexture.empty() ) { + ImageData tex { doc, data.sheenRoughnessTexture.index, filePath }; + if ( !tex.Info().IsBinary() ) { + built->m_sheenRoughnessTexture = tex.Info().FileName; + built->m_hasSheenRoughnessTexture = true; + } + else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } + // get sampler information for this texture + int samplerIndex = doc.textures[data.sheenRoughnessTexture.index].sampler; + if ( samplerIndex >= 0 ) { + built->m_sheenRoughnessTextureSampler = + convertSampler( doc.samplers[samplerIndex] ); + } + getMaterialTextureTransform( data.sheenRoughnessTexture.extensionsAndExtras, + built->m_sheenRoughnessTextureTransform ); + } + return std::move( built ); + } } }; + +void getMaterialExtensions( const gltf::Document& doc, + const std::string& filePath, + const MaterialData& meshMaterial, + BaseGLTFMaterial* mat, + const std::vector exept = {} ) { + auto extensionsAndExtras = meshMaterial.Data().extensionsAndExtras; + if ( !extensionsAndExtras.empty() ) { + auto extensions = extensionsAndExtras.find( "extensions" ); + if ( extensions != extensionsAndExtras.end() ) { + // first search for unlit extension becaus it will prevent the use of other extensions + // (Specification of 12/2021) + // https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_unlit + // Here, according to "Implementation Note: When KHR_materials_unlit is included with + // another extension specifying a shading model on the same material, the result is + // undefined", we choose to activate only the unlit extension + if ( extensions->find( "KHR_materials_unlit" ) != extensions->end() ) { + mat->prohibitAllExtensions(); + mat->allowExtension( "KHR_materials_unlit" ); + } + // load supported extensions + auto& extensionList = *extensions; + for ( auto& [key, value] : extensionList.items() ) { + if ( std::any_of( exept.begin(), exept.end(), [k = key]( const auto& e ) { + return e == k; + } ) ) { + continue; + } + if ( mat->supportExtension( key ) ) { + mat->m_extensions[key] = + instanciateExtension[key]( doc, filePath, value, mat->getName() ); + } + else { + LOG( logINFO ) << "Extension " << key << " is NOT allowed for " + << mat->getType() << std::endl; + } + } + } + } +} + +Ra::Core::Asset::MaterialData* buildDefaultMaterial( const gltf::Document& doc, + int32_t meshIndex, + const std::string& filePath, + int32_t meshPartNumber, + const MaterialData& meshMaterial ) { + auto gltfMaterial = meshMaterial.Data(); + std::string materialName { gltfMaterial.name }; + if ( materialName.empty() ) { materialName = "material_"; } + materialName += std::to_string( meshIndex ) + "_p_" + std::to_string( meshPartNumber ); + auto mat = new MetallicRoughnessData( materialName ); + getCommonMaterialParameters( doc, filePath, gltfMaterial, mat ); + getMaterialExtensions( doc, filePath, meshMaterial, mat ); + return mat; +} + +Ra::Core::Asset::MaterialData* buildMetallicRoughnessMaterial( const gltf::Document& doc, + int32_t meshIndex, + const std::string& filePath, + int32_t meshPartNumber, + const MaterialData& meshMaterial ) { + auto gltfMaterial = meshMaterial.Data(); + std::string materialName { gltfMaterial.name }; + if ( materialName.empty() ) { + materialName = "material_"; + materialName += std::to_string( meshIndex ) + "_p_" + std::to_string( meshPartNumber ); + } + + auto mat = new MetallicRoughnessData( materialName ); + + getCommonMaterialParameters( doc, filePath, gltfMaterial, mat ); + + // Base color Component + mat->m_baseColorFactor = { gltfMaterial.pbrMetallicRoughness.baseColorFactor[0], + gltfMaterial.pbrMetallicRoughness.baseColorFactor[1], + gltfMaterial.pbrMetallicRoughness.baseColorFactor[2], + gltfMaterial.pbrMetallicRoughness.baseColorFactor[3] }; + if ( !gltfMaterial.pbrMetallicRoughness.baseColorTexture.empty() ) { + ImageData tex { doc, gltfMaterial.pbrMetallicRoughness.baseColorTexture.index, filePath }; + if ( !tex.Info().IsBinary() ) { + mat->m_baseColorTexture = tex.Info().FileName; + mat->m_hasBaseColorTexture = true; + } + else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } + // get sampler information for this texture + int samplerIndex = + doc.textures[gltfMaterial.pbrMetallicRoughness.baseColorTexture.index].sampler; + if ( samplerIndex >= 0 ) { + mat->m_baseSampler = convertSampler( doc.samplers[samplerIndex] ); + } + getMaterialTextureTransform( + gltfMaterial.pbrMetallicRoughness.baseColorTexture.extensionsAndExtras, + mat->m_baseTextureTransform ); + } + + // Metalic-Roughness Component + mat->m_metallicFactor = gltfMaterial.pbrMetallicRoughness.metallicFactor; + mat->m_roughnessFactor = gltfMaterial.pbrMetallicRoughness.roughnessFactor; + if ( !gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.empty() ) { + ImageData tex { + doc, gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index, filePath }; + if ( !tex.Info().IsBinary() ) { + mat->m_metallicRoughnessTexture = tex.Info().FileName; + mat->m_hasMetallicRoughnessTexture = true; + } + else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } + // get sampler information for this texture + int samplerIndex = + doc.textures[gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.index].sampler; + if ( samplerIndex >= 0 ) { + mat->m_metallicRoughnessSampler = convertSampler( doc.samplers[samplerIndex] ); + } + getMaterialTextureTransform( + gltfMaterial.pbrMetallicRoughness.metallicRoughnessTexture.extensionsAndExtras, + mat->m_metallicRoughnessTextureTransform ); + } + + getMaterialExtensions( doc, filePath, meshMaterial, mat ); + return mat; +} + +Ra::Core::Asset::MaterialData* +buildSpecularGlossinessMaterial( const gltf::Document& doc, + int32_t meshIndex, + const std::string& filePath, + int32_t meshPartNumber, + const MaterialData& meshMaterial, + const gltf_PBRSpecularGlossiness& specularGloss ) { + auto gltfMaterial = meshMaterial.Data(); + std::string materialName { gltfMaterial.name }; + if ( materialName.empty() ) { materialName = "material_"; } + materialName += std::to_string( meshIndex ) + "_p_" + std::to_string( meshPartNumber ); + + auto mat = new SpecularGlossinessData( materialName ); + + getCommonMaterialParameters( doc, filePath, gltfMaterial, mat ); + + // Diffuse color Component + mat->m_diffuseFactor = { specularGloss.diffuseFactor[0], + specularGloss.diffuseFactor[1], + specularGloss.diffuseFactor[2], + specularGloss.diffuseFactor[3] }; + + if ( !specularGloss.diffuseTexture.empty() ) { + ImageData tex { doc, specularGloss.diffuseTexture.index, filePath }; + if ( !tex.Info().IsBinary() ) { + mat->m_diffuseTexture = tex.Info().FileName; + mat->m_hasDiffuseTexture = true; + } + else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } + // get sampler information for this texture + int samplerIndex = doc.textures[specularGloss.diffuseTexture.index].sampler; + if ( samplerIndex >= 0 ) { + mat->m_diffuseSampler = convertSampler( doc.samplers[samplerIndex] ); + } + getMaterialTextureTransform( specularGloss.diffuseTexture.extensionsAndExtras, + mat->m_diffuseTextureTransform ); + } + + // Specular-glossiness Component + mat->m_glossinessFactor = specularGloss.glossinessFactor; + mat->m_specularFactor = { specularGloss.specularFactor[0], + specularGloss.specularFactor[1], + specularGloss.specularFactor[2], + 1_ra }; + if ( !specularGloss.specularGlossinessTexture.empty() ) { + ImageData tex { doc, specularGloss.specularGlossinessTexture.index, filePath }; + if ( !tex.Info().IsBinary() ) { + mat->m_specularGlossinessTexture = tex.Info().FileName; + mat->m_hasSpecularGlossinessTexture = true; + } + else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } + // get sampler information for this texture + int samplerIndex = doc.textures[specularGloss.specularGlossinessTexture.index].sampler; + if ( samplerIndex >= 0 ) { + mat->m_specularGlossinessSampler = convertSampler( doc.samplers[samplerIndex] ); + } + getMaterialTextureTransform( specularGloss.specularGlossinessTexture.extensionsAndExtras, + mat->m_specularGlossinessTransform ); + } + + getMaterialExtensions( + doc, filePath, meshMaterial, mat, { "KHR_materials_pbrSpecularGlossiness" } ); + return mat; +} + +Ra::Core::Asset::MaterialData* buildMaterial( const gltf::Document& doc, + int32_t meshIndex, + const std::string& filePath, + int32_t meshPartNumber, + const MaterialData& meshMaterial ) { +#ifdef LEGACY_IMPLEMENTATION + if ( meshMaterial.isMetallicRoughness() ) { + return buildMetallicRoughnessMaterial( + doc, meshIndex, filePath, meshPartNumber, meshMaterial ); + } + else { + // Check if extension "KHR_materials_pbrSpecularGlossiness" is available + auto extensionsAndExtras = meshMaterial.Data().extensionsAndExtras; + if ( !extensionsAndExtras.empty() ) { + auto extensions = extensionsAndExtras.find( "extensions" ); + if ( extensions != extensionsAndExtras.end() ) { + auto iter = extensions->find( "KHR_materials_pbrSpecularGlossiness" ); + if ( iter != extensions->end() ) { + gltf_PBRSpecularGlossiness gltfMaterial; + from_json( *iter, gltfMaterial ); + return buildSpecularGlossinessMaterial( + doc, meshIndex, filePath, meshPartNumber, meshMaterial, gltfMaterial ); + } + } + } + /// TODO : generate a default MetallicRoughness with the base parameters + return buildDefaultMaterial( doc, meshIndex, filePath, meshPartNumber, meshMaterial ); + } +#else + if ( meshMaterial.isSpecularGlossiness() ) { + auto extensions = meshMaterial.Data().extensionsAndExtras.find( "extensions" ); + auto iter = extensions->find( "KHR_materials_pbrSpecularGlossiness" ); + gltf_PBRSpecularGlossiness gltfMaterial; + from_json( *iter, gltfMaterial ); + return buildSpecularGlossinessMaterial( + doc, meshIndex, filePath, meshPartNumber, meshMaterial, gltfMaterial ); + } + else { + if ( meshMaterial.hasData() ) { + return buildMetallicRoughnessMaterial( + doc, meshIndex, filePath, meshPartNumber, meshMaterial ); + } + else { + /// generate a default MetallicRoughness with the base parameters + return buildDefaultMaterial( doc, meshIndex, filePath, meshPartNumber, meshMaterial ); + } + } +#endif +} + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/GLTFConverter/MaterialConverter.hpp b/src/IO/Gltf/internal/GLTFConverter/MaterialConverter.hpp new file mode 100644 index 00000000000..6ed2eef63b2 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/MaterialConverter.hpp @@ -0,0 +1,77 @@ +#pragma once +#include + +namespace Ra::Core::Asset { +class MaterialData; +} // namespace Ra::Core::Asset + +namespace Ra { +namespace IO { +namespace GLTF { + +/** + * Representation of the material extracted from a json gltf scene + */ +class MaterialData +{ + public: + /** + * constructor + */ + MaterialData() = default; + + /** + * Initialize the data from a json node + * @param material + */ + void SetData( fx::gltf::Material const& material ) { + m_material = material; + m_hasData = true; + } + + /** + * Access to the data + * @return + */ + [[nodiscard]] fx::gltf::Material const& Data() const noexcept { return m_material; } + +#ifdef LEGACY_IMPLEMENTATION + /*** + * Test if the data are valid + * @return + */ + [[nodiscard]] bool isMetallicRoughness() const noexcept { + return m_hasData && !m_material.pbrMetallicRoughness.empty(); + } +#else + /** + * Test if material is specularGlossiness + */ + [[nodiscard]] bool isSpecularGlossiness() const noexcept { + auto extensionsAndExtras = Data().extensionsAndExtras; + if ( !extensionsAndExtras.empty() ) { + auto extensions = extensionsAndExtras.find( "extensions" ); + if ( extensions != extensionsAndExtras.end() ) { + auto iter = extensions->find( "KHR_materials_pbrSpecularGlossiness" ); + if ( iter != extensions->end() ) { return true; } + } + } + return false; + } + + [[nodiscard]] bool hasData() const noexcept { return m_hasData; } +#endif + private: + fx::gltf::Material m_material {}; + bool m_hasData {}; +}; + +Ra::Core::Asset::MaterialData* buildMaterial( const fx::gltf::Document& doc, + int32_t meshIndex, + const std::string& filePath, + int32_t meshPartNumber, + const MaterialData& meshMaterial ); + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp b/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp new file mode 100644 index 00000000000..4becfb109c4 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp @@ -0,0 +1,264 @@ +#include +#include +#include + +using namespace fx; + +namespace Ra { +namespace IO { +namespace GLTF { + +using namespace Ra::Core; +using namespace Ra::Core::Asset; +using namespace Ra::Core::Utils; + +// used to convert position and normals and ... +template +void convertVectors( Vector3Array& vectors, const void* data, int count ) { + auto mem = reinterpret_cast( data ); + for ( int i = 0; i < count; ++i ) { + vectors.emplace_back( mem[3 * i], mem[3 * i + 1], mem[3 * i + 2] ); + } +} + +// GLTF texCoord are vec2 +// Warning : textCoord could be normalized integers +template +void convertTexCoord( Vector3Array& vectors, const void* data, int count ) { + auto mem = reinterpret_cast( data ); + for ( int i = 0; i < count; ++i ) { + float u = float( mem[2 * i] ) / std::numeric_limits::max(); + float v = 1 - float( mem[2 * i + 1] ) / std::numeric_limits::max(); + vectors.emplace_back( u, v, 0 ); + } +} + +template <> +void convertTexCoord( Vector3Array& vectors, const void* data, int count ) { + auto mem = reinterpret_cast( data ); + for ( int i = 0; i < count; ++i ) { + vectors.emplace_back( mem[2 * i], 1 - mem[2 * i + 1], 0 ); + } +} + +// GLTF tangents are vec4 with the last component indicating handedness. +// Multiply the tangent coordinates by the handedness to have always right handed local frame +// Must verify this +template +void convertTangents( Vector3Array& vectors, const void* data, int count ) { + auto mem = reinterpret_cast( data ); + for ( int i = 0; i < count; ++i ) { + vectors.emplace_back( mem[4 * i] * mem[4 * i + 3], + mem[4 * i + 1] * mem[4 * i + 3], + mem[4 * i + 2] * mem[4 * i + 3] ); + } +} + +// used to convert face indices +template +void convertIndices( Vector3uArray& indices, const uint8_t* data, uint32_t count ) { + auto mem = reinterpret_cast( data ); + for ( uint32_t i = 0; i < count; ++i ) { + indices.push_back( { mem[3 * i], mem[3 * i + 1], mem[3 * i + 2] } ); + } +} + +std::vector> buildMesh( const gltf::Document& doc, + int32_t meshIndex, + const std::string& filePath, + int32_t nodeNum ) { + std::vector> meshParts; + for ( int32_t meshPartNumber = 0; meshPartNumber < doc.meshes[meshIndex].primitives.size(); + ++meshPartNumber ) { + MeshData mesh { doc, meshIndex, meshPartNumber }; + if ( mesh.mode() != fx::gltf::Primitive::Mode::Triangles ) { + LOG( logERROR ) + << "GLTF buildMesh -- RadiumGLTF only supports Triangles primitive right now !"; + continue; + } + const MeshData::BufferInfo& vBuffer = mesh.VertexBuffer(); + const MeshData::BufferInfo& nBuffer = mesh.NormalBuffer(); + const MeshData::BufferInfo& tBuffer = mesh.TangentBuffer(); + const MeshData::BufferInfo& cBuffer = mesh.TexCoord0Buffer(); + const MeshData::BufferInfo& iBuffer = mesh.IndexBuffer(); + + // we need at least vertices to render an object + if ( vBuffer.HasData() ) { + std::string meshName = doc.meshes[meshIndex].name; + if ( meshName.empty() ) { + meshName = "mesh_"; + meshName += "_n_" + std::to_string( nodeNum ) + "_m_" + + std::to_string( meshIndex ) + "_p_" + std::to_string( meshPartNumber ); + } + auto nameIsNew = MeshNameCache::addName( meshName ); + if ( !nameIsNew.second ) { + meshName += "_" + std::to_string( meshIndex ); + MeshNameCache::addName( meshName ); + } + + auto meshPart = + std::make_unique( meshName, GeometryData::GeometryType::TRI_MESH ); + // Convert vertices + if ( ( vBuffer.Accessor->type != gltf::Accessor::Type::Vec3 ) || + ( vBuffer.Accessor->componentType != gltf::Accessor::ComponentType::Float ) ) { + LOG( logERROR ) << "GLTF buildMesh -- Vertices must be Vec3 of Float!"; + continue; + } + auto attribVertices = meshPart->getGeometry().addAttrib( + getAttribName( Ra::Core::Geometry::MeshAttrib::VERTEX_POSITION ) ); + auto& vertices = + meshPart->getGeometry().vertexAttribs().getDataWithLock( attribVertices ); + vertices.reserve( vBuffer.Accessor->count ); + convertVectors( vertices, vBuffer.Data, vBuffer.Accessor->count ); + meshPart->getGeometry().vertexAttribs().unlock( attribVertices ); + // Convert faces + if ( iBuffer.HasData() ) { + if ( iBuffer.Accessor->type != gltf::Accessor::Type::Scalar ) { + if ( iBuffer.Accessor->type == gltf::Accessor::Type::None ) { + LOG( logERROR ) << "GLTF buildMesh -- Indices must be Scalar !" + << static_cast( iBuffer.Accessor->type ); + continue; + } + } + else { + // Gltf only support triangle mesh right now + meshPart->setPrimitiveCount( iBuffer.Accessor->count / 3 ); + auto layer = std::make_unique(); + auto& indices = layer->collection(); + indices.reserve( meshPart->getPrimitiveCount() ); + switch ( iBuffer.Accessor->componentType ) { + case gltf::Accessor::ComponentType::UnsignedByte: + convertIndices( + indices, iBuffer.Data, meshPart->getPrimitiveCount() ); + break; + case gltf::Accessor::ComponentType::UnsignedShort: + convertIndices( + indices, iBuffer.Data, meshPart->getPrimitiveCount() ); + break; + case gltf::Accessor::ComponentType::UnsignedInt: + convertIndices( + indices, iBuffer.Data, meshPart->getPrimitiveCount() ); + break; + default: + LOG( logERROR ) << "GLTF buildMesh -- Indices must be UnsignedByte, " + "UnsignedShort or UnsignedInt !"; + continue; + } + meshPart->getGeometry().addLayer( std::move( layer ), false, "indices" ); + } + } + else { + meshPart->setPrimitiveCount( meshPart->getGeometry().vertices().size() / 3 ); + auto layer = std::make_unique(); + auto& indices = layer->collection(); + indices.reserve( meshPart->getPrimitiveCount() ); + for ( uint vi = 0; vi < meshPart->getPrimitiveCount(); ++vi ) { + indices.emplace_back( Vector3ui { 3 * vi, 3 * vi + 1, 3 * vi + 2 } ); + } + meshPart->getGeometry().addLayer( std::move( layer ), false, "indices" ); + } + // Convert or compute normals + if ( nBuffer.HasData() ) { + if ( ( nBuffer.Accessor->type != gltf::Accessor::Type::Vec3 ) || + ( nBuffer.Accessor->componentType != gltf::Accessor::ComponentType::Float ) ) { + LOG( logERROR ) << "GLTF buildMesh -- Normals must be Vec3 of Float!"; + continue; + } + auto attribHandle = meshPart->getGeometry().addAttrib( + getAttribName( Ra::Core::Geometry::MeshAttrib::VERTEX_NORMAL ) ); + auto& normals = + meshPart->getGeometry().vertexAttribs().getDataWithLock( attribHandle ); + normals.reserve( nBuffer.Accessor->count ); + convertVectors( normals, nBuffer.Data, nBuffer.Accessor->count ); + meshPart->getGeometry().vertexAttribs().unlock( attribHandle ); + } + else { + NormalCalculator nrmCalculator; + nrmCalculator( meshPart.get() ); + } + // Convert TexCoord if any + if ( cBuffer.HasData() ) { + if ( ( cBuffer.Accessor->type != gltf::Accessor::Type::Vec2 ) ) { + LOG( logERROR ) << "GLTF buildMesh -- TexCoord must be Vec2"; + continue; + } + auto attribHandle = meshPart->getGeometry().addAttrib( + getAttribName( Ra::Core::Geometry::MeshAttrib::VERTEX_TEXCOORD ) ); + auto& texcoords = + meshPart->getGeometry().vertexAttribs().getDataWithLock( attribHandle ); + texcoords.reserve( cBuffer.Accessor->count ); + switch ( cBuffer.Accessor->componentType ) { + case gltf::Accessor::ComponentType::UnsignedByte: + convertTexCoord( + texcoords, cBuffer.Data, cBuffer.Accessor->count ); + break; + case gltf::Accessor::ComponentType::UnsignedShort: + convertTexCoord( + texcoords, cBuffer.Data, cBuffer.Accessor->count ); + break; + case gltf::Accessor::ComponentType::Float: + convertTexCoord( texcoords, cBuffer.Data, cBuffer.Accessor->count ); + break; + default: + LOG( logERROR ) << "GLTF buildMesh -- texCoord must be UnsignedByte, " + "UnsignedShort or Float !"; + continue; + } + meshPart->getGeometry().vertexAttribs().unlock( attribHandle ); + } + else { LOG( logDEBUG ) << "GLTF buildMesh -- No texCoord provided. !"; } + // Convert tangent if any + if ( tBuffer.HasData() ) { + if ( ( tBuffer.Accessor->type != gltf::Accessor::Type::Vec4 ) || + ( tBuffer.Accessor->componentType != gltf::Accessor::ComponentType::Float ) ) { + LOG( logERROR ) << "GLTF buildMesh -- Tangents must be Vec4 of Float!"; + continue; + } + auto attribHandle = meshPart->getGeometry().addAttrib( + getAttribName( Ra::Core::Geometry::MeshAttrib::VERTEX_TANGENT ) ); + auto& tangents = + meshPart->getGeometry().vertexAttribs().getDataWithLock( attribHandle ); + tangents.reserve( tBuffer.Accessor->count ); + convertTangents( tangents, tBuffer.Data, tBuffer.Accessor->count ); + meshPart->getGeometry().vertexAttribs().unlock( attribHandle ); + } + else { + if ( cBuffer.HasData() ) { + // LOG(logINFO) << "GLTF buildMesh -- No tangents provided. + // Must computed tangents!"; + TangentCalculator tgtBuilder; + tgtBuilder( meshPart.get() ); + } + else { + LOG( logDEBUG ) << "GLTF buildMesh -- No tangents nor texcoords. Texture " + "mapping will be not correct!"; + } + } + // MATERIAL PART + meshPart->setMaterial( + buildMaterial( doc, meshIndex, filePath, meshPartNumber, mesh.Material() ) ); + + meshParts.emplace_back( std::move( meshPart ) ); + } + else { LOG( logERROR ) << "GLTF converter -- No vertices found, skip primitive."; } + } + return meshParts; +} + +std::set MeshNameCache::s_nameCache; + +void MeshNameCache::resetCache() { + s_nameCache.clear(); +} + +size_t MeshNameCache::cacheSize() { + return s_nameCache.size(); +} + +std::pair::iterator, bool> MeshNameCache::addName( const std::string& name ) { + return s_nameCache.insert( name ); +} + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp b/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp new file mode 100644 index 00000000000..507851693a2 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp @@ -0,0 +1,307 @@ +#pragma once +#include +#include +#include +#include + +#include + +#include + +namespace Ra::Core::Asset { +class GeometryData; +} // namespace Ra::Core::Asset + +namespace Ra { +namespace IO { +namespace GLTF { + +/** + * Representation of geometrical data extracted from a json GLTF file + */ +class MeshData +{ + public: + /** + * Buffer information about the geometric data + */ + struct BufferInfo { + /** + * The GLTF accesssor for the geometry component + */ + fx::gltf::Accessor const* Accessor { nullptr }; + + /** + * the raw data representing the geometry component + */ + uint8_t const* Data { nullptr }; + uint32_t DataStride { 0 }; + uint32_t TotalSize { 0 }; + + /** + * Test if the Mesh contains data + * @return + */ + [[nodiscard]] bool HasData() const noexcept { return Data != nullptr; } + }; + + /** + * Constructor from a json document + * @param doc the json document + * @param meshIndex the index of the mesh in the json document + * @param primitveIndex The index of the primitive description for the mesh + */ + MeshData( const fx::gltf::Document& doc, int32_t meshIndex, int32_t primitveIndex ) : + m_mode { doc.meshes[meshIndex].primitives[primitveIndex].mode } { + const fx::gltf::Mesh& mesh = doc.meshes[meshIndex]; + const fx::gltf::Primitive& primitive = mesh.primitives[primitveIndex]; + + for ( const auto& attrib : primitive.attributes ) { + if ( attrib.first == "POSITION" ) { + m_vertexBuffer = GetData( doc, doc.accessors[attrib.second] ); + } + else if ( attrib.first == "NORMAL" ) { + m_normalBuffer = GetData( doc, doc.accessors[attrib.second] ); + } + else if ( attrib.first == "TANGENT" ) { + m_tangentBuffer = GetData( doc, doc.accessors[attrib.second] ); + } + else if ( attrib.first == "TEXCOORD_0" ) { + m_texCoord0Buffer = GetData( doc, doc.accessors[attrib.second] ); + } + } + + if ( primitive.indices >= 0 ) { + m_indexBuffer = GetData( doc, doc.accessors[primitive.indices] ); + } + + if ( primitive.material >= 0 ) { + m_materialData.SetData( doc.materials[primitive.material] ); + } + } + + /** + * + * @return the face buffer of the mesh + */ + [[nodiscard]] const BufferInfo& IndexBuffer() const noexcept { return m_indexBuffer; } + + /** + * + * @return the vertex buffer of the mesh + */ + [[nodiscard]] const BufferInfo& VertexBuffer() const noexcept { return m_vertexBuffer; } + + /** + * + * @return the normal buffer of the mesh + */ + [[nodiscard]] const BufferInfo& NormalBuffer() const noexcept { return m_normalBuffer; } + + /** + * + * @return the tangent buffer of the mesh + */ + [[nodiscard]] const BufferInfo& TangentBuffer() const noexcept { return m_tangentBuffer; } + + /** + * + * @return the texcoord buffer of the mesh + */ + [[nodiscard]] const BufferInfo& TexCoord0Buffer() const noexcept { return m_texCoord0Buffer; } + + /** + * + * @return the mesh material data + */ + [[nodiscard]] const MaterialData& Material() const noexcept { return m_materialData; } + + /** + * + * @return the primitive type (triangle, points, ...) of the mesh + */ + fx::gltf::Primitive::Mode mode() { return m_mode; } + + /** + * + * @param doc the gltf document to read from + * @param accessor + * @return a vector of Vector4 joint indices + */ + static void GetJoints( Ra::Core::VectorArray& joints, + const fx::gltf::Document& doc, + const fx::gltf::Accessor& accessor ) { + MeshData::BufferInfo buf = MeshData::GetData( doc, accessor ); + if ( buf.HasData() ) { + if ( buf.Accessor->type != fx::gltf::Accessor::Type::Vec4 ) { + LOG( Ra::Core::Utils::logERROR ) + << "GLTF GetJoints -- Joint indices (JOINTS_*) must be Vec4 !" + << static_cast( buf.Accessor->type ); + } + else { + switch ( buf.Accessor->componentType ) { + case fx::gltf::Accessor::ComponentType::UnsignedByte: { + auto mem = buf.Data; + for ( uint32_t i = 0; i < buf.Accessor->count; ++i ) { + joints.push_back( Ra::Core::Vector4ui { + mem[4 * i], mem[4 * i + 1], mem[4 * i + 2], mem[4 * i + 3] } ); + } + break; + } + case fx::gltf::Accessor::ComponentType::UnsignedShort: { + auto mem = reinterpret_cast( buf.Data ); + for ( uint32_t i = 0; i < buf.Accessor->count; ++i ) { + joints.push_back( Ra::Core::Vector4ui { + mem[4 * i], mem[4 * i + 1], mem[4 * i + 2], mem[4 * i + 3] } ); + } + break; + } + default: + LOG( Ra::Core::Utils::logERROR ) + << "GLTF GetJoints -- Joint indices (JOINTS_*) must be " + << "UnsignedByte, UnsignedShort or UnsignedInt !"; + } + } + } + } + + /** + * + * @param doc the gltf document to read from + * @param accessor + * @return a vector of Vector4 weights + */ + static void GetWeights( Ra::Core::VectorArray& weights, + const fx::gltf::Document& doc, + const fx::gltf::Accessor& accessor ) { + MeshData::BufferInfo buf = MeshData::GetData( doc, accessor ); + if ( buf.HasData() ) { + if ( buf.Accessor->type != fx::gltf::Accessor::Type::Vec4 ) { + LOG( Ra::Core::Utils::logERROR ) << "GLTF GetWeights -- Weights must be Vec4 !" + << static_cast( buf.Accessor->type ); + } + else { + switch ( buf.Accessor->componentType ) { + case fx::gltf::Accessor::ComponentType::Float: { + auto mem = reinterpret_cast( buf.Data ); + for ( uint32_t i = 0; i < buf.Accessor->count; ++i ) { + weights.push_back( Ra::Core::Vector4f { + mem[4 * i], mem[4 * i + 1], mem[4 * i + 2], mem[4 * i + 3] } ); + } + break; + } + case fx::gltf::Accessor::ComponentType::UnsignedByte: { + auto mem = buf.Data; + for ( uint32_t i = 0; i < buf.Accessor->count; ++i ) { + weights.push_back( + Ra::Core::Vector4f { float( mem[4 * i] ) / UCHAR_MAX, + float( mem[4 * i + 1] ) / UCHAR_MAX, + float( mem[4 * i + 2] ) / UCHAR_MAX, + float( mem[4 * i + 3] ) / UCHAR_MAX } ); + } + break; + } + case fx::gltf::Accessor::ComponentType::UnsignedShort: { + auto mem = reinterpret_cast( buf.Data ); + for ( uint32_t i = 0; i < buf.Accessor->count; ++i ) { + weights.push_back( + Ra::Core::Vector4f { float( mem[4 * i] ) / USHRT_MAX, + float( mem[4 * i + 1] ) / USHRT_MAX, + float( mem[4 * i + 2] ) / USHRT_MAX, + float( mem[4 * i + 3] ) / USHRT_MAX } ); + } + break; + } + default: + LOG( Ra::Core::Utils::logERROR ) + << "GLTF GetWeights -- Weights must be Float or UnsignedByte or" + << " UnsignedShort !"; + } + } + } + } + + private: + BufferInfo m_indexBuffer {}; + BufferInfo m_vertexBuffer {}; + BufferInfo m_normalBuffer {}; + BufferInfo m_tangentBuffer {}; + BufferInfo m_texCoord0Buffer {}; + + MaterialData m_materialData {}; + + fx::gltf::Primitive::Mode m_mode; + + static BufferInfo GetData( const fx::gltf::Document& doc, const fx::gltf::Accessor& accessor ) { + const fx::gltf::BufferView& bufferView = doc.bufferViews[accessor.bufferView]; + const fx::gltf::Buffer& buffer = doc.buffers[bufferView.buffer]; + + const uint32_t dataTypeSize = CalculateDataTypeSize( accessor ); + return BufferInfo { + &accessor, + &buffer.data[static_cast( bufferView.byteOffset ) + accessor.byteOffset], + dataTypeSize, + accessor.count * dataTypeSize }; + } + + static uint32_t CalculateDataTypeSize( const fx::gltf::Accessor& accessor ) noexcept { + uint32_t elementSize; + switch ( accessor.componentType ) { + case fx::gltf::Accessor::ComponentType::Byte: + case fx::gltf::Accessor::ComponentType::UnsignedByte: + elementSize = 1; + break; + case fx::gltf::Accessor::ComponentType::Short: + case fx::gltf::Accessor::ComponentType::UnsignedShort: + elementSize = 2; + break; + case fx::gltf::Accessor::ComponentType::Float: + case fx::gltf::Accessor::ComponentType::UnsignedInt: + elementSize = 4; + break; + default: + elementSize = 0; + } + + switch ( accessor.type ) { + case fx::gltf::Accessor::Type::Mat2: + return 4 * elementSize; + case fx::gltf::Accessor::Type::Mat3: + return 9 * elementSize; + case fx::gltf::Accessor::Type::Mat4: + return 16 * elementSize; + case fx::gltf::Accessor::Type::Scalar: + return elementSize; + case fx::gltf::Accessor::Type::Vec2: + return 2 * elementSize; + case fx::gltf::Accessor::Type::Vec3: + return 3 * elementSize; + case fx::gltf::Accessor::Type::Vec4: + return 4 * elementSize; + default: + return 0; + } + } +}; + +class MeshNameCache +{ + public: + static void resetCache(); + static size_t cacheSize(); + static std::pair::iterator, bool> addName( const std::string& name ); + + private: + /** Collection of loaded names */ + static std::set s_nameCache; +}; + +std::vector> +buildMesh( const fx::gltf::Document& doc, + int32_t meshIndex, + const std::string& filePath, + int32_t nodeNum ); + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/GLTFConverter/NormalCalculator.cpp b/src/IO/Gltf/internal/GLTFConverter/NormalCalculator.cpp new file mode 100644 index 00000000000..b753dd9e8d0 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/NormalCalculator.cpp @@ -0,0 +1,50 @@ +#include +#include + +namespace Ra { +namespace IO { +namespace GLTF { +using namespace Ra::Core; +using namespace Ra::Core::Asset; + +void NormalCalculator::operator()( GeometryData* gdp, bool basic ) { + + auto& geo = gdp->getGeometry(); + const auto& [layerKey, layerBase] = + geo.getFirstLayerOccurrence( Ra::Core::Geometry::TriangleIndexLayer::staticSemanticName ); + const auto& triangle = static_cast( layerBase ); + const auto& faces = triangle.collection(); + const auto& vertices = geo.vertices(); + + auto attribHandle = + geo.addAttrib( getAttribName( Ra::Core::Geometry::MeshAttrib::VERTEX_NORMAL ) ); + auto& normals = geo.vertexAttribs().getDataWithLock( attribHandle ); + normals.clear(); + normals.resize( geo.vertices().size(), Vector3::Zero() ); + + for ( const auto& t : faces ) { + Vector3 n = getTriangleNormal( t, basic, vertices ); + for ( uint i = 0; i < 3; ++i ) { + normals[t[i]] += n; + } + } + normals.getMap().colwise().normalize(); + geo.vertexAttribs().unlock( attribHandle ); +} + +Vector3 NormalCalculator::getTriangleNormal( const Vector3ui& t, + bool basic, + const Vector3Array& vertices ) { + auto p = vertices[t[0]]; + auto q = vertices[t[1]]; + auto r = vertices[t[2]]; + + const Vector3 n = ( q - p ).cross( r - p ); + if ( n.isApprox( Vector3::Zero() ) ) { return Vector3::Zero(); } + if ( basic ) { return ( n.normalized() ); } + else { return ( n * 0.5 ); } +} + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/GLTFConverter/NormalCalculator.hpp b/src/IO/Gltf/internal/GLTFConverter/NormalCalculator.hpp new file mode 100644 index 00000000000..890ec451159 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/NormalCalculator.hpp @@ -0,0 +1,36 @@ +#pragma once +#include +#include + +namespace Ra::Core::Asset { +class GeometryData; +} + +namespace Ra { +namespace IO { +namespace GLTF { + +/** + * Functor that computes normals of a triangle meshmesh + */ +class NormalCalculator +{ + public: + /** Compute the normals for the given mesh + * @note : assume that both vertices and faces are set on the geometry data + * + * @param gdp The geometry data on which the normal must be computed + * @param basic true if vertices' normals must be face normal average, false if normals must be + * area-weighted average + */ + void operator()( Ra::Core::Asset::GeometryData* gdp, bool basic = true ); + + private: + static Ra::Core::Vector3 getTriangleNormal( const Ra::Core::Vector3ui& t, + bool basic, + const Ra::Core::Vector3Array& vertices ); +}; + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/GLTFConverter/SceneNode.cpp b/src/IO/Gltf/internal/GLTFConverter/SceneNode.cpp new file mode 100644 index 00000000000..1695ca25789 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/SceneNode.cpp @@ -0,0 +1,29 @@ +#include + +#include + +namespace Ra { +namespace IO { +namespace GLTF { + +void SceneNode::initPropsFromExtensionsAndExtra( const nlohmann::json& extensionsAndExtras ) { + // manage node extension + if ( !extensionsAndExtras.empty() ) { + auto extensions = extensionsAndExtras.find( "extensions" ); + if ( extensions != extensionsAndExtras.end() ) { + auto iter = extensions->find( "KHR_lights_punctual" ); + if ( iter != extensions->end() ) { + gltf_node_KHR_lights_punctual light; + from_json( *iter, light ); + // TODO do wee need more than that ? + // Do we need to keep the full extension definition (gltf_node_KHR_lights_punctual) + // ? + this->m_lightIndex = light.light; + } + } + } +} + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/GLTFConverter/SceneNode.hpp b/src/IO/Gltf/internal/GLTFConverter/SceneNode.hpp new file mode 100644 index 00000000000..6d857990c51 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/SceneNode.hpp @@ -0,0 +1,26 @@ +#pragma once +#include + +#include + +namespace Ra { +namespace IO { +namespace GLTF { + +/// TODO : make graph node more adapted +struct SceneNode { + Ra::Core::Transform m_transform; + int32_t m_cameraIndex { -1 }; + int32_t m_meshIndex { -1 }; + int32_t m_skinIndex { -1 }; + std::string m_nodeName; + /// only used with KHR_lights_punctual extension + int32_t m_lightIndex { -1 }; + std::vector children {}; + + void initPropsFromExtensionsAndExtra( const nlohmann::json& extensionsAndExtras ); +}; + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/GLTFConverter/TangentCalculator.cpp b/src/IO/Gltf/internal/GLTFConverter/TangentCalculator.cpp new file mode 100644 index 00000000000..744c6a9be1d --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/TangentCalculator.cpp @@ -0,0 +1,176 @@ +#include +#include + +namespace Ra { +namespace IO { +namespace GLTF { + +using namespace Ra::Core; +using namespace Ra::Core::Asset; + +// Initialize MikkTSpaceInterface with callbacks and run calculator. +void TangentCalculator::operator()( GeometryData* gdp, bool basic ) { + SMikkTSpaceInterface iface; + iface.m_getNumFaces = getNumFaces; + iface.m_getNumVerticesOfFace = getNumVerticesOfFace; + iface.m_getPosition = getPosition; + iface.m_getNormal = getNormal; + iface.m_getTexCoord = getTexCoord; + iface.m_setTSpaceBasic = basic ? setTSpaceBasic : nullptr; + iface.m_setTSpace = basic ? nullptr : setTSpace; + + auto& geo = gdp->getGeometry(); + // According to gltf specification, this should be Vector4, but RAdium requires Vector3 as + // tangents + auto attribHandle = + geo.addAttrib( getAttribName( Ra::Core::Geometry::MeshAttrib::VERTEX_TANGENT ) ); + auto& tangents = geo.vertexAttribs().getDataWithLock( attribHandle ); + tangents.resize( geo.vertices().size() ); + geo.vertexAttribs().unlock( attribHandle ); + if ( !basic ) { + auto attribBiTangents = geo.addAttrib( + getAttribName( Ra::Core::Geometry::MeshAttrib::VERTEX_BITANGENT ) ); + auto& bitangents = geo.vertexAttribs().getDataWithLock( attribBiTangents ); + bitangents.resize( geo.vertices().size() ); + geo.vertexAttribs().unlock( attribBiTangents ); + } + SMikkTSpaceContext context; + context.m_pInterface = &iface; + context.m_pUserData = gdp; + + genTangSpaceDefault( &context ); + + // Do we need to renormalize/orthogonalize ? +} + +// Return number of primitives in the geometry. +int TangentCalculator::getNumFaces( const SMikkTSpaceContext* context ) { + // Cast the void pointer from context data to our GeometryData pointer. + auto gdp = static_cast( context->m_pUserData ); + return int( gdp->getPrimitiveCount() ); +} + +// Return number of vertices in the primitive given by index. +// Right now, GLTF only manage triangle meshes +int TangentCalculator::getNumVerticesOfFace( const SMikkTSpaceContext* /*context*/, + int /*primnum*/ ) { + return 3; +} + +// Write 3-float position of the vertex's point. +void TangentCalculator::getPosition( const SMikkTSpaceContext* context, + float outpos[], + int primnum, + int vtxnum ) { + auto gdp = static_cast( context->m_pUserData ); + auto& geo = gdp->getGeometry(); + const auto& [layerKey, layerBase] = + geo.getFirstLayerOccurrence( Ra::Core::Geometry::TriangleIndexLayer::staticSemanticName ); + const auto& triangle = static_cast( layerBase ); + const auto& face = triangle.collection()[primnum]; + const auto& vertex = geo.vertices()[face[vtxnum]]; + + // Write into the input 3-float array. + outpos[0] = vertex[0]; + outpos[1] = vertex[1]; + outpos[2] = vertex[2]; +} + +// Write 3-float vertex normal. +void TangentCalculator::getNormal( const SMikkTSpaceContext* context, + float outnormal[], + int primnum, + int vtxnum ) { + auto gdp = static_cast( context->m_pUserData ); + auto& geo = gdp->getGeometry(); + const auto& [layerKey, layerBase] = + geo.getFirstLayerOccurrence( Ra::Core::Geometry::TriangleIndexLayer::staticSemanticName ); + const auto& triangle = static_cast( layerBase ); + const auto& face = triangle.collection()[primnum]; + const auto& normal = geo.normals()[face[vtxnum]]; + + outnormal[0] = normal[0]; + outnormal[1] = normal[1]; + outnormal[2] = normal[2]; +} + +// Write 2-float vertex uv. +void TangentCalculator::getTexCoord( const SMikkTSpaceContext* context, + float outuv[], + int primnum, + int vtxnum ) { + auto gdp = static_cast( context->m_pUserData ); + auto& geo = gdp->getGeometry(); + const auto& [layerKey, layerBase] = + geo.getFirstLayerOccurrence( Ra::Core::Geometry::TriangleIndexLayer::staticSemanticName ); + const auto& triangle = static_cast( layerBase ); + const auto& face = triangle.collection()[primnum]; + + auto attribHandle = + geo.addAttrib( getAttribName( Ra::Core::Geometry::MeshAttrib::VERTEX_TEXCOORD ) ); + const auto& texCoords = geo.vertexAttribs().getData( attribHandle ); + const auto& uv = texCoords[face[vtxnum]]; + + outuv[0] = uv[0]; + outuv[1] = uv[1]; +} + +// Compute and set attributes on the geometry vertex. Basic version. +void TangentCalculator::setTSpaceBasic( const SMikkTSpaceContext* context, + const float tangentu[], + float sign, + int primnum, + int vtxnum ) { + auto gdp = static_cast( context->m_pUserData ); + auto& geo = gdp->getGeometry(); + const auto& [layerKey, layerBase] = + geo.getFirstLayerOccurrence( Ra::Core::Geometry::TriangleIndexLayer::staticSemanticName ); + const auto& triangle = static_cast( layerBase ); + const auto& face = triangle.collection()[primnum]; + + auto attribHandle = + geo.addAttrib( getAttribName( Ra::Core::Geometry::MeshAttrib::VERTEX_TANGENT ) ); + Vector3Array& tangents = geo.vertexAttribs().getDataWithLock( attribHandle ); + int tgtindex = face[vtxnum]; + + // well, some liberties against the spec and the MKKTSpace algo ... + // tangents[tgtindex] = tangents[tgtindex] + Ra::Core::Vector3(tangentu[0], tangentu[1], + // tangentu[2]) * sign; + tangents[tgtindex] = Vector3( tangentu[0], tangentu[1], tangentu[2] ) * sign; + + geo.vertexAttribs().unlock( attribHandle ); +} + +// Compute and set attributes on the geometry vertex. +void TangentCalculator::setTSpace( const SMikkTSpaceContext* context, + const float tangentu[], + const float tangentv[], + const float magu, + const float magv, + const tbool /*keep*/, + const int primnum, + const int vtxnum ) { + auto gdp = static_cast( context->m_pUserData ); + auto& geo = gdp->getGeometry(); + auto [layerKey, layerBase] = + geo.getFirstLayerOccurrence( Ra::Core::Geometry::TriangleIndexLayer::staticSemanticName ); + const auto& triangle = static_cast( layerBase ); + const auto& face = triangle.collection()[primnum]; + + auto attribTangents = + geo.addAttrib( getAttribName( Ra::Core::Geometry::MeshAttrib::VERTEX_TANGENT ) ); + auto attribBiTangents = + geo.addAttrib( getAttribName( Ra::Core::Geometry::MeshAttrib::VERTEX_BITANGENT ) ); + auto unlocker = geo.vertexAttribs().getScopedLockState(); + + Vector3Array& tangents = geo.vertexAttribs().getDataWithLock( attribTangents ); + Vector3Array& bitangents = geo.vertexAttribs().getDataWithLock( attribBiTangents ); + int tgtindex = face[vtxnum]; + + tangents[tgtindex] = Vector3( tangentu[0], tangentu[1], tangentu[2] ) * magu; + bitangents[tgtindex] = Vector3( tangentv[0], tangentv[1], tangentv[2] ) * magv; +} + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/GLTFConverter/TangentCalculator.hpp b/src/IO/Gltf/internal/GLTFConverter/TangentCalculator.hpp new file mode 100644 index 00000000000..c41aa2d516d --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/TangentCalculator.hpp @@ -0,0 +1,137 @@ +#pragma once +#include + +#include + +namespace Ra { +namespace IO { +namespace GLTF { + +// inspired by https://github.com/teared/mikktspace-for-houdini +/** + * Functor that computes tangent of a triangle mesh. + * Tangents are computed according to the GLTF specification + * inspired by https://github.com/teared/mikktspace-for-houdini + * + * Note that, as stated in https://github.com/KhronosGroup/glTF-Sample-Models/issues/174, the + tangents could be + * computed in the glsl shader by the following code : + * + * The "reference" glTF PBR shader contains the following code. This code is intended by the working + group to be the standard implementation of the shader calculations: + + vec3 pos_dx = dFdx(v_Position); + vec3 pos_dy = dFdy(v_Position); + vec3 tex_dx = dFdx(vec3(v_UV, 0.0)); + vec3 tex_dy = dFdy(vec3(v_UV, 0.0)); + vec3 t = (tex_dy.t * pos_dx - tex_dx.t * pos_dy) / (tex_dx.s * tex_dy.t - tex_dy.s * tex_dx.t); + + vec3 ng = normalize(v_Normal); + + t = normalize(t - ng * dot(ng, t)); + vec3 b = normalize(cross(ng, t)); + mat3 tbn = mat3(t, b, ng); + + vec3 n = texture2D(u_NormalSampler, v_UV).rgb; + n = normalize(tbn * ((2.0 * n - 1.0) * vec3(u_NormalScale, u_NormalScale, 1.0))); + + * + * + */ +class TangentCalculator +{ + public: + /** Initialize MikkTSpaceInterface with callbacks and run calculator. + * + * @param gdp the geometry data for which tangents must be computed + * @param basic tru if only tangents must be computed, fals if binormals are required. + */ + void operator()( Ra::Core::Asset::GeometryData* gdp, bool basic = true ); + + /** + * MikkTSpace callback that returns number of primitives in the geometry. + * @param context + * @return + */ + static int getNumFaces( const SMikkTSpaceContext* context ); + + /** + * MikkTSpace callback that returns number of vertices in the primitive given by index. + * + * @param context + * @param primnum + * @return + */ + static int getNumVerticesOfFace( const SMikkTSpaceContext* context, int primnum ); + + /** + * MikkTSpace callback that extract 3-float position of the vertex's point. + * + * @param context + * @param pos + * @param primnum + * @param vtxnum + */ + static void + getPosition( const SMikkTSpaceContext* context, float pos[], int primnum, int vtxnum ); + + /** + * MikkTSpace callback that extract 3-float vertex normal. + * + * @param context + * @param normal + * @param primnum + * @param vtxnum + */ + static void + getNormal( const SMikkTSpaceContext* context, float normal[], int primnum, int vtxnum ); + + /** + * MikkTSpace callback that extract 2-float vertex uv. + * + * @param context + * @param uv + * @param primnum + * @param vtxnum + */ + static void + getTexCoord( const SMikkTSpaceContext* context, float uv[], int primnum, int vtxnum ); + + /** + * MikkTSpace callback that set tangent attribute on the geometry vertex. + * + * @param context + * @param tangentu + * @param sign + * @param primnum + * @param vtxnum + */ + static void setTSpaceBasic( const SMikkTSpaceContext* context, + const float tangentu[], + float sign, + int primnum, + int vtxnum ); + /** + * MikkTSpace callback that set tangent (tangentu) and binormal (tangentv) attributes on the + * geometry vertex. + * + * @param context + * @param tangentu + * @param tangentu + * @param sign + * @param primnum + * @param vtxnum + */ + static void setTSpace( const SMikkTSpaceContext* context, + const float tangentu[], + const float tangentv[], + float magu, + float magv, + tbool keep, + int primnum, + int vtxnum ); +}; + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/GLTFConverter/TransformationManager.cpp b/src/IO/Gltf/internal/GLTFConverter/TransformationManager.cpp new file mode 100644 index 00000000000..171b5ab394a --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/TransformationManager.cpp @@ -0,0 +1,163 @@ +#include +#include +#include + +using namespace Ra::Core; + +namespace Ra { +namespace IO { +namespace GLTF { + +void TransformationManager::insert( int32_t node, + const std::string& path, + float* times, + float* transformations, + int32_t count, + fx::gltf::Animation::Sampler::Type /*interpolation*/, + const std::array& nodeRotation, + const std::array& nodeScale, + const std::array& nodeTranslation ) { + // FIXME : interpolation not use + // weights' animation and scales' animation not handle by radium + if ( path == "weights" ) { + LOG( Ra::Core::Utils::logINFO ) + << "GLTF file contain weights animation. It's not handle by radium."; + return; + } + + // add times to m_times + if ( m_times.find( node ) == m_times.end() ) { + std::set timesVec; + std::map rotations; + std::map scales; + std::map translations; + m_times.insert( std::pair( node, timesVec ) ); + m_rotation.insert( std::pair( node, rotations ) ); + m_scale.insert( std::pair( node, scales ) ); + m_translation.insert( std::pair( node, translations ) ); + m_nodeVisited.push_back( node ); + m_nodeBaseTransform.insert( + std::pair( node, std::tuple( nodeRotation, nodeScale, nodeTranslation ) ) ); + } + std::set& timesVec = m_times[node]; + for ( int32_t i = 0; i < count; ++i ) { + timesVec.insert( times[i] ); + } + // insert times and transformations in map + if ( path == "rotation" ) { + std::map& rotations = m_rotation[node]; + for ( int32_t i = 0; i < count; ++i ) { + Quaternionf quat( transformations[i * 4 + 3], + transformations[i * 4], + transformations[i * 4 + 1], + transformations[i * 4 + 2] ); + rotations.insert( std::pair( times[i], quat ) ); + } + return; + } + if ( path == "scale" ) { + std::map& scale = m_scale[node]; + for ( int32_t i = 0; i < count; ++i ) { + Vector3 vec( + transformations[i * 3], transformations[i * 3 + 1], transformations[i * 3 + 2] ); + scale.insert( std::pair( times[i], vec ) ); + } + return; + } + if ( path == "translation" ) { + std::map& translations = m_translation[node]; + for ( int32_t i = 0; i < count; ++i ) { + Vector3f vec( + transformations[i * 3], transformations[i * 3 + 1], transformations[i * 3 + 2] ); + translations.insert( std::pair( times[i], vec ) ); + } + return; + } +} + +void TransformationManager::buildAnimation( + std::vector& animations ) { + for ( unsigned int node : m_nodeVisited ) { + Ra::Core::Asset::HandleAnimation animation; + std::map& rotations = m_rotation[node]; + std::map& scales = m_scale[node]; + std::map& translations = m_translation[node]; + + // if there is no animation, then use Node's transform! + if ( rotations.empty() ) { + const auto& quat = std::get<0>( m_nodeBaseTransform[node] ); + Quaternionf quaternionf( quat[3], quat[0], quat[1], quat[2] ); + rotations.insert( std::pair( 0.0f, quaternionf ) ); + } + if ( scales.empty() ) { + const auto& vec = std::get<1>( m_nodeBaseTransform[node] ); + Vector3 vector3( vec[0], vec[1], vec[2] ); + scales.insert( std::pair( 0.0f, vector3 ) ); + } + if ( translations.empty() ) { + const auto& vec = std::get<2>( m_nodeBaseTransform[node] ); + Vector3 vector3( vec[0], vec[1], vec[2] ); + translations.insert( std::pair( 0.0f, vector3 ) ); + } + + auto it_rotation = rotations.begin(); + auto it_scale = scales.begin(); + auto it_translation = translations.begin(); + for ( float time : m_times[node] ) { + // Rotation + Quaternionf rotation; + Vector3 scale; + Vector3 translation; + + for ( ; it_rotation != rotations.end() && it_rotation->first < time; ++it_rotation ) + ; + for ( ; it_scale != scales.end() && it_scale->first < time; ++it_scale ) + ; + for ( ; it_translation != translations.end() && it_translation->first < time; + ++it_translation ) + ; + + if ( it_rotation == rotations.end() ) { rotation = prev( it_rotation )->second; } + else if ( it_rotation->first == time || it_rotation == rotations.begin() ) { + rotation = it_rotation->second; + } + else { + float t = ( time - prev( it_rotation )->first ) / + ( it_rotation->first - prev( it_rotation )->first ); + rotation = Math::linearInterpolate( + prev( it_rotation, 1 )->second, it_rotation->second, t ); + } + if ( it_scale == scales.end() ) { scale = prev( it_scale )->second; } + else if ( it_scale->first == time || it_scale == scales.begin() ) { + scale = it_scale->second; + } + else { + float t = ( time - prev( it_scale )->first ) / + ( it_scale->first - prev( it_scale )->first ); + scale = Math::linearInterpolate( prev( it_scale, 1 )->second, it_scale->second, t ); + } + if ( it_translation == translations.end() ) { + translation = prev( it_translation )->second; + } + else if ( it_translation->first == time || it_translation == translations.begin() ) { + translation = it_translation->second; + } + else { + float t = ( time - prev( it_translation )->first ) / + ( it_translation->first - prev( it_translation )->first ); + translation = Math::linearInterpolate( + prev( it_translation, 1 )->second, it_translation->second, t ); + } + + Transform transform; + transform.fromPositionOrientationScale( translation, rotation, scale ); + animation.m_anim.insertKeyFrame( time, transform ); + } + animation.m_name = m_nodeIdToBoneName[node]; + animations.push_back( animation ); + } +} + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/GLTFConverter/TransformationManager.hpp b/src/IO/Gltf/internal/GLTFConverter/TransformationManager.hpp new file mode 100644 index 00000000000..45e9bb7a106 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/TransformationManager.hpp @@ -0,0 +1,46 @@ +#pragma once +#include +#include + +#include + +#include +#include +#include +#include + +namespace Ra { +namespace IO { +namespace GLTF { + +class TransformationManager +{ + public: + explicit TransformationManager( std::map& map ) : + m_nodeIdToBoneName { map } {}; + void insert( int32_t node, + const std::string& path, + float* times, + float* transformations, + int32_t count, + fx::gltf::Animation::Sampler::Type interpolation, + const std::array& nodeRotation, + const std::array& nodeScale, + const std::array& nodeTranslation ); + + void buildAnimation( std::vector& animations ); + + private: + std::vector m_nodeVisited; + std::map> m_rotation; + std::map> m_scale; + std::map> m_translation; + std::map> m_times; + std::map& m_nodeIdToBoneName; + std::map, std::array, std::array>> + m_nodeBaseTransform; +}; + +} // namespace GLTF +} // namespace IO +} // namespace Ra diff --git a/src/IO/Gltf/internal/GLTFConverter/mikktspace.c b/src/IO/Gltf/internal/GLTFConverter/mikktspace.c new file mode 100644 index 00000000000..1bffdd66cef --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/mikktspace.c @@ -0,0 +1,1938 @@ +/** \file mikktspace/mikktspace.c + * \ingroup mikktspace + */ +/** + * Copyright (C) 2011 by Morten S. Mikkelsen + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#include +#include +#include +#include +#include +#include + +#include "mikktspace.h" + +#define TFALSE 0 +#define TTRUE 1 + +#ifndef M_PI +# define M_PI 3.1415926535897932384626433832795 +#endif + +#define INTERNAL_RND_SORT_SEED 39871946 + +// internal structure +typedef struct { + float x, y, z; +} SVec3; + +static tbool veq( const SVec3 v1, const SVec3 v2 ) { + return ( v1.x == v2.x ) && ( v1.y == v2.y ) && ( v1.z == v2.z ); +} + +static SVec3 vadd( const SVec3 v1, const SVec3 v2 ) { + SVec3 vRes; + + vRes.x = v1.x + v2.x; + vRes.y = v1.y + v2.y; + vRes.z = v1.z + v2.z; + + return vRes; +} + +static SVec3 vsub( const SVec3 v1, const SVec3 v2 ) { + SVec3 vRes; + + vRes.x = v1.x - v2.x; + vRes.y = v1.y - v2.y; + vRes.z = v1.z - v2.z; + + return vRes; +} + +static SVec3 vscale( const float fS, const SVec3 v ) { + SVec3 vRes; + + vRes.x = fS * v.x; + vRes.y = fS * v.y; + vRes.z = fS * v.z; + + return vRes; +} + +static float LengthSquared( const SVec3 v ) { + return v.x * v.x + v.y * v.y + v.z * v.z; +} + +static float Length( const SVec3 v ) { + return sqrtf( LengthSquared( v ) ); +} + +static SVec3 Normalize( const SVec3 v ) { + return vscale( 1 / Length( v ), v ); +} + +static float vdot( const SVec3 v1, const SVec3 v2 ) { + return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; +} + +static tbool NotZero( const float fX ) { + // could possibly use FLT_EPSILON instead + return fabsf( fX ) > FLT_MIN; +} + +static tbool VNotZero( const SVec3 v ) { + // might change this to an epsilon based test + return NotZero( v.x ) || NotZero( v.y ) || NotZero( v.z ); +} + +typedef struct { + int iNrFaces; + int* pTriMembers; +} SSubGroup; + +typedef struct { + int iNrFaces; + int* pFaceIndices; + int iVertexRepresentitive; + tbool bOrientPreservering; +} SGroup; + +// +#define MARK_DEGENERATE 1 +#define QUAD_ONE_DEGEN_TRI 2 +#define GROUP_WITH_ANY 4 +#define ORIENT_PRESERVING 8 + +typedef struct { + int FaceNeighbors[3]; + SGroup* AssignedGroup[3]; + + // normalized first order face derivatives + SVec3 vOs, vOt; + float fMagS, fMagT; // original magnitudes + + // determines if the current and the next triangle are a quad. + int iOrgFaceNumber; + int iFlag, iTSpacesOffs; + unsigned char vert_num[4]; +} STriInfo; + +typedef struct { + SVec3 vOs; + float fMagS; + SVec3 vOt; + float fMagT; + int iCounter; // this is to average back into quads. + tbool bOrient; +} STSpace; + +static int GenerateInitialVerticesIndexList( STriInfo pTriInfos[], + int piTriList_out[], + const SMikkTSpaceContext* pContext, + const int iNrTrianglesIn ); +static void GenerateSharedVerticesIndexList( int piTriList_in_and_out[], + const SMikkTSpaceContext* pContext, + const int iNrTrianglesIn ); +static void InitTriInfo( STriInfo pTriInfos[], + const int piTriListIn[], + const SMikkTSpaceContext* pContext, + const int iNrTrianglesIn ); +static int Build4RuleGroups( STriInfo pTriInfos[], + SGroup pGroups[], + int piGroupTrianglesBuffer[], + const int piTriListIn[], + const int iNrTrianglesIn ); +static tbool GenerateTSpaces( STSpace psTspace[], + const STriInfo pTriInfos[], + const SGroup pGroups[], + const int iNrActiveGroups, + const int piTriListIn[], + const float fThresCos, + const SMikkTSpaceContext* pContext ); + +static int MakeIndex( const int iFace, const int iVert ) { + assert( iVert >= 0 && iVert < 4 && iFace >= 0 ); + return ( iFace << 2 ) | ( iVert & 0x3 ); +} + +static void IndexToData( int* piFace, int* piVert, const int iIndexIn ) { + piVert[0] = iIndexIn & 0x3; + piFace[0] = iIndexIn >> 2; +} + +static STSpace AvgTSpace( const STSpace* pTS0, const STSpace* pTS1 ) { + STSpace ts_res; + + // this if is important. Due to floating point precision + // averaging when ts0==ts1 will cause a slight difference + // which results in tangent space splits later on + if ( pTS0->fMagS == pTS1->fMagS && pTS0->fMagT == pTS1->fMagT && veq( pTS0->vOs, pTS1->vOs ) && + veq( pTS0->vOt, pTS1->vOt ) ) { + ts_res.fMagS = pTS0->fMagS; + ts_res.fMagT = pTS0->fMagT; + ts_res.vOs = pTS0->vOs; + ts_res.vOt = pTS0->vOt; + } + else { + ts_res.fMagS = 0.5f * ( pTS0->fMagS + pTS1->fMagS ); + ts_res.fMagT = 0.5f * ( pTS0->fMagT + pTS1->fMagT ); + ts_res.vOs = vadd( pTS0->vOs, pTS1->vOs ); + ts_res.vOt = vadd( pTS0->vOt, pTS1->vOt ); + if ( VNotZero( ts_res.vOs ) ) ts_res.vOs = Normalize( ts_res.vOs ); + if ( VNotZero( ts_res.vOt ) ) ts_res.vOt = Normalize( ts_res.vOt ); + } + + return ts_res; +} + +static SVec3 GetPosition( const SMikkTSpaceContext* pContext, const int index ); +static SVec3 GetNormal( const SMikkTSpaceContext* pContext, const int index ); +static SVec3 GetTexCoord( const SMikkTSpaceContext* pContext, const int index ); + +// degen triangles +static void DegenPrologue( STriInfo pTriInfos[], + int piTriList_out[], + const int iNrTrianglesIn, + const int iTotTris ); +static void DegenEpilogue( STSpace psTspace[], + STriInfo pTriInfos[], + int piTriListIn[], + const SMikkTSpaceContext* pContext, + const int iNrTrianglesIn, + const int iTotTris ); + +tbool genTangSpaceDefault( const SMikkTSpaceContext* pContext ) { + return genTangSpace( pContext, 180.0f ); +} + +tbool genTangSpace( const SMikkTSpaceContext* pContext, const float fAngularThreshold ) { + // count nr_triangles + int *piTriListIn = NULL, *piGroupTrianglesBuffer = NULL; + STriInfo* pTriInfos = NULL; + SGroup* pGroups = NULL; + STSpace* psTspace = NULL; + int iNrTrianglesIn = 0, f = 0, t = 0, i = 0; + int iNrTSPaces = 0, iTotTris = 0, iDegenTriangles = 0, iNrMaxGroups = 0; + int iNrActiveGroups = 0, index = 0; + const int iNrFaces = pContext->m_pInterface->m_getNumFaces( pContext ); + tbool bRes = TFALSE; + const float fThresCos = (float)cos( ( fAngularThreshold * (float)M_PI ) / 180.0f ); + + // verify all call-backs have been set + if ( pContext->m_pInterface->m_getNumFaces == NULL || + pContext->m_pInterface->m_getNumVerticesOfFace == NULL || + pContext->m_pInterface->m_getPosition == NULL || + pContext->m_pInterface->m_getNormal == NULL || + pContext->m_pInterface->m_getTexCoord == NULL ) + return TFALSE; + + // count triangles on supported faces + for ( f = 0; f < iNrFaces; f++ ) { + const int verts = pContext->m_pInterface->m_getNumVerticesOfFace( pContext, f ); + if ( verts == 3 ) + ++iNrTrianglesIn; + else if ( verts == 4 ) + iNrTrianglesIn += 2; + } + if ( iNrTrianglesIn <= 0 ) return TFALSE; + + // allocate memory for an index list + piTriListIn = (int*)malloc( sizeof( int ) * 3 * iNrTrianglesIn ); + pTriInfos = (STriInfo*)malloc( sizeof( STriInfo ) * iNrTrianglesIn ); + if ( piTriListIn == NULL || pTriInfos == NULL ) { + if ( piTriListIn != NULL ) free( piTriListIn ); + if ( pTriInfos != NULL ) free( pTriInfos ); + return TFALSE; + } + + // make an initial triangle --> face index list + iNrTSPaces = + GenerateInitialVerticesIndexList( pTriInfos, piTriListIn, pContext, iNrTrianglesIn ); + + // make a welded index list of identical positions and attributes (pos, norm, texc) + // printf("gen welded index list begin\n"); + GenerateSharedVerticesIndexList( piTriListIn, pContext, iNrTrianglesIn ); + // printf("gen welded index list end\n"); + + // Mark all degenerate triangles + iTotTris = iNrTrianglesIn; + iDegenTriangles = 0; + for ( t = 0; t < iTotTris; t++ ) { + const int i0 = piTriListIn[t * 3 + 0]; + const int i1 = piTriListIn[t * 3 + 1]; + const int i2 = piTriListIn[t * 3 + 2]; + const SVec3 p0 = GetPosition( pContext, i0 ); + const SVec3 p1 = GetPosition( pContext, i1 ); + const SVec3 p2 = GetPosition( pContext, i2 ); + if ( veq( p0, p1 ) || veq( p0, p2 ) || veq( p1, p2 ) ) // degenerate + { + pTriInfos[t].iFlag |= MARK_DEGENERATE; + ++iDegenTriangles; + } + } + iNrTrianglesIn = iTotTris - iDegenTriangles; + + // mark all triangle pairs that belong to a quad with only one + // good triangle. These need special treatment in DegenEpilogue(). + // Additionally, move all good triangles to the start of + // pTriInfos[] and piTriListIn[] without changing order and + // put the degenerate triangles last. + DegenPrologue( pTriInfos, piTriListIn, iNrTrianglesIn, iTotTris ); + + // evaluate triangle level attributes and neighbor list + // printf("gen neighbors list begin\n"); + InitTriInfo( pTriInfos, piTriListIn, pContext, iNrTrianglesIn ); + // printf("gen neighbors list end\n"); + + // based on the 4 rules, identify groups based on connectivity + iNrMaxGroups = iNrTrianglesIn * 3; + pGroups = (SGroup*)malloc( sizeof( SGroup ) * iNrMaxGroups ); + piGroupTrianglesBuffer = (int*)malloc( sizeof( int ) * iNrTrianglesIn * 3 ); + if ( pGroups == NULL || piGroupTrianglesBuffer == NULL ) { + if ( pGroups != NULL ) free( pGroups ); + if ( piGroupTrianglesBuffer != NULL ) free( piGroupTrianglesBuffer ); + free( piTriListIn ); + free( pTriInfos ); + return TFALSE; + } + // printf("gen 4rule groups begin\n"); + iNrActiveGroups = + Build4RuleGroups( pTriInfos, pGroups, piGroupTrianglesBuffer, piTriListIn, iNrTrianglesIn ); + // printf("gen 4rule groups end\n"); + + // + + psTspace = (STSpace*)malloc( sizeof( STSpace ) * iNrTSPaces ); + if ( psTspace == NULL ) { + free( piTriListIn ); + free( pTriInfos ); + free( pGroups ); + free( piGroupTrianglesBuffer ); + return TFALSE; + } + memset( psTspace, 0, sizeof( STSpace ) * iNrTSPaces ); + for ( t = 0; t < iNrTSPaces; t++ ) { + psTspace[t].vOs.x = 1.0f; + psTspace[t].vOs.y = 0.0f; + psTspace[t].vOs.z = 0.0f; + psTspace[t].fMagS = 1.0f; + psTspace[t].vOt.x = 0.0f; + psTspace[t].vOt.y = 1.0f; + psTspace[t].vOt.z = 0.0f; + psTspace[t].fMagT = 1.0f; + } + + // make tspaces, each group is split up into subgroups if necessary + // based on fAngularThreshold. Finally a tangent space is made for + // every resulting subgroup + // printf("gen tspaces begin\n"); + bRes = GenerateTSpaces( + psTspace, pTriInfos, pGroups, iNrActiveGroups, piTriListIn, fThresCos, pContext ); + // printf("gen tspaces end\n"); + + // clean up + free( pGroups ); + free( piGroupTrianglesBuffer ); + + if ( !bRes ) // if an allocation in GenerateTSpaces() failed + { + // clean up and return false + free( pTriInfos ); + free( piTriListIn ); + free( psTspace ); + return TFALSE; + } + + // degenerate quads with one good triangle will be fixed by copying a space from + // the good triangle to the coinciding vertex. + // all other degenerate triangles will just copy a space from any good triangle + // with the same welded index in piTriListIn[]. + DegenEpilogue( psTspace, pTriInfos, piTriListIn, pContext, iNrTrianglesIn, iTotTris ); + + free( pTriInfos ); + free( piTriListIn ); + + index = 0; + for ( f = 0; f < iNrFaces; f++ ) { + const int verts = pContext->m_pInterface->m_getNumVerticesOfFace( pContext, f ); + if ( verts != 3 && verts != 4 ) continue; + + // I've decided to let degenerate triangles and group-with-anythings + // vary between left/right hand coordinate systems at the vertices. + // All healthy triangles on the other hand are built to always be either or. + + /*// force the coordinate system orientation to be uniform for every face. + // (this is already the case for good triangles but not for + // degenerate ones and those with bGroupWithAnything==true) + bool bOrient = psTspace[index].bOrient; + if (psTspace[index].iCounter == 0) // tspace was not derived from a group + { + // look for a space created in GenerateTSpaces() by iCounter>0 + bool bNotFound = true; + int i=1; + while (i 0) bNotFound=false; + else ++i; + } + if (!bNotFound) bOrient = psTspace[index+i].bOrient; + }*/ + + // set data + for ( i = 0; i < verts; i++ ) { + const STSpace* pTSpace = &psTspace[index]; + float tang[] = { pTSpace->vOs.x, pTSpace->vOs.y, pTSpace->vOs.z }; + if ( pContext->m_pInterface->m_setTSpace != NULL ) { + float bitang[] = { pTSpace->vOt.x, pTSpace->vOt.y, pTSpace->vOt.z }; + pContext->m_pInterface->m_setTSpace( pContext, + tang, + bitang, + pTSpace->fMagS, + pTSpace->fMagT, + pTSpace->bOrient, + f, + i ); + } + if ( pContext->m_pInterface->m_setTSpaceBasic != NULL ) + pContext->m_pInterface->m_setTSpaceBasic( + pContext, tang, pTSpace->bOrient == TTRUE ? 1.0f : ( -1.0f ), f, i ); + + ++index; + } + } + + free( psTspace ); + + return TTRUE; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +typedef struct { + float vert[3]; + int index; +} STmpVert; + +static const int g_iCells = 2048; + +#ifdef _MSC_VER +# define NOINLINE __declspec( noinline ) +#else +# define NOINLINE __attribute__( ( noinline ) ) +#endif + +// it is IMPORTANT that this function is called to evaluate the hash since +// inlining could potentially reorder instructions and generate different +// results for the same effective input value fVal. +static NOINLINE int FindGridCell( const float fMin, const float fMax, const float fVal ) { + const float fIndex = g_iCells * ( ( fVal - fMin ) / ( fMax - fMin ) ); + const int iIndex = (int)fIndex; + return iIndex < g_iCells ? ( iIndex >= 0 ? iIndex : 0 ) : ( g_iCells - 1 ); +} + +static void MergeVertsFast( int piTriList_in_and_out[], + STmpVert pTmpVert[], + const SMikkTSpaceContext* pContext, + const int iL_in, + const int iR_in ); +static void MergeVertsSlow( int piTriList_in_and_out[], + const SMikkTSpaceContext* pContext, + const int pTable[], + const int iEntries ); +static void GenerateSharedVerticesIndexListSlow( int piTriList_in_and_out[], + const SMikkTSpaceContext* pContext, + const int iNrTrianglesIn ); + +static void GenerateSharedVerticesIndexList( int piTriList_in_and_out[], + const SMikkTSpaceContext* pContext, + const int iNrTrianglesIn ) { + + // Generate bounding box + int *piHashTable = NULL, *piHashCount = NULL, *piHashOffsets = NULL, *piHashCount2 = NULL; + STmpVert* pTmpVert = NULL; + int i = 0, iChannel = 0, k = 0, e = 0; + int iMaxCount = 0; + SVec3 vMin = GetPosition( pContext, 0 ), vMax = vMin, vDim; + float fMin, fMax; + for ( i = 1; i < ( iNrTrianglesIn * 3 ); i++ ) { + const int index = piTriList_in_and_out[i]; + + const SVec3 vP = GetPosition( pContext, index ); + if ( vMin.x > vP.x ) + vMin.x = vP.x; + else if ( vMax.x < vP.x ) + vMax.x = vP.x; + if ( vMin.y > vP.y ) + vMin.y = vP.y; + else if ( vMax.y < vP.y ) + vMax.y = vP.y; + if ( vMin.z > vP.z ) + vMin.z = vP.z; + else if ( vMax.z < vP.z ) + vMax.z = vP.z; + } + + vDim = vsub( vMax, vMin ); + iChannel = 0; + fMin = vMin.x; + fMax = vMax.x; + if ( vDim.y > vDim.x && vDim.y > vDim.z ) { + iChannel = 1; + fMin = vMin.y, fMax = vMax.y; + } + else if ( vDim.z > vDim.x ) { + iChannel = 2; + fMin = vMin.z, fMax = vMax.z; + } + + // make allocations + piHashTable = (int*)malloc( sizeof( int ) * iNrTrianglesIn * 3 ); + piHashCount = (int*)malloc( sizeof( int ) * g_iCells ); + piHashOffsets = (int*)malloc( sizeof( int ) * g_iCells ); + piHashCount2 = (int*)malloc( sizeof( int ) * g_iCells ); + + if ( piHashTable == NULL || piHashCount == NULL || piHashOffsets == NULL || + piHashCount2 == NULL ) { + if ( piHashTable != NULL ) free( piHashTable ); + if ( piHashCount != NULL ) free( piHashCount ); + if ( piHashOffsets != NULL ) free( piHashOffsets ); + if ( piHashCount2 != NULL ) free( piHashCount2 ); + GenerateSharedVerticesIndexListSlow( piTriList_in_and_out, pContext, iNrTrianglesIn ); + return; + } + memset( piHashCount, 0, sizeof( int ) * g_iCells ); + memset( piHashCount2, 0, sizeof( int ) * g_iCells ); + + // count amount of elements in each cell unit + for ( i = 0; i < ( iNrTrianglesIn * 3 ); i++ ) { + const int index = piTriList_in_and_out[i]; + const SVec3 vP = GetPosition( pContext, index ); + const float fVal = iChannel == 0 ? vP.x : ( iChannel == 1 ? vP.y : vP.z ); + const int iCell = FindGridCell( fMin, fMax, fVal ); + ++piHashCount[iCell]; + } + + // evaluate start index of each cell. + piHashOffsets[0] = 0; + for ( k = 1; k < g_iCells; k++ ) + piHashOffsets[k] = piHashOffsets[k - 1] + piHashCount[k - 1]; + + // insert vertices + for ( i = 0; i < ( iNrTrianglesIn * 3 ); i++ ) { + const int index = piTriList_in_and_out[i]; + const SVec3 vP = GetPosition( pContext, index ); + const float fVal = iChannel == 0 ? vP.x : ( iChannel == 1 ? vP.y : vP.z ); + const int iCell = FindGridCell( fMin, fMax, fVal ); + int* pTable = NULL; + + assert( piHashCount2[iCell] < piHashCount[iCell] ); + pTable = &piHashTable[piHashOffsets[iCell]]; + pTable[piHashCount2[iCell]] = i; // vertex i has been inserted. + ++piHashCount2[iCell]; + } + for ( k = 0; k < g_iCells; k++ ) + assert( piHashCount2[k] == piHashCount[k] ); // verify the count + free( piHashCount2 ); + + // find maximum amount of entries in any hash entry + iMaxCount = piHashCount[0]; + for ( k = 1; k < g_iCells; k++ ) + if ( iMaxCount < piHashCount[k] ) iMaxCount = piHashCount[k]; + pTmpVert = (STmpVert*)malloc( sizeof( STmpVert ) * iMaxCount ); + + // complete the merge + for ( k = 0; k < g_iCells; k++ ) { + // extract table of cell k and amount of entries in it + int* pTable = &piHashTable[piHashOffsets[k]]; + const int iEntries = piHashCount[k]; + if ( iEntries < 2 ) continue; + + if ( pTmpVert != NULL ) { + for ( e = 0; e < iEntries; e++ ) { + int i = pTable[e]; + const SVec3 vP = GetPosition( pContext, piTriList_in_and_out[i] ); + pTmpVert[e].vert[0] = vP.x; + pTmpVert[e].vert[1] = vP.y; + pTmpVert[e].vert[2] = vP.z; + pTmpVert[e].index = i; + } + MergeVertsFast( piTriList_in_and_out, pTmpVert, pContext, 0, iEntries - 1 ); + } + else + MergeVertsSlow( piTriList_in_and_out, pContext, pTable, iEntries ); + } + + if ( pTmpVert != NULL ) { free( pTmpVert ); } + free( piHashTable ); + free( piHashCount ); + free( piHashOffsets ); +} + +static void MergeVertsFast( int piTriList_in_and_out[], + STmpVert pTmpVert[], + const SMikkTSpaceContext* pContext, + const int iL_in, + const int iR_in ) { + // make bbox + int c = 0, l = 0, channel = 0; + float fvMin[3], fvMax[3]; + float dx = 0, dy = 0, dz = 0, fSep = 0; + for ( c = 0; c < 3; c++ ) { + fvMin[c] = pTmpVert[iL_in].vert[c]; + fvMax[c] = fvMin[c]; + } + for ( l = ( iL_in + 1 ); l <= iR_in; l++ ) + for ( c = 0; c < 3; c++ ) + if ( fvMin[c] > pTmpVert[l].vert[c] ) + fvMin[c] = pTmpVert[l].vert[c]; + else if ( fvMax[c] < pTmpVert[l].vert[c] ) + fvMax[c] = pTmpVert[l].vert[c]; + + dx = fvMax[0] - fvMin[0]; + dy = fvMax[1] - fvMin[1]; + dz = fvMax[2] - fvMin[2]; + + channel = 0; + if ( dy > dx && dy > dz ) + channel = 1; + else if ( dz > dx ) + channel = 2; + + fSep = 0.5f * ( fvMax[channel] + fvMin[channel] ); + + // terminate recursion when the separation/average value + // is no longer strictly between fMin and fMax values. + if ( fSep >= fvMax[channel] || fSep <= fvMin[channel] ) { + // complete the weld + for ( l = iL_in; l <= iR_in; l++ ) { + int i = pTmpVert[l].index; + const int index = piTriList_in_and_out[i]; + const SVec3 vP = GetPosition( pContext, index ); + const SVec3 vN = GetNormal( pContext, index ); + const SVec3 vT = GetTexCoord( pContext, index ); + + tbool bNotFound = TTRUE; + int l2 = iL_in, i2rec = -1; + while ( l2 < l && bNotFound ) { + const int i2 = pTmpVert[l2].index; + const int index2 = piTriList_in_and_out[i2]; + const SVec3 vP2 = GetPosition( pContext, index2 ); + const SVec3 vN2 = GetNormal( pContext, index2 ); + const SVec3 vT2 = GetTexCoord( pContext, index2 ); + i2rec = i2; + + // if (vP==vP2 && vN==vN2 && vT==vT2) + if ( vP.x == vP2.x && vP.y == vP2.y && vP.z == vP2.z && vN.x == vN2.x && + vN.y == vN2.y && vN.z == vN2.z && vT.x == vT2.x && vT.y == vT2.y && + vT.z == vT2.z ) + bNotFound = TFALSE; + else + ++l2; + } + + // merge if previously found + if ( !bNotFound ) piTriList_in_and_out[i] = piTriList_in_and_out[i2rec]; + } + } + else { + int iL = iL_in, iR = iR_in; + assert( ( iR_in - iL_in ) > 0 ); // at least 2 entries + + // separate (by fSep) all points between iL_in and iR_in in pTmpVert[] + while ( iL < iR ) { + tbool bReadyLeftSwap = TFALSE, bReadyRightSwap = TFALSE; + while ( ( !bReadyLeftSwap ) && iL < iR ) { + assert( iL >= iL_in && iL <= iR_in ); + bReadyLeftSwap = !( pTmpVert[iL].vert[channel] < fSep ); + if ( !bReadyLeftSwap ) ++iL; + } + while ( ( !bReadyRightSwap ) && iL < iR ) { + assert( iR >= iL_in && iR <= iR_in ); + bReadyRightSwap = pTmpVert[iR].vert[channel] < fSep; + if ( !bReadyRightSwap ) --iR; + } + assert( ( iL < iR ) || !( bReadyLeftSwap && bReadyRightSwap ) ); + + if ( bReadyLeftSwap && bReadyRightSwap ) { + const STmpVert sTmp = pTmpVert[iL]; + assert( iL < iR ); + pTmpVert[iL] = pTmpVert[iR]; + pTmpVert[iR] = sTmp; + ++iL; + --iR; + } + } + + assert( iL == ( iR + 1 ) || ( iL == iR ) ); + if ( iL == iR ) { + const tbool bReadyRightSwap = pTmpVert[iR].vert[channel] < fSep; + if ( bReadyRightSwap ) + ++iL; + else + --iR; + } + + // only need to weld when there is more than 1 instance of the (x,y,z) + if ( iL_in < iR ) + MergeVertsFast( + piTriList_in_and_out, pTmpVert, pContext, iL_in, iR ); // weld all left of fSep + if ( iL < iR_in ) + MergeVertsFast( piTriList_in_and_out, + pTmpVert, + pContext, + iL, + iR_in ); // weld all right of (or equal to) fSep + } +} + +static void MergeVertsSlow( int piTriList_in_and_out[], + const SMikkTSpaceContext* pContext, + const int pTable[], + const int iEntries ) { + // this can be optimized further using a tree structure or more hashing. + int e = 0; + for ( e = 0; e < iEntries; e++ ) { + int i = pTable[e]; + const int index = piTriList_in_and_out[i]; + const SVec3 vP = GetPosition( pContext, index ); + const SVec3 vN = GetNormal( pContext, index ); + const SVec3 vT = GetTexCoord( pContext, index ); + + tbool bNotFound = TTRUE; + int e2 = 0, i2rec = -1; + while ( e2 < e && bNotFound ) { + const int i2 = pTable[e2]; + const int index2 = piTriList_in_and_out[i2]; + const SVec3 vP2 = GetPosition( pContext, index2 ); + const SVec3 vN2 = GetNormal( pContext, index2 ); + const SVec3 vT2 = GetTexCoord( pContext, index2 ); + i2rec = i2; + + if ( veq( vP, vP2 ) && veq( vN, vN2 ) && veq( vT, vT2 ) ) + bNotFound = TFALSE; + else + ++e2; + } + + // merge if previously found + if ( !bNotFound ) piTriList_in_and_out[i] = piTriList_in_and_out[i2rec]; + } +} + +static void GenerateSharedVerticesIndexListSlow( int piTriList_in_and_out[], + const SMikkTSpaceContext* pContext, + const int iNrTrianglesIn ) { + int iNumUniqueVerts = 0, t = 0, i = 0; + for ( t = 0; t < iNrTrianglesIn; t++ ) { + for ( i = 0; i < 3; i++ ) { + const int offs = t * 3 + i; + const int index = piTriList_in_and_out[offs]; + + const SVec3 vP = GetPosition( pContext, index ); + const SVec3 vN = GetNormal( pContext, index ); + const SVec3 vT = GetTexCoord( pContext, index ); + + tbool bFound = TFALSE; + int t2 = 0, index2rec = -1; + while ( !bFound && t2 <= t ) { + int j = 0; + while ( !bFound && j < 3 ) { + const int index2 = piTriList_in_and_out[t2 * 3 + j]; + const SVec3 vP2 = GetPosition( pContext, index2 ); + const SVec3 vN2 = GetNormal( pContext, index2 ); + const SVec3 vT2 = GetTexCoord( pContext, index2 ); + + if ( veq( vP, vP2 ) && veq( vN, vN2 ) && veq( vT, vT2 ) ) + bFound = TTRUE; + else + ++j; + } + if ( !bFound ) ++t2; + } + + assert( bFound ); + // if we found our own + if ( index2rec == index ) { ++iNumUniqueVerts; } + + piTriList_in_and_out[offs] = index2rec; + } + } +} + +static int GenerateInitialVerticesIndexList( STriInfo pTriInfos[], + int piTriList_out[], + const SMikkTSpaceContext* pContext, + const int iNrTrianglesIn ) { + int iTSpacesOffs = 0, f = 0, t = 0; + int iDstTriIndex = 0; + for ( f = 0; f < pContext->m_pInterface->m_getNumFaces( pContext ); f++ ) { + const int verts = pContext->m_pInterface->m_getNumVerticesOfFace( pContext, f ); + if ( verts != 3 && verts != 4 ) continue; + + pTriInfos[iDstTriIndex].iOrgFaceNumber = f; + pTriInfos[iDstTriIndex].iTSpacesOffs = iTSpacesOffs; + + if ( verts == 3 ) { + unsigned char* pVerts = pTriInfos[iDstTriIndex].vert_num; + pVerts[0] = 0; + pVerts[1] = 1; + pVerts[2] = 2; + piTriList_out[iDstTriIndex * 3 + 0] = MakeIndex( f, 0 ); + piTriList_out[iDstTriIndex * 3 + 1] = MakeIndex( f, 1 ); + piTriList_out[iDstTriIndex * 3 + 2] = MakeIndex( f, 2 ); + ++iDstTriIndex; // next + } + else { + { + pTriInfos[iDstTriIndex + 1].iOrgFaceNumber = f; + pTriInfos[iDstTriIndex + 1].iTSpacesOffs = iTSpacesOffs; + } + + { + // need an order independent way to evaluate + // tspace on quads. This is done by splitting + // along the shortest diagonal. + const int i0 = MakeIndex( f, 0 ); + const int i1 = MakeIndex( f, 1 ); + const int i2 = MakeIndex( f, 2 ); + const int i3 = MakeIndex( f, 3 ); + const SVec3 T0 = GetTexCoord( pContext, i0 ); + const SVec3 T1 = GetTexCoord( pContext, i1 ); + const SVec3 T2 = GetTexCoord( pContext, i2 ); + const SVec3 T3 = GetTexCoord( pContext, i3 ); + const float distSQ_02 = LengthSquared( vsub( T2, T0 ) ); + const float distSQ_13 = LengthSquared( vsub( T3, T1 ) ); + tbool bQuadDiagIs_02; + if ( distSQ_02 < distSQ_13 ) + bQuadDiagIs_02 = TTRUE; + else if ( distSQ_13 < distSQ_02 ) + bQuadDiagIs_02 = TFALSE; + else { + const SVec3 P0 = GetPosition( pContext, i0 ); + const SVec3 P1 = GetPosition( pContext, i1 ); + const SVec3 P2 = GetPosition( pContext, i2 ); + const SVec3 P3 = GetPosition( pContext, i3 ); + const float distSQ_02 = LengthSquared( vsub( P2, P0 ) ); + const float distSQ_13 = LengthSquared( vsub( P3, P1 ) ); + + bQuadDiagIs_02 = distSQ_13 < distSQ_02 ? TFALSE : TTRUE; + } + + if ( bQuadDiagIs_02 ) { + { + unsigned char* pVerts_A = pTriInfos[iDstTriIndex].vert_num; + pVerts_A[0] = 0; + pVerts_A[1] = 1; + pVerts_A[2] = 2; + } + piTriList_out[iDstTriIndex * 3 + 0] = i0; + piTriList_out[iDstTriIndex * 3 + 1] = i1; + piTriList_out[iDstTriIndex * 3 + 2] = i2; + ++iDstTriIndex; // next + { + unsigned char* pVerts_B = pTriInfos[iDstTriIndex].vert_num; + pVerts_B[0] = 0; + pVerts_B[1] = 2; + pVerts_B[2] = 3; + } + piTriList_out[iDstTriIndex * 3 + 0] = i0; + piTriList_out[iDstTriIndex * 3 + 1] = i2; + piTriList_out[iDstTriIndex * 3 + 2] = i3; + ++iDstTriIndex; // next + } + else { + { + unsigned char* pVerts_A = pTriInfos[iDstTriIndex].vert_num; + pVerts_A[0] = 0; + pVerts_A[1] = 1; + pVerts_A[2] = 3; + } + piTriList_out[iDstTriIndex * 3 + 0] = i0; + piTriList_out[iDstTriIndex * 3 + 1] = i1; + piTriList_out[iDstTriIndex * 3 + 2] = i3; + ++iDstTriIndex; // next + { + unsigned char* pVerts_B = pTriInfos[iDstTriIndex].vert_num; + pVerts_B[0] = 1; + pVerts_B[1] = 2; + pVerts_B[2] = 3; + } + piTriList_out[iDstTriIndex * 3 + 0] = i1; + piTriList_out[iDstTriIndex * 3 + 1] = i2; + piTriList_out[iDstTriIndex * 3 + 2] = i3; + ++iDstTriIndex; // next + } + } + } + + iTSpacesOffs += verts; + assert( iDstTriIndex <= iNrTrianglesIn ); + } + + for ( t = 0; t < iNrTrianglesIn; t++ ) + pTriInfos[t].iFlag = 0; + + // return total amount of tspaces + return iTSpacesOffs; +} + +static SVec3 GetPosition( const SMikkTSpaceContext* pContext, const int index ) { + int iF, iI; + SVec3 res; + float pos[3]; + IndexToData( &iF, &iI, index ); + pContext->m_pInterface->m_getPosition( pContext, pos, iF, iI ); + res.x = pos[0]; + res.y = pos[1]; + res.z = pos[2]; + return res; +} + +static SVec3 GetNormal( const SMikkTSpaceContext* pContext, const int index ) { + int iF, iI; + SVec3 res; + float norm[3]; + IndexToData( &iF, &iI, index ); + pContext->m_pInterface->m_getNormal( pContext, norm, iF, iI ); + res.x = norm[0]; + res.y = norm[1]; + res.z = norm[2]; + return res; +} + +static SVec3 GetTexCoord( const SMikkTSpaceContext* pContext, const int index ) { + int iF, iI; + SVec3 res; + float texc[2]; + IndexToData( &iF, &iI, index ); + pContext->m_pInterface->m_getTexCoord( pContext, texc, iF, iI ); + res.x = texc[0]; + res.y = texc[1]; + res.z = 1.0f; + return res; +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////// + +typedef union { + struct { + int i0, i1, f; + }; + int array[3]; +} SEdge; + +static void BuildNeighborsFast( STriInfo pTriInfos[], + SEdge* pEdges, + const int piTriListIn[], + const int iNrTrianglesIn ); +static void +BuildNeighborsSlow( STriInfo pTriInfos[], const int piTriListIn[], const int iNrTrianglesIn ); + +// returns the texture area times 2 +static float CalcTexArea( const SMikkTSpaceContext* pContext, const int indices[] ) { + const SVec3 t1 = GetTexCoord( pContext, indices[0] ); + const SVec3 t2 = GetTexCoord( pContext, indices[1] ); + const SVec3 t3 = GetTexCoord( pContext, indices[2] ); + + const float t21x = t2.x - t1.x; + const float t21y = t2.y - t1.y; + const float t31x = t3.x - t1.x; + const float t31y = t3.y - t1.y; + + const float fSignedAreaSTx2 = t21x * t31y - t21y * t31x; + + return fSignedAreaSTx2 < 0 ? ( -fSignedAreaSTx2 ) : fSignedAreaSTx2; +} + +static void InitTriInfo( STriInfo pTriInfos[], + const int piTriListIn[], + const SMikkTSpaceContext* pContext, + const int iNrTrianglesIn ) { + int f = 0, i = 0, t = 0; + // pTriInfos[f].iFlag is cleared in GenerateInitialVerticesIndexList() which is called before + // this function. + + // generate neighbor info list + for ( f = 0; f < iNrTrianglesIn; f++ ) + for ( i = 0; i < 3; i++ ) { + pTriInfos[f].FaceNeighbors[i] = -1; + pTriInfos[f].AssignedGroup[i] = NULL; + + pTriInfos[f].vOs.x = 0.0f; + pTriInfos[f].vOs.y = 0.0f; + pTriInfos[f].vOs.z = 0.0f; + pTriInfos[f].vOt.x = 0.0f; + pTriInfos[f].vOt.y = 0.0f; + pTriInfos[f].vOt.z = 0.0f; + pTriInfos[f].fMagS = 0; + pTriInfos[f].fMagT = 0; + + // assumed bad + pTriInfos[f].iFlag |= GROUP_WITH_ANY; + } + + // evaluate first order derivatives + for ( f = 0; f < iNrTrianglesIn; f++ ) { + // initial values + const SVec3 v1 = GetPosition( pContext, piTriListIn[f * 3 + 0] ); + const SVec3 v2 = GetPosition( pContext, piTriListIn[f * 3 + 1] ); + const SVec3 v3 = GetPosition( pContext, piTriListIn[f * 3 + 2] ); + const SVec3 t1 = GetTexCoord( pContext, piTriListIn[f * 3 + 0] ); + const SVec3 t2 = GetTexCoord( pContext, piTriListIn[f * 3 + 1] ); + const SVec3 t3 = GetTexCoord( pContext, piTriListIn[f * 3 + 2] ); + + const float t21x = t2.x - t1.x; + const float t21y = t2.y - t1.y; + const float t31x = t3.x - t1.x; + const float t31y = t3.y - t1.y; + const SVec3 d1 = vsub( v2, v1 ); + const SVec3 d2 = vsub( v3, v1 ); + + const float fSignedAreaSTx2 = t21x * t31y - t21y * t31x; + // assert(fSignedAreaSTx2!=0); + SVec3 vOs = vsub( vscale( t31y, d1 ), vscale( t21y, d2 ) ); // eq 18 + SVec3 vOt = vadd( vscale( -t31x, d1 ), vscale( t21x, d2 ) ); // eq 19 + + pTriInfos[f].iFlag |= ( fSignedAreaSTx2 > 0 ? ORIENT_PRESERVING : 0 ); + + if ( NotZero( fSignedAreaSTx2 ) ) { + const float fAbsArea = fabsf( fSignedAreaSTx2 ); + const float fLenOs = Length( vOs ); + const float fLenOt = Length( vOt ); + const float fS = ( pTriInfos[f].iFlag & ORIENT_PRESERVING ) == 0 ? ( -1.0f ) : 1.0f; + if ( NotZero( fLenOs ) ) pTriInfos[f].vOs = vscale( fS / fLenOs, vOs ); + if ( NotZero( fLenOt ) ) pTriInfos[f].vOt = vscale( fS / fLenOt, vOt ); + + // evaluate magnitudes prior to normalization of vOs and vOt + pTriInfos[f].fMagS = fLenOs / fAbsArea; + pTriInfos[f].fMagT = fLenOt / fAbsArea; + + // if this is a good triangle + if ( NotZero( pTriInfos[f].fMagS ) && NotZero( pTriInfos[f].fMagT ) ) + pTriInfos[f].iFlag &= ( ~GROUP_WITH_ANY ); + } + } + + // force otherwise healthy quads to a fixed orientation + while ( t < ( iNrTrianglesIn - 1 ) ) { + const int iFO_a = pTriInfos[t].iOrgFaceNumber; + const int iFO_b = pTriInfos[t + 1].iOrgFaceNumber; + if ( iFO_a == iFO_b ) // this is a quad + { + const tbool bIsDeg_a = ( pTriInfos[t].iFlag & MARK_DEGENERATE ) != 0 ? TTRUE : TFALSE; + const tbool bIsDeg_b = + ( pTriInfos[t + 1].iFlag & MARK_DEGENERATE ) != 0 ? TTRUE : TFALSE; + + // bad triangles should already have been removed by + // DegenPrologue(), but just in case check bIsDeg_a and bIsDeg_a are false + if ( ( bIsDeg_a || bIsDeg_b ) == TFALSE ) { + const tbool bOrientA = + ( pTriInfos[t].iFlag & ORIENT_PRESERVING ) != 0 ? TTRUE : TFALSE; + const tbool bOrientB = + ( pTriInfos[t + 1].iFlag & ORIENT_PRESERVING ) != 0 ? TTRUE : TFALSE; + // if this happens the quad has extremely bad mapping!! + if ( bOrientA != bOrientB ) { + // printf("found quad with bad mapping\n"); + tbool bChooseOrientFirstTri = TFALSE; + if ( ( pTriInfos[t + 1].iFlag & GROUP_WITH_ANY ) != 0 ) + bChooseOrientFirstTri = TTRUE; + else if ( CalcTexArea( pContext, &piTriListIn[t * 3 + 0] ) >= + CalcTexArea( pContext, &piTriListIn[( t + 1 ) * 3 + 0] ) ) + bChooseOrientFirstTri = TTRUE; + + // force match + { + const int t0 = bChooseOrientFirstTri ? t : ( t + 1 ); + const int t1 = bChooseOrientFirstTri ? ( t + 1 ) : t; + pTriInfos[t1].iFlag &= ( ~ORIENT_PRESERVING ); // clear first + pTriInfos[t1].iFlag |= + ( pTriInfos[t0].iFlag & ORIENT_PRESERVING ); // copy bit + } + } + } + t += 2; + } + else + ++t; + } + + // match up edge pairs + { + SEdge* pEdges = (SEdge*)malloc( sizeof( SEdge ) * iNrTrianglesIn * 3 ); + if ( pEdges == NULL ) + BuildNeighborsSlow( pTriInfos, piTriListIn, iNrTrianglesIn ); + else { + BuildNeighborsFast( pTriInfos, pEdges, piTriListIn, iNrTrianglesIn ); + + free( pEdges ); + } + } +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////// + +static tbool AssignRecur( const int piTriListIn[], + STriInfo psTriInfos[], + const int iMyTriIndex, + SGroup* pGroup ); +static void AddTriToGroup( SGroup* pGroup, const int iTriIndex ); + +static int Build4RuleGroups( STriInfo pTriInfos[], + SGroup pGroups[], + int piGroupTrianglesBuffer[], + const int piTriListIn[], + const int iNrTrianglesIn ) { + const int iNrMaxGroups = iNrTrianglesIn * 3; + int iNrActiveGroups = 0; + int iOffset = 0, f = 0, i = 0; + (void)iNrMaxGroups; /* quiet warnings in non debug mode */ + for ( f = 0; f < iNrTrianglesIn; f++ ) { + for ( i = 0; i < 3; i++ ) { + // if not assigned to a group + if ( ( pTriInfos[f].iFlag & GROUP_WITH_ANY ) == 0 && + pTriInfos[f].AssignedGroup[i] == NULL ) { + tbool bOrPre; + int neigh_indexL, neigh_indexR; + const int vert_index = piTriListIn[f * 3 + i]; + assert( iNrActiveGroups < iNrMaxGroups ); + pTriInfos[f].AssignedGroup[i] = &pGroups[iNrActiveGroups]; + pTriInfos[f].AssignedGroup[i]->iVertexRepresentitive = vert_index; + pTriInfos[f].AssignedGroup[i]->bOrientPreservering = + ( pTriInfos[f].iFlag & ORIENT_PRESERVING ) != 0; + pTriInfos[f].AssignedGroup[i]->iNrFaces = 0; + pTriInfos[f].AssignedGroup[i]->pFaceIndices = &piGroupTrianglesBuffer[iOffset]; + ++iNrActiveGroups; + + AddTriToGroup( pTriInfos[f].AssignedGroup[i], f ); + bOrPre = ( pTriInfos[f].iFlag & ORIENT_PRESERVING ) != 0 ? TTRUE : TFALSE; + neigh_indexL = pTriInfos[f].FaceNeighbors[i]; + neigh_indexR = pTriInfos[f].FaceNeighbors[i > 0 ? ( i - 1 ) : 2]; + if ( neigh_indexL >= 0 ) // neighbor + { + const tbool bAnswer = AssignRecur( + piTriListIn, pTriInfos, neigh_indexL, pTriInfos[f].AssignedGroup[i] ); + + const tbool bOrPre2 = + ( pTriInfos[neigh_indexL].iFlag & ORIENT_PRESERVING ) != 0 ? TTRUE : TFALSE; + const tbool bDiff = bOrPre != bOrPre2 ? TTRUE : TFALSE; + assert( bAnswer || bDiff ); + (void)bAnswer, (void)bDiff; /* quiet warnings in non debug mode */ + } + if ( neigh_indexR >= 0 ) // neighbor + { + const tbool bAnswer = AssignRecur( + piTriListIn, pTriInfos, neigh_indexR, pTriInfos[f].AssignedGroup[i] ); + + const tbool bOrPre2 = + ( pTriInfos[neigh_indexR].iFlag & ORIENT_PRESERVING ) != 0 ? TTRUE : TFALSE; + const tbool bDiff = bOrPre != bOrPre2 ? TTRUE : TFALSE; + assert( bAnswer || bDiff ); + (void)bAnswer, (void)bDiff; /* quiet warnings in non debug mode */ + } + + // update offset + iOffset += pTriInfos[f].AssignedGroup[i]->iNrFaces; + // since the groups are disjoint a triangle can never + // belong to more than 3 groups. Subsequently something + // is completely screwed if this assertion ever hits. + assert( iOffset <= iNrMaxGroups ); + } + } + } + + return iNrActiveGroups; +} + +static void AddTriToGroup( SGroup* pGroup, const int iTriIndex ) { + pGroup->pFaceIndices[pGroup->iNrFaces] = iTriIndex; + ++pGroup->iNrFaces; +} + +static tbool AssignRecur( const int piTriListIn[], + STriInfo psTriInfos[], + const int iMyTriIndex, + SGroup* pGroup ) { + STriInfo* pMyTriInfo = &psTriInfos[iMyTriIndex]; + + // track down vertex + const int iVertRep = pGroup->iVertexRepresentitive; + const int* pVerts = &piTriListIn[3 * iMyTriIndex + 0]; + int i = -1; + if ( pVerts[0] == iVertRep ) + i = 0; + else if ( pVerts[1] == iVertRep ) + i = 1; + else if ( pVerts[2] == iVertRep ) + i = 2; + assert( i >= 0 && i < 3 ); + + // early out + if ( pMyTriInfo->AssignedGroup[i] == pGroup ) + return TTRUE; + else if ( pMyTriInfo->AssignedGroup[i] != NULL ) + return TFALSE; + if ( ( pMyTriInfo->iFlag & GROUP_WITH_ANY ) != 0 ) { + // first to group with a group-with-anything triangle + // determines it's orientation. + // This is the only existing order dependency in the code!! + if ( pMyTriInfo->AssignedGroup[0] == NULL && pMyTriInfo->AssignedGroup[1] == NULL && + pMyTriInfo->AssignedGroup[2] == NULL ) { + pMyTriInfo->iFlag &= ( ~ORIENT_PRESERVING ); + pMyTriInfo->iFlag |= ( pGroup->bOrientPreservering ? ORIENT_PRESERVING : 0 ); + } + } + { + const tbool bOrient = ( pMyTriInfo->iFlag & ORIENT_PRESERVING ) != 0 ? TTRUE : TFALSE; + if ( bOrient != pGroup->bOrientPreservering ) return TFALSE; + } + + AddTriToGroup( pGroup, iMyTriIndex ); + pMyTriInfo->AssignedGroup[i] = pGroup; + + { + const int neigh_indexL = pMyTriInfo->FaceNeighbors[i]; + const int neigh_indexR = pMyTriInfo->FaceNeighbors[i > 0 ? ( i - 1 ) : 2]; + if ( neigh_indexL >= 0 ) AssignRecur( piTriListIn, psTriInfos, neigh_indexL, pGroup ); + if ( neigh_indexR >= 0 ) AssignRecur( piTriListIn, psTriInfos, neigh_indexR, pGroup ); + } + + return TTRUE; +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////// + +static tbool CompareSubGroups( const SSubGroup* pg1, const SSubGroup* pg2 ); +static void QuickSort( int* pSortBuffer, int iLeft, int iRight, unsigned int uSeed ); +static STSpace EvalTspace( int face_indices[], + const int iFaces, + const int piTriListIn[], + const STriInfo pTriInfos[], + const SMikkTSpaceContext* pContext, + const int iVertexRepresentitive ); + +static tbool GenerateTSpaces( STSpace psTspace[], + const STriInfo pTriInfos[], + const SGroup pGroups[], + const int iNrActiveGroups, + const int piTriListIn[], + const float fThresCos, + const SMikkTSpaceContext* pContext ) { + STSpace* pSubGroupTspace = NULL; + SSubGroup* pUniSubGroups = NULL; + int* pTmpMembers = NULL; + int iMaxNrFaces = 0, iUniqueTspaces = 0, g = 0, i = 0; + for ( g = 0; g < iNrActiveGroups; g++ ) + if ( iMaxNrFaces < pGroups[g].iNrFaces ) iMaxNrFaces = pGroups[g].iNrFaces; + + if ( iMaxNrFaces == 0 ) return TTRUE; + + // make initial allocations + pSubGroupTspace = (STSpace*)malloc( sizeof( STSpace ) * iMaxNrFaces ); + pUniSubGroups = (SSubGroup*)malloc( sizeof( SSubGroup ) * iMaxNrFaces ); + pTmpMembers = (int*)malloc( sizeof( int ) * iMaxNrFaces ); + if ( pSubGroupTspace == NULL || pUniSubGroups == NULL || pTmpMembers == NULL ) { + if ( pSubGroupTspace != NULL ) free( pSubGroupTspace ); + if ( pUniSubGroups != NULL ) free( pUniSubGroups ); + if ( pTmpMembers != NULL ) free( pTmpMembers ); + return TFALSE; + } + + iUniqueTspaces = 0; + for ( g = 0; g < iNrActiveGroups; g++ ) { + const SGroup* pGroup = &pGroups[g]; + int iUniqueSubGroups = 0, s = 0; + + for ( i = 0; i < pGroup->iNrFaces; i++ ) // triangles + { + const int f = pGroup->pFaceIndices[i]; // triangle number + int index = -1, iVertIndex = -1, iOF_1 = -1, iMembers = 0, j = 0, l = 0; + SSubGroup tmp_group; + tbool bFound; + SVec3 n, vOs, vOt; + if ( pTriInfos[f].AssignedGroup[0] == pGroup ) + index = 0; + else if ( pTriInfos[f].AssignedGroup[1] == pGroup ) + index = 1; + else if ( pTriInfos[f].AssignedGroup[2] == pGroup ) + index = 2; + assert( index >= 0 && index < 3 ); + + iVertIndex = piTriListIn[f * 3 + index]; + assert( iVertIndex == pGroup->iVertexRepresentitive ); + + // is normalized already + n = GetNormal( pContext, iVertIndex ); + + // project + vOs = vsub( pTriInfos[f].vOs, vscale( vdot( n, pTriInfos[f].vOs ), n ) ); + vOt = vsub( pTriInfos[f].vOt, vscale( vdot( n, pTriInfos[f].vOt ), n ) ); + if ( VNotZero( vOs ) ) vOs = Normalize( vOs ); + if ( VNotZero( vOt ) ) vOt = Normalize( vOt ); + + // original face number + iOF_1 = pTriInfos[f].iOrgFaceNumber; + + iMembers = 0; + for ( j = 0; j < pGroup->iNrFaces; j++ ) { + const int t = pGroup->pFaceIndices[j]; // triangle number + const int iOF_2 = pTriInfos[t].iOrgFaceNumber; + + // project + SVec3 vOs2 = vsub( pTriInfos[t].vOs, vscale( vdot( n, pTriInfos[t].vOs ), n ) ); + SVec3 vOt2 = vsub( pTriInfos[t].vOt, vscale( vdot( n, pTriInfos[t].vOt ), n ) ); + if ( VNotZero( vOs2 ) ) vOs2 = Normalize( vOs2 ); + if ( VNotZero( vOt2 ) ) vOt2 = Normalize( vOt2 ); + + { + const tbool bAny = + ( ( pTriInfos[f].iFlag | pTriInfos[t].iFlag ) & GROUP_WITH_ANY ) != 0 + ? TTRUE + : TFALSE; + // make sure triangles which belong to the same quad are joined. + const tbool bSameOrgFace = iOF_1 == iOF_2 ? TTRUE : TFALSE; + + const float fCosS = vdot( vOs, vOs2 ); + const float fCosT = vdot( vOt, vOt2 ); + + assert( f != t || bSameOrgFace ); // sanity check + if ( bAny || bSameOrgFace || ( fCosS > fThresCos && fCosT > fThresCos ) ) + pTmpMembers[iMembers++] = t; + } + } + + // sort pTmpMembers + tmp_group.iNrFaces = iMembers; + tmp_group.pTriMembers = pTmpMembers; + if ( iMembers > 1 ) { + unsigned int uSeed = INTERNAL_RND_SORT_SEED; // could replace with a random seed? + QuickSort( pTmpMembers, 0, iMembers - 1, uSeed ); + } + + // look for an existing match + bFound = TFALSE; + l = 0; + while ( l < iUniqueSubGroups && !bFound ) { + bFound = CompareSubGroups( &tmp_group, &pUniSubGroups[l] ); + if ( !bFound ) ++l; + } + + // assign tangent space index + assert( bFound || l == iUniqueSubGroups ); + // piTempTangIndices[f*3+index] = iUniqueTspaces+l; + + // if no match was found we allocate a new subgroup + if ( !bFound ) { + // insert new subgroup + int* pIndices = (int*)malloc( sizeof( int ) * iMembers ); + if ( pIndices == NULL ) { + // clean up and return false + int s = 0; + for ( s = 0; s < iUniqueSubGroups; s++ ) + free( pUniSubGroups[s].pTriMembers ); + free( pUniSubGroups ); + free( pTmpMembers ); + free( pSubGroupTspace ); + return TFALSE; + } + pUniSubGroups[iUniqueSubGroups].iNrFaces = iMembers; + pUniSubGroups[iUniqueSubGroups].pTriMembers = pIndices; + memcpy( pIndices, tmp_group.pTriMembers, iMembers * sizeof( int ) ); + pSubGroupTspace[iUniqueSubGroups] = EvalTspace( tmp_group.pTriMembers, + iMembers, + piTriListIn, + pTriInfos, + pContext, + pGroup->iVertexRepresentitive ); + ++iUniqueSubGroups; + } + + // output tspace + { + const int iOffs = pTriInfos[f].iTSpacesOffs; + const int iVert = pTriInfos[f].vert_num[index]; + STSpace* pTS_out = &psTspace[iOffs + iVert]; + assert( pTS_out->iCounter < 2 ); + assert( ( ( pTriInfos[f].iFlag & ORIENT_PRESERVING ) != 0 ) == + pGroup->bOrientPreservering ); + if ( pTS_out->iCounter == 1 ) { + *pTS_out = AvgTSpace( pTS_out, &pSubGroupTspace[l] ); + pTS_out->iCounter = 2; // update counter + pTS_out->bOrient = pGroup->bOrientPreservering; + } + else { + assert( pTS_out->iCounter == 0 ); + *pTS_out = pSubGroupTspace[l]; + pTS_out->iCounter = 1; // update counter + pTS_out->bOrient = pGroup->bOrientPreservering; + } + } + } + + // clean up and offset iUniqueTspaces + for ( s = 0; s < iUniqueSubGroups; s++ ) + free( pUniSubGroups[s].pTriMembers ); + iUniqueTspaces += iUniqueSubGroups; + } + + // clean up + free( pUniSubGroups ); + free( pTmpMembers ); + free( pSubGroupTspace ); + + return TTRUE; +} + +static STSpace EvalTspace( int face_indices[], + const int iFaces, + const int piTriListIn[], + const STriInfo pTriInfos[], + const SMikkTSpaceContext* pContext, + const int iVertexRepresentitive ) { + STSpace res; + float fAngleSum = 0; + int face = 0; + res.vOs.x = 0.0f; + res.vOs.y = 0.0f; + res.vOs.z = 0.0f; + res.vOt.x = 0.0f; + res.vOt.y = 0.0f; + res.vOt.z = 0.0f; + res.fMagS = 0; + res.fMagT = 0; + + for ( face = 0; face < iFaces; face++ ) { + const int f = face_indices[face]; + + // only valid triangles get to add their contribution + if ( ( pTriInfos[f].iFlag & GROUP_WITH_ANY ) == 0 ) { + SVec3 n, vOs, vOt, p0, p1, p2, v1, v2; + float fCos, fAngle, fMagS, fMagT; + int i = -1, index = -1, i0 = -1, i1 = -1, i2 = -1; + if ( piTriListIn[3 * f + 0] == iVertexRepresentitive ) + i = 0; + else if ( piTriListIn[3 * f + 1] == iVertexRepresentitive ) + i = 1; + else if ( piTriListIn[3 * f + 2] == iVertexRepresentitive ) + i = 2; + assert( i >= 0 && i < 3 ); + + // project + index = piTriListIn[3 * f + i]; + n = GetNormal( pContext, index ); + vOs = vsub( pTriInfos[f].vOs, vscale( vdot( n, pTriInfos[f].vOs ), n ) ); + vOt = vsub( pTriInfos[f].vOt, vscale( vdot( n, pTriInfos[f].vOt ), n ) ); + if ( VNotZero( vOs ) ) vOs = Normalize( vOs ); + if ( VNotZero( vOt ) ) vOt = Normalize( vOt ); + + i2 = piTriListIn[3 * f + ( i < 2 ? ( i + 1 ) : 0 )]; + i1 = piTriListIn[3 * f + i]; + i0 = piTriListIn[3 * f + ( i > 0 ? ( i - 1 ) : 2 )]; + + p0 = GetPosition( pContext, i0 ); + p1 = GetPosition( pContext, i1 ); + p2 = GetPosition( pContext, i2 ); + v1 = vsub( p0, p1 ); + v2 = vsub( p2, p1 ); + + // project + v1 = vsub( v1, vscale( vdot( n, v1 ), n ) ); + if ( VNotZero( v1 ) ) v1 = Normalize( v1 ); + v2 = vsub( v2, vscale( vdot( n, v2 ), n ) ); + if ( VNotZero( v2 ) ) v2 = Normalize( v2 ); + + // weight contribution by the angle + // between the two edge vectors + fCos = vdot( v1, v2 ); + fCos = fCos > 1 ? 1 : ( fCos < ( -1 ) ? ( -1 ) : fCos ); + fAngle = (float)acos( fCos ); + fMagS = pTriInfos[f].fMagS; + fMagT = pTriInfos[f].fMagT; + + res.vOs = vadd( res.vOs, vscale( fAngle, vOs ) ); + res.vOt = vadd( res.vOt, vscale( fAngle, vOt ) ); + res.fMagS += ( fAngle * fMagS ); + res.fMagT += ( fAngle * fMagT ); + fAngleSum += fAngle; + } + } + + // normalize + if ( VNotZero( res.vOs ) ) res.vOs = Normalize( res.vOs ); + if ( VNotZero( res.vOt ) ) res.vOt = Normalize( res.vOt ); + if ( fAngleSum > 0 ) { + res.fMagS /= fAngleSum; + res.fMagT /= fAngleSum; + } + + return res; +} + +static tbool CompareSubGroups( const SSubGroup* pg1, const SSubGroup* pg2 ) { + tbool bStillSame = TTRUE; + int i = 0; + if ( pg1->iNrFaces != pg2->iNrFaces ) return TFALSE; + while ( i < pg1->iNrFaces && bStillSame ) { + bStillSame = pg1->pTriMembers[i] == pg2->pTriMembers[i] ? TTRUE : TFALSE; + if ( bStillSame ) ++i; + } + return bStillSame; +} + +static void QuickSort( int* pSortBuffer, int iLeft, int iRight, unsigned int uSeed ) { + int iL, iR, n, index, iMid, iTmp; + + // Random + unsigned int t = uSeed & 31; + t = ( uSeed << t ) | ( uSeed >> ( 32 - t ) ); + uSeed = uSeed + t + 3; + // Random end + + iL = iLeft; + iR = iRight; + n = ( iR - iL ) + 1; + assert( n >= 0 ); + index = (int)( uSeed % n ); + + iMid = pSortBuffer[index + iL]; + + do { + while ( pSortBuffer[iL] < iMid ) + ++iL; + while ( pSortBuffer[iR] > iMid ) + --iR; + + if ( iL <= iR ) { + iTmp = pSortBuffer[iL]; + pSortBuffer[iL] = pSortBuffer[iR]; + pSortBuffer[iR] = iTmp; + ++iL; + --iR; + } + } while ( iL <= iR ); + + if ( iLeft < iR ) QuickSort( pSortBuffer, iLeft, iR, uSeed ); + if ( iL < iRight ) QuickSort( pSortBuffer, iL, iRight, uSeed ); +} + +///////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////// + +static void +QuickSortEdges( SEdge* pSortBuffer, int iLeft, int iRight, const int channel, unsigned int uSeed ); +static void GetEdge( int* i0_out, + int* i1_out, + int* edgenum_out, + const int indices[], + const int i0_in, + const int i1_in ); + +static void BuildNeighborsFast( STriInfo pTriInfos[], + SEdge* pEdges, + const int piTriListIn[], + const int iNrTrianglesIn ) { + // build array of edges + unsigned int uSeed = INTERNAL_RND_SORT_SEED; // could replace with a random seed? + int iEntries = 0, iCurStartIndex = -1, f = 0, i = 0; + for ( f = 0; f < iNrTrianglesIn; f++ ) + for ( i = 0; i < 3; i++ ) { + const int i0 = piTriListIn[f * 3 + i]; + const int i1 = piTriListIn[f * 3 + ( i < 2 ? ( i + 1 ) : 0 )]; + pEdges[f * 3 + i].i0 = i0 < i1 ? i0 : i1; // put minimum index in i0 + pEdges[f * 3 + i].i1 = !( i0 < i1 ) ? i0 : i1; // put maximum index in i1 + pEdges[f * 3 + i].f = f; // record face number + } + + // sort over all edges by i0, this is the pricy one. + QuickSortEdges( pEdges, 0, iNrTrianglesIn * 3 - 1, 0, uSeed ); // sort channel 0 which is i0 + + // sub sort over i1, should be fast. + // could replace this with a 64 bit int sort over (i0,i1) + // with i0 as msb in the quicksort call above. + iEntries = iNrTrianglesIn * 3; + iCurStartIndex = 0; + for ( i = 1; i < iEntries; i++ ) { + if ( pEdges[iCurStartIndex].i0 != pEdges[i].i0 ) { + const int iL = iCurStartIndex; + const int iR = i - 1; + // const int iElems = i-iL; + iCurStartIndex = i; + QuickSortEdges( pEdges, iL, iR, 1, uSeed ); // sort channel 1 which is i1 + } + } + + // sub sort over f, which should be fast. + // this step is to remain compliant with BuildNeighborsSlow() when + // more than 2 triangles use the same edge (such as a butterfly topology). + iCurStartIndex = 0; + for ( i = 1; i < iEntries; i++ ) { + if ( pEdges[iCurStartIndex].i0 != pEdges[i].i0 || + pEdges[iCurStartIndex].i1 != pEdges[i].i1 ) { + const int iL = iCurStartIndex; + const int iR = i - 1; + // const int iElems = i-iL; + iCurStartIndex = i; + QuickSortEdges( pEdges, iL, iR, 2, uSeed ); // sort channel 2 which is f + } + } + + // pair up, adjacent triangles + for ( i = 0; i < iEntries; i++ ) { + const int i0 = pEdges[i].i0; + const int i1 = pEdges[i].i1; + const int f = pEdges[i].f; + tbool bUnassigned_A; + + int i0_A, i1_A; + int edgenum_A, edgenum_B = 0; // 0,1 or 2 + GetEdge( &i0_A, + &i1_A, + &edgenum_A, + &piTriListIn[f * 3], + i0, + i1 ); // resolve index ordering and edge_num + bUnassigned_A = pTriInfos[f].FaceNeighbors[edgenum_A] == -1 ? TTRUE : TFALSE; + + if ( bUnassigned_A ) { + // get true index ordering + int j = i + 1, t; + tbool bNotFound = TTRUE; + while ( j < iEntries && i0 == pEdges[j].i0 && i1 == pEdges[j].i1 && bNotFound ) { + tbool bUnassigned_B; + int i0_B, i1_B; + t = pEdges[j].f; + // flip i0_B and i1_B + GetEdge( &i1_B, + &i0_B, + &edgenum_B, + &piTriListIn[t * 3], + pEdges[j].i0, + pEdges[j].i1 ); // resolve index ordering and edge_num + // assert(!(i0_A==i1_B && i1_A==i0_B)); + bUnassigned_B = pTriInfos[t].FaceNeighbors[edgenum_B] == -1 ? TTRUE : TFALSE; + if ( i0_A == i0_B && i1_A == i1_B && bUnassigned_B ) + bNotFound = TFALSE; + else + ++j; + } + + if ( !bNotFound ) { + int t = pEdges[j].f; + pTriInfos[f].FaceNeighbors[edgenum_A] = t; + // assert(pTriInfos[t].FaceNeighbors[edgenum_B]==-1); + pTriInfos[t].FaceNeighbors[edgenum_B] = f; + } + } + } +} + +static void +BuildNeighborsSlow( STriInfo pTriInfos[], const int piTriListIn[], const int iNrTrianglesIn ) { + int f = 0, i = 0; + for ( f = 0; f < iNrTrianglesIn; f++ ) { + for ( i = 0; i < 3; i++ ) { + // if unassigned + if ( pTriInfos[f].FaceNeighbors[i] == -1 ) { + const int i0_A = piTriListIn[f * 3 + i]; + const int i1_A = piTriListIn[f * 3 + ( i < 2 ? ( i + 1 ) : 0 )]; + + // search for a neighbor + tbool bFound = TFALSE; + int t = 0, j = 0; + while ( !bFound && t < iNrTrianglesIn ) { + if ( t != f ) { + j = 0; + while ( !bFound && j < 3 ) { + // in rev order + const int i1_B = piTriListIn[t * 3 + j]; + const int i0_B = piTriListIn[t * 3 + ( j < 2 ? ( j + 1 ) : 0 )]; + // assert(!(i0_A==i1_B && i1_A==i0_B)); + if ( i0_A == i0_B && i1_A == i1_B ) + bFound = TTRUE; + else + ++j; + } + } + + if ( !bFound ) ++t; + } + + // assign neighbors + if ( bFound ) { + pTriInfos[f].FaceNeighbors[i] = t; + // assert(pTriInfos[t].FaceNeighbors[j]==-1); + pTriInfos[t].FaceNeighbors[j] = f; + } + } + } + } +} + +static void +QuickSortEdges( SEdge* pSortBuffer, int iLeft, int iRight, const int channel, unsigned int uSeed ) { + unsigned int t; + int iL, iR, n, index, iMid; + + // early out + SEdge sTmp; + const int iElems = iRight - iLeft + 1; + if ( iElems < 2 ) + return; + else if ( iElems == 2 ) { + if ( pSortBuffer[iLeft].array[channel] > pSortBuffer[iRight].array[channel] ) { + sTmp = pSortBuffer[iLeft]; + pSortBuffer[iLeft] = pSortBuffer[iRight]; + pSortBuffer[iRight] = sTmp; + } + return; + } + + // Random + t = uSeed & 31; + t = ( uSeed << t ) | ( uSeed >> ( 32 - t ) ); + uSeed = uSeed + t + 3; + // Random end + + iL = iLeft, iR = iRight; + n = ( iR - iL ) + 1; + assert( n >= 0 ); + index = (int)( uSeed % n ); + + iMid = pSortBuffer[index + iL].array[channel]; + + do { + while ( pSortBuffer[iL].array[channel] < iMid ) + ++iL; + while ( pSortBuffer[iR].array[channel] > iMid ) + --iR; + + if ( iL <= iR ) { + sTmp = pSortBuffer[iL]; + pSortBuffer[iL] = pSortBuffer[iR]; + pSortBuffer[iR] = sTmp; + ++iL; + --iR; + } + } while ( iL <= iR ); + + if ( iLeft < iR ) QuickSortEdges( pSortBuffer, iLeft, iR, channel, uSeed ); + if ( iL < iRight ) QuickSortEdges( pSortBuffer, iL, iRight, channel, uSeed ); +} + +// resolve ordering and edge number +static void GetEdge( int* i0_out, + int* i1_out, + int* edgenum_out, + const int indices[], + const int i0_in, + const int i1_in ) { + *edgenum_out = -1; + + // test if first index is on the edge + if ( indices[0] == i0_in || indices[0] == i1_in ) { + // test if second index is on the edge + if ( indices[1] == i0_in || indices[1] == i1_in ) { + edgenum_out[0] = 0; // first edge + i0_out[0] = indices[0]; + i1_out[0] = indices[1]; + } + else { + edgenum_out[0] = 2; // third edge + i0_out[0] = indices[2]; + i1_out[0] = indices[0]; + } + } + else { + // only second and third index is on the edge + edgenum_out[0] = 1; // second edge + i0_out[0] = indices[1]; + i1_out[0] = indices[2]; + } +} + +///////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////// Degenerate triangles //////////////////////////////////// + +static void DegenPrologue( STriInfo pTriInfos[], + int piTriList_out[], + const int iNrTrianglesIn, + const int iTotTris ) { + int iNextGoodTriangleSearchIndex = -1; + tbool bStillFindingGoodOnes; + + // locate quads with only one good triangle + int t = 0; + while ( t < ( iTotTris - 1 ) ) { + const int iFO_a = pTriInfos[t].iOrgFaceNumber; + const int iFO_b = pTriInfos[t + 1].iOrgFaceNumber; + if ( iFO_a == iFO_b ) // this is a quad + { + const tbool bIsDeg_a = ( pTriInfos[t].iFlag & MARK_DEGENERATE ) != 0 ? TTRUE : TFALSE; + const tbool bIsDeg_b = + ( pTriInfos[t + 1].iFlag & MARK_DEGENERATE ) != 0 ? TTRUE : TFALSE; + if ( ( bIsDeg_a ^ bIsDeg_b ) != 0 ) { + pTriInfos[t].iFlag |= QUAD_ONE_DEGEN_TRI; + pTriInfos[t + 1].iFlag |= QUAD_ONE_DEGEN_TRI; + } + t += 2; + } + else + ++t; + } + + // reorder list so all degen triangles are moved to the back + // without reordering the good triangles + iNextGoodTriangleSearchIndex = 1; + t = 0; + bStillFindingGoodOnes = TTRUE; + while ( t < iNrTrianglesIn && bStillFindingGoodOnes ) { + const tbool bIsGood = ( pTriInfos[t].iFlag & MARK_DEGENERATE ) == 0 ? TTRUE : TFALSE; + if ( bIsGood ) { + if ( iNextGoodTriangleSearchIndex < ( t + 2 ) ) iNextGoodTriangleSearchIndex = t + 2; + } + else { + int t0, t1; + // search for the first good triangle. + tbool bJustADegenerate = TTRUE; + while ( bJustADegenerate && iNextGoodTriangleSearchIndex < iTotTris ) { + const tbool bIsGood = + ( pTriInfos[iNextGoodTriangleSearchIndex].iFlag & MARK_DEGENERATE ) == 0 + ? TTRUE + : TFALSE; + if ( bIsGood ) + bJustADegenerate = TFALSE; + else + ++iNextGoodTriangleSearchIndex; + } + + t0 = t; + t1 = iNextGoodTriangleSearchIndex; + ++iNextGoodTriangleSearchIndex; + assert( iNextGoodTriangleSearchIndex > ( t + 1 ) ); + + // swap triangle t0 and t1 + if ( !bJustADegenerate ) { + int i = 0; + for ( i = 0; i < 3; i++ ) { + const int index = piTriList_out[t0 * 3 + i]; + piTriList_out[t0 * 3 + i] = piTriList_out[t1 * 3 + i]; + piTriList_out[t1 * 3 + i] = index; + } + { + const STriInfo tri_info = pTriInfos[t0]; + pTriInfos[t0] = pTriInfos[t1]; + pTriInfos[t1] = tri_info; + } + } + else + bStillFindingGoodOnes = TFALSE; // this is not supposed to happen + } + + if ( bStillFindingGoodOnes ) ++t; + } + + assert( bStillFindingGoodOnes ); // code will still work. + assert( iNrTrianglesIn == t ); +} + +static void DegenEpilogue( STSpace psTspace[], + STriInfo pTriInfos[], + int piTriListIn[], + const SMikkTSpaceContext* pContext, + const int iNrTrianglesIn, + const int iTotTris ) { + int t = 0, i = 0; + // deal with degenerate triangles + // punishment for degenerate triangles is O(N^2) + for ( t = iNrTrianglesIn; t < iTotTris; t++ ) { + // degenerate triangles on a quad with one good triangle are skipped + // here but processed in the next loop + const tbool bSkip = ( pTriInfos[t].iFlag & QUAD_ONE_DEGEN_TRI ) != 0 ? TTRUE : TFALSE; + + if ( !bSkip ) { + for ( i = 0; i < 3; i++ ) { + const int index1 = piTriListIn[t * 3 + i]; + // search through the good triangles + tbool bNotFound = TTRUE; + int j = 0; + while ( bNotFound && j < ( 3 * iNrTrianglesIn ) ) { + const int index2 = piTriListIn[j]; + if ( index1 == index2 ) + bNotFound = TFALSE; + else + ++j; + } + + if ( !bNotFound ) { + const int iTri = j / 3; + const int iVert = j % 3; + const int iSrcVert = pTriInfos[iTri].vert_num[iVert]; + const int iSrcOffs = pTriInfos[iTri].iTSpacesOffs; + const int iDstVert = pTriInfos[t].vert_num[i]; + const int iDstOffs = pTriInfos[t].iTSpacesOffs; + + // copy tspace + psTspace[iDstOffs + iDstVert] = psTspace[iSrcOffs + iSrcVert]; + } + } + } + } + + // deal with degenerate quads with one good triangle + for ( t = 0; t < iNrTrianglesIn; t++ ) { + // this triangle belongs to a quad where the + // other triangle is degenerate + if ( ( pTriInfos[t].iFlag & QUAD_ONE_DEGEN_TRI ) != 0 ) { + SVec3 vDstP; + int iOrgF = -1, i = 0; + tbool bNotFound; + unsigned char* pV = pTriInfos[t].vert_num; + int iFlag = ( 1 << pV[0] ) | ( 1 << pV[1] ) | ( 1 << pV[2] ); + int iMissingIndex = 0; + if ( ( iFlag & 2 ) == 0 ) + iMissingIndex = 1; + else if ( ( iFlag & 4 ) == 0 ) + iMissingIndex = 2; + else if ( ( iFlag & 8 ) == 0 ) + iMissingIndex = 3; + + iOrgF = pTriInfos[t].iOrgFaceNumber; + vDstP = GetPosition( pContext, MakeIndex( iOrgF, iMissingIndex ) ); + bNotFound = TTRUE; + i = 0; + while ( bNotFound && i < 3 ) { + const int iVert = pV[i]; + const SVec3 vSrcP = GetPosition( pContext, MakeIndex( iOrgF, iVert ) ); + if ( veq( vSrcP, vDstP ) == TTRUE ) { + const int iOffs = pTriInfos[t].iTSpacesOffs; + psTspace[iOffs + iMissingIndex] = psTspace[iOffs + iVert]; + bNotFound = TFALSE; + } + else + ++i; + } + assert( !bNotFound ); + } + } +} diff --git a/src/IO/Gltf/internal/GLTFConverter/mikktspace.h b/src/IO/Gltf/internal/GLTFConverter/mikktspace.h new file mode 100644 index 00000000000..73da52107f1 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/mikktspace.h @@ -0,0 +1,160 @@ +/** \file mikktspace/mikktspace.h + * \ingroup mikktspace + */ +/** + * Copyright (C) 2011 by Morten S. Mikkelsen + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any damages + * arising from the use of this software. + * + * Permission is granted to anyone to use this software for any purpose, + * including commercial applications, and to alter it and redistribute it + * freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you must not + * claim that you wrote the original software. If you use this software + * in a product, an acknowledgment in the product documentation would be + * appreciated but is not required. + * 2. Altered source versions must be plainly marked as such, and must not be + * misrepresented as being the original software. + * 3. This notice may not be removed or altered from any source distribution. + */ + +#ifndef __MIKKTSPACE_H__ +#define __MIKKTSPACE_H__ + +#ifdef __cplusplus +extern "C" +{ +#endif + + /* Author: Morten S. Mikkelsen + * Version: 1.0 + * + * The files mikktspace.h and mikktspace.c are designed to be + * stand-alone files and it is important that they are kept this way. + * Not having dependencies on structures/classes/libraries specific + * to the program, in which they are used, allows them to be copied + * and used as is into any tool, program or plugin. + * The code is designed to consistently generate the same + * tangent spaces, for a given mesh, in any tool in which it is used. + * This is done by performing an internal welding step and subsequently an order-independent + * evaluation of tangent space for meshes consisting of triangles and quads. This means faces + * can be received in any order and the same is true for the order of vertices of each face. The + * generated result will not be affected by such reordering. Additionally, whether degenerate + * (vertices or texture coordinates) primitives are present or not will not affect the generated + * results either. Once tangent space calculation is done the vertices of degenerate primitives + * will simply inherit tangent space from neighboring non degenerate primitives. The analysis + * behind this implementation can be found in my master's thesis which is available for download + * --> http://image.diku.dk/projects/media/morten.mikkelsen.08.pdf Note that though the tangent + * spaces at the vertices are generated in an order-independent way, by this implementation, the + * interpolated tangent space is still affected by which diagonal is chosen to split each quad. + * A sensible solution is to have your tools pipeline always split quads by the shortest + * diagonal. This choice is order-independent and works with mirroring. If these have the same + * length then compare the diagonals defined by the texture coordinates. XNormal which is a tool + * for baking normal maps allows you to write your own tangent space plugin and also quad + * triangulator plugin. + */ + + typedef int tbool; + typedef struct SMikkTSpaceContext SMikkTSpaceContext; + + typedef struct { + // Returns the number of faces (triangles/quads) on the mesh to be processed. + int ( *m_getNumFaces )( const SMikkTSpaceContext* pContext ); + + // Returns the number of vertices on face number iFace + // iFace is a number in the range {0, 1, ..., getNumFaces()-1} + int ( *m_getNumVerticesOfFace )( const SMikkTSpaceContext* pContext, const int iFace ); + + // returns the position/normal/texcoord of the referenced face of vertex number iVert. + // iVert is in the range {0,1,2} for triangles and {0,1,2,3} for quads. + void ( *m_getPosition )( const SMikkTSpaceContext* pContext, + float fvPosOut[], + const int iFace, + const int iVert ); + void ( *m_getNormal )( const SMikkTSpaceContext* pContext, + float fvNormOut[], + const int iFace, + const int iVert ); + void ( *m_getTexCoord )( const SMikkTSpaceContext* pContext, + float fvTexcOut[], + const int iFace, + const int iVert ); + + // either (or both) of the two setTSpace callbacks can be set. + // The call-back m_setTSpaceBasic() is sufficient for basic normal mapping. + + // This function is used to return the tangent and fSign to the application. + // fvTangent is a unit length vector. + // For normal maps it is sufficient to use the following simplified version of the bitangent + // which is generated at pixel/vertex level. bitangent = fSign * cross(vN, tangent); Note + // that the results are returned unindexed. It is possible to generate a new index list But + // averaging/overwriting tangent spaces by using an already existing index list WILL produce + // INCRORRECT results. DO NOT! use an already existing index list. + void ( *m_setTSpaceBasic )( const SMikkTSpaceContext* pContext, + const float fvTangent[], + const float fSign, + const int iFace, + const int iVert ); + + // This function is used to return tangent space results to the application. + // fvTangent and fvBiTangent are unit length vectors and fMagS and fMagT are their + // true magnitudes which can be used for relief mapping effects. + // fvBiTangent is the "real" bitangent and thus may not be perpendicular to fvTangent. + // However, both are perpendicular to the vertex normal. + // For normal maps it is sufficient to use the following simplified version of the bitangent + // which is generated at pixel/vertex level. fSign = bIsOrientationPreserving ? 1.0f : + // (-1.0f); bitangent = fSign * cross(vN, tangent); Note that the results are returned + // unindexed. It is possible to generate a new index list But averaging/overwriting tangent + // spaces by using an already existing index list WILL produce INCRORRECT results. DO NOT! + // use an already existing index list. + void ( *m_setTSpace )( const SMikkTSpaceContext* pContext, + const float fvTangent[], + const float fvBiTangent[], + const float fMagS, + const float fMagT, + const tbool bIsOrientationPreserving, + const int iFace, + const int iVert ); + } SMikkTSpaceInterface; + + struct SMikkTSpaceContext { + SMikkTSpaceInterface* m_pInterface; // initialized with callback functions + void* m_pUserData; // pointer to client side mesh data etc. (passed as the first parameter + // with every interface call) + }; + + // these are both thread safe! + tbool genTangSpaceDefault( + const SMikkTSpaceContext* pContext ); // Default (recommended) fAngularThreshold is 180 + // degrees (which means threshold disabled) + tbool genTangSpace( const SMikkTSpaceContext* pContext, const float fAngularThreshold ); + + // To avoid visual errors (distortions/unwanted hard edges in lighting), when using sampled + // normal maps, the normal map sampler must use the exact inverse of the pixel shader + // transformation. The most efficient transformation we can possibly do in the pixel shader is + // achieved by using, directly, the "unnormalized" interpolated tangent, bitangent and vertex + // normal: vT, vB and vN. pixel shader (fast transform out) vNout = normalize( vNt.x * vT + + // vNt.y * vB + vNt.z * vN ); where vNt is the tangent space normal. The normal map sampler must + // likewise use the interpolated and "unnormalized" tangent, bitangent and vertex normal to be + // compliant with the pixel shader. sampler does (exact inverse of pixel shader): float3 row0 = + // cross(vB, vN); float3 row1 = cross(vN, vT); float3 row2 = cross(vT, vB); float fSign = + // dot(vT, row0)<0 ? -1 : 1; vNt = normalize( fSign * float3(dot(vNout,row0), dot(vNout,row1), + // dot(vNout,row2)) ); where vNout is the sampled normal in some chosen 3D space. + // + // Should you choose to reconstruct the bitangent in the pixel shader instead + // of the vertex shader, as explained earlier, then be sure to do this in the normal map sampler + // also. Finally, beware of quad triangulations. If the normal map sampler doesn't use the same + // triangulation of quads as your renderer then problems will occur since the interpolated + // tangent spaces will differ eventhough the vertex level tangent spaces match. This can be + // solved either by triangulating before sampling/exporting or by using the order-independent + // choice of diagonal for splitting quads suggested earlier. However, this must be used both by + // the sampler and your tools/rendering pipeline. + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/IO/Gltf/internal/fx/gltf.h b/src/IO/Gltf/internal/fx/gltf.h new file mode 100644 index 00000000000..696afea55a7 --- /dev/null +++ b/src/IO/Gltf/internal/fx/gltf.h @@ -0,0 +1,1716 @@ +// ------------------------------------------------------------ +// Copyright(c) 2018-2021 Jesse Yurkovich +// Licensed under the MIT License . +// See the LICENSE file in the repo root for full license information. +// ------------------------------------------------------------ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#if ( defined( __cplusplus ) && __cplusplus >= 201703L ) || \ + ( defined( _MSVC_LANG ) && ( _MSVC_LANG >= 201703L ) && ( _MSC_VER >= 1911 ) ) +# define FX_GLTF_HAS_CPP_17 +# define FX_GLTF_NODISCARD [[nodiscard]] +# include + +#else +# define FX_GLTF_NODISCARD +#endif + +namespace fx { +namespace base64 { +namespace detail { +// clang-format off + constexpr std::array EncodeMap = + { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' + }; + + constexpr std::array DecodeMap = + { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; +// clang-format on +} // namespace detail + +inline std::string Encode( std::vector const& bytes ) { + const std::size_t length = bytes.size(); + if ( length == 0 ) { return {}; } + + std::string out {}; + out.reserve( ( ( length * 4 / 3 ) + 3 ) & ( ~3u ) ); // round up to nearest 4 + + uint32_t value = 0; + int32_t bitCount = -6; + for ( const uint8_t c : bytes ) { + value = ( value << 8u ) + c; + bitCount += 8; + while ( bitCount >= 0 ) { + const uint32_t shiftOperand = bitCount; + out.push_back( detail::EncodeMap.at( ( value >> shiftOperand ) & 0x3fu ) ); + bitCount -= 6; + } + } + + if ( bitCount > -6 ) { + const uint32_t shiftOperand = bitCount + 8; + out.push_back( detail::EncodeMap.at( ( ( value << 8u ) >> shiftOperand ) & 0x3fu ) ); + } + + while ( out.size() % 4 != 0 ) { + out.push_back( '=' ); + } + + return out; +} + +#if defined( FX_GLTF_HAS_CPP_17 ) +inline bool TryDecode( std::string_view in, std::vector& out ) +#else +inline bool TryDecode( std::string const& in, std::vector& out ) +#endif +{ + out.clear(); + + const std::size_t length = in.length(); + if ( length == 0 ) { return true; } + + if ( length % 4 != 0 ) { return false; } + + out.reserve( ( length / 4 ) * 3 ); + + bool invalid = false; + uint32_t value = 0; + int32_t bitCount = -8; + for ( std::size_t i = 0; i < length; i++ ) { + const uint8_t c = static_cast( in[i] ); + const char map = detail::DecodeMap.at( c ); + if ( map == -1 ) { + if ( c != '=' ) // Non base64 character + { + invalid = true; + } + else { + // Padding characters not where they should be + const std::size_t remaining = length - i - 1; + if ( remaining > 1 || ( remaining == 1 ? in[i + 1] != '=' : false ) ) { + invalid = true; + } + } + + break; + } + + value = ( value << 6u ) + map; + bitCount += 6; + if ( bitCount >= 0 ) { + const uint32_t shiftOperand = bitCount; + out.push_back( static_cast( value >> shiftOperand ) ); + bitCount -= 8; + } + } + + if ( invalid ) { out.clear(); } + + return !invalid; +} +} // namespace base64 + +namespace gltf { +class invalid_gltf_document : public std::runtime_error +{ + public: + explicit invalid_gltf_document( char const* message ) : std::runtime_error( message ) {} + + invalid_gltf_document( char const* message, std::string const& extra ) : + std::runtime_error( CreateMessage( message, extra ).c_str() ) {} + + private: + static std::string CreateMessage( char const* message, std::string const& extra ) { + return std::string( message ).append( " : " ).append( extra ); + } +}; + +namespace detail { +#if defined( FX_GLTF_HAS_CPP_17 ) +template +inline void ReadRequiredField( std::string_view key, nlohmann::json const& json, TTarget& target ) +#else +template +inline void ReadRequiredField( TKey&& key, nlohmann::json const& json, TTarget& target ) +#endif +{ + const nlohmann::json::const_iterator iter = json.find( key ); + if ( iter == json.end() ) { + throw invalid_gltf_document( "Required field not found", std::string( key ) ); + } + + target = iter->get(); +} + +#if defined( FX_GLTF_HAS_CPP_17 ) +template +inline void ReadOptionalField( std::string_view key, nlohmann::json const& json, TTarget& target ) +#else +template +inline void ReadOptionalField( TKey&& key, nlohmann::json const& json, TTarget& target ) +#endif +{ + const nlohmann::json::const_iterator iter = json.find( key ); + if ( iter != json.end() ) { target = iter->get(); } +} + +inline void ReadExtensionsAndExtras( nlohmann::json const& json, + nlohmann::json& extensionsAndExtras ) { + const nlohmann::json::const_iterator iterExtensions = json.find( "extensions" ); + const nlohmann::json::const_iterator iterExtras = json.find( "extras" ); + if ( iterExtensions != json.end() ) { extensionsAndExtras["extensions"] = *iterExtensions; } + + if ( iterExtras != json.end() ) { extensionsAndExtras["extras"] = *iterExtras; } +} + +template +inline void WriteField( std::string const& key, nlohmann::json& json, TValue const& value ) { + if ( !value.empty() ) { json[key] = value; } +} + +template +inline void WriteField( std::string const& key, + nlohmann::json& json, + TValue const& value, + TValue const& defaultValue ) { + if ( value != defaultValue ) { json[key] = value; } +} + +inline void WriteExtensions( nlohmann::json& json, nlohmann::json const& extensionsAndExtras ) { + if ( !extensionsAndExtras.empty() ) { + for ( nlohmann::json::const_iterator it = extensionsAndExtras.begin(); + it != extensionsAndExtras.end(); + ++it ) { + json[it.key()] = it.value(); + } + } +} + +inline std::string GetDocumentRootPath( std::string const& documentFilePath ) { + const std::size_t pos = documentFilePath.find_last_of( "/\\" ); + if ( pos != std::string::npos ) { return documentFilePath.substr( 0, pos ); } + + return {}; +} + +inline std::string CreateBufferUriPath( std::string const& documentRootPath, + std::string const& bufferUri ) { + // Prevent simple forms of path traversal from malicious uri references... + if ( bufferUri.empty() || bufferUri.find( ".." ) != std::string::npos || + bufferUri.front() == '/' || bufferUri.front() == '\\' ) { + throw invalid_gltf_document( "Invalid buffer.uri value", bufferUri ); + } + + std::string documentRoot = documentRootPath; + if ( documentRoot.length() > 0 ) { + if ( documentRoot.back() != '/' ) { documentRoot.push_back( '/' ); } + } + + return documentRoot + bufferUri; +} + +struct ChunkHeader { + uint32_t chunkLength {}; + uint32_t chunkType {}; +}; + +struct GLBHeader { + uint32_t magic {}; + uint32_t version {}; + uint32_t length {}; + + ChunkHeader jsonHeader {}; +}; + +constexpr uint32_t DefaultMaxBufferCount = 8; +constexpr uint32_t DefaultMaxMemoryAllocation = 32 * 1024 * 1024; +constexpr std::size_t HeaderSize { sizeof( GLBHeader ) }; +constexpr std::size_t ChunkHeaderSize { sizeof( ChunkHeader ) }; +constexpr uint32_t GLBHeaderMagic = 0x46546c67u; +constexpr uint32_t GLBChunkJSON = 0x4e4f534au; +constexpr uint32_t GLBChunkBIN = 0x004e4942u; + +constexpr char const* const MimetypeApplicationOctet = "data:application/octet-stream;base64"; +constexpr char const* const MimetypeGLTFBuffer = "data:application/gltf-buffer;base64"; +constexpr char const* const MimetypeImagePNG = "data:image/png;base64"; +constexpr char const* const MimetypeImageJPG = "data:image/jpeg;base64"; +} // namespace detail + +namespace defaults { +constexpr std::array IdentityMatrix { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }; +constexpr std::array IdentityRotation { 0, 0, 0, 1 }; +constexpr std::array IdentityVec4 { 1, 1, 1, 1 }; +constexpr std::array IdentityVec3 { 1, 1, 1 }; +constexpr std::array NullVec3 { 0, 0, 0 }; +constexpr float IdentityScalar = 1; +constexpr float FloatSentinel = 10000; + +constexpr bool AccessorNormalized = false; + +constexpr float MaterialAlphaCutoff = 0.5f; +constexpr bool MaterialDoubleSided = false; +} // namespace defaults + +using Attributes = std::unordered_map; + +struct NeverEmpty { + FX_GLTF_NODISCARD static bool empty() noexcept { return false; } +}; + +struct Accessor { + enum class ComponentType : uint16_t { + None = 0, + Byte = 5120, + UnsignedByte = 5121, + Short = 5122, + UnsignedShort = 5123, + UnsignedInt = 5125, + Float = 5126 + }; + + enum class Type : uint8_t { None, Scalar, Vec2, Vec3, Vec4, Mat2, Mat3, Mat4 }; + + struct Sparse { + struct Indices : NeverEmpty { + uint32_t bufferView {}; + uint32_t byteOffset {}; + ComponentType componentType { ComponentType::None }; + + nlohmann::json extensionsAndExtras {}; + }; + + struct Values : NeverEmpty { + uint32_t bufferView {}; + uint32_t byteOffset {}; + + nlohmann::json extensionsAndExtras {}; + }; + + int32_t count {}; + Indices indices {}; + Values values {}; + + nlohmann::json extensionsAndExtras {}; + + FX_GLTF_NODISCARD bool empty() const noexcept { return count == 0; } + }; + + int32_t bufferView { -1 }; + uint32_t byteOffset {}; + uint32_t count {}; + bool normalized { defaults::AccessorNormalized }; + + ComponentType componentType { ComponentType::None }; + Type type { Type::None }; + Sparse sparse; + + std::string name; + std::vector max {}; + std::vector min {}; + + nlohmann::json extensionsAndExtras {}; +}; + +struct Animation { + struct Channel { + struct Target : NeverEmpty { + int32_t node { -1 }; + std::string path {}; + + nlohmann::json extensionsAndExtras {}; + }; + + int32_t sampler { -1 }; + Target target {}; + + nlohmann::json extensionsAndExtras {}; + }; + + struct Sampler { + enum class Type { Linear, Step, CubicSpline }; + + int32_t input { -1 }; + int32_t output { -1 }; + + Type interpolation { Sampler::Type::Linear }; + + nlohmann::json extensionsAndExtras {}; + }; + + std::string name {}; + std::vector channels {}; + std::vector samplers {}; + + nlohmann::json extensionsAndExtras {}; +}; + +struct Asset : NeverEmpty { + std::string copyright {}; + std::string generator {}; + std::string minVersion {}; + std::string version { "2.0" }; + + nlohmann::json extensionsAndExtras {}; +}; + +struct Buffer { + uint32_t byteLength {}; + + std::string name; + std::string uri; + + nlohmann::json extensionsAndExtras {}; + + std::vector data {}; + + FX_GLTF_NODISCARD bool IsEmbeddedResource() const noexcept { + return uri.find( detail::MimetypeApplicationOctet ) == 0 || + uri.find( detail::MimetypeGLTFBuffer ) == 0; + } + + void SetEmbeddedResource() { + uri = std::string( detail::MimetypeApplicationOctet ) + .append( "," ) + .append( base64::Encode( data ) ); + } +}; + +struct BufferView { + enum class TargetType : uint16_t { None = 0, ArrayBuffer = 34962, ElementArrayBuffer = 34963 }; + + std::string name; + + int32_t buffer { -1 }; + uint32_t byteOffset {}; + uint32_t byteLength {}; + uint32_t byteStride {}; + + TargetType target { TargetType::None }; + + nlohmann::json extensionsAndExtras {}; +}; + +struct Camera { + enum class Type { None, Orthographic, Perspective }; + + struct Orthographic : NeverEmpty { + float xmag { defaults::FloatSentinel }; + float ymag { defaults::FloatSentinel }; + float zfar { -defaults::FloatSentinel }; + float znear { -defaults::FloatSentinel }; + + nlohmann::json extensionsAndExtras {}; + }; + + struct Perspective : NeverEmpty { + float aspectRatio {}; + float yfov {}; + float zfar {}; + float znear {}; + + nlohmann::json extensionsAndExtras {}; + }; + + std::string name {}; + Type type { Type::None }; + + Orthographic orthographic; + Perspective perspective; + + nlohmann::json extensionsAndExtras {}; +}; + +struct Image { + int32_t bufferView {}; + + std::string name; + std::string uri; + std::string mimeType; + + nlohmann::json extensionsAndExtras {}; + + FX_GLTF_NODISCARD bool IsEmbeddedResource() const noexcept { + return uri.find( detail::MimetypeImagePNG ) == 0 || + uri.find( detail::MimetypeImageJPG ) == 0; + } + + void MaterializeData( std::vector& data ) const { + char const* const mimetype = uri.find( detail::MimetypeImagePNG ) == 0 + ? detail::MimetypeImagePNG + : detail::MimetypeImageJPG; + const std::size_t startPos = std::char_traits::length( mimetype ) + 1; + +#if defined( FX_GLTF_HAS_CPP_17 ) + const std::size_t base64Length = uri.length() - startPos; + const bool success = base64::TryDecode( { &uri[startPos], base64Length }, data ); +#else + const bool success = base64::TryDecode( uri.substr( startPos ), data ); +#endif + if ( !success ) { + throw invalid_gltf_document( "Invalid buffer.uri value", "malformed base64" ); + } + } +}; + +struct Material { + enum class AlphaMode : uint8_t { Opaque, Mask, Blend }; + + struct Texture { + int32_t index { -1 }; + int32_t texCoord {}; + + nlohmann::json extensionsAndExtras {}; + + FX_GLTF_NODISCARD bool empty() const noexcept { return index == -1; } + }; + + struct NormalTexture : Texture { + float scale { defaults::IdentityScalar }; + }; + + struct OcclusionTexture : Texture { + float strength { defaults::IdentityScalar }; + }; + + struct PBRMetallicRoughness { + std::array baseColorFactor = { defaults::IdentityVec4 }; + Texture baseColorTexture; + + float roughnessFactor { defaults::IdentityScalar }; + float metallicFactor { defaults::IdentityScalar }; + Texture metallicRoughnessTexture; + + nlohmann::json extensionsAndExtras {}; + + FX_GLTF_NODISCARD bool empty() const { + return baseColorTexture.empty() && metallicRoughnessTexture.empty() && + metallicFactor == 1.0f && roughnessFactor == 1.0f && + baseColorFactor == defaults::IdentityVec4; + } + }; + + float alphaCutoff { defaults::MaterialAlphaCutoff }; + AlphaMode alphaMode { AlphaMode::Opaque }; + + bool doubleSided { defaults::MaterialDoubleSided }; + + NormalTexture normalTexture; + OcclusionTexture occlusionTexture; + PBRMetallicRoughness pbrMetallicRoughness; + + Texture emissiveTexture; + std::array emissiveFactor = { defaults::NullVec3 }; + + std::string name; + nlohmann::json extensionsAndExtras {}; +}; + +struct Primitive { + enum class Mode : uint8_t { + Points = 0, + Lines = 1, + LineLoop = 2, + LineStrip = 3, + Triangles = 4, + TriangleStrip = 5, + TriangleFan = 6 + }; + + int32_t indices { -1 }; + int32_t material { -1 }; + + Mode mode { Mode::Triangles }; + + Attributes attributes {}; + std::vector targets {}; + + nlohmann::json extensionsAndExtras {}; +}; + +struct Mesh { + std::string name; + + std::vector weights {}; + std::vector primitives {}; + + nlohmann::json extensionsAndExtras {}; +}; + +struct Node { + std::string name; + + int32_t camera { -1 }; + int32_t mesh { -1 }; + int32_t skin { -1 }; + + std::array matrix { defaults::IdentityMatrix }; + std::array rotation { defaults::IdentityRotation }; + std::array scale { defaults::IdentityVec3 }; + std::array translation { defaults::NullVec3 }; + + std::vector children {}; + std::vector weights {}; + + nlohmann::json extensionsAndExtras {}; +}; + +struct Sampler { + enum class MagFilter : uint16_t { None, Nearest = 9728, Linear = 9729 }; + + enum class MinFilter : uint16_t { + None, + Nearest = 9728, + Linear = 9729, + NearestMipMapNearest = 9984, + LinearMipMapNearest = 9985, + NearestMipMapLinear = 9986, + LinearMipMapLinear = 9987 + }; + + enum class WrappingMode : uint16_t { + ClampToEdge = 33071, + MirroredRepeat = 33648, + Repeat = 10497 + }; + + std::string name; + + MagFilter magFilter { MagFilter::None }; + MinFilter minFilter { MinFilter::None }; + + WrappingMode wrapS { WrappingMode::Repeat }; + WrappingMode wrapT { WrappingMode::Repeat }; + + nlohmann::json extensionsAndExtras {}; + + FX_GLTF_NODISCARD bool empty() const noexcept { + return name.empty() && magFilter == MagFilter::None && minFilter == MinFilter::None && + wrapS == WrappingMode::Repeat && wrapT == WrappingMode::Repeat && + extensionsAndExtras.empty(); + } +}; + +struct Scene { + std::string name; + + std::vector nodes {}; + + nlohmann::json extensionsAndExtras {}; +}; + +struct Skin { + int32_t inverseBindMatrices { -1 }; + int32_t skeleton { -1 }; + + std::string name; + std::vector joints {}; + + nlohmann::json extensionsAndExtras {}; +}; + +struct Texture { + std::string name; + + int32_t sampler { -1 }; + int32_t source { -1 }; + + nlohmann::json extensionsAndExtras {}; +}; + +struct Document { + Asset asset; + + std::vector accessors {}; + std::vector animations {}; + std::vector buffers {}; + std::vector bufferViews {}; + std::vector cameras {}; + std::vector images {}; + std::vector materials {}; + std::vector meshes {}; + std::vector nodes {}; + std::vector samplers {}; + std::vector scenes {}; + std::vector skins {}; + std::vector textures {}; + + int32_t scene { -1 }; + std::vector extensionsUsed {}; + std::vector extensionsRequired {}; + + nlohmann::json extensionsAndExtras {}; +}; + +struct ReadQuotas { + uint32_t MaxBufferCount { detail::DefaultMaxBufferCount }; + uint32_t MaxFileSize { detail::DefaultMaxMemoryAllocation }; + uint32_t MaxBufferByteLength { detail::DefaultMaxMemoryAllocation }; +}; + +inline void from_json( nlohmann::json const& json, Accessor::Type& accessorType ) { + std::string type = json.get(); + if ( type == "SCALAR" ) { accessorType = Accessor::Type::Scalar; } + else if ( type == "VEC2" ) { accessorType = Accessor::Type::Vec2; } + else if ( type == "VEC3" ) { accessorType = Accessor::Type::Vec3; } + else if ( type == "VEC4" ) { accessorType = Accessor::Type::Vec4; } + else if ( type == "MAT2" ) { accessorType = Accessor::Type::Mat2; } + else if ( type == "MAT3" ) { accessorType = Accessor::Type::Mat3; } + else if ( type == "MAT4" ) { accessorType = Accessor::Type::Mat4; } + else { throw invalid_gltf_document( "Unknown accessor.type value", type ); } +} + +inline void from_json( nlohmann::json const& json, Accessor::Sparse::Values& values ) { + detail::ReadRequiredField( "bufferView", json, values.bufferView ); + + detail::ReadOptionalField( "byteOffset", json, values.byteOffset ); + + detail::ReadExtensionsAndExtras( json, values.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Accessor::Sparse::Indices& indices ) { + detail::ReadRequiredField( "bufferView", json, indices.bufferView ); + detail::ReadRequiredField( "componentType", json, indices.componentType ); + + detail::ReadOptionalField( "byteOffset", json, indices.byteOffset ); + + detail::ReadExtensionsAndExtras( json, indices.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Accessor::Sparse& sparse ) { + detail::ReadRequiredField( "count", json, sparse.count ); + detail::ReadRequiredField( "indices", json, sparse.indices ); + detail::ReadRequiredField( "values", json, sparse.values ); + + detail::ReadExtensionsAndExtras( json, sparse.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Accessor& accessor ) { + detail::ReadRequiredField( "componentType", json, accessor.componentType ); + detail::ReadRequiredField( "count", json, accessor.count ); + detail::ReadRequiredField( "type", json, accessor.type ); + + detail::ReadOptionalField( "bufferView", json, accessor.bufferView ); + detail::ReadOptionalField( "byteOffset", json, accessor.byteOffset ); + detail::ReadOptionalField( "max", json, accessor.max ); + detail::ReadOptionalField( "min", json, accessor.min ); + detail::ReadOptionalField( "name", json, accessor.name ); + detail::ReadOptionalField( "normalized", json, accessor.normalized ); + detail::ReadOptionalField( "sparse", json, accessor.sparse ); + + detail::ReadExtensionsAndExtras( json, accessor.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, + Animation::Channel::Target& animationChannelTarget ) { + detail::ReadRequiredField( "path", json, animationChannelTarget.path ); + + detail::ReadOptionalField( "node", json, animationChannelTarget.node ); + + detail::ReadExtensionsAndExtras( json, animationChannelTarget.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Animation::Channel& animationChannel ) { + detail::ReadRequiredField( "sampler", json, animationChannel.sampler ); + detail::ReadRequiredField( "target", json, animationChannel.target ); + + detail::ReadExtensionsAndExtras( json, animationChannel.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, + Animation::Sampler::Type& animationSamplerType ) { + std::string type = json.get(); + if ( type == "LINEAR" ) { animationSamplerType = Animation::Sampler::Type::Linear; } + else if ( type == "STEP" ) { animationSamplerType = Animation::Sampler::Type::Step; } + else if ( type == "CUBICSPLINE" ) { + animationSamplerType = Animation::Sampler::Type::CubicSpline; + } + else { throw invalid_gltf_document( "Unknown animation.sampler.interpolation value", type ); } +} + +inline void from_json( nlohmann::json const& json, Animation::Sampler& animationSampler ) { + detail::ReadRequiredField( "input", json, animationSampler.input ); + detail::ReadRequiredField( "output", json, animationSampler.output ); + + detail::ReadOptionalField( "interpolation", json, animationSampler.interpolation ); + + detail::ReadExtensionsAndExtras( json, animationSampler.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Animation& animation ) { + detail::ReadRequiredField( "channels", json, animation.channels ); + detail::ReadRequiredField( "samplers", json, animation.samplers ); + + detail::ReadOptionalField( "name", json, animation.name ); + + detail::ReadExtensionsAndExtras( json, animation.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Asset& asset ) { + detail::ReadRequiredField( "version", json, asset.version ); + detail::ReadOptionalField( "copyright", json, asset.copyright ); + detail::ReadOptionalField( "generator", json, asset.generator ); + detail::ReadOptionalField( "minVersion", json, asset.minVersion ); + + detail::ReadExtensionsAndExtras( json, asset.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Buffer& buffer ) { + detail::ReadRequiredField( "byteLength", json, buffer.byteLength ); + + detail::ReadOptionalField( "name", json, buffer.name ); + detail::ReadOptionalField( "uri", json, buffer.uri ); + + detail::ReadExtensionsAndExtras( json, buffer.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, BufferView& bufferView ) { + detail::ReadRequiredField( "buffer", json, bufferView.buffer ); + detail::ReadRequiredField( "byteLength", json, bufferView.byteLength ); + + detail::ReadOptionalField( "byteOffset", json, bufferView.byteOffset ); + detail::ReadOptionalField( "byteStride", json, bufferView.byteStride ); + detail::ReadOptionalField( "name", json, bufferView.name ); + detail::ReadOptionalField( "target", json, bufferView.target ); + + detail::ReadExtensionsAndExtras( json, bufferView.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Camera::Type& cameraType ) { + std::string type = json.get(); + if ( type == "orthographic" ) { cameraType = Camera::Type::Orthographic; } + else if ( type == "perspective" ) { cameraType = Camera::Type::Perspective; } + else { throw invalid_gltf_document( "Unknown camera.type value", type ); } +} + +inline void from_json( nlohmann::json const& json, Camera::Orthographic& camera ) { + detail::ReadRequiredField( "xmag", json, camera.xmag ); + detail::ReadRequiredField( "ymag", json, camera.ymag ); + detail::ReadRequiredField( "zfar", json, camera.zfar ); + detail::ReadRequiredField( "znear", json, camera.znear ); + + detail::ReadExtensionsAndExtras( json, camera.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Camera::Perspective& camera ) { + detail::ReadRequiredField( "yfov", json, camera.yfov ); + detail::ReadRequiredField( "znear", json, camera.znear ); + + detail::ReadOptionalField( "aspectRatio", json, camera.aspectRatio ); + detail::ReadOptionalField( "zfar", json, camera.zfar ); + + detail::ReadExtensionsAndExtras( json, camera.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Camera& camera ) { + detail::ReadRequiredField( "type", json, camera.type ); + + detail::ReadOptionalField( "name", json, camera.name ); + + detail::ReadExtensionsAndExtras( json, camera.extensionsAndExtras ); + + if ( camera.type == Camera::Type::Perspective ) { + detail::ReadRequiredField( "perspective", json, camera.perspective ); + } + else if ( camera.type == Camera::Type::Orthographic ) { + detail::ReadRequiredField( "orthographic", json, camera.orthographic ); + } +} + +inline void from_json( nlohmann::json const& json, Image& image ) { + detail::ReadOptionalField( "bufferView", json, image.bufferView ); + detail::ReadOptionalField( "mimeType", json, image.mimeType ); + detail::ReadOptionalField( "name", json, image.name ); + detail::ReadOptionalField( "uri", json, image.uri ); + + detail::ReadExtensionsAndExtras( json, image.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Material::AlphaMode& materialAlphaMode ) { + std::string alphaMode = json.get(); + if ( alphaMode == "OPAQUE" ) { materialAlphaMode = Material::AlphaMode::Opaque; } + else if ( alphaMode == "MASK" ) { materialAlphaMode = Material::AlphaMode::Mask; } + else if ( alphaMode == "BLEND" ) { materialAlphaMode = Material::AlphaMode::Blend; } + else { throw invalid_gltf_document( "Unknown material.alphaMode value", alphaMode ); } +} + +inline void from_json( nlohmann::json const& json, Material::Texture& materialTexture ) { + detail::ReadRequiredField( "index", json, materialTexture.index ); + detail::ReadOptionalField( "texCoord", json, materialTexture.texCoord ); + + detail::ReadExtensionsAndExtras( json, materialTexture.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Material::NormalTexture& materialTexture ) { + from_json( json, static_cast( materialTexture ) ); + detail::ReadOptionalField( "scale", json, materialTexture.scale ); + + detail::ReadExtensionsAndExtras( json, materialTexture.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Material::OcclusionTexture& materialTexture ) { + from_json( json, static_cast( materialTexture ) ); + detail::ReadOptionalField( "strength", json, materialTexture.strength ); + + detail::ReadExtensionsAndExtras( json, materialTexture.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, + Material::PBRMetallicRoughness& pbrMetallicRoughness ) { + detail::ReadOptionalField( "baseColorFactor", json, pbrMetallicRoughness.baseColorFactor ); + detail::ReadOptionalField( "baseColorTexture", json, pbrMetallicRoughness.baseColorTexture ); + detail::ReadOptionalField( "metallicFactor", json, pbrMetallicRoughness.metallicFactor ); + detail::ReadOptionalField( + "metallicRoughnessTexture", json, pbrMetallicRoughness.metallicRoughnessTexture ); + detail::ReadOptionalField( "roughnessFactor", json, pbrMetallicRoughness.roughnessFactor ); + + detail::ReadExtensionsAndExtras( json, pbrMetallicRoughness.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Material& material ) { + detail::ReadOptionalField( "alphaMode", json, material.alphaMode ); + detail::ReadOptionalField( "alphaCutoff", json, material.alphaCutoff ); + detail::ReadOptionalField( "doubleSided", json, material.doubleSided ); + detail::ReadOptionalField( "emissiveFactor", json, material.emissiveFactor ); + detail::ReadOptionalField( "emissiveTexture", json, material.emissiveTexture ); + detail::ReadOptionalField( "name", json, material.name ); + detail::ReadOptionalField( "normalTexture", json, material.normalTexture ); + detail::ReadOptionalField( "occlusionTexture", json, material.occlusionTexture ); + detail::ReadOptionalField( "pbrMetallicRoughness", json, material.pbrMetallicRoughness ); + + detail::ReadExtensionsAndExtras( json, material.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Mesh& mesh ) { + detail::ReadRequiredField( "primitives", json, mesh.primitives ); + + detail::ReadOptionalField( "name", json, mesh.name ); + detail::ReadOptionalField( "weights", json, mesh.weights ); + + detail::ReadExtensionsAndExtras( json, mesh.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Node& node ) { + detail::ReadOptionalField( "camera", json, node.camera ); + detail::ReadOptionalField( "children", json, node.children ); + detail::ReadOptionalField( "matrix", json, node.matrix ); + detail::ReadOptionalField( "mesh", json, node.mesh ); + detail::ReadOptionalField( "name", json, node.name ); + detail::ReadOptionalField( "rotation", json, node.rotation ); + detail::ReadOptionalField( "scale", json, node.scale ); + detail::ReadOptionalField( "skin", json, node.skin ); + detail::ReadOptionalField( "translation", json, node.translation ); + + detail::ReadExtensionsAndExtras( json, node.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Primitive& primitive ) { + detail::ReadRequiredField( "attributes", json, primitive.attributes ); + + detail::ReadOptionalField( "indices", json, primitive.indices ); + detail::ReadOptionalField( "material", json, primitive.material ); + detail::ReadOptionalField( "mode", json, primitive.mode ); + detail::ReadOptionalField( "targets", json, primitive.targets ); + + detail::ReadExtensionsAndExtras( json, primitive.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Sampler& sampler ) { + detail::ReadOptionalField( "magFilter", json, sampler.magFilter ); + detail::ReadOptionalField( "minFilter", json, sampler.minFilter ); + detail::ReadOptionalField( "name", json, sampler.name ); + detail::ReadOptionalField( "wrapS", json, sampler.wrapS ); + detail::ReadOptionalField( "wrapT", json, sampler.wrapT ); + + detail::ReadExtensionsAndExtras( json, sampler.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Scene& scene ) { + detail::ReadOptionalField( "name", json, scene.name ); + detail::ReadOptionalField( "nodes", json, scene.nodes ); + + detail::ReadExtensionsAndExtras( json, scene.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Skin& skin ) { + detail::ReadRequiredField( "joints", json, skin.joints ); + + detail::ReadOptionalField( "inverseBindMatrices", json, skin.inverseBindMatrices ); + detail::ReadOptionalField( "name", json, skin.name ); + detail::ReadOptionalField( "skeleton", json, skin.skeleton ); + + detail::ReadExtensionsAndExtras( json, skin.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Texture& texture ) { + detail::ReadOptionalField( "name", json, texture.name ); + detail::ReadOptionalField( "sampler", json, texture.sampler ); + detail::ReadOptionalField( "source", json, texture.source ); + + detail::ReadExtensionsAndExtras( json, texture.extensionsAndExtras ); +} + +inline void from_json( nlohmann::json const& json, Document& document ) { + detail::ReadRequiredField( "asset", json, document.asset ); + + detail::ReadOptionalField( "accessors", json, document.accessors ); + detail::ReadOptionalField( "animations", json, document.animations ); + detail::ReadOptionalField( "buffers", json, document.buffers ); + detail::ReadOptionalField( "bufferViews", json, document.bufferViews ); + detail::ReadOptionalField( "cameras", json, document.cameras ); + detail::ReadOptionalField( "materials", json, document.materials ); + detail::ReadOptionalField( "meshes", json, document.meshes ); + detail::ReadOptionalField( "nodes", json, document.nodes ); + detail::ReadOptionalField( "images", json, document.images ); + detail::ReadOptionalField( "samplers", json, document.samplers ); + detail::ReadOptionalField( "scene", json, document.scene ); + detail::ReadOptionalField( "scenes", json, document.scenes ); + detail::ReadOptionalField( "skins", json, document.skins ); + detail::ReadOptionalField( "textures", json, document.textures ); + + detail::ReadOptionalField( "extensionsUsed", json, document.extensionsUsed ); + detail::ReadOptionalField( "extensionsRequired", json, document.extensionsRequired ); + detail::ReadExtensionsAndExtras( json, document.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Accessor::ComponentType const& accessorComponentType ) { + if ( accessorComponentType == Accessor::ComponentType::None ) { + throw invalid_gltf_document( "Unknown accessor.componentType value" ); + } + + json = static_cast( accessorComponentType ); +} + +inline void to_json( nlohmann::json& json, Accessor::Type const& accessorType ) { + switch ( accessorType ) { + case Accessor::Type::Scalar: + json = "SCALAR"; + break; + case Accessor::Type::Vec2: + json = "VEC2"; + break; + case Accessor::Type::Vec3: + json = "VEC3"; + break; + case Accessor::Type::Vec4: + json = "VEC4"; + break; + case Accessor::Type::Mat2: + json = "MAT2"; + break; + case Accessor::Type::Mat3: + json = "MAT3"; + break; + case Accessor::Type::Mat4: + json = "MAT4"; + break; + default: + throw invalid_gltf_document( "Unknown accessor.type value" ); + } +} + +inline void to_json( nlohmann::json& json, Accessor::Sparse::Values const& values ) { + detail::WriteField( "bufferView", json, values.bufferView, static_cast( -1 ) ); + detail::WriteField( "byteOffset", json, values.byteOffset, {} ); + detail::WriteExtensions( json, values.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Accessor::Sparse::Indices const& indices ) { + detail::WriteField( + "componentType", json, indices.componentType, Accessor::ComponentType::None ); + detail::WriteField( "bufferView", json, indices.bufferView, static_cast( -1 ) ); + detail::WriteField( "byteOffset", json, indices.byteOffset, {} ); + detail::WriteExtensions( json, indices.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Accessor::Sparse const& sparse ) { + detail::WriteField( "count", json, sparse.count, -1 ); + detail::WriteField( "indices", json, sparse.indices ); + detail::WriteField( "values", json, sparse.values ); + detail::WriteExtensions( json, sparse.extensionsAndExtras ); +} + +namespace detail { +template +inline void WriteMinMaxConvert( nlohmann::json& json, Accessor const& accessor ) { + if ( !accessor.min.empty() ) { + auto& item = json["min"]; + for ( float v : accessor.min ) { + item.push_back( static_cast( v ) ); + } + } + + if ( !accessor.max.empty() ) { + auto& item = json["max"]; + for ( float v : accessor.max ) { + item.push_back( static_cast( v ) ); + } + } +} + +inline void WriteAccessorMinMax( nlohmann::json& json, Accessor const& accessor ) { + switch ( accessor.componentType ) { + // fast path + case Accessor::ComponentType::Float: + detail::WriteField( "max", json, accessor.max ); + detail::WriteField( "min", json, accessor.min ); + break; + // slow path conversions... + case Accessor::ComponentType::Byte: + WriteMinMaxConvert( json, accessor ); + break; + case Accessor::ComponentType::UnsignedByte: + WriteMinMaxConvert( json, accessor ); + break; + case Accessor::ComponentType::Short: + WriteMinMaxConvert( json, accessor ); + break; + case Accessor::ComponentType::UnsignedShort: + WriteMinMaxConvert( json, accessor ); + break; + case Accessor::ComponentType::UnsignedInt: + WriteMinMaxConvert( json, accessor ); + break; + case Accessor::ComponentType::None: + default: + break; + } +} +} // namespace detail + +inline void to_json( nlohmann::json& json, Accessor const& accessor ) { + detail::WriteField( "bufferView", json, accessor.bufferView, -1 ); + detail::WriteField( "byteOffset", json, accessor.byteOffset, {} ); + detail::WriteField( + "componentType", json, accessor.componentType, Accessor::ComponentType::None ); + detail::WriteField( "count", json, accessor.count, {} ); + detail::WriteAccessorMinMax( json, accessor ); + detail::WriteField( "name", json, accessor.name ); + detail::WriteField( "normalized", json, accessor.normalized, false ); + detail::WriteField( "sparse", json, accessor.sparse ); + detail::WriteField( "type", json, accessor.type, Accessor::Type::None ); + detail::WriteExtensions( json, accessor.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, + Animation::Channel::Target const& animationChannelTarget ) { + detail::WriteField( "node", json, animationChannelTarget.node, -1 ); + detail::WriteField( "path", json, animationChannelTarget.path ); + detail::WriteExtensions( json, animationChannelTarget.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Animation::Channel const& animationChannel ) { + detail::WriteField( "sampler", json, animationChannel.sampler, -1 ); + detail::WriteField( "target", json, animationChannel.target ); + detail::WriteExtensions( json, animationChannel.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Animation::Sampler::Type const& animationSamplerType ) { + switch ( animationSamplerType ) { + case Animation::Sampler::Type::Linear: + json = "LINEAR"; + break; + case Animation::Sampler::Type::Step: + json = "STEP"; + break; + case Animation::Sampler::Type::CubicSpline: + json = "CUBICSPLINE"; + break; + } +} + +inline void to_json( nlohmann::json& json, Animation::Sampler const& animationSampler ) { + detail::WriteField( "input", json, animationSampler.input, -1 ); + detail::WriteField( + "interpolation", json, animationSampler.interpolation, Animation::Sampler::Type::Linear ); + detail::WriteField( "output", json, animationSampler.output, -1 ); + detail::WriteExtensions( json, animationSampler.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Animation const& animation ) { + detail::WriteField( "channels", json, animation.channels ); + detail::WriteField( "name", json, animation.name ); + detail::WriteField( "samplers", json, animation.samplers ); + detail::WriteExtensions( json, animation.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Asset const& asset ) { + detail::WriteField( "copyright", json, asset.copyright ); + detail::WriteField( "generator", json, asset.generator ); + detail::WriteField( "minVersion", json, asset.minVersion ); + detail::WriteField( "version", json, asset.version ); + detail::WriteExtensions( json, asset.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Buffer const& buffer ) { + detail::WriteField( "byteLength", json, buffer.byteLength, {} ); + detail::WriteField( "name", json, buffer.name ); + detail::WriteField( "uri", json, buffer.uri ); + detail::WriteExtensions( json, buffer.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, BufferView const& bufferView ) { + detail::WriteField( "buffer", json, bufferView.buffer, -1 ); + detail::WriteField( "byteLength", json, bufferView.byteLength, {} ); + detail::WriteField( "byteOffset", json, bufferView.byteOffset, {} ); + detail::WriteField( "byteStride", json, bufferView.byteStride, {} ); + detail::WriteField( "name", json, bufferView.name ); + detail::WriteField( "target", json, bufferView.target, BufferView::TargetType::None ); + detail::WriteExtensions( json, bufferView.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Camera::Type const& cameraType ) { + switch ( cameraType ) { + case Camera::Type::Orthographic: + json = "orthographic"; + break; + case Camera::Type::Perspective: + json = "perspective"; + break; + default: + throw invalid_gltf_document( "Unknown camera.type value" ); + } +} + +inline void to_json( nlohmann::json& json, Camera::Orthographic const& camera ) { + detail::WriteField( "xmag", json, camera.xmag, defaults::FloatSentinel ); + detail::WriteField( "ymag", json, camera.ymag, defaults::FloatSentinel ); + detail::WriteField( "zfar", json, camera.zfar, -defaults::FloatSentinel ); + detail::WriteField( "znear", json, camera.znear, -defaults::FloatSentinel ); + detail::WriteExtensions( json, camera.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Camera::Perspective const& camera ) { + detail::WriteField( "aspectRatio", json, camera.aspectRatio, {} ); + detail::WriteField( "yfov", json, camera.yfov, {} ); + detail::WriteField( "zfar", json, camera.zfar, {} ); + detail::WriteField( "znear", json, camera.znear, {} ); + detail::WriteExtensions( json, camera.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Camera const& camera ) { + detail::WriteField( "name", json, camera.name ); + detail::WriteField( "type", json, camera.type, Camera::Type::None ); + detail::WriteExtensions( json, camera.extensionsAndExtras ); + + if ( camera.type == Camera::Type::Perspective ) { + detail::WriteField( "perspective", json, camera.perspective ); + } + else if ( camera.type == Camera::Type::Orthographic ) { + detail::WriteField( "orthographic", json, camera.orthographic ); + } +} + +inline void to_json( nlohmann::json& json, Image const& image ) { + detail::WriteField( + "bufferView", + json, + image.bufferView, + image.uri.empty() ? -1 : 0 ); // bufferView or uri need to be written; even if default 0 + detail::WriteField( "mimeType", json, image.mimeType ); + detail::WriteField( "name", json, image.name ); + detail::WriteField( "uri", json, image.uri ); + detail::WriteExtensions( json, image.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Material::AlphaMode const& materialAlphaMode ) { + switch ( materialAlphaMode ) { + case Material::AlphaMode::Opaque: + json = "OPAQUE"; + break; + case Material::AlphaMode::Mask: + json = "MASK"; + break; + case Material::AlphaMode::Blend: + json = "BLEND"; + break; + } +} + +inline void to_json( nlohmann::json& json, Material::Texture const& materialTexture ) { + detail::WriteField( "index", json, materialTexture.index, -1 ); + detail::WriteField( "texCoord", json, materialTexture.texCoord, 0 ); + detail::WriteExtensions( json, materialTexture.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Material::NormalTexture const& materialTexture ) { + to_json( json, static_cast( materialTexture ) ); + detail::WriteField( "scale", json, materialTexture.scale, defaults::IdentityScalar ); + detail::WriteExtensions( json, materialTexture.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Material::OcclusionTexture const& materialTexture ) { + to_json( json, static_cast( materialTexture ) ); + detail::WriteField( "strength", json, materialTexture.strength, defaults::IdentityScalar ); + detail::WriteExtensions( json, materialTexture.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, + Material::PBRMetallicRoughness const& pbrMetallicRoughness ) { + detail::WriteField( + "baseColorFactor", json, pbrMetallicRoughness.baseColorFactor, defaults::IdentityVec4 ); + detail::WriteField( "baseColorTexture", json, pbrMetallicRoughness.baseColorTexture ); + detail::WriteField( + "metallicFactor", json, pbrMetallicRoughness.metallicFactor, defaults::IdentityScalar ); + detail::WriteField( + "metallicRoughnessTexture", json, pbrMetallicRoughness.metallicRoughnessTexture ); + detail::WriteField( + "roughnessFactor", json, pbrMetallicRoughness.roughnessFactor, defaults::IdentityScalar ); + detail::WriteExtensions( json, pbrMetallicRoughness.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Material const& material ) { + detail::WriteField( "alphaCutoff", json, material.alphaCutoff, defaults::MaterialAlphaCutoff ); + detail::WriteField( "alphaMode", json, material.alphaMode, Material::AlphaMode::Opaque ); + detail::WriteField( "doubleSided", json, material.doubleSided, defaults::MaterialDoubleSided ); + detail::WriteField( "emissiveTexture", json, material.emissiveTexture ); + detail::WriteField( "emissiveFactor", json, material.emissiveFactor, defaults::NullVec3 ); + detail::WriteField( "name", json, material.name ); + detail::WriteField( "normalTexture", json, material.normalTexture ); + detail::WriteField( "occlusionTexture", json, material.occlusionTexture ); + detail::WriteField( "pbrMetallicRoughness", json, material.pbrMetallicRoughness ); + + detail::WriteExtensions( json, material.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Mesh const& mesh ) { + detail::WriteField( "name", json, mesh.name ); + detail::WriteField( "primitives", json, mesh.primitives ); + detail::WriteField( "weights", json, mesh.weights ); + detail::WriteExtensions( json, mesh.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Node const& node ) { + detail::WriteField( "camera", json, node.camera, -1 ); + detail::WriteField( "children", json, node.children ); + detail::WriteField( "matrix", json, node.matrix, defaults::IdentityMatrix ); + detail::WriteField( "mesh", json, node.mesh, -1 ); + detail::WriteField( "name", json, node.name ); + detail::WriteField( "rotation", json, node.rotation, defaults::IdentityRotation ); + detail::WriteField( "scale", json, node.scale, defaults::IdentityVec3 ); + detail::WriteField( "skin", json, node.skin, -1 ); + detail::WriteField( "translation", json, node.translation, defaults::NullVec3 ); + detail::WriteField( "weights", json, node.weights ); + detail::WriteExtensions( json, node.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Primitive const& primitive ) { + detail::WriteField( "attributes", json, primitive.attributes ); + detail::WriteField( "indices", json, primitive.indices, -1 ); + detail::WriteField( "material", json, primitive.material, -1 ); + detail::WriteField( "mode", json, primitive.mode, Primitive::Mode::Triangles ); + detail::WriteField( "targets", json, primitive.targets ); + detail::WriteExtensions( json, primitive.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Sampler const& sampler ) { + if ( !sampler.empty() ) { + detail::WriteField( "name", json, sampler.name ); + detail::WriteField( "magFilter", json, sampler.magFilter, Sampler::MagFilter::None ); + detail::WriteField( "minFilter", json, sampler.minFilter, Sampler::MinFilter::None ); + detail::WriteField( "wrapS", json, sampler.wrapS, Sampler::WrappingMode::Repeat ); + detail::WriteField( "wrapT", json, sampler.wrapT, Sampler::WrappingMode::Repeat ); + detail::WriteExtensions( json, sampler.extensionsAndExtras ); + } + else { + // If a sampler is completely empty we still need to write out an empty object for the + // encompassing array... + json = nlohmann::json::object(); + } +} + +inline void to_json( nlohmann::json& json, Scene const& scene ) { + detail::WriteField( "name", json, scene.name ); + detail::WriteField( "nodes", json, scene.nodes ); + detail::WriteExtensions( json, scene.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Skin const& skin ) { + detail::WriteField( "inverseBindMatrices", json, skin.inverseBindMatrices, -1 ); + detail::WriteField( "name", json, skin.name ); + detail::WriteField( "skeleton", json, skin.skeleton, -1 ); + detail::WriteField( "joints", json, skin.joints ); + detail::WriteExtensions( json, skin.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Texture const& texture ) { + detail::WriteField( "name", json, texture.name ); + detail::WriteField( "sampler", json, texture.sampler, -1 ); + detail::WriteField( "source", json, texture.source, -1 ); + detail::WriteExtensions( json, texture.extensionsAndExtras ); +} + +inline void to_json( nlohmann::json& json, Document const& document ) { + detail::WriteField( "accessors", json, document.accessors ); + detail::WriteField( "animations", json, document.animations ); + detail::WriteField( "asset", json, document.asset ); + detail::WriteField( "buffers", json, document.buffers ); + detail::WriteField( "bufferViews", json, document.bufferViews ); + detail::WriteField( "cameras", json, document.cameras ); + detail::WriteField( "images", json, document.images ); + detail::WriteField( "materials", json, document.materials ); + detail::WriteField( "meshes", json, document.meshes ); + detail::WriteField( "nodes", json, document.nodes ); + detail::WriteField( "samplers", json, document.samplers ); + detail::WriteField( "scene", json, document.scene, -1 ); + detail::WriteField( "scenes", json, document.scenes ); + detail::WriteField( "skins", json, document.skins ); + detail::WriteField( "textures", json, document.textures ); + + detail::WriteField( "extensionsUsed", json, document.extensionsUsed ); + detail::WriteField( "extensionsRequired", json, document.extensionsRequired ); + detail::WriteExtensions( json, document.extensionsAndExtras ); +} + +namespace detail { +struct DataContext { + std::string bufferRootPath {}; + ReadQuotas readQuotas; + + std::vector* binaryData {}; +}; + +inline void ThrowIfBad( std::ios const& io ) { + if ( !io.good() ) { throw std::system_error( std::make_error_code( std::errc::io_error ) ); } +} + +inline void MaterializeData( Buffer& buffer ) { + std::size_t startPos = 0; + if ( buffer.uri.find( detail::MimetypeApplicationOctet ) == 0 ) { + startPos = std::char_traits::length( detail::MimetypeApplicationOctet ) + 1; + } + else if ( buffer.uri.find( detail::MimetypeGLTFBuffer ) == 0 ) { + startPos = std::char_traits::length( detail::MimetypeGLTFBuffer ) + 1; + } + + const std::size_t base64Length = buffer.uri.length() - startPos; + const std::size_t decodedEstimate = base64Length / 4 * 3; + if ( startPos == 0 || + ( decodedEstimate - 2 ) > buffer.byteLength ) // we need to give room for padding... + { + throw invalid_gltf_document( "Invalid buffer.uri value", "malformed base64" ); + } + +#if defined( FX_GLTF_HAS_CPP_17 ) + const bool success = base64::TryDecode( { &buffer.uri[startPos], base64Length }, buffer.data ); +#else + const bool success = base64::TryDecode( buffer.uri.substr( startPos ), buffer.data ); +#endif + if ( !success ) { + throw invalid_gltf_document( "Invalid buffer.uri value", "malformed base64" ); + } +} + +inline Document Create( nlohmann::json const& json, DataContext const& dataContext ) { + Document document = json; + + if ( document.buffers.size() > dataContext.readQuotas.MaxBufferCount ) { + throw invalid_gltf_document( "Quota exceeded : number of buffers > MaxBufferCount" ); + } + + for ( auto& buffer : document.buffers ) { + if ( buffer.byteLength == 0 ) { + throw invalid_gltf_document( "Invalid buffer.byteLength value : 0" ); + } + + if ( buffer.byteLength > dataContext.readQuotas.MaxBufferByteLength ) { + throw invalid_gltf_document( + "Quota exceeded : buffer.byteLength > MaxBufferByteLength" ); + } + + if ( !buffer.uri.empty() ) { + if ( buffer.IsEmbeddedResource() ) { detail::MaterializeData( buffer ); } + else { + std::ifstream fileData( + detail::CreateBufferUriPath( dataContext.bufferRootPath, buffer.uri ), + std::ios::binary ); + if ( !fileData.good() ) { + throw invalid_gltf_document( "Invalid buffer.uri value", buffer.uri ); + } + + buffer.data.resize( buffer.byteLength ); + fileData.read( reinterpret_cast( &buffer.data[0] ), buffer.byteLength ); + } + } + else if ( dataContext.binaryData != nullptr ) { + std::vector& binary = *dataContext.binaryData; + if ( binary.size() < buffer.byteLength ) { + throw invalid_gltf_document( "Invalid GLB buffer data" ); + } + + buffer.data.resize( buffer.byteLength ); + std::memcpy( &buffer.data[0], &binary[0], buffer.byteLength ); + } + } + + return document; +} + +inline void ValidateBuffers( Document const& document, bool useBinaryFormat ) { + if ( document.buffers.empty() ) { + throw invalid_gltf_document( + "Invalid glTF document. A document must have at least 1 buffer." ); + } + + bool foundBinaryBuffer = false; + for ( std::size_t bufferIndex = 0; bufferIndex < document.buffers.size(); bufferIndex++ ) { + Buffer const& buffer = document.buffers[bufferIndex]; + if ( buffer.byteLength == 0 ) { + throw invalid_gltf_document( "Invalid buffer.byteLength value : 0" ); + } + + if ( buffer.byteLength != buffer.data.size() ) { + throw invalid_gltf_document( + "Invalid buffer.byteLength value : does not match buffer.data size" ); + } + + if ( buffer.uri.empty() ) { + foundBinaryBuffer = true; + if ( bufferIndex != 0 ) { + throw invalid_gltf_document( + "Invalid glTF document. Only 1 buffer, the very first, is allowed to have an " + "empty buffer.uri field." ); + } + } + } + + if ( useBinaryFormat && !foundBinaryBuffer ) { + throw invalid_gltf_document( "Invalid glTF document. No buffer found which can meet the " + "criteria for saving to a .glb file." ); + } +} + +inline void Save( Document const& document, + std::ostream& output, + std::string const& documentRootPath, + bool useBinaryFormat ) { + // There is no way to check if an ostream has been opened in binary mode or not. Just checking + // if it's "good" is the best we can do from here... + detail::ThrowIfBad( output ); + + nlohmann::json json = document; + + std::size_t externalBufferIndex = 0; + if ( useBinaryFormat ) { + detail::GLBHeader header { detail::GLBHeaderMagic, 2, 0, { 0, detail::GLBChunkJSON } }; + detail::ChunkHeader binHeader { 0, detail::GLBChunkBIN }; + + std::string jsonText = json.dump(); + + Buffer const& binBuffer = document.buffers.front(); + const uint32_t binPaddedLength = ( ( binBuffer.byteLength + 3 ) & ( ~3u ) ); + const uint32_t binPadding = binPaddedLength - binBuffer.byteLength; + binHeader.chunkLength = binPaddedLength; + + header.jsonHeader.chunkLength = ( ( jsonText.length() + 3 ) & ( ~3u ) ); + const uint32_t headerPadding = + static_cast( header.jsonHeader.chunkLength - jsonText.length() ); + header.length = detail::HeaderSize + header.jsonHeader.chunkLength + + detail::ChunkHeaderSize + binHeader.chunkLength; + + constexpr std::array spaces = { ' ', ' ', ' ' }; + constexpr std::array nulls = { 0, 0, 0 }; + + output.write( reinterpret_cast( &header ), detail::HeaderSize ); + output.write( jsonText.c_str(), jsonText.length() ); + output.write( &spaces[0], headerPadding ); + output.write( reinterpret_cast( &binHeader ), detail::ChunkHeaderSize ); + output.write( reinterpret_cast( &binBuffer.data[0] ), binBuffer.byteLength ); + output.write( &nulls[0], binPadding ); + + externalBufferIndex = 1; + } + else { output << json.dump( 2 ); } + + // The glTF 2.0 spec allows a document to have more than 1 buffer. However, only the first one + // will be included in the .glb All others must be considered as External/Embedded resources. + // Process them if necessary... + for ( ; externalBufferIndex < document.buffers.size(); externalBufferIndex++ ) { + Buffer const& buffer = document.buffers[externalBufferIndex]; + if ( !buffer.IsEmbeddedResource() ) { + std::ofstream fileData( detail::CreateBufferUriPath( documentRootPath, buffer.uri ), + std::ios::binary ); + if ( !fileData.good() ) { + throw invalid_gltf_document( "Invalid buffer.uri value", buffer.uri ); + } + + fileData.write( reinterpret_cast( &buffer.data[0] ), buffer.byteLength ); + } + } +} +} // namespace detail + +inline Document LoadFromText( std::istream& input, + std::string const& documentRootPath, + ReadQuotas const& readQuotas = {} ) { + try { + detail::ThrowIfBad( input ); + + nlohmann::json json; + input >> json; + + return detail::Create( json, { documentRootPath, readQuotas } ); + } + catch ( invalid_gltf_document& ) { + throw; + } + catch ( std::system_error& ) { + throw; + } + catch ( ... ) { + std::throw_with_nested( + invalid_gltf_document( "Invalid glTF document. See nested exception for details." ) ); + } +} + +inline Document LoadFromText( std::string const& documentFilePath, + ReadQuotas const& readQuotas = {} ) { + std::ifstream input( documentFilePath ); + if ( !input.is_open() ) { + throw std::system_error( std::make_error_code( std::errc::no_such_file_or_directory ) ); + } + + return LoadFromText( input, detail::GetDocumentRootPath( documentFilePath ), readQuotas ); +} + +inline Document LoadFromBinary( std::istream& input, + std::string const& documentRootPath, + ReadQuotas const& readQuotas = {} ) { + try { + detail::GLBHeader header {}; + detail::ThrowIfBad( input.read( reinterpret_cast( &header ), detail::HeaderSize ) ); + if ( header.magic != detail::GLBHeaderMagic || + header.jsonHeader.chunkType != detail::GLBChunkJSON || + header.jsonHeader.chunkLength + detail::HeaderSize > header.length ) { + throw invalid_gltf_document( "Invalid GLB header" ); + } + + std::vector json {}; + json.resize( header.jsonHeader.chunkLength ); + detail::ThrowIfBad( + input.read( reinterpret_cast( &json[0] ), header.jsonHeader.chunkLength ) ); + + std::size_t totalSize = detail::HeaderSize + header.jsonHeader.chunkLength; + if ( totalSize > readQuotas.MaxFileSize ) { + throw invalid_gltf_document( "Quota exceeded : file size > MaxFileSize" ); + } + + detail::ChunkHeader binHeader {}; + detail::ThrowIfBad( + input.read( reinterpret_cast( &binHeader ), detail::ChunkHeaderSize ) ); + if ( binHeader.chunkType != detail::GLBChunkBIN ) { + throw invalid_gltf_document( "Invalid GLB header" ); + } + + totalSize += detail::ChunkHeaderSize + binHeader.chunkLength; + if ( totalSize > readQuotas.MaxFileSize ) { + throw invalid_gltf_document( "Quota exceeded : file size > MaxFileSize" ); + } + + std::vector binary {}; + binary.resize( binHeader.chunkLength ); + detail::ThrowIfBad( + input.read( reinterpret_cast( &binary[0] ), binHeader.chunkLength ) ); + + return detail::Create( nlohmann::json::parse( json.begin(), json.end() ), + { documentRootPath, readQuotas, &binary } ); + } + catch ( invalid_gltf_document& ) { + throw; + } + catch ( std::system_error& ) { + throw; + } + catch ( ... ) { + std::throw_with_nested( + invalid_gltf_document( "Invalid glTF document. See nested exception for details." ) ); + } +} + +inline Document LoadFromBinary( std::string const& documentFilePath, + ReadQuotas const& readQuotas = {} ) { + std::ifstream input( documentFilePath, std::ios::binary ); + if ( !input.is_open() ) { + throw std::system_error( std::make_error_code( std::errc::no_such_file_or_directory ) ); + } + + return LoadFromBinary( input, detail::GetDocumentRootPath( documentFilePath ), readQuotas ); +} + +inline void Save( Document const& document, + std::ostream& output, + std::string const& documentRootPath, + bool useBinaryFormat ) { + try { + detail::ValidateBuffers( document, useBinaryFormat ); + + detail::Save( document, output, documentRootPath, useBinaryFormat ); + } + catch ( invalid_gltf_document& ) { + throw; + } + catch ( std::system_error& ) { + throw; + } + catch ( ... ) { + std::throw_with_nested( + invalid_gltf_document( "Invalid glTF document. See nested exception for details." ) ); + } +} + +inline void +Save( Document const& document, std::string const& documentFilePath, bool useBinaryFormat ) { + std::ofstream output( documentFilePath, useBinaryFormat ? std::ios::binary : std::ios::out ); + Save( document, output, detail::GetDocumentRootPath( documentFilePath ), useBinaryFormat ); +} +} // namespace gltf + +// A general-purpose utility to format an exception hierarchy into a string for output +inline void FormatException( std::string& output, std::exception const& ex, int level = 0 ) { + output.append( std::string( level, ' ' ) ).append( ex.what() ); + try { + std::rethrow_if_nested( ex ); + } + catch ( std::exception const& e ) { + FormatException( output.append( "\n" ), e, level + 2 ); + } +} + +} // namespace fx + +#undef FX_GLTF_HAS_CPP_17 +#undef FX_GLTF_NODISCARD diff --git a/src/IO/filelist.cmake b/src/IO/filelist.cmake index 5ab4a4083d2..2fab2229b75 100644 --- a/src/IO/filelist.cmake +++ b/src/IO/filelist.cmake @@ -56,3 +56,54 @@ if(RADIUM_IO_VOLUMES) list(APPEND io_headers VolumesLoader/VolumeLoader.hpp VolumesLoader/pvmutils.hpp) endif(RADIUM_IO_VOLUMES) +if(RADIUM_IO_GLTF) + list(APPEND io_sources Gltf/Loader/glTFFileLoader.cpp) + + list( + APPEND + io_sources + Gltf/internal/GLTFConverter/AccessorReader.cpp + Gltf/internal/GLTFConverter/Converter.cpp + Gltf/internal/GLTFConverter/HandleData.cpp + Gltf/internal/GLTFConverter/MaterialConverter.cpp + Gltf/internal/GLTFConverter/MeshData.cpp + Gltf/internal/GLTFConverter/NormalCalculator.cpp + Gltf/internal/GLTFConverter/SceneNode.cpp + Gltf/internal/GLTFConverter/TangentCalculator.cpp + Gltf/internal/GLTFConverter/TransformationManager.cpp + ) + + list(APPEND io_sources Gltf/internal/GLTFConverter/mikktspace.c) + + list(APPEND io_headers Gltf/Loader/glTFFileLoader.hpp) + + list( + APPEND + io_private_headers + Gltf/internal/GLTFConverter/AccessorReader.hpp + Gltf/internal/GLTFConverter/Converter.hpp + Gltf/internal/GLTFConverter/HandleData.hpp + Gltf/internal/GLTFConverter/ImageData.hpp + Gltf/internal/GLTFConverter/MaterialConverter.hpp + Gltf/internal/GLTFConverter/MeshData.hpp + Gltf/internal/GLTFConverter/NormalCalculator.hpp + Gltf/internal/GLTFConverter/SceneNode.hpp + Gltf/internal/GLTFConverter/TangentCalculator.hpp + Gltf/internal/GLTFConverter/TransformationManager.hpp + ) + + list(APPEND io_private_headers Gltf/internal/GLTFConverter/mikktspace.h) + + list(APPEND io_private_headers Gltf/internal/Extensions/LightExtensions.hpp + Gltf/internal/Extensions/MaterialExtensions.hpp + ) + + list(APPEND io_private_headers Gltf/internal/fx/gltf.h) + + if(RADIUM_IO_GLTF_WRITER) + list(APPEND io_sources Gltf/Writer/glTFFileWriter.cpp) + + list(APPEND io_headers Gltf/Writer/glTFFileWriter.hpp) + + endif(RADIUM_IO_GLTF_WRITER) +endif(RADIUM_IO_GLTF) From c1969caebc063597d75790afb2bdccd09a32a27d Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Wed, 19 Jul 2023 19:15:16 +0200 Subject: [PATCH 06/27] [gui] add gltf 2.0 custom file loader support --- src/Gui/BaseApplication.cpp | 11 ++++++++--- src/Gui/CMakeLists.txt | 13 +++++++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Gui/BaseApplication.cpp b/src/Gui/BaseApplication.cpp index 8681c07ab92..d13d161471d 100644 --- a/src/Gui/BaseApplication.cpp +++ b/src/Gui/BaseApplication.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -17,7 +16,6 @@ #include #include #include -#include #include #include #include @@ -35,6 +33,9 @@ #ifdef IO_HAS_VOLUMES # include #endif +#ifdef IO_HAS_GLTF +# include +#endif #include #include #include @@ -320,9 +321,13 @@ void BaseApplication::initialize( const WindowFactory& factory, } // == Configure bundled Radium::IO services == // // Make builtin loaders the fallback if no plugins can load some file format -#ifdef IO_HAS_TINYPLY // Register before AssimpFileLoader, in order to ease override of such // custom loader (first loader able to load is taking the file) +#ifdef IO_HAS_GLTF + m_engine->registerFileLoader( + std::shared_ptr( new IO::GLTF::glTFFileLoader() ) ); +#endif +#ifdef IO_HAS_TINYPLY m_engine->registerFileLoader( std::shared_ptr( new IO::TinyPlyFileLoader() ) ); #endif diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index c348f148ee9..8e2ea303db0 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -45,18 +45,23 @@ endif() # Ask RadiumIO for supported loaders get_target_property(USE_ASSIMP IO IO_HAS_ASSIMP) if(${USE_ASSIMP}) - target_compile_definitions(${ra_gui_target} PRIVATE "-DIO_HAS_ASSIMP") + target_compile_definitions(${ra_gui_target} PRIVATE IO_HAS_ASSIMP) endif() get_target_property(USE_TINYPLY IO IO_HAS_TINYPLY) if(${USE_TINYPLY}) - target_compile_definitions(${ra_gui_target} PRIVATE "-DIO_HAS_TINYPLY") + target_compile_definitions(${ra_gui_target} PRIVATE IO_HAS_TINYPLY) endif() get_target_property(HAS_VOLUMES IO IO_HAS_VOLUMES) if(${HAS_VOLUMES}) - target_compile_definitions(${ra_gui_target} PRIVATE "-DIO_HAS_VOLUMES") + target_compile_definitions(${ra_gui_target} PRIVATE IO_HAS_VOLUMES) endif() -target_compile_definitions(${ra_gui_target} PRIVATE "-DRA_GUI_EXPORTS") +get_target_property(HAS_GLTF IO IO_HAS_GLTF) +if(${HAS_GLTF}) + target_compile_definitions(${ra_gui_target} PRIVATE IO_HAS_GLTF) +endif() + +target_compile_definitions(${ra_gui_target} PRIVATE RA_GUI_EXPORTS) message(STATUS "Configuring library ${ra_gui_target} with standard settings") configure_radium_target(${ra_gui_target}) From 8d8c397967b80f5deacd2fb9438da28e208dcff0 Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Wed, 19 Jul 2023 19:16:04 +0200 Subject: [PATCH 07/27] [example] use gltf 2.0 custom file writer if enable --- examples/MaterialEdition/CMakeLists.txt | 17 ++++------------- examples/MaterialEdition/main.cpp | 20 ++++---------------- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/examples/MaterialEdition/CMakeLists.txt b/examples/MaterialEdition/CMakeLists.txt index bbada6d7dd5..d072a4313bd 100644 --- a/examples/MaterialEdition/CMakeLists.txt +++ b/examples/MaterialEdition/CMakeLists.txt @@ -46,19 +46,10 @@ add_executable(${PROJECT_NAME} ${app_sources} ${app_headers} ${app_uis} ${app_re target_link_libraries(${PROJECT_NAME} PUBLIC Radium::Gui ${Qt_LIBRARIES}) # ------------------------------------------------------------------------------ -# RadiumGlTF is available here https://gitlab.irit.fr/storm/repos/radium/libgltf.git (branch -# Material_Edition_#950). Compile and install RadiumGlTF first, e.g. into -# "path/to/RadiumGlTFinstall/", then configure using cmake [your configure args] -# -DRadiumGlTF_DIR="path/to/RadiumGlTFinstall/lib/cmake" -DUSE_RADIUMGLTF=ON to use it for this -# example. -option(USE_RADIUMGLTF "Enable loading/saving files with RadiumGltf extension" OFF) -if(USE_RADIUMGLTF) - message(STATUS "${PROJECT_NAME} uses RadiumGltf extension") - # TODO : find why this find_package is needed (at least on MacOs whe ). - find_package(OpenMP QUIET) - find_package(RadiumGlTF REQUIRED) - target_compile_definitions(${PROJECT_NAME} PUBLIC USE_RADIUMGLTF) - target_link_libraries(${PROJECT_NAME} PUBLIC RadiumGlTF::RadiumGlTF) +get_target_property(HAS_GLTF_WRITER Radium::IO IO_HAS_GLTF_WRITER) +if(HAS_GLTF_WRITER) + message(STATUS "${PROJECT_NAME} uses RadiumGltf writer extension") + target_compile_definitions(${PROJECT_NAME} PRIVATE HAS_GLTF_WRITER) endif() configure_radium_app(NAME ${PROJECT_NAME}) diff --git a/examples/MaterialEdition/main.cpp b/examples/MaterialEdition/main.cpp index 22315b33213..12097af07ba 100644 --- a/examples/MaterialEdition/main.cpp +++ b/examples/MaterialEdition/main.cpp @@ -35,11 +35,9 @@ #include #include -#ifdef USE_RADIUMGLTF +#ifdef HAS_GLTF_WRITER # include -# include # include -# include #endif using namespace Ra::Gui::Widgets; @@ -74,7 +72,7 @@ class DemoWindow : public Ra::Gui::SimpleWindow allexts.append( exts + " " ); filter.append( QString::fromStdString( loader->name() ) + " (" + exts + ");;" ); } - // add a filter concetenatting all the supported extensions + // add a filter concatenatting all the supported extensions filter.prepend( "Supported files (" + allexts + ");;" ); // remove the last ";;" of the string @@ -95,11 +93,7 @@ class DemoWindow : public Ra::Gui::SimpleWindow }; connect( fileOpenAction, &QAction::triggered, openFile ); -#ifdef USE_RADIUMGLTF - // register the gltf loader - std::shared_ptr loader = - std::make_shared(); - Ra::Engine::RadiumEngine::getInstance()->registerFileLoader( loader ); +#ifdef HAS_GLTF_WRITER // allow to save in gltf format auto fileSaveAction = new QAction( "&Save..." ); @@ -117,7 +111,7 @@ class DemoWindow : public Ra::Gui::SimpleWindow entities.begin(), entities.end(), std::back_inserter( toExport ), []( auto e ) { return e != Ra::Engine::Scene::SystemEntity::getInstance(); } ); - GLTF::glTFFileWriter writer { fileName.toStdString(), "textures/", false }; + Ra::IO::GLTF::glTFFileWriter writer { fileName.toStdString(), "textures/", false }; writer.write( toExport ); }; connect( fileSaveAction, &QAction::triggered, saveFile ); @@ -196,12 +190,6 @@ int main( int argc, char* argv[] ) { //! [Initializing the application] app.initialize( DemoWindowFactory() ); -#ifdef USE_RADIUMGLTF - app.m_mainWindow->getViewer()->makeCurrent(); - // initialize the use of GLTF library - GLTF::initializeGltf(); - app.m_mainWindow->getViewer()->doneCurrent(); -#endif app.setContinuousUpdate( false ); //! [Initializing the application] From 987def4a71a20f81e40666c0e9a8d2624bb93e27 Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Wed, 19 Jul 2023 22:15:20 +0200 Subject: [PATCH 08/27] [io] fix precompile header problem other language than C++ --- src/IO/CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/IO/CMakeLists.txt b/src/IO/CMakeLists.txt index 681b30062e6..6ff33ed3d23 100644 --- a/src/IO/CMakeLists.txt +++ b/src/IO/CMakeLists.txt @@ -76,5 +76,7 @@ configure_radium_library( set(RADIUM_COMPONENTS ${RADIUM_COMPONENTS} ${ra_io_target} PARENT_SCOPE) if(RADIUM_ENABLE_PCH) - target_precompile_headers(${ra_io_target} PRIVATE pch.hpp) + target_precompile_headers( + ${ra_io_target} PRIVATE "$<$:${CMAKE_CURRENT_SOURCE_DIR}/pch.hpp>" + ) endif() From 59fd39a7d6ef14e86a287a7cd299450750539232 Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Wed, 19 Jul 2023 22:15:52 +0200 Subject: [PATCH 09/27] [radium] add GLTF status in the configure summary --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index d80b1bddacb..e0d20f64b04 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -299,6 +299,8 @@ if(NOT ${RADIUM_MISSING_COMPONENTS} STREQUAL "") message_info(" -- Missing components: ${COMPONENTS_LIST} (see log to find why)") endif() message_setting("RADIUM_IO_ASSIMP") +message_setting("RADIUM_IO_GLTF") +message_setting("RADIUM_IO_GLTF_WRITER") message_setting("RADIUM_IO_TINYPLY") message_setting("RADIUM_IO_VOLUMES") message_setting("RADIUM_IO_DEPRECATED") From b4a2c48e8a9b31706630c7c64441603f3844c061 Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Wed, 19 Jul 2023 22:48:00 +0200 Subject: [PATCH 10/27] [doc] add GLTF 2.0 support status in the documentation --- doc/concepts.md | 1 + doc/concepts/gltfconformance.md | 250 ++++++++++++++++++++++++++++++++ doc/developer/material.md | 2 + 3 files changed, 253 insertions(+) create mode 100644 doc/concepts/gltfconformance.md diff --git a/doc/concepts.md b/doc/concepts.md index 91f1f94b593..4cf7c8c3b4f 100644 --- a/doc/concepts.md +++ b/doc/concepts.md @@ -5,3 +5,4 @@ * \subpage eventSystem * \subpage pluginSystem * \subpage forwardRenderer +* \subpage gltfConformance diff --git a/doc/concepts/gltfconformance.md b/doc/concepts/gltfconformance.md new file mode 100644 index 00000000000..66d8c91cceb --- /dev/null +++ b/doc/concepts/gltfconformance.md @@ -0,0 +1,250 @@ +\page gltfConformance GLTF 2.0 support status +[TOC] + +# GLTF (2.0) support for Radium engine + +This document indicates, from the main structures defined in the GLTF2.0 specification, what is supported, +partly supported or not supported. + +For more information about gltf file format and its use by 3D engines/apps visit https://www.khronos.org/gltf/ + +## GLTF2.0 coverage + +The implementation of the gltf specification is tested (interactively and visually) using several _official_ gltf +sample models available at https://github.com/KhronosGroup/glTF-Sample-Models. + +The structure of this document is the same than those of the +[official specification](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0) + +* [Concepts](#concepts) + * [Asset](#asset) + * [Indices and Names](#indices-and-names) + * [Coordinate System and Units](#coordinate-system-and-units) + * [Scenes](#scenes) + * [Nodes and Hierarchy](#nodes-and-hierarchy) + * [Transformations](#transformations) + * [Binary Data Storage](#binary-data-storage) + * [Buffers and Buffer Views](#buffers-and-buffer-views) + * [GLB-stored Buffer](#glb-stored-buffer) + * [Accessors](#accessors) + * [Accessors Bounds](#accessors-bounds) + * [Sparse Accessors](#sparse-accessors) + * [Data Alignment](#data-alignment) + * [Geometry](#geometry) + * [Meshes](#meshes) + * [Tangent-space definition](#tangent-space-definition) + * [Morph Targets](#morph-targets) + * [Skins](#skins) + * [Skinned Mesh Attributes](#skinned-mesh-attributes) + * [Joint Hierarchy](#joint-hierarchy) + * [Instantiation](#instantiation) + * [Texture Data](#texture-data) + * [Textures](#textures) + * [Images](#images) + * [Samplers](#samplers) + * [Materials](#materials) + * [Metallic-Roughness Material](#metallic-roughness-material) + * [Additional Maps](#additional-maps) + * [Alpha Coverage](#alpha-coverage) + * [Double Sided](#double-sided) + * [Default Material](#default-material) + * [Point and Line Materials](#point-and-line-materials) + * [Cameras](#cameras) + * [Projection Matrices](#projection-matrices) + * [Animations](#animations) + * [Specifying Extensions](#specifying-extensions) + * [Supported extensions](#supported-extensions) + +## Concepts {#concepts} + +### Asset {#asset} + +The `asset` node ([specification](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#reference-asset)) +is parsed when loading a glTF file and its content is logged in the Radium `LogINFO` logger. + +### Indices and Names {#indices-and-names} + +Names of glTF entities are transmitted to the Radium Engine. These names then serve as keys for several resources management components. + +### Coordinate System and Units {#coordinate-system-and-units} + +glTF uses a right-handed coordinate system that correspond to the coordinate system of Radium. +The test [Boom with axes](https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/BoomBoxWithAxes) pass. + +### Scenes {#scenes} + +All the scenes are loaded but only the one referenced by the `scene` property is converted to Radium asset. +If `scene`is undefined, the scene `0` is converted. + +#### Nodes and Hierarchy {#nodes-and-hierarchy} + +After loading, the node hierarchy defined in the glTF file is flattened. + +> In a future version of Radium, node hierarchy should be maintained. + +#### Transformations {#transformations} + +All the glTF transformations are integrated when flattening the nodes tree. + +### Binary Data Storage {#binary-data-storage} + +#### Buffers and Buffer Views {#buffers-and-buffer-views} + +All buffers are loaded when parsing a glTF file. +Buffer views will be used to extract data from buffers and fill Radium IO data structure. +The `target` property of a buffer is not used by the loader that infer usage of the buffer from the mesh `accessor` properties. + +> the `byteStride` property, defined only for buffer views that contain vertex attributes, is not used in the converter. +Even if all supported sample models loads well (as well as sketchfab downloaded models) this should be taken it into +account in the near future. + +##### GLB-stored Buffer {#glb-stored-buffer} + +GLB-stored buffer are transparently managed by the loader. + +#### Accessors {#accessors} + +Accessors are used to build `RadiumIO` compatible description of the Geometry. + +##### Accessors Bounds {#accessors-bounds} + +The `min`and `max`properties are not used as they are recomputed by Radium when building a mesh. + +##### Sparse Accessors {#sparse-accessors} + +Not tested. + +##### Data Alignment {#data-alignment} + +For the moment, data are considered as non aligned. + +> Should take into account alignment to handle all the test cases. + +### Geometry {#geometry} + +Only non morphed meshes are managed. +The loader read all from the file but the Radium converter ignores morphing based animation targets. + +#### Meshes {#meshes} + +The way a mesh is divided into `primitives` is kept while building radium geometries. +Each primitive defines a Radium mesh on its own. + +Due to restriction in Radium, only the texture coord layer 0 (`TEXCOORD_0`) is converted and used. + +> In the next version of Radium, both `TEXCOORD_0` and `TEXCOORD_1` should be made available. + +The `COLOR_0` property is not used for now. + +> In a near future, this should be OK as Radium manage color attributes per vertex. + +Weighs and joints are correctly loaded and transmitted to Radium for skeleton based animation. + +> Du to limitations in the Radium animation system, regular nodes and nodes hierarchies attached to a joint are not +handled the way it is requested by the gtlf specification. + +If normals are not given, they are computed, as well as tangent, according to the GLTF specification. + +##### Tangent-space definition {#tangent-space-definition} + +If tangent are not defined, the loader use the [MikkTSpace algorithm](http://image.diku.dk/projects/media/morten.mikkelsen.08.pdf), +in its original implementation by the author to compute the tangents. This respect the GLTF specification. + +##### Morph Targets {#morph-targets} + +Not yet managed by the converter. + +#### Skins {#skins} + +Skins are loaded and skeletons are build according to the specification but with the restriction imposed by Radium +for now (one single mesh per skeleton, no node hierarchy attached to a joint, ...) + +### Texture Data {#texture-data} + +glTF separates texture access into three distinct types of objects: Textures, Images, and Samplers. +For the moment, Radium only represents textures by a single type of object. +Several conformance tests, where the same image is used with different samplers, do not pass yet. + +> In an upcomming Radium PR, textures should be managed as an association of images and samplers. + +#### Images {#images} + +Only images defined by a URI to an external file are managed. + +> In the future, allow to build textures from already defined data instead of an external file. +Also allow to generate procedural textures (needed by some glTF extensions) + +#### Samplers {#samplers} + +Sampler are used to parameterize the Radium textures. Due to limitations in texture management in Radium, there are +some inconsistencies in the sampler management. +When a texture is referenced through several samplers, only the first will be used for all the instances. + +### Materials {#materials} + +Physically based rendering is supported in Radium. glTF materials are fully supported if they belong to the core specification. +Adding support for other materials might be done easily as soon as there specification and json schema are known. + +#### Metallic-Roughness Material {#metallic-roughness-material} + +This material is integrated in the glTF plugin. + +#### Additional Maps {#additional-maps} + +The main texture maps defined by the specification are integrated. + +#### Alpha Coverage {#alpha-coverage} + +Alpha coverage test succeed as soon as there is no sampler collision (see above) + +#### Double Sided {#double-sided} + +Double sided materials are manage directly in the shader by discarding (or not) fragments that are back-side. + +#### Point and Line Materials {#point-and-line-materials} + +Not yet supported. + +### Cameras {#cameras} + +Camera are loaded and transmitted to Radium. + +#### Projection Matrices {#projection-matrices} + +Both perspective and orthogonal projection matrices are managed + +### Animations {#animations} + +Animation key-framed definition is properly loaded and transmitted to Radium. Note that all interpolators are transmitted to Radium as the linear one. + +> In a next version, support all the key-frames interpolators. + +### Specifying Extensions {#specifying-extensions} + +glTF defines an extension mechanism that allows the base format to be extended with new capabilities. +Any glTF object can have an optional `extensions` property, as in the following example: + +#### Supported extensions {#supported-extensions} + +* __KHR_materials_pbrSpecularGlossiness__, defines a PBR material based on specular color and + shininess exponent is supported (spec at https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness). + +* __KHR_texture_transform__, allows to apply transformation on texture coordinate is supported + (spec at https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_transform) + +* __KHR_lights_punctual__, defines three "punctual" light types: directional, point and spot. +Punctual lights are defined as parameterized, infinitely small points that emit light in well-defined directions and +intensities. (spec at https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_lights_punctual) + +* __KHR_materials_ior__, allows users to set the index of refraction of a material. +(spec at https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_ior) + +* __KHR_materials_clearcoat__, represents a protective layer applied to a base material. +(spec at https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_clearcoat) + +* __KHR_materials_specular__, allows users to configure the strength of the specular reflection in the dielectric BRDF. +(spec at https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_specular) + +* __KHR_materials_sheen__, defines a sheen that can be layered on top of an existing glTF material definition. A sheen +layer is a common technique used in Physically-Based Rendering to represent cloth and fabric materials +(spec at https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_sheen) diff --git a/doc/developer/material.md b/doc/developer/material.md index 963f567a88c..493dad3dc6f 100644 --- a/doc/developer/material.md +++ b/doc/developer/material.md @@ -23,7 +23,9 @@ to the Ra::Engine::Rendering::ForwardRenderer default renderer. The Radium Material Library defines two default material : - BlinnPhong, Ra::Engine::Data::BlinnPhongMaterial, corresponding to the Blinn-Phong BSDF. +- Lambertian, Ra::Engine::Data::LambertianMaterial, corresponding to the _Lambertian_ (Oren-Nayar model) BSDF. - Plain, Ra::Engine::Data::PlainMaterial, corresponding to a diffuse, lambertian BSDF. +- GLTF materials, see chapter on [GLTF 2.0 support](@ref gltfConformance) ). The _Radium Material Library_ can be used as this by any Radium Application or can be extended by an application or a Radium Plugin by implementing the corresponding interfaces as described in the From 78349ba53ac2240db8799716dd6bd3c60c36aaaf Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Thu, 20 Jul 2023 07:48:01 +0200 Subject: [PATCH 11/27] [io] fix codacy on gltf internals --- .../internal/GLTFConverter/AccessorReader.cpp | 41 ++++++++++--------- .../internal/GLTFConverter/AccessorReader.hpp | 4 +- .../Gltf/internal/GLTFConverter/Converter.cpp | 12 +++--- .../internal/GLTFConverter/HandleData.cpp | 9 ++-- .../GLTFConverter/MaterialConverter.cpp | 21 +++++----- .../Gltf/internal/GLTFConverter/MeshData.hpp | 2 +- 6 files changed, 48 insertions(+), 41 deletions(-) diff --git a/src/IO/Gltf/internal/GLTFConverter/AccessorReader.cpp b/src/IO/Gltf/internal/GLTFConverter/AccessorReader.cpp index 88088d7d344..eaa6cdf8d36 100644 --- a/src/IO/Gltf/internal/GLTFConverter/AccessorReader.cpp +++ b/src/IO/Gltf/internal/GLTFConverter/AccessorReader.cpp @@ -33,9 +33,9 @@ template void minmax( uint8_t* data, const gltf::Accessor& accessor, int nbComponents ) { std::vector min; std::vector max; - T defaultMin = std::numeric_limits::min(); - T defaultMax = std::numeric_limits::max(); - auto* convertedData = (T*)data; + T defaultMin = std::numeric_limits::min(); + T defaultMax = std::numeric_limits::max(); + auto convertedData = reinterpret_cast( data ); if ( accessor.min.empty() ) { min = std::vector( nbComponents, defaultMin ); } else { std::transform( accessor.min.begin(), @@ -66,7 +66,7 @@ void sparseData( uint8_t* data, int nbBytesByComponents, const uint8_t* indices, const uint8_t* values ) { - auto* indicesT = (T*)indices; + auto indicesT = reinterpret_cast( indices ); for ( int i = 0; i < sparse.count; ++i ) { for ( int j = 0; j < nbBytesByComponents; ++j ) { data[indicesT[i] * nbBytesByComponents + j] = values[i * nbBytesByComponents + j]; @@ -77,7 +77,7 @@ void sparseData( uint8_t* data, template uint8_t* normalizeData( uint8_t* data, uint32_t nbComponents ) { T max = std::numeric_limits::max(); - T* dataT = (T*)data; + auto dataT = reinterpret_cast( data ); auto* dataFloat = new float[nbComponents]; for ( uint32_t i = 0; i < nbComponents; ++i ) { dataFloat[i] = std::max( dataT[i] / (float)max, -1.0f ); @@ -205,20 +205,23 @@ uint8_t* AccessorReader::read( int32_t accessorIndex ) { return m_accessors[accessorIndex]; } const gltf::Document& doc = m_doc; - gltf::Accessor accessor = doc.accessors[accessorIndex]; - int nbByteByValue = nbByteByValueMap.at( accessor.componentType ); - int nbValueByComponent = nbValueByComponentMap.at( accessor.type ); - uint8_t* data = readBufferView( doc, - accessor.bufferView, - accessor.byteOffset, - accessor.count, - nbValueByComponent, - nbByteByValue ); - // sparse and min-max - sparseCapDataNormalize( data, doc, accessorIndex ); - // add data to map - m_accessors.insert( std::pair( accessorIndex, data ) ); - return data; + if ( 0 <= accessorIndex && accessorIndex < int32_t( doc.accessors.size() ) ) { + gltf::Accessor accessor = doc.accessors[accessorIndex]; + int nbByteByValue = nbByteByValueMap.at( accessor.componentType ); + int nbValueByComponent = nbValueByComponentMap.at( accessor.type ); + uint8_t* data = readBufferView( doc, + accessor.bufferView, + accessor.byteOffset, + accessor.count, + nbValueByComponent, + nbByteByValue ); + // sparse and min-max + sparseCapDataNormalize( data, doc, accessorIndex ); + // add data to map + m_accessors.insert( std::pair( accessorIndex, data ) ); + return data; + } + return nullptr; } } // namespace GLTF diff --git a/src/IO/Gltf/internal/GLTFConverter/AccessorReader.hpp b/src/IO/Gltf/internal/GLTFConverter/AccessorReader.hpp index 81d0aca2a80..fb55a7d73d7 100644 --- a/src/IO/Gltf/internal/GLTFConverter/AccessorReader.hpp +++ b/src/IO/Gltf/internal/GLTFConverter/AccessorReader.hpp @@ -32,7 +32,9 @@ class AccessorReader * Read the accessor * @param accessorIndex index of the gltf's accessor * @return a pointer to the data. The pointer can be cast the the corresponding type. - * If the data should be normalized, the stored data's type is float + * If the data should be normalized, the stored data's type is float. returns nullptr if + * accessorIndex is invalid + * */ uint8_t* read( int32_t accessorIndex ); diff --git a/src/IO/Gltf/internal/GLTFConverter/Converter.cpp b/src/IO/Gltf/internal/GLTFConverter/Converter.cpp index d47ff3b6968..38ba2b24706 100644 --- a/src/IO/Gltf/internal/GLTFConverter/Converter.cpp +++ b/src/IO/Gltf/internal/GLTFConverter/Converter.cpp @@ -57,7 +57,7 @@ bool checkExtensions( const gltf::Document& gltfscene ) { LightData* getLight( const gltf_KHR_lights_punctual& lights, int32_t lightIndex, const Transform& transform ) { - if ( lightIndex < lights.lights.size() ) { + if ( lightIndex < int32_t( lights.lights.size() ) ) { const auto gltfLight = lights.lights[lightIndex]; std::string lightName = gltfLight.name; if ( lightName.empty() ) { lightName = "light_" + std::to_string( lightIndex ); } @@ -104,7 +104,7 @@ getLight( const gltf_KHR_lights_punctual& lights, int32_t lightIndex, const Tran Camera* buildCamera( const gltf::Document& doc, int32_t cameraIndex, const Transform& parentTransform, - const std::string& filePath, + const std::string& /*filePath*/, int32_t nodeNum ) { // Radium Camera have problems if there is a scaling in the matrix : remove the scaling // TODO : verify and check against the gltf specification @@ -230,8 +230,8 @@ void buildAnimation( std::vector& animations, gltf::Animation::Sampler sampler = samplers[channel.sampler]; // weights' and scale's animations not handle by radium => now it does, so what is that // comment for? - auto* times = (float*)accessorReader.read( sampler.input ); - auto* transformation = (float*)accessorReader.read( sampler.output ); + auto times = reinterpret_cast( accessorReader.read( sampler.input ) ); + auto transformation = reinterpret_cast( accessorReader.read( sampler.output ) ); transformationManager.insert( target.node, target.path, times, @@ -335,14 +335,14 @@ bool Converter::operator()( const gltf::Document& gltfscene ) { if ( !gltfscene.animations.empty() ) { int activeAnimation = 0; // find the first animation that affect the scene - while ( activeAnimation < gltfscene.animations.size() && + while ( activeAnimation < int( gltfscene.animations.size() ) && ( visitedNodes.find( gltfscene.animations[activeAnimation].channels[0].target.node ) == visitedNodes.end() ) ) { ++activeAnimation; } // if animation found - if ( activeAnimation < gltfscene.animations.size() ) { + if ( activeAnimation < int( gltfscene.animations.size() ) ) { auto animationData = new AnimationData(); // set m_dt animationData->setTimeStep( 1.0f / 60.0f ); diff --git a/src/IO/Gltf/internal/GLTFConverter/HandleData.cpp b/src/IO/Gltf/internal/GLTFConverter/HandleData.cpp index 81619d453e4..eb53bc019e5 100644 --- a/src/IO/Gltf/internal/GLTFConverter/HandleData.cpp +++ b/src/IO/Gltf/internal/GLTFConverter/HandleData.cpp @@ -63,7 +63,7 @@ HandleDataLoader::loadSkeleton( const fx::gltf::Document& gltfScene, size_t nodeNum = 0; for ( auto visited : visitedNodes ) { auto& graphNode = graphNodes[visited]; - if ( graphNode.m_skinIndex == skinIndex ) { + if ( graphNode.m_skinIndex == int32_t( skinIndex ) ) { addMeshesWeightsAndBindMatrices( gltfScene, graphNode, nodeNum, @@ -85,7 +85,7 @@ HandleDataLoader::loadSkeleton( const fx::gltf::Document& gltfScene, std::set skinnedNodes; // fill all the joints data - for ( int32_t i = 0; i < skeletonJoints.size(); ++i ) { + for ( int32_t i = 0; i < int32_t( skeletonJoints.size() ); ++i ) { // initialize the weighs and bind matrices for ( auto it : allJointWeights ) { if ( !it.second[i].empty() ) { skeletonJoints[i].m_weights[it.first] = it.second[i]; } @@ -142,7 +142,8 @@ std::vector HandleDataLoader::getBindMatrices( const fx::gltf::Document& gltfScene, const fx::gltf::Skin& skin ) { std::vector jointBindMatrix( skin.joints.size(), Transform::Identity() ); - float* invBindMatrices = (float*)AccessorReader( gltfScene ).read( skin.inverseBindMatrices ); + auto* invBindMatrices = + reinterpret_cast( AccessorReader( gltfScene ).read( skin.inverseBindMatrices ) ); for ( uint i = 0; i < skin.joints.size(); ++i ) { Matrix4 mat; mat << invBindMatrices[16 * i], invBindMatrices[16 * i + 1], invBindMatrices[16 * i + 2], @@ -235,7 +236,7 @@ void HandleDataLoader::buildSkeletonTopology( const std::vector& grap const auto& skeletonJoints = handle->getComponentData(); for ( auto& component : skeletonJoints ) { const auto& node = graphNodes[componentNameToNodeNum.at( component.m_name )]; - for ( auto j = 0; j < node.children.size(); ++j ) { + for ( auto j = 0; j < int32_t( node.children.size() ); ++j ) { const auto itChild = skinnedNodes.find( node.children[j] ); if ( itChild != skinnedNodes.end() ) { edgeList.emplace_back( std::pair { diff --git a/src/IO/Gltf/internal/GLTFConverter/MaterialConverter.cpp b/src/IO/Gltf/internal/GLTFConverter/MaterialConverter.cpp index 6f0f5677639..a548975a4b7 100644 --- a/src/IO/Gltf/internal/GLTFConverter/MaterialConverter.cpp +++ b/src/IO/Gltf/internal/GLTFConverter/MaterialConverter.cpp @@ -78,7 +78,8 @@ GLTFSampler convertSampler( const fx::gltf::Sampler& sampler ) { return rasampler; } -void getMaterialExtensions( const nlohmann::json& extensionsAndExtras, BaseGLTFMaterial* mat ) { +void getMaterialExtensions( const nlohmann::json& /*extensionsAndExtras*/, + BaseGLTFMaterial* /*mat*/ ) { // Manage non standard material extensions #if 0 if ( !extensionsAndExtras.empty() ) { @@ -190,15 +191,15 @@ std::map> instanciateExtension { { "KHR_materials_ior", - []( const gltf::Document& doc, - const std::string& filePath, + []( const gltf::Document& /*doc*/, + const std::string& /*filePath*/, const nlohmann::json& jsonData, const std::string& basename ) { gltf_KHRMaterialsIor data; from_json( jsonData, data ); auto built = std::make_unique( basename + " - IOR" ); built->m_ior = data.ior; - return std::move( built ); + return built; } }, { "KHR_materials_clearcoat", []( const gltf::Document& doc, @@ -261,7 +262,7 @@ std::mapm_clearcoatNormalTextureTransform ); } - return std::move( built ); + return built; } }, { "KHR_materials_specular", []( const gltf::Document& doc, @@ -308,7 +309,7 @@ std::mapm_specularColorTextureTransform ); } - return std::move( built ); + return built; } }, { "KHR_materials_sheen", []( const gltf::Document& doc, @@ -357,19 +358,19 @@ std::mapm_sheenRoughnessTextureTransform ); } - return std::move( built ); + return built; } } }; void getMaterialExtensions( const gltf::Document& doc, const std::string& filePath, const MaterialData& meshMaterial, BaseGLTFMaterial* mat, - const std::vector exept = {} ) { + const std::vector& except = {} ) { auto extensionsAndExtras = meshMaterial.Data().extensionsAndExtras; if ( !extensionsAndExtras.empty() ) { auto extensions = extensionsAndExtras.find( "extensions" ); if ( extensions != extensionsAndExtras.end() ) { - // first search for unlit extension becaus it will prevent the use of other extensions + // first search for unlit extension because it will prevent the use of other extensions // (Specification of 12/2021) // https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_unlit // Here, according to "Implementation Note: When KHR_materials_unlit is included with @@ -382,7 +383,7 @@ void getMaterialExtensions( const gltf::Document& doc, // load supported extensions auto& extensionList = *extensions; for ( auto& [key, value] : extensionList.items() ) { - if ( std::any_of( exept.begin(), exept.end(), [k = key]( const auto& e ) { + if ( std::any_of( except.begin(), except.end(), [k = key]( const auto& e ) { return e == k; } ) ) { continue; diff --git a/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp b/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp index 507851693a2..630d3611d13 100644 --- a/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp +++ b/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp @@ -183,7 +183,7 @@ class MeshData else { switch ( buf.Accessor->componentType ) { case fx::gltf::Accessor::ComponentType::Float: { - auto mem = reinterpret_cast( buf.Data ); + const auto mem = reinterpret_cast( buf.Data ); for ( uint32_t i = 0; i < buf.Accessor->count; ++i ) { weights.push_back( Ra::Core::Vector4f { mem[4 * i], mem[4 * i + 1], mem[4 * i + 2], mem[4 * i + 3] } ); From 7dc07cfe4f82e54effa63f8d1e6581e38a854b82 Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Thu, 20 Jul 2023 08:56:45 +0200 Subject: [PATCH 12/27] [io] fix codacy on gltf internals --- src/IO/Gltf/internal/GLTFConverter/MeshData.hpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp b/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp index 630d3611d13..21608147a82 100644 --- a/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp +++ b/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp @@ -174,7 +174,7 @@ class MeshData static void GetWeights( Ra::Core::VectorArray& weights, const fx::gltf::Document& doc, const fx::gltf::Accessor& accessor ) { - MeshData::BufferInfo buf = MeshData::GetData( doc, accessor ); + const auto buf = MeshData::GetData( doc, accessor ); if ( buf.HasData() ) { if ( buf.Accessor->type != fx::gltf::Accessor::Type::Vec4 ) { LOG( Ra::Core::Utils::logERROR ) << "GLTF GetWeights -- Weights must be Vec4 !" @@ -183,7 +183,8 @@ class MeshData else { switch ( buf.Accessor->componentType ) { case fx::gltf::Accessor::ComponentType::Float: { - const auto mem = reinterpret_cast( buf.Data ); + const auto* mem = + reinterpret_cast( reinterpret_cast( buf.Data ) ); for ( uint32_t i = 0; i < buf.Accessor->count; ++i ) { weights.push_back( Ra::Core::Vector4f { mem[4 * i], mem[4 * i + 1], mem[4 * i + 2], mem[4 * i + 3] } ); @@ -191,7 +192,7 @@ class MeshData break; } case fx::gltf::Accessor::ComponentType::UnsignedByte: { - auto mem = buf.Data; + const auto* mem = buf.Data; for ( uint32_t i = 0; i < buf.Accessor->count; ++i ) { weights.push_back( Ra::Core::Vector4f { float( mem[4 * i] ) / UCHAR_MAX, @@ -202,7 +203,7 @@ class MeshData break; } case fx::gltf::Accessor::ComponentType::UnsignedShort: { - auto mem = reinterpret_cast( buf.Data ); + const auto* mem = reinterpret_cast( buf.Data ); for ( uint32_t i = 0; i < buf.Accessor->count; ++i ) { weights.push_back( Ra::Core::Vector4f { float( mem[4 * i] ) / USHRT_MAX, From 385a6a2af0db5d19dd80fdd7e9e07b2dba77c474 Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Thu, 20 Jul 2023 11:27:58 +0200 Subject: [PATCH 13/27] [io] fix codacy on gltf internals --- src/IO/Gltf/Writer/glTFFileWriter.cpp | 9 +++++++-- src/IO/Gltf/Writer/glTFFileWriter.hpp | 2 +- .../Gltf/internal/GLTFConverter/HandleData.cpp | 2 +- src/IO/Gltf/internal/GLTFConverter/MeshData.cpp | 5 +++-- src/IO/Gltf/internal/GLTFConverter/MeshData.hpp | 17 ++++++++--------- src/IO/Gltf/internal/fx/gltf.h | 3 +++ 6 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/IO/Gltf/Writer/glTFFileWriter.cpp b/src/IO/Gltf/Writer/glTFFileWriter.cpp index a17dfaa1df8..e6fd2601822 100644 --- a/src/IO/Gltf/Writer/glTFFileWriter.cpp +++ b/src/IO/Gltf/Writer/glTFFileWriter.cpp @@ -203,7 +203,7 @@ void VertexAttribWriter::operator()( const Ra::Core::Utils::AttribBase* att ) co reinterpret_cast( dataToCopy ); Ra::Core::Vector3 minAtt = arrayOfAttribs[0]; Ra::Core::Vector3 maxAtt = arrayOfAttribs[0]; - for ( auto i = 0; i < att->getSize(); ++i ) { + for ( size_t i = 0; i < att->getSize(); ++i ) { if ( arrayOfAttribs[i].x() < minAtt.x() ) { minAtt.x() = arrayOfAttribs[i].x(); } else if ( arrayOfAttribs[i].x() > maxAtt.x() ) { maxAtt.x() = arrayOfAttribs[i].x(); } if ( arrayOfAttribs[i].y() < minAtt.y() ) { minAtt.y() = arrayOfAttribs[i].y(); } @@ -296,7 +296,7 @@ void VertexAttribWriter::operator()( const Ra::Core::Utils::AttribBase* att ) co reinterpret_cast( dataToCopy ); Ra::Core::Vector2 minAtt = arrayOfAttribs[0]; Ra::Core::Vector2 maxAtt = arrayOfAttribs[0]; - for ( auto i = 0; i < att->getSize(); ++i ) { + for ( size_t i = 0; i < att->getSize(); ++i ) { if ( arrayOfAttribs[i].x() < minAtt.x() ) { minAtt.x() = arrayOfAttribs[i].x(); } else if ( arrayOfAttribs[i].x() > maxAtt.x() ) { maxAtt.x() = arrayOfAttribs[i].x(); } if ( arrayOfAttribs[i].y() < minAtt.y() ) { minAtt.y() = arrayOfAttribs[i].y(); } @@ -357,6 +357,7 @@ int addImage( gltf::Document& document, gltf::Image img ) { int addTexture( gltf::Document& document, int /* buffer // Use this to save embeded textures */, const Ra::Engine::Data::TextureParameters& params ) { + gltf::Texture texture; gltf::Image image; @@ -603,6 +604,10 @@ void glTFFileWriter::write( std::vector toExport ) { LOG( logWARNING ) << "No entities selected : abort file save."; return; } + if ( m_writeImages ) { + LOG( logWARNING ) << "Texture image writing is not yet supported. exporting only texture " + "uri (file name) "; + } g_texturePrefix = m_texturePrefix; auto roManager = RadiumEngine::getInstance()->getRenderObjectManager(); gltf::Document radiumScene; diff --git a/src/IO/Gltf/Writer/glTFFileWriter.hpp b/src/IO/Gltf/Writer/glTFFileWriter.hpp index 5e0b45c7100..c2f28108c1c 100644 --- a/src/IO/Gltf/Writer/glTFFileWriter.hpp +++ b/src/IO/Gltf/Writer/glTFFileWriter.hpp @@ -39,7 +39,7 @@ class RA_IO_API glTFFileWriter private: std::string m_fileName; std::string m_texturePrefix; - bool m_writeImages; + bool m_writeImages { false }; std::string m_bufferName; std::string m_rootName; }; diff --git a/src/IO/Gltf/internal/GLTFConverter/HandleData.cpp b/src/IO/Gltf/internal/GLTFConverter/HandleData.cpp index eb53bc019e5..647d326afaa 100644 --- a/src/IO/Gltf/internal/GLTFConverter/HandleData.cpp +++ b/src/IO/Gltf/internal/GLTFConverter/HandleData.cpp @@ -142,7 +142,7 @@ std::vector HandleDataLoader::getBindMatrices( const fx::gltf::Document& gltfScene, const fx::gltf::Skin& skin ) { std::vector jointBindMatrix( skin.joints.size(), Transform::Identity() ); - auto* invBindMatrices = + auto invBindMatrices = reinterpret_cast( AccessorReader( gltfScene ).read( skin.inverseBindMatrices ) ); for ( uint i = 0; i < skin.joints.size(); ++i ) { Matrix4 mat; diff --git a/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp b/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp index 4becfb109c4..c1c7ca1c9ea 100644 --- a/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp +++ b/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp @@ -68,7 +68,8 @@ std::vector> buildMesh( const glt const std::string& filePath, int32_t nodeNum ) { std::vector> meshParts; - for ( int32_t meshPartNumber = 0; meshPartNumber < doc.meshes[meshIndex].primitives.size(); + for ( int32_t meshPartNumber = 0; + meshPartNumber < int32_t( doc.meshes[meshIndex].primitives.size() ); ++meshPartNumber ) { MeshData mesh { doc, meshIndex, meshPartNumber }; if ( mesh.mode() != fx::gltf::Primitive::Mode::Triangles ) { @@ -152,7 +153,7 @@ std::vector> buildMesh( const glt auto layer = std::make_unique(); auto& indices = layer->collection(); indices.reserve( meshPart->getPrimitiveCount() ); - for ( uint vi = 0; vi < meshPart->getPrimitiveCount(); ++vi ) { + for ( uint vi = 0; vi < uint( meshPart->getPrimitiveCount() ); ++vi ) { indices.emplace_back( Vector3ui { 3 * vi, 3 * vi + 1, 3 * vi + 2 } ); } meshPart->getGeometry().addLayer( std::move( layer ), false, "indices" ); diff --git a/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp b/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp index 21608147a82..008c672de34 100644 --- a/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp +++ b/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp @@ -131,7 +131,7 @@ class MeshData static void GetJoints( Ra::Core::VectorArray& joints, const fx::gltf::Document& doc, const fx::gltf::Accessor& accessor ) { - MeshData::BufferInfo buf = MeshData::GetData( doc, accessor ); + const auto buf = MeshData::GetData( doc, accessor ); if ( buf.HasData() ) { if ( buf.Accessor->type != fx::gltf::Accessor::Type::Vec4 ) { LOG( Ra::Core::Utils::logERROR ) @@ -183,7 +183,7 @@ class MeshData else { switch ( buf.Accessor->componentType ) { case fx::gltf::Accessor::ComponentType::Float: { - const auto* mem = + const auto mem = reinterpret_cast( reinterpret_cast( buf.Data ) ); for ( uint32_t i = 0; i < buf.Accessor->count; ++i ) { weights.push_back( Ra::Core::Vector4f { @@ -192,7 +192,7 @@ class MeshData break; } case fx::gltf::Accessor::ComponentType::UnsignedByte: { - const auto* mem = buf.Data; + const auto mem = buf.Data; for ( uint32_t i = 0; i < buf.Accessor->count; ++i ) { weights.push_back( Ra::Core::Vector4f { float( mem[4 * i] ) / UCHAR_MAX, @@ -203,7 +203,7 @@ class MeshData break; } case fx::gltf::Accessor::ComponentType::UnsignedShort: { - const auto* mem = reinterpret_cast( buf.Data ); + const auto mem = reinterpret_cast( buf.Data ); for ( uint32_t i = 0; i < buf.Accessor->count; ++i ) { weights.push_back( Ra::Core::Vector4f { float( mem[4 * i] ) / USHRT_MAX, @@ -238,11 +238,10 @@ class MeshData const fx::gltf::Buffer& buffer = doc.buffers[bufferView.buffer]; const uint32_t dataTypeSize = CalculateDataTypeSize( accessor ); - return BufferInfo { - &accessor, - &buffer.data[static_cast( bufferView.byteOffset ) + accessor.byteOffset], - dataTypeSize, - accessor.count * dataTypeSize }; + return BufferInfo { &accessor, + &buffer.data[bufferView.byteOffset + accessor.byteOffset], + dataTypeSize, + accessor.count * dataTypeSize }; } static uint32_t CalculateDataTypeSize( const fx::gltf::Accessor& accessor ) noexcept { diff --git a/src/IO/Gltf/internal/fx/gltf.h b/src/IO/Gltf/internal/fx/gltf.h index 696afea55a7..b0a0a0d6167 100644 --- a/src/IO/Gltf/internal/fx/gltf.h +++ b/src/IO/Gltf/internal/fx/gltf.h @@ -1464,6 +1464,9 @@ inline Document Create( nlohmann::json const& json, DataContext const& dataConte buffer.data.resize( buffer.byteLength ); fileData.read( reinterpret_cast( &buffer.data[0] ), buffer.byteLength ); + if ( fileData.gcount() != buffer.byteLength ) { + throw invalid_gltf_document( "Unable to read buffer", buffer.uri ); + } } } else if ( dataContext.binaryData != nullptr ) { From e2a7a36d661627869e62ade4b238234a674ff092 Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Tue, 25 Jul 2023 09:39:40 +0200 Subject: [PATCH 14/27] [io] gltf loader now loads 2 texture coordinateSet (but still transmits only one to Radium) --- src/IO/Gltf/Loader/glTFFileLoader.cpp | 5 ++--- src/IO/Gltf/internal/GLTFConverter/MeshData.cpp | 15 ++++++++++----- src/IO/Gltf/internal/GLTFConverter/MeshData.hpp | 15 ++++++++++----- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/IO/Gltf/Loader/glTFFileLoader.cpp b/src/IO/Gltf/Loader/glTFFileLoader.cpp index 511cb7c0b36..9d85f83fee9 100644 --- a/src/IO/Gltf/Loader/glTFFileLoader.cpp +++ b/src/IO/Gltf/Loader/glTFFileLoader.cpp @@ -31,8 +31,7 @@ FileData* glTFFileLoader::loadFile( const std::string& filename ) { return nullptr; } - std::clock_t startTime; - startTime = std::clock(); + auto startTime = std::clock(); fileData->m_geometryData.clear(); fileData->m_animationData.clear(); @@ -53,7 +52,7 @@ FileData* glTFFileLoader::loadFile( const std::string& filename ) { else { gltfFile = fx::gltf::LoadFromText( filename, readQuotas ); } } catch ( std::exception& e ) { - LOG( logERROR ) << "Catched std::exception exception : " << e.what(); + LOG( logERROR ) << "glTFFileLoader::loadFile : Catch std::exception : " << e.what(); delete fileData; return nullptr; } diff --git a/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp b/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp index c1c7ca1c9ea..6c928c6ed37 100644 --- a/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp +++ b/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp @@ -77,13 +77,9 @@ std::vector> buildMesh( const glt << "GLTF buildMesh -- RadiumGLTF only supports Triangles primitive right now !"; continue; } - const MeshData::BufferInfo& vBuffer = mesh.VertexBuffer(); - const MeshData::BufferInfo& nBuffer = mesh.NormalBuffer(); - const MeshData::BufferInfo& tBuffer = mesh.TangentBuffer(); - const MeshData::BufferInfo& cBuffer = mesh.TexCoord0Buffer(); - const MeshData::BufferInfo& iBuffer = mesh.IndexBuffer(); // we need at least vertices to render an object + const MeshData::BufferInfo& vBuffer = mesh.VertexBuffer(); if ( vBuffer.HasData() ) { std::string meshName = doc.meshes[meshIndex].name; if ( meshName.empty() ) { @@ -112,7 +108,9 @@ std::vector> buildMesh( const glt vertices.reserve( vBuffer.Accessor->count ); convertVectors( vertices, vBuffer.Data, vBuffer.Accessor->count ); meshPart->getGeometry().vertexAttribs().unlock( attribVertices ); + // Convert faces + const MeshData::BufferInfo& iBuffer = mesh.IndexBuffer(); if ( iBuffer.HasData() ) { if ( iBuffer.Accessor->type != gltf::Accessor::Type::Scalar ) { if ( iBuffer.Accessor->type == gltf::Accessor::Type::None ) { @@ -158,7 +156,9 @@ std::vector> buildMesh( const glt } meshPart->getGeometry().addLayer( std::move( layer ), false, "indices" ); } + // Convert or compute normals + const MeshData::BufferInfo& nBuffer = mesh.NormalBuffer(); if ( nBuffer.HasData() ) { if ( ( nBuffer.Accessor->type != gltf::Accessor::Type::Vec3 ) || ( nBuffer.Accessor->componentType != gltf::Accessor::ComponentType::Float ) ) { @@ -177,7 +177,10 @@ std::vector> buildMesh( const glt NormalCalculator nrmCalculator; nrmCalculator( meshPart.get() ); } + // Convert TexCoord if any + // As Radium only manage 1 texture coordinate set, only use the texcoord0 + const MeshData::BufferInfo& cBuffer = mesh.TexCoordBuffer( 0 ); if ( cBuffer.HasData() ) { if ( ( cBuffer.Accessor->type != gltf::Accessor::Type::Vec2 ) ) { LOG( logERROR ) << "GLTF buildMesh -- TexCoord must be Vec2"; @@ -208,7 +211,9 @@ std::vector> buildMesh( const glt meshPart->getGeometry().vertexAttribs().unlock( attribHandle ); } else { LOG( logDEBUG ) << "GLTF buildMesh -- No texCoord provided. !"; } + // Convert tangent if any + const MeshData::BufferInfo& tBuffer = mesh.TangentBuffer(); if ( tBuffer.HasData() ) { if ( ( tBuffer.Accessor->type != gltf::Accessor::Type::Vec4 ) || ( tBuffer.Accessor->componentType != gltf::Accessor::ComponentType::Float ) ) { diff --git a/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp b/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp index 008c672de34..ed2622ff23f 100644 --- a/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp +++ b/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp @@ -66,8 +66,9 @@ class MeshData else if ( attrib.first == "TANGENT" ) { m_tangentBuffer = GetData( doc, doc.accessors[attrib.second] ); } - else if ( attrib.first == "TEXCOORD_0" ) { - m_texCoord0Buffer = GetData( doc, doc.accessors[attrib.second] ); + else if ( attrib.first.substr( 0, 9 ) == "TEXCOORD_" ) { + auto idTexCoord = std::stoi( attrib.first.substr( 9 ) ); + m_texCoordBuffers[idTexCoord] = GetData( doc, doc.accessors[attrib.second] ); } } @@ -106,9 +107,11 @@ class MeshData /** * - * @return the texcoord buffer of the mesh + * @return the textcoord buffer i of the mesh. */ - [[nodiscard]] const BufferInfo& TexCoord0Buffer() const noexcept { return m_texCoord0Buffer; } + [[nodiscard]] const BufferInfo& TexCoordBuffer( int i ) const noexcept { + return m_texCoordBuffers[i]; + } /** * @@ -227,7 +230,9 @@ class MeshData BufferInfo m_vertexBuffer {}; BufferInfo m_normalBuffer {}; BufferInfo m_tangentBuffer {}; - BufferInfo m_texCoord0Buffer {}; + // TODO : spec require to manage at least two texture coordinate sets + std::array m_texCoordBuffers; + // BufferInfo m_texCoord0Buffer {}; MaterialData m_materialData {}; From 47ae2f114d86ce810f5fde37f8b27060796d1b94 Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Tue, 25 Jul 2023 09:40:19 +0200 Subject: [PATCH 15/27] [engine] add support for KHR_unlit material extension --- .../baseGLTFMaterial_LitOIT.frag.glsl | 4 + .../baseGLTFMaterial_LitOpaque.frag.glsl | 7 + .../baseGLTFMaterial_Zprepass.frag.glsl | 11 +- src/Core/Material/BaseGLTFMaterial.cpp | 5 +- src/Core/Material/BaseGLTFMaterial.hpp | 9 + src/Engine/Data/GLTFMaterial.cpp | 12 + src/Engine/Data/GLTFMaterial.hpp | 9 +- .../Extensions/MaterialExtensions.hpp | 48 +- .../Gltf/internal/GLTFConverter/Converter.cpp | 12 +- .../GLTFConverter/MaterialConverter.cpp | 427 +++++++++--------- .../GLTFConverter/MaterialConverter.hpp | 16 +- 11 files changed, 292 insertions(+), 268 deletions(-) diff --git a/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_LitOIT.frag.glsl b/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_LitOIT.frag.glsl index f7add31c9d7..e6156ae96fb 100644 --- a/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_LitOIT.frag.glsl +++ b/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_LitOIT.frag.glsl @@ -40,6 +40,9 @@ float weight( float z, float alpha ) { } void main() { +#ifdef MATERIAL_UNLIT + discard; +#else vec3 tc = getPerVertexTexCoord(); // only render non opaque fragments and not fully transparent fragments vec4 bc = getBaseColor( material, tc ); @@ -66,4 +69,5 @@ void main() { float w = weight( gl_FragCoord.z, a ); f_Accumulation = vec4( color * a, a ) * w; f_Revealage = vec4( a ); +#endif } diff --git a/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_LitOpaque.frag.glsl b/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_LitOpaque.frag.glsl index ba8180b0bc9..bb552b2f815 100644 --- a/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_LitOpaque.frag.glsl +++ b/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_LitOpaque.frag.glsl @@ -14,6 +14,12 @@ layout( location = 5 ) in vec3 in_viewVector; layout( location = 6 ) in vec3 in_lightVector; void main() { +// Unefficient but simple workaround to support unlit GLTF material +// This restrict unlit to opaque materials even if the spec allows unlit transparent +// TODO, modify radium renderers to support unlit opaque and transparent objects +#ifdef MATERIAL_UNLIT + discard; +#else vec3 tc = getPerVertexTexCoord(); // discard non opaque fragment vec4 bc = getBaseColor( material, tc ); @@ -40,4 +46,5 @@ void main() { #endif // color = color + getEmissiveColor(material.baseMaterial, getPerVertexTexCoord()); fragColor = vec4( color, 1.0 ); +#endif } diff --git a/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_Zprepass.frag.glsl b/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_Zprepass.frag.glsl index b48af264ea6..f326de83586 100644 --- a/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_Zprepass.frag.glsl +++ b/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_Zprepass.frag.glsl @@ -15,7 +15,16 @@ void main() { if (toDiscard(material, bc)) discard; NormalInfo nrm_info = getNormalInfo(material.baseMaterial, in_texcoord); + out_normal = vec4(nrm_info.n * 0.5 + 0.5, 1.0); +// Unefficient but simple workaround to support unlit GLTF material +// This restrict unlit to opaque materials even if the spec allows unlit transparent +// TODO, modify radium renderers to support unlit opaque and transparent objects +#ifdef MATERIAL_UNLIT + out_ambient = bc; + out_diffuse = vec4(bc.rgb, 1.0); + out_specular = vec4(vec3(0.0), 1.0); +#else MaterialInfo bsdf_params; BsdfInfo layers; @@ -26,7 +35,7 @@ void main() { bsdf_params, layers ); out_ambient = vec4(layers.f_diffuse * 0.01 + getEmissiveColor(material, in_texcoord), 1.0); - out_normal = vec4(nrm_info.n * 0.5 + 0.5, 1.0); out_diffuse = vec4(layers.f_diffuse, 1.0); out_specular = vec4(layers.f_specular, 1.0); +#endif } diff --git a/src/Core/Material/BaseGLTFMaterial.cpp b/src/Core/Material/BaseGLTFMaterial.cpp index 2b694fd8803..ffb07d817eb 100644 --- a/src/Core/Material/BaseGLTFMaterial.cpp +++ b/src/Core/Material/BaseGLTFMaterial.cpp @@ -6,10 +6,7 @@ namespace Material { BaseGLTFMaterial::BaseGLTFMaterial( const std::string& gltfType, const std::string& instanceName ) : Ra::Core::Asset::MaterialData( instanceName, gltfType ) { // extension supported by all gltf materials - // TODO : uncomment the extension supported by the implementation. - /* - allowExtension("KHR_materials_unlit"); - */ + allowExtension( "KHR_materials_unlit" ); } } // namespace Material diff --git a/src/Core/Material/BaseGLTFMaterial.hpp b/src/Core/Material/BaseGLTFMaterial.hpp index 8eb4bbf9a47..a713eebb06d 100644 --- a/src/Core/Material/BaseGLTFMaterial.hpp +++ b/src/Core/Material/BaseGLTFMaterial.hpp @@ -160,6 +160,15 @@ struct RA_CORE_API GLTFIor : public GLTFMaterialExtensionData { float m_ior { 1.5 }; }; +/** + * \brief Unlit extension + */ +struct RA_CORE_API GLTFUnlit : public GLTFMaterialExtensionData { + explicit GLTFUnlit( const std::string& name = std::string {} ) : + GLTFMaterialExtensionData( "Unlit", name ) {} + bool active { false }; +}; + } // namespace Material } // namespace Core } // namespace Ra diff --git a/src/Engine/Data/GLTFMaterial.cpp b/src/Engine/Data/GLTFMaterial.cpp index 72436ef3912..cc37a426ee3 100644 --- a/src/Engine/Data/GLTFMaterial.cpp +++ b/src/Engine/Data/GLTFMaterial.cpp @@ -197,9 +197,19 @@ std::map( source ); + // Not a layer, set a property on the material baseMaterial.setIndexOfRefraction( iorProvider->m_ior ); return nullptr; } }, + { "KHR_materials_unlit", + []( GLTFMaterial& baseMaterial, + const std::string& /*instanceName*/, + const auto* source ) { + auto unlitProvider = reinterpret_cast( source ); + // Not a layer, set a property on the material + baseMaterial.setUnlitStatus( unlitProvider->active ); + return nullptr; + } }, { "KHR_materials_clearcoat", []( GLTFMaterial& baseMaterial, const std::string& instanceName, const auto* source ) { return std::make_unique( @@ -262,6 +272,8 @@ std::list GLTFMaterial::getPropertyList() const { // Expose the new GLTF__INTERFACE that will eveolve until it is submitted to Radium // GLSL/Material interface props.emplace_back( "GLTF_MATERIAL_INTERFACE" ); + // unlit + if ( getUnlitStatus() ) { props.emplace_back( "MATERIAL_UNLIT" ); } // textures if ( m_pendingTextures.find( { "TEX_NORMAL" } ) != m_pendingTextures.end() || getTexture( { "TEX_NORMAL" } ) != nullptr ) { diff --git a/src/Engine/Data/GLTFMaterial.hpp b/src/Engine/Data/GLTFMaterial.hpp index d8e53961cf4..4af3c65545d 100644 --- a/src/Engine/Data/GLTFMaterial.hpp +++ b/src/Engine/Data/GLTFMaterial.hpp @@ -241,7 +241,7 @@ class RA_ENGINE_API GLTFMaterial : public Material, public ParameterSetEditingIn [[nodiscard]] bool isTransparent() const override; /** - * Get the list of properties the material migh use in a shader. + * Get the list of properties the material might use in a shader. */ [[nodiscard]] std::list getPropertyList() const override; @@ -293,6 +293,10 @@ class RA_ENGINE_API GLTFMaterial : public Material, public ParameterSetEditingIn float getIndexOfRefraction() const { return m_indexOfRefraction; } void setIndexOfRefraction( float ior ) { m_indexOfRefraction = ior; } + + float getUnlitStatus() const { return m_isUnlit; } + void setUnlitStatus( bool status ) { m_isUnlit = status; } + /******************************************************************/ protected: @@ -310,6 +314,9 @@ class RA_ENGINE_API GLTFMaterial : public Material, public ParameterSetEditingIn // attributes having default value in the spec with allowed modifications from extensions float m_indexOfRefraction { 1.5 }; + // Should the material be lit ? (manage by the extension KHR_materials_unlit), + bool m_isUnlit { false }; + std::map m_textures; std::map m_pendingTextures; diff --git a/src/IO/Gltf/internal/Extensions/MaterialExtensions.hpp b/src/IO/Gltf/internal/Extensions/MaterialExtensions.hpp index 22ddfeecef7..5485b72729b 100644 --- a/src/IO/Gltf/internal/Extensions/MaterialExtensions.hpp +++ b/src/IO/Gltf/internal/Extensions/MaterialExtensions.hpp @@ -307,37 +307,37 @@ inline void to_json( nlohmann::json& json, gltf_KHRMaterialsSheen const& khrMate } /** - * INN_material_atlas_V1 - * + * KHR_materials_unlit + * https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_unlit */ -struct gltf_INNMaterialAtlas { - struct INN_AtlasTexture : fx::gltf::Material::Texture { - int nbMaterial { 0 }; - }; - - INN_AtlasTexture atlasTexture; +struct gltf_KHRMaterialsUnlit { + bool active { false }; + nlohmann::json extensionsAndExtras {}; }; -inline void from_json( nlohmann::json const& json, - gltf_INNMaterialAtlas::INN_AtlasTexture& atlasTexture ) { - from_json( json, static_cast( atlasTexture ) ); - fx::gltf::detail::ReadRequiredField( "nbMaterial", json, atlasTexture.nbMaterial ); +inline void from_json( nlohmann::json const& json, gltf_KHRMaterialsUnlit& khrMaterialsUnlit ) { + khrMaterialsUnlit.active = true; + fx::gltf::detail::ReadExtensionsAndExtras( json, khrMaterialsUnlit.extensionsAndExtras ); } -inline void from_json( nlohmann::json const& json, gltf_INNMaterialAtlas& textureAtlas ) { - fx::gltf::detail::ReadRequiredField( "atlasTexture", json, textureAtlas.atlasTexture ); -} - -inline void to_json( nlohmann::json& json, - gltf_INNMaterialAtlas::INN_AtlasTexture const& atlasTexture ) { - to_json( json, static_cast( atlasTexture ) ); - fx::gltf::detail::WriteField( { "nbMaterial" }, json, atlasTexture.nbMaterial, 0 ); -} - -inline void to_json( nlohmann::json& json, gltf_INNMaterialAtlas const& textureAtlas ) { - fx::gltf::detail::WriteField( { "atlasTexture" }, json, textureAtlas.atlasTexture ); +inline void to_json( nlohmann::json& json, gltf_KHRMaterialsUnlit const& khrMaterialsUnlit ) { + fx::gltf::detail::WriteExtensions( json, khrMaterialsUnlit.extensionsAndExtras ); } +/** + * Adding a material extension + * 1. define in this file the json parsing of the extension, based on its specification and json + * schema from https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/ + * 2. add the extension in the supported extensions list in Core/Material/xxGLTFxx.cpp (for + * materials accepting this extension) + * 3. add the extension in the supported extensions list (gltfSupportedExtensions) in Converter.cpp + * 3. Define the Core/Material representation of the extension as inheriting from + * GLTFMaterialExtensionData + * 4. Add the extension converter in MaterialConverter.cpp + * 5. Define the Engine/Data representation and management of the extension as a material layer in + * Engine/Data/GLTFMaterial.xpp + * 6. Write the glsl management of the extension in the shaders + */ } // namespace GLTF } // namespace IO } // namespace Ra diff --git a/src/IO/Gltf/internal/GLTFConverter/Converter.cpp b/src/IO/Gltf/internal/GLTFConverter/Converter.cpp index 38ba2b24706..5ef5063f9f2 100644 --- a/src/IO/Gltf/internal/GLTFConverter/Converter.cpp +++ b/src/IO/Gltf/internal/GLTFConverter/Converter.cpp @@ -24,7 +24,7 @@ static std::vector gltfSupportedExtensions { { "KHR_lights_punctual { "KHR_materials_clearcoat" }, { "KHR_materials_specular" }, { "KHR_materials_sheen" }, - { "INN_material_atlas_V1" } }; + { "KHR_materials_unlit" } }; // Check extensions used or required by this gltf scene bool checkExtensions( const gltf::Document& gltfscene ) { @@ -211,7 +211,7 @@ void glTfVisitor( const gltf::Document& scene, else { graphNode.initPropsFromExtensionsAndExtra( node.extensionsAndExtras ); } graphNode.children = node.children; - for ( auto childIndex : node.children ) { + for ( auto childIndex : graphNode.children ) { glTfVisitor( scene, childIndex, graphNode.m_transform, graphNodes, visitedNodes ); } } @@ -280,14 +280,13 @@ bool Converter::operator()( const gltf::Document& gltfscene ) { int activeScene = gltfscene.scene; if ( activeScene == -1 ) activeScene = 0; std::set visitedNodes; - for ( const uint32_t sceneNode : gltfscene.scenes[activeScene].nodes ) { + for ( auto sceneNode : gltfscene.scenes[activeScene].nodes ) { glTfVisitor( gltfscene, sceneNode, rootTransform, graphNodes, visitedNodes ); } int32_t nodeNum = 0; - // For skeleton and animation data - HandleDataLoader::IntToString nodeNumToComponentName; + // build Radium representation of the scene elements for ( auto visited : visitedNodes ) { auto& graphNode = graphNodes[visited]; // Is the node a mesh ? @@ -318,7 +317,8 @@ bool Converter::operator()( const gltf::Document& gltfscene ) { } } - // Build Skeletons + // build Radium Skeletons + HandleDataLoader::IntToString nodeNumToComponentName; if ( !gltfscene.skins.empty() ) { size_t skinIndex = 0; for ( const auto& skin : gltfscene.skins ) { diff --git a/src/IO/Gltf/internal/GLTFConverter/MaterialConverter.cpp b/src/IO/Gltf/internal/GLTFConverter/MaterialConverter.cpp index a548975a4b7..e5dcee246a5 100644 --- a/src/IO/Gltf/internal/GLTFConverter/MaterialConverter.cpp +++ b/src/IO/Gltf/internal/GLTFConverter/MaterialConverter.cpp @@ -54,24 +54,24 @@ GLTFSampler convertSampler( const fx::gltf::Sampler& sampler ) { } switch ( sampler.wrapS ) { case fx::gltf::Sampler::WrappingMode::ClampToEdge: - rasampler.wrapS = GLTFSampler::WrappingMode ::ClampToEdge; + rasampler.wrapS = GLTFSampler::WrappingMode::ClampToEdge; break; case fx::gltf::Sampler::WrappingMode::MirroredRepeat: - rasampler.wrapS = GLTFSampler::WrappingMode ::MirroredRepeat; + rasampler.wrapS = GLTFSampler::WrappingMode::MirroredRepeat; break; case fx::gltf::Sampler::WrappingMode::Repeat: - rasampler.wrapS = GLTFSampler::WrappingMode ::Repeat; + rasampler.wrapS = GLTFSampler::WrappingMode::Repeat; break; } switch ( sampler.wrapT ) { case fx::gltf::Sampler::WrappingMode::ClampToEdge: - rasampler.wrapT = GLTFSampler::WrappingMode ::ClampToEdge; + rasampler.wrapT = GLTFSampler::WrappingMode::ClampToEdge; break; case fx::gltf::Sampler::WrappingMode::MirroredRepeat: - rasampler.wrapT = GLTFSampler::WrappingMode ::MirroredRepeat; + rasampler.wrapT = GLTFSampler::WrappingMode::MirroredRepeat; break; case fx::gltf::Sampler::WrappingMode::Repeat: - rasampler.wrapT = GLTFSampler::WrappingMode ::Repeat; + rasampler.wrapT = GLTFSampler::WrappingMode::Repeat; break; } @@ -81,19 +81,6 @@ GLTFSampler convertSampler( const fx::gltf::Sampler& sampler ) { void getMaterialExtensions( const nlohmann::json& /*extensionsAndExtras*/, BaseGLTFMaterial* /*mat*/ ) { // Manage non standard material extensions -#if 0 - if ( !extensionsAndExtras.empty() ) { - auto ext = extensionsAndExtras.find( "extensions" ); - if ( ext != extensionsAndExtras.end() ) { - auto extensions = *ext; - const nlohmann::json::const_iterator iter = extensions.find( "INN_material_atlas_V1" ); - if ( iter != extensions.end() ) { - mat->m_inn_materialAtlas = new gltf_INNMaterialAtlas; - from_json( *iter, *( mat->m_inn_materialAtlas ) ); - } - } - } -#endif } void getMaterialTextureTransform( const nlohmann::json& extensionsAndExtras, @@ -184,182 +171,204 @@ void getCommonMaterialParameters( const gltf::Document& doc, getMaterialExtensions( gltfMaterial.extensionsAndExtras, mat ); } -std::map( const gltf::Document& doc, - const std::string& filePath, - const nlohmann::json& jsonData, - const std::string& basename )>> - instanciateExtension { - { "KHR_materials_ior", - []( const gltf::Document& /*doc*/, - const std::string& /*filePath*/, - const nlohmann::json& jsonData, - const std::string& basename ) { - gltf_KHRMaterialsIor data; - from_json( jsonData, data ); - auto built = std::make_unique( basename + " - IOR" ); - built->m_ior = data.ior; - return built; - } }, - { "KHR_materials_clearcoat", - []( const gltf::Document& doc, - const std::string& filePath, - const nlohmann::json& jsonData, - const std::string& basename ) { - gltf_KHRMaterialsClearcoat data; - from_json( jsonData, data ); - auto built = std::make_unique( basename + " - Clearcoat layer" ); - // clearcoat layer - built->m_clearcoatFactor = data.clearcoatFactor; - if ( !data.clearcoatTexture.empty() ) { - ImageData tex { doc, data.clearcoatTexture.index, filePath }; - if ( !tex.Info().IsBinary() ) { - built->m_clearcoatTexture = tex.Info().FileName; - built->m_hasClearcoatTexture = true; - } - else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } - // get sampler information for this texture - int samplerIndex = doc.textures[data.clearcoatTexture.index].sampler; - if ( samplerIndex >= 0 ) { - built->m_clearcoatSampler = convertSampler( doc.samplers[samplerIndex] ); - } - getMaterialTextureTransform( data.clearcoatTexture.extensionsAndExtras, - built->m_clearcoatTextureTransform ); - } - // clearcoat roughness - built->m_clearcoatRoughnessFactor = data.clearcoatRoughnessFactor; - if ( !data.clearcoatRoughnessTexture.empty() ) { - ImageData tex { doc, data.clearcoatRoughnessTexture.index, filePath }; - if ( !tex.Info().IsBinary() ) { - built->m_clearcoatRoughnessTexture = tex.Info().FileName; - built->m_hasClearcoatRoughnessTexture = true; - } - else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } - // get sampler information for this texture - int samplerIndex = doc.textures[data.clearcoatRoughnessTexture.index].sampler; - if ( samplerIndex >= 0 ) { - built->m_clearcoatRoughnessSampler = - convertSampler( doc.samplers[samplerIndex] ); - } - getMaterialTextureTransform( data.clearcoatRoughnessTexture.extensionsAndExtras, - built->m_clearcoatRoughnessTextureTransform ); - } - // clearcoat Normal texture - if ( !data.clearcoatNormalTexture.empty() ) { - ImageData tex { doc, data.clearcoatNormalTexture.index, filePath }; - if ( !tex.Info().IsBinary() ) { - built->m_clearcoatNormalTexture = tex.Info().FileName; - built->m_clearcoatNormalTextureScale = data.clearcoatNormalTexture.scale; - built->m_hasClearcoatNormalTexture = true; - } - else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } - // get sampler information for this texture - int samplerIndex = doc.textures[data.clearcoatNormalTexture.index].sampler; - if ( samplerIndex >= 0 ) { - built->m_clearcoatNormalSampler = - convertSampler( doc.samplers[samplerIndex] ); - } - getMaterialTextureTransform( data.clearcoatNormalTexture.extensionsAndExtras, - built->m_clearcoatNormalTextureTransform ); - } - return built; - } }, - { "KHR_materials_specular", - []( const gltf::Document& doc, - const std::string& filePath, - const nlohmann::json& jsonData, - const std::string& basename ) { - gltf_KHRMaterialsSpecular data; - from_json( jsonData, data ); - auto built = std::make_unique( basename + " - Specular layer" ); - // spacular strength - built->m_specularFactor = data.specularFactor; - if ( !data.specularTexture.empty() ) { - ImageData tex { doc, data.specularTexture.index, filePath }; - if ( !tex.Info().IsBinary() ) { - built->m_specularTexture = tex.Info().FileName; - built->m_hasSpecularTexture = true; - } - else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } - // get sampler information for this texture - int samplerIndex = doc.textures[data.specularTexture.index].sampler; - if ( samplerIndex >= 0 ) { - built->m_specularSampler = convertSampler( doc.samplers[samplerIndex] ); - } - getMaterialTextureTransform( data.specularTexture.extensionsAndExtras, - built->m_specularTextureTransform ); - } - // specular color - built->m_specularColorFactor = Ra::Core::Utils::Color { data.specularColorFactor[0], - data.specularColorFactor[1], - data.specularColorFactor[2], - 1_ra }; - if ( !data.specularColorTexture.empty() ) { - ImageData tex { doc, data.specularColorTexture.index, filePath }; - if ( !tex.Info().IsBinary() ) { - built->m_specularColorTexture = tex.Info().FileName; - built->m_hasSpecularColorTexture = true; - } - else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } - // get sampler information for this texture - int samplerIndex = doc.textures[data.specularColorTexture.index].sampler; - if ( samplerIndex >= 0 ) { - built->m_specularColorSampler = convertSampler( doc.samplers[samplerIndex] ); - } - getMaterialTextureTransform( data.specularColorTexture.extensionsAndExtras, - built->m_specularColorTextureTransform ); - } - return built; - } }, - { "KHR_materials_sheen", - []( const gltf::Document& doc, - const std::string& filePath, - const nlohmann::json& jsonData, - const std::string& basename ) { - gltf_KHRMaterialsSheen data; - from_json( jsonData, data ); - auto built = std::make_unique( basename + " - Sheen layer" ); - // Sheen color. - built->m_sheenColorFactor = Ra::Core::Utils::Color { data.sheenColorFactor[0], - data.sheenColorFactor[1], - data.sheenColorFactor[2], - 1_ra }; - if ( !data.sheenColorTexture.empty() ) { - ImageData tex { doc, data.sheenColorTexture.index, filePath }; - if ( !tex.Info().IsBinary() ) { - built->m_sheenColorTexture = tex.Info().FileName; - built->m_hasSheenColorTexture = true; - } - else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } - // get sampler information for this texture - int samplerIndex = doc.textures[data.sheenColorTexture.index].sampler; - if ( samplerIndex >= 0 ) { - built->m_sheenColorTextureSampler = - convertSampler( doc.samplers[samplerIndex] ); - } - getMaterialTextureTransform( data.sheenColorTexture.extensionsAndExtras, - built->m_sheenColorTextureTransform ); - } - // Sheen roughness - built->m_sheenRoughnessFactor = data.sheenRoughnessFactor; - if ( !data.sheenRoughnessTexture.empty() ) { - ImageData tex { doc, data.sheenRoughnessTexture.index, filePath }; - if ( !tex.Info().IsBinary() ) { - built->m_sheenRoughnessTexture = tex.Info().FileName; - built->m_hasSheenRoughnessTexture = true; - } - else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } - // get sampler information for this texture - int samplerIndex = doc.textures[data.sheenRoughnessTexture.index].sampler; - if ( samplerIndex >= 0 ) { - built->m_sheenRoughnessTextureSampler = - convertSampler( doc.samplers[samplerIndex] ); - } - getMaterialTextureTransform( data.sheenRoughnessTexture.extensionsAndExtras, - built->m_sheenRoughnessTextureTransform ); - } - return built; - } } }; +// Converter functor for material supported extensions. + +using ExtensionConverter = + std::function( const gltf::Document& doc, + const std::string& filePath, + const nlohmann::json& jsonData, + const std::string& basename )>; + +// Material extension converters +ExtensionConverter KHR_materials_iorConverter = []( const gltf::Document& /*doc*/, + const std::string& /*filePath*/, + const nlohmann::json& jsonData, + const std::string& basename ) { + gltf_KHRMaterialsIor data; + from_json( jsonData, data ); + auto built = std::make_unique( basename + " - IOR" ); + built->m_ior = data.ior; + return built; +}; + +struct KHR_materials_clearcoatConverter { + std::unique_ptr operator()( const gltf::Document& doc, + const std::string& filePath, + const nlohmann::json& jsonData, + const std::string& basename ) { + gltf_KHRMaterialsClearcoat data; + from_json( jsonData, data ); + auto built = std::make_unique( basename + " - Clearcoat layer" ); + // clearcoat layer + built->m_clearcoatFactor = data.clearcoatFactor; + if ( !data.clearcoatTexture.empty() ) { + ImageData tex { doc, data.clearcoatTexture.index, filePath }; + if ( !tex.Info().IsBinary() ) { + built->m_clearcoatTexture = tex.Info().FileName; + built->m_hasClearcoatTexture = true; + } + else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } + // get sampler information for this texture + int samplerIndex = doc.textures[data.clearcoatTexture.index].sampler; + if ( samplerIndex >= 0 ) { + built->m_clearcoatSampler = convertSampler( doc.samplers[samplerIndex] ); + } + getMaterialTextureTransform( data.clearcoatTexture.extensionsAndExtras, + built->m_clearcoatTextureTransform ); + } + // clearcoat roughness + built->m_clearcoatRoughnessFactor = data.clearcoatRoughnessFactor; + if ( !data.clearcoatRoughnessTexture.empty() ) { + ImageData tex { doc, data.clearcoatRoughnessTexture.index, filePath }; + if ( !tex.Info().IsBinary() ) { + built->m_clearcoatRoughnessTexture = tex.Info().FileName; + built->m_hasClearcoatRoughnessTexture = true; + } + else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } + // get sampler information for this texture + int samplerIndex = doc.textures[data.clearcoatRoughnessTexture.index].sampler; + if ( samplerIndex >= 0 ) { + built->m_clearcoatRoughnessSampler = convertSampler( doc.samplers[samplerIndex] ); + } + getMaterialTextureTransform( data.clearcoatRoughnessTexture.extensionsAndExtras, + built->m_clearcoatRoughnessTextureTransform ); + } + // clearcoat Normal texture + if ( !data.clearcoatNormalTexture.empty() ) { + ImageData tex { doc, data.clearcoatNormalTexture.index, filePath }; + if ( !tex.Info().IsBinary() ) { + built->m_clearcoatNormalTexture = tex.Info().FileName; + built->m_clearcoatNormalTextureScale = data.clearcoatNormalTexture.scale; + built->m_hasClearcoatNormalTexture = true; + } + else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } + // get sampler information for this texture + int samplerIndex = doc.textures[data.clearcoatNormalTexture.index].sampler; + if ( samplerIndex >= 0 ) { + built->m_clearcoatNormalSampler = convertSampler( doc.samplers[samplerIndex] ); + } + getMaterialTextureTransform( data.clearcoatNormalTexture.extensionsAndExtras, + built->m_clearcoatNormalTextureTransform ); + } + return built; + } +}; + +struct KHR_materials_specularConverter { + std::unique_ptr operator()( const gltf::Document& doc, + const std::string& filePath, + const nlohmann::json& jsonData, + const std::string& basename ) { + gltf_KHRMaterialsSpecular data; + from_json( jsonData, data ); + auto built = std::make_unique( basename + " - Specular layer" ); + // spacular strength + built->m_specularFactor = data.specularFactor; + if ( !data.specularTexture.empty() ) { + ImageData tex { doc, data.specularTexture.index, filePath }; + if ( !tex.Info().IsBinary() ) { + built->m_specularTexture = tex.Info().FileName; + built->m_hasSpecularTexture = true; + } + else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } + // get sampler information for this texture + int samplerIndex = doc.textures[data.specularTexture.index].sampler; + if ( samplerIndex >= 0 ) { + built->m_specularSampler = convertSampler( doc.samplers[samplerIndex] ); + } + getMaterialTextureTransform( data.specularTexture.extensionsAndExtras, + built->m_specularTextureTransform ); + } + // specular color + built->m_specularColorFactor = Ra::Core::Utils::Color { data.specularColorFactor[0], + data.specularColorFactor[1], + data.specularColorFactor[2], + 1_ra }; + if ( !data.specularColorTexture.empty() ) { + ImageData tex { doc, data.specularColorTexture.index, filePath }; + if ( !tex.Info().IsBinary() ) { + built->m_specularColorTexture = tex.Info().FileName; + built->m_hasSpecularColorTexture = true; + } + else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } + // get sampler information for this texture + int samplerIndex = doc.textures[data.specularColorTexture.index].sampler; + if ( samplerIndex >= 0 ) { + built->m_specularColorSampler = convertSampler( doc.samplers[samplerIndex] ); + } + getMaterialTextureTransform( data.specularColorTexture.extensionsAndExtras, + built->m_specularColorTextureTransform ); + } + return built; + } +}; + +struct KHR_materials_sheenConverter { + std::unique_ptr operator()( const gltf::Document& doc, + const std::string& filePath, + const nlohmann::json& jsonData, + const std::string& basename ) { + gltf_KHRMaterialsSheen data; + from_json( jsonData, data ); + auto built = std::make_unique( basename + " - Sheen layer" ); + // Sheen color. + built->m_sheenColorFactor = Ra::Core::Utils::Color { + data.sheenColorFactor[0], data.sheenColorFactor[1], data.sheenColorFactor[2], 1_ra }; + if ( !data.sheenColorTexture.empty() ) { + ImageData tex { doc, data.sheenColorTexture.index, filePath }; + if ( !tex.Info().IsBinary() ) { + built->m_sheenColorTexture = tex.Info().FileName; + built->m_hasSheenColorTexture = true; + } + else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } + // get sampler information for this texture + int samplerIndex = doc.textures[data.sheenColorTexture.index].sampler; + if ( samplerIndex >= 0 ) { + built->m_sheenColorTextureSampler = convertSampler( doc.samplers[samplerIndex] ); + } + getMaterialTextureTransform( data.sheenColorTexture.extensionsAndExtras, + built->m_sheenColorTextureTransform ); + } + // Sheen roughness + built->m_sheenRoughnessFactor = data.sheenRoughnessFactor; + if ( !data.sheenRoughnessTexture.empty() ) { + ImageData tex { doc, data.sheenRoughnessTexture.index, filePath }; + if ( !tex.Info().IsBinary() ) { + built->m_sheenRoughnessTexture = tex.Info().FileName; + built->m_hasSheenRoughnessTexture = true; + } + else { LOG( logINFO ) << "GLTF converter -- Embeded texture not supported yet"; } + // get sampler information for this texture + int samplerIndex = doc.textures[data.sheenRoughnessTexture.index].sampler; + if ( samplerIndex >= 0 ) { + built->m_sheenRoughnessTextureSampler = + convertSampler( doc.samplers[samplerIndex] ); + } + getMaterialTextureTransform( data.sheenRoughnessTexture.extensionsAndExtras, + built->m_sheenRoughnessTextureTransform ); + } + return built; + } +}; + +ExtensionConverter KHR_materials_unlitConverter = []( const gltf::Document& /*doc*/, + const std::string& /*filePath*/, + const nlohmann::json& jsonData, + const std::string& basename ) { + gltf_KHRMaterialsUnlit data; + from_json( jsonData, data ); + auto built = std::make_unique( basename + " - unlit" ); + built->active = true; + return built; +}; + +// Populate this map with each supported material extension +std::map instantiateExtension { + { "KHR_materials_ior", KHR_materials_iorConverter }, + { "KHR_materials_clearcoat", KHR_materials_clearcoatConverter() }, + { "KHR_materials_specular", KHR_materials_specularConverter() }, + { "KHR_materials_sheen", KHR_materials_sheenConverter() }, + { "KHR_materials_unlit", KHR_materials_unlitConverter } }; void getMaterialExtensions( const gltf::Document& doc, const std::string& filePath, @@ -390,7 +399,7 @@ void getMaterialExtensions( const gltf::Document& doc, } if ( mat->supportExtension( key ) ) { mat->m_extensions[key] = - instanciateExtension[key]( doc, filePath, value, mat->getName() ); + instantiateExtension[key]( doc, filePath, value, mat->getName() ); } else { LOG( logINFO ) << "Extension " << key << " is NOT allowed for " @@ -551,30 +560,7 @@ Ra::Core::Asset::MaterialData* buildMaterial( const gltf::Document& doc, const std::string& filePath, int32_t meshPartNumber, const MaterialData& meshMaterial ) { -#ifdef LEGACY_IMPLEMENTATION - if ( meshMaterial.isMetallicRoughness() ) { - return buildMetallicRoughnessMaterial( - doc, meshIndex, filePath, meshPartNumber, meshMaterial ); - } - else { - // Check if extension "KHR_materials_pbrSpecularGlossiness" is available - auto extensionsAndExtras = meshMaterial.Data().extensionsAndExtras; - if ( !extensionsAndExtras.empty() ) { - auto extensions = extensionsAndExtras.find( "extensions" ); - if ( extensions != extensionsAndExtras.end() ) { - auto iter = extensions->find( "KHR_materials_pbrSpecularGlossiness" ); - if ( iter != extensions->end() ) { - gltf_PBRSpecularGlossiness gltfMaterial; - from_json( *iter, gltfMaterial ); - return buildSpecularGlossinessMaterial( - doc, meshIndex, filePath, meshPartNumber, meshMaterial, gltfMaterial ); - } - } - } - /// TODO : generate a default MetallicRoughness with the base parameters - return buildDefaultMaterial( doc, meshIndex, filePath, meshPartNumber, meshMaterial ); - } -#else + if ( meshMaterial.isSpecularGlossiness() ) { auto extensions = meshMaterial.Data().extensionsAndExtras.find( "extensions" ); auto iter = extensions->find( "KHR_materials_pbrSpecularGlossiness" ); @@ -593,7 +579,6 @@ Ra::Core::Asset::MaterialData* buildMaterial( const gltf::Document& doc, return buildDefaultMaterial( doc, meshIndex, filePath, meshPartNumber, meshMaterial ); } } -#endif } } // namespace GLTF diff --git a/src/IO/Gltf/internal/GLTFConverter/MaterialConverter.hpp b/src/IO/Gltf/internal/GLTFConverter/MaterialConverter.hpp index 6ed2eef63b2..9c9b7312ce4 100644 --- a/src/IO/Gltf/internal/GLTFConverter/MaterialConverter.hpp +++ b/src/IO/Gltf/internal/GLTFConverter/MaterialConverter.hpp @@ -35,17 +35,11 @@ class MaterialData */ [[nodiscard]] fx::gltf::Material const& Data() const noexcept { return m_material; } -#ifdef LEGACY_IMPLEMENTATION - /*** - * Test if the data are valid - * @return - */ - [[nodiscard]] bool isMetallicRoughness() const noexcept { - return m_hasData && !m_material.pbrMetallicRoughness.empty(); - } -#else /** - * Test if material is specularGlossiness + * Test if material is specularGlossiness. + * Right now, two materials are defined for RadiumGLTF : specularGlossiness and + * MetallicRoughness Each of these materials accepts some extensions described in the spec. + * \note specularGlossiness is deprecated in the spec */ [[nodiscard]] bool isSpecularGlossiness() const noexcept { auto extensionsAndExtras = Data().extensionsAndExtras; @@ -60,7 +54,7 @@ class MaterialData } [[nodiscard]] bool hasData() const noexcept { return m_hasData; } -#endif + private: fx::gltf::Material m_material {}; bool m_hasData {}; From e61bc3ce0a00e3dc8e4cef41df03efe2024135af Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Tue, 25 Jul 2023 14:56:06 +0200 Subject: [PATCH 16/27] [doc] add notes on extensions --- doc/concepts/gltfconformance.md | 64 +++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/doc/concepts/gltfconformance.md b/doc/concepts/gltfconformance.md index 66d8c91cceb..a0eeeed4d75 100644 --- a/doc/concepts/gltfconformance.md +++ b/doc/concepts/gltfconformance.md @@ -16,7 +16,7 @@ sample models available at https://github.com/KhronosGroup/glTF-Sample-Models. The structure of this document is the same than those of the [official specification](https://github.com/KhronosGroup/glTF/tree/master/specification/2.0) -* [Concepts](#concepts) +* [GLTF 2.0 elements](#gltf_elements) * [Asset](#asset) * [Indices and Names](#indices-and-names) * [Coordinate System and Units](#coordinate-system-and-units) @@ -52,10 +52,11 @@ The structure of this document is the same than those of the * [Cameras](#cameras) * [Projection Matrices](#projection-matrices) * [Animations](#animations) - * [Specifying Extensions](#specifying-extensions) - * [Supported extensions](#supported-extensions) +* [Specifying Extensions](#extensions) + * [Supported extensions](#supported-extensions) + * [Unsupported extensions](#unsupported-extensions) -## Concepts {#concepts} +## GLTF 2.0 elements {#gltf_elements} ### Asset {#asset} @@ -219,12 +220,14 @@ Animation key-framed definition is properly loaded and transmitted to Radium. No > In a next version, support all the key-frames interpolators. -### Specifying Extensions {#specifying-extensions} +## Extensions {#extensions} glTF defines an extension mechanism that allows the base format to be extended with new capabilities. -Any glTF object can have an optional `extensions` property, as in the following example: +Some extensions are ratified by Khronos Groups (__KHR_extensions__), others are proposed by vendors (Adobe, nvidia, microsoft, ...). +Radium GLTF will integrate __KHR__ extensions as much as possible. +Bellow is the list of supported, WIP and unsupported extensions in RadiumGLTF. -#### Supported extensions {#supported-extensions} +### Supported extensions {#supported-extensions} * __KHR_materials_pbrSpecularGlossiness__, defines a PBR material based on specular color and shininess exponent is supported (spec at https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness). @@ -248,3 +251,50 @@ intensities. (spec at https://github.com/KhronosGroup/glTF/tree/main/extensions/ * __KHR_materials_sheen__, defines a sheen that can be layered on top of an existing glTF material definition. A sheen layer is a common technique used in Physically-Based Rendering to represent cloth and fabric materials (spec at https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_sheen) + +* __KHR_materials_unlit__, defines an unlit shading model for use in glTF 2.0 materials, as an alternative to the +Physically Based Rendering (PBR) shading models provided by the core specification. Three motivating uses cases for +unlit materials include: + * Mobile devices with limited resources, where unlit materials offer a performant alternative to higher-quality +shading models. + * Photogrammetry, in which lighting information is already present and additional lighting should not be applied. + * Stylized materials (such as "anime" or "hand-drawn" looks) in which lighting is undesirable for aesthetic reasons. + + (spec at https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_unlit) + +### Unsupported extensions {#unsupported-extensions} + +* __KHR_draco_mesh_compression__, defines a schema to use Draco geometry compression (non-normative) libraries in glTF format. + (spec at https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_draco_mesh_compression) + +* __KHR_materials_anisotropy (WIP)__, defines the anisotropic property of a material as observable with brushed metals for example. + (spec at https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_anisotropy) + +* __KHR_materials_emissive_strength (WIP)__, defines emissiveStrength scalar factor, that governs the upper limit of emissive +strength per material. + (spec at https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_emissive_strength) + +* __KHR_materials_iridescence (WIP)__, describes an effect where hue varies depending on the viewing angle and illumination angle. + (spec at https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_iridescence) + +* __KHR_materials_transmission (WIP)__, describes transparency in a PBR way. + (spec at https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_transmission) + +* __KHR_materials_variants__, allows for a compact glTF representation of multiple material variants of an asset, +structured to allow low-latency switching at runtime. + (spec at https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_variants) + +* __KHR_materials_volume__, makes it possible to turn the surface into an interface between volumes. The volume +extension needs to be combined with an extension which allows light to transmit through the surface, e.g. +__KHR_materials_transmission__. + (spec at https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_volume) + +* __KHR_mesh_quantization__, expands the set of allowed component types for mesh attribute storage to provide a +memory/precision tradeoff - depending on the application needs, 16-bit or 8-bit storage can be sufficient. + (spec at https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_mesh_quantization) + +* __KHR_texture_basisu__, adds the ability to specify textures using KTX v2 images with Basis Universal supercompression. + (spec at https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_texture_basisu) + +* __KHR_xmp_json_ld__, adds support for XMP (Extensible Metadata Platform) (ISO 16684-1) metadata to glTF. + (spec at https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_xmp_json_ld) From a22c360270d5c5fd4df425466dd1c89b84013a55 Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Tue, 25 Jul 2023 16:33:50 +0200 Subject: [PATCH 17/27] [io] decode uri from gltf json --- .../Gltf/internal/GLTFConverter/ImageData.hpp | 3 ++- src/IO/Gltf/internal/fx/gltf.h | 25 ++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/IO/Gltf/internal/GLTFConverter/ImageData.hpp b/src/IO/Gltf/internal/GLTFConverter/ImageData.hpp index 3ca256027be..59795248cb8 100644 --- a/src/IO/Gltf/internal/GLTFConverter/ImageData.hpp +++ b/src/IO/Gltf/internal/GLTFConverter/ImageData.hpp @@ -38,7 +38,8 @@ class ImageData bool isEmbedded = image.IsEmbeddedResource(); if ( !image.uri.empty() && !isEmbedded ) { - m_info.FileName = fx::gltf::detail::GetDocumentRootPath( modelPath ) + "/" + image.uri; + m_info.FileName = fx::gltf::detail::GetDocumentRootPath( modelPath ) + "/" + + fx::gltf::detail::UriDecode( image.uri ); } else { if ( isEmbedded ) { diff --git a/src/IO/Gltf/internal/fx/gltf.h b/src/IO/Gltf/internal/fx/gltf.h index b0a0a0d6167..447d7422a3b 100644 --- a/src/IO/Gltf/internal/fx/gltf.h +++ b/src/IO/Gltf/internal/fx/gltf.h @@ -161,6 +161,29 @@ class invalid_gltf_document : public std::runtime_error }; namespace detail { +// according to https://github.com/KhronosGroup/glTF/issues/1449 +// uri should be decoded to access files +inline std::string UriDecode( const std::string& value ) { + std::string result; + result.reserve( value.size() ); + + for ( std::size_t i = 0; i < value.size(); ++i ) { + auto ch = value[i]; + + if ( ch == '%' && ( i + 2 ) < value.size() ) { + auto hex = value.substr( i + 1, 2 ); + auto dec = static_cast( std::strtol( hex.c_str(), nullptr, 16 ) ); + result.push_back( dec ); + i += 2; + } + // Not needed here + // else if ( ch == '+' ) { result.push_back( ' ' ); } + else { result.push_back( ch ); } + } + + return result; +} + #if defined( FX_GLTF_HAS_CPP_17 ) template inline void ReadRequiredField( std::string_view key, nlohmann::json const& json, TTarget& target ) @@ -241,7 +264,7 @@ inline std::string CreateBufferUriPath( std::string const& documentRootPath, if ( documentRoot.back() != '/' ) { documentRoot.push_back( '/' ); } } - return documentRoot + bufferUri; + return documentRoot + UriDecode( bufferUri ); } struct ChunkHeader { From 8a84290e573a9cd33080e1c534e68cdfebaac435 Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Tue, 25 Jul 2023 16:39:40 +0200 Subject: [PATCH 18/27] [engine] simplify gltf files --- src/Engine/Data/MetallicRoughnessMaterial.cpp | 33 +++++++++++++++- src/Engine/Data/MetallicRoughnessMaterial.hpp | 9 ++++- .../MetallicRoughnessMaterialConverter.cpp | 38 ------------------- .../MetallicRoughnessMaterialConverter.hpp | 26 ------------- .../Data/SpecularGlossinessMaterial.cpp | 29 +++++++++++++- .../Data/SpecularGlossinessMaterial.hpp | 10 ++++- .../SpecularGlossinessMaterialConverter.cpp | 36 ------------------ .../SpecularGlossinessMaterialConverter.hpp | 25 ------------ src/Engine/filelist.cmake | 4 -- 9 files changed, 77 insertions(+), 133 deletions(-) delete mode 100644 src/Engine/Data/MetallicRoughnessMaterialConverter.cpp delete mode 100644 src/Engine/Data/MetallicRoughnessMaterialConverter.hpp delete mode 100644 src/Engine/Data/SpecularGlossinessMaterialConverter.cpp delete mode 100644 src/Engine/Data/SpecularGlossinessMaterialConverter.hpp diff --git a/src/Engine/Data/MetallicRoughnessMaterial.cpp b/src/Engine/Data/MetallicRoughnessMaterial.cpp index 9af3fa7e6b4..bec4acdfcde 100644 --- a/src/Engine/Data/MetallicRoughnessMaterial.cpp +++ b/src/Engine/Data/MetallicRoughnessMaterial.cpp @@ -1,8 +1,8 @@ +#include #include #include #include #include -#include #include #include #include @@ -160,6 +160,37 @@ MetallicRoughness::getTextureTransform( const TextureSemantic& semantic ) const return GLTFMaterial::getTextureTransform( semantic ); } +/* + * Core to Engine converter + */ +using namespace Ra::Core::Asset; + +Material* +MetallicRoughnessMaterialConverter::operator()( const Ra::Core::Asset::MaterialData* toconvert ) { + auto result = new MetallicRoughness( toconvert->getName() ); + auto source = static_cast( toconvert ); + + result->fillBaseFrom( source ); + + result->m_baseColorFactor = source->m_baseColorFactor; + if ( source->m_hasBaseColorTexture ) { + result->addTexture( + { "TEX_BASECOLOR" }, source->m_baseColorTexture, source->m_baseSampler ); + result->m_baseTextureTransform = std::move( source->m_baseTextureTransform ); + } + result->m_metallicFactor = source->m_metallicFactor; + result->m_roughnessFactor = source->m_roughnessFactor; + if ( source->m_hasMetallicRoughnessTexture ) { + result->addTexture( { "TEX_METALLICROUGHNESS" }, + source->m_metallicRoughnessTexture, + source->m_metallicRoughnessSampler ); + result->m_metallicRoughnessTextureTransform = + std::move( source->m_metallicRoughnessTextureTransform ); + } + + return result; +} + } // namespace Data } // namespace Engine } // namespace Ra diff --git a/src/Engine/Data/MetallicRoughnessMaterial.hpp b/src/Engine/Data/MetallicRoughnessMaterial.hpp index e59e7feb828..115b5cecfef 100644 --- a/src/Engine/Data/MetallicRoughnessMaterial.hpp +++ b/src/Engine/Data/MetallicRoughnessMaterial.hpp @@ -12,7 +12,14 @@ namespace Ra { namespace Engine { namespace Data { -class MetallicRoughnessMaterialConverter; +/** + * Radium IO to Engine conversion for pbrMetallicRoughness + */ +class RA_ENGINE_API MetallicRoughnessMaterialConverter +{ + public: + Ra::Engine::Data::Material* operator()( const Ra::Core::Asset::MaterialData* toconvert ); +}; /** * Radium Engine material representation of pbrMetallicRoughness diff --git a/src/Engine/Data/MetallicRoughnessMaterialConverter.cpp b/src/Engine/Data/MetallicRoughnessMaterialConverter.cpp deleted file mode 100644 index 82b5c04728f..00000000000 --- a/src/Engine/Data/MetallicRoughnessMaterialConverter.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include -#include -#include - -namespace Ra { -namespace Engine { -namespace Data { -using namespace Ra::Core::Asset; - -Material* -MetallicRoughnessMaterialConverter::operator()( const Ra::Core::Asset::MaterialData* toconvert ) { - auto result = new MetallicRoughness( toconvert->getName() ); - auto source = static_cast( toconvert ); - - result->fillBaseFrom( source ); - - result->m_baseColorFactor = source->m_baseColorFactor; - if ( source->m_hasBaseColorTexture ) { - result->addTexture( - { "TEX_BASECOLOR" }, source->m_baseColorTexture, source->m_baseSampler ); - result->m_baseTextureTransform = std::move( source->m_baseTextureTransform ); - } - result->m_metallicFactor = source->m_metallicFactor; - result->m_roughnessFactor = source->m_roughnessFactor; - if ( source->m_hasMetallicRoughnessTexture ) { - result->addTexture( { "TEX_METALLICROUGHNESS" }, - source->m_metallicRoughnessTexture, - source->m_metallicRoughnessSampler ); - result->m_metallicRoughnessTextureTransform = - std::move( source->m_metallicRoughnessTextureTransform ); - } - - return result; -} - -} // namespace Data -} // namespace Engine -} // namespace Ra diff --git a/src/Engine/Data/MetallicRoughnessMaterialConverter.hpp b/src/Engine/Data/MetallicRoughnessMaterialConverter.hpp deleted file mode 100644 index c89c9557301..00000000000 --- a/src/Engine/Data/MetallicRoughnessMaterialConverter.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include - -#include -#include - -namespace Ra { -namespace Engine { -namespace Data { -/** - * Radium IO to Engine conversion for pbrMetallicRoughness - */ -class RA_ENGINE_API MetallicRoughnessMaterialConverter -{ - public: - MetallicRoughnessMaterialConverter() = default; - - ~MetallicRoughnessMaterialConverter() = default; - - Ra::Engine::Data::Material* operator()( const Ra::Core::Asset::MaterialData* toconvert ); -}; - -} // namespace Data -} // namespace Engine -} // namespace Ra diff --git a/src/Engine/Data/SpecularGlossinessMaterial.cpp b/src/Engine/Data/SpecularGlossinessMaterial.cpp index 3e9aabd8906..0c21e02ad66 100644 --- a/src/Engine/Data/SpecularGlossinessMaterial.cpp +++ b/src/Engine/Data/SpecularGlossinessMaterial.cpp @@ -1,9 +1,9 @@ +#include #include #include #include #include #include -#include #include #include @@ -174,6 +174,33 @@ SpecularGlossiness::getTextureTransform( const TextureSemantic& semantic ) const return GLTFMaterial::getTextureTransform( semantic ); } +/* + * Core to Engine converter + */ +using namespace Ra::Core::Asset; + +Material* SpecularGlossinessMaterialConverter::operator()( const MaterialData* toconvert ) { + auto result = new SpecularGlossiness( toconvert->getName() ); + auto source = static_cast( toconvert ); + + result->fillBaseFrom( source ); + + result->m_diffuseFactor = source->m_diffuseFactor; + if ( source->m_hasDiffuseTexture ) { + result->addTexture( { "TEX_DIFFUSE" }, source->m_diffuseTexture, source->m_diffuseSampler ); + result->m_diffuseTextureTransform = std::move( source->m_diffuseTextureTransform ); + } + result->m_specularFactor = source->m_specularFactor; + result->m_glossinessFactor = source->m_glossinessFactor; + if ( source->m_hasSpecularGlossinessTexture ) { + result->addTexture( { "TEX_SPECULARGLOSSINESS" }, + source->m_specularGlossinessTexture, + source->m_specularGlossinessSampler ); + result->m_specularGlossinessTransform = std::move( source->m_specularGlossinessTransform ); + } + + return result; +} } // namespace Data } // namespace Engine } // namespace Ra diff --git a/src/Engine/Data/SpecularGlossinessMaterial.hpp b/src/Engine/Data/SpecularGlossinessMaterial.hpp index 26176847428..a2f4e2ab222 100644 --- a/src/Engine/Data/SpecularGlossinessMaterial.hpp +++ b/src/Engine/Data/SpecularGlossinessMaterial.hpp @@ -11,7 +11,15 @@ namespace Ra { namespace Engine { namespace Data { -class SpecularGlossinessMaterialConverter; +/** + * Radium IO to Engine conversion for pbrSpecularGlossiness + */ +class RA_ENGINE_API SpecularGlossinessMaterialConverter +{ + public: + Ra::Engine::Data::Material* operator()( const Ra::Core::Asset::MaterialData* toconvert ); +}; + /** * Radium Engine material representation of GLTF SpecularGlossiness Material * Texture semantics defined by this material : diff --git a/src/Engine/Data/SpecularGlossinessMaterialConverter.cpp b/src/Engine/Data/SpecularGlossinessMaterialConverter.cpp deleted file mode 100644 index b76cdcd709d..00000000000 --- a/src/Engine/Data/SpecularGlossinessMaterialConverter.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include -#include -#include - -namespace Ra { -namespace Engine { -namespace Data { - -using namespace Ra::Core::Asset; - -Material* SpecularGlossinessMaterialConverter::operator()( const MaterialData* toconvert ) { - auto result = new SpecularGlossiness( toconvert->getName() ); - auto source = static_cast( toconvert ); - - result->fillBaseFrom( source ); - - result->m_diffuseFactor = source->m_diffuseFactor; - if ( source->m_hasDiffuseTexture ) { - result->addTexture( { "TEX_DIFFUSE" }, source->m_diffuseTexture, source->m_diffuseSampler ); - result->m_diffuseTextureTransform = std::move( source->m_diffuseTextureTransform ); - } - result->m_specularFactor = source->m_specularFactor; - result->m_glossinessFactor = source->m_glossinessFactor; - if ( source->m_hasSpecularGlossinessTexture ) { - result->addTexture( { "TEX_SPECULARGLOSSINESS" }, - source->m_specularGlossinessTexture, - source->m_specularGlossinessSampler ); - result->m_specularGlossinessTransform = std::move( source->m_specularGlossinessTransform ); - } - - return result; -} - -} // namespace Data -} // namespace Engine -} // namespace Ra diff --git a/src/Engine/Data/SpecularGlossinessMaterialConverter.hpp b/src/Engine/Data/SpecularGlossinessMaterialConverter.hpp deleted file mode 100644 index fe16bca886b..00000000000 --- a/src/Engine/Data/SpecularGlossinessMaterialConverter.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once -#include - -#include -#include - -namespace Ra { -namespace Engine { -namespace Data { -/** - * Radium IO to Engine conversion for pbrSpecularGlossiness - */ -class RA_ENGINE_API SpecularGlossinessMaterialConverter -{ - public: - SpecularGlossinessMaterialConverter() = default; - - ~SpecularGlossinessMaterialConverter() = default; - - Ra::Engine::Data::Material* operator()( const Ra::Core::Asset::MaterialData* toconvert ); -}; - -} // namespace Data -} // namespace Engine -} // namespace Ra diff --git a/src/Engine/filelist.cmake b/src/Engine/filelist.cmake index 0dcd036441e..8e6dd1a5596 100644 --- a/src/Engine/filelist.cmake +++ b/src/Engine/filelist.cmake @@ -14,7 +14,6 @@ set(engine_sources Data/MaterialConverters.cpp Data/Mesh.cpp Data/MetallicRoughnessMaterial.cpp - Data/MetallicRoughnessMaterialConverter.cpp Data/PlainMaterial.cpp Data/RawShaderMaterial.cpp Data/RenderParameters.cpp @@ -24,7 +23,6 @@ set(engine_sources Data/ShaderProgramManager.cpp Data/SimpleMaterial.cpp Data/SpecularGlossinessMaterial.cpp - Data/SpecularGlossinessMaterialConverter.cpp Data/Texture.cpp Data/TextureManager.cpp Data/VolumeObject.cpp @@ -72,7 +70,6 @@ set(engine_headers Data/MaterialConverters.hpp Data/Mesh.hpp Data/MetallicRoughnessMaterial.hpp - Data/MetallicRoughnessMaterialConverter.hpp Data/PlainMaterial.hpp Data/RawShaderMaterial.hpp Data/RenderParameters.hpp @@ -82,7 +79,6 @@ set(engine_headers Data/ShaderProgramManager.hpp Data/SimpleMaterial.hpp Data/SpecularGlossinessMaterial.hpp - Data/SpecularGlossinessMaterialConverter.hpp Data/Texture.hpp Data/TextureManager.hpp Data/ViewingParameters.hpp From ebf2f25b616f348610dd1ce5d44b9143fd580fd8 Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Tue, 25 Jul 2023 16:40:05 +0200 Subject: [PATCH 19/27] [io] fix material extension howto --- .../Gltf/internal/Extensions/MaterialExtensions.hpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/IO/Gltf/internal/Extensions/MaterialExtensions.hpp b/src/IO/Gltf/internal/Extensions/MaterialExtensions.hpp index 5485b72729b..f9b60e94b27 100644 --- a/src/IO/Gltf/internal/Extensions/MaterialExtensions.hpp +++ b/src/IO/Gltf/internal/Extensions/MaterialExtensions.hpp @@ -328,15 +328,16 @@ inline void to_json( nlohmann::json& json, gltf_KHRMaterialsUnlit const& khrMate * Adding a material extension * 1. define in this file the json parsing of the extension, based on its specification and json * schema from https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/ - * 2. add the extension in the supported extensions list in Core/Material/xxGLTFxx.cpp (for + * 2. add the extension in the supported extensions list (gltfSupportedExtensions) in Converter.cpp + * 3. add the extension in the compatible extensions list in Core/Material/xxGLTFxx.cpp (for * materials accepting this extension) - * 3. add the extension in the supported extensions list (gltfSupportedExtensions) in Converter.cpp - * 3. Define the Core/Material representation of the extension as inheriting from + * 4. Define the Core/Material representation of the extension as inheriting from * GLTFMaterialExtensionData - * 4. Add the extension converter in MaterialConverter.cpp - * 5. Define the Engine/Data representation and management of the extension as a material layer in + * 5. Add the extension converter in MaterialConverter.cpp that translate gltf representation of + * the extension to its Radium Core representation + * 6. Define the Engine/Data representation and management of the extension as a material layer in * Engine/Data/GLTFMaterial.xpp - * 6. Write the glsl management of the extension in the shaders + * 7. Write the glsl management of the extension in the shaders */ } // namespace GLTF } // namespace IO From d4b9e825f4d65f27f3f343ba344f38df0683c7f9 Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Tue, 25 Jul 2023 18:25:28 +0200 Subject: [PATCH 20/27] [io] loads interleaved buffers --- .../Gltf/internal/GLTFConverter/MeshData.cpp | 71 ++++++++++++------- .../Gltf/internal/GLTFConverter/MeshData.hpp | 2 +- 2 files changed, 47 insertions(+), 26 deletions(-) diff --git a/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp b/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp index 6c928c6ed37..b16f0009f58 100644 --- a/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp +++ b/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp @@ -13,31 +13,45 @@ using namespace Ra::Core::Asset; using namespace Ra::Core::Utils; // used to convert position and normals and ... +// takes care of interleaved buffers template -void convertVectors( Vector3Array& vectors, const void* data, int count ) { - auto mem = reinterpret_cast( data ); - for ( int i = 0; i < count; ++i ) { - vectors.emplace_back( mem[3 * i], mem[3 * i + 1], mem[3 * i + 2] ); +void convertVectors( Vector3Array& vectors, + const uint8_t* data, + uint32_t count, + uint32_t stride = 0 ) { + for ( uint32_t i = 0; i < count; ++i ) { + auto rawVector = reinterpret_cast( data ); + vectors.emplace_back( rawVector[0], rawVector[1], rawVector[2] ); + data += std::max( uint32_t( 3 * sizeof( T ) ), stride ); } } // GLTF texCoord are vec2 +// takes care of interleaved buffers // Warning : textCoord could be normalized integers template -void convertTexCoord( Vector3Array& vectors, const void* data, int count ) { - auto mem = reinterpret_cast( data ); - for ( int i = 0; i < count; ++i ) { - float u = float( mem[2 * i] ) / std::numeric_limits::max(); - float v = 1 - float( mem[2 * i + 1] ) / std::numeric_limits::max(); +void convertTexCoord( Vector3Array& vectors, + const uint8_t* data, + uint32_t count, + uint32_t stride = 0 ) { + for ( uint32_t i = 0; i < count; ++i ) { + auto rawTexCoord = reinterpret_cast( data ); + float u = float( rawTexCoord[0] ) / std::numeric_limits::max(); + float v = 1 - float( rawTexCoord[1] ) / std::numeric_limits::max(); vectors.emplace_back( u, v, 0 ); + data += std::max( uint32_t( 2 * sizeof( T ) ), stride ); } } template <> -void convertTexCoord( Vector3Array& vectors, const void* data, int count ) { - auto mem = reinterpret_cast( data ); - for ( int i = 0; i < count; ++i ) { - vectors.emplace_back( mem[2 * i], 1 - mem[2 * i + 1], 0 ); +void convertTexCoord( Vector3Array& vectors, + const uint8_t* data, + uint32_t count, + uint32_t stride ) { + for ( uint32_t i = 0; i < count; ++i ) { + auto rawVector = reinterpret_cast( data ); + vectors.emplace_back( rawVector[0], 1 - rawVector[1], 0 ); + data += std::max( uint32_t( 2 * sizeof( float ) ), stride ); } } @@ -45,12 +59,15 @@ void convertTexCoord( Vector3Array& vectors, const void* data, int count // Multiply the tangent coordinates by the handedness to have always right handed local frame // Must verify this template -void convertTangents( Vector3Array& vectors, const void* data, int count ) { - auto mem = reinterpret_cast( data ); - for ( int i = 0; i < count; ++i ) { - vectors.emplace_back( mem[4 * i] * mem[4 * i + 3], - mem[4 * i + 1] * mem[4 * i + 3], - mem[4 * i + 2] * mem[4 * i + 3] ); +void convertTangents( Vector3Array& vectors, + const uint8_t* data, + uint32_t count, + uint32_t stride = 0 ) { + for ( uint32_t i = 0; i < count; ++i ) { + auto rawVector = reinterpret_cast( data ); + vectors.emplace_back( + rawVector[0] * rawVector[3], rawVector[1] * rawVector[3], rawVector[2] * rawVector[3] ); + data += std::max( uint32_t( 4 * sizeof( T ) ), stride ); } } @@ -106,7 +123,8 @@ std::vector> buildMesh( const glt auto& vertices = meshPart->getGeometry().vertexAttribs().getDataWithLock( attribVertices ); vertices.reserve( vBuffer.Accessor->count ); - convertVectors( vertices, vBuffer.Data, vBuffer.Accessor->count ); + convertVectors( + vertices, vBuffer.Data, vBuffer.Accessor->count, vBuffer.DataStride ); meshPart->getGeometry().vertexAttribs().unlock( attribVertices ); // Convert faces @@ -170,7 +188,8 @@ std::vector> buildMesh( const glt auto& normals = meshPart->getGeometry().vertexAttribs().getDataWithLock( attribHandle ); normals.reserve( nBuffer.Accessor->count ); - convertVectors( normals, nBuffer.Data, nBuffer.Accessor->count ); + convertVectors( + normals, nBuffer.Data, nBuffer.Accessor->count, nBuffer.DataStride ); meshPart->getGeometry().vertexAttribs().unlock( attribHandle ); } else { @@ -194,14 +213,15 @@ std::vector> buildMesh( const glt switch ( cBuffer.Accessor->componentType ) { case gltf::Accessor::ComponentType::UnsignedByte: convertTexCoord( - texcoords, cBuffer.Data, cBuffer.Accessor->count ); + texcoords, cBuffer.Data, cBuffer.Accessor->count, cBuffer.DataStride ); break; case gltf::Accessor::ComponentType::UnsignedShort: convertTexCoord( - texcoords, cBuffer.Data, cBuffer.Accessor->count ); + texcoords, cBuffer.Data, cBuffer.Accessor->count, cBuffer.DataStride ); break; case gltf::Accessor::ComponentType::Float: - convertTexCoord( texcoords, cBuffer.Data, cBuffer.Accessor->count ); + convertTexCoord( + texcoords, cBuffer.Data, cBuffer.Accessor->count, cBuffer.DataStride ); break; default: LOG( logERROR ) << "GLTF buildMesh -- texCoord must be UnsignedByte, " @@ -225,7 +245,8 @@ std::vector> buildMesh( const glt auto& tangents = meshPart->getGeometry().vertexAttribs().getDataWithLock( attribHandle ); tangents.reserve( tBuffer.Accessor->count ); - convertTangents( tangents, tBuffer.Data, tBuffer.Accessor->count ); + convertTangents( + tangents, tBuffer.Data, tBuffer.Accessor->count, tBuffer.DataStride ); meshPart->getGeometry().vertexAttribs().unlock( attribHandle ); } else { diff --git a/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp b/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp index ed2622ff23f..2c0a6394ce9 100644 --- a/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp +++ b/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp @@ -245,7 +245,7 @@ class MeshData const uint32_t dataTypeSize = CalculateDataTypeSize( accessor ); return BufferInfo { &accessor, &buffer.data[bufferView.byteOffset + accessor.byteOffset], - dataTypeSize, + bufferView.byteStride, accessor.count * dataTypeSize }; } From d4452244fe9706aa30f5844ce0e880ab326e048f Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Wed, 26 Jul 2023 09:19:34 +0200 Subject: [PATCH 21/27] [engine] allows pervertex color for any materials (missing code) --- src/Engine/Scene/GeometryComponent.cpp | 6 ++++-- src/Engine/Scene/GeometryComponent.hpp | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Engine/Scene/GeometryComponent.cpp b/src/Engine/Scene/GeometryComponent.cpp index c9056fed4dd..d1f78a00d44 100644 --- a/src/Engine/Scene/GeometryComponent.cpp +++ b/src/Engine/Scene/GeometryComponent.cpp @@ -90,13 +90,15 @@ void PointCloudComponent::finalizeROFromGeometry( const Core::Asset::MaterialDat if ( data != nullptr ) { auto converter = Data::EngineMaterialConverters::getMaterialConverter( data->getType() ); auto mat = converter.second( data ); + mat->setColoredByVertexAttrib( m_displayMesh->getCoreGeometry().hasAttrib( + Ra::Core::Geometry::getAttribName( Ra::Core::Geometry::VERTEX_COLOR ) ) ); roMaterial.reset( mat ); } else { auto mat = new Data::BlinnPhongMaterial( m_contentName + "_DefaultBPMaterial" ); mat->m_renderAsSplat = m_displayMesh->getNumFaces() == 0; - mat->m_perVertexColor = m_displayMesh->getCoreGeometry().hasAttrib( - Ra::Core::Geometry::getAttribName( Ra::Core::Geometry::VERTEX_COLOR ) ); + mat->setColoredByVertexAttrib( m_displayMesh->getCoreGeometry().hasAttrib( + Ra::Core::Geometry::getAttribName( Ra::Core::Geometry::VERTEX_COLOR ) ) ); roMaterial.reset( mat ); } // initialize with a default rendertechique that draws nothing diff --git a/src/Engine/Scene/GeometryComponent.hpp b/src/Engine/Scene/GeometryComponent.hpp index 16fa5104e15..76255f2d4e6 100644 --- a/src/Engine/Scene/GeometryComponent.hpp +++ b/src/Engine/Scene/GeometryComponent.hpp @@ -258,13 +258,15 @@ void SurfaceMeshComponent::finalizeROFromGeometry( if ( data != nullptr ) { auto converter = Data::EngineMaterialConverters::getMaterialConverter( data->getType() ); auto mat = converter.second( data ); + mat->setColoredByVertexAttrib( m_displayMesh->getCoreGeometry().hasAttrib( + Ra::Core::Geometry::getAttribName( Ra::Core::Geometry::VERTEX_COLOR ) ) ); roMaterial.reset( mat ); } else { auto mat = new Data::BlinnPhongMaterial( m_contentName + "_DefaultBPMaterial" ); mat->m_renderAsSplat = m_displayMesh->getNumFaces() == 0; - mat->m_perVertexColor = m_displayMesh->getCoreGeometry().hasAttrib( - Ra::Core::Geometry::getAttribName( Ra::Core::Geometry::VERTEX_COLOR ) ); + mat->setColoredByVertexAttrib( m_displayMesh->getCoreGeometry().hasAttrib( + Ra::Core::Geometry::getAttribName( Ra::Core::Geometry::VERTEX_COLOR ) ) ); roMaterial.reset( mat ); } // initialize with a default rendertechique that draws nothing From 6d6e56d0f1b972716e86232d3a281d27198a5f89 Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Wed, 26 Jul 2023 09:20:12 +0200 Subject: [PATCH 22/27] [io] loads gltf color buffer (only one as requested by the spec) --- .../Gltf/internal/GLTFConverter/MeshData.cpp | 86 +++++++++++++++++++ .../Gltf/internal/GLTFConverter/MeshData.hpp | 16 +++- 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp b/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp index b16f0009f58..12f69821c1f 100644 --- a/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp +++ b/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp @@ -55,6 +55,47 @@ void convertTexCoord( Vector3Array& vectors, } } +// GLTF colors are vec3 or vec4. if vec3, alpha is assumed to be 1 +// takes care of interleaved buffers +// Warning : Colors could be normalized integers +template +void convertColor( Vector4Array& colors, + const uint8_t* data, + fx::gltf::Accessor::Type type, + uint32_t count, + uint32_t stride = 0 ) { + uint32_t numComponents = 3; + if ( type == gltf::Accessor::Type::Vec4 ) { numComponents = 4; } + for ( uint32_t i = 0; i < count; ++i ) { + auto rawColors = reinterpret_cast( data ); + Vector4 clr { 0, 0, 0, 1 }; + for ( uint32_t c = 0; c < numComponents; c++ ) { + clr[c] = float( rawColors[c] ) / std::numeric_limits::max(); + } + colors.emplace_back( clr ); + data += std::max( uint32_t( numComponents * sizeof( T ) ), stride ); + } +} + +template <> +void convertColor( Vector4Array& colors, + const uint8_t* data, + fx::gltf::Accessor::Type type, + uint32_t count, + uint32_t stride ) { + uint32_t numComponents = 3; + if ( type == gltf::Accessor::Type::Vec4 ) { numComponents = 4; } + for ( uint32_t i = 0; i < count; ++i ) { + auto rawColors = reinterpret_cast( data ); + Vector4 clr { 0, 0, 0, 1 }; + for ( uint32_t c = 0; c < numComponents; c++ ) { + clr[c] = rawColors[c]; + } + colors.emplace_back( clr ); + data += std::max( uint32_t( numComponents * sizeof( float ) ), stride ); + } +} + // GLTF tangents are vec4 with the last component indicating handedness. // Multiply the tangent coordinates by the handedness to have always right handed local frame // Must verify this @@ -232,6 +273,51 @@ std::vector> buildMesh( const glt } else { LOG( logDEBUG ) << "GLTF buildMesh -- No texCoord provided. !"; } + // Convert vertexColorColor if any (limited to one COLOR attribute) + const MeshData::BufferInfo& colorBuffer = mesh.ColorBuffer(); + if ( colorBuffer.HasData() ) { + if ( ( colorBuffer.Accessor->type != gltf::Accessor::Type::Vec3 ) && + ( colorBuffer.Accessor->type != gltf::Accessor::Type::Vec4 ) ) { + LOG( logERROR ) << "GLTF buildMesh -- Color must be Vec3 or Vec4"; + continue; + } + // Radium colors are always vec4 + auto attribHandle = meshPart->getGeometry().addAttrib( + getAttribName( Ra::Core::Geometry::MeshAttrib::VERTEX_COLOR ) ); + auto& colorAtttrib = + meshPart->getGeometry().vertexAttribs().getDataWithLock( attribHandle ); + colorAtttrib.reserve( colorBuffer.Accessor->count ); + switch ( colorBuffer.Accessor->componentType ) { + case gltf::Accessor::ComponentType::UnsignedByte: + convertColor( colorAtttrib, + colorBuffer.Data, + colorBuffer.Accessor->type, + colorBuffer.Accessor->count, + colorBuffer.DataStride ); + break; + case gltf::Accessor::ComponentType::UnsignedShort: + convertColor( colorAtttrib, + colorBuffer.Data, + colorBuffer.Accessor->type, + colorBuffer.Accessor->count, + colorBuffer.DataStride ); + break; + case gltf::Accessor::ComponentType::Float: + convertColor( colorAtttrib, + colorBuffer.Data, + colorBuffer.Accessor->type, + colorBuffer.Accessor->count, + colorBuffer.DataStride ); + break; + default: + LOG( logERROR ) << "GLTF buildMesh -- Color attrib must be UnsignedByte, " + "UnsignedShort or Float !"; + continue; + } + meshPart->getGeometry().vertexAttribs().unlock( attribHandle ); + } + else { LOG( logDEBUG ) << "GLTF buildMesh -- No Color attrib provided. !"; } + // Convert tangent if any const MeshData::BufferInfo& tBuffer = mesh.TangentBuffer(); if ( tBuffer.HasData() ) { diff --git a/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp b/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp index 2c0a6394ce9..821cee9bc97 100644 --- a/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp +++ b/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp @@ -70,6 +70,9 @@ class MeshData auto idTexCoord = std::stoi( attrib.first.substr( 9 ) ); m_texCoordBuffers[idTexCoord] = GetData( doc, doc.accessors[attrib.second] ); } + else if ( attrib.first == "COLOR_0" ) { + m_ColorBuffer = GetData( doc, doc.accessors[attrib.second] ); + } } if ( primitive.indices >= 0 ) { @@ -113,6 +116,12 @@ class MeshData return m_texCoordBuffers[i]; } + /** + * + * @return the color buffer of the mesh + */ + [[nodiscard]] const BufferInfo& ColorBuffer() const noexcept { return m_ColorBuffer; } + /** * * @return the mesh material data @@ -230,9 +239,12 @@ class MeshData BufferInfo m_vertexBuffer {}; BufferInfo m_normalBuffer {}; BufferInfo m_tangentBuffer {}; - // TODO : spec require to manage at least two texture coordinate sets + // spec require to manage at least two texture coordinate sets. + // only one will be given to Radium as Radium does not support multiple texcoord + // TODO, update RAdium to support at least 2 texcoord set std::array m_texCoordBuffers; - // BufferInfo m_texCoord0Buffer {}; + // spec require to support at least one color attribute + BufferInfo m_ColorBuffer {}; MaterialData m_materialData {}; From 984340d23073495c1803db02445dd21916936539 Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Wed, 26 Jul 2023 09:21:10 +0200 Subject: [PATCH 23/27] [engine] gltf material support pervertex color --- src/Engine/Data/GLTFMaterial.cpp | 4 +++- src/Engine/Data/GLTFMaterial.hpp | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Engine/Data/GLTFMaterial.cpp b/src/Engine/Data/GLTFMaterial.cpp index cc37a426ee3..af955da5d5e 100644 --- a/src/Engine/Data/GLTFMaterial.cpp +++ b/src/Engine/Data/GLTFMaterial.cpp @@ -269,9 +269,11 @@ void GLTFMaterial::fillBaseFrom( const Core::Material::BaseGLTFMaterial* source std::list GLTFMaterial::getPropertyList() const { std::list props = Ra::Engine::Data::Material::getPropertyList(); - // Expose the new GLTF__INTERFACE that will eveolve until it is submitted to Radium + // Expose the new GLTF__INTERFACE that will evolve until it is submitted to Radium // GLSL/Material interface props.emplace_back( "GLTF_MATERIAL_INTERFACE" ); + // per vertex color + if ( isColoredByVertexAttrib() ) { props.emplace_back( "HAS_PERVERTEX_COLOR" ); } // unlit if ( getUnlitStatus() ) { props.emplace_back( "MATERIAL_UNLIT" ); } // textures diff --git a/src/Engine/Data/GLTFMaterial.hpp b/src/Engine/Data/GLTFMaterial.hpp index 4af3c65545d..de668d7322e 100644 --- a/src/Engine/Data/GLTFMaterial.hpp +++ b/src/Engine/Data/GLTFMaterial.hpp @@ -250,6 +250,20 @@ class RA_ENGINE_API GLTFMaterial : public Material, public ParameterSetEditingIn */ nlohmann::json getParametersMetadata() const override; + /** + * \brief Makes the Material take its base color from the VERTEX_COLOR attribute of the rendered + * geometry \param state activate (true) or deactivate (false) VERTEX_COLOR attribute usage + * + * Any material that support per-vertex color parameterization should implement this method + * accordingly + */ + void setColoredByVertexAttrib( bool state ) override { m_isColoredByVertex = state; }; + + /** + * \brief Indicates if the material takes the VERTEX_COLOR attribute into account. + */ + bool isColoredByVertexAttrib() const override { return m_isColoredByVertex; } + /** * Register the material to the Radium Material subsystem */ @@ -342,6 +356,8 @@ class RA_ENGINE_API GLTFMaterial : public Material, public ParameterSetEditingIn typename std::underlying_type::type>; static std::shared_ptr s_AlphaModeEnum; + bool m_isColoredByVertex { false }; + protected: // todo : make this private with set/reset methods bool m_isOpenGlConfigured { false }; From d85e9a9d3f4b993fdf80ea1bfa699414f8a7871d Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Wed, 26 Jul 2023 15:18:14 +0200 Subject: [PATCH 24/27] [io] remove reinterpret_cast when loading gltf --- .../Gltf/internal/GLTFConverter/MeshData.cpp | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp b/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp index 12f69821c1f..0d0c91e42af 100644 --- a/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp +++ b/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp @@ -20,8 +20,9 @@ void convertVectors( Vector3Array& vectors, uint32_t count, uint32_t stride = 0 ) { for ( uint32_t i = 0; i < count; ++i ) { - auto rawVector = reinterpret_cast( data ); - vectors.emplace_back( rawVector[0], rawVector[1], rawVector[2] ); + std::array values { 0, 0, 0 }; + std::memcpy( values.data(), data, 3 * sizeof( float ) ); + vectors.emplace_back( values[0], values[1], values[2] ); data += std::max( uint32_t( 3 * sizeof( T ) ), stride ); } } @@ -29,28 +30,32 @@ void convertVectors( Vector3Array& vectors, // GLTF texCoord are vec2 // takes care of interleaved buffers // Warning : textCoord could be normalized integers +// any normalized integer version template void convertTexCoord( Vector3Array& vectors, const uint8_t* data, uint32_t count, uint32_t stride = 0 ) { for ( uint32_t i = 0; i < count; ++i ) { - auto rawTexCoord = reinterpret_cast( data ); - float u = float( rawTexCoord[0] ) / std::numeric_limits::max(); - float v = 1 - float( rawTexCoord[1] ) / std::numeric_limits::max(); - vectors.emplace_back( u, v, 0 ); + std::array values { 0, 0 }; + std::memcpy( values.data(), data, 2 * sizeof( T ) ); + vectors.emplace_back( Scalar( values[0] ) / std::numeric_limits::max(), + 1 - Scalar( values[1] ) / std::numeric_limits::max(), + 0 ); data += std::max( uint32_t( 2 * sizeof( T ) ), stride ); } } +// specialization for float template <> void convertTexCoord( Vector3Array& vectors, const uint8_t* data, uint32_t count, uint32_t stride ) { for ( uint32_t i = 0; i < count; ++i ) { - auto rawVector = reinterpret_cast( data ); - vectors.emplace_back( rawVector[0], 1 - rawVector[1], 0 ); + std::array values { 0, 0 }; + std::memcpy( values.data(), data, 2 * sizeof( float ) ); + vectors.emplace_back( values[0], 1 - values[1], 0 ); data += std::max( uint32_t( 2 * sizeof( float ) ), stride ); } } @@ -67,11 +72,11 @@ void convertColor( Vector4Array& colors, uint32_t numComponents = 3; if ( type == gltf::Accessor::Type::Vec4 ) { numComponents = 4; } for ( uint32_t i = 0; i < count; ++i ) { - auto rawColors = reinterpret_cast( data ); - Vector4 clr { 0, 0, 0, 1 }; - for ( uint32_t c = 0; c < numComponents; c++ ) { - clr[c] = float( rawColors[c] ) / std::numeric_limits::max(); - } + std::array values { 0, 0, 0, std::numeric_limits::max() }; + std::memcpy( values.data(), data, numComponents * sizeof( T ) ); + Vector4 clr { + Scalar( values[0] ), Scalar( values[1] ), Scalar( values[2] ), Scalar( values[3] ) }; + clr /= std::numeric_limits::max(); colors.emplace_back( clr ); data += std::max( uint32_t( numComponents * sizeof( T ) ), stride ); } @@ -86,12 +91,9 @@ void convertColor( Vector4Array& colors, uint32_t numComponents = 3; if ( type == gltf::Accessor::Type::Vec4 ) { numComponents = 4; } for ( uint32_t i = 0; i < count; ++i ) { - auto rawColors = reinterpret_cast( data ); - Vector4 clr { 0, 0, 0, 1 }; - for ( uint32_t c = 0; c < numComponents; c++ ) { - clr[c] = rawColors[c]; - } - colors.emplace_back( clr ); + std::array values { 0, 0, 0, 1 }; + std::memcpy( values.data(), data, numComponents * sizeof( float ) ); + colors.emplace_back( values[0], values[1], values[2], values[3] ); data += std::max( uint32_t( numComponents * sizeof( float ) ), stride ); } } @@ -105,9 +107,9 @@ void convertTangents( Vector3Array& vectors, uint32_t count, uint32_t stride = 0 ) { for ( uint32_t i = 0; i < count; ++i ) { - auto rawVector = reinterpret_cast( data ); - vectors.emplace_back( - rawVector[0] * rawVector[3], rawVector[1] * rawVector[3], rawVector[2] * rawVector[3] ); + std::array values { 0, 0, 0, 1 }; + std::memcpy( values.data(), data, 4 * sizeof( T ) ); + vectors.emplace_back( values[0] * values[3], values[1] * values[3], values[2] * values[3] ); data += std::max( uint32_t( 4 * sizeof( T ) ), stride ); } } @@ -115,9 +117,11 @@ void convertTangents( Vector3Array& vectors, // used to convert face indices template void convertIndices( Vector3uArray& indices, const uint8_t* data, uint32_t count ) { - auto mem = reinterpret_cast( data ); for ( uint32_t i = 0; i < count; ++i ) { - indices.push_back( { mem[3 * i], mem[3 * i + 1], mem[3 * i + 2] } ); + std::array values { 0, 0, 0 }; + std::memcpy( values.data(), data, 3 * sizeof( T ) ); + indices.push_back( { uint( values[0] ), uint( values[1] ), uint( values[2] ) } ); + data += uint32_t( 3 * sizeof( T ) ); } } From 6a4274a64ba23e6b368b1dfd5f707949d4128ca5 Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Tue, 1 Aug 2023 13:11:56 +0200 Subject: [PATCH 25/27] [core] improve doc --- src/Core/Material/BaseGLTFMaterial.hpp | 27 ++++++++++++++++--- src/Core/Material/GLTFTextureParameters.hpp | 2 +- .../MetallicRoughnessMaterialData.cpp | 9 ++++--- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/Core/Material/BaseGLTFMaterial.hpp b/src/Core/Material/BaseGLTFMaterial.hpp index a713eebb06d..2e1714c3c17 100644 --- a/src/Core/Material/BaseGLTFMaterial.hpp +++ b/src/Core/Material/BaseGLTFMaterial.hpp @@ -10,7 +10,7 @@ namespace Ra { namespace Core { namespace Material { -/// GLTF Alpha mode definition +/// \brief GLTF Alpha mode definition enum AlphaMode : unsigned int { Opaque = 0, Mask, Blend }; /** @@ -32,40 +32,52 @@ struct RA_CORE_API GLTFMaterialExtensionData : public Ra::Core::Asset::MaterialD class RA_CORE_API BaseGLTFMaterial : public Ra::Core::Asset::MaterialData { public: - /// Attributes of a gltf material /// Normal texture + ///\{ std::string m_normalTexture {}; float m_normalTextureScale { 1 }; GLTFSampler m_normalSampler {}; bool m_hasNormalTexture { false }; mutable std::unique_ptr m_normalTextureTransform { nullptr }; + ///\} /// Occlusion texture + ///\{ std::string m_occlusionTexture {}; float m_occlusionStrength { 1 }; GLTFSampler m_occlusionSampler {}; bool m_hasOcclusionTexture { false }; mutable std::unique_ptr m_occlusionTextureTransform { nullptr }; + ///} + /// Emissive texture + ///\{ std::string m_emissiveTexture {}; Ra::Core::Utils::Color m_emissiveFactor { 0.0, 0.0, 0.0, 1.0 }; GLTFSampler m_emissiveSampler {}; bool m_hasEmissiveTexture { false }; mutable std::unique_ptr m_emissiveTextureTransform { nullptr }; + ///\} /// Transparency parameters + ///\{ AlphaMode m_alphaMode { AlphaMode::Opaque }; float m_alphaCutoff { 0.5 }; + ///\} /// Face culling parameter bool m_doubleSided { false }; - // Extension data pass through the system + /// Material extensions data std::map> m_extensions {}; explicit BaseGLTFMaterial( const std::string& gltfType, const std::string& instanceName ); ~BaseGLTFMaterial() override = default; + /** + * \defgroup ExtensionManagement Material extension management + * \{ + */ virtual bool supportExtension( const std::string& extensionName ) { auto it = m_allowedExtensions.find( extensionName ); return ( it != m_allowedExtensions.end() ) && ( it->second ); @@ -79,7 +91,16 @@ class RA_CORE_API BaseGLTFMaterial : public Ra::Core::Asset::MaterialData void allowExtension( const std::string& extension ) { m_allowedExtensions[extension] = true; } + void allowExtensionList( std::initializer_list extensions ) { + std::for_each( extensions.begin(), extensions.end(), [this]( const std::string& e ) { + this->allowExtension( e ); + } ); + } + /// \} private: + /*** + * \ingroup ExtensionManagement + */ std::map m_allowedExtensions {}; }; diff --git a/src/Core/Material/GLTFTextureParameters.hpp b/src/Core/Material/GLTFTextureParameters.hpp index edabd42b0c1..b541569c3bc 100644 --- a/src/Core/Material/GLTFTextureParameters.hpp +++ b/src/Core/Material/GLTFTextureParameters.hpp @@ -38,7 +38,7 @@ struct RA_CORE_API GLTFTextureTransform { /** * \brief Sampler Data as defined by GlTF specification - * Enums correspond to OpenGL specification + * Enums correspond to OpenGL specification as requested by gltf */ struct RA_CORE_API GLTFSampler { enum class MagFilter : uint16_t { Nearest = 9728, Linear = 9729 }; diff --git a/src/Core/Material/MetallicRoughnessMaterialData.cpp b/src/Core/Material/MetallicRoughnessMaterialData.cpp index f0a7e6060f4..9233741120d 100644 --- a/src/Core/Material/MetallicRoughnessMaterialData.cpp +++ b/src/Core/Material/MetallicRoughnessMaterialData.cpp @@ -7,10 +7,11 @@ namespace Material { MetallicRoughnessData::MetallicRoughnessData( const std::string& name ) : BaseGLTFMaterial( { "MetallicRoughness" }, name ) { // extension supported by MetallicRoughness gltf materials - allowExtension( "KHR_materials_clearcoat" ); - allowExtension( "KHR_materials_ior" ); - allowExtension( "KHR_materials_specular" ); - allowExtension( "KHR_materials_sheen" ); + allowExtensionList( { "KHR_materials_clearcoat", + "KHR_materials_ior", + "KHR_materials_specular", + "KHR_materials_sheen" } ); + // TODO : uncomment the extension when supported by the implementation. /* allowExtension("KHR_materials_transmission"); From ea48c902d866f90b5dcdde1b651a7a67a6284e0d Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Tue, 29 Aug 2023 08:32:17 +0200 Subject: [PATCH 26/27] [io] remove warnings in gltf mikktspace.c --- .../Gltf/internal/GLTFConverter/mikktspace.c | 98 +++++++++---------- 1 file changed, 47 insertions(+), 51 deletions(-) diff --git a/src/IO/Gltf/internal/GLTFConverter/mikktspace.c b/src/IO/Gltf/internal/GLTFConverter/mikktspace.c index 1bffdd66cef..cb846cb5819 100644 --- a/src/IO/Gltf/internal/GLTFConverter/mikktspace.c +++ b/src/IO/Gltf/internal/GLTFConverter/mikktspace.c @@ -568,12 +568,12 @@ static void GenerateSharedVerticesIndexList( int piTriList_in_and_out[], if ( pTmpVert != NULL ) { for ( e = 0; e < iEntries; e++ ) { - int i = pTable[e]; - const SVec3 vP = GetPosition( pContext, piTriList_in_and_out[i] ); + int i_l = pTable[e]; + const SVec3 vP = GetPosition( pContext, piTriList_in_and_out[i_l] ); pTmpVert[e].vert[0] = vP.x; pTmpVert[e].vert[1] = vP.y; pTmpVert[e].vert[2] = vP.z; - pTmpVert[e].index = i; + pTmpVert[e].index = i_l; } MergeVertsFast( piTriList_in_and_out, pTmpVert, pContext, 0, iEntries - 1 ); } @@ -741,9 +741,8 @@ static void MergeVertsSlow( int piTriList_in_and_out[], static void GenerateSharedVerticesIndexListSlow( int piTriList_in_and_out[], const SMikkTSpaceContext* pContext, const int iNrTrianglesIn ) { - int iNumUniqueVerts = 0, t = 0, i = 0; - for ( t = 0; t < iNrTrianglesIn; t++ ) { - for ( i = 0; i < 3; i++ ) { + for ( int t = 0; t < iNrTrianglesIn; t++ ) { + for ( int i = 0; i < 3; i++ ) { const int offs = t * 3 + i; const int index = piTriList_in_and_out[offs]; @@ -770,9 +769,6 @@ static void GenerateSharedVerticesIndexListSlow( int piTriList_in_and_out[], } assert( bFound ); - // if we found our own - if ( index2rec == index ) { ++iNumUniqueVerts; } - piTriList_in_and_out[offs] = index2rec; } } @@ -827,14 +823,14 @@ static int GenerateInitialVerticesIndexList( STriInfo pTriInfos[], else if ( distSQ_13 < distSQ_02 ) bQuadDiagIs_02 = TFALSE; else { - const SVec3 P0 = GetPosition( pContext, i0 ); - const SVec3 P1 = GetPosition( pContext, i1 ); - const SVec3 P2 = GetPosition( pContext, i2 ); - const SVec3 P3 = GetPosition( pContext, i3 ); - const float distSQ_02 = LengthSquared( vsub( P2, P0 ) ); - const float distSQ_13 = LengthSquared( vsub( P3, P1 ) ); - - bQuadDiagIs_02 = distSQ_13 < distSQ_02 ? TFALSE : TTRUE; + const SVec3 P0 = GetPosition( pContext, i0 ); + const SVec3 P1 = GetPosition( pContext, i1 ); + const SVec3 P2 = GetPosition( pContext, i2 ); + const SVec3 P3 = GetPosition( pContext, i3 ); + const float distSQ_02_l = LengthSquared( vsub( P2, P0 ) ); + const float distSQ_13_l = LengthSquared( vsub( P3, P1 ) ); + + bQuadDiagIs_02 = distSQ_13_l < distSQ_02_l ? TFALSE : TTRUE; } if ( bQuadDiagIs_02 ) { @@ -968,13 +964,12 @@ static void InitTriInfo( STriInfo pTriInfos[], const int piTriListIn[], const SMikkTSpaceContext* pContext, const int iNrTrianglesIn ) { - int f = 0, i = 0, t = 0; // pTriInfos[f].iFlag is cleared in GenerateInitialVerticesIndexList() which is called before // this function. // generate neighbor info list - for ( f = 0; f < iNrTrianglesIn; f++ ) - for ( i = 0; i < 3; i++ ) { + for ( int f = 0; f < iNrTrianglesIn; f++ ) + for ( int i = 0; i < 3; i++ ) { pTriInfos[f].FaceNeighbors[i] = -1; pTriInfos[f].AssignedGroup[i] = NULL; @@ -992,7 +987,7 @@ static void InitTriInfo( STriInfo pTriInfos[], } // evaluate first order derivatives - for ( f = 0; f < iNrTrianglesIn; f++ ) { + for ( int f = 0; f < iNrTrianglesIn; f++ ) { // initial values const SVec3 v1 = GetPosition( pContext, piTriListIn[f * 3 + 0] ); const SVec3 v2 = GetPosition( pContext, piTriListIn[f * 3 + 1] ); @@ -1034,6 +1029,7 @@ static void InitTriInfo( STriInfo pTriInfos[], } // force otherwise healthy quads to a fixed orientation + int t = 0; while ( t < ( iNrTrianglesIn - 1 ) ) { const int iFO_a = pTriInfos[t].iOrgFaceNumber; const int iFO_b = pTriInfos[t + 1].iOrgFaceNumber; @@ -1105,10 +1101,10 @@ static int Build4RuleGroups( STriInfo pTriInfos[], const int iNrTrianglesIn ) { const int iNrMaxGroups = iNrTrianglesIn * 3; int iNrActiveGroups = 0; - int iOffset = 0, f = 0, i = 0; + int iOffset = 0; (void)iNrMaxGroups; /* quiet warnings in non debug mode */ - for ( f = 0; f < iNrTrianglesIn; f++ ) { - for ( i = 0; i < 3; i++ ) { + for ( int f = 0; f < iNrTrianglesIn; f++ ) { + for ( int i = 0; i < 3; i++ ) { // if not assigned to a group if ( ( pTriInfos[f].iFlag & GROUP_WITH_ANY ) == 0 && pTriInfos[f].AssignedGroup[i] == NULL ) { @@ -1242,8 +1238,10 @@ static tbool GenerateTSpaces( STSpace psTspace[], STSpace* pSubGroupTspace = NULL; SSubGroup* pUniSubGroups = NULL; int* pTmpMembers = NULL; - int iMaxNrFaces = 0, iUniqueTspaces = 0, g = 0, i = 0; - for ( g = 0; g < iNrActiveGroups; g++ ) + int iMaxNrFaces = 0; + // int iUniqueTspaces = 0; + + for ( int g = 0; g < iNrActiveGroups; g++ ) if ( iMaxNrFaces < pGroups[g].iNrFaces ) iMaxNrFaces = pGroups[g].iNrFaces; if ( iMaxNrFaces == 0 ) return TTRUE; @@ -1259,12 +1257,12 @@ static tbool GenerateTSpaces( STSpace psTspace[], return TFALSE; } - iUniqueTspaces = 0; - for ( g = 0; g < iNrActiveGroups; g++ ) { + // iUniqueTspaces = 0; + for ( int g = 0; g < iNrActiveGroups; g++ ) { const SGroup* pGroup = &pGroups[g]; int iUniqueSubGroups = 0, s = 0; - for ( i = 0; i < pGroup->iNrFaces; i++ ) // triangles + for ( int i = 0; i < pGroup->iNrFaces; i++ ) // triangles { const int f = pGroup->pFaceIndices[i]; // triangle number int index = -1, iVertIndex = -1, iOF_1 = -1, iMembers = 0, j = 0, l = 0; @@ -1348,9 +1346,8 @@ static tbool GenerateTSpaces( STSpace psTspace[], int* pIndices = (int*)malloc( sizeof( int ) * iMembers ); if ( pIndices == NULL ) { // clean up and return false - int s = 0; - for ( s = 0; s < iUniqueSubGroups; s++ ) - free( pUniSubGroups[s].pTriMembers ); + for ( int s_l = 0; s_l < iUniqueSubGroups; s_l++ ) + free( pUniSubGroups[s_l].pTriMembers ); free( pUniSubGroups ); free( pTmpMembers ); free( pSubGroupTspace ); @@ -1393,7 +1390,7 @@ static tbool GenerateTSpaces( STSpace psTspace[], // clean up and offset iUniqueTspaces for ( s = 0; s < iUniqueSubGroups; s++ ) free( pUniSubGroups[s].pTriMembers ); - iUniqueTspaces += iUniqueSubGroups; + // iUniqueTspaces += iUniqueSubGroups; } // clean up @@ -1554,9 +1551,9 @@ static void BuildNeighborsFast( STriInfo pTriInfos[], const int iNrTrianglesIn ) { // build array of edges unsigned int uSeed = INTERNAL_RND_SORT_SEED; // could replace with a random seed? - int iEntries = 0, iCurStartIndex = -1, f = 0, i = 0; - for ( f = 0; f < iNrTrianglesIn; f++ ) - for ( i = 0; i < 3; i++ ) { + int iEntries = 0, iCurStartIndex = -1; + for ( int f = 0; f < iNrTrianglesIn; f++ ) + for ( int i = 0; i < 3; i++ ) { const int i0 = piTriListIn[f * 3 + i]; const int i1 = piTriListIn[f * 3 + ( i < 2 ? ( i + 1 ) : 0 )]; pEdges[f * 3 + i].i0 = i0 < i1 ? i0 : i1; // put minimum index in i0 @@ -1572,7 +1569,7 @@ static void BuildNeighborsFast( STriInfo pTriInfos[], // with i0 as msb in the quicksort call above. iEntries = iNrTrianglesIn * 3; iCurStartIndex = 0; - for ( i = 1; i < iEntries; i++ ) { + for ( int i = 1; i < iEntries; i++ ) { if ( pEdges[iCurStartIndex].i0 != pEdges[i].i0 ) { const int iL = iCurStartIndex; const int iR = i - 1; @@ -1586,7 +1583,7 @@ static void BuildNeighborsFast( STriInfo pTriInfos[], // this step is to remain compliant with BuildNeighborsSlow() when // more than 2 triangles use the same edge (such as a butterfly topology). iCurStartIndex = 0; - for ( i = 1; i < iEntries; i++ ) { + for ( int i = 1; i < iEntries; i++ ) { if ( pEdges[iCurStartIndex].i0 != pEdges[i].i0 || pEdges[iCurStartIndex].i1 != pEdges[i].i1 ) { const int iL = iCurStartIndex; @@ -1598,7 +1595,7 @@ static void BuildNeighborsFast( STriInfo pTriInfos[], } // pair up, adjacent triangles - for ( i = 0; i < iEntries; i++ ) { + for ( int i = 0; i < iEntries; i++ ) { const int i0 = pEdges[i].i0; const int i1 = pEdges[i].i1; const int f = pEdges[i].f; @@ -1616,12 +1613,12 @@ static void BuildNeighborsFast( STriInfo pTriInfos[], if ( bUnassigned_A ) { // get true index ordering - int j = i + 1, t; + int j = i + 1; tbool bNotFound = TTRUE; while ( j < iEntries && i0 == pEdges[j].i0 && i1 == pEdges[j].i1 && bNotFound ) { tbool bUnassigned_B; int i0_B, i1_B; - t = pEdges[j].f; + int t = pEdges[j].f; // flip i0_B and i1_B GetEdge( &i1_B, &i0_B, @@ -1649,9 +1646,8 @@ static void BuildNeighborsFast( STriInfo pTriInfos[], static void BuildNeighborsSlow( STriInfo pTriInfos[], const int piTriListIn[], const int iNrTrianglesIn ) { - int f = 0, i = 0; - for ( f = 0; f < iNrTrianglesIn; f++ ) { - for ( i = 0; i < 3; i++ ) { + for ( int f = 0; f < iNrTrianglesIn; f++ ) { + for ( int i = 0; i < 3; i++ ) { // if unassigned if ( pTriInfos[f].FaceNeighbors[i] == -1 ) { const int i0_A = piTriListIn[f * 3 + i]; @@ -1816,11 +1812,11 @@ static void DegenPrologue( STriInfo pTriInfos[], // search for the first good triangle. tbool bJustADegenerate = TTRUE; while ( bJustADegenerate && iNextGoodTriangleSearchIndex < iTotTris ) { - const tbool bIsGood = + const tbool bIsGood_l = ( pTriInfos[iNextGoodTriangleSearchIndex].iFlag & MARK_DEGENERATE ) == 0 ? TTRUE : TFALSE; - if ( bIsGood ) + if ( bIsGood_l ) bJustADegenerate = TFALSE; else ++iNextGoodTriangleSearchIndex; @@ -1905,7 +1901,7 @@ static void DegenEpilogue( STSpace psTspace[], // other triangle is degenerate if ( ( pTriInfos[t].iFlag & QUAD_ONE_DEGEN_TRI ) != 0 ) { SVec3 vDstP; - int iOrgF = -1, i = 0; + int iOrgF = -1, i_l = 0; tbool bNotFound; unsigned char* pV = pTriInfos[t].vert_num; int iFlag = ( 1 << pV[0] ) | ( 1 << pV[1] ) | ( 1 << pV[2] ); @@ -1920,9 +1916,9 @@ static void DegenEpilogue( STSpace psTspace[], iOrgF = pTriInfos[t].iOrgFaceNumber; vDstP = GetPosition( pContext, MakeIndex( iOrgF, iMissingIndex ) ); bNotFound = TTRUE; - i = 0; - while ( bNotFound && i < 3 ) { - const int iVert = pV[i]; + i_l = 0; + while ( bNotFound && i_l < 3 ) { + const int iVert = pV[i_l]; const SVec3 vSrcP = GetPosition( pContext, MakeIndex( iOrgF, iVert ) ); if ( veq( vSrcP, vDstP ) == TTRUE ) { const int iOffs = pTriInfos[t].iTSpacesOffs; @@ -1930,7 +1926,7 @@ static void DegenEpilogue( STSpace psTspace[], bNotFound = TFALSE; } else - ++i; + ++i_l; } assert( !bNotFound ); } From 0d5abb879b9b09714b2a01bcd43f930f9b1131d9 Mon Sep 17 00:00:00 2001 From: Mathias Paulin Date: Tue, 29 Aug 2023 08:52:24 +0200 Subject: [PATCH 27/27] [engine] fix culling when material is singleSided --- src/Engine/Data/GLTFMaterial.hpp | 2 +- src/Engine/Data/Material.hpp | 5 +++++ src/Engine/Rendering/RenderObject.cpp | 4 ++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Engine/Data/GLTFMaterial.hpp b/src/Engine/Data/GLTFMaterial.hpp index de668d7322e..b59a70d54ac 100644 --- a/src/Engine/Data/GLTFMaterial.hpp +++ b/src/Engine/Data/GLTFMaterial.hpp @@ -302,7 +302,7 @@ class RA_ENGINE_API GLTFMaterial : public Material, public ParameterSetEditingIn float getAlphaCutoff() const { return m_alphaCutoff; } void setAlphaCutoff( float alphaCutoff ) { m_alphaCutoff = alphaCutoff; } - bool isDoubleSided() const { return m_doubleSided; } + bool isDoubleSided() const override { return m_doubleSided; } void setDoubleSided( bool doubleSided ) { m_doubleSided = doubleSided; } float getIndexOfRefraction() const { return m_indexOfRefraction; } diff --git a/src/Engine/Data/Material.hpp b/src/Engine/Data/Material.hpp index bc6b64bc262..ce260c9a622 100644 --- a/src/Engine/Data/Material.hpp +++ b/src/Engine/Data/Material.hpp @@ -83,6 +83,11 @@ class RA_ENGINE_API Material : public Data::ShaderParameterProvider */ virtual bool isTransparent() const; + /** Test if material is transperent. + * @return true if the material is transparent + */ + virtual bool isDoubleSided() const { return true; } + /** * Get the list of properties the material migh use in a shader. * each property will be added to the shader used for rendering this material under the form diff --git a/src/Engine/Rendering/RenderObject.cpp b/src/Engine/Rendering/RenderObject.cpp index 56bd674eae9..3b0daab98e9 100644 --- a/src/Engine/Rendering/RenderObject.cpp +++ b/src/Engine/Rendering/RenderObject.cpp @@ -285,7 +285,11 @@ void RenderObject::render( const Data::RenderParameters& lightParams, // Note that this hack implies the inclusion of OpenGL.h in this file if ( viewParams.viewMatrix.determinant() < 0 ) { glFrontFace( GL_CW ); } else { glFrontFace( GL_CCW ); } + // To enable correct culling when required (e.g. by gltf material) + GLboolean cullEnable = glIsEnabled( GL_CULL_FACE ); + if ( !m_material->isDoubleSided() ) { glEnable( GL_CULL_FACE ); } m_mesh->render( shader ); + if ( !cullEnable ) glDisable( GL_CULL_FACE ); } void RenderObject::render( const Data::RenderParameters& lightParams,