diff --git a/addon/io_scs_tools/__init__.py b/addon/io_scs_tools/__init__.py index 72018b7..e0df0a6 100644 --- a/addon/io_scs_tools/__init__.py +++ b/addon/io_scs_tools/__init__.py @@ -22,7 +22,7 @@ "name": "SCS Tools", "description": "Setup models, Import-Export SCS data format", "author": "Simon Lusenc (50keda), Milos Zajic (4museman)", - "version": (2, 4, "1909305e"), + "version": (2, 4, "aeadde03"), "blender": (3, 2, 0), "location": "File > Import-Export", "doc_url": "http://modding.scssoft.com/wiki/Documentation/Tools/SCS_Blender_Tools", diff --git a/addon/io_scs_tools/exp/pim/piece.py b/addon/io_scs_tools/exp/pim/piece.py index 3ba2431..30653a2 100644 --- a/addon/io_scs_tools/exp/pim/piece.py +++ b/addon/io_scs_tools/exp/pim/piece.py @@ -74,7 +74,7 @@ def __calc_vertex_hash(index, normal, uvs, rgba, tangent): :return: calculated vertex hash :rtype: str """ - fprec = 10 * 4 + fprec = 10 ** 4 if tangent: vertex_hash = (index, diff --git a/addon/io_scs_tools/internals/containers/parsers/sii.py b/addon/io_scs_tools/internals/containers/parsers/sii.py index 9b956c5..be49008 100644 --- a/addon/io_scs_tools/internals/containers/parsers/sii.py +++ b/addon/io_scs_tools/internals/containers/parsers/sii.py @@ -108,7 +108,7 @@ def parse_token(self): else: - lprint("W No included SII file found, ignoring include: %r\n\t from: %r", + lprint("D No included SII file found, ignoring include: %r\n\t from: %r", (match.group(1), self.filepath)) new_array = self.input[:self.current_line] diff --git a/addon/io_scs_tools/operators/mesh.py b/addon/io_scs_tools/operators/mesh.py index d7676a3..b6ec3cc 100644 --- a/addon/io_scs_tools/operators/mesh.py +++ b/addon/io_scs_tools/operators/mesh.py @@ -416,14 +416,15 @@ def initialize(self, context): vcolor = mesh.color_attributes.new(name=layer_name, type='FLOAT_COLOR', domain='CORNER') buffer = None + color = list(Color((0.5,) * 3).from_srgb_to_scene_linear()) + [1.0, ] # our default is 0.5, even for alpha where 0.5 is also max if layer_name == _VCT_consts.ColoringLayersTypes.Color: - buffer = numpy.array([0.5] * (len(mesh.loops) * 4)) + buffer = numpy.array(color * len(mesh.loops)) elif layer_name == _VCT_consts.ColoringLayersTypes.Decal: - buffer = numpy.array([1.0] * (len(mesh.loops) * 4)) + buffer = numpy.array(color * len(mesh.loops)) elif layer_name == _VCT_consts.ColoringLayersTypes.AO: - buffer = numpy.array([0.5] * (len(mesh.loops) * 4)) + buffer = numpy.array(color * len(mesh.loops)) elif layer_name == _VCT_consts.ColoringLayersTypes.AO2: - buffer = numpy.array([0.5] * (len(mesh.loops) * 4)) + buffer = numpy.array(color * len(mesh.loops)) if buffer is not None: vcolor.data.foreach_set("color", buffer) @@ -534,6 +535,19 @@ def execute(self, context): if VertexColorTools.SCS_TOOLS_OT_StartVColoring.__static_is_active: return {'CANCELLED'} + # ensure our output layers definition + # + # NOTE: here is the deal: when switching to vertex paint mode + # blender creates first color attribute if none is present. + # As it happens it's name is the same as we use, so to ensure it creates proper + # color type and domain we rather create our output layers beforehand. + mesh_vcolors = context.active_object.data.color_attributes + if _MESH_consts.default_vcol not in mesh_vcolors: + mesh_vcolors.new(name=_MESH_consts.default_vcol, type='FLOAT_COLOR', domain='CORNER') + + if _MESH_consts.default_vcol + _MESH_consts.vcol_a_suffix not in mesh_vcolors: + mesh_vcolors.new(name=_MESH_consts.default_vcol + _MESH_consts.vcol_a_suffix, type='FLOAT_COLOR', domain='CORNER') + bpy.ops.object.mode_set(mode="VERTEX_PAINT") # NOTE: We have to push undo event otherwise undo was just @@ -629,7 +643,8 @@ def execute(self, context): lprint(message) self.report({'INFO'}, message[2:]) - _view3d_utils.tag_redraw_all_view3d() # trigger view update to see rebaked colors + # trigger view update to see rebaked colors, with fake reassignment. + mesh.color_attributes.active_color = mesh.color_attributes.active_color else: diff --git a/addon/io_scs_tools/operators/scene.py b/addon/io_scs_tools/operators/scene.py index 67200c9..fb03e18 100644 --- a/addon/io_scs_tools/operators/scene.py +++ b/addon/io_scs_tools/operators/scene.py @@ -1798,8 +1798,9 @@ def execute(self, context): target_dirpath = os.path.join(vehicle_accessory_def_dirpath, upgrade_type) - curr_models = self.gather_model_paths(target_dirpath, "accessory_addon_data", ("exterior_model",)) - self.update_model_paths_dict(upgrade_model_paths[upgrade_type], curr_models) + for unit_type in ("accessory_addon_data", "accessory_addon_tank_data"): + curr_models = self.gather_model_paths(target_dirpath, unit_type, ("exterior_model",)) + self.update_model_paths_dict(upgrade_model_paths[upgrade_type], curr_models) if len(upgrade_model_paths[upgrade_type]) <= 0: # if no models for upgrade, remove set also del upgrade_model_paths[upgrade_type] @@ -1980,6 +1981,10 @@ class SCS_TOOLS_OT_ExportPaintjobUVLayoutAndMesh(bpy.types.Operator): default=True ) + @staticmethod + def is_out_of_uv_bounds(uv): + return uv[0] > 1 or uv[0] < 0 or uv[1] > 1 or uv[1] < 0 + @staticmethod def transform_uvs(obj, texture_portion): """Transform paintjob uvs on given object by data from texture portion. @@ -2015,58 +2020,6 @@ def transform_uvs(obj, texture_portion): bm.to_mesh(obj.data) bm.free() - @staticmethod - def check_and_prepare_uvs(obj, orig_uvs_name_2nd, orig_uvs_name_3rd, export_2nd_uvs, export_3rd_uvs): - """Check and prepare uvs for export by creating a copy of the ones used in SCS truckpaint material. - - :param obj: Blender mesh object to be transformed - :type obj: bpy.types.Object - :param orig_uvs_name_2nd: name of the uv used in first paintjob slot from truckpaint material - :type orig_uvs_name_2nd: str - :param orig_uvs_name_3rd: name of the uv used in second paintjob slot from truckpaint material - :type orig_uvs_name_3rd: str - :param export_2nd_uvs: is 2nd uv layer used for export? - :type export_2nd_uvs: bool - :param export_3rd_uvs: is 3rd uv layer used for export? - :type export_3rd_uvs: bool - :return: True if check is successful and none of the UVs is out of bounds; False otherwise - :rtype: bool - """ - - invalid_uv = False - - bm = bmesh.new() - bm.from_mesh(obj.data) - - uv_lay_2nd = bm.loops.layers.uv[orig_uvs_name_2nd] - uv_lay_3rd = bm.loops.layers.uv[orig_uvs_name_3rd] - - # check for validity - for face in bm.faces: - for loop in face.loops: - if export_2nd_uvs and (1 < loop[uv_lay_2nd].uv[0] < 0 or 1 < loop[uv_lay_2nd].uv[1] < 0): - invalid_uv = True - break - if export_3rd_uvs and (1 < loop[uv_lay_3rd].uv[0] < 0 or 1 < loop[uv_lay_3rd].uv[1] < 0): - invalid_uv = True - break - - if invalid_uv: - break - - # copy over if valid - if not invalid_uv: - bm.loops.layers.uv.new(_PT_consts.uvs_name_2nd) - bm.loops.layers.uv[_PT_consts.uvs_name_2nd].copy_from(uv_lay_2nd) - - bm.loops.layers.uv.new(_PT_consts.uvs_name_3rd) - bm.loops.layers.uv[_PT_consts.uvs_name_3rd].copy_from(uv_lay_3rd) - - bm.to_mesh(obj.data) - bm.free() - - return not invalid_uv - @staticmethod def cleanup(*args): """Interprets given argumens as iterables holding blender object that shall be cleaned aka removed from datablocks. @@ -2138,6 +2091,53 @@ def do_report(self, type, message, do_report=False): for line in message.split("\n"): self.report(type, line.replace("\t", "").replace(" ", " ")) + def check_and_prepare_uvs(self, obj, orig_uvs_name_2nd, orig_uvs_name_3rd): + """Check and prepare uvs for export by creating a copy of the ones used in SCS truckpaint material. + + :param obj: Blender mesh object to be transformed + :type obj: bpy.types.Object + :param orig_uvs_name_2nd: name of the uv used in first paintjob slot from truckpaint material + :type orig_uvs_name_2nd: str + :param orig_uvs_name_3rd: name of the uv used in second paintjob slot from truckpaint material + :type orig_uvs_name_3rd: str + :return: True if check is successful and none of the UVs is out of bounds; False otherwise + :rtype: bool + """ + + invalid_uv = False + + bm = bmesh.new() + bm.from_mesh(obj.data) + + uv_lay_2nd = bm.loops.layers.uv[orig_uvs_name_2nd] + uv_lay_3rd = bm.loops.layers.uv[orig_uvs_name_3rd] + + # check for validity + for face in bm.faces: + for loop in face.loops: + if self.export_2nd_uvs and self.is_out_of_uv_bounds(loop[uv_lay_2nd].uv): + invalid_uv = True + break + if self.export_3rd_uvs and self.is_out_of_uv_bounds(loop[uv_lay_3rd].uv): + invalid_uv = True + break + + if invalid_uv: + break + + # copy over if valid + if not invalid_uv: + bm.loops.layers.uv.new(_PT_consts.uvs_name_2nd) + bm.loops.layers.uv[_PT_consts.uvs_name_2nd].copy_from(uv_lay_2nd) + + bm.loops.layers.uv.new(_PT_consts.uvs_name_3rd) + bm.loops.layers.uv[_PT_consts.uvs_name_3rd].copy_from(uv_lay_3rd) + + bm.to_mesh(obj.data) + bm.free() + + return not invalid_uv + def execute(self, context): ################################## @@ -2215,15 +2215,16 @@ def execute(self, context): else: self.do_report({'WARNING'}, "Collection %r won't be exported as some objects with truckpaint material are missing tex_coords!" % - collection.name, - do_report=True) + collection.name) + continue else: uvs_2nd_name = curr_truckpaint_mat.scs_props.shader_texture_base_uv[1].value uvs_3rd_name = curr_truckpaint_mat.scs_props.shader_texture_base_uv[2].value - if not self.check_and_prepare_uvs(curr_merged_object, uvs_2nd_name, uvs_3rd_name, self.export_2nd_uvs, self.export_3rd_uvs): + + if not self.check_and_prepare_uvs(curr_merged_object, uvs_2nd_name, uvs_3rd_name): self.do_report({'WARNING'}, - "Collection %r won't be exported as one or more objects have UVs out of <0;1> bounds!" % collection.name, - do_report=True) + "Collection %r won't be exported as one or more objects have UVs out of <0;1> bounds!" % + collection.name) self.cleanup((curr_merged_object,)) continue @@ -2241,7 +2242,7 @@ def execute(self, context): merged_objects_to_export[curr_merged_object] = collection if len(merged_objects_to_export) <= 0: - self.do_report({'ERROR'}, "No objects to export!") + self.do_report({'ERROR'}, "No objects to export!", do_report=True) return {'CANCELLED'} lprint("S Merged objects to export: %s", (merged_objects_to_export.values(),)) @@ -2259,7 +2260,7 @@ def execute(self, context): unit_type="paintjobs_metadata", req_props=("common_texture_size",)): - self.do_report({'ERROR'}, "Validation failed on SII: %r" % _path_utils.readable_norm(self.config_meta_filepath)) + self.do_report({'ERROR'}, "Validation failed on SII: %r" % _path_utils.readable_norm(self.config_meta_filepath), do_report=True) self.cleanup(merged_objects_to_export.keys()) return {'CANCELLED'} @@ -2286,6 +2287,25 @@ def execute(self, context): "is not defined in paintjob layout meta data!" % (unit_id, parent)) continue + position = texture_portion.get_prop("position") + size = texture_portion.get_prop("size") + if position is not None and size is not None: + position = [float(x) for x in position] + size = [float(x) for x in size] + if self.is_out_of_uv_bounds(position): + self.do_report({'WARNING'}, + "Ignoring used texture portion with name %r as it's position is not within (0,1) bounds: %r" % + (unit_id, position)) + continue + else: + position_size = (position[0] + size[0], position[1] + size[1]) + if self.is_out_of_uv_bounds(position_size): + self.do_report({'WARNING'}, + "Ignoring used texture portion with name %r as it's calculated " + "position+size is not within (0,1) bounds: %r" % + (unit_id, position_size)) + continue + texture_portions[unit_id] = texture_portion # do model sii validation @@ -2508,7 +2528,7 @@ def execute(self, context): start_time = time() # intialize pixel values for id mask texture - img_pixels = [0.0] * common_texture_size[0] * common_texture_size[1] * 4 + img_pixels = numpy.zeros((common_texture_size[1], common_texture_size[0], 4), numpy.float16) id_mask_color_idx = 0 for unit_id in texture_portions: @@ -2533,26 +2553,16 @@ def execute(self, context): portion_col[2] /= 255.0 portion_col.append(self.id_mask_alpha) - # define array buffers for application of id masking color - img_pixels_buffer = numpy.array([0.0] * 4 * portion_width) - portion_col_buffer = numpy.array(portion_col * portion_width) - - # write proper pixels row by row - for row_i in range(0, portion_height): - - start_px = (row_i + portion_pos_y) * common_texture_size[0] * 4 + portion_pos_x * 4 - end_px = start_px + portion_width * 4 - - img_pixels_buffer[:] = img_pixels[start_px:end_px] - img_pixels_buffer += portion_col_buffer - - img_pixels[start_px:end_px] = img_pixels_buffer + img_pixels[portion_pos_y:portion_pos_y + portion_height, portion_pos_x:portion_pos_x + portion_width, 0] = portion_col[0] + img_pixels[portion_pos_y:portion_pos_y + portion_height, portion_pos_x:portion_pos_x + portion_width, 1] = portion_col[1] + img_pixels[portion_pos_y:portion_pos_y + portion_height, portion_pos_x:portion_pos_x + portion_width, 2] = portion_col[2] + img_pixels[portion_pos_y:portion_pos_y + portion_height, portion_pos_x:portion_pos_x + portion_width, 3] = portion_col[3] # create image data block img = bpy.data.images.new("tmp_img", common_texture_size[0], common_texture_size[1], alpha=True) img.colorspace_settings.name = "sRGB" # make sure we use sRGB color-profile img.alpha_mode = 'CHANNEL_PACKED' - img.pixels[:] = img_pixels + img.pixels[:] = img_pixels.reshape((common_texture_size[1] * common_texture_size[0] * 4,)) # save scene = bpy.context.scene @@ -2770,6 +2780,7 @@ class ColorVariantItem(bpy.types.PropertyGroup): pjs_flake_noise: StringProperty(default="/material/custom/flake_noise.tobj") pjs_alternate_uvset: BoolProperty(default=False) + pjs_alternate_flipflake_uvset: BoolProperty(default=False) pjs_flipflake: BoolProperty(default=False) pjs_airbrush: BoolProperty(default=False) pjs_stock: BoolProperty(default=False) @@ -3162,6 +3173,9 @@ def execute(self, context): start_time = time() + # clear message queue so that we will report only warnings and errors from this export! + lprint("", report_warnings=1, report_errors=1) + ################################## # # 1. parse & validate input settings @@ -3169,7 +3183,7 @@ def execute(self, context): ################################## if not os.path.isfile(self.config_meta_filepath): - self.do_report({'WARNING'}, "Given paintjob layout META file does not exist: %r!" % self.config_meta_filepath) + self.do_report({'WARNING'}, "Given paintjob layout META file does not exist: %r!" % self.config_meta_filepath, do_report=True) return {'CANCELLED'} # get vehicle brand model token @@ -3183,15 +3197,15 @@ def execute(self, context): elif os.path.basename(curr_dir) == _PT_consts.VehicleTypes.TRAILER: self.vehicle_type = _PT_consts.VehicleTypes.TRAILER else: - self.do_report({'ERROR'}, "Given paintjob layout META file is in wrong directory!") + self.do_report({'ERROR'}, "Given paintjob layout META file is in wrong directory!", do_report=True) return {'CANCELLED'} if not self.common_texture_path.endswith(".tif"): - self.do_report({'ERROR'}, "Given common texture is not TIF file: %r!" % self.common_texture_path) + self.do_report({'ERROR'}, "Given common texture is not TIF file: %r!" % self.common_texture_path, do_report=True) return {'CANCELLED'} if not os.path.isfile(self.common_texture_path): - self.do_report({'ERROR'}, "Given common texture file does not exist: %r!" % self.common_texture_path) + self.do_report({'ERROR'}, "Given common texture file does not exist: %r!" % self.common_texture_path, do_report=True) return {'CANCELLED'} # solve project path, if not given try to get it from given common texture path @@ -3200,12 +3214,13 @@ def execute(self, context): orig_project_path = _path_utils.readable_norm(self.project_path) if not os.path.isdir(orig_project_path): - self.do_report({'ERROR'}, "Given paintjob project path does not exist: %r!" % orig_project_path) + self.do_report({'ERROR'}, "Given paintjob project path does not exist: %r!" % orig_project_path, do_report=True) return {'CANCELLED'} # there has to be sibling base directory, otherwise we for sure aren't in right place if not os.path.isdir(os.path.join(os.path.join(orig_project_path, os.pardir), "base")): - self.do_report({'ERROR'}, "Given pointjob project path is invalid, can't find sibling 'base' project: %r" % orig_project_path) + self.do_report({'ERROR'}, "Given pointjob project path is invalid, can't find sibling 'base' project: %r" % orig_project_path, + do_report=True) return {'CANCELLED'} else: @@ -3217,9 +3232,11 @@ def execute(self, context): orig_project_path = _path_utils.readable_norm(os.path.join(orig_project_path, os.pardir)) if not os.path.isdir(orig_project_path): - self.do_report({'ERROR'}, "Paintjob TGA seems to be saved outside proper structure, should be inside\n" - "'/vehicle//upgrade/paintjob//', instead is in:\n" - "%r" % self.common_texture_path) + self.do_report({'ERROR'}, + "Paintjob TGA seems to be saved outside proper structure, should be inside\n" + "'/vehicle//upgrade/paintjob//', instead is in:\n" + "%r" % self.common_texture_path, + do_report=True) return {'CANCELLED'} # get paint job token from texture name @@ -3228,7 +3245,8 @@ def execute(self, context): if _name_utils.tokenize_name(pj_token) != pj_token: self.do_report({'ERROR'}, "Given common texture name is invalid, can't be tokenized (max. length: 11, accepted chars: a-z, 0-9, _): %r" - % pj_token) + % pj_token, + do_report=True) return {'CANCELLED'} # get brand & model unit name from texture path @@ -3239,7 +3257,8 @@ def execute(self, context): if underscore_idx == -1: self.do_report({'ERROR'}, "Paintjob TGA file parent directory name seems to be invalid should be '' instead is: %r." % - brand_model_dir) + brand_model_dir, + do_report=True) return {'CANCELLED'} brand_token = brand_model_dir[0:underscore_idx] @@ -3251,9 +3270,11 @@ def execute(self, context): ) if is_common_tex_path_invalid: - self.do_report({'ERROR'}, "Paintjob TGA file isn't saved on correct place, should be inside\n" - "'/vehicle//upgrade/paintjob/%s' instead is saved in:\n" - "%r." % (brand_model_token.replace(".", "_"), common_tex_dirpath)) + self.do_report({'ERROR'}, + "Paintjob TGA file isn't saved on correct place, should be inside\n" + "'/vehicle//upgrade/paintjob/%s' instead is saved in:\n" + "%r." % (brand_model_token.replace(".", "_"), common_tex_dirpath), + do_report=True) return {'CANCELLED'} lprint("D : %r, : %r, : %r" % (brand_token, model_token, pj_token)) @@ -3269,7 +3290,7 @@ def execute(self, context): unit_type="paintjobs_metadata", req_props=("common_texture_size",)): - self.do_report({'ERROR'}, "Validation failed on SII: %r" % _path_utils.readable_norm(self.config_meta_filepath)) + self.do_report({'ERROR'}, "Validation failed on SII: %r" % _path_utils.readable_norm(self.config_meta_filepath), do_report=True) return {'CANCELLED'} # interpret common texture size vector as two ints @@ -3314,8 +3335,10 @@ def execute(self, context): # check unique suffixes if master_unit_suffix in master_unit_suffixes: - self.do_report({'ERROR'}, "Multiple master textures using same unit suffix: %r. " - "Make sure all unit suffixes are unique." % master_unit_suffix) + self.do_report({'ERROR'}, + "Multiple master textures using same unit suffix: %r. " + "Make sure all unit suffixes are unique." % master_unit_suffix, + do_report=True) return {'CANCELLED'} # check for no model sii definition @@ -3328,8 +3351,10 @@ def execute(self, context): master_portions.append(texture_portion) if no_model_sii_master_count > 0 and (no_model_sii_master_count + model_sii_master_count) > 1: - self.do_report({'ERROR'}, "One or more master texture portions detected without model SII path. " - "Either define model SII path for all of them or use only one master portion without it!") + self.do_report({'ERROR'}, + "One or more master texture portions detected without model SII path. " + "Either define model SII path for all of them or use only one master portion without it!", + do_report=True) return {'CANCELLED'} lprint("D Found texture portions: %r", (list(texture_portions.keys()),)) @@ -3347,8 +3372,10 @@ def execute(self, context): self.initialize_nodes(context, common_tex_img) if tuple(common_tex_img.size) != tuple(common_texture_size) and not self.export_configs_only: - self.do_report({'ERROR'}, "Wrong size of common texture TGA: [%s, %s], paintjob layout META is prescribing different size: %r!" - % (common_tex_img.size[0], common_tex_img.size[1], common_texture_size)) + self.do_report({'ERROR'}, + "Wrong size of common texture TGA: [%s, %s], paintjob layout META is prescribing different size: %r!" % + (common_tex_img.size[0], common_tex_img.size[1], common_texture_size), + do_report=True) return {'CANCELLED'} # get textures export dir @@ -3450,7 +3477,10 @@ def execute(self, context): break if not sii_exists and requires_valid_model_sii: - lprint("E Can't find referenced 'model_sii' file for texture portion %r, aborting overrides SII write!", (texture_portion.id,)) + self.do_report({'ERROR'}, + "Can't find referenced 'model_sii' file for texture portion %r, aborting overrides SII write!" % + texture_portion.id, + do_report=True) return {'CANCELLED'} # assamble paintjob properties that will be written in overrides SII (currently: paint_job_mask, flake_uvscale, flake_vratio) @@ -3468,15 +3498,19 @@ def execute(self, context): master_unit_suffix = texture_portion.get_prop("master_unit_suffix", "") suffixed_pj_unit_name = pj_token + master_unit_suffix if _name_utils.tokenize_name(suffixed_pj_unit_name) != suffixed_pj_unit_name: - lprint("E Can't tokenize generated paintjob unit name: %r for texture portion %r, aborting SII write!", - (suffixed_pj_unit_name, texture_portion.id)) + self.do_report({'ERROR'}, + "Can't tokenize generated paintjob unit name: %r for texture portion %r, aborting SII write!" % + (suffixed_pj_unit_name, texture_portion.id), + do_report=True) return {'CANCELLED'} # get model sii unit name to use it in suitable for field model_sii_cont = _sii_container.get_data_from_file(model_sii_path) if not model_sii_cont and requires_valid_model_sii: - lprint("E SII is there but getting unit name from 'model_sii' failed for texture portion %r, aborting SII write!", - (texture_portion.id,)) + self.do_report({'ERROR'}, + "SII is there but getting unit name from 'model_sii' failed for texture portion %r, aborting SII write!" % + texture_portion.id, + do_report=True) return {'CANCELLED'} # unit name of referenced model sii used for suitable_for field in master paint jobs @@ -3544,9 +3578,11 @@ def execute(self, context): else: - lprint("E Can not collect override for texture portion: %r, as 'model_sii' property is not one of: " - "accessory, cabin or chassis neither is texture portion marked with 'is_master'!", - (texture_portion.id,)) + self.do_report({'ERROR'}, + "Can not collect override for texture portion: %r, as 'model_sii' property is not one of: " + "accessory, cabin or chassis neither is texture portion marked with 'is_master'!" % + texture_portion.id, + do_report=True) return {'CANCELLED'} # export overrides SII files @@ -3558,8 +3594,7 @@ def execute(self, context): if not self.preserve_common_texture and os.path.isfile(self.common_texture_path): os.remove(self.common_texture_path) - lprint("\nI Export of paintjobs took: %0.3f sec" % (time() - start_time)) - + self.do_report({'INFO'}, "Export of paintjobs took: %0.3f sec" % (time() - start_time), do_report=True) return {'FINISHED'} diff --git a/addon/io_scs_tools/properties/object.py b/addon/io_scs_tools/properties/object.py index 6af2a94..606c9d3 100644 --- a/addon/io_scs_tools/properties/object.py +++ b/addon/io_scs_tools/properties/object.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2021: SCS Software +# Copyright (C) 2013-2022: SCS Software import bpy from bpy.props import (StringProperty, @@ -293,6 +293,10 @@ def update_empty_object_type(self, context): obj.empty_display_size = 5.0 obj.empty_display_type = "ARROWS" obj.show_name = True + + # ensure default part + part_inventory = obj.scs_object_part_inventory + _inventory.add_item(part_inventory, _PART_consts.default_name, conditional=True) else: obj.empty_display_size = 1.0 obj.empty_display_type = "PLAIN_AXES" diff --git a/addon/io_scs_tools/utils/convert.py b/addon/io_scs_tools/utils/convert.py index 7f0c0f7..00f86b4 100644 --- a/addon/io_scs_tools/utils/convert.py +++ b/addon/io_scs_tools/utils/convert.py @@ -16,11 +16,12 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2021: SCS Software +# Copyright (C) 2013-2022: SCS Software import struct import math import re +from numpy import vectorize from mathutils import Matrix, Quaternion, Vector, Color from io_scs_tools.consts import Colors as _COL_consts from io_scs_tools.utils.printout import lprint @@ -32,9 +33,44 @@ _BYTE_STRUCT = struct.Struct(">I") +def __linear_to_srgb(x): + """Converts value from linear to srgb + NOTE: taken from game and blender code + + :param x: value to convert + :type x: float + """ + + if x <= 0.0031308: + return 12.92 * x + else: + return (x ** (1.0 / 2.4)) * 1.055 - 0.055 + + +def __srgb_to_linear(x): + """Converts value from linear to srgb + NOTE: taken from game and blender code + + :param x: value to convert + :type x: float + """ + + if x <= 0.04045: + return x / 12.92 + else: + return ((x + 0.055) / 1.055) ** 2.4 + + +np_linear_to_srgb = vectorize(__linear_to_srgb) +"""Vectorizes linear to srgb function to be used with numpy arrays.""" + + +np_srgb_to_linear = vectorize(__srgb_to_linear) +"""Vectorizes srgb to linear function to be used with numpy arrays.""" + + def linear_to_srgb(value): """Converts linear color to srgb colorspace. Function can convert single float or list of floats. - NOTE: taken from game code :param value: list of floats or float :type value: float | collections.Iterable[float] @@ -44,26 +80,13 @@ def linear_to_srgb(value): is_float = isinstance(value, float) if is_float: - vals = [value] - else: - vals = list(value) - - for i, v in enumerate(vals): - if v <= 0.0031308: - vals[i] = 12.92 * v - else: - a = 0.055 - vals[i] = (v ** (1.0 / 2.4) * (1.0 + a)) - a - - if is_float: - return vals[0] + return __linear_to_srgb(value) else: - return vals + return [__linear_to_srgb(v) for v in list(value)] def srgb_to_linear(value): """Converts srgb color to linear colorspace. Function can convert single float or list of floats. - NOTE: taken from game code :param value: list of floats or float :type value: float | collections.Iterable[float] @@ -73,22 +96,9 @@ def srgb_to_linear(value): is_float = isinstance(value, float) if is_float: - vals = [value] - else: - vals = list(value) - - for i, v in enumerate(vals): - - if v <= 0.04045: - vals[i] = v / 12.92 - else: - a = 0.055 - vals[i] = ((v + a) / (1.0 + a)) ** 2.4 - - if is_float: - return vals[0] + return __srgb_to_linear(value) else: - return vals + return [__srgb_to_linear(v) for v in list(value)] def pre_gamma_corrected_col(color): diff --git a/addon/io_scs_tools/utils/mesh.py b/addon/io_scs_tools/utils/mesh.py index 50a1baa..26c4537 100644 --- a/addon/io_scs_tools/utils/mesh.py +++ b/addon/io_scs_tools/utils/mesh.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2019: SCS Software +# Copyright (C) 2013-2022: SCS Software import bpy import bmesh @@ -386,13 +386,14 @@ def vcoloring_rebake(mesh, vcolor_arrays, old_array_hash): mesh_vcolors.new(name=_MESH_consts.default_vcol + _MESH_consts.vcol_a_suffix, type='FLOAT_COLOR', domain='CORNER') # get vertex color data for hash calculation - if mesh_vcolors.active.name == _VCT_consts.ColoringLayersTypes.Color: + active_color_name = mesh_vcolors.active_color.name + if active_color_name == _VCT_consts.ColoringLayersTypes.Color: color_loops.foreach_get("color", vcolor_arrays[0]) - elif mesh_vcolors.active.name == _VCT_consts.ColoringLayersTypes.Decal: + elif active_color_name == _VCT_consts.ColoringLayersTypes.Decal: decal_loops.foreach_get("color", vcolor_arrays[0]) - elif mesh_vcolors.active.name == _VCT_consts.ColoringLayersTypes.AO: + elif active_color_name == _VCT_consts.ColoringLayersTypes.AO: ao_loops.foreach_get("color", vcolor_arrays[0]) - elif mesh_vcolors.active.name == _VCT_consts.ColoringLayersTypes.AO2: + elif active_color_name == _VCT_consts.ColoringLayersTypes.AO2: ao2_loops.foreach_get("color", vcolor_arrays[0]) new_array_hash = hash(vcolor_arrays[0].tobytes()) @@ -406,7 +407,16 @@ def vcoloring_rebake(mesh, vcolor_arrays, old_array_hash): ao_loops.foreach_get("color", vcolor_arrays[2]) ao2_loops.foreach_get("color", vcolor_arrays[3]) + # convert to srgb + for i in (0, 2, 3): + vcolor_arrays[i] = _convert.np_linear_to_srgb(vcolor_arrays[i]) + + # combine vcolor_arrays[0] = vcolor_arrays[0] * vcolor_arrays[2] * vcolor_arrays[3] * 4.0 + + # convert back to scene linear + vcolor_arrays[0] = _convert.np_srgb_to_linear(vcolor_arrays[0]) + # alpha is donated only by decal layer color, thus we just comment it out # vcolor_arrays[1] = vcolor_arrays[1]