Source code for cgl.plugins.substance.tasks.tex

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))