diff --git a/examples/webgpu_cubemap_adjustments.html b/examples/webgpu_cubemap_adjustments.html index 7b7d0b579a12c7..414aee699a3738 100644 --- a/examples/webgpu_cubemap_adjustments.html +++ b/examples/webgpu_cubemap_adjustments.html @@ -72,6 +72,12 @@ cube2Texture.generateMipmaps = true; cube2Texture.minFilter = THREE.LinearMipmapLinearFilter; + renderer = new THREE.WebGPURenderer( { antialias: true } ); + await renderer.init(); + + const cube2Blur = new THREE.CubeRenderTarget(cube2Texture.source.data[0].width); + cube2Blur.fromCubeTexture(renderer, cube2Texture, 0.1); + // nodes and environment const adjustments = { @@ -95,7 +101,7 @@ const custom1UV = reflectNode.xyz.mul( uniform( rotateY1Matrix ) ); const custom2UV = reflectNode.xyz.mul( uniform( rotateY2Matrix ) ); - const mixCubeMaps = mix( pmremTexture( cube1Texture, custom1UV ), pmremTexture( cube2Texture, custom2UV ), positionNode.y.add( mixNode ).clamp() ); + const mixCubeMaps = mix( pmremTexture( cube1Texture, custom1UV ), pmremTexture( cube2Blur.texture, custom2UV ), positionNode.y.add( mixNode ).clamp() ); const proceduralEnv = mix( mixCubeMaps, normalWorld, proceduralNode ); @@ -133,7 +139,6 @@ // renderer and controls - renderer = new THREE.WebGPURenderer( { antialias: true } ); renderer.setPixelRatio( window.devicePixelRatio ); renderer.setSize( window.innerWidth, window.innerHeight ); renderer.toneMapping = THREE.LinearToneMapping; diff --git a/src/Three.WebGPU.js b/src/Three.WebGPU.js index 4289377b368d9a..7c641a4642955e 100644 --- a/src/Three.WebGPU.js +++ b/src/Three.WebGPU.js @@ -6,6 +6,7 @@ export { default as Lighting } from './renderers/common/Lighting.js'; export { default as BundleGroup } from './renderers/common/BundleGroup.js'; export { default as QuadMesh } from './renderers/common/QuadMesh.js'; export { default as PMREMGenerator } from './renderers/common/extras/PMREMGenerator.js'; +export { default as CubeRenderTarget } from './renderers/common/CubeRenderTarget.js'; export { default as PostProcessing } from './renderers/common/PostProcessing.js'; import * as PostProcessingUtils from './renderers/common/PostProcessingUtils.js'; export { PostProcessingUtils }; diff --git a/src/nodes/pmrem/PMREMUtils.js b/src/nodes/pmrem/PMREMUtils.js index 3fa2c47d75bad3..4b0df995f33cea 100644 --- a/src/nodes/pmrem/PMREMUtils.js +++ b/src/nodes/pmrem/PMREMUtils.js @@ -214,7 +214,7 @@ export const textureCubeUV = /*@__PURE__*/ Fn( ( [ envMap, sampleDir_immutable, } ); -const bilinearCubeUV = /*@__PURE__*/ Fn( ( [ envMap, direction_immutable, mipInt_immutable, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP ] ) => { +export const bilinearCubeUV = /*@__PURE__*/ Fn( ( [ 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 ); @@ -241,7 +241,7 @@ const bilinearCubeUV = /*@__PURE__*/ Fn( ( [ envMap, direction_immutable, mipInt } ); -const getSample = /*@__PURE__*/ Fn( ( { envMap, mipInt, outputDirection, theta, axis, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP } ) => { +const getSample = /*@__PURE__*/ Fn( ( { outputDirection, theta, axis, sampler } ) => { const cosTheta = cos( theta ); @@ -250,11 +250,11 @@ const getSample = /*@__PURE__*/ Fn( ( { envMap, mipInt, outputDirection, theta, .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 ); + return sampler( sampleDirection ); } ); -export const blur = /*@__PURE__*/ Fn( ( { n, latitudinal, poleAxis, outputDirection, weights, samples, dTheta, mipInt, envMap, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP } ) => { +export const blur = /*@__PURE__*/ Fn( ( { n, latitudinal, poleAxis, outputDirection, weights, samples, dTheta, sampler } ) => { const axis = vec3( select( latitudinal, poleAxis, cross( poleAxis, outputDirection ) ) ).toVar(); @@ -267,7 +267,7 @@ export const blur = /*@__PURE__*/ Fn( ( { n, latitudinal, poleAxis, outputDirect 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 } ) ) ); + gl_FragColor.addAssign( weights.element( int( 0 ) ).mul( getSample( { theta: 0.0, axis, outputDirection, sampler } ) ) ); Loop( { start: int( 1 ), end: n }, ( { i } ) => { @@ -278,11 +278,59 @@ export const blur = /*@__PURE__*/ Fn( ( { n, latitudinal, poleAxis, outputDirect } ); 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 } ) ) ); + gl_FragColor.addAssign( weights.element( i ).mul( getSample( { theta: theta.mul( - 1.0 ), axis, outputDirection, sampler } ) ) ); + gl_FragColor.addAssign( weights.element( i ).mul( getSample( { theta, axis, outputDirection, sampler } ) ) ); } ); return vec4( gl_FragColor, 1 ); } ); + +export const getBlurParams = ( sigmaRadians, cubeRes, maxSamples )=>{ + + // Number of standard deviations at which to cut off the discrete approximation. + const STANDARD_DEVIATIONS = 3; + + const radiansPerPixel = Math.PI / ( 2 * cubeRes ); + const sigmaPixels = sigmaRadians / radiansPerPixel; + const samples = 1 + Math.floor( STANDARD_DEVIATIONS * sigmaPixels ); + + if ( samples > maxSamples ) { + + console.warn( `sigmaRadians, ${ + sigmaRadians}, is too large and will clip, as it requested ${ + samples} samples when the maximum is set to ${maxSamples}` ); + + } + + const weights = new Array( maxSamples ).fill( 0 ); + let sum = 0; + + for ( let i = 0; i < samples; ++ i ) { + + const x = i / sigmaPixels; + const weight = Math.exp( - x * x / 2 ); + weights[ i ] = weight; + + if ( i === 0 ) { + + sum += weight; + + } else { + + sum += 2 * weight; + + } + + } + + for ( let i = 0; i < weights.length; i ++ ) { + + weights[ i ] = weights[ i ] / sum; + + } + + return { radiansPerPixel, samples, weights }; + +}; diff --git a/src/renderers/common/CubeRenderTarget.js b/src/renderers/common/CubeRenderTarget.js index 29bd3a6fbc7822..8beea5136d8a2a 100644 --- a/src/renderers/common/CubeRenderTarget.js +++ b/src/renderers/common/CubeRenderTarget.js @@ -2,6 +2,10 @@ import { equirectUV } from '../../nodes/utils/EquirectUVNode.js'; import { texture as TSL_Texture } from '../../nodes/accessors/TextureNode.js'; import { positionWorldDirection } from '../../nodes/accessors/Position.js'; import NodeMaterial from '../../materials/nodes/NodeMaterial.js'; +import { blur, getBlurParams } from '../../nodes/pmrem/PMREMUtils.js'; +import { uniform } from '../../nodes/core/UniformNode.js'; +import { uniformArray } from '../../nodes/accessors/UniformArrayNode.js'; +import { float, vec3, Fn } from '../../nodes/tsl/TSLBase.js'; import { WebGLCubeRenderTarget } from '../../renderers/WebGLCubeRenderTarget.js'; import { Scene } from '../../scenes/Scene.js'; @@ -9,6 +13,8 @@ import { CubeCamera } from '../../cameras/CubeCamera.js'; import { BoxGeometry } from '../../geometries/BoxGeometry.js'; import { Mesh } from '../../objects/Mesh.js'; import { BackSide, NoBlending, LinearFilter, LinearMipmapLinearFilter } from '../../constants.js'; +import { cubeTexture as TSL_CubeTexture } from '../../nodes/accessors/CubeTextureNode.js'; +import { Vector3 } from '../../math/Vector3.js'; // @TODO: Consider rename WebGLCubeRenderTarget to just CubeRenderTarget @@ -72,6 +78,91 @@ class CubeRenderTarget extends WebGLCubeRenderTarget { } + fromCubeTexture( renderer, cubeTex, sigmaRadians = 0, poleAxis = new Vector3( 0, 1, 0 ) ) { + + const currentGenerateMipmaps = cubeTex.generateMipmaps; + + cubeTex.generateMipmaps = true; + + this.texture.type = cubeTex.type; + this.texture.colorSpace = cubeTex.colorSpace; + + this.texture.generateMipmaps = cubeTex.generateMipmaps; + this.texture.minFilter = cubeTex.minFilter; + this.texture.magFilter = cubeTex.magFilter; + + // 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 blurMaterial = new NodeMaterial(); + blurMaterial.side = BackSide; + blurMaterial.depthTest = false; + blurMaterial.depthWrite = false; + blurMaterial.blending = NoBlending; + + const weights = uniformArray( new Array( MAX_SAMPLES ).fill( 0 ) ); + const dTheta = uniform( 0 ); + const n = float( MAX_SAMPLES ); + const latitudinal = uniform( 0 ); // false, bool + const samples = uniform( 1 ); // int + const envMap = TSL_CubeTexture( null ); + + const cubeSampler = Fn( ( [ sampleDirection ] )=>{ + + return envMap.sample( sampleDirection ); + + } ); + blurMaterial.fragmentNode = blur( { n, latitudinal: latitudinal.equal( 1 ), poleAxis: vec3( poleAxis ), outputDirection: positionWorldDirection, weights, samples, dTheta, sampler: cubeSampler } ); + + const geometry = new BoxGeometry( 5, 5, 5 ); + const mesh = new Mesh( geometry, blurMaterial ); + + const scene = new Scene(); + scene.add( mesh ); + + const camera = new CubeCamera( 1, 10, this ); + + const width = cubeTex.source.data[0].width; + + envMap.value = cubeTex; + latitudinal.value = 1; + const blurParams1 = getBlurParams( sigmaRadians, width, MAX_SAMPLES ); + weights.value = blurParams1.weights; + samples.value = blurParams1.samples; + dTheta.value = blurParams1.radiansPerPixel; + + if ( sigmaRadians <= 0 ) { + + camera.update( renderer, scene ); + + } else { + + const blurTarget = new CubeRenderTarget( Math.min( this.width, width ) ); + camera.renderTarget = blurTarget; + + camera.update( renderer, scene ); + + camera.renderTarget = this; + envMap.value = blurTarget.texture; + latitudinal.value = 0; + const blurParams2 = getBlurParams( sigmaRadians, blurTarget.width, MAX_SAMPLES ); + weights.value = blurParams2.weights; + samples.value = blurParams2.samples; + dTheta.value = blurParams2.radiansPerPixel; + + camera.update( renderer, scene ); + + blurTarget.dispose(); + + } + + cubeTex.currentGenerateMipmaps = currentGenerateMipmaps; + geometry.dispose(); + blurMaterial.dispose(); + + } + } export default CubeRenderTarget; diff --git a/src/renderers/common/extras/PMREMGenerator.js b/src/renderers/common/extras/PMREMGenerator.js index 1f9d70a5b4e303..fc305dd618ad3e 100644 --- a/src/renderers/common/extras/PMREMGenerator.js +++ b/src/renderers/common/extras/PMREMGenerator.js @@ -1,11 +1,10 @@ import NodeMaterial from '../../../materials/nodes/NodeMaterial.js'; -import { getDirection, blur } from '../../../nodes/pmrem/PMREMUtils.js'; +import { getDirection, blur, bilinearCubeUV, getBlurParams } from '../../../nodes/pmrem/PMREMUtils.js'; import { equirectUV } from '../../../nodes/utils/EquirectUVNode.js'; -import { uniform } from '../../../nodes/core/UniformNode.js'; -import { uniformArray } from '../../../nodes/accessors/UniformArrayNode.js'; +import { userData } from '../../../nodes/accessors/UserDataNode.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, int, vec3, Fn } from '../../../nodes/tsl/TSLBase.js'; import { uv } from '../../../nodes/accessors/UV.js'; import { attribute } from '../../../nodes/core/AttributeNode.js'; @@ -619,70 +618,23 @@ class PMREMGenerator { } - // Number of standard deviations at which to cut off the discrete approximation. - const STANDARD_DEVIATIONS = 3; - const blurMesh = this._lodMeshes[ lodOut ]; blurMesh.material = 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; - - } - targetIn.texture.frame = ( targetIn.texture.frame || 0 ) + 1; - - 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; - - } + blurMaterial._envMap.value = targetIn.texture; const { _lodMax } = this; - blurUniforms.dTheta.value = radiansPerPixel; - blurUniforms.mipInt.value = _lodMax - lodIn; + const { radiansPerPixel, samples, weights } = getBlurParams( sigmaRadians, this._sizeLods[ lodIn ] - 1, MAX_SAMPLES ); + + blurMesh.userData = { + samples, + weights, + poleAxis, + latitudinal: direction === 'latitudinal' ? 1 : 0, + dTheta: radiansPerPixel, + mipInt: _lodMax - lodIn + }; const outputSize = this._sizeLods[ lodOut ]; const x = 3 * outputSize * ( lodOut > _lodMax - LOD_MIN ? lodOut - _lodMax + LOD_MIN : 0 ); @@ -812,36 +764,28 @@ function _getMaterial( type ) { function _getBlurShader( lodMax, width, height ) { - const weights = uniformArray( new Array( MAX_SAMPLES ).fill( 0 ) ); - const poleAxis = uniform( new Vector3( 0, 1, 0 ) ); - const dTheta = uniform( 0 ); + const weights = userData( 'weights', 'float' ); + const poleAxis = userData( 'poleAxis', 'vec3' ); + const dTheta = userData( 'dTheta', 'float' ); const n = float( MAX_SAMPLES ); - const latitudinal = uniform( 0 ); // false, bool - const samples = uniform( 1 ); // int + const latitudinal = userData( 'latitudinal', 'int' ); // bool + const samples = userData( 'samples', 'int' ); + const mipInt = userData( 'mipInt', '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( 'blur' ); - material.uniforms = materialUniforms; // TODO: Move to outside of the material - material.fragmentNode = blur( { ...materialUniforms, latitudinal: latitudinal.equal( 1 ) } ); + material._envMap = envMap; + + const cubeUVsampler = Fn( ( [ sampleDirection ] )=>{ + + return bilinearCubeUV( envMap, sampleDirection, mipInt, CUBEUV_TEXEL_WIDTH, CUBEUV_TEXEL_HEIGHT, CUBEUV_MAX_MIP ); + + } ); + material.fragmentNode = blur( { n, latitudinal: latitudinal.equal( int( 1 ) ), poleAxis, outputDirection, weights, samples, dTheta, sampler: cubeUVsampler } ); return material;