From add8fad8cf17887bd01e4b3a01e3c0ad86b16c2f Mon Sep 17 00:00:00 2001 From: sunag Date: Wed, 26 Jan 2022 14:07:28 -0300 Subject: [PATCH] NodeEditor: add Basic and Points Material (#23339) --- examples/jsm/node-editor/NodeEditor.js | 44 ++++- .../materials/BasicMaterialEditor.js | 87 ++++++++++ .../materials/PointsMaterialEditor.js | 97 +++++++++++ examples/jsm/node-editor/scene/MeshEditor.js | 133 ++------------- .../jsm/node-editor/scene/Object3DEditor.js | 160 ++++++++++++++++++ .../jsm/node-editor/scene/PointsEditor.js | 99 +++++++++++ examples/webgl_nodes_playground.html | 12 +- examples/webgpu_nodes_playground.html | 8 + 8 files changed, 512 insertions(+), 128 deletions(-) create mode 100644 examples/jsm/node-editor/materials/BasicMaterialEditor.js create mode 100644 examples/jsm/node-editor/materials/PointsMaterialEditor.js create mode 100644 examples/jsm/node-editor/scene/Object3DEditor.js create mode 100644 examples/jsm/node-editor/scene/PointsEditor.js diff --git a/examples/jsm/node-editor/NodeEditor.js b/examples/jsm/node-editor/NodeEditor.js index e5db9d54296efc..da9600c326c6db 100644 --- a/examples/jsm/node-editor/NodeEditor.js +++ b/examples/jsm/node-editor/NodeEditor.js @@ -1,5 +1,7 @@ import { Styles, Canvas, CircleMenu, ButtonInput, ContextMenu, Tips, Search, Loader } from '../libs/flow.module.js'; +import { BasicMaterialEditor } from './materials/BasicMaterialEditor.js'; import { StandardMaterialEditor } from './materials/StandardMaterialEditor.js'; +import { PointsMaterialEditor } from './materials/PointsMaterialEditor.js'; import { OperatorEditor } from './math/OperatorEditor.js'; import { NormalizeEditor } from './math/NormalizeEditor.js'; import { InvertEditor } from './math/InvertEditor.js'; @@ -23,6 +25,7 @@ import { OscillatorEditor } from './utils/OscillatorEditor.js'; import { SplitEditor } from './utils/SplitEditor.js'; import { JoinEditor } from './utils/JoinEditor.js'; import { CheckerEditor } from './procedural/CheckerEditor.js'; +import { PointsEditor } from './scene/PointsEditor.js'; import { MeshEditor } from './scene/MeshEditor.js'; import { EventDispatcher } from 'three'; @@ -199,17 +202,30 @@ export const NodeList = [ name: 'Material', icon: 'circles', children: [ + { + name: 'Basic Material', + icon: 'circle', + nodeClass: BasicMaterialEditor + }, { name: 'Standard Material', icon: 'circle', nodeClass: StandardMaterialEditor + }, + { + name: 'Points Material', + icon: 'circle-dotted', + nodeClass: PointsMaterialEditor } ] } ]; export const ClassLib = { + BasicMaterialEditor, StandardMaterialEditor, + PointsMaterialEditor, + PointsEditor, MeshEditor, OperatorEditor, NormalizeEditor, @@ -524,17 +540,35 @@ export class NodeEditor extends EventDispatcher { object3d.traverse( ( obj3d ) => { - if ( obj3d.isMesh === true ) { + if ( obj3d.isMesh === true || obj3d.isPoints === true ) { + + let prefix = null; + let icon = null; + let editorClass = null; + + if ( obj3d.isMesh === true ) { + + prefix = 'Mesh'; + icon = 'ti ti-3d-cube-sphere'; + editorClass = MeshEditor; + + } else if ( obj3d.isPoints === true ) { + + prefix = 'Points'; + icon = 'ti ti-border-none'; + editorClass = PointsEditor; + + } - const button = new ButtonInput( `Mesh - ${obj3d.name}` ); - button.setIcon( 'ti ti-3d-cube-sphere' ); + const button = new ButtonInput( `${prefix} - ${obj3d.name}` ); + button.setIcon( icon ); button.addEventListener( 'complete', () => { for ( const node of this.canvas.nodes ) { if ( node.value === obj3d ) { - // not duplicated node + // prevent duplicated node this.canvas.select( node ); @@ -544,7 +578,7 @@ export class NodeEditor extends EventDispatcher { } - const node = new MeshEditor( obj3d ); + const node = new editorClass( obj3d ); this.add( node ); diff --git a/examples/jsm/node-editor/materials/BasicMaterialEditor.js b/examples/jsm/node-editor/materials/BasicMaterialEditor.js new file mode 100644 index 00000000000000..f879b2ba479d31 --- /dev/null +++ b/examples/jsm/node-editor/materials/BasicMaterialEditor.js @@ -0,0 +1,87 @@ +import { ColorInput, SliderInput, LabelElement } from '../../libs/flow.module.js'; +import { BaseNode } from '../core/BaseNode.js'; +import { MeshBasicNodeMaterial } from '../../renderers/nodes/Nodes.js'; +import * as THREE from 'three'; + +export class BasicMaterialEditor extends BaseNode { + + constructor() { + + const material = new MeshBasicNodeMaterial(); + + super( 'Basic Material', 1, material ); + + this.setWidth( 300 ); + + const color = new LabelElement( 'color' ).setInput( 3 ); + const opacity = new LabelElement( 'opacity' ).setInput( 1 ); + const position = new LabelElement( 'position' ).setInput( 3 ); + + color.add( new ColorInput( material.color.getHex() ).onChange( ( input ) => { + + material.color.setHex( input.getValue() ); + + } ) ); + + opacity.add( new SliderInput( material.opacity, 0, 1 ).onChange( ( input ) => { + + material.opacity = input.getValue(); + + this.updateTransparent(); + + } ) ); + + color.onConnect( () => this.update(), true ); + opacity.onConnect( () => this.update(), true ); + position.onConnect( () => this.update(), true ); + + this.add( color ) + .add( opacity ) + .add( position ); + + this.color = color; + this.opacity = opacity; + this.position = position; + + this.material = material; + + this.update(); + + } + + update() { + + const { material, color, opacity, position } = this; + + color.setEnabledInputs( ! color.getLinkedObject() ); + opacity.setEnabledInputs( ! opacity.getLinkedObject() ); + + material.colorNode = color.getLinkedObject(); + material.opacityNode = opacity.getLinkedObject() || null; + + material.positionNode = position.getLinkedObject() || null; + + material.dispose(); + + this.updateTransparent(); + + // TODO: Fix on NodeMaterial System + material.customProgramCacheKey = () => { + + return THREE.MathUtils.generateUUID(); + + }; + + } + + updateTransparent() { + + const { material, opacity } = this; + + material.transparent = opacity.getLinkedObject() || material.opacity < 1 ? true : false; + + opacity.setIcon( material.transparent ? 'ti ti-layers-intersect' : 'ti ti-layers-subtract' ); + + } + +} diff --git a/examples/jsm/node-editor/materials/PointsMaterialEditor.js b/examples/jsm/node-editor/materials/PointsMaterialEditor.js new file mode 100644 index 00000000000000..eb19c2304b394d --- /dev/null +++ b/examples/jsm/node-editor/materials/PointsMaterialEditor.js @@ -0,0 +1,97 @@ +import { ColorInput, SliderInput, LabelElement } from '../../libs/flow.module.js'; +import { BaseNode } from '../core/BaseNode.js'; +import { PointsNodeMaterial } from '../../renderers/nodes/Nodes.js'; +import * as THREE from 'three'; + +export class PointsMaterialEditor extends BaseNode { + + constructor() { + + const material = new PointsNodeMaterial( { + depthWrite: false, + transparent: true, + sizeAttenuation: true, + blending: THREE.AdditiveBlending + } ); + + super( 'Points Material', 1, material ); + + this.setWidth( 300 ); + + const color = new LabelElement( 'color' ).setInput( 3 ); + const opacity = new LabelElement( 'opacity' ).setInput( 1 ); + const size = new LabelElement( 'size' ).setInput( 1 ); + const position = new LabelElement( 'position' ).setInput( 3 ); + + color.add( new ColorInput( material.color.getHex() ).onChange( ( input ) => { + + material.color.setHex( input.getValue() ); + + } ) ); + + opacity.add( new SliderInput( material.opacity, 0, 1 ).onChange( ( input ) => { + + material.opacity = input.getValue(); + + this.updateTransparent(); + + } ) ); + + color.onConnect( () => this.update(), true ); + opacity.onConnect( () => this.update(), true ); + size.onConnect(() => this.update(), true ); + position.onConnect(() => this.update(), true ); + + this.add( color ) + .add( opacity ) + .add( size ) + .add( position ); + + this.color = color; + this.opacity = opacity; + this.size = size; + this.position = position; + + this.material = material; + + this.update(); + + } + + update() { + + const { material, color, opacity, size, position } = this; + + color.setEnabledInputs( ! color.getLinkedObject() ); + opacity.setEnabledInputs( ! opacity.getLinkedObject() ); + + material.colorNode = color.getLinkedObject(); + material.opacityNode = opacity.getLinkedObject() || null; + + material.sizeNode = size.getLinkedObject() || null; + material.positionNode = position.getLinkedObject() || null; + + material.dispose(); + + this.updateTransparent(); + + // TODO: Fix on NodeMaterial System + material.customProgramCacheKey = () => { + + return THREE.MathUtils.generateUUID(); + + }; + + } + + updateTransparent() { + + const { material, opacity } = this; + + material.transparent = opacity.getLinkedObject() || material.opacity < 1 ? true : false; + + opacity.setIcon( material.transparent ? 'ti ti-layers-intersect' : 'ti ti-layers-subtract' ); + + } + +} diff --git a/examples/jsm/node-editor/scene/MeshEditor.js b/examples/jsm/node-editor/scene/MeshEditor.js index 033a359f95344e..35e7b472f40166 100644 --- a/examples/jsm/node-editor/scene/MeshEditor.js +++ b/examples/jsm/node-editor/scene/MeshEditor.js @@ -1,8 +1,8 @@ -import { NumberInput, StringInput, LabelElement } from '../../libs/flow.module.js'; -import { BaseNode } from '../core/BaseNode.js'; -import { Mesh, MathUtils, Vector3 } from 'three'; +import { LabelElement } from '../../libs/flow.module.js'; +import { Object3DEditor } from './Object3DEditor.js'; +import { Mesh } from 'three'; -export class MeshEditor extends BaseNode { +export class MeshEditor extends Object3DEditor { constructor( mesh = null ) { @@ -12,50 +12,18 @@ export class MeshEditor extends BaseNode { } - super( 'Mesh', 1, mesh ); + super( mesh, 'Mesh' ); this.material = null; this.defaultMaterial = null; - this.defaultPosition = new Vector3(); - this.defaultRotation = new Vector3(); - this.defaultScale = new Vector3( 100, 100, 100 ); - this._initTags(); - this._initTransform(); this._initMaterial(); this.updateDefault(); + this.restoreDefault(); this.update(); - this.onValidElement = () => {}; - - } - - setEditor( editor ) { - - if ( this.editor ) { - - this._restoreDefault(); - - } - - super.setEditor( editor ); - - if ( editor ) { - - const name = this.nameInput.getValue(); - const mesh = editor.scene.getObjectByName( name ); - - this.value = mesh; - - this.updateDefault(); - this.update(); - - } - - return this; - } get mesh() { @@ -98,71 +66,14 @@ export class MeshEditor extends BaseNode { } - _initTags() { - - this.nameInput = new StringInput( this.mesh.name ).setReadOnly( true ) - .onChange( () => this.mesh.name = this.nameInput.getValue() ); - - this.add( new LabelElement( 'Name' ).add( this.nameInput ) ); - - } - - _initTransform() { - - const update = () => this.update(); - - const posX = new NumberInput().setTagColor( 'red' ).onChange( update ); - const posY = new NumberInput().setTagColor( 'green' ).onChange( update ); - const posZ = new NumberInput().setTagColor( 'blue' ).onChange( update ); - - const rotationStep = 1; - - const rotX = new NumberInput().setTagColor( 'red' ).setStep( rotationStep ).onChange( update ); - const rotY = new NumberInput().setTagColor( 'green' ).setStep( rotationStep ).onChange( update ); - const rotZ = new NumberInput().setTagColor( 'blue' ).setStep( rotationStep ).onChange( update ); - - const scaleX = new NumberInput( 100 ).setTagColor( 'red' ).setStep( rotationStep ).onChange( update ); - const scaleY = new NumberInput( 100 ).setTagColor( 'green' ).setStep( rotationStep ).onChange( update ); - const scaleZ = new NumberInput( 100 ).setTagColor( 'blue' ).setStep( rotationStep ).onChange( update ); - - this.add( new LabelElement( 'Position' ).add( posX ).add( posY ).add( posZ ) ) - .add( new LabelElement( 'Rotation' ).add( rotX ).add( rotY ).add( rotZ ) ) - .add( new LabelElement( 'Scale' ).add( scaleX ).add( scaleY ).add( scaleZ ) ); - - this.posX = posX; - this.posY = posY; - this.posZ = posZ; - - this.rotX = rotX; - this.rotY = rotY; - this.rotZ = rotZ; - - this.scaleX = scaleX; - this.scaleY = scaleY; - this.scaleZ = scaleZ; - - } - update() { + super.update(); + const mesh = this.mesh; if ( mesh ) { - const { position, rotation, scale } = mesh; - - position.x = this.posX.getValue(); - position.y = this.posY.getValue(); - position.z = this.posZ.getValue(); - - rotation.x = MathUtils.degToRad( this.rotX.getValue() ); - rotation.y = MathUtils.degToRad( this.rotY.getValue() ); - rotation.z = MathUtils.degToRad( this.rotZ.getValue() ); - - scale.x = this.scaleX.getValue() / 100; - scale.y = this.scaleY.getValue() / 100; - scale.z = this.scaleZ.getValue() / 100; - mesh.material = this.material || this.defaultMaterial; } @@ -171,35 +82,15 @@ export class MeshEditor extends BaseNode { updateDefault() { - const { material, position, rotation, scale } = this.mesh; + super.updateDefault(); - this.defaultMaterial = material; - - this.defaultPosition = position.clone(); - this.defaultRotation = new Vector3( MathUtils.radToDeg( rotation.x ), MathUtils.radToDeg( rotation.y ), MathUtils.radToDeg( rotation.z ) ); - this.defaultScale = scale.clone().multiplyScalar( 100 ); - - this._restoreDefault(); + this.defaultMaterial = this.mesh.material; } - _restoreDefault() { - - const position = this.defaultPosition; - const rotation = this.defaultRotation; - const scale = this.defaultScale; - - this.posX.setValue( position.x ); - this.posY.setValue( position.y ); - this.posZ.setValue( position.z ); - - this.rotX.setValue( rotation.x ); - this.rotY.setValue( rotation.y ); - this.rotZ.setValue( rotation.z ); + restoreDefault() { - this.scaleX.setValue( scale.x ); - this.scaleY.setValue( scale.y ); - this.scaleZ.setValue( scale.z ); + super.restoreDefault(); this.mesh.material = this.defaultMaterial; diff --git a/examples/jsm/node-editor/scene/Object3DEditor.js b/examples/jsm/node-editor/scene/Object3DEditor.js new file mode 100644 index 00000000000000..9754d37f687909 --- /dev/null +++ b/examples/jsm/node-editor/scene/Object3DEditor.js @@ -0,0 +1,160 @@ +import { NumberInput, StringInput, LabelElement } from '../../libs/flow.module.js'; +import { BaseNode } from '../core/BaseNode.js'; +import { Group, MathUtils, Vector3 } from 'three'; + +export class Object3DEditor extends BaseNode { + + constructor( object3d = null, name = 'Object 3D' ) { + + if ( object3d === null ) { + + object3d = new Group(); + + } + + super( name, 1, object3d ); + + this.defaultPosition = new Vector3(); + this.defaultRotation = new Vector3(); + this.defaultScale = new Vector3( 100, 100, 100 ); + + this._initTags(); + this._initTransform(); + + this.onValidElement = () => {}; + + } + + setEditor( editor ) { + + if ( this.editor ) { + + this.restoreDefault(); + + } + + super.setEditor( editor ); + + if ( editor ) { + + const name = this.nameInput.getValue(); + const object3d = editor.scene.getObjectByName( name ); + + this.value = object3d; + + this.updateDefault(); + this.restoreDefault(); + this.update(); + + } + + return this; + + } + + get object3d() { + + return this.value; + + } + + _initTags() { + + this.nameInput = new StringInput( this.object3d.name ).setReadOnly( true ) + .onChange( () => this.object3d.name = this.nameInput.getValue() ); + + this.add( new LabelElement( 'Name' ).add( this.nameInput ) ); + + } + + _initTransform() { + + const update = () => this.update(); + + const posX = new NumberInput().setTagColor( 'red' ).onChange( update ); + const posY = new NumberInput().setTagColor( 'green' ).onChange( update ); + const posZ = new NumberInput().setTagColor( 'blue' ).onChange( update ); + + const rotationStep = 1; + + const rotX = new NumberInput().setTagColor( 'red' ).setStep( rotationStep ).onChange( update ); + const rotY = new NumberInput().setTagColor( 'green' ).setStep( rotationStep ).onChange( update ); + const rotZ = new NumberInput().setTagColor( 'blue' ).setStep( rotationStep ).onChange( update ); + + const scaleX = new NumberInput( 100 ).setTagColor( 'red' ).setStep( rotationStep ).onChange( update ); + const scaleY = new NumberInput( 100 ).setTagColor( 'green' ).setStep( rotationStep ).onChange( update ); + const scaleZ = new NumberInput( 100 ).setTagColor( 'blue' ).setStep( rotationStep ).onChange( update ); + + this.add( new LabelElement( 'Position' ).add( posX ).add( posY ).add( posZ ) ) + .add( new LabelElement( 'Rotation' ).add( rotX ).add( rotY ).add( rotZ ) ) + .add( new LabelElement( 'Scale' ).add( scaleX ).add( scaleY ).add( scaleZ ) ); + + this.posX = posX; + this.posY = posY; + this.posZ = posZ; + + this.rotX = rotX; + this.rotY = rotY; + this.rotZ = rotZ; + + this.scaleX = scaleX; + this.scaleY = scaleY; + this.scaleZ = scaleZ; + + } + + update() { + + const object3d = this.object3d; + + if ( object3d ) { + + const { position, rotation, scale } = object3d; + + position.x = this.posX.getValue(); + position.y = this.posY.getValue(); + position.z = this.posZ.getValue(); + + rotation.x = MathUtils.degToRad( this.rotX.getValue() ); + rotation.y = MathUtils.degToRad( this.rotY.getValue() ); + rotation.z = MathUtils.degToRad( this.rotZ.getValue() ); + + scale.x = this.scaleX.getValue() / 100; + scale.y = this.scaleY.getValue() / 100; + scale.z = this.scaleZ.getValue() / 100; + + } + + } + + updateDefault() { + + const { position, rotation, scale } = this.object3d; + + this.defaultPosition = position.clone(); + this.defaultRotation = new Vector3( MathUtils.radToDeg( rotation.x ), MathUtils.radToDeg( rotation.y ), MathUtils.radToDeg( rotation.z ) ); + this.defaultScale = scale.clone().multiplyScalar( 100 ); + + } + + restoreDefault() { + + const position = this.defaultPosition; + const rotation = this.defaultRotation; + const scale = this.defaultScale; + + this.posX.setValue( position.x ); + this.posY.setValue( position.y ); + this.posZ.setValue( position.z ); + + this.rotX.setValue( rotation.x ); + this.rotY.setValue( rotation.y ); + this.rotZ.setValue( rotation.z ); + + this.scaleX.setValue( scale.x ); + this.scaleY.setValue( scale.y ); + this.scaleZ.setValue( scale.z ); + + } + +} diff --git a/examples/jsm/node-editor/scene/PointsEditor.js b/examples/jsm/node-editor/scene/PointsEditor.js new file mode 100644 index 00000000000000..4ff55491c85622 --- /dev/null +++ b/examples/jsm/node-editor/scene/PointsEditor.js @@ -0,0 +1,99 @@ +import { LabelElement } from '../../libs/flow.module.js'; +import { Object3DEditor } from './Object3DEditor.js'; +import { Points } from 'three'; + +export class PointsEditor extends Object3DEditor { + + constructor( points = null ) { + + if ( points === null ) { + + points = new Points(); + + } + + super( points, 'Points' ); + + this.material = null; + + this.defaultMaterial = null; + + this._initMaterial(); + + this.updateDefault(); + this.restoreDefault(); + this.update(); + + } + + get points() { + + return this.value; + + } + + _initMaterial() { + + const materialElement = new LabelElement( 'Material' ).setInputColor( 'forestgreen' ).setInput( 1 ); + + materialElement.onValid( ( source, target, stage ) => { + + const object = target.getObject(); + + if ( object && object.isMaterial !== true ) { + + if ( stage === 'dragged' ) { + + const name = target.node.getName(); + + this.editor.tips.error( `"${name}" is not a Material.` ); + + } + + return false; + + } + + } ).onConnect( () => { + + this.material = materialElement.getLinkedObject() || this.defaultMaterial; + + this.update(); + + } ); + + this.add( materialElement ); + + } + + update() { + + super.update(); + + const points = this.points; + + if ( points ) { + + points.material = this.material || this.defaultMaterial; + + } + + } + + updateDefault() { + + super.updateDefault(); + + this.defaultMaterial = this.points.material; + + } + + restoreDefault() { + + super.restoreDefault(); + + this.points.material = this.defaultMaterial; + + } + +} diff --git a/examples/webgl_nodes_playground.html b/examples/webgl_nodes_playground.html index a74126aa6d61ed..e04a1a5ba21452 100644 --- a/examples/webgl_nodes_playground.html +++ b/examples/webgl_nodes_playground.html @@ -133,16 +133,24 @@ const defaultMaterial = new Nodes.MeshBasicNodeMaterial(); defaultMaterial.colorNode = new Nodes.FloatNode( 0 ); - const sphere = new THREE.Mesh( new THREE.SphereGeometry( 200, 32, 16 ), defaultMaterial ) ; + const sphere = new THREE.Mesh( new THREE.SphereGeometry( 200, 32, 16 ), defaultMaterial ) ; sphere.name = 'Sphere'; sphere.position.set( 500, 0, -500 ); scene.add( sphere ); - const box = new THREE.Mesh( new THREE.BoxGeometry( 200, 200, 200 ), defaultMaterial ) ; + const box = new THREE.Mesh( new THREE.BoxGeometry( 200, 200, 200 ), defaultMaterial ) ; box.name = 'Box'; box.position.set( -500, 0, -500 ); scene.add( box ); + const defaultPointsMaterial = new Nodes.PointsNodeMaterial(); + defaultPointsMaterial.colorNode = new Nodes.FloatNode( 0 ); + + const torusKnot = new THREE.Points( new THREE.TorusKnotGeometry( 100, 30, 100, 16 ), defaultPointsMaterial ) ; + torusKnot.name = 'Torus Knot ( Points )'; + torusKnot.position.set( 0, 0, -500 ); + scene.add( torusKnot ); + model = object.children[ 0 ]; model.position.set( 0, 0, 10 ); model.scale.setScalar( 1 ); diff --git a/examples/webgpu_nodes_playground.html b/examples/webgpu_nodes_playground.html index c6b17e4b9b86a4..44dea7dba989f2 100644 --- a/examples/webgpu_nodes_playground.html +++ b/examples/webgpu_nodes_playground.html @@ -174,6 +174,14 @@ box.position.set( -500, 0, -500 ); scene.add( box ); + const defaultPointsMaterial = new Nodes.PointsNodeMaterial(); + defaultPointsMaterial.colorNode = new Nodes.FloatNode( 0 ); + + const torusKnot = new THREE.Points( new THREE.TorusKnotGeometry( 100, 30, 100, 16 ), defaultPointsMaterial ) ; + torusKnot.name = 'Torus Knot ( Points )'; + torusKnot.position.set( 0, 0, -500 ); + scene.add( torusKnot ); + model = object.children[ 0 ]; model.position.set( 0, 0, 10 ); model.scale.setScalar( 1 );