import { CubeReflectionMapping, CubeRefractionMapping, CubeUVReflectionMapping, LinearEncoding, LinearFilter, NoToneMapping, NoBlending, RGBAFormat, HalfFloatType, } from '../constants.js'; import { BufferAttribute } from '../core/BufferAttribute.js'; import { BufferGeometry } from '../core/BufferGeometry.js'; import { Mesh } from '../objects/Mesh.js'; import { OrthographicCamera } from '../cameras/OrthographicCamera.js'; import { PerspectiveCamera } from '../cameras/PerspectiveCamera.js'; import { RawShaderMaterial } from '../materials/RawShaderMaterial.js'; import { Vector2 } from '../math/Vector2.js'; import { Vector3 } from '../math/Vector3.js'; import { Color } from '../math/Color.js'; import { WebGLRenderTarget } from '../renderers/WebGLRenderTarget.js'; import { MeshBasicMaterial } from '../materials/MeshBasicMaterial.js'; import { BoxGeometry } from '../geometries/BoxGeometry.js'; import { BackSide } from '../constants.js'; const LOD_MIN = 4; const LOD_MAX = 8; const SIZE_MAX = Math.pow(2, LOD_MAX); // 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]; const TOTAL_LODS = LOD_MAX - LOD_MIN + 1 + EXTRA_LOD_SIGMA.length; // 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(); const { _lodPlanes, _sizeLods, _sigmas } = /*@__PURE__*/ _createPlanes(); const _clearColor = /*@__PURE__*/ new Color(); let _oldTarget = null; // 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), ]; /** * 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._blurMaterial = _getBlurShader(MAX_SAMPLES); this._equirectShader = null; this._cubemapShader = null; this._compileMaterial(this._blurMaterial); } /** * 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(); const cubeUVRenderTarget = this._allocateTargets(); 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._cubemapShader === null) { this._cubemapShader = _getCubemapShader(); this._compileMaterial(this._cubemapShader); } } /** * 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._equirectShader === null) { this._equirectShader = _getEquirectShader(); this._compileMaterial(this._equirectShader); } } /** * 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._blurMaterial.dispose(); if (this._pingPongRenderTarget !== null) this._pingPongRenderTarget.dispose(); if (this._cubemapShader !== null) this._cubemapShader.dispose(); if (this._equirectShader !== null) this._equirectShader.dispose(); for (let i = 0; i < _lodPlanes.length; i++) { _lodPlanes[i].dispose(); } } // private interface _cleanup(outputTarget) { this._renderer.setRenderTarget(_oldTarget); outputTarget.scissorTest = false; _setViewport(outputTarget, 0, 0, outputTarget.width, outputTarget.height); } _fromTexture(texture, renderTarget) { _oldTarget = this._renderer.getRenderTarget(); const cubeUVRenderTarget = renderTarget || this._allocateTargets(texture); this._textureToCubeUV(texture, cubeUVRenderTarget); this._applyPMREM(cubeUVRenderTarget); this._cleanup(cubeUVRenderTarget); return cubeUVRenderTarget; } _allocateTargets(texture) { // warning: null texture is valid const params = { magFilter: LinearFilter, minFilter: LinearFilter, generateMipmaps: false, type: HalfFloatType, format: RGBAFormat, encoding: LinearEncoding, depthBuffer: false, }; const cubeUVRenderTarget = _createRenderTarget(params); cubeUVRenderTarget.depthBuffer = texture ? false : true; if (this._pingPongRenderTarget === null) { this._pingPongRenderTarget = _createRenderTarget(params); } return cubeUVRenderTarget; } _compileMaterial(material) { const tmpMesh = new Mesh(_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); 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; } 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]); } _setViewport(cubeUVRenderTarget, col * SIZE_MAX, i > 2 ? SIZE_MAX : 0, SIZE_MAX, SIZE_MAX); renderer.setRenderTarget(cubeUVRenderTarget); if (useSolidColor) { renderer.render(backgroundBox, cubeCamera); } 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._cubemapShader === null) { this._cubemapShader = _getCubemapShader(); } this._cubemapShader.uniforms.flipEnvMap.value = texture.isRenderTargetTexture === false ? -1 : 1; } else { if (this._equirectShader === null) { this._equirectShader = _getEquirectShader(); } } const material = isCubeTexture ? this._cubemapShader : this._equirectShader; const mesh = new Mesh(_lodPlanes[0], material); const uniforms = material.uniforms; uniforms['envMap'].value = texture; if (!isCubeTexture) { uniforms['texelSize'].value.set(1.0 / texture.image.width, 1.0 / texture.image.height); } _setViewport(cubeUVRenderTarget, 0, 0, 3 * SIZE_MAX, 2 * SIZE_MAX); 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 < TOTAL_LODS; i++) { const sigma = Math.sqrt(_sigmas[i] * _sigmas[i] - _sigmas[i - 1] * _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(_lodPlanes[lodOut], blurMaterial); const blurUniforms = blurMaterial.uniforms; const pixels = _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'].value = weights; blurUniforms['latitudinal'].value = direction === 'latitudinal'; if (poleAxis) { blurUniforms['poleAxis'].value = poleAxis; } blurUniforms['dTheta'].value = radiansPerPixel; blurUniforms['mipInt'].value = LOD_MAX - lodIn; const outputSize = _sizeLods[lodOut]; const x = 3 * Math.max(0, SIZE_MAX - 2 * outputSize); const y = (lodOut === 0 ? 0 : 2 * SIZE_MAX) + 2 * outputSize * (lodOut > LOD_MAX - LOD_MIN ? lodOut - LOD_MAX + LOD_MIN : 0); _setViewport(targetOut, x, y, 3 * outputSize, 2 * outputSize); renderer.setRenderTarget(targetOut); renderer.render(blurMesh, _flatCamera); } } function _createPlanes() { const _lodPlanes = []; const _sizeLods = []; const _sigmas = []; let lod = LOD_MAX; for (let i = 0; i < TOTAL_LODS; i++) { const sizeLod = Math.pow(2, lod); _sizeLods.push(sizeLod); let sigma = 1.0 / sizeLod; if (i > LOD_MAX - LOD_MIN) { sigma = EXTRA_LOD_SIGMA[i - LOD_MAX + LOD_MIN - 1]; } else if (i === 0) { sigma = 0; } _sigmas.push(sigma); const texelSize = 1.0 / (sizeLod - 1); const min = -texelSize / 2; const max = 1 + texelSize / 2; 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]; position.set(coordinates, positionSize * vertices * face); uv.set(uv1, uvSize * vertices * face); const fill = [face, face, face, face, face, face]; faceIndex.set(fill, faceIndexSize * vertices * face); } 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(params) { const cubeUVRenderTarget = new WebGLRenderTarget(3 * SIZE_MAX, 3 * SIZE_MAX, params); cubeUVRenderTarget.texture.mapping = CubeUVReflectionMapping; cubeUVRenderTarget.texture.name = 'PMREM.cubeUv'; cubeUVRenderTarget.scissorTest = true; return cubeUVRenderTarget; } function _setViewport(target, x, y, width, height) { target.viewport.set(x, y, width, height); target.scissor.set(x, y, width, height); } function _getBlurShader(maxSamples) { const weights = new Float32Array(maxSamples); const poleAxis = new Vector3(0, 1, 0); const shaderMaterial = new RawShaderMaterial({ name: 'SphericalGaussianBlur', defines: { n: maxSamples }, uniforms: { envMap: { value: null }, samples: { value: 1 }, weights: { value: weights }, latitudinal: { value: false }, dTheta: { value: 0 }, mipInt: { value: 0 }, poleAxis: { value: poleAxis }, }, vertexShader: _getCommonVertexShader(), fragmentShader: /* glsl */ ` precision mediump float; precision mediump int; varying vec3 vOutputDirection; uniform sampler2D envMap; uniform int samples; uniform float weights[ n ]; uniform bool latitudinal; uniform float dTheta; uniform float mipInt; uniform vec3 poleAxis; #define ENVMAP_TYPE_CUBE_UV #include vec3 getSample( float theta, vec3 axis ) { float cosTheta = cos( theta ); // Rodrigues' axis-angle rotation vec3 sampleDirection = vOutputDirection * cosTheta + cross( axis, vOutputDirection ) * sin( theta ) + axis * dot( axis, vOutputDirection ) * ( 1.0 - cosTheta ); return bilinearCubeUV( envMap, sampleDirection, mipInt ); } void main() { vec3 axis = latitudinal ? poleAxis : cross( poleAxis, vOutputDirection ); if ( all( equal( axis, vec3( 0.0 ) ) ) ) { axis = vec3( vOutputDirection.z, 0.0, - vOutputDirection.x ); } axis = normalize( axis ); gl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 ); gl_FragColor.rgb += weights[ 0 ] * getSample( 0.0, axis ); for ( int i = 1; i < n; i++ ) { if ( i >= samples ) { break; } float theta = dTheta * float( i ); gl_FragColor.rgb += weights[ i ] * getSample( -1.0 * theta, axis ); gl_FragColor.rgb += weights[ i ] * getSample( theta, axis ); } } `, blending: NoBlending, depthTest: false, depthWrite: false, }); return shaderMaterial; } function _getEquirectShader() { const texelSize = new Vector2(1, 1); const shaderMaterial = new RawShaderMaterial({ name: 'EquirectangularToCubeUV', uniforms: { envMap: { value: null }, texelSize: { value: texelSize }, }, vertexShader: _getCommonVertexShader(), fragmentShader: /* glsl */ ` precision mediump float; precision mediump int; varying vec3 vOutputDirection; uniform sampler2D envMap; uniform vec2 texelSize; #include void main() { gl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 ); vec3 outputDirection = normalize( vOutputDirection ); vec2 uv = equirectUv( outputDirection ); vec2 f = fract( uv / texelSize - 0.5 ); uv -= f * texelSize; vec3 tl = texture2D ( envMap, uv ).rgb; uv.x += texelSize.x; vec3 tr = texture2D ( envMap, uv ).rgb; uv.y += texelSize.y; vec3 br = texture2D ( envMap, uv ).rgb; uv.x -= texelSize.x; vec3 bl = texture2D ( envMap, uv ).rgb; vec3 tm = mix( tl, tr, f.x ); vec3 bm = mix( bl, br, f.x ); gl_FragColor.rgb = mix( tm, bm, f.y ); } `, blending: NoBlending, depthTest: false, depthWrite: false, }); return shaderMaterial; } function _getCubemapShader() { const shaderMaterial = new RawShaderMaterial({ name: 'CubemapToCubeUV', uniforms: { envMap: { value: null }, flipEnvMap: { value: -1 }, }, vertexShader: _getCommonVertexShader(), fragmentShader: /* glsl */ ` precision mediump float; precision mediump int; uniform float flipEnvMap; varying vec3 vOutputDirection; uniform samplerCube envMap; void main() { gl_FragColor = textureCube( envMap, vec3( flipEnvMap * vOutputDirection.x, vOutputDirection.yz ) ); } `, blending: NoBlending, depthTest: false, depthWrite: false, }); return shaderMaterial; } function _getCommonVertexShader() { return /* glsl */ ` precision mediump float; precision mediump int; attribute vec3 position; attribute vec2 uv; attribute float faceIndex; varying vec3 vOutputDirection; // RH coordinate system; PMREM face-indexing convention vec3 getDirection( vec2 uv, float face ) { uv = 2.0 * uv - 1.0; vec3 direction = vec3( uv, 1.0 ); if ( face == 0.0 ) { direction = direction.zyx; // ( 1, v, u ) pos x } else if ( face == 1.0 ) { direction = direction.xzy; direction.xz *= -1.0; // ( -u, 1, -v ) pos y } else if ( face == 2.0 ) { direction.x *= -1.0; // ( -u, v, 1 ) pos z } else if ( face == 3.0 ) { direction = direction.zyx; direction.xz *= -1.0; // ( -1, v, -u ) neg x } else if ( face == 4.0 ) { direction = direction.xzy; direction.xy *= -1.0; // ( -u, -1, v ) neg y } else if ( face == 5.0 ) { direction.z *= -1.0; // ( u, v, -1 ) neg z } return direction; } void main() { vOutputDirection = getDirection( uv, faceIndex ); gl_Position = vec4( position, 1.0 ); } `; } export { PMREMGenerator };