2025-07-01
This commit is contained in:
@@ -0,0 +1,407 @@
|
||||
# #### 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]
|
||||
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:
|
||||
_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)
|
||||
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:
|
||||
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"}
|
||||
Reference in New Issue
Block a user