diff --git a/src/Three.TSL.js b/src/Three.TSL.js index e31e626a38a2b9..0ea6849fbe2758 100644 --- a/src/Three.TSL.js +++ b/src/Three.TSL.js @@ -464,6 +464,7 @@ export const remapClamp = TSL.remapClamp; export const renderGroup = TSL.renderGroup; export const renderOutput = TSL.renderOutput; export const rendererReference = TSL.rendererReference; +export const replaceDefaultUV = TSL.replaceDefaultUV; export const rotate = TSL.rotate; export const rotateUV = TSL.rotateUV; export const roughness = TSL.roughness; diff --git a/src/materials/nodes/NodeMaterial.js b/src/materials/nodes/NodeMaterial.js index a976d86a22db20..dbd2acf22e795d 100644 --- a/src/materials/nodes/NodeMaterial.js +++ b/src/materials/nodes/NodeMaterial.js @@ -25,7 +25,7 @@ import { modelViewMatrix } from '../../nodes/accessors/ModelNode.js'; import { vertexColor } from '../../nodes/accessors/VertexColorNode.js'; import { premultiplyAlpha } from '../../nodes/display/BlendModes.js'; import { subBuild } from '../../nodes/core/SubBuildNode.js'; -import { warn } from '../../utils.js'; +import { error, warn } from '../../utils.js'; /** * Base class for all node materials. @@ -382,6 +382,14 @@ class NodeMaterial extends Material { */ this.vertexNode = null; + /** + * This node can be used as a global context management component for this material. + * + * @type {?ContextNode} + * @default null + */ + this.contextNode = null; + // Deprecated properties Object.defineProperty( this, 'shadowPositionNode', { // @deprecated, r176 @@ -489,6 +497,32 @@ class NodeMaterial extends Material { const renderer = builder.renderer; const renderTarget = renderer.getRenderTarget(); + // < CONTEXT > + + if ( renderer.contextNode.isContextNode === true ) { + + builder.context = { ...builder.context, ...renderer.contextNode.getFlowContextData() }; + + } else { + + error( 'NodeMaterial: "renderer.contextNode" must be an instance of `context()`.' ); + + } + + if ( this.contextNode !== null ) { + + if ( this.contextNode.isContextNode === true ) { + + builder.context = { ...builder.context, ...this.contextNode.getFlowContextData() }; + + } else { + + error( 'NodeMaterial: "material.contextNode" must be an instance of `context()`.' ); + + } + + } + // < VERTEX STAGE > builder.addStack(); @@ -558,6 +592,14 @@ class NodeMaterial extends Material { if ( isCustomOutput ) resultNode = this.outputNode; + // + + if ( builder.context.getOutput ) { + + resultNode = builder.context.getOutput( resultNode, builder ); + + } + // MRT if ( renderTarget !== null ) { @@ -1301,6 +1343,8 @@ class NodeMaterial extends Material { this.fragmentNode = source.fragmentNode; this.vertexNode = source.vertexNode; + this.contextNode = source.contextNode; + return super.copy( source ); } diff --git a/src/materials/nodes/manager/NodeMaterialObserver.js b/src/materials/nodes/manager/NodeMaterialObserver.js index 4f397ae3e9828e..6dabe0e75148d7 100644 --- a/src/materials/nodes/manager/NodeMaterialObserver.js +++ b/src/materials/nodes/manager/NodeMaterialObserver.js @@ -259,7 +259,7 @@ class NodeMaterialObserver { } - if ( builder.renderer.overrideNodes.modelViewMatrix !== null || builder.renderer.overrideNodes.modelNormalViewMatrix !== null ) + if ( builder.context.modelViewMatrix || builder.context.modelNormalViewMatrix ) return true; return false; diff --git a/src/nodes/accessors/ModelNode.js b/src/nodes/accessors/ModelNode.js index dd7293249bbf26..196c29d9f09ce1 100644 --- a/src/nodes/accessors/ModelNode.js +++ b/src/nodes/accessors/ModelNode.js @@ -123,7 +123,7 @@ export const modelWorldMatrixInverse = /*@__PURE__*/ uniform( new Matrix4() ).on */ export const modelViewMatrix = /*@__PURE__*/ ( Fn( ( builder ) => { - return builder.renderer.overrideNodes.modelViewMatrix || mediumpModelViewMatrix; + return builder.context.modelViewMatrix || mediumpModelViewMatrix; } ).once() )().toVar( 'modelViewMatrix' ); diff --git a/src/nodes/accessors/Normal.js b/src/nodes/accessors/Normal.js index 4701974a8961a7..a9134c2adf5697 100644 --- a/src/nodes/accessors/Normal.js +++ b/src/nodes/accessors/Normal.js @@ -182,9 +182,9 @@ export const transformNormal = /*@__PURE__*/ Fn( ( [ normal, matrix = modelWorld */ export const transformNormalToView = /*@__PURE__*/ Fn( ( [ normal ], builder ) => { - const modelNormalViewMatrix = builder.renderer.overrideNodes.modelNormalViewMatrix; + const modelNormalViewMatrix = builder.context.modelNormalViewMatrix; - if ( modelNormalViewMatrix !== null ) { + if ( modelNormalViewMatrix ) { return modelNormalViewMatrix.transformDirection( normal ); diff --git a/src/nodes/core/ContextNode.js b/src/nodes/core/ContextNode.js index 079a6c100fa48e..f618b113e82f75 100644 --- a/src/nodes/core/ContextNode.js +++ b/src/nodes/core/ContextNode.js @@ -1,5 +1,5 @@ import Node from './Node.js'; -import { addMethodChaining, nodeProxy } from '../tsl/TSLCore.js'; +import { addMethodChaining } from '../tsl/TSLCore.js'; import { warn } from '../../utils.js'; /** @@ -9,6 +9,12 @@ import { warn } from '../../utils.js'; * * ```js *node.context( { getUV: () => customCoord } ); + *\// or + *material.contextNode = context( { getUV: () => customCoord } ); + *\// or + *renderer.contextNode = context( { getUV: () => customCoord } ); + *\// or + *scenePass.contextNode = context( { getUV: () => customCoord } ); *``` * @augments Node */ @@ -26,7 +32,7 @@ class ContextNode extends Node { * @param {Node} node - The node whose context should be modified. * @param {Object} [value={}] - The modified context data. */ - constructor( node, value = {} ) { + constructor( node = null, value = {} ) { super(); @@ -79,6 +85,29 @@ class ContextNode extends Node { } + /** + * Gathers the context data from all parent context nodes. + * + * @return {Object} The gathered context data. + */ + getFlowContextData() { + + const children = []; + + this.traverse( ( node ) => { + + if ( node.isContextNode === true ) { + + children.push( node.value ); + + } + + } ); + + return Object.assign( {}, ...children ); + + } + /** * This method is overwritten to ensure it returns the member type of {@link ContextNode#node}. * @@ -133,11 +162,24 @@ export default ContextNode; * * @tsl * @function - * @param {Node} node - The node whose context should be modified. + * @param {Node|Object} [nodeOrValue={}] - The node whose context should be modified or the modified context data. * @param {Object} [value={}] - The modified context data. * @returns {ContextNode} */ -export const context = /*@__PURE__*/ nodeProxy( ContextNode ).setParameterLength( 1, 2 ); +export const context = /*@__PURE__*/ ( nodeOrValue = null, value = {} ) => { + + let node = nodeOrValue; + + if ( node === null || node.isNode !== true ) { + + value = node || value; + node = null; + + } + + return new ContextNode( node, value ); + +}; /** * TSL function for defining a uniformFlow context value for a given node. diff --git a/src/nodes/display/PassNode.js b/src/nodes/display/PassNode.js index 381d31ef96640e..068cfe1af28801 100644 --- a/src/nodes/display/PassNode.js +++ b/src/nodes/display/PassNode.js @@ -1,7 +1,7 @@ import TempNode from '../core/TempNode.js'; import { default as TextureNode/*, texture*/ } from '../accessors/TextureNode.js'; import { NodeUpdateType } from '../core/constants.js'; -import { nodeObject } from '../tsl/TSLBase.js'; +import { nodeObject, context } from '../tsl/TSLBase.js'; import { uniform } from '../core/UniformNode.js'; import { viewZToOrthographicDepth, perspectiveDepthToViewZ } from './ViewportDepthNode.js'; @@ -248,6 +248,22 @@ class PassNode extends TempNode { */ this.renderTarget = renderTarget; + /** + * An optional global context for the pass. + * + * @type {ContextNode|null} + */ + this.contextNode = null; + + /** + * A cache for the context node. + * + * @private + * @type {?Object} + * @default null + */ + this._contextNodeCache = null; + /** * A dictionary holding the internal result textures. * @@ -739,6 +755,7 @@ class PassNode extends TempNode { const currentMRT = renderer.getMRT(); const currentAutoClear = renderer.autoClear; const currentMask = camera.layers.mask; + const currentContextNode = renderer.contextNode; this._cameraNear.value = camera.near; this._cameraFar.value = camera.far; @@ -759,6 +776,21 @@ class PassNode extends TempNode { renderer.setMRT( this._mrt ); renderer.autoClear = true; + if ( this.contextNode !== null ) { + + if ( this._contextNodeCache === null || this._contextNodeCache.version !== this.version ) { + + this._contextNodeCache = { + version: this.version, + context: context( { ...renderer.contextNode.value, ...this.contextNode.getFlowContextData() } ) + }; + + } + + renderer.contextNode = this._contextNodeCache.context; + + } + const currentSceneName = scene.name; scene.name = this.name ? this.name : scene.name; @@ -770,6 +802,7 @@ class PassNode extends TempNode { renderer.setRenderTarget( currentRenderTarget ); renderer.setMRT( currentMRT ); renderer.autoClear = currentAutoClear; + renderer.contextNode = currentContextNode; camera.layers.mask = currentMask; diff --git a/src/nodes/pmrem/PMREMNode.js b/src/nodes/pmrem/PMREMNode.js index da68832663a3b4..1ff22170a64cb9 100644 --- a/src/nodes/pmrem/PMREMNode.js +++ b/src/nodes/pmrem/PMREMNode.js @@ -308,7 +308,7 @@ class PMREMNode extends TempNode { if ( uvNode === null && builder.context.getUV ) { - uvNode = builder.context.getUV( this ); + uvNode = builder.context.getUV( this, builder ); } diff --git a/src/nodes/utils/UVUtils.js b/src/nodes/utils/UVUtils.js index be70aed374685b..fd270daabd2cf4 100644 --- a/src/nodes/utils/UVUtils.js +++ b/src/nodes/utils/UVUtils.js @@ -1,5 +1,31 @@ import { Fn, vec2 } from '../tsl/TSLBase.js'; import { rotate } from './RotateNode.js'; +import { context } from '../core/ContextNode.js'; + +/** + * Replaces the default UV coordinates used in texture lookups. + * + * ```js + *material.contextNode = replaceDefaultUV( ( textureNode ) => { + * + * // ... + * return customUVCoordinates; + * + *} ); + *``` + * + * @tsl + * @function + * @param {function(Node):Node} callback - A callback that receives the texture node + * and must return the new uv coordinates. + * @param {Node} [node=null] - An optional node to which the context will be applied. + * @return {ContextNode} A context node that replaces the default UV coordinates. + */ +export function replaceDefaultUV( callback, node = null ) { + + return context( node, { getUV: callback } ); + +} /** * Rotates the given uv coordinates around a center point diff --git a/src/renderers/common/Background.js b/src/renderers/common/Background.js index 48cb24c7366791..1fd94a1f03e535 100644 --- a/src/renderers/common/Background.js +++ b/src/renderers/common/Background.js @@ -1,6 +1,6 @@ import DataMap from './DataMap.js'; import Color4 from './Color4.js'; -import { vec4, context, normalWorldGeometry, backgroundBlurriness, backgroundIntensity, backgroundRotation, positionLocal, cameraProjectionMatrix, modelViewMatrix, div } from '../../nodes/TSL.js'; +import { vec4, normalWorldGeometry, backgroundBlurriness, backgroundIntensity, backgroundRotation, positionLocal, cameraProjectionMatrix, modelViewMatrix, div } from '../../nodes/TSL.js'; import NodeMaterial from '../../materials/nodes/NodeMaterial.js'; import { Mesh } from '../../objects/Mesh.js'; @@ -88,7 +88,7 @@ class Background extends DataMap { if ( backgroundMesh === undefined ) { - const backgroundMeshNode = context( vec4( backgroundNode ).mul( backgroundIntensity ), { + const backgroundMeshNode = vec4( backgroundNode ).mul( backgroundIntensity ).context( { // @TODO: Add Texture2D support using node context getUV: () => backgroundRotation.mul( normalWorldGeometry ), getTextureLevel: () => backgroundBlurriness diff --git a/src/renderers/common/RenderObject.js b/src/renderers/common/RenderObject.js index d02f55c564a182..da1fbf2dee742e 100644 --- a/src/renderers/common/RenderObject.js +++ b/src/renderers/common/RenderObject.js @@ -871,7 +871,7 @@ class RenderObject { } - cacheKey = hash( cacheKey, this.camera.id ); + cacheKey = hash( cacheKey, this.camera.id, this.renderer.contextNode.id, this.renderer.contextNode.version ); return cacheKey; diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js index 38f9e7c0516d40..87829026246269 100644 --- a/src/renderers/common/Renderer.js +++ b/src/renderers/common/Renderer.js @@ -35,6 +35,7 @@ import { DoubleSide, BackSide, FrontSide, SRGBColorSpace, NoToneMapping, LinearF import { float, vec3, vec4 } from '../../nodes/tsl/TSLCore.js'; import { reference } from '../../nodes/accessors/ReferenceNode.js'; import { highpModelNormalViewMatrix, highpModelViewMatrix } from '../../nodes/accessors/ModelNode.js'; +import { context } from '../../nodes/core/ContextNode.js'; import { error, warn, warnOnce } from '../../utils.js'; const _scene = /*@__PURE__*/ new Scene(); @@ -222,17 +223,13 @@ class Renderer { this.info = new Info(); /** - * Stores override nodes for specific transformations or calculations. + * A global context node that stores override nodes for specific transformations or calculations. * These nodes can be used to replace default behavior in the rendering pipeline. * - * @type {Object} - * @property {?Node} modelViewMatrix - An override node for the model-view matrix. - * @property {?Node} modelNormalViewMatrix - An override node for the model normal view matrix. + * @type {ContextNode} + * @property {Object} value - The context value object. */ - this.overrideNodes = { - modelViewMatrix: null, - modelNormalViewMatrix: null - }; + this.contextNode = context(); /** * The node library defines how certain library objects like materials, lights @@ -1031,15 +1028,17 @@ class Renderer { */ set highPrecision( value ) { + const contextNodeData = this.contextNode.value; + if ( value === true ) { - this.overrideNodes.modelViewMatrix = highpModelViewMatrix; - this.overrideNodes.modelNormalViewMatrix = highpModelNormalViewMatrix; + contextNodeData.modelViewMatrix = highpModelViewMatrix; + contextNodeData.modelNormalViewMatrix = highpModelNormalViewMatrix; } else if ( this.highPrecision ) { - this.overrideNodes.modelViewMatrix = null; - this.overrideNodes.modelNormalViewMatrix = null; + delete contextNodeData.modelViewMatrix; + delete contextNodeData.modelNormalViewMatrix; } @@ -1053,7 +1052,9 @@ class Renderer { */ get highPrecision() { - return this.overrideNodes.modelViewMatrix === highpModelViewMatrix && this.overrideNodes.modelNormalViewMatrix === highpModelNormalViewMatrix; + const contextNodeData = this.contextNode.value; + + return contextNodeData.modelViewMatrix === highpModelViewMatrix && contextNodeData.modelNormalViewMatrix === highpModelNormalViewMatrix; }