TSL: How do I create a nullable texture uniform in TSL?

I’m trying to create a texture node that starts out null, but can be set later if needed. But, if I create this texture node:

const mapNode = tsl.texture(null, tsl.uv(), null);

I get the following error:

Uncaught (in promise) Error: THREE.TSL: texture( value ) function expects a valid instance of THREE.Texture().

My question is, how can I create a texture node that can be null if needed?

I’d like to check if the texture uniform is set in the shader, like this:

material.colorNode = tsl.Fn(() => {
    const n = tsl.vec3().toVar();
    tsl.If(mapNode.isSet(), () => {
        // do something
    }).Else(() => {
        // do something else
    });
    return n;
})();

Not TSL related but could you,
a) create an isNullTexture uniform
b) create a 1x1 pixel texture that you treat/call “null”?

I’m already doing that lol. My goal is to get away from having to do that

Why?

This sentence is to make the post have 10 characters.

Because it’s messy and feels unnecessary

Ah fair, that makes sense.

Are you sure .isSet would be checked at shader’s run-time? It smells like a compile-time method.

1 Like

TSL is so confusing. Wouldn’t this, by the virtue of JavaScript, be a runtime method?

I see no reason why TSL couldn’t check if the uniform node is set at run time. I believe in the shader, there is no such thing as a “null” texture, but the TSL node could be set up to support something like that on the JavaScript side.

It was more of a question, rather than a statement.

The reason is that there are two “run-times” in TSL. The first one is when a TSL function is executed – this is done before it is compiled to a shader; and the second is when the shader itself is executed.

Some node data is only available during the first phase, while other node data is passed down to the second phase.

  • If isSet uses data that is not passed down, then isSet is run only during the first phase, so the texture check is done only once and the result is hardcoded in the shader. In this case you do not need TSL If.
  • If isSet uses data that exists in the low-level shader, then the check is done many times during rendering. As only fragments of node data are passed down the shader code, I’m not aware whether mapNode as a node exists there. Honestly, I’d be surprised. But it depends on TSL internals which I’m not familiar with.

TSL couldn’t check if the uniform node is set at run time

Yes, I also believe so. TSL could check this but only once and only during function compilation, as this is the only moment when a TSL function is run.

Yes, I also believe so. TSL could check this but only once and only during function compilation, as this is the only moment when a TSL function is run.

I would just assume uniform.isSet() or just simply uniform.isSet would function identically to how (for example) node.greaterThan(0) works. It would probably be better for it to just be a property (not a function) like uniform.isSet.

I see what you mean. These two cases look very different: node.greaterThan(0) can be compiled as there is shader operation for comparing numbers; but I do not remember any instruction to test whether a uniform is set … because there is no such thing – uniforms are always initialized either by the user, or automatically with 0. The only way to find whether a uniform is being explicitly initialized is to dedicate a special magic unexpected value and check against it. As for textures the trick was (if I remember it correctly) to query for the texture size and if it is 0, then there is no valid texture. I hope someone with a better TSL understanding would provide more accurate info, as mine might be outdated.

If you have a proposal for node.isSet, you can file it as an issue in Three.js GitHub, so the developers would consider implementing it.

1 Like

So the solution for now (but possibly forever?) is to make a uniform bool “isFooSet” and check for null in JavaScript? This is because WebGL, opengl, vulkan, metal etc don’t specify an “is uniform ‘set’” instruction - a uniform is either created or not?
And null itself is ambiguous, I think threejs itself creates a null texture object rather than using the null value itself.

1 Like

Maybe, although it could be possible for TSL to have single uniform node at TSL level (e.g. named foo), but to automatically manage two uniform variables at shader level (e.g. foo and isFooSet). So, technically it is possible, but I’m not sure whether this is a good idea or bad idea – most uniforms do not need this.

I’m really curious to know what would warrant such a use case. Why and how would a shader run with an “unset” uniform.

One specific use case is if you have a material applied to an object in your scene, that switches between a solid color and a diffuse map. For example, an unlit billboard that can either be “off” (solid black color, no diffuse map) or “on” (diffuse map is set).

This way you could set many billboards to use the same material, and be able to turn all the billboards on and off by setting/unsetting the diffuse map.

But you can do that by just setting a Boolean uniform. I still don’t see why you would want to change how the underlying API and hardware basically work. Or, by multiplying a color with your texture. (Black is null white is not null).

I would not implement some sort of “setness” to the next generation of graphics cards if all I want to do is multiply color A (texture) with color b (black/white mask). Any shader language including TSL and any programmable pipeline can do that.

2 Likes

I have an object with a floor, ceiling, walls, etc.
If you use the material as normal it works as expected, but what if you wanted to have a different texture on the floor?
Well you make a wallTexture and then a floorTexture, then all the rest.
I have to pass a bool too each time which is annoying, and doubles the uniforms I have to create.
Yes theres ways like buffers or bit conditionals etc. but I get the issue.

texture actually does this internally, but when you pass null it tells it to pass nothing. I think if you did undefined or set the UV’s outside the constructor it would do what you want.

To check is another thing though, you can do texture.value which will return the reference object, and if its the empty texture the uuid should be the same regardless of instance. So you could make a utility function like IfTextureSet(texture, secondAction); that gets the uuid of the empty texture and checks if the uuid of the texture is the same etc.

I’m playing with this idea now, if I find a cleaner pattern I’ll update

1 Like

What do you mean?