Skip to content

Commit

Permalink
Merge pull request #6262 from ynput/bugfix/AY-4570_Substance-project-…
Browse files Browse the repository at this point in the history
…attributes

Substance Painter: Allow users to set texture resolutions when loading mesh to create project
  • Loading branch information
moonyuet committed Apr 24, 2024
2 parents f976e5b + 39932cc commit 41e72ff
Show file tree
Hide file tree
Showing 7 changed files with 380 additions and 39 deletions.
4 changes: 1 addition & 3 deletions openpype/hosts/substancepainter/api/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ def _setup_prompt():
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.
Expand All @@ -623,8 +623,6 @@ def _setup_prompt():
"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:
Expand Down
186 changes: 153 additions & 33 deletions openpype/hosts/substancepainter/plugins/load/load_mesh.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import copy
from qtpy import QtWidgets, QtCore
from openpype.pipeline import (
load,
get_representation_path,
Expand All @@ -11,7 +13,131 @@
from openpype.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):
Expand All @@ -25,48 +151,42 @@ 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)
result = SubstanceProjectConfigurationWindow.prompt(
self.project_templates)
if not result:
# cancelling loader action
return
sp_settings = substance_painter.project.Settings(
import_cameras=import_cameras
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"]
)
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
Expand All @@ -92,7 +212,7 @@ def on_mesh_reload(status: substance_painter.project.ReloadMeshStatus): # noqa
# 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,38 @@
"rules": {}
}
},
"shelves": {}
"shelves": {},
"load": {
"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
}
]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,70 @@
"object_type": {
"type": "text"
}
},
{
"type": "dict",
"collapsible": true,
"key": "load",
"label": "Loaders",
"use_label_wrap": true,
"children": [
{
"type": "dict",
"collapsible": true,
"key": "SubstanceLoadProjectMesh",
"label": "Load Mesh",
"children": [
{
"type": "list",
"collapsible": true,
"key": "project_templates",
"label": "Project Templates",
"object_type": {
"type": "dict",
"children": [
{
"type": "text",
"key": "name",
"label": "Name"
},
{
"type": "number",
"key": "default_texture_resolution",
"label": "Document Resolution"
},
{
"type": "boolean",
"key": "import_cameras",
"label": "Import Cameras"
},
{
"type": "text",
"key": "normal_map_format",
"label": "Normal Map Format"
},
{
"type": "text",
"key": "project_workflow",
"label": "UV Tile Settings"
},
{
"type": "text",
"key": "tangent_space_mode",
"label": "Normal Map Format"
},
{
"type": "boolean",
"key": "preserve_strokes",
"label": "Preserve Strokes"
}
]
}
}
]
}

]
}
]
}
Loading

0 comments on commit 41e72ff

Please sign in to comment.