diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40bb4e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode/ +__pycache__/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 4a22f79..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "python.linting.pylintArgs": [ - "--init-hook", - "import sys; sys.path.append('D:\\Programmering\\2.90')" - ], - "python.autoComplete.extraPaths": [ - "D:\\Programmering\\2.90" - ], - "python.linting.pylintEnabled": false, - "python.linting.flake8Enabled": true, - "python.linting.enabled": true, -} \ No newline at end of file diff --git a/__init__.py b/__init__.py index a75b76c..6cf25f4 100644 --- a/__init__.py +++ b/__init__.py @@ -1,6 +1,6 @@ bl_info = { "name" : "SUT - Sandbox Unreal Tools", - "author" : "Exomemphiz & Viter", + "author" : "ExoMemphiz & Viter", "description" : "Makes prototyping / greyboxing levels in unreal engine an easier process. Idea is to reduce 'double work' when having to place assets multiple times when you are prototyping anyway.", "blender" : (2, 91, 0), "version" : (0, 0, 1), @@ -9,29 +9,33 @@ "category" : "Generic" } -import bpy +from . operator.sut_op import Sut_OT_Operator +from . panel.sut_properties import SutProperties +from . panel.sut_panel import Sut_PT_Panel -from . sut_op import Sut_OT_Operator -from . sut_properties import SutProperties -from . sut_panel import Sut_PT_Panel - -from bpy.utils import (register_class, unregister_class) +from bpy.utils import ( + register_class, + unregister_class +) from bpy.props import PointerProperty -classes = (Sut_OT_Operator, Sut_PT_Panel, SutProperties) +from bpy.types import Scene + +classes = ( + Sut_OT_Operator, + Sut_PT_Panel, + SutProperties +) def register(): - from bpy.utils import register_class for cls in classes: register_class(cls) - - bpy.types.Scene.my_tool = PointerProperty(type=SutProperties) + Scene.sut_tool = PointerProperty(type=SutProperties) def unregister(): - from bpy.utils import unregister_class for cls in reversed(classes): unregister_class(cls) - del bpy.types.Scene.my_tool + del Scene.sut_tool if __name__ == "__main__": diff --git a/__pycache__/__init__.cpython-37.pyc b/__pycache__/__init__.cpython-37.pyc deleted file mode 100644 index 13f2bc7..0000000 Binary files a/__pycache__/__init__.cpython-37.pyc and /dev/null differ diff --git a/__pycache__/my_properties.cpython-37.pyc b/__pycache__/my_properties.cpython-37.pyc deleted file mode 100644 index 8f4e518..0000000 Binary files a/__pycache__/my_properties.cpython-37.pyc and /dev/null differ diff --git a/__pycache__/sut_op.cpython-37.pyc b/__pycache__/sut_op.cpython-37.pyc deleted file mode 100644 index 6fc2cab..0000000 Binary files a/__pycache__/sut_op.cpython-37.pyc and /dev/null differ diff --git a/__pycache__/sut_panel.cpython-37.pyc b/__pycache__/sut_panel.cpython-37.pyc deleted file mode 100644 index 85891bc..0000000 Binary files a/__pycache__/sut_panel.cpython-37.pyc and /dev/null differ diff --git a/__pycache__/sut_properties.cpython-37.pyc b/__pycache__/sut_properties.cpython-37.pyc deleted file mode 100644 index 3f7a78c..0000000 Binary files a/__pycache__/sut_properties.cpython-37.pyc and /dev/null differ diff --git a/__pycache__/test_op.cpython-37.pyc b/__pycache__/test_op.cpython-37.pyc deleted file mode 100644 index 99e06da..0000000 Binary files a/__pycache__/test_op.cpython-37.pyc and /dev/null differ diff --git a/__pycache__/test_panel.cpython-37.pyc b/__pycache__/test_panel.cpython-37.pyc deleted file mode 100644 index 597f527..0000000 Binary files a/__pycache__/test_panel.cpython-37.pyc and /dev/null differ diff --git a/addon.png b/assets/addon.png similarity index 100% rename from addon.png rename to assets/addon.png diff --git a/functions/sut_make_mesh.py b/functions/sut_make_mesh.py new file mode 100644 index 0000000..930b592 --- /dev/null +++ b/functions/sut_make_mesh.py @@ -0,0 +1,72 @@ +import bpy + +from . sut_unwrap import auto_unwrap +from . sut_utils import ( + copy_selected_objects, + select_children_objects_recursively +) + +def make_single_mesh(selected_collection, context): + # For scene / own global variables + sut_tool = context.scene.sut_tool + + # Go into object mode (maybe actually check stuff instead of this) + try: + bpy.ops.object.mode_set(mode="OBJECT") + except: + pass + + # Deselect everything + bpy.ops.object.select_all(action='DESELECT') + + # Temp work + select_children_objects_recursively(selected_collection) + + # Clone the selected collection + temp_collection_name = 'SUT_COL_' + selected_collection.name + temp_collection = bpy.data.collections.new(name=temp_collection_name) + copy_selected_objects(temp_collection, 0) + bpy.context.scene.collection.children.link(temp_collection) + + # Deselect everything + bpy.ops.object.select_all(action='DESELECT') + + # Select all objects as active + select_children_objects_recursively(temp_collection) + + # Merge the objects + bpy.ops.object.join() + new_merged = temp_collection.all_objects[0] + + # Apply transform (Fixes scaling when uv unwrapping + sets object origin to world center) + new_merged.select_set(True) + bpy.ops.object.transform_apply( + location=True, + rotation=False, + scale=True + ) + new_merged.select_set(False) + + # Prepare naming for the final mesh + mesh_name = 'SM_' + selected_collection.name + mesh_collection = bpy.data.collections[sut_tool.final_col_name] + + # Check if mesh exists, remove if it does + try: + old_mesh = bpy.data.collections[sut_tool.final_col_name].objects[mesh_name] + bpy.data.objects.remove(old_mesh) + except: + pass + + # Set the new name, now that it is available + new_merged.name = mesh_name + + # Move to "Mesh" collection + mesh_collection.objects.link(new_merged) + temp_collection.objects.unlink(new_merged) + + # Delete the temp collection + bpy.data.collections.remove(temp_collection) + + # Auto unwrap + auto_unwrap(new_merged, context) \ No newline at end of file diff --git a/functions/sut_unwrap.py b/functions/sut_unwrap.py new file mode 100644 index 0000000..0df0f6f --- /dev/null +++ b/functions/sut_unwrap.py @@ -0,0 +1,27 @@ +import bpy +import math + +from bpy.ops import ( + object as obj, + mesh, + uv +) + +def auto_unwrap(input_obj, context): + '''Uses the smart uv unwrapping tool with desired values''' + sut_tool = context.scene.sut_tool + + obj.select_all(action='DESELECT') + if (input_obj.type == 'MESH'): + input_obj.select_set(True) + obj.mode_set(mode="EDIT") + mesh.select_all(action='SELECT') # for all faces + uv.smart_project( + angle_limit=math.radians(66), + island_margin=sut_tool.sut_island_margin, + area_weight=0.0, + correct_aspect=True, + scale_to_bounds=False + ) + obj.mode_set(mode="OBJECT") + input_obj.select_set(False) \ No newline at end of file diff --git a/functions/sut_utils.py b/functions/sut_utils.py new file mode 100644 index 0000000..2041bd3 --- /dev/null +++ b/functions/sut_utils.py @@ -0,0 +1,55 @@ +import bpy + +def select_children_objects_recursively(input_col): + '''Recursively selects all objects within a collection, and its children''' + # Select all the objects + for obj in input_col.all_objects: + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + + for child_col in input_col.children: + select_children_objects_recursively(child_col) + +def copy_selected_objects(to_col, linked): + '''Duplicates selected objects into a desired collection''' + for o in bpy.context.selected_objects: + dupe = o.copy() + if not linked and o.data: + dupe.data = dupe.data.copy() + to_col.objects.link(dupe) + +def use_texel_density(sut_tool): + '''Uses the Texel Density addon with desired values''' + bpy.ops.object.preset_set(td_value=sut_tool.sut_texel_density) + bpy.context.scene.td.texture_size = sut_tool.sut_texture_size + bpy.ops.object.texel_density_set() + bpy.ops.object.select_all(action='DESELECT') + +def validate_collection_names(self, sut_tool): + """Ensures greybox collection is made & final collection either exists or will be created + + Returns: + bool: Returning value + """ + + # Check if collections don't exist, and create them + try: + bpy.data.collections[sut_tool.greybox_col_name] + except: + self.report( + {'ERROR'}, + "You have not created a '" + sut_tool.greybox_col_name + "' greybox collection yet!" + ) + return False + try: + bpy.data.collections[sut_tool.final_col_name] + except: + new_col = bpy.data.collections.new(name=sut_tool.final_col_name) + bpy.context.scene.collection.children.link(new_col) + + return True + +def hide_collection_viewport(collection_name, flag): + '''Hides or Shows a specified collection, based on which flag is given''' + # We don't know what is going on but it works + bpy.context.scene.view_layers[0].layer_collection.children[collection_name].hide_viewport = flag \ No newline at end of file diff --git a/operator/sut_op.py b/operator/sut_op.py new file mode 100644 index 0000000..5b544e9 --- /dev/null +++ b/operator/sut_op.py @@ -0,0 +1,54 @@ +import bpy + +from .. functions.sut_make_mesh import make_single_mesh +from .. functions.sut_utils import ( + hide_collection_viewport, + select_children_objects_recursively, + use_texel_density, + validate_collection_names +) + +class Sut_OT_Operator(bpy.types.Operator): + bl_idname = "view3d.sut" + bl_label = "SUT" + bl_description = "SUT" + + def execute(self, context): + # For scene / own global variables + sut_tool = context.scene.sut_tool + + if not validate_collection_names(self, sut_tool): + return {'FINISHED WITH ERRORS'} + + # Unhide the Greybox Collection + hide_collection_viewport(sut_tool.greybox_col_name, False) + + # Unhide the Final Collection + hide_collection_viewport(sut_tool.final_col_name, False) + + + # Cleanup Mesh Collection + for obj in bpy.data.collections[sut_tool.final_col_name].all_objects: + bpy.data.objects.remove(obj) + + # Every collection gets merged into it's own unique mesh (or all collections is a single big mesh) + greybox_collection = bpy.data.collections[sut_tool.greybox_col_name] + if sut_tool.every_col_is_own_mesh is True: + # Get final collection, loop over every child collection and run make_single_mesh + for col in greybox_collection.children: + make_single_mesh(col, context) + else: + make_single_mesh(greybox_collection, context) + + # For every single object in the final collection, set the texel density + final_collection = bpy.data.collections[sut_tool.final_col_name] + select_children_objects_recursively(final_collection) + + use_texel_density(sut_tool) + + if sut_tool.sut_send_to_unreal is True: + bpy.ops.wm.send2ue() + + # Hide the Collection + hide_collection_viewport(sut_tool.final_col_name, True) + return {'FINISHED'} diff --git a/panel/sut_panel.py b/panel/sut_panel.py new file mode 100644 index 0000000..4ecaef5 --- /dev/null +++ b/panel/sut_panel.py @@ -0,0 +1,33 @@ +import bpy + +from bpy.types import Panel + +# Add to this list after adding a property to SutProperties +UI_PROPERTIES = [ + "sut_island_margin", + "sut_angle_limit", + "sut_texture_size", + "sut_texel_density", + "greybox_col_name", + "final_col_name", + "every_col_is_own_mesh", + "sut_send_to_unreal" +] + +class Sut_PT_Panel(Panel): + bl_idname = "SUT_PT_Panel" + bl_label = "Sandbox Unreal Tools" + bl_category = "SUT" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + + def draw(self, context): + layout = self.layout + scene = context.scene + sut_tool = scene.sut_tool + + for prop in UI_PROPERTIES: + layout.prop(sut_tool, prop) + + row = layout.row() + row.operator('view3d.sut', text="SUT") \ No newline at end of file diff --git a/sut_properties.py b/panel/sut_properties.py similarity index 56% rename from sut_properties.py rename to panel/sut_properties.py index 2a484db..416c1cf 100644 --- a/sut_properties.py +++ b/panel/sut_properties.py @@ -1,59 +1,83 @@ import bpy -from bpy.props import (BoolProperty, StringProperty, FloatProperty, EnumProperty) +from bpy.props import ( + BoolProperty, + EnumProperty, + FloatProperty, + StringProperty +) from bpy.types import PropertyGroup - +# After adding a property, add it as a string to the UI_PROPERTIES constant in sut_panel.py class SutProperties(PropertyGroup): + sut_island_margin: FloatProperty( name="UV Island Margin", description="Bigger greyboxes might need a bigger island margin when smart uv unwrapping", - step=0.01, min=0, max=5, + step=0.01, precision=3, default=0.005 - ) + ) + + sut_angle_limit: FloatProperty( + name="UV Degree Angle Limit", + description="Given in degrees, it will automatically be converted to radians", + min=0, + max=360, + step=1, + precision=1, + default=66 + ) + sut_texel_density: EnumProperty( name="Texel Density", description="Set texel density in the Texel Density addon", - items=[ ("40.96", "40.96", ""), - ("20.48", "20.48", ""), - ("10.24", "10.24", ""), - ("5.12", "5.12", ""), - ("2.56", "2.56", ""), - ("1.28", "1.28", ""), - ("0.64", "0.64", ""), - ], + items=[ + ("40.96", "40.96", ""), + ("20.48", "20.48", ""), + ("10.24", "10.24", ""), + ("5.12", "5.12", ""), + ("2.56", "2.56", ""), + ("1.28", "1.28", ""), + ("0.64", "0.64", ""), + ], default="20.48", - ) + ) + sut_texture_size: EnumProperty( name="Texture Size", description="Set texture size in the Texel Density addon", - items=[ ("0", "512px", ""), - ("1", "1024px", ""), - ("2", "2048px", ""), - ("3", "4096px", "") - ], + items=[ + ("0", "512px", ""), + ("1", "1024px", ""), + ("2", "2048px", ""), + ("3", "4096px", "") + ], default="2", - ) + ) + every_col_is_own_mesh: BoolProperty( name="Split collection result", description="Check if you want every collection to be their own seperate merged mesh\n(Origin will still be world origin)", default = False - ) + ) + sut_send_to_unreal: BoolProperty( name="Send to Unreal Engine", description="Avoids 2 clicks", default = False - ) + ) + greybox_col_name: StringProperty( - name="Greybox Col Name", + name="Greybox Coll. Name", description="The greyboxing collection name", default = "SUT" - ) + ) + final_col_name: StringProperty( - name="Final Col Name", + name="Final Coll. Name", description="Final collection for unreal engine, default is 'Mesh' from the UE Tools addon", default = "Mesh" - ) + ) diff --git a/readme.md b/readme.md index 94ba75d..93ddd31 100644 --- a/readme.md +++ b/readme.md @@ -1,17 +1,19 @@ # Sandbox Unreal Tools (SUT) + Tools made for fast prototyping / greyboxing levels in unreal to avoid some repeated actions (auto projecting uv's, sending to unreal, texel density, merging etc...) + +# Requirements Depends on the following addons: -1. Texel Density Checker 3 https://github.com/mrven/Blender-Texel-Density-Checker | https://gumroad.com/l/CEIOR -2. Send To Unreal https://github.com/EpicGames/BlenderTools (You need to follow this: https://www.unrealengine.com/en-US/blog/download-our-new-blender-addons) +1. [Texel Density Checker 3 Github](https://github.com/mrven/Blender-Texel-Density-Checker) | https://gumroad.com/l/CEIOR +2. [Send To Unreal Github](https://github.com/EpicGames/BlenderTools) (You need to follow this: https://www.unrealengine.com/en-US/blog/download-our-new-blender-addons) # Setup -1. Setup unreal use these folders: -- bla -- bla 2 - -2. Whatever +1. Setup TODO +![SUT](assets/addon.png) -![SUT](addon.png) \ No newline at end of file +# Future Features +1. Landscape Synchronization (Rough) (Landscape in unreal -> synchronize to blender and greybox back and forth) +2. Instance Dupliactes in blender to world position in unreal (Make your scene in blender with a modular kit) \ No newline at end of file diff --git a/sut_op.py b/sut_op.py deleted file mode 100644 index b102f66..0000000 --- a/sut_op.py +++ /dev/null @@ -1,152 +0,0 @@ -import bpy -import math -import copy - -def auto_unwrap(input_obj, context): - sut_tool = context.scene.my_tool - - bpy.ops.object.select_all(action='DESELECT') - if (input_obj.type == 'MESH'): - input_obj.select_set(True) - bpy.ops.object.mode_set(mode="EDIT") - bpy.ops.mesh.select_all(action='SELECT') # for all faces - bpy.ops.uv.smart_project(angle_limit=math.radians(66), island_margin=sut_tool.sut_island_margin, area_weight=0.0, correct_aspect=True, scale_to_bounds=False) - bpy.ops.object.mode_set(mode="OBJECT") - input_obj.select_set(False) - -def copy_selected_objects_(to_col, linked): - for o in bpy.context.selected_objects: - dupe = o.copy() - if not linked and o.data: - dupe.data = dupe.data.copy() - to_col.objects.link(dupe) - -def select_children_objects_recursively(input_col): - # Select all the objects - for obj in input_col.all_objects: - obj.select_set(True) - bpy.context.view_layer.objects.active = obj - - for child_col in input_col.children: - select_children_objects_recursively(child_col) - -def make_single_mesh(selected_collection, context): - # For scene / own global variables - sut_tool = context.scene.my_tool - - # Go into object mode (maybe actually check stuff instead of this) - try: - bpy.ops.object.mode_set(mode="OBJECT") - except: - dont_complain = 85 - - # Deselect everything - bpy.ops.object.select_all(action='DESELECT') - - # Temp work - select_children_objects_recursively(selected_collection) - - # Clone the selected collection - temp_collection_name = 'SUT_COL_' + selected_collection.name - temp_collection = bpy.data.collections.new(name=temp_collection_name) - copy_selected_objects_(temp_collection, 0) - bpy.context.scene.collection.children.link(temp_collection) - - # Deselect everything - bpy.ops.object.select_all(action='DESELECT') - - # Select all objects as active - select_children_objects_recursively(temp_collection) - - # Merge the objects - bpy.ops.object.join() - new_merged = temp_collection.all_objects[0] - - # Apply transform (Fixes scaling when uv unwrapping + sets object origin to world center) - new_merged.select_set(True) - bpy.ops.object.transform_apply(location=True, rotation=False, scale=True) - new_merged.select_set(False) - - # Prepare naming for the final mesh - mesh_name = 'SM_' + selected_collection.name - mesh_collection = bpy.data.collections[sut_tool.final_col_name] - - # Check if mesh exists, remove if it does - try: - old_mesh = bpy.data.collections[sut_tool.final_col_name].objects[mesh_name] - bpy.data.objects.remove(old_mesh) - except: - dont_complain = 85 - - # Set the new name, now that it is available - new_merged.name = mesh_name - - # Move to "Mesh" collection - mesh_collection.objects.link(new_merged) - temp_collection.objects.unlink(new_merged) - - # Delete the temp collection - bpy.data.collections.remove(temp_collection) - - # Auto unwrap - auto_unwrap(new_merged, context) - - - -class Sut_OT_Operator(bpy.types.Operator): - bl_idname = "view3d.sut" - bl_label = "SUT" - bl_description = "SUT" - - def execute(self, context): - # For scene / own global variables - sut_tool = context.scene.my_tool - - # Check if collections don't exist, and create them - try: - bpy.data.collections[sut_tool.greybox_col_name] - except: - self.report({'ERROR'}, "You have not created a '" + sut_tool.greybox_col_name + "' greybox collection yet!") - return {'FINISHED'} - try: - bpy.data.collections[sut_tool.final_col_name] - except: - new_col = bpy.data.collections.new(name=sut_tool.final_col_name) - bpy.context.scene.collection.children.link(new_col) - - - # Unhide the Final Collection | We don't know what is going on but it works - bpy.context.scene.view_layers[0].layer_collection.children[sut_tool.final_col_name].hide_viewport = False - - # Unhide the Greybox Collection | We don't know what is going on but it works - bpy.context.scene.view_layers[0].layer_collection.children[sut_tool.greybox_col_name].hide_viewport = False - - # Cleanup Mesh Collection - for obj in bpy.data.collections[sut_tool.final_col_name].all_objects: - bpy.data.objects.remove(obj) - - # Every collection gets merged into it's own unique mesh (or all collections is a single big mesh) - greybox_collection = bpy.data.collections[sut_tool.greybox_col_name] - if sut_tool.every_col_is_own_mesh is True: - # Get final collection, loop over every child collection and run make_single_mesh - for col in greybox_collection.children: - make_single_mesh(col, context) - else: - make_single_mesh(greybox_collection, context) - - # For every single object in the final collection, set the texel density - final_collection = bpy.data.collections[sut_tool.final_col_name] - select_children_objects_recursively(final_collection) - - - bpy.ops.object.preset_set(td_value=sut_tool.sut_texel_density) - bpy.context.scene.td.texture_size = sut_tool.sut_texture_size - bpy.ops.object.texel_density_set() - bpy.ops.object.select_all(action='DESELECT') - - if sut_tool.sut_send_to_unreal is True: - bpy.ops.wm.send2ue() - - # Hide the Collection | We don't know what is going on but it works - bpy.context.scene.view_layers[0].layer_collection.children[sut_tool.final_col_name].hide_viewport = True - return {'FINISHED'} diff --git a/sut_panel.py b/sut_panel.py deleted file mode 100644 index a33c5be..0000000 --- a/sut_panel.py +++ /dev/null @@ -1,27 +0,0 @@ -import bpy - -class Sut_PT_Panel(bpy.types.Panel): - bl_idname = "SUT_PT_Panel" - bl_label = "Sandbox Unreal Tools" - bl_category = "SUT" - bl_space_type = "VIEW_3D" - bl_region_type = "UI" - - def draw(self, context): - layout = self.layout - scene = context.scene - sut_tool = scene.my_tool - - layout.prop(sut_tool, "sut_island_margin") - layout.prop(sut_tool, "sut_texture_size") - layout.prop(sut_tool, "sut_texel_density") - layout.prop(sut_tool, "greybox_col_name") - layout.prop(sut_tool, "final_col_name") - layout.prop(sut_tool, "every_col_is_own_mesh") - layout.prop(sut_tool, "sut_send_to_unreal") - row = layout.row() - row.operator('view3d.sut', text="SUT") - - # Collection name for greybox collection (SUT) - - # Collection name for final mesh collection (Mesh) \ No newline at end of file