소스 검색

Merge pull request #8284 from Popov72/detailmap

Add detail map support to the standard and PBR materials
David Catuhe 5 년 전
부모
커밋
a26130db62

BIN
Playground/textures/detailmap.png


+ 1 - 0
dist/preview release/what's new.md

@@ -35,6 +35,7 @@
 - Fix Draco decoder when running on IE11 ([bghgary](https://github.com/bghgary))
 - Fix Draco decoder when running on IE11 ([bghgary](https://github.com/bghgary))
 - Change default camera calculations to only include visible and enabled meshes ([bghgary](https://github.com/bghgary))
 - Change default camera calculations to only include visible and enabled meshes ([bghgary](https://github.com/bghgary))
 - Optimized frozen instances ([Deltakosh](https://github.com/deltakosh))
 - Optimized frozen instances ([Deltakosh](https://github.com/deltakosh))
+- Add support for detail maps in both the standard and PBR materials ([Popov72](https://github.com/Popov72))
 
 
 ### NME
 ### NME
 
 

+ 25 - 3
src/Materials/PBR/pbrBaseMaterial.ts

@@ -40,6 +40,7 @@ import "../../Shaders/pbr.fragment";
 import "../../Shaders/pbr.vertex";
 import "../../Shaders/pbr.vertex";
 
 
 import { EffectFallbacks } from '../effectFallbacks';
 import { EffectFallbacks } from '../effectFallbacks';
+import { IMaterialDetailMapDefines, DetailMapConfiguration } from '../material.detailMapConfiguration';
 
 
 const onCreatedEffectParameters = { effect: null as unknown as Effect, subMesh: null as unknown as Nullable<SubMesh> };
 const onCreatedEffectParameters = { effect: null as unknown as Effect, subMesh: null as unknown as Nullable<SubMesh> };
 
 
@@ -53,7 +54,8 @@ export class PBRMaterialDefines extends MaterialDefines
     IMaterialAnisotropicDefines,
     IMaterialAnisotropicDefines,
     IMaterialBRDFDefines,
     IMaterialBRDFDefines,
     IMaterialSheenDefines,
     IMaterialSheenDefines,
-    IMaterialSubSurfaceDefines {
+    IMaterialSubSurfaceDefines,
+    IMaterialDetailMapDefines {
     public PBR = true;
     public PBR = true;
 
 
     public NUM_SAMPLES = "0";
     public NUM_SAMPLES = "0";
@@ -69,6 +71,10 @@ export class PBRMaterialDefines extends MaterialDefines
     public ALBEDODIRECTUV = 0;
     public ALBEDODIRECTUV = 0;
     public VERTEXCOLOR = false;
     public VERTEXCOLOR = false;
 
 
+    public DETAIL = false;
+    public DETAILDIRECTUV = 0;
+    public DETAIL_NORMALBLENDMETHOD = 0;
+
     public AMBIENT = false;
     public AMBIENT = false;
     public AMBIENTDIRECTUV = 0;
     public AMBIENTDIRECTUV = 0;
     public AMBIENTINGRAYSCALE = false;
     public AMBIENTINGRAYSCALE = false;
@@ -792,6 +798,11 @@ export abstract class PBRBaseMaterial extends PushMaterial {
      */
      */
     public readonly subSurface = new PBRSubSurfaceConfiguration(this._markAllSubMeshesAsTexturesDirty.bind(this));
     public readonly subSurface = new PBRSubSurfaceConfiguration(this._markAllSubMeshesAsTexturesDirty.bind(this));
 
 
+    /**
+     * Defines the detail map parameters for the material.
+     */
+    public readonly detailMap = new DetailMapConfiguration(this._markAllSubMeshesAsTexturesDirty.bind(this));
+
     protected _rebuildInParallel = false;
     protected _rebuildInParallel = false;
 
 
     /**
     /**
@@ -1015,7 +1026,8 @@ export abstract class PBRBaseMaterial extends PushMaterial {
         if (!this.subSurface.isReadyForSubMesh(defines, scene) ||
         if (!this.subSurface.isReadyForSubMesh(defines, scene) ||
             !this.clearCoat.isReadyForSubMesh(defines, scene, engine, this._disableBumpMap) ||
             !this.clearCoat.isReadyForSubMesh(defines, scene, engine, this._disableBumpMap) ||
             !this.sheen.isReadyForSubMesh(defines, scene) ||
             !this.sheen.isReadyForSubMesh(defines, scene) ||
-            !this.anisotropy.isReadyForSubMesh(defines, scene)) {
+            !this.anisotropy.isReadyForSubMesh(defines, scene) ||
+            !this.detailMap.isReadyForSubMesh(defines, scene)) {
             return false;
             return false;
         }
         }
 
 
@@ -1235,6 +1247,9 @@ export abstract class PBRBaseMaterial extends PushMaterial {
 
 
         var uniformBuffers = ["Material", "Scene"];
         var uniformBuffers = ["Material", "Scene"];
 
 
+        DetailMapConfiguration.AddUniforms(uniforms);
+        DetailMapConfiguration.AddSamplers(samplers);
+
         PBRSubSurfaceConfiguration.AddUniforms(uniforms);
         PBRSubSurfaceConfiguration.AddUniforms(uniforms);
         PBRSubSurfaceConfiguration.AddSamplers(samplers);
         PBRSubSurfaceConfiguration.AddSamplers(samplers);
 
 
@@ -1563,6 +1578,7 @@ export abstract class PBRBaseMaterial extends PushMaterial {
         }
         }
 
 
         // External config
         // External config
+        this.detailMap.prepareDefines(defines, scene);
         this.subSurface.prepareDefines(defines, scene);
         this.subSurface.prepareDefines(defines, scene);
         this.clearCoat.prepareDefines(defines, scene);
         this.clearCoat.prepareDefines(defines, scene);
         this.anisotropy.prepareDefines(defines, mesh, scene);
         this.anisotropy.prepareDefines(defines, mesh, scene);
@@ -1653,6 +1669,7 @@ export abstract class PBRBaseMaterial extends PushMaterial {
         PBRAnisotropicConfiguration.PrepareUniformBuffer(ubo);
         PBRAnisotropicConfiguration.PrepareUniformBuffer(ubo);
         PBRSheenConfiguration.PrepareUniformBuffer(ubo);
         PBRSheenConfiguration.PrepareUniformBuffer(ubo);
         PBRSubSurfaceConfiguration.PrepareUniformBuffer(ubo);
         PBRSubSurfaceConfiguration.PrepareUniformBuffer(ubo);
+        DetailMapConfiguration.PrepareUniformBuffer(ubo);
 
 
         ubo.create();
         ubo.create();
     }
     }
@@ -1957,6 +1974,7 @@ export abstract class PBRBaseMaterial extends PushMaterial {
                 }
                 }
             }
             }
 
 
+            this.detailMap.bindForSubMesh(ubo, scene, this.isFrozen);
             this.subSurface.bindForSubMesh(ubo, scene, engine, this.isFrozen, defines.LODBASEDMICROSFURACE, this.realTimeFiltering);
             this.subSurface.bindForSubMesh(ubo, scene, engine, this.isFrozen, defines.LODBASEDMICROSFURACE, this.realTimeFiltering);
             this.clearCoat.bindForSubMesh(ubo, scene, engine, this._disableBumpMap, this.isFrozen, this._invertNormalMapX, this._invertNormalMapY);
             this.clearCoat.bindForSubMesh(ubo, scene, engine, this._disableBumpMap, this.isFrozen, this._invertNormalMapX, this._invertNormalMapY);
             this.anisotropy.bindForSubMesh(ubo, scene, this.isFrozen);
             this.anisotropy.bindForSubMesh(ubo, scene, this.isFrozen);
@@ -2053,6 +2071,7 @@ export abstract class PBRBaseMaterial extends PushMaterial {
             results.push(this._lightmapTexture);
             results.push(this._lightmapTexture);
         }
         }
 
 
+        this.detailMap.getAnimatables(results);
         this.subSurface.getAnimatables(results);
         this.subSurface.getAnimatables(results);
         this.clearCoat.getAnimatables(results);
         this.clearCoat.getAnimatables(results);
         this.sheen.getAnimatables(results);
         this.sheen.getAnimatables(results);
@@ -2124,6 +2143,7 @@ export abstract class PBRBaseMaterial extends PushMaterial {
             activeTextures.push(this._lightmapTexture);
             activeTextures.push(this._lightmapTexture);
         }
         }
 
 
+        this.detailMap.getActiveTextures(activeTextures);
         this.subSurface.getActiveTextures(activeTextures);
         this.subSurface.getActiveTextures(activeTextures);
         this.clearCoat.getActiveTextures(activeTextures);
         this.clearCoat.getActiveTextures(activeTextures);
         this.sheen.getActiveTextures(activeTextures);
         this.sheen.getActiveTextures(activeTextures);
@@ -2182,7 +2202,8 @@ export abstract class PBRBaseMaterial extends PushMaterial {
             return true;
             return true;
         }
         }
 
 
-        return this.subSurface.hasTexture(texture) ||
+        return this.detailMap.hasTexture(texture) ||
+            this.subSurface.hasTexture(texture) ||
             this.clearCoat.hasTexture(texture) ||
             this.clearCoat.hasTexture(texture) ||
             this.sheen.hasTexture(texture) ||
             this.sheen.hasTexture(texture) ||
             this.anisotropy.hasTexture(texture);
             this.anisotropy.hasTexture(texture);
@@ -2212,6 +2233,7 @@ export abstract class PBRBaseMaterial extends PushMaterial {
             this._microSurfaceTexture?.dispose();
             this._microSurfaceTexture?.dispose();
         }
         }
 
 
+        this.detailMap.dispose(forceDisposeTextures);
         this.subSurface.dispose(forceDisposeTextures);
         this.subSurface.dispose(forceDisposeTextures);
         this.clearCoat.dispose(forceDisposeTextures);
         this.clearCoat.dispose(forceDisposeTextures);
         this.sheen.dispose(forceDisposeTextures);
         this.sheen.dispose(forceDisposeTextures);

+ 267 - 0
src/Materials/material.detailMapConfiguration.ts

@@ -0,0 +1,267 @@
+import { Nullable } from "../types";
+import { Scene } from "../scene";
+import { Material } from "./material";
+import { _TypeStore } from "../Misc/typeStore";
+import { serialize, expandToProperty, serializeAsTexture, SerializationHelper } from '../Misc/decorators';
+import { MaterialFlags } from './materialFlags';
+import { MaterialHelper } from './materialHelper';
+import { BaseTexture } from './Textures/baseTexture';
+import { UniformBuffer } from './uniformBuffer';
+import { IAnimatable } from '../Animations/animatable.interface';
+
+/**
+ * @hidden
+ */
+export interface IMaterialDetailMapDefines {
+    DETAIL: boolean;
+    DETAILDIRECTUV : number;
+    DETAIL_NORMALBLENDMETHOD: number;
+
+    /** @hidden */
+    _areTexturesDirty: boolean;
+}
+
+/**
+ * Define the code related to the detail map parameters of a material
+ *
+ * Inspired from:
+ *   Unity: https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@9.0/manual/Mask-Map-and-Detail-Map.html and https://docs.unity3d.com/Manual/StandardShaderMaterialParameterDetail.html
+ *   Unreal: https://docs.unrealengine.com/en-US/Engine/Rendering/Materials/HowTo/DetailTexturing/index.html
+ *   Cryengine: https://docs.cryengine.com/display/SDKDOC2/Detail+Maps
+ */
+export class DetailMapConfiguration {
+
+    private _texture: Nullable<BaseTexture> = null;
+    /**
+     * The detail texture of the material.
+     */
+    @serializeAsTexture("detailTexture")
+    @expandToProperty("_markAllSubMeshesAsTexturesDirty")
+    public texture: Nullable<BaseTexture>;
+
+    /**
+     * Defines how strongly the detail diffuse/albedo channel is blended with the regular diffuse/albedo texture
+     * Bigger values mean stronger blending
+     */
+    @serialize()
+    public diffuseBlendLevel = 0.5;
+
+    /**
+     * Defines how strongly the detail roughness channel is blended with the regular roughness value
+     * Bigger values mean stronger blending. Only used with PBR materials
+     */
+    @serialize()
+    public roughnessBlendLevel = 0.5;
+
+    /**
+     * Defines how strong the bump effect from the detail map is
+     * Bigger values mean stronger effect
+     */
+    @serialize()
+    public bumpLevel = 1;
+
+    private _normalBlendMethod = Material.MATERIAL_NORMALBLENDMETHOD_WHITEOUT;
+    /**
+     * The method used to blend the bump and detail normals together
+     */
+    @serialize()
+    @expandToProperty("_markAllSubMeshesAsTexturesDirty")
+    public normalBlendMethod: number;
+
+    private _isEnabled = false;
+    /**
+     * Enable or disable the detail map on this material
+     */
+    @serialize()
+    @expandToProperty("_markAllSubMeshesAsTexturesDirty")
+    public isEnabled = false;
+
+    /** @hidden */
+    private _internalMarkAllSubMeshesAsTexturesDirty: () => void;
+
+    /** @hidden */
+    public _markAllSubMeshesAsTexturesDirty(): void {
+        this._internalMarkAllSubMeshesAsTexturesDirty();
+    }
+
+    /**
+     * Instantiate a new detail map
+     * @param markAllSubMeshesAsTexturesDirty Callback to flag the material to dirty
+     */
+    constructor(markAllSubMeshesAsTexturesDirty: () => void) {
+        this._internalMarkAllSubMeshesAsTexturesDirty = markAllSubMeshesAsTexturesDirty;
+    }
+
+    /**
+     * Gets whether the submesh is ready to be used or not.
+     * @param defines the list of "defines" to update.
+     * @param scene defines the scene the material belongs to.
+     * @returns - boolean indicating that the submesh is ready or not.
+     */
+    public isReadyForSubMesh(defines: IMaterialDetailMapDefines, scene: Scene): boolean {
+        const engine = scene.getEngine();
+
+        if (defines._areTexturesDirty && scene.texturesEnabled) {
+            if (engine.getCaps().standardDerivatives && this._texture && MaterialFlags.DetailTextureEnabled) {
+                // Detail texture cannot be not blocking.
+                if (!this._texture.isReady()) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Update the defines for detail map usage
+     * @param defines the list of "defines" to update.
+     * @param scene defines the scene the material belongs to.
+     */
+    public prepareDefines(defines: IMaterialDetailMapDefines, scene: Scene): void {
+        if (this._isEnabled) {
+            defines.DETAIL_NORMALBLENDMETHOD = this._normalBlendMethod;
+
+            const engine = scene.getEngine();
+
+            if (defines._areTexturesDirty) {
+                if (engine.getCaps().standardDerivatives && this._texture && MaterialFlags.DetailTextureEnabled && this._isEnabled) {
+                    MaterialHelper.PrepareDefinesForMergedUV(this._texture, defines, "DETAIL");
+                    defines.DETAIL_NORMALBLENDMETHOD = this._normalBlendMethod;
+                } else {
+                    defines.DETAIL = false;
+                }
+            }
+        } else {
+            defines.DETAIL = false;
+        }
+    }
+
+    /**
+     * Binds the material data.
+     * @param uniformBuffer defines the Uniform buffer to fill in.
+     * @param scene defines the scene the material belongs to.
+     * @param isFrozen defines whether the material is frozen or not.
+     */
+    public bindForSubMesh(uniformBuffer: UniformBuffer, scene: Scene, isFrozen: boolean): void {
+        if (!this._isEnabled) {
+            return;
+        }
+
+        if (!uniformBuffer.useUbo || !isFrozen || !uniformBuffer.isSync) {
+            if (this._texture && MaterialFlags.DetailTextureEnabled) {
+                uniformBuffer.updateFloat4("vDetailInfos", this._texture.coordinatesIndex, this.diffuseBlendLevel, this.bumpLevel, this.roughnessBlendLevel);
+                MaterialHelper.BindTextureMatrix(this._texture, uniformBuffer, "detail");
+            }
+        }
+
+        // Textures
+        if (scene.texturesEnabled) {
+            if (this._texture && MaterialFlags.DetailTextureEnabled) {
+                uniformBuffer.setTexture("detailSampler", this._texture);
+            }
+        }
+    }
+
+    /**
+     * Checks to see if a texture is used in the material.
+     * @param texture - Base texture to use.
+     * @returns - Boolean specifying if a texture is used in the material.
+     */
+    public hasTexture(texture: BaseTexture): boolean {
+        if (this._texture === texture) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns an array of the actively used textures.
+     * @param activeTextures Array of BaseTextures
+     */
+    public getActiveTextures(activeTextures: BaseTexture[]): void {
+        if (this._texture) {
+            activeTextures.push(this._texture);
+        }
+    }
+
+    /**
+     * Returns the animatable textures.
+     * @param animatables Array of animatable textures.
+     */
+    public getAnimatables(animatables: IAnimatable[]): void {
+        if (this._texture && this._texture.animations && this._texture.animations.length > 0) {
+            animatables.push(this._texture);
+        }
+    }
+
+    /**
+     * Disposes the resources of the material.
+     * @param forceDisposeTextures - Forces the disposal of all textures.
+     */
+    public dispose(forceDisposeTextures?: boolean): void {
+        if (forceDisposeTextures) {
+            this._texture?.dispose();
+        }
+    }
+
+    /**
+    * Get the current class name useful for serialization or dynamic coding.
+    * @returns "DetailMap"
+    */
+    public getClassName(): string {
+        return "DetailMap";
+    }
+
+    /**
+     * Add the required uniforms to the current list.
+     * @param uniforms defines the current uniform list.
+     */
+    public static AddUniforms(uniforms: string[]): void {
+        uniforms.push("vDetailInfos");
+    }
+
+    /**
+     * Add the required samplers to the current list.
+     * @param samplers defines the current sampler list.
+     */
+    public static AddSamplers(samplers: string[]): void {
+        samplers.push("detailSampler");
+    }
+
+    /**
+     * Add the required uniforms to the current buffer.
+     * @param uniformBuffer defines the current uniform buffer.
+     */
+    public static PrepareUniformBuffer(uniformBuffer: UniformBuffer): void {
+        uniformBuffer.addUniform("vDetailInfos", 4);
+        uniformBuffer.addUniform("detailMatrix", 16);
+    }
+
+    /**
+     * Makes a duplicate of the current instance into another one.
+     * @param detailMap define the instance where to copy the info
+     */
+    public copyTo(detailMap: DetailMapConfiguration): void {
+        SerializationHelper.Clone(() => detailMap, this);
+    }
+
+    /**
+     * Serializes this detail map instance
+     * @returns - An object with the serialized instance.
+     */
+    public serialize(): any {
+        return SerializationHelper.Serialize(this);
+    }
+
+    /**
+     * Parses a detail map setting from a serialized object.
+     * @param source - Serialized object.
+     * @param scene Defines the scene we are parsing for
+     * @param rootUrl Defines the rootUrl to load from
+     */
+    public parse(source: any, scene: Scene, rootUrl: string): void {
+        SerializationHelper.Parse(() => this, source, scene, rootUrl);
+    }
+}

+ 12 - 0
src/Materials/material.ts

@@ -155,6 +155,18 @@ export class Material implements IAnimatable {
     public static readonly MATERIAL_ALPHATESTANDBLEND = 3;
     public static readonly MATERIAL_ALPHATESTANDBLEND = 3;
 
 
     /**
     /**
+     * The Whiteout method is used to blend normals.
+     * Details of the algorithm can be found here: https://blog.selfshadow.com/publications/blending-in-detail/
+     */
+    public static readonly MATERIAL_NORMALBLENDMETHOD_WHITEOUT = 0;
+
+    /**
+     * The Reoriented Normal Mapping method is used to blend normals.
+     * Details of the algorithm can be found here: https://blog.selfshadow.com/publications/blending-in-detail/
+     */
+    public static readonly MATERIAL_NORMALBLENDMETHOD_RNM = 1;
+
+    /**
      * Custom callback helping to override the default shader used in the material.
      * Custom callback helping to override the default shader used in the material.
      */
      */
     public customShaderNameResolve: (shaderName: string, uniforms: string[], uniformBuffers: string[], samplers: string[], defines: MaterialDefines | string[], attributes?: string[], options?: ICustomShaderNameResolveOptions) => string;
     public customShaderNameResolve: (shaderName: string, uniforms: string[], uniformBuffers: string[], samplers: string[], defines: MaterialDefines | string[], attributes?: string[], options?: ICustomShaderNameResolveOptions) => string;

+ 16 - 0
src/Materials/materialFlags.ts

@@ -22,6 +22,22 @@ export class MaterialFlags {
         Engine.MarkAllMaterialsAsDirty(Constants.MATERIAL_TextureDirtyFlag);
         Engine.MarkAllMaterialsAsDirty(Constants.MATERIAL_TextureDirtyFlag);
     }
     }
 
 
+    private static _DetailTextureEnabled = true;
+    /**
+     * Are detail textures enabled in the application.
+     */
+    public static get DetailTextureEnabled(): boolean {
+        return this._DetailTextureEnabled;
+    }
+    public static set DetailTextureEnabled(value: boolean) {
+        if (this._DetailTextureEnabled === value) {
+            return;
+        }
+
+        this._DetailTextureEnabled = value;
+        Engine.MarkAllMaterialsAsDirty(Constants.MATERIAL_TextureDirtyFlag);
+    }
+
     private static _AmbientTextureEnabled = true;
     private static _AmbientTextureEnabled = true;
     /**
     /**
      * Are ambient textures enabled in the application.
      * Are ambient textures enabled in the application.

+ 50 - 37
src/Materials/standardMaterial.ts

@@ -32,15 +32,19 @@ import "../Shaders/default.vertex";
 import { Constants } from "../Engines/constants";
 import { Constants } from "../Engines/constants";
 import { EffectFallbacks } from './effectFallbacks';
 import { EffectFallbacks } from './effectFallbacks';
 import { Effect, IEffectCreationOptions } from './effect';
 import { Effect, IEffectCreationOptions } from './effect';
+import { IMaterialDetailMapDefines, DetailMapConfiguration } from './material.detailMapConfiguration';
 
 
 const onCreatedEffectParameters = { effect: null as unknown as Effect, subMesh: null as unknown as Nullable<SubMesh> };
 const onCreatedEffectParameters = { effect: null as unknown as Effect, subMesh: null as unknown as Nullable<SubMesh> };
 
 
 /** @hidden */
 /** @hidden */
-export class StandardMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines {
+export class StandardMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines, IMaterialDetailMapDefines {
     public MAINUV1 = false;
     public MAINUV1 = false;
     public MAINUV2 = false;
     public MAINUV2 = false;
     public DIFFUSE = false;
     public DIFFUSE = false;
     public DIFFUSEDIRECTUV = 0;
     public DIFFUSEDIRECTUV = 0;
+    public DETAIL = false;
+    public DETAILDIRECTUV = 0;
+    public DETAIL_NORMALBLENDMETHOD = 0;
     public AMBIENT = false;
     public AMBIENT = false;
     public AMBIENTDIRECTUV = 0;
     public AMBIENTDIRECTUV = 0;
     public OPACITY = false;
     public OPACITY = false;
@@ -669,6 +673,11 @@ export class StandardMaterial extends PushMaterial {
         this._imageProcessingConfiguration.colorCurves = value;
         this._imageProcessingConfiguration.colorCurves = value;
     }
     }
 
 
+    /**
+     * Defines the detail map parameters for the material.
+     */
+    public readonly detailMap = new DetailMapConfiguration(this._markAllSubMeshesAsTexturesDirty.bind(this));
+
     protected _renderTargets = new SmartArray<RenderTargetTexture>(16);
     protected _renderTargets = new SmartArray<RenderTargetTexture>(16);
     protected _worldViewProjectionMatrix = Matrix.Zero();
     protected _worldViewProjectionMatrix = Matrix.Zero();
     protected _globalAmbientColor = new Color3(0, 0, 0);
     protected _globalAmbientColor = new Color3(0, 0, 0);
@@ -991,6 +1000,10 @@ export class StandardMaterial extends PushMaterial {
             defines.ALPHABLEND = this.transparencyMode === null || this.needAlphaBlendingForMesh(mesh); // check on null for backward compatibility
             defines.ALPHABLEND = this.transparencyMode === null || this.needAlphaBlendingForMesh(mesh); // check on null for backward compatibility
         }
         }
 
 
+        if (!this.detailMap.isReadyForSubMesh(defines, scene)) {
+            return false;
+        }
+
         if (defines._areImageProcessingDirty && this._imageProcessingConfiguration) {
         if (defines._areImageProcessingDirty && this._imageProcessingConfiguration) {
             if (!this._imageProcessingConfiguration.isReady()) {
             if (!this._imageProcessingConfiguration.isReady()) {
                 return false;
                 return false;
@@ -1038,6 +1051,9 @@ export class StandardMaterial extends PushMaterial {
         // Values that need to be evaluated on every frame
         // Values that need to be evaluated on every frame
         MaterialHelper.PrepareDefinesForFrameBoundValues(scene, engine, defines, useInstances, null, subMesh.getRenderingMesh().hasThinInstances);
         MaterialHelper.PrepareDefinesForFrameBoundValues(scene, engine, defines, useInstances, null, subMesh.getRenderingMesh().hasThinInstances);
 
 
+        // External config
+        this.detailMap.prepareDefines(defines, scene);
+
         // Get correct effect
         // Get correct effect
         if (defines.isDirty) {
         if (defines.isDirty) {
             const lightDisposed = defines._areLightsDisposed;
             const lightDisposed = defines._areLightsDisposed;
@@ -1152,6 +1168,9 @@ export class StandardMaterial extends PushMaterial {
 
 
             var uniformBuffers = ["Material", "Scene"];
             var uniformBuffers = ["Material", "Scene"];
 
 
+            DetailMapConfiguration.AddUniforms(uniforms);
+            DetailMapConfiguration.AddSamplers(samplers);
+
             if (ImageProcessingConfiguration) {
             if (ImageProcessingConfiguration) {
                 ImageProcessingConfiguration.PrepareUniforms(uniforms, defines);
                 ImageProcessingConfiguration.PrepareUniforms(uniforms, defines);
                 ImageProcessingConfiguration.PrepareSamplers(samplers, defines);
                 ImageProcessingConfiguration.PrepareSamplers(samplers, defines);
@@ -1269,6 +1288,8 @@ export class StandardMaterial extends PushMaterial {
         ubo.addUniform("visibility", 1);
         ubo.addUniform("visibility", 1);
         ubo.addUniform("vDiffuseColor", 4);
         ubo.addUniform("vDiffuseColor", 4);
 
 
+        DetailMapConfiguration.PrepareUniformBuffer(ubo);
+
         ubo.create();
         ubo.create();
     }
     }
 
 
@@ -1502,6 +1523,8 @@ export class StandardMaterial extends PushMaterial {
                 }
                 }
             }
             }
 
 
+            this.detailMap.bindForSubMesh(ubo, scene, this.isFrozen);
+
             // Clip plane
             // Clip plane
             MaterialHelper.BindClipPlane(effect, scene);
             MaterialHelper.BindClipPlane(effect, scene);
 
 
@@ -1589,6 +1612,8 @@ export class StandardMaterial extends PushMaterial {
             results.push(this._refractionTexture);
             results.push(this._refractionTexture);
         }
         }
 
 
+        this.detailMap.getAnimatables(results);
+
         return results;
         return results;
     }
     }
 
 
@@ -1635,6 +1660,8 @@ export class StandardMaterial extends PushMaterial {
             activeTextures.push(this._refractionTexture);
             activeTextures.push(this._refractionTexture);
         }
         }
 
 
+        this.detailMap.getActiveTextures(activeTextures);
+
         return activeTextures;
         return activeTextures;
     }
     }
 
 
@@ -1684,7 +1711,7 @@ export class StandardMaterial extends PushMaterial {
             return true;
             return true;
         }
         }
 
 
-        return false;
+        return this.detailMap.hasTexture(texture);
     }
     }
 
 
     /**
     /**
@@ -1694,43 +1721,19 @@ export class StandardMaterial extends PushMaterial {
      */
      */
     public dispose(forceDisposeEffect?: boolean, forceDisposeTextures?: boolean): void {
     public dispose(forceDisposeEffect?: boolean, forceDisposeTextures?: boolean): void {
         if (forceDisposeTextures) {
         if (forceDisposeTextures) {
-            if (this._diffuseTexture) {
-                this._diffuseTexture.dispose();
-            }
-
-            if (this._ambientTexture) {
-                this._ambientTexture.dispose();
-            }
-
-            if (this._opacityTexture) {
-                this._opacityTexture.dispose();
-            }
-
-            if (this._reflectionTexture) {
-                this._reflectionTexture.dispose();
-            }
-
-            if (this._emissiveTexture) {
-                this._emissiveTexture.dispose();
-            }
-
-            if (this._specularTexture) {
-                this._specularTexture.dispose();
-            }
-
-            if (this._bumpTexture) {
-                this._bumpTexture.dispose();
-            }
-
-            if (this._lightmapTexture) {
-                this._lightmapTexture.dispose();
-            }
-
-            if (this._refractionTexture) {
-                this._refractionTexture.dispose();
-            }
+            this._diffuseTexture?.dispose();
+            this._ambientTexture?.dispose();
+            this._opacityTexture?.dispose();
+            this._reflectionTexture?.dispose();
+            this._emissiveTexture?.dispose();
+            this._specularTexture?.dispose();
+            this._bumpTexture?.dispose();
+            this._lightmapTexture?.dispose();
+            this._refractionTexture?.dispose();
         }
         }
 
 
+        this.detailMap.dispose(forceDisposeTextures);
+
         if (this._imageProcessingConfiguration && this._imageProcessingObserver) {
         if (this._imageProcessingConfiguration && this._imageProcessingObserver) {
             this._imageProcessingConfiguration.onUpdateParameters.remove(this._imageProcessingObserver);
             this._imageProcessingConfiguration.onUpdateParameters.remove(this._imageProcessingObserver);
         }
         }
@@ -1783,6 +1786,16 @@ export class StandardMaterial extends PushMaterial {
     }
     }
 
 
     /**
     /**
+     * Are detail textures enabled in the application.
+     */
+    public static get DetailTextureEnabled(): boolean {
+        return MaterialFlags.DetailTextureEnabled;
+    }
+    public static set DetailTextureEnabled(value: boolean) {
+        MaterialFlags.DetailTextureEnabled = value;
+    }
+
+    /**
      * Are ambient textures enabled in the application.
      * Are ambient textures enabled in the application.
      */
      */
     public static get AmbientTextureEnabled(): boolean {
     public static get AmbientTextureEnabled(): boolean {

+ 30 - 4
src/Shaders/ShadersInclude/bumpFragment.fx

@@ -1,16 +1,20 @@
 vec2 uvOffset = vec2(0.0, 0.0);
 vec2 uvOffset = vec2(0.0, 0.0);
 
 
-#if defined(BUMP) || defined(PARALLAX)
+#if defined(BUMP) || defined(PARALLAX) || defined(DETAIL)
 	#ifdef NORMALXYSCALE
 	#ifdef NORMALXYSCALE
 		float normalScale = 1.0;
 		float normalScale = 1.0;
-	#else		
+	#elif defined(BUMP)
 		float normalScale = vBumpInfos.y;
 		float normalScale = vBumpInfos.y;
+	#else
+		float normalScale = vDetailInfos.z;
 	#endif
 	#endif
 
 
 	#if defined(TANGENT) && defined(NORMAL)
 	#if defined(TANGENT) && defined(NORMAL)
 		mat3 TBN = vTBN;
 		mat3 TBN = vTBN;
-	#else
+	#elif defined(BUMP)
 		mat3 TBN = cotangent_frame(normalW * normalScale, vPositionW, vBumpUV);
 		mat3 TBN = cotangent_frame(normalW * normalScale, vPositionW, vBumpUV);
+    #else
+		mat3 TBN = cotangent_frame(normalW * normalScale, vPositionW, vDetailUV, vec2(1., 1.));
 	#endif
 	#endif
 #elif defined(ANISOTROPIC)
 #elif defined(ANISOTROPIC)
 	#if defined(TANGENT) && defined(NORMAL)
 	#if defined(TANGENT) && defined(NORMAL)
@@ -30,11 +34,33 @@
 	#endif
 	#endif
 #endif
 #endif
 
 
+#ifdef DETAIL
+	vec4 detailColor = texture2D(detailSampler, vDetailUV + uvOffset);
+    vec2 detailNormalRG = detailColor.wy * 2.0 - 1.0;
+    float detailNormalB = sqrt(1. - saturate(dot(detailNormalRG, detailNormalRG)));
+    vec3 detailNormal = vec3(detailNormalRG, detailNormalB);
+#endif
+
 #ifdef BUMP
 #ifdef BUMP
 	#ifdef OBJECTSPACE_NORMALMAP
 	#ifdef OBJECTSPACE_NORMALMAP
 		normalW = normalize(texture2D(bumpSampler, vBumpUV).xyz  * 2.0 - 1.0);
 		normalW = normalize(texture2D(bumpSampler, vBumpUV).xyz  * 2.0 - 1.0);
 		normalW = normalize(mat3(normalMatrix) * normalW);	
 		normalW = normalize(mat3(normalMatrix) * normalW);	
-	#else
+	#elif !defined(DETAIL)
 		normalW = perturbNormal(TBN, vBumpUV + uvOffset);
 		normalW = perturbNormal(TBN, vBumpUV + uvOffset);
+    #else
+        vec3 bumpNormal = texture2D(bumpSampler, vBumpUV + uvOffset).xyz * 2.0 - 1.0;
+        // Reference for normal blending: https://blog.selfshadow.com/publications/blending-in-detail/
+        #if DETAIL_NORMALBLENDMETHOD == 0 // whiteout
+            detailNormal.xy *= vDetailInfos.z;
+            vec3 blendedNormal = normalize(vec3(bumpNormal.xy + detailNormal.xy, bumpNormal.z * detailNormal.z));
+        #elif DETAIL_NORMALBLENDMETHOD == 1 // RNM
+            detailNormal.xy *= vDetailInfos.z;
+            bumpNormal += vec3(0.0, 0.0, 1.0);
+            detailNormal *= vec3(-1.0, -1.0, 1.0);
+            vec3 blendedNormal = bumpNormal * dot(bumpNormal, detailNormal) / bumpNormal.z - detailNormal;
+        #endif
+        normalW = perturbNormalBase(TBN, blendedNormal, vBumpInfos.y);
 	#endif
 	#endif
+#elif defined(DETAIL)    
+		normalW = perturbNormalBase(TBN, detailNormal, vDetailInfos.z);
 #endif
 #endif

+ 11 - 0
src/Shaders/ShadersInclude/bumpFragmentFunctions.fx

@@ -14,6 +14,17 @@
 	}
 	}
 #endif
 #endif
 
 
+#if defined(DETAIL)
+	#if DETAILDIRECTUV == 1
+		#define vDetailUV vMainUV1
+	#elif DETAILDIRECTUV == 2
+		#define vDetailUV vMainUV2
+	#else
+		varying vec2 vDetailUV;
+	#endif
+	uniform sampler2D detailSampler;
+#endif
+
 #if defined(BUMP)
 #if defined(BUMP)
 	vec3 perturbNormal(mat3 cotangentFrame, vec3 color)
 	vec3 perturbNormal(mat3 cotangentFrame, vec3 color)
 	{
 	{

+ 9 - 6
src/Shaders/ShadersInclude/bumpFragmentMainFunctions.fx

@@ -1,4 +1,4 @@
-#if defined(BUMP) || defined(CLEARCOAT_BUMP) || defined(ANISOTROPIC)
+#if defined(BUMP) || defined(CLEARCOAT_BUMP) || defined(ANISOTROPIC) || defined(DETAIL)
 	#if defined(TANGENT) && defined(NORMAL) 
 	#if defined(TANGENT) && defined(NORMAL) 
 		varying mat3 vTBN;
 		varying mat3 vTBN;
 	#endif
 	#endif
@@ -7,15 +7,18 @@
 		uniform mat4 normalMatrix;
 		uniform mat4 normalMatrix;
 	#endif
 	#endif
 
 
-	vec3 perturbNormal(mat3 cotangentFrame, vec3 textureSample, float scale)
+	vec3 perturbNormalBase(mat3 cotangentFrame, vec3 normal, float scale)
 	{
 	{
-		textureSample = textureSample * 2.0 - 1.0;
-
 		#ifdef NORMALXYSCALE
 		#ifdef NORMALXYSCALE
-			textureSample = normalize(textureSample * vec3(scale, scale, 1.0));
+			normal = normalize(normal * vec3(scale, scale, 1.0));
 		#endif
 		#endif
 
 
-		return normalize(cotangentFrame * textureSample);
+		return normalize(cotangentFrame * normal);
+	}
+
+	vec3 perturbNormal(mat3 cotangentFrame, vec3 textureSample, float scale)
+	{
+		return perturbNormalBase(cotangentFrame, textureSample * 2.0 - 1.0, scale);
 	}
 	}
 
 
 	// Thanks to http://www.thetenthplanet.de/archives/1180
 	// Thanks to http://www.thetenthplanet.de/archives/1180

+ 3 - 0
src/Shaders/ShadersInclude/defaultUboDeclaration.fx

@@ -37,6 +37,9 @@ uniform Material
 	vec3 vEmissiveColor;
 	vec3 vEmissiveColor;
 	float visibility;
 	float visibility;
 	vec4 vDiffuseColor;
 	vec4 vDiffuseColor;
+
+	vec4 vDetailInfos;
+	mat4 detailMatrix;
 };
 };
 
 
 uniform Scene {
 uniform Scene {

+ 9 - 0
src/Shaders/ShadersInclude/pbrBlockAlbedoOpacity.fx

@@ -15,6 +15,10 @@ void albedoOpacityBlock(
     const in vec4 opacityMap,
     const in vec4 opacityMap,
     const in vec2 vOpacityInfos,
     const in vec2 vOpacityInfos,
 #endif
 #endif
+#ifdef DETAIL
+    const in vec4 detailColor,
+    const in vec4 vDetailInfos,
+#endif
     out albedoOpacityOutParams outParams
     out albedoOpacityOutParams outParams
 )
 )
 {
 {
@@ -40,6 +44,11 @@ void albedoOpacityBlock(
         surfaceAlbedo *= vColor.rgb;
         surfaceAlbedo *= vColor.rgb;
     #endif
     #endif
 
 
+    #ifdef DETAIL
+        float detailAlbedo = 2.0 * mix(0.5, detailColor.r, vDetailInfos.y);
+        surfaceAlbedo.rgb = surfaceAlbedo.rgb * detailAlbedo * detailAlbedo; // should be pow(detailAlbedo, 2.2) but detailAlbedo² is close enough and cheaper to compute
+    #endif
+
     #define CUSTOM_FRAGMENT_UPDATE_ALBEDO
     #define CUSTOM_FRAGMENT_UPDATE_ALBEDO
 
 
     // _____________________________ Alpha Information _______________________________
     // _____________________________ Alpha Information _______________________________

+ 9 - 0
src/Shaders/ShadersInclude/pbrBlockReflectivity.fx

@@ -34,6 +34,10 @@ void reflectivityBlock(
 #ifdef MICROSURFACEMAP
 #ifdef MICROSURFACEMAP
     const in vec4 microSurfaceTexel,
     const in vec4 microSurfaceTexel,
 #endif
 #endif
+#ifdef DETAIL
+    const in vec4 detailColor,
+    const in vec4 vDetailInfos,
+#endif
     out reflectivityOutParams outParams
     out reflectivityOutParams outParams
 )
 )
 {
 {
@@ -68,6 +72,11 @@ void reflectivityBlock(
             #endif
             #endif
         #endif
         #endif
 
 
+        #ifdef DETAIL
+            float detailRoughness = 2.0 * mix(0.5, detailColor.b, vDetailInfos.w);
+            metallicRoughness.g = saturate(metallicRoughness.g * detailRoughness);
+        #endif
+
         #ifdef MICROSURFACEMAP
         #ifdef MICROSURFACEMAP
             metallicRoughness.g *= microSurfaceTexel.r;
             metallicRoughness.g *= microSurfaceTexel.r;
         #endif
         #endif

+ 3 - 0
src/Shaders/ShadersInclude/pbrUboDeclaration.fx

@@ -69,6 +69,9 @@ uniform Material
     uniform vec3 vDiffusionDistance;
     uniform vec3 vDiffusionDistance;
     uniform vec4 vTintColor;
     uniform vec4 vTintColor;
     uniform vec3 vSubSurfaceIntensity;
     uniform vec3 vSubSurfaceIntensity;
+
+    uniform vec4 vDetailInfos;
+    uniform mat4 detailMatrix;
 };
 };
 
 
 uniform Scene {
 uniform Scene {

+ 4 - 0
src/Shaders/default.fragment.fx

@@ -218,6 +218,10 @@ void main(void) {
 	baseColor.rgb *= vColor.rgb;
 	baseColor.rgb *= vColor.rgb;
 #endif
 #endif
 
 
+#ifdef DETAIL
+    baseColor.rgb = baseColor.rgb * 2.0 * mix(0.5, detailColor.r, vDetailInfos.y);
+#endif
+
 #define CUSTOM_FRAGMENT_UPDATE_DIFFUSE
 #define CUSTOM_FRAGMENT_UPDATE_DIFFUSE
 
 
 	// Ambient color
 	// Ambient color

+ 15 - 0
src/Shaders/default.vertex.fx

@@ -39,6 +39,10 @@ attribute vec4 color;
 varying vec2 vDiffuseUV;
 varying vec2 vDiffuseUV;
 #endif
 #endif
 
 
+#if defined(DETAIL) && DETAILDIRECTUV == 0
+varying vec2 vDetailUV;
+#endif
+
 #if defined(AMBIENT) && AMBIENTDIRECTUV == 0
 #if defined(AMBIENT) && AMBIENTDIRECTUV == 0
 varying vec2 vAmbientUV;
 varying vec2 vAmbientUV;
 #endif
 #endif
@@ -184,6 +188,17 @@ void main(void) {
 	}
 	}
 #endif
 #endif
 
 
+#if defined(DETAIL) && DETAILDIRECTUV == 0
+	if (vDetailInfos.x == 0.)
+	{
+		vDetailUV = vec2(detailMatrix * vec4(uvUpdated, 1.0, 0.0));
+	}
+	else
+	{
+		vDetailUV = vec2(detailMatrix * vec4(uv2, 1.0, 0.0));
+	}
+#endif
+
 #if defined(AMBIENT) && AMBIENTDIRECTUV == 0
 #if defined(AMBIENT) && AMBIENTDIRECTUV == 0
 	if (vAmbientInfos.x == 0.)
 	if (vAmbientInfos.x == 0.)
 	{
 	{

+ 8 - 0
src/Shaders/pbr.fragment.fx

@@ -96,6 +96,10 @@ void main(void) {
         opacityMap,
         opacityMap,
         vOpacityInfos,
         vOpacityInfos,
     #endif
     #endif
+    #ifdef DETAIL
+        detailColor,
+        vDetailInfos,
+    #endif
         albedoOpacityOut
         albedoOpacityOut
     );
     );
 
 
@@ -170,6 +174,10 @@ void main(void) {
     #ifdef MICROSURFACEMAP
     #ifdef MICROSURFACEMAP
         microSurfaceTexel,
         microSurfaceTexel,
     #endif
     #endif
+    #ifdef DETAIL
+        detailColor,
+        vDetailInfos,
+    #endif
         reflectivityOut
         reflectivityOut
     );
     );
 
 

+ 15 - 0
src/Shaders/pbr.vertex.fx

@@ -38,6 +38,10 @@ attribute vec4 color;
 varying vec2 vAlbedoUV;
 varying vec2 vAlbedoUV;
 #endif
 #endif
 
 
+#if defined(DETAIL) && DETAILDIRECTUV == 0
+varying vec2 vDetailUV;
+#endif
+
 #if defined(AMBIENT) && AMBIENTDIRECTUV == 0
 #if defined(AMBIENT) && AMBIENTDIRECTUV == 0
 varying vec2 vAmbientUV;
 varying vec2 vAmbientUV;
 #endif
 #endif
@@ -240,6 +244,17 @@ void main(void) {
     }
     }
 #endif
 #endif
 
 
+#if defined(DETAIL) && DETAILDIRECTUV == 0
+	if (vDetailInfos.x == 0.)
+	{
+		vDetailUV = vec2(detailMatrix * vec4(uvUpdated, 1.0, 0.0));
+	}
+	else
+	{
+		vDetailUV = vec2(detailMatrix * vec4(uv2, 1.0, 0.0));
+	}
+#endif
+
 #if defined(AMBIENT) && AMBIENTDIRECTUV == 0 
 #if defined(AMBIENT) && AMBIENTDIRECTUV == 0 
     if (vAmbientInfos.x == 0.)
     if (vAmbientInfos.x == 0.)
     {
     {