Files
2026-03-17 14:58:51 -06:00

414 lines
14 KiB
Python

# #### BEGIN GPL LICENSE BLOCK #####
#
# 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 2
# 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, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
from typing import List
from bpy.types import Operator
from bpy.props import (
BoolProperty,
EnumProperty,
FloatProperty,
IntProperty,
StringProperty)
import bpy.utils.previews
from ..modules.poliigon_core.multilingual import _t
from ..material_import_utils import find_identical_material
from .utils_operator import fill_size_drop_down
from ..toolbox import get_context
from ..utils import (
is_cycles,
is_eevee_next)
from .. import reporting
UV_OPTIONS = [("UV", "UV", "UV"),
("MOSAIC", "Mosaic", "Poliigon Mosaic"),
("FLAT", "Flat", "Flat"),
("BOX", "Box", "Box"),
("SPHERE", "Sphere", "Sphere"),
("TUBE", "Tube", "Tube")]
def set_op_mat_disp_strength(ops, asset_name: str, mode_disp: str) -> None:
if asset_name.startswith("Poliigon_"):
ops.displacement = 0.2
elif mode_disp == "MICRO":
ops.displacement = 0.05
else:
ops.displacement = 0.0
class POLIIGON_OT_material(Operator):
bl_idname = "poliigon.poliigon_material"
bl_label = _t("Poliigon Material Import")
bl_description = _t("Create Material")
bl_options = {"GRAB_CURSOR", "BLOCKING", "REGISTER", "INTERNAL", "UNDO"}
def _get_dispopts(self, context):
options = [
("NORMAL", "Normal Only", "Use the Normal Map for surface details")
]
if is_cycles() or is_eevee_next():
# Only cycles and 4.2's eevee next support displacement
options.append(
("BUMP",
"Bump Only",
("Use the displacement map for surface details without "
"displacement"))
)
options.append(
("DISP",
"Displacement and Bump",
("Use the displacement map for surface details and physical "
"displacement"))
)
if is_cycles():
# While Eevee next in blender 4.2 could have the above options,
# below is still only relevnat in cycles.
options.append(
("MICRO",
"Adaptive Displacement Only",
("Use the displacement map for physical displacement with "
"adaptive geometry subdivisions\n"
"Note: This has a high render performance cost!"))
)
return options
def _fill_size_drop_down(self, context):
return fill_size_drop_down(cTB, self.asset_id)
def _update_displacement_options(self, context):
# We can not access self.asset_data, here!
asset_data = cTB._asset_index.get_asset(self.asset_id)
if asset_data.local_convention == 1:
self.displacement = 0.2
else:
self.displacement = 0.05
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
asset_id: IntProperty(options={"HIDDEN"}) # noqa: F821
size: EnumProperty(
name=_t("Texture"), # noqa: F821
items=_fill_size_drop_down,
description=_t("Change size of assigned textures.") # noqa: F722
)
mapping: EnumProperty(
name=_t("Mapping"), # noqa: F821
items=UV_OPTIONS,
default="UV" # noqa: F821
)
scale: FloatProperty(
name=_t("Scale"), # noqa: F821
default=1.0
)
if bpy.app.version >= (3, 0):
mode_disp: EnumProperty(name="Displacement", # noqa: F821
items=_get_dispopts,
default=0, # noqa: F821
update=_update_displacement_options)
else:
mode_disp: EnumProperty(name="Displacement", # noqa: F821
items=_get_dispopts,
update=_update_displacement_options)
displacement: FloatProperty(
name=_t("Displacement Strength"), # noqa: F722
default=0.0
)
use_16bit: BoolProperty(
name=_t("16-Bit Textures (if any)"), # noqa: F722
default=False
)
reuse_material: BoolProperty(
name=_t("Reuse Material"), # noqa: F722
default=True
)
keep_unused_tex_nodes: BoolProperty(
name=_t("Keep Additional Texture Nodes"), # noqa: F722
default=True
)
# TODO(Andreas): Can do_rename be removed, seems not in use at all?
do_rename: BoolProperty(options={"HIDDEN"}) # noqa: F821
do_apply: BoolProperty(options={"HIDDEN"}, default=True) # noqa: F821
ignore_map_prefs: BoolProperty(options={"HIDDEN"}, default=False) # noqa: F821
def __init__(self, *args, **kwargs):
"""Runs once per operator call before drawing occurs."""
super().__init__(*args, **kwargs)
# Note: During property update handlers (like e.g.
# _update_displacement_options()) we can not rely on on these
# members to be defined!
self.asset_data = None
self.exec_count = 0
@staticmethod
def init_context(addon_version: str) -> None:
"""Called from operators.py to init global addon context."""
global cTB
cTB = get_context(addon_version)
@classmethod
def description(cls, context, properties):
return properties.tooltip
def draw(self, context):
if self.asset_data is None:
self.asset_data = cTB._asset_index.get_asset(self.asset_id)
is_backplate = self.asset_data.is_backplate()
col = self.layout.column()
col.prop(self, "size")
row = col.row()
row.prop(self, "mapping")
row.enabled = not is_backplate
if is_cycles() or is_eevee_next():
row = col.row()
row.prop(self, "mode_disp")
row.enabled = not is_backplate
if self.mode_disp != "NORMAL" and (is_cycles() or is_eevee_next()):
row = col.row()
row.prop(self, "displacement")
row.enabled = not is_backplate
row = col.row()
row.prop(self, "scale") # TODO(Andreas): implement for backplate?
row.enabled = not is_backplate
row = col.row()
row.prop(self, "use_16bit")
row.enabled = not is_backplate
col.prop(self, "reuse_material")
row = col.row()
row.prop(self, "keep_unused_tex_nodes")
row.enabled = not self.reuse_material
def evaluate_displacement_method_property(self) -> None:
"""Tries to set dislacement method from prefs, if not set already."""
if self.properties.is_property_set("mode_disp"):
return
try:
self.mode_disp = cTB.prefs.mode_disp
except TypeError:
# Could be Eevee active while assigning a displacement value
self.mode_disp = "NORMAL"
except AttributeError as e:
reporting.capture_exception(e)
self.mode_disp = "NORMAL"
def determine_objects_needing_subdiv(
self, context) -> List[bpy.types.Object]:
"""Returns a list of selected objects, which still need a Subdivision
modifier for microdisplacement.
"""
if not is_cycles() or self.mode_disp != "MICRO":
return []
objs_selected = [_obj for _obj in context.selected_objects]
objs_add_subdiv = [_obj
for _obj in objs_selected
if "Subdivision" not in _obj.modifiers]
if bpy.app.version < (5, 0):
# Blender 5.0 no longer has feature sets, adaptive is included
bpy.context.scene.cycles.feature_set = "EXPERIMENTAL"
return objs_add_subdiv
def add_subdiv_to_objects(self, obj_list: List[bpy.types.Object]) -> None:
"""Adds a Subdivision modifier to all objects in list."""
for _obj in obj_list:
if bpy.app.version < (5, 0):
# In blender 5.0, there's no special settings for this.
_obj.cycles.use_adaptive_subdivision = True
modifier = _obj.modifiers.new("Subdivision", "SUBSURF")
if modifier is None:
# TODO(Andreas): Would we want to report failure to create modifier?
continue
modifier.subdivision_type = "SIMPLE"
modifier.levels = 0 # Don't do subdiv in viewport
def check_material_reuse(self) -> bool:
"""Optionally re-uses an already imported identical material."""
if not self.reuse_material:
return False
identical_mat = find_identical_material(
self.asset_data,
self.size,
self.mapping,
self.scale,
self.displacement,
self.use_16bit,
self.mode_disp
)
if identical_mat is None:
return False
# Prevent duplicate materials from being created unintentionally
# but we probably want to provide an option for that at some point.
cTB.logger.debug("POLIIGON_OT_material Applying existing material: "
f"{identical_mat.name}")
self.report({"WARNING"}, _t("Applying existing material"))
result = bpy.ops.poliigon.poliigon_apply(
"INVOKE_DEFAULT",
asset_id=self.asset_id,
name_material=identical_mat.name
)
if result == {"CANCELLED"}:
self.report(
{"WARNING"}, _t("Could not apply materials to selection"))
else:
self.signal_import()
return True
def signal_import(self) -> None:
if self.exec_count == 0:
cTB.signal_import_asset(asset_id=self.asset_id)
cTB.set_first_local_asset()
self.exec_count += 1
@reporting.handle_operator()
def execute(self, context):
self.asset_data = cTB._asset_index.get_asset(self.asset_id)
# TODO(Andreas): Can do_rename be removed, seems not in use at all?
if self.do_rename:
mat = bpy.data.materials[cTB.vActiveMat]
cTB.vActiveMat = bpy.context.scene.vEditMatName
mat.name = cTB.vActiveMat
return {"FINISHED"}
asset_type_data = self.asset_data.get_type_data()
asset_name = self.asset_data.asset_name
asset_type = self.asset_data.asset_type
workflow = asset_type_data.get_workflow("METALNESS")
self.size = asset_type_data.get_size(
self.size,
local_only=True,
addon_convention=cTB.addon_convention,
local_convention=self.asset_data.local_convention
)
self.evaluate_displacement_method_property()
objs_add_disp = self.determine_objects_needing_subdiv(context)
# TODO(Andreas): Not sure what this was supposed to be good for:
# msg_error_local = None
# msg_error_my_assets = None
# if msg_error_local is not None:
# self.report({"ERROR"}, msg_error_local)
# return {'CANCELLED'}
# if msg_error_my_assets is not None:
# cTB.print_debug_(
# 0, "apply_mat_size_not_local", asset_name, self.size)
# self.report({"ERROR"}, msg_error_my_assets)
# reporting.capture_message("apply_mat_size_not_local", asset_name)
# return {"CANCELLED"}
cTB.logger.debug(f"POLIIGON_OT_material Size: {self.size}")
did_reuse = self.check_material_reuse()
if did_reuse:
self.signal_import()
return {"FINISHED"}
tex_maps = asset_type_data.get_maps(
workflow=workflow,
size=self.size,
prefer_16_bit=self.use_16bit,
variant=None)
if len(tex_maps) == 0:
self.report({"WARNING"}, _t("No Textures found."))
reporting.capture_message("apply_mat_tex_not_found", asset_name)
return {"CANCELLED"}
if self.ignore_map_prefs:
map_prefs = None
else:
map_prefs = cTB.user.map_preferences
mat = cTB.mat_import.import_material(
asset_data=self.asset_data,
do_apply=False,
workflow="METALNESS",
size=self.size,
lod="",
projection=self.mapping,
use_16bit=self.use_16bit,
mode_disp=self.mode_disp,
translate_x=0.0,
translate_y=0.0,
scale=self.scale,
global_rotation=0.0,
aspect_ratio=1.0,
displacement=self.displacement,
keep_unused_tex_nodes=self.keep_unused_tex_nodes,
reuse_existing=False, # we already checked for reusable mats before
map_prefs=map_prefs
)
if mat is None:
reporting.capture_message(
"could_not_create_mat", asset_name, "error")
self.report({"ERROR"}, _t("Material could not be created."))
return {"CANCELLED"}
self.add_subdiv_to_objects(objs_add_disp)
cTB.f_GetSceneAssets()
# TODO(Andreas): check what it is used for
# TODO(Andreas): Even more strange, op.poliigon_active does the
# same again...
# TODO(Andreas): Even more more strange, op.poliigon_apply then does
# this again and also calls op.poliigon_active, again?
cTB.vActiveType = asset_type.name # self.vType
cTB.active_asset_id = self.asset_id
cTB.vActiveMat = mat.name
bpy.ops.poliigon.poliigon_active(
mode="mat", asset_type=cTB.vActiveType, data=cTB.vActiveMat
)
self.signal_import()
if not self.do_apply:
return {"FINISHED"}
rtn = bpy.ops.poliigon.poliigon_apply(
"INVOKE_DEFAULT",
asset_id=self.asset_id,
name_material=mat.name
)
if rtn == {"CANCELLED"}:
self.report(
{"WARNING"}, _t("Could not apply materials to selection"))
return {"FINISHED"}