# This file performs the following operations # Remove unused material slots on all objects # Remap duplicate material slots on all objects # Replace all materials with templates from the KK Shader file # Remove all duplicate node groups after importing everything # Import all textures from .pmx directory # Saturates all main textures and creates dark versions of all main textures # Load all textures to correct spot on all materials # Sets up the normal smoothing geometry nodes group # Sets up drivers to make the gag eye shapekeys work correctly # Load all colors from KK_MaterialDataComplete.json to correct spot on all materials # Adds an outline modifier and outline materials to the face, body, hair and outfit meshes # Color and image saturation code taken from MediaMoots https://github.com/FlailingFog/KK-Blender-Porter-Pack/blob/ecad6a136e86aaf6c51194705157200797f91e5f/importing/importcolors.py # Dark color conversion code taken from Xukmi https://github.com/xukmi/KKShadersPlus/tree/main/Shaders import bpy, os, numpy, math, time, concurrent.futures, threading, queue from pathlib import Path from .. import common as c class modify_material(bpy.types.Operator): bl_idname = "kkbp.modifymaterial" bl_label = bl_idname bl_description = bl_idname bl_options = {'REGISTER', 'UNDO'} # numpy's precision np_number_precision = numpy.float32 # these three parameters should be exposed in the gui and decided by user # This is how many cpu cores you want to use to saturate the images. # If you have a better CPU, you can set it higher. kkbp_package_name = __package__[:__package__.rindex('.')] max_thread_num = bpy.context.preferences.addons[kkbp_package_name].preferences.max_thread_num # this is related to memory usage. # Actually it's not perfect because the size of each image varies. # If loading four 4096 * 4096, the peak memory usage could reach 16000MB. # If the user doesn't have this much available memory, the program will crash. # In that case, the user should lower the value max_image_num = bpy.context.preferences.addons[kkbp_package_name].preferences.max_image_num # this is related to cpu and memory usage. # This is the number of rows of pixels to process in one batch (images are saturated in batches). # Simply separate images in rows, ignoring that the num of column usually increase as num of rows increasing # For a 1024 * 1024, a batch is 512 * 1024.But for 2048 * 2048, a batch is 512 * 2048 batch_rows = bpy.context.preferences.addons[kkbp_package_name].preferences.batch_rows # constants for later lut_pixels = None coord_scale = None coord_offset = None texel_height_X0 = None # used to protect the data_queue queue_lock = threading.Lock() data_queue = queue.Queue() def execute(self, context): try: self.remove_unused_material_slots() self.remap_duplicate_material_slots() self.replace_materials_for_body() self.replace_materials_for_hair() self.replace_materials_for_outfits() self.replace_materials_for_tears_tongue_gageye() self.remove_duplicate_node_groups() self.load_images() self.link_textures_for_face_body() self.link_textures_for_hair() self.link_textures_for_clothes() self.link_textures_for_tongue_tear_gag() self.create_dark_textures() self.import_and_setup_smooth_normals() self.setup_gag_eye_material_drivers() self.add_outlines_to_body() self.add_outlines_to_hair() self.add_outlines_to_clothes() self.load_luts() self.load_json_colors() self.set_color_management() c.clean_orphaned_data() return {'FINISHED'} except Exception as error: c.handle_error(self, error) return {"CANCELLED"} def init_prefab_data(self): '''Initialize constants for saturating textures''' modify_material.lut_pixels = numpy.array(bpy.data.images['Lut_TimeDay.png'].pixels[:],dtype=modify_material.np_number_precision).reshape(bpy.data.images['Lut_TimeDay.png'].size[1], bpy.data.images['Lut_TimeDay.png'].size[0], 4) #constants to ensure bot and top are within the 32 x 1024 dimensions of the lut modify_material.coord_scale = numpy.array([0.0302734375, 0.96875, 31.0],dtype=modify_material.np_number_precision) modify_material.coord_offset = numpy.array([0.5 / 1024, 0.5 / 32, 0.0], dtype=modify_material.np_number_precision) modify_material.texel_height_X0 = numpy.array([1 / 32, 0], dtype=modify_material.np_number_precision) pass # %% Main functions def remove_unused_material_slots(self): '''Remove unused mat slots on all visible objects''' objects = c.get_outfits() objects.extend(c.get_alts()) objects.append(c.get_body) objects.extend(c.get_hairs()) for object in objects: try: c.switch(object, 'object') bpy.ops.object.material_slot_remove_unused() #If this is an outfit, and there's only one material slot, check if it's a duplicate material. #If it's a duplicate the object actually had no materials to begin with, but the material_slot_remove_unused function left a slot behind #This can happen if the outfit does not contain any clothes if len(object.material_slots) == 1 and object.get('outfit') and bpy.data.objects.get('Hair ' + object.name): if object.material_slots[0].material.get('id') == bpy.data.objects.get(object.name.replace('Ouftit', 'Hair Outfit')).material_slots[0].material.get('id'): c.kklog(f'No materials detected in outfit "{object.name}". Deleting...', 'warn') bpy.data.objects.remove(object) except: pass c.print_timer('remove_unused_material_slots') def remap_duplicate_material_slots(self): c.switch(c.get_body(), 'object') objects = c.get_outfits() objects.extend(c.get_alts()) objects.append(c.get_body()) objects.extend(c.get_hairs()) for obj in objects: c.switch(obj, 'object') bpy.ops.object.material_slot_remove_unused() c.switch(obj, 'edit') #combine duplicated material slots for mat_name in [o.name for o in obj.data.materials]: index = 1 base_material = bpy.data.materials.get(mat_name) while redundant_material := bpy.data.materials.get(f'{mat_name}.{index:03d}'): index += 1 redundant_material.user_remap(base_material) bpy.data.materials.remove(redundant_material) #then clean material slots by going through each slot and reassigning the slots that are repeated repeats = {} material_list = obj.data.materials for index, mat in enumerate(material_list): if mat.name not in repeats: repeats[mat.name] = [index] else: repeats[mat.name].append(index) for material_name in repeats.keys(): if len(repeats[material_name]) > 1: for repeated_slot in repeats[material_name]: #don't touch the first slot if repeated_slot == repeats[material_name][0]: continue c.kklog("Moving duplicate material {} in slot {} to the original slot {}".format(material_name, repeated_slot, repeats[material_name][0])) obj.active_material_index = repeated_slot bpy.ops.object.material_slot_select() obj.active_material_index = repeats[material_name][0] bpy.ops.object.material_slot_assign() bpy.ops.mesh.select_all(action='DESELECT') c.switch(obj, 'object') bpy.ops.object.material_slot_remove_unused() c.print_timer('remap_duplicate_material_slots') def replace_materials_for_body(self): c.switch(c.get_body(), 'object') if bpy.app.version[0] != 3: c.get_body().visible_shadow = False templateList = [ 'KK Body', 'KK Tears', 'KK Gag00', 'KK Gag01', 'KK Gag02', 'KK EyeR (hitomi)', 'KK EyeL (hitomi)', 'KK Eyebrows (mayuge)', 'KK Eyeline down', 'KK Eyeline kage', 'KK Eyeline up', 'KK Eyewhites (sirome)', 'KK Face', 'KK General', 'KK Hair', 'KK Nose', 'KK Teeth (tooth)', 'KK Simple', 'KK Glasses', 'Outline General', 'Outline Body', ] c.import_from_library_file(category='Material', list_of_items=templateList, use_fake_user=True) #Replace all materials on the body with templates def swap_body_material(original_materials: list[str], template_name: str): c_name = c.get_name() #remove dupes and check the material slot actually exists original_materials = list(set(original_materials)) for original_material in original_materials: if c.get_body().material_slots.get(original_material): template = bpy.data.materials[template_name].copy() template['body'] = True template['name'] = c_name template['id'] = original_material template['bake'] = True template.name = bpy.data.materials[template_name].name + ' ' + c_name c.get_body().material_slots[original_material].material = template template_group = template.node_tree.nodes['textures'].node_tree.copy() template_group.name = 'Tex ' + original_material + ' ' + c_name template.node_tree.nodes['textures'].node_tree = template_group else: c.kklog(f'material or template wasn\'t found when replacing body materials: {str(original_material)} / {str(template_name)}', 'warn') swap_body_material(c.get_material_names('cf_O_face'),'KK Face') # However, some model has multiple textures on face, like nose material. Simply converting all of them to KK Face gets a white face(could be fixed by removing those material slots manually).Meanwhile, face's material name could change, getting face's material by its general name(cf_m_face_00) may fail under some circumstances swap_body_material(c.get_material_names('cf_O_mayuge'),'KK Eyebrows (mayuge)') swap_body_material(c.get_material_names('cf_O_noseline'),'KK Nose') swap_body_material(c.get_material_names('cf_O_eyeline_low'),'KK Eyeline down') swap_body_material(c.get_material_names('cf_Ohitomi_L'),'KK Eyewhites (sirome)') # swap_body_material(c.get_material_names('cf_Ohitomi_R'),'KK Eyewhites (sirome)') swap_body_material(c.get_material_names('cf_Ohitomi_L02'),'KK EyeL (hitomi)') swap_body_material(c.get_material_names('cf_Ohitomi_R02'),'KK EyeR (hitomi)') swap_body_material(c.get_material_names('o_body_a'),'KK Body') swap_body_material(c.get_material_names('cf_O_tooth'),'KK Teeth (tooth)') swap_body_material(c.get_material_names('o_tang'),'KK General') #Replace the eyeline materials separately if c.get_material_names('cf_O_eyeline'): swap_body_material([c.get_material_names('cf_O_eyeline')[0]],'KK Eyeline up') if len(c.get_material_names('cf_O_eyeline')) > 1: swap_body_material([c.get_material_names('cf_O_eyeline')[1]],'KK Eyeline kage') c.print_timer('replace_materials_for_body') def replace_materials_for_hair(self): '''Replace all of the Hair materials with hair templates and name accordingly''' c_name = c.get_name() for hair in c.get_hairs(): if bpy.app.version[0] != 3: hair.visible_shadow = False for material_slot in hair.material_slots: original_name = material_slot.material.name template = bpy.data.materials['KK Hair'].copy() template['hair'] = True template['name'] = c_name template['bake'] = True #Some hair materials are repeated. The order goes 'hair_material', 'hair_material 00', 'hair_material 01', etc. #If this happens use the name without numbers or the color information from the json will not be loaded correctly if original_name[-2:].isnumeric() and original_name[-3] == ' ': template['id'] = original_name[:-3] else: template['id'] = original_name template.name = 'KK ' + original_name + ' ' + c_name material_slot.material = bpy.data.materials[template.name] template_group = template.node_tree.nodes['textures'].node_tree.copy() template_group.name = 'Tex ' + original_name + ' ' + c_name template.node_tree.nodes['textures'].node_tree = template_group template_group_pos = template.node_tree.nodes['textures'].node_tree.nodes['pospattern'].node_tree.copy() template_group_pos.name = 'Pos ' + original_name + ' ' + c_name template.node_tree.nodes['textures'].node_tree.nodes['pospattern'].node_tree = template_group_pos c.print_timer('replace_materials_for_hair') def replace_materials_for_outfits(self): #Replace all other materials with the general template and name accordingly outfits = c.get_outfits() outfits.extend(c.get_alts()) c_name = c.get_name() for ob in outfits: if bpy.app.version[0] != 3: ob.visible_shadow = False for material_slot in ob.material_slots: original_name = material_slot.material.name template = bpy.data.materials['KK General'].copy() template['outfit'] = True template['name'] = c_name template['bake'] = True #Some outfit materials are repeated. The order goes 'outfit_material', 'outfit_material 00', 'outfit_material 01', etc. #If this happens use the name without numbers or the color information from the json will not be loaded correctly if original_name[-2:].isnumeric() and original_name[-3] == ' ': template['id'] = original_name[:-3] else: template['id'] = original_name template.name = 'KK ' + original_name + ' ' + c_name material_slot.material = bpy.data.materials[template.name] template_group = template.node_tree.nodes['textures'].node_tree.copy() template_group.name = 'Tex ' + original_name + ' ' + c_name template.node_tree.nodes['textures'].node_tree = template_group template_group_pos = template.node_tree.nodes['textures'].node_tree.nodes['pospattern'].node_tree.copy() template_group_pos.name = 'Pos ' + original_name + ' ' + c_name template.node_tree.nodes['textures'].node_tree.nodes['pospattern'].node_tree = template_group_pos c.print_timer('replace_materials_for_outfits') def replace_materials_for_tears_tongue_gageye(self): #give the tears a material template c_name = c.get_name() if (tears := c.get_tears()): template = bpy.data.materials['KK Tears'].copy() template.name = 'KK Tears ' + c_name template['tears'] = True template['id'] = c.get_material_names('cf_O_namida_L')[0] tears.material_slots[0].material = bpy.data.materials[template.name] template_group = template.node_tree.nodes['textures'].node_tree.copy() template.node_tree.nodes['textures'].node_tree = template_group template_group.name += ' ' + c_name #replace tongue material if it exists if c.get_body().material_slots.get('KK General ' + c_name): #Make the tongue material unique so parts of the General Template aren't overwritten template = bpy.data.materials['KK General'].copy() template.name = 'KK Tongue ' + c_name template['tongue'] = True template['bake'] = True # template['id'] = c.get_material_names('o_tang')[0] for material_name in c.get_material_names('o_tang'): # avoid getting the deleted material if bpy.data.materials.get(material_name): template['id'] = material_name break if template.get('id') is None: c.kklog('Failed to replace tongue material', 'warn') c.get_body().material_slots['KK General ' + c_name].material = template template_group = template.node_tree.nodes['textures'].node_tree.copy() template.node_tree.nodes['textures'].node_tree = template_group template_group.name = 'Tex Tongue ' + c_name template_group_pos = template.node_tree.nodes['textures'].node_tree.nodes['pospattern'].node_tree.copy() template.node_tree.nodes['textures'].node_tree.nodes['pospattern'].node_tree = template_group_pos template_group_pos.name = 'Position Tongue ' + c_name #give the rigged tongue the existing material template if c.get_tongue(): c.get_tongue().material_slots[0].material = template #give the gag eyes a material template if they exist if c.get_gags(): gag = c.get_gags() for num in ['00', '01', '02']: template = bpy.data.materials['KK Gag'+num].copy() template['gag'] = True template['id'] = c.get_material_names('cf_O_gag_eye_'+num)[0] gag.material_slots['cf_m_gageye_'+num].material = template template.name = 'KK Gag' + num + ' ' + c_name template_group = template.node_tree.nodes['textures'].node_tree.copy() template.node_tree.nodes['textures'].node_tree = template_group template_group.name = 'Tex Gag' + num + ' ' + c_name c.print_timer('replace_materials_for_tears_tongue_gageye') def remove_duplicate_node_groups(self): # Get rid of the duplicate node groups cause there's a lot #stolen from somewhere def eliminate(node): node_groups = bpy.data.node_groups # Get the node group name as 3-tuple (base, separator, extension) (base, sep, ext) = node.node_tree.name.rpartition('.') # Replace the numeric duplicate if ext.isnumeric(): if base in node_groups: node.node_tree.use_fake_user = False node.node_tree = node_groups.get(base) #--- Search for duplicates in actual node groups node_groups = bpy.data.node_groups for group in node_groups: for node in group.nodes: if node.type == 'GROUP': eliminate(node) #--- Search for duplicates in materials mats = list(bpy.data.materials) worlds = list(bpy.data.worlds) for mat in mats + worlds: if mat.use_nodes: for node in mat.node_tree.nodes: if node.type == 'GROUP': eliminate(node) c.print_timer('remove_duplicate_node_groups') def load_images(self): c.switch(c.get_body(), 'object') file_dir = os.path.dirname(__file__) lut_image = os.path.join(file_dir, 'Lut_TimeDay.png') lut_image = bpy.data.images.load(str(lut_image)) self.init_prefab_data() fileList = Path(bpy.context.scene.kkbp.import_dir).rglob('*.png') files = [file for file in fileList if file.is_file() and "_MT" in file.name] unloaded = unprocessed = len(files) # unloaded: unloaded images to saturate, unprocessed: unfinished images that still need to be saturated last_miss_time = time.time() # last time of accessing queue current_image_num = 0 # current num of concurrent processing images futures = [] record = {} # as each image is separated to several batches, this is to record each image's base info with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_thread_num) as executor: # the to-output data occupies a lot of memory, so we should save them timely before loading more images while unloaded > 0 or unprocessed > 0: if (current_time := time.time()) - last_miss_time > 0.3: # accessing queue every 0.3s, because queue is empty most of the time, so is no need to access it every loop with self.queue_lock: try: while True: # fetching all data from queue if not empty result = self.data_queue.get(timeout=0.1) (result := record[result])[0] -= 1 # data in tuple: (current_image's batch num, name that the image to be saved with, image, image_data in numpy array, start time to process this image) # record[result][0] -= 1 if result[0] == 0: image = result[2] image_pixels = result[3] image.pixels.foreach_set(image_pixels.ravel()) image.save_render(os.path.join(bpy.context.scene.kkbp.import_dir, "saturated_files", result[1])) del image_pixels unprocessed -= 1 current_image_num -= 1 c.kklog(f'Saturating image {result[1]} takes {round(time.time() - result[4], 1)}s') except queue.Empty: last_miss_time = current_time # if there has unloaded images and current num of image in processing is smaller than the max value, then load a image and submit it.This prevent loading too many images, which pushes memory usage to a high level if unloaded > 0 and current_image_num < self.max_image_num: unloaded -= 1 # skip this file if it has already been converted if os.path.isfile(os.path.join(bpy.context.scene.kkbp.import_dir, 'saturated_files', (save_file_name := files[unloaded].name.replace('_MT', '_ST')))): c.kklog('File already saturated. Skipping {}'.format(files[unloaded].name)) unprocessed -= 1 continue current_image_num += 1 # Load image start_time = time.time() image = bpy.data.images.load(str(files[unloaded])) # Submit task width, height = image.size image_pixels = numpy.array(image.pixels[:], dtype=modify_material.np_number_precision).reshape(height, width, 4) # separating an image to several batches to make full use of CPU start_row = 0 while start_row < height: end_row = start_row + self.batch_rows if end_row > height: end_row = height future = executor.submit( self.saturate_texture, unloaded, image_pixels[start_row:end_row], start_row == 0 ) start_row = end_row - 1 if start_row == height - 1: break futures.append(future) record[unloaded] = [math.ceil(height / self.batch_rows), save_file_name, image, image_pixels, start_time] bpy.data.use_autopack = True # enable autopack on file save # Load all textures fileList = Path(bpy.context.scene.kkbp.import_dir).rglob('*.png') files = [file for file in fileList if file.is_file()] for image_file in files: bpy.ops.image.open(filepath=str(image_file), use_udim_detecting=False) try: bpy.data.images[image_file.name].pack() except: c.kklog('This image was not automatically loaded in because its filename exceeds 64 characters: ' + image_file.name, type = 'error') # Monitor completion for future in concurrent.futures.as_completed(futures): try: future.result() except Exception as e: c.kklog(f'Processing failed: {str(e)}') c.print_timer('load_images') def saturate_texture(self, index, slice_image, is_first_batch): '''The Secret Sauce. Accepts a bpy image and saturates it to match the in-game look.''' # Find the XY coordinates of the LUT image needed to saturate each pixel coord = slice_image[:, :, :3] * self.coord_scale + self.coord_offset coord_frac, coord_floor = numpy.modf(coord) coord_frac_z = coord_frac[:, :, 2:3] del coord_frac # free temporary variables after they're used coord_bot = coord[:, :, :2] + coord_floor[:, :, 2:3] * self.texel_height_X0 del coord del coord_floor #use those XY coordinates to find the saturated version of the color from the LUT image lutcol_bot = self.__bilinear_interpolation__(self.lut_pixels, coord_bot) lut_colors = lutcol_bot * (1 - coord_frac_z) del lutcol_bot coord_top = numpy.clip(coord_bot + self.texel_height_X0, 0, 1) lutcol_top = self.__bilinear_interpolation__(self.lut_pixels, coord_top) lut_colors += lutcol_top * coord_frac_z del lutcol_top del coord_top # slice_image[:, :, :3] = lut_colors[:,:,:3] if is_first_batch: slice_image[:, :, :3] = lut_colors[:,:,:3] else: slice_image[1:, :, :3] = lut_colors[1:, :, :3] with self.queue_lock: self.data_queue.put(index) def link_textures_for_face_body(self): '''Load all body textures into their texture slots''' self.image_load('Body', '_ST_CT.png') self.image_load('Body', '_ST_CT.png', node_override='_ST_DT.png') #attempt to default to light in case dark is not available later on #default to colors if there's no maintex if c.get_body().material_slots['KK Body ' + c.get_name()].material.node_tree.nodes['textures'].node_tree.nodes['_ST_CT.png'].image.name == 'Template: Placeholder': c.get_body().material_slots['KK Body ' + c.get_name()].material.node_tree.nodes['dark' ].inputs['Use main texture instead?'].default_value = 0 c.get_body().material_slots['KK Body ' + c.get_name()].material.node_tree.nodes['light'].inputs['Use main texture instead?'].default_value = 0 self.image_load('Body', '_CM.png') #color mask self.image_load('Body', '_DM.png') #cfm female self.image_load('Body', '_LM.png') #line mask for lips self.image_load('Body', '_NMP_CNV.png') self.image_load('Body', '_NMPD_CNV.png') self.image_load('Body', '_ST.png', group_override='texturesnsfw') #chara main texture self.image_load('Body', '_ot2.png', group_override='texturesnsfw') #pubic hair self.image_load('Body', '_ot1.png', group_override='texturesnsfw') #cfm female self.image_load('Body', '_ot1.png', group_override='texturesnsfw', node_override='_ot1.pngleft') self.image_load('Body', '_T3.png') #body overlays self.image_load('Body', '_T6.png') self.set_uv_type('Body', 'nippleuv', 'uv_nipple_and_shine', group= 'texturesnsfw') self.set_uv_type('Body', 'underuv', 'uv_underhair', group= 'texturesnsfw') #find the appropriate alpha mask alpha_mask = None if bpy.data.images.get('_AM.png'): alpha_mask = bpy.data.images.get('_AM.png') elif bpy.data.images.get('_AM_00.png'): alpha_mask = bpy.data.images.get('_AM_00.png') else: #check the other alpha mask numbers for image in bpy.data.images: if '_m_body_AM_' in image.name and image.name[-6:-4].isnumeric(): alpha_mask = image break #if there was an alpha mask detected, load it in if alpha_mask: self.image_load('Body', image_override = alpha_mask.name, node_override='_AM.png') #load in face textures if c.get_material_names('cf_O_face'): self.image_load('Face', '_ST_CT.png') self.image_load('Face', '_ST_CT.png', node_override='_ST_DT.png') #attempt to default to light in case dark is not available #default to colors if there's no maintex if c.get_body().material_slots['KK Face ' + c.get_name()].material.node_tree.nodes['textures'].node_tree.nodes['_ST_CT.png'].image.name == 'Template: Placeholder': c.get_body().material_slots['KK Face ' + c.get_name()].material.node_tree.nodes['light'].inputs['Use main texture instead?'].default_value = 0 c.get_body().material_slots['KK Face ' + c.get_name()].material.node_tree.nodes['dark' ].inputs['Use main texture instead?'].default_value = 0 self.image_load('Face', '_CM.png') self.image_load('Face', '_DM.png') self.image_load('Face', '_T4.png') #blush self.image_load('Face', '_ST.png') #mouth interior self.image_load('Face', '_LM.png') self.image_load('Face', '_T5.png') #lower lip mask self.image_load('Face', '_ot1.png') #lipstick self.image_load('Face', '_ot2.png') #flush self.image_load('Face', '_T3.png') self.image_load('Face', '_T7.png') self.image_load('Face', '_ot3.png') #eyeshadow self.set_uv_type('Face', 'eyeshadowuv', 'uv_eyeshadow') #load in the remaining face materials if they exist if c.get_material_names('cf_O_mayuge'): self.image_load('Eyebrows (mayuge)', '_ST_CT.png') self.image_load('Eyebrows (mayuge)', '_ST_CT.png') if c.get_material_names('cf_O_noseline'): self.image_load('Nose', '_ST_CT.png') if c.get_material_names('cf_O_tooth'): self.image_load('Teeth (tooth)', '_ST_CT.png') if c.get_material_names('cf_Ohitomi_R'): self.image_load('Eyewhites (sirome)', image_override = c.get_material_names('cf_Ohitomi_L')[0] + '_ST_CT.png', node_override = '_ST_CT.png') self.image_load('Eyewhites (sirome)', image_override = c.get_material_names('cf_Ohitomi_R')[0] + '_ST_CT.png', node_override = '_ST_CT.png') if c.get_material_names('cf_O_eyeline'): self.image_load('Eyeline up', image_override= c.get_material_names('cf_O_eyeline')[0] + '_ST_CT.png', node_override='_ST_CT.png') if len(c.get_material_names('cf_O_eyeline')) > 1: self.image_load('Eyeline up', image_override=c.get_material_names('cf_O_eyeline')[1] + '_ST_CT.png', node_override='_ST_CT.pngkage') if c.get_material_names('cf_O_eyeline_low'): self.image_load('Eyeline up', image_override=c.get_material_names('cf_O_eyeline_low')[0] + '_ST_CT.png', node_override='_ST_CT.pngdown') #eyes for side in ['L', 'R']: if c.get_material_names(f'cf_Ohitomi_{side}02'): eye_mat = c.get_material_names(f'cf_Ohitomi_{side}02')[0] self.image_load(f'Eye{side} (hitomi)', '_ST_CT.png') self.image_load(f'Eye{side} (hitomi)', '_ST_CT.png', node_override='_ST_DT.png') #attempt to default to light in case dark is not available self.image_load(f'Eye{side} (hitomi)', '_ot1.png') self.image_load(f'Eye{side} (hitomi)', '_ot2.png') self.image_load(f'Eye{side} (hitomi)', image_override = eye_mat[:-15] + '_cf_t_expression_00_EXPR.png', node_override= '_cf_t_expression_00_EXPR.png') self.image_load(f'Eye{side} (hitomi)', image_override = eye_mat[:-15] + '_cf_t_expression_01_EXPR.png', node_override= '_cf_t_expression_01_EXPR.png') #correct the eye scaling using info from the KK_ChaFileCustomFace.json face_data = c.json_file_manager.get_json_file('KK_ChaFileCustomFace.json') bpy.data.node_groups['.Eye Textures positioning'].nodes['eye_scale'].inputs[1].default_value = 1/(float(face_data[18]['Value']) + 0.0001) bpy.data.node_groups['.Eye Textures positioning'].nodes['eye_scale'].inputs[2].default_value = 1/(float(face_data[19]['Value']) + 0.0001) c.print_timer('link_textures_for_face_body') def link_textures_for_hair(self): '''Load all hair textures into their texture slots''' for current_obj in c.get_hairs(): for hairMat in current_obj.material_slots: #use the material name instead of hairMat.material['id'] to catch any instances of 00 01 02 materials hairType = hairMat.name.replace('KK ','').replace(' ' + c.get_name(), '') self.image_load( hairType, '_ST_CT.png') self.image_load( hairType, '_ST_CT.png', node_override='_ST_DT.png') #attempt to default to light in case dark is not available self.image_load( hairType, '_DM.png') self.image_load( hairType, '_CM.png') self.image_load( hairType, '_HGLS.png') self.image_load( hairType, '_AM.png') self.set_uv_type(hairType, 'hairuv', 'uv_nipple_and_shine') c.print_timer('link_textures_for_hair') def link_textures_for_clothes(self): '''Load all clothes textures into their texture slots''' outfits = c.get_outfits() outfits.extend(c.get_alts()) for outfit in outfits: for genMat in outfit.material_slots: #use the material name instead of genMat.material['id'] to catch any instances of 00 01 02 materials genType = genMat.name.replace('KK ','').replace(' ' + c.get_name(), '') #load these textures if they are present self.image_load(genType, '_ST.png') self.image_load(genType, '_ST_CT.png') self.image_load(genType, '_AM.png') self.image_load(genType, '_CM.png') self.image_load(genType, '_DM.png') self.image_load(genType, '_NMP.png') self.image_load(genType, '_NMPD_CNV.png') self.image_load(genType, '_PM1.png') self.image_load(genType, '_PM2.png') self.image_load(genType, '_PM3.png') #If there's a plain maintex loaded, but no colored maintex loaded, make the shader use the plain maintex plain_but_no_main = ( genMat.material.node_tree.nodes['textures'].node_tree.nodes['_ST_CT.png'].image.name == 'Template: Placeholder' and genMat.material.node_tree.nodes['textures'].node_tree.nodes['_ST.png'].image.name != 'Template: Placeholder' ) if plain_but_no_main: genMat.material.node_tree.nodes['combine'].inputs['Use plain main texture?'].default_value = 1 #If there's an AnotherRamp (AR) texture present, the material is likely supposed to be metallic on the red parts of the detail mask #I don't have a template for this, so the material will just look pure white. Turn off the shine intensity to avoid this image_name = genMat.material['id'] + '_AR.png' if bpy.data.images.get(image_name): genMat.material.node_tree.nodes['light'].inputs['Detail intensity (shine)'].default_value = 0 genMat.material.node_tree.nodes['dark' ].inputs['Detail intensity (shine)'].default_value = 0 shader_name = c.get_shader_name(genMat.material['id']) #If the shader of this material is set to "main opaque" then there is NOT supposed to be a color mask, but the kkbp exporter exports one anyway #Move the colormask to the opaque slot if one was loaded in. This way it can still be used by the plain main texture if genMat.material.node_tree.nodes['textures'].node_tree.nodes['_CM.png'].image: shaders = ['Koikano/main_clothes_opaque', 'Shader Forge/main_opaque', 'xukmi/MainOpaquePlus', 'xukmi/MainOpaquePlusTess', 'Shader Forge/main_opaque2', 'Shader Forge/main_opaque_low'] if shader_name in shaders: c.kklog('Detected opaque shader. Moving color mask to color mask (plain) slot: {}'.format(genMat.material['id'])) genMat.material.node_tree.nodes['textures'].node_tree.nodes['_CM.pngopaque'].image = genMat.material.node_tree.nodes['textures'].node_tree.nodes['_CM.png'].image genMat.material.node_tree.nodes['textures'].node_tree.nodes['_CM.png'].image = None #If the shader of this material is set to "main alpha", set the material to "blended" in blender shaders = ['Shader Forge/main_alpha', 'Koikano/main_clothes_alpha', 'xukmi/MainAlphaPlus', 'xukmi/MainAlphaPlusTess', 'xukmi/MainItemAlphaPlus', 'IBL_Shader_alpha', ] #find this material in the MaterialDataComplete.json and see if it's an alpha shader if shader_name in shaders: c.kklog('Detected alpha shader. Setting render method to blended: {}'.format(genMat.material['id'])) if bpy.app.version[0] == 3: genMat.material.blend_method = 'BLEND' else: genMat.material.surface_render_method = 'BLENDED' #If the shader of this material is set to "glasses", replace the entire shader with shaders = ['Shader Forge/toon_glasses_lod0', 'Koikano/main_clothes_item_glasses',] #find this material in the MaterialDataComplete.json and see if it's a glasses shader if shader_name in shaders: c.kklog('Detected glasses shader. Replacing material with KK Glasses: {}'.format(genMat.material['id'])) original_textures_group = genMat.material.node_tree.nodes['textures'].node_tree template = bpy.data.materials['KK Glasses'].copy() template.node_tree.nodes['textures'].node_tree = original_textures_group bpy.data.materials.remove(genMat.material) template['bake'] = True template['glasses'] = True template.name = 'KK ' + genType + ' ' + c.get_name() genMat.material = template #special exception to clip the emblem image because I am tired of seeing it repeat at the edges if 'KK cf_m_emblem ' in genMat.material.name: genMat.material.node_tree.nodes['textures'].node_tree.nodes['_ST_CT.png'].extension = 'CLIP' c.print_timer('link_textures_for_clothes') def link_textures_for_tongue_tear_gag(self): if c.get_tongue(): tongue_mat = c.get_material_names('o_tang') tongue_mat = tongue_mat if tongue_mat else ['cf_m_tang'] #check for bugged/missing SMR Tongue data self.image_load('Tongue', '_CM.png', node_override='_ST_CT.png') #done on purpose self.image_load('Tongue', '_CM.png', node_override='_ST_DT.png') #still done on purpose self.image_load('Tongue', '_CM.png') self.image_load('Tongue', '_DM.png') self.image_load('Tongue', '_NMP.png') self.image_load('Tongue', '_NMP_CNV.png', node_override = '_NMPD_CNV.png') #load regular map by default self.image_load('Tongue', '_NMPD_CNV.png') #then the detail map if it's there #load all gag eye textures if it exists if c.get_gags(): self.image_load('Gag00', '_cf_t_gageye_00_ST_CT.png') self.image_load('Gag00', '_cf_t_gageye_02_ST_CT.png') self.image_load('Gag00', '_cf_t_gageye_04_ST_CT.png') self.image_load('Gag00', '_cf_t_gageye_05_ST_CT.png') self.image_load('Gag00', '_cf_t_gageye_06_ST_CT.png') self.image_load('Gag01', '_cf_t_gageye_03_ST_CT.png') self.image_load('Gag01', '_cf_t_gageye_01_ST_CT.png') self.image_load('Gag02', '_cf_t_gageye_07_ST_CT.png') self.image_load('Gag02', '_cf_t_gageye_08_ST_CT.png') self.image_load('Gag02', '_cf_t_gageye_09_ST_CT.png') #load the tears texture in if c.get_tears(): self.image_load('Tears', '_ST_CT.png') c.print_timer('link_textures_for_tongue_tear_gag') def create_dark_textures(self): """ Creates dark versions of textures for body, hair, and outfit materials. This method retrieves all body, hair, and outfit materials, and for each material, it checks if the material has a 'textures' node and if it contains a '_ST_DT.png' texture. If the texture is not a placeholder, it creates a dark version of the texture using the shadow color specific to the material and assigns it to the '_ST_DT.png' texture node. """ materials = c.get_body_materials() materials.extend(c.get_hair_materials()) materials.extend(c.get_outfit_materials()) for material in materials: if material.node_tree.nodes.get('textures'): if material.node_tree.nodes['textures'].node_tree.nodes.get('_ST_DT.png'): maintex = material.node_tree.nodes['textures'].node_tree.nodes['_ST_CT.png'].image #if this isn't a placeholder image, create a dark version of it if maintex.name != 'Template: Placeholder' and maintex.name != 'cf_m_tang_CM.png': shadow_color = c.json_file_manager.get_shadow_color(material.name) darktex = self.create_darktex(maintex, shadow_color) material.node_tree.nodes['textures'].node_tree.nodes['_ST_DT.png'].image = darktex c.print_timer('create_dark_textures') def import_and_setup_smooth_normals(self): '''Sets up the Smooth Normals geo nodes setup for smoother face, body, hair and clothes normals''' try: #import all the node groups body = c.get_body() c.import_from_library_file('NodeTree', ['.Raw Shading (smooth normals)', '.Raw Shading (smooth body normals)', '.Smooth Normals', '.Other Smooth Normals'], bpy.context.scene.kkbp.use_material_fake_user) c.switch(body, 'object') geo_nodes = body.modifiers.new(name = 'Normal Smoothing', type = 'NODES') geo_nodes.node_group = bpy.data.node_groups['.Smooth Normals'] geo_nodes.show_viewport = False geo_nodes.show_render = False for ob in c.get_hairs(): geo_nodes = ob.modifiers.new(name = 'Normal Smoothing', type = 'NODES') geo_nodes.node_group = bpy.data.node_groups['.Other Smooth Normals'] geo_nodes.show_viewport = False geo_nodes.show_render = False outfits = c.get_outfits() outfits.extend(c.get_alts()) for ob in outfits: geo_nodes = ob.modifiers.new(name = 'Normal Smoothing', type = 'NODES') geo_nodes.node_group = bpy.data.node_groups['.Other Smooth Normals'] geo_nodes.show_viewport = False geo_nodes.show_render = False except: #i don't feel like dealing with any errors related to this c.kklog('The normal smoothing wasnt setup correctly. Oh well.', 'warn') c.print_timer('import_and_setup_smooth_normals') def setup_gag_eye_material_drivers(self): '''setup gag eye drivers''' if c.get_gags(): body = c.get_body() gag_keys = [ 'Circle Eyes 1', 'Circle Eyes 2', 'Spiral Eyes', 'Heart Eyes', 'Fiery Eyes', 'Cartoony Wink', 'Vertical Line', 'Cartoony Closed', 'Horizontal Line', 'Cartoony Crying' ] def create_driver(material, expression1, expression2): skey_driver = bpy.data.materials[material].node_tree.nodes['Parser'].inputs[0].driver_add('default_value') skey_driver.driver.type = 'SCRIPTED' for key in gag_keys: newVar = skey_driver.driver.variables.new() newVar.name = key.replace(' ','') newVar.type = 'SINGLE_PROP' newVar.targets[0].id_type = 'KEY' newVar.targets[0].id = body.data.shape_keys newVar.targets[0].data_path = 'key_blocks["' + key + '"].value' skey_driver.driver.expression = expression1 skey_driver = bpy.data.materials[material].node_tree.nodes['hider'].inputs[0].driver_add('default_value') skey_driver.driver.type = 'SCRIPTED' for key in gag_keys: newVar = skey_driver.driver.variables.new() newVar.name = key.replace(' ','') newVar.type = 'SINGLE_PROP' newVar.targets[0].id_type = 'KEY' newVar.targets[0].id = body.data.shape_keys newVar.targets[0].data_path = 'key_blocks["' + key + '"].value' skey_driver.driver.expression = expression2 create_driver ( 'KK Gag00 ' + c.get_name(), '0 if CircleEyes1 else 1 if CircleEyes2 else 2 if CartoonyClosed else 3 if VerticalLine else 4', 'CircleEyes1 or CircleEyes2 or CartoonyClosed or VerticalLine or HorizontalLine' ) create_driver ( 'KK Gag01 ' + c.get_name(), '0 if HeartEyes else 1', 'HeartEyes or SpiralEyes' ) create_driver ( 'KK Gag02 ' + c.get_name(), '0 if CartoonyCrying else 1 if CartoonyWink else 2', 'CartoonyCrying or CartoonyWink or FieryEyes' ) c.print_timer('setup_gag_eye_material_drivers') def add_outlines_to_body(self): #Add face and body outlines, then load in the clothes transparency mask to body outline body = c.get_body() c.switch(body, 'object') mod = body.modifiers.new(type='SOLIDIFY', name='Outline Modifier') mod.thickness = 0.0005 mod.offset = 0 mod.material_offset = len(body.material_slots) mod.use_flip_normals = True mod.use_rim = False mod.name = 'Outline Modifier' mod.show_expanded = False #face first faceOutlineMat = bpy.data.materials['Outline General'].copy() faceOutlineMat.name = 'Outline Face ' + c.get_name() body.data.materials.append(faceOutlineMat) faceOutlineMat.blend_method = 'CLIP' body_outline_mat = bpy.data.materials['Outline Body'].copy() body_outline_mat.name = 'Outline Body ' + c.get_name() body_outline_mat.node_tree.nodes['textures'].node_tree = bpy.data.materials['KK Body ' + c.get_name()].node_tree.nodes['textures'].node_tree body.data.materials.append(body_outline_mat) c.print_timer('add_outlines_to_body') def add_outlines_to_hair(self): #Give each piece of hair with an alphamask on each hair object it's own outline group if not bpy.context.scene.kkbp.use_single_outline: for ob in c.get_hairs(): #Get the length of the material list before starting outlineStart = len(ob.material_slots) #link all polygons to material name mats_to_gons = {} for slot in ob.material_slots: mats_to_gons[slot.material.name] = [] for gon in ob.data.polygons: mats_to_gons[ob.material_slots[gon.material_index].material.name].append(gon) #find all materials that use an alpha mask or maintex alpha_users = [] for mat in ob.material_slots: AlphaImage = mat.material.node_tree.nodes['textures'].node_tree.nodes['_AM.png'].image MainImage = mat.material.node_tree.nodes['textures'].node_tree.nodes['_ST_CT.png'].image if AlphaImage or MainImage: alpha_users.append(mat.material.name) #reorder material_list to place alpha/maintex users first new_mat_list_order = [mat_slot.material.name for mat_slot in ob.material_slots if mat_slot.material.name not in alpha_users] new_mat_list_order = alpha_users + new_mat_list_order #reorder mat slot list for index, mat_slot in enumerate(ob.material_slots): mat_slot.material = bpy.data.materials[new_mat_list_order[index]] #create empty slots for new alpha user outlines for mat in alpha_users: ob.data.materials.append(None) #fill alpha user outline materials, and fill image node for index, mat in enumerate(alpha_users): OutlineMat = bpy.data.materials['Outline General'].copy() OutlineMat.name = mat.replace('KK ', 'Outline ') OutlineMat.node_tree.nodes['textures'].node_tree = bpy.data.materials[mat].node_tree.nodes['textures'].node_tree ob.material_slots[index + outlineStart].material = OutlineMat #update polygon material indexes for mat in mats_to_gons: for gon in mats_to_gons[mat]: gon.material_index = new_mat_list_order.index(mat) #Add a general outline that covers the rest of the materials on the hair object that don't need transparency for ob in c.get_hairs(): bpy.context.view_layer.objects.active = ob mod = ob.modifiers.new( type='SOLIDIFY', name='Outline Modifier') mod.thickness = 0.0005 mod.offset = 1 mod.material_offset = outlineStart if not bpy.context.scene.kkbp.use_single_outline else 200 mod.use_flip_normals = True mod.use_rim = False mod.show_expanded = False hairOutlineMat = bpy.data.materials['Outline General'].copy() hairOutlineMat.name = 'Outline Hair ' + c.get_name() hairOutlineMat.node_tree.nodes['combine'].inputs['Force visibility'].default_value = 1 ob.data.materials.append(hairOutlineMat) c.print_timer('add_outlines_to_hair') def add_outlines_to_clothes(self): #Add a standard outline to all other objects #keep a dictionary of the material length list for the next loop outlineStart = {} body = c.get_body() c.switch(body, 'object') outfits = c.get_outfits() outfits.extend(c.get_alts()) if not bpy.context.scene.kkbp.use_single_outline: #If the material has a maintex or alphamask then give it it's own outline for ob in outfits: #Get the length of the material list before starting outlineStart[ob.name] = len(ob.material_slots) #link all polygons to material name mats_to_gons = {} for slot in ob.material_slots: mats_to_gons[slot.material.name] = [] for gon in ob.data.polygons: mats_to_gons[ob.material_slots[gon.material_index].material.name].append(gon) #find all materials that use an alpha mask or maintex alpha_users = [] for mat in ob.material_slots: AlphaImage = mat.material.node_tree.nodes['textures'].node_tree.nodes['_AM.png'].image MainImage = mat.material.node_tree.nodes['textures'].node_tree.nodes['_ST_CT.png'].image if AlphaImage or MainImage: alpha_users.append(mat.material.name) #reorder material_list to place alpha/maintex users first new_mat_list_order = [mat_slot.material.name for mat_slot in ob.material_slots if mat_slot.material.name not in alpha_users] new_mat_list_order = alpha_users + new_mat_list_order #reorder mat slot list for index, mat_slot in enumerate(ob.material_slots): mat_slot.material = bpy.data.materials[new_mat_list_order[index]] #create empty slots for new alpha user outlines for mat in alpha_users: ob.data.materials.append(None) #fill alpha user outline materials, and fill image node for index, mat in enumerate(alpha_users): OutlineMat = bpy.data.materials['Outline General'].copy() OutlineMat.name = mat.replace('KK ', 'Outline ') OutlineMat.node_tree.nodes['textures'].node_tree = bpy.data.materials[mat].node_tree.nodes['textures'].node_tree ob.material_slots[index + outlineStart[ob.name]].material = OutlineMat #if the outline material is for a glasses material, disable it if bpy.data.materials[mat].get('glasses'): nodes = OutlineMat.node_tree.nodes links = OutlineMat.node_tree.links links.remove(nodes['combine'].inputs['Main texture (alpha)'].links[0]) #update polygon material indexes for mat in mats_to_gons: for gon in mats_to_gons[mat]: gon.material_index = new_mat_list_order.index(mat) for ob in outfits: #Add a general outline that covers the rest of the materials on the object that don't need transparency mod = ob.modifiers.new( type='SOLIDIFY', name='Outline Modifier') mod.thickness = 0.0005 mod.offset = 1 mod.material_offset = outlineStart[ob.name] if not bpy.context.scene.kkbp.use_single_outline else 200 mod.use_flip_normals = True mod.use_rim = False mod.show_expanded = False outline_mat = bpy.data.materials['Outline General'].copy() outline_mat.name = 'Outline ' + ob.name outline_mat.node_tree.nodes['combine'].inputs['Force visibility'].default_value = 1 ob.data.materials.append(outline_mat) c.print_timer('add_outlines_to_clothes') @classmethod def load_luts(cls): self = cls self.lut_selection = bpy.context.scene.kkbp.colors_dropdown self.lut_light = 'Lut_TimeDay.png' self.lut_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), self.lut_light) day_lut = bpy.data.images.load(self.lut_path, check_existing=True) day_lut.use_fake_user = True def load_json_colors(self): self.update_shaders('light') # Set light colors self.update_shaders('dark') # Set dark colors c.print_timer('load_json_colors') def set_color_management(self): if bpy.app.version[0] != 3: #disable shadows in the scene. The toon shading in 4.2 is fucking broken but the broken-ness can be hidden with this setting bpy.data.scenes[0].eevee.use_shadows = False c.print_timer('set_color_management') # %% Supporting functions @staticmethod def apply_texture_data_to_image(mat: str, image: str, node:str, group = 'textures'): '''Sets offset and scale of an image node using the TextureData.json ''' json_tex_data = c.json_file_manager.get_json_file('KK_TextureData.json') texture_data = [t for t in json_tex_data if t["textureName"] == image] if texture_data and bpy.data.materials.get(mat): #Apply Offset and Scale bpy.data.materials[mat].node_tree.nodes[group].node_tree.nodes[node].texture_mapping.translation[0] = texture_data[0]["offset"]["x"] bpy.data.materials[mat].node_tree.nodes[group].node_tree.nodes[node].texture_mapping.translation[1] = texture_data[0]["offset"]["y"] bpy.data.materials[mat].node_tree.nodes[group].node_tree.nodes[node].texture_mapping.scale[0] = texture_data[0]["scale"]["x"] bpy.data.materials[mat].node_tree.nodes[group].node_tree.nodes[node].texture_mapping.scale[1] = texture_data[0]["scale"]["y"] def image_load(self, material_name: str, image_suffix = '', image_override = None, node_override = None, group_override = None): '''Automatically load image into mat's texture slot''' #get the id from the material material_name = 'KK ' + material_name + ' ' + c.get_name() material = bpy.data.materials[material_name] #get the image name using the id and the suffix image_name = image_override if image_override else material['id'] + image_suffix #then load the image into the texture slot if bpy.data.images.get(image_name): node = node_override if node_override else image_name.replace(material['id'], '') group = group_override if group_override else 'textures' bpy.data.materials[material_name].node_tree.nodes[group].node_tree.nodes[node].image = bpy.data.images[image_name] #also apply scaling and offset data to the image self.apply_texture_data_to_image(material_name, image_name, node, group) else: c.kklog('File wasnt found, skipping: ' + image_name) @staticmethod def set_uv_type(mat: str, uvnode: str, uv_name: str, group = 'textures'): bpy.data.materials['KK ' + mat + ' ' + c.get_name()].node_tree.nodes[group].node_tree.nodes['pos'].node_tree.nodes[uvnode].uv_map = uv_name @staticmethod def __bilinear_interpolation__(lut_pixels, coords): h, w, _ = lut_pixels.shape x = coords[:, :, 0] * (w - 1) # Fudge x coordinates based on x position. subtract -0.5 if at x position 0 and add 0.5 if at x position 1024 of the LUT. # this helps with some kind of overflow / underflow issue where it reads from the next LUT square when it's not supposed to x = x + (x / 1024 - 0.5) y = coords[:, :, 1] * (h - 1) # Get integer and fractional parts of each coordinate. # Also make sure each coordinate is clipped to the LUT image bounds x0 = numpy.clip(numpy.floor(x).astype(int), 0, w - 1) x1 = numpy.clip(x0 + 1, 0, w - 1) y0 = numpy.clip(numpy.floor(y).astype(int), 0, h - 1) y1 = numpy.clip(y0 + 1, 0, h - 1) x_frac = x - x0 y_frac = y - y0 # Get the pixel values at four corners of this coordinate f00 = lut_pixels[y0, x0] f01 = lut_pixels[y1, x0] f10 = lut_pixels[y0, x1] f11 = lut_pixels[y1, x1] del x0 del x1 del y0 del y1 # Perform the bilinear interpolation using the fractional part of each coordinate # This will ensure the LUT can provide the correct color every single time, even if that color isn't found in the LUT itself # If this isn't performed, the resulting image will look very blocky because it will snap to colors only found in the LUT. lut_col_bot = f00 * (1 - y_frac)[:, :, numpy.newaxis] + f01 * y_frac[:, :, numpy.newaxis] lut_col_top = f10 * (1 - y_frac)[:, :, numpy.newaxis] + f11 * y_frac[:, :, numpy.newaxis] interpolated_colors = lut_col_bot * (1 - x_frac)[:, :, numpy.newaxis] + lut_col_top * x_frac[:, :, numpy.newaxis] return interpolated_colors def saturate_color(self, color: float, light_pass = 'light', shadow_color = {'r':0.764, 'g':0.880, 'b':1}) -> dict[str, float]: '''The Secret Sauce. Accepts a 0-1 float rgba color dict, saturates it to match the in-game look and returns it in the form of a 0-1 float rgba array''' #fix the color if it does not have an alpha color['a'] = color.get('a', 1) #make the color a dark color if the light_pass is set to dark color = color if light_pass == 'light' else self.clothes_dark_color(color, shadow_color) width, height = 1,1 # Load image and LUT image pixels into array image_pixels = numpy.array([color['r'], color['g'], color['b'], 1],dtype=modify_material.np_number_precision).reshape(height, width, 4) # Find the XY coordinates of the LUT image needed to saturate each pixel coord = image_pixels[:, :, :3] * self.coord_scale + self.coord_offset coord_frac, coord_floor = numpy.modf(coord) coord_bot = coord[:, :, :2] + numpy.tile(coord_floor[:, :, 2].reshape(height, width, 1), (1, 1, 2)) * self.texel_height_X0 coord_top = numpy.clip(coord_bot + self.texel_height_X0, 0, 1) lutcol_bot = self.__bilinear_interpolation__(self.lut_pixels, coord_bot) lutcol_top = self.__bilinear_interpolation__(self.lut_pixels, coord_top) #After the older gpu code uses the texture lookup the colorspace is converted from srgb to linear, # so replicate that behavior here. def srgb_to_linear(srgb): linear_rgb = numpy.where( srgb <= 0.04045, srgb / 12.92, numpy.power((srgb + 0.055) / 1.055, 2.4)) return linear_rgb lutcol_bot = srgb_to_linear(lutcol_bot) lutcol_top = srgb_to_linear(lutcol_top) lut_colors = lutcol_bot * (1 - coord_frac[:, :, 2].reshape(height, width, 1)) + lutcol_top * coord_frac[:, :, 2].reshape(height, width, 1) image_pixels[:, :, :3] = lut_colors[:,:,:3] return image_pixels.flatten().tolist()[0:4] def update_shaders(self, light_pass: str): '''Set the colors for everything. This is run once for the light colors and again for the dark colors''' #set the tongue colors if it exists #if c.get_material_names('o_tang') and (tongue := c.get_tongue()): if c.get_material_names('o_tang') and (tongue := c.get_tongue()): shader_inputs = tongue.material_slots[0].material.node_tree.nodes[light_pass].inputs shader_inputs['Maintex Saturation'].default_value = 0.6 shader_inputs['Detail intensity (green)'].default_value = 0.01 shader_inputs['Color mask (base)'].default_value = [1, 1, 1, 1] mat_name = tongue.material_slots[0].name shader_inputs['Color mask (red)'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_Color "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(mat_name)) shader_inputs['Color mask (green)'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_Color2 "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(mat_name)) shader_inputs['Color mask (blue)'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_Color3 "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(mat_name)) #set all of the hair colors hair_materials = [m for m in bpy.data.materials if m.get('hair') == True and m.get('name') == c.get_name()] for hair_material in hair_materials: shader_inputs = hair_material.node_tree.nodes[light_pass].inputs shader_inputs['Hair color'].default_value = self.saturate_color(c.json_file_manager.get_color(hair_material.name, "_Color " ), light_pass, shadow_color = c.json_file_manager.get_shadow_color(hair_material.name)) shader_inputs['Color mask (root)'].default_value = self.saturate_color(c.json_file_manager.get_color(hair_material.name, "_Color2 "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(hair_material.name)) shader_inputs['Color mask (tip)'].default_value = self.saturate_color(c.json_file_manager.get_color(hair_material.name, "_Color3 "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(hair_material.name)) #set body colors if c.get_body(): if c.get_material_names('o_body_a'): shader_inputs = c.get_body().material_slots['KK Body ' + c.get_name()].material.node_tree.nodes[light_pass].inputs mat_name = 'KK Body ' + c.get_name() if light_pass == 'light': shader_inputs['Skin color'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_Color "), light_pass = 'light') else: shader_inputs['Skin color'].default_value = self.saturate_color(self.skin_dark_color(c.json_file_manager.get_color(mat_name, "_Color ")), light_pass = 'light') shader_inputs['Detail color'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_Color2 " ), light_pass, shadow_color = c.json_file_manager.get_shadow_color(mat_name)) shader_inputs['Line mask color'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_Color2 " ), light_pass, shadow_color = c.json_file_manager.get_shadow_color(mat_name)) #use same color for both detail and line shader_inputs['Nail color (multiplied)'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_Color5 " ), light_pass, shadow_color = c.json_file_manager.get_shadow_color(mat_name)) if not bpy.context.scene.kkbp.sfw_mode: shader_inputs['Underhair color'].default_value = [0, 0, 0, 1] # shader_inputs['Nipple base'].default_value = [1.0, 0.48, 0.48, 1.0] #these don't seem to be the correct colors. Just use hardcoded colors in .blend file # shader_inputs['Nipple base 2'].default_value = [0.9, 0.0, 0.1, 1.0] # shader_inputs['Nipple shine'].default_value = [1.0, 0.8, 0.8, 1.0] # shader_inputs['Nipple rim'].default_value = [1.0, 0.08, 0.09, 1.0] #face #Note that some headmods have multiple face materials. This will only replace the first one if c.get_material_names('cf_O_face'): #setup the face material mat_name = 'KK Face ' + c.get_name() shader_inputs = c.get_body().material_slots[mat_name].material.node_tree.nodes[light_pass].inputs shader_inputs['Skin color'].default_value = c.get_body().material_slots['KK Body ' + c.get_name()].material.node_tree.nodes[light_pass].inputs['Skin color'].default_value shader_inputs['Detail color'].default_value = self.saturate_color(c.json_file_manager.get_color('KK Body ' + c.get_name(), "_Color2 " ), light_pass, shadow_color = c.json_file_manager.get_shadow_color('KK Body ' + c.get_name())) shader_inputs['Light blush color'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_overcolor2 " ), light_pass, shadow_color = c.json_file_manager.get_shadow_color(mat_name)) shader_inputs['Lipstick multiplier'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_overcolor1 " ), light_pass, shadow_color = c.json_file_manager.get_shadow_color(mat_name)) #eyebrows if c.get_material_names('cf_O_mayuge'): mat_name = 'KK Eyebrows (mayuge) ' + c.get_name() shader_inputs = c.get_body().material_slots[mat_name].material.node_tree.nodes['light'].inputs shader_inputs['Eyebrow color'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_Color ")) shader_inputs['Eyebrow color dark'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_Color "), 'dark' , shadow_color = c.json_file_manager.get_shadow_color(mat_name)) #eyeline if c.get_material_names('cf_O_eyeline'): mat_name = 'KK Eyeline up ' + c.get_name() shader_inputs = c.get_body().material_slots[mat_name].material.node_tree.nodes['light'].inputs shader_inputs['Eyeline fade color'].default_value = self.saturate_color(c.json_file_manager.get_color(mat_name, "_Color "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(mat_name)) #the below doesn't seem to be the correct color. Use the hardcoded one in the blend file for now # if len(c.get_material_names('cf_O_eyeline')) > 1: # shader_inputs['Kage color'].default_value = self.saturate_color(c.json_file_manager.get_color('KK Eyeline kage ' + c.get_name(), "_Color "), light_pass, shadow_color = c.json_file_manager.get_shadow_color('KK Eyeline kage ' + c.get_name())) if c.get_material_names('cf_O_eyeline_low'): shader_inputs = c.get_body().material_slots[mat_name].material.node_tree.nodes['light'].inputs shader_inputs['Eyeline down fade color'].default_value = self.saturate_color(c.json_file_manager.get_color('KK Eyeline down ' + c.get_name(), "_Color "), light_pass, shadow_color = c.json_file_manager.get_shadow_color('KK Eyeline down ' + c.get_name())) #set the clothes colors materials = [m for m in bpy.data.materials if m.get('outfit') == True and m.get('name') == c.get_name()] for material in materials: shader_inputs = material.node_tree.nodes[light_pass].inputs shader_inputs['Color mask (red)'].default_value = self.saturate_color(c.json_file_manager.get_color(material.name, "_Color "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(material.name)) shader_inputs['Color mask (green)'].default_value = self.saturate_color(c.json_file_manager.get_color(material.name, "_Color2 "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(material.name)) shader_inputs['Color mask (blue)'].default_value = self.saturate_color(c.json_file_manager.get_color(material.name, "_Color3 "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(material.name)) shader_inputs['Pattern color (red)'].default_value = self.saturate_color(c.json_file_manager.get_color(material.name, "_Color1_2 "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(material.name)) shader_inputs['Pattern color (green)'].default_value = self.saturate_color(c.json_file_manager.get_color(material.name, "_Color2_2 "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(material.name)) shader_inputs['Pattern color (blue)'].default_value = self.saturate_color(c.json_file_manager.get_color(material.name, "_Color3_2 "), light_pass, shadow_color = c.json_file_manager.get_shadow_color(material.name)) #something is wrong with this one, currently unused # def hair_dark_color(self, color, shadow_color): # diffuse = float4(color[0], color[1], color[2], 1) # _ShadowColor = float4(shadow_color['r'], shadow_color['g'], shadow_color['b'], 1) # finalAmbientShadow = 0.7225; #constant # invertFinalAmbientShadow = finalAmbientShadow #this shouldn't be equal to this but it works so whatever # finalAmbientShadow = finalAmbientShadow * _ShadowColor # finalAmbientShadow += finalAmbientShadow; # shadowCol = _ShadowColor - 0.5; # shadowCol = -shadowCol * 2 + 1; # invertFinalAmbientShadow = -shadowCol * invertFinalAmbientShadow + 1; # shadeCheck = 0.5 < _ShadowColor; # hlslcc_movcTemp = finalAmbientShadow; # hlslcc_movcTemp.x = invertFinalAmbientShadow.x if (shadeCheck.x) else finalAmbientShadow.x; # hlslcc_movcTemp.y = invertFinalAmbientShadow.y if (shadeCheck.y) else finalAmbientShadow.y; # hlslcc_movcTemp.z = invertFinalAmbientShadow.z if (shadeCheck.z) else finalAmbientShadow.z; # finalAmbientShadow = (hlslcc_movcTemp).saturate(); # diffuse *= finalAmbientShadow; # finalDiffuse = diffuse.saturate(); # shading = 1 - finalAmbientShadow; # shading = 1 * shading + finalAmbientShadow; # finalDiffuse *= shading; # shading = 1.0656; # finalDiffuse *= shading; # return [finalDiffuse.x, finalDiffuse.y, finalDiffuse.z]; def MapValuesMain(self, color): #-> float4 '''mapvaluesmain function is from https://github.com/xukmi/KKShadersPlus/blob/main/Shaders/Skin/KKPDiffuse.cginc''' t0 = color; tb30 = t0.y>=t0.z; t30 = 1 if tb30 else float(0.0); t1 = float4(t0.z, t0.y, t0.z, t0.w); t2 = float4(t0.y - t1.x, t0.z - t1.y); t1.z = float(-1.0); t1.w = float(0.666666687); t2.z = float(1.0); t2.w = float(-1.0); t1 = float4(t30, t30, t30, t30) * float4(t2.x, t2.y, t2.w, t2.z) + float4(t1.x, t1.y, t1.w, t1.z); tb30 = t0.x>=t1.x; t30 = 1 if tb30 else 0.0; t2.z = t1.w; t1.w = t0.x; t2 = float4(t1.w, t1.y, t2.z, t1.x) t2 = (-t1) + t2; t1 = float4(t30, t30, t30, t30) * t2 + t1; t30 = min(t1.y, t1.w); t30 = (-t30) + t1.x; t2.x = t30 * 6.0 + 1.00000001e-10; t11 = (-t1.y) + t1.w; t11 = t11 / t2.x; t11 = t11 + t1.z; t1.x = t1.x + 1.00000001e-10; t30 = t30 / t1.x; t30 = t30 * 0.660000026; #w component isn't used anymore so ignore t2 = float4(t11, t11, t11).abs() + float4(-0.0799999982, -0.413333356, 0.25333333) t2 = t2.frac() t2 = (-t2) * float4(2.0, 2.0, 2.0) + float4(1.0, 1.0, 1.0); t2 = t2.abs() * float4(3.0, 3.0, 3.0) + float4(-1.0, -1.0, -1.0); t2 = t2.clamp() t2 = t2 + float4(-1.0, -1.0, -1.0); t2 = float4(t30, t30, t30) * t2 + float4(1.0, 1.0, 1.0); return float4(t2.x, t2.y, t2.z, 1); def skin_dark_color(self, color) -> dict[str, float]: '''Takes a 1.0 max rgba dict and returns a 1.0 max rgba dict. skin is from https://github.com/xukmi/KKShadersPlus/blob/main/Shaders/Skin/KKPSkinFrag.cginc ''' diffuse = float4(color['r'], color['g'], color['b'], 1) shadingAdjustment = self.MapValuesMain(diffuse); diffuseShaded = shadingAdjustment * 0.899999976 - 0.5; diffuseShaded = -diffuseShaded * 2 + 1; compTest = 0.555555582 < shadingAdjustment; shadingAdjustment *= 1.79999995; diffuseShaded = -diffuseShaded * 0.7225 + 1; hlslcc_movcTemp = shadingAdjustment; hlslcc_movcTemp.x = diffuseShaded.x if (compTest.x) else shadingAdjustment.x; #370 hlslcc_movcTemp.y = diffuseShaded.y if (compTest.y) else shadingAdjustment.y; #371 hlslcc_movcTemp.z = diffuseShaded.z if (compTest.z) else shadingAdjustment.z; #372 shadingAdjustment = (hlslcc_movcTemp).saturate(); #374 the lerp result (and shadowCol) is going to be this because shadowColor's alpha is always 1 making shadowCol 1 finalDiffuse = diffuse * shadingAdjustment; bodyShine = float4(1.0656, 1.0656, 1.0656, 1); finalDiffuse *= bodyShine; fudge_factor = float4(0.02, 0.05, 0, 0) #result is slightly off but it looks consistently off so add a fudge factor finalDiffuse += fudge_factor return {'r':finalDiffuse.x, 'g':finalDiffuse.y, 'b':finalDiffuse.z, 'a':1} def ShadeAdjustItem(self, col, _ShadowColor): #-> float4 '''#shadeadjust function is from https://github.com/xukmi/KKShadersPlus/blob/main/Shaders/Item/KKPItemDiffuse.cginc . lines with comments at the end have been translated from C# to python. lines without comments at the end have been copied verbatim from the C# source''' #start at line 63 t0 = col t1 = float4(t0.y, t0.z, None, t0.x) * float4(_ShadowColor.y, _ShadowColor.z, None, _ShadowColor.x) #line 65 t2 = float4(t1.y, t1.x) #66 t3 = float4(t0.y, t0.z) * float4(_ShadowColor.y, _ShadowColor.z) + (-float4(t2.x, t2.y)); #67 tb30 = t2.y >= t1.y; t30 = 1 if tb30 else 0; t2 = float4(t2.x, t2.y, -1.0, 0.666666687); #70-71 t3 = float4(t3.x, t3.y, 1.0, -1); #72-73 t2 = (t30) * t3 + t2; tb30 = t1.w >= t2.x; t30 = 1 if tb30 else float(0.0); t1 = float4(t2.x, t2.y, t2.w, t1.w) #77 t2 = float4(t1.w, t1.y, t2.z, t1.x) #78 t2 = (-t1) + t2; t1 = (t30) * t2 + t1; t30 = min(t1.y, t1.w); t30 = (-t30) + t1.x; t2.x = t30 * 6.0 + 1.00000001e-10; t11 = (-t1.y) + t1.w; t11 = t11 / t2.x; t11 = t11 + t1.z; t1.x = t1.x + 1.00000001e-10; t30 = t30 / t1.x; t30 = t30 * 0.5; #the w component of t1 is no longer used, so ignore it t1 = abs((t11)) + float4(0.0, -0.333333343, 0.333333343, 1); #90 t1 = t1.frac(); #91 t1 = -t1 * 2 + 1; #92 t1 = t1.abs() * 3 + (-1) #93 t1 = t1.clamp() #94 t1 = t1 + (-1); #95 t1 = (t30) * t1 + 1; #96 return float4(t1.x, t1.y, t1.z, 1) #97 def clothes_dark_color(self, color: dict, shadow_color: dict) -> dict[str, float]: '''Takes a 1.0 max rgba dict and returns a 1.0 max rgba dict. clothes is from https://github.com/xukmi/KKShadersPlus/blob/main/Shaders/Item/MainItemPlus.shader This was stripped down to just the shadow portion, and to remove all constants''' ################### variable setup _ambientshadowG = float4(0.15, 0.15, 0.15, 0.15) #constant from experimentation diffuse = float4(color['r'],color['g'],color['b'],1) #maintex color _ShadowColor = float4(shadow_color['r'],shadow_color['g'],shadow_color['b'],1) #the shadow color from material editor ########################## #start at line 344 because the other one is for outlines shadingAdjustment = self.ShadeAdjustItem(diffuse, _ShadowColor) #skip to line 352 diffuseShaded = shadingAdjustment * 0.899999976 - 0.5; diffuseShaded = -diffuseShaded * 2 + 1; compTest = 0.555555582 < shadingAdjustment; shadingAdjustment *= 1.79999995; diffuseShaded = -diffuseShaded * 0.7225 + 1; #invertfinalambient shadow is a constant 0.7225, so don't calc it hlslcc_movcTemp = shadingAdjustment; hlslcc_movcTemp.x = diffuseShaded.x if (compTest.x) else shadingAdjustment.x; #370 hlslcc_movcTemp.y = diffuseShaded.y if (compTest.y) else shadingAdjustment.y; #371 hlslcc_movcTemp.z = diffuseShaded.z if (compTest.z) else shadingAdjustment.z; #372 shadingAdjustment = (hlslcc_movcTemp).saturate(); #374 the lerp result (and shadowCol) is going to be this because shadowColor's alpha is always 1 making shadowCol 1 diffuseShadow = diffuse * shadingAdjustment; # lightCol is constant [1.0656, 1.0656, 1.0656, 1] calculated from the custom ambient of [0.666, 0.666, 0.666, 1] and sun light color [0.666, 0.666, 0.666, 1], # so ambientCol always results in lightCol after the max function ambientCol = float4(1.0656, 1.0656, 1.0656, 1); diffuseShadow = diffuseShadow * ambientCol; return {'r':diffuseShadow.x, 'g':diffuseShadow.y, 'b':diffuseShadow.z, 'a':1} @staticmethod def create_darktex(maintex: bpy.types.Image, shadow_color: float) -> bpy.types.Image: '''#accepts a bpy image and creates a dark alternate using a modified version of the darkening code above. Returns a new bpy image''' if not os.path.isfile(bpy.context.scene.kkbp.import_dir + '/dark_files/' + maintex.name[:-6] + 'DT.png'): ok = time.time() image_array = numpy.asarray(maintex.pixels,dtype=modify_material.np_number_precision) image_length = len(image_array) image_row_length = int(image_length/4) image_array = image_array.reshape((image_row_length, 4)) ################### variable setup _ambientshadowG = numpy.asarray([0.15, 0.15, 0.15, 0.15],dtype=modify_material.np_number_precision) #constant from experimentation diffuse = image_array #maintex color _ShadowColor = numpy.asarray([shadow_color['r'],shadow_color['g'],shadow_color['b'], 1],dtype=modify_material.np_number_precision) #the shadow color from material editor ########################## #start at line 344 because the other one is for outlines #shadingAdjustment = ShadeAdjustItemNumpy(diffuse, _ShadowColor) #start at line 63 x=0;y=1;z=2;w=3; t0 = diffuse t1 = t0[:, [y, z, z, x]] * _ShadowColor[[y,z,z,x]] t2 = t1[:, [y,x]] t3 = t0[:, [y,z]] * _ShadowColor[[y,z]] + (-t2) tb30 = t2[:, [y]] >= t1[:, [y]] t30 = tb30.astype(int) t2 = numpy.hstack((t2[:, [x,y]], numpy.full((t2.shape[0], 1), -1, t2.dtype), numpy.full((t2.shape[0], 1), 0.666666687, t2.dtype))) t3 = numpy.hstack((t3[:, [x,y]], numpy.full((t3.shape[0], 1), 1, t3.dtype), numpy.full((t3.shape[0], 1), -1, t3.dtype))) t2 = t30 * t3 + t2 tb30 = t1[:, [w]] >= t1[:, [x]] t30 = tb30.astype(int) t1 = numpy.hstack((t2[:, [x, y, w]], t1[:, [w]])) t2 = numpy.hstack((t1[:, [w, y]], t2[:, [z]], t1[:, [x]])) t2 = -t1 + t2 t1 = t30 * t2 + t1 t30 = numpy.minimum(t1[:, [y]], t1[:, [w]]) t30 = -t30 + t1[:, [x]] t2[:, [x]] = t30 * 6 + 1.00000001e-10 t11 = -t1[:, [y]] + t1[:, [w]] t11 = t11 / t2[:, [x]]; t11 = t11 + t1[:, [z]]; t1[:, [x]] = t1[:, [x]] + 1.00000001e-10; t30 = t30 / t1[:, [x]]; t30 = t30 * 0.5; #the w component of t1 is no longer used, so ignore it t1 = numpy.absolute(t11) + numpy.asarray([0.0, -0.333333343, 0.333333343, 1]); #90 t1 = t1 - numpy.floor(t1) t1 = -t1 * 2 + 1 t1 = numpy.absolute(t1) * 3 + (-1) t1 = numpy.clip(t1, 0, 1) t1 = t1 + (-1); #95 t1 = (t30) * t1 + 1; #96 shadingAdjustment = t1 #skip to line 352 diffuseShaded = shadingAdjustment * 0.899999976 - 0.5; diffuseShaded = -diffuseShaded * 2 + 1; compTest = 0.555555582 < shadingAdjustment; shadingAdjustment *= 1.79999995; diffuseShaded = -diffuseShaded * 0.7225 + 1; #invertfinalambient shadow is a constant 0.7225, so don't calc it hlslcc_movcTemp = shadingAdjustment; #reframe ifs as selects hlslcc_movcTemp[:, [x]] = numpy.select(condlist=[compTest[:, [x]], numpy.invert(compTest[:, [x]])], choicelist=[diffuseShaded[:, [x]], shadingAdjustment[:, [x]]]) hlslcc_movcTemp[:, [y]] = numpy.select(condlist=[compTest[:, [y]], numpy.invert(compTest[:, [y]])], choicelist=[diffuseShaded[:, [y]], shadingAdjustment[:, [y]]]) hlslcc_movcTemp[:, [z]] = numpy.select(condlist=[compTest[:, [z]], numpy.invert(compTest[:, [z]])], choicelist=[diffuseShaded[:, [z]], shadingAdjustment[:, [z]]]) shadingAdjustment = numpy.clip(hlslcc_movcTemp, 0, 1) #374 the lerp result (and shadowCol) is going to be this because shadowColor's alpha is always 1 making shadowCol 1 diffuseShadow = diffuse * shadingAdjustment; # lightCol is constant [1.0656, 1.0656, 1.0656, 1] calculated from the custom ambient of [0.666, 0.666, 0.666, 1] and sun light color [0.666, 0.666, 0.666, 1], # so ambientCol always results in lightCol after the max function ambientCol = numpy.asarray([1.0656, 1.0656, 1.0656, 1],dtype=modify_material.np_number_precision); diffuseShadow = diffuseShadow * ambientCol; #make a new image and place the dark pixels into it dark_array = diffuseShadow darktex = bpy.data.images.new(maintex.name[:-7] + '_DT.png', width=maintex.size[0], height=maintex.size[1], alpha = True) darktex.file_format = 'PNG' darktex.pixels = dark_array.ravel() darktex.use_fake_user = True darktex_filename = maintex.filepath_raw[maintex.filepath_raw.find(maintex.name):][:-7]+ '_DT.png' darktex_filepath = bpy.context.scene.kkbp.import_dir + '/dark_files/' + darktex_filename darktex.filepath_raw = darktex_filepath darktex.pack() darktex.save() c.kklog('Created dark version of {} in {} seconds'.format(darktex.name, time.time() - ok)) return darktex else: if bpy.app.version[0] == 3: bpy.ops.image.open(filepath=str(bpy.context.scene.kkbp.import_dir + '/dark_files/' + maintex.name[:-6] + 'DT.png'), use_udim_detecting=False) else: bpy.data.images.load(filepath=str(bpy.context.scene.kkbp.import_dir + '/dark_files/' + maintex.name[:-6] + 'DT.png')) darktex = bpy.data.images[maintex.name[:-6] + 'DT.png'] c.kklog('Loading in existing dark version of {}'.format(darktex.name)) try: darktex.pack() darktex.save() except: c.kklog('This image was not automatically loaded in because its name exceeds 64 characters: ' + darktex.name, type = 'error') return darktex class float4: '''class to mimic part of float4 class in Unity multiplying things per element according to https://github.com/Unity-Technologies/Unity.Mathematics/blob/master/src/Unity.Mathematics/float4.gen.cs#L330 returning things like float.XZW as [Xposition = X, Yposition = Z, Zposition = W] according to https://github.com/Unity-Technologies/Unity.Mathematics/blob/master/src/Unity.Mathematics/float4.gen.cs#L3056 using the variable order x, y, z, w according to https://github.com/Unity-Technologies/Unity.Mathematics/blob/master/src/Unity.Mathematics/float4.gen.cs#L42''' def __init__(self, x = None, y = None, z = None, w = None): self.x = x self.y = y self.z = z self.w = w def __mul__ (self, vector): #if a float4, multiply piece by piece, else multiply full vector if type(vector) in [float, int]: vector = float4(vector, vector, vector, vector) x = self.x * vector.x if self.get('x') != None else None y = self.y * vector.y if self.get('y') != None else None z = self.z * vector.z if self.get('z') != None else None w = self.w * vector.w if self.get('w') != None else None return float4(x,y,z,w) __rmul__ = __mul__ def __add__ (self, vector): #if a float4, add piece by piece, else add full vector if type(vector) in [float, int]: vector = float4(vector, vector, vector, vector) x = self.x + vector.x if self.get('x') != None else None y = self.y + vector.y if self.get('y') != None else None z = self.z + vector.z if self.get('z') != None else None w = self.w + vector.w if self.get('w') != None else None return float4(x,y,z,w) __radd__ = __add__ def __sub__ (self, vector): #if a float4, subtract piece by piece, else subtract full vector if type(vector) in [float, int]: vector = float4(vector, vector, vector, vector) x = self.x - vector.x if self.get('x') != None else None y = self.y - vector.y if self.get('y') != None else None z = self.z - vector.z if self.get('z') != None else None w = self.w - vector.w if self.get('w') != None else None return float4(x,y,z,w) __rsub__ = __sub__ def __gt__ (self, vector): #if a float4, compare piece by piece, else compare full vector if type(vector) in [float, int]: vector = float4(vector, vector, vector, vector) x = self.x > vector.x if self.get('x') != None else None y = self.y > vector.y if self.get('y') != None else None z = self.z > vector.z if self.get('z') != None else None w = self.w > vector.w if self.get('w') != None else None return float4(x,y,z,w) def __neg__ (self): x = -self.x if self.get('x') != None else None y = -self.y if self.get('y') != None else None z = -self.z if self.get('z') != None else None w = -self.w if self.get('w') != None else None return float4(x,y,z,w) def frac(self): x = self.x - math.floor (self.x) if self.get('x') != None else None y = self.y - math.floor (self.y) if self.get('y') != None else None z = self.z - math.floor (self.z) if self.get('z') != None else None w = self.w - math.floor (self.w) if self.get('w') != None else None return float4(x,y,z,w) def abs(self): x = abs(self.x) if self.get('x') != None else None y = abs(self.y) if self.get('y') != None else None z = abs(self.z) if self.get('z') != None else None w = abs(self.w) if self.get('w') != None else None return float4(x,y,z,w) def clamp(self): x = (0 if self.x < 0 else 1 if self.x > 1 else self.x) if self.get('x') != None else None y = (0 if self.y < 0 else 1 if self.y > 1 else self.y) if self.get('y') != None else None z = (0 if self.z < 0 else 1 if self.z > 1 else self.z) if self.get('z') != None else None w = (0 if self.w < 0 else 1 if self.w > 1 else self.w) if self.get('w') != None else None return float4(x,y,z,w) saturate = clamp def clamphalf(self): x = (0 if self.x < 0 else .5 if self.x > .5 else self.x) if self.get('x') != None else None y = (0 if self.y < 0 else .5 if self.y > .5 else self.y) if self.get('y') != None else None z = (0 if self.z < 0 else .5 if self.z > .5 else self.z) if self.get('z') != None else None w = (0 if self.w < 0 else .5 if self.w > .5 else self.w) if self.get('w') != None else None return float4(x,y,z,w) def get(self, var): if hasattr(self, var): return getattr(self, var) else: return None def __str__(self): return str([self.x, self.y, self.z, self.w]) __repr__ = __str__