Three.js: NodeMaterial
ほぼほぼsunagというコントリビュータによって組まれている超大作 Examples
使い方
基本はふつうのマテリアルと同等
material.colorNode や material.roughnessNode などのNodeがexposeされており、差し替えて見た目を変えることが可能
code:js
import * as THREE from 'three';
import * as Nodes from 'three/nodes';
import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
const renderer = new WebGPURenderer( { canvas } );
// ...
const geometry = new THREE.SphereGeometry( 1.0 );
const material = new Nodes.MeshStandardNodeMaterial();
material.colorNode = Nodes.uniform( material.color ).mul( new THREE.Color( 1.0, 0.5, 0.0 ) );
material.roughnessNode = Nodes.float( 0.2 );
const mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );
// ...
function render() {
renderer.render( scene, camera );
}
ビルドの仕組み
Cache Key
従来のMaterial同様、Cache Keyが存在する
object以外のだいたいのプロパティは RenderObject.getMaterialCacheKey() が回収してくれる
NodeMaterialを継承したクラスでカスタムのCache Keyを設定したい場合、 customProgramCacheKey(): string を実装する
Renderer._renderObjectDirect() 🔗 RenderObjects.createRenderObject() 🔗 RenderObject.getCacheKey() 🔗 RenderObject.getNodesCacheKey() 🔗 のほうは、 Nodes.getCacheKey() 🔗 を叩いている Light・Environment・Fog・ToneMappingの状況が含まれる
RenderObject.getMaterialCacheKey() 🔗 NodeMaterial.customProgramCacheKey() 🔗 既に this.type と getCacheKey( this ) がある。継承する場合、 super.customProgramCacheKey() を呼んだほうが良さそう
getCacheKey() @ NodeUtils.js 🔗 少なくとも、 MeshStandardNodeMaterial で使った場合は "{}" が帰ってきた
依存Nodeを刺した場合に、括弧内に各Nodeのuuid等が入るっぽい
例えば、 "{,colorNode:{uuid:"...",aNode:"...",bNode:"..."}}"
getForRender
Renderer._renderObjectDirect() 🔗 RenderObject.getNodeBuilderState() 🔗 WebGPUBackend.createNodeBuilder() 🔗 コンストラクタの段階では特に何も構築しない
getForRender 内で、 nodeBuilder に material ・ lightsNode ・ environmentNode 等、重要そうなコンポーネントを多数くっつけている
で最後に nodeBuilder.build() を叩いている
Setup
NodeMaterial.fromMaterial( material ) は、渡されたマテリアルが既に NodeMaterial であればそれを使うし、そうでなければ(e.g. 普通の MeshStandardMaterial など)代替となるNodeMaterialを代わりに取ってくる、というもの
NodeMaterial を継承して新しいマテリアルを作る場合、基本的にはここを触ることになる。 setup() 内で呼ばれている setupDiffuseColor() や setupVariants() などをオーバーライドしていく BRDFあたりを改造するときに触る setupLightingModel() は、ここで呼ばれる setupLighting() 経由で呼ばれる 一度、各ステージごとの構成要素を builder.stack 上で調理した上で( builder.stack.addStack ・ builder.stack.outputNode )、その stack を各ステージのFlowに乗せている感じっぽい( builder.addFlow )
例えば、シンプルな単色のStandardマテリアルの場合、fragmentには以下のようなStackNodeがFlowに1個存在する
code:js
// r157調べ
// 色々端折ったコードなので、実際のヒエラルキーとはだいぶ違う点に注意
[
StackNode { // fragment root
nodes: [
Proxy(BypassNode) {
callNode: Proxy(OperatorNode) {
aNode: Proxy(PropertyNode) {
name: "TransformedNormalView",
nodeType: "vec3",
},
bNode: Proxy(ExtendedMaterialNode), # materialNormal
op: "=",
},
outputNode: Proxy(ExpressionNode) { nodeType: "void" },
},
Proxy(BypassNode) {
callNode: Proxy(OperatorNode) {
aNode: Proxy(PropertyNode) {
name: "DiffuseColor",
nodeType: "vec4",
},
bNode: Proxy(ConvertNode) {
convertTo: "vec4",
node: Proxy(OperatorNode) {
aNode: Proxy(UniformNode) {
value: Color(1, 1, 1),
},
bNode: Proxy(UniformNode) {
value: Color(1, 0.5, 0),
},
op: "*",
},
},
op: "=",
},
outputNode: Proxy(ExpressionNode) { nodeType: "void" },
},
Proxy(BypassNode) { /* ... */ }, // DiffuseColor.w
Proxy(BypassNode) { /* ... */ }, // Metalness
Proxy(BypassNode) { /* ... */ }, // Roughness
Proxy(BypassNode) { /* ... */ }, // SpecularColor
Proxy(BypassNode) { /* ... */ }, // DiffuseColor
Proxy(BypassNode) { /* ... */ }, // Output
]
}
]
MeshStandardNodeMaterial の場合、 Output のステージに LightingContextNode というのが刺さっており、これがなかなか厚い
実体は setupLightingModel() というメソッドで返される LightingModel 🔗 Generate
ビルドのプロセスは3つの"build stages"に分かれているらしい
setup: 各ノードの生成・ノードのrefの生成を行う
analyze: 各ノードの最適化・バリデーションを行う
generate: 各ノードをシェーダコードの文字列として書き出す
最後の buildCode() については NodeBuilder 自体は抽象メソッドで、 GLSLNodeBuilder ・ WGSLNodeBuilder に実装がある WGSLNodeBuilder.buildCode() 🔗 GLSLNodeBuilder.buildCode() 🔗 Cache
renderer._nodes.nodeBuilderCache を見ると、cacheKeyと NodeBuilderState の対を見ることができる
Variantが何個生まれたか・どんなシェーダがビルドされたかの確認に良い
Manual build
ビルドはドローコール経由でなくても、以下の手順でも可能
code:js
import WGSLNodeBuilder from 'three/addons/renderers/webgpu/nodes/WGSLNodeBuilder.js';
// ...
const nodeBuilder = new WGSLNodeBuilder( mesh, renderer ).build();
console.log( nodeBuilder );
console.log( nodeBuilder.fragmentShader );