Exception_Access_Violation caused by FBX export?

Hello.

I’m an experienced coder. But, not with python.

I’m trying to write a Blender script that, on button press, iterates through each scene within a blend file and exports specific armatures each as separate FBX files.

For some reason, if I uncomment the fbx export line, it crashes with an EAV error.

I’m not sure if I need to make a timer to wait for each export to finish before moving the loop forward? Other examples on the web seem to suggest that its ok to export multiple separate files without wait/timer schemes…

Or, does the FBX export trigger some kind of clean-up that causes some of my references to break? I read something about broken references causing EAV errors. But, I don’t follow that 100%. I tried a lot of experiments with references and cutting code up into functions, etc. But, no fix there.

Here is the relevant part of the code:

import bpy
import os

selected_objs = []
current_mode = ''
current_scene = None
fCount = 0
mCount = 0
bCount = 0

def record_state():

    global selected_objs
    global current_mode
    global current_scene

    # Record selected objects
    selected_objs = bpy.context.selected_objects

    # Record current mode
    current_mode = bpy.context.object.mode

    # Record current scene
    current_scene = bpy.context.scene

    # Switch to Object mode
    bpy.ops.object.mode_set ( mode = 'OBJECT' )

def restore_state():

    global selected_objs
    global current_mode
    global current_scene

    # Return to mode we were in
    bpy.ops.object.mode_set ( mode = current_mode )

    # Restore scene
    bpy.context.window.scene = current_scene

    # Restore the selections we had
    for o in selected_objs:
        o.select_set(True)



def publishObject(myObject, myScene):

    global selected_objs
    global current_mode
    global current_scene

    global fCount
    global mCount
    global gCount

    if myObject.type == 'ARMATURE':

        prefix = myObject.name[:9]

        if prefix == "FO4Export" and myObject.library == None:

            print("Found: > " + myObject.name_full)

            # Create parent dictionary
            parent = dict()
            for c in bpy.data.collections:
                parent[c] = None
            for c in bpy.data.collections:
                for ch in c.children:
                    parent[ch] = c

            if myScene.name[:5] == "Scene":

                # Store title based on file name
                title = bpy.path.basename(bpy.context.blend_data.filepath)[:-6]
            else:

                # Store title based on scene name
                title = myScene.name
            
            print("TITLE: " + title)

            # Store racecode based on the export armature name
            raceCode = myObject.name[10:]

            # Trim gender code off of racecode
            raceCode = raceCode[:2]

            if parent[myObject.users_collection[0]] == None:

                # Set gender as blank/unknown
                gender = ""

            else:

                # Get gender from root collection name
                gender = parent[myObject.users_collection[0]].name[:1]

            # Track the number of each gender
            if gender == "F":
                fCount = fCount + 1
                gCount = fCount
            elif gender == "M":
                mCount = mCount + 1
                gCount = mCount
            else:
                bCount = bCount + 1
                gCount = bCount

            # Record object visibility
            current_visibility = myObject.visible_get(view_layer=myScene.view_layers[0])

            # Unhide the target object
            myObject.hide_set(False, view_layer=myScene.view_layers[0])

            # Deselect all objects
            bpy.ops.object.select_all(action='DESELECT')

            # Select the target object
            myObject.select_set(True, view_layer=myScene.view_layers[0])

            # Create the relative file path
            file_path = bpy.path.abspath("//")

            # Create the path to parent based on removing "scenes\"
            parent_path = file_path[:-7]

            # If the parent path can be followed to confirm existence of "compile" folder, output to that folder
            # Otherwise, just use the relative path we made earlier
            if os.path.exists(os.path.join(parent_path, "compile")) == True:
                file_path = os.path.join(parent_path, "compile")

            cName = bpy.path.clean_name(title + "-" + raceCode + "-" + gender + str(gCount))

            # Create the full output path and file name
            obj_path = os.path.join(file_path, cName)
            
            print("EXPORT: " + obj_path)

            # Export the scene, using the selection
            bpy.ops.export_scene.fbx(filepath=obj_path + ".fbx", use_selection=True)

            # Return visibility of the target object to what it was before
            myObject.hide_set(current_visibility == False, view_layer=myScene.view_layers[0])

def publishScene(myScene):

    global fCount
    global mCount
    global gCount

    fCount = 0
    mCount = 0
    bCount = 0

    print("Found: " + myScene.name_full)

    bpy.context.window.scene = myScene

    objectList = myScene.collection.all_objects

    for obj in objectList:

        publishObject(obj, myScene)




# Selects each deform armature and publishes it as a separate FBX
class PublishOperator(bpy.types.Operator):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'TOOL_PROPS'
    bl_idname = "publish.fo4"
    bl_label = "Publish"
    bl_category = "Tools"

    def execute(self, context):

        self.report({'INFO'}, 'Started Fallout 4 publish.')

        record_state()

        sceneList = bpy.data.scenes
        for scene in sceneList:

            publishScene(scene)
        
        restore_state()

        self.report({'INFO'}, 'Fallout 4 publish complete.')
        
        return {'FINISHED'} 

Any help would be greatly appreciated!

Hello ! Don’t store pointers to objects, store object names and fetch them afterwards. Object pointers are dynamically reallocated under certain conditions and it is advised against storing them.

eg

    selected_objs = [o.name for o in bpy.context.selected_objects]
    current_scene = bpy.context.scene.name

then

[o.select_set(True) for o in bpy.data.objects if o.name in selected_objs]
bpy.context.window.scene = bpy.data.scenes[current_scene]

BTW I suggest you use blender properties instead of global variables

I think the issue is not the pointers, but the global variables. I could have read the code wrong, but it looks like the record state is the only thing that generates the state data, and restore state consumes it. I’d create a state class that contains all of those variables you’re interested in and have the record state create that and the restore state use it.

But, basically what’s happening is if you run this code, your select list will get a bunch of objects. If the objects are deleted, then that list contains stale data. Either you have to make sure that list is clean when you run that code, or replace the use of globals with arguments and return values (the better option). Using globals like this is generally reserved for doing things like pre-generating data or caching data, but using it to pass data around from one function to another is not great. If you want to pass data around from function to function, then you’d want to use a class or make use of return values and arguments.

I’m on my phone right now, but I can create some example code when I’m on my computer if interested.

Thank you. This is super helpful and makes sense.

I will go through the code to apply this and report back.

Got it. I will remove the globals and make a var storage class instead.

I appreciate the offer for example code. But, I think I understand and can build it. I’d rather preserve your time for potential help if I get stuck after! :wink:

I might need a day or two to get to this. But, will report back.

[Actually, I did think of a few quick questions:

  1. When referencing functions within other classes, can I just write: “MyVarHolderClass.getSavedScene()”? (In other languages that would only work for global functions. Otherwise would need to make class instances…)

  2. Do I even need to make these state functions? In other words, is there a more elegant way to save the current state and restore to it after running some code manipulations? Seems like this would be a very common problem…]

I think what you are looking for is operator override. This is a vast topic and you’ll get tons of info about it if you use these keywords on your fav. Search engine. But basically operators in Blender are first and foremost designed to work with the user interface, so they rely on a bunch of information available from the context that gets passed to it when you click on it, like the currently selected objects, or the editor the you’re currently hovering, etc. But you can pass some custom data by overriding the operator with curated information. Its a bit tricky because the override arguments are poorly documented but you can get there either via trial and error or a lucky google search.

In your case it may solve this state problem altogether because you wont care which objects are selected when you export, you’ll pass whatever objects you want whether they’re selected or not

Ok. So I tried to make a state class as you recommend. But, I think I must be missing something. It appears that it’s not retaining the class variables between function calls.

This is the class I wrote:

class BlenderState():

    selected_objs = []
    saved_mode = ''
    saved_scene = None

    def record():

        # Record selected objects
        selected_objs = bpy.context.selected_objects

        # Record current mode
        saved_mode = bpy.context.object.mode

        # Record current scene
        saved_scene = bpy.context.scene

        # Switch to Object mode
        bpy.ops.object.mode_set ( mode = 'OBJECT' )

        print("WAS: " + saved_mode)

    def restore():

        try:

            print("NOW: " + saved_mode)
            # Return to mode we were in
            bpy.ops.object.mode_set(mode=saved_mode)

            # Restore scene
            bpy.context.window.scene = saved_scene

            # Restore the selections we had
            for o in selected_objs:
                o.select_set(True)
        except Exception as e:
            print("Error restoring state:", e)

Then, I later call that like this:

BlenderState.record()

'''
other code
'''
  
BlenderState.restore()

But, that results in this error (as if the whole class is basically re-started, erasing its vars):

WAS: OBJECT
Error restoring state: name 'saved_mode' is not defined

I see that I can store variables into the Blender data-blocks. And it appears typical for coders to store in the scene block. But, since my script needs to iterate through scenes before returning to these state vars, storing on scene seems a little problematic. These should be vars that only exist during execution and dont get saved to any data-blocks.

Any advice?

Thank you.

This does sound like what I’m after. But, I got a little lost on how it would be achieved with overrides?

I’m familiar with the concept of overriding in general. But, may not follow how it would help in this context/python. Are you saying that I would override the fbx export function and add some custom parameters and routines to deal with the custom export I’m shooting for?

In the context of the blender python api an operator override doesnt share the same meaning as in other languages. You have to use a certain syntax when you call the operator using with bpy.context.temp_override(override_parameters): bpy.ops.your_operator()

See https://blender.stackexchange.com/questions/248274/a-comprehensive-list-of-operator-overrides and related links for more info

I see.

I think I follow.

Correct me if I’m wrong. But, I think you are saying that if I use these Blender-context overrides, I can achieve my export goals without having to dance around with the states/visibility, etc. at all.

I will dig further into how that works.

You’re almost there. There’s a few ways to do what you’re doing.

Plain Old Python

import bpy


class BlenderState:
    def __init__(self) -> None:
        self.selected_objects: list = []
        self.saved_mode: str = ""
        self.saved_scene: bpy.types.Scene | None = None

    def record(self) -> None:
        # NOTE: The self parameter is very important here. This will basically 
        # say "carry the state in the instance of the class."

        # Record selected objects
        self.selected_objs = bpy.context.selected_objects

        # Record current mode
        self.saved_mode = bpy.context.object.mode

        # Record current scene
        self.saved_scene = bpy.context.scene

        # Switch to Object mode
        bpy.ops.object.mode_set(mode='OBJECT')

        print(f"WAS: {self.saved_mode}")

    def restore(self):
        ...


def main():
    state = BlenderState()
    state.record()
    # Do something
    state.restore()

Data Classes

import dataclasses
import bpy


@dataclasses.dataclass
class BlenderState:
    selected_objects: list = dataclasses.field(default_factory=list)
    saved_mode: str = ""
    saved_scene: bpy.types.Scene | None = None

    def record(self) -> None:
        # NOTE: The self parameter is very important here. This will basically 
        # say "carry the state in the instance of the class."

        # Record selected objects
        self.selected_objs = bpy.context.selected_objects

        # Record current mode
        self.saved_mode = bpy.context.object.mode

        # Record current scene
        self.saved_scene = bpy.context.scene

        # Switch to Object mode
        bpy.ops.object.mode_set(mode='OBJECT')

        print(f"WAS: {self.saved_mode}")

    def restore(self):
        ...


def main():
    state = BlenderState()
    state.record()
    # Do something
    state.restore()

Context Manager

import contextlib

import bpy


@contextlib.contextmanager
def blender_state():
    # Record current state

    # Record selected objects
    selected_objs = bpy.context.selected_objects

    # Record current mode
    saved_mode = bpy.context.object.mode

    # Record current scene
    saved_scene = bpy.context.scene

    # Switch to Object mode
    bpy.ops.object.mode_set ( mode = 'OBJECT' )

    print(f"WAS: {saved_mode}")

    # Send the state to the caller
    yield {
        "selected_objs": selected_objs,
        "saved_mode": saved_mode,
        "saved_scene": saved_scene
    }

    # Restore everything
    ...


def main():
    # Before getting the state...

    with blender_state() as state:
        # Do something with the scene
        selected_objs = state["selected_objs"]
        ...

    # After resetting the state back to its original state...

There may be a few others, but this might help.

Right on! Thank you. I went with the first “Plain Old Python” solution and it worked. This gives me a much better idea of how to structure things. I already made another similar class to track the character counts.

The bad news is that it didn’t fix my crash issue. I even completely commented out calling the state class and it still crashes.

It’s odd because sometimes it publishes without crashing. Other times it finishes publishing and then immediately crashes. All with the same code.

I’m going to work on it some more to see if I can fix or narrow the issue down to ask a more precise question here…

This ended up being the solution to the crash issue. Once I iterated through names instead of the pointer the exports became stable.

I guess the nuance others might get reading this is that exporting files apparently can trigger some type of clean-up under the hood. So, it can look like the exporting itself is the issue when its really a matter of storing references more safely.

Thank you.

And thank you also to @propersquid !

Alright, sorry I was on my phone so I couldn’t write much code. Here’s a very simple snippet :

import bpy

objects_to_export = [bpy.data.objects["Cube"], bpy.data.objects["Suzanne"]] # ,etc
# Note you can export only one object but it must be contained in a list

with bpy.context.temp_override(selected_objects=objects_to_export):
    bpy.ops.export_scene.fbx(filepath=r"C:\Users\nhild\Desktop\test\test.fbx", use_selection=True)

Effectively this passes custom data to the operator, overriding its default behaviour. This solves :

  • Having to store / restore a selection state
  • Having to hide / unhide objects because an object can’t be selected if it is hidden. Overriding doesn’t care if the object is hidden since you don’t actually select it.
  • This will however set the exported object’s mode to OBJECT so you might still need this part of your code

So you can get rid of all the boilerplate related to storing and restoring the selection state. This is why the temp_override feature exists.

Further reading :

https://docs.blender.org/api/current/bpy.types.Context.html#bpy.types.Context.temp_override

Thank you. This is much cleaner. I will work on implementing this way.