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...
+
+
+
+
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