diff --git a/Apps/Sandcastle/gallery/development/glTF PBR Anisotropy.html b/Apps/Sandcastle/gallery/development/glTF PBR Anisotropy.html new file mode 100644 index 000000000000..f4799989a865 --- /dev/null +++ b/Apps/Sandcastle/gallery/development/glTF PBR Anisotropy.html @@ -0,0 +1,400 @@ + + + + + + + + + Cesium Demo + + + + + +
+

Loading...

+
+ + + + + + + +
Luminance at Zenith + + +
+
+
+
+ + + diff --git a/CHANGES.md b/CHANGES.md index c838761c3414..213c9c87c1fc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ ###### Additions :tada: - Added support for glTF models with the [KHR_materials_specular extension](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_specular). [#11970](https://github.com/CesiumGS/cesium/pull/11970) +- Added support for glTF models with the [KHR_materials_anisotropy extension](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md). [#11988](https://github.com/CesiumGS/cesium/pull/11988) #### Fixes :wrench: @@ -14,6 +15,7 @@ - Fixed a bug where `TaskProcessor` worker loading would check the worker module ID rather than the absolute URL when determining if it is cross-origin. [#11833](https://github.com/CesiumGS/cesium/pull/11833) - Fixed a bug where cross-origin workers would error when loaded with the CommonJS `importScripts` shim instead of an ESM `import`. [#11833](https://github.com/CesiumGS/cesium/pull/11833) - Corrected the Typescript types for `Billboard.id` and `Label.id` to be `any` [#11973](https://github.com/CesiumGS/cesium/issues/11973) +- Fixed a normalization error in image-based lighting [#11994](https://github.com/CesiumGS/cesium/issues/11994) ### 1.117 - 2024-05-01 diff --git a/Specs/Data/Models/glTF-2.0/BoxAnisotropy/README.md b/Specs/Data/Models/glTF-2.0/BoxAnisotropy/README.md new file mode 100644 index 000000000000..0dc1fd980c9f --- /dev/null +++ b/Specs/Data/Models/glTF-2.0/BoxAnisotropy/README.md @@ -0,0 +1,11 @@ +# Box Anisotropy + +## Screenshot + +![screenshot](screenshot/screenshot.png) + +## License Information + +Developed by Cesium for testing the KHR_materials_anisotropy extension. Please follow the [Cesium Trademark Terms and Conditions](https://github.com/AnalyticalGraphicsInc/cesium/wiki/CesiumTrademark.pdf). + +This model is licensed under a [Creative Commons Attribution 4.0 International License](http://creativecommons.org/licenses/by/4.0/). diff --git a/Specs/Data/Models/glTF-2.0/BoxAnisotropy/glTF/BoxAnisotropy.bin b/Specs/Data/Models/glTF-2.0/BoxAnisotropy/glTF/BoxAnisotropy.bin new file mode 100644 index 000000000000..d2a73551f945 Binary files /dev/null and b/Specs/Data/Models/glTF-2.0/BoxAnisotropy/glTF/BoxAnisotropy.bin differ diff --git a/Specs/Data/Models/glTF-2.0/BoxAnisotropy/glTF/BoxAnisotropy.gltf b/Specs/Data/Models/glTF-2.0/BoxAnisotropy/glTF/BoxAnisotropy.gltf new file mode 100644 index 000000000000..19db9c8eef13 --- /dev/null +++ b/Specs/Data/Models/glTF-2.0/BoxAnisotropy/glTF/BoxAnisotropy.gltf @@ -0,0 +1,201 @@ +{ + "asset": { + "generator": "COLLADA2GLTF", + "version": "2.0" + }, + "scene": 0, + "scenes": [ + { + "nodes": [ + 0 + ] + } + ], + "nodes": [ + { + "children": [ + 1 + ], + "matrix": [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0 + ] + }, + { + "mesh": 0 + } + ], + "meshes": [ + { + "primitives": [ + { + "attributes": { + "NORMAL": 1, + "POSITION": 2, + "TEXCOORD_0": 3 + }, + "indices": 0, + "mode": 4, + "material": 0 + } + ], + "name": "Mesh" + } + ], + "accessors": [ + { + "bufferView": 0, + "byteOffset": 0, + "componentType": 5123, + "count": 36, + "max": [ + 23 + ], + "min": [ + 0 + ], + "type": "SCALAR" + }, + { + "bufferView": 1, + "byteOffset": 0, + "componentType": 5126, + "count": 24, + "max": [ + 1.0, + 1.0, + 1.0 + ], + "min": [ + -1.0, + -1.0, + -1.0 + ], + "type": "VEC3" + }, + { + "bufferView": 1, + "byteOffset": 288, + "componentType": 5126, + "count": 24, + "max": [ + 0.5, + 0.5, + 0.5 + ], + "min": [ + -0.5, + -0.5, + -0.5 + ], + "type": "VEC3" + }, + { + "bufferView": 2, + "byteOffset": 0, + "componentType": 5126, + "count": 24, + "max": [ + 6.0, + 1.0 + ], + "min": [ + 0.0, + 0.0 + ], + "type": "VEC2" + } + ], + "materials": [ + { + "pbrMetallicRoughness": { + "baseColorTexture": { + "index": 0 + }, + "metallicFactor": 0.0 + }, + "normalTexture": { + "index": 0 + }, + "name": "Texture", + "extensions": { + "KHR_materials_anisotropy": { + "anisotropyStrength": 0.5, + "anisotropyRotation": 0.349065850398866, + "anisotropyTexture": { + "index": 0 + } + } + } + } + ], + "textures": [ + { + "sampler": 0, + "source": 0 + } + ], + "images": [ + { + "uri": "CesiumLogoFlat.png" + } + ], + "samplers": [ + { + "magFilter": 9729, + "minFilter": 9986, + "wrapS": 10497, + "wrapT": 10497 + } + ], + "bufferViews": [ + { + "buffer": 0, + "byteOffset": 768, + "byteLength": 72, + "target": 34963 + }, + { + "buffer": 0, + "byteOffset": 0, + "byteLength": 576, + "byteStride": 12, + "target": 34962 + }, + { + "buffer": 0, + "byteOffset": 576, + "byteLength": 192, + "byteStride": 8, + "target": 34962 + } + ], + "buffers": [ + { + "byteLength": 840, + "uri": "BoxAnisotropy.bin" + } + ], + "extensionsRequired": [ + "KHR_draco_mesh_compression", + "KHR_materials_anisotropy" + ], + "extensionsUsed": [ + "KHR_draco_mesh_compression", + "KHR_materials_anisotropy" + ] +} diff --git a/Specs/Data/Models/glTF-2.0/BoxAnisotropy/glTF/CesiumLogoFlat.png b/Specs/Data/Models/glTF-2.0/BoxAnisotropy/glTF/CesiumLogoFlat.png new file mode 100644 index 000000000000..8159c4c4afd6 Binary files /dev/null and b/Specs/Data/Models/glTF-2.0/BoxAnisotropy/glTF/CesiumLogoFlat.png differ diff --git a/packages/engine/Source/Scene/GltfLoader.js b/packages/engine/Source/Scene/GltfLoader.js index 4b0433005e50..94a0f50b824c 100644 --- a/packages/engine/Source/Scene/GltfLoader.js +++ b/packages/engine/Source/Scene/GltfLoader.js @@ -54,6 +54,7 @@ const { MetallicRoughness, SpecularGlossiness, Specular, + Anisotropy, Material, } = ModelComponents; @@ -1565,6 +1566,27 @@ function loadSpecular(loader, specularInfo, frameState) { return specular; } +function loadAnisotropy(loader, anisotropyInfo, frameState) { + const { + anisotropyStrength = Anisotropy.DEFAULT_ANISOTROPY_STRENGTH, + anisotropyRotation = Anisotropy.DEFAULT_ANISOTROPY_ROTATION, + anisotropyTexture, + } = anisotropyInfo; + + const anisotropy = new Anisotropy(); + if (defined(anisotropyTexture)) { + anisotropy.anisotropyTexture = loadTexture( + loader, + anisotropyTexture, + frameState + ); + } + anisotropy.anisotropyStrength = anisotropyStrength; + anisotropy.anisotropyRotation = anisotropyRotation; + + return anisotropy; +} + /** * Load textures and parse factors and flags for a glTF material * @@ -1583,6 +1605,7 @@ function loadMaterial(loader, gltfMaterial, frameState) { ); const pbrSpecularGlossiness = extensions.KHR_materials_pbrSpecularGlossiness; const pbrSpecular = extensions.KHR_materials_specular; + const pbrAnisotropy = extensions.KHR_materials_anisotropy; const pbrMetallicRoughness = gltfMaterial.pbrMetallicRoughness; material.unlit = defined(extensions.KHR_materials_unlit); @@ -1601,9 +1624,12 @@ function loadMaterial(loader, gltfMaterial, frameState) { frameState ); } - if (defined(pbrSpecular)) { + if (defined(pbrSpecular) && !material.unlit) { material.specular = loadSpecular(loader, pbrSpecular, frameState); } + if (defined(pbrAnisotropy) && !material.unlit) { + material.anisotropy = loadAnisotropy(loader, pbrAnisotropy, frameState); + } } // Top level textures diff --git a/packages/engine/Source/Scene/Model/GeometryPipelineStage.js b/packages/engine/Source/Scene/Model/GeometryPipelineStage.js index ca753b6a01ef..28b769a07c96 100644 --- a/packages/engine/Source/Scene/Model/GeometryPipelineStage.js +++ b/packages/engine/Source/Scene/Model/GeometryPipelineStage.js @@ -63,8 +63,7 @@ GeometryPipelineStage.process = function ( primitive, frameState ) { - const shaderBuilder = renderResources.shaderBuilder; - const model = renderResources.model; + const { shaderBuilder, model } = renderResources; // These structs are similar, though the fragment shader version has a couple // additional fields. @@ -125,8 +124,7 @@ GeometryPipelineStage.process = function ( ); // .pnts point clouds store sRGB color rather than linear color - const modelType = model.type; - if (modelType === ModelType.TILE_PNTS) { + if (model.type === ModelType.TILE_PNTS) { shaderBuilder.addDefine( "HAS_SRGB_COLOR", undefined, @@ -246,8 +244,7 @@ function processAttribute( } function addSemanticDefine(shaderBuilder, attribute) { - const semantic = attribute.semantic; - const setIndex = attribute.setIndex; + const { semantic, setIndex } = attribute; switch (semantic) { case VertexAttributeSemantic.NORMAL: shaderBuilder.addDefine("HAS_NORMALS"); @@ -272,19 +269,11 @@ function addAttributeToRenderResources( attributeIndex, modifyFor2D ) { - const quantization = attribute.quantization; - let type; - let componentDatatype; - if (defined(quantization)) { - type = quantization.type; - componentDatatype = quantization.componentDatatype; - } else { - type = attribute.type; - componentDatatype = attribute.componentDatatype; - } + const { quantization, semantic, setIndex } = attribute; + const { type, componentDatatype } = defined(quantization) + ? quantization + : attribute; - const semantic = attribute.semantic; - const setIndex = attribute.setIndex; if ( semantic === VertexAttributeSemantic.FEATURE_ID && setIndex >= renderResources.featureIdVertexAttributeSetIndex @@ -337,18 +326,10 @@ function addMatrixAttributeToRenderResources( attributeIndex, columnCount ) { - const quantization = attribute.quantization; - let type; - let componentDatatype; - if (defined(quantization)) { - type = quantization.type; - componentDatatype = quantization.componentDatatype; - } else { - type = attribute.type; - componentDatatype = attribute.componentDatatype; - } - - const normalized = attribute.normalized; + const { quantization, normalized } = attribute; + const { type, componentDatatype } = defined(quantization) + ? quantization + : attribute; // componentCount is either 4, 9 or 16 const componentCount = AttributeType.getNumberOfComponents(type); @@ -435,7 +416,7 @@ function addAttributeDeclaration(shaderBuilder, attributeInfo, modifyFor2D) { function updateAttributesStruct(shaderBuilder, attributeInfo, use2D) { const vsStructId = GeometryPipelineStage.STRUCT_ID_PROCESSED_ATTRIBUTES_VS; const fsStructId = GeometryPipelineStage.STRUCT_ID_PROCESSED_ATTRIBUTES_FS; - const variableName = attributeInfo.variableName; + const { variableName, glslType } = attributeInfo; if (variableName === "tangentMC") { // The w component of the tangent is only used for computing the bitangent, @@ -451,16 +432,8 @@ function updateAttributesStruct(shaderBuilder, attributeInfo, use2D) { shaderBuilder.addStructField(vsStructId, "vec3", "normalMC"); shaderBuilder.addStructField(fsStructId, "vec3", "normalEC"); } else { - shaderBuilder.addStructField( - vsStructId, - attributeInfo.glslType, - variableName - ); - shaderBuilder.addStructField( - fsStructId, - attributeInfo.glslType, - variableName - ); + shaderBuilder.addStructField(vsStructId, glslType, variableName); + shaderBuilder.addStructField(fsStructId, glslType, variableName); } if (variableName === "positionMC" && use2D) { @@ -501,8 +474,7 @@ function updateInitializeAttributesFunction( } function updateSetDynamicVaryingsFunction(shaderBuilder, attributeInfo) { - const semantic = attributeInfo.attribute.semantic; - const setIndex = attributeInfo.attribute.setIndex; + const { semantic, setIndex } = attributeInfo.attribute; if (defined(semantic) && !defined(setIndex)) { // positions, normals, and tangents are handled statically in // GeometryStageVS diff --git a/packages/engine/Source/Scene/Model/MaterialPipelineStage.js b/packages/engine/Source/Scene/Model/MaterialPipelineStage.js index 0d8f04c2e4df..880a123e0fc4 100644 --- a/packages/engine/Source/Scene/Model/MaterialPipelineStage.js +++ b/packages/engine/Source/Scene/Model/MaterialPipelineStage.js @@ -63,20 +63,19 @@ MaterialPipelineStage.process = function ( // gltf-pipeline automatically creates a default material so this will always // be defined. const material = primitive.material; - const model = renderResources.model; + const { model, uniformMap, shaderBuilder } = renderResources; // Classification models only use position and feature ID attributes, // so textures should be disabled to avoid compile errors. const hasClassification = defined(model.classificationType); const disableTextures = hasClassification; - const uniformMap = renderResources.uniformMap; - const shaderBuilder = renderResources.shaderBuilder; - // When textures are loaded incrementally, fall back to a default 1x1 texture - const defaultTexture = frameState.context.defaultTexture; - const defaultNormalTexture = frameState.context.defaultNormalTexture; - const defaultEmissiveTexture = frameState.context.defaultEmissiveTexture; + const { + defaultTexture, + defaultNormalTexture, + defaultEmissiveTexture, + } = frameState.context; processMaterialUniforms( material, @@ -90,7 +89,7 @@ MaterialPipelineStage.process = function ( if (defined(material.specularGlossiness)) { processSpecularGlossinessUniforms( - material, + material.specularGlossiness, uniformMap, shaderBuilder, defaultTexture, @@ -102,7 +101,19 @@ MaterialPipelineStage.process = function ( ModelUtility.supportedExtensions.KHR_materials_specular ) { processSpecularUniforms( - material, + material.specular, + uniformMap, + shaderBuilder, + defaultTexture, + disableTextures + ); + } + if ( + defined(material.anisotropy) && + ModelUtility.supportedExtensions.KHR_materials_anisotropy + ) { + processAnisotropyUniforms( + material.anisotropy, uniformMap, shaderBuilder, defaultTexture, @@ -110,7 +121,7 @@ MaterialPipelineStage.process = function ( ); } processMetallicRoughnessUniforms( - material, + material.metallicRoughness, uniformMap, shaderBuilder, defaultTexture, @@ -321,14 +332,23 @@ function processMaterialUniforms( } } +/** + * Add uniforms and defines for the KHR_materials_pbrSpecularGlossiness extension + * + * @param {ModelComponents.SpecularGlossiness} specularGlossiness + * @param {Object} uniformMap The uniform map to modify. + * @param {ShaderBuilder} shaderBuilder + * @param {Texture} defaultTexture + * @param {boolean} disableTextures + * @private + */ function processSpecularGlossinessUniforms( - material, + specularGlossiness, uniformMap, shaderBuilder, defaultTexture, disableTextures ) { - const { specularGlossiness } = material; const { diffuseTexture, diffuseFactor, @@ -426,14 +446,23 @@ function processSpecularGlossinessUniforms( } } +/** + * Add uniforms and defines for the KHR_materials_specular extension + * + * @param {ModelComponents.Specular} specular + * @param {Object} uniformMap The uniform map to modify. + * @param {ShaderBuilder} shaderBuilder + * @param {Texture} defaultTexture + * @param {boolean} disableTextures + * @private + */ function processSpecularUniforms( - material, + specular, uniformMap, shaderBuilder, defaultTexture, disableTextures ) { - const { specular } = material; const { specularTexture, specularFactor, @@ -511,14 +540,80 @@ function processSpecularUniforms( } } +const scratchAnisotropy = new Cartesian3(); + +/** + * Add uniforms and defines for the KHR_materials_anisotropy extension + * + * @param {ModelComponents.Anisotropy} anisotropy + * @param {Object} uniformMap The uniform map to modify. + * @param {ShaderBuilder} shaderBuilder + * @param {Texture} defaultTexture + * @param {boolean} disableTextures + * @private + */ +function processAnisotropyUniforms( + anisotropy, + uniformMap, + shaderBuilder, + defaultTexture, + disableTextures +) { + const { + anisotropyStrength, + anisotropyRotation, + anisotropyTexture, + } = anisotropy; + + shaderBuilder.addDefine( + "USE_ANISOTROPY", + undefined, + ShaderDestination.FRAGMENT + ); + + if (defined(anisotropyTexture) && !disableTextures) { + processTexture( + shaderBuilder, + uniformMap, + anisotropyTexture, + "u_anisotropyTexture", + "ANISOTROPY", + defaultTexture + ); + } + + // Pre-compute cos and sin of rotation, since they are the same for all fragments. + // Combine with strength as one vec3 uniform. + const cosRotation = Math.cos(anisotropyRotation); + const sinRotation = Math.sin(anisotropyRotation); + shaderBuilder.addUniform("vec3", "u_anisotropy", ShaderDestination.FRAGMENT); + uniformMap.u_anisotropy = function () { + return Cartesian3.fromElements( + cosRotation, + sinRotation, + anisotropyStrength, + scratchAnisotropy + ); + }; +} + +/** + * Add uniforms and defines for the PBR metallic roughness model + * + * @param {ModelComponents.MetallicRoughness} metallicRoughness + * @param {Object} uniformMap The uniform map to modify. + * @param {ShaderBuilder} shaderBuilder + * @param {Texture} defaultTexture + * @param {boolean} disableTextures + * @private + */ function processMetallicRoughnessUniforms( - material, + metallicRoughness, uniformMap, shaderBuilder, defaultTexture, disableTextures ) { - const metallicRoughness = material.metallicRoughness; shaderBuilder.addDefine( "USE_METALLIC_ROUGHNESS", undefined, diff --git a/packages/engine/Source/Scene/Model/ModelUtility.js b/packages/engine/Source/Scene/Model/ModelUtility.js index 90bb54a51094..5c0089da25c7 100644 --- a/packages/engine/Source/Scene/Model/ModelUtility.js +++ b/packages/engine/Source/Scene/Model/ModelUtility.js @@ -358,6 +358,7 @@ ModelUtility.supportedExtensions = { KHR_materials_common: true, KHR_materials_pbrSpecularGlossiness: true, KHR_materials_specular: true, + KHR_materials_anisotropy: true, KHR_materials_unlit: true, KHR_mesh_quantization: true, KHR_texture_basisu: true, diff --git a/packages/engine/Source/Scene/ModelComponents.js b/packages/engine/Source/Scene/ModelComponents.js index 4cb9f75bc3d0..6eaec0918c2d 100644 --- a/packages/engine/Source/Scene/ModelComponents.js +++ b/packages/engine/Source/Scene/ModelComponents.js @@ -1385,6 +1385,45 @@ Specular.DEFAULT_SPECULAR_FACTOR = 1.0; */ Specular.DEFAULT_SPECULAR_COLOR_FACTOR = Cartesian3.ONE; +function Anisotropy() { + /** + * The anisotropy strength. + * + * @type {number} + * @default 0.0 + * @private + */ + this.anisotropyStrength = Anisotropy.DEFAULT_ANISOTROPY_STRENGTH; + + /** + * The rotation of the anisotropy in tangent, bitangent space, + * measured in radians counter-clockwise from the tangent. + * + * @type {number} + * @default 0.0 + * @private + */ + this.anisotropyRotation = Anisotropy.DEFAULT_ANISOTROPY_ROTATION; + + /** + * The anisotropy texture reader + * + * @type {ModelComponents.TextureReader} + * @private + */ + this.anisotropyTexture = undefined; +} + +/** + * @private + */ +Anisotropy.DEFAULT_ANISOTROPY_STRENGTH = 0.0; + +/** + * @private + */ +Anisotropy.DEFAULT_ANISOTROPY_ROTATION = 0.0; + /** * The material appearance of a primitive. * @@ -1418,6 +1457,14 @@ function Material() { */ this.specular = undefined; + /** + * Material properties for the PBR anisotropy shading model + * + * @type {ModelComponents.anisotropy} + * @private + */ + this.anisotropy = undefined; + /** * The emissive texture reader. * @@ -1518,6 +1565,7 @@ ModelComponents.TextureReader = TextureReader; ModelComponents.MetallicRoughness = MetallicRoughness; ModelComponents.SpecularGlossiness = SpecularGlossiness; ModelComponents.Specular = Specular; +ModelComponents.Anisotropy = Anisotropy; ModelComponents.Material = Material; export default ModelComponents; diff --git a/packages/engine/Source/Shaders/Builtin/Functions/pbrLighting.glsl b/packages/engine/Source/Shaders/Builtin/Functions/pbrLighting.glsl index 8fc541c07ceb..467423b1cbd8 100644 --- a/packages/engine/Source/Shaders/Builtin/Functions/pbrLighting.glsl +++ b/packages/engine/Source/Shaders/Builtin/Functions/pbrLighting.glsl @@ -11,6 +11,35 @@ vec3 fresnelSchlick2(vec3 f0, vec3 f90, float VdotH) return f0 + (f90 - f0) * versineSquared * versineSquared * versine; } +#ifdef USE_ANISOTROPY +/** + * @param {float} roughness Material roughness (along the anisotropy bitangent) + * @param {float} tangentialRoughness Anisotropic roughness (along the anisotropy tangent) + * @param {vec3} lightDirection The direction from the fragment to the light source, transformed to tangent-bitangent-normal coordinates + * @param {vec3} viewDirection The direction from the fragment to the camera, transformed to tangent-bitangent-normal coordinates + */ +float smithVisibilityGGX_anisotropic(float roughness, float tangentialRoughness, vec3 lightDirection, vec3 viewDirection) +{ + vec3 roughnessScale = vec3(tangentialRoughness, roughness, 1.0); + float GGXV = lightDirection.z * length(roughnessScale * viewDirection); + float GGXL = viewDirection.z * length(roughnessScale * lightDirection); + float v = 0.5 / (GGXV + GGXL); + return clamp(v, 0.0, 1.0); +} + +/** + * @param {float} roughness Material roughness (along the anisotropy bitangent) + * @param {float} tangentialRoughness Anisotropic roughness (along the anisotropy tangent) + * @param {vec3} halfwayDirection The unit vector halfway between light and view directions, transformed to tangent-bitangent-normal coordinates + */ +float GGX_anisotropic(float roughness, float tangentialRoughness, vec3 halfwayDirection) +{ + float roughnessSquared = roughness * tangentialRoughness; + vec3 f = halfwayDirection * vec3(roughness, tangentialRoughness, roughnessSquared); + float w2 = roughnessSquared / dot(f, f); + return roughnessSquared * w2 * w2 / czm_pi; +} +#else float smithVisibilityG1(float NdotV, float roughness) { // this is the k value for direct lighting. @@ -24,7 +53,7 @@ float smithVisibilityGGX(float roughness, float NdotL, float NdotV) return ( smithVisibilityG1(NdotL, roughness) * smithVisibilityG1(NdotV, roughness) - ); + ) / (4.0 * NdotL * NdotV); } float GGX(float roughness, float NdotH) @@ -33,6 +62,7 @@ float GGX(float roughness, float NdotH) float f = (NdotH * roughnessSquared - NdotH) * NdotH + 1.0; return roughnessSquared / (czm_pi * f * f); } +#endif /** * Compute the diffuse and specular contributions using physically based @@ -77,9 +107,6 @@ vec3 czm_pbrLighting( vec3 l = normalize(lightDirectionEC); vec3 h = normalize(v + l); vec3 n = normalEC; - float NdotL = clamp(dot(n, l), 0.001, 1.0); - float NdotV = abs(dot(n, v)) + 0.001; - float NdotH = clamp(dot(n, h), 0.0, 1.0); float VdotH = clamp(dot(v, h), 0.0, 1.0); vec3 f0 = pbrParameters.f0; @@ -90,13 +117,29 @@ vec3 czm_pbrLighting( vec3 F = fresnelSchlick2(f0, f90, VdotH); #if defined(USE_SPECULAR) - F *= pbrParameters.specularWeight; + F *= pbrParameters.specularWeight; #endif float alpha = pbrParameters.roughness; - float G = smithVisibilityGGX(alpha, NdotL, NdotV); - float D = GGX(alpha, NdotH); - vec3 specularContribution = F * G * D / (4.0 * NdotL * NdotV); + #ifdef USE_ANISOTROPY + mat3 tbn = mat3(pbrParameters.anisotropicT, pbrParameters.anisotropicB, n); + vec3 lightDirection = l * tbn; + vec3 viewDirection = v * tbn; + vec3 halfwayDirection = h * tbn; + float anisotropyStrength = pbrParameters.anisotropyStrength; + float tangentialRoughness = mix(alpha, 1.0, anisotropyStrength * anisotropyStrength); + float G = smithVisibilityGGX_anisotropic(alpha, tangentialRoughness, lightDirection, viewDirection); + float D = GGX_anisotropic(alpha, tangentialRoughness, halfwayDirection); + float NdotL = clamp(lightDirection.z, 0.001, 1.0); + #else + float NdotL = clamp(dot(n, l), 0.001, 1.0); + float NdotV = abs(dot(n, v)) + 0.001; + float NdotH = clamp(dot(n, h), 0.0, 1.0); + float G = smithVisibilityGGX(alpha, NdotL, NdotV); + float D = GGX(alpha, NdotH); + #endif + + vec3 specularContribution = F * G * D; vec3 diffuseColor = pbrParameters.diffuseColor; // F here represents the specular contribution diff --git a/packages/engine/Source/Shaders/Builtin/Structs/modelMaterial.glsl b/packages/engine/Source/Shaders/Builtin/Structs/modelMaterial.glsl index ff7f67c91e83..0a61b46b1b8e 100644 --- a/packages/engine/Source/Shaders/Builtin/Structs/modelMaterial.glsl +++ b/packages/engine/Source/Shaders/Builtin/Structs/modelMaterial.glsl @@ -26,7 +26,12 @@ struct czm_modelMaterial { vec3 normalEC; float occlusion; vec3 emissive; -#if defined(USE_SPECULAR) +#ifdef USE_SPECULAR float specularWeight; #endif +#ifdef USE_ANISOTROPY + vec3 anisotropicT; + vec3 anisotropicB; + float anisotropyStrength; +#endif }; diff --git a/packages/engine/Source/Shaders/Builtin/Structs/pbrParameters.glsl b/packages/engine/Source/Shaders/Builtin/Structs/pbrParameters.glsl index 9039b0b48252..31ed199d9133 100644 --- a/packages/engine/Source/Shaders/Builtin/Structs/pbrParameters.glsl +++ b/packages/engine/Source/Shaders/Builtin/Structs/pbrParameters.glsl @@ -13,7 +13,12 @@ struct czm_pbrParameters vec3 diffuseColor; float roughness; vec3 f0; -#if defined(USE_SPECULAR) +#ifdef USE_SPECULAR float specularWeight; #endif +#ifdef USE_ANISOTROPY + vec3 anisotropicT; + vec3 anisotropicB; + float anisotropyStrength; +#endif }; diff --git a/packages/engine/Source/Shaders/Model/ImageBasedLightingStageFS.glsl b/packages/engine/Source/Shaders/Model/ImageBasedLightingStageFS.glsl index f01a35dba6f6..e50d3eb45f8a 100644 --- a/packages/engine/Source/Shaders/Model/ImageBasedLightingStageFS.glsl +++ b/packages/engine/Source/Shaders/Model/ImageBasedLightingStageFS.glsl @@ -5,7 +5,7 @@ vec3 proceduralIBL( vec3 lightColorHdr, czm_pbrParameters pbrParameters ) { - vec3 v = -positionEC; + vec3 v = -normalize(positionEC); vec3 positionWC = vec3(czm_inverseView * vec4(positionEC, 1.0)); vec3 vWC = -normalize(positionWC); vec3 l = normalize(lightDirectionEC); @@ -99,29 +99,36 @@ vec3 computeDiffuseIBL(czm_pbrParameters pbrParameters, vec3 cubeDir) #endif #ifdef SPECULAR_IBL +vec3 sampleSpecularEnvironment(vec3 cubeDir, float roughness) +{ + #ifdef CUSTOM_SPECULAR_IBL + float maxLod = model_specularEnvironmentMapsMaximumLOD; + float lod = roughness * maxLod; + return czm_sampleOctahedralProjection(model_specularEnvironmentMaps, model_specularEnvironmentMapsSize, cubeDir, lod, maxLod); + #else + float maxLod = czm_specularEnvironmentMapsMaximumLOD; + float lod = roughness * maxLod; + return czm_sampleOctahedralProjection(czm_specularEnvironmentMaps, czm_specularEnvironmentMapSize, cubeDir, lod, maxLod); + #endif +} vec3 computeSpecularIBL(czm_pbrParameters pbrParameters, vec3 cubeDir, float NdotV, float VdotH) { float roughness = pbrParameters.roughness; - vec3 specularColor = pbrParameters.f0; + vec3 f0 = pbrParameters.f0; - vec3 r0 = specularColor.rgb; - float reflectance = max(max(r0.r, r0.g), r0.b); - vec3 r90 = vec3(clamp(reflectance * 25.0, 0.0, 1.0)); - vec3 F = fresnelSchlick2(r0, r90, VdotH); + float reflectance = max(max(f0.r, f0.g), f0.b); + vec3 f90 = vec3(clamp(reflectance * 25.0, 0.0, 1.0)); + vec3 F = fresnelSchlick2(f0, f90, VdotH); vec2 brdfLut = texture(czm_brdfLut, vec2(NdotV, roughness)).rg; - #ifdef CUSTOM_SPECULAR_IBL - vec3 specularIBL = czm_sampleOctahedralProjection(model_specularEnvironmentMaps, model_specularEnvironmentMapsSize, cubeDir, roughness * model_specularEnvironmentMapsMaximumLOD, model_specularEnvironmentMapsMaximumLOD); - #else - vec3 specularIBL = czm_sampleOctahedralProjection(czm_specularEnvironmentMaps, czm_specularEnvironmentMapSize, cubeDir, roughness * czm_specularEnvironmentMapsMaximumLOD, czm_specularEnvironmentMapsMaximumLOD); - #endif + vec3 specularIBL = sampleSpecularEnvironment(cubeDir, roughness); specularIBL *= F * brdfLut.x + brdfLut.y; #ifdef USE_SPECULAR specularIBL *= pbrParameters.specularWeight; #endif - return specularColor * specularIBL; + return f0 * specularIBL; } #endif @@ -132,7 +139,7 @@ vec3 textureIBL( vec3 lightDirectionEC, czm_pbrParameters pbrParameters ) { - vec3 v = -positionEC; + vec3 v = -normalize(positionEC); vec3 n = normalEC; vec3 l = normalize(lightDirectionEC); vec3 h = normalize(v + l); @@ -140,12 +147,14 @@ vec3 textureIBL( float NdotV = abs(dot(n, v)) + 0.001; float VdotH = clamp(dot(v, h), 0.0, 1.0); + // Find the direction in which to sample the environment map const mat3 yUpToZUp = mat3( -1.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.0, 1.0, 0.0 - ); - vec3 cubeDir = normalize(yUpToZUp * model_iblReferenceFrameMatrix * normalize(reflect(-v, n))); + ); + mat3 cubeDirTransform = yUpToZUp * model_iblReferenceFrameMatrix; + vec3 cubeDir = normalize(cubeDirTransform * normalize(reflect(-v, n))); #ifdef DIFFUSE_IBL vec3 diffuseContribution = computeDiffuseIBL(pbrParameters, cubeDir); @@ -153,6 +162,20 @@ vec3 textureIBL( vec3 diffuseContribution = vec3(0.0); #endif + #ifdef USE_ANISOTROPY + // Update environment map sampling direction to account for anisotropic distortion of specular reflection + float roughness = pbrParameters.roughness; + vec3 anisotropyDirection = pbrParameters.anisotropicB; + float anisotropyStrength = pbrParameters.anisotropyStrength; + + vec3 anisotropicTangent = cross(anisotropyDirection, v); + vec3 anisotropicNormal = cross(anisotropicTangent, anisotropyDirection); + float bendFactor = 1.0 - anisotropyStrength * (1.0 - roughness); + float bendFactorPow4 = bendFactor * bendFactor * bendFactor * bendFactor; + vec3 bentNormal = normalize(mix(anisotropicNormal, n, bendFactorPow4)); + cubeDir = normalize(cubeDirTransform * normalize(reflect(-v, bentNormal))); + #endif + #ifdef SPECULAR_IBL vec3 specularContribution = computeSpecularIBL(pbrParameters, cubeDir, NdotV, VdotH); #else diff --git a/packages/engine/Source/Shaders/Model/LightingStageFS.glsl b/packages/engine/Source/Shaders/Model/LightingStageFS.glsl index f34fcfde6eeb..49b07b660724 100644 --- a/packages/engine/Source/Shaders/Model/LightingStageFS.glsl +++ b/packages/engine/Source/Shaders/Model/LightingStageFS.glsl @@ -8,6 +8,11 @@ vec3 computePbrLighting(czm_modelMaterial inputMaterial, ProcessedAttributes att #ifdef USE_SPECULAR pbrParameters.specularWeight = inputMaterial.specularWeight; #endif + #ifdef USE_ANISOTROPY + pbrParameters.anisotropicT = inputMaterial.anisotropicT; + pbrParameters.anisotropicB = inputMaterial.anisotropicB; + pbrParameters.anisotropyStrength = inputMaterial.anisotropyStrength; + #endif #ifdef USE_CUSTOM_LIGHT_COLOR vec3 lightColorHdr = model_lightColorHdr; diff --git a/packages/engine/Source/Shaders/Model/MaterialStageFS.glsl b/packages/engine/Source/Shaders/Model/MaterialStageFS.glsl index 0b8f4d1833c7..ed32c917d08c 100644 --- a/packages/engine/Source/Shaders/Model/MaterialStageFS.glsl +++ b/packages/engine/Source/Shaders/Model/MaterialStageFS.glsl @@ -16,36 +16,96 @@ vec2 computeTextureTransform(vec2 texCoord, mat3 textureTransform) return vec2(textureTransform * vec3(texCoord, 1.0)); } -#if defined(HAS_NORMAL_TEXTURE) && !defined(HAS_WIREFRAME) -vec3 getNormalFromTexture(ProcessedAttributes attributes, vec3 geometryNormal) +#ifdef HAS_NORMAL_TEXTURE +vec2 getNormalTexCoords() { - vec2 normalTexCoords = TEXCOORD_NORMAL; + vec2 texCoord = TEXCOORD_NORMAL; #ifdef HAS_NORMAL_TEXTURE_TRANSFORM - normalTexCoords = computeTextureTransform(normalTexCoords, u_normalTextureTransform); + texCoord = vec2(u_normalTextureTransform * vec3(texCoord, 1.0)); + #endif + return texCoord; +} + +vec3 computeTangent(in vec3 position, in vec2 normalTexCoords) +{ + vec2 tex_dx = dFdx(normalTexCoords); + vec2 tex_dy = dFdy(normalTexCoords); + float determinant = tex_dx.x * tex_dy.y - tex_dy.x * tex_dx.y; + vec3 tangent = tex_dy.t * dFdx(position) - tex_dx.t * dFdy(position); + return tangent / determinant; +} +#endif + +#ifdef USE_ANISOTROPY +struct NormalInfo { + vec3 tangent; + vec3 bitangent; + vec3 normal; + vec3 geometryNormal; +}; + +NormalInfo getNormalInfo(ProcessedAttributes attributes) +{ + vec3 geometryNormal = attributes.normalEC; + #ifdef HAS_NORMAL_TEXTURE + vec2 normalTexCoords = getNormalTexCoords(); #endif + #ifdef HAS_BITANGENTS + vec3 tangent = attributes.tangentEC; + vec3 bitangent = attributes.bitangentEC; + #else // Assume HAS_NORMAL_TEXTURE + vec3 tangent = computeTangent(attributes.positionEC, normalTexCoords); + tangent = normalize(tangent - geometryNormal * dot(geometryNormal, tangent)); + vec3 bitangent = normalize(cross(geometryNormal, tangent)); + #endif + + #ifdef HAS_NORMAL_TEXTURE + mat3 tbn = mat3(tangent, bitangent, geometryNormal); + vec3 normalSample = texture(u_normalTexture, normalTexCoords).rgb; + vec3 normal = normalize(tbn * (2.0 * normalSample - 1.0)); + #else + vec3 normal = geometryNormal; + #endif + + #ifdef HAS_DOUBLE_SIDED_MATERIAL + if (czm_backFacing()) { + tangent *= -1.0; + bitangent *= -1.0; + normal *= -1.0; + geometryNormal *= -1.0; + } + #endif + + NormalInfo normalInfo; + normalInfo.tangent = tangent; + normalInfo.bitangent = bitangent; + normalInfo.normal = normal; + normalInfo.geometryNormal = geometryNormal; + + return normalInfo; +} +#endif + +#if defined(HAS_NORMAL_TEXTURE) && !defined(HAS_WIREFRAME) +vec3 getNormalFromTexture(ProcessedAttributes attributes, vec3 geometryNormal) +{ + vec2 normalTexCoords = getNormalTexCoords(); + // If HAS_BITANGENTS is set, then HAS_TANGENTS is also set #ifdef HAS_BITANGENTS vec3 t = attributes.tangentEC; vec3 b = attributes.bitangentEC; - mat3 tbn = mat3(t, b, geometryNormal); - vec3 n = texture(u_normalTexture, normalTexCoords).rgb; - vec3 textureNormal = normalize(tbn * (2.0 * n - 1.0)); - #elif (__VERSION__ == 300 || defined(GL_OES_standard_derivatives)) - // If derivatives are available (not IE 10), compute tangents - vec3 positionEC = attributes.positionEC; - vec3 pos_dx = dFdx(positionEC); - vec3 pos_dy = dFdy(positionEC); - vec3 tex_dx = dFdx(vec3(normalTexCoords,0.0)); - vec3 tex_dy = dFdy(vec3(normalTexCoords,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); + #else + vec3 t = computeTangent(attributes.positionEC, normalTexCoords); t = normalize(t - geometryNormal * dot(geometryNormal, t)); vec3 b = normalize(cross(geometryNormal, t)); - mat3 tbn = mat3(t, b, geometryNormal); - vec3 n = texture(u_normalTexture, normalTexCoords).rgb; - vec3 textureNormal = normalize(tbn * (2.0 * n - 1.0)); #endif + mat3 tbn = mat3(t, b, geometryNormal); + vec3 n = texture(u_normalTexture, normalTexCoords).rgb; + vec3 textureNormal = normalize(tbn * (2.0 * n - 1.0)); + return textureNormal; } #endif @@ -74,7 +134,6 @@ vec3 computeNormal(ProcessedAttributes attributes) vec4 getBaseColorFromTexture() { vec2 baseColorTexCoords = TEXCOORD_BASE_COLOR; - #ifdef HAS_BASE_COLOR_TEXTURE_TRANSFORM baseColorTexCoords = computeTextureTransform(baseColorTexCoords, u_baseColorTextureTransform); #endif @@ -209,7 +268,7 @@ float setMetallicRoughness(inout czm_modelMaterial material) return metalness; } -#if defined(USE_SPECULAR) +#ifdef USE_SPECULAR void setSpecular(inout czm_modelMaterial material, in float metalness) { #ifdef HAS_SPECULAR_TEXTURE @@ -234,7 +293,8 @@ void setSpecular(inout czm_modelMaterial material, in float metalness) #ifdef HAS_SPECULAR_COLOR_TEXTURE_TRANSFORM specularColorTexCoords = computeTextureTransform(specularColorTexCoords, u_specularColorTextureTransform); #endif - vec3 specularColorFactor = texture(u_specularColorTexture, specularColorTexCoords).rgb; + vec3 specularColorSample = texture(u_specularColorTexture, specularColorTexCoords).rgb; + vec3 specularColorFactor = czm_srgbToLinear(specularColorSample); #ifdef HAS_SPECULAR_COLOR_FACTOR specularColorFactor *= u_specularColorFactor; #endif @@ -251,11 +311,41 @@ void setSpecular(inout czm_modelMaterial material, in float metalness) material.specular = mix(dielectricSpecularF0, material.diffuse, metalness); } #endif +#ifdef USE_ANISOTROPY +void setAnisotropy(inout czm_modelMaterial material, in NormalInfo normalInfo) +{ + mat2 rotation = mat2(u_anisotropy.xy, -u_anisotropy.y, u_anisotropy.x); + float anisotropyStrength = u_anisotropy.z; + + vec2 direction = vec2(1.0, 0.0); + #ifdef HAS_ANISOTROPY_TEXTURE + vec2 anisotropyTexCoords = TEXCOORD_ANISOTROPY; + #ifdef HAS_ANISOTROPY_TEXTURE_TRANSFORM + anisotropyTexCoords = computeTextureTransform(anisotropyTexCoords, u_anisotropyTextureTransform); + #endif + vec3 anisotropySample = texture(u_anisotropyTexture, anisotropyTexCoords).rgb; + direction = anisotropySample.rg * 2.0 - vec2(1.0); + anisotropyStrength *= anisotropySample.b; + #endif + + direction = rotation * direction; + mat3 tbn = mat3(normalInfo.tangent, normalInfo.bitangent, normalInfo.normal); + vec3 anisotropicT = tbn * normalize(vec3(direction, 0.0)); + vec3 anisotropicB = cross(normalInfo.geometryNormal, anisotropicT); + + material.anisotropicT = anisotropicT; + material.anisotropicB = anisotropicB; + material.anisotropyStrength = anisotropyStrength; +} +#endif #endif void materialStage(inout czm_modelMaterial material, ProcessedAttributes attributes, SelectedFeature feature) { - #ifdef HAS_NORMALS + #ifdef USE_ANISOTROPY + NormalInfo normalInfo = getNormalInfo(attributes); + material.normalEC = normalInfo.normal; + #elif defined(HAS_NORMALS) material.normalEC = computeNormal(attributes); #endif @@ -303,8 +393,11 @@ void materialStage(inout czm_modelMaterial material, ProcessedAttributes attribu setSpecularGlossiness(material); #elif defined(LIGHTING_PBR) float metalness = setMetallicRoughness(material); - #if defined(USE_SPECULAR) + #ifdef USE_SPECULAR setSpecular(material, metalness); #endif + #ifdef USE_ANISOTROPY + setAnisotropy(material, normalInfo); + #endif #endif } diff --git a/packages/engine/Specs/Scene/GltfLoaderSpec.js b/packages/engine/Specs/Scene/GltfLoaderSpec.js index 99bad3129023..d976d1ec22c3 100644 --- a/packages/engine/Specs/Scene/GltfLoaderSpec.js +++ b/packages/engine/Specs/Scene/GltfLoaderSpec.js @@ -124,6 +124,8 @@ describe( "./Data/Models/glTF-2.0/BoxWeb3dQuantizedAttributes/glTF/BoxWeb3dQuantizedAttributes.gltf"; const specularTestData = "./Data/Models/glTF-2.0/BoxSpecular/glTF/BoxSpecular.gltf"; + const anisotropyTestData = + "./Data/Models/glTF-2.0/BoxAnisotropy/glTF/BoxAnisotropy.gltf"; let scene; const gltfLoaders = []; @@ -4153,6 +4155,21 @@ describe( expect(material.specular.specularTexture.texture.width).toBe(256); }); + it("loads model with KHR_materials_anisotropy extension", async function () { + const gltfLoader = await loadGltf(anisotropyTestData); + + const { material } = gltfLoader.components.nodes[1].primitives[0]; + const { + anisotropyStrength, + anisotropyRotation, + anisotropyTexture, + } = material.anisotropy; + + expect(anisotropyStrength).toBe(0.5); + expect(anisotropyRotation).toBe(0.349065850398866); + expect(anisotropyTexture.texture.width).toBe(256); + }); + it("parses copyright field", function () { return loadGltf(boxWithCredits).then(function (gltfLoader) { const components = gltfLoader.components; diff --git a/packages/engine/Specs/Scene/Model/MaterialPipelineStageSpec.js b/packages/engine/Specs/Scene/Model/MaterialPipelineStageSpec.js index a1479d3860d6..ba532ed789b9 100644 --- a/packages/engine/Specs/Scene/Model/MaterialPipelineStageSpec.js +++ b/packages/engine/Specs/Scene/Model/MaterialPipelineStageSpec.js @@ -87,6 +87,8 @@ describe( "./Data/Models/glTF-2.0/TwoSidedPlane/glTF/TwoSidedPlane.gltf"; const specularTestData = "./Data/Models/glTF-2.0/BoxSpecular/glTF/BoxSpecular.gltf"; + const anisotropyTestData = + "./Data/Models/glTF-2.0/BoxAnisotropy/glTF/BoxAnisotropy.gltf"; function expectUniformMap(uniformMap, expected) { for (const key in expected) { @@ -477,6 +479,53 @@ describe( expectUniformMap(uniformMap, expectedUniforms); }); + it("adds uniforms and defines for KHR_materials_anisotropy", async function () { + const gltfLoader = await loadGltf(anisotropyTestData); + + const primitive = gltfLoader.components.nodes[1].primitives[0]; + const renderResources = mockRenderResources(); + MaterialPipelineStage.process(renderResources, primitive, mockFrameState); + const { shaderBuilder, uniformMap } = renderResources; + + ShaderBuilderTester.expectHasVertexUniforms(shaderBuilder, []); + ShaderBuilderTester.expectHasFragmentUniforms(shaderBuilder, [ + "uniform float u_metallicFactor;", + "uniform sampler2D u_anisotropyTexture;", + "uniform sampler2D u_baseColorTexture;", + "uniform sampler2D u_normalTexture;", + "uniform vec3 u_anisotropy;", + ]); + + ShaderBuilderTester.expectHasVertexDefines(shaderBuilder, []); + ShaderBuilderTester.expectHasFragmentDefines(shaderBuilder, [ + "HAS_ANISOTROPY_TEXTURE", + "HAS_BASE_COLOR_TEXTURE", + "HAS_METALLIC_FACTOR", + "HAS_NORMAL_TEXTURE", + "TEXCOORD_ANISOTROPY v_texCoord_0", + "TEXCOORD_BASE_COLOR v_texCoord_0", + "TEXCOORD_NORMAL v_texCoord_0", + "USE_ANISOTROPY", + "USE_METALLIC_ROUGHNESS", + ]); + + const { + anisotropyStrength, + anisotropyRotation, + anisotropyTexture, + } = primitive.material.anisotropy; + const expectedAnisotropy = Cartesian3.fromElements( + Math.cos(anisotropyRotation), + Math.sin(anisotropyRotation), + anisotropyStrength + ); + const expectedUniforms = { + u_anisotropy: expectedAnisotropy, + u_anisotropyTexture: anisotropyTexture.texture, + }; + expectUniformMap(uniformMap, expectedUniforms); + }); + it("doesn't add texture uniforms for classification models", function () { return loadGltf(boomBox).then(function (gltfLoader) { const components = gltfLoader.components; diff --git a/packages/engine/Specs/Scene/Model/ModelSpec.js b/packages/engine/Specs/Scene/Model/ModelSpec.js index a9c67ba6140a..fec7b5ea0444 100644 --- a/packages/engine/Specs/Scene/Model/ModelSpec.js +++ b/packages/engine/Specs/Scene/Model/ModelSpec.js @@ -108,6 +108,8 @@ describe( "./Data/Models/glTF-2.0/BoomBox/glTF-pbrSpecularGlossiness/BoomBox.gltf"; const boxSpecularUrl = "./Data/Models/glTF-2.0/BoxSpecular/glTF/BoxSpecular.gltf"; + const boxAnisotropyUrl = + "./Data/Models/glTF-2.0/BoxAnisotropy/glTF/BoxAnisotropy.gltf"; const riggedFigureUrl = "./Data/Models/glTF-2.0/RiggedFigureTest/glTF/RiggedFigureTest.gltf"; const dracoCesiumManUrl = @@ -721,6 +723,19 @@ describe( verifyRender(model, true); }); + it("renders model with the KHR_materials_anisotropy extension", async function () { + const resource = Resource.createIfNeeded(boxAnisotropyUrl); + const gltf = await resource.fetchJson(); + const model = await loadAndZoomToModelAsync( + { + gltf: gltf, + basePath: boxAnisotropyUrl, + }, + scene + ); + verifyRender(model, true); + }); + it("transforms property textures with KHR_texture_transform", async function () { const resource = Resource.createIfNeeded( propertyTextureWithTextureTransformUrl