Files
blender-portable-repo/scripts/addons/cc_blender_tools-2_4_0/shaders.py
T
2026-03-17 15:34:28 -06:00

1516 lines
52 KiB
Python

# Copyright (C) 2021 Victor Soupday
# This file is part of CC/iC Blender Tools <https://github.com/soupday/cc_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 <https://www.gnu.org/licenses/>.
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_rpsqrt(cc, v):
prefs = vars.prefs()
p = 1.0
if cc.get_render_target() == "CYCLES":
p = prefs.cycles_roughness_power_b443b if utils.B440() else prefs.cycles_roughness_power_b341
else:
p = prefs.eevee_roughness_power_b443b if utils.B440() else prefs.eevee_roughness_power_b341
return pow(v, p / 2)
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))
# region init_character_property_defaults
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)")
ext_eyelash = jsonutils.has_node_type(chr_json, "Eyelash")
# 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: ...
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: ...
if mat_cache.is_eyelash() and ext_eyelash:
try:
utils.log_info(f"Disabling standard eyelash ...")
mat_cache.parameters.default_opacity = 0.0
except: ...
if mat_cache.is_hair():
node_type = jsonutils.get_material_node_type(mat_json)
if node_type in ["Brow", "Beard"]:
mat_cache.parameters.hair_alpha_power = 2.0
utils.log_recess()
utils.log_recess()
# endregion
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, ext_eyelash=False):
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():
if ext_eyelash:
mat_cache.parameters.default_opacity = 0.0
nodeutils.set_node_input_value(group, "Opacity", 0.0)
nodeutils.set_node_input_value(group, "Specular Scale", 0.25)
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", 0.0)
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", 0.0)
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)