diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 93aac0bb..83fe1e8c --- a/README.md +++ b/README.md @@ -1 +1,2 @@ -# zblend +zblend +============== diff --git a/LICENSE b/blender/LICENSE similarity index 100% rename from LICENSE rename to blender/LICENSE diff --git a/blender/__init__.py b/blender/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/blender/assets.py b/blender/assets.py new file mode 100755 index 00000000..a7da7b69 --- /dev/null +++ b/blender/assets.py @@ -0,0 +1,194 @@ +import shutil +import bpy +import os +import json +from bpy.types import Menu, Panel, UIList +from bpy.props import * + + + +class ListItem(bpy.types.PropertyGroup): + # Group of properties representing an item in the list + name = bpy.props.StringProperty( + name="Name", + description="A name for this item", + default="Untitled") + + enabled_prop = bpy.props.BoolProperty( + name="", + description="A name for this item", + default=True) + + type_prop = bpy.props.EnumProperty( + items = [('Image', 'Image', 'Image'), + ('Blob', 'Blob', 'Blob'), + ('Sound', 'Sound', 'Sound'), + ('Music', 'Music', 'Music'), + ('Font', 'Font', 'Font')], + name = "Type") + + size_prop = bpy.props.IntProperty( + name="Size", + description="A name for this item", + default=0) + + +bpy.utils.register_class(ListItem) + + + +class MY_UL_List(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + # We could write some code to decide which icon to use here... + custom_icon = 'OBJECT_DATAMODE' + + # Make sure your code supports all 3 layout types + if self.layout_type in {'DEFAULT', 'COMPACT'}: + layout.prop(item, "enabled_prop") + #layout.label(item.name, icon = custom_icon) + layout.prop(item, "name", text="", emboss=False, icon=custom_icon) + + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label("", icon = custom_icon) +bpy.utils.register_class(MY_UL_List) + + + +def initWorldProperties(): + bpy.types.World.my_list = bpy.props.CollectionProperty(type = ListItem) + bpy.types.World.list_index = bpy.props.IntProperty(name = "Index for my_list", default = 0) +initWorldProperties() + + + +class LIST_OT_NewItem(bpy.types.Operator): + # Add a new item to the list + bl_idname = "my_list.new_item" + bl_label = "Add a new item" + + def execute(self, context): + bpy.data.worlds[0].my_list.add() + return{'FINISHED'} + + +class LIST_OT_DeleteItem(bpy.types.Operator): + # Delete the selected item from the list + bl_idname = "my_list.delete_item" + bl_label = "Deletes an item" + + @classmethod + def poll(self, context): + """ Enable if there's something in the list """ + return len(bpy.data.worlds[0].my_list) > 0 + + def execute(self, context): + list = bpy.data.worlds[0].my_list + index = bpy.data.worlds[0].list_index + + list.remove(index) + + if index > 0: + index = index - 1 + + bpy.data.worlds[0].list_index = index + return{'FINISHED'} + + +class LIST_OT_MoveItem(bpy.types.Operator): + # Move an item in the list + bl_idname = "my_list.move_item" + bl_label = "Move an item in the list" + direction = bpy.props.EnumProperty( + items=( + ('UP', 'Up', ""), + ('DOWN', 'Down', ""),)) + + @classmethod + def poll(self, context): + """ Enable if there's something in the list. """ + return len(bpy.data.worlds[0].my_list) > 0 + + + def move_index(self): + # Move index of an item render queue while clamping it + index = bpy.data.worlds[0].list_index + list_length = len(bpy.data.worlds[0].my_list) - 1 # (index starts at 0) + new_index = 0 + + if self.direction == 'UP': + new_index = index - 1 + elif self.direction == 'DOWN': + new_index = index + 1 + + new_index = max(0, min(new_index, list_length)) + index = new_index + + + def execute(self, context): + list = bpy.data.worlds[0].my_list + index = bpy.data.worlds[0].list_index + + if self.direction == 'DOWN': + neighbor = index + 1 + #queue.move(index,neighbor) + self.move_index() + + elif self.direction == 'UP': + neighbor = index - 1 + #queue.move(neighbor, index) + self.move_index() + else: + return{'CANCELLED'} + return{'FINISHED'} + + + + + + +# +# Menu in tools region +# +class ToolsAssetsPanel(bpy.types.Panel): + bl_label = "zblend_assets" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + + def draw(self, context): + layout = self.layout + scene = bpy.data.worlds[0] + + rows = 2 + if len(scene.my_list) > 1: + rows = 4 + + row = layout.row() + row.template_list("MY_UL_List", "The_List", scene, "my_list", scene, "list_index", rows=rows) + + col = row.column(align=True) + col.operator("my_list.new_item", icon='ZOOMIN', text="") + col.operator("my_list.delete_item", icon='ZOOMOUT', text="")#.all = False + + if len(scene.my_list) > 1: + col.separator() + col.operator("my_list.move_item", icon='TRIA_UP', text="").direction = 'UP' + col.operator("my_list.move_item", icon='TRIA_DOWN', text="").direction = 'DOWN' + + if scene.list_index >= 0 and len(scene.my_list) > 0: + item = scene.my_list[scene.list_index] + #row = layout.row() + #row.prop(item, "name") + row = layout.row() + row.prop(item, "type_prop") + + if item.type_prop == 'Font': + row = layout.row() + row.prop(item, "size_prop") + + +# +# Registration +# +bpy.utils.register_module(__name__) diff --git a/blender/build.py b/blender/build.py new file mode 100755 index 00000000..0cc8efe9 --- /dev/null +++ b/blender/build.py @@ -0,0 +1,53 @@ +import os +import webbrowser +import sys +import bpy +import shutil + +def runProject(fp, target, name): + os.chdir(fp) + # OSX + if (target == '0'): + bashCommand = "xcodebuild -project 'build/osx-build/" + name + ".xcodeproj' && open 'build/osx-build/build/Release/" + name + ".app/Contents/MacOS/" + name + "'" + os.system(bashCommand) + # HTML5 + elif (target == '3'): + webbrowser.open("http://127.0.0.1:8000/build/html5",new=2) + +def openProject(fp, target, name): + os.chdir(fp) + # OSX + if (target == '0'): + bashCommand = "open -a Xcode.app 'build/osx-build/" + name + ".xcodeproj'" + os.system(bashCommand) + +def build(): + argv = sys.argv + argv = argv[argv.index("--") + 1:] # Get all args after "--" + + s = bpy.data.filepath.split(os.path.sep) + name = s.pop() + name = name.split(".") + name = name[0] + fp = os.path.sep.join(s) + os.chdir(fp) + + bashCommand = argv[0] + build_type = argv[1] + target = argv[2] + + os.system(bashCommand) + + # Copy ammo.js if necessary + if (target == '3'): + if not os.path.isfile('build/html5/ammo.js'): + shutil.copy('Libraries/haxebullet/js/ammo/ammo.js', 'build/html5') + + if build_type == '2': + openProject(fp, target, name) + elif build_type == '1': + runProject(fp, target, name) + + print("Build done!") + +build() diff --git a/blender/fetch.py b/blender/fetch.py new file mode 100755 index 00000000..175209b6 --- /dev/null +++ b/blender/fetch.py @@ -0,0 +1,81 @@ +import os +import bpy + +def fetch(): + s = bpy.data.filepath.split(os.path.sep) + name = s.pop() + name = name.split(".") + name = name[0] + fp = os.path.sep.join(s) + + # Update scripts + os.chdir(fp + "/Libraries/zblend/blender") + os.system("git pull") + + # Clone kha + #self.report({'INFO'}, "Fetching Kha...") + os.chdir(fp) + if not os.path.exists('Kha'): + os.system("git clone --depth=1 --recursive https://github.com/ktxsoftware/Kha") + + os.chdir(fp + "/Kha") + os.system("git pull && git submodule foreach --recursive git checkout master && git submodule foreach --recursive git pull origin master") + + # Create sources directories + os.chdir(fp) + if not os.path.exists('Sources/Shaders'): + os.makedirs('Sources/Shaders') + if not os.path.exists('Libraries/zblend/Sources'): + os.makedirs('Libraries/zblend/Sources') + if not os.path.exists('Libraries/dependencies'): + os.makedirs('Libraries/dependencies') + if not os.path.exists('Assets'): + os.makedirs('Assets') + + # Clone dependencies + #self.report({'INFO'}, "Fetching dependencies...") + os.chdir(fp + "/Libraries/dependencies") + if not os.path.exists('Sources'): + os.system("git clone --depth=1 https://github.com/luboslenco/zblend_dependencies Sources") + + os.chdir(fp + "/Libraries/dependencies/Sources") + os.system("git pull") + + # Clone shaders + #self.report({'INFO'}, "Fetching shaders...") + os.chdir(fp + "/Libraries/zblend/Sources") + if not os.path.exists('Shaders'): + os.system("git clone --depth=1 https://github.com/luboslenco/zblend_shaders Shaders") + + os.chdir(fp + "/Libraries/zblend/Sources/Shaders") + os.system("git pull") + + # Clone oimo + os.chdir(fp + "/Libraries") + if not os.path.exists('oimo'): + os.system("git clone --depth=1 https://github.com/luboslenco/oimo oimo") + + os.chdir(fp + "/Libraries/oimo") + os.system("git pull") + + # Clone haxebullet + #self.report({'INFO'}, "Fetching physics...") + os.chdir(fp + "/Libraries") + if not os.path.exists('haxebullet'): + os.system("git clone --depth=1 https://github.com/luboslenco/haxebullet haxebullet") + + os.chdir(fp + "/Libraries/haxebullet") + os.system("git pull") + + # Clone zblend + #self.report({'INFO'}, "Fetching zblend...") + os.chdir(fp + "/Libraries/zblend/Sources") + if not os.path.exists('zblend'): + os.system("git clone --depth=1 https://github.com/luboslenco/zblend") + + os.chdir(fp + "/Libraries/zblend/Sources/zblend") + os.system("git pull") + + print("Fetch complete!") + +fetch() diff --git a/blender/libraries.py b/blender/libraries.py new file mode 100755 index 00000000..89d4f84b --- /dev/null +++ b/blender/libraries.py @@ -0,0 +1,173 @@ +import shutil +import bpy +import os +import json +from bpy.types import Menu, Panel, UIList +from bpy.props import * + +class LibListItem(bpy.types.PropertyGroup): + # Group of properties representing an item in the list + name = bpy.props.StringProperty( + name="Name", + description="A name for this item", + default="Untitled") + + enabled_prop = bpy.props.BoolProperty( + name="", + description="A name for this item", + default=True) +bpy.utils.register_class(LibListItem) + + + +class MY_UL_LibList(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + # We could write some code to decide which icon to use here... + custom_icon = 'OBJECT_DATAMODE' + + # Make sure your code supports all 3 layout types + if self.layout_type in {'DEFAULT', 'COMPACT'}: + layout.prop(item, "enabled_prop") + #layout.label(item.name, icon = custom_icon) + layout.prop(item, "name", text="", emboss=False, icon=custom_icon) + + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label("", icon = custom_icon) +bpy.utils.register_class(MY_UL_LibList) + + + +def initWorldProperties(): + bpy.types.World.my_liblist = bpy.props.CollectionProperty(type = LibListItem) + bpy.types.World.liblist_index = bpy.props.IntProperty(name = "Index for my_liblist", default = 0) +initWorldProperties() + + + +class LIBLIST_OT_NewItem(bpy.types.Operator): + # Add a new item to the list + bl_idname = "my_liblist.new_item" + bl_label = "Add a new item" + + def execute(self, context): + bpy.data.worlds[0].my_liblist.add() + return{'FINISHED'} + + +class LIBLIST_OT_DeleteItem(bpy.types.Operator): + # Delete the selected item from the list + bl_idname = "my_liblist.delete_item" + bl_label = "Deletes an item" + + @classmethod + def poll(self, context): + """ Enable if there's something in the list """ + return len(bpy.data.worlds[0].my_liblist) > 0 + + def execute(self, context): + list = bpy.data.worlds[0].my_liblist + index = bpy.data.worlds[0].liblist_index + + list.remove(index) + + if index > 0: + index = index - 1 + + bpy.data.worlds[0].liblist_index = index + return{'FINISHED'} + + +class LIST_OT_MoveItem(bpy.types.Operator): + # Move an item in the list + bl_idname = "my_liblist.move_item" + bl_label = "Move an item in the list" + direction = bpy.props.EnumProperty( + items=( + ('UP', 'Up', ""), + ('DOWN', 'Down', ""),)) + + @classmethod + def poll(self, context): + """ Enable if there's something in the list. """ + return len(bpy.data.worlds[0].my_liblist) > 0 + + + def move_index(self): + # Move index of an item render queue while clamping it + index = bpy.data.worlds[0].liblist_index + list_length = len(bpy.data.worlds[0].my_liblist) - 1 # (index starts at 0) + new_index = 0 + + if self.direction == 'UP': + new_index = index - 1 + elif self.direction == 'DOWN': + new_index = index + 1 + + new_index = max(0, min(new_index, list_length)) + index = new_index + + + def execute(self, context): + list = bpy.data.worlds[0].my_liblist + index = bpy.data.worlds[0].liblist_index + + if self.direction == 'DOWN': + neighbor = index + 1 + #queue.move(index,neighbor) + self.move_index() + + elif self.direction == 'UP': + neighbor = index - 1 + #queue.move(neighbor, index) + self.move_index() + else: + return{'CANCELLED'} + return{'FINISHED'} + + + + + + +# +# Menu in tools region +# +class ToolsLibrariesPanel(bpy.types.Panel): + bl_label = "zblend_libraries" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + + def draw(self, context): + layout = self.layout + scene = bpy.data.worlds[0] + + rows = 2 + if len(scene.my_liblist) > 1: + rows = 4 + + row = layout.row() + row.template_list("MY_UL_LibList", "The_List", scene, "my_liblist", scene, "liblist_index", rows=rows) + + col = row.column(align=True) + col.operator("my_liblist.new_item", icon='ZOOMIN', text="") + col.operator("my_liblist.delete_item", icon='ZOOMOUT', text="")#.all = False + + if len(scene.my_liblist) > 1: + col.separator() + col.operator("my_liblist.move_item", icon='TRIA_UP', text="").direction = 'UP' + col.operator("my_liblist.move_item", icon='TRIA_DOWN', text="").direction = 'DOWN' + + #if scene.liblist_index >= 0 and len(scene.my_liblist) > 0: + #item = scene.my_liblist[scene.liblist_index] + #row = layout.row() + #row.prop(item, "name") + #row = layout.row() + #row.prop(item, "type_prop") + + +# +# Registration +# +bpy.utils.register_module(__name__) diff --git a/blender/nodes.py b/blender/nodes.py new file mode 100755 index 00000000..ebf33550 --- /dev/null +++ b/blender/nodes.py @@ -0,0 +1,226 @@ +import bpy +from bpy.types import NodeTree, Node, NodeSocket +from bpy.props import * +# Implementation of custom nodes from Python + +# Derived from the NodeTree base type, similar to Menu, Operator, Panel, etc. +class MyCustomTree(NodeTree): + # Description string + '''ZBlend logic nodes''' + # Optional identifier string. If not explicitly defined, the python class name is used. + bl_idname = 'CustomTreeType' + # Label for nice name display + bl_label = 'ZBlend 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 + bl_icon = 'GAME' + + #def update(self): + #print("runing tree") + +# Mix-in class for all custom nodes in this tree type. +# Defines a poll function to enable instantiation. +class MyCustomTreeNode: + @classmethod + def poll(cls, ntree): + return ntree.bl_idname == 'CustomTreeType' + + +# Derived from the Node base type. +class TransformNode(Node, MyCustomTreeNode): + # Description string + '''A custom node''' + # Optional identifier string. If not explicitly defined, the python class name is used. + bl_idname = 'TransformNodeType' + # Label for nice name display + bl_label = '@Transform' + # Icon identifier + bl_icon = 'SOUND' + + # These work just like custom properties in ID data blocks + # Extensive information can be found under + # http://wiki.blender.org/index.php/Doc:2.6/Manual/Extensions/Python/Properties + #objname = bpy.props.StringProperty() + + # 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! + def init(self, context): + self.inputs.new('NodeSocketVector', "Position") + self.inputs.new('NodeSocketVector', "Rotation") + self.inputs.new('NodeSocketVector', "Scale") + + self.inputs["Scale"].default_value = [1.0, 1.0, 1.0] + + self.outputs.new('NodeSocketString', "Transform") + + # Copy function to initialize a copied node from an existing one. + def copy(self, node): + print("Copying from node ", node) + + # Free function to clean up on removal. + def free(self): + print("Removing node ", self, ", Goodbye!") + + # Additional buttons displayed on the node. + #def draw_buttons(self, context, layout): + #layout.prop_search(self, "objname", context.scene, "objects", text = "") + +# Derived from the Node base type. +class TimeNode(Node, MyCustomTreeNode): + + # Description string + '''Time node''' + # Optional identifier string. If not explicitly defined, the python class name is used. + bl_idname = 'TimeNodeType' + # Label for nice name display + bl_label = 'Time' + # Icon identifier + bl_icon = 'TIME' + + def init(self, context): + self.inputs.new('NodeSocketFloat', "Start") + self.inputs.new('NodeSocketFloat', "Stop") + self.inputs.new('NodeSocketFloat', "Scale") + self.inputs.new('NodeSocketBool', "Enabled") + self.inputs.new('NodeSocketBool', "Loop") + self.inputs.new('NodeSocketBool', "Reflect") + + self.inputs["Stop"].default_value = -1 + self.inputs["Scale"].default_value = 1 + self.inputs["Enabled"].default_value = True + + self.outputs.new('NodeSocketFloat', "Time") + + #def draw_buttons(self, context, layout): + #layout.prop(self, "startTime") + #layout.prop(self, "stopTime") + + def free(self): + print("Removing node ", self, ", Goodbye!") + +class VectorNode(Node, MyCustomTreeNode): + bl_idname = 'VectorNodeType' + # Label for nice name display + bl_label = 'Vector' + # Icon identifier + bl_icon = 'CURVE_PATH' + + def init(self, context): + self.inputs.new('NodeSocketFloat', "X") + self.inputs.new('NodeSocketFloat', "Y") + self.inputs.new('NodeSocketFloat', "Z") + + self.outputs.new('NodeSocketVector', "Vector") + + def draw_buttons(self, context, layout): + pass + + def free(self): + print("Removing node ", self, ", Goodbye!") + + def update(self): + print("Updating node: ", self.name) + render() + + +class ScaleValueNode(Node, MyCustomTreeNode): + bl_idname = 'ScaleValueNodeType' + # Label for nice name display + bl_label = 'ScaleValue' + # Icon identifier + bl_icon = 'CURVE_PATH' + + def init(self, context): + self.inputs.new('NodeSocketFloat', "Factor") + self.inputs.new('NodeSocketFloat', "Value") + + self.inputs["Factor"].default_value = 1.0 + + self.outputs.new('NodeSocketFloat', "Value") + + def draw_buttons(self, context, layout): + pass + + def free(self): + print("Removing node ", self, ", Goodbye!") + + def update(self): + print("Updating node: ", self.name) + render() + + +class SineNode(Node, MyCustomTreeNode): + bl_idname = 'SineNodeType' + # Label for nice name display + bl_label = 'Sine' + # Icon identifier + bl_icon = 'CURVE_PATH' + + def init(self, context): + self.inputs.new('NodeSocketFloat', "Value") + + self.outputs.new('NodeSocketFloat', "Value") + + def draw_buttons(self, context, layout): + pass + + def free(self): + print("Removing node ", self, ", Goodbye!") + + def update(self): + print("Updating node: ", self.name) + render() + + +### 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 + + +# our own base class with an appropriate poll function, +# so the categories only show up in our own tree type +class MyNodeCategory(NodeCategory): + @classmethod + def poll(cls, context): + return context.space_data.tree_type == 'CustomTreeType' + +# all categories in a list +node_categories = [ + # identifier, label, items list + MyNodeCategory("TRANSFORMNODES", "Transform Nodes", items=[ + # our basic node + NodeItem("TransformNodeType"), + NodeItem("TimeNodeType"), + NodeItem("VectorNodeType"), + NodeItem("ScaleValueNodeType"), + NodeItem("SineNodeType"), + ]), + ] + + +classes = [MyCustomTree, TransformNode, TimeNode, VectorNode, ScaleValueNode, SineNode] + +def register(): + for c in classes: + bpy.utils.register_class(c) + try: + # nodeitems_utils.unregister_node_categories("CUSTOM_NODES") + nodeitems_utils.register_node_categories("CUSTOM_NODES", node_categories) + except: + pass + +def unregister(): + for c in classes: + bpy.utils.unregister_class(c) + nodeitems_utils.unregister_node_categories("CUSTOM_NODES") + +if __name__ == "__main__": + register() diff --git a/blender/project.py b/blender/project.py new file mode 100755 index 00000000..d2338845 --- /dev/null +++ b/blender/project.py @@ -0,0 +1,780 @@ +import shutil +import bpy +import os +import platform +import json +from bpy.props import * +import subprocess +import atexit +import webbrowser + +def defaultSettings(): + wrd = bpy.data.worlds[0] + wrd['TargetZblendVersion'] = "0.1.0" + wrd['TargetEnum'] = 3 + wrd['TargetRenderer'] = 0 + wrd['TargetProjectName'] = "my_project" + wrd['TargetProjectPackage'] = "my_project" + wrd['TargetProjectWidth'] = 1136 + wrd['TargetProjectHeight'] = 640 + wrd['TargetProjectScale'] = 1.0 + wrd['TargetProjectOrient'] = 0 + wrd['TargetScene'] = bpy.data.scenes[0].name + wrd['TargetGravity'] = bpy.data.scenes[0].name + wrd['TargetClear'] = bpy.data.worlds[0].name + wrd['TargetFog'] = (False) + wrd['TargetFogColor'] = (0.5, 0.5, 0.7, 1.0) + wrd['TargetFogDensity'] = (0.04) + wrd['TargetShadowMapping'] = (False) + wrd['TargetShadowMapSize'] = 1024 + wrd['TargetShading'] = 0 + wrd['TargetShader'] = 0 + wrd['TargetAA'] = 1 + wrd['TargetPhysics'] = 1 + wrd['TargetSSAO'] = (False) + wrd['TargetAutoBuildNodes'] = (True) + wrd['TargetMinimize'] = (True) + # Make sure we are using cycles + if bpy.data.scenes[0].render.engine == 'BLENDER_RENDER': + for scene in bpy.data.scenes: + scene.render.engine = 'CYCLES' + +# Store properties in the world object +def initWorldProperties(): + bpy.types.World.TargetZblendVersion = StringProperty(name = "ZblendVersion") + bpy.types.World.TargetEnum = EnumProperty( + items = [('OSX', 'OSX', 'OSX'), + ('Windows', 'Windows', 'Windows'), + ('Linux', 'Linux', 'Linux'), + ('HTML5', 'HTML5', 'HTML5'), + ('iOS', 'iOS', 'iOS'), + ('Android', 'Android', 'Android')], + name = "Target") + bpy.types.World.TargetRenderer = EnumProperty( + items = [('OGL', 'OGL', 'OGL'), + ('D3D9', 'D3D9', 'D3D9'), + ('D3D11', 'D3D11', 'D3D11')], + name = "Renderer") + bpy.types.World.TargetProjectName = StringProperty(name = "Name") + bpy.types.World.TargetProjectPackage = StringProperty(name = "Package") + bpy.types.World.TargetProjectWidth = IntProperty(name = "Width") + bpy.types.World.TargetProjectHeight = IntProperty(name = "Height") + bpy.types.World.TargetProjectScale = FloatProperty(name = "Scale", default=1.0) + bpy.types.World.TargetProjectOrient = EnumProperty( + items = [('Portrait', 'Portrait', 'Portrait'), + ('Landscape', 'Landscape', 'Landscape')], + name = "Orient") + bpy.types.World.TargetScene = StringProperty(name = "Scene") + bpy.types.World.TargetGravity = StringProperty(name = "Gravity") + bpy.types.World.TargetClear = StringProperty(name = "Clear") + bpy.types.World.TargetFog = BoolProperty(name = "Fog") + bpy.types.World.TargetFogColor = FloatVectorProperty(name = "Fog Color", default=[0.5,0.5,0.7,1], size=4, subtype="COLOR", min=0, max=1) + bpy.types.World.TargetFogDensity = FloatProperty(name = "Fog Density", min=0, max=1) + bpy.types.World.TargetShadowMapping = BoolProperty(name = "Shadow mapping") + bpy.types.World.TargetShadowMapSize = IntProperty(name = "Shadow map size") + bpy.types.World.TargetShading = EnumProperty( + items = [('Forward', 'Forward', 'Forward'), + ('Deferred', 'Deferred', 'Deferred')], + name = "Shading") + bpy.types.World.TargetShader = EnumProperty( + items = [('Physically based', 'Physically based', 'Physically based'), + ('Flat', 'Flat', 'Flat'), + ('Unlit', 'Unlit', 'Unlit')], + name = "Shader") + bpy.types.World.TargetAA = EnumProperty( + items = [('Disabled', 'Disabled', 'Disabled'), + ('2X', '2X', '2X')], + name = "Anti-aliasing") + bpy.types.World.TargetPhysics = EnumProperty( + items = [('Disabled', 'Disabled', 'Disabled'), + ('Bullet', 'Bullet', 'Bullet')], + name = "Physics") + bpy.types.World.TargetSSAO = BoolProperty(name = "SSAO") + bpy.types.World.TargetAutoBuildNodes = BoolProperty(name = "Auto-build nodes") + bpy.types.World.TargetMinimize = BoolProperty(name = "Minimize") + + # Default settings + # todo: check version + if not 'TargetZblendVersion' in bpy.data.worlds[0]: + defaultSettings() + + # Make sure we are using nodes for every material + # Use material nodes + for mat in bpy.data.materials: + bpy.ops.cycles.use_shading_nodes({"material":mat}) + # Use world nodes + for wrd in bpy.data.worlds: + bpy.ops.cycles.use_shading_nodes({"world":wrd}) + + return + +# Store properties in world for now +initWorldProperties() + +# Info panel play +def draw_play_item(self, context): + layout = self.layout + layout.operator("zblend.play") + +# Menu in tools region +class ToolsPanel(bpy.types.Panel): + bl_label = "zblend_project" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + + # Info panel play + bpy.types.INFO_HT_header.prepend(draw_play_item) + + def draw(self, context): + layout = self.layout + wrd = bpy.data.worlds[0] + layout.prop(wrd, 'TargetProjectName') + layout.prop(wrd, 'TargetProjectPackage') + layout.prop(wrd, 'TargetProjectWidth') + layout.prop(wrd, 'TargetProjectHeight') + row = layout.row() + row.active = False + row.prop(wrd, 'TargetProjectScale') + layout.prop_search(wrd, "TargetScene", bpy.data, "scenes", "Scene") + layout.prop(wrd, 'TargetEnum') + if wrd['TargetEnum'] == 4 or wrd['TargetEnum'] == 5: + layout.prop(wrd, 'TargetProjectOrient') + row = layout.row() + row.active = False + row.prop(wrd, 'TargetRenderer') + row = layout.row(align=True) + row.alignment = 'EXPAND' + row.operator("zblend.play") + row.operator("zblend.build") + row.operator("zblend.project") + row = layout.row(align=True) + row.alignment = 'EXPAND' + row.operator("zblend.folder") + row.operator("zblend.clean") + layout.prop_search(wrd, "TargetGravity", bpy.data, "scenes", "Gravity") + layout.prop_search(wrd, "TargetClear", bpy.data, "worlds", "Clear Color") + layout.prop(wrd, 'TargetFog') + if wrd['TargetFog'] == True: + layout.prop(wrd, 'TargetFogColor') + layout.prop(wrd, 'TargetFogDensity') + layout.prop(wrd, 'TargetShadowMapping') + if wrd['TargetShadowMapping'] == True: + layout.prop(wrd, 'TargetShadowMapSize') + layout.prop(wrd, 'TargetShading') + layout.prop(wrd, 'TargetShader') + layout.prop(wrd, 'TargetAA') + layout.prop(wrd, 'TargetPhysics') + layout.prop(wrd, 'TargetSSAO') + row = layout.row() + row.prop(wrd, 'TargetAutoBuildNodes') + if wrd['TargetAutoBuildNodes'] == False: + row.operator("zblend.buildnodes") + layout.prop(wrd, 'TargetMinimize') + layout.operator("zblend.defaultsettings") + +# Used to output json +class Object: + def to_JSON(self): + if bpy.data.worlds[0]['TargetMinimize'] == True: + return json.dumps(self, default=lambda o: o.__dict__, separators=(',',':')) + else: + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + +# Creates asset node in project.kha +def createAsset(filename, type, splitExt=None): + if (splitExt == None): + splitExt = True + + str = filename.split(".") + l = len(str) + name = str[0] + for i in range(1, l - 1): + name += "." + str[i] + + if (splitExt == False): + name = filename + + x = Object() + x.type = type + x.file = filename + x.name = name + return x + +# Creates room node in project.kha +def createRoom(name, assets): + x = Object() + x.name = name + x.parent = None + x.neighbours = [] + x.assets = [] + for a in assets: + if a.type == 'font': + x.assets.append(a.name + str(a.size) + '.kravur') + else: + x.assets.append(a.name) + return x + +# Convert Blender data into game data +def exportGameData(): + # TODO: Set armatures to center of world so skin transform is zero + armatures = [] + for o in bpy.data.objects: + if o.type == 'ARMATURE': + a = Object() + a.armature = o + a.x = o.location.x + a.y = o.location.y + a.z = o.location.z + armatures.append(a) + o.location.x = 0 + o.location.y = 0 + o.location.z = 0 + + # Export scene data + for scene in bpy.data.scenes: + if scene.name[0] != '.': # Skip hidden scenes + bpy.ops.export_scene.zblend({"scene":scene}, filepath='Assets/' + scene.name + '.json') + + # Move armatures back + for a in armatures: + a.armature.location.x = a.x + a.armature.location.y = a.y + a.armature.location.z = a.z + + # Export project file + x = Object() + x.format = 2 + x.game = Object() + x.game.name = bpy.data.worlds[0]['TargetProjectName'] + x.game.width = bpy.data.worlds[0]['TargetProjectWidth'] + x.game.height = bpy.data.worlds[0]['TargetProjectHeight'] + if bpy.data.worlds[0]['TargetAA'] == 1: + x.game.antiAliasingSamples = 2 + x.libraries = ["zblend", "haxebullet", "dependencies"] + # Defined libraries + for o in bpy.data.worlds[0].my_liblist: + if o.enabled_prop: + x.libraries.append(o.name) + # Assets + x.assets = [] + x.rooms = [] + # - Data + x.assets.append(createAsset("data.json", "blob")) + # - Scenes + for s in bpy.data.scenes: + x.assets.append(createAsset(s.name + ".json", "blob")) + # - Defined assets + for o in bpy.data.worlds[0].my_list: + if o.enabled_prop: + if (o.type_prop == 'Atlas'): + x.assets.append(createAsset(o.name + "_metadata.json", "blob")) + x.assets.append(createAsset(o.name + "_atlas.png", "image")) + elif (o.type_prop == 'Font'): + asset = createAsset(o.name, "font") + asset.size = o.size_prop + x.assets.append(asset) + else: + typeName = o.type_prop.lower() + x.assets.append(createAsset(o.name, typeName)) + # - Rooms + x.rooms.append(createRoom("room1", x.assets)) + # Write project file + with open('project.kha', 'w') as f: + f.write(x.to_JSON()) + + + # Export scene properties + data = Object() + + # Objects + objs = [] + for o in bpy.data.objects: + x = Object() + x.name = o.name + x.traits = [] + for t in o.my_traitlist: + # Disabled trait + if t.enabled_prop == False: + continue + + y = Object() + y.type = t.type_prop + + # Script + if y.type == 'Script': + y.class_name = t.class_name_prop + # Custom traits + elif y.type == 'Mesh Renderer': + y.class_name = 'MeshRenderer' + if t.default_material_prop: # Use object material + y.material = "" + else: + y.material = t.material_prop + y.lighting = t.lighting_prop + y.cast_shadow = t.cast_shadow_prop + y.receive_shadow = t.receive_shadow_prop + elif y.type == 'Custom Renderer': + y.class_name = t.class_name_prop + + if t.default_material_prop: # Use object material + y.material = "" + else: + y.material = t.material_prop + + y.shader = t.shader_prop + y.data = t.data_prop + elif y.type == 'Billboard Renderer': + y.type = 'Custom Renderer' + y.class_name = 'BillboardRenderer' + if t.default_material_prop: # Use object material + y.material = "" + else: + y.material = t.material_prop + y.shader = 'billboardshader' + elif y.type == 'Particles Renderer': + y.type = 'Custom Renderer' + y.class_name = 'ParticlesRenderer' + if t.default_material_prop: # Use object material + y.material = "" + else: + y.material = t.material_prop + y.shader = 'particlesshader' + y.data = t.data_prop + # Convert to scripts + elif y.type == 'Nodes': + y.type = 'Script' + y.class_name = t.nodes_name_prop.replace('.', '_') + elif y.type == 'Scene Instance': + y.type = 'Script' + y.class_name = "SceneInstance:'" + t.scene_prop + "'" + elif y.type == 'Animation': + y.type = 'Script' + y.class_name = "Animation:'" + t.start_track_name_prop + "':" + # Names + anim_names = [] + anim_starts = [] + anim_ends = [] + for animt in o.my_animationtraitlist: + if animt.enabled_prop == False: + continue + anim_names.append(animt.name) + anim_starts.append(animt.start_prop) + anim_ends.append(animt.end_prop) + y.class_name += str(anim_names) + ":" + y.class_name += str(anim_starts) + ":" + y.class_name += str(anim_ends) + # Armature offset + for a in armatures: + if o.parent == a.armature: + y.class_name += ":" + str(a.x) + ":" + str(a.y) + ":" + str(a.z) + break + + elif y.type == 'Camera': + y.type = 'Script' + cam = bpy.data.cameras[t.camera_link_prop] + if cam.type == 'PERSP': + y.class_name = 'PerspectiveCamera' + elif cam.type == 'ORTHO': + y.class_name = 'OrthoCamera' + elif y.type == 'Light': + y.type = 'Script' + y.class_name = 'Light' + elif y.type == 'Rigid Body': + if bpy.data.worlds[0]['TargetPhysics'] == 0: + continue + y.type = 'Script' + # Get rigid body + if t.default_body_prop == True: + rb = o.rigid_body + else: + rb = bpy.data.objects[t.body_link_prop].rigid_body + shape = '0' # BOX + if t.custom_shape_prop == True: + if t.custom_shape_type_prop == 'Terrain': + shape = '7' + elif t.custom_shape_type_prop == 'Static Mesh': + shape = '8' + elif rb.collision_shape == 'SPHERE': + shape = '1' + elif rb.collision_shape == 'CONVEX_HULL': + shape = '2' + elif rb.collision_shape == 'MESH': + shape = '3' + elif rb.collision_shape == 'CONE': + shape = '4' + elif rb.collision_shape == 'CYLINDER': + shape = '5' + elif rb.collision_shape == 'CAPSULE': + shape = '6' + body_mass = 0 + if rb.enabled: + body_mass = rb.mass + y.class_name = 'RigidBody:' + str(body_mass) + \ + ':' + shape + \ + ":" + str(rb.friction) + \ + ":" + str(t.shape_size_scale_prop[0]) + \ + ":" + str(t.shape_size_scale_prop[1]) + \ + ":" + str(t.shape_size_scale_prop[2]) + + # Append trait + x.traits.append(y) + + # Material slots + x.materials = [] + if o.material_slots: + for ms in o.material_slots: + x.materials.append(ms.name) + objs.append(x) + + # Materials + mats = [] + for m in bpy.data.materials: + # Make sure material is using nodes + if m.node_tree == None: + continue + x = Object() + x.name = m.name + nodes = m.node_tree.nodes + # Diffuse + if 'Diffuse BSDF' in nodes: + x.diffuse = True + dnode = nodes['Diffuse BSDF'] + dcol = dnode.inputs[0].default_value + x.diffuse_color = [dcol[0], dcol[1], dcol[2], dcol[3]] + else: + x.diffuse = False + # Glossy + if 'Glossy BSDF' in nodes: + x.glossy = True + gnode = nodes['Glossy BSDF'] + gcol = gnode.inputs[0].default_value + x.glossy_color = [gcol[0], gcol[1], gcol[2], gcol[3]] + x.roughness = gnode.inputs[1].default_value + else: + x.glossy = False + # Texture + if 'Image Texture' in nodes: + x.texture = nodes['Image Texture'].image.name.split(".")[0] + else: + x.texture = '' + mats.append(x) + + # Output data json + data.objects = objs + data.materials = mats + data.orient = bpy.data.worlds[0]['TargetProjectOrient'] + data.scene = bpy.data.worlds[0]['TargetScene'] + data.packageName = bpy.data.worlds[0]['TargetProjectPackage'] + gravityscn = bpy.data.scenes[bpy.data.worlds[0]['TargetGravity']] + if gravityscn.use_gravity: + data.gravity = [gravityscn.gravity[0], gravityscn.gravity[1], gravityscn.gravity[2]] + else: + data.gravity = [0.0, 0.0, 0.0] + clearwrd = bpy.data.worlds[bpy.data.worlds[0]['TargetClear']] + # Only 'Background' surface for now + clearcol = clearwrd.node_tree.nodes['Background'].inputs[0].default_value + data.clear = [clearcol[0], clearcol[1], clearcol[2], clearcol[3]] + data.fog = bpy.data.worlds[0]['TargetFog'] + data.fogColor = [bpy.data.worlds[0]['TargetFogColor'][0], bpy.data.worlds[0]['TargetFogColor'][1], bpy.data.worlds[0]['TargetFogColor'][2], bpy.data.worlds[0]['TargetFogColor'][3]] + data.fogDensity = bpy.data.worlds[0]['TargetFogDensity'] + data.shadowMapping = bpy.data.worlds[0]['TargetShadowMapping'] + data.shadowMapSize = bpy.data.worlds[0]['TargetShadowMapSize'] + data.physics = bpy.data.worlds[0]['TargetPhysics'] + data.ssao = bpy.data.worlds[0]['TargetSSAO'] + with open('Assets/data.json', 'w') as f: + f.write(data.to_JSON()) + + # Write Main.hx + # TODO: move to separate file + #if not os.path.isfile('Sources/Main.hx'): + with open('Sources/Main.hx', 'w') as f: + f.write( +"""// Auto-generated +package ; +class Main { + public static function main() { + CompileTime.importPackage('zblend.trait'); + CompileTime.importPackage('""" + bpy.data.worlds[0]['TargetProjectPackage'] + """'); + #if js + untyped __js__(" + function loadScript(url, callback) { + var head = document.getElementsByTagName('head')[0]; + var script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = url; + script.onreadystatechange = callback; + script.onload = callback; + head.appendChild(script); + } + "); + untyped loadScript('ammo.js', start); + #else + start(); + #end + } + static function start() { + var starter = new kha.Starter(); + starter.start(new zblend.Root("ZBlend", "room1", Game)); + } +} +class Game { + public function new() { + zblend.Root.setScene(zblend.Root.gameData.scene); + } +} +""") + +def buildProject(self, build_type=0): + # Save scripts + #area = bpy.context.area + #old_type = area.type + #area.type = 'TEXT_EDITOR' + #for text in bpy.data.texts: + #area.spaces[0].text = text + #bpy.ops.text.save() + ##bpy.ops.text.save() + #area.type = old_type + + # Auto-build nodes + if (bpy.data.worlds[0]['TargetAutoBuildNodes'] == True): + buildNodeTrees() + + # Set dir + s = bpy.data.filepath.split(os.path.sep) + name = s.pop() + name = name.split(".") + name = name[0] + fp = os.path.sep.join(s) + os.chdir(fp) + + # Save blend + bpy.ops.wm.save_mainfile() + + # Export + #self.report({'INFO'}, "Exporting game data...") + exportGameData() + + # Set build command + if (bpy.data.worlds[0]['TargetEnum'] == 0): + bashCommand = "Kha/make.js -t osx" + elif (bpy.data.worlds[0]['TargetEnum'] == 1): + bashCommand = "Kha/make.js -t windows" + elif (bpy.data.worlds[0]['TargetEnum'] == 2): + bashCommand = "Kha/make.js -t linux" + elif (bpy.data.worlds[0]['TargetEnum'] == 3): + bashCommand = "Kha/make.js -t html5" + elif (bpy.data.worlds[0]['TargetEnum'] == 4): + bashCommand = "Kha/make.js -t ios" + elif (bpy.data.worlds[0]['TargetEnum'] == 5): + bashCommand = "Kha/make.js -t android" + + # Build + prefix = "node " + if (platform.system() == "Darwin"): + prefix = "/usr/local/bin/node " + elif (platform.system() == "Linux"): + prefix = "nodejs " + + #p = Process(target=build_process, args=(prefix + bashCommand, open, run, fp, bpy.data.worlds[0]['TargetEnum'], name,)) + #p.start() + #atexit.register(p.terminate) + + blender_path = bpy.app.binary_path + blend_path = bpy.data.filepath + p = subprocess.Popen([blender_path, blend_path, '-b', '-P', fp + '/Libraries/zblend/blender/zblend_build.py', '--', prefix + bashCommand, str(build_type), str(bpy.data.worlds[0]['TargetEnum'])]) + atexit.register(p.terminate) + + self.report({'INFO'}, "Building, see console...") + +def cleanProject(self): + # Set dir + s = bpy.data.filepath.split(os.path.sep) + name = s.pop() + name = name.split(".") + name = name[0] + fp = os.path.sep.join(s) + os.chdir(fp) + + # Remove build dir + if os.path.isdir("build"): + shutil.rmtree('build') + + # Remove compiled nodes + if (bpy.data.worlds[0]['TargetAutoBuildNodes'] == True): + path = 'Sources/' + bpy.data.worlds[0].TargetProjectPackage.replace(".", "/") + "/" + for node_group in bpy.data.node_groups: + node_group_name = node_group.name.replace('.', '_') + os.remove(path + node_group_name + '.hx') + + self.report({'INFO'}, "Clean done") + +# Play +class OBJECT_OT_PLAYButton(bpy.types.Operator): + bl_idname = "zblend.play" + bl_label = "Play" + + def execute(self, context): + buildProject(self, 1) + return{'FINISHED'} + +# Build +class OBJECT_OT_BUILDButton(bpy.types.Operator): + bl_idname = "zblend.build" + bl_label = "Build" + + def execute(self, context): + buildProject(self, 0) + return{'FINISHED'} + +# Open project +class OBJECT_OT_PROJECTButton(bpy.types.Operator): + bl_idname = "zblend.project" + bl_label = "Project" + + def execute(self, context): + buildProject(self, 2) + return{'FINISHED'} + +# Open project folder +class OBJECT_OT_FOLDERButton(bpy.types.Operator): + bl_idname = "zblend.folder" + bl_label = "Folder" + + def execute(self, context): + s = bpy.data.filepath.split(os.path.sep) + name = s.pop() + name = name.split(".") + name = name[0] + fp = os.path.sep.join(s) + + webbrowser.open('file://' + fp) + return{'FINISHED'} + +# Clean project +class OBJECT_OT_CLEANButton(bpy.types.Operator): + bl_idname = "zblend.clean" + bl_label = "Clean" + + def execute(self, context): + cleanProject(self) + return{'FINISHED'} + +# Build nodes +class OBJECT_OT_BUILDNODESButton(bpy.types.Operator): + bl_idname = "zblend.buildnodes" + bl_label = "Build Nodes" + + def execute(self, context): + + buildNodeTrees(); + + self.report({'INFO'}, "Nodes built") + return{'FINISHED'} + +# Default settings +class OBJECT_OT_DEFAULTSETTINGSButton(bpy.types.Operator): + bl_idname = "zblend.defaultsettings" + bl_label = "Default Settings" + + def execute(self, context): + defaultSettings() + self.report({'INFO'}, "Defaults set") + return{'FINISHED'} + + + + +def buildNodeTrees(): + # Set dir + 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].TargetProjectPackage.replace(".", "/")): + os.makedirs('Sources/' + bpy.data.worlds[0].TargetProjectPackage.replace(".", "/")) + + # Export node scripts + for node_group in bpy.data.node_groups: + buildNodeTree(node_group) + +def buildNodeTree(node_group): + rn = getRootNode(node_group) + + path = 'Sources/' + bpy.data.worlds[0].TargetProjectPackage.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].TargetProjectPackage + ';\n\n') + f.write('import zblend.node.*;\n\n') + f.write('class ' + node_group_name + ' extends zblend.trait.NodeExecutor {\n\n') + f.write('\tpublic function new() { super(); }\n\n') + f.write('\toverride function onItemAdd() {\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 = owner.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 +bpy.utils.register_module(__name__) diff --git a/blender/scene.py b/blender/scene.py new file mode 100755 index 00000000..70048ee1 --- /dev/null +++ b/blender/scene.py @@ -0,0 +1,1962 @@ +# ============================================================= +# +# Open Game Engine Exchange +# http://opengex.org/ +# +# Export plugin for Blender +# by Eric Lengyel +# +# Version 1.1.2.2 +# +# Copyright 2015, Terathon Software LLC +# +# This software is licensed under the Creative Commons +# Attribution-ShareAlike 3.0 Unported License: +# +# http://creativecommons.org/licenses/by-sa/3.0/deed.en_US +# +# Adapted to ZBlend framework +# http://zblend.org/ +# by Lubos Lenco +# +# ============================================================= + + +bl_info = { + "name": "ZBlend format (.json)", + "description": "ZBlend Exporter", + "author": "Lubos Lenco", + "version": (1, 0, 0, 0), + "location": "File > Import-Export", + "wiki_url": "http://zblend.org/", + "category": "Import-Export"} + + +import bpy +import math +import json +from bpy_extras.io_utils import ExportHelper + + +kNodeTypeNode = 0 +kNodeTypeBone = 1 +kNodeTypeGeometry = 2 +kNodeTypeLight = 3 +kNodeTypeCamera = 4 + +kAnimationSampled = 0 +kAnimationLinear = 1 +kAnimationBezier = 2 + +kExportEpsilon = 1.0e-6 + + +structIdentifier = ["node", "bone_node", "geometry_node", "light_node", "camera_node"] + + +subtranslationName = ["xpos", "ypos", "zpos"] +subrotationName = ["xrot", "yrot", "zrot"] +subscaleName = ["xscl", "yscl", "zscl"] +deltaSubtranslationName = ["dxpos", "dypos", "dzpos"] +deltaSubrotationName = ["dxrot", "dyrot", "dzrot"] +deltaSubscaleName = ["dxscl", "dyscl", "dzscl"] +axisName = ["x", "y", "z"] + +class ExportVertex: + __slots__ = ("hash", "vertexIndex", "faceIndex", "position", "normal", "color", "texcoord0", "texcoord1") + + def __init__(self): + self.color = [1.0, 1.0, 1.0] + self.texcoord0 = [0.0, 0.0] + self.texcoord1 = [0.0, 0.0] + + def __eq__(self, v): + if (self.hash != v.hash): + return (False) + if (self.position != v.position): + return (False) + if (self.normal != v.normal): + return (False) + if (self.color != v.color): + return (False) + if (self.texcoord0 != v.texcoord0): + return (False) + if (self.texcoord1 != v.texcoord1): + return (False) + return (True) + + def Hash(self): + h = hash(self.position[0]) + h = h * 21737 + hash(self.position[1]) + h = h * 21737 + hash(self.position[2]) + h = h * 21737 + hash(self.normal[0]) + h = h * 21737 + hash(self.normal[1]) + h = h * 21737 + hash(self.normal[2]) + h = h * 21737 + hash(self.color[0]) + h = h * 21737 + hash(self.color[1]) + h = h * 21737 + hash(self.color[2]) + h = h * 21737 + hash(self.texcoord0[0]) + h = h * 21737 + hash(self.texcoord0[1]) + h = h * 21737 + hash(self.texcoord1[0]) + h = h * 21737 + hash(self.texcoord1[1]) + self.hash = h + +class Object: + def to_JSON(self): + if bpy.data.worlds[0]['TargetMinimize'] == True: + return json.dumps(self, default=lambda o: o.__dict__, separators=(',',':')) + else: + return json.dumps(self, default=lambda o: o.__dict__, sort_keys=True, indent=4) + + +class ZBlendExporter(bpy.types.Operator, ExportHelper): + """Export to ZBlend format""" + bl_idname = "export_scene.zblend" + bl_label = "Export ZBlend" + filename_ext = ".json" + + option_export_selection = bpy.props.BoolProperty(name = "Export Selection Only", description = "Export only selected objects", default = False) + option_sample_animation = bpy.props.BoolProperty(name = "Force Sampled Animation", description = "Always export animation as per-frame samples", default = False) + + def WriteColor(self, color): + return [color[0], color[1], color[2]] + + def WriteMatrix(self, matrix): + return [matrix[0][0], matrix[1][0], matrix[2][0], matrix[3][0], + matrix[0][1], matrix[1][1], matrix[2][1], matrix[3][1], + matrix[0][2], matrix[1][2], matrix[2][2], matrix[3][2], + matrix[0][3], matrix[1][3], matrix[2][3], matrix[3][3]] + + def WriteMatrixFlat(self, matrix): + return [matrix[0][0], matrix[1][0], matrix[2][0], matrix[3][0], + matrix[0][1], matrix[1][1], matrix[2][1], matrix[3][1], + matrix[0][2], matrix[1][2], matrix[2][2], matrix[3][2], + matrix[0][3], matrix[1][3], matrix[2][3], matrix[3][3]] + + def WriteVector2D(self, vector): + return [vector[0], vector[1]] + + def WriteVector3D(self, vector): + return [vector[0], vector[1], vector[2]] + + def WriteVertexArray2D(self, vertexArray, attrib): + va = [] + count = len(vertexArray) + k = 0 + + lineCount = count >> 3 + for i in range(lineCount): + for j in range(7): + va += self.WriteVector2D(getattr(vertexArray[k], attrib)) + k += 1 + + va += self.WriteVector2D(getattr(vertexArray[k], attrib)) + k += 1 + + count &= 7 + if (count != 0): + for j in range(count - 1): + va += self.WriteVector2D(getattr(vertexArray[k], attrib)) + k += 1 + + va += self.WriteVector2D(getattr(vertexArray[k], attrib)) + + return va + + def WriteVertexArray3D(self, vertexArray, attrib): + va = [] + count = len(vertexArray) + k = 0 + + lineCount = count >> 3 + for i in range(lineCount): + + for j in range(7): + va += self.WriteVector3D(getattr(vertexArray[k], attrib)) + k += 1 + + va += self.WriteVector3D(getattr(vertexArray[k], attrib)) + k += 1 + + count &= 7 + if (count != 0): + for j in range(count - 1): + va += self.WriteVector3D(getattr(vertexArray[k], attrib)) + k += 1 + + va += self.WriteVector3D(getattr(vertexArray[k], attrib)) + + return va + + def WriteInt(self, i): + return i + + def WriteFloat(self, f): + return f + + def WriteTriangle(self, triangleIndex, indexTable): + i = triangleIndex * 3 + return [indexTable[i], indexTable[i + 1], indexTable[i + 2]] + + + def WriteTriangleArray(self, count, indexTable): + va = [] + triangleIndex = 0 + + lineCount = count >> 4 + for i in range(lineCount): + + for j in range(15): + va += self.WriteTriangle(triangleIndex, indexTable) + triangleIndex += 1 + + va += self.WriteTriangle(triangleIndex, indexTable) + triangleIndex += 1 + + count &= 15 + if (count != 0): + + for j in range(count - 1): + va += self.WriteTriangle(triangleIndex, indexTable) + triangleIndex += 1 + + va += self.WriteTriangle(triangleIndex, indexTable) + + return va + + + + + + @staticmethod + def GetNodeType(node): + if (node.type == "MESH"): + if (len(node.data.polygons) != 0): + return (kNodeTypeGeometry) + elif (node.type == "LAMP"): + type = node.data.type + if ((type == "SUN") or (type == "POINT") or (type == "SPOT")): + return (kNodeTypeLight) + elif (node.type == "CAMERA"): + return (kNodeTypeCamera) + + return (kNodeTypeNode) + + @staticmethod + def GetShapeKeys(mesh): + shapeKeys = mesh.shape_keys + if ((shapeKeys) and (len(shapeKeys.key_blocks) > 1)): + return (shapeKeys) + + return (None) + + def FindNode(self, name): + for nodeRef in self.nodeArray.items(): + if (nodeRef[0].name == name): + return (nodeRef) + return (None) + + @staticmethod + def ClassifyAnimationCurve(fcurve): + linearCount = 0 + bezierCount = 0 + + for key in fcurve.keyframe_points: + interp = key.interpolation + if (interp == "LINEAR"): + linearCount += 1 + elif (interp == "BEZIER"): + bezierCount += 1 + else: + return (kAnimationSampled) + + if (bezierCount == 0): + return (kAnimationLinear) + elif (linearCount == 0): + return (kAnimationBezier) + + return (kAnimationSampled) + + @staticmethod + def AnimationKeysDifferent(fcurve): + keyCount = len(fcurve.keyframe_points) + if (keyCount > 0): + key1 = fcurve.keyframe_points[0].co[1] + + for i in range(1, keyCount): + key2 = fcurve.keyframe_points[i].co[1] + if (math.fabs(key2 - key1) > kExportEpsilon): + return (True) + + return (False) + + + @staticmethod + def AnimationTangentsNonzero(fcurve): + keyCount = len(fcurve.keyframe_points) + if (keyCount > 0): + key = fcurve.keyframe_points[0].co[1] + left = fcurve.keyframe_points[0].handle_left[1] + right = fcurve.keyframe_points[0].handle_right[1] + if ((math.fabs(key - left) > kExportEpsilon) or (math.fabs(right - key) > kExportEpsilon)): + return (True) + + for i in range(1, keyCount): + key = fcurve.keyframe_points[i].co[1] + left = fcurve.keyframe_points[i].handle_left[1] + right = fcurve.keyframe_points[i].handle_right[1] + if ((math.fabs(key - left) > kExportEpsilon) or (math.fabs(right - key) > kExportEpsilon)): + return (True) + + return (False) + + @staticmethod + def MatricesDifferent(m1, m2): + for i in range(4): + for j in range(4): + if (math.fabs(m1[i][j] - m2[i][j]) > kExportEpsilon): + return (True) + + return (False) + + @staticmethod + def CollectBoneAnimation(armature, name): + path = "pose.bones[\"" + name + "\"]." + curveArray = [] + + if (armature.animation_data): + action = armature.animation_data.action + if (action): + for fcurve in action.fcurves: + if (fcurve.data_path.startswith(path)): + curveArray.append(fcurve) + + return (curveArray) + + @staticmethod + def AnimationPresent(fcurve, kind): + if (kind != kAnimationBezier): + return (ZBlendExporter.AnimationKeysDifferent(fcurve)) + + return ((ZBlendExporter.AnimationKeysDifferent(fcurve)) or (ZBlendExporter.AnimationTangentsNonzero(fcurve))) + + + @staticmethod + def DeindexMesh(mesh, materialTable): + + # This function deindexes all vertex positions, colors, and texcoords. + # Three separate ExportVertex structures are created for each triangle. + + vertexArray = mesh.vertices + exportVertexArray = [] + faceIndex = 0 + + for face in mesh.tessfaces: + k1 = face.vertices[0] + k2 = face.vertices[1] + k3 = face.vertices[2] + + v1 = vertexArray[k1] + v2 = vertexArray[k2] + v3 = vertexArray[k3] + + exportVertex = ExportVertex() + exportVertex.vertexIndex = k1 + exportVertex.faceIndex = faceIndex + exportVertex.position = v1.co + exportVertex.normal = v1.normal if (face.use_smooth) else face.normal + exportVertexArray.append(exportVertex) + + exportVertex = ExportVertex() + exportVertex.vertexIndex = k2 + exportVertex.faceIndex = faceIndex + exportVertex.position = v2.co + exportVertex.normal = v2.normal if (face.use_smooth) else face.normal + exportVertexArray.append(exportVertex) + + exportVertex = ExportVertex() + exportVertex.vertexIndex = k3 + exportVertex.faceIndex = faceIndex + exportVertex.position = v3.co + exportVertex.normal = v3.normal if (face.use_smooth) else face.normal + exportVertexArray.append(exportVertex) + + materialTable.append(face.material_index) + + if (len(face.vertices) == 4): + k1 = face.vertices[0] + k2 = face.vertices[2] + k3 = face.vertices[3] + + v1 = vertexArray[k1] + v2 = vertexArray[k2] + v3 = vertexArray[k3] + + exportVertex = ExportVertex() + exportVertex.vertexIndex = k1 + exportVertex.faceIndex = faceIndex + exportVertex.position = v1.co + exportVertex.normal = v1.normal if (face.use_smooth) else face.normal + exportVertexArray.append(exportVertex) + + exportVertex = ExportVertex() + exportVertex.vertexIndex = k2 + exportVertex.faceIndex = faceIndex + exportVertex.position = v2.co + exportVertex.normal = v2.normal if (face.use_smooth) else face.normal + exportVertexArray.append(exportVertex) + + exportVertex = ExportVertex() + exportVertex.vertexIndex = k3 + exportVertex.faceIndex = faceIndex + exportVertex.position = v3.co + exportVertex.normal = v3.normal if (face.use_smooth) else face.normal + exportVertexArray.append(exportVertex) + + materialTable.append(face.material_index) + + faceIndex += 1 + + colorCount = len(mesh.tessface_vertex_colors) + if (colorCount > 0): + colorFace = mesh.tessface_vertex_colors[0].data + vertexIndex = 0 + faceIndex = 0 + + for face in mesh.tessfaces: + cf = colorFace[faceIndex] + exportVertexArray[vertexIndex].color = cf.color1 + vertexIndex += 1 + exportVertexArray[vertexIndex].color = cf.color2 + vertexIndex += 1 + exportVertexArray[vertexIndex].color = cf.color3 + vertexIndex += 1 + + if (len(face.vertices) == 4): + exportVertexArray[vertexIndex].color = cf.color1 + vertexIndex += 1 + exportVertexArray[vertexIndex].color = cf.color3 + vertexIndex += 1 + exportVertexArray[vertexIndex].color = cf.color4 + vertexIndex += 1 + + faceIndex += 1 + + texcoordCount = len(mesh.tessface_uv_textures) + if (texcoordCount > 0): + texcoordFace = mesh.tessface_uv_textures[0].data + vertexIndex = 0 + faceIndex = 0 + + for face in mesh.tessfaces: + tf = texcoordFace[faceIndex] + exportVertexArray[vertexIndex].texcoord0 = tf.uv1 + vertexIndex += 1 + exportVertexArray[vertexIndex].texcoord0 = tf.uv2 + vertexIndex += 1 + exportVertexArray[vertexIndex].texcoord0 = tf.uv3 + vertexIndex += 1 + + if (len(face.vertices) == 4): + exportVertexArray[vertexIndex].texcoord0 = tf.uv1 + vertexIndex += 1 + exportVertexArray[vertexIndex].texcoord0 = tf.uv3 + vertexIndex += 1 + exportVertexArray[vertexIndex].texcoord0 = tf.uv4 + vertexIndex += 1 + + faceIndex += 1 + + if (texcoordCount > 1): + texcoordFace = mesh.tessface_uv_textures[1].data + vertexIndex = 0 + faceIndex = 0 + + for face in mesh.tessfaces: + tf = texcoordFace[faceIndex] + exportVertexArray[vertexIndex].texcoord1 = tf.uv1 + vertexIndex += 1 + exportVertexArray[vertexIndex].texcoord1 = tf.uv2 + vertexIndex += 1 + exportVertexArray[vertexIndex].texcoord1 = tf.uv3 + vertexIndex += 1 + + if (len(face.vertices) == 4): + exportVertexArray[vertexIndex].texcoord1 = tf.uv1 + vertexIndex += 1 + exportVertexArray[vertexIndex].texcoord1 = tf.uv3 + vertexIndex += 1 + exportVertexArray[vertexIndex].texcoord1 = tf.uv4 + vertexIndex += 1 + + faceIndex += 1 + + for ev in exportVertexArray: + ev.Hash() + + return (exportVertexArray) + + @staticmethod + def FindExportVertex(bucket, exportVertexArray, vertex): + for index in bucket: + if (exportVertexArray[index] == vertex): + return (index) + + return (-1) + + @staticmethod + def UnifyVertices(exportVertexArray, indexTable): + + # This function looks for identical vertices having exactly the same position, normal, + # color, and texcoords. Duplicate vertices are unified, and a new index table is returned. + + bucketCount = len(exportVertexArray) >> 3 + if (bucketCount > 1): + + # Round down to nearest power of two. + + while True: + count = bucketCount & (bucketCount - 1) + if (count == 0): + break + bucketCount = count + else: + bucketCount = 1 + + hashTable = [[] for i in range(bucketCount)] + unifiedVertexArray = [] + + for i in range(len(exportVertexArray)): + ev = exportVertexArray[i] + bucket = ev.hash & (bucketCount - 1) + index = ZBlendExporter.FindExportVertex(hashTable[bucket], exportVertexArray, ev) + if (index < 0): + indexTable.append(len(unifiedVertexArray)) + unifiedVertexArray.append(ev) + hashTable[bucket].append(i) + else: + indexTable.append(indexTable[index]) + + return (unifiedVertexArray) + + + + + + + + def ExportBone(self, armature, bone, scene, o): + nodeRef = self.nodeArray.get(bone) + + if (nodeRef): + o.type = structIdentifier[nodeRef["nodeType"]] + o.id = nodeRef["structName"] + + name = bone.name + if (name != ""): + o.name = name + + self.ExportBoneTransform(armature, bone, scene, o) + + o.nodes = [] # TODO + for subnode in bone.children: + so = Object() + self.ExportBone(armature, subnode, scene, so) + o.nodes.append(so) + + # Export any ordinary nodes that are parented to this bone. + + boneSubnodeArray = self.boneParentArray.get(bone.name) + if (boneSubnodeArray): + poseBone = None + if (not bone.use_relative_parent): + poseBone = armature.pose.bones.get(bone.name) + + for subnode in boneSubnodeArray: + self.ExportNode(subnode, scene, poseBone, o) + + + + + def ExportNodeSampledAnimation(self, node, scene, o): + + # This function exports animation as full 4x4 matrices for each frame. + + currentFrame = scene.frame_current + currentSubframe = scene.frame_subframe + + animationFlag = False + m1 = node.matrix_local.copy() + + for i in range(self.beginFrame, self.endFrame): + scene.frame_set(i) + m2 = node.matrix_local + if (ZBlendExporter.MatricesDifferent(m1, m2)): + animationFlag = True + break + + if (animationFlag): + o.animation = Object() # TODO: multiple tracks? + + o.animation.track = Object() + o.animation.track.target = "transform" + + o.animation.track.time = Object() + + o.animation.track.time = Object() + o.animation.track.time.values = [] + + for i in range(self.beginFrame, self.endFrame): + o.animation.track.time.values.append(self.WriteFloat((i - self.beginFrame) * self.frameTime)) + + o.animation.track.time.values.append(self.WriteFloat(self.endFrame * self.frameTime)) + + o.animation.track.value = Object() + o.animation.track.value.values = [] + + for i in range(self.beginFrame, self.endFrame): + scene.frame_set(i) + o.animation.track.value.values.append(self.WriteMatrixFlat(node.matrix_local)) + + scene.frame_set(self.endFrame) + o.animation.track.value.values.append(self.WriteMatrixFlat(node.matrix_local)) + + scene.frame_set(currentFrame, currentSubframe) + + + def ExportBoneSampledAnimation(self, poseBone, scene, o): + + # This function exports bone animation as full 4x4 matrices for each frame. + + currentFrame = scene.frame_current + currentSubframe = scene.frame_subframe + + animationFlag = False + m1 = poseBone.matrix.copy() + + for i in range(self.beginFrame, self.endFrame): + scene.frame_set(i) + m2 = poseBone.matrix + if (ZBlendExporter.MatricesDifferent(m1, m2)): + animationFlag = True + break + + if (animationFlag): + o.animation = Object() + o.animation.track = Object() + o.animation.track.target = "transform" + o.animation.track.time = Object() + o.animation.track.time.values = [] + + for i in range(self.beginFrame, self.endFrame): + o.animation.track.time.values.append(self.WriteFloat((i - self.beginFrame) * self.frameTime)) + + o.animation.track.time.values.append(self.WriteFloat(self.endFrame * self.frameTime)) + + o.animation.track.value = Object() + o.animation.track.value.values = [] + + parent = poseBone.parent + if (parent): + for i in range(self.beginFrame, self.endFrame): + scene.frame_set(i) + o.animation.track.value.values.append(self.WriteMatrixFlat(parent.matrix.inverted() * poseBone.matrix)) + + scene.frame_set(self.endFrame) + o.animation.track.value.values.append(self.WriteMatrixFlat(parent.matrix.inverted() * poseBone.matrix)) + + else: + for i in range(self.beginFrame, self.endFrame): + scene.frame_set(i) + o.animation.track.value.values.append(self.WriteMatrixFlat(poseBone.matrix)) + + scene.frame_set(self.endFrame) + o.animation.track.value.values.append(self.WriteMatrixFlat(poseBone.matrix)) + + scene.frame_set(currentFrame, currentSubframe) + + + + def ExportNodeTransform(self, node, scene, o): + posAnimCurve = [None, None, None] + rotAnimCurve = [None, None, None] + sclAnimCurve = [None, None, None] + posAnimKind = [0, 0, 0] + rotAnimKind = [0, 0, 0] + sclAnimKind = [0, 0, 0] + + deltaPosAnimCurve = [None, None, None] + deltaRotAnimCurve = [None, None, None] + deltaSclAnimCurve = [None, None, None] + deltaPosAnimKind = [0, 0, 0] + deltaRotAnimKind = [0, 0, 0] + deltaSclAnimKind = [0, 0, 0] + + positionAnimated = False + rotationAnimated = False + scaleAnimated = False + posAnimated = [False, False, False] + rotAnimated = [False, False, False] + sclAnimated = [False, False, False] + + deltaPositionAnimated = False + deltaRotationAnimated = False + deltaScaleAnimated = False + deltaPosAnimated = [False, False, False] + deltaRotAnimated = [False, False, False] + deltaSclAnimated = [False, False, False] + + mode = node.rotation_mode + sampledAnimation = ((self.sampleAnimationFlag) or (mode == "QUATERNION") or (mode == "AXIS_ANGLE")) + + if ((not sampledAnimation) and (node.animation_data)): + action = node.animation_data.action + if (action): + for fcurve in action.fcurves: + kind = ZBlendExporter.ClassifyAnimationCurve(fcurve) + if (kind != kAnimationSampled): + if (fcurve.data_path == "location"): + for i in range(3): + if ((fcurve.array_index == i) and (not posAnimCurve[i])): + posAnimCurve[i] = fcurve + posAnimKind[i] = kind + if (ZBlendExporter.AnimationPresent(fcurve, kind)): + posAnimated[i] = True + elif (fcurve.data_path == "delta_location"): + for i in range(3): + if ((fcurve.array_index == i) and (not deltaPosAnimCurve[i])): + deltaPosAnimCurve[i] = fcurve + deltaPosAnimKind[i] = kind + if (ZBlendExporter.AnimationPresent(fcurve, kind)): + deltaPosAnimated[i] = True + elif (fcurve.data_path == "rotation_euler"): + for i in range(3): + if ((fcurve.array_index == i) and (not rotAnimCurve[i])): + rotAnimCurve[i] = fcurve + rotAnimKind[i] = kind + if (ZBlendExporter.AnimationPresent(fcurve, kind)): + rotAnimated[i] = True + elif (fcurve.data_path == "delta_rotation_euler"): + for i in range(3): + if ((fcurve.array_index == i) and (not deltaRotAnimCurve[i])): + deltaRotAnimCurve[i] = fcurve + deltaRotAnimKind[i] = kind + if (ZBlendExporter.AnimationPresent(fcurve, kind)): + deltaRotAnimated[i] = True + elif (fcurve.data_path == "scale"): + for i in range(3): + if ((fcurve.array_index == i) and (not sclAnimCurve[i])): + sclAnimCurve[i] = fcurve + sclAnimKind[i] = kind + if (ZBlendExporter.AnimationPresent(fcurve, kind)): + sclAnimated[i] = True + elif (fcurve.data_path == "delta_scale"): + for i in range(3): + if ((fcurve.array_index == i) and (not deltaSclAnimCurve[i])): + deltaSclAnimCurve[i] = fcurve + deltaSclAnimKind[i] = kind + if (ZBlendExporter.AnimationPresent(fcurve, kind)): + deltaSclAnimated[i] = True + elif ((fcurve.data_path == "rotation_axis_angle") or (fcurve.data_path == "rotation_quaternion") or (fcurve.data_path == "delta_rotation_quaternion")): + sampledAnimation = True + break + else: + sampledAnimation = True + break + + positionAnimated = posAnimated[0] | posAnimated[1] | posAnimated[2] + rotationAnimated = rotAnimated[0] | rotAnimated[1] | rotAnimated[2] + scaleAnimated = sclAnimated[0] | sclAnimated[1] | sclAnimated[2] + + deltaPositionAnimated = deltaPosAnimated[0] | deltaPosAnimated[1] | deltaPosAnimated[2] + deltaRotationAnimated = deltaRotAnimated[0] | deltaRotAnimated[1] | deltaRotAnimated[2] + deltaScaleAnimated = deltaSclAnimated[0] | deltaSclAnimated[1] | deltaSclAnimated[2] + + if ((sampledAnimation) or ((not positionAnimated) and (not rotationAnimated) and (not scaleAnimated) and (not deltaPositionAnimated) and (not deltaRotationAnimated) and (not deltaScaleAnimated))): + + # If there's no keyframe animation at all, then write the node transform as a single 4x4 matrix. + # We might still be exporting sampled animation below. + + o.transform = Object() + + if (sampledAnimation): + o.transform.target = "transform" + + o.transform.values = self.WriteMatrix(node.matrix_local) + + if (sampledAnimation): + self.ExportNodeSampledAnimation(node, scene, o) + + else: + structFlag = False + + deltaTranslation = node.delta_location + if (deltaPositionAnimated): + + # When the delta location is animated, write the x, y, and z components separately + # so they can be targeted by different tracks having different sets of keys. + + for i in range(3): + pos = deltaTranslation[i] + if ((deltaPosAnimated[i]) or (math.fabs(pos) > kExportEpsilon)): + # self.IndentWrite(B"Translation %", 0, structFlag) + # self.Write(deltaSubtranslationName[i]) + # self.Write(B" (kind = \"") + # self.Write(axisName[i]) + # self.Write(B"\")\n") + # self.IndentWrite(B"{\n") + # self.IndentWrite(B"float {", 1) + # self.WriteFloat(pos) + # self.Write(B"}") + # self.IndentWrite(B"}\n", 0, True) + + structFlag = True + + elif ((math.fabs(deltaTranslation[0]) > kExportEpsilon) or (math.fabs(deltaTranslation[1]) > kExportEpsilon) or (math.fabs(deltaTranslation[2]) > kExportEpsilon)): + # self.IndentWrite(B"Translation\n") + # self.IndentWrite(B"{\n") + # self.IndentWrite(B"float[3] {", 1) + # self.WriteVector3D(deltaTranslation) + # self.Write(B"}") + # self.IndentWrite(B"}\n", 0, True) + + structFlag = True + + translation = node.location + if (positionAnimated): + + # When the location is animated, write the x, y, and z components separately + # so they can be targeted by different tracks having different sets of keys. + + for i in range(3): + pos = translation[i] + if ((posAnimated[i]) or (math.fabs(pos) > kExportEpsilon)): + # self.IndentWrite(B"Translation %", 0, structFlag) + # self.Write(subtranslationName[i]) + # self.Write(B" (kind = \"") + # self.Write(axisName[i]) + # self.Write(B"\")\n") + # self.IndentWrite(B"{\n") + # self.IndentWrite(B"float {", 1) + # self.WriteFloat(pos) + # self.Write(B"}") + # self.IndentWrite(B"}\n", 0, True) + + structFlag = True + + elif ((math.fabs(translation[0]) > kExportEpsilon) or (math.fabs(translation[1]) > kExportEpsilon) or (math.fabs(translation[2]) > kExportEpsilon)): + # self.IndentWrite(B"Translation\n") + # self.IndentWrite(B"{\n") + # self.IndentWrite(B"float[3] {", 1) + # self.WriteVector3D(translation) + # self.Write(B"}") + # self.IndentWrite(B"}\n", 0, True) + + structFlag = True + + if (deltaRotationAnimated): + + # When the delta rotation is animated, write three separate Euler angle rotations + # so they can be targeted by different tracks having different sets of keys. + + for i in range(3): + axis = ord(mode[2 - i]) - 0x58 + angle = node.delta_rotation_euler[axis] + if ((deltaRotAnimated[axis]) or (math.fabs(angle) > kExportEpsilon)): + # self.IndentWrite(B"Rotation %", 0, structFlag) + # self.Write(deltaSubrotationName[axis]) + # self.Write(B" (kind = \"") + # self.Write(axisName[axis]) + # self.Write(B"\")\n") + # self.IndentWrite(B"{\n") + # self.IndentWrite(B"float {", 1) + # self.WriteFloat(angle) + # self.Write(B"}") + # self.IndentWrite(B"}\n", 0, True) + + structFlag = True + + else: + + # When the delta rotation is not animated, write it in the representation given by + # the node's current rotation mode. (There is no axis-angle delta rotation.) + + if (mode == "QUATERNION"): + quaternion = node.delta_rotation_quaternion + if ((math.fabs(quaternion[0] - 1.0) > kExportEpsilon) or (math.fabs(quaternion[1]) > kExportEpsilon) or (math.fabs(quaternion[2]) > kExportEpsilon) or (math.fabs(quaternion[3]) > kExportEpsilon)): + # self.IndentWrite(B"Rotation (kind = \"quaternion\")\n", 0, structFlag) + # self.IndentWrite(B"{\n") + # self.IndentWrite(B"float[4] {", 1) + # self.WriteQuaternion(quaternion) + # self.Write(B"}") + # self.IndentWrite(B"}\n", 0, True) + + structFlag = True + + else: + for i in range(3): + axis = ord(mode[2 - i]) - 0x58 + angle = node.delta_rotation_euler[axis] + if (math.fabs(angle) > kExportEpsilon): + # self.IndentWrite(B"Rotation (kind = \"", 0, structFlag) + # self.Write(axisName[axis]) + # self.Write(B"\")\n") + # self.IndentWrite(B"{\n") + # self.IndentWrite(B"float {", 1) + # self.WriteFloat(angle) + # self.Write(B"}") + # self.IndentWrite(B"}\n", 0, True) + + structFlag = True + + if (rotationAnimated): + + # When the rotation is animated, write three separate Euler angle rotations + # so they can be targeted by different tracks having different sets of keys. + + for i in range(3): + axis = ord(mode[2 - i]) - 0x58 + angle = node.rotation_euler[axis] + if ((rotAnimated[axis]) or (math.fabs(angle) > kExportEpsilon)): + # self.IndentWrite(B"Rotation %", 0, structFlag) + # self.Write(subrotationName[axis]) + # self.Write(B" (kind = \"") + # self.Write(axisName[axis]) + # self.Write(B"\")\n") + # self.IndentWrite(B"{\n") + # self.IndentWrite(B"float {", 1) + # self.WriteFloat(angle) + # self.Write(B"}") + # self.IndentWrite(B"}\n", 0, True) + + structFlag = True + + else: + + # When the rotation is not animated, write it in the representation given by + # the node's current rotation mode. + + if (mode == "QUATERNION"): + quaternion = node.rotation_quaternion + if ((math.fabs(quaternion[0] - 1.0) > kExportEpsilon) or (math.fabs(quaternion[1]) > kExportEpsilon) or (math.fabs(quaternion[2]) > kExportEpsilon) or (math.fabs(quaternion[3]) > kExportEpsilon)): + # self.IndentWrite(B"Rotation (kind = \"quaternion\")\n", 0, structFlag) + # self.IndentWrite(B"{\n") + # self.IndentWrite(B"float[4] {", 1) + # self.WriteQuaternion(quaternion) + # self.Write(B"}") + # self.IndentWrite(B"}\n", 0, True) + + structFlag = True + + elif (mode == "AXIS_ANGLE"): + if (math.fabs(node.rotation_axis_angle[0]) > kExportEpsilon): + # self.IndentWrite(B"Rotation (kind = \"axis\")\n", 0, structFlag) + # self.IndentWrite(B"{\n") + # self.IndentWrite(B"float[4] {", 1) + # self.WriteVector4D(node.rotation_axis_angle) + # self.Write(B"}") + # self.IndentWrite(B"}\n", 0, True) + + structFlag = True + + else: + for i in range(3): + axis = ord(mode[2 - i]) - 0x58 + angle = node.rotation_euler[axis] + if (math.fabs(angle) > kExportEpsilon): + # self.IndentWrite(B"Rotation (kind = \"", 0, structFlag) + # self.Write(axisName[axis]) + # self.Write(B"\")\n") + # self.IndentWrite(B"{\n") + # self.IndentWrite(B"float {", 1) + # self.WriteFloat(angle) + # self.Write(B"}") + # self.IndentWrite(B"}\n", 0, True) + + structFlag = True + + deltaScale = node.delta_scale + if (deltaScaleAnimated): + + # When the delta scale is animated, write the x, y, and z components separately + # so they can be targeted by different tracks having different sets of keys. + + for i in range(3): + scl = deltaScale[i] + if ((deltaSclAnimated[i]) or (math.fabs(scl) > kExportEpsilon)): + # self.IndentWrite(B"Scale %", 0, structFlag) + # self.Write(deltaSubscaleName[i]) + # self.Write(B" (kind = \"") + # self.Write(axisName[i]) + # self.Write(B"\")\n") + # self.IndentWrite(B"{\n") + # self.IndentWrite(B"float {", 1) + # self.WriteFloat(scl) + # self.Write(B"}") + # self.IndentWrite(B"}\n", 0, True) + + structFlag = True + + elif ((math.fabs(deltaScale[0] - 1.0) > kExportEpsilon) or (math.fabs(deltaScale[1] - 1.0) > kExportEpsilon) or (math.fabs(deltaScale[2] - 1.0) > kExportEpsilon)): + # self.IndentWrite(B"Scale\n", 0, structFlag) + # self.IndentWrite(B"{\n") + # self.IndentWrite(B"float[3] {", 1) + # self.WriteVector3D(deltaScale) + # self.Write(B"}") + # self.IndentWrite(B"}\n", 0, True) + + structFlag = True + + scale = node.scale + if (scaleAnimated): + + # When the scale is animated, write the x, y, and z components separately + # so they can be targeted by different tracks having different sets of keys. + + for i in range(3): + scl = scale[i] + if ((sclAnimated[i]) or (math.fabs(scl) > kExportEpsilon)): + # self.IndentWrite(B"Scale %", 0, structFlag) + # self.Write(subscaleName[i]) + # self.Write(B" (kind = \"") + # self.Write(axisName[i]) + # self.Write(B"\")\n") + # self.IndentWrite(B"{\n") + # self.IndentWrite(B"float {", 1) + # self.WriteFloat(scl) + # self.Write(B"}") + # self.IndentWrite(B"}\n", 0, True) + + structFlag = True + + elif ((math.fabs(scale[0] - 1.0) > kExportEpsilon) or (math.fabs(scale[1] - 1.0) > kExportEpsilon) or (math.fabs(scale[2] - 1.0) > kExportEpsilon)): + # self.IndentWrite(B"Scale\n", 0, structFlag) + # self.IndentWrite(B"{\n") + # self.IndentWrite(B"float[3] {", 1) + # self.WriteVector3D(scale) + # self.Write(B"}") + # self.IndentWrite(B"}\n", 0, True) + + structFlag = True + + # Export the animation tracks. + + #o.animation = Object() + ''' + self.IndentWrite(B"Animation (begin = ", 0, True) + self.WriteFloat((action.frame_range[0] - self.beginFrame) * self.frameTime) + self.Write(B", end = ") + self.WriteFloat((action.frame_range[1] - self.beginFrame) * self.frameTime) + self.Write(B")\n") + self.IndentWrite(B"{\n") + self.indentLevel += 1 + + structFlag = False + + if (positionAnimated): + for i in range(3): + if (posAnimated[i]): + self.ExportAnimationTrack(posAnimCurve[i], posAnimKind[i], subtranslationName[i], structFlag) + structFlag = True + + if (rotationAnimated): + for i in range(3): + if (rotAnimated[i]): + self.ExportAnimationTrack(rotAnimCurve[i], rotAnimKind[i], subrotationName[i], structFlag) + structFlag = True + + if (scaleAnimated): + for i in range(3): + if (sclAnimated[i]): + self.ExportAnimationTrack(sclAnimCurve[i], sclAnimKind[i], subscaleName[i], structFlag) + structFlag = True + + if (deltaPositionAnimated): + for i in range(3): + if (deltaPosAnimated[i]): + self.ExportAnimationTrack(deltaPosAnimCurve[i], deltaPosAnimKind[i], deltaSubtranslationName[i], structFlag) + structFlag = True + + if (deltaRotationAnimated): + for i in range(3): + if (deltaRotAnimated[i]): + self.ExportAnimationTrack(deltaRotAnimCurve[i], deltaRotAnimKind[i], deltaSubrotationName[i], structFlag) + structFlag = True + + if (deltaScaleAnimated): + for i in range(3): + if (deltaSclAnimated[i]): + self.ExportAnimationTrack(deltaSclAnimCurve[i], deltaSclAnimKind[i], deltaSubscaleName[i], structFlag) + structFlag = True + ''' + + + + def ProcessBone(self, bone): + if ((self.exportAllFlag) or (bone.select)): + self.nodeArray[bone] = {"nodeType" : kNodeTypeBone, "structName" : "node" + str(len(self.nodeArray) + 1)} + + for subnode in bone.children: + self.ProcessBone(subnode) + + + def ProcessNode(self, node): + if ((self.exportAllFlag) or (node.select)): + type = ZBlendExporter.GetNodeType(node) + self.nodeArray[node] = {"nodeType" : type, "structName" : "node" + str(len(self.nodeArray) + 1)} + + if (node.parent_type == "BONE"): + boneSubnodeArray = self.boneParentArray.get(node.parent_bone) + if (boneSubnodeArray): + boneSubnodeArray.append(node) + else: + self.boneParentArray[node.parent_bone] = [node] + + if (node.type == "ARMATURE"): + skeleton = node.data + if (skeleton): + for bone in skeleton.bones: + if (not bone.parent): + self.ProcessBone(bone) + + for subnode in node.children: + self.ProcessNode(subnode) + + + def ProcessSkinnedMeshes(self): + for nodeRef in self.nodeArray.items(): + if (nodeRef[1]["nodeType"] == kNodeTypeGeometry): + armature = nodeRef[0].find_armature() + if (armature): + for bone in armature.data.bones: + boneRef = self.FindNode(bone.name) + if (boneRef): + + # If a node is used as a bone, then we force its type to be a bone. + + boneRef[1]["nodeType"] = kNodeTypeBone + + + + def ExportBoneTransform(self, armature, bone, scene, o): + + curveArray = self.CollectBoneAnimation(armature, bone.name) + animation = ((len(curveArray) != 0) or (self.sampleAnimationFlag)) + + transform = bone.matrix_local.copy() + parentBone = bone.parent + if (parentBone): + transform = parentBone.matrix_local.inverted() * transform + + poseBone = armature.pose.bones.get(bone.name) + if (poseBone): + transform = poseBone.matrix.copy() + parentPoseBone = poseBone.parent + if (parentPoseBone): + transform = parentPoseBone.matrix.inverted() * transform + + + o.transform = Object(); + + #if (animation): + # self.Write(B" %transform") + + o.transform.values = self.WriteMatrix(transform) + + if ((animation) and (poseBone)): + self.ExportBoneSampledAnimation(poseBone, scene, o) + + + + def ExportMaterialRef(self, material, index, o): + if (not material in self.materialArray): + self.materialArray[material] = {"structName" : "material" + str(len(self.materialArray) + 1)} + + o.material_refs.append(self.materialArray[material]["structName"]) + + + + def ExportNode(self, node, scene, poseBone = None, parento = None): + + # This function exports a single node in the scene and includes its name, + # object reference, material references (for geometries), and transform. + # Subnodes are then exported recursively. + + nodeRef = self.nodeArray.get(node) + if (nodeRef): + type = nodeRef["nodeType"] + + o = Object() + o.type = structIdentifier[type] + o.id = nodeRef["structName"] + + if (type == kNodeTypeGeometry): + if (node.hide_render): + o.visible = False + + # Export the node's name if it has one. + + name = node.name + if (name != ""): + o.name = name + + # Export the object reference and material references. + + object = node.data + + if (type == kNodeTypeGeometry): + if (not object in self.geometryArray): + self.geometryArray[object] = {"structName" : "geometry" + str(len(self.geometryArray) + 1), "nodeTable" : [node]} + else: + self.geometryArray[object]["nodeTable"].append(node) + + o.object_ref = self.geometryArray[object]["structName"] + o.material_refs = [] + + for i in range(len(node.material_slots)): + self.ExportMaterialRef(node.material_slots[i].material, i, o) + + shapeKeys = ZBlendExporter.GetShapeKeys(object) + #if (shapeKeys): + # self.ExportMorphWeights(node, shapeKeys, scene, o) + # TODO + + elif (type == kNodeTypeLight): + if (not object in self.lightArray): + self.lightArray[object] = {"structName" : "light" + str(len(self.lightArray) + 1), "nodeTable" : [node]} + else: + self.lightArray[object]["nodeTable"].append(node) + + o.object_ref = self.lightArray[object]["structName"] + + elif (type == kNodeTypeCamera): + if (not object in self.cameraArray): + self.cameraArray[object] = {"structName" : "camera" + str(len(self.cameraArray) + 1), "nodeTable" : [node]} + else: + self.cameraArray[object]["nodeTable"].append(node) + + o.object_ref = self.cameraArray[object]["structName"] + + if (poseBone): + + # If the node is parented to a bone and is not relative, then undo the bone's transform. + + o.transform = Object() + o.transform.values = self.WriteMatrix(poseBone.matrix.inverted()) + + # Export the transform. If the node is animated, then animation tracks are exported here. + + self.ExportNodeTransform(node, scene, o) + + if (node.type == "ARMATURE"): + skeleton = node.data + if (skeleton): + o.nodes = [] + for bone in skeleton.bones: + if (not bone.parent): + co = Object() # TODO + self.ExportBone(node, bone, scene, co) + o.nodes.append(co) + + if (parento == None): + self.output.nodes.append(o) + else: + parento.nodes.append(o) + + + if not hasattr(o, 'nodes'): + o.nodes = [] + for subnode in node.children: + if (subnode.parent_type != "BONE"): + self.ExportNode(subnode, scene, None, o) + + + + + + + + + def ExportSkin(self, node, armature, exportVertexArray, om): + + # This function exports all skinning data, which includes the skeleton + # and per-vertex bone influence data. + + om.skin = Object() + + # Write the skin bind pose transform. + + om.skin.transform = Object() + om.skin.transform.values = self.WriteMatrix(node.matrix_world) + + # Export the skeleton, which includes an array of bone node references + # and and array of per-bone bind pose transforms. + + om.skin.skeleton = Object() + + # Write the bone node reference array. + + om.skin.skeleton.bone_ref_array = [] + + boneArray = armature.data.bones + boneCount = len(boneArray) + + #self.IndentWrite(B"ref\t\t\t// ") + #self.WriteInt(boneCount) + + for i in range(boneCount): + boneRef = self.FindNode(boneArray[i].name) + if (boneRef): + om.skin.skeleton.bone_ref_array.append(boneRef[1]["structName"]) + else: + om.skin.skeleton.bone_ref_array.append("null") + + # Write the bind pose transform array. + + om.skin.skeleton.transforms = [] + + #self.IndentWrite(B"float[16]\t// ") + #self.WriteInt(boneCount) + + for i in range(boneCount): + om.skin.skeleton.transforms.append(self.WriteMatrixFlat(armature.matrix_world * boneArray[i].matrix_local)) + + # Export the per-vertex bone influence data. + + groupRemap = [] + + for group in node.vertex_groups: + groupName = group.name + for i in range(boneCount): + if (boneArray[i].name == groupName): + groupRemap.append(i) + break + else: + groupRemap.append(-1) + + boneCountArray = [] + boneIndexArray = [] + boneWeightArray = [] + + meshVertexArray = node.data.vertices + for ev in exportVertexArray: + boneCount = 0 + totalWeight = 0.0 + for element in meshVertexArray[ev.vertexIndex].groups: + boneIndex = groupRemap[element.group] + boneWeight = element.weight + if ((boneIndex >= 0) and (boneWeight != 0.0)): + boneCount += 1 + totalWeight += boneWeight + boneIndexArray.append(boneIndex) + boneWeightArray.append(boneWeight) + boneCountArray.append(boneCount) + + if (totalWeight != 0.0): + normalizer = 1.0 / totalWeight + for i in range(-boneCount, 0): + boneWeightArray[i] *= normalizer + + # Write the bone count array. There is one entry per vertex. + + om.skin.bone_count_array = boneCountArray + + #self.IndentWrite(B"unsigned_int16\t\t// ") + #self.WriteInt(len(boneCountArray)) + #self.WriteIntArray(boneCountArray) + + # Write the bone index array. The number of entries is the sum of the bone counts for all vertices. + + om.skin.bone_index_array = boneIndexArray + + # Write the bone weight array. The number of entries is the sum of the bone counts for all vertices. + + om.skin.bone_weight_array = boneWeightArray + + + + + + + + + + + def ExportGeometry(self, objectRef, scene): + + # This function exports a single geometry object. + + o = Object() + + o.id = objectRef[1]["structName"] + #self.WriteNodeTable(objectRef) #// + # TODO + + node = objectRef[1]["nodeTable"][0] + mesh = objectRef[0] + + structFlag = False; + + # Save the morph state if necessary. + + activeShapeKeyIndex = node.active_shape_key_index + showOnlyShapeKey = node.show_only_shape_key + currentMorphValue = [] + + shapeKeys = ZBlendExporter.GetShapeKeys(mesh) + if (shapeKeys): + node.active_shape_key_index = 0 + node.show_only_shape_key = True + + baseIndex = 0 + relative = shapeKeys.use_relative + if (relative): + morphCount = 0 + baseName = shapeKeys.reference_key.name + for block in shapeKeys.key_blocks: + if (block.name == baseName): + baseIndex = morphCount + break + morphCount += 1 + + morphCount = 0 + for block in shapeKeys.key_blocks: + currentMorphValue.append(block.value) + block.value = 0.0 + + if (block.name != ""): + # self.IndentWrite(B"Morph (index = ", 0, structFlag) + # self.WriteInt(morphCount) + + # if ((relative) and (morphCount != baseIndex)): + # self.Write(B", base = ") + # self.WriteInt(baseIndex) + + # self.Write(B")\n") + # self.IndentWrite(B"{\n") + # self.IndentWrite(B"Name {string {\"", 1) + # self.Write(bytes(block.name, "UTF-8")) + # self.Write(B"\"}}\n") + # self.IndentWrite(B"}\n") + structFlag = True + + morphCount += 1 + + shapeKeys.key_blocks[0].value = 1.0 + mesh.update() + + om = Object() + om.primitive = "triangles" + + armature = node.find_armature() + applyModifiers = (not armature) + + # Apply all modifiers to create a new mesh with tessfaces. + + # We don't apply modifiers for a skinned mesh because we need the vertex positions + # before they are deformed by the armature modifier in order to export the proper + # bind pose. This does mean that modifiers preceding the armature modifier are ignored, + # but the Blender API does not provide a reasonable way to retrieve the mesh at an + # arbitrary stage in the modifier stack. + + exportMesh = node.to_mesh(scene, applyModifiers, "RENDER", True, False) + + # Triangulate mesh and remap vertices to eliminate duplicates. + + materialTable = [] + exportVertexArray = ZBlendExporter.DeindexMesh(exportMesh, materialTable) + triangleCount = len(materialTable) + + indexTable = [] + unifiedVertexArray = ZBlendExporter.UnifyVertices(exportVertexArray, indexTable) + vertexCount = len(unifiedVertexArray) + + # Write the position array. + + om.vertex_arrays = [] + + pa = Object() + pa.attrib = "position" + pa.size = 3 + pa.values = self.WriteVertexArray3D(unifiedVertexArray, "position") + #self.WriteInt(vertexCount) + om.vertex_arrays.append(pa) + + # Write the normal array. + + na = Object() + na.attrib = "normal" + na.size = 3 + na.values = self.WriteVertexArray3D(unifiedVertexArray, "normal") + om.vertex_arrays.append(na) + + # Write the color array if it exists. + + colorCount = len(exportMesh.tessface_vertex_colors) + if (colorCount > 0): + ca = Object() + ca.attrib = "color" + ca.size = 3 + ca.values = self.WriteVertexArray3D(unifiedVertexArray, "color") + om.vertex_arrays.append(ca) + + # Write the texcoord arrays. + + texcoordCount = len(exportMesh.tessface_uv_textures) + if (texcoordCount > 0): + ta = Object() + ta.attrib = "texcoord" + ta.size = 2 + ta.values = self.WriteVertexArray2D(unifiedVertexArray, "texcoord0") + om.vertex_arrays.append(ta) + + if (texcoordCount > 1): + ta2 = Object() + ta2.attrib = "texcoord[1]" + ta2.size = 2 + ta2.values = self.WriteVertexArray2D(unifiedVertexArray, "texcoord1") + om.vertex_arrays.append(ta2) + + # If there are multiple morph targets, export them here. + ''' + if (shapeKeys): + shapeKeys.key_blocks[0].value = 0.0 + for m in range(1, len(currentMorphValue)): + shapeKeys.key_blocks[m].value = 1.0 + mesh.update() + + node.active_shape_key_index = m + morphMesh = node.to_mesh(scene, applyModifiers, "RENDER", True, False) + + # Write the morph target position array. + + self.IndentWrite(B"VertexArray (attrib = \"position\", morph = ", 0, True) + self.WriteInt(m) + self.Write(B")\n") + self.IndentWrite(B"{\n") + self.indentLevel += 1 + + self.IndentWrite(B"float[3]\t\t// ") + self.WriteInt(vertexCount) + self.IndentWrite(B"{\n", 0, True) + self.WriteMorphPositionArray3D(unifiedVertexArray, morphMesh.vertices) + self.IndentWrite(B"}\n") + + self.indentLevel -= 1 + self.IndentWrite(B"}\n\n") + + # Write the morph target normal array. + + self.IndentWrite(B"VertexArray (attrib = \"normal\", morph = ") + self.WriteInt(m) + self.Write(B")\n") + self.IndentWrite(B"{\n") + self.indentLevel += 1 + + self.IndentWrite(B"float[3]\t\t// ") + self.WriteInt(vertexCount) + self.IndentWrite(B"{\n", 0, True) + self.WriteMorphNormalArray3D(unifiedVertexArray, morphMesh.vertices, morphMesh.tessfaces) + self.IndentWrite(B"}\n") + + self.indentLevel -= 1 + self.IndentWrite(B"}\n") + + bpy.data.meshes.remove(morphMesh) + ''' + # Write the index arrays. + + om.index_arrays = [] + + maxMaterialIndex = 0 + for i in range(len(materialTable)): + index = materialTable[i] + if (index > maxMaterialIndex): + maxMaterialIndex = index + + if (maxMaterialIndex == 0): + + # There is only one material, so write a single index array. + + ia = Object() + ia.size = 3 + ia.values = self.WriteTriangleArray(triangleCount, indexTable) + ia.material = self.WriteInt(0) + om.index_arrays.append(ia) + + else: + + # If there are multiple material indexes, then write a separate index array for each one. + + materialTriangleCount = [0 for i in range(maxMaterialIndex + 1)] + for i in range(len(materialTable)): + materialTriangleCount[materialTable[i]] += 1 + + for m in range(maxMaterialIndex + 1): + if (materialTriangleCount[m] != 0): + materialIndexTable = [] + for i in range(len(materialTable)): + if (materialTable[i] == m): + k = i * 3 + materialIndexTable.append(indexTable[k]) + materialIndexTable.append(indexTable[k + 1]) + materialIndexTable.append(indexTable[k + 2]) + + ia = Object() + ia.size = 3 + ia.values = self.WriteTriangleArray(materialTriangleCount[m], materialIndexTable) + ia.material = self.WriteInt(m) + om.index_arrays.append(ia) + + # If the mesh is skinned, export the skinning data here. + + if (armature): + self.ExportSkin(node, armature, unifiedVertexArray, om) + + # Restore the morph state. + + if (shapeKeys): + node.active_shape_key_index = activeShapeKeyIndex + node.show_only_shape_key = showOnlyShapeKey + + for m in range(len(currentMorphValue)): + shapeKeys.key_blocks[m].value = currentMorphValue[m] + + mesh.update() + + # Delete the new mesh that we made earlier. + + bpy.data.meshes.remove(exportMesh) + + o.mesh = om + self.output.geometry_objects.append(o) + + + def ExportLight(self, objectRef): + + # This function exports a single light object. + + o = Object() + o.id = objectRef[1]["structName"] + + object = objectRef[0] + type = object.type + + pointFlag = False + spotFlag = False + + if (type == "SUN"): + o.type = "infinite" + elif (type == "POINT"): + o.type = "point" + pointFlag = True + else: + o.type = "spot" + pointFlag = True + spotFlag = True + + #if (not object.use_shadow): + # self.Write(B", shadow = false") + + #self.WriteNodeTable(objectRef) + + # Export the light's color, and include a separate intensity if necessary. + + lc = Object() + lc.attrib = "light" + lc.size = 3 + lc.values = self.WriteColor(object.color) + o.color = lc + + ''' + intensity = object.energy + if (intensity != 1.0): + self.IndentWrite(B"Param (attrib = \"intensity\") {float {") + self.WriteFloat(intensity) + self.Write(B"}}\n") + + if (pointFlag): + + # Export a separate attenuation function for each type that's in use. + + falloff = object.falloff_type + + if (falloff == "INVERSE_LINEAR"): + self.IndentWrite(B"Atten (curve = \"inverse\")\n", 0, True) + self.IndentWrite(B"{\n") + + self.IndentWrite(B"Param (attrib = \"scale\") {float {", 1) + self.WriteFloat(object.distance) + self.Write(B"}}\n") + + self.IndentWrite(B"}\n") + + elif (falloff == "INVERSE_SQUARE"): + self.IndentWrite(B"Atten (curve = \"inverse_square\")\n", 0, True) + self.IndentWrite(B"{\n") + + self.IndentWrite(B"Param (attrib = \"scale\") {float {", 1) + self.WriteFloat(math.sqrt(object.distance)) + self.Write(B"}}\n") + + self.IndentWrite(B"}\n") + + elif (falloff == "LINEAR_QUADRATIC_WEIGHTED"): + if (object.linear_attenuation != 0.0): + self.IndentWrite(B"Atten (curve = \"inverse\")\n", 0, True) + self.IndentWrite(B"{\n") + + self.IndentWrite(B"Param (attrib = \"scale\") {float {", 1) + self.WriteFloat(object.distance) + self.Write(B"}}\n") + + self.IndentWrite(B"Param (attrib = \"constant\") {float {", 1) + self.WriteFloat(1.0) + self.Write(B"}}\n") + + self.IndentWrite(B"Param (attrib = \"linear\") {float {", 1) + self.WriteFloat(object.linear_attenuation) + self.Write(B"}}\n") + + self.IndentWrite(B"}\n\n") + + if (object.quadratic_attenuation != 0.0): + self.IndentWrite(B"Atten (curve = \"inverse_square\")\n") + self.IndentWrite(B"{\n") + + self.IndentWrite(B"Param (attrib = \"scale\") {float {", 1) + self.WriteFloat(object.distance) + self.Write(B"}}\n") + + self.IndentWrite(B"Param (attrib = \"constant\") {float {", 1) + self.WriteFloat(1.0) + self.Write(B"}}\n") + + self.IndentWrite(B"Param (attrib = \"quadratic\") {float {", 1) + self.WriteFloat(object.quadratic_attenuation) + self.Write(B"}}\n") + + self.IndentWrite(B"}\n") + + if (object.use_sphere): + self.IndentWrite(B"Atten (curve = \"linear\")\n", 0, True) + self.IndentWrite(B"{\n") + + self.IndentWrite(B"Param (attrib = \"end\") {float {", 1) + self.WriteFloat(object.distance) + self.Write(B"}}\n") + + self.IndentWrite(B"}\n") + + if (spotFlag): + + # Export additional angular attenuation for spot lights. + + self.IndentWrite(B"Atten (kind = \"angle\", curve = \"linear\")\n", 0, True) + self.IndentWrite(B"{\n") + + endAngle = object.spot_size * 0.5 + beginAngle = endAngle * (1.0 - object.spot_blend) + + self.IndentWrite(B"Param (attrib = \"begin\") {float {", 1) + self.WriteFloat(beginAngle) + self.Write(B"}}\n") + + self.IndentWrite(B"Param (attrib = \"end\") {float {", 1) + self.WriteFloat(endAngle) + self.Write(B"}}\n") + + self.IndentWrite(B"}\n") + + ''' + self.output.light_objects.append(o) + + def ExportCamera(self, objectRef): + + # This function exports a single camera object. + + o = Object() + o.id = objectRef[1]["structName"] + + #self.WriteNodeTable(objectRef) + + object = objectRef[0] + + o.params = [] + + p1 = Object() + p1.attrib = "fov" + p1.value = self.WriteFloat(object.angle_x) + o.params.append(p1) + + p2 = Object() + p2.attrib = "near" + p2.value = self.WriteFloat(object.clip_start) + o.params.append(p2) + + p3 = Object() + p3.attrib = "far" + p3.value = self.WriteFloat(object.clip_end) + o.params.append(p3) + + self.output.camera_objects.append(o) + + + def ExportMaterials(self): + + # This function exports all of the materials used in the scene. + + for materialRef in self.materialArray.items(): + o = Object() + material = materialRef[0] + + # If the material is unlinked, material becomes None. + if material == None: + continue + + o.id = materialRef[1]["structName"] + + if (material.name != ""): + o.name = material.name + + intensity = material.diffuse_intensity + diffuse = [material.diffuse_color[0] * intensity, material.diffuse_color[1] * intensity, material.diffuse_color[2] * intensity] + + o.colors = [] + + dc = Object() + dc.attrib = "diffuse" + dc.size = 3 + dc.values = self.WriteColor(diffuse) + o.colors.append(dc) + + intensity = material.specular_intensity + specular = [material.specular_color[0] * intensity, material.specular_color[1] * intensity, material.specular_color[2] * intensity] + + ''' + if ((specular[0] > 0.0) or (specular[1] > 0.0) or (specular[2] > 0.0)): + self.IndentWrite(B"Color (attrib = \"specular\") {float[3] {") + self.WriteColor(specular) + self.Write(B"}}\n") + + self.IndentWrite(B"Param (attrib = \"specular_power\") {float {") + self.WriteFloat(material.specular_hardness) + self.Write(B"}}\n") + + emission = material.emit + if (emission > 0.0): + self.IndentWrite(B"Color (attrib = \"emission\") {float[3] {") + self.WriteColor([emission, emission, emission]) + self.Write(B"}}\n") + + diffuseTexture = None + specularTexture = None + emissionTexture = None + transparencyTexture = None + normalTexture = None + + for textureSlot in material.texture_slots: + if ((textureSlot) and (textureSlot.use) and (textureSlot.texture.type == "IMAGE")): + if (((textureSlot.use_map_color_diffuse) or (textureSlot.use_map_diffuse)) and (not diffuseTexture)): + diffuseTexture = textureSlot + elif (((textureSlot.use_map_color_spec) or (textureSlot.use_map_specular)) and (not specularTexture)): + specularTexture = textureSlot + elif ((textureSlot.use_map_emit) and (not emissionTexture)): + emissionTexture = textureSlot + elif ((textureSlot.use_map_translucency) and (not transparencyTexture)): + transparencyTexture = textureSlot + elif ((textureSlot.use_map_normal) and (not normalTexture)): + normalTexture = textureSlot + + if (diffuseTexture): + self.ExportTexture(diffuseTexture, B"diffuse") + if (specularTexture): + self.ExportTexture(specularTexture, B"specular") + if (emissionTexture): + self.ExportTexture(emissionTexture, B"emission") + if (transparencyTexture): + self.ExportTexture(transparencyTexture, B"transparency") + if (normalTexture): + self.ExportTexture(normalTexture, B"normal") + ''' + self.output.materials.append(o) + + + def ExportObjects(self, scene): + for objectRef in self.geometryArray.items(): + self.ExportGeometry(objectRef, scene) + for objectRef in self.lightArray.items(): + self.ExportLight(objectRef) + for objectRef in self.cameraArray.items(): + self.ExportCamera(objectRef) + + + def execute(self, context): + self.output = Object() + + scene = context.scene + + originalFrame = scene.frame_current + originalSubframe = scene.frame_subframe + self.restoreFrame = False + + self.beginFrame = scene.frame_start + self.endFrame = scene.frame_end + self.frameTime = 1.0 / (scene.render.fps_base * scene.render.fps) + + self.nodeArray = {} + self.geometryArray = {} + self.lightArray = {} + self.cameraArray = {} + self.materialArray = {} + self.boneParentArray = {} + + self.exportAllFlag = not self.option_export_selection + self.sampleAnimationFlag = self.option_sample_animation + + + for object in scene.objects: + if (not object.parent): + self.ProcessNode(object) + + self.ProcessSkinnedMeshes() + + + self.output.nodes = [] + for object in scene.objects: + if (not object.parent): + self.ExportNode(object, scene) + + self.output.geometry_objects = []; + self.output.light_objects = []; + self.output.camera_objects = []; + self.ExportObjects(scene) + + self.output.materials = [] + self.ExportMaterials() + + + if (self.restoreFrame): + scene.frame_set(originalFrame, originalSubframe) + + # Output + with open(self.filepath, 'w') as f: + f.write(self.output.to_JSON()) + + return {'FINISHED'} + + + +def menu_func(self, context): + self.layout.operator(ZBlendExporter.bl_idname, text = "ZBlend (.json)") + +def register(): + bpy.utils.register_class(ZBlendExporter) + bpy.types.INFO_MT_file_export.append(menu_func) + +def unregister(): + bpy.types.INFO_MT_file_export.remove(menu_func) + bpy.utils.unregister_class(ZBlendExporter) + +if __name__ == "__main__": + register() diff --git a/blender/server.py b/blender/server.py new file mode 100755 index 00000000..0605e585 --- /dev/null +++ b/blender/server.py @@ -0,0 +1,14 @@ +import http.server +import socketserver + +# Can't use multiprocessing on Windows +#p = Process(target=run_server) +#p.start() +#atexit.register(p.terminate) + +def run_server(): + Handler = http.server.SimpleHTTPRequestHandler + httpd = socketserver.TCPServer(("", 8080), Handler) + httpd.serve_forever() + +run_server() diff --git a/blender/traits.py b/blender/traits.py new file mode 100755 index 00000000..d6528622 --- /dev/null +++ b/blender/traits.py @@ -0,0 +1,396 @@ +import shutil +import bpy +import os +import json +from zblend_traits_animation import ListAnimationTraitItem +from bpy.types import Menu, Panel, UIList +from bpy.props import * + +class ListTraitItem(bpy.types.PropertyGroup): + # Group of properties representing an item in the list + name = bpy.props.StringProperty( + name="Name", + description="A name for this item", + default="Untitled") + + enabled_prop = bpy.props.BoolProperty( + name="", + description="A name for this item", + default=True) + + type_prop = bpy.props.EnumProperty( + items = [('Script', 'Script', 'Script'), + ('Nodes', 'Nodes', 'Nodes'), + ('Scene Instance', 'Scene Instance', 'Scene Instance'), + ('Animation', 'Animation', 'Animation'), + ('Camera', 'Camera', 'Camera'), + ('Light', 'Light', 'Light'), + ('Rigid Body', 'Rigid Body', 'Rigid Body'), + + ('Mesh Renderer', 'Mesh Renderer', 'Mesh Renderer'), + ('Billboard Renderer', 'Billboard Renderer', 'Billboard Renderer'), + ('Particles Renderer', 'Particles Renderer', 'Particles Renderer'), + ('Skydome Renderer', 'Skydome Renderer', 'Skydome Renderer'), + ('Custom Renderer', 'Custom Renderer', 'Custom Renderer') + ], + name = "Type") + + default_material_prop = bpy.props.BoolProperty( + name="Default Material", + description="A name for this item", + default=True) + + material_prop = bpy.props.StringProperty( + name="Material", + description="A name for this item", + default="") + + lighting_prop = bpy.props.BoolProperty( + name="Lighting", + default=True) + + cast_shadow_prop = bpy.props.BoolProperty( + name="Cast Shadow", + default=True) + + receive_shadow_prop = bpy.props.BoolProperty( + name="Receive Shadow", + default=True) + + shader_prop = bpy.props.StringProperty( + name="Shader", + description="A name for this item", + default="") + + data_prop = bpy.props.StringProperty( + name="Data", + description="A name for this item", + default="") + + camera_link_prop = bpy.props.StringProperty( + name="Camera", + description="A name for this item", + default="Camera") + + light_link_prop = bpy.props.StringProperty( + name="Light", + description="A name for this item", + default="Lamp") + + default_body_prop = bpy.props.BoolProperty( + name="Default Body", + description="A name for this item", + default=True) + + body_link_prop = bpy.props.StringProperty( + name="Body", + description="A name for this item", + default="") + + custom_shape_prop = bpy.props.BoolProperty( + name="Custom Shape", + description="A name for this item", + default=False) + + custom_shape_type_prop = bpy.props.EnumProperty( + items = [('Terrain', 'Terrain', 'Terrain'), + ('Static Mesh', 'Static Mesh', 'Static Mesh') + ], + name = "Shape") + + shape_size_scale_prop = bpy.props.FloatVectorProperty( + name = "Size Scale", + default=[1,1,1], + size=3, min=0, max=10) + + scene_prop = bpy.props.StringProperty( + name="Scene", + description="A name for this item", + default="") + + class_name_prop = bpy.props.StringProperty( + name="Class", + description="A name for this item", + default="Untitled") + + nodes_name_prop = bpy.props.StringProperty( + name="Nodes", + description="A name for this item", + default="") + + start_track_name_prop = bpy.props.StringProperty( + name="Start Track", + description="A name for this item", + default="") + +bpy.utils.register_class(ListTraitItem) + + +class MY_UL_TraitList(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + # We could write some code to decide which icon to use here... + custom_icon = 'OBJECT_DATAMODE' + + # Make sure your code supports all 3 layout types + if self.layout_type in {'DEFAULT', 'COMPACT'}: + layout.prop(item, "enabled_prop") + layout.label(item.name, icon = custom_icon) + + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label("", icon = custom_icon) +bpy.utils.register_class(MY_UL_TraitList) + + +def initObjectProperties(): + bpy.types.Object.my_traitlist = bpy.props.CollectionProperty(type = ListTraitItem) + bpy.types.Object.traitlist_index = bpy.props.IntProperty(name = "Index for my_list", default = 0) +initObjectProperties() + + +class LIST_OT_TraitNewItem(bpy.types.Operator): + # Add a new item to the list + bl_idname = "my_traitlist.new_item" + bl_label = "Add a new item" + + def execute(self, context): + bpy.context.object.my_traitlist.add() + bpy.context.object.traitlist_index += 1 + return{'FINISHED'} + + +class LIST_OT_TraitDeleteItem(bpy.types.Operator): + # Delete the selected item from the list + bl_idname = "my_traitlist.delete_item" + bl_label = "Deletes an item" + + @classmethod + def poll(self, context): + """ Enable if there's something in the list """ + return len(bpy.context.object.my_traitlist) > 0 + + def execute(self, context): + list = bpy.context.object.my_traitlist + index = bpy.context.object.traitlist_index + + list.remove(index) + + if index > 0: + index = index - 1 + + bpy.context.object.traitlist_index = index + return{'FINISHED'} + + +class LIST_OT_TraitMoveItem(bpy.types.Operator): + # Move an item in the list + bl_idname = "my_traitlist.move_item" + bl_label = "Move an item in the list" + direction = bpy.props.EnumProperty( + items=( + ('UP', 'Up', ""), + ('DOWN', 'Down', ""),)) + + @classmethod + def poll(self, context): + """ Enable if there's something in the list. """ + return len(bpy.context.object.my_traitlist) > 0 + + + def move_index(self): + # Move index of an item render queue while clamping it + index = bpy.context.object.traitlist_index + list_length = len(bpy.context.object.my_traitlist) - 1 + new_index = 0 + + if self.direction == 'UP': + new_index = index - 1 + elif self.direction == 'DOWN': + new_index = index + 1 + + new_index = max(0, min(new_index, list_length)) + index = new_index + + + def execute(self, context): + list = bpy.context.object.my_traitlist + index = bpy.context.object.traitlist_index + + if self.direction == 'DOWN': + neighbor = index + 1 + #queue.move(index,neighbor) + self.move_index() + + elif self.direction == 'UP': + neighbor = index - 1 + #queue.move(neighbor, index) + self.move_index() + else: + return{'CANCELLED'} + return{'FINISHED'} + + +# Menu in tools region +class ToolsTraitsPanel(bpy.types.Panel): + bl_label = "zblend_traits" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + + def draw(self, context): + layout = self.layout + + obj = bpy.context.object + + rows = 2 + if len(obj.my_traitlist) > 1: + rows = 4 + + row = layout.row() + row.template_list("MY_UL_TraitList", "The_List", obj, "my_traitlist", obj, "traitlist_index", rows=rows) + + col = row.column(align=True) + col.operator("my_traitlist.new_item", icon='ZOOMIN', text="") + col.operator("my_traitlist.delete_item", icon='ZOOMOUT', text="")#.all = False + + if len(obj.my_traitlist) > 1: + col.separator() + col.operator("my_traitlist.move_item", icon='TRIA_UP', text="").direction = 'UP' + col.operator("my_traitlist.move_item", icon='TRIA_DOWN', text="").direction = 'DOWN' + + if obj.traitlist_index >= 0 and len(obj.my_traitlist) > 0: + item = obj.my_traitlist[obj.traitlist_index] + # Default props + row = layout.row() + row.prop(item, "type_prop") + + # Script + if item.type_prop =='Script': + item.name = item.class_name_prop + row = layout.row() + row.prop(item, "class_name_prop") + # Nodes + elif item.type_prop =='Nodes': + item.name = item.nodes_name_prop + row = layout.row() + row.prop_search(item, "nodes_name_prop", bpy.data, "node_groups", "Tree") + # Camera + elif item.type_prop == 'Camera': + item.name = item.type_prop + row = layout.row() + row.prop_search(item, "camera_link_prop", bpy.data, "cameras", "Camera") + + # Light + elif item.type_prop == 'Light': + item.name = item.type_prop + row = layout.row() + row.prop_search(item, "light_link_prop", bpy.data, "lamps", "Light") + + # Rigid body + elif item.type_prop == 'Rigid Body': + item.name = item.type_prop + row = layout.row() + row.prop(item, "default_body_prop") + if item.default_body_prop == False: + row.prop_search(item, "body_link_prop", bpy.context.scene.rigidbody_world.group, "objects", "") + row = layout.row() + row.prop(item, "custom_shape_prop") + if item.custom_shape_prop == True: + row.prop(item, "custom_shape_type_prop") + row = layout.row() + row.prop(item, "shape_size_scale_prop") + + # Scene instance + elif item.type_prop == 'Scene Instance': + item.name = item.type_prop + row = layout.row() + row.prop_search(item, "scene_prop", bpy.data, "scenes", "Scene") + #row.prop(item, "scene_prop") + + # Animation + elif item.type_prop == 'Animation': + item.name = item.type_prop + row = layout.row() + row.prop_search(item, "start_track_name_prop", obj, "my_animationtraitlist", "Start Track") + # List + animrow = layout.row() + animrows = 2 + if len(obj.my_animationtraitlist) > 1: + animrows = 4 + + row = layout.row() + row.template_list("MY_UL_AnimationTraitList", "The_List", obj, "my_animationtraitlist", obj, "animationtraitlist_index", rows=animrows) + + col = row.column(align=True) + col.operator("my_animationtraitlist.new_item", icon='ZOOMIN', text="") + col.operator("my_animationtraitlist.delete_item", icon='ZOOMOUT', text="") + + if len(obj.my_animationtraitlist) > 1: + col.separator() + col.operator("my_animationtraitlist.move_item", icon='TRIA_UP', text="").direction = 'UP' + col.operator("my_animationtraitlist.move_item", icon='TRIA_DOWN', text="").direction = 'DOWN' + + if obj.animationtraitlist_index >= 0 and len(obj.my_animationtraitlist) > 0: + item = obj.my_animationtraitlist[obj.animationtraitlist_index] + + row = layout.row() + row.prop(item, "start_prop") + row.prop(item, "end_prop") + # Animation end + # Mesh renderer + elif item.type_prop == 'Mesh Renderer': + item.name = item.type_prop + row = layout.row() + row.prop(item, "default_material_prop") + if item.default_material_prop == False: + row.prop_search(item, "material_prop", bpy.data, "materials", "") + row = layout.row() + row.prop(item, "lighting_prop") + row = layout.row() + row.prop(item, "cast_shadow_prop") + row = layout.row() + row.prop(item, "receive_shadow_prop") + + # Custom renderer + elif item.type_prop == 'Custom Renderer': + item.name = item.class_name_prop + row = layout.row() + row.prop(item, "class_name_prop") + + row = layout.row() + row.prop(item, "default_material_prop") + if item.default_material_prop == False: + row.prop_search(item, "material_prop", bpy.data, "materials", "") + row = layout.row() + row.prop(item, "shader_prop") + row = layout.row() + row.prop(item, "data_prop") + + # Billboard renderer + elif item.type_prop == 'Billboard Renderer': + item.name = item.type_prop + row = layout.row() + row.prop(item, "default_material_prop") + if item.default_material_prop == False: + row.prop_search(item, "material_prop", bpy.data, "materials", "") + + # Particles renderer + elif item.type_prop == 'Particles Renderer': + item.name = item.type_prop + row = layout.row() + row.prop(item, "default_material_prop") + if item.default_material_prop == False: + row.prop_search(item, "material_prop", bpy.data, "materials", "") + row = layout.row() + row.prop(item, "data_prop") + + # Skydome renderer + elif item.type_prop == 'Skydome Renderer': + item.name = item.type_prop + row = layout.row() + row.prop(item, "default_material_prop") + if item.default_material_prop == False: + row.prop_search(item, "material_prop", bpy.data, "materials", "") + +# Registration +bpy.utils.register_module(__name__) diff --git a/blender/traits_animation.py b/blender/traits_animation.py new file mode 100755 index 00000000..6c62cd01 --- /dev/null +++ b/blender/traits_animation.py @@ -0,0 +1,136 @@ +import shutil +import bpy +import os +import json +from bpy.types import Menu, Panel, UIList +from bpy.props import * + +class ListAnimationTraitItem(bpy.types.PropertyGroup): + # Group of properties representing an item in the list + name = bpy.props.StringProperty( + name="Name", + description="A name for this item", + default="Untitled") + + enabled_prop = bpy.props.BoolProperty( + name="", + description="A name for this item", + default=True) + + start_prop = bpy.props.IntProperty( + name="Start", + description="A name for this item", + default=0) + + end_prop = bpy.props.IntProperty( + name="End", + description="A name for this item", + default=0) + +bpy.utils.register_class(ListAnimationTraitItem) + + +class MY_UL_AnimationTraitList(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + # We could write some code to decide which icon to use here... + custom_icon = 'OBJECT_DATAMODE' + + # Make sure your code supports all 3 layout types + if self.layout_type in {'DEFAULT', 'COMPACT'}: + layout.prop(item, "enabled_prop") + #layout.label(item.name, icon = custom_icon) + layout.prop(item, "name", text="", emboss=False, icon=custom_icon) + + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label("", icon = custom_icon) +bpy.utils.register_class(MY_UL_AnimationTraitList) + +def initObjectProperties(): + bpy.types.Object.my_animationtraitlist = bpy.props.CollectionProperty(type = ListAnimationTraitItem) + bpy.types.Object.animationtraitlist_index = bpy.props.IntProperty(name = "Index for my_list", default = 0) +initObjectProperties() + +class LIST_OT_AnimationTraitNewItem(bpy.types.Operator): + # Add a new item to the list + bl_idname = "my_animationtraitlist.new_item" + bl_label = "Add a new item" + + def execute(self, context): + bpy.context.object.my_animationtraitlist.add() + bpy.context.object.animationtraitlist_index += 1 + return{'FINISHED'} + + +class LIST_OT_AnimationTraitDeleteItem(bpy.types.Operator): + # Delete the selected item from the list + bl_idname = "my_animationtraitlist.delete_item" + bl_label = "Deletes an item" + + @classmethod + def poll(self, context): + """ Enable if there's something in the list """ + return len(bpy.context.object.my_animationtraitlist) > 0 + + def execute(self, context): + list = bpy.context.object.my_animationtraitlist + index = bpy.context.object.animationtraitlist_index + + list.remove(index) + + if index > 0: + index = index - 1 + + bpy.context.object.animationtraitlist_index = index + return{'FINISHED'} + + +class LIST_OT_AnimationTraitMoveItem(bpy.types.Operator): + # Move an item in the list + bl_idname = "my_animationtraitlist.move_item" + bl_label = "Move an item in the list" + direction = bpy.props.EnumProperty( + items=( + ('UP', 'Up', ""), + ('DOWN', 'Down', ""),)) + + @classmethod + def poll(self, context): + """ Enable if there's something in the list. """ + return len(bpy.context.object.my_animationtraitlist) > 0 + + + def move_index(self): + # Move index of an item render queue while clamping it + index = bpy.context.object.animationtraitlist_index + list_length = len(bpy.context.object.my_animationtraitlist) - 1 + new_index = 0 + + if self.direction == 'UP': + new_index = index - 1 + elif self.direction == 'DOWN': + new_index = index + 1 + + new_index = max(0, min(new_index, list_length)) + index = new_index + + + def execute(self, context): + list = bpy.context.object.my_animationtraitlist + index = bpy.context.object.animationtraitlist_index + + if self.direction == 'DOWN': + neighbor = index + 1 + #queue.move(index,neighbor) + self.move_index() + + elif self.direction == 'UP': + neighbor = index - 1 + #queue.move(neighbor, index) + self.move_index() + else: + return{'CANCELLED'} + return{'FINISHED'} + +# Registration +bpy.utils.register_module(__name__)