Blender to Unreal FBX Batch Export Static Mesh with Collision

I have created a script in blender that batch exports FBX static mesh, textures, collision and LOD (LOD in separate FBX Files) for unreal engine. This script is very useful for indie game developers who want to keep a library of assets in a scene ready for export to Unreal. Advantage is if you have 100 models in your scene and you need to make changes or modifications to models this script will quickly batch export based on selected models.

Paste script in blender> save scene> select all desired mesh> click batch export.

The script takes each mesh resets the transformation to the center of grid and exports each mesh out as an fbx in the saved scene directory. No need to select collision mesh and LOD, they will get exported automatically based on the base model selection. Accidentally selected a collision mesh or LOD, export still works. No Collision mesh or no LOD, export still works!

Exporting base model+collision mesh in blender for unreal compatibility in one fbx file requires the collision mesh to have the same name with a prefix, [UBX_…(Box Collision),USP_…(Sphere Collision), UCX_… (Convex Collision)].
eg. UBX_Cube (collision mesh) and Cube (Base Mesh)>Select both meshes> Export FBX.

Exporting LOD and base model in one FBX file in blender is not supported. LOD is exported separately.
LOD must be saved with a suffix …_1, up to …_6 LOD is supported.
eg. Cube (Base Model), Cube_1 (First LOD), Cube_2 (Second LOD)…

Textures are mostly created in Unreal. You can add different materials to different objects, join the objects and export it out to Unreal. In unreal you can assign different textures to the different materials.

*** IF SOMEONE AT BLENDER OR EPIC COULD PLEASE CREATE BLENDER-UNREAL LOD AND BASE MODEL SUPPORT IN ONE FBX FILE THIS SCRIPT WOULD BE PERFECT ***
or if anyone could help me figure this out that would be great.

I will continue adding more to this script. If anybody has any questions, errors or ideas, please post I would be happy to hear your feedback.

Script is on GitHub:

I made an Unreal Developers Group for blender if anybody wants to join.
http://blenderartists.org/forum/group.php?groupid=261 :rolleyes:

Ray Davis & Mike Fricker @ Epic talking about blender and fbx support in UE4:

Epic Games has become a main sponsor of the Blender Development Fund, donating €10,000 (around $13,500) to improve Blender’s FBX export capabilities: :eek:


Here is my script:



########################################### 
########################################### 
####          UNREAL TOOLS v1.01       #### 
####            NIKHIL JEEWA           ####
####                2014               ####  
########################################### 
########################################### 
import bpy 
import os
########################################### 
#####        $#!t just got unreal      ####
########################################### 
		
class UnrealTools(bpy.types.Panel): 
    bl_space_type="VIEW_3D"
    bl_region_type="TOOLS"
    bl_category = "Unreal"
    bl_label = "Unreal Tools"

    def draw(self, context): 
        layout = self.layout          
        col = layout.column(align=True) 
        col.label(text="Unreal Units Setup:")
        col.label(text="[1cm Blender = 1cm Unreal]")   
        row = col.row(align=True)  
        row.operator("wm.unreal_units", text="Unreal Units", icon='LOGIC')
        row.operator("wm.blender_units", text="Blender Units", icon='BLENDER')		
        
        row = col.row(align=True)
        col.label(text="Shade Smooth object or edge, face, vertex:")
        row=col.row(align=True)
        row.operator("wm.sel_vertex", text="Vertex", icon='VERTEXSEL') 
        row.operator("wm.sel_edge", text="Edge", icon='EDGESEL') 
        row.operator("wm.sel_face", text="Face", icon='FACESEL')   
        row = col.row(align=True)
        row.operator("wm.smooth", text="Shade Smooth [Ctrl F]", icon='MOD_SUBSURF')
        
        row = col.row(align=True) 
        col.label(text="Tools:")  
        row = col.row(align=True) 
        row.operator("object.join",text="Join [Ctrl J]",icon='AUTOMERGE_OFF') 
        row.operator("mesh.separate",text="Seperate [P]",icon='UV_ISLANDSEL') 
        row.operator("object.duplicate_move",text="Duplicate [Shift+D]",icon='ROTATECOLLECTION')

        col.label(text="Selection Tools:")        
        row = col.row(align=True)         
        row.operator("wm.swap", text=" Invert All [Ctrl I]",icon="ALIGN")
        row.operator("wm.everything", text=" All Select [A]",icon="STICKY_UVS_LOC")
        row.operator("view3d.select_border", text=" Border    Select",icon="VIEW3D_VEC") 
        row.operator("view3d.select_circle", text=" Circle Select",icon="ALIASED")           
        row.operator("object.select_pattern", text=" Pattern...Search Select",icon="SEQ_LUMA_WAVEFORM")       
        
        row = col.row(align=True)
        col.label(text="Origin to Center of Grid [Select Vertex]:")
        row = col.row(align=True)
        row.operator("wm.origin_vertex",text="O-V-CoG Origin to Vertex Center of Grid",icon='VERTEXSEL')
        row.operator("wm.origin_com",text="O-CoM-CoG Origin to Center of Mass-Center of Grid",icon='FORCE_FORCE')
		
        row=col.row(align=True)
        col.label(text="Freeze Transformation of object: [Ctrl+A]")
        row = col.row(align=True)
        row.operator("wm.freeze_loc",text="Freeze Location",icon='FILE_REFRESH')
        row.operator("wm.freeze",text="Freeze Rotation+Scale",icon='FILE_REFRESH')        
        
        col = layout.column(align=True) 
        col.label(text="Blender>Unreal FBX Batch Export:")
        
        row = col.row(align=True)  
        row.operator("wm.batch_export", text="Batch Export", icon='EXPORT')
		
        row=col.row(align=True)
		
class wm_unreal_Units(bpy.types.Operator):
    bl_idname="wm.unreal_units"
    bl_label="Minimal Operator"

    def execute(self,context):
        bpy.context.scene.unit_settings.system='METRIC'
        bpy.context.scene.unit_settings.scale_length = 0.01
        bpy.context.space_data.clip_end = 800000
        bpy.context.space_data.clip_start = 0.1
        bpy.context.space_data.grid_lines = 1024
		#bpy.context.space_data.grid_scale = 0.5
        return {'FINISHED'}
    
class wm_blender_Units(bpy.types.Operator):
    bl_idname="wm.blender_units"
    bl_label="Minimal Operator"

    def execute(self,context):
        bpy.context.scene.unit_settings.system='NONE'
        bpy.context.scene.unit_settings.scale_length = 1
        bpy.context.space_data.clip_end = 500
        bpy.context.space_data.clip_start = 1
        bpy.context.space_data.grid_lines = 16
        return {'FINISHED'}	

class wm_Vertex_Select(bpy.types.Operator):  
    bl_idname = "wm.sel_vertex"
    bl_label = "Minimal Operator"
    def execute(self, context):     
        bpy.ops.object.mode_set(mode='EDIT') 
        bpy.ops.mesh.select_mode(type='VERT') 
        return {'FINISHED'}         
class wm_Edge_Select(bpy.types.Operator):
    bl_idname = "wm.sel_edge"
    bl_label = "Minimal Operator"
    def execute(self, context):     
        bpy.ops.object.mode_set(mode='EDIT') 
        bpy.ops.mesh.select_mode(type='EDGE') 
        return {'FINISHED'}   
    
class wm_Face_Select(bpy.types.Operator):      
    bl_idname = "wm.sel_face"
    bl_label = "Minimal Operator"
    def execute(self, context):     
        bpy.ops.object.mode_set(mode='EDIT') 
        bpy.ops.mesh.select_mode(type='FACE') 
        return {'FINISHED'}   

class wm_smooth(bpy.types.Operator):
    bl_idname="wm.smooth"
    bl_label="Minimal Operator"

    def execute(self,context):
        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.mesh.faces_shade_smooth()
        bpy.ops.object.mode_set(mode='OBJECT')
        return{'FINISHED'}
    
class wm_Invert(bpy.types.Operator):  
    bl_idname = "wm.swap"
    bl_label = "Minimal Operator"
    def execute(self, context):     
        if bpy.context.mode.startswith("EDIT"):
            bpy.ops.mesh.select_all(action='INVERT')
        else:
            bpy.ops.object.select_all(action='INVERT')
        return {'FINISHED'}
    
class wm_Select_All(bpy.types.Operator):  
    bl_idname = "wm.everything"
    bl_label = "Minimal Operator"
    def execute(self, context):     
        if bpy.context.mode.startswith("EDIT"):
            bpy.ops.mesh.select_all(action='TOGGLE')
        else:
            bpy.ops.object.select_all(action='TOGGLE')
        return {'FINISHED'}   
        
		
class wm_origin_vertex(bpy.types.Operator):
    bl_idname="wm.origin_vertex"
    bl_label="Minimal Operator"
	
    def execute(self,context):
        bpy.ops.view3d.snap_cursor_to_selected()
        bpy.ops.object.mode_set(mode='OBJECT')		
        bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
        bpy.context.object.location[0] = 0
        bpy.context.object.location[1] = 0
        bpy.context.object.location[2] = 0
        return{'FINISHED'}
		
class wm_origin_com(bpy.types.Operator):
    bl_idname="wm.origin_com"
    bl_label="Minimal Operator"

    def execute(self,context):
        bpy.ops.view3d.snap_cursor_to_selected()
        bpy.ops.object.mode_set(mode='OBJECT')		
        bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
        bpy.context.object.location[0] = 0
        bpy.context.object.location[1] = 0
        bpy.context.object.location[2] = 0
        bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_MASS')
        bpy.context.object.location[0] = 0
        bpy.context.object.location[1] = 0
        bpy.ops.view3d.snap_cursor_to_center()
        bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
        return{'FINISHED'}
		
class wm_freeze_loc(bpy.types.Operator):
    bl_idname="wm.freeze_loc"
    bl_label="Minimal Operator"
    def execute(self,context):
        bpy.ops.object.transform_apply(location=True, rotation=False, scale=False)
        return {'FINISHED'}
		
class wm_freeze(bpy.types.Operator):
    bl_idname="wm.freeze"
    bl_label="Minimal Operator"
	
    def execute(self,context):
        bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
        return {'FINISHED'}
    
#################### BATCH EXPORT ###################
class wm_batch_export(bpy.types.Operator):
    bl_idname="wm.batch_export"
    bl_label="Minimal Operator"
    
    def execute(self,context):
        basedir = os.path.dirname(bpy.data.filepath)
        if not basedir:
            raise Exception("Blend file is not saved")
                    
        collision=]    
        mesh=]
        lod=]
        print ("")
        selection = bpy.context.selected_objects
        bpy.ops.object.select_all(action='DESELECT')

        for obj in selection:
            if not obj.name.startswith("UBX_") and not obj.name.startswith("USP_") and not obj.name.startswith("UCX_"):
                if not obj.name.endswith("_1") and not obj.name.endswith( "_2") and not obj.name.endswith( "_3") and not obj.name.endswith( "_4") and not obj.name.endswith( "_5") and not obj.name.endswith( "_6"):
                    mesh.append(obj)
                    print (mesh)

        bpy.ops.object.select_by_type(type='MESH')
        col = bpy.context.selected_objects
        for obj in col:
            if obj.name.startswith("UBX_") or obj.name.startswith("USP_") or obj.name.startswith("UCX_"):
                collision.append(obj)
            if obj.name.endswith("_1") or obj.name.endswith( "_2") or obj.name.endswith( "_3") or obj.name.endswith( "_4") and not obj.name.endswith( "_5") or obj.name.endswith( "_6"):
                lod.append(obj)    
        
        bpy.ops.object.select_all(action='DESELECT')
        for obj in mesh:    
            meshX=obj.name
            meshXo=obj
            obj.select=True
            hasCollision=0
            for obj in collision:
                colX = (obj.name[4:])
                if colX==meshX:
                    hasCollision=1
                    obj.select=True
                    locc = obj.location.xyz
                    rotXc = obj.rotation_euler[0]
                    rotYc = obj.rotation_euler[1]
                    rotZc = obj.rotation_euler[2]    
                    scac = obj.scale.xyz
                    obj.location.xyz = [0,0,0]
                    obj.rotation_euler = [0,0,0]
                    obj.scale.xyz = [1,1,1]
                    locm = meshXo.location.xyz
                    rotXm = meshXo.rotation_euler[0]
                    rotYm = meshXo.rotation_euler[1]
                    rotZm = meshXo.rotation_euler[2]    
                    scam = meshXo.scale.xyz
                    meshXo.location.xyz = [0,0,0]
                    meshXo.rotation_euler = [0,0,0]
                    meshXo.scale.xyz = [1,1,1]
                    name = bpy.path.clean_name(meshX)
                    fn = os.path.join(basedir, name)
                    bpy.ops.export_scene.fbx(filepath=fn + ".fbx", use_selection=True,axis_forward='-Z', axis_up='Y')
                    obj.location.xyz = (locc)        
                    obj.rotation_euler = [rotXc,rotYc,rotZc]
                    obj.scale.xyz =(scac) 
                    meshXo.location.xyz = (locm)        
                    meshXo.rotation_euler = [rotXm,rotYm,rotZm]
                    meshXo.scale.xyz =(scam) 
                    obj.select=False
                    for obj in lod:
                        lodX=(obj.name:-2])
                        if lodX==meshX:
                            meshXo.select=False
                            obj.select=True
                            loc = obj.location.xyz
                            rotX = obj.rotation_euler[0]
                            rotY = obj.rotation_euler[1]
                            rotZ = obj.rotation_euler[2]    
                            sca = obj.scale.xyz
                            bpy.ops.object.location_clear()
                            bpy.ops.object.rotation_clear()
                            bpy.ops.object.scale_clear()
                            name =bpy.path.clean_name(obj.name)
                            fn = os.path.join(basedir,name)
                            bpy.ops.export_scene.fbx(filepath=fn + ".fbx", use_selection=True,axis_forward='-Z', axis_up='Y')
                            obj.location.xyz = (loc)        
                            obj.rotation_euler = [rotX,rotY,rotZ]
                            obj.scale.xyz =(sca) 
                            meshXo.select=True
                            obj.select=False
                    bpy.ops.object.select_all(action='DESELECT')
            if hasCollision==0:
                locm = meshXo.location.xyz
                rotXm = meshXo.rotation_euler[0]
                rotYm = meshXo.rotation_euler[1]
                rotZm = meshXo.rotation_euler[2]    
                scam = meshXo.scale.xyz
                meshXo.location.xyz = [0,0,0]
                meshXo.rotation_euler = [0,0,0]
                meshXo.scale.xyz = [1,1,1]
                name = bpy.path.clean_name(meshX)
                fn = os.path.join(basedir, name)
                bpy.ops.export_scene.fbx(filepath=fn + ".fbx", use_selection=True,axis_forward='-Z', axis_up='Y')
                meshXo.location.xyz = (locm)        
                meshXo.rotation_euler = [rotXm,rotYm,rotZm]
                meshXo.scale.xyz =(scam) 
                obj.select=False
                for obj in lod:
                    lodX=(obj.name:-2])
                    if lodX==meshX:
                        meshXo.select=False
                        obj.select=True
                        loc = obj.location.xyz
                        rotX = obj.rotation_euler[0]
                        rotY = obj.rotation_euler[1]
                        rotZ = obj.rotation_euler[2]    
                        sca = obj.scale.xyz
                        bpy.ops.object.location_clear()
                        bpy.ops.object.rotation_clear()
                        bpy.ops.object.scale_clear()
                        name =bpy.path.clean_name(obj.name)
                        fn = os.path.join(basedir,name)
                        bpy.ops.export_scene.fbx(filepath=fn + ".fbx", use_selection=True,axis_forward='-Z', axis_up='Y')
                        obj.location.xyz = (loc)        
                        obj.rotation_euler = [rotX,rotY,rotZ]
                        obj.scale.xyz =(sca) 
                        meshXo.select=True
                        obj.select=False
                    bpy.ops.object.select_all(action='DESELECT')
                                         
        for obj in selection:
            obj.select = True
                        
        return {'FINISHED'}
###########################################  
     
if __name__== "__main__": 
    bpy.utils.register_module(__name__) 



if obj.name.startswith(“UBX_”) or obj.name.startswith(“UPS_”) or obj.name.startswith(“UCX_”):

it’s USP_ not UPS_ :wink:

thanks Samir, I have made the changes

question you create a git repo ??? o i can create one for this source code

Sorry for the late reply , I have been busy with some other work. I have added the script to GitHub. :slight_smile:

This is how I automatically source the script every time I start Blender.

Open/Edit the following blender file:
C:\Program Files\Blender Foundation\Blender\2.X\scripts\startup\bl_ui\space_view3d_toolbar.py

Add the following two lines after the line: (# ********** default tools for object-mode ****************) (Make sure your file path is / not **)

filename = “C:/Users/name/Documents/GitHub/Blender-UE4-FBX-Export”
exec(compile(open(filename).read(), filename, ‘exec’))

Start Blender :slight_smile:

Just want to say I like this program(script), it makes exporting a complete scene from Blender really fast…

Very cool! just did a couple tests with it, but i do wonder, does the script use the last used fbx export settings in the main fbx exporter?

Its been quite some time since I worked on Blender, I have been busy working on other projects. Im sure the script uses the latest fbx, if im not mistaken this is still the line of code for exporting fbx that I used “bpy.ops.export_scene.fbx”.

Hi, i just rewrite this script for my self. I fix some bugs, like - olny one collision primitive per mesh can be exported, also i done some optimisation. Also add some features for naming objects, and remove some default blender features, cause it simplier for me to done by hands. Also i wrap this script in to addon, becaue i tierd to run it every time when i restart blender.
Change file extension from “.txt” to “.py”, to install it as addon.
In blender, go to File -> Add-Ons -> Instal from file -> ‘path to “UnrealFBXTools.py”’

Here a little manual:

This script helps you export your meshes, with collisions and LODs in to UE4

Naming process is prettey simple you just create collision meshes.

After creation you should select all collision of same type(Boxes, spherec or convex hulls),

active selection should be a Rendermesh, because collision will be named

in this style “UXX_%RENDMESH_NAME%_##”. So, you should do it for each collision primitive type.

LODs naming have unpredictable behaviour, because blender doesnt care about selection order,

(or actualy dose, but i dont know how exactly it works…) and names that you got, are unsorted.

For example, you have 4 lod, and final names will be like “LOD_%RENDMESH_NAME%_#”, but you cant

know which of meshes will be _1 or _3, so on and so forth. After naming lods, you should check

mesh names, and if it wrong, change it by hands, it’s prettey easy by the way, because you allready

have correct name, and only thing that you sould change, it’s a digit at the end of object name.

Batch export, do its job. You just select everything that you whant to export, and then hit

buton “Batch Export”, it will store all of selected meshes in to sepparate files and name them

as object named in your scene. Lods will be export separatly, in to its own fbx containers,

and will be named as “%RENDMESH_NAME%_#.fbx”

Hey thanks, I was just about to look for something like this! Because adding collisions in UE4 is tedious as it right now, but this will remove a ton of headache :smiley:

Thats awesome! :slight_smile:

Thanks for thanks!
I planed to add more features to this script:

  1. Lod generation - type number, press button, and you will got some meshes that copied close to original mesh(1 bounding box left/right or some thing like that), to this meshes will be applied decimate modifier, with degrading values (0.5 -> 0.25 -> 0.125 -> …)
    Of course it very far from perfect solution, but it can do some routine job. At least, in this case naming will be correct, and you got some set up, to create some lods.

  2. LOD / Coolision utilities

  • Group ungroup collisions/lod or link unlink to object
  • Hide/Show toggle for collisions/lods
  • Something else…
  1. ID Mat factory - Some times you need an ID map, to done some fast texturing in substance or ddo, so, this tools should make your life easier.
  • Generate specified number of materials with random or evenly shifted by hue colors
  • In a pannel will be a matrix of buttons, that helps you to fast assign first of 25 id mats
  • Fast delete this id-mats
  1. Automatic baking of normals, AO, ID…
  • Normal map will be baked from selected to active
  • Maps will be saved in to folder called “Bakes”, that lives along .blend file. Images will be called “%RenderMesh%_MAPTYPE”
  • One big cool button, that will bake everything for all/selected objects in scene
  1. Some quick actions
  • Move object in to 10’s layer and move it to empty place(Good for bundle moddeling, create your model on first layer, when done put it to the stash on tenth. When you done moddeling process you will got a nice row of your models on tenth layer)
  • Unwrap for lightmapping (it will create new uv set, and then two variants - 1’st - alias to default blender lightmap unwrapper, and repack copy of your current unwrap)
  • Add checker texture
  • to all render meshes
  • to selected meshes
  • Delete unused materials and textures

But, i dont know when exactly it happens. Now i am learning a blenders python API. I planing to done some of this untill february. Later i will post a link to the reposytory on bitbucket or github, with some of this features.

P.S. There are those here who would help me understand how the fbx lod groups works inside? I know that blender exporter can’t export lod groups, but as far as i know this exporter is open source, so, may be we can together implement this feature?

Simplygon is a better solution for LOD but yes LOD generation in blender would be great for simple models but would require allot of polishing.
ID Mat looks like a good feature.
Baking of Normals, Curvature, AO etc. is better in Substance, there is no baking in quixel at the moment they are working on that but we use quixel mostly for texturing and yes I think blender is great for baking simple models.
The rest of your features looks great cant wait to check them out.
About the .fbx exporting LODs, I have heard that Autodesk keeps changing their fbx code and it becomes quite tricky and tedious working with open source as it would be a constant development and not a practical solution I dont know maybe things will change.

I have not used blender for some time as im developing my game in other aspects of the project that can be quite time consuming but you look psyched! All the best. :slight_smile:

P.S. start your own thread, paste a simple code on the thread and as your script grows move it to bitbucket or git. Here is some html to get you started… :slight_smile:

https://forums.unrealengine.com/misc.php?do=bbcode

…I tried messing around with blender addons but I wasn’t successful then so I created a better solution where I put my code on dropbox which people in different locations with a bit of a modification to blenders engine can source my script automatically whenever they start blender…its good for internal development but addon would be better for the community.

Ok, got you.
Now i am feel that i need to do some research about modern texturing approaches.
Thanks for your replies.

Hi. Love this script, saves an incredible amount of time! However I haven’t been doing my own collision mesh until now and I’m having some trouble exporting it with the rendermesh.
Has something changed recently in Blender to break the collision part of the export? I’ve tried naming things in every way possible, following the “manuals” to a T but the collision mesh just doesn’t seem to wanna be exported, this is with either one of your scripts, Nikhil and Paradox072.
Manually exporting to FBX worked as expected and the collision mesh was included when I imported into UE4.
If it wasn’t for the fact that I have a massive scene with multiple assets that I have to move to 0,0,0 for every single export it wouldn’t be such a hassle doing it manually :\

Hi all, Thanks a lot for the blender script, it is indeed very useful and speeds up the blender-to-unreal process. One quick question though: did anyone finally managed to import several LODs automatically into Unreal? (either using the FBX importer, or using another option)? I’d like to import a large number of meshes, each with 4 Levels of Details, and could not find a solution during the past week.
Thanks for the help!

Can someone who knows how to do python please update this?