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