Skip to content

Instantly share code, notes, and snippets.

@Oppodelldog
Last active July 21, 2024 10:43
Show Gist options
  • Save Oppodelldog/9b1d53f411420e32bae485536418148d to your computer and use it in GitHub Desktop.
Save Oppodelldog/9b1d53f411420e32bae485536418148d to your computer and use it in GitHub Desktop.
blender add on that supports export to godot and creating mesh colliders in blender
bl_info = {
"name": "Godot Colliders and Export Addon",
"blender": (3, 0, 0),
"category": "Object",
"description": "Creates and removes collider objects and exports GLTF files easily",
}
import bpy
import os
import re
bpy.types.Scene.gltf_export_target_path = bpy.props.StringProperty()
addon_keymaps = []
# Collider Object Addon Classes
class OBJECT_OT_create_collider(bpy.types.Operator):
bl_idname = "object.create_collider"
bl_label = "Create Collider"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
selected_objects = bpy.context.selected_objects
if not selected_objects:
self.report({'WARNING'}, "No objects selected")
return {'CANCELLED'}
for original in selected_objects:
new_object_name = "_collider-" + original.name + "-colonly"
# Remove existing collider with the same name
for child in original.children:
if child.name == new_object_name:
bpy.data.objects.remove(child, do_unlink=True)
# Duplicate the object and set it as a child with the new name
bpy.ops.object.select_all(action='DESELECT')
original.select_set(True)
bpy.context.view_layer.objects.active = original
bpy.ops.object.duplicate(linked=True)
new_obj = bpy.context.selected_objects[0]
# Convert geometry nodes to mesh
bpy.context.view_layer.objects.active = new_obj
bpy.ops.object.convert(target='MESH')
# Set the duplicate's transformation properties to match the original object
new_obj.location = original.location
new_obj.rotation_euler = original.rotation_euler
new_obj.scale = original.scale
new_obj.parent = original
new_obj.name = new_object_name
new_obj.hide_viewport = True
# todo: check geometry nodes before collider creation, existing geom nodes modifier will prevent collider creation
for modifier in new_obj.modifiers:
if modifier.type == 'NODES':
new_obj.modifiers.remove(modifier)
# restore original selection
bpy.ops.object.select_all(action='DESELECT')
for obj in selected_objects:
obj.select_set(True)
return {'FINISHED'}
class OBJECT_OT_recreate_colliders(bpy.types.Operator):
bl_idname = "object.recreate_colliders"
bl_label = "Recreate Colliders"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
selected_objects = bpy.context.selected_objects
if not selected_objects:
self.report({'WARNING'}, "No objects selected")
return {'CANCELLED'}
# select all colliders using GodotSelectColliders
bpy.ops.wm.godot_select_colliders()
# make a list of all collider parents
collider_parents = []
for obj in bpy.context.selected_objects:
if is_collider(obj):
collider_parents.append(obj.parent)
# remove all colliders
bpy.ops.object.remove_colliders()
# select parents
bpy.ops.object.select_all(action='DESELECT')
for obj in collider_parents:
obj.select_set(True)
# create new colliders
bpy.ops.object.create_collider()
# restore original selection
bpy.ops.object.select_all(action='DESELECT')
for obj in selected_objects:
obj.select_set(True)
return {'FINISHED'}
class OBJECT_OT_remove_colliders(bpy.types.Operator):
bl_idname = "object.remove_colliders"
bl_label = "Remove Colliders"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
selected_objects = bpy.context.selected_objects
if not selected_objects:
self.report({'WARNING'}, "No objects selected")
return {'CANCELLED'}
colliders_to_remove = []
def collect_colliders(obj):
if obj.name.startswith("_collider-") and obj.name.endswith("-colonly"):
colliders_to_remove.append(obj)
for child in obj.children:
collect_colliders(child)
# Collect colliders from all selected objects
for original in selected_objects:
collect_colliders(original)
# Remove collected colliders
for collider in colliders_to_remove:
bpy.data.objects.remove(collider, do_unlink=True)
return {'FINISHED'}
class SetGLFTExportTargetPath(bpy.types.Operator):
bl_idname = "wm.godot_gltf_export_set_export_target_path"
bl_label = "Set Export Target Path"
gltf_export_target_path: bpy.props.StringProperty(name="Path")
def execute(self, context):
scene = context.scene
scene.gltf_export_target_path = self.gltf_export_target_path
return {'FINISHED'}
def invoke(self, context, event):
wm = context.window_manager
self.gltf_export_target_path = context.scene.gltf_export_target_path
return wm.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.prop(self, "gltf_export_target_path")
class GodotShowColliders(bpy.types.Operator):
bl_idname = "wm.godot_show_colliders"
bl_label = "Show Colliders"
def execute(self, context):
selected_objects = bpy.context.selected_objects
if not selected_objects:
self.report({'WARNING'}, "No objects selected")
return {'CANCELLED'}
for obj in selected_objects:
showColliders(obj)
return {'FINISHED'}
class GodotHideColliders(bpy.types.Operator):
bl_idname = "wm.godot_hide_colliders"
bl_label = "Hide Colliders"
def execute(self, context):
selected_objects = bpy.context.selected_objects
if not selected_objects:
self.report({'WARNING'}, "No objects selected")
return {'CANCELLED'}
for obj in selected_objects:
hideColliders(obj)
return {'FINISHED'}
class GodotSelectColliders(bpy.types.Operator):
bl_idname = "wm.godot_select_colliders"
bl_label = "Select Colliders"
def execute(self, context):
selected_objects = bpy.context.selected_objects
if not selected_objects:
self.report({'WARNING'}, "No objects selected")
return {'CANCELLED'}
bpy.ops.object.select_all(action='DESELECT')
for obj in selected_objects:
selectColliders(obj)
return {'FINISHED'}
class LowercaseEverything(bpy.types.Operator):
bl_idname = "wm.lowercase_everything"
bl_label = "Lowercase Everything"
def execute(self, context):
lowercase_and_underscore_names()
return {'FINISHED'}
class GodotGLTFExportSelected(bpy.types.Operator):
bl_idname = "wm.godot_gltf_export_selected"
bl_label = "Export Selected"
def execute(self, context):
print("Godot Export: Starting")
scene = context.scene
if not scene.gltf_export_target_path:
self.report({'ERROR'}, "gltf export path not set")
return {'FINISHED'}
bpy.ops.wm.godot_show_colliders()
original_selected = allSelected(bpy.context.selected_objects)
selectList(original_selected, False);
export_count = 0
for obj in original_selected:
collectionName = get_collection_name(obj)
path = "{0}\\{1}\\{2}".format(scene.gltf_export_target_path, collectionName, obj.name)
if not os.path.exists(path):
os.makedirs(path)
filename = r'{0}\\{1}.gltf'.format(path, obj.name)
selectBranch(obj, True)
bpy.ops.export_scene.gltf(filepath=filename,
export_format="GLB",
use_selection=True,
use_visible=False,
export_apply=True,
export_gn_mesh=True
)
selectBranch(obj, False)
export_count += 1
print("Godot Export: exported to {0}".format(filename))
selectList(original_selected, True)
bpy.ops.wm.godot_hide_colliders()
self.report({'INFO'}, "Godot Export finished ({0})".format(export_count))
print("Godot Export: Finished")
return {'FINISHED'}
# Utility Functions
def is_collider(obj):
return obj.name.startswith("_collider-") and obj.name.endswith("-colonly")
def get_collection_name(obj):
for collection in bpy.data.collections:
if obj.name in collection.objects:
return collection.name
return None
def showColliders(obj):
if is_collider(obj):
obj.hide_viewport = False
for child in obj.children:
showColliders(child)
def hideColliders(obj):
if is_collider(obj):
obj.hide_viewport = True
for child in obj.children:
hideColliders(child)
def selectColliders(obj):
if is_collider(obj):
obj.hide_viewport = False
obj.select_set(True)
for child in obj.children:
selectColliders(child)
def selectList(objs, select):
for child in objs:
child.select_set(select)
def selectBranch(obj, select):
obj.select_set(select)
for child in obj.children:
child.select_set(select)
selectBranch(child, select)
def allSelected(objects):
all_selected = []
for obj in objects:
if obj.select_get():
all_selected.append(obj)
obj.select_set(False)
all_selected.extend(allSelected(obj.children))
return all_selected
def to_snake_case(name):
"""Converts CamelCase or PascalCase to snake_case."""
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
def lowercase_and_underscore_names():
# Lowercase and underscore object names
for obj in bpy.data.objects:
obj.name = to_snake_case(obj.name)
# Lowercase and underscore scene names
for scene in bpy.data.scenes:
scene.name = to_snake_case(scene.name)
# Lowercase and underscore mesh names
for mesh in bpy.data.meshes:
mesh.name = to_snake_case(mesh.name)
# Lowercase and underscore armature names and their bones
for armature in bpy.data.armatures:
armature.name = to_snake_case(armature.name)
for bone in armature.bones:
bone.name = to_snake_case(bone.name)
# Lowercase and underscore material names
for material in bpy.data.materials:
material.name = to_snake_case(material.name)
# Lowercase and underscore vertex group names for all objects
for obj in bpy.data.objects:
if obj.type == 'MESH':
for vgroup in obj.vertex_groups:
vgroup.name = to_snake_case(vgroup.name)
# Panel Class
class OBJECT_PT_collider_and_gltf_panel(bpy.types.Panel):
bl_label = "Godot Export"
bl_idname = "OBJECT_PT_collider_and_gltf_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Godot'
def draw(self, context):
layout = self.layout
layout.operator("object.create_collider")
layout.operator("object.remove_colliders")
layout.operator("object.recreate_colliders")
layout.operator("wm.godot_select_colliders")
layout.operator("wm.godot_show_colliders")
layout.operator("wm.godot_hide_colliders")
layout.operator("wm.lowercase_everything")
layout.separator()
layout.operator("wm.godot_gltf_export_set_export_target_path")
layout.operator("wm.godot_gltf_export_selected")
# Register and Unregister Classes
classes = [
OBJECT_OT_create_collider,
OBJECT_OT_remove_colliders,
OBJECT_OT_recreate_colliders,
SetGLFTExportTargetPath,
GodotSelectColliders,
GodotShowColliders,
GodotHideColliders,
LowercaseEverything,
GodotGLTFExportSelected,
OBJECT_PT_collider_and_gltf_panel
]
def menu_func(self, context):
self.layout.operator(GodotGLTFExportSelected.bl_idname, text="Godot Export Selected")
self.layout.operator(SetGLFTExportTargetPath.bl_idname, text="Godot Export Set Target Path")
self.layout.operator(GodotShowColliders.bl_idname, text="Godot Show Colliders")
self.layout.operator(GodotHideColliders.bl_idname, text="Godot Hide Colliders")
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.VIEW3D_MT_view.append(menu_func)
wm = bpy.context.window_manager
km = wm.keyconfigs.addon.keymaps.new(name="Object Mode", space_type="EMPTY")
kmi = km.keymap_items.new(GodotGLTFExportSelected.bl_idname, 'D', 'PRESS', ctrl=True, shift=True)
addon_keymaps.append((km, kmi))
km = wm.keyconfigs.addon.keymaps.new(name="Mesh", space_type="EMPTY")
kmi = km.keymap_items.new(GodotGLTFExportSelected.bl_idname, 'D', 'PRESS', ctrl=True, shift=True)
addon_keymaps.append((km, kmi))
km = wm.keyconfigs.addon.keymaps.new(name="Outliner", space_type="OUTLINER")
kmi = km.keymap_items.new(GodotGLTFExportSelected.bl_idname, 'D', 'PRESS', ctrl=True, shift=True)
addon_keymaps.append((km, kmi))
km = wm.keyconfigs.addon.keymaps.new(name="Sculpt Mode", space_type="EMPTY")
kmi = km.keymap_items.new(GodotGLTFExportSelected.bl_idname, 'D', 'PRESS', ctrl=True, shift=True)
addon_keymaps.append((km, kmi))
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
wm = bpy.context.window_manager
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
if __name__ == "__main__":
register()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment