Browse Source

Major refactor to make it easier to develop. Removed lots of commented out and broken code. Added physics export

Squashed commit of the following:

commit d3fb838a92177db440d6e2b9b76bde9c599c5ba1
Author: Geoffrey <[email protected]>
Date:   Mon Mar 12 16:05:58 2018 +0100

    Fixed UV Offset

commit e0667c3f43541b83fd647eb71875564bc8a780c9
Author: Geoffrey <[email protected]>
Date:   Mon Mar 12 13:47:18 2018 +0100

    Fix for incorrect transforms

commit 2cc4ad4b2a28457bc0f5c53225cbf553ea21256a
Author: Geoffrey <[email protected]>
Date:   Mon Mar 12 13:25:01 2018 +0100

    Added control over export from the addon UI

commit 94d56a4cae5d323b84480de02704defbe64496e5
Author: Geoffrey <[email protected]>
Date:   Mon Mar 12 12:16:21 2018 +0100

    Now exports triangle mesh collision shapes

commit b5f3b2a218cf09374291c6bc63ca5d9754b58428
Author: Geoffrey <[email protected]>
Date:   Mon Mar 12 10:56:09 2018 +0100

    Now has a material search, whereby it can look in the godot directory for materials with the same name as a blender material

commit 73ca97c511b75e302f67d8c201efcf4959efb1ad
Author: Geoffrey <[email protected]>
Date:   Mon Mar 12 09:33:35 2018 +0100

    Now supports exporting compound physics shapes

commit ea35efed0e9a4e6862bf5900998feb1ef7e4217b
Author: sdfgeoff <[email protected]>
Date:   Fri Mar 9 21:23:47 2018 +0100

    Pylint....

commit 1aee66b568372527d608b311e621493ac9f52223
Author: sdfgeoff <[email protected]>
Date:   Fri Mar 9 20:30:20 2018 +0100

    Changed architecture slightly so that each exporter now controls what it's children are parented to

commit 292256b9c5d0aed91f999b4ddce6ca37f0349930
Author: sdfgeoff <[email protected]>
Date:   Fri Mar 9 20:17:34 2018 +0100

    Fixed issue with orthographic camera scale

commit 06c4acd5c425d70f153956d0c8a519c7f89f9d76
Author: Geoffrey <[email protected]>
Date:   Fri Mar 9 17:35:59 2018 +0100

    Some more restructuring, and started on material export

commit c2f13d219057d0e18358cb3ea107c74435cbb469
Author: Geoffrey <[email protected]>
Date:   Thu Mar 8 16:30:53 2018 +0100

    Now exports some of the physics primitives

commit 198216410cc51c30d46ba7c1c73c25ea11e116a7
Author: Geoffrey <[email protected]>
Date:   Thu Mar 8 15:12:47 2018 +0100

    Major refactoring. As part of this, the commented out and incomplete exporters (bezier, armature) were removed

commit e1968f42bcad70ca988c60d47c1b31429eec18e2
Author: Geoffrey <[email protected]>
Date:   Thu Mar 8 11:01:23 2018 +0100

    Fixed issues with UV maps, and vertex colors causing failed imports. Tidied mesh resource generation

commit a5912e19180c023067fc8be3a5368ad2a8586041
Author: Geoffrey <[email protected]>
Date:   Wed Mar 7 21:31:20 2018 +0100

    Added test setup. You can now run "make test-blends" and it will export all of the blend files in tests/scenes. Currently comparison has to be done manually.

commit e931c0f5f82837b48f3e828bc0c05683110d4e30
Author: Geoffrey <[email protected]>
Date:   Wed Mar 7 21:30:45 2018 +0100

    Split off the conversion from blender/python objects into strings to another file. More pylint

commit fc8d97b489a45d1b5df2d37cd8e2d21f57fa40b3
Author: Geoffrey <[email protected]>
Date:   Wed Mar 7 20:16:50 2018 +0100

    Pep8 and pylint improvements - still a long way to go

commit e156da77c2e4bb5745a545ccbc397249b59772d8
Author: Geoffrey <[email protected]>
Date:   Wed Mar 7 18:00:00 2018 +0100

    Added in a "Node Template" class to help with exporting nodes without doing hundreds of writes
Geoffrey 7 years ago
parent
commit
40197867ed

+ 8 - 0
.gitignore

@@ -0,0 +1,8 @@
+*.pyc
+__pycache__
+
+*.blend[0-9]
+*.escn
+
+.import 
+*.import

+ 377 - 0
.pylintrc

@@ -0,0 +1,377 @@
+[MASTER]
+
+# Specify a configuration file.
+#rcfile=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+# Use multiple processes to speed up Pylint.
+jobs=1
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code
+extension-pkg-whitelist=
+
+# Allow optimization of some AST trees. This will activate a peephole AST
+# optimizer, which will apply various small optimizations. For instance, it can
+# be used to obtain the result of joining multiple strings with the addition
+# operator. Joining a lot of strings can lead to a maximum recursion error in
+# Pylint and this flag can prevent that. It has one side effect, the resulting
+# AST will be different than the one from reality.
+optimize-ast=no
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
+confidence=
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time. See also the "--disable" option for examples.
+#enable=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once).You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use"--disable=all --enable=classes
+# --disable=W"
+disable=attribute-defined-outside-init, import-error, unused-argument, too-few-public-methods
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html. You can also give a reporter class, eg
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]".
+files-output=no
+
+# Tells whether to display a full report or only the messages
+reports=yes
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details
+#msg-template=
+
+
+[SPELLING]
+
+# Spelling dictionary name. Available dictionaries: none. To make it working
+# install python-enchant package.
+spelling-dict=
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to indicated private dictionary in
+# --spelling-private-dict-file option instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[LOGGING]
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format
+logging-modules=logging
+
+
+[VARIABLES]
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching the name of dummy variables (i.e. expectedly
+# not used).
+dummy-variables-rgx=_$|dummy
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,_cb
+
+
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=100
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )?<?https?://\S+>?$
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+# List of optional constructs for which whitespace checking is disabled. `dict-
+# separator` is used to allow tabulation in dicts, etc.: {1  : 1,\n222: 2}.
+# `trailing-comma` allows a space between comma and closing bracket: (a, ).
+# `empty-line` allows space-only lines.
+no-space-check=trailing-comma,dict-separator
+
+# Maximum number of lines in a module
+max-module-lines=1000
+
+# String used as indentation unit. This is usually "    " (4 spaces) or "\t" (1
+# tab).
+indent-string='    '
+
+# Number of spaces of indent required inside a hanging  or continued line.
+indent-after-paren=4
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,XXX,TODO
+
+
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+
+[TYPECHECK]
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis. It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=
+
+# List of classes names for which member attributes should not be checked
+# (useful for classes with attributes dynamically set). This supports can work
+# with qualified names.
+ignored-classes=
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+
+[BASIC]
+
+# List of builtins function names that should not be used, separated by a comma
+bad-functions=map,filter
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,j,k,ex,Run,_
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Include a hint for the correct naming format with invalid-name
+include-naming-hint=no
+
+# Regular expression matching correct function names
+function-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Naming hint for function names
+function-name-hint=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression matching correct class names
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+
+# Naming hint for class names
+class-name-hint=[A-Z_][a-zA-Z0-9]+$
+
+# Regular expression matching correct class attribute names
+class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
+
+# Naming hint for class attribute names
+class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
+
+# Regular expression matching correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Naming hint for module names
+module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression matching correct method names
+method-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Naming hint for method names
+method-name-hint=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression matching correct variable names
+variable-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Naming hint for variable names
+variable-name-hint=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression matching correct attribute names
+attr-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Naming hint for attribute names
+attr-name-hint=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression matching correct constant names
+const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Naming hint for constant names
+const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Regular expression matching correct inline iteration names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Naming hint for inline iteration names
+inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
+
+# Regular expression matching correct argument names
+argument-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Naming hint for argument names
+argument-name-hint=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=^_
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+
+[ELIF]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=mcs
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,_fields,_replace,_source,_make
+
+
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=optparse
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=5
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of branch for function / method body
+max-branches=12
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of boolean expressions in a if statement
+max-bool-expr=5
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=Exception

+ 16 - 0
Makefile

@@ -0,0 +1,16 @@
+PYLINT = pylint3
+PEP8 = pep8
+BLENDER = blender
+
+pylint:
+	$(PYLINT) io_scene_godot
+	$(PYLINT) io_scene_godot
+
+pep8:
+	$(PEP8) io_scene_godot
+
+
+.PHONY = test-blends
+test-blends:
+	rm -rf ./tests/.import  # Ensure we don't have any hangover data
+	$(BLENDER) -b --python ./tests/scenes/export_blends.py

+ 60 - 67
io_scene_godot/__init__.py

@@ -1,3 +1,7 @@
+"""
+Export to godot's escn file format - a format that Godot can work with
+without significant importing (it's the same as Godot's tscn format).
+"""
 # ##### BEGIN GPL LICENSE BLOCK #####
 #
 #  This program is free software; you can redistribute it and/or
@@ -19,7 +23,7 @@ import bpy
 from bpy.props import StringProperty, BoolProperty, FloatProperty, EnumProperty
 
 from bpy_extras.io_utils import ExportHelper
-bl_info = {
+bl_info = {  # pylint: disable=invalid-name
     "name": "Godot Engine Exporter",
     "author": "Juan Linietsky",
     "blender": (2, 5, 8),
@@ -30,12 +34,8 @@ bl_info = {
     "wiki_url": ("https://godotengine.org"),
     "tracker_url": "https://github.com/godotengine/blender-exporter",
     "support": "OFFICIAL",
-    "category": "Import-Export"}
-
-if "bpy" in locals():
-    import imp
-    if "export_godot" in locals():
-        imp.reload(export_godot)  # noqa
+    "category": "Import-Export"
+}
 
 
 class ExportGodot(bpy.types.Operator, ExportHelper):
@@ -52,15 +52,23 @@ class ExportGodot(bpy.types.Operator, ExportHelper):
     object_types = EnumProperty(
         name="Object Types",
         options={"ENUM_FLAG"},
-        items=(("EMPTY", "Empty", ""),
-               ("CAMERA", "Camera", ""),
-               ("LAMP", "Lamp", ""),
-               ("ARMATURE", "Armature", ""),
-               ("MESH", "Mesh", ""),
-               ("CURVE", "Curve", ""),
-               ),
-        default={"EMPTY", "CAMERA", "LAMP", "ARMATURE", "MESH", "CURVE"},
-        )
+        items=(
+            ("EMPTY", "Empty", ""),
+            ("CAMERA", "Camera", ""),
+            ("LAMP", "Lamp", ""),
+            # ("ARMATURE", "Armature", ""),
+            ("MESH", "Mesh", ""),
+            # ("CURVE", "Curve", ""),
+        ),
+        default={
+            "EMPTY",
+            "CAMERA",
+            "LAMP",
+            # "ARMATURE",
+            "MESH",
+            # "CURVE"
+        },
+    )
 
     use_export_selected = BoolProperty(
         name="Selected Objects",
@@ -71,92 +79,77 @@ class ExportGodot(bpy.types.Operator, ExportHelper):
     use_mesh_modifiers = BoolProperty(
         name="Apply Modifiers",
         description="Apply modifiers to mesh objects (on a copy!).",
-        default=False,
+        default=True,
         )
     use_active_layers = BoolProperty(
         name="Active Layers",
         description="Export only objects on the active layers.",
         default=True,
         )
-    use_exclude_ctrl_bones = BoolProperty(
-        name="Exclude Control Bones",
-        description="Exclude skeleton bones with names beginning with 'ctrl'.",
-        default=True,
-        )
-    use_anim = BoolProperty(
-        name="Export Animation",
-        description="Export keyframe animation",
-        default=False,
-        )
-    use_anim_action_all = BoolProperty(
-        name="All Actions",
-        description=("Export all actions for the first armature found "
-                     "in separate Godot files"),
-        default=False,
-        )
-    use_anim_skip_noexp = BoolProperty(
-        name="Skip (-noexp) Actions",
-        description="Skip exporting of actions whose name end in (-noexp)."
-                    " Useful to skip control animations.",
-        default=True,
-        )
-    use_anim_optimize = BoolProperty(
-        name="Optimize Keyframes",
-        description="Remove double keyframes",
-        default=True,
-        )
-
-    anim_optimize_precision = FloatProperty(
-        name="Precision",
-        description=("Tolerence for comparing double keyframes "
-                     "(higher for greater accuracy)"),
-        min=1, max=16,
-        soft_min=1, soft_max=16,
-        default=6.0,
-        )
-
-    use_metadata = BoolProperty(
-        name="Use Metadata",
-        default=True,
-        options={"HIDDEN"},
+    material_search_paths = EnumProperty(
+        name="Material Search Paths",
+        description="Search for existing godot materials with names that match"
+                    "the blender material names (ie the file <matname>.tres"
+                    "containing a material resource)",
+        default="PROJECT_DIR",
+        items=(
+            (
+                "NONE", "None",
+                "Don't search for materials"
+            ),
+            (
+                "EXPORT_DIR", "Export Directory",
+                "Search the folder where the escn is exported to"
+            ),
+            (
+                "PROJECT_DIR", "Project Directory",
+                "Search for materials in the godot project directory"
+            ),
         )
+    )
 
     @property
     def check_extension(self):
+        """Checks if the file extension is valid. It appears we don't
+        really care.... """
         return True
 
     def execute(self, context):
+        """Begin the export"""
         if not self.filepath:
             raise Exception("filepath not set")
 
-        print("esporting scene "+str(len(context.scene.objects)))
-   
-        keywords = self.as_keywords(ignore=("axis_forward",
-                                            "axis_up",
-                                            "global_scale",
-                                            "check_existing",
-                                            "filter_glob",
-                                            "xna_validate",
-                                            ))
+        keywords = self.as_keywords(ignore=(
+            "axis_forward",
+            "axis_up",
+            "global_scale",
+            "check_existing",
+            "filter_glob",
+            "xna_validate",
+        ))
 
         from . import export_godot
         return export_godot.save(self, context, **keywords)
 
 
 def menu_func(self, context):
+    """Add to the manu"""
     self.layout.operator(ExportGodot.bl_idname, text="Godot Engine (.escn)")
 
 
 def register():
+    """Add addon to blender"""
     bpy.utils.register_module(__name__)
 
     bpy.types.INFO_MT_file_export.append(menu_func)
 
 
 def unregister():
+    """Remove addon from blender"""
     bpy.utils.unregister_module(__name__)
 
     bpy.types.INFO_MT_file_export.remove(menu_func)
 
+
 if __name__ == "__main__":
     register()

+ 28 - 0
io_scene_godot/converters/__init__.py

@@ -0,0 +1,28 @@
+"""
+This file provides the conversion for a single blend object into one or
+more godot nodes. All the converters should take as input arguments:
+ - The ESCN file (so you can use add_internal_resource() method etc.)
+ - The exporter config (so you can see what options the user selected)
+ - The node to export
+ - The path to the parent node
+
+All converters that convert nodes should return the path to the node. All
+converters that convert resources should return the resource ID. Additional,
+converters for resources should have internal protection against importing
+twice
+
+One-function exporters are stored in simple_nodes. Others (such as meshes)
+are stored in individual files.
+"""
+
+from .simple_nodes import *  # pylint: disable=wildcard-import
+from .mesh import export_mesh_node
+from .physics import export_physics_properties
+
+
+BLENDER_TYPE_TO_EXPORTER = {
+    "MESH": export_mesh_node,
+    "CAMERA": export_camera_node,
+    "LAMP": export_lamp_node,
+    "EMPTY": export_empty_node
+}

+ 110 - 0
io_scene_godot/converters/material.py

@@ -0,0 +1,110 @@
+"""
+Exports materials. For now I'm targetting the blender internal, however this
+will be deprecated in Blender 2.8 in favor of EEVEE. EEVEE has PBR and
+should be able to match Godot better, but unfortunately parseing a node
+tree into a flat bunch of parameters is not trivial. So for someone else:"""
+# TODO: Add EEVEE support
+
+import logging
+import os
+import bpy
+from ..structures import InternalResource, ExternalResource
+
+
+def export_image(escn_file, export_settings, image):
+    """
+    Saves an image as an external reference relative to the blend location
+    """
+    image_id = escn_file.get_external_resource(image)
+    if image_id is not None:
+        return image_id
+
+    imgpath = image.filepath
+    if imgpath.startswith("//"):
+        imgpath = bpy.path.abspath(imgpath)
+
+    imgpath = os.path.relpath(
+        imgpath,
+        os.path.dirname(export_settings['path'])
+    ).replace("\\", "/")
+
+    # Add the image to the file
+    image_resource = ExternalResource(imgpath, "Image")
+    image_id = escn_file.add_external_resource(image_resource, image)
+
+    return image_id
+
+
+def export_material(escn_file, export_settings, material):
+    """ Exports a blender internal material as best it can"""
+
+    external_material = find_material(export_settings, material)
+    if external_material is not None:
+        resource_id = escn_file.get_external_resource(material)
+        if resource_id is None:
+            ext_mat = ExternalResource(
+                external_material[0],  # Path
+                external_material[1]  # Material Type
+            )
+            resource_id = escn_file.add_external_resource(ext_mat, material)
+        return "ExtResource({})".format(resource_id)
+
+    resource_id = escn_file.get_internal_resource(material)
+    # Existing internal resource
+    if resource_id is not None:
+        return "SubResource({})".format(resource_id)
+
+    mat = InternalResource("SpatialMaterial")
+
+    mat.flags_unshaded = material.use_shadeless
+    mat.flags_vertex_lighting = material.use_vertex_color_light
+    mat.flags_transparent = material.use_transparency
+    mat.vertex_color_use_as_albedo = material.use_vertex_color_paint
+    mat.albedo_color = material.diffuse_color
+    mat.subsurf_scatter_enabled = material.subsurface_scattering.use
+
+    resource_id = escn_file.add_internal_resource(mat, material)
+    return "SubResource({})".format(resource_id)
+
+
+# ------------------- Tools for finding existing materials -------------------
+def _find_material_in_subtree(folder, material):
+    """Searches for godot materials that match a blender material. If found,
+    it returns (path, type) otherwise it returns None"""
+    candidates = []
+
+    material_file_name = material.name + '.tres'
+    for folder, _subdirs, files in os.walk(folder):
+        if material_file_name in files:
+            candidates.append(os.path.join(folder, material_file_name))
+
+    # Checks it is a material and finds out what type
+    valid_candidates = []
+    for candidate in candidates:
+        with open(candidate) as mat_file:
+            first_line = mat_file.readline()
+            if "SpatialMaterial" in first_line:
+                valid_candidates.append((candidate, "SpatialMaterial"))
+            if "ShaderMaterial" in first_line:
+                valid_candidates.append((candidate, "ShaderMaterial"))
+
+    if not valid_candidates:
+        return None
+    if len(valid_candidates) > 1:
+        logging.warning("Multiple materials found for %s", material.name)
+    return valid_candidates[0]
+
+
+def find_material(export_settings, material):
+    """Searches for an existing Godot material"""
+    search_type = export_settings["material_search_paths"]
+    if search_type == "PROJECT_DIR":
+        search_dir = export_settings["project_path"]
+    elif search_type == "EXPORT_DIR":
+        search_dir = export_settings["path"]
+    else:
+        search_dir = None
+
+    if search_dir is None:
+        return None
+    return _find_material_in_subtree(search_dir, material)

+ 339 - 0
io_scene_godot/converters/mesh.py

@@ -0,0 +1,339 @@
+import logging
+import bpy
+import bmesh
+import mathutils
+
+from .material import export_material
+from ..structures import Array, NodeTemplate, InternalResource
+from . import physics
+
+
+# ------------------------------- The Mesh -----------------------------------
+def export_mesh_node(escn_file, export_settings, node, parent_path):
+    """Exports a MeshInstance. If the mesh is not already exported, it will
+    trigger the export of that mesh"""
+    if (node.data is None or
+            "MESH" not in export_settings['object_types']):
+        return parent_path
+
+    # If this mesh object has physics properties, we need to export them first
+    # because they need to be higher in the scene-tree
+    if physics.has_physics(node):
+        parent_path = physics.export_physics_properties(
+            escn_file, export_settings, node, parent_path
+        )
+
+    if node.hide_render:
+        return parent_path
+
+    else:
+        armature = None
+        if node.parent is not None and node.parent.type == "ARMATURE":
+            armature = node.parent
+
+        mesh_id = export_mesh(escn_file, export_settings, node, armature)  # We need to export the mesh
+
+        mesh_node = NodeTemplate(node.name, "MeshInstance", parent_path)
+        mesh_node.mesh = "SubResource({})".format(mesh_id)
+        if not physics.has_physics(node) or not physics.is_physics_root(node):
+            mesh_node.transform = node.matrix_local
+        else:
+            mesh_node.transform = mathutils.Matrix.Identity(4)
+        escn_file.add_node(mesh_node)
+
+        return parent_path + '/' + node.name
+
+
+def export_mesh(escn_file, export_settings, node, armature):
+    """Saves a mesh into the escn file """
+    # Check if it exists so we don't bother to export it twice
+    mesh = node.data
+    mesh_id = escn_file.get_internal_resource(mesh)
+
+    if mesh_id is not None:
+        return mesh_id
+
+    mesh_resource = InternalResource('ArrayMesh')
+
+    mesh_lines = []
+    mesh_materials = []
+    make_arrays(export_settings, node, armature, mesh_lines, mesh_materials)
+
+
+    for i in range(len(mesh_lines)):
+        mesh_resource.contents += "surfaces/" + str(i) + "={\n"
+        if mesh_materials[i] is not None:
+            mat_resource = export_material(escn_file, export_settings, mesh_materials[i])
+            mesh_resource.contents += "\t" + "\"material\":" + mat_resource + ",\n"
+        mesh_resource.contents += "\t" + "\"primitive\":4,\n"
+        mesh_resource.contents += "\t" + "\"arrays\":[\n"
+
+        arrays = ",\n\t\t".join(mesh_lines[i])
+        mesh_resource.contents += "\t\t" + arrays + "\n"
+
+        mesh_resource.contents += "\t" + "],\n"
+        mesh_resource.contents += "\t" + "\"morph_arrays\":[]\n"
+        mesh_resource.contents += "}\n"
+
+    mesh_id = escn_file.add_internal_resource(mesh_resource, mesh)
+    assert mesh_id is not None
+
+    return mesh_id
+
+
+def make_arrays(export_settings, node, armature, mesh_lines, ret_materials, skeyindex=-1):
+
+    mesh = node.to_mesh(bpy.context.scene,
+                        export_settings['use_mesh_modifiers'],
+                        "RENDER")
+
+    if True:  # Triangulate, always
+        bm = bmesh.new()
+        bm.from_mesh(mesh)
+        bmesh.ops.triangulate(bm, faces=bm.faces)
+        bm.to_mesh(mesh)
+        bm.free()
+
+    surfaces = []
+    material_to_surface = {}
+
+    mesh.update(calc_tessface=True)
+
+    si = None
+    #if armature is not None:
+    #    si = self.skeleton_info[armature]
+
+    # TODO: Implement automatic tangent detection
+    has_tangents = True  # always use tangents, we are grown up now.
+
+    has_colors = len(mesh.vertex_colors)
+
+    uv_layer_count = len(mesh.uv_textures)
+    if uv_layer_count > 2:
+        uv_layer_count = 2
+
+    if has_tangents and len(mesh.uv_textures):
+        try:
+            mesh.calc_tangents()
+        except:
+            logging.warning(
+                "CalcTangets failed for mesh %s, no tangets will be "
+                "exported.", mesh.name
+            )
+            mesh.calc_normals_split()
+            has_tangents = False
+
+    else:
+        mesh.calc_normals_split()
+        has_tangents = False
+
+    for face_index in range(len(mesh.polygons)):
+        face = mesh.polygons[face_index]
+
+        if face.material_index not in material_to_surface:
+            material_to_surface[face.material_index] = len(surfaces)
+            surfaces.append(Surface())
+            if mesh.materials:
+                mat = mesh.materials[face.material_index]
+                ret_materials.append(mat)
+            else:
+                ret_materials.append(None)
+
+        surface = surfaces[material_to_surface[face.material_index]]
+        vi = []
+
+        for lt in range(face.loop_total):
+            loop_index = face.loop_start + lt
+            ml = mesh.loops[loop_index]
+            mv = mesh.vertices[ml.vertex_index]
+
+            v = Vertex()
+            v.vertex = fix_vertex(mathutils.Vector(mv.co))
+
+            for xt in mesh.uv_layers:
+                v.uv.append(mathutils.Vector(xt.data[loop_index].uv))
+
+            if has_colors:
+                v.color = mathutils.Vector(
+                    mesh.vertex_colors[0].data[loop_index].color)
+
+            v.normal = fix_vertex(mathutils.Vector(ml.normal))
+
+            if has_tangents:
+                v.tangent = fix_vertex(mathutils.Vector(ml.tangent))
+                v.bitangent = fix_vertex(mathutils.Vector(ml.bitangent))
+
+            tup = v.get_tup()
+            idx = 0
+            # Do not optmize if using shapekeys
+            if skeyindex == -1 and tup in surface.vertex_map:
+                idx = surface.vertex_map[tup]
+            else:
+                idx = len(surface.vertices)
+                surface.vertices.append(v)
+                surface.vertex_map[tup] = idx
+
+            vi.append(idx)
+
+        if len(vi) > 2:  # Only triangles and above
+            surface.indices.append(vi)
+
+    for s in surfaces:
+        mesh_lines.append(s.generate_lines(has_tangents, has_colors, uv_layer_count, armature))
+
+    bpy.data.meshes.remove(mesh)
+
+
+class Surface:
+    """A surface is a single part of a mesh (eg in blender, one mesh can have
+    multiple materials. Godot calls these separate parts separate surfaces"""
+    def __init__(self):
+        self.vertices = []
+        self.vertex_map = {}
+        self.indices = []
+
+    def calc_tangent_dp(self, vert):
+        """Calculates the dot product of the tangent. I think this has
+        something to do with normal mapping"""
+        cross_product = vert.normal.cross(vert.tangent)
+        dot_product = cross_product.dot(vert.bitangent)
+        return 1.0 if dot_product > 0.0 else -1.0
+
+    def generate_lines(self, has_tangents, has_colors, uv_layer_count, armature):
+        surface_lines = []
+
+        position_vals = Array("Vector3Array(", values=[v.vertex for v in self.vertices])
+        normal_vals = Array("Vector3Array(", values=[v.normal for v in self.vertices])
+
+
+        if has_tangents:
+            tangent_vals = Array("FloatArray(")
+            for vert in self.vertices:
+                tangent_vals.extend(list(vert.tangent) + [self.calc_tangent_dp(vert)])
+        else:
+            tangent_vals = Array("null, ; No Tangents", "", "")
+
+        if has_colors:
+            color_vals = Array("ColorArray(")
+            for vert in self.vertices:
+                color_vals.extend(list(vert.color)+[1.0])
+        else:
+            color_vals = Array("null, ; no Vertex Colors", "", "")
+
+        surface_lines.append(position_vals.to_string())
+        surface_lines.append(normal_vals.to_string())
+        surface_lines.append(tangent_vals.to_string())
+        surface_lines.append(color_vals.to_string())
+
+        # UV Arrays
+        for i in range(2):  # Godot always expects two arrays for UV's
+            if i >= uv_layer_count:
+                # but if there aren't enough in blender, make one of them into null
+                surface_lines.append("null, ; No UV"+str(i+1))
+                continue
+            uv_vals = Array("Vector2Array(")
+            for vert in self.vertices:
+                uv_vals.extend([vert.uv[i].x, 1.0-vert.uv[i].y])
+
+            surface_lines.append(uv_vals.to_string())
+
+        # Bones and Weights
+        # Export armature data (if armature exists)
+        if armature is not None:
+            # Skin Weights!
+            float_values = "FloatArray("
+            float_valuesw = "FloatArray("
+            first = True
+            for vert in self.vertices:
+                #skin_weights_total += len(v.weights)
+                weights = []
+                for i in len(vert.bones):
+                    weights += (vert.bones[i], vert.weights[i])
+
+                weights = sorted(weights, key=lambda x: -x[1])
+                totalw = 0.0
+                for weight in weights:
+                    totalw += weight[1]
+                if totalw == 0.0:
+                    totalw = 0.000000001
+
+                for i in range(4):
+                    if i > 0:
+                        float_values += ","
+                        float_valuesw += ","
+                    if i < len(weights):
+                        float_values += " {}".format(weights[i][0])
+                        float_valuesw += " {}".format(weights[i][1]/totalw)
+                    else:
+                        float_values += " 0"
+                        float_valuesw += " 0.0"
+
+                if not first:
+                    float_values += ","
+                    float_valuesw += ","
+                else:
+                    first = False
+
+            float_values += "),"
+            surface_lines.append(float_values)
+            float_valuesw += "),"
+            surface_lines.append(float_valuesw)
+
+        else:
+            surface_lines.append("null, ; No Bones")
+            surface_lines.append("null, ; No Weights")
+
+        # Indices- each face is made of 3 verts, and these are the indices
+        # in the vertex arrays. The backface is computed from the winding
+        # order, hence v[2] before v[1]
+        int_values = Array(
+            "IntArray(",
+            values=[[v[0], v[2], v[1]] for v in self.indices]
+        )
+        surface_lines.append(int_values.to_string())
+
+        return surface_lines
+
+
+CMP_EPSILON = 0.0001
+
+
+def fix_vertex(vtx):
+    """Changes a single position vector from y-up to z-up"""
+    return mathutils.Vector((vtx.x, vtx.z, -vtx.y))
+
+
+class Vertex:
+    def get_tup(self):
+        """Returns a tuple form of this vertex so that it can be hashed"""
+        tup = (self.vertex.x, self.vertex.y, self.vertex.z, self.normal.x,
+               self.normal.y, self.normal.z)
+        for uv_data in self.uv:
+            tup = tup + (uv_data.x, uv_data.y)
+        if self.color is not None:
+            tup = tup + (self.color.x, self.color.y, self.color.z)
+        if self.tangent is not None:
+            tup = tup + (self.tangent.x, self.tangent.y, self.tangent.z)
+        if self.bitangent is not None:
+            tup = tup + (self.bitangent.x, self.bitangent.y,
+                         self.bitangent.z)
+        for bone in self.bones:
+            tup = tup + (float(bone), )
+        for weight in self.weights:
+            tup = tup + (float(weight), )
+
+        return tup
+
+    __slots__ = ("vertex", "normal", "tangent", "bitangent", "color", "uv",
+                 "uv2", "bones", "weights")
+
+    def __init__(self):
+        self.vertex = mathutils.Vector((0.0, 0.0, 0.0))
+        self.normal = mathutils.Vector((0.0, 0.0, 0.0))
+        self.tangent = None
+        self.bitangent = None
+        self.color = None
+        self.uv = []
+        self.uv2 = mathutils.Vector((0.0, 0.0))
+        self.bones = []
+        self.weights = []

+ 204 - 0
io_scene_godot/converters/physics.py

@@ -0,0 +1,204 @@
+"""
+The physics converter is a little special as it runs before the blender
+object is exported. In blender, the object owns the physics. In Godot, the
+physics owns the object.
+"""
+
+import os
+import math
+import logging
+import bpy
+import mathutils
+import bmesh
+from ..structures import NodeTemplate, InternalResource, Array
+
+
+AXIS_CORRECT = mathutils.Matrix.Rotation(math.radians(-90), 4, 'X')
+
+
+def has_physics(node):
+    """Returns True if the object has physics enabled"""
+    return node.rigid_body is not None
+
+
+def is_physics_root(node):
+    """Checks to see if this object is the root of the physics tree. This is
+    True if none of the parents of the object have physics."""
+    return get_physics_root(node)[0] is None
+
+
+def get_physics_root(node):
+    """ Check upstream for other rigid bodies (to allow compound shapes).
+    Returns the upstream-most rigid body and how many nodes there are between
+    this node and the parent """
+    parent_rbd = None
+    current_node = node
+    counter = 0
+    while current_node.parent is not None:
+        counter += 1
+        if current_node.parent.rigid_body is not None:
+            parent_rbd = current_node.parent
+
+        current_node = current_node.parent
+    return parent_rbd, counter
+
+
+def get_extents(node):
+    """Returns X, Y and Z total height"""
+    raw = node.bound_box
+    vecs = [mathutils.Vector(v) for v in raw]
+    mins = vecs[0].copy()
+    maxs = vecs[0].copy()
+
+    for vec in vecs:
+        mins.x = min(vec.x, mins.x)
+        mins.y = min(vec.y, mins.y)
+        mins.z = min(vec.z, mins.z)
+
+        maxs.x = max(vec.x, maxs.x)
+        maxs.y = max(vec.y, maxs.y)
+        maxs.z = max(vec.z, maxs.z)
+    return maxs - mins
+
+
+def export_collision_shape(escn_file, export_settings, node, parent_path,
+                           parent_override=None):
+    """Exports the collision primitives/geometry"""
+    col_name = node.name + 'Collision'
+    col_node = NodeTemplate(col_name, "CollisionShape", parent_path)
+
+    if parent_override is None:
+        col_node.transform = mathutils.Matrix.Identity(4) * AXIS_CORRECT
+    else:
+        parent_to_world = parent_override.matrix_world.inverted()
+        col_node.transform = parent_to_world * node.matrix_world
+
+    rbd = node.rigid_body
+
+    col_shape = None
+    bounds = get_extents(node)
+
+    if rbd.collision_shape == "BOX":
+        col_shape = InternalResource("BoxShape")
+        col_shape.extents = mathutils.Vector(bounds/2)
+        shape_id = escn_file.add_internal_resource(col_shape, rbd)
+
+    elif rbd.collision_shape == "SPHERE":
+        col_shape = InternalResource("SphereShape")
+        col_shape.radius = max(list(bounds))/2
+        shape_id = escn_file.add_internal_resource(col_shape, rbd)
+
+    elif rbd.collision_shape == "CAPSULE":
+        col_shape = InternalResource("CapsuleShape")
+        col_shape.radius = max(bounds.x, bounds.y) / 2
+        col_shape.height = bounds.z - col_shape.radius * 2
+        shape_id = escn_file.add_internal_resource(col_shape, rbd)
+    # elif rbd.collision_shape == "CONVEX_HULL":
+    #   pass
+    elif rbd.collision_shape == "MESH":
+        shape_id = generate_triangle_mesh_array(
+            escn_file, export_settings, 
+            node
+        )
+
+    else:
+        logging.warning("Unable to export physics shape for %s", node.name)
+
+    if shape_id is not None:
+        col_node.shape = "SubResource({})".format(shape_id)
+    escn_file.add_node(col_node)
+
+    return parent_path + "/" + col_name
+
+
+def generate_triangle_mesh_array(escn_file, export_settings, node):
+    """Generates godots ConcavePolygonShape from an object"""
+    mesh = node.data
+    key = (mesh, "TriangleCollisionMesh")
+    resource_id = escn_file.get_internal_resource(key)
+    if resource_id is not None:
+        return resource_id
+
+    col_shape = InternalResource("ConcavePolygonShape")
+
+
+    mesh = node.to_mesh(bpy.context.scene,
+                        export_settings['use_mesh_modifiers'],
+                        "RENDER")
+
+    # Triangulate
+    triangulated_mesh = bmesh.new()
+    triangulated_mesh.from_mesh(mesh)
+    bmesh.ops.triangulate(triangulated_mesh, faces=triangulated_mesh.faces)
+    triangulated_mesh.to_mesh(mesh)
+    triangulated_mesh.free()
+
+    vert_array = list()
+    for poly in mesh.polygons:
+        for vert_id in poly.vertices:
+            vert_array.append(list(mesh.vertices[vert_id].co))
+
+    bpy.data.meshes.remove(mesh)
+
+    col_shape.data = Array("PoolVector3Array(", values=vert_array)
+
+    return escn_file.add_internal_resource(col_shape, key)
+
+
+def export_physics_controller(escn_file, export_settings, node, parent_path):
+    """Exports the physics body "type" as a separate node. In blender, the
+    physics body type and the collision shape are one object, in godot they
+    are two. This is the physics body type"""
+    phys_name = node.name + 'Physics'
+
+    rbd = node.rigid_body
+    if rbd.type == "ACTIVE":
+        if rbd.kinematic:
+            phys_controller = 'KinematicBody'
+        else:
+            phys_controller = 'RigidBody'
+    else:
+        phys_controller = 'StaticBody'
+
+    phys_obj = NodeTemplate(phys_name, phys_controller, parent_path)
+
+    #  OPTIONS FOR ALL PHYSICS TYPES
+    phys_obj.friction = rbd.friction
+    phys_obj.bounce = rbd.restitution
+
+    col_groups = 0
+    for offset, bit in enumerate(rbd.collision_groups):
+        col_groups += bit << offset
+
+    phys_obj.transform = node.matrix_local
+    phys_obj.collision_layer = col_groups
+    phys_obj.collision_mask = col_groups
+
+    if phys_controller == "RigidBody":
+        phys_obj.can_sleep = rbd.use_deactivation
+        phys_obj.linear_damp = rbd.linear_damping
+        phys_obj.angular_damp = rbd.angular_damping
+        phys_obj.sleeping = rbd.use_start_deactivated
+
+    escn_file.add_node(phys_obj)
+
+    return parent_path + '/' + phys_name
+
+
+def export_physics_properties(escn_file, export_settings, node, parent_path):
+    """Creates the necessary nodes for the physics"""
+    parent_rbd, counter = get_physics_root(node)
+
+    if parent_rbd is None:
+        parent_path = export_physics_controller(
+            escn_file, export_settings, node, parent_path
+        )
+
+    tmp_parent_path = os.path.normpath(parent_path + "/.." * counter)
+
+    export_collision_shape(
+        escn_file, export_settings, node, tmp_parent_path,
+        parent_override=parent_rbd
+    )
+
+    return parent_path

+ 102 - 0
io_scene_godot/converters/simple_nodes.py

@@ -0,0 +1,102 @@
+"""
+Any exporters that can be written in a single function can go in here.
+Anything more complex should go in it's own file
+"""
+
+import math
+import logging
+import mathutils
+from ..structures import NodeTemplate
+
+# Used to correct spotlights and cameras, which in blender are Z-forwards and
+# in Godot are Y-forwards
+AXIS_CORRECT = mathutils.Matrix.Rotation(math.radians(-90), 4, 'X')
+
+
+def export_empty_node(escn_file, export_settings, node, parent_path):
+    """Converts an empty (or any unknown node) into a spatial"""
+    if "EMPTY" not in export_settings['object_types']:
+        return parent_path
+    empty_node = NodeTemplate(node.name, "Spatial", parent_path)
+    empty_node.transform = node.matrix_local
+    escn_file.add_node(empty_node)
+
+    return parent_path + '/' + node.name
+
+
+def export_camera_node(escn_file, export_settings, node, parent_path):
+    """Exports a camera"""
+    if (node.data is None or node.hide_render or
+            "CAMERA" not in export_settings['object_types']):
+        return parent_path
+
+    cam_node = NodeTemplate(node.name, "Camera", parent_path)
+    camera = node.data
+
+    cam_node.far = camera.clip_end
+    cam_node.near = camera.clip_start
+
+    if camera.type == "PERSP":
+        cam_node.projection = 0
+        cam_node.fov = math.degrees(camera.angle)
+    else:
+        cam_node.projection = 1
+        cam_node.size = camera.ortho_scale
+
+    cam_node.transform = node.matrix_local * AXIS_CORRECT
+    escn_file.add_node(cam_node)
+
+    return parent_path + '/' + node.name
+
+
+def export_lamp_node(escn_file, export_settings, node, parent_path):
+    """Exports lights - well, the ones it knows about. Other light types
+    just throw a warning"""
+    if (node.data is None or node.hide_render or
+            "LAMP" not in export_settings['object_types']):
+        return parent_path
+
+    light = node.data
+
+    if light.type == "POINT":
+        light_node = NodeTemplate(node.name, "OmniLight", parent_path)
+        light_node.omni_range = light.distance
+        light_node.shadow_enabled = light.shadow_method != "NOSHADOW"
+
+        if not light.use_sphere:
+            logging.warning(
+                "Ranged light without sphere enabled: %s", node.name
+            )
+
+    elif light.type == "SPOT":
+        light_node = NodeTemplate(node.name, "SpotLight", parent_path)
+        light_node.spot_range = light.distance
+        light_node.spot_angle = math.degrees(light.spot_size/2)
+        light_node.spot_angle_attenuation = 0.2/(light.spot_blend + 0.01)
+        light_node.shadow_enabled = light.shadow_method != "NOSHADOW"
+
+        if not light.use_sphere:
+            logging.warning(
+                "Ranged light without sphere enabled: %s", node.name
+            )
+
+    elif light.type == "SUN":
+        light_node = NodeTemplate(node.name, "DirectionalLight", parent_path)
+        light_node.shadow_enabled = light.shadow_method != "NOSHADOW"
+    else:
+        light_node = None
+        logging.warning(
+            "Unknown light type. Use Point, Spot or Sun: %s", node.name
+        )
+
+    if light_node is not None:
+        # Properties common to all lights
+        light_node.light_color = mathutils.Color(light.color)
+        light_node.transform = node.matrix_local * AXIS_CORRECT
+        light_node.light_negative = light.use_negative
+        light_node.light_specular = 1.0 if light.use_specular else 0.0
+        light_node.light_energy = light.energy
+
+        escn_file.add_node(light_node)
+
+    return parent_path + '/' + node.name

+ 77 - 0
io_scene_godot/encoders.py

@@ -0,0 +1,77 @@
+"""This file contains operations that take a blender or python concept and
+translate it into a string that godot will understand when it is parsed
+
+It also contains a dictionary called CONVERSIONS which encapsulates all the
+encoders by their types
+"""
+import mathutils
+
+
+def mat4_to_string(mtx):
+    """Converts a matrix to a "Transform" string that can be parsed by Godot"""
+    def fix_matrix(mtx):
+        """ Shuffles a matrix to change from y-up to z-up"""
+        # Todo: can this be replaced my a matrix multiplcation?
+        trans = mathutils.Matrix(mtx)
+        up_axis = 2
+
+        for i in range(3):
+            trans[1][i], trans[up_axis][i] = trans[up_axis][i], trans[1][i]
+        for i in range(3):
+            trans[i][1], trans[i][up_axis] = trans[i][up_axis], trans[i][1]
+
+        trans[1][3], trans[up_axis][3] = trans[up_axis][3], trans[1][3]
+
+        trans[up_axis][0] = -trans[up_axis][0]
+        trans[up_axis][1] = -trans[up_axis][1]
+        trans[0][up_axis] = -trans[0][up_axis]
+        trans[1][up_axis] = -trans[1][up_axis]
+        trans[up_axis][3] = -trans[up_axis][3]
+
+        return trans
+
+    mtx = fix_matrix(mtx)
+    out_str = ""
+    for row in range(3):
+        for col in range(3):
+            out_str += " {},".format(mtx[row][col])
+
+    # Export the basis
+    for axis in range(3):
+        out_str += " {},".format(mtx[axis][3])
+
+    out_str = out_str[:-1]  # Remove trailing comma
+
+    out_str = "Transform( {} )".format(out_str)
+    return out_str
+
+
+def color_to_string(rgba):
+    """Converts an RGB colors in range 0-1 into a fomat Godot can read. Accepts
+    iterables of 3 or 4 in length, but is designed for mathutils.Color"""
+    alpha = 1.0 if len(rgba) < 4 else rgba[3]
+    return "Color( {}, {}, {}, {} )".format(
+        rgba[0],
+        rgba[1],
+        rgba[2],
+        alpha,
+    )
+
+
+def vector_to_string(vec):
+    """Encode a mathutils.vector. actually, it accepts iterable of any length,
+    but 2, 3 are best...."""
+    elements = list(vec)
+    return "Vector{}({})".format(
+        len(elements),
+        ", ".join(str(e) for e in elements)
+    )
+
+
+# Finds the correct conversion function for a datatype
+CONVERSIONS = {
+    bool: lambda x: 'true' if x else 'false',
+    mathutils.Matrix: mat4_to_string,
+    mathutils.Color: color_to_string,
+    mathutils.Vector: vector_to_string,
+}

+ 71 - 1356
io_scene_godot/export_godot.py

@@ -25,1423 +25,138 @@ http://www.godotengine.org
 """
 
 import os
-import time
-import math
-import shutil
+import logging
 import bpy
-import bmesh
-from mathutils import Vector, Matrix
 
-#sections (in this order)
-S_EXTERNAL_RES = 0
-S_INTERNAL_RES = 1
-S_NODES = 2
+from . import structures
+from . import converters
 
-CMP_EPSILON = 0.0001
+logging.basicConfig(level=logging.INFO, format="[%(levelname)s]: %(message)s")
 
 
-def snap_tup(tup):
-    ret = ()
-    for x in tup:
-        ret += (x - math.fmod(x, 0.0001), )
 
-    return tup
+def find_godot_project_dir(export_path):
+    """Finds the project.godot file assuming that the export path
+    is inside a project (looks for a project.godot file)"""
+    project_dir = export_path
 
-
-def fix_matrix(mtx):
-    
-    tr = Matrix(mtx)
-    up_axis = 2
-    
-    for i in range(3):
-        tr[1][i], tr[up_axis][i] = tr[up_axis][i], tr[1][i]
-    for i in range(3):
-        tr[i][1], tr[i][up_axis] = tr[i][up_axis], tr[i][1]
-                   
-    tr[1][3], tr[up_axis][3] = tr[up_axis][3], tr[1][3] 
-
-    tr[up_axis][0] = -tr[up_axis][0];
-    tr[up_axis][1] = -tr[up_axis][1];
-    tr[0][up_axis] = -tr[0][up_axis];
-    tr[1][up_axis] = -tr[1][up_axis];
-    tr[up_axis][3] = -tr[up_axis][3]
-    
-    return tr
-
-def fix_vertex(vtx):
-    return Vector((vtx.x,vtx.z,-vtx.y))
-    
-
-def strmtx(mtx):
-    mtx = fix_matrix(mtx)
-    s = ""
-    for x in range(3):
-        for y in range(3):
-            if (x!=0 or y!=0):
-                s+=", "
-            s += "{} ".format(mtx[x][y])
-            
-    for x in range(3):
-        s += ",{} ".format(mtx[x][3])
-    
-    s = "Transform( {} )".format(s)
-    return s
-
-
-def numarr(a, mult=1.0):
-    s = " "
-    for x in a:
-        s += " {}".format(x * mult)
-    s += " "
-    return s
-
-
-def numarr_alpha(a, mult=1.0):
-    s = " "
-    for x in a:
-        s += " {}".format(x * mult)
-    if len(a) == 3:
-        s += " 1.0"
-    s += " "
-    return s
-
-
-def strarr(arr):
-    s = " "
-    for x in arr:
-        s += " {}".format(x)
-    s += " "
-    return s
+    while not os.path.isfile(os.path.join(project_dir, "project.godot")):
+        project_dir = os.path.split(project_dir)[0]
+        if project_dir == "/" or len(project_dir) < 3:
+            logging.error("Unable to find godot project file")
+            return None
+    logging.info("Found godot project directory at %s", project_dir)
+    return project_dir
 
 
 class GodotExporter:
-
-    def validate_id(self, d):
-        if (d.find("id-") == 0):
-            return "z{}".format(d)
-        return d
-
-
-    def new_resource_id(self):
-        self.last_res_id += 1
-        return self.last_res_id
-
-    def new_external_resource_id(self):
-        self.last_ext_res_id += 1
-        return self.last_ext_res_id
-
-    class Vertex:
-
-        def close_to(self, v):
-            if self.vertex - v.vertex.length() > CMP_EPSILON:
-                return False
-            if self.normal - v.normal.length() > CMP_EPSILON:
-                return False
-            if self.uv - v.uv.length() > CMP_EPSILON:
-                return False
-            if self.uv2 - v.uv2.length() > CMP_EPSILON:
-                return False
-
-            return True
-
-        def get_tup(self):
-            tup = (self.vertex.x, self.vertex.y, self.vertex.z, self.normal.x,
-                   self.normal.y, self.normal.z)
-            for t in self.uv:
-                tup = tup + (t.x, t.y)
-            if self.color is not None:
-                tup = tup + (self.color.x, self.color.y, self.color.z)
-            if self.tangent is not None:
-                tup = tup + (self.tangent.x, self.tangent.y, self.tangent.z)
-            if self.bitangent is not None:
-                tup = tup + (self.bitangent.x, self.bitangent.y,
-                             self.bitangent.z)
-            for t in self.bones:
-                tup = tup + (float(t), )
-            for t in self.weights:
-                tup = tup + (float(t), )
-
-            return tup
-
-        __slots__ = ("vertex", "normal", "tangent", "bitangent", "color", "uv",
-                     "uv2", "bones", "weights")
-
-        def __init__(self):
-            self.vertex = Vector((0.0, 0.0, 0.0))
-            self.normal = Vector((0.0, 0.0, 0.0))
-            self.tangent = None
-            self.bitangent = None
-            self.color = None
-            self.uv = []
-            self.uv2 = Vector((0.0, 0.0))
-            self.bones = []
-            self.weights = []
-
-    def writel(self, section, indent, text):
-        if (not (section in self.sections)):
-            self.sections[section] = []
-        line = "{}{}".format(indent * "\t", text)
-        self.sections[section].append(line)
-
-    def purge_empty_nodes(self):
-        sections = {}
-        for k, v in self.sections.items():
-            if not (len(v) == 2 and v[0][1:] == v[1][2:]):
-                sections[k] = v
-        self.sections = sections
-        
-    def to_color(self,color):
-        return "Color( {}, {}, {}, 1.0 )".format(color[0],color[1],color[2])
-
-    def export_image(self, image):
-        img_id = self.image_cache.get(image)
-        if img_id:
-            return img_id
-
-        imgpath = image.filepath
-        if imgpath.startswith("//"):
-            imgpath = bpy.path.abspath(imgpath)
-
-        try:
-            imgpath = os.path.relpath(imgpath, os.path.dirname(self.path)).replace("\\", "/")
-        except:
-            # TODO: Review, not sure why it fails
-            pass
-
-        imgid = str(self.new_external_resource_id())
-
-        self.image_cache[image]=imgid
-        self.writel(S_EXTERNAL_RES, 0,'[ext_resource path="'+imgpath+'" type="Texture" id='+imgid+']')
-        return imgid
-
-    def export_material(self, material, double_sided_hint=True):
-        material_id = self.material_cache.get(material)
-        if material_id:
-            return material_id
-
-        material_id = str(self.new_resource_id())
-        self.material_cache[material]=material_id
-        
-        self.writel(S_INTERNAL_RES,0,'\n[sub_resource type="SpatialMaterial" id='+material_id+']\n')
-        return material_id
-
-
-    class Surface:
-        def __init__(self):
-            self.vertices = []
-            self.vertex_map = {}
-            self.indices = []
-    
-    def make_arrays(self, node, armature, mesh_lines, ret_materials, skeyindex=-1):
-        
-        mesh = node.to_mesh(self.scene, self.config["use_mesh_modifiers"],
-                            "RENDER")  # TODO: Review
-        self.temp_meshes.add(mesh)
-        
-        if (True): # Triangulate, always
-            bm = bmesh.new()
-            bm.from_mesh(mesh)
-            bmesh.ops.triangulate(bm, faces=bm.faces)
-            bm.to_mesh(mesh)
-            bm.free()
-
-    
-        surfaces = []
-        material_to_surface = {}
-
-        mesh.update(calc_tessface=True)
-
-        si = None
-        if armature is not None:
-            si = self.skeleton_info[armature]
-
-        # TODO: Implement automatic tangent detection
-        has_tangents = True # always use tangents, we are grown up now.
-
-        has_colors = len(mesh.vertex_colors)
-        mat_assign = []
-
-        uv_layer_count = len(mesh.uv_textures)
-        if (uv_layer_count>2):
-            uv_layer_count=2
-            
-        if has_tangents and len(mesh.uv_textures):
-            try:
-                mesh.calc_tangents()
-            except:
-                self.operator.report(
-                    {"WARNING"},
-                    "CalcTangets failed for mesh \"{}\", no tangets will be "
-                    "exported.".format(mesh.name))
-                mesh.calc_normals_split()
-                has_tangents = False
-
-        else:
-            mesh.calc_normals_split()
-            has_tangents = False
-            
-
-        for fi in range(len(mesh.polygons)):
-            f = mesh.polygons[fi]
-
-            if not (f.material_index in material_to_surface):
-                material_to_surface[f.material_index] = len(surfaces)
-                surfaces.append( self.Surface() )
-
-                try:
-                    # TODO: Review, understand why it throws
-                    mat = mesh.materials[f.material_index]
-                except:
-                    mat = None
-
-                if (mat is not None):
-                     ret_materials.append(self.export_material(
-                        mat, mesh.show_double_sided))
-                else:
-                     ret_materials.append(None)
-                    
-               
-
-            surface = surfaces[material_to_surface[f.material_index]]
-            vi = []
-
-            for lt in range(f.loop_total):
-                loop_index = f.loop_start + lt
-                ml = mesh.loops[loop_index]
-                mv = mesh.vertices[ml.vertex_index]
-
-                v = self.Vertex()
-                v.vertex = fix_vertex(Vector(mv.co))
-
-                for xt in mesh.uv_layers:
-                    v.uv.append(Vector(xt.data[loop_index].uv))
-
-                if (has_colors):
-                    v.color = Vector(
-                        mesh.vertex_colors[0].data[loop_index].color)
-
-                v.normal = fix_vertex(Vector(ml.normal))
-
-                if (has_tangents):
-                    v.tangent = fix_vertex(Vector(ml.tangent))
-                    v.bitangent = fix_vertex(Vector(ml.bitangent))
-
-                if armature is not None:
-                    wsum = 0.0
-
-                    for vg in mv.groups:
-                        if vg.group >= len(node.vertex_groups):
-                            continue
-                        name = node.vertex_groups[vg.group].name
-
-                        if (name in si["bone_index"]):
-                            # TODO: Try using 0.0001 since Blender uses
-                            #       zero weight
-                            if (vg.weight > 0.001):
-                                v.bones.append(si["bone_index"][name])
-                                v.weights.append(vg.weight)
-                                wsum += vg.weight
-                    if (wsum == 0.0):
-                        if not self.wrongvtx_report:
-                            self.operator.report(
-                                {"WARNING"},
-                                "Mesh for object \"{}\" has unassigned "
-                                "weights. This may look wrong in exported "
-                                "model.".format(node.name))
-                            self.wrongvtx_report = True
-
-                        # TODO: Explore how to deal with zero-weight bones,
-                        #       which remain local
-                        v.bones.append(0)
-                        v.weights.append(1)
-
-                tup = v.get_tup()
-                idx = 0
-                # Do not optmize if using shapekeys
-                if (skeyindex == -1 and tup in surface.vertex_map):
-                    idx = surface.vertex_map[tup]
-                else:
-                    idx = len(surface.vertices)
-                    surface.vertices.append(v)
-                    surface.vertex_map[tup] = idx
-
-                vi.append(idx)
-
-            if (len(vi) > 2):  # Only triangles and above
-                surface.indices.append(vi)
-
-       
-        for s in surfaces:
-            surface_lines=[]
-                        
-            #Vertices
-            float_values = "Vector3Array("   
-            first=""
-            for v in s.vertices:
-                float_values += first+" {}, {}, {}".format(
-                    v.vertex.x, v.vertex.y, v.vertex.z)
-                first=","
-            float_values+="),"
-            surface_lines.append(float_values)
-            
-            # Normals Array
-            float_values = "Vector3Array("
-            first=""
-            for v in s.vertices:
-                float_values += first+" {}, {}, {}".format(
-                    v.normal.x, v.normal.y, v.normal.z)
-                first=","
-            float_values+="),"
-            surface_lines.append(float_values)
-                
-
-            if (has_tangents):
-                float_values = "FloatArray("
-                first=""
-                for v in s.vertices:
-                    cr = [(v.normal.y * v.tangent.z) - (v.normal.z * v.tangent.y),
-                (v.normal.z * v.tangent.x) - (v.normal.x * v.tangent.z),
-                (v.normal.x * v.tangent.y) - (v.normal.y * v.tangent.x)]
-                    dp = cr[0]*v.bitangent.x + cr[1]*v.bitangent.y + cr[2]*v.bitangent.z
-                    if (dp>0):
-                        dp=1.0
-                    else:
-                        dp=-1.0
-
-                    float_values += first+" {}, {}, {}, {}".format(
-                        v.tangent.x, v.tangent.y, v.tangent.z,dp)
-                    first=","
-                float_values+="),"
-                surface_lines.append(float_values)
-            else:
-                surface_lines.append("null, ; No Tangents")
-                
-            # Color Arrays
-            if (has_colors):
-                float_values = "ColorArray("
-                first=""
-                for v in s.vertices:
-                    float_values += first+" {}, {}, {}".format(
-                        v.color.x, v.color.y, v.color.z)
-                    first=","
-                float_values+="),"
-                surface_lines.append(float_values)
-            else:
-                surface_lines.append("null, ; No Colors")
-
-            # UV Arrays
-            for i in range(2):
-                if (i >= uv_layer_count):
-                    surface_lines.append("null, ; No UV"+str(i+1))
-                    continue
-                float_values = "Vector2Array("
-                first=","                
-                for v in s.vertices:
-                    try:
-                        float_values += " {}, {}".format(v.uv[i].x, v.uv[i].y)+first
-                    except:
-                        # TODO: Review, understand better the multi-uv-layer API
-                        float_values += " 0, 0 "
-                        
-                    first=""
-                float_values+="),"
-                surface_lines.append(float_values)
-
-            # Bones and Weights
-            # Export armature data (if armature exists)
-            if (armature is not None):
-                # Skin Weights!
-                float_values = "FloatArray("
-                float_valuesw = "FloatArray("
-                first=True
-                for v in s.vertices:
-                    skin_weights_total += len(v.weights)
-                    w = []
-                    for i in len(v.bones):
-                        w += (v.bones[i],v.weights[i])
-                        
-                    w = sorted( w, key=lambda x: -x[1])
-                    totalw = 0.0
-                    for x in w:
-                        totalw+=x[1]
-                    if (totalw==0.0):
-                        totalw=0.000000001
-                    
-                        
-                    for i in range(4):
-                        if (i>0):
-                            float_values+=","
-                            float_valuesw+=","
-                        if (i<len(w)):
-                            float_values+=" {}".format(w[i][0])
-                            float_valuesw+=" {}".format(w[i][1]/totalw)
-                        else:
-                            float_values+=" 0" 
-                            float_valuesw+=" 0.0"
-
-                    if (not first):
-                            float_values+=","
-                            float_valuesw+=","
-                    else:
-                        first=False
-
-                float_values+="),"
-                surface_lines.append(float_values)
-                float_valuesw+="),"
-                surface_lines.append(float_valuesw)
-
-            else:
-                surface_lines.append("null, ; No Bones")
-                surface_lines.append("null, ; No Weights")
-    
-    
-            # Indices
-            int_values = "IntArray("
-            first=""            
-            for v in s.indices:
-                int_values += first+" {}, {}, {} ".format(v[0],v[2],v[1]) #flip order as godot uses front is clockwise
-                first=","
-                
-            int_values+="),"
-            surface_lines.append(int_values)
-            mesh_lines.append(surface_lines)
-       
-    
-    def export_mesh(self, node, armature=None, skeyindex=-1, skel_source=None,
-                    custom_name=None):
-        mesh = node.data
-
-        if (node.data in self.mesh_cache):
-            return self.mesh_cache[mesh]
-        
-        morph_target_arrays=[]
-        morph_target_names= []
-
-        if (mesh.shape_keys is not None and len(
-                mesh.shape_keys.key_blocks)):
-            values = []
-            morph_targets = []
-            md = None
-            for k in range(0, len(mesh.shape_keys.key_blocks)):
-                shape = node.data.shape_keys.key_blocks[k]
-                values += [shape.value]
-                shape.value = 0
-
-            mid = self.new_id("morph")
-
-            for k in range(0, len(mesh.shape_keys.key_blocks)):
-                shape = node.data.shape_keys.key_blocks[k]
-                node.show_only_shape_key = True
-                node.active_shape_key_index = k
-                shape.value = 1.0
-                mesh.update()
-                p = node.data
-                v = node.to_mesh(bpy.context.scene, True, "RENDER")
-                self.temp_meshes.add(v)
-                node.data = v
-                node.data.update()
-                
-                morph_target_lines = []
-                md = self.make_arrays(node, None, morph_target_lines, [], k)
-                    
-                morph_target_names.append(shape.name)
-                morph_target_arrays.append(morph_target_lines)
-                
-                morph_targ
-                node.data = p
-                node.data.update()
-                shape.value = 0.0
-                
-            node.show_only_shape_key = False
-            node.active_shape_key_index = 0
-            
-        
-        
-        mesh_lines = []
-        mesh_materials = []
-        self.make_arrays(node, armature, mesh_lines, mesh_materials)
-
-        mesh_id = str(self.new_resource_id())
-        self.mesh_cache[mesh]=mesh_id
-
-        self.writel(S_INTERNAL_RES,0,'\n[sub_resource type="ArrayMesh" id='+mesh_id+']\n')
-        
-        
-        
-        for i in range(len(mesh_lines)):
-            pfx = "surfaces/"+str(i)+"/"
-            self.writel(S_INTERNAL_RES,0,"surfaces/"+str(i)+"={")
-            if (mesh_materials[i]!=None):
-                self.writel(S_INTERNAL_RES,1,"\"material\":SubResource("+str(mesh_materials[i])+"),")
-            self.writel(S_INTERNAL_RES,1,"\"primitive\":4,")
-            self.writel(S_INTERNAL_RES,1,"\"arrays\":[")
-            for sline in mesh_lines[i]:
-                self.writel(S_INTERNAL_RES,2,sline)
-            self.writel(S_INTERNAL_RES,1,"],")
-            self.writel(S_INTERNAL_RES,1,"\"morph_arrays\":[]")            
-            self.writel(S_INTERNAL_RES,0,"}")
-                
-        return mesh_id
-
-    def export_mesh_node(self, node, parent_path):
-        if (node.data is None):
-            return
-
-        armature = None
-        armcount = 0
-        for n in node.modifiers:
-            if (n.type == "ARMATURE"):
-                armcount += 1
-
-        self.writel(S_NODES,0, '\n[node name="'+node.name+'" type="MeshInstance" parent="'+parent_path+'"]\n')
-
-        """ Armature should happen just by direct relationship, since godot supports it the same way as Blender now
-        if (node.parent is not None):
-            if (node.parent.type == "ARMATURE"):
-                armature = node.parent
-                if (armcount > 1):
-                    self.operator.report(
-                        {"WARNING"}, "Object \"{}\" refers "
-                        "to more than one armature! "
-                        "This is unsupported.".format(node.name))
-                if (armcount == 0):
-                    self.operator.report(
-                        {"WARNING"}, "Object \"{}\" is child "
-                        "of an armature, but has no armature modifier.".format(
-                            node.name))
-        
-        if (armcount > 0 and not armature):
-            self.operator.report(
-                {"WARNING"},
-                "Object \"{}\" has armature modifier, but is not a child of "
-                "an armature. This is unsupported.".format(node.name))
-        """
-
-        if (node.data.shape_keys is not None):
-            sk = node.data.shape_keys
-            if (sk.animation_data):
-                for d in sk.animation_data.drivers:
-                    if (d.driver):
-                        for v in d.driver.variables:
-                            for t in v.targets:
-                                if (t.id is not None and
-                                        t.id.name in self.scene.objects):
-                                    self.armature_for_morph[
-                                        node] = self.scene.objects[t.id.name]
-
-        meshdata = self.export_mesh(node, armature)
-        
-        self.writel(S_NODES,0, 'mesh=SubResource('+str(meshdata)+")")
-        
-        close_controller = False
-
-        """
-        Rest of armature/morph stuff
-        if ("skin_id" in meshdata):
-            close_controller = True
-            self.writel(
-                S_NODES, il, "<instance_controller url=\"#{}\">".format(
-                    meshdata["skin_id"]))
-            for sn in self.skeleton_info[armature]["skeleton_nodes"]:
-                self.writel(
-                    S_NODES, il + 1, "<skeleton>#{}</skeleton>".format(sn))
-        elif ("morph_id" in meshdata):
-            self.writel(
-                S_NODES, il, "<instance_controller url=\"#{}\">".format(
-                    meshdata["morph_id"]))
-            close_controller = True
-        elif (armature is None):
-            self.writel(S_NODES, il, "<instance_geometry url=\"#{}\">".format(
-                meshdata["id"]))
-
-        if (len(meshdata["material_assign"]) > 0):
-            self.writel(S_NODES, il + 1, "<bind_material>")
-            self.writel(S_NODES, il + 2, "<technique_common>")
-            for m in meshdata["material_assign"]:
-                self.writel(
-                    S_NODES, il + 3,
-                    "<instance_material symbol=\"{}\" target=\"#{}\"/>".format(
-                        m[1], m[0]))
-
-            self.writel(S_NODES, il + 2, "</technique_common>")
-            self.writel(S_NODES, il + 1, "</bind_material>")
-
-        if (close_controller):
-            self.writel(S_NODES, il, "</instance_controller>")
-        else:
-            self.writel(S_NODES, il, "</instance_geometry>")
-        """
-        
-    """
-    def export_armature_bone(self, bone, il, si):
-        is_ctrl_bone = (
-            bone.name.startswith("ctrl") and
-            self.config["use_exclude_ctrl_bones"])
-        if (bone.parent is None and is_ctrl_bone is True):
-            self.operator.report(
-                {"WARNING"}, "Root bone cannot be a control bone.")
-            is_ctrl_bone = False
-
-        if (is_ctrl_bone is False):
-            boneid = self.new_id("bone")
-            boneidx = si["bone_count"]
-            si["bone_count"] += 1
-            bonesid = "{}-{}".format(si["id"], boneidx)
-            if (bone.name in self.used_bones):
-                if (self.config["use_anim_action_all"]):
-                    self.operator.report(
-                        {"WARNING"}, "Bone name \"{}\" used in more than one "
-                        "skeleton. Actions might export wrong.".format(
-                            bone.name))
-            else:
-                self.used_bones.append(bone.name)
-
-            si["bone_index"][bone.name] = boneidx
-            si["bone_ids"][bone] = boneid
-            si["bone_names"].append(bonesid)
-            self.writel(
-                S_NODES, il, "<node id=\"{}\" sid=\"{}\" name=\"{}\" "
-                "type=\"JOINT\">".format(boneid, bonesid, bone.name))
-
-        if (is_ctrl_bone is False):
-            il += 1
-
-        xform = bone.matrix_local
-        if (is_ctrl_bone is False):
-            si["bone_bind_poses"].append(
-                    (si["armature_xform"] * xform).inverted_safe())
-
-        if (bone.parent is not None):
-            xform = bone.parent.matrix_local.inverted_safe() * xform
-        else:
-            si["skeleton_nodes"].append(boneid)
-
-        if (is_ctrl_bone is False):
-            self.writel(
-                S_NODES, il, "<matrix sid=\"transform\">{}</matrix>".format(
-                    strmtx(xform)))
-
-        for c in bone.children:
-            self.export_armature_bone(c, il, si)
-
-        if (is_ctrl_bone is False):
-            il -= 1
-            self.writel(S_NODES, il, "</node>")
-
-    def export_armature_node(self, node, il, parent_path):
-        if (node.data is None):
-            return
-
-        self.skeletons.append(node)
-
-        armature = node.data
-        self.skeleton_info[node] = {
-            "bone_count": 0,
-            "id": self.new_id("skelbones"),
-            "name": node.name,
-            "bone_index": {},
-            "bone_ids": {},
-            "bone_names": [],
-            "bone_bind_poses": [],
-            "skeleton_nodes": [],
-            "armature_xform": node.matrix_world
-        }
-
-        for b in armature.bones:
-            if (b.parent is not None):
-                continue
-            self.export_armature_bone(b, il, self.skeleton_info[node])
-
-        if (node.pose):
-            for b in node.pose.bones:
-                for x in b.constraints:
-                    if (x.type == "ACTION"):
-                        self.action_constraints.append(x.action)
-    """
-    def export_camera_node(self, node, parent_path):
-        if (node.data is None):
-            return
-
-        self.writel(S_NODES,0, '\n[node name="'+node.name+'" type="Camera" parent="'+parent_path+'"]\n')
-        camera = node.data
-  
-        if (camera.type == "PERSP"):
-            self.writel(S_NODES,0, "projection=0")
-            self.writel(S_NODES,0, "fov="+str(math.degrees(camera.angle)))
-            self.writel(S_NODES,0, "far="+str(math.degrees(camera.clip_end)))
-            self.writel(S_NODES,0, "near="+str(math.degrees(camera.clip_start)))
-            
-        else:
-            self.writel(S_NODES,0, "projection=1")
-            self.writel(S_NODES,0, "size="+str(math.degrees(camera.ortho_scale * 0.5)))
-            self.writel(S_NODES,0, "far="+str(math.degrees(camera.clip_end)))
-            self.writel(S_NODES,0, "near="+str(math.degrees(camera.clip_start)))
-            
-
-    def export_lamp_node(self, node, parent_path):
-        if (node.data is None):
-            return
-
-        light = node.data
-
-        if (light.type == "POINT"):
-            self.writel(S_NODES,0, '\n[node name="'+node.name+'" type="OmniLight" parent="'+parent_path+'"]\n')
-
-            self.writel(S_NODES, 0,"light_color="+self.to_color(light.color))
-            if (light.use_sphere):
-                self.writel(S_NODES, "omni_range={}".format(strarr(light.distance)))
-
-        elif (light.type == "SPOT"):
-            self.writel(S_NODES,0, '\n[node name="'+node.name+'" type="SpotLight" parent="'+parent_path+'"]\n')
-
-            self.writel(S_NODES,0, "light_color="+self.to_color(light.color))
-            
-        else:  # Write a sun lamp for everything else (not supported)
-            self.writel(S_NODES,0, '\n[node name="'+node.name+'" type="DirectionalLight" parent="'+parent_path+'"]\n')
-            self.writel(S_NODES,0, "light_color="+self.to_color(light.color))
-            
-
-
-    def export_empty_node(self, node, il, parent_path):
-        self.writel(S_NODES, '\n[node name="'+node.name+'" type="Position3D" parent="'+parent_path+'"]\n')
-
-    """
-    def export_curve(self, curve):
-        splineid = self.new_id("spline")
-
-        self.writel(
-            S_GEOM, 1, "<geometry id=\"{}\" name=\"{}\">".format(
-                splineid, curve.name))
-        self.writel(S_GEOM, 2, "<spline closed=\"0\">")
-
-        points = []
-        interps = []
-        handles_in = []
-        handles_out = []
-        tilts = []
-
-        for cs in curve.splines:
-
-            if (cs.type == "BEZIER"):
-                for s in cs.bezier_points:
-                    points.append(s.co[0])
-                    points.append(s.co[1])
-                    points.append(s.co[2])
-
-                    handles_in.append(s.handle_left[0])
-                    handles_in.append(s.handle_left[1])
-                    handles_in.append(s.handle_left[2])
-
-                    handles_out.append(s.handle_right[0])
-                    handles_out.append(s.handle_right[1])
-                    handles_out.append(s.handle_right[2])
-
-                    tilts.append(s.tilt)
-                    interps.append("BEZIER")
-            else:
-
-                for s in cs.points:
-                    points.append(s.co[0])
-                    points.append(s.co[1])
-                    points.append(s.co[2])
-                    handles_in.append(s.co[0])
-                    handles_in.append(s.co[1])
-                    handles_in.append(s.co[2])
-                    handles_out.append(s.co[0])
-                    handles_out.append(s.co[1])
-                    handles_out.append(s.co[2])
-                    tilts.append(s.tilt)
-                    interps.append("LINEAR")
-
-        self.writel(S_GEOM, 3, "<source id=\"{}-positions\">".format(splineid))
-        position_values = ""
-        for x in points:
-            position_values += " {}".format(x)
-        self.writel(
-            S_GEOM, 4, "<float_array id=\"{}-positions-array\" "
-            "count=\"{}\">{}</float_array>".format(
-                splineid, len(points), position_values))
-        self.writel(S_GEOM, 4, "<technique_common>")
-        self.writel(
-            S_GEOM, 4, "<accessor source=\"#{}-positions-array\" "
-            "count=\"{}\" stride=\"3\">".format(splineid, len(points) / 3))
-        self.writel(S_GEOM, 5, "<param name=\"X\" type=\"float\"/>")
-        self.writel(S_GEOM, 5, "<param name=\"Y\" type=\"float\"/>")
-        self.writel(S_GEOM, 5, "<param name=\"Z\" type=\"float\"/>")
-        self.writel(S_GEOM, 4, "</accessor>")
-        self.writel(S_GEOM, 3, "</source>")
-
-        self.writel(
-            S_GEOM, 3, "<source id=\"{}-intangents\">".format(splineid))
-        intangent_values = ""
-        for x in handles_in:
-            intangent_values += " {}".format(x)
-        self.writel(
-            S_GEOM, 4, "<float_array id=\"{}-intangents-array\" "
-            "count=\"{}\">{}</float_array>".format(
-                splineid, len(points), intangent_values))
-        self.writel(S_GEOM, 4, "<technique_common>")
-        self.writel(
-            S_GEOM, 4, "<accessor source=\"#{}-intangents-array\" "
-            "count=\"{}\" stride=\"3\">".format(splineid, len(points) / 3))
-        self.writel(S_GEOM, 5, "<param name=\"X\" type=\"float\"/>")
-        self.writel(S_GEOM, 5, "<param name=\"Y\" type=\"float\"/>")
-        self.writel(S_GEOM, 5, "<param name=\"Z\" type=\"float\"/>")
-        self.writel(S_GEOM, 4, "</accessor>")
-        self.writel(S_GEOM, 3, "</source>")
-
-        self.writel(S_GEOM, 3, "<source id=\"{}-outtangents\">".format(
-            splineid))
-        outtangent_values = ""
-        for x in handles_out:
-            outtangent_values += " {}".format(x)
-        self.writel(
-            S_GEOM, 4, "<float_array id=\"{}-outtangents-array\" "
-            "count=\"{}\">{}</float_array>".format(
-                splineid, len(points), outtangent_values))
-        self.writel(S_GEOM, 4, "<technique_common>")
-        self.writel(
-            S_GEOM, 4, "<accessor source=\"#{}-outtangents-array\" "
-            "count=\"{}\" stride=\"3\">".format(splineid, len(points) / 3))
-        self.writel(S_GEOM, 5, "<param name=\"X\" type=\"float\"/>")
-        self.writel(S_GEOM, 5, "<param name=\"Y\" type=\"float\"/>")
-        self.writel(S_GEOM, 5, "<param name=\"Z\" type=\"float\"/>")
-        self.writel(S_GEOM, 4, "</accessor>")
-        self.writel(S_GEOM, 3, "</source>")
-
-        self.writel(
-            S_GEOM, 3, "<source id=\"{}-interpolations\">".format(splineid))
-        interpolation_values = ""
-        for x in interps:
-            interpolation_values += " {}".format(x)
-        self.writel(
-            S_GEOM, 4, "<Name_array id=\"{}-interpolations-array\" "
-            "count=\"{}\">{}</Name_array>"
-            .format(splineid, len(interps), interpolation_values))
-        self.writel(S_GEOM, 4, "<technique_common>")
-        self.writel(
-            S_GEOM, 4, "<accessor source=\"#{}-interpolations-array\" "
-            "count=\"{}\" stride=\"1\">".format(splineid, len(interps)))
-        self.writel(S_GEOM, 5, "<param name=\"INTERPOLATION\" type=\"name\"/>")
-        self.writel(S_GEOM, 4, "</accessor>")
-        self.writel(S_GEOM, 3, "</source>")
-
-        self.writel(S_GEOM, 3, "<source id=\"{}-tilts\">".format(splineid))
-        tilt_values = ""
-        for x in tilts:
-            tilt_values += " {}".format(x)
-        self.writel(
-            S_GEOM, 4,
-            "<float_array id=\"{}-tilts-array\" count=\"{}\">{}</float_array>"
-            .format(splineid, len(tilts), tilt_values))
-        self.writel(S_GEOM, 4, "<technique_common>")
-        self.writel(
-            S_GEOM, 4, "<accessor source=\"#{}-tilts-array\" "
-            "count=\"{}\" stride=\"1\">".format(splineid, len(tilts)))
-        self.writel(S_GEOM, 5, "<param name=\"TILT\" type=\"float\"/>")
-        self.writel(S_GEOM, 4, "</accessor>")
-        self.writel(S_GEOM, 3, "</source>")
-
-        self.writel(S_GEOM, 3, "<control_vertices>")
-        self.writel(
-            S_GEOM, 4,
-            "<input semantic=\"POSITION\" source=\"#{}-positions\"/>"
-            .format(splineid))
-        self.writel(
-            S_GEOM, 4,
-            "<input semantic=\"IN_TANGENT\" source=\"#{}-intangents\"/>"
-            .format(splineid))
-        self.writel(
-            S_GEOM, 4, "<input semantic=\"OUT_TANGENT\" "
-            "source=\"#{}-outtangents\"/>".format(splineid))
-        self.writel(
-            S_GEOM, 4, "<input semantic=\"INTERPOLATION\" "
-            "source=\"#{}-interpolations\"/>".format(splineid))
-        self.writel(
-            S_GEOM, 4, "<input semantic=\"TILT\" source=\"#{}-tilts\"/>"
-            .format(splineid))
-        self.writel(S_GEOM, 3, "</control_vertices>")
-
-        self.writel(S_GEOM, 2, "</spline>")
-        self.writel(S_GEOM, 1, "</geometry>")
-
-        return splineid
-    def export_curve_node(self, node, il):
-        if (node.data is None):
-            return
-
-        curveid = self.export_curve(node.data)
-
-        self.writel(S_NODES, il, "<instance_geometry url=\"#{}\">".format(
-            curveid))
-        self.writel(S_NODES, il, "</instance_geometry>")
-    """
+    """Handles picking what nodes to export and kicks off the export process"""
 
     def export_node(self, node, parent_path):
-        if (node not in self.valid_nodes):
+        """Recursively export a node. It calls the export_node function on
+        all of the nodes children. If you have heirarchies more than 1000 nodes
+        deep, this will fail with a recursion error"""
+        if node not in self.valid_nodes:
             return
+        logging.info("Exporting Blender Object: %s", node.name)
 
-        
         prev_node = bpy.context.scene.objects.active
         bpy.context.scene.objects.active = node
-        
-        node_name = node.name
-        
 
-        if (node.type == "MESH"):
-            self.export_mesh_node(node, parent_path)
-        #elif (node.type == "CURVE"):
-        #    self.export_curve_node(node, il)        
-        #elif (node.type == "ARMATURE"):
-        #    self.export_armature_node(node, il, node_name, parent_path)
-        elif (node.type == "CAMERA"):
-            self.export_camera_node(node, parent_path)
-        elif (node.type == "LAMP"):
-            self.export_lamp_node(node, parent_path)
-        elif (node.type == "EMPTY"):
-            self.export_empty_node(node, parent_path)
+        # Figure out what function will perform the export of this object
+        if node.type in converters.BLENDER_TYPE_TO_EXPORTER:
+            exporter = converters.BLENDER_TYPE_TO_EXPORTER[node.type]
         else:
-            self.writel(S_NODES,0, '\n[node name="'+node.name+'" type="Spatial" parent="'+parent_path+'"]\n')
-            
+            logging.warning(
+                "Unknown object type. Treating as empty: %s", node.name
+            )
+            exporter = converters.BLENDER_TYPE_TO_EXPORTER["EMPTY"]
 
-        self.writel(
-            S_NODES, 0, "transform="+strmtx(node.matrix_local))
+        # Perform the export
+        parent_path = exporter(self.escn_file, self.config, node, parent_path)
 
-        if (parent_path=="."):
-            parent_path = node_name
-        else:
-            parent_path = parent_path+"/"+node_name
-            
-        for x in node.children:
-            self.export_node(x, parent_path)
+        for child in node.children:
+            self.export_node(child, parent_path)
 
         bpy.context.scene.objects.active = prev_node
 
-    def is_node_valid(self, node):
-        if (node.type not in self.config["object_types"]):
+    def should_export_node(self, node):
+        """Checks if a node should be exported:"""
+        if node.type not in self.config["object_types"]:
             return False
 
-        if (self.config["use_active_layers"]):
+        if self.config["use_active_layers"]:
             valid = False
             for i in range(20):
-                if (node.layers[i] and self.scene.layers[i]):
+                if node.layers[i] and self.scene.layers[i]:
                     valid = True
                     break
-            if (not valid):
+            if not valid:
                 return False
 
-        if (self.config["use_export_selected"] and not node.select):
+        if self.config["use_export_selected"] and not node.select:
             return False
 
         return True
 
     def export_scene(self):
+        """Decide what objects to export, and export them!"""
+        self.escn_file.add_node(structures.SectionHeading(
+            "node", type="Spatial", name=self.scene.name
+        ))
+        logging.info("Exporting scene: %s", self.scene.name)
 
-        print("esporting scene "+str(len(self.scene.objects)))
+        # Decide what objects to export
         for obj in self.scene.objects:
-            print("OBJ: "+obj.name)
-            if (obj in self.valid_nodes):
+            if obj in self.valid_nodes:
                 continue
-            if (self.is_node_valid(obj)):
-                n = obj
-                while (n is not None):
-                    if (n not in self.valid_nodes):
-                        self.valid_nodes.append(n)
-                        print("VALID: "+n.name)
-                    n = n.parent
+            if self.should_export_node(obj):
+                # Ensure all parents are also going to be exported
+                node = obj
+                while node is not None:
+                    if node not in self.valid_nodes:
+                        self.valid_nodes.append(node)
+                    node = node.parent
 
-        self.writel(S_NODES,0, '\n[node name="scene" type="Spatial"]\n')            
+        logging.info("Exporting %d objects", len(self.valid_nodes))
 
         for obj in self.scene.objects:
-            if (obj in self.valid_nodes and obj.parent is None):
-                self.export_node(obj,".")
-
-
-    """
-    def export_animation_transform_channel(self, target, keys, matrices=True):
-        frame_total = len(keys)
-        anim_id = self.new_id("anim")
-        self.writel(S_ANIM, 1, "<animation id=\"{}\">".format(anim_id))
-        source_frames = ""
-        source_transforms = ""
-        source_interps = ""
-
-        for k in keys:
-            source_frames += " {}".format(k[0])
-            if (matrices):
-                source_transforms += " {}".format(strmtx(k[1]))
-            else:
-                source_transforms += " {}".format(k[1])
-
-            source_interps += " LINEAR"
-
-        # Time Source
-        self.writel(S_ANIM, 2, "<source id=\"{}-input\">".format(anim_id))
-        self.writel(
-            S_ANIM, 3, "<float_array id=\"{}-input-array\" "
-            "count=\"{}\">{}</float_array>".format(
-                anim_id, frame_total, source_frames))
-        self.writel(S_ANIM, 3, "<technique_common>")
-        self.writel(
-            S_ANIM, 4, "<accessor source=\"#{}-input-array\" "
-            "count=\"{}\" stride=\"1\">".format(anim_id, frame_total))
-        self.writel(S_ANIM, 5, "<param name=\"TIME\" type=\"float\"/>")
-        self.writel(S_ANIM, 4, "</accessor>")
-        self.writel(S_ANIM, 3, "</technique_common>")
-        self.writel(S_ANIM, 2, "</source>")
-
-        if (matrices):
-            # Transform Source
-            self.writel(
-                S_ANIM, 2, "<source id=\"{}-transform-output\">".format(
-                    anim_id))
-            self.writel(
-                S_ANIM, 3, "<float_array id=\"{}-transform-output-array\" "
-                "count=\"{}\">{}</float_array>".format(
-                    anim_id, frame_total * 16, source_transforms))
-            self.writel(S_ANIM, 3, "<technique_common>")
-            self.writel(
-                S_ANIM, 4,
-                "<accessor source=\"#{}-transform-output-array\" count=\"{}\" "
-                "stride=\"16\">".format(anim_id, frame_total))
-            self.writel(
-                S_ANIM, 5, "<param name=\"TRANSFORM\" type=\"float4x4\"/>")
-            self.writel(S_ANIM, 4, "</accessor>")
-            self.writel(S_ANIM, 3, "</technique_common>")
-            self.writel(S_ANIM, 2, "</source>")
-        else:
-            # Value Source
-            self.writel(
-                S_ANIM, 2,
-                "<source id=\"{}-transform-output\">".format(anim_id))
-            self.writel(
-                S_ANIM, 3, "<float_array id=\"{}-transform-output-array\" "
-                "count=\"{}\">{}</float_array>".format(
-                    anim_id, frame_total, source_transforms))
-            self.writel(S_ANIM, 3, "<technique_common>")
-            self.writel(
-                S_ANIM, 4, "<accessor source=\"#{}-transform-output-array\" "
-                "count=\"{}\" stride=\"1\">".format(anim_id, frame_total))
-            self.writel(S_ANIM, 5, "<param name=\"X\" type=\"float\"/>")
-            self.writel(S_ANIM, 4, "</accessor>")
-            self.writel(S_ANIM, 3, "</technique_common>")
-            self.writel(S_ANIM, 2, "</source>")
-
-        # Interpolation Source
-        self.writel(
-            S_ANIM, 2, "<source id=\"{}-interpolation-output\">".format(
-                anim_id))
-        self.writel(
-            S_ANIM, 3, "<Name_array id=\"{}-interpolation-output-array\" "
-            "count=\"{}\">{}</Name_array>".format(
-                anim_id, frame_total, source_interps))
-        self.writel(S_ANIM, 3, "<technique_common>")
-        self.writel(
-            S_ANIM, 4, "<accessor source=\"#{}-interpolation-output-array\" "
-            "count=\"{}\" stride=\"1\">".format(anim_id, frame_total))
-        self.writel(S_ANIM, 5, "<param name=\"INTERPOLATION\" type=\"Name\"/>")
-        self.writel(S_ANIM, 4, "</accessor>")
-        self.writel(S_ANIM, 3, "</technique_common>")
-        self.writel(S_ANIM, 2, "</source>")
-
-        self.writel(S_ANIM, 2, "<sampler id=\"{}-sampler\">".format(anim_id))
-        self.writel(
-            S_ANIM, 3,
-            "<input semantic=\"INPUT\" source=\"#{}-input\"/>".format(anim_id))
-        self.writel(
-            S_ANIM, 3, "<input semantic=\"OUTPUT\" "
-            "source=\"#{}-transform-output\"/>".format(anim_id))
-        self.writel(
-            S_ANIM, 3, "<input semantic=\"INTERPOLATION\" "
-            "source=\"#{}-interpolation-output\"/>".format(anim_id))
-        self.writel(S_ANIM, 2, "</sampler>")
-        if (matrices):
-            self.writel(
-                S_ANIM, 2, "<channel source=\"#{}-sampler\" "
-                "target=\"{}/transform\"/>".format(anim_id, target))
-        else:
-            self.writel(
-                S_ANIM, 2, "<channel source=\"#{}-sampler\" "
-                "target=\"{}\"/>".format(anim_id, target))
-        self.writel(S_ANIM, 1, "</animation>")
-
-        return [anim_id]
-
-    def export_animation(self, start, end, allowed=None):
-        # TODO: Blender -> Collada frames needs a little work
-        #       Collada starts from 0, blender usually from 1.
-        #       The last frame must be included also
-
-        frame_orig = self.scene.frame_current
+            if obj in self.valid_nodes and obj.parent is None:
+                self.export_node(obj, ".")
 
-        frame_len = 1.0 / self.scene.render.fps
-        frame_sub = 0
-        if (start > 0):
-            frame_sub = start * frame_len
-
-        tcn = []
-        xform_cache = {}
-        blend_cache = {}
-
-        # Change frames first, export objects last, boosts performance
-        for t in range(start, end + 1):
-            self.scene.frame_set(t)
-            key = t * frame_len - frame_sub
-
-            for node in self.scene.objects:
-                if (node not in self.valid_nodes):
-                    continue
-                if (allowed is not None and not (node in allowed)):
-                    if (node.type == "MESH" and node.data is not None and
-                        (node in self.armature_for_morph) and (
-                            self.armature_for_morph[node] in allowed)):
-                        pass
-                    else:
-                        continue
-                if (node.type == "MESH" and node.data is not None and
-                    node.data.shape_keys is not None and (
-                        node.data in self.mesh_cache) and len(
-                            node.data.shape_keys.key_blocks)):
-                    target = self.mesh_cache[node.data]["morph_id"]
-                    for i in range(len(node.data.shape_keys.key_blocks)):
-
-                        if (i == 0):
-                            continue
-
-                        name = "{}-morph-weights({})".format(target, i - 1)
-                        if (not (name in blend_cache)):
-                            blend_cache[name] = []
-
-                        blend_cache[name].append(
-                            (key, node.data.shape_keys.key_blocks[i].value))
-
-                if (node.type == "MESH" and node.parent and
-                        node.parent.type == "ARMATURE"):
-                    # In Collada, nodes that have skin modifier must not export
-                    # animation, animate the skin instead
-                    continue
-
-                if (len(node.constraints) > 0 or
-                        node.animation_data is not None):
-                    # If the node has constraints, or animation data, then
-                    # export a sampled animation track
-                    name = self.validate_id(node.name)
-                    if (not (name in xform_cache)):
-                        xform_cache[name] = []
-
-                    mtx = node.matrix_world.copy()
-                    if (node.parent):
-                        mtx = node.parent.matrix_world.inverted_safe() * mtx
-
-                    xform_cache[name].append((key, mtx))
-
-                if (node.type == "ARMATURE"):
-                    # All bones exported for now
-                    for bone in node.data.bones:
-                        if((bone.name.startswith("ctrl") and
-                                self.config["use_exclude_ctrl_bones"])):
-                            continue
-
-                        bone_name = self.skeleton_info[node]["bone_ids"][bone]
-
-                        if (not (bone_name in xform_cache)):
-                            xform_cache[bone_name] = []
-
-                        posebone = node.pose.bones[bone.name]
-                        parent_posebone = None
-
-                        mtx = posebone.matrix.copy()
-                        if (bone.parent):
-                            if (self.config["use_exclude_ctrl_bones"]):
-                                current_parent_posebone = bone.parent
-                                while (current_parent_posebone.name
-                                        .startswith("ctrl") and
-                                        current_parent_posebone.parent):
-                                    current_parent_posebone = (
-                                        current_parent_posebone.parent)
-                                parent_posebone = node.pose.bones[
-                                    current_parent_posebone.name]
-                            else:
-                                parent_posebone = node.pose.bones[
-                                    bone.parent.name]
-                            parent_invisible = False
-
-                            for i in range(3):
-                                if (parent_posebone.scale[i] == 0.0):
-                                    parent_invisible = True
-
-                            if (not parent_invisible):
-                                mtx = (
-                                    parent_posebone.matrix
-                                    .inverted_safe() * mtx)
-
-                        xform_cache[bone_name].append((key, mtx))
-
-        self.scene.frame_set(frame_orig)
-
-        # Export animation XML
-        for nid in xform_cache:
-            tcn += self.export_animation_transform_channel(
-                nid, xform_cache[nid], True)
-        for nid in blend_cache:
-            tcn += self.export_animation_transform_channel(
-                nid, blend_cache[nid], False)
-
-        return tcn
-
-    def export_animations(self):
-        tmp_mat = []
-        for s in self.skeletons:
-            tmp_bone_mat = []
-            for bone in s.pose.bones:
-                tmp_bone_mat.append(Matrix(bone.matrix_basis))
-                bone.matrix_basis = Matrix()
-            tmp_mat.append([Matrix(s.matrix_local), tmp_bone_mat])
-
-        self.writel(S_ANIM, 0, "<library_animations>")
-
-        if (self.config["use_anim_action_all"] and len(self.skeletons)):
-
-            cached_actions = {}
-
-            for s in self.skeletons:
-                if s.animation_data and s.animation_data.action:
-                    cached_actions[s] = s.animation_data.action.name
-
-            self.writel(S_ANIM_CLIPS, 0, "<library_animation_clips>")
-
-            for x in bpy.data.actions[:]:
-                if x.users == 0 or x in self.action_constraints:
-                    continue
-                if (self.config["use_anim_skip_noexp"] and
-                        x.name.endswith("-noexp")):
-                    continue
-
-                bones = []
-                # Find bones used
-                for p in x.fcurves:
-                    dp = p.data_path
-                    base = "pose.bones[\""
-                    if dp.startswith(base):
-                        dp = dp[len(base):]
-                        if (dp.find("\"") != -1):
-                            dp = dp[:dp.find("\"")]
-                            if (dp not in bones):
-                                bones.append(dp)
-
-                allowed_skeletons = []
-                for i, y in enumerate(self.skeletons):
-                    if (y.animation_data):
-                        for z in y.pose.bones:
-                            if (z.bone.name in bones):
-                                if (y not in allowed_skeletons):
-                                    allowed_skeletons.append(y)
-                        y.animation_data.action = x
-
-                        y.matrix_local = tmp_mat[i][0]
-                        for j, bone in enumerate(s.pose.bones):
-                            bone.matrix_basis = Matrix()
-
-                tcn = self.export_animation(int(x.frame_range[0]), int(
-                    x.frame_range[1] + 0.5), allowed_skeletons)
-                framelen = (1.0 / self.scene.render.fps)
-                start = x.frame_range[0] * framelen
-                end = x.frame_range[1] * framelen
-                self.writel(
-                    S_ANIM_CLIPS, 1, "<animation_clip name=\"{}\" "
-                    "start=\"{}\" end=\"{}\">".format(x.name, start, end))
-                for z in tcn:
-                    self.writel(S_ANIM_CLIPS, 2,
-                                "<instance_animation url=\"#{}\"/>".format(z))
-                self.writel(S_ANIM_CLIPS, 1, "</animation_clip>")
-                if (len(tcn) == 0):
-                    self.operator.report(
-                        {"WARNING"}, "Animation clip \"{}\" contains no "
-                        "tracks.".format(x.name))
-
-            self.writel(S_ANIM_CLIPS, 0, "</library_animation_clips>")
-
-            for i, s in enumerate(self.skeletons):
-                if (s.animation_data is None):
-                    continue
-                if s in cached_actions:
-                    s.animation_data.action = bpy.data.actions[
-                        cached_actions[s]]
-                else:
-                    s.animation_data.action = None
-                    for j, bone in enumerate(s.pose.bones):
-                        bone.matrix_basis = tmp_mat[i][1][j]
-
-        else:
-            self.export_animation(self.scene.frame_start, self.scene.frame_end)
-
-        self.writel(S_ANIM, 0, "</library_animations>")
-    """
     def export(self):
+        """Begin the export"""
+        self.escn_file = structures.ESCNFile(
+            structures.SectionHeading("gd_scene", load_steps=1, format=2)
+        )
 
         self.export_scene()
-        self.purge_empty_nodes()
-
-        #if (self.config["use_anim"]):
-        #    self.export_animations()
-
-        try:
-            f = open(self.path, "wb")
-        except:
-            return False
-
-
-        f.write(bytes("[gd_scene load_steps=1 format=2]\n\n", "UTF-8")) # TOODO count nodes and resources written for proper steps, though this is kinda useless on import anyway
-
-        if (S_EXTERNAL_RES in self.sections):
-            for l in self.sections[S_EXTERNAL_RES]:
-                f.write(bytes(l + "\n", "UTF-8"))
-
-        if (S_INTERNAL_RES in self.sections):
-            for l in self.sections[S_INTERNAL_RES]:
-                f.write(bytes(l + "\n", "UTF-8"))
-
-        for l in self.sections[S_NODES]:
-            f.write(bytes(l + "\n", "UTF-8"))
+        self.escn_file.fix_paths(self.config)
+        with open(self.path, 'w') as out_file:
+            out_file.write(self.escn_file.to_string())
 
         return True
 
-    __slots__ = ("operator", "scene", "last_res_id", "last_ext_res_id",  "sections",
-                 "path", "mesh_cache", "curve_cache", "material_cache",
-                 "image_cache", "skeleton_info", "config", "valid_nodes",
-                 "armature_for_morph", "used_bones", "wrongvtx_report",
-                 "skeletons", "action_constraints", "temp_meshes")
-
     def __init__(self, path, kwargs, operator):
+        self.path = path
         self.operator = operator
         self.scene = bpy.context.scene
-        self.last_res_id = 0
-        self.last_ext_res_id = 0
-        self.sections = {}
-        self.path = path
-        self.mesh_cache = {}
-        self.temp_meshes = set()
-        self.curve_cache = {}
-        self.material_cache = {}
-        self.image_cache = {}
-        self.skeleton_info = {}
         self.config = kwargs
+        self.config["path"] = path
+        self.config["project_path"] = find_godot_project_dir(path)
         self.valid_nodes = []
-        self.armature_for_morph = {}
-        self.used_bones = []
-        self.wrongvtx_report = False
-        self.skeletons = []
-        self.action_constraints = []
+
+        self.escn_file = None
 
     def __enter__(self):
         return self
 
     def __exit__(self, *exc):
-        for mesh in self.temp_meshes:
-            bpy.data.meshes.remove(mesh)
+        pass
 
 
-def save(operator, context, filepath="", use_selection=False, **kwargs):
+def save(operator, context, filepath="", **kwargs):
+    """Begin the export"""
     with GodotExporter(filepath, kwargs, operator) as exp:
         exp.export()
 

+ 268 - 0
io_scene_godot/structures.py

@@ -0,0 +1,268 @@
+"""The Godot file format has several concepts such as headings and subresources
+
+This file contains classes to help dealing with the actual writing to the file
+"""
+import os
+from .encoders import CONVERSIONS
+
+
+class ESCNFile:
+    """The ESCN file consists of three major sections:
+     - paths to external resources
+     - internal resources
+     - nodes
+
+    Because the write order is important, you have to know all the resources
+    before you can start writing nodes. This class acts as a container to store
+    the file before it can be written out in full
+
+    Things appended to this file should have the method "to_string()" which is
+    used when writing the file
+    """
+    def __init__(self, heading):
+        self.heading = heading
+        self.nodes = []
+        self.internal_resources = []
+        self._internal_hashes = {}
+
+        self.external_resources = []
+        self._external_hashes = {}
+
+    def get_external_resource(self, hashable):
+        """Searches for existing external resources, and returns their
+        resource ID. Returns None if it isn't in the file"""
+        return self._external_hashes.get(hashable)
+
+    def add_external_resource(self, item, hashable):
+        """External resources are indexed by ID. This function ensures no
+        two items have the same ID. It returns the index to the resource.
+        An error is thrown if the hashable matches an existing resource. You
+        should check get_external_resource before converting it into godot
+        format
+
+        The resource is not written to the file until the end, so you can
+        modify the resource after adding it to the file"""
+        if self.get_external_resource(hashable) is not None:
+            raise Exception("Attempting to add object to file twice")
+
+        self.external_resources.append(item)
+        index = len(self.external_resources)
+        item._heading.id = index
+        self._external_hashes[hashable] = index
+        return index
+
+    def get_internal_resource(self, hashable):
+        """Searches for existing internal resources, and returns their
+        resource ID"""
+        return self._internal_hashes.get(hashable)
+
+    def add_internal_resource(self, item, hashable):
+        """See comment on external resources. It's the same"""
+        if self.get_internal_resource(hashable) is not None:
+            raise Exception("Attempting to add object to file twice")
+        self.internal_resources.append(item)
+        index = len(self.internal_resources)
+        item._heading.id = index
+        self._internal_hashes[hashable] = index
+        return index
+
+    def add_node(self, item):
+        """Adds a node to this file. Nodes aren't indexed, so none of
+        the complexity of the other resource types"""
+        self.nodes.append(item)
+
+    def fix_paths(self, export_settings):
+        """Ensures all external resource paths are relative to the exported
+        file"""
+        for res in self.external_resources:
+            res.fix_path(export_settings)
+
+    def to_string(self):
+        """Serializes the file ready to dump out to disk"""
+
+        return "{}\n\n{}\n\n{}\n\n{}\n".format(
+            self.heading.to_string(),
+            '\n'.join(i.to_string() for i in self.external_resources),
+            '\n'.join(e.to_string() for e in self.internal_resources),
+            '\n'.join(n.to_string() for n in self.nodes)
+        )
+
+
+class SectionHeading:
+    """Many things in the escn file are separated by headings. These consist
+    of square brackets with key=value pairs inside them. The first element
+    is not a key-value pair, but describes what type of heading it is.
+
+    This class generates a section heading from it's attributes, so you can go:
+    sect = SectionHeading('thingo')
+    sect.foo = "bar"
+    sect.bar = 1234
+
+    and then sect.to_string() will return:
+    [thingo foo=bar bar=1234]
+    """
+    def __init__(self, section_type, **kwargs):
+        self._type = section_type
+        for key in kwargs:
+            self.__dict__[key] = kwargs[key]
+
+    def generate_prop_list(self):
+        """Generate all the key=value pairs into a string from all the
+        attributes in this class"""
+        out_str = ''
+        attribs = vars(self)
+        for var in attribs:
+            if var.startswith('_'):
+                continue  # Ignore hidden variables
+            val = attribs[var]
+            converter = CONVERSIONS.get(type(val))
+            if converter is not None:
+                val = converter(val)
+
+            # Extra wrapper for str's
+            if isinstance(val, str):
+                val = '"{}"'.format(val)
+
+            out_str += ' {}={}'.format(var, val)
+
+        return out_str
+
+    def to_string(self):
+        """Serializes this heading to a string"""
+        return '\n\n[{} {}]\n'.format(self._type, self.generate_prop_list())
+
+
+class NodeTemplate:
+    """Most things inside the escn file are Nodes that make up the scene tree.
+    This is a template node that can be used to contruct nodes of any type.
+    It is not intended that other classes in the exporter inherit from this,
+    but rather that all the exported nodes use this template directly.
+
+    Similar to the Sectionheading, this class uses it's attributes to
+    determine the properties of the node."""
+    def __init__(self, name, node_type, parent_path):
+        if parent_path.startswith("./"):
+            parent_path = parent_path[2:]
+
+        self._heading = SectionHeading(
+            "node",
+            name=name,
+            type=node_type,
+            parent=parent_path,
+        )
+
+    def generate_prop_list(self):
+        """Generate key/value pairs from the attributes of the node"""
+        out_str = ''
+        attribs = vars(self)
+        for var in attribs:
+            if var.startswith('_'):
+                continue  # Ignore hidden variables
+            val = attribs[var]
+            converter = CONVERSIONS.get(type(val))
+            if converter is not None:
+                val = converter(val)
+            out_str += '\n{} = {}'.format(var, val)
+
+        return out_str
+
+    def to_string(self):
+        """Serialize the node for writing to the file"""
+        return '{}{}\n'.format(
+            self._heading.to_string(),
+            self.generate_prop_list()
+        )
+
+
+class ExternalResource():
+    """External Resouces are references to external files. In the case of
+    an escn export, this is mostly used for images, sounds and so on"""
+    def __init__(self, path, resource_type):
+        self._heading = SectionHeading(
+            'ext_resource',
+            id=None,  # This is overwritten by ESCN_File.add_external_resource
+            path=path,
+            type=resource_type
+        )
+
+    def fix_path(self, export_settings):
+        """Makes the resource path relative to the exported file"""
+        self._heading.path = os.path.relpath(
+            self._heading.path,
+            os.path.dirname(export_settings["path"]),
+        )
+
+    def to_string(self):
+        """Serialize for export"""
+        return self._heading.to_string()
+
+
+class InternalResource():
+    """ A resource stored internally to the escn file, such as the
+    description of a material """
+    def __init__(self, resource_type):
+        self._heading = SectionHeading(
+            'sub_resource',
+            id=None,  # This is overwritten by ESCN_File.add_external_resource
+            type=resource_type
+        )
+
+        # This string is dumped verbatim, so can be used it the key=value
+        # would be hard to manage (Eg meshes, custom array types
+        self.contents = ''
+
+    def generate_prop_list(self):
+        """Generate key/value pairs from the attributes of the node"""
+        out_str = ''
+        attribs = vars(self)
+        for var in attribs:
+            if var.startswith('_') or var == 'contents':
+                continue  # Ignore hidden variables
+            val = attribs[var]
+            converter = CONVERSIONS.get(type(val))
+            if converter is not None:
+                val = converter(val)
+            if hasattr(val, "to_string"):
+                val = val.to_string()
+            out_str += '\n{} = {}'.format(var, val)
+
+        return out_str
+
+    def to_string(self):
+        """Serialize the node for writing to the file"""
+        return '{}{}\n{}'.format(
+            self._heading.to_string(),
+            self.generate_prop_list(),
+            self.contents,
+        )
+
+
+class Array(list):
+    """In the escn file there are lots of arrays which are defined by
+    a type (eg Vector3Array) and then have lots of values. This helps
+    to serialize that sort of array. You can also pass in custom separators
+    and suffixes.
+
+    Note that the constructor values parameter flattens the list using the
+    add_elements method
+    """
+    def __init__(self, prefix, seperator=', ', suffix=')', values=()):
+        self.prefix = prefix
+        self.seperator = seperator
+        self.suffix = suffix
+        super().__init__()
+        self.add_elements(values)
+
+    def add_elements(self, list_of_lists):
+        """Add each element from a list of lists to the array (flatten the
+        list of lists)"""
+        for lis in list_of_lists:
+            self.extend(lis)
+
+    def to_string(self):
+        """Convert the array to serialized form"""
+        return "{}{}{}".format(
+            self.prefix,
+            self.seperator.join([str(v) for v in self]),
+            self.suffix
+        )

+ 102 - 0
tests/default_env.tres

@@ -0,0 +1,102 @@
+[gd_resource type="Environment" load_steps=2 format=2]
+
+[sub_resource type="ProceduralSky" id=1]
+
+radiance_size = 4
+sky_top_color = Color( 0.0470588, 0.454902, 0.976471, 1 )
+sky_horizon_color = Color( 0.556863, 0.823529, 0.909804, 1 )
+sky_curve = 0.25
+sky_energy = 1.0
+ground_bottom_color = Color( 0.101961, 0.145098, 0.188235, 1 )
+ground_horizon_color = Color( 0.482353, 0.788235, 0.952941, 1 )
+ground_curve = 0.01
+ground_energy = 1.0
+sun_color = Color( 1, 1, 1, 1 )
+sun_latitude = 35.0
+sun_longitude = 0.0
+sun_angle_min = 1.0
+sun_angle_max = 100.0
+sun_curve = 0.05
+sun_energy = 16.0
+texture_size = 2
+
+[resource]
+
+background_mode = 2
+background_sky = SubResource( 1 )
+background_sky_custom_fov = 0.0
+background_color = Color( 0, 0, 0, 1 )
+background_energy = 0.0
+background_canvas_max_layer = 0
+ambient_light_color = Color( 0, 0, 0, 1 )
+ambient_light_energy = 0.0
+ambient_light_sky_contribution = 1.0
+fog_enabled = false
+fog_color = Color( 0.5, 0.6, 0.7, 1 )
+fog_sun_color = Color( 1, 0.9, 0.7, 1 )
+fog_sun_amount = 0.0
+fog_depth_enabled = true
+fog_depth_begin = 10.0
+fog_depth_curve = 1.0
+fog_transmit_enabled = false
+fog_transmit_curve = 1.0
+fog_height_enabled = false
+fog_height_min = 0.0
+fog_height_max = 100.0
+fog_height_curve = 1.0
+tonemap_mode = 0
+tonemap_exposure = 1.0
+tonemap_white = 1.0
+auto_exposure_enabled = false
+auto_exposure_scale = 0.4
+auto_exposure_min_luma = 0.05
+auto_exposure_max_luma = 8.0
+auto_exposure_speed = 0.5
+ss_reflections_enabled = false
+ss_reflections_max_steps = 64
+ss_reflections_fade_in = 0.15
+ss_reflections_fade_out = 2.0
+ss_reflections_depth_tolerance = 0.2
+ss_reflections_roughness = true
+ssao_enabled = false
+ssao_radius = 1.0
+ssao_intensity = 1.0
+ssao_radius2 = 0.0
+ssao_intensity2 = 1.0
+ssao_bias = 0.01
+ssao_light_affect = 0.0
+ssao_color = Color( 0, 0, 0, 1 )
+ssao_quality = 0
+ssao_blur = 3
+ssao_edge_sharpness = 4.0
+dof_blur_far_enabled = false
+dof_blur_far_distance = 10.0
+dof_blur_far_transition = 5.0
+dof_blur_far_amount = 0.1
+dof_blur_far_quality = 1
+dof_blur_near_enabled = false
+dof_blur_near_distance = 2.0
+dof_blur_near_transition = 1.0
+dof_blur_near_amount = 0.1
+dof_blur_near_quality = 1
+glow_enabled = false
+glow_levels/1 = false
+glow_levels/2 = false
+glow_levels/3 = true
+glow_levels/4 = false
+glow_levels/5 = true
+glow_levels/6 = false
+glow_levels/7 = false
+glow_intensity = 0.8
+glow_strength = 1.0
+glow_bloom = 0.0
+glow_blend_mode = 2
+glow_hdr_threshold = 1.0
+glow_hdr_scale = 2.0
+glow_bicubic_upscale = false
+adjustment_enabled = false
+adjustment_brightness = 1.0
+adjustment_contrast = 1.0
+adjustment_saturation = 1.0
+_sections_unfolded = [ "Ambient Light", "Background" ]
+

BIN
tests/icon.png


+ 62 - 0
tests/multi_uv_tester.tres

@@ -0,0 +1,62 @@
+[gd_resource type="ShaderMaterial" load_steps=3 format=2]
+
+[ext_resource path="res://uvtester.jpg" type="Texture" id=1]
+
+[sub_resource type="Shader" id=1]
+
+code = "shader_type spatial;
+render_mode blend_mix,depth_draw_opaque,cull_back,diffuse_burley,specular_schlick_ggx;
+uniform vec4 albedo : hint_color;
+uniform sampler2D texture_albedo : hint_albedo;
+uniform float specular;
+uniform float metallic;
+uniform float roughness : hint_range(0,1);
+uniform float point_size : hint_range(0,128);
+uniform sampler2D texture_metallic : hint_white;
+uniform vec4 metallic_texture_channel;
+uniform sampler2D texture_roughness : hint_white;
+uniform vec4 roughness_texture_channel;
+uniform vec3 uv1_scale;
+uniform vec3 uv1_offset;
+uniform vec3 uv2_scale;
+uniform vec3 uv2_offset;
+
+
+void vertex() {
+	UV=UV*uv1_scale.xy+uv1_offset.xy;
+}
+
+
+
+
+void fragment() {
+	vec2 base_uv = UV;
+	vec2 base_uv2 = UV2;
+	vec4 albedo_tex = texture(texture_albedo,base_uv);
+	vec4 albedo_tex2 = texture(texture_albedo,base_uv2);
+	ALBEDO = albedo.rgb * (albedo_tex.rgb*0.5 + albedo_tex2.rgb*0.5);
+	float metallic_tex = dot(texture(texture_metallic,base_uv),metallic_texture_channel);
+	METALLIC = metallic_tex * metallic;
+	float roughness_tex = dot(texture(texture_roughness,base_uv),roughness_texture_channel);
+	ROUGHNESS = roughness_tex * roughness;
+	SPECULAR = specular;
+}
+"
+
+[resource]
+
+render_priority = 0
+shader = SubResource( 1 )
+shader_param/albedo = Color( 1, 1, 1, 1 )
+shader_param/specular = 0.5
+shader_param/metallic = 0.0
+shader_param/roughness = 0.0
+shader_param/point_size = 1.0
+shader_param/metallic_texture_channel = Plane( 1, 0, 0, 0 )
+shader_param/roughness_texture_channel = Plane( 1, 0, 0, 0 )
+shader_param/uv1_scale = Vector3( 1, 1, 1 )
+shader_param/uv1_offset = Vector3( 0, 0, 0 )
+shader_param/uv2_scale = Vector3( 1, 1, 1 )
+shader_param/uv2_offset = Vector3( 0, 0, 0 )
+shader_param/texture_albedo = ExtResource( 1 )
+

+ 18 - 0
tests/project.godot

@@ -0,0 +1,18 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+;   [section] ; section goes between []
+;   param=value ; assign values to parameters
+
+config_version=3
+
+[application]
+
+config/name="blender_exporter_tests"
+config/icon="res://icon.png"
+
+[rendering]
+
+environment/default_environment="res://default_env.tres"

+ 38 - 0
tests/scenes/export_blends.py

@@ -0,0 +1,38 @@
+import bpy
+import os
+import sys
+
+
+sys.path = [os.getcwd()] + sys.path  # Ensure exporter from this folder
+from io_scene_godot import export_godot
+
+
+def export_escn(out_file):
+	"""Fake the export operator call"""
+	class op:
+		def __init__(self):
+			self.report = print
+			
+	res = export_godot.save(op(), bpy.context, out_file, 
+		object_types={"EMPTY", "CAMERA", "LAMP", "ARMATURE", "MESH", "CURVE"},
+		use_active_layers=False,
+		use_export_selected=False,
+		use_mesh_modifiers=True,
+		material_search_paths = 'PROJECT_DIR'
+	)
+
+
+
+def main():
+	target_dir = os.path.join(os.getcwd(), "tests/scenes")
+	for file_name in os.listdir(target_dir):
+		full_path = os.path.join(target_dir, file_name)
+		if full_path.endswith(".blend"):
+			print("Exporting {}".format(full_path))
+			bpy.ops.wm.open_mainfile(filepath=full_path)
+			export_escn(full_path.replace('.blend', '.escn'))
+			print("Exported")
+
+
+if __name__ == "__main__":
+	main()

BIN
tests/scenes/just_cameras.blend


BIN
tests/scenes/just_mesh.blend


+ 29 - 0
tests/scenes/just_mesh.tscn

@@ -0,0 +1,29 @@
+[gd_scene load_steps=2 format=2]
+
+[ext_resource path="res://scenes/just_mesh.escn" type="PackedScene" id=1]
+
+[node name="Scene Root" index="0" instance=ExtResource( 1 )]
+
+[node name="OmniLight" type="OmniLight" parent="." index="3"]
+
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.19843, 2.40187 )
+layers = 1
+light_color = Color( 1, 1, 1, 1 )
+light_energy = 1.0
+light_indirect_energy = 1.0
+light_negative = false
+light_specular = 0.5
+light_bake_mode = 1
+light_cull_mask = -1
+shadow_enabled = false
+shadow_color = Color( 0, 0, 0, 1 )
+shadow_bias = 0.15
+shadow_contact = 0.0
+shadow_reverse_cull_face = false
+editor_only = false
+omni_range = 5.0
+omni_attenuation = 1.0
+omni_shadow_mode = 1
+omni_shadow_detail = 1
+
+

BIN
tests/scenes/just_point_lights.blend


BIN
tests/scenes/just_spot_lights.blend


BIN
tests/scenes/material_search.blend


BIN
tests/scenes/parented_meshes.blend


BIN
tests/scenes/physics.blend


BIN
tests/scenes/simple_materials.blend


BIN
tests/scenes/tangent_test.blend


+ 89 - 0
tests/scenes/tangent_test.tscn

@@ -0,0 +1,89 @@
+[gd_scene load_steps=4 format=2]
+
+[ext_resource path="res://scenes/tangent_test.escn" type="PackedScene" id=1]
+[ext_resource path="res://uvtester_normals.jpg" type="Texture" id=2]
+
+[sub_resource type="SpatialMaterial" id=1]
+
+render_priority = 0
+flags_transparent = false
+flags_unshaded = false
+flags_vertex_lighting = false
+flags_no_depth_test = false
+flags_use_point_size = false
+flags_world_triplanar = false
+flags_fixed_size = false
+flags_albedo_tex_force_srgb = false
+vertex_color_use_as_albedo = false
+vertex_color_is_srgb = false
+params_diffuse_mode = 0
+params_specular_mode = 0
+params_blend_mode = 0
+params_cull_mode = 0
+params_depth_draw_mode = 0
+params_line_width = 1.0
+params_point_size = 1.0
+params_billboard_mode = 0
+params_grow = false
+params_use_alpha_scissor = false
+albedo_color = Color( 1, 1, 1, 1 )
+metallic = 0.0
+metallic_specular = 0.5
+metallic_texture_channel = 0
+roughness = 0.0
+roughness_texture_channel = 0
+emission_enabled = false
+normal_enabled = true
+normal_scale = 1.0
+normal_texture = ExtResource( 2 )
+rim_enabled = false
+clearcoat_enabled = false
+anisotropy_enabled = false
+ao_enabled = false
+depth_enabled = false
+subsurf_scatter_enabled = false
+transmission_enabled = false
+refraction_enabled = false
+detail_enabled = false
+uv1_scale = Vector3( 1, 1, 1 )
+uv1_offset = Vector3( 0, 0, 0 )
+uv1_triplanar = false
+uv1_triplanar_sharpness = 1.0
+uv2_scale = Vector3( 1, 1, 1 )
+uv2_offset = Vector3( 0, 0, 0 )
+uv2_triplanar = false
+uv2_triplanar_sharpness = 1.0
+proximity_fade_enable = false
+distance_fade_enable = false
+_sections_unfolded = [ "Albedo", "NormalMap" ]
+
+[node name="Scene Root" index="0" instance=ExtResource( 1 )]
+
+[node name="Plane" parent="." index="0"]
+
+material/0 = SubResource( 1 )
+_sections_unfolded = [ "material" ]
+
+[node name="OmniLight" type="OmniLight" parent="." index="1"]
+
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 1.42957, 1.1802, 0 )
+layers = 1
+light_color = Color( 1, 1, 1, 1 )
+light_energy = 1.0
+light_indirect_energy = 1.0
+light_negative = false
+light_specular = 0.5
+light_bake_mode = 1
+light_cull_mask = -1
+shadow_enabled = false
+shadow_color = Color( 0, 0, 0, 1 )
+shadow_bias = 0.15
+shadow_contact = 0.0
+shadow_reverse_cull_face = false
+editor_only = false
+omni_range = 5.0
+omni_attenuation = 1.0
+omni_shadow_mode = 1
+omni_shadow_detail = 1
+
+

BIN
tests/scenes/uv_testing.blend


+ 52 - 0
tests/scenes/uv_testing.tscn

@@ -0,0 +1,52 @@
+[gd_scene load_steps=4 format=2]
+
+[ext_resource path="res://scenes/uv_testing.escn" type="PackedScene" id=1]
+[ext_resource path="res://multi_uv_tester.tres" type="Material" id=2]
+[ext_resource path="res://uv_tester_material.tres" type="Material" id=3]
+
+[node name="Scene Root" index="0" instance=ExtResource( 1 )]
+
+[node name="Cube.001" parent="." index="0"]
+
+material/0 = ExtResource( 2 )
+_sections_unfolded = [ "material" ]
+
+[node name="MultiUV" parent="." index="1"]
+
+material/0 = ExtResource( 2 )
+_sections_unfolded = [ "material" ]
+
+[node name="OmniLight" type="OmniLight" parent="." index="2"]
+
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0.522455, 3.58174, 5.86328 )
+layers = 1
+light_color = Color( 1, 1, 1, 1 )
+light_energy = 1.0
+light_indirect_energy = 1.0
+light_negative = false
+light_specular = 0.5
+light_bake_mode = 1
+light_cull_mask = -1
+shadow_enabled = false
+shadow_color = Color( 0, 0, 0, 1 )
+shadow_bias = 0.15
+shadow_contact = 0.0
+shadow_reverse_cull_face = false
+editor_only = false
+omni_range = 10.0
+omni_attenuation = 1.0
+omni_shadow_mode = 1
+omni_shadow_detail = 1
+_sections_unfolded = [ "Omni" ]
+
+[node name="Cube" parent="." index="3"]
+
+material/0 = ExtResource( 3 )
+_sections_unfolded = [ "material" ]
+
+[node name="Plane" parent="." index="4"]
+
+material/0 = ExtResource( 3 )
+_sections_unfolded = [ "material" ]
+
+

BIN
tests/scenes/vertex_color.blend


+ 88 - 0
tests/scenes/vertex_color.tscn

@@ -0,0 +1,88 @@
+[gd_scene load_steps=3 format=2]
+
+[ext_resource path="res://scenes/vertex_color.escn" type="PackedScene" id=1]
+
+[sub_resource type="SpatialMaterial" id=1]
+
+render_priority = 0
+flags_transparent = false
+flags_unshaded = false
+flags_vertex_lighting = false
+flags_no_depth_test = false
+flags_use_point_size = false
+flags_world_triplanar = false
+flags_fixed_size = false
+flags_albedo_tex_force_srgb = false
+vertex_color_use_as_albedo = true
+vertex_color_is_srgb = false
+params_diffuse_mode = 0
+params_specular_mode = 0
+params_blend_mode = 0
+params_cull_mode = 0
+params_depth_draw_mode = 0
+params_line_width = 1.0
+params_point_size = 1.0
+params_billboard_mode = 0
+params_grow = false
+params_use_alpha_scissor = false
+albedo_color = Color( 1, 1, 1, 1 )
+metallic = 0.0
+metallic_specular = 0.5
+metallic_texture_channel = 0
+roughness = 0.0
+roughness_texture_channel = 0
+emission_enabled = false
+normal_enabled = false
+rim_enabled = false
+clearcoat_enabled = false
+anisotropy_enabled = false
+ao_enabled = false
+depth_enabled = false
+subsurf_scatter_enabled = false
+transmission_enabled = false
+refraction_enabled = false
+detail_enabled = false
+uv1_scale = Vector3( 1, 1, 1 )
+uv1_offset = Vector3( 0, 0, 0 )
+uv1_triplanar = false
+uv1_triplanar_sharpness = 1.0
+uv2_scale = Vector3( 1, 1, 1 )
+uv2_offset = Vector3( 0, 0, 0 )
+uv2_triplanar = false
+uv2_triplanar_sharpness = 1.0
+proximity_fade_enable = false
+distance_fade_enable = false
+_sections_unfolded = [ "Vertex Color" ]
+
+[node name="Scene Root" index="0" instance=ExtResource( 1 )]
+
+editor/display_folded = true
+
+[node name="Plane" parent="." index="0"]
+
+material/0 = SubResource( 1 )
+_sections_unfolded = [ "material" ]
+
+[node name="OmniLight" type="OmniLight" parent="." index="1"]
+
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.89537, 0 )
+layers = 1
+light_color = Color( 1, 1, 1, 1 )
+light_energy = 1.0
+light_indirect_energy = 1.0
+light_negative = false
+light_specular = 0.5
+light_bake_mode = 1
+light_cull_mask = -1
+shadow_enabled = false
+shadow_color = Color( 0, 0, 0, 1 )
+shadow_bias = 0.15
+shadow_contact = 0.0
+shadow_reverse_cull_face = false
+editor_only = false
+omni_range = 5.0
+omni_attenuation = 1.0
+omni_shadow_mode = 1
+omni_shadow_detail = 1
+
+

+ 57 - 0
tests/uv_tester_material.tres

@@ -0,0 +1,57 @@
+[gd_resource type="SpatialMaterial" load_steps=2 format=2]
+
+[ext_resource path="res://uvtester.jpg" type="Texture" id=1]
+
+[resource]
+
+render_priority = 0
+flags_transparent = false
+flags_unshaded = false
+flags_vertex_lighting = false
+flags_no_depth_test = false
+flags_use_point_size = false
+flags_world_triplanar = false
+flags_fixed_size = false
+flags_albedo_tex_force_srgb = false
+vertex_color_use_as_albedo = false
+vertex_color_is_srgb = false
+params_diffuse_mode = 0
+params_specular_mode = 0
+params_blend_mode = 0
+params_cull_mode = 0
+params_depth_draw_mode = 0
+params_line_width = 1.0
+params_point_size = 1.0
+params_billboard_mode = 0
+params_grow = false
+params_use_alpha_scissor = false
+albedo_color = Color( 1, 1, 1, 1 )
+albedo_texture = ExtResource( 1 )
+metallic = 0.0
+metallic_specular = 0.5
+metallic_texture_channel = 0
+roughness = 0.0
+roughness_texture_channel = 0
+emission_enabled = false
+normal_enabled = false
+rim_enabled = false
+clearcoat_enabled = false
+anisotropy_enabled = false
+ao_enabled = false
+depth_enabled = false
+subsurf_scatter_enabled = false
+transmission_enabled = false
+refraction_enabled = false
+detail_enabled = false
+uv1_scale = Vector3( 1, 1, 1 )
+uv1_offset = Vector3( 0, 0, 0 )
+uv1_triplanar = false
+uv1_triplanar_sharpness = 1.0
+uv2_scale = Vector3( 1, 1, 1 )
+uv2_offset = Vector3( 0, 0, 0 )
+uv2_triplanar = false
+uv2_triplanar_sharpness = 1.0
+proximity_fade_enable = false
+distance_fade_enable = false
+_sections_unfolded = [ "Albedo" ]
+

BIN
tests/uvtester.jpg


BIN
tests/uvtester_normals.jpg