# Copyright (C) 2021 Victor Soupday # This file is part of CC/iC Blender Tools # # CC/iC Blender Tools 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 3 of the License, or # (at your option) any later version. # # CC/iC Blender Tools 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 CC/iC Blender Tools. If not, see . import bpy import math import os from mathutils import Vector, Color from . import imageutils, jsonutils, meshutils, materials, modifiers, wrinkle, nodeutils, params, lib, utils, vars def eval_texture_rules(tex_type): prefs = vars.prefs() if tex_type in params.TEXTURE_RULES: tex_rule = params.TEXTURE_RULES[tex_type] try: return eval(tex_rule, None, locals()) except: return False else: return True def exec_var_param(var_def, mat_cache, mat_json): try: parameters = mat_cache.parameters prop_name = var_def[0] default_value = var_def[1] func = var_def[2] args = var_def[3:] if type(default_value) is list: material_type = jsonutils.get_json(mat_json, "Material Type") if material_type == "Tra": default_value = default_value[1] else: default_value = default_value[0] exec_expression = str(default_value) if mat_json: if func == "" or func == "=": # expression is json var value json_value = jsonutils.get_material_json_var(mat_json, args[0]) if json_value is not None: exec_expression = str(json_value) elif func != "DEF" and not args: exec_expression = func + f"({default_value})" elif func != "DEF" and args: # construct eval function code func_expression = func + "(mat_cache, " first = True missing_args = False for arg in args: if not first: func_expression += ", " first = False arg_value = jsonutils.get_material_json_var(mat_json, arg) if arg_value is None: missing_args = True func_expression += str(arg_value) func_expression += ")" if not missing_args: exec_expression = func_expression exec_code = "parameters." + prop_name + " = " + exec_expression exec(exec_code, None, locals()) utils.log_info("Applying: " + exec_code) except: utils.log_error("exec_var_param(): error in expression: " + exec_code) utils.log_error(str(var_def)) def eval_input_param(input_def, mat_cache): try: parameters = mat_cache.parameters input_socket = input_def[0] func = input_def[1] args = input_def[2:] if func == "" or func == "=": # expression is mat_cache parameter exec_expression = "parameters." + args[0] else: # construct eval function code exec_expression = func + "(mat_cache," first = True for arg in args: if not first: exec_expression += ", " first = False exec_expression += "parameters." + arg exec_expression += ")" return eval(exec_expression, None, locals()) except: utils.log_error("eval_input_param(): error in expression: " + exec_expression) return None def eval_tiling_param(texture_def, mat_cache, start_index = 4): try: parameters = mat_cache.parameters func = texture_def[start_index] args = texture_def[start_index + 1:] if func == "" or func == "=": # expression is mat_cache parameter exec_expression = "parameters." + args[0] else: # construct eval function code exec_expression = func + "(mat_cache, " first = True for arg in args: if not first: exec_expression += ", " first = False exec_expression += "parameters." + arg exec_expression += ")" return eval(exec_expression, None, locals()) except: utils.log_error("eval_tiling_param(): error in expression: " + exec_expression) return None def eval_parameters_func(mat_cache, func, args, default = None): try: parameters = mat_cache.parameters # construct eval function code if func == "" or func == "=": # expression is mat_cache parameter exec_expression = "parameters." + args[0] else: # construct eval function code exec_expression = func + "(mat_cache, " first = True for arg in args: if not first: exec_expression += ", " first = False exec_expression += "parameters." + arg exec_expression += ")" return eval(exec_expression, None, locals()) except: utils.log_error("eval_parameters_func(): error in expression: " + exec_expression) return default def eval_prop(prop_name, mat_cache): try: parameters = mat_cache.parameters exec_expression = "parameters." + prop_name return eval(exec_expression, None, locals()) except: utils.log_error("eval_prop(): error in expression: " + exec_expression) return None def exec_prop(prop_name, mat_cache, value): try: parameters = mat_cache.parameters exec_expression = "parameters." + prop_name + " = " + str(value) exec(exec_expression, None, locals()) except: utils.log_error("exec_prop(): error in expression: " + exec_expression) return None def fetch_prop_defaults(obj, mat_cache, mat_json): vars.block_property_update = True shader = params.get_shader_name(mat_cache) matrix_group = params.get_shader_def(shader) if matrix_group and "vars" in matrix_group.keys(): for var_def in matrix_group["vars"]: exec_var_param(var_def, mat_cache, mat_json) if shader == "rl_hair_shader": check_legacy_hair(obj, mat_cache, mat_json) #if mat_cache.get_base_name() in vars.GAME_BASE_SKIN_NAMES: # mat_cache.parameters.default_roughness_power = 0.75 vars.block_property_update = False def check_legacy_hair(obj, mat_cache, mat_json): root_map_path = None id_map_path = None flow_map_path = None try: root_map_path = mat_json["Custom Shader"]["Image"]["Hair Root Map"]["Texture Path"] except: pass try: id_map_path = mat_json["Custom Shader"]["Image"]["Hair ID Map"]["Texture Path"] except: pass try: flow_map_path = mat_json["Custom Shader"]["Image"]["Hair Flow Map"]["Texture Path"] except: pass if not meshutils.has_vertex_color_data(obj): mat_cache.parameters.hair_vertex_color_strength = 0.0 # if hair does not have a root map or id map or flow map, then it is (probably) legacy and needs adjusting if not root_map_path and not id_map_path and not flow_map_path: mat_cache.parameters.hair_enable_color = 0.0 mat_cache.parameters.hair_vertex_color_strength = 0.0 mat_cache.parameters.hair_specular_blend = 1.0 mat_cache.parameters.hair_anisotropic_roughness = 0.05 mat_cache.parameters.hair_anisotropic_strength = 0.15 mat_cache.parameters.hair_anisotropic_strength2 = 0.15 return def apply_prop_matrix(bsdf_node, group_node, mat_cache, shader_name): matrix_group = params.get_shader_def(shader_name) if group_node and matrix_group and "inputs" in matrix_group.keys(): for input_def in matrix_group["inputs"]: socket_name = input_def[0] socket = nodeutils.input_socket(group_node, socket_name) if socket: prop_value = eval_input_param(input_def, mat_cache) if prop_value is not None: nodeutils.set_node_input_value(group_node, socket, prop_value) if bsdf_node and matrix_group and "bsdf" in matrix_group.keys(): bsdf_nodes = nodeutils.get_custom_bsdf_nodes(bsdf_node) for input_def in matrix_group["bsdf"]: socket_name = input_def[0] for n in bsdf_nodes: socket = nodeutils.input_socket(n, socket_name) if socket: prop_value = eval_input_param(input_def, mat_cache) if prop_value is not None: nodeutils.set_node_input_value(n, socket, prop_value) def apply_basic_prop_matrix(node: bpy.types.Node, mat_cache, shader_name): matrix_group = params.get_shader_def(shader_name) if matrix_group and "inputs" in matrix_group.keys(): for input_def in matrix_group["inputs"]: socket_name = input_def[0] socket = nodeutils.input_socket(node, socket_name) if socket: prop_value = eval_input_param(input_def, mat_cache) if prop_value is not None: nodeutils.set_node_input_value(node, socket, prop_value) # Prop matrix eval, parameter conversion functions # def func_iris_brightness(cc, v): prefs = vars.prefs() if cc.get_render_target() == "CYCLES": v = v * prefs.cycles_iris_brightness_b443b elif cc.get_render_target() == "EEVEE": v = v * prefs.eevee_iris_brightness_b443b return v def func_sss_skin(cc, s): prefs = vars.prefs() if cc.get_render_target() == "CYCLES": if utils.B400(): s = s * prefs.cycles_sss_skin_b443b else: s = s * prefs.cycles_sss_skin_b341 else: if utils.B420(): s = s * prefs.eevee_sss_skin_b443b else: s = s * prefs.eevee_sss_skin_b341 return s def func_sss_hair(cc, s): prefs = vars.prefs() if cc.get_render_target() == "CYCLES": if utils.B400(): s = s * prefs.cycles_sss_hair_b443b else: s = s * prefs.cycles_sss_hair_b341 else: if utils.B420(): s = s * prefs.eevee_sss_hair_b443b else: s = s * prefs.eevee_sss_hair_b341 return s def func_sss_teeth(cc, s): prefs = vars.prefs() if cc.get_render_target() == "CYCLES": if utils.B400(): s = s * prefs.cycles_sss_teeth_b443b else: s = s * prefs.cycles_sss_teeth_b341 else: if utils.B420(): s = s * prefs.eevee_sss_teeth_b443b else: s = s * prefs.eevee_sss_teeth_b341 return s def func_sss_tongue(cc, s): prefs = vars.prefs() if cc.get_render_target() == "CYCLES": if utils.B400(): s = s * prefs.cycles_sss_tongue_b443b else: s = s * prefs.cycles_sss_tongue_b341 else: if utils.B420(): s = s * prefs.eevee_sss_tongue_b443b else: s = s * prefs.eevee_sss_tongue_b341 return s def func_sss_eyes(cc, s): prefs = vars.prefs() if cc.get_render_target() == "CYCLES": if utils.B400(): s = s * prefs.cycles_sss_eyes_b443b else: s = s * prefs.cycles_sss_eyes_b341 else: if utils.B420(): s = s * prefs.eevee_sss_eyes_b443b else: s = s * prefs.eevee_sss_eyes_b341 return s def func_sss_default(cc, s): prefs = vars.prefs() if cc.get_render_target() == "CYCLES": if utils.B400(): s = s * prefs.cycles_sss_default_b443b else: s = s * prefs.cycles_sss_default_b341 else: if utils.B420(): s = s * prefs.eevee_sss_default_b443b else: s = s * prefs.eevee_sss_default_b341 return s def func_sss_falloff_saturated(cc, f, s): falloff = Color((f[0], f[1], f[2])) falloff.s *= s return [falloff.r, falloff.g, falloff.b, 1.0] def func_sss_radius_eyes_cycles(cc, r): prefs = vars.prefs() r = r * vars.EYES_SSS_RADIUS_SCALE return r def func_sss_radius_eyes_eevee(cc, r, f): prefs = vars.prefs() r = r * vars.EYES_SSS_RADIUS_SCALE return [f[0] * r, f[1] * r, f[2] * r] def func_sss_radius_hair_cycles(cc, r): prefs = vars.prefs() r = r * vars.HAIR_SSS_RADIUS_SCALE return r def func_sss_radius_hair_eevee(cc, r, f, s): prefs = vars.prefs() r = r * vars.HAIR_SSS_RADIUS_SCALE falloff = Color((f[0], f[1], f[2])) falloff.s *= s return [falloff.r * r, falloff.g * r, falloff.b * r] def func_sss_radius_teeth_eevee(cc, r, f): prefs = vars.prefs() r = r * vars.TEETH_SSS_RADIUS_SCALE return [f[0] * r, f[1] * r, f[2] * r] def func_sss_radius_tongue_eevee(cc, r, f): prefs = vars.prefs() r = r * vars.TONGUE_SSS_RADIUS_SCALE return [f[0] * r, f[1] * r, f[2] * r] def func_sss_radius_default_eevee(cc, r, f): prefs = vars.prefs() r = r * vars.DEFAULT_SSS_RADIUS_SCALE return [f[0] * r, f[1] * r, f[2] * r] def func_sss_radius_skin_cycles(cc, r): prefs = vars.prefs() r = r * vars.SKIN_SSS_RADIUS_SCALE #if utils.B400(): # r *= 2/3 return r def func_sss_radius_skin_eevee(cc, r, f, s): prefs = vars.prefs() r = r * vars.SKIN_SSS_RADIUS_SCALE falloff = Color((f[0], f[1], f[2])) falloff.s *= s return [falloff.r * r, falloff.g * r, falloff.b * r] def func_roughness_power(cc, p): prefs = vars.prefs() #if prefs.build_skin_shader_dual_spec: # return p * 1.0 #else: # return p if cc.get_render_target() == "CYCLES": if utils.B410(): return p * prefs.cycles_roughness_power_b443b else: return p * prefs.cycles_roughness_power_b341 else: if utils.B420(): return p * prefs.eevee_roughness_power_b443b else: return p * prefs.eevee_roughness_power_b341 def func_a(cc, a, b, c): return a def func_b(cc, a, b, c): return b def func_b(cc, a, b, c): return c def func_mul(cc, a, b): return a * b def func_tiling(cc, scale): return 1.0 / scale def func_emission_scale(cc, v): return v * vars.EMISSION_SCALE def func_color_bytes(cc, jc: list): return [ jc[0] / 255.0, jc[1] / 255.0, jc[2] / 255.0, 1.0 ] def func_color_bytes_linear(cc, jc: list): return utils.srgb_to_linear([ jc[0] / 255.0, jc[1] / 255.0, jc[2] / 255.0, 1.0 ]) def func_color_vector(cc, jc: list): if type(jc) == list: for i in range(0, len(jc)): jc[i] /= 255.0 return jc def func_export_byte3(cc, c): return [c[0] * 255.0, c[1] * 255.0, c[2] * 255.0] def func_export_byte3_linear(cc, c): c = utils.linear_to_srgb(c) return [c[0] * 255.0, c[1] * 255.0, c[2] * 255.0] def func_occlusion_range(cc, r, m): return utils.lerp(m, 1.0, r) def func_occlusion_strength(cc, s): return pow(s, 1.0 / 3.0) def func_occlusion_contrast(cc, v): return min(0.999, max(0.001, v)) def func_occlusion_color(cc, c): return utils.lerp_color(c, (0,0,0,1), 0.75) def func_one_minus(cc, v): return 1.0 - v def func_sqrt(cc, v): return math.sqrt(v) def func_pow_2(cc, v): return math.pow(v, 2.0) def func_sclera_brightness(cc, b): prefs = vars.prefs() if cc.get_render_target() == "CYCLES": b *= 1.0 return b def func_eye_tiling(cc, ir, ss): return (1.0 / 6.81) / (ir * ss) def func_half(cc, s): return s * 0.5 def func_third(cc, s): return s * 0.3333 def func_two_third(cc, s): return s * 0.6666 def func_divide_1000(cc, v): return v / 1000.0 def func_divide_100(cc, v): return v / 100.0 def func_divide_10(cc, v): return v / 10.0 def func_divide_200(cc, v): return v / 200.0 def func_divide_5(cc, v): return v / 5.0 def func_divide_2(cc, v): return v / 2.0 def func_mul_1000(cc, v): return v * 1000.0 def func_mul_100(cc, v): return v * 100.0 def func_mul_10(cc, v): return v * 10.0 def func_mul_5(cc, v): return v * 5.0 def func_mul_2(cc, v): return v * 2.0 def func_brightness(cc, b): """Shader brightness adjust""" if b <= 1.0: return b B = (b - 1)*4 + 1 return B def func_export_brightness(cc, B): """Shader brightness adjust""" if B <= 1.0: return B b = (B - 1)/4 + 1 return b def func_saturation(cc, s): """Shader saturation adjust""" if s <= 1.0: return s S = (s - 1)*3 + 1 return S def func_export_saturation(cc, S): """Shader saturation adjust""" if S <= 1.0: return S s = (S - 1)/3 + 1 return s def func_brightness_mod(cc, b): """Brightness adjust to be used directly in modify color BCHS""" B = (b - 1)*5 + 1 return B def func_export_brightness_mod(cc, B): """Brightness adjust to be used directly in modify color BCHS""" b = (B - 1)/5 + 1 return b def func_saturation_mod(cc, s): """Saturation adjust to be used directly in modify color BCHS""" S = (s - 1)*3 + 1 return S def func_export_saturation_mod(cc, S): """Saturation adjust to be used directly in modify color BCHS""" s = (S - 1)/3 + 1 return s def func_get_eye_depth(cc, depth): return (depth / 3.0) def func_export_eye_depth(cc, depth): return (depth) * 3.0 def func_set_eye_depth(cc, depth): return depth * 1.0 def func_set_parallax_iris_depth(cc, depth): return depth * 1.0 def func_set_parallax_pupil_scale(cc, scale): return scale * 0.6666 def func_index_f0(cc, v: list): return v[0] def func_index_f1(cc, v: list): return v[1] def func_index_f2(cc, v: list): return v[2] def func_index_b0(cc, values: list): return values[0] / 255.0 def func_index_b1(cc, values: list): return values[1] / 255.0 def func_index_b2(cc, values: list): return values[2] / 255.0 def func_export_combine_xyz(cc, x, y, z): return [x * 255.0, y * 255.0, z * 255.0] def func_normal_strength(cc, s): prefs = vars.prefs() if cc.get_render_target() == "CYCLES": if utils.B400(): s = s * prefs.cycles_normal_b443b else: s = s * prefs.cycles_normal_b341 else: if utils.B420(): s = s * prefs.eevee_normal_b443b else: s = s * prefs.eevee_normal_b341 return s def func_skin_normal_strength(cc, s): prefs = vars.prefs() if cc.get_render_target() == "CYCLES": if utils.B400(): s = s * prefs.cycles_normal_skin_b443b else: s = s * prefs.cycles_normal_skin_b341 else: if utils.B420(): s = s * prefs.eevee_normal_skin_b443b else: s = s * prefs.eevee_normal_skin_b341 return s def func_micro_normal_strength(cc, s): prefs = vars.prefs() if cc.get_render_target() == "CYCLES": if utils.B400(): s = s * prefs.cycles_micro_normal_b443b else: s = s * prefs.cycles_micro_normal_b341 else: if utils.B420(): s = s * prefs.eevee_micro_normal_b443b else: s = s * prefs.eevee_micro_normal_b341 return s def func_set_occlusion_inv_contrast(cc, c): c = min(1, max(1-c, 0.01)) mc = 0.5/(c*c) return min(100, max(0.01, mc)) def func_get_occlusion_inv_contrast(cc, mc): mc = min(100, max(0.01, mc)) c = pow(0.5/mc, 0.5) return min(1, max(0, 1-c)) def func_set_occlusion_contrast(cc, c): c = min(1, max(c, 0.01)) mc = 0.5/(c*c) return min(100, max(0.01, mc)) def func_get_occlusion_contrast(cc, mc): mc = min(100, max(0.01, mc)) c = pow(0.5/mc, 0.5) return min(1, max(0, c)) def func_invert(cc, inv: bool): return 1.0 if inv else 0.0 def func_eye_invert(cc, inv: bool): return 0.0 if inv else 1.0 def func_to_bool(cc, f: float): return True if f > 0.0001 else False def func_from_bool(cc, b: bool): return 1.0 if b else 0.0 # # End Prop matrix eval, parameter conversion functions def set_image_node_tiling(nodes, links, node, mat_cache, texture_def, shader, shader_node, tex_json): prefs = vars.prefs() tex_type = texture_def[2] tiling_mode = "NONE" if len(texture_def) > 3: tiling_mode = texture_def[3] tiling = (1, 1, 1) offset = (0, 0, 0) rotation = (0, 0, 0) # fetch any tiling and offset from the json data (if available) if tex_json: if "Tiling" in tex_json.keys(): tiling = tex_json["Tiling"] if len(tiling) == 2: tiling.append(1) if tiling != [1,1,1]: tiling_mode = "OFFSET" if "Offset" in tex_json.keys(): offset = tex_json["Offset"] if len(offset) == 2: offset.append(0) if offset != [0,0,0]: tiling_mode = "OFFSET" elif mat_cache: for tex_mapping in mat_cache.texture_mappings: if tex_mapping: if tex_mapping.image == node.image: tiling = tex_mapping.scale offset = tex_mapping.location rotation = tex_mapping.rotation tiling_mode = "OFFSET" break # evaluate any tiling parameter from the texture def if len(texture_def) > 5: tiling_value = eval_tiling_param(texture_def, mat_cache) if tiling_value is not None: tiling = (tiling_value, tiling_value, 1) node_name = "tiling_" + shader + "_" + tex_type + "_mapping" node_label = tex_type + " Mapping" location = node.location location = (location[0] - 900, location[1] - 100) if tiling_mode == "EYE_PARALLAX": if prefs.refractive_eyes == "SSR" or mat_cache.is_eye(): tiling_mode = "CENTERED" tiling_node = None if tiling_mode == "CENTERED": node_group = lib.get_node_group("tiling_pivot_mapping") tiling_node = nodeutils.make_node_group_node(nodes, node_group, node_label, node_name) tiling_node.location = location nodeutils.set_node_input_value(tiling_node, "Tiling", tiling) nodeutils.set_node_input_value(tiling_node, "Pivot", (0.5, 0.5, 0)) nodeutils.link_nodes(links, tiling_node, "Vector", node, "Vector") nodeutils.link_nodes(links, tiling_node, "Vector", shader_node, "Iris UV") elif tiling_mode == "OFFSET": node_group = lib.get_node_group("tiling_offset_mapping") tiling_node = nodeutils.make_node_group_node(nodes, node_group, node_label, node_name) tiling_node.location = location nodeutils.set_node_input_value(tiling_node, "Tiling", tiling) nodeutils.set_node_input_value(tiling_node, "Offset", offset) nodeutils.link_nodes(links, tiling_node, "Vector", node, "Vector") elif tiling_mode == "EYE_PARALLAX": node_group = lib.get_node_group("tiling_cornea_parallax_mapping") tiling_node = nodeutils.make_node_group_node(nodes, node_group, node_label, node_name) tiling_node.location = location nodeutils.link_nodes(links, tiling_node, "Vector", node, "Vector") nodeutils.link_nodes(links, tiling_node, "Vector", shader_node, "Iris UV") shader_name = params.get_shader_name(mat_cache) shader_def = params.get_shader_def(shader_name) if tiling_node and "mapping" in shader_def.keys(): mapping_defs = shader_def["mapping"] for mapping_def in mapping_defs: if tex_type == mapping_def[0]: socket_name = mapping_def[1] nodeutils.set_node_input_value(tiling_node, socket_name, eval_tiling_param(mapping_def, mat_cache, 2)) def init_character_property_defaults(chr_cache, chr_json, only:list=None): prefs = vars.prefs() processed = [] utils.log_info("") utils.log_info("Initializing Material Property Defaults:") utils.log_info("----------------------------------------") if chr_json: utils.log_info("(Using Json Data)") else: utils.log_info("(No Json Data)") # Advanced properties for obj in chr_cache.get_cache_objects(): obj_cache = chr_cache.get_object_cache(obj) if obj_cache and not obj_cache.disabled and obj_cache.is_mesh() and obj not in processed: processed.append(obj) obj_json = jsonutils.get_object_json(chr_json, obj) utils.log_info("Object: " + obj.name + " (" + obj_cache.object_type + ")") utils.log_indent() for mat in obj.data.materials: if only and mat not in only: continue if mat and mat not in processed: processed.append(mat) mat_cache = chr_cache.get_material_cache(mat) if mat_cache and not mat_cache.user_added: mat_json = jsonutils.get_material_json(obj_json, mat) utils.log_info("Material: " + mat.name + " (" + mat_cache.material_type + ")") utils.log_indent() if mat_cache.is_eye(): cornea_mat, cornea_mat_cache = materials.get_cornea_mat(obj, mat, mat_cache) if cornea_mat: mat_json = jsonutils.get_material_json(obj_json, cornea_mat) fetch_prop_defaults(obj, mat_cache, mat_json) if chr_json is None and chr_cache.is_actor_core(): try: mat_cache.parameters.default_ao_strength = 0.4 mat_cache.parameters.default_ao_power = 1.0 mat_cache.parameters.default_specular_scale = 0.4 except: pass if mat_cache.source_name.startswith("Ga_Skin_"): try: if chr_cache.get_render_target() == "EEVEE": mat_cache.parameters.default_roughness_power = 0.5 else: mat_cache.parameters.default_roughness_power = 0.75 except: pass utils.log_recess() utils.log_recess() def set_shader_input_props(shader_def, mat_cache, socket, value): """Look up and set the properties for the shader inputs. """ for texture_def in shader_def["inputs"]: if texture_def[0] == socket: props = texture_def[2:] for prop in props: vars.block_property_update = True exec_prop(prop, mat_cache, value) vars.block_property_update = False def apply_texture_matrix(nodes, links, shader_node, mat, mat_cache, shader_name, mat_json, obj, processed_images, offset = Vector((0,0)), sub_shader = False, textures = None): if textures is None: textures = {} shader_def = params.get_shader_def(shader_name) location = shader_node.location x = location[0] - 600 + offset.x y = location[1] + 300 + offset.y c = 0 image_nodes = [] if shader_def and "textures" in shader_def.keys(): for shader_input in shader_node.inputs: for texture_def in shader_def["textures"]: socket_name = texture_def[0] if socket_name == shader_input.name: alpha_socket_name = texture_def[1] tex_type = texture_def[2] is_lib = imageutils.is_library_tex(tex_type) sample_map = len(texture_def) > 3 and texture_def[3] == "SAMPLE" # check texture rules, if we should connect this texture at all if not eval_texture_rules(tex_type): continue # there is no need to sample vertex colors for hair if there is Json Data present if mat_json and sample_map and tex_type == "HAIRVERTEXCOLOR": continue json_id = imageutils.get_image_type_json_id(tex_type) tex_json = jsonutils.get_texture_info(mat_json, json_id) tex_path = None suffix = None image_id = "(" + tex_type + ")" image_node = nodeutils.get_node_by_id(nodes, image_id) # if using json, assume if no tex_json then there is no texture in this socket # this should prevent rogue diffuse alpha channels getting set into alpha channels # (The FBX import will do this) if not is_lib and mat_json and not tex_json: continue # for user added materials, don't mess with the users textures... image = None if image_node and image_node.image and mat_cache.user_added: image = image_node.image elif tex_type == "HAIRVERTEXCOLOR" or tex_type == "WEIGHTMAP" or tex_type == "COLORID" or tex_type == "RGBMASK": image = imageutils.find_material_image(mat, tex_type, processed_images, tex_json) else: image = imageutils.find_material_image(mat, tex_type, processed_images, tex_json, mat_json) if image_node and image_node.image and image: if image != image_node.image: utils.log_info("Replacing image node image with: " + image.name) image_node.image = image try: if image and image.filepath: tex_path = image.filepath else: tex_path = tex_json["Texture Path"] suffix = os.path.splitext(os.path.basename(tex_path))[0].split("_")[-1] except: tex_path = "" suffix = "" if sample_map: # SAMPLE is a special case where the texture is sampled into a color value property: # e.g Vertex Color sampled into hair_vertex_color if image == None or len(obj.data.vertex_colors) == 0: # if there is no sample map, set it's corresponding strength properties to zero: # e.g. Vertex Color uses Vertex Color Strength with props: hair_vertex_color_strength strength_socket_name = socket_name + " Strength" nodeutils.set_node_input_value(shader_node, strength_socket_name, 0.0) set_shader_input_props(shader_def, mat_cache, strength_socket_name, 0.0) else: vars.block_property_update = True sample_prop = texture_def[4] sample_color = [image.pixels[0], image.pixels[1], image.pixels[2], 1.0] exec_prop(sample_prop, mat_cache, sample_color) nodeutils.set_node_input_value(shader_node, socket_name, sample_color) utils.log_detail(f"Sample Map Removing Image: {image}") bpy.data.images.remove(image) vars.block_property_update = False elif image: if not image_node: image_node = nodeutils.make_image_node(nodes, image, image_id) image_node.location = (x, y) y += 100 x -= 300 c += 1 if c == 3: c = 0 x += 900 y -= 700 set_image_node_tiling(nodes, links, image_node, mat_cache, texture_def, shader_name, shader_node, tex_json) # ensure bump maps are connected to the correct socket if socket_name == "Normal Map" and suffix and suffix.lower() == "bump": socket_name = "Bump Map" if socket_name: if tex_type == "ALPHA" and "_diffuse" in image.name.lower(): nodeutils.link_nodes(links, image_node, "Alpha", shader_node, socket_name) else: nodeutils.link_nodes(links, image_node, "Color", shader_node, socket_name) if alpha_socket_name: nodeutils.link_nodes(links, image_node, "Alpha", shader_node, alpha_socket_name) if image_node and image_node.image: image_nodes.append(image_node) textures[tex_type] = { "node": image_node, "image": image_node.image } # main shader post processing if not sub_shader: # remove any extra image nodes: if not mat_cache.user_added: for n in nodes: if n.type == "TEX_IMAGE" and n not in image_nodes: utils.log_info("Removing unused image node: " + n.name) nodes.remove(n) # finally disconnect bump map if normal map is also present (this is only supposed to be one, but it is possible to bug CC3 and get both): if nodeutils.has_connected_input(shader_node, "Bump Map") and nodeutils.has_connected_input(shader_node, "Normal Map"): bump_node, bump_socket = nodeutils.get_node_and_socket_connected_to_input(shader_node, "Bump Map") nodeutils.unlink_node_output(links, shader_node, "Bump Map") def connect_tearline_shader(obj_cache, obj, mat, mat_json, processed_images): props = vars.props() prefs = vars.prefs() mat_cache = props.get_material_cache(mat) nodes = mat.node_tree.nodes links = mat.node_tree.links shader_label = "Tearline Shader" shader_name = params.get_shader_name(mat_cache) shader_group = shader_name mix_shader_group = "" if mat_cache.get_render_target() == "CYCLES" and shader_name == "rl_tearline_shader": shader_group = "rl_tearline_cycles_shader" mix_shader_group = "rl_tearline_cycles_mix_shader" is_plus = (shader_name == "rl_tearline_plus_shader") bsdf, group = nodeutils.reset_shader(mat_cache, nodes, links, shader_label, shader_name, shader_group, mix_shader_group) apply_prop_matrix(bsdf, group, mat_cache, shader_name) nodeutils.clean_unused_image_nodes(nodes) materials.set_material_alpha(mat, "BLEND", refraction=is_plus, shadows=False) obj.visible_shadow = False def connect_eye_occlusion_shader(obj_cache, obj, mat, mat_json, processed_images): props = vars.props() prefs = vars.prefs() mat_cache = props.get_material_cache(mat) nodes = mat.node_tree.nodes links = mat.node_tree.links shader_label = "Eye Occlusion Shader" shader_name = params.get_shader_name(mat_cache) shader_group = shader_name mix_shader_group = "" is_plus = (shader_name == "rl_eye_occlusion_plus_shader") bsdf, group = nodeutils.reset_shader(mat_cache, nodes, links, shader_label, shader_name, shader_group, mix_shader_group) apply_prop_matrix(bsdf, group, mat_cache, shader_name) nodeutils.clean_unused_image_nodes(nodes) materials.set_material_alpha(mat, "BLEND", refraction=is_plus, shadows=False) obj.visible_shadow = False obj.visible_glossy = False if bsdf: try: bsdf.inputs['IOR'].default_value = 1.0 except: ... def connect_skin_shader(chr_cache, obj_cache, obj, mat, mat_json, processed_images): props = vars.props() prefs = vars.prefs() mat_cache = props.get_material_cache(mat) nodes = mat.node_tree.nodes links = mat.node_tree.links if mat_cache.is_head(): shader_label = "Skin Head Shader" shader_name = "rl_head_shader" shader_group = "rl_head_shader" elif mat_cache.is_body(): shader_label = "Skin Body Shader" shader_name = "rl_skin_shader" shader_group = "rl_skin_shader" elif mat_cache.is_arm(): shader_label = "Skin Arm Shader" shader_name = "rl_skin_shader" shader_group = "rl_skin_shader" else: #if mat_cache.is_leg(): shader_label = "Skin Leg Shader" shader_name = "rl_skin_shader" shader_group = "rl_skin_shader" mix_shader_group = "" custom_bsdf = None if prefs.build_skin_shader_dual_spec: custom_bsdf = "rl_bsdf_dual_specular" bsdf, group = nodeutils.reset_shader(mat_cache, nodes, links, shader_label, shader_name, shader_group, mix_shader_group, custom_bsdf) nodeutils.reset_cursor() # use shader_group here instead of shader_name apply_prop_matrix(bsdf, group, mat_cache, shader_name) apply_texture_matrix(nodes, links, group, mat, mat_cache, shader_name, mat_json, obj, processed_images) if not prefs.build_limit_textures: if props.wrinkle_mode and mat_json and "Wrinkle" in mat_json.keys(): utils.log_info("Applying Wrinkle System:") apply_wrinkle_system(chr_cache, nodes, links, group, shader_name, mat, mat_cache, mat_json, obj, processed_images) utils.log_info("Cleaning up unused image nodes:") nodeutils.clean_unused_image_nodes(nodes) fix_sss_method(bsdf, is_skin=True) if not utils.B420(): mat.use_sss_translucency = True materials.set_material_alpha(mat, "OPAQUE") add_displacement(obj, mat, mat_json, 2, 0) def connect_tongue_shader(obj_cache, obj, mat, mat_json, processed_images): props = vars.props() prefs = vars.prefs() mat_cache = props.get_material_cache(mat) nodes = mat.node_tree.nodes links = mat.node_tree.links shader_label = "Tongue Shader" shader_name = "rl_tongue_shader" shader_group = "rl_tongue_shader" mix_shader_group = "" bsdf, group = nodeutils.reset_shader(mat_cache, nodes, links, shader_label, shader_name, shader_group, mix_shader_group) apply_prop_matrix(bsdf, group, mat_cache, shader_name) apply_texture_matrix(nodes, links, group, mat, mat_cache, shader_name, mat_json, obj, processed_images) nodeutils.clean_unused_image_nodes(nodes) fix_sss_method(bsdf) materials.set_material_alpha(mat, "OPAQUE") if not utils.B420(): mat.use_sss_translucency = True def connect_teeth_shader(obj_cache, obj, mat, mat_json, processed_images): props = vars.props() prefs = vars.prefs() mat_cache = props.get_material_cache(mat) nodes = mat.node_tree.nodes links = mat.node_tree.links shader_label = "Teeth Shader" shader_name = "rl_teeth_shader" shader_group = "rl_teeth_shader" mix_shader_group = "" bsdf, group = nodeutils.reset_shader(mat_cache, nodes, links, shader_label, shader_name, shader_group, mix_shader_group) apply_prop_matrix(bsdf, group, mat_cache, shader_name) apply_texture_matrix(nodes, links, group, mat, mat_cache, shader_name, mat_json, obj, processed_images) if mat_cache.is_upper_teeth(): nodeutils.set_node_input_value(group, "Is Upper Teeth", 1.0) else: nodeutils.set_node_input_value(group, "Is Upper Teeth", 0.0) nodeutils.clean_unused_image_nodes(nodes) fix_sss_method(bsdf) materials.set_material_alpha(mat, "OPAQUE") if not utils.B420(): mat.use_sss_translucency = True def connect_eye_shader(obj_cache, obj, mat, obj_json, mat_json, processed_images): props = vars.props() prefs = vars.prefs() mat_cache = props.get_material_cache(mat) nodes = mat.node_tree.nodes links = mat.node_tree.links # there is no need to set up the eye_L/R materials for parallax eyes if mat_cache.is_eye() and prefs.refractive_eyes == "PARALLAX": return # to build eye materials we need some textures from the cornea: cornea_mat = mat cornea_mat_cache = mat_cache cornea_json = mat_json connect_as_pbr = False if mat_cache.is_eye(): connect_as_pbr = True if prefs.refractive_eyes == "SSR": cornea_mat, cornea_mat_cache = materials.get_cornea_mat(obj, mat, mat_cache) if cornea_mat: cornea_json = jsonutils.get_material_json(obj_json, cornea_mat) # for SSR eyes, use the textures and settings from the cornea material, if available connect_as_pbr = False if connect_as_pbr: connect_pbr_shader(obj_cache, obj, mat, mat_json, processed_images) return mix_shader_group = "" if mat_cache.is_cornea(): if prefs.refractive_eyes == "SSR": shader_label = "Cornea Shader" shader_name = "rl_cornea_shader" shader_group = "rl_cornea_refractive_shader" else: shader_label = "Cornea Shader" shader_name = "rl_cornea_shader" shader_group = "rl_cornea_parallax_shader" else: if prefs.refractive_eyes == "SSR": shader_label = "Eye Shader" shader_name = "rl_eye_shader" shader_group = "rl_eye_refractive_shader" else: shader_label = "Eye Shader" shader_name = "rl_eye_shader" # TODO rl_eye_pbr_shader??? shader_group = "rl_eye_refractive_shader" bsdf, group = nodeutils.reset_shader(mat_cache, nodes, links, shader_label, shader_name, shader_group, mix_shader_group) apply_prop_matrix(bsdf, group, mat_cache, shader_name) apply_texture_matrix(nodes, links, group, cornea_mat, cornea_mat_cache, shader_name, cornea_json, obj, processed_images) nodeutils.clean_unused_image_nodes(nodes) fix_sss_method(bsdf, is_eyes=True) obj.visible_shadow = False if not utils.B420(): mat.use_sss_translucency = True if mat_cache.is_cornea(): if prefs.refractive_eyes == "SSR": materials.set_material_alpha(mat, "OPAQUE", refraction=True, depth=mat_cache.parameters.eye_refraction_depth / 1000) else: materials.set_material_alpha(mat, "OPAQUE", refraction=False) else: materials.set_material_alpha(mat, "OPAQUE", refraction=False) def connect_hair_shader(obj_cache, obj, mat, mat_json, processed_images): props = vars.props() prefs = vars.prefs() mat_cache = props.get_material_cache(mat) nodes = mat.node_tree.nodes links = mat.node_tree.links shader_label = "Hair Shader" shader_name = "rl_hair_shader" shader_group = "rl_hair_shader" mix_shader_group = "" if mat_cache.get_render_target() == "CYCLES": shader_group = "rl_hair_cycles_shader" bsdf, group = nodeutils.reset_shader(mat_cache, nodes, links, shader_label, shader_name, shader_group, mix_shader_group) apply_prop_matrix(bsdf, group, mat_cache, shader_name) apply_texture_matrix(nodes, links, group, mat, mat_cache, shader_name, mat_json, obj, processed_images) nodeutils.clean_unused_image_nodes(nodes) fix_sss_method(bsdf, is_hair=True) materials.set_material_alpha(mat, "HASHED") if not utils.B420(): mat.use_sss_translucency = True def connect_pbr_shader(obj_cache, obj, mat: bpy.types.Material, mat_json, processed_images): props = vars.props() prefs = vars.prefs() mat_cache = props.get_material_cache(mat) nodes = mat.node_tree.nodes links = mat.node_tree.links shader_label = "Pbr Shader" shader_name = "rl_pbr_shader" shader_group = "rl_pbr_shader" mix_shader_group = "" bsdf, group = nodeutils.reset_shader(mat_cache, nodes, links, shader_label, shader_name, shader_group, mix_shader_group) apply_prop_matrix(bsdf, group, mat_cache, shader_name) apply_texture_matrix(nodes, links, group, mat, mat_cache, shader_name, mat_json, obj, processed_images) nodeutils.clean_unused_image_nodes(nodes) # material alpha blend settings method = materials.determine_material_alpha(obj_cache, mat_cache, mat_json) materials.set_material_alpha(mat, method) if mat_cache.is_eyelash(): nodeutils.set_node_input_value(group, "Specular Scale", 0.25) nodeutils.set_node_input_value(bsdf, "Subsurface", 0.001) fix_sss_method(bsdf, is_scalp=True) elif mat_cache.is_scalp(): nodeutils.set_node_input_value(group, "Specular Scale", 0) nodeutils.set_node_input_value(bsdf, "Subsurface", 0.01) fix_sss_method(bsdf, is_scalp=True) else: fix_sss_method(bsdf) if not mat_cache.is_eyelash(): add_displacement(obj, mat, mat_json, 2, 0) def add_displacement(obj, mat, mat_json, max_render=5, max_view=3): props = vars.props() prefs = vars.prefs() mat_cache = props.get_material_cache(mat) method = "DISPLACEMENT" if mat_cache.get_render_target() == "CYCLES" else "BOTH" texture_path, strength, level, multiplier, base = jsonutils.get_displacement_data(mat_json) if texture_path: if strength == 0 or multiplier == 0: level = 0 # add a subdivision modifer but set it to zero. # lots of clothing in CC/iC uses tesselation and displacement, but # subdividing all of it would significantly slow down blender. # so the modifiers are added, but the user must then set their levels. mod = modifiers.add_subdivision(obj, level, "Displacement_Subdiv", max_render=max_render, max_view=max_view) if mod: modifiers.move_mod_first(obj, mod) if utils.B410(): mat.displacement_method = method else: mat.cycles.displacement_method = method def connect_sss_shader(obj_cache, obj, mat, mat_json, processed_images): props = vars.props() prefs = vars.prefs() mat_cache = props.get_material_cache(mat) nodes = mat.node_tree.nodes links = mat.node_tree.links shader_label = "SSS Shader" shader_name = "rl_sss_shader" shader_group = "rl_sss_shader" mix_shader_group = "" bsdf, group = nodeutils.reset_shader(mat_cache, nodes, links, shader_label, shader_name, shader_group, mix_shader_group) apply_prop_matrix(bsdf, group, mat_cache, shader_name) apply_texture_matrix(nodes, links, group, mat, mat_cache, shader_name, mat_json, obj, processed_images) nodeutils.clean_unused_image_nodes(nodes) fix_sss_method(bsdf) if nodeutils.has_connected_input(group, "Alpha Map"): materials.set_material_alpha(mat, "HASHED") add_displacement(obj, mat, mat_json, 2, 0) def fix_sss_method(bsdf, is_skin=False, is_hair=False, is_eyes=False, is_scalp=False): prefs = vars.prefs() bsdf_nodes = nodeutils.get_custom_bsdf_nodes(bsdf) if utils.B400(): # Blender 4.0+ for bsdf in bsdf_nodes: if is_skin or is_hair or is_eyes or is_scalp: bsdf.subsurface_method = "RANDOM_WALK_SKIN" bsdf.inputs['Subsurface Scale'].default_value = 1.0 if is_hair: bsdf.inputs['Subsurface Anisotropy'].default_value = 1.0 elif is_skin: bsdf.inputs['Subsurface Anisotropy'].default_value = 0.8 elif is_eyes: bsdf.inputs['Subsurface Anisotropy'].default_value = 1.0 bsdf.inputs['Subsurface Scale'].default_value = 0.01 else: bsdf.inputs['Subsurface Anisotropy'].default_value = 0.5 else: bsdf.subsurface_method = "BURLEY" else: # Blender 3.4 - 3.6 if utils.B340(): for bsdf in bsdf_nodes: if is_skin or is_eyes or is_scalp: bsdf.subsurface_method = "RANDOM_WALK" bsdf.inputs['Subsurface Anisotropy'].default_value = 0.5 else: bsdf.subsurface_method = "BURLEY" def get_connected_textures(node: bpy.types.NodeGroup, tex_nodes: set, done=None): if done is None: done = [] for input in node.inputs: n, s = nodeutils.get_node_and_socket_connected_to_input(node, input) if n and n not in done: done.append(n) if n.type == "TEX_IMAGE": tex_nodes.add(n) get_connected_textures(n, tex_nodes, done) return tex_nodes def check_tex_count(links, shader_node, wrinkle_shader_node, max_images=32): tex_nodes = set() for node in [shader_node, wrinkle_shader_node]: tex_nodes = get_connected_textures(node, tex_nodes) active_tex_count = len(tex_nodes) if active_tex_count > max_images: if nodeutils.has_connected_input(shader_node, "Specular Map"): nodeutils.unlink_node_input(links, shader_node, "Specular Map") active_tex_count -= 1 if active_tex_count > max_images: nbs = nodeutils.get_node_input_value(shader_node, "Normal Blend Strength") if nbs < 0.01 and nodeutils.has_connected_input(shader_node, "Normal Blend Map"): nodeutils.unlink_node_input(links, shader_node, "Normal Blend Map") active_tex_count -= 1 if active_tex_count > max_images: cbs = nodeutils.get_node_input_value(shader_node, "Blend Overlay Strength") if cbs < 0.01 and nodeutils.has_connected_input(shader_node, "Blender Overlay"): nodeutils.unlink_node_input(links, shader_node, "Blender Overlay") active_tex_count -= 1 if active_tex_count > max_images: if nodeutils.has_connected_input(shader_node, "EN Map"): nodeutils.unlink_node_input(links, shader_node, "EN Map") nodeutils.unlink_node_input(links, shader_node, "EN Alpha") active_tex_count -= 1 if active_tex_count > max_images: if nodeutils.has_connected_input(shader_node, "CFULC Map"): nodeutils.unlink_node_input(links, shader_node, "CFULC Map") nodeutils.unlink_node_input(links, shader_node, "CFULC Alpha") active_tex_count -= 1 if active_tex_count > max_images: if nodeutils.has_connected_input(shader_node, "NMUIL Map"): nodeutils.unlink_node_input(links, shader_node, "NMUIL Map") nodeutils.unlink_node_input(links, shader_node, "NMUIL Alpha") active_tex_count -= 1 def apply_wrinkle_system(chr_cache, nodes, links, shader_node, main_shader_name, mat, mat_cache, mat_json, obj, processed_images, textures=None): wrinkle_shader_node = wrinkle.add_wrinkle_shader(chr_cache, links, mat, mat_json, main_shader_name, wrinkle_shader_name=wrinkle.WRINKLE_SHADER_NAME) apply_texture_matrix(nodes, links, wrinkle_shader_node, mat, mat_cache, wrinkle.WRINKLE_SHADER_NAME, mat_json, obj, processed_images, sub_shader = True, textures = textures) max_images = 32 if not utils.B420() else 40 check_tex_count(links, shader_node, wrinkle_shader_node, max_images=max_images)