Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add CubeTexture blur #30165

Open
wants to merge 12 commits into
base: dev
Choose a base branch
from
1 change: 1 addition & 0 deletions src/Three.WebGPU.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why this wasn't exported before for WebGPU - hopefully this is okay?

export { default as PostProcessing } from './renderers/common/PostProcessing.js';
import * as PostProcessingUtils from './renderers/common/PostProcessingUtils.js';
export { PostProcessingUtils };
Expand Down
62 changes: 55 additions & 7 deletions src/nodes/pmrem/PMREMUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ] ) => {
elalish marked this conversation as resolved.
Show resolved Hide resolved

const mipInt = float( mipInt_immutable ).toVar();
const direction = vec3( direction_immutable );
Expand All @@ -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 );

Expand All @@ -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();

Expand All @@ -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 } ) => {

Expand All @@ -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 };

};
89 changes: 89 additions & 0 deletions src/renderers/common/CubeRenderTarget.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ 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';
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';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is weird - when I tried to have a local variable named cubeTexture it didn't work - said it was a function instead (this one). Looks like it's getting properly renamed here, but apparently not? Is it somehow globally included somewhere?

import { Vector3 } from '../../math/Vector3.js';

// @TODO: Consider rename WebGLCubeRenderTarget to just CubeRenderTarget

Expand Down Expand Up @@ -72,6 +78,89 @@ class CubeRenderTarget extends WebGLCubeRenderTarget {

}

fromCubeTexture( renderer, cubeTexture, sigmaRadians = 0, poleAxis = new Vector3( 0, 1, 0 ) ) {

const currentGenerateMipmaps = cubeTexture.generateMipmaps;

cubeTexture.generateMipmaps = true;

this.texture.type = cubeTexture.type;
this.texture.colorSpace = cubeTexture.colorSpace;

this.texture.generateMipmaps = cubeTexture.generateMipmaps;
this.texture.minFilter = cubeTexture.minFilter;
this.texture.magFilter = cubeTexture.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 } );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sunag thanks for getting the PMREM version of this blur working - now this one is giving me trouble. I'm getting a lot of exceptions like getOutputNode( builder ) where builder is undefined. Also this line device.queue.writeBuffer( bufferGPU, 0, buffer, 0 ); is giving me TypeError: Overload resolution failed. which sounds pretty bad. I'm going to try to make a simpler repro in a three.js example instead of MV.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I think this is just async issues with WebGPURenderer in MV.


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 );

envMap.value = cubeTexture;
latitudinal.value = 1;
const blurParams1 = getBlurParams( sigmaRadians, cubeTexture.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, cubeTexture.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 );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that this function is finally running I'm getting an exception that comes from here (oddly not the first camera.update call?): TypeError: Failed to execute 'writeBuffer' on 'GPUQueue': Overload resolution failed. No idea what to do with that.


blurTarget.dispose();

}

cubeTexture.currentGenerateMipmaps = currentGenerateMipmaps;
geometry.dispose();
blurMaterial.dispose();

}

}

export default CubeRenderTarget;
114 changes: 29 additions & 85 deletions src/renderers/common/extras/PMREMGenerator.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -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;

Expand Down
Loading