Skip to content
This repository has been archived by the owner on Sep 20, 2024. It is now read-only.

Substance Painter: Allow users to set texture resolutions when loading mesh to create project #6262

Merged
merged 20 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading