Python script to ease 3D mesh import iteration

Hi all, here’s a rather blunt script I’ve made to automatically generate files needed to go from throwing a .dae into a folder in your project, to being able to instantiate that model in your game with Lua.

Basic usage is like this: create a folder in your project main named objects. This will contain the script. Run the script once (I use python 3) and it will create the necessary folders. Otherwise within objects create a folder called data, and within that three folders called daes, gameobjects, and collections.

Export with your 3D editor the models for your game into the daes folder.

When you run the script again, it will generate the necessary .go, .collection, and .collectionfactory files. It will also generate text for a game object called factories containing all collection factories that you can paste into your main.collection, output to objects/all-factories.txt.

In your code, factories are then accessed via “factories#f.modelname”.

Note that it’s a very naive script and will only generate missing files, ie for new models you’ve added to the project, and it assumes the same material and texture for all of them. It would be easy enough to build a dictionary of models in your project and associate the materials and textures you want them to have, then generate the rest of the text files based on that. In theory that’s a purpose the “Model” type in defold could serve, but I found it was unnecessary as .go embeds all data.

If you start using 3D models you might run into two problems that bug me personally, working with Blender. The world position of your model on export either gets baked into the model, or imported from scene data by defold, so you need to clear transforms (or apply them) in order to control the correct origin on your object in defold. You also need to ensure your model only has one material applied, otherwise you’ll have missing faces if you only intend on applying one material in defold. This seems reasonable but if there’s a discrepancy between your model in your editor – for example, multi-material for baking textures, but only needing one textured material on export to engine – you’ll have to assign everything to the first material slot before export.

These two problems could be automated with a blender export script, which I’ll surely eventually write for myself to speed up iteration on my own projects, but for now just keep them in mind.

Here’s the simple file-generating script.

import os

material = "/main/materials/3d.material"
texture = "/main/images/ENVIRON.jpg"

gotext = '''
embedded_components {{
  id: "model"
  type: "model"
  data: "mesh: \\"/main/objects/data/daes/{0}.dae\\"\\nmaterial: \\"{1}\\"\\ntextures: \\"{2}\\"\\n"
  position {{
    x: 0.0
    y: 0.0
    z: 0.0
  }}
  rotation {{
    x: 0.0
    y: 0.0
    z: 0.0
    w: 1.0
  }}
}}
'''

colltext = '''
name: "{0}"
instances {{
  id: "{0}"
  prototype: "/main/objects/data/gameobjects/{0}.go"
  scale3 {{
    x: 1.0
    y: 1.0
    z: 1.0
  }}
}}
'''

facttext = '''
prototype: "/main/objects/data/collections/{0}.collection"
'''

facts_comp = '''
components {{
  id: \\"f.{0}\\"
  component: \\"/main/objects/{0}.collectionfactory\\"
  position {{
    x: 0.0
    y: 0.0
    z: 0.0
  }}
  rotation {{
    x: 0.0
    y: 0.0
    z: 0.0
    w: 1.0
  }}
}}
'''

facts_gotext = '''
embedded_instances {{
  id: "factories"
  data: "{0}"
  scale3 {{
    x: 1.0
    y: 1.0
    z: 1.0
  }}
}}
'''

directories = ["./data", "./data/daes", "./data/gameobjects", "./data/collections"]
for directory in directories:
    if not os.path.exists(directory):
        os.makedirs(directory)

for f in os.listdir("./data/daes/"):
    if f.endswith(".dae"):
        fname = f.split(".")[0]
    
        gopath = "./data/gameobjects/%s.go"%fname
        collpath = "./data/collections/%s.collection"%fname
        factpath = "./%s.collectionfactory"%fname
        
        if not os.path.exists(gopath):
            with open(gopath, 'w') as f:
                text = gotext.format(fname, material, texture)
                f.write(text)
        if not os.path.exists(collpath):
            with open(collpath, 'w') as f:
                text = colltext.format(fname)
                f.write(text)
        if not os.path.exists(factpath):
            with open(factpath, 'w') as f:
                text = facttext.format(fname)
                f.write(text)

all_factories_string = ""
for f in os.listdir("./"):
    if f.endswith(".collectionfactory"):
        fname = f.split(".")[0]
        all_factories_string += facts_comp.format(fname).replace("\n", "\\n")

with open("./all_factories.txt", 'w') as f:
    f.write(facts_gotext.format(all_factories_string))

Happy iterating!

6 Likes

Excellent stuff! Thank you for sharing this!

1 Like

Here’s my very simple, hackable script for Blender to quickly export selected objects. Good for iterating on the art. It temporarily removes location and applies the final material slot in the object to all faces, so the first texture slot in Defold picks up on all faces. (Maybe this is working around a bug in Defold’s model import, but I haven’t tried assigning multiple materials or textures to models on the Defold end to see if the invisible faces I was getting are in fact a feature.)

To use, open the text editor in Blender, paste the script, set the path at the top to the proper directory, and click Run Script. Selected models will be exported according to the settings in the script.

It doesn’t support animated models right now, but when I have need of that I’ll probably add it. Maybe by then I’ll also feel motivated to make an actual “Export for Defold Project” style addon for myself that combines both scripts’ features and has a convenient UI. But I haven’t been developing quite hard enough to need to go that far just yet. :slight_smile:

import bpy
from mathutils import Vector

outpath = "/path/to/your/branch/master/main/objects/data/daes/%s.dae"

def export_object(ob):
    original_location = ob.location.copy()
    ob.location = Vector((0.0, 0.0, 0.0))
    
    final_mat_slot = len(ob.material_slots) - 1
    original_materials = []
    for poly in ob.data.polygons:
        original_materials.append(poly.material_index)
        poly.material_index = final_mat_slot

    bpy.ops.wm.collada_export(
        filepath=outpath%ob.name,
        apply_modifiers=False,
        export_mesh_type=0,
        export_mesh_type_selection='view',
        selected=True,
        include_children=False,
        include_armatures=False,
        include_shapekeys=True,
        deform_bones_only=False,
        active_uv_only=False,
        include_uv_textures=False,
        include_material_textures=False,
        use_texture_copies=True,
        triangulate=True,
        use_object_instantiation=True,
        use_blender_profile=True,
        sort_by_name=False,
        export_transformation_type=0,
        export_transformation_type_selection='matrix',
        open_sim=False)

    ob.location = original_location

    i = 0
    for poly in ob.data.polygons:
        poly.material_index = original_materials[i]
        i += 1

objects = bpy.context.selected_objects.copy()

for ob in objects:
    ob.select = False

for ob in objects:
    ob.select = True
    export_object(ob)
    ob.select = False

for ob in objects:
    ob.select = True
5 Likes