Compare commits

...

3 Commits

Author SHA1 Message Date
Mark Moissette 178fe548f9
Merge 013087eebe into 9b50d77790 2024-08-11 23:57:46 +00:00
kaosat.dev 013087eebe Merge branch 'blenvy' of github.com:kaosat-dev/Blender_bevy_components_worklflow into blenvy 2024-08-12 01:57:35 +02:00
kaosat.dev a3ff1b6c1b feat(Blender): added basics for spliting out (armature) animations
* all boilerplate , including finding relevant armatures & their objects, exporting animations, settings & ui etc
added
 * renamed material library to "split materials"
 * a ton of related changes
2024-08-12 01:50:43 +02:00
22 changed files with 277 additions and 44 deletions

View File

@ -0,0 +1,96 @@
import os
import bpy
from ....core.helpers_collections import traverse_tree
from ..common.duplicate_object import copy_animation_data
from ..common.generate_temporary_scene_and_export import generate_temporary_scene_and_export
from ..common.export_gltf import (generate_gltf_export_settings)
def duplicate_object(object, destination_collection):
copy = None
# for objects which are NOT collection instances or when embeding
# we create a copy of our object and its children, to leave the original one as it is
original_name = object.name
#object.name = original_name + "____bak"
copy = object.copy()
copy.name = original_name
destination_collection.objects.link(copy)
copy_animation_data(object, copy)
animation_data = copy.animation_data
print("COPY ANIMATION DATA", animation_data)
# we want to hide our objects so we only export their bones if any
#copy.hide_set(True)
# clear & remove "hollow scene"
def clear_animation_scene_alt(temp_scene):
# remove any data we created
temp_root_collection = temp_scene.collection
temp_scene_objects = [o for o in temp_root_collection.all_objects]
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
for object in temp_root_collection.all_objects:
if object.name.endswith("____bak"):
object.name = object.name.replace("____bak", "")
# generates a scene for a given animated object
def generate_animation_scene_content(root_collection, animation):
"""for object in animation["objects"]:
duplicate_object(object, root_collection)"""
armature_object = animation["armature_object"] # we want the object , not the armature itself (the object is a container, and we can only copy objects to our temporary scene, not armatures)
print("generate scene for", armature_object)
duplicate_object(armature_object, root_collection)
#raise Exception("arg")
return {}
def clear_animation_scene(temp_scene):
print("CLEAR ANIMATION SCENE")
root_collection = temp_scene.collection
scene_objects = [o for o in root_collection.objects]
for object in scene_objects:
try:
bpy.data.objects.remove(object, do_unlink=True)
except:pass
bpy.data.scenes.remove(temp_scene)
# exports the animations used inside the current project:
# see https://forum.babylonjs.com/t/blender-how-to-export-animations-only-like-elf-glft/45716/2
# PROBLEM: it grabs the wrong objects !
# we need to hide the meshes that have armatures and keep the armature itself
def export_animations(animations_to_export, settings, blueprints_data):
gltf_export_settings = generate_gltf_export_settings(settings)
animations_path_full = getattr(settings,"animations_path_full", "")
gltf_export_settings = { **gltf_export_settings,
'use_active_scene': True,
'use_active_collection':True,
'use_active_collection_with_nested':True,
'use_visible': True,
'use_renderable': False,
'export_apply':True,
'export_animations': True # since we want to export animations , forced to true
}
for animation in animations_to_export:
print("exporting animation from ", animation)
gltf_output_path = os.path.join(animations_path_full, animation["armature"].name)
generate_temporary_scene_and_export(
settings=settings,
gltf_export_settings=gltf_export_settings,
temp_scene_name="__animation_scene"+ animation["armature"].name,
gltf_output_path=gltf_output_path,
tempScene_filler= lambda temp_collection: generate_animation_scene_content(temp_collection, animation),
tempScene_cleaner= lambda temp_scene, params: clear_animation_scene(temp_scene=temp_scene)
)

View File

@ -0,0 +1,103 @@
import bpy
import os
# TODO: move to helpers
def find_animations_not_on_disk(animations, animations_path_full, extension):
not_found_animations = []
for animation in animations:
gltf_output_path = os.path.join(animations_path_full, animation["armature"].name + extension)
# print("gltf_output_path", gltf_output_path)
found = os.path.exists(gltf_output_path) and os.path.isfile(gltf_output_path)
if not found:
not_found_animations.append(animation)
return not_found_animations
def add_animation_info_to_objects(animations_per, settings):
materials_path = getattr(settings, "materials_path")
export_gltf_extension = getattr(settings, "export_gltf_extension", ".glb")
for object in materials_per_object.keys():
material_infos = []
for material in materials_per_object[object]:
materials_exported_path = posixpath.join(materials_path, f"{material.name}{export_gltf_extension}")
material_info = f'(name: "{material.name}", path: "{materials_exported_path}")'
material_infos.append(material_info)
# problem with using actual components: you NEED the type registry/component infos, so if there is none , or it is not loaded yet, it does not work
# for a few components we could hardcode this
component_value = f"({material_infos})".replace("'","")
'''try:
bpy.ops.blenvy.component_add(target_item_name=object.name, target_item_type="OBJECT", component_type="blenvy::blueprints::materials::MaterialInfos", component_value=component_value )
except:'''
object['MaterialInfos'] = f"({material_infos})".replace("'","")
#upsert_bevy_component(object, "blenvy::blueprints::materials::MaterialInfos", f"({material_infos})".replace("'","") )
#apply_propertyGroup_values_to_item_customProperties_for_component(object, "MaterialInfos")
print("adding materialInfos to object", object, "material infos", material_infos)
def get_animations_to_export(changed_animations, changed_export_parameters, blueprints_data, settings):
export_gltf_extension = getattr(settings, "export_gltf_extension", ".glb")
animations_path_full = getattr(settings,"animations_path_full", "")
split_out_animations = getattr(settings.auto_export, "split_out_animations")
change_detection = getattr(settings.auto_export, "change_detection")
all_animations = []
animations_to_export = []
objects_per_armature = {}
armature_objects = {} # often we need the object of the armature, not the armature itself
# TODO: how to deal with non armatures ?
for object in bpy.data.objects:
if len(object.modifiers) > 0:
for modifier in object.modifiers:
if modifier.type == 'ARMATURE':
ref = modifier.object.name
armature_name = bpy.data.objects[ref].data.name
armature = bpy.data.armatures[armature_name]
if not armature.name in objects_per_armature :
objects_per_armature[armature.name] = []
objects_per_armature[armature.name].append(object)
armature_objects[armature.name] = modifier.object
print("Object has armature", object, modifier, modifier.object, "armature", armature.name)
"""animation_data = object.animation_data
if animation_data is not None:
print("ANIMATION DATA", animation_data, "for object", object)
if len(object.modifiers) > 0:
for modifier in object.modifiers:
if modifier.type == 'ARMATURE':
print("YOHOHOHO", modifier, modifier.object)
ref = modifier.object.name
armature_name = bpy.data.objects[ref].data.name
armature = bpy.data.armatures[armature_name]
if not armature.name in objects_per_armature :
objects_per_armature[armature.name] = []
objects_per_armature[armature.name].append(object)"""
#animations_to_export.append(object)
for armature_name in objects_per_armature.keys():
all_animations.append({"armature": bpy.data.armatures[armature_name], "armature_object": armature_objects[armature_name], "objects": objects_per_armature[armature_name]})
local_animations = [animation for animation in all_animations if animation["armature_object"].library is None]
animations_to_export = []
if split_out_animations and change_detection:
if changed_export_parameters:
animations_to_export = local_animations
else :
# first check if all animations have already been exported before (if this is the first time the exporter is run
# in your current Blender session for example)
animations_not_on_disk = find_animations_not_on_disk(local_animations, animations_path_full, export_gltf_extension)
animations_always_export = []
animations_to_export = list(set(changed_animations + animations_not_on_disk + animations_always_export))
print("animations_to_export", animations_to_export)
return animations_to_export

View File

@ -9,7 +9,8 @@ from ..utils import upsert_blueprint_assets, write_blueprint_metadata_file
def export_blueprints(blueprints, settings, blueprints_data):
blueprints_path_full = getattr(settings, "blueprints_path_full")
gltf_export_settings = generate_gltf_export_settings(settings)
export_materials_library = getattr(settings.auto_export, "export_materials_library")
split_out_materials = getattr(settings.auto_export, "split_out_materials")
split_out_animations = getattr(settings.auto_export, "split_out_animations")
try:
# save current active collection
@ -21,9 +22,12 @@ def export_blueprints(blueprints, settings, blueprints_data):
gltf_export_settings = { **gltf_export_settings, 'use_active_scene': True, 'use_active_collection': True, 'use_active_collection_with_nested':True}
collection = bpy.data.collections[blueprint.name]
# if we are using the material library option, do not export materials, use placeholder instead
if export_materials_library:
# if we are using the split material option, do not export materials, use placeholder instead
if split_out_materials:
gltf_export_settings['export_materials'] = 'PLACEHOLDER'
# if we are using the split animations options, do not export animations
if split_out_animations:
gltf_export_settings['export_animations'] = False
# inject blueprint asset data
upsert_blueprint_assets(blueprint, blueprints_data=blueprints_data, settings=settings)

View File

@ -16,6 +16,8 @@ from ..materials.get_materials_to_export import get_materials_to_export
from ..materials.export_materials import cleanup_materials, export_materials
from ..levels.bevy_scene_components import remove_scene_components, upsert_scene_components
from ..animations.get_animations_to_export import get_animations_to_export
from ..animations.export_animations import export_animations
"""this is the main 'central' function for all auto export """
def auto_export(changes_per_scene, changes_per_collection, changes_per_material, changed_export_parameters, settings):
@ -26,7 +28,8 @@ def auto_export(changes_per_scene, changes_per_collection, changes_per_material,
change_detection = getattr(settings.auto_export, "change_detection")
export_scene_settings = getattr(settings.auto_export, "export_scene_settings")
export_blueprints_enabled = getattr(settings.auto_export, "export_blueprints")
export_materials_library = getattr(settings.auto_export, "export_materials_library")
split_out_materials = getattr(settings.auto_export, "split_out_materials")
split_out_animations = getattr(settings.auto_export, "split_out_animations")
# standard gltf export settings are stored differently
standard_gltf_exporter_settings = get_standard_exporter_settings()
@ -73,8 +76,11 @@ def auto_export(changes_per_scene, changes_per_collection, changes_per_material,
# export materials & inject materials components into relevant objects
materials_to_export = get_materials_to_export(changes_per_material, changed_export_parameters, blueprints_data, settings)
# since seperate animation exports also changes blueprint exports we need to call this before blueprints are exported
animations_to_export = get_animations_to_export(changes_per_scene, changed_export_parameters, blueprints_data, settings)
# update the list of tracked exports
exports_total = len(blueprints_to_export) + len(level_scenes_to_export) + (1 if export_materials_library else 0)
exports_total = len(blueprints_to_export) + len(level_scenes_to_export) + (1 if split_out_materials else 0)
bpy.context.window_manager.auto_export_tracker.exports_total = exports_total
bpy.context.window_manager.auto_export_tracker.exports_count = exports_total
@ -101,10 +107,15 @@ def auto_export(changes_per_scene, changes_per_collection, changes_per_material,
old_selections = bpy.context.selected_objects
# deal with materials
if export_materials_library and (not change_detection or changed_export_parameters or len(materials_to_export) > 0) :
if split_out_materials and (not change_detection or changed_export_parameters or len(materials_to_export) > 0) :
print("export MATERIALS")
export_materials(materials_to_export, settings, blueprints_data)
# and animations
if split_out_animations and (not change_detection or changed_export_parameters or len(animations_to_export) > 0):
print("export ANIMATIONS")
export_animations(animations_to_export, settings, blueprints_data)
# export any level/world scenes
if not change_detection or changed_export_parameters or len(level_scenes_to_export) > 0:
print("export LEVELS")
@ -123,7 +134,7 @@ def auto_export(changes_per_scene, changes_per_collection, changes_per_material,
# reset selections
for obj in old_selections:
obj.select_set(True)
if export_materials_library:
if split_out_materials:
cleanup_materials(blueprints_data.blueprint_names, settings.library_scenes)
else:

View File

@ -76,7 +76,6 @@ def duplicate_object(object, parent, combine_mode, destination_collection, bluep
internal_blueprint_names = [blueprint.name for blueprint in blueprints_data.internal_blueprints]
# print("COMBINE MODE", combine_mode)
if object.instance_type == 'COLLECTION' and (combine_mode == 'Split' or (combine_mode == 'EmbedExternal' and (object.instance_collection.name in internal_blueprint_names)) ):
#print("creating empty for", object.name, object.instance_collection.name, internal_blueprint_names, combine_mode)
original_collection = object.instance_collection
original_name = object.name
blueprint_name = original_collection.name

View File

@ -192,10 +192,10 @@ def mesh_hash(obj):
h = str(h1_hash(vertices_np.tobytes()))
return h
# TODO: redo this one, this is essentially modifiec copy & pasted data, not fitting
# TODO: redo this one, this is essentially modified copy & pasted data, not fitting
def animation_hash(obj):
animation_data = obj.animation_data
if not animation_data:
if animation_data is None:
return None
blender_actions = []
blender_tracks = {}

View File

@ -18,7 +18,7 @@ parameter_names_whitelist_auto_export = [
'export_scene_settings',
'export_blueprints',
'export_separate_dynamic_and_static_objects',
'export_materials_library',
'split_out_materials',
'collection_instances_combine_mode',
]

View File

@ -69,7 +69,6 @@ def clear_materials_scene(temp_scene):
bpy.data.scenes.remove(temp_scene)
# exports the materials used inside the current project:
# the name of the output path is <materials_folder>/<name_of_your_blend_file>_materials_library.gltf/glb
def export_materials(materials_to_export, settings, blueprints_data):
gltf_export_settings = generate_gltf_export_settings(settings)
materials_path_full = getattr(settings,"materials_path_full")

View File

@ -4,19 +4,17 @@ from ....materials.materials_helpers import find_materials_not_on_disk
def get_materials_to_export(changes_per_material, changed_export_parameters, blueprints_data, settings):
export_gltf_extension = getattr(settings, "export_gltf_extension", ".glb")
blueprints_path_full = getattr(settings,"blueprints_path_full", "")
materials_path_full = getattr(settings,"materials_path_full", "")
materials_path_full = getattr(settings, "materials_path_full", "")
change_detection = getattr(settings.auto_export, "change_detection")
export_materials_library = getattr(settings.auto_export, "export_materials_library")
collection_instances_combine_mode = getattr(settings.auto_export, "collection_instances_combine_mode")
split_out_materials = getattr(settings.auto_export, "split_out_materials")
all_materials = bpy.data.materials
local_materials = [material for material in all_materials if material.library is None]
materials_to_export = []
# print("export_materials_library", export_materials_library, "change detection", change_detection, "changed_export_parameters", changed_export_parameters)
if export_materials_library and change_detection:
# print("split_out_materials", split_out_materials, "change detection", change_detection, "changed_export_parameters", changed_export_parameters)
if split_out_materials and change_detection:
if changed_export_parameters:
materials_to_export = local_materials
else :

View File

@ -72,13 +72,19 @@ class AutoExportSettings(PropertyGroup):
update=save_settings
) # type: ignore
export_materials_library: BoolProperty(
name='Export materials library',
description='remove materials from blueprints and use the material library instead',
split_out_materials: BoolProperty(
name='Split out materials',
description='removes materials from blueprints and exports them separately ',
default=True,
update=save_settings
) # type: ignore
split_out_animations: BoolProperty(
name='Split out animations',
description='removes animations/armatures from blueprints and exports them separately ',
default=False,
update=save_settings
) # type: ignore
""" combine mode can be
- 'Split' (default): replace with an empty, creating links to sub blueprints

View File

@ -57,7 +57,10 @@ def draw_settings_ui(layout, auto_export_settings):
section.separator()
# materials
section.prop(auto_export_settings, "export_materials_library")
section.prop(auto_export_settings, "split_out_materials")
# animations
section.prop(auto_export_settings, "split_out_animations")

View File

@ -25,5 +25,5 @@ def inject_export_path_into_internal_blueprints(internal_blueprints, blueprints_
blueprint_exported_path = posixpath.join(blueprints_path, f"{blueprint.name}{gltf_extension}")
# print("injecting blueprint path", blueprint_exported_path, "for", blueprint.name)
blueprint.collection["export_path"] = blueprint_exported_path
"""if export_materials_library:
"""if split_out_materials:
blueprint.collection["materials_path"] = materials_exported_path"""

View File

@ -115,16 +115,28 @@ class BlenvyManager(PropertyGroup):
materials_path: StringProperty(
name='Materials path',
description='path to export the materials libraries to (relative to the assets folder)',
description='path to export the materials to (relative to the assets folder)',
default='materials',
update= save_settings
) # type: ignore
# computed property for the absolute path of blueprints
# computed property for the absolute path of materials
materials_path_full: StringProperty(
get=lambda self: os.path.abspath(os.path.join(os.path.dirname(bpy.data.filepath), self.project_root_path, self.assets_path, self.materials_path))
) # type: ignore
animations_path: StringProperty(
name='Animations path',
description='path to export the animations to (relative to the assets folder)',
default='animations',
update= save_settings
) # type: ignore
# computed property for the absolute path of animations
animations_path_full: StringProperty(
get=lambda self: os.path.abspath(os.path.join(os.path.dirname(bpy.data.filepath), self.project_root_path, self.assets_path, self.animations_path))
) # type: ignore
# sub ones
auto_export: PointerProperty(type=AutoExportSettings) # type: ignore
components: PointerProperty(type=ComponentsSettings) # type: ignore

View File

@ -121,6 +121,8 @@ def draw_common_settings_ui(layout, settings):
draw_folder_browser(layout=row, label="Levels Folder", prop_origin=blenvy, target_property="levels_path")
row = layout.row()
draw_folder_browser(layout=row, label="Materials Folder", prop_origin=blenvy, target_property="materials_path")
row = layout.row()
draw_folder_browser(layout=row, label="Animations Folder", prop_origin=blenvy, target_property="animations_path")
layout.separator()
# scenes selection

View File

@ -93,10 +93,10 @@ def add_material_info_to_objects(materials_per_object, settings):
# problem with using actual components: you NEED the type registry/component infos, so if there is none , or it is not loaded yet, it does not work
# for a few components we could hardcode this
component_value = f"({material_infos})".replace("'","")
try:
'''try:
bpy.ops.blenvy.component_add(target_item_name=object.name, target_item_type="OBJECT", component_type="blenvy::blueprints::materials::MaterialInfos", component_value=component_value )
except:
object['MaterialInfos'] = f"({material_infos})".replace("'","")
except:'''
object['MaterialInfos'] = f"({material_infos})".replace("'","")
#upsert_bevy_component(object, "blenvy::blueprints::materials::MaterialInfos", f"({material_infos})".replace("'","") )
#apply_propertyGroup_values_to_item_customProperties_for_component(object, "MaterialInfos")
print("adding materialInfos to object", object, "material infos", material_infos)

View File

@ -106,7 +106,7 @@ def test_export_complex(setup_data):
blenvy.auto_export.auto_export = True
blenvy.auto_export.export_scene_settings = True
blenvy.auto_export.export_blueprints = True
blenvy.auto_export.export_materials_library = True
blenvy.auto_export.split_out_materials = True
bpy.data.scenes['World'].blenvy_scene_type = 'Level' # set scene as main/level scene
bpy.data.scenes['Library'].blenvy_scene_type = 'Library' # set scene as Library scene

View File

@ -53,7 +53,7 @@ def test_export_external_blueprints(setup_data):
blenvy.auto_export.auto_export = True
blenvy.auto_export.export_scene_settings = True
blenvy.auto_export.export_blueprints = True
#blenvy.auto_export.export_materials_library = True
#blenvy.auto_export.split_out_materials = True
print("SCENES", bpy.data.scenes)
for scene in bpy.data.scenes:

View File

@ -107,7 +107,7 @@ def test_export_complex(setup_data):
blenvy.auto_export.auto_export = True
blenvy.auto_export.export_scene_settings = True
blenvy.auto_export.export_blueprints = True
blenvy.auto_export.export_materials_library = True
blenvy.auto_export.split_out_materials = True
bpy.data.scenes['World'].blenvy_scene_type = 'Level' # set scene as main/level scene
bpy.data.scenes['Library'].blenvy_scene_type = 'Library' # set scene as Library scene

View File

@ -81,7 +81,7 @@ def test_export_no_parameters(setup_data):
auto_export_operator(
auto_export=True,
direct_mode=True,
export_materials_library=True,
split_out_materials=True,
project_root_path = os.path.abspath(setup_data["root_path"]),
export_output_folder="./models",
)
@ -106,7 +106,7 @@ def test_export_auto_export_parameters_only(setup_data):
direct_mode=True,
project_root_path = os.path.abspath(setup_data["root_path"]),
export_output_folder="./models",
export_materials_library=True
split_out_materials=True
)
world_file_path = os.path.join(setup_data["levels_path"], "World.glb")
@ -144,7 +144,7 @@ def test_export_changed_parameters(setup_data):
export_output_folder="./models",
export_scene_settings=True,
export_blueprints=True,
export_materials_library=True
split_out_materials=True
)
world_file_path = os.path.join(setup_data["levels_path"], "World.glb")
@ -163,7 +163,7 @@ def test_export_changed_parameters(setup_data):
export_output_folder="./models",
export_scene_settings=True,
export_blueprints=True,
export_materials_library=True
split_out_materials=True
)
modification_times_no_change = list(map(lambda file_path: os.path.getmtime(file_path), model_library_file_paths))
@ -188,7 +188,7 @@ def test_export_changed_parameters(setup_data):
export_output_folder="./models",
export_scene_settings=True,
export_blueprints=True,
export_materials_library=True
split_out_materials=True
)
modification_times_changed_gltf = list(map(lambda file_path: os.path.getmtime(file_path), model_library_file_paths))
@ -204,7 +204,7 @@ def test_export_changed_parameters(setup_data):
export_output_folder="./models",
export_scene_settings=True,
export_blueprints=True,
export_materials_library=True
split_out_materials=True
)
modification_times_changed_gltf = list(map(lambda file_path: os.path.getmtime(file_path), model_library_file_paths))
@ -218,7 +218,7 @@ def test_export_changed_parameters(setup_data):
export_props = {
"level_scene_names" : ['World'],
"library_scene_names": ['Library'],
"export_materials_library": False # we need to add it here, as the direct settings set on the operator will only be used for the NEXT run
"split_out_materials": False # we need to add it here, as the direct settings set on the operator will only be used for the NEXT run
}
# store settings for the auto_export part
@ -233,7 +233,7 @@ def test_export_changed_parameters(setup_data):
export_output_folder="./models",
export_scene_settings=True,
export_blueprints=True,
export_materials_library=False
split_out_materials=False
)
modification_times_changed_auto = list(map(lambda file_path: os.path.getmtime(file_path), model_library_file_paths))
@ -249,7 +249,7 @@ def test_export_changed_parameters(setup_data):
export_output_folder="./models",
export_scene_settings=True,
export_blueprints=True,
export_materials_library=False
split_out_materials=False
)
modification_times_changed_gltf = list(map(lambda file_path: os.path.getmtime(file_path), model_library_file_paths))

View File

@ -137,7 +137,7 @@ def test_export_materials_library(setup_data):
export_output_folder="./models",
export_scene_settings=True,
export_blueprints=True,
export_materials_library = True
split_out_materials = True
)
assert os.path.exists(os.path.join(setup_data["blueprints_path"], "Blueprint1.glb")) == True
@ -164,7 +164,7 @@ def test_export_materials_library_custom_path(setup_data):
export_output_folder="./models",
export_scene_settings=True,
export_blueprints=True,
export_materials_library = True,
split_out_materials = True,
materials_path="assets/other_materials"
)

View File

@ -33,7 +33,7 @@ def run_auto_export(setup_data):
export_output_folder="./models",
export_scene_settings=True,
export_blueprints=True,
export_materials_library=False
split_out_materials=False
)
levels_path = setup_data["levels_path"]