import logging
import os
import re
from importlib import reload
import cgl.core.config.query as cfg_query
try:
import cgl.plugins.substance.alchemy as alc
import substance_painter.export as sp_export
import substance_painter.project as sp_project
import substance_painter.resource as spres
import substance_painter.textureset as spts
reload(alc)
except ModuleNotFoundError:
pass
from cgl.core.path.object import PathObject
from cgl.core.path.support import add_root
from cgl.core.path.support import remove_root
from cgl.core.utils.general import save_json
CFG = cfg_query.AlchemyConfigManager()
UDIM_RE = re.compile(r"(?P<sep>[._])(?P<udim>1\d{3})(?=(?:\D|$))")
[docs]
class Task:
"""
This is a template for a "task" within the cookbook. It covers common areas when dealing with digital assets
specific to different tasks.
"""
path_object = None
msd_path = ""
def __init__(self, path_object=None):
"""
:param path_object: must be a "PathObject"
"""
if path_object:
self.path_object = path_object
try:
so = alc.Scene().scene_object()
self.path_object = so
except Exception as e:
logging.info(f"Error: No Scene Object Found in Scene: {e}")
so = None
if not so:
startup_substance_file = CFG.user_config["current_substance_asset_path"]
path_object = PathObject().from_path_string(startup_substance_file)
if path_object.get_path():
self.path_object = path_object
else:
logging.info("Error: No Path Object Found in Scene or user config")
return
[docs]
def build(self):
if sp_project.is_open():
logging.info("Error: Project Already Opened")
return False
asset_path = get_asset_path()
path_object = PathObject.from_path_string(asset_path)
tex_path_object = path_object.copy(
tree="source", task="tex", latest=True, ext=".spp", set_filename=True
)
out_path = tex_path_object.get_path()
os.makedirs(os.path.dirname(out_path), exist_ok=True)
fbx_path = get_latest_mdl(self.path_object, ext=".fbx")
if not os.path.exists(fbx_path):
logging.info(f"Error: No Published Mdl Found for Asset\n{fbx_path}")
return False
logging.info(f"Importing: {fbx_path}")
return alc.start_project_with_picker(
fbx_path=fbx_path, default_res=2048, post_create_save_path=out_path
)
[docs]
def get_msd_data(self):
"""
creates the msd dictionary for the task being called.
Args:
task_name:
Returns:
data: dictionary of the msd data
"""
render_dir = self.path_object.get_render_path(dirname=True)
print("222 Render Directory:", render_dir)
data = {}
full_materials_dict = {}
for material_name in os.listdir(render_dir):
print(material_name)
material_dir = os.path.join(render_dir, material_name)
if not os.path.isdir(material_dir):
continue
material_dict = {}
full_materials_dict[material_name] = material_dict
if os.path.isdir(material_dir):
for channel_name in os.listdir(material_dir):
print("\t", channel_name, material_dir)
channel_dict = {}
material_dict[channel_name] = channel_dict
channel_path = os.path.join(material_dir, channel_name)
if os.path.isdir(channel_path):
for texture_path in os.listdir(channel_path):
_, ext = os.path.splitext(texture_path)
full_path = os.path.join(
channel_path, texture_path
).replace("\\", "/")
pub_object = (
PathObject()
.from_path_string(full_path)
.get_publish_object(tree="render")
)
relative_path = remove_root(full_path)
channel_dict["channel"] = channel_name
channel_dict["filepath"] = relative_path
channel_dict["rgba"] = ""
channel_dict["ext"] = ext
channel_dict["pubpath"] = pub_object.get_abs_path(
relative=True
)
else:
print(render_dir, "not a directory")
data["materials"] = full_materials_dict
return data
[docs]
def render(self, preset_name="Alchemy Unreal Engine"):
"""
Goal: Render the textures for the current Substance Painter project.
"""
render_dir = self.path_object.get_render_path(dirname=True)
cfg = export_config(
export_dir=render_dir,
preset_name=preset_name,
file_format="png",
bit_depth="8",
max_size_log2=11, # 2^11 = 2048
padding="infinite",
dithering=True,
)
# plan_files = collect_export_plan_or_scan(cfg)
sp_export.export_project_textures(cfg)
# Some exporters finish asynchronously; a tiny safety net to include late files:
data = self.get_msd_data()
msd_path = self.path_object.get_msd_path(pub=False)
save_json(msd_path, data)
return msd_path
[docs]
def get_asset_path():
user_globals = CFG.user_config
try:
substance_mdl_path = user_globals["current_substance_asset_path"]
except KeyError:
logging.info("ERROR: No Mdl Path Saved for Tex Task")
return None
return substance_mdl_path
[docs]
def handle_save():
try:
asset_path = get_asset_path()
path_object = PathObject.from_path_string(asset_path)
tex_path_object = path_object.copy(
tree="source", task="tex", latest=True, ext=".spp", set_filename=True
)
out_path = tex_path_object.get_path()
os.makedirs(os.path.dirname(out_path), exist_ok=True)
sp_project.save_as(out_path)
logging.info(f"Saved Substance project to: {out_path}")
except sp_project.exception.ProjectError as e:
logging.exception(f"Painter ProjectError while saving: {e}")
except Exception as e:
logging.exception(f"Unexpected error while saving Painter project: {e}")
[docs]
def update_mesh():
from cgl.ui.widgets.dialog import InputDialog
scene_object = alc.Scene().scene_object()
if scene_object:
latest_mdl_publish = scene_object.copy(
tree="render",
user="publish",
task="mdl",
latest=True,
ext=".fbx",
set_filename=True,
)
substance_mesh_path = sp_project.last_imported_mesh_path()
if latest_mdl_publish.path_root == substance_mesh_path:
dialog_ = InputDialog(
title="Update Error",
message=f"Error: Can't Update.\nAlready using latest publish mdl\n Substance mdl: {substance_mesh_path}\n Latest published mdl: {latest_mdl_publish.get_path()}",
force_top_level=True,
)
dialog_.exec()
else:
logging.info(f"Loading mesh: {latest_mdl_publish.get_path()}")
sp_project.reload_mesh(
latest_mdl_publish.get_path(),
sp_project.MeshReloadingSettings(),
loaded_callback,
)
[docs]
def loaded_callback(status):
from cgl.ui.widgets.dialog import InputDialog
if status == sp_project.ReloadMeshStatus.SUCCESS:
dialog_ = InputDialog(
title="Update Successful",
message="Successfully updated project",
force_top_level=True,
)
dialog_.exec()
else:
dialog_ = InputDialog(
title="Failed Update",
message="Mesh could not be reloaded.\nSee substance log for more info",
force_top_level=True,
)
dialog_.exec()
[docs]
def get_template_path(template_name):
"""
Gets filepath to a project template file in substance
"""
program_files_path = os.getenv("ProgramFiles")
substance_path = os.path.join(
program_files_path, "Adobe", "Adobe Substance 3D Painter"
)
templates_path = os.path.join(
substance_path, "resources", "starter_assets", "templates"
)
template_file_path = os.path.join(templates_path, template_name) + ".spt"
return template_file_path
[docs]
def get_latest_mdl(path_object, ext=".fbx"):
"""
gets the latest published model
Returns:
"""
if not path_object:
return
mdl_object = path_object.copy(task="mdl")
msd = mdl_object.get_msd_dict()
render_files = msd["render_files"][ext]
if len(render_files) == 1:
return add_root(render_files[0])
elif len(render_files) > 1:
return render_files
return None
[docs]
def get_alchemy_ue_preset_url():
path_ = r"C:\Users\tmiko\PycharmProjects\cglumberjack\resources\substance"
import substance_painter.resource as spr
spres.Shelves.add("alchemy_presets", path_)
spres.Shelves.refresh_all()
preset_url = spr.ResourceID(
context="alchemy_presets", name="Alchemy Unreal Engine"
).url()
return preset_url
[docs]
def resolve_export_preset_url(
preset_name, alchemy_shelf_path=None, shelf_name="alchemy_presets"
):
# Optionally mount your Alchemy shelf (session-only)
if alchemy_shelf_path and os.path.isdir(alchemy_shelf_path):
from substance_painter import resource as spr
spr.Shelves.add(shelf_name, alchemy_shelf_path)
spr.Shelves.refresh_all()
# Prefer Alchemy shelf if provided, then user_assets, then starter_assets
for ctx in ["export-presets", "starter_assets"]:
rid = spres.ResourceID(context=ctx, name=preset_name)
# If the resource exists, export will accept this URL. Return it.
try:
# Cheap existence check: search by name and match context
for res in spres.search(preset_name):
ident = res.identifier()
if ident.name == preset_name and ident.context == ctx:
return rid.url()
except Exception:
pass
# Fallback: return the URL anyway; many installs resolve it fine
if ctx != "starter_assets": # keep trying; final fallback is starter_assets
url = rid.url()
if url:
return url
# Last resort: built-in context
return spres.ResourceID(context="starter_assets", name=preset_name).url()
[docs]
def export_config(
export_dir,
preset_name: str,
file_format="png",
bit_depth="8",
max_size_log2=11, # 2^11 = 2048
padding="infinite",
dithering=True,
):
# Built-in preset lives in the "starter_assets" context
# preset_url = resolve_export_preset_url(
# preset_name,
# # if you keep the preset in your repo instead of copying to user folder:
# # alchemy_shelf_path=r"E:\Alchemy\resources\painter_presets"
# )
preset_url = import_export_preset(preset_name).identifier().url()
# Build the export list for all texture sets (and stacks if layered)
export_list = []
for ts in spts.all_texture_sets():
if ts.is_layered_material():
for st in ts.all_stacks():
export_list.append({"rootPath": f"{ts.name()}/{st.name()}"})
else:
export_list.append({"rootPath": ts.name()})
# Catch-all rule sets format/bit depth/size globally
export_params = [
{
"filter": {},
"parameters": {
"fileFormat": file_format,
"bitDepth": bit_depth,
"sizeLog2": max_size_log2,
"paddingAlgorithm": padding,
"dithering": dithering,
},
}
]
return {
"exportPath": export_dir,
"exportShaderParams": False,
"defaultExportPreset": preset_url,
"exportList": export_list,
"exportParameters": export_params,
}
# def render_preflight_and_export(export_dir):
# os.makedirs(export_dir, exist_ok=True)
# cfg = unreal_export_config(export_dir)
# # Optional: preview what will be written
# _preview = sp_export.list_project_textures(cfg) # use for logging if you like
# result = sp_export.export_project_textures(cfg)
# return result
[docs]
def import_export_preset(preset_name):
code_root = CFG.get_code_root()
export_preset_path = (
os.path.join(code_root, "resources", "substance", "export-presets", preset_name)
+ ".spexp"
)
try:
new_resource = spres.import_session_resource(
export_preset_path, spres.Usage.EXPORT
)
except ValueError:
logging.info(f"Error Could Not Find Preset at: {export_preset_path}")
return None
return new_resource
if __name__ == "__main__":
import glob
render_dir = r"E:\Alchemy\JHCS\jcma\VERSIONS\render\assets\chr\frannyC\tex\default\tom\000.000\high"
folder_pattern = CFG.folders_config["filename"]["tex"]["folders"]
for i in folder_pattern:
render_dir = os.path.join(render_dir, "*")
render_dir = os.path.join(render_dir, "*")
glob_files = glob.glob(render_dir)
print(glob_files)
# path_object = PathObject().from_path_string(paths)
# print(get_latest_mdl(path_object))