소스 검색

MeshPhysicalMaterial: Support iridescence / thin-film materials (#23869)

* Add iridescence parameters to Physical Material

* Add iridescence fragment shader code

* Iridescence shader integration and glTF loading

* Update iridescence default values

* Add KHR_materials_iridescence to supported extensions in GLTFLoader docu

* Enable iridescence in Editor

* Update build results

* Remove build files from PR

* Honor iridescence parameters in program cache

* Use range for iridescence thin-film thickness

* Fixed linting errors

* Fix always-true conditional

* Add iridescence to glTF export

* Instantiate iridescenceThicknessRange in GLTFLoader if undefined

* Make iridescence Fresnel consistent for IBL

* Rename iridescenceIOR to iridescenceIor

* Rename iridescenceIor back to iridescenceIOR except for glTF extension
Pascal Schön 3 년 전
부모
커밋
12f550e0f4
27개의 변경된 파일929개의 추가작업 그리고 25개의 파일을 삭제
  1. 1 0
      docs/examples/en/loaders/GLTFLoader.html
  2. 1 0
      docs/examples/zh/loaders/GLTFLoader.html
  3. 51 1
      editor/js/Sidebar.Material.MapProperty.js
  4. 66 0
      editor/js/Sidebar.Material.RangeValueProperty.js
  5. 26 0
      editor/js/Sidebar.Material.js
  6. 5 0
      editor/js/Strings.js
  7. 83 0
      editor/js/commands/SetMaterialRangeCommand.js
  8. 92 0
      examples/js/loaders/GLTFLoader.js
  9. 60 0
      examples/jsm/exporters/GLTFExporter.js
  10. 95 0
      examples/jsm/loaders/GLTFLoader.js
  11. 51 0
      examples/jsm/renderers/webgl/nodes/WebGLNodeBuilder.js
  12. 6 0
      src/loaders/MaterialLoader.js
  13. 16 0
      src/materials/Material.js
  14. 30 0
      src/materials/MeshPhysicalMaterial.js
  15. 4 0
      src/renderers/shaders/ShaderChunk.js
  16. 46 0
      src/renderers/shaders/ShaderChunk/bsdfs.glsl.js
  17. 1 0
      src/renderers/shaders/ShaderChunk/common.glsl.js
  18. 107 0
      src/renderers/shaders/ShaderChunk/iridescence_fragment.glsl.js
  19. 14 0
      src/renderers/shaders/ShaderChunk/iridescence_pars_fragment.glsl.js
  20. 25 0
      src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js
  21. 23 0
      src/renderers/shaders/ShaderChunk/lights_physical_fragment.glsl.js
  22. 43 5
      src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js
  23. 6 0
      src/renderers/shaders/ShaderLib.js
  24. 9 0
      src/renderers/shaders/ShaderLib/meshphysical.glsl.js
  25. 35 4
      src/renderers/webgl/WebGLMaterials.js
  26. 7 0
      src/renderers/webgl/WebGLProgram.js
  27. 26 15
      src/renderers/webgl/WebGLPrograms.js

+ 1 - 0
docs/examples/en/loaders/GLTFLoader.html

@@ -39,6 +39,7 @@
 			<li>KHR_materials_pbrSpecularGlossiness</li>
 			<li>KHR_materials_specular</li>
 			<li>KHR_materials_transmission</li>
+			<li>KHR_materials_iridescence</li>
 			<li>KHR_materials_unlit</li>
 			<li>KHR_materials_volume</li>
 			<li>KHR_mesh_quantization</li>

+ 1 - 0
docs/examples/zh/loaders/GLTFLoader.html

@@ -37,6 +37,7 @@
 			<li>KHR_materials_pbrSpecularGlossiness</li>
 			<li>KHR_materials_specular</li>
 			<li>KHR_materials_transmission</li>
+			<li>KHR_materials_iridescence</li>
 			<li>KHR_materials_unlit</li>
 			<li>KHR_materials_volume</li>
 			<li>KHR_mesh_quantization</li>

+ 51 - 1
editor/js/Sidebar.Material.MapProperty.js

@@ -1,9 +1,10 @@
 import * as THREE from 'three';
 
-import { UICheckbox, UINumber, UIRow, UIText } from './libs/ui.js';
+import { UICheckbox, UIDiv, UINumber, UIRow, UIText } from './libs/ui.js';
 import { UITexture } from './libs/ui.three.js';
 import { SetMaterialMapCommand } from './commands/SetMaterialMapCommand.js';
 import { SetMaterialValueCommand } from './commands/SetMaterialValueCommand.js';
+import { SetMaterialRangeCommand } from './commands/SetMaterialRangeCommand.js';
 import { SetMaterialVectorCommand } from './commands/SetMaterialVectorCommand.js';
 
 function SidebarMaterialMapProperty( editor, property, name ) {
@@ -51,6 +52,36 @@ function SidebarMaterialMapProperty( editor, property, name ) {
 
 	}
 
+	let rangeMin, rangeMax;
+
+	if ( property === 'iridescenceThicknessMap' ) {
+		
+		const range = new UIDiv().setMarginLeft( '3px' );
+		container.add( range );
+
+		const rangeMinRow = new UIRow().setMarginBottom( '0px' ).setStyle( 'min-height', '0px' );
+		range.add( rangeMinRow );
+
+		rangeMinRow.add( new UIText( 'min:' ).setWidth( '35px' ) );
+
+		rangeMin = new UINumber().setWidth( '40px' ).onChange( onRangeChange );
+		rangeMinRow.add( rangeMin );
+
+		const rangeMaxRow = new UIRow().setMarginBottom( '6px' ).setStyle( 'min-height', '0px' );
+		range.add( rangeMaxRow );
+
+		rangeMaxRow.add( new UIText( 'max:' ).setWidth( '35px' ) );
+
+		rangeMax = new UINumber().setWidth( '40px' ).onChange( onRangeChange );
+		rangeMaxRow.add( rangeMax );
+
+		// Additional settings for iridescenceThicknessMap
+		// Please add conditional if more maps are having a range property
+		rangeMin.setPrecision( 0 ).setRange( 0, Infinity ).setNudge( 1 ).setStep( 10 ).setUnit( 'nm' );
+		rangeMax.setPrecision( 0 ).setRange( 0, Infinity ).setNudge( 1 ).setStep( 10 ).setUnit( 'nm' );
+
+	}
+
 	let object = null;
 	let material = null;
 
@@ -127,6 +158,18 @@ function SidebarMaterialMapProperty( editor, property, name ) {
 
 	}
 
+	function onRangeChange() {
+
+		const value = [ rangeMin.getValue(), rangeMax.getValue() ];
+
+		if ( material[ `${ mapType }Range` ][ 0 ] !== value[ 0 ] || material[ `${ mapType }Range` ][ 1 ] !== value[ 1 ] ) {
+
+			editor.execute( new SetMaterialRangeCommand( editor, object, `${ mapType }Range`, value[ 0 ], value[ 1 ], 0 /* TODOL currentMaterialSlot */ ) );
+
+		}
+
+	}
+
 	function update() {
 
 		if ( object === null ) return;
@@ -164,6 +207,13 @@ function SidebarMaterialMapProperty( editor, property, name ) {
 
 			}
 
+			if ( rangeMin !== undefined ) {
+
+				rangeMin.setValue( material[ `${ mapType }Range` ][ 0 ] );
+				rangeMax.setValue( material[ `${ mapType }Range` ][ 1 ] );
+
+			}
+
 			container.setDisplay( '' );
 
 		} else {

+ 66 - 0
editor/js/Sidebar.Material.RangeValueProperty.js

@@ -0,0 +1,66 @@
+import { UINumber, UIRow, UIText } from './libs/ui.js';
+import { SetMaterialRangeCommand } from './commands/SetMaterialRangeCommand.js';
+
+function SidebarMaterialRangeValueProperty( editor, property, name, isMin, range = [ - Infinity, Infinity ], precision = 2, step = 1, nudge = 0.01, unit = '' ) {
+
+	const signals = editor.signals;
+
+	const container = new UIRow();
+	container.add( new UIText( name ).setWidth( '90px' ) );
+
+	const number = new UINumber().setWidth( '60px' ).setRange( range[ 0 ], range[ 1 ] ).setPrecision( precision ).setStep( step ).setNudge( nudge ).setUnit( unit ).onChange( onChange );
+	container.add( number );
+
+	let object = null;
+	let material = null;
+
+	function onChange() {
+
+		if ( material[ property ][ isMin ? 0 : 1 ] !== number.getValue() ) {
+
+			const minValue = isMin ? number.getValue() : material[ property ][ 0 ];
+			const maxValue = isMin ? material[ property ][ 1 ] : number.getValue();
+
+			editor.execute( new SetMaterialRangeCommand( editor, object, property, minValue, maxValue, 0 /* TODO: currentMaterialSlot */ ) );
+
+		}
+
+	}
+
+	function update() {
+
+		if ( object === null ) return;
+		if ( object.material === undefined ) return;
+
+		material = object.material;
+
+		if ( property in material ) {
+
+			number.setValue( material[ property ][ isMin ? 0 : 1 ] );
+			container.setDisplay( '' );
+
+		} else {
+
+			container.setDisplay( 'none' );
+
+		}
+
+	}
+
+	//
+
+	signals.objectSelected.add( function ( selected ) {
+
+		object = selected;
+
+		update();
+
+	} );
+
+	signals.materialChanged.add( update );
+
+	return container;
+
+}
+
+export { SidebarMaterialRangeValueProperty };

+ 26 - 0
editor/js/Sidebar.Material.js

@@ -10,6 +10,7 @@ import { SidebarMaterialColorProperty } from './Sidebar.Material.ColorProperty.j
 import { SidebarMaterialConstantProperty } from './Sidebar.Material.ConstantProperty.js';
 import { SidebarMaterialMapProperty } from './Sidebar.Material.MapProperty.js';
 import { SidebarMaterialNumberProperty } from './Sidebar.Material.NumberProperty.js';
+import { SidebarMaterialRangeValueProperty } from './Sidebar.Material.RangeValueProperty.js';
 import { SidebarMaterialProgram } from './Sidebar.Material.Program.js';
 
 function SidebarMaterial( editor ) {
@@ -130,6 +131,21 @@ function SidebarMaterial( editor ) {
 	const materialClearcoatRoughness = new SidebarMaterialNumberProperty( editor, 'clearcoatRoughness', strings.getKey( 'sidebar/material/clearcoatroughness' ), [ 0, 1 ] );
 	container.add( materialClearcoatRoughness );
 
+	// iridescence
+
+	const materialIridescence = new SidebarMaterialNumberProperty( editor, 'iridescence', strings.getKey( 'sidebar/material/iridescence' ), [ 0, 1 ] );
+	container.add( materialIridescence );
+
+	// iridescenceIOR
+
+	const materialIridescenceIOR = new SidebarMaterialNumberProperty( editor, 'iridescenceIOR', strings.getKey( 'sidebar/material/iridescenceIOR' ), [ 1, 5 ] );
+	container.add( materialIridescenceIOR );
+
+	// iridescenceThicknessMax
+
+	const materialIridescenceThicknessMax = new SidebarMaterialRangeValueProperty( editor, 'iridescenceThicknessRange', strings.getKey( 'sidebar/material/iridescenceThicknessMax' ), false, [ 0, Infinity ], 0, 10, 1, 'nm' );
+	container.add( materialIridescenceThicknessMax );
+
 	// transmission
 
 	const materialTransmission = new SidebarMaterialNumberProperty( editor, 'transmission', strings.getKey( 'sidebar/material/transmission' ), [ 0, 1 ] );
@@ -220,6 +236,16 @@ function SidebarMaterial( editor ) {
 	const materialMetalnessMap = new SidebarMaterialMapProperty( editor, 'metalnessMap', strings.getKey( 'sidebar/material/metalnessmap' ) );
 	container.add( materialMetalnessMap );
 
+	// iridescence map
+
+	const materialIridescenceMap = new SidebarMaterialMapProperty( editor, 'iridescenceMap', strings.getKey( 'sidebar/material/iridescencemap' ) );
+	container.add( materialIridescenceMap );
+
+	// iridescence thickness map
+
+	const materialIridescenceThicknessMap = new SidebarMaterialMapProperty( editor, 'iridescenceThicknessMap', strings.getKey( 'sidebar/material/iridescencethicknessmap' ) );
+	container.add( materialIridescenceThicknessMap );
+
 	// env map
 
 	const materialEnvMap = new SidebarMaterialMapProperty( editor, 'envMap', strings.getKey( 'sidebar/material/envmap' ) );

+ 5 - 0
editor/js/Strings.js

@@ -257,6 +257,9 @@ function Strings( config ) {
 			'sidebar/material/shininess': 'Shininess',
 			'sidebar/material/clearcoat': 'Clearcoat',
 			'sidebar/material/clearcoatroughness': 'Clearcoat Roughness',
+			'sidebar/material/iridescence': 'Iridescence',
+			'sidebar/material/iridescenceIOR': 'Thin-Film IOR',
+			'sidebar/material/iridescenceThicknessMax': 'Thin-Film Thickness',
 			'sidebar/material/transmission': 'Transmission',
 			'sidebar/material/attenuationDistance': 'Attenuation Distance',
 			'sidebar/material/attenuationColor': 'Attenuation Color',
@@ -272,6 +275,8 @@ function Strings( config ) {
 			'sidebar/material/roughnessmap': 'Rough. Map',
 			'sidebar/material/metalnessmap': 'Metal. Map',
 			'sidebar/material/specularmap': 'Specular Map',
+			'sidebar/material/iridescencemap': 'Irid. Map',
+			'sidebar/material/iridescencethicknessmap': 'Thin-Film Thickness Map',
 			'sidebar/material/envmap': 'Env Map',
 			'sidebar/material/lightmap': 'Light Map',
 			'sidebar/material/aomap': 'AO Map',

+ 83 - 0
editor/js/commands/SetMaterialRangeCommand.js

@@ -0,0 +1,83 @@
+import { Command } from '../Command.js';
+
+/**
+ * @param editor Editor
+ * @param object THREE.Object3D
+ * @param attributeName string
+ * @param newMinValue number
+ * @param newMaxValue number
+ * @constructor
+ */
+class SetMaterialRangeCommand extends Command {
+
+	constructor( editor, object, attributeName, newMinValue, newMaxValue, materialSlot ) {
+
+		super( editor );
+
+		this.type = 'SetMaterialRangeCommand';
+		this.name = `Set Material.${attributeName}`;
+		this.updatable = true;
+
+		this.object = object;
+		this.material = this.editor.getObjectMaterial( object, materialSlot );
+
+		this.oldRange = ( this.material !== undefined && this.material[ attributeName ] !== undefined ) ? [ ...this.material[ attributeName ] ] : undefined;
+		this.newRange = [ newMinValue, newMaxValue ];
+
+		this.attributeName = attributeName;
+
+	}
+
+	execute() {
+
+		this.material[ this.attributeName ] = [ ...this.newRange ];
+		this.material.needsUpdate = true;
+
+		this.editor.signals.objectChanged.dispatch( this.object );
+		this.editor.signals.materialChanged.dispatch( this.material );
+
+	}
+
+	undo() {
+
+		this.material[ this.attributeName ] = [ ...this.oldRange ];
+		this.material.needsUpdate = true;
+
+		this.editor.signals.objectChanged.dispatch( this.object );
+		this.editor.signals.materialChanged.dispatch( this.material );
+
+	}
+
+	update( cmd ) {
+
+		this.newRange = [ ...cmd.newRange ];
+
+	}
+
+	toJSON() {
+
+		const output = super.toJSON( this );
+
+		output.objectUuid = this.object.uuid;
+		output.attributeName = this.attributeName;
+		output.oldRange = [ ...this.oldRange ];
+		output.newRange = [ ...this.newRange ];
+
+		return output;
+
+	}
+
+	fromJSON( json ) {
+
+		super.fromJSON( json );
+
+		this.attributeName = json.attributeName;
+		this.oldRange = [ ...json.oldRange ];
+		this.newRange = [ ...json.newRange ];
+		this.object = this.editor.objectByUuid( json.objectUuid );
+
+	}
+
+}
+
+export { SetMaterialRangeCommand };

+ 92 - 0
examples/js/loaders/GLTFLoader.js

@@ -53,6 +53,11 @@
 
 				return new GLTFMaterialsSpecularExtension( parser );
 
+			} );
+			this.register( function ( parser ) {
+
+				return new GLTFMaterialsIridescenceExtension( parser );
+
 			} );
 			this.register( function ( parser ) {
 
@@ -359,6 +364,7 @@
 		KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 'KHR_materials_pbrSpecularGlossiness',
 		KHR_MATERIALS_SHEEN: 'KHR_materials_sheen',
 		KHR_MATERIALS_SPECULAR: 'KHR_materials_specular',
+		KHR_MATERIALS_IRIDESCENCE: 'KHR_materials_iridescence',
 		KHR_MATERIALS_TRANSMISSION: 'KHR_materials_transmission',
 		KHR_MATERIALS_UNLIT: 'KHR_materials_unlit',
 		KHR_MATERIALS_VOLUME: 'KHR_materials_volume',
@@ -655,6 +661,92 @@
 
 		}
 
+	}
+	/**
+ * Iridescence Materials Extension
+ *
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_iridescence
+ */
+
+
+	class GLTFMaterialsIridescenceExtension {
+
+		constructor( parser ) {
+
+			this.parser = parser;
+			this.name = EXTENSIONS.KHR_MATERIALS_IRIDESCENCE;
+
+		}
+
+		getMaterialType( materialIndex ) {
+
+			const parser = this.parser;
+			const materialDef = parser.json.materials[ materialIndex ];
+			if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null;
+			return THREE.MeshPhysicalMaterial;
+
+		}
+
+		extendMaterialParams( materialIndex, materialParams ) {
+
+			const parser = this.parser;
+			const materialDef = parser.json.materials[ materialIndex ];
+
+			if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) {
+
+				return Promise.resolve();
+
+			}
+
+			const pending = [];
+			const extension = materialDef.extensions[ this.name ];
+
+			if ( extension.iridescenceFactor !== undefined ) {
+
+				materialParams.iridescence = extension.iridescenceFactor;
+
+			}
+
+			if ( extension.iridescenceTexture !== undefined ) {
+
+				pending.push( parser.assignTexture( materialParams, 'iridescenceMap', extension.iridescenceTexture ) );
+
+			}
+
+			if ( extension.iridescenceIor !== undefined ) {
+
+				materialParams.iridescenceIOR = extension.iridescenceIor;
+
+			}
+
+			if ( materialParams.iridescenceThicknessRange === undefined ) {
+
+				materialParams.iridescenceThicknessRange = [ 100, 400 ];
+
+			}
+
+			if ( extension.iridescenceThicknessMinimum !== undefined ) {
+
+				materialParams.iridescenceThicknessRange[ 0 ] = extension.iridescenceThicknessMinimum;
+
+			}
+
+			if ( extension.iridescenceThicknessMaximum !== undefined ) {
+
+				materialParams.iridescenceThicknessRange[ 1 ] = extension.iridescenceThicknessMaximum;
+
+			}
+
+			if ( extension.iridescenceThicknessTexture !== undefined ) {
+
+				pending.push( parser.assignTexture( materialParams, 'iridescenceThicknessMap', extension.iridescenceThicknessTexture ) );
+
+			}
+
+			return Promise.all( pending );
+
+		}
+
 	}
 	/**
  * Sheen Materials Extension

+ 60 - 0
examples/jsm/exporters/GLTFExporter.js

@@ -64,6 +64,12 @@ class GLTFExporter {
 
 		} );
 
+		this.register( function ( writer ) {
+
+			return new GLTFMaterialsIridescenceExtension( writer );
+
+		} );
+
 	}
 
 	register( callback ) {
@@ -2411,6 +2417,60 @@ class GLTFMaterialsClearcoatExtension {
 
 }
 
+/**
+ * Iridescence Materials Extension
+ *
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_iridescence
+ */
+class GLTFMaterialsIridescenceExtension {
+
+	constructor( writer ) {
+
+		this.writer = writer;
+		this.name = 'KHR_materials_iridescence';
+
+	}
+
+	writeMaterial( material, materialDef ) {
+
+		if ( ! material.isMeshPhysicalMaterial ) return;
+
+		const writer = this.writer;
+		const extensionsUsed = writer.extensionsUsed;
+
+		const extensionDef = {};
+
+		extensionDef.iridescenceFactor = material.iridescence;
+
+		if ( material.iridescenceMap ) {
+
+			const iridescenceMapDef = { index: writer.processTexture( material.iridescenceMap ) };
+			writer.applyTextureTransform( iridescenceMapDef, material.iridescenceMap );
+			extensionDef.iridescenceTexture = iridescenceMapDef;
+
+		}
+
+		extensionDef.iridescenceIor = material.iridescenceIOR;
+		extensionDef.iridescenceThicknessMinimum = material.iridescenceThicknessRange[ 0 ];
+		extensionDef.iridescenceThicknessMaximum = material.iridescenceThicknessRange[ 1 ];
+
+		if ( material.iridescenceThicknessMap ) {
+
+			const iridescenceThicknessMapDef = { index: writer.processTexture( material.iridescenceThicknessMap ) };
+			writer.applyTextureTransform( iridescenceThicknessMapDef, material.iridescenceThicknessMap );
+			extensionDef.iridescenceThicknessTexture = iridescenceThicknessMapDef;
+
+		}
+
+		materialDef.extensions = materialDef.extensions || {};
+		materialDef.extensions[ this.name ] = extensionDef;
+
+		extensionsUsed[ this.name ] = true;
+
+	}
+
+}
+
 /**
  * Transmission Materials Extension
  *

+ 95 - 0
examples/jsm/loaders/GLTFLoader.js

@@ -129,6 +129,12 @@ class GLTFLoader extends Loader {
 
 		} );
 
+		this.register( function ( parser ) {
+
+			return new GLTFMaterialsIridescenceExtension( parser );
+
+		} );
+
 		this.register( function ( parser ) {
 
 			return new GLTFLightsExtension( parser );
@@ -454,6 +460,7 @@ const EXTENSIONS = {
 	KHR_MATERIALS_SHEEN: 'KHR_materials_sheen',
 	KHR_MATERIALS_SPECULAR: 'KHR_materials_specular',
 	KHR_MATERIALS_TRANSMISSION: 'KHR_materials_transmission',
+	KHR_MATERIALS_IRIDESCENCE: 'KHR_materials_iridescence',
 	KHR_MATERIALS_UNLIT: 'KHR_materials_unlit',
 	KHR_MATERIALS_VOLUME: 'KHR_materials_volume',
 	KHR_TEXTURE_BASISU: 'KHR_texture_basisu',
@@ -768,6 +775,94 @@ class GLTFMaterialsClearcoatExtension {
 
 }
 
+/**
+ * Iridescence Materials Extension
+ *
+ * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_iridescence
+ */
+class GLTFMaterialsIridescenceExtension {
+
+	constructor( parser ) {
+
+		this.parser = parser;
+		this.name = EXTENSIONS.KHR_MATERIALS_IRIDESCENCE;
+
+	}
+
+	getMaterialType( materialIndex ) {
+
+		const parser = this.parser;
+		const materialDef = parser.json.materials[ materialIndex ];
+
+		if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) return null;
+
+		return MeshPhysicalMaterial;
+
+	}
+
+	extendMaterialParams( materialIndex, materialParams ) {
+
+		const parser = this.parser;
+		const materialDef = parser.json.materials[ materialIndex ];
+
+		if ( ! materialDef.extensions || ! materialDef.extensions[ this.name ] ) {
+
+			return Promise.resolve();
+
+		}
+
+		const pending = [];
+
+		const extension = materialDef.extensions[ this.name ];
+
+		if ( extension.iridescenceFactor !== undefined ) {
+
+			materialParams.iridescence = extension.iridescenceFactor;
+
+		}
+
+		if ( extension.iridescenceTexture !== undefined ) {
+
+			pending.push( parser.assignTexture( materialParams, 'iridescenceMap', extension.iridescenceTexture ) );
+
+		}
+
+		if ( extension.iridescenceIor !== undefined ) {
+
+			materialParams.iridescenceIOR = extension.iridescenceIor;
+
+		}
+
+		if ( materialParams.iridescenceThicknessRange === undefined ) {
+
+			materialParams.iridescenceThicknessRange = [ 100, 400 ];
+
+		}
+
+		if ( extension.iridescenceThicknessMinimum !== undefined ) {
+
+			materialParams.iridescenceThicknessRange[ 0 ] = extension.iridescenceThicknessMinimum;
+
+		}
+
+		if ( extension.iridescenceThicknessMaximum !== undefined ) {
+
+			materialParams.iridescenceThicknessRange[ 1 ] = extension.iridescenceThicknessMaximum;
+
+		}
+
+		if ( extension.iridescenceThicknessTexture !== undefined ) {
+
+			pending.push( parser.assignTexture( materialParams, 'iridescenceThicknessMap', extension.iridescenceThicknessTexture ) );
+
+		}
+
+		return Promise.all( pending );
+
+	}
+
+}
+
 /**
  * Sheen Materials Extension
  *

+ 51 - 0
examples/jsm/renderers/webgl/nodes/WebGLNodeBuilder.js

@@ -141,6 +141,24 @@ class WebGLNodeBuilder extends NodeBuilder {
 
 		}
 
+		if ( material.iridescenceNode && material.iridescenceNode.isNode ) {
+
+			this.addSlot( 'fragment', new SlotNode( material.iridescenceNode, 'IRIDESCENCE', 'float' ) );
+
+		}
+
+		if ( material.iridescenceIORNode && material.iridescenceIORNode.isNode ) {
+
+			this.addSlot( 'fragment', new SlotNode( material.iridescenceIORNode, 'IRIDESCENCE_IOR', 'float' ) );
+
+		}
+
+		if ( material.iridescenceThicknessNode && material.iridescenceThicknessNode.isNode ) {
+
+			this.addSlot( 'fragment', new SlotNode( material.iridescenceThicknessNode, 'IRIDESCENCE_THICKNESS', 'float' ) );
+
+		}
+
 		if ( material.envNode && material.envNode.isNode ) {
 
 			const envRadianceNode = new WebGLPhysicalContextNode( WebGLPhysicalContextNode.RADIANCE, material.envNode );
@@ -423,6 +441,9 @@ ${this.shader[ getShaderStageProperty( shaderStage ) ]}
 		const metalnessNode = this.getSlot( 'fragment', 'METALNESS' );
 		const clearcoatNode = this.getSlot( 'fragment', 'CLEARCOAT' );
 		const clearcoatRoughnessNode = this.getSlot( 'fragment', 'CLEARCOAT_ROUGHNESS' );
+		const iridescenceNode = this.getSlot( 'fragment', 'IRIDESCENCE' );
+		const iridescenceIORNode = this.getSlot( 'fragment', 'IRIDESCENCE_IOR' );
+		const iridescenceThicknessNode = this.getSlot( 'fragment', 'IRIDESCENCE_THICKNESS' );
 
 		const positionNode = this.getSlot( 'vertex', 'POSITION' );
 		const sizeNode = this.getSlot( 'vertex', 'SIZE' );
@@ -507,6 +528,36 @@ ${this.shader[ getShaderStageProperty( shaderStage ) ]}
 
 		}
 
+		if ( iridescenceNode !== undefined ) {
+
+			this.addCodeAfterSnippet(
+				'fragment',
+				'iridescence_fragment',
+				`${iridescenceNode.code}\n\tmaterial.iridescence = ${iridescenceNode.result};`
+			);
+
+		}
+
+		if ( iridescenceIORNode !== undefined ) {
+
+			this.addCodeAfterSnippet(
+				'fragment',
+				'iridescence_fragment',
+				`${iridescenceIORNode.code}\n\tmaterial.iridescenceIOR = ${iridescenceIORNode.result};`
+			);
+
+		}
+
+		if ( iridescenceThicknessNode !== undefined ) {
+
+			this.addCodeAfterSnippet(
+				'fragment',
+				'iridescence_fragment',
+				`${iridescenceThicknessNode.code}\n\tmaterial.iridescenceThickness = ${iridescenceThicknessNode.result};`
+			);
+
+		}
+
 		if ( positionNode !== undefined ) {
 
 			this.addCodeAfterInclude(

+ 6 - 0
src/loaders/MaterialLoader.js

@@ -84,6 +84,9 @@ class MaterialLoader extends Loader {
 		if ( json.shininess !== undefined ) material.shininess = json.shininess;
 		if ( json.clearcoat !== undefined ) material.clearcoat = json.clearcoat;
 		if ( json.clearcoatRoughness !== undefined ) material.clearcoatRoughness = json.clearcoatRoughness;
+		if ( json.iridescence !== undefined ) material.iridescence = json.iridescence;
+		if ( json.iridescenceIOR !== undefined ) material.iridescenceIOR = json.iridescenceIOR;
+		if ( json.iridescenceThicknessRange !== undefined ) material.iridescenceThicknessRange = json.iridescenceThicknessRange;
 		if ( json.transmission !== undefined ) material.transmission = json.transmission;
 		if ( json.thickness !== undefined ) material.thickness = json.thickness;
 		if ( json.attenuationDistance !== undefined ) material.attenuationDistance = json.attenuationDistance;
@@ -284,6 +287,9 @@ class MaterialLoader extends Loader {
 		if ( json.clearcoatNormalMap !== undefined ) material.clearcoatNormalMap = getTexture( json.clearcoatNormalMap );
 		if ( json.clearcoatNormalScale !== undefined ) material.clearcoatNormalScale = new Vector2().fromArray( json.clearcoatNormalScale );
 
+		if ( json.iridescenceMap !== undefined ) material.iridescenceMap = getTexture( json.iridescenceMap );
+		if ( json.iridescenceThicknessMap !== undefined ) material.iridescenceThicknessMap = getTexture( json.iridescenceThicknessMap );
+
 		if ( json.transmissionMap !== undefined ) material.transmissionMap = getTexture( json.transmissionMap );
 		if ( json.thicknessMap !== undefined ) material.thicknessMap = getTexture( json.thicknessMap );
 

+ 16 - 0
src/materials/Material.js

@@ -222,6 +222,22 @@ class Material extends EventDispatcher {
 
 		}
 
+		if ( this.iridescence !== undefined ) data.iridescence = this.iridescence;
+		if ( this.iridescenceIOR !== undefined ) data.iridescenceIOR = this.iridescenceIOR;
+		if ( this.iridescenceThicknessRange !== undefined ) data.iridescenceThicknessRange = this.iridescenceThicknessRange;
+
+		if ( this.iridescenceMap && this.iridescenceMap.isTexture ) {
+
+			data.iridescenceMap = this.iridescenceMap.toJSON( meta ).uuid;
+
+		}
+
+		if ( this.iridescenceThicknessMap && this.iridescenceThicknessMap.isTexture ) {
+
+			data.iridescenceThicknessMap = this.iridescenceThicknessMap.toJSON( meta ).uuid;
+
+		}
+
 		if ( this.map && this.map.isTexture ) data.map = this.map.toJSON( meta ).uuid;
 		if ( this.matcap && this.matcap.isTexture ) data.matcap = this.matcap.toJSON( meta ).uuid;
 		if ( this.alphaMap && this.alphaMap.isTexture ) data.alphaMap = this.alphaMap.toJSON( meta ).uuid;

+ 30 - 0
src/materials/MeshPhysicalMaterial.js

@@ -41,6 +41,11 @@ class MeshPhysicalMaterial extends MeshStandardMaterial {
 			}
 		} );
 
+		this.iridescenceMap = null;
+		this.iridescenceIOR = 1.3;
+		this.iridescenceThicknessRange = [ 100, 400 ];
+		this.iridescenceThicknessMap = null;
+
 		this.sheenColor = new Color( 0x000000 );
 		this.sheenColorMap = null;
 		this.sheenRoughness = 1.0;
@@ -60,6 +65,7 @@ class MeshPhysicalMaterial extends MeshStandardMaterial {
 
 		this._sheen = 0.0;
 		this._clearcoat = 0;
+		this._iridescence = 0;
 		this._transmission = 0;
 
 		this.setValues( parameters );
@@ -102,6 +108,24 @@ class MeshPhysicalMaterial extends MeshStandardMaterial {
 
 	}
 
+	get iridescence() {
+
+		return this._iridescence;
+
+	}
+
+	set iridescence( value ) {
+
+		if ( this._iridescence > 0 !== value > 0 ) {
+
+			this.version ++;
+
+		}
+
+		this._iridescence = value;
+
+	}
+
 	get transmission() {
 
 		return this._transmission;
@@ -140,6 +164,12 @@ class MeshPhysicalMaterial extends MeshStandardMaterial {
 
 		this.ior = source.ior;
 
+		this.iridescence = source.iridescence;
+		this.iridescenceMap = source.iridescenceMap;
+		this.iridescenceIOR = source.iridescenceIOR;
+		this.iridescenceThicknessRange = [ ...source.iridescenceThicknessRange ];
+		this.iridescenceThicknessMap = source.iridescenceThicknessMap;
+
 		this.sheen = source.sheen;
 		this.sheenColor.copy( source.sheenColor );
 		this.sheenColorMap = source.sheenColorMap;

+ 4 - 0
src/renderers/shaders/ShaderChunk.js

@@ -7,6 +7,7 @@ import aomap_pars_fragment from './ShaderChunk/aomap_pars_fragment.glsl.js';
 import begin_vertex from './ShaderChunk/begin_vertex.glsl.js';
 import beginnormal_vertex from './ShaderChunk/beginnormal_vertex.glsl.js';
 import bsdfs from './ShaderChunk/bsdfs.glsl.js';
+import iridescence_fragment from './ShaderChunk/iridescence_fragment.glsl.js';
 import bumpmap_pars_fragment from './ShaderChunk/bumpmap_pars_fragment.glsl.js';
 import clipping_planes_fragment from './ShaderChunk/clipping_planes_fragment.glsl.js';
 import clipping_planes_pars_fragment from './ShaderChunk/clipping_planes_pars_fragment.glsl.js';
@@ -72,6 +73,7 @@ import normalmap_pars_fragment from './ShaderChunk/normalmap_pars_fragment.glsl.
 import clearcoat_normal_fragment_begin from './ShaderChunk/clearcoat_normal_fragment_begin.glsl.js';
 import clearcoat_normal_fragment_maps from './ShaderChunk/clearcoat_normal_fragment_maps.glsl.js';
 import clearcoat_pars_fragment from './ShaderChunk/clearcoat_pars_fragment.glsl.js';
+import iridescence_pars_fragment from './ShaderChunk/iridescence_pars_fragment.glsl.js';
 import output_fragment from './ShaderChunk/output_fragment.glsl.js';
 import packing from './ShaderChunk/packing.glsl.js';
 import premultiplied_alpha_fragment from './ShaderChunk/premultiplied_alpha_fragment.glsl.js';
@@ -129,6 +131,7 @@ export const ShaderChunk = {
 	begin_vertex: begin_vertex,
 	beginnormal_vertex: beginnormal_vertex,
 	bsdfs: bsdfs,
+	iridescence_fragment: iridescence_fragment,
 	bumpmap_pars_fragment: bumpmap_pars_fragment,
 	clipping_planes_fragment: clipping_planes_fragment,
 	clipping_planes_pars_fragment: clipping_planes_pars_fragment,
@@ -194,6 +197,7 @@ export const ShaderChunk = {
 	clearcoat_normal_fragment_begin: clearcoat_normal_fragment_begin,
 	clearcoat_normal_fragment_maps: clearcoat_normal_fragment_maps,
 	clearcoat_pars_fragment: clearcoat_pars_fragment,
+	iridescence_pars_fragment: iridescence_pars_fragment,
 	output_fragment: output_fragment,
 	packing: packing,
 	premultiplied_alpha_fragment: premultiplied_alpha_fragment,

+ 46 - 0
src/renderers/shaders/ShaderChunk/bsdfs.glsl.js

@@ -19,6 +19,27 @@ vec3 F_Schlick( const in vec3 f0, const in float f90, const in float dotVH ) {
 
 } // validated
 
+float F_Schlick( const in float f0, const in float f90, const in float dotVH ) {
+
+	// Original approximation by Christophe Schlick '94
+	// float fresnel = pow( 1.0 - dotVH, 5.0 );
+
+	// Optimized variant (presented by Epic at SIGGRAPH '13)
+	// https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
+	float fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH );
+
+	return f0 * ( 1.0 - fresnel ) + ( f90 * fresnel );
+
+} // validated
+
+vec3 Schlick_to_F0( const in vec3 f, const in float f90, const in float dotVH ) {
+    float x = clamp( 1.0 - dotVH, 0.0, 1.0 );
+    float x2 = x * x;
+    float x5 = clamp( x * x2 * x2, 0.0, 0.9999 );
+
+    return ( f - vec3( f90 ) * x5 ) / ( 1.0 - x5 );
+}
+
 // Moving Frostbite to Physically Based Rendering 3.0 - page 12, listing 2
 // https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf
 float V_GGX_SmithCorrelated( const in float alpha, const in float dotNL, const in float dotNV ) {
@@ -67,6 +88,31 @@ vec3 BRDF_GGX( const in vec3 lightDir, const in vec3 viewDir, const in vec3 norm
 
 }
 
+#ifdef USE_IRIDESCENCE
+
+vec3 BRDF_GGX_Iridescence( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in vec3 f0, const in float f90, const in float iridescence, const in vec3 iridescenceFresnel, const in float roughness ) {
+
+	float alpha = pow2( roughness ); // UE4's roughness
+
+	vec3 halfDir = normalize( lightDir + viewDir );
+
+	float dotNL = saturate( dot( normal, lightDir ) );
+	float dotNV = saturate( dot( normal, viewDir ) );
+	float dotNH = saturate( dot( normal, halfDir ) );
+	float dotVH = saturate( dot( viewDir, halfDir ) );
+
+	vec3 F = mix(F_Schlick( f0, f90, dotVH ), iridescenceFresnel, iridescence);
+
+	float V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV );
+
+	float D = D_GGX( alpha, dotNH );
+
+	return F * ( V * D );
+
+}
+
+#endif
+
 // Rect Area Light
 
 // Real-Time Polygonal-Light Shading with Linearly Transformed Cosines

+ 1 - 0
src/renderers/shaders/ShaderChunk/common.glsl.js

@@ -13,6 +13,7 @@ export default /* glsl */`
 #define whiteComplement( a ) ( 1.0 - saturate( a ) )
 
 float pow2( const in float x ) { return x*x; }
+vec3 pow2( const in vec3 x ) { return x*x; }
 float pow3( const in float x ) { return x*x*x; }
 float pow4( const in float x ) { float x2 = x*x; return x2*x2; }
 float max3( const in vec3 v ) { return max( max( v.x, v.y ), v.z ); }

+ 107 - 0
src/renderers/shaders/ShaderChunk/iridescence_fragment.glsl.js

@@ -0,0 +1,107 @@
+export default /* glsl */`
+
+#ifdef USE_IRIDESCENCE
+
+// XYZ to sRGB color space
+const mat3 XYZ_TO_REC709 = mat3(
+    3.2404542, -0.9692660,  0.0556434,
+   -1.5371385,  1.8760108, -0.2040259,
+   -0.4985314,  0.0415560,  1.0572252
+);
+
+// Assume air interface for top
+// Note: We don't handle the case fresnel0 == 1
+vec3 Fresnel0ToIor( vec3 fresnel0 ) {
+   vec3 sqrtF0 = sqrt( fresnel0 );
+   return ( vec3( 1.0 ) + sqrtF0 ) / ( vec3( 1.0 ) - sqrtF0 );
+}
+
+// Conversion FO/IOR
+vec3 IorToFresnel0( vec3 transmittedIor, float incidentIor ) {
+   return pow2( ( transmittedIor - vec3( incidentIor ) ) / ( transmittedIor + vec3( incidentIor ) ) );
+}
+
+// ior is a value between 1.0 and 3.0. 1.0 is air interface
+float IorToFresnel0( float transmittedIor, float incidentIor ) {
+   return pow2( ( transmittedIor - incidentIor ) / ( transmittedIor + incidentIor ));
+}
+
+// Fresnel equations for dielectric/dielectric interfaces.
+// Ref: https://belcour.github.io/blog/research/2017/05/01/brdf-thin-film.html
+// Evaluation XYZ sensitivity curves in Fourier space
+vec3 evalSensitivity( float OPD, vec3 shift ) {
+   float phase = 2.0 * PI * OPD * 1.0e-9;
+   vec3 val = vec3( 5.4856e-13, 4.4201e-13, 5.2481e-13 );
+   vec3 pos = vec3( 1.6810e+06, 1.7953e+06, 2.2084e+06 );
+   vec3 var = vec3( 4.3278e+09, 9.3046e+09, 6.6121e+09 );
+
+   vec3 xyz = val * sqrt( 2.0 * PI * var ) * cos( pos * phase + shift ) * exp( -pow2( phase ) * var );
+   xyz.x += 9.7470e-14 * sqrt( 2.0 * PI * 4.5282e+09 ) * cos( 2.2399e+06 * phase + shift[0] ) * exp( -4.5282e+09 * pow2( phase ) );
+   xyz /= 1.0685e-7;
+
+   vec3 srgb = XYZ_TO_REC709 * xyz;
+   return srgb;
+}
+
+vec3 evalIridescence( float outsideIOR, float eta2, float cosTheta1, float thinFilmThickness, vec3 baseF0 ) {
+   vec3 I;
+
+   // Force iridescenceIOR -> outsideIOR when thinFilmThickness -> 0.0
+   float iridescenceIOR = mix( outsideIOR, eta2, smoothstep( 0.0, 0.03, thinFilmThickness ) );
+   // Evaluate the cosTheta on the base layer (Snell law)
+   float sinTheta2Sq = pow2( outsideIOR / iridescenceIOR ) * ( 1.0 - pow2( cosTheta1 ) );
+
+   // Handle TIR:
+   float cosTheta2Sq = 1.0 - sinTheta2Sq;
+   if ( cosTheta2Sq < 0.0 ) {
+       return vec3( 1.0 );
+   }
+
+   float cosTheta2 = sqrt( cosTheta2Sq );
+
+   // First interface
+   float R0 = IorToFresnel0( iridescenceIOR, outsideIOR );
+   float R12 = F_Schlick( R0, 1.0, cosTheta1 );
+   float R21 = R12;
+   float T121 = 1.0 - R12;
+   float phi12 = 0.0;
+   if ( iridescenceIOR < outsideIOR ) phi12 = PI;
+   float phi21 = PI - phi12;
+
+   // Second interface
+   vec3 baseIOR = Fresnel0ToIor( clamp( baseF0, 0.0, 0.9999 ) ); // guard against 1.0
+   vec3 R1 = IorToFresnel0( baseIOR, iridescenceIOR );
+   vec3 R23 = F_Schlick( R1, 1.0, cosTheta2 );
+   vec3 phi23 = vec3( 0.0 );
+   if ( baseIOR[0] < iridescenceIOR ) phi23[0] = PI;
+   if ( baseIOR[1] < iridescenceIOR ) phi23[1] = PI;
+   if ( baseIOR[2] < iridescenceIOR ) phi23[2] = PI;
+
+   // Phase shift
+   float OPD = 2.0 * iridescenceIOR * thinFilmThickness * cosTheta2;
+   vec3 phi = vec3( phi21 ) + phi23;
+
+   // Compound terms
+   vec3 R123 = clamp( R12 * R23, 1e-5, 0.9999 );
+   vec3 r123 = sqrt( R123 );
+   vec3 Rs = pow2( T121 ) * R23 / ( vec3( 1.0 ) - R123 );
+
+   // Reflectance term for m = 0 (DC term amplitude)
+   vec3 C0 = R12 + Rs;
+   I = C0;
+
+   // Reflectance term for m > 0 (pairs of diracs)
+   vec3 Cm = Rs - T121;
+   for ( int m = 1; m <= 2; ++m ) {
+       Cm *= r123;
+       vec3 Sm = 2.0 * evalSensitivity( float( m ) * OPD, float( m ) * phi );
+       I += Cm * Sm;
+   }
+
+   // Since out of gamut colors might be produced, negative color values are clamped to 0.
+   return max( I, vec3( 0.0 ) );
+}
+
+#endif
+
+`;

+ 14 - 0
src/renderers/shaders/ShaderChunk/iridescence_pars_fragment.glsl.js

@@ -0,0 +1,14 @@
+export default /* glsl */`
+
+#ifdef USE_IRIDESCENCEMAP
+
+	uniform sampler2D iridescenceMap;
+
+#endif
+
+#ifdef USE_IRIDESCENCE_THICKNESSMAP
+
+	uniform sampler2D iridescenceThicknessMap;
+
+#endif
+`;

+ 25 - 0
src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js

@@ -26,6 +26,31 @@ geometry.viewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( vViewPositi
 
 #endif
 
+#ifdef USE_IRIDESCENCE
+
+float dotNV = saturate( dot( normal, geometry.viewDir ) );
+
+if ( material.iridescenceThickness == 0.0 ) {
+
+	material.iridescence = 0.0;
+
+} else {
+
+	material.iridescence = saturate( material.iridescence );
+
+}
+
+if ( material.iridescence > 0.0 ) {
+
+	material.iridescenceFresnel = evalIridescence( 1.0, material.iridescenceIOR, dotNV, material.iridescenceThickness, material.specularColor );
+
+	// Iridescence F0 approximation
+	material.iridescenceF0 = Schlick_to_F0( material.iridescenceFresnel, 1.0, dotNV );
+
+}
+
+#endif
+
 IncidentLight directLight;
 
 #if ( NUM_POINT_LIGHTS > 0 ) && defined( RE_Direct )

+ 23 - 0
src/renderers/shaders/ShaderChunk/lights_physical_fragment.glsl.js

@@ -73,6 +73,29 @@ material.roughness = min( material.roughness, 1.0 );
 
 #endif
 
+#ifdef USE_IRIDESCENCE
+
+	material.iridescence = iridescence;
+	material.iridescenceIOR = iridescenceIOR;
+
+	#ifdef USE_IRIDESCENCEMAP
+
+		material.iridescence *= texture2D( iridescenceMap, vUv ).r;
+
+	#endif
+
+	#ifdef USE_IRIDESCENCE_THICKNESSMAP
+
+		material.iridescenceThickness = (iridescenceThicknessMaximum - iridescenceThicknessMinimum) * texture2D( iridescenceThicknessMap, vUv ).g + iridescenceThicknessMinimum;
+
+	#else
+
+		material.iridescenceThickness = iridescenceThicknessMaximum;
+
+	#endif
+
+#endif
+
 #ifdef USE_SHEEN
 
 	material.sheenColor = sheenColor;

+ 43 - 5
src/renderers/shaders/ShaderChunk/lights_physical_pars_fragment.glsl.js

@@ -13,6 +13,14 @@ struct PhysicalMaterial {
 		float clearcoatF90;
 	#endif
 
+	#ifdef USE_IRIDESCENCE
+		float iridescence;
+		float iridescenceIOR;
+		float iridescenceThickness;
+		vec3 iridescenceFresnel;
+		vec3 iridescenceF0;
+	#endif
+
 	#ifdef USE_SHEEN
 		vec3 sheenColor;
 		float sheenRoughness;
@@ -76,16 +84,30 @@ vec3 EnvironmentBRDF( const in vec3 normal, const in vec3 viewDir, const in vec3
 // Fdez-Agüera's "Multiple-Scattering Microfacet Model for Real-Time Image Based Lighting"
 // Approximates multiscattering in order to preserve energy.
 // http://www.jcgt.org/published/0008/01/03/
+#ifdef USE_IRIDESCENCE
+void computeMultiscatteringIridescence( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float iridescence, const in vec3 iridescenceF0, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) {
+#else
 void computeMultiscattering( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) {
+#endif
 
 	vec2 fab = DFGApprox( normal, viewDir, roughness );
 
-	vec3 FssEss = specularColor * fab.x + specularF90 * fab.y;
+	#ifdef USE_IRIDESCENCE
+
+		vec3 Fr = mix( specularColor, iridescenceF0, iridescence );
+
+	#else
+
+		vec3 Fr = specularColor;
+
+	#endif
+
+	vec3 FssEss = Fr * fab.x + specularF90 * fab.y;
 
 	float Ess = fab.x + fab.y;
 	float Ems = 1.0 - Ess;
 
-	vec3 Favg = specularColor + ( 1.0 - specularColor ) * 0.047619; // 1/21
+	vec3 Favg = Fr + ( 1.0 - Fr ) * 0.047619; // 1/21
 	vec3 Fms = FssEss * Favg / ( 1.0 - Ems * Favg );
 
 	singleScatter += FssEss;
@@ -157,8 +179,15 @@ void RE_Direct_Physical( const in IncidentLight directLight, const in GeometricC
 
 	#endif
 
-	reflectedLight.directSpecular += irradiance * BRDF_GGX( directLight.direction, geometry.viewDir, geometry.normal, material.specularColor, material.specularF90, material.roughness );
+	#ifdef USE_IRIDESCENCE
 
+		reflectedLight.directSpecular += irradiance * BRDF_GGX_Iridescence( directLight.direction, geometry.viewDir, geometry.normal, material.specularColor, material.specularF90, material.iridescence, material.iridescenceFresnel, material.roughness );
+
+	#else
+
+		reflectedLight.directSpecular += irradiance * BRDF_GGX( directLight.direction, geometry.viewDir, geometry.normal, material.specularColor, material.specularF90, material.roughness );
+
+	#endif
 
 	reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor );
 }
@@ -189,9 +218,18 @@ void RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 irradia
 	vec3 multiScattering = vec3( 0.0 );
 	vec3 cosineWeightedIrradiance = irradiance * RECIPROCAL_PI;
 
-	computeMultiscattering( geometry.normal, geometry.viewDir, material.specularColor, material.specularF90, material.roughness, singleScattering, multiScattering );
+	#ifdef USE_IRIDESCENCE
+
+		computeMultiscatteringIridescence( geometry.normal, geometry.viewDir, material.specularColor, material.specularF90, material.iridescence, material.iridescenceFresnel, material.roughness, singleScattering, multiScattering );
+
+	#else
+
+		computeMultiscattering( geometry.normal, geometry.viewDir, material.specularColor, material.specularF90, material.roughness, singleScattering, multiScattering );
+
+	#endif
 
-	vec3 diffuse = material.diffuseColor * ( 1.0 - ( singleScattering + multiScattering ) );
+	vec3 totalScattering = singleScattering + multiScattering;
+	vec3 diffuse = material.diffuseColor * ( 1.0 - max( max( totalScattering.r, totalScattering.g ), totalScattering.b ) );
 
 	reflectedLight.indirectSpecular += radiance * singleScattering;
 	reflectedLight.indirectSpecular += multiScattering * cosineWeightedIrradiance;

+ 6 - 0
src/renderers/shaders/ShaderLib.js

@@ -296,6 +296,12 @@ ShaderLib.physical = {
 			clearcoatRoughnessMap: { value: null },
 			clearcoatNormalScale: { value: new Vector2( 1, 1 ) },
 			clearcoatNormalMap: { value: null },
+			iridescence: { value: 0 },
+			iridescenceMap: { value: null },
+			iridescenceIOR: { value: 1.3 },
+			iridescenceThicknessMinimum: { value: 100 },
+			iridescenceThicknessMaximum: { value: 400 },
+			iridescenceThicknessMap: { value: null },
 			sheen: { value: 0 },
 			sheenColor: { value: new Color( 0x000000 ) },
 			sheenColorMap: { value: null },

+ 9 - 0
src/renderers/shaders/ShaderLib/meshphysical.glsl.js

@@ -94,6 +94,13 @@ uniform float opacity;
 	uniform float clearcoatRoughness;
 #endif
 
+#ifdef USE_IRIDESCENCE
+	uniform float iridescence;
+	uniform float iridescenceIOR;
+	uniform float iridescenceThicknessMinimum;
+	uniform float iridescenceThicknessMaximum;
+#endif
+
 #ifdef USE_SHEEN
 	uniform vec3 sheenColor;
 	uniform float sheenRoughness;
@@ -122,6 +129,7 @@ varying vec3 vViewPosition;
 #include <lightmap_pars_fragment>
 #include <emissivemap_pars_fragment>
 #include <bsdfs>
+#include <iridescence_fragment>
 #include <cube_uv_reflection_fragment>
 #include <envmap_common_pars_fragment>
 #include <envmap_physical_pars_fragment>
@@ -134,6 +142,7 @@ varying vec3 vViewPosition;
 #include <bumpmap_pars_fragment>
 #include <normalmap_pars_fragment>
 #include <clearcoat_pars_fragment>
+#include <iridescence_pars_fragment>
 #include <roughnessmap_pars_fragment>
 #include <metalnessmap_pars_fragment>
 #include <logdepthbuf_pars_fragment>

+ 35 - 4
src/renderers/webgl/WebGLMaterials.js

@@ -214,10 +214,12 @@ function WebGLMaterials( renderer, properties ) {
 		// 10. clearcoat map
 		// 11. clearcoat normal map
 		// 12. clearcoat roughnessMap map
-		// 13. specular intensity map
-		// 14. specular tint map
-		// 15. transmission map
-		// 16. thickness map
+		// 13. iridescence map
+		// 14. iridescence thickness map
+		// 15. specular intensity map
+		// 16. specular tint map
+		// 17. transmission map
+		// 18. thickness map
 
 		let uvScaleMap;
 
@@ -269,6 +271,14 @@ function WebGLMaterials( renderer, properties ) {
 
 			uvScaleMap = material.clearcoatRoughnessMap;
 
+		} else if ( material.iridescenceMap ) {
+
+			uvScaleMap = material.iridescenceMap;
+
+		} else if ( material.iridescenceThicknessMap ) {
+
+			uvScaleMap = material.iridescenceThicknessMap;
+
 		} else if ( material.specularIntensityMap ) {
 
 			uvScaleMap = material.specularIntensityMap;
@@ -576,6 +586,27 @@ function WebGLMaterials( renderer, properties ) {
 
 		}
 
+		if ( material.iridescence > 0 ) {
+
+			uniforms.iridescence.value = material.iridescence;
+			uniforms.iridescenceIOR.value = material.iridescenceIOR;
+			uniforms.iridescenceThicknessMinimum.value = material.iridescenceThicknessRange[ 0 ];
+			uniforms.iridescenceThicknessMaximum.value = material.iridescenceThicknessRange[ 1 ];
+
+			if ( material.iridescenceMap ) {
+
+				uniforms.iridescenceMap.value = material.iridescenceMap;
+
+			}
+
+			if ( material.iridescenceThicknessMap ) {
+
+				uniforms.iridescenceThicknessMap.value = material.iridescenceThicknessMap;
+
+			}
+
+		}
+
 		if ( material.transmission > 0 ) {
 
 			uniforms.transmission.value = material.transmission;

+ 7 - 0
src/renderers/webgl/WebGLProgram.js

@@ -478,6 +478,9 @@ function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) {
 			parameters.clearcoatRoughnessMap ? '#define USE_CLEARCOAT_ROUGHNESSMAP' : '',
 			parameters.clearcoatNormalMap ? '#define USE_CLEARCOAT_NORMALMAP' : '',
 
+			parameters.iridescenceMap ? '#define USE_IRIDESCENCEMAP' : '',
+			parameters.iridescenceThicknessMap ? '#define USE_IRIDESCENCE_THICKNESSMAP' : '',
+
 			parameters.displacementMap && parameters.supportsVertexTextures ? '#define USE_DISPLACEMENTMAP' : '',
 
 			parameters.specularMap ? '#define USE_SPECULARMAP' : '',
@@ -633,6 +636,10 @@ function WebGLProgram( renderer, cacheKey, parameters, bindingStates ) {
 			parameters.clearcoatRoughnessMap ? '#define USE_CLEARCOAT_ROUGHNESSMAP' : '',
 			parameters.clearcoatNormalMap ? '#define USE_CLEARCOAT_NORMALMAP' : '',
 
+			parameters.iridescence ? '#define USE_IRIDESCENCE' : '',
+			parameters.iridescenceMap ? '#define USE_IRIDESCENCEMAP' : '',
+			parameters.iridescenceThicknessMap ? '#define USE_IRIDESCENCE_THICKNESSMAP' : '',
+
 			parameters.specularMap ? '#define USE_SPECULARMAP' : '',
 			parameters.specularIntensityMap ? '#define USE_SPECULARINTENSITYMAP' : '',
 			parameters.specularColorMap ? '#define USE_SPECULARCOLORMAP' : '',

+ 26 - 15
src/renderers/webgl/WebGLPrograms.js

@@ -99,6 +99,7 @@ function WebGLPrograms( renderer, cubemaps, cubeuvmaps, extensions, capabilities
 
 		const useAlphaTest = material.alphaTest > 0;
 		const useClearcoat = material.clearcoat > 0;
+		const useIridescence = material.iridescence > 0;
 
 		const parameters = {
 
@@ -144,6 +145,10 @@ function WebGLPrograms( renderer, cubemaps, cubeuvmaps, extensions, capabilities
 			clearcoatRoughnessMap: useClearcoat && !! material.clearcoatRoughnessMap,
 			clearcoatNormalMap: useClearcoat && !! material.clearcoatNormalMap,
 
+			iridescence: useIridescence,
+			iridescenceMap: useIridescence && !! material.iridescenceMap,
+			iridescenceThicknessMap: useIridescence && !! material.iridescenceThicknessMap,
+
 			displacementMap: !! material.displacementMap,
 			roughnessMap: !! material.roughnessMap,
 			metalnessMap: !! material.metalnessMap,
@@ -171,8 +176,8 @@ function WebGLPrograms( renderer, cubemaps, cubeuvmaps, extensions, capabilities
 			vertexTangents: ( !! material.normalMap && !! geometry.attributes.tangent ),
 			vertexColors: material.vertexColors,
 			vertexAlphas: material.vertexColors === true && !! geometry.attributes.color && geometry.attributes.color.itemSize === 4,
-			vertexUvs: !! material.map || !! material.bumpMap || !! material.normalMap || !! material.specularMap || !! material.alphaMap || !! material.emissiveMap || !! material.roughnessMap || !! material.metalnessMap || !! material.clearcoatMap || !! material.clearcoatRoughnessMap || !! material.clearcoatNormalMap || !! material.displacementMap || !! material.transmissionMap || !! material.thicknessMap || !! material.specularIntensityMap || !! material.specularColorMap || !! material.sheenColorMap || !! material.sheenRoughnessMap,
-			uvsVertexOnly: ! ( !! material.map || !! material.bumpMap || !! material.normalMap || !! material.specularMap || !! material.alphaMap || !! material.emissiveMap || !! material.roughnessMap || !! material.metalnessMap || !! material.clearcoatNormalMap || material.transmission > 0 || !! material.transmissionMap || !! material.thicknessMap || !! material.specularIntensityMap || !! material.specularColorMap || material.sheen > 0 || !! material.sheenColorMap || !! material.sheenRoughnessMap ) && !! material.displacementMap,
+			vertexUvs: !! material.map || !! material.bumpMap || !! material.normalMap || !! material.specularMap || !! material.alphaMap || !! material.emissiveMap || !! material.roughnessMap || !! material.metalnessMap || !! material.clearcoatMap || !! material.clearcoatRoughnessMap || !! material.clearcoatNormalMap || !! material.iridescenceMap || !! material.iridescenceThicknessMap || !! material.displacementMap || !! material.transmissionMap || !! material.thicknessMap || !! material.specularIntensityMap || !! material.specularColorMap || !! material.sheenColorMap || !! material.sheenRoughnessMap,
+			uvsVertexOnly: ! ( !! material.map || !! material.bumpMap || !! material.normalMap || !! material.specularMap || !! material.alphaMap || !! material.emissiveMap || !! material.roughnessMap || !! material.metalnessMap || !! material.clearcoatNormalMap || !! material.iridescenceMap || !! material.iridescenceThicknessMap || material.transmission > 0 || !! material.transmissionMap || !! material.thicknessMap || !! material.specularIntensityMap || !! material.specularColorMap || material.sheen > 0 || !! material.sheenColorMap || !! material.sheenRoughnessMap ) && !! material.displacementMap,
 
 			fog: !! fog,
 			useFog: material.fog === true,
@@ -347,32 +352,38 @@ function WebGLPrograms( renderer, cubemaps, cubeuvmaps, extensions, capabilities
 			_programLayers.enable( 16 );
 		if ( parameters.clearcoatNormalMap )
 			_programLayers.enable( 17 );
-		if ( parameters.displacementMap )
+		if ( parameters.iridescence )
 			_programLayers.enable( 18 );
-		if ( parameters.specularMap )
+		if ( parameters.iridescenceMap )
 			_programLayers.enable( 19 );
-		if ( parameters.roughnessMap )
+		if ( parameters.iridescenceThicknessMap )
 			_programLayers.enable( 20 );
-		if ( parameters.metalnessMap )
+		if ( parameters.displacementMap )
 			_programLayers.enable( 21 );
-		if ( parameters.gradientMap )
+		if ( parameters.specularMap )
 			_programLayers.enable( 22 );
-		if ( parameters.alphaMap )
+		if ( parameters.roughnessMap )
 			_programLayers.enable( 23 );
-		if ( parameters.alphaTest )
+		if ( parameters.metalnessMap )
 			_programLayers.enable( 24 );
-		if ( parameters.vertexColors )
+		if ( parameters.gradientMap )
 			_programLayers.enable( 25 );
-		if ( parameters.vertexAlphas )
+		if ( parameters.alphaMap )
 			_programLayers.enable( 26 );
-		if ( parameters.vertexUvs )
+		if ( parameters.alphaTest )
 			_programLayers.enable( 27 );
-		if ( parameters.vertexTangents )
+		if ( parameters.vertexColors )
 			_programLayers.enable( 28 );
-		if ( parameters.uvsVertexOnly )
+		if ( parameters.vertexAlphas )
 			_programLayers.enable( 29 );
-		if ( parameters.fog )
+		if ( parameters.vertexUvs )
 			_programLayers.enable( 30 );
+		if ( parameters.vertexTangents )
+			_programLayers.enable( 31 );
+		if ( parameters.uvsVertexOnly )
+			_programLayers.enable( 32 );
+		if ( parameters.fog )
+			_programLayers.enable( 33 );
 
 		array.push( _programLayers.mask );
 		_programLayers.disableAll();