Browse Source

Sbsgraph: base implementation

lviguier 1 year ago
parent
commit
4e0bdba2cf

+ 79 - 0
bin/style.css

@@ -2086,6 +2086,85 @@ input[type=checkbox]:checked:after {
   border: 1px solid #666;
   border: 1px solid #666;
   padding: 2px;
   padding: 2px;
 }
 }
+/* Substance Editor */
+.substance-editor {
+  display: flex;
+  flex-direction: row;
+}
+.substance-editor .flex.vertical {
+  position: relative;
+}
+.substance-editor #right-panel {
+  flex: 0;
+  display: flex;
+  flex-direction: column;
+  min-width: 270px;
+  width: 280px;
+  border-left: 1px solid #444444;
+  background: #222222;
+  height: 100%;
+  user-select: none;
+}
+.substance-editor #right-panel .group .header {
+  display: flex;
+  background-color: #444444;
+  padding: 5px;
+}
+.substance-editor #right-panel .group .header:hover {
+  background-color: #666666;
+}
+.substance-editor #right-panel .group .header .title {
+  text-transform: uppercase;
+  font-weight: bold;
+  margin-left: 7px;
+  font-family: Verdana, serif;
+  font-size: 9pt;
+  color: #aaa;
+}
+.substance-editor #right-panel .group .content {
+  display: block;
+  padding: 10px 20px 10px 20px;
+}
+.substance-editor #right-panel .group .content .title {
+  font-weight: bold;
+  font-family: Verdana, serif;
+  font-size: 9pt;
+  color: #aaa;
+  margin: 0 0 5px 0;
+}
+.substance-editor #right-panel .group .content .fields {
+  display: grid;
+  grid-template-columns: auto auto;
+  gap: 5px 5px;
+  padding-bottom: 10px;
+  border-bottom: 1px solid;
+  margin-bottom: 10px;
+  border-color: #373737;
+}
+.substance-editor #right-panel .group .content .fields.grid-3 {
+  grid-template-columns: auto auto 2%;
+}
+.substance-editor #right-panel .group .content .fields .reset {
+  padding-top: 3px;
+}
+.substance-editor #right-panel .group .content .fields .reset:hover {
+  color: #ffffff;
+}
+.substance-editor #tex-preview {
+  z-index: 3;
+  position: absolute;
+  width: 300px;
+  height: 300px;
+  background-color: #111;
+  border: 1px solid #444;
+  right: 35px;
+  bottom: 35px;
+}
+.substance-editor #tex-preview .hide-toolbar2 {
+  position: absolute;
+  right: 8px;
+  top: 8px;
+}
 .graph-view {
 .graph-view {
   outline: none !important;
   outline: none !important;
   position: relative;
   position: relative;

+ 99 - 0
bin/style.less

@@ -2335,6 +2335,105 @@ input[type=checkbox] {
 	}
 	}
 }
 }
 
 
+/* Substance Editor */
+.substance-editor {
+	display: flex;
+	flex-direction: row;
+
+	.flex.vertical {
+		position:relative;
+	}
+
+	#right-panel {
+		flex: 0;
+		display: flex;
+		flex-direction: column;
+		min-width: 270px;
+
+		width: 280px;
+		border-left: 1px solid #444444;
+    	background: #222222;
+		height: 100%;
+		user-select: none;
+
+		.group {
+			.header {
+				display: flex;
+				background-color: #444444;
+				padding: 5px;
+
+				&:hover {
+					background-color: #666666;
+				}
+
+				.title {
+					text-transform: uppercase;
+					font-weight: bold;
+					margin-left: 7px;
+					font-family: Verdana, serif;
+					font-size: 9pt;
+					color: #aaa;
+				}
+			}
+
+			.content {
+				display: block;
+				padding: 10px 20px 10px 20px;
+
+				.title {
+					font-weight: bold;
+					font-family: Verdana, serif;
+					font-size: 9pt;
+					color: #aaa;
+					margin: 0 0 5px 0;
+				}
+
+				.fields {
+					display: grid;
+					grid-template-columns: auto auto;
+					gap: 5px 5px;
+					padding-bottom: 10px;
+					border-bottom: 1px solid;
+					margin-bottom: 10px;
+					border-color: #373737;
+
+					&.grid-3 {
+						grid-template-columns: auto auto 2%;
+					}
+
+					.reset {
+						&:hover {
+							color: #ffffff;
+						}
+
+						padding-top: 3px;
+					}
+				}
+			}
+		}
+	}
+
+	#tex-preview {
+		z-index: 3;
+		position: absolute;
+
+		width: 300px;
+		height: 300px;
+
+		background-color: #111;
+		border: 1px solid #444;
+
+		right: 35px;
+		bottom: 35px;
+
+		.hide-toolbar2 {
+			position: absolute;
+			right: 8px;
+			top: 8px;
+		}
+	}
+}
+
 .graph-view {
 .graph-view {
 	outline: none !important;
 	outline: none !important;
 	position: relative;
 	position: relative;

+ 57 - 32
hide/view/GraphEditor.hx

@@ -28,6 +28,23 @@ typedef CopySelectionData = {
 	edges: Array<Edge>,
 	edges: Array<Edge>,
 };
 };
 
 
+class PreviewShaderAlpha extends hxsl.Shader {
+	static var SRC = {
+		@input var input : {
+			var uv : Vec2;
+		};
+
+		var pixelColor : Vec4;
+
+		function fragment() {
+			var cb = floor(mod(input.uv * 10.0, vec2(2.0)));
+			var check = mod(cb.x + cb.y, 2.0);
+			var color = check >= 1.0 ? vec3(0.22) : vec3(0.44);
+			pixelColor.rgb = mix(color, pixelColor.rgb, pixelColor.a);
+		}
+	}
+}
+
 @:access(hide.view.shadereditor.Box)
 @:access(hide.view.shadereditor.Box)
 class GraphEditor extends hide.comp.Component {
 class GraphEditor extends hide.comp.Component {
 	public var editor : hide.view.GraphInterface.IGraphEditor;
 	public var editor : hide.view.GraphInterface.IGraphEditor;
@@ -348,6 +365,10 @@ class GraphEditor extends hide.comp.Component {
 
 
 	}
 	}
 
 
+	public dynamic function onSelectionChanged(selectedNodes: Array<IGraphNode>) {
+
+	}
+
 	function onMiniPreviewUpdate(dt: Float) {
 	function onMiniPreviewUpdate(dt: Float) {
 		@:privateAccess
 		@:privateAccess
 		/*if (sceneEditor?.scene?.s3d?.renderer?.ctx?.time != null) {
 		/*if (sceneEditor?.scene?.s3d?.renderer?.ctx?.time != null) {
@@ -432,7 +453,7 @@ class GraphEditor extends hide.comp.Component {
 		var prevY = Box.tmpPoint.y;
 		var prevY = Box.tmpPoint.y;
 		if (prevX == x && prevY == y)
 		if (prevX == x && prevY == y)
 			return;
 			return;
-		var id = box.node.getId();
+		var id = box.node.id;
 		function exec(isUndo: Bool) {
 		function exec(isUndo: Bool) {
 			var x = !isUndo ? x : prevX;
 			var x = !isUndo ? x : prevX;
 			var y = !isUndo ? y : prevY;
 			var y = !isUndo ? y : prevY;
@@ -447,7 +468,7 @@ class GraphEditor extends hide.comp.Component {
 
 
 	public function opResize(box: Box, w: Float, h: Float, undoBuffer: UndoBuffer) {
 	public function opResize(box: Box, w: Float, h: Float, undoBuffer: UndoBuffer) {
 		box.info.comment.getSize(Box.tmpPoint);
 		box.info.comment.getSize(Box.tmpPoint);
-		var id = box.node.getId();
+		var id = box.node.id;
 		var prevW = Box.tmpPoint.x;
 		var prevW = Box.tmpPoint.x;
 		var prevH = Box.tmpPoint.y;
 		var prevH = Box.tmpPoint.y;
 		function exec(isUndo : Bool) {
 		function exec(isUndo : Bool) {
@@ -477,6 +498,9 @@ class GraphEditor extends hide.comp.Component {
 				var box = boxes[id];
 				var box = boxes[id];
 				box.setSelected(false);
 				box.setSelected(false);
 			}
 			}
+
+			var selectedNodes = [for (k in boxesSelected.keys()) boxes.get(k).node];
+			onSelectionChanged(selectedNodes);
 		}
 		}
 		undoBuffer.push(exec);
 		undoBuffer.push(exec);
 		exec(false);
 		exec(false);
@@ -546,20 +570,20 @@ class GraphEditor extends hide.comp.Component {
 		});
 		});
 
 
 		var nodes = editor.getAddNodesMenu();
 		var nodes = editor.getAddNodesMenu();
-		var prevGroup = null;
+		var prevGroups : Map<String, Element> = [];
 		for (i => node in nodes) {
 		for (i => node in nodes) {
-			if (node.group != prevGroup) {
-				new Element('
+			if (prevGroups.get(node.group) == null) {
+				var groupEl = new Element('
 				<div class="group" >
 				<div class="group" >
 					<span> ${node.group} </span>
 					<span> ${node.group} </span>
 				</div>').appendTo(results);
 				</div>').appendTo(results);
-				prevGroup = node.group;
+				prevGroups.set(node.group, groupEl);
 			}
 			}
 
 
 			new Element('
 			new Element('
 				<div node="$i" >
 				<div node="$i" >
 					<span> ${node.name} </span> <span> ${node.description} </span>
 					<span> ${node.name} </span> <span> ${node.description} </span>
-				</div>').appendTo(results);
+				</div>').insertAfter(prevGroups.get(node.group));
 		}
 		}
 
 
 		var menuWidth = Std.parseInt(addMenu.css("width")) + 10;
 		var menuWidth = Std.parseInt(addMenu.css("width")) + 10;
@@ -626,10 +650,10 @@ class GraphEditor extends hide.comp.Component {
 
 
 
 
 			if (createLinkInput != null) {
 			if (createLinkInput != null) {
-				createLinkOutput = packIO(instance.getId(), 0);
+				createLinkOutput = packIO(instance.id, 0);
 			}
 			}
 			else if (createLinkOutput != null) {
 			else if (createLinkOutput != null) {
-				createLinkInput = packIO(instance.getId(), 0);
+				createLinkInput = packIO(instance.id, 0);
 			}
 			}
 
 
 			var pos = new h2d.col.Point();
 			var pos = new h2d.col.Point();
@@ -642,10 +666,10 @@ class GraphEditor extends hide.comp.Component {
 			instance.setPos(pos);
 			instance.setPos(pos);
 			opBox(instance, true, currentUndoBuffer);
 			opBox(instance, true, currentUndoBuffer);
 			if (createLinkInput != null && createLinkOutput != null) {
 			if (createLinkInput != null && createLinkOutput != null) {
-				var box = boxes[instance.getId()];
+				var box = boxes[instance.id];
 				var x = (fromInput ? @:privateAccess box.width : 0) - Box.NODE_RADIUS;
 				var x = (fromInput ? @:privateAccess box.width : 0) - Box.NODE_RADIUS;
 				var y = box.getNodeHeight(0) - Box.NODE_RADIUS;
 				var y = box.getNodeHeight(0) - Box.NODE_RADIUS;
-				opMove(boxes[instance.getId()], pos.x - x, pos.y - y, currentUndoBuffer);
+				opMove(boxes[instance.id], pos.x - x, pos.y - y, currentUndoBuffer);
 				opEdge(createLinkOutput, createLinkInput, true, currentUndoBuffer);
 				opEdge(createLinkOutput, createLinkInput, true, currentUndoBuffer);
 			}
 			}
 
 
@@ -778,10 +802,10 @@ class GraphEditor extends hide.comp.Component {
 
 
 				if (shouldSelect) {
 				if (shouldSelect) {
 					box.setSelected(true);
 					box.setSelected(true);
-					save.newSelections.set(box.node.getId(), true);
+					save.newSelections.set(box.node.id, true);
 				} else {
 				} else {
 					box.setSelected(false);
 					box.setSelected(false);
-					save.newSelections.remove(box.node.getId());
+					save.newSelections.remove(box.node.id);
 				}
 				}
 			}
 			}
 			return;
 			return;
@@ -824,7 +848,7 @@ class GraphEditor extends hide.comp.Component {
 	function moveBox(b: Box, x: Float, y: Float) {
 	function moveBox(b: Box, x: Float, y: Float) {
 		b.setPosition(x, y);
 		b.setPosition(x, y);
 
 
-		var id = b.node.getId();
+		var id = b.node.id;
 		// move edges from and to this box
 		// move edges from and to this box
 		for (i => _ in b.info.inputs) {
 		for (i => _ in b.info.inputs) {
 			var input = packIO(id, i);
 			var input = packIO(id, i);
@@ -887,7 +911,7 @@ class GraphEditor extends hide.comp.Component {
 
 
 				for (bb in boxes) {
 				for (bb in boxes) {
 					if (isFullyInside(bb, min, max)) {
 					if (isFullyInside(bb, min, max)) {
-						boxesToMove.set(bb.node.getId(), true);
+						boxesToMove.set(bb.node.id, true);
 					}
 					}
 				}
 				}
 			}
 			}
@@ -899,7 +923,7 @@ class GraphEditor extends hide.comp.Component {
 		for (id => _ in boxesToMove) {
 		for (id => _ in boxesToMove) {
 			var b = boxes[id];
 			var b = boxes[id];
 			b.node.getPos(Box.tmpPoint);
 			b.node.getPos(Box.tmpPoint);
-			save.set(b.node.getId(), {x:Box.tmpPoint.x, y: Box.tmpPoint.y});
+			save.set(b.node.id, {x:Box.tmpPoint.x, y: Box.tmpPoint.y});
 		}
 		}
 		return save;
 		return save;
 	}
 	}
@@ -1013,21 +1037,22 @@ class GraphEditor extends hide.comp.Component {
 				editor.addNode(node);
 				editor.addNode(node);
 			}
 			}
 			else {
 			else {
-				var id = node.getId();
+				var id = node.id;
 				var box = boxes.get(id);
 				var box = boxes.get(id);
+
 				removeBox(id);
 				removeBox(id);
 				editor.removeNode(id);
 				editor.removeNode(id);
 
 
 				// Sanity check
 				// Sanity check
 				for (i => _ in box.info.inputs) {
 				for (i => _ in box.info.inputs) {
-					var inputIO = packIO(box.node.getId(), i);
+					var inputIO = packIO(box.node.id, i);
 					var outputIO = outputsToInputs.getLeft(inputIO);
 					var outputIO = outputsToInputs.getLeft(inputIO);
 					if (outputIO != null)
 					if (outputIO != null)
 						throw "box has remaining inputs, operation is not atomic";
 						throw "box has remaining inputs, operation is not atomic";
 				}
 				}
 
 
 				for (i => _ in box.info.outputs) {
 				for (i => _ in box.info.outputs) {
-					var outputIO = packIO(box.node.getId(), i);
+					var outputIO = packIO(box.node.id, i);
 					for (inputIO in outputsToInputs.iterRights(outputIO)) {
 					for (inputIO in outputsToInputs.iterRights(outputIO)) {
 						throw "box has remaining outputs, operation is not atomic";
 						throw "box has remaining outputs, operation is not atomic";
 					}
 					}
@@ -1043,12 +1068,12 @@ class GraphEditor extends hide.comp.Component {
 		var box = boxes.get(id);
 		var box = boxes.get(id);
 
 
 		box.dispose();
 		box.dispose();
-		var id = box.node.getId();
+		var id = box.node.id;
 		boxes.remove(id);
 		boxes.remove(id);
 	}
 	}
 
 
 	public function opComment(box: Box, newComment: String, undoBuffer: UndoBuffer) : Void {
 	public function opComment(box: Box, newComment: String, undoBuffer: UndoBuffer) : Void {
-		var id = box.node.getId();
+		var id = box.node.id;
 		var prev = box.info.comment.getComment();
 		var prev = box.info.comment.getComment();
 		if (newComment == prev)
 		if (newComment == prev)
 			return;
 			return;
@@ -1133,7 +1158,7 @@ class GraphEditor extends hide.comp.Component {
 					// when not group selection and click on box not selected
 					// when not group selection and click on box not selected
 					clearSelectionBoxesUndo(currentUndoBuffer);
 					clearSelectionBoxesUndo(currentUndoBuffer);
 				}
 				}
-				opSelect(box.node.getId(), true, currentUndoBuffer);
+				opSelect(box.node.id, true, currentUndoBuffer);
 				commitUndo();
 				commitUndo();
 			}
 			}
 			elt.get(0).setPointerCapture(e.pointerId);
 			elt.get(0).setPointerCapture(e.pointerId);
@@ -1145,7 +1170,7 @@ class GraphEditor extends hide.comp.Component {
 			elt.get(0).releasePointerCapture(e.pointerId);
 			elt.get(0).releasePointerCapture(e.pointerId);
 			endMove();
 			endMove();
 		};
 		};
-		boxes.set(box.node.getId(), box);
+		boxes.set(box.node.id, box);
 
 
 		for (inputId => input in box.info.inputs) {
 		for (inputId => input in box.info.inputs) {
 			var defaultValue : String = input.defaultParam?.get();
 			var defaultValue : String = input.defaultParam?.get();
@@ -1161,7 +1186,7 @@ class GraphEditor extends hide.comp.Component {
 						fieldEditInput.addClass("error");
 						fieldEditInput.addClass("error");
 						fieldEditInput.val(prevValue);
 						fieldEditInput.val(prevValue);
 					} else {
 					} else {
-						var id = box.node.getId();
+						var id = box.node.id;
 						function exec(isUndo : Bool) {
 						function exec(isUndo : Bool) {
 							var box = boxes.get(id);
 							var box = boxes.get(id);
 							var val = isUndo ? prevValue : tmpValue;
 							var val = isUndo ? prevValue : tmpValue;
@@ -1191,7 +1216,7 @@ class GraphEditor extends hide.comp.Component {
 					e.stopPropagation();
 					e.stopPropagation();
 					cancelAll();
 					cancelAll();
 					heapsScene.get(0).setPointerCapture(e.pointerId);
 					heapsScene.get(0).setPointerCapture(e.pointerId);
-					edgeCreationInput = packIO(box.node.getId(), inputId);
+					edgeCreationInput = packIO(box.node.id, inputId);
 					edgeCreationMode = FromInput;
 					edgeCreationMode = FromInput;
 				}
 				}
 			});
 			});
@@ -1204,7 +1229,7 @@ class GraphEditor extends hide.comp.Component {
 					e.stopPropagation();
 					e.stopPropagation();
 					cancelAll();
 					cancelAll();
 					heapsScene.get(0).setPointerCapture(e.pointerId);
 					heapsScene.get(0).setPointerCapture(e.pointerId);
-					edgeCreationOutput = packIO(box.node.getId(), outputId);
+					edgeCreationOutput = packIO(box.node.id, outputId);
 					edgeCreationMode = FromOutput;
 					edgeCreationMode = FromOutput;
 				}
 				}
 			});
 			});
@@ -1237,7 +1262,7 @@ class GraphEditor extends hide.comp.Component {
 	}
 	}
 
 
 	function removeBoxEdges(box : Box, ?undoBuffer : UndoBuffer) {
 	function removeBoxEdges(box : Box, ?undoBuffer : UndoBuffer) {
-		var id = box.getInstance().getId();
+		var id = box.getInstance().id;
 		for (i => _ in box.info.inputs) {
 		for (i => _ in box.info.inputs) {
 			var inputIO = packIO(id, i);
 			var inputIO = packIO(id, i);
 			var outputIO = outputsToInputs.getLeft(inputIO);
 			var outputIO = outputsToInputs.getLeft(inputIO);
@@ -1380,7 +1405,7 @@ class GraphEditor extends hide.comp.Component {
 
 
 		var val = null;
 		var val = null;
 		if (minDistNode < NODE_TRIGGER_NEAR && nearestId >= 0) {
 		if (minDistNode < NODE_TRIGGER_NEAR && nearestId >= 0) {
-			val = packIO(nearestBox.node.getId(), nearestId);
+			val = packIO(nearestBox.node.id, nearestId);
 		}
 		}
 
 
 		if (edgeCreationMode == FromInput) {
 		if (edgeCreationMode == FromInput) {
@@ -1446,7 +1471,7 @@ class GraphEditor extends hide.comp.Component {
 			for (nodeInfo in data.nodes) {
 			for (nodeInfo in data.nodes) {
 				var node = editor.unserializeNode(nodeInfo.serData, true);
 				var node = editor.unserializeNode(nodeInfo.serData, true);
 				nodes.push(node);
 				nodes.push(node);
-				var newId = node.getId();
+				var newId = node.id;
 				idRemap.set(nodeInfo.id, newId);
 				idRemap.set(nodeInfo.id, newId);
 			}
 			}
 			for (e in data.edges) {
 			for (e in data.edges) {
@@ -1478,7 +1503,7 @@ class GraphEditor extends hide.comp.Component {
 			pt.y += lY(ide.mouseY);
 			pt.y += lY(ide.mouseY);
 			node.setPos(pt);
 			node.setPos(pt);
 			opBox(node, true, undoBuffer);
 			opBox(node, true, undoBuffer);
-			opSelect(node.getId(), true, undoBuffer);
+			opSelect(node.id, true, undoBuffer);
 		}
 		}
 
 
 		for (edge in edges) {
 		for (edge in edges) {
@@ -1498,7 +1523,7 @@ class GraphEditor extends hide.comp.Component {
 			for (nodeInfo in data.nodes) {
 			for (nodeInfo in data.nodes) {
 				var node = editor.unserializeNode(nodeInfo.serData, true);
 				var node = editor.unserializeNode(nodeInfo.serData, true);
 				nodes.push(node);
 				nodes.push(node);
-				var newId = node.getId();
+				var newId = node.id;
 				idRemap.set(nodeInfo.id, newId);
 				idRemap.set(nodeInfo.id, newId);
 			}
 			}
 			for (e in data.edges) {
 			for (e in data.edges) {
@@ -1533,7 +1558,7 @@ class GraphEditor extends hide.comp.Component {
 			pt.y += lY(ide.mouseY);
 			pt.y += lY(ide.mouseY);
 			node.setPos(pt);
 			node.setPos(pt);
 			opBox(node, true, currentUndoBuffer);
 			opBox(node, true, currentUndoBuffer);
-			opSelect(node.getId(), true, currentUndoBuffer);
+			opSelect(node.id, true, currentUndoBuffer);
 		}
 		}
 
 
 		for (edge in edges) {
 		for (edge in edges) {

+ 8 - 9
hide/view/GraphInterface.hx

@@ -67,19 +67,18 @@ typedef Edge = {
 };
 };
 
 
 interface IGraphNode {
 interface IGraphNode {
-    public function getInfo() : GraphNodeInfo;
-
-    /**
-        Returns an unique ID that identifies this node.
-        The ID of a given node MUST NERVER change for the entire lifetime of the GraphEditor
+     /**
+        Unique identifier for this node. The ID of a given node MUST NERVER change for the entire lifetime of the GraphEditor
     **/
     **/
-    public function getId() : Int;
+    public var id : Int;
+    public var x : Float;
+    public var y : Float;
+    public var editor : GraphEditor;
+
+    public function getInfo() : GraphNodeInfo;
     public function getPos(p : h2d.col.Point) : Void;
     public function getPos(p : h2d.col.Point) : Void;
     public function setPos(p : h2d.col.Point) : Void;
     public function setPos(p : h2d.col.Point) : Void;
-
     public function getPropertiesHTML(width : Float) : Array<hide.Element>;
     public function getPropertiesHTML(width : Float) : Array<hide.Element>;
-
-    public var editor : GraphEditor;
 }
 }
 
 
 interface IGraphEditor {
 interface IGraphEditor {

+ 1 - 1
hide/view/shadereditor/Box.hx

@@ -64,7 +64,7 @@ class Box {
 		//var className = node.nameOverride ?? ((metas.name != null) ? metas.name[0] : "Undefined");
 		//var className = node.nameOverride ?? ((metas.name != null) ? metas.name[0] : "Undefined");
 
 
 		element = editor.editorDisplay.group(parent).addClass("box").addClass("not-selected");
 		element = editor.editorDisplay.group(parent).addClass("box").addClass("not-selected");
-		element.attr("id", node.getId());
+		element.attr("id", node.id);
 
 
 		if (info.comment != null) {
 		if (info.comment != null) {
 			info.comment.getSize(tmpPoint);
 			info.comment.getSize(tmpPoint);

+ 3 - 20
hide/view/shadereditor/ShaderEditor.hx

@@ -201,23 +201,6 @@ class PreviewShaderBase extends hxsl.Shader {
 	}
 	}
 }
 }
 
 
-class PreviewShaderAlpha extends hxsl.Shader {
-	static var SRC = {
-		@input var input : {
-			var uv : Vec2;
-		};
-
-		var pixelColor : Vec4;
-
-		function fragment() {
-			var cb = floor(mod(input.uv * 10.0, vec2(2.0)));
-			var check = mod(cb.x + cb.y, 2.0);
-			var color = check >= 1.0 ? vec3(0.22) : vec3(0.44);
-			pixelColor.rgb = mix(color, pixelColor.rgb, pixelColor.a);
-		}
-	}
-}
-
 typedef ClassRepoEntry =
 typedef ClassRepoEntry =
 {
 {
 	/**
 	/**
@@ -270,7 +253,7 @@ class ShaderEditor extends hide.view.FileView implements GraphInterface.IGraphEd
 	var compiledShaderPreview : hrt.prefab.Cache.ShaderDef;
 	var compiledShaderPreview : hrt.prefab.Cache.ShaderDef;
 
 
 	var previewShaderBase : PreviewShaderBase;
 	var previewShaderBase : PreviewShaderBase;
-	var previewShaderAlpha : PreviewShaderAlpha;
+	var previewShaderAlpha : GraphEditor.PreviewShaderAlpha;
 	var previewVar : hxsl.Ast.TVar;
 	var previewVar : hxsl.Ast.TVar;
 	var needRecompile : Bool = true;
 	var needRecompile : Bool = true;
 
 
@@ -302,7 +285,7 @@ class ShaderEditor extends hide.view.FileView implements GraphInterface.IGraphEd
  		shaderGraph = cast hide.Ide.inst.loadPrefab(state.path, null,  true);
  		shaderGraph = cast hide.Ide.inst.loadPrefab(state.path, null,  true);
 		currentGraph = shaderGraph.getGraph(Fragment);
 		currentGraph = shaderGraph.getGraph(Fragment);
 		previewShaderBase = new PreviewShaderBase();
 		previewShaderBase = new PreviewShaderBase();
-		previewShaderAlpha = new PreviewShaderAlpha();
+		previewShaderAlpha = new GraphEditor.PreviewShaderAlpha();
 
 
 		if (graphEditor != null)
 		if (graphEditor != null)
 			graphEditor.remove();
 			graphEditor.remove();
@@ -1345,7 +1328,7 @@ class ShaderEditor extends hide.view.FileView implements GraphInterface.IGraphEd
 		for (init in compiledShaderPreview.inits) {
 		for (init in compiledShaderPreview.inits) {
 			@:privateAccess graphEditor.previewsScene.checkCurrent();
 			@:privateAccess graphEditor.previewsScene.checkCurrent();
 			if (init.variable == previewVar)
 			if (init.variable == previewVar)
-				setParamValue(shader, previewVar, node.getId() + 1);
+				setParamValue(shader, previewVar, node.id + 1);
 			else {
 			else {
 				var param = shaderGraph.parametersAvailable.find((v) -> v.name == init.variable.name);
 				var param = shaderGraph.parametersAvailable.find((v) -> v.name == init.variable.name);
 				if (param !=null) {
 				if (param !=null) {

+ 459 - 0
hide/view/substanceeditor/SubstanceEditor.hx

@@ -0,0 +1,459 @@
+package hide.view.substanceeditor;
+
+import hrt.sbsgraph.SubstanceNode;
+import hide.view.GraphInterface;
+
+class SubstanceEditor extends hide.view.FileView implements GraphInterface.IGraphEditor {
+	public static var DEFAULT_PREVIEW_COLOR = 16716947;
+	public static var DEFAULT_PREVIEW_SIZE = 2048;
+
+	var graphEditor : hide.view.GraphEditor;
+	var substanceGraph : hrt.sbsgraph.SubstanceGraph;
+
+	// Preview
+	var previewScene : hide.comp.Scene;
+	var previewBitmap : h2d.Bitmap;
+	var previewShaderAlpha : GraphEditor.PreviewShaderAlpha;
+	var initializedPreviews : Map<h2d.Bitmap, Bool> = [];
+	var camController : hide.view.l3d.CameraController2D;
+
+	var generationRequested : Bool = true;
+	var previewRefreshRequested : Bool = true;
+
+	var selectedNodes : Array<SubstanceNode>;
+
+	override function onDisplay() {
+		super.onDisplay();
+		element.html("");
+		element.addClass("substance-editor");
+		substanceGraph = cast hide.Ide.inst.loadPrefab(state.path, null,  true);
+		previewShaderAlpha = new GraphEditor.PreviewShaderAlpha();
+
+		if (graphEditor != null)
+			graphEditor.remove();
+
+		graphEditor = new hide.view.GraphEditor(config, this, this.element);
+		graphEditor.onDisplay();
+
+		var rightPanel = new Element(
+			'<div id="right-panel">
+				<div class="group">
+					<div class="header">
+						<div class="icon ico ico-caret-down"></div>
+						<span class="title">Base parameters</span>
+					</div>
+					<div class="content" id="base-parameters"></div>
+				</div>
+				<div class="group">
+					<div class="header">
+						<div class="icon ico ico-caret-down"></div>
+						<span class="title">Specific parameters</span>
+					</div>
+					<div class="content" id="specific-parameters"></div>
+				</div>
+			</div>'
+		);
+
+		var baseParameters = rightPanel.find("#base-parameters");
+		var outputSize = new Element('
+		<h2 class="title">Output size</h2>
+		<div class="fields grid-3">
+			<label>Width</label>
+			<input type="number" id="output-width" title="Width of the output texture (in px)"/>
+			<label>Height</label>
+			<input type="number" id="output-height" title="Height of the output texture (in px)"/>
+		</div>');
+		outputSize.appendTo(baseParameters);
+
+		var outputFormat = new Element('
+		<h2 class="title">Output format</h2>
+		<div class="fields grid-3">
+			<label>Format</label>
+			<select name="formats" id="output-format"/>
+				${ [for (f in Type.allEnums(hxd.PixelFormat)) '<option value="${f.getIndex()}">${f.getName()}</option>'].join("")}
+			</select>
+		</div>');
+		outputFormat.appendTo(baseParameters);
+
+		function addResetButton(fieldEl : Element, fieldName : String) {
+			var resetEl = new Element('<div class="reset icon ico ico-ban" title="Reset value to default (graph global value)"></div>');
+			resetEl.insertAfter(fieldEl);
+
+			resetEl.on("click", function() {
+				Reflect.deleteField(this.selectedNodes[0].overrides, fieldName);
+				Reflect.setField(this.selectedNodes[0], fieldName, Reflect.field(this.substanceGraph, fieldName));
+				resetEl.css({ visibility : "hidden", "pointers-event" : "none"});
+				updateParameters(this.selectedNodes[0]);
+				generate();
+			});
+
+			if (this.selectedNodes == null || this.selectedNodes.length <= 0 || !Reflect.hasField(this.selectedNodes[0].overrides, fieldName)) {
+				resetEl.css({ visibility : "hidden", "pointers-event" : "none"});
+				return;
+			}
+		}
+
+		addResetButton(rightPanel.find("#output-width"), "outputWidth");
+		addResetButton(rightPanel.find("#output-height"), "outputHeight");
+		addResetButton(rightPanel.find("#output-format"), "outputFormat");
+
+		var headers = rightPanel.find(".header");
+		headers.on("click", function(e) {
+			var header = new Element(e.target).closest(".header");
+			var content = header.siblings(".content");
+
+			if (content.css("display") == "none") {
+				content.css({ display: "block" });
+				header.find(".icon").removeClass("ico-caret-right").addClass("ico-caret-down");
+			}
+			else {
+				content.css({ display: "none" });
+				header.find(".icon").removeClass("ico-caret-down").addClass("ico-caret-right");
+			}
+		});
+
+		rightPanel.find("#specific-parameters").prev(".header").css({ display : "none" });
+
+		rightPanel.appendTo(element);
+		updateParameters(null);
+
+		// Preview
+		if (previewScene != null)
+			previewScene.element.remove();
+
+		var texPreview = new Element(
+			'<div id="tex-preview">
+			</div>'
+		);
+
+		previewScene = new hide.comp.Scene(config, null, texPreview);
+		previewScene.onReady = onPreviewSceneReady;
+		previewScene.onUpdate = onPreviewSceneUpdate;
+
+		texPreview.appendTo(graphEditor.element);
+
+		var toolbar = new Element('<div class="hide-toolbar2"></div>').appendTo(texPreview);
+		var group = new Element('<div class="tb-group"></div>').appendTo(toolbar);
+		var menu = new Element('<div class="button2 transparent" title="More options"><div class="ico ico-navicon"></div></div>');
+		menu.appendTo(group);
+		menu.click((e) -> {
+			var menu = new hide.comp.ContextMenu([
+				{ label: "Center preview", click: centerPreviewCamera }
+			]);
+		});
+
+		graphEditor.onPreviewUpdate = onPreviewUpdate;
+		graphEditor.onNodePreviewUpdate = onNodePreviewUpdate;
+		graphEditor.onSelectionChanged = onSelectionChanged;
+	}
+
+	override function getDefaultContent() : haxe.io.Bytes {
+		var p = (new hrt.sbsgraph.SubstanceGraph(null, null)).serialize();
+		return haxe.io.Bytes.ofString(ide.toJSON(p));
+	}
+
+	override function save() {
+		var content = substanceGraph.saveToText();
+		currentSign = ide.makeSignature(content);
+		sys.io.File.saveContent(getPath(), content);
+		super.save();
+	}
+
+	public function onPreviewSceneReady() {
+		if (previewScene.s3d == null || previewScene.s2d == null)
+			throw "Preview scene not ready";
+
+		camController = new hide.view.l3d.CameraController2D(previewScene.s2d);
+
+		var tile = h2d.Tile.fromColor(SubstanceEditor.DEFAULT_PREVIEW_COLOR);
+		previewBitmap = new h2d.Bitmap(tile, previewScene.s2d);
+
+		centerPreviewCamera();
+	}
+
+	public function onPreviewSceneUpdate(dt: Float) {
+		if (!previewRefreshRequested)
+			return;
+
+		previewRefreshRequested = false;
+
+		var outputNodes = substanceGraph.getOutputNodes();
+		if (outputNodes == null || outputNodes.length <= 0)
+			return;
+
+		var outputs = substanceGraph.cachedOutputs.get(outputNodes[0].id);
+		if (outputs == null || outputs.length <= 0 || outputs[0] == null || previewBitmap == null)
+			return;
+
+		try {
+			var tex = Std.downcast(outputs[0], h3d.mat.Texture);
+			var pixels = tex.capturePixels();
+			var outputTexture = new h3d.mat.Texture(tex.width, tex.height, substanceGraph.outputFormat);
+			outputTexture.uploadPixels(pixels);
+			previewBitmap.tile = h2d.Tile.fromTexture(outputTexture);
+
+			centerPreviewCamera();
+		}
+		catch(e) Ide.inst.quickError("Can't create 2D preview");
+	}
+
+	public function onPreviewUpdate() : Bool {
+		checkGeneration();
+
+		@:privateAccess
+		{
+			var engine = graphEditor.previewsScene.engine;
+			var t = engine.getCurrentTarget();
+			graphEditor.previewsScene.s2d.ctx.globals.set("global.pixelSize", new h3d.Vector(2 / (t == null ? engine.width : t.width), 2 / (t == null ? engine.height : t.height)));
+		}
+
+		@:privateAccess
+		if (previewScene.s2d != null) {
+			previewScene.s2d.ctx.time = graphEditor.previewsScene.s2d.ctx.time;
+		}
+
+		return true;
+	}
+
+	public function onNodePreviewUpdate(node: IGraphNode, bitmap: h2d.Bitmap) @:privateAccess {
+		if (initializedPreviews.get(bitmap) == null) {
+			bitmap.addShader(previewShaderAlpha);
+			initializedPreviews.set(bitmap, true);
+		}
+
+		if (substanceGraph.cachedOutputs != null) {
+			var outputs = substanceGraph.cachedOutputs.get(node.id);
+
+			if (outputs == null || outputs.length <= 0)
+				return;
+
+			bitmap.tile = h2d.Tile.fromTexture(outputs[0]);
+		}
+	}
+
+	public function onSelectionChanged(selectedNodes: Array<IGraphNode>) {
+		this.selectedNodes = cast selectedNodes;
+
+		// If there's no selection, show graph editor parameters
+		if (this.selectedNodes.length == 0) {
+			updateParameters(null);
+
+			element.find("#specific-parameters").empty();
+			element.find("#specific-parameters").prev(".header").css({ display : "none" });
+			return;
+		}
+
+		// If there's only one node selected, show its parameters
+		if (this.selectedNodes.length == 1) {
+			updateParameters(this.selectedNodes[0]);
+
+			var el = this.selectedNodes[0].getSpecificParametersHTML();
+			if (el != null) {
+				element.find("#specific-parameters").append(el);
+				element.find("#specific-parameters").prev(".header").css({ display : "flex" });
+			}
+
+			return;
+		}
+	}
+
+	public function getNodes():Iterator<IGraphNode> {
+		return substanceGraph.nodes.iterator();
+	}
+
+	public function getEdges():Iterator<Edge> {
+		var edges : Array<Edge> = [];
+		for (id => node in substanceGraph.nodes) {
+			for (inputId => connection in node.connections) {
+				if (connection != null) {
+					edges.push(
+						{
+							nodeFromId: connection.from.id,
+							outputFromId: connection.outputId,
+							nodeToId: id,
+							inputToId: inputId,
+						});
+				}
+			}
+		}
+		return edges.iterator();
+	}
+
+	public function getAddNodesMenu():Array<AddNodeMenuEntry> {
+		var entries : Array<AddNodeMenuEntry> = [];
+		var id = 0;
+		for (i => node in hrt.sbsgraph.SubstanceNode.registeredNodes) {
+			var metas = haxe.rtti.Meta.getType(node);
+			if (metas.group == null) {
+				continue;
+			}
+
+			var group = metas.group != null ? metas.group[0] : "Other";
+			var name = metas.name != null ? metas.name[0] : "unknown";
+			var description = metas.description != null ? metas.description[0] : "";
+
+			entries.push(
+				{
+					name: name,
+					group: group,
+					description: description,
+					onConstructNode: () -> {
+						@:privateAccess var id = hrt.sbsgraph.SubstanceGraph.CURRENT_NODE_ID++;
+						var inst = std.Type.createInstance(node, []);
+						inst.id = id;
+						return inst;
+					},
+				}
+			);
+		}
+
+		return entries;
+	}
+
+	public function addNode(node: IGraphNode) {
+		substanceGraph.addNode(cast node);
+		generate();
+	}
+
+	public function removeNode(id:Int) {
+		substanceGraph.removeNode(id);
+		generate();
+	}
+
+	public function serializeNode(node: IGraphNode):Dynamic {
+		return (cast node:SubstanceNode).serializeToDynamic();
+	}
+
+	public function unserializeNode(data:Dynamic, newId:Bool): IGraphNode {
+		var node = SubstanceNode.createFromDynamic(data, substanceGraph);
+		if (newId) {
+			@:privateAccess var newId = hrt.sbsgraph.SubstanceGraph.CURRENT_NODE_ID++;
+			node.id = newId;
+		}
+		return node;
+	}
+
+	public function createCommentNode():Null<IGraphNode> {
+		var node = new hrt.sbsgraph.nodes.Comment();
+		node.comment = "Comment";
+		@:privateAccess var newId = hrt.sbsgraph.SubstanceGraph.CURRENT_NODE_ID++;
+		node.id = newId;
+		return node;
+	}
+
+	public function canAddEdge(edge: Edge):Bool {
+		return substanceGraph.canAddEdge({ outputNodeId: edge.nodeFromId, outputId: edge.outputFromId, inputNodeId: edge.nodeToId, inputId: edge.inputToId });
+	}
+
+	public function addEdge(edge: Edge) {
+		var input = substanceGraph.nodes.get(edge.nodeToId);
+		input.connections[edge.inputToId] = {from: substanceGraph.nodes.get(edge.nodeFromId), outputId: edge.outputFromId};
+		generate();
+	}
+
+	public function removeEdge(nodeToId:Int, inputToId:Int) {
+		var input = substanceGraph.nodes.get(nodeToId);
+		input.connections[inputToId] = null;
+		generate();
+	}
+
+	public function getUndo() : hide.ui.UndoHistory {
+		return undo;
+	}
+
+	public function generate() {
+		generationRequested = true;
+	}
+
+	public function refreshPreview() {
+		previewRefreshRequested = true;
+	}
+
+
+	function checkGeneration() {
+		if (!generationRequested)
+			return;
+
+		generationRequested = false;
+		substanceGraph.generate();
+
+		refreshPreview();
+	}
+
+	function centerPreviewCamera() {
+		var tile = previewBitmap.tile;
+
+		var ratio = tile.width > tile.height ? tile.width / tile.height : tile.width / tile.height;
+		previewBitmap.width = tile.width > tile.height ? SubstanceEditor.DEFAULT_PREVIEW_SIZE : SubstanceEditor.DEFAULT_PREVIEW_SIZE * ratio;
+		previewBitmap.height = tile.height > tile.width ? SubstanceEditor.DEFAULT_PREVIEW_SIZE  : SubstanceEditor.DEFAULT_PREVIEW_SIZE * ratio;
+
+		@:privateAccess camController.targetPos.set(previewBitmap.height / 2, previewBitmap.width / 2, (1 / previewBitmap.width) * 300);
+	}
+
+	function updateParameters(node: SubstanceNode) {
+		function updateResetButtonVisibility(el : Element, fieldName : String) {
+			var resetEl = el.next(".reset");
+			if (node == null)
+				resetEl.css({ visibility : "hidden", "pointers-event" : "none" });
+			else {
+				if (Reflect.hasField(node.overrides, fieldName))
+					resetEl.css({ visibility : "visible", "pointers-event" : "auto" });
+				else
+					resetEl.css({ visibility : "hidden", "pointers-event" : "none" });
+			}
+		}
+
+		var outputWidth = element.find("#output-width");
+		outputWidth.val(node == null ? substanceGraph.outputWidth : node.outputWidth);
+		outputWidth.off();
+		outputWidth.on("change", function() {
+			var v = Std.parseInt(outputWidth.val());
+			if (node == null) {
+				substanceGraph.outputWidth = v;
+			}
+			else {
+				node.outputWidth = v;
+				Reflect.setField(node.overrides, "outputWidth", v);
+			}
+			updateResetButtonVisibility(outputWidth, "outputWidth");
+			generate();
+		});
+		updateResetButtonVisibility(outputWidth, "outputWidth");
+
+		var outputHeight = element.find("#output-height");
+		outputHeight.val(node == null ? substanceGraph.outputHeight : node.outputHeight);
+		outputHeight.off();
+		outputHeight.on("change", function() {
+			var v = Std.parseInt(outputHeight.val());
+			if (node == null) {
+				substanceGraph.outputHeight = v;
+			}
+			else {
+				node.outputHeight = v;
+				Reflect.setField(node.overrides, "outputHeight", v);
+			}
+			updateResetButtonVisibility(outputHeight, "outputHeight");
+			generate();
+		});
+		updateResetButtonVisibility(outputHeight, "outputHeight");
+
+		var outputFormat = element.find("#output-format");
+		outputFormat.val(node == null ? substanceGraph.outputFormat.getIndex() : node.outputFormat.getIndex());
+		outputFormat.off();
+		outputFormat.on("change", function() {
+			var v = hxd.PixelFormat.createByIndex(Std.parseInt(outputFormat.val()));
+			if (node == null) {
+				substanceGraph.outputFormat = v;
+			}
+			else {
+				node.outputFormat = v;
+				Reflect.setField(node.overrides, "outputFormat", v);
+			}
+			updateResetButtonVisibility(outputFormat, "outputFormat");
+			generate();
+		});
+		updateResetButtonVisibility(outputFormat, "outputFormat");
+	}
+
+	static var _ = FileTree.registerExtension(SubstanceEditor, ["sbsgraph"], { icon : "scribd", createNew: "Substance Graph" });
+}

+ 215 - 0
hrt/sbsgraph/Macros.hx

@@ -0,0 +1,215 @@
+package hrt.sbsgraph;
+
+import haxe.macro.Context;
+import haxe.macro.Expr;
+import hxsl.Ast;
+using hxsl.Ast;
+using haxe.macro.Tools;
+
+class Macros {
+	#if macro
+	// static function buildNode() {
+	// 	var fields = Context.getBuildFields();
+	// 	for (f in fields) {
+	// 		if (f.name == "SRC") {
+	// 			switch (f.kind) {
+	// 				case FVar(_, expr) if (expr != null):
+	// 					var pos = expr.pos;
+	// 					if( !Lambda.has(f.access, AStatic) ) f.access.push(AStatic);
+	// 					Context.getLocalClass().get().meta.add(":src", [expr], pos);
+	// 					try {
+	// 						var c = Context.getLocalClass();
+
+	// 						// function map(e: haxe.macro.Expr) {
+	// 						// 	switch(e) {
+	// 						// 		case EMeta("sginput", args, e):
+	// 						// 			trace("sginput");
+	// 						// 	}
+	// 						// }
+
+	// 						// expr.map()
+
+	// 						var varmap : Map<String, hrt.shgraph.SgHxslVar> = [];
+
+	// 						function iter(e: haxe.macro.Expr) : Void {
+	// 							switch(e.expr) {
+	// 								case EMeta(meta, subexpr):
+	// 									switch (meta.name) {
+	// 										case "sginput":
+	// 											var defValue : hrt.shgraph.SgHxslVar.ShaderDefInput = null;
+	// 											if (meta.params != null && meta.params.length > 0) {
+	// 												switch (meta.params[0].expr) {
+	// 													case EConst(v):
+	// 														switch(v) {
+	// 															case CIdent(name):
+	// 																defValue = Var(name);
+	// 															case CString(val):
+	// 																defValue = Var(val);
+	// 															case CFloat(val), CInt(val):
+	// 																defValue = Const(Std.parseFloat(val));
+	// 															default:
+	// 																throw "sginput default param must be an identifier or a integer";
+	// 														}
+	// 													default:
+	// 														trace(meta.params[0].expr);
+	// 														throw "sginput default param must be a constant value";
+	// 												}
+	// 											}
+
+	// 											switch(subexpr.expr) {
+	// 												case EVars(vars):
+	// 													for (v in vars) {
+	// 															var isDynamic = false;
+	// 															switch (v.type) {
+	// 																case TPath(p) if (p.name == "Dynamic"):
+	// 																	isDynamic = true;
+	// 																	p.name = "Vec4"; // Convert dynamic value back to vec4 as a hack
+	// 																default:
+	// 															}
+	// 															varmap.set(v.name, SgInput(isDynamic, defValue));
+	// 														}
+	// 														e.expr = subexpr.expr;
+	// 												default:
+	// 													throw "sginput must be used with variables only";
+	// 											}
+	// 										case "sgoutput":
+	// 											switch(subexpr.expr) {
+	// 												case EVars(vars):
+	// 													for (v in vars) {
+	// 														var isDynamic = false;
+	// 														switch (v.type) {
+	// 															case TPath(p) if (p.name == "Dynamic"):
+	// 																isDynamic = true;
+	// 																p.name = "Vec4"; // Convert dynamic value back to vec4 as a hack
+	// 															default:
+	// 														}
+
+	// 														varmap.set(v.name, SgOutput(isDynamic));
+	// 													}
+	// 													e.expr = subexpr.expr;
+	// 												default:
+	// 													throw "sgoutput must be used with variables only";
+	// 											}
+	// 										case "sgconst":
+	// 											switch(subexpr.expr) {
+	// 												case EVars(vars):
+	// 													for (v in vars) {
+	// 														varmap.set(v.name, SgConst);
+	// 													}
+	// 													e.expr = subexpr.expr;
+	// 												default:
+	// 													throw "sgconst must be used with variables only";
+	// 											}
+	// 										default:
+	// 									}
+	// 								default:
+	// 							}
+	// 						}
+
+	// 						expr.iter(iter);
+	// 						var shader = new hxsl.MacroParser().parseExpr(expr);
+	// 						f.kind = FVar(null, macro @:pos(pos) $v{shader});
+	// 						var name = Std.string(c);
+
+	// 						var check = new hxsl.Checker();
+	// 						check.warning = function(msg,pos) {
+	// 							haxe.macro.Context.warning(msg, pos);
+	// 						};
+
+	// 						check.loadShader = loadShader;
+
+	// 						var shader = check.check(name, shader);
+	// 						//trace(shader);
+	// 						//Printer.check(shader);
+	// 						var serializer = new hxsl.Serializer();
+	// 						var str = Context.defined("display") ? "" : serializer.serialize(shader);
+	// 						f.kind = FVar(null, { expr : EConst(CString(str)), pos : pos } );
+	// 						f.meta.push({
+	// 							name : ":keep",
+	// 							pos : pos,
+	// 						});
+
+	// 						function makeField(name: String, arr: Array<String>) : Field
+	// 						{
+	// 							return {
+	// 								name: name,
+	// 								access: [APublic, AStatic],
+	// 								kind: FVar(macro : Array<String>, macro $v{arr}),
+	// 								pos: f.pos,
+	// 								meta: [{
+	// 										name : ":keep",
+	// 										pos : pos,}
+	// 								],
+	// 							};
+	// 						}
+
+	// 						var finalMap : Map<Int, hrt.shgraph.SgHxslVar> = [];
+
+	// 						for (v in shader.vars) {
+	// 							var info = varmap.get(v.name);
+	// 							if (info != null) {
+	// 								var serId = @:privateAccess serializer.idMap.get(v.id);
+	// 								finalMap.set(serId, info);
+	// 							}
+	// 						}
+
+	// 						fields.push(
+	// 						{
+	// 							name: "_variablesInfos",
+	// 							access: [APublic, AStatic],
+	// 							kind: FVar(macro : Map<Int, hrt.shgraph.SgHxslVar>, macro $v{finalMap}),
+	// 							pos: f.pos,
+	// 							meta: [{
+	// 									name : ":keep",
+	// 									pos : pos,}
+	// 							],
+	// 						}
+	// 						);
+
+
+	// 					} catch( e : hxsl.Ast.Error ) {
+	// 						fields.remove(f);
+	// 						Context.error(e.msg, e.pos);
+	// 					}
+	// 				default:
+	// 			}
+	// 		}
+	// 	}
+	// 	return fields;
+	// }
+
+	// static function loadShader( path : String ) {
+	// 	var m = Context.follow(Context.getType(path));
+	// 	switch( m ) {
+	// 	case TInst(c, _):
+	// 		var c = c.get();
+	// 		for( m in c.meta.get() )
+	// 			if( m.name == ":src" )
+	// 				return new hxsl.MacroParser().parseExpr(m.params[0]);
+	// 	default:
+	// 	}
+	// 	throw path + " is not a shader";
+	// 	return null;
+	// }
+
+	static function autoRegisterNode() {
+		var fields = Context.getBuildFields();
+
+		var thisClass = Context.getLocalClass();
+		var cl = thisClass.get();
+		var clPath = cl.pack.copy();
+		clPath.push(cl.name);
+
+		#if editor
+		fields.push({
+			name: "_",
+			access: [Access.AStatic],
+			kind: FieldType.FVar(macro:Bool, macro SubstanceNode.register($v{cl.name}, ${clPath.toFieldExpr()})),
+			pos: Context.currentPos(),
+		});
+		#end
+
+		return fields;
+	}
+	#end
+}

+ 251 - 0
hrt/sbsgraph/SubstanceGraph.hx

@@ -0,0 +1,251 @@
+package hrt.sbsgraph;
+
+typedef Edge = {
+	?inputNodeId : Int,
+	?nameInput : String, // Fallback if name has changed
+	?inputId : Int,
+	?outputNodeId : Int,
+	?nameOutput : String, // Fallback if name has changed
+	?outputId : Int,
+};
+
+typedef Connection = {
+	from : SubstanceNode,
+	outputId : Int,
+};
+
+class SubstanceGraph extends hrt.prefab.Prefab {
+	public static var CURRENT_NODE_ID = 0;
+
+	public var cachedOutputs : Map<Int, Array<h3d.mat.Texture>> = [];
+	public var nodes : Map<Int, SubstanceNode> = [];
+
+	// Base parameters that can be overrided by nodes
+	@:s public var outputHeight = 256;
+	@:s public var outputWidth = 256;
+	@:s public var outputFormat = hxd.PixelFormat.RGBA;
+
+	override function save() {
+		var json = super.save();
+		Reflect.setField(json, "graph", saveToDynamic());
+		return json;
+	}
+
+	override function load(json : Dynamic) : Void {
+		super.load(json);
+
+		nodes = [];
+		CURRENT_NODE_ID = 0;
+
+		var graphJson = Reflect.getProperty(json, "graph");
+
+		var nodesJson : Array<Dynamic> = Reflect.getProperty(graphJson, "nodes");
+		for (n in nodesJson) {
+			var node = SubstanceNode.createFromDynamic(n, this);
+			this.nodes.set(node.id, node);
+			CURRENT_NODE_ID = hxd.Math.imax(CURRENT_NODE_ID, node.id+1);
+		}
+
+		var edgesJson : Array<Dynamic> = Reflect.getProperty(graphJson, "edges");
+		for (e in edgesJson) {
+			addEdge(e);
+		}
+	}
+
+	override function copy(other: hrt.prefab.Prefab) : Void {
+		throw "Substance graph is not meant to be put in a prefab tree. Use a dynamic shader that references this shadergraph instead";
+	}
+
+	public function saveToDynamic() : Dynamic {
+		var edgesJson : Array<Edge> = [];
+		for (n in nodes) {
+			for (inputId => connection in n.connections) {
+				if (connection == null) continue;
+				var outputId = connection.outputId;
+				edgesJson.push({ outputNodeId: connection.from.id, nameOutput: connection.from.getOutputs()[outputId].name, inputNodeId: n.id, nameInput: n.getInputs()[inputId].name, inputId: inputId, outputId: outputId });
+			}
+		}
+
+		var json = {
+			nodes: [
+				for (n in nodes) n.serializeToDynamic(),
+				],
+				edges: edgesJson
+			};
+
+		return json;
+	}
+
+	public function saveToText() : String {
+		return haxe.Json.stringify(save(), "\t");
+	}
+
+
+	public function addEdge(edge : Edge) {
+		var node = this.nodes.get(edge.inputNodeId);
+		var output = this.nodes.get(edge.outputNodeId);
+
+		var inputs = node.getInputs();
+		var outputs = output.getOutputs();
+
+		var outputId = edge.outputId;
+		var inputId = edge.inputId;
+
+		{
+			// Check if there is an output with that id and if it has the same name
+			// else try to find the id of another output with the same name
+			var output = outputs[outputId];
+			if (output == null || output.name != edge.nameOutput) {
+				for (id => o in outputs) {
+					if (o.name == edge.nameOutput) {
+						outputId = id;
+						break;
+					}
+				}
+			};
+		}
+
+		{
+			var input = inputs[inputId];
+			if (input == null || input.name != edge.nameInput) {
+				for (id => i in inputs) {
+					if (i.name == edge.nameInput) {
+						inputId = id;
+						break;
+					}
+				}
+			}
+		}
+
+		node.connections[inputId] = { from: output, outputId: outputId };
+
+		return true;
+	}
+
+	public function removeEdge(idNode : Int, inputId : Int, update : Bool = true) {
+		var node = this.nodes.get(idNode);
+		if (node.connections[inputId] == null) return;
+
+		node.connections[inputId] = null;
+	}
+
+	public function hasCycle() {
+		var visited : Array<Bool> = [for (n in nodes) false];
+		var recStack : Array<Bool> = [for (n in nodes) false];
+
+		function hasCycleUtil(current : Int, visited : Array<Bool>, recStack: Array<Bool>) {
+			if (recStack[current])
+				return true;
+
+			if (visited[current])
+				return false;
+
+			visited[current] = true;
+			recStack[current] = true;
+
+			var children: Array<Int> = [];
+			for (c in nodes[current].connections) {
+				for (idx => n in nodes) {
+					if (n == c.from)
+						children.push(idx);
+				}
+			}
+
+			for (idx in children) {
+				if (hasCycleUtil(idx, visited, recStack))
+					return true;
+			}
+
+			recStack[current] = false;
+			return false;
+		}
+
+		for (idx => n in nodes)
+			if (hasCycleUtil(idx, visited, recStack))
+				return true;
+
+		return false;
+	}
+
+	public function canAddEdge(edge : Edge) {
+		var node = this.nodes.get(edge.inputNodeId);
+		var output = this.nodes.get(edge.outputNodeId);
+
+		var inputs = node.getInputs();
+		var outputs = output.getOutputs();
+
+		var inputType = inputs[edge.inputId].type;
+		var outputType = outputs[edge.outputId].type;
+
+		// Temp add edge to check for cycle, remove it after
+		var prev = node.connections[edge.inputId];
+		node.connections[edge.inputId] = {from: output, outputId: edge.outputId};
+		var res = hasCycle();
+		node.connections[edge.inputId] = prev;
+
+		if(res)
+			return false;
+
+		return true;
+	}
+
+	public function addNode(sbsNode : SubstanceNode) {
+		this.nodes.set(sbsNode.id, sbsNode);
+	}
+
+	public function removeNode(idNode : Int) {
+		this.nodes.remove(idNode);
+	}
+
+	public function getOutputNodes() : Array<hrt.sbsgraph.nodes.SubstanceOutput> {
+		var outputNodes : Array<hrt.sbsgraph.nodes.SubstanceOutput> = [];
+		for (n in nodes) {
+			if (Std.downcast(n, hrt.sbsgraph.nodes.SubstanceOutput) != null)
+				outputNodes.push(cast n);
+		}
+
+		return outputNodes;
+	}
+
+
+	public function generate() : Map<String, h3d.mat.Texture> {
+		var engine = h3d.Engine.getCurrent();
+		if (engine == null)
+			return null;
+
+		cachedOutputs = [];
+
+		function generateNode(node : SubstanceNode) {
+			for (c in node.connections) {
+				if (c != null && cachedOutputs.get(c.from.id) == null)
+					generateNode(c.from);
+			}
+
+			// Apply parameters before generation
+			node.outputFormat = outputFormat;
+			node.outputHeight = outputHeight;
+			node.outputWidth = outputWidth;
+
+			for (f in Reflect.fields(node.overrides))
+				Reflect.setField(node, f, Reflect.field(node.overrides, f));
+
+			var outputs = node.apply(cachedOutputs);
+			cachedOutputs.set(node.id, outputs);
+		}
+
+		for (n in nodes) {
+			if (cachedOutputs.get(n.id) != null)
+				continue;
+
+			generateNode(n);
+		}
+
+		var outputs : Map<String, h3d.mat.Texture> = [];
+		for (o in getOutputNodes())
+			outputs.set(o.label, cachedOutputs.get(o.id)[0]);
+
+		return outputs;
+	}
+
+	static var _ = hrt.prefab.Prefab.register("sbsgraph", SubstanceGraph, "sbsgraph");
+}

+ 217 - 0
hrt/sbsgraph/SubstanceNode.hx

@@ -0,0 +1,217 @@
+package hrt.sbsgraph;
+
+import Type as HaxeType;
+import hrt.sbsgraph.SubstanceGraph;
+#if editor
+import hide.view.GraphInterface;
+#end
+
+using Lambda;
+
+typedef AliasInfo = { ?nameSearch: String, ?nameOverride : String, ?description : String, ?args : Array<Dynamic>, ?group: String };
+
+@:autoBuild(hrt.sbsgraph.Macros.autoRegisterNode())
+@:keepSub
+@:keep
+class SubstanceNode
+#if editor
+implements hide.view.GraphInterface.IGraphNode
+#end
+{
+	public static var registeredNodes = new Map<String, Class<SubstanceNode>>();
+
+	public var id : Int;
+	public var connections : Array<SubstanceGraph.Connection> = [];
+	public var defaults : Dynamic = {};
+	#if editor
+	public var editor : hide.view.GraphEditor;
+	#end
+	public var x : Float;
+	public var y : Float;
+	public var showPreview : Bool = true;
+	@prop public var nameOverride : String;
+
+	// Base parameters that are overriding graph's parameters
+	public var overrides = {};
+	public var outputHeight = 256;
+	public var outputWidth = 256;
+	public var outputFormat = hxd.PixelFormat.RGBA;
+
+
+	static public function register(key : String, cl : Class<SubstanceNode>) : Bool {
+		registeredNodes.set(key, cl);
+		return true;
+	}
+
+	#if editor
+	public function getInfo() : GraphNodeInfo {
+		var metas = haxe.rtti.Meta.getType(HaxeType.getClass(this));
+		return {
+			name: nameOverride ?? (metas.name != null ? metas.name[0] : "undefined"),
+			inputs: [
+				for (i in getInputs()) {
+					{
+						name: i.name,
+						color: SubstanceNode.getTypeColor(i.type),
+					}
+				}
+			],
+			outputs: [
+				for (o in getOutputs()) {
+					{
+						name: o.name,
+						color: SubstanceNode.getTypeColor(o.type),
+					}
+				}
+			],
+			preview: {
+				getVisible: () -> showPreview,
+				setVisible: (b:Bool) -> showPreview = b,
+				fullSize: false,
+			},
+			width: metas.width != null ? metas.width[0] : null,
+			noHeader: Reflect.hasField(metas, "noheader"),
+		};
+	}
+
+	public function getPos(p : h2d.col.Point) : Void {
+		p.set(x,y);
+	}
+
+	public function setPos(p : h2d.col.Point) : Void {
+		x = p.x;
+		y = p.y;
+	}
+
+	public function getPropertiesHTML(width : Float) : Array<hide.Element> {
+		return [];
+	}
+	#end
+
+	public static function createFromDynamic(data: Dynamic, graph: SubstanceGraph) : SubstanceNode {
+		var type = std.Type.resolveClass(data.type);
+		var inst = std.Type.createInstance(type, []);
+		inst.x = data.x;
+		inst.y = data.y;
+		inst.id = data.id;
+		inst.connections = [];
+		inst.loadProperties(data.properties);
+		return inst;
+	}
+
+	static function getTypeColor(type : Dynamic ) : Int {
+		return switch (type) {
+			case h3d.mat.Texture:
+				0xffaf41;
+			default:
+				0xc8c8c8;
+		}
+	}
+
+	public function serializeToDynamic() : Dynamic {
+		return {
+			x: x,
+			y: y,
+			id: id,
+			type: std.Type.getClassName(std.Type.getClass(this)),
+			properties: saveProperties(),
+		};
+	}
+
+	public function loadProperties(props : Dynamic) {
+		var fields = Reflect.fields(props);
+		showPreview = props.showPreview ?? true;
+		nameOverride = props.nameOverride;
+		overrides = props.overrides != null ? props.overrides : {};
+
+		for (f in fields) {
+			if (f == "defaults") {
+				defaults = Reflect.field(props, f);
+			}
+			else {
+				if (Reflect.hasField(this, f)) {
+					Reflect.setField(this, f, Reflect.field(props, f));
+				}
+			}
+		}
+	}
+
+	public function saveProperties() : Dynamic {
+		var parameters : Dynamic = {};
+
+		var thisClass = std.Type.getClass(this);
+		var fields = std.Type.getInstanceFields(thisClass);
+		var metas = haxe.rtti.Meta.getFields(thisClass);
+		var metaSuperClass = haxe.rtti.Meta.getFields(std.Type.getSuperClass(thisClass));
+
+		for (f in fields) {
+			var m = Reflect.field(metas, f);
+			if (m == null) {
+				m = Reflect.field(metaSuperClass, f);
+				if (m == null)
+					continue;
+			}
+			if (Reflect.hasField(m, "prop")) {
+				var metaData : Array<String> = Reflect.field(m, "prop");
+				if (metaData == null || metaData.length == 0 || metaData[0] != "macro") {
+					Reflect.setField(parameters, f, Reflect.getProperty(this, f));
+				}
+			}
+		}
+
+		if (Reflect.fields(defaults).length > 0) {
+			Reflect.setField(parameters, "defaults", defaults);
+		}
+
+		parameters.nameOverride = nameOverride;
+		parameters.showPreview = showPreview;
+		parameters.overrides = overrides;
+
+		return parameters;
+	}
+
+	public function getInputs() : Array<Dynamic> {
+		return Reflect.field(this, "inputs")??[];
+	}
+
+	public function getOutputs() : Array<Dynamic> {
+		return Reflect.field(this, "outputs")??[];
+	}
+
+	public function apply(vars : Dynamic) : Array<h3d.mat.Texture> {
+		return [];
+	}
+
+	public function getInputData(vars : Dynamic, inputIdx : Int) : Dynamic {
+		if (connections.length <= inputIdx || connections[inputIdx] == null)
+			return getDefaultInputData(inputIdx);
+
+		var outputs = vars.get(connections[inputIdx].from.id);
+		if (outputs == null || outputs.length <= connections[inputIdx].outputId)
+			return getDefaultInputData(inputIdx);
+
+		return vars.get(connections[inputIdx].from.id)[connections[inputIdx].outputId];
+	}
+
+	#if editor
+	public function getSpecificParametersHTML() : hide.Element {
+		return null;
+	}
+	#end
+
+	function getDefaultInputData(inputIdx : Int) {
+		var input = getInputs()[inputIdx];
+
+		switch (input.type) {
+			case h3d.mat.Texture:
+				return new h3d.mat.Texture(1, 1);
+			default:
+				return null;
+		}
+	}
+
+	function createTexture() : h3d.mat.Texture {
+		var tex = new h3d.mat.Texture(outputWidth, outputHeight, null, outputFormat);
+		return tex;
+	}
+}

+ 98 - 0
hrt/sbsgraph/nodes/BnWNoise.hx

@@ -0,0 +1,98 @@
+package hrt.sbsgraph.nodes;
+
+class BnWSpotsNoiseShader extends h3d.shader.ScreenShader {
+    static var SRC = {
+		@param var tex : Sampler2D;
+		@param var seed : Float;
+		@param var scale : Float;
+
+		function random(inVector : Vec2) : Float {
+			return fract(sin(dot(inVector.xy, vec2(12.9898,78.233))) * 43758.5453123);
+		}
+
+		// 2D Noise based on Morgan McGuire @morgan3d
+		// https://www.shadertoy.com/view/4dS3Wd
+		function noise(inVector : Vec2) : Float {
+			var i = floor(inVector);
+			var f = fract(inVector);
+
+			var a = random(i);
+			var b = random(i + vec2(1.0, 0.0));
+			var c = random(i + vec2(0.0, 1.0));
+			var d = random(i + vec2(1.0, 1.0));
+
+			var u = f*f*(3.0-2.0*f);
+
+			return mix(a, b, u.x) +
+					(c - a)* u.y * (1.0 - u.x) +
+					(d - b) * u.x * u.y;
+		}
+
+        function fragment() {
+			pixelColor = vec4(vec3(noise((uvToScreen(calculatedUV) + seed) * scale)) , 1.);
+        }
+
+    }
+}
+
+@name("BnW Spots Noise")
+@description("Black and white spots noise texture")
+@width(120)
+@group("Texture generation")
+class BnWSpotsNoise extends SubstanceNode {
+	var inputs = [];
+	var outputs = [
+		{ name : "output", type: h3d.mat.Texture }
+	];
+
+	@prop var seed : Float = 0;
+	@prop var scale : Float = 1;
+
+	override function apply(vars : Dynamic) : Array<h3d.mat.Texture> {
+		var engine = h3d.Engine.getCurrent();
+		var out = new h3d.mat.Texture(outputWidth, outputHeight, null, outputFormat);
+
+		var shader = new BnWSpotsNoiseShader();
+		shader.tex = out;
+		shader.seed = seed;
+		shader.scale = scale;
+
+		var pass = new h3d.pass.ScreenFx(shader);
+
+		engine.pushTarget(out);
+		pass.render();
+		engine.popTarget();
+
+		return [ out ];
+	}
+
+	#if editor
+	override function getSpecificParametersHTML() {
+		var el = new hide.Element('
+		<div class="fields">
+			<label>Seed</label>
+			<input type="number" id="seed"/>
+			<label>Scale</label>
+			<input type="number" id="scale"/>
+		</div>');
+
+		var seedEl = el.find("#seed");
+		seedEl.val(seed);
+		seedEl.on("change", function(e) {
+			this.seed = Std.parseFloat(seedEl.val());
+			var substanceEditor = Std.downcast(editor.editor, hide.view.substanceeditor.SubstanceEditor);
+			substanceEditor.generate();
+		});
+
+		var scaleEl = el.find("#scale");
+		scaleEl.val(scale);
+		scaleEl.on("change", function(e) {
+			this.scale = Std.parseFloat(scaleEl.val());
+			var substanceEditor = Std.downcast(editor.editor, hide.view.substanceeditor.SubstanceEditor);
+			substanceEditor.generate();
+		});
+
+		return el;
+	}
+	#end
+}

+ 34 - 0
hrt/sbsgraph/nodes/Comment.hx

@@ -0,0 +1,34 @@
+package hrt.sbsgraph.nodes;
+
+#if editor
+import hide.view.GraphInterface;
+#end
+
+@name("Comment")
+@description("A box that allows you to comment your graph")
+@group("Comment")
+class Comment extends SubstanceNode {
+	@prop() public var comment : String = "Comment";
+	@prop() public var width : Float = 200;
+	@prop() public var height : Float = 200;
+
+	public function new () {}
+
+	override function apply(vars : Dynamic) : Array<h3d.mat.Texture> {
+		return null;
+	}
+
+	#if editor
+	override function getInfo() : GraphNodeInfo {
+		var info = super.getInfo();
+		info.comment = {
+			getComment: () -> comment,
+			setComment: (v:String) -> comment = v,
+			getSize: (p: h2d.col.Point) -> p.set(width, height),
+			setSize: (p: h2d.col.Point) -> { width = p.x; height = p.y; },
+		};
+		info.preview = null;
+		return info;
+	}
+	#end
+}

+ 17 - 0
hrt/sbsgraph/nodes/Disc.hx

@@ -0,0 +1,17 @@
+package hrt.sbsgraph.nodes;
+
+@name("Disc")
+@description("Basic disc texture")
+@width(80)
+@group("Texture generation")
+class Disc extends SubstanceNode {
+	var inputs = [];
+	var outputs = [
+		{ name : "output", type: h3d.mat.Texture }
+	];
+
+	override function apply(vars : Dynamic) : Array<h3d.mat.Texture> {
+		var out = h3d.mat.Texture.genDisc(outputWidth, 16777215, 1);
+		return [ out ];
+	}
+}

+ 44 - 0
hrt/sbsgraph/nodes/Multiply.hx

@@ -0,0 +1,44 @@
+package hrt.sbsgraph.nodes;
+
+class MultiplyShader extends h3d.shader.ScreenShader {
+    static var SRC = {
+		@param var tex1 : Sampler2D;
+		@param var tex2 : Sampler2D;
+
+        function fragment() {
+			pixelColor = tex1.get(calculatedUV) * tex2.get(calculatedUV);
+        }
+    }
+}
+
+@name("Multiply")
+@description("The output is the result of the inputs multiplied")
+@width(80)
+@group("Operation")
+class Multiply extends SubstanceNode {
+	var inputs = [
+		{ name : "input1", type: h3d.mat.Texture },
+		{ name : "input2", type: h3d.mat.Texture },
+	];
+
+	var outputs = [
+		{ name : "output", type: h3d.mat.Texture }
+	];
+
+	override function apply(vars : Dynamic) : Array<h3d.mat.Texture> {
+		var engine = h3d.Engine.getCurrent();
+		var out = createTexture();
+
+		var shader = new MultiplyShader();
+		shader.tex1 = cast getInputData(vars, 0);
+		shader.tex2 = cast getInputData(vars , 1);
+
+		var pass = new h3d.pass.ScreenFx(shader);
+
+		engine.pushTarget(out);
+		pass.render();
+		engine.popTarget();
+
+		return [ out ];
+	}
+}

+ 52 - 0
hrt/sbsgraph/nodes/RGBAMerge.hx

@@ -0,0 +1,52 @@
+package hrt.sbsgraph.nodes;
+
+class RGBAMergeShader extends h3d.shader.ScreenShader {
+    static var SRC = {
+		@param var r : Sampler2D;
+		@param var g : Sampler2D;
+		@param var b : Sampler2D;
+		@param var a : Sampler2D;
+
+        function fragment() {
+			pixelColor.r = r.get(calculatedUV).r;
+			pixelColor.g = g.get(calculatedUV).r;
+			pixelColor.b = b.get(calculatedUV).r;
+			pixelColor.a = a.get(calculatedUV).r;
+        }
+    }
+}
+
+@name("RGBA Merge")
+@description("Merge 4 grayscale images entry in a RGBA image")
+@width(100)
+@group("Channel")
+class RGBAMerge extends SubstanceNode {
+	var inputs = [
+		{ name : "R", type: h3d.mat.Texture },
+		{ name : "G", type: h3d.mat.Texture },
+		{ name : "B", type: h3d.mat.Texture },
+		{ name : "A", type: h3d.mat.Texture }
+	];
+	var outputs = [
+		{ name : "output", type: h3d.mat.Texture }
+	];
+
+	override function apply(vars : Dynamic) : Array<h3d.mat.Texture> {
+		var engine = h3d.Engine.getCurrent();
+		var out = createTexture();
+
+		var shader = new RGBAMergeShader();
+		shader.r = cast getInputData(vars, 0);
+		shader.g = cast getInputData(vars , 1);
+		shader.b = cast getInputData(vars , 2);
+		shader.a = cast getInputData(vars , 3);
+
+		var pass = new h3d.pass.ScreenFx(shader);
+
+		engine.pushTarget(out);
+		pass.render();
+		engine.popTarget();
+
+		return [ out ];
+	}
+}

+ 64 - 0
hrt/sbsgraph/nodes/RGBASplit.hx

@@ -0,0 +1,64 @@
+package hrt.sbsgraph.nodes;
+
+class RGBASplitShader extends h3d.shader.ScreenShader {
+    static var SRC = {
+		@param var tex : Sampler2D;
+		@const var channels : Int;
+
+        function fragment() {
+			switch( channels ) {
+				case 0:
+					pixelColor = vec4(tex.get(calculatedUV).rrr, 1.);
+				case 1:
+					pixelColor = vec4(tex.get(calculatedUV).ggg, 1.);
+				case 2:
+					pixelColor = vec4(tex.get(calculatedUV).bbb, 1.);
+				case 3:
+					pixelColor = vec4(tex.get(calculatedUV).aaa, 1.);
+				default:
+					pixelColor = vec4(0,0,0,0);
+			}
+        }
+    }
+}
+
+@name("RGBA Split")
+@description("Separate a RGBA image entry in 4 grayscale images")
+@width(100)
+@group("Channel")
+class RGBASplit extends SubstanceNode {
+	var inputs = [
+		{ name : "RGBA", type: h3d.mat.Texture }
+	];
+
+	var outputs = [
+		{ name : "R", type: h3d.mat.Texture },
+		{ name : "G", type: h3d.mat.Texture },
+		{ name : "B", type: h3d.mat.Texture },
+		{ name : "A", type: h3d.mat.Texture }
+	];
+
+	override function apply(vars : Dynamic) : Array<h3d.mat.Texture> {
+		var engine = h3d.Engine.getCurrent();
+
+		var r = createTexture();
+		var g = createTexture();
+		var b = createTexture();
+		var a = createTexture();
+
+		var texs = [ r, g, b, a];
+
+		var shader = new RGBASplitShader();
+		shader.tex = cast getInputData(vars, 0);
+
+		var pass = new h3d.pass.ScreenFx(shader);
+		for (idx => t in texs) {
+			shader.channels = idx;
+			engine.pushTarget(t);
+			pass.render();
+			engine.popTarget();
+		}
+
+		return texs;
+	}
+}

+ 41 - 0
hrt/sbsgraph/nodes/SubstanceOutput.hx

@@ -0,0 +1,41 @@
+package hrt.sbsgraph.nodes;
+
+@name("Outputs")
+@description("Parameters outputs")
+@width(80)
+@group("Output")
+@color("#A90707")
+class SubstanceOutput extends SubstanceNode {
+	@prop public var label : String = "base-color";
+
+	var inputs = [
+		{ name : "input", type: h3d.mat.Texture }
+	];
+
+	var outputs = [];
+
+	override function apply(vars : Dynamic) : Array<h3d.mat.Texture> {
+		var out = cast getInputData(vars, 0);
+		return [ out ];
+	}
+
+	#if editor
+	override function getSpecificParametersHTML() {
+		var el = new hide.Element('
+		<div class="fields">
+			<label>Label</label>
+			<input id="label"/>
+		</div>');
+
+		var labelEl = el.find("#label");
+		labelEl.val(label);
+		labelEl.on("change", function(e) {
+			this.label = labelEl.val();
+			var substanceEditor = Std.downcast(editor.editor, hide.view.substanceeditor.SubstanceEditor);
+			substanceEditor.generate();
+		});
+
+		return el;
+	}
+	#end
+}

+ 67 - 0
hrt/sbsgraph/nodes/WhiteNoise.hx

@@ -0,0 +1,67 @@
+package hrt.sbsgraph.nodes;
+
+class WhiteNoiseShader extends h3d.shader.ScreenShader {
+    static var SRC = {
+		@param var tex : Sampler2D;
+		@param var seed : Float;
+
+		function random(inVector : Vec2) : Float {
+			return fract(sin(dot(inVector.xy, vec2(12.9898,78.233))) * 43758.5453123);
+		}
+
+        function fragment() {
+			pixelColor = vec4(vec3(random(uvToScreen(calculatedUV) + seed)) , 1.);
+        }
+
+    }
+}
+
+@name("White Noise")
+@description("White noise texture")
+@width(100)
+@group("Texture generation")
+class WhiteNoise extends SubstanceNode {
+	var inputs = [];
+	var outputs = [
+		{ name : "output", type: h3d.mat.Texture }
+	];
+
+	@prop var seed : Float = 0;
+
+	override function apply(vars : Dynamic) : Array<h3d.mat.Texture> {
+		var engine = h3d.Engine.getCurrent();
+		var out = new h3d.mat.Texture(outputWidth, outputHeight, null, outputFormat);
+
+		var shader = new WhiteNoiseShader();
+		shader.tex = out;
+		shader.seed = seed;
+
+		var pass = new h3d.pass.ScreenFx(shader);
+
+		engine.pushTarget(out);
+		pass.render();
+		engine.popTarget();
+
+		return [ out ];
+	}
+
+	#if editor
+	override function getSpecificParametersHTML() {
+		var el = new hide.Element('
+		<div class="fields">
+			<label>Seed</label>
+			<input type="number" id="seed"/>
+		</div>');
+
+		var seedEl = el.find("#seed");
+		seedEl.val(seed);
+		seedEl.on("change", function(e) {
+			this.seed = Std.parseFloat(seedEl.val());
+			var substanceEditor = Std.downcast(editor.editor, hide.view.substanceeditor.SubstanceEditor);
+			substanceEditor.generate();
+		});
+
+		return el;
+	}
+	#end
+}