first attempt (no success yet)
For starters I tried to port this TSL example,
const map = new THREE.TextureLoader().load('...')
map.offset.x = -0.5
const uMapMatrix = uniform(map.matrix)
var opacityNode = Fn(() => {
const vMapUv = uMapMatrix.mul(vec3(uv(), 1))
const edgePos = vMapUv.x
const pixelWidth = fwidth(edgePos)
const fade = pixelWidth.mul(1.5)
const alphaFactor = smoothstep(fade.negate(), 0, edgePos)
const newOpacity = materialOpacity.mul(alphaFactor)
If(newOpacity.lessThanEqual(0), () => Discard())
return newOpacity
})();
var cubix = new THREE.Mesh( new THREE.BoxGeometry(1,1,1),
new THREE.MeshPhysicalNodeMaterial( { opacityNode } ) );
(example on codepen using TSL only)
to use only Node classes directly, not mutating any nodes only using their constructors to start with, but was unable to full convert:
const map = new THREE.TextureLoader().load('...')
map.offset.x = -0.5
// Type is not automatically inferred with manual nodes like
// UniformNode (maybe it should be?)
const uMapMatrix = new UniformNode(map.matrix, 'mat3')
// Unable to convert Fn() to manual FnNode because FnNode is
// not exported from 'three/webgpu' (it is not even exported
// from the original src/nodes/tsl/TSLCore.js file). Is this
// an oversight in src/nodes/Nodes.js? Or is it intentional?
var opacityNode = Fn(() => {
// This is not quite the same as Const() in TSL,
// and the type is not automatically inferred
const one = new ConstNode(1, 'uint')
const uv = new AttributeNode('uv', 'vec2')
// There is no equivalent Vec3Node (maybe there should be), instead the
// vec3() TSL function does a dance with ConvertType to create the sub-node
// graph with inferred types and a JoinNode. Perhaps a Vec3Node class should
// do a similar dance to enable the equivalent Node class, for consistency.
const uvVec3 = new JoinNode([uv, one], 'vec3')
// Unable to convert mul() to OperatorNode because OperatorNode
// is not exported from 'three/webgpu' (but is exported as default
// from src/nodes/math/OperatorNode.js). Is this an oversight? Or intentional?
const vMapUv = uMapMatrix.mul(uvVec3)
// const vMapUv = new OperatorNode('*', uMapMatrix, uvVec3)
const edgePos = vMapUv.x
// Unable to convert fwidth() to MathNode, MathNode is not
// exported from three/webgpu (but is exported as default from
// src/nodes/math/MathNode.js). Is this an oversight?
const pixelWidth = fwidth(edgePos)
// Unable to convert mul() to OperatorNode because OperatorNode
// is not exported from 'three/webgpu' (but is exported as default
// from src/nodes/math/OperatorNode.js). Is this an oversight? Or intentional?
const fade = pixelWidth.mul(1.5)
// const fade = new OperatorNode('*', pixelWidth, new ConstNode(1.5, 'float'))
// Unable to convert negate() to MathNode, MathNode is not
// exported from three/webgpu (but is exported as default from
// src/nodes/math/MathNode.js). Is this an oversight?
const negFade = fade.negate()
const zero = new ConstNode(0, 'uint')
// Unable to convert smoothstep() to MathNode, MathNode is not
// exported from three/webgpu (but is exported as default from
// src/nodes/math/MathNode.js). Is this an oversight?
const alphaFactor = smoothstep(negFade, zero, edgePos)
// Unable to convert mul() to OperatorNode because OperatorNode
// is not exported from 'three/webgpu' (but is exported as default
// from src/nodes/math/OperatorNode.js). Is this an oversight? Or intentional?
const newOpacity = materialOpacity.mul(alphaFactor)
// Not even sure how to convert If() to a Node class, and how
// to manage the special stack (why is that required?).
If(newOpacity.lessThanEqual(0), () => Discard())
return newOpacity
})();
var cubix = new THREE.Mesh( new THREE.BoxGeometry(1,1,1),
new THREE.MeshPhysicalNodeMaterial( { opacityNode } ) );
(example on codepen trying to use Node classes directly)
The main issues I see are
- some nodes are not exported at all even from original source files, that makes using plain
Node classes impossible
- some node classes are exported fro original source files, but not from the index (
three/webgpu).
- Importing these from
src/... will cause a fork in the library that will import multiple versions of Three.js APIs
- certain features like automatic type inference are a feature of TSL only, not the node classes.
- If the Node classes had the inference instead, the APIs would be more consistent with each other, and TSL would be a thinner wrapper
- I don’t know what certain things like
nodeProxy, nodeProxyIntent, nodeObjectIntent, etc, are doing, but I was able to omit them (f.e. using JoinNode directly for uvVec3 without the nodeObjectIntent call that vec3() calls)
- I don’t know if this causes some issue, but it also seems like a TSL-only feature that is also not consistent with Node classes
Overall, I think if
- logic from TSL is moved into node classes,
- and all Node classes are exported and easy to use (f.e. auto type inference),
- then TSL becomes a thinner wrapper over the classes,
- and most importantly, making a mutable node graph becomes easy
Benefits of “a mutable node graph”
The idea is that the node graph of a material could be modified after it has been created, for example by disconnecting nodes, reconnecting them, adding nodes between nodes, etc.
Right now, the graph is created once only (typically with static TSL code), then set in stone (via setup()), and none of the examples deal with the underlying graph directly. Despite the term “nodes” being prominent in the contept of TSL, the overall node graph is more of an internal result at the moment.
There could be something akin to the old onBeforeCompile, for example, but that receives the material’s node graph, where we would be able to traverse the graph, modify/replace nodes, inject nodes, etc.
mapping mutable nodes to mutable UI nodes
It isn’t clear how the currently somewhat-static node graph (based on the limitations I commented in the conversion example), can cleanly map to a mutable GUI node graph in a one-to-one fashion.
Maybe it is for this reason that the node playground is currently missing certain nodes. For example, I am not able to find If or Fn in the list of nodes, so the visual paradigm differs a bit from the TSL paradigm.
What I’m imagining is that, if we were able to control the graph purely with mutable Node instances (currently some Node classes are not even exported), then
- a thinner lighter TSL would be implemented on top without certain features being TSL-specific, those features would be in the Node classes directly
- a mutable node graph would more simply map to a mutable GUI graph, there’d be nothing that would prevent any of the current features from appearing as a node in the node list (f.e.
Fn, If, Discard, etc).
- it would become easier to take any node graph and serialize, deserialize, modify, repeat.
- a three-specific GUI would become a thin wrapper, without necessarily even having to be mapped to from non-GUI nodes. Nodes themselves could contain all of the information for the GUI including all input/output connections (the graph itself).
I would like to see the full expressivity of a mutable node graph for those reasons.
Has going in this direction already been discussed anywhere?