diff --git a/io_scene_gltf2/__init__.py b/io_scene_gltf2/__init__.py index 8d55d472635b9f8b3115e9ee8653d5b43e598698..b75313d252c83acac1d2d487d2c8cb2f517c6438 100755 --- a/io_scene_gltf2/__init__.py +++ b/io_scene_gltf2/__init__.py @@ -5,7 +5,7 @@ bl_info = { 'name': 'glTF 2.0 format', 'author': 'Julien Duroure, Scurest, Norbert Nopper, Urs Hanselmann, Moritz Becher, Benjamin Schmithüsen, Jim Eckerlein, and many external contributors', - "version": (4, 1, 26), + "version": (4, 1, 27), 'blender': (4, 1, 0), 'location': 'File > Import-Export', 'description': 'Import-Export as glTF 2.0', @@ -251,6 +251,12 @@ class ExportGLTF2_Base(ConvertGLTF2_Base): default=True ) + export_gn_mesh: BoolProperty( + name='Geometry Nodes Instances (Experimental)', + description='Export Geometry nodes instance meshes', + default=False + ) + export_draco_mesh_compression_enable: BoolProperty( name='Draco mesh compression', description='Compress mesh using Draco', @@ -808,6 +814,8 @@ class ExportGLTF2_Base(ConvertGLTF2_Base): else: export_settings['gltf_draco_mesh_compression'] = False + export_settings['gltf_gn_mesh'] = self.export_gn_mesh + export_settings['gltf_materials'] = self.export_materials export_settings['gltf_attributes'] = self.export_attributes export_settings['gltf_cameras'] = self.export_cameras @@ -1065,6 +1073,7 @@ class GLTF_PT_export_data_scene(bpy.types.Panel): sfile = context.space_data operator = sfile.active_operator + layout.prop(operator, 'export_gn_mesh') layout.prop(operator, 'export_gpu_instances') layout.prop(operator, 'export_hierarchy_flatten_objs') @@ -1101,7 +1110,6 @@ class GLTF_PT_export_data_mesh(bpy.types.Panel): col.prop(operator, 'use_mesh_edges') col.prop(operator, 'use_mesh_vertices') - class GLTF_PT_export_data_material(bpy.types.Panel): bl_space_type = 'FILE_BROWSER' bl_region_type = 'TOOL_PROPS' diff --git a/io_scene_gltf2/blender/com/gltf2_blender_default.py b/io_scene_gltf2/blender/com/gltf2_blender_default.py index 8ed916d2c9ee32de505576d0e79f1b0d0aadde8e..4cf8930ed0e3030ecc6dab1247e76d2b30212a5d 100644 --- a/io_scene_gltf2/blender/com/gltf2_blender_default.py +++ b/io_scene_gltf2/blender/com/gltf2_blender_default.py @@ -7,3 +7,9 @@ BLENDER_SPECULAR = 0.5 BLENDER_SPECULAR_TINT = 0.0 BLENDER_GLTF_SPECIAL_COLLECTION = "glTF_not_exported" + +LIGHTS = { + "POINT": "point", + "SUN": "directional", + "SPOT": "spot" + } diff --git a/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_action.py b/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_action.py index 4f1df836f889bafdef42cc2799d440173dd72059..6201c2da4c607994b9f83b390c941e7bb769e0f2 100644 --- a/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_action.py +++ b/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_action.py @@ -223,14 +223,14 @@ def gather_action_animations( obj_uuid: int, current_action = None current_sk_action = None current_world_matrix = None - if blender_object.animation_data and blender_object.animation_data.action: + if blender_object and blender_object.animation_data and blender_object.animation_data.action: # There is an active action. Storing it, to be able to restore after switching all actions during export current_action = blender_object.animation_data.action elif len(blender_actions) != 0 and blender_object.animation_data is not None and blender_object.animation_data.action is None: # No current action set, storing world matrix of object current_world_matrix = blender_object.matrix_world.copy() - if blender_object.type == "MESH" \ + if blender_object and blender_object.type == "MESH" \ and blender_object.data is not None \ and blender_object.data.shape_keys is not None \ and blender_object.data.shape_keys.animation_data is not None \ @@ -239,7 +239,7 @@ def gather_action_animations( obj_uuid: int, # Remove any solo (starred) NLA track. Restored after export solo_track = None - if blender_object.animation_data: + if blender_object and blender_object.animation_data: for track in blender_object.animation_data.nla_tracks: if track.is_solo: solo_track = track @@ -247,11 +247,11 @@ def gather_action_animations( obj_uuid: int, break # Remove any tweak mode. Restore after export - if blender_object.animation_data: + if blender_object and blender_object.animation_data: restore_tweak_mode = blender_object.animation_data.use_tweak_mode # Remove use of NLA. Restore after export - if blender_object.animation_data: + if blender_object and blender_object.animation_data: current_use_nla = blender_object.animation_data.use_nla blender_object.animation_data.use_nla = False @@ -400,7 +400,7 @@ def gather_action_animations( obj_uuid: int, # Restore action status # TODO: do this in a finally - if blender_object.animation_data: + if blender_object and blender_object.animation_data: if blender_object.animation_data.action is not None: if current_action is None: # remove last exported action @@ -415,14 +415,14 @@ def gather_action_animations( obj_uuid: int, blender_object.animation_data.use_tweak_mode = restore_tweak_mode blender_object.animation_data.use_nla = current_use_nla - if blender_object.type == "MESH" \ + if blender_object and blender_object.type == "MESH" \ and blender_object.data is not None \ and blender_object.data.shape_keys is not None \ and blender_object.data.shape_keys.animation_data is not None: reset_sk_data(blender_object, blender_actions, export_settings) blender_object.data.shape_keys.animation_data.action = current_sk_action - if current_world_matrix is not None: + if blender_object and current_world_matrix is not None: blender_object.matrix_world = current_world_matrix export_user_extensions('animation_switch_loop_hook', export_settings, blender_object, True) @@ -441,7 +441,7 @@ def __get_blender_actions(obj_uuid: str, export_user_extensions('pre_gather_actions_hook', export_settings, blender_object) - if blender_object.animation_data is not None: + if blender_object and blender_object.animation_data is not None: # Collect active action. if blender_object.animation_data.action is not None: blender_actions.append(blender_object.animation_data.action) @@ -462,7 +462,7 @@ def __get_blender_actions(obj_uuid: str, action_on_type[strip.action.name] = "OBJECT" # For caching, actions linked to SK must be after actions about TRS - if export_settings['gltf_morph_anim'] and blender_object.type == "MESH" \ + if export_settings['gltf_morph_anim'] and blender_object and blender_object.type == "MESH" \ and blender_object.data is not None \ and blender_object.data.shape_keys is not None \ and blender_object.data.shape_keys.animation_data is not None: @@ -488,7 +488,7 @@ def __get_blender_actions(obj_uuid: str, # But only if armature has already some animation_data # If not, we says that this armature is never animated, so don't add these additional actions if export_settings['gltf_export_anim_single_armature'] is True: - if blender_object.type == "ARMATURE" and blender_object.animation_data is not None: + if blender_object and blender_object.type == "ARMATURE" and blender_object.animation_data is not None: if len(export_settings['vtree'].get_all_node_of_type(VExportNode.ARMATURE)) == 1: # Keep all actions on objects (no Shapekey animation) for act in [a for a in bpy.data.actions if a.id_root == "OBJECT"]: diff --git a/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_animation_utils.py b/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_animation_utils.py index bd8c75ef29bb096e98c9e48b7caa9958ddef9678..a6c0e6509d62eb5f10a3131150d732e63f1e5c1c 100644 --- a/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_animation_utils.py +++ b/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_animation_utils.py @@ -174,7 +174,7 @@ def bake_animation(obj_uuid: str, animation_key: str, export_settings, mode=None # If force sampling is OFF, can lead to inconsistent export anyway if (export_settings['gltf_bake_animation'] is True \ or export_settings['gltf_animation_mode'] == "NLA_TRACKS") \ - and blender_object.type != "ARMATURE" and export_settings['gltf_force_sampling'] is True: + and blender_object and blender_object.type != "ARMATURE" and export_settings['gltf_force_sampling'] is True: animation = None # We also have to check if this is a skinned mesh, because we don't have to force animation baking on this case # (skinned meshes TRS must be ignored, says glTF specification) @@ -186,6 +186,7 @@ def bake_animation(obj_uuid: str, animation_key: str, export_settings, mode=None # Need to bake sk only if not linked to a driver sk by parent armature # In case of NLA track export, no baking of SK if export_settings['gltf_morph_anim'] \ + and blender_object \ and blender_object.type == "MESH" \ and blender_object.data is not None \ and blender_object.data.shape_keys is not None: @@ -220,6 +221,7 @@ def bake_animation(obj_uuid: str, animation_key: str, export_settings, mode=None elif (export_settings['gltf_bake_animation'] is True \ or export_settings['gltf_animation_mode'] == "NLA_TRACKS") \ + and blender_object \ and blender_object.type == "ARMATURE" \ and mode is None or mode == "OBJECT": # We need to bake all bones. Because some bone can have some constraints linking to diff --git a/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_scene_animation.py b/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_scene_animation.py index 0337dcc483f03fe71e3344440f9f2cac8dc8a46e..543f204459322215d0998d0089b1243235ad215a 100644 --- a/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_scene_animation.py +++ b/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_scene_animation.py @@ -14,8 +14,9 @@ from .gltf2_blender_gather_animation_utils import link_samplers, add_slide_data def gather_scene_animations(export_settings): - # if there is no animation in file => no need to bake - if len(bpy.data.actions) == 0: + # if there is no animation in file => no need to bake. Except if we are trying to bake GN instances + if len(bpy.data.actions) == 0 and export_settings['gltf_gn_mesh'] is False: + #TODO : get a better filter by checking we really have some GN instances... return [] total_channels = [] @@ -42,11 +43,11 @@ def gather_scene_animations(export_settings): else: continue - blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object + blender_object = export_settings['vtree'].nodes[obj_uuid].blender_object # blender_object can be None for GN instances export_settings['ranges'][obj_uuid] = {} export_settings['ranges'][obj_uuid][obj_uuid] = {'start': start_frame, 'end': end_frame} - if blender_object.type == "ARMATURE": + if blender_object and blender_object.type == "ARMATURE": # Manage sk drivers obj_drivers = get_sk_drivers(obj_uuid, export_settings) for obj_dr in obj_drivers: @@ -61,7 +62,7 @@ def gather_scene_animations(export_settings): # Perform baking animation export - if blender_object.type != "ARMATURE": + if blender_object and blender_object.type != "ARMATURE": # We have to check if this is a skinned mesh, because we don't have to force animation baking on this case if export_settings['vtree'].nodes[obj_uuid].skin is None: channels = gather_object_sampled_channels(obj_uuid, obj_uuid, export_settings) @@ -83,6 +84,12 @@ def gather_scene_animations(export_settings): channels = gather_sk_sampled_channels(obj_uuid, obj_uuid, export_settings) if channels is not None: total_channels.extend(channels) + elif blender_object is None: + # This is GN instances + # Currently, not checking if this instance is skinned.... #TODO + channels = gather_object_sampled_channels(obj_uuid, obj_uuid, export_settings) + if channels is not None: + total_channels.extend(channels) else: channels = gather_armature_sampled_channels(obj_uuid, obj_uuid, export_settings) if channels is not None: @@ -94,7 +101,7 @@ def gather_scene_animations(export_settings): channels=total_channels, extensions=None, extras=__gather_extras(blender_object, export_settings), - name=blender_object.name, + name=blender_object.name if blender_object else "GN Instance", samplers=[] ) link_samplers(animation, export_settings) diff --git a/io_scene_gltf2/blender/exp/animation/sampled/gltf2_blender_gather_animation_sampling_cache.py b/io_scene_gltf2/blender/exp/animation/sampled/gltf2_blender_gather_animation_sampling_cache.py index e73da0da1ef6c0145e752838a0edcf164c4800c2..c0d1d1d34357cd34039c474b4e0058ff8104615b 100644 --- a/io_scene_gltf2/blender/exp/animation/sampled/gltf2_blender_gather_animation_sampling_cache.py +++ b/io_scene_gltf2/blender/exp/animation/sampled/gltf2_blender_gather_animation_sampling_cache.py @@ -35,12 +35,18 @@ def get_cache_data(path: str, if export_settings['gltf_animation_mode'] in "NLA_TRACKS": obj_uuids = [blender_obj_uuid] + depsgraph = bpy.context.evaluated_depsgraph_get() + frame = min_ while frame <= max_: bpy.context.scene.frame_set(int(frame)) + current_instance = {} # For GN instances, we are going to track instances by their order in instance iterator for obj_uuid in obj_uuids: blender_obj = export_settings['vtree'].nodes[obj_uuid].blender_object + if blender_obj is None: #GN instance + if export_settings['vtree'].nodes[obj_uuid].parent_uuid not in current_instance.keys(): + current_instance[export_settings['vtree'].nodes[obj_uuid].parent_uuid] = 0 # TODO: we may want to avoid looping on all objects, but an accurate filter must be found @@ -63,12 +69,23 @@ def get_cache_data(path: str, if export_settings['vtree'].nodes[obj_uuid].parent_uuid is not None and export_settings['vtree'].nodes[export_settings['vtree'].nodes[obj_uuid].parent_uuid].blender_type == VExportNode.COLLECTION: parent_mat = mathutils.Matrix.Identity(4).freeze() - mat = parent_mat.inverted_safe() @ blender_obj.matrix_world + if blender_obj: + mat = parent_mat.inverted_safe() @ blender_obj.matrix_world + else: + eval = export_settings['vtree'].nodes[export_settings['vtree'].nodes[obj_uuid].parent_uuid].blender_object.evaluated_get(depsgraph) + cpt_inst = 0 + for inst in depsgraph.object_instances: # use only as iterator + if inst.parent == eval: + if current_instance[export_settings['vtree'].nodes[obj_uuid].parent_uuid] == cpt_inst: + mat = inst.matrix_world.copy() + current_instance[export_settings['vtree'].nodes[obj_uuid].parent_uuid] += 1 + break + cpt_inst += 1 if obj_uuid not in data.keys(): data[obj_uuid] = {} - if blender_obj.animation_data and blender_obj.animation_data.action \ + if blender_obj and blender_obj.animation_data and blender_obj.animation_data.action \ and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS"]: if blender_obj.animation_data.action.name not in data[obj_uuid].keys(): data[obj_uuid][blender_obj.animation_data.action.name] = {} @@ -91,7 +108,7 @@ def get_cache_data(path: str, data[obj_uuid][obj_uuid]['matrix'][None][frame] = mat # Store data for all bones, if object is an armature - if blender_obj.type == "ARMATURE": + if blender_obj and blender_obj.type == "ARMATURE": bones = export_settings['vtree'].get_all_bones(obj_uuid) if blender_obj.animation_data and blender_obj.animation_data.action \ and export_settings['gltf_animation_mode'] in ["ACTIVE_ACTIONS", "ACTIONS"]: @@ -140,10 +157,18 @@ def get_cache_data(path: str, data[obj_uuid][obj_uuid]['bone'][blender_bone.name] = {} data[obj_uuid][obj_uuid]['bone'][blender_bone.name][frame] = matrix + elif blender_obj is None: # GN instances + # case of baking object, for GN instances + # There is no animation, so use uuid of object as key + if obj_uuid not in data[obj_uuid].keys(): + data[obj_uuid][obj_uuid] = {} + data[obj_uuid][obj_uuid]['matrix'] = {} + data[obj_uuid][obj_uuid]['matrix'][None] = {} + data[obj_uuid][obj_uuid]['matrix'][None][frame] = mat # Check SK animation here, as we are caching data # This will avoid to have to do it again when exporting SK animation - if export_settings['gltf_morph_anim'] and blender_obj.type == "MESH" \ + if export_settings['gltf_morph_anim'] and blender_obj and blender_obj.type == "MESH" \ and blender_obj.data is not None \ and blender_obj.data.shape_keys is not None \ and blender_obj.data.shape_keys.animation_data is not None \ @@ -156,7 +181,7 @@ def get_cache_data(path: str, data[obj_uuid][blender_obj.data.shape_keys.animation_data.action.name]['sk'][None] = {} data[obj_uuid][blender_obj.data.shape_keys.animation_data.action.name]['sk'][None][frame] = [k.value for k in get_sk_exported(blender_obj.data.shape_keys.key_blocks)] - elif export_settings['gltf_morph_anim'] and blender_obj.type == "MESH" \ + elif export_settings['gltf_morph_anim'] and blender_obj and blender_obj.type == "MESH" \ and blender_obj.data is not None \ and blender_obj.data.shape_keys is not None \ and blender_obj.data.shape_keys.animation_data is not None \ @@ -173,7 +198,7 @@ def get_cache_data(path: str, - elif export_settings['gltf_morph_anim'] and blender_obj.type == "MESH" \ + elif export_settings['gltf_morph_anim'] and blender_obj and blender_obj.type == "MESH" \ and blender_obj.data is not None \ and blender_obj.data.shape_keys is not None: if obj_uuid not in data[obj_uuid].keys(): @@ -187,7 +212,7 @@ def get_cache_data(path: str, # caching driver sk meshes # This will avoid to have to do it again when exporting SK animation - if blender_obj.type == "ARMATURE": + if blender_obj and blender_obj.type == "ARMATURE": sk_drivers = get_sk_drivers(obj_uuid, export_settings) for dr_obj in sk_drivers: driver_object = export_settings['vtree'].nodes[dr_obj].blender_object diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_lights.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_lights.py index f058d2e3c77537c2c99272edf8cef937c447421f..04ad435c638796f5018436b4c42383bf95c68fdf 100644 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_lights.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_lights.py @@ -9,6 +9,7 @@ from ...io.com import gltf2_io_lights_punctual from ...io.com import gltf2_io_debug from ..com.gltf2_blender_extras import generate_extras from ..com.gltf2_blender_conversion import PBR_WATTS_TO_LUMENS +from ..com.gltf2_blender_default import LIGHTS from .gltf2_blender_gather_cache import cached from . import gltf2_blender_gather_light_spots from .material import gltf2_blender_search_node_tree @@ -96,11 +97,7 @@ def __gather_spot(blender_lamp, export_settings) -> Optional[gltf2_io_lights_pun def __gather_type(blender_lamp, _) -> str: - return { - "POINT": "point", - "SUN": "directional", - "SPOT": "spot" - }[blender_lamp.type] + return LIGHTS[blender_lamp.type] def __gather_range(blender_lamp, export_settings) -> Optional[float]: diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py index 6583a7043821c5a4fd140eaef0dd962f192391b9..fd495fa1445b8faff01ade9c4978f695fd25645a 100755 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_nodes.py @@ -11,6 +11,7 @@ from ...io.com import gltf2_io from ...io.com import gltf2_io_extensions from ...io.exp.gltf2_io_user_extensions import export_user_extensions from ..com.gltf2_blender_extras import generate_extras +from ..com.gltf2_blender_default import LIGHTS from ..com import gltf2_blender_math from . import gltf2_blender_gather_tree from . import gltf2_blender_gather_skins @@ -31,7 +32,7 @@ def gather_node(vnode, export_settings): node = gltf2_io.Node( camera=__gather_camera(blender_object, export_settings), children=__gather_children(vnode, export_settings), - extensions=__gather_extensions(blender_object, export_settings), + extensions=__gather_extensions(vnode, export_settings), extras=__gather_extras(blender_object, export_settings), matrix=__gather_matrix(blender_object, export_settings), mesh=__gather_mesh(vnode, blender_object, export_settings), @@ -56,6 +57,8 @@ def gather_node(vnode, export_settings): def __gather_camera(blender_object, export_settings): + if not blender_object: + return if blender_object.type != 'CAMERA': return None @@ -160,11 +163,18 @@ def __find_parent_joint(joints, name): return None -def __gather_extensions(blender_object, export_settings): +def __gather_extensions(vnode, export_settings): + blender_object = vnode.blender_object extensions = {} - if export_settings["gltf_lights"] and (blender_object.type == "LAMP" or blender_object.type == "LIGHT"): + blender_lamp = None + if export_settings["gltf_lights"] and vnode.blender_type == VExportNode.INSTANCE: + if vnode.data.type in LIGHTS: + blender_lamp = vnode.data + elif export_settings["gltf_lights"] and blender_object is not None and (blender_object.type == "LAMP" or blender_object.type == "LIGHT"): blender_lamp = blender_object.data + + if blender_lamp is not None: light = gltf2_blender_gather_lights.gather_lights_punctual( blender_lamp, export_settings @@ -195,83 +205,97 @@ def __gather_matrix(blender_object, export_settings): # return blender_object.matrix_local return [] - def __gather_mesh(vnode, blender_object, export_settings): - if blender_object.type in ['CURVE', 'SURFACE', 'FONT']: + if blender_object and blender_object.type in ['CURVE', 'SURFACE', 'FONT']: return __gather_mesh_from_nonmesh(blender_object, export_settings) + if blender_object is None and type(vnode.data).__name__ not in ["Mesh"]: + return None #TODO + if blender_object is None: + # GN instance + blender_mesh = vnode.data + # Keep materials from the tmp mesh, but if no material, keep from object + materials = tuple(mat for mat in blender_mesh.materials) + if len(materials) == 1 and materials[0] is None: + materials = tuple(ms.material for ms in vnode.original_object.material_slots) + + uuid_for_skined_data = None + modifiers = None - if blender_object.type != "MESH": - return None + if blender_mesh is None: + return None - # For duplis instancer, when show is off -> export as empty - if vnode.force_as_empty is True: - return None + else: + if blender_object.type != "MESH": + return None + # For duplis instancer, when show is off -> export as empty + if vnode.force_as_empty is True: + return None + # Be sure that object is valid (no NaN for example) + res = blender_object.data.validate() + if res is True: + print_console("WARNING", "Mesh " + blender_object.data.name + " is not valid, and may be exported wrongly") - # Be sure that object is valid (no NaN for example) - res = blender_object.data.validate() - if res is True: - print_console("WARNING", "Mesh " + blender_object.data.name + " is not valid, and may be exported wrongly") + modifiers = blender_object.modifiers + if len(modifiers) == 0: + modifiers = None - modifiers = blender_object.modifiers - if len(modifiers) == 0: - modifiers = None + if export_settings['gltf_apply']: + if modifiers is None: # If no modifier, use original mesh, it will instance all shared mesh in a single glTF mesh + blender_mesh = blender_object.data + # Keep materials from object, as no modifiers are applied, so no risk that + # modifiers changed them + materials = tuple(ms.material for ms in blender_object.material_slots) + else: + armature_modifiers = {} + if export_settings['gltf_skins']: + # temporarily disable Armature modifiers if exporting skins + for idx, modifier in enumerate(blender_object.modifiers): + if modifier.type == 'ARMATURE': + armature_modifiers[idx] = modifier.show_viewport + modifier.show_viewport = False + + depsgraph = bpy.context.evaluated_depsgraph_get() + blender_mesh_owner = blender_object.evaluated_get(depsgraph) + blender_mesh = blender_mesh_owner.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph) + for prop in blender_object.data.keys(): + blender_mesh[prop] = blender_object.data[prop] + + if export_settings['gltf_skins']: + # restore Armature modifiers + for idx, show_viewport in armature_modifiers.items(): + blender_object.modifiers[idx].show_viewport = show_viewport + + # Keep materials from the newly created tmp mesh, but if no materials, keep from object + materials = tuple(mat for mat in blender_mesh.materials) + if len(materials) == 1 and materials[0] is None: + materials = tuple(ms.material for ms in blender_object.material_slots) - if export_settings['gltf_apply']: - if modifiers is None: # If no modifier, use original mesh, it will instance all shared mesh in a single glTF mesh + else: blender_mesh = blender_object.data + if not export_settings['gltf_skins']: + modifiers = None + else: + # Check if there is an armature modidier + if len([mod for mod in blender_object.modifiers if mod.type == "ARMATURE"]) == 0: + modifiers = None + # Keep materials from object, as no modifiers are applied, so no risk that # modifiers changed them materials = tuple(ms.material for ms in blender_object.material_slots) - else: - armature_modifiers = {} - if export_settings['gltf_skins']: - # temporarily disable Armature modifiers if exporting skins - for idx, modifier in enumerate(blender_object.modifiers): - if modifier.type == 'ARMATURE': - armature_modifiers[idx] = modifier.show_viewport - modifier.show_viewport = False - - depsgraph = bpy.context.evaluated_depsgraph_get() - blender_mesh_owner = blender_object.evaluated_get(depsgraph) - blender_mesh = blender_mesh_owner.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph) - for prop in blender_object.data.keys(): - blender_mesh[prop] = blender_object.data[prop] - - if export_settings['gltf_skins']: - # restore Armature modifiers - for idx, show_viewport in armature_modifiers.items(): - blender_object.modifiers[idx].show_viewport = show_viewport - - # Keep materials from the newly created tmp mesh - materials = tuple(mat for mat in blender_mesh.materials) - if len(materials) == 1 and materials[0] is None: - materials = tuple(ms.material for ms in blender_object.material_slots) - else: - blender_mesh = blender_object.data - # If no skin are exported, no need to have vertex group, this will create a cache miss - if not export_settings['gltf_skins']: - modifiers = None - else: - # Check if there is an armature modidier - if len([mod for mod in blender_object.modifiers if mod.type == "ARMATURE"]) == 0: - modifiers = None - # Keep materials from object, as no modifiers are applied, so no risk that - # modifiers changed them - materials = tuple(ms.material for ms in blender_object.material_slots) - - # retrieve armature - # Because mesh data will be transforms to skeleton space, - # we can't instantiate multiple object at different location, skined by same armature - uuid_for_skined_data = None - if export_settings['gltf_skins']: - for idx, modifier in enumerate(blender_object.modifiers): - if modifier.type == 'ARMATURE': - uuid_for_skined_data = vnode.uuid + + # retrieve armature + # Because mesh data will be transforms to skeleton space, + # we can't instantiate multiple object at different location, skined by same armature + uuid_for_skined_data = None + if export_settings['gltf_skins']: + for idx, modifier in enumerate(blender_object.modifiers): + if modifier.type == 'ARMATURE': + uuid_for_skined_data = vnode.uuid result = gltf2_blender_gather_mesh.gather_mesh(blender_mesh, uuid_for_skined_data, - blender_object.vertex_groups, + blender_object.vertex_groups if blender_object else None, modifiers, materials, None, @@ -329,10 +353,12 @@ def __gather_mesh_from_nonmesh(blender_object, export_settings): def __gather_name(blender_object, export_settings): + new_name = blender_object.name if blender_object else "GN Instance" + class GltfHookName: def __init__(self, name): self.name = name - gltf_hook_name = GltfHookName(blender_object.name) + gltf_hook_name = GltfHookName(new_name) export_user_extensions('gather_node_name_hook', export_settings, gltf_hook_name, blender_object) return gltf_hook_name.name @@ -360,7 +386,7 @@ def __gather_trans_rot_scale(vnode, export_settings): rot = __convert_swizzle_rotation(rot, export_settings) sca = __convert_swizzle_scale(sca, export_settings) - if vnode.blender_object.instance_type == 'COLLECTION' and vnode.blender_object.instance_collection: + if vnode.blender_object and vnode.blender_object.instance_type == 'COLLECTION' and vnode.blender_object.instance_collection: offset = -__convert_swizzle_location( vnode.blender_object.instance_collection.instance_offset, export_settings) @@ -389,7 +415,7 @@ def __gather_trans_rot_scale(vnode, export_settings): def gather_skin(vnode, export_settings): blender_object = export_settings['vtree'].nodes[vnode].blender_object - modifiers = {m.type: m for m in blender_object.modifiers} + modifiers = {m.type: m for m in blender_object.modifiers} if blender_object else {} if "ARMATURE" not in modifiers or modifiers["ARMATURE"].object is None: return None diff --git a/io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py b/io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py index ed4af136401b261fce7e1c1d6ff641bb5b3770cf..7fd39751347592976af525ca2ec7809089a77662 100644 --- a/io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py +++ b/io_scene_gltf2/blender/exp/gltf2_blender_gather_tree.py @@ -23,6 +23,10 @@ class VExportNode: LIGHT = 4 CAMERA = 5 COLLECTION = 6 + INSTANCE = 7 # For instances of GN + + INSTANCIER = 8 + NOT_INSTANCIER = 9 # Parent type, to be set on child regarding its parent NO_PARENT = 54 @@ -67,6 +71,12 @@ class VExportNode: # glTF self.node = None + # For mesh instance data of GN instances + self.data = None + self.materials = None + + self.is_instancier = VExportNode.NOT_INSTANCIER + def add_child(self, uuid): self.children.append(uuid) @@ -77,7 +87,7 @@ class VExportNode: def recursive_display(self, tree, mode): if mode == "simple": for c in self.children: - print(tree.nodes[c].uuid, self.blender_object.name, "/", self.blender_bone.name if self.blender_bone else "", "-->", tree.nodes[c].blender_object.name, "/", tree.nodes[c].blender_bone.name if tree.nodes[c].blender_bone else "" ) + print(tree.nodes[c].uuid, self.blender_object.name if self.blender_object is not None else "GN" + self.data.name, "/", self.blender_bone.name if self.blender_bone else "", "-->", tree.nodes[c].blender_object.name if tree.nodes[c].blender_object else "GN" + tree.nodes[c].data.name, "/", tree.nodes[c].blender_bone.name if tree.nodes[c].blender_bone else "" ) tree.nodes[c].recursive_display(tree, mode) class VExportTree: @@ -116,22 +126,27 @@ class VExportTree: for blender_object in [obj.original for obj in scene_eval.objects if obj.parent is None]: self.recursive_node_traverse(blender_object, None, None, Matrix.Identity(4), False, blender_children) - def recursive_node_traverse(self, blender_object, blender_bone, parent_uuid, parent_coll_matrix_world, delta, blender_children, armature_uuid=None, dupli_world_matrix=None, is_children_in_collection=False): + def recursive_node_traverse(self, blender_object, blender_bone, parent_uuid, parent_coll_matrix_world, delta, blender_children, armature_uuid=None, dupli_world_matrix=None, data=None, original_object=None, is_children_in_collection=False): node = VExportNode() node.uuid = str(uuid.uuid4()) node.parent_uuid = parent_uuid node.set_blender_data(blender_object, blender_bone) + if blender_object is None: + node.data = data + node.original_object = original_object # add to parent if needed if parent_uuid is not None: self.add_children(parent_uuid, node.uuid) - if self.nodes[parent_uuid].blender_type == VExportNode.COLLECTION: + if self.nodes[parent_uuid].blender_type == VExportNode.COLLECTION or original_object is not None: self.nodes[parent_uuid].children_type[node.uuid] = VExportNode.CHILDREN_IS_IN_COLLECTION if is_children_in_collection is True else VExportNode.CHILDREN_REAL else: self.roots.append(node.uuid) # Set blender type - if blender_bone is not None: + if blender_object is None: #GN instance + node.blender_type = VExportNode.INSTANCE + elif blender_bone is not None: node.blender_type = VExportNode.BONE self.nodes[armature_uuid].bones[blender_bone.name] = node.uuid node.use_deform = blender_bone.id_data.data.bones[blender_bone.name].use_deform @@ -229,7 +244,7 @@ class VExportTree: # Force empty ? # For duplis, if instancer is not display, we should create an empty - if blender_object.is_instancer is True and blender_object.show_instancer_for_render is False: + if blender_object and blender_object.is_instancer is True and blender_object.show_instancer_for_render is False: node.force_as_empty = True # Storing this node @@ -237,6 +252,10 @@ class VExportTree: ###### Manage children ###### + # GN instance have no children + if blender_object is None: + return + # standard children (of object, or of instance collection) if blender_bone is None: for child_object in blender_children[blender_object]: @@ -276,6 +295,21 @@ class VExportTree: for (dupl, mat) in [(dup.object.original, dup.matrix_world.copy()) for dup in depsgraph.object_instances if dup.parent and id(dup.parent.original) == id(blender_object)]: self.recursive_node_traverse(dupl, None, node.uuid, parent_coll_matrix_world, new_delta or delta, blender_children, dupli_world_matrix=mat) + # Geometry Nodes instances + if self.export_settings['gltf_gn_mesh'] is True: + # Do not force export as empty + # Because GN graph can have both geometry and instances + depsgraph = bpy.context.evaluated_depsgraph_get() + eval = blender_object.evaluated_get(depsgraph) + for inst in depsgraph.object_instances: # use only as iterator + if inst.parent == eval: + if not inst.is_instance: + continue + if type(inst.object.data).__name__ == "Mesh" and len(inst.object.data.vertices) == 0: + continue # This is nested instances, and this mesh has no vertices, so is an instancier for other instances + node.is_instancier = VExportNode.INSTANCIER + self.recursive_node_traverse(None, None, node.uuid, parent_coll_matrix_world, new_delta or delta, blender_children, dupli_world_matrix=inst.matrix_world.copy(), data=inst.object.data, original_object=blender_object, is_children_in_collection=True) + def get_all_objects(self): return [n.uuid for n in self.nodes.values() if n.blender_type != VExportNode.BONE] @@ -320,7 +354,7 @@ class VExportTree: def display(self, mode): if mode == "simple": for n in self.roots: - print(self.nodes[n].uuid, "Root", self.nodes[n].blender_object.name, "/", self.nodes[n].blender_bone.name if self.nodes[n].blender_bone else "" ) + print(self.nodes[n].uuid, "Root", self.nodes[n].blender_object.name if self.nodes[n].blender_object else "GN instance", "/", self.nodes[n].blender_bone.name if self.nodes[n].blender_bone else "" ) self.nodes[n].recursive_display(self, mode) def filter_tag(self): @@ -355,7 +389,7 @@ class VExportTree: print("This should not happen!") for child in self.nodes[uuid].children: - if self.nodes[uuid].blender_type == VExportNode.COLLECTION: + if self.nodes[uuid].blender_type == VExportNode.COLLECTION or self.nodes[uuid].is_instancier == VExportNode.INSTANCIER: # We need to split children into 2 categories: real children, and objects inside the collection if self.nodes[uuid].children_type[child] == VExportNode.CHILDREN_IS_IN_COLLECTION: self.recursive_filter_tag(child, self.nodes[uuid].keep_tag) @@ -419,6 +453,10 @@ class VExportTree: def node_filter_inheritable_is_kept(self, uuid): + if self.nodes[uuid].blender_object is None: + # geometry node instances + return True + if self.export_settings['gltf_selected'] and self.nodes[uuid].blender_object.select_get() is False: return False