# #### 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 ##### import mathutils import os from typing import Dict, List, Optional, Tuple import bpy from .modules.poliigon_core.assets import (ASSET_TYPE_TO_CATEGORY_NAME, AssetData, AssetType, MapType, TextureMap) from .material_import_cycles_port_names import get_socket_name from .material_importer_params import MaterialImportParameters from .material_import_utils import ( create_link, create_link_sock_out, get_node_by_type, set_value) from . import material_import_utils_nodes as node_utils from . import reporting from .utils import copy_simple_property_group RENDERER_CYCLES = "Cycles" # TODO(Andreas): Keep name, but have different label? "Simple UV Mapping" # Just to look a bit cleaner. NAME_GROUP_SIMPLE_UV = ".simple_uv_mapping" # {map_type: (color space, alpha, interpolation)} # None means default, do not change MAP_TYPE_TO_CYCLES_IMG_PARAMS = { MapType.DEFAULT: ("sRGB", "STRAIGHT", "Linear"), MapType.UNKNOWN: ("sRGB", "STRAIGHT", "Linear"), MapType.ALPHA: ("Raw", "STRAIGHT", "Linear"), MapType.ALPHAMASKED: ("sRGB", "CHANNEL_PACKED", "Linear"), MapType.AO: ("Raw", "STRAIGHT", "Linear"), MapType.BUMP: ("Raw", "STRAIGHT", "Linear"), MapType.BUMP16: ("Raw", "STRAIGHT", "Linear"), MapType.COL: ("sRGB", "STRAIGHT", "Linear"), MapType.ALBEDO: ("sRGB", "STRAIGHT", "Linear"), MapType.DIFF: ("sRGB", "STRAIGHT", "Linear"), MapType.DISP: ("Raw", "STRAIGHT", "Cubic"), MapType.DISP16: ("Raw", "STRAIGHT", "Cubic"), MapType.EMISSIVE: ("sRGB", "STRAIGHT", "Linear"), MapType.ENV: ("sRGB", "STRAIGHT", "Linear"), MapType.JPG: ("sRGB", "STRAIGHT", "Linear"), MapType.FUZZ: ("sRGB", "STRAIGHT", "Linear"), MapType.GLOSS: ("Raw", "STRAIGHT", "Linear"), MapType.IDMAP: ("Raw", "STRAIGHT", "Linear"), MapType.DIRECTION: ("sRGB", "STRAIGHT", "Linear"), MapType.LIGHT: ("Raw", "STRAIGHT", "Linear"), MapType.HDR: ("Raw", "STRAIGHT", "Linear"), MapType.MASK: ("Raw", "STRAIGHT", "Linear"), MapType.METALNESS: ("Raw", "STRAIGHT", "Linear"), MapType.NRM: ("Raw", "STRAIGHT", "Linear"), MapType.NRM16: ("Raw", "STRAIGHT", "Linear"), MapType.OVERLAY: ("Raw", "STRAIGHT", "Linear"), MapType.OVERLAY16: ("sRGB", "STRAIGHT", "Linear"), MapType.REFL: ("sRGB", "STRAIGHT", "Linear"), MapType.ROUGHNESS: ("Raw", "STRAIGHT", "Linear"), MapType.SSS: ("sRGB", "STRAIGHT", "Linear"), MapType.TRANSLUCENCY: ("sRGB", "STRAIGHT", "Linear"), MapType.TRANSMISSION: ("Raw", "STRAIGHT", "Linear"), MapType.OPACITY: ("Raw", "STRAIGHT", "Linear"), MapType.NA_ORM: ("Raw", "STRAIGHT", "Linear"), } X_OFFSET_COLUMN_WIDE = 350.0 X_OFFSET_COLUMN_NARROW = 250.0 Y_OFFSET_ROW_TEX = 350.0 Y_GAP = 50.0 W_NODE_WIDE = X_OFFSET_COLUMN_WIDE - 100.0 # This list defines the sort order for texture nodes in Texture node column- # Purpose is to minimize link crossings and provide a more digestable node # layout. TEX_COLUMN_ORDER = [ MapType.COL, MapType.DIFF, MapType.ALPHAMASKED, MapType.NA_ORM, MapType.AO, MapType.FUZZ, MapType.TRANSLUCENCY, MapType.SSS, MapType.METALNESS, MapType.REFL, MapType.ROUGHNESS, MapType.GLOSS, MapType.EMISSIVE, MapType.ALPHA, MapType.MASK, MapType.OPACITY, MapType.NRM16, MapType.NRM, MapType.DISP16, MapType.DISP, MapType.BUMP16, MapType.BUMP, MapType.TRANSMISSION, # Currently unused map types MapType.OVERLAY, MapType.IDMAP, # Non-material asset map types MapType.ENV, MapType.HDR, MapType.JPG, MapType.LIGHT, ] VARIANT_TAGS = [f"VAR{idx}" for idx in range(1, 10)] class CyclesNodesSimpleUVMapping(): scale_mul: bpy.types.Node = None ar_factor: bpy.types.Node = None ar_mul: bpy.types.Node = None translate_offset: bpy.types.Node = None translate_add: bpy.types.Node = None rotate_rad: bpy.types.Node = None rotate: bpy.types.Node = None class CyclesNodesTopLevel(): bsdf_principled: bpy.types.Node = None color_mix_ao: bpy.types.Node = None displacement: bpy.types.Node = None fabric_fresnel: bpy.types.Node = None fabric_mix: bpy.types.Node = None mat_out: bpy.types.Node = None mapping: bpy.types.Node = None mosaic: bpy.types.Node = None normal: bpy.types.Node = None simple_uv_group: bpy.types.Node = None simple_uv: CyclesNodesSimpleUVMapping = CyclesNodesSimpleUVMapping() specular_invert_gloss: bpy.types.Node = None sss_multiply: bpy.types.Node = None tex: Dict[MapType, bpy.types.Node] = {} tex_alt: Dict[MapType, List[bpy.types.Node]] = {} tex_coords: bpy.types.Node = None translucency_add_shader: bpy.types.Node = None translucency_bsdf_translucent: bpy.types.Node = None translucency_bsdf_transparent: bpy.types.Node = None translucency_color_invert: bpy.types.Node = None translucency_mix_color: bpy.types.Node = None translucency_mix_shader: bpy.types.Node = None translucency_mix_translucency: bpy.types.Node = None translucency_value: bpy.types.Node = None transmission_vol_abs: bpy.types.Node = None def __init__(self): self.simple_uv = CyclesNodesSimpleUVMapping() self.tex = {} self.tex_alt = {} def get_ao_tex_node(self) -> Optional[bpy.types.Node]: """Returns the 'Image Texture' node for Ambient Occlusion channel.""" node_tex_ao = self.tex.get(MapType.AO, None) return node_tex_ao def get_color_tex_node(self) -> Optional[bpy.types.Node]: """Returns the 'Image Texture' node for Color/Diffuse channel.""" node_tex_color = self.tex.get(MapType.ALPHAMASKED, None) if node_tex_color is None: node_tex_color = self.tex.get(MapType.COL, None) if node_tex_color is None: node_tex_color = self.tex.get(MapType.DIFF, None) return node_tex_color def get_color_effective_output(self, ignore_translucency: bool = False ) -> Optional[bpy.types.NodeSocket]: """Returns the (potentially mixed) Color/Diffuse output socket.""" node_color = None if not ignore_translucency: node_color = self.translucency_mix_color name_color_out = "Result" if node_color is None: node_color = self.color_mix_ao name_color_out = "Result" if node_color is None: node_color = self.get_color_tex_node() name_color_out = "Color" if node_color is None: return None name_color_out = get_socket_name(node_color, name_color_out) return node_color.outputs[name_color_out] def get_displacement_tex_node( self, use_16bit: bool) -> Optional[bpy.types.Node]: """Returns the 'Image Texture' node for Displacement channel.""" node_tex_displacement = None if use_16bit: node_tex_displacement = self.tex.get(MapType.DISP16, None) if node_tex_displacement is None: node_tex_displacement = self.tex.get(MapType.DISP, None) # TODO(Andreas): Check, if anything else needs to be done to get # bump maps working if node_tex_displacement is None and use_16bit: node_tex_displacement = self.tex.get(MapType.BUMP16, None) if node_tex_displacement is None: node_tex_displacement = self.tex.get(MapType.BUMP, None) return node_tex_displacement def get_emission_tex_node(self) -> Optional[bpy.types.Node]: """Returns the 'Image Texture' node for Emission channel.""" node_tex_emission = self.tex.get(MapType.EMISSIVE, None) return node_tex_emission def get_fuzz_tex_node(self) -> Optional[bpy.types.Node]: """Returns the 'Image Texture' node for Fabric/Fuzz channel.""" node_tex_fuzz = self.tex.get(MapType.FUZZ, None) return node_tex_fuzz def get_gloss_tex_node(self) -> Optional[bpy.types.Node]: """Returns the 'Image Texture' node for Gloss channel.""" node_tex_gloss = self.tex.get(MapType.GLOSS, None) return node_tex_gloss def get_metalness_tex_node(self) -> Optional[bpy.types.Node]: """Returns the 'Image Texture' node for Metallness/Metallic channel.""" node_tex_metalness = self.tex.get(MapType.METALNESS, None) return node_tex_metalness def get_normal_tex_node(self, use_16bit: bool) -> Optional[bpy.types.Node]: """Returns the 'Image Texture' node for Normal channel.""" node_tex_normal = None if use_16bit: node_tex_normal = self.tex.get(MapType.NRM16, None) if node_tex_normal is None: node_tex_normal = self.tex.get(MapType.NRM, None) return node_tex_normal def get_opacity_tex_node(self) -> Optional[bpy.types.Node]: """Returns the 'Image Texture' node for Alpha/Opacity channel.""" node_tex_opacity = self.tex.get(MapType.OPACITY, None) if node_tex_opacity is None: node_tex_opacity = self.tex.get(MapType.ALPHAMASKED, None) if node_tex_opacity is None: node_tex_opacity = self.tex.get(MapType.ALPHA, None) if node_tex_opacity is None: node_tex_opacity = self.tex.get(MapType.MASK, None) return node_tex_opacity def get_opacity_effective_output(self) -> Optional[bpy.types.NodeSocket]: """Returns the effective Alpha/Opacity output socket.""" node_tex_opacity = self.get_opacity_tex_node() if node_tex_opacity is None: return None name_alpha_out = "Color" if node_tex_opacity is self.tex.get(MapType.ALPHAMASKED, None): name_alpha_out = "Alpha" name_alpha_out = get_socket_name(node_tex_opacity, name_alpha_out) return node_tex_opacity.outputs[name_alpha_out] def get_reflection_tex_node(self) -> Optional[bpy.types.Node]: """Returns the 'Image Texture' node for Reflection channel.""" node_tex_reflection = self.tex.get(MapType.REFL, None) return node_tex_reflection def get_roughness_tex_node(self) -> Optional[bpy.types.Node]: """Returns the 'Image Texture' node for Roughness channel.""" node_tex_roughness = self.tex.get(MapType.ROUGHNESS, None) return node_tex_roughness def get_roughness_effective_node( self) -> Tuple[Optional[bpy.types.Node], bool]: """Returns effective node for Roughness channel (could be gloss).""" node_tex_roughness = self.get_roughness_tex_node() is_specular = False if node_tex_roughness is None: node_tex_roughness = self.get_gloss_tex_node() is_specular = True return node_tex_roughness, is_specular def get_sss_tex_node(self) -> Optional[bpy.types.Node]: """Returns the 'Image Texture' node for SSS channel.""" node_tex_sss = self.tex.get(MapType.SSS, None) return node_tex_sss def get_transmission_tex_node(self) -> Optional[bpy.types.Node]: """Returns the 'Image Texture' node for Transmission channel.""" node_tex_transmission = self.tex.get(MapType.TRANSMISSION, None) return node_tex_transmission def get_translucency_tex_node(self) -> Optional[bpy.types.Node]: """Returns the 'Image Texture' node for Translucency channel.""" node_tex_translucency = self.tex.get(MapType.TRANSLUCENCY, None) return node_tex_translucency class CyclesMaterial(): def __init__(self): self.init(asset_data=None, params=None) def init(self, asset_data: AssetData, params: MaterialImportParameters ) -> None: self.asset_data = asset_data self.params = params self.tex_maps = None self.nodes = CyclesNodesTopLevel() self.mat = None self.error_missing_colorspace = [] def _try_to_assign_non_color_space(self, image: bpy.types.Image) -> None: """Tries to assign a non-color/raw color space to an image.""" # Note: Changed order compared to old P4B, since Mateusz's # setups all use Raw NON_COLOR_SPACES = ["Raw", "Linear", "Blender Linear", "Non-Color", "Non-Colour Data", "Generic Data", # From docs: https://docs.blender.org/api/current/bpy.types.ColorManagedInputColorspaceSettings.html#bpy.types.ColorManagedInputColorspaceSettings # Nevertheless I doubt, the next two would ever be # regular values "NONE", None ] found_color_space = False for color_space_name in NON_COLOR_SPACES: try: image.colorspace_settings.name = color_space_name except TypeError: continue found_color_space = True break if found_color_space: return # success self.error_missing_colorspace.append(image.name) colorspace_settings = type( image).bl_rna.properties["colorspace_settings"] colorspace_properties = colorspace_settings.fixed_type.properties spaces_avail = colorspace_properties["name"].enum_items.keys() msg = ( f"No non-color colorspace found - " f"image: {image.name}, " f"spaces: {spaces_avail}" ) reporting.capture_message( "build_mat_error_colorspace", msg, "error") def _try_to_assign_color_space( self, image: bpy.types.Image, colorspace_desired: str) -> None: """Tries to assign a color space to an image.""" colorspace_settings = type( image).bl_rna.properties["colorspace_settings"] colorspace_properties = colorspace_settings.fixed_type.properties spaces_avail = colorspace_properties["name"].enum_items.keys() colorspace_to_use = None for _colorspace in spaces_avail: if _colorspace == colorspace_desired: colorspace_to_use = colorspace_desired break try: if colorspace_to_use is None: raise TypeError image.colorspace_settings.name = colorspace_to_use except TypeError: self.error_missing_colorspace.append(image.name) msg = ( f"No {colorspace_desired} colorspace found - " f"image: {image.name}, " f"spaces: {spaces_avail}" ) reporting.capture_message( "build_mat_error_colorspace", msg, "error") def configure_tex_node_image( self, node: bpy.types.Node, tex_map: TextureMap) -> None: """Configures the image referenced by a texture node.""" path_tex = tex_map.get_path() filename_tex = os.path.basename(path_tex) name_tex = os.path.splitext(filename_tex)[0] if tex_map.size not in name_tex: # Convention 1 tex maps have no size in filename, # but we need unique image names for consecutive imports with # different sizes to work correctly name_tex += f"_{tex_map.size}" file_format = tex_map.file_format[1:].lower() name_tex += f"_{file_format}" if name_tex in bpy.data.images.keys(): image = bpy.data.images[name_tex] else: path_tex_norm = os.path.normpath(path_tex) image = bpy.data.images.load(path_tex_norm) image.name = name_tex map_type_effective = tex_map.map_type.get_effective() colorspace, alpha, interpolation = MAP_TYPE_TO_CYCLES_IMG_PARAMS[ map_type_effective] if colorspace == "Raw": self._try_to_assign_non_color_space(image) else: self._try_to_assign_color_space(image, colorspace) image.alpha_mode = alpha node.image = image node.interpolation = interpolation def simple_uv_group_set_defaults(self, node_group: bpy.types.Node) -> None: """Sets default values to the paramter node sockets of Poliigon's Simple UV Mapping node group. """ set_value( node=node_group, sock_name="Scale", sock_bl_idname_expected="NodeSocketFloat", value=self.params.scale) if bpy.app.version >= (4, 0): # TODO(Andreas): Docs say NodeSocketFloatAngle exists... # We'd like to have a degree slider... socket_type_angle = "NodeSocketFloat" else: socket_type_angle = "NodeSocketFloatAngle" set_value( node=node_group, sock_name="Rotation", sock_bl_idname_expected=socket_type_angle, value=self.params.global_rotation) set_value( node=node_group, sock_name="Translate X", sock_bl_idname_expected="NodeSocketFloat", value=self.params.translate_x) set_value( node=node_group, sock_name="Translate Y", sock_bl_idname_expected="NodeSocketFloat", value=self.params.translate_y) set_value( node=node_group, sock_name="Aspect Ratio", sock_bl_idname_expected="NodeSocketFloat", value=self.params.aspect_ratio) def prepare_simple_uv_group_node( self, parent_frame: bpy.types.Node) -> bpy.types.Node: node_group = node_utils.create_group_node( group=self.mat, parent=parent_frame, name=NAME_GROUP_SIMPLE_UV, width=W_NODE_WIDE ) node_utils.create_node_socket(node_group, socket_type="NodeSocketVector", in_out="INPUT", name="UV") node_utils.create_node_socket(node_group, socket_type="NodeSocketVector", in_out="OUTPUT", name="UV") node_utils.create_node_socket(node_group, socket_type="NodeSocketFloat", in_out="INPUT", name="Scale") if bpy.app.version >= (4, 0): # TODO(Andreas): Docs say NodeSocketFloatAngle exists... # We'd like to have a degree slider... socket_type_angle = "NodeSocketFloat" else: socket_type_angle = "NodeSocketFloatAngle" node_utils.create_node_socket(node_group, socket_type=socket_type_angle, in_out="INPUT", name="Rotation") node_utils.create_node_socket(node_group, socket_type="NodeSocketFloat", in_out="INPUT", name="Translate X") node_utils.create_node_socket(node_group, socket_type="NodeSocketFloat", in_out="INPUT", name="Translate Y") node_utils.create_node_socket(node_group, socket_type="NodeSocketFloat", in_out="INPUT", name="Aspect Ratio") self.simple_uv_group_set_defaults(node_group) return node_group def reuse_simple_uv_group( self, parent_frame: bpy.types.Node) -> bpy.types.Node: """Reuses an already created node tree to create a new Simple UV Mapping node group. """ if NAME_GROUP_SIMPLE_UV not in bpy.data.node_groups.keys(): return None node_group = node_utils.create_group_node( group=self.mat, parent=parent_frame, node_tree=bpy.data.node_groups[NAME_GROUP_SIMPLE_UV], name=NAME_GROUP_SIMPLE_UV, width=W_NODE_WIDE ) self.simple_uv_group_set_defaults(node_group) self.nodes.simple_uv_group = node_group return node_group def create_simple_uv_group( self, parent_frame: Optional[bpy.types.Node] = None ) -> bpy.types.Node: """Creates Poliigon's Simple UV Mapping node group.""" node_group = self.reuse_simple_uv_group(parent_frame) if node_group is not None: return node_group node_group = self.prepare_simple_uv_group_node(parent_frame) node_group_inputs_internal = get_node_by_type( node_group, "NodeGroupInput") node_group_outputs_internal = get_node_by_type( node_group, "NodeGroupOutput") self.nodes.simple_uv_group = node_group loc_inputs = node_group_inputs_internal.location pos_x = loc_inputs[0] + X_OFFSET_COLUMN_NARROW pos_y_row_0 = loc_inputs[1] pos_y_row_1 = pos_y_row_0 - Y_OFFSET_ROW_TEX scale = [self.params.scale] * 3 node_scale = node_utils.create_vector_math_node( group=node_group, parent=None, operation="MULTIPLY", value2=scale, name="Scale (Multiply)", location=[pos_x, pos_y_row_0] ) self.nodes.simple_uv.scale_mul = node_scale node_aspect = node_utils.create_combine_xyz_node( group=node_group, parent=None, value_x=1.0, value_y=self.params.aspect_ratio, value_z=1.0, name="Aspect Ratio Factor", location=[pos_x, pos_y_row_1] ) self.nodes.simple_uv.ar_factor = node_aspect pos_x += X_OFFSET_COLUMN_NARROW node_aspect_mul = node_utils.create_vector_math_node( group=node_group, parent=None, operation="MULTIPLY", name="Aspect Ration (Multiply)", location=[pos_x, pos_y_row_0] ) self.nodes.simple_uv.ar_mul = node_aspect_mul node_translate = node_utils.create_combine_xyz_node( group=node_group, parent=None, value_x=self.params.translate_x, value_y=self.params.translate_y, value_z=0.0, name="Translation Offset", location=[pos_x, pos_y_row_1] ) self.nodes.simple_uv.translate_offset = node_translate pos_x += X_OFFSET_COLUMN_NARROW node_translate_add = node_utils.create_vector_math_node( group=node_group, parent=None, operation="ADD", name="Translation (Add)", location=[pos_x, pos_y_row_0] ) self.nodes.simple_uv.translate_add = node_translate_add if bpy.app.version >= (4, 0): node_rotation_rad = node_utils.create_math_node( group=node_group, parent=None, operation="RADIANS", use_clamp=False, value1=self.params.global_rotation, location=[pos_x, pos_y_row_1] ) self.nodes.simple_uv.rotate_rad = node_rotation_rad pos_x += X_OFFSET_COLUMN_NARROW node_rotate = node_utils.create_vector_rotate_node( group=node_group, parent=None, location=[pos_x, pos_y_row_0] ) self.nodes.simple_uv.rotate = node_rotate pos_x += X_OFFSET_COLUMN_NARROW node_group_outputs_internal.location = [pos_x, pos_y_row_0] create_link( node_tree=node_group.node_tree, node_out=node_group_inputs_internal, sock_out_name="UV", sock_out_bl_idname_expected="NodeSocketVector", node_in=node_scale, sock_in_name="A", sock_in_bl_idname_expected="NodeSocketVector", allow_index=True) # Vector Math node needs indexes create_link( node_tree=node_group.node_tree, node_out=node_group_inputs_internal, sock_out_name="Scale", sock_out_bl_idname_expected="NodeSocketFloat", node_in=node_scale, sock_in_name="B", sock_in_bl_idname_expected="NodeSocketVector", allow_index=True) # Vector Math node needs indexes) create_link( node_tree=node_group.node_tree, node_out=node_group_inputs_internal, sock_out_name="Aspect Ratio", sock_out_bl_idname_expected="NodeSocketFloat", node_in=node_aspect, sock_in_name="Y", sock_in_bl_idname_expected="NodeSocketFloat") create_link( node_tree=node_group.node_tree, node_out=node_group_inputs_internal, sock_out_name="Translate X", sock_out_bl_idname_expected="NodeSocketFloat", node_in=node_translate, sock_in_name="X", sock_in_bl_idname_expected="NodeSocketFloat") create_link( node_tree=node_group.node_tree, node_out=node_group_inputs_internal, sock_out_name="Translate Y", sock_out_bl_idname_expected="NodeSocketFloat", node_in=node_translate, sock_in_name="Y", sock_in_bl_idname_expected="NodeSocketFloat") if bpy.app.version >= (4, 0): create_link( node_tree=node_group.node_tree, node_out=node_group_inputs_internal, sock_out_name="Rotation", sock_out_bl_idname_expected="NodeSocketFloat", node_in=node_rotation_rad, sock_in_name="A", sock_in_bl_idname_expected="NodeSocketFloat", allow_index=True) # Math node needs indexes create_link( node_tree=node_group.node_tree, node_out=node_scale, sock_out_name="Vector", sock_out_bl_idname_expected="NodeSocketVector", node_in=node_aspect_mul, sock_in_name="A", sock_in_bl_idname_expected="NodeSocketVector", allow_index=True) # Vector Math node needs indexes create_link( node_tree=node_group.node_tree, node_out=node_aspect, sock_out_name="Vector", sock_out_bl_idname_expected="NodeSocketVector", node_in=node_aspect_mul, sock_in_name="B", sock_in_bl_idname_expected="NodeSocketVector", allow_index=True) # Vector Math node needs indexes create_link( node_tree=node_group.node_tree, node_out=node_aspect_mul, sock_out_name="Vector", sock_out_bl_idname_expected="NodeSocketVector", node_in=node_translate_add, sock_in_name="A", sock_in_bl_idname_expected="NodeSocketVector", allow_index=True) # Vector Math node needs indexes create_link( node_tree=node_group.node_tree, node_out=node_translate, sock_out_name="Vector", sock_out_bl_idname_expected="NodeSocketVector", node_in=node_translate_add, sock_in_name="B", sock_in_bl_idname_expected="NodeSocketVector", allow_index=True) # Vector Math node needs indexes create_link( node_tree=node_group.node_tree, node_out=node_translate_add, sock_out_name="Vector", sock_out_bl_idname_expected="NodeSocketVector", node_in=node_rotate, sock_in_name="Vector", sock_in_bl_idname_expected="NodeSocketVector") if bpy.app.version >= (4, 0): create_link( node_tree=node_group.node_tree, node_out=node_rotation_rad, sock_out_name="Value", sock_out_bl_idname_expected="NodeSocketFloat", node_in=node_rotate, sock_in_name="Angle", sock_in_bl_idname_expected="NodeSocketFloatAngle") else: create_link( node_tree=node_group.node_tree, node_out=node_group_inputs_internal, sock_out_name="Rotation", sock_out_bl_idname_expected="NodeSocketFloatAngle", # socke_type_angle node_in=node_rotate, sock_in_name="Angle", sock_in_bl_idname_expected="NodeSocketFloatAngle", allow_index=True) # Math node needs indexes create_link( node_tree=node_group.node_tree, node_out=node_rotate, sock_out_name="Vector", sock_out_bl_idname_expected="NodeSocketVector", node_in=node_group_outputs_internal, sock_in_name="UV", sock_in_bl_idname_expected="NodeSocketVector") return node_group def create_tex_node( self, tex_map: TextureMap, group: bpy.types.Node, parent: Optional[bpy.types.Node] = None, *, projection: Optional[str] = "FLAT", extension: Optional[str] = "REPEAT", location: Optional[mathutils.Vector] = None, width: Optional[float] = None, height: Optional[float] = None, hide: bool = False ) -> bpy.types.Node: """Creates an 'Image Texture' node.""" map_type_effective = tex_map.map_type.get_effective() if map_type_effective in self.nodes.tex: # If we already have a tex node for this map type, # fold all following ones (alternative tex nodes). hide = True filename_parts = tex_map.filename.split("_") variant = [var for var in VARIANT_TAGS if var in filename_parts] # For name_node we deliberately use the original map type names, # NOT the effective ones! if len(variant) > 0: name_node = f"{tex_map.map_type.name} ({variant[0]})" else: name_node = tex_map.map_type.name node_tex = node_utils.create_node( group=group, bl_idname="ShaderNodeTexImage", parent=parent, name=name_node, location=location, width=width, height=height, hide=hide ) if map_type_effective not in self.nodes.tex: self.nodes.tex[map_type_effective] = node_tex elif map_type_effective in self.nodes.tex_alt: self.nodes.tex_alt[map_type_effective].append(node_tex) else: self.nodes.tex_alt[map_type_effective] = [node_tex] self.configure_tex_node_image(node_tex, tex_map) if projection is not None: if projection in ["UV", "MOSAIC"]: node_tex.projection = "FLAT" else: node_tex.projection = projection if extension is not None: node_tex.extension = extension return node_tex def create_texture_nodes(self) -> None: """Creates 'Image Texture' nodes for all TextureMaps provided to this import. """ params = self.params asset_type = self.asset_data.asset_type asset_type_data = self.asset_data.get_type_data() self.tex_maps = asset_type_data.get_maps( workflow=params.workflow, size=params.size, lod=params.lod, prefer_16_bit=params.use_16bit, variant=params.variant, effective=True, map_preferences=params.map_prefs ) if len(self.tex_maps) == 0: raise RuntimeError("No textures") if asset_type == AssetType.MODEL: for name in [params.name_mesh, params.name_material]: (has_maps, base_map_name, tex_maps_mesh) = asset_type_data.filter_mesh_maps( asset_maps=self.tex_maps, mesh_name=name, original_material_name=params.name_material ) if not has_maps: continue self.tex_maps = tex_maps_mesh frame = node_utils.create_frame( group=self.mat, parent=None, name="Textures" ) for _tex_map in self.tex_maps: self.create_tex_node( tex_map=_tex_map, group=self.mat, parent=frame, projection=params.projection ) def connect_color(self) -> None: """Sets up and connects the Color channel.""" node_tex_ao = self.nodes.get_ao_tex_node() node_tex_color = self.nodes.get_color_tex_node() if node_tex_color is None and node_tex_ao is None: return node_bsdf = self.nodes.bsdf_principled if node_tex_ao is None: create_link( node_tree=self.mat.node_tree, node_in=node_bsdf, sock_in_name="Base Color", sock_in_bl_idname_expected="NodeSocketColor", node_out=node_tex_color, sock_out_name="Color", sock_out_bl_idname_expected="NodeSocketColor") return node_mix = node_utils.create_mix_node( group=self.mat, parent=None, data_type="RGBA", use_clamp=True, clamp_result=False, blend_type="MULTIPLY", blend_factor=0.0, name="COLOR * AO" ) self.nodes.color_mix_ao = node_mix # NOTE: Below we need to allow indexed port addressing # (-> allow_index=True), # as the Mix node in Blender 3.4 has no way of addressing the # ports by name (different type ports have identical names). :( create_link( node_tree=self.mat.node_tree, node_out=node_tex_color, sock_out_name="Color", sock_out_bl_idname_expected="NodeSocketColor", node_in=node_mix, sock_in_name="A", sock_in_bl_idname_expected="NodeSocketColor", allow_index=True) create_link( node_tree=self.mat.node_tree, node_out=node_tex_ao, sock_out_name="Color", sock_out_bl_idname_expected="NodeSocketColor", node_in=node_mix, sock_in_name="B", sock_in_bl_idname_expected="NodeSocketColor", allow_index=True) create_link( node_tree=self.mat.node_tree, node_out=node_mix, sock_out_name="Result", sock_out_bl_idname_expected="NodeSocketColor", node_in=node_bsdf, sock_in_name="Base Color", sock_in_bl_idname_expected="NodeSocketColor", allow_index=True) def connect_displacement(self) -> None: """Sets up and connects the Displacement channel.""" node_tex_displacement = self.nodes.get_displacement_tex_node( self.params.use_16bit) if node_tex_displacement is None: return if self.params.mode_disp == "BUMP": displacement_method = "BUMP" elif self.params.mode_disp == "DISP": displacement_method = "BOTH" elif self.params.mode_disp == "MICRO": displacement_method = "DISPLACEMENT" else: # self.params.mode_disp == "NORMAL" displacement_method = "BUMP" if bpy.app.version >= (4, 1): self.mat.displacement_method = displacement_method else: self.mat.cycles.displacement_method = displacement_method node_out = self.nodes.mat_out node_displacement = node_utils.create_displacement_node( group=self.mat, parent=None, midlevel=0.5, scale=self.params.displacement ) self.nodes.displacement = node_displacement create_link( node_tree=self.mat.node_tree, node_out=node_tex_displacement, sock_out_name="Color", sock_out_bl_idname_expected="NodeSocketColor", node_in=node_displacement, sock_in_name="Height", sock_in_bl_idname_expected="NodeSocketFloat") if self.params.mode_disp != "NORMAL": create_link( node_tree=self.mat.node_tree, node_out=node_displacement, sock_out_name="Displacement", sock_out_bl_idname_expected="NodeSocketVector", node_in=node_out, sock_in_name="Displacement", sock_in_bl_idname_expected="NodeSocketVector") def connect_emission(self) -> None: """Sets up and connects the Emission channel.""" node_tex_emission = self.nodes.get_emission_tex_node() if node_tex_emission is None: return node_bsdf = self.nodes.bsdf_principled if bpy.app.version >= (3, 0): # There seems no option to set emission strength in Blender < 3.0 set_value( node=node_bsdf, sock_name="Emission Strength", sock_bl_idname_expected="NodeSocketFloat", value=0.0) create_link( node_tree=self.mat.node_tree, node_out=node_tex_emission, sock_out_name="Color", sock_out_bl_idname_expected="NodeSocketColor", node_in=node_bsdf, sock_in_name="Emission Color", sock_in_bl_idname_expected="NodeSocketColor") def connect_fabric(self) -> None: """Sets up and connects the Fabric/Fuzz channel.""" node_tex_fuzz = self.nodes.get_fuzz_tex_node() if node_tex_fuzz is None: return node_bsdf = self.nodes.bsdf_principled if bpy.app.version >= (4, 0): create_link( node_tree=self.mat.node_tree, node_out=node_tex_fuzz, sock_out_name="Color", sock_out_bl_idname_expected="NodeSocketColor", node_in=node_bsdf, sock_in_name="Sheen Tint", sock_in_bl_idname_expected="NodeSocketColor") set_value( node=node_bsdf, sock_name="Sheen Weight", sock_bl_idname_expected="NodeSocketFloatFactor", value=1.0) set_value( node=node_bsdf, sock_name="Sheen Roughness", sock_bl_idname_expected="NodeSocketFloatFactor", value=0.3) else: output_color = self.nodes.get_color_effective_output() node_fresnel = node_utils.create_fresnel_node( group=self.mat, parent=None, ior=1.150 ) self.nodes.fabric_fresnel = node_fresnel node_mix = node_utils.create_mix_node( group=self.mat, parent=None, data_type="RGBA", use_clamp=True, clamp_result=False, blend_type="SCREEN", blend_factor=0.0 ) self.nodes.fabric_mix = node_mix if bpy.app.version >= (3, 2): bl_idname_expected_fresnel_factor = "NodeSocketFloat" else: bl_idname_expected_fresnel_factor = "NodeSocketFloatFactor" create_link( node_tree=self.mat.node_tree, node_out=node_fresnel, sock_out_name="Fac", sock_out_bl_idname_expected=bl_idname_expected_fresnel_factor, node_in=node_mix, sock_in_name="Factor", sock_in_bl_idname_expected="NodeSocketFloatFactor") create_link_sock_out( node_tree=self.mat.node_tree, sock_out=output_color, node_in=node_mix, sock_in_name="A", sock_in_bl_idname_expected="NodeSocketColor") create_link( node_tree=self.mat.node_tree, node_out=node_tex_fuzz, sock_out_name="Color", sock_out_bl_idname_expected="NodeSocketColor", node_in=node_mix, sock_in_name="B", sock_in_bl_idname_expected="NodeSocketColor") create_link( node_tree=self.mat.node_tree, node_out=node_mix, sock_out_name="Result", sock_out_bl_idname_expected="NodeSocketColor", node_in=node_bsdf, sock_in_name="Base Color", sock_in_bl_idname_expected="NodeSocketColor") def connect_metalness(self) -> None: """Sets up and connects the Metalness channel. This works with Metalness map as well as with Reflection map in case of Specular workflow. """ node_tex_metalness = self.nodes.get_metalness_tex_node() if node_tex_metalness is None: node_tex_metalness = self.nodes.get_reflection_tex_node() if node_tex_metalness is None: return node_bsdf = self.nodes.bsdf_principled create_link( node_tree=self.mat.node_tree, node_out=node_tex_metalness, sock_out_name="Color", sock_out_bl_idname_expected="NodeSocketColor", node_in=node_bsdf, sock_in_name="Metallic", sock_in_bl_idname_expected="NodeSocketFloatFactor") def connect_normal(self) -> None: """Sets up and connects the Normal channel.""" node_tex_normal = self.nodes.get_normal_tex_node(self.params.use_16bit) if node_tex_normal is None: return normal_strength = 1.0 node_bsdf = self.nodes.bsdf_principled node_normal = node_utils.create_normal_node( group=self.mat, parent=None, space="TANGENT", strength=normal_strength ) self.nodes.normal = node_normal create_link( node_tree=self.mat.node_tree, node_out=node_tex_normal, sock_out_name="Color", sock_out_bl_idname_expected="NodeSocketColor", node_in=node_normal, sock_in_name="Color", sock_in_bl_idname_expected="NodeSocketColor") if self.params.mode_disp == "NORMAL": create_link( node_tree=self.mat.node_tree, node_out=node_normal, sock_out_name="Normal", sock_out_bl_idname_expected="NodeSocketVector", node_in=node_bsdf, sock_in_name="Normal", sock_in_bl_idname_expected="NodeSocketVector") def connect_opacity(self) -> None: """Sets up and connects the Alpha/Opacity channel.""" output_opacity = self.nodes.get_opacity_effective_output() if output_opacity is None: return node_bsdf = self.nodes.bsdf_principled create_link_sock_out( node_tree=self.mat.node_tree, sock_out=output_opacity, node_in=node_bsdf, sock_in_name="Alpha", sock_in_bl_idname_expected="NodeSocketFloatFactor") def connect_roughness(self) -> None: """Sets up and connects the Roughness channel. This works with Roughness map as well as with Gloss map (implicitly introducing the invert node). """ (node_tex_roughness, is_specular) = self.nodes.get_roughness_effective_node() if node_tex_roughness is None: return node_bsdf = self.nodes.bsdf_principled if not is_specular: create_link( node_tree=self.mat.node_tree, node_out=node_tex_roughness, sock_out_name="Color", sock_out_bl_idname_expected="NodeSocketColor", node_in=node_bsdf, sock_in_name="Roughness", sock_in_bl_idname_expected="NodeSocketFloatFactor") else: node_gloss_invert = node_utils.create_color_invert_node( group=self.mat, parent=None, factor=1.0, name="Invert Gloss" ) self.nodes.specular_invert_gloss = node_gloss_invert create_link( node_tree=self.mat.node_tree, node_out=node_tex_roughness, sock_out_name="Color", sock_out_bl_idname_expected="NodeSocketColor", node_in=node_gloss_invert, sock_in_name="Color", sock_in_bl_idname_expected="NodeSocketColor") create_link( node_tree=self.mat.node_tree, node_out=node_gloss_invert, sock_out_name="Color", sock_out_bl_idname_expected="NodeSocketColor", node_in=node_bsdf, sock_in_name="Roughness", sock_in_bl_idname_expected="NodeSocketFloatFactor") def connect_sss(self) -> None: """Sets up and connects the SSS channel.""" node_tex_sss = self.nodes.get_sss_tex_node() if node_tex_sss is None: return node_bsdf = self.nodes.bsdf_principled if bpy.app.version >= (4, 0): create_link( node_tree=self.mat.node_tree, node_out=node_tex_sss, sock_out_name="Color", sock_out_bl_idname_expected="NodeSocketColor", node_in=node_bsdf, sock_in_name="Subsurface Radius", sock_in_bl_idname_expected="NodeSocketVector") set_value( node=node_bsdf, sock_name="Subsurface Weight", sock_bl_idname_expected="NodeSocketFloatFactor", value=1.0) set_value( node=node_bsdf, sock_name="Subsurface Scale", sock_bl_idname_expected="NodeSocketFloatDistance", value=0.1) node_bsdf.subsurface_method = "RANDOM_WALK" else: node_math = node_utils.create_math_node( group=self.mat, parent=None, operation="MULTIPLY", use_clamp=True, value1=None, value2=0.3, name="SSS Strength (Multiply)" ) self.nodes.sss_multiply = node_math create_link( node_tree=self.mat.node_tree, node_out=node_tex_sss, sock_out_name="Color", sock_out_bl_idname_expected="NodeSocketColor", node_in=node_math, sock_in_name="Value", sock_in_bl_idname_expected="NodeSocketFloat") create_link( node_tree=self.mat.node_tree, node_out=node_math, sock_out_name="Value", sock_out_bl_idname_expected="NodeSocketFloat", node_in=node_bsdf, sock_in_name="Subsurface Radius", sock_in_bl_idname_expected="NodeSocketVector") set_value( node=node_bsdf, sock_name="Subsurface", sock_bl_idname_expected="NodeSocketFloatFactor", value=1.0) output_color = self.nodes.get_color_effective_output( ignore_translucency=True) create_link_sock_out( node_tree=self.mat.node_tree, sock_out=output_color, node_in=node_bsdf, sock_in_name="Subsurface Color", sock_in_bl_idname_expected="NodeSocketColor") def connect_translucency(self) -> None: """Sets up and connects the Translucency channel.""" if self.nodes.get_sss_tex_node() is not None: # SSS supersedes Translucency and takes priority return node_tex_translucency = self.nodes.get_translucency_tex_node() if node_tex_translucency is None: return node_bsdf = self.nodes.bsdf_principled output_color = self.nodes.get_color_effective_output( ignore_translucency=True) if output_color is None: # TODO(Andreas): Need logger, here! print("Translucency workflow without diffuse map???") node_normal = self.nodes.normal output_opacity = self.nodes.get_opacity_effective_output() node_out = self.nodes.mat_out node_color_invert = node_utils.create_color_invert_node( group=self.mat, parent=None, factor=1.0, name="Inv. Transl." ) self.nodes.translucency_color_invert = node_color_invert node_value = node_utils.create_value_node( group=self.mat, parent=None, value=0.350, name="Translucency Strength" ) self.nodes.translucency_value = node_value node_transparent_bsdf = node_utils.create_transparent_bsdf_node( group=self.mat, parent=None) self.nodes.translucency_bsdf_transparent = node_transparent_bsdf node_translucent_bsdf = node_utils.create_translucent_bsdf_node( group=self.mat, parent=None) self.nodes.translucency_bsdf_translucent = node_translucent_bsdf node_add_shader = node_utils.create_add_shader_node( group=self.mat, parent=None) self.nodes.translucency_add_shader = node_add_shader node_mix_shader = node_utils.create_mix_shader_node( group=self.mat, parent=None) self.nodes.translucency_mix_shader = node_mix_shader node_mix_color = node_utils.create_mix_node( group=self.mat, parent=None, data_type="RGBA", use_clamp=True, clamp_result=False, blend_type="MULTIPLY", blend_factor=1.0, name="TRANSL. + COLOR (Multiply)" ) self.nodes.translucency_mix_color = node_mix_color node_mix_translucency = node_utils.create_mix_node( group=self.mat, parent=None, data_type="RGBA", use_clamp=True, clamp_result=False, blend_type="MULTIPLY", blend_factor=1.0, name="Transl. Mult." ) self.nodes.translucency_mix_translucency = node_mix_translucency create_link( node_tree=self.mat.node_tree, node_out=node_value, sock_out_name="Value", sock_out_bl_idname_expected="NodeSocketFloat", node_in=node_color_invert, sock_in_name="Color", sock_in_bl_idname_expected="NodeSocketColor") create_link_sock_out( node_tree=self.mat.node_tree, sock_out=output_color, node_in=node_mix_color, sock_in_name="A", sock_in_bl_idname_expected="NodeSocketColor") create_link( node_tree=self.mat.node_tree, node_out=node_color_invert, sock_out_name="Color", sock_out_bl_idname_expected="NodeSocketColor", node_in=node_mix_color, sock_in_name="B", sock_in_bl_idname_expected="NodeSocketColor") create_link( node_tree=self.mat.node_tree, node_out=node_mix_color, sock_out_name="Result", sock_out_bl_idname_expected="NodeSocketColor", node_in=node_bsdf, sock_in_name="Base Color", sock_in_bl_idname_expected="NodeSocketColor") create_link( node_tree=self.mat.node_tree, node_out=node_tex_translucency, sock_out_name="Color", sock_out_bl_idname_expected="NodeSocketColor", node_in=node_mix_translucency, sock_in_name="A", sock_in_bl_idname_expected="NodeSocketColor", allow_index=True) # Mix node needs indexes create_link( node_tree=self.mat.node_tree, node_out=node_value, sock_out_name="Value", sock_out_bl_idname_expected="NodeSocketFloat", node_in=node_mix_translucency, sock_in_name="B", sock_in_bl_idname_expected="NodeSocketColor", allow_index=True) # Mix node needs indexes create_link( node_tree=self.mat.node_tree, node_out=node_mix_translucency, sock_out_name="Result", sock_out_bl_idname_expected="NodeSocketColor", node_in=node_translucent_bsdf, sock_in_name="Color", sock_in_bl_idname_expected="NodeSocketColor") if node_normal is not None: create_link( node_tree=self.mat.node_tree, node_out=node_normal, sock_out_name="Normal", sock_out_bl_idname_expected="NodeSocketVector", node_in=node_translucent_bsdf, sock_in_name="Normal", sock_in_bl_idname_expected="NodeSocketVector") create_link( node_tree=self.mat.node_tree, node_out=node_bsdf, sock_out_name="BSDF", sock_out_bl_idname_expected="NodeSocketShader", node_in=node_add_shader, sock_in_name="A", sock_in_bl_idname_expected="NodeSocketShader", allow_index=True) create_link( node_tree=self.mat.node_tree, node_out=node_translucent_bsdf, sock_out_name="BSDF", sock_out_bl_idname_expected="NodeSocketShader", node_in=node_add_shader, sock_in_name="B", sock_in_bl_idname_expected="NodeSocketShader", allow_index=True) if output_opacity is not None: create_link_sock_out( node_tree=self.mat.node_tree, sock_out=output_opacity, node_in=node_mix_shader, sock_in_name="Fac", sock_in_bl_idname_expected="NodeSocketFloatFactor") else: # Mateusz recommended to use a white value in this case set_value( node=node_mix_shader, sock_name="Fac", sock_bl_idname_expected="NodeSocketFloatFactor", value=1.0) create_link( node_tree=self.mat.node_tree, node_out=node_transparent_bsdf, sock_out_name="BSDF", sock_out_bl_idname_expected="NodeSocketShader", node_in=node_mix_shader, sock_in_name="A", sock_in_bl_idname_expected="NodeSocketShader") create_link( node_tree=self.mat.node_tree, node_out=node_add_shader, sock_out_name="Shader", sock_out_bl_idname_expected="NodeSocketShader", node_in=node_mix_shader, sock_in_name="B", sock_in_bl_idname_expected="NodeSocketShader") create_link( node_tree=self.mat.node_tree, node_out=node_mix_shader, sock_out_name="Shader", sock_out_bl_idname_expected="NodeSocketShader", node_in=node_out, sock_in_name="Surface", sock_in_bl_idname_expected="NodeSocketShader") def connect_transmission(self) -> None: """Sets up and connects the Transmission channel.""" if self.nodes.get_sss_tex_node() is not None: # SSS takes priority over Transmission, just in case there are # assets having both maps in parallel. return node_tex_transmission = self.nodes.get_transmission_tex_node() if node_tex_transmission is None: return node_tex_color = self.nodes.get_color_tex_node() node_bsdf = self.nodes.bsdf_principled # node_out = self.nodes.mat_out node_vol_abs = node_utils.create_volume_absorption_node( group=self.mat, parent=None, density=100.0 ) self.nodes.transmission_vol_abs = node_vol_abs create_link( node_tree=self.mat.node_tree, node_out=node_tex_color, sock_out_name="Color", sock_out_bl_idname_expected="NodeSocketColor", node_in=node_vol_abs, sock_in_name="Color", sock_in_bl_idname_expected="NodeSocketColor") # TODO(SOFT-2638): could show a redo last setting for this connection # create_link( # node_tree=self.mat.node_tree, # node_out=node_vol_abs, # sock_out_name="Volume", # sock_out_bl_idname_expected="NodeSocketShader", # node_in=node_out, # sock_in_name="Volume", # sock_in_bl_idname_expected="NodeSocketShader") create_link( node_tree=self.mat.node_tree, node_out=node_tex_transmission, sock_out_name="Color", sock_out_bl_idname_expected="NodeSocketColor", node_in=node_bsdf, sock_in_name="Transmission Weight", sock_in_bl_idname_expected="NodeSocketFloatFactor") def connect_uv(self) -> None: """Sets up and connects the nodes for UV mapping.""" frame = node_utils.create_frame( group=self.mat, parent=None, name="Texture Projection/Mapping" ) node_tex_coord = node_utils.create_texture_coordinate_node( group=self.mat, parent=frame) self.nodes.tex_coords = node_tex_coord projection = self.params.projection # Create mapping node based on selected projection/mapping if projection == "MOSAIC": node_mapping = node_utils.create_mosaic_node( group=self.mat, parent=frame, width=W_NODE_WIDE, scale=self.params.scale ) name_uv_out = "UV" self.nodes.mosaic = node_mapping self.mat.node_tree.links.new( node_mapping.inputs["UV"], node_tex_coord.outputs["UV"]) elif projection == "UV": node_mapping = self.create_simple_uv_group(parent_frame=frame) name_uv_out = "UV" self.mat.node_tree.links.new( node_mapping.inputs["UV"], node_tex_coord.outputs["UV"]) else: node_mapping = node_utils.create_mapping_node( group=self.mat, parent=frame, scale=self.params.scale ) name_uv_out = "Vector" self.nodes.mapping = node_mapping self.mat.node_tree.links.new( node_mapping.inputs["Vector"], node_tex_coord.outputs["Generated"]) # Finally connect mapping node to texture UV inputs for _node in self.nodes.tex.values(): self.mat.node_tree.links.new( _node.inputs["Vector"], node_mapping.outputs[name_uv_out]) if _node.name == "COL": _node.select = True self.mat.node_tree.nodes.active = _node for _node_list in self.nodes.tex_alt.values(): for _node in _node_list: self.mat.node_tree.links.new( _node.inputs["Vector"], node_mapping.outputs[name_uv_out]) def position_node_rel_y(self, node: bpy.types.Node, node_anchor: bpy.types.Node, x: float, y_offset: float = 0.0 ) -> bool: """Positions a node in the node graph. In x-direction position is absolute (usually a column coordinate). In y-direction position is relative to node_anchor. """ if node is None: return False if node_anchor is not None: loc_node = node_anchor.location.copy() else: # Should not happen, but we'll just take the node's # original y instead (so, row location is likely wrong # from here onward). loc_node = node.location loc_node[0] = x loc_node[1] += y_offset node.location = loc_node return True def position_tex_nodes_in_rows(self, y_top: float) -> None: """Positions all 'Image Texture' nodes in node graph in y-direction. Basically the 'Image Texture' nodes are positioned in rows in a specific order defined by TEX_COLUMN_ORDER. """ y_coord = y_top for _map_type in TEX_COLUMN_ORDER: if _map_type not in self.nodes.tex: continue node_tex = self.nodes.tex[_map_type] loc_node_tex = node_tex.location loc_node_tex[1] = y_coord y_coord -= Y_OFFSET_ROW_TEX if _map_type not in self.nodes.tex_alt: continue for _node_tex in self.nodes.tex_alt[_map_type]: loc_node_tex = _node_tex.location loc_node_tex[1] = y_coord y_coord -= 100.0 def position_tex_nodes_in_column(self, x: float) -> None: """Positions all 'Image Texture' nodes in node graph in x-direction.""" for _map_type, _node_tex in self.nodes.tex.items(): loc_node_tex = _node_tex.location loc_node_tex[0] = x for _map_type, _nodes_tex_alt in self.nodes.tex_alt.items(): for _node_tex in _nodes_tex_alt: loc_node_tex = _node_tex.location loc_node_tex[0] = x def position_nodes(self) -> None: """Positions all nodes in node graph.""" node_out = self.nodes.mat_out loc_node_out = node_out.location node_bsdf = self.nodes.bsdf_principled y_top = loc_node_out[1] # First run through texture nodes and position them vertically # to be used as row anchor points. The horizontal positioning of these # nodes happens at the end, when we know the final horizontal position # of the texure column. self.position_tex_nodes_in_rows(y_top) # We'll layout the columns right to left, starting at material output # node. # Columns are numbered left to right, beginning at zero. At max there # will be 11 columns (including material output node). If a column is # not used at all, it collapses into nothingness. x_column = loc_node_out[0] - X_OFFSET_COLUMN_NARROW # Column 10 is fixed: The material output node # Column 9 (only for Translucency) column_populated = self.position_node_rel_y( self.nodes.translucency_mix_shader, node_out, x=x_column) # Column 8 (only for Translucency or Transmission) if column_populated: x_column -= X_OFFSET_COLUMN_NARROW column_populated = self.position_node_rel_y( self.nodes.translucency_bsdf_transparent, node_out, x=x_column) column_populated |= self.position_node_rel_y( self.nodes.translucency_add_shader, node_out, x=x_column, y_offset=-Y_OFFSET_ROW_TEX) column_populated |= self.position_node_rel_y( self.nodes.transmission_vol_abs, node_out, x=x_column, y_offset=-2 * Y_OFFSET_ROW_TEX) # Column 7 (Principled BSDF and optionally Translucency) if column_populated: x_column -= X_OFFSET_COLUMN_WIDE else: # If column 8 was not populated (was a narrow one) # we need to compensate for the wider column 9 x_column -= (X_OFFSET_COLUMN_WIDE - X_OFFSET_COLUMN_NARROW) column_populated = self.position_node_rel_y( node_bsdf, node_out, x=x_column) column_populated |= self.position_node_rel_y( self.nodes.translucency_bsdf_translucent, self.nodes.get_translucency_tex_node(), x=x_column) # Column 6 (only for Fabrics) if column_populated: x_column -= X_OFFSET_COLUMN_NARROW column_populated = self.position_node_rel_y( self.nodes.fabric_mix, node_bsdf, x=x_column) # Column 5 (only for Translucency) if column_populated: x_column -= X_OFFSET_COLUMN_NARROW column_populated = self.position_node_rel_y( self.nodes.translucency_mix_color, self.nodes.get_color_tex_node(), x=x_column) column_populated |= self.position_node_rel_y( self.nodes.translucency_mix_translucency, self.nodes.get_translucency_tex_node(), x=x_column) # Column 4 (only for Translucency) if column_populated: x_column -= X_OFFSET_COLUMN_NARROW self.position_node_rel_y( self.nodes.translucency_color_invert, self.nodes.get_translucency_tex_node(), x=x_column) # Column 3 if column_populated: x_column -= X_OFFSET_COLUMN_NARROW column_populated = self.position_node_rel_y( self.nodes.color_mix_ao, self.nodes.get_color_tex_node(), x=x_column) column_populated |= self.position_node_rel_y( self.nodes.displacement, self.nodes.get_displacement_tex_node(self.params.use_16bit), x=x_column) column_populated |= self.position_node_rel_y( self.nodes.specular_invert_gloss, self.nodes.get_gloss_tex_node(), x=x_column) column_populated |= self.position_node_rel_y( self.nodes.normal, self.nodes.get_normal_tex_node(self.params.use_16bit), x=x_column) column_populated |= self.position_node_rel_y( self.nodes.sss_multiply, self.nodes.get_sss_tex_node(), x=x_column) column_populated |= self.position_node_rel_y( self.nodes.translucency_value, self.nodes.get_translucency_tex_node(), x=x_column) column_populated |= self.position_node_rel_y( self.nodes.fabric_fresnel, self.nodes.get_fuzz_tex_node(), x=x_column) # Column 2 (Static column, Texture nodes) if column_populated: x_column -= X_OFFSET_COLUMN_WIDE self.position_tex_nodes_in_column(x_column) # Column 1 (Default mapping node, Mosaic or Simple UV node group) x_column -= X_OFFSET_COLUMN_WIDE self.position_node_rel_y( self.nodes.mapping, self.nodes.get_color_tex_node(), x=x_column) self.position_node_rel_y( self.nodes.mosaic, self.nodes.get_color_tex_node(), x=x_column) self.position_node_rel_y( self.nodes.simple_uv_group, self.nodes.get_color_tex_node(), x=x_column) # Column 0 (Texture Coordinate node) x_column -= X_OFFSET_COLUMN_NARROW self.position_node_rel_y( self.nodes.tex_coords, self.nodes.get_color_tex_node(), x=x_column) def remove_unused_tex_nodes(self) -> None: """Optionally removes any unconnected Texture Image nodes.""" if self.params.keep_unused_tex_nodes: return for _map_type, _node in self.nodes.tex.copy().items(): linked_color_output = len(_node.outputs[0].links) > 0 linked_alpha_output = len(_node.outputs[1].links) > 0 if linked_color_output or linked_alpha_output: continue del self.nodes.tex[_map_type] self.mat.node_tree.nodes.remove(_node) for _map_type, _node_list in self.nodes.tex_alt.items(): for _node in _node_list.copy(): linked_color_output = len(_node.outputs[0].links) > 0 linked_alpha_output = len(_node.outputs[1].links) > 0 if linked_color_output or linked_alpha_output: continue _node_list.remove(_node) self.mat.node_tree.nodes.remove(_node) def configure_material(self) -> None: """Configures the freshly created material.""" self.mat.use_nodes = True self.mat.blend_method = "HASHED" @staticmethod def configure_principled_bsdf(node_bsdf: bpy.types.Node) -> None: """Configures the 'Principled BSDF' node.""" node_bsdf.distribution = "GGX" if bpy.app.version >= (4, 0): node_bsdf.subsurface_method = "RANDOM_WALK_SKIN" elif bpy.app.version >= (3, 0): node_bsdf.subsurface_method = "RANDOM_WALK_FIXED_RADIUS" else: node_bsdf.subsurface_method = "RANDOM_WALK" def create_material(self) -> bool: """Creates a new nodal material.""" name_mat = self.params.name_material mats_before = [mat for mat in bpy.data.materials] bpy.data.materials.new(name=name_mat) mats_new = [_mat for _mat in bpy.data.materials if _mat not in mats_before ] if len(mats_new) == 0: msg = "Failed to create nodal material" reporting.capture_message( "build_mat_error_create", msg, "error") return False self.mat = mats_new[0] self.configure_material() self.nodes = CyclesNodesTopLevel() node_mat_out = get_node_by_type(self.mat, "ShaderNodeOutputMaterial") if node_mat_out is None: bpy.data.materials.remove(self.mat) self.mat = None msg = "Failed to find material output node" reporting.capture_message( "build_mat_error_create", msg, "error") return False node_mat_out.select = False self.nodes.mat_out = node_mat_out node_bsdf = get_node_by_type(self.mat, "ShaderNodeBsdfPrincipled") if node_bsdf is None: bpy.data.materials.remove(self.mat) self.mat = None msg = "Failed to find principled BSDF node" reporting.capture_message( "build_mat_error_create", msg, "error") return False node_bsdf.select = False self.nodes.bsdf_principled = node_bsdf self.configure_principled_bsdf(node_bsdf) return True def set_material_properties(self) -> None: """Stores material import parameters as properties into the material, so it can be re-used for later imports. """ params = self.params asset_data = self.asset_data mat = self.mat asset_id = asset_data.asset_id asset_name = asset_data.asset_name # Need to use category name (or rather old P4B's asset type name), # here. This is needed to make reusing material work (also in old # projects). asset_type_name = ASSET_TYPE_TO_CATEGORY_NAME[asset_data.asset_type] size = params.size projection = params.projection use_16bit = params.use_16bit mode_disp = params.mode_disp scale = params.scale displacement = params.displacement mat.poliigon = f"{asset_type_name};{asset_name}" mat.poliigon_props.asset_name = asset_name mat.poliigon_props.asset_id = asset_id mat.poliigon_props.asset_type = asset_type_name mat.poliigon_props.size = size mat.poliigon_props.mapping = projection mat.poliigon_props.scale = scale mat.poliigon_props.displacement = displacement mat.poliigon_props.use_16bit = use_16bit mat.poliigon_props.mode_disp = mode_disp copy_simple_property_group( bpy.context.window_manager.polligon_map_prefs, mat.poliigon_props.map_prefs) def import_material(self, asset_data: AssetData, params: MaterialImportParameters, remove_unused_tex_nodes: bool = False ) -> Optional[bpy.types.Material]: """Executes the actual material import configured in MaterialImporter for Blender/Cycles.""" self.init(asset_data, params) result = self.create_material() if not result: return None self.create_texture_nodes() self.connect_color() self.connect_displacement() self.connect_emission() self.connect_metalness() self.connect_normal() self.connect_opacity() self.connect_roughness() self.connect_sss() self.connect_translucency() self.connect_transmission() self.connect_fabric() # Do after translucency! self.connect_uv() self.remove_unused_tex_nodes() self.position_nodes() self.set_material_properties() return self.mat