Jelajahi Sumber

WebGPURenderer: PMREM (#27829)

* WebGPURenderer: PMREM (WIP)

* add temporary example

* Added fromCubemap()

* added pmrem and examples

* cleanup

* fix circular dependency

* Revert "fix circular dependency"

This reverts commit 83d68820b4a4b615124cc8292498f8929875083c.

* update pmrem examples

* NodeBuilder: Rename .getRenderTarget() -> .createRenderTarget()

* fix circular dependency (2)

* revision

* revision

* update `webgpu_cubemap_mix` example

* update `webgpu_cubemap_adjustments` example

* update `webgpu_equirectangular` example

* update screenshots
sunag 1 tahun lalu
induk
melakukan
12b08b230c
31 mengubah file dengan 1626 tambahan dan 142 penghapusan
  1. 3 0
      examples/files.json
  2. 4 1
      examples/jsm/nodes/Nodes.js
  3. 0 6
      examples/jsm/nodes/accessors/TextureNode.js
  4. 14 2
      examples/jsm/nodes/core/NodeBuilder.js
  5. 1 1
      examples/jsm/nodes/lighting/AnalyticLightNode.js
  6. 10 68
      examples/jsm/nodes/lighting/EnvironmentNode.js
  7. 165 0
      examples/jsm/nodes/pmrem/PMREMNode.js
  8. 288 0
      examples/jsm/nodes/pmrem/PMREMUtils.js
  9. 1 1
      examples/jsm/nodes/utils/EquirectUVNode.js
  10. 0 37
      examples/jsm/nodes/utils/SpecularMIPLevelNode.js
  11. 2 2
      examples/jsm/renderers/common/Background.js
  12. 757 0
      examples/jsm/renderers/common/extras/PMREMGenerator.js
  13. 4 17
      examples/jsm/renderers/common/nodes/Nodes.js
  14. TEMPAT SAMPAH
      examples/screenshots/webgpu_cubemap_adjustments.jpg
  15. TEMPAT SAMPAH
      examples/screenshots/webgpu_cubemap_dynamic.jpg
  16. TEMPAT SAMPAH
      examples/screenshots/webgpu_cubemap_mix.jpg
  17. TEMPAT SAMPAH
      examples/screenshots/webgpu_equirectangular.jpg
  18. TEMPAT SAMPAH
      examples/screenshots/webgpu_loader_gltf.jpg
  19. TEMPAT SAMPAH
      examples/screenshots/webgpu_loader_gltf_iridescence.jpg
  20. TEMPAT SAMPAH
      examples/screenshots/webgpu_loader_gltf_sheen.jpg
  21. TEMPAT SAMPAH
      examples/screenshots/webgpu_parallax_uv.jpg
  22. TEMPAT SAMPAH
      examples/screenshots/webgpu_pmrem_cubemap.jpg
  23. TEMPAT SAMPAH
      examples/screenshots/webgpu_pmrem_equirectangular.jpg
  24. TEMPAT SAMPAH
      examples/screenshots/webgpu_pmrem_scene.jpg
  25. TEMPAT SAMPAH
      examples/screenshots/webgpu_postprocessing_anamorphic.jpg
  26. 3 3
      examples/webgpu_cubemap_adjustments.html
  27. 3 3
      examples/webgpu_cubemap_mix.html
  28. 0 1
      examples/webgpu_equirectangular.html
  29. 114 0
      examples/webgpu_pmrem_cubemap.html
  30. 116 0
      examples/webgpu_pmrem_equirectangular.html
  31. 141 0
      examples/webgpu_pmrem_scene.html

+ 3 - 0
examples/files.json

@@ -372,6 +372,9 @@
 		"webgpu_tsl_editor",
 		"webgpu_tsl_transpiler",
 		"webgpu_video_panorama",
+		"webgpu_pmrem_cubemap",
+		"webgpu_pmrem_equirectangular",
+		"webgpu_pmrem_scene",
 		"webgpu_postprocessing_afterimage",
 		"webgpu_postprocessing_anamorphic",
 		"webgpu_mirror",

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

@@ -64,7 +64,6 @@ export { default as RemapNode, remap, remapClamp } from './utils/RemapNode.js';
 export { default as RotateUVNode, rotateUV } from './utils/RotateUVNode.js';
 export { default as RotateNode, rotate } from './utils/RotateNode.js';
 export { default as SetNode } from './utils/SetNode.js';
-export { default as SpecularMIPLevelNode, specularMIPLevel } from './utils/SpecularMIPLevelNode.js';
 export { default as SplitNode } from './utils/SplitNode.js';
 export { default as SpriteSheetUVNode, spritesheetUV } from './utils/SpriteSheetUVNode.js';
 export { default as StorageArrayElementNode } from './utils/StorageArrayElementNode.js';
@@ -160,6 +159,10 @@ export { default as EnvironmentNode } from './lighting/EnvironmentNode.js';
 export { default as AONode } from './lighting/AONode.js';
 export { default as AnalyticLightNode } from './lighting/AnalyticLightNode.js';
 
+// pmrem
+export { default as PMREMNode, pmremTexture } from './pmrem/PMREMNode.js';
+export * as PMREMUtils from './pmrem/PMREMUtils.js';
+
 // procedural
 export { default as CheckerNode, checker } from './procedural/CheckerNode.js';
 

+ 0 - 6
examples/jsm/nodes/accessors/TextureNode.js

@@ -126,12 +126,6 @@ class TextureNode extends UniformNode {
 
 		}
 
-		if ( levelNode !== null && builder.context.getTextureLevelAlgorithm !== undefined ) {
-
-			levelNode = builder.context.getTextureLevelAlgorithm( this, levelNode );
-
-		}
-
 		//
 
 		properties.uvNode = uvNode;

+ 14 - 2
examples/jsm/nodes/core/NodeBuilder.js

@@ -23,6 +23,8 @@ import { getCurrentStack, setCurrentStack } from '../shadernode/ShaderNode.js';
 import CubeRenderTarget from '../../renderers/common/CubeRenderTarget.js';
 import ChainMap from '../../renderers/common/ChainMap.js';
 
+import PMREMGenerator from '../../renderers/common/extras/PMREMGenerator.js';
+
 const uniformsGroupCache = new ChainMap();
 
 const typeFromLength = new Map( [
@@ -113,18 +115,26 @@ class NodeBuilder {
 
 	}
 
-	getRenderTarget( width, height, options ) {
+	createRenderTarget( width, height, options ) {
 
 		return new RenderTarget( width, height, options );
 
 	}
 
-	getCubeRenderTarget( size, options ) {
+	createCubeRenderTarget( size, options ) {
 
 		return new CubeRenderTarget( size, options );
 
 	}
 
+	createPMREMGenerator() {
+
+		// TODO: Move Materials.js to outside of the Nodes.js in order to remove this function and improve tree-shaking support
+
+		return new PMREMGenerator( this.renderer );
+
+	}
+
 	includes( node ) {
 
 		return this.nodes.includes( node );
@@ -1168,6 +1178,8 @@ class NodeBuilder {
 
 	createNodeMaterial( type = 'NodeMaterial' ) {
 
+		// TODO: Move Materials.js to outside of the Nodes.js in order to remove this function and improve tree-shaking support
+
 		return createNodeMaterialFromType( type );
 
 	}

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

@@ -63,7 +63,7 @@ class AnalyticLightNode extends LightingNode {
 			}
 
 			const shadow = this.light.shadow;
-			const rtt = builder.getRenderTarget( shadow.mapSize.width, shadow.mapSize.height );
+			const rtt = builder.createRenderTarget( shadow.mapSize.width, shadow.mapSize.height );
 
 			const depthTexture = new DepthTexture();
 			depthTexture.minFilter = NearestFilter;

+ 10 - 68
examples/jsm/nodes/lighting/EnvironmentNode.js

@@ -1,17 +1,14 @@
 import LightingNode from './LightingNode.js';
 import { cache } from '../core/CacheNode.js';
 import { context } from '../core/ContextNode.js';
-import { maxMipLevel } from '../utils/MaxMipLevelNode.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 { transformedClearcoatNormalView, transformedNormalView, transformedNormalWorld } from '../accessors/NormalNode.js';
 import { positionViewDirection } from '../accessors/PositionNode.js';
 import { addNodeClass } from '../core/Node.js';
-import { vec2 } from '../shadernode/ShaderNode.js';
-import { cubeTexture } from '../accessors/CubeTextureNode.js';
+import { float } from '../shadernode/ShaderNode.js';
 import { reference } from '../accessors/ReferenceNode.js';
+import { pmremTexture } from '../pmrem/PMREMNode.js';
 
 const envNodeCache = new WeakMap();
 
@@ -29,19 +26,13 @@ class EnvironmentNode extends LightingNode {
 
 		let envNode = this.envNode;
 
-		if ( envNode.isTextureNode && envNode.value.isCubeTexture !== true ) {
+		if ( envNode.isTextureNode ) {
 
 			let cacheEnvNode = envNodeCache.get( envNode.value );
 
 			if ( cacheEnvNode === undefined ) {
 
-				const texture = envNode.value;
-				const renderer = builder.renderer;
-
-				// @TODO: Add dispose logic here
-				const cubeRTT = builder.getCubeRenderTarget( 512 ).fromEquirectangularTexture( renderer, texture );
-
-				cacheEnvNode = cubeTexture( cubeRTT.texture );
+				cacheEnvNode = pmremTexture( envNode.value );
 
 				envNodeCache.set( envNode.value, cacheEnvNode );
 
@@ -86,12 +77,9 @@ class EnvironmentNode extends LightingNode {
 const createRadianceContext = ( roughnessNode, normalViewNode ) => {
 
 	let reflectVec = null;
-	let textureUVNode = null;
 
 	return {
-		getUV: ( textureNode ) => {
-
-			let node = null;
+		getUV: () => {
 
 			if ( reflectVec === null ) {
 
@@ -101,36 +89,13 @@ const createRadianceContext = ( roughnessNode, normalViewNode ) => {
 
 			}
 
-			if ( textureNode.isCubeTextureNode ) {
-
-				node = reflectVec;
-
-			} else if ( textureNode.isTextureNode ) {
-
-				if ( textureUVNode === null ) {
-
-					// @TODO: Needed PMREM
-
-					textureUVNode = equirectUV( reflectVec );
-
-				}
-
-				node = textureUVNode;
-
-			}
-
-			return node;
+			return reflectVec;
 
 		},
 		getTextureLevel: () => {
 
 			return roughnessNode;
 
-		},
-		getTextureLevelAlgorithm: ( textureNode, levelNode ) => {
-
-			return specularMIPLevel( textureNode, levelNode );
-
 		}
 	};
 
@@ -138,38 +103,15 @@ const createRadianceContext = ( roughnessNode, normalViewNode ) => {
 
 const createIrradianceContext = ( normalWorldNode ) => {
 
-	let textureUVNode = null;
-
 	return {
-		getUV: ( textureNode ) => {
-
-			let node = null;
-
-			if ( textureNode.isCubeTextureNode ) {
-
-				node = normalWorldNode;
-
-			} else if ( textureNode.isTextureNode ) {
-
-				if ( textureUVNode === null ) {
+		getUV: () => {
 
-					// @TODO: Needed PMREM
-
-					textureUVNode = equirectUV( normalWorldNode );
-					textureUVNode = vec2( textureUVNode.x, textureUVNode.y.oneMinus() );
-
-				}
-
-				node = textureUVNode;
-
-			}
-
-			return node;
+			return normalWorldNode;
 
 		},
-		getTextureLevel: ( textureNode ) => {
+		getTextureLevel: () => {
 
-			return maxMipLevel( textureNode );
+			return float( 1.0 );
 
 		}
 	};

+ 165 - 0
examples/jsm/nodes/pmrem/PMREMNode.js

@@ -0,0 +1,165 @@
+import TempNode from '../core/TempNode.js';
+import { addNodeClass } from '../core/Node.js';
+import { texture } from '../accessors/TextureNode.js';
+import { textureCubeUV } from './PMREMUtils.js';
+import { uniform } from '../core/UniformNode.js';
+import { NodeUpdateType } from '../core/constants.js';
+import { nodeProxy } from '../shadernode/ShaderNode.js';
+
+let _generator = null;
+
+const _cache = new WeakMap();
+
+function _generateCubeUVSize( imageHeight ) {
+
+	const maxMip = Math.log2( imageHeight ) - 2;
+
+	const texelHeight = 1.0 / imageHeight;
+
+	const texelWidth = 1.0 / ( 3 * Math.max( Math.pow( 2, maxMip ), 7 * 16 ) );
+
+	return { texelWidth, texelHeight, maxMip };
+
+}
+
+function _getPMREMFromTexture( texture ) {
+
+	let cacheTexture = _cache.get( texture );
+
+	if ( cacheTexture === undefined ) {
+
+		if ( texture.isCubeTexture ) {
+
+			cacheTexture = _generator.fromCubemap( texture );
+
+		} else {
+
+			cacheTexture = _generator.fromEquirectangular( texture );
+
+		}
+
+		_cache.set( texture, cacheTexture );
+
+	}
+
+	return cacheTexture.texture;
+
+}
+
+class PMREMNode extends TempNode {
+
+	constructor( value, uvNode = null, levelNode = null ) {
+
+		super( 'vec3' );
+
+		this._value = value;
+		this._pmrem = null;
+
+		this.uvNode = uvNode;
+		this.levelNode = levelNode;
+
+		this._generator = null;
+		this._texture = texture( null );
+		this._width = uniform( 0 );
+		this._height = uniform( 0 );
+		this._maxMip = uniform( 0 );
+
+		this.updateBeforeType = NodeUpdateType.RENDER;
+
+	}
+
+	set value( value ) {
+
+		this._value = value;
+		this._pmrem = null;
+
+	}
+
+	get value() {
+
+		return this._value;
+
+	}
+
+	updateFromTexture( texture ) {
+
+		const cubeUVSize = _generateCubeUVSize( texture.image.height );
+
+		this._texture.value = texture;
+		this._width.value = cubeUVSize.texelWidth;
+		this._height.value = cubeUVSize.texelHeight;
+		this._maxMip.value = cubeUVSize.maxMip;
+
+	}
+
+	updateBefore( frame ) {
+
+		let pmrem = this._pmrem;
+
+		if ( pmrem === null ) {
+
+			const texture = this._value;
+
+			if ( texture.isPMREMTexture === true ) {
+
+				pmrem = texture;
+
+			} else {
+
+				pmrem = _getPMREMFromTexture( texture );
+
+			}
+
+			this._pmrem = pmrem;
+
+			this.updateFromTexture( pmrem );
+
+		}
+
+	}
+
+	setup( builder ) {
+
+		if ( _generator === null ) {
+
+			_generator = builder.createPMREMGenerator();
+
+		}
+
+		//
+
+		this.updateBefore( builder );
+
+		//
+
+		let uvNode = this.uvNode;
+
+		if ( uvNode === null && builder.context.getUV ) {
+
+			uvNode = builder.context.getUV( this );
+
+		}
+
+		//
+
+		let levelNode = this.levelNode;
+
+		if ( levelNode === null && builder.context.getTextureLevel ) {
+
+			levelNode = builder.context.getTextureLevel( this );
+
+		}
+
+		//
+
+		return textureCubeUV( this._texture, uvNode, levelNode, this._width, this._height, this._maxMip );
+
+	}
+
+}
+
+export const pmremTexture = nodeProxy( PMREMNode );
+
+addNodeClass( 'PMREMNode', PMREMNode );
+
+export default PMREMNode;

+ 288 - 0
examples/jsm/nodes/pmrem/PMREMUtils.js

@@ -0,0 +1,288 @@
+import { tslFn, int, float, vec2, vec3, vec4, If } from '../shadernode/ShaderNode.js';
+import { cos, sin, abs, max, exp2, log2, clamp, fract, mix, floor, normalize, cross, all } from '../math/MathNode.js';
+import { mul } from '../math/OperatorNode.js';
+import { cond } from '../math/CondNode.js';
+import { loop, Break } from '../utils/LoopNode.js';
+
+// These defines must match with PMREMGenerator
+
+const cubeUV_r0 = float( 1.0 );
+const cubeUV_m0 = float( - 2.0 );
+const cubeUV_r1 = float( 0.8 );
+const cubeUV_m1 = float( - 1.0 );
+const cubeUV_r4 = float( 0.4 );
+const cubeUV_m4 = float( 2.0 );
+const cubeUV_r5 = float( 0.305 );
+const cubeUV_m5 = float( 3.0 );
+const cubeUV_r6 = float( 0.21 );
+const cubeUV_m6 = float( 4.0 );
+
+const cubeUV_minMipLevel = float( 4.0 );
+const cubeUV_minTileSize = float( 16.0 );
+
+// These shader functions convert between the UV coordinates of a single face of
+// a cubemap, the 0-5 integer index of a cube face, and the direction vector for
+// sampling a textureCube (not generally normalized ).
+
+const getFace = tslFn( ( [ direction ] ) => {
+
+	const absDirection = vec3( abs( direction ) ).toVar();
+	const face = float( - 1.0 ).toVar();
+
+	If( absDirection.x.greaterThan( absDirection.z ), () => {
+
+		If( absDirection.x.greaterThan( absDirection.y ), () => {
+
+			face.assign( cond( direction.x.greaterThan( 0.0 ), 0.0, 3.0 ) );
+
+		} ).else( () => {
+
+			face.assign( cond( direction.y.greaterThan( 0.0 ), 1.0, 4.0 ) );
+
+		} );
+
+	} ).else( () => {
+
+		If( absDirection.z.greaterThan( absDirection.y ), () => {
+
+			face.assign( cond( direction.z.greaterThan( 0.0 ), 2.0, 5.0 ) );
+
+		} ).else( () => {
+
+			face.assign( cond( direction.y.greaterThan( 0.0 ), 1.0, 4.0 ) );
+
+		} );
+
+	} );
+
+	return face;
+
+} ).setLayout( {
+	name: 'getFace',
+	type: 'float',
+	inputs: [
+		{ name: 'direction', type: 'vec3' }
+	]
+} );
+
+// RH coordinate system; PMREM face-indexing convention
+const getUV = tslFn( ( [ direction, face ] ) => {
+
+	const uv = vec2().toVar();
+
+	If( face.equal( 0.0 ), () => {
+
+		uv.assign( vec2( direction.z, direction.y ).div( abs( direction.x ) ) ); // pos x
+
+	} ).elseif( face.equal( 1.0 ), () => {
+
+		uv.assign( vec2( direction.x.negate(), direction.z.negate() ).div( abs( direction.y ) ) ); // pos y
+
+	} ).elseif( face.equal( 2.0 ), () => {
+
+		uv.assign( vec2( direction.x.negate(), direction.y ).div( abs( direction.z ) ) ); // pos z
+
+	} ).elseif( face.equal( 3.0 ), () => {
+
+		uv.assign( vec2( direction.z.negate(), direction.y ).div( abs( direction.x ) ) ); // neg x
+
+	} ).elseif( face.equal( 4.0 ), () => {
+
+		uv.assign( vec2( direction.x.negate(), direction.z ).div( abs( direction.y ) ) ); // neg y
+
+	} ).else( () => {
+
+		uv.assign( vec2( direction.x, direction.y ).div( abs( direction.z ) ) ); // neg z
+
+	} );
+
+	return mul( 0.5, uv.add( 1.0 ) );
+
+} ).setLayout( {
+	name: 'getUV',
+	type: 'vec2',
+	inputs: [
+		{ name: 'direction', type: 'vec3' },
+		{ name: 'face', type: 'float' }
+	]
+} );
+
+const roughnessToMip = tslFn( ( [ roughness ] ) => {
+
+	const mip = float( 0.0 ).toVar();
+
+	If( roughness.greaterThanEqual( cubeUV_r1 ), () => {
+
+		mip.assign( cubeUV_r0.sub( roughness ).mul( cubeUV_m1.sub( cubeUV_m0 ) ).div( cubeUV_r0.sub( cubeUV_r1 ) ).add( cubeUV_m0 ) );
+
+	} ).elseif( roughness.greaterThanEqual( cubeUV_r4 ), () => {
+
+		mip.assign( cubeUV_r1.sub( roughness ).mul( cubeUV_m4.sub( cubeUV_m1 ) ).div( cubeUV_r1.sub( cubeUV_r4 ) ).add( cubeUV_m1 ) );
+
+	} ).elseif( roughness.greaterThanEqual( cubeUV_r5 ), () => {
+
+		mip.assign( cubeUV_r4.sub( roughness ).mul( cubeUV_m5.sub( cubeUV_m4 ) ).div( cubeUV_r4.sub( cubeUV_r5 ) ).add( cubeUV_m4 ) );
+
+	} ).elseif( roughness.greaterThanEqual( cubeUV_r6 ), () => {
+
+		mip.assign( cubeUV_r5.sub( roughness ).mul( cubeUV_m6.sub( cubeUV_m5 ) ).div( cubeUV_r5.sub( cubeUV_r6 ) ).add( cubeUV_m5 ) );
+
+	} ).else( () => {
+
+		mip.assign( float( - 2.0 ).mul( log2( mul( 1.16, roughness ) ) ) ); // 1.16 = 1.79^0.25
+
+	} );
+
+	return mip;
+
+} ).setLayout( {
+	name: 'roughnessToMip',
+	type: 'float',
+	inputs: [
+		{ name: 'roughness', type: 'float' }
+	]
+} );
+
+// RH coordinate system; PMREM face-indexing convention
+export const getDirection = tslFn( ( [ uv_immutable, face ] ) => {
+
+	const uv = uv_immutable.toVar();
+	uv.assign( mul( 2.0, uv ).sub( 1.0 ) );
+	const direction = vec3( uv, 1.0 ).toVar();
+
+	If( face.equal( 0.0 ), () => {
+
+		direction.assign( direction.zyx ); // ( 1, v, u ) pos x
+
+	} ).elseif( face.equal( 1.0 ), () => {
+
+		direction.assign( direction.xzy );
+		direction.xz.mulAssign( - 1.0 ); // ( -u, 1, -v ) pos y
+
+	} ).elseif( face.equal( 2.0 ), () => {
+
+		direction.x.mulAssign( - 1.0 ); // ( -u, v, 1 ) pos z
+
+	} ).elseif( face.equal( 3.0 ), () => {
+
+		direction.assign( direction.zyx );
+		direction.xz.mulAssign( - 1.0 ); // ( -1, v, -u ) neg x
+
+	} ).elseif( face.equal( 4.0 ), () => {
+
+		direction.assign( direction.xzy );
+		direction.xy.mulAssign( - 1.0 ); // ( -u, -1, v ) neg y
+
+	} ).elseif( face.equal( 5.0 ), () => {
+
+		direction.z.mulAssign( - 1.0 ); // ( u, v, -1 ) neg zS
+
+	} );
+
+	return direction;
+
+} ).setLayout( {
+	name: 'getDirection',
+	type: 'vec3',
+	inputs: [
+		{ name: 'uv', type: 'vec2' },
+		{ name: 'face', type: 'float' }
+	]
+} );
+
+//
+
+export const textureCubeUV = tslFn( ( [ envMap, sampleDir_immutable, roughness_immutable, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP ] ) => {
+
+	const roughness = float( roughness_immutable );
+	const sampleDir = vec3( sampleDir_immutable );
+
+	const mip = clamp( roughnessToMip( roughness ), cubeUV_m0, CUBEUV_MAX_MIP );
+	const mipF = fract( mip );
+	const mipInt = floor( mip );
+	const color0 = vec3( bilinearCubeUV( envMap, sampleDir, mipInt, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP ) ).toVar();
+
+	If( mipF.notEqual( 0.0 ), () => {
+
+		const color1 = vec3( bilinearCubeUV( envMap, sampleDir, mipInt.add( 1.0 ), CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP ) ).toVar();
+
+		color0.assign( mix( color0, color1, mipF ) );
+
+	} );
+
+	return color0;
+
+} );
+
+const bilinearCubeUV = tslFn( ( [ envMap, direction_immutable, mipInt_immutable, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP ] ) => {
+
+	const mipInt = float( mipInt_immutable ).toVar();
+	const direction = vec3( direction_immutable );
+	const face = float( getFace( direction ) ).toVar();
+	const filterInt = float( max( cubeUV_minMipLevel.sub( mipInt ), 0.0 ) ).toVar();
+	mipInt.assign( max( mipInt, cubeUV_minMipLevel ) );
+	const faceSize = float( exp2( mipInt ) ).toVar();
+	const uv = vec2( getUV( direction, face ).mul( faceSize.sub( 2.0 ) ).add( 1.0 ) ).toVar();
+
+	If( face.greaterThan( 2.0 ), () => {
+
+		uv.y.addAssign( faceSize );
+		face.subAssign( 3.0 );
+
+	} );
+
+	uv.x.addAssign( face.mul( faceSize ) );
+	uv.x.addAssign( filterInt.mul( mul( 3.0, cubeUV_minTileSize ) ) );
+	uv.y.addAssign( mul( 4.0, exp2( CUBEUV_MAX_MIP ).sub( faceSize ) ) );
+	uv.x.mulAssign( CUBEUV_TEXEL_WIDTH );
+	uv.y.mulAssign( CUBEUV_TEXEL_HEIGHT );
+
+	return envMap.uv( uv );
+
+} );
+
+const getSample = tslFn( ( { envMap, mipInt, outputDirection, theta, axis, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP } ) => {
+
+	const cosTheta = cos( theta );
+
+	// Rodrigues' axis-angle rotation
+	const sampleDirection = outputDirection.mul( cosTheta )
+		.add( axis.cross( outputDirection ).mul( sin( theta ) ) )
+		.add( axis.mul( axis.dot( outputDirection ).mul( cosTheta.oneMinus() ) ) );
+
+	return bilinearCubeUV( envMap, sampleDirection, mipInt, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP );
+
+} );
+
+export const blur = tslFn( ( { n, latitudinal, poleAxis, outputDirection, weights, samples, dTheta, mipInt, envMap, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP } ) => {
+
+	const axis = vec3( cond( latitudinal, poleAxis, cross( poleAxis, outputDirection ) ) ).toVar();
+
+	If( all( axis.equals( vec3( 0.0 ) ) ), () => {
+
+		axis.assign( vec3( outputDirection.z, 0.0, outputDirection.x.negate() ) );
+
+	} );
+
+	axis.assign( normalize( axis ) );
+
+	const gl_FragColor = vec3().toVar();
+	gl_FragColor.addAssign( weights.element( int( 0 ) ).mul( getSample( { theta: 0.0, axis, outputDirection, mipInt, envMap, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP } ) ) );
+
+	loop( { start: int( 1 ), end: n }, ( { i } ) => {
+
+		If( i.greaterThanEqual( samples ), () => {
+
+			Break();
+
+		} );
+
+		const theta = float( dTheta.mul( float( i ) ) ).toVar();
+		gl_FragColor.addAssign( weights.element( i ).mul( getSample( { theta: theta.mul( - 1.0 ), axis, outputDirection, mipInt, envMap, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP } ) ) );
+		gl_FragColor.addAssign( weights.element( i ).mul( getSample( { theta, axis, outputDirection, mipInt, envMap, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP } ) ) );
+
+	} );
+
+	return vec4( gl_FragColor, 1 );
+
+} );

+ 1 - 1
examples/jsm/nodes/utils/EquirectUVNode.js

@@ -18,7 +18,7 @@ class EquirectUVNode extends TempNode {
 		const dir = this.dirNode;
 
 		const u = dir.z.atan2( dir.x ).mul( 1 / ( Math.PI * 2 ) ).add( 0.5 );
-		const v = dir.y.negate().clamp( - 1.0, 1.0 ).asin().mul( 1 / Math.PI ).add( 0.5 ); // @TODO: The use of negate() here could be an NDC issue.
+		const v = dir.y.clamp( - 1.0, 1.0 ).asin().mul( 1 / Math.PI ).add( 0.5 );
 
 		return vec2( u, v );
 

+ 0 - 37
examples/jsm/nodes/utils/SpecularMIPLevelNode.js

@@ -1,37 +0,0 @@
-import Node, { addNodeClass } from '../core/Node.js';
-import { maxMipLevel } from './MaxMipLevelNode.js';
-import { nodeProxy } from '../shadernode/ShaderNode.js';
-
-class SpecularMIPLevelNode extends Node {
-
-	constructor( textureNode, roughnessNode = null ) {
-
-		super( 'float' );
-
-		this.textureNode = textureNode;
-		this.roughnessNode = roughnessNode;
-
-	}
-
-	setup() {
-
-		const { textureNode, roughnessNode } = this;
-
-		// taken from here: http://casual-effects.blogspot.ca/2011/08/plausible-environment-lighting-in-two.html
-
-		const maxMIPLevelScalar = maxMipLevel( textureNode );
-
-		const sigma = roughnessNode.mul( roughnessNode ).mul( Math.PI ).div( roughnessNode.add( 1.0 ) );
-		const desiredMIPLevel = maxMIPLevelScalar.add( sigma.log2() );
-
-		return desiredMIPLevel.clamp( 0.0, maxMIPLevelScalar );
-
-	}
-
-}
-
-export default SpecularMIPLevelNode;
-
-export const specularMIPLevel = nodeProxy( SpecularMIPLevelNode );
-
-addNodeClass( 'SpecularMIPLevelNode', SpecularMIPLevelNode );

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

@@ -1,7 +1,7 @@
 import DataMap from './DataMap.js';
 import Color4 from './Color4.js';
 import { Mesh, SphereGeometry, BackSide } from 'three';
-import { vec4, context, normalWorld, backgroundBlurriness, backgroundIntensity, NodeMaterial, modelViewProjection, maxMipLevel } from '../../nodes/Nodes.js';
+import { vec4, context, normalWorld, backgroundBlurriness, backgroundIntensity, NodeMaterial, modelViewProjection } from '../../nodes/Nodes.js';
 
 const _clearColor = new Color4();
 
@@ -53,7 +53,7 @@ class Background extends DataMap {
 				const backgroundMeshNode = context( vec4( backgroundNode ), {
 					// @TODO: Add Texture2D support using node context
 					getUV: () => normalWorld,
-					getTextureLevel: ( textureNode ) => backgroundBlurriness.mul( maxMipLevel( textureNode ) )
+					getTextureLevel: () => backgroundBlurriness
 				} ).mul( backgroundIntensity );
 
 				let viewProj = modelViewProjection();

+ 757 - 0
examples/jsm/renderers/common/extras/PMREMGenerator.js

@@ -0,0 +1,757 @@
+import NodeMaterial from '../../../nodes/materials/NodeMaterial.js';
+import { getDirection, blur } from '../../../nodes/pmrem/PMREMUtils.js';
+import { equirectUV } from '../../../nodes/utils/EquirectUVNode.js';
+import { uniform } from '../../../nodes/core/UniformNode.js';
+import { uniforms } from '../../../nodes/accessors/UniformsNode.js';
+import { texture } from '../../../nodes/accessors/TextureNode.js';
+import { cubeTexture } from '../../../nodes/accessors/CubeTextureNode.js';
+import { float, vec3 } from '../../../nodes/shadernode/ShaderNode.js';
+import { uv } from '../../../nodes/accessors/UVNode.js';
+import { attribute } from '../../../nodes/core/AttributeNode.js';
+import {
+	OrthographicCamera,
+	Color,
+	Vector3,
+	BufferGeometry,
+	BufferAttribute,
+	RenderTarget,
+	Mesh,
+	CubeReflectionMapping,
+	CubeRefractionMapping,
+	CubeUVReflectionMapping,
+	LinearFilter,
+	NoToneMapping,
+	NoBlending,
+	RGBAFormat,
+	HalfFloatType,
+	BackSide,
+	LinearSRGBColorSpace,
+	PerspectiveCamera,
+	MeshBasicMaterial,
+	BoxGeometry
+} from 'three';
+
+const LOD_MIN = 4;
+
+// The standard deviations (radians) associated with the extra mips. These are
+// chosen to approximate a Trowbridge-Reitz distribution function times the
+// geometric shadowing function. These sigma values squared must match the
+// variance #defines in cube_uv_reflection_fragment.glsl.js.
+const EXTRA_LOD_SIGMA = [ 0.125, 0.215, 0.35, 0.446, 0.526, 0.582 ];
+
+// The maximum length of the blur for loop. Smaller sigmas will use fewer
+// samples and exit early, but not recompile the shader.
+const MAX_SAMPLES = 20;
+
+const _flatCamera = /*@__PURE__*/ new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
+const _clearColor = /*@__PURE__*/ new Color();
+let _oldTarget = null;
+let _oldActiveCubeFace = 0;
+let _oldActiveMipmapLevel = 0;
+
+// Golden Ratio
+const PHI = ( 1 + Math.sqrt( 5 ) ) / 2;
+const INV_PHI = 1 / PHI;
+
+// Vertices of a dodecahedron (except the opposites, which represent the
+// same axis), used as axis directions evenly spread on a sphere.
+const _axisDirections = [
+	/*@__PURE__*/ new Vector3( 1, 1, 1 ),
+	/*@__PURE__*/ new Vector3( - 1, 1, 1 ),
+	/*@__PURE__*/ new Vector3( 1, 1, - 1 ),
+	/*@__PURE__*/ new Vector3( - 1, 1, - 1 ),
+	/*@__PURE__*/ new Vector3( 0, PHI, INV_PHI ),
+	/*@__PURE__*/ new Vector3( 0, PHI, - INV_PHI ),
+	/*@__PURE__*/ new Vector3( INV_PHI, 0, PHI ),
+	/*@__PURE__*/ new Vector3( - INV_PHI, 0, PHI ),
+	/*@__PURE__*/ new Vector3( PHI, INV_PHI, 0 ),
+	/*@__PURE__*/ new Vector3( - PHI, INV_PHI, 0 )
+];
+
+//
+
+// WebGPU Face indices
+const _faceLib = [
+	3, 1, 5,
+	0, 4, 2
+];
+
+const direction = getDirection( uv(), attribute( 'faceIndex' ) ).normalize();
+const outputDirection = vec3( direction.x, direction.y.negate(), direction.z );
+
+/**
+ * This class generates a Prefiltered, Mipmapped Radiance Environment Map
+ * (PMREM) from a cubeMap environment texture. This allows different levels of
+ * blur to be quickly accessed based on material roughness. It is packed into a
+ * special CubeUV format that allows us to perform custom interpolation so that
+ * we can support nonlinear formats such as RGBE. Unlike a traditional mipmap
+ * chain, it only goes down to the LOD_MIN level (above), and then creates extra
+ * even more filtered 'mips' at the same LOD_MIN resolution, associated with
+ * higher roughness levels. In this way we maintain resolution to smoothly
+ * interpolate diffuse lighting while limiting sampling computation.
+ *
+ * Paper: Fast, Accurate Image-Based Lighting
+ * https://drive.google.com/file/d/15y8r_UpKlU9SvV4ILb0C3qCPecS8pvLz/view
+*/
+
+class PMREMGenerator {
+
+	constructor( renderer ) {
+
+		this._renderer = renderer;
+		this._pingPongRenderTarget = null;
+
+		this._lodMax = 0;
+		this._cubeSize = 0;
+		this._lodPlanes = [];
+		this._sizeLods = [];
+		this._sigmas = [];
+
+		this._blurMaterial = null;
+		this._cubemapMaterial = null;
+		this._equirectMaterial = null;
+
+	}
+
+	/**
+	 * Generates a PMREM from a supplied Scene, which can be faster than using an
+	 * image if networking bandwidth is low. Optional sigma specifies a blur radius
+	 * in radians to be applied to the scene before PMREM generation. Optional near
+	 * and far planes ensure the scene is rendered in its entirety (the cubeCamera
+	 * is placed at the origin).
+	 */
+	fromScene( scene, sigma = 0, near = 0.1, far = 100 ) {
+
+		_oldTarget = this._renderer.getRenderTarget();
+		_oldActiveCubeFace = this._renderer.getActiveCubeFace();
+		_oldActiveMipmapLevel = this._renderer.getActiveMipmapLevel();
+
+		this._setSize( 256 );
+
+		const cubeUVRenderTarget = this._allocateTargets();
+		cubeUVRenderTarget.depthBuffer = true;
+
+		this._sceneToCubeUV( scene, near, far, cubeUVRenderTarget );
+
+		if ( sigma > 0 ) {
+
+			this._blur( cubeUVRenderTarget, 0, 0, sigma );
+
+		}
+
+		this._applyPMREM( cubeUVRenderTarget );
+
+		this._cleanup( cubeUVRenderTarget );
+
+		return cubeUVRenderTarget;
+
+	}
+
+	/**
+	 * Generates a PMREM from an equirectangular texture, which can be either LDR
+	 * or HDR. The ideal input image size is 1k (1024 x 512),
+	 * as this matches best with the 256 x 256 cubemap output.
+	 */
+	fromEquirectangular( equirectangular, renderTarget = null ) {
+
+		return this._fromTexture( equirectangular, renderTarget );
+
+	}
+
+	/**
+	 * Generates a PMREM from an cubemap texture, which can be either LDR
+	 * or HDR. The ideal input cube size is 256 x 256,
+	 * as this matches best with the 256 x 256 cubemap output.
+	 */
+	fromCubemap( cubemap, renderTarget = null ) {
+
+		return this._fromTexture( cubemap, renderTarget );
+
+	}
+
+	/**
+	 * Pre-compiles the cubemap shader. You can get faster start-up by invoking this method during
+	 * your texture's network fetch for increased concurrency.
+	 */
+	compileCubemapShader() {
+
+		if ( this._cubemapMaterial === null ) {
+
+			this._cubemapMaterial = _getCubemapMaterial();
+			this._compileMaterial( this._cubemapMaterial );
+
+		}
+
+	}
+
+	/**
+	 * Pre-compiles the equirectangular shader. You can get faster start-up by invoking this method during
+	 * your texture's network fetch for increased concurrency.
+	 */
+	compileEquirectangularShader() {
+
+		if ( this._equirectMaterial === null ) {
+
+			this._equirectMaterial = _getEquirectMaterial();
+			this._compileMaterial( this._equirectMaterial );
+
+		}
+
+	}
+
+	/**
+	 * Disposes of the PMREMGenerator's internal memory. Note that PMREMGenerator is a static class,
+	 * so you should not need more than one PMREMGenerator object. If you do, calling dispose() on
+	 * one of them will cause any others to also become unusable.
+	 */
+	dispose() {
+
+		this._dispose();
+
+		if ( this._cubemapMaterial !== null ) this._cubemapMaterial.dispose();
+		if ( this._equirectMaterial !== null ) this._equirectMaterial.dispose();
+
+	}
+
+	// private interface
+
+	_setSize( cubeSize ) {
+
+		this._lodMax = Math.floor( Math.log2( cubeSize ) );
+		this._cubeSize = Math.pow( 2, this._lodMax );
+
+	}
+
+	_dispose() {
+
+		if ( this._blurMaterial !== null ) this._blurMaterial.dispose();
+
+		if ( this._pingPongRenderTarget !== null ) this._pingPongRenderTarget.dispose();
+
+		for ( let i = 0; i < this._lodPlanes.length; i ++ ) {
+
+			this._lodPlanes[ i ].dispose();
+
+		}
+
+	}
+
+	_cleanup( outputTarget ) {
+
+		this._renderer.setRenderTarget( _oldTarget, _oldActiveCubeFace, _oldActiveMipmapLevel );
+		outputTarget.scissorTest = false;
+		_setViewport( outputTarget, 0, 0, outputTarget.width, outputTarget.height );
+
+	}
+
+	_fromTexture( texture, renderTarget ) {
+
+		if ( texture.mapping === CubeReflectionMapping || texture.mapping === CubeRefractionMapping ) {
+
+			this._setSize( texture.image.length === 0 ? 16 : ( texture.image[ 0 ].width || texture.image[ 0 ].image.width ) );
+
+		} else { // Equirectangular
+
+			this._setSize( texture.image.width / 4 );
+
+		}
+
+		_oldTarget = this._renderer.getRenderTarget();
+		_oldActiveCubeFace = this._renderer.getActiveCubeFace();
+		_oldActiveMipmapLevel = this._renderer.getActiveMipmapLevel();
+
+		const cubeUVRenderTarget = renderTarget || this._allocateTargets();
+		this._textureToCubeUV( texture, cubeUVRenderTarget );
+		this._applyPMREM( cubeUVRenderTarget );
+		this._cleanup( cubeUVRenderTarget );
+
+		return cubeUVRenderTarget;
+
+	}
+
+	_allocateTargets() {
+
+		const width = 3 * Math.max( this._cubeSize, 16 * 7 );
+		const height = 4 * this._cubeSize;
+
+		const params = {
+			magFilter: LinearFilter,
+			minFilter: LinearFilter,
+			generateMipmaps: false,
+			type: HalfFloatType,
+			format: RGBAFormat,
+			colorSpace: LinearSRGBColorSpace,
+			//depthBuffer: false
+		};
+
+		const cubeUVRenderTarget = _createRenderTarget( width, height, params );
+
+		if ( this._pingPongRenderTarget === null || this._pingPongRenderTarget.width !== width || this._pingPongRenderTarget.height !== height ) {
+
+			if ( this._pingPongRenderTarget !== null ) {
+
+				this._dispose();
+
+			}
+
+			this._pingPongRenderTarget = _createRenderTarget( width, height, params );
+
+			const { _lodMax } = this;
+			( { sizeLods: this._sizeLods, lodPlanes: this._lodPlanes, sigmas: this._sigmas } = _createPlanes( _lodMax ) );
+
+			this._blurMaterial = _getBlurShader( _lodMax, width, height );
+
+		}
+
+		return cubeUVRenderTarget;
+
+	}
+
+	_compileMaterial( material ) {
+
+		const tmpMesh = new Mesh( this._lodPlanes[ 0 ], material );
+		this._renderer.compile( tmpMesh, _flatCamera );
+
+	}
+
+	_sceneToCubeUV( scene, near, far, cubeUVRenderTarget ) {
+
+		const fov = 90;
+		const aspect = 1;
+		const cubeCamera = new PerspectiveCamera( fov, aspect, near, far );
+
+		// px, py, pz, nx, ny, nz
+		const upSign = [ - 1, 1, - 1, - 1, - 1, - 1 ];
+		const forwardSign = [ 1, 1, 1, - 1, - 1, - 1 ];
+
+		const renderer = this._renderer;
+
+		const originalAutoClear = renderer.autoClear;
+		const toneMapping = renderer.toneMapping;
+		renderer.getClearColor( _clearColor );
+
+		renderer.toneMapping = NoToneMapping;
+		renderer.autoClear = false;
+
+		const backgroundMaterial = new MeshBasicMaterial( {
+			name: 'PMREM.Background',
+			side: BackSide,
+			depthWrite: false,
+			depthTest: false
+		} );
+
+		const backgroundBox = new Mesh( new BoxGeometry(), backgroundMaterial );
+
+		let useSolidColor = false;
+		const background = scene.background;
+
+		if ( background ) {
+
+			if ( background.isColor ) {
+
+				backgroundMaterial.color.copy( background );
+				scene.background = null;
+				useSolidColor = true;
+
+			}
+
+		} else {
+
+			backgroundMaterial.color.copy( _clearColor );
+			useSolidColor = true;
+
+		}
+
+		renderer.setRenderTarget( cubeUVRenderTarget );
+
+		renderer.clear();
+
+		if ( useSolidColor ) {
+
+			renderer.render( backgroundBox, cubeCamera );
+
+		}
+
+		for ( let i = 0; i < 6; i ++ ) {
+
+			const col = i % 3;
+
+			if ( col === 0 ) {
+
+				cubeCamera.up.set( 0, upSign[ i ], 0 );
+				cubeCamera.lookAt( forwardSign[ i ], 0, 0 );
+
+			} else if ( col === 1 ) {
+
+				cubeCamera.up.set( 0, 0, upSign[ i ] );
+				cubeCamera.lookAt( 0, forwardSign[ i ], 0 );
+
+			} else {
+
+				cubeCamera.up.set( 0, upSign[ i ], 0 );
+				cubeCamera.lookAt( 0, 0, forwardSign[ i ] );
+
+			}
+
+			const size = this._cubeSize;
+
+			_setViewport( cubeUVRenderTarget, col * size, i > 2 ? size : 0, size, size );
+
+			renderer.render( scene, cubeCamera );
+
+		}
+
+		backgroundBox.geometry.dispose();
+		backgroundBox.material.dispose();
+
+		renderer.toneMapping = toneMapping;
+		renderer.autoClear = originalAutoClear;
+		scene.background = background;
+
+	}
+
+	_textureToCubeUV( texture, cubeUVRenderTarget ) {
+
+		const renderer = this._renderer;
+
+		const isCubeTexture = ( texture.mapping === CubeReflectionMapping || texture.mapping === CubeRefractionMapping );
+
+		if ( isCubeTexture ) {
+
+			if ( this._cubemapMaterial === null ) {
+
+				this._cubemapMaterial = _getCubemapMaterial();
+
+			}
+
+		} else {
+
+			if ( this._equirectMaterial === null ) {
+
+				this._equirectMaterial = _getEquirectMaterial();
+
+			}
+
+		}
+
+		const material = isCubeTexture ? this._cubemapMaterial : this._equirectMaterial;
+		const mesh = new Mesh( this._lodPlanes[ 0 ], material );
+
+		material.fragmentNode.value = texture;
+
+		const size = this._cubeSize;
+
+		_setViewport( cubeUVRenderTarget, 0, 0, 3 * size, 2 * size );
+
+		renderer.setRenderTarget( cubeUVRenderTarget );
+		renderer.render( mesh, _flatCamera );
+
+	}
+
+	_applyPMREM( cubeUVRenderTarget ) {
+
+		const renderer = this._renderer;
+		const autoClear = renderer.autoClear;
+		renderer.autoClear = false;
+
+		for ( let i = 1; i < this._lodPlanes.length; i ++ ) {
+
+			const sigma = Math.sqrt( this._sigmas[ i ] * this._sigmas[ i ] - this._sigmas[ i - 1 ] * this._sigmas[ i - 1 ] );
+
+			const poleAxis = _axisDirections[ ( i - 1 ) % _axisDirections.length ];
+
+			this._blur( cubeUVRenderTarget, i - 1, i, sigma, poleAxis );
+
+		}
+
+		renderer.autoClear = autoClear;
+
+	}
+
+	/**
+	 * This is a two-pass Gaussian blur for a cubemap. Normally this is done
+	 * vertically and horizontally, but this breaks down on a cube. Here we apply
+	 * the blur latitudinally (around the poles), and then longitudinally (towards
+	 * the poles) to approximate the orthogonally-separable blur. It is least
+	 * accurate at the poles, but still does a decent job.
+	 */
+	_blur( cubeUVRenderTarget, lodIn, lodOut, sigma, poleAxis ) {
+
+		const pingPongRenderTarget = this._pingPongRenderTarget;
+
+		this._halfBlur(
+			cubeUVRenderTarget,
+			pingPongRenderTarget,
+			lodIn,
+			lodOut,
+			sigma,
+			'latitudinal',
+			poleAxis );
+
+		this._halfBlur(
+			pingPongRenderTarget,
+			cubeUVRenderTarget,
+			lodOut,
+			lodOut,
+			sigma,
+			'longitudinal',
+			poleAxis );
+
+	}
+
+	_halfBlur( targetIn, targetOut, lodIn, lodOut, sigmaRadians, direction, poleAxis ) {
+
+		const renderer = this._renderer;
+		const blurMaterial = this._blurMaterial;
+
+		if ( direction !== 'latitudinal' && direction !== 'longitudinal' ) {
+
+			console.error(
+				'blur direction must be either latitudinal or longitudinal!' );
+
+		}
+
+		// Number of standard deviations at which to cut off the discrete approximation.
+		const STANDARD_DEVIATIONS = 3;
+
+		const blurMesh = new Mesh( this._lodPlanes[ lodOut ], blurMaterial );
+		const blurUniforms = blurMaterial.uniforms;
+
+		const pixels = this._sizeLods[ lodIn ] - 1;
+		const radiansPerPixel = isFinite( sigmaRadians ) ? Math.PI / ( 2 * pixels ) : 2 * Math.PI / ( 2 * MAX_SAMPLES - 1 );
+		const sigmaPixels = sigmaRadians / radiansPerPixel;
+		const samples = isFinite( sigmaRadians ) ? 1 + Math.floor( STANDARD_DEVIATIONS * sigmaPixels ) : MAX_SAMPLES;
+
+		if ( samples > MAX_SAMPLES ) {
+
+			console.warn( `sigmaRadians, ${
+				sigmaRadians}, is too large and will clip, as it requested ${
+				samples} samples when the maximum is set to ${MAX_SAMPLES}` );
+
+		}
+
+		const weights = [];
+		let sum = 0;
+
+		for ( let i = 0; i < MAX_SAMPLES; ++ i ) {
+
+			const x = i / sigmaPixels;
+			const weight = Math.exp( - x * x / 2 );
+			weights.push( weight );
+
+			if ( i === 0 ) {
+
+				sum += weight;
+
+			} else if ( i < samples ) {
+
+				sum += 2 * weight;
+
+			}
+
+		}
+
+		for ( let i = 0; i < weights.length; i ++ ) {
+
+			weights[ i ] = weights[ i ] / sum;
+
+		}
+
+		blurUniforms.envMap.value = targetIn.texture;
+		blurUniforms.samples.value = samples;
+		blurUniforms.weights.array = weights;
+		blurUniforms.latitudinal.value = direction === 'latitudinal' ? 1 : 0;
+
+		if ( poleAxis ) {
+
+			blurUniforms.poleAxis.value = poleAxis;
+
+		}
+
+		const { _lodMax } = this;
+		blurUniforms.dTheta.value = radiansPerPixel;
+		blurUniforms.mipInt.value = _lodMax - lodIn;
+
+		const outputSize = this._sizeLods[ lodOut ];
+		const x = 3 * outputSize * ( lodOut > _lodMax - LOD_MIN ? lodOut - _lodMax + LOD_MIN : 0 );
+		const y = 4 * ( this._cubeSize - outputSize );
+
+		_setViewport( targetOut, x, y, 3 * outputSize, 2 * outputSize );
+		renderer.setRenderTarget( targetOut );
+		renderer.render( blurMesh, _flatCamera );
+
+	}
+
+}
+
+function _createPlanes( lodMax ) {
+
+	const lodPlanes = [];
+	const sizeLods = [];
+	const sigmas = [];
+
+	let lod = lodMax;
+
+	const totalLods = lodMax - LOD_MIN + 1 + EXTRA_LOD_SIGMA.length;
+
+	for ( let i = 0; i < totalLods; i ++ ) {
+
+		const sizeLod = Math.pow( 2, lod );
+		sizeLods.push( sizeLod );
+		let sigma = 1.0 / sizeLod;
+
+		if ( i > lodMax - LOD_MIN ) {
+
+			sigma = EXTRA_LOD_SIGMA[ i - lodMax + LOD_MIN - 1 ];
+
+		} else if ( i === 0 ) {
+
+			sigma = 0;
+
+		}
+
+		sigmas.push( sigma );
+
+		const texelSize = 1.0 / ( sizeLod - 2 );
+		const min = - texelSize;
+		const max = 1 + texelSize;
+		const uv1 = [ min, min, max, min, max, max, min, min, max, max, min, max ];
+
+		const cubeFaces = 6;
+		const vertices = 6;
+		const positionSize = 3;
+		const uvSize = 2;
+		const faceIndexSize = 1;
+
+		const position = new Float32Array( positionSize * vertices * cubeFaces );
+		const uv = new Float32Array( uvSize * vertices * cubeFaces );
+		const faceIndex = new Float32Array( faceIndexSize * vertices * cubeFaces );
+
+		for ( let face = 0; face < cubeFaces; face ++ ) {
+
+			const x = ( face % 3 ) * 2 / 3 - 1;
+			const y = face > 2 ? 0 : - 1;
+			const coordinates = [
+				x, y, 0,
+				x + 2 / 3, y, 0,
+				x + 2 / 3, y + 1, 0,
+				x, y, 0,
+				x + 2 / 3, y + 1, 0,
+				x, y + 1, 0
+			];
+
+			const faceIdx = _faceLib[ face ];
+			position.set( coordinates, positionSize * vertices * faceIdx );
+			uv.set( uv1, uvSize * vertices * faceIdx );
+			const fill = [ faceIdx, faceIdx, faceIdx, faceIdx, faceIdx, faceIdx ];
+			faceIndex.set( fill, faceIndexSize * vertices * faceIdx );
+
+		}
+
+		const planes = new BufferGeometry();
+		planes.setAttribute( 'position', new BufferAttribute( position, positionSize ) );
+		planes.setAttribute( 'uv', new BufferAttribute( uv, uvSize ) );
+		planes.setAttribute( 'faceIndex', new BufferAttribute( faceIndex, faceIndexSize ) );
+		lodPlanes.push( planes );
+
+		if ( lod > LOD_MIN ) {
+
+			lod --;
+
+		}
+
+	}
+
+	return { lodPlanes, sizeLods, sigmas };
+
+}
+
+function _createRenderTarget( width, height, params ) {
+
+	const cubeUVRenderTarget = new RenderTarget( width, height, params );
+	cubeUVRenderTarget.texture.mapping = CubeUVReflectionMapping;
+	cubeUVRenderTarget.texture.name = 'PMREM.cubeUv';
+	cubeUVRenderTarget.texture.isPMREMTexture = true;
+	cubeUVRenderTarget.scissorTest = true;
+	return cubeUVRenderTarget;
+
+}
+
+function _setViewport( target, x, y, width, height ) {
+
+	const viewY = target.height - height - y;
+
+	target.viewport.set( x, viewY, width, height );
+	target.scissor.set( x, viewY, width, height );
+
+}
+
+function _getMaterial() {
+
+	const material = new NodeMaterial();
+	material.colorSpaced = false;
+	material.toneMapped = false;
+	material.depthTest = false;
+	material.depthWrite = false;
+	material.blending = NoBlending;
+
+	return material;
+
+}
+
+function _getBlurShader( lodMax, width, height ) {
+
+	const weights = uniforms( new Array( MAX_SAMPLES ).fill( 0 ) );
+	const poleAxis = uniform( new Vector3( 0, 1, 0 ) );
+	const dTheta = uniform( 0 );
+	const n = float( MAX_SAMPLES );
+	const latitudinal = uniform( 0 ); // false, bool
+	const samples = uniform( 1 ); // int
+	const envMap = texture( null );
+	const mipInt = uniform( 0 ); // int
+	const CUBEUV_TEXEL_WIDTH = float( 1 / width );
+	const CUBEUV_TEXEL_HEIGHT = float( 1 / height );
+	const CUBEUV_MAX_MIP = float( lodMax );
+
+	const materialUniforms = {
+		n,
+		latitudinal,
+		weights,
+		poleAxis,
+		outputDirection,
+		dTheta,
+		samples,
+		envMap,
+		mipInt,
+		CUBEUV_TEXEL_WIDTH,
+		CUBEUV_TEXEL_HEIGHT,
+		CUBEUV_MAX_MIP
+	};
+
+	const material = _getMaterial();
+	material.uniforms = materialUniforms; // TODO: Move to outside of the material
+	material.fragmentNode = blur( { ...materialUniforms, latitudinal: latitudinal.equal( 1 ) } );
+
+	return material;
+
+}
+
+function _getCubemapMaterial() {
+
+	const material = _getMaterial();
+	material.fragmentNode = cubeTexture( texture, outputDirection );
+
+	return material;
+
+}
+
+function _getEquirectMaterial() {
+
+	const material = _getMaterial();
+	material.fragmentNode = texture( texture, equirectUV( outputDirection ), 0 );
+
+	return material;
+
+}
+
+export default PMREMGenerator;

+ 4 - 17
examples/jsm/renderers/common/nodes/Nodes.js

@@ -2,7 +2,7 @@ import DataMap from '../DataMap.js';
 import ChainMap from '../ChainMap.js';
 import NodeBuilderState from './NodeBuilderState.js';
 import { NoToneMapping, EquirectangularReflectionMapping, EquirectangularRefractionMapping } from 'three';
-import { NodeFrame, objectGroup, renderGroup, frameGroup, cubeTexture, texture, rangeFog, densityFog, reference, toneMapping, equirectUV, viewportBottomLeft, normalWorld } from '../../../nodes/Nodes.js';
+import { NodeFrame, objectGroup, renderGroup, frameGroup, cubeTexture, texture, rangeFog, densityFog, reference, toneMapping, viewportBottomLeft, normalWorld, pmremTexture } from '../../../nodes/Nodes.js';
 
 class Nodes extends DataMap {
 
@@ -305,26 +305,13 @@ class Nodes extends DataMap {
 
 				let backgroundNode = null;
 
-				if ( background.isCubeTexture === true ) {
+				if ( background.isCubeTexture === true || ( background.mapping === EquirectangularReflectionMapping || background.mapping === EquirectangularRefractionMapping ) ) {
 
-					backgroundNode = cubeTexture( background, normalWorld );
+					backgroundNode = pmremTexture( background, normalWorld );
 
 				} else if ( background.isTexture === true ) {
 
-					let nodeUV = null;
-
-					if ( background.mapping === EquirectangularReflectionMapping || background.mapping === EquirectangularRefractionMapping ) {
-
-						nodeUV = equirectUV();
-						background.flipY = false;
-
-					} else {
-
-						nodeUV = viewportBottomLeft;
-
-					}
-
-					backgroundNode = texture( background, nodeUV ).setUpdateMatrix( true );
+					backgroundNode = texture( background, viewportBottomLeft ).setUpdateMatrix( true );
 
 				} else if ( background.isColor !== true ) {
 

TEMPAT SAMPAH
examples/screenshots/webgpu_cubemap_adjustments.jpg


TEMPAT SAMPAH
examples/screenshots/webgpu_cubemap_dynamic.jpg


TEMPAT SAMPAH
examples/screenshots/webgpu_cubemap_mix.jpg


TEMPAT SAMPAH
examples/screenshots/webgpu_equirectangular.jpg


TEMPAT SAMPAH
examples/screenshots/webgpu_loader_gltf.jpg


TEMPAT SAMPAH
examples/screenshots/webgpu_loader_gltf_iridescence.jpg


TEMPAT SAMPAH
examples/screenshots/webgpu_loader_gltf_sheen.jpg


TEMPAT SAMPAH
examples/screenshots/webgpu_parallax_uv.jpg


TEMPAT SAMPAH
examples/screenshots/webgpu_pmrem_cubemap.jpg


TEMPAT SAMPAH
examples/screenshots/webgpu_pmrem_equirectangular.jpg


TEMPAT SAMPAH
examples/screenshots/webgpu_pmrem_scene.jpg


TEMPAT SAMPAH
examples/screenshots/webgpu_postprocessing_anamorphic.jpg


+ 3 - 3
examples/webgpu_cubemap_adjustments.html

@@ -27,7 +27,7 @@
 		<script type="module">
 
 			import * as THREE from 'three';
-			import { uniform, mix, cubeTexture, reference, positionLocal, positionWorld, normalWorld, positionWorldDirection, reflectVector, toneMapping, maxMipLevel } from 'three/nodes';
+			import { uniform, mix, pmremTexture, reference, positionLocal, positionWorld, normalWorld, positionWorldDirection, reflectVector, toneMapping } from 'three/nodes';
 
 			import WebGPU from 'three/addons/capabilities/WebGPU.js';
 			import WebGL from 'three/addons/capabilities/WebGL.js';
@@ -107,7 +107,7 @@
 
 					const custom1UV = reflectNode.xyz.mul( uniform( rotateY1Matrix ) );
 					const custom2UV = reflectNode.xyz.mul( uniform( rotateY2Matrix ) );
-					const mixCubeMaps = mix( cubeTexture( cube1Texture, custom1UV ), cubeTexture( cube2Texture, custom2UV ), positionNode.y.add( mixNode ).clamp() );
+					const mixCubeMaps = mix( pmremTexture( cube1Texture, custom1UV ), pmremTexture( cube2Texture, custom2UV ), positionNode.y.add( mixNode ).clamp() );
 
 					const proceduralEnv = mix( mixCubeMaps, normalWorld, proceduralNode );
 
@@ -122,7 +122,7 @@
 				scene.environmentNode = getEnvironmentNode( reflectVector, positionWorld );
 
 				scene.backgroundNode = getEnvironmentNode( positionWorldDirection, positionLocal ).context( {
-					getTextureLevel: ( textureNode ) => blurNode.mul( maxMipLevel( textureNode ) )
+					getTextureLevel: () => blurNode
 				} );
 
 				// scene objects

+ 3 - 3
examples/webgpu_cubemap_mix.html

@@ -27,7 +27,7 @@
 		<script type="module">
 
 			import * as THREE from 'three';
-			import { mix, oscSine, timerLocal, cubeTexture, maxMipLevel, toneMapping } from 'three/nodes';
+			import { mix, oscSine, timerLocal, pmremTexture, float, toneMapping } from 'three/nodes';
 
 			import WebGPU from 'three/addons/capabilities/WebGPU.js';
 			import WebGL from 'three/addons/capabilities/WebGL.js';
@@ -78,10 +78,10 @@
 				cube2Texture.generateMipmaps = true;
 				cube2Texture.minFilter = THREE.LinearMipmapLinearFilter;
 
-				scene.environmentNode = mix( cubeTexture( cube2Texture ), cubeTexture( cube1Texture ), oscSine( timerLocal( .1 ) ) );
+				scene.environmentNode = mix( pmremTexture( cube2Texture ), pmremTexture( cube1Texture ), oscSine( timerLocal( .1 ) ) );
 
 				scene.backgroundNode = scene.environmentNode.context( {
-					getTextureLevel: ( textureNode ) => maxMipLevel( textureNode )
+					getTextureLevel: () => float( .5 )
 				} );
 
 				const loader = new GLTFLoader().setPath( 'models/gltf/DamagedHelmet/glTF/' );

+ 0 - 1
examples/webgpu_equirectangular.html

@@ -56,7 +56,6 @@
 				camera.position.set( 1, 0, 0 );
 
 				const equirectTexture = new THREE.TextureLoader().load( 'textures/2294472375_24a3b8ef46_o.jpg' );
-				equirectTexture.flipY = false;
 
 				scene = new THREE.Scene();
 				scene.backgroundNode = texture( equirectTexture, equirectUV(), 0 );

+ 114 - 0
examples/webgpu_pmrem_cubemap.html

@@ -0,0 +1,114 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - pmrem cubemap</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>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/",
+					"three/nodes": "./jsm/nodes/Nodes.js"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+
+			import { normalWorld, uniform, normalView, positionViewDirection, cameraViewMatrix, pmremTexture, MeshBasicNodeMaterial } from 'three/nodes';
+
+			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+
+			import { RGBMLoader } from 'three/addons/loaders/RGBMLoader.js';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			let camera, scene, renderer;
+
+			init();
+
+			async function init() {
+
+				const container = document.createElement( 'div' );
+				document.body.appendChild( container );
+
+				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.25, 20 );
+				camera.position.set( - 1.8, 0.6, 2.7 );
+
+				scene = new THREE.Scene();
+
+				const forceWebGL = false;
+
+				renderer = new WebGPURenderer( { antialias: true, forceWebGL } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.toneMapping = THREE.ACESFilmicToneMapping;
+
+				await renderer.init();
+
+				container.appendChild( renderer.domElement );
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.addEventListener( 'change', render ); // use if there is no animation loop
+				controls.minDistance = 2;
+				controls.maxDistance = 10;
+				controls.update();
+
+				new RGBMLoader()
+					.setPath( './textures/cube/pisaRGBM16/' )
+					.loadCubemap( [ 'px.png', 'nx.png', 'py.png', 'ny.png', 'pz.png', 'nz.png' ], function ( map ) {
+
+						const reflectVec = positionViewDirection.negate().reflect( normalView ).transformDirection( cameraViewMatrix );
+
+						const pmremRoughness = uniform( .5 );
+						const pmremNode = pmremTexture( map, reflectVec, pmremRoughness );
+
+						scene.backgroundNode = pmremTexture( map, normalWorld, pmremRoughness );
+
+						scene.add( new THREE.Mesh( new THREE.SphereGeometry( .5, 64, 64 ), new MeshBasicNodeMaterial( { colorNode: pmremNode } ) ) );
+
+						// gui
+
+						const gui = new GUI();
+						gui.add( pmremRoughness, 'value', 0, 1, 0.001 ).name( 'roughness' ).onChange( () => render() );
+
+						render();
+
+					} );
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+				render();
+
+			}
+
+			//
+
+			function render() {
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+
+	</body>
+</html>

+ 116 - 0
examples/webgpu_pmrem_equirectangular.html

@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - pmrem equirectangular</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>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/",
+					"three/nodes": "./jsm/nodes/Nodes.js"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+
+			import { normalWorld, uniform, normalView, positionViewDirection, cameraViewMatrix, pmremTexture, MeshBasicNodeMaterial } from 'three/nodes';
+
+			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+
+			import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			let camera, scene, renderer;
+
+			init();
+
+			async function init() {
+
+				const container = document.createElement( 'div' );
+				document.body.appendChild( container );
+
+				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.25, 20 );
+				camera.position.set( - 1.8, 0.6, 2.7 );
+
+				scene = new THREE.Scene();
+
+				const forceWebGL = false;
+
+				renderer = new WebGPURenderer( { antialias: true, forceWebGL } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				renderer.toneMapping = THREE.ACESFilmicToneMapping;
+
+				await renderer.init();
+
+				container.appendChild( renderer.domElement );
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.addEventListener( 'change', render ); // use if there is no animation loop
+				controls.minDistance = 2;
+				controls.maxDistance = 10;
+				controls.update();
+
+				new RGBELoader()
+					.setPath( 'textures/equirectangular/' )
+					.load( 'royal_esplanade_1k.hdr', function ( map ) {
+
+						map.mapping = THREE.EquirectangularReflectionMapping;
+
+						const reflectVec = positionViewDirection.negate().reflect( normalView ).transformDirection( cameraViewMatrix );
+
+						const pmremRoughness = uniform( .5 );
+						const pmremNode = pmremTexture( map, reflectVec, pmremRoughness );
+
+						scene.backgroundNode = pmremTexture( map, normalWorld, pmremRoughness );
+
+						scene.add( new THREE.Mesh( new THREE.SphereGeometry( .5, 64, 64 ), new MeshBasicNodeMaterial( { colorNode: pmremNode } ) ) );
+
+						// gui
+
+						const gui = new GUI();
+						gui.add( pmremRoughness, 'value', 0, 1, 0.001 ).name( 'roughness' ).onChange( () => render() );
+
+						render();
+
+					} );
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+				render();
+
+			}
+
+			//
+
+			function render() {
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+
+	</body>
+</html>

+ 141 - 0
examples/webgpu_pmrem_scene.html

@@ -0,0 +1,141 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - pmrem scene</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>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/",
+					"three/nodes": "./jsm/nodes/Nodes.js"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+
+			import { normalWorld, uniform, pmremTexture, MeshBasicNodeMaterial } from 'three/nodes';
+
+			import PMREMGenerator from 'three/addons/renderers/common/extras/PMREMGenerator.js';
+
+			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+
+			let camera, scene, renderer;
+
+			init();
+
+			async function init() {
+
+				const container = document.createElement( 'div' );
+				document.body.appendChild( container );
+
+				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 0.25, 20 );
+				camera.position.set( - 1.8, 0.6, 2.7 );
+
+				scene = new THREE.Scene();
+
+				const forceWebGL = false;
+
+				renderer = new WebGPURenderer( { antialias: true, forceWebGL } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				container.appendChild( renderer.domElement );
+
+				await renderer.init();
+
+				const controls = new OrbitControls( camera, renderer.domElement );
+				controls.addEventListener( 'change', render ); // use if there is no animation loop
+				controls.minDistance = 2;
+				controls.maxDistance = 10;
+				controls.update();
+
+				//
+
+				scene.background = new THREE.Color( 0x006699 );
+
+				let model;
+
+				model = new THREE.Mesh( new THREE.SphereGeometry( .2, 64, 64 ), new THREE.MeshBasicMaterial( { color: 0x0000ff } ) );
+				model.position.z -= 1;
+				scene.add( model );
+
+				model = new THREE.Mesh( new THREE.SphereGeometry( .2, 64, 64 ), new THREE.MeshBasicMaterial( { color: 0xff0000 } ) );
+				model.position.z += 1;
+				scene.add( model );
+
+				model = new THREE.Mesh( new THREE.SphereGeometry( .2, 64, 64 ), new THREE.MeshBasicMaterial( { color: 0xff00ff } ) );
+				model.position.x += 1;
+				scene.add( model );
+
+				model = new THREE.Mesh( new THREE.SphereGeometry( .2, 64, 64 ), new THREE.MeshBasicMaterial( { color: 0x00ffff } ) );
+				model.position.x -= 1;
+				scene.add( model );
+
+				model = new THREE.Mesh( new THREE.SphereGeometry( .2, 64, 64 ), new THREE.MeshBasicMaterial( { color: 0xffff00 } ) );
+				model.position.y -= 1;
+				scene.add( model );
+
+				model = new THREE.Mesh( new THREE.SphereGeometry( .2, 64, 64 ), new THREE.MeshBasicMaterial( { color: 0x00ff00 } ) );
+				model.position.y += 1;
+				scene.add( model );
+
+				//while ( scene.children.length > 0 ) scene.remove( scene.children[ 0 ] );
+
+				const sceneRT = new PMREMGenerator( renderer ).fromScene( scene );
+
+				scene.background = null;
+				scene.backgroundNode = null;
+
+				//
+
+				const pmremRoughness = uniform( .5 );
+				const pmremNode = pmremTexture( sceneRT.texture, normalWorld, pmremRoughness );
+
+				scene.add( new THREE.Mesh( new THREE.SphereGeometry( .5, 64, 64 ), new MeshBasicNodeMaterial( { colorNode: pmremNode } ) ) );
+
+				// gui
+
+				const gui = new GUI();
+				gui.add( pmremRoughness, 'value', 0, 1, 0.001 ).name( 'roughness' ).onChange( () => render() );
+
+				render();
+
+				window.addEventListener( 'resize', onWindowResize );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+				render();
+
+			}
+
+			//
+
+			function render() {
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+
+	</body>
+</html>