1
$\begingroup$

I'm writing an add-on that exports objects to FBX file format.

One of its features will be the ability to merge another object for export. So the user selects object A, clicks the "choose merge object" button, then clicks object B. During export, the objects are combined as per Ctrl+J type operation, exported as a single object, then everything is restored to normal (back to 2 objects).

This is my idea:

  1. Duplicate both objects - remaining steps deal with copies
  2. Set B as selected and A as active
  3. Use bpy.ops.object.join()
  4. Export A (active_object)
  5. Delete A

I have a couple questions regarding this if anyone might know something about it..

X. Is there an alternative to avoid the need to create temporaries and delete them afterward? The objects could be any complexity imaginable, so any way to use the inputs as read-only would be better. Some alternate merger? A+B=C instead of A+=delete(B)?

Edit: I later found out that you can duplicate an object as a link (which doesn't copy mesh data), then use that linked duplicate as an input object for join(). This is very similar to not needing to create temporaries because the mesh data is never duplicated.

Y. In the scenario above using join(), would the temporary copy of B be deleted automatically? Not sure how join() works under the hood. And a little worried about messing around with a deleted object reference in code.

Edit: Yes, all input objects except the active one get deleted by join().

I really appreciate any advice. I'm very new to python and add-on development, so details are also appreciated.

$\endgroup$
2
  • 1
    $\begingroup$ Sounds like a job for our good old friend: Undo. I know it sounds stupid, but... you know. $\endgroup$
    – brockmann
    Sep 5, 2019 at 21:54
  • $\begingroup$ Does undo work efficiently in these situations? Also, I'm not knowledgeable (at all) about how Blender's undo system works. I've noticed that it sometimes seems to be restoring the entire scene/file when I undo certain changes. Some things that changed before the undo action are undone as a side effect. I would be worried that with my limited knowledge of it, I may cause more damage than good. $\endgroup$
    – Robert
    Sep 5, 2019 at 22:38

2 Answers 2

2
$\begingroup$

Sounds like a job for our good old friend: Undo. You can make it complicated for sure, but since the operation is in memory anyway, it should make it pretty fast.

import bpy

from bpy.props import (BoolProperty, FloatProperty, StringProperty)
from bpy.types import (Operator)
from bpy_extras.io_utils import ExportHelper

# ExportHelper is a helper class, defines filename and
# invoke() function which calls the file selector.
class EXPORT_OT_customFBX(Operator, ExportHelper):
    """Export the scene to FBX"""
    bl_idname = "export_scene.custom_fbx"
    bl_label = "Export FBX"

    # ExportHelper mixin class uses this
    filename_ext = ".fbx"

    filter_glob: StringProperty(
        default="*.fbx",
        options={'HIDDEN'},
        maxlen=255,  # Max internal buffer length, longer would be clamped.
    )

    # List of operator properties, the attributes will be assigned
    # to the class instance from the operator settings before calling.
    global_scale: FloatProperty(
        name="Scale",
        description="Scale",
        default=1.0,
    )

    use_subsurf: BoolProperty(
        name="Use Subsurf",
        description="Use Subsurf",
        default=False,
    )

    apply_unit_scale: BoolProperty(
        name="Apply Unit Scale",
        description="Use Subsurf",
        default=True,
    )

    def execute(self, context):

        viewport_selection = [o for o in context.selected_objects if o.type == 'MESH']

        if len(viewport_selection) == 2:
            if context.active_object in viewport_selection:
                # Join! 
                # https://blender.stackexchange.com/q/13986
                # https://blender.stackexchange.com/q/50160
                bpy.ops.object.join()
            else:
                print ("Can not call join operator")
        else:
            print ("Nothing to join.")

        # Export
        bpy.ops.export_scene.fbx(
                filepath=self.filepath,
                global_scale=self.global_scale, 
                apply_unit_scale=self.apply_unit_scale, 
                use_subsurf=self.use_subsurf,
                use_metadata=True, 
                axis_forward='-Z', 
                axis_up='Y'
            )

        # Undo!
        bpy.ops.ed.undo()
        return {'FINISHED'} 

# Only needed if you want to add into a dynamic menu
def draw_export_fbx(self, context):
    self.layout.operator(EXPORT_OT_customFBX.bl_idname, text="Custom FBX (.fbx)", icon="MESH_MONKEY")


# Registration
classes = (
    EXPORT_OT_customFBX,
)

def register():
    from bpy.utils import register_class
    for cls in classes:
        register_class(cls)

    bpy.types.TOPBAR_MT_file_export.prepend(draw_export_fbx)


def unregister():
    from bpy.utils import unregister_class
    for cls in reversed(classes):
        unregister_class(cls)

    bpy.types.TOPBAR_MT_file_export.remove(draw_export_fbx)


if __name__ == "__main__":
    register()

    # test call
    bpy.ops.export_scene.custom_fbx('INVOKE_DEFAULT')

Operator is based on Templates > Python > Operator File Export


Another quite boring approach is saving the file before exporting to fbx and then reload the blend:

def execute(self, context):

    # Save!
    if bpy.data.is_dirty:
        bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)

    viewport_selection = [o for o in context.selected_objects if o.type == 'MESH']

    if len(viewport_selection) == 2:
        if context.active_object in viewport_selection:
            # Join! # https://blender.stackexchange.com/q/13986
            bpy.ops.object.join()
        else:
            print ("Can not call join operator")
    else:
        print ("Nothing to join.")

    # Export
    bpy.ops.export_scene.fbx(
            filepath=self.filepath,
            global_scale=self.global_scale, 
            apply_unit_scale=self.apply_unit_scale, 
            use_subsurf=self.use_subsurf,
            use_metadata=True, 
            axis_forward='-Z', 
            axis_up='Y'
        )

    # Reload
    bpy.ops.wm.open_mainfile(filepath=bpy.data.filepath)
    return {'FINISHED'} 

Might be nice doing some tests to measure execution time of both... in memory.

$\endgroup$
4
  • $\begingroup$ Thanks for the detailed information! Does the undo system work based off of operations? In the sense that one operation = one undo? The file saving concept is also a clever idea and simple way to handle it. I would just be worried that many developers probably make temporary edits to their models right before export (part of the reason I'm trying to make this add-on). Maybe if I saved to a temporary folder, it could work. But if the undo system works this easily, that seems like a good way to handle it. Thanks again! $\endgroup$
    – Robert
    Sep 6, 2019 at 11:19
  • $\begingroup$ Everything is an operation @Robert. Not sure what you mean by developers make temporary edits? Do you mean artists? What developers or artists can do with the models exactly? To be on the save side, you can increment the version of the blend as well as using the second approach to save before exporting it and even report that the file is not ready for export in case it's not saved by using the report method of the operator: self.report({'ERROR'}, "Save!") $\endgroup$
    – brockmann
    Sep 6, 2019 at 11:32
  • $\begingroup$ I mean models inside of Blender are rarely in the correct state for a game engine. Sometimes they need to be optimized or reconfigured in a way that makes destructive changes to the model. So the artist would make these changes, export, then undo, reload, or close without saving. The more changes needed, the more likely they would close or reload instead of using undo. So if I save their scene during export, I would make their temporary change permanent. It could probably be avoided by saving to an alternate file. $\endgroup$
    – Robert
    Sep 6, 2019 at 19:15
  • $\begingroup$ Ok get it. Should I implement the report method I mentioned? @Robert $\endgroup$
    – brockmann
    Sep 6, 2019 at 19:27
0
$\begingroup$

I don't see the need to join the objects at all inside Blender. Since you are? writing the fbx export code, just merge the triangle/texture/animation data for the two meshes yourself and write it out to the fbx format as a single object.

$\endgroup$
3
  • $\begingroup$ I'm not writing the entire exporter. Mearly executing the built in FBX exporter with extended options like different batch modes and prefix/suffix options. Primarily intended for game development (assets, low poly, high poly). For example, the user can mark a specific collection or object as "high poly", then that gets exported to a specific directory and/or with different prefix. Merging objects is another feature. But I would not want to try recoding the file parsers. $\endgroup$
    – Robert
    Sep 5, 2019 at 21:17
  • 1
    $\begingroup$ I see. Sorry for the confusion. I would say that your approach is good. Doing some practice runs on my setup seems to show that B no longer exists after merging with A. $\endgroup$ Sep 5, 2019 at 21:24
  • $\begingroup$ Thanks for checking that out. I'm surprised Blender doesn't have some type of advanced mesh joining function. I figured Join() was probably calling something else behind the scenes. Maybe I can try to actually find that function to find out when I get back to a computer $\endgroup$
    – Robert
    Sep 5, 2019 at 21:33

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .