diff --git a/tools/gltf_auto_export/__init__.py b/tools/gltf_auto_export/__init__.py index 6defe3f..7f544fb 100644 --- a/tools/gltf_auto_export/__init__.py +++ b/tools/gltf_auto_export/__init__.py @@ -213,7 +213,6 @@ def register(): """bpy.utils.register_class(AutoExportExtensionProperties) bpy.types.Scene.AutoExportExtensionProperties = bpy.props.PointerProperty(type=AutoExportExtensionProperties)""" - def unregister(): for cls in classes: bpy.utils.unregister_class(cls) diff --git a/tools/gltf_auto_export/auto_export/auto_export.py b/tools/gltf_auto_export/auto_export/auto_export.py index 3802c85..469fb23 100644 --- a/tools/gltf_auto_export/auto_export/auto_export.py +++ b/tools/gltf_auto_export/auto_export/auto_export.py @@ -83,7 +83,7 @@ def auto_export(changes_per_scene, changed_export_parameters, addon_prefs): print("EXPORTING") # get blueprints/collections infos (collections, collections_to_export, library_collections, collections_per_scene) = get_collections_to_export(changes_per_scene, changed_export_parameters, addon_prefs) - + # get level/main scenes infos (main_scenes_to_export) = get_levels_to_export(changes_per_scene, changed_export_parameters, addon_prefs) diff --git a/tools/gltf_auto_export/auto_export/get_collections_to_export.py b/tools/gltf_auto_export/auto_export/get_collections_to_export.py index 9328225..93733d5 100644 --- a/tools/gltf_auto_export/auto_export/get_collections_to_export.py +++ b/tools/gltf_auto_export/auto_export/get_collections_to_export.py @@ -6,6 +6,7 @@ from ..helpers.helpers_collections import get_exportable_collections from ..helpers.helpers_collections import (get_collections_in_library, get_exportable_collections, get_collections_per_scene, find_collection_ascendant_target_collection) from ..helpers.helpers_scenes import (get_scenes, ) +# TODO: this should also take the split/embed mode into account: if a nested collection changes AND embed is active, its container collection should also be exported def get_collections_to_export(changes_per_scene, changed_export_parameters, addon_prefs): export_change_detection = getattr(addon_prefs, "export_change_detection") export_gltf_extension = getattr(addon_prefs, "export_gltf_extension", ".glb") @@ -16,7 +17,7 @@ def get_collections_to_export(changes_per_scene, changed_export_parameters, addo (collections, blueprint_hierarchy) = get_exportable_collections(level_scenes, library_scenes, addon_prefs) collections_to_export = collections # just for clarity - print("export_change_detection", export_change_detection, "changed_export_parameters", changed_export_parameters, "changes_per_scene", changes_per_scene) + # print("export_change_detection", export_change_detection, "changed_export_parameters", changed_export_parameters, "changes_per_scene", changes_per_scene) # if the export parameters have changed, bail out early # we need to re_export everything if the export parameters have been changed diff --git a/tools/gltf_auto_export/auto_export/get_levels_to_export.py b/tools/gltf_auto_export/auto_export/get_levels_to_export.py index 78adf54..4aba3a5 100644 --- a/tools/gltf_auto_export/auto_export/get_levels_to_export.py +++ b/tools/gltf_auto_export/auto_export/get_levels_to_export.py @@ -2,6 +2,7 @@ import bpy from .export_blueprints import check_if_blueprint_on_disk from ..helpers.helpers_scenes import (get_scenes, ) +# TODO: this should also take the split/embed mode into account: if a collection instance changes AND embed is active, its container level/world should also be exported def get_levels_to_export(changes_per_scene, changed_export_parameters, addon_prefs): export_change_detection = getattr(addon_prefs, "export_change_detection") export_gltf_extension = getattr(addon_prefs, "export_gltf_extension") @@ -9,6 +10,7 @@ def get_levels_to_export(changes_per_scene, changed_export_parameters, addon_pre [main_scene_names, level_scenes, library_scene_names, library_scenes] = get_scenes(addon_prefs) + # print("levels export", "export_change_detection", export_change_detection, "changed_export_parameters",changed_export_parameters, "export_models_path", export_models_path, "export_gltf_extension", export_gltf_extension, "changes_per_scene", changes_per_scene) # determine list of main scenes to export # we have more relaxed rules to determine if the main scenes have changed : any change is ok, (allows easier handling of changes, render settings etc) main_scenes_to_export = [scene_name for scene_name in main_scene_names if not export_change_detection or changed_export_parameters or scene_name in changes_per_scene.keys() or not check_if_blueprint_on_disk(scene_name, export_models_path, export_gltf_extension)] diff --git a/tools/gltf_auto_export/auto_export/operators.py b/tools/gltf_auto_export/auto_export/operators.py index ed19de5..9dea855 100644 --- a/tools/gltf_auto_export/auto_export/operators.py +++ b/tools/gltf_auto_export/auto_export/operators.py @@ -10,12 +10,18 @@ from .auto_export import auto_export from ..helpers.generate_complete_preferences_dict import generate_complete_preferences_dict_auto from ..helpers.serialize_scene import serialize_scene +def bubble_up_changes(object, changes_per_scene): + if object.parent: + changes_per_scene[object.parent.name] = bpy.data.objects[object.parent.name] + bubble_up_changes(object.parent, changes_per_scene) + + class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper): """auto export gltf""" #bl_idname = "object.xxx" bl_idname = "export_scenes.auto_gltf" bl_label = "Apply settings" - bl_options = {'PRESET', 'UNDO'} + bl_options = {'PRESET'} # we do not add UNDO otherwise it leads to an invisible operation that resets the state of the saved serialized scene, breaking compares for normal undo/redo operations # ExportHelper mixin class uses this filename_ext = '' @@ -203,15 +209,28 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper): return changed def did_objects_change(self): - previous_stored = bpy.data.texts[".TESTING"] if ".TESTING" in bpy.data.texts else None # bpy.data.texts.new(".TESTING") + # sigh... you need to save & reset the frame otherwise it saves the values AT THE CURRENT FRAME WHICH CAN DIFFER ACROSS SCENES + current_frames = [scene.frame_current for scene in bpy.data.scenes] + for scene in bpy.data.scenes: + scene.frame_set(0) + + current_scene = bpy.context.window.scene + bpy.context.window.scene = bpy.data.scenes[0] + #serialize scene at frame 0 + """with bpy.context.temp_override(scene=bpy.data.scenes[1]): + bpy.context.scene.frame_set(0)""" current = serialize_scene() + bpy.context.window.scene = current_scene + + # reset previous frames + for (index, scene) in enumerate(bpy.data.scenes): + scene.frame_set(int(current_frames[index])) + + previous_stored = bpy.data.texts[".TESTING"] if ".TESTING" in bpy.data.texts else None # bpy.data.texts.new(".TESTING") if previous_stored == None: - print("setting bla") previous_stored = bpy.data.texts.new(".TESTING") previous_stored.write(current) return {} - - previous = json.loads(previous_stored.as_string()) current = json.loads(current) @@ -219,7 +238,6 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper): # TODO : how do we deal with changed scene names ??? for scene in current: print('scene', scene) - changes_per_scene[scene] = {} previous_object_names = list(previous[scene].keys()) current_object_names =list(current[scene].keys()) #print("previous_object_names", len(previous_object_names), previous_object_names) @@ -231,12 +249,16 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper): print("added")""" added = list(set(current_object_names) - set(previous_object_names)) removed = list(set(previous_object_names) - set(current_object_names)) - print("removed", removed) - print("added",added) + """print("removed", removed) + print("added",added)""" for obj in added: + if not scene in changes_per_scene: + changes_per_scene[scene] = {} changes_per_scene[scene][obj] = bpy.data.objects[obj] # TODO: how do we deal with this, as we obviously do not have data for removed objects ? for obj in removed: + if not scene in changes_per_scene: + changes_per_scene[scene] = {} changes_per_scene[scene][obj] = None # bpy.data.objects[obj] for object_name in list(current[scene].keys()): # todo : exclude directly added/removed objects @@ -249,19 +271,31 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper): if "Camera" in object_name: pass#print(" current", current_obj, prev_obj) - if "Fox" in object_name: + """if "Fox" in object_name: print(" current", current_obj) print(" previou", prev_obj) - print(" same?", same) + print(" same?", same)""" #print("foo", same) if not same: + """ print(" current", current_obj) + print(" previou", prev_obj)""" + if not scene in changes_per_scene: + changes_per_scene[scene] = {} + changes_per_scene[scene][object_name] = bpy.data.objects[object_name] + bubble_up_changes(bpy.data.objects[object_name], changes_per_scene[scene]) + # now bubble up for instances & parents + + - """if len(current[scene]) != len(previous[scene]) : - print("toto")""" + previous_stored.clear() previous_stored.write(json.dumps(current)) + + + print("changes per scene alternative", changes_per_scene) + return changes_per_scene def execute(self, context): @@ -273,12 +307,12 @@ class AutoExportGLTF(Operator, AutoExportGltfAddonPreferences, ExportHelper): self.save_settings(context) if self.auto_export: # only do the actual exporting if auto export is actually enabled - changes_per_scene = context.window_manager.auto_export_tracker.changed_objects_per_scene - changes_per_scene_2 = self.did_objects_change() + #changes_per_scene = context.window_manager.auto_export_tracker.changed_objects_per_scene #& do the export if self.direct_mode: #Do not auto export when applying settings in the menu, do it on save only # determine changed objects + changes_per_scene = self.did_objects_change() # determine changed parameters params_changed = self.did_export_settings_change() auto_export(changes_per_scene, params_changed, self) diff --git a/tools/gltf_auto_export/auto_export/tracker.py b/tools/gltf_auto_export/auto_export/tracker.py index 7baed73..571a45d 100644 --- a/tools/gltf_auto_export/auto_export/tracker.py +++ b/tools/gltf_auto_export/auto_export/tracker.py @@ -150,6 +150,7 @@ class AutoExportTracker(PropertyGroup): #print(" changed object", obj.id.name, "changes", obj, "evalutated", obj.id.is_evaluated, "transforms", obj.is_updated_transform, "geometry", obj.is_updated_geometry) if obj.is_updated_transform or obj.is_updated_geometry: cls.changed_objects_per_scene[scene.name][obj.id.name] = object + elif isinstance(obj.id, bpy.types.Material): # or isinstance(obj.id, bpy.types.ShaderNodeTree): # print(" changed material", obj.id, "scene", scene.name,) material = bpy.data.materials[obj.id.name] @@ -158,19 +159,21 @@ class AutoExportTracker(PropertyGroup): for slot in obj.material_slots: if slot.material == material: cls.changed_objects_per_scene[scene.name][obj.name] = obj - + #print("changed_objects_per_scene", cls.changed_objects_per_scene) + """for obj_name_original in cls.changed_objects_per_scene[scene_name]: + if obj_name_original != ls.changed_objects_per_scene[scene_name][obj_name_original]""" items = 0 for scene_name in cls.changed_objects_per_scene: items += len(cls.changed_objects_per_scene[scene_name].keys()) if items == 0: cls.changed_objects_per_scene.clear() - # print("changed_objects_per_scene", cls.changed_objects_per_scene) + #print("changed_objects_per_scene", cls.changed_objects_per_scene) # filter out invalid objects - for scene_name in cls.changed_objects_per_scene.keys(): + """for scene_name in cls.changed_objects_per_scene.keys(): bla = {} for object_name in cls.changed_objects_per_scene[scene.name]: - object = cls.changed_objects_per_scene[scene.name][object_name] + object = cls.changed_objects_per_scene[scene.name][object_name]""" #print("sdfsd", object, object.valid) #if not cls.changed_objects_per_scene[scene.name][object_name].invalid: # bla[object_name] = cls.changed_objects_per_scene[scene.name][object_name] @@ -181,7 +184,7 @@ class AutoExportTracker(PropertyGroup): # keep it simple, just use Simplenamespace for compatibility with the rest of our code # TODO: debounce - export_settings_changed = did_export_settings_change() + """export_settings_changed = did_export_settings_change() tmp = {} for k in AutoExportGltfAddonPreferences.__annotations__: item = AutoExportGltfAddonPreferences.__annotations__[k] @@ -217,7 +220,7 @@ class AutoExportTracker(PropertyGroup): except Exception as error: pass #self.report({"ERROR"}, "Failed to populate list of exported collections/blueprints") - + """ """depsgraph = bpy.context.evaluated_depsgraph_get() for update in depsgraph.updates: print("update", update)""" diff --git a/tools/gltf_auto_export/helpers/helpers_scenes.py b/tools/gltf_auto_export/helpers/helpers_scenes.py index 4cb567a..7bc0b63 100644 --- a/tools/gltf_auto_export/helpers/helpers_scenes.py +++ b/tools/gltf_auto_export/helpers/helpers_scenes.py @@ -214,8 +214,6 @@ def clear_hollow_scene(temp_scene, original_root_collection): for child_collection in collection.children: restore_original_names(child_collection) - # reset original names - restore_original_names(original_root_collection) # remove any data we created temp_root_collection = temp_scene.collection @@ -223,8 +221,12 @@ def clear_hollow_scene(temp_scene, original_root_collection): for object in temp_scene_objects: #print("removing", object.name) bpy.data.objects.remove(object, do_unlink=True) + # remove the temporary scene bpy.data.scenes.remove(temp_scene, do_unlink=True) + + # reset original names + restore_original_names(original_root_collection) # convenience utility to get lists of scenes def get_scenes(addon_prefs): diff --git a/tools/gltf_auto_export/helpers/serialize_scene.py b/tools/gltf_auto_export/helpers/serialize_scene.py index d1be213..060afe4 100644 --- a/tools/gltf_auto_export/helpers/serialize_scene.py +++ b/tools/gltf_auto_export/helpers/serialize_scene.py @@ -1,8 +1,7 @@ import json import numpy as np - import bpy - +from ..constants import TEMPSCENE_PREFIX fields_to_ignore_generic = ["tag", "type", "update_tag", "use_extra_user", "use_fake_user", "user_clear", "user_of_id", "user_remap", "users", 'animation_data_clear', 'animation_data_create', 'asset_clear', 'asset_data', 'asset_generate_preview', 'asset_mark', 'bl_rna', 'evaluated_get', @@ -19,18 +18,18 @@ def mesh_hash(obj): h = str(hash(vertices_np.tobytes())) return h +# TODO: redo this one, this is essentially modifiec copy & pasted data, not fitting def animation_hash(obj): animation_data = obj.animation_data if not animation_data: return None - return None blender_actions = [] blender_tracks = {} # TODO: this might need to be modified/ adapted to match the standard gltf exporter settings for track in animation_data.nla_tracks: - non_muted_strips = [strip for strip in track.strips if strip.action is not None and strip.mute is False] - for strip in non_muted_strips: #t.strips: + strips = [strip for strip in track.strips if strip.action is not None] + for strip in strips: # print(" ", source.name,'uses',strip.action.name, "active", strip.active, "action", strip.action) blender_actions.append(strip.action) blender_tracks[strip.action.name] = track.name @@ -55,6 +54,9 @@ def animation_hash(obj): markers_per_animation[animation_name][marker.frame] = [] markers_per_animation[animation_name][marker.frame].append(marker.name) + compact_result = hash(str((blender_actions, blender_tracks, markers_per_animation, animations_infos))) + return compact_result + def camera_hash(obj): camera_fields = ["angle", "angle_x", "angle_y", "animation_data", "background_images", "clip_end", "clip_start", "display_size", "dof", "fisheye_fov"] @@ -83,7 +85,7 @@ def bones_hash(bones): all_field_names = dir(bone) fields = [getattr(bone, prop, None) for prop in all_field_names if not prop.startswith("__") and not prop in fields_to_ignore and not prop.startswith("show_")] bones_result.append(fields) - print("fields of bone", bones_result) + #print("fields of bone", bones_result) return str(hash(str(bones_result))) # fixme: not good enough ? @@ -104,13 +106,15 @@ def serialize_scene(): print("serializing scene") data = {} for scene in bpy.data.scenes: + if scene.name.startswith(TEMPSCENE_PREFIX): + continue data[scene.name] = {} for object in scene.objects: + object = bpy.data.objects[object.name] #print("object", object.name, object.location) - transform = str((object.location, object.rotation_euler, object.scale)) - visibility = object.visible_get() - - print("object type", object.type) + transform = str((object.location, object.rotation_euler, object.scale)) #str((object.matrix_world.to_translation(), object.matrix_world.to_euler('XYZ'), object.matrix_world.to_quaternion()))# + visibility = object.visible_get() + #print("object type", object.type) custom_properties = {} for K in object.keys(): if K not in '_RNA_UI' and K != 'components_meta': @@ -122,6 +126,8 @@ def serialize_scene(): camera = camera_hash(object) if object.type == 'CAMERA' else None light = light_hash(object) if object.type == 'LIGHT' else None armature = armature_hash(object) if object.type == 'ARMATURE' else None + parent = object.parent.name if object.parent else None + collections = [collection.name for collection in object.users_collection] data[scene.name][object.name] = { "name": object.name, @@ -132,7 +138,9 @@ def serialize_scene(): "mesh": mesh, "camera": camera, "light": light, - "armature": armature + "armature": armature, + "parent": parent, + "collections": collections } """print("data", data) diff --git a/tools/gltf_auto_export/tests/test_change_tracking.py b/tools/gltf_auto_export/tests/test_change_tracking.py index 8953652..1b5441c 100644 --- a/tools/gltf_auto_export/tests/test_change_tracking.py +++ b/tools/gltf_auto_export/tests/test_change_tracking.py @@ -304,7 +304,7 @@ def test_export_changed_parameters(setup_data): other_files_modification_times = [value for index, value in enumerate(modification_times) if index not in [world_file_index, blueprint1_file_index]] other_files_modification_times_first = [value for index, value in enumerate(modification_times_first) if index not in [world_file_index, blueprint1_file_index]] - assert modification_times[world_file_index] != modification_times_first[world_file_index] + assert modification_times[world_file_index] == modification_times_first[world_file_index] assert modification_times[blueprint1_file_index] != modification_times_first[blueprint1_file_index] assert other_files_modification_times == other_files_modification_times_first # reset the comparing @@ -331,26 +331,23 @@ def test_export_changed_parameters(setup_data): modification_times = list(map(lambda file_path: os.path.getmtime(file_path), model_library_file_paths + [world_file_path])) assert modification_times != modification_times_first - # the "world" file should have changed (TODO: double check: this is since changing an instances collection changes the instance too ?) + # the "world" file should not have changed world_file_index = mapped_files_to_timestamps_and_index["World"][1] - # and the blueprint3 file too, since that is the collection we changed + # the blueprint3 file should have changed, since that is the collection we changed blueprint3_file_index = mapped_files_to_timestamps_and_index["Blueprint3"][1] - # and the blueprint4 file too, since it contains the collection we changed + # the blueprint4 file NOT, since, while it contains an instance of the collection we changed, the default export mode is "split" blueprint4_file_index = mapped_files_to_timestamps_and_index["Blueprint4_nested"][1] other_files_modification_times = [value for index, value in enumerate(modification_times) if index not in [world_file_index, blueprint3_file_index, blueprint4_file_index]] other_files_modification_times_first = [value for index, value in enumerate(modification_times_first) if index not in [world_file_index, blueprint3_file_index, blueprint4_file_index]] - assert modification_times[world_file_index] != modification_times_first[world_file_index] + assert modification_times[world_file_index] == modification_times_first[world_file_index] assert modification_times[blueprint3_file_index] != modification_times_first[blueprint3_file_index] - assert modification_times[blueprint4_file_index] != modification_times_first[blueprint4_file_index] + assert modification_times[blueprint4_file_index] == modification_times_first[blueprint4_file_index] assert other_files_modification_times == other_files_modification_times_first # reset the comparing modification_times_first = modification_times - - - # now same, but using an operator print("----------------") print("change using operator") @@ -362,13 +359,9 @@ def test_export_changed_parameters(setup_data): bpy.ops.transform.translate(value=mathutils.Vector((2.0, 1.0, -5.0))) bpy.ops.transform.rotate(value=0.378874, constraint_axis=(False, False, True), mirror=False, proportional_edit_falloff='SMOOTH', proportional_size=1) bpy.ops.object.transform_apply() - bpy.ops.transform.translate(value=(0.5, 0, 0), constraint_axis=(True, False, False)) - - #force an update, as apparently all the operators above do not trigger changes ??? - rna_prop_ui.rna_idprop_ui_create(bpy.data.objects["Cube"], "________temp", default=0) - rna_prop_ui.rna_idprop_ui_prop_clear(bpy.data.objects["Cube"], "________temp") - + bpy.ops.transform.translate(value=(3.5, 0, 0), constraint_axis=(True, False, False)) + auto_export_operator( auto_export=True, direct_mode=True, diff --git a/tools/gltf_auto_export/todo.md b/tools/gltf_auto_export/todo.md index fe0c0f0..2929287 100644 --- a/tools/gltf_auto_export/todo.md +++ b/tools/gltf_auto_export/todo.md @@ -1,4 +1,9 @@ - investigate remove_blueprints_list_from_main_scene (could be a case of changes to bpy.data not being applied immediatly) - investigate clearing of changed_objects_per_scene - it seems bevy_components does not trigger updates -- undo redo is ignored: ie save, do something, undo it, you still get changes \ No newline at end of file +- undo redo is ignored: ie save, do something, undo it, you still get changes + + +- for collection instances: + * [ ] blueprints export should also take the split/embed mode into account: if a nested collection changes AND embed is active, its container collection should also be exported + * [ ] level exports should do the same \ No newline at end of file