Przeglądaj źródła

Merge pull request #8309 from BabylonJS/master

Nightly
mergify[bot] 5 lat temu
rodzic
commit
577c8cb338
47 zmienionych plików z 2620 dodań i 501 usunięć
  1. BIN
      Playground/textures/detailmap.png
  2. 176 2
      dist/preview release/babylon.d.ts
  3. 2 2
      dist/preview release/babylon.js
  4. 351 41
      dist/preview release/babylon.max.js
  5. 1 1
      dist/preview release/babylon.max.js.map
  6. 359 4
      dist/preview release/babylon.module.d.ts
  7. 176 2
      dist/preview release/documentation.d.ts
  8. 4 4
      dist/preview release/inspector/babylon.inspector.bundle.js
  9. 215 137
      dist/preview release/inspector/babylon.inspector.bundle.max.js
  10. 1 1
      dist/preview release/inspector/babylon.inspector.bundle.max.js.map
  11. 28 3
      dist/preview release/inspector/babylon.inspector.d.ts
  12. 62 6
      dist/preview release/inspector/babylon.inspector.module.d.ts
  13. 4 4
      dist/preview release/nodeEditor/babylon.nodeEditor.d.ts
  14. 1 1
      dist/preview release/nodeEditor/babylon.nodeEditor.js
  15. 25 15
      dist/preview release/nodeEditor/babylon.nodeEditor.max.js
  16. 1 1
      dist/preview release/nodeEditor/babylon.nodeEditor.max.js.map
  17. 10 9
      dist/preview release/nodeEditor/babylon.nodeEditor.module.d.ts
  18. 1 1
      dist/preview release/packagesSizeBaseLine.json
  19. 359 4
      dist/preview release/viewer/babylon.module.d.ts
  20. 73 69
      dist/preview release/viewer/babylon.viewer.js
  21. 1 1
      dist/preview release/viewer/babylon.viewer.max.js
  22. 3 1
      dist/preview release/what's new.md
  23. 2 0
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/addAnimation.tsx
  24. 8 123
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationCurveEditorComponent.tsx
  25. 47 2
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/curveEditor.scss
  26. 197 0
      inspector/src/components/actionTabs/tabs/propertyGrids/animations/editorControls.tsx
  27. 10 7
      nodeEditor/src/diagram/graphCanvas.tsx
  28. 1 1
      nodeEditor/src/diagram/graphFrame.ts
  29. 3 3
      nodeEditor/src/diagram/properties/framePropertyComponent.tsx
  30. 1 1
      nodeEditor/src/globalState.ts
  31. 11 5
      nodeEditor/src/serializationTools.ts
  32. 25 3
      src/Materials/PBR/pbrBaseMaterial.ts
  33. 267 0
      src/Materials/material.detailMapConfiguration.ts
  34. 12 0
      src/Materials/material.ts
  35. 16 0
      src/Materials/materialFlags.ts
  36. 50 37
      src/Materials/standardMaterial.ts
  37. 31 4
      src/Shaders/ShadersInclude/bumpFragment.fx
  38. 11 0
      src/Shaders/ShadersInclude/bumpFragmentFunctions.fx
  39. 9 6
      src/Shaders/ShadersInclude/bumpFragmentMainFunctions.fx
  40. 3 0
      src/Shaders/ShadersInclude/defaultUboDeclaration.fx
  41. 9 0
      src/Shaders/ShadersInclude/pbrBlockAlbedoOpacity.fx
  42. 9 0
      src/Shaders/ShadersInclude/pbrBlockReflectivity.fx
  43. 3 0
      src/Shaders/ShadersInclude/pbrUboDeclaration.fx
  44. 4 0
      src/Shaders/default.fragment.fx
  45. 15 0
      src/Shaders/default.vertex.fx
  46. 8 0
      src/Shaders/pbr.fragment.fx
  47. 15 0
      src/Shaders/pbr.vertex.fx

BIN
Playground/textures/detailmap.png


+ 176 - 2
dist/preview release/babylon.d.ts

@@ -23781,6 +23781,16 @@ declare module BABYLON {
          */
         static readonly MATERIAL_ALPHATESTANDBLEND: number;
         /**
+         * The Whiteout method is used to blend normals.
+         * Details of the algorithm can be found here: https://blog.selfshadow.com/publications/blending-in-detail/
+         */
+        static readonly MATERIAL_NORMALBLENDMETHOD_WHITEOUT: number;
+        /**
+         * 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/
+         */
+        static readonly MATERIAL_NORMALBLENDMETHOD_RNM: number;
+        /**
          * Custom callback helping to override the default shader used in the material.
          */
         customShaderNameResolve: (shaderName: string, uniforms: string[], uniformBuffers: string[], samplers: string[], defines: MaterialDefines | string[], attributes?: string[], options?: ICustomShaderNameResolveOptions) => string;
@@ -29089,6 +29099,12 @@ declare module BABYLON {
          */
         static get DiffuseTextureEnabled(): boolean;
         static set DiffuseTextureEnabled(value: boolean);
+        private static _DetailTextureEnabled;
+        /**
+         * Are detail textures enabled in the application.
+         */
+        static get DetailTextureEnabled(): boolean;
+        static set DetailTextureEnabled(value: boolean);
         private static _AmbientTextureEnabled;
         /**
          * Are ambient textures enabled in the application.
@@ -29349,12 +29365,154 @@ declare module BABYLON {
     };
 }
 declare module BABYLON {
+    /**
+     * @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;
+        /**
+         * The detail texture of the material.
+         */
+        texture: Nullable<BaseTexture>;
+        /**
+         * Defines how strongly the detail diffuse/albedo channel is blended with the regular diffuse/albedo texture
+         * Bigger values mean stronger blending
+         */
+        diffuseBlendLevel: number;
+        /**
+         * Defines how strongly the detail roughness channel is blended with the regular roughness value
+         * Bigger values mean stronger blending. Only used with PBR materials
+         */
+        roughnessBlendLevel: number;
+        /**
+         * Defines how strong the bump effect from the detail map is
+         * Bigger values mean stronger effect
+         */
+        bumpLevel: number;
+        private _normalBlendMethod;
+        /**
+         * The method used to blend the bump and detail normals together
+         */
+        normalBlendMethod: number;
+        private _isEnabled;
+        /**
+         * Enable or disable the detail map on this material
+         */
+        isEnabled: boolean;
+        /** @hidden */
+        private _internalMarkAllSubMeshesAsTexturesDirty;
+        /** @hidden */
+        _markAllSubMeshesAsTexturesDirty(): void;
+        /**
+         * Instantiate a new detail map
+         * @param markAllSubMeshesAsTexturesDirty Callback to flag the material to dirty
+         */
+        constructor(markAllSubMeshesAsTexturesDirty: () => void);
+        /**
+         * 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.
+         */
+        isReadyForSubMesh(defines: IMaterialDetailMapDefines, scene: Scene): boolean;
+        /**
+         * Update the defines for detail map usage
+         * @param defines the list of "defines" to update.
+         * @param scene defines the scene the material belongs to.
+         */
+        prepareDefines(defines: IMaterialDetailMapDefines, scene: Scene): void;
+        /**
+         * 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.
+         */
+        bindForSubMesh(uniformBuffer: UniformBuffer, scene: Scene, isFrozen: boolean): void;
+        /**
+         * 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.
+         */
+        hasTexture(texture: BaseTexture): boolean;
+        /**
+         * Returns an array of the actively used textures.
+         * @param activeTextures Array of BaseTextures
+         */
+        getActiveTextures(activeTextures: BaseTexture[]): void;
+        /**
+         * Returns the animatable textures.
+         * @param animatables Array of animatable textures.
+         */
+        getAnimatables(animatables: IAnimatable[]): void;
+        /**
+         * Disposes the resources of the material.
+         * @param forceDisposeTextures - Forces the disposal of all textures.
+         */
+        dispose(forceDisposeTextures?: boolean): void;
+        /**
+        * Get the current class name useful for serialization or dynamic coding.
+        * @returns "DetailMap"
+        */
+        getClassName(): string;
+        /**
+         * Add the required uniforms to the current list.
+         * @param uniforms defines the current uniform list.
+         */
+        static AddUniforms(uniforms: string[]): void;
+        /**
+         * Add the required samplers to the current list.
+         * @param samplers defines the current sampler list.
+         */
+        static AddSamplers(samplers: string[]): void;
+        /**
+         * Add the required uniforms to the current buffer.
+         * @param uniformBuffer defines the current uniform buffer.
+         */
+        static PrepareUniformBuffer(uniformBuffer: UniformBuffer): void;
+        /**
+         * Makes a duplicate of the current instance into another one.
+         * @param detailMap define the instance where to copy the info
+         */
+        copyTo(detailMap: DetailMapConfiguration): void;
+        /**
+         * Serializes this detail map instance
+         * @returns - An object with the serialized instance.
+         */
+        serialize(): any;
+        /**
+         * 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
+         */
+        parse(source: any, scene: Scene, rootUrl: string): void;
+    }
+}
+declare module BABYLON {
     /** @hidden */
-    export class StandardMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines {
+    export class StandardMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines, IMaterialDetailMapDefines {
         MAINUV1: boolean;
         MAINUV2: boolean;
         DIFFUSE: boolean;
         DIFFUSEDIRECTUV: number;
+        DETAIL: boolean;
+        DETAILDIRECTUV: number;
+        DETAIL_NORMALBLENDMETHOD: number;
         AMBIENT: boolean;
         AMBIENTDIRECTUV: number;
         OPACITY: boolean;
@@ -29780,6 +29938,10 @@ declare module BABYLON {
          * corresponding to low luminance, medium luminance, and high luminance areas respectively.
          */
         set cameraColorCurves(value: Nullable<ColorCurves>);
+        /**
+         * Defines the detail map parameters for the material.
+         */
+        readonly detailMap: DetailMapConfiguration;
         protected _renderTargets: SmartArray<RenderTargetTexture>;
         protected _worldViewProjectionMatrix: Matrix;
         protected _globalAmbientColor: Color3;
@@ -29899,6 +30061,11 @@ declare module BABYLON {
         static get DiffuseTextureEnabled(): boolean;
         static set DiffuseTextureEnabled(value: boolean);
         /**
+         * Are detail textures enabled in the application.
+         */
+        static get DetailTextureEnabled(): boolean;
+        static set DetailTextureEnabled(value: boolean);
+        /**
          * Are ambient textures enabled in the application.
          */
         static get AmbientTextureEnabled(): boolean;
@@ -54770,7 +54937,7 @@ declare module BABYLON {
      * Manages the defines for the PBR Material.
      * @hidden
      */
-    export class PBRMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines, IMaterialClearCoatDefines, IMaterialAnisotropicDefines, IMaterialBRDFDefines, IMaterialSheenDefines, IMaterialSubSurfaceDefines {
+    export class PBRMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines, IMaterialClearCoatDefines, IMaterialAnisotropicDefines, IMaterialBRDFDefines, IMaterialSheenDefines, IMaterialSubSurfaceDefines, IMaterialDetailMapDefines {
         PBR: boolean;
         NUM_SAMPLES: string;
         REALTIME_FILTERING: boolean;
@@ -54782,6 +54949,9 @@ declare module BABYLON {
         GAMMAALBEDO: boolean;
         ALBEDODIRECTUV: number;
         VERTEXCOLOR: boolean;
+        DETAIL: boolean;
+        DETAILDIRECTUV: number;
+        DETAIL_NORMALBLENDMETHOD: number;
         AMBIENT: boolean;
         AMBIENTDIRECTUV: number;
         AMBIENTINGRAYSCALE: boolean;
@@ -55346,6 +55516,10 @@ declare module BABYLON {
          * Defines the SubSurface parameters for the material.
          */
         readonly subSurface: PBRSubSurfaceConfiguration;
+        /**
+         * Defines the detail map parameters for the material.
+         */
+        readonly detailMap: DetailMapConfiguration;
         protected _rebuildInParallel: boolean;
         /**
          * Instantiates a new PBRMaterial instance.

Plik diff jest za duży
+ 2 - 2
dist/preview release/babylon.js


Plik diff jest za duży
+ 351 - 41
dist/preview release/babylon.max.js


Plik diff jest za duży
+ 1 - 1
dist/preview release/babylon.max.js.map


+ 359 - 4
dist/preview release/babylon.module.d.ts

@@ -24638,6 +24638,16 @@ declare module "babylonjs/Materials/material" {
          */
         static readonly MATERIAL_ALPHATESTANDBLEND: number;
         /**
+         * The Whiteout method is used to blend normals.
+         * Details of the algorithm can be found here: https://blog.selfshadow.com/publications/blending-in-detail/
+         */
+        static readonly MATERIAL_NORMALBLENDMETHOD_WHITEOUT: number;
+        /**
+         * 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/
+         */
+        static readonly MATERIAL_NORMALBLENDMETHOD_RNM: number;
+        /**
          * Custom callback helping to override the default shader used in the material.
          */
         customShaderNameResolve: (shaderName: string, uniforms: string[], uniformBuffers: string[], samplers: string[], defines: MaterialDefines | string[], attributes?: string[], options?: ICustomShaderNameResolveOptions) => string;
@@ -30064,6 +30074,12 @@ declare module "babylonjs/Materials/materialFlags" {
          */
         static get DiffuseTextureEnabled(): boolean;
         static set DiffuseTextureEnabled(value: boolean);
+        private static _DetailTextureEnabled;
+        /**
+         * Are detail textures enabled in the application.
+         */
+        static get DetailTextureEnabled(): boolean;
+        static set DetailTextureEnabled(value: boolean);
         private static _AmbientTextureEnabled;
         /**
          * Are ambient textures enabled in the application.
@@ -30367,6 +30383,150 @@ declare module "babylonjs/Shaders/default.vertex" {
         shader: string;
     };
 }
+declare module "babylonjs/Materials/material.detailMapConfiguration" {
+    import { Nullable } from "babylonjs/types";
+    import { Scene } from "babylonjs/scene";
+    import { BaseTexture } from "babylonjs/Materials/Textures/baseTexture";
+    import { UniformBuffer } from "babylonjs/Materials/uniformBuffer";
+    import { IAnimatable } from "babylonjs/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;
+        /**
+         * The detail texture of the material.
+         */
+        texture: Nullable<BaseTexture>;
+        /**
+         * Defines how strongly the detail diffuse/albedo channel is blended with the regular diffuse/albedo texture
+         * Bigger values mean stronger blending
+         */
+        diffuseBlendLevel: number;
+        /**
+         * Defines how strongly the detail roughness channel is blended with the regular roughness value
+         * Bigger values mean stronger blending. Only used with PBR materials
+         */
+        roughnessBlendLevel: number;
+        /**
+         * Defines how strong the bump effect from the detail map is
+         * Bigger values mean stronger effect
+         */
+        bumpLevel: number;
+        private _normalBlendMethod;
+        /**
+         * The method used to blend the bump and detail normals together
+         */
+        normalBlendMethod: number;
+        private _isEnabled;
+        /**
+         * Enable or disable the detail map on this material
+         */
+        isEnabled: boolean;
+        /** @hidden */
+        private _internalMarkAllSubMeshesAsTexturesDirty;
+        /** @hidden */
+        _markAllSubMeshesAsTexturesDirty(): void;
+        /**
+         * Instantiate a new detail map
+         * @param markAllSubMeshesAsTexturesDirty Callback to flag the material to dirty
+         */
+        constructor(markAllSubMeshesAsTexturesDirty: () => void);
+        /**
+         * 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.
+         */
+        isReadyForSubMesh(defines: IMaterialDetailMapDefines, scene: Scene): boolean;
+        /**
+         * Update the defines for detail map usage
+         * @param defines the list of "defines" to update.
+         * @param scene defines the scene the material belongs to.
+         */
+        prepareDefines(defines: IMaterialDetailMapDefines, scene: Scene): void;
+        /**
+         * 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.
+         */
+        bindForSubMesh(uniformBuffer: UniformBuffer, scene: Scene, isFrozen: boolean): void;
+        /**
+         * 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.
+         */
+        hasTexture(texture: BaseTexture): boolean;
+        /**
+         * Returns an array of the actively used textures.
+         * @param activeTextures Array of BaseTextures
+         */
+        getActiveTextures(activeTextures: BaseTexture[]): void;
+        /**
+         * Returns the animatable textures.
+         * @param animatables Array of animatable textures.
+         */
+        getAnimatables(animatables: IAnimatable[]): void;
+        /**
+         * Disposes the resources of the material.
+         * @param forceDisposeTextures - Forces the disposal of all textures.
+         */
+        dispose(forceDisposeTextures?: boolean): void;
+        /**
+        * Get the current class name useful for serialization or dynamic coding.
+        * @returns "DetailMap"
+        */
+        getClassName(): string;
+        /**
+         * Add the required uniforms to the current list.
+         * @param uniforms defines the current uniform list.
+         */
+        static AddUniforms(uniforms: string[]): void;
+        /**
+         * Add the required samplers to the current list.
+         * @param samplers defines the current sampler list.
+         */
+        static AddSamplers(samplers: string[]): void;
+        /**
+         * Add the required uniforms to the current buffer.
+         * @param uniformBuffer defines the current uniform buffer.
+         */
+        static PrepareUniformBuffer(uniformBuffer: UniformBuffer): void;
+        /**
+         * Makes a duplicate of the current instance into another one.
+         * @param detailMap define the instance where to copy the info
+         */
+        copyTo(detailMap: DetailMapConfiguration): void;
+        /**
+         * Serializes this detail map instance
+         * @returns - An object with the serialized instance.
+         */
+        serialize(): any;
+        /**
+         * 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
+         */
+        parse(source: any, scene: Scene, rootUrl: string): void;
+    }
+}
 declare module "babylonjs/Materials/standardMaterial" {
     import { SmartArray } from "babylonjs/Misc/smartArray";
     import { IAnimatable } from "babylonjs/Animations/animatable.interface";
@@ -30386,12 +30546,16 @@ declare module "babylonjs/Materials/standardMaterial" {
     import { RenderTargetTexture } from "babylonjs/Materials/Textures/renderTargetTexture";
     import "babylonjs/Shaders/default.fragment";
     import "babylonjs/Shaders/default.vertex";
+    import { IMaterialDetailMapDefines, DetailMapConfiguration } from "babylonjs/Materials/material.detailMapConfiguration";
     /** @hidden */
-    export class StandardMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines {
+    export class StandardMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines, IMaterialDetailMapDefines {
         MAINUV1: boolean;
         MAINUV2: boolean;
         DIFFUSE: boolean;
         DIFFUSEDIRECTUV: number;
+        DETAIL: boolean;
+        DETAILDIRECTUV: number;
+        DETAIL_NORMALBLENDMETHOD: number;
         AMBIENT: boolean;
         AMBIENTDIRECTUV: number;
         OPACITY: boolean;
@@ -30817,6 +30981,10 @@ declare module "babylonjs/Materials/standardMaterial" {
          * corresponding to low luminance, medium luminance, and high luminance areas respectively.
          */
         set cameraColorCurves(value: Nullable<ColorCurves>);
+        /**
+         * Defines the detail map parameters for the material.
+         */
+        readonly detailMap: DetailMapConfiguration;
         protected _renderTargets: SmartArray<RenderTargetTexture>;
         protected _worldViewProjectionMatrix: Matrix;
         protected _globalAmbientColor: Color3;
@@ -30936,6 +31104,11 @@ declare module "babylonjs/Materials/standardMaterial" {
         static get DiffuseTextureEnabled(): boolean;
         static set DiffuseTextureEnabled(value: boolean);
         /**
+         * Are detail textures enabled in the application.
+         */
+        static get DetailTextureEnabled(): boolean;
+        static set DetailTextureEnabled(value: boolean);
+        /**
          * Are ambient textures enabled in the application.
          */
         static get AmbientTextureEnabled(): boolean;
@@ -57215,11 +57388,12 @@ declare module "babylonjs/Materials/PBR/pbrBaseMaterial" {
     import "babylonjs/Materials/Textures/baseTexture.polynomial";
     import "babylonjs/Shaders/pbr.fragment";
     import "babylonjs/Shaders/pbr.vertex";
+    import { IMaterialDetailMapDefines, DetailMapConfiguration } from "babylonjs/Materials/material.detailMapConfiguration";
     /**
      * Manages the defines for the PBR Material.
      * @hidden
      */
-    export class PBRMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines, IMaterialClearCoatDefines, IMaterialAnisotropicDefines, IMaterialBRDFDefines, IMaterialSheenDefines, IMaterialSubSurfaceDefines {
+    export class PBRMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines, IMaterialClearCoatDefines, IMaterialAnisotropicDefines, IMaterialBRDFDefines, IMaterialSheenDefines, IMaterialSubSurfaceDefines, IMaterialDetailMapDefines {
         PBR: boolean;
         NUM_SAMPLES: string;
         REALTIME_FILTERING: boolean;
@@ -57231,6 +57405,9 @@ declare module "babylonjs/Materials/PBR/pbrBaseMaterial" {
         GAMMAALBEDO: boolean;
         ALBEDODIRECTUV: number;
         VERTEXCOLOR: boolean;
+        DETAIL: boolean;
+        DETAILDIRECTUV: number;
+        DETAIL_NORMALBLENDMETHOD: number;
         AMBIENT: boolean;
         AMBIENTDIRECTUV: number;
         AMBIENTINGRAYSCALE: boolean;
@@ -57795,6 +57972,10 @@ declare module "babylonjs/Materials/PBR/pbrBaseMaterial" {
          * Defines the SubSurface parameters for the material.
          */
         readonly subSurface: PBRSubSurfaceConfiguration;
+        /**
+         * Defines the detail map parameters for the material.
+         */
+        readonly detailMap: DetailMapConfiguration;
         protected _rebuildInParallel: boolean;
         /**
          * Instantiates a new PBRMaterial instance.
@@ -101318,6 +101499,16 @@ declare module BABYLON {
          */
         static readonly MATERIAL_ALPHATESTANDBLEND: number;
         /**
+         * The Whiteout method is used to blend normals.
+         * Details of the algorithm can be found here: https://blog.selfshadow.com/publications/blending-in-detail/
+         */
+        static readonly MATERIAL_NORMALBLENDMETHOD_WHITEOUT: number;
+        /**
+         * 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/
+         */
+        static readonly MATERIAL_NORMALBLENDMETHOD_RNM: number;
+        /**
          * Custom callback helping to override the default shader used in the material.
          */
         customShaderNameResolve: (shaderName: string, uniforms: string[], uniformBuffers: string[], samplers: string[], defines: MaterialDefines | string[], attributes?: string[], options?: ICustomShaderNameResolveOptions) => string;
@@ -106626,6 +106817,12 @@ declare module BABYLON {
          */
         static get DiffuseTextureEnabled(): boolean;
         static set DiffuseTextureEnabled(value: boolean);
+        private static _DetailTextureEnabled;
+        /**
+         * Are detail textures enabled in the application.
+         */
+        static get DetailTextureEnabled(): boolean;
+        static set DetailTextureEnabled(value: boolean);
         private static _AmbientTextureEnabled;
         /**
          * Are ambient textures enabled in the application.
@@ -106886,12 +107083,154 @@ declare module BABYLON {
     };
 }
 declare module BABYLON {
+    /**
+     * @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;
+        /**
+         * The detail texture of the material.
+         */
+        texture: Nullable<BaseTexture>;
+        /**
+         * Defines how strongly the detail diffuse/albedo channel is blended with the regular diffuse/albedo texture
+         * Bigger values mean stronger blending
+         */
+        diffuseBlendLevel: number;
+        /**
+         * Defines how strongly the detail roughness channel is blended with the regular roughness value
+         * Bigger values mean stronger blending. Only used with PBR materials
+         */
+        roughnessBlendLevel: number;
+        /**
+         * Defines how strong the bump effect from the detail map is
+         * Bigger values mean stronger effect
+         */
+        bumpLevel: number;
+        private _normalBlendMethod;
+        /**
+         * The method used to blend the bump and detail normals together
+         */
+        normalBlendMethod: number;
+        private _isEnabled;
+        /**
+         * Enable or disable the detail map on this material
+         */
+        isEnabled: boolean;
+        /** @hidden */
+        private _internalMarkAllSubMeshesAsTexturesDirty;
+        /** @hidden */
+        _markAllSubMeshesAsTexturesDirty(): void;
+        /**
+         * Instantiate a new detail map
+         * @param markAllSubMeshesAsTexturesDirty Callback to flag the material to dirty
+         */
+        constructor(markAllSubMeshesAsTexturesDirty: () => void);
+        /**
+         * 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.
+         */
+        isReadyForSubMesh(defines: IMaterialDetailMapDefines, scene: Scene): boolean;
+        /**
+         * Update the defines for detail map usage
+         * @param defines the list of "defines" to update.
+         * @param scene defines the scene the material belongs to.
+         */
+        prepareDefines(defines: IMaterialDetailMapDefines, scene: Scene): void;
+        /**
+         * 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.
+         */
+        bindForSubMesh(uniformBuffer: UniformBuffer, scene: Scene, isFrozen: boolean): void;
+        /**
+         * 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.
+         */
+        hasTexture(texture: BaseTexture): boolean;
+        /**
+         * Returns an array of the actively used textures.
+         * @param activeTextures Array of BaseTextures
+         */
+        getActiveTextures(activeTextures: BaseTexture[]): void;
+        /**
+         * Returns the animatable textures.
+         * @param animatables Array of animatable textures.
+         */
+        getAnimatables(animatables: IAnimatable[]): void;
+        /**
+         * Disposes the resources of the material.
+         * @param forceDisposeTextures - Forces the disposal of all textures.
+         */
+        dispose(forceDisposeTextures?: boolean): void;
+        /**
+        * Get the current class name useful for serialization or dynamic coding.
+        * @returns "DetailMap"
+        */
+        getClassName(): string;
+        /**
+         * Add the required uniforms to the current list.
+         * @param uniforms defines the current uniform list.
+         */
+        static AddUniforms(uniforms: string[]): void;
+        /**
+         * Add the required samplers to the current list.
+         * @param samplers defines the current sampler list.
+         */
+        static AddSamplers(samplers: string[]): void;
+        /**
+         * Add the required uniforms to the current buffer.
+         * @param uniformBuffer defines the current uniform buffer.
+         */
+        static PrepareUniformBuffer(uniformBuffer: UniformBuffer): void;
+        /**
+         * Makes a duplicate of the current instance into another one.
+         * @param detailMap define the instance where to copy the info
+         */
+        copyTo(detailMap: DetailMapConfiguration): void;
+        /**
+         * Serializes this detail map instance
+         * @returns - An object with the serialized instance.
+         */
+        serialize(): any;
+        /**
+         * 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
+         */
+        parse(source: any, scene: Scene, rootUrl: string): void;
+    }
+}
+declare module BABYLON {
     /** @hidden */
-    export class StandardMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines {
+    export class StandardMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines, IMaterialDetailMapDefines {
         MAINUV1: boolean;
         MAINUV2: boolean;
         DIFFUSE: boolean;
         DIFFUSEDIRECTUV: number;
+        DETAIL: boolean;
+        DETAILDIRECTUV: number;
+        DETAIL_NORMALBLENDMETHOD: number;
         AMBIENT: boolean;
         AMBIENTDIRECTUV: number;
         OPACITY: boolean;
@@ -107317,6 +107656,10 @@ declare module BABYLON {
          * corresponding to low luminance, medium luminance, and high luminance areas respectively.
          */
         set cameraColorCurves(value: Nullable<ColorCurves>);
+        /**
+         * Defines the detail map parameters for the material.
+         */
+        readonly detailMap: DetailMapConfiguration;
         protected _renderTargets: SmartArray<RenderTargetTexture>;
         protected _worldViewProjectionMatrix: Matrix;
         protected _globalAmbientColor: Color3;
@@ -107436,6 +107779,11 @@ declare module BABYLON {
         static get DiffuseTextureEnabled(): boolean;
         static set DiffuseTextureEnabled(value: boolean);
         /**
+         * Are detail textures enabled in the application.
+         */
+        static get DetailTextureEnabled(): boolean;
+        static set DetailTextureEnabled(value: boolean);
+        /**
          * Are ambient textures enabled in the application.
          */
         static get AmbientTextureEnabled(): boolean;
@@ -132307,7 +132655,7 @@ declare module BABYLON {
      * Manages the defines for the PBR Material.
      * @hidden
      */
-    export class PBRMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines, IMaterialClearCoatDefines, IMaterialAnisotropicDefines, IMaterialBRDFDefines, IMaterialSheenDefines, IMaterialSubSurfaceDefines {
+    export class PBRMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines, IMaterialClearCoatDefines, IMaterialAnisotropicDefines, IMaterialBRDFDefines, IMaterialSheenDefines, IMaterialSubSurfaceDefines, IMaterialDetailMapDefines {
         PBR: boolean;
         NUM_SAMPLES: string;
         REALTIME_FILTERING: boolean;
@@ -132319,6 +132667,9 @@ declare module BABYLON {
         GAMMAALBEDO: boolean;
         ALBEDODIRECTUV: number;
         VERTEXCOLOR: boolean;
+        DETAIL: boolean;
+        DETAILDIRECTUV: number;
+        DETAIL_NORMALBLENDMETHOD: number;
         AMBIENT: boolean;
         AMBIENTDIRECTUV: number;
         AMBIENTINGRAYSCALE: boolean;
@@ -132883,6 +133234,10 @@ declare module BABYLON {
          * Defines the SubSurface parameters for the material.
          */
         readonly subSurface: PBRSubSurfaceConfiguration;
+        /**
+         * Defines the detail map parameters for the material.
+         */
+        readonly detailMap: DetailMapConfiguration;
         protected _rebuildInParallel: boolean;
         /**
          * Instantiates a new PBRMaterial instance.

+ 176 - 2
dist/preview release/documentation.d.ts

@@ -23781,6 +23781,16 @@ declare module BABYLON {
          */
         static readonly MATERIAL_ALPHATESTANDBLEND: number;
         /**
+         * The Whiteout method is used to blend normals.
+         * Details of the algorithm can be found here: https://blog.selfshadow.com/publications/blending-in-detail/
+         */
+        static readonly MATERIAL_NORMALBLENDMETHOD_WHITEOUT: number;
+        /**
+         * 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/
+         */
+        static readonly MATERIAL_NORMALBLENDMETHOD_RNM: number;
+        /**
          * Custom callback helping to override the default shader used in the material.
          */
         customShaderNameResolve: (shaderName: string, uniforms: string[], uniformBuffers: string[], samplers: string[], defines: MaterialDefines | string[], attributes?: string[], options?: ICustomShaderNameResolveOptions) => string;
@@ -29089,6 +29099,12 @@ declare module BABYLON {
          */
         static get DiffuseTextureEnabled(): boolean;
         static set DiffuseTextureEnabled(value: boolean);
+        private static _DetailTextureEnabled;
+        /**
+         * Are detail textures enabled in the application.
+         */
+        static get DetailTextureEnabled(): boolean;
+        static set DetailTextureEnabled(value: boolean);
         private static _AmbientTextureEnabled;
         /**
          * Are ambient textures enabled in the application.
@@ -29349,12 +29365,154 @@ declare module BABYLON {
     };
 }
 declare module BABYLON {
+    /**
+     * @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;
+        /**
+         * The detail texture of the material.
+         */
+        texture: Nullable<BaseTexture>;
+        /**
+         * Defines how strongly the detail diffuse/albedo channel is blended with the regular diffuse/albedo texture
+         * Bigger values mean stronger blending
+         */
+        diffuseBlendLevel: number;
+        /**
+         * Defines how strongly the detail roughness channel is blended with the regular roughness value
+         * Bigger values mean stronger blending. Only used with PBR materials
+         */
+        roughnessBlendLevel: number;
+        /**
+         * Defines how strong the bump effect from the detail map is
+         * Bigger values mean stronger effect
+         */
+        bumpLevel: number;
+        private _normalBlendMethod;
+        /**
+         * The method used to blend the bump and detail normals together
+         */
+        normalBlendMethod: number;
+        private _isEnabled;
+        /**
+         * Enable or disable the detail map on this material
+         */
+        isEnabled: boolean;
+        /** @hidden */
+        private _internalMarkAllSubMeshesAsTexturesDirty;
+        /** @hidden */
+        _markAllSubMeshesAsTexturesDirty(): void;
+        /**
+         * Instantiate a new detail map
+         * @param markAllSubMeshesAsTexturesDirty Callback to flag the material to dirty
+         */
+        constructor(markAllSubMeshesAsTexturesDirty: () => void);
+        /**
+         * 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.
+         */
+        isReadyForSubMesh(defines: IMaterialDetailMapDefines, scene: Scene): boolean;
+        /**
+         * Update the defines for detail map usage
+         * @param defines the list of "defines" to update.
+         * @param scene defines the scene the material belongs to.
+         */
+        prepareDefines(defines: IMaterialDetailMapDefines, scene: Scene): void;
+        /**
+         * 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.
+         */
+        bindForSubMesh(uniformBuffer: UniformBuffer, scene: Scene, isFrozen: boolean): void;
+        /**
+         * 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.
+         */
+        hasTexture(texture: BaseTexture): boolean;
+        /**
+         * Returns an array of the actively used textures.
+         * @param activeTextures Array of BaseTextures
+         */
+        getActiveTextures(activeTextures: BaseTexture[]): void;
+        /**
+         * Returns the animatable textures.
+         * @param animatables Array of animatable textures.
+         */
+        getAnimatables(animatables: IAnimatable[]): void;
+        /**
+         * Disposes the resources of the material.
+         * @param forceDisposeTextures - Forces the disposal of all textures.
+         */
+        dispose(forceDisposeTextures?: boolean): void;
+        /**
+        * Get the current class name useful for serialization or dynamic coding.
+        * @returns "DetailMap"
+        */
+        getClassName(): string;
+        /**
+         * Add the required uniforms to the current list.
+         * @param uniforms defines the current uniform list.
+         */
+        static AddUniforms(uniforms: string[]): void;
+        /**
+         * Add the required samplers to the current list.
+         * @param samplers defines the current sampler list.
+         */
+        static AddSamplers(samplers: string[]): void;
+        /**
+         * Add the required uniforms to the current buffer.
+         * @param uniformBuffer defines the current uniform buffer.
+         */
+        static PrepareUniformBuffer(uniformBuffer: UniformBuffer): void;
+        /**
+         * Makes a duplicate of the current instance into another one.
+         * @param detailMap define the instance where to copy the info
+         */
+        copyTo(detailMap: DetailMapConfiguration): void;
+        /**
+         * Serializes this detail map instance
+         * @returns - An object with the serialized instance.
+         */
+        serialize(): any;
+        /**
+         * 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
+         */
+        parse(source: any, scene: Scene, rootUrl: string): void;
+    }
+}
+declare module BABYLON {
     /** @hidden */
-    export class StandardMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines {
+    export class StandardMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines, IMaterialDetailMapDefines {
         MAINUV1: boolean;
         MAINUV2: boolean;
         DIFFUSE: boolean;
         DIFFUSEDIRECTUV: number;
+        DETAIL: boolean;
+        DETAILDIRECTUV: number;
+        DETAIL_NORMALBLENDMETHOD: number;
         AMBIENT: boolean;
         AMBIENTDIRECTUV: number;
         OPACITY: boolean;
@@ -29780,6 +29938,10 @@ declare module BABYLON {
          * corresponding to low luminance, medium luminance, and high luminance areas respectively.
          */
         set cameraColorCurves(value: Nullable<ColorCurves>);
+        /**
+         * Defines the detail map parameters for the material.
+         */
+        readonly detailMap: DetailMapConfiguration;
         protected _renderTargets: SmartArray<RenderTargetTexture>;
         protected _worldViewProjectionMatrix: Matrix;
         protected _globalAmbientColor: Color3;
@@ -29899,6 +30061,11 @@ declare module BABYLON {
         static get DiffuseTextureEnabled(): boolean;
         static set DiffuseTextureEnabled(value: boolean);
         /**
+         * Are detail textures enabled in the application.
+         */
+        static get DetailTextureEnabled(): boolean;
+        static set DetailTextureEnabled(value: boolean);
+        /**
          * Are ambient textures enabled in the application.
          */
         static get AmbientTextureEnabled(): boolean;
@@ -54770,7 +54937,7 @@ declare module BABYLON {
      * Manages the defines for the PBR Material.
      * @hidden
      */
-    export class PBRMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines, IMaterialClearCoatDefines, IMaterialAnisotropicDefines, IMaterialBRDFDefines, IMaterialSheenDefines, IMaterialSubSurfaceDefines {
+    export class PBRMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines, IMaterialClearCoatDefines, IMaterialAnisotropicDefines, IMaterialBRDFDefines, IMaterialSheenDefines, IMaterialSubSurfaceDefines, IMaterialDetailMapDefines {
         PBR: boolean;
         NUM_SAMPLES: string;
         REALTIME_FILTERING: boolean;
@@ -54782,6 +54949,9 @@ declare module BABYLON {
         GAMMAALBEDO: boolean;
         ALBEDODIRECTUV: number;
         VERTEXCOLOR: boolean;
+        DETAIL: boolean;
+        DETAILDIRECTUV: number;
+        DETAIL_NORMALBLENDMETHOD: number;
         AMBIENT: boolean;
         AMBIENTDIRECTUV: number;
         AMBIENTINGRAYSCALE: boolean;
@@ -55346,6 +55516,10 @@ declare module BABYLON {
          * Defines the SubSurface parameters for the material.
          */
         readonly subSurface: PBRSubSurfaceConfiguration;
+        /**
+         * Defines the detail map parameters for the material.
+         */
+        readonly detailMap: DetailMapConfiguration;
         protected _rebuildInParallel: boolean;
         /**
          * Instantiates a new PBRMaterial instance.

Plik diff jest za duży
+ 4 - 4
dist/preview release/inspector/babylon.inspector.bundle.js


Plik diff jest za duży
+ 215 - 137
dist/preview release/inspector/babylon.inspector.bundle.max.js


Plik diff jest za duży
+ 1 - 1
dist/preview release/inspector/babylon.inspector.bundle.max.js.map


+ 28 - 3
dist/preview release/inspector/babylon.inspector.d.ts

@@ -687,6 +687,7 @@ declare module INSPECTOR {
         entity: BABYLON.IAnimatable;
         onPropertyChangedObservable?: BABYLON.Observable<PropertyChangedEvent>;
         setNotificationMessage: (message: string) => void;
+        changed: () => void;
     }
     export class AddAnimation extends React.Component<IAddAnimationProps, {
         animationName: string;
@@ -708,6 +709,33 @@ declare module INSPECTOR {
     }
 }
 declare module INSPECTOR {
+    interface IEditorControlsProps {
+        isTargetedAnimation: boolean;
+        entity: BABYLON.IAnimatable | BABYLON.TargetedAnimation;
+        selected: BABYLON.Animation | null;
+        onPropertyChangedObservable?: BABYLON.Observable<PropertyChangedEvent>;
+        setNotificationMessage: (message: string) => void;
+        selectAnimation: (selected: BABYLON.Animation) => void;
+    }
+    export class EditorControls extends React.Component<IEditorControlsProps, {
+        isAnimationTabOpen: boolean;
+        isEditTabOpen: boolean;
+        isLoadTabOpen: boolean;
+        isSaveTabOpen: boolean;
+        isLoopActive: boolean;
+        animationsCount: number;
+        framesPerSecond: number;
+    }> {
+        constructor(props: IEditorControlsProps);
+        animationsChanged(): void;
+        deleteAnimation(): void;
+        setListItem(animation: BABYLON.Animation, i: number): JSX.Element | null;
+        handleTabs(tab: number): void;
+        handleChangeFps(fps: number): void;
+        render(): JSX.Element;
+    }
+}
+declare module INSPECTOR {
     interface IAnimationCurveEditorComponentProps {
         close: (event: any) => void;
         playOrPause?: () => void;
@@ -738,7 +766,6 @@ declare module INSPECTOR {
         lastFrame: number;
         playheadPos: number;
         isPlaying: boolean;
-        isAnimationDialogOpen: boolean;
     }> {
         private _heightScale;
         readonly _entityName: string;
@@ -766,8 +793,6 @@ declare module INSPECTOR {
         setAxesLength(): void;
         getValueLabel(i: number): number;
         resetPlayheadOffset(): void;
-        setListItem(animation: BABYLON.Animation, i: number): JSX.Element | null;
-        deleteAnimation(): void;
         /**
         * Keyframe Manipulation
         * This section handles events from SvgDraggableArea.

+ 62 - 6
dist/preview release/inspector/babylon.inspector.module.d.ts

@@ -774,6 +774,7 @@ declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/ani
         entity: IAnimatable;
         onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
         setNotificationMessage: (message: string) => void;
+        changed: () => void;
     }
     export class AddAnimation extends React.Component<IAddAnimationProps, {
         animationName: string;
@@ -794,6 +795,39 @@ declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/ani
         render(): JSX.Element;
     }
 }
+declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/animations/editorControls" {
+    import * as React from "react";
+    import { Observable } from "babylonjs/Misc/observable";
+    import { PropertyChangedEvent } from "babylonjs-inspector/components/propertyChangedEvent";
+    import { Animation } from 'babylonjs/Animations/animation';
+    import { IAnimatable } from 'babylonjs/Animations/animatable.interface';
+    import { TargetedAnimation } from "babylonjs/Animations/animationGroup";
+    interface IEditorControlsProps {
+        isTargetedAnimation: boolean;
+        entity: IAnimatable | TargetedAnimation;
+        selected: Animation | null;
+        onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
+        setNotificationMessage: (message: string) => void;
+        selectAnimation: (selected: Animation) => void;
+    }
+    export class EditorControls extends React.Component<IEditorControlsProps, {
+        isAnimationTabOpen: boolean;
+        isEditTabOpen: boolean;
+        isLoadTabOpen: boolean;
+        isSaveTabOpen: boolean;
+        isLoopActive: boolean;
+        animationsCount: number;
+        framesPerSecond: number;
+    }> {
+        constructor(props: IEditorControlsProps);
+        animationsChanged(): void;
+        deleteAnimation(): void;
+        setListItem(animation: Animation, i: number): JSX.Element | null;
+        handleTabs(tab: number): void;
+        handleChangeFps(fps: number): void;
+        render(): JSX.Element;
+    }
+}
 declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/animations/animationCurveEditorComponent" {
     import * as React from "react";
     import { Animation } from 'babylonjs/Animations/animation';
@@ -834,7 +868,6 @@ declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/ani
         lastFrame: number;
         playheadPos: number;
         isPlaying: boolean;
-        isAnimationDialogOpen: boolean;
     }> {
         private _heightScale;
         readonly _entityName: string;
@@ -862,8 +895,6 @@ declare module "babylonjs-inspector/components/actionTabs/tabs/propertyGrids/ani
         setAxesLength(): void;
         getValueLabel(i: number): number;
         resetPlayheadOffset(): void;
-        setListItem(animation: Animation, i: number): JSX.Element | null;
-        deleteAnimation(): void;
         /**
         * Keyframe Manipulation
         * This section handles events from SvgDraggableArea.
@@ -4018,6 +4049,7 @@ declare module INSPECTOR {
         entity: BABYLON.IAnimatable;
         onPropertyChangedObservable?: BABYLON.Observable<PropertyChangedEvent>;
         setNotificationMessage: (message: string) => void;
+        changed: () => void;
     }
     export class AddAnimation extends React.Component<IAddAnimationProps, {
         animationName: string;
@@ -4039,6 +4071,33 @@ declare module INSPECTOR {
     }
 }
 declare module INSPECTOR {
+    interface IEditorControlsProps {
+        isTargetedAnimation: boolean;
+        entity: BABYLON.IAnimatable | BABYLON.TargetedAnimation;
+        selected: BABYLON.Animation | null;
+        onPropertyChangedObservable?: BABYLON.Observable<PropertyChangedEvent>;
+        setNotificationMessage: (message: string) => void;
+        selectAnimation: (selected: BABYLON.Animation) => void;
+    }
+    export class EditorControls extends React.Component<IEditorControlsProps, {
+        isAnimationTabOpen: boolean;
+        isEditTabOpen: boolean;
+        isLoadTabOpen: boolean;
+        isSaveTabOpen: boolean;
+        isLoopActive: boolean;
+        animationsCount: number;
+        framesPerSecond: number;
+    }> {
+        constructor(props: IEditorControlsProps);
+        animationsChanged(): void;
+        deleteAnimation(): void;
+        setListItem(animation: BABYLON.Animation, i: number): JSX.Element | null;
+        handleTabs(tab: number): void;
+        handleChangeFps(fps: number): void;
+        render(): JSX.Element;
+    }
+}
+declare module INSPECTOR {
     interface IAnimationCurveEditorComponentProps {
         close: (event: any) => void;
         playOrPause?: () => void;
@@ -4069,7 +4128,6 @@ declare module INSPECTOR {
         lastFrame: number;
         playheadPos: number;
         isPlaying: boolean;
-        isAnimationDialogOpen: boolean;
     }> {
         private _heightScale;
         readonly _entityName: string;
@@ -4097,8 +4155,6 @@ declare module INSPECTOR {
         setAxesLength(): void;
         getValueLabel(i: number): number;
         resetPlayheadOffset(): void;
-        setListItem(animation: BABYLON.Animation, i: number): JSX.Element | null;
-        deleteAnimation(): void;
         /**
         * Keyframe Manipulation
         * This section handles events from SvgDraggableArea.

+ 4 - 4
dist/preview release/nodeEditor/babylon.nodeEditor.d.ts

@@ -81,8 +81,8 @@ declare module NODEEDITOR {
 }
 declare module NODEEDITOR {
     export class SerializationTools {
-        static UpdateLocations(material: BABYLON.NodeMaterial, globalState: GlobalState): void;
-        static Serialize(material: BABYLON.NodeMaterial, globalState: GlobalState, selectedBlocks?: BABYLON.NodeMaterialBlock[]): string;
+        static UpdateLocations(material: BABYLON.NodeMaterial, globalState: GlobalState, frame?: BABYLON.Nullable<GraphFrame>): void;
+        static Serialize(material: BABYLON.NodeMaterial, globalState: GlobalState, frame?: BABYLON.Nullable<GraphFrame>): string;
         static Deserialize(serializationObject: any, globalState: GlobalState): void;
     }
 }
@@ -1202,7 +1202,7 @@ declare module NODEEDITOR {
         hostElement: HTMLElement;
         hostDocument: HTMLDocument;
         hostWindow: Window;
-        onSelectionChangedObservable: BABYLON.Observable<BABYLON.Nullable<GraphNode | NodePort | GraphFrame | NodeLink | FramePortData>>;
+        onSelectionChangedObservable: BABYLON.Observable<BABYLON.Nullable<GraphFrame | GraphNode | NodePort | NodeLink | FramePortData>>;
         onRebuildRequiredObservable: BABYLON.Observable<void>;
         onBuiltObservable: BABYLON.Observable<void>;
         onResetRequiredObservable: BABYLON.Observable<void>;
@@ -1238,7 +1238,7 @@ declare module NODEEDITOR {
         directionalLight0: boolean;
         directionalLight1: boolean;
         controlCamera: boolean;
-        storeEditorData: (serializationObject: any) => void;
+        storeEditorData: (serializationObject: any, frame?: BABYLON.Nullable<GraphFrame>) => void;
         _mode: BABYLON.NodeMaterialModes;
         /** Gets the mode */
         get mode(): BABYLON.NodeMaterialModes;

Plik diff jest za duży
+ 1 - 1
dist/preview release/nodeEditor/babylon.nodeEditor.js


+ 25 - 15
dist/preview release/nodeEditor/babylon.nodeEditor.max.js

@@ -62540,14 +62540,19 @@ var GraphCanvasComponent = /** @class */ (function (_super) {
             _this._ctrlKeyIsPressed = false;
         }, false);
         // Store additional data to serialization object
-        _this.props.globalState.storeEditorData = function (editorData) {
-            editorData.zoom = _this.zoom;
-            editorData.x = _this.x;
-            editorData.y = _this.y;
+        _this.props.globalState.storeEditorData = function (editorData, graphFrame) {
             editorData.frames = [];
-            for (var _i = 0, _a = _this._frames; _i < _a.length; _i++) {
-                var frame = _a[_i];
-                editorData.frames.push(frame.serialize());
+            if (graphFrame) {
+                editorData.frames.push(graphFrame.serialize());
+            }
+            else {
+                editorData.x = _this.x;
+                editorData.y = _this.y;
+                editorData.zoom = _this.zoom;
+                for (var _i = 0, _a = _this._frames; _i < _a.length; _i++) {
+                    var frame = _a[_i];
+                    editorData.frames.push(frame.serialize());
+                }
             }
         };
         return _this;
@@ -64433,7 +64438,7 @@ var GraphFrame = /** @class */ (function () {
     };
     GraphFrame.prototype.export = function () {
         var state = this._ownerCanvas.globalState;
-        var json = _serializationTools__WEBPACK_IMPORTED_MODULE_1__["SerializationTools"].Serialize(state.nodeMaterial, state, this.nodes.map(function (n) { return n.block; }));
+        var json = _serializationTools__WEBPACK_IMPORTED_MODULE_1__["SerializationTools"].Serialize(state.nodeMaterial, state, this);
         _stringTools__WEBPACK_IMPORTED_MODULE_2__["StringTools"].DownloadAsFile(state.hostDocument, json, this._name + ".json");
     };
     GraphFrame.Parse = function (serializationData, canvas, map) {
@@ -65411,7 +65416,10 @@ var FramePropertyTabComponent = /** @class */ (function (_super) {
                     this.props.frame.isCollapsed &&
                         react__WEBPACK_IMPORTED_MODULE_1__["createElement"](_sharedComponents_buttonLineComponent__WEBPACK_IMPORTED_MODULE_5__["ButtonLineComponent"], { label: "Expand", onClick: function () {
                                 _this.props.frame.isCollapsed = false;
-                            } })))));
+                            } }),
+                    react__WEBPACK_IMPORTED_MODULE_1__["createElement"](_sharedComponents_buttonLineComponent__WEBPACK_IMPORTED_MODULE_5__["ButtonLineComponent"], { label: "Export", onClick: function () {
+                            _this.props.frame.export();
+                        } })))));
     };
     return FramePropertyTabComponent;
 }(react__WEBPACK_IMPORTED_MODULE_1__["Component"]));
@@ -67556,13 +67564,14 @@ __webpack_require__.r(__webpack_exports__);
 var SerializationTools = /** @class */ (function () {
     function SerializationTools() {
     }
-    SerializationTools.UpdateLocations = function (material, globalState) {
+    SerializationTools.UpdateLocations = function (material, globalState, frame) {
         material.editorData = {
             locations: []
         };
         // Store node locations
-        for (var _i = 0, _a = material.attachedBlocks; _i < _a.length; _i++) {
-            var block = _a[_i];
+        var blocks = frame ? frame.nodes.map(function (n) { return n.block; }) : material.attachedBlocks;
+        for (var _i = 0, blocks_1 = blocks; _i < blocks_1.length; _i++) {
+            var block = blocks_1[_i];
             var node = globalState.onGetNodeFromBlock(block);
             material.editorData.locations.push({
                 blockId: block.uniqueId,
@@ -67570,12 +67579,13 @@ var SerializationTools = /** @class */ (function () {
                 y: node ? node.y : 0
             });
         }
-        globalState.storeEditorData(material.editorData);
+        globalState.storeEditorData(material.editorData, frame);
     };
-    SerializationTools.Serialize = function (material, globalState, selectedBlocks) {
+    SerializationTools.Serialize = function (material, globalState, frame) {
         var bufferSerializationState = babylonjs_Materials_Textures_texture__WEBPACK_IMPORTED_MODULE_0__["Texture"].SerializeBuffers;
         babylonjs_Materials_Textures_texture__WEBPACK_IMPORTED_MODULE_0__["Texture"].SerializeBuffers = babylonjs_Materials_Textures_texture__WEBPACK_IMPORTED_MODULE_0__["DataStorage"].ReadBoolean("EmbedTextures", true);
-        this.UpdateLocations(material, globalState);
+        this.UpdateLocations(material, globalState, frame);
+        var selectedBlocks = frame ? frame.nodes.map(function (n) { return n.block; }) : undefined;
         var serializationObject = material.serialize(selectedBlocks);
         babylonjs_Materials_Textures_texture__WEBPACK_IMPORTED_MODULE_0__["Texture"].SerializeBuffers = bufferSerializationState;
         return JSON.stringify(serializationObject, undefined, 2);

Plik diff jest za duży
+ 1 - 1
dist/preview release/nodeEditor/babylon.nodeEditor.max.js.map


+ 10 - 9
dist/preview release/nodeEditor/babylon.nodeEditor.module.d.ts

@@ -163,10 +163,11 @@ declare module "babylonjs-node-editor/nodeLocationInfo" {
 declare module "babylonjs-node-editor/serializationTools" {
     import { NodeMaterial } from 'babylonjs/Materials/Node/nodeMaterial';
     import { GlobalState } from "babylonjs-node-editor/globalState";
-    import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
+    import { Nullable } from 'babylonjs/types';
+    import { GraphFrame } from "babylonjs-node-editor/diagram/graphFrame";
     export class SerializationTools {
-        static UpdateLocations(material: NodeMaterial, globalState: GlobalState): void;
-        static Serialize(material: NodeMaterial, globalState: GlobalState, selectedBlocks?: NodeMaterialBlock[]): string;
+        static UpdateLocations(material: NodeMaterial, globalState: GlobalState, frame?: Nullable<GraphFrame>): void;
+        static Serialize(material: NodeMaterial, globalState: GlobalState, frame?: Nullable<GraphFrame>): string;
         static Deserialize(serializationObject: any, globalState: GlobalState): void;
     }
 }
@@ -1478,7 +1479,7 @@ declare module "babylonjs-node-editor/globalState" {
         hostElement: HTMLElement;
         hostDocument: HTMLDocument;
         hostWindow: Window;
-        onSelectionChangedObservable: Observable<Nullable<GraphNode | NodePort | GraphFrame | NodeLink | FramePortData>>;
+        onSelectionChangedObservable: Observable<Nullable<GraphFrame | GraphNode | NodePort | NodeLink | FramePortData>>;
         onRebuildRequiredObservable: Observable<void>;
         onBuiltObservable: Observable<void>;
         onResetRequiredObservable: Observable<void>;
@@ -1514,7 +1515,7 @@ declare module "babylonjs-node-editor/globalState" {
         directionalLight0: boolean;
         directionalLight1: boolean;
         controlCamera: boolean;
-        storeEditorData: (serializationObject: any) => void;
+        storeEditorData: (serializationObject: any, frame?: Nullable<GraphFrame>) => void;
         _mode: NodeMaterialModes;
         /** Gets the mode */
         get mode(): NodeMaterialModes;
@@ -1937,8 +1938,8 @@ declare module NODEEDITOR {
 }
 declare module NODEEDITOR {
     export class SerializationTools {
-        static UpdateLocations(material: BABYLON.NodeMaterial, globalState: GlobalState): void;
-        static Serialize(material: BABYLON.NodeMaterial, globalState: GlobalState, selectedBlocks?: BABYLON.NodeMaterialBlock[]): string;
+        static UpdateLocations(material: BABYLON.NodeMaterial, globalState: GlobalState, frame?: BABYLON.Nullable<GraphFrame>): void;
+        static Serialize(material: BABYLON.NodeMaterial, globalState: GlobalState, frame?: BABYLON.Nullable<GraphFrame>): string;
         static Deserialize(serializationObject: any, globalState: GlobalState): void;
     }
 }
@@ -3058,7 +3059,7 @@ declare module NODEEDITOR {
         hostElement: HTMLElement;
         hostDocument: HTMLDocument;
         hostWindow: Window;
-        onSelectionChangedObservable: BABYLON.Observable<BABYLON.Nullable<GraphNode | NodePort | GraphFrame | NodeLink | FramePortData>>;
+        onSelectionChangedObservable: BABYLON.Observable<BABYLON.Nullable<GraphFrame | GraphNode | NodePort | NodeLink | FramePortData>>;
         onRebuildRequiredObservable: BABYLON.Observable<void>;
         onBuiltObservable: BABYLON.Observable<void>;
         onResetRequiredObservable: BABYLON.Observable<void>;
@@ -3094,7 +3095,7 @@ declare module NODEEDITOR {
         directionalLight0: boolean;
         directionalLight1: boolean;
         controlCamera: boolean;
-        storeEditorData: (serializationObject: any) => void;
+        storeEditorData: (serializationObject: any, frame?: BABYLON.Nullable<GraphFrame>) => void;
         _mode: BABYLON.NodeMaterialModes;
         /** Gets the mode */
         get mode(): BABYLON.NodeMaterialModes;

+ 1 - 1
dist/preview release/packagesSizeBaseLine.json

@@ -1 +1 @@
-{"thinEngineOnly":116078,"engineOnly":152481,"sceneOnly":512616,"minGridMaterial":648499,"minStandardMaterial":791777}
+{"thinEngineOnly":116078,"engineOnly":152481,"sceneOnly":512691,"minGridMaterial":648842,"minStandardMaterial":797399}

+ 359 - 4
dist/preview release/viewer/babylon.module.d.ts

@@ -24638,6 +24638,16 @@ declare module "babylonjs/Materials/material" {
          */
         static readonly MATERIAL_ALPHATESTANDBLEND: number;
         /**
+         * The Whiteout method is used to blend normals.
+         * Details of the algorithm can be found here: https://blog.selfshadow.com/publications/blending-in-detail/
+         */
+        static readonly MATERIAL_NORMALBLENDMETHOD_WHITEOUT: number;
+        /**
+         * 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/
+         */
+        static readonly MATERIAL_NORMALBLENDMETHOD_RNM: number;
+        /**
          * Custom callback helping to override the default shader used in the material.
          */
         customShaderNameResolve: (shaderName: string, uniforms: string[], uniformBuffers: string[], samplers: string[], defines: MaterialDefines | string[], attributes?: string[], options?: ICustomShaderNameResolveOptions) => string;
@@ -30064,6 +30074,12 @@ declare module "babylonjs/Materials/materialFlags" {
          */
         static get DiffuseTextureEnabled(): boolean;
         static set DiffuseTextureEnabled(value: boolean);
+        private static _DetailTextureEnabled;
+        /**
+         * Are detail textures enabled in the application.
+         */
+        static get DetailTextureEnabled(): boolean;
+        static set DetailTextureEnabled(value: boolean);
         private static _AmbientTextureEnabled;
         /**
          * Are ambient textures enabled in the application.
@@ -30367,6 +30383,150 @@ declare module "babylonjs/Shaders/default.vertex" {
         shader: string;
     };
 }
+declare module "babylonjs/Materials/material.detailMapConfiguration" {
+    import { Nullable } from "babylonjs/types";
+    import { Scene } from "babylonjs/scene";
+    import { BaseTexture } from "babylonjs/Materials/Textures/baseTexture";
+    import { UniformBuffer } from "babylonjs/Materials/uniformBuffer";
+    import { IAnimatable } from "babylonjs/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;
+        /**
+         * The detail texture of the material.
+         */
+        texture: Nullable<BaseTexture>;
+        /**
+         * Defines how strongly the detail diffuse/albedo channel is blended with the regular diffuse/albedo texture
+         * Bigger values mean stronger blending
+         */
+        diffuseBlendLevel: number;
+        /**
+         * Defines how strongly the detail roughness channel is blended with the regular roughness value
+         * Bigger values mean stronger blending. Only used with PBR materials
+         */
+        roughnessBlendLevel: number;
+        /**
+         * Defines how strong the bump effect from the detail map is
+         * Bigger values mean stronger effect
+         */
+        bumpLevel: number;
+        private _normalBlendMethod;
+        /**
+         * The method used to blend the bump and detail normals together
+         */
+        normalBlendMethod: number;
+        private _isEnabled;
+        /**
+         * Enable or disable the detail map on this material
+         */
+        isEnabled: boolean;
+        /** @hidden */
+        private _internalMarkAllSubMeshesAsTexturesDirty;
+        /** @hidden */
+        _markAllSubMeshesAsTexturesDirty(): void;
+        /**
+         * Instantiate a new detail map
+         * @param markAllSubMeshesAsTexturesDirty Callback to flag the material to dirty
+         */
+        constructor(markAllSubMeshesAsTexturesDirty: () => void);
+        /**
+         * 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.
+         */
+        isReadyForSubMesh(defines: IMaterialDetailMapDefines, scene: Scene): boolean;
+        /**
+         * Update the defines for detail map usage
+         * @param defines the list of "defines" to update.
+         * @param scene defines the scene the material belongs to.
+         */
+        prepareDefines(defines: IMaterialDetailMapDefines, scene: Scene): void;
+        /**
+         * 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.
+         */
+        bindForSubMesh(uniformBuffer: UniformBuffer, scene: Scene, isFrozen: boolean): void;
+        /**
+         * 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.
+         */
+        hasTexture(texture: BaseTexture): boolean;
+        /**
+         * Returns an array of the actively used textures.
+         * @param activeTextures Array of BaseTextures
+         */
+        getActiveTextures(activeTextures: BaseTexture[]): void;
+        /**
+         * Returns the animatable textures.
+         * @param animatables Array of animatable textures.
+         */
+        getAnimatables(animatables: IAnimatable[]): void;
+        /**
+         * Disposes the resources of the material.
+         * @param forceDisposeTextures - Forces the disposal of all textures.
+         */
+        dispose(forceDisposeTextures?: boolean): void;
+        /**
+        * Get the current class name useful for serialization or dynamic coding.
+        * @returns "DetailMap"
+        */
+        getClassName(): string;
+        /**
+         * Add the required uniforms to the current list.
+         * @param uniforms defines the current uniform list.
+         */
+        static AddUniforms(uniforms: string[]): void;
+        /**
+         * Add the required samplers to the current list.
+         * @param samplers defines the current sampler list.
+         */
+        static AddSamplers(samplers: string[]): void;
+        /**
+         * Add the required uniforms to the current buffer.
+         * @param uniformBuffer defines the current uniform buffer.
+         */
+        static PrepareUniformBuffer(uniformBuffer: UniformBuffer): void;
+        /**
+         * Makes a duplicate of the current instance into another one.
+         * @param detailMap define the instance where to copy the info
+         */
+        copyTo(detailMap: DetailMapConfiguration): void;
+        /**
+         * Serializes this detail map instance
+         * @returns - An object with the serialized instance.
+         */
+        serialize(): any;
+        /**
+         * 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
+         */
+        parse(source: any, scene: Scene, rootUrl: string): void;
+    }
+}
 declare module "babylonjs/Materials/standardMaterial" {
     import { SmartArray } from "babylonjs/Misc/smartArray";
     import { IAnimatable } from "babylonjs/Animations/animatable.interface";
@@ -30386,12 +30546,16 @@ declare module "babylonjs/Materials/standardMaterial" {
     import { RenderTargetTexture } from "babylonjs/Materials/Textures/renderTargetTexture";
     import "babylonjs/Shaders/default.fragment";
     import "babylonjs/Shaders/default.vertex";
+    import { IMaterialDetailMapDefines, DetailMapConfiguration } from "babylonjs/Materials/material.detailMapConfiguration";
     /** @hidden */
-    export class StandardMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines {
+    export class StandardMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines, IMaterialDetailMapDefines {
         MAINUV1: boolean;
         MAINUV2: boolean;
         DIFFUSE: boolean;
         DIFFUSEDIRECTUV: number;
+        DETAIL: boolean;
+        DETAILDIRECTUV: number;
+        DETAIL_NORMALBLENDMETHOD: number;
         AMBIENT: boolean;
         AMBIENTDIRECTUV: number;
         OPACITY: boolean;
@@ -30817,6 +30981,10 @@ declare module "babylonjs/Materials/standardMaterial" {
          * corresponding to low luminance, medium luminance, and high luminance areas respectively.
          */
         set cameraColorCurves(value: Nullable<ColorCurves>);
+        /**
+         * Defines the detail map parameters for the material.
+         */
+        readonly detailMap: DetailMapConfiguration;
         protected _renderTargets: SmartArray<RenderTargetTexture>;
         protected _worldViewProjectionMatrix: Matrix;
         protected _globalAmbientColor: Color3;
@@ -30936,6 +31104,11 @@ declare module "babylonjs/Materials/standardMaterial" {
         static get DiffuseTextureEnabled(): boolean;
         static set DiffuseTextureEnabled(value: boolean);
         /**
+         * Are detail textures enabled in the application.
+         */
+        static get DetailTextureEnabled(): boolean;
+        static set DetailTextureEnabled(value: boolean);
+        /**
          * Are ambient textures enabled in the application.
          */
         static get AmbientTextureEnabled(): boolean;
@@ -57215,11 +57388,12 @@ declare module "babylonjs/Materials/PBR/pbrBaseMaterial" {
     import "babylonjs/Materials/Textures/baseTexture.polynomial";
     import "babylonjs/Shaders/pbr.fragment";
     import "babylonjs/Shaders/pbr.vertex";
+    import { IMaterialDetailMapDefines, DetailMapConfiguration } from "babylonjs/Materials/material.detailMapConfiguration";
     /**
      * Manages the defines for the PBR Material.
      * @hidden
      */
-    export class PBRMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines, IMaterialClearCoatDefines, IMaterialAnisotropicDefines, IMaterialBRDFDefines, IMaterialSheenDefines, IMaterialSubSurfaceDefines {
+    export class PBRMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines, IMaterialClearCoatDefines, IMaterialAnisotropicDefines, IMaterialBRDFDefines, IMaterialSheenDefines, IMaterialSubSurfaceDefines, IMaterialDetailMapDefines {
         PBR: boolean;
         NUM_SAMPLES: string;
         REALTIME_FILTERING: boolean;
@@ -57231,6 +57405,9 @@ declare module "babylonjs/Materials/PBR/pbrBaseMaterial" {
         GAMMAALBEDO: boolean;
         ALBEDODIRECTUV: number;
         VERTEXCOLOR: boolean;
+        DETAIL: boolean;
+        DETAILDIRECTUV: number;
+        DETAIL_NORMALBLENDMETHOD: number;
         AMBIENT: boolean;
         AMBIENTDIRECTUV: number;
         AMBIENTINGRAYSCALE: boolean;
@@ -57795,6 +57972,10 @@ declare module "babylonjs/Materials/PBR/pbrBaseMaterial" {
          * Defines the SubSurface parameters for the material.
          */
         readonly subSurface: PBRSubSurfaceConfiguration;
+        /**
+         * Defines the detail map parameters for the material.
+         */
+        readonly detailMap: DetailMapConfiguration;
         protected _rebuildInParallel: boolean;
         /**
          * Instantiates a new PBRMaterial instance.
@@ -101318,6 +101499,16 @@ declare module BABYLON {
          */
         static readonly MATERIAL_ALPHATESTANDBLEND: number;
         /**
+         * The Whiteout method is used to blend normals.
+         * Details of the algorithm can be found here: https://blog.selfshadow.com/publications/blending-in-detail/
+         */
+        static readonly MATERIAL_NORMALBLENDMETHOD_WHITEOUT: number;
+        /**
+         * 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/
+         */
+        static readonly MATERIAL_NORMALBLENDMETHOD_RNM: number;
+        /**
          * Custom callback helping to override the default shader used in the material.
          */
         customShaderNameResolve: (shaderName: string, uniforms: string[], uniformBuffers: string[], samplers: string[], defines: MaterialDefines | string[], attributes?: string[], options?: ICustomShaderNameResolveOptions) => string;
@@ -106626,6 +106817,12 @@ declare module BABYLON {
          */
         static get DiffuseTextureEnabled(): boolean;
         static set DiffuseTextureEnabled(value: boolean);
+        private static _DetailTextureEnabled;
+        /**
+         * Are detail textures enabled in the application.
+         */
+        static get DetailTextureEnabled(): boolean;
+        static set DetailTextureEnabled(value: boolean);
         private static _AmbientTextureEnabled;
         /**
          * Are ambient textures enabled in the application.
@@ -106886,12 +107083,154 @@ declare module BABYLON {
     };
 }
 declare module BABYLON {
+    /**
+     * @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;
+        /**
+         * The detail texture of the material.
+         */
+        texture: Nullable<BaseTexture>;
+        /**
+         * Defines how strongly the detail diffuse/albedo channel is blended with the regular diffuse/albedo texture
+         * Bigger values mean stronger blending
+         */
+        diffuseBlendLevel: number;
+        /**
+         * Defines how strongly the detail roughness channel is blended with the regular roughness value
+         * Bigger values mean stronger blending. Only used with PBR materials
+         */
+        roughnessBlendLevel: number;
+        /**
+         * Defines how strong the bump effect from the detail map is
+         * Bigger values mean stronger effect
+         */
+        bumpLevel: number;
+        private _normalBlendMethod;
+        /**
+         * The method used to blend the bump and detail normals together
+         */
+        normalBlendMethod: number;
+        private _isEnabled;
+        /**
+         * Enable or disable the detail map on this material
+         */
+        isEnabled: boolean;
+        /** @hidden */
+        private _internalMarkAllSubMeshesAsTexturesDirty;
+        /** @hidden */
+        _markAllSubMeshesAsTexturesDirty(): void;
+        /**
+         * Instantiate a new detail map
+         * @param markAllSubMeshesAsTexturesDirty Callback to flag the material to dirty
+         */
+        constructor(markAllSubMeshesAsTexturesDirty: () => void);
+        /**
+         * 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.
+         */
+        isReadyForSubMesh(defines: IMaterialDetailMapDefines, scene: Scene): boolean;
+        /**
+         * Update the defines for detail map usage
+         * @param defines the list of "defines" to update.
+         * @param scene defines the scene the material belongs to.
+         */
+        prepareDefines(defines: IMaterialDetailMapDefines, scene: Scene): void;
+        /**
+         * 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.
+         */
+        bindForSubMesh(uniformBuffer: UniformBuffer, scene: Scene, isFrozen: boolean): void;
+        /**
+         * 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.
+         */
+        hasTexture(texture: BaseTexture): boolean;
+        /**
+         * Returns an array of the actively used textures.
+         * @param activeTextures Array of BaseTextures
+         */
+        getActiveTextures(activeTextures: BaseTexture[]): void;
+        /**
+         * Returns the animatable textures.
+         * @param animatables Array of animatable textures.
+         */
+        getAnimatables(animatables: IAnimatable[]): void;
+        /**
+         * Disposes the resources of the material.
+         * @param forceDisposeTextures - Forces the disposal of all textures.
+         */
+        dispose(forceDisposeTextures?: boolean): void;
+        /**
+        * Get the current class name useful for serialization or dynamic coding.
+        * @returns "DetailMap"
+        */
+        getClassName(): string;
+        /**
+         * Add the required uniforms to the current list.
+         * @param uniforms defines the current uniform list.
+         */
+        static AddUniforms(uniforms: string[]): void;
+        /**
+         * Add the required samplers to the current list.
+         * @param samplers defines the current sampler list.
+         */
+        static AddSamplers(samplers: string[]): void;
+        /**
+         * Add the required uniforms to the current buffer.
+         * @param uniformBuffer defines the current uniform buffer.
+         */
+        static PrepareUniformBuffer(uniformBuffer: UniformBuffer): void;
+        /**
+         * Makes a duplicate of the current instance into another one.
+         * @param detailMap define the instance where to copy the info
+         */
+        copyTo(detailMap: DetailMapConfiguration): void;
+        /**
+         * Serializes this detail map instance
+         * @returns - An object with the serialized instance.
+         */
+        serialize(): any;
+        /**
+         * 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
+         */
+        parse(source: any, scene: Scene, rootUrl: string): void;
+    }
+}
+declare module BABYLON {
     /** @hidden */
-    export class StandardMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines {
+    export class StandardMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines, IMaterialDetailMapDefines {
         MAINUV1: boolean;
         MAINUV2: boolean;
         DIFFUSE: boolean;
         DIFFUSEDIRECTUV: number;
+        DETAIL: boolean;
+        DETAILDIRECTUV: number;
+        DETAIL_NORMALBLENDMETHOD: number;
         AMBIENT: boolean;
         AMBIENTDIRECTUV: number;
         OPACITY: boolean;
@@ -107317,6 +107656,10 @@ declare module BABYLON {
          * corresponding to low luminance, medium luminance, and high luminance areas respectively.
          */
         set cameraColorCurves(value: Nullable<ColorCurves>);
+        /**
+         * Defines the detail map parameters for the material.
+         */
+        readonly detailMap: DetailMapConfiguration;
         protected _renderTargets: SmartArray<RenderTargetTexture>;
         protected _worldViewProjectionMatrix: Matrix;
         protected _globalAmbientColor: Color3;
@@ -107436,6 +107779,11 @@ declare module BABYLON {
         static get DiffuseTextureEnabled(): boolean;
         static set DiffuseTextureEnabled(value: boolean);
         /**
+         * Are detail textures enabled in the application.
+         */
+        static get DetailTextureEnabled(): boolean;
+        static set DetailTextureEnabled(value: boolean);
+        /**
          * Are ambient textures enabled in the application.
          */
         static get AmbientTextureEnabled(): boolean;
@@ -132307,7 +132655,7 @@ declare module BABYLON {
      * Manages the defines for the PBR Material.
      * @hidden
      */
-    export class PBRMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines, IMaterialClearCoatDefines, IMaterialAnisotropicDefines, IMaterialBRDFDefines, IMaterialSheenDefines, IMaterialSubSurfaceDefines {
+    export class PBRMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines, IMaterialClearCoatDefines, IMaterialAnisotropicDefines, IMaterialBRDFDefines, IMaterialSheenDefines, IMaterialSubSurfaceDefines, IMaterialDetailMapDefines {
         PBR: boolean;
         NUM_SAMPLES: string;
         REALTIME_FILTERING: boolean;
@@ -132319,6 +132667,9 @@ declare module BABYLON {
         GAMMAALBEDO: boolean;
         ALBEDODIRECTUV: number;
         VERTEXCOLOR: boolean;
+        DETAIL: boolean;
+        DETAILDIRECTUV: number;
+        DETAIL_NORMALBLENDMETHOD: number;
         AMBIENT: boolean;
         AMBIENTDIRECTUV: number;
         AMBIENTINGRAYSCALE: boolean;
@@ -132883,6 +133234,10 @@ declare module BABYLON {
          * Defines the SubSurface parameters for the material.
          */
         readonly subSurface: PBRSubSurfaceConfiguration;
+        /**
+         * Defines the detail map parameters for the material.
+         */
+        readonly detailMap: DetailMapConfiguration;
         protected _rebuildInParallel: boolean;
         /**
          * Instantiates a new PBRMaterial instance.

Plik diff jest za duży
+ 73 - 69
dist/preview release/viewer/babylon.viewer.js


Plik diff jest za duży
+ 1 - 1
dist/preview release/viewer/babylon.viewer.max.js


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

@@ -8,7 +8,7 @@
 - Added HDR texture filtering tools to the sandbox ([Sebavan](https://github.com/sebavan/))
 - Reflection probes can now be used to give accurate shading with PBR ([CraigFeldpsar](https://github.com/craigfeldspar) and ([Sebavan](https://github.com/sebavan/)))
 - Added editing of PBR materials, Post processes and Particle fragment shaders in the node material editor ([Popov72](https://github.com/Popov72))
-- Added Curve editor to manage selected entity's animations and edit animation groups in Inspector ([pixelspace](https://github.com/devpixelspace))
+- Added Curve editor to manage entity's animations and edit animation groups in Inspector ([pixelspace](https://github.com/devpixelspace))
 - Added support in `ShadowGenerator` for fast fake soft transparent shadows ([Popov72](https://github.com/Popov72))
 - Added support for **thin instances** for faster mesh instances. [Doc](https://doc.babylonjs.com/how_to/how_to_use_thininstances) ([Popov72](https://github.com/Popov72))
 
@@ -35,6 +35,7 @@
 - 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))
 - 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
 
@@ -48,6 +49,7 @@
 - Add the `FragCoord` and `ScreenSize` blocks ([Popov72](https://github.com/Popov72))
 - Particle systems: add the `ParticlePositionWorld` block ([Popov72](https://github.com/Popov72))
 - Add isExposedOnFrame property to connection points ([belfortk](https://github.com/belfortk))
+- Add support for exporting frames ([belfortk](https://github.com/belfortk))
 
 ### Inspector
 

+ 2 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/animations/addAnimation.tsx

@@ -15,6 +15,7 @@ interface IAddAnimationProps {
    entity: IAnimatable;
    onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
    setNotificationMessage: (message: string) => void;
+   changed: () => void;
 }
 
 export class AddAnimation extends React.Component<IAddAnimationProps, {animationName: string, animationTargetProperty: string, animationType:string, loopMode: number, animationTargetPath:string}>{ 
@@ -187,6 +188,7 @@ export class AddAnimation extends React.Component<IAddAnimationProps, {animation
                         const updatedCollection = [...this.props.entity.animations, animation]
                         this.raiseOnPropertyChanged(updatedCollection, store);
                         this.props.entity.animations = updatedCollection;
+                        this.props.changed();
                         this.props.close();
                     }   
                 }

+ 8 - 123
inspector/src/components/actionTabs/tabs/propertyGrids/animations/animationCurveEditorComponent.tsx

@@ -10,12 +10,9 @@ import { Playhead } from './playhead';
 import { Notification } from './notification';
 import { GraphActionsBar } from './graphActionsBar';
 import { Scene } from "babylonjs/scene";
-import { ButtonLineComponent } from '../../../lines/buttonLineComponent';
 import { IAnimatable } from 'babylonjs/Animations/animatable.interface';
 import { TargetedAnimation } from "babylonjs/Animations/animationGroup";
-import { AddAnimation } from './addAnimation';
-import { Nullable } from 'babylonjs/types';
-import { IconButtonLineComponent } from '../../../lines/iconButtonLineComponent';
+import { EditorControls } from './editorControls';
 
 require("./curveEditor.scss");
 
@@ -51,7 +48,6 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
     lastFrame: number,
     playheadPos: number,
     isPlaying: boolean,
-    isAnimationDialogOpen: boolean
 }> {
 
     // Height scale *Review this functionaliy
@@ -118,7 +114,6 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
             scale: 1,
             playheadPos: 0,
             isPlaying: this.isAnimationPlaying(),
-            isAnimationDialogOpen: false
         }
     }
 
@@ -193,92 +188,6 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
         }
     }
 
-    setListItem(animation: Animation, i: number) {
-        let element;
-
-        switch (animation.dataType) {
-            case Animation.ANIMATIONTYPE_FLOAT:
-                element = <li className={this.state.selected && this.state.selected.name === animation.name ? 'active' : ''} key={i} onClick={() => this.selectAnimation(animation)}>
-                    <p>{animation.name}&nbsp;
-                    <span>{animation.targetProperty}</span></p>
-                    {!(this.props.entity instanceof TargetedAnimation) ? this.state.selected && this.state.selected.name === animation.name ? <ButtonLineComponent label={"Remove"} onClick={() => this.deleteAnimation()} /> : null : null}
-                </li>
-                break;
-            case Animation.ANIMATIONTYPE_VECTOR2:
-                element = <li className="property" key={i}><p>{animation.targetProperty}</p>
-                    <ul>
-                        <li key={`${i}_x`}>Property <strong>X</strong></li>
-                        <li key={`${i}_y`}>Property <strong>Y</strong></li>
-                    </ul>
-                </li>
-                break;
-            case Animation.ANIMATIONTYPE_VECTOR3:
-                element = <li className="property" key={i}><p>{animation.targetProperty}</p>
-                    <ul>
-                        <li key={`${i}_x`}>Property <strong>X</strong></li>
-                        <li key={`${i}_y`}>Property <strong>Y</strong></li>
-                        <li key={`${i}_z`}>Property <strong>Z</strong></li>
-                    </ul>
-                </li>
-                break;
-            case Animation.ANIMATIONTYPE_QUATERNION:
-                element = <li className="property" key={i}><p>{animation.targetProperty}</p>
-                    <ul>
-                        <li key={`${i}_x`}>Property <strong>X</strong></li>
-                        <li key={`${i}_y`}>Property <strong>Y</strong></li>
-                        <li key={`${i}_z`}>Property <strong>Z</strong></li>
-                        <li key={`${i}_w`}>Property <strong>W</strong></li>
-                    </ul>
-                </li>
-                break;
-            case Animation.ANIMATIONTYPE_COLOR3:
-                element = <li className="property" key={i}><p>{animation.targetProperty}</p>
-                    <ul>
-                        <li key={`${i}_r`}>Property <strong>R</strong></li>
-                        <li key={`${i}_g`}>Property <strong>G</strong></li>
-                        <li key={`${i}_b`}>Property <strong>B</strong></li>
-                    </ul>
-                </li>
-                break;
-            case Animation.ANIMATIONTYPE_COLOR4:
-                element = <li className="property" key={i}><p>{animation.targetProperty}</p>
-                    <ul>
-                        <li key={`${i}_r`}>Property <strong>R</strong></li>
-                        <li key={`${i}_g`}>Property <strong>G</strong></li>
-                        <li key={`${i}_b`}>Property <strong>B</strong></li>
-                        <li key={`${i}_a`}>Property <strong>A</strong></li>
-                    </ul>
-                </li>
-                break;
-            case Animation.ANIMATIONTYPE_SIZE:
-                element = <li className="property" key={i}><p>{animation.targetProperty}</p>
-                    <ul>
-                        <li key={`${i}_width`}>Property <strong>Width</strong></li>
-                        <li key={`${i}_height`}>Property <strong>Height</strong></li>
-                    </ul>
-                </li>
-                break;
-            default: console.log("not recognized");
-                element = null;
-                break;
-        }
-
-        return element;
-    }
-
-    deleteAnimation() {
-        let currentSelected = this.state.selected;
-        if (this.props.entity instanceof TargetedAnimation) {
-            console.log("no animation remove allowed");
-        } else {
-            let animations = (this.props.entity as IAnimatable).animations;
-            if (animations) {
-                let updatedAnimations = animations.filter(anim => anim !== currentSelected);
-                (this.props.entity as IAnimatable).animations = updatedAnimations as Nullable<Animation[]>;
-            }
-        }
-    }
-
     /**
     * Keyframe Manipulation
     * This section handles events from SvgDraggableArea.
@@ -1087,37 +996,13 @@ export class AnimationCurveEditorComponent extends React.Component<IAnimationCur
                     
                 <div className="content">
                     <div className="row">
-                        <div className="animation-list">
-                            <div className="controls-header">
-                            {this._isTargetedAnimation ? null : <IconButtonLineComponent active={this.state.isAnimationDialogOpen} tooltip="Add Animation" icon="medium add-animation" onClick={() => { this.setState({ isAnimationDialogOpen: true})}}></IconButtonLineComponent>}
-                            <IconButtonLineComponent tooltip="Load Animation" icon="medium load" onClick={() => { this.setState({ isAnimationDialogOpen: true})}}></IconButtonLineComponent>
-                            <IconButtonLineComponent tooltip="Save Animation" icon="medium save" onClick={() => { this.setState({ isAnimationDialogOpen: true})}}></IconButtonLineComponent>
-                            <IconButtonLineComponent tooltip="Edit Animations" icon="medium animation-edit" onClick={() => { this.setState({ isAnimationDialogOpen: true})}}></IconButtonLineComponent>
-                            <IconButtonLineComponent tooltip="Loop/Unloop" icon="medium loop-active" onClick={() => { this.setState({ isAnimationDialogOpen: true})}}></IconButtonLineComponent>
-                            </div>
-                            { (this.props.entity instanceof TargetedAnimation) ? null : 
-                                <AddAnimation 
-                                    isOpen={this.state.isAnimationDialogOpen} 
-                                    close={() => { this.setState({isAnimationDialogOpen: false})}} 
-                                    entity={this.props.entity} 
-                                    setNotificationMessage={(message: string) => { this.setState({notification: message})}} />
-                            }
-
-                            <div className="object-tree">
-                                <ul>
-                                    {
-
-                                        this.props.entity instanceof TargetedAnimation ? this.setListItem(this.props.entity.animation, 0) :
-                                            this.props.entity.animations && this.props.entity.animations.map((animation, i) => {
-
-                                                return this.setListItem(animation, i);
-
-                                            })}
-
-                                </ul>
-                            </div>
-                        </div>
-
+                        <EditorControls selectAnimation={(animation: Animation) => this.selectAnimation(animation)} 
+                        isTargetedAnimation={this._isTargetedAnimation} 
+                        entity={this.props.entity} 
+                        selected={this.state.selected} 
+                        setNotificationMessage={(message: string) => { this.setState({notification: message})}}
+                        />
+                        
                         <div ref={this._graphCanvas} className="graph-chart" onWheel={(e) => this.zoom(e)} >
 
                             <Playhead frame={this.state.currentFrame} offset={this.state.playheadOffset} />

+ 47 - 2
inspector/src/components/actionTabs/tabs/propertyGrids/animations/curveEditor.scss

@@ -3,6 +3,10 @@
 
     font-family: acumin-pro-condensed;
 
+    .last {
+        margin-left: 3px;
+    }
+
     .icon {
         width: 40px;
         height: 40px;
@@ -221,6 +225,7 @@
             background-color: transparent;
             background-size: contain;
             color: white;
+            cursor: pointer;
         }
 
         &.loop-inactive {
@@ -229,6 +234,7 @@
             background-color: transparent;
             background-size: contain;
             color: white;
+            cursor: pointer;
         }
 
         &.move {
@@ -245,6 +251,7 @@
             background-color: transparent;
             background-size: contain;
             color: white;
+            cursor: pointer;
         }
 
         &.load {
@@ -253,6 +260,7 @@
             background-color: transparent;
             background-size: contain;
             color: white;
+            cursor: pointer;
         }
 
         &.checked {
@@ -471,6 +479,40 @@
             .controls-header {
                 display: flex;
                 background-color: #252525;
+
+               .input-fps{
+                display: flex;
+                align-items: center;
+                width: 52px;
+                padding-left: 4px;
+                    .numeric{
+                        input{
+                            width: 52px;
+                            font-size: 12px;
+                            height: 22px;
+                            background-color: black;
+                            border: none;
+                            color: white;
+                            text-align: center;
+                            &::-webkit-inner-spin-button, &::-webkit-outer-spin-button {
+                                appearance: none;
+                                -webkit-appearance: none; 
+                                margin: 0;
+                            }
+                            font-family: acumin-pro-condensed;
+                        }
+                    }
+                    p {
+                        color: white;
+                        font-size: 12px;
+                        margin: 0px;
+                        position: relative;
+                        right: 17px;
+                        line-height: 20px;
+                        height: 20px;
+                        margin-top: -2px;
+                    }
+               } 
             }
 
             .new-animation{
@@ -511,6 +553,8 @@
                                 font-size: 12px;
                                 line-height: 11px;
                                 margin: 5px;
+                                font-size: 10px;
+                                font-family: acumin-pro-condensed;
                             }
                         }
                     }
@@ -557,9 +601,8 @@
                 background-color:#111111;
                 padding: 10px;
                 margin-top: 19px;
-                height: 11em;
                 overflow: scroll;
-                overflow-x: hidden;
+                overflow-x: auto;
             }
 
             .label-input{
@@ -582,6 +625,7 @@
                         outline-style: auto;
                         outline-color: lightgrey;
                     }
+                    font-family: acumin-pro-condensed;
                 }
 
                 select {
@@ -596,6 +640,7 @@
                         outline-style: auto;
                         outline-color: lightgrey;
                     }
+                    font-family: acumin-pro-condensed;
                 }
             }
         }

+ 197 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/animations/editorControls.tsx

@@ -0,0 +1,197 @@
+
+import * as React from "react";
+
+import { Observable } from "babylonjs/Misc/observable";
+import { PropertyChangedEvent } from "../../../../../components/propertyChangedEvent";
+import { Animation } from 'babylonjs/Animations/animation';
+import { IconButtonLineComponent } from '../../../lines/iconButtonLineComponent';
+import { NumericInputComponent } from '../../../lines/numericInputComponent';
+import { AddAnimation } from './addAnimation';
+import { IAnimatable } from 'babylonjs/Animations/animatable.interface';
+import { TargetedAnimation } from "babylonjs/Animations/animationGroup";
+import { Nullable } from 'babylonjs/types';
+
+interface IEditorControlsProps {
+   isTargetedAnimation: boolean;
+   entity: IAnimatable | TargetedAnimation;
+   selected: Animation | null
+   onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
+   setNotificationMessage: (message: string) => void;
+   selectAnimation: (selected: Animation) => void;
+}
+
+export class EditorControls extends React.Component<IEditorControlsProps, {isAnimationTabOpen: boolean, isEditTabOpen: boolean, isLoadTabOpen: boolean, isSaveTabOpen: boolean, isLoopActive: boolean, animationsCount: number; framesPerSecond: number}>{ 
+    
+    constructor(props: IEditorControlsProps) {
+        super(props);
+        let count = this.props.isTargetedAnimation ? 1 : (this.props.entity as IAnimatable).animations?.length ?? 0;
+        this.state = { isAnimationTabOpen: false, isEditTabOpen: false, isSaveTabOpen: false, isLoadTabOpen: false, isLoopActive: false, animationsCount: count, framesPerSecond: 60 }
+    }
+
+    animationsChanged(){
+        let recount = (this.props.entity as IAnimatable).animations?.length ?? 0;
+        this.setState( { animationsCount: recount } );
+    }
+
+    deleteAnimation() {
+        let currentSelected = this.props.selected;
+        if (this.props.entity instanceof TargetedAnimation) {
+            console.log("no animation remove allowed");
+        } else {
+            let animations = (this.props.entity as IAnimatable).animations;
+            if (animations) {
+                let updatedAnimations = animations.filter(anim => anim !== currentSelected);
+                (this.props.entity as IAnimatable).animations = updatedAnimations as Nullable<Animation[]>;
+            }
+        }
+    }
+
+    setListItem(animation: Animation, i: number) {
+        let element;
+
+        switch (animation.dataType) {
+            case Animation.ANIMATIONTYPE_FLOAT:
+                element = <li className={this.props.selected && this.props.selected.name === animation.name ? 'active' : ''} key={i} onClick={() => this.props.selectAnimation(animation)}>
+                    <p>{animation.name}&nbsp;
+                    <span>{animation.targetProperty}</span></p>
+                    {!(this.props.entity instanceof TargetedAnimation) ? this.props.selected && this.props.selected.name === animation.name ? <IconButtonLineComponent tooltip="Remove" icon="delete" onClick={() => this.deleteAnimation()} /> : null : null}
+                </li>
+                break;
+            case Animation.ANIMATIONTYPE_VECTOR2:
+                element = <li className="property" key={i}><p>{animation.targetProperty}</p>
+                    <ul>
+                        <li key={`${i}_x`}>Property <strong>X</strong></li>
+                        <li key={`${i}_y`}>Property <strong>Y</strong></li>
+                    </ul>
+                </li>
+                break;
+            case Animation.ANIMATIONTYPE_VECTOR3:
+                element = <li className="property" key={i}><p>{animation.targetProperty}</p>
+                    <ul>
+                        <li key={`${i}_x`}>Property <strong>X</strong></li>
+                        <li key={`${i}_y`}>Property <strong>Y</strong></li>
+                        <li key={`${i}_z`}>Property <strong>Z</strong></li>
+                    </ul>
+                </li>
+                break;
+            case Animation.ANIMATIONTYPE_QUATERNION:
+                element = <li className="property" key={i}><p>{animation.targetProperty}</p>
+                    <ul>
+                        <li key={`${i}_x`}>Property <strong>X</strong></li>
+                        <li key={`${i}_y`}>Property <strong>Y</strong></li>
+                        <li key={`${i}_z`}>Property <strong>Z</strong></li>
+                        <li key={`${i}_w`}>Property <strong>W</strong></li>
+                    </ul>
+                </li>
+                break;
+            case Animation.ANIMATIONTYPE_COLOR3:
+                element = <li className="property" key={i}><p>{animation.targetProperty}</p>
+                    <ul>
+                        <li key={`${i}_r`}>Property <strong>R</strong></li>
+                        <li key={`${i}_g`}>Property <strong>G</strong></li>
+                        <li key={`${i}_b`}>Property <strong>B</strong></li>
+                    </ul>
+                </li>
+                break;
+            case Animation.ANIMATIONTYPE_COLOR4:
+                element = <li className="property" key={i}><p>{animation.targetProperty}</p>
+                    <ul>
+                        <li key={`${i}_r`}>Property <strong>R</strong></li>
+                        <li key={`${i}_g`}>Property <strong>G</strong></li>
+                        <li key={`${i}_b`}>Property <strong>B</strong></li>
+                        <li key={`${i}_a`}>Property <strong>A</strong></li>
+                    </ul>
+                </li>
+                break;
+            case Animation.ANIMATIONTYPE_SIZE:
+                element = <li className="property" key={i}><p>{animation.targetProperty}</p>
+                    <ul>
+                        <li key={`${i}_width`}>Property <strong>Width</strong></li>
+                        <li key={`${i}_height`}>Property <strong>Height</strong></li>
+                    </ul>
+                </li>
+                break;
+            default: console.log("not recognized");
+                element = null;
+                break;
+        }
+
+        return element;
+    }
+
+    handleTabs(tab: number){
+
+        let state = {isAnimationTabOpen: true, isLoadTabOpen: false, isSaveTabOpen: false, isEditTabOpen: false };
+
+        switch(tab){
+            case 0:
+                state = {isAnimationTabOpen: true, isLoadTabOpen: false, isSaveTabOpen: false, isEditTabOpen: false };
+                break;
+            case 1:
+                state = {isAnimationTabOpen: false, isLoadTabOpen: true, isSaveTabOpen: false, isEditTabOpen: false };
+                break;
+            case 2:
+                state = {isAnimationTabOpen: false, isLoadTabOpen: false, isSaveTabOpen: true, isEditTabOpen: false };
+                break;
+            case 3:
+                state = {isAnimationTabOpen: false, isLoadTabOpen: false, isSaveTabOpen: false, isEditTabOpen: true };
+                break;
+        }
+
+        this.setState(state);
+    }
+
+    handleChangeFps(fps: number){
+        this.setState({framesPerSecond: fps});
+    }
+
+    render() { 
+        return (
+            <div className="animation-list">
+            <div className="controls-header">
+            {this.props.isTargetedAnimation ? null : <IconButtonLineComponent active={this.state.isAnimationTabOpen} tooltip="Add Animation" icon="medium add-animation" onClick={() => this.handleTabs(0)}></IconButtonLineComponent>}
+            <IconButtonLineComponent active={this.state.isLoadTabOpen} tooltip="Load Animation" icon="medium load" onClick={() => this.handleTabs(1)}></IconButtonLineComponent>
+            <IconButtonLineComponent active={this.state.isSaveTabOpen} tooltip="Save Animation" icon="medium save" onClick={() => this.handleTabs(2)}></IconButtonLineComponent>
+            {this.state.animationsCount === 0 ? null : 
+            <IconButtonLineComponent active={this.state.isEditTabOpen} tooltip="Edit Animations" icon="medium animation-edit" onClick={() => this.handleTabs(3)}></IconButtonLineComponent> 
+            }
+            { this.state.isEditTabOpen ?
+            <div className="input-fps">
+                <NumericInputComponent label={""} precision={0} value={this.state.framesPerSecond} onChange={(framesPerSecond: number) => this.handleChangeFps(framesPerSecond)}/>
+                <p>fps</p>
+            </div> : null
+            }
+            { this.state.isEditTabOpen ?
+            <IconButtonLineComponent tooltip="Loop/Unloop" icon={`medium ${this.state.isLoopActive ? 'loop-active last' : 'loop-inactive last'}`} onClick={() => { this.setState({ isLoopActive: !this.state.isLoopActive })}}></IconButtonLineComponent> : null
+            }
+            </div>
+            { this.props.isTargetedAnimation ? null : 
+                <AddAnimation 
+                    isOpen={this.state.isAnimationTabOpen} 
+                    close={() => { this.setState({isAnimationTabOpen: false})}} 
+                    entity={(this.props.entity as IAnimatable)} 
+                    setNotificationMessage={(message: string) => { this.props.setNotificationMessage(message) }}
+                    changed={() => this.animationsChanged() }
+                    onPropertyChangedObservable={this.props.onPropertyChangedObservable}/>
+            }
+
+            { this.state.isLoadTabOpen ? <div>Load</div> : null }
+
+            { this.state.isSaveTabOpen ? <div>Save</div> : null }
+
+            { this.state.isEditTabOpen ?
+                <div className="object-tree">
+                    <ul>
+                        {
+                            this.props.isTargetedAnimation ? this.setListItem((this.props.entity as TargetedAnimation).animation, 0) :
+                                (this.props.entity as IAnimatable).animations && (this.props.entity as IAnimatable).animations?.map((animation, i) => {
+                                    return this.setListItem(animation, i);
+                                })}
+
+                    </ul>
+                </div>
+            : null }
+        </div>
+        );
+    }
+}

+ 10 - 7
nodeEditor/src/diagram/graphCanvas.tsx

@@ -233,14 +233,17 @@ export class GraphCanvasComponent extends React.Component<IGraphCanvasComponentP
         }, false);     
 
         // Store additional data to serialization object
-        this.props.globalState.storeEditorData = (editorData) => {
-            editorData.zoom = this.zoom;
-            editorData.x = this.x;
-            editorData.y = this.y;
-
+        this.props.globalState.storeEditorData = (editorData, graphFrame) => {
             editorData.frames = [];
-            for (var frame of this._frames) {
-                editorData.frames.push(frame.serialize());
+            if (graphFrame) {
+                editorData.frames.push(graphFrame!.serialize());
+            } else {
+                editorData.x = this.x;
+                editorData.y = this.y;
+                editorData.zoom = this.zoom;
+                for (var frame of this._frames) {
+                    editorData.frames.push(frame.serialize());
+                }
             }
         }
     }

+ 1 - 1
nodeEditor/src/diagram/graphFrame.ts

@@ -1302,7 +1302,7 @@ export class GraphFrame {
 
     public export() {
         const state = this._ownerCanvas.globalState;
-        const json = SerializationTools.Serialize(state.nodeMaterial, state, this.nodes.map(n => n.block));
+        const json = SerializationTools.Serialize(state.nodeMaterial, state, this);
         StringTools.DownloadAsFile(state.hostDocument, json, this._name + ".json");
     }
 

+ 3 - 3
nodeEditor/src/diagram/properties/framePropertyComponent.tsx

@@ -59,9 +59,9 @@ export class FramePropertyTabComponent extends React.Component<IFramePropertyTab
                                 this.props.frame!.isCollapsed = false;
                             }} />
                     }
-                    {/* <ButtonLineComponent label="Export" onClick={() => {
-                                this.state.currentFrame!.export();
-                            }} /> */}
+                    <ButtonLineComponent label="Export" onClick={() => {
+                                this.props.frame!.export();
+                            }} />
                 </LineContainerComponent>
             </div>
         </div>

+ 1 - 1
nodeEditor/src/globalState.ts

@@ -56,7 +56,7 @@ export class GlobalState {
     directionalLight0: boolean;
     directionalLight1: boolean;
     controlCamera: boolean;
-    storeEditorData: (serializationObject: any) => void;
+    storeEditorData: (serializationObject: any, frame?: Nullable<GraphFrame>) => void;
     _mode: NodeMaterialModes;
 
     /** Gets the mode */

+ 11 - 5
nodeEditor/src/serializationTools.ts

@@ -3,16 +3,20 @@ import { GlobalState } from './globalState';
 import { Texture } from 'babylonjs/Materials/Textures/texture';
 import { DataStorage } from 'babylonjs/Misc/dataStorage';
 import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
+import { Nullable } from 'babylonjs/types';
+import { GraphFrame } from './diagram/graphFrame';
 
 export class SerializationTools {
 
-    public static UpdateLocations(material: NodeMaterial, globalState: GlobalState) {
+    public static UpdateLocations(material: NodeMaterial, globalState: GlobalState, frame?: Nullable<GraphFrame>) {
         material.editorData = {
             locations: []
         };
 
         // Store node locations
-        for (var block of material.attachedBlocks) {
+        const blocks: NodeMaterialBlock[] = frame ? frame.nodes.map(n => n.block) : material.attachedBlocks;
+
+        for (var block of blocks) {
             let node = globalState.onGetNodeFromBlock(block);
 
             material.editorData.locations.push({
@@ -22,14 +26,16 @@ export class SerializationTools {
             });
         }
 
-        globalState.storeEditorData(material.editorData);
+        globalState.storeEditorData(material.editorData, frame);
     }
 
-    public static Serialize(material: NodeMaterial, globalState: GlobalState, selectedBlocks?: NodeMaterialBlock[]) {
+    public static Serialize(material: NodeMaterial, globalState: GlobalState, frame?: Nullable<GraphFrame>) {
         let bufferSerializationState = Texture.SerializeBuffers;
         Texture.SerializeBuffers = DataStorage.ReadBoolean("EmbedTextures", true);
 
-        this.UpdateLocations(material, globalState);
+        this.UpdateLocations(material, globalState, frame);
+
+        const selectedBlocks = frame ? frame.nodes.map(n => n.block) : undefined;
 
         let serializationObject = material.serialize(selectedBlocks);
 

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

@@ -40,6 +40,7 @@ import "../../Shaders/pbr.fragment";
 import "../../Shaders/pbr.vertex";
 
 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> };
 
@@ -53,7 +54,8 @@ export class PBRMaterialDefines extends MaterialDefines
     IMaterialAnisotropicDefines,
     IMaterialBRDFDefines,
     IMaterialSheenDefines,
-    IMaterialSubSurfaceDefines {
+    IMaterialSubSurfaceDefines,
+    IMaterialDetailMapDefines {
     public PBR = true;
 
     public NUM_SAMPLES = "0";
@@ -69,6 +71,10 @@ export class PBRMaterialDefines extends MaterialDefines
     public ALBEDODIRECTUV = 0;
     public VERTEXCOLOR = false;
 
+    public DETAIL = false;
+    public DETAILDIRECTUV = 0;
+    public DETAIL_NORMALBLENDMETHOD = 0;
+
     public AMBIENT = false;
     public AMBIENTDIRECTUV = 0;
     public AMBIENTINGRAYSCALE = false;
@@ -792,6 +798,11 @@ export abstract class PBRBaseMaterial extends PushMaterial {
      */
     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;
 
     /**
@@ -1015,7 +1026,8 @@ export abstract class PBRBaseMaterial extends PushMaterial {
         if (!this.subSurface.isReadyForSubMesh(defines, scene) ||
             !this.clearCoat.isReadyForSubMesh(defines, scene, engine, this._disableBumpMap) ||
             !this.sheen.isReadyForSubMesh(defines, scene) ||
-            !this.anisotropy.isReadyForSubMesh(defines, scene)) {
+            !this.anisotropy.isReadyForSubMesh(defines, scene) ||
+            !this.detailMap.isReadyForSubMesh(defines, scene)) {
             return false;
         }
 
@@ -1235,6 +1247,9 @@ export abstract class PBRBaseMaterial extends PushMaterial {
 
         var uniformBuffers = ["Material", "Scene"];
 
+        DetailMapConfiguration.AddUniforms(uniforms);
+        DetailMapConfiguration.AddSamplers(samplers);
+
         PBRSubSurfaceConfiguration.AddUniforms(uniforms);
         PBRSubSurfaceConfiguration.AddSamplers(samplers);
 
@@ -1563,6 +1578,7 @@ export abstract class PBRBaseMaterial extends PushMaterial {
         }
 
         // External config
+        this.detailMap.prepareDefines(defines, scene);
         this.subSurface.prepareDefines(defines, scene);
         this.clearCoat.prepareDefines(defines, scene);
         this.anisotropy.prepareDefines(defines, mesh, scene);
@@ -1653,6 +1669,7 @@ export abstract class PBRBaseMaterial extends PushMaterial {
         PBRAnisotropicConfiguration.PrepareUniformBuffer(ubo);
         PBRSheenConfiguration.PrepareUniformBuffer(ubo);
         PBRSubSurfaceConfiguration.PrepareUniformBuffer(ubo);
+        DetailMapConfiguration.PrepareUniformBuffer(ubo);
 
         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.clearCoat.bindForSubMesh(ubo, scene, engine, this._disableBumpMap, this.isFrozen, this._invertNormalMapX, this._invertNormalMapY);
             this.anisotropy.bindForSubMesh(ubo, scene, this.isFrozen);
@@ -2053,6 +2071,7 @@ export abstract class PBRBaseMaterial extends PushMaterial {
             results.push(this._lightmapTexture);
         }
 
+        this.detailMap.getAnimatables(results);
         this.subSurface.getAnimatables(results);
         this.clearCoat.getAnimatables(results);
         this.sheen.getAnimatables(results);
@@ -2124,6 +2143,7 @@ export abstract class PBRBaseMaterial extends PushMaterial {
             activeTextures.push(this._lightmapTexture);
         }
 
+        this.detailMap.getActiveTextures(activeTextures);
         this.subSurface.getActiveTextures(activeTextures);
         this.clearCoat.getActiveTextures(activeTextures);
         this.sheen.getActiveTextures(activeTextures);
@@ -2182,7 +2202,8 @@ export abstract class PBRBaseMaterial extends PushMaterial {
             return true;
         }
 
-        return this.subSurface.hasTexture(texture) ||
+        return this.detailMap.hasTexture(texture) ||
+            this.subSurface.hasTexture(texture) ||
             this.clearCoat.hasTexture(texture) ||
             this.sheen.hasTexture(texture) ||
             this.anisotropy.hasTexture(texture);
@@ -2212,6 +2233,7 @@ export abstract class PBRBaseMaterial extends PushMaterial {
             this._microSurfaceTexture?.dispose();
         }
 
+        this.detailMap.dispose(forceDisposeTextures);
         this.subSurface.dispose(forceDisposeTextures);
         this.clearCoat.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;
 
     /**
+     * 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.
      */
     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);
     }
 
+    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;
     /**
      * 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 { EffectFallbacks } from './effectFallbacks';
 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> };
 
 /** @hidden */
-export class StandardMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines {
+export class StandardMaterialDefines extends MaterialDefines implements IImageProcessingConfigurationDefines, IMaterialDetailMapDefines {
     public MAINUV1 = false;
     public MAINUV2 = false;
     public DIFFUSE = false;
     public DIFFUSEDIRECTUV = 0;
+    public DETAIL = false;
+    public DETAILDIRECTUV = 0;
+    public DETAIL_NORMALBLENDMETHOD = 0;
     public AMBIENT = false;
     public AMBIENTDIRECTUV = 0;
     public OPACITY = false;
@@ -669,6 +673,11 @@ export class StandardMaterial extends PushMaterial {
         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 _worldViewProjectionMatrix = Matrix.Zero();
     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
         }
 
+        if (!this.detailMap.isReadyForSubMesh(defines, scene)) {
+            return false;
+        }
+
         if (defines._areImageProcessingDirty && this._imageProcessingConfiguration) {
             if (!this._imageProcessingConfiguration.isReady()) {
                 return false;
@@ -1038,6 +1051,9 @@ export class StandardMaterial extends PushMaterial {
         // Values that need to be evaluated on every frame
         MaterialHelper.PrepareDefinesForFrameBoundValues(scene, engine, defines, useInstances, null, subMesh.getRenderingMesh().hasThinInstances);
 
+        // External config
+        this.detailMap.prepareDefines(defines, scene);
+
         // Get correct effect
         if (defines.isDirty) {
             const lightDisposed = defines._areLightsDisposed;
@@ -1152,6 +1168,9 @@ export class StandardMaterial extends PushMaterial {
 
             var uniformBuffers = ["Material", "Scene"];
 
+            DetailMapConfiguration.AddUniforms(uniforms);
+            DetailMapConfiguration.AddSamplers(samplers);
+
             if (ImageProcessingConfiguration) {
                 ImageProcessingConfiguration.PrepareUniforms(uniforms, defines);
                 ImageProcessingConfiguration.PrepareSamplers(samplers, defines);
@@ -1269,6 +1288,8 @@ export class StandardMaterial extends PushMaterial {
         ubo.addUniform("visibility", 1);
         ubo.addUniform("vDiffuseColor", 4);
 
+        DetailMapConfiguration.PrepareUniformBuffer(ubo);
+
         ubo.create();
     }
 
@@ -1502,6 +1523,8 @@ export class StandardMaterial extends PushMaterial {
                 }
             }
 
+            this.detailMap.bindForSubMesh(ubo, scene, this.isFrozen);
+
             // Clip plane
             MaterialHelper.BindClipPlane(effect, scene);
 
@@ -1589,6 +1612,8 @@ export class StandardMaterial extends PushMaterial {
             results.push(this._refractionTexture);
         }
 
+        this.detailMap.getAnimatables(results);
+
         return results;
     }
 
@@ -1635,6 +1660,8 @@ export class StandardMaterial extends PushMaterial {
             activeTextures.push(this._refractionTexture);
         }
 
+        this.detailMap.getActiveTextures(activeTextures);
+
         return activeTextures;
     }
 
@@ -1684,7 +1711,7 @@ export class StandardMaterial extends PushMaterial {
             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 {
         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) {
             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.
      */
     public static get AmbientTextureEnabled(): boolean {

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

@@ -1,16 +1,20 @@
 vec2 uvOffset = vec2(0.0, 0.0);
 
-#if defined(BUMP) || defined(PARALLAX)
+#if defined(BUMP) || defined(PARALLAX) || defined(DETAIL)
 	#ifdef NORMALXYSCALE
 		float normalScale = 1.0;
-	#else		
+	#elif defined(BUMP)
 		float normalScale = vBumpInfos.y;
+	#else
+		float normalScale = 1.0;
 	#endif
 
 	#if defined(TANGENT) && defined(NORMAL)
 		mat3 TBN = vTBN;
-	#else
+	#elif defined(BUMP)
 		mat3 TBN = cotangent_frame(normalW * normalScale, vPositionW, vBumpUV);
+    #else
+		mat3 TBN = cotangent_frame(normalW * normalScale, vPositionW, vDetailUV, vec2(1., 1.));
 	#endif
 #elif defined(ANISOTROPIC)
 	#if defined(TANGENT) && defined(NORMAL)
@@ -30,11 +34,34 @@
 	#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 OBJECTSPACE_NORMALMAP
 		normalW = normalize(texture2D(bumpSampler, vBumpUV).xyz  * 2.0 - 1.0);
 		normalW = normalize(mat3(normalMatrix) * normalW);	
-	#else
+	#elif !defined(DETAIL)
 		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
+#elif defined(DETAIL)
+        detailNormal.xy *= vDetailInfos.z;
+		normalW = perturbNormalBase(TBN, detailNormal, vDetailInfos.z);
 #endif

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

@@ -14,6 +14,17 @@
 	}
 #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)
 	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) 
 		varying mat3 vTBN;
 	#endif
@@ -7,15 +7,18 @@
 		uniform mat4 normalMatrix;
 	#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
-			textureSample = normalize(textureSample * vec3(scale, scale, 1.0));
+			normal = normalize(normal * vec3(scale, scale, 1.0));
 		#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

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

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

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

@@ -15,6 +15,10 @@ void albedoOpacityBlock(
     const in vec4 opacityMap,
     const in vec2 vOpacityInfos,
 #endif
+#ifdef DETAIL
+    const in vec4 detailColor,
+    const in vec4 vDetailInfos,
+#endif
     out albedoOpacityOutParams outParams
 )
 {
@@ -40,6 +44,11 @@ void albedoOpacityBlock(
         surfaceAlbedo *= vColor.rgb;
     #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
 
     // _____________________________ Alpha Information _______________________________

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

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

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

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

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

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

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

@@ -39,6 +39,10 @@ attribute vec4 color;
 varying vec2 vDiffuseUV;
 #endif
 
+#if defined(DETAIL) && DETAILDIRECTUV == 0
+varying vec2 vDetailUV;
+#endif
+
 #if defined(AMBIENT) && AMBIENTDIRECTUV == 0
 varying vec2 vAmbientUV;
 #endif
@@ -184,6 +188,17 @@ void main(void) {
 	}
 #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 (vAmbientInfos.x == 0.)
 	{

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

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

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

@@ -38,6 +38,10 @@ attribute vec4 color;
 varying vec2 vAlbedoUV;
 #endif
 
+#if defined(DETAIL) && DETAILDIRECTUV == 0
+varying vec2 vDetailUV;
+#endif
+
 #if defined(AMBIENT) && AMBIENTDIRECTUV == 0
 varying vec2 vAmbientUV;
 #endif
@@ -240,6 +244,17 @@ void main(void) {
     }
 #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 (vAmbientInfos.x == 0.)
     {