diff --git a/Assets/pipeline_resource.json b/Assets/pipeline_resource.json index d4dcfb94..7f446012 100644 --- a/Assets/pipeline_resource.json +++ b/Assets/pipeline_resource.json @@ -56,7 +56,7 @@ }, { "command": "draw_quad", - "params": ["material_resource", "material1", "env_map"] + "params": ["material_resource/material1/env_map"] }, { "command": "bind_target", @@ -64,7 +64,7 @@ }, { "command": "draw_quad", - "params": ["material_resource", "material_deferred", "deferred_pass"] + "params": ["material_resource/material_deferred/deferred_pass"] } ] } diff --git a/blender/armory.py b/blender/armory.py index 83eeb03d..8f23e3c8 100644 --- a/blender/armory.py +++ b/blender/armory.py @@ -2058,7 +2058,7 @@ class ArmoryExporter(bpy.types.Operator, ExportHelper): o.draw_calls_sort = 'front_to_back' else: o.draw_calls_sort = 'none' - o.pipeline = object.pipeline_path + o.pipeline = object.pipeline_path + '/' + object.pipeline_path # Same file name and id if 'Background' in bpy.data.worlds[0].node_tree.nodes: # TODO: parse node tree col = bpy.data.worlds[0].node_tree.nodes['Background'].inputs[0].default_value diff --git a/blender/nodes.py b/blender/nodes.py index 09545ece..d28e1f93 100755 --- a/blender/nodes.py +++ b/blender/nodes.py @@ -1,6 +1,8 @@ import bpy from bpy.types import NodeTree, Node, NodeSocket from bpy.props import * +import os +import sys # Implementation of custom nodes from Python # Derived from the NodeTree base type, similar to Menu, Operator, Panel, etc. @@ -13,8 +15,8 @@ class CGTree(NodeTree): bl_label = 'CG Node Tree' # Icon identifier # NOTE: If no icon is defined, the node tree will not show up in the editor header! - # This can be used to make additional tree types for groups and similar nodes (see below) - # Only one base tree class is needed in the editor for selecting the general category + # This can be used to make additional tree types for groups and similar nodes (see below) + # Only one base tree class is needed in the editor for selecting the general category bl_icon = 'GAME' #def update(self): @@ -47,7 +49,7 @@ class TransformNode(Node, CGTreeNode): # Initialization function, called when a new node is created. # This is the most common place to create the sockets for a node, as shown below. # NOTE: this is not the same as the standard __init__ function in Python, which is - # a purely internal Python method and unknown to the node system! + # a purely internal Python method and unknown to the node system! def init(self, context): self.inputs.new('NodeSocketVector', "Position") self.inputs.new('NodeSocketVector', "Rotation") @@ -215,3 +217,95 @@ def register(): def unregister(): nodeitems_utils.unregister_node_categories("CG_NODES") bpy.utils.unregister_module(__name__) + +# Generating node sources +def buildNodeTrees(): + s = bpy.data.filepath.split(os.path.sep) + s.pop() + fp = os.path.sep.join(s) + os.chdir(fp) + + # Make sure package dir exists + if not os.path.exists('Sources/' + bpy.data.worlds[0].CGProjectPackage.replace(".", "/")): + os.makedirs('Sources/' + bpy.data.worlds[0].CGProjectPackage.replace(".", "/")) + + # Export node scripts + for node_group in bpy.data.node_groups: + if node_group.bl_idname == 'CGTreeType': # Build only cycles game trees + buildNodeTree(node_group) + +def buildNodeTree(node_group): + rn = getRootNode(node_group) + + path = 'Sources/' + bpy.data.worlds[0].CGProjectPackage.replace(".", "/") + "/" + + node_group_name = node_group.name.replace('.', '_') + + with open(path + node_group_name + '.hx', 'w') as f: + f.write('package ' + bpy.data.worlds[0].CGProjectPackage + ';\n\n') + f.write('import cycles.node.*;\n\n') + f.write('class ' + node_group_name + ' extends cycles.trait.NodeExecutor {\n\n') + f.write('\tpublic function new() { super(); requestAdd(add); }\n\n') + f.write('\tfunction add() {\n') + # Make sure root node exists + if rn != None: + name = '_' + rn.name.replace(".", "_").replace("@", "") + buildNode(node_group, rn, f, []) + f.write('\n\t\tstart(' + name + ');\n') + f.write('\t}\n') + f.write('}\n') + +def buildNode(node_group, node, f, created_nodes): + # Get node name + name = '_' + node.name.replace(".", "_").replace("@", "") + + # Check if node already exists + for n in created_nodes: + if n == name: + return name + + # Create node + type = node.name.split(".")[0].replace("@", "") + "Node" + f.write('\t\tvar ' + name + ' = new ' + type + '();\n') + created_nodes.append(name) + + # Variables + if type == "TransformNode": + f.write('\t\t' + name + '.transform = node.transform;\n') + + # Create inputs + for inp in node.inputs: + # Is linked - find node + inpname = '' + if inp.is_linked: + n = findNodeByLink(node_group, node, inp) + inpname = buildNode(node_group, n, f, created_nodes) + # Not linked - create node with default values + else: + inpname = buildDefaultNode(inp) + + # Add input + f.write('\t\t' + name + '.inputs.push(' + inpname + ');\n') + + return name + +def findNodeByLink(node_group, to_node, inp): + for link in node_group.links: + if link.to_node == to_node and link.to_socket == inp: + return link.from_node + +def getRootNode(node_group): + for n in node_group.nodes: + if n.outputs[0].is_linked == False: + return n + +def buildDefaultNode(inp): + inpname = '' + if inp.type == "VECTOR": + inpname = 'VectorNode.create(' + str(inp.default_value[0]) + ', ' + str(inp.default_value[1]) + ", " + str(inp.default_value[2]) + ')' + elif inp.type == "VALUE": + inpname = 'FloatNode.create(' + str(inp.default_value) + ')' + elif inp.type == 'BOOLEAN': + inpname = 'BoolNode.create(' + str(inp.default_value).lower() + ')' + + return inpname diff --git a/blender/pipeline_nodes.py b/blender/pipeline_nodes.py new file mode 100755 index 00000000..fb34e72a --- /dev/null +++ b/blender/pipeline_nodes.py @@ -0,0 +1,295 @@ +import bpy +from bpy.types import NodeTree, Node, NodeSocket +from bpy.props import * +import os +import sys +import json + +class CGPipelineTree(NodeTree): + '''Pipeline nodes''' + bl_idname = 'CGPipelineTreeType' + bl_label = 'CG Pipeline Node Tree' + bl_icon = 'GAME' + +class CGPipelineTreeNode: + @classmethod + def poll(cls, ntree): + return ntree.bl_idname == 'CGPipelineTreeType' + +class DrawGeometryNode(Node, CGPipelineTreeNode): + '''A custom node''' + bl_idname = 'DrawGeometryNodeType' + bl_label = 'Draw Geometry' + bl_icon = 'SOUND' + + def init(self, context): + self.inputs.new('NodeSocketShader', "Stage") + self.inputs.new('NodeSocketString', "Context") + + self.outputs.new('NodeSocketShader', "Stage") + + def copy(self, node): + print("Copying from node ", node) + + def free(self): + print("Removing node ", self, ", Goodbye!") + +class ClearTargetNode(Node, CGPipelineTreeNode): + '''A custom node''' + bl_idname = 'ClearTargetNodeType' + bl_label = 'Clear Target' + bl_icon = 'SOUND' + + def init(self, context): + self.inputs.new('NodeSocketShader', "Stage") + self.inputs.new('NodeSocketBool', "Color") + self.inputs.new('NodeSocketBool', "Depth") + + self.outputs.new('NodeSocketShader', "Stage") + + def copy(self, node): + print("Copying from node ", node) + + def free(self): + print("Removing node ", self, ", Goodbye!") + +class SetTargetNode(Node, CGPipelineTreeNode): + '''A custom node''' + bl_idname = 'SetTargetNodeType' + bl_label = 'Set Target' + bl_icon = 'SOUND' + + def init(self, context): + self.inputs.new('NodeSocketShader', "Stage") + self.inputs.new('NodeSocketShader', "Target") + + self.outputs.new('NodeSocketShader', "Stage") + + def copy(self, node): + print("Copying from node ", node) + + def free(self): + print("Removing node ", self, ", Goodbye!") + +class TargetNode(Node, CGPipelineTreeNode): + '''A custom node''' + bl_idname = 'TargetNodeType' + bl_label = 'Target' + bl_icon = 'SOUND' + + def init(self, context): + self.inputs.new('NodeSocketString', "ID") + self.inputs.new('NodeSocketInt', "Width") + self.inputs.new('NodeSocketInt', "Height") + self.inputs.new('NodeSocketInt', "Color Buffers") + self.inputs.new('NodeSocketBool', "Depth") + self.inputs.new('NodeSocketString', "Format") + + self.outputs.new('NodeSocketShader', "Target") + + def copy(self, node): + print("Copying from node ", node) + + def free(self): + print("Removing node ", self, ", Goodbye!") + +class FramebufferNode(Node, CGPipelineTreeNode): + '''A custom node''' + bl_idname = 'FramebufferNodeType' + bl_label = 'Framebuffer' + bl_icon = 'SOUND' + + def init(self, context): + self.outputs.new('NodeSocketShader', "Target") + + def copy(self, node): + print("Copying from node ", node) + + def free(self): + print("Removing node ", self, ", Goodbye!") + +class BindTargetNode(Node, CGPipelineTreeNode): + '''A custom node''' + bl_idname = 'BindTargetNodeType' + bl_label = 'Bind Target' + bl_icon = 'SOUND' + + def init(self, context): + self.inputs.new('NodeSocketShader', "Stage") + self.inputs.new('NodeSocketShader', "Target") + self.inputs.new('NodeSocketString', "Constant") + + self.outputs.new('NodeSocketShader', "Stage") + + def copy(self, node): + print("Copying from node ", node) + + def free(self): + print("Removing node ", self, ", Goodbye!") + +class DrawQuadNode(Node, CGPipelineTreeNode): + '''A custom node''' + bl_idname = 'DrawQuadNodeType' + bl_label = 'Draw Quad' + bl_icon = 'SOUND' + + def init(self, context): + self.inputs.new('NodeSocketShader', "Stage") + self.inputs.new('NodeSocketString', "Material Context") + + self.outputs.new('NodeSocketShader', "Stage") + + def copy(self, node): + print("Copying from node ", node) + + def free(self): + print("Removing node ", self, ", Goodbye!") + +### Node Categories ### +# Node categories are a python system for automatically +# extending the Add menu, toolbar panels and search operator. +# For more examples see release/scripts/startup/nodeitems_builtins.py + +import nodeitems_utils +from nodeitems_utils import NodeCategory, NodeItem + +class MyPipelineNodeCategory(NodeCategory): + @classmethod + def poll(cls, context): + return context.space_data.tree_type == 'CGPipelineTreeType' + +node_categories = [ + MyPipelineNodeCategory("PIPELINENODES", "Pipeline Nodes", items=[ + NodeItem("DrawGeometryNodeType"), + NodeItem("ClearTargetNodeType"), + NodeItem("SetTargetNodeType"), + NodeItem("BindTargetNodeType"), + NodeItem("DrawQuadNodeType"), + NodeItem("TargetNodeType"), + NodeItem("FramebufferNodeType"), + ]), + ] + +def register(): + bpy.utils.register_module(__name__) + try: + nodeitems_utils.register_node_categories("CG_PIELINE_PNODES", node_categories) + except: + pass + +def unregister(): + nodeitems_utils.unregister_node_categories("CG_PIELINE_PNODES") + bpy.utils.unregister_module(__name__) + + +# Generating pipeline resources +class Object: + def to_JSON(self): + # return json.dumps(self, default=lambda o: o.__dict__, separators=(',',':')) + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + +def buildNodeTrees(): + s = bpy.data.filepath.split(os.path.sep) + s.pop() + fp = os.path.sep.join(s) + os.chdir(fp) + + # Make sure Assets dir exists + if not os.path.exists('Assets/generated/pipelines'): + os.makedirs('Assets/generated/pipelines') + + # Export pipelines + for node_group in bpy.data.node_groups: + if node_group.bl_idname == 'CGPipelineTreeType': # Build only render pipeline trees + buildNodeTree(node_group) + +def buildNodeTree(node_group): + output = Object() + res = Object() + output.pipeline_resources = [res] + + path = 'Assets/generated/pipelines/' + node_group_name = node_group.name.replace('.', '_') + + res.id = node_group_name + res.render_targets = get_render_targets(node_group) + res.stages = [] + + rn = getRootNode(node_group) + buildNode(res, rn, node_group) + + with open(path + node_group_name + '.json', 'w') as f: + f.write(output.to_JSON()) + +def buildNode(res, node, node_group): + stage = Object() + stage.params = [] + + if node.bl_idname == 'SetTargetNodeType': + stage.command = 'set_target' + targetNode = findNodeByLink(node_group, node, node.inputs[1]) + if targetNode.bl_idname == 'TargetNodeType': + targetId = targetNode.inputs[0].default_value + else: # Framebuffer + targetId = '' + stage.params.append(targetId) + + elif node.bl_idname == 'ClearTargetNodeType': + stage.command = 'clear_target' + if node.inputs[1].default_value == True: + stage.params.append('color') + if node.inputs[2].default_value == True: + stage.params.append('depth') + + elif node.bl_idname == 'DrawGeometryNodeType': + stage.command = 'draw_geometry' + stage.params.append(node.inputs[1].default_value) # Context + + elif node.bl_idname == 'BindTargetNodeType': + stage.command = 'bind_target' + targetNode = findNodeByLink(node_group, node, node.inputs[1]) + if targetNode.bl_idname == 'TargetNodeType': + targetId = targetNode.inputs[0].default_value + stage.params.append(targetId) + + elif node.bl_idname == 'DrawQuadNodeType': + stage.command = 'draw_quad' + stage.params.append(node.inputs[1].default_value) # Material context + + res.stages.append(stage) + + # Build next stage + if node.outputs[0].is_linked: + stageNode = findNodeByFromLink(node_group, node, node.outputs[0]) + buildNode(res, stageNode, node_group) + +def findNodeByLink(node_group, to_node, inp): + for link in node_group.links: + if link.to_node == to_node and link.to_socket == inp: + return link.from_node + +def findNodeByFromLink(node_group, from_node, outp): + for link in node_group.links: + if link.from_node == from_node and link.from_socket == outp: + return link.to_node + +def getRootNode(node_group): + # First node with empty stage input + for n in node_group.nodes: + if len(n.inputs) > 0 and n.inputs[0].is_linked == False and n.inputs[0].name == 'Stage': + return n + +def get_render_targets(node_group): + render_targets = [] + for n in node_group.nodes: + if n.bl_idname == 'TargetNodeType': + target = Object() + target.id = n.inputs[0].default_value + target.width = n.inputs[1].default_value + target.height = n.inputs[2].default_value + target.color_buffers = n.inputs[3].default_value + target.depth = n.inputs[4].default_value + target.format = n.inputs[5].default_value + render_targets.append(target) + return render_targets + diff --git a/blender/project.py b/blender/project.py index 4ecc1cd2..34ef1ce5 100755 --- a/blender/project.py +++ b/blender/project.py @@ -9,6 +9,8 @@ import subprocess import atexit import webbrowser import write_data +import nodes +import pipeline_nodes from armory import ArmoryExporter def defaultSettings(): @@ -159,7 +161,8 @@ def buildProject(self, build_type=0): #area.type = old_type # Auto-build nodes # TODO: only if needed - buildNodeTrees() + nodes.buildNodeTrees() + pipeline_nodes.buildNodeTrees() # Set dir s = bpy.data.filepath.split(os.path.sep) @@ -272,99 +275,6 @@ class OBJECT_OT_CLEANButton(bpy.types.Operator): cleanProject(self) return{'FINISHED'} - - -def buildNodeTrees(): - s = bpy.data.filepath.split(os.path.sep) - s.pop() - fp = os.path.sep.join(s) - os.chdir(fp) - - # Make sure package dir exists - if not os.path.exists('Sources/' + bpy.data.worlds[0].CGProjectPackage.replace(".", "/")): - os.makedirs('Sources/' + bpy.data.worlds[0].CGProjectPackage.replace(".", "/")) - - # Export node scripts - for node_group in bpy.data.node_groups: - if node_group.bl_idname == 'CGTreeType': # Build only cycles game trees - buildNodeTree(node_group) - -def buildNodeTree(node_group): - rn = getRootNode(node_group) - - path = 'Sources/' + bpy.data.worlds[0].CGProjectPackage.replace(".", "/") + "/" - - node_group_name = node_group.name.replace('.', '_') - - with open(path + node_group_name + '.hx', 'w') as f: - f.write('package ' + bpy.data.worlds[0].CGProjectPackage + ';\n\n') - f.write('import cycles.node.*;\n\n') - f.write('class ' + node_group_name + ' extends cycles.trait.NodeExecutor {\n\n') - f.write('\tpublic function new() { super(); requestAdd(add); }\n\n') - f.write('\tfunction add() {\n') - # Make sure root node exists - if rn != None: - name = '_' + rn.name.replace(".", "_").replace("@", "") - buildNode(node_group, rn, f, []) - f.write('\n\t\tstart(' + name + ');\n') - f.write('\t}\n') - f.write('}\n') - -def buildNode(node_group, node, f, created_nodes): - # Get node name - name = '_' + node.name.replace(".", "_").replace("@", "") - - # Check if node already exists - for n in created_nodes: - if n == name: - return name - - # Create node - type = node.name.split(".")[0].replace("@", "") + "Node" - f.write('\t\tvar ' + name + ' = new ' + type + '();\n') - created_nodes.append(name) - - # Variables - if type == "TransformNode": - f.write('\t\t' + name + '.transform = node.transform;\n') - - # Create inputs - for inp in node.inputs: - # Is linked - find node - inpname = '' - if inp.is_linked: - n = findNodeByLink(node_group, node, inp) - inpname = buildNode(node_group, n, f, created_nodes) - # Not linked - create node with default values - else: - inpname = buildDefaultNode(inp) - - # Add input - f.write('\t\t' + name + '.inputs.push(' + inpname + ');\n') - - return name - -def findNodeByLink(node_group, to_node, inp): - for link in node_group.links: - if link.to_node == to_node and link.to_socket == inp: - return link.from_node - -def getRootNode(node_group): - for n in node_group.nodes: - if n.outputs[0].is_linked == False: - return n - -def buildDefaultNode(inp): - inpname = '' - if inp.type == "VECTOR": - inpname = 'VectorNode.create(' + str(inp.default_value[0]) + ', ' + str(inp.default_value[1]) + ", " + str(inp.default_value[2]) + ')' - elif inp.type == "VALUE": - inpname = 'FloatNode.create(' + str(inp.default_value) + ')' - elif inp.type == 'BOOLEAN': - inpname = 'BoolNode.create(' + str(inp.default_value).lower() + ')' - - return inpname - # Registration def register(): bpy.utils.register_module(__name__) diff --git a/blender/props.py b/blender/props.py index 02526e1e..2dbff87b 100755 --- a/blender/props.py +++ b/blender/props.py @@ -61,7 +61,7 @@ class DataPropsPanel(bpy.types.Panel): if obj.type == 'CAMERA': layout.prop(obj.data, 'frustum_culling') layout.prop(obj.data, 'sort_front_to_back') - layout.prop(obj.data, 'pipeline_path') + layout.prop_search(obj.data, "pipeline_path", bpy.data, "node_groups") layout.prop(obj.data, 'pipeline_pass') elif obj.type == 'MESH': layout.prop(obj.data, 'static_usage') diff --git a/blender/start.py b/blender/start.py index 354ae649..237df62d 100755 --- a/blender/start.py +++ b/blender/start.py @@ -1,5 +1,6 @@ import project import nodes +import pipeline_nodes import armory import traits_animation import traits @@ -8,6 +9,7 @@ import props def register(): project.register() nodes.register() + pipeline_nodes.register() armory.register() traits_animation.register() traits.register() @@ -16,6 +18,7 @@ def register(): def unregister(): project.unregister() nodes.unregister() + pipeline_nodes.unregister() armory.unregister() traits_animation.unregister() traits.unregister()