2025-07-01

This commit is contained in:
2026-03-17 14:30:01 -06:00
parent f9a22056dd
commit 62b5978595
4579 changed files with 1257472 additions and 0 deletions
@@ -0,0 +1,81 @@
"""
Copyright (C) 2023 Adobe.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
# file: sbsar/async_ops.py
# brief: Asynchronous substance operations
# author Adobe - 3D & Immersive
# copyright 2023 Adobe Inc. All rights reserved.
import os
import bpy
import traceback
from ..utils import SUBSTANCE_Utils
from ..thread_ops import SUBSTANCE_Threads
from ..common import RENDER_KEY, ADDON_PACKAGE
def _render_sbsar(context, sbsar, index):
_addon_prefs = context.preferences.addons[ADDON_PACKAGE].preferences
if not os.path.exists(_addon_prefs.path_default):
os.makedirs(_addon_prefs.path_default)
from ..api import SUBSTANCE_Api
_render_id = RENDER_KEY.format(sbsar.uuid, sbsar.graphs[index].index)
_graph = sbsar.graphs[index]
_outputs = []
for _output in _graph.outputs:
_item = {
"identifier": _output.identifier,
"enabled": _output.shader_enabled,
"format": _output.shader_format,
"bitdepth": _output.shader_bitdepth
}
_outputs.append(_item)
_data = {
"uuid": sbsar.uuid,
"index": index,
"out_path": _addon_prefs.path_default,
"outputs": _outputs
}
SUBSTANCE_Api.sbsar_render(_render_id, _data)
def _set_input_visibility(sbsar, graph, inputs):
try:
def _callback_visibility():
_selected_sbsar = None
for _item in bpy.context.scene.loaded_sbsars:
if _item.uuid == sbsar.uuid:
_selected_sbsar = _item
break
if _selected_sbsar is None:
return
_graph = _selected_sbsar.graphs[int(graph.index)]
for _input in inputs:
if _input["identifier"] in _graph.inputs:
_graph.inputs[_input["identifier"]].visibleIf = _input["visibleIf"]
SUBSTANCE_Threads.main_thread_run(_callback_visibility)
except Exception:
SUBSTANCE_Utils.log_data("ERROR", "Exception - Unknown Error while setting parameter visibility:")
SUBSTANCE_Utils.log_traceback(traceback.format_exc())
@@ -0,0 +1,139 @@
"""
Copyright (C) 2023 Adobe.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
# file: shader/callbacks.py
# brief: Callbacks for susbtance outputs and properties
# author Adobe - 3D & Immersive
# copyright 2023 Adobe Inc. All rights reserved.
import bpy
from ..thread_ops import SUBSTANCE_Threads
from ..sbsar.async_ops import _render_sbsar, _set_input_visibility
from ..utils import SUBSTANCE_Utils
from ..common import (
PRESET_CUSTOM,
Code_InputType,
Code_InputIdentifier,
Code_InputWidget,
Code_OutputSizeSuffix,
Code_Response
)
class SUBSTANCE_SbsarCallbacks():
# Output Size
@staticmethod
def on_linked_changed(self, context, parm_identifier):
if not hasattr(self, Code_InputIdentifier.outputsize.value + Code_OutputSizeSuffix.linked.value):
return
_linked = getattr(self, Code_InputIdentifier.outputsize.value + Code_OutputSizeSuffix.linked.value)
_new_width_width = getattr(self, Code_InputIdentifier.outputsize.value + Code_OutputSizeSuffix.width.value)
if _linked:
setattr(self, Code_InputIdentifier.outputsize.value + Code_OutputSizeSuffix.height.value, _new_width_width)
else:
SUBSTANCE_SbsarCallbacks.on_outputsize_changed(self, context, parm_identifier)
@staticmethod
def on_outputsize_changed(self, context, identifier):
if not self.callback["enabled"]:
return
_sbsar = context.scene.loaded_sbsars[context.scene.sbsar_index]
_graph = SUBSTANCE_Utils.get_selected_graph(context)
_preset = int(_graph.presets_list)
if not _graph.outputsize_exists:
return
_input = _graph.inputs[Code_InputIdentifier.outputsize.value]
_output_size_x = getattr(self, Code_InputIdentifier.outputsize.value + Code_OutputSizeSuffix.width.value)
_output_size_y = getattr(self, Code_InputIdentifier.outputsize.value + Code_OutputSizeSuffix.height.value)
_new_value = [int(_output_size_x), int(_output_size_y)]
from ..api import SUBSTANCE_Api
_result = SUBSTANCE_Api.sbsar_input_update(
context,
_sbsar,
_graph,
_input,
_new_value,
SUBSTANCE_SbsarCallbacks.on_input_update
)
if _result[0] == Code_Response.success:
_new_preset_value = SUBSTANCE_Utils.update_preset_outputsize(
_graph.presets[_preset].value,
_input,
Code_InputType.integer2.value,
_new_value
)
_graph.presets[_preset].value = _new_preset_value
# Parameters
@staticmethod
def on_input_update(context, sbsar, graph, input, value):
_value = SUBSTANCE_Utils.value_fix_type(input.identifier, input.guiWidget, input.type, value)
if input.guiWidget == Code_InputWidget.image.value:
if _value == "":
_value = "NONE"
else:
_filepath = bpy.data.images[_value].filepath
if _filepath == "":
_filepath = SUBSTANCE_Utils.render_image_input(bpy.data.images[_value], bpy.context)
_value = _filepath
from ..api import SUBSTANCE_Api
_result = SUBSTANCE_Api.sbsar_input_set(sbsar, graph, input, _value)
if context is not None and _result[0] == Code_Response.success:
SUBSTANCE_Threads.alt_thread_run(_set_input_visibility, (
sbsar,
graph,
_result[1]["data"]["inputs"]))
SUBSTANCE_Threads.alt_thread_run(_render_sbsar, (
context,
sbsar,
int(graph.index)))
graph.presets[PRESET_CUSTOM].value = _result[1]["data"]["preset"]
return _result
@staticmethod
def on_input_changed(self, context, identifier):
if not self.callback["enabled"]:
return
_sbsar = context.scene.loaded_sbsars[context.scene.sbsar_index]
_graph = SUBSTANCE_Utils.get_selected_graph(context)
_preset = int(_graph.presets_list)
if _graph.presets[_preset].name != PRESET_CUSTOM:
_graph.preset_callback = False
_graph.presets_list = _graph.presets[PRESET_CUSTOM].index
_graph.preset_callback = True
_input = _graph.inputs[identifier]
_new_value = getattr(self, identifier)
from ..api import SUBSTANCE_Api
SUBSTANCE_Api.sbsar_input_update(
context,
_sbsar,
_graph,
_input,
_new_value,
SUBSTANCE_SbsarCallbacks.on_input_update
)
@@ -0,0 +1,57 @@
"""
Copyright (C) 2023 Adobe.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
# file: sbsar/manager.py
# brief: Substance operations manager
# author Adobe - 3D & Immersive
# copyright 2023 Adobe Inc. All rights reserved.
import traceback
from ..factory.sbsar import SUBSTANCE_SbsarFactory
from ..common import Code_Response
from ..utils import SUBSTANCE_Utils
class SUBSTANCE_SbsarManager():
def __init__(self):
self.sbsars = {}
def get(self, uuid):
if uuid not in self.sbsars:
return None
return self.sbsars[uuid]
def register(self, sbsar):
self.sbsars[sbsar.uuid] = sbsar
_result = SUBSTANCE_SbsarFactory.register_class(sbsar)
return _result
def unregister(self, uuid):
try:
if uuid in self.sbsars:
_sbsar = self.sbsars[uuid]
for _graph in _sbsar.graphs:
SUBSTANCE_SbsarFactory.unregister_class(_graph.inputs_class_name)
del self.sbsars[uuid]
return (Code_Response.success, None)
else:
return (Code_Response.sbsar_remove_not_found_error, None)
except Exception:
SUBSTANCE_Utils.log_data("ERROR", "Exception - Substance removal error:")
SUBSTANCE_Utils.log_traceback(traceback.format_exc())
return (Code_Response.sbsar_factory_unregister_error, None)
@@ -0,0 +1,344 @@
"""
Copyright (C) 2023 Adobe.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
# file: sbsar/sbsar.py
# brief: Substance class object definition
# author Adobe - 3D & Immersive
# copyright 2023 Adobe Inc. All rights reserved.
from ..utils import SUBSTANCE_Utils
from ..common import (
Code_InputType,
Code_InputWidget,
INPUT_DEFAULT_GROUP,
INPUT_IMAGE_DEFAULT_GROUP,
PRESET_CUSTOM,
CLASS_GRAPH_INPUTS
)
class SBS_EnumValue():
def __init__(self, enum):
self.first = enum["first"]
self.second = enum["second"]
def to_json(self):
_obj = {
"first": self.first,
"second": self.second,
}
return _obj
class SBS_Input():
def __init__(self, index, input):
if "index" in input:
self.index = input["index"]
else:
self.index = index
self.id = input["id"]
self.graphID = input["graphID"]
self.identifier = input["identifier"]
self.label = input["label"]
self.guiDescription = input["guiDescription"]
if input["type"] == Code_InputType.image.name:
self.guiGroup = INPUT_IMAGE_DEFAULT_GROUP
elif len(input["guiGroup"]) == 0:
self.guiGroup = INPUT_DEFAULT_GROUP
else:
self.guiGroup = input["guiGroup"]
self.guiVisibleIf = input["guiVisibleIf"]
self.userTag = input["userTag"]
self.type = input["type"]
self.guiWidget = input["guiWidget"]
self.showAsPin = input["showAsPin"]
self.useCache = input["useCache"]
self.visibleIf = input["visibleIf"]
self.isHeavyDuty = input["isHeavyDuty"]
self.enumValues = []
if "labelFalse" in input:
self.labelFalse = input["labelFalse"]
if "labelTrue" in input:
self.labelTrue = input["labelTrue"]
if "sliderClamp" in input:
self.sliderClamp = input["sliderClamp"]
if "sliderStep" in input:
self.sliderStep = input["sliderStep"]
if "maxValue" in input:
self.maxValue = input["maxValue"]
if "minValue" in input:
self.minValue = input["minValue"]
if "defaultValue" in input:
self.defaultValue = input["defaultValue"]
if "value" in input:
self.value = input["value"]
if "channelUse" in input:
self.channelUse = input["channelUse"]
if "enumValues" in input:
for _enum in input["enumValues"]:
_new_enum = SBS_EnumValue(_enum)
self.enumValues.append(_new_enum)
def to_json(self):
_obj = {
"id": self.id,
"graphID": self.graphID,
"identifier": self.identifier,
"label": self.label,
"guiDescription": self.guiDescription,
"guiGroup": self.guiGroup,
"guiVisibleIf": self.guiVisibleIf,
"userTag": self.userTag,
"type": self.type,
"guiWidget": self.guiWidget,
"showAsPin": self.showAsPin,
"useCache": self.useCache,
"visibleIf": self.visibleIf,
"isHeavyDuty": self.isHeavyDuty
}
if hasattr(self, "labelFalse"):
_obj["labelFalse"] = self.labelFalse
if hasattr(self, "labelTrue"):
_obj["labelTrue"] = self.labelTrue
if hasattr(self, "sliderClamp"):
_obj["sliderClamp"] = self.sliderClamp
if hasattr(self, "sliderStep"):
_obj["sliderStep"] = self.sliderStep
if hasattr(self, "maxValue"):
_obj["maxValue"] = self.maxValue
if hasattr(self, "minValue"):
_obj["minValue"] = self.minValue
if hasattr(self, "defaultValue"):
_obj["defaultValue"] = self.defaultValue
if hasattr(self, "value"):
_obj["value"] = self.value
if hasattr(self, "channelUse"):
_obj["channelUse"] = self.channelUse
if hasattr(self, "enumValues"):
_obj["enumValues"] = self.enumValues
return _obj
class SBS_Output():
def __init__(self, output, index):
self.id = output["id"]
self.index = index
self.graphID = output["graphID"]
self.format = output["format"]
self.mipmaps = output["mipmaps"]
self.identifier = output["identifier"]
self.label = output["label"]
self.guiDescription = output["guiDescription"]
self.group = output["group"]
self.guiVisibleIf = output["guiVisibleIf"]
self.userTag = output["userTag"]
self.type = output["type"]
self.guiType = output["guiType"]
self.defaultChannelUse = output["defaultChannelUse"]
self.enabled = output["enabled"]
self.channelUseSpecified = output["channelUseSpecified"]
self.channelUse = output["channelUse"]
def to_json(self):
_obj = {
"id": self.id,
"graphID": self.graphID,
"format": self.format,
"mipmaps": self.mipmaps,
"identifier": self.identifier,
"label": self.label,
"guiDescription": self.guiDescription,
"group": self.group,
"guiVisibleIf": self.guiVisibleIf,
"userTag": self.userTag,
"type": self.type,
"guiType": self.guiType,
"defaultChannelUse": self.defaultChannelUse,
"enabled": self.enabled,
"channelUseSpecified": self.channelUseSpecified,
"channelUse": self.channelUse
}
return _obj
class SBS_Preset():
def __init__(self, preset, addon_prefs=None, inputs=None):
self.index = str(preset["index"])
self.label = preset["label"]
self.value = preset["value"]
if "icon" in preset:
self.icon = preset["icon"]
else:
self.icon = "LOCKED" if self.label != PRESET_CUSTOM else "UNLOCKED"
if "embedded" in preset:
self.embedded = preset["embedded"]
else:
self.embedded = True if self.label != PRESET_CUSTOM else False
if addon_prefs is not None and inputs is not None:
if "$outputsize" in inputs:
self.value = SUBSTANCE_Utils.update_preset_outputsize(
self.value,
inputs["$outputsize"],
Code_InputType.integer2.value,
addon_prefs.default_resolution.get())
if "normal_format" in inputs:
self.value = SUBSTANCE_Utils.update_preset_outputsize(
self.value,
inputs["normal_format"],
Code_InputType.integer.value,
0 if addon_prefs.default_normal_format == "DirectX" else 1)
def to_json(self):
_obj = {
"index": int(self.index),
"label": self.label,
"value": self.value,
"icon": self.icon,
"embedded": self.embedded
}
return _obj
class SBS_Graph():
def __init__(self, unique_name, uuid, index, graph, multi_graph, addon_prefs=None):
self.index = str(index)
self.uid = str(graph["uid"])
self.label = graph["label"]
self.identifier = graph["packageUrl"].replace("pkg://", "")
self.packageUrl = graph["packageUrl"]
if graph["physicalSize"][0] == 0 or graph["physicalSize"][1] == 0:
self.physicalSize = (1/100, 1/100, 1/100)
else:
self.physicalSize = (graph["physicalSize"][0]/100,
graph["physicalSize"][1]/100,
graph["physicalSize"][2]/100)
self.tiling = addon_prefs.default_tiling.get()
self.inputs = {}
self.inputs_groups = {}
self.outputs = {}
self.presets = []
if multi_graph:
_material_name = "{}-{}".format(unique_name, graph["label"]).replace(" ", "_")
_class_name = "{}-{}".format(uuid, self.uid)
else:
_material_name = "{}".format(unique_name).replace(" ", "_")
_class_name = "{}".format(uuid)
self.material = _material_name
self.inputs_class_name = CLASS_GRAPH_INPUTS.format(_class_name)
if len(graph["inputs"]) > 0 and "index" in graph["inputs"][0]:
_sorted_inputs = sorted(graph["inputs"], key=lambda d: d['index'])
else:
_sorted_inputs = graph["inputs"]
_input_index = 0
for _input in _sorted_inputs:
if _input["guiWidget"] != Code_InputWidget.nowidget.value or _input["type"] != Code_InputType.string.name:
if _input["type"] == Code_InputType.image.name:
_group = INPUT_IMAGE_DEFAULT_GROUP
else:
_group = _input["guiGroup"] if len(_input["guiGroup"]) > 0 else INPUT_DEFAULT_GROUP
if _group not in self.inputs_groups:
self.inputs_groups[_group] = [_input["identifier"]]
else:
self.inputs_groups[_group].append(_input["identifier"])
_new_input = SBS_Input(_input_index, _input)
_input_index += 1
self.inputs[_input["identifier"]] = _new_input
for _idx, _output in enumerate(graph["outputs"]):
_new_output = SBS_Output(_output, _idx)
self.outputs[_output["identifier"]] = _new_output
_pre_sort_presets = []
for _preset in graph["presets"]:
_new_preset = SBS_Preset(_preset, addon_prefs, self.inputs)
_pre_sort_presets.append(_new_preset)
self.presets = sorted(_pre_sort_presets, key=lambda _preset: _preset.index, reverse=False)
def to_json(self):
_obj = {
"uid": self.uid,
"label": self.label,
"physicalSize": self.physicalSize,
"inputs": [],
"outputs": [],
"presets": []
}
for _preset in self.presets:
_obj["presets"].append(_preset.to_json())
for _key, _output in self.outputs.items():
_obj["outputs"].append(_output.to_json())
for _key, _input in self.inputs.items():
_obj["inputs"].append(_input.to_json())
return _obj
def reset_presets(self, presets):
self.presets = []
_pre_sort_presets = []
for _preset in presets:
_new_preset = SBS_Preset(_preset)
_pre_sort_presets.append(_new_preset)
self.presets = sorted(_pre_sort_presets, key=lambda _preset: _preset.index, reverse=False)
class SBSAR():
def __init__(self, unique_name, filename, data, addon_prefs=None):
self.uuid = data["uuid"]
self.version = data["version"]
self.name = unique_name
self.filename = filename
self.filepath = data["filename"]
self.graphs = []
_multi_graph = len(data["graphs"]) > 1
for _idx, _graph in enumerate(data["graphs"]):
_new_graph = SBS_Graph(unique_name, self.uuid, _idx, _graph, _multi_graph, addon_prefs)
self.graphs.append(_new_graph)
def to_json(self):
_obj = {
"uuid": self.uuid,
"name": self.name,
"filename": self.filename,
"filepath": self.filepath,
"graphs": []
}
for _graph in self.graphs:
_obj["graphs"].append(_graph.to_json())
return _obj