├── .gitmodules ├── LICENSE ├── README.md ├── docs ├── example.png └── screenshot.png └── object_ue4convexhull.py /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "v-hacd"] 2 | path = v-hacd 3 | url = https://github.com/kmammou/v-hacd 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Calem Bendell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UnrealConvexHullGenerator 2 | 3 | This is a Blender addon that generates convex collision hulls for concave objects in Unreal Engine. 4 | 5 | 6 | ## How to Use 7 | 8 | 1. Clone this repository 9 | 1. Clone hte v-hacd submodule 10 | 1. Copy the Python script in the top directory to your Blender addons directory. For Blender 2.78 this directory will be "Blender Foundation/Blender/2.78/scripts/addons/". You're at the right place if you see other scripts prefixed with object\_. 11 | 1. The addon must be enabled before use. After copying the script to the addons directory, open Blender and navigate to File > User Preferences > Add-ons. Object: V-HACD will be featured on the list. Check its mark to enable the addon and save user settings. 12 | 1. Use the addon as instructed for the sister addon in the v-hacd submodule. This version is convenient for use with Unreal Engine. 13 | 14 | 15 | ## Images 16 | 17 | ![Usage](https://github.com/calben/UnrealConvexHullGenerator/blob/master/docs/screenshot.png) 18 | 19 | ![Example](https://github.com/calben/UnrealConvexHullGenerator/blob/master/docs/example.png) 20 | -------------------------------------------------------------------------------- /docs/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calben/UnrealConvexHullGenerator/f26dd9acd04a1509a4aca5d3838a999af9a77b10/docs/example.png -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calben/UnrealConvexHullGenerator/f26dd9acd04a1509a4aca5d3838a999af9a77b10/docs/screenshot.png -------------------------------------------------------------------------------- /object_ue4convexhull.py: -------------------------------------------------------------------------------- 1 | # 2 | # This edition of the plugin has been modified by Calem Bendell 3 | # to produce convex collision hulls for Unreal Engine 4 with less 4 | # effort. 5 | # 6 | 7 | # ---------------- 8 | # V-HACD Blender add-on 9 | # Copyright (c) 2014, Alain Ducharme 10 | # ---------------- 11 | # This software is provided 'as-is', without any express or implied warranty. 12 | # In no event will the authors be held liable for any damages arising from the use of this software. 13 | # Permission is granted to anyone to use this software for any purpose, 14 | # including commercial applications, and to alter it and redistribute it freely, 15 | # subject to the following restrictions: 16 | # 17 | # 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 18 | # 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 19 | # 3. This notice may not be removed or altered from any source distribution. 20 | 21 | # 22 | # NOTE: requires/calls Khaled Mamou's testVHACD executable found here: https://github.com/kmammou/v-hacd 23 | # 24 | 25 | bl_info = { 26 | 'name': 'UE4-Convex-Hull', 27 | 'description': 'Hierarchical Approximate Convex Decomposition for Unreal Engine 4 Collisions', 28 | 'author': 'Calem Bendell', 29 | 'version': (0, 1), 30 | 'blender': (2, 78, 0), 31 | 'location': '3D View, Tools tab -> UE4-Convex-Hull, in Object mode', 32 | 'warning': "Requires Khaled Mamou's V-HACD v2.0 textVHACD executable: (see documentation)", 33 | 'wiki_url': 'https://github.com/calben/UnrealConvexHullGenerator', 34 | 'category': 'Object', 35 | } 36 | 37 | import bpy 38 | from bpy.props import BoolProperty, EnumProperty, FloatProperty, IntProperty, PointerProperty, StringProperty 39 | from bl_operators.presets import AddPresetBase 40 | import bmesh 41 | from mathutils import Matrix, Vector 42 | from os import path as os_path 43 | from subprocess import Popen 44 | from tempfile import gettempdir 45 | 46 | def physics_mass_center(mesh): 47 | '''Calculate (final triangulated) mesh's mass and center of mass based on volume''' 48 | volume = 0. 49 | mass = 0. 50 | com = Vector() 51 | # Based on Stan Melax's volint 52 | for face in mesh.polygons: 53 | a = Matrix((mesh.vertices[face.vertices[0]].co, mesh.vertices[face.vertices[1]].co, mesh.vertices[face.vertices[2]].co)) 54 | vol = a.determinant() 55 | volume += vol 56 | com += vol * (a[0] + a[1] + a[2]) 57 | if volume > 0: 58 | com /= volume * 4. 59 | mass = volume / 6. 60 | return mass, com 61 | else: 62 | return mass, Vector() 63 | 64 | def off_export(mesh, fullpath): 65 | '''Export triangulated mesh to Object File Format''' 66 | with open(fullpath, 'wb') as off: 67 | off.write(b'OFF\n') 68 | off.write(str.encode('{} {} 0\n'.format(len(mesh.vertices), len(mesh.polygons)))) 69 | for vert in mesh.vertices: 70 | off.write(str.encode('{:g} {:g} {:g}\n'.format(*vert.co))) 71 | for face in mesh.polygons: 72 | off.write(str.encode('3 {} {} {}\n'.format(*face.vertices))) 73 | 74 | class VHACD(bpy.types.Operator): 75 | bl_idname = 'object.vhacd' 76 | bl_label = 'Hierarchical Approximate Convex Decomposition' 77 | bl_description = 'Hierarchical Approximate Convex Decomposition, see http://code.google.com/p/v-hacd/' 78 | bl_options = {'PRESET'} 79 | 80 | # ------------------- 81 | # pre-process options 82 | remove_doubles = BoolProperty( 83 | name = 'Remove Doubles', 84 | description = 'Collapse overlapping vertices in generated mesh', 85 | default = True) 86 | 87 | apply_transforms = EnumProperty( 88 | name = 'Apply', 89 | description = 'Apply Transformations to generated mesh', 90 | items = ( 91 | ('LRS', 'Location + Rotation + Scale', 'Apply location, rotation and scale'), 92 | ('RS', 'Rotation + Scale', 'Apply rotation and scale'), 93 | ('S', 'Scale', 'Apply scale only'), 94 | ('NONE', 'None', 'Do not apply transformations'), 95 | ), 96 | default = 'RS') 97 | 98 | # --------------- 99 | # VHACD parameters 100 | resolution = IntProperty( 101 | name = 'Voxel Resolution', 102 | description = 'Maximum number of voxels generated during the voxelization stage', 103 | default = 100000, min = 10000, max = 64000000) 104 | 105 | depth = IntProperty( 106 | name = 'Clipping Depth', 107 | description = 'Maximum number of clipping stages. During each split stage, all the model parts (with a concavity higher than the user defined threshold) are clipped according the "best" clipping plane', 108 | default = 20, min = 1, max = 32) 109 | 110 | concavity = FloatProperty( 111 | name = 'Maximum Concavity', 112 | description = 'Maximum concavity', 113 | default = 0.0025, min = 0.0, max = 1.0, precision = 4) 114 | 115 | planeDownsampling = IntProperty( 116 | name = 'Plane Downsampling', 117 | description = 'Granularity of the search for the "best" clipping plane', 118 | default = 4, min = 1, max = 16) 119 | 120 | convexhullDownsampling = IntProperty( 121 | name = 'Convex Hull Downsampling', 122 | description = 'Precision of the convex-hull generation process during the clipping plane selection stage', 123 | default = 4, min = 1, max = 16) 124 | 125 | alpha = FloatProperty( 126 | name = 'Alpha', 127 | description = 'Bias toward clipping along symmetry planes', 128 | default = 0.05, min = 0.0, max = 1.0, precision = 4) 129 | 130 | beta = FloatProperty( 131 | name = 'Beta', 132 | description = 'Bias toward clipping along revolution axes', 133 | default = 0.05, min = 0.0, max = 1.0, precision = 4) 134 | 135 | gamma = FloatProperty( 136 | name = 'Gamma', 137 | description = 'Maximum allowed concavity during the merge stage', 138 | default = 0.00125, min = 0.0, max = 1.0, precision = 5) 139 | 140 | pca = BoolProperty( 141 | name = 'PCA', 142 | description = 'Enable/disable normalizing the mesh before applying the convex decomposition', 143 | default = False) 144 | 145 | mode = EnumProperty( 146 | name = 'ACD Mode', 147 | description = 'Approximate convex decomposition mode', 148 | items = ( 149 | ('VOXEL', 'Voxel', 'Voxel ACD Mode'), 150 | ('TETRAHEDRON', 'Tetrahedron', 'Tetrahedron ACD Mode')), 151 | default = 'VOXEL') 152 | 153 | maxNumVerticesPerCH = IntProperty( 154 | name = 'Maximum Vertices Per CH', 155 | description = 'Maximum number of vertices per convex-hull', 156 | default = 32, min = 4, max = 1024) 157 | 158 | minVolumePerCH = FloatProperty( 159 | name = 'Minimum Volume Per CH', 160 | description = 'Minimum volume to add vertices to convex-hulls', 161 | default = 0.0001, min = 0.0, max = 0.01, precision = 5) 162 | 163 | # ------------------- 164 | # post-process options 165 | show_transparent = BoolProperty( 166 | name = 'Show Transparent', 167 | description = 'Enable transparency for ACD hulls', 168 | default=True) 169 | 170 | use_generated = BoolProperty( 171 | name = 'Use Generated Mesh', 172 | description = 'Use triangulated mesh generated for V-HACD (for game engine visuals; otherwise use original object)', 173 | default=True) 174 | 175 | hide_render = BoolProperty( 176 | name = 'Hide Render', 177 | description = 'Disable rendering of convex hulls (for game engine)', 178 | default=True) 179 | 180 | mass_com = BoolProperty( 181 | name = 'Center of Mass', 182 | description = 'Calculate physics mass and set center of mass (origin) based on volume and density (best to apply rotation and scale)', 183 | default=True) 184 | 185 | density = FloatProperty( 186 | name = 'Density', 187 | description = 'Material density used to calculate mass from volume', 188 | default = 10.0, min = 0.0) 189 | 190 | def execute(self, context): 191 | 192 | # Check textVHACD executable path 193 | vhacd_path = bpy.path.abspath(context.scene.vhacd.vhacd_path) 194 | if os_path.isdir(vhacd_path): 195 | vhacd_path = os_path.join(vhacd_path, 'testVHACD') 196 | elif not os_path.isfile(vhacd_path): 197 | self.report({'ERROR'}, 'Path to testVHACD executable required') 198 | return {'CANCELLED'} 199 | if not os_path.exists(vhacd_path): 200 | self.report({'ERROR'}, 'Cannot find testVHACD executable at specified path') 201 | return {'CANCELLED'} 202 | 203 | # Check Data path 204 | data_path = bpy.path.abspath(context.scene.vhacd.data_path) 205 | if data_path.endswith('/') or data_path.endswith('\\'): 206 | data_path = os_path.dirname(data_path) 207 | if not os_path.exists(data_path): 208 | self.report({'ERROR'}, 'Invalid data directory') 209 | return {'CANCELLED'} 210 | 211 | selected = bpy.context.selected_objects 212 | if not selected: 213 | self.report({'ERROR'}, 'Object(s) must be selected first') 214 | return {'CANCELLED'} 215 | for ob in selected: 216 | ob.select = False 217 | 218 | new_objects = [] 219 | for ob in selected: 220 | filename = ''.join(c for c in ob.name if c.isalnum() or c in (' ','.','_')).rstrip() 221 | 222 | off_filename = os_path.join(data_path, '{}.off'.format(filename)) 223 | outFileName = os_path.join(data_path, '{}.wrl'.format(filename)) 224 | logFileName = os_path.join(data_path, '{}_log.txt'.format(filename)) 225 | 226 | try: 227 | mesh = ob.to_mesh(context.scene, True, 'PREVIEW', calc_tessface=False) 228 | except: 229 | continue 230 | 231 | translation, quaternion, scale = ob.matrix_world.decompose() 232 | scale_matrix = Matrix(((scale.x,0,0,0),(0,scale.y,0,0),(0,0,scale.z,0),(0,0,0,1))) 233 | if self.apply_transforms in ['S', 'RS', 'LRS']: 234 | pre_matrix = scale_matrix 235 | post_matrix = Matrix() 236 | else: 237 | pre_matrix = Matrix() 238 | post_matrix = scale_matrix 239 | if self.apply_transforms in ['RS', 'LRS']: 240 | pre_matrix = quaternion.to_matrix().to_4x4() * pre_matrix 241 | else: 242 | post_matrix = quaternion.to_matrix().to_4x4() * post_matrix 243 | if self.apply_transforms == 'LRS': 244 | pre_matrix = Matrix.Translation(translation) * pre_matrix 245 | else: 246 | post_matrix = Matrix.Translation(translation) * post_matrix 247 | 248 | mesh.transform(pre_matrix) 249 | 250 | bm = bmesh.new() 251 | bm.from_mesh(mesh) 252 | if self.remove_doubles: 253 | bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001) 254 | bmesh.ops.triangulate(bm, faces=bm.faces) 255 | bm.to_mesh(mesh) 256 | bm.free() 257 | 258 | print('\nExporting mesh for UE4 Convex Hull Generator: {}...'.format(off_filename)) 259 | off_export(mesh, off_filename) 260 | cmd_line = '"{}" --input "{}" --resolution {} --depth {} --concavity {:g} --planeDownsampling {} --convexhullDownsampling {} --alpha {:g} --beta {:g} --gamma {:g} --pca {:b} --mode {:b} --maxNumVerticesPerCH {} --minVolumePerCH {:g} --output "{}" --log "{}"'.format( 261 | vhacd_path, 262 | off_filename, 263 | self.resolution, 264 | self.depth, 265 | self.concavity, 266 | self.planeDownsampling, 267 | self.convexhullDownsampling, 268 | self.alpha, 269 | self.beta, 270 | self.gamma, 271 | self.pca, 272 | self.mode == 'TETRAHEDRON', 273 | self.maxNumVerticesPerCH, 274 | self.minVolumePerCH, 275 | outFileName, 276 | logFileName) 277 | 278 | print('Running UE4 Convex Hull Generator...\n{}\n'.format(cmd_line)) 279 | vhacd_process = Popen(cmd_line, bufsize=-1, close_fds=True, shell=True) 280 | 281 | mass = 1.0 282 | com = Vector() 283 | if self.mass_com: 284 | mass, com = physics_mass_center(mesh) 285 | mass *= self.density 286 | post_matrix = Matrix.Translation(com * post_matrix) * post_matrix 287 | pre_matrix = Matrix.Translation(-com) * pre_matrix 288 | if not self.use_generated: 289 | bpy.data.meshes.remove(mesh) 290 | 291 | vhacd_process.wait() 292 | if not os_path.exists(outFileName): 293 | continue 294 | 295 | bpy.ops.import_scene.x3d(filepath=outFileName, axis_forward='Y', axis_up='Z') 296 | imported = bpy.context.selected_objects 297 | new_objects.extend(imported) 298 | counter = 1 299 | object_name = "helloworld" 300 | 301 | for hull in imported: 302 | # Make hull a compound rigid body 303 | hull.select = False 304 | hull.show_transparent = self.show_transparent 305 | if self.mass_com: 306 | for vert in hull.data.vertices: 307 | vert.co -= com 308 | hull.hide_render = self.hide_render 309 | hull.name = "UCX_" + ob.name + "_" + ("%03d" % (counter,)) 310 | hull.data.name = hull.name 311 | counter += 1 312 | 313 | if len(new_objects): 314 | for ob in new_objects: 315 | ob.select = True 316 | else: 317 | for ob in selected: 318 | ob.select = True 319 | self.report({'WARNING'}, 'No meshes to process!') 320 | return {'CANCELLED'} 321 | 322 | return {'FINISHED'} 323 | 324 | def invoke(self, context, event): 325 | wm = context.window_manager 326 | return wm.invoke_props_dialog(self, width=384) 327 | 328 | def draw(self, context): 329 | layout = self.layout 330 | col = layout.column() 331 | col.label('Pre-Processing Options (generated mesh):') 332 | row = col.row() 333 | row.prop(self, 'remove_doubles') 334 | row.prop(self, 'apply_transforms') 335 | 336 | layout.separator() 337 | col = layout.column() 338 | col.label('UE4 Convex Hull Generator Parameters:') 339 | col.prop(self, 'resolution') 340 | col.prop(self, 'depth') 341 | col.prop(self, 'concavity') 342 | col.prop(self, 'planeDownsampling') 343 | col.prop(self, 'convexhullDownsampling') 344 | row = col.row() 345 | row.prop(self, 'alpha') 346 | row.prop(self, 'beta') 347 | row.prop(self, 'gamma') 348 | row = col.row() 349 | row.prop(self, 'pca') 350 | row.prop(self, 'mode') 351 | col.prop(self, 'maxNumVerticesPerCH') 352 | col.prop(self, 'minVolumePerCH') 353 | 354 | layout.separator() 355 | col = layout.column() 356 | col.label('Post-Processing Options:') 357 | row = col.row() 358 | row.prop(self, 'show_transparent') 359 | row.prop(self, 'use_generated') 360 | row = col.row() 361 | row.prop(self, 'hide_render') 362 | row.prop(self, 'mass_com') 363 | row.prop(self, 'density') 364 | 365 | layout.separator() 366 | col = layout.column() 367 | col.label('WARNING:', icon='ERROR') 368 | col.label(' -> Processing can take several minutes per object!') 369 | col.label(' -> ALL selected objects will be processed sequentially!') 370 | col.label(' -> Game Engine physics compound generated for each object') 371 | col.label(' -> See Console Window for progress..,') 372 | 373 | class VIEW3D_PT_tools_vhacd(bpy.types.Panel): 374 | bl_space_type = 'VIEW_3D' 375 | bl_region_type = 'TOOLS' 376 | bl_category = 'Tools' 377 | bl_label = 'UE4 Convex Hull Generator' 378 | bl_context = 'objectmode' 379 | bl_options = {'DEFAULT_CLOSED'} 380 | 381 | def draw(self, context): 382 | layout = self.layout 383 | row = layout.row(align=True) 384 | row.menu('VHACD_MT_path_presets', text=bpy.types.VHACD_MT_path_presets.bl_label) 385 | row.operator('scene.vhacd_preset_add', text='', icon='ZOOMIN') 386 | row.operator('scene.vhacd_preset_add', text='', icon='ZOOMOUT').remove_active = True 387 | col = layout.column() 388 | col.prop(context.scene.vhacd, 'vhacd_path') 389 | col.prop(context.scene.vhacd, 'data_path') 390 | col.operator('object.vhacd', text='UE4 Convex Hull Generator') 391 | 392 | class AddPresetVHACD(AddPresetBase, bpy.types.Operator): 393 | '''Add V-HACD Paths Preset''' 394 | bl_idname = 'scene.vhacd_preset_add' 395 | bl_label = 'Add VHACD Path Preset' 396 | preset_menu = 'VHACD_MT_path_presets' 397 | 398 | preset_defines = ['vhacd = bpy.context.scene.vhacd'] 399 | 400 | preset_values = [ 401 | 'vhacd.vhacd_path', 402 | 'vhacd.data_path', 403 | ] 404 | preset_subdir = 'vhacd' 405 | 406 | class VHACD_MT_path_presets(bpy.types.Menu): 407 | bl_label = 'V-HACD Path Presets' 408 | preset_subdir = 'vhacd' 409 | preset_operator = 'script.execute_preset' 410 | draw = bpy.types.Menu.draw_preset 411 | 412 | class VHACDPaths(bpy.types.PropertyGroup): 413 | @classmethod 414 | def register(cls): 415 | bpy.types.Scene.vhacd = PointerProperty( 416 | name = 'V-HACD Settings', 417 | description = 'V-HACD settings', 418 | type = cls, 419 | ) 420 | cls.vhacd_path = StringProperty( 421 | name = 'VHACD Path', 422 | description = 'Path to testVHACD executable', 423 | default = '', maxlen = 1024, subtype = 'FILE_PATH') 424 | 425 | cls.data_path = StringProperty( 426 | name = 'Data Path', 427 | description = 'Data path to store V-HACD meshes and logs', 428 | default = gettempdir(), maxlen = 1024, subtype = 'DIR_PATH') 429 | 430 | @classmethod 431 | def unregister(cls): 432 | if 'vhacd' in dir(bpy.types.Scene): 433 | del bpy.types.Scene.vhacd 434 | 435 | def register(): 436 | bpy.utils.register_module(__name__) 437 | 438 | def unregister(): 439 | bpy.utils.unregister_module(__name__) 440 | --------------------------------------------------------------------------------