11import NodeMaterial from '../../../materials/nodes/NodeMaterial.js' ;
2- import { getDirection , blur } from '../../../nodes/pmrem/PMREMUtils.js' ;
2+ import { getDirection , blur , ggxConvolution } from '../../../nodes/pmrem/PMREMUtils.js' ;
33import { equirectUV } from '../../../nodes/utils/EquirectUV.js' ;
44import { uniform } from '../../../nodes/core/UniformNode.js' ;
55import { uniformArray } from '../../../nodes/accessors/UniformArrayNode.js' ;
66import { texture } from '../../../nodes/accessors/TextureNode.js' ;
77import { cubeTexture } from '../../../nodes/accessors/CubeTextureNode.js' ;
8- import { float , vec3 } from '../../../nodes/tsl/TSLBase.js' ;
8+ import { float , uint , vec3 } from '../../../nodes/tsl/TSLBase.js' ;
99import { uv } from '../../../nodes/accessors/UV.js' ;
1010import { attribute } from '../../../nodes/core/AttributeNode.js' ;
1111
@@ -34,42 +34,25 @@ import { warn, error, warnOnce } from '../../../utils.js';
3434
3535const LOD_MIN = 4 ;
3636
37- // The standard deviations (radians) associated with the extra mips. These are
38- // chosen to approximate a Trowbridge-Reitz distribution function times the
39- // geometric shadowing function. These sigma values squared must match the
40- // variance #defines in cube_uv_reflection_fragment.glsl.js.
37+ // The standard deviations (radians) associated with the extra mips.
38+ // Used for scene blur in fromScene() method.
4139const EXTRA_LOD_SIGMA = [ 0.125 , 0.215 , 0.35 , 0.446 , 0.526 , 0.582 ] ;
4240
4341// The maximum length of the blur for loop. Smaller sigmas will use fewer
4442// samples and exit early, but not recompile the shader.
43+ // Used for scene blur in fromScene() method.
4544const MAX_SAMPLES = 20 ;
4645
46+ // GGX VNDF importance sampling configuration
47+ const GGX_SAMPLES = 1024 ;
48+
4749const _flatCamera = /*@__PURE__ */ new OrthographicCamera ( - 1 , 1 , 1 , - 1 , 0 , 1 ) ;
4850const _cubeCamera = /*@__PURE__ */ new PerspectiveCamera ( 90 , 1 ) ;
4951const _clearColor = /*@__PURE__ */ new Color ( ) ;
5052let _oldTarget = null ;
5153let _oldActiveCubeFace = 0 ;
5254let _oldActiveMipmapLevel = 0 ;
5355
54- // Golden Ratio
55- const PHI = ( 1 + Math . sqrt ( 5 ) ) / 2 ;
56- const INV_PHI = 1 / PHI ;
57-
58- // Vertices of a dodecahedron (except the opposites, which represent the
59- // same axis), used as axis directions evenly spread on a sphere.
60- const _axisDirections = [
61- /*@__PURE__ */ new Vector3 ( - PHI , INV_PHI , 0 ) ,
62- /*@__PURE__ */ new Vector3 ( PHI , INV_PHI , 0 ) ,
63- /*@__PURE__ */ new Vector3 ( - INV_PHI , 0 , PHI ) ,
64- /*@__PURE__ */ new Vector3 ( INV_PHI , 0 , PHI ) ,
65- /*@__PURE__ */ new Vector3 ( 0 , PHI , - INV_PHI ) ,
66- /*@__PURE__ */ new Vector3 ( 0 , PHI , INV_PHI ) ,
67- /*@__PURE__ */ new Vector3 ( - 1 , 1 , - 1 ) ,
68- /*@__PURE__ */ new Vector3 ( 1 , 1 , - 1 ) ,
69- /*@__PURE__ */ new Vector3 ( - 1 , 1 , 1 ) ,
70- /*@__PURE__ */ new Vector3 ( 1 , 1 , 1 )
71- ] ;
72-
7356const _origin = /*@__PURE__ */ new Vector3 ( ) ;
7457
7558// maps blur materials to their uniforms dictionary
@@ -96,9 +79,11 @@ const _outputDirection = /*@__PURE__*/ vec3( _direction.x, _direction.y, _direct
9679 * higher roughness levels. In this way we maintain resolution to smoothly
9780 * interpolate diffuse lighting while limiting sampling computation.
9881 *
99- * Paper: Fast, Accurate Image-Based Lighting:
100- * {@link https://drive.google.com/file/d/15y8r_UpKlU9SvV4ILb0C3qCPecS8pvLz/view}
101- */
82+ * The prefiltering uses GGX VNDF (Visible Normal Distribution Function)
83+ * importance sampling based on "Sampling the GGX Distribution of Visible Normals"
84+ * (Heitz, 2018) to generate environment maps that accurately match the GGX BRDF
85+ * used in material rendering for physically-based image-based lighting.
86+ */
10287class PMREMGenerator {
10388
10489 /**
@@ -119,6 +104,7 @@ class PMREMGenerator {
119104 this . _lodMeshes = [ ] ;
120105
121106 this . _blurMaterial = null ;
107+ this . _ggxMaterial = null ;
122108 this . _cubemapMaterial = null ;
123109 this . _equirectMaterial = null ;
124110 this . _backgroundBox = null ;
@@ -408,6 +394,7 @@ class PMREMGenerator {
408394 _dispose ( ) {
409395
410396 if ( this . _blurMaterial !== null ) this . _blurMaterial . dispose ( ) ;
397+ if ( this . _ggxMaterial !== null ) this . _ggxMaterial . dispose ( ) ;
411398
412399 if ( this . _pingPongRenderTarget !== null ) this . _pingPongRenderTarget . dispose ( ) ;
413400
@@ -632,17 +619,80 @@ class PMREMGenerator {
632619 renderer . autoClear = false ;
633620 const n = this . _lodPlanes . length ;
634621
622+ // Use GGX VNDF importance sampling
635623 for ( let i = 1 ; i < n ; i ++ ) {
636624
637- const sigma = Math . sqrt ( this . _sigmas [ i ] * this . _sigmas [ i ] - this . _sigmas [ i - 1 ] * this . _sigmas [ i - 1 ] ) ;
625+ this . _applyGGXFilter ( cubeUVRenderTarget , i - 1 , i ) ;
626+
627+ }
628+
629+ renderer . autoClear = autoClear ;
630+
631+ }
632+
633+ /**
634+ * Applies GGX VNDF importance sampling filter to generate a prefiltered environment map.
635+ * Uses Monte Carlo integration with VNDF importance sampling to accurately represent the
636+ * GGX BRDF for physically-based rendering. Reads from the previous LOD level and
637+ * applies incremental roughness filtering to avoid over-blurring.
638+ *
639+ * @private
640+ * @param {RenderTarget } cubeUVRenderTarget
641+ * @param {number } lodIn - Source LOD level to read from
642+ * @param {number } lodOut - Target LOD level to write to
643+ */
644+ _applyGGXFilter ( cubeUVRenderTarget , lodIn , lodOut ) {
645+
646+ const renderer = this . _renderer ;
647+ const pingPongRenderTarget = this . _pingPongRenderTarget ;
638648
639- const poleAxis = _axisDirections [ ( n - i - 1 ) % _axisDirections . length ] ;
649+ // Lazy create GGX material only when first used
650+ if ( this . _ggxMaterial === null ) {
640651
641- this . _blur ( cubeUVRenderTarget , i - 1 , i , sigma , poleAxis ) ;
652+ this . _ggxMaterial = _getGGXShader ( this . _lodMax , this . _pingPongRenderTarget . width , this . _pingPongRenderTarget . height ) ;
642653
643654 }
644655
645- renderer . autoClear = autoClear ;
656+ const ggxMaterial = this . _ggxMaterial ;
657+ const ggxMesh = this . _lodMeshes [ lodOut ] ;
658+ ggxMesh . material = ggxMaterial ;
659+
660+ const ggxUniforms = _uniformsMap . get ( ggxMaterial ) ;
661+
662+ // Calculate incremental roughness between LOD levels
663+ const targetRoughness = lodOut / ( this . _lodPlanes . length - 1 ) ;
664+ const sourceRoughness = lodIn / ( this . _lodPlanes . length - 1 ) ;
665+ const incrementalRoughness = Math . sqrt ( targetRoughness * targetRoughness - sourceRoughness * sourceRoughness ) ;
666+
667+ // Apply blur strength mapping for better quality across the roughness range
668+ const blurStrength = 0.05 + targetRoughness * 0.95 ;
669+ const adjustedRoughness = incrementalRoughness * blurStrength ;
670+
671+ // Calculate viewport position based on output LOD level
672+ const { _lodMax } = this ;
673+ const outputSize = this . _sizeLods [ lodOut ] ;
674+ const x = 3 * outputSize * ( lodOut > _lodMax - LOD_MIN ? lodOut - _lodMax + LOD_MIN : 0 ) ;
675+ const y = 4 * ( this . _cubeSize - outputSize ) ;
676+
677+ // Read from previous LOD with incremental roughness
678+ cubeUVRenderTarget . texture . frame = ( cubeUVRenderTarget . texture . frame || 0 ) + 1 ;
679+ ggxUniforms . envMap . value = cubeUVRenderTarget . texture ;
680+ ggxUniforms . roughness . value = adjustedRoughness ;
681+ ggxUniforms . mipInt . value = _lodMax - lodIn ; // Sample from input LOD
682+
683+ _setViewport ( pingPongRenderTarget , x , y , 3 * outputSize , 2 * outputSize ) ;
684+ renderer . setRenderTarget ( pingPongRenderTarget ) ;
685+ renderer . render ( ggxMesh , _flatCamera ) ;
686+
687+ // Copy from pingPong back to cubeUV (simple direct copy)
688+ pingPongRenderTarget . texture . frame = ( pingPongRenderTarget . texture . frame || 0 ) + 1 ;
689+ ggxUniforms . envMap . value = pingPongRenderTarget . texture ;
690+ ggxUniforms . roughness . value = 0.0 ; // Direct copy
691+ ggxUniforms . mipInt . value = _lodMax - lodOut ; // Read from the level we just wrote
692+
693+ _setViewport ( cubeUVRenderTarget , x , y , 3 * outputSize , 2 * outputSize ) ;
694+ renderer . setRenderTarget ( cubeUVRenderTarget ) ;
695+ renderer . render ( ggxMesh , _flatCamera ) ;
646696
647697 }
648698
@@ -653,6 +703,8 @@ class PMREMGenerator {
653703 * the poles) to approximate the orthogonally-separable blur. It is least
654704 * accurate at the poles, but still does a decent job.
655705 *
706+ * Used for initial scene blur in fromScene() method when sigma > 0.
707+ *
656708 * @private
657709 * @param {RenderTarget } cubeUVRenderTarget - The cubemap render target.
658710 * @param {number } lodIn - The input level-of-detail.
@@ -904,7 +956,7 @@ function _getBlurShader( lodMax, width, height ) {
904956 const n = float ( MAX_SAMPLES ) ;
905957 const latitudinal = uniform ( 0 ) ; // false, bool
906958 const samples = uniform ( 1 ) ; // int
907- const envMap = texture ( null ) ;
959+ const envMap = texture ( ) ;
908960 const mipInt = uniform ( 0 ) ; // int
909961 const CUBEUV_TEXEL_WIDTH = float ( 1 / width ) ;
910962 const CUBEUV_TEXEL_HEIGHT = float ( 1 / height ) ;
@@ -934,6 +986,37 @@ function _getBlurShader( lodMax, width, height ) {
934986
935987}
936988
989+ function _getGGXShader ( lodMax , width , height ) {
990+
991+ const envMap = texture ( ) ;
992+ const roughness = uniform ( 0 ) ;
993+ const mipInt = uniform ( 0 ) ;
994+ const CUBEUV_TEXEL_WIDTH = float ( 1 / width ) ;
995+ const CUBEUV_TEXEL_HEIGHT = float ( 1 / height ) ;
996+ const CUBEUV_MAX_MIP = float ( lodMax ) ;
997+
998+ const materialUniforms = {
999+ envMap,
1000+ roughness,
1001+ mipInt,
1002+ CUBEUV_TEXEL_WIDTH ,
1003+ CUBEUV_TEXEL_HEIGHT ,
1004+ CUBEUV_MAX_MIP
1005+ } ;
1006+
1007+ const material = _getMaterial ( 'ggx' ) ;
1008+ material . fragmentNode = ggxConvolution ( {
1009+ ...materialUniforms ,
1010+ N_immutable : _outputDirection ,
1011+ GGX_SAMPLES : uint ( GGX_SAMPLES )
1012+ } ) ;
1013+
1014+ _uniformsMap . set ( material , materialUniforms ) ;
1015+
1016+ return material ;
1017+
1018+ }
1019+
9371020function _getCubemapMaterial ( envTexture ) {
9381021
9391022 const material = _getMaterial ( 'cubemap' ) ;
0 commit comments