Skip to content

Conversation

@sunag
Copy link
Collaborator

@sunag sunag commented Jun 13, 2025

Related issue: #31177, #31168

Description

The PR introduces a user-customizable sub-build system, which allows for automatic control flows based on the build steps of the input materials. This is already advanced programming in TSL, but it is crucial for the variable intents to remain the same regardless of the input material being used.

The PR also replaces the namespace() approach that did not allow for layering and deep detection of sub-builds if any dependent node uses Fn.once( subBuilds ) allowing to generate different flows automatically if necessary.

Example in POSITION

A: This creates a simple positionWorld in vertex-stage which is sent to colorNode

const material = new THREE.MeshBasicNodeMaterial();
material.colorNode = positionWorld;

/* WGSL - vertex stage
positionLocal = position;
varyings.v_positionWorld = ( object.nodeUniform0 * vec4<f32>( positionLocal, 1.0 ) ).xyz;
*/

B: Now if I need to modify the object's position using positionWorld, the system will have to identify the flow change and recreate the positionWord based on the updates, based on the user's intention.

const material = new THREE.MeshBasicNodeMaterial();
material.colorNode = positionWorld;
material.positionNode = positionWorld.sub( vec3( 2, 0, 0 ) );

/* WGSL - vertex stage
positionLocal = position;
POSITION_v_positionWorld = ( object.nodeUniform0 * vec4<f32>( positionLocal, 1.0 ) ).xyz;
positionLocal = ( POSITION_v_positionWorld - vec3<f32>( 2.0, 0.0, 0.0 ) );
varyings.v_positionWorld = ( object.nodeUniform0 * vec4<f32>( positionLocal, 1.0 ) ).xyz;
*/

The system can also have different sub-build layers so that the node can have different codes if this is used in colorNode or in normalNode or positionNode, thus unifying the variables across user intent.

Sub-Build System Test Code

The code below is for testing purposes only.

// This is a fixed variable because it is not dependent on the sub-build,
// the flow happens after .once() is declared, so it doesn't need to be recreated.
// The node system will cache or recreate the variable automatically.

const fixedVar = vec3( 0, 0, 0 ).toVar( 'fixedVar' );

// Dynamic Sub-Build
// This is a dynamic sub-build that will call once for each sub-build type.

const myFlow = Fn( ( { subBuildFn } ) => {

	const a = vec3( 0, 0, 0 );
	const b = vec3( 0, 0, 1 );
	const c = a.add( b );

	const dynamicCacheTesting = c.add( c ); // cache testing

	console.log( 'Sub-Build Function:', subBuildFn );

	if ( subBuildFn === 'NORMAL' ) {

		return vec3( dynamicCacheTesting ).add( fixedVar ).add( FixedFn() ).toVar( 'NOR_VAR' );

	} else if ( subBuildFn === 'POSITION' ) {

		return vec3( dynamicCacheTesting ).add( fixedVar ).add( FixedFn() ).toVar( 'POS_VAR' );

	} else {

		return vec3( 1 ).add( fixedVar ).toVar( 'VAR' );

	}

} ).once( [ 'POSITION', 'NORMAL' ] )();

// Automatic Sub-Build flow detection
// This will automatically detect the sub-builds and create a new flow for each sub-build type.
// The result will be: myNode, POSITION_myNode, NORMAL_myNode
// VERTEX_myNode will not be created because it is not defined in the .once() method.

const myNode = myFlow.add( myFlow ).toVar( 'myNode' );

// Sub-Builds Layers

const myNodeLayer0 = subBuild( myNode, 'NORMAL' );
const myNodeLayer0_POS = subBuild( myNode, 'POSITION' );

const myNodeLayer1 = subBuild( myNodeLayer0, 'POSITION' );
const myNodeLayer2 = subBuild( myNodeLayer1, 'VERTEX' ); // vertex sub-build never be used in myFlow because it is not defined in the .once() method

// Debugging

const output = myNode.add( myNodeLayer2 ).mul( myNodeLayer2 ).add( myNodeLayer0_POS );

// Material

const material = new THREE.MeshBasicNodeMaterial();
material.colorNode = vec4( output ).debug();

Code Generated

// #--- TSL debug - fragment shader ---#
fixedVar = vec3<f32>( 0.0, 0.0, 0.0 );
VAR = ( vec3<f32>( 1.0, 1.0, 1.0 ) + fixedVar );
myNode = ( VAR + VAR );
nodeVar0 = ( vec3<f32>( 0.0, 0.0, 0.0 ) + vec3<f32>( 0.0, 0.0, 1.0 ) );
NOR_VAR = ( ( ( nodeVar0 + nodeVar0 ) + fixedVar ) + nodeVarying3 );
NORMAL_myNode = ( NOR_VAR + NOR_VAR );
nodeVar1 = ( vec3<f32>( 0.0, 0.0, 0.0 ) + vec3<f32>( 0.0, 0.0, 1.0 ) );
POS_VAR = ( ( ( nodeVar1 + nodeVar1 ) + fixedVar ) + nodeVarying3 );
POSITION_myNode = ( POS_VAR + POS_VAR );

/* ... */ vec4<f32>( ( ( ( myNode + NORMAL_myNode ) * NORMAL_myNode ) + POSITION_myNode ), 1.0 ) /* ... */
// #-----------------------------------#

@github-actions
Copy link

github-actions bot commented Jun 13, 2025

📦 Bundle size

Full ESM build, minified and gzipped.

Before After Diff
WebGL 337.54
78.73
337.54
78.73
+0 B
+0 B
WebGPU 554.01
153.42
555.73
153.92
+1.72 kB
+497 B
WebGPU Nodes 553.36
153.26
555.08
153.76
+1.72 kB
+497 B

🌳 Bundle size after tree-shaking

Minimal build including a renderer, camera, empty scene, and dependencies.

Before After Diff
WebGL 468.74
113.38
468.74
113.38
+0 B
+0 B
WebGPU 629.63
170.32
631.33
170.79
+1.7 kB
+467 B
WebGPU Nodes 584.48
159.67
586.18
160.12
+1.7 kB
+455 B

@sunag sunag marked this pull request as ready for review June 15, 2025 03:15
@sunag sunag merged commit 4437d86 into mrdoob:dev Jun 15, 2025
12 checks passed
@sunag sunag deleted the dev-subbuild branch June 15, 2025 03:37
@Mugen87 Mugen87 added this to the r178 milestone Aug 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants