Files
blender-portable-repo/scripts/addons/poliigon-addon-blender/operators/operator_hdri.py
T
2026-03-17 14:58:51 -06:00

755 lines
33 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, Optional, Tuple
import mathutils
import os
import re
from math import pi
import bpy
from bpy.types import Operator
from bpy.props import (
BoolProperty,
EnumProperty,
FloatProperty,
IntProperty,
StringProperty,
)
import bpy.utils.previews
from ..modules.poliigon_core.assets import AssetData
from ..modules.poliigon_core.multilingual import _t
from ..toolbox import get_context
from .. import reporting
class POLIIGON_OT_hdri(Operator):
bl_idname = "poliigon.poliigon_hdri"
bl_label = _t("HDRI Import")
bl_description = _t("Import HDRI")
bl_options = {"GRAB_CURSOR", "BLOCKING", "REGISTER", "INTERNAL", "UNDO"}
def _fill_light_size_drop_down(
self, context) -> List[Tuple[str, str, str]]:
asset_data = cTB._asset_index.get_asset(self.asset_id)
asset_type_data = asset_data.get_type_data()
# Get list of locally available sizes
asset_files = {}
asset_type_data.get_files(asset_files)
asset_files = list(asset_files.keys())
# Populate dropdown items
local_exr_sizes = []
for path_asset in asset_files:
filename = os.path.basename(path_asset)
if not filename.endswith(".exr"):
continue
match_object = re.search(r"_(\d+K)[_\.]", filename)
if match_object:
local_exr_sizes.append(match_object.group(1))
# Sort by comparing integer size without "K"
local_exr_sizes.sort(key=lambda s: int(s[:-1]))
items_size = []
for size in local_exr_sizes:
# Tuple: (id, name, description, icon, enum value)
items_size.append((size, f"{size} EXR", f"{size} EXR"))
return items_size
def _fill_bg_size_drop_down(self, context) -> List[Tuple[str, str, str]]:
asset_data = cTB._asset_index.get_asset(self.asset_id)
asset_type_data = asset_data.get_type_data()
# Get list of locally available sizes
asset_files = {}
asset_type_data.get_files(asset_files)
asset_files = list(asset_files.keys())
# Populate dropdown items
local_exr_sizes = []
local_jpg_sizes = []
for path_asset in asset_files:
filename = os.path.basename(path_asset)
is_exr = filename.endswith(".exr")
is_jpg = filename.lower().endswith(".jpg")
is_jpg &= "_JPG" in filename
if not is_exr and not is_jpg:
continue
match_object = re.search(r"_(\d+K)[_\.]", filename)
if not match_object:
continue
local_size = match_object.group(1)
if is_exr:
local_exr_sizes.append(f"{local_size}_EXR")
elif is_jpg:
local_jpg_sizes.append(f"{local_size}_JPG")
local_sizes = local_exr_sizes + local_jpg_sizes
# Sort by comparing integer size without "K_JPG" or "K_EXR"
local_sizes.sort(key=lambda s: int(s[:-5]))
items_size = []
for size in local_sizes:
# Tuple: (id, name, description, icon, enum value)
label = size.replace("_", " ")
items_size.append((size, label, label))
return items_size
tooltip: StringProperty(options={"HIDDEN"}) # noqa: F821
asset_id: IntProperty(options={"HIDDEN"}) # noqa: F821
# If do_apply is set True, the sizes are ignored and set internally
do_apply: BoolProperty(options={"HIDDEN"}, default=False) # noqa: F821
size: EnumProperty(
name=_t("Light Texture"), # noqa F722
items=_fill_light_size_drop_down,
description=_t("Change size of light texture.")) # noqa F722
# This is not a pure size, but is a string like "4K_JPG"
size_bg: EnumProperty(
name=_t("Background Texture"), # noqa F722
items=_fill_bg_size_drop_down,
description=_t("Change size of background texture.")) # noqa F722
hdr_strength: FloatProperty(
name=_t("HDR Strength"), # noqa F722
description=_t("Strength of Light and Background textures"), # noqa F722
soft_min=0.0,
step=10,
default=1.0)
rotation: FloatProperty(
name=_t("Z-Rotation"), # noqa: F821
description=_t("Z-Rotation"), # noqa: F821
unit="ROTATION", # noqa: F821
soft_min=-2.0 * pi,
soft_max=2.0 * pi,
# precision needed here, otherwise Redo Last and node show different values
precision=3,
step=10,
default=0.0)
def __init__(self, *args, **kwargs):
"""Runs once per operator call before drawing occurs."""
super().__init__(*args, **kwargs)
self.exec_count = 0
self.node_output_world = None
self.node_tex_coord = None
self.node_mapping = None
self.node_tex_env_light = None
self.node_background_light = None
self.node_tex_env_bg = None
self.node_background_bg = None
self.node_mix_shader = None
self.node_light_path = None
@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
@staticmethod
def get_imported_hdri(asset_id: int) -> Optional[bpy.types.Image]:
"""Returns the imported HDRI image (if any)."""
img_hdri = None
for _img in bpy.data.images:
try:
asset_id_img = _img.poliigon_props.asset_id
if asset_id_img != asset_id:
continue
img_hdri = _img
break
except BaseException:
# skip non-Poliigon images (no props)
continue
return img_hdri
@reporting.handle_operator()
def execute(self, context):
asset_data = cTB._asset_index.get_asset(self.asset_id)
asset_type_data = asset_data.get_type_data()
asset_name = asset_data.asset_name
local_convention = asset_data.get_convention(local=True)
addon_convention = cTB.addon_convention
name_light = f"{asset_name}_Light"
name_bg = f"{asset_name}_Background"
try:
if "_" not in self.size_bg:
raise ValueError
size_bg_eff, filetype_bg = self.size_bg.split("_")
except Exception:
msg = ("POLIIGON_OT_hdri: Wrong size_bg format "
f"({self.size_bg}), expected '4K_JPG' or '1K_EXR'")
raise ValueError(msg)
cTB.logger.debug("POLIIGON_OT_hdri "
f"{asset_name}, {name_light}, {name_bg}")
existing = self.get_imported_hdri(self.asset_id)
# Whenever an HDR is loaded, it fully replaces the prior loaded
# images/resolutions. Thus, if we are "applying" an already imported
# one, we don't need to worry about resolution selection.
if not self.do_apply or not existing:
# Remove existing images to force loading this resolution.
if name_light in bpy.data.images.keys():
bpy.data.images.remove(bpy.data.images[name_light])
if name_bg in bpy.data.images.keys():
bpy.data.images.remove(bpy.data.images[name_bg])
elif self.do_apply:
if name_light in bpy.data.images.keys():
path_light = bpy.data.images[name_light].filepath
filename = os.path.basename(path_light)
match_object = re.search(r"_(\d+K)[_\.]", filename)
size_light = match_object.group(1) if match_object else cTB.settings["hdri"]
self.size = size_light
if name_bg in bpy.data.images.keys():
path_bg = bpy.data.images[name_bg].filepath
filename = os.path.basename(path_bg)
file_type = "JPG" if "_JPG" in filename else "EXR"
match_object = re.search(r"_(\d+K)[_\.]", filename)
# TODO(Andreas): should next line use cTB.settings["hdribg"] ?
size_bg = match_object.group(1) if match_object else cTB.settings["hdri"]
self.size_bg = f"{size_bg}_{file_type}"
size_bg_eff = size_bg
filetype_bg = file_type
light_exists = name_light in bpy.data.images.keys()
bg_exists = name_bg in bpy.data.images.keys()
if not light_exists or not bg_exists:
if not self.size or self.do_apply:
# Edge case that shouldn't occur as the resolution should be
# explicitly set, or just applying a local tex already,
# but fallback if needed.
self.size = cTB.settings["hdri"]
size_light = asset_type_data.get_size(
self.size,
local_only=True,
addon_convention=addon_convention,
local_convention=local_convention)
files = {}
asset_type_data.get_files(files)
files = list(files.keys())
files_tex_exr = [_file for _file in files
if size_light in os.path.basename(_file) and _file.lower().endswith(".exr")]
if len(files_tex_exr) == 0:
# TODO(Andreas): Shouldn't be needed anymore with AssetIndex
# cTB.f_GetLocalAssets() # Refresh local assets data structure.
msg = (f"Unable to locate image {name_light} with size {size_light}, "
f"try downloading {asset_name} again.")
reporting.capture_message(
"failed_load_light_hdri", msg, "error")
msg = _t(
"Unable to locate image {0} with size {1}, try downloading {2} again."
).format(name_light, size_light, asset_name)
self.report({"ERROR"}, msg)
return {"CANCELLED"}
file_tex_light = files_tex_exr[0]
if filetype_bg == "JPG":
size_bg = asset_type_data.get_size(
size_bg_eff,
local_only=True,
addon_convention=addon_convention,
local_convention=local_convention)
files_tex_jpg = [_file for _file in files
if size_bg in os.path.basename(_file) and _file.lower().endswith(".jpg")]
if len(files_tex_jpg) == 0:
# TODO(Andreas): Shouldn't be needed anymore with AssetIndex
# cTB.f_GetLocalAssets() # Refresh local assets data structure.
msg = (f"Unable to locate image {name_bg} with size {size_bg} (JPG), "
f"try downloading {asset_name} again.")
reporting.capture_message(
"failed_load_bg_jpg", msg, "error")
msg = _t(
"Unable to locate image {0} with size {1} (JPG), try downloading {2} again."
).format(name_bg, size_bg, asset_name)
self.report({"ERROR"}, msg)
return {"CANCELLED"}
file_tex_bg = files_tex_jpg[0]
elif size_light != size_bg_eff:
size_bg = size_bg_eff
files_tex_exr = [_file for _file in files
if size_bg_eff in os.path.basename(_file) and _file.lower().endswith(".exr")]
if len(files_tex_exr) == 0:
# TODO(Andreas): Shouldn't be needed anymore with AssetIndex
# cTB.f_GetLocalAssets() # Refresh local assets data structure.
msg = (f"Unable to locate image {name_bg} with size {size_bg} (EXR), "
f"try downloading {asset_name} again")
reporting.capture_message(
"failed_load_bg_hdri", msg, "error")
msg = _t(
"Unable to locate image {0} with size {1} (EXR), try downloading {2} again"
).format(name_bg, size_bg, asset_name)
self.report({"ERROR"}, msg)
return {"CANCELLED"}
file_tex_bg = files_tex_exr[0]
else:
size_bg = size_light
file_tex_bg = file_tex_light
# Reset apply for Redo Last menu to work properly
self.do_apply = False
# Check if same texture is used for both lighting and background
use_simple_layout = file_tex_bg == file_tex_light
# Use same name for both light and background only when using same file (simple layout)
if use_simple_layout:
name_bg = name_light
# ...............................................................................................
if use_simple_layout:
self._simple_layout(name_light,
file_tex_light,
asset_data)
else:
self._complex_layout(name_light,
file_tex_light,
name_bg,
file_tex_bg,
asset_data)
self.set_poliigon_props_world(context, asset_data)
cTB.f_GetSceneAssets()
if self.exec_count == 0:
cTB.signal_import_asset(asset_id=self.asset_id)
cTB.set_first_local_asset()
self.exec_count += 1
self.report({"INFO"}, _t("HDRI Imported : {0}").format(asset_name))
return {"FINISHED"}
def _get_poliigon_hdri_nodes(self, nodes_world) -> dict:
"""Identify existing Poliigon HDRI nodes by their labels and types.
Returns a dictionary mapping node roles to existing nodes, or None if not found.
This allows us to preserve user-authored nodes while only replacing Poliigon ones.
"""
poliigon_nodes = {
'tex_coord': None,
'mapping': None,
'tex_env_light': None,
'tex_env_bg': None,
'background_light': None,
'background_bg': None,
'mix_shader': None,
'light_path': None,
'output_world': None
}
for node in nodes_world:
try:
# Skip invalid nodes
if not hasattr(node, 'type') or not hasattr(node, 'label'):
continue
if node.type == "TEX_COORD" and node.label == "Mapping":
poliigon_nodes['tex_coord'] = node
elif node.type == "MAPPING" and node.label == "Mapping":
poliigon_nodes['mapping'] = node
elif node.type == "TEX_ENVIRONMENT":
if node.label == "Lighting":
poliigon_nodes['tex_env_light'] = node
elif node.label == "Background":
poliigon_nodes['tex_env_bg'] = node
elif node.type == "BACKGROUND":
if node.label == "Lighting":
poliigon_nodes['background_light'] = node
elif node.label == "Background":
poliigon_nodes['background_bg'] = node
elif poliigon_nodes['background_light'] is None:
# If background_light isn't assigned yet, assign the
# first one we identify. This avoids clutter in the
# default blender world layout by reusing both nodes.
poliigon_nodes['background_light'] = node
elif node.type == "MIX_SHADER":
# Mix shader nodes created by Poliigon typically don't have custom labels
# but we only want to replace if it's connected to our setup
if self._is_poliigon_mix_shader(node):
poliigon_nodes['mix_shader'] = node
elif node.type == "LIGHT_PATH":
# Similar to mix shader - identify by connection pattern
if self._is_poliigon_light_path(node):
poliigon_nodes['light_path'] = node
elif node.type == "OUTPUT_WORLD":
poliigon_nodes['output_world'] = node
except Exception as e:
# Skip problematic nodes instead of crashing
cTB.logger.warning(f"Skipping problematic node during identification: {str(e)}")
continue
return poliigon_nodes
def _is_poliigon_mix_shader(self, node) -> bool:
"""Check if a MixShader node is part of the Poliigon HDRI setup."""
try:
# Validate node has required attributes
if not hasattr(node, 'inputs') or len(node.inputs) < 3:
return False
# Check if it's connected to background nodes with Poliigon labels
input_1 = node.inputs[1]
input_2 = node.inputs[2]
if hasattr(input_1, 'is_linked') and input_1.is_linked:
if hasattr(input_1, 'links') and input_1.links:
linked_node = input_1.links[0].from_node
if (hasattr(linked_node, 'type') and linked_node.type == "BACKGROUND" and
hasattr(linked_node, 'label') and linked_node.label == "Lighting"):
return True
if hasattr(input_2, 'is_linked') and input_2.is_linked:
if hasattr(input_2, 'links') and input_2.links:
linked_node = input_2.links[0].from_node
if (hasattr(linked_node, 'type') and linked_node.type == "BACKGROUND" and
hasattr(linked_node, 'label') and linked_node.label == "Background"):
return True
except Exception:
pass
return False
def _is_poliigon_light_path(self, node) -> bool:
"""Check if a LightPath node is part of the Poliigon HDRI setup."""
try:
# Validate node has required attributes
if not hasattr(node, 'outputs'):
return False
# Check if it's connected to a mix shader that's part of Poliigon setup
for output in node.outputs:
if hasattr(output, 'is_linked') and output.is_linked:
if hasattr(output, 'links'):
for link in output.links:
if hasattr(link, 'to_node'):
linked_node = link.to_node
if (hasattr(linked_node, 'type') and linked_node.type == "MIX_SHADER" and
self._is_poliigon_mix_shader(linked_node)):
return True
except Exception:
pass
return False
def _remove_poliigon_nodes(self, nodes_world, poliigon_nodes: dict) -> None:
"""Safely remove only the identified Poliigon HDRI nodes."""
nodes_to_remove = []
for node_role, node in poliigon_nodes.items():
if node is not None and node_role != 'output_world': # Never remove output_world
nodes_to_remove.append(node)
# Remove nodes in a separate loop to avoid modifying collection during iteration
for node in nodes_to_remove:
try:
# Additional validation before removal
if hasattr(node, 'name') and node.name in nodes_world:
nodes_world.remove(node)
except Exception as e:
cTB.logger.warning(f"Failed to remove node {getattr(node, 'name', 'unnamed')}: {str(e)}")
continue
def _create_or_reuse_output_world(self, nodes_world, poliigon_nodes: dict) -> object:
"""Create or reuse the output world node."""
output_world = poliigon_nodes.get('output_world')
# Validate that the existing output world is still valid
if output_world is not None:
try:
# Test if the node is still accessible and valid
_ = output_world.type
_ = output_world.location
return output_world
except Exception:
# Node is corrupted or no longer accessible
cTB.logger.warning("Existing output world node is corrupted, creating new one")
output_world = None
if output_world is None:
# Create new output world node
output_world = nodes_world.new("ShaderNodeOutputWorld")
output_world.location = mathutils.Vector((250, 225.0))
return output_world
def _validate_node_setup(self) -> bool:
"""Validate that all required nodes were created successfully."""
required_nodes = ['node_output_world', 'node_tex_coord', 'node_mapping', 'node_tex_env_light', 'node_background_light']
for node_attr in required_nodes:
if not hasattr(self, node_attr) or getattr(self, node_attr) is None:
cTB.logger.error(f"Required node {node_attr} was not created")
return False
return True
def _simple_layout(self,
name_light: str,
file_tex_light: str,
asset_data: AssetData
) -> None:
if not bpy.context.scene.world:
bpy.ops.world.new()
bpy.context.scene.world = bpy.data.worlds[-1]
bpy.context.scene.world.use_nodes = True
nodes_world = bpy.context.scene.world.node_tree.nodes
links_world = bpy.context.scene.world.node_tree.links
# Use improved selective node management
try:
poliigon_nodes = self._get_poliigon_hdri_nodes(nodes_world)
self._remove_poliigon_nodes(nodes_world, poliigon_nodes)
self.node_output_world = self._create_or_reuse_output_world(nodes_world, poliigon_nodes)
except Exception as e:
cTB.logger.error(f"Failed during node identification/removal: {str(e)}")
# Fallback to more aggressive approach if needed
self.node_output_world = None
for node in nodes_world:
if node.type == "OUTPUT_WORLD":
self.node_output_world = node
break
if self.node_output_world is None:
self.node_output_world = nodes_world.new("ShaderNodeOutputWorld")
self.node_output_world.location = mathutils.Vector((320, 300))
# Create new Poliigon nodes
try:
self.node_tex_coord = nodes_world.new("ShaderNodeTexCoord")
self.node_tex_coord.label = "Mapping"
self.node_tex_coord.location = mathutils.Vector((-720, 300))
self.node_mapping = nodes_world.new("ShaderNodeMapping")
self.node_mapping.label = "Mapping"
self.node_mapping.location = mathutils.Vector((-470, 300))
self.node_tex_env_light = nodes_world.new("ShaderNodeTexEnvironment")
self.node_tex_env_light.label = "Lighting"
self.node_tex_env_light.location = mathutils.Vector((-220, 300))
self.node_background_light = nodes_world.new("ShaderNodeBackground")
self.node_background_light.label = "Lighting"
self.node_background_light.location = mathutils.Vector((120, 300))
# Validate node creation
if not self._validate_node_setup():
raise Exception("Failed to create required nodes")
except Exception as e:
cTB.logger.error(f"Failed to create nodes in simple layout: {str(e)}")
raise
# Create connections with error handling
try:
links_world.new(self.node_tex_coord.outputs["Generated"], self.node_mapping.inputs["Vector"])
links_world.new(self.node_mapping.outputs["Vector"], self.node_tex_env_light.inputs["Vector"])
links_world.new(self.node_tex_env_light.outputs["Color"], self.node_background_light.inputs["Color"])
links_world.new(self.node_background_light.outputs[0], self.node_output_world.inputs["Surface"])
except Exception as e:
cTB.logger.error(f"Failed to create node links in simple layout: {str(e)}")
raise
# Load images
try:
if name_light in bpy.data.images.keys():
img_light = bpy.data.images[name_light]
else:
file_tex_light_norm = os.path.normpath(file_tex_light)
img_light = bpy.data.images.load(file_tex_light_norm)
img_light.name = name_light
img_light.poliigon = "HDRIs;" + asset_data.asset_name
self.set_poliigon_props_image(img_light, asset_data)
except Exception as e:
cTB.logger.error(f"Failed to load images in simple layout: {str(e)}")
raise
# Set node properties
try:
if "Rotation" in self.node_mapping.inputs:
self.node_mapping.inputs["Rotation"].default_value[2] = self.rotation
else:
self.node_mapping.rotation[2] = self.rotation
self.node_tex_env_light.image = img_light
self.node_background_light.inputs["Strength"].default_value = self.hdr_strength
except Exception as e:
cTB.logger.error(f"Failed to set node properties in simple layout: {str(e)}")
raise
def _complex_layout(self,
name_light: str,
file_tex_light: str,
name_bg: str,
file_tex_bg: str,
asset_data: AssetData
) -> None:
if not bpy.context.scene.world:
bpy.ops.world.new()
bpy.context.scene.world = bpy.data.worlds[-1]
bpy.context.scene.world.use_nodes = True
nodes_world = bpy.context.scene.world.node_tree.nodes
links_world = bpy.context.scene.world.node_tree.links
# Use improved selective node management
try:
poliigon_nodes = self._get_poliigon_hdri_nodes(nodes_world)
self._remove_poliigon_nodes(nodes_world, poliigon_nodes)
self.node_output_world = self._create_or_reuse_output_world(nodes_world, poliigon_nodes)
except Exception as e:
cTB.logger.error(f"Failed during node identification/removal: {str(e)}")
# Fallback to more aggressive approach if needed
self.node_output_world = None
for node in nodes_world:
if node.type == "OUTPUT_WORLD":
self.node_output_world = node
break
if self.node_output_world is None:
self.node_output_world = nodes_world.new("ShaderNodeOutputWorld")
self.node_output_world.location = mathutils.Vector((250, 225.0))
# Create all required nodes
try:
self.node_tex_coord = nodes_world.new("ShaderNodeTexCoord")
self.node_tex_coord.label = "Mapping"
self.node_tex_coord.location = mathutils.Vector((-950, 225))
self.node_mapping = nodes_world.new("ShaderNodeMapping")
self.node_mapping.label = "Mapping"
self.node_mapping.location = mathutils.Vector((-750, 225))
self.node_tex_env_light = nodes_world.new("ShaderNodeTexEnvironment")
self.node_tex_env_light.label = "Lighting"
self.node_tex_env_light.location = mathutils.Vector((-550, 350))
self.node_tex_env_bg = nodes_world.new("ShaderNodeTexEnvironment")
self.node_tex_env_bg.label = "Background"
self.node_tex_env_bg.location = mathutils.Vector((-550, 100))
self.node_background_light = nodes_world.new("ShaderNodeBackground")
self.node_background_light.label = "Lighting"
self.node_background_light.location = mathutils.Vector((-250, 100))
self.node_background_bg = nodes_world.new("ShaderNodeBackground")
self.node_background_bg.label = "Background"
self.node_background_bg.location = mathutils.Vector((-250, -50))
self.node_mix_shader = nodes_world.new("ShaderNodeMixShader")
self.node_mix_shader.location = mathutils.Vector((0, 225))
self.node_light_path = nodes_world.new("ShaderNodeLightPath")
self.node_light_path.location = mathutils.Vector((-250, 440))
# Validate critical nodes for complex layout
critical_nodes = ['node_output_world', 'node_tex_coord', 'node_mapping',
'node_tex_env_light', 'node_background_light', 'node_tex_env_bg',
'node_background_bg', 'node_mix_shader', 'node_light_path']
for node_attr in critical_nodes:
if not hasattr(self, node_attr) or getattr(self, node_attr) is None:
raise Exception(f"Required node {node_attr} was not created")
except Exception as e:
cTB.logger.error(f"Failed to create nodes in complex layout: {str(e)}")
raise
# Create all links after ensuring nodes exist
try:
links_world.new(self.node_tex_coord.outputs["Generated"], self.node_mapping.inputs["Vector"])
links_world.new(self.node_mapping.outputs["Vector"], self.node_tex_env_light.inputs["Vector"])
links_world.new(self.node_tex_env_light.outputs["Color"], self.node_background_light.inputs["Color"])
links_world.new(self.node_background_light.outputs[0], self.node_mix_shader.inputs[1])
links_world.new(self.node_mapping.outputs["Vector"], self.node_tex_env_bg.inputs["Vector"])
links_world.new(self.node_tex_env_bg.outputs["Color"], self.node_background_bg.inputs["Color"])
links_world.new(self.node_background_bg.outputs[0], self.node_mix_shader.inputs[2])
links_world.new(self.node_light_path.outputs[0], self.node_mix_shader.inputs[0])
links_world.new(self.node_mix_shader.outputs[0], self.node_output_world.inputs[0])
except Exception as e:
cTB.logger.error(f"Failed to create node links: {str(e)}")
raise
# Load images safely
try:
# Load light image
if name_light in bpy.data.images.keys():
img_light = bpy.data.images[name_light]
else:
file_tex_light_norm = os.path.normpath(file_tex_light)
img_light = bpy.data.images.load(file_tex_light_norm)
img_light.name = name_light
img_light.poliigon = "HDRIs;" + asset_data.asset_name
self.set_poliigon_props_image(img_light, asset_data)
# Load background image
if name_bg in bpy.data.images.keys():
img_bg = bpy.data.images[name_bg]
else:
file_tex_bg_norm = os.path.normpath(file_tex_bg)
img_bg = bpy.data.images.load(file_tex_bg_norm)
img_bg.name = name_bg
self.set_poliigon_props_image(img_bg, asset_data)
# Set node properties safely
if "Rotation" in self.node_mapping.inputs:
self.node_mapping.inputs["Rotation"].default_value[2] = self.rotation
else:
self.node_mapping.rotation[2] = self.rotation
self.node_tex_env_light.image = img_light
self.node_background_light.inputs["Strength"].default_value = self.hdr_strength
self.node_tex_env_bg.image = img_bg
self.node_background_bg.inputs["Strength"].default_value = self.hdr_strength
except Exception as e:
cTB.logger.error(f"Failed to load or setup images: {str(e)}")
raise
def set_poliigon_props_image(
self, img: bpy.types.Image, asset_data: AssetData) -> None:
"""Sets Poliigon property of an imported image."""
img.poliigon_props.asset_name = asset_data.asset_name
img.poliigon_props.asset_id = self.asset_id
img.poliigon_props.asset_type = asset_data.asset_type.name
img.poliigon_props.size = self.size
img.poliigon_props.size_bg = self.size_bg
img.poliigon_props.hdr_strength = self.hdr_strength
img.poliigon_props.rotation = self.rotation
def set_poliigon_props_world(self, context, asset_data: AssetData) -> None:
"""Sets Poliigon property of world."""
context_world = context.scene.world
context_world.poliigon_props.asset_name = asset_data.asset_name
context_world.poliigon_props.asset_id = self.asset_id
context_world.poliigon_props.asset_type = asset_data.asset_type.name
context_world.poliigon_props.size = self.size
context_world.poliigon_props.size_bg = self.size_bg
context_world.poliigon_props.hdr_strength = self.hdr_strength
context_world.poliigon_props.rotation = self.rotation