From 983ae41be0220dc0e5ca607b8a8ba18224e1c32a Mon Sep 17 00:00:00 2001 From: simon50keda Date: Fri, 22 Feb 2019 18:12:40 +0100 Subject: [PATCH] Release - 1.12 --- README.md | 6 +- addon/io_scs_tools/__init__.py | 2 +- addon/io_scs_tools/consts.py | 106 ++- addon/io_scs_tools/exp/pim/exporter.py | 3 + addon/io_scs_tools/exp/pim_ef/exporter.py | 8 +- addon/io_scs_tools/exp/pim_ef/piece_face.py | 14 +- addon/io_scs_tools/exp/pim_ef/piece_stream.py | 5 +- addon/io_scs_tools/exp/pip/exporter.py | 2 +- addon/io_scs_tools/exp/pit.py | 4 +- addon/io_scs_tools/imp/pim_ef.py | 9 +- .../internals/containers/parsers/sii.py | 27 +- .../io_scs_tools/internals/containers/sii.py | 18 +- addon/io_scs_tools/internals/open_gl/core.py | 38 + .../internals/persistent/file_save.py | 4 + .../internals/preview_models/__init__.py | 16 +- .../internals/shaders/eut2/__init__.py | 4 + .../shaders/eut2/dif_spec_oclu/__init__.py | 67 -- .../shaders/eut2/retroreflective/__init__.py | 110 +++ .../io_scs_tools/internals/shaders/shader.py | 6 +- addon/io_scs_tools/internals/structure.py | 6 +- addon/io_scs_tools/operators/material.py | 4 + addon/io_scs_tools/operators/mesh.py | 19 +- addon/io_scs_tools/operators/object.py | 9 + addon/io_scs_tools/operators/scene.py | 890 +++++++++++++----- addon/io_scs_tools/operators/wm.py | 31 + addon/io_scs_tools/properties/material.py | 8 + addon/io_scs_tools/properties/object.py | 5 +- addon/io_scs_tools/shader_presets.txt | 28 + addon/io_scs_tools/supported_effects.bin | Bin 194087 -> 193943 bytes addon/io_scs_tools/ui/material.py | 2 + addon/io_scs_tools/ui/tool_shelf.py | 2 +- addon/io_scs_tools/utils/__init__.py | 12 + addon/io_scs_tools/utils/curve.py | 3 + addon/io_scs_tools/utils/material.py | 2 +- addon/io_scs_tools/utils/mesh.py | 22 +- addon/io_scs_tools/utils/object.py | 6 +- 36 files changed, 1128 insertions(+), 370 deletions(-) create mode 100644 addon/io_scs_tools/internals/shaders/eut2/retroreflective/__init__.py diff --git a/README.md b/README.md index a147f32..4fd7e7b 100644 --- a/README.md +++ b/README.md @@ -26,14 +26,14 @@ Installation and Usage: Addon is located in "addon/io_scs_tools" folder. Use standard Blender addon installation procedure for installing it. For more information visit wiki at -"https://github.com/SCSSoftware/BlenderTools/wiki". +"http://modding.scssoft.com/wiki/Documentation/Tools/SCS_Blender_Tools". Notes: ------ - In case of trouble installing SCS Blender Tools make sure you're using compatible Blender version. SCS Blender Tools for Blender versions - prior 2.75 are not supported. + prior 2.73 are not supported. Help, questions, troubleshooting: @@ -41,7 +41,7 @@ Help, questions, troubleshooting: If you encounter any problems or have questions regarding SCS Blender Tools, please visit forum at "http://forum.scssoft.com/viewforum.php?f=159" and don't hesitate to ask if your problem wasn't addressed already. Also -don't miss the wiki ("https://github.com/SCSSoftware/BlenderTools/wiki") +don't miss wiki ("http://modding.scssoft.com/wiki/Documentation/Tools/SCS_Blender_Tools") for many useful tips and docs. diff --git a/addon/io_scs_tools/__init__.py b/addon/io_scs_tools/__init__.py index 1ae3f5d..fbf40a1 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": (1, 11, "2032583"), + "version": (1, 12, "be00ed8"), "blender": (2, 78, 0), "location": "File > Import-Export", "wiki_url": "http://modding.scssoft.com/wiki/Documentation/Tools/SCS_Blender_Tools", diff --git a/addon/io_scs_tools/consts.py b/addon/io_scs_tools/consts.py index fe6b07a..a296af4 100644 --- a/addon/io_scs_tools/consts.py +++ b/addon/io_scs_tools/consts.py @@ -88,6 +88,24 @@ class View3DReport: (400, 45), # used when report text is shown (415, 45) # used when report text is hidden (aka condensed mode) ) + SCROLLUP_BTN_AREA = (545, 585, 26, 54) + SCROLLUP_BTN_TEXT = ( + "↑", # used when report text is shown + "" # used when report text is hidden (aka condensed mode) + ) + SCROLLUP_BTN_TEXT_POS = ( + (560, 45), # used when report text is shown + (560, 45) # used when report text is hidden (aka condensed mode) + ) + SCROLLDOWN_BTN_AREA = (585, 625, 26, 54) + SCROLLDOWN_BTN_TEXT = ( + "↓", # used when report text is shown + "" # used when report text is hidden (aka condensed mode) + ) + SCROLLDOWN_BTN_TEXT_POS = ( + (600, 45), # used when report text is shown + (600, 45) # used when report text is hidden (aka condensed mode) + ) class Icons: @@ -354,9 +372,12 @@ class PSP: UNLOAD_HARD_POS = 20 UNLOAD_RIGID_POS = 21 WEIGHT_CAT_POS = 22 + COMPANY_UNLOAD_POS = 23 + TRAILER_SPAWN = 24 + LONG_TRAILER_POS = 25 class TST: - """Constants representing type of traffic semaphores. + """Constants representing type of traffic semaphores. """ PROFILE = 0 MODEL_ONLY = 1 @@ -474,6 +495,13 @@ class PaintjobTools: """Constants for paintjob tools. """ + class VehicleTypes: + """Vehicle types, defining where vehicle will be placed in defs and model paths. + """ + NONE = "none" + TRUCK = "truck" + TRAILER = "trailer_owned" + uvs_name_2nd = "scs_paintjob_2nd" """2nd uvs layer name used during unification on export""" uvs_name_3rd = "scs_paintjob_3rd" @@ -483,3 +511,79 @@ class PaintjobTools: """Name of the property for saving references paths to models inside a group data-block.""" export_tag_obj_name = ".scs_export_group" """Name of the object inside the group which visibility tells us either group should be exported or no.""" + model_variant_prop = ".scs_variant" + """Name of the property for saving variant of the model inside group encapsulating imported paintable model.""" + + id_mask_colors = ( + (51, 0, 0), + (255, 136, 0), + (217, 202, 0), + (134, 179, 140), + (0, 190, 204), + (0, 31, 115), + (117, 70, 140), + (191, 96, 147), + (242, 61, 61), + (127, 68, 0), + (102, 95, 0), + (64, 255, 140), + (0, 204, 255), + (0, 0, 51), + (41, 0, 51), + (204, 0, 82), + (204, 102, 102), + (178, 137, 89), + (173, 179, 89), + (0, 77, 41), + (0, 41, 51), + (108, 108, 217), + (230, 128, 255), + (89, 0, 36), + (230, 172, 172), + (230, 203, 172), + (100, 102, 77), + (48, 191, 124), + (0, 170, 255), + (191, 191, 255), + (83, 0, 89), + (166, 124, 141), + (140, 49, 35), + (128, 113, 96), + (57, 77, 19), + (57, 77, 68), + (64, 106, 128), + (38, 38, 51), + (217, 0, 202), + (127, 0, 34), + (255, 115, 64), + (229, 172, 57), + (234, 255, 191), + (0, 51, 34), + (0, 68, 128), + (34, 0, 255), + (64, 32, 62), + (115, 57, 65), + (76, 34, 19), + (102, 77, 26), + (133, 204, 51), + (0, 255, 238), + (0, 27, 51), + (48, 0, 179), + (255, 191, 251), + (51, 26, 29), + (191, 156, 143), + (51, 38, 13), + (68, 255, 0), + (0, 115, 107), + (153, 180, 204), + (119, 54, 217), + (153, 0, 122), + (204, 112, 51), + (51, 47, 38), + (32, 128, 45), + (143, 191, 188), + (83, 116, 166), + (119, 105, 140), + (255, 64, 166) + ) + """Array of unique colors for building ID mask texture.""" diff --git a/addon/io_scs_tools/exp/pim/exporter.py b/addon/io_scs_tools/exp/pim/exporter.py index 2ae53af..29fa53b 100644 --- a/addon/io_scs_tools/exp/pim/exporter.py +++ b/addon/io_scs_tools/exp/pim/exporter.py @@ -151,6 +151,9 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat for scale_axis in parent.scale: scale_sign *= scale_axis + for scale_axis in parent.delta_scale: + scale_sign *= scale_axis + parent = parent.parent face_flip = scale_sign < 0 diff --git a/addon/io_scs_tools/exp/pim_ef/exporter.py b/addon/io_scs_tools/exp/pim_ef/exporter.py index b31b4a3..f42b90a 100644 --- a/addon/io_scs_tools/exp/pim_ef/exporter.py +++ b/addon/io_scs_tools/exp/pim_ef/exporter.py @@ -125,6 +125,9 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat for scale_axis in parent.scale: scale_sign *= scale_axis + for scale_axis in parent.delta_scale: + scale_sign *= scale_axis + parent = parent.parent winding_order = 1 @@ -268,8 +271,7 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat rgbas.append(vcol) rgbas_names[_MESH_consts.default_vcol] = True - # export rest of the vertex colors too, but do not apply extra multiplies of SCS exporter - # as rest of the layers are just artist layers + # export rest of the vertex colors too (also multiply with 2 and with vcol multiplicator) for vcol_layer in mesh.vertex_colors: # we already computed thoose so ignore them @@ -277,7 +279,7 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat continue color = vcol_layer.data[loop_i].color - vcol = (color[0], color[1], color[2], 1.0) + vcol = (color[0] * 2 * vcol_multi, color[1] * 2 * vcol_multi, color[2] * 2 * vcol_multi) rgbas.append(vcol) rgbas_names[vcol_layer.name] = True diff --git a/addon/io_scs_tools/exp/pim_ef/piece_face.py b/addon/io_scs_tools/exp/pim_ef/piece_face.py index 107d73a..4b455bc 100644 --- a/addon/io_scs_tools/exp/pim_ef/piece_face.py +++ b/addon/io_scs_tools/exp/pim_ef/piece_face.py @@ -102,11 +102,17 @@ def add_rgbas(self, rgbas, rgbas_names): for i, rgba in enumerate(rgbas): - rgba_type = Stream.Types.RGBA + str(i) - if rgba_type not in self.__streams: - self.__streams[rgba_type] = Stream(Stream.Types.RGBA, i, rgbas_names[i]) + if len(rgba) == 3: + stream_type = Stream.Types.RGB + vcol_type = Stream.Types.RGB + str(i) + else: + stream_type = Stream.Types.RGBA + vcol_type = Stream.Types.RGBA + str(i) - stream = self.__streams[rgba_type] + if vcol_type not in self.__streams: + self.__streams[vcol_type] = Stream(stream_type, i, rgbas_names[i]) + + stream = self.__streams[vcol_type] """:type: Stream""" stream.add_entry(rgba) diff --git a/addon/io_scs_tools/exp/pim_ef/piece_stream.py b/addon/io_scs_tools/exp/pim_ef/piece_stream.py index e60a6aa..2c28169 100644 --- a/addon/io_scs_tools/exp/pim_ef/piece_stream.py +++ b/addon/io_scs_tools/exp/pim_ef/piece_stream.py @@ -39,7 +39,10 @@ def __init__(self, stream_type, index, name=""): super().__init__(stream_type, index) # exchange format support multiple vertex color layers - if stream_type == Stream.Types.RGBA: + if stream_type == Stream.Types.RGB: + self.__tag_index = index + self.__format = "FLOAT3" + elif stream_type == Stream.Types.RGBA: self.__tag_index = index self.__format = "FLOAT4" diff --git a/addon/io_scs_tools/exp/pip/exporter.py b/addon/io_scs_tools/exp/pip/exporter.py index d1a94a9..20ff6dd 100644 --- a/addon/io_scs_tools/exp/pip/exporter.py +++ b/addon/io_scs_tools/exp/pip/exporter.py @@ -248,7 +248,7 @@ def execute(dirpath, filename, name_suffix, prefab_locator_list, offset_matrix, curve.set_flags(loc1.scs_props, False) curve.set_semaphore_id(int(loc0_scs_props.locator_prefab_np_traffic_semaphore)) - curve.set_traffic_rule(loc1_scs_props.locator_prefab_np_traffic_rule) + curve.set_traffic_rule(loc0_scs_props.locator_prefab_np_traffic_rule) # set next/prev curves for next_key in curve_entry.next_curves: diff --git a/addon/io_scs_tools/exp/pit.py b/addon/io_scs_tools/exp/pit.py index c7b40e4..2209b0e 100644 --- a/addon/io_scs_tools/exp/pit.py +++ b/addon/io_scs_tools/exp/pit.py @@ -480,6 +480,8 @@ def export(root_object, filepath, name_suffix, used_parts, used_materials): # print(' value: %s' % str(value)) if format_prop == 'FLOAT': attribute_data.props.append((rec[0], ["&&", (value,)])) + elif format_prop == 'INT': + attribute_data.props.append((rec[0], ["ii", (value,)])) else: attribute_data.props.append((rec[0], ["i", tuple(value)])) attribute_sections.append(attribute_data) @@ -542,7 +544,7 @@ def export(root_object, filepath, name_suffix, used_parts, used_materials): if attr_prop == "Format": format_value = attribute_dict[attr_prop] - if attr_prop == "Value" and ("FLOAT" in format_value or "STRING" in format_value): + if attr_prop == "Value" and ("FLOAT" in format_value or "STRING" in format_value or "INT" in format_value): tag_prop = attribute_dict["Tag"].replace("[", "").replace("]", "") if "aux" in tag_prop: diff --git a/addon/io_scs_tools/imp/pim_ef.py b/addon/io_scs_tools/imp/pim_ef.py index 21f476f..b9209da 100644 --- a/addon/io_scs_tools/imp/pim_ef.py +++ b/addon/io_scs_tools/imp/pim_ef.py @@ -257,12 +257,11 @@ def _create_piece( context.window_manager.progress_update(0.5) # VERTEX COLOR + mesh_rgb_final = {} if mesh_rgba: - mesh_rgb_final = mesh_rgba - elif mesh_rgb: - mesh_rgb_final = mesh_rgb - else: - mesh_rgb_final = [] + mesh_rgb_final.update(mesh_rgba) + if mesh_rgb: + mesh_rgb_final.update(mesh_rgb) for vc_layer_name in mesh_rgb_final: max_value = mesh_rgb_final[vc_layer_name][0][0][0] / 2 diff --git a/addon/io_scs_tools/internals/containers/parsers/sii.py b/addon/io_scs_tools/internals/containers/parsers/sii.py index 226b8e1..918101f 100644 --- a/addon/io_scs_tools/internals/containers/parsers/sii.py +++ b/addon/io_scs_tools/internals/containers/parsers/sii.py @@ -322,11 +322,36 @@ def _parse_unit(tokenizer): return None -def parse_file(filepath, print_info=False): +def _parse_bare_file(filepath, print_info=False): + if print_info: + print("** SII Parser ...") + unit = _UnitData("", "", is_headless=True) + + file = open(filepath, mode="r", encoding="utf8") + lines = file.readlines() + file.close() + + tokenizer = _Tokenizer(lines, filepath, []) + + while 1: + if tokenizer.consume_token_if_match('eof', '') is not None: + if print_info: + print("** Bare SII Parser END") + return [unit] + + if not _parse_unit_property(tokenizer, unit): + print("Unit property parsing failed") + return None + + +def parse_file(filepath, is_sui=False, print_info=False): """ Reads SCS SII definition file from disk, parse it and return its full content in a form of hierarchical structure. """ + if is_sui: + return _parse_bare_file(filepath, print_info) + if print_info: print("** SII Parser ...") sii_container = [] diff --git a/addon/io_scs_tools/internals/containers/sii.py b/addon/io_scs_tools/internals/containers/sii.py index 12cdf36..7ea5148 100644 --- a/addon/io_scs_tools/internals/containers/sii.py +++ b/addon/io_scs_tools/internals/containers/sii.py @@ -25,13 +25,21 @@ from io_scs_tools.internals.containers.writers import sii as _sii_writer -def get_data_from_file(filepath): - """Returns entire data in data container from specified SII definition file.""" +def get_data_from_file(filepath, is_sui=False): + """Returns entire data in data container from specified SII definition file. + + :param filepath: absolute file path where SII should be read from + :type filepath: str + :param is_sui: True if file should be read as SUI, in that case only one unit will be returned + :type is_sui: bool + :return: list of SII Units if parsing succeded; otherwise None + :rtype: list[io_scs_tools.internals.structure.UnitData] | None + """ container = None if filepath: if os.path.isfile(filepath): - container = _sii_reader.parse_file(filepath) + container = _sii_reader.parse_file(filepath, is_sui=is_sui) if container: if len(container) < 1: lprint('D SII file "%s" is empty!', (_path_utils.readable_norm(filepath),)) @@ -136,7 +144,7 @@ def get_unit_property(container, prop, unit_instance=0): :param unit_instance: index of unit instance in container list that we are validating :type unit_instance: int :return: None if property is not found insde unit instance; otherwise value of the property - :rtype: None|object + :rtype: None|any """ value = None @@ -157,7 +165,7 @@ def get_direct_unit_property(unit, prop): :param prop: name of the property we are looking for :type prop: str :return: None if property is not found insde unit instance; otherwise value of the property - :rtype: None|object + :rtype: None|any """ value = None diff --git a/addon/io_scs_tools/internals/open_gl/core.py b/addon/io_scs_tools/internals/open_gl/core.py index c799faa..0e9304c 100644 --- a/addon/io_scs_tools/internals/open_gl/core.py +++ b/addon/io_scs_tools/internals/open_gl/core.py @@ -417,6 +417,7 @@ def _draw_3dview_report(region): :type region: bpy.types.Region """ pos = region.height - 62 + show_scroll_controls = _Show3DViewReportOperator.is_scrolled() if _Show3DViewReportOperator.has_lines(): @@ -466,6 +467,7 @@ def _draw_3dview_report(region): if pos - 60 < 0: blf.position(0, 20, pos, 0) blf.draw(0, "...") + show_scroll_controls = True break blf.position(0, 20, pos, 0) @@ -516,3 +518,39 @@ def _draw_3dview_report(region): # draw hide button text blf.position(0, hide_btn_text_pos[0], region.height - hide_btn_text_pos[1], 0) blf.draw(0, hide_btn_text) + + # draw scroll controls + if show_scroll_controls: + + # draw scroll up button + glColor3f(.4, .4, .4) + glBegin(GL_POLYGON) + glVertex3f(_OP_consts.View3DReport.SCROLLUP_BTN_AREA[0], region.height - _OP_consts.View3DReport.SCROLLUP_BTN_AREA[2], 0) + glVertex3f(_OP_consts.View3DReport.SCROLLUP_BTN_AREA[1], region.height - _OP_consts.View3DReport.SCROLLUP_BTN_AREA[2], 0) + glVertex3f(_OP_consts.View3DReport.SCROLLUP_BTN_AREA[1], region.height - _OP_consts.View3DReport.SCROLLUP_BTN_AREA[3], 0) + glVertex3f(_OP_consts.View3DReport.SCROLLUP_BTN_AREA[0], region.height - _OP_consts.View3DReport.SCROLLUP_BTN_AREA[3], 0) + glEnd() + + # draw scroll down button + glBegin(GL_POLYGON) + glVertex3f(_OP_consts.View3DReport.SCROLLDOWN_BTN_AREA[0], region.height - _OP_consts.View3DReport.SCROLLDOWN_BTN_AREA[2], 0) + glVertex3f(_OP_consts.View3DReport.SCROLLDOWN_BTN_AREA[1], region.height - _OP_consts.View3DReport.SCROLLDOWN_BTN_AREA[2], 0) + glVertex3f(_OP_consts.View3DReport.SCROLLDOWN_BTN_AREA[1], region.height - _OP_consts.View3DReport.SCROLLDOWN_BTN_AREA[3], 0) + glVertex3f(_OP_consts.View3DReport.SCROLLDOWN_BTN_AREA[0], region.height - _OP_consts.View3DReport.SCROLLDOWN_BTN_AREA[3], 0) + glEnd() + + # gather texts and positions + scrollup_btn_text_pos = _OP_consts.View3DReport.SCROLLUP_BTN_TEXT_POS[int(not _Show3DViewReportOperator.is_shown())] + scrollup_btn_text = _OP_consts.View3DReport.SCROLLUP_BTN_TEXT[int(not _Show3DViewReportOperator.is_shown())] + + scrolldown_btn_text_pos = _OP_consts.View3DReport.SCROLLDOWN_BTN_TEXT_POS[int(not _Show3DViewReportOperator.is_shown())] + scrolldown_btn_text = _OP_consts.View3DReport.SCROLLDOWN_BTN_TEXT[int(not _Show3DViewReportOperator.is_shown())] + + # draw scroll up button text + glColor3f(1, 1, 1) + blf.position(0, scrollup_btn_text_pos[0], region.height - scrollup_btn_text_pos[1], 0) + blf.draw(0, scrollup_btn_text) + + # draw scroll down button text + blf.position(0, scrolldown_btn_text_pos[0], region.height - scrolldown_btn_text_pos[1], 0) + blf.draw(0, scrolldown_btn_text) diff --git a/addon/io_scs_tools/internals/persistent/file_save.py b/addon/io_scs_tools/internals/persistent/file_save.py index 6c11543..7c0e507 100644 --- a/addon/io_scs_tools/internals/persistent/file_save.py +++ b/addon/io_scs_tools/internals/persistent/file_save.py @@ -22,6 +22,7 @@ from bpy.app.handlers import persistent from io_scs_tools.internals.containers import config as _config_container from io_scs_tools.utils import get_scs_globals as _get_scs_globals +from io_scs_tools.utils import ensure_scs_globals_save as _ensure_scs_globals_save @persistent @@ -55,6 +56,9 @@ def pre_save(scene): if scs_anim.action in bpy.data.actions: bpy.data.actions[scs_anim.action].use_fake_user = True + # make sure to save world holding SCS globals + _ensure_scs_globals_save() + @persistent def post_save(scene): diff --git a/addon/io_scs_tools/internals/preview_models/__init__.py b/addon/io_scs_tools/internals/preview_models/__init__.py index 4d5839c..365a825 100644 --- a/addon/io_scs_tools/internals/preview_models/__init__.py +++ b/addon/io_scs_tools/internals/preview_models/__init__.py @@ -99,12 +99,14 @@ def unlink(preview_model): bpy.context.scene.scs_cached_num_objects = len(bpy.context.scene.objects) -def load(locator): +def load(locator, deep_reload=False): """Makes a preview model for a locator and link it to it NOTE: locator preview model path must be set :param locator: locator object to which preview model should be set :type locator: bpy.types.Object + :param deep_reload: should model be reloaded completely? Use in case model mesh should be freshly loaded from disc + :type deep_reload: bool :return: True if preview model was set; False otherwise :rtype: bool """ @@ -131,7 +133,7 @@ def load(locator): if load_model: - unload(locator) + unload(locator, do_mesh_unlink=deep_reload) prem_name = str("prem_" + locator.name) obj = _get_model_mesh(locator, prem_name) @@ -165,11 +167,13 @@ def load(locator): return False -def unload(locator): +def unload(locator, do_mesh_unlink=False): """Clears a preview model from a locator :param locator: locator object from which preview model should be deleted :type locator: bpy.types.Object + :param do_mesh_unlink: should mesh be unloaded too? Use it only when model should be reloaded + :type do_mesh_unlink: bool """ for child in locator.children: @@ -178,9 +182,15 @@ def unload(locator): # first uncache it _cache.delete_entry(child.name) + # delete object & mesh + mesh = child.data + bpy.context.scene.objects.unlink(child) bpy.data.objects.remove(child, do_unlink=True) + if do_mesh_unlink: + bpy.data.meshes.remove(mesh, do_unlink=True) + # update scene children count to prevent delete to be triggered bpy.context.scene.scs_cached_num_objects = len(bpy.context.scene.objects) diff --git a/addon/io_scs_tools/internals/shaders/eut2/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/__init__.py index b58ebba..488a411 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/__init__.py @@ -80,6 +80,10 @@ def get_shader(effect): from io_scs_tools.internals.shaders.eut2.light_tex import LightTex as Shader + elif effect.startswith("retroreflective"): + + from io_scs_tools.internals.shaders.eut2.retroreflective import Retroreflective as Shader + elif effect.startswith("unlit.tex"): from io_scs_tools.internals.shaders.eut2.unlit_tex import UnlitTex as Shader diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_oclu/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_oclu/__init__.py index 9f0ed1a..6fc4a1e 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_oclu/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_oclu/__init__.py @@ -58,9 +58,6 @@ def init(node_tree): spec_mult_n = node_tree.nodes[DifSpec.SPEC_MULT_NODE] vcol_mult_n = node_tree.nodes[DifSpec.VCOLOR_MULT_NODE] - # delete existing - node_tree.nodes.remove(node_tree.nodes[DifSpec.OPACITY_NODE]) - # move existing for node in node_tree.nodes: if node.location.x > start_pos_x + pos_x_shift: @@ -136,67 +133,3 @@ def set_oclu_uv(node_tree, uv_layer): uv_layer = _MESH_consts.none_uv node_tree.nodes[DifSpecOclu.SEC_GEOM_NODE].uv_layer = uv_layer - - @staticmethod - def set_alpha_test_flavor(node_tree, switch_on): - """Set alpha test flavor to this shader. - - :param node_tree: node tree of current shader - :type node_tree: bpy.types.NodeTree - :param switch_on: flag indication if alpha test should be switched on or off - :type switch_on: bool - """ - - if switch_on and not blend_over.is_set(node_tree): - out_node = node_tree.nodes[DifSpec.OUT_MAT_NODE] - in_node = node_tree.nodes[DifSpec.VCOL_GROUP_NODE] - - location = (out_node.location.x - 185 * 2, out_node.location.y - 500) - - alpha_test.init(node_tree, location, in_node.outputs['Vertex Color Alpha'], out_node.inputs['Alpha']) - else: - alpha_test.delete(node_tree) - - @staticmethod - def set_blend_over_flavor(node_tree, switch_on): - """Set blend over flavor to this shader. - - :param node_tree: node tree of current shader - :type node_tree: bpy.types.NodeTree - :param switch_on: flag indication if blend over should be switched on or off - :type switch_on: bool - """ - - # remove alpha test flavor if it was set already. Because these two can not coexist - if alpha_test.is_set(node_tree): - DifSpec.set_alpha_test_flavor(node_tree, False) - - out_node = node_tree.nodes[DifSpec.OUT_MAT_NODE] - in_node = node_tree.nodes[DifSpec.VCOL_GROUP_NODE] - - if switch_on: - blend_over.init(node_tree, in_node.outputs['Vertex Color Alpha'], out_node.inputs['Alpha']) - else: - blend_over.delete(node_tree) - - @staticmethod - def set_blend_add_flavor(node_tree, switch_on): - """Set blend add flavor to this shader. - - :param node_tree: node tree of current shader - :type node_tree: bpy.types.NodeTree - :param switch_on: flag indication if blend add should be switched on or off - :type switch_on: bool - """ - - # remove alpha test flavor if it was set already. Because these two can not coexist - if alpha_test.is_set(node_tree): - DifSpec.set_alpha_test_flavor(node_tree, False) - - out_node = node_tree.nodes[DifSpec.OUT_MAT_NODE] - in_node = node_tree.nodes[DifSpec.VCOL_GROUP_NODE] - - if switch_on: - blend_add.init(node_tree, in_node.outputs['Vertex Color Alpha'], out_node.inputs['Alpha']) - else: - blend_add.delete(node_tree) diff --git a/addon/io_scs_tools/internals/shaders/eut2/retroreflective/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/retroreflective/__init__.py new file mode 100644 index 0000000..343f843 --- /dev/null +++ b/addon/io_scs_tools/internals/shaders/eut2/retroreflective/__init__.py @@ -0,0 +1,110 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# Copyright (C) 2019: SCS Software + + +from io_scs_tools.internals.shaders.eut2.dif import Dif +from io_scs_tools.internals.shaders.flavors import blend_over + + +class Retroreflective(Dif): + SPEC_MULT_NODE = "SpecMultiplier" + + @staticmethod + def get_name(): + """Get name of this shader file with full modules path.""" + return __name__ + + @staticmethod + def init(node_tree): + Retroreflective.init(node_tree) + + @staticmethod + def init(node_tree): + """Initialize node tree with links for this shader. + + :param node_tree: node tree on which this shader should be created + :type node_tree: bpy.types.NodeTree + """ + + pos_x_shift = 185 + + # init parent + Dif.init(node_tree) + + opacity_n = node_tree.nodes[Dif.OPACITY_NODE] + vcol_mult_n = node_tree.nodes[Dif.VCOLOR_MULT_NODE] + out_mat_n = node_tree.nodes[Dif.OUT_MAT_NODE] + compose_lighting_n = node_tree.nodes[Dif.COMPOSE_LIGHTING_NODE] + + # delete existing + node_tree.nodes.remove(node_tree.nodes[Dif.SPEC_COL_NODE]) + node_tree.nodes.remove(node_tree.nodes[Dif.DIFF_COL_NODE]) + node_tree.nodes.remove(node_tree.nodes[Dif.DIFF_MULT_NODE]) + + # node creation + spec_mult_n = node_tree.nodes.new("ShaderNodeMath") + spec_mult_n.name = Retroreflective.SPEC_MULT_NODE + spec_mult_n.label = Retroreflective.SPEC_MULT_NODE + spec_mult_n.location = (opacity_n.location[0] + pos_x_shift, opacity_n.location[1]) + spec_mult_n.operation = "MULTIPLY" + spec_mult_n.inputs[1].default_value = 0.2 # used for spcular color designed for the best visual on traffic signs + + # links creation + node_tree.links.new(spec_mult_n.inputs[0], opacity_n.outputs['Value']) + + node_tree.links.new(compose_lighting_n.inputs['Diffuse Color'], vcol_mult_n.outputs['Color']) + node_tree.links.new(out_mat_n.inputs['Color'], vcol_mult_n.outputs['Color']) + + node_tree.links.new(out_mat_n.inputs['Spec'], spec_mult_n.outputs['Value']) + + @staticmethod + def set_material(node_tree, material): + """Set output material for this shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param material: blender material for used in this tree node as output + :type material: bpy.types.Material + """ + + # set hardcoded shininness + material.specular_hardness = 60 + + Dif.set_material(node_tree, material) + + @staticmethod + def set_retroreflective_decal_flavor(node_tree, switch_on): + """Set depth retroreflective decal flavor to this shader. + NOTE: this is essentially same flavor as blend_over, thus just use blend over internally + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param switch_on: flag indication if retroreflective decal should be switched on or off + :type switch_on: bool + """ + + # remove alpha test flavor if it was set already. Because these two can not coexist + out_mat_node = node_tree.nodes[Dif.OUT_MAT_NODE] + opacity_n = node_tree.nodes[Dif.OPACITY_NODE] + + if switch_on: + blend_over.init(node_tree, opacity_n.outputs['Value'], out_mat_node.inputs['Alpha']) + else: + blend_over.delete(node_tree) diff --git a/addon/io_scs_tools/internals/shaders/shader.py b/addon/io_scs_tools/internals/shaders/shader.py index 84f9b1d..0ef35bc 100644 --- a/addon/io_scs_tools/internals/shaders/shader.py +++ b/addon/io_scs_tools/internals/shaders/shader.py @@ -47,7 +47,7 @@ def setup_nodes(material, effect, attr_dict, tex_dict, recreate): flavors["alpha_test"] = material.use_transparency = True material.transparency_method = "Z_TRANSPARENCY" - if (effect.endswith(".over") or ".over." in effect) and ".over.dif" not in effect: + if (effect.endswith(".over") or ".over." in effect) and ".over.dif" not in effect and ".retroreflective" not in effect: flavors["blend_over"] = material.use_transparency = True material.transparency_method = "Z_TRANSPARENCY" @@ -99,6 +99,10 @@ def setup_nodes(material, effect, attr_dict, tex_dict, recreate): if effect.endswith(".paint") or ".paint." in effect: flavors["paint"] = True + if effect.endswith(".decal.over") and ".retroreflective" in effect: + flavors["retroreflective_decal"] = material.use_transparency = True + material.transparency_method = "Z_TRANSPARENCY" + __setup_nodes__(material, effect, attr_dict, tex_dict, {}, flavors, recreate) diff --git a/addon/io_scs_tools/internals/structure.py b/addon/io_scs_tools/internals/structure.py index 1cc6a29..9418143 100755 --- a/addon/io_scs_tools/internals/structure.py +++ b/addon/io_scs_tools/internals/structure.py @@ -165,16 +165,18 @@ def get_prop_as_color(self, prop_name): return prop_value - def get_prop(self, prop_name): + def get_prop(self, prop_name, default=None): """Gets properety from unit. :param prop_name: name of the property we are searching for :type prop_name: str + :param default: default value that should be returned if property not found + :type default: any :return: None if property not found, otherwise object representing it's data :rtype: None|object """ if prop_name not in self.props: - return None + return default return self.props[prop_name] diff --git a/addon/io_scs_tools/operators/material.py b/addon/io_scs_tools/operators/material.py index 9c28475..01cd245 100644 --- a/addon/io_scs_tools/operators/material.py +++ b/addon/io_scs_tools/operators/material.py @@ -211,6 +211,8 @@ class WriteThrough(bpy.types.Operator): "( Ctrl + Click to WT on other SCS Root Objects on same look, " "Ctrl + Shift + Click to WT all looks of all SCS Root Objects )" ) + bl_options = {'REGISTER', 'UNDO'} + property_str = StringProperty( description="String representing which property should be written through.", default="", @@ -396,6 +398,7 @@ class LoadAliasedMaterial(bpy.types.Operator): bl_label = "Load Aliased Mat" bl_idname = "material.load_aliased_material" bl_description = "Load values from aliased material." + bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): @@ -575,6 +578,7 @@ class SelectShaderTextureFilePath(bpy.types.Operator): bl_label = "Select Shader Texture File" bl_idname = "material.scs_select_shader_texture_filepath" bl_description = "Open a Texture file browser" + bl_options = {'REGISTER', 'UNDO'} shader_texture = bpy.props.StringProperty(options={'HIDDEN'}) filepath = StringProperty( diff --git a/addon/io_scs_tools/operators/mesh.py b/addon/io_scs_tools/operators/mesh.py index ea6e326..793043f 100644 --- a/addon/io_scs_tools/operators/mesh.py +++ b/addon/io_scs_tools/operators/mesh.py @@ -447,17 +447,21 @@ def modal(self, context, event): active_obj = self.__get_active_object__() active_object_changed = active_obj != context.active_object + + # abort immeadiatelly if active object was changed + if active_object_changed: + self.cancel(context) + return {'CANCELLED'} + is_object_mode_changed = self.__active_object_mode != context.active_object.mode # allow changing into the edit mode as user might go there just to reselect # masked faces on which he wants to paint - if is_object_mode_changed and context.active_object.mode == "EDIT" and not active_object_changed: + if is_object_mode_changed and context.active_object.mode == "EDIT": return {'PASS_THROUGH'} - # abort if: - # 1. active object mode has changed - # 2. if user changed active object - if is_object_mode_changed or active_object_changed: + # abort if active object mode has changed + if is_object_mode_changed: self.cancel(context) return {'CANCELLED'} @@ -510,7 +514,10 @@ def cancel(self, context): # finish operator execution - go back to object mode if active_obj.mode == "VERTEX_PAINT": - bpy.ops.object.mode_set(mode="OBJECT") + override = context.copy() + override['mode'] = "OBJECT" + override['active_object'] = active_obj + bpy.ops.object.mode_set(override) # one last time rebake start_time = time() diff --git a/addon/io_scs_tools/operators/object.py b/addon/io_scs_tools/operators/object.py index 3003d1d..b3bd6b2 100755 --- a/addon/io_scs_tools/operators/object.py +++ b/addon/io_scs_tools/operators/object.py @@ -1190,6 +1190,7 @@ class FixHookups(bpy.types.Operator): bl_label = "Fix SCS Hookup Names on Model Locators" bl_idname = "object.scs_fix_model_locator_hookups" bl_description = "Tries to convert existing pure hookup ids to valid hookup name (valid Hookup Library is required)." + bl_options = {'REGISTER', 'UNDO'} def execute(self, context): lprint("D " + self.bl_label + "...") @@ -1274,6 +1275,7 @@ class AssignTerrainPoints(bpy.types.Operator): bl_idname = "object.assign_terrain_points" bl_description = str("Assigns terrain point to currently selected prefab Control Node " "(confirm requested if some vertices from this mesh are already assigned).") + bl_options = {'REGISTER', 'UNDO'} vg_name = StringProperty() """Name of the vertex group for terrain points. It consists of vertex group prefix and node index.""" @@ -1333,6 +1335,7 @@ class ClearTerrainPointsOperator(bpy.types.Operator): bl_label = "Clear All Terrain Points" bl_idname = "object.clear_all_terrain_points" bl_description = "Clears all terrain points for currently selected prefab Control Node" + bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): @@ -1788,6 +1791,7 @@ class ConnectPrefabLocators(bpy.types.Operator): bl_label = "Connect Prefab Locators" bl_idname = "object.connect_prefab_locators" bl_description = "To connect prefab locators two of them must be selected and they have to be same type" + bl_options = {'REGISTER', 'UNDO'} def execute(self, context): @@ -1822,6 +1826,7 @@ class DisconnectPrefabLocators(bpy.types.Operator): bl_label = "Disconnect Prefab Locators" bl_idname = "object.disconnect_prefab_locators" bl_description = "To disconnect navigation points two connected prefab locators must be selected" + bl_options = {'REGISTER', 'UNDO'} def execute(self, context): @@ -2009,6 +2014,7 @@ class PreviewModelPath(bpy.types.Operator): bl_label = "Select Preview Model (*.pim)" bl_idname = "object.select_preview_model_path" bl_description = "Open a file browser" + bl_options = {'REGISTER', 'UNDO'} filepath = StringProperty( name="Preview Model File Path", @@ -2062,6 +2068,7 @@ class CreateSCSRootObject(bpy.types.Operator): bl_idname = "object.create_scs_root_object" bl_description = "Create a new 'SCS Root Object' with initial setup. If any objects are selected," \ "they automatically become a part of the new 'SCS Game Object'." + bl_options = {'REGISTER', 'UNDO'} def execute(self, context): lprint('D Create New SCS Root Object...') @@ -2074,6 +2081,7 @@ class CreateSCSRootObjectDialog(bpy.types.Operator): bl_idname = "object.create_scs_root_object_dialog" bl_description = "Create a new 'SCS Root Object' with initial setup.\nIf any objects are selected," \ "they automatically become a part of the new 'SCS Game Object'." + bl_options = {'REGISTER', 'UNDO'} def execute(self, context): lprint('D Create New SCS Root Object with Name dialog...') @@ -2254,6 +2262,7 @@ class AddObject(bpy.types.Operator): bl_idname = "object.scs_add_object" bl_description = "Create SCS object of choosen type at 3D coursor position \n" \ "(when locator is created it will also be parented to SCS Root, if currently active)." + bl_options = {'REGISTER', 'UNDO'} # create function for retrieving items so custom icons can be used def new_object_type_items(self, context): diff --git a/addon/io_scs_tools/operators/scene.py b/addon/io_scs_tools/operators/scene.py index f705769..85e493e 100644 --- a/addon/io_scs_tools/operators/scene.py +++ b/addon/io_scs_tools/operators/scene.py @@ -20,6 +20,7 @@ import bpy import bmesh +import numpy import os import subprocess from collections import OrderedDict @@ -1414,23 +1415,24 @@ class PaintjobTools: """ class ImportFromDataSII(bpy.types.Operator): - bl_label = "Import SCS Truck From data.sii" + bl_label = "Import SCS Vehicle From data.sii" bl_idname = "scene.scs_import_from_data_sii" - bl_description = ("Import all models having paintable parts of a truck (including upgrades)" - "from choosen '/def/vehicle/truck//data.sii' file.") + bl_description = ("Import all models having paintable parts of a vehicle (including upgrades)" + "from choosen '/def/vehicle///data.sii' file.") bl_options = set() directory = StringProperty( - name="Import Truck", + name="Import Vehicle", subtype='DIR_PATH', ) filepath = StringProperty( - name="Truck 'data.sii' filepath", - description="File path to truck 'data.sii", + name="Vehicle 'data.sii' filepath", + description="File path to vehicle 'data.sii", subtype='FILE_PATH', ) filter_glob = StringProperty(default="*.sii", options={'HIDDEN'}) + vehicle_type = _PT_consts.VehicleTypes.NONE start_time = None # saving start time when initialize is called # saving old settings from scs globals @@ -1563,13 +1565,15 @@ def import_and_clean_model(context, project_path, model_path): return curr_scs_root @staticmethod - def add_model_to_group(scs_root, group_name_prefix, linked_to_defs=set()): + def add_model_to_group(scs_root, model_type, model_name, linked_to_defs=set()): """Adds model to group so it can be distinguished amongs all other models. :param scs_root: blender object representing SCS Root :type scs_root: bpy.types.Object - :param group_name_prefix: prefix name for - :type group_name_prefix: str + :param model_type: type of the model (chassis, cabin, upgrade) + :type model_type: str + :param model_name: name of the model + :type model_name: str :param linked_to_defs: set of the sii file paths where this model was defined :type linked_to_defs: set[str] """ @@ -1583,15 +1587,17 @@ def add_model_to_group(scs_root, group_name_prefix, linked_to_defs=set()): variant = _sii_container.get_unit_property(sii_container, "variant") if variant is not None: - used_variants_by_linked_defs.add(variant) + used_variants_by_linked_defs.add(variant.lower()) else: # if no variant specified "default" is used by game, so add it to our set used_variants_by_linked_defs.add("default") # create groups per variant for i, variant in enumerate(scs_root.scs_object_variant_inventory): + variant_name = variant.name.lower() + # do not create groups for unused variants - if variant.name not in used_variants_by_linked_defs: + if variant_name not in used_variants_by_linked_defs: continue bpy.ops.object.select_all(action="DESELECT") @@ -1600,7 +1606,8 @@ def add_model_to_group(scs_root, group_name_prefix, linked_to_defs=set()): override["active_object"] = scs_root # operator searches for scs root from active object, so make sure context will be correct bpy.ops.object.switch_variant_selection(override, select_type=_OP_consts.SelectionType.select, variant_index=i) - group = bpy.data.groups.new(group_name_prefix + " | " + variant.name) + group_name = model_type + " | " + model_name + " | " + variant_name + group = bpy.data.groups.new(group_name) mesh_objects_count = 0 for obj in scs_root.children: @@ -1619,6 +1626,7 @@ def add_model_to_group(scs_root, group_name_prefix, linked_to_defs=set()): bpy.data.groups.remove(group, do_unlink=True) continue + group[_PT_consts.model_variant_prop] = variant_name group[_PT_consts.model_refs_to_sii] = list(linked_to_defs) obj = bpy.data.objects.new(_PT_consts.export_tag_obj_name + "_" + str(len(bpy.data.groups)), None) @@ -1665,6 +1673,8 @@ def initalize(self): scs_globals.import_pic_file = False scs_globals.import_use_welding = False + self.vehicle_type = _PT_consts.VehicleTypes.NONE + def finalize(self): """Restore scs globals settings to the state they were before. """ @@ -1688,8 +1698,12 @@ def execute(self, context): data_sii_container = _sii_container.get_data_from_file(data_sii_path) # initial checkups - if not _sii_container.has_valid_unit_instance(data_sii_container, unit_type="accessory_truck_data", req_props=("fallback",)): - message = "Chosen file is not a valid truck 'data.sii' file!" + if _sii_container.has_valid_unit_instance(data_sii_container, unit_type="accessory_truck_data", req_props=("fallback",)): + self.vehicle_type = _PT_consts.VehicleTypes.TRUCK + elif _sii_container.has_valid_unit_instance(data_sii_container, unit_type="accessory_trailer_data", req_props=("info",)): + self.vehicle_type = _PT_consts.VehicleTypes.TRAILER + else: + message = "Chosen file is not a valid vehicle 'data.sii' file!" lprint("E " + message) self.report({'ERROR'}, message) self.finalize() @@ -1703,10 +1717,10 @@ def execute(self, context): # first find path of whole game project game_project_path = dir_path - for _ in range(0, 4): # we can simply go 4 dirs up, as def has to be properly placed /def/vehicle/truck/ + for _ in range(0, 4): # we can simply go 4 dirs up, as def has to be properly placed /def/vehicle// game_project_path = _path_utils.readable_norm(os.path.join(game_project_path, os.pardir)) - truck_sub_dir = os.path.relpath(dir_path, game_project_path) + vehicle_sub_dir = os.path.relpath(dir_path, game_project_path) game_project_path = os.path.join(game_project_path, os.pardir) # if data.sii was inside dlc or mod we have to go up once more in filesystem level @@ -1725,55 +1739,63 @@ def execute(self, context): ################################## # - # 2. import truck models + # 2. import vehicle models # ################################## - # collect all models paths for truck chassis and cabins - truck_model_paths = {} # holds list of SII files that each model was referenced from {KEY: model path, VALUE: list of SII paths} + # collect all models paths for vehicle chassis, cabins and possible trailer body + vehicle_model_paths = {} # holds list of SII files that each model was referenced from {KEY: model path, VALUE: list of SII paths} for project_path in project_paths: - truck_def_dirpath = os.path.join(project_path, truck_sub_dir) + vehicle_def_dirpath = os.path.join(project_path, vehicle_sub_dir) - target_dirpath = os.path.join(truck_def_dirpath, "chassis") + target_dirpath = os.path.join(vehicle_def_dirpath, "chassis") curr_models = self.gather_model_paths(target_dirpath, "accessory_chassis_data", ("detail_model", "model")) - self.update_model_paths_dict(truck_model_paths, curr_models) + self.update_model_paths_dict(vehicle_model_paths, curr_models) + + if self.vehicle_type == _PT_consts.VehicleTypes.TRUCK: + + target_dirpath = os.path.join(vehicle_def_dirpath, "cabin") + curr_models = self.gather_model_paths(target_dirpath, "accessory_cabin_data", ("detail_model", "model")) + self.update_model_paths_dict(vehicle_model_paths, curr_models) + + elif self.vehicle_type == _PT_consts.VehicleTypes.TRAILER: - target_dirpath = os.path.join(truck_def_dirpath, "cabin") - curr_models = self.gather_model_paths(target_dirpath, "accessory_cabin_data", ("detail_model", "model")) - self.update_model_paths_dict(truck_model_paths, curr_models) + target_dirpath = os.path.join(vehicle_def_dirpath, "body") + curr_models = self.gather_model_paths(target_dirpath, "accessory_trailer_body_data", ("detail_model", "model")) + self.update_model_paths_dict(vehicle_model_paths, curr_models) - lprint("S Truck Paths:\n%r" % truck_model_paths) + lprint("S Vehicle Paths:\n%r" % vehicle_model_paths) # import and properly group imported models possible_upgrade_locators = {} # dictionary holding all locators that can be used as candidates for upgrades positioning already_imported = set() # set holding imported path of already imported model, to avoid double importing - multiple_project_truck_models = set() # set of model paths found in multiple projects (for reporting purposes) + multiple_project_vehicle_models = set() # set of model paths found in multiple projects (for reporting purposes) for project_path in project_paths: - for truck_model_path in truck_model_paths: + for vehicle_model_path in vehicle_model_paths: - model_path = os.path.join(project_path, truck_model_path.lstrip("/")) + model_path = os.path.join(project_path, vehicle_model_path.lstrip("/")) # initial checks if not os.path.isfile(model_path + ".pim"): continue - if truck_model_path in already_imported: - multiple_project_truck_models.add(truck_model_path) + if vehicle_model_path in already_imported: + multiple_project_vehicle_models.add(vehicle_model_path) continue - already_imported.add(truck_model_path) + already_imported.add(vehicle_model_path) # import model - curr_truck_scs_root = self.import_and_clean_model(context, project_path, model_path) + curr_vehicle_scs_root = self.import_and_clean_model(context, project_path, model_path) # truck did not have any paintable parts, go to next - if curr_truck_scs_root is None: + if curr_vehicle_scs_root is None: continue # collect all locators as candidates for being used for upgrades positioning - for obj in curr_truck_scs_root.children: + for obj in curr_vehicle_scs_root.children: if obj.type != "EMPTY" or obj.scs_props.empty_object_type != "Locator": continue @@ -1781,11 +1803,14 @@ def execute(self, context): possible_upgrade_locators[obj.name] = obj # put imported model into it's own groups per variant - self.add_model_to_group(curr_truck_scs_root, "truck | " + os.path.basename(truck_model_path), truck_model_paths[truck_model_path]) + self.add_model_to_group(curr_vehicle_scs_root, + self.vehicle_type, + os.path.basename(vehicle_model_path), + vehicle_model_paths[vehicle_model_path]) - # if none truck models were properly imported it makes no sense to go forward on upgrades + # if none vehicle models were properly imported it makes no sense to go forward on upgrades if len(already_imported) <= 0: - message = "No truck models properly imported!" + message = "No vehicle models properly imported!" lprint("E " + message) self.report({"ERROR"}, message) self.finalize() @@ -1797,33 +1822,33 @@ def execute(self, context): # ################################## - # collect all upgrade models, by listing all upgrades directories in all projects for this truck + # collect all upgrade models, by listing all upgrades directories in all projects for this vehicle upgrade_model_paths = {} # model paths dictionary {key: upgrade type (eg "f_intake_cab"); value: set of model paths for this upgrade} for project_path in project_paths: # collect any possible upgrade over all projects - truck_accessory_def_dirpath = os.path.join(project_path, truck_sub_dir) - truck_accessory_def_dirpath = os.path.join(truck_accessory_def_dirpath, "accessory") + vehicle_accessory_def_dirpath = os.path.join(project_path, vehicle_sub_dir) + vehicle_accessory_def_dirpath = os.path.join(vehicle_accessory_def_dirpath, "accessory") # if current project path doesn't have accessories defined just skip it - if not os.path.isdir(truck_accessory_def_dirpath): + if not os.path.isdir(vehicle_accessory_def_dirpath): continue - for upgrade_name in os.listdir(truck_accessory_def_dirpath): + for upgrade_type in os.listdir(vehicle_accessory_def_dirpath): # ignore files - if not os.path.isdir(os.path.join(truck_accessory_def_dirpath, upgrade_name)): + if not os.path.isdir(os.path.join(vehicle_accessory_def_dirpath, upgrade_type)): continue - if upgrade_name not in upgrade_model_paths: - upgrade_model_paths[upgrade_name] = {} + if upgrade_type not in upgrade_model_paths: + upgrade_model_paths[upgrade_type] = {} - target_dirpath = os.path.join(truck_accessory_def_dirpath, upgrade_name) + 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_name], curr_models) + self.update_model_paths_dict(upgrade_model_paths[upgrade_type], curr_models) - if len(upgrade_model_paths[upgrade_name]) <= 0: # if no models for upgrade, remove set also - del upgrade_model_paths[upgrade_name] + if len(upgrade_model_paths[upgrade_type]) <= 0: # if no models for upgrade, remove set also + del upgrade_model_paths[upgrade_type] # import models, group and position them properly already_imported = set() # set holding imported path of already imported model, to avoid double importing @@ -1839,11 +1864,14 @@ def execute(self, context): if not os.path.isfile(model_path + ".pim"): continue - if upgrade_model_path in already_imported: - multiple_project_upgrade_models.add(upgrade_model_path) + # construct key for checking already imported models by same type of upgrade + model_path_key = upgrade_type + " | " + upgrade_model_path + + if model_path_key in already_imported: + multiple_project_upgrade_models.add(model_path_key) continue - already_imported.add(upgrade_model_path) + already_imported.add(model_path_key) # import model curr_upgrade_scs_root = self.import_and_clean_model(context, project_path, model_path) @@ -1852,7 +1880,9 @@ def execute(self, context): continue # put imported model into it's own groups - self.add_model_to_group(curr_upgrade_scs_root, upgrade_type + " | " + os.path.basename(upgrade_model_path), + self.add_model_to_group(curr_upgrade_scs_root, + upgrade_type, + os.path.basename(upgrade_model_path), upgrade_model_paths[upgrade_type][upgrade_model_path]) # find upgrade locator by prefix & position upgrade by locator aka make parent on it @@ -1865,7 +1895,7 @@ def execute(self, context): # Now we are trying to find "perfect" match, which is found, # when matched prefixed upgrade locator is also assigned to at least one group. # This way we eliminate locators that are in variants - # not used by any chassis or cabin of our truck. + # not used by any chassis, cabin or trailer body of our vehicle. # However cases involving "suitable_for" fields are not covered here! if upgrade_locator is None: @@ -1885,13 +1915,14 @@ def execute(self, context): curr_upgrade_scs_root.parent = upgrade_locator # on the end report multiple project model problems - if len(multiple_project_truck_models) > 0: - lprint("W Truck models found in multiple projects, one from 'mod_' or 'dlc_' project was used! Multiple project models:") - for truck_model_path in multiple_project_truck_models: - lprint("W %r", (truck_model_path,)) + if len(multiple_project_vehicle_models) > 0: + lprint("W Same vehicle models referenced by multiple SIIsprojects, one from 'mod_' or 'dlc_' project was used! Multiple project " + "models:") + for vehicle_model_path in multiple_project_vehicle_models: + lprint("W %r", (vehicle_model_path,)) if len(multiple_project_upgrade_models) > 0: - lprint("W Upgrade models found in multiple projects, one from 'mod_' or 'dlc_' project was used! Multiple project models:") + lprint("W Same upgrade models referenced by multiple upgrades SIIs or multiple projects ('mod_' or 'dlc_'):") for upgrade_model_path in multiple_project_upgrade_models: lprint("W %r", (upgrade_model_path,)) @@ -1941,6 +1972,21 @@ class ExportUVLayoutAndMesh(bpy.types.Operator): default=True ) + export_id_mask = BoolProperty( + name="Export ID Mask", + description="Should be id mask marking texture portions be exported?", + default=True + ) + + id_mask_alpha = FloatProperty( + name="ID Mask Color Alpha", + description="Alpha value of ID color when exporting ID Mask\n" + "(For debugging purposes of texture portion overlaying, value 0.5 is advised otherwise 1.0 should be used.)", + default=0.5, + min=0.1, + max=1.0 + ) + export_mesh = BoolProperty( name="Export Mesh as OBJ", description="Should OBJ mesh also be exported?", @@ -2021,6 +2067,15 @@ def cleanup(*args): for mesh in meshes: bpy.data.meshes.remove(mesh, do_unlink=True) + @staticmethod + def cleanup_meshes(): + """Cleanups any meshes with zero users that might be left-overs from join operator. + """ + + for m in bpy.data.meshes: + if m.users == 0: + bpy.data.meshes.remove(m) + def check(self, context): if self.layout_sii_selection_mode: @@ -2042,6 +2097,9 @@ def draw(self, context): col.label("What to export?", icon='QUESTION') col.prop(self, "export_2nd_uvs") col.prop(self, "export_3rd_uvs") + col.prop(self, "export_id_mask") + if self.export_id_mask: + col.prop(self, "id_mask_alpha", slider=True) col.prop(self, "export_mesh") def do_report(self, type, message, do_report=False): @@ -2114,7 +2172,8 @@ def execute(self, context): context.scene.objects.active = context.selected_objects[0] override = context.copy() override["selected_objects"] = context.selected_objects - bpy.ops.object.join(override) # NOTE: this operator leaves old meshes behind, but for now we won't solve this issue + bpy.ops.object.join(override) + self.cleanup_meshes() curr_merged_object = context.selected_objects[0] curr_truckpaint_mat = None @@ -2196,9 +2255,17 @@ def execute(self, context): texture_portions[unit_id] = texture_portion + # do model sii validation + for unit_id in texture_portions: + texture_portion = texture_portions[unit_id] + + if not texture_portion.get_prop("model_sii") and not texture_portion.get_prop("is_master", False): + self.do_report({'ERROR'}, "Invalid texture portion with name %r as 'model_sii' is not defined!" % unit_id, do_report=True) + return {'CANCELLED'} + lprint("S Found texture portions: %r", (texture_portions.keys(),)) - # 2. bind each merged object to it's texture portion and filter to three categories: + # 2. bind each merged object to it's texture portion and filter to four categories: # objects which are independently exported by transformation defined in their texture potion (be it original or parent portion) independent_export_objects = {} @@ -2207,6 +2274,8 @@ def execute(self, context): # objects which are (direct or indirect) children of master export objects; # they have to be duplicated & included in master objects before export (to see uvs on all master layouts) master_child_export_objects = {} + # objects without referenced SII, which won't be exported and should be removed manually via cleanup method + unconfigured_objects = [] for obj in merged_objects_to_export: group = merged_objects_to_export[obj] @@ -2217,6 +2286,10 @@ def execute(self, context): model_sii = texture_portions[unit_id].get_prop("model_sii") + # ignore texture portions without reference to model sii + if not model_sii: + continue + for reference_to_sii in group[_PT_consts.model_refs_to_sii]: # yep we found possible sii of the model, but not quite yet @@ -2225,11 +2298,13 @@ def execute(self, context): sii_cont = _sii_container.get_data_from_file(reference_to_sii) variant = _sii_container.get_unit_property(sii_cont, "variant") - if not variant: - variant = "default" # if variant is not specified in sii, our games use default + if variant: + variant = variant.lower() + else: + variant = "default" # if variant is not specified in sii, our games uses default # now check variant: if it's the same then we have it! - if variant == group.name.split(" | ")[2]: # yep, 3rd split of group name suggest variant + if variant == group[_PT_consts.model_variant_prop]: texture_portion = texture_portions[unit_id] break @@ -2243,7 +2318,8 @@ def execute(self, context): self.do_report({'WARNING'}, "Model %r wasn't referenced by any SII defined in paintjob configuration metadata, please reconfigure!\n\t " - "SII files from which model was referenced:\n\t %s" % (group.name, referenced_siis), do_report=True) + "SII files from which model was referenced:\n\t %s" % (group.name, referenced_siis)) + unconfigured_objects.append(obj) continue # filter out objects using master texture portions @@ -2266,13 +2342,17 @@ def execute(self, context): else: independent_export_objects[obj] = texture_portion # even if it has parent it's exported independent; no duplicates needed + # cleanup unconfigured objects + if len(unconfigured_objects) > 0: + self.cleanup(unconfigured_objects) + # nonsense to go further if nothing to export if len(independent_export_objects) + len(master_export_objects) <= 0: self.do_report({"ERROR"}, "Nothing to export, independent objects: %s, master objects: %s, objects with master parent: %s!" % (len(independent_export_objects), len(master_export_objects), len(master_child_export_objects)), do_report=True) - self.cleanup(merged_objects_to_export) + self.cleanup(merged_objects_to_export.keys()) return {'CANCELLED'} # 3. do uv transformations and distribute master children objects: @@ -2298,6 +2378,7 @@ def execute(self, context): override = context.copy() override["selected_objects"] = context.selected_objects bpy.ops.object.join(override) + self.cleanup_meshes() bpy.data.objects.remove(obj, do_unlink=True) @@ -2325,6 +2406,7 @@ def execute(self, context): override = context.copy() override["selected_objects"] = context.selected_objects bpy.ops.object.join(override) + self.cleanup_meshes() ################################## # @@ -2383,6 +2465,76 @@ def execute(self, context): # remove final merged object now as we done our work here bpy.data.objects.remove(final_merged_object, do_unlink=True) + ################################## + # + # 5. export texture portions id mask + # + ################################## + + if self.export_id_mask: + + start_time = time() + + # intialize pixel values for id mask texture + img_pixels = [0.0] * common_texture_size[0] * common_texture_size[1] * 4 + + id_mask_color_idx = 0 + for unit_id in texture_portions: + + # ignore portions with parent attribute, they don't use it's own texture space + if texture_portions[unit_id].get_prop("parent") is not None: + continue + + position = [float(i) for i in texture_portions[unit_id].get_prop("position")] + size = [float(i) for i in texture_portions[unit_id].get_prop("size")] + + portion_width = round(size[0] * common_texture_size[0]) + portion_height = round(size[1] * common_texture_size[1]) + portion_pos_x = round(position[0] * common_texture_size[0]) + portion_pos_y = round(position[1] * common_texture_size[1]) + + # calculate this portion color from RGB values + portion_col = list(_PT_consts.id_mask_colors[id_mask_color_idx % len(_PT_consts.id_mask_colors)]) + id_mask_color_idx += 1 + portion_col[0] /= 255.0 + portion_col[1] /= 255.0 + 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 + + # 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.use_alpha = True + img.pixels[:] = img_pixels + + # save + scene = bpy.context.scene + scene.render.image_settings.file_format = "PNG" + scene.render.image_settings.color_mode = "RGBA" + img.save_render(self.filepath + ".id_mask.png", bpy.context.scene) + + # remove image data-block, as we don't need it anymore + img.buffers_free() + bpy.data.images.remove(img, do_unlink=True) + + lprint("I Exported ID mask texture in %.2f sec!" % (time() - start_time)) + + self.do_report({'INFO'}, "Somehow we made it to the end, if no warning & errros appeared, then you are perfect!", do_report=True) return {'FINISHED'} def invoke(self, context, event): @@ -2396,6 +2548,126 @@ class GeneratePaintjob(bpy.types.Operator): bl_description = "Generates complete setup for given paintjob: definitions, TGAs & TOBJs." bl_options = {'INTERNAL'} + class Overrides: + """Class encapsulating paintjob overrides creation and export.""" + PROPS_KEY = "props" + ACC_LIST_KEY = "accessories" + + __overrides = OrderedDict() + + @staticmethod + def __generate_props_hash(props): + hash_str = "" + + for key in props: + hash_str += str(props[key]) + + return hash_str + + def __init__(self): + self.__overrides = OrderedDict() + + def __append_to_acc_list(self, props_hash, acc_type_id): + """Add accessory to proper override list depending on given properties hash + + NOTE: There is no check if override with given properties hash exists! So check it before. + + :param props_hash: hash of properties values under which this override will be saved + :type props_hash: str + :param acc_type_id: accessory type and id as concatenated string divided by dot (.) + :type acc_type_id: str + """ + if acc_type_id in self.__overrides[props_hash][self.ACC_LIST_KEY]: + return + + self.__overrides[props_hash][self.ACC_LIST_KEY].add(acc_type_id) + + def __create_override(self, props_hash, acc_type_id, props): + """Creates and saves override structure. + + One override structure is represented with dictonary of two members: + 1. props -> all paintjob properties saved in this override + 2. accessory list -> list of accessoreis compatible with this override + + NOTE: There is no check if override with given propreties hash already exists! So check it before. + + :param props_hash: hash of properties values under which this override will be saved + :type props_hash: str + :param acc_type_id: accessory type and id as concatenated string divided by dot (.) + :type acc_type_id: str + :param props: properties used in this override + :type props: OrderedDict[str, any] + """ + self.__overrides[props_hash] = { + self.PROPS_KEY: props, + self.ACC_LIST_KEY: {acc_type_id, } + } + + def add_accessory(self, acc_type_id, props=OrderedDict()): + """Add acceesory to overides. + + :param acc_type_id: accessory type and id as concatenated string divided by dot (. -> exhaust_m.mg01) + :type acc_type_id: str + :param props: simple paint job properites of this accessory + :type props: OrderedDict[str, any] + """ + props_hash = self.__generate_props_hash(props) + + if props_hash in self.__overrides: + self.__append_to_acc_list(props_hash, acc_type_id) + else: + self.__create_override(props_hash, acc_type_id, props) + + def export_to_sii(self, op_inst, config_path): + """Exports overrides in given config path. + + :param op_inst: generate paintjob operator instance, to bi able to get default values of paintjob properties + :type op_inst: bpy.types.Operator + :param config_path: absolute filepath where overrides should be exported: + /def/vehicle///paint_job/accessory/.sii + :type config_path: str + :return: True if export was successful, False otherwise + :rtype: bool + """ + export_units = [] + + # 1. collect overrides as units + for props_hash in self.__overrides: + override = self.__overrides[props_hash] + + # if no accessories for current override ignore it + if len(override[self.ACC_LIST_KEY]) == 0: + continue + + unit = _UnitData("simple_paint_job_data", ".ovr%i" % len(export_units)) + + # export extra properties only if different than default value + pj_props = override[self.PROPS_KEY] + for key in pj_props: + assert op_inst.append_prop_if_not_default(unit, "pjs_" + key, pj_props[key]) + + # as it can happen now that we don't have any properties in our unit, then it's useless to export it + if len(unit.props) == 0: + continue + + # now fill accessory list + unit.props["acc_list"] = [] + for acc_type_id in sorted(override[self.ACC_LIST_KEY]): + unit.props["acc_list"].append(acc_type_id) + + # finally add it to export units + export_units.append(unit) + + # 2. export overrides + return _sii_container.write_data_to_file(config_path, tuple(export_units), create_dirs=True) + + vehicle_type = _PT_consts.VehicleTypes.NONE + + img_node = None + premul_node = None + translate_node = None + viewer_node = None + config_meta_filepath = StringProperty( description="File path to paintjob configuration SII file." ) @@ -2481,156 +2753,232 @@ def do_report(the_type, message, do_report=False): lprint(prefix + message, report_errors=do_report, report_warnings=do_report) - def export_texture(self, orig_img, paintjob_path, texture_portion): + def initialize_nodes(self, context, img): + """Initializes nodes and scene proeprties to be able to use compositor for rendering of texture portions TGAs. + + :param context: blender context + :type context: bpy.types.Context + :param img: original big common texture image + :type img: bpy.types.Image + """ + + # ensure compositing and scene node tree + + context.scene.render.use_compositing = True + context.scene.use_nodes = True + + _X_SHIFT = 250 + + tree = context.scene.node_tree + nodes = tree.nodes + links = tree.links + + # remove any existing nodes (they are created by defoult when switching to compositiong) + + while len(nodes) > 0: + nodes.remove(nodes[0]) + + # create nodes + + self.img_node = nodes.new(type="CompositorNodeImage") + self.img_node.location = (-_X_SHIFT, 0) + self.img_node.image = img + + self.premul_node = nodes.new(type="CompositorNodePremulKey") + self.premul_node.location = (0, 0) + self.premul_node.mapping = 'STRAIGHT_TO_PREMUL' + + self.translate_node = nodes.new(type="CompositorNodeTranslate") + self.translate_node.location = (_X_SHIFT, 0) + + self.viewer_node = nodes.new(type="CompositorNodeComposite") + self.viewer_node.location = (_X_SHIFT * 2, 0) + self.viewer_node.use_alpha = True + + # create links + + links.new(self.premul_node.inputs["Image"], self.img_node.outputs["Image"]) + links.new(self.translate_node.inputs["Image"], self.premul_node.outputs["Image"]) + links.new(self.viewer_node.inputs["Image"], self.translate_node.outputs["Image"]) + + def export_texture(self, orig_img, tgas_dir_path, texture_portion): """Export given texture portion into given paintjob path. :param orig_img: Blender image datablock representing common texture :type orig_img: bpy.types.Image - :param paintjob_path: absolute path to export TGA and TOBJ to - :type paintjob_path: str + :param tgas_dir_path: absolute directory path to export TGA and TOBJ to + :type tgas_dir_path: str :param texture_portion: texture portion defining portion position and size :type texture_portion: io_scs_tools.internals.structure.UnitData - :return: True if export was successful, otherwise False - :rtype: bool + :return: TOBJ path of exported texture, in case sth went wrong return None + :rtype: str | None """ position = [float(i) for i in texture_portion.get_prop("position")] size = [float(i) for i in texture_portion.get_prop("size")] + is_master = bool(texture_portion.get_prop("is_master")) orig_img_width = orig_img.size[0] orig_img_height = orig_img.size[1] - orig_img_start_x = int(orig_img_width * position[0]) - orig_img_start_y = int(orig_img_height * position[1]) + orig_img_start_x = round(orig_img_width * position[0]) + orig_img_start_y = round(orig_img_height * position[1]) - img_width = int(orig_img_width * size[0]) - img_height = int(orig_img_height * size[1]) + img_width = round(orig_img_width * size[0]) + img_height = round(orig_img_height * size[1]) - # create copied data - # We "invoke get" for original image pixels only on the rows where actual portion is positioned. - # Additionally we do that in chunks, so we take only part of the height, - # gaining smaller RAM usage as getting pixels from original image really eats it up. - # In case of having portion of 4kx4k, getting pixels from original image can take up to 7GB rams, - # which isn't really what user might have available. - img_pixels = [0.0] * img_width * img_height * 4 - orig_img_pixels = [] - rows_in_chunk = 1024 # this size of chunk seems to work the best for ration of used ram/export speed + # setup compositing nodes for current texture + # NOTE: render clips original image by it's resolution and is centered on the output image, + # thus we have to translate our original image so that portion texture is in the middle of the render - is_img_single_color = self.optimize_single_color_textures # if no optimization then we can already mark image as not a single color - comparing_pixel = [0.0] * 4 # for computation of single color image - for row in range(0, img_height): + orig_img_mid_x = orig_img_start_x + (img_width / 2) + orig_img_mid_y = orig_img_start_y + (img_height / 2) - # on the beginning of the chunk refill pixels from original image - if row % rows_in_chunk == 0: + self.translate_node.inputs["X"].default_value = (orig_img_width / 2) - orig_img_mid_x + self.translate_node.inputs["Y"].default_value = (orig_img_height / 2) - orig_img_mid_y - start_pixel = (orig_img_start_y + row) * orig_img_width * 4 - end_pixel = start_pixel + orig_img_width * rows_in_chunk * 4 - end_pixel = min(end_pixel, start_pixel + orig_img_width * img_height * 4) + # we encode texture name with portion position and size, thus any possible duplicates will end up in same texture + tga_name = "pjm_at_%ix%i_size_%ix%i.tga" % (orig_img_start_x, + orig_img_start_y, + img_width, + img_height) + tga_path = os.path.join(tgas_dir_path, tga_name) - orig_img_pixels = orig_img.pixels[start_pixel:end_pixel] + # export texture portion image by rendering compositor - # use first pixel of current texture portion for comparison - if row == 0: - comparing_pixel = orig_img_pixels[0:4] + scene = bpy.context.scene + scene.render.image_settings.file_format = "TARGA" + scene.render.image_settings.color_mode = "RGBA" if self.export_alpha else "RGB" + scene.render.resolution_percentage = 100 + scene.render.resolution_x = img_width + scene.render.resolution_y = img_height + scene.render.filepath = tga_path + bpy.ops.render.render(write_still=True) - orig_start_pixel = (row % rows_in_chunk) * orig_img_width * 4 + orig_img_start_x * 4 - orig_end_pixel = orig_start_pixel + img_width * 4 + # if no optimization or is master then we can skip optimization processing, + # otherwise texture is re-opened and analyzed and + # in case only one color is inside, we export 4x4 texture and update tga path - start_pixel = row * img_width * 4 - end_pixel = start_pixel + img_width * 4 + if self.optimize_single_color_textures and not is_master: - img_pixels[start_pixel:end_pixel] = orig_img_pixels[orig_start_pixel:orig_end_pixel] + lprint("I Analyzing texture for single color...") - # compare for single color image only until all searched pixels have the same color - if is_img_single_color: + is_img_single_color = True + img = bpy.data.images.load(tga_path, check_existing=True) - for i in range(orig_start_pixel, orig_end_pixel, 4): + _CHUNK_SIZE = 2048 * 1024 # testing showed that this chunk size works best + rows_in_chunk = round(_CHUNK_SIZE / img_width) + buffer = [] + comparing_pixel = [0.0] * 4 + for row in range(0, img_height): + + # on the beginning of the chunk refill pixels from image + if row % rows_in_chunk == 0: + + start_pixel = row * img_width * 4 + end_pixel = start_pixel + img_width * rows_in_chunk * 4 + end_pixel = min(end_pixel, img_width * img_height * 4) + + buffer = img.pixels[start_pixel:end_pixel] + + # use first pixel for comparison + if row == 0: + comparing_pixel = buffer[0:4] - # mark image as none single color as soon as first different pixel is found and break for loop - if orig_img_pixels[i:i + 4] != comparing_pixel: + start_px = (row % rows_in_chunk) * img_width * 4 + for i in range(0, img_width * 4, 4): + if buffer[(start_px + i):(start_px + i + 4)] != comparing_pixel: is_img_single_color = False break - # create new texture and copy over data or copy only 4x4 if only one color is detected in the texture - if is_img_single_color: + if not is_img_single_color: + break - img_width = img_height = 4 - img_pixels[:] = img_pixels[0:64] + # already exported image not longer needed in Blender, so remove it! - lprint("I Texture portion %r has only one color in common texture, optimizing it by exporting 4x4px TGA!", - (texture_portion.id,)) + img.buffers_free() + bpy.data.images.remove(img, do_unlink=True) - img = bpy.data.images.new(texture_portion.id, img_width, img_height, alpha=True) - img.colorspace_settings.name = "sRGB" # make sure we use sRGB color-profile - img.use_alpha = True - img.pixels[:] = img_pixels + # finally export single texture - # save - scene = bpy.context.scene - scene.render.image_settings.file_format = "TARGA" - scene.render.image_settings.color_mode = "RGBA" if self.export_alpha else "RGB" - img.save_render(paintjob_path, bpy.context.scene) + if is_img_single_color: + + # don't forget to remove previously exported big TGA + os.remove(tga_path) + + _SINGLE_COLOR_IMAGE_NAME = "single_color_image" + + if _SINGLE_COLOR_IMAGE_NAME not in bpy.data.images: + bpy.data.images.new(_SINGLE_COLOR_IMAGE_NAME, 4, 4, alpha=True) + + img = bpy.data.images[_SINGLE_COLOR_IMAGE_NAME] + img.colorspace_settings.name = "sRGB" # make sure we use sRGB color-profile + img.use_alpha = True + img.pixels[:] = comparing_pixel * 16 + + # we use shared prefix for 4x4 textures in case any other portion will be using same one + tga_name = "shared_%.2x%.2x%.2x%.2x.tga" % (int(comparing_pixel[0] * 255.0), + int(comparing_pixel[1] * 255.0), + int(comparing_pixel[2] * 255.0), + int(comparing_pixel[3] * 255.0)) + tga_path = os.path.join(tgas_dir_path, tga_name) + + img.save_render(tga_path, bpy.context.scene) - # remove image data-block, as we don't need it anymore - orig_img.buffers_free() - bpy.data.images.remove(img, do_unlink=True) + lprint("I Texture portion %r has only one color in common texture, optimizing it by exporting 4x4px TGA!", (texture_portion.id,)) # write TOBJ beside tga file + + tobj_path = tga_path[:-4] + ".tobj" tobj_cont = _TobjContainer() tobj_cont.map_type = "2d" - tobj_cont.map_names.append(os.path.basename(paintjob_path)) + tobj_cont.map_names.append(tga_name) tobj_cont.addr.append("clamp_to_edge") tobj_cont.addr.append("clamp_to_edge") - tobj_cont.filepath = paintjob_path[:-4] + ".tobj" + tobj_cont.filepath = tobj_path + + # if there is any error by writting just return none + if not tobj_cont.write_data_to_file(): + return None - return tobj_cont.write_data_to_file() + return tobj_path - def export_sii(self, config_path, pj_token, pj_full_unit_name, pj_props, is_master=False, master_model_sii_unit_name=None): - """Export SII configuration file into given absolute path. + def export_master_sii(self, config_path, pj_token, pj_full_unit_name, pj_props, suitable_for=None): + """Export master SII configuration file into given absolute path. :param config_path: absolute file path for SII where it should be epxorted :type config_path: str :param pj_token: string of original paintjob token got from original TGA file name :type pj_token: str - :param pj_full_unit_name: for master paintjob: ..paint_job, otherwise .simplepj + :param pj_full_unit_name: ..paint_job :type pj_full_unit_name: str :param pj_props: dictionary of sii unit attributes to be writen in sii (key: name of attribute, value: any sii unit compatible object) :type pj_props: dict[str | object] - :param is_master: True for master paint job texture, otherwise False - :type is_master: bool - :param master_model_sii_unit_name: full unit name of referenced model inside master; if None suitable_for property won't be written - :type master_model_sii_unit_name: str | None + :param suitable_for: possible suitables of this paintjob; if None suitable_for property won't be written + :type suitable_for: list(str) | None :return: True if export was successful, False otherwise :rtype: bool """ - if is_master: - data_type = "accessory_paint_job_data" - else: - data_type = "simple_paint_job_data" - - unit = _UnitData(data_type, pj_full_unit_name) - - # write paint job settings only into master - if is_master: + unit = _UnitData("accessory_paint_job_data", pj_full_unit_name) - pj_settings_sui_name = pj_token + "_settings.sui" + pj_settings_sui_name = pj_token + "_settings.sui" - # export paint job settings SUI file - assert self.export_settings_sui(os.path.join(os.path.dirname(config_path), pj_settings_sui_name)) + # export paint job settings SUI file + assert self.export_settings_sui(os.path.join(os.path.dirname(config_path), pj_settings_sui_name)) - # write include into paint job sii - unit.props["@include"] = pj_settings_sui_name - - # create suitable to model sii unit name - if master_model_sii_unit_name: - unit.props["suitable_for"] = [master_model_sii_unit_name, ] + # write include into paint job sii + unit.props["@include"] = pj_settings_sui_name # export extra properties only if different than default value for key in pj_props: assert self.append_prop_if_not_default(unit, "pjs_" + key, pj_props[key]) + # export suitable for property + if suitable_for and len(suitable_for) > 0: + unit.props["suitable_for"] = suitable_for + # as it can happen now that we don't have any properties in our unit, then it's useless to export it if len(unit.props) == 0: lprint("I Unit has not properties thus useless to export empty SII, ignoring it: %r", (config_path,)) @@ -2654,6 +3002,11 @@ def export_settings_sui(self, settings_sui_path): unit = _UnitData("", "", is_headless=True) + # if old settings file has steam_inventory_id attribute, recover it! + old_settings_container = _sii_container.get_data_from_file(settings_sui_path, is_sui=True) + if old_settings_container and "steam_inventory_id" in old_settings_container[0].props: + unit.props["steam_inventory_id"] = int(old_settings_container[0].props["steam_inventory_id"]) + # force export of mandatory properties unit.props["name"] = self.pjs_name unit.props["price"] = self.pjs_price @@ -2741,11 +3094,22 @@ def execute(self, context): self.do_report({'WARNING'}, "Given paintjob layout META file does not exist: %r!" % self.config_meta_filepath) return {'CANCELLED'} - # get truck brand model token - brand_model_token = os.path.basename(os.path.abspath(os.path.join(self.config_meta_filepath, os.pardir))) + # get vehicle brand model token + curr_dir = os.path.abspath(os.path.join(self.config_meta_filepath, os.pardir)) + brand_model_token = os.path.basename(curr_dir) + + # get vehicle type + curr_dir = os.path.abspath(os.path.join(curr_dir, os.pardir)) + if os.path.basename(curr_dir) == _PT_consts.VehicleTypes.TRUCK: + self.vehicle_type = _PT_consts.VehicleTypes.TRUCK + 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!") + return {'CANCELLED'} - if not self.common_texture_path.endswith(".tga"): - self.do_report({'ERROR'}, "Given common texture is not TGA file: %r!" % self.common_texture_path) + if not self.common_texture_path.endswith(".tif"): + self.do_report({'ERROR'}, "Given common texture is not TIF file: %r!" % self.common_texture_path) return {'CANCELLED'} if not os.path.isfile(self.common_texture_path): @@ -2770,13 +3134,13 @@ def execute(self, context): orig_project_path = _path_utils.readable_norm(os.path.dirname(self.common_texture_path)) - # we can simply go 5 dirs up, as paintjob has to be properly placed /vehicle/truck/upgrade/paintjob/ + # we can simply go 5 dirs up, as paintjob has to be properly placed /vehicle//upgrade/paintjob/ for _ in range(0, 5): 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/truck/upgrade/paintjob//', instead is in:\n" + "'/vehicle//upgrade/paintjob//', instead is in:\n" "%r" % self.common_texture_path) return {'CANCELLED'} @@ -2804,13 +3168,13 @@ def execute(self, context): model_token = brand_model_dir[underscore_idx + 1:] is_common_tex_path_invalid = ( - brand_model_token != brand_token + "." + model_token or - not common_tex_dirpath.endswith("/vehicle/truck/upgrade/paintjob/" + brand_model_dir) + brand_model_token != brand_token + "." + model_token or + not common_tex_dirpath.endswith("/vehicle/" + self.vehicle_type + "/upgrade/paintjob/" + brand_model_dir) ) if is_common_tex_path_invalid: self.do_report({'ERROR'}, "Paintjob TGA file isn't saved on correct place, should be inside\n" - "'/vehicle/truck/upgrade/paintjob/%s' instead is saved in:\n" + "'/vehicle//upgrade/paintjob/%s' instead is saved in:\n" "%r." % (brand_model_token.replace(".", "_"), common_tex_dirpath)) return {'CANCELLED'} @@ -2818,7 +3182,7 @@ def execute(self, context): ################################## # - # 2. parse paintjob layout config file + # 2. parse and validate paintjob layout config file # ################################## @@ -2834,7 +3198,7 @@ def execute(self, context): common_texture_size = [int(i) for i in _sii_container.get_unit_property(pj_config_sii_container, "common_texture_size")] # get and validate texture portion unit existence - texture_portions = {} + texture_portions = OrderedDict() texture_portion_names = _sii_container.get_unit_property(pj_config_sii_container, "texture_portions") if texture_portion_names: @@ -2857,13 +3221,38 @@ def execute(self, context): # collect master portions to be able to properly export all override paintjob masks and other paint job attributes master_portions = [] + no_model_sii_master_count = 0 + model_sii_master_count = 0 + master_unit_suffixes = set() for unit_id in texture_portions: texture_portion = texture_portions[unit_id] is_master = bool(texture_portion.get_prop("is_master")) + master_unit_suffix = texture_portion.get_prop("master_unit_suffix", "") + model_sii = texture_portion.get_prop("model_sii") - if is_master is True: - master_portions.append(texture_portion) + if not is_master: + continue + + # 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) + return {'CANCELLED'} + + # check for no model sii definition + if model_sii: + model_sii_master_count += 1 + else: + no_model_sii_master_count += 1 + + master_unit_suffixes.add(master_unit_suffix) + 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!") + return {'CANCELLED'} lprint("D Found texture portions: %r", (texture_portions.keys(),)) @@ -2876,11 +3265,24 @@ def execute(self, context): common_tex_img = bpy.data.images.load(self.common_texture_path, check_existing=False) common_tex_img.use_alpha = self.export_alpha + 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)) return {'CANCELLED'} + # get textures export dir + tgas_dir_path = os.path.join(common_tex_dirpath, pj_token) + + # first remove old TGAs, TOBJs if directory already exists + if os.path.isdir(tgas_dir_path): + for file in os.listdir(tgas_dir_path): + current_file_path = os.path.join(tgas_dir_path, file) + if os.path.isfile(current_file_path) and (current_file_path.endswith(".tga") or current_file_path.endswith(".tobj")): + os.remove(current_file_path) + + # do export by portion id texture_portions_tobj_paths = {} # storing TGA paths for each texture portion, used later for referencing textures in SIIs exported_portion_textures = set() # storing already exported texture portion to avoid double exporting same TGA for unit_id in texture_portions: @@ -2891,28 +3293,19 @@ def execute(self, context): texture_portion = texture_portions[unit_id] - parent = texture_portions[unit_id].get_prop("parent") - while parent: - texture_portion = _sii_container.get_unit_by_id(pj_config_sii_container, parent, texture_portion.type) - parent = texture_portion.get_prop("parent") - - # get TGA path for this texture portion - tga_path = os.path.join(common_tex_dirpath, pj_token) - tga_path = os.path.join(tga_path, texture_portion.id.lstrip(".")) + ".tga" # TGA file name is always texture portion unit id - - # save TOBJ path to dictionary for later usage in config generation - texture_portions_tobj_paths[unit_id] = tga_path[:-4] + ".tobj" - - # filter out already exported texture portions - if texture_portion.id in exported_portion_textures: + # as parented texture portions do not own texture just ignore them + if texture_portions[unit_id].get_prop("parent"): continue + # mark this portion as exported exported_portion_textures.add(texture_portion.id) - # export TGA - assert self.export_texture(common_tex_img, tga_path, texture_portion) + # export TGA & save TOBJ path to dictionary for later usage in config generation + exported_tobj_path = self.export_texture(common_tex_img, tgas_dir_path, texture_portion) + assert exported_tobj_path is not None # nothing should go wrong thus we have to assert here + texture_portions_tobj_paths[unit_id] = exported_tobj_path - lprint("I Exported: %r", (tga_path,)) + lprint("I Exported: %r", (exported_tobj_path,)) ################################## # @@ -2926,65 +3319,45 @@ def execute(self, context): game_project_path = _path_utils.readable_norm(os.path.join(game_project_path, os.pardir)) project_paths = sorted(_path_utils.get_projects_paths(game_project_path), reverse=True) # sort them so dlcs & mods have priority - truck_def_subdir = os.path.join("def/vehicle/truck", brand_model_token) - - # clean old override simple paintjob configs - for truck_part in ("cabin", "chassis", "accessory"): + vehicle_def_subdir = os.path.join("def/vehicle/" + self.vehicle_type, brand_model_token) - # config path: "/def/vehicle/truck///paint_job/" - config_path = os.path.join(orig_project_path, truck_def_subdir) - config_path = os.path.join(config_path, truck_part) + # prepare overrides and their directory path: "/def/vehicle///paint_job/accessory/" + overrides = {} - if truck_part == "accessory" and os.path.isdir(config_path): + overrides_config_dir = os.path.join(orig_project_path, vehicle_def_subdir) + overrides_config_dir = os.path.join(overrides_config_dir, "paint_job") + overrides_config_dir = os.path.join(overrides_config_dir, "accessory") - for directory in os.listdir(config_path): + # delete old overrides files for this paintjob if they exists + if os.path.isdir(overrides_config_dir): + for file in os.listdir(overrides_config_dir): + pj_config_path = os.path.join(overrides_config_dir, file) + # match beginning and end of the file name + if os.path.isfile(pj_config_path) and file.startswith(pj_token) and file.endswith(".sii"): + os.remove(pj_config_path) - # accessory_dir: "/def/vehicle/truck//accessory//paint_job/" - accessory_dir = os.path.join(config_path, directory) - accessory_dir = os.path.join(accessory_dir, "paint_job") - - if not os.path.isdir(accessory_dir): - continue - - for file in os.listdir(accessory_dir): - pj_config_path = os.path.join(accessory_dir, file) - # match beginning and end of the file name - if os.path.isfile(pj_config_path) and file.startswith(pj_token) and file.endswith(".sii"): - os.remove(pj_config_path) - - else: - - # truck_part_dir: "/def/vehicle/truck///paint_job/" - truck_part_dir = os.path.join(config_path, "paint_job") - - if os.path.isdir(truck_part_dir): - - for file in os.listdir(truck_part_dir): - pj_config_path = os.path.join(truck_part_dir, file) - # match beginning and end of the file name - if os.path.isfile(pj_config_path) and file.startswith(pj_token) and file.endswith(".sii"): - os.remove(pj_config_path) - - # iterate texture portions and write all needed configs for it + # iterate texture portions, write master configs and collect overrides for unit_id in texture_portions: texture_portion = texture_portions[unit_id] - model_sii = texture_portion.get_prop("model_sii") + model_sii = texture_portion.get_prop("model_sii", "") is_master = bool(texture_portion.get_prop("is_master")) - master_unit_suffix = texture_portion.get_prop("master_unit_suffix") + + # master can exist without model sii reference! + requires_valid_model_sii = (not is_master) or (model_sii != "") parent = curr_parent = texture_portion.get_prop("parent") while curr_parent: parent = curr_parent curr_parent = texture_portions[parent].get_prop("parent") - # don't write config for texture portions when top most parent is master + # don't collect override for texture portions when top most parent is master if parent and bool(texture_portions[parent].get_prop("is_master")): continue # check for SIIs from "model_sii" in all projects - model_sii_subpath = os.path.join(truck_def_subdir, model_sii) + model_sii_subpath = os.path.join(vehicle_def_subdir, model_sii) model_sii_path = os.path.join(orig_project_path, model_sii_subpath) @@ -2997,20 +3370,23 @@ def execute(self, context): sii_exists = True break - if not sii_exists: - lprint("E Can't find referenced 'model_sii' file for texture portion %r, aborting SII write!", (texture_portion.id,)) + 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,)) return {'CANCELLED'} - # assamble paintjob properties that will be written in each SII (currently: paint_job_mask, ) + # assamble paintjob properties that will be written in overrides SII (currently: paint_job_mask, flake_uvscale, flake_vratio) pj_props = OrderedDict() + # collect paintjob mask texture if not self.export_configs_only: - rel_tobj_path = os.path.relpath(texture_portions_tobj_paths[unit_id], orig_project_path) + tobj_paths_unit_id = texture_portions[parent].id if parent else unit_id + rel_tobj_path = os.path.relpath(texture_portions_tobj_paths[tobj_paths_unit_id], orig_project_path) pj_props["paint_job_mask"] = _path_utils.readable_norm("/" + rel_tobj_path) - # export either master paint job config or override + # export either master paint job config or collect override if is_master: + 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!", @@ -3019,46 +3395,40 @@ def execute(self, context): # 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: + 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,)) return {'CANCELLED'} # unit name of referenced model sii used for suitable_for field in master paint jobs - master_model_sii_unit_name = model_sii_cont[0].id + pj_suitable_for = [] + if model_sii_cont: + pj_suitable_for.append(model_sii_cont[0].id) - # config path: "/def/vehicle/truck//paint_job/.sii" - config_path = os.path.join(orig_project_path, truck_def_subdir) + # if there are any other suitables in master texture portion also add it + suitable_for = texture_portion.get_prop("suitable_for", []) + pj_suitable_for.extend(suitable_for) + + # config path: "/def/vehicle///paint_job/.sii" + config_path = os.path.join(orig_project_path, vehicle_def_subdir) config_path = os.path.join(config_path, "paint_job") config_path = os.path.join(config_path, suffixed_pj_unit_name + ".sii") # full paint job unit name: ..paint_job pj_full_unit_name = suffixed_pj_unit_name + "." + brand_model_token + ".paint_job" - assert self.export_sii(config_path, pj_token, pj_full_unit_name, pj_props, True, master_model_sii_unit_name) + assert self.export_master_sii(config_path, pj_token, pj_full_unit_name, pj_props, pj_suitable_for) lprint("I Created master SII config for %r: %r", (texture_portion.id, config_path)) else: model_type = str(model_sii).split("/")[0] - if model_type in ("accessory", "cabin", "chassis"): + if model_type in ("accessory", "cabin", "chassis", "body"): model_sii_cont = _sii_container.get_data_from_file(model_sii_path) - truck_acc_unit_name = model_sii_cont[0].id.split(".")[0] # first token - acc_type_unit_name = model_sii_cont[0].id.split(".")[-1] # last token - - # config path: "/def/vehicle/truck//accessory//paint_job/..sii" - config_path = os.path.join(orig_project_path, truck_def_subdir) - config_path = os.path.join(config_path, model_type) - - if model_type == "accessory": - config_path = os.path.join(config_path, acc_type_unit_name) - - config_path = os.path.join(config_path, "paint_job") - - # for overrides full paint job name is always .simplepj - pj_full_unit_name = ".simplepj" + acc_id_token = model_sii_cont[0].id.split(".")[0] # first token + acc_type_token = model_sii_cont[0].id.split(".")[-1] # last token if parent: portion_size = [float(i) for i in texture_portions[parent].get_prop("size")] @@ -3070,9 +3440,7 @@ def execute(self, context): for master_portion in master_portions: master_size = [float(i) for i in master_portion.get_prop("size")] - master_unit_suffix = master_portion.get_prop("master_unit_suffix") - - curr_config_path = os.path.join(config_path, pj_token + master_unit_suffix + "." + truck_acc_unit_name + ".sii") + master_unit_suffix = master_portion.get_prop("master_unit_suffix", "") if self.pjs_flipflake: @@ -3083,16 +3451,30 @@ def execute(self, context): # then just divide original texture height with calculated one pj_props["flake_vratio"] = portion_size[1] / (master_size[1] * portion_size[0] / master_size[0]) - assert self.export_sii(curr_config_path, pj_token, pj_full_unit_name, pj_props, False) - lprint("I Created override SII config for %r: %r", (texture_portion.id, config_path)) + # don't create override if there is no properties to override! + if len(pj_props) == 0: + continue + + # ensure current master portion has it's own overrides + config_path = os.path.join(overrides_config_dir, pj_token + master_unit_suffix + ".sii") + if config_path not in overrides: + overrides[config_path] = PaintjobTools.GeneratePaintjob.Overrides() + + # now add current accessory to overrides + overrides[config_path].add_accessory(acc_type_token + "." + acc_id_token, pj_props) else: - lprint("E Can not create paintjob config for texture portion: %r, as 'model_sii' property is not one of: " + 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,)) return {'CANCELLED'} + # export overrides SII files + for config_path in overrides: + assert overrides[config_path].export_to_sii(self, config_path) + lprint("I Create override SII: %r", (config_path,)) + # finally we can remove original TGA if not self.preserve_common_texture and os.path.isfile(self.common_texture_path): os.remove(self.common_texture_path) diff --git a/addon/io_scs_tools/operators/wm.py b/addon/io_scs_tools/operators/wm.py index 0a08015..986ac5b 100644 --- a/addon/io_scs_tools/operators/wm.py +++ b/addon/io_scs_tools/operators/wm.py @@ -97,6 +97,8 @@ class Show3DViewReport(bpy.types.Operator): """Used for saving progress message inside class to be able to retrieve it on open gl draw.""" __static_abort = False """Used to propage abort message to all instances, so when abort is requested all instances will kill itself.""" + __static_scroll_pos = 0 + """Used to designate current scroll position in case not all warnings can be shown in 3D view.""" esc_abort = 0 """Used for staging ESC key press in operator: @@ -164,6 +166,14 @@ def get_lines(): lines = [] lines.extend(Show3DViewReport.__static_progress_message_l) lines.extend(Show3DViewReport.__static_message_l) + + # do scrolling + + lines_to_scroll = Show3DViewReport.__static_scroll_pos + while lines_to_scroll > 0: + lines.pop(0) + lines_to_scroll = lines_to_scroll - 1 + return lines @staticmethod @@ -200,6 +210,15 @@ def is_in_btn_area(x, y, btn_area): return (btn_area[0] < x < btn_area[1] and btn_area[2] < y < btn_area[3]) + @staticmethod + def is_scrolled(): + """Tells if text is scrolled down. + + :return: True if we are not on zero scroll position; False otherwise + :rtype: bool + """ + return Show3DViewReport.__static_scroll_pos != 0 + def __init__(self): Show3DViewReport.__static_running_instances += 1 @@ -280,7 +299,19 @@ def modal(self, context, event): if Show3DViewReport.is_in_btn_area(curr_x, curr_y, _OP_consts.View3DReport.HIDE_BTN_AREA): # show/hide Show3DViewReport.__static_is_shown = not Show3DViewReport.__static_is_shown + _view3d_utils.tag_redraw_all_view3d() + return {'RUNNING_MODAL'} + + # scrool up/down + if Show3DViewReport.is_in_btn_area(curr_x, curr_y, _OP_consts.View3DReport.SCROLLUP_BTN_AREA): + + Show3DViewReport.__static_scroll_pos = max(Show3DViewReport.__static_scroll_pos - 5, 0) + _view3d_utils.tag_redraw_all_view3d() + return {'RUNNING_MODAL'} + + elif Show3DViewReport.is_in_btn_area(curr_x, curr_y, _OP_consts.View3DReport.SCROLLDOWN_BTN_AREA): + Show3DViewReport.__static_scroll_pos = min(Show3DViewReport.__static_scroll_pos + 5, len(Show3DViewReport.__static_message_l)) _view3d_utils.tag_redraw_all_view3d() return {'RUNNING_MODAL'} diff --git a/addon/io_scs_tools/properties/material.py b/addon/io_scs_tools/properties/material.py index 38e213b..f883721 100644 --- a/addon/io_scs_tools/properties/material.py +++ b/addon/io_scs_tools/properties/material.py @@ -673,6 +673,14 @@ def update_shader_texture_reflection_settings(self, context): update=update_shader_attribute_tint_opacity ) + shader_attribute_queue_bias = IntProperty( + name="Queue Bias", + description="SCS shader 'Queue Bias' value", + default=2, + options={'HIDDEN'}, + update=__update_look__ + ) + # TEXTURE: BASE shader_texture_base = StringProperty( name="Texture Base", diff --git a/addon/io_scs_tools/properties/object.py b/addon/io_scs_tools/properties/object.py index 0353e27..7e9d49b 100644 --- a/addon/io_scs_tools/properties/object.py +++ b/addon/io_scs_tools/properties/object.py @@ -535,7 +535,7 @@ def locator_preview_model_path_update(self, context): """ obj = context.object - if _preview_models.load(obj): + if _preview_models.load(obj, deep_reload=True): # fix selection because in case of actual loading model from file selection will be messed up obj.select = True @@ -838,11 +838,14 @@ def locator_prefab_type_update(self, context): (_PL_consts.PSP.BUS_STATION, (str(_PL_consts.PSP.BUS_STATION), "Bus Station", "")), (_PL_consts.PSP.CAMERA_POINT, (str(_PL_consts.PSP.CAMERA_POINT), "Camera Point", "")), (_PL_consts.PSP.COMPANY_POS, (str(_PL_consts.PSP.COMPANY_POS), "Company Point", "")), + (_PL_consts.PSP.COMPANY_UNLOAD_POS, (str(_PL_consts.PSP.COMPANY_UNLOAD_POS), "Company Unload Point", "")), # (_PL_consts.PSP.CUSTOM, (str(_PL_consts.PSP.CUSTOM), "Custom", "")), (_PL_consts.PSP.GARAGE_POS, (str(_PL_consts.PSP.GARAGE_POS), "Garage Point", "")), (_PL_consts.PSP.GAS_POS, (str(_PL_consts.PSP.GAS_POS), "Gas Station", "")), # (_PL_consts.PSP.HOTEL, (str(_PL_consts.PSP.HOTEL), "Hotel", "")), + (_PL_consts.PSP.LONG_TRAILER_POS, (str(_PL_consts.PSP.LONG_TRAILER_POS), "Long Trailer", "")), # (_PL_consts.PSP.MEET_POS, (str(_PL_consts.PSP.MEET_POS), "Meet", "")), + (_PL_consts.PSP.TRAILER_SPAWN, (str(_PL_consts.PSP.TRAILER_SPAWN), "Owned Trailer", "")), (_PL_consts.PSP.PARKING, (str(_PL_consts.PSP.PARKING), "Parking", "")), (_PL_consts.PSP.RECRUITMENT_POS, (str(_PL_consts.PSP.RECRUITMENT_POS), "Recruitment", "")), (_PL_consts.PSP.SERVICE_POS, (str(_PL_consts.PSP.SERVICE_POS), "Service Station", "")), diff --git a/addon/io_scs_tools/shader_presets.txt b/addon/io_scs_tools/shader_presets.txt index b4086c6..54bbcf8 100644 --- a/addon/io_scs_tools/shader_presets.txt +++ b/addon/io_scs_tools/shader_presets.txt @@ -1430,6 +1430,17 @@ Shader { TexCoord: ( 0 ) } } +Shader { + PresetName: "retroreflective" + Effect: "eut2.retroreflective" + Flavors: ( "NMAP_TS|NMAP_TS_UV|NMAP_TS_16|NMAP_TS_UV_16" "RETROREFLECTIVE_DIM_ALLDIR|RETROREFLECTIVE_PIKO_ALLDIR" "RETROREFLECTIVE_DECAL" ) + Flags: 0 + Texture { + Tag: "texture[X]:texture_base" + Value: "" + TexCoord: ( 0 ) + } +} Shader { PresetName: "shadowmap" Effect: "eut2.shadowmap" @@ -1846,6 +1857,11 @@ Flavor { Flavor { Type: "DEPTH" Name: "decal" + Attribute { + Format: INT + Tag: "queue_bias" + Value: ( 0 ) + } } Flavor { Type: "ENVMAP" @@ -1963,6 +1979,18 @@ Flavor { Type: "PAINT" Name: "paint" } +Flavor { + Type: "RETROREFLECTIVE_DECAL" + Name: "decal.over" +} +Flavor { + Type: "RETROREFLECTIVE_DIM_ALLDIR" + Name: "dim.alldir" +} +Flavor { + Type: "RETROREFLECTIVE_PIKO_ALLDIR" + Name: "piko.alldir" +} Flavor { Type: "TEXGEN0" Name: "tg0" diff --git a/addon/io_scs_tools/supported_effects.bin b/addon/io_scs_tools/supported_effects.bin index a5b9611d7e82d58f5e52e95808cdcab7bf3f0562..ef6efe16194d04130a2079cd3feab3194ddceaaa 100644 GIT binary patch literal 193943 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 EKSXELPXGV_ literal 194087 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 2: # multiple users, switch of fake one as data will get saved anyway + world.use_fake_user = False diff --git a/addon/io_scs_tools/utils/curve.py b/addon/io_scs_tools/utils/curve.py index b64ad60..a56490b 100644 --- a/addon/io_scs_tools/utils/curve.py +++ b/addon/io_scs_tools/utils/curve.py @@ -383,6 +383,9 @@ def curves_intersect(curve1_p1, curve1_t1, curve1_p2, curve1_t2, length1, start2 = smooth_curve_position(curve2_p1, curve2_t1, curve2_p2, curve2_t2, pos2 / length2) end2 = smooth_curve_position(curve2_p1, curve2_t1, curve2_p2, curve2_t2, (pos2 + step2) / length2) + if abs(start1[1] - start2[1]) > 4.0 or abs(end1[1] - end2[1]) > 4.0: + continue + denom = ((end2[2] - start2[2]) * (end1[0] - start1[0])) - ((end2[0] - start2[0]) * (end1[2] - start1[2])) nume_a = ((end2[0] - start2[0]) * (start1[2] - start2[2])) - ((end2[2] - start2[2]) * (start1[0] - start2[0])) nume_b = ((end1[0] - start1[0]) * (start1[2] - start2[2])) - ((end1[2] - start1[2]) * (start1[0] - start2[0])) diff --git a/addon/io_scs_tools/utils/material.py b/addon/io_scs_tools/utils/material.py index 4cb1904..852a8de 100644 --- a/addon/io_scs_tools/utils/material.py +++ b/addon/io_scs_tools/utils/material.py @@ -307,7 +307,7 @@ def set_shader_data_to_material(material, section, is_import=False, override_bac created_attributes[attribute_type] = material.scs_props["shader_attribute_" + attribute_type] - elif attribute_type in ("shininess", "add_ambient", "reflection", "reflection2", "shadow_bias", "tint_opacity"): + elif attribute_type in ("shininess", "add_ambient", "reflection", "reflection2", "shadow_bias", "tint_opacity", "queue_bias"): if not old_value: material.scs_props["shader_attribute_" + attribute_type] = attribute_data['Value'][0] diff --git a/addon/io_scs_tools/utils/mesh.py b/addon/io_scs_tools/utils/mesh.py index 1a0205a..54be0ff 100644 --- a/addon/io_scs_tools/utils/mesh.py +++ b/addon/io_scs_tools/utils/mesh.py @@ -401,13 +401,22 @@ def bm_make_vc_layer(pim_version, bm, vc_layer_name, vc_layer_data, multiplier=1 :param vc_layer_data: Vertex Color Layer data :type vc_layer_data: list """ + # only 5 and 7 versions are supported currently + assert (pim_version == 5 or pim_version == 7) + color_lay = bm.loops.layers.color.new(vc_layer_name) - color_a_lay = bm.loops.layers.color.new(vc_layer_name + _MESH_consts.vcol_a_suffix) + + vc_alpha_layer_name = vc_layer_name + _MESH_consts.vcol_a_suffix + if pim_version == 5 and len(vc_layer_data[0]) == 4: + color_a_lay = bm.loops.layers.color.new(vc_alpha_layer_name) + elif pim_version == 7 and len(vc_layer_data[0][0]) == 4: + color_a_lay = bm.loops.layers.color.new(vc_alpha_layer_name) + for face_i, face in enumerate(bm.faces): f_v = [x.index for x in face.verts] for loop_i, loop in enumerate(face.loops): - alpha = 1.0 - if pim_version < 6: + alpha = -1.0 + if pim_version == 5: if len(vc_layer_data[0]) == 3: vcol = vc_layer_data[f_v[loop_i]] else: @@ -421,9 +430,12 @@ def bm_make_vc_layer(pim_version, bm, vc_layer_name, vc_layer_data, multiplier=1 alpha = vc_layer_data[face_i][loop_i][3] vcol = (vcol[0] / 2 / multiplier, vcol[1] / 2 / multiplier, vcol[2] / 2 / multiplier) - vcol_a = (alpha / 2 / multiplier,) * 3 loop[color_lay] = vcol - loop[color_a_lay] = vcol_a + + if alpha != -1.0: + assert color_a_lay + vcol_a = (alpha / 2 / multiplier,) * 3 + loop[color_a_lay] = vcol_a def bm_delete_loose(mesh): diff --git a/addon/io_scs_tools/utils/object.py b/addon/io_scs_tools/utils/object.py index 8bcd154..9304147 100644 --- a/addon/io_scs_tools/utils/object.py +++ b/addon/io_scs_tools/utils/object.py @@ -759,7 +759,7 @@ def get_mesh(obj): if _get_scs_globals().export_output_type.startswith('EF'): if _get_scs_globals().export_apply_modifiers: if _get_scs_globals().export_exclude_edgesplit: - disabled_modifiers.append(disable_modifiers(obj, modifier_type_to_disable='EDGE_SPLIT')) + disabled_modifiers.extend(disable_modifiers(obj, modifier_type_to_disable='EDGE_SPLIT')) mesh = obj.to_mesh(scene, True, 'PREVIEW') else: mesh = obj.data @@ -768,10 +768,10 @@ def get_mesh(obj): mesh = obj.to_mesh(scene, True, 'PREVIEW') else: if _get_scs_globals().export_include_edgesplit: - disabled_modifiers.append(disable_modifiers(obj, modifier_type_to_disable='EDGE_SPLIT', inverse=True)) + disabled_modifiers.extend(disable_modifiers(obj, modifier_type_to_disable='EDGE_SPLIT', inverse=True)) mesh = obj.to_mesh(scene, True, 'PREVIEW') else: - disabled_modifiers.append(disable_modifiers(obj, modifier_type_to_disable='ANY', inverse=True)) + disabled_modifiers.extend(disable_modifiers(obj, modifier_type_to_disable='ANY', inverse=True)) mesh = obj.to_mesh(scene, True, 'PREVIEW') restore_modifiers(obj, disabled_modifiers)