浏览代码

WebGPURenderer & PhysicalLightingModel: Added `clearcoat` (#26211)

* Added materialClearcoatNormal

* Added materialClearcoat, materialClearcoatRoughness

* Added transformedClearcoatNormalView and revision

* Added clearcoat, clearcoatRoughness

* NodeBuilder: Added .includes( node )

* PhysicalLightingModel: Added Clearcoat

* Added `webgpu_clearcoat` example

* cleanup

* Background: Improve precision

* example adjustments

* fix type

* WebGLNodeBuilder: Fix transformedNormalView property

* WebGLNodeBuilder: Fix transformedNormalView property (2)

* cleanup
sunag 2 年之前
父节点
当前提交
de8f1460d9

+ 1 - 0
examples/files.json

@@ -308,6 +308,7 @@
 		"webgpu_audio_processing",
 		"webgpu_backdrop",
 		"webgpu_backdrop_area",
+		"webgpu_clearcoat",
 		"webgpu_compute",
 		"webgpu_cubemap_adjustments",
 		"webgpu_cubemap_dynamic",

+ 1 - 1
examples/jsm/nodes/Nodes.js

@@ -74,7 +74,7 @@ export { default as MaterialReferenceNode, materialReference } from './accessors
 export { default as TextureBicubicNode, textureBicubic } from './accessors/TextureBicubicNode.js';
 export { default as ModelNode, modelDirection, modelViewMatrix, modelNormalMatrix, modelWorldMatrix, modelPosition, modelViewPosition, modelScale } from './accessors/ModelNode.js';
 export { default as ModelViewProjectionNode, modelViewProjection } from './accessors/ModelViewProjectionNode.js';
-export { default as NormalNode, normalGeometry, normalLocal, normalView, normalWorld, transformedNormalView, transformedNormalWorld } from './accessors/NormalNode.js';
+export { default as NormalNode, normalGeometry, normalLocal, normalView, normalWorld, transformedNormalView, transformedNormalWorld, transformedClearcoatNormalView } from './accessors/NormalNode.js';
 export { default as Object3DNode, objectDirection, objectViewMatrix, objectNormalMatrix, objectWorldMatrix, objectPosition, objectScale, objectViewPosition } from './accessors/Object3DNode.js';
 export { default as PointUVNode, pointUV } from './accessors/PointUVNode.js';
 export { default as PositionNode, positionGeometry, positionLocal, positionWorld, positionWorldDirection, positionView, positionViewDirection } from './accessors/PositionNode.js';

+ 7 - 1
examples/jsm/nodes/accessors/ExtendedMaterialNode.js

@@ -20,7 +20,7 @@ class ExtendedMaterialNode extends MaterialNode {
 		const scope = this.scope;
 		let type = null;
 
-		if ( scope === ExtendedMaterialNode.NORMAL ) {
+		if ( scope === ExtendedMaterialNode.NORMAL || scope === ExtendedMaterialNode.CLEARCOAT_NORMAL ) {
 
 			type = 'vec3';
 
@@ -41,6 +41,10 @@ class ExtendedMaterialNode extends MaterialNode {
 
 			node = material.normalMap ? normalMap( this.getTexture( 'normalMap' ), materialReference( 'normalScale', 'vec2' ) ) : normalView;
 
+		} else if ( scope === ExtendedMaterialNode.CLEARCOAT_NORMAL ) {
+
+			node = material.clearcoatNormalMap ? normalMap( this.getTexture( 'clearcoatNormalMap' ), materialReference( 'clearcoatNormalScale', 'vec2' ) ) : normalView;
+
 		}
 
 		return node || super.construct( builder );
@@ -50,9 +54,11 @@ class ExtendedMaterialNode extends MaterialNode {
 }
 
 ExtendedMaterialNode.NORMAL = 'normal';
+ExtendedMaterialNode.CLEARCOAT_NORMAL = 'clearcoatNormal';
 
 export default ExtendedMaterialNode;
 
 export const materialNormal = nodeImmutable( ExtendedMaterialNode, ExtendedMaterialNode.NORMAL );
+export const materialClearcoatNormal = nodeImmutable( ExtendedMaterialNode, ExtendedMaterialNode.CLEARCOAT_NORMAL );
 
 addNodeClass( ExtendedMaterialNode );

+ 32 - 0
examples/jsm/nodes/accessors/MaterialNode.js

@@ -173,6 +173,34 @@ class MaterialNode extends Node {
 
 			}
 
+		} else if ( scope === MaterialNode.CLEARCOAT ) {
+
+			const clearcoatNode = this.getFloat( 'clearcoat' );
+
+			if ( material.clearcoatMap && material.clearcoatMap.isTexture === true ) {
+
+				node = clearcoatNode.mul( this.getTexture( 'clearcoatMap' ).r );
+
+			} else {
+
+				node = clearcoatNode;
+
+			}
+
+		} else if ( scope === MaterialNode.CLEARCOAT_ROUGHNESS ) {
+
+			const clearcoatRoughnessNode = this.getFloat( 'clearcoatRoughness' );
+
+			if ( material.clearcoatRoughnessMap && material.clearcoatRoughnessMap.isTexture === true ) {
+
+				node = clearcoatRoughnessNode.mul( this.getTexture( 'clearcoatRoughnessMap' ).r );
+
+			} else {
+
+				node = clearcoatRoughnessNode;
+
+			}
+
 		} else if ( scope === MaterialNode.ROTATION ) {
 
 			node = this.getFloat( 'rotation' );
@@ -248,6 +276,8 @@ MaterialNode.SPECULAR_COLOR = 'specularColor';
 MaterialNode.REFLECTIVITY = 'reflectivity';
 MaterialNode.ROUGHNESS = 'roughness';
 MaterialNode.METALNESS = 'metalness';
+MaterialNode.CLEARCOAT = 'clearcoat';
+MaterialNode.CLEARCOAT_ROUGHNESS = 'clearcoatRoughness';
 MaterialNode.EMISSIVE = 'emissive';
 MaterialNode.ROTATION = 'rotation';
 MaterialNode.UV = 'uv';
@@ -264,6 +294,8 @@ export const materialSpecularColor = nodeImmutable( MaterialNode, MaterialNode.S
 export const materialReflectivity = nodeImmutable( MaterialNode, MaterialNode.REFLECTIVITY );
 export const materialRoughness = nodeImmutable( MaterialNode, MaterialNode.ROUGHNESS );
 export const materialMetalness = nodeImmutable( MaterialNode, MaterialNode.METALNESS );
+export const materialClearcoat = nodeImmutable( MaterialNode, MaterialNode.CLEARCOAT );
+export const materialClearcoatRoughness = nodeImmutable( MaterialNode, MaterialNode.CLEARCOAT_ROUGHNESS );
 export const materialRotation = nodeImmutable( MaterialNode, MaterialNode.ROTATION );
 
 addNodeClass( MaterialNode );

+ 3 - 2
examples/jsm/nodes/accessors/NormalNode.js

@@ -1,7 +1,7 @@
 import Node, { addNodeClass } from '../core/Node.js';
 import { attribute } from '../core/AttributeNode.js';
-import { label } from '../core/VarNode.js';
 import { varying } from '../core/VaryingNode.js';
+import { property } from '../core/PropertyNode.js';
 import { normalize } from '../math/MathNode.js';
 import { cameraViewMatrix } from './CameraNode.js';
 import { modelNormalMatrix } from './ModelNode.js';
@@ -89,7 +89,8 @@ export const normalGeometry = nodeImmutable( NormalNode, NormalNode.GEOMETRY );
 export const normalLocal = nodeImmutable( NormalNode, NormalNode.LOCAL );
 export const normalView = nodeImmutable( NormalNode, NormalNode.VIEW );
 export const normalWorld = nodeImmutable( NormalNode, NormalNode.WORLD );
-export const transformedNormalView = label( normalView, 'TransformedNormalView' );
+export const transformedNormalView = property( 'vec3', 'TransformedNormalView' );
 export const transformedNormalWorld = transformedNormalView.transformDirection( cameraViewMatrix ).normalize();
+export const transformedClearcoatNormalView = property( 'vec3', 'TransformedClearcoatNormalView' );
 
 addNodeClass( NormalNode );

+ 2 - 1
examples/jsm/nodes/core/LightingModel.js

@@ -1,7 +1,8 @@
 class LightingModel {
 
-	constructor( direct = null, indirectDiffuse = null, indirectSpecular = null, ambientOcclusion = null ) {
+	constructor( init = null, direct = null, indirectDiffuse = null, indirectSpecular = null, ambientOcclusion = null ) {
 
+		this.init = init;
 		this.direct = direct;
 		this.indirectDiffuse = indirectDiffuse;
 		this.indirectSpecular = indirectSpecular;

+ 6 - 0
examples/jsm/nodes/core/NodeBuilder.js

@@ -97,6 +97,12 @@ class NodeBuilder {
 
 	}
 
+	includes( node ) {
+
+		return this.nodes.includes( node );
+
+	}
+
 	getBindings() {
 
 		let bindingsArray = this.bindingsArray;

+ 2 - 0
examples/jsm/nodes/core/PropertyNode.js

@@ -47,6 +47,8 @@ export const property = ( type, name ) => nodeObject( new PropertyNode( type, na
 export const diffuseColor = nodeImmutable( PropertyNode, 'vec4', 'DiffuseColor' );
 export const roughness = nodeImmutable( PropertyNode, 'float', 'Roughness' );
 export const metalness = nodeImmutable( PropertyNode, 'float', 'Metalness' );
+export const clearcoat = nodeImmutable( PropertyNode, 'float', 'Clearcoat' );
+export const clearcoatRoughness = nodeImmutable( PropertyNode, 'float', 'ClearcoatRoughness' );
 export const specularColor = nodeImmutable( PropertyNode, 'color', 'SpecularColor' );
 export const shininess = nodeImmutable( PropertyNode, 'float', 'Shininess' );
 

+ 5 - 3
examples/jsm/nodes/functions/BSDF/BRDF_GGX.js

@@ -10,13 +10,15 @@ const BRDF_GGX = new ShaderNode( ( inputs ) => {
 
 	const { lightDirection, f0, f90, roughness } = inputs;
 
+	const normalView = inputs.normalView || transformedNormalView;
+
 	const alpha = roughness.pow2(); // UE4's roughness
 
 	const halfDir = lightDirection.add( positionViewDirection ).normalize();
 
-	const dotNL = transformedNormalView.dot( lightDirection ).clamp();
-	const dotNV = transformedNormalView.dot( positionViewDirection ).clamp();
-	const dotNH = transformedNormalView.dot( halfDir ).clamp();
+	const dotNL = normalView.dot( lightDirection ).clamp();
+	const dotNV = normalView.dot( positionViewDirection ).clamp(); // @ TODO: Move to core dotNV
+	const dotNH = normalView.dot( halfDir ).clamp();
 	const dotVH = positionViewDirection.dot( halfDir ).clamp();
 
 	const F = F_Schlick.call( { f0, f90, dotVH } );

+ 2 - 2
examples/jsm/nodes/functions/BSDF/DFGApprox.js

@@ -10,14 +10,14 @@ const DFGApprox = new ShaderNode( ( inputs ) => {
 
 	const { roughness } = inputs;
 
+	const dotNV = inputs.dotNV || transformedNormalView.dot( positionViewDirection ).clamp(); // @ TODO: Move to core dotNV
+
 	const c0 = vec4( - 1, - 0.0275, - 0.572, 0.022 );
 
 	const c1 = vec4( 1, 0.0425, 1.04, - 0.04 );
 
 	const r = roughness.mul( c0 ).add( c1 );
 
-	const dotNV = transformedNormalView.dot( positionViewDirection ).clamp();
-
 	const a004 = r.x.mul( r.x ).min( dotNV.mul( - 9.28 ).exp2() ).mul( r.x ).add( r.y );
 
 	const fab = vec2( - 1.04, 1.04 ).mul( a004 ).add( r.zw );

+ 13 - 0
examples/jsm/nodes/functions/BSDF/EnvironmentBRDF.js

@@ -0,0 +1,13 @@
+import DFGApprox from './DFGApprox.js';
+import { ShaderNode } from '../../shadernode/ShaderNode.js';
+
+const EnvironmentBRDF = new ShaderNode( ( inputs ) => {
+
+	const { dotNV, specularColor, specularF90, roughness } = inputs;
+
+	const fab = DFGApprox.call( { dotNV, roughness } );
+	return specularColor.mul( fab.x ).add( specularF90.mul( fab.y ) );
+
+} );
+
+export default EnvironmentBRDF;

+ 1 - 1
examples/jsm/nodes/functions/PhongLightingModel.js

@@ -23,6 +23,6 @@ const RE_IndirectDiffuse_BlinnPhong = new ShaderNode( ( { irradiance, reflectedL
 
 } );
 
-const phongLightingModel = lightingModel( RE_Direct_BlinnPhong, RE_IndirectDiffuse_BlinnPhong );
+const phongLightingModel = lightingModel( null, RE_Direct_BlinnPhong, RE_IndirectDiffuse_BlinnPhong );
 
 export default phongLightingModel;

+ 66 - 12
examples/jsm/nodes/functions/PhysicalLightingModel.js

@@ -1,13 +1,17 @@
 import BRDF_Lambert from './BSDF/BRDF_Lambert.js';
 import BRDF_GGX from './BSDF/BRDF_GGX.js';
 import DFGApprox from './BSDF/DFGApprox.js';
+import EnvironmentBRDF from './BSDF/EnvironmentBRDF.js';
+import F_Schlick from './BSDF/F_Schlick.js';
 import { lightingModel } from '../core/LightingModel.js';
-import { temp } from '../core/VarNode.js';
-import { diffuseColor, specularColor, roughness } from '../core/PropertyNode.js';
-import { transformedNormalView } from '../accessors/NormalNode.js';
+import { diffuseColor, specularColor, roughness, clearcoat, clearcoatRoughness } from '../core/PropertyNode.js';
+import { transformedNormalView, transformedClearcoatNormalView } from '../accessors/NormalNode.js';
 import { positionViewDirection } from '../accessors/PositionNode.js';
 import { ShaderNode, float, vec3 } from '../shadernode/ShaderNode.js';
 
+const clearcoatF0 = vec3( 0.04 );
+const clearcoatF90 = vec3( 1 );
+
 // 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/
@@ -28,14 +32,53 @@ const computeMultiscattering = ( singleScatter, multiScatter, specularF90 = floa
 
 };
 
-const RE_IndirectSpecular_Physical = new ShaderNode( ( inputs ) => {
+const LM_Init = new ShaderNode( ( context, stack, builder ) => {
+
+	if ( builder.includes( clearcoat ) ) {
+
+		context.clearcoatRadiance = vec3().temp();
+		context.reflectedLight.clearcoatSpecular = vec3().temp();
+
+		const dotNVcc = transformedClearcoatNormalView.dot( positionViewDirection ).clamp();
+
+		const Fcc = F_Schlick.call( {
+			dotVH: dotNVcc,
+			f0: clearcoatF0,
+			f90: clearcoatF90
+		} );
+
+		const outgoingLight = context.reflectedLight.total;
+		const clearcoatLight = outgoingLight.mul( clearcoat.mul( Fcc ).oneMinus() ).add( context.reflectedLight.clearcoatSpecular.mul( clearcoat ) );
+
+		outgoingLight.assign( clearcoatLight );
+
+	}
+
+} );
+
+const RE_IndirectSpecular_Physical = new ShaderNode( ( context ) => {
 
-	const { radiance, iblIrradiance, reflectedLight } = inputs;
+	const { radiance, iblIrradiance, reflectedLight } = context;
+
+	if ( reflectedLight.clearcoatSpecular ) {
+
+		const dotNVcc = transformedClearcoatNormalView.dot( positionViewDirection ).clamp();
+
+		const clearcoatEnv = EnvironmentBRDF.call( {
+			dotNV: dotNVcc,
+			specularColor: clearcoatF0,
+			specularF90: clearcoatF90,
+			roughness: clearcoatRoughness
+		} );
+
+		reflectedLight.clearcoatSpecular.addAssign( context.clearcoatRadiance.mul( clearcoatEnv ) );
+
+	}
 
 	// Both indirect specular and indirect diffuse light accumulate here
 
-	const singleScattering = temp( vec3() );
-	const multiScattering = temp( vec3() );
+	const singleScattering = vec3().temp();
+	const multiScattering = vec3().temp();
 	const cosineWeightedIrradiance = iblIrradiance.mul( 1 / Math.PI );
 
 	computeMultiscattering( singleScattering, multiScattering );
@@ -51,9 +94,9 @@ const RE_IndirectSpecular_Physical = new ShaderNode( ( inputs ) => {
 
 } );
 
-const RE_IndirectDiffuse_Physical = new ShaderNode( ( inputs ) => {
+const RE_IndirectDiffuse_Physical = new ShaderNode( ( context ) => {
 
-	const { irradiance, reflectedLight } = inputs;
+	const { irradiance, reflectedLight } = context;
 
 	reflectedLight.indirectDiffuse.addAssign( irradiance.mul( BRDF_Lambert.call( { diffuseColor } ) ) );
 
@@ -66,15 +109,26 @@ const RE_Direct_Physical = new ShaderNode( ( inputs ) => {
 	const dotNL = transformedNormalView.dot( lightDirection ).clamp();
 	const irradiance = dotNL.mul( lightColor );
 
+	if ( reflectedLight.clearcoatSpecular ) {
+
+		const dotNLcc = transformedClearcoatNormalView.dot( lightDirection ).clamp();
+		const ccIrradiance = dotNLcc.mul( lightColor );
+
+		reflectedLight.clearcoatSpecular.addAssign( ccIrradiance.mul( BRDF_GGX.call( { lightDirection, f0: clearcoatF0, f90: clearcoatF90, roughness: clearcoatRoughness, normalView: transformedClearcoatNormalView } ) ) );
+
+	}
+
 	reflectedLight.directDiffuse.addAssign( irradiance.mul( BRDF_Lambert.call( { diffuseColor: diffuseColor.rgb } ) ) );
 
 	reflectedLight.directSpecular.addAssign( irradiance.mul( BRDF_GGX.call( { lightDirection, f0: specularColor, f90: 1, roughness } ) ) );
 
 } );
 
-const RE_AmbientOcclusion_Physical = new ShaderNode( ( { ambientOcclusion, reflectedLight } ) => {
+const RE_AmbientOcclusion_Physical = new ShaderNode( ( context ) => {
+
+	const { ambientOcclusion, reflectedLight } = context;
 
-	const dotNV = transformedNormalView.dot( positionViewDirection ).clamp();
+	const dotNV = transformedNormalView.dot( positionViewDirection ).clamp(); // @ TODO: Move to core dotNV
 
 	const aoNV = dotNV.add( ambientOcclusion );
 	const aoExp = roughness.mul( - 16.0 ).oneMinus().negate().exp2();
@@ -88,6 +142,6 @@ const RE_AmbientOcclusion_Physical = new ShaderNode( ( { ambientOcclusion, refle
 
 } );
 
-const physicalLightingModel = lightingModel( RE_Direct_Physical, RE_IndirectDiffuse_Physical, RE_IndirectSpecular_Physical, RE_AmbientOcclusion_Physical );
+const physicalLightingModel = lightingModel( LM_Init, RE_Direct_Physical, RE_IndirectDiffuse_Physical, RE_IndirectSpecular_Physical, RE_AmbientOcclusion_Physical );
 
 export default physicalLightingModel;

+ 1 - 1
examples/jsm/nodes/lighting/DirectionalLightNode.js

@@ -28,7 +28,7 @@ class DirectionalLightNode extends AnalyticLightNode {
 				lightDirection,
 				lightColor,
 				reflectedLight
-			}, builder );
+			} );
 
 		}
 

+ 91 - 62
examples/jsm/nodes/lighting/EnvironmentNode.js

@@ -1,11 +1,11 @@
 import LightingNode from './LightingNode.js';
 import { cache } from '../core/CacheNode.js';
 import { context } from '../core/ContextNode.js';
-import { roughness } from '../core/PropertyNode.js';
+import { roughness, clearcoatRoughness } from '../core/PropertyNode.js';
 import { equirectUV } from '../utils/EquirectUVNode.js';
 import { specularMIPLevel } from '../utils/SpecularMIPLevelNode.js';
 import { cameraViewMatrix } from '../accessors/CameraNode.js';
-import { transformedNormalView, transformedNormalWorld } from '../accessors/NormalNode.js';
+import { transformedClearcoatNormalView, transformedNormalView, transformedNormalWorld } from '../accessors/NormalNode.js';
 import { positionViewDirection } from '../accessors/PositionNode.js';
 import { addNodeClass } from '../core/Node.js';
 import { float, vec2 } from '../shadernode/ShaderNode.js';
@@ -39,113 +39,142 @@ class EnvironmentNode extends LightingNode {
 
 		}
 
-		let reflectVec;
-		let radianceTextureUVNode;
-		let irradianceTextureUVNode;
+		//
 
-		const radianceContext = context( envNode, {
-			getUVNode: ( textureNode ) => {
+		const intensity = reference( 'envMapIntensity', 'float', builder.material ); // @TODO: Add materialEnvIntensity in MaterialNode
 
-				let node = null;
+		const radiance = context( envNode, createRadianceContext( roughness, transformedNormalView ) ).mul( intensity );
+		const irradiance = context( envNode, createIrradianceContext( transformedNormalWorld ) ).mul( Math.PI ).mul( intensity );
 
-				if ( reflectVec === undefined ) {
+		const isolateRadiance = cache( radiance );
 
-					reflectVec = positionViewDirection.negate().reflect( transformedNormalView );
-					reflectVec = roughness.mul( roughness ).mix( reflectVec, transformedNormalView ).normalize();
-					reflectVec = reflectVec.transformDirection( cameraViewMatrix );
+		//
 
-				}
+		builder.context.radiance.addAssign( isolateRadiance );
 
-				if ( textureNode.isCubeTextureNode ) {
+		builder.context.iblIrradiance.addAssign( irradiance );
 
-					node = reflectVec;
+		//
 
-				} else if ( textureNode.isTextureNode ) {
+		let isolateClearcoatRadiance = null;
 
-					if ( radianceTextureUVNode === undefined ) {
+		if ( builder.context.clearcoatRadiance  ) {
 
-						// @TODO: Needed PMREM
+			const clearcoatRadiance = context( envNode, createRadianceContext( clearcoatRoughness, transformedClearcoatNormalView ) ).mul( intensity );
 
-						radianceTextureUVNode = equirectUV( reflectVec );
+			isolateClearcoatRadiance = cache( clearcoatRadiance );
 
-					}
+			builder.context.clearcoatRadiance.addAssign( isolateClearcoatRadiance );
 
-					node = radianceTextureUVNode;
+		}
 
-				}
+		//
 
-				return node;
+		properties.radiance = isolateRadiance;
+		properties.clearcoatRadiance = isolateClearcoatRadiance;
+		properties.irradiance = irradiance;
 
-			},
-			getSamplerLevelNode: () => {
+	}
 
-				return roughness;
+}
 
-			},
-			getMIPLevelAlgorithmNode: ( textureNode, levelNode ) => {
+const createRadianceContext = ( roughnessNode, normalViewNode ) => {
 
-				return specularMIPLevel( textureNode, levelNode );
+	let reflectVec = null;
+	let textureUVNode = null;
 
-			}
-		} );
+	return {
+		getUVNode: ( textureNode ) => {
 
-		const irradianceContext = context( envNode, {
-			getUVNode: ( textureNode ) => {
+			let node = null;
 
-				let node = null;
+			if ( reflectVec === null ) {
 
-				if ( textureNode.isCubeTextureNode ) {
+				reflectVec = positionViewDirection.negate().reflect( normalViewNode );
+				reflectVec = roughnessNode.mul( roughnessNode ).mix( reflectVec, normalViewNode ).normalize();
+				reflectVec = reflectVec.transformDirection( cameraViewMatrix );
 
-					node = transformedNormalWorld;
+			}
 
-				} else if ( textureNode.isTextureNode ) {
+			if ( textureNode.isCubeTextureNode ) {
 
-					if ( irradianceTextureUVNode === undefined ) {
+				node = reflectVec;
 
-						// @TODO: Needed PMREM
+			} else if ( textureNode.isTextureNode ) {
 
-						irradianceTextureUVNode = equirectUV( transformedNormalWorld );
-						irradianceTextureUVNode = vec2( irradianceTextureUVNode.x, irradianceTextureUVNode.y.oneMinus() );
+				if ( textureUVNode === null ) {
 
-					}
+					// @TODO: Needed PMREM
 
-					node = irradianceTextureUVNode;
+					textureUVNode = equirectUV( reflectVec );
 
 				}
 
-				return node;
+				node = textureUVNode;
 
-			},
-			getSamplerLevelNode: () => {
+			}
 
-				return float( 1 );
+			return node;
 
-			},
-			getMIPLevelAlgorithmNode: ( textureNode, levelNode ) => {
+		},
+		getSamplerLevelNode: () => {
 
-				return specularMIPLevel( textureNode, levelNode );
+			return roughnessNode;
 
-			}
-		} );
+		},
+		getMIPLevelAlgorithmNode: ( textureNode, levelNode ) => {
 
-		//
+			return specularMIPLevel( textureNode, levelNode );
 
-		const isolateRadianceFlowContext = cache( radianceContext );
+		}
+	};
 
-		//
+};
 
-		const intensity = reference( 'envMapIntensity', 'float', builder.material );
+const createIrradianceContext = ( normalWorldNode ) => {
 
-		builder.context.radiance.addAssign( isolateRadianceFlowContext.mul( intensity ) );
+	let textureUVNode = null;
 
-		builder.context.iblIrradiance.addAssign( irradianceContext.mul( Math.PI ).mul( intensity ) );
+	return {
+		getUVNode: ( textureNode ) => {
 
-		properties.radianceContext = isolateRadianceFlowContext;
-		properties.irradianceContext = irradianceContext;
+			let node = null;
 
-	}
+			if ( textureNode.isCubeTextureNode ) {
 
-}
+				node = normalWorldNode;
+
+			} else if ( textureNode.isTextureNode ) {
+
+				if ( textureUVNode === null ) {
+
+					// @TODO: Needed PMREM
+
+					textureUVNode = equirectUV( normalWorldNode );
+					textureUVNode = vec2( textureUVNode.x, textureUVNode.y.oneMinus() );
+
+				}
+
+				node = textureUVNode;
+
+			}
+
+			return node;
+
+		},
+		getSamplerLevelNode: () => {
+
+			return float( 1 );
+
+		},
+		getMIPLevelAlgorithmNode: ( textureNode, levelNode ) => {
+
+			return specularMIPLevel( textureNode, levelNode );
+
+		}
+	};
+
+};
 
 export default EnvironmentNode;
 

+ 17 - 15
examples/jsm/nodes/lighting/LightingContextNode.js

@@ -1,5 +1,4 @@
 import ContextNode from '../core/ContextNode.js';
-import { temp } from '../core/VarNode.js';
 import { add } from '../math/OperatorNode.js';
 import { mix } from '../math/MathNode.js';
 import { addNodeClass } from '../core/Node.js';
@@ -30,10 +29,10 @@ class LightingContextNode extends ContextNode {
 		const context = this.context = {}; // reset context
 		const properties = builder.getNodeProperties( this );
 
-		const directDiffuse = temp( vec3() ),
-			directSpecular = temp( vec3() ),
-			indirectDiffuse = temp( vec3() ),
-			indirectSpecular = temp( vec3() );
+		const directDiffuse = vec3().temp(),
+			directSpecular = vec3().temp(),
+			indirectDiffuse = vec3().temp(),
+			indirectSpecular = vec3().temp();
 
 		let totalDiffuse = add( directDiffuse, indirectDiffuse );
 
@@ -44,7 +43,7 @@ class LightingContextNode extends ContextNode {
 		}
 
 		const totalSpecular = add( directSpecular, indirectSpecular );
-		const total = add( totalDiffuse, totalSpecular );
+		const total = add( totalDiffuse, totalSpecular ).temp();
 
 		const reflectedLight = {
 			directDiffuse,
@@ -55,21 +54,24 @@ class LightingContextNode extends ContextNode {
 		};
 
 		const lighting = {
-			radiance: temp( vec3() ),
-			irradiance: temp( vec3() ),
-			iblIrradiance: temp( vec3() ),
-			ambientOcclusion: temp( float( 1 ) )
+			radiance: vec3().temp(),
+			irradiance: vec3().temp(),
+			iblIrradiance: vec3().temp(),
+			ambientOcclusion: float( 1 ).temp()
 		};
 
+		context.reflectedLight = reflectedLight;
+		context.lightingModelNode = lightingModelNode || context.lightingModelNode;
+
 		Object.assign( properties, reflectedLight, lighting );
 		Object.assign( context, lighting );
 
-		context.reflectedLight = reflectedLight;
-		context.lightingModelNode = lightingModelNode || context.lightingModelNode;
+		// @TODO: Call needed return a new node ( or rename the ShaderNodeInternal.call() function ), it's not moment to run
+		if ( lightingModelNode && lightingModelNode.init ) lightingModelNode.init.call( context, builder.stack, builder );
 
-		if ( lightingModelNode && lightingModelNode.indirectDiffuse ) lightingModelNode.indirectDiffuse.call( context );
-		if ( lightingModelNode && lightingModelNode.indirectSpecular ) lightingModelNode.indirectSpecular.call( context );
-		if ( lightingModelNode && lightingModelNode.ambientOcclusion ) lightingModelNode.ambientOcclusion.call( context );
+		if ( lightingModelNode && lightingModelNode.indirectDiffuse ) lightingModelNode.indirectDiffuse.call( context, builder.stack, builder );
+		if ( lightingModelNode && lightingModelNode.indirectSpecular ) lightingModelNode.indirectSpecular.call( context, builder.stack, builder );
+		if ( lightingModelNode && lightingModelNode.ambientOcclusion ) lightingModelNode.ambientOcclusion.call( context, builder.stack, builder );
 
 		return super.construct( builder );
 

+ 1 - 1
examples/jsm/nodes/lighting/PointLightNode.js

@@ -56,7 +56,7 @@ class PointLightNode extends AnalyticLightNode {
 				lightDirection,
 				lightColor,
 				reflectedLight
-			}, builder );
+			} );
 
 		}
 

+ 1 - 1
examples/jsm/nodes/lighting/SpotLightNode.js

@@ -77,7 +77,7 @@ class SpotLightNode extends AnalyticLightNode {
 				lightDirection,
 				lightColor,
 				reflectedLight
-			}, builder );
+			} );
 
 		}
 

+ 33 - 0
examples/jsm/nodes/materials/MeshPhysicalNodeMaterial.js

@@ -1,4 +1,9 @@
 import { addNodeMaterial } from './NodeMaterial.js';
+import { transformedClearcoatNormalView } from '../accessors/NormalNode.js';
+import { clearcoat, clearcoatRoughness } from '../core/PropertyNode.js';
+import { materialClearcoatNormal } from '../accessors/ExtendedMaterialNode.js';
+import { materialClearcoat, materialClearcoatRoughness } from '../accessors/MaterialNode.js';
+import { float, vec3 } from '../shadernode/ShaderNode.js';
 import MeshStandardNodeMaterial from './MeshStandardNodeMaterial.js';
 
 import { MeshPhysicalMaterial } from 'three';
@@ -38,6 +43,34 @@ class MeshPhysicalNodeMaterial extends MeshStandardNodeMaterial {
 
 	}
 
+	constructVariants( builder ) {
+
+		super.constructVariants( builder );
+
+		const { stack } = builder;
+
+		// CLEARCOAT
+
+		const clearcoatNode = this.clearcoatNode ? float( this.clearcoatNode ) : materialClearcoat;
+		const clearcoatRoughnessNode = this.clearcoatRoughnessNode ? float( this.clearcoatRoughnessNode ) : materialClearcoatRoughness;
+
+		stack.assign( clearcoat, clearcoatNode );
+		stack.assign( clearcoatRoughness, clearcoatRoughnessNode );
+
+	}
+
+	constructNormal( builder ) {
+
+		super.constructNormal( builder );
+
+		// CLEARCOAT NORMAL
+
+		const clearcoatNormalNode = this.clearcoatNormalNode ? vec3( this.clearcoatNormalNode ) : materialClearcoatNormal;
+
+		builder.stack.assign( transformedClearcoatNormalView, clearcoatNormalNode );
+
+	}
+
 	copy( source ) {
 
 		this.clearcoatNode = source.clearcoatNode;

+ 2 - 1
examples/jsm/nodes/materials/NodeMaterial.js

@@ -236,12 +236,13 @@ class NodeMaterial extends ShaderMaterial {
 		const lights = this.lights === true || this.lightsNode !== null;
 
 		const lightsNode = lights ? this.constructLights( builder ) : null;
-		const lightingModelNode = lightsNode ? this.constructLightingModel( builder ) : null;
 
 		let outgoingLightNode = diffuseColor.rgb;
 
 		if ( lightsNode && lightsNode.hasLight !== false ) {
 
+			const lightingModelNode = this.constructLightingModel( builder );
+
 			outgoingLightNode = lightsNode.lightingContext( lightingModelNode, backdropNode, backdropAlphaNode );
 
 		} else if ( backdropNode !== null ) {

+ 2 - 2
examples/jsm/renderers/common/Background.js

@@ -1,5 +1,5 @@
 import DataMap from './DataMap.js';
-import { Color, Mesh, BoxGeometry, BackSide } from 'three';
+import { Color, Mesh, SphereGeometry, BackSide } from 'three';
 import { context, positionWorldDirection, MeshBasicNodeMaterial } from '../../nodes/Nodes.js';
 
 let _clearAlpha;
@@ -65,7 +65,7 @@ class Background extends DataMap {
 				nodeMaterial.depthWrite = false;
 				nodeMaterial.fog = false;
 
-				this.boxMesh = boxMesh = new Mesh( new BoxGeometry( 1, 1, 1 ), nodeMaterial );
+				this.boxMesh = boxMesh = new Mesh( new SphereGeometry( 1, 32, 32 ), nodeMaterial );
 				boxMesh.frustumCulled = false;
 
 				boxMesh.onBeforeRender = function ( renderer, scene, camera ) {

+ 9 - 12
examples/jsm/renderers/webgl/nodes/WebGLNodeBuilder.js

@@ -1,4 +1,4 @@
-import { defaultShaderStages, NodeFrame, MathNode, GLSLNodeParser, NodeBuilder } from 'three/nodes';
+import { defaultShaderStages, NodeFrame, MathNode, GLSLNodeParser, NodeBuilder, normalView } from 'three/nodes';
 import SlotNode from './SlotNode.js';
 import { PerspectiveCamera, ShaderChunk, ShaderLib, UniformsUtils, UniformsLib } from 'three';
 
@@ -97,6 +97,14 @@ class WebGLNodeBuilder extends NodeBuilder {
 
 		const { material, renderer } = this;
 
+		this.addSlot( 'fragment', new SlotNode( {
+			node: normalView,
+			nodeType: 'vec3',
+			source: getIncludeSnippet( 'clipping_planes_fragment' ),
+			target: 'vec3 TransformedNormalView = %RESULT%;',
+			inclusionType: 'append'
+		} ) );
+
 		if ( renderer.toneMappingNode && renderer.toneMappingNode.isNode === true ) {
 
 			this.addSlot( 'fragment', new SlotNode( {
@@ -348,17 +356,6 @@ class WebGLNodeBuilder extends NodeBuilder {
 
 					}
 
-					if ( material.thicknessNode && material.thicknessNode.isNode ) {
-
-						this.addSlot( 'fragment', new SlotNode( {
-							node: material.thicknessNode,
-							nodeType: 'float',
-							source: 'material.thickness = thickness;',
-							target: 'material.thickness = %RESULT%;'
-						} ) );
-
-					}
-
 					if ( material.attenuationDistanceNode && material.attenuationDistanceNode.isNode ) {
 
 						this.addSlot( 'fragment', new SlotNode( {

二进制
examples/screenshots/webgpu_clearcoat.jpg


+ 263 - 0
examples/webgpu_clearcoat.html

@@ -0,0 +1,263 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - materials - clearcoat</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="main.css">
+	</head>
+	<body>
+		<div id="info">
+			<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> webgpu - clearcoat
+		</div>
+
+		<!-- Import maps polyfill -->
+		<!-- Remove this when import maps will be widely supported -->
+		<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+
+			import WebGPU from 'three/addons/capabilities/WebGPU.js';
+			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+
+			import Stats from 'three/addons/libs/stats.module.js';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { HDRCubeTextureLoader } from 'three/addons/loaders/HDRCubeTextureLoader.js';
+
+			import { FlakesTexture } from 'three/addons/textures/FlakesTexture.js';
+
+			let container, stats;
+
+			let camera, scene, renderer;
+
+			let particleLight;
+			let group;
+
+			init();
+			animate();
+
+			function init() {
+
+				if ( WebGPU.isAvailable() === false ) {
+
+					document.body.appendChild( WebGPU.getErrorMessage() );
+
+					throw new Error( 'No WebGPU support' );
+
+				}
+
+				container = document.createElement( 'div' );
+				document.body.appendChild( container );
+
+				camera = new THREE.PerspectiveCamera( 27, window.innerWidth / window.innerHeight, 0.25, 50 );
+				camera.position.z = 10;
+
+				scene = new THREE.Scene();
+
+				group = new THREE.Group();
+				scene.add( group );
+
+				new HDRCubeTextureLoader()
+					.setPath( 'textures/cube/pisaHDR/' )
+					.load( [ 'px.hdr', 'nx.hdr', 'py.hdr', 'ny.hdr', 'pz.hdr', 'nz.hdr' ],
+						function ( texture ) {
+
+							const geometry = new THREE.SphereGeometry( .8, 64, 32 );
+
+							const textureLoader = new THREE.TextureLoader();
+
+							const diffuse = textureLoader.load( 'textures/carbon/Carbon.png' );
+							diffuse.colorSpace = THREE.SRGBColorSpace;
+							diffuse.wrapS = THREE.RepeatWrapping;
+							diffuse.wrapT = THREE.RepeatWrapping;
+							diffuse.repeat.x = 10;
+							diffuse.repeat.y = 10;
+
+							const normalMap = textureLoader.load( 'textures/carbon/Carbon_Normal.png' );
+							normalMap.wrapS = THREE.RepeatWrapping;
+							normalMap.wrapT = THREE.RepeatWrapping;
+							normalMap.repeat.x = 10;
+							normalMap.repeat.y = 10;
+
+							const normalMap2 = textureLoader.load( 'textures/water/Water_1_M_Normal.jpg' );
+
+							const normalMap3 = new THREE.CanvasTexture( new FlakesTexture() );
+							normalMap3.wrapS = THREE.RepeatWrapping;
+							normalMap3.wrapT = THREE.RepeatWrapping;
+							normalMap3.repeat.x = 10;
+							normalMap3.repeat.y = 6;
+							normalMap3.anisotropy = 16;
+
+							const normalMap4 = textureLoader.load( 'textures/golfball.jpg' );
+
+							const clearcoatNormalMap = textureLoader.load( 'textures/pbr/Scratched_gold/Scratched_gold_01_1K_Normal.png' );
+
+							// car paint
+
+							let material = new THREE.MeshPhysicalMaterial( {
+								clearcoat: 1.0,
+								clearcoatRoughness: 0.1,
+								metalness: 0.9,
+								roughness: 0.5,
+								color: 0x0000ff,
+								normalMap: normalMap3,
+								normalScale: new THREE.Vector2( 0.15, 0.15 )
+							} );
+							let mesh = new THREE.Mesh( geometry, material );
+							mesh.position.x = - 1;
+							mesh.position.y = 1;
+							group.add( mesh );
+
+							// fibers
+
+							material = new THREE.MeshPhysicalMaterial( {
+								roughness: 0.5,
+								clearcoat: 1.0,
+								clearcoatRoughness: 0.1,
+								map: diffuse,
+								normalMap: normalMap
+							} );
+							mesh = new THREE.Mesh( geometry, material );
+							mesh.position.x = 1;
+							mesh.position.y = 1;
+							group.add( mesh );
+
+							// golf
+
+							material = new THREE.MeshPhysicalMaterial( {
+								metalness: 0.0,
+								roughness: 0.1,
+								clearcoat: 1.0,
+								normalMap: normalMap4,
+								clearcoatNormalMap: clearcoatNormalMap,
+
+								// y scale is negated to compensate for normal map handedness.
+								clearcoatNormalScale: new THREE.Vector2( 2.0, - 2.0 )
+							} );
+							mesh = new THREE.Mesh( geometry, material );
+							mesh.position.x = - 1;
+							mesh.position.y = - 1;
+							group.add( mesh );
+
+							// clearcoat + normalmap
+
+							material = new THREE.MeshPhysicalMaterial( {
+								clearcoat: 1.0,
+								metalness: 1.0,
+								color: 0xff0000,
+								normalMap: normalMap2,
+								normalScale: new THREE.Vector2( 0.15, 0.15 ),
+								clearcoatNormalMap: clearcoatNormalMap,
+
+								// y scale is negated to compensate for normal map handedness.
+								clearcoatNormalScale: new THREE.Vector2( 2.0, - 2.0 )
+							} );
+							mesh = new THREE.Mesh( geometry, material );
+							mesh.position.x = 1;
+							mesh.position.y = - 1;
+							group.add( mesh );
+
+							//
+
+							scene.background = texture;
+							scene.environment = texture;
+
+						}
+
+					);
+
+				// LIGHTS
+
+				particleLight = new THREE.Mesh(
+					new THREE.SphereGeometry( .05, 8, 8 ),
+					new THREE.MeshBasicMaterial( { color: 0xffffff } )
+				);
+				scene.add( particleLight );
+
+				particleLight.add( new THREE.PointLight( 0xffffff, 30 ) );
+
+				renderer = new WebGPURenderer();
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				container.appendChild( renderer.domElement );
+
+				//
+
+				renderer.toneMapping = THREE.ACESFilmicToneMapping;
+				renderer.toneMappingExposure = 1.25;
+
+				//
+
+				stats = new Stats();
+				container.appendChild( stats.dom );
+
+				// EVENTS
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.minDistance = 3;
+				controls.maxDistance = 30;
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			//
+
+			function onWindowResize() {
+
+				const width = window.innerWidth;
+				const height = window.innerHeight;
+
+				camera.aspect = width / height;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( width, height );
+
+			}
+
+			//
+
+			function animate() {
+
+				requestAnimationFrame( animate );
+
+				render();
+
+				stats.update();
+
+			}
+
+			function render() {
+
+				const timer = Date.now() * 0.00025;
+
+				particleLight.position.x = Math.sin( timer * 7 ) * 3;
+				particleLight.position.y = Math.cos( timer * 5 ) * 4;
+				particleLight.position.z = Math.cos( timer * 3 ) * 3;
+
+				for ( let i = 0; i < group.children.length; i ++ ) {
+
+					const child = group.children[ i ];
+					child.rotation.y += 0.005;
+
+				}
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+	</body>
+</html>

+ 1 - 0
test/e2e/puppeteer.js

@@ -95,6 +95,7 @@ const exceptionList = [
 	'webgpu_audio_processing',
 	'webgpu_backdrop',
 	'webgpu_backdrop_area',
+	'webgpu_clearcoat',
 	'webgpu_compute',
 	'webgpu_cubemap_adjustments',
 	'webgpu_cubemap_dynamic',