Three.js: Pass alphaTest as a uniform, rather than a macro.

Created on 28 Jan 2019  Â·  23Comments  Â·  Source: mrdoob/three.js

A material's .alphaTest property is currently translated into:

#ifdef ALPHATEST

if ( alpha <= ALPHATEST ) discard;

#endif

As a result of using a macro, the value cannot be changed or animated without recompiling. I'd like to propose that we allow animating the value, which may require an API addition to skip the conditional when it isn't needed. For example:

// Mask
material.alphaMode = THREE.AlphaMask; // or AlphaClip
material.alphaTest = 0.5;

// Blend
material.alphaMode = THREE.AlphaBlend; // or AlphaTransparent

// Both
material.alphaMode = THREE.AlphaBlendMask; // or AlphaTransparentMask

In the example above, alphaMode replaces .transparent. Alternatively, we could use separate boolean flags like .transparent=true and .mask=true.

Animating alphaTest is of limited use with fixed material inputs, but it becomes much more interesting when paired with NodeMaterial, where procedural offset, opacity, alphaTest, and emissive values can be used for effects like these:

| dissolve | smoke |
|-----|-----|
| 51575747-353af880-1e68-11e9-8852-eebcaa14fa1a | ezgif-4-e5b3bd8afcf8 |

For further reading, see http://glowfishinteractive.com/dissolving-the-world-part-2/. Previous discussion in https://github.com/mrdoob/three.js/issues/5623.

Do we want a PR for this, and if so: (1) any preference on the API, and (2) should it affect core materials or just NodeMaterial?

/cc @sunag

Suggestion

Most helpful comment

Do we want a PR for this, and if so: (1) any preference on the API, and (2) should it affect core materials or just NodeMaterial?

I see no problem implementing Material.alphaTest as a uniform. We just need to be careful how we implement it to avoid unnecessary overhead in the shader.

Perhaps we can change the default value of Material.alphaTest from 0 to null, and then define USE_ALPHATEST if Material.alphaTest !== null.

All 23 comments

Interesting... In case of NodeMaterial perhaps we could consider:

.alpha = new THREE.ThresholdNode( alphaMapNode, thresholdNode );

and deprecated (maintain only for backward compatibility)
the #ifdef ALPHATEST

// Mask
.alpha = new THREE.ThresholdNode( mapNode, floatNode );

// Blend
.alpha = mapNode;

// Both
.alpha = new THREE.ThresholdNode( mapNode, floatNode, mapNode );
// thrid map is blend

do you have the source of smoke example?

do you have the source of smoke example?

I do not think I need to, I was kind of confused, it seems to have a displace map too.

Do we want a PR for this, and if so: (1) any preference on the API, and (2) should it affect core materials or just NodeMaterial?

I see no problem implementing Material.alphaTest as a uniform. We just need to be careful how we implement it to avoid unnecessary overhead in the shader.

Perhaps we can change the default value of Material.alphaTest from 0 to null, and then define USE_ALPHATEST if Material.alphaTest !== null.

do you have the source of smoke example?

The screenshot is from the Shade for iOS app (https://twitter.com/_LucasRizzotto/status/1078140430036828160) which generates a JSON representation of the node graph and a Unity shader. I have both available if that's helpful. I was able to get a dissolve effect working in NodeMaterial, aside from this alphaTest question and adding a custom Noise3DNode, but haven't tried reproducing the smoke effect yet.

I like the idea of defaulting to alphaTest=null and creating a uniform otherwise. That will work in existing materials fine, and in NodeMaterial would enable this usage:

material.alpha = new NoiseNode( ... );
material.alphaTest = new Math1Node( new TimerNode(), Math1Node.SIN );

Hmm. Shade is quite impressive.

About create new slots is not the way that I would follow only if there is no output more for it.
This limits the node system because it loses the tree process.

For example ThresholdNode could be used in vertex/position in place of a noise in alpha, similar to this example in void-function:
https://github.com/mrdoob/three.js/blob/e3bc361a010caa8d87ed4b0540afd5e9661ec388/examples/webgl_materials_nodes.html#L2144-L2160

another example would be for the developer extend the class of ThresholdNode:

// ConditionalThresholdNode is extended of ThresholdNode
// its add conditional feature
var mask = new THREE.ConditionalThresholdNode( maskNode, floatNode );
mask.conditional = ">=";

material.alpha = mask;

otherwise it would closely resemble the parameterized system, for example:

material.alpha = maskNode;
material.alphaTest = floatNode;
material.alphaTestConditional = ">=";

@sunag I don't think I understand... the suggested ThresholdNode would still have a float output connected to .alpha, but also have a side effect of calling discard when its own output is less than a threshold? A name like ClipNode would be clearer I think, but even then it seems a little confusing. For the tree process to work well in the node system, I don't think nodes should have effects other than their output values.

Because threejs already has an alphaTest parameter, users are going to expect it to take a node as input. I did, until I got this output: #define ALPHATEST [Object object]. 😅

Really ClipNode for me too is a better name. But for the purpose you want it would be necessary to add alphaMode. I think this is unnecessary for NodeMaterial since we can do everything with a node and containing more features like the conditional example.

var mask = new THREE.ClipNode( maskNode, floatNode );
mask.conditional = ">=";

material.alpha = mask;

instead of:

material.alpha = maskNode;
material.alphaTest = floatNode;
material.alphaTestConditional = ">=";
material.alphaMode = THREE.AlphaMask;

For the tree process to work well in the node system, I don't think nodes should have effects other than their output values.

I see a lot of usefulness in BypassNode for create advanced shaders like the examples varying and void-function. I understand the question of returning values, but I think this is related to architecture. Nothing here prevents BypassNode working well in your application. NodeMaterial can be used for both traditional as much the more specifics cases.

I update with glow
image

I don't think I understand... the suggested

I think I generated this confusion because of the third argument. Sorry.

material.color = new THREE.ClipNode( maskNode, floatNode, colorNode );

But for the purpose you want it would be necessary to add alphaMode.

It don't see a reason for that – if we consider the default value of .alphaTest to be null, then any other value implies that clipping is enabled. By adding a ClipNode are you suggesting we remove .alphaTest from NodeMaterial, or keep them both? In this example...

material.alpha = /* procedural texture, etc. */;
material.alphaTest = new Math1Node( new TimerNode(), Math1Node.SIN );

... I would argue that we haven't added any new sockets. We've just made alphaTest behave like the other inputs.

A weird part of ClipNode is that it requires that the value used for clipping also be used some other way in rendering, and it doesn't actually matter what. For example:

material.metalness = new OperatorNode(
  new ClipNode( ... ),
  new FloatNode( 0.0 ),
  OperatorNode.MUL
);

In that case, the main effect of ClipNode has nothing to do with its output... I'm not sure I see an advantage in that, over the current .alphaTest? I suppose there could be uses for making clipping totally independent of alpha, but I can't think of one right now. I'm also not sure why it needs a custom conditional other than <=, since the same effect can be done by remapping its output after the conditional.

In your code...

var clipCode = "if ( " + testValue + " " + this.conditional + " " + thresholdValue + " ) discard;"

// clip
builder.addNodeCode( clipCode );

return this.output.build( builder, output );

... is this order-independent enough to be safe? What guarantees that the appended clipCode line will appear after any possible changes to the test and threshold values?

I see a lot of usefulness in BypassNode for create advanced shaders

What do you mean by BypassNode?


Also, I like the new example! 🎊 We should create a Noise3DNode so it doesn't need to follow UVs maybe.

By adding a ClipNode are you suggesting we remove .alphaTest from NodeMaterial, or keep them both?

for now keep them both. remove .alphaTest in the future, in root node materials like PhongNodeMaterial for example but keep .alphaTest in MeshStandardNodeMaterial built in ClipNode.

I'm not sure I see an advantage in that, over the current .alphaTest?

ClipNode can be used in another process, such material.position and RawNode for example.

is this order-independent enough to be safe? What guarantees that the appended clipCode line will appear after any possible changes to the test and threshold values?

this follows a hierarchy, and share nodes between slots for optimization, in case of material.color = new ClipNode(...) the clipCode is ever added here:
https://github.com/mrdoob/three.js/blob/e3bc361a010caa8d87ed4b0540afd5e9661ec388/examples/js/nodes/materials/nodes/PhongNode.js#L162

if added only in metalness here:
https://github.com/mrdoob/three.js/blob/e3bc361a010caa8d87ed4b0540afd5e9661ec388/examples/js/nodes/materials/nodes/StandardNode.js#L208

in case of added the same ClipNode in both color and metalness, clipCode is added only in color because color slot is processed first and it is unnecessary to use this code two time, this process of optimization is automatic and done in parser.
https://github.com/mrdoob/three.js/blob/e3bc361a010caa8d87ed4b0540afd5e9661ec388/examples/js/nodes/core/Node.js#L23

What do you mean by BypassNode?

It came from of this issue:
https://github.com/mrdoob/three.js/issues/7522#issuecomment-245903051

It has been assigned to define varys and too can be used in void functions.

Also, I like the new example! confetti_ball We should create a Noise3DNode so it doesn't need to follow UVs maybe.

Yes, this would be wonderful. Thanks!

I see, thanks! I'm ok with whatever API for clipping you find best, then. :)

@donmccurdy Thank you for your effort and support in NodeMaterial you always address very relevant needs.

I'm still not completely satisfied with ClipNode, the problem of alphaTest for me is the link with alpha slot.

I thought of a third solution, a boolean slot called clip, seems to me be a mix of the two ideas:

material.clip = new CondNode( ... )

it would also be .mask

material.mask = new CondNode( ... );

https://rawgit.com/sunag/three.js/dev-mask/examples/webgl_materials_nodes.html?e=dissolve

// CondNode returns a boolean value
// show fragment only if CondNode returns true
material.mask = new THREE.CondNode(
    maskNode, // mask node
    new THREE.FloatNode( .1 ), // threshold
    THREE.CondNode.GREATER // condition
);

image

I post a PR to help the workflow, but if we see something better we will changes in sequence. :+1:

@donmccurdy Do you support https://github.com/mrdoob/three.js/issues/15654#issuecomment-458127272?

I have hacked a proof-of-concept and it seems to work great. I have not touched node materials or GLTF import/export though.

@WestLangley I'm fine with that change too, yes. The use cases for procedural clipping that I can think of are specific to NodeMaterial; glTF does not animate alphaTest, and I'm not aware that users have requested it for fixed-input materials. But it's a simple backward-compatible change that may save someone a recompile, so why not. :)

@donmccurdy Well, unless there is renewed demand for it, I don't see the point, to be honest. I think I will leave the built-in materials as-is.

Ok, let's close this since #15663 has merged and reconsider if we get more requests for it.

Was this page helpful?
0 / 5 - 0 ratings