TSL: Is a NodeMaterial's default "outputNode" property already "pre-calculated"?

Using TSL, let’s say I’ve replaced the outputNode property of a MeshLambertNodeMaterial to set the output’s opacity to 0.5, like this:

const material = new MeshLambertNodeMaterial();

const originalOutputNode = material.outputNode;

material.outputNode = Fn(() => {

    return vec4(
        originalOutputNode.r,
        originalOutputNode.g,
        originalOutputNode.b,
        0.5
    );

})();

Will material.outputNode be redundantly re-calculated for each access of r, g, and b?

Or is material.outputNode already a “pre-calculated” vec4?

Note that I prefer it NOT to be “pre-calculated”, as it gives me freedom when to choose when it should be calculated.

I am just curious of what the node actually is internally.

I believe you can think of it as a global variable referencing the output of the material — prior to the output color space conversions, tone mapping, and any changes to the output node itself. You don’t need to keep track of the original node specifically, it’s global to the material, and this should have the same effect:

import { output, vec4 } from 'three/tsl';

material.outputNode = vec4(
  output.r,
  output.g,
  output.b,
  0.5  
);

Or that’s my understanding and how I’ve been using it, at least! :slight_smile:

That’s neat, I didn’t know output was essentially the same thing.

However, I’m still curious to know if output is already pre-calculated.

For example, with TSL, you can have an “operation node” or a “variable node”.

Every time you use an “operation node” it will calculate the same thing over and over again. Let’s say you do this:

const myFloat = float(2).sub(1);

const myVector = vec4(myFloat, myFloat, myFloat, myFloat);

^^^ In this example, the operation 2 - 1 is repeated 4 times.

However, let’s say you add a toVar() at the end of the operation:

const myFloat = float(2).sub(1).toVar();

const myVector = vec4(myFloat, myFloat, myFloat, myFloat);

^^^ In this example, the operation 2 - 1 is only done 1 time, because we made it a variable with .toVar().

My question is, is output or material.outputNode an “operation node” or a “variable node”? Does it get “re-calculated” every time I use it somewhere?

You can probably see now how referencing properties on the output node (like output.r, output.g, output.g, etc.) can get quite expensive if output is an “operation” node, since the entire node will get re-calculated every time you do it.

Note that I PREFER output (as well as colorNode, normalNode, etc) to be an “operation node” as its default state, so I can control when and where it gets calculated. I am just curious if it’s an operation node by default.

Have you tried to use .debug() to see the generated code?

PavelBoytchev said:

Have you tried to use .debug() to see the generated code?

I was wondering if it were possible to do that. I will try using .debug() and see.

I’m having trouble getting .debug() to do anything.

It appears that the .debug() method is only available on a node after you call .toVar() on it.

Also, is it supposed to print something to the console? Or do I need to wrap it in a console.log(node.debug()) call? I’ve tried logging the return value from .debug() to the console, but it just shows a reference to a proxy object like this:

Proxy { <target>: {…}, <handler>: {…} }

I also couldn’t find anything about .debug() at the TSL documentation here: Three.js Shading Language · mrdoob/three.js Wiki · GitHub

Usually I attach .debugto the return statement of a Fn function. The result is dumped in the console. Note that if the calculations use in-line functions, their bodies will be in-lined in the dump.

Fn( (...)=>{

	var p = ...

	return p.debug();

} );

The upcoming release 179 will bring better support for .debug().

Thanks, that helped.

Here is the example I have been testing:

const material = new MeshNormalNodeMaterial();
material.outputNode = Fn(() => {
    const solidColor = vec4(0, 0, 0.3, 1);
    return solidColor.debug(arg => {
        setTimeout(() => {
            console.log(arg.fragmentShader);
        }, 1000);
    });
})();

As you can see in the example, I am completely replacing material.outputNode with the solid color vec4(0, 0, 0.3, 1).

However, if you look at the generated shader code, the Output is still calculated, even though it never got used. How can I stop the original Output from being calculated if I provide my own outputNode?

Here is the generated fragment shader:

I don’t think overriding .outputNode is intended to be a way to skip the entire material’s default implementation, though you could file a feature request and maybe it would be implemented. But trivially it should be safe to assume the GLSL or WGSL compiler removes dead code anyway.

I’m not so experienced with the internals of TSL processed. Even if this Output is generated in vain, I’m sure the low-level shader compiler will skip it and its calculation.