Can I define and access uniforms in raw WGSL?

I am using the Three.js node system to render some stuff in WebGPU. It seems I can, to some extent, write my own raw WGSL using wgslFn(). However, it seems I have to pass all its input, including uniforms, as parameters to the WGSL function.

Now assume I have a material with lots of constant properties. Example below:

const material = new MeshBasicNodeMaterial();

const myColorNode = Nodes.wgslFn(`
	fn calculateColor(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32, g: f32, h: f32, i: f32) -> vec4<f32> {
		// ... My colour calculations based on the parameters ...
		return color;
	}
`);

material.colorNode = myColorNode({a, b, c, d, e, f, g, h, i});

where a, ..., i are floats or float uniforms describing the material properties. This works fine, and I understand that the node system will take care of the uniform handling if I define the parameters as uniforms. However, in my WGSL code, I must still define all of these as explicit function parameters. Now if I use a series of helper functions in my shader code, this quickly becomes very clunky.

So my question is, is there currently a way to just pass the parameters as uniforms to the material, and access them globally from within my WGSL code, as I would do in WebGL? I know that the WGSL builder does create a struct of uniforms, but I can’t access them directly in WGSL as their names are generated, rather than set by me.

I am also able to declare uniforms in my WGSL code, but have not found a way to populate them from the JS side through Three.js’ interfaces.

I’d be very eager to know whether this is possible and how it can be done.

Here is an example from me. I pass the time as a uniform to the shader and change it in the render loop.

I hope this helps you

//initialize with your custom values
const yourParams = {
   a: uniform(0),
   b: uniform(0),
   c: uniform(0),
   d: uniform(0),
   e: uniform(0),
   f: uniform(0),
   g: uniform(0),
   h: uniform(0),
   i: uniform(0),
}

const myColorNode = Nodes.wgslFn(`
	fn calculateColor(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32, g: f32, h: f32, i: f32) -> vec4<f32> {
		// ... My colour calculations based on the parameters ...
		return color;
	}
`);

const material = new MeshBasicNodeMaterial();
material.colorNode = myColorNode(yourParams);
//And access and change the uniform value
material.colorNode.parameters.a.value = 7;
1 Like

Thanks Attila for your answer! This is in fact what I have been doing so far; as I wrote, I am able to pass them as float uniforms (I actually learned about it from one of your previous posts, so thanks!) From a functional point of view, that should be all I need.

However, I was rather wondering here whether I can avoid the huge list of function parameters in the WGSL code itself - to access the uniforms directly, as I would in WebGL. Especially when the shaders start to grow, and split into several functions, this becomes very messy and hard to maintain. In addition, it would make it easier to reuse/port shaders that were not originally written with the Three.js node system in mind.

So would you happen to know if it’s possible?

EDIT: To clarify a bit what I’m wishing for:
Imagine I could write my shader like this (I actually can):

const myColorNode = Nodes.wgslFn(`
	fn calculateColor() -> vec4<f32> {
		var x = myUniform.a + myUniform.b * myUniform.c; // Access like this
		// ... calculations ...
		return color;
	}

	struct MaterialParams {
		a: f32, b: f32, c: f32, d: f32, e: f32, f: f32, g: f32, h: f32, i: f32
	}

	@binding(0) @group(0)
	var<uniform> myUniform: MaterialParams;
`);

However I don’t know if there is a way to populate myUniform from the JS side through Three.js.

I haven’t needed it in wgsl yet but do you mean something like this?

//modular shaders
import { headerVS } from "../../resources/shader/headerVS.js";
import { headerFS } from "../../resources/shader/headerFS.js";
import { oceanVS } from "../../resources/shader/functionsVS.js";
import { oceanFS } from "../../resources/shader/functionsFS.js"; 


		
this.material_ = new THREE.RawShaderMaterial({
	glslVersion: THREE.GLSL3,
	uniforms: uniform,
	vertexShader:  
	   headerVS + '\n' +
	   functionsVS,
	fragmentShader:
	   headerFS + '\n' +
	   functionsFS,
});

These are individual shader parts that I simply put together. In the header I then have the long list with all the uniforms

export const headerVS = `

	precision highp float;
	precision highp int;
	precision highp sampler2D;

    // uniforms

	// Attributes

	// Outputs	
	
`;
export const functionsVS = `

    //functions

	void main(){

		gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);			
	}
`;

This is how I organize large shaders for myself. As far as I know, the node system handles the bindings and groups, but I don’t know everything about it. Unfortunately, I don’t know yet whether you can do struct materialParams like this, it would be elegant. If so, and perhaps one of the moderators or developers can say that, the WGSL shader could be broken down into smaller parts in a completely analog way like i showed with my glsl example.

P.S. I misunderstood your question. In any case, you can access js constant values from inside your shader like this ${variable}
Unfortunately, I don’t know whether this works with uniforms

1 Like

Yes that’s a good example of importing/reusing shaders. Being able to define the uniforms without WGSL function parameters would make this type of reuse easier, as well as other things that I am likely to need in the future, which is why I ask.

Either way thanks for the ideas: It would indeed be great if one of the devs was able to give a quick yes/no answer.

I also do understand that the node system is not primarily designed around writing raw WGSL, but rather use TSL and the node system JS interface, but my use case might benefit from full WGSL access. I feel like the system almost allows full WGSL control already, but not quite. Another thing I feel would be nice is a struct node type, to pass JS objects as parameters that serialize into WGSL structs. Perhaps I just need to be patient, or try to contribute.

1 Like

With the wgslFn I can use wgsl very well. I have already created more complex shaders. I think it’s good that the node system takes care of the bindings, because with larger shaders this quickly becomes annoying and prone to errors. Especially when I have to rearrange them because I have changed something that requires it.
But I’m glad that interest in the node system is growing and that you’re interested in it too. In my opinion it’s a good improvement and it’s developing very quickly.

A struct node type sounds good

1 Like

This has not been the focus so far, but it is interesting to have these options too.

At the moment, you can define a global variable and assigns from a main() function and use it in other functions in your code, for example:

// based from `webgpu_compute_texture_pingpong`
const computeInitWGSL = wgslFn( `
	fn init( writeTex: texture_storage_2d<${ wgslFormat }, write>, index: u32 ) -> void {

		computeUV( index );

		textureStore( writeTex, globalIndexUV, vec4( globalUV, 0, 1 ) );

	}

	var<private> globalUV : vec2f;
	var<private> globalIndexUV : vec2u;

	fn computeUV( index: u32 ) {

		let posX = index % ${ width };
		let posY = index / ${ width };

		globalIndexUV = vec2u( posX, posY );
		globalUV = vec2f( f32( posX ) / ${ width }.0, f32( posY ) / ${ height }.0 );

	}
` );

Other example with Struct.

// based from `webgpu_compute_texture_pingpong`
const computeInitWGSL = wgslFn( `
	fn init( writeTex: texture_storage_2d<${ wgslFormat }, write>, index: u32 ) -> void {

		computeUV( index );

		textureStore( writeTex, global.indexUV, vec4( global.uv, 0, 1 ) );

	}

	struct MyStruct {
		uv: vec2f,
		indexUV: vec2u
	}
	var<private> global : MyStruct;

	fn computeUV( index: u32 ) {

		let posX = index % ${ width };
		let posY = index / ${ width };

		let indexUV = vec2u( posX, posY );
		let uv = vec2f( f32( posX ) / ${ width }.0, f32( posY ) / ${ height }.0 );

		global = MyStruct( uv, indexUV );

	}
` );

I didn’t pay attention to assigning the variables to the initialization function, the example was just to demonstrate the possibility.

2 Likes

Other way is using TSL label(), we already have an example TSL editor:

https://threejs.org/examples/webgpu_tsl_editor.html

1 Like

Thanks @sunag, label() looks really promising!

I managed to apply a label to one of my uniforms and it did indeed appear under that name in the uniforms struct in WGSL. However, I still had to pass the uniform as a parameter to my wgslFn for it to be registered. And more importantly, it had to be defined in the WGSL function definition too. Do you know if I could just register the uniform into my NodeMaterial without requiring it in my wgslFn parameters?

I realise I’m going a bit towards the flow of the node system development here, but it would be very useful for my particular case. And it seems I am so close now, learning about how label() works.

1 Like

I could just register the uniform into my NodeMaterial without requiring it in my wgslFn parameters?

It should be compatible with include, like:

const myValue = uniform( 0 ).label( 'myValue' );

const myFunc = wgslFn( `wgsl code`, [ myValue ] );
1 Like

Thanks a lot @sunag, I tested it and it works: Exactly what I was looking for!

I was aware of the second wgslFn() parameter, which I use to pass helper functions, but I had no idea you could also pass it uniforms that way. This is perfect.

1 Like

The struct thing could perhaps be the solution for me.
In glsl I had something like this:

//in the vertex shader
out vec3 vPosition;

void main(){
  //morph calculations
  vPosition = displacedPosition;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(displacedPosition, 1.0);
}
//in the fragment shader
in vec3 vPosition

If I now want to use the morphed vPosition in the colorNode wgslFn shader instead of the unmorphed positions from the position attribute, I have to make the vPosition in the positionNode wgslFn available for the colorNode wgslFn.
In wgsl I can use a struct with several variables as a return value, but how does the positionNode know which variable in it is intended for it?
And does the colorNode see the struct from the positionNode wgslFn so that it can access vPosition in it?

There is a certain irony in the fact that something so banal represents an obstacle after all the more demanding challenges have now been solved.

1 Like