Last active
July 21, 2024 10:43
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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