浏览代码

Merge pull request #200 from HeapsIO/ft-shadergraph-improvements

Shader graph improvements
trethaller 4 年之前
父节点
当前提交
286b09d3c0
共有 44 个文件被更改,包括 1449 次插入414 次删除
  1. 4 1
      bin/defaultProps.json
  2. 21 2
      bin/style.css
  3. 21 1
      bin/style.less
  4. 6 0
      hide/comp/Component.hx
  5. 91 45
      hide/comp/SceneEditor.hx
  6. 62 82
      hide/view/Graph.hx
  7. 0 10
      hide/view/Prefab.hx
  8. 55 5
      hide/view/shadereditor/Box.hx
  9. 475 149
      hide/view/shadereditor/ShaderEditor.hx
  10. 1 1
      hrt/prefab/Shader.hx
  11. 2 3
      hrt/prefab/ShaderGraph.hx
  12. 0 10
      hrt/prefab/rfx/RendererFX.hx
  13. 265 0
      hrt/prefab/rfx/ScreenShaderGraph.hx
  14. 74 0
      hrt/shgraph/CustomVarChooser.hx
  15. 2 2
      hrt/shgraph/ShaderGlobalInput.hx
  16. 12 5
      hrt/shgraph/ShaderGraph.hx
  17. 1 1
      hrt/shgraph/ShaderNode.hx
  18. 55 5
      hrt/shgraph/ShaderOutput.hx
  19. 15 1
      hrt/shgraph/ShaderParam.hx
  20. 3 3
      hrt/shgraph/nodes/Clamp.hx
  21. 4 2
      hrt/shgraph/nodes/Combine.hx
  22. 60 0
      hrt/shgraph/nodes/CombineAlpha.hx
  23. 4 4
      hrt/shgraph/nodes/Cond.hx
  24. 2 2
      hrt/shgraph/nodes/Cross.hx
  25. 2 2
      hrt/shgraph/nodes/Dot.hx
  26. 2 2
      hrt/shgraph/nodes/Exp.hx
  27. 1 1
      hrt/shgraph/nodes/Floor.hx
  28. 1 1
      hrt/shgraph/nodes/Fract.hx
  29. 3 3
      hrt/shgraph/nodes/IfCondition.hx
  30. 24 0
      hrt/shgraph/nodes/Length.hx
  31. 2 2
      hrt/shgraph/nodes/Log.hx
  32. 4 4
      hrt/shgraph/nodes/Mix.hx
  33. 2 2
      hrt/shgraph/nodes/Mod.hx
  34. 1 1
      hrt/shgraph/nodes/Normalize.hx
  35. 2 2
      hrt/shgraph/nodes/Pow.hx
  36. 1 1
      hrt/shgraph/nodes/Preview.hx
  37. 7 7
      hrt/shgraph/nodes/Sampler.hx
  38. 24 0
      hrt/shgraph/nodes/Saturate.hx
  39. 6 6
      hrt/shgraph/nodes/SmoothStep.hx
  40. 2 2
      hrt/shgraph/nodes/Step.hx
  41. 49 0
      hrt/shgraph/nodes/StripAlpha.hx
  42. 53 40
      hrt/shgraph/nodes/SubGraph.hx
  43. 4 4
      hrt/shgraph/nodes/UVScroll.hx
  44. 24 0
      hrt/shgraph/nodes/UvToScreen.hx

+ 4 - 1
bin/defaultProps.json

@@ -140,11 +140,14 @@
 		"h3d.shader.ColorAdd",
 		"h3d.shader.ColorMult",
 		"h3d.shader.UVDelta",
-		"h3d.shader.UVScroll"
+		"h3d.shader.UVScroll",
+		"shaders/"
 	],
 
 	"fx.tags": [ { "id": "tag", "color": "#802000" } ],
 
+	"shadergraph.libfolders": ["shaders", "shaders/subshaders"],
+
 	// DomKit css files list
 	"domkit.css" : [],
 

+ 21 - 2
bin/style.css

@@ -1379,6 +1379,12 @@ input[type=checkbox]:checked:after {
   display: block;
   margin-bottom: 10px;
   width: 100%;
+  text-align: center;
+  box-sizing: border-box;
+}
+.graph-view .tabs .tab .options-block > div {
+  border: 1px solid #666;
+  padding: 2px;
 }
 .graph-view #add-menu {
   position: absolute;
@@ -1529,12 +1535,14 @@ input[type=checkbox]:checked:after {
   border: 1px solid #444;
   left: -1px;
   bottom: -1px;
+  max-height: 60%;
+  overflow: auto;
 }
-.graph-view .heaps-scene #status-bar span {
+.graph-view .heaps-scene #status-bar pre {
   padding: 10px;
   vertical-align: sub;
 }
-.graph-view .heaps-scene #status-bar span.error {
+.graph-view .heaps-scene #status-bar pre.error {
   color: #c74848;
 }
 .graph-view .heaps-scene svg g {
@@ -1632,6 +1640,17 @@ input[type=checkbox]:checked:after {
   float: left;
   margin-right: 10px;
 }
+.graph-view .heaps-scene svg .properties-group .custom-var {
+  display: table;
+  width: 100%;
+}
+.graph-view .heaps-scene svg .properties-group .custom-var div.custom-var-row {
+  display: table-row;
+}
+.graph-view .heaps-scene svg .properties-group .custom-var div.custom-var-row label,
+.graph-view .heaps-scene svg .properties-group .custom-var div.custom-var-row span {
+  display: table-cell;
+}
 .graph-view .heaps-scene svg .edge {
   stroke-width: 2;
   stroke: #c8c8c8;

+ 21 - 1
bin/style.less

@@ -1558,6 +1558,13 @@ input[type=checkbox] {
 					display: block;
 					margin-bottom: 10px;
 					width: 100%;
+					text-align: center;
+					box-sizing: border-box;
+				}
+
+				& > div {
+					border: 1px solid #666;
+					padding: 2px;
 				}
 			}
 		}
@@ -1739,8 +1746,10 @@ input[type=checkbox] {
 
 			left: -1px;
 			bottom: -1px;
+			max-height: 60%;
+			overflow: auto;
 
-			span {
+			pre {
 				padding: 10px;
 				vertical-align: sub;
 
@@ -1865,6 +1874,17 @@ input[type=checkbox] {
 						margin-right: 10px;
 					}
 				}
+
+				.custom-var {
+					display: table;
+					width: 100%;
+					div.custom-var-row {
+						display: table-row;
+						label, span {
+							display: table-cell;
+						}
+					}
+				}
 			}
 
 			.edge {

+ 6 - 0
hide/comp/Component.hx

@@ -37,4 +37,10 @@ class Component {
 		js.Browser.window.localStorage.setItem(saveDisplayKey + "/" + key, haxe.Json.stringify(value));
 	}
 
+	function removeDisplayState( key : String ) {
+		if( saveDisplayKey == null )
+			return;
+		js.Browser.window.localStorage.removeItem(saveDisplayKey + "/" + key);
+	}
+
 }

+ 91 - 45
hide/comp/SceneEditor.hx

@@ -1,7 +1,6 @@
 package hide.comp;
 
 import hrt.prefab.Reference;
-import h3d.col.Sphere;
 import h3d.scene.Mesh;
 import h3d.col.FPoint;
 import h3d.col.Ray;
@@ -18,6 +17,7 @@ import hrt.prefab.Object3D;
 import h3d.scene.Object;
 
 import hide.comp.cdb.DataFiles;
+import hide.view.CameraController.CamController as CameraController;
 
 enum SelectMode {
 	/**
@@ -38,15 +38,6 @@ enum SelectMode {
 	Nothing;
 }
 
-class CameraController extends h3d.scene.CameraController {
-	override function sync(ctx:h3d.scene.RenderContext) {
-		var old = ctx.elapsedTime;
-		ctx.elapsedTime = hxd.Timer.dt;
-		super.sync(ctx);
-		ctx.elapsedTime = old;
-	}
-}
-
 @:access(hide.comp.SceneEditor)
 class SceneEditorContext extends hide.prefab.EditContext {
 
@@ -147,6 +138,7 @@ class SceneEditor {
 	public var cameraController2D : hide.view.l3d.CameraController2D;
 	public var editorDisplay(default,set) : Bool;
 	public var camera2D(default,set) : Bool = false;
+	public var objectAreSelectable = true;
 
 	// Windows default is 0.5
 	public var dblClickDuration = 0.2;
@@ -295,7 +287,7 @@ class SceneEditor {
 	}
 
 	function makeCamController() : h3d.scene.CameraController {
-		var c = new CameraController(scene.s3d);
+		var c = new CameraController(scene.s3d, this);
 		c.friction = 0.9;
 		c.panSpeed = 0.6;
 		c.zoomAmount = 1.05;
@@ -819,6 +811,8 @@ class SceneEditor {
 	}
 
 	function selectNewObject() {
+		if( !objectAreSelectable )
+			return;
 		var parentEl = sceneData;
 		 // for now always create at scene root, not `curEdit.rootElements[0];`
 		var group = getParentGroup(parentEl);
@@ -1588,13 +1582,13 @@ class SceneEditor {
 		return e != root;
 	}
 
-	public function resetCamera() {
+	public function resetCamera(distanceFactor = 1.5) {
 		if( camera2D ) {
 			cameraController2D.initFromScene();
 		} else {
 			scene.s3d.camera.zNear = scene.s3d.camera.zFar = 0;
 			scene.s3d.camera.fovY = 25; // reset to default fov
-			scene.resetCamera(1.5);
+			scene.resetCamera(distanceFactor);
 			cameraController.lockZPlanes = scene.s3d.camera.zNear != 0;
 			cameraController.loadFromCamera();
 		}
@@ -2357,17 +2351,27 @@ class SceneEditor {
 		return newItems;
 	}
 
-	function getNewTypeMenuItem(ptype: String, parent: PrefabElement, onMake: PrefabElement->Void, ?label: String) : hide.comp.ContextMenu.ContextMenuItem {
+	function getNewTypeMenuItem(
+		ptype: String,
+		parent: PrefabElement,
+		onMake: PrefabElement->Void,
+		?label: String,
+		?objectName: String,
+		?path: String
+	) : hide.comp.ContextMenu.ContextMenuItem {
 		var pmodel = hrt.prefab.Library.getRegistered().get(ptype);
 		return {
 			label : label != null ? label : pmodel.inf.name,
 			click : function() {
-				function make(?path) {
+				function make(?sourcePath) {
 					var p = Type.createInstance(pmodel.cl, [parent]);
 					@:privateAccess p.type = ptype;
-					if(path != null)
-						p.source = path;
-					autoName(p);
+					if(sourcePath != null)
+						p.source = sourcePath;
+					if( objectName != null)
+						p.name = objectName;
+					else
+						autoName(p);
 					if(onMake != null)
 						onMake(p);
 					var recents : Array<String> = ide.currentConfig.get("sceneeditor.newrecents", []);
@@ -2379,17 +2383,18 @@ class SceneEditor {
 					return p;
 				}
 
-				if( pmodel.inf.fileSource != null )
-					ide.chooseFile(pmodel.inf.fileSource, function(path) {
+				if( pmodel.inf.fileSource != null ) {
+					if( path != null ) {
 						var p = make(path);
 						addElements([p]);
 						var recents : Array<String> = ide.currentConfig.get("sceneeditor.newrecents", []);
 						recents.remove(p.type);
-						recents.unshift(p.type);
-						var recentSize : Int = view.config.get("sceneeditor.recentsize");
-						if (recents.length > recentSize) recents.splice(recentSize, recents.length - recentSize);
-						ide.currentConfig.set("sceneeditor.newrecents", recents);
-					});
+					} else {
+						ide.chooseFile(pmodel.inf.fileSource, function(path) {
+							addElements([make(path)]);
+						});
+					}
+				}
 				else
 					addElements([make()]);
 			},
@@ -2397,38 +2402,79 @@ class SceneEditor {
 		};
 	}
 
-	function getNewShaderMenu(parentElt: PrefabElement, onMake: PrefabElement->Void) : hide.comp.ContextMenu.ContextMenuItem {
-		var custom = getNewTypeMenuItem("shader", parentElt, onMake, "Custom...");
-
-		function shaderItem(name, path) : hide.comp.ContextMenu.ContextMenuItem {
-			return {
-				label : name,
-				click : function() {
-					var s = new hrt.prefab.DynamicShader(parentElt);
-					s.source = path;
-					s.name = name;
-					addElements([s]);
-				}
-			}
+	function getNewShaderMenu(parentElt: PrefabElement, ?onMake: PrefabElement->Void) : hide.comp.ContextMenu.ContextMenuItem {
+		function isClassShader(path) {
+			if(StringTools.endsWith(path, ".hx")) path = path.substr(0, -3);
+			var cpath = path.split("/").join(".");
+			var cl = Type.resolveClass(cpath);
+			return cl != null;
 		}
 
-		var menu = [custom];
+		var shModel = hrt.prefab.Library.getRegistered().get("shader");
+		var graphModel = hrt.prefab.Library.getRegistered().get("shgraph");
+		var custom = {
+			label : "Custom...",
+			click : function() {
+				ide.chooseFile(shModel.inf.fileSource.concat(graphModel.inf.fileSource), function(path) {
+					var cl = isClassShader(path) ? shModel.cl : graphModel.cl;
+					var p = Type.createInstance(cl, [parentElt]);
+					p.source = path;
+					autoName(p);
+					if(onMake != null)
+						onMake(p);
+					addElements([p]);
+				});
+			},
+			icon : shModel.inf.icon,
+		};
 
-		var shaders : Array<String> = hide.Ide.inst.currentConfig.get("fx.shaders", []);
-		for(path in shaders) {
+		function classShaderItem(path) : hide.comp.ContextMenu.ContextMenuItem {
 			var name = path;
 			if(StringTools.endsWith(name, ".hx")) {
-				name = name.substr(0, -3);
-				name = name.split("/").pop();
+				name = new haxe.io.Path(path).file;
 			}
 			else {
 				name = name.split(".").pop();
 			}
-			menu.push(shaderItem(name, path));
+			return getNewTypeMenuItem("shader", parentElt, onMake, name, name, path);
+		}
+
+		function graphShaderItem(path) : hide.comp.ContextMenu.ContextMenuItem {
+			var name = new haxe.io.Path(path).file;
+			return getNewTypeMenuItem("shgraph", parentElt, onMake, name, name, path);
+		}
+
+		var menu : Array<hide.comp.ContextMenu.ContextMenuItem> = [];
+
+		var shaders : Array<String> = hide.Ide.inst.currentConfig.get("fx.shaders", []);
+		for(path in shaders) {
+			var strippedSlash = StringTools.endsWith(path, "/") ? path.substr(0, -1) : path;
+			var fullPath = ide.getPath(strippedSlash);
+			if( isClassShader(path) ) {
+				menu.push(classShaderItem(path));
+			} else if( StringTools.endsWith(path, ".shgraph")) {
+				menu.push(graphShaderItem(path));
+			} else if( sys.FileSystem.exists(fullPath) && sys.FileSystem.isDirectory(fullPath) ) {
+				var submenu : Array<hide.comp.ContextMenu.ContextMenuItem> = [];
+				for( c in sys.FileSystem.readDirectory(fullPath) ) {
+					var relPath = ide.makeRelative(fullPath + "/" + c);
+					if( isClassShader(relPath) ) {
+						submenu.push(classShaderItem(relPath));
+					} else if( StringTools.endsWith(relPath, ".shgraph")) {
+						submenu.push(graphShaderItem(relPath));
+					}
+				}
+				if( submenu.length > 0 ) {
+					menu.push({ label : path, menu : submenu });
+				}
+			}
 		}
 
+		menu.sort(function(l1,l2) return Reflect.compare(l1.label,l2.label));
+		menu.unshift(custom);
+
 		return {
-			label: "Shaders",
+			label: "Shader",
 			menu: menu
 		};
 	}

+ 62 - 82
hide/view/Graph.hx

@@ -23,8 +23,6 @@ class Graph extends FileView {
 	var editorMatrix : JQuery;
 	var statusBar : JQuery;
 
-	var contextMenu : JQuery;
-
 	var listOfBoxes : Array<Box> = [];
 	var listOfEdges : Array<Edge> = [];
 
@@ -48,6 +46,7 @@ class Graph extends FileView {
 	// used to build edge
 	static var NODE_TRIGGER_NEAR = 2000.0;
 	var isCreatingLink : EdgeState = None;
+	var edgeStyle = {stroke : ""};
 	var startLinkBox : Box;
 	var endLinkBox : Box;
 	var startLinkGrNode : JQuery;
@@ -69,7 +68,8 @@ class Graph extends FileView {
 			</div>');
 		parent = element.find(".heaps-scene");
 		editor = new SVG(parent);
-		statusBar = new Element('<div id="status-bar" ><span> </span></div>').appendTo(parent).find("span");
+		statusBar = new Element('<div id="status-bar" ><pre> </pre></div>').appendTo(parent).find("pre");
+		statusBar.on("wheel", (e) -> { e.stopPropagation(); });
 
 		editorMatrix = editor.group(editor.element);
 
@@ -100,8 +100,8 @@ class Graph extends FileView {
 			mouseMoveFunction(e.clientX, e.clientY);
 		});
 
-
-		parent.on("mouseup", function(e) {
+		var document = new Element(js.Browser.document);
+		document.on("mouseup", function(e) {
 			if(timerUpdateView != null)
 				stopUpdateViewPosition();
 			if (e.button == 0) {
@@ -137,24 +137,6 @@ class Graph extends FileView {
 			}
 		});
 
-		parent.on("keydown", function(e) {
-
-			if (e.keyCode == 46) {
-				if (currentEdge != null) {
-					removeEdge(currentEdge);
-				}
-				if (listOfBoxesSelected.length > 0) {
-					for (b in listOfBoxesSelected) {
-						removeBox(b);
-					}
-					clearSelectionBoxes();
-				}
-				return;
-			} else if (e.keyCode == 32) {
-
-			}
-		});
-
 		listOfBoxes = [];
 		listOfEdges = [];
 
@@ -230,6 +212,7 @@ class Graph extends FileView {
 				for (edge in listOfEdges) {
 					if (edge.from == b || edge.to == b) {
 						edge.elt.remove();
+						edgeStyle.stroke = edge.nodeFrom.css("fill");
 						edge.elt = createCurve(edge.nodeFrom, edge.nodeTo);
 
 						edge.elt.on("mousedown", function(e) {
@@ -276,9 +259,6 @@ class Graph extends FileView {
 			if (e.button != 0)
 				return;
 			lastClickDrag = null;
-			if (listOfBoxesSelected.length == 1 && box.selected && !e.ctrlKey) {
-				clearSelectionBoxes();
-			}
 		});
 		listOfBoxes.push(box);
 
@@ -296,7 +276,7 @@ class Graph extends FileView {
 					defaultValue = "0";
 				}
 			}
-			var grNode = box.addInput(editor, inputInfo.name, defaultValue);
+			var grNode = box.addInput(editor, inputInfo.name, defaultValue, inputInfo.type);
 			if (defaultValue != null) {
 				var fieldEditInput = grNode.find("input");
 				fieldEditInput.on("change", function(ev) {
@@ -321,19 +301,21 @@ class Graph extends FileView {
 				isCreatingLink = FromInput;
 				startLinkGrNode = grNode;
 				startLinkBox = box;
+				edgeStyle.stroke = node.css("fill");
 				setAvailableOutputNodes(box, grNode.find(".node").attr("field"));
 			});
 		}
 		for (outputKey in box.getInstance().getOutputInfoKeys()) {
 			var outputInfo = box.getInstance().getOutputInfo(outputKey);
-
-			var grNode = box.addOutput(editor, outputInfo.name);
+			var grNode = box.addOutput(editor, outputInfo.name, box.getInstance().getOutputType(outputKey));
 			grNode.find(".node").attr("field", outputKey);
 			grNode.on("mousedown", function(e) {
 				e.stopPropagation();
+				var node = grNode.find(".node");
 				isCreatingLink = FromOutput;
 				startLinkGrNode = grNode;
 				startLinkBox = box;
+				edgeStyle.stroke = node.css("fill");
 				setAvailableInputNodes(box, startLinkGrNode.find(".node").attr("field"));
 			});
 		}
@@ -343,7 +325,7 @@ class Graph extends FileView {
 		return box;
 	}
 
-	function removeBox(box : Box) {
+	function removeBox(box : Box, trackChanges = true) {
 		removeEdges(box);
 		box.dispose();
 		listOfBoxes.remove(box);
@@ -374,6 +356,7 @@ class Graph extends FileView {
 						isCreatingLink = FromOutput;
 						startLinkGrNode = e.nodeFrom.parent();
 						startLinkBox = e.from;
+						edgeStyle.stroke = e.nodeFrom.css("fill");
 						setAvailableInputNodes(e.from, e.nodeFrom.attr("field"));
 						removeEdge(e);
 						createLink(x, y);
@@ -386,6 +369,7 @@ class Graph extends FileView {
 						isCreatingLink = FromInput;
 						startLinkGrNode = e.nodeTo.parent();
 						startLinkBox = e.to;
+						edgeStyle.stroke = e.nodeFrom.css("fill");
 						setAvailableOutputNodes(e.to, e.nodeTo.attr("field"));
 						removeEdge(e);
 						createLink(x, y);
@@ -565,7 +549,8 @@ class Graph extends FileView {
 							lX(offsetEnd.left) + Box.NODE_RADIUS,
 							lY(offsetEnd.top) + Box.NODE_RADIUS,
 							startX + valueCurveX * (Math.min(maxDistanceY, diffDistanceY)/maxDistanceY),
-							startY + signCurveY * valueCurveY * (Math.min(maxDistanceY, diffDistanceY)/maxDistanceY))
+							startY + signCurveY * valueCurveY * (Math.min(maxDistanceY, diffDistanceY)/maxDistanceY),
+							edgeStyle)
 							.addClass("edge");
 		editorMatrix.prepend(curve);
 		if (isDraft)
@@ -574,45 +559,6 @@ class Graph extends FileView {
 		return curve;
 	}
 
-	function customContextMenu( elts : Array<Element>, ?x : Int, ?y : Int ) {
-		closeCustomContextMenu();
-
-		if (elts.length == 0) return;
-
-		contextMenu = new Element('
-		<div id="context-menu">
-			<div id="options"></div>
-		</div>').appendTo(parent);
-
-		var options = contextMenu.find("#options");
-
-		if (x == null) x = Std.int(ide.mouseX - parent.offset().left);
-		if (y == null) y = Std.int(ide.mouseY - parent.offset().top);
-
-		contextMenu.on("mousedown", function(e) {
-			e.stopPropagation();
-		});
-
-		contextMenu.on("click", function(e) {
-			closeCustomContextMenu();
-			e.stopPropagation();
-		});
-
-		for (elt in elts) {
-			elt.appendTo(options);
-		}
-
-		contextMenu.css("left", Math.min(x, element.width() - contextMenu.width() - 5));
-		contextMenu.css("top", Math.min(y, element.height() - contextMenu.height() - 5));
-	}
-
-	function closeCustomContextMenu() {
-		if (contextMenu != null) {
-			contextMenu.remove();
-			contextMenu = null;
-		}
-	}
-
 	function clearSelectionBoxes() {
 		for(b in listOfBoxesSelected) b.setSelected(false);
 		listOfBoxesSelected = [];
@@ -657,14 +603,16 @@ class Graph extends FileView {
 		}
 	}
 
-	function centerView() {
-		if (listOfBoxes.length == 0) return;
-		var xMin = listOfBoxes[0].getX();
-		var yMin = listOfBoxes[0].getY();
-		var xMax = xMin + listOfBoxes[0].getWidth();
-		var yMax = yMin + listOfBoxes[0].getHeight();
-		for (i in 1...listOfBoxes.length) {
-			var b = listOfBoxes[i];
+	function getGraphDims(?boxes) {
+		if( boxes == null )
+			boxes = listOfBoxes;
+		if( boxes.length == 0 ) return null;
+		var xMin = boxes[0].getX();
+		var yMin = boxes[0].getY();
+		var xMax = xMin + boxes[0].getWidth();
+		var yMax = yMin + boxes[0].getHeight();
+		for (i in 1...boxes.length) {
+			var b = boxes[i];
 			xMin = Math.min(xMin, b.getX());
 			yMin = Math.min(yMin, b.getY());
 			xMax = Math.max(xMax, b.getX() + b.getWidth());
@@ -672,10 +620,22 @@ class Graph extends FileView {
 		}
 		var center = new IPoint(Std.int(xMin + (xMax - xMin)/2), Std.int(yMin + (yMax - yMin)/2));
 		center.y += Std.int(editor.element.height()*CENTER_OFFSET_Y);
-		var scale = Math.min(1, Math.min((editor.element.width() - 50) / (xMax - xMin), (editor.element.height() - 50) / (yMax - yMin)));
+		return {
+			xMin : xMin,
+			yMin : yMin,
+			xMax : xMax,
+			yMax : yMax,
+			center : center,
+		};
+	}
+
+	function centerView() {
+		if (listOfBoxes.length == 0) return;
+		var dims = getGraphDims();
+		var scale = Math.min(1, Math.min((editor.element.width() - 50) / (dims.xMax - dims.xMin), (editor.element.height() - 50) / (dims.yMax - dims.yMin)));
 
-		transformMatrix[4] = editor.element.width()/2 - center.x;
-		transformMatrix[5] = editor.element.height()/2 - center.y;
+		transformMatrix[4] = editor.element.width()/2 - dims.center.x;
+		transformMatrix[5] = editor.element.height()/2 - dims.center.y;
 
 		transformMatrix[0] = scale;
 		transformMatrix[3] = scale;
@@ -689,6 +649,24 @@ class Graph extends FileView {
 		updateMatrix();
 	}
 
+	function clampView() {
+		if (listOfBoxes.length == 0) return;
+		var dims = getGraphDims();
+
+		var width = editor.element.width();
+		var height = editor.element.height();
+		var scale = transformMatrix[0];
+
+		if( transformMatrix[4] + dims.xMin * scale > width )
+			transformMatrix[4] = width - dims.xMin * scale;
+		if( transformMatrix[4] + dims.xMax * scale < 0 )
+			transformMatrix[4] = -1 * dims.xMax * scale;
+		if( transformMatrix[5] + dims.yMin * scale > height )
+			transformMatrix[5] = height - dims.yMin * scale;
+		if( transformMatrix[5] + dims.yMax * scale < 0 )
+			transformMatrix[5] = -1 * dims.yMax * scale;
+	}
+
 	function updateMatrix() {
 		editorMatrix.attr({transform: 'matrix(${transformMatrix.join(' ')})'});
 	}
@@ -707,6 +685,7 @@ class Graph extends FileView {
 		transformMatrix[4] = x - (x - transformMatrix[4]) * scale;
 		transformMatrix[5] = y - (y - transformMatrix[5]) * scale;
 
+		clampView();
 		updateMatrix();
 	}
 
@@ -714,10 +693,11 @@ class Graph extends FileView {
 		transformMatrix[4] += p.x;
 		transformMatrix[5] += p.y;
 
+		clampView();
 		updateMatrix();
 	}
 
-	function IsVisible() : Bool {
+	function isVisible() : Bool {
 		return editor.element.is(":visible");
 	}
 

+ 0 - 10
hide/view/Prefab.hx

@@ -22,16 +22,6 @@ private class PrefabSceneEditor extends hide.comp.SceneEditor {
 		this.localTransform = false; // TODO: Expose option
 	}
 
-	override function makeCamController() {
-		var c = new CamController(scene.s3d, this);
-		c.friction = 0.9;
-		c.panSpeed = 0.6;
-		c.zoomAmount = 1.05;
-		c.smooth = 0.7;
-		c.minDistance = 1;
-		return c;
-	}
-
 	override function refresh(?mode, ?callback) {
 		parent.onRefresh();
 		super.refresh(mode, callback);

+ 55 - 5
hide/view/shadereditor/Box.hx

@@ -3,9 +3,18 @@ package hide.view.shadereditor;
 import hide.comp.SVG;
 import js.jquery.JQuery;
 import hrt.shgraph.ShaderNode;
-
 class Box {
 
+	var boolColor = "#cc0505";
+	var numberColor = "#00ffea";
+	var floatColor = "#00ff73";
+	var intColor = "#00ffea";
+	var vec2Color = "#5eff00";
+	var vec3Color = "#eeff00";
+	var vec4Color = "#fc6703";
+	var samplerColor = "#600aff";
+	var defaultColor = "#c8c8c8";
+
 	var nodeInstance : ShaderNode;
 
 	var x : Float;
@@ -77,10 +86,30 @@ class Box {
 		//editor.line(element, width/2, HEADER_HEIGHT, width/2, 0, {display: "none"}).addClass("nodes-separator");
 	}
 
-	public function addInput(editor : SVG, name : String, valueDefault : String = null) {
+	public function addInput(editor : SVG, name : String, valueDefault : String = null, type : hrt.shgraph.ShaderType.SType) {
 		var node = editor.group(element).addClass("input-node-group");
 		var nodeHeight = HEADER_HEIGHT + NODE_MARGIN * (inputs.length+1) + NODE_RADIUS * inputs.length;
-		var nodeCircle = editor.circle(node, 0, nodeHeight, NODE_RADIUS).addClass("node input-node");
+		var style = {fill : ""}
+		switch (type) {
+			case Bool:
+				style.fill = boolColor;
+			case Number:
+				style.fill = numberColor;
+			case Float:
+				style.fill = floatColor;
+			case Vec2:
+				style.fill = vec2Color;
+			case Vec3:
+				style.fill = vec3Color;
+			case Vec4:
+				style.fill = vec4Color;
+			case Sampler:
+				style.fill = samplerColor;
+			default:
+				style.fill = defaultColor;
+
+		}
+		var nodeCircle = editor.circle(node, 0, nodeHeight, NODE_RADIUS, style).addClass("node input-node");
 
 		if (name.length > 0)
 			editor.text(node, NODE_TITLE_PADDING, nodeHeight + 4, name).addClass("title-node");
@@ -96,10 +125,31 @@ class Box {
 		return node;
 	}
 
-	public function addOutput(editor : SVG, name : String) {
+	public function addOutput(editor : SVG, name : String, ?type : hxsl.Ast.Type) {
 		var node = editor.group(element).addClass("output-node-group");
 		var nodeHeight = HEADER_HEIGHT + NODE_MARGIN * (outputs.length+1) + NODE_RADIUS * outputs.length;
-		var nodeCircle = editor.circle(node, width, nodeHeight, NODE_RADIUS).addClass("node output-node");
+		var style = {fill : ""}
+		switch (type) {
+			case TBool:
+				style.fill = boolColor;
+			case TInt:
+				style.fill = intColor;
+			case TFloat:
+				style.fill = floatColor;
+			case TVec(size, t):
+				if (size == 2)
+					style.fill = vec2Color;
+				else if (size == 3)
+					style.fill = vec3Color;
+				else if (size == 4)
+					style.fill = vec4Color;
+			case TSampler2D:
+				style.fill = samplerColor;
+			default:
+				style.fill = defaultColor;
+
+		}
+		var nodeCircle = editor.circle(node, width, nodeHeight, NODE_RADIUS, style).addClass("node output-node");
 
 		if (name.length > 0)
 			editor.text(node, width - NODE_TITLE_PADDING - (name.length * 6.75), nodeHeight + 4, name).addClass("title-node");

+ 475 - 149
hide/view/shadereditor/ShaderEditor.hx

@@ -8,7 +8,6 @@ import hrt.shgraph.ShaderException;
 import haxe.Timer;
 using hxsl.Ast.Type;
 
-import haxe.rtti.Meta;
 import hide.comp.SceneEditor;
 import js.jquery.JQuery;
 import h2d.col.Point;
@@ -19,6 +18,15 @@ import hrt.shgraph.ShaderNode;
 
 typedef NodeInfo = { name : String, description : String, key : String };
 
+typedef SavedClipboard = {
+	nodes : Array<{
+		pos : Point,
+		nodeType : Class<ShaderNode>,
+		props : Dynamic,
+	}>,
+	edges : Array<{ fromIdx : Int, fromName : String, toIdx : Int, toName : String }>,
+}
+
 class ShaderEditor extends hide.view.Graph {
 
 	var parametersList : JQuery;
@@ -30,9 +38,11 @@ class ShaderEditor extends hide.view.Graph {
 
 	// used to preview
 	var sceneEditor : SceneEditor;
+	var defaultLight : hrt.prefab.Light;
 
 	var root : hrt.prefab.Prefab;
 	var obj : h3d.scene.Object;
+	var prefabObj : hrt.prefab.Prefab;
 	var shaderGraph : ShaderGraph;
 
 	var lastSnapshot : haxe.Json;
@@ -43,6 +53,9 @@ class ShaderEditor extends hide.view.Graph {
 	var currentShader : DynamicShader;
 	var currentShaderDef : hrt.prefab.ContextShared.ShaderDef;
 
+	static var clipboard : SavedClipboard = null;
+	static var lastCopyEditor : ShaderEditor = null;
+
 	override function onDisplay() {
 		super.onDisplay();
 		shaderGraph = new ShaderGraph(state.path);
@@ -59,8 +72,19 @@ class ShaderEditor extends hide.view.Graph {
 								<input id="createParameter" type="button" value="Add parameter" />
 								<input id="launchCompileShader" type="button" value="Compile shader" />
 								<input id="saveShader" type="button" value="Save" />
-								<input id="changeModel" type="button" value="Change Model" />
+								<div>
+									<input id="changeModel" type="button" value="Change Model" />
+									<input id="removeModel" type="button" value="Remove Model" />
+								</div>
 								<input id="centerView" type="button" value="Center View" />
+								<div>
+									Display Compiled
+									<input id="displayHxsl" type="button" value="Hxsl" />
+									<input id="displayGlsl" type="button" value="Glsl" />
+									<input id="displayHlsl" type="button" value="Hlsl" />
+								</div>
+								<input id="togglelight" type="button" value="Toggle Default Lights" />
+								<input id="refreshGraph" type="button" value="Refresh Shader Graph" />
 							</div>
 						</div>)');
 		parent.on("drop", function(e) {
@@ -82,31 +106,31 @@ class ShaderEditor extends hide.view.Graph {
 
 		var def = new hrt.prefab.Library();
 		new hrt.prefab.RenderProps(def).name = "renderer";
-		var l = new hrt.prefab.Light(def);
-		l.name = "sunLight";
-		l.kind = Directional;
-		l.power = 1.5;
+		defaultLight = new hrt.prefab.Light(def);
+		defaultLight.name = "sunLight";
+		defaultLight.kind = Directional;
+		defaultLight.power = 1.5;
 		var q = new h3d.Quat();
 		q.initDirection(new h3d.Vector(-1,-1.5,-3));
 		var a = q.toEuler();
-		l.rotationX = Math.round(a.x * 180 / Math.PI);
-		l.rotationY = Math.round(a.y * 180 / Math.PI);
-		l.rotationZ = Math.round(a.z * 180 / Math.PI);
-		l.shadows.mode = Dynamic;
-		l.shadows.size = 1024;
+		defaultLight.rotationX = Math.round(a.x * 180 / Math.PI);
+		defaultLight.rotationY = Math.round(a.y * 180 / Math.PI);
+		defaultLight.rotationZ = Math.round(a.z * 180 / Math.PI);
+		defaultLight.shadows.mode = Dynamic;
+		defaultLight.shadows.size = 1024;
 		root = def;
 
 		sceneEditor = new hide.comp.SceneEditor(this, root);
 		sceneEditor.editorDisplay = false;
 		sceneEditor.onRefresh = onRefresh;
 		sceneEditor.onUpdate = function(dt : Float) {};
+		sceneEditor.objectAreSelectable = false;
 		sceneEditor.view.keys = new hide.ui.Keys(null); // Remove SceneEditor Shortcuts
 
 		editorMatrix = editor.group(editor.element);
 
 		element.on("mousedown", function(e) {
 			closeAddMenu();
-			closeCustomContextMenu();
 		});
 
 		parent.on("mouseup", function(e) {
@@ -145,51 +169,52 @@ class ShaderEditor extends hide.view.Graph {
 		keys = new hide.ui.Keys(element);
 		keys.register("undo", function() undo.undo());
 		keys.register("redo", function() undo.redo());
+		keys.register("delete", deleteSelection);
+		keys.register("duplicate", duplicateSelection);
+		keys.register("copy", onCopy);
+		keys.register("paste", onPaste);
+		keys.register("sceneeditor.focus", centerView);
+		keys.register("view.refresh", rebuild);
 
 		parent.on("contextmenu", function(e) {
-			var elements = [];
-
-			var addNode = new Element("<div> Add node </div>");
-			addNode.on("click", function(e) {
-				contextMenuAddNode(Std.parseInt(contextMenu.css("left")), Std.parseInt(contextMenu.css("top")));
-			});
-			elements.push(addNode);
-
-			var deleteNode = new Element("<div> Delete nodes </div>");
-			deleteNode.on("click", function(e) {
-				if (listOfBoxesSelected.length > 0) {
-					if (ide.confirm("Delete all theses nodes ?")) {
-						for (b in listOfBoxesSelected) {
-							removeBox(b);
+			e.preventDefault();
+			new hide.comp.ContextMenu([
+				{ label : "Add node", menu : contextMenuAddNode() },
+				{ label : "", isSeparator : true },
+				{
+					label : "Delete nodes",
+					click : function() {
+						if (listOfBoxesSelected.length > 0) {
+							if (ide.confirm("Delete all theses nodes ?")) {
+								deleteSelection();
+							}
 						}
-						clearSelectionBoxes();
-					}
-				}
-			});
-			elements.push(deleteNode);
+					},
+				},
+			]);
+			return false;
+		});
+
+		var newParamCtxMenu : Array<hide.comp.ContextMenu.ContextMenuItem> = [
+			{ label : "Number", click : () -> createParameter(TFloat) },
+			{ label : "Color", click : () -> createParameter(TVec(4, VFloat)) },
+			{ label : "Texture", click : () -> createParameter(TSampler2D) },
+		];
 
-			customContextMenu(elements);
+		parametersList = element.find("#parametersList");
+		parametersList.on("contextmenu", function(e) {
 			e.preventDefault();
-			return false;
+			e.stopPropagation();
+			new hide.comp.ContextMenu([
+				{
+					label : "Add Parameter",
+					menu : newParamCtxMenu,
+				},
+			]);
 		});
 
 		element.find("#createParameter").on("click", function() {
-			function createElement(name : String, type : Type) : Element {
-				var elt = new Element('
-					<div>
-						<span> ${name} </span>
-					</div>');
-				elt.on("click", function() {
-					createParameter(type);
-				});
-				return elt;
-			}
-
-			customContextMenu([
-				createElement("Number", TFloat),
-				createElement("Color", TVec(4, VFloat)),
-				createElement("Texture", TSampler2D)
-				]);
+			new hide.comp.ContextMenu(newParamCtxMenu);
 		});
 
 		element.find("#launchCompileShader").on("click", function() {
@@ -201,21 +226,39 @@ class ShaderEditor extends hide.view.Graph {
 		});
 
 		element.find("#changeModel").on("click", function() {
-			ide.chooseFile(["fbx"], function(path) {
+			ide.chooseFile(["fbx", "l3d", "prefab"], function(path) {
 				sceneEditor.scene.setCurrent();
-				sceneEditor.scene.s3d.removeChild(obj);
-				obj = sceneEditor.scene.loadModel(path, true);
+				if( prefabObj != null ) {
+					sceneEditor.deleteElements([prefabObj], false, false);
+					prefabObj = null;
+				}
+				else {
+					sceneEditor.scene.s3d.removeChild(obj);
+				}
+				loadPreviewPrefab(path);
 				saveDisplayState("customModel", path);
-				sceneEditor.scene.s3d.addChild(obj);
-				compileShader();
+				if( prefabObj == null )
+					sceneEditor.scene.s3d.addChild(obj);
+				sceneEditor.resetCamera(1.05);
+				launchCompileShader();
 			});
 		});
 
+		element.find("#removeModel").on("click", resetPreviewDefault);
+
 		element.find("#centerView").on("click", function() {
 			centerView();
-		});
+		})
+			.prop("title", 'Center around full graph (${config.get("key.sceneeditor.focus")})');
 
-		parametersList = element.find("#parametersList");
+		element.find("#togglelight").on("click", toggleDefaultLight);
+
+		element.find("#refreshGraph").on("click", rebuild)
+			.prop("title", 'Refresh the Shader (${config.get("key.view.refresh")})');
+
+		element.find("#displayHxsl").on("click", () -> displayCompiled("hxsl"));
+		element.find("#displayGlsl").on("click", () -> displayCompiled("glsl"));
+		element.find("#displayHlsl").on("click", () -> displayCompiled("hlsl"));
 
 		editorMatrix.on("click", "input, select", function(ev) {
 			beforeChange();
@@ -224,27 +267,6 @@ class ShaderEditor extends hide.view.Graph {
 		editorMatrix.on("change", "input, select", function(ev) {
 			try {
 				var idBox = ev.target.closest(".box").id;
-				for (b in listOfBoxes) {
-					if (b.getId() == idBox) {
-						var subGraph = Std.downcast(b.getInstance(), hrt.shgraph.nodes.SubGraph);
-						if (subGraph != null) {
-							if (ev.currentTarget.getAttribute('field') != "filesubgraph") {
-								break;
-							}
-							var length = listOfEdges.length;
-							for (i in 0...length) {
-								var edge = listOfEdges[length-i-1];
-								if (edge.from == b || edge.to == b) {
-									removeShaderGraphEdge(edge);
-								}
-							}
-							refreshBox(b);
-							afterChange();
-							return;
-						}
-						break;
-					}
-				}
 				shaderGraph.nodeUpdated(idBox);
 				afterChange();
 				launchCompileShader();
@@ -271,6 +293,33 @@ class ShaderEditor extends hide.view.Graph {
 			listOfClasses[group].push({ name : (metas.name != null) ? metas.name[0] : key , description : (metas.description != null) ? metas.description[0] : "" , key : key });
 		}
 
+		var libPaths : Array<String> = config.get("shadergraph.libfolders", ["shaders"]);
+		for( lpath in libPaths ) {
+			var basePath = ide.getPath(lpath);
+			if( !sys.FileSystem.exists(basePath) || !sys.FileSystem.isDirectory(basePath) )
+				continue;
+			for( c in sys.FileSystem.readDirectory(basePath) ) {
+				var relPath = ide.makeRelative(basePath + "/" + c);
+				if(
+					this.state.path.toLowerCase() != relPath.toLowerCase()
+					&& haxe.io.Path.extension(relPath).toLowerCase() == "shgraph"
+				) {
+					var group = 'SubGraph from $lpath';
+					if (listOfClasses[group] == null)
+						listOfClasses[group] = new Array<NodeInfo>();
+
+					var fileName = new haxe.io.Path(relPath).file;
+
+					listOfClasses[group].push({
+						name : fileName,
+						// TODO: Add a the description to the shgraph file
+						description : "",
+						key : relPath,
+					});
+				}
+			}
+		}
+
 		for (key in listOfClasses.keys()) {
 			listOfClasses[key].sort(function (a, b): Int {
 				if (a.name < b.name) return -1;
@@ -281,11 +330,10 @@ class ShaderEditor extends hide.view.Graph {
 
 		new Element("svg").ready(function(e) {
 			refreshShaderGraph();
-			if (IsVisible()) {
+			if (isVisible()) {
 				centerView();
 			}
 		});
-
 	}
 
 	override function save() {
@@ -296,10 +344,47 @@ class ShaderEditor extends hide.view.Graph {
 		info("Shader saved");
 	}
 
+	function loadPreviewPrefab(path : String) {
+		if( path == null )
+			return;
+		prefabObj = null;
+		var ext = haxe.io.Path.extension(path).toLowerCase();
+		var relative = ide.makeRelative(path);
+		if( ext == "fbx" )
+			obj = sceneEditor.scene.loadModel(path, true);
+		else if( hrt.prefab.Library.getPrefabType(relative) != null ) {
+			var ref = new hrt.prefab.Reference(root);
+			ref.source = relative;
+			sceneEditor.addElements([ref], false, true, false);
+			prefabObj = ref;
+			obj = sceneEditor.getObject(prefabObj);
+		}
+	}
+
+	function resetPreviewDefault() {
+		sceneEditor.scene.setCurrent();
+		if( prefabObj != null ) {
+			sceneEditor.deleteElements([prefabObj], false, false);
+			prefabObj = null;
+		}
+		else {
+			sceneEditor.scene.s3d.removeChild(obj);
+		}
+		removeDisplayState("customModel");
+
+		var sp = new h3d.prim.Sphere(1, 128, 128);
+		sp.addNormals();
+		sp.addUVs();
+		obj = new h3d.scene.Mesh(sp);
+		sceneEditor.scene.s3d.addChild(obj);
+		sceneEditor.resetCamera(1.05);
+		launchCompileShader();
+	}
+
 	function onRefresh() {
 		var saveCustomModel = getDisplayState("customModel");
 		if (saveCustomModel != null)
-			obj = sceneEditor.scene.loadModel(saveCustomModel, true);
+			loadPreviewPrefab(saveCustomModel);
 		else {
 			// obj = sceneEditor.scene.loadModel("res/PrimitiveShapes/Sphere.fbx", true);
 			var sp = new h3d.prim.Sphere(1, 128, 128);
@@ -307,16 +392,18 @@ class ShaderEditor extends hide.view.Graph {
 			sp.addUVs();
 			obj = new h3d.scene.Mesh(sp);
 		}
-		sceneEditor.scene.s3d.addChild(obj);
+		if( prefabObj == null )
+			sceneEditor.scene.s3d.addChild(obj);
+		sceneEditor.resetCamera(1.05);
 
 		element.find("#preview").first().append(sceneEditor.scene.element);
 
-		if (IsVisible()) {
+		if (isVisible()) {
 			launchCompileShader();
 		} else {
 			var timer = new Timer(VIEW_VISIBLE_CHECK_TIMER);
 			timer.run = function() {
-				if (IsVisible()) {
+				if (isVisible()) {
 					centerView();
 					generateEdges();
 					launchCompileShader();
@@ -324,10 +411,16 @@ class ShaderEditor extends hide.view.Graph {
 				}
 			}
 		}
+		@:privateAccess
+		if( sceneEditor.scene.window != null )
+			sceneEditor.scene.window.checkResize();
 	}
 
-	function refreshShaderGraph(readyEvent : Bool = true) {
+	function toggleDefaultLight() {
+		sceneEditor.setEnabled([defaultLight], !defaultLight.enabled);
+	}
 
+	function refreshShaderGraph(readyEvent : Bool = true) {
 		listOfBoxes = [];
 		listOfEdges = [];
 
@@ -358,11 +451,23 @@ class ShaderEditor extends hide.view.Graph {
 			} else {
 				addBox(new Point(node.x, node.y), std.Type.getClass(node.instance), node.instance);
 			}
+			var subGraphNode = Std.downcast(node.instance, SubGraph);
+			if( subGraphNode != null ) {
+				var found = false;
+				for( el in watches ) {
+					if( el.path == subGraphNode.pathShaderGraph ) {
+						found = true;
+						break;
+					}
+				}
+				if( !found )
+					watch(subGraphNode.pathShaderGraph, rebuild, { keepOnRebuild: false });
+			}
 		}
 
 		if (readyEvent) {
 			new Element(".nodes").ready(function(e) {
-				if (IsVisible()) {
+				if (isVisible()) {
 					generateEdges();
 				}
 			});
@@ -388,9 +493,10 @@ class ShaderEditor extends hide.view.Graph {
 				for (b in listOfBoxes) {
 					for (key in b.getInstance().getInputsKey()) {
 						var input = b.getInstance().getInput(key);
-						if (input != null && input.node.id == box.getId()) {
+						if (input != null && input.node.id == box.getId() && input.keyOutput == outputKey) {
 							var nodeFrom = box.getElement().find('[field=${outputKey}]');
 							var nodeTo = b.getElement().find('[field=${key}]');
+							edgeStyle.stroke = nodeFrom.css("fill");
 							createEdgeInEditorGraph({from: box, nodeFrom: nodeFrom, to : b, nodeTo: nodeTo, elt : createCurve(nodeFrom, nodeTo) });
 						}
 					}
@@ -412,6 +518,7 @@ class ShaderEditor extends hide.view.Graph {
 				}
 				var nodeFrom = fromBox.getElement().find('[field=${input.getKey()}]');
 				var nodeTo = box.getElement().find('[field=${key}]');
+				edgeStyle.stroke = nodeFrom.css("fill");
 				createEdgeInEditorGraph({from: fromBox, nodeFrom: nodeFrom, to : box, nodeTo: nodeTo, elt : createCurve(nodeFrom, nodeTo) });
 			}
 		}
@@ -456,6 +563,7 @@ class ShaderEditor extends hide.view.Graph {
 	function addParameter(id : Int, name : String, type : Type, ?value : Dynamic) {
 
 		var elt = new Element('<div id="param_${id}" class="parameter" draggable="true" ></div>').appendTo(parametersList);
+		elt.on("click", function(e) {e.stopPropagation();});
 		var content = new Element('<div class="content" ></div>');
 		content.hide();
 		var defaultValue = new Element("<div><span>Default: </span></div>").appendTo(content);
@@ -488,7 +596,7 @@ class ShaderEditor extends hide.view.Graph {
 				var parentPicker = new Element('<div style="width: 35px; height: 25px; display: inline-block;"></div>').appendTo(defaultValue);
 				var picker = new hide.comp.ColorPicker(true, parentPicker);
 
-				
+
 				if (value == null)
 					value = [0, 0, 0, 1];
 				var start : h3d.Vector = h3d.Vector.fromArray(value);
@@ -554,6 +662,27 @@ class ShaderEditor extends hide.view.Graph {
 		});
 		deleteBtn.appendTo(actionBtns);
 
+		var perInstanceCb = new Element('<div><span>PerInstance</span><input type="checkbox"/><div>');
+		var shaderParam : ShaderParam = null;
+		for (b in listOfBoxes) {
+			var tmpShaderParam = Std.downcast(b.getInstance(), ShaderParam);
+			if (tmpShaderParam != null && tmpShaderParam.parameterId == id) {
+				shaderParam = tmpShaderParam;
+				break;
+			}
+		}
+		if (shaderParam != null) {
+			perInstanceCb.prop("checked", shaderParam.perInstance);
+			perInstanceCb.on("change", function() {
+				beforeChange();
+				var checked : Bool = perInstanceCb.prop("checked");
+				shaderParam.perInstance = checked;
+				afterChange();
+				compileShader();
+			});
+			perInstanceCb.appendTo(content);
+		}
+
 		var inputTitle = elt.find(".input-title");
 		inputTitle.on("click", function(e) {
 			e.stopPropagation();
@@ -574,6 +703,8 @@ class ShaderEditor extends hide.view.Graph {
 				}
 			}
 		});
+		inputTitle.on("focus", function() { inputTitle.select(); } );
+
 		elt.find(".header").on("click", function() {
 			toggleParameter(elt);
 		});
@@ -667,6 +798,21 @@ class ShaderEditor extends hide.view.Graph {
 		};
 	}
 
+	function displayCompiled(type : String) {
+		var text = "\n";
+		if( currentShaderDef == null || currentShader == null )
+			text += "No valid shader in memory";
+		if( currentShaderDef != null) {
+			text += switch( type ) {
+				case "hxsl": hxsl.Printer.shaderToString(currentShaderDef.shader.data);
+				case "glsl": hxsl.GlslOut.compile(currentShaderDef.shader.data);
+				case "hlsl": new hxsl.HlslOut().run(currentShaderDef.shader.data);
+				default: "";
+			}
+		}
+		info(text);
+	}
+
 	function compileShader() {
 		var newShader : DynamicShader = null;
 		try {
@@ -685,7 +831,7 @@ class ShaderEditor extends hide.view.Graph {
 			for (m in obj.getMaterials()) {
 				m.mainPass.addShader(newShader);
 			}
-			@:privateAccess sceneEditor.scene.render(sceneEditor.scene.engine);
+			sceneEditor.scene.render(sceneEditor.scene.engine);
 			currentShader = newShader;
 			currentShaderDef = shaderGraphDef;
 			info('Shader compiled in  ${Date.now().getTime() - timeStart}ms');
@@ -774,32 +920,42 @@ class ShaderEditor extends hide.view.Graph {
 		@:privateAccess ShaderGraph.setParamValue(sceneEditor.context.shared, shader, variable, value);
 	}
 
-	function addNode(p : Point, nodeClass : Class<ShaderNode>) {
-		beforeChange();
-
-		var node = shaderGraph.addNode(p.x, p.y, nodeClass);
-		afterChange();
-
+	function initSpecifics(node : Null<ShaderNode>) {
+		if( node == null )
+			return;
 		var shaderPreview = Std.downcast(node, hrt.shgraph.nodes.Preview);
 		if (shaderPreview != null) {
 			shaderPreview.config = config;
 			shaderPreview.shaderGraph = shaderGraph;
-			addBox(p, nodeClass, shaderPreview);
-			return node;
+			return;
 		}
-
 		var subGraphNode = Std.downcast(node, hrt.shgraph.nodes.SubGraph);
 		if (subGraphNode != null) {
 			subGraphNode.loadGraphShader();
-			addBox(p, nodeClass, subGraphNode);
-			return node;
+			return;
 		}
+	}
+
+	function addNode(p : Point, nodeClass : Class<ShaderNode>) {
+		beforeChange();
+
+		var node = shaderGraph.addNode(p.x, p.y, nodeClass);
+		afterChange();
+
+		initSpecifics(node);
 
 		addBox(p, nodeClass, node);
 
 		return node;
 	}
 
+	function addSubGraph(p : Point, path : String) {
+		var node : SubGraph = cast addNode(p, SubGraph);
+		@:privateAccess node.pathShaderGraph = path;
+		node.loadGraphShader();
+		return node;
+	}
+
 	function createEdgeInShaderGraph() : Bool {
 		var startLinkNode = startLinkGrNode.find(".node");
 		if (isCreatingLink == FromInput) {
@@ -816,7 +972,8 @@ class ShaderEditor extends hide.view.Graph {
 		if (endLinkNode.attr("hasLink") != null) {
 			for (edge in listOfEdges) {
 				if (edge.nodeTo.is(endLinkNode)) {
-					removeEdge(edge);
+					super.removeEdge(edge);
+					removeShaderGraphEdge(edge);
 					break;
 				}
 			}
@@ -829,6 +986,7 @@ class ShaderEditor extends hide.view.Graph {
 				currentLink.removeClass("draft");
 				currentLink = null;
 				launchCompileShader();
+				refreshBox(endLinkBox);
 				return true;
 			} else {
 				error("This edge creates a cycle.");
@@ -845,12 +1003,28 @@ class ShaderEditor extends hide.view.Graph {
 	function openAddMenu(?x : Int, ?y : Int) {
 		if (x == null) x = 0;
 		if (y == null) y = 0;
+
+		var boundsWidth = Std.parseInt(element.css("width"));
+		var boundsHeight = Std.parseInt(element.css("height"));
+
+		var posCursor = new IPoint(Std.int(ide.mouseX - parent.offset().left) + x, Std.int(ide.mouseY - parent.offset().top) + y);
+		if( posCursor.x < 0 )
+			posCursor.x = 0;
+		if( posCursor.y < 0)
+			posCursor.y = 0;
+
 		if (addMenu != null) {
+			var menuWidth = Std.parseInt(addMenu.css("width")) + 10;
+			var menuHeight = Std.parseInt(addMenu.css("height")) + 10;
+			if( posCursor.x + menuWidth > boundsWidth )
+				posCursor.x = boundsWidth - menuWidth;
+			if( posCursor.y + menuHeight > boundsHeight )
+				posCursor.y = boundsHeight - menuHeight;
+
 			var input = addMenu.find("#search-input");
 			input.val("");
 			addMenu.show();
 			input.focus();
-			var posCursor = new IPoint(Std.int(ide.mouseX - parent.offset().left) + x, Std.int(ide.mouseY - parent.offset().top) + y);
 
 			addMenu.css("left", posCursor.x);
 			addMenu.css("top", posCursor.y);
@@ -874,11 +1048,6 @@ class ShaderEditor extends hide.view.Graph {
 			</div>
 		</div>').appendTo(parent);
 
-		var posCursor = new IPoint(Std.int(ide.mouseX - parent.offset().left) + x, Std.int(ide.mouseY - parent.offset().top) + y);
-
-		addMenu.css("left", posCursor.x);
-		addMenu.css("top", posCursor.y);
-
 		addMenu.on("mousedown", function(e) {
 			e.stopPropagation();
 		});
@@ -911,6 +1080,14 @@ class ShaderEditor extends hide.view.Graph {
 					</div>').appendTo(results);
 			}
 		}
+		var menuWidth = Std.parseInt(addMenu.css("width")) + 10;
+		var menuHeight = Std.parseInt(addMenu.css("height")) + 10;
+		if( posCursor.x + menuWidth > boundsWidth )
+			posCursor.x = boundsWidth - menuWidth;
+		if( posCursor.y + menuHeight > boundsHeight )
+			posCursor.y = boundsHeight - menuHeight;
+		addMenu.css("left", posCursor.x);
+		addMenu.css("top", posCursor.y);
 
 		var input = addMenu.find("#search-input");
 		input.focus();
@@ -957,8 +1134,15 @@ class ShaderEditor extends hide.view.Graph {
 			if (ev.keyCode == 13) {
 				var key = this.selectedNode.attr("node");
 				var posCursor = new Point(lX(ide.mouseX - 25), lY(ide.mouseY - 10));
-				addNode(posCursor, ShaderNode.registeredNodes[key]);
-				closeAddMenu();
+
+				if( key.toLowerCase().indexOf(".shgraph") != -1 ) {
+					addSubGraph(posCursor, key);
+					closeAddMenu();
+					refreshShaderGraph();
+				} else {
+					addNode(posCursor, ShaderNode.registeredNodes[key]);
+					closeAddMenu();
+				}
 			} else {
 				if (this.selectedNode != null)
 					this.selectedNode.removeClass("selected");
@@ -972,7 +1156,7 @@ class ShaderEditor extends hide.view.Graph {
 						elt.hide();
 						continue;
 					}
-					if (value.length == 0 || elt.children().first().html().toLowerCase().indexOf(value.toLowerCase()) == 1) {
+					if (value.length == 0 || elt.children().first().html().toLowerCase().indexOf(value.toLowerCase()) != -1) {
 						if (isFirst) {
 							this.selectedNode = elt;
 							isFirst = false;
@@ -1003,8 +1187,14 @@ class ShaderEditor extends hide.view.Graph {
 			}
 			var key = ev.getThis().attr("node");
 			var posCursor = new Point(lX(ide.mouseX - 25), lY(ide.mouseY - 10));
-			addNode(posCursor, ShaderNode.registeredNodes[key]);
-			closeAddMenu();
+			if( key.toLowerCase().indexOf(".shgraph") != -1 ) {
+				addSubGraph(posCursor, key);
+				closeAddMenu();
+				refreshShaderGraph();
+			} else {
+				addNode(posCursor, ShaderNode.registeredNodes[key]);
+				closeAddMenu();
+			}
 		});
 	}
 
@@ -1017,13 +1207,14 @@ class ShaderEditor extends hide.view.Graph {
 
 
 	// CONTEXT MENU
-	function contextMenuAddNode(x : Int, y : Int) {
-		var elements = [];
-		var searchItem = new Element("<div class='grey-item' >Search</div>");
-		searchItem.on("click", function() {
-			openAddMenu(-40, -16);
-		});
-		elements.push(searchItem);
+	function contextMenuAddNode() : Array<hide.comp.ContextMenu.ContextMenuItem> {
+		var items : Array<hide.comp.ContextMenu.ContextMenuItem> = [
+			{
+				label : "Search",
+				click : function() { openAddMenu(-40, -16); },
+			},
+			{ label : "", isSeparator : true },
+		];
 
 		var keys = listOfClasses.keys();
 		var sortedKeys = [];
@@ -1036,31 +1227,28 @@ class ShaderEditor extends hide.view.Graph {
 			return 0;
 		});
 
-		for (key in sortedKeys) {
-			var group = new Element('<div> ${key} </div>');
-			group.on("click", function(e) {
-				var eltsGroup = [];
-				var goBack = new Element("<div class='grey-item' > <i class='ico ico-chevron-left' /> Go back </div>");
-				goBack.on("click", function(e) {
-					contextMenuAddNode(Std.parseInt(contextMenu.css("left")), Std.parseInt(contextMenu.css("top")));
-				});
-				eltsGroup.push(goBack);
-				for (node in listOfClasses[key]) {
-					var itemNode = new Element('
-						<div >
-							<span> ${node.name} </span>
-						</div>');
-					itemNode.on("click", function() {
-						var posCursor = new Point(lX(ide.mouseX - 25), lY(ide.mouseY - 10));
-						addNode(posCursor, ShaderNode.registeredNodes[node.key]);
-					});
-					eltsGroup.push(itemNode);
-				}
-				customContextMenu(eltsGroup, Std.parseInt(contextMenu.css("left")), Std.parseInt(contextMenu.css("top")));
-			});
-			elements.push(group);
+		function createNewNode(node : NodeInfo) {
+			var posCursor = new Point(lX(ide.mouseX - 25), lY(ide.mouseY - 10));
+			if( node.key.toLowerCase().indexOf(".shgraph") != -1 ) {
+				addSubGraph(posCursor, node.key);
+				refreshShaderGraph();
+			} else {
+				addNode(posCursor, ShaderNode.registeredNodes[node.key]);
+			}
 		}
-		customContextMenu(elements, x, y);
+
+		var newItems : Array<hide.comp.ContextMenu.ContextMenuItem> = [ for (key in sortedKeys)
+			{
+				label : key,
+				menu : [ for (node in listOfClasses[key])
+					{
+						label : node.name,
+						click : () -> createNewNode(node),
+					}
+				]
+			}
+		];
+		return items.concat(newItems);
 	}
 
 	function beforeChange() {
@@ -1135,8 +1323,147 @@ class ShaderEditor extends hide.view.Graph {
 		return box;
 	}
 
-	override function removeBox(box : Box) {
+	function saveSelection(?boxes) : SavedClipboard {
+		if( boxes == null )
+			boxes = listOfBoxesSelected;
+		if( boxes.length == 0 )
+			return null;
+		var dims = getGraphDims(boxes);
+		var baseX = dims.xMin;
+		var baseY = dims.yMin;
+		var box = boxes[0];
+		var nodes = [
+			for( b in boxes )
+				{
+					pos : new Point(b.getX() - baseX, b.getY() - baseY),
+					nodeType : std.Type.getClass(b.getInstance()),
+					props : b.getInstance().saveProperties(),
+				}
+		];
+
+		var edges : Array<{ fromIdx : Int, fromName : String, toIdx : Int, toName : String }> = [];
+
+		for( edge in listOfEdges ) {
+			for( fromIdx in 0...boxes.length ) {
+				if( boxes[fromIdx] == edge.from ) {
+					for( toIdx in 0...boxes.length ) {
+						if( boxes[toIdx] == edge.to ) {
+							edges.push({
+								fromIdx : fromIdx,
+								fromName : edge.nodeFrom.attr("field"),
+								toIdx : toIdx,
+								toName : edge.nodeTo.attr("field"),
+							});
+						}
+					}
+				}
+			}
+		}
+
+		return {
+			nodes : nodes,
+			edges : edges,
+		};
+	}
+
+	function loadClipboard(offset : Point, val : SavedClipboard, selectNew = true) {
+		if( val == null )
+			return;
+		if( offset == null )
+			offset = new Point(0, 0);
+		var instancedBoxes : Array<Null<Box>> = [];
+		for( n in val.nodes ) {
+			if( n.nodeType == ShaderParam && lastCopyEditor != this ) {
+				instancedBoxes.push(null);
+				continue;
+			}
+			var node = shaderGraph.addNode(offset.x + n.pos.x, offset.y + n.pos.y, n.nodeType);
+			node.loadProperties(n.props);
+			initSpecifics(node);
+			var shaderParam = Std.downcast(node, ShaderParam);
+			if( shaderParam != null ) {
+				var paramShader = shaderGraph.getParameter(shaderParam.parameterId);
+				if( paramShader == null ) {
+					shaderGraph.removeNode(node.id);
+					instancedBoxes.push(null);
+					continue;
+				}
+				shaderParam.variable = paramShader.variable;
+				shaderParam.setName(paramShader.name);
+				setDisplayValue(shaderParam, paramShader.type, paramShader.defaultValue);
+				shaderParam.computeOutputs();
+			}
+			var box = addBox(offset.add(n.pos), n.nodeType, node);
+			instancedBoxes.push(box);
+		}
+		for( edge in val.edges ) {
+			if( instancedBoxes[edge.fromIdx] == null || instancedBoxes[edge.toIdx] == null )
+				continue;
+			var toCreate = {
+				idOutput: instancedBoxes[edge.fromIdx].getId(),
+				nameOutput: edge.fromName,
+				idInput: instancedBoxes[edge.toIdx].getId(),
+				nameInput: edge.toName,
+			}
+			if( !shaderGraph.addEdge(toCreate) ) {
+				error("A pasted edge creates a cycle");
+			}
+		}
+		var newBoxes = [ for( box in instancedBoxes ) if( box != null ) refreshBox(box) ];
+		if( selectNew ) {
+			clearSelectionBoxes();
+			for( box in newBoxes ) {
+				box.setSelected(true);
+			}
+			listOfBoxesSelected = newBoxes;
+		}
+	}
+
+	function duplicateSelection() {
+		if (listOfBoxesSelected.length <= 0)
+			return;
+		var vals = saveSelection(listOfBoxesSelected);
+		var dims = getGraphDims(listOfBoxesSelected);
+		var offset = new Point(dims.xMin + 30, dims.yMin + 30);
+		lastCopyEditor = this;
 		beforeChange();
+		loadClipboard(offset, vals);
+		afterChange();
+	}
+
+	function onCopy() {
+		clipboard = saveSelection(listOfBoxesSelected);
+		lastCopyEditor = this;
+		ide.setClipboard(haxe.Json.stringify(clipboard));
+	}
+
+	function onPaste() {
+		var jsonClipboard = haxe.Json.stringify(clipboard);
+		if( jsonClipboard != ide.getClipboard() || lastCopyEditor == null)
+			return;
+		var posOffset = new Point(lX(ide.mouseX - 40), lY(ide.mouseY - 20));
+		beforeChange();
+		loadClipboard(posOffset, clipboard);
+		afterChange();
+	}
+
+	function deleteSelection() {
+		if (currentEdge != null) {
+			removeEdge(currentEdge);
+		}
+		if (listOfBoxesSelected.length > 0) {
+			beforeChange();
+			for (b in listOfBoxesSelected) {
+				removeBox(b, false);
+			}
+			afterChange();
+			clearSelectionBoxes();
+		}
+	}
+
+	override function removeBox(box : Box, trackChanges = true) {
+		if( trackChanges )
+			beforeChange();
 		var isSubShader = Std.is(box.getInstance(), SubGraph);
 		var length = listOfEdges.length;
 		for (i in 0...length) {
@@ -1148,7 +1475,8 @@ class ShaderEditor extends hide.view.Graph {
 			}
 		}
 		shaderGraph.removeNode(box.getId());
-		afterChange();
+		if( trackChanges )
+			afterChange();
 		box.dispose();
 		listOfBoxes.remove(box);
 		launchCompileShader();
@@ -1190,12 +1518,10 @@ class ShaderEditor extends hide.view.Graph {
 		var valid = false;
 		var offset = 0;
 		for (i in items) {
-			if (i.indexOf("hlshader") != -1 && i != state.path) {
+			if (i.indexOf("shgraph") != -1 && i != state.path) {
 				if (isDrop) {
 					var posCursor = new Point(lX(ide.mouseX - 25 + offset), lY(ide.mouseY - 10 + offset));
-					var node : SubGraph = cast addNode(posCursor, SubGraph);
-					@:privateAccess node.pathShaderGraph = i;
-					node.loadGraphShader();
+					addSubGraph(posCursor, i);
 					offset += 25;
 				}
 				valid = true;
@@ -1207,6 +1533,6 @@ class ShaderEditor extends hide.view.Graph {
 		return valid;
 	}
 
-	static var _ = FileTree.registerExtension(ShaderEditor,["hlshader"],{ icon : "scribd", createNew: "Shader Graph" });
+	static var _ = FileTree.registerExtension(ShaderEditor,["shgraph"],{ icon : "scribd", createNew: "Shader Graph" });
 
 }

+ 1 - 1
hrt/prefab/Shader.hx

@@ -157,7 +157,7 @@ class Shader extends Prefab {
 		for(v in shaderDef.data.vars) {
 			if( v.kind != Param )
 				continue;
-			if( v.qualifiers != null && v.qualifiers.indexOf(Ignore) >= 0 )
+			if( v.qualifiers != null && v.qualifiers.contains(Ignore) )
 				continue;
 			var prop = makeShaderParam(v);
 			if( prop == null ) continue;

+ 2 - 3
hrt/prefab/ShaderGraph.hx

@@ -26,7 +26,7 @@ class ShaderGraph extends DynamicShader {
 
 	#if editor
 	override function getHideProps() : HideProps {
-		return { icon : "cog", name : "Shader Graph", fileSource : ["hlshader"], allowParent : function(p) return p.to(Object2D) != null || p.to(Object3D) != null };
+		return { icon : "scribd", name : "Shader Graph", fileSource : ["shgraph"], allowParent : function(p) return p.to(Object2D) != null || p.to(Object3D) != null };
 	}
 
 	override function edit( ctx : EditContext ) {
@@ -36,12 +36,11 @@ class ShaderGraph extends DynamicShader {
 		btn.on("click", function() {
  			ctx.ide.openFile(source);
 		});
-
 		ctx.properties.add(btn,this.props, function(pname) {
 			ctx.onChange(this, pname);
 		});
 	}
 	#end
 
-	static var _ = Library.register("hlshader", ShaderGraph);
+	static var _ = Library.register("shgraph", ShaderGraph);
 }

+ 0 - 10
hrt/prefab/rfx/RendererFX.hx

@@ -13,16 +13,6 @@ class RendererFX extends Prefab implements h3d.impl.RendererFX {
 	public function dispose() {
 	}
 
-	override function load(obj:Dynamic) {
-		if( obj.props != null ) {
-			// backward compatibility : copy all props to object
-			for( f in Reflect.fields(obj.props) )
-				Reflect.setField(obj, f, Reflect.field(obj.props,f));
-			Reflect.deleteField(obj,"props");
-		}
-		super.load(obj);
-	}
-
 	inline function checkEnabled() {
 		return enabled #if editor && enableInEditor #end;
 	}

+ 265 - 0
hrt/prefab/rfx/ScreenShaderGraph.hx

@@ -0,0 +1,265 @@
+package hrt.prefab.rfx;
+
+import hrt.prefab.rfx.RendererFX;
+import hrt.prefab.Library;
+import hxd.Math;
+
+private class GraphShader extends h3d.shader.ScreenShader {
+
+	static var SRC = {
+		@param var source : Sampler2D;
+
+		function fragment() {
+			pixelColor = source.get(calculatedUV);
+		}
+	}
+}
+class ScreenShaderGraph extends RendererFX {
+
+	var shaderPass = new h3d.pass.ScreenFx(new GraphShader());
+	var shaderGraph : hrt.shgraph.ShaderGraph;
+	var shaderDef : hrt.prefab.ContextShared.ShaderDef;
+	var shader : hxsl.DynamicShader;
+
+	override function end(r:h3d.scene.Renderer, step:h3d.impl.RendererFX.Step) {
+		if( !checkEnabled() ) return;
+		if( step == AfterTonemapping ) {
+			r.mark("ScreenShaderGraph");
+			if (shader != null) {
+				var ctx = r.ctx;
+				var target = r.allocTarget("ppTarget", false);
+				shaderPass.shader.source = ctx.getGlobal("ldrMap");
+
+				ctx.engine.pushTarget(target);
+				shaderPass.render();
+				ctx.engine.popTarget();
+
+				ctx.setGlobal("ldrMap", target);
+				r.setTarget(target);
+			}
+		}
+		if( step == BeforeTonemapping ) {
+			r.mark("ScreenShaderGraph");
+			if (shader != null) {
+				var ctx = r.ctx;
+				var target = r.allocTarget("ppTarget", false);
+				shaderPass.shader.source = ctx.getGlobal("hdrMap");
+
+				ctx.engine.pushTarget(target);
+				shaderPass.render();
+				ctx.engine.popTarget();
+
+				ctx.setGlobal("hdrMap", target);
+				r.setTarget(target);
+			}
+		}
+	}
+
+	override function load( obj : Dynamic ) {
+		loadSerializedFields(obj);
+	}
+
+	public function loadShaderDef() {
+		shaderDef = shaderGraph.compile();
+		if(shaderDef == null)
+			return;
+
+		#if editor
+		for( v in shaderDef.inits ) {
+			if (props == null)
+				props = {};
+			if(!Reflect.hasField(props, v.variable.name)) {
+				Reflect.setField(props, v.variable.name, v.value);
+			}
+		}
+		#end
+	}
+
+	function getShaderDefinition():hxsl.SharedShader {
+		if( shaderDef == null )
+			loadShaderDef();
+		return shaderDef == null ? null : shaderDef.shader;
+	}
+
+	function setShaderParam(shader:hxsl.Shader, v:hxsl.Ast.TVar, value:Dynamic) {
+		cast(shader,hxsl.DynamicShader).setParamValue(v, value);
+	}
+
+	function syncShaderVars() {
+		for(v in shaderDef.shader.data.vars) {
+			if(v.kind != Param)
+				continue;
+			var val : Dynamic = Reflect.field(props, v.name);
+			switch(v.type) {
+			case TVec(_, VFloat):
+				if(val != null) {
+					if( Std.is(val,Int) ) {
+						var v = new h3d.Vector();
+						v.setColor(val);
+						val = v;
+					} else
+						val = h3d.Vector.fromArray(val);
+				} else
+					val = new h3d.Vector();
+			case TSampler2D:
+				if( val != null )
+					val = hxd.res.Loader.currentInstance.load(val).toTexture();
+				else {
+					var childNoise = getOpt(hrt.prefab.l2d.NoiseGenerator, v.name);
+					if(childNoise != null)
+						val = childNoise.toTexture();
+				}
+			default:
+			}
+			if(val == null)
+				continue;
+			setShaderParam(shader,v,val);
+		}
+	}
+
+	function makeShader() {
+		if( getShaderDefinition() == null )
+			return null;
+		var dshader = new hxsl.DynamicShader(shaderDef.shader);
+		for( v in shaderDef.inits ) {
+			#if !hscript
+			throw "hscript required";
+			#else
+			dshader.hscriptSet(v.variable.name, v.value);
+			#end
+		}
+		shader = dshader;
+		syncShaderVars();
+		shaderPass.addShader(shader);
+		return shader;
+	}
+
+	override function makeInstance(ctx: Context) : Context {
+		ctx = super.makeInstance(ctx);
+		updateInstance(ctx);
+		return ctx;
+	}
+
+	override function updateInstance( ctx: Context, ?propName : String ) {
+		var p = resolveRef(ctx.shared);
+		if(p == null)
+			return;
+		if (shader == null)
+			shader = makeShader();
+		else
+			syncShaderVars();
+	}
+
+	public function resolveRef(shared : hrt.prefab.ContextShared) {
+		if(shaderGraph != null)
+			return shaderGraph;
+		if(source == null)
+			return null;
+
+		#if editor
+		shaderGraph = new hrt.shgraph.ShaderGraph(source);
+		#else
+		return null;
+		#end
+		return shaderGraph;
+	}
+
+	function makeShaderParam( v : hxsl.Ast.TVar ) : hrt.prefab.Props.PropType {
+		var min : Null<Float> = null, max : Null<Float> = null;
+		if( v.qualifiers != null )
+			for( q in v.qualifiers )
+				switch( q ) {
+				case Range(rmin, rmax): min = rmin; max = rmax;
+				default:
+				}
+		return switch( v.type ) {
+		case TInt:
+			PInt(min == null ? null : Std.int(min), max == null ? null : Std.int(max));
+		case TFloat:
+			PFloat(min != null ? min : 0.0, max != null ? max : 1.0);
+		case TBool:
+			PBool;
+		case TSampler2D:
+			PTexture;
+		case TVec(n, VFloat):
+			PVec(n);
+		default:
+			PUnsupported(hxsl.Ast.Tools.toString(v.type));
+		}
+	}
+
+	#if editor
+	override function edit( ectx : hide.prefab.EditContext ) {
+		var element = new hide.Element('
+			<div class="group" name="Reference">
+			<dl>
+				<dt>Reference</dt><dd><input type="fileselect" extensions="shgraph" field="source"/></dd>
+			</dl>
+			</div>');
+
+		function updateProps() {
+			var input = element.find("input");
+			updateInstance(ectx.rootContext);
+			var found = shaderGraph != null;
+			input.toggleClass("error", !found);
+		}
+		updateProps();
+
+		ectx.properties.add(element, this, function(pname) {
+			ectx.onChange(this, pname);
+			if(pname == "source") {
+				shaderGraph = null;
+				shaderPass.removeShader(shader);
+				shader = null;
+				if (shaderDef != null) {
+					for(v in shaderDef.inits) {
+						if (Reflect.hasField(props, v.variable.name))
+							Reflect.deleteField(props, v.variable.name);
+					}
+					shaderDef = null;
+				}
+
+				updateProps();
+				ectx.properties.clear();
+				edit(ectx);
+			}
+		});
+
+
+		super.edit(ectx);
+		if (shaderGraph == null)
+			return;
+		getShaderDefinition();
+
+		var group = new hide.Element('<div class="group" name="Shader"></div>');
+		var props = [];
+		for(v in shaderDef.shader.data.vars) {
+			if( v.kind != Param )
+				continue;
+			if( v.qualifiers != null && v.qualifiers.contains(Ignore) )
+				continue;
+			var prop = makeShaderParam(v);
+			if( prop == null ) continue;
+			props.push({name: v.name, t: prop, def: Reflect.field(this.props, v.name)});
+		}
+		group.append(hide.comp.PropsEditor.makePropsList(props));
+		ectx.properties.add(group, this.props, function(pname) {
+			ectx.onChange(this, pname);
+			updateInstance(ectx.rootContext, pname);
+
+		});
+
+		var btn = new hide.Element("<input type='submit' style='width: 100%; margin-top: 10px;' value='Open Shader Graph' />");
+		btn.on("click", function() {
+ 			ectx.ide.openFile(source);
+		});
+
+		ectx.properties.add(btn, this, function(pname) {
+			ectx.onChange(this, pname);
+		});
+	}
+	#end
+
+	static var _ = Library.register("rfx.ScreenShaderGraph", ScreenShaderGraph);
+
+}

+ 74 - 0
hrt/shgraph/CustomVarChooser.hx

@@ -0,0 +1,74 @@
+package hrt.shgraph;
+
+using hxsl.Ast;
+
+#if editor
+// TODO block or raise an error on illegal names
+class CustomVarChooser extends hide.comp.Component {
+	public var variable : TVar = null;
+
+	public function new(?parent : hide.Element, ?initialName : String, ?initialType : Type, onChange : (TVar) -> Void) {
+		var el = new hide.Element('<div class="custom-var">
+			<div class="custom-var-row">
+				<label for="customName">Name</label><span><input type="text" id="customName"/></span>
+			</div>
+			<div class="custom-var-row">
+				<label for="customType">Type</label><span><select id="customType"></select></span>
+			</div>
+		</div>');
+		super(parent, el);
+
+		var textInput = element.find("#customName");
+		var select = element.find("#customType");
+		var availableTypes = [
+			"Color" => TVec( 4, VFloat ),
+			"Int" => TInt,
+			"Bool" => TBool,
+			"Float" => TFloat,
+			"String" => TString,
+			"Vec2" => TVec( 2, VFloat ),
+			"Vec3" => TVec( 3, VFloat ),
+		];
+		for( key => value in availableTypes ) {
+			select.append(new hide.Element('<option value="${key}">${key}</option>'));
+			if( value == initialType )
+				select.val(key);
+		}
+		if( initialName != null ) {
+			textInput.val(initialName);
+		}
+		if( initialName != null && initialName != "" && initialType != null ) {
+			variable = {
+				parent: null,
+				id: 0,
+				kind: Local,
+				name: initialName,
+				type: availableTypes[select.val()],
+			};
+		}
+		function changedFun(_) {
+			var name = textInput.val();
+			if( name == "" )
+				return;
+			variable = {
+				parent: null,
+				id: 0,
+				kind: Local,
+				name: name,
+				type: availableTypes[select.val()],
+			};
+			onChange(variable);
+		}
+		select.on("change", changedFun);
+		textInput.on("change", changedFun);
+	}
+
+	public function show() {
+		element.show();
+	}
+
+	public function hide() {
+		element.hide();
+	}
+}
+#end

+ 2 - 2
hrt/shgraph/ShaderGlobalInput.hx

@@ -29,9 +29,9 @@ class ShaderGlobalInput extends ShaderInput {
 		var element = new hide.Element('<div style="width: 120px; height: 30px"></div>');
 		element.append(new hide.Element('<select id="variable"></select>'));
 
-		if (this.variable == null) 
+		if (this.variable == null)
 			this.variable = ShaderGlobalInput.globalInputs[0];
-		
+
 		var input = element.children("select");
 		var indexOption = 0;
 		for (c in ShaderGlobalInput.globalInputs) {

+ 12 - 5
hrt/shgraph/ShaderGraph.hx

@@ -101,8 +101,8 @@ class ShaderGraph {
 			var shaderParam = Std.downcast(n.instance, ShaderParam);
 			if (shaderParam != null) {
 				var paramShader = getParameter(shaderParam.parameterId);
-				shaderParam.computeOutputs();
 				shaderParam.variable = paramShader.variable;
+				shaderParam.computeOutputs();
 			}
 		}
 		if (nodes[nodes.length-1] != null)
@@ -118,6 +118,15 @@ class ShaderGraph {
 		var output = this.nodes.get(edge.idOutput);
 		node.instance.setInput(edge.nameInput, new NodeVar(output.instance, edge.nameOutput));
 		output.outputs.push(node);
+
+		var subShaderIn = Std.downcast(node.instance, hrt.shgraph.nodes.SubGraph);
+		var subShaderOut = Std.downcast(output.instance, hrt.shgraph.nodes.SubGraph);
+		if( @:privateAccess ((subShaderIn != null) && !subShaderIn.inputInfoKeys.contains(edge.nameInput))
+			|| @:privateAccess ((subShaderOut != null) && !subShaderOut.outputInfoKeys.contains(edge.nameOutput))
+		) {
+			removeEdge(edge.idInput, edge.nameInput, false);
+		}
+
 		#if editor
 		if (hasCycle()){
 			removeEdge(edge.idInput, edge.nameInput, false);
@@ -217,7 +226,8 @@ class ShaderGraph {
 			}
 			allVariables.push(shaderParam.variable);
 			allParameters.push(shaderParam.variable);
-			allParamDefaultValue.push(getParameter(shaderParam.parameterId).defaultValue);
+			if (parametersAvailable.exists(shaderParam.parameterId))
+				allParamDefaultValue.push(getParameter(shaderParam.parameterId).defaultValue);
 		}
 		if (isSubGraph) {
 			var subGraph = Std.downcast(node, hrt.shgraph.nodes.SubGraph);
@@ -281,9 +291,6 @@ class ShaderGraph {
 		var contentFragment = [];
 
 		for (n in nodes) {
-			if (subShaderId != null && Std.is(n.instance, hrt.shgraph.nodes.SubGraph)) {
-				throw ShaderException.t("A subgraph can't have a subgraph", -1);
-			}
 			if (!variableNamesAlreadyUpdated && subShaderId != null && !Std.is(n.instance, ShaderInput)) {
 				for (outputKey in n.instance.getOutputInfoKeys()) {
 					var output = n.instance.getOutput(outputKey);

+ 1 - 1
hrt/shgraph/ShaderNode.hx

@@ -2,7 +2,7 @@ package hrt.shgraph;
 
 using hxsl.Ast;
 
-typedef InputInfo = { name : String, type : ShaderType.SType, hasProperty : Bool, isRequired : Bool, ?id : Int };
+typedef InputInfo = { name : String, type : ShaderType.SType, hasProperty : Bool, isRequired : Bool, ?ids : Array<Int> };
 typedef OutputInfo = { name : String, type : ShaderType.SType, ?id : Int };
 
 @:autoBuild(hrt.shgraph.ParseFieldsMacro.build())

+ 55 - 5
hrt/shgraph/ShaderOutput.hx

@@ -19,7 +19,6 @@ class ShaderOutput extends ShaderNode {
 	}
 
 	override public function build(key : String) : TExpr {
-
 		return {
 				p : null,
 				t : TVoid,
@@ -71,7 +70,9 @@ class ShaderOutput extends ShaderNode {
 	];
 
 	override public function loadProperties(props : Dynamic) {
-		var paramVariable : Array<String> = Reflect.field(props, "variable");
+		var paramVariable : Array<Dynamic> = Reflect.field(props, "variable");
+		if( paramVariable[0] == null)
+			return;
 
 		for (c in ShaderNode.availableVariables) {
 			if (c.name == paramVariable[0]) {
@@ -85,11 +86,30 @@ class ShaderOutput extends ShaderNode {
 				return;
 			}
 		}
+		var type: Type;
+		try {
+			type = haxe.EnumTools.createByName(Type, paramVariable[1], paramVariable[2]);
+		} catch( e ) {
+			trace('Received invalid props for output node. id: $id, variable name: ${paramVariable[0]}');
+			return;
+		}
+		this.variable = {
+			parent: null,
+			id: 0,
+			kind: Local,
+			name: paramVariable[0],
+			type: type,
+		};
 	}
 
 	override public function saveProperties() : Dynamic {
+		var content : Array<Dynamic> = (variable == null) ? [null] : [
+			variable.name,
+			variable.type.getName(),
+			variable.type.getParameters()
+		];
 		var parameters = {
-			variable: (variable == null) ? [null] : [variable.name, variable.type.getName()]
+			variable: content,
 		};
 
 		return parameters;
@@ -99,7 +119,7 @@ class ShaderOutput extends ShaderNode {
 	#if editor
 	override public function getPropertiesHTML(width : Float) : Array<hide.Element> {
 		var elements = super.getPropertiesHTML(width);
-		var element = new hide.Element('<div style="width: 110px; height: 30px"></div>');
+		var element = new hide.Element('<div style="width: 110px; height: 70px"></div>');
 		element.append(new hide.Element('<select id="variable"></select>'));
 
 		if (this.variable == null) {
@@ -107,10 +127,12 @@ class ShaderOutput extends ShaderNode {
 		}
 		var input = element.children("select");
 		var indexOption = 0;
+		var selectingDefault = false;
 		for (c in ShaderNode.availableVariables) {
 			input.append(new hide.Element('<option value="${indexOption}">${c.name}</option>'));
 			if (this.variable.name == c.name) {
 				input.val(indexOption);
+				selectingDefault = true;
 			}
 			indexOption++;
 		}
@@ -118,16 +140,44 @@ class ShaderOutput extends ShaderNode {
 			input.append(new hide.Element('<option value="${indexOption}">${c.name}</option>'));
 			if (this.variable.name == c.name) {
 				input.val(indexOption);
+				selectingDefault = true;
 			}
 			indexOption++;
 		}
+		var maxIndex = indexOption;
+		input.append(new hide.Element('<option value="${maxIndex}">Other...</option>'));
+		var initialName : String = null;
+		var initialType : Type = null;
+		if( !selectingDefault ) {
+			input.val(maxIndex);
+			initialName = this.variable.name;
+			initialType = this.variable.type;
+		}
+
+		var customVarChooser = new CustomVarChooser(element, initialName, initialType, function(val) {
+			this.variable = val;
+		});
+
+		if( !selectingDefault )
+			customVarChooser.show();
+		else
+			customVarChooser.hide();
+
 		input.on("change", function(e) {
 			var value = input.val();
 			if (value < ShaderNode.availableVariables.length) {
 				this.variable = ShaderNode.availableVariables[value];
-			} else {
+			} else if (value < maxIndex) {
 				this.variable = ShaderOutput.availableOutputs[value-ShaderNode.availableVariables.length];
 			}
+			if (value == maxIndex) {
+				customVarChooser.show();
+				if (customVarChooser.variable != null) {
+					this.variable = customVarChooser.variable;
+				}
+			} else {
+				customVarChooser.hide();
+			}
 		});
 
 		elements.push(element);

+ 15 - 1
hrt/shgraph/ShaderParam.hx

@@ -10,6 +10,8 @@ class ShaderParam extends ShaderNode {
 	@output() var output = SType.Variant;
 
 	@prop() public var parameterId : Int;
+	@prop() public var perInstance : Bool;
+
 
 	public var variable : TVar;
 
@@ -26,17 +28,29 @@ class ShaderParam extends ShaderNode {
 
 	override public function loadProperties(props : Dynamic) {
 		parameterId = Reflect.field(props, "parameterId");
+		perInstance = Reflect.field(props, "perInstance");
 	}
 
 	override public function saveProperties() : Dynamic {
 		var parameters = {
-			parameterId: parameterId
+			parameterId: parameterId,
+			perInstance: perInstance
 		};
 
 		return parameters;
 	}
 
 	override public function build(key : String) : TExpr {
+		if (variable != null){
+			if (variable.qualifiers == null)
+				variable.qualifiers = [];
+			if (perInstance)
+				if (!variable.qualifiers.contains(PerInstance(1)))
+					variable.qualifiers.push(PerInstance(1));
+			else
+				if (variable.qualifiers.contains(PerInstance(1)))
+					variable.qualifiers.remove(PerInstance(1));
+		}
 		return null;
 	}
 

+ 3 - 3
hrt/shgraph/nodes/Clamp.hx

@@ -8,9 +8,9 @@ using hxsl.Ast;
 @group("Math")
 class Clamp extends ShaderFunction {
 
-	@input("x") var x = SType.Number;
-	@input("min", true) var min = SType.Number;
-	@input("max", true) var max = SType.Number;
+	@input("X") var x = SType.Number;
+	@input("Min", true) var min = SType.Number;
+	@input("Max", true) var max = SType.Number;
 
 	public function new() {
 		super(Saturate);

+ 4 - 2
hrt/shgraph/nodes/Combine.hx

@@ -15,7 +15,7 @@ class Combine extends ShaderNode {
 	@input("B", false, false) var b = SType.Float;
 	@input("A", false, false) var a = SType.Float;
 
-	@output() var output = SType.Variant;
+	@output() var output = SType.Number;
 
 	var components = [X, Y, Z, W];
 	var componentsString = ["r", "g", "b", "a"];
@@ -41,13 +41,15 @@ class Combine extends ShaderNode {
 	}
 
 	override public function computeOutputs() {
-		numberOutputs = 1;
+		numberOutputs = 0;
 		if (a != null && !a.isEmpty()) {
 			numberOutputs = 4;
 		} else if (b != null && !b.isEmpty()) {
 			numberOutputs = 3;
 		} else if (g != null && !g.isEmpty()) {
 			numberOutputs = 2;
+		} else if (r != null && !r.isEmpty()) {
+			numberOutputs = 1;
 		}
 		if (numberOutputs == 1) {
 			addOutput("output", TFloat);

+ 60 - 0
hrt/shgraph/nodes/CombineAlpha.hx

@@ -0,0 +1,60 @@
+package hrt.shgraph.nodes;
+
+import hxsl.Types.Vec;
+import hxsl.*;
+
+using hxsl.Ast;
+
+@name("Combine Alpha")
+@description("Create a vector of size 4 from a RGB and an Alpha float")
+@group("Channel")
+class CombineAlpha extends ShaderNode {
+
+	@input("RGB") var rgb = SType.Vec3;
+	@input("A", true) var a = SType.Float;
+
+	@output("RGBA") var output = SType.Vec4;
+
+	override public function computeOutputs() {
+
+		addOutput("output", TVec(4, VFloat));
+	}
+
+	override public function build(key : String) : TExpr {
+
+		var args = [];
+		var valueArgs = [];
+		var opTGlobal : TGlobal = Vec4;
+		args.push({ name: "rgb", type : TVec(3, VFloat) });
+		valueArgs.push(rgb.getVar());
+		args.push({ name: "a", type : TFloat });
+		valueArgs.push(a.getVar());
+		opTGlobal = Vec4;
+
+		return {
+			p : null,
+			t : output.type,
+			e : TBinop(OpAssign, {
+				e: TVar(output),
+				p: null,
+				t : output.type
+			},
+			{
+				e: TCall({
+					e: TGlobal(opTGlobal),
+					p: null,
+					t: TFun([
+						{
+							ret: output.type,
+							args: args
+						}
+					])
+				}, valueArgs
+				),
+				p: null,
+				t: output.type
+			})
+		};
+	}
+
+}

+ 4 - 4
hrt/shgraph/nodes/Cond.hx

@@ -7,10 +7,10 @@ using hxsl.Ast;
 @group("Condition")
 class Cond extends ShaderNode {
 
-	@input("left") var leftVar = SType.Number;
-	@input("right") var rightVar = SType.Number;
+	@input("Left") var leftVar = SType.Number;
+	@input("Right") var rightVar = SType.Number;
 
-	@output("boolean") var output = SType.Bool;
+	@output("Boolean") var output = SType.Bool;
 
 	@prop() var condition : Binop;
 
@@ -60,7 +60,7 @@ class Cond extends ShaderNode {
 	var conditionStrings 	= ["==", "!=",    ">",  ">=",  "<",  "<=",  "AND", "OR"];
 
 	override public function loadProperties(props : Dynamic) {
-		this.condition = std.Type.createEnum(Binop, Reflect.field(props, "condition"));
+		this.condition = std.Type.createEnum(Binop, Reflect.field(props, "Condition"));
 	}
 
 	override public function saveProperties() : Dynamic {

+ 2 - 2
hrt/shgraph/nodes/Cross.hx

@@ -8,8 +8,8 @@ using hxsl.Ast;
 @group("Math")
 class Cross extends ShaderFunction {
 
-	@input("a") var a = SType.Number;
-	@input("b") var b = SType.Number;
+	@input("A") var a = SType.Number;
+	@input("B") var b = SType.Number;
 
 	public function new() {
 		super(Cross);

+ 2 - 2
hrt/shgraph/nodes/Dot.hx

@@ -8,8 +8,8 @@ using hxsl.Ast;
 @group("Math")
 class Dot extends ShaderFunction {
 
-	@input("a") var a = SType.Number;
-	@input("b") var b = SType.Number;
+	@input("A") var a = SType.Number;
+	@input("B") var b = SType.Number;
 
 	public function new() {
 		super(Dot);

+ 2 - 2
hrt/shgraph/nodes/Exp.hx

@@ -8,8 +8,8 @@ using hxsl.Ast;
 @group("Math")
 class Exp extends ShaderFunction {
 
-	@input("x") var x = SType.Number;
-	@input("p", true) var p = SType.Number;
+	@input("X") var x = SType.Number;
+	@input("P", true) var p = SType.Number;
 
 	public function new() {
 		super(Exp);

+ 1 - 1
hrt/shgraph/nodes/Floor.hx

@@ -8,7 +8,7 @@ using hxsl.Ast;
 @group("Math")
 class Floor extends ShaderFunction {
 
-	@input("x") var x = SType.Number;
+	@input("X") var x = SType.Number;
 
 	public function new() {
 		super(Floor);

+ 1 - 1
hrt/shgraph/nodes/Fract.hx

@@ -8,7 +8,7 @@ using hxsl.Ast;
 @group("Math")
 class Fract extends ShaderFunction {
 
-	@input("x") var x = SType.Number;
+	@input("X") var x = SType.Number;
 
 	public function new() {
 		super(Fract);

+ 3 - 3
hrt/shgraph/nodes/IfCondition.hx

@@ -7,9 +7,9 @@ using hxsl.Ast;
 @group("Condition")
 class IfCondition extends ShaderNode {
 
-	@input("condition") var condition = SType.Bool;
-	@input("true") var trueVar = SType.Variant;
-	@input("false") var falseVar = SType.Variant;
+	@input("Condition") var condition = SType.Bool;
+	@input("True") var trueVar = SType.Variant;
+	@input("False") var falseVar = SType.Variant;
 
 	@output() var output = SType.Variant;
 

+ 24 - 0
hrt/shgraph/nodes/Length.hx

@@ -0,0 +1,24 @@
+package hrt.shgraph.nodes;
+
+using hxsl.Ast;
+
+@name("Length")
+@description("")
+@width(80)
+@group("Math")
+class Length extends ShaderFunction {
+
+	@input("A") var a = SType.Vec2;
+
+	public function new() {
+		super(Length);
+	}
+
+	override public function computeOutputs() {
+		if (a != null && !a.isEmpty())
+			addOutput("output", TFloat);
+		else
+			removeOutput("output");
+	}
+
+}

+ 2 - 2
hrt/shgraph/nodes/Log.hx

@@ -8,8 +8,8 @@ using hxsl.Ast;
 @group("Math")
 class Log extends ShaderFunction {
 
-	@input("x") var x = SType.Number;
-	@input("p", true) var p = SType.Number;
+	@input("X") var x = SType.Number;
+	@input("P", true) var p = SType.Number;
 
 	public function new() {
 		super(Log);

+ 4 - 4
hrt/shgraph/nodes/Mix.hx

@@ -3,14 +3,14 @@ package hrt.shgraph.nodes;
 using hxsl.Ast;
 
 @name("Mix")
-@description("Linear interpolation between a and b using mix")
+@description("Linear interpolation between A and B using Mix")
 @width(80)
 @group("Math")
 class Mix extends ShaderFunction {
 
-	@input("a") var x = SType.Number;
-	@input("b") var y = SType.Number;
-	@input("mix") var a = SType.Number;
+	@input("A") var x = SType.Number;
+	@input("B") var y = SType.Number;
+	@input("Mix") var a = SType.Number;
 
 	public function new() {
 		super(Mix);

+ 2 - 2
hrt/shgraph/nodes/Mod.hx

@@ -8,8 +8,8 @@ using hxsl.Ast;
 @group("Math")
 class Mod extends ShaderFunction {
 
-	@input("x") var x = SType.Variant;
-	@input("mod", true) var mod = SType.Float;
+	@input("X") var x = SType.Variant;
+	@input("Mod", true) var mod = SType.Float;
 
 	public function new() {
 		super(Mod);

+ 1 - 1
hrt/shgraph/nodes/Normalize.hx

@@ -8,7 +8,7 @@ using hxsl.Ast;
 @group("Math")
 class Normalize extends ShaderFunction {
 
-	@input("x") var x = SType.Number;
+	@input("X") var x = SType.Number;
 
 	public function new() {
 		super(Normalize);

+ 2 - 2
hrt/shgraph/nodes/Pow.hx

@@ -8,8 +8,8 @@ using hxsl.Ast;
 @group("Math")
 class Pow extends ShaderFunction {
 
-	@input("x") var x = SType.Number;
-	@input("p", true) var p = SType.Number;
+	@input("X") var x = SType.Number;
+	@input("P", true) var p = SType.Number;
 
 	public function new() {
 		super(Pow);

+ 1 - 1
hrt/shgraph/nodes/Preview.hx

@@ -11,7 +11,7 @@ using hxsl.Ast;
 @noheader()
 class Preview extends ShaderNode {
 
-	@input("input") var input = SType.Vec4;
+	@input("Input") var input = SType.Vec4;
 
 	public var variable : TVar;
 

+ 7 - 7
hrt/shgraph/nodes/Sampler.hx

@@ -7,14 +7,14 @@ using hxsl.Ast;
 @group("Property")
 class Sampler extends ShaderNode {
 
-	@input("texture") var texture = SType.Sampler;
-	@input("uv") var uv = SType.Vec2;
+	@input("Texture") var texture = SType.Sampler;
+	@input("UV") var uv = SType.Vec2;
 
-	@output("rgba") var rgba = SType.Vec4;
-	@output("r") var r = SType.Float;
-	@output("g") var g = SType.Float;
-	@output("b") var b = SType.Float;
-	@output("a") var a = SType.Float;
+	@output("RGBA") var rgba = SType.Vec4;
+	@output("R") var r = SType.Float;
+	@output("G") var g = SType.Float;
+	@output("B") var b = SType.Float;
+	@output("A") var a = SType.Float;
 
 	var components = [X, Y, Z, W];
 	var componentsString = ["r", "g", "b", "a"];

+ 24 - 0
hrt/shgraph/nodes/Saturate.hx

@@ -0,0 +1,24 @@
+package hrt.shgraph.nodes;
+
+using hxsl.Ast;
+
+@name("Saturate")
+@description("Saturate input A")
+@width(80)
+@group("Math")
+class Saturate extends ShaderFunction {
+
+	@input("A") var a = SType.Float;
+
+	public function new() {
+		super(Saturate);
+	}
+
+	override public function computeOutputs() {
+		if (a != null && !a.isEmpty())
+			addOutput("output", a.getType());
+		else
+			removeOutput("output");
+	}
+
+}

+ 6 - 6
hrt/shgraph/nodes/SmoothStep.hx

@@ -2,15 +2,15 @@ package hrt.shgraph.nodes;
 
 using hxsl.Ast;
 
-@name("SmoothStep")
-@description("Linear interpolation between a and b using mix")
-@width(80)
+@name("Smooth Step")
+@description("Linear interpolation between A and B using Mix")
+@width(100)
 @group("Math")
 class SmoothStep extends ShaderFunction {
 
-	@input("a") var x = SType.Number;
-	@input("b") var y = SType.Number;
-	@input("mix") var a = SType.Number;
+	@input("A") var x = SType.Number;
+	@input("B") var y = SType.Number;
+	@input("Mix") var a = SType.Number;
 
 	public function new() {
 		super(Smoothstep);

+ 2 - 2
hrt/shgraph/nodes/Step.hx

@@ -8,8 +8,8 @@ using hxsl.Ast;
 @group("Math")
 class Step extends ShaderFunction {
 
-	@input("edge") var edge = SType.Number;
-	@input("a") var x = SType.Number;
+	@input("Edge") var edge = SType.Number;
+	@input("A") var x = SType.Number;
 
 	public function new() {
 		super(Step);

+ 49 - 0
hrt/shgraph/nodes/StripAlpha.hx

@@ -0,0 +1,49 @@
+package hrt.shgraph.nodes;
+
+using hxsl.Ast;
+
+@name("Strip Alpha")
+@description("Separate the rgb and a components of an rgba vector")
+@group("Channel")
+class StripAlpha extends ShaderNode {
+
+	@input("RGBA") var input = SType.Vec4;
+
+	@output("RGB") var rgb = SType.Vec3;
+	@output("A") var a = SType.Float;
+
+	override public function computeOutputs() {
+		addOutput("rgb", TVec(3, VFloat));
+		addOutput("a", TFloat);
+	}
+
+	override public function build(key : String) : TExpr {
+        if( key == "a" ) {
+            return { e: TBinop(OpAssign, {
+                    e: TVar(getOutput(key)),
+                    p: null,
+                    t: getOutput(key).type
+                }, {
+                    e: TSwiz(input.getVar(TVec(4, VFloat)), [W]),
+                    p: null,
+                    t: getOutput(key).type
+                }),
+                p: null,
+                t: getOutput(key).type
+            };
+	    }
+        return { e: TBinop(OpAssign, {
+                e: TVar(getOutput(key)),
+                p: null,
+                t: getOutput(key).type
+            }, {
+                e: TSwiz(input.getVar(TVec(4, VFloat)), [X, Y, Z]),
+                p: null,
+                t: getOutput(key).type
+            }),
+            p: null,
+            t: getOutput(key).type
+        };
+    }
+
+}

+ 53 - 40
hrt/shgraph/nodes/SubGraph.hx

@@ -4,7 +4,6 @@ using hxsl.Ast;
 
 @name("SubGraph")
 @description("Include a subgraph")
-@group("Other")
 @width(250)
 @alwaysshowinputs()
 class SubGraph extends ShaderNode {
@@ -27,7 +26,7 @@ class SubGraph extends ShaderNode {
 			try {
 				subShaderGraph = new ShaderGraph(pathShaderGraph);
 			} catch (e : Dynamic) {
-				trace("The shader doesn't not exist.");
+				trace("The shader does not exist.");
 				return;
 			}
 			inputsInfo = new Map<String, ShaderNode.InputInfo>();
@@ -43,28 +42,57 @@ class SubGraph extends ShaderNode {
 					case "ShaderParam": // params become inputs
 						var shaderParam = Std.downcast(node.instance, ShaderParam);
 						var paramName = subShaderGraph.getParameter(shaderParam.parameterId).name;
-
-						inputsInfo.set(prefixSubGraph+node.id, { name : paramName , type: ShaderType.getSType(shaderParam.variable.type), hasProperty: false, isRequired : false, id : node.id });
-						inputInfoKeys.push(prefixSubGraph+node.id);
+						var paramId = "param_" + shaderParam.parameterId;
+						var paramInfo = inputsInfo.get(prefixSubGraph+paramId);
+						var ids = [];
+						if (paramInfo != null && paramInfo.ids != null)
+							ids = paramInfo.ids;
+						ids.push(node.id);
+						if (!inputsInfo.exists(prefixSubGraph + paramId)) {
+							inputInfoKeys.push(prefixSubGraph+paramId);
+						}
+						inputsInfo.set(prefixSubGraph+paramId, { name : paramName , type: ShaderType.getSType(shaderParam.variable.type), hasProperty: false, isRequired : false, ids : ids });
 					case "ShaderInput":
 						var shaderInput = Std.downcast(node.instance, ShaderInput);
-
-						inputsInfo.set(prefixSubGraph+node.id, { name : "*" + shaderInput.variable.name , type: ShaderType.getSType(shaderInput.variable.type), hasProperty: false, isRequired : false, id : node.id });
-						inputInfoKeys.push(prefixSubGraph+node.id);
+						var inputId = "input_" + shaderInput.variable.name;
+						var inputInfo = inputsInfo.get(prefixSubGraph+inputId);
+						var ids = [];
+						if (inputInfo != null && inputInfo.ids != null)
+							ids = inputInfo.ids;
+						ids.push(node.id);
+						if (!inputsInfo.exists(prefixSubGraph+inputId)) {
+							inputInfoKeys.push(prefixSubGraph+inputId);
+						}
+						inputsInfo.set(prefixSubGraph+inputId, { name : "*" + shaderInput.variable.name , type: ShaderType.getSType(shaderInput.variable.type), hasProperty: false, isRequired : false, ids : ids });
 					case "ShaderGlobalInput":
 						var shaderInput = Std.downcast(node.instance, ShaderGlobalInput);
-
-						inputsInfo.set(prefixSubGraph+node.id, { name : "*" + shaderInput.variable.name , type: ShaderType.getSType(shaderInput.variable.type), hasProperty: false, isRequired : false, id : node.id });
-						inputInfoKeys.push(prefixSubGraph+node.id);
+						var inputId = "globalInput_" + shaderInput.variable.name;
+						var inputInfo = inputsInfo.get(prefixSubGraph+inputId);
+						var ids = [];
+						if (inputInfo != null && inputInfo.ids != null)
+							ids = inputInfo.ids;
+						ids.push(node.id);
+						if (!inputsInfo.exists(prefixSubGraph+inputId)) {
+							inputInfoKeys.push(prefixSubGraph+inputId);
+						}
+						inputsInfo.set(prefixSubGraph+inputId, { name : "*" + shaderInput.variable.name , type: ShaderType.getSType(shaderInput.variable.type), hasProperty: false, isRequired : false, ids : ids });
 					case "ShaderCameraInput":
 						var shaderInput = Std.downcast(node.instance, ShaderCameraInput);
-
-						inputsInfo.set(prefixSubGraph+node.id, { name : "*" + shaderInput.variable.name , type: ShaderType.getSType(shaderInput.variable.type), hasProperty: false, isRequired : false, id : node.id });
-						inputInfoKeys.push(prefixSubGraph+node.id);
+						var inputId = "cameraInput_" + shaderInput.variable.name;
+						var inputInfo = inputsInfo.get(prefixSubGraph+inputId);
+						var ids = [];
+						if (inputInfo != null && inputInfo.ids != null)
+							ids = inputInfo.ids;
+						ids.push(node.id);
+						if (!inputsInfo.exists(prefixSubGraph+inputId)) {
+							inputInfoKeys.push(prefixSubGraph+inputId);
+						}
+						inputsInfo.set(prefixSubGraph+inputId, { name : "*" + shaderInput.variable.name , type: ShaderType.getSType(shaderInput.variable.type), hasProperty: false, isRequired : false, ids : ids });
 					case "ShaderOutput":
 						var shaderOutput = Std.downcast(node.instance, ShaderOutput);
+						var prefix = shaderOutput.variable.kind == Local ? "" : "*";
 
-						outputsInfo.set(prefixSubGraph+node.id, { name : shaderOutput.variable.name , type: ShaderType.getSType(shaderOutput.variable.type), id : node.id });
+						outputsInfo.set(prefixSubGraph+node.id, { name : prefix + shaderOutput.variable.name , type: ShaderType.getSType(shaderOutput.variable.type), id : node.id });
 						outputInfoKeys.push(prefixSubGraph+node.id);
 
 						addOutput(prefixSubGraph+node.id, shaderOutput.variable.type);
@@ -93,14 +121,16 @@ class SubGraph extends ShaderNode {
 			var inputTVar = getInput(inputKey);
 
 			if (inputTVar != null) {
-				var nodeToReplace = subShaderGraph.getNodes().get(inputInfo.id);
-				for (i in 0...nodeToReplace.outputs.length) {
-					var inputNode = nodeToReplace.outputs[i];
-
-					for (inputKey in inputNode.instance.getInputsKey()) {
-						var input = inputNode.instance.getInput(inputKey);
-						if (input.node == nodeToReplace.instance) {
-							inputNode.instance.setInput(inputKey, inputTVar);
+				for (id in inputInfo.ids) {
+					var nodeToReplace = subShaderGraph.getNodes().get(id);
+					for (i in 0...nodeToReplace.outputs.length) {
+						var inputNode = nodeToReplace.outputs[i];
+
+						for (inputKey in inputNode.instance.getInputsKey()) {
+							var input = inputNode.instance.getInput(inputKey);
+							if (input.node == nodeToReplace.instance) {
+								inputNode.instance.setInput(inputKey, inputTVar);
+							}
 						}
 					}
 				}
@@ -224,23 +254,6 @@ class SubGraph extends ShaderNode {
 	#if editor
 	override public function getPropertiesHTML(width : Float) : Array<hide.Element> {
 		var elements = super.getPropertiesHTML(width);
-		var element = new hide.Element('<div style="width: ${width * 0.8}px; height: 25px"></div>');
-		var fileInput = new hide.Element('<input type="text" field="filesubgraph" />').appendTo(element);
-
-		fileInput.on("mousedown", function(e) {
-			e.stopPropagation();
-		});
-
-		var tfile = new hide.comp.FileSelect(["hlshader"], null, fileInput);
-		if (this.pathShaderGraph != null && this.pathShaderGraph.length > 0) tfile.path = this.pathShaderGraph;
-		tfile.onChange = function() {
-			this.pathShaderGraph = tfile.path;
-			loadGraphShader();
-			fileInput.trigger("change");
-		}
-		elements.push(element);
-		elements.push(new hide.Element('<div style="background: #202020; height: 1px; margin-bottom: 5px;"></div>'));
-
 		for (p in parameters) {
 			var element = new hide.Element('<div class="propertySubShader" style="width: 200px;"></div>');
 			element.on("mousedown", function(e) {

+ 4 - 4
hrt/shgraph/nodes/UVScroll.hx

@@ -7,9 +7,9 @@ using hxsl.Ast;
 @group("Specials")
 class UVScroll extends ShaderNode {
 
-	@input("uv") var uv = SType.Vec2;
-	@input("uSpeed", true) var uSpeed = SType.Number;
-	@input("vSpeed", true) var vSpeed = SType.Number;
+	@input("UV") var uv = SType.Vec2;
+	@input("USpeed", true) var uSpeed = SType.Number;
+	@input("VSpeed", true) var vSpeed = SType.Number;
 
 	@output("") var output = SType.Vec2;
 
@@ -27,7 +27,7 @@ class UVScroll extends ShaderNode {
 	}
 
 	override public function build(key : String) : TExpr {
-		
+
 		var globalTime : TVar = @:privateAccess ShaderGlobalInput.globalInputs.filter(i -> i.name.indexOf("time") != -1)[0];
 		var timeExpr : TExpr = { e: TVar(globalTime), p: null, t: globalTime.type };
 

+ 24 - 0
hrt/shgraph/nodes/UvToScreen.hx

@@ -0,0 +1,24 @@
+package hrt.shgraph.nodes;
+
+using hxsl.Ast;
+
+@name("UV To Screen")
+@description("")
+@width(100)
+@group("Math")
+class UvToScreen extends ShaderFunction {
+
+	@input("UV") var uv = SType.Vec2;
+
+	public function new() {
+		super(UvToScreen);
+	}
+
+	override public function computeOutputs() {
+		if (uv != null && !uv.isEmpty())
+			addOutput("output", uv.getType());
+		else
+			removeOutput("output");
+	}
+
+}