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

2018 lines
73 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 #####
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