From 1e4dcdc0910e6a42bd13f5a905e4a27bffed845c Mon Sep 17 00:00:00 2001 From: sunag Date: Sun, 23 Aug 2026 08:38:59 -0300 Subject: [PATCH 1/2] Examples: Add post-processing volumetric fog example (#34319) --- examples/files.json | 1 + examples/jsm/tsl/display/GaussianBlurNode.js | 118 ++-- .../screenshots/webgpu_postprocessing_fog.jpg | Bin 0 -> 13021 bytes examples/tags.json | 7 +- examples/webgpu_postprocessing_fog.html | 595 ++++++++++++++++++ src/nodes/utils/RTTNode.js | 74 ++- 6 files changed, 727 insertions(+), 68 deletions(-) create mode 100644 examples/screenshots/webgpu_postprocessing_fog.jpg create mode 100644 examples/webgpu_postprocessing_fog.html diff --git a/examples/files.json b/examples/files.json index e704fd87b33b47..6c28f7775ae399 100644 --- a/examples/files.json +++ b/examples/files.json @@ -443,6 +443,7 @@ "webgpu_postprocessing_difference", "webgpu_postprocessing_dof", "webgpu_postprocessing_dof_basic", + "webgpu_postprocessing_fog", "webgpu_postprocessing_fxaa", "webgpu_postprocessing_godrays", "webgpu_postprocessing_lensflare", diff --git a/examples/jsm/tsl/display/GaussianBlurNode.js b/examples/jsm/tsl/display/GaussianBlurNode.js index b9f13b1c7cf18a..a45e35dd90670e 100644 --- a/examples/jsm/tsl/display/GaussianBlurNode.js +++ b/examples/jsm/tsl/display/GaussianBlurNode.js @@ -1,5 +1,5 @@ -import { RenderTarget, Vector2, NodeMaterial, RendererUtils, QuadMesh, TempNode, NodeUpdateType } from 'three/webgpu'; -import { Fn, float, uv, uniform, convertToTexture, vec2, vec4, passTexture, premultiplyAlpha, unpremultiplyAlpha, context } from 'three/tsl'; +import { RenderTarget, Vector2, NodeMaterial, RendererUtils, QuadMesh, TempNode, NodeUpdateType, warnOnce } from 'three/webgpu'; +import { Fn, float, uv, uniform, convertToTexture, vec2, vec4, passTexture, premultiplyAlpha, unpremultiplyAlpha, context, texture } from 'three/tsl'; const _quadMesh = /*@__PURE__*/ new QuadMesh(); @@ -62,15 +62,6 @@ class GaussianBlurNode extends TempNode { */ this._invSize = uniform( new Vector2() ); - /** - * Gaussian blur is applied in two passes (horizontal, vertical). - * This node controls the direction of each pass. - * - * @private - * @type {UniformNode} - */ - this._passDirection = uniform( new Vector2() ); - /** * The render target used for the horizontal pass. * @@ -104,7 +95,15 @@ class GaussianBlurNode extends TempNode { * @private * @type {?NodeMaterial} */ - this._material = null; + this._hMaterial = null; + + /** + * The material for the vertical pass. + * + * @private + * @type {?NodeMaterial} + */ + this._vMaterial = null; /** * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node renders @@ -173,12 +172,9 @@ class GaussianBlurNode extends TempNode { // - const textureNode = this.textureNode; - const map = textureNode.value; + const map = this.textureNode.value; - const currentTexture = textureNode.value; - - _quadMesh.material = this._material; + _quadMesh.material = this._hMaterial; this.setSize( map.image.width, map.image.height ); @@ -191,25 +187,20 @@ class GaussianBlurNode extends TempNode { renderer.setRenderTarget( this._horizontalRT ); - this._passDirection.value.set( 1, 0 ); - _quadMesh.name = 'Gaussian Blur [ Horizontal Pass ]'; _quadMesh.render( renderer ); // vertical - textureNode.value = this._horizontalRT.texture; - renderer.setRenderTarget( this._verticalRT ); + _quadMesh.material = this._vMaterial; - this._passDirection.value.set( 0, 1 ); + renderer.setRenderTarget( this._verticalRT ); _quadMesh.name = 'Gaussian Blur [ Vertical Pass ]'; _quadMesh.render( renderer ); // restore - textureNode.value = currentTexture; - RendererUtils.restoreRendererState( renderer, _rendererState ); } @@ -233,36 +224,32 @@ class GaussianBlurNode extends TempNode { */ setup( builder ) { - const textureNode = this.textureNode; - - // - const uvNode = uv(); const directionNode = vec2( this.directionNode || 1 ); - let sampleTexture, output; + const blur = Fn( ( [ textureNode, passDirection ] ) => { - if ( this.premultipliedAlpha ) { + let sampleTexture, output; - // https://lisyarus.github.io/blog/posts/blur-coefficients-generator.html + if ( this.premultipliedAlpha ) { - sampleTexture = ( uv ) => premultiplyAlpha( textureNode.sample( uv ) ); - output = ( color ) => unpremultiplyAlpha( color ); + // https://lisyarus.github.io/blog/posts/blur-coefficients-generator.html - } else { + sampleTexture = ( uv ) => premultiplyAlpha( textureNode.sample( uv ) ); + output = ( color ) => unpremultiplyAlpha( color ); - sampleTexture = ( uv ) => textureNode.sample( uv ); - output = ( color ) => color; + } else { - } + sampleTexture = ( uv ) => textureNode.sample( uv ); + output = ( color ) => color; - const blur = Fn( () => { + } const kernelSize = 3 + ( 2 * this.sigma ); const gaussianCoefficients = this._getCoefficients( kernelSize ); const invSize = this._invSize; - const direction = directionNode.mul( this._passDirection ); + const direction = directionNode.mul( passDirection ); const diffuseSum = vec4( sampleTexture( uvNode ).mul( gaussianCoefficients[ 0 ] ) ).toVar(); @@ -286,16 +273,26 @@ class GaussianBlurNode extends TempNode { // - const material = this._material || ( this._material = new NodeMaterial() ); - material.contextNode = context( builder.getSharedContext() ); - material.fragmentNode = blur(); - material.name = 'Gaussian_blur'; - material.needsUpdate = true; + const hTextureNode = this.textureNode; + + this._hMaterial = this._hMaterial || ( new NodeMaterial() ); + this._hMaterial.contextNode = context( builder.getSharedContext() ); + this._hMaterial.fragmentNode = blur( hTextureNode, vec2( 1, 0 ) ); + this._hMaterial.name = 'Gaussian_blur_horizontal'; + this._hMaterial.needsUpdate = true; + + const vTextureNode = texture( this._horizontalRT.texture, uv() ); + + this._vMaterial = this._vMaterial || new NodeMaterial(); + this._vMaterial.fragmentNode = blur( vTextureNode, vec2( 0, 1 ) ); + this._vMaterial.name = 'Gaussian_blur_vertical'; + this._vMaterial.needsUpdate = true; // const properties = builder.getNodeProperties( this ); - properties.textureNode = textureNode; + properties.hTextureNode = hTextureNode; + properties.vTextureNode = vTextureNode; // @@ -312,7 +309,17 @@ class GaussianBlurNode extends TempNode { this._horizontalRT.dispose(); this._verticalRT.dispose(); - if ( this._material !== null ) this._material.dispose(); + if ( this._hMaterial !== null ) { + + this._hMaterial.dispose(); + this._vMaterial.dispose(); + + this._hMaterial = null; + this._vMaterial = null; + + } + + super.dispose(); } @@ -328,13 +335,18 @@ class GaussianBlurNode extends TempNode { const coefficients = []; const sigma = kernelRadius / 3; - for ( let i = 0; i < kernelRadius; i ++ ) { + let sum = 1; + coefficients.push( 1 ); + + for ( let i = 1; i < kernelRadius; i ++ ) { - coefficients.push( 0.39894 * Math.exp( - 0.5 * i * i / ( sigma * sigma ) ) / sigma ); + const w = Math.exp( - 0.5 * i * i / ( sigma * sigma ) ); + coefficients.push( w ); + sum += 2 * w; } - return coefficients; + return coefficients.map( c => c / sum ); } @@ -347,7 +359,7 @@ class GaussianBlurNode extends TempNode { */ get resolution() { - console.warn( 'THREE.GaussianBlurNode: The "resolution" property has been renamed to "resolutionScale" and is now of type `number`.' ); // @deprecated r180 + warnOnce( 'THREE.GaussianBlurNode: The "resolution" property has been renamed to "resolutionScale" and is now of type `number`.' ); // @deprecated r180 return new Vector2( this.resolutionScale, this.resolutionScale ); @@ -355,7 +367,7 @@ class GaussianBlurNode extends TempNode { set resolution( value ) { - console.warn( 'THREE.GaussianBlurNode: The "resolution" property has been renamed to "resolutionScale" and is now of type `number`.' ); // @deprecated r180 + warnOnce( 'THREE.GaussianBlurNode: The "resolution" property has been renamed to "resolutionScale" and is now of type `number`.' ); // @deprecated r180 this.resolutionScale = value.x; @@ -385,7 +397,7 @@ export const gaussianBlur = ( node, directionNode, sigma, options = {} ) => new * * @tsl * @function - * @deprecated since r180. Use `gaussianBlur()` with `premultipliedAlpha: true` option instead. + * @deprecated since r180. Use `gaussianBlur()` with `premultipliedAlpha: true` option instead. * @param {Node} node - The node that represents the input of the effect. * @param {Node} directionNode - Defines the direction and radius of the blur. * @param {number} sigma - Controls the kernel of the blur filter. Higher values mean a wider blur radius. @@ -393,7 +405,7 @@ export const gaussianBlur = ( node, directionNode, sigma, options = {} ) => new */ export function premultipliedGaussianBlur( node, directionNode, sigma ) { - console.warn( 'THREE.TSL: "premultipliedGaussianBlur()" is deprecated. Use "gaussianBlur()" with "premultipliedAlpha: true" option instead.' ); // deprecated, r180 + warnOnce( 'THREE.TSL: "premultipliedGaussianBlur()" is deprecated. Use "gaussianBlur()" with "premultipliedAlpha: true" option instead.' ); // @deprecated r180 return gaussianBlur( node, directionNode, sigma, { premultipliedAlpha: true } ); diff --git a/examples/screenshots/webgpu_postprocessing_fog.jpg b/examples/screenshots/webgpu_postprocessing_fog.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8b1e3671261e247ca78429b3fcae0fa5b6dbf6b0 GIT binary patch literal 13021 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 + + + three.js webgpu - post-processing fog + + + + + + + + + + +
+ + +
+ three.jsVolumetric Fog +
+ + + Post-Processing Volumetric Cloud Fog using Depth Reconstruction, JBU & Gaussian Filtering in TSL. + +
+ + + + + + diff --git a/src/nodes/utils/RTTNode.js b/src/nodes/utils/RTTNode.js index 487f9651dbe76e..8b1a4952f50a06 100644 --- a/src/nodes/utils/RTTNode.js +++ b/src/nodes/utils/RTTNode.js @@ -9,6 +9,7 @@ import QuadMesh from '../../renderers/common/QuadMesh.js'; import { RenderTarget } from '../../core/RenderTarget.js'; import { Vector2 } from '../../math/Vector2.js'; import { HalfFloatType } from '../../constants.js'; +import { error } from '../../utils.js'; const _size = /*@__PURE__*/ new Vector2(); @@ -32,7 +33,7 @@ class RTTNode extends TextureNode { * Constructs a new RTT node. * * @param {Node} node - The node to render a texture with. - * @param {?number} [width=null] - The width of the internal render target. If not width is applied, the render target is automatically resized. + * @param {?number} [width=null] - The width of the internal render target. If no width is applied, the render target is automatically resized. * @param {?number} [height=null] - The height of the internal render target. * @param {Object} [options={}] - The options for the internal render target. * @param {number} [options.type=HalfFloatType] - The texture type. @@ -124,13 +125,13 @@ class RTTNode extends TextureNode { this._quadMesh = new QuadMesh( new NodeMaterial() ); /** - * The `updateBeforeType` is set to `NodeUpdateType.RENDER` since the node updates - * the texture once per render in its {@link RTTNode#updateBefore} method. + * The `updateBeforeType` is set to `NodeUpdateType.FRAME` since the node updates + * the texture once per frame in its {@link RTTNode#updateBefore} method. * * @type {string} - * @default 'render' + * @default 'frame' */ - this.updateBeforeType = NodeUpdateType.RENDER; + this.updateBeforeType = NodeUpdateType.FRAME; } @@ -159,10 +160,10 @@ class RTTNode extends TextureNode { } /** - * Sets the size of the internal render target + * Sets the size of the internal render target. * * @param {number} width - The width to set. - * @param {number} height - The width to set. + * @param {number} height - The height to set. */ setSize( width, height ) { @@ -207,7 +208,42 @@ class RTTNode extends TextureNode { } - updateBefore( { renderer } ) { + /** + * Overwritten since the value is defined by the internal render target. + * + * @param {Texture} value - The texture value. + */ + set value( value ) { + + if ( this.renderTarget && value !== this.renderTarget.texture ) { + + error( 'TSL: "rtt()" does not allow overwriting the value.' ); + + } + + } + + /** + * The texture of the internal render target. + * + * @type {Texture} + */ + get value() { + + return this.renderTarget ? this.renderTarget.texture : null; + + } + + /** + * Renders the node's output into the internal render target before the main render pass. + * Handles automatic resizing of the render target when `autoResize` is enabled, + * and skips rendering if neither `textureNeedsUpdate` nor `autoUpdate` is true. + * + * @param {NodeFrame} frame - The current node frame, providing access to the renderer and other frame data. + */ + updateBefore( frame ) { + + const { renderer } = frame; if ( this.textureNeedsUpdate === false && this.autoUpdate === false ) return; @@ -238,9 +274,11 @@ class RTTNode extends TextureNode { let name = 'RTT'; - if ( this.node.name ) { + const callName = this.name || this.node.name; + + if ( callName ) { - name = this.node.name + ' [ ' + name + ' ]'; + name = callName + ' [ ' + name + ' ]'; } @@ -266,6 +304,18 @@ class RTTNode extends TextureNode { } + /** + * Frees internal resources. Should be called when the node is no longer in use. + */ + dispose() { + + this.renderTarget.dispose(); + this._quadMesh.material.dispose(); + + super.dispose(); + + } + } export default RTTNode; @@ -276,7 +326,7 @@ export default RTTNode; * @tsl * @function * @param {Node} node - The node to render a texture with. - * @param {?number} [width=null] - The width of the internal render target. If not width is applied, the render target is automatically resized. + * @param {?number} [width=null] - The width of the internal render target. If no width is applied, the render target is automatically resized. * @param {?number} [height=null] - The height of the internal render target. * @param {Object} [options={}] - The options for the internal render target. * @param {number} [options.type=HalfFloatType] - The texture type. @@ -292,7 +342,7 @@ export const rtt = ( node, ...params ) => new RTTNode( nodeObject( node ), ...pa * @tsl * @function * @param {Node} node - The node to render a texture with. - * @param {?number} [width=null] - The width of the internal render target. If not width is applied, the render target is automatically resized. + * @param {?number} [width=null] - The width of the internal render target. If no width is applied, the render target is automatically resized. * @param {?number} [height=null] - The height of the internal render target. * @param {Object} [options={}] - The options for the internal render target. * @param {number} [options.type=HalfFloatType] - The texture type. From 0d8573d1b75e015e4bce318f69292756d64863b8 Mon Sep 17 00:00:00 2001 From: WestLangley Date: Sun, 23 Aug 2026 12:57:02 -0400 Subject: [PATCH 2/2] RotateNode: Add support for Euler Order (#34348) --- src/nodes/utils/RotateNode.js | 87 +++++++++++++++++++++++-- test/unit/addons/tsl/TSLRotate.tests.js | 48 +++++++++++++- 2 files changed, 127 insertions(+), 8 deletions(-) diff --git a/src/nodes/utils/RotateNode.js b/src/nodes/utils/RotateNode.js index e42182302a3258..706b2d5d7eb2a9 100644 --- a/src/nodes/utils/RotateNode.js +++ b/src/nodes/utils/RotateNode.js @@ -1,6 +1,7 @@ import TempNode from '../core/TempNode.js'; import { nodeProxy, vec4, mat2, mat4 } from '../tsl/TSLBase.js'; import { cos, sin } from '../math/MathNode.js'; +import { hashString } from '../core/NodeUtils.js'; /** * Applies a rotation to the given position node. @@ -21,8 +22,9 @@ class RotateNode extends TempNode { * @param {Node} positionNode - The position node. * @param {Node} rotationNode - Represents the rotation that is applied to the position node. Depending * on whether the position data are 2D or 3D, the rotation is expressed a single float value or an Euler value. + * @param {string} [order='XYZ'] - The Euler rotation order. Only used for 3D rotation. */ - constructor( positionNode, rotationNode ) { + constructor( positionNode, rotationNode, order = 'XYZ' ) { super(); @@ -34,13 +36,59 @@ class RotateNode extends TempNode { this.positionNode = positionNode; /** - * Represents the rotation that is applied to the position node. - * Depending on whether the position data are 2D or 3D, the rotation is expressed a single float value or an Euler value. + * Represents the rotation that is applied to the position node. + * Depending on whether the position data are 2D or 3D, the rotation is expressed a single float value or an Euler value. * * @type {Node} */ this.rotationNode = rotationNode; + /** + * The Euler rotation order. + * + * @private + * @type {string} + * @default 'XYZ' + */ + this._order = order; + + } + + /** + * Overwrites the default `customCacheKey()` implementation by including the + * Euler order into the cache key. + * + * @return {number} The hash. + */ + customCacheKey() { + + return hashString( this._order ); + + } + + /** + * Sets the Euler rotation order. + * + * @param {string} value - The Euler rotation order. + * @return {RotateNode} A reference to this node. + */ + setOrder( value ) { + + this._order = value; + + return this; + + } + + /** + * Gets the Euler rotation order. + * + * @return {string} The Euler rotation order. + */ + getOrder() { + + return this._order; + } /** @@ -76,16 +124,44 @@ class RotateNode extends TempNode { } else { const rotation = rotationNode; + const order = this._order; + const rotationXMatrix = mat4( vec4( 1.0, 0.0, 0.0, 0.0 ), vec4( 0.0, cos( rotation.x ), sin( rotation.x ), 0.0 ), vec4( 0.0, sin( rotation.x ).negate(), cos( rotation.x ), 0.0 ), vec4( 0.0, 0.0, 0.0, 1.0 ) ); const rotationYMatrix = mat4( vec4( cos( rotation.y ), 0.0, sin( rotation.y ).negate(), 0.0 ), vec4( 0.0, 1.0, 0.0, 0.0 ), vec4( sin( rotation.y ), 0.0, cos( rotation.y ), 0.0 ), vec4( 0.0, 0.0, 0.0, 1.0 ) ); const rotationZMatrix = mat4( vec4( cos( rotation.z ), sin( rotation.z ), 0.0, 0.0 ), vec4( sin( rotation.z ).negate(), cos( rotation.z ), 0.0, 0.0 ), vec4( 0.0, 0.0, 1.0, 0.0 ), vec4( 0.0, 0.0, 0.0, 1.0 ) ); - return rotationXMatrix.mul( rotationYMatrix ).mul( rotationZMatrix ).mul( vec4( positionNode, 1.0 ) ).xyz; + const matrixMap = { + 'X': rotationXMatrix, + 'Y': rotationYMatrix, + 'Z': rotationZMatrix + }; + + const matrixChain = matrixMap[ order.charAt( 0 ) ] + .mul( matrixMap[ order.charAt( 1 ) ] ) + .mul( matrixMap[ order.charAt( 2 ) ] ); + + return matrixChain.mul( vec4( positionNode, 1.0 ) ).xyz; } } + serialize( data ) { + + super.serialize( data ); + + data.order = this._order; + + } + + deserialize( data ) { + + super.deserialize( data ); + + this._order = data.order; + + } + } export default RotateNode; @@ -98,6 +174,7 @@ export default RotateNode; * @param {Node} positionNode - The position node. * @param {Node} rotationNode - Represents the rotation that is applied to the position node. Depending * on whether the position data are 2D or 3D, the rotation is expressed a single float value or an Euler value. + * @param {string} [order='XYZ'] - The Euler rotation order. Only used for 3D rotation. * @returns {RotateNode} */ -export const rotate = /*@__PURE__*/ nodeProxy( RotateNode ).setParameterLength( 2 ); +export const rotate = /*@__PURE__*/ nodeProxy( RotateNode ).setParameterLength( 2, 3 ); diff --git a/test/unit/addons/tsl/TSLRotate.tests.js b/test/unit/addons/tsl/TSLRotate.tests.js index d59dd7fa4b04c9..e2064763171141 100644 --- a/test/unit/addons/tsl/TSLRotate.tests.js +++ b/test/unit/addons/tsl/TSLRotate.tests.js @@ -14,8 +14,8 @@ export default QUnit.module( 'TSL', () => { assert.closeAbs( rotated90, vec2( 0, 1 ), 1e-4, 'rotate((1,0), PI/2) == (0,1)' ); // A full 2*PI rotation is the identity (up to floating-point drift). - const rotatedFull = rotate( vec2( 3, -2 ), float( Math.PI * 2 ) ); - assert.closeAbs( rotatedFull, vec2( 3, -2 ), 1e-3, 'rotate(v, 2*PI) returns to the original vector' ); + const rotatedFull = rotate( vec2( 3, - 2 ), float( Math.PI * 2 ) ); + assert.closeAbs( rotatedFull, vec2( 3, - 2 ), 1e-3, 'rotate(v, 2*PI) returns to the original vector' ); // Zero rotation is exactly the identity. assert.closeAbs( rotate( vec2( 5, 7 ), float( 0 ) ), vec2( 5, 7 ), 1e-5, 'rotate(v, 0) is the identity' ); @@ -58,7 +58,49 @@ export default QUnit.module( 'TSL', () => { // opposite way round from what the X/Z pattern alone would // suggest: x' = x*cos + z*sin, z' = -x*sin + z*cos. const rotated = rotate( vec3( 1, 5, 0 ), vec3( 0, Math.PI / 2, 0 ) ); - assert.closeAbs( rotated, vec3( 0, 5, -1 ), 1e-4, 'rotating (1,5,0) by PI/2 about Y gives (0,5,-1)' ); + assert.closeAbs( rotated, vec3( 0, 5, - 1 ), 1e-4, 'rotating (1,5,0) by PI/2 about Y gives (0,5,-1)' ); + + } ); + + gpuTest( 'rotate() default order is XYZ', ( { assert } ) => { + + // Omitting the order argument must be identical to passing 'XYZ'. + const position = vec3( 1, 0, 0 ); + const rotation = vec3( Math.PI / 2, Math.PI / 4, 0 ); + + assert.closeAbs( + rotate( position, rotation ), + rotate( position, rotation, 'XYZ' ), + 1e-5, + 'omit order == order "XYZ"' + ); + + } ); + + gpuTest( 'rotate() with order XYZ applies Rx*Ry*Rz', ( { assert } ) => { + + // (1,0,0) rotated by (π/2, π/4, 0) under XYZ → (√2/2, √2/2, 0). + const rotated = rotate( vec3( 1, 0, 0 ), vec3( Math.PI / 2, Math.PI / 4, 0 ), 'XYZ' ); + assert.closeAbs( rotated, vec3( Math.SQRT1_2, Math.SQRT1_2, 0 ), 1e-4, + 'XYZ: (1,0,0) by (π/2, π/4, 0) → (√2/2, √2/2, 0)' ); + + } ); + + gpuTest( 'rotate() with order YXZ applies Ry*Rx*Rz', ( { assert } ) => { + + // Same angles under YXZ → (√2/2, 0, -√2/2). Distinct from the XYZ result above. + const rotated = rotate( vec3( 1, 0, 0 ), vec3( Math.PI / 2, Math.PI / 4, 0 ), 'YXZ' ); + assert.closeAbs( rotated, vec3( Math.SQRT1_2, 0, - Math.SQRT1_2 ), 1e-4, + 'YXZ: (1,0,0) by (π/2, π/4, 0) → (√2/2, 0, -√2/2)' ); + + } ); + + gpuTest( 'rotate() zero rotation is identity for non-default orders', ( { assert } ) => { + + const v = vec3( 3, - 2, 7 ); + + assert.closeAbs( rotate( v, vec3( 0, 0, 0 ), 'YXZ' ), v, 1e-5, 'YXZ zero rotation is identity' ); + assert.closeAbs( rotate( v, vec3( 0, 0, 0 ), 'ZYX' ), v, 1e-5, 'ZYX zero rotation is identity' ); } );