Merge pull request #400 from ynput/bugfix/AY-4570_Substance-project-attributes

Substance Painter: Allow users to customize the template settings for project creation
This commit is contained in:
Kayla Man 2024-04-24 16:14:35 +08:00 committed by GitHub
commit 8f42c50100
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 277 additions and 42 deletions

View file

@ -586,7 +586,6 @@ def prompt_new_file_with_mesh(mesh_filepath):
# TODO: find a way to improve the process event to
# load more complicated mesh
app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents, 3000)
file_dialog.done(file_dialog.Accepted)
app.processEvents(QtCore.QEventLoop.AllEvents)
@ -606,7 +605,7 @@ def prompt_new_file_with_mesh(mesh_filepath):
mesh_select.setVisible(False)
# Ensure UI is visually up-to-date
app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents)
app.processEvents(QtCore.QEventLoop.ExcludeUserInputEvents, 8000)
# Trigger the 'select file' dialog to set the path and have the
# new file dialog to use the path.
@ -623,8 +622,6 @@ def prompt_new_file_with_mesh(mesh_filepath):
"Failed to set mesh path with the prompt dialog:"
f"{mesh_filepath}\n\n"
"Creating new project directly with the mesh path instead.")
else:
dialog.done(dialog.Accepted)
new_action = _get_new_project_action()
if not new_action:

View file

@ -1,3 +1,5 @@
import copy
from qtpy import QtWidgets, QtCore
from ayon_core.pipeline import (
load,
get_representation_path,
@ -8,10 +10,133 @@ from ayon_core.hosts.substancepainter.api.pipeline import (
set_container_metadata,
remove_container_metadata
)
from ayon_core.hosts.substancepainter.api.lib import prompt_new_file_with_mesh
import substance_painter.project
import qargparse
def _convert(substance_attr):
"""Return Substance Painter Python API Project attribute from string.
This converts a string like "ProjectWorkflow.Default" to for example
the Substance Painter Python API equivalent object, like:
`substance_painter.project.ProjectWorkflow.Default`
Args:
substance_attr (str): The `substance_painter.project` attribute,
for example "ProjectWorkflow.Default"
Returns:
Any: Substance Python API object of the project attribute.
Raises:
ValueError: If attribute does not exist on the
`substance_painter.project` python api.
"""
root = substance_painter.project
for attr in substance_attr.split("."):
root = getattr(root, attr, None)
if root is None:
raise ValueError(
"Substance Painter project attribute"
f" does not exist: {substance_attr}")
return root
def get_template_by_name(name: str, templates: list[dict]) -> dict:
return next(
template for template in templates
if template["name"] == name
)
class SubstanceProjectConfigurationWindow(QtWidgets.QDialog):
"""The pop-up dialog allows users to choose material
duplicate options for importing Max objects when updating
or switching assets.
"""
def __init__(self, project_templates):
super(SubstanceProjectConfigurationWindow, self).__init__()
self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint)
self.configuration = None
self.template_names = [template["name"] for template
in project_templates]
self.project_templates = project_templates
self.widgets = {
"label": QtWidgets.QLabel(
"Select your template for project configuration"),
"template_options": QtWidgets.QComboBox(),
"import_cameras": QtWidgets.QCheckBox("Import Cameras"),
"preserve_strokes": QtWidgets.QCheckBox("Preserve Strokes"),
"clickbox": QtWidgets.QWidget(),
"combobox": QtWidgets.QWidget(),
"buttons": QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.Ok
| QtWidgets.QDialogButtonBox.Cancel)
}
self.widgets["template_options"].addItems(self.template_names)
template_name = self.widgets["template_options"].currentText()
self._update_to_match_template(template_name)
# Build clickboxes
layout = QtWidgets.QHBoxLayout(self.widgets["clickbox"])
layout.addWidget(self.widgets["import_cameras"])
layout.addWidget(self.widgets["preserve_strokes"])
# Build combobox
layout = QtWidgets.QHBoxLayout(self.widgets["combobox"])
layout.addWidget(self.widgets["template_options"])
# Build buttons
layout = QtWidgets.QHBoxLayout(self.widgets["buttons"])
# Build layout.
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.widgets["label"])
layout.addWidget(self.widgets["combobox"])
layout.addWidget(self.widgets["clickbox"])
layout.addWidget(self.widgets["buttons"])
self.widgets["template_options"].currentTextChanged.connect(
self._update_to_match_template)
self.widgets["buttons"].accepted.connect(self.on_accept)
self.widgets["buttons"].rejected.connect(self.on_reject)
def on_accept(self):
self.configuration = self.get_project_configuration()
self.close()
def on_reject(self):
self.close()
def _update_to_match_template(self, template_name):
template = get_template_by_name(template_name, self.project_templates)
self.widgets["import_cameras"].setChecked(template["import_cameras"])
self.widgets["preserve_strokes"].setChecked(
template["preserve_strokes"])
def get_project_configuration(self):
templates = self.project_templates
template_name = self.widgets["template_options"].currentText()
template = get_template_by_name(template_name, templates)
template = copy.deepcopy(template) # do not edit the original
template["import_cameras"] = self.widgets["import_cameras"].isChecked()
template["preserve_strokes"] = (
self.widgets["preserve_strokes"].isChecked()
)
for key in ["normal_map_format",
"project_workflow",
"tangent_space_mode"]:
template[key] = _convert(template[key])
return template
@classmethod
def prompt(cls, templates):
dialog = cls(templates)
dialog.exec_()
configuration = dialog.configuration
dialog.deleteLater()
return configuration
class SubstanceLoadProjectMesh(load.LoaderPlugin):
@ -25,48 +150,35 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin):
icon = "code-fork"
color = "orange"
options = [
qargparse.Boolean(
"preserve_strokes",
default=True,
help="Preserve strokes positions on mesh.\n"
"(only relevant when loading into existing project)"
),
qargparse.Boolean(
"import_cameras",
default=True,
help="Import cameras from the mesh file."
)
]
# Defined via settings
project_templates = []
def load(self, context, name, namespace, data):
def load(self, context, name, namespace, options=None):
# Get user inputs
import_cameras = data.get("import_cameras", True)
preserve_strokes = data.get("preserve_strokes", True)
sp_settings = substance_painter.project.Settings(
import_cameras=import_cameras
)
result = SubstanceProjectConfigurationWindow.prompt(
self.project_templates)
if not result:
# cancelling loader action
return
if not substance_painter.project.is_open():
# Allow to 'initialize' a new project
path = self.filepath_from_context(context)
# TODO: improve the prompt dialog function to not
# only works for simple polygon scene
result = prompt_new_file_with_mesh(mesh_filepath=path)
if not result:
self.log.info("User cancelled new project prompt."
"Creating new project directly from"
" Substance Painter API Instead.")
settings = substance_painter.project.create(
mesh_file_path=path, settings=sp_settings
)
sp_settings = substance_painter.project.Settings(
import_cameras=result["import_cameras"],
normal_map_format=result["normal_map_format"],
project_workflow=result["project_workflow"],
tangent_space_mode=result["tangent_space_mode"],
default_texture_resolution=result["default_texture_resolution"]
)
settings = substance_painter.project.create(
mesh_file_path=path, settings=sp_settings
)
else:
# Reload the mesh
settings = substance_painter.project.MeshReloadingSettings(
import_cameras=import_cameras,
preserve_strokes=preserve_strokes
)
import_cameras=result["import_cameras"],
preserve_strokes=result["preserve_strokes"])
def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): # noqa
if status == substance_painter.project.ReloadMeshStatus.SUCCESS: # noqa
@ -92,7 +204,7 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin):
# from the user's original choice. We don't store 'preserve_strokes'
# as we always preserve strokes on updates.
container["options"] = {
"import_cameras": import_cameras,
"import_cameras": result["import_cameras"],
}
set_container_metadata(project_mesh_object_name, container)

View file

@ -0,0 +1,122 @@
from ayon_server.settings import BaseSettingsModel, SettingsField
def normal_map_format_enum():
return [
{"label": "DirectX", "value": "NormalMapFormat.DirectX"},
{"label": "OpenGL", "value": "NormalMapFormat.OpenGL"},
]
def tangent_space_enum():
return [
{"label": "Per Fragment", "value": "TangentSpace.PerFragment"},
{"label": "Per Vertex", "value": "TangentSpace.PerVertex"},
]
def uv_workflow_enum():
return [
{"label": "Default", "value": "ProjectWorkflow.Default"},
{"label": "UV Tile", "value": "ProjectWorkflow.UVTile"},
{"label": "Texture Set Per UV Tile",
"value": "ProjectWorkflow.TextureSetPerUVTile"}
]
def document_resolution_enum():
return [
{"label": "128", "value": 128},
{"label": "256", "value": 256},
{"label": "512", "value": 512},
{"label": "1024", "value": 1024},
{"label": "2048", "value": 2048},
{"label": "4096", "value": 4096}
]
class ProjectTemplatesModel(BaseSettingsModel):
_layout = "expanded"
name: str = SettingsField("default", title="Template Name")
default_texture_resolution: int = SettingsField(
1024, enum_resolver=document_resolution_enum,
title="Document Resolution",
description=("Set texture resolution when "
"creating new project.")
)
import_cameras: bool = SettingsField(
True, title="Import Cameras",
description="Import cameras from the mesh file.")
normal_map_format: str = SettingsField(
"DirectX", enum_resolver=normal_map_format_enum,
title="Normal Map Format",
description=("Set normal map format when "
"creating new project.")
)
project_workflow: str = SettingsField(
"Default", enum_resolver=uv_workflow_enum,
title="UV Tile Settings",
description=("Set UV workflow when "
"creating new project.")
)
tangent_space_mode: str = SettingsField(
"PerFragment", enum_resolver=tangent_space_enum,
title="Tangent Space",
description=("An option to compute tangent space "
"when creating new project.")
)
preserve_strokes: bool = SettingsField(
True, title="Preserve Strokes",
description=("Preserve strokes positions on mesh.\n"
"(only relevant when loading into "
"existing project)")
)
class ProjectTemplateSettingModel(BaseSettingsModel):
project_templates: list[ProjectTemplatesModel] = SettingsField(
default_factory=ProjectTemplatesModel,
title="Project Templates"
)
class LoadersModel(BaseSettingsModel):
SubstanceLoadProjectMesh: ProjectTemplateSettingModel = SettingsField(
default_factory=ProjectTemplateSettingModel,
title="Load Mesh"
)
DEFAULT_LOADER_SETTINGS = {
"SubstanceLoadProjectMesh": {
"project_templates": [
{
"name": "2K(Default)",
"default_texture_resolution": 2048,
"import_cameras": True,
"normal_map_format": "NormalMapFormat.DirectX",
"project_workflow": "ProjectWorkflow.Default",
"tangent_space_mode": "TangentSpace.PerFragment",
"preserve_strokes": True
},
{
"name": "2K(UV tile)",
"default_texture_resolution": 2048,
"import_cameras": True,
"normal_map_format": "NormalMapFormat.DirectX",
"project_workflow": "ProjectWorkflow.UVTile",
"tangent_space_mode": "TangentSpace.PerFragment",
"preserve_strokes": True
},
{
"name": "4K(Custom)",
"default_texture_resolution": 4096,
"import_cameras": True,
"normal_map_format": "NormalMapFormat.OpenGL",
"project_workflow": "ProjectWorkflow.UVTile",
"tangent_space_mode": "TangentSpace.PerFragment",
"preserve_strokes": True
}
]
}
}

View file

@ -1,5 +1,6 @@
from ayon_server.settings import BaseSettingsModel, SettingsField
from .imageio import ImageIOSettings, DEFAULT_IMAGEIO_SETTINGS
from .load_plugins import LoadersModel, DEFAULT_LOADER_SETTINGS
class ShelvesSettingsModel(BaseSettingsModel):
@ -17,9 +18,12 @@ class SubstancePainterSettings(BaseSettingsModel):
default_factory=list,
title="Shelves"
)
load: LoadersModel = SettingsField(
default_factory=DEFAULT_LOADER_SETTINGS, title="Loaders")
DEFAULT_SPAINTER_SETTINGS = {
"imageio": DEFAULT_IMAGEIO_SETTINGS,
"shelves": []
"shelves": [],
"load": DEFAULT_LOADER_SETTINGS,
}

View file

@ -1 +1 @@
__version__ = "0.1.0"
__version__ = "0.1.1"