|
|
@@ -1,11 +1,11 @@
|
|
|
import NodeMaterial from '../../../materials/nodes/NodeMaterial.js';
|
|
|
-import { getDirection, blur } from '../../../nodes/pmrem/PMREMUtils.js';
|
|
|
+import { getDirection, blur, ggxConvolution } from '../../../nodes/pmrem/PMREMUtils.js';
|
|
|
import { equirectUV } from '../../../nodes/utils/EquirectUV.js';
|
|
|
import { uniform } from '../../../nodes/core/UniformNode.js';
|
|
|
import { uniformArray } from '../../../nodes/accessors/UniformArrayNode.js';
|
|
|
import { texture } from '../../../nodes/accessors/TextureNode.js';
|
|
|
import { cubeTexture } from '../../../nodes/accessors/CubeTextureNode.js';
|
|
|
-import { float, vec3 } from '../../../nodes/tsl/TSLBase.js';
|
|
|
+import { float, uint, vec3 } from '../../../nodes/tsl/TSLBase.js';
|
|
|
import { uv } from '../../../nodes/accessors/UV.js';
|
|
|
import { attribute } from '../../../nodes/core/AttributeNode.js';
|
|
|
|
|
|
@@ -34,16 +34,18 @@ import { warn, error, warnOnce } from '../../../utils.js';
|
|
|
|
|
|
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.
|
|
|
+// The standard deviations (radians) associated with the extra mips.
|
|
|
+// Used for scene blur in fromScene() method.
|
|
|
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.
|
|
|
+// Used for scene blur in fromScene() method.
|
|
|
const MAX_SAMPLES = 20;
|
|
|
|
|
|
+// GGX VNDF importance sampling configuration
|
|
|
+const GGX_SAMPLES = 1024;
|
|
|
+
|
|
|
const _flatCamera = /*@__PURE__*/ new OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
|
|
|
const _cubeCamera = /*@__PURE__*/ new PerspectiveCamera( 90, 1 );
|
|
|
const _clearColor = /*@__PURE__*/ new Color();
|
|
|
@@ -51,25 +53,6 @@ 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( - PHI, INV_PHI, 0 ),
|
|
|
- /*@__PURE__*/ new Vector3( PHI, INV_PHI, 0 ),
|
|
|
- /*@__PURE__*/ new Vector3( - INV_PHI, 0, PHI ),
|
|
|
- /*@__PURE__*/ new Vector3( INV_PHI, 0, PHI ),
|
|
|
- /*@__PURE__*/ new Vector3( 0, PHI, - INV_PHI ),
|
|
|
- /*@__PURE__*/ new Vector3( 0, PHI, INV_PHI ),
|
|
|
- /*@__PURE__*/ new Vector3( - 1, 1, - 1 ),
|
|
|
- /*@__PURE__*/ new Vector3( 1, 1, - 1 ),
|
|
|
- /*@__PURE__*/ new Vector3( - 1, 1, 1 ),
|
|
|
- /*@__PURE__*/ new Vector3( 1, 1, 1 )
|
|
|
-];
|
|
|
-
|
|
|
const _origin = /*@__PURE__*/ new Vector3();
|
|
|
|
|
|
// maps blur materials to their uniforms dictionary
|
|
|
@@ -96,9 +79,11 @@ const _outputDirection = /*@__PURE__*/ vec3( _direction.x, _direction.y, _direct
|
|
|
* higher roughness levels. In this way we maintain resolution to smoothly
|
|
|
* interpolate diffuse lighting while limiting sampling computation.
|
|
|
*
|
|
|
- * Paper: Fast, Accurate Image-Based Lighting:
|
|
|
- * {@link https://drive.google.com/file/d/15y8r_UpKlU9SvV4ILb0C3qCPecS8pvLz/view}
|
|
|
-*/
|
|
|
+ * The prefiltering uses GGX VNDF (Visible Normal Distribution Function)
|
|
|
+ * importance sampling based on "Sampling the GGX Distribution of Visible Normals"
|
|
|
+ * (Heitz, 2018) to generate environment maps that accurately match the GGX BRDF
|
|
|
+ * used in material rendering for physically-based image-based lighting.
|
|
|
+ */
|
|
|
class PMREMGenerator {
|
|
|
|
|
|
/**
|
|
|
@@ -119,6 +104,7 @@ class PMREMGenerator {
|
|
|
this._lodMeshes = [];
|
|
|
|
|
|
this._blurMaterial = null;
|
|
|
+ this._ggxMaterial = null;
|
|
|
this._cubemapMaterial = null;
|
|
|
this._equirectMaterial = null;
|
|
|
this._backgroundBox = null;
|
|
|
@@ -408,6 +394,7 @@ class PMREMGenerator {
|
|
|
_dispose() {
|
|
|
|
|
|
if ( this._blurMaterial !== null ) this._blurMaterial.dispose();
|
|
|
+ if ( this._ggxMaterial !== null ) this._ggxMaterial.dispose();
|
|
|
|
|
|
if ( this._pingPongRenderTarget !== null ) this._pingPongRenderTarget.dispose();
|
|
|
|
|
|
@@ -632,17 +619,80 @@ class PMREMGenerator {
|
|
|
renderer.autoClear = false;
|
|
|
const n = this._lodPlanes.length;
|
|
|
|
|
|
+ // Use GGX VNDF importance sampling
|
|
|
for ( let i = 1; i < n; i ++ ) {
|
|
|
|
|
|
- const sigma = Math.sqrt( this._sigmas[ i ] * this._sigmas[ i ] - this._sigmas[ i - 1 ] * this._sigmas[ i - 1 ] );
|
|
|
+ this._applyGGXFilter( cubeUVRenderTarget, i - 1, i );
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ renderer.autoClear = autoClear;
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Applies GGX VNDF importance sampling filter to generate a prefiltered environment map.
|
|
|
+ * Uses Monte Carlo integration with VNDF importance sampling to accurately represent the
|
|
|
+ * GGX BRDF for physically-based rendering. Reads from the previous LOD level and
|
|
|
+ * applies incremental roughness filtering to avoid over-blurring.
|
|
|
+ *
|
|
|
+ * @private
|
|
|
+ * @param {RenderTarget} cubeUVRenderTarget
|
|
|
+ * @param {number} lodIn - Source LOD level to read from
|
|
|
+ * @param {number} lodOut - Target LOD level to write to
|
|
|
+ */
|
|
|
+ _applyGGXFilter( cubeUVRenderTarget, lodIn, lodOut ) {
|
|
|
+
|
|
|
+ const renderer = this._renderer;
|
|
|
+ const pingPongRenderTarget = this._pingPongRenderTarget;
|
|
|
|
|
|
- const poleAxis = _axisDirections[ ( n - i - 1 ) % _axisDirections.length ];
|
|
|
+ // Lazy create GGX material only when first used
|
|
|
+ if ( this._ggxMaterial === null ) {
|
|
|
|
|
|
- this._blur( cubeUVRenderTarget, i - 1, i, sigma, poleAxis );
|
|
|
+ this._ggxMaterial = _getGGXShader( this._lodMax, this._pingPongRenderTarget.width, this._pingPongRenderTarget.height );
|
|
|
|
|
|
}
|
|
|
|
|
|
- renderer.autoClear = autoClear;
|
|
|
+ const ggxMaterial = this._ggxMaterial;
|
|
|
+ const ggxMesh = this._lodMeshes[ lodOut ];
|
|
|
+ ggxMesh.material = ggxMaterial;
|
|
|
+
|
|
|
+ const ggxUniforms = _uniformsMap.get( ggxMaterial );
|
|
|
+
|
|
|
+ // Calculate incremental roughness between LOD levels
|
|
|
+ const targetRoughness = lodOut / ( this._lodPlanes.length - 1 );
|
|
|
+ const sourceRoughness = lodIn / ( this._lodPlanes.length - 1 );
|
|
|
+ const incrementalRoughness = Math.sqrt( targetRoughness * targetRoughness - sourceRoughness * sourceRoughness );
|
|
|
+
|
|
|
+ // Apply blur strength mapping for better quality across the roughness range
|
|
|
+ const blurStrength = 0.05 + targetRoughness * 0.95;
|
|
|
+ const adjustedRoughness = incrementalRoughness * blurStrength;
|
|
|
+
|
|
|
+ // Calculate viewport position based on output LOD level
|
|
|
+ const { _lodMax } = this;
|
|
|
+ const outputSize = this._sizeLods[ lodOut ];
|
|
|
+ const x = 3 * outputSize * ( lodOut > _lodMax - LOD_MIN ? lodOut - _lodMax + LOD_MIN : 0 );
|
|
|
+ const y = 4 * ( this._cubeSize - outputSize );
|
|
|
+
|
|
|
+ // Read from previous LOD with incremental roughness
|
|
|
+ cubeUVRenderTarget.texture.frame = ( cubeUVRenderTarget.texture.frame || 0 ) + 1;
|
|
|
+ ggxUniforms.envMap.value = cubeUVRenderTarget.texture;
|
|
|
+ ggxUniforms.roughness.value = adjustedRoughness;
|
|
|
+ ggxUniforms.mipInt.value = _lodMax - lodIn; // Sample from input LOD
|
|
|
+
|
|
|
+ _setViewport( pingPongRenderTarget, x, y, 3 * outputSize, 2 * outputSize );
|
|
|
+ renderer.setRenderTarget( pingPongRenderTarget );
|
|
|
+ renderer.render( ggxMesh, _flatCamera );
|
|
|
+
|
|
|
+ // Copy from pingPong back to cubeUV (simple direct copy)
|
|
|
+ pingPongRenderTarget.texture.frame = ( pingPongRenderTarget.texture.frame || 0 ) + 1;
|
|
|
+ ggxUniforms.envMap.value = pingPongRenderTarget.texture;
|
|
|
+ ggxUniforms.roughness.value = 0.0; // Direct copy
|
|
|
+ ggxUniforms.mipInt.value = _lodMax - lodOut; // Read from the level we just wrote
|
|
|
+
|
|
|
+ _setViewport( cubeUVRenderTarget, x, y, 3 * outputSize, 2 * outputSize );
|
|
|
+ renderer.setRenderTarget( cubeUVRenderTarget );
|
|
|
+ renderer.render( ggxMesh, _flatCamera );
|
|
|
|
|
|
}
|
|
|
|
|
|
@@ -653,6 +703,8 @@ class PMREMGenerator {
|
|
|
* the poles) to approximate the orthogonally-separable blur. It is least
|
|
|
* accurate at the poles, but still does a decent job.
|
|
|
*
|
|
|
+ * Used for initial scene blur in fromScene() method when sigma > 0.
|
|
|
+ *
|
|
|
* @private
|
|
|
* @param {RenderTarget} cubeUVRenderTarget - The cubemap render target.
|
|
|
* @param {number} lodIn - The input level-of-detail.
|
|
|
@@ -904,7 +956,7 @@ function _getBlurShader( lodMax, width, height ) {
|
|
|
const n = float( MAX_SAMPLES );
|
|
|
const latitudinal = uniform( 0 ); // false, bool
|
|
|
const samples = uniform( 1 ); // int
|
|
|
- const envMap = texture( null );
|
|
|
+ const envMap = texture();
|
|
|
const mipInt = uniform( 0 ); // int
|
|
|
const CUBEUV_TEXEL_WIDTH = float( 1 / width );
|
|
|
const CUBEUV_TEXEL_HEIGHT = float( 1 / height );
|
|
|
@@ -934,6 +986,37 @@ function _getBlurShader( lodMax, width, height ) {
|
|
|
|
|
|
}
|
|
|
|
|
|
+function _getGGXShader( lodMax, width, height ) {
|
|
|
+
|
|
|
+ const envMap = texture();
|
|
|
+ const roughness = uniform( 0 );
|
|
|
+ const mipInt = uniform( 0 );
|
|
|
+ const CUBEUV_TEXEL_WIDTH = float( 1 / width );
|
|
|
+ const CUBEUV_TEXEL_HEIGHT = float( 1 / height );
|
|
|
+ const CUBEUV_MAX_MIP = float( lodMax );
|
|
|
+
|
|
|
+ const materialUniforms = {
|
|
|
+ envMap,
|
|
|
+ roughness,
|
|
|
+ mipInt,
|
|
|
+ CUBEUV_TEXEL_WIDTH,
|
|
|
+ CUBEUV_TEXEL_HEIGHT,
|
|
|
+ CUBEUV_MAX_MIP
|
|
|
+ };
|
|
|
+
|
|
|
+ const material = _getMaterial( 'ggx' );
|
|
|
+ material.fragmentNode = ggxConvolution( {
|
|
|
+ ...materialUniforms,
|
|
|
+ N_immutable: _outputDirection,
|
|
|
+ GGX_SAMPLES: uint( GGX_SAMPLES )
|
|
|
+ } );
|
|
|
+
|
|
|
+ _uniformsMap.set( material, materialUniforms );
|
|
|
+
|
|
|
+ return material;
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
function _getCubemapMaterial( envTexture ) {
|
|
|
|
|
|
const material = _getMaterial( 'cubemap' );
|