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") 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 00000000000..7e6c0db3583 Binary files /dev/null and b/Shaders/Materials/GLTF/BSDF_LUTs/lut_charlie.png differ 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 00000000000..40dd648eaa2 Binary files /dev/null and b/Shaders/Materials/GLTF/BSDF_LUTs/lut_ggx.png differ 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 00000000000..03ac3f22bde Binary files /dev/null and b/Shaders/Materials/GLTF/BSDF_LUTs/lut_sheen_E.png differ diff --git a/Shaders/Materials/GLTF/Materials/MetallicRoughness.glsl b/Shaders/Materials/GLTF/Materials/MetallicRoughness.glsl new file mode 100644 index 00000000000..419cbe6ff41 --- /dev/null +++ b/Shaders/Materials/GLTF/Materials/MetallicRoughness.glsl @@ -0,0 +1,108 @@ +#ifndef METALLICROUGHNESS_GLSL +#define METALLICROUGHNESS_GLSL +#include "baseGLTFMaterial.glsl" + +struct Material { + GLTFBaseMaterial baseMaterial; + vec4 baseColorFactor; + float metallicFactor; + float roughnessFactor; +#ifdef TEXTURE_BASECOLOR + sampler2D baseColor; +# ifdef TEXTURE_COORD_TRANSFORM_BASECOLOR + mat3 baseTransform; +# endif +#endif +#ifdef TEXTURE_METALLICROUGHNESS + sampler2D metallicRoughness; +# ifdef TEXTURE_COORD_TRANSFORM_METALLICROUGHNESS + mat3 metallicRoughnessTransform; +# endif +#endif +}; + +bool toDiscard( Material material, vec4 color ) { + return toDiscardBase( material.baseMaterial, color ); +} + +vec3 getEmissiveColor( Material material, vec3 textCoord ) { + return getEmissiveColorBase( material.baseMaterial, textCoord ); +} + +vec4 getBaseColor( Material material, vec3 texCoord ) { + vec4 basecolor = material.baseColorFactor; + +#ifdef TEXTURE_BASECOLOR + vec2 tc = texCoord.xy; +# ifdef TEXTURE_COORD_TRANSFORM_BASECOLOR + tc = ( material.baseTransform * vec3( tc, 1 ) ).xy; +# endif + basecolor *= texture( material.baseColor, tc ); +#endif +#ifdef HAS_PERVERTEX_COLOR + basecolor *= getPerVertexBaseColor(); +#endif + if ( material.baseMaterial.alphaMode == 1 && basecolor.a < material.baseMaterial.alphaCutoff ) { + basecolor.a = 0; + } + return basecolor; +} + +// Metallic and roughness are packed scalars +// Following GLTF specification, Roughness is stored in the 'g' channel, metallic is stored in the +// 'b' channel. The 'r' channel is ignored for MetallicRougness by gltf, we use it to store the +// alphaRoughness +vec3 getMetallicRoughnessFactors( Material material, vec2 tc ) { + vec3 mr = vec3( 0, material.roughnessFactor, material.metallicFactor ); +#ifdef TEXTURE_METALLICROUGHNESS +# ifdef TEXTURE_COORD_TRANSFORM_METALLICROUGHNESS + vec2 ct = ( material.metallicRoughnessTransform * vec3( tc, 1 ) ).xy; +# else + vec2 ct = tc; +# endif + vec3 mrsample = texture( material.metallicRoughness, ct ).rgb; + mr = mr * mrsample; +#endif + clamp( mr, vec3( 0, c_MinRoughness, 0 ), vec3( 0, 1, 1 ) ); + mr.r = mr.g * mr.g; + return mr; +} + +int extractBSDFParameters( Material material, vec3 tc, NormalInfo N, inout MaterialInfo params ) { + params.basecolor = getBaseColor( material, tc ).rgb; + params.f0 = vec3( dielectricSpecular( material.baseMaterial.ior ) ); + params.f90 = vec3( 1 ); // f90( wich color to use ?) + params.RoughnessMetalness = getMetallicRoughnessFactors( material, tc.xy ); + params.diffusebase = mix( params.basecolor, vec3( 0 ), params.RoughnessMetalness.b ); + params.f0 = mix( params.f0, params.basecolor, params.RoughnessMetalness.b ); + // In case an extension want to modify the material data ... + return extractBSDFParametersBase( material.baseMaterial, tc, N, params ); +} + +int getSeparateBSDFComponent( Material material, + vec3 tc, + vec3 V, + NormalInfo N, + inout MaterialInfo params, + out BsdfInfo layers ) { + if ( extractBSDFParameters( material, tc, N, params ) == 1 ) + { + return getSeparateBSDFComponentBase( + material.baseMaterial, params, N, V, layers ); + } + else + { return 0; } +} + +BsdfInfo evaluateBSDF( Material material, + MaterialInfo bsdf_params, + NormalInfo N, + vec3 wi, + vec3 wo, + vec3 light_intensity ) { + return evaluateBSDFBase( material.baseMaterial, bsdf_params, N, wi, wo, light_intensity ); +} + +uniform Material material; + +#endif // METALLICROUGHNESS_GLSL diff --git a/Shaders/Materials/GLTF/Materials/SpecularGlossiness.glsl b/Shaders/Materials/GLTF/Materials/SpecularGlossiness.glsl new file mode 100644 index 00000000000..fdc82f26235 --- /dev/null +++ b/Shaders/Materials/GLTF/Materials/SpecularGlossiness.glsl @@ -0,0 +1,104 @@ +#ifndef SPECULARGLOSSINESS_GLSL +#define SPECULARGLOSSINESS_GLSL +#include "baseGLTFMaterial.glsl" + +struct Material { + GLTFBaseMaterial baseMaterial; + vec4 diffuseFactor; + vec4 specularFactor; + float glossinessFactor; +#ifdef TEXTURE_DIFFUSE + sampler2D diffuse; +# ifdef TEXTURE_COORD_TRANSFORM_DIFFUSE + mat3 diffuseTransform; +# endif +#endif +#ifdef TEXTURE_SPECULARGLOSSINESS + sampler2D specularGlossiness; +# ifdef TEXTURE_COORD_TRANSFORM_SPECULARGLOSSINESS + mat3 specularGlossinessTransform; +# endif +#endif +}; + +bool toDiscard( Material material, vec4 color ) { + return toDiscardBase( material.baseMaterial, color ); +} + +vec3 getEmissiveColor( Material material, vec3 textCoord ) { + return getEmissiveColorBase( material.baseMaterial, textCoord ); +} + +vec4 getBaseColor( Material material, vec3 texCoord ) { + vec4 basecolor = material.diffuseFactor; +#ifdef TEXTURE_DIFFUSE +# ifdef TEXTURE_COORD_TRANSFORM_DIFFUSE + vec2 ct = ( material.diffuseTransform * vec3( texCoord.xy, 1 ) ).xy; +# else + vec2 ct = texCoord.xy; +# endif + basecolor *= texture( material.diffuse, ct ); +#endif +#ifdef HAS_PERVERTEX_COLOR + return basecolor * getPerVertexBaseColor(); +#else + return basecolor; +#endif +} + +// Specular and glossiness are packed scalars +// Following GLTF specification, Specular is stored in the 'rgb' channel, glossiness is stored in +// the 'a' channel. The 'a' channel of the returned color is the computed alphaRoughness of the BSDF +// model +vec4 getSpecularGlossinessFactor( Material material, vec2 tc ) { + vec4 specGloss = vec4( material.specularFactor.rgb, material.glossinessFactor ); +#ifdef TEXTURE_SPECULARGLOSSINESS +# ifdef TEXTURE_COORD_TRANSFORM_SPECULARGLOSSINESS + vec2 ct = ( material.specularGlossinessTransform * vec3( tc, 1 ) ).xy; +# else + vec2 ct = tc; +# endif + specGloss *= texture( material.specularGlossiness, ct ); +#endif + // convert glossinesss to the alpha roughness (alpha = (1-glossiness)^2 + specGloss.a = ( 1 - specGloss.a ); + clamp( specGloss.a, c_MinRoughness, 1 ); + return specGloss; +} + +int extractBSDFParameters( Material material, vec3 tc, NormalInfo N, inout MaterialInfo params ) { + params.basecolor = getBaseColor( material, tc ).rgb; + vec4 spec_gloss = getSpecularGlossinessFactor( material, tc.xy ); + params.f0 = spec_gloss.rgb; + params.f90 = vec3( 1 ); // f90( wich color to use ?) + params.RoughnessMetalness = vec3( spec_gloss.a * spec_gloss.a, spec_gloss.a, 0. ); + params.diffusebase = + params.basecolor * ( 1. - max( max( params.f0.r, params.f0.g ), params.f0.b ) ); + // In case an extension want to modify the material data ... + return extractBSDFParametersBase( material.baseMaterial, tc, N, params ); +} + +int getSeparateBSDFComponent( Material material, + vec3 tc, + vec3 V, + NormalInfo N, + inout MaterialInfo params, + out BsdfInfo layers ) { + if ( extractBSDFParameters( material, tc, N, params ) == 1 ) + { return getSeparateBSDFComponentBase( material.baseMaterial, params, N, V, layers ); } + else + { return 0; } +} + +BsdfInfo evaluateBSDF( Material material, + MaterialInfo bsdf_params, + NormalInfo N, + vec3 wi, + vec3 wo, + vec3 light_intensity ) { + return evaluateBSDFBase( material.baseMaterial, bsdf_params, N, wi, wo, light_intensity ); +} + +uniform Material material; + +#endif // SPECULARGLOSSINESS_GLSL diff --git a/Shaders/Materials/GLTF/Materials/baseGLTFMaterial.glsl b/Shaders/Materials/GLTF/Materials/baseGLTFMaterial.glsl new file mode 100644 index 00000000000..e81a2537561 --- /dev/null +++ b/Shaders/Materials/GLTF/Materials/baseGLTFMaterial.glsl @@ -0,0 +1,690 @@ +#ifndef GLTF_BASEMATERIAL_GLSL +#define GLTF_BASEMATERIAL_GLSL + +// new version of the GLTF material implementation +// ---------------------------------------------------- +// ---------------------------------------------------- +#include "VertexAttribInterface.frag.glsl" + +/* + * https://github.com/KhronosGroup/glTF-Sample-Viewer/tree/master/source/Renderer/shaders + * + * Partly update with specification of 12/2021 + * https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#appendix-b-brdf-implementation + */ + +/* + * Define constant, datastructure and operators used for all pbr materials of a GLTF scene + */ + +const float M_PI = 3.141592653589793; +const float InvPi = 0.31830988618379067154; + +const float local_epsilon = 1e-6; +const float c_MinRoughness = 0.001; + +float clampedDot( vec3 x, vec3 y ) { + // clamp to [local_epsilon, 1 ) ? + return clamp( dot( x, y ), 0.0, 1.0 ); +} + +vec3 F_Schlick( vec3 f0, vec3 f90, float VdotH ) { + return f0 + ( f90 - f0 ) * pow( clamp( 1.0 - VdotH, 0.0, 1.0 ), 5.0 ); +} + +float V_GGX( float NdotL, float NdotV, float alphaRoughness ) { + float alphaRoughnessSq = alphaRoughness * alphaRoughness; + + float GGXV = NdotL * sqrt( NdotV * NdotV * ( 1.0 - alphaRoughnessSq ) + alphaRoughnessSq ); + float GGXL = NdotV * sqrt( NdotL * NdotL * ( 1.0 - alphaRoughnessSq ) + alphaRoughnessSq ); + + float GGX = GGXV + GGXL; + if ( GGX > 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..e6156ae96fb --- /dev/null +++ b/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_LitOIT.frag.glsl @@ -0,0 +1,73 @@ +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() { +#ifdef MATERIAL_UNLIT + discard; +#else + 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 ); +#endif +} 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..bb552b2f815 --- /dev/null +++ b/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_LitOpaque.frag.glsl @@ -0,0 +1,50 @@ +// 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() { +// 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 ); + 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 ); +#endif +} 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..f326de83586 --- /dev/null +++ b/Shaders/Materials/GLTF/Materials/baseGLTFMaterial_Zprepass.frag.glsl @@ -0,0 +1,41 @@ +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); + 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; + + 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_diffuse = vec4(layers.f_diffuse, 1.0); + out_specular = vec4(layers.f_specular, 1.0); +#endif +} 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" + } +} 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..a0eeeed4d75 --- /dev/null +++ b/doc/concepts/gltfconformance.md @@ -0,0 +1,300 @@ +\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) + +* [GLTF 2.0 elements](#gltf_elements) + * [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](#extensions) + * [Supported extensions](#supported-extensions) + * [Unsupported extensions](#unsupported-extensions) + +## GLTF 2.0 elements {#gltf_elements} + +### 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. + +## Extensions {#extensions} + +glTF defines an extension mechanism that allows the base format to be extended with new capabilities. +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} + +* __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) + +* __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) 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 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] 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}" diff --git a/src/Core/Material/BaseGLTFMaterial.cpp b/src/Core/Material/BaseGLTFMaterial.cpp new file mode 100644 index 00000000000..ffb07d817eb --- /dev/null +++ b/src/Core/Material/BaseGLTFMaterial.cpp @@ -0,0 +1,14 @@ +#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 + 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..2e1714c3c17 --- /dev/null +++ b/src/Core/Material/BaseGLTFMaterial.hpp @@ -0,0 +1,195 @@ +#pragma once +#include + +#include +#include +#include + +#include +namespace Ra { +namespace Core { +namespace Material { + +/// \brief 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: + /// 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 }; + + /// 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 ); + } + + 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; } + + 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 {}; +}; + +/** + * \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 }; +}; + +/** + * \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/Core/Material/GLTFTextureParameters.hpp b/src/Core/Material/GLTFTextureParameters.hpp new file mode 100644 index 00000000000..b541569c3bc --- /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 as requested by gltf + */ +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..9233741120d --- /dev/null +++ b/src/Core/Material/MetallicRoughnessMaterialData.cpp @@ -0,0 +1,24 @@ +#include + +namespace Ra { +namespace Core { +namespace Material { + +MetallicRoughnessData::MetallicRoughnessData( const std::string& name ) : + BaseGLTFMaterial( { "MetallicRoughness" }, name ) { + // extension supported by MetallicRoughness gltf materials + 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"); + 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 diff --git a/src/Engine/Data/GLTFMaterial.cpp b/src/Engine/Data/GLTFMaterial.cpp new file mode 100644 index 00000000000..af955da5d5e --- /dev/null +++ b/src/Engine/Data/GLTFMaterial.cpp @@ -0,0 +1,659 @@ +#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 ); + // 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( + 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 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 + 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..b59a70d54ac --- /dev/null +++ b/src/Engine/Data/GLTFMaterial.hpp @@ -0,0 +1,566 @@ +#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 might 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; + + /** + * \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 + */ + 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 override { return m_doubleSided; } + void setDoubleSided( bool doubleSided ) { m_doubleSided = doubleSided; } + + 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: + 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 }; + + // Should the material be lit ? (manage by the extension KHR_materials_unlit), + bool m_isUnlit { false }; + + 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; + + bool m_isColoredByVertex { false }; + + 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/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/Data/MetallicRoughnessMaterial.cpp b/src/Engine/Data/MetallicRoughnessMaterial.cpp new file mode 100644 index 00000000000..bec4acdfcde --- /dev/null +++ b/src/Engine/Data/MetallicRoughnessMaterial.cpp @@ -0,0 +1,196 @@ +#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 ); +} + +/* + * 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 new file mode 100644 index 00000000000..115b5cecfef --- /dev/null +++ b/src/Engine/Data/MetallicRoughnessMaterial.hpp @@ -0,0 +1,110 @@ +#pragma once + +#include + +#include + +namespace Ra::Engine { +class Texture; +} // namespace Ra::Engine + +namespace Ra { +namespace Engine { +namespace Data { + +/** + * 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 + * + * 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/SpecularGlossinessMaterial.cpp b/src/Engine/Data/SpecularGlossinessMaterial.cpp new file mode 100644 index 00000000000..0c21e02ad66 --- /dev/null +++ b/src/Engine/Data/SpecularGlossinessMaterial.cpp @@ -0,0 +1,206 @@ +#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 ); +} + +/* + * 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 new file mode 100644 index 00000000000..a2f4e2ab222 --- /dev/null +++ b/src/Engine/Data/SpecularGlossinessMaterial.hpp @@ -0,0 +1,125 @@ +#pragma once +#include + +#include + +namespace Ra::Engine { +class Texture; +} // namespace Ra::Engine + +namespace Ra { +namespace Engine { +namespace Data { + +/** + * 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 : + * "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/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/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, 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 diff --git a/src/Engine/filelist.cmake b/src/Engine/filelist.cmake index 274f82178ba..8e6dd1a5596 100644 --- a/src/Engine/filelist.cmake +++ b/src/Engine/filelist.cmake @@ -8,10 +8,12 @@ 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/PlainMaterial.cpp Data/RawShaderMaterial.cpp Data/RenderParameters.cpp @@ -20,6 +22,7 @@ set(engine_sources Data/ShaderProgram.cpp Data/ShaderProgramManager.cpp Data/SimpleMaterial.cpp + Data/SpecularGlossinessMaterial.cpp Data/Texture.cpp Data/TextureManager.cpp Data/VolumeObject.cpp @@ -61,10 +64,12 @@ 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/PlainMaterial.hpp Data/RawShaderMaterial.hpp Data/RenderParameters.hpp @@ -73,6 +78,7 @@ set(engine_headers Data/ShaderProgram.hpp Data/ShaderProgramManager.hpp Data/SimpleMaterial.hpp + Data/SpecularGlossinessMaterial.hpp Data/Texture.hpp Data/TextureManager.hpp Data/ViewingParameters.hpp @@ -141,6 +147,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 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}) diff --git a/src/IO/CMakeLists.txt b/src/IO/CMakeLists.txt index 100f29c98c9..6ff33ed3d23 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) @@ -62,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() 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..9d85f83fee9 --- /dev/null +++ b/src/IO/Gltf/Loader/glTFFileLoader.cpp @@ -0,0 +1,88 @@ +#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; + } + + auto 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 ) << "glTFFileLoader::loadFile : Catch std::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..e6fd2601822 --- /dev/null +++ b/src/IO/Gltf/Writer/glTFFileWriter.cpp @@ -0,0 +1,681 @@ +#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 ( 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(); } + 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 ( 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(); } + 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; + } + 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; + 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..c2f28108c1c --- /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 { false }; + 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..f9b60e94b27 --- /dev/null +++ b/src/IO/Gltf/internal/Extensions/MaterialExtensions.hpp @@ -0,0 +1,344 @@ +#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 ); +} + +/** + * KHR_materials_unlit + * https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_unlit + */ +struct gltf_KHRMaterialsUnlit { + bool active { false }; + nlohmann::json extensionsAndExtras {}; +}; + +inline void from_json( nlohmann::json const& json, gltf_KHRMaterialsUnlit& khrMaterialsUnlit ) { + khrMaterialsUnlit.active = true; + fx::gltf::detail::ReadExtensionsAndExtras( json, khrMaterialsUnlit.extensionsAndExtras ); +} + +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 (gltfSupportedExtensions) in Converter.cpp + * 3. add the extension in the compatible extensions list in Core/Material/xxGLTFxx.cpp (for + * materials accepting this extension) + * 4. Define the Core/Material representation of the extension as inheriting from + * GLTFMaterialExtensionData + * 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 + * 7. Write the glsl management of the extension in the shaders + */ +} // 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..eaa6cdf8d36 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/AccessorReader.cpp @@ -0,0 +1,229 @@ +#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 = reinterpret_cast( 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 = 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]; + } + } +} + +template +uint8_t* normalizeData( uint8_t* data, uint32_t nbComponents ) { + T max = std::numeric_limits::max(); + 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 ); + } + 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; + 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 +} // 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..fb55a7d73d7 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/AccessorReader.hpp @@ -0,0 +1,48 @@ +#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. returns nullptr if + * accessorIndex is invalid + * + */ + 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..5ef5063f9f2 --- /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" }, + { "KHR_materials_unlit" } }; + +// 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 < 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 ); } + 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 : graphNode.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 = reinterpret_cast( accessorReader.read( sampler.input ) ); + auto transformation = reinterpret_cast( 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 ( auto sceneNode : gltfscene.scenes[activeScene].nodes ) { + glTfVisitor( gltfscene, sceneNode, rootTransform, graphNodes, visitedNodes ); + } + + int32_t nodeNum = 0; + + // build Radium representation of the scene elements + 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 Radium Skeletons + HandleDataLoader::IntToString nodeNumToComponentName; + 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 < int( gltfscene.animations.size() ) && + ( visitedNodes.find( + gltfscene.animations[activeAnimation].channels[0].target.node ) == + visitedNodes.end() ) ) { + ++activeAnimation; + } + // if animation found + if ( activeAnimation < int( 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..647d326afaa --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/HandleData.cpp @@ -0,0 +1,274 @@ +#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 == int32_t( 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 < 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]; } + } + 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() ); + 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], + 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 < int32_t( 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..59795248cb8 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/ImageData.hpp @@ -0,0 +1,74 @@ +#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 ) + "/" + + fx::gltf::detail::UriDecode( 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..e5dcee246a5 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/MaterialConverter.cpp @@ -0,0 +1,586 @@ +#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 +} + +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 ); +} + +// 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, + const MaterialData& meshMaterial, + BaseGLTFMaterial* mat, + 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 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 + // 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( except.begin(), except.end(), [k = key]( const auto& e ) { + return e == k; + } ) ) { + continue; + } + if ( mat->supportExtension( key ) ) { + mat->m_extensions[key] = + instantiateExtension[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 ) { + + 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 ); + } + } +} + +} // 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..9c9b7312ce4 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/MaterialConverter.hpp @@ -0,0 +1,71 @@ +#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; } + + /** + * 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; + 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; } + + 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..0d0c91e42af --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/MeshData.cpp @@ -0,0 +1,381 @@ +#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 ... +// takes care of interleaved buffers +template +void convertVectors( Vector3Array& vectors, + const uint8_t* data, + uint32_t count, + uint32_t stride = 0 ) { + for ( uint32_t i = 0; i < count; ++i ) { + 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 ); + } +} + +// 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 ) { + 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 ) { + 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 ); + } +} + +// 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 ) { + 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 ); + } +} + +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 ) { + 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 ); + } +} + +// 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 uint8_t* data, + uint32_t count, + uint32_t stride = 0 ) { + for ( uint32_t i = 0; i < count; ++i ) { + 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 ); + } +} + +// used to convert face indices +template +void convertIndices( Vector3uArray& indices, const uint8_t* data, uint32_t count ) { + for ( uint32_t i = 0; i < count; ++i ) { + 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 ) ); + } +} + +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 < int32_t( 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; + } + + // 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() ) { + 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, vBuffer.DataStride ); + 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 ) { + 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 < uint( 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 + const MeshData::BufferInfo& nBuffer = mesh.NormalBuffer(); + 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, nBuffer.DataStride ); + meshPart->getGeometry().vertexAttribs().unlock( attribHandle ); + } + else { + 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"; + 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, cBuffer.DataStride ); + break; + case gltf::Accessor::ComponentType::UnsignedShort: + convertTexCoord( + texcoords, cBuffer.Data, cBuffer.Accessor->count, cBuffer.DataStride ); + break; + case gltf::Accessor::ComponentType::Float: + convertTexCoord( + texcoords, cBuffer.Data, cBuffer.Accessor->count, cBuffer.DataStride ); + 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 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() ) { + 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, tBuffer.DataStride ); + 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..821cee9bc97 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/MeshData.hpp @@ -0,0 +1,324 @@ +#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.substr( 0, 9 ) == "TEXCOORD_" ) { + 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 ) { + 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 textcoord buffer i of the mesh. + */ + [[nodiscard]] const BufferInfo& TexCoordBuffer( int i ) const noexcept { + 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 + */ + [[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 ) { + 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 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 ) { + 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 !" + << static_cast( buf.Accessor->type ); + } + else { + switch ( buf.Accessor->componentType ) { + case fx::gltf::Accessor::ComponentType::Float: { + 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] } ); + } + break; + } + case fx::gltf::Accessor::ComponentType::UnsignedByte: { + 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, + 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: { + 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, + 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 {}; + // 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; + // spec require to support at least one color attribute + BufferInfo m_ColorBuffer {}; + + 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[bufferView.byteOffset + accessor.byteOffset], + bufferView.byteStride, + 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..cb846cb5819 --- /dev/null +++ b/src/IO/Gltf/internal/GLTFConverter/mikktspace.c @@ -0,0 +1,1934 @@ +/** \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_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_l; + } + 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 ) { + 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]; + + 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 ); + 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_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 ) { + { + 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 ) { + // pTriInfos[f].iFlag is cleared in GenerateInitialVerticesIndexList() which is called before + // this function. + + // generate neighbor info list + for ( int f = 0; f < iNrTrianglesIn; f++ ) + for ( int 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 ( 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] ); + 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 + int t = 0; + 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; + (void)iNrMaxGroups; /* quiet warnings in non debug mode */ + 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 ) { + 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; + // int iUniqueTspaces = 0; + + for ( int 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 ( int g = 0; g < iNrActiveGroups; g++ ) { + const SGroup* pGroup = &pGroups[g]; + int iUniqueSubGroups = 0, s = 0; + + 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; + 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 + for ( int s_l = 0; s_l < iUniqueSubGroups; s_l++ ) + free( pUniSubGroups[s_l].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; + 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 + 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 ( int 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 ( int 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 ( int 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; + tbool bNotFound = TTRUE; + while ( j < iEntries && i0 == pEdges[j].i0 && i1 == pEdges[j].i1 && bNotFound ) { + tbool bUnassigned_B; + int i0_B, i1_B; + int 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 ) { + 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]; + 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_l = + ( pTriInfos[iNextGoodTriangleSearchIndex].iFlag & MARK_DEGENERATE ) == 0 + ? TTRUE + : TFALSE; + if ( bIsGood_l ) + 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_l = 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_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; + psTspace[iOffs + iMissingIndex] = psTspace[iOffs + iVert]; + bNotFound = TFALSE; + } + else + ++i_l; + } + 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..447d7422a3b --- /dev/null +++ b/src/IO/Gltf/internal/fx/gltf.h @@ -0,0 +1,1742 @@ +// ------------------------------------------------------------ +// 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 { +// 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 ) +#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 + UriDecode( 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 ); + if ( fileData.gcount() != buffer.byteLength ) { + throw invalid_gltf_document( "Unable to read buffer", buffer.uri ); + } + } + } + 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)