[SOLVED] Images As Planes For Unreal

I put together a short script that scans a folder and creates a mesh for each image. It links the image to a material and assigns the material to the mesh. It stacks up your images in z-space.



# Images As Planes, for the Unreal Editor
# Author: Atom
# This script will scan a folder for image files with a specific file extenstion.
# A companion mesh will be generated for each file found in the folder.
# A material will be assigned to the mesh linking the image.
# (c) 2020 Atom
# 10/17/20

import unreal
import os, re, platform

def returnValidUnrealMaterialName(passedItem):
# Replace any illegal characters for names here.
s = re.sub("^0-9a-zA-Z\.]+", "_", passedItem)
s = s.replace(".","_")
return s

# Crude width height detection for now.
# Only supports GIFs, PNGs, and JPGs.
# Alternately install PIL or dimensions.
# Or maybe someone can demonstrate how to get the width/height out of an image map?
def get_image_size(file_path):
"""
Return (width, height) for a given img file content - no external
dependencies except the os and struct modules from core
"""
size = os.path.getsize(file_path)
#print size
with open(file_path) as input:
height = -1
width = -1
data = input.read(25)
#print data

if (size >= 10) and data:6] in ('GIF87a', 'GIF89a'):
# GIFs
w, h = struct.unpack("<HH", data[6:10])
width = int(w)
height = int(h)
elif ((size >= 24) and data.startswith('\211PNG
\032
')
and (data[12:16] == 'IHDR')):
# PNGs
w, h = struct.unpack(">LL", data[16:24])
width = int(w)
size = os.path.getsize(file_path)
# Weird?
with open(file_path) as input:
height = -1
width = -1
data = input.read(25)
print data
height = int(h)
elif (size >= 16) and data.startswith('\211PNG
\032
'):
# older PNGs?
w, h = struct.unpack(">LL", data[8:16])
width = int(w)
height = int(h)
elif (size >= 2) and data.startswith('\377\330'):
# JPEG
msg = " raised while trying to decode as JPEG."
input.seek(0)
input.read(2)
b = input.read(1)
try:
while (b and ord(b) != 0xDA):
while (ord(b) != 0xFF): b = input.read(1)
while (ord(b) == 0xFF): b = input.read(1)
if (ord(b) >= 0xC0 and ord(b) <= 0xC3):
input.read(3)
h, w = struct.unpack(">HH", input.read(4))
break
else:
input.read(int(struct.unpack(">H", input.read(2))[0])-2)
b = input.read(1)
width = int(w)
height = int(h)
except:
# Unsupported jpeg file type.
print "Unsupported JPG type."
pass
else:
# Unsupported file extension, extend to suport more file types.
print "Unsupported file extension."
pass
return width, height

def returnFilesLike(passedFolderName, passedFileExtension = ".jpg"):
result = ]
#print passedFolderName
for file in os.listdir(passedFolderName):
if file.endswith(passedFileExtension):
result.append(os.path.join(passedFolderName,file))
return result

def importListOfImageMaps(passedList, passedTargetFolder):
lst_texture2D = ]
data = unreal.AutomatedAssetImportData()
data.set_editor_property('destination_path', passedTargetFolder) # Unreal game folder.
data.set_editor_property('filenames', passedList)
lst_texture2D = unreal.AssetToolsHelpers.get_asset_tools().import_assets_automated(data)
return lst_texture2D

def createNewImageMapOpaqueMaterial(passedAssetPath, passedMaterialName, passedDiffuseTexture, passedNormalTexture, passedDiffuseColor, passedSpec, passedRough):
# Create a material.
assetTools = unreal.AssetToolsHelpers.get_asset_tools()
mf = unreal.MaterialFactoryNew()
mat_closure = assetTools.create_asset("M_%s" % passedMaterialName, passedAssetPath, unreal.Material, mf)

# Make a texture diffuse node.
if passedDiffuseTexture!=None:
# Add an image node.
ts_node_diffuse = unreal.MaterialEditingLibrary.create_material_expression(mat_closure,unreal.MaterialExpressionTextureSample,-384,-200)
ts_node_diffuse.texture = passedDiffuseTexture
unreal.MaterialEditingLibrary.connect_material_property(ts_node_diffuse, "RGBA", unreal.MaterialProperty.MP_BASE_COLOR)
else:
# Add a color constant node.
ts_node_diffuse = unreal.MaterialEditingLibrary.create_material_expression(mat_closure,unreal.MaterialExpressionConstant3Vector,-384,-200)
value = unreal.LinearColor(float(passedDiffuseColor[0]),float(passedDiffuseColor[1]),float(passedDiffuseColor[2]),1.0)
ts_node_diffuse.set_editor_property("constant", value)
unreal.MaterialEditingLibrary.connect_material_property(ts_node_diffuse, "", unreal.MaterialProperty.MP_BASE_COLOR)
'''
# Make a texture normal node.
if passedNormalTexture!=None:
ts_node_normal = unreal.MaterialEditingLibrary.create_material_expression(mat_closure,unreal.MaterialExpressionTextureSample,-384,200)
unreal.MaterialEditingLibrary.connect_material_property(ts_node_normal, "RGB", unreal.MaterialProperty.MP_NORMAL)
# Change this sampler color sample type to work with normal types.
ts_node_normal.sampler_type = unreal.MaterialSamplerType.SAMPLERTYPE_NORMAL
ts_node_normal.texture = passedNormalTexture
'''
# Make a constant float node.
ts_node_roughness = unreal.MaterialEditingLibrary.create_material_expression(mat_closure,unreal.MaterialExpressionConstant,-125,150)
ts_node_roughness.set_editor_property('R', passedRough)
unreal.MaterialEditingLibrary.connect_material_property(ts_node_roughness, "", unreal.MaterialProperty.MP_ROUGHNESS)

# Make a constant float node.
ts_node_specular = unreal.MaterialEditingLibrary.create_material_expression(mat_closure,unreal.MaterialExpressionConstant,-125,50)
ts_node_specular.set_editor_property('R', passedSpec)
unreal.MaterialEditingLibrary.connect_material_property(ts_node_specular, "", unreal.MaterialProperty.MP_SPECULAR)

unreal.MaterialEditingLibrary.recompile_material(mat_closure)
return mat_closure

def imagesToPlanes (passedPath, passedType, passedMaterialPath, passedTexturePath):
lst_files = returnFilesLike(passedPath, passedType)
if len(lst_files):
as_cube = True
# Collect list of image files associated with this folder.
lst_textures = importListOfImageMaps(lst_files, texture_path) #should match the length of list_files.

for i,file in enumerate(lst_files):
depth = i * 25.0
thickness = 0.002
w,h = get_image_size(file)

# Create a map file asset for each image in our list.
local_name = os.path.basename(file)
local_name_only = os.path.splitext(local_name)[0]
shader_name = returnValidUnrealMaterialName("%s" % local_name_only)
mesh_name = returnValidUnrealMaterialName("SM_%s" % local_name_only)

# Defaults for new material.
reflection_weight = 0.1
reflection_roughness = 0.23
diffuse_color = [0.18,0.18,0.18]
temp_material = createNewImageMapOpaqueMaterial(passedMaterialPath, shader_name, lst_textures*, None, diffuse_color, reflection_weight, reflection_roughness)

# Make a static mesh plane.
ell = unreal.EditorLevelLibrary
eal = unreal.EditorAssetLibrary
mesh_actor = ell.spawn_actor_from_class(unreal.StaticMeshActor.static_class(), unreal.Vector(depth, 0, 100), unreal.Rotator(0, 0, 0))
mesh_actor.set_actor_label(mesh_name)
mesh_comp = mesh_actor.get_component_by_class(unreal.StaticMeshComponent.static_class())
if as_cube:
temp_mesh = eal.load_asset("StaticMesh'/Engine/BasicShapes/Cube.Cube'") # Assume asset is created at default scale of 1.0.
else:
temp_mesh = eal.load_asset("StaticMesh'/Engine/BasicShapes/Plane.Plane'") # Assume asset is created at default scale of 1.0.
mesh_comp.set_static_mesh(temp_mesh)
mesh_comp.set_editor_property("override_materials", [temp_material])
if w != -1:
# Guestimate from brief file header review.
mesh_comp.set_editor_property("relative_scale3d", unreal.Vector(thickness, w*0.001, h*0.001))
else:
# Default to 16:9.
mesh_comp.set_editor_property("relative_scale3d", unreal.Vector(thickness, 1.6, 0.9)) # Set the aspect ratio of image here
else:
print"This combination %s].%s] produces no results." % (passedPath, passedType)

# Program begins here.
asset_path = "/Game"
# Select which disk folder to scan.
n = 2
if n==1:
folder_path = r'F:\Keep\Maps\Bullet_Holes'
extension = ".png"
material_path = "%s/Bullet_Holes/Mats" % asset_path
texture_path = "%s/Bullet_Holes/Tex" % asset_path
if n==2:
folder_path = r'F:\Keep\Maps\fur'
extension = ".jpg"
material_path = "%s/Fur/Mats" % asset_path
texture_path = "%s/Fur/Tex" % asset_path

imagesToPlanes (folder_path,extension,material_path,texture_path)
print "Images To Planes complete."



1 Like

Hi @AtomicPerception,

This script looks like exactly what I need, but when running it in UE5, I’m getting back some errors. For instance, there’s an open bracket on line 37. I’m trying to reconstruct it because it’d be insanely useful for me.

Any chance you can check that out to see which part is missing?

Thanks,

Eric

had any luck with that ?