Asset loading is unfinished warning

Hello,
I’m writing a small script that loads geometry from external files and then does some processing on the geometry. The script is called via blender in background mode from the command line.

One step in the processing is to apply “Auto Smooth”. With Blender 4.1 this will be replaced by a modifier. I found the command by which I can apply this modifier to the selected object:

bpy.ops.object.modifier_add_node_group(asset_library_type="ESSENTIALS", asset_library_identifier="", relative_asset_identifier="geometry_nodes\\smooth_by_angle.blend\\NodeTree\\Smooth by Angle")

This works fine if applied inside of Blender. But using my script I get

Warning: Asset loading is unfinished

and only an empty Geometry Nodes block is added to the Modifier stack. After a few objects that produced this warning it works for all following objects.

Looks to me as if the loading of the assets (where the Smoothing Geometry Node setup is stored) is not finished when my script starts to add geometry. Any idea if/how I can enforce waiting for the assets?

Thank you!

I don’t know exactly if is a problem of the user context or a matter of timing.

For user-context the case is that some operators might have special code in their poll method and under circumstances they are either allowed or not to run.

If is a matter of timing, the best thing you can try is to delay execution of your logic upon startup like this. I have used this techniques many times since Blender might be threaded, or context is built with Python during startup, so you need to allow a bit of time to pass first in order to make everything settle nicely first.

import bpy

if __name__ == "__main__":

    def run():
        print('hello world')

    bpy.app.timers.register(run, first_interval=1) # 1 second

I don’t know if also you need to see something else in terms of the command line arguments. Say for example that you use the --background just in case it makes any difference (you essentially exclude graphics and user mode).
https://docs.blender.org/manual/en/latest/advanced/command_line/arguments.html

Thank you. Sorry for the delayed reply but I was off for a few days.

The idea is good. And it works for delaying things in Blender if I use your snippet from inside of Blender. But when using the same lines in my script that runs in the background from the command line

blender my_blend.blend --background -P my_script.py 

the timer does not lead to the delay (and I do not get the printout on the command line). I guess I have to check about timers and background etc. My knowledge on this is rather limited :wink:

Very odd that this happens. Perhaps there is a special case that using the --background argument that causes certain infrastructure of Blender to be deactivated. Since UI and graphics are tightly coupled with event timers, perhaps there a specific reason that this sort of event callbacks are deactivated.

As for example trying other techniques as well, nothing worked.
https://docs.blender.org/api/current/bpy.app.timers.html

This however worked, that is based on good old threads.

import bpy
import time

def run():
    print('> RUN', time.ctime())

# starting
print('> ENTER', time.ctime())

# register the timer
bpy.app.timers.register(run, first_interval=1)

# do a fake blocking
time.sleep(2)
print('> END', time.ctime())


However the case is that Blender developers do not recommend threading. It is said that it might cause crashes. I have no clue about the details in this, however as it said the problem involves Graphics and Events, provided that you run with --background chances are that you might not cause such crashes (fingers crossed).
https://docs.blender.org/api/current/info_gotcha.html#strange-errors-when-using-the-threading-module

The other case to avoid threading, is definitely when you are on UI, running operators and such. There the problem has to do with ‘parallelism’ in that sense and not literally ‘multithreading’.

The final approach is to use safe timers but do not show the window:
blender -P delay.py --window-geometry 0 5000 0 0 --no-window-focus



import bpy
import time

def run():
    print('> RUN', time.ctime())

# starting
print('> ENTER', time.ctime())

# register the timer
bpy.app.timers.register(run, first_interval=1)

# do a fake blocking
time.sleep(2)
print('> END', time.ctime())


# quit the application
bpy.ops.wm.quit_blender()

If you manage to find something good, let me know. :slight_smile:

Thank you again. I agree, it might be something with the UI not loaded that makes things difficult with timers. But actually the timers are not really what I am looking for. I want to ensure that the “Smooth by Angle” node group is correctly loaded before I have to use it.

Your first idea does not work in background mode. Yes, the sleep delays the execution but as it blocks everything is also prevents Blender from continuing to load the Assets in the background.

Your second approach (window not showing) works fine with respect to the timer but it still does not ensure that the assets are loaded completely as I still get the warning message. Additionally the environment where my scripts will run are not guaranteed to have the ability to open a window (HPC systems).

If I find something working I will note it here!

Not 100% sure if it counts as “Issue” but I created a ticket for the developers:

If I am correct it is a common to use async loading in such cases, because is the only viable way to not hog the entire application. As you might have seen a few times in EEVEE that does partial texture loading, or perhaps partial light calculation and gradually enhances the quality until it becomes correct.

This works great in user mode since everything is done with the mindset of parallelism. However the real catch here is that you might have no exact way to tell what is going on with parallel states what is their status, about when an asset is fully loaded or still loading.

As in this case you might tell that Blender lacks this sort of function callback (ie: application handler) or even some sort of hardcoded property eg: bpy.context.load_status = DONE|LOADING) that tells exactly what is the current status of asset loading.

This is also quite interesting thing to think about, see if you can start conversation with developers, if they can suggest or come up with a good idea.

My very simple workaround here, to run the code as normal and use try-except blocks. If you get an exception that the asset is not available you will wait for 1 second and then loop again.

Something like this:

import bpy
import time

exec_ok = False
def run():
    # run first the questionable code
    print('> RUN', time.ctime())
    # at the end set the value to true
    exec_ok = True

# starting
print('> ENTER', time.ctime())

# register the timer
while not exec_ok:
    try:
        bpy.app.timers.register(run, first_interval=1)
    except:
        print('loading...')

print('> END', time.ctime())

Yes, I had a similar idea as in your last snippet. But as timers are limited in background Python scripts this does not work. Regardless of how long I let the script run I never get the “> RUN” printed nor the “loading…:”. Just the while loop is going on for ever.

Feedback from devs was that they confirm my findings and that it is related to the non-blocking way how Blender loads the assets (as you guessed). But there is not yet any proposal how to fix things. Let’s give them time :wink:

I found out that I might be able to work around temporarily. I will load all my geometry first and hopefully that leaves Blender enough time to load the assets in the background. Then, after all geometry is loaded, I will step through the objects and apply the modifier on each. So far I was loading the geometry and then applied the modifier directly. Some quick tests showed that this reordering of the loop might work. But of course this is a bit random as things then will depend on the complexity of my loaded geometry and the size of the loaded assets.

Thank you for all your comments. If I find a solution I will put it here.

Great, have a nice day.

Just a short recap… The topic is marked as known issue for now.

After testing many things I decided to manually create the related node setup with some Python code. Of course this is not very flexible when the modifier is changed in the future but on the other hand it works nicely and was a nice exercise for me in scripting geometry nodes…

If anyone is looking for an example feel free to grab ideas from here:

import math
import bpy
from bpy.app.translations import pgettext_data as data_

def create_geometry_node_group(object, name, has_initial_link=True):
    """create_geometry_node_group"""
    modifier = object.modifiers.new(name, "NODES")
    node_group = bpy.data.node_groups.new(name, "GeometryNodeTree")
    node_group.interface.new_socket(data_("Geometry"), in_out="INPUT", socket_type="NodeSocketGeometry")
    input_node = node_group.nodes.new("NodeGroupInput")
    input_node.select = False
    input_node.location.x = -200 - input_node.width

    node_group.interface.new_socket(data_("Geometry"), in_out="OUTPUT", socket_type="NodeSocketGeometry")
    output_node = node_group.nodes.new("NodeGroupOutput")
    output_node.is_active_output = True
    output_node.select = False
    output_node.location.x = 200

    if has_initial_link:
        node_group.links.new(node_group.nodes[data_("Group Input")].outputs[0], node_group.nodes[data_("Group Output")].inputs[0])

    node_group.is_modifier = True
    modifier.node_group = node_group

    return modifier, node_group

def add_auto_smooth_geometry_nodes(object, name=None):
    """add_auto_smooth_geometry_nodes"""

    if name is None:
        name = "Smooth by Angle"
    # create empty geometry nodes group
    modifier, node_group = create_geometry_node_group(object, name=name, has_initial_link=False)

    nodes = node_group.nodes

    geom_in = nodes.get(data_("Group Input"))
    geom_out = nodes.get(data_("Group Output"))

    # set some geometrical layout details
    dx = geom_in.width
    dy = geom_in.height
    x0 = -500
    x1 = x0 + 7.5 * dx

    # location of the input and output nodes
    geom_in.location = (x0, 0)
    geom_out.location = (x1, 0)

    # new Angle socket for input node
    angle_socket = node_group.interface.new_socket(data_("Angle"), in_out="INPUT", socket_type="NodeSocketFloat")
    angle_socket.subtype = "ANGLE"
    angle_socket.default_value = 30.0 / 180.0 * math.pi
    angle_socket.min_value = 0.0
    angle_socket.max_value = math.pi
    modifier[angle_socket.identifier] = 30.0 / 180.0 * math.pi

    # new Sharpness socket for input node
    sharpness_socket = node_group.interface.new_socket(data_("Ignore Sharpness"), in_out="INPUT", socket_type="NodeSocketBool")
    sharpness_socket.force_non_field = True

    # Edge Angle node
    node_edge_angle = nodes.new(data_("GeometryNodeInputMeshEdgeAngle"))
    node_edge_angle.label = "Edge Angle"
    node_edge_angle.location = (x0, 0 - 1.5 * dy)

    # Is Face smooth node
    node_is_face_smooth = nodes.new(data_("GeometryNodeInputShadeSmooth"))
    node_is_face_smooth.label = "Is Face Smooth"
    node_is_face_smooth.location = (x0, 0 - 2.5 * dy)

    # Is Edge smooth node
    node_is_edge_smooth = nodes.new(data_("GeometryNodeInputEdgeSmooth"))
    node_is_edge_smooth.label = "Is Edge Smooth"
    node_is_edge_smooth.location = (x0 + 0 * dx, 0 + 1.0 * dy)

    # Compare less or equal node
    node_less_than_or_equal = nodes.new(data_("FunctionNodeCompare"))
    node_less_than_or_equal.operation = "LESS_EQUAL"
    node_less_than_or_equal.location = (x0 + 1.5 * dx, 0 - 0.5 * dy)

    # Boolean OR node
    node_or_01 = nodes.new(data_("FunctionNodeBooleanMath"))
    node_or_01.operation = "OR"
    node_or_01.label = "OR_1"
    node_or_01.location = (x0 + 1.5 * dx, 0 - 2.5 * dy)

    # Boolean OR node
    node_or_02 = nodes.new(data_("FunctionNodeBooleanMath"))
    node_or_02.operation = "OR"
    node_or_02.label = "OR_2"
    node_or_02.location = (x0 + 3 * dx, 0 + 1.5 * dy)

    # Boolean AND node
    node_and = nodes.new(data_("FunctionNodeBooleanMath"))
    node_and.operation = "AND"
    node_and.label = "AND"
    node_and.location = (x0 + 3 * dx, 0)

    # Shade Smooth node
    node_set_smooth_edge = nodes.new(data_("GeometryNodeSetShadeSmooth"))
    node_set_smooth_edge.label = "Set Shade Smooth (Edge)"
    node_set_smooth_edge.domain = "EDGE"
    node_set_smooth_edge.location = (x0 + 4.5 * dx, 0)

    # Shade Smooth node
    node_set_smooth_face = nodes.new(data_("GeometryNodeSetShadeSmooth"))
    node_set_smooth_face.label = "Set Shade Smooth (Face)"
    node_set_smooth_face.domain = "FACE"
    node_set_smooth_face.inputs["Shade Smooth"].default_value = True
    node_set_smooth_face.location = (x0 + 6 * dx, 0)

    # Linking all the nodes
    node_group.links.new(node_edge_angle.outputs["Unsigned Angle"], node_less_than_or_equal.inputs["A"])
    node_group.links.new(geom_in.outputs["Angle"], node_less_than_or_equal.inputs["B"])

    node_group.links.new(geom_in.outputs["Ignore Sharpness"], node_or_01.inputs[0])
    node_group.links.new(node_is_face_smooth.outputs["Smooth"], node_or_01.inputs[1])

    node_group.links.new(node_less_than_or_equal.outputs["Result"], node_and.inputs[0])
    node_group.links.new(node_or_01.outputs["Boolean"], node_and.inputs[1])

    node_group.links.new(node_is_edge_smooth.outputs["Smooth"], node_or_02.inputs[0])
    node_group.links.new(geom_in.outputs["Ignore Sharpness"], node_or_02.inputs[1])

    node_group.links.new(geom_in.outputs["Geometry"], node_set_smooth_edge.inputs["Geometry"])
    node_group.links.new(node_or_02.outputs["Boolean"], node_set_smooth_edge.inputs["Selection"])
    node_group.links.new(node_and.outputs["Boolean"], node_set_smooth_edge.inputs["Shade Smooth"])

    node_group.links.new(node_set_smooth_edge.outputs["Geometry"], node_set_smooth_face.inputs["Geometry"])

    node_group.links.new(node_set_smooth_face.outputs["Geometry"], geom_out.inputs["Geometry"])


Hey FWIW I was confused about this variable naming but TIL single trailing underscore is an accepted convention to not override builtin types. However here I tink it’s overkill since data is not a reserved word in python ? Just my 2 cents.

Well, to be honest, I guess you can remove all the data_ code. But I copied it directly from the Blender sources and thus thought I keep it as it is (https://projects.blender.org/blender/blender/src/branch/main/scripts/startup/bl_operators/geometry_nodes.py). I am not saying this code snippet is perfect but it works for me and hopefully I can remove it in the future when the issue is fixed directly in Blender. On the other hand this modifier approach for “Auto smooth” is new in Blender 4.1 and we will see how it evolves.

Hey, folks. This annoying issue is happening with a seemingly simple script I just wrote, and I would like to understand what I should do prevent it.

I made this add-on (script) which is supposed to set the render resolution to whatever the active camera’s background image dimensions are. Somehow, the way it’s implemented interferes with applying Shade Auto-Smooth. I frequently get the warning message,

Warning: Asset loading is unfinished

It will work when I click Shade Auto-Smooth again right after, but I don’t understand exactly what causes the problem. My script is this (bl_info metadata is placeholder):

bl_info = {'...'}

import bpy

def camera_handler(scene):
    if not scene.camera or not scene.camera.data:
        return
        
    cam_data = scene.camera.data
    if cam_data.background_images:
        bg_img = cam_data.background_images[0].image
        if bg_img:
            scene.render.resolution_x = bg_img.size[0]
            scene.render.resolution_y = bg_img.size[1]

def register():
    bpy.app.handlers.depsgraph_update_post.append(camera_handler)

def unregister():
    if camera_handler in bpy.app.handlers.depsgraph_update_post:
        bpy.app.handlers.depsgraph_update_post.remove(camera_handler)

If I had to guess (and at this stage I do), it has something to do with how often this code is running via the handler type and it just happens to be doing this work when I try to apply Auto-Smooth.

What do you think I can do differently to prevent this interference? I only really need to change the render resolution when the user changes the active camera. Previous versions that did this check still encountered this Auto-Smooth logjam.

Thank you every/anyone for your input.

Sorry for not offering a solution, but the very same is happening in 5.0.1 when I try to apply the new Array modifier. Legacy and other modifiers works just fine - haven’t tried all though.