Okay, here it is in its hacked-together-glory.
Funny story, I tried to share a link to this thread in the Xolotl Studios discord, but I guess it does too much for free and I got evicted, lol. Even though I paid for his plugin. Oops!
Some assumptions are:
1. Your project is set up where meshes exist in a < your path >< asset name > folder, with a < Textures > sub-folder underneath. If you don't supply any textures, it will still wire them up, but the sampler nodes will be blank with an 'error'. (This means a 'Textures' folder under every asset folder containing fbx meshes. If you don't like this, not too hard to change.)
2. I didn't implement any checks for assets already existing, but there's a comment in the code to give you a starting point if you want to do that. It may just be easier to change the root scan path to a specific subset of what you want, but I leave that to you.
3. This is pretty trivial now to change over to 'legacy' texture methods, you'll have to change the way rglob looks for those files instead of the UDIM '1001' method it uses now, but it isn't too hard. (In addition to some other minor changes in the wiring code.)
4. I included references to the different material properties and sampler node types so you can edit easily without diving into the UE5 Python API labyrinth. (You're welcome.)
5. Lots that could be improved, namely how it chooses where to create/import things in your root '/Game' folder in UE5, but this example is functional enough to be changed.
6. Not liable for it pushing things off your shelves, ruining dinner, or totally trashing a project you've been working on. (You do have backups, right? I would...)
Enjoy!
Now I can finally get back to making things in Houdini.
## ______ ___________ _
## /_ __/___ _/ / /_ __(_)___ ___ ( )_____
## / / / __ `/ / / / / / / __ `__ \|// ___/
## / / / /_/ / / / / / / / / / / / / (__ )
##/_/ \__,_/_/_/ /_/ /_/_/ /_/ /_/ /____/
##
## Automagic Asset Import & Material Wiring Utility
## Less manual drudgery, more asset creation!
##
## November 9th, 2023 - Feel free to use, just give me a name credit somewhere...
##
## Note - This relies on a project structure like:
## < Root Project Path>< Project Asset FBX Directiory >
## < Asset "Texture" Sub-directory >
## Failing to provide any textures in this sub-directory will result in a material created and 'wired' up, but the texture slots will be blank
# ---- Modules
import unreal
import pathlib # This is for filtering directories
# ---- Variables
test_flag = "False" # Just to make things easier when testing material assignment - Make sure to change the list used in the last FOR loop
#test_flag = "True"
# Unreal utility references
AT = unreal.AssetToolsHelpers.get_asset_tools()
AID = unreal.AutomatedAssetImportData()
MEL = unreal.MaterialEditingLibrary
EAL = unreal.EditorAssetLibrary
# Path declarations
importDestPath = '/Game/<YourDestFolder>' # Where the asset will be imported into the UE5 Content Browser '/Game' is always root
myProjectPath = '<Drive>:<YourProjectPath>' # Root path to scan for files
# Global list vars for asset lists
FBXList = []
TextureList = []
# New list from scanned folders
scannedFileNames = []
# Test fileset definitions - Just useful if doing dev testing on a smaller sub-set of assets
# Your project path to FBX meshes here
test_FBXList = ['<Drive>:<ProjectPath>\Mesh_<YourAssetName>.fbx']
# Your project path to Textures here - Set up for UDIMs
test_TextureList = ['<Drive>:<ProjectPath>\Textures\<YourAssetName>_DefaultMaterial_BaseColor.1001.png',
'<Drive>:<ProjectPath>\Textures\<YourAssetName>_DefaultMaterial_Normal.1001.png',
'<Drive>:<ProjectPath>\Textures\<YourAssetName>_DefaultMaterial_OcclusionRoughnessMetallic.1001.png']
# ---- Functions
# This function takes a set object, output list and converts to a list of strings
def convertSet(mySetObject, myOutputList):
for item in mySetObject:
tempstr = str(item)
myOutputList.append(tempstr)
return
# This function takes your project root and makes different string element lists for further processing
def readProjectAssets(assetRootPath):
# Temp list merging all results
masterOutputList = []
# Get directory contents under Assets
assetListRaw = pathlib.Path(myProjectPath) # Set root directory to recursively make list from
# Isolate FBX, UDIM Textures
FBX_Assets = assetListRaw.rglob("*.fbx")
Texture_Assets = assetListRaw.rglob("*_DefaultMaterial_*.1001.*") # Only returns the 'head' of UDIM texture sets
# Convert from rglob to set - might be redundant, but whatever, I need to get some work done lol
FBX_SetObject = map(str, FBX_Assets)
Texture_SetObject = map(str, Texture_Assets)
# Iterate set objects and convert to string list
convertSet(FBX_SetObject, FBXList)
convertSet(Texture_SetObject, TextureList)
# A hand test flag just in case you want to do some debug on a limited set defined above
if test_flag == "False":
masterOutputList = FBXList + TextureList
if test_flag == "True":
masterOutputList = test_FBXList + test_TextureList
return masterOutputList
def importAssets(myAssetList):
# Set import attributes
AID.destination_path = importDestPath
AID.filenames = myAssetList
AID.replace_existing = True # Supposed to replace existing, but when testing it seemed to prompt anyway
# Check if assets exist using - if not unreal.EditorAssetLibrary.does_asset_exist(assetpath=<your /Game/path>)
# Haven't implemented this, but its here if you want to attempt it
# Do the import
AT.import_assets_automated(AID)
return
def createBlankMaterial(myMaterialName):
tempPath = importDestPath
tempName = myMaterialName
unreal.AssetToolsHelpers.get_asset_tools().create_asset(asset_name=myMaterialName, package_path=tempPath, asset_class=unreal.Material, factory=unreal.MaterialFactoryNew())
return tempPath, tempName
def wireUpTextures(myAssetName):
# Debug
unreal.log_warning("Wiring up texture with material name: " + myAssetName)
# Get material reference for wiring operations
tempMatObject = unreal.load_asset(importDestPath + '/' + myAssetName)
# UDIM texture references
# Split out prefix to use asset name in reference - naming convention is 'Mat_YourMaterialName' in this example
UDIM_Name = (myAssetName.split('_'))[1]
# Set up references to UDIM textures we've already imported into the content browser
UDIM_Base_Color = unreal.load_asset(importDestPath + '/' + UDIM_Name + '_DefaultMaterial_BaseColor')
UDIM_Normal = unreal.load_asset(importDestPath + '/' + UDIM_Name + '_DefaultMaterial_Normal')
UDIM_Occlusion_Roughness_Metallic = unreal.load_asset(importDestPath + '/' + UDIM_Name + '_DefaultMaterial_OcclusionRoughnessMetallic')
# Make Nodes - Target Material Reference, Sampler Node, X coord, Y coord
Tex_BaseColor = MEL.create_material_expression(tempMatObject, unreal.MaterialExpressionTextureSample, -400, 0)
Tex_Normal = MEL.create_material_expression(tempMatObject, unreal.MaterialExpressionTextureSample, -400, 300)
Tex_OccRoughMetallic = MEL.create_material_expression(tempMatObject, unreal.MaterialExpressionTextureSample, -400, 600)
# Connect sampler Nodes to material
MEL.connect_material_property(Tex_BaseColor, "RGB", unreal.MaterialProperty.MP_BASE_COLOR)
MEL.connect_material_property(Tex_Normal, "RGB", unreal.MaterialProperty.MP_NORMAL)
# In this case different color channels represent properties: Red = Occlusion, Green = Roughness, Blue = Metallic
# Properties from unreal.MaterialProperty:
# 'MP_AMBIENT_OCCLUSION', 'MP_ANISOTROPY', 'MP_BASE_COLOR', 'MP_EMISSIVE_COLOR', 'MP_METALLIC', 'MP_NORMAL', 'MP_OPACITY', 'MP_OPACITY_MASK', 'MP_REFRACTION', 'MP_ROUGHNESS', 'MP_SPECULAR', 'MP_SUBSURFACE_COLOR', 'MP_TANGENT'
MEL.connect_material_property(Tex_OccRoughMetallic, "R", unreal.MaterialProperty.MP_AMBIENT_OCCLUSION)
MEL.connect_material_property(Tex_OccRoughMetallic, "G", unreal.MaterialProperty.MP_ROUGHNESS)
MEL.connect_material_property(Tex_OccRoughMetallic, "B", unreal.MaterialProperty.MP_METALLIC)
# Set Texture Sample nodes to UDIM texture reference
Tex_BaseColor.texture = UDIM_Base_Color
Tex_Normal.texture = UDIM_Normal
Tex_OccRoughMetallic.texture = UDIM_Occlusion_Roughness_Metallic
# Set sampler node type for Virtual Color / UDIM
# Properties from unreal.MaterialSamplerType:
# SAMPLERTYPE_ALPHA', 'SAMPLERTYPE_COLOR', 'SAMPLERTYPE_DATA', 'SAMPLERTYPE_DISTANCE_FIELD_FONT', 'SAMPLERTYPE_EXTERNAL', 'SAMPLERTYPE_GRAYSCALE', 'SAMPLERTYPE_LINEAR_COLOR', 'SAMPLERTYPE_LINEAR_GRAYSCALE', 'SAMPLERTYPE_MASKS', 'SAMPLERTYPE_NORMAL', 'SAMPLERTYPE_VIRTUAL_ALPHA', 'SAMPLERTYPE_VIRTUAL_COLOR', 'SAMPLERTYPE_VIRTUAL_GRAYSCALE', 'SAMPLERTYPE_VIRTUAL_LINEAR_COLOR', 'SAMPLERTYPE_VIRTUAL_LINEAR_GRAYSCALE', 'SAMPLERTYPE_VIRTUAL_MASKS', SAMPLERTYPE_VIRTUAL_NORMAL
Tex_BaseColor.set_editor_property("SamplerType", unreal.MaterialSamplerType.SAMPLERTYPE_VIRTUAL_COLOR)
Tex_Normal.set_editor_property("SamplerType", unreal.MaterialSamplerType.SAMPLERTYPE_VIRTUAL_NORMAL)
Tex_OccRoughMetallic.set_editor_property("SamplerType", unreal.MaterialSamplerType.SAMPLERTYPE_VIRTUAL_COLOR)
return
# Static mesh name convention in this project is 'Mesh_Yourname.fbx'
def assignMaterial(myStaticMeshName, myMaterialName): # Assumes filename input with no extension
if myStaticMeshName is not None: # Basic check in case of errors
# Get imported mesh reference
myAsset = unreal.load_asset(importDestPath + '/' + myStaticMeshName)
# Get created material reference
myMatToAssign = unreal.load_asset(importDestPath + '/' + myMaterialName)
# Set material in index slot zero
myAsset.set_material(0, myMatToAssign)
return
# Debug - using warning color to highlight output for visibility in the UE5 Log Window
unreal.log_warning("----------[ TallTim's Automagic Asset Import & Material Wiring Utility ]---------- \n")
# Store results of scanned directories
scannedFileNames = readProjectAssets(myProjectPath)
# Do the import from your scanned project root folder
importAssets(scannedFileNames) # UE5 seems to handle it fine, if you encounter problems, maybe use threading/wait methods or time.sleep
# Iterate through our fbx list to assign materials
# Note - when you are testing using a limited set, this needs to use test_FBXList
# I could've wrapped this in a IF statement, but honestly why duplicate code for that.. one line change is easier
# Below for normal processing
for meshName in FBXList:
# Below for testing using a limited defined set of files
#for meshName in test_FBXList:
if meshName is not None: # Basic check in case of errors
# Strip everything except the actual 'Mesh_<fbx mesh name>' in the list
# Split out the path slashes first
pathSplitRaw = meshName.split('\\')
# Return last element which is meshfilename.fbx
fileNameRaw = pathSplitRaw[-1]
# Split out the <filename>.<fbx> to get the asset name
splitDot = fileNameRaw.split('.')
fbxAssetName = splitDot[0]
# Strip out the 'Mesh_' prefix on the fbx filename -- Its assumed all fbx meshes are named this way
splitMeshPrefix = fbxAssetName.split('_')
# Add our material prefix -- If you don't like my naming conventions, feel free to change it - just catch it in the other functions
textureAssetName = 'Mat_' + splitMeshPrefix[1]
# Create our blank material with the same asset name
myPath, myName = createBlankMaterial(textureAssetName) # The returned values were for debugging, redact if needed
# Wire up the materials with our imported UDIM textures - This is easily changed to use regular 2D Texture types, see function
wireUpTextures(textureAssetName)
# Assign wired material to the imported fbx asset
assignMaterial(fbxAssetName, textureAssetName)