From 7ca2c43d73c45a982e7e9708149a68026e90444a Mon Sep 17 00:00:00 2001 From: Simon Lusenc Date: Tue, 17 Dec 2019 13:20:05 +0100 Subject: [PATCH] Release - 2.0 --- addon/io_scs_tools/__init__.py | 364 +-- addon/io_scs_tools/consts.py | 103 +- addon/io_scs_tools/exp/__init__.py | 9 +- addon/io_scs_tools/exp/pia.py | 20 +- addon/io_scs_tools/exp/pic.py | 22 +- addon/io_scs_tools/exp/pim/exporter.py | 68 +- addon/io_scs_tools/exp/pim/globall.py | 5 +- addon/io_scs_tools/exp/pim/material.py | 1 + addon/io_scs_tools/exp/pim/piece_skin.py | 87 + .../{skin_stream.py => piece_skin_stream.py} | 75 +- addon/io_scs_tools/exp/pim/skin.py | 53 - addon/io_scs_tools/exp/pim_ef/exporter.py | 66 +- addon/io_scs_tools/exp/pim_ef/globall.py | 5 +- .../exp/pim_ef/{skin.py => piece_skin.py} | 6 +- .../{skin_stream.py => piece_skin_stream.py} | 4 +- addon/io_scs_tools/exp/pip/curve.py | 10 +- addon/io_scs_tools/exp/pip/exporter.py | 30 +- addon/io_scs_tools/exp/pis.py | 5 +- addon/io_scs_tools/exp/pit.py | 11 +- addon/io_scs_tools/exp/pit_ef.py | 2 + addon/io_scs_tools/imp/pia.py | 40 +- addon/io_scs_tools/imp/pim.py | 265 ++- addon/io_scs_tools/imp/pim_ef.py | 136 +- addon/io_scs_tools/imp/pip.py | 20 +- addon/io_scs_tools/imp/pis.py | 8 +- addon/io_scs_tools/imp/pit.py | 6 +- addon/io_scs_tools/imp/pix.py | 73 +- addon/io_scs_tools/imp/tobj.py | 13 +- .../internals/callbacks/lighting_east_lock.py | 42 +- .../internals/callbacks/open_gl.py | 32 +- .../internals/callbacks/persistent.py | 51 +- .../internals/connections/collector.py | 14 +- .../internals/connections/core.py | 8 +- .../wrappers/{group.py => collection.py} | 83 +- .../internals/containers/config.py | 1578 +++++++++---- .../internals/containers/parsers/pix.py | 9 +- .../internals/containers/parsers/sii.py | 28 +- .../internals/containers/writers/pix.py | 9 +- .../io_scs_tools/internals/icons/__init__.py | 154 +- addon/io_scs_tools/internals/inventory.py | 46 + .../io_scs_tools/internals/looks/__init__.py | 80 +- addon/io_scs_tools/internals/open_gl/cache.py | 180 ++ addon/io_scs_tools/internals/open_gl/core.py | 829 +++---- .../internals/open_gl/locators/collider.py | 91 +- .../internals/open_gl/locators/model.py | 58 +- .../internals/open_gl/locators/prefab.py | 291 ++- .../internals/open_gl/primitive.py | 798 ++++--- .../internals/open_gl/shaders/__init__.py | 78 + .../shaders/smooth_color_clipped_3d_frag.glsl | 15 + .../shaders/smooth_color_clipped_3d_vert.glsl | 22 + .../smooth_color_stipple_clipped_3d_frag.glsl | 20 + .../smooth_color_stipple_clipped_3d_vert.glsl | 17 + .../open_gl/storage/terrain_points.py | 2 +- .../internals/persistent/file_load.py | 112 +- .../internals/persistent/file_save.py | 74 +- .../internals/persistent/initialization.py | 178 +- .../internals/persistent/loop_check.py | 102 +- .../internals/persistent/open_gl.py | 178 ++ .../internals/persistent/shaders_update.py | 36 + .../internals/preview_models/__init__.py | 259 +- .../internals/shaders/__init__.py | 11 +- addon/io_scs_tools/internals/shaders/base.py | 52 + .../internals/shaders/eut2/__init__.py | 4 + .../shaders/eut2/building/add_env_day.py | 7 - .../internals/shaders/eut2/building/day.py | 1 - .../shaders/eut2/building/lvcol_day.py | 1 - .../shaders/eut2/decalshadow/__init__.py | 26 +- .../internals/shaders/eut2/dif/__init__.py | 311 +-- .../shaders/eut2/dif_anim/__init__.py | 115 +- .../eut2/dif_anim/anim_blend_factor_ng.py | 13 +- .../shaders/eut2/dif_lum/__init__.py | 166 +- .../shaders/eut2/dif_lum_spec/__init__.py | 189 +- .../shaders/eut2/dif_spec/__init__.py | 42 +- .../shaders/eut2/dif_spec_add_env/__init__.py | 5 +- .../eut2/dif_spec_add_env/nofresnel.py | 1 - .../eut2/dif_spec_fade_dif_spec/__init__.py | 84 +- .../dif_spec_fade_dif_spec/detail_nmap.py | 126 +- .../dif_spec_fade_dif_spec/detail_setup_ng.py | 2 +- .../eut2/dif_spec_mult_dif_spec/__init__.py | 133 +- .../eut2/dif_spec_mult_dif_spec/add_env.py | 13 +- .../__init__.py | 69 +- .../shaders/eut2/dif_spec_oclu/__init__.py | 58 +- .../eut2/dif_spec_oclu_add_env/__init__.py | 13 +- .../dif_spec_oclu_weight_add_env/__init__.py | 28 +- .../eut2/dif_spec_over_dif_opac/__init__.py | 49 +- .../shaders/eut2/dif_spec_weight/__init__.py | 20 +- .../eut2/dif_spec_weight_add_env/__init__.py | 19 +- .../eut2/dif_spec_weight_mult2/__init__.py | 85 +- .../dif_spec_weight_mult2_weight2/__init__.py | 128 +- .../__init__.py | 184 +- .../shaders/eut2/dif_weight_dif/__init__.py | 158 +- .../shaders/eut2/fakeshadow/__init__.py | 54 +- .../internals/shaders/eut2/flare/__init__.py | 18 +- .../internals/shaders/eut2/glass/__init__.py | 502 ++-- .../internals/shaders/eut2/grass/__init__.py | 35 +- .../internals/shaders/eut2/lamp/__init__.py | 105 +- .../internals/shaders/eut2/lamp/add_env.py | 9 +- .../shaders/eut2/light_tex/__init__.py | 58 +- .../internals/shaders/eut2/lightmap/night.py | 1 - .../shaders/eut2/mlaaweight/__init__.py | 34 +- .../internals/shaders/eut2/none/__init__.py | 51 +- .../shaders/eut2/reflective/__init__.py | 151 +- .../shaders/eut2/retroreflective/__init__.py | 36 +- .../shaders/eut2/shadowmap/__init__.py | 18 +- .../shaders/eut2/shadowonly/__init__.py | 36 +- .../internals/shaders/eut2/sign/__init__.py | 16 +- .../internals/shaders/eut2/sky/__init__.py | 198 +- .../shaders/eut2/std_node_groups/add_env.py | 219 -- .../eut2/std_node_groups/add_env_ng.py | 230 ++ .../{alpha_remap.py => alpha_remap_ng.py} | 0 .../eut2/std_node_groups/compose_lighting.py | 139 -- .../std_node_groups/compose_lighting_ng.py | 185 ++ .../{fresnel.py => fresnel_ng.py} | 2 +- ...lampmask_mixer.py => lampmask_mixer_ng.py} | 24 +- .../std_node_groups/lighting_evaluator_ng.py | 356 +++ .../eut2/std_node_groups/linear_to_srgb_ng.py | 161 ++ .../{mult2_mix.py => mult2_mix_ng.py} | 11 +- .../eut2/std_node_groups/refl_normal.py | 91 - .../eut2/std_node_groups/refl_normal_ng.py | 104 + .../std_node_groups/scs_uvs_combine_ng.py | 85 + .../std_node_groups/scs_uvs_separate_ng.py | 84 + .../eut2/std_node_groups/vcolor_input.py | 131 -- .../eut2/std_node_groups/vcolor_input_ng.py | 139 ++ .../eut2/std_node_groups/window_uv_factor.py | 127 - .../eut2/std_node_groups/window_uv_offset.py | 187 -- .../shaders/eut2/std_passes/add_env.py | 85 +- .../shaders/eut2/truckpaint/__init__.py | 162 +- .../shaders/eut2/truckpaint/airbrush.py | 9 +- .../shaders/eut2/truckpaint/colormask.py | 9 +- .../shaders/eut2/unlit_tex/__init__.py | 195 +- .../internals/shaders/eut2/unlit_tex/a8.py | 62 + .../shaders/eut2/unlit_vcol_tex/__init__.py | 311 +-- .../internals/shaders/eut2/water/__init__.py | 373 +-- .../shaders/eut2/water/mix_factor_ng.py | 26 +- .../shaders/eut2/water/water_stream_ng.py | 127 + .../internals/shaders/eut2/window/day.py | 38 +- .../internals/shaders/eut2/window/night.py | 115 +- .../shaders/eut2/window/window_final_uv_ng.py | 104 + .../eut2/window/window_offset_factor_ng.py | 115 + .../eut2/window/window_uv_offset_ng.py | 262 +++ .../internals/shaders/flavors/alpha_test.py | 95 +- .../internals/shaders/flavors/asafew.py | 10 +- .../internals/shaders/flavors/awhite.py | 12 +- .../internals/shaders/flavors/blend_add.py | 45 +- .../internals/shaders/flavors/blend_mult.py | 92 +- .../internals/shaders/flavors/blend_over.py | 21 +- .../shaders/flavors/nmap/__init__.py | 109 +- .../internals/shaders/flavors/paint.py | 21 +- .../internals/shaders/flavors/tg0.py | 19 +- .../internals/shaders/flavors/tg1.py | 19 +- .../io_scs_tools/internals/shaders/shader.py | 81 +- .../shaders/std_node_groups/alpha_test_ng.py | 129 + .../shaders/std_node_groups/blend_add_ng.py | 125 + .../shaders/std_node_groups/blend_mult_ng.py | 125 + addon/io_scs_tools/internals/structure.py | 24 +- addon/io_scs_tools/operators/__init__.py | 20 +- addon/io_scs_tools/operators/bases/export.py | 171 ++ .../io_scs_tools/operators/bases/selection.py | 6 +- addon/io_scs_tools/operators/bases/view.py | 18 +- addon/io_scs_tools/operators/material.py | 705 +++++- addon/io_scs_tools/operators/mesh.py | 118 +- addon/io_scs_tools/operators/object.py | 1110 ++++++--- addon/io_scs_tools/operators/scene.py | 877 +++---- addon/io_scs_tools/operators/wm.py | 346 ++- addon/io_scs_tools/operators/world.py | 430 +--- addon/io_scs_tools/properties/__init__.py | 29 +- addon/io_scs_tools/properties/action.py | 20 +- .../{world.py => addon_preferences.py} | 817 ++++--- .../properties/dynamic/__init__.py | 140 ++ .../io_scs_tools/properties/dynamic/scene.py | 4 +- addon/io_scs_tools/properties/material.py | 384 +-- addon/io_scs_tools/properties/mesh.py | 23 +- addon/io_scs_tools/properties/object.py | 224 +- addon/io_scs_tools/properties/scene.py | 152 +- addon/io_scs_tools/properties/workspace.py | 91 + addon/io_scs_tools/shader_presets.txt | 43 +- addon/io_scs_tools/supported_effects.bin | Bin 193943 -> 213398 bytes addon/io_scs_tools/ui/__init__.py | 28 +- .../ui/banners/.scs_bt_banner.png | Bin 0 -> 7598 bytes .../ui/banners/.scs_bt_banner_with_ctrls.png | Bin 0 -> 7720 bytes addon/io_scs_tools/ui/icons/.scs_bt_logo.png | Bin 4838 -> 0 bytes .../ui/icons/black/.01_mesh_model_object.png | Bin 0 -> 698 bytes ....02_object_with_shadow_caster_material.png | Bin 0 -> 750 bytes .../black/.03_object_with_glass_material.png | Bin 0 -> 1306 bytes .../.04_object_with_physical_material.png | Bin 0 -> 1706 bytes .../ui/icons/black/.05_locator_all_types.png | Bin 0 -> 748 bytes .../ui/icons/black/.06_locator_model.png | Bin 0 -> 911 bytes .../ui/icons/black/.07_locator_prefab.png | Bin 0 -> 897 bytes .../ui/icons/black/.08_locator_collision.png | Bin 0 -> 1032 bytes .../icons/black/.09_locator_prefab_node.png | Bin 0 -> 497 bytes .../icons/black/.10_locator_prefab_sign.png | Bin 0 -> 770 bytes .../icons/black/.11_locator_prefab_spawn.png | Bin 0 -> 876 bytes .../black/.12_locator_prefab_semaphore.png | Bin 0 -> 1035 bytes .../black/.13_locator_prefab_navigation.png | Bin 0 -> 1116 bytes .../ui/icons/black/.14_locator_prefab_map.png | Bin 0 -> 1651 bytes .../black/.15_locator_prefab_trigger.png | Bin 0 -> 826 bytes .../ui/icons/black/.16_collider_box.png | Bin 0 -> 651 bytes .../ui/icons/black/.17_collider_sphere.png | Bin 0 -> 859 bytes .../ui/icons/black/.18_collider_capsule.png | Bin 0 -> 668 bytes .../ui/icons/black/.19_collider_cylinder.png | Bin 0 -> 873 bytes .../ui/icons/black/.20_collider_convex.png | Bin 0 -> 1105 bytes .../ui/icons/black/.21_scs_root_object.png | Bin 0 -> 863 bytes .../ui/icons/black/.22_scs_object_menu.png | Bin 0 -> 1676 bytes .../ui/icons/black/.icon_scs_bt_logo.png | Bin 0 -> 1676 bytes .../{ => legacy}/.01_mesh_model_object.png | Bin ....02_object_with_shadow_caster_material.png | Bin .../.03_object_with_glass_material.png | Bin .../.04_object_with_physical_material.png | Bin .../{ => legacy}/.05_locator_all_types.png | Bin .../icons/{ => legacy}/.06_locator_model.png | Bin .../icons/{ => legacy}/.07_locator_prefab.png | Bin .../{ => legacy}/.08_locator_collision.png | Bin .../{ => legacy}/.09_locator_prefab_node.png | Bin .../{ => legacy}/.10_locator_prefab_sign.png | Bin .../{ => legacy}/.11_locator_prefab_spawn.png | Bin .../.12_locator_prefab_semaphore.png | Bin .../.13_locator_prefab_navigation.png | Bin .../{ => legacy}/.14_locator_prefab_map.png | Bin .../.15_locator_prefab_trigger.png | Bin .../icons/{ => legacy}/.16_collider_box.png | Bin .../{ => legacy}/.17_collider_sphere.png | Bin .../{ => legacy}/.18_collider_capsule.png | Bin .../{ => legacy}/.19_collider_cylinder.png | Bin .../{ => legacy}/.20_collider_convex.png | Bin .../{ => legacy}/.21_scs_root_object.png | Bin .../.22_scs_object_menu.png} | Bin .../icons/{ => legacy}/.icon_scs_bt_logo.png | Bin .../ui/icons/white/.01_mesh_model_object.png | Bin 0 -> 726 bytes ....02_object_with_shadow_caster_material.png | Bin 0 -> 765 bytes .../white/.03_object_with_glass_material.png | Bin 0 -> 1322 bytes .../.04_object_with_physical_material.png | Bin 0 -> 1764 bytes .../ui/icons/white/.05_locator_all_types.png | Bin 0 -> 779 bytes .../ui/icons/white/.06_locator_model.png | Bin 0 -> 933 bytes .../ui/icons/white/.07_locator_prefab.png | Bin 0 -> 919 bytes .../ui/icons/white/.08_locator_collision.png | Bin 0 -> 1058 bytes .../icons/white/.09_locator_prefab_node.png | Bin 0 -> 514 bytes .../icons/white/.10_locator_prefab_sign.png | Bin 0 -> 798 bytes .../icons/white/.11_locator_prefab_spawn.png | Bin 0 -> 877 bytes .../white/.12_locator_prefab_semaphore.png | Bin 0 -> 1069 bytes .../white/.13_locator_prefab_navigation.png | Bin 0 -> 1124 bytes .../ui/icons/white/.14_locator_prefab_map.png | Bin 0 -> 1732 bytes .../white/.15_locator_prefab_trigger.png | Bin 0 -> 870 bytes .../ui/icons/white/.16_collider_box.png | Bin 0 -> 691 bytes .../ui/icons/white/.17_collider_sphere.png | Bin 0 -> 911 bytes .../ui/icons/white/.18_collider_capsule.png | Bin 0 -> 685 bytes .../ui/icons/white/.19_collider_cylinder.png | Bin 0 -> 913 bytes .../ui/icons/white/.20_collider_convex.png | Bin 0 -> 1146 bytes .../ui/icons/white/.21_scs_root_object.png | Bin 0 -> 882 bytes .../ui/icons/white/.22_scs_object_menu.png | Bin 0 -> 1685 bytes .../ui/icons/white/.icon_scs_bt_logo.png | Bin 0 -> 1685 bytes addon/io_scs_tools/ui/material.py | 1216 +++++----- addon/io_scs_tools/ui/mesh.py | 53 +- addon/io_scs_tools/ui/object.py | 2091 ++++++++--------- addon/io_scs_tools/ui/output.py | 239 ++ addon/io_scs_tools/ui/scene.py | 473 ---- addon/io_scs_tools/ui/shared.py | 223 +- addon/io_scs_tools/ui/tool_shelf.py | 380 ++- addon/io_scs_tools/ui/workspace.py | 295 +++ addon/io_scs_tools/ui/world.py | 160 +- addon/io_scs_tools/utils/__init__.py | 50 +- addon/io_scs_tools/utils/collection.py | 46 + addon/io_scs_tools/utils/convert.py | 59 +- addon/io_scs_tools/utils/material.py | 367 ++- addon/io_scs_tools/utils/mesh.py | 54 +- addon/io_scs_tools/utils/object.py | 532 +++-- addon/io_scs_tools/utils/path.py | 70 +- addon/io_scs_tools/utils/printout.py | 2 +- addon/io_scs_tools/utils/property.py | 57 +- addon/io_scs_tools/utils/view3d.py | 122 +- 269 files changed, 17203 insertions(+), 10653 deletions(-) create mode 100644 addon/io_scs_tools/exp/pim/piece_skin.py rename addon/io_scs_tools/exp/pim/{skin_stream.py => piece_skin_stream.py} (74%) delete mode 100644 addon/io_scs_tools/exp/pim/skin.py rename addon/io_scs_tools/exp/pim_ef/{skin.py => piece_skin.py} (82%) rename addon/io_scs_tools/exp/pim_ef/{skin_stream.py => piece_skin_stream.py} (88%) rename addon/io_scs_tools/internals/connections/wrappers/{group.py => collection.py} (78%) create mode 100644 addon/io_scs_tools/internals/open_gl/cache.py create mode 100644 addon/io_scs_tools/internals/open_gl/shaders/__init__.py create mode 100644 addon/io_scs_tools/internals/open_gl/shaders/smooth_color_clipped_3d_frag.glsl create mode 100644 addon/io_scs_tools/internals/open_gl/shaders/smooth_color_clipped_3d_vert.glsl create mode 100644 addon/io_scs_tools/internals/open_gl/shaders/smooth_color_stipple_clipped_3d_frag.glsl create mode 100644 addon/io_scs_tools/internals/open_gl/shaders/smooth_color_stipple_clipped_3d_vert.glsl create mode 100644 addon/io_scs_tools/internals/persistent/open_gl.py create mode 100644 addon/io_scs_tools/internals/persistent/shaders_update.py create mode 100644 addon/io_scs_tools/internals/shaders/base.py delete mode 100644 addon/io_scs_tools/internals/shaders/eut2/std_node_groups/add_env.py create mode 100644 addon/io_scs_tools/internals/shaders/eut2/std_node_groups/add_env_ng.py rename addon/io_scs_tools/internals/shaders/eut2/std_node_groups/{alpha_remap.py => alpha_remap_ng.py} (100%) delete mode 100644 addon/io_scs_tools/internals/shaders/eut2/std_node_groups/compose_lighting.py create mode 100644 addon/io_scs_tools/internals/shaders/eut2/std_node_groups/compose_lighting_ng.py rename addon/io_scs_tools/internals/shaders/eut2/std_node_groups/{fresnel.py => fresnel_ng.py} (99%) rename addon/io_scs_tools/internals/shaders/eut2/std_node_groups/{lampmask_mixer.py => lampmask_mixer_ng.py} (96%) create mode 100644 addon/io_scs_tools/internals/shaders/eut2/std_node_groups/lighting_evaluator_ng.py create mode 100644 addon/io_scs_tools/internals/shaders/eut2/std_node_groups/linear_to_srgb_ng.py rename addon/io_scs_tools/internals/shaders/eut2/std_node_groups/{mult2_mix.py => mult2_mix_ng.py} (92%) delete mode 100644 addon/io_scs_tools/internals/shaders/eut2/std_node_groups/refl_normal.py create mode 100644 addon/io_scs_tools/internals/shaders/eut2/std_node_groups/refl_normal_ng.py create mode 100644 addon/io_scs_tools/internals/shaders/eut2/std_node_groups/scs_uvs_combine_ng.py create mode 100644 addon/io_scs_tools/internals/shaders/eut2/std_node_groups/scs_uvs_separate_ng.py delete mode 100644 addon/io_scs_tools/internals/shaders/eut2/std_node_groups/vcolor_input.py create mode 100644 addon/io_scs_tools/internals/shaders/eut2/std_node_groups/vcolor_input_ng.py delete mode 100644 addon/io_scs_tools/internals/shaders/eut2/std_node_groups/window_uv_factor.py delete mode 100644 addon/io_scs_tools/internals/shaders/eut2/std_node_groups/window_uv_offset.py create mode 100644 addon/io_scs_tools/internals/shaders/eut2/unlit_tex/a8.py create mode 100644 addon/io_scs_tools/internals/shaders/eut2/water/water_stream_ng.py create mode 100644 addon/io_scs_tools/internals/shaders/eut2/window/window_final_uv_ng.py create mode 100644 addon/io_scs_tools/internals/shaders/eut2/window/window_offset_factor_ng.py create mode 100644 addon/io_scs_tools/internals/shaders/eut2/window/window_uv_offset_ng.py create mode 100644 addon/io_scs_tools/internals/shaders/std_node_groups/alpha_test_ng.py create mode 100644 addon/io_scs_tools/internals/shaders/std_node_groups/blend_add_ng.py create mode 100644 addon/io_scs_tools/internals/shaders/std_node_groups/blend_mult_ng.py create mode 100644 addon/io_scs_tools/operators/bases/export.py rename addon/io_scs_tools/properties/{world.py => addon_preferences.py} (70%) create mode 100644 addon/io_scs_tools/properties/dynamic/__init__.py create mode 100644 addon/io_scs_tools/properties/workspace.py create mode 100644 addon/io_scs_tools/ui/banners/.scs_bt_banner.png create mode 100644 addon/io_scs_tools/ui/banners/.scs_bt_banner_with_ctrls.png delete mode 100644 addon/io_scs_tools/ui/icons/.scs_bt_logo.png create mode 100644 addon/io_scs_tools/ui/icons/black/.01_mesh_model_object.png create mode 100644 addon/io_scs_tools/ui/icons/black/.02_object_with_shadow_caster_material.png create mode 100644 addon/io_scs_tools/ui/icons/black/.03_object_with_glass_material.png create mode 100644 addon/io_scs_tools/ui/icons/black/.04_object_with_physical_material.png create mode 100644 addon/io_scs_tools/ui/icons/black/.05_locator_all_types.png create mode 100644 addon/io_scs_tools/ui/icons/black/.06_locator_model.png create mode 100644 addon/io_scs_tools/ui/icons/black/.07_locator_prefab.png create mode 100644 addon/io_scs_tools/ui/icons/black/.08_locator_collision.png create mode 100644 addon/io_scs_tools/ui/icons/black/.09_locator_prefab_node.png create mode 100644 addon/io_scs_tools/ui/icons/black/.10_locator_prefab_sign.png create mode 100644 addon/io_scs_tools/ui/icons/black/.11_locator_prefab_spawn.png create mode 100644 addon/io_scs_tools/ui/icons/black/.12_locator_prefab_semaphore.png create mode 100644 addon/io_scs_tools/ui/icons/black/.13_locator_prefab_navigation.png create mode 100644 addon/io_scs_tools/ui/icons/black/.14_locator_prefab_map.png create mode 100644 addon/io_scs_tools/ui/icons/black/.15_locator_prefab_trigger.png create mode 100644 addon/io_scs_tools/ui/icons/black/.16_collider_box.png create mode 100644 addon/io_scs_tools/ui/icons/black/.17_collider_sphere.png create mode 100644 addon/io_scs_tools/ui/icons/black/.18_collider_capsule.png create mode 100644 addon/io_scs_tools/ui/icons/black/.19_collider_cylinder.png create mode 100644 addon/io_scs_tools/ui/icons/black/.20_collider_convex.png create mode 100644 addon/io_scs_tools/ui/icons/black/.21_scs_root_object.png create mode 100644 addon/io_scs_tools/ui/icons/black/.22_scs_object_menu.png create mode 100644 addon/io_scs_tools/ui/icons/black/.icon_scs_bt_logo.png rename addon/io_scs_tools/ui/icons/{ => legacy}/.01_mesh_model_object.png (100%) rename addon/io_scs_tools/ui/icons/{ => legacy}/.02_object_with_shadow_caster_material.png (100%) rename addon/io_scs_tools/ui/icons/{ => legacy}/.03_object_with_glass_material.png (100%) rename addon/io_scs_tools/ui/icons/{ => legacy}/.04_object_with_physical_material.png (100%) rename addon/io_scs_tools/ui/icons/{ => legacy}/.05_locator_all_types.png (100%) rename addon/io_scs_tools/ui/icons/{ => legacy}/.06_locator_model.png (100%) rename addon/io_scs_tools/ui/icons/{ => legacy}/.07_locator_prefab.png (100%) rename addon/io_scs_tools/ui/icons/{ => legacy}/.08_locator_collision.png (100%) rename addon/io_scs_tools/ui/icons/{ => legacy}/.09_locator_prefab_node.png (100%) rename addon/io_scs_tools/ui/icons/{ => legacy}/.10_locator_prefab_sign.png (100%) rename addon/io_scs_tools/ui/icons/{ => legacy}/.11_locator_prefab_spawn.png (100%) rename addon/io_scs_tools/ui/icons/{ => legacy}/.12_locator_prefab_semaphore.png (100%) rename addon/io_scs_tools/ui/icons/{ => legacy}/.13_locator_prefab_navigation.png (100%) rename addon/io_scs_tools/ui/icons/{ => legacy}/.14_locator_prefab_map.png (100%) rename addon/io_scs_tools/ui/icons/{ => legacy}/.15_locator_prefab_trigger.png (100%) rename addon/io_scs_tools/ui/icons/{ => legacy}/.16_collider_box.png (100%) rename addon/io_scs_tools/ui/icons/{ => legacy}/.17_collider_sphere.png (100%) rename addon/io_scs_tools/ui/icons/{ => legacy}/.18_collider_capsule.png (100%) rename addon/io_scs_tools/ui/icons/{ => legacy}/.19_collider_cylinder.png (100%) rename addon/io_scs_tools/ui/icons/{ => legacy}/.20_collider_convex.png (100%) rename addon/io_scs_tools/ui/icons/{ => legacy}/.21_scs_root_object.png (100%) rename addon/io_scs_tools/ui/icons/{.icon_scs_bt_logo_orange.png => legacy/.22_scs_object_menu.png} (100%) rename addon/io_scs_tools/ui/icons/{ => legacy}/.icon_scs_bt_logo.png (100%) create mode 100644 addon/io_scs_tools/ui/icons/white/.01_mesh_model_object.png create mode 100644 addon/io_scs_tools/ui/icons/white/.02_object_with_shadow_caster_material.png create mode 100644 addon/io_scs_tools/ui/icons/white/.03_object_with_glass_material.png create mode 100644 addon/io_scs_tools/ui/icons/white/.04_object_with_physical_material.png create mode 100644 addon/io_scs_tools/ui/icons/white/.05_locator_all_types.png create mode 100644 addon/io_scs_tools/ui/icons/white/.06_locator_model.png create mode 100644 addon/io_scs_tools/ui/icons/white/.07_locator_prefab.png create mode 100644 addon/io_scs_tools/ui/icons/white/.08_locator_collision.png create mode 100644 addon/io_scs_tools/ui/icons/white/.09_locator_prefab_node.png create mode 100644 addon/io_scs_tools/ui/icons/white/.10_locator_prefab_sign.png create mode 100644 addon/io_scs_tools/ui/icons/white/.11_locator_prefab_spawn.png create mode 100644 addon/io_scs_tools/ui/icons/white/.12_locator_prefab_semaphore.png create mode 100644 addon/io_scs_tools/ui/icons/white/.13_locator_prefab_navigation.png create mode 100644 addon/io_scs_tools/ui/icons/white/.14_locator_prefab_map.png create mode 100644 addon/io_scs_tools/ui/icons/white/.15_locator_prefab_trigger.png create mode 100644 addon/io_scs_tools/ui/icons/white/.16_collider_box.png create mode 100644 addon/io_scs_tools/ui/icons/white/.17_collider_sphere.png create mode 100644 addon/io_scs_tools/ui/icons/white/.18_collider_capsule.png create mode 100644 addon/io_scs_tools/ui/icons/white/.19_collider_cylinder.png create mode 100644 addon/io_scs_tools/ui/icons/white/.20_collider_convex.png create mode 100644 addon/io_scs_tools/ui/icons/white/.21_scs_root_object.png create mode 100644 addon/io_scs_tools/ui/icons/white/.22_scs_object_menu.png create mode 100644 addon/io_scs_tools/ui/icons/white/.icon_scs_bt_logo.png create mode 100644 addon/io_scs_tools/ui/output.py delete mode 100644 addon/io_scs_tools/ui/scene.py create mode 100644 addon/io_scs_tools/ui/workspace.py create mode 100644 addon/io_scs_tools/utils/collection.py diff --git a/addon/io_scs_tools/__init__.py b/addon/io_scs_tools/__init__.py index fbf40a1..3b6421a 100644 --- a/addon/io_scs_tools/__init__.py +++ b/addon/io_scs_tools/__init__.py @@ -8,7 +8,7 @@ # 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. +# 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, @@ -16,14 +16,14 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software bl_info = { "name": "SCS Tools", "description": "Setup models, Import-Export SCS data format", "author": "Simon Lusenc (50keda), Milos Zajic (4museman)", - "version": (1, 12, "be00ed8"), - "blender": (2, 78, 0), + "version": (2, 0, "c95d7ac"), + "blender": (2, 81, 0), "location": "File > Import-Export", "wiki_url": "http://modding.scssoft.com/wiki/Documentation/Tools/SCS_Blender_Tools", "tracker_url": "http://forum.scssoft.com/viewforum.php?f=163", @@ -40,36 +40,33 @@ from io_scs_tools.internals.callbacks import open_gl as _open_gl_callback from io_scs_tools.internals.callbacks import persistent as _persistent_callback from io_scs_tools.internals import icons as _icons +from io_scs_tools.operators.bases.export import SCSExportHelper as _SCSExportHelper from io_scs_tools.utils import get_scs_globals as _get_scs_globals -from io_scs_tools.utils.view3d import switch_layers_visibility as _switch_layers_visibility from io_scs_tools.utils.view3d import has_view3d_space as _has_view3d_space from io_scs_tools.utils.printout import lprint -# importing all SCS Tools modules which creates panels in UI -from io_scs_tools import ui -from io_scs_tools import operators -class ImportSCS(bpy.types.Operator, ImportHelper): +class SCS_TOOLS_OT_Import(bpy.types.Operator, ImportHelper): """ Load various SCS file formats containing geometries and numerous settings. """ - bl_idname = "import_mesh.pim" + bl_idname = "scs_tools.import_pim" bl_label = "SCS Import" bl_description = "Load various SCS file formats containing geometries and numerous settings." bl_options = {'UNDO'} - files = CollectionProperty(name="File Path", - description="File path used for importing the SCS files", - type=bpy.types.OperatorFileListElement) + filename_ext = "*.pim;*.pim.ef;" + files: CollectionProperty(name="File Path", + description="File path used for importing the SCS files", + type=bpy.types.OperatorFileListElement) - scs_project_path_mode = BoolProperty( + scs_project_path_mode: BoolProperty( default=False, description="Set currently selected directory as SCS Project Path" ) - directory = StringProperty() - filename_ext = "*.pim;*.pim.ef;" - filter_glob = StringProperty(default=filename_ext, options={'HIDDEN'}) + directory: StringProperty() + filter_glob: StringProperty(default=filename_ext, options={'HIDDEN'}) def check(self, context): @@ -81,17 +78,15 @@ def check(self, context): def execute(self, context): + from io_scs_tools.internals.containers.config import AsyncPathsInit + # if paths are still initializing report that to user and don't execute import - if operators.world.SCSPathsInitialization.is_running(): + if AsyncPathsInit.is_running(): self.report({'INFO'}, "Can't import yet, paths initialization is still in progress! Try again in few moments.") - # there is no way to keep current operator alive if we want to abort import sequence. - # That's why we call another import operator, which will end up with - # printing out above info and taking us back to import screen with file browser. - bpy.ops.import_mesh.pim('INVOKE_DEFAULT') - - return {'FINISHED'} + # revoke to add new fileselect dialog otherwise this operator will finish + return self.invoke(context, None) if not _has_view3d_space(context.screen): message = "Cannot import SCS Models, no 3D viewport found! Make sure you have at least one 3D view visible." @@ -150,6 +145,10 @@ def draw(self, context): :return: """ + from io_scs_tools.ui.shared import get_on_off_icon + from io_scs_tools.ui.shared import draw_common_settings + from io_scs_tools.internals.containers.config import AsyncPathsInit + scs_globals = _get_scs_globals() # importer_version = round(import_pix.version(), 2) @@ -158,181 +157,193 @@ def draw(self, context): # SCS Project Path box1 = layout.box() layout_box_col = box1.column(align=True) - layout_box_col.label('SCS Project Base Path:', icon='FILE_FOLDER') + layout_box_col.label(text="SCS Project Base Path:", icon='FILE_FOLDER') + layout_box_col.separator() layout_box_row = layout_box_col.row(align=True) layout_box_row.alert = not os.path.isdir(scs_globals.scs_project_path) - layout_box_row.prop(scs_globals, 'scs_project_path', text='') + layout_box_row.prop(scs_globals, 'scs_project_path', text="") layout_box_row = layout_box_col.row(align=True) - layout_box_row.prop(self, "scs_project_path_mode", toggle=True, text="Set Current Dir as Project Base", icon='SCREEN_BACK') + layout_box_row.prop(self, "scs_project_path_mode", toggle=True, text="Set Current Dir as Project Base", icon='PASTEDOWN') - if operators.world.SCSPathsInitialization.is_running(): # report running path initialization operator + if AsyncPathsInit.is_running(): # report running path initialization operator layout_box_row = layout_box_col.row(align=True) - layout_box_row.label("Paths initialization in progress...") - layout_box_row.label("", icon='TIME') + layout_box_row.scale_y = 2 + layout_box_row.label(text="Paths initialization in progress...") + layout_box_row.label(text="", icon='SORTTIME') # import settings box2 = layout.box() - col = box2.column(align=True) + col = box2.column() - col.row().prop(scs_globals, "import_scale") - col.row().separator() - col.row().prop(scs_globals, "import_preserve_path_for_export") - col.row().separator() - col.row().prop(scs_globals, "import_pim_file", toggle=True, icon="FILE_TICK" if scs_globals.import_pim_file else "X") + col.label(text="Import Options:", icon='SETTINGS') + col.separator() + col.prop(scs_globals, "import_scale") + col.prop(scs_globals, "import_preserve_path_for_export") + col.prop(scs_globals, "import_pim_file", toggle=True, icon=get_on_off_icon(scs_globals.import_pim_file)) if scs_globals.import_pim_file: - col.row().prop(scs_globals, "import_use_normals") - col.row().prop(scs_globals, "import_use_welding") + col.prop(scs_globals, "import_use_normals") + col.prop(scs_globals, "import_use_welding") if scs_globals.import_use_welding: - col.row().prop(scs_globals, "import_welding_precision") - col.row().separator() - col.row().prop(scs_globals, "import_pit_file", toggle=True, icon="FILE_TICK" if scs_globals.import_pit_file else "X") + col.prop(scs_globals, "import_welding_precision") + col.prop(scs_globals, "import_pit_file", toggle=True, icon=get_on_off_icon(scs_globals.import_pit_file)) if scs_globals.import_pit_file: - col.row().prop(scs_globals, "import_load_textures") - col.row().separator() - col.row().prop(scs_globals, "import_pic_file", toggle=True, icon="FILE_TICK" if scs_globals.import_pic_file else "X") - col.row().separator() - col.row().prop(scs_globals, "import_pip_file", toggle=True, icon="FILE_TICK" if scs_globals.import_pip_file else "X") - col.row().separator() - col.row(align=True).prop(scs_globals, "import_pis_file", toggle=True, icon="FILE_TICK" if scs_globals.import_pis_file else "X") + col.prop(scs_globals, "import_load_textures") + col.prop(scs_globals, "import_pic_file", toggle=True, icon=get_on_off_icon(scs_globals.import_pic_file)) + col.prop(scs_globals, "import_pip_file", toggle=True, icon=get_on_off_icon(scs_globals.import_pip_file)) + col.prop(scs_globals, "import_pis_file", toggle=True, icon=get_on_off_icon(scs_globals.import_pis_file)) if scs_globals.import_pis_file: - col.row(align=True).prop(scs_globals, "import_bone_scale") - col.row().separator() - col.row().prop(scs_globals, "import_pia_file", toggle=True, icon="FILE_TICK" if scs_globals.import_pia_file else "X") + col.prop(scs_globals, "import_bone_scale") + col.prop(scs_globals, "import_pia_file", toggle=True, icon=get_on_off_icon(scs_globals.import_pia_file)) if scs_globals.import_pia_file: - col.row().prop(scs_globals, "import_include_subdirs_for_pia") + col.prop(scs_globals, "import_include_subdirs_for_pia") # Common global settings - ui.shared.draw_common_settings(layout, log_level_only=True) + box3 = layout.box() + box3.label(text="Log Level:", icon='MOD_EXPLODE') + box3.prop(scs_globals, 'dump_level', text="") -class ExportSCS(bpy.types.Operator, ExportHelper): +class SCS_TOOLS_OT_Export(bpy.types.Operator, _SCSExportHelper, ExportHelper): """ Export complex geometries to the SCS file formats. """ - bl_idname = "export_mesh.pim" + bl_idname = "scs_tools.export_pim" bl_label = "SCS Export" bl_description = "Export complex geometries to the SCS file formats." bl_options = set() filename_ext = ".pim" - filter_glob = StringProperty(default=str("*" + filename_ext), options={'HIDDEN'}) + filter_glob: StringProperty(default=str("*" + filename_ext), options={'HIDDEN'}) + + def execute(self, context): + return self.execute_export(context, True) - layers_visibilities = [] - """List for storing layers visibility in the init of operator""" + def draw(self, context): + from io_scs_tools.ui.shared import draw_export_panel - def __init__(self): - """Constructor used for showing all scene visibility layers. - This provides possibility to updates world matrixes even on hidden objects. - """ - self.layers_visibilities = _switch_layers_visibility([], True) + box0 = self.layout.box() + box0.use_property_split = True + box0.use_property_decorate = False - def __del__(self): - """Destructor with reverting visible layers. - """ - _switch_layers_visibility(self.layers_visibilities, False) + box0.label(text="Export Options:", icon='SETTINGS') + box0.prop(_get_scs_globals(), 'export_scope', expand=True) + + draw_export_panel(box0, ignore_extra_boxes=True) - def execute(self, context): - lprint('D Export From Menu...') - - from io_scs_tools import exp as _export - from io_scs_tools.utils import object as _object_utils - - filepath = os.path.dirname(self.filepath) - - # convert it to None, so export will ignore given menu file path and try to export to other none menu set paths - if self.filepath == "": - filepath = None - - export_scope = _get_scs_globals().export_scope - init_obj_list = {} - if export_scope == "selection": - for obj in bpy.context.selected_objects: - root = _object_utils.get_scs_root(obj) - if root: - if root != obj: # add only selected children - init_obj_list[obj.name] = obj - init_obj_list[root.name] = root - else: # add every children if all are unselected - children = _object_utils.get_children(obj) - local_reselected_objs = [] - for child_obj in children: - local_reselected_objs.append(child_obj) - # if some child is selected this means we won't reselect nothing in this game object - if child_obj.select: - local_reselected_objs = [] - break - - for reselected_obj in local_reselected_objs: - init_obj_list[reselected_obj.name] = reselected_obj - - init_obj_list = tuple(init_obj_list.values()) - elif export_scope == "scene": - init_obj_list = tuple(bpy.context.scene.objects) - elif export_scope == 'scenes': - init_obj_list = tuple(bpy.data.objects) - - # check extension for EF format and properly assign it to name suffix - ef_name_suffix = "" - if _get_scs_globals().export_output_type == "EF": - ef_name_suffix = ".ef" - - try: - result = _export.batch_export(self, init_obj_list, name_suffix=ef_name_suffix, menu_filepath=filepath) - except Exception as e: - - result = {"CANCELLED"} - context.window.cursor_modal_restore() - - trace_str = traceback.format_exc().replace("\n", "\n\t ") - lprint("E Unexpected %r accured during batch export:\n\t %s", - (type(e).__name__, trace_str), - report_errors=1, - report_warnings=1) - - return result + +class SCS_TOOLS_MT_AddObject(bpy.types.Menu): + bl_label = "Add" + bl_description = "Creates menu for adding SCS objects" def draw(self, context): - box0 = self.layout.box() - row = box0.row() - row.prop(_get_scs_globals(), 'export_scope', expand=True) - ui.shared.draw_export_panel(self.layout) + self.layout.operator_enum("object.scs_tools_add_object", "new_object_type") -class SCSAddObject(bpy.types.Menu): - bl_idname = "INFO_MT_SCS_add_object" - bl_label = "SCS Object" - bl_description = "Creates menu for adding SCS objects." +class SCS_TOOLS_MT_ObjectsMisc(bpy.types.Menu): + bl_label = "Objects Misc" + bl_description = "Creates menu for SCS objects miscellaneous actions" def draw(self, context): - self.layout.operator_enum("object.scs_add_object", "new_object_type") + self.layout.operator("object.scs_tools_fix_model_locator_hookups") + + +class SCS_TOOLS_MT_MaterialsMisc(bpy.types.Menu): + bl_label = "Materials Misc" + bl_description = "Creates menu for SCS materials miscellaneous actions" + + def draw(self, context): + self.layout.operator("material.scs_tools_reload_materials") + self.layout.operator("material.scs_tools_merge_materials") + self.layout.operator("material.scs_tools_adapt_color_management") + + +class SCS_TOOLS_MT_MainMenu(bpy.types.Menu): + bl_label = "SCS Tools" + bl_description = "Global menu for accessing all SCS Blender Tools features in one place" + + __static_popovers = { + "sidebar": [], + "props": [], + "output": [] + } + + @staticmethod + def append_sidebar_entry(menu_item_name, panel_id): + SCS_TOOLS_MT_MainMenu.__static_popovers["sidebar"].append((menu_item_name, panel_id)) + + @staticmethod + def append_props_entry(menu_item_name, panel_id): + SCS_TOOLS_MT_MainMenu.__static_popovers["props"].append((menu_item_name, panel_id)) + + @staticmethod + def append_output_entry(menu_item_name, panel_id): + SCS_TOOLS_MT_MainMenu.__static_popovers["output"].append((menu_item_name, panel_id)) + + @classmethod + def unregister(cls): + for category in SCS_TOOLS_MT_MainMenu.__static_popovers: + SCS_TOOLS_MT_MainMenu.__static_popovers[category].clear() + + def draw(self, context): + layout = self.layout + + column = layout.column() + + # sub-menus + column.menu(SCS_TOOLS_MT_AddObject.__name__) + column.menu(SCS_TOOLS_MT_ObjectsMisc.__name__) + column.menu(SCS_TOOLS_MT_MaterialsMisc.__name__) + + # popovers by category + for category in SCS_TOOLS_MT_MainMenu.__static_popovers: + column.separator() + for menu_item_name, panel_id in SCS_TOOLS_MT_MainMenu.__static_popovers[category]: + column.popover(panel_id, text=menu_item_name) def add_menu_func(self, context): - self.layout.menu("INFO_MT_SCS_add_object", text="SCS Object", icon_value=_icons.get_icon(_ICONS_consts.Types.scs_logo_orange)) + self.layout.menu(SCS_TOOLS_MT_AddObject.__name__, text="SCS Object", icon_value=_icons.get_icon(_ICONS_consts.Types.scs_object_menu)) self.layout.separator() def menu_func_import(self, context): - self.layout.operator(ImportSCS.bl_idname, text="SCS Formats (.pim)") + self.layout.operator(SCS_TOOLS_OT_Import.bl_idname, text="SCS Game Object (.pim)", + icon_value=_icons.get_icon(_ICONS_consts.Types.scs_object_menu)) def menu_func_export(self, context): - self.layout.operator(ExportSCS.bl_idname, text="SCS Formats (.pim)") + self.layout.operator(SCS_TOOLS_OT_Export.bl_idname, text="SCS Game Object(s) (.pim)", + icon_value=_icons.get_icon(_ICONS_consts.Types.scs_object_menu)) -# ################################################# +def menu_scs_tools(self, context): + self.layout.menu(SCS_TOOLS_MT_MainMenu.__name__) -def register(): - from . import properties - bpy.utils.register_module(__name__) +classes = ( + SCS_TOOLS_OT_Import, + SCS_TOOLS_OT_Export, + SCS_TOOLS_MT_AddObject, + SCS_TOOLS_MT_ObjectsMisc, + SCS_TOOLS_MT_MaterialsMisc, + SCS_TOOLS_MT_MainMenu +) + + +# ################################################# + +def register(): # CUSTOM ICONS INITIALIZATION - _icons.init() + _icons.register() + + # REGISTRATION OF OUR PROPERTIES + from io_scs_tools.properties import register as props_register + props_register() - # PROPERTIES REGISTRATION + # PROPERTIES REGISTRATION INTO EXISTING CLASSES bpy.types.Object.scs_object_look_inventory = CollectionProperty( type=properties.object.ObjectLooksInventoryItem ) @@ -349,10 +360,10 @@ def register(): type=properties.object.ObjectAnimationInventoryItem ) - bpy.types.World.scs_globals = PointerProperty( - name="SCS Tools Global Variables", - type=properties.world.GlobalSCSProps, - description="SCS Tools global variables", + bpy.types.WorkSpace.scs_props = PointerProperty( + name="SCS Tools Workspace Variables", + type=properties.workspace.WorkspaceSCSProps, + description="SCS Tools workspace variables" ) bpy.types.Object.scs_props = PointerProperty( @@ -385,26 +396,31 @@ def register(): description="SCS Tools Action variables", ) - # REGISTER DYNAMIC PROPERTIES - properties.object_dynamic.register() - properties.scene_dynamic.register() + # REGISTER UI + from io_scs_tools.ui import register as ui_register + ui_register() - # PERSISTENT HANDLERS - _persistent_callback.enable() + # REGISTER OPERATORS + from io_scs_tools.operators import register as ops_register + ops_register() + + # MAIN MODULE REGISTRATION + for cls in classes: + bpy.utils.register_class(cls) # MENU REGISTRATION - bpy.types.INFO_MT_file_import.append(menu_func_import) - bpy.types.INFO_MT_file_export.append(menu_func_export) - bpy.types.INFO_MT_add.prepend(add_menu_func) + bpy.types.TOPBAR_MT_editor_menus.append(menu_scs_tools) + bpy.types.TOPBAR_MT_file_import.append(menu_func_import) + bpy.types.TOPBAR_MT_file_export.append(menu_func_export) + bpy.types.VIEW3D_MT_add.prepend(add_menu_func) + # PERSISTENT HANDLERS + _persistent_callback.enable() -def unregister(): - bpy.utils.unregister_module(__name__) - # REMOVE MENU ENTRIES - bpy.types.INFO_MT_file_export.remove(menu_func_export) - bpy.types.INFO_MT_file_import.remove(menu_func_import) - bpy.types.INFO_MT_add.remove(add_menu_func) +def unregister(): + # DELETE CUSTOM ICONS + _icons.unregister() # REMOVE OPENGL HANDLERS _open_gl_callback.disable() @@ -412,19 +428,41 @@ def unregister(): # REMOVE PERSISTENT HANDLERS _persistent_callback.disable() + # REMOVE MENU ENTRIES + bpy.types.TOPBAR_MT_editor_menus.remove(menu_scs_tools) + bpy.types.TOPBAR_MT_file_import.remove(menu_func_export) + bpy.types.TOPBAR_MT_file_export.remove(menu_func_import) + bpy.types.VIEW3D_MT_add.remove(add_menu_func) + + # REMOVE MAIN MODULE CLASSES + for cls in classes: + bpy.utils.unregister_class(cls) + + # UNREGISTER OPERATORS + from io_scs_tools.operators import unregister as ops_unregister + ops_unregister() + + # UNREGISTER UI + from io_scs_tools.ui import unregister as ui_unregister + ui_unregister() + # REMOVE PROPERTIES FROM DATA del bpy.types.Action.scs_props del bpy.types.Material.scs_props del bpy.types.Mesh.scs_props del bpy.types.Scene.scs_props del bpy.types.Object.scs_props - del bpy.types.World.scs_globals + del bpy.types.WorkSpace.scs_props del bpy.types.Object.scs_object_look_inventory del bpy.types.Object.scs_object_part_inventory del bpy.types.Object.scs_object_variant_inventory del bpy.types.Object.scs_object_animation_inventory + # UNREGISTER PROPS + from io_scs_tools.properties import unregister as props_unregister + props_unregister() + if __name__ == "__main__": register() diff --git a/addon/io_scs_tools/consts.py b/addon/io_scs_tools/consts.py index a296af4..ad1a400 100644 --- a/addon/io_scs_tools/consts.py +++ b/addon/io_scs_tools/consts.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2017: SCS Software +# Copyright (C) 2013-2019: SCS Software """ Constants for data group of map and navigation curves @@ -31,8 +31,8 @@ class ConnectionsStorage: """Constants related for storage of connections used in custom drawing """ - group_name = ".scs_connection_storage" - """Name of bpy.data.group which will be used for storing Custom Property for connections dictionary""" + collection_name = ".scs_connection_storage" + """Name of bpy.data.collection which will be used for storing Custom Property for connections dictionary""" custom_prop_name = "scs_locator_connections" """Name of the Blender Custom Property where dictionary for connections will be stored""" @@ -67,51 +67,35 @@ class TerrainPoints: class View3DReport: """Constants related to 3D view report operator. """ - # constants defining BT logo image and texts/positions of close/hide controls - BT_LOGO_IMG_NAME = ".scs_bt_logo.png" - BT_LOGO_AREA = (20, 217, 30, 50) - CLOSE_BTN_AREA = (230, 370, 26, 54) - CLOSE_BTN_TEXT = ( - "[Click] Close", # used when report text is shown - "[Click/ESC] Close" # used when report text is hidden (aka condensed mode) - ) - CLOSE_BTN_TEXT_POS = ( - (260, 45), # used when report text is shown - (240, 45) # used when report text is hidden (aka condensed mode) - ) - HIDE_BTN_AREA = (385, 530, 26, 54) - HIDE_BTN_TEXT = ( - "[Click/ESC] Hide", # used when report text is shown - "[Click] Show" # used when report text is hidden (aka condensed mode) - ) - HIDE_BTN_TEXT_POS = ( - (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) - ) + # constants defining BT banner image and texts/positions of close/hide controls + BT_BANNER_IMG_NAME = ".scs_bt_banner.png" + BT_BANNER_WITH_CTRLS_IMG_NAME = ".scs_bt_banner_with_ctrls.png" + CLOSE_BTN_AREA = (270, 290, -25, -5) + CLOSE_BTN_TEXT_POS = (272, -9) + CLOSE_BTN_TEXT = "×" + HIDE_BTN_AREA = (245, 265, -25, -5) + HIDE_BTN_TEXT_POS = (250, -5) + HIDE_BTN_TEXT = "–" + SCROLLUP_BTN_AREA = (225, 240, -25, -5) + SCROLLUP_BTN_TEXT_POS = (226, -9) + SCROLLUP_BTN_TEXT = "↑" + SCROLLDOWN_BTN_AREA = (205, 220, -25, -5) + SCROLLDOWN_BTN_TEXT_POS = (206, -9) + SCROLLDOWN_BTN_TEXT = "↓" + + class InventoryMoveType: + """Constants related to moving type in operators for inventories items moving. + """ + move_down = "down" + move_up = "up" class Icons: """Constants related to loading of custom icons. """ + default_icon_theme = "white" + class Types: """This class saves names of all custom icons for Blender Tools. """ @@ -136,8 +120,8 @@ class Types: loc_collider_cylinder = ".19_collider_cylinder.png" loc_collider_convex = ".20_collider_convex.png" scs_root = ".21_scs_root_object.png" + scs_object_menu = ".22_scs_object_menu.png" scs_logo = ".icon_scs_bt_logo.png" - scs_logo_orange = ".icon_scs_bt_logo_orange.png" @staticmethod def as_list(): @@ -151,7 +135,7 @@ def as_list(): Icons.Types.loc_prefab_navigation, Icons.Types.loc_prefab_map, Icons.Types.loc_prefab_trigger, Icons.Types.loc_collider_box, Icons.Types.loc_collider_sphere, Icons.Types.loc_collider_capsule, Icons.Types.loc_collider_cylinder, Icons.Types.loc_collider_convex, Icons.Types.scs_root, - Icons.Types.scs_logo_orange, Icons.Types.scs_logo] + Icons.Types.scs_object_menu, Icons.Types.scs_logo] class Part: @@ -186,6 +170,8 @@ class Material: """Unset value of material substance (used for identifying if this value in material was set)""" node_group_prefix = ".SCS_NG_" """Prefix for naming node groups used by SCS materials""" + prevm_material_name = ".scs_prevm" + """Name of the material used on SCS preview models.""" class Colors: @@ -193,8 +179,8 @@ class Colors: """ gamma = 2.2 """Gamma value used by Blender for correcting display colors.""" - saturation = 1.15 - """Amount of saturation for vertex colors in nodes.""" + prevm_color = (0.36, 0.29, 0.57, 1) + """Color array used for preview models.""" class LampTools: @@ -474,21 +460,19 @@ class ConvHlpr: class SCSLigthing: - """Constants for scs lighting scene. Lighting scene is created from sun profile loaded from SII file. + """Constants for scs lighting. """ scene_name = ".scs_lighting" """Name of lighting scene. It should be prefixed with dot to be partially hidden in scene selection theme.""" - ambient_lamps = ( - (".scs_ambient_z+", (pi, 0, 0), 0.5), - (".scs_ambient_z-", (0, 0, 0), 1.1) - ) - """Ambient lamps definitions. Each lamp is defined as (name, direction, energy_factor). - There has to be 6 hemi lamps to point in each direction, which should reassemble ambient lights. - Energy factor in each of lamps tells percent of given light by that ambient lamp.""" + sun_lamp_name = ".scs_sun" + """Name of scs sun object in the lighting scene.""" - diffuse_lamp_name = ".scs_diffuse" - specular_lamp_name = ".scs_specular" + default_ambient = (0.3,) * 3 + default_diffuse = (0.9,) * 3 + default_specular = (0.5,) * 3 + default_env = 1.0 + """Default lighting values used when scs lighting is disabled, gotten from effect/eut/defaults.sii""" class PaintjobTools: @@ -587,3 +571,10 @@ class VehicleTypes: (255, 64, 166) ) """Array of unique colors for building ID mask texture.""" + + +class Cache: + dir_name = "blender_scs_blender_tools" + """Name of the directory inside tmp directory, that will be used for cache storage.""" + max_size = 40 * 1024 * 1024 # 40MB + """Maximum size of tmp directory cache.""" diff --git a/addon/io_scs_tools/exp/__init__.py b/addon/io_scs_tools/exp/__init__.py index f445c9e..222b49d 100644 --- a/addon/io_scs_tools/exp/__init__.py +++ b/addon/io_scs_tools/exp/__init__.py @@ -74,13 +74,6 @@ def batch_export(operator_instance, init_obj_list, name_suffix="", menu_filepath scs_game_objects_rejected.append("> \"" + root_object.name + "\"") continue - # update root object location to invoke update tagging on it and - # then update scene to make sure all children objects will have all transforms up to date - # NOTE: needed because Blender doesn't update objects on invisible layers on it's own - root_object.location = root_object.location - for scene in bpy.data.scenes: - scene.update() - # GET CUSTOM FILE PATH custom_filepath = _path_utils.get_custom_scs_root_export_path(root_object) @@ -122,7 +115,7 @@ def batch_export(operator_instance, init_obj_list, name_suffix="", menu_filepath if not lprint("\nI Export procces completed, summaries are printed below!", report_errors=True, report_warnings=True): operator_instance.report({'INFO'}, "Export successfully completed, exported %s game object(s)!" % len(scs_game_objects_exported)) - bpy.ops.wm.show_3dview_report('INVOKE_DEFAULT', abort=True) # abort 3d view reporting operator + bpy.ops.wm.scs_tools_show_3dview_report('INVOKE_DEFAULT', abort=True) # abort 3d view reporting operator if len(scs_game_objects_exported) > 0: message = "EXPORTED GAME OBJECTS (" + str(len(scs_game_objects_exported)) + "):\n\t " + "=" * 26 + "\n\t " diff --git a/addon/io_scs_tools/exp/pia.py b/addon/io_scs_tools/exp/pia.py index a4a1cbf..9d350ef 100644 --- a/addon/io_scs_tools/exp/pia.py +++ b/addon/io_scs_tools/exp/pia.py @@ -97,7 +97,7 @@ def _get_bone_channels(scs_root_obj, armature, scs_animation, action, export_sca # armature matrix stores transformation of armature object against scs root # and has to be added to all bones as they only armature space transformations - armature_mat = scs_root_obj.matrix_world.inverted() * armature.matrix_world + armature_mat = scs_root_obj.matrix_world.inverted() @ armature.matrix_world invalid_data = False # flag to indicate invalid data state curves_per_bone = OrderedDict() # store all the curves we are interested in per bone names @@ -143,10 +143,12 @@ def _get_bone_channels(scs_root_obj, armature, scs_animation, action, export_sca quat_rot_curves = bone_curves["quat_rotation"] sca_curves = bone_curves["scale"] - bone_rest_mat = armature_mat * bone.matrix_local + bone_rest_mat = armature_mat @ bone.matrix_local if bone.parent: - parent_bone_rest_mat = (Matrix.Scale(export_scale, 4) * _convert_utils.scs_to_blend_matrix().inverted() * - armature_mat * bone.parent.matrix_local) + parent_bone_rest_mat = (Matrix.Scale(export_scale, 4) @ + _convert_utils.scs_to_blend_matrix().inverted() @ + armature_mat @ + bone.parent.matrix_local) else: parent_bone_rest_mat = Matrix() @@ -202,7 +204,7 @@ def _get_bone_channels(scs_root_obj, armature, scs_animation, action, export_sca mat_sca[3] = (0, 0, 0, 1) # BLENDER FRAME MATRIX - mat = mat_loc * mat_rot * mat_sca + mat = mat_loc @ mat_rot @ mat_sca # SCALE REMOVAL MATRIX rest_location, rest_rotation, rest_scale = bone_rest_mat.decompose() @@ -218,8 +220,12 @@ def _get_bone_channels(scs_root_obj, armature, scs_animation, action, export_sca scale_matrix = Matrix.Scale(export_scale, 4) # COMPUTE SCS FRAME MATRIX - frame_matrix = (parent_bone_rest_mat.inverted() * _convert_utils.scs_to_blend_matrix().inverted() * - scale_matrix.inverted() * bone_rest_mat * mat * scale_removal_matrix.inverted()) + frame_matrix = (parent_bone_rest_mat.inverted() @ + _convert_utils.scs_to_blend_matrix().inverted() @ + scale_matrix.inverted() @ + bone_rest_mat @ + mat @ + scale_removal_matrix.inverted()) # print(' actual_frame: %s - value: %s' % (actual_frame, frame_matrix)) timings_stream.append(("__time__", scs_animation.length / total_frames), ) diff --git a/addon/io_scs_tools/exp/pic.py b/addon/io_scs_tools/exp/pic.py index c0a912d..3fe81f5 100644 --- a/addon/io_scs_tools/exp/pic.py +++ b/addon/io_scs_tools/exp/pic.py @@ -97,7 +97,7 @@ def _fill_piece_sections(convex_coll_locators, export_scale): for vert in verts: # scs_position = Matrix.Scale(scs_globals.export_scale, 4) * io_utils.scs_to_blend_matrix().inverted() * mat_world * position ## # POSITION - scs_position = Matrix.Scale(export_scale, 4) * _convert_utils.scs_to_blend_matrix().inverted() * Vector(vert) # POSITION + scs_position = Matrix.Scale(export_scale, 4) @ _convert_utils.scs_to_blend_matrix().inverted() @ Vector(vert) # POSITION vector_verts.append(Vector(scs_position)) section.sections.append(_pix_container.make_stream_section(vector_verts, "_POSITION", ())) @@ -166,25 +166,25 @@ def _make_common_part(item, index, col_type): if not item.scs_props.locator_collider_centered: if item.scs_props.locator_collider_type == 'Box': - offset_matrix = (item.matrix_world * - Matrix.Translation((0.0, -item.scs_props.locator_collider_box_y / 2, 0.0)) * - (Matrix.Scale(item.scs_props.locator_collider_box_x, 4, (1.0, 0.0, 0.0)) * - Matrix.Scale(item.scs_props.locator_collider_box_y, 4, (0.0, 1.0, 0.0)) * + offset_matrix = (item.matrix_world @ + Matrix.Translation((0.0, -item.scs_props.locator_collider_box_y / 2, 0.0)) @ + (Matrix.Scale(item.scs_props.locator_collider_box_x, 4, (1.0, 0.0, 0.0)) @ + Matrix.Scale(item.scs_props.locator_collider_box_y, 4, (0.0, 1.0, 0.0)) @ Matrix.Scale(item.scs_props.locator_collider_box_z, 4, (0.0, 0.0, 1.0)))) elif item.scs_props.locator_collider_type == 'Sphere': - offset_matrix = (item.matrix_world * - Matrix.Translation((0.0, -item.scs_props.locator_collider_dia / 2, 0.0)) * + offset_matrix = (item.matrix_world @ + Matrix.Translation((0.0, -item.scs_props.locator_collider_dia / 2, 0.0)) @ Matrix.Scale(item.scs_props.locator_collider_dia, 4)) elif item.scs_props.locator_collider_type in ('Capsule', 'Cylinder'): - offset_matrix = (item.matrix_world * - Matrix.Translation((0.0, -item.scs_props.locator_collider_len / 2, 0.0)) * + offset_matrix = (item.matrix_world @ + Matrix.Translation((0.0, -item.scs_props.locator_collider_len / 2, 0.0)) @ Matrix.Scale(item.scs_props.locator_collider_dia, 4)) else: offset_matrix = item.matrix_world - loc, qua, sca = _convert_utils.get_scs_transformation_components(scs_root.matrix_world.inverted() * offset_matrix) + loc, qua, sca = _convert_utils.get_scs_transformation_components(scs_root.matrix_world.inverted() @ offset_matrix) else: - loc, qua, sca = _convert_utils.get_scs_transformation_components(scs_root.matrix_world.inverted() * item.matrix_world) + loc, qua, sca = _convert_utils.get_scs_transformation_components(scs_root.matrix_world.inverted() @ item.matrix_world) section = _SectionData("Locator") section.props.append(("Name", _name_utils.tokenize_name(item.name))) diff --git a/addon/io_scs_tools/exp/pim/exporter.py b/addon/io_scs_tools/exp/pim/exporter.py index 29fa53b..9e61d31 100644 --- a/addon/io_scs_tools/exp/pim/exporter.py +++ b/addon/io_scs_tools/exp/pim/exporter.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import os import collections @@ -32,8 +32,8 @@ from io_scs_tools.exp.pim.part import Part from io_scs_tools.exp.pim.locator import Locator from io_scs_tools.exp.pim.bones import Bones -from io_scs_tools.exp.pim.skin import Skin -from io_scs_tools.exp.pim.skin import SkinStream +from io_scs_tools.exp.pim.piece_skin import PieceSkin +from io_scs_tools.exp.pim.piece_skin import PieceSkinStream from io_scs_tools.internals.containers import pix as _pix_container from io_scs_tools.utils import mesh as _mesh_utils from io_scs_tools.utils import name as _name_utils @@ -97,12 +97,14 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat """:type: dict[str, Part]""" pim_locators = [] # list of Locator class instances representing model locators """:type: list[Locator]""" + pim_piece_skins = collections.OrderedDict() + """:type: dict[str, PieceSkin""" objects_with_default_material = {} # stores object names which has no material set missing_mappings_data = {} # indicates if material doesn't have set any uv layer for export invalid_objects_for_tangents = set() # stores object names which tangents calculation failed because of N-gons existence - bones = skin = skin_stream = None + bones = None if is_skin_used: invalid_bone_names = set() # set for saving bones with invalid names, they are used for reporting to user @@ -117,10 +119,6 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat if _name_utils.tokenize_name(bone.name) != bone.name: invalid_bone_names.add(bone.name) - # create skin data section - skin_stream = SkinStream(SkinStream.Types.POSITION) - skin = Skin(skin_stream) - # report invalid bone names if len(invalid_bone_names) > 0: lprint("W Invalid bone names detected, max. length of valid bone name is 12 and must consists from [a-z, 0-9 and _ ] characters.\n\t " @@ -159,12 +157,11 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat face_flip = scale_sign < 0 # calculate transformation matrix for current object (root object transforms are always subtracted!) - mesh_transf_mat = root_object.matrix_world.inverted() * mesh_obj.matrix_world + mesh_transf_mat = root_object.matrix_world.inverted() @ mesh_obj.matrix_world """:type: mathutils.Matrix""" # calculate vertex position transformation matrix for this object - pos_transf_mat = (Matrix.Scale(scs_globals.export_scale, 4) * - _scs_to_blend_matrix().inverted()) + pos_transf_mat = Matrix.Scale(scs_globals.export_scale, 4) @ _scs_to_blend_matrix().inverted() """:type: mathutils.Matrix""" # calculate vertex normals transformation matrix for this object @@ -173,9 +170,7 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat scale_matrix_x = Matrix.Scale(scale.x, 3, Vector((1, 0, 0))).to_4x4() scale_matrix_y = Matrix.Scale(scale.y, 3, Vector((0, 1, 0))).to_4x4() scale_matrix_z = Matrix.Scale(scale.z, 3, Vector((0, 0, 1))).to_4x4() - nor_transf_mat = (_scs_to_blend_matrix().inverted() * - rot.to_matrix().to_4x4() * - scale_matrix_x * scale_matrix_y * scale_matrix_z) + nor_transf_mat = _scs_to_blend_matrix().inverted() @ rot.to_matrix().to_4x4() @ scale_matrix_x @ scale_matrix_y @ scale_matrix_z """:type: mathutils.Matrix""" tangent_transf_mat = _scs_to_blend_matrix().inverted() @@ -183,11 +178,10 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat # get initial mesh & extra copy of the mesh for normals mesh = _object_utils.get_mesh(mesh_obj) - mesh_for_normals = mesh.copy() + mesh_for_normals = _mesh_utils.get_mesh_for_normals(mesh) # prepare meshes faces_mapping = _mesh_utils.bm_prepare_mesh_for_export(mesh, mesh_transf_mat, triangulate=True) - mesh_for_normals.calc_normals_split() missing_uv_layers = {} # stores missing uvs specified by materials of this object missing_vcolor = False # indicates if object is missing vertex color layer @@ -262,6 +256,18 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat mesh_piece = mesh_pieces[piece_key] """:type: Piece""" + # create/get skin data section for current piece + if is_skin_used: + mesh_piece_idx = mesh_piece.get_index() + + if mesh_piece_idx not in pim_piece_skins: + new_skin_stream = PieceSkinStream(PieceSkinStream.Types.POSITION) + pim_piece_skins[mesh_piece_idx] = PieceSkin(mesh_piece_idx, new_skin_stream) + + skin_stream = pim_piece_skins[mesh_piece_idx].get_skin_stream_by_type(PieceSkinStream.Types.POSITION) + else: + skin_stream = None + # get polygon loop indices for normals depending on mapped triangulated face if poly.index in faces_mapping: normals_poly_loop_indices = list(mesh_for_normals.polygons[faces_mapping[poly.index]].loop_indices) @@ -278,7 +284,7 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat # get data of current vertex # 1. position -> mesh.vertices[loop.vertex_index].co - position = tuple(pos_transf_mat * mesh.vertices[vert_i].co) + position = tuple(pos_transf_mat @ mesh.vertices[vert_i].co) # 2. normal -> mesh_for_normals.loops[loop_i].normal -> calc_normals_split() has to be called before normal = (0, 0, 0) @@ -287,7 +293,7 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat # match by vertex index as triangle will for sure have three unique vertices if vert_i == normal_loop.vertex_index: - normal = nor_transf_mat * normal_loop.normal + normal = nor_transf_mat @ normal_loop.normal normal = tuple(Vector(normal).normalized()) del normals_poly_loop_indices[i] break @@ -350,7 +356,7 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat # 5. tangent -> loop.tangent; loop.bitangent_sign -> calc_tangents() has to be called before if pim_materials[pim_mat_name].get_nmap_uv_name(): # calculate tangents only if needed - tangent = tuple(tangent_transf_mat * loop.tangent) + tangent = tuple(tangent_transf_mat @ loop.tangent) tangent = tuple(Vector(tangent).normalized()) tangent = (tangent[0], tangent[1], tangent[2], loop.bitangent_sign) else: @@ -378,14 +384,14 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat bone_weights[bone_indx] = bone_weight bone_weights_sum += bone_weight - skin_entry = SkinStream.Entry(mesh_piece.get_index(), piece_vert_index, position, bone_weights, bone_weights_sum) - skin_stream.add_entry(skin_entry) - - # report un-skinned vertices (no bones or zero sum weight) or badly skinned model - if bone_weights_sum <= 0: + if bone_weights_sum > 0: + skin_entry = PieceSkinStream.Entry(piece_vert_index, position, bone_weights, bone_weights_sum) + skin_stream.add_entry(skin_entry) + else: + # report un-skinned vertices (no bones or zero sum weight) or badly skinned model missing_skinned_verts.add(vert_i) - elif bone_weights_sum < 1: - has_unnormalized_skin = True + if bone_weights_sum < 1: + has_unnormalized_skin = True # 9. Terrain Points: save vertex to terrain points storage, if present in correct vertex group for group in mesh.vertices[vert_i].groups: @@ -434,9 +440,9 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat else: mesh_piece.add_triangle(tuple(triangle_pvert_indices[::-1])) # yep it's weird but it simply works vice versa - # free normals calculations - _mesh_utils.cleanup_mesh(mesh) + # free normals calculations & remove temporary mesh _mesh_utils.cleanup_mesh(mesh_for_normals) + mesh_obj.to_mesh_clear() # report missing data for each object if len(missing_uv_layers) > 0: @@ -487,7 +493,7 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat # create locators data sections for loc_obj in model_locators: - pos, qua, sca = _get_scs_transformation_components(root_object.matrix_world.inverted() * loc_obj.matrix_world) + pos, qua, sca = _get_scs_transformation_components(root_object.matrix_world.inverted() @ loc_obj.matrix_world) if sca[0] * sca[1] * sca[2] < 0: lprint("W Model locator %r inside SCS Root Object %r not exported because of invalid scale.\n\t " + @@ -542,7 +548,9 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat if is_skin_used: pim_container.append(bones.get_as_section()) - pim_container.append(skin.get_as_section()) + + for piece_key in pim_piece_skins: + pim_container.append(pim_piece_skins[piece_key].get_as_section()) # write to file ind = " " diff --git a/addon/io_scs_tools/exp/pim/globall.py b/addon/io_scs_tools/exp/pim/globall.py index 1cbc975..01b6f63 100644 --- a/addon/io_scs_tools/exp/pim/globall.py +++ b/addon/io_scs_tools/exp/pim/globall.py @@ -16,12 +16,13 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software from io_scs_tools.exp.pim.piece import Piece from io_scs_tools.exp.pim.material import Material from io_scs_tools.exp.pim.locator import Locator from io_scs_tools.exp.pim.bones import Bones +from io_scs_tools.exp.pim.piece_skin import PieceSkin from io_scs_tools.internals.structure import SectionData as _SectionData @@ -40,6 +41,7 @@ def __init__(self, part_count, skeleton): Material.reset_counter() Locator.reset_counter() Bones.reset_counter() + PieceSkin.reset_counter() self.__part_count = part_count self.__skeleton = skeleton.replace("\\", "/") # make sure to replace backslashes for windows paths @@ -59,5 +61,6 @@ def get_as_section(self): section.props.append(("BoneCount", Bones.get_global_bones_count())) section.props.append(("LocatorCount", Locator.get_global_locator_count())) section.props.append(("Skeleton", self.__skeleton)) + section.props.append(("PieceSkinCount", PieceSkin.get_global_piece_skin_count())) return section diff --git a/addon/io_scs_tools/exp/pim/material.py b/addon/io_scs_tools/exp/pim/material.py index 3b75c3f..2deb809 100644 --- a/addon/io_scs_tools/exp/pim/material.py +++ b/addon/io_scs_tools/exp/pim/material.py @@ -152,6 +152,7 @@ def get_as_section(self): """ section = _SectionData("Material") + section.props.append(("Index", self.__index)) section.props.append(("Alias", self.__alias)) section.props.append(("Effect", self.__effect)) diff --git a/addon/io_scs_tools/exp/pim/piece_skin.py b/addon/io_scs_tools/exp/pim/piece_skin.py new file mode 100644 index 0000000..cd4c90c --- /dev/null +++ b/addon/io_scs_tools/exp/pim/piece_skin.py @@ -0,0 +1,87 @@ +# ##### 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) 2013-2019: SCS Software + +from collections import OrderedDict +from io_scs_tools.exp.pim.piece_skin_stream import PieceSkinStream +from io_scs_tools.internals.structure import SectionData as _SectionData + + +class PieceSkin: + __global_piece_skin_counter = 0 + + @staticmethod + def reset_counter(): + PieceSkin.__global_piece_skin_counter = 0 + + @staticmethod + def get_global_piece_skin_count(): + return PieceSkin.__global_piece_skin_counter + + __piece = -1 + __skin_streams_count = 0 + __piece_skin_streams = None + + def __init__(self, piece_idx, skin_stream): + """Initialize skin with given skin stream object. + :param piece_idx: index of the piece inside SCS game object + :type piece_idx: int + :param skin_stream: position skin stream for this skin + :type skin_stream: PieceSkinStream + """ + + self.__piece = piece_idx + self.__skin_streams_count = 0 + self.__piece_skin_streams = OrderedDict() + self.__piece_skin_streams[skin_stream.get_tag()] = skin_stream + + PieceSkin.__global_piece_skin_counter += 1 + + def get_skin_stream_by_type(self, stream_type): + """Gets skin stream by given type. + + :param stream_type: type of the stream to get, one of PieceSkinStream.Types + :type stream_type: str + :return: piece skin stream of given type or None if stream type deosn't exists + :rtype: PieceSkinStream | None + """ + + skin_types = (PieceSkinStream.Types.POSITION, PieceSkinStream.Types.NORMAL, PieceSkinStream.Types.TANGENT) + if stream_type in skin_types and stream_type in self.__piece_skin_streams: + return self.__piece_skin_streams[stream_type] + + return None + + def get_as_section(self): + """Gets whole model skin represented with SectionData structure class. + :return: packed skin as section data + :rtype: io_scs_tools.internals.structure.SectionData + """ + + self.__skin_streams_count = len(self.__piece_skin_streams) + + section = _SectionData("PieceSkin") + + section.props.append(("Piece", self.__piece)) + section.props.append(("StreamCount", self.__skin_streams_count)) + + for piece_skin_stream in self.__piece_skin_streams.values(): + section.sections.append(piece_skin_stream.get_as_section()) + + return section diff --git a/addon/io_scs_tools/exp/pim/skin_stream.py b/addon/io_scs_tools/exp/pim/piece_skin_stream.py similarity index 74% rename from addon/io_scs_tools/exp/pim/skin_stream.py rename to addon/io_scs_tools/exp/pim/piece_skin_stream.py index 69bf975..3c21bb6 100644 --- a/addon/io_scs_tools/exp/pim/skin_stream.py +++ b/addon/io_scs_tools/exp/pim/piece_skin_stream.py @@ -16,13 +16,13 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2015: SCS Software +# Copyright (C) 2013-2019: SCS Software from collections import OrderedDict from io_scs_tools.internals.structure import SectionData as _SectionData -class SkinStream: +class PieceSkinStream: class Types: """Enumerator class for storing types of possible PIM streams """ @@ -40,14 +40,12 @@ class Entry: __position = None __bone_weights = None - __clones = None + __vertex_indices = None - def __init__(self, piece_index, vertex_index, vertex_pos, bone_weights, bone_weights_sum): + def __init__(self, vertex_index, vertex_pos, bone_weights, bone_weights_sum): """Create new entry instance with given indices, position and bone weights. NOTE: There is no check-up for zero bone weights sum, which will result in division by zero error - :param piece_index: index of the piece inside SCS game object - :type piece_index: int :param vertex_index: index of the vertex inside piece :type vertex_index: int :param vertex_pos: global position of the vertex in non animated state @@ -59,7 +57,7 @@ def __init__(self, piece_index, vertex_index, vertex_pos, bone_weights, bone_wei """ self.__bone_weights = OrderedDict() - self.__clones = OrderedDict() + self.__vertex_indices = OrderedDict() self.__position = vertex_pos @@ -67,33 +65,31 @@ def __init__(self, piece_index, vertex_index, vertex_pos, bone_weights, bone_wei for bone_indx in bone_weights.keys(): self.__bone_weights[bone_indx] = bone_weights[bone_indx] / bone_weights_sum - self.add_clone(piece_index, vertex_index) + self.add_vertex_index(vertex_index) - def add_clone(self, piece_index, vertex_index): - """Add clone to this skin entry with given piece and vertex index. + def add_vertex_index(self, vertex_index): + """Add vertex index to this skin entry. NOTE: in case of already existing clone value will be overwritten; so there are only unique entries - :param piece_index: index of the piece inside SCS game object - :type piece_index: int + :param vertex_index: index of the vertex inside piece :type vertex_index: int :return: True if clone was added; False if clone already exists; :rtype: bool """ - clone_hash = str(piece_index) + ":" + str(vertex_index) - if clone_hash not in self.__clones: - self.__clones[clone_hash] = (piece_index, vertex_index) + if vertex_index not in self.__vertex_indices: + self.__vertex_indices[vertex_index] = None return True return False - def get_original_piece_info(self): - """Get piece data for origin of this skin entry. + def get_first_vertex_index(self): + """Get first index from vertex indices in this entry.. - :return: first clone which should be also origin of this skin entry - :rtype: tuple + :return: first vertex index which should be also origin of this skin entry + :rtype: int """ - return self.__clones[list(self.__clones.keys())[0]] + return next(iter(self.__vertex_indices.keys())) def get_hash(self): """Gets hash for this entry used for identifying clones in skin streams. @@ -113,6 +109,7 @@ def get_hash(self): def get_weight_count(self): """Gets number of weights written in this skin entry. Shall be used as helper for global weight count inside skin stream. + :return: number of weights :rtype: int """ @@ -120,6 +117,7 @@ def get_weight_count(self): def get_section_repr(self): """Gets representation of skin entry used in skin stream section. + :return: tuple of (position, list of (bone_index, bone_weight), list of (piece_index, vertex_index) :rtype: tuple """ @@ -128,37 +126,37 @@ def get_section_repr(self): for bone_indx in sorted(self.__bone_weights.keys()): weights.append((bone_indx, self.__bone_weights[bone_indx])) - clones = [] - for clone_hash in sorted(self.__clones.keys()): - clones.append(self.__clones[clone_hash]) + vertex_indices = [] + for vertex_indx in sorted(self.__vertex_indices.keys()): + vertex_indices.append(vertex_indx) - return self.__position, weights, clones + return self.__position, weights, vertex_indices __format = "" # defined by type of tag __tag = Types.POSITION __item_count = 0 __total_weight_count = 0 - __total_clone_count = 0 + __total_vertex_index_count = 0 _data = None def __init__(self, stream_type): """Creates new skin stream with specified type. :param stream_type: stream type for skin - :type stream_type: SkinStream.Types + :type stream_type: PieceSkinStream.Types """ self.__tag = stream_type - if stream_type == SkinStream.Types.POSITION: + if stream_type == PieceSkinStream.Types.POSITION: self.__format = "FLOAT3" - elif stream_type == SkinStream.Types.NORMAL: + elif stream_type == PieceSkinStream.Types.NORMAL: self.__format = "FLOAT3" - elif stream_type == SkinStream.Types.TANGENT: + elif stream_type == PieceSkinStream.Types.TANGENT: self.__format = "FLOAT4" self.__total_weight_count = 0 - self.__total_clone_count = 0 + self.__total_vertex_index_count = 0 self._data = OrderedDict() @@ -166,19 +164,18 @@ def add_entry(self, skin_entry): """Adds new skin stream entry to list. NOTE: same entries can be added as duplicates will be ignored! :param skin_entry: entry of the skin stream - :type skin_entry: SkinStream.Entry + :type skin_entry: PieceSkinStream.Entry """ entry_hash = skin_entry.get_hash() if entry_hash not in self._data: # create new entry self._data[entry_hash] = skin_entry self.__total_weight_count += skin_entry.get_weight_count() - self.__total_clone_count += 1 - else: - # add clone and increment clone count if clone was added - piece_index, vertex_index = skin_entry.get_original_piece_info() - if self._data[entry_hash].add_clone(piece_index, vertex_index): - self.__total_clone_count += 1 + self.__total_vertex_index_count += 1 + else: # add new vertex indedx and increment vertex index count if index was actually added + vertex_index = skin_entry.get_first_vertex_index() + if self._data[entry_hash].add_vertex_index(vertex_index): + self.__total_vertex_index_count += 1 def get_tag(self): """Returns tag which represents type of this skin stream @@ -195,13 +192,13 @@ def get_as_section(self): self.__item_count = len(self._data) - section = _SectionData("SkinStream") + section = _SectionData("PieceSkinStream") section.props.append(("Format", self.__format)) section.props.append(("Tag", self.__tag)) section.props.append(("ItemCount", self.__item_count)) section.props.append(("TotalWeightCount", self.__total_weight_count)) - section.props.append(("TotalCloneCount", self.__total_clone_count)) + section.props.append(("TotalVertexIndexCount", self.__total_vertex_index_count)) for skin_entry in self._data.values(): section.data.append(("__skin__", skin_entry.get_section_repr())) diff --git a/addon/io_scs_tools/exp/pim/skin.py b/addon/io_scs_tools/exp/pim/skin.py deleted file mode 100644 index 26fc1a4..0000000 --- a/addon/io_scs_tools/exp/pim/skin.py +++ /dev/null @@ -1,53 +0,0 @@ -# ##### 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) 2013-2015: SCS Software - -from collections import OrderedDict -from io_scs_tools.exp.pim.skin_stream import SkinStream -from io_scs_tools.internals.structure import SectionData as _SectionData - - -class Skin: - __skin_streams_count = 0 - __skin_streams = OrderedDict() - - def __init__(self, skin_stream): - """Initialize skin with given skin stream object. - :param skin_stream: position skin stream for this skin - :type skin_stream: SkinStream - """ - - self.__skin_streams[skin_stream.get_tag()] = skin_stream - - def get_as_section(self): - """Gets whole model skin represented with SectionData structure class. - :return: packed skin as section data - :rtype: io_scs_tools.internals.structure.SectionData - """ - - self.__skin_streams_count = len(self.__skin_streams) - - section = _SectionData("Skin") - - section.props.append(("StreamCount", self.__skin_streams_count)) - - for skin_stream in self.__skin_streams.values(): - section.sections.append(skin_stream.get_as_section()) - - return section diff --git a/addon/io_scs_tools/exp/pim_ef/exporter.py b/addon/io_scs_tools/exp/pim_ef/exporter.py index f42b90a..1d0fe07 100644 --- a/addon/io_scs_tools/exp/pim_ef/exporter.py +++ b/addon/io_scs_tools/exp/pim_ef/exporter.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2017: SCS Software +# Copyright (C) 2017-2019: SCS Software import os import collections @@ -32,8 +32,8 @@ from io_scs_tools.exp.pim_ef.part import Part from io_scs_tools.exp.pim_ef.locator import Locator from io_scs_tools.exp.pim_ef.bones import Bones -from io_scs_tools.exp.pim_ef.skin import Skin -from io_scs_tools.exp.pim_ef.skin import SkinStream +from io_scs_tools.exp.pim_ef.piece_skin import PieceSkin +from io_scs_tools.exp.pim_ef.piece_skin import PieceSkinStream from io_scs_tools.internals.containers import pix as _pix_container from io_scs_tools.utils import mesh as _mesh_utils from io_scs_tools.utils import name as _name_utils @@ -96,11 +96,13 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat """:type: dict[str, Part]""" pim_locators = [] # list of Locator class instances representing model locators """:type: list[Locator]""" + pim_piece_skins = [] + """:type: dict[str, PieceSkin""" objects_with_default_material = {} # stores object names which has no material set missing_mappings_data = {} # indicates if material doesn't have set any uv layer for export - bones = skin = skin_stream = None + bones = None if is_skin_used: # create bones data section bones = Bones() @@ -108,10 +110,6 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat bones.add_bone(bone.name) used_bones.add(bone.name) - # create skin data section - skin_stream = SkinStream(SkinStream.Types.POSITION) - skin = Skin(skin_stream) - # create mesh object data sections for mesh_obj in mesh_objects: @@ -135,11 +133,11 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat winding_order = -1 # calculate transformation matrix for current object (root object transforms are always subtracted!) - mesh_transf_mat = root_object.matrix_world.inverted() * mesh_obj.matrix_world + mesh_transf_mat = root_object.matrix_world.inverted() @ mesh_obj.matrix_world """:type: mathutils.Matrix""" # calculate vertex position transformation matrix for this object - pos_transf_mat = (Matrix.Scale(scs_globals.export_scale, 4) * + pos_transf_mat = (Matrix.Scale(scs_globals.export_scale, 4) @ _scs_to_blend_matrix().inverted()) """:type: mathutils.Matrix""" @@ -149,18 +147,14 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat scale_matrix_x = Matrix.Scale(scale.x, 3, Vector((1, 0, 0))).to_4x4() scale_matrix_y = Matrix.Scale(scale.y, 3, Vector((0, 1, 0))).to_4x4() scale_matrix_z = Matrix.Scale(scale.z, 3, Vector((0, 0, 1))).to_4x4() - nor_transf_mat = (_scs_to_blend_matrix().inverted() * - rot.to_matrix().to_4x4() * - scale_matrix_x * scale_matrix_y * scale_matrix_z) + nor_transf_mat = _scs_to_blend_matrix().inverted() @ rot.to_matrix().to_4x4() @ scale_matrix_x @ scale_matrix_y @ scale_matrix_z """:type: mathutils.Matrix""" - # get initial mesh and vertex groups for it + # get initial mesh and extra copy for normals only mesh = _object_utils.get_mesh(mesh_obj) - _mesh_utils.bm_prepare_mesh_for_export(mesh, mesh_transf_mat) + mesh_for_normals = _mesh_utils.get_mesh_for_normals(mesh) - # get extra mesh only for normals - mesh_for_normals = _object_utils.get_mesh(mesh_obj) - mesh_for_normals.calc_normals_split() + _mesh_utils.bm_prepare_mesh_for_export(mesh, mesh_transf_mat) missing_uv_layers = {} # stores missing uvs specified by materials of this object missing_vcolor = False # indicates if object is missing vertex color layer @@ -171,6 +165,15 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat hard_edges = set() mesh_piece = Piece(len(pim_pieces)) """:type: Piece""" + + # create/get skin data section for current piece + if is_skin_used: + skin_stream = PieceSkinStream(PieceSkinStream.Types.POSITION) + piece_skin = PieceSkin(mesh_piece.get_index(), skin_stream) + pim_piece_skins.append(piece_skin) + else: + skin_stream = None + for poly in mesh.polygons: mat_index = poly.material_index @@ -212,10 +215,10 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat # get data of current vertex # 1. position -> mesh.vertices[loop.vertex_index].co - position = tuple(pos_transf_mat * mesh.vertices[vert_i].co) + position = tuple(pos_transf_mat @ mesh.vertices[vert_i].co) # 2. normal -> loop.normal -> calc_normals_split() has to be called before - normal = nor_transf_mat * mesh_for_normals.loops[loop_i].normal + normal = nor_transf_mat @ mesh_for_normals.loops[loop_i].normal normal = tuple(Vector(normal).normalized()) vert_normals.append(normal) @@ -303,14 +306,14 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat bone_weights[bone_indx] = bone_weight bone_weights_sum += bone_weight - skin_entry = SkinStream.Entry(mesh_piece.get_index(), piece_vert_index, position, bone_weights, bone_weights_sum) - skin_stream.add_entry(skin_entry) - - # report un-skinned vertices (no bones or zero sum weight) or badly skinned model - if bone_weights_sum <= 0: + if bone_weights_sum > 0: + skin_entry = PieceSkinStream.Entry(piece_vert_index, position, bone_weights, bone_weights_sum) + skin_stream.add_entry(skin_entry) + else: + # report un-skinned vertices (no bones or zero sum weight) or badly skinned model missing_skinned_verts.add(vert_i) - elif bone_weights_sum < 1: - has_unnormalized_skin = True + if bone_weights_sum < 1: + has_unnormalized_skin = True # save to terrain points storage if present in correct vertex group for group in mesh.vertices[vert_i].groups: @@ -368,9 +371,9 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat (vert1_i, vert2_i) = mesh.edges[hard_edge].vertices assert mesh_piece.add_edge(vert1_i, vert2_i, blender_mesh_indices=True) - # free normals calculations and eventually remove mesh object - _mesh_utils.cleanup_mesh(mesh) + # free normals calculations & remove temporary mesh _mesh_utils.cleanup_mesh(mesh_for_normals) + mesh_obj.to_mesh_clear() # create part if it doesn't exists yet part_name = mesh_obj.scs_props.scs_part @@ -415,7 +418,7 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat # create locators data sections for loc_obj in model_locators: - pos, qua, sca = _get_scs_transformation_components(root_object.matrix_world.inverted() * loc_obj.matrix_world) + pos, qua, sca = _get_scs_transformation_components(root_object.matrix_world.inverted() @ loc_obj.matrix_world) if sca[0] * sca[1] * sca[2] < 0: lprint("W Model locator %r inside SCS Root Object %r not exported because of invalid scale.\n\t " + @@ -472,7 +475,8 @@ def execute(dirpath, name_suffix, root_object, armature_object, skeleton_filepat if is_skin_used: pim_container.append(bones.get_as_section()) - pim_container.append(skin.get_as_section()) + for piece_skin in pim_piece_skins: + pim_container.append(piece_skin.get_as_section()) # write to file ind = " " diff --git a/addon/io_scs_tools/exp/pim_ef/globall.py b/addon/io_scs_tools/exp/pim_ef/globall.py index 4190ebc..f716763 100644 --- a/addon/io_scs_tools/exp/pim_ef/globall.py +++ b/addon/io_scs_tools/exp/pim_ef/globall.py @@ -16,12 +16,13 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2017: SCS Software +# Copyright (C) 2017-2019: SCS Software from io_scs_tools.exp.pim_ef.piece import Piece from io_scs_tools.exp.pim.material import Material from io_scs_tools.exp.pim.locator import Locator from io_scs_tools.exp.pim.bones import Bones +from io_scs_tools.exp.pim.piece_skin import PieceSkin from io_scs_tools.internals.structure import SectionData as _SectionData @@ -40,6 +41,7 @@ def __init__(self, part_count, skeleton): Material.reset_counter() Locator.reset_counter() Bones.reset_counter() + PieceSkin.reset_counter() self.__part_count = part_count self.__skeleton = skeleton.replace("\\", "/") # make sure to replace backslashes for windows paths @@ -59,5 +61,6 @@ def get_as_section(self): section.props.append(("BoneCount", Bones.get_global_bones_count())) section.props.append(("LocatorCount", Locator.get_global_locator_count())) section.props.append(("Skeleton", self.__skeleton)) + section.props.append(("PieceSkinCount", PieceSkin.get_global_piece_skin_count())) return section diff --git a/addon/io_scs_tools/exp/pim_ef/skin.py b/addon/io_scs_tools/exp/pim_ef/piece_skin.py similarity index 82% rename from addon/io_scs_tools/exp/pim_ef/skin.py rename to addon/io_scs_tools/exp/pim_ef/piece_skin.py index 269a04b..f9598c7 100644 --- a/addon/io_scs_tools/exp/pim_ef/skin.py +++ b/addon/io_scs_tools/exp/pim_ef/piece_skin.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2017: SCS Software +# Copyright (C) 2017-2019: SCS Software -from io_scs_tools.exp.pim.skin import Skin -from io_scs_tools.exp.pim_ef.skin_stream import SkinStream +from io_scs_tools.exp.pim.piece_skin import PieceSkin +from io_scs_tools.exp.pim_ef.piece_skin_stream import PieceSkinStream diff --git a/addon/io_scs_tools/exp/pim_ef/skin_stream.py b/addon/io_scs_tools/exp/pim_ef/piece_skin_stream.py similarity index 88% rename from addon/io_scs_tools/exp/pim_ef/skin_stream.py rename to addon/io_scs_tools/exp/pim_ef/piece_skin_stream.py index 5b79c8a..52d288d 100644 --- a/addon/io_scs_tools/exp/pim_ef/skin_stream.py +++ b/addon/io_scs_tools/exp/pim_ef/piece_skin_stream.py @@ -16,6 +16,6 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2017: SCS Software +# Copyright (C) 2017-2019: SCS Software -from io_scs_tools.exp.pim.skin_stream import SkinStream +from io_scs_tools.exp.pim.piece_skin_stream import PieceSkinStream diff --git a/addon/io_scs_tools/exp/pip/curve.py b/addon/io_scs_tools/exp/pip/curve.py index f11fe6b..a8dfafa 100644 --- a/addon/io_scs_tools/exp/pip/curve.py +++ b/addon/io_scs_tools/exp/pip/curve.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015-2017: SCS Software +# Copyright (C) 2015-2019: SCS Software from mathutils import Vector, Quaternion from io_scs_tools.consts import PrefabLocators as _PL_consts @@ -57,8 +57,8 @@ def prepare_curves(curves_l): pos1, dir1 = curve.get_end(cartes_tang=False) for i in range(4): # converge to actual length with four iterations - curr_dir0 = Vector(dir0 * Vector((0, 0, -1)) * Curve.__NODE_DIR_LEN_COEF * curve.get_length()) - curr_dir1 = Vector(dir1 * Vector((0, 0, -1)) * Curve.__NODE_DIR_LEN_COEF * curve.get_length()) + curr_dir0 = Vector(dir0 @ Vector((0, 0, -1)) * Curve.__NODE_DIR_LEN_COEF * curve.get_length()) + curr_dir1 = Vector(dir1 @ Vector((0, 0, -1)) * Curve.__NODE_DIR_LEN_COEF * curve.get_length()) new_length = _curve_utils.compute_smooth_curve_length(pos0, curr_dir0, pos1, curr_dir1, _PL_consts.CURVE_MEASURE_STEPS) curve.set_length(new_length) @@ -343,7 +343,7 @@ def get_start(self, cartes_tang=True): position, rotation = self.__bezier.get_start() if cartes_tang: - rotation = rotation * Vector((0, 0, -1)) * Curve.__NODE_DIR_LEN_COEF * self.get_length() + rotation = rotation @ Vector((0, 0, -1)) * Curve.__NODE_DIR_LEN_COEF * self.get_length() return position, rotation @@ -361,7 +361,7 @@ def get_end(self, cartes_tang=True): position, rotation = self.__bezier.get_end() if cartes_tang: - rotation = rotation * Vector((0, 0, -1)) * Curve.__NODE_DIR_LEN_COEF * self.get_length() + rotation = rotation @ Vector((0, 0, -1)) * Curve.__NODE_DIR_LEN_COEF * self.get_length() return position, rotation diff --git a/addon/io_scs_tools/exp/pip/exporter.py b/addon/io_scs_tools/exp/pip/exporter.py index 20ff6dd..4a9415c 100644 --- a/addon/io_scs_tools/exp/pip/exporter.py +++ b/addon/io_scs_tools/exp/pip/exporter.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015-2017: SCS Software +# Copyright (C) 2015-2019: SCS Software from os import path from collections import OrderedDict @@ -33,7 +33,7 @@ from io_scs_tools.exp.pip.spawn_point import SpawnPoint from io_scs_tools.exp.pip.trigger_point import TriggerPoint from io_scs_tools.internals.containers import pix as _pix_container -from io_scs_tools.internals.connections.wrappers import group as _connections_group_wrapper +from io_scs_tools.internals.connections.wrappers import collection as _connections_wrapper from io_scs_tools.utils.convert import get_scs_transformation_components as _get_scs_transformation_components from io_scs_tools.utils.name import tokenize_name as _tokenize_name from io_scs_tools.utils.printout import lprint @@ -156,7 +156,7 @@ def execute(dirpath, filename, name_suffix, prefab_locator_list, offset_matrix, """ # CLEANUP CONNECTIONS DATA - _connections_group_wrapper.cleanup_on_export() + _connections_wrapper.cleanup_on_export() print("\n************************************") print("** SCS PIP Exporter **") @@ -200,8 +200,8 @@ def execute(dirpath, filename, name_suffix, prefab_locator_list, offset_matrix, curr_node_i = int(locator_scs_props.locator_prefab_con_node_index) if curr_node_i not in pip_nodes: - pos, rot, scale = _get_scs_transformation_components(offset_matrix.inverted() * locator.matrix_world) - rot = Quaternion(rot) * Vector((0, 0, -1)) + pos, rot, scale = _get_scs_transformation_components(offset_matrix.inverted() @ locator.matrix_world) + rot = Quaternion(rot) @ Vector((0, 0, -1)) # create node with position and direction cn = Node(curr_node_i, pos, rot) @@ -223,7 +223,7 @@ def execute(dirpath, filename, name_suffix, prefab_locator_list, offset_matrix, "Check Control Nodes in SCS Game Object with Root: %r", (filename,)) # curves creation - curves_dict = _connections_group_wrapper.get_curves(nav_point_locs.values()) + curves_dict = _connections_wrapper.get_curves(nav_point_locs.values()) for key, curve_entry in curves_dict.items(): loc0 = nav_point_locs[curves_dict[key].start] @@ -236,9 +236,9 @@ def execute(dirpath, filename, name_suffix, prefab_locator_list, offset_matrix, # create curve and set properties curve = __get_curve__(pip_curves, curve_entry.index, loc0.name) - pos, rot, scale = _get_scs_transformation_components(offset_matrix.inverted() * loc0.matrix_world) + pos, rot, scale = _get_scs_transformation_components(offset_matrix.inverted() @ loc0.matrix_world) curve.set_start(pos, rot) - pos, rot, scale = _get_scs_transformation_components(offset_matrix.inverted() * loc1.matrix_world) + pos, rot, scale = _get_scs_transformation_components(offset_matrix.inverted() @ loc1.matrix_world) curve.set_end(pos, rot) curve.set_input_boundaries(loc0_scs_props) @@ -300,7 +300,7 @@ def execute(dirpath, filename, name_suffix, prefab_locator_list, offset_matrix, # create sign and set properties sign = Sign(locator.name, used_parts.ensure_part(locator)) - pos, rot, scale = _get_scs_transformation_components(offset_matrix.inverted() * locator.matrix_world) + pos, rot, scale = _get_scs_transformation_components(offset_matrix.inverted() @ locator.matrix_world) sign.set_position(pos) sign.set_rotation(rot) @@ -321,7 +321,7 @@ def execute(dirpath, filename, name_suffix, prefab_locator_list, offset_matrix, # create spawn point and set properties spawn_point = SpawnPoint(locator.name) - pos, rot, scale = _get_scs_transformation_components(offset_matrix.inverted() * locator.matrix_world) + pos, rot, scale = _get_scs_transformation_components(offset_matrix.inverted() @ locator.matrix_world) spawn_point.set_position(pos) spawn_point.set_rotation(rot) @@ -338,7 +338,7 @@ def execute(dirpath, filename, name_suffix, prefab_locator_list, offset_matrix, # create semaphore and set properties semaphore = Semaphore(int(locator_scs_props.locator_prefab_tsem_type)) - pos, rot, scale = _get_scs_transformation_components(offset_matrix.inverted() * locator.matrix_world) + pos, rot, scale = _get_scs_transformation_components(offset_matrix.inverted() @ locator.matrix_world) semaphore.set_position(pos) semaphore.set_rotation(rot) @@ -367,12 +367,12 @@ def execute(dirpath, filename, name_suffix, prefab_locator_list, offset_matrix, # create map point and set properties map_point = __get_map_point__(pip_map_points, locator.name) - pos, rot, scale = _get_scs_transformation_components(offset_matrix.inverted() * locator.matrix_world) + pos, rot, scale = _get_scs_transformation_components(offset_matrix.inverted() @ locator.matrix_world) map_point.set_position(pos) map_point.set_flags(locator_scs_props) - for neighbour_name in _connections_group_wrapper.get_neighbours(locator): + for neighbour_name in _connections_wrapper.get_neighbours(locator): assert map_point.add_neighbour(__get_map_point__(pip_map_points, neighbour_name)) @@ -389,7 +389,7 @@ def execute(dirpath, filename, name_suffix, prefab_locator_list, offset_matrix, # create trigger point and set properties trigger_point = __get_trigger_point__(pip_trigger_points, locator.name) - pos, rot, scale = _get_scs_transformation_components(offset_matrix.inverted() * locator.matrix_world) + pos, rot, scale = _get_scs_transformation_components(offset_matrix.inverted() @ locator.matrix_world) trigger_point.set_position(pos) if ":" in locator_scs_props.locator_prefab_tp_action: @@ -402,7 +402,7 @@ def execute(dirpath, filename, name_suffix, prefab_locator_list, offset_matrix, trigger_point.set_reset_delay(locator_scs_props.locator_prefab_tp_reset_delay) trigger_point.set_flags(locator_scs_props) - for neighbour_name in _connections_group_wrapper.get_neighbours(locator): + for neighbour_name in _connections_wrapper.get_neighbours(locator): assert trigger_point.add_neighbour(__get_trigger_point__(pip_trigger_points, neighbour_name)) diff --git a/addon/io_scs_tools/exp/pis.py b/addon/io_scs_tools/exp/pis.py index 7b43a0b..af7242c 100644 --- a/addon/io_scs_tools/exp/pis.py +++ b/addon/io_scs_tools/exp/pis.py @@ -58,10 +58,9 @@ def _fill_bones_sections(scs_root_obj, armature_obj, used_bones, export_scale): # armature matrix stores transformation of armature object against scs root # and has to be added to all bones as they only armature space transformations - armature_mat = scs_root_obj.matrix_world.inverted() * armature_obj.matrix_world + armature_mat = scs_root_obj.matrix_world.inverted() @ armature_obj.matrix_world - bone_mat = (Matrix.Scale(export_scale, 4) * _convert_utils.scs_to_blend_matrix().inverted() * - armature_mat * bone.matrix_local) + bone_mat = (Matrix.Scale(export_scale, 4) @ _convert_utils.scs_to_blend_matrix().inverted() @ armature_mat @ bone.matrix_local) section.data.append(("__bone__", bone.name, bone.parent, bone_mat.transposed())) return section diff --git a/addon/io_scs_tools/exp/pit.py b/addon/io_scs_tools/exp/pit.py index 2209b0e..dd091ce 100644 --- a/addon/io_scs_tools/exp/pit.py +++ b/addon/io_scs_tools/exp/pit.py @@ -295,7 +295,7 @@ def fill_part_list(parts, used_parts_names, all_parts=False): """Fills up "Part" sections in "Varian" section :param parts: SCS Root part inventory or parts collection property from variant inventory - :type parts: io_scs_tools.properties.object.ObjectPartInventoryItem | list[io_scs_tools.properties.object.ObjectVariantPartInclusion] + :type parts: io_scs_tools.properties.object.ObjectPartInventoryItem | list[io_scs_tools.properties.object.ObjectVariantPartInclusionItem] :param used_parts_names: list of part names that are actually used in game object :type used_parts_names: list[str] :param all_parts: flag for all parts are visible (handy for creating default visibilities) @@ -371,10 +371,8 @@ def export(root_object, filepath, name_suffix, used_parts, used_materials): # apply each look from inventory first if len(looks_inventory) > 0: - root_object.scs_props.active_scs_look = i - - # actually write values to material because Blender might not refresh data yet - _looks.apply_active_look(root_object) + root_object.scs_props.active_scs_look = i # set index for curret look + _looks.apply_active_look(root_object) # apply look manually, as active look setter method works only when user sets index from UI curr_look_name = looks_inventory[i].name else: # if no looks create default @@ -608,7 +606,8 @@ def export(root_object, filepath, name_suffix, used_parts, used_materials): look_list.append(look_data) # restore look applied before export - root_object.scs_props.active_scs_look = saved_active_look + root_object.scs_props.active_scs_look = saved_active_look # set index for curret look + _looks.apply_active_look(root_object) # apply look manually, as active look setter method works only when user sets index from UI # PARTS AND VARIANTS... used_parts_names = used_parts.get_as_list() diff --git a/addon/io_scs_tools/exp/pit_ef.py b/addon/io_scs_tools/exp/pit_ef.py index 7f3b59e..1f3d0fd 100644 --- a/addon/io_scs_tools/exp/pit_ef.py +++ b/addon/io_scs_tools/exp/pit_ef.py @@ -214,6 +214,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) diff --git a/addon/io_scs_tools/imp/pia.py b/addon/io_scs_tools/imp/pia.py index 86bebd4..42e15c0 100644 --- a/addon/io_scs_tools/imp/pia.py +++ b/addon/io_scs_tools/imp/pia.py @@ -132,36 +132,36 @@ def _create_fcurves(anim_action, anim_group, anim_curve, rot_euler=True, types=' """ pos_fcurves = rot_fcurves = sca_fcurves = None if 'Loc' in types: - fcurve_pos_x = anim_action.fcurves.new(str(anim_curve + '.location'), 0) - fcurve_pos_y = anim_action.fcurves.new(str(anim_curve + '.location'), 1) - fcurve_pos_z = anim_action.fcurves.new(str(anim_curve + '.location'), 2) + fcurve_pos_x = anim_action.fcurves.new(str(anim_curve + '.location'), index=0) + fcurve_pos_y = anim_action.fcurves.new(str(anim_curve + '.location'), index=1) + fcurve_pos_z = anim_action.fcurves.new(str(anim_curve + '.location'), index=2) fcurve_pos_x.group = anim_group fcurve_pos_y.group = anim_group fcurve_pos_z.group = anim_group pos_fcurves = (fcurve_pos_x, fcurve_pos_y, fcurve_pos_z) if 'Rot' in types: if rot_euler: - fcurve_rot_x = anim_action.fcurves.new(str(anim_curve + '.rotation_euler'), 0) - fcurve_rot_y = anim_action.fcurves.new(str(anim_curve + '.rotation_euler'), 1) - fcurve_rot_z = anim_action.fcurves.new(str(anim_curve + '.rotation_euler'), 2) + fcurve_rot_x = anim_action.fcurves.new(str(anim_curve + '.rotation_euler'), index=0) + fcurve_rot_y = anim_action.fcurves.new(str(anim_curve + '.rotation_euler'), index=1) + fcurve_rot_z = anim_action.fcurves.new(str(anim_curve + '.rotation_euler'), index=2) fcurve_rot_x.group = anim_group fcurve_rot_y.group = anim_group fcurve_rot_z.group = anim_group rot_fcurves = (fcurve_rot_x, fcurve_rot_y, fcurve_rot_z) else: - fcurve_rot_w = anim_action.fcurves.new(str(anim_curve + '.rotation_quaternion'), 0) - fcurve_rot_x = anim_action.fcurves.new(str(anim_curve + '.rotation_quaternion'), 1) - fcurve_rot_y = anim_action.fcurves.new(str(anim_curve + '.rotation_quaternion'), 2) - fcurve_rot_z = anim_action.fcurves.new(str(anim_curve + '.rotation_quaternion'), 3) + fcurve_rot_w = anim_action.fcurves.new(str(anim_curve + '.rotation_quaternion'), index=0) + fcurve_rot_x = anim_action.fcurves.new(str(anim_curve + '.rotation_quaternion'), index=1) + fcurve_rot_y = anim_action.fcurves.new(str(anim_curve + '.rotation_quaternion'), index=2) + fcurve_rot_z = anim_action.fcurves.new(str(anim_curve + '.rotation_quaternion'), index=3) fcurve_rot_w.group = anim_group fcurve_rot_x.group = anim_group fcurve_rot_y.group = anim_group fcurve_rot_z.group = anim_group rot_fcurves = (fcurve_rot_w, fcurve_rot_x, fcurve_rot_y, fcurve_rot_z) if 'Sca' in types: - fcurve_sca_x = anim_action.fcurves.new(str(anim_curve + '.scale'), 0) - fcurve_sca_y = anim_action.fcurves.new(str(anim_curve + '.scale'), 1) - fcurve_sca_z = anim_action.fcurves.new(str(anim_curve + '.scale'), 2) + fcurve_sca_x = anim_action.fcurves.new(str(anim_curve + '.scale'), index=0) + fcurve_sca_y = anim_action.fcurves.new(str(anim_curve + '.scale'), index=1) + fcurve_sca_z = anim_action.fcurves.new(str(anim_curve + '.scale'), index=2) fcurve_sca_x.group = anim_group fcurve_sca_y.group = anim_group fcurve_sca_z.group = anim_group @@ -181,10 +181,10 @@ def _get_delta_matrix(bone_rest_matrix_scs, parent_bone_rest_matrix_scs, bone_an scale[1] = (0, sca[1], 0, 0) scale[2] = (0, 0, sca[2], 0) - return (scale_matrix * - scale * - bone_rest_matrix_scs.inverted() * - parent_bone_rest_matrix_scs * + return (scale_matrix @ + scale @ + bone_rest_matrix_scs.inverted() @ + parent_bone_rest_matrix_scs @ bone_animation_matrix_scs) @@ -378,9 +378,9 @@ def load(root_object, pia_files, armature, pis_filepath=None, bones=None): # pos_fcurves, rot_fcurves, sca_fcurves = _create_fcurves(anim_action, anim_group, anim_curve, rot_euler=True, # types='LocRotSca') # pos_fcurves, rot_fcurves, sca_fcurves = _create_fcurves(anim_action, anim_group, anim_curve, types='Loc') - fcurve_pos_x = anim_action.fcurves.new('location', 0) - fcurve_pos_y = anim_action.fcurves.new('location', 1) - fcurve_pos_z = anim_action.fcurves.new('location', 2) + fcurve_pos_x = anim_action.fcurves.new('location', index=0) + fcurve_pos_y = anim_action.fcurves.new('location', index=1) + fcurve_pos_z = anim_action.fcurves.new('location', index=2) fcurve_pos_x.group = anim_group fcurve_pos_y.group = anim_group fcurve_pos_z.group = anim_group diff --git a/addon/io_scs_tools/imp/pim.py b/addon/io_scs_tools/imp/pim.py index 06a5b5a..275a408 100644 --- a/addon/io_scs_tools/imp/pim.py +++ b/addon/io_scs_tools/imp/pim.py @@ -16,14 +16,13 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy import bmesh import array from re import match from mathutils import Vector -from bpy_extras import object_utils as bpy_object_utils from io_scs_tools.consts import Mesh as _MESH_consts from io_scs_tools.consts import Operators as _OP_consts from io_scs_tools.imp.transition_structs.terrain_points import TerrainPntsTrans @@ -65,7 +64,7 @@ def get_header(pim_container): def get_global(pim_container): """Receives PIM container and returns all its Global properties in its own variables. For any item that fails to be found, it returns None.""" - vertex_count = face_count = edge_count = material_count = piece_count = part_count = bone_count = locator_count = 0 + vertex_count = face_count = edge_count = material_count = piece_count = part_count = bone_count = locator_count = piece_skin_count = 0 skeleton = None for section in pim_container: if section.type == "Global": @@ -90,25 +89,29 @@ def get_global(pim_container): locator_count = prop[1] elif prop[0] == "Skeleton": skeleton = prop[1] + elif prop[0] == "PieceSkinCount": + piece_skin_count = prop[1] else: lprint('\nW Unknown property in "Global" data: "%s"!', prop[0]) - return vertex_count, face_count, edge_count, material_count, piece_count, part_count, bone_count, locator_count, skeleton + return vertex_count, face_count, edge_count, material_count, piece_count, part_count, bone_count, locator_count, skeleton, piece_skin_count def get_material_properties(section): """Receives a Material section and returns its properties in its own variables. For any item that fails to be found, it returns None.""" - materials_alias = materials_effect = None + material_index = materials_alias = materials_effect = None for prop in section.props: if prop[0] in ("", "#"): pass + elif prop[0] == "Index": + material_index = prop[1] elif prop[0] == "Alias": materials_alias = prop[1] elif prop[0] == "Effect": materials_effect = prop[1] else: lprint('\nW Unknown property in "Material" data: "%s"!', prop[0]) - return materials_alias, materials_effect + return material_index, materials_alias, materials_effect def get_piece_properties(section): @@ -312,6 +315,31 @@ def _get_skin_stream(section): return skin_stream +def _get_piece_skin_stream(section): + piece_skin_stream = [] + stream_format = stream_tag = stream_item_count = stream_total_weight_count = stream_total_vertex_count = None + for prop in section.props: + # print('prop: %s' % prop) + if prop[0] in ("", "#"): + pass + elif prop[0] == "Format": + stream_format = prop[1] + elif prop[0] == "Tag": + stream_tag = prop[1] + elif prop[0] == "ItemCount": + stream_item_count = prop[1] + elif prop[0] == "TotalWeightCount": + stream_total_weight_count = prop[1] + elif prop[0] == "TotalVertexIndexCount": + stream_total_vertex_count = prop[1] + data_block = [] + for data_rec in section.data: + data_block.append(data_rec) + + piece_skin_stream.append((stream_format, stream_tag, stream_item_count, stream_total_weight_count, stream_total_vertex_count, data_block)) + return piece_skin_stream + + def get_skin_properties(section): """Receives a Bones section and returns its properties in its own variables. For any item that fails to be found, it returns None.""" @@ -333,6 +361,28 @@ def get_skin_properties(section): return skin_stream_cnt, skin_streams +def get_piece_skin_properties(section): + skin_piece_idx = None + skin_stream_cnt = None + skin_streams = [] + + for prop in section.props: + if prop[0] in ("", "#"): + pass + elif prop[0] == "Piece": + skin_piece_idx = prop[1] + elif prop[0] == "StreamCount": + skin_stream_cnt = prop[1] + else: + lprint('\nW Unknown property in "Bones" data: "%s"!', prop[0]) + for sec in section.sections: + if sec.type == "PieceSkinStream": + skin_stream = _get_piece_skin_stream(sec) + skin_streams.append(skin_stream) + + return skin_piece_idx, skin_stream_cnt, skin_streams + + def _create_piece( context, preview_model, @@ -423,7 +473,7 @@ def _create_piece( # first set normals directly to loops for loop in mesh.loops: - curr_n = _convert_utils.scs_to_blend_matrix() * Vector(mesh_normals[loop.vertex_index]) + curr_n = _convert_utils.scs_to_blend_matrix() @ Vector(mesh_normals[loop.vertex_index]) loop.normal[:] = curr_n # then we have to go trough very important step they say, @@ -440,17 +490,20 @@ def _create_piece( mesh.use_auto_smooth = True mesh.free_normals_split() + else: + # set polygons to use smooth representation only + mesh.polygons.foreach_set("use_smooth", [True] * len(mesh.polygons)) context.window_manager.progress_update(0.6) - # Add the mesh as an object into the scene with this utility module. - obj = bpy_object_utils.object_data_add(context, mesh, use_active_layer=False).object + # Create object out of mesh and link it to active layer collection. + obj = bpy.data.objects.new(mesh.name, mesh) obj.scs_props.object_identity = obj.name obj.location = (0.0, 0.0, 0.0) + context.view_layer.active_layer_collection.collection.objects.link(obj) - obj.select = True - bpy.context.scene.objects.active = obj - bpy.ops.object.shade_smooth() + obj.select_set(True) + bpy.context.view_layer.objects.active = obj context.window_manager.progress_update(0.7) @@ -471,7 +524,7 @@ def _create_piece( vg_name = str(tp_entry.variant_i).zfill(6) + _OP_consts.TerrainPoints.vg_name_prefix + str(tp_entry.node_i) if vg_name not in obj.vertex_groups: - obj.vertex_groups.new(vg_name) + obj.vertex_groups.new(name=vg_name) vertex_group = obj.vertex_groups[vg_name] vertex_group.add([vertex_i], 1.0, "REPLACE") @@ -480,15 +533,12 @@ def _create_piece( if object_skinning: if name in object_skinning: for vertex_group_name in object_skinning[name]: - vertex_group = obj.vertex_groups.new(vertex_group_name) + vertex_group = obj.vertex_groups.new(name=vertex_group_name) for vertex_i, vertex in enumerate(object_skinning[name][vertex_group_name]): weight = object_skinning[name][vertex_group_name][vertex] if weight != 0.0: - for rec in points_to_weld_list: - for vert in rec: - if vert == vertex: - vertex = rec[0] - break + if vertex in points_to_weld_list: + vertex = points_to_weld_list[vertex] vertex_group.add([vertex], weight, "ADD") else: lprint('\nE Missing skin group %r! Skipping...', name) @@ -496,36 +546,29 @@ def _create_piece( context.window_manager.progress_update(0.9) # DELETE ORPHAN VERTICES (LEFT IN THE GEOMETRY FROM SMOOTHING RECONSTRUCTION) - bpy.ops.object.mode_set(mode='EDIT') - bm = bmesh.from_edit_mesh(mesh) - - bm.verts.ensure_lookup_table() - for rec in points_to_weld_list: - for vert_i, vert in enumerate(rec): - if vert_i != 0: - bm.verts[vert].select = True - - verts = [v for v in bm.verts if v.select] - if verts: - bmesh.ops.delete(bm, geom=verts, context=1) - - # APPLYING BMESH TO MESH - # bm.to_mesh(mesh) - bmesh.update_edit_mesh(mesh, tessface=True, destructive=True) - bpy.ops.object.mode_set(mode='OBJECT') - mesh.update() - # bm.free() + if points_to_weld_list: + bm = bmesh.new() + bm.from_mesh(mesh) + + bm.verts.ensure_lookup_table() + for vert_i in points_to_weld_list.keys(): + bm.verts[vert_i].select_set(True) + + verts = [v for v in bm.verts if v.select] + if verts: + bmesh.ops.delete(bm, geom=verts, context='VERTS') + + # APPLYING BMESH TO MESH + bm.to_mesh(mesh) + bm.free() context.window_manager.progress_update(1.0) # MATERIAL if len(materials_data) > 0 and not preview_model: - override = bpy.context.copy() - override['object'] = obj - bpy.ops.object.material_slot_add(override) # Add a material slot # Assign a material to the last slot used_material = bpy.data.materials[materials_data[ob_material][0]] - obj.material_slots[len(obj.material_slots) - 1].material = used_material + obj.data.materials.append(used_material) # NOTE: we are setting texture aliases only first time to avoid duplicates etc. # So we assume that pieces which are using same material will also have same uv aliases alignement @@ -577,11 +620,10 @@ def _create_piece( back_obj.data = _mesh_utils.bm_delete_loose(back_obj.data) # finally join back object with original - bpy.ops.object.select_all(action="DESELECT") - obj.select = True - back_obj.select = True - bpy.context.scene.objects.active = obj - bpy.ops.object.join() + override = context.copy() + override["active_object"] = obj + override["selected_editable_objects"] = (obj, back_obj) + bpy.ops.object.join(override) return obj @@ -671,7 +713,8 @@ def load_pim_file(context, filepath, terrain_points_trans=None, preview_model=Fa part_count, bone_count, locator_count, - skeleton) = get_global(pim_container) + skeleton, + piece_skin_count) = get_global(pim_container) # DATA LOADING materials_data = {} @@ -679,21 +722,24 @@ def load_pim_file(context, filepath, terrain_points_trans=None, preview_model=Fa parts_data = {} locators_data = {} bones = {} - skin_data = [] - - material_i = 0 + skin_streams = [] + piece_skin_data = {} for section in pim_container: if section.type == 'Material': if scs_globals.import_pim_file: - materials_alias, materials_effect = get_material_properties(section) + material_i, materials_alias, materials_effect = get_material_properties(section) # print('\nmaterials_alias: %r' % materials_alias) # print(' materials_effect: %s' % materials_effect) + + # suport legacy format without index + if not material_i: + material_i = len(materials_data.keys()) + materials_data[material_i] = [ materials_alias, materials_effect, ] - material_i += 1 elif section.type == 'Piece': if scs_globals.import_pim_file: ob_index, ob_material, ob_vertex_cnt, ob_edge_cnt, ob_face_cnt, ob_stream_cnt = get_piece_properties(section) @@ -709,7 +755,7 @@ def load_pim_file(context, filepath, terrain_points_trans=None, preview_model=Fa mesh_uv, mesh_tuv, mesh_triangles) = _get_piece_streams(section) - points_to_weld_list = [] + points_to_weld_list = {} if mesh_normals: # print('Piece %i going to "make_posnorm_list"...' % ob_index) if scs_globals.import_use_welding: @@ -789,10 +835,16 @@ def load_pim_file(context, filepath, terrain_points_trans=None, preview_model=Fa # SKINNING elif section.type == 'Skin': # Always only one skin in current SCS game implementation. if scs_globals.import_pim_file and scs_globals.import_pis_file: - skin_stream_cnt, skin_data = get_skin_properties(section) + skin_stream_cnt, skin_streams = get_skin_properties(section) # print('\nskin_stream_cnt: %r' % skin_stream_cnt) # print('skin_data: %r\n' % str(skin_data)) + elif section.type == "PieceSkin": + if scs_globals.import_pim_file and scs_globals.import_pis_file: + skin_piece_idx, skin_stream_cnt, skin_piece_streams = get_piece_skin_properties(section) + piece_skin_data[skin_piece_idx] = skin_piece_streams + piece_skin_count -= 1 + # CREATE MATERIALS if scs_globals.import_pim_file and not preview_model: lprint("\nI ------ Creating materials: ------") @@ -809,22 +861,39 @@ def load_pim_file(context, filepath, terrain_points_trans=None, preview_model=Fa # PREPARE VERTEX GROUPS FOR SKINNING object_skinning = {} - if scs_globals.import_pim_file and scs_globals.import_pis_file and bones and skin_data: - for skin in skin_data: - for stream_i, stream in enumerate(skin): - for data in stream[5]: - # print(' ORIGIN - data: %s' % str(data)) - for rec in data['clones']: - obj = objects_data[rec[0]][1] - if obj not in object_skinning: - object_skinning[obj] = {} - vertex = rec[1] - for weight in data['weights']: - vg = bones[weight[0]] - if vg not in object_skinning[obj]: - object_skinning[obj][vg] = {} - vw = weight[1] - object_skinning[obj][vg][vertex] = vw + if scs_globals.import_pim_file and scs_globals.import_pis_file and bones: + if skin_streams: # global skinning section + for skin_stream in skin_streams: + for stream_i, stream in enumerate(skin_stream): + for data in stream[5]: # index 5 is data block, see _get_skin_stream + # print(' ORIGIN - data: %s' % str(data)) + for rec in data['clones']: + obj = objects_data[rec[0]][1] # piece name + if obj not in object_skinning: + object_skinning[obj] = {} + vertex = rec[1] + for weight in data['weights']: + vg = bones[weight[0]] + if vg not in object_skinning[obj]: + object_skinning[obj][vg] = {} + vw = weight[1] + object_skinning[obj][vg][vertex] = vw + elif piece_skin_data: # or skinning per piece + for piece_idx, piece_skin_streams in piece_skin_data.items(): + obj = objects_data[piece_idx][1] # piece name + for skin_stream in piece_skin_streams: + for stream_i, stream in enumerate(skin_stream): + for data in stream[5]: # index 5 is data block, see _get_skin_stream + # print(' ORIGIN - data: %s' % str(data)) + for vertex_idx in data['vertex_indices']: + if obj not in object_skinning: + object_skinning[obj] = {} + for weight in data['weights']: + vg = bones[weight[0]] + if vg not in object_skinning[obj]: + object_skinning[obj][vg] = {} + vw = weight[1] + object_skinning[obj][vg][vertex_idx] = vw # CREATE OBJECTS lprint("\nI ------ Creating mesh objects: -------") @@ -902,27 +971,12 @@ def load_pim_file(context, filepath, terrain_points_trans=None, preview_model=Fa if len(objects) == 0 and len(skinned_objects) == 0: return None - bases = [] - # get the bases of newly created objects for override - for base in bpy.context.scene.object_bases: - if base.object in objects: - bases.append(base) - if base.object in skinned_objects: - bases.append(base) - # get active object for joining meshes into it active_object = objects[0] if len(objects) > 0 else skinned_objects[0] - override = { - 'window': bpy.context.window, - 'screen': bpy.context.screen, - 'blend_data': bpy.context.blend_data, - 'scene': bpy.context.scene, - 'region': None, - 'area': None, - 'active_object': active_object, - 'selected_editable_bases': bases - } + override = context.copy() + override["active_object"] = active_object + override["selected_editable_objects"] = objects + skinned_objects bpy.ops.object.join(override) return active_object @@ -969,36 +1023,37 @@ def load_pim_file(context, filepath, terrain_points_trans=None, preview_model=Fa # CREATE SKELETON (ARMATURE) armature = None if scs_globals.import_pis_file and bones: - bpy.ops.object.add(type='ARMATURE', view_align=False, enter_editmode=False, location=(0.0, 0.0, 0.0), rotation=(0.0, 0.0, 0.0)) - # bpy.ops.object.armature_add(view_align=False, enter_editmode=False) + bpy.ops.object.add(type='ARMATURE') bpy.ops.object.editmode_toggle() for bone in bones: bpy.ops.armature.bone_primitive_add(name=bone) bpy.ops.object.editmode_toggle() - bpy.context.object.show_x_ray = True # bpy.context.object.data.show_names = True armature = bpy.context.object # ADD ARMATURE MODIFIERS TO SKINNED OBJECTS - if skin_data: - for obj in skinned_objects: - # print('...adding Armature modifier to %r...' % str(obj.name)) - bpy.context.scene.objects.active = obj - bpy.ops.object.modifier_add(type='ARMATURE') - arm_modifier = None - for modifier in obj.modifiers: - if modifier.type == 'ARMATURE': - arm_modifier = modifier - break - if arm_modifier: - arm_modifier.object = armature - obj.parent = armature + for obj in skinned_objects: + # print('...adding Armature modifier to %r...' % str(obj.name)) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.modifier_add(type='ARMATURE') + arm_modifier = None + for modifier in obj.modifiers: + if modifier.type == 'ARMATURE': + arm_modifier = modifier + break + if arm_modifier: + arm_modifier.object = armature + obj.parent = armature # WARNING PRINTOUTS if piece_count < 0: lprint("W More Pieces found than were declared!") if piece_count > 0: lprint("W Some Pieces not found, but were declared!") + if piece_skin_count > 0: + lprint("W More PieceSkins found than were declared!") + if piece_skin_count < 0: + lprint("W Some PieceSkins not found, but were declared!") return {'FINISHED'}, objects, locators, armature, skeleton, materials_data.values() diff --git a/addon/io_scs_tools/imp/pim_ef.py b/addon/io_scs_tools/imp/pim_ef.py index b9209da..e4a7773 100644 --- a/addon/io_scs_tools/imp/pim_ef.py +++ b/addon/io_scs_tools/imp/pim_ef.py @@ -16,13 +16,12 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2017: SCS Software +# Copyright (C) 2017-2019: SCS Software import bpy import bmesh import array from mathutils import Vector -from bpy_extras import object_utils as bpy_object_utils from io_scs_tools.consts import Operators as _OP_consts from io_scs_tools.imp.pim import get_header from io_scs_tools.imp.pim import get_global @@ -32,6 +31,7 @@ from io_scs_tools.imp.pim import get_locator_properties from io_scs_tools.imp.pim import get_bones_properties from io_scs_tools.imp.pim import get_skin_properties +from io_scs_tools.imp.pim import get_piece_skin_properties from io_scs_tools.imp.transition_structs.terrain_points import TerrainPntsTrans from io_scs_tools.internals.containers import pix as _pix_container from io_scs_tools.utils.printout import lprint @@ -291,7 +291,7 @@ def _create_piece( for poly_loop_i, loop_i in enumerate(poly.loop_indices): - curr_n = _convert_utils.scs_to_blend_matrix() * Vector(mesh_normals[poly_i][poly_loop_i]) + curr_n = _convert_utils.scs_to_blend_matrix() @ Vector(mesh_normals[poly_i][poly_loop_i]) mesh.loops[loop_i].normal[:] = curr_n # then we have to go trough very important step they say, @@ -308,22 +308,25 @@ def _create_piece( mesh.use_auto_smooth = True mesh.free_normals_split() + else: + # set polygons to use smooth representation only + mesh.polygons.foreach_set("use_smooth", [True] * len(mesh.polygons)) context.window_manager.progress_update(0.6) - # Add the mesh as an object into the scene with this utility module. - obj = bpy_object_utils.object_data_add(context, mesh, use_active_layer=False).object + # Create object out of mesh and link it to active layer collection. + obj = bpy.data.objects.new(mesh.name, mesh) obj.scs_props.object_identity = obj.name obj.location = (0.0, 0.0, 0.0) + context.view_layer.active_layer_collection.collection.objects.link(obj) - obj.select = True - bpy.context.scene.objects.active = obj - bpy.ops.object.shade_smooth() + obj.select_set(True) + bpy.context.view_layer.objects.active = obj # SCALAR LAYERS if mesh_scalars: for sca_layer_name in mesh_scalars: - vertex_group = obj.vertex_groups.new(sca_layer_name) + vertex_group = obj.vertex_groups.new(name=sca_layer_name) for val_i, val in enumerate(mesh_scalars[sca_layer_name]): val = float(val[0]) if val != 0.0: @@ -345,7 +348,7 @@ def _create_piece( vg_name = str(tp_entry.variant_i).zfill(6) + _OP_consts.TerrainPoints.vg_name_prefix + str(tp_entry.node_i) if vg_name not in obj.vertex_groups: - obj.vertex_groups.new(vg_name) + obj.vertex_groups.new(name=vg_name) vertex_group = obj.vertex_groups[vg_name] vertex_group.add([vertex_i], 1.0, "REPLACE") @@ -354,7 +357,7 @@ def _create_piece( if object_skinning: if name in object_skinning: for vertex_group_name in object_skinning[name]: - vertex_group = obj.vertex_groups.new(vertex_group_name) + vertex_group = obj.vertex_groups.new(name=vertex_group_name) for vertex_i, vertex in enumerate(object_skinning[name][vertex_group_name]): weight = object_skinning[name][vertex_group_name][vertex] if weight != 0.0: @@ -367,7 +370,6 @@ def _create_piece( bpy.ops.object.modifier_add(type='EDGE_SPLIT') bpy.context.object.modifiers["EdgeSplit"].use_edge_angle = False bpy.context.object.modifiers["EdgeSplit"].name = "ES_" + name - bpy.context.object.data.show_edge_sharp = True # MATERIALS used_mat_indices = set() @@ -458,7 +460,8 @@ def load_pim_file(context, filepath, terrain_points_trans=None, preview_model=Fa part_count, bone_count, locator_count, - skeleton) = get_global(pim_container) + skeleton, + piece_skin_count) = get_global(pim_container) # DATA LOADING materials_data = {} @@ -466,21 +469,24 @@ def load_pim_file(context, filepath, terrain_points_trans=None, preview_model=Fa parts_data = {} locators_data = {} bones = {} - skin_data = [] - - material_i = 0 + skin_streams = [] + piece_skin_data = {} for section in pim_container: if section.type == 'Material': if scs_globals.import_pim_file: - materials_alias, materials_effect = get_material_properties(section) + material_i, materials_alias, materials_effect = get_material_properties(section) # print('\nmaterials_alias: %r' % materials_alias) # print(' materials_effect: %s' % materials_effect) + + # suport legacy format without index + if not material_i: + material_i = len(materials_data.keys()) + materials_data[material_i] = [ materials_alias, materials_effect, ] - material_i += 1 elif section.type == 'Piece': if scs_globals.import_pim_file: ob_index, ob_material, ob_vertex_cnt, ob_edge_cnt, ob_face_cnt, ob_stream_cnt = get_piece_properties(section) @@ -570,10 +576,16 @@ def load_pim_file(context, filepath, terrain_points_trans=None, preview_model=Fa # SKINNING elif section.type == 'Skin': # Always only one skin in current SCS game implementation. if scs_globals.import_pim_file and scs_globals.import_pis_file: - skin_stream_cnt, skin_data = get_skin_properties(section) + skin_stream_cnt, skin_streams = get_skin_properties(section) # print('\nskin_stream_cnt: %r' % skin_stream_cnt) # print('skin_data: %r\n' % str(skin_data)) + elif section.type == "PieceSkin": + if scs_globals.import_pim_file and scs_globals.import_pis_file: + skin_piece_idx, skin_stream_cnt, skin_piece_streams = get_piece_skin_properties(section) + piece_skin_data[skin_piece_idx] = skin_piece_streams + piece_skin_count -= 1 + # CREATE MATERIALS if scs_globals.import_pim_file and not preview_model: lprint('\nI MATERIALS:') @@ -587,22 +599,39 @@ def load_pim_file(context, filepath, terrain_points_trans=None, preview_model=Fa # PREPARE VERTEX GROUPS FOR SKINNING object_skinning = {} - if scs_globals.import_pim_file and scs_globals.import_pis_file and bones and skin_data: - for skin in skin_data: - for stream_i, stream in enumerate(skin): - for data in stream[5]: - # print(' ORIGIN - data: %s' % str(data)) - for rec in data['clones']: - obj = objects_data[rec[0]][1] - if obj not in object_skinning: - object_skinning[obj] = {} - vertex = rec[1] - for weight in data['weights']: - vg = bones[weight[0]] - if vg not in object_skinning[obj]: - object_skinning[obj][vg] = {} - vw = weight[1] - object_skinning[obj][vg][vertex] = vw + if scs_globals.import_pim_file and scs_globals.import_pis_file and bones: + if skin_streams: # global skinning section + for skin_stream in skin_streams: + for stream_i, stream in enumerate(skin_stream): + for data in stream[5]: # index 5 is data block, see _get_skin_stream + # print(' ORIGIN - data: %s' % str(data)) + for rec in data['clones']: + obj = objects_data[rec[0]][1] # piece name + if obj not in object_skinning: + object_skinning[obj] = {} + vertex = rec[1] + for weight in data['weights']: + vg = bones[weight[0]] + if vg not in object_skinning[obj]: + object_skinning[obj][vg] = {} + vw = weight[1] + object_skinning[obj][vg][vertex] = vw + elif piece_skin_data: # or skinning per piece + for piece_idx, piece_skin_streams in piece_skin_data.items(): + obj = objects_data[piece_idx][1] # piece name + for skin_stream in piece_skin_streams: + for stream_i, stream in enumerate(skin_stream): + for data in stream[5]: # index 5 is data block, see _get_skin_stream + # print(' ORIGIN - data: %s' % str(data)) + for vertex_idx in data['vertex_indices']: + if obj not in object_skinning: + object_skinning[obj] = {} + for weight in data['weights']: + vg = bones[weight[0]] + if vg not in object_skinning[obj]: + object_skinning[obj][vg] = {} + vw = weight[1] + object_skinning[obj][vg][vertex_idx] = vw # CREATE OBJECTS lprint('\nI OBJECTS:') @@ -717,36 +746,37 @@ def load_pim_file(context, filepath, terrain_points_trans=None, preview_model=Fa # CREATE SKELETON (ARMATURE) armature = None if scs_globals.import_pis_file and bones: - bpy.ops.object.add(type='ARMATURE', view_align=False, enter_editmode=False, location=(0.0, 0.0, 0.0), rotation=(0.0, 0.0, 0.0)) - # bpy.ops.object.armature_add(view_align=False, enter_editmode=False) + bpy.ops.object.add(type='ARMATURE', enter_editmode=False) bpy.ops.object.editmode_toggle() for bone in bones: bpy.ops.armature.bone_primitive_add(name=bone) bpy.ops.object.editmode_toggle() - bpy.context.object.show_x_ray = True # bpy.context.object.data.show_names = True armature = bpy.context.object # ADD ARMATURE MODIFIERS TO SKINNED OBJECTS - if skin_data: - for obj in skinned_objects: - # print('...adding Armature modifier to %r...' % str(obj.name)) - bpy.context.scene.objects.active = obj - bpy.ops.object.modifier_add(type='ARMATURE') - arm_modifier = None - for modifier in obj.modifiers: - if modifier.type == 'ARMATURE': - arm_modifier = modifier - break - if arm_modifier: - arm_modifier.object = armature - obj.parent = armature + for obj in skinned_objects: + # print('...adding Armature modifier to %r...' % str(obj.name)) + bpy.context.view_layer.objects.active = obj + bpy.ops.object.modifier_add(type='ARMATURE') + arm_modifier = None + for modifier in obj.modifiers: + if modifier.type == 'ARMATURE': + arm_modifier = modifier + break + if arm_modifier: + arm_modifier.object = armature + obj.parent = armature # WARNING PRINTOUTS if piece_count < 0: - lprint('\nW More Pieces found than were declared!') + lprint('W More Pieces found than were declared!') if piece_count > 0: - lprint('\nW Some Pieces not found, but were declared!') + lprint('W Some Pieces not found, but were declared!') + if piece_skin_count > 0: + lprint("W More PieceSkins found than were declared!") + if piece_skin_count < 0: + lprint("W Some PieceSkins not found, but were declared!") return {'FINISHED'}, objects, locators, armature, skeleton, materials_data.values() diff --git a/addon/io_scs_tools/imp/pip.py b/addon/io_scs_tools/imp/pip.py index a556cf2..6da37c3 100644 --- a/addon/io_scs_tools/imp/pip.py +++ b/addon/io_scs_tools/imp/pip.py @@ -16,17 +16,17 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import math from io_scs_tools.consts import PrefabLocators as _PL_consts from io_scs_tools.internals import inventory as _inventory from io_scs_tools.internals.containers import pix as _pix_container -from io_scs_tools.internals.connections.wrappers import group as _group_connections_wrapper +from io_scs_tools.internals.connections.wrappers import collection as _connections_wrapper from io_scs_tools.utils import curve as _curve_utils from io_scs_tools.utils import name as _name_utils from io_scs_tools.utils import object as _object_utils -from io_scs_tools.utils import get_scs_globals as _get_scs_globals +from io_scs_tools.utils import get_scs_inventories as _get_scs_inventories from io_scs_tools.utils.printout import lprint @@ -627,7 +627,7 @@ def load(filepath, terrain_points_trans): :return: set of operator result and list of created locators :rtype: tuple[set, list[bpy.types.Objects]] """ - scs_globals = _get_scs_globals() + scs_inventories = _get_scs_inventories() print("\n************************************") print("** SCS PIP Importer **") @@ -901,7 +901,7 @@ def load(filepath, terrain_points_trans): signs_data[name][2], signs_data[name][3], signs_data[name][4], - scs_globals.scs_sign_model_inventory + scs_inventories.sign_models ) if loc: _print_locator_result(loc, "Sign", name) @@ -932,7 +932,7 @@ def load(filepath, terrain_points_trans): traffic_lights_data[name][4], # tsem_intervals traffic_lights_data[name][5], # tsem_cycle traffic_lights_data[name][6], # tsem_profile - scs_globals.scs_tsem_profile_inventory + scs_inventories.tsem_profiles ) if loc: _print_locator_result(loc, "Traffic Semaphore", name) @@ -1107,7 +1107,7 @@ def load(filepath, terrain_points_trans): # CREATE CONNECTIONS BETWEEN NAVIGATION POINTS for connection in conns_dict.values(): - _group_connections_wrapper.create_connection(connection["start"], connection["end"]) + _connections_wrapper.create_connection(connection["start"], connection["end"]) # COLLECT MAP POINT CONNECTIONS connections = [] @@ -1169,7 +1169,7 @@ def load(filepath, terrain_points_trans): lprint('E Map connection out of range: %s', (str(connection),)) continue - _group_connections_wrapper.create_connection(start_node, end_node) + _connections_wrapper.create_connection(start_node, end_node) # COLLECT TRIGGER POINT CONNECTIONS connections = [] @@ -1196,7 +1196,7 @@ def load(filepath, terrain_points_trans): tr_point[4], tr_point[5], tr_point[6], - scs_globals.scs_trigger_actions_inventory + scs_inventories.trigger_actions ) _print_locator_result(loc, "Trigger Point", name) @@ -1215,7 +1215,7 @@ def load(filepath, terrain_points_trans): print('E Trigger connection: %s', (str(connection),)) continue - _group_connections_wrapper.create_connection(start_node, end_node) + _connections_wrapper.create_connection(start_node, end_node) print("************************************") return {'FINISHED'}, locators diff --git a/addon/io_scs_tools/imp/pis.py b/addon/io_scs_tools/imp/pis.py index 5e86741..8342234 100644 --- a/addon/io_scs_tools/imp/pis.py +++ b/addon/io_scs_tools/imp/pis.py @@ -142,7 +142,7 @@ def load(filepath, armature, get_only=False): lprint('\nE No Armature for file "%s"!', (os.path.basename(filepath),)) return {'CANCELLED'}, None - bpy.context.scene.objects.active = armature + bpy.context.view_layer.objects.active = armature bpy.ops.object.mode_set(mode='EDIT') # CONNECTED BONES - Add information about all children... @@ -169,7 +169,7 @@ def load(filepath, armature, get_only=False): # COMPUTE BONE TRANSFORMATION matrix = bones[bone.name][1] - bone_matrix = _convert_utils.scs_to_blend_matrix() * matrix.transposed() + bone_matrix = _convert_utils.scs_to_blend_matrix() @ matrix.transposed() axis, angle = _convert_utils.mat3_to_vec_roll(bone_matrix) # print(' * %r - angle: %s' % (bone.name, angle)) @@ -194,13 +194,13 @@ def load(filepath, armature, get_only=False): if connected_bones: if len(bones[bone.name][2]) == 1: matrix = bones[bones[bone.name][2][0]][1] - bone_matrix = _convert_utils.scs_to_blend_matrix() * matrix.transposed() + bone_matrix = _convert_utils.scs_to_blend_matrix() @ matrix.transposed() armature.data.edit_bones[bone.name].tail = bone_matrix.to_translation().to_3d() * import_scale armature.data.edit_bones[bones[bone.name][2][0]].use_connect = True bpy.ops.object.mode_set(mode='OBJECT') armature.data.show_axes = True - armature.draw_type = 'WIRE' + armature.display_type = 'WIRE' # WARNING PRINTOUTS # if piece_count < 0: Print(dump_level, '\nW More Pieces found than were declared!') diff --git a/addon/io_scs_tools/imp/pit.py b/addon/io_scs_tools/imp/pit.py index a934026..b72cacf 100644 --- a/addon/io_scs_tools/imp/pit.py +++ b/addon/io_scs_tools/imp/pit.py @@ -154,7 +154,8 @@ def _get_look(section): if has_flipflake: mat_effect = mat_effect.replace(".flipflake", "") - textures.pop("texture_flakenoise", None) + if textures.pop("texture_flakenoise", None): + sec.remove_section("Texture", "Tag", r"^[\w\[\]]+:texture_flakenoise$") lprint("W Flipflake flavor detected in material %r, ignoring it!", (mat_alias,)) @@ -176,7 +177,8 @@ def _get_look(section): (mat_alias,)) if textures.pop("texture_paintjob", None) is not None: - lprint("W Needless truckpaint texture: 'texture_paintjob' in current materialconfiguration inside material %r, ignoring it!", + sec.remove_section("Texture", "Tag", r"^[\w\[\]]+:texture_paintjob$") + lprint("W Needless truckpaint texture: 'texture_paintjob' in current material configuration inside material %r, ignoring it!", (mat_alias,)) # Extra treatment for building shaders diff --git a/addon/io_scs_tools/imp/pix.py b/addon/io_scs_tools/imp/pix.py index 3fbd86a..f3cae5c 100644 --- a/addon/io_scs_tools/imp/pix.py +++ b/addon/io_scs_tools/imp/pix.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy import os @@ -158,17 +158,10 @@ def _create_scs_root_object(name, loaded_variants, loaded_looks, mats_info, obje # MAKE THE 'SCS ROOT OBJECT' NAME UNIQUE name = _name_utils.get_unique(name, bpy.data.objects) - # CREATE EMPTY OBJECT - bpy.ops.object.empty_add( - view_align=False, - location=(0.0, 0.0, 0.0), - # rotation=rot, - ) # , layers=(False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, - # False, False)) - - # MAKE A PROPER SETTINGS TO THE 'SCS Game Object' OBJECT - scs_root_object = context.active_object - scs_root_object.name = name + # CREATE EMPTY OBJECT & MAKE A PROPER SETTINGS TO THE 'SCS Game Object' OBJECT + scs_root_object = bpy.data.objects.new(name, None) + bpy.context.view_layer.active_layer_collection.collection.objects.link(scs_root_object) + bpy.context.view_layer.objects.active = scs_root_object scs_root_object.scs_props.scs_root_object_export_enabled = True scs_root_object.scs_props.empty_object_type = 'SCS_Root' @@ -176,39 +169,36 @@ def _create_scs_root_object(name, loaded_variants, loaded_looks, mats_info, obje # print('CUR.pos: %s' % str(context.space_data.cursor_location)) # PARENTING + bpy.ops.object.select_all(action='DESELECT') + if armature: # if armature is present we can specify our game object as animated scs_root_object.scs_props.scs_root_animated = "anim" # print('ARM.pos: %s' % str(armature.location)) - bpy.ops.object.select_all(action='DESELECT') - armature.select = True - bpy.ops.object.parent_set(type='OBJECT', keep_transform=False) + armature.select_set(True) armature.scs_props.parent_identity = scs_root_object.name for obj in objects: # print('OBJ.pos: %s' % str(object.location)) - bpy.ops.object.select_all(action='DESELECT') - obj.select = True - bpy.ops.object.parent_set(type='OBJECT', keep_transform=False) + obj.select_set(True) obj.scs_props.parent_identity = scs_root_object.name for obj in locators: - bpy.ops.object.select_all(action='DESELECT') - obj.select = True - bpy.ops.object.parent_set(type='OBJECT', keep_transform=False) + obj.select_set(True) obj.scs_props.parent_identity = scs_root_object.name + bpy.ops.object.parent_set(type='OBJECT', keep_transform=False) + bpy.ops.object.parent_clear(type='CLEAR_INVERSE') + # LOCATION - scs_root_object.location = context.scene.cursor_location + scs_root_object.location = context.scene.cursor.location # MAKE ONLY 'SCS GAME OBJECT' SELECTED bpy.ops.object.select_all(action='DESELECT') - for obj in bpy.data.objects: - obj.select = False - scs_root_object.select = True - context.scene.objects.active = scs_root_object + scs_root_object.select_set(True) + context.view_layer.objects.active = scs_root_object # MAKE PART RECORD part_inventory = scs_root_object.scs_object_part_inventory @@ -295,6 +285,13 @@ def _create_scs_root_object(name, loaded_variants, loaded_looks, mats_info, obje # try to find suitable preset (preset_name, preset_section) = _material_utils.find_preset(material_effect, material_textures) + # we don't support effect mismatch between looks, even if game works with it, somehow. + # most probably imported model is result of third party software, thus let user know and skip look for this material + if look_i != 0 and material_effect != mat.scs_props.mat_effect_name: + lprint("E Look %r skipped on material %r, mismatched material effect: %r but should be %r.", + (look_name, mat.name, material_effect, mat.scs_props.mat_effect_name)) + continue + # preset name is found & shader data are compatible with found preset if preset_name and _are_shader_data_compatible(preset_section, material_attributes, material_textures, mat_info[0]): @@ -330,7 +327,7 @@ def _create_scs_root_object(name, loaded_variants, loaded_looks, mats_info, obje del mat["scs_tex_aliases"] # create new look entry on root - bpy.ops.object.add_scs_look(look_name=look_name, instant_apply=False) + bpy.ops.object.scs_tools_add_look(look_name=look_name, instant_apply=False) # apply first look after everything is done scs_root_object.scs_props.active_scs_look = 0 @@ -378,6 +375,7 @@ def load(context, filepath, name_suffix="", suppress_reports=False): # IMPORT PIP -> has to be loaded before PIM because of terrain points if scs_globals.import_pip_file: + lprint("I Importing PIP ...") pip_filepath = filepath + ".pip" + name_suffix if os.path.isfile(pip_filepath): lprint('\nD PIP filepath:\n %s', (pip_filepath,)) @@ -389,6 +387,7 @@ def load(context, filepath, name_suffix="", suppress_reports=False): # IMPORT PIM if scs_globals.import_pim_file or scs_globals.import_pis_file: + lprint("I Importing PIM ...") pim_filepath = filepath + ".pim" + name_suffix if pim_filepath: if os.path.isfile(pim_filepath): @@ -414,8 +413,9 @@ def load(context, filepath, name_suffix="", suppress_reports=False): lprint('\nI No filepath provided!') # IMPORT PIT - bpy.context.scene.objects.active = None + bpy.context.view_layer.objects.active = None if scs_globals.import_pit_file: + lprint("I Importing PIT ...") pit_filepath = filepath + ".pit" + name_suffix if os.path.isfile(pit_filepath): lprint('\nD PIT filepath:\n %s', (pit_filepath,)) @@ -427,6 +427,7 @@ def load(context, filepath, name_suffix="", suppress_reports=False): # IMPORT PIC if scs_globals.import_pic_file: + lprint("I Importing PIC ...") pic_filepath = filepath + ".pic" + name_suffix if os.path.isfile(pic_filepath): lprint('\nD PIC filepath:\n %s', (pic_filepath,)) @@ -437,6 +438,7 @@ def load(context, filepath, name_suffix="", suppress_reports=False): # print('INFO - No PIC file.') # SETUP 'SCS GAME OBJECTS' + lprint("I Setup of SCS game object ...") for item in collision_locators: locators.append(item) for item in prefab_locators: @@ -458,6 +460,7 @@ def load(context, filepath, name_suffix="", suppress_reports=False): # IMPORT PIS if scs_globals.import_pis_file: + lprint("I Importing PIS ...") # pis file path is created from directory of pim file and skeleton definition inside pim header pis_filepath = os.path.dirname(filepath) + os.sep + skeleton if os.path.isfile(pis_filepath): @@ -479,6 +482,7 @@ def load(context, filepath, name_suffix="", suppress_reports=False): # IMPORT PIA if scs_globals.import_pia_file and bones: + lprint("I Importing PIAs ...") basepath = os.path.dirname(filepath) # Search for PIA files in model's directory and its subdirectiories... lprint('\nD Searching the directory for PIA files:\n %s', (basepath,)) @@ -508,19 +512,6 @@ def load(context, filepath, name_suffix="", suppress_reports=False): # fix scene objects count so it won't trigger copy cycle bpy.context.scene.scs_cached_num_objects = len(bpy.context.scene.objects) - # Turn on Textured Solid in 3D view... - for bl_screen in bpy.data.screens: - for bl_area in bl_screen.areas: - for bl_space in bl_area.spaces: - if bl_space.type == 'VIEW_3D': - bl_space.show_textured_solid = True - - # Turn on GLSL in 3D view... - bpy.context.scene.game_settings.material_mode = 'GLSL' - - # Turn on "Frame Dropping" for animation playback... - bpy.context.scene.use_frame_drop = True - # FINAL FEEDBACK bpy.context.window.cursor_modal_restore() if suppress_reports: diff --git a/addon/io_scs_tools/imp/tobj.py b/addon/io_scs_tools/imp/tobj.py index 62be113..7a4fc99 100644 --- a/addon/io_scs_tools/imp/tobj.py +++ b/addon/io_scs_tools/imp/tobj.py @@ -43,18 +43,19 @@ def get_settings_and_type(filepath, as_set=False): if container and container.map_type == "2d": - addr = "" + loaded_addr = "" for addr_value in container.addr: if addr_value == "clamp_to_edge": - addr += "0" + loaded_addr += "0" elif addr_value == "repeat": - addr += "1" + loaded_addr += "1" else: # NOTE: there are also other options for addr like: mirror etc. - # But artists are not encouraged to really used them, so use default: "clamp_to_edge". - addr += "0" + # But artists are not encouraged to really use them, so use default: "clamp_to_edge". + loaded_addr += "0" - addr = addr[::-1] + if len(loaded_addr) == 2: + addr = loaded_addr[::-1] if container.usage == "tsnormal": tsnormal = "1" diff --git a/addon/io_scs_tools/internals/callbacks/lighting_east_lock.py b/addon/io_scs_tools/internals/callbacks/lighting_east_lock.py index d4dd2f9..a5dc65c 100644 --- a/addon/io_scs_tools/internals/callbacks/lighting_east_lock.py +++ b/addon/io_scs_tools/internals/callbacks/lighting_east_lock.py @@ -23,6 +23,7 @@ from io_scs_tools.consts import SCSLigthing as _LIGHTING_consts from io_scs_tools.utils import view3d as _view3d_utils from io_scs_tools.utils import get_scs_globals as _get_scs_globals +from io_scs_tools.internals.shaders.eut2.std_node_groups import lighting_evaluator_ng as _lighting_evaluator_ng _callback = { "handle": None, @@ -31,23 +32,18 @@ } -def _get_diffuse_and_specular_lamps_from_scs_lighting(): +def _get_scs_sun(): if _LIGHTING_consts.scene_name not in bpy.data.scenes: - return None, None + return None lighting_scene = bpy.data.scenes[_LIGHTING_consts.scene_name] - if _LIGHTING_consts.diffuse_lamp_name not in lighting_scene.objects: - return None, None + if _LIGHTING_consts.sun_lamp_name not in lighting_scene.collection.objects: + return None - diffuse_lamp_obj = lighting_scene.objects[_LIGHTING_consts.diffuse_lamp_name] + sun_obj = lighting_scene.collection.objects[_LIGHTING_consts.sun_lamp_name] - if _LIGHTING_consts.specular_lamp_name not in lighting_scene.objects: - return None, None - - specular_lamp_obj = lighting_scene.objects[_LIGHTING_consts.specular_lamp_name] - - return diffuse_lamp_obj, specular_lamp_obj + return sun_obj def _lightning_east_lock_draw_callback(reset): @@ -61,8 +57,8 @@ def _lightning_east_lock_draw_callback(reset): # 1. check lighting scene integrity and region data integrity and # finish callback if everything is not up and ready - diffuse_lamp_obj, specular_lamp_obj = _get_diffuse_and_specular_lamps_from_scs_lighting() - if diffuse_lamp_obj is None or specular_lamp_obj is None: + sun_obj = _get_scs_sun() + if sun_obj is None: return if reset: @@ -79,7 +75,7 @@ def _lightning_east_lock_draw_callback(reset): # 2. on first run just remember initial rotation and end callback if _callback["initial_view_rotation"] == -1: - _callback["initial_lamp_rotation"] = diffuse_lamp_obj.rotation_euler[2] + _callback["initial_lamp_rotation"] = sun_obj.rotation_euler[2] _callback["initial_view_rotation"] = current_view_z_rot return @@ -87,27 +83,29 @@ def _lightning_east_lock_draw_callback(reset): east_rotation = current_view_z_rot - _callback["initial_view_rotation"] + _callback["initial_lamp_rotation"] # as last apply rotations to lamps - if abs(east_rotation - diffuse_lamp_obj.rotation_euler[2]) > 0.001: - diffuse_lamp_obj.rotation_euler[2] = east_rotation + if abs(east_rotation - sun_obj.rotation_euler[2]) > 0.001: + sun_obj.rotation_euler[2] = east_rotation - if abs(east_rotation - specular_lamp_obj.rotation_euler[2]) > 0.001: - specular_lamp_obj.rotation_euler[2] = east_rotation + if abs(east_rotation - sun_obj.rotation_euler[2]) > 0.001: + sun_obj.rotation_euler[2] = east_rotation + + _lighting_evaluator_ng.set_light_direction(sun_obj) def correct_lighting_east(): """Corrects lighting east depending on current rotation of directed lamps in SCS Lighting scene """ - diffuse_lamp_obj, specular_lamp_obj = _get_diffuse_and_specular_lamps_from_scs_lighting() - if diffuse_lamp_obj is None: + sun_obj = _get_scs_sun() + if sun_obj is None: return - _get_scs_globals().lighting_scene_east_direction = (diffuse_lamp_obj.rotation_euler[2] * 180 / pi + 360) % 360 + _get_scs_globals().lighting_scene_east_direction = (sun_obj.rotation_euler[2] * 180 / pi + 360) % 360 def set_lighting_east(): """Sets lighting east according to current value set in SCS Globals. - + """ _lightning_east_lock_draw_callback(True) diff --git a/addon/io_scs_tools/internals/callbacks/open_gl.py b/addon/io_scs_tools/internals/callbacks/open_gl.py index a409dfa..07958cf 100644 --- a/addon/io_scs_tools/internals/callbacks/open_gl.py +++ b/addon/io_scs_tools/internals/callbacks/open_gl.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy import console_python @@ -34,19 +34,18 @@ def enable(mode="Normal"): if _callback_handle: disable() - if mode == "Normal": - - handle_pixel = bpy.types.SpaceView3D.draw_handler_add(_gl_core.draw_custom_2d_elements, (), 'WINDOW', 'POST_PIXEL') - handle_pre_view = bpy.types.SpaceView3D.draw_handler_add(_gl_core.draw_custom_3d_elements, (mode,), 'WINDOW', 'PRE_VIEW') - handle_post_view = bpy.types.SpaceView3D.draw_handler_add(_gl_core.disable_depth_test, (), 'WINDOW', 'POST_VIEW') + # no open gl in background mode, thus ignore open gl draw callbacks + if bpy.app.background: + return - else: # X-ray mode + # fill buffers when initiated + if hasattr(bpy.context, "visible_objects"): + _gl_core.fill_buffers(bpy.context.visible_objects) - handle_pixel = bpy.types.SpaceView3D.draw_handler_add(_gl_core.draw_custom_2d_elements, (), 'WINDOW', 'POST_PIXEL') - handle_pre_view = None - handle_post_view = bpy.types.SpaceView3D.draw_handler_add(_gl_core.draw_custom_3d_elements, (mode,), 'WINDOW', 'POST_VIEW') + handle_post_pixel = bpy.types.SpaceView3D.draw_handler_add(_gl_core.draw_custom_2d_elements, (), 'WINDOW', 'POST_PIXEL') + handle_post_view = bpy.types.SpaceView3D.draw_handler_add(_gl_core.draw_custom_3d_elements, (mode,), 'WINDOW', 'POST_VIEW') - _callback_handle[:] = handle_pixel, handle_pre_view, handle_post_view + _callback_handle[:] = handle_post_pixel, handle_post_view console_python.execute.hooks.append((_view3d_utils.tag_redraw_all_view3d, ())) @@ -61,11 +60,12 @@ def disable(): console_python.execute.hooks.remove((_view3d_utils.tag_redraw_all_view3d, ())) - handle_pixel, handle_pre_view, handle_post_view = _callback_handle + handle_post_pixel, handle_post_view = _callback_handle - bpy.types.SpaceView3D.draw_handler_remove(handle_pixel, 'WINDOW') - if handle_pre_view: - bpy.types.SpaceView3D.draw_handler_remove(handle_pre_view, 'WINDOW') + bpy.types.SpaceView3D.draw_handler_remove(handle_post_pixel, 'WINDOW') bpy.types.SpaceView3D.draw_handler_remove(handle_post_view, 'WINDOW') - _callback_handle[:] = [] \ No newline at end of file + _callback_handle[:] = [] + + # clear buffers + _gl_core.fill_buffers([]) diff --git a/addon/io_scs_tools/internals/callbacks/persistent.py b/addon/io_scs_tools/internals/callbacks/persistent.py index b3e8889..30ea300 100644 --- a/addon/io_scs_tools/internals/callbacks/persistent.py +++ b/addon/io_scs_tools/internals/callbacks/persistent.py @@ -16,13 +16,15 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy from io_scs_tools.internals.persistent import initialization as _persistent_init from io_scs_tools.internals.persistent import loop_check as _persistent_loop from io_scs_tools.internals.persistent import file_save as _persistent_file_save from io_scs_tools.internals.persistent import file_load as _persistent_file_load +from io_scs_tools.internals.persistent import open_gl as _persistent_open_gl +from io_scs_tools.internals.persistent import shaders_update as _persistent_shaders_update def enable(): @@ -30,31 +32,50 @@ def enable(): Used for: 1. initialization of SCS Tools 2. checking object data (unique naming etc.) - 3. removing of custom icons datablock before saving blend file + 3. applying fixes for blend files saved with old blender tools versions + 4. removing not needed and securing needed data before file save """ - # covers: start-up, reload, enable/disable - bpy.app.handlers.scene_update_post.append(_persistent_init.initialise_scs_dict) - # covers: opening file manually from Blender - bpy.app.handlers.load_post.append(_persistent_init.initialise_scs_dict) + # covers: enable/disable + if bpy.context.preferences.is_dirty: + bpy.app.timers.register(_persistent_init.on_enable) - bpy.app.handlers.scene_update_pre.append(_persistent_loop.object_data_check) + # covers: reload, start-up, open file + bpy.app.handlers.load_post.append(_persistent_init.post_load) + + bpy.app.handlers.depsgraph_update_pre.append(_persistent_loop.object_data_check) + + # register custom drawing and shaders callbacks only in none-background mode + if not bpy.app.background: + bpy.app.handlers.depsgraph_update_post.append(_persistent_open_gl.post_depsgraph) + bpy.app.handlers.frame_change_post.append(_persistent_open_gl.post_frame_change) + bpy.app.handlers.frame_change_post.append(_persistent_shaders_update.post_frame_change) + bpy.app.handlers.undo_post.append(_persistent_open_gl.post_undo) + bpy.app.handlers.redo_post.append(_persistent_open_gl.post_redo) bpy.app.handlers.load_post.append(_persistent_file_load.post_load) bpy.app.handlers.save_pre.append(_persistent_file_save.pre_save) - bpy.app.handlers.save_post.append(_persistent_file_save.post_save) def disable(): - """Remove scene_update_post and load_post callbacks + """Remove callbacks added with enable function call. """ - if _persistent_init.initialise_scs_dict in bpy.app.handlers.load_post: - bpy.app.handlers.load_post.remove(_persistent_init.initialise_scs_dict) - if _persistent_loop.object_data_check in bpy.app.handlers.scene_update_pre: - bpy.app.handlers.scene_update_pre.remove(_persistent_loop.object_data_check) + + if _persistent_init.post_load in bpy.app.handlers.load_post: + bpy.app.handlers.load_post.remove(_persistent_init.post_load) + if _persistent_loop.object_data_check in bpy.app.handlers.depsgraph_update_pre: + bpy.app.handlers.depsgraph_update_pre.remove(_persistent_loop.object_data_check) + if _persistent_open_gl.post_depsgraph in bpy.app.handlers.depsgraph_update_post: + bpy.app.handlers.depsgraph_update_post.remove(_persistent_open_gl.post_depsgraph) + if _persistent_open_gl.post_frame_change in bpy.app.handlers.frame_change_post: + bpy.app.handlers.frame_change_post.remove(_persistent_open_gl.post_frame_change) + if _persistent_shaders_update.post_frame_change in bpy.app.handlers.frame_change_post: + bpy.app.handlers.frame_change_post.remove(_persistent_shaders_update.post_frame_change) + if _persistent_open_gl.post_undo in bpy.app.handlers.undo_post: + bpy.app.handlers.undo_post.remove(_persistent_open_gl.post_undo) + if _persistent_open_gl.post_redo in bpy.app.handlers.redo_post: + bpy.app.handlers.redo_post.remove(_persistent_open_gl.post_redo) if _persistent_file_load.post_load in bpy.app.handlers.load_post: bpy.app.handlers.load_post.remove(_persistent_file_load.post_load) if _persistent_file_save.pre_save in bpy.app.handlers.save_pre: bpy.app.handlers.save_pre.remove(_persistent_file_save.pre_save) - if _persistent_file_save.post_save in bpy.app.handlers.save_pre: - bpy.app.handlers.save_pre.remove(_persistent_file_save.post_save) diff --git a/addon/io_scs_tools/internals/connections/collector.py b/addon/io_scs_tools/internals/connections/collector.py index ea42423..e61ae41 100644 --- a/addon/io_scs_tools/internals/connections/collector.py +++ b/addon/io_scs_tools/internals/connections/collector.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy from mathutils import Vector @@ -46,10 +46,10 @@ def collect_nav_curve_data(loc0_obj, loc1_obj): nav_point_0_loc = loc0_obj.matrix_world.translation nav_point_0_rot = loc0_obj.matrix_world.to_euler('XYZ') - nav_point_0_dir = loc0_obj.matrix_world.to_quaternion() * Vector((0, 1, 0)) + nav_point_0_dir = loc0_obj.matrix_world.to_quaternion() @ Vector((0, 1, 0)) nav_point_1_loc = loc1_obj.matrix_world.translation nav_point_1_rot = loc1_obj.matrix_world.to_euler('XYZ') - nav_point_1_dir = loc1_obj.matrix_world.to_quaternion() * Vector((0, 1, 0)) + nav_point_1_dir = loc1_obj.matrix_world.to_quaternion() @ Vector((0, 1, 0)) curve_data = _curve_utils.compute_curve(nav_point_0_loc, nav_point_0_dir, nav_point_1_loc, nav_point_1_dir, curve_steps) @@ -87,10 +87,10 @@ def collect_nav_curve_data(loc0_obj, loc1_obj): curve_data['curve_color0'] = (0, 0.898, 0) curve_data['curve_color1'] = (0, 0.898, 0) - curve_data['locrot_0'] = (nav_point_0_loc.x, nav_point_0_loc.y, nav_point_0_loc.z, nav_point_0_rot.x, - nav_point_0_rot.y, nav_point_0_rot.z) - curve_data['locrot_1'] = (nav_point_1_loc.x, nav_point_1_loc.y, nav_point_1_loc.z, nav_point_1_rot.x, - nav_point_1_rot.y, nav_point_1_rot.z) + curve_data['locrot_0'] = (nav_point_0_loc.x, nav_point_0_loc.y, nav_point_0_loc.z, + nav_point_0_rot.x, nav_point_0_rot.y, nav_point_0_rot.z) + curve_data['locrot_1'] = (nav_point_1_loc.x, nav_point_1_loc.y, nav_point_1_loc.z, + nav_point_1_rot.x, nav_point_1_rot.y, nav_point_1_rot.z) return curve_data diff --git a/addon/io_scs_tools/internals/connections/core.py b/addon/io_scs_tools/internals/connections/core.py index 810b428..82bcab1 100644 --- a/addon/io_scs_tools/internals/connections/core.py +++ b/addon/io_scs_tools/internals/connections/core.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy import hashlib @@ -58,7 +58,7 @@ def init(data_block): if MAIN_DICT in data_block: lprint("W Custom property on datablock %s will be reset to init value and used for connections storage.", (str(data_block),)) else: - lprint("D Just to inform that init was invoked!") + lprint("D Just to inform that connections init was invoked!") data_block[MAIN_DICT] = { CACHE: { @@ -144,7 +144,7 @@ def update_for_redraw(data_block, selection): # create a complete list of objects to recalculate connections if selection is None: - objects_to_check = bpy.context.selected_objects # this is entry point for selection because only selected objects can be transformed + objects_to_check = bpy.context.view_layer.objects.selected.values() # get selection because only selected objects can be transformed else: objects_to_check = selection @@ -157,7 +157,7 @@ def update_for_redraw(data_block, selection): # if root is selected all of children should be checked too for child_obj in obj.children: - if len(child_obj.children) > 0: + if child_obj.children: objects_to_check.append(child_obj) diff --git a/addon/io_scs_tools/internals/connections/wrappers/group.py b/addon/io_scs_tools/internals/connections/wrappers/collection.py similarity index 78% rename from addon/io_scs_tools/internals/connections/wrappers/group.py rename to addon/io_scs_tools/internals/connections/wrappers/collection.py index 8dc9092..58750fd 100644 --- a/addon/io_scs_tools/internals/connections/wrappers/group.py +++ b/addon/io_scs_tools/internals/connections/wrappers/collection.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy from io_scs_tools.consts import ConnectionsStorage as _CS_consts @@ -25,7 +25,7 @@ from io_scs_tools.utils import get_scs_globals as _get_scs_globals from io_scs_tools.utils.printout import lprint as lprint -_GROUP_NAME = _CS_consts.group_name +_COLLECTION_NAME = _CS_consts.collection_name _DATA_UP_TO_DATE = "up_to_date" _CACHE = {_DATA_UP_TO_DATE: False} # saving if connections data are up to date @@ -62,16 +62,16 @@ def init(): Called if starting Blender or if all connections should be deleted """ - # create proper group if needed - if _GROUP_NAME not in bpy.data.groups: - bpy.data.groups.new(_GROUP_NAME) + # create proper collection if needed + if _COLLECTION_NAME not in bpy.data.collections: + bpy.data.collections.new(_COLLECTION_NAME) # just make sure that data block won't get deleted after several savings of blend file - bpy.data.groups[_GROUP_NAME].use_fake_user = True + bpy.data.collections[_COLLECTION_NAME].use_fake_user = True # check if connections data block already exists then don't initilaze it - if not _core.exists(bpy.data.groups[_GROUP_NAME]): - _core.init(bpy.data.groups[_GROUP_NAME]) + if not _core.exists(bpy.data.collections[_COLLECTION_NAME]): + _core.init(bpy.data.collections[_COLLECTION_NAME]) def create_connection(loc0_obj, loc1_obj): @@ -91,7 +91,7 @@ def create_connection(loc0_obj, loc1_obj): :return: True if connection creation is successful otherwise False :rtype: bool """ - return _core.create_connection(bpy.data.groups[_GROUP_NAME], loc0_obj, loc1_obj) + return _core.create_connection(bpy.data.collections[_COLLECTION_NAME], loc0_obj, loc1_obj) def delete_connection(loc0_name, loc1_name): @@ -104,11 +104,11 @@ def delete_connection(loc0_name, loc1_name): :return: True if connection was deleted; False otherwise :rtype: bool """ - return _core.delete_connection(bpy.data.groups[_GROUP_NAME], loc0_name, loc1_name) + return _core.delete_connection(bpy.data.collections[_COLLECTION_NAME], loc0_name, loc1_name) def has_connection(loc0_obj, loc1_obj): - result = _core.get_connection(bpy.data.groups[_GROUP_NAME], loc0_obj.name, loc1_obj.name) + result = _core.get_connection(bpy.data.collections[_COLLECTION_NAME], loc0_obj.name, loc1_obj.name) return result is not None @@ -121,7 +121,7 @@ def delete_locator(loc_name): :return: True if locator and references were successfully deleted; False otherwise :rtype: bool """ - return _core.delete_locator(bpy.data.groups[_GROUP_NAME], loc_name) + return _core.delete_locator(bpy.data.collections[_COLLECTION_NAME], loc_name) def rename_locator(old_name, new_name): @@ -135,7 +135,7 @@ def rename_locator(old_name, new_name): :return: True if renaming was successfully done; False if new name is already taken :rtype: bool """ - return _core.rename_locator(bpy.data.groups[_GROUP_NAME], old_name, new_name) + return _core.rename_locator(bpy.data.collections[_COLLECTION_NAME], old_name, new_name) def copy_check(old_objs, new_objs): @@ -148,8 +148,7 @@ def copy_check(old_objs, new_objs): :param new_objs: new Blender objects :type new_objs: list of bpy.types.Object """ - - new_conns_count = _core.copy_connections(bpy.data.groups[_GROUP_NAME], old_objs, new_objs) + new_conns_count = _core.copy_connections(bpy.data.collections[_COLLECTION_NAME], old_objs, new_objs) lprint("D Copy connection opearation created: %s new connections", (new_conns_count,)) @@ -157,7 +156,7 @@ def cleanup_on_export(): """Cleanups connections in the case that something was changed without making another redraw call to 3D view. """ - _core.cleanup_check(bpy.data.groups[_GROUP_NAME]) + _core.cleanup_check(bpy.data.collections[_COLLECTION_NAME]) def get_neighbours(loc_obj): @@ -168,8 +167,7 @@ def get_neighbours(loc_obj): :return: list of neighbour locator names :rtype: list """ - - conns_entries = _core.get_connections(bpy.data.groups[_GROUP_NAME], loc_obj.name) + conns_entries = _core.get_connections(bpy.data.collections[_COLLECTION_NAME], loc_obj.name) neighbours = [] @@ -198,9 +196,8 @@ def get_curves(np_locators): :return: dictionary of curves within given locators with given keys structure; :rtype: dict[int, ConnEntry] """ - - connections = bpy.data.groups[_GROUP_NAME][_core.MAIN_DICT][_core.REFS][_core.CONNECTIONS][_core.ENTRIES] - locators = bpy.data.groups[_GROUP_NAME][_core.MAIN_DICT][_core.REFS][_core.LOCATORS] + connections = bpy.data.collections[_COLLECTION_NAME][_core.MAIN_DICT][_core.REFS][_core.CONNECTIONS][_core.ENTRIES] + locators = bpy.data.collections[_COLLECTION_NAME][_core.MAIN_DICT][_core.REFS][_core.LOCATORS] # filter out all Navigation Point locators np_locs_names = {} @@ -209,7 +206,7 @@ def get_curves(np_locators): np_locs_names[loc.name] = 1 # gather all the connections of navigation points - conns_keys_dict = _core.gather_connections_upon_selected(bpy.data.groups[_GROUP_NAME], np_locs_names) + conns_keys_dict = _core.gather_connections_upon_selected(bpy.data.collections[_COLLECTION_NAME], np_locs_names) curves = {} i = 0 @@ -252,47 +249,33 @@ def force_recalculate(object_list): :param object_list: list of the objects for which connection update should be triggered :type object_list: list of bpy.types.Object """ - _core.update_for_redraw(bpy.data.groups[_GROUP_NAME], object_list) + _core.update_for_redraw(bpy.data.collections[_COLLECTION_NAME], object_list) -def switch_to_update(): - """Switches state of connections drawing to update which will prevent connections to be drawn. - NOTE: this will effect drawing only if optimized drawing is switched on +def invalidate(): + """Invalidates cached connectinos data, which will prevent connections to be drawn. """ _CACHE[_DATA_UP_TO_DATE] = False -def switch_to_stall(): - """Switches state of connections drawing to stalling and will trigger check for connections recalculations - if they are not yet up to date. - NOTE: this will effect drawing only if optimized drawing is switched on +def update_for_redraw(): + """Recalculate connections, if they are not yet up to date. """ - # if initialization wasn't triggered yet, then skip switching to stall and wait until it gets initialised - if _GROUP_NAME not in bpy.data.groups: + if _COLLECTION_NAME not in bpy.data.collections: return # if data was marked as updated if not _CACHE[_DATA_UP_TO_DATE]: - # recalculate curves and lines whos locators were visiblly changed and force redraw - _core.update_for_redraw(bpy.data.groups[_GROUP_NAME], None) - bpy.context.scene.unit_settings.system = bpy.context.scene.unit_settings.system + # recalculate curves and lines whos locators were visibly changed + _core.update_for_redraw(bpy.data.collections[_COLLECTION_NAME], None) _CACHE[_DATA_UP_TO_DATE] = True -def _execute_draw(optimized_drawing): - """Checks if connections should be drawn. - - :param optimized_drawing: optimized connections drawing property from SCS globals - :type optimized_drawing: bool +def ready_for_draw(): + """Checks if connections cache is ready for draw. """ - - # exclude optimization drawing - if not optimized_drawing: - _core.update_for_redraw(bpy.data.groups[_GROUP_NAME], None) - return True - return _CACHE[_DATA_UP_TO_DATE] @@ -303,20 +286,20 @@ def draw(visible_loc_names): :param visible_loc_names: dictionary of visible prefab locators names :type visible_loc_names: dict """ - scs_globals = _get_scs_globals() - if _execute_draw(scs_globals.optimized_connections_drawing): + if ready_for_draw(): - connections = bpy.data.groups[_GROUP_NAME][_core.MAIN_DICT][_core.REFS][_core.CONNECTIONS][_core.ENTRIES] + connections = bpy.data.collections[_COLLECTION_NAME][_core.MAIN_DICT][_core.REFS][_core.CONNECTIONS][_core.ENTRIES] # gets visible connections and draw them - conns_to_draw = _core.gather_connections_upon_selected(bpy.data.groups[_GROUP_NAME], visible_loc_names) + conns_to_draw = _core.gather_connections_upon_selected(bpy.data.collections[_COLLECTION_NAME], visible_loc_names) for conn_key in conns_to_draw.keys(): conn_entry = connections[conn_key] locator_type = bpy.data.objects[conn_entry[_core.IN]].scs_props.locator_prefab_type + if locator_type == "Navigation Point": _gl_primitive.draw_shape_curve(conn_entry[_core.DATA], not conn_entry[_core.VALID], scs_globals) else: diff --git a/addon/io_scs_tools/internals/containers/config.py b/addon/io_scs_tools/internals/containers/config.py index ada5865..12b0a4b 100644 --- a/addon/io_scs_tools/internals/containers/config.py +++ b/addon/io_scs_tools/internals/containers/config.py @@ -16,21 +16,801 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy import os import pickle +import tempfile +from hashlib import sha256 +from time import time +from io_scs_tools.consts import Icons as _ICONS_consts +from io_scs_tools.consts import Cache as _CACHE_consts from io_scs_tools.utils.printout import lprint from io_scs_tools.utils import path as _path_utils -from io_scs_tools.utils import property as _property_utils +from io_scs_tools.utils import view3d as _view3d_utils from io_scs_tools.utils import get_scs_globals as _get_scs_globals +from io_scs_tools.utils import get_scs_inventories as _get_scs_inventories +from io_scs_tools.utils import load_scs_globals_from_blend as _load_scs_globals_from_blend from io_scs_tools.utils.info import get_combined_ver_str +from io_scs_tools.utils.property import get_default from io_scs_tools.internals import shader_presets as _shader_presets from io_scs_tools.internals.containers import pix as _pix from io_scs_tools.internals.containers import sii as _sii from io_scs_tools.internals.structure import SectionData as _SectionData -from io_scs_tools.operators.world import SCSPathsInitialization as _SCSPathsInitialization + + +class _PathsCache: + """Class for caching loaded inventory paths, preventing multiple inventory reload for same paths. + + The thing is that most of the time users are operating on same projects and same configurations, + which means they will be loading same inventories all over again. Now as our inventories are saved + globally in the addon we can skip their reload. + + So this serves as high level cache which only saves paths last modified times for each inventory type and + gets invalid as soon as one of the paths is missing or last modified time of at least one file has changed. + """ + __cache = {} + """Main cache static variable, saving all cached entries.""" + + def __init__(self, inventory_type): + """Construct cache instance, for given inventory type. + + :param inventory_type: uniquse string representing invetory type for which cache will be used. + :type inventory_type: str + """ + self.__dict_key = inventory_type + + def is_valid(self, paths): + """Check if given paths are cached inside the dictionary for given paths type. + + :param paths: file paths to be checked against cache + :type paths: collections.Sequence + :return: True if all given paths are cached and thier last modified date is recent enough; False otherwise; + :rtype: bool + """ + + # no path is provided, also treat as invalid + if not paths: + return False + + # empty paths iterable, invalid + paths_count = len(paths) + if paths_count <= 0: + return False + + # no entries yet, invalid + if self.__dict_key not in _PathsCache.__cache: + return False + + # different number of paths, invalid because + # if more/less paths is provided, than we cached alredy, + # we have potentially different entries in library + if paths_count != len(_PathsCache.__cache[self.__dict_key]): + return False + + # finally check cache for already loaded paths + for path in paths: + # path not yet cached, invalid + if path not in _PathsCache.__cache[self.__dict_key]: + break + # last modified time is more recent than the cached one, invalid + if os.path.getmtime(path) > _PathsCache.__cache[self.__dict_key][path]: + break + else: + return True + + return False + + def clear(self): + """Clear cached paths. + """ + if self.__dict_key in _PathsCache.__cache: + _PathsCache.__cache[self.__dict_key].clear() + + def add_path(self, path): + """Adds a path last modified time into the cache. If entry exists, it will be overwritten. + + :param path: file path to be saved to cache + :type path: str + """ + + if self.__dict_key not in _PathsCache.__cache: + _PathsCache.__cache[self.__dict_key] = {} + + if os.path.isfile(path): + _PathsCache.__cache[self.__dict_key][path] = os.path.getmtime(path) + + +class _ContainersCache: + """Class for caching SII containers using pickle module and temporary directory, speeding up + usage of same containers all over again. + + Once paths cache (:class: _PathsCache) fails (user restarts blender, opens multiple instances), + this low level cache kicks in, as inventories in blend data are empty, we need to refill them, + thus load/dump pickles, which is way faster then loading SIIs from scratch. + + During reload of inventory each opened container is dumped into temporary directory + and next time this path is requested, cache first recovers container from temp directory or + if not found loads SII file from scratch. + """ + + # TODO: Rather then tmp dir, we should use cache directory, which currently is not implemented in blender API, so either: + # 1. use "user_cache_dir" from https://developer.blender.org/diffusion/BCA/browse/master/blender_cloud/appdirs.py + # 2. wait for Blender to have implemented: https://developer.blender.org/T47684 + __tmp_dir = os.path.join(tempfile.gettempdir(), _CACHE_consts.dir_name) + """Temporary direcrtory to which we dump loaded SII container for later reuse.""" + + @staticmethod + def __hashed_path(path): + """Returns hashed path. + + :param path: absolute path of the container + :type path: str + :return: hash of concatenated last modified date string and full path name; empty string if path is non-existing + :rtype: str + """ + + if not os.path.exists(path): + return "" + + str_to_hash = str(os.path.getmtime(path)) + _path_utils.full_norm(path) + file_name_hash = sha256(str_to_hash.encode('utf-8')).hexdigest() + + return os.path.join(_ContainersCache.__tmp_dir, file_name_hash) + + @staticmethod + def __retrieve_from_cache(path): + """Retrieves container that is cached for given path. None if not entry is found. + + :param path: absolute path to the continer we want to retrieve + :type path: str + :return: list of SII Units if parsing succeded; otherwise None + :rtype: list[io_scs_tools.internals.structure.UnitData] | None + """ + + hashed_path = _ContainersCache.__hashed_path(path) + + hashed_file_exists = os.path.isfile(hashed_path) + original_file_exists = os.path.isfile(path) + + container = None + if original_file_exists and hashed_file_exists: # both need to exist to retrieve + with open(hashed_path, mode='rb') as file: + container = pickle.load(file) + elif hashed_file_exists: # remove hashed entry if original file was deleted + _path_utils.rmtree(hashed_path) + + return container + + @staticmethod + def __cache_it(path, container): + """Caches given container for given path. If container is None nothing will be cached. + + :param path: absolute path of the container + :type path: str + :param container: list of SII Units + :type container: list[io_scs_tools.internals.structure.UnitData] | None + """ + + if not container: + return + + tmp_dir = _ContainersCache.__tmp_dir + + # check temp directory max size and do a cleanup if cache exceeded it + if _path_utils.get_tree_size(tmp_dir) >= _CACHE_consts.max_size: + _path_utils.rmtree(tmp_dir) + + # ensure temp directory + os.makedirs(tmp_dir, exist_ok=True) + + # dump the container into hashed path + with open(_ContainersCache.__hashed_path(path), mode='wb') as file: + pickle.dump(container, file) + + @staticmethod + def retrieve(path): + """Retrieve SII container for given path. + + If item is not yet in cache SII file is accessed and read directly, then cached and returned. + + :param path: absolute path of the container + :type path: str + :return: list of SII Units if parsing succeded; otherwise None + :rtype: list[io_scs_tools.internals.structure.UnitData] | None + """ + + # try to retrieve cached container + cached_container = _ContainersCache.__retrieve_from_cache(path) + if cached_container: + return cached_container + + # otherwise get fresh data + sii_container = _sii.get_data_from_file(path) + + # and cache it before return + _ContainersCache.__cache_it(path, sii_container) + + return sii_container + + +class _ConfigSection: + """Class implementing common functionalities of all config sections.""" + + def __init__(self, section_type): + self.type = section_type + self.props = {} + + def apply_settings(self, settings_to_apply=None): + """Applies settings of this config section to scs globals in blend data. + + :param settings_to_apply: set of properties that should be applied + :type settings_to_apply: set[str] | None + """ + scs_globals = _get_scs_globals() + + for prop_name, prop_data in self.props.items(): + (cast_type, value, attr) = prop_data + + # if filter set is provided, ignore all props not included in it. + if settings_to_apply and prop_name not in settings_to_apply: + continue + + # ignore props without attribute info + if not attr: + continue + + setattr(scs_globals, attr, value) + + def fill_from_pix_section(self, section): + """Fill config section with data from given pix section. + + NOTE: Unrecognized properties are ignored! + + :param section: section from which properties should be taken + :type section: io_scs_tools.internals.structure.SectionData + :return: fill status: True if successfully filled, False otherwise + :rtype: bool + """ + if section.type != self.type: + return False + + for prop_name, prop_value in section.props: + if prop_name in self.props: + (cast_type, old_value, attr) = self.props[prop_name] + self.props[prop_name] = (cast_type, cast_type(prop_value), attr) + + return True + + def get_as_pix_section(self): + """Gets header data represented with SectionData structure class. + + :return: packed header as section data + :rtype: io_scs_tools.internals.structure.SectionData + """ + + section = _SectionData(self.type) + + for prop_name, prop_data in self.props.items(): + (cast_type, value, attr) = prop_data + section.props.append((prop_name, cast_type(value))) + + return section + + +class Header(_ConfigSection): + """Class defining main configuration settings.""" + + def __init__(self): + """Constructor.""" + super().__init__("Header") + + scs_globals = _get_scs_globals() + + self.props = { + "FormatVersion": (int, 1, None), + "Source": (str, get_combined_ver_str(), None), + "Type": (str, "Configuration", None), + "Note": (str, "User settings of SCS Blender Tools", None), + # "Author": (str, bpy.context.user_preferences.system.author, None), + "ConfigStoragePlace": (str, get_default(scs_globals, 'config_storage_place'), 'config_storage_place'), + "DumpLevel": (str, get_default(scs_globals, 'dump_level'), 'dump_level') + } + + def apply_config_storage_place(self): + """Applies config storage place.""" + self.apply_settings(settings_to_apply=set(["ConfigStoragePlace", ])) + + def is_valid(self): + """Tells if currently loaded header is valid or no. + + :return: True if format version and type are correct; False otherwise + :rtype: bool + """ + return self.props["FormatVersion"][1] == 1 and self.props["Type"][1] == "Configuration" + + def is_applicable(self): + """Tells if settings can be applied to blender data. + + :return: True if header is valid and config storage place is set to config; False otherwise + :rtype: bool + """ + return self.is_valid() and self.props["ConfigStoragePlace"][1] == "ConfigFile" + + +class Paths(_ConfigSection): + """Class for path settings and handling of their reload.""" + + def __init__(self): + """Constructor.""" + super().__init__("Paths") + + scs_globals = _get_scs_globals() + + self.props = { + "ProjectPath": (str, get_default(scs_globals, 'scs_project_path'), None), + "ShaderPresetsFilePath": (str, get_default(scs_globals, 'shader_presets_filepath'), None), + "ShaderPresetsUseCustom": (int, get_default(scs_globals, 'shader_presets_use_custom'), 'shader_presets_use_custom'), + "TriggerActionsRelFilePath": (str, get_default(scs_globals, 'trigger_actions_rel_path'), None), + "TriggerActionsUseInfixed": (int, get_default(scs_globals, 'trigger_actions_use_infixed'), 'trigger_actions_use_infixed'), + "SignRelFilePath": (str, get_default(scs_globals, 'sign_library_rel_path'), None), + "SignUseInfixed": (int, get_default(scs_globals, 'sign_library_use_infixed'), 'sign_library_use_infixed'), + "TSemProfileRelFilePath": (str, get_default(scs_globals, 'tsem_library_rel_path'), None), + "TSemProfileUseInfixed": (int, get_default(scs_globals, 'tsem_library_use_infixed'), 'tsem_library_use_infixed'), + "TrafficRulesRelFilePath": (str, get_default(scs_globals, 'traffic_rules_library_rel_path'), None), + "TrafficRulesUseInfixed": (int, get_default(scs_globals, 'traffic_rules_library_use_infixed'), 'traffic_rules_library_use_infixed'), + "HookupRelDirPath": (str, get_default(scs_globals, 'hookup_library_rel_path'), None), + "MatSubsRelFilePath": (str, get_default(scs_globals, 'matsubs_library_rel_path'), None), + "SunProfilesFilePath": (str, get_default(scs_globals, 'sun_profiles_lib_path'), None), + "ConvertersPath": (str, get_default(scs_globals, 'conv_hlpr_converters_path'), None), + "UseAlternativeBases": (int, get_default(scs_globals, 'use_alternative_bases'), 'use_alternative_bases'), + + } + + def apply_paths(self, load_internal=False): + """Apllies paths settings from config or from blend file. Settings are applied directly or via asynchronous paths init operator. + + NOTE: Applying paths is crucial for libraries/inventories (they are reloaded/initiated in property update functions). + """ + scs_globals = _get_scs_globals() + + if load_internal: + scs_project_path = scs_globals.scs_project_path + shader_presets_filepath = scs_globals.shader_presets_filepath + trigger_actions_rel_path = scs_globals.trigger_actions_rel_path + sign_library_rel_path = scs_globals.sign_library_rel_path + tsem_library_rel_path = scs_globals.tsem_library_rel_path + traffic_rules_library_rel_path = scs_globals.traffic_rules_library_rel_path + hookup_library_rel_path = scs_globals.hookup_library_rel_path + matsubs_library_rel_path = scs_globals.matsubs_library_rel_path + sun_profiles_library_path = scs_globals.sun_profiles_lib_path + conv_hlpr_converters_path = scs_globals.conv_hlpr_converters_path + else: + scs_project_path = self.props["ProjectPath"][1] + shader_presets_filepath = self.props["ShaderPresetsFilePath"][1] + trigger_actions_rel_path = self.props["TriggerActionsRelFilePath"][1] + sign_library_rel_path = self.props["SignRelFilePath"][1] + tsem_library_rel_path = self.props["TSemProfileRelFilePath"][1] + traffic_rules_library_rel_path = self.props["TrafficRulesRelFilePath"][1] + hookup_library_rel_path = self.props["HookupRelDirPath"][1] + matsubs_library_rel_path = self.props["MatSubsRelFilePath"][1] + sun_profiles_library_path = self.props["SunProfilesFilePath"][1] + conv_hlpr_converters_path = self.props["ConvertersPath"][1] + + if bpy.app.background: # without UI apply libraries directly as we want our tools to be to be ready for any scripts execution + scs_globals.scs_project_path = scs_project_path + scs_globals.shader_presets_filepath = shader_presets_filepath + scs_globals.trigger_actions_rel_path = trigger_actions_rel_path + scs_globals.sign_library_rel_path = sign_library_rel_path + scs_globals.tsem_library_rel_path = tsem_library_rel_path + scs_globals.traffic_rules_library_rel_path = traffic_rules_library_rel_path + scs_globals.hookup_library_rel_path = hookup_library_rel_path + scs_globals.matsubs_library_rel_path = matsubs_library_rel_path + scs_globals.sun_profiles_lib_path = sun_profiles_library_path + scs_globals.conv_hlpr_converters_path = conv_hlpr_converters_path + + else: # if blender is started normally use asynchronous operator to reload libraries + + AsyncPathsInit.execute([ + {"name": "project base path", "attr": "scs_project_path", "path": scs_project_path}, + {"name": "shader presets", "attr": "shader_presets_filepath", "path": shader_presets_filepath}, + {"name": "trigger actions library", "attr": "trigger_actions_rel_path", "path": trigger_actions_rel_path}, + {"name": "sign library", "attr": "sign_library_rel_path", "path": sign_library_rel_path}, + {"name": "traffic semaphore library", "attr": "tsem_library_rel_path", "path": tsem_library_rel_path}, + {"name": "traffic rules library", "attr": "traffic_rules_library_rel_path", "path": traffic_rules_library_rel_path}, + {"name": "hookups library", "attr": "hookup_library_rel_path", "path": hookup_library_rel_path}, + {"name": "material substance library", "attr": "matsubs_library_rel_path", "path": matsubs_library_rel_path}, + {"name": "sun profiles library", "attr": "sun_profiles_lib_path", "path": sun_profiles_library_path}, + {"name": "converters file path", "attr": "conv_hlpr_converters_path", "path": conv_hlpr_converters_path}, + ]) + + +class Import(_ConfigSection): + """Class for import settings.""" + + def __init__(self): + """Constructor.""" + super().__init__("Import") + + scs_globals = _get_scs_globals() + + self.props = { + "ImportScale": (float, get_default(scs_globals, 'import_scale'), 'import_scale'), + "PreservePathForExport": (int, get_default(scs_globals, 'import_preserve_path_for_export'), 'import_preserve_path_for_export'), + "ImportPimFile": (int, get_default(scs_globals, 'import_pim_file'), 'import_pim_file'), + "UseWelding": (int, get_default(scs_globals, 'import_use_welding'), 'import_use_welding'), + "WeldingPrecision": (int, get_default(scs_globals, 'import_welding_precision'), 'import_welding_precision'), + "UseNormals": (int, get_default(scs_globals, 'import_use_normals'), 'import_use_normals'), + "ImportPitFile": (int, get_default(scs_globals, 'import_pit_file'), 'import_pit_file'), + "LoadTextures": (int, get_default(scs_globals, 'import_load_textures'), 'import_load_textures'), + "ImportPicFile": (int, get_default(scs_globals, 'import_pic_file'), 'import_pic_file'), + "ImportPipFile": (int, get_default(scs_globals, 'import_pip_file'), 'import_pip_file'), + "ImportPisFile": (int, get_default(scs_globals, 'import_pis_file'), 'import_pis_file'), + "ConnectedBones": (int, get_default(scs_globals, 'import_connected_bones'), 'import_connected_bones'), + "BoneImportScale": (float, get_default(scs_globals, 'import_bone_scale'), 'import_bone_scale'), + "ImportPiaFile": (int, get_default(scs_globals, 'import_pia_file'), 'import_pia_file'), + "IncludeSubdirsForPia": (int, get_default(scs_globals, 'import_include_subdirs_for_pia'), 'import_include_subdirs_for_pia'), + } + + +class Export(_ConfigSection): + """Class for export settings.""" + + def __init__(self): + """Constructor.""" + super().__init__("Export") + + scs_globals = _get_scs_globals() + + self.props = { + "ExportScale": (float, get_default(scs_globals, 'export_scale'), 'export_scale'), + "ApplyModifiers": (int, get_default(scs_globals, 'export_apply_modifiers'), 'export_apply_modifiers'), + "ExcludeEdgesplit": (int, get_default(scs_globals, 'export_exclude_edgesplit'), 'export_exclude_edgesplit'), + "IncludeEdgesplit": (int, get_default(scs_globals, 'export_include_edgesplit'), 'export_include_edgesplit'), + "ActiveUVOnly": (int, get_default(scs_globals, 'export_active_uv_only'), 'export_active_uv_only'), + "ExportVertexGroups": (int, get_default(scs_globals, 'export_vertex_groups'), 'export_vertex_groups'), + "ExportVertexColor": (int, get_default(scs_globals, 'export_vertex_color'), 'export_vertex_color'), + "ExportVertexColorType": (str, get_default(scs_globals, 'export_vertex_color_type'), 'export_vertex_color_type'), + "ExportVertexColorType7": (str, get_default(scs_globals, 'export_vertex_color_type_7'), 'export_vertex_color_type_7'), + # "ExportAnimFile": (int, get_default(scs_globals, 'export_anim_file'), 'export_anim_file'), + "ExportPimFile": (int, get_default(scs_globals, 'export_pim_file'), 'export_pim_file'), + "OutputType": (str, get_default(scs_globals, 'export_output_type'), 'export_output_type'), + "ExportPitFile": (int, get_default(scs_globals, 'export_pit_file'), 'export_pit_file'), + "ExportPicFile": (int, get_default(scs_globals, 'export_pic_file'), 'export_pic_file'), + "ExportPipFile": (int, get_default(scs_globals, 'export_pip_file'), 'export_pip_file'), + "SignExport": (int, get_default(scs_globals, 'export_write_signature'), 'export_write_signature'), + } + + +class GlobalDisplay(_ConfigSection): + """Class for global display settings.""" + + def __init__(self): + """Constructor.""" + super().__init__("GlobalDisplay") + + scs_globals = _get_scs_globals() + self.props = { + "DisplayLocators": (int, get_default(scs_globals, 'display_locators'), 'display_locators'), + "DisplayPreviewModels": (int, get_default(scs_globals, 'show_preview_models'), 'show_preview_models'), + "LocatorSize": (float, get_default(scs_globals, 'locator_size'), 'locator_size'), + "LocatorEmptySize": (float, get_default(scs_globals, 'locator_empty_size'), 'locator_empty_size'), + "DisplayConnections": (int, get_default(scs_globals, 'display_connections'), 'display_connections'), + "OptimizedConnsDrawing": (int, get_default(scs_globals, 'optimized_connections_drawing'), 'optimized_connections_drawing'), + "CurveSegments": (int, get_default(scs_globals, 'curve_segments'), 'curve_segments'), + "DisplayTextInfo": (str, get_default(scs_globals, 'display_info'), 'display_info'), + "IconTheme": (str, _ICONS_consts.default_icon_theme, 'icon_theme'), + } + + +class GlobalColors(_ConfigSection): + """Class for global color settings.""" + + def __init__(self): + """Constructor.""" + super().__init__("GlobalColors") + + scs_globals = _get_scs_globals() + + self.props = { + "PrefabLocatorsWire": (tuple, get_default(scs_globals, 'locator_prefab_wire_color'), 'locator_prefab_wire_color'), + "ModelLocatorsWire": (tuple, get_default(scs_globals, 'locator_model_wire_color'), 'locator_model_wire_color'), + "ColliderLocatorsWire": (tuple, get_default(scs_globals, 'locator_coll_wire_color'), 'locator_coll_wire_color'), + "ColliderLocatorsFace": (tuple, get_default(scs_globals, 'locator_coll_face_color'), 'locator_coll_face_color'), + "NavigationCurveBase": (tuple, get_default(scs_globals, 'np_connection_base_color'), 'np_connection_base_color'), + "MapLineBase": (tuple, get_default(scs_globals, 'mp_connection_base_color'), 'mp_connection_base_color'), + "TriggerLineBase": (tuple, get_default(scs_globals, 'tp_connection_base_color'), 'tp_connection_base_color'), + "InfoText": (tuple, get_default(scs_globals, 'info_text_color'), 'info_text_color'), + "BasePaint": (tuple, get_default(scs_globals, 'base_paint_color'), 'base_paint_color'), + } + + +class ConfigContainer: + """Class implementing config container handler.""" + + def __init__(self): + """Constructor.""" + self.sections = { + "Header": Header(), + "Paths": Paths(), + "Import": Import(), + "Export": Export(), + "GlobalDisplay": GlobalDisplay(), + "GlobalColors": GlobalColors(), + } + + def set_property(self, section_type, prop_name, value): + """Set property of given section and name with new value. + + :param section_type: section type property belongs to + :type section_type: str + :param prop_name: name of the property that should be set + :type prop_name: str + :param value: new value that should be set + :type value: any + :return: True if section exists and property could be inserted/rewritten, False otherwise + :rtype: bool + """ + if section_type not in self.sections: + return False + + cast_type, _, attr = self.sections[section_type].props[prop_name] + self.sections[section_type].props[prop_name] = (cast_type, cast_type(value), attr) + return True + + def apply_settings(self): + """Apllies currently loaded settings from config container to blender data. + + :return: True if settings were applicable; False otherwise + :rtype: bool + """ + + settings_applied = False + + if self.sections["Header"].is_valid() and not self.sections["Header"].is_applicable(): + self.sections["Header"].apply_config_storage_place() + elif self.sections["Header"].is_applicable(): + for _, section in self.sections.items(): + section.apply_settings() + settings_applied = True + + # apply paths on the end to make sure all of the settings are applied first. + # This is needed as some libraries are dependend on applied settings, for example: "*_use_infixed" + self.sections["Paths"].apply_paths(load_internal=not settings_applied) + + return settings_applied + + def fill_from_pix_container(self, pix_container): + """Fill container from given PIX container. + + :param pix_container: pix continer from which configs should be loaded + :type pix_container: list[io_scs_tools.internals.structure.SectionData] + :return: True if fill was successful; False otherwise + :rtype: bool + """ + + for section in pix_container: + if section.type not in self.sections: + return False + + section_filled = self.sections[section.type].fill_from_pix_section(section) + if not section_filled: + return False + + return True + + def get_pix_container(self): + """Returns currently set section read for writting to with PIX writter. + + :return: all sections data as list, ready to be used by PIX writter + :rtype: list[io_scs_tools.internals.structure.SectionData] + """ + + container = [] + + for section in self.sections.values(): + container.append(section.get_as_pix_section()) + + return container + + +class AsyncPathsInit: + """Class for fake-asychronous paths intialization, implemented with app.timers API.""" + + DUMP_LEVEL = 3 + """Constant for log level index according in SCS Globals, on which operator should printout extended report.""" + + # Static running variables + __paths_count = 0 + """Static variable holding number of all paths that had to be processed. Used for reporting progress eg. 'X of Y paths done'.""" + __paths_done = 0 + """Static variable holding number of already processed paths. Used for reporting progress eg. 'X of Y paths done'.""" + + # Static data storage + __message = "" + """Static variable holding printout extended message. This message used only if dump level is high enough.""" + __paths_list = [] + """Static variable holding list with dictonariy entries each of them representing Filepath class entry that needs in initialization. + Processed paths are removed on the fly. + """ + __callbacks = [] + """Static variable holding list of callbacks that will be executed once operator is finished or cancelled. + """ + + @staticmethod + def _report_progress(message="", abort=False, hide_controls=False): + """Reports progress into 3D view report mechanism. + + :param message: message to report + :type message: str + :param abort: should abort any 3D view reports? + :type abort: bool + :param hide_controls: controls can be hidden if user shouldn't be able to abort 3D view reports + :type hide_controls: bool + """ + + windows = bpy.data.window_managers[0].windows + + # we need window, otherwise 3d view report can't add modal handler for user close/minimize/scroll actions + assert len(windows) > 0 + + override = bpy.context.copy() + override["window"] = windows[-1] # using first one borks filebrowser UI for unknown reason, but last one works + bpy.ops.wm.scs_tools_show_3dview_report(override, 'INVOKE_DEFAULT', + message=message, + abort=abort, + hide_controls=hide_controls, + is_progress_message=True) + + @staticmethod + def _finish(): + """Cleanup and callbacks execution. + """ + + # reset static variables + AsyncPathsInit.__message = "" + AsyncPathsInit.__paths_list.clear() + + # report finished progress to 3d view report mechanism + if int(_get_scs_globals().dump_level) < AsyncPathsInit.DUMP_LEVEL: + AsyncPathsInit._report_progress(abort=True) + + # when done, tag everything for redraw in the case some UI components + # are reporting status of this operator + _view3d_utils.tag_redraw_all_regions() + + # as last invoke any callbacks and afterwards delete them + while len(AsyncPathsInit.__callbacks) > 0: + + callback = AsyncPathsInit.__callbacks[0] + + callback() + AsyncPathsInit.__callbacks.remove(callback) + + lprint("D Paths initialization finish invoked!") + + @staticmethod + def _process_paths(): + """Timer function for processing paths that are currently saved in static paths list. + + :return: time after which next path should be processed; None when all paths are being processed + :rtype: float | None + """ + + # do not proceed if list is already empty + if len(AsyncPathsInit.__paths_list) <= 0: + AsyncPathsInit._finish() + lprint("I Paths initialization finished, timer unregistered!") + return None + + scs_globals = _get_scs_globals() + + start_time = time() + + # update message with current path and apply it + AsyncPathsInit.__message += "Initializing " + AsyncPathsInit.__paths_list[0]["name"] + "..." + setattr(scs_globals, AsyncPathsInit.__paths_list[0]["attr"], AsyncPathsInit.__paths_list[0]["path"]) + + # calculate execution time, remove processed path, update message and counter + execution_time = time() - start_time + AsyncPathsInit.__paths_list = AsyncPathsInit.__paths_list[1:] # remove just processed item + AsyncPathsInit.__message += " Done in %.2f s!\n" % execution_time + AsyncPathsInit.__paths_done += 1 + + # when executing last one, also print out hiding message + if len(AsyncPathsInit.__paths_list) == 0: + AsyncPathsInit.__message += "SCS Blender Tools are ready!" + _view3d_utils.tag_redraw_all_view3d() + + # if debug then report whole progress message otherwise print out condensed message + if int(scs_globals.dump_level) >= AsyncPathsInit.DUMP_LEVEL: + message = AsyncPathsInit.__message + hide_controls = False + else: + message = "Paths and libraries initialization %s/%s ..." % (AsyncPathsInit.__paths_done, + AsyncPathsInit.__paths_count) + hide_controls = True + + AsyncPathsInit._report_progress(message=message, hide_controls=hide_controls) + + # if execution was ligthing fast, process another one or more, to shorten time of initialization + if len(AsyncPathsInit.__paths_list) > 0 and execution_time < 0.01: + lprint("S Initalizing next path directly ...") + return 0.01 + + return 0.2 + + @staticmethod + def is_running(): + """Tells if paths initialization is still in progress. + + :return: True if scs paths initialization is still in progress; False if none instances are running + :rtype: bool + """ + return len(AsyncPathsInit.__paths_list) > 0 and bpy.app.timers.is_registered(AsyncPathsInit._process_paths) + + @staticmethod + def append_callback(callback): + """Appends given callback function to callback list. Callbacks are called once paths initialization is done. + If operator is not running then False is returned and callback is not added to the list! + NOTE: there is no check if given callback is already in list. + + :param callback: callback function without arguments + :type callback: object + :return: True if operator is running and callback is added to the list properly; False if callback won't be added and executed + :rtype: bool + """ + if AsyncPathsInit.is_running(): + AsyncPathsInit.__callbacks.append(callback) + return True + + return False + + @staticmethod + def execute(paths_list): + """Executes asynchronous paths initialization for given paths list. + + :param paths_list: list of dictionaries of path entries: dict("name": "UI name", "attr": "name_of_attribute","path": "path_string") + :type paths_list: list[dict[str, str]] + """ + + AsyncPathsInit.__paths_done = 0 # reset done paths counter as everything starts here + + # now fill up new paths to static inventory + for filepath_prop in paths_list: + + # sort out only unique paths and merge them with current static path list + old_item = None + for item in AsyncPathsInit.__paths_list: + if item["attr"] == filepath_prop["attr"]: + old_item = item + break + + # if old item is found just reuse it instead of adding new item to list + if old_item: + old_item["name"] = filepath_prop["name"] + old_item["path"] = filepath_prop["path"] + else: + AsyncPathsInit.__paths_list.append( + { + "name": filepath_prop["name"], + "attr": filepath_prop["attr"], + "path": filepath_prop["path"] + } + ) + + # update paths counter to the current paths list length + AsyncPathsInit.__paths_count = len(AsyncPathsInit.__paths_list) + + AsyncPathsInit.__message = "Starting initialization...\n" + AsyncPathsInit._report_progress(message=AsyncPathsInit.__message, hide_controls=True) + + # in case any operator was previously invoked timer might still be running, so only register timer if needed + if not bpy.app.timers.is_registered(AsyncPathsInit._process_paths): + bpy.app.timers.register(AsyncPathsInit._process_paths, first_interval=0.2) + + lprint("I Paths initialization started...") def update_item_in_file(item_pointer, new_value): @@ -46,44 +826,76 @@ def update_item_in_file(item_pointer, new_value): if _get_scs_globals().config_storage_place == "BlendFile" and not item_pointer == "Header.ConfigStoragePlace": return False + # create config container + config = ConfigContainer() + filepath = get_config_filepath() ind = ' ' - config_container = _pix.get_data_from_file(filepath, ind) + pix_container = _pix.get_data_from_file(filepath, ind) - # if config container is still none, there had to be permission denied - # by it's creation, so no sense to try to write it again - if config_container is None: + # now load pix data into config container + if pix_container and not config.fill_from_pix_container(pix_container): + # no pix container or invalid, no sense to continue. + lprint("E Updating item inside config file failed!") return False - new_settings_container = [] - new_value_changed = False - item_pointer_split = item_pointer.split('.', 1) - for section in config_container: + # update property + section_type, prop_name = item_pointer.split('.', 1) + config.set_property(section_type, prop_name, new_value) - new_section = _SectionData(section.type) - for prop in section.props: - if section.type == item_pointer_split[0] and prop[0] == item_pointer_split[1]: - new_section.props.append((prop[0], new_value)) - new_value_changed = True - else: - new_section.props.append((prop[0], prop[1])) + # always update source! + config.set_property("Header", "Source", get_combined_ver_str()) - # append new properties if they are not yet there - if not new_value_changed and section.type == item_pointer_split[0]: - new_section.props.append((item_pointer_split[1], new_value)) + # write updated config back + _pix.write_data_to_file(config.get_pix_container(), filepath, ind) - new_settings_container.append(new_section) + return True - _pix.write_data_to_file(new_settings_container, filepath, ind) - return True +def update_scs_project_path(scs_project_path, reload_only=False): + """The function triggeres reload of paths that are dependend on scs project path and updates corresponding record in config file. + + :param scs_project_path: Absolute path to the desired scs project + :type scs_project_path: str + :param reload_only: flag for triggereing libraries reload only + :type reload_only: bool + """ + + scs_globals = _get_scs_globals() + + # prevent updating if config update is in progress ... + if scs_globals.config_update_lock: + return + + # before we are locking config, we need to update project path + if not reload_only: + update_item_in_file('Paths.ProjectPath', scs_project_path) + + # enable update lock because we only want to reload libraries, as their paths are not changed + engage_config_lock() + # trigger update functions via asynchronous operator for library paths initialization + AsyncPathsInit.execute([ + {"name": "trigger actions library", "attr": "trigger_actions_rel_path", "path": scs_globals.trigger_actions_rel_path}, + {"name": "sign library", "attr": "sign_library_rel_path", "path": scs_globals.sign_library_rel_path}, + {"name": "traffic semaphore library", "attr": "tsem_library_rel_path", "path": scs_globals.tsem_library_rel_path}, + {"name": "traffic rules library", "attr": "traffic_rules_library_rel_path", "path": scs_globals.traffic_rules_library_rel_path}, + {"name": "hookups library", "attr": "hookup_library_rel_path", "path": scs_globals.hookup_library_rel_path}, + {"name": "material substance library", "attr": "matsubs_library_rel_path", "path": scs_globals.matsubs_library_rel_path}, + {"name": "sun profiles library", "attr": "sun_profiles_lib_path", "path": scs_globals.sun_profiles_lib_path}, + ]) -def update_shader_presets_path(shader_presets_filepath): + # release lock as properties are applied + release_config_lock(use_paths_init_callback=True) + + +def update_shader_presets_path(shader_presets_filepath, reload_only=False): """The function deletes and populates again a list of Shader Preset items in inventory. It also updates corresponding record in config file. :param shader_presets_filepath: Absolute or relative path to the file with Shader Presets :type shader_presets_filepath: str + :param reload_only: flag for triggereing libraries reload only + :type reload_only: bool """ shader_presets_abs_path = _path_utils.get_abs_path(shader_presets_filepath) @@ -91,6 +903,10 @@ def update_shader_presets_path(shader_presets_filepath): lprint("I Shader presets library is up-to date, no update will happen!") return + # prevent updating if config update is in progress ... + if reload_only and _get_scs_globals().config_update_lock: + return + # CLEAR INVENTORY AND CACHE _shader_presets.clear() @@ -200,27 +1016,48 @@ def update_shader_presets_path(shader_presets_filepath): # set path from which this library was initialized _shader_presets.set_library_initialized(shader_presets_abs_path) - update_item_in_file('Paths.ShaderPresetsFilePath', shader_presets_filepath) + if not reload_only: + update_item_in_file('Paths.ShaderPresetsFilePath', shader_presets_filepath) -def update_trigger_actions_rel_path(scs_trigger_actions_inventory, trigger_actions_rel_path, readonly=False): +def update_trigger_actions_rel_path(trigger_actions_rel_path, reload_only=False): """The function deletes and populates again a list of Trigger Actions in inventory. It also updates corresponding record in config file. :param trigger_actions_rel_path: Relative path to the directory with Trigger Action files :type trigger_actions_rel_path: str + :param reload_only: flag for triggering reload only, which will be executed under condition that config lock is not engaged + :type reload_only: bool """ + scs_globals = _get_scs_globals() - gathered_library_filepaths = _path_utils.get_abs_paths(trigger_actions_rel_path, - use_infixed_search=_get_scs_globals().trigger_actions_use_infixed) + # prevent updating if config update is in progress ... + if reload_only and scs_globals.config_update_lock: + return + + # first update path in config file + if not reload_only: + update_item_in_file('Paths.TriggerActionsRelFilePath', trigger_actions_rel_path) + + gathered_library_filepaths = _path_utils.get_abs_paths(trigger_actions_rel_path, use_infixed_search=scs_globals.trigger_actions_use_infixed) + scs_trigger_actions_inventory = _get_scs_inventories().trigger_actions + + # get cache for trigger actions + cache = _PathsCache("TriggerActions") + + # check cache for validity and end if valid + if cache.is_valid(gathered_library_filepaths): + lprint("I Trigger actions library is up-to date, no update will happen!") + return - # CLEAR INVENTORY + # clear cache & inventory + cache.clear() scs_trigger_actions_inventory.clear() for trig_actions_path in gathered_library_filepaths: lprint("D Going to parse trigger actions file:\n\t %r", (trig_actions_path,)) - trig_actions_container = _sii.get_data_from_file(trig_actions_path) + trig_actions_container = _ContainersCache.retrieve(trig_actions_path) if trig_actions_container: # ADD ALL ITEMS FROM CONTAINER INTO INVENTORY @@ -236,28 +1073,48 @@ def update_trigger_actions_rel_path(scs_trigger_actions_inventory, trigger_actio trig_item.name = trg_action_name + " : " + item.id[12:] trig_item.item_id = item.id[12:] - if not readonly: - update_item_in_file('Paths.TriggerActionsRelFilePath', trigger_actions_rel_path) + # now as it's loaded add it to cache + cache.add_path(trig_actions_path) -def update_sign_library_rel_path(scs_sign_model_inventory, sign_library_rel_path, readonly=False): +def update_sign_library_rel_path(sign_library_rel_path, reload_only=False): """The function deletes and populates again a list of Sign names in inventory. It also updates corresponding record in config file. :param sign_library_rel_path: Relative path to the directory with Sign files :type sign_library_rel_path: str + :param reload_only: flag for triggering reload only, which will be executed under condition that config lock is not engaged + :type reload_only: bool """ + scs_globals = _get_scs_globals() + + # prevent updating if config update is in progress ... + if reload_only and scs_globals.config_update_lock: + return + + # first update path in config file + if not reload_only: + update_item_in_file('Paths.SignRelFilePath', sign_library_rel_path) + + gathered_library_filepaths = _path_utils.get_abs_paths(sign_library_rel_path, use_infixed_search=scs_globals.sign_library_use_infixed) + scs_sign_model_inventory = _get_scs_inventories().sign_models - gathered_library_filepaths = _path_utils.get_abs_paths(sign_library_rel_path, - use_infixed_search=_get_scs_globals().sign_library_use_infixed) + # get cache for sign models + cache = _PathsCache("SignModels") - # CLEAR INVENTORY + # check cache for validity and end if valid + if cache.is_valid(gathered_library_filepaths): + lprint("I Sign models library is up-to date, no update will happen!") + return + + # clear cache & inventory + cache.clear() scs_sign_model_inventory.clear() for sign_library_filepath in gathered_library_filepaths: lprint("D Going to parse sign library file:\n\t %r", (sign_library_filepath,)) - sign_container = _sii.get_data_from_file(sign_library_filepath) + sign_container = _ContainersCache.retrieve(sign_library_filepath) if sign_container: # ADD ALL ITEMS FROM CONTAINER INTO INVENTORY @@ -286,29 +1143,49 @@ def update_sign_library_rel_path(scs_sign_model_inventory, sign_library_rel_path if item.props['dynamic'] == 'true': sign_item.dynamic = True - if not readonly: - update_item_in_file('Paths.SignRelFilePath', sign_library_rel_path) + # now as it's loaded add it to cache + cache.add_path(sign_library_filepath) -def update_tsem_library_rel_path(scs_tsem_profile_inventory, tsem_library_rel_path, readonly=False): +def update_tsem_library_rel_path(tsem_library_rel_path, reload_only=False): """The function deletes and populates again a list of Traffic Semaphore Profile names in inventory. It also updates corresponding record in config file. :param tsem_library_rel_path: Relative path to the directory with Traffic Semaphore Profile files :type tsem_library_rel_path: str + :param reload_only: flag for triggering reload only, which will be executed under condition that config lock is not engaged + :type reload_only: bool """ + scs_globals = _get_scs_globals() - gathered_library_filepaths = _path_utils.get_abs_paths(tsem_library_rel_path, - use_infixed_search=_get_scs_globals().tsem_library_use_infixed) + # prevent updating if config update is in progress ... + if reload_only and scs_globals.config_update_lock: + return + + # first update path in config file + if not reload_only: + update_item_in_file('Paths.TSemProfileRelFilePath', tsem_library_rel_path) - # CLEAR INVENTORY + gathered_library_filepaths = _path_utils.get_abs_paths(tsem_library_rel_path, use_infixed_search=scs_globals.tsem_library_use_infixed) + scs_tsem_profile_inventory = _get_scs_inventories().tsem_profiles + + # get cache for tsem profiles + cache = _PathsCache("TsemProfiles") + + # check cache for validity and end if valid + if cache.is_valid(gathered_library_filepaths): + lprint("I Traffic semaphore profiles library is up-to date, no update will happen!") + return + + # clear cache & inventory + cache.clear() scs_tsem_profile_inventory.clear() for tsem_library_filepath in gathered_library_filepaths: lprint("D Going to parse tsem library file:\n\t %r", (tsem_library_filepath,)) - tsem_container = _sii.get_data_from_file(tsem_library_filepath) + tsem_container = _ContainersCache.retrieve(tsem_library_filepath) if tsem_container: # ADD ALL ITEMS FROM CONTAINER INTO INVENTORY @@ -327,28 +1204,49 @@ def update_tsem_library_rel_path(scs_tsem_profile_inventory, tsem_library_rel_pa if 'model' in item.props: tsem_item.model = item.props['model'][0] - if not readonly: - update_item_in_file('Paths.TSemProfileRelFilePath', tsem_library_rel_path) + # now as it's loaded add it to cache + cache.add_path(tsem_library_filepath) -def update_traffic_rules_library_rel_path(scs_traffic_rules_inventory, traffic_rules_library_rel_path, readonly=False): +def update_traffic_rules_library_rel_path(traffic_rules_library_rel_path, reload_only=False): """The function deletes and populates again a list of Traffic Rules names in inventory. It also updates corresponding record in config file. :param traffic_rules_library_rel_path: Relative path to the directory with Traffic Rules files :type traffic_rules_library_rel_path: str + :param reload_only: flag for triggering reload only, which will be executed under condition that config lock is not engaged + :type reload_only: bool """ + scs_globals = _get_scs_globals() + + # prevent updating if config update is in progress ... + if reload_only and scs_globals.config_update_lock: + return + + # first update path in config file + if not reload_only: + update_item_in_file('Paths.TrafficRulesRelFilePath', traffic_rules_library_rel_path) gathered_library_filepaths = _path_utils.get_abs_paths(traffic_rules_library_rel_path, - use_infixed_search=_get_scs_globals().traffic_rules_library_use_infixed) + use_infixed_search=scs_globals.traffic_rules_library_use_infixed) + scs_traffic_rules_inventory = _get_scs_inventories().traffic_rules + + # get cache for traffic rules + cache = _PathsCache("TrafficRules") + + # check cache for validity and end if valid + if cache.is_valid(gathered_library_filepaths): + lprint("I Traffic rules library is up-to date, no update will happen!") + return - # CLEAR INVENTORY + # clear cache & inventory + cache.clear() scs_traffic_rules_inventory.clear() for traffic_rules_library_filepath in gathered_library_filepaths: lprint("D Going to parse traffic rules file:\n\t %r", (traffic_rules_library_filepath,)) - trul_container = _sii.get_data_from_file(traffic_rules_library_filepath) + trul_container = _ContainersCache.retrieve(traffic_rules_library_filepath) if trul_container: # ADD ALL ITEMS FROM CONTAINER INTO INVENTORY @@ -365,98 +1263,144 @@ def update_traffic_rules_library_rel_path(scs_traffic_rules_inventory, traffic_r if 'num_params' in item.props: traffic_rule_item.num_params = str(item.props['num_params']) - if not readonly: - update_item_in_file('Paths.TrafficRulesRelFilePath', traffic_rules_library_rel_path) + # now as it's loaded add it to cache + cache.add_path(traffic_rules_library_filepath) -def update_hookup_library_rel_path(scs_hookup_inventory, hookup_library_rel_path, readonly=False): +def update_hookup_library_rel_path(hookup_library_rel_path, reload_only=False): """The function deletes and populates again a list of Hookup names in inventory. It also updates corresponding record in config file. :param hookup_library_rel_path: Relative path to the directory with Hookup files :type hookup_library_rel_path: str + :param reload_only: flag for triggering reload only, which will be executed under condition that config lock is not engaged + :type reload_only: bool """ + scs_globals = _get_scs_globals() - # CLEAR INVENTORY - scs_hookup_inventory.clear() + # prevent updating if config update is in progress ... + if reload_only and scs_globals.config_update_lock: + return - taken_hoookup_ids = {} # temp dict for identifying unique hookups and preventing creation of duplicates (same type and id) - for abs_path in _path_utils.get_abs_paths(hookup_library_rel_path, is_dir=True): + # first update path in config file + if not reload_only: + update_item_in_file('Paths.HookupRelDirPath', hookup_library_rel_path) + + gathered_hookups_paths = _path_utils.get_abs_paths(hookup_library_rel_path, is_dir=True) + scs_hookup_inventory = _get_scs_inventories().hookups + + # collect final hookups SII files from all directories + final_hookups_sii_paths = [] + for abs_path in gathered_hookups_paths: if abs_path: # READ ALL "SII" FILES IN INVENTORY FOLDER for root, dirs, files in os.walk(abs_path): - lprint("D Going to parse hookup directory:\n\t %r", (root,)) + lprint("D Going to collect hookup files from directory:\n\t %r", (root,)) # print(' root: "%s"\n dirs: "%s"\n files: "%s"' % (root, dirs, files)) for file in files: if file.endswith(".sii"): filepath = os.path.join(root, file) - # print(' filepath: "%s"' % str(filepath)) - hookup_container = _sii.get_data_from_file(filepath) - - # ADD ALL ITEMS FROM CONTAINER INTO INVENTORY - if hookup_container: - for item in hookup_container: - # if item.type == 'sign_model': - if item.id.startswith('_'): - continue - else: - typeid = str(item.type + " : " + item.id) + final_hookups_sii_paths.append(filepath) - # ignore taken type&ids - if typeid in taken_hoookup_ids: - continue - else: - taken_hoookup_ids[typeid] = True + if '.svn' in dirs: + dirs.remove('.svn') # ignore SVN - hookup_file = scs_hookup_inventory.add() - hookup_file.name = typeid - hookup_file.item_id = item.id + # get cache for hookups + cache = _PathsCache("Hookups") - if 'model' in item.props: - # if model is defined as array ( appears if additional lod models are defined ) - # then use first none lod model - if isinstance(item.props['model'], type(list())): - hookup_file.model = item.props['model'][0] - else: - hookup_file.model = item.props['model'] + # check cache for validity and end if valid + if cache.is_valid(final_hookups_sii_paths): + lprint("I Hookups library is up-to date, no update will happen!") + return - if 'brand_idx' in item.props: - try: - hookup_file.brand_idx = int(item.props['brand_idx']) - except: - pass + # clear cache & inventory + cache.clear() + scs_hookup_inventory.clear() - if 'dir_type' in item.props: - hookup_file.dir_type = item.props['dir_type'] + taken_hoookup_ids = {} # temp dict for identifying unique hookups and preventing creation of duplicates (same type and id) + for filepath in final_hookups_sii_paths: + # print(' filepath: "%s"' % str(filepath)) + hookup_container = _ContainersCache.retrieve(filepath) + + # ADD ALL ITEMS FROM CONTAINER INTO INVENTORY + if hookup_container: + for item in hookup_container: + # if item.type == 'sign_model': + if item.id.startswith('_'): + continue + else: + typeid = str(item.type + " : " + item.id) + + # ignore taken type&ids + if typeid in taken_hoookup_ids: + continue + else: + taken_hoookup_ids[typeid] = True + + hookup_file = scs_hookup_inventory.add() + hookup_file.name = typeid + hookup_file.item_id = item.id + + if 'model' in item.props: + # if model is defined as array ( appears if additional lod models are defined ) + # then use first none lod model + if isinstance(item.props['model'], type(list())): + hookup_file.model = item.props['model'][0] + else: + hookup_file.model = item.props['model'] - if 'low_poly_only' in item.props: - if item.props['low_poly_only'] == 'true': - hookup_file.low_poly_only = True + if 'brand_idx' in item.props: + try: + hookup_file.brand_idx = int(item.props['brand_idx']) + except: + pass - if '.svn' in dirs: - dirs.remove('.svn') # ignore SVN + if 'dir_type' in item.props: + hookup_file.dir_type = item.props['dir_type'] - if not readonly: - update_item_in_file('Paths.HookupRelDirPath', hookup_library_rel_path) + if 'low_poly_only' in item.props: + if item.props['low_poly_only'] == 'true': + hookup_file.low_poly_only = True + + # now as it's loaded add it to cache + cache.add_path(filepath) -def update_matsubs_inventory(scs_matsubs_inventory, matsubs_library_rel_path, readonly=False): +def update_matsubs_inventory(matsubs_library_rel_path, reload_only=False): """The function deletes and populates again a list of Material Substance names in inventory. It also updates corresponding record in config file. :param matsubs_library_rel_path: Relative path to the directory with Material Substance files :type matsubs_library_rel_path: str + :param reload_only: flag for triggering reload only, which will be executed under condition that config lock is not engaged + :type reload_only: bool """ + + # prevent updating if config update is in progress ... + if reload_only and _get_scs_globals().config_update_lock: + return + matsubs_library_filepath = _path_utils.get_abs_path(matsubs_library_rel_path) + scs_matsubs_inventory = _get_scs_inventories().matsubs + + # get cache for material substances + cache = _PathsCache("MatSubs") + + # check cache for validity and end if valid + if cache.is_valid((matsubs_library_filepath,)): + lprint("I Material substance library is up-to date, no update will happen!") + return + + # clear cache & inventory + cache.clear() + scs_matsubs_inventory.clear() + if matsubs_library_filepath: - matsubs_container = _sii.get_data_from_file(matsubs_library_filepath) + matsubs_container = _ContainersCache.retrieve(matsubs_library_filepath) if matsubs_container: - # CLEAR INVENTORY - scs_matsubs_inventory.clear() - # ADD "NONE" ITEM IN INVENTORY matsubs_item = scs_matsubs_inventory.add() matsubs_item.name = "None" @@ -476,28 +1420,46 @@ def update_matsubs_inventory(scs_matsubs_inventory, matsubs_library_rel_path, re matsubs_item.item_id = item.id[1:] # matsubs_item.item_description = "" - if not readonly: + # now as it's loaded add it to cache + cache.add_path(matsubs_library_filepath) + + if not reload_only: update_item_in_file('Paths.MatSubsRelFilePath', matsubs_library_rel_path) -def update_sun_profiles_library_path(scs_sun_profiles_inventory, sun_profiles_library_path, readonly=False): +def update_sun_profiles_library_path(sun_profiles_library_path, reload_only=False): """The function deletes and populates again a list of sun profiles used for scs ligthning inside Blednder. It also updates corresponding record in config file. - :param scs_sun_profiles_inventory: sun profiles inventory from scs globals - :type scs_sun_profiles_inventory: bpy.types.CollectionProperty :param sun_profiles_library_path: sun profiles library path (relative to SCS Project Base Path or absolute) :type sun_profiles_library_path: str - :param readonly: flag indicating if path should be updated in config file or not - :type readonly: bool + :param reload_only: flag for triggering reload only, which will be executed under condition that config lock is not engaged + :type reload_only: bool """ + if not reload_only: + update_item_in_file('Paths.SunProfilesFilePath', sun_profiles_library_path) + + # prevent updating if config update is in progress ... + if reload_only and _get_scs_globals().config_update_lock: + return + sun_profiles_lib_filepath = _path_utils.get_abs_path(sun_profiles_library_path) + scs_sun_profiles_inventory = _get_scs_inventories().sun_profiles + + # get cache for sun profiles + cache = _PathsCache("SunProfiles") - # CLEAR INVENTORY + # check cache for validity and end if valid + if cache.is_valid((sun_profiles_lib_filepath,)): + lprint("I Sun profiles library is up-to date, no update will happen!") + return + + # clear cache & inventory + cache.clear() scs_sun_profiles_inventory.clear() if sun_profiles_lib_filepath: - sun_profiles_container = _sii.get_data_from_file(sun_profiles_lib_filepath) + sun_profiles_container = _ContainersCache.retrieve(sun_profiles_lib_filepath) if sun_profiles_container: for item in sun_profiles_container: @@ -530,144 +1492,19 @@ def update_sun_profiles_library_path(scs_sun_profiles_inventory, sun_profiles_li else: lprint("E Property: %r could not be parsed! Sun profile loading is incomplete." % color_prop) - if not readonly: - update_item_in_file('Paths.SunProfilesFilePath', sun_profiles_library_path) - - -def gather_default(): - """Creates a new setting container for saving to the file.""" - - def fill_header_section(): - """Fills up "Header" section.""" - section = _SectionData("Header") - section.props.append(("FormatVersion", 1)) - section.props.append(("Source", get_combined_ver_str())) - section.props.append(("Type", "Configuration")) - section.props.append(("Note", "User settings of SCS Blender Tools")) - author = bpy.context.user_preferences.system.author - if author: - section.props.append(("Author", str(author))) - section.props.append(("ConfigStoragePlace", _property_utils.get_by_type(bpy.types.GlobalSCSProps.config_storage_place))) - section.props.append(("DumpLevel", _property_utils.get_by_type(bpy.types.GlobalSCSProps.dump_level))) - return section - - def fill_paths_section(): - """Fills up "Paths" section.""" - section = _SectionData("Paths") - section.props.append(("ProjectPath", _property_utils.get_by_type(bpy.types.GlobalSCSProps.scs_project_path))) - section.props.append(("", "")) - section.props.append(("ShaderPresetsFilePath", _property_utils.get_by_type(bpy.types.GlobalSCSProps.shader_presets_filepath))) - section.props.append(("TriggerActionsRelFilePath", _property_utils.get_by_type(bpy.types.GlobalSCSProps.trigger_actions_rel_path))) - section.props.append(("TriggerActionsUseInfixed", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.trigger_actions_use_infixed)))) - section.props.append(("SignRelFilePath", _property_utils.get_by_type(bpy.types.GlobalSCSProps.sign_library_rel_path))) - section.props.append(("SignUseInfixed", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.sign_library_use_infixed)))) - section.props.append(("TSemProfileRelFilePath", _property_utils.get_by_type(bpy.types.GlobalSCSProps.tsem_library_rel_path))) - section.props.append(("TSemProfileUseInfixed", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.tsem_library_use_infixed)))) - section.props.append(("TrafficRulesRelFilePath", _property_utils.get_by_type(bpy.types.GlobalSCSProps.traffic_rules_library_rel_path))) - section.props.append(("TrafficRulesUseInfixed", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.traffic_rules_library_use_infixed)))) - section.props.append(("HookupRelDirPath", _property_utils.get_by_type(bpy.types.GlobalSCSProps.hookup_library_rel_path))) - section.props.append(("MatSubsRelFilePath", _property_utils.get_by_type(bpy.types.GlobalSCSProps.matsubs_library_rel_path))) - section.props.append(("ConvertersPath", _property_utils.get_by_type(bpy.types.GlobalSCSProps.conv_hlpr_converters_path))) - section.props.append(("UseAlternativeBases", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.use_alternative_bases)))) - return section - - def fill_import_section(): - """Fills up "Import" section.""" - section = _SectionData("Import") - section.props.append(("ImportScale", _property_utils.get_by_type(bpy.types.GlobalSCSProps.import_scale))) - section.props.append(("PreservePathForExport", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.import_preserve_path_for_export)))) - section.props.append(("ImportPimFile", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.import_pim_file)))) - section.props.append(("UseWelding", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.import_use_welding)))) - section.props.append(("WeldingPrecision", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.import_welding_precision)))) - section.props.append(("UseNormals", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.import_use_normals)))) - section.props.append(("ImportPitFile", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.import_pit_file)))) - section.props.append(("LoadTextures", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.import_load_textures)))) - section.props.append(("ImportPicFile", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.import_pic_file)))) - section.props.append(("ImportPipFile", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.import_pip_file)))) - section.props.append(("ImportPisFile", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.import_pis_file)))) - section.props.append(("ConnectedBones", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.import_connected_bones)))) - section.props.append(("BoneImportScale", _property_utils.get_by_type(bpy.types.GlobalSCSProps.import_bone_scale))) - section.props.append(("ImportPiaFile", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.import_pia_file)))) - section.props.append(("IncludeSubdirsForPia", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.import_include_subdirs_for_pia)))) - return section - - def fill_export_section(): - """Fills up "Export" section.""" - section = _SectionData("Export") - section.props.append(("ExportScale", _property_utils.get_by_type(bpy.types.GlobalSCSProps.export_scale))) - section.props.append(("ApplyModifiers", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.export_apply_modifiers)))) - section.props.append(("ExcludeEdgesplit", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.export_exclude_edgesplit)))) - section.props.append(("IncludeEdgesplit", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.export_include_edgesplit)))) - section.props.append(("ActiveUVOnly", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.export_active_uv_only)))) - section.props.append(("ExportVertexGroups", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.export_vertex_groups)))) - section.props.append(("ExportVertexColor", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.export_vertex_color)))) - section.props.append(("ExportVertexColorType", _property_utils.get_by_type(bpy.types.GlobalSCSProps.export_vertex_color_type))) - section.props.append(("ExportVertexColorType7", _property_utils.get_by_type(bpy.types.GlobalSCSProps.export_vertex_color_type_7))) - # section.props.append(("ExportAnimFile", info.get_default_prop_value(bpy.types.GlobalSCSProps.export_anim_file))) - section.props.append(("ExportPimFile", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.export_pim_file)))) - section.props.append(("OutputType", _property_utils.get_by_type(bpy.types.GlobalSCSProps.export_output_type))) - section.props.append(("ExportPitFile", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.export_pit_file)))) - section.props.append(("ExportPicFile", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.export_pic_file)))) - section.props.append(("ExportPipFile", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.export_pip_file)))) - section.props.append(("SignExport", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.export_write_signature)))) - return section + # as last unlock profile + sun_profile_item.is_blocked = False - def fill_global_display_section(): - """Fills up "GlobalDisplay" section.""" - section = _SectionData("GlobalDisplay") - section.props.append(("DisplayLocators", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.display_locators)))) - section.props.append(("LocatorSize", _property_utils.get_by_type(bpy.types.GlobalSCSProps.locator_size))) - section.props.append(("LocatorEmptySize", _property_utils.get_by_type(bpy.types.GlobalSCSProps.locator_empty_size))) - section.props.append(("DisplayConnections", int(_property_utils.get_by_type(bpy.types.GlobalSCSProps.display_connections)))) - section.props.append(("CurveSegments", _property_utils.get_by_type(bpy.types.GlobalSCSProps.curve_segments))) - section.props.append(("DisplayTextInfo", _property_utils.get_by_type(bpy.types.GlobalSCSProps.display_info))) - return section - - def fill_global_colors_section(): - """Fills up "GlobalColors" section.""" - section = _SectionData("GlobalColors") - section.props.append(("PrefabLocatorsWire", tuple(_property_utils.get_by_type(bpy.types.GlobalSCSProps.locator_prefab_wire_color)))) - section.props.append(("ModelLocatorsWire", tuple(_property_utils.get_by_type(bpy.types.GlobalSCSProps.locator_model_wire_color)))) - section.props.append(("ColliderLocatorsWire", tuple(_property_utils.get_by_type(bpy.types.GlobalSCSProps.locator_coll_wire_color)))) - section.props.append(("ColliderLocatorsFace", tuple(_property_utils.get_by_type(bpy.types.GlobalSCSProps.locator_coll_face_color)))) - section.props.append(("NavigationCurveBase", tuple(_property_utils.get_by_type(bpy.types.GlobalSCSProps.np_connection_base_color)))) - section.props.append(("MapLineBase", tuple(_property_utils.get_by_type(bpy.types.GlobalSCSProps.mp_connection_base_color)))) - section.props.append(("TriggerLineBase", tuple(_property_utils.get_by_type(bpy.types.GlobalSCSProps.tp_connection_base_color)))) - section.props.append(("InfoText", tuple(_property_utils.get_by_type(bpy.types.GlobalSCSProps.info_text_color)))) - section.props.append(("BasePaint", tuple(_property_utils.get_by_type(bpy.types.GlobalSCSProps.base_paint_color)))) - return section - - ''' - def fill_various_section(): - """Fills up "Various" section.""" - section = data_SectionData("Various") - section.props.append(("DumpLevel", _get_scs_globals().dump_level)) - return section - ''' - - # DATA CREATION - header_section = fill_header_section() - paths_section = fill_paths_section() - import_section = fill_import_section() - export_section = fill_export_section() - global_display_section = fill_global_display_section() - global_colors_section = fill_global_colors_section() - # various_section = fill_various_section() - - # DATA ASSEMBLING - config_container = [header_section, paths_section, import_section, export_section, global_display_section, global_colors_section] - # config_container.append(various_section) - - return config_container + # now as it's loaded add it to cache + cache.add_path(sun_profiles_lib_filepath) def new_config_file(filepath): """Creates a new config file at given location and name.""" - config_container = gather_default() - ind = " " + config_container = ConfigContainer().get_pix_container() try: - if _pix.write_data_to_file(config_container, filepath, ind): + if _pix.write_data_to_file(config_container, filepath, " "): return filepath except PermissionError: @@ -725,13 +1562,20 @@ def release_config_lock(use_paths_init_callback=False): :type use_paths_init_callback: bool """ if use_paths_init_callback: - _SCSPathsInitialization.append_callback(release_config_lock) + AsyncPathsInit.append_callback(release_config_lock) else: _get_scs_globals().config_update_lock = False -def apply_settings(): - """Applies all the settings to the active scene.""" +def apply_settings(preload_from_blend=False): + """Applies all the settings to the active scene. + + :param preload_from_blend: should load scs globals from blend file before trying to apply settings? + :param preload_from_blend: bool + """ + + if preload_from_blend: + _load_scs_globals_from_blend() scs_globals = _get_scs_globals() @@ -739,244 +1583,20 @@ def apply_settings(): if scs_globals.config_update_lock: return False - # NOTE: save file paths in extra variables and apply them on the end - # to make sure all of the settings are loaded first. - # This is needed as some libraries reading are driven by other values from config file. - # For example: "use_infixed" - scs_project_path = _property_utils.get_by_type(bpy.types.GlobalSCSProps.scs_project_path, scs_globals) - shader_presets_filepath = _property_utils.get_by_type(bpy.types.GlobalSCSProps.shader_presets_filepath, scs_globals) - trigger_actions_rel_path = _property_utils.get_by_type(bpy.types.GlobalSCSProps.trigger_actions_rel_path, scs_globals) - sign_library_rel_path = _property_utils.get_by_type(bpy.types.GlobalSCSProps.sign_library_rel_path, scs_globals) - tsem_library_rel_path = _property_utils.get_by_type(bpy.types.GlobalSCSProps.tsem_library_rel_path, scs_globals) - traffic_rules_library_rel_path = _property_utils.get_by_type(bpy.types.GlobalSCSProps.traffic_rules_library_rel_path, scs_globals) - hookup_library_rel_path = _property_utils.get_by_type(bpy.types.GlobalSCSProps.hookup_library_rel_path, scs_globals) - matsubs_library_rel_path = _property_utils.get_by_type(bpy.types.GlobalSCSProps.matsubs_library_rel_path, scs_globals) - sun_profiles_library_path = _property_utils.get_by_type(bpy.types.GlobalSCSProps.sun_profiles_lib_path, scs_globals) - conv_hlpr_converters_path = _property_utils.get_by_type(bpy.types.GlobalSCSProps.conv_hlpr_converters_path, scs_globals) - - # NOTE: as dump level is written in same section as config type - # applying it directly might take place before we get information about config type - # so it has to be saved into variable and applied only if global settings are loaded from config file - dump_level = scs_globals.dump_level - - # lock update now, as we don't want any properties update functions to trigger rewrite of config file - # which would lead to unwanted recursion - engage_config_lock() - config_container = _pix.get_data_from_file(get_config_filepath(), " ") # avoid applying process of config if not present (most probably permission problems on config creation) if config_container is not None: - settings_file_valid = 0 - for section in config_container: - if settings_file_valid == 2: - if section.type == "Paths": - for prop in section.props: - if prop[0] in ("", "#"): - pass - elif prop[0] == "ProjectPath": - scs_project_path = prop[1] - elif prop[0] == "ShaderPresetsFilePath": - shader_presets_filepath = prop[1] - elif prop[0] == "TriggerActionsRelFilePath": - trigger_actions_rel_path = prop[1] - elif prop[0] == "TriggerActionsUseInfixed": - scs_globals.trigger_actions_use_infixed = prop[1] - elif prop[0] == "SignRelFilePath": - sign_library_rel_path = prop[1] - elif prop[0] == "SignUseInfixed": - scs_globals.sign_library_use_infixed = prop[1] - elif prop[0] == "TSemProfileRelFilePath": - tsem_library_rel_path = prop[1] - elif prop[0] == "TSemProfileUseInfixed": - scs_globals.tsem_library_use_infixed = prop[1] - elif prop[0] == "TrafficRulesRelFilePath": - traffic_rules_library_rel_path = prop[1] - elif prop[0] == "TrafficRulesUseInfixed": - scs_globals.traffic_rules_library_use_infixed = prop[1] - elif prop[0] == "HookupRelDirPath": - hookup_library_rel_path = prop[1] - elif prop[0] == "MatSubsRelFilePath": - matsubs_library_rel_path = prop[1] - elif prop[0] == "SunProfilesFilePath": - sun_profiles_library_path = prop[1] - elif prop[0] == "ConvertersPath": - conv_hlpr_converters_path = prop[1] - elif prop[0] == "UseAlternativeBases": - scs_globals.use_alternative_bases = prop[1] - else: - lprint('W Unrecognised item "%s" has been found in setting file! Skipping...', (str(prop[0]),)) - elif section.type == "Import": - for prop in section.props: - if prop[0] in ("", "#"): - pass - elif prop[0] == "ImportScale": - scs_globals.import_scale = float(prop[1]) - elif prop[0] == "PreservePathForExport": - scs_globals.import_preserve_path_for_export = prop[1] - elif prop[0] == "ImportPimFile": - scs_globals.import_pim_file = prop[1] - elif prop[0] == "UseWelding": - scs_globals.import_use_welding = prop[1] - elif prop[0] == "WeldingPrecision": - scs_globals.import_welding_precision = prop[1] - elif prop[0] == "UseNormals": - scs_globals.import_use_normals = prop[1] - elif prop[0] == "ImportPitFile": - scs_globals.import_pit_file = prop[1] - elif prop[0] == "LoadTextures": - scs_globals.import_load_textures = prop[1] - elif prop[0] == "ImportPicFile": - scs_globals.import_pic_file = prop[1] - elif prop[0] == "ImportPipFile": - scs_globals.import_pip_file = prop[1] - elif prop[0] == "ImportPisFile": - scs_globals.import_pis_file = prop[1] - elif prop[0] == "ConnectedBones": - scs_globals.import_connected_bones = prop[1] - elif prop[0] == "BoneImportScale": - scs_globals.import_bone_scale = float(prop[1]) - elif prop[0] == "ImportPiaFile": - scs_globals.import_pia_file = prop[1] - elif prop[0] == "IncludeSubdirsForPia": - scs_globals.import_include_subdirs_for_pia = prop[1] - elif section.type == "Export": - for prop in section.props: - if prop[0] in ("", "#"): - pass - elif prop[0] == "ExportScale": - scs_globals.export_scale = float(prop[1]) - elif prop[0] == "ApplyModifiers": - scs_globals.export_apply_modifiers = prop[1] - elif prop[0] == "ExcludeEdgesplit": - scs_globals.export_exclude_edgesplit = prop[1] - elif prop[0] == "IncludeEdgesplit": - scs_globals.export_include_edgesplit = prop[1] - elif prop[0] == "ActiveUVOnly": - scs_globals.export_active_uv_only = prop[1] - elif prop[0] == "ExportVertexGroups": - scs_globals.export_vertex_groups = prop[1] - elif prop[0] == "ExportVertexColor": - scs_globals.export_vertex_color = prop[1] - elif prop[0] == "ExportVertexColorType": - scs_globals.export_vertex_color_type = str(prop[1]) - elif prop[0] == "ExportVertexColorType7": - scs_globals.export_vertex_color_type_7 = str(prop[1]) - elif prop[0] == "ExportPimFile": - scs_globals.export_pim_file = prop[1] - elif prop[0] == "OutputType": - scs_globals.export_output_type = prop[1] - elif prop[0] == "ExportPitFile": - scs_globals.export_pit_file = prop[1] - elif prop[0] == "ExportPicFile": - scs_globals.export_pic_file = prop[1] - elif prop[0] == "ExportPipFile": - scs_globals.export_pip_file = prop[1] - elif prop[0] == "SignExport": - scs_globals.export_write_signature = prop[1] - elif section.type == "GlobalDisplay": - for prop in section.props: - if prop[0] in ("", "#"): - pass - elif prop[0] == "DisplayLocators": - scs_globals.display_locators = prop[1] - elif prop[0] == "LocatorSize": - scs_globals.locator_size = float(prop[1]) - elif prop[0] == "LocatorEmptySize": - scs_globals.locator_empty_size = float(prop[1]) - elif prop[0] == "DisplayConnections": - scs_globals.display_connections = prop[1] - elif prop[0] == "CurveSegments": - scs_globals.curve_segments = prop[1] - elif prop[0] == "OptimizedConnsDrawing": - scs_globals.optimized_connections_drawing = prop[1] - elif prop[0] == "DisplayTextInfo": - scs_globals.display_info = prop[1] - else: - lprint('W Unrecognised item "%s" has been found in setting file! Skipping...', (str(prop[0]),)) - elif section.type == "GlobalColors": - for prop in section.props: - if prop[0] in ("", "#"): - pass - elif prop[0] == "PrefabLocatorsWire": - scs_globals.locator_prefab_wire_color = prop[1] - elif prop[0] == "ModelLocatorsWire": - scs_globals.locator_model_wire_color = prop[1] - elif prop[0] == "ColliderLocatorsWire": - scs_globals.locator_coll_wire_color = prop[1] - elif prop[0] == "ColliderLocatorsFace": - scs_globals.locator_coll_face_color = prop[1] - elif prop[0] == "NavigationCurveBase": - scs_globals.np_connection_base_color = prop[1] - elif prop[0] == "MapLineBase": - scs_globals.mp_connection_base_color = prop[1] - elif prop[0] == "TriggerLineBase": - scs_globals.tp_connection_base_color = prop[1] - elif prop[0] == "InfoText": - scs_globals.info_text_color = prop[1] - elif prop[0] == "BasePaint": - scs_globals.base_paint_color = prop[1] - else: - lprint('W Unrecognised item "%s" has been found in setting file! Skipping...', (str(prop[0]),)) - elif section.type == "Header": - for prop in section.props: - if prop[0] == "FormatVersion": - if prop[1] == 1: - settings_file_valid += 1 - elif prop[0] == "Type": - if prop[1] == "Configuration": - settings_file_valid += 1 - elif prop[0] == "DumpLevel": - dump_level = prop[1] - elif prop[0] == "ConfigStoragePlace": - scs_globals.config_storage_place = prop[1] - - # if settings are read directly from blend file, - # release update lock and don't search/apply any settings further - if prop[1] == "BlendFile": - settings_file_valid += 1 - - # as dump level can be read already (it can be placed above config storage place property), - # reset local variable back to value that was saved with blend file - dump_level = scs_globals.dump_level - - break # to avoid further reading of header properties, so dump_level won't be overwritten unintentionally - - scs_globals.dump_level = dump_level - - # now as last apply all of the file paths - # NOTE: applying paths is crucial for libraries - # (they are reloaded/initiated in property update functions). - if bpy.app.background: # if blender runs without UI then apply libraries directly as async operator is UI depended - - scs_globals.scs_project_path = scs_project_path - scs_globals.shader_presets_filepath = shader_presets_filepath - scs_globals.trigger_actions_rel_path = trigger_actions_rel_path - scs_globals.sign_library_rel_path = sign_library_rel_path - scs_globals.tsem_library_rel_path = tsem_library_rel_path - scs_globals.traffic_rules_library_rel_path = traffic_rules_library_rel_path - scs_globals.hookup_library_rel_path = hookup_library_rel_path - scs_globals.matsubs_library_rel_path = matsubs_library_rel_path - scs_globals.sun_profiles_lib_path = sun_profiles_library_path - scs_globals.conv_hlpr_converters_path = conv_hlpr_converters_path - - else: # if blender is started normally use asynchronous operator to reload libraries - - bpy.ops.world.scs_paths_initialization('INVOKE_DEFAULT', paths_list=[ - {"name": "project base path", "attr": "scs_project_path", "path": scs_project_path}, - {"name": "shader presets", "attr": "shader_presets_filepath", "path": shader_presets_filepath}, - {"name": "trigger actions library", "attr": "trigger_actions_rel_path", "path": trigger_actions_rel_path}, - {"name": "sign library", "attr": "sign_library_rel_path", "path": sign_library_rel_path}, - {"name": "traffic semaphore library", "attr": "tsem_library_rel_path", "path": tsem_library_rel_path}, - {"name": "traffic rules library", "attr": "traffic_rules_library_rel_path", "path": traffic_rules_library_rel_path}, - {"name": "hookups library", "attr": "hookup_library_rel_path", "path": hookup_library_rel_path}, - {"name": "material substance library", "attr": "matsubs_library_rel_path", "path": matsubs_library_rel_path}, - {"name": "sun profiles library", "attr": "sun_profiles_lib_path", "path": sun_profiles_library_path}, - {"name": "converters file path", "attr": "conv_hlpr_converters_path", "path": conv_hlpr_converters_path}, - ]) + # lock update now, as we don't want any properties update functions to trigger + # rewrite of config file which would lead to unwanted recursion + engage_config_lock() - # release lock as properties are applied - release_config_lock(use_paths_init_callback=not bpy.app.background) + config = ConfigContainer() + config.fill_from_pix_container(config_container) + config.apply_settings() + + # release lock as properties are applied + release_config_lock(use_paths_init_callback=not bpy.app.background) return True diff --git a/addon/io_scs_tools/internals/containers/parsers/pix.py b/addon/io_scs_tools/internals/containers/parsers/pix.py index c608c83..ac8e7ba 100644 --- a/addon/io_scs_tools/internals/containers/parsers/pix.py +++ b/addon/io_scs_tools/internals/containers/parsers/pix.py @@ -162,7 +162,7 @@ def _get_data(file, line): # print(' %i---%s' % (w_index, w_value)) data['weights'].append((w_index, w_value)) - # CLONES + # CLONES & VERTEX_VERTICES data_type, line = next_line(file) line_split = re.split(' +', line.strip()) if line_split[0] == "Clones:": @@ -173,6 +173,13 @@ def _get_data(file, line): c_vertex = int(line_split[(cnt * 2) + 3]) # print(' %i---%s' % (c_piece, c_vertex)) data['clones'].append((c_piece, c_vertex)) + elif line_split[0] == "VertexIndices:": + # print('%s =-> "%s"' % (data_type, line_split)) + data['vertex_indices'] = [] + for cnt in range(int(line_split[1])): + c_vertex = int(line_split[cnt + 2]) + # print(' %i---%s' % (c_piece, c_vertex)) + data['vertex_indices'].append(c_vertex) # CLOSING BRACKET data_type, line = next_line(file) diff --git a/addon/io_scs_tools/internals/containers/parsers/sii.py b/addon/io_scs_tools/internals/containers/parsers/sii.py index 918101f..9b956c5 100644 --- a/addon/io_scs_tools/internals/containers/parsers/sii.py +++ b/addon/io_scs_tools/internals/containers/parsers/sii.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software import re import os @@ -125,6 +125,12 @@ def parse_token(self): self.current_pos = 0 continue + # Skip block comments. + if re.match('/\*.*$', line_remainder): + self.current_pos += 2 + self.skip_to_end_of_block_comment() + continue + # Is this a identifier? match = re.match('\w+', line_remainder) if match: @@ -145,6 +151,26 @@ def parse_token(self): # even if we are called more than once for some reason. return _Token('eof', '') + def skip_to_end_of_block_comment(self): + """Parses input contents until it reaches end of the block comment.""" + + # end if we are at eof already + if self.current_line >= len(self.input): + return + + line_remainder = self.input[self.current_line][self.current_pos:] + + # inline block comment + match = re.match('.*\*/', line_remainder) + if match: + self.current_pos += match.end(0) + return + + # jump to next line and do recursive parsing until we reach eof or end of comment + self.current_line += 1 + self.current_pos = 0 + self.skip_to_end_of_block_comment() + def _parse_unit_name(tokenizer): result = '' diff --git a/addon/io_scs_tools/internals/containers/writers/pix.py b/addon/io_scs_tools/internals/containers/writers/pix.py index 53b2c27..12618af 100644 --- a/addon/io_scs_tools/internals/containers/writers/pix.py +++ b/addon/io_scs_tools/internals/containers/writers/pix.py @@ -136,15 +136,12 @@ def _write_properties_and_data(fw, section, ind, print_info): else: weight_string = weight_string + " " + str(i[0]).ljust(5, ' ') + float_to_hex_string(i[1]) - clones_string = "" + vertex_indices_string = "" for i_i, i in enumerate(data_line[1][2]): - if i_i + 1 == len(data_line[1][2]): - clones_string = clones_string + str(i[0]).ljust(5, ' ') + str(i[1]) - else: - clones_string = clones_string + str(i[0]).ljust(5, ' ') + str(i[1]).ljust(7, ' ') + vertex_indices_string = vertex_indices_string + str(i).ljust(6, ' ') fw('%s%sWeights: %s%s\n' % (ind, 8 * " ", str(len(data_line[1][1])).ljust(7, ' '), weight_string)) - fw('%s%sClones: %s%s\n' % (ind, 8 * " ", str(len(data_line[1][2])).ljust(7, ' '), clones_string)) + fw('%s%sVertexIndices: %s%s\n' % (ind, 8 * " ", str(len(data_line[1][2])).ljust(7, ' '), vertex_indices_string)) fw('%s%s)\n' % (ind, 6 * " ")) elif data_line[0] == "__matrix__": # print('MATRIX - data_line: %s' % str(data_line)) diff --git a/addon/io_scs_tools/internals/icons/__init__.py b/addon/io_scs_tools/internals/icons/__init__.py index 50f7e92..3b85504 100644 --- a/addon/io_scs_tools/internals/icons/__init__.py +++ b/addon/io_scs_tools/internals/icons/__init__.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import os from bpy.utils import previews @@ -24,44 +24,142 @@ from io_scs_tools.utils import path as _path from io_scs_tools.utils.printout import lprint -CUSTOM_ICONS = "custom_icons" +PCOLLS = "custom_icons" +PCOLLS_IDX_MAP = PCOLLS + "_index_map" +PCOLLS_NAME_MAP = PCOLLS + "_name_map" +CURRENT_THEME = "theme" -# Multiple preview collections can be stored in this dictionary, but we will use only one -_preview_collections = {} +_cache = { + PCOLLS: {}, # multiple preview collections, one per theme + PCOLLS_IDX_MAP: {}, # map of theme indices to theme names + PCOLLS_NAME_MAP: {}, # map of theme names to theme indices + CURRENT_THEME: _ICON_consts.default_icon_theme # default theme +} -def init(): +def register(): """Initialization function for getting hold of preview collection variable with already created custom icon objects. """ - # create new preview only once. - # NOTE: We removed previews cleanup from code - # because it was crashing Blender when rapidly enabling/disabling BT addon. - # So instead of always creating new preview collection we rather reuse existing and - # only load icons again with force reload flag, to ensure icons are always reloaded when init is called. - if CUSTOM_ICONS not in _preview_collections: - - pcoll = previews.new() - _preview_collections[CUSTOM_ICONS] = pcoll - - else: - - pcoll = _preview_collections[CUSTOM_ICONS] - print("INFO\t - Icon collection is already in python memory, re-using it!") - - # load icons tools_paths = _path.get_addon_installation_paths() - if len(tools_paths) > 0: + if len(tools_paths) <= 0: + return + + # load icon themes + icon_themes_dir = os.path.join(tools_paths[0], 'ui', 'icons') + if not os.path.isdir(icon_themes_dir): + return + + theme_idx = 0 + for theme_name in os.listdir(icon_themes_dir): + icon_theme_dir = os.path.join(icon_themes_dir, theme_name) + + # ignore all none directory entries + if not os.path.isdir(icon_theme_dir): + continue + + # create new preview only once. + # NOTE: We removed previews cleanup from code + # because it was crashing Blender when rapidly enabling/disabling BT addon. + # So instead of always creating new preview collection we rather reuse existing and + # only load icons again with force reload flag, to ensure icons are always reloaded when init is called. + if theme_name not in _cache[PCOLLS]: + + pcoll = previews.new() + _cache[PCOLLS][theme_name] = pcoll + _cache[PCOLLS_NAME_MAP][theme_idx] = theme_name + _cache[PCOLLS_IDX_MAP][theme_name] = theme_idx + theme_idx += 1 + else: + + pcoll = _cache[PCOLLS][theme_name] + print("INFO\t- Icon collection is already in python memory, re-using it!") for icon_type in _ICON_consts.Types.as_list(): - # create path to current icon "ui/icons/icon_type" - icon_path = os.path.join(tools_paths[0], 'ui' + os.sep + 'icons' + os.sep + icon_type) + # create path to current icon "ui/icons//" + icon_path = os.path.join(icon_theme_dir, icon_type) if os.path.isfile(icon_path): if icon_type not in pcoll: pcoll.load(icon_type, icon_path, 'IMAGE', force_reload=True) else: - lprint("W Icon %r is missing. Please try to install addon again!", (icon_type,)) + print("WARNING\t- Icon %r is missing. Please try to install addon again!" % icon_type) + + # if current theme doesn't exists, use first instead + if _cache[CURRENT_THEME] not in _cache[PCOLLS] and len(_cache[PCOLLS]) > 0: + set_theme(get_theme_name(0)) + print("WARNING\t- Default icon theme doesn't exist, fallback to first available!") + + +def unregister(): + """Clearing preview collections for custom icons. Should be called on addon unregister. + """ + for theme_name, pcoll in _cache[PCOLLS].items(): + pcoll.close() + + _cache[PCOLLS].clear() + _cache[PCOLLS_IDX_MAP].clear() + _cache[PCOLLS_NAME_MAP].clear() + + +def set_theme(theme): + """Set current used icons theme + + :param theme: icons theme to use + :type theme: str + """ + _cache[CURRENT_THEME] = theme + + +def get_theme_name(idx): + """Gets theme name from given index. + NOTE: No safety checks, for performance reasons! + + :param idx: index of the desired theme + :type idx: int + :return: name of given theme index + :rtype: str + """ + return _cache[PCOLLS_NAME_MAP][idx] + + +def get_theme_idx(name): + """Gets theme index from given name. + NOTE: No safety checks, for performance reasons! + + :param name: name of the desired theme + :type name: str + :return: index of given theme name + :rtype: int + """ + return _cache[PCOLLS_IDX_MAP][name] + + +def get_current_theme_idx(): + """Gets current used theme index. + + :return: index of current theme + :rtype: int + """ + return get_theme_idx(_cache[CURRENT_THEME]) + + +def get_loaded_themes(): + """Gets list of loaded theme names. + + :return: random ordered theme names + :rtype: list[str] + """ + return _cache[PCOLLS].keys() + + +def has_loaded_themes(): + """Tells if there are any loaded themes available. + + :return: True if any themes are currently loaded, False otherwise + :rtype: bool + """ + return len(_cache[PCOLLS]) > 0 def get_icon(icon_type): @@ -72,11 +170,13 @@ def get_icon(icon_type): :rtype: int """ - if CUSTOM_ICONS not in _preview_collections: + current_theme = _cache[CURRENT_THEME] + + if current_theme not in _cache[PCOLLS]: lprint("E Icons not yet initialized, Blender Tools were not properply initialized!") return 0 - pcoll = _preview_collections[CUSTOM_ICONS] + pcoll = _cache[PCOLLS][current_theme] if icon_type in pcoll: return pcoll[icon_type].icon_id diff --git a/addon/io_scs_tools/internals/inventory.py b/addon/io_scs_tools/internals/inventory.py index 228a6c8..cd77634 100644 --- a/addon/io_scs_tools/internals/inventory.py +++ b/addon/io_scs_tools/internals/inventory.py @@ -163,3 +163,49 @@ def get_item_name(inventory, data_id, report_errors=False): (inventory_class_name, data_id)) return result + + +def move_up(inventory, idx): + """Move item in inventory with given index up for one place. + + :param inventory: SCS collection property inventory + :type inventory: bpy.types.CollectionProperty + :param idx: index of item that should be moved + :type idx: int + :return: new index of moved item or unchanged index if out of range + :rtype: int + """ + + if idx < 0: + return idx + + if idx >= len(inventory): + return idx + + first_idx = idx + second_idx = max(0, idx - 1) + inventory.move(first_idx, second_idx) + return second_idx + + +def move_down(inventory, idx): + """Move item in inventory with given index down for one place. + + :param inventory: SCS collection property inventory + :type inventory: bpy.types.CollectionProperty + :param idx: index of item that should be moved + :type idx: int + :return: new index of moved item or unchanged index if out of range + :rtype: int + """ + + if idx < 0: + return idx + + if idx >= len(inventory): + return idx + + first_idx = idx + second_idx = min(len(inventory) - 1, idx + 1) + inventory.move(first_idx, second_idx) + return second_idx diff --git a/addon/io_scs_tools/internals/looks/__init__.py b/addon/io_scs_tools/internals/looks/__init__.py index b96c9fa..149e14e 100644 --- a/addon/io_scs_tools/internals/looks/__init__.py +++ b/addon/io_scs_tools/internals/looks/__init__.py @@ -16,10 +16,11 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software from collections import Iterable from io_scs_tools.consts import Look as _LOOK_consts +from io_scs_tools.utils import property as _property_utils from io_scs_tools.utils.printout import lprint _MAIN_DICT = _LOOK_consts.custom_prop_name @@ -395,6 +396,83 @@ def clean_unused(root_obj): lprint("D %s material entries cleaned from looks dictionary in %r", (len(unused_mats_ids), root_obj.name)) +def reassign_material(root_obj, new_mat, old_mat): + """Re-assign material entries from old material to new material in all of the looks in given SCS root object. + + NOTE: once reassign is triggered old material entry is removed from looks for given root object, + so it won't be accessible anymore + + :param root_obj: scs root object on which looks datablock new materials should be added + :type root_obj: bpy.types.Object + :param new_mat: new material to assign to + :type new_mat: bpy.types.Material + :param old_mat: old material to assign from + :type old_mat: bpy.types.Material + """ + + if not root_obj or not new_mat or not old_mat: + return + + if _MAIN_DICT not in root_obj or len(root_obj[_MAIN_DICT]) <= 0: + return + + existing_mats_ids = root_obj[_MAIN_DICT][root_obj[_MAIN_DICT].keys()[0]].keys() + + new_mat_id_str = str(new_mat.scs_props.id) + old_mat_id_str = str(old_mat.scs_props.id) + + # old material not found, nothing to do + if old_mat_id_str not in existing_mats_ids: + return + + # add material to looks only if it doesn't yet exists in dictionary + if new_mat_id_str not in existing_mats_ids: + + # re-assign old entry in all of the looks and remove it + for look_id in root_obj[_MAIN_DICT]: + old_mat_entries = root_obj[_MAIN_DICT][look_id][old_mat_id_str] + + root_obj[_MAIN_DICT][look_id][new_mat_id_str] = old_mat_entries + del root_obj[_MAIN_DICT][look_id][old_mat_id_str] + + +def get_material_entries(root_obj, material): + """Get material entries from all looks for given material on given root object. + + :param root_obj: scs root object on which looks datablock should be read from + :type root_obj: bpy.types.Object + :param material: blender material for which material entries should be gathered + :type material: bpy.type.Material + :return: + :rtype: + """ + if not root_obj or not material: + return {} + + if _MAIN_DICT not in root_obj or len(root_obj[_MAIN_DICT]) <= 0: + return {} + + existing_mats_ids = root_obj[_MAIN_DICT][root_obj[_MAIN_DICT].keys()[0]].keys() + + mat_id_str = str(material.scs_props.id) + + # material not found, nothing to do + if mat_id_str not in existing_mats_ids: + return {} + + material_entries = {} + for look_id in root_obj[_MAIN_DICT]: + mat_entries = {} + + # construct material look entries as native python objects so we can compare for equality + for prop_name, prop_value in root_obj[_MAIN_DICT][look_id][mat_id_str].items(): + mat_entries[prop_name] = _property_utils.get_id_prop_as_py_object(prop_value) + + material_entries[look_id] = mat_entries + + return material_entries + + def __collect_materials__(root_obj): """Collect all materials on given SCS root object. diff --git a/addon/io_scs_tools/internals/open_gl/cache.py b/addon/io_scs_tools/internals/open_gl/cache.py new file mode 100644 index 0000000..71047e1 --- /dev/null +++ b/addon/io_scs_tools/internals/open_gl/cache.py @@ -0,0 +1,180 @@ +# ##### 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 time import time +from bpy_extras.view3d_utils import location_3d_to_region_2d +from mathutils import Vector + +from io_scs_tools.internals.open_gl import locators as _locators + +INFOS_CACHE = "infos" +INFOS_LAST_UPDATE = "infos_last_update" +INFOS_DATA = "infos_data" + +LOC_2D_CACHE = "loc_2d" +LOC_2D_VALID = "loc_2d_valid_locators" +LOC_2D_DATA = "loc_2d_data" + + +class LocatorsCache: + """Class for implementing drawn locators info cache storing it's 2d location in region and info text that should be drawn. + + There are two caches: + 1. Info cache which will store locators infos (same for all views, can be calculated only once per second) + 2. 2D region locations cache per view matrix (this has to be calculated each time view matrix changes, as each view has it's own matrix) + """ + + def __init__(self): + self.__cache = { + INFOS_CACHE: { + INFOS_LAST_UPDATE: -99.9, + INFOS_DATA: {} + }, + LOC_2D_CACHE: { + LOC_2D_VALID: {}, + LOC_2D_DATA: {} + }, + } + + def cache_locations_2d(self, objs, region, region_3d): + """Caches given objects 2d locations in given region and saves list of valid objects for this given region perspective matrix. + + :param objs: list of locators that should be cached and checked for visibility + :type objs: collections.Sequence[bpy.types.Object] + :param region: blender area 2d region for which for which locators 2d location should be calculated + :type region: bpy.types.Region + :param region_3d: blender area 3d region from which locators 2d locations should be calculated + :type region_3d: bpy.types.RegionView3D + """ + cache = self.__cache[LOC_2D_CACHE][LOC_2D_DATA] + + persp_matrix_str = str(region_3d.perspective_matrix) + + if persp_matrix_str not in cache: # this perspective matrix not yet in cache, create new entry, continue to caching + + # pop first element if we have too many of entries already + if len(cache) >= 10: + first_key = list(cache.keys())[0] + del cache[first_key] + + cache[persp_matrix_str] = {} + + elif len(cache[persp_matrix_str]) != len(objs): # matrix found, but objects count changed, cleaar and continue to caching + cache[persp_matrix_str].clear() + + # cache given locator objects one by one & filter out of bounds objects + valid_locators = [] + for obj in objs: + + loc_ws = Vector((obj.matrix_world[0][3], obj.matrix_world[1][3], obj.matrix_world[2][3])) + loc_ws_str = str(loc_ws) + + # same location already cached for this locator, ignore it! + if obj in cache[persp_matrix_str] and loc_ws_str == cache[persp_matrix_str][obj][0]: + valid_locators.append(obj) + continue + + # calculate 2d location! + loc_2d = location_3d_to_region_2d(region, region_3d, loc_ws, None) + + # if out of bounds, ignore it! + if not loc_2d or loc_2d.x < 0 or loc_2d.x > region.width or loc_2d.y < 0 or loc_2d.y > region.height: + continue + + valid_locators.append(obj) + cache[persp_matrix_str][obj] = (loc_ws_str, loc_2d) + + self.__cache[LOC_2D_CACHE][LOC_2D_VALID][persp_matrix_str] = valid_locators + + def cache_infos(self, objs): + """Caches given objects comprehensive infos. + + :param objs: list of locators that should be cached for infos + :type objs: collections.Sequence[bpy.types.Object] + """ + cache = self.__cache[INFOS_CACHE] + + # cache infos once per second (also eliminates caching for same redraw, if called from multiple 3d views) + now_time = time() + if now_time - cache[INFOS_LAST_UPDATE] < 1.0: + return + + # set last updated time and clear old cache + cache[INFOS_LAST_UPDATE] = now_time + cache[INFOS_DATA].clear() + + for obj in objs: + if obj.scs_props.locator_type == 'Prefab': + info_txt = _locators.prefab.get_prefab_locator_comprehensive_info(obj) + elif obj.scs_props.locator_type == 'Model': + info_txt = _locators.model.get_model_locator_comprehensive_info(obj) + elif obj.scs_props.locator_type == 'Collision': + info_txt = _locators.collider.get_collision_locator_comprehensive_info(obj) + else: + raise TypeError("Unsupported locator type: %s" % obj.scs_props.locator_type) + + cache[INFOS_DATA][obj] = info_txt + + def get_valid_locators(self, persp_matrix_str): + """Gets list of locator objects that were cached for 2d locations for given perspective matrix string. + + :param persp_matrix_str: perspective matrix of 3d region converted to string + :type persp_matrix_str: str + :return: list of valid object or empty list if given perspective matrix has no cached 2d locations + :rtype: list[bpy.types.Object] + """ + cache = self.__cache[LOC_2D_CACHE][LOC_2D_VALID] + + if persp_matrix_str in cache: + return cache[persp_matrix_str] + else: + return [] + + def get_locator_location_2d(self, obj, persp_matrix_str): + """Gets cached 2d location for given locator object with given perspective matrix string. + + :param obj: locator for which 2d location should be returned + :type obj: bpy.types.Object + :param persp_matrix_str: perspective matrixs of 3d region converted to string + :type persp_matrix_str: str + :return: vector of size 2 with cached x and y positions for given perspective matrix + :rtype: mathutils.Vector + """ + cache = self.__cache[LOC_2D_CACHE][LOC_2D_DATA] + + if obj in cache[persp_matrix_str]: + return cache[persp_matrix_str][obj][1] + else: + raise KeyError("Given perspective matrix has no entries in locations cache, contact developer!") + + def get_locator_info(self, obj): + """Gets cached comprehensive info of given locator object. + + :param obj: locator for which comprehensive info should be returned + :type obj: bpy.types.Object + :return: formatted string including new line separators + :rtype: str + """ + cache = self.__cache[INFOS_CACHE][INFOS_DATA] + + if obj in cache: + return cache[obj] + else: + return "n/a" diff --git a/addon/io_scs_tools/internals/open_gl/core.py b/addon/io_scs_tools/internals/open_gl/core.py index 0e9304c..3a9d2e9 100644 --- a/addon/io_scs_tools/internals/open_gl/core.py +++ b/addon/io_scs_tools/internals/open_gl/core.py @@ -16,541 +16,430 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy import blf -from bgl import (glColor3f, glPointSize, glLineWidth, glEnable, glDisable, glClear, glBegin, glEnd, glVertex3f, glBindTexture, glTexCoord2f, - GL_DEPTH_TEST, GL_DEPTH_BUFFER_BIT, GL_POLYGON, GL_TEXTURE_2D, GL_BLEND, glBlendFunc, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) +import bgl from mathutils import Vector -from io_scs_tools.consts import PrefabLocators as _PL_consts +from gpu_extras.presets import draw_texture_2d from io_scs_tools.consts import Operators as _OP_consts -from io_scs_tools.internals import preview_models as _preview_models from io_scs_tools.internals.open_gl import locators as _locators from io_scs_tools.internals.open_gl import primitive as _primitive +from io_scs_tools.internals.open_gl.cache import LocatorsCache from io_scs_tools.internals.open_gl.storage import terrain_points as _terrain_points_storage -from io_scs_tools.internals.connections.wrappers import group as _connections_group_wrapper -from io_scs_tools.operators.wm import Show3DViewReport as _Show3DViewReportOperator +from io_scs_tools.internals.connections.wrappers import collection as _connections_wrapper from io_scs_tools.utils import info as _info_utils -from io_scs_tools.utils import math as _math_utils from io_scs_tools.utils import object as _object_utils +from io_scs_tools.utils import view3d as _view3d_utils from io_scs_tools.utils import get_scs_globals as _get_scs_globals +_2d_elements_cache = LocatorsCache() -def disable_depth_test(): - glDisable(GL_DEPTH_TEST) +def _cache_custom_2d_elements(region, region_3d, space): + """Caches custom 2D visual elements, that require custom OpenGL drawing in given 2D region of given 3D view. -def draw_custom_3d_elements(mode): - """Get's updated custom 3D elements and draws them + :param region: + :type region: bpy.types.Region + :param region_3d: + :type region_3d: bpy.types.RegionView3D + :return: list of valid cached custom elements for given region + :rtype: list[bpy.types.Object] + """ + scs_globals = _get_scs_globals() - :param mode: drawing mode for custom elements (can be: 'Normal' or 'X-ray') - :type mode: str + # locators display is switched off, don't even bother with caching and return empty list + if not scs_globals.display_locators: + return [] + + # current viewport has disabled empties drawing, don't even bother with caching and return empty list + if not bpy.context.space_data.show_object_viewport_empty: + return [] + + # no display info is currently enabled in scs globals, don't bother with caching and return empty list + if scs_globals.display_info not in {'locnames', 'locinfo', 'locnodes', 'loclanes'}: + return [] + + # gather visible locators + locators = [] + for visib_obj in bpy.context.visible_objects: + # filter out any none locator objects + if visib_obj.type != 'EMPTY' or visib_obj.scs_props.empty_object_type != 'Locator': + continue + + # filter out none prefav/model/collision locators + if visib_obj.scs_props.locator_type not in {'Prefab', 'Model', 'Collision'}: + continue + + # filter out clipped locator objects + is_clipped = False + if region_3d.use_clip_planes: + for clip_plane in region_3d.clip_planes: + clip_plane_vec = Vector(clip_plane) + obj_loc_vec = Vector((visib_obj.matrix_world[0][3], visib_obj.matrix_world[1][3], visib_obj.matrix_world[2][3], 1.0)) + if clip_plane_vec.dot(obj_loc_vec) < 0.0: + is_clipped = True + break + + if is_clipped: + continue + + # filter out locator objects not present in local view + if space.local_view and not visib_obj.local_view_get(space): + continue + + locators.append(visib_obj) + + # cache infos only if proper display option is used + if scs_globals.display_info == 'locinfo': + _2d_elements_cache.cache_infos(locators) + + # cache location for current 3D view + _2d_elements_cache.cache_locations_2d(locators, region, region_3d) + + return _2d_elements_cache.get_valid_locators(str(region_3d.perspective_matrix)) + + +def _draw_3dview_report(window, area, region): + """Draws reports in 3d views. + + :param window: window of 3D viewport + :type window: bpy.types.Window + :param area: area of 3D viewport + :type area: bpy.types.Area + :param region: region of 3D viewport + :type region: bpy.types.Region """ - if mode == "Normal": - glEnable(GL_DEPTH_TEST) - glClear(GL_DEPTH_BUFFER_BIT) - else: # X-ray mode - disable_depth_test() + from io_scs_tools.operators.wm import SCS_TOOLS_OT_Show3DViewReport as _Show3DViewReport - scs_globals = _get_scs_globals() + # no reported lines, we don't draw anything + if not _Show3DViewReport.has_lines(): + return - # TERRAIN POINTS - glPointSize(5.0) + # calculate dynamic left and top margins + if bpy.context.preferences.system.use_region_overlap: + pos_x = 75 + # try to find tools region and properly adopt position X + for reg in area.regions: + if reg.type == 'TOOLS': + pos_x = reg.width + 20 + break + pos_y = region.height - 105 + else: + pos_x = 20 + pos_y = region.height - 80 + + # draw BT banner + (bindcode, width, height) = _Show3DViewReport.get_scs_banner_img_data(window) + + bgl.glEnable(bgl.GL_BLEND) + bgl.glBlendFunc(bgl.GL_SRC_ALPHA, bgl.GL_ONE_MINUS_SRC_ALPHA) + draw_texture_2d(bindcode, (pos_x - 5, pos_y), width, height) + bgl.glDisable(bgl.GL_BLEND) + + # draw control buttons, if controls are enabled + if _Show3DViewReport.has_controls(window): + + blf.size(0, 20, 72) + blf.color(0, .8, .8, .8, 1) + + # set x and y offsets to report operator, so that area calculations for buttons can be calculated properly + # considering dynamic left and top margins. + _Show3DViewReport.set_btns_xy_offset(pos_x, region.height - pos_y) + + # draw close button + _primitive.draw_rect_2d( + ( + (pos_x + _OP_consts.View3DReport.CLOSE_BTN_AREA[0], pos_y - _OP_consts.View3DReport.CLOSE_BTN_AREA[2]), + (pos_x + _OP_consts.View3DReport.CLOSE_BTN_AREA[1], pos_y - _OP_consts.View3DReport.CLOSE_BTN_AREA[2]), + (pos_x + _OP_consts.View3DReport.CLOSE_BTN_AREA[1], pos_y - _OP_consts.View3DReport.CLOSE_BTN_AREA[3]), + (pos_x + _OP_consts.View3DReport.CLOSE_BTN_AREA[0], pos_y - _OP_consts.View3DReport.CLOSE_BTN_AREA[3]) + ), + (.25, .25, .25, 1) + ) + + # draw close button text + blf.position(0, pos_x + _OP_consts.View3DReport.CLOSE_BTN_TEXT_POS[0], pos_y - _OP_consts.View3DReport.CLOSE_BTN_TEXT_POS[1], 0) + blf.draw(0, _OP_consts.View3DReport.CLOSE_BTN_TEXT) + + # draw hide button + _primitive.draw_rect_2d( + ( + (pos_x + _OP_consts.View3DReport.HIDE_BTN_AREA[0], pos_y - _OP_consts.View3DReport.HIDE_BTN_AREA[2]), + (pos_x + _OP_consts.View3DReport.HIDE_BTN_AREA[1], pos_y - _OP_consts.View3DReport.HIDE_BTN_AREA[2]), + (pos_x + _OP_consts.View3DReport.HIDE_BTN_AREA[1], pos_y - _OP_consts.View3DReport.HIDE_BTN_AREA[3]), + (pos_x + _OP_consts.View3DReport.HIDE_BTN_AREA[0], pos_y - _OP_consts.View3DReport.HIDE_BTN_AREA[3]) + ), + (.25, .25, .25, 1) + ) + + # draw hide button text + blf.position(0, pos_x + _OP_consts.View3DReport.HIDE_BTN_TEXT_POS[0], pos_y - _OP_consts.View3DReport.HIDE_BTN_TEXT_POS[1], 0) + blf.draw(0, _OP_consts.View3DReport.HIDE_BTN_TEXT) + + # draw scroll controls + if _Show3DViewReport.is_scrolled() and _Show3DViewReport.is_shown(): + + blf.size(0, 16, 72) + + # draw scroll up button + _primitive.draw_rect_2d( + ( + (pos_x + _OP_consts.View3DReport.SCROLLUP_BTN_AREA[0], pos_y - _OP_consts.View3DReport.SCROLLUP_BTN_AREA[2]), + (pos_x + _OP_consts.View3DReport.SCROLLUP_BTN_AREA[1], pos_y - _OP_consts.View3DReport.SCROLLUP_BTN_AREA[2]), + (pos_x + _OP_consts.View3DReport.SCROLLUP_BTN_AREA[1], pos_y - _OP_consts.View3DReport.SCROLLUP_BTN_AREA[3]), + (pos_x + _OP_consts.View3DReport.SCROLLUP_BTN_AREA[0], pos_y - _OP_consts.View3DReport.SCROLLUP_BTN_AREA[3]) + ), + (.25, .25, .25, 1) + ) + + # draw scroll up button text + blf.position(0, pos_x + _OP_consts.View3DReport.SCROLLUP_BTN_TEXT_POS[0], pos_y - _OP_consts.View3DReport.SCROLLUP_BTN_TEXT_POS[1], 0) + blf.draw(0, _OP_consts.View3DReport.SCROLLUP_BTN_TEXT) + + # draw scroll down button + _primitive.draw_rect_2d( + ( + (pos_x + _OP_consts.View3DReport.SCROLLDOWN_BTN_AREA[0], pos_y - _OP_consts.View3DReport.SCROLLDOWN_BTN_AREA[2]), + (pos_x + _OP_consts.View3DReport.SCROLLDOWN_BTN_AREA[1], pos_y - _OP_consts.View3DReport.SCROLLDOWN_BTN_AREA[2]), + (pos_x + _OP_consts.View3DReport.SCROLLDOWN_BTN_AREA[1], pos_y - _OP_consts.View3DReport.SCROLLDOWN_BTN_AREA[3]), + (pos_x + _OP_consts.View3DReport.SCROLLDOWN_BTN_AREA[0], pos_y - _OP_consts.View3DReport.SCROLLDOWN_BTN_AREA[3]) + ), + (.25, .25, .25, 1) + ) + + # draw scroll down button text + blf.position(0, pos_x + _OP_consts.View3DReport.SCROLLDOWN_BTN_TEXT_POS[0], pos_y - _OP_consts.View3DReport.SCROLLDOWN_BTN_TEXT_POS[1], 0) + blf.draw(0, _OP_consts.View3DReport.SCROLLDOWN_BTN_TEXT) + + # draw version string + pos_y -= 12 + blf.size(0, 10, 72) + blf.color(0, .952, .635, .062, 1) + blf.position(0, pos_x, pos_y, 0) + blf.draw(0, _info_utils.get_combined_ver_str(only_version_numbers=True)) + pos_y -= 20 + + # draw actual operator title and message if shown + if _Show3DViewReport.is_shown(): + + blf.size(0, 12, 72) + blf.color(0, 1, 1, 1, 1) + blf.shadow(0, 5, 0, 0, 0, 1) + + if _Show3DViewReport.get_title() != "": + blf.position(0, pos_x, pos_y, 0) + blf.draw(0, _Show3DViewReport.get_title()) + pos_y -= 15 + + blf.enable(0, blf.SHADOW) + _Show3DViewReport.set_out_of_bounds(False) + for line in _Show3DViewReport.get_lines(): + + # finish printing if running out of space + if pos_y - 60 < 0: + blf.position(0, pos_x, pos_y, 0) + blf.draw(0, "...") + pos_y -= 15 + _Show3DViewReport.set_out_of_bounds(True) + break + + blf.position(0, pos_x, pos_y, 0) + if "ERROR" in line: + blf.shadow(0, 5, 0.5, 0., 0, 1) + elif "WARNING" in line: + blf.shadow(0, 5, 0.3, 0.15, 0, 1) + + blf.draw(0, line) + pos_y -= 15 + + blf.disable(0, blf.SHADOW) + + +def fill_buffers(object_list): + """Fill drawing buffers with custom 3D visual elements for given objects. + + :param object_list: visible objects that should be checked and filled as custom visual elements; empty list when buffers should be emptied + :type object_list: collections.Iterable + """ - for tp_position, tp_color in _terrain_points_storage.get_positions_and_colors(): - _primitive.draw_point(tp_position, tp_color) + # assemble locators dictionaries and properly set empties display sizes + prefab_locators = {} + model_locators = {} + collision_locators = {} - glPointSize(1.0) + for visib_obj in object_list: - (prefab_locators, collision_locators, model_locators) = _get_custom_visual_elements() + if visib_obj.type != 'EMPTY' or visib_obj.scs_props.empty_object_type != 'Locator' or not visib_obj.visible_get(): + continue - # reset point size to 1.0 and set line width to 2.0 before drawing curves and lines - glPointSize(1.0) - glLineWidth(2.0) + if visib_obj.scs_props.locator_type == 'Prefab': + prefab_locators[visib_obj.name] = visib_obj + _object_utils.store_locators_original_display_size_and_type(visib_obj) + _object_utils.set_locators_prefab_display_size_and_type(visib_obj) + elif visib_obj.scs_props.locator_type == 'Model': + model_locators[visib_obj.name] = visib_obj + _object_utils.store_locators_original_display_size_and_type(visib_obj) + _object_utils.set_locators_model_display_size_and_type(visib_obj) + elif visib_obj.scs_props.locator_type == 'Collision': + collision_locators[visib_obj.name] = visib_obj + _object_utils.store_locators_original_display_size_and_type(visib_obj) + _object_utils.set_locators_coll_display_size_and_type(visib_obj) + elif visib_obj.scs_props.locators_orig_display_size != 0.0: + _object_utils.set_locators_original_size_and_type(visib_obj) - # CURVES AND LINES - if scs_globals.display_connections: - _connections_group_wrapper.draw(prefab_locators) + # clear buffers + _primitive.clear_buffers() - # reset line width to 1.0 after drawing curves and lines - glLineWidth(1.0) + # reset any set active buffer, switch back to main ones + _primitive.set_active_buffers(None) - # LOCATORS - if scs_globals.display_locators: + # fill buffers for main view + _fill_active_buffers(prefab_locators, model_locators, collision_locators) - # PREFAB LOCATORS - if prefab_locators: - for obj in prefab_locators.values(): - _locators.prefab.draw_prefab_locator(obj, scs_globals) + # extra buffers filling for each local view + for space in _view3d_utils.get_spaces_with_local_view(): - # COLLISION LOCATORS - if collision_locators: - for obj in collision_locators.values(): - _locators.collider.draw_collision_locator(obj, scs_globals) + local_prefab_locators = {} + local_model_locators = {} + local_collision_locators = {} - # MODEL LOCATORS - if model_locators: - for obj in model_locators.values(): - _locators.model.draw_model_locator(obj, scs_globals) + for obj_name in prefab_locators: + if prefab_locators[obj_name].local_view_get(space): + local_prefab_locators[obj_name] = prefab_locators[obj_name] + for obj_name in model_locators: + if model_locators[obj_name].local_view_get(space): + local_model_locators[obj_name] = model_locators[obj_name] -def draw_custom_2d_elements(): - context = bpy.context + for obj_name in collision_locators: + if collision_locators[obj_name].local_view_get(space): + local_collision_locators[obj_name] = collision_locators[obj_name] + + # set active buffers depending on given 3d space + _primitive.set_active_buffers(space) + + # now fill in locators visible in this space + _fill_active_buffers(local_prefab_locators, local_model_locators, local_collision_locators) + + +def _fill_active_buffers(prefab_locators, model_locators, collision_locators): + """Fill active buffers with given locator dictionaries. + + :param prefab_locators: prefab locators that should be filled into active buffers + :type prefab_locators: dict[bpy.types.Object] + :param model_locators: model locators that should be filled into active buffers + :type model_locators: dict[bpy.types.Object] + :param collision_locators: collision locators that should be filled into active buffers + :type collision_locators: dict[bpy.types.Object] + """ scs_globals = _get_scs_globals() - font_id = 0 # TODO: Need to find out how best to get this. - blf.size(font_id, 12, 72) + # fill terrain points always + for tp_position, tp_color in _terrain_points_storage.get_positions_and_colors(): + _primitive.draw_point(tp_position, tp_color, 5.0) - region = context.region + # fill curves and lines + if scs_globals.display_connections: + _connections_wrapper.draw(prefab_locators) - # draw 3d view import/export reports - _draw_3dview_report(region) + # fill locators + if scs_globals.display_locators: - (prefab_locators, collision_locators, model_locators) = _get_custom_visual_elements() + for obj in prefab_locators.values(): + _locators.prefab.draw_prefab_locator(obj, scs_globals) - if not prefab_locators and not collision_locators and not model_locators: - return + for obj in model_locators.values(): + _locators.model.draw_model_locator(obj, scs_globals) - glColor3f( - scs_globals.info_text_color[0], - scs_globals.info_text_color[1], - scs_globals.info_text_color[2], - ) + for obj in collision_locators.values(): + _locators.collider.draw_collision_locator(obj, scs_globals) - region3d = context.space_data.region_3d - region_mid_width = region.width / 2.0 - region_mid_height = region.height / 2.0 +def draw_custom_3d_elements(mode): + """Draws custom 3D elements filled in buffers (if local view is active, then buffers are refilled also). - # VARS FOR PROJECTION - perspective_matrix = region3d.perspective_matrix.copy() + :param mode: drawing mode for custom 3D elements (can be: 'Normal' or 'X-ray') + :type mode: str + """ + # if empties are disabled in this viewport ... our locators and connections should be too + if not bpy.context.space_data.show_object_viewport_empty: + return - region_data = (perspective_matrix, region_mid_width, region_mid_height) + if mode == "Normal": + bgl.glEnable(bgl.GL_DEPTH_TEST) + bgl.glEnable(bgl.GL_BLEND) + bgl.glBlendFunc(bgl.GL_SRC_ALPHA, bgl.GL_ONE_MINUS_SRC_ALPHA) - # LOCATOR NAMES - if scs_globals.display_info == 'locnames': - if prefab_locators: - for key, obj in prefab_locators.items(): - mat = obj.matrix_world - _primitive.draw_text(key, font_id, Vector((mat[0][3], mat[1][3], mat[2][3])), region_data) + # draw buffers + _primitive.draw_buffers(bpy.context.space_data) - if collision_locators: - for key, obj in collision_locators.items(): - mat = obj.matrix_world - _primitive.draw_text(key, font_id, Vector((mat[0][3], mat[1][3], mat[2][3])), region_data) + if mode == "Normal": + bgl.glDisable(bgl.GL_DEPTH_TEST) + bgl.glDisable(bgl.GL_BLEND) - if model_locators: - for key, obj in model_locators.items(): - mat = obj.matrix_world - _primitive.draw_text(key, font_id, Vector((mat[0][3], mat[1][3], mat[2][3])), region_data) - # LOCATOR COMPREHENSIVE INFO - elif scs_globals.display_info == 'locinfo': - if prefab_locators: - for key, obj in prefab_locators.items(): - mat = obj.matrix_world - textlines = ['"' + key + '"', - str(obj.scs_props.locator_type + " - " + obj.scs_props.locator_prefab_type)] +def draw_custom_2d_elements(): + """Draws custom 2D elements from cache. + """ + context = bpy.context + scs_globals = _get_scs_globals() - if obj.scs_props.locator_prefab_type == 'Control Node': + region = context.region + region_3d = context.region_data + space = context.space_data + area = context.area + window = context.window - textlines.append(str("Node Index: " + str(obj.scs_props.locator_prefab_con_node_index))) + # draw 3d view import/export reports + _draw_3dview_report(window, area, region) - elif obj.scs_props.locator_prefab_type == 'Spawn Point': + # cache & get valid locators for current region boundaries + locators = _cache_custom_2d_elements(region, region_3d, space) - spawn_type_i = int(obj.scs_props.locator_prefab_spawn_type) - textlines.append(str("Type: " + obj.scs_props.enum_spawn_type_items[spawn_type_i][1])) + # no locators, we can safely finish here + if not locators: + return - elif obj.scs_props.locator_prefab_type == 'Traffic Semaphore': + font_id = 0 # default font + blf.size(font_id, 12, 72) + blf.color(font_id, scs_globals.info_text_color[0], scs_globals.info_text_color[1], scs_globals.info_text_color[2], 1.0) + blf.word_wrap(font_id, 999) + blf.enable(font_id, blf.WORD_WRAP) - textlines.append(str("ID: " + str(obj.scs_props.locator_prefab_tsem_id))) - if obj.scs_props.locator_prefab_tsem_profile != '': - textlines.append(str("Profile: " + str(obj.scs_props.locator_prefab_tsem_profile))) - tsem_type_i = int(obj.scs_props.locator_prefab_tsem_type) - if tsem_type_i != _PL_consts.TST.PROFILE: - textlines.append(str("Type: " + obj.scs_props.enum_tsem_type_items[tsem_type_i][1])) - textlines.append(str("G: %.2f" % obj.scs_props.locator_prefab_tsem_gs + - " - O: %.2f" % obj.scs_props.locator_prefab_tsem_os1 + - " - R: %.2f" % obj.scs_props.locator_prefab_tsem_rs + - " - O: %.2f" % obj.scs_props.locator_prefab_tsem_os2)) - if obj.scs_props.locator_prefab_tsem_cyc_delay != 0: - textlines.append(str("Cycle Delay: " + "%.2f s" % obj.scs_props.locator_prefab_tsem_cyc_delay)) + persp_matrix_str = str(region_3d.perspective_matrix) - elif obj.scs_props.locator_prefab_type == 'Navigation Point': + # LOCATOR NAMES + if scs_globals.display_info == 'locnames': + for obj in locators: + loc_2d = _2d_elements_cache.get_locator_location_2d(obj, persp_matrix_str) + _primitive.draw_text(obj.name, font_id, loc_2d.x, loc_2d.y) - np_boundary_i = int(obj.scs_props.locator_prefab_np_boundary) - if np_boundary_i != 0: - textlines.append(str("Boundary: " + obj.scs_props.enum_np_boundary_items[np_boundary_i][1])) - - textlines.append(str("B. Node: " + str(obj.scs_props.locator_prefab_np_boundary_node))) - if obj.scs_props.locator_prefab_np_traffic_semaphore != '-1': - textlines.append(str("T. Light ID: " + str(obj.scs_props.locator_prefab_np_traffic_semaphore))) - - elif obj.scs_props.locator_prefab_type == 'Map Point': - - if obj.scs_props.locator_prefab_mp_road_over: - textlines.append("Road Over: YES") - if obj.scs_props.locator_prefab_mp_no_outline: - textlines.append("No Outline: YES") - if obj.scs_props.locator_prefab_mp_no_arrow: - textlines.append("No Arrow: YES") - if obj.scs_props.locator_prefab_mp_prefab_exit: - textlines.append("Prefab Exit: YES") - - road_size_i = int(obj.scs_props.locator_prefab_mp_road_size) - textlines.append(str("Road Size: " + obj.scs_props.enum_mp_road_size_items[road_size_i][1])) - - road_offset_i = int(obj.scs_props.locator_prefab_mp_road_offset) - if road_offset_i != _PL_consts.MPVF.ROAD_OFFSET_0: - textlines.append(str("Offset: " + obj.scs_props.enum_mp_road_offset_items[road_offset_i][1])) - - custom_color_i = int(obj.scs_props.locator_prefab_mp_custom_color) - if custom_color_i != 0: - textlines.append(str("Custom Color: " + obj.scs_props.enum_mp_custom_color_items[custom_color_i][1])) - - assigned_node_i = int(obj.scs_props.locator_prefab_mp_assigned_node) - if assigned_node_i != 0: - textlines.append(str("Node: " + obj.scs_props.enum_mp_assigned_node_items[assigned_node_i][1])) - - des_nodes = "Destination Nodes:" - for index in obj.scs_props.locator_prefab_mp_dest_nodes: - des_nodes += " " + index - if des_nodes != "Destination Nodes:": - textlines.append(des_nodes) - - elif obj.scs_props.locator_prefab_type == 'Trigger Point': - - textlines.append(str("Range: %.2f m" % obj.scs_props.locator_prefab_tp_range)) - if obj.scs_props.locator_prefab_tp_reset_delay != 0: - textlines.append(str("Reset Delay: %.2f s" % obj.scs_props.locator_prefab_tp_reset_delay)) - if obj.scs_props.locator_prefab_tp_sphere_trigger: - textlines.append("Sphere Trigger: YES") - if obj.scs_props.locator_prefab_tp_partial_activ: - textlines.append("Partial Activation: YES") - if obj.scs_props.locator_prefab_tp_onetime_activ: - textlines.append("One-Time Activation: YES") - if obj.scs_props.locator_prefab_tp_manual_activ: - textlines.append("Manual Activation: YES") - - for textline_i, textline in enumerate(textlines): - y_pos = ((len(textlines) * 15) / 2) + (textline_i * -15) - 7 - _primitive.draw_text(textline, font_id, Vector((mat[0][3], mat[1][3], mat[2][3])), region_data, 0, y_pos) - - if collision_locators: - for key, obj in collision_locators.items(): - mat = obj.matrix_world - textlines = ['"' + key + '"', - str(obj.scs_props.locator_type + " - " + obj.scs_props.locator_collider_type), - str("Mass: " + str(obj.scs_props.locator_collider_mass))] - - # if obj.scs_props.locator_collider_centered: - # textlines.append("Locator Centered") - if obj.scs_props.locator_collider_margin != 0: - textlines.append(str("Margin: " + str(obj.scs_props.locator_collider_margin))) - - for textline_i, textline in enumerate(textlines): - y_pos = ((len(textlines) * 15) / 2) + (textline_i * -15) - 7 - _primitive.draw_text(textline, font_id, Vector((mat[0][3], mat[1][3], mat[2][3])), region_data, 0, y_pos) - - if model_locators: - for key, obj in model_locators.items(): - mat = obj.matrix_world - textlines = ['"' + key + '"', - str(obj.scs_props.locator_type), - str(obj.scs_props.locator_model_hookup)] - - if obj.scs_props.locator_show_preview_model: - textlines.append(str(obj.scs_props.locator_preview_model_path)) - - for textline_i, textline in enumerate(textlines): - y_pos = ((len(textlines) * 15) / 2) + (textline_i * -15) - 7 - _primitive.draw_text(textline, font_id, Vector((mat[0][3], mat[1][3], mat[2][3])), region_data, 0, y_pos) + # LOCATOR COMPREHENSIVE INFO + elif scs_globals.display_info == 'locinfo': + for obj in locators: + loc_2d = _2d_elements_cache.get_locator_location_2d(obj, persp_matrix_str) + loc_info = _2d_elements_cache.get_locator_info(obj) + _primitive.draw_text(loc_info, font_id, loc_2d.x, loc_2d.y) # LOCATOR BOUNDARY NODES elif scs_globals.display_info == 'locnodes': - for key, obj in prefab_locators.items(): + for obj in locators: if obj.scs_props.locator_prefab_type == 'Navigation Point': - mat = obj.matrix_world - _primitive.draw_text(str(obj.scs_props.locator_prefab_np_boundary_node), font_id, - Vector((mat[0][3], mat[1][3], mat[2][3])), region_data) + loc_2d = _2d_elements_cache.get_locator_location_2d(obj, persp_matrix_str) + _primitive.draw_text(str(obj.scs_props.locator_prefab_np_boundary_node), font_id, loc_2d.x, loc_2d.y) # LOCATOR BOUNDARY LANES elif scs_globals.display_info == 'loclanes': - for key, obj in prefab_locators.items(): + for obj in locators: if obj.scs_props.locator_prefab_type == 'Navigation Point': if obj.scs_props.locator_prefab_np_boundary != 'no': - mat = obj.matrix_world np_boundary_i = int(obj.scs_props.locator_prefab_np_boundary) if np_boundary_i == 0: continue - _primitive.draw_text(str(obj.scs_props.enum_np_boundary_items[np_boundary_i][1]), font_id, - Vector((mat[0][3], mat[1][3], mat[2][3])), region_data) - - -def _get_custom_visual_elements(): - """Returns dictionaries of elements, that require custom OpenGL drawing in 3D viewports.""" - prefab_locators = {} - collision_locators = {} - model_locators = {} - - scs_globals = _get_scs_globals() - - # print(' time: %s' % str(time())) - - def set_locators_original_draw_size(obj): - """Set original drawing style for Empty object. - - :param obj: Blender Object - :type obj: bpy.types.Object - """ - # _object.set_attr_if_different(obj, "empty_draw_size", obj.scs_props.locators_orig_draw_size) - if obj.empty_draw_size != obj.scs_props.locators_orig_draw_size: - obj.empty_draw_size = obj.scs_props.locators_orig_draw_size - obj.scs_props.locators_orig_draw_size = 0.0 - if obj.empty_draw_type != obj.scs_props.locators_orig_draw_type: - obj.empty_draw_type = obj.scs_props.locators_orig_draw_type - obj.scs_props.locators_orig_draw_type = '' - - def store_locators_original_draw_size(obj): - """Set Prefab Locator drawing style for Empty object. - - :param obj: Blender Object - :type obj: bpy.types.Object - """ - if obj.scs_props.locators_orig_draw_size == 0.0: - _object_utils.set_attr_if_different(obj.scs_props, "locators_orig_draw_size", obj.empty_draw_size) - _object_utils.set_attr_if_different(obj.scs_props, "locators_orig_draw_type", obj.empty_draw_type) - - def set_locators_prefab_draw_size(obj): - """Set Prefab Locator drawing style for Empty object. - - :param obj: Blender Object - :type obj: bpy.types.Object - """ - new_draw_size = scs_globals.locator_empty_size * scs_globals.locator_size - _object_utils.set_attr_if_different(obj, "empty_draw_size", new_draw_size) - _object_utils.set_attr_if_different(obj, "empty_draw_type", 'PLAIN_AXES') - - def set_locators_model_draw_size(obj): - """Set Model Locator drawing style for Empty object. - - :param obj: Blender Object - :type obj: bpy.types.Object - """ - new_draw_size = scs_globals.locator_empty_size * scs_globals.locator_size - _object_utils.set_attr_if_different(obj, "empty_draw_size", new_draw_size) - _object_utils.set_attr_if_different(obj, "empty_draw_type", 'PLAIN_AXES') - - def set_locators_coll_draw_size(obj): - """Set Collision Locator drawing style for Empty object. - - :param obj: Blender Object - :type obj: bpy.types.Object - """ - new_draw_size = 0.5 - if obj.scs_props.locator_collider_type == 'Box': - bigest_value = max(obj.scs_props.locator_collider_box_x, obj.scs_props.locator_collider_box_y, obj.scs_props.locator_collider_box_z) - new_draw_size = (0.1 * bigest_value) * 12 - elif obj.scs_props.locator_collider_type in ('Sphere', 'Capsule', 'Cylinder'): - new_draw_size = (0.1 * obj.scs_props.locator_collider_dia) * 12 - elif obj.scs_props.locator_collider_type == 'Convex': - bbox = obj.scs_props.get("coll_convex_bbox", None) - bbcenter = obj.scs_props.get("coll_convex_bbcenter", None) - if bbox and bbcenter: - scaling = _math_utils.scaling_width_margin(bbox, obj.scs_props.locator_collider_margin) - val = [] - for axis in range(3): - val.append((bbox[axis] + abs(bbcenter[axis])) * scaling[axis]) - bigest_value = max(val) - new_draw_size = (0.1 * bigest_value) * 12 - _object_utils.set_attr_if_different(obj, "empty_draw_size", new_draw_size) - _object_utils.set_attr_if_different(obj, "empty_draw_type", 'PLAIN_AXES') - - for visib_obj in bpy.context.visible_objects: - # print('object: "%s"' % obj.name) - if visib_obj.type == 'EMPTY' and visib_obj.scs_props.empty_object_type == 'Locator': - - if visib_obj.scs_props.locator_type == 'Prefab': - prefab_locators[visib_obj.name] = visib_obj - if visib_obj.scs_props.locators_orig_draw_size == 0.0: - store_locators_original_draw_size(visib_obj) - set_locators_prefab_draw_size(visib_obj) - elif visib_obj.scs_props.locator_type == 'Model': - model_locators[visib_obj.name] = visib_obj - store_locators_original_draw_size(visib_obj) - set_locators_model_draw_size(visib_obj) - elif visib_obj.scs_props.locator_type == 'Collision': - collision_locators[visib_obj.name] = visib_obj - store_locators_original_draw_size(visib_obj) - set_locators_coll_draw_size(visib_obj) - else: - if visib_obj.scs_props.locators_orig_draw_size != 0.0: - set_locators_original_draw_size(visib_obj) - - # load any lost preview models or switch their layers if needed - if scs_globals.show_preview_models: - if visib_obj.scs_props.locator_preview_model_path != "": - if visib_obj.scs_props.locator_preview_model_present is False: - _preview_models.load(visib_obj) - else: - # identify preview model and alter it's layers array to object layers array - _preview_models.fix_visibility(visib_obj) - - else: - - # fix all preview models which parents are not visible anymore - is_scs_mesh = visib_obj.type == "MESH" and visib_obj.data and "scs_props" in visib_obj.data - if is_scs_mesh and visib_obj.data.scs_props.locator_preview_model_path != "": - - if visib_obj.parent and visib_obj.parent not in bpy.context.visible_objects: - _preview_models.fix_visibility(visib_obj.parent) - - return prefab_locators, collision_locators, model_locators - - -def _draw_3dview_report(region): - """Draws reports in 3d views. - - :param region: region of 3D viewport - :type region: bpy.types.Region - """ - pos = region.height - 62 - show_scroll_controls = _Show3DViewReportOperator.is_scrolled() - - if _Show3DViewReportOperator.has_lines(): - - glEnable(GL_TEXTURE_2D) - glEnable(GL_BLEND) - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) - glBindTexture(GL_TEXTURE_2D, _Show3DViewReportOperator.get_scs_logo_img_bindcode()) - - # draw BT logo - glBegin(GL_POLYGON) - glColor3f(1, 1, 1) - glTexCoord2f(0, 1) - glVertex3f(_OP_consts.View3DReport.BT_LOGO_AREA[0], region.height - _OP_consts.View3DReport.BT_LOGO_AREA[2], 0) - glTexCoord2f(1, 1) - glVertex3f(_OP_consts.View3DReport.BT_LOGO_AREA[1], region.height - _OP_consts.View3DReport.BT_LOGO_AREA[2], 0) - glTexCoord2f(1, 0) - glVertex3f(_OP_consts.View3DReport.BT_LOGO_AREA[1], region.height - _OP_consts.View3DReport.BT_LOGO_AREA[3], 0) - glTexCoord2f(0, 0) - glVertex3f(_OP_consts.View3DReport.BT_LOGO_AREA[0], region.height - _OP_consts.View3DReport.BT_LOGO_AREA[3], 0) - glEnd() - - glDisable(GL_TEXTURE_2D) - - # draw version string - blf.size(0, 10, 72) - glColor3f(.952, .635, .062) - blf.position(0, 20, pos, 0) - blf.draw(0, _info_utils.get_combined_ver_str(only_version_numbers=True)) - pos -= 20 - - # draw actual operator title and message if shown - if _Show3DViewReportOperator.is_shown(): - - blf.size(0, 12, 72) - glColor3f(1, 1, 1) - blf.shadow(0, 5, 0, 0, 0, 1) - - if _Show3DViewReportOperator.get_title() != "": - blf.position(0, 20, pos, 0) - blf.draw(0, _Show3DViewReportOperator.get_title()) - pos -= 15 - - blf.enable(0, blf.SHADOW) - for line in _Show3DViewReportOperator.get_lines(): - - # finish printing if running out of space - if pos - 60 < 0: - blf.position(0, 20, pos, 0) - blf.draw(0, "...") - show_scroll_controls = True - break + loc_2d = _2d_elements_cache.get_locator_location_2d(obj, persp_matrix_str) + _primitive.draw_text(str(obj.scs_props.enum_np_boundary_items[np_boundary_i][1]), font_id, loc_2d.x, loc_2d.y) - blf.position(0, 20, pos, 0) - if "ERROR" in line: - blf.shadow(0, 5, 0.5, 0., 0, 1) - elif "WARNING" in line: - blf.shadow(0, 5, 0.3, 0.15, 0, 1) - - blf.draw(0, line) - pos -= 15 - - blf.disable(0, blf.SHADOW) - - # draw control buttons if controls are enabled - if _Show3DViewReportOperator.has_controls(): - - # draw close button - glColor3f(.4, .4, .4) - glBegin(GL_POLYGON) - glVertex3f(_OP_consts.View3DReport.CLOSE_BTN_AREA[0], region.height - _OP_consts.View3DReport.CLOSE_BTN_AREA[2], 0) - glVertex3f(_OP_consts.View3DReport.CLOSE_BTN_AREA[1], region.height - _OP_consts.View3DReport.CLOSE_BTN_AREA[2], 0) - glVertex3f(_OP_consts.View3DReport.CLOSE_BTN_AREA[1], region.height - _OP_consts.View3DReport.CLOSE_BTN_AREA[3], 0) - glVertex3f(_OP_consts.View3DReport.CLOSE_BTN_AREA[0], region.height - _OP_consts.View3DReport.CLOSE_BTN_AREA[3], 0) - glEnd() - - # draw hide button - glBegin(GL_POLYGON) - glVertex3f(_OP_consts.View3DReport.HIDE_BTN_AREA[0], region.height - _OP_consts.View3DReport.HIDE_BTN_AREA[2], 0) - glVertex3f(_OP_consts.View3DReport.HIDE_BTN_AREA[1], region.height - _OP_consts.View3DReport.HIDE_BTN_AREA[2], 0) - glVertex3f(_OP_consts.View3DReport.HIDE_BTN_AREA[1], region.height - _OP_consts.View3DReport.HIDE_BTN_AREA[3], 0) - glVertex3f(_OP_consts.View3DReport.HIDE_BTN_AREA[0], region.height - _OP_consts.View3DReport.HIDE_BTN_AREA[3], 0) - glEnd() - - # gather texts and positions - close_btn_text_pos = _OP_consts.View3DReport.CLOSE_BTN_TEXT_POS[int(not _Show3DViewReportOperator.is_shown())] - close_btn_text = _OP_consts.View3DReport.CLOSE_BTN_TEXT[int(not _Show3DViewReportOperator.is_shown())] - - hide_btn_text_pos = _OP_consts.View3DReport.HIDE_BTN_TEXT_POS[int(not _Show3DViewReportOperator.is_shown())] - hide_btn_text = _OP_consts.View3DReport.HIDE_BTN_TEXT[int(not _Show3DViewReportOperator.is_shown())] - - blf.size(0, 15, 72) - - # draw close button text - glColor3f(1, 1, 1) - blf.position(0, close_btn_text_pos[0], region.height - close_btn_text_pos[1], 0) - blf.draw(0, close_btn_text) - - # 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) + blf.disable(font_id, blf.WORD_WRAP) diff --git a/addon/io_scs_tools/internals/open_gl/locators/collider.py b/addon/io_scs_tools/internals/open_gl/locators/collider.py index 11e1387..83a98e2 100644 --- a/addon/io_scs_tools/internals/open_gl/locators/collider.py +++ b/addon/io_scs_tools/internals/open_gl/locators/collider.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software from io_scs_tools.internals.open_gl import primitive as _primitive from mathutils import Matrix @@ -38,10 +38,10 @@ def draw_shape_box(mat, obj_scs_props, scs_globals): else: shift = -obj_scs_props.locator_collider_box_y / 2 - mat1 = mat * Matrix.Translation((0.0, shift, 0.0)) * ( - Matrix.Scale(obj_scs_props.locator_collider_box_x, 4, (1.0, 0.0, 0.0)) * - Matrix.Scale(obj_scs_props.locator_collider_box_y, 4, (0.0, 1.0, 0.0)) * - Matrix.Scale(obj_scs_props.locator_collider_box_z, 4, (0.0, 0.0, 1.0))) + mat1 = mat @ Matrix.Translation((0.0, shift, 0.0)) @ ( + Matrix.Scale(obj_scs_props.locator_collider_box_x, 4, (1.0, 0.0, 0.0)) @ + Matrix.Scale(obj_scs_props.locator_collider_box_y, 4, (0.0, 1.0, 0.0)) @ + Matrix.Scale(obj_scs_props.locator_collider_box_z, 4, (0.0, 0.0, 1.0))) cube_vertices, cube_faces, cube_wire_lines = _primitive.get_box_data() @@ -51,8 +51,8 @@ def draw_shape_box(mat, obj_scs_props, scs_globals): scs_globals.locator_coll_face_color, obj_scs_props.locator_collider_faces, obj_scs_props.locator_collider_wires, - cube_wire_lines, - scs_globals.locator_coll_wire_color) + wire_lines=cube_wire_lines, + wire_color=scs_globals.locator_coll_wire_color) def draw_shape_sphere(mat, obj_scs_props, scs_globals): @@ -70,7 +70,7 @@ def draw_shape_sphere(mat, obj_scs_props, scs_globals): shift = 0.0 else: shift = -obj_scs_props.locator_collider_dia / 2 - mat1 = mat * Matrix.Translation((0.0, shift, 0.0)) * Matrix.Scale(obj_scs_props.locator_collider_dia, 4) + mat1 = mat @ Matrix.Translation((0.0, shift, 0.0)) @ Matrix.Scale(obj_scs_props.locator_collider_dia, 4) sphere_vertices, sphere_faces, sphere_wire_lines = _primitive.get_sphere_data() @@ -80,8 +80,8 @@ def draw_shape_sphere(mat, obj_scs_props, scs_globals): scs_globals.locator_coll_face_color, obj_scs_props.locator_collider_faces, obj_scs_props.locator_collider_wires, - sphere_wire_lines, - scs_globals.locator_coll_wire_color) + wire_lines=sphere_wire_lines, + wire_color=scs_globals.locator_coll_wire_color) def draw_shape_capsule(mat, obj_scs_props, scs_globals): @@ -99,23 +99,23 @@ def draw_shape_capsule(mat, obj_scs_props, scs_globals): shift = obj_scs_props.locator_collider_len / 2 else: shift = 0.0 - mat1 = Matrix.Translation((0.0, shift, 0.0)) * Matrix.Scale(obj_scs_props.locator_collider_dia, 4) - mat2 = Matrix.Translation((0.0, shift - obj_scs_props.locator_collider_len, 0.0)) * Matrix.Scale(obj_scs_props.locator_collider_dia, 4) + mat1 = Matrix.Translation((0.0, shift, 0.0)) @ Matrix.Scale(obj_scs_props.locator_collider_dia, 4) + mat2 = Matrix.Translation((0.0, shift - obj_scs_props.locator_collider_len, 0.0)) @ Matrix.Scale(obj_scs_props.locator_collider_dia, 4) capsule_vertices, capsule_faces, capsule_wire_lines = _primitive.get_capsule_data() face_transforms = [] - vertices = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, - 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44) + vertices = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44} face_transforms.append((mat1, vertices), ) - vertices = (45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, - 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89) + vertices = {45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, + 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89} face_transforms.append((mat2, vertices), ) wire_transforms = [] - vertices = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 26, 27, 28, 29, 30, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 59, 60, 61, 62, 63) + vertices = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 26, 27, 28, 29, 30, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 59, 60, 61, 62, 63} wire_transforms.append((mat1, vertices), ) - vertices = (13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 31, 32, 33, 34, 35, 36, 37, 38, 39, 50, 51, 52, 53, 54, 55, 56, 57, 58) + vertices = {13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 31, 32, 33, 34, 35, 36, 37, 38, 39, 50, 51, 52, 53, 54, 55, 56, 57, 58} wire_transforms.append((mat2, vertices), ) _primitive.draw_polygon_object(mat, @@ -124,10 +124,10 @@ def draw_shape_capsule(mat, obj_scs_props, scs_globals): scs_globals.locator_coll_face_color, obj_scs_props.locator_collider_faces, obj_scs_props.locator_collider_wires, - capsule_wire_lines, - scs_globals.locator_coll_wire_color, - face_transforms, - wire_transforms) + wire_lines=capsule_wire_lines, + wire_color=scs_globals.locator_coll_wire_color, + face_transforms=face_transforms, + wire_transforms=wire_transforms) def draw_shape_cylinder(mat, obj_scs_props, scs_globals): @@ -145,21 +145,21 @@ def draw_shape_cylinder(mat, obj_scs_props, scs_globals): shift = obj_scs_props.locator_collider_len / 2 else: shift = 0.0 - mat1 = Matrix.Translation((0.0, shift, 0.0)) * Matrix.Scale(obj_scs_props.locator_collider_dia, 4) - mat2 = Matrix.Translation((0.0, shift - obj_scs_props.locator_collider_len, 0.0)) * Matrix.Scale(obj_scs_props.locator_collider_dia, 4) + mat1 = Matrix.Translation((0.0, shift, 0.0)) @ Matrix.Scale(obj_scs_props.locator_collider_dia, 4) + mat2 = Matrix.Translation((0.0, shift - obj_scs_props.locator_collider_len, 0.0)) @ Matrix.Scale(obj_scs_props.locator_collider_dia, 4) cylinder_vertices, cylinder_faces, cylinder_wire_lines = _primitive.get_cylinder_data() face_transforms = [] - vertices = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) + vertices = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} face_transforms.append((mat1, vertices), ) - vertices = (12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23) + vertices = {12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23} face_transforms.append((mat2, vertices), ) wire_transforms = [] - vertices = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 26, 28, 30, 32) + vertices = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 26, 28, 30, 32} wire_transforms.append((mat1, vertices), ) - vertices = (13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 27, 29, 31, 33) + vertices = {13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 27, 29, 31, 33} wire_transforms.append((mat2, vertices), ) _primitive.draw_polygon_object(mat, @@ -168,10 +168,10 @@ def draw_shape_cylinder(mat, obj_scs_props, scs_globals): scs_globals.locator_coll_face_color, obj_scs_props.locator_collider_faces, obj_scs_props.locator_collider_wires, - cylinder_wire_lines, - scs_globals.locator_coll_wire_color, - face_transforms, - wire_transforms) + wire_lines=cylinder_wire_lines, + wire_color=scs_globals.locator_coll_wire_color, + face_transforms=face_transforms, + wire_transforms=wire_transforms) def draw_shape_convex(mat, obj_scs_props, scs_globals): @@ -194,8 +194,7 @@ def draw_shape_convex(mat, obj_scs_props, scs_globals): scs_globals.locator_coll_face_color, obj_scs_props.locator_collider_faces, obj_scs_props.locator_collider_wires, - None, - scs_globals.locator_coll_wire_color) + wire_color=scs_globals.locator_coll_wire_color) def draw_collision_locator(obj, scs_globals): @@ -208,7 +207,7 @@ def draw_collision_locator(obj, scs_globals): """ tran, rot, sca = obj.matrix_world.decompose() - mat_orig = Matrix.Translation(tran).to_4x4() * rot.to_matrix().to_4x4() + mat_orig = Matrix.Translation(tran).to_4x4() @ rot.to_matrix().to_4x4() if obj.scs_props.locator_collider_type == 'Box': draw_shape_box(mat_orig, obj.scs_props, scs_globals) @@ -219,4 +218,24 @@ def draw_collision_locator(obj, scs_globals): if obj.scs_props.locator_collider_type == 'Cylinder': draw_shape_cylinder(mat_orig, obj.scs_props, scs_globals) if obj.scs_props.locator_collider_type == 'Convex': - draw_shape_convex(mat_orig, obj.scs_props, scs_globals) \ No newline at end of file + draw_shape_convex(mat_orig, obj.scs_props, scs_globals) + + +def get_collision_locator_comprehensive_info(obj): + """Gets comprehensive info from collisiion locator. + + :param obj: collision locator to get infos from + :type obj: bpy.types.Object + :return: formatted string ready to be drawn with blf.draw() + :rtype: str + """ + textlines = ['"%s"' % obj.name, + "%s - %s" % (obj.scs_props.locator_type, obj.scs_props.locator_collider_type), + "Mass: %.2f" % obj.scs_props.locator_collider_mass] + + # if obj.scs_props.locator_collider_centered: + # textlines.append("Locator Centered") + if obj.scs_props.locator_collider_margin != 0: + textlines.append("Margin: %.2f" % obj.scs_props.locator_collider_margin) + + return "\n".join(textlines) diff --git a/addon/io_scs_tools/internals/open_gl/locators/model.py b/addon/io_scs_tools/internals/open_gl/locators/model.py index 125d05d..d3a4a50 100644 --- a/addon/io_scs_tools/internals/open_gl/locators/model.py +++ b/addon/io_scs_tools/internals/open_gl/locators/model.py @@ -16,9 +16,8 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software -from bgl import (glColor3f, glVertex3f, glLineWidth, glBegin, glEnd, GL_LINES) from io_scs_tools.internals.open_gl import primitive as _primitive from mathutils import Vector, Matrix @@ -30,18 +29,19 @@ def draw_shape_model_locator(mat, scs_globals): :param scs_globals: :return: """ - glLineWidth(2.0) - glBegin(GL_LINES) - glColor3f(scs_globals.locator_model_wire_color.r, scs_globals.locator_model_wire_color.g, - scs_globals.locator_model_wire_color.b) - glVertex3f(*(mat * Vector((0.0, scs_globals.locator_empty_size * 1, 0.0)))) - glVertex3f(*(mat * Vector((0.0, 0.75, 0.0)))) - glVertex3f(*(mat * Vector((-0.15, 0.45, 0.0)))) - glVertex3f(*(mat * Vector((0.0, 0.75, 0.0)))) - glVertex3f(*(mat * Vector((0.0, 0.75, 0.0)))) - glVertex3f(*(mat * Vector((0.15, 0.45, 0.0)))) - glEnd() - glLineWidth(1.0) + color = ( + scs_globals.locator_model_wire_color.r, + scs_globals.locator_model_wire_color.g, + scs_globals.locator_model_wire_color.b, + 1.0 + ) + + _primitive.append_line_vertex((mat @ Vector((0.0, scs_globals.locator_empty_size * 1, 0.0))), color) + _primitive.append_line_vertex((mat @ Vector((0.0, 0.75, 0.0))), color) + _primitive.append_line_vertex((mat @ Vector((-0.15, 0.45, 0.0))), color) + _primitive.append_line_vertex((mat @ Vector((0.0, 0.75, 0.0))), color) + _primitive.append_line_vertex((mat @ Vector((0.0, 0.75, 0.0))), color) + _primitive.append_line_vertex((mat @ Vector((0.15, 0.45, 0.0))), color) def draw_model_box(mat, scs_globals): @@ -52,9 +52,9 @@ def draw_model_box(mat, scs_globals): :return: """ - mat1 = mat * (Matrix.Translation((0.0, 0.0, 0.0)) * - Matrix.Scale(scs_globals.locator_size / 5, 4, (1.0, 0.0, 0.0)) * - Matrix.Scale(scs_globals.locator_size / 5, 4, (0.0, 1.0, 0.0)) * + mat1 = mat @ (Matrix.Translation((0.0, 0.0, 0.0)) @ + Matrix.Scale(scs_globals.locator_size / 5, 4, (1.0, 0.0, 0.0)) @ + Matrix.Scale(scs_globals.locator_size / 5, 4, (0.0, 1.0, 0.0)) @ Matrix.Scale(scs_globals.locator_size / 5, 4, (0.0, 0.0, 1.0))) cube_vertices, cube_faces, cube_wire_lines = _primitive.get_box_data() @@ -65,8 +65,8 @@ def draw_model_box(mat, scs_globals): scs_globals.locator_coll_face_color, False, True, - cube_wire_lines, - scs_globals.locator_model_wire_color) + wire_lines=cube_wire_lines, + wire_color=scs_globals.locator_model_wire_color) def draw_model_locator(obj, scs_globals): @@ -81,10 +81,28 @@ def draw_model_locator(obj, scs_globals): empty_size = scs_globals.locator_empty_size mat_sca = mathutils.Matrix.Scale(size, 4) mat_orig = obj.matrix_world - mat = mat_orig * mat_sca + mat = mat_orig @ mat_sca _primitive.draw_shape_x_axis(mat, empty_size) _primitive.draw_shape_y_axis_neg(mat, empty_size) _primitive.draw_shape_z_axis(mat, empty_size) draw_shape_model_locator(mat, scs_globals) if not obj.scs_props.locator_preview_model_present: draw_model_box(mat_orig, scs_globals) + + +def get_model_locator_comprehensive_info(obj): + """Gets comprehensive info from model locator. + + :param obj: model locator to get infos from + :type obj: bpy.types.Object + :return: formatted string ready to be drawn with blf.draw() + :rtype: str + """ + textlines = ['"%s"' % obj.name, + "%s" % obj.scs_props.locator_type, + "Hookup: %s" % obj.scs_props.locator_model_hookup] + + if obj.scs_props.locator_show_preview_model and obj.scs_props.locator_preview_model_present: + textlines.append("Preview Model: %s" % obj.scs_props.locator_preview_model_path) + + return "\n".join(textlines) diff --git a/addon/io_scs_tools/internals/open_gl/locators/prefab.py b/addon/io_scs_tools/internals/open_gl/locators/prefab.py index 6c8ae78..0b60ba7 100644 --- a/addon/io_scs_tools/internals/open_gl/locators/prefab.py +++ b/addon/io_scs_tools/internals/open_gl/locators/prefab.py @@ -16,12 +16,9 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software - -from bgl import (glEnable, glDisable, glColor3f, glVertex3f, glPointSize, - glLineWidth, glBegin, glEnd, GL_POINTS, - GL_LINE_STRIP, GL_LINES, GL_LINE_LOOP, GL_LINE_STIPPLE) +# Copyright (C) 2013-2019: SCS Software +from io_scs_tools.consts import PrefabLocators as _PL_consts from io_scs_tools.internals.open_gl import primitive as _primitive from mathutils import Vector, Matrix @@ -33,19 +30,20 @@ def draw_shape_control_node(mat, scs_globals): :param scs_globals: :return: """ - glLineWidth(2.0) - glBegin(GL_LINES) - glColor3f(scs_globals.locator_prefab_wire_color.r, - scs_globals.locator_prefab_wire_color.g, - scs_globals.locator_prefab_wire_color.b) - glVertex3f(*(mat * Vector((0.0, scs_globals.locator_empty_size, 0.0)))) - glVertex3f(*(mat * Vector((0.0, 0.75, 0.0)))) - glVertex3f(*(mat * Vector((-0.15, 0.45, 0.0)))) - glVertex3f(*(mat * Vector((0.0, 0.75, 0.0)))) - glVertex3f(*(mat * Vector((0.0, 0.75, 0.0)))) - glVertex3f(*(mat * Vector((0.15, 0.45, 0.0)))) - glEnd() - glLineWidth(1.0) + + color = ( + scs_globals.locator_prefab_wire_color.r, + scs_globals.locator_prefab_wire_color.g, + scs_globals.locator_prefab_wire_color.b, + 1.0 + ) + + _primitive.append_line_vertex((mat @ Vector((0.0, scs_globals.locator_empty_size, 0.0))), color) + _primitive.append_line_vertex((mat @ Vector((0.0, 0.75, 0.0))), color) + _primitive.append_line_vertex((mat @ Vector((-0.15, 0.45, 0.0))), color) + _primitive.append_line_vertex((mat @ Vector((0.0, 0.75, 0.0))), color) + _primitive.append_line_vertex((mat @ Vector((0.0, 0.75, 0.0))), color) + _primitive.append_line_vertex((mat @ Vector((0.15, 0.45, 0.0))), color) def draw_shape_sign(mat, scs_globals): @@ -55,24 +53,24 @@ def draw_shape_sign(mat, scs_globals): :param scs_globals: :return: """ - glLineWidth(2.0) - glBegin(GL_LINES) - glColor3f(scs_globals.locator_prefab_wire_color.r, - scs_globals.locator_prefab_wire_color.g, - scs_globals.locator_prefab_wire_color.b) - glVertex3f(*(mat * Vector((0.0, 0.0, scs_globals.locator_empty_size)))) - glVertex3f(*(mat * Vector((0.0, 0.0, 0.45)))) - glEnd() - glBegin(GL_LINE_STRIP) - glVertex3f(*(mat * Vector((0.0, 0.0, 0.75)))) - glVertex3f(*(mat * Vector((0.1299, 0.0, 0.675)))) - glVertex3f(*(mat * Vector((0.1299, 0.0, 0.525)))) - glVertex3f(*(mat * Vector((0.0, 0.0, 0.45)))) - glVertex3f(*(mat * Vector((-0.1299, 0.0, 0.525)))) - glVertex3f(*(mat * Vector((-0.1299, 0.0, 0.675)))) - glVertex3f(*(mat * Vector((0.0, 0.0, 0.75)))) - glEnd() - glLineWidth(1.0) + + color = ( + scs_globals.locator_prefab_wire_color.r, + scs_globals.locator_prefab_wire_color.g, + scs_globals.locator_prefab_wire_color.b, + 1.0 + ) + + _primitive.append_line_vertex((mat @ Vector((0.0, 0.0, scs_globals.locator_empty_size))), color) + _primitive.append_line_vertex((mat @ Vector((0.0, 0.0, 0.45))), color) + + _primitive.append_line_vertex((mat @ Vector((0.0, 0.0, 0.75))), color) + _primitive.append_line_vertex((mat @ Vector((0.1299, 0.0, 0.675))), color, is_strip=True) + _primitive.append_line_vertex((mat @ Vector((0.1299, 0.0, 0.525))), color, is_strip=True) + _primitive.append_line_vertex((mat @ Vector((0.0, 0.0, 0.45))), color, is_strip=True) + _primitive.append_line_vertex((mat @ Vector((-0.1299, 0.0, 0.525))), color, is_strip=True) + _primitive.append_line_vertex((mat @ Vector((-0.1299, 0.0, 0.675))), color, is_strip=True) + _primitive.append_line_vertex((mat @ Vector((0.0, 0.0, 0.75))), color) def draw_shape_spawn_point(mat, scs_globals): @@ -82,23 +80,24 @@ def draw_shape_spawn_point(mat, scs_globals): :param scs_globals: :return: """ - glLineWidth(2.0) - glBegin(GL_LINES) - glColor3f(scs_globals.locator_prefab_wire_color.r, - scs_globals.locator_prefab_wire_color.g, - scs_globals.locator_prefab_wire_color.b) - glVertex3f(*(mat * Vector((0.0, 0.0, scs_globals.locator_empty_size)))) - glVertex3f(*(mat * Vector((0.0, 0.0, 0.75)))) - glVertex3f(*(mat * Vector((-0.1299, 0.0, 0.525)))) - glVertex3f(*(mat * Vector((0.1299, 0.0, 0.675)))) - glVertex3f(*(mat * Vector((0.1299, 0.0, 0.525)))) - glVertex3f(*(mat * Vector((-0.1299, 0.0, 0.675)))) - glVertex3f(*(mat * Vector((0.0, -0.1299, 0.525)))) - glVertex3f(*(mat * Vector((0.0, 0.1299, 0.675)))) - glVertex3f(*(mat * Vector((0.0, 0.1299, 0.525)))) - glVertex3f(*(mat * Vector((0.0, -0.1299, 0.675)))) - glEnd() - glLineWidth(1.0) + + color = ( + scs_globals.locator_prefab_wire_color.r, + scs_globals.locator_prefab_wire_color.g, + scs_globals.locator_prefab_wire_color.b, + 1.0 + ) + + _primitive.append_line_vertex((mat @ Vector((0.0, 0.0, scs_globals.locator_empty_size))), color) + _primitive.append_line_vertex((mat @ Vector((0.0, 0.0, 0.75))), color) + _primitive.append_line_vertex((mat @ Vector((-0.1299, 0.0, 0.525))), color) + _primitive.append_line_vertex((mat @ Vector((0.1299, 0.0, 0.675))), color) + _primitive.append_line_vertex((mat @ Vector((0.1299, 0.0, 0.525))), color) + _primitive.append_line_vertex((mat @ Vector((-0.1299, 0.0, 0.675))), color) + _primitive.append_line_vertex((mat @ Vector((0.0, -0.1299, 0.525))), color) + _primitive.append_line_vertex((mat @ Vector((0.0, 0.1299, 0.675))), color) + _primitive.append_line_vertex((mat @ Vector((0.0, 0.1299, 0.525))), color) + _primitive.append_line_vertex((mat @ Vector((0.0, -0.1299, 0.675))), color) def draw_shape_traffic_light(mat, scs_globals): @@ -107,30 +106,31 @@ def draw_shape_traffic_light(mat, scs_globals): :param mat: :return: """ - glLineWidth(2.0) - glBegin(GL_LINE_STRIP) - glColor3f(scs_globals.locator_prefab_wire_color.r, - scs_globals.locator_prefab_wire_color.g, - scs_globals.locator_prefab_wire_color.b) - glVertex3f(*(mat * Vector((0.0, 0.0, scs_globals.locator_empty_size)))) - glVertex3f(*(mat * Vector((0.0, 0.0, 0.45)))) - glVertex3f(*(mat * Vector((-0.0866, 0.0, 0.5)))) - glVertex3f(*(mat * Vector((-0.0866, 0.0, 0.84)))) - glVertex3f(*(mat * Vector((0.0, 0.0, 0.89)))) - glVertex3f(*(mat * Vector((0.0866, 0.0, 0.84)))) - glVertex3f(*(mat * Vector((0.0866, 0.0, 0.5)))) - glVertex3f(*(mat * Vector((0.0, 0.0, 0.45)))) - glEnd() + + color = ( + scs_globals.locator_prefab_wire_color.r, + scs_globals.locator_prefab_wire_color.g, + scs_globals.locator_prefab_wire_color.b, + 1.0 + ) + + _primitive.append_line_vertex((mat @ Vector((0.0, 0.0, scs_globals.locator_empty_size))), color) + _primitive.append_line_vertex((mat @ Vector((0.0, 0.0, 0.45))), color, is_strip=True) + _primitive.append_line_vertex((mat @ Vector((-0.0866, 0.0, 0.5))), color, is_strip=True) + _primitive.append_line_vertex((mat @ Vector((-0.0866, 0.0, 0.84))), color, is_strip=True) + _primitive.append_line_vertex((mat @ Vector((0.0, 0.0, 0.89))), color, is_strip=True) + _primitive.append_line_vertex((mat @ Vector((0.0866, 0.0, 0.84))), color, is_strip=True) + _primitive.append_line_vertex((mat @ Vector((0.0866, 0.0, 0.5))), color, is_strip=True) + _primitive.append_line_vertex((mat @ Vector((0.0, 0.0, 0.45))), color) + for val in (0.5, 0.62, 0.74): - glBegin(GL_LINE_LOOP) - glVertex3f(*(mat * Vector((0.0, 0.0, val)))) - glVertex3f(*(mat * Vector((-0.0433, 0.0, 0.025 + val)))) - glVertex3f(*(mat * Vector((-0.0433, 0.0, 0.075 + val)))) - glVertex3f(*(mat * Vector((0.0, 0.0, 0.1 + val)))) - glVertex3f(*(mat * Vector((0.0433, 0.0, 0.075 + val)))) - glVertex3f(*(mat * Vector((0.0433, 0.0, 0.025 + val)))) - glEnd() - glLineWidth(1.0) + _primitive.append_line_vertex((mat @ Vector((0.0, 0.0, val))), color) + _primitive.append_line_vertex((mat @ Vector((-0.0433, 0.0, 0.025 + val))), color, is_strip=True) + _primitive.append_line_vertex((mat @ Vector((-0.0433, 0.0, 0.075 + val))), color, is_strip=True) + _primitive.append_line_vertex((mat @ Vector((0.0, 0.0, 0.1 + val))), color, is_strip=True) + _primitive.append_line_vertex((mat @ Vector((0.0433, 0.0, 0.075 + val))), color, is_strip=True) + _primitive.append_line_vertex((mat @ Vector((0.0433, 0.0, 0.025 + val))), color, is_strip=True) + _primitive.append_line_vertex((mat @ Vector((0.0, 0.0, val))), color) def draw_shape_map_point(mat, scs_globals): @@ -140,21 +140,22 @@ def draw_shape_map_point(mat, scs_globals): :param scs_globals: :return: """ - glLineWidth(2.0) - glBegin(GL_LINES) - glColor3f(scs_globals.locator_prefab_wire_color.r, - scs_globals.locator_prefab_wire_color.g, - scs_globals.locator_prefab_wire_color.b) - glVertex3f(*(mat * Vector((-0.17678, -0.17678, 0.17678)))) - glVertex3f(*(mat * Vector((0.17678, 0.17678, -0.17678)))) - glVertex3f(*(mat * Vector((-0.17678, 0.17678, 0.17678)))) - glVertex3f(*(mat * Vector((0.17678, -0.17678, -0.17678)))) - glVertex3f(*(mat * Vector((-0.17678, -0.17678, -0.17678)))) - glVertex3f(*(mat * Vector((0.17678, 0.17678, 0.17678)))) - glVertex3f(*(mat * Vector((-0.17678, 0.17678, -0.17678)))) - glVertex3f(*(mat * Vector((0.17678, -0.17678, 0.17678)))) - glEnd() - glLineWidth(1.0) + + color = ( + scs_globals.locator_prefab_wire_color.r, + scs_globals.locator_prefab_wire_color.g, + scs_globals.locator_prefab_wire_color.b, + 1.0 + ) + + _primitive.append_line_vertex((mat @ Vector((-0.17678, -0.17678, 0.17678))), color) + _primitive.append_line_vertex((mat @ Vector((0.17678, 0.17678, -0.17678))), color) + _primitive.append_line_vertex((mat @ Vector((-0.17678, 0.17678, 0.17678))), color) + _primitive.append_line_vertex((mat @ Vector((0.17678, -0.17678, -0.17678))), color) + _primitive.append_line_vertex((mat @ Vector((-0.17678, -0.17678, -0.17678))), color) + _primitive.append_line_vertex((mat @ Vector((0.17678, 0.17678, 0.17678))), color) + _primitive.append_line_vertex((mat @ Vector((-0.17678, 0.17678, -0.17678))), color) + _primitive.append_line_vertex((mat @ Vector((0.17678, -0.17678, 0.17678))), color) def draw_shape_trigger_point(mat, mat_orig, radius, scs_globals, draw_range): @@ -167,14 +168,11 @@ def draw_shape_trigger_point(mat, mat_orig, radius, scs_globals, draw_range): :param draw_range: :return: """ - glLineWidth(2.0) _primitive.draw_circle(0.25, 8, mat, scs_globals) _primitive.draw_circle(0.4, 8, mat, scs_globals) - glEnable(GL_LINE_STIPPLE) + if draw_range: _primitive.draw_circle(radius, 32, mat_orig, scs_globals) - glDisable(GL_LINE_STIPPLE) - glLineWidth(1.0) def draw_prefab_locator(obj, scs_globals): @@ -188,22 +186,18 @@ def draw_prefab_locator(obj, scs_globals): empty_size = scs_globals.locator_empty_size mat_sca = Matrix.Scale(size, 4) mat_orig = obj.matrix_world - mat = mat_orig * mat_sca + mat = mat_orig @ mat_sca if obj.scs_props.locator_prefab_type == 'Control Node': _primitive.draw_shape_x_axis(mat, empty_size) _primitive.draw_shape_y_axis_neg(mat, empty_size) _primitive.draw_shape_z_axis(mat, empty_size) draw_shape_control_node(mat, scs_globals) - glPointSize(12.0) - glBegin(GL_POINTS) if obj.scs_props.locator_prefab_con_node_index == '0': - glColor3f(1, 0, 0) + color = (1, 0, 0, 1) else: - glColor3f(0, 1, 0) - glVertex3f(*(mat * Vector((0.0, 0.0, 0.0)))) - glEnd() - glPointSize(1.0) + color = (0, 1, 0, 1) + _primitive.draw_point((mat @ Vector((0.0, 0.0, 0.0))), color, 12.0) elif obj.scs_props.locator_prefab_type == 'Sign': _primitive.draw_shape_x_axis(mat, empty_size) @@ -244,3 +238,94 @@ def draw_prefab_locator(obj, scs_globals): _primitive.draw_shape_z_axis(mat, empty_size) is_sphere = obj.scs_props.locator_prefab_tp_sphere_trigger draw_shape_trigger_point(mat, mat_orig, obj.scs_props.locator_prefab_tp_range, scs_globals, is_sphere) + + +def get_prefab_locator_comprehensive_info(obj): + """Gets comprehensive info from prefab locator. + + :param obj: prefab locator to get infos from + :type obj: bpy.types.Object + :return: formatted string ready to be drawn with blf.draw() + :rtype: str + """ + textlines = ['"%s"' % obj.name, + "%s - %s" % (obj.scs_props.locator_type, obj.scs_props.locator_prefab_type)] + + if obj.scs_props.locator_prefab_type == 'Control Node': + + textlines.append("Node Index: %s" % obj.scs_props.locator_prefab_con_node_index) + + elif obj.scs_props.locator_prefab_type == 'Spawn Point': + + spawn_type_i = int(obj.scs_props.locator_prefab_spawn_type) + textlines.append("Type: %s" % obj.scs_props.enum_spawn_type_items[spawn_type_i][1]) + + elif obj.scs_props.locator_prefab_type == 'Traffic Semaphore': + + textlines.append("ID: %s" % obj.scs_props.locator_prefab_tsem_id) + if obj.scs_props.locator_prefab_tsem_profile != '': + textlines.append("Profile: %s" % obj.scs_props.locator_prefab_tsem_profile) + tsem_type_i = int(obj.scs_props.locator_prefab_tsem_type) + if tsem_type_i != _PL_consts.TST.PROFILE: + textlines.append("Type: %s" + obj.scs_props.enum_tsem_type_items[tsem_type_i][1]) + textlines.append("G: %.2f" % obj.scs_props.locator_prefab_tsem_gs + + " - O: %.2f" % obj.scs_props.locator_prefab_tsem_os1 + + " - R: %.2f" % obj.scs_props.locator_prefab_tsem_rs + + " - O: %.2f" % obj.scs_props.locator_prefab_tsem_os2) + if obj.scs_props.locator_prefab_tsem_cyc_delay != 0: + textlines.append("Cycle Delay: %.2f s" % obj.scs_props.locator_prefab_tsem_cyc_delay) + + elif obj.scs_props.locator_prefab_type == 'Navigation Point': + + np_boundary_i = int(obj.scs_props.locator_prefab_np_boundary) + if np_boundary_i != 0: + textlines.append("Boundary: %s" % obj.scs_props.enum_np_boundary_items[np_boundary_i][1]) + + textlines.append("B. Node: %s" % obj.scs_props.locator_prefab_np_boundary_node) + if obj.scs_props.locator_prefab_np_traffic_semaphore != '-1': + textlines.append("T. Light ID: %s" % obj.scs_props.locator_prefab_np_traffic_semaphore) + + elif obj.scs_props.locator_prefab_type == 'Map Point': + + if obj.scs_props.locator_prefab_mp_road_over: + textlines.append("Road Over: YES") + if obj.scs_props.locator_prefab_mp_no_outline: + textlines.append("No Outline: YES") + if obj.scs_props.locator_prefab_mp_no_arrow: + textlines.append("No Arrow: YES") + if obj.scs_props.locator_prefab_mp_prefab_exit: + textlines.append("Prefab Exit: YES") + + road_size_i = int(obj.scs_props.locator_prefab_mp_road_size) + textlines.append("Road Size: %s" % obj.scs_props.enum_mp_road_size_items[road_size_i][1]) + + road_offset_i = int(obj.scs_props.locator_prefab_mp_road_offset) + if road_offset_i != _PL_consts.MPVF.ROAD_OFFSET_0: + textlines.append("Offset: %s" % obj.scs_props.enum_mp_road_offset_items[road_offset_i][1]) + + custom_color_i = int(obj.scs_props.locator_prefab_mp_custom_color) + if custom_color_i != 0: + textlines.append("Custom Color: %s" % obj.scs_props.enum_mp_custom_color_items[custom_color_i][1]) + + assigned_node_i = int(obj.scs_props.locator_prefab_mp_assigned_node) + if assigned_node_i != 0: + textlines.append("Node: %s" % obj.scs_props.enum_mp_assigned_node_items[assigned_node_i][1]) + + if len(obj.scs_props.locator_prefab_mp_dest_nodes) > 0: + textlines.append("Destination Nodes: %s" % " ".join(obj.scs_props.locator_prefab_mp_dest_nodes)) + + elif obj.scs_props.locator_prefab_type == 'Trigger Point': + + textlines.append("Range: %.2f m" % obj.scs_props.locator_prefab_tp_range) + if obj.scs_props.locator_prefab_tp_reset_delay != 0: + textlines.append("Reset Delay: %.2f s" % obj.scs_props.locator_prefab_tp_reset_delay) + if obj.scs_props.locator_prefab_tp_sphere_trigger: + textlines.append("Sphere Trigger: YES") + if obj.scs_props.locator_prefab_tp_partial_activ: + textlines.append("Partial Activation: YES") + if obj.scs_props.locator_prefab_tp_onetime_activ: + textlines.append("One-Time Activation: YES") + if obj.scs_props.locator_prefab_tp_manual_activ: + textlines.append("Manual Activation: YES") + + return "\n".join(textlines) diff --git a/addon/io_scs_tools/internals/open_gl/primitive.py b/addon/io_scs_tools/internals/open_gl/primitive.py index 841b2f5..e3c5550 100644 --- a/addon/io_scs_tools/internals/open_gl/primitive.py +++ b/addon/io_scs_tools/internals/open_gl/primitive.py @@ -16,14 +16,354 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software +import bgl import blf -import bpy -from bgl import (glEnable, glDisable, glColor3f, glVertex3f, glPointSize, - glLineWidth, glBegin, glEnd, GL_POINTS, - GL_LINE_STRIP, GL_LINES, GL_LINE_LOOP, GL_POLYGON, GL_LINE_STIPPLE) +import gpu +from array import array +from gpu_extras.batch import batch_for_shader from mathutils import Vector +from io_scs_tools.internals.open_gl.shaders import get_shader, ShaderTypes + + +class _Buffer: + """Buffer class being able to store and dispatch drawing of primitives.""" + + class Types: + POINTS = 1 + LINES = 2 + TRIS = 3 + + def __init__(self, buffer_type, draw_size, shader_type, attr_names): + """Create buffer instance with given type drawing size and shader type. + + :param buffer_type: type of the buffer from _Buffer.Types + :type buffer_type: int + :param draw_size: size of the drawing elements + :type draw_size: float + :param shader_type: type of the shader for given buffer from ShaderTypes + :type shader_type: int + :param attr_names: tuple of string defining attributes that this buffer is holding + :type attr_names: tuple[str] + """ + if buffer_type not in {_Buffer.Types.LINES, _Buffer.Types.POINTS, _Buffer.Types.TRIS}: + raise TypeError("Unsupported buffer type requested: %s!" % buffer_type) + + self.__type = buffer_type + self.__draw_size = draw_size + self.__shader = get_shader(shader_type) + self.__data = {} + + for att_name in attr_names: + self.__data[att_name] = [] + + # depending on type setup callbacks executed before and after dispatching + if buffer_type == _Buffer.Types.LINES: + + self.__bgl_callback = bgl.glLineWidth + self.__bgl_callback_param_before = self.__draw_size + self.__bgl_callback_param_after = 1.0 + self.__draw_type = 'LINES' + + elif buffer_type == _Buffer.Types.POINTS: + + self.__bgl_callback = bgl.glPointSize + self.__bgl_callback_param_before = self.__draw_size + self.__bgl_callback_param_after = 1.0 + self.__draw_type = 'POINTS' + + elif buffer_type == _Buffer.Types.TRIS: + + self.__bgl_callback = lambda *args: None + self.__bgl_callback_param_before = None + self.__bgl_callback_param_after = None + self.__draw_type = 'TRIS' + + def append_attr(self, attr_name, value): + """Appends given value into the data fields for given attribute name + + NOTE: for performance no safety checks on existing attribute name are made + :param attr_name: name of the attribute for which value should be append + :type attr_name: str + :param value: value that should be append (tuple of floats for position, color etc.) + :type value: tuple[float] | tuple[int] | float | int + """ + self.__data[attr_name].append(value) + + def clear(self): + """Clears all entries in the buffer. + """ + for attr_name in self.__data: + self.__data[attr_name].clear() + + def draw(self, uniforms, space_3d): + """Dispatches drawing for the buffer by creating and drawing batch. + + :param uniforms: list of uniforms tuples to be sent to shader + :type uniforms: collections.Iterable[(str, type, bytearray, int, int)] + :param space_3d: space 3D data of viewport to which buffers should be drawn + :type space_3d: bpy.types.SpaceView3D + """ + + # nothing to draw really + if not self.has_entries(): + return + + # triangles are not drawn into wireframe views + if self.__type == _Buffer.Types.TRIS and space_3d.shading.type == 'WIREFRAME': + return + + self.__bgl_callback(self.__bgl_callback_param_before) + + # bind shader + self.__shader.bind() + + # fill the uniforms to binded shader + for uniform_name, uniform_type, uniform_data, uniform_length, uniform_count in uniforms: + uniform_loc = self.__shader.uniform_from_name(uniform_name) + if uniform_type == float: + self.__shader.uniform_vector_float(uniform_loc, uniform_data, uniform_length, uniform_count) + elif uniform_type == int: + self.__shader.uniform_vector_int(uniform_loc, uniform_data, uniform_length, uniform_count) + else: + raise TypeError("Invalid uniform type: %s" % uniform_type) + + # create batch and dispatch draw + batch = batch_for_shader(self.__shader, self.__draw_type, self.__data) + batch.draw(self.__shader) + + self.__bgl_callback(self.__bgl_callback_param_after) + + def has_entries(self): + """Checks if there is any antries in this buffer. + + :return: True if either position or color have any entries; False otherwise + :rtype: bool + """ + return len(self.__data["pos"]) + len(self.__data["color"]) > 0 + + +class _ViewsBufferHandler: + """Buffers handler class used to implement main view and all local view buffers.""" + + @staticmethod + def __get_new_buffers__(): + """Gets new instance for all buffers we currently use in one view. + + :return: currently we have 3 buffers: 1 for lines and 2 for points of different size + :rtype: tuple[_Buffer] + """ + return ( + _Buffer(_Buffer.Types.LINES, 2, ShaderTypes.SMOOTH_COLOR_CLIPPED_3D, ("pos", "color")), # 0 + _Buffer(_Buffer.Types.LINES, 2, ShaderTypes.SMOOTH_COLOR_STIPPLE_CLIPPED_3D, ("pos", "color")), # 1 + _Buffer(_Buffer.Types.POINTS, 5, ShaderTypes.SMOOTH_COLOR_CLIPPED_3D, ("pos", "color")), # 2 + _Buffer(_Buffer.Types.POINTS, 12, ShaderTypes.SMOOTH_COLOR_CLIPPED_3D, ("pos", "color")), # 3 + _Buffer(_Buffer.Types.TRIS, 0, ShaderTypes.SMOOTH_COLOR_CLIPPED_3D, ("pos", "color")), # 4 + ) + + def __init__(self): + """Creates instance of buffers handler. Should be used only once. + """ + self.__current = None + self.__buffers = {} + + def __get_buffers__(self, space_3d): + """Return list of bufffers for given space 3d view. If none is given empty list is returned. + + :param space_3d: space for which buffers should be returned + :type space_3d: bpy.types.SpaceView3D | None + :return: list of buffers for given space + :rtype: list[_Buffer] + """ + if space_3d in self.__buffers: + return self.__buffers[space_3d] + else: + return [] + + def set_current(self, space_3d): + """Sets currently used view and creates empty buffers to which and point or line will be added. + + :param space_3d: view which should be set as current one, None if main buffers should be set as current + :type space_3d: bpy.types.SpaceView3D | None + """ + self.__current = space_3d + + if self.__current not in self.__buffers: + self.__buffers[self.__current] = self.__get_new_buffers__() + + def append_tris_vertex(self, pos, color): + """Appends new tris vertex into the current buffers. + + :param pos: world space position of the vertex + :type pos: mathutils.Vector | tuple + :param color: color of the vertex, has to be of size 4 and fromat: (r, g, b, a) + :type color: mathutils.Vector | bpy.types.bpy_prop_collection | tuple + """ + buffer = self.__buffers[self.__current][4] + + buffer.append_attr("pos", tuple(pos)) + buffer.append_attr("color", tuple(color)) + + def append_line_vertex(self, pos, color, is_stipple=False): + """Appends new line start/end segment into the current buffers. + + :param pos: world space position of the start/end line point + :type pos: mathutils.Vector | tuple + :param color: color of the start/end line point, has to be of size 4 and fromat: (r, g, b, a) + :type color: mathutils.Vector | bpy.types.bpy_prop_collection | tuple + :param is_stipple: should line be stippled? + :type is_stipple: bool + """ + if is_stipple: + buffer = self.__buffers[self.__current][1] + else: + buffer = self.__buffers[self.__current][0] + + buffer.append_attr("pos", tuple(pos)) + buffer.append_attr("color", tuple(color)) + + def append_point_vertex(self, pos, color, size): + """Appends new point into the current buffers. + + :param pos: world space position of the point + :type pos: mathutils.Vector | tuple + :param color: color of the point, has to be of size 4 and fromat: (r, g, b, a) + :type color: mathutils.Vector | bpy.types.bpy_prop_collection | tuple + :param size: size in which point should be drawn (5.0 and 12.0 currently supported!) + :type size: float + """ + if size == 5.0: + buffer = self.__buffers[self.__current][2] + elif size == 12.0: + buffer = self.__buffers[self.__current][3] + else: + raise ValueError("Unsupported point size: %.2f. Only 5.0 or 12.0 are supported!" % size) + + buffer.append_attr("pos", tuple(pos)) + buffer.append_attr("color", tuple(color)) + + def clear_buffers(self): + """Clears all the buffers in handler. Then deletes all of them except the main one. + """ + for buffers in self.__buffers.values(): + for buffer in buffers: + buffer.clear() + + for space_3d in list(self.__buffers.keys()): + # ignore main view + if space_3d is None: + continue + + # delete other local space buffers + del self.__buffers[space_3d] + + def draw_buffers(self, space_3d): + """Draws buffers of given space. If no space is provided main buffers are drawn. + + :param space_3d: space 3D data of viewport to which buffers should be drawn + :type space_3d: bpy.types.SpaceView3D + """ + + # get clip planes as bytes array ready to be sent into shader + clip_planes_linear = array('f') + num_clip_planes = 0 + if space_3d.region_3d.use_clip_planes: + for clip_plane in space_3d.region_3d.clip_planes: + if clip_plane[0] == clip_plane[1] == clip_plane[2] == clip_plane[3] == 0.0: + continue + + clip_planes_linear.extend(list(clip_plane)) + num_clip_planes += 1 + + # put uniforms together + uniforms = ( + ("clip_planes", float, clip_planes_linear.tobytes(), 4, num_clip_planes), + ("num_clip_planes", int, array('i', (num_clip_planes,)), 1, 1) + ) + + # if current view has local view use it's buffers otherwise select main ones + if space_3d.local_view: + buffers_key = space_3d.local_view + else: + buffers_key = None + + # draw selected buffers + for buffer in self.__get_buffers__(buffers_key): + buffer.draw(uniforms, space_3d) + + +_views_buffer_handler = _ViewsBufferHandler() +"""Instace of views buffers handler to be able to have custom handlers for local views.""" + + +def clear_buffers(): + """Clears all drawing buffers we have. + """ + _views_buffer_handler.clear_buffers() + + +def draw_buffers(space_3d): + """Draws current state of buffers onto 3D viewport. If no lines or points, nothing is drawn. + + :param space_3d: space 3D data of viewport to which buffers should be drawn + :type space_3d: bpy.types.SpaceView3D + """ + _views_buffer_handler.draw_buffers(space_3d) + + +def set_active_buffers(space_3d): + """If given space has local view, then sets it as active, otherwise main buffers are set as active. + + :param space_3d: space 3D data of viewport to which should be set as active; None if main buffers should be set as active + :type space_3d: bpy.types.SpaceView3D | None + """ + if space_3d: + _views_buffer_handler.set_current(space_3d.local_view) + else: + _views_buffer_handler.set_current(None) + + +def append_tris_vertex(pos, color): + """Appends vertex into the tris buffers (to draw one triangle at least three vertices has to be added). + + :param pos: world space position of the vertex + :type pos: mathutils.Vector | tuple + :param color: color of the vertex, has to be of size 4 and fromat: (r, g, b, a) + :type color: mathutils.Vector | bpy.types.bpy_prop_collection | tuple + """ + _views_buffer_handler.append_tris_vertex(pos, color) + + +def append_line_vertex(pos, color, is_strip=False, is_stipple=False): + """Appends start and/or end segnement of line to buffers (so to draw one line, at least two calls of this function should be made). + + :param pos: world space position of the start/end line point + :type pos: mathutils.Vector | tuple + :param color: color of the start/end line point, has to be of size 4 and fromat: (r, g, b, a) + :type color: mathutils.Vector | bpy.types.bpy_prop_collection | tuple + :param is_strip: should next line continue from this point on? Similar to GL_LINE_STRIP. + :type is_strip: bool + :param is_stipple: should line be stippled? + :type is_stipple: bool + """ + _views_buffer_handler.append_line_vertex(pos, color, is_stipple=is_stipple) + + if is_strip: + append_line_vertex(pos, color, is_stipple=is_stipple) + + +def append_point_vertex(pos, color, size): + """Appends point with given position color and size, to buffer + + :param pos: world space position of the point + :type pos: mathutils.Vector | tuple + :param color: color of the point, has to be of size 4 and fromat: (r, g, b, a) + :type color: mathutils.Vector | bpy.types.bpy_prop_collection | tuple + :param size: size in which point should be drawn (5.0 and 12.0 currently supported!) + :type size: float + """ + _views_buffer_handler.append_point_vertex(pos, color, size) def get_box_data(): @@ -234,31 +574,42 @@ def draw_polygon_object(mat, vertices, faces, face_color, draw_faces, draw_wires :param wire_transforms: :return: """ + if draw_faces: + color = (face_color[0], face_color[1], face_color[2], 1.0) for face in faces: - glBegin(GL_POLYGON) - glColor3f(face_color[0], face_color[1], face_color[2]) - for vert in face: - if face_transforms: - trans = mat - for transformation in face_transforms: - if vert in transformation[1]: - trans = trans * transformation[0] - glVertex3f(*(trans * Vector(vertices[vert]))) - else: - glVertex3f(*(mat * Vector(vertices[vert]))) - glEnd() + face_vert_count = len(face) + if face_vert_count == 3: # only one triangle + for vert in face: + if face_transforms: + trans = mat + for transformation in face_transforms: + if vert in transformation[1]: + trans = trans @ transformation[0] + append_tris_vertex(trans @ Vector(vertices[vert]), color) + else: + append_tris_vertex(mat @ Vector(vertices[vert]), color) + else: # fan like triangles + i = 1 + while i < face_vert_count - 1: + for vert_i in (i, i + 1, 0): + vert = face[vert_i] + if face_transforms: + trans = mat + for transformation in face_transforms: + if vert in transformation[1]: + trans = trans @ transformation[0] + append_tris_vertex(trans @ Vector(vertices[vert]), color) + else: + append_tris_vertex(mat @ Vector(vertices[vert]), color) + i += 1 + if draw_wires: + color = (wire_color[0], wire_color[1], wire_color[2], 1.0) if wire_lines: - # DRAW CUSTOM LINES vert_i_global = 0 for line in wire_lines: - # glLineWidth(2.0) - glEnable(GL_LINE_STIPPLE) - glBegin(GL_LINES) - glColor3f(wire_color[0], wire_color[1], wire_color[2]) - for vert_i, vert1 in enumerate(line): if vert_i + 1 < len(line): vert2 = line[vert_i + 1] @@ -270,104 +621,60 @@ def draw_polygon_object(mat, vertices, faces, face_color, draw_faces, draw_wires trans1 = trans2 = mat for transformation in wire_transforms: if vert_i_global in transformation[1]: - trans1 = trans1 * transformation[0] + trans1 = trans1 @ transformation[0] if vert_i_global + 1 in transformation[1]: - trans2 = trans2 * transformation[0] - glVertex3f(*(trans1 * Vector(vert1))) - glVertex3f(*(trans2 * Vector(vert2))) + trans2 = trans2 @ transformation[0] + append_line_vertex(trans1 @ Vector(vert1), color, is_stipple=True) + append_line_vertex(trans2 @ Vector(vert2), color, is_stipple=True) else: - glVertex3f(*(mat * Vector(vert1))) - glVertex3f(*(mat * Vector(vert2))) + append_line_vertex(mat @ Vector(vert1), color, is_stipple=True) + append_line_vertex(mat @ Vector(vert2), color, is_stipple=True) vert_i_global += 1 vert_i_global += 1 - glEnd() - glDisable(GL_LINE_STIPPLE) - # glLineWidth(1.0) else: + lines_to_draw = set() for face in faces: - # glLineWidth(2.0) - glEnable(GL_LINE_STIPPLE) - glBegin(GL_LINES) - glColor3f(wire_color[0], wire_color[1], wire_color[2]) for vert_i, vert1 in enumerate(face): if vert_i + 1 == len(face): vert2 = face[0] else: vert2 = face[vert_i + 1] - if face_transforms: - trans1 = mat - trans2 = mat - vec1 = Vector(vertices[vert1]) - vec2 = Vector(vertices[vert2]) - for transformation in face_transforms: - if vert1 in transformation[1]: - trans1 = trans1 * transformation[0] - if vert2 in transformation[1]: - trans2 = trans2 * transformation[0] - glVertex3f(*(trans1 * vec1)) - glVertex3f(*(trans2 * vec2)) - else: - glVertex3f(*(mat * Vector(vertices[vert1]))) - glVertex3f(*(mat * Vector(vertices[vert2]))) - glEnd() - glDisable(GL_LINE_STIPPLE) - # glLineWidth(1.0) - if 0: # DEBUG: draw points from faces geometry - glPointSize(3.0) - glBegin(GL_POINTS) - glColor3f(0.5, 0.5, 1) - for vertex_i, vertex in enumerate(vertices): - vec = Vector(vertex) - if face_transforms: - trans = mat - for transformation in face_transforms: - if vertex_i in transformation[1]: - trans = trans * transformation[0] - glVertex3f(*(trans * vec)) - else: - glVertex3f(*(mat * vec.to_3d())) - glEnd() - glPointSize(1.0) - if 0: # DEBUG: draw points from lines geometry - if wire_lines: - glPointSize(3.0) - glBegin(GL_POINTS) - glColor3f(1, 0, 0.5) - vert_i_global = 0 - for line in wire_lines: - for vert_i, vertex in enumerate(line): - if vert_i + 1 < len(line): - vec = Vector(vertex) - else: + + if (vert1, vert2) in lines_to_draw or (vert2, vert1) in lines_to_draw: continue - if wire_transforms: - trans = mat - for transformation in wire_transforms: - if vert_i_global in transformation[1]: - trans = trans * transformation[0] - glVertex3f(*(trans * vec.to_3d())) - else: - glVertex3f(*(mat * vec.to_3d())) - vert_i_global += 1 - vert_i_global += 1 - glEnd() - glPointSize(1.0) + lines_to_draw.add((vert1, vert2)) -def draw_point(vector, color): + for vert1, vert2 in lines_to_draw: + if face_transforms: + trans1 = mat + trans2 = mat + vec1 = Vector(vertices[vert1]) + vec2 = Vector(vertices[vert2]) + for transformation in face_transforms: + if vert1 in transformation[1]: + trans1 = trans1 @ transformation[0] + if vert2 in transformation[1]: + trans2 = trans2 @ transformation[0] + append_line_vertex((trans1 @ vec1), color, is_stipple=True) + append_line_vertex((trans2 @ vec2), color, is_stipple=True) + else: + append_line_vertex(mat @ Vector(vertices[vert1]), color, is_stipple=True) + append_line_vertex(mat @ Vector(vertices[vert2]), color, is_stipple=True) + + +def draw_point(vector, color, size): """Draw point on given vector, with given color :param vector: position vector of point in Blender coordinates :type vector: mathutils.Vector :param color: tuple of RGB color :type color: tuple + :param size: size of point + :type size: float """ - glBegin(GL_POINTS) - glColor3f(color[0], color[1], color[2]) - glVertex3f(vector[0], vector[1], vector[2]) - - glEnd() + append_point_vertex(vector, color, size) def draw_circle(radius, steps, mat, scs_globals): @@ -381,14 +688,21 @@ def draw_circle(radius, steps, mat, scs_globals): """ import math - glBegin(GL_LINE_LOOP) - glColor3f(scs_globals.locator_prefab_wire_color.r, - scs_globals.locator_prefab_wire_color.g, - scs_globals.locator_prefab_wire_color.b) - for step in range(steps): - a = (math.pi * 2 / steps) * step - glVertex3f(*(mat * Vector((0 + radius * math.cos(a), 0 + radius * math.sin(a), 0.0)))) - glEnd() + color = ( + scs_globals.locator_prefab_wire_color.r, + scs_globals.locator_prefab_wire_color.g, + scs_globals.locator_prefab_wire_color.b, + 1.0 + ) + + first_a = 0 + append_line_vertex((mat @ Vector((0 + radius * math.cos(first_a), 0 + radius * math.sin(first_a), 0.0))), color) + + for step in range(steps - 1): + a = (math.pi * 2 / steps) * (step + 1) + append_line_vertex((mat @ Vector((0 + radius * math.cos(a), 0 + radius * math.sin(a), 0.0))), color, is_strip=True) + + append_line_vertex((mat @ Vector((0 + radius * math.cos(first_a), 0 + radius * math.sin(first_a), 0.0))), color) def draw_shape_x_axis(mat, size): @@ -398,16 +712,12 @@ def draw_shape_x_axis(mat, size): :param size: :return: """ - glLineWidth(2.0) - glBegin(GL_LINES) - glColor3f(1.0, 0.2, 0.2) - glVertex3f(*(mat * Vector((size, 0.0, 0.0)))) - glVertex3f(*(mat * Vector((0.25, 0.0, 0.0)))) - glColor3f(0.5, 0.0, 0.0) - glVertex3f(*(mat * Vector((-0.25, 0.0, 0.0)))) - glVertex3f(*(mat * Vector((size * -1, 0.0, 0.0)))) - glEnd() - glLineWidth(1.0) + color = (1.0, 0.2, 0.2, 1.0) + append_line_vertex((mat @ Vector((size, 0.0, 0.0))), color) + append_line_vertex((mat @ Vector((0.25, 0.0, 0.0))), color) + color = (0.5, 0.0, 0.0, 1.0) + append_line_vertex((mat @ Vector((-0.25, 0.0, 0.0))), color) + append_line_vertex((mat @ Vector((size * -1, 0.0, 0.0))), color) def draw_shape_y_axis(mat, size): @@ -417,16 +727,12 @@ def draw_shape_y_axis(mat, size): :param size :return: """ - glLineWidth(2.0) - glBegin(GL_LINES) - glColor3f(0.2, 1.0, 0.2) - glVertex3f(*(mat * Vector((0.0, size, 0.0)))) - glVertex3f(*(mat * Vector((0.0, 0.25, 0.0)))) - glColor3f(0.0, 0.5, 0.0) - glVertex3f(*(mat * Vector((0.0, -0.25, 0.0)))) - glVertex3f(*(mat * Vector((0.0, size * -1, 0.0)))) - glEnd() - glLineWidth(1.0) + color = (0.2, 1.0, 0.2, 1.0) + append_line_vertex((mat @ Vector((0.0, size, 0.0))), color) + append_line_vertex((mat @ Vector((0.0, 0.25, 0.0))), color) + color = (0.0, 0.5, 0.0, 1.0) + append_line_vertex((mat @ Vector((0.0, -0.25, 0.0))), color) + append_line_vertex((mat @ Vector((0.0, size * -1, 0.0))), color) def draw_shape_y_axis_neg(mat, size): @@ -436,13 +742,9 @@ def draw_shape_y_axis_neg(mat, size): :param size: :return: """ - glLineWidth(2.0) - glBegin(GL_LINES) - glColor3f(0.0, 0.5, 0.0) - glVertex3f(*(mat * Vector((0.0, -0.25, 0.0)))) - glVertex3f(*(mat * Vector((0.0, size * -1, 0.0)))) - glEnd() - glLineWidth(1.0) + color = (0.0, 0.5, 0.0, 1.0) + append_line_vertex((mat @ Vector((0.0, -0.25, 0.0))), color) + append_line_vertex((mat @ Vector((0.0, size * -1, 0.0))), color) def draw_shape_z_axis(mat, size): @@ -452,16 +754,12 @@ def draw_shape_z_axis(mat, size): :param size: :return: """ - glLineWidth(2.0) - glBegin(GL_LINES) - glColor3f(0.2, 0.2, 1.0) - glVertex3f(*(mat * Vector((0.0, 0.0, size)))) - glVertex3f(*(mat * Vector((0.0, 0.0, 0.25)))) - glColor3f(0.0, 0.0, 0.5) - glVertex3f(*(mat * Vector((0.0, 0.0, -0.25)))) - glVertex3f(*(mat * Vector((0.0, 0.0, size * -1)))) - glEnd() - glLineWidth(1.0) + color = (0.2, 0.2, 1.0, 1.0) + append_line_vertex((mat @ Vector((0.0, 0.0, size))), color) + append_line_vertex((mat @ Vector((0.0, 0.0, 0.25))), color) + color = (0.0, 0.0, 0.5, 1.0) + append_line_vertex((mat @ Vector((0.0, 0.0, -0.25))), color) + append_line_vertex((mat @ Vector((0.0, 0.0, size * -1))), color) def draw_shape_z_axis_neg(mat, size): @@ -471,13 +769,9 @@ def draw_shape_z_axis_neg(mat, size): :param size: :return: """ - glLineWidth(2.0) - glBegin(GL_LINES) - glColor3f(0.0, 0.0, 0.5) - glVertex3f(*(mat * Vector((0.0, 0.0, -0.25)))) - glVertex3f(*(mat * Vector((0.0, 0.0, size * -1)))) - glEnd() - glLineWidth(1.0) + color = (0.0, 0.0, 0.5, 1.0) + append_line_vertex((mat @ Vector((0.0, 0.0, -0.25))), color) + append_line_vertex((mat @ Vector((0.0, 0.0, size * -1))), color) def draw_shape_line(line, stipple, is_map_line, scs_globals): @@ -499,41 +793,37 @@ def draw_shape_line(line, stipple, is_map_line, scs_globals): :rtype: """ if 'line_color0' in line: - color0 = line['line_color0'] + color0 = tuple(line['line_color0']) + (1.0,) else: if is_map_line: color0 = (scs_globals.mp_connection_base_color.r, scs_globals.mp_connection_base_color.g, - scs_globals.mp_connection_base_color.b) + scs_globals.mp_connection_base_color.b, + 1.0) else: color0 = (scs_globals.tp_connection_base_color.r, scs_globals.tp_connection_base_color.g, - scs_globals.tp_connection_base_color.b) + scs_globals.tp_connection_base_color.b, + 1.0) if 'line_color1' in line: - color1 = line['line_color1'] + color1 = tuple(line['line_color1']) + (1.0,) else: if is_map_line: color1 = (scs_globals.mp_connection_base_color.r, scs_globals.mp_connection_base_color.g, - scs_globals.mp_connection_base_color.b) + scs_globals.mp_connection_base_color.b, + 1.0) else: color1 = (scs_globals.tp_connection_base_color.r, scs_globals.tp_connection_base_color.g, - scs_globals.tp_connection_base_color.b) - - if stipple: - glEnable(GL_LINE_STIPPLE) - glBegin(GL_LINES) - glColor3f(color0[0], color0[1], color0[2]) - glVertex3f(*line['loc_0']) - glVertex3f(*line['loc_btw']) - glColor3f(color1[0], color1[1], color1[2]) - glVertex3f(*line['loc_btw']) - glVertex3f(*line['loc_1']) - glEnd() - if stipple: - glDisable(GL_LINE_STIPPLE) + scs_globals.tp_connection_base_color.b, + 1.0) + + append_line_vertex(line['loc_0'], color0, is_stipple=stipple) + append_line_vertex(line['loc_btw'], color0, is_stipple=stipple) + append_line_vertex(line['loc_btw'], color1, is_stipple=stipple) + append_line_vertex(line['loc_1'], color1, is_stipple=stipple) def draw_shape_curve(curve, stipple, scs_globals): @@ -555,144 +845,70 @@ def draw_shape_curve(curve, stipple, scs_globals): :rtype: """ if 'curve_color0' in curve: - color0 = curve['curve_color0'] + color0 = tuple(curve['curve_color0']) + (1.0,) else: color0 = (scs_globals.np_connection_base_color.r, scs_globals.np_connection_base_color.g, - scs_globals.np_connection_base_color.b) + scs_globals.np_connection_base_color.b, + 1.0) if 'curve_color1' in curve: - color1 = curve['curve_color1'] + color1 = tuple(curve['curve_color1']) + (1.0,) else: color1 = (scs_globals.np_connection_base_color.r, scs_globals.np_connection_base_color.g, - scs_globals.np_connection_base_color.b) + scs_globals.np_connection_base_color.b, + 1.0) - glColor3f(color0[0], color0[1], color0[2]) + real_color = color0 - if stipple: - glEnable(GL_LINE_STIPPLE) - # switch = 1 - glBegin(GL_LINE_STRIP) + last_item_i = len(curve['curve_points']) - 1 for vec_i, vec in enumerate(curve['curve_points']): if vec_i == int(curve['curve_steps'] / 2 + 1.5): # if vec_i > curve['curve_steps'] / 2 and switch: - glColor3f(color1[0], color1[1], color1[2]) - # switch = 0 - glVertex3f(*vec) - glEnd() - if stipple: - glDisable(GL_LINE_STIPPLE) + real_color = color1 + if vec_i == 0 or vec_i == last_item_i: + append_line_vertex(vec, real_color, is_stipple=stipple) + else: + append_line_vertex(vec, real_color, is_strip=True, is_stipple=stipple) -def draw_text(text, font_id, vec, region_data, x_offset=0, y_offset=0): - """ - :param text: - :type text: - :param vec: - :type vec: - :param font_id: - :type font_id: - :param region_data: (region3d_perspective_matrix, region2d_mid_width, region2d_mid_height) - :type region_data: tuple - :param x_offset: - :type x_offset: - :param y_offset: - :type y_offset: - :return: - :rtype: +def draw_text(text, font_id, x, y): + """Draws given text at x,y position. + + :param text: text to be drawn + :type text: str + :param font_id: font id with which text should be drawn + :type font_id: int + :param x: x position in 3d view region + :type x: float + :param y: y position in 3d view region + :type y: float """ - vec_4d = region_data[0] * vec.to_4d() - if vec_4d.w > 0.0: - x = region_data[1] + region_data[1] * (vec_4d.x / vec_4d.w) - y = region_data[2] + region_data[2] * (vec_4d.y / vec_4d.w) - blf.position(font_id, x + 15.0 + x_offset, y - 4.0 + y_offset, 0.0) - blf.draw(font_id, text) + # static offset to move from origin + x += 15.0 + y += -4.0 + blf.position(font_id, x, y, 0.0) + blf.draw(font_id, text) -''' -def draw_matrix(mat): - """ - Draw Matrix lines. - :param mat: - :return: + +def draw_rect_2d(positions, color): + """Draw 2D rectangle with given color. Use it to draw in SpaceView3D on 'POST_PIXEL'. + + :param positions: 2D position in the region, that we want to draw to. + :type positions: tuple(int, int) + :param color: RGBA of rectangle + :type color: tuple(float, float, float, float) """ - zero_tx = mat * zero - - glLineWidth(2.0) - - # X - glColor3f(1.0, 0.2, 0.2) - glBegin(GL_LINES) - glVertex3f(*zero_tx) - glVertex3f(*(mat * x_p)) - glEnd() - - glColor3f(0.6, 0.0, 0.0) - glBegin(GL_LINES) - glVertex3f(*zero_tx) - glVertex3f(*(mat * x_n)) - glEnd() - - ## Y - glColor3f(0.2, 1.0, 0.2) - glBegin(GL_LINES) - glVertex3f(*zero_tx) - glVertex3f(*(mat * y_p)) - glEnd() - - glColor3f(0.0, 0.6, 0.0) - glBegin(GL_LINES) - glVertex3f(*zero_tx) - glVertex3f(*(mat * y_n)) - glEnd() - - ## Z - glColor3f(0.2, 0.2, 1.0) - glBegin(GL_LINES) - glVertex3f(*zero_tx) - glVertex3f(*(mat * z_p)) - glEnd() - - glColor3f(0.0, 0.0, 0.6) - glBegin(GL_LINES) - glVertex3f(*zero_tx) - glVertex3f(*(mat * z_n)) - glEnd() - - ## BOUNDING BOX - i = 0 - glColor3f(1.0, 1.0, 1.0) - for x in (-1.0, 1.0): - for y in (-1.0, 1.0): - for z in (-1.0, 1.0): - bb[i][:] = x, y, z - bb[i] = mat * bb[i] - i += 1 - - ## STRIP - glLineWidth(1.0) - glLineStipple(1, 0xAAAA) - glEnable(GL_LINE_STIPPLE) - - glBegin(GL_LINE_STRIP) - for i in 0, 1, 3, 2, 0, 4, 5, 7, 6, 4: - glVertex3f(*bb[i]) - pnt = mat * Vector((0.75, -0.75, -0.75)) - glVertex3f(pnt[0], pnt[1], pnt[2]) - glEnd() - - ## NOT DONE BY THE STRIP - glBegin(GL_LINES) - glVertex3f(*bb[1]) - glVertex3f(*bb[5]) - - glVertex3f(*bb[2]) - glVertex3f(*bb[6]) - - glVertex3f(*bb[3]) - glVertex3f(*bb[7]) - glEnd() - glDisable(GL_LINE_STIPPLE) -''' + shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR') + batch = batch_for_shader( + shader, 'TRI_FAN', + { + "pos": positions, + }, + ) + shader.bind() + shader.uniform_float("color", color) + batch.draw(shader) diff --git a/addon/io_scs_tools/internals/open_gl/shaders/__init__.py b/addon/io_scs_tools/internals/open_gl/shaders/__init__.py new file mode 100644 index 0000000..081ce4a --- /dev/null +++ b/addon/io_scs_tools/internals/open_gl/shaders/__init__.py @@ -0,0 +1,78 @@ +# ##### 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 + +import os +import gpu + + +class ShaderTypes: + SMOOTH_COLOR_CLIPPED_3D = 1 + SMOOTH_COLOR_STIPPLE_CLIPPED_3D = 2 + + +__cache = {} +"""Simple dictonary holding shader instances by shader type. To prevent loading shader each time one requests it.""" + + +def __get_shader_data__(shader_filename): + """Loads and gets shader data from given filename. + + :param shader_filename: filename of shader file inside io_scs_tools/internals/open_gl/shaders + :type shader_filename: str + :return: shader data string from given glsl shader + :rtype: str + """ + shader_filepath = os.path.join(os.path.dirname(os.path.realpath(__file__)), shader_filename) + + with open(shader_filepath, "r") as file: + shader_data_str = file.read() + + return shader_data_str + + +def get_shader(shader_type): + """Get GPU shader for given type. + + :param shader_type: shader type from ShaderTypes + :type shader_type: int + :return: + :rtype: gpu.types.GPUShader + """ + if shader_type == ShaderTypes.SMOOTH_COLOR_CLIPPED_3D: + + if shader_type not in __cache: + __cache[shader_type] = gpu.types.GPUShader( + __get_shader_data__("smooth_color_clipped_3d_vert.glsl"), + __get_shader_data__("smooth_color_clipped_3d_frag.glsl") + ) + + elif shader_type == ShaderTypes.SMOOTH_COLOR_STIPPLE_CLIPPED_3D: + + if shader_type not in __cache: + __cache[shader_type] = gpu.types.GPUShader( + __get_shader_data__("smooth_color_stipple_clipped_3d_vert.glsl"), + __get_shader_data__("smooth_color_stipple_clipped_3d_frag.glsl"), + ) + + else: + + raise TypeError("Failed generating shader, unexepected shader type: %r!" % shader_type) + + return __cache[shader_type] diff --git a/addon/io_scs_tools/internals/open_gl/shaders/smooth_color_clipped_3d_frag.glsl b/addon/io_scs_tools/internals/open_gl/shaders/smooth_color_clipped_3d_frag.glsl new file mode 100644 index 0000000..5a2f0db --- /dev/null +++ b/addon/io_scs_tools/internals/open_gl/shaders/smooth_color_clipped_3d_frag.glsl @@ -0,0 +1,15 @@ +in vec4 finalColor; +in vec4 vertexPos; +out vec4 fragColor; + +uniform vec4 clip_planes[6]; +uniform int num_clip_planes; + +void main() +{ + for (int i=0; i SCS tools needs to configured 2. Opening .blend file -> because all the configs needs to be moved to current world 3. addon reloading and enable/disable -> for SCS tools this is the same as opening Blender - - :param scene: Current Blender Scene - :type scene: bpy.types.Scene """ # SCREEN CHECK... - if bpy.context.screen: - lprint("I Initialization of SCS scene, BT version: " + _info_utils.get_tools_version()) - - # NOTE: covers: start-up, reload, enable/disable and it should be immediately removed - # from handlers as soon as it's executed for the first time - if initialise_scs_dict in bpy.app.handlers.scene_update_post: - bpy.app.handlers.scene_update_post.remove(initialise_scs_dict) - - # INITIALIZE CUSTOM CONNECTIONS DRAWING SYSTEM - _connections_group_wrapper.init() - - # release lock as user might saved blender file during engaged lock. - # If that happens config lock property gets saved to blend file and if user opens that file again, - # lock will be still engaged and no settings could be applied without releasing lock here. - _config_container.release_config_lock() - - # USE SETTINGS FROM CONFIG... - # NOTE: Reapplying the settings from config file to the currently opened Blender file datablock. - # The thing is, that every Blend file holds its own copy of SCS Global Settings from the machine on which it got saved. - # The SCS Global Settings needs to be overwritten upon each file load to reflect the settings from local config file, - # but also upon every SCS Project Base Path change. - _config_container.apply_settings() - - # GLOBAL PATH CHECK... - if _get_scs_globals().scs_project_path != "": - if not os.path.isdir(_get_scs_globals().scs_project_path): - lprint("\nW The Project Path %r is NOT VALID!\n\tPLEASE SELECT A VALID PATH TO THE PROJECT BASE FOLDER.\n", - (_get_scs_globals().scs_project_path,)) - - # CREATE PREVIEW MODEL LIBRARY - _preview_models.init() - - # ADD DRAW HANDLERS - _open_gl_callback.enable(mode=_get_scs_globals().drawing_mode) - - # ENABLE LIGHTING EAST LOCK HANDLER - # Blender doesn't call update on properties when file is opened, - # so in case lighting east was locked in saved blend file, we have to manually enable callback for it - # On the other hand if user previously had east locked and now loaded the file without it, - # again we have to manually disable callback. - if _get_scs_globals().lighting_east_lock: - _lighting_east_lock_callback.enable() - else: - _lighting_east_lock_callback.disable() - - # as last notify user if his Blender version is outdated - if not _info_utils.is_blender_able_to_run_tools(): - - message = "Your Blender version %s is outdated, all SCS Blender Tools functionalities were internally disabled.\n\t " \ - "Please update Blender before continue, minimal required version for SCS Blender Tools is: %s!" - message = message % (_info_utils.get_blender_version()[0], _info_utils.get_required_blender_version()) - - # first report error with blender tools printing system - lprint("E " + message) - - # then disable add-on as it's not usable in the case Blender is out-dated - bpy.ops.wm.addon_disable('INVOKE_DEFAULT', module="io_scs_tools") - - # and as last show warning message in the form of popup menu for user to see info about outdated Blender - # As we don't have access to our 3D view report operator anymore, - # we have to register our ShowWarningMessage class back and invoke it. - from io_scs_tools.operators.wm import ShowWarningMessage - bpy.utils.register_class(ShowWarningMessage) - - bpy.ops.wm.show_warning_message('INVOKE_DEFAULT', - is_modal=True, - title="SCS Blender Tools Initialization Problem", - message="\n\n" + message.replace("\t ", "") + "\n\n", # some nasty formatting for better visibility - width=580, # this is minimal width to properly fit in given message - height=bpy.context.window.height if bpy.context and bpy.context.window else 200) + if not hasattr(bpy.data, "worlds"): + lprint("I Initialization abort, context incorrect ...") + return False + + lprint("I Initialization of SCS scene, BT version: " + _info_utils.get_tools_version()) + + # NOTE: covers: start-up, reload, enable/disable and it should be immediately removed + # from handlers as soon as it's executed for the first time + # if initialise_scs_dict in bpy.app.handlers.scene_update_post: + # bpy.app.handlers.scene_update_post.remove(initialise_scs_dict) + + # INITIALIZE CUSTOM CONNECTIONS DRAWING SYSTEM + _connections_wrapper.init() + + # TODO: this should not be needed anymore, as we don't config locks shouldn't be saved in blend file anymore see: scs_globals.get_writtable_keys + # release lock as user might saved blender file during engaged lock. + # If that happens config lock property gets saved to blend file and if user opens that file again, + # lock will be still engaged and no settings could be applied without releasing lock here. + _config_container.release_config_lock() + + # USE SETTINGS FROM CONFIG... + # NOTE: Reapplying the settings from config file to the currently opened Blender file datablock. + # The thing is, that every Blend file holds its own copy of SCS Global Settings from the machine on which it got saved. + # The SCS Global Settings needs to be overwritten upon each file load to reflect the settings from local config file, + # but also upon every SCS Project Base Path change. + _config_container.apply_settings(preload_from_blend=True) + + # GLOBAL PATH CHECK... + if _get_scs_globals().scs_project_path != "": + if not os.path.isdir(_get_scs_globals().scs_project_path): + lprint("\nW The Project Path %r is NOT VALID!\n\tPLEASE SELECT A VALID PATH TO THE PROJECT BASE FOLDER.\n", + (_get_scs_globals().scs_project_path,)) + + # CREATE PREVIEW MODEL LIBRARY + _preview_models.init() + + # ADD DRAW HANDLERS + _open_gl_callback.enable(mode=_get_scs_globals().drawing_mode) + + # ENABLE LIGHTING EAST LOCK HANDLER + # Blender doesn't call update on properties when file is opened, + # so in case lighting east was locked in saved blend file, we have to manually enable callback for it + # On the other hand if user previously had east locked and now loaded the file without it, + # again we have to manually disable callback. + if _get_scs_globals().lighting_east_lock: + _lighting_east_lock_callback.enable() + else: + _lighting_east_lock_callback.disable() + + # as last notify user if his Blender version is outdated + if not _info_utils.is_blender_able_to_run_tools(): + + message = "Your Blender version %s is outdated, all SCS Blender Tools functionalities were internally disabled.\n\t " \ + "Please update Blender before continue, minimal required version for SCS Blender Tools is: %s!" + message = message % (_info_utils.get_blender_version()[0], _info_utils.get_required_blender_version()) + + # first report error with blender tools printing system + lprint("E " + message) + + # then disable add-on as it's not usable in the case Blender is out-dated + bpy.ops.preferences.addon_disable(module="io_scs_tools") + + # and as last show warning message in the form of popup menu for user to see info about outdated Blender + # As we don't have access to our 3D view report operator anymore, + # we have to register our SCS_TOOLS_OT_ShowMessageInPopup class back and invoke it. + from io_scs_tools.operators.wm import SCS_TOOLS_OT_ShowMessageInPopup + bpy.utils.register_class(SCS_TOOLS_OT_ShowMessageInPopup) + + bpy.ops.wm.scs_tools_show_message_in_popup(context, 'INVOKE_DEFAULT', + is_modal=True, + title="SCS Blender Tools Initialization Problem", + message="\n\n" + message.replace("\t ", "") + "\n\n", # formatting for better visibility + width=580, # this is minimal width to properly fit in given message + height=bpy.context.window.height if bpy.context and bpy.context.window else 200) + + return True diff --git a/addon/io_scs_tools/internals/persistent/loop_check.py b/addon/io_scs_tools/internals/persistent/loop_check.py index 6a4c43e..30bbfec 100644 --- a/addon/io_scs_tools/internals/persistent/loop_check.py +++ b/addon/io_scs_tools/internals/persistent/loop_check.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software from time import time @@ -24,8 +24,7 @@ from bpy.app.handlers import persistent from io_scs_tools.internals import looks as _looks from io_scs_tools.internals import preview_models as _preview_models -from io_scs_tools.internals.connections.wrappers import group as _connections_group_wrapper -from io_scs_tools.internals.shaders import update_shaders as _update_shaders +from io_scs_tools.internals.connections.wrappers import collection as _connections_wrapper from io_scs_tools.utils import get_scs_globals as _get_scs_globals from io_scs_tools.utils import object as _object_utils from io_scs_tools.utils import view3d as _view3d_utils @@ -40,15 +39,61 @@ class _Timer: """Marking object update iterations during stalling.""" data_updated = 0 """Marking object data update iteration during stalling.""" + scene_updated = 0 + """Marking scene update iterations during stalling.""" + collections_updated = 0 + """Margin collection update iterations during stalling.""" @staticmethod def can_execute(): + """Gets execution status of timer. + + :return: True if timer tick was reached; False otherwise + :rtype: bool + """ + if time() - _Timer._last_execute_time > _Timer._interval: _Timer._last_execute_time = time() return True else: return False + @staticmethod + def increment_updated_ids(depsgraph): + """Increments update states of MESH, OBJECT, SCENE and COLLECTION ids from given depsgraph. + + :param depsgraph: depsgraph from which update states should be read + :type depsgraph: bpy.types.Depsgraph + """ + if depsgraph.id_type_updated('MESH'): + _Timer.data_updated += 1 + + if depsgraph.id_type_updated('OBJECT'): + _Timer.updated += 1 + + if depsgraph.id_type_updated('SCENE'): + _Timer.scene_updated += 1 + + if depsgraph.id_type_updated('COLLECTION'): + _Timer.collections_updated += 1 + + @staticmethod + def get_and_reset_updated_states(): + """Gets and resets update states. + + :return: tuple of updated ids (mesh, objects, scenes, collections) + :rtype: bool, bool, bool, bool + """ + + is_updated = _Timer.updated > 0 + is_data_updated = _Timer.data_updated > 0 + is_scene_updated = _Timer.scene_updated > 0 + is_collections_updated = _Timer.collections_updated > 0 + + _Timer.updated = _Timer.data_updated = _Timer.scene_updated = _Timer.collections_updated = 0 + + return is_updated, is_data_updated, is_scene_updated, is_collections_updated + @persistent def object_data_check(scene): @@ -58,44 +103,25 @@ def object_data_check(scene): active_obj = bpy.context.active_object selected_objs = bpy.context.selected_objects + depsgraph = bpy.context.view_layer.depsgraph # skip persistent check if import is in the process if _get_scs_globals().import_in_progress: return - # CHECK FOR DATA CHANGE UPON SELECTION - if active_obj: - - if active_obj.is_updated_data: - _Timer.data_updated += 1 - - if active_obj.is_updated: - _Timer.updated += 1 - - elif len(selected_objs) > 0: - # search for any updated object - for sel_obj in selected_objs[:2]: - if sel_obj.is_updated: - _Timer.updated += 1 - break - if _Timer.updated > 0: - _connections_group_wrapper.switch_to_update() + # CHECK FOR DATA CHANGE UPON DEPSGRAPH + _Timer.increment_updated_ids(depsgraph) # BREAK EXECUTION IF TIMER SAYS SO if not _Timer.can_execute(): return - # DO ANY SHADER RELATED TIME UPDATES WHEN ANIMATION PLAYBACK IS ACTIVE - if bpy.context.screen and bpy.context.screen.is_animation_playing: - _update_shaders() - # GET UPDATE STATES - updated = _Timer.updated > 0 - data_updated = _Timer.data_updated > 0 - _Timer.updated = _Timer.data_updated = 0 + objs_updated, meshes_updated, scenes_updated, collections_updated = _Timer.get_and_reset_updated_states() - if not updated: - _connections_group_wrapper.switch_to_stall() + # PREVIEW MODELS CHECKS + if objs_updated or scenes_updated or collections_updated: + _preview_models.fix_visibilites() # NEW/COPY if len(scene.objects) > scene.scs_cached_num_objects: @@ -109,7 +135,7 @@ def object_data_check(scene): real_sel_objs = selected_objs.copy() for sel_obj in selected_objs: for child in sel_obj.children: - if child.select: + if child.select_get(): real_sel_objs.append(child) if real_sel_objs[0].scs_props.object_identity != "": @@ -172,7 +198,7 @@ def object_data_check(scene): _fix_children(bpy.data.objects[old_name]) # switching names causes invalid connections data so recalculate curves for these objects - _connections_group_wrapper.force_recalculate([bpy.data.objects[old_name], bpy.data.objects[new_name]]) + _connections_wrapper.force_recalculate([bpy.data.objects[old_name], bpy.data.objects[new_name]]) lprint("D ---> RENAME of the active object!") return @@ -206,7 +232,7 @@ def object_data_check(scene): return # UNPARENT - if not active_obj.select and not active_obj.parent and active_obj.scs_props.parent_identity != "": + if not active_obj.select_get() and not active_obj.parent and active_obj.scs_props.parent_identity != "": _fix_ex_parent(active_obj) active_obj.scs_props.parent_identity = "" @@ -236,7 +262,7 @@ def object_data_check(scene): return # MATERIAL ASSIGNEMENT ACTION - if data_updated or updated: + if meshes_updated or objs_updated: mats_ids = {} new_mats = {} @@ -338,7 +364,7 @@ def __objects_copy__(old_objects, new_objects): """ # try to copy connections for new objects - _connections_group_wrapper.copy_check(old_objects, new_objects) + _connections_wrapper.copy_check(old_objects, new_objects) _view3d_utils.tag_redraw_all_view3d() # also check for any preview models which should be also copied to new ones @@ -356,10 +382,10 @@ def __objects_delete__(unparented_objects): """ # fix connections recalculations for unparented - _connections_group_wrapper.force_recalculate(unparented_objects) + _connections_wrapper.force_recalculate(unparented_objects) _view3d_utils.tag_redraw_all_view3d() - # delete unused previe models + # delete unused preview models for obj in bpy.data.objects: if obj.type == 'MESH': if "scs_props" in obj.data and obj.data.scs_props.locator_preview_model_path != "" and not obj.parent: @@ -377,7 +403,7 @@ def __object_rename__(old_name, new_name): """ # send rename notify into connections storage - if _connections_group_wrapper.rename_locator(old_name, new_name): + if _connections_wrapper.rename_locator(old_name, new_name): _view3d_utils.tag_redraw_all_view3d() # send rename notify to preview models @@ -406,7 +432,7 @@ def _fix_children(obj): """Fixes ex children which wrer caused by re/unparenting. It takes children of given object and resets their parent to given object. - + :param obj: SCS Blender object which children should be fixed :type obj: bpy.types.Object """ diff --git a/addon/io_scs_tools/internals/persistent/open_gl.py b/addon/io_scs_tools/internals/persistent/open_gl.py new file mode 100644 index 0000000..c7c7008 --- /dev/null +++ b/addon/io_scs_tools/internals/persistent/open_gl.py @@ -0,0 +1,178 @@ +# ##### 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 + +import bpy +from bpy.app.handlers import persistent +from io_scs_tools.internals.connections.wrappers import collection as _connections_wrapper +from io_scs_tools.internals.open_gl import core as _open_gl_core +from io_scs_tools.utils import view3d as _view3d_utils +from io_scs_tools.utils import get_scs_globals as _get_scs_globals + +_LAST_SCENE = "last_scene" +"""Key for caching last scene, so we are able to detect scene change.""" + +_cache = { + _LAST_SCENE: None +} + + +class _DelayedUpdate: + """Class for employing delayed update of coonections recalculation and buffers refill, implemented with new timers API.""" + + __objects_to_refill = [] + """Storing objects on which buffers refill should be executed, when delayed update is engaged. + NOTE: We don't use functools as propsed in API examples, as then we can not unregister our function anymore. + """ + + @staticmethod + def __update_internal__(): + """Execute delayed update by updating connections and re-filling the drawing buffers. + + NOTE: should be used as timer function. + + :return: float if function should be called again, or None to unregister this function from timers + :rtype: float | None + """ + _connections_wrapper.update_for_redraw() + + _open_gl_core.fill_buffers(_DelayedUpdate.__objects_to_refill) + + # trigger redraw as in some cases draw may not be called, + # due the fact we just executed delayed update from timer + _view3d_utils.tag_redraw_all_view3d() + + return None + + @staticmethod + def schedule(delay, object_list): + """Schedules delayed update of connections and buffers refill. + + :param delay: when update should be executed in seconds + :type delay: float + :param object_list: list of object to use on scheduled update delay + :type object_list: list + """ + assert delay > 0.0 + + # check if already registered, then remove it first + if bpy.app.timers.is_registered(_DelayedUpdate.__update_internal__): + bpy.app.timers.unregister(_DelayedUpdate.__update_internal__) + + # now reschedule + _DelayedUpdate.__objects_to_refill = object_list + bpy.app.timers.register(_DelayedUpdate.__update_internal__, first_interval=delay) + + +@persistent +def post_undo(scene): + """OpenGL drawing on post undo. We need it as post depsgraph handler is not called after executing undo. + + :param scene: current scene + :type scene: bpy.type.Scene + """ + post_depsgraph(scene) + + +@persistent +def post_redo(scene): + """OpenGL drawing on post redo. We need it as post depsgraph handler is not called after executing undo. + + :param scene: current scene + :type scene: bpy.type.Scene + """ + post_depsgraph(scene) + + +@persistent +def post_frame_change(scene): + """OpenGL drawing on post frame change. We need it for: + 1. Scene switch detection + 2. During animation playback as post depsgraph handler is not called as last. + + :param scene: current scene + :type scene: bpy.type.Scene + """ + + do_update = False + + # on scene change we need to refill our buffers, as different objects will be displayed + if scene != _cache[_LAST_SCENE]: + _cache[_LAST_SCENE] = scene + do_update = True + # take over during animation playback, where late update should be initiated each time objects are updated + elif bpy.context.screen and bpy.context.screen.is_animation_playing and bpy.context.view_layer.depsgraph.id_type_updated('OBJECT'): + do_update = True + + if do_update and not _get_scs_globals().import_in_progress: + _connections_wrapper.invalidate() + + # in case of playback we don't want to slow down FPS, empty buffers and wait for delayed update to do it's job + _open_gl_core.fill_buffers([]) + + # NOTE: while animation is playing context.visible_objects for some reason + # returns only visible objects of current 3d view if user mouse is over 3d view. + # To prevent wrong object selection in that case we are rather sendin all view layer objects. + _DelayedUpdate.schedule(0.1, bpy.context.view_layer.objects.values()) + + +@persistent +def post_depsgraph(scene): + """OpenGL drawing on post depsgraph. Main hookup to update connections and re-fill positions and colors of custom 3d elements. + + :param scene: current scene + :type scene: bpy.type.Scene + """ + scs_globals = _get_scs_globals() + + # during animation playback, leave everything up to post frame change + screen = bpy.context.screen + if screen and screen.is_animation_playing: + return + + # nothing we are interested in was updated, means we don't have to refill our buffers + depsgraph = bpy.context.view_layer.depsgraph + if depsgraph and not depsgraph.id_type_updated('OBJECT') and not depsgraph.id_type_updated('SCENE'): + # OBJECT not updated, none of the objects was transformed or hidden + # SCENE not updated, collections did not get hidden or shown in outliner + return + + _connections_wrapper.invalidate() + + # if post graph get's called with global context only (e.g. blend data changes in timers), + # then screen is not availabe nor is visible_objects variable, in that case use all view layer objects + if bpy.context.screen: + objects_list = bpy.context.visible_objects + else: + objects_list = bpy.context.view_layer.objects.values() + + # during import always re-schedule delayed update, so once import ends refill will be triggered + if scs_globals.import_in_progress: + _DelayedUpdate.schedule(0.1, objects_list) + return + + # if optimized do late connections update and refill, otherwise prepare connections for filling into buffers + if scs_globals.optimized_connections_drawing: + _DelayedUpdate.schedule(0.1, objects_list) + else: + _connections_wrapper.update_for_redraw() + + # fill buffers in any case, when connections are optimized, only locators will be filled to buffers, + # otherwise everything will be filled. + _open_gl_core.fill_buffers(objects_list) diff --git a/addon/io_scs_tools/internals/persistent/shaders_update.py b/addon/io_scs_tools/internals/persistent/shaders_update.py new file mode 100644 index 0000000..8a9ebce --- /dev/null +++ b/addon/io_scs_tools/internals/persistent/shaders_update.py @@ -0,0 +1,36 @@ +# ##### 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 + +import bpy +from bpy.app.handlers import persistent +from io_scs_tools.internals.shaders import update_shaders as _update_shaders + + +@persistent +def post_frame_change(scene): + """Shader update on post frame change. + + :param scene: current scene + :type scene: bpy.type.Scene + """ + + # do any shader related time updates when animation playback is active + if bpy.context.screen and bpy.context.screen.is_animation_playing: + _update_shaders(scene) diff --git a/addon/io_scs_tools/internals/preview_models/__init__.py b/addon/io_scs_tools/internals/preview_models/__init__.py index 365a825..a11bef0 100644 --- a/addon/io_scs_tools/internals/preview_models/__init__.py +++ b/addon/io_scs_tools/internals/preview_models/__init__.py @@ -16,22 +16,28 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy import os +import bmesh from io_scs_tools.internals.preview_models.cache import PrevModelsCache +from io_scs_tools.consts import Material as _MAT_consts +from io_scs_tools.consts import Colors as _COL_consts from io_scs_tools.utils import path as _path_utils from io_scs_tools.utils.printout import lprint +from io_scs_tools.utils import get_scs_globals as _get_scs_globals _cache = PrevModelsCache() def init(): - """Initialize preview models system + """Initialize preview models system by initalizing cache and directly updating all preview models in blend file. """ _cache.init() + update() + def rename(old_name, new_name): """Renames entry in preview models system if it exists @@ -53,14 +59,6 @@ def link(locator, preview_model): :type preview_model: bpy.types.Object """ - # link object to the scene - scene = bpy.context.scene - if not preview_model.name in scene.objects: - scene.objects.link(preview_model) - - # fix scene objects count so it won't trigger another copy cycle - scene.scs_cached_num_objects = len(scene.objects) - # parenting preview_model.parent = locator preview_model.scs_props.parent_identity = locator.name @@ -70,12 +68,21 @@ def link(locator, preview_model): preview_model.scs_props.object_identity = preview_model.name preview_model.hide_select = True - preview_model.layers = locator.layers + # now add preview model to all collections locator is in + for col in locator.users_collection: + if preview_model.name in col.objects: + continue + col.objects.link(preview_model) + + # fix scene objects count so it won't trigger another copy cycle + bpy.context.scene.scs_cached_num_objects = len(bpy.context.scene.objects) + + # insert it to preview models cache _cache.add_entry(locator.name, preview_model.name) # set the correct draw type of the preview model - preview_model.draw_type = locator.scs_props.locator_preview_model_type + preview_model.display_type = locator.scs_props.locator_preview_model_type locator.scs_props.locator_preview_model_present = True @@ -89,12 +96,11 @@ def unlink(preview_model): :type preview_model: bpy.types.Object """ - lprint('D Preview model %r deleted', (preview_model.name,)) + lprint('D Preview model to be deleted: %r', (preview_model.name,)) _cache.delete_entry(preview_model.name) - bpy.context.scene.objects.unlink(preview_model) - bpy.data.objects.remove(preview_model, do_unlink=True) + bpy.data.objects.remove(preview_model) bpy.context.scene.scs_cached_num_objects = len(bpy.context.scene.objects) @@ -132,13 +138,12 @@ def load(locator, deep_reload=False): load_model = False if load_model: - - unload(locator, do_mesh_unlink=deep_reload) - prem_name = str("prem_" + locator.name) - obj = _get_model_mesh(locator, prem_name) + old_mesh = _get_model_mesh(locator) + new_mesh = None - if not obj: + # we need to load preview model, if old mesh is not found or deep reload is requested + if not old_mesh or deep_reload: from io_scs_tools.imp import pim as _pim_import obj = _pim_import.load_pim_file(bpy.context, abs_filepath, preview_model=True) @@ -148,57 +153,81 @@ def load(locator, deep_reload=False): # from possible callbacks trying to fix not present preview model if not obj: message = "Selected PIM model doesn't have any mesh inside, so it can not be used as a preview model." - bpy.ops.wm.show_warning_message('INVOKE_DEFAULT', is_modal=True, title="Preview Model Load Error!", message=message, - width=500, - height=100) + bpy.ops.wm.scs_tools_show_message_in_popup('INVOKE_DEFAULT', + is_modal=True, title="Preview Model Load Error!", message=message, + width=500, + height=100) lprint("E " + message) locator.scs_props.locator_preview_model_path = "" return False - obj.name = prem_name - obj.data.name = prem_name - obj.data.scs_props.locator_preview_model_path = locator.scs_props.locator_preview_model_path - obj.select = False + # got the new mesh! + new_mesh = obj.data + + # set preview model path to mesh, so it can be reused next time user requests same preview model + new_mesh.scs_props.locator_preview_model_path = locator.scs_props.locator_preview_model_path + + # now remove imported object, as we need only mesh + bpy.data.objects.remove(obj, do_unlink=True) + + # if new mesh was created, make sure to copy it over to old one, + # so any locator with same preview model path, will also get reloaded mesh. + if new_mesh and old_mesh: + + # use bmesh module to copy data from mesh to mesh + bm = bmesh.new() + bm.from_mesh(new_mesh) + bm.to_mesh(old_mesh) + bm.free() + + # once copied new mesh can be removed + bpy.data.meshes.remove(new_mesh) + elif not old_mesh: # there is no old mesh yet, thus just link new one to old + old_mesh = new_mesh - link(locator, obj) + # (re)assign material to mesh + old_mesh.materials.clear() + old_mesh.materials.append(_get_material()) + + # recover preview model object if exists, otherwise create new + prev_model_name = _cache.get_entry(locator.name) + if prev_model_name and prev_model_name in bpy.data.objects: + prev_model = bpy.data.objects[prev_model_name] + else: + prev_model = bpy.data.objects.new(name=prem_name, object_data=old_mesh) + + # finally link preview model back to locator + link(locator, prev_model) return True else: return False -def unload(locator, do_mesh_unlink=False): +def unload(locator): """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: if child.data and "scs_props" in child.data: if child.data.scs_props.locator_preview_model_path != "": - # first uncache it - _cache.delete_entry(child.name) - - # delete object & mesh - mesh = child.data + unlink(child) - bpy.context.scene.objects.unlink(child) - bpy.data.objects.remove(child, do_unlink=True) + locator.scs_props.locator_preview_model_present = False - 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) +def fix_visibilites(): + """Fix preview model visibilites and collection assignemenet over all visible objects.""" - locator.scs_props.locator_preview_model_present = False + for obj in bpy.context.view_layer.objects: + fix_visibility(obj) def fix_visibility(locator): - """Fix layers visibility and hide property for preview model on given locator + """Fix collections linking and hide property for preview model on given locator :param locator: locator object for which preview model layers should synced :type locator: bpy.types.Object @@ -217,23 +246,147 @@ def fix_visibility(locator): prevm_obj = bpy.data.objects[prevm_id] # layers check - if list(prevm_obj.layers) != list(locator.layers): - prevm_obj.layers = tuple(locator.layers) + if prevm_obj.users_collection != locator.users_collection: + + # first remove from existing collections + for col in prevm_obj.users_collection: + col.objects.unlink(prevm_obj) + + # now add preview model back to all collections locator is in + for col in locator.users_collection: + col.objects.link(prevm_obj) # hide property check - if prevm_obj.hide != locator.hide: - prevm_obj.hide = locator.hide + if prevm_obj.hide_get() != locator.hide_get(): + prevm_obj.hide_set(locator.hide_get()) + + if prevm_obj.hide_viewport != locator.hide_viewport: + prevm_obj.hide_viewport = locator.hide_viewport + + +def update(force=False): + """Updates all locators with assigned preview models to current visibility settings. + + :param force: force reloads preview models even when they are flagged as loaded (however not a deep reload, existing mesh won't be re-imported) + :type force: bool + """ + show_preview_models = _get_scs_globals().show_preview_models + + # due to delete reasons collect names and operate on names instead directly on bpy.data.objects iterator, just to be safe + obj_names = [] + for obj in bpy.data.objects: + obj_names.append(obj.name) + + for obj_name in obj_names: + if obj_name not in bpy.data.objects: + continue -def _get_model_mesh(locator, prem_name): + obj = bpy.data.objects[obj_name] + + is_prev_model_locator = ( + obj.type == "EMPTY" and + obj.scs_props.empty_object_type == "Locator" and + obj.scs_props.locator_type in {"Prefab", "Model"} + ) + + if not is_prev_model_locator: + continue + + should_have_prev_model = ( + show_preview_models and + obj.scs_props.locator_show_preview_model and + obj.scs_props.locator_preview_model_path != '' + ) + + if should_have_prev_model: + if not obj.scs_props.locator_preview_model_present or force: + if not load(obj): + unload(obj) + else: + fix_visibility(obj) + elif obj.scs_props.locator_preview_model_path != '': + unload(obj) + + +def _get_model_mesh(locator): """Creates and returns new object if mesh already exists in Blender data block - :return: object if mesh exists; None otherwise - :rtype: None | bpy.types.Object + :return: mesh if exists; None otherwise + :rtype: None | bpy.types.Mesh """ for mesh in bpy.data.meshes: if mesh.scs_props.locator_preview_model_path == locator.scs_props.locator_preview_model_path: - obj = bpy.data.objects.new(name=prem_name, object_data=mesh) - return obj + return mesh return None + + +def _get_material(reload=False): + """Creates and returns material for preview objects. + + :param reload: True if material should be rebuilt (use in case sth changes in implementation) + :type reload: bool + :return: preview model material + :rtype: + """ + + # if exists return immediatelly + if _MAT_consts.prevm_material_name in bpy.data.materials and not reload: + return bpy.data.materials[_MAT_consts.prevm_material_name] + + # create new or get existing + if _MAT_consts.prevm_material_name in bpy.data.materials: + mat = bpy.data.materials[_MAT_consts.prevm_material_name] + else: + mat = bpy.data.materials.new(_MAT_consts.prevm_material_name) + + # enable nodes + mat.use_nodes = True + + # sets viewport color for solid rendering + mat.diffuse_color = _COL_consts.prevm_color + + # clear node tree + node_tree = mat.node_tree + node_tree.nodes.clear() + + pos_x_shift = 185 + + # create nodes + light_dir_n = node_tree.nodes.new("ShaderNodeVectorTransform") + light_dir_n.location = (0, 150) + light_dir_n.vector_type = "VECTOR" + light_dir_n.convert_from = "CAMERA" + light_dir_n.convert_to = "WORLD" + light_dir_n.inputs[0].default_value = (0, 0.7071, -0.7071) + + geom_n = node_tree.nodes.new("ShaderNodeNewGeometry") + geom_n.location = (0, -100) + + dot_product_n = node_tree.nodes.new("ShaderNodeVectorMath") + dot_product_n.location = (pos_x_shift, 0) + dot_product_n.operation = "DOT_PRODUCT" + + color_mult_n = node_tree.nodes.new("ShaderNodeVectorMath") + color_mult_n.location = (pos_x_shift * 2, 0) + color_mult_n.operation = "MULTIPLY" + color_mult_n.inputs[1].default_value = _COL_consts.prevm_color[:3] + + emission_shader_n = node_tree.nodes.new("ShaderNodeEmission") + emission_shader_n.location = (pos_x_shift * 3, 0) + + out_mat_n = node_tree.nodes.new("ShaderNodeOutputMaterial") + out_mat_n.location = (pos_x_shift * 4, 0) + + # create links + node_tree.links.new(dot_product_n.inputs[0], light_dir_n.outputs[0]) + node_tree.links.new(dot_product_n.inputs[1], geom_n.outputs['Normal']) + + node_tree.links.new(color_mult_n.inputs[0], dot_product_n.outputs['Value']) + + node_tree.links.new(emission_shader_n.inputs['Color'], color_mult_n.outputs[0]) + + node_tree.links.new(out_mat_n.inputs['Surface'], emission_shader_n.outputs['Emission']) + + return mat diff --git a/addon/io_scs_tools/internals/shaders/__init__.py b/addon/io_scs_tools/internals/shaders/__init__.py index 5a247b6..48bb2c0 100644 --- a/addon/io_scs_tools/internals/shaders/__init__.py +++ b/addon/io_scs_tools/internals/shaders/__init__.py @@ -20,16 +20,23 @@ from io_scs_tools.internals.shaders.eut2.dif_anim import anim_blend_factor_ng +from io_scs_tools.internals.shaders.eut2.water import water_stream_ng from io_scs_tools.internals.shaders.eut2.truckpaint import Truckpaint from io_scs_tools.internals.shaders.flavors import paint -def update_shaders(): +def update_shaders(scene): """Update any time changes in shaders. + + :param scene: scene in which time for shaders is being updated + :type scene: bpy.types.Scene """ # update animation blend factor group node - anim_blend_factor_ng.update_time() + anim_blend_factor_ng.update_time(scene) + + # update water streams + water_stream_ng.update_time(scene) def set_base_paint_color(node_tree, color): diff --git a/addon/io_scs_tools/internals/shaders/base.py b/addon/io_scs_tools/internals/shaders/base.py new file mode 100644 index 0000000..e2f14d1 --- /dev/null +++ b/addon/io_scs_tools/internals/shaders/base.py @@ -0,0 +1,52 @@ +# ##### 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 + + +class BaseShader: + """Class implementing base shader interface. + + This should be parent class of any shader implementation, + to ensure compatibility of shader setup invoking. + """ + + @staticmethod + def get_name(): + """Get name of this shader file with full modules path.""" + raise NotImplementedError("Base effect get_name invoked, shader needs it's own implementation") + + @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 + """ + raise NotImplementedError("Base effect init invoked, shader needs it's own implementation") + + @staticmethod + def finalize(node_tree, material): + """Finalize node tree and material settings. Should be called as last. + + :param node_tree: node tree on which this shader should be finalized + :type node_tree: bpy.types.NodeTree + :param material: material used for this shader + :type material: bpy.types.Material + """ + raise NotImplementedError("Base effect finalize invoked, shader needs it's own implementation") diff --git a/addon/io_scs_tools/internals/shaders/eut2/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/__init__.py index 488a411..8e9cd4e 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/__init__.py @@ -84,6 +84,10 @@ def get_shader(effect): from io_scs_tools.internals.shaders.eut2.retroreflective import Retroreflective as Shader + elif effect.startswith("unlit.tex.a8"): + + from io_scs_tools.internals.shaders.eut2.unlit_tex.a8 import UnlitTexA8 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/building/add_env_day.py b/addon/io_scs_tools/internals/shaders/eut2/building/add_env_day.py index 3257bf0..a65a84a 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/building/add_env_day.py +++ b/addon/io_scs_tools/internals/shaders/eut2/building/add_env_day.py @@ -18,7 +18,6 @@ # Copyright (C) 2015: SCS Software - from io_scs_tools.internals.shaders.eut2.dif_spec_add_env import DifSpecAddEnv @@ -39,12 +38,6 @@ def init(node_tree): # init parent DifSpecAddEnv.init(node_tree) - geometry_n = node_tree.nodes[DifSpecAddEnv.GEOM_NODE] - refl_tex_n = node_tree.nodes[DifSpecAddEnv.REFL_TEX_NODE] - - # change mapping of reflection texture to View coordinates, it gives better effect for plain surfaces - node_tree.links.new(refl_tex_n.inputs['Vector'], geometry_n.outputs['View']) - @staticmethod def set_aux5(node_tree, aux_property): """Set luminance boost factor for the shader. diff --git a/addon/io_scs_tools/internals/shaders/eut2/building/day.py b/addon/io_scs_tools/internals/shaders/eut2/building/day.py index efa2de0..c4dd6e8 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/building/day.py +++ b/addon/io_scs_tools/internals/shaders/eut2/building/day.py @@ -18,7 +18,6 @@ # Copyright (C) 2015: SCS Software - from io_scs_tools.internals.shaders.eut2.dif_spec import DifSpec diff --git a/addon/io_scs_tools/internals/shaders/eut2/building/lvcol_day.py b/addon/io_scs_tools/internals/shaders/eut2/building/lvcol_day.py index 2ebf728..27d63fa 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/building/lvcol_day.py +++ b/addon/io_scs_tools/internals/shaders/eut2/building/lvcol_day.py @@ -18,7 +18,6 @@ # Copyright (C) 2015: SCS Software - from io_scs_tools.internals.shaders.eut2.dif_spec import DifSpec diff --git a/addon/io_scs_tools/internals/shaders/eut2/decalshadow/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/decalshadow/__init__.py index 88aff70..779b565 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/decalshadow/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/decalshadow/__init__.py @@ -16,13 +16,12 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software +from io_scs_tools.internals.shaders.eut2.unlit_tex.a8 import UnlitTexA8 -from io_scs_tools.internals.shaders.eut2.unlit_vcol_tex import UnlitVcolTex - -class Decalshadow(UnlitVcolTex): +class Decalshadow(UnlitTexA8): @staticmethod def get_name(): """Get name of this shader file with full modules path.""" @@ -37,22 +36,7 @@ def init(node_tree): """ # init parent - UnlitVcolTex.init(node_tree) + UnlitTexA8.init(node_tree) # enable hardcoded flavors: DEPTH, BLEND_OVER - UnlitVcolTex.set_blend_over_flavor(node_tree, True) - - @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 - """ - - UnlitVcolTex.set_material(node_tree, material) - - material.use_transparency = True - material.transparency_method = "MASK" + UnlitTexA8.set_blend_over_flavor(node_tree, True) diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/dif/__init__.py index 9caddcb..8c9d893 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif/__init__.py @@ -16,32 +16,36 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.consts import Mesh as _MESH_consts -from io_scs_tools.internals.shaders.eut2.std_node_groups import compose_lighting -from io_scs_tools.internals.shaders.eut2.std_node_groups import vcolor_input +from io_scs_tools.internals.shaders.base import BaseShader +from io_scs_tools.internals.shaders.eut2.std_node_groups import compose_lighting_ng +from io_scs_tools.internals.shaders.eut2.std_node_groups import lighting_evaluator_ng +from io_scs_tools.internals.shaders.eut2.std_node_groups import vcolor_input_ng from io_scs_tools.internals.shaders.flavors import alpha_test from io_scs_tools.internals.shaders.flavors import blend_over from io_scs_tools.internals.shaders.flavors import blend_add +from io_scs_tools.internals.shaders.flavors import blend_mult from io_scs_tools.internals.shaders.flavors import nmap from io_scs_tools.internals.shaders.flavors import paint from io_scs_tools.internals.shaders.flavors import tg0 from io_scs_tools.utils import convert as _convert_utils +from io_scs_tools.utils import material as _material_utils -class Dif: +class Dif(BaseShader): DIFF_COL_NODE = "DiffuseColor" SPEC_COL_NODE = "SpecularColor" GEOM_NODE = "Geometry" + UVMAP_NODE = "FirstUVs" VCOL_GROUP_NODE = "VColorGroup" OPACITY_NODE = "OpacityMultiplier" BASE_TEX_NODE = "BaseTex" DIFF_MULT_NODE = "DiffMultiplier" VCOLOR_MULT_NODE = "VertexColorMultiplier" VCOLOR_SCALE_NODE = "VertexColorScale" - OUT_MAT_NODE = "InputMaterial" + LIGHTING_EVAL_NODE = "LightingEvaluator" COMPOSE_LIGHTING_NODE = "ComposeLighting" OUTPUT_NODE = "Output" @@ -68,13 +72,18 @@ def init(node_tree): vcol_group_n.name = Dif.VCOL_GROUP_NODE vcol_group_n.label = Dif.VCOL_GROUP_NODE vcol_group_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1650) - vcol_group_n.node_tree = vcolor_input.get_node_group() + vcol_group_n.node_tree = vcolor_input_ng.get_node_group() + + uvmap_n = node_tree.nodes.new("ShaderNodeUVMap") + uvmap_n.name = Dif.UVMAP_NODE + uvmap_n.label = Dif.UVMAP_NODE + uvmap_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1500) + uvmap_n.uv_map = _MESH_consts.none_uv - geometry_n = node_tree.nodes.new("ShaderNodeGeometry") + geometry_n = node_tree.nodes.new("ShaderNodeNewGeometry") geometry_n.name = Dif.GEOM_NODE geometry_n.label = Dif.GEOM_NODE - geometry_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1500) - geometry_n.uv_layer = _MESH_consts.none_uv + geometry_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1350) diff_col_n = node_tree.nodes.new("ShaderNodeRGB") diff_col_n.name = Dif.DIFF_COL_NODE @@ -86,13 +95,12 @@ def init(node_tree): spec_col_n.label = Dif.SPEC_COL_NODE spec_col_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1900) - vcol_scale_n = node_tree.nodes.new("ShaderNodeMixRGB") + vcol_scale_n = node_tree.nodes.new("ShaderNodeVectorMath") vcol_scale_n.name = Dif.VCOLOR_SCALE_NODE vcol_scale_n.label = Dif.VCOLOR_SCALE_NODE vcol_scale_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1550) - vcol_scale_n.blend_type = "MULTIPLY" - vcol_scale_n.inputs['Fac'].default_value = 1 - vcol_scale_n.inputs['Color2'].default_value = (2,) * 4 + vcol_scale_n.operation = "MULTIPLY" + vcol_scale_n.inputs[1].default_value = (2,) * 3 opacity_n = node_tree.nodes.new("ShaderNodeMath") opacity_n.name = Dif.OPACITY_NODE @@ -100,83 +108,104 @@ def init(node_tree): opacity_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1300) opacity_n.operation = "MULTIPLY" - base_tex_n = node_tree.nodes.new("ShaderNodeTexture") + base_tex_n = node_tree.nodes.new("ShaderNodeTexImage") base_tex_n.name = Dif.BASE_TEX_NODE base_tex_n.label = Dif.BASE_TEX_NODE base_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1500) + base_tex_n.width = 140 - vcol_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") + vcol_mult_n = node_tree.nodes.new("ShaderNodeVectorMath") vcol_mult_n.name = Dif.VCOLOR_MULT_NODE vcol_mult_n.label = Dif.VCOLOR_MULT_NODE vcol_mult_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 1500) - vcol_mult_n.blend_type = "MULTIPLY" - vcol_mult_n.inputs['Fac'].default_value = 1 + vcol_mult_n.operation = "MULTIPLY" - diff_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") + diff_mult_n = node_tree.nodes.new("ShaderNodeVectorMath") diff_mult_n.name = Dif.DIFF_MULT_NODE diff_mult_n.label = Dif.DIFF_MULT_NODE diff_mult_n.location = (start_pos_x + pos_x_shift * 5, start_pos_y + 1650) - diff_mult_n.blend_type = "MULTIPLY" - diff_mult_n.inputs['Fac'].default_value = 1 - diff_mult_n.inputs['Color2'].default_value = (0, 0, 0, 1) - - out_mat_n = node_tree.nodes.new("ShaderNodeExtendedMaterial") - out_mat_n.name = Dif.OUT_MAT_NODE - out_mat_n.label = Dif.OUT_MAT_NODE - if "SpecTra" in out_mat_n: - out_mat_n.inputs['SpecTra'].default_value = 0.0 - if "Refl" in out_mat_n: - out_mat_n.inputs['Refl'].default_value = 1.0 - elif "Reflectivity" in out_mat_n: - out_mat_n.inputs['Reflectivity'].default_value = 1.0 - out_mat_n.location = (start_pos_x + pos_x_shift * 7, start_pos_y + 1800) + diff_mult_n.operation = "MULTIPLY" + diff_mult_n.inputs[1].default_value = (0, 0, 0) + + lighting_eval_n = node_tree.nodes.new("ShaderNodeGroup") + lighting_eval_n.name = Dif.LIGHTING_EVAL_NODE + lighting_eval_n.label = Dif.LIGHTING_EVAL_NODE + lighting_eval_n.location = (start_pos_x + pos_x_shift * 7, start_pos_y + 1800) + lighting_eval_n.node_tree = lighting_evaluator_ng.get_node_group() compose_lighting_n = node_tree.nodes.new("ShaderNodeGroup") compose_lighting_n.name = Dif.COMPOSE_LIGHTING_NODE compose_lighting_n.label = Dif.COMPOSE_LIGHTING_NODE compose_lighting_n.location = (start_pos_x + pos_x_shift * 8, start_pos_y + 2000) - compose_lighting_n.node_tree = compose_lighting.get_node_group() + compose_lighting_n.node_tree = compose_lighting_ng.get_node_group() + compose_lighting_n.inputs["Alpha"].default_value = 1.0 - output_n = node_tree.nodes.new("ShaderNodeOutput") + output_n = node_tree.nodes.new("ShaderNodeOutputMaterial") output_n.name = Dif.OUTPUT_NODE output_n.label = Dif.OUTPUT_NODE output_n.location = (start_pos_x + pos_x_shift * 9, start_pos_y + 1800) # links creation - node_tree.links.new(base_tex_n.inputs['Vector'], geometry_n.outputs['UV']) - node_tree.links.new(vcol_scale_n.inputs['Color1'], vcol_group_n.outputs['Vertex Color']) + node_tree.links.new(base_tex_n.inputs['Vector'], uvmap_n.outputs['UV']) + node_tree.links.new(vcol_scale_n.inputs[0], vcol_group_n.outputs['Vertex Color']) - node_tree.links.new(vcol_mult_n.inputs['Color1'], vcol_scale_n.outputs['Color']) - node_tree.links.new(vcol_mult_n.inputs['Color2'], base_tex_n.outputs['Color']) + node_tree.links.new(vcol_mult_n.inputs[0], vcol_scale_n.outputs[0]) + node_tree.links.new(vcol_mult_n.inputs[1], base_tex_n.outputs['Color']) - node_tree.links.new(diff_mult_n.inputs['Color1'], diff_col_n.outputs['Color']) - node_tree.links.new(diff_mult_n.inputs['Color2'], vcol_mult_n.outputs['Color']) - node_tree.links.new(opacity_n.inputs[0], base_tex_n.outputs["Value"]) + node_tree.links.new(diff_mult_n.inputs[0], diff_col_n.outputs['Color']) + node_tree.links.new(diff_mult_n.inputs[1], vcol_mult_n.outputs[0]) + node_tree.links.new(opacity_n.inputs[0], base_tex_n.outputs["Alpha"]) node_tree.links.new(opacity_n.inputs[1], vcol_group_n.outputs["Vertex Color Alpha"]) - node_tree.links.new(out_mat_n.inputs['Color'], diff_mult_n.outputs['Color']) - node_tree.links.new(out_mat_n.inputs['Spec'], spec_col_n.outputs['Color']) + node_tree.links.new(lighting_eval_n.inputs['Normal Vector'], geometry_n.outputs['Normal']) + node_tree.links.new(lighting_eval_n.inputs['Incoming Vector'], geometry_n.outputs['Incoming']) - node_tree.links.new(compose_lighting_n.inputs['Diffuse Color'], diff_mult_n.outputs['Color']) - node_tree.links.new(compose_lighting_n.inputs['Material Color'], out_mat_n.outputs['Color']) + node_tree.links.new(compose_lighting_n.inputs['Specular Color'], spec_col_n.outputs['Color']) + node_tree.links.new(compose_lighting_n.inputs['Diffuse Color'], diff_mult_n.outputs[0]) + node_tree.links.new(compose_lighting_n.inputs['Specular Lighting'], lighting_eval_n.outputs['Specular Lighting']) + node_tree.links.new(compose_lighting_n.inputs['Diffuse Lighting'], lighting_eval_n.outputs['Diffuse Lighting']) + node_tree.links.new(compose_lighting_n.inputs['Alpha'], opacity_n.outputs['Value']) - node_tree.links.new(output_n.inputs['Color'], compose_lighting_n.outputs['Composed Color']) - node_tree.links.new(output_n.inputs['Alpha'], out_mat_n.outputs['Alpha']) + node_tree.links.new(output_n.inputs['Surface'], compose_lighting_n.outputs['Shader']) @staticmethod - def set_material(node_tree, material): - """Set output material for this shader. + def finalize(node_tree, material): + """Finalize node tree and material settings. Should be called as last. - :param node_tree: node tree of current shader + :param node_tree: node tree on which this shader should be finalized :type node_tree: bpy.types.NodeTree - :param material: blender material for used in this tree node as output + :param material: material used for this shader :type material: bpy.types.Material """ - node_tree.nodes[Dif.OUT_MAT_NODE].material = material + material.use_backface_culling = True + material.blend_method = "OPAQUE" + + # set proper blend method and possible alpha test pass + if alpha_test.is_set(node_tree): + material.blend_method = "CLIP" + material.alpha_threshold = 0.05 + + # add alpha test pass if multiply blend enabled, where alphed pixels shouldn't be multiplied as they are discarded + if blend_mult.is_set(node_tree): + compose_lighting_n = node_tree.nodes[Dif.COMPOSE_LIGHTING_NODE] + + # alpha test pass has to get fully opaque input, thus remove transparency linkage + if compose_lighting_n.inputs['Alpha'].links: + node_tree.links.remove(compose_lighting_n.inputs['Alpha'].links[0]) - # make sure to reset to lambert always as flat flavor might use fresnel diffuse shader - material.diffuse_shader = "LAMBERT" + shader_from = compose_lighting_n.outputs['Shader'] + alpha_from = node_tree.nodes[Dif.OPACITY_NODE].outputs[0] + shader_to = compose_lighting_n.outputs['Shader'].links[0].to_socket + + alpha_test.add_pass(node_tree, shader_from, alpha_from, shader_to) + + if blend_add.is_set(node_tree): + material.blend_method = "BLEND" + if blend_mult.is_set(node_tree): + material.blend_method = "BLEND" + if blend_over.is_set(node_tree): + material.blend_method = "BLEND" @staticmethod def set_add_ambient(node_tree, factor): @@ -203,8 +232,6 @@ def set_diffuse(node_tree, color): color = _convert_utils.to_node_color(color) node_tree.nodes[Dif.DIFF_COL_NODE].outputs['Color'].default_value = color - # fix intensity each time if user might changed it by hand directly on material - node_tree.nodes[Dif.OUT_MAT_NODE].material.diffuse_intensity = 0.7 @staticmethod def set_specular(node_tree, color): @@ -219,8 +246,6 @@ def set_specular(node_tree, color): color = _convert_utils.to_node_color(color) node_tree.nodes[Dif.SPEC_COL_NODE].outputs['Color'].default_value = color - # fix intensity each time if user might changed it by hand directly on material - node_tree.nodes[Dif.OUT_MAT_NODE].material.specular_intensity = 1.0 @staticmethod def set_shininess(node_tree, factor): @@ -232,7 +257,7 @@ def set_shininess(node_tree, factor): :type factor: float """ - node_tree.nodes[Dif.OUT_MAT_NODE].material.specular_hardness = factor + node_tree.nodes[Dif.LIGHTING_EVAL_NODE].inputs["Shininess"].default_value = factor @staticmethod def set_reflection(node_tree, value): @@ -252,23 +277,46 @@ def set_shadow_bias(node_tree, value): :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param value: blender material for used in this tree node as output + :param value: shador bias factor :type value: float """ pass # NOTE: shadow bias won't be visualized as game uses it's own implementation @staticmethod - def set_base_texture(node_tree, texture): + def set_queue_bias(node_tree, value): + """Set queue bias attirbute for this shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param value: queue bias index + :type value: int + """ + + pass # NOTE: shadow bias won't be visualized as game uses it's own implementation + + @staticmethod + def set_base_texture(node_tree, image): """Set base texture to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assignet to base texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assignet to base texture node + :type image: bpy.types.Image """ - node_tree.nodes[Dif.BASE_TEX_NODE].texture = texture + node_tree.nodes[Dif.BASE_TEX_NODE].image = image + + @staticmethod + def set_base_texture_settings(node_tree, settings): + """Set base texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + _material_utils.set_texture_settings_to_node(node_tree.nodes[Dif.BASE_TEX_NODE], settings) @staticmethod def set_base_uv(node_tree, uv_layer): @@ -283,7 +331,7 @@ def set_base_uv(node_tree, uv_layer): if uv_layer is None or uv_layer == "": uv_layer = _MESH_consts.none_uv - node_tree.nodes[Dif.GEOM_NODE].uv_layer = uv_layer + node_tree.nodes[Dif.UVMAP_NODE].uv_map = uv_layer @staticmethod def set_alpha_test_flavor(node_tree, switch_on): @@ -295,12 +343,8 @@ def set_alpha_test_flavor(node_tree, switch_on): :type switch_on: bool """ - if switch_on and not blend_over.is_set(node_tree): - out_node = node_tree.nodes[Dif.OUT_MAT_NODE] - in_node = node_tree.nodes[Dif.OPACITY_NODE] - location = (out_node.location.x - 185 * 2, out_node.location.y - 500) - - alpha_test.init(node_tree, location, in_node.outputs['Value'], out_node.inputs['Alpha']) + if switch_on: + alpha_test.init(node_tree) else: alpha_test.delete(node_tree) @@ -310,19 +354,12 @@ def set_blend_over_flavor(node_tree, switch_on): :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 + :param switch_on: flag indication if blending 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): - Dif.set_alpha_test_flavor(node_tree, False) - - out_node = node_tree.nodes[Dif.OUT_MAT_NODE] - in_node = node_tree.nodes[Dif.OPACITY_NODE] - if switch_on: - blend_over.init(node_tree, in_node.outputs['Value'], out_node.inputs['Alpha']) + blend_over.init(node_tree) else: blend_over.delete(node_tree) @@ -332,22 +369,48 @@ def set_blend_add_flavor(node_tree, switch_on): :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 + :param switch_on: flag indication if blending 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): - Dif.set_alpha_test_flavor(node_tree, False) + if switch_on: + out_node = node_tree.nodes[Dif.OUTPUT_NODE] + in_node = node_tree.nodes[Dif.COMPOSE_LIGHTING_NODE] - out_node = node_tree.nodes[Dif.OUT_MAT_NODE] - in_node = node_tree.nodes[Dif.OPACITY_NODE] + # put it on location of output node & move output node for one slot to the right + location = tuple(out_node.location) + out_node.location.x += 185 - if switch_on: - blend_add.init(node_tree, in_node.outputs['Value'], out_node.inputs['Alpha']) + blend_add.init(node_tree, location, in_node.outputs['Shader'], out_node.inputs['Surface']) else: blend_add.delete(node_tree) + @staticmethod + def set_blend_mult_flavor(node_tree, switch_on): + """Set blend mult flavor to this shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param switch_on: flag indication if blending should be switched on or off + :type switch_on: bool + """ + + if switch_on: + out_node = node_tree.nodes[Dif.OUTPUT_NODE] + in_node = node_tree.nodes[Dif.COMPOSE_LIGHTING_NODE] + + # break link to compose lighting node alpha as mult uses DST_COLOR as source factor in blend function + if in_node.inputs['Alpha'].links: + node_tree.links.remove(in_node.inputs['Alpha'].links[0]) + + # put it on location of output node & move output node for one slot to the right + location = tuple(out_node.location) + out_node.location.x += 185 + + blend_mult.init(node_tree, location, in_node.outputs['Shader'], out_node.inputs['Surface']) + else: + blend_mult.delete(node_tree) + @staticmethod def set_nmap_flavor(node_tree, switch_on): """Set normal map flavor to this shader. @@ -366,24 +429,36 @@ def set_nmap_flavor(node_tree, switch_on): if node.location.x <= 185 and (min_y is None or min_y > node.location.y): min_y = node.location.y - out_node = node_tree.nodes[Dif.OUT_MAT_NODE] - location = (out_node.location.x - 185, min_y - 400) + lighting_eval_n = node_tree.nodes[Dif.LIGHTING_EVAL_NODE] + geom_n = node_tree.nodes[Dif.GEOM_NODE] + location = (lighting_eval_n.location.x - 185, min_y - 400) - nmap.init(node_tree, location, out_node.inputs['Normal']) + nmap.init(node_tree, location, lighting_eval_n.inputs['Normal Vector'], geom_n.outputs['Normal']) else: nmap.delete(node_tree) @staticmethod - def set_nmap_texture(node_tree, texture): + def set_nmap_texture(node_tree, image): """Set normal map texture to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assignet to nmap texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assignet to nmap texture node + :type image: bpy.types.Image """ - nmap.set_texture(node_tree, texture) + nmap.set_texture(node_tree, image) + + @staticmethod + def set_nmap_texture_settings(node_tree, settings): + """Set normal map texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + nmap.set_texture_settings(node_tree, settings) @staticmethod def set_nmap_uv(node_tree, uv_layer): @@ -415,7 +490,7 @@ def set_tg0_flavor(node_tree, switch_on): out_node.location.x -= 185 location = (out_node.location.x + 185, out_node.location.y) - tg0.init(node_tree, location, out_node.outputs["Global"], in_node.inputs["Vector"]) + tg0.init(node_tree, location, out_node.outputs["Position"], in_node.inputs["Vector"]) elif not switch_on: @@ -445,46 +520,12 @@ def set_flat_flavor(node_tree, switch_on): :type switch_on: bool """ - _FLAT_FAC_MULT_NODE = "FlatFlavorMult" - - out_mat_n = node_tree.nodes[Dif.OUT_MAT_NODE] - compose_lighting_n = node_tree.nodes[Dif.COMPOSE_LIGHTING_NODE] - output_n = node_tree.nodes[Dif.OUTPUT_NODE] - diff_mult_n = node_tree.nodes[Dif.DIFF_MULT_NODE] + lighting_eval_n = node_tree.nodes[Dif.LIGHTING_EVAL_NODE] if switch_on: - - out_mat_n.use_specular = False - out_mat_n.material.diffuse_shader = "FRESNEL" - out_mat_n.material.diffuse_fresnel = 0 - - out_mat_n.location.x += 185 - output_n.location.x += 185 - - flat_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") - flat_mult_n.name = flat_mult_n.label = _FLAT_FAC_MULT_NODE - flat_mult_n.location = (out_mat_n.location.x - 185 * 2, diff_mult_n.location.y) - flat_mult_n.blend_type = "MULTIPLY" - flat_mult_n.inputs['Fac'].default_value = 1 - flat_mult_n.inputs['Color2'].default_value = (0.4,) * 3 + (1,) # factor is 0.4 - - node_tree.links.new(flat_mult_n.inputs['Color1'], diff_mult_n.outputs['Color']) - node_tree.links.new(out_mat_n.inputs['Color'], flat_mult_n.outputs['Color']) - node_tree.links.new(compose_lighting_n.inputs['Material Color'], flat_mult_n.outputs['Color']) - + lighting_eval_n.inputs["Flat Lighting"].default_value = 1.0 else: - - out_mat_n.use_specular = True - out_mat_n.material.diffuse_shader = "LAMBERT" - - out_mat_n.location.x -= 185 - output_n.location.x -= 185 - - if _FLAT_FAC_MULT_NODE in node_tree.nodes: - node_tree.nodes.remove(node_tree.nodes[_FLAT_FAC_MULT_NODE]) - - node_tree.links.new(out_mat_n.inputs['Color'], diff_mult_n.outputs['Color']) - node_tree.links.new(compose_lighting_n.inputs['Material Color'], diff_mult_n.outputs['Color']) + lighting_eval_n.inputs["Flat Lighting"].default_value = 0.0 @staticmethod def set_paint_flavor(node_tree, switch_on): @@ -502,7 +543,7 @@ def set_paint_flavor(node_tree, switch_on): if switch_on: location = (diff_mult_n.location.x - 185, diff_mult_n.location.y + 50) - paint.init(node_tree, location, diff_col_n.outputs["Color"], diff_mult_n.inputs["Color1"]) + paint.init(node_tree, location, diff_col_n.outputs["Color"], diff_mult_n.inputs[0]) else: paint.delete(node_tree) diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_anim/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/dif_anim/__init__.py index 57b4518..243cbfb 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_anim/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_anim/__init__.py @@ -16,20 +16,18 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.consts import Mesh as _MESH_consts from io_scs_tools.internals.shaders.eut2.dif import Dif from io_scs_tools.internals.shaders.eut2.dif_anim import anim_blend_factor_ng -from io_scs_tools.internals.shaders.flavors import alpha_test -from io_scs_tools.internals.shaders.flavors import blend_add -from io_scs_tools.internals.shaders.flavors import blend_over +from io_scs_tools.utils import material as _material_utils class DifAnim(Dif): ANIM_SPEED_NODE = "SpeedNode" BLEND_FACTOR_NODE = "BlendFactorGNode" - SEC_GEOM_NODE = "SecondGeometry" + SEC_UVMAP_NODE = "SecondUVMap" OVER_TEX_NODE = "OverTex" BASE_OVER_MIX_NODE = "BaseOverMix" @@ -56,6 +54,7 @@ def init(node_tree): base_tex_n = node_tree.nodes[Dif.BASE_TEX_NODE] vcol_mult_n = node_tree.nodes[Dif.VCOLOR_MULT_NODE] + compose_lighting_n = node_tree.nodes[Dif.COMPOSE_LIGHTING_NODE] # delete existing node_tree.nodes.remove(node_tree.nodes[Dif.OPACITY_NODE]) @@ -65,19 +64,20 @@ def init(node_tree): anim_speed_n.name = anim_speed_n.label = DifAnim.ANIM_SPEED_NODE anim_speed_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1100) - sec_geom_n = node_tree.nodes.new("ShaderNodeGeometry") - sec_geom_n.name = sec_geom_n.label = DifAnim.SEC_GEOM_NODE - sec_geom_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1000) - sec_geom_n.uv_layer = _MESH_consts.none_uv + sec_uvmap_n = node_tree.nodes.new("ShaderNodeUVMap") + sec_uvmap_n.name = sec_uvmap_n.label = DifAnim.SEC_UVMAP_NODE + sec_uvmap_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1000) + sec_uvmap_n.uv_map = _MESH_consts.none_uv blend_fac_gn = node_tree.nodes.new("ShaderNodeGroup") blend_fac_gn.name = blend_fac_gn.label = anim_blend_factor_ng.BLEND_FACTOR_G blend_fac_gn.location = (start_pos_x + pos_x_shift, start_pos_y + 1200) blend_fac_gn.node_tree = anim_blend_factor_ng.get_node_group() - over_tex_n = node_tree.nodes.new("ShaderNodeTexture") + over_tex_n = node_tree.nodes.new("ShaderNodeTexImage") over_tex_n.name = over_tex_n.label = DifAnim.OVER_TEX_NODE over_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1000) + over_tex_n.width = 140 base_over_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") base_over_mix_n.name = base_over_mix_n.label = DifAnim.BASE_OVER_MIX_NODE @@ -91,7 +91,7 @@ def init(node_tree): # links creation node_tree.links.new(blend_fac_gn.inputs['Speed'], anim_speed_n.outputs[0]) - node_tree.links.new(over_tex_n.inputs['Vector'], sec_geom_n.outputs['UV']) + node_tree.links.new(over_tex_n.inputs['Vector'], sec_uvmap_n.outputs['UV']) # pass 1 node_tree.links.new(base_over_mix_n.inputs['Fac'], blend_fac_gn.outputs['Factor']) @@ -99,101 +99,52 @@ def init(node_tree): node_tree.links.new(base_over_mix_n.inputs['Color2'], over_tex_n.outputs['Color']) node_tree.links.new(opacity_n.inputs['Fac'], blend_fac_gn.outputs['Factor']) - node_tree.links.new(opacity_n.inputs['Color1'], base_tex_n.outputs['Value']) - node_tree.links.new(opacity_n.inputs['Color2'], over_tex_n.outputs['Value']) + node_tree.links.new(opacity_n.inputs['Color1'], base_tex_n.outputs['Alpha']) + node_tree.links.new(opacity_n.inputs['Color2'], over_tex_n.outputs['Alpha']) # pass 2 - node_tree.links.new(vcol_mult_n.inputs['Color2'], base_over_mix_n.outputs['Color']) + node_tree.links.new(vcol_mult_n.inputs[1], base_over_mix_n.outputs['Color']) - @staticmethod - def set_over_texture(node_tree, texture): - """Set overlying texture to shader. - - :param node_tree: node tree of current shader - :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assigned to over texture node - :type texture: bpy.types.Texture - """ - - node_tree.nodes[DifAnim.OVER_TEX_NODE].texture = texture + # pass 3 + node_tree.links.new(compose_lighting_n.inputs['Alpha'], opacity_n.outputs['Color']) @staticmethod - def set_over_uv(node_tree, uv_layer): - """Set UV layer to overlying texture in shader. - - :param node_tree: node tree of current shader - :type node_tree: bpy.types.NodeTree - :param uv_layer: uv layer string used for over texture - :type uv_layer: str - """ - - if uv_layer is None or uv_layer == "": - uv_layer = _MESH_consts.none_uv - - node_tree.nodes[DifAnim.SEC_GEOM_NODE].uv_layer = uv_layer - - @staticmethod - def set_alpha_test_flavor(node_tree, switch_on): - """Set alpha test flavor to this shader. + def set_over_texture(node_tree, image): + """Set overlying texture to 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 + :param image: texture image which should be assigned to over texture node + :type image: bpy.types.Image """ - if switch_on and not blend_over.is_set(node_tree): - out_node = node_tree.nodes[Dif.OUT_MAT_NODE] - in_node = node_tree.nodes[Dif.OPACITY_NODE] - location = (out_node.location.x - 185 * 2, out_node.location.y - 500) - - alpha_test.init(node_tree, location, in_node.outputs['Color'], out_node.inputs['Alpha']) - else: - alpha_test.delete(node_tree) + node_tree.nodes[DifAnim.OVER_TEX_NODE].image = image @staticmethod - def set_blend_over_flavor(node_tree, switch_on): - """Set blend over flavor to this shader. + def set_over_texture_settings(node_tree, settings): + """Set over texture settings to 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 + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str """ - - # remove alpha test flavor if it was set already. Because these two can not coexist - if alpha_test.is_set(node_tree): - Dif.set_alpha_test_flavor(node_tree, False) - - out_node = node_tree.nodes[Dif.OUT_MAT_NODE] - in_node = node_tree.nodes[Dif.OPACITY_NODE] - - if switch_on: - blend_over.init(node_tree, in_node.outputs['Color'], out_node.inputs['Alpha']) - else: - blend_over.delete(node_tree) + _material_utils.set_texture_settings_to_node(node_tree.nodes[DifAnim.OVER_TEX_NODE], settings) @staticmethod - def set_blend_add_flavor(node_tree, switch_on): - """Set blend add flavor to this shader. + def set_over_uv(node_tree, uv_layer): + """Set UV layer to overlying texture in 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 + :param uv_layer: uv layer string used for over texture + :type uv_layer: str """ - # remove alpha test flavor if it was set already. Because these two can not coexist - if alpha_test.is_set(node_tree): - Dif.set_alpha_test_flavor(node_tree, False) - - out_node = node_tree.nodes[Dif.OUT_MAT_NODE] - in_node = node_tree.nodes[Dif.OPACITY_NODE] + if uv_layer is None or uv_layer == "": + uv_layer = _MESH_consts.none_uv - if switch_on: - blend_add.init(node_tree, in_node.outputs['Color'], out_node.inputs['Alpha']) - else: - blend_add.delete(node_tree) + node_tree.nodes[DifAnim.SEC_UVMAP_NODE].uv_map = uv_layer @staticmethod def set_aux0(node_tree, aux_property): diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_anim/anim_blend_factor_ng.py b/addon/io_scs_tools/internals/shaders/eut2/dif_anim/anim_blend_factor_ng.py index 1e356c0..8f02e4f 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_anim/anim_blend_factor_ng.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_anim/anim_blend_factor_ng.py @@ -19,12 +19,11 @@ # Copyright (C) 2015: SCS Software import bpy -from time import time from io_scs_tools.consts import Material as _MAT_consts BLEND_FACTOR_G = _MAT_consts.node_group_prefix + "BlendFactor" -ANIM_TIME_NODE = "AnimTime" +_ANIM_TIME_NODE = "AnimTime" def get_node_group(): @@ -40,14 +39,18 @@ def get_node_group(): return bpy.data.node_groups[BLEND_FACTOR_G] -def update_time(): +def update_time(scene): """Updates time value used in calculation of blend factor. + + :param scene: scene in which time for shaders is being updated + :type scene: bpy.types.Scene """ if BLEND_FACTOR_G not in bpy.data.node_groups: return - bpy.data.node_groups[BLEND_FACTOR_G].nodes[ANIM_TIME_NODE].outputs[0].default_value = time() % 60.0 + time = scene.frame_current / (scene.render.fps / scene.render.fps_base) + bpy.data.node_groups[BLEND_FACTOR_G].nodes[_ANIM_TIME_NODE].outputs[0].default_value = time def __create_node_group__(): @@ -77,7 +80,7 @@ def __create_node_group__(): # node creation anim_time_n = blend_fac_g.nodes.new("ShaderNodeValue") - anim_time_n.name = anim_time_n.label = ANIM_TIME_NODE + anim_time_n.name = anim_time_n.label = _ANIM_TIME_NODE anim_time_n.location = (start_pos_x, start_pos_y + 200) equation_nodes = [] diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_lum/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/dif_lum/__init__.py index 166b1f7..f3570f0 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_lum/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_lum/__init__.py @@ -16,14 +16,19 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.internals.shaders.eut2.dif import Dif +from io_scs_tools.internals.shaders.flavors import alpha_test +from io_scs_tools.internals.shaders.flavors import blend_over +from io_scs_tools.internals.shaders.flavors import blend_add +from io_scs_tools.internals.shaders.flavors import blend_mult class DifLum(Dif): LUM_MIX_NODE = "LuminosityMix" + LUM_A_INVERSE_NODE = "LumTransp=1-Alpha" + LUM_OUT_SHADER_NODE = "LumShader" @staticmethod def get_name(): @@ -38,9 +43,6 @@ def init(node_tree): :type node_tree: bpy.types.NodeTree """ - start_pos_x = 0 - start_pos_y = 0 - pos_x_shift = 185 # init parent @@ -51,21 +53,163 @@ def init(node_tree): base_tex_n = node_tree.nodes[Dif.BASE_TEX_NODE] # move existing - output_n.location.x += pos_x_shift + output_n.location.x += pos_x_shift * 3 # nodes creation lum_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") lum_mix_n.name = DifLum.LUM_MIX_NODE lum_mix_n.label = DifLum.LUM_MIX_NODE - lum_mix_n.location = (compose_lighting_n.location.x + pos_x_shift, compose_lighting_n.location.y - 100) + lum_mix_n.location = (compose_lighting_n.location.x + pos_x_shift * 2, compose_lighting_n.location.y - 100) lum_mix_n.blend_type = "MIX" + lum_a_inv_n = node_tree.nodes.new("ShaderNodeMath") + lum_a_inv_n.name = lum_a_inv_n.label = DifLum.LUM_A_INVERSE_NODE + lum_a_inv_n.location = (compose_lighting_n.location.x + pos_x_shift * 2, compose_lighting_n.location.y - 300) + lum_a_inv_n.operation = "SUBTRACT" + lum_a_inv_n.use_clamp = True + lum_a_inv_n.inputs[0].default_value = 0.999999 # TODO: change back to 1.0 after bug is fixed: https://developer.blender.org/T71426 + + lum_out_shader_n = node_tree.nodes.new("ShaderNodeEeveeSpecular") + lum_out_shader_n.name = lum_out_shader_n.label = DifLum.LUM_OUT_SHADER_NODE + lum_out_shader_n.location = (compose_lighting_n.location.x + pos_x_shift * 3, compose_lighting_n.location.y - 200) + lum_out_shader_n.inputs["Base Color"].default_value = (0.0,) * 4 + lum_out_shader_n.inputs["Specular"].default_value = (0.0,) * 4 + # links creation - node_tree.links.new(lum_mix_n.inputs['Fac'], base_tex_n.outputs['Value']) - node_tree.links.new(lum_mix_n.inputs['Color1'], compose_lighting_n.outputs['Composed Color']) + node_tree.links.new(lum_mix_n.inputs['Fac'], base_tex_n.outputs['Alpha']) + node_tree.links.new(lum_mix_n.inputs['Color1'], compose_lighting_n.outputs['Color']) node_tree.links.new(lum_mix_n.inputs['Color2'], base_tex_n.outputs['Color']) - node_tree.links.new(output_n.inputs['Color'], lum_mix_n.outputs['Color']) + node_tree.links.new(lum_a_inv_n.inputs[1], compose_lighting_n.outputs['Alpha']) + + node_tree.links.new(lum_out_shader_n.inputs['Emissive Color'], lum_mix_n.outputs['Color']) + node_tree.links.new(lum_out_shader_n.inputs['Transparency'], lum_a_inv_n.outputs['Value']) + + node_tree.links.new(output_n.inputs['Surface'], lum_out_shader_n.outputs['BSDF']) + + @staticmethod + def finalize(node_tree, material): + """Finalize node tree and material settings. Should be called as last. + + :param node_tree: node tree on which this shader should be finalized + :type node_tree: bpy.types.NodeTree + :param material: material used for this shader + :type material: bpy.types.Material + """ + + material.use_backface_culling = True + material.blend_method = "OPAQUE" + + # set proper blend method + if alpha_test.is_set(node_tree): + material.blend_method = "CLIP" + material.alpha_threshold = 0.05 + + # add alpha test pass if multiply blend enabled, where alphed pixels shouldn't be multiplied as they are discarded + if blend_mult.is_set(node_tree): + lum_out_shader_n = node_tree.nodes[DifLum.LUM_OUT_SHADER_NODE] + + # alpha test pass has to get fully opaque input, thus remove transparency linkage + compose_lighting_n = node_tree.nodes[Dif.COMPOSE_LIGHTING_NODE] + if compose_lighting_n.inputs['Alpha'].links: + node_tree.links.remove(compose_lighting_n.inputs['Alpha'].links[0]) + if lum_out_shader_n.inputs['Transparency'].links: + node_tree.links.remove(lum_out_shader_n.inputs['Transparency'].links[0]) + + shader_from = lum_out_shader_n.outputs['BSDF'] + alpha_from = node_tree.nodes[Dif.OPACITY_NODE].outputs[0] + shader_to = lum_out_shader_n.outputs['BSDF'].links[0].to_socket + + alpha_test.add_pass(node_tree, shader_from, alpha_from, shader_to) + + if blend_add.is_set(node_tree): + material.blend_method = "BLEND" + if blend_mult.is_set(node_tree): + material.blend_method = "BLEND" + if blend_over.is_set(node_tree): + material.blend_method = "BLEND" + + @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: + alpha_test.init(node_tree) + 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 blending should be switched on or off + :type switch_on: bool + """ + + if switch_on: + blend_over.init(node_tree) + 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 blending should be switched on or off + :type switch_on: bool + """ + + if switch_on: + out_node = node_tree.nodes[Dif.OUTPUT_NODE] + in_node = node_tree.nodes[DifLum.LUM_OUT_SHADER_NODE] + + # put it on location of output node & move output node for one slot to the right + location = tuple(out_node.location) + out_node.location.x += 185 + + blend_add.init(node_tree, location, in_node.outputs['BSDF'], out_node.inputs['Surface']) + else: + blend_add.delete(node_tree) + + @staticmethod + def set_blend_mult_flavor(node_tree, switch_on): + """Set blend mult flavor to this shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param switch_on: flag indication if blending should be switched on or off + :type switch_on: bool + """ + + if switch_on: + out_node = node_tree.nodes[Dif.OUTPUT_NODE] + in_node = node_tree.nodes[DifLum.LUM_OUT_SHADER_NODE] + + # break link to lum out shader transparency as mult uses DST_COLOR as source factor in blend function + compose_lighting_n = node_tree.nodes[Dif.COMPOSE_LIGHTING_NODE] + if compose_lighting_n.inputs['Alpha'].links: + node_tree.links.remove(compose_lighting_n.inputs['Alpha'].links[0]) + lum_out_shader_n = node_tree.nodes[DifLum.LUM_OUT_SHADER_NODE] + if lum_out_shader_n.inputs['Transparency'].links: + node_tree.links.remove(lum_out_shader_n.inputs['Transparency'].links[0]) + + # put it on location of output node & move output node for one slot to the right + location = tuple(out_node.location) + out_node.location.x += 185 + + blend_mult.init(node_tree, location, in_node.outputs['BSDF'], out_node.inputs['Surface']) + else: + blend_mult.delete(node_tree) @staticmethod def set_lvcol_flavor(node_tree, switch_on): @@ -82,6 +226,6 @@ def set_lvcol_flavor(node_tree, switch_on): lum_mix_n = node_tree.nodes[DifLum.LUM_MIX_NODE] if switch_on: - node_tree.links.new(lum_mix_n.inputs['Color2'], vcol_mult_n.outputs['Color']) + node_tree.links.new(lum_mix_n.inputs['Color2'], vcol_mult_n.outputs[0]) else: node_tree.links.new(lum_mix_n.inputs['Color2'], base_tex_n.outputs['Color']) diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_lum_spec/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/dif_lum_spec/__init__.py index 270f666..1a3d1e6 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_lum_spec/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_lum_spec/__init__.py @@ -16,14 +16,19 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.internals.shaders.eut2.dif_spec import DifSpec +from io_scs_tools.internals.shaders.flavors import alpha_test +from io_scs_tools.internals.shaders.flavors import blend_over +from io_scs_tools.internals.shaders.flavors import blend_add +from io_scs_tools.internals.shaders.flavors import blend_mult class DifLumSpec(DifSpec): LUM_MIX_NODE = "LuminosityMix" + LUM_A_INVERSE_NODE = "LumTransp=1-Alpha" + LUM_OUT_SHADER_NODE = "LumShader" LUM_BOOST_MIX_NODE = "LuminosityBoostMix" LUM_BOOST_VALUE_NODE = "LuminosityBoost" @@ -40,9 +45,6 @@ def init(node_tree): :type node_tree: bpy.types.NodeTree """ - start_pos_x = 0 - start_pos_y = 0 - pos_x_shift = 185 # init parent @@ -54,33 +56,92 @@ def init(node_tree): spec_col_n = node_tree.nodes[DifSpec.SPEC_COL_NODE] # move existing - output_n.location.x += pos_x_shift + output_n.location.x += pos_x_shift * 4 # nodes creation lum_boost_val_n = node_tree.nodes.new("ShaderNodeValue") lum_boost_val_n.name = lum_boost_val_n.label = DifLumSpec.LUM_BOOST_VALUE_NODE lum_boost_val_n.location = (spec_col_n.location.x, spec_col_n.location.y + 100) - lum_boost_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") + lum_boost_mix_n = node_tree.nodes.new("ShaderNodeVectorMath") lum_boost_mix_n.name = lum_boost_mix_n.label = DifLumSpec.LUM_BOOST_MIX_NODE lum_boost_mix_n.location = (compose_lighting_n.location.x, compose_lighting_n.location.y + 200) - lum_boost_mix_n.blend_type = "MULTIPLY" - lum_boost_mix_n.inputs["Fac"].default_value = 1.0 + lum_boost_mix_n.operation = "MULTIPLY" lum_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") lum_mix_n.name = lum_mix_n.label = DifLumSpec.LUM_MIX_NODE - lum_mix_n.location = (compose_lighting_n.location.x + pos_x_shift, compose_lighting_n.location.y + 100) + lum_mix_n.location = (compose_lighting_n.location.x + pos_x_shift * 2, compose_lighting_n.location.y + 100) lum_mix_n.blend_type = "MIX" + lum_a_inv_n = node_tree.nodes.new("ShaderNodeMath") + lum_a_inv_n.name = lum_a_inv_n.label = DifLumSpec.LUM_A_INVERSE_NODE + lum_a_inv_n.location = (compose_lighting_n.location.x + pos_x_shift * 2, compose_lighting_n.location.y - 300) + lum_a_inv_n.operation = "SUBTRACT" + lum_a_inv_n.use_clamp = True + lum_a_inv_n.inputs[0].default_value = 0.999999 # TODO: change back to 1.0 after bug is fixed: https://developer.blender.org/T71426 + + lum_out_shader_n = node_tree.nodes.new("ShaderNodeEeveeSpecular") + lum_out_shader_n.name = lum_out_shader_n.label = DifLumSpec.LUM_OUT_SHADER_NODE + lum_out_shader_n.location = (compose_lighting_n.location.x + pos_x_shift * 3, compose_lighting_n.location.y - 200) + lum_out_shader_n.inputs["Base Color"].default_value = (0.0,) * 4 + lum_out_shader_n.inputs["Specular"].default_value = (0.0,) * 4 + # links creation - node_tree.links.new(lum_boost_mix_n.inputs['Color1'], lum_boost_val_n.outputs['Value']) - node_tree.links.new(lum_boost_mix_n.inputs['Color2'], base_tex_n.outputs['Color']) + node_tree.links.new(lum_boost_mix_n.inputs[0], lum_boost_val_n.outputs['Value']) + node_tree.links.new(lum_boost_mix_n.inputs[1], base_tex_n.outputs['Color']) + + node_tree.links.new(lum_mix_n.inputs['Fac'], base_tex_n.outputs['Alpha']) + node_tree.links.new(lum_mix_n.inputs['Color1'], compose_lighting_n.outputs['Color']) + node_tree.links.new(lum_mix_n.inputs['Color2'], lum_boost_mix_n.outputs[0]) + + node_tree.links.new(lum_a_inv_n.inputs[1], compose_lighting_n.outputs['Alpha']) + + node_tree.links.new(lum_out_shader_n.inputs['Emissive Color'], lum_mix_n.outputs['Color']) + node_tree.links.new(lum_out_shader_n.inputs['Transparency'], lum_a_inv_n.outputs['Value']) + + node_tree.links.new(output_n.inputs['Surface'], lum_out_shader_n.outputs['BSDF']) + + @staticmethod + def finalize(node_tree, material): + """Finalize node tree and material settings. Should be called as last. + + :param node_tree: node tree on which this shader should be finalized + :type node_tree: bpy.types.NodeTree + :param material: material used for this shader + :type material: bpy.types.Material + """ + + material.use_backface_culling = True + material.blend_method = "OPAQUE" + + # set proper blend method + if alpha_test.is_set(node_tree): + material.blend_method = "CLIP" + material.alpha_threshold = 0.05 + + # add alpha test pass if multiply blend enabled, where alphed pixels shouldn't be multiplied as they are discarded + if blend_mult.is_set(node_tree): + lum_out_shader_n = node_tree.nodes[DifLumSpec.LUM_OUT_SHADER_NODE] - node_tree.links.new(lum_mix_n.inputs['Fac'], base_tex_n.outputs['Color']) - node_tree.links.new(lum_mix_n.inputs['Color1'], compose_lighting_n.outputs['Composed Color']) - node_tree.links.new(lum_mix_n.inputs['Color2'], lum_boost_mix_n.outputs['Color']) + # alpha test pass has to get fully opaque input, thus remove transparency linkage + compose_lighting_n = node_tree.nodes[DifSpec.COMPOSE_LIGHTING_NODE] + if compose_lighting_n.inputs['Alpha'].links: + node_tree.links.remove(compose_lighting_n.inputs['Alpha'].links[0]) + if lum_out_shader_n.inputs['Transparency'].links: + node_tree.links.remove(lum_out_shader_n.inputs['Transparency'].links[0]) - node_tree.links.new(output_n.inputs['Color'], lum_mix_n.outputs['Color']) + shader_from = lum_out_shader_n.outputs['BSDF'] + alpha_from = node_tree.nodes[DifSpec.OPACITY_NODE].outputs[0] + shader_to = lum_out_shader_n.outputs['BSDF'].links[0].to_socket + + alpha_test.add_pass(node_tree, shader_from, alpha_from, shader_to) + + if blend_add.is_set(node_tree): + material.blend_method = "BLEND" + if blend_mult.is_set(node_tree): + material.blend_method = "BLEND" + if blend_over.is_set(node_tree): + material.blend_method = "BLEND" @staticmethod def set_aux5(node_tree, aux_property): @@ -94,6 +155,88 @@ def set_aux5(node_tree, aux_property): node_tree.nodes[DifLumSpec.LUM_BOOST_VALUE_NODE].outputs[0].default_value = 1 + aux_property[0]['value'] + @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: + alpha_test.init(node_tree) + 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 blending should be switched on or off + :type switch_on: bool + """ + + if switch_on: + blend_over.init(node_tree) + 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 blending should be switched on or off + :type switch_on: bool + """ + + if switch_on: + out_node = node_tree.nodes[DifSpec.OUTPUT_NODE] + in_node = node_tree.nodes[DifLumSpec.LUM_OUT_SHADER_NODE] + + # put it on location of output node & move output node for one slot to the right + location = tuple(out_node.location) + out_node.location.x += 185 + + blend_add.init(node_tree, location, in_node.outputs['BSDF'], out_node.inputs['Surface']) + else: + blend_add.delete(node_tree) + + @staticmethod + def set_blend_mult_flavor(node_tree, switch_on): + """Set blend mult flavor to this shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param switch_on: flag indication if blending should be switched on or off + :type switch_on: bool + """ + + if switch_on: + out_node = node_tree.nodes[DifSpec.OUTPUT_NODE] + in_node = node_tree.nodes[DifLumSpec.LUM_OUT_SHADER_NODE] + + # break link to lum out shader transparency as mult uses DST_COLOR as source factor in blend function + compose_lighting_n = node_tree.nodes[DifSpec.COMPOSE_LIGHTING_NODE] + if compose_lighting_n.inputs['Alpha'].links: + node_tree.links.remove(compose_lighting_n.inputs['Alpha'].links[0]) + lum_out_shader_n = node_tree.nodes[DifLumSpec.LUM_OUT_SHADER_NODE] + if lum_out_shader_n.inputs['Transparency'].links: + node_tree.links.remove(lum_out_shader_n.inputs['Transparency'].links[0]) + + # put it on location of output node & move output node for one slot to the right + location = tuple(out_node.location) + out_node.location.x += 185 + + blend_mult.init(node_tree, location, in_node.outputs['BSDF'], out_node.inputs['Surface']) + else: + blend_mult.delete(node_tree) + @staticmethod def set_linv_flavor(node_tree, switch_on): """Set luminance inverse flavor to this shader. @@ -106,14 +249,14 @@ def set_linv_flavor(node_tree, switch_on): lum_mix_n = node_tree.nodes[DifLumSpec.LUM_MIX_NODE] lum_boost_mix_n = node_tree.nodes[DifLumSpec.LUM_BOOST_MIX_NODE] - out_mat_n = node_tree.nodes[DifSpec.OUT_MAT_NODE] + compose_lighting_n = node_tree.nodes[DifSpec.COMPOSE_LIGHTING_NODE] if switch_on: - node_tree.links.new(lum_mix_n.inputs['Color1'], lum_boost_mix_n.outputs['Color']) - node_tree.links.new(lum_mix_n.inputs['Color2'], out_mat_n.outputs['Color']) + node_tree.links.new(lum_mix_n.inputs['Color1'], lum_boost_mix_n.outputs[0]) + node_tree.links.new(lum_mix_n.inputs['Color2'], compose_lighting_n.outputs['Color']) else: - node_tree.links.new(lum_mix_n.inputs['Color1'], out_mat_n.outputs['Color']) - node_tree.links.new(lum_mix_n.inputs['Color2'], lum_boost_mix_n.outputs['Color']) + node_tree.links.new(lum_mix_n.inputs['Color1'], compose_lighting_n.outputs['Color']) + node_tree.links.new(lum_mix_n.inputs['Color2'], lum_boost_mix_n.outputs[0]) @staticmethod def set_lvcol_flavor(node_tree, switch_on): @@ -130,6 +273,6 @@ def set_lvcol_flavor(node_tree, switch_on): lum_boost_mix_n = node_tree.nodes[DifLumSpec.LUM_BOOST_MIX_NODE] if switch_on: - node_tree.links.new(lum_boost_mix_n.inputs['Color2'], vcol_mult_n.outputs['Color']) + node_tree.links.new(lum_boost_mix_n.inputs[1], vcol_mult_n.outputs[0]) else: - node_tree.links.new(lum_boost_mix_n.inputs['Color2'], base_tex_n.outputs['Color']) + node_tree.links.new(lum_boost_mix_n.inputs[1], base_tex_n.outputs['Color']) diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_spec/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/dif_spec/__init__.py index e7a6ed6..4962fe3 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_spec/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_spec/__init__.py @@ -16,11 +16,10 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.internals.shaders.eut2.dif import Dif -from io_scs_tools.internals.shaders.eut2.std_node_groups import alpha_remap +from io_scs_tools.internals.shaders.eut2.std_node_groups import alpha_remap_ng from io_scs_tools.internals.shaders.flavors import asafew @@ -57,35 +56,34 @@ def init(node_tree, disable_remap_alpha=False): base_tex_n = node_tree.nodes[Dif.BASE_TEX_NODE] spec_col_n = node_tree.nodes[Dif.SPEC_COL_NODE] - out_mat_n = node_tree.nodes[Dif.OUT_MAT_NODE] + compose_lighting_n = node_tree.nodes[Dif.COMPOSE_LIGHTING_NODE] # node creation - spec_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") + spec_mult_n = node_tree.nodes.new("ShaderNodeVectorMath") spec_mult_n.name = DifSpec.SPEC_MULT_NODE spec_mult_n.label = DifSpec.SPEC_MULT_NODE spec_mult_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1900) - spec_mult_n.blend_type = "MULTIPLY" - spec_mult_n.inputs['Fac'].default_value = 1 + spec_mult_n.operation = "MULTIPLY" - remap_alpha_gn = None + remap_alpha_n = None if not disable_remap_alpha: - remap_alpha_gn = node_tree.nodes.new("ShaderNodeGroup") - remap_alpha_gn.name = remap_alpha_gn.label = DifSpec.REMAP_ALPHA_GNODE - remap_alpha_gn.location = (start_pos_x + pos_x_shift * 2, start_pos_y + 1900) - remap_alpha_gn.node_tree = alpha_remap.get_node_group() - remap_alpha_gn.inputs['Factor1'].default_value = 1.0 - remap_alpha_gn.inputs['Factor2'].default_value = 0.0 + remap_alpha_n = node_tree.nodes.new("ShaderNodeGroup") + remap_alpha_n.name = remap_alpha_n.label = DifSpec.REMAP_ALPHA_GNODE + remap_alpha_n.location = (start_pos_x + pos_x_shift * 2, start_pos_y + 1900) + remap_alpha_n.node_tree = alpha_remap_ng.get_node_group() + remap_alpha_n.inputs['Factor1'].default_value = 1.0 + remap_alpha_n.inputs['Factor2'].default_value = 0.0 # links creation if not disable_remap_alpha: - node_tree.links.new(remap_alpha_gn.inputs['Alpha'], base_tex_n.outputs['Value']) - node_tree.links.new(spec_mult_n.inputs['Color2'], remap_alpha_gn.outputs['Weighted Alpha']) + node_tree.links.new(remap_alpha_n.inputs['Alpha'], base_tex_n.outputs['Alpha']) + node_tree.links.new(spec_mult_n.inputs[1], remap_alpha_n.outputs['Weighted Alpha']) else: - node_tree.links.new(spec_mult_n.inputs['Color2'], base_tex_n.outputs['Value']) + node_tree.links.new(spec_mult_n.inputs[1], base_tex_n.outputs['Alpha']) - node_tree.links.new(spec_mult_n.inputs['Color1'], spec_col_n.outputs['Color']) + node_tree.links.new(spec_mult_n.inputs[0], spec_col_n.outputs['Color']) - node_tree.links.new(out_mat_n.inputs['Spec'], spec_mult_n.outputs['Color']) + node_tree.links.new(compose_lighting_n.inputs['Specular Color'], spec_mult_n.outputs[0]) @staticmethod def set_asafew_flavor(node_tree, switch_on): @@ -100,9 +98,9 @@ def set_asafew_flavor(node_tree, switch_on): :type switch_on: bool """ - remap_alpha_gn = node_tree.nodes[DifSpec.REMAP_ALPHA_GNODE] + remap_alpha_n = node_tree.nodes[DifSpec.REMAP_ALPHA_GNODE] if switch_on: - asafew.init(node_tree, remap_alpha_gn) + asafew.init(node_tree, remap_alpha_n) else: - asafew.delete(node_tree, remap_alpha_gn) + asafew.delete(node_tree, remap_alpha_n) diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_add_env/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_add_env/__init__.py index 5531a46..a156fb1 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_add_env/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_add_env/__init__.py @@ -16,8 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.internals.shaders.eut2.dif_spec import DifSpec from io_scs_tools.internals.shaders.eut2.std_passes.add_env import StdAddEnv @@ -43,7 +42,7 @@ def init(node_tree): DifSpec.GEOM_NODE, node_tree.nodes[DifSpec.SPEC_COL_NODE].outputs['Color'], node_tree.nodes[DifSpec.REMAP_ALPHA_GNODE].outputs['Weighted Alpha'], - node_tree.nodes[DifSpec.OUT_MAT_NODE].outputs['Normal'], + node_tree.nodes[DifSpec.LIGHTING_EVAL_NODE].outputs['Normal'], node_tree.nodes[DifSpec.COMPOSE_LIGHTING_NODE].inputs['Env Color']) @staticmethod diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_add_env/nofresnel.py b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_add_env/nofresnel.py index d0b9065..5a91584 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_add_env/nofresnel.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_add_env/nofresnel.py @@ -18,7 +18,6 @@ # Copyright (C) 2015: SCS Software - from io_scs_tools.internals.shaders.eut2.dif_spec_add_env import DifSpecAddEnv diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_fade_dif_spec/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_fade_dif_spec/__init__.py index df372d5..11e2592 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_fade_dif_spec/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_fade_dif_spec/__init__.py @@ -16,12 +16,12 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.internals.shaders.eut2.dif_spec import DifSpec from io_scs_tools.internals.shaders.eut2.dif_spec_fade_dif_spec import detail_nmap from io_scs_tools.internals.shaders.eut2.dif_spec_fade_dif_spec import detail_setup_ng +from io_scs_tools.utils import material as _material_utils class DifSpecFadeDifSpec(DifSpec): @@ -53,9 +53,8 @@ def init(node_tree): # init parent DifSpec.init(node_tree, disable_remap_alpha=True) - geom_n = node_tree.nodes[DifSpec.GEOM_NODE] + first_uv_n = node_tree.nodes[DifSpec.UVMAP_NODE] base_tex_n = node_tree.nodes[DifSpec.BASE_TEX_NODE] - opacity_n = node_tree.nodes[DifSpec.OPACITY_NODE] spec_mult_n = node_tree.nodes[DifSpec.SPEC_MULT_NODE] vcol_mult_n = node_tree.nodes[DifSpec.VCOLOR_MULT_NODE] @@ -69,15 +68,15 @@ def init(node_tree): uv_scale_n.name = uv_scale_n.label = DifSpecFadeDifSpec.UV_SCALE_NODE uv_scale_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1200) - detail_uv_scaling_n = node_tree.nodes.new("ShaderNodeMixRGB") + detail_uv_scaling_n = node_tree.nodes.new("ShaderNodeVectorMath") detail_uv_scaling_n.name = detail_uv_scaling_n.label = DifSpecFadeDifSpec.DETAIL_UV_SCALING_NODE detail_uv_scaling_n.location = (start_pos_x, start_pos_y + 1200) - detail_uv_scaling_n.blend_type = "MULTIPLY" - detail_uv_scaling_n.inputs['Fac'].default_value = 1.0 + detail_uv_scaling_n.operation = "MULTIPLY" - detail_tex_n = node_tree.nodes.new("ShaderNodeTexture") + detail_tex_n = node_tree.nodes.new("ShaderNodeTexImage") detail_tex_n.name = detail_tex_n.label = DifSpecFadeDifSpec.DETAIL_TEX_NODE detail_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1200) + detail_tex_n.width = 140 detail_setup_group_n = node_tree.nodes.new("ShaderNodeGroup") detail_setup_group_n.name = detail_setup_group_n.label = DifSpecFadeDifSpec.DETAIL_SETUP_GNODE @@ -95,38 +94,49 @@ def init(node_tree): base_detail_mix_n.blend_type = "MIX" # links creation - node_tree.links.new(detail_uv_scaling_n.inputs['Color1'], geom_n.outputs['UV']) - node_tree.links.new(detail_uv_scaling_n.inputs['Color2'], uv_scale_n.outputs[0]) + node_tree.links.new(detail_uv_scaling_n.inputs[0], first_uv_n.outputs['UV']) + node_tree.links.new(detail_uv_scaling_n.inputs[1], uv_scale_n.outputs[0]) # geom pass - node_tree.links.new(detail_tex_n.inputs['Vector'], detail_uv_scaling_n.outputs['Color']) + node_tree.links.new(detail_tex_n.inputs['Vector'], detail_uv_scaling_n.outputs[0]) # pass 1 node_tree.links.new(base_detail_mix_a_n.inputs['Fac'], detail_setup_group_n.outputs['Blend Factor']) - node_tree.links.new(base_detail_mix_a_n.inputs['Color1'], base_tex_n.outputs['Value']) - node_tree.links.new(base_detail_mix_a_n.inputs['Color2'], detail_tex_n.outputs['Value']) + node_tree.links.new(base_detail_mix_a_n.inputs['Color1'], base_tex_n.outputs['Alpha']) + node_tree.links.new(base_detail_mix_a_n.inputs['Color2'], detail_tex_n.outputs['Alpha']) node_tree.links.new(base_detail_mix_n.inputs['Fac'], detail_setup_group_n.outputs['Blend Factor']) node_tree.links.new(base_detail_mix_n.inputs['Color1'], base_tex_n.outputs['Color']) node_tree.links.new(base_detail_mix_n.inputs['Color2'], detail_tex_n.outputs['Color']) # pass 2 - node_tree.links.new(spec_mult_n.inputs['Color2'], base_detail_mix_a_n.outputs['Color']) + node_tree.links.new(spec_mult_n.inputs[1], base_detail_mix_a_n.outputs['Color']) # pass 3 - node_tree.links.new(vcol_mult_n.inputs['Color2'], base_detail_mix_n.outputs['Color']) + node_tree.links.new(vcol_mult_n.inputs[1], base_detail_mix_n.outputs['Color']) @staticmethod - def set_detail_texture(node_tree, texture): + def set_detail_texture(node_tree, image): """Set detail texture to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assignet to detail texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assigned to detail texture node + :type image: bpy.types.Texture """ - node_tree.nodes[DifSpecFadeDifSpec.DETAIL_TEX_NODE].texture = texture + node_tree.nodes[DifSpecFadeDifSpec.DETAIL_TEX_NODE].image = image + + @staticmethod + def set_detail_texture_settings(node_tree, settings): + """Set detail texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + _material_utils.set_texture_settings_to_node(node_tree.nodes[DifSpecFadeDifSpec.DETAIL_TEX_NODE], settings) @staticmethod def set_detail_uv(node_tree, uv_layer): @@ -158,30 +168,43 @@ def set_nmap_flavor(node_tree, switch_on): if node.location.x <= 185 and (min_y is None or min_y > node.location.y): min_y = node.location.y - out_mat_n = node_tree.nodes[DifSpec.OUT_MAT_NODE] + out_mat_n = node_tree.nodes[DifSpec.LIGHTING_EVAL_NODE] uv_scale_n = node_tree.nodes[DifSpecFadeDifSpec.UV_SCALE_NODE] detail_setup_group_n = node_tree.nodes[DifSpecFadeDifSpec.DETAIL_SETUP_GNODE] + geom_n = node_tree.nodes[DifSpecFadeDifSpec.GEOM_NODE] location = (out_mat_n.location.x - 185, min_y - 400) detail_nmap.init(node_tree, location, uv_scale_n.outputs[0], detail_setup_group_n.outputs['Detail Strength'], - out_mat_n.inputs['Normal']) + out_mat_n.inputs['Normal Vector'], + geom_n.outputs['Normal']) else: detail_nmap.delete(node_tree) @staticmethod - def set_nmap_texture(node_tree, texture): + def set_nmap_texture(node_tree, image): """Set normal map texture to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assignet to nmap texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assignet to nmap texture node + :type image: bpy.types.Texture """ - detail_nmap.set_texture(node_tree, texture) + detail_nmap.set_texture(node_tree, image) + + @staticmethod + def set_nmap_texture_settings(node_tree, settings): + """Set normal map texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + detail_nmap.set_texture_settings(node_tree, settings) @staticmethod def set_nmap_uv(node_tree, uv_layer): @@ -207,6 +230,17 @@ def set_nmap_detail_texture(node_tree, texture): detail_nmap.set_detail_texture(node_tree, texture) + @staticmethod + def set_nmap_detail_texture_settings(node_tree, settings): + """Set detail normal map texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + detail_nmap.set_detail_texture_settings(node_tree, settings) + @staticmethod def set_nmap_detail_uv(node_tree, uv_layer): """Set UV layer to detail normal map texture in shader. diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_fade_dif_spec/detail_nmap.py b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_fade_dif_spec/detail_nmap.py index b94421c..663965e 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_fade_dif_spec/detail_nmap.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_fade_dif_spec/detail_nmap.py @@ -16,10 +16,11 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software import bpy from io_scs_tools.internals.shaders.flavors import nmap +from io_scs_tools.utils import material as _material_utils DET_NMAP_NODE = "DetailNormalMapMat" DET_NMAP_TEX_NODE = "DetailNMapTex" @@ -33,7 +34,7 @@ NMAP_NORMALIZE_NODE = "NormalizeNMapNormal" -def __create_nodes__(node_tree, location, uv_scale_from, det_nmap_strength_from, normal_to): +def __create_nodes__(node_tree, location, uv_scale_from, det_nmap_strength_from, normal_to, normal_from): """Create node for detail normal maps. :param node_tree: node tree on which normal map will be used @@ -41,23 +42,24 @@ def __create_nodes__(node_tree, location, uv_scale_from, det_nmap_strength_from, """ frame = node_tree.nodes[nmap.NMAP_FLAVOR_FRAME_NODE] - nmap_geom_n = node_tree.nodes[nmap.NMAP_GEOM_NODE] + nmap_uvmap_n = node_tree.nodes[nmap.NMAP_UVMAP_NODE] + nmap_scale_gn = node_tree.nodes[nmap.NMAP_SCALE_GNODE] # move existing - nmap_geom_n.location.y -= 300 + nmap_uvmap_n.location.y -= 300 # nodes creation - det_nmap_uv_scale_n = node_tree.nodes.new("ShaderNodeMixRGB") + det_nmap_uv_scale_n = node_tree.nodes.new("ShaderNodeVectorMath") det_nmap_uv_scale_n.parent = frame det_nmap_uv_scale_n.name = det_nmap_uv_scale_n.label = DET_NMAP_UV_SCALE_NODE det_nmap_uv_scale_n.location = (location[0] - 185 * 2, location[1] - 700) - det_nmap_uv_scale_n.blend_type = "MULTIPLY" - det_nmap_uv_scale_n.inputs['Fac'].default_value = 1.0 + det_nmap_uv_scale_n.operation = "MULTIPLY" - det_nmap_tex_n = node_tree.nodes.new("ShaderNodeTexture") + det_nmap_tex_n = node_tree.nodes.new("ShaderNodeTexImage") det_nmap_tex_n.parent = frame det_nmap_tex_n.name = det_nmap_tex_n.label = DET_NMAP_TEX_NODE det_nmap_tex_n.location = (location[0] - 185, location[1] - 600) + det_nmap_tex_n.width = 140 det_nmap_n = node_tree.nodes.new("ShaderNodeNormalMap") det_nmap_n.parent = frame @@ -66,37 +68,35 @@ def __create_nodes__(node_tree, location, uv_scale_from, det_nmap_strength_from, det_nmap_n.space = "TANGENT" det_nmap_n.inputs["Strength"].default_value = 1 - det_nmap_scale_gn = node_tree.nodes.new("ShaderNodeGroup") - det_nmap_scale_gn.parent = frame - det_nmap_scale_gn.name = det_nmap_scale_gn.label = DET_NMAP_SCALE_GNODE - det_nmap_scale_gn.location = (location[0] + 185, location[1] - 600) - det_nmap_scale_gn.node_tree = nmap.scale_ng.get_node_group() + det_nmap_scale_n = node_tree.nodes.new("ShaderNodeGroup") + det_nmap_scale_n.parent = frame + det_nmap_scale_n.name = det_nmap_scale_n.label = DET_NMAP_SCALE_GNODE + det_nmap_scale_n.location = (location[0] + 185, location[1] - 600) + det_nmap_scale_n.node_tree = nmap.scale_ng.get_node_group() - det_nmap_strength_n = node_tree.nodes.new("ShaderNodeMixRGB") + det_nmap_strength_n = node_tree.nodes.new("ShaderNodeVectorMath") det_nmap_strength_n.parent = frame det_nmap_strength_n.name = det_nmap_strength_n.label = DET_NMAP_STRENGTH_NODE det_nmap_strength_n.location = (location[0] + 185 * 2, location[1] - 450) - det_nmap_strength_n.blend_type = "MULTIPLY" - det_nmap_strength_n.inputs['Fac'].default_value = 1.0 + det_nmap_strength_n.operation = "MULTIPLY" - det_nmap_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") + det_nmap_mix_n = node_tree.nodes.new("ShaderNodeVectorMath") det_nmap_mix_n.parent = frame det_nmap_mix_n.name = det_nmap_mix_n.label = DET_NMAP_STRENGTH_NODE det_nmap_mix_n.location = (location[0] + 185 * 3, location[1] - 300) - det_nmap_mix_n.blend_type = "ADD" - det_nmap_mix_n.inputs['Fac'].default_value = 1.0 + det_nmap_mix_n.operation = "ADD" - det_nmap_sep_n = node_tree.nodes.new("ShaderNodeSeparateRGB") + det_nmap_sep_n = node_tree.nodes.new("ShaderNodeSeparateXYZ") det_nmap_sep_n.parent = frame det_nmap_sep_n.name = det_nmap_sep_n.label = DET_NMAP_SEPARATE_NODE det_nmap_sep_n.location = (location[0] + 185 * 4, location[1] - 300) - nmap_sep_n = node_tree.nodes.new("ShaderNodeSeparateRGB") + nmap_sep_n = node_tree.nodes.new("ShaderNodeSeparateXYZ") nmap_sep_n.parent = frame nmap_sep_n.name = nmap_sep_n.label = NMAP_SEPARATE_NODE nmap_sep_n.location = (location[0] + 185 * 4, location[1] - 450) - nmap_det_nmap_combine_n = node_tree.nodes.new("ShaderNodeCombineRGB") + nmap_det_nmap_combine_n = node_tree.nodes.new("ShaderNodeCombineXYZ") nmap_det_nmap_combine_n.parent = frame nmap_det_nmap_combine_n.name = nmap_det_nmap_combine_n.label = NMAP_SEPARATE_NODE nmap_det_nmap_combine_n.location = (location[0] + 185 * 5, location[1] - 350) @@ -108,48 +108,46 @@ def __create_nodes__(node_tree, location, uv_scale_from, det_nmap_strength_from, nmap_normalize_n.operation = "NORMALIZE" # links creation - nodes = node_tree.nodes - # pass 1 - node_tree.links.new(det_nmap_uv_scale_n.inputs['Color1'], nodes[nmap.NMAP_GEOM_NODE].outputs['UV']) - node_tree.links.new(det_nmap_uv_scale_n.inputs['Color2'], uv_scale_from) + node_tree.links.new(det_nmap_uv_scale_n.inputs[0], nmap_uvmap_n.outputs['UV']) + node_tree.links.new(det_nmap_uv_scale_n.inputs[1], uv_scale_from) # pass 2 - node_tree.links.new(det_nmap_tex_n.inputs['Vector'], det_nmap_uv_scale_n.outputs['Color']) + node_tree.links.new(det_nmap_tex_n.inputs['Vector'], det_nmap_uv_scale_n.outputs[0]) # pass 3 node_tree.links.new(det_nmap_n.inputs['Color'], det_nmap_tex_n.outputs['Color']) # pass 4 - node_tree.links.new(det_nmap_scale_gn.inputs['NMap Tex Color'], det_nmap_tex_n.outputs['Color']) - node_tree.links.new(det_nmap_scale_gn.inputs['Original Normal'], nodes[nmap.NMAP_GEOM_NODE].outputs['Normal']) - node_tree.links.new(det_nmap_scale_gn.inputs['Modified Normal'], det_nmap_n.outputs['Normal']) + node_tree.links.new(det_nmap_scale_n.inputs['NMap Tex Color'], det_nmap_tex_n.outputs['Color']) + node_tree.links.new(det_nmap_scale_n.inputs['Original Normal'], normal_from) + node_tree.links.new(det_nmap_scale_n.inputs['Modified Normal'], det_nmap_n.outputs['Normal']) # pass 5 - node_tree.links.new(det_nmap_strength_n.inputs['Color1'], det_nmap_strength_from) - node_tree.links.new(det_nmap_strength_n.inputs['Color2'], det_nmap_scale_gn.outputs['Normal']) + node_tree.links.new(det_nmap_strength_n.inputs[0], det_nmap_strength_from) + node_tree.links.new(det_nmap_strength_n.inputs[1], det_nmap_scale_n.outputs['Normal']) # pass 6 - node_tree.links.new(det_nmap_mix_n.inputs['Color1'], nodes[nmap.NMAP_SCALE_GNODE].outputs['Normal']) - node_tree.links.new(det_nmap_mix_n.inputs['Color2'], det_nmap_strength_n.outputs['Color']) + node_tree.links.new(det_nmap_mix_n.inputs[0], nmap_scale_gn.outputs['Normal']) + node_tree.links.new(det_nmap_mix_n.inputs[1], det_nmap_strength_n.outputs[0]) # pass 7 - node_tree.links.new(det_nmap_sep_n.inputs['Image'], det_nmap_mix_n.outputs['Color']) + node_tree.links.new(det_nmap_sep_n.inputs['Vector'], det_nmap_mix_n.outputs[0]) - node_tree.links.new(nmap_sep_n.inputs['Image'], nodes[nmap.NMAP_SCALE_GNODE].outputs['Normal']) + node_tree.links.new(nmap_sep_n.inputs['Vector'], nmap_scale_gn.outputs['Normal']) # pass 8 - node_tree.links.new(nmap_det_nmap_combine_n.inputs['R'], det_nmap_sep_n.outputs['R']) - node_tree.links.new(nmap_det_nmap_combine_n.inputs['G'], det_nmap_sep_n.outputs['G']) - node_tree.links.new(nmap_det_nmap_combine_n.inputs['B'], nmap_sep_n.outputs['B']) + node_tree.links.new(nmap_det_nmap_combine_n.inputs['X'], det_nmap_sep_n.outputs['X']) + node_tree.links.new(nmap_det_nmap_combine_n.inputs['Y'], det_nmap_sep_n.outputs['Y']) + node_tree.links.new(nmap_det_nmap_combine_n.inputs['Z'], nmap_sep_n.outputs['Z']) # pass 9 - node_tree.links.new(nmap_normalize_n.inputs[0], nmap_det_nmap_combine_n.outputs['Image']) + node_tree.links.new(nmap_normalize_n.inputs[0], nmap_det_nmap_combine_n.outputs['Vector']) node_tree.links.new(normal_to, nmap_normalize_n.outputs['Vector']) -def init(node_tree, location, uv_scale_from, det_nmap_strength_from, normal_to): +def init(node_tree, location, uv_scale_from, det_nmap_strength_from, normal_to, normal_from): """Initialize normal map nodes. :param node_tree: node tree on which normal map will be used @@ -162,31 +160,44 @@ def init(node_tree, location, uv_scale_from, det_nmap_strength_from, normal_to): :type det_nmap_strength_from: bpy.types.NodeSocket :param normal_to: node socket to which result of normal map material should be send :type normal_to: bpy.types.NodeSocket + :param normal_from: node socket from which original mesh normal should be taken + :type normal_from: bpy.types.NodeSocket """ if nmap.NMAP_FLAVOR_FRAME_NODE not in node_tree.nodes: - nmap.init(node_tree, location, normal_to) - __create_nodes__(node_tree, location, uv_scale_from, det_nmap_strength_from, normal_to) + nmap.init(node_tree, location, normal_to, normal_from) + __create_nodes__(node_tree, location, uv_scale_from, det_nmap_strength_from, normal_to, normal_from) -def set_texture(node_tree, texture): +def set_texture(node_tree, image): """Set texture to normal map flavor. :param node_tree: node tree on which normal map is used :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assignet to nmap texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assignet to nmap texture node + :type image: bpy.types.Texture + """ + nmap.set_texture(node_tree, image) + + +def set_texture_settings(node_tree, settings): + """Set normal map texture settings to flavor. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str """ - nmap.set_texture(node_tree, texture) + nmap.set_texture_settings(node_tree, settings) -def set_detail_texture(node_tree, texture): +def set_detail_texture(node_tree, image): """Set texture to normal map flavor. :param node_tree: node tree on which normal map is used :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assignet to nmap texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assignet to nmap texture node + :type image: bpy.types.Texture """ # save currently active node to properly reset it on the end @@ -194,7 +205,7 @@ def set_detail_texture(node_tree, texture): old_active = node_tree.nodes.active # ignore empty texture - if texture is None: + if image is None: delete(node_tree, True) return @@ -203,11 +214,22 @@ def set_detail_texture(node_tree, texture): return # assign texture to texture node first - node_tree.nodes[DET_NMAP_TEX_NODE].texture = texture + node_tree.nodes[DET_NMAP_TEX_NODE].image = image node_tree.nodes.active = old_active +def set_detail_texture_settings(node_tree, settings): + """Set detail normal map texture settings to flavor. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + _material_utils.set_texture_settings_to_node(node_tree.nodes[DET_NMAP_TEX_NODE], settings) + + def set_uv(node_tree, uv_layer): """Set UV layer to texture in normal map flavor. diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_fade_dif_spec/detail_setup_ng.py b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_fade_dif_spec/detail_setup_ng.py index 5d9e0b7..4c199d3 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_fade_dif_spec/detail_setup_ng.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_fade_dif_spec/detail_setup_ng.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software import bpy from io_scs_tools.consts import Material as _MAT_consts diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_mult_dif_spec/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_mult_dif_spec/__init__.py index c6bd716..1791006 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_mult_dif_spec/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_mult_dif_spec/__init__.py @@ -16,19 +16,16 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.consts import Mesh as _MESH_consts from io_scs_tools.internals.shaders.eut2.dif_spec import DifSpec -from io_scs_tools.internals.shaders.flavors import alpha_test -from io_scs_tools.internals.shaders.flavors import blend_over -from io_scs_tools.internals.shaders.flavors import blend_add from io_scs_tools.internals.shaders.flavors import tg1 +from io_scs_tools.utils import material as _material_utils class DifSpecMultDifSpec(DifSpec): - SEC_GEOM_NODE = "SecondGeometry" + SEC_UVMAP_NODE = "SecondUVMap" MULT_TEX_NODE = "MultTex" MULT_BASE_COL_MIX_NODE = "MultBaseColorMix" MULT_BASE_A_MIX_NODE = "MultBaseAlphaMix" @@ -54,9 +51,11 @@ def init(node_tree): # init parent DifSpec.init(node_tree, disable_remap_alpha=True) + vcol_gn = node_tree.nodes[DifSpec.VCOL_GROUP_NODE] base_tex_n = node_tree.nodes[DifSpec.BASE_TEX_NODE] vcol_mult_n = node_tree.nodes[DifSpec.VCOLOR_MULT_NODE] spec_mult_n = node_tree.nodes[DifSpec.SPEC_MULT_NODE] + compose_lighting_n = node_tree.nodes[DifSpec.COMPOSE_LIGHTING_NODE] # delete existing node_tree.nodes.remove(node_tree.nodes[DifSpec.OPACITY_NODE]) @@ -67,23 +66,23 @@ def init(node_tree): node.location.x += pos_x_shift # node creation - sec_geom_n = node_tree.nodes.new("ShaderNodeGeometry") - sec_geom_n.name = DifSpecMultDifSpec.SEC_GEOM_NODE - sec_geom_n.label = DifSpecMultDifSpec.SEC_GEOM_NODE - sec_geom_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1200) - sec_geom_n.uv_layer = _MESH_consts.none_uv + sec_uvmap_n = node_tree.nodes.new("ShaderNodeUVMap") + sec_uvmap_n.name = DifSpecMultDifSpec.SEC_UVMAP_NODE + sec_uvmap_n.label = DifSpecMultDifSpec.SEC_UVMAP_NODE + sec_uvmap_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1200) + sec_uvmap_n.uv_map = _MESH_consts.none_uv - mult_tex_n = node_tree.nodes.new("ShaderNodeTexture") + mult_tex_n = node_tree.nodes.new("ShaderNodeTexImage") mult_tex_n.name = DifSpecMultDifSpec.MULT_TEX_NODE mult_tex_n.label = DifSpecMultDifSpec.MULT_TEX_NODE mult_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1200) + mult_tex_n.width = 140 - mult_base_col_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") + mult_base_col_mix_n = node_tree.nodes.new("ShaderNodeVectorMath") mult_base_col_mix_n.name = DifSpecMultDifSpec.MULT_BASE_COL_MIX_NODE mult_base_col_mix_n.label = DifSpecMultDifSpec.MULT_BASE_COL_MIX_NODE mult_base_col_mix_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1400) - mult_base_col_mix_n.blend_type = "MULTIPLY" - mult_base_col_mix_n.inputs['Fac'].default_value = 1 + mult_base_col_mix_n.operation = "MULTIPLY" mult_base_a_mix_n = node_tree.nodes.new("ShaderNodeMath") mult_base_a_mix_n.name = DifSpecMultDifSpec.MULT_BASE_A_MIX_NODE @@ -92,105 +91,55 @@ def init(node_tree): mult_base_a_mix_n.operation = "MULTIPLY" # links creation - node_tree.links.new(mult_tex_n.inputs['Vector'], sec_geom_n.outputs['UV']) - - node_tree.links.new(mult_base_col_mix_n.inputs['Color1'], base_tex_n.outputs['Color']) - node_tree.links.new(mult_base_col_mix_n.inputs['Color2'], mult_tex_n.outputs['Color']) - node_tree.links.new(mult_base_a_mix_n.inputs[0], base_tex_n.outputs['Value']) - node_tree.links.new(mult_base_a_mix_n.inputs[1], mult_tex_n.outputs['Value']) - - node_tree.links.new(vcol_mult_n.inputs['Color2'], mult_base_col_mix_n.outputs['Color']) - node_tree.links.new(spec_mult_n.inputs['Color2'], mult_base_a_mix_n.outputs['Value']) + node_tree.links.new(mult_tex_n.inputs['Vector'], sec_uvmap_n.outputs['UV']) - @staticmethod - def set_mult_texture(node_tree, texture): - """Set multiplication texture to shader. + node_tree.links.new(mult_base_col_mix_n.inputs[0], base_tex_n.outputs['Color']) + node_tree.links.new(mult_base_col_mix_n.inputs[1], mult_tex_n.outputs['Color']) + node_tree.links.new(mult_base_a_mix_n.inputs[0], base_tex_n.outputs['Alpha']) + node_tree.links.new(mult_base_a_mix_n.inputs[1], mult_tex_n.outputs['Alpha']) - :param node_tree: node tree of current shader - :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assigned to mult texture node - :type texture: bpy.types.Texture - """ + node_tree.links.new(vcol_mult_n.inputs[1], mult_base_col_mix_n.outputs[0]) + node_tree.links.new(spec_mult_n.inputs[1], mult_base_a_mix_n.outputs['Value']) - node_tree.nodes[DifSpecMultDifSpec.MULT_TEX_NODE].texture = texture + node_tree.links.new(compose_lighting_n.inputs['Alpha'], vcol_gn.outputs['Vertex Color Alpha']) @staticmethod - def set_mult_uv(node_tree, uv_layer): - """Set UV layer to multiplication texture in shader. - - :param node_tree: node tree of current shader - :type node_tree: bpy.types.NodeTree - :param uv_layer: uv layer string used for mult texture - :type uv_layer: str - """ - - if uv_layer is None or uv_layer == "": - uv_layer = _MESH_consts.none_uv - - node_tree.nodes[DifSpecMultDifSpec.SEC_GEOM_NODE].uv_layer = uv_layer - - @staticmethod - def set_alpha_test_flavor(node_tree, switch_on): - """Set alpha test flavor to this shader. + def set_mult_texture(node_tree, image): + """Set multiplication texture to 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 + :param image: texture image which should be assigned to mult texture node + :type image: bpy.types.Texture """ - if switch_on and not blend_over.is_set(node_tree): - out_node = node_tree.nodes[DifSpecMultDifSpec.OUT_MAT_NODE] - in_node = node_tree.nodes[DifSpecMultDifSpec.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) + node_tree.nodes[DifSpecMultDifSpec.MULT_TEX_NODE].image = image @staticmethod - def set_blend_over_flavor(node_tree, switch_on): - """Set blend over flavor to this shader. + def set_mult_texture_settings(node_tree, settings): + """Set multiplication texture settings to 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 + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str """ - - # remove alpha test flavor if it was set already. Because these two can not coexist - if alpha_test.is_set(node_tree): - DifSpecMultDifSpec.set_alpha_test_flavor(node_tree, False) - - out_node = node_tree.nodes[DifSpecMultDifSpec.OUT_MAT_NODE] - in_node = node_tree.nodes[DifSpecMultDifSpec.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) + _material_utils.set_texture_settings_to_node(node_tree.nodes[DifSpecMultDifSpec.MULT_TEX_NODE], settings) @staticmethod - def set_blend_add_flavor(node_tree, switch_on): - """Set blend add flavor to this shader. + def set_mult_uv(node_tree, uv_layer): + """Set UV layer to multiplication texture in 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 + :param uv_layer: uv layer string used for mult texture + :type uv_layer: str """ - # remove alpha test flavor if it was set already. Because these two can not coexist - if alpha_test.is_set(node_tree): - DifSpecMultDifSpec.set_alpha_test_flavor(node_tree, False) - - out_node = node_tree.nodes[DifSpecMultDifSpec.OUT_MAT_NODE] - in_node = node_tree.nodes[DifSpecMultDifSpec.VCOL_GROUP_NODE] + if uv_layer is None or uv_layer == "": + uv_layer = _MESH_consts.none_uv - if switch_on: - blend_add.init(node_tree, in_node.outputs['Vertex Color Alpha'], out_node.inputs['Alpha']) - else: - blend_add.delete(node_tree) + node_tree.nodes[DifSpecMultDifSpec.SEC_UVMAP_NODE].uv_map = uv_layer @staticmethod def set_tg1_flavor(node_tree, switch_on): @@ -204,13 +153,13 @@ def set_tg1_flavor(node_tree, switch_on): if switch_on and not tg1.is_set(node_tree): - out_node = node_tree.nodes[DifSpecMultDifSpec.SEC_GEOM_NODE] + out_node = node_tree.nodes[DifSpecMultDifSpec.GEOM_NODE] in_node = node_tree.nodes[DifSpecMultDifSpec.MULT_TEX_NODE] out_node.location.x -= 185 location = (out_node.location.x + 185, out_node.location.y) - tg1.init(node_tree, location, out_node.outputs["Global"], in_node.inputs["Vector"]) + tg1.init(node_tree, location, out_node.outputs["Position"], in_node.inputs["Vector"]) elif not switch_on: diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_mult_dif_spec/add_env.py b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_mult_dif_spec/add_env.py index 8798802..9ee1e42 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_mult_dif_spec/add_env.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_mult_dif_spec/add_env.py @@ -16,8 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.internals.shaders.eut2.dif_spec_mult_dif_spec import DifSpecMultDifSpec from io_scs_tools.internals.shaders.eut2.std_passes.add_env import StdAddEnv @@ -42,6 +41,12 @@ def init(node_tree): StdAddEnv.add(node_tree, DifSpecMultDifSpec.GEOM_NODE, node_tree.nodes[DifSpecMultDifSpec.SPEC_COL_NODE].outputs['Color'], - node_tree.nodes[DifSpecMultDifSpec.BASE_TEX_NODE].outputs['Value'], - node_tree.nodes[DifSpecMultDifSpec.OUT_MAT_NODE].outputs['Normal'], + node_tree.nodes[DifSpecMultDifSpec.BASE_TEX_NODE].outputs['Alpha'], + node_tree.nodes[DifSpecMultDifSpec.LIGHTING_EVAL_NODE].outputs['Normal'], node_tree.nodes[DifSpecMultDifSpec.COMPOSE_LIGHTING_NODE].inputs['Env Color']) + + mult_tex_n = node_tree.nodes[DifSpecMultDifSpec.MULT_TEX_NODE] + add_env_gn = node_tree.nodes[StdAddEnv.ADD_ENV_GROUP_NODE] + + # links creation + node_tree.links.new(add_env_gn.inputs['Strength Multiplier'], mult_tex_n.outputs['Alpha']) diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_mult_dif_spec_iamod_dif_spec/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_mult_dif_spec_iamod_dif_spec/__init__.py index 694b300..39aba7e 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_mult_dif_spec_iamod_dif_spec/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_mult_dif_spec_iamod_dif_spec/__init__.py @@ -16,15 +16,15 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.consts import Mesh as _MESH_consts from io_scs_tools.internals.shaders.eut2.dif_spec_mult_dif_spec import DifSpecMultDifSpec +from io_scs_tools.utils import material as _material_utils class DifSpecMultDifSpecIamodDifSpec(DifSpecMultDifSpec): - THIRD_GEOM_NODE = "ThirdGeometry" + THIRD_UVMAP_NODE = "ThirdUVMap" IAMOD_TEX_NODE = "IamodTex" IAMOD_SCALE_NODE = "IamodScaled" IAMOD_SCALE_A_NODE = "IamodAlphaScaled" @@ -69,16 +69,17 @@ def init(node_tree): mult_base_col_mix_n.location.y -= 200 # node creation - third_geom_n = node_tree.nodes.new("ShaderNodeGeometry") - third_geom_n.name = DifSpecMultDifSpecIamodDifSpec.THIRD_GEOM_NODE - third_geom_n.label = DifSpecMultDifSpecIamodDifSpec.THIRD_GEOM_NODE - third_geom_n.location = (start_pos_x - pos_x_shift, start_pos_y + 900) - third_geom_n.uv_layer = _MESH_consts.none_uv + third_uvmap_n = node_tree.nodes.new("ShaderNodeUVMap") + third_uvmap_n.name = DifSpecMultDifSpecIamodDifSpec.THIRD_UVMAP_NODE + third_uvmap_n.label = DifSpecMultDifSpecIamodDifSpec.THIRD_UVMAP_NODE + third_uvmap_n.location = (start_pos_x - pos_x_shift, start_pos_y + 900) + third_uvmap_n.uv_map = _MESH_consts.none_uv - iamod_tex_n = node_tree.nodes.new("ShaderNodeTexture") + iamod_tex_n = node_tree.nodes.new("ShaderNodeTexImage") iamod_tex_n.name = DifSpecMultDifSpecIamodDifSpec.IAMOD_TEX_NODE iamod_tex_n.label = DifSpecMultDifSpecIamodDifSpec.IAMOD_TEX_NODE iamod_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 900) + iamod_tex_n.width = 140 iamod_scale_col_n = node_tree.nodes.new("ShaderNodeMixRGB") iamod_scale_col_n.name = DifSpecMultDifSpecIamodDifSpec.IAMOD_SCALE_NODE @@ -94,12 +95,11 @@ def init(node_tree): iamod_scale_a_n.blend_type = "MIX" iamod_scale_a_n.inputs['Color2'].default_value = (1,) * 4 - iamod_multbase_col_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") + iamod_multbase_col_mix_n = node_tree.nodes.new("ShaderNodeVectorMath") iamod_multbase_col_mix_n.name = DifSpecMultDifSpecIamodDifSpec.IAMOD_MULTBASE_COL_MIX_NODE iamod_multbase_col_mix_n.label = DifSpecMultDifSpecIamodDifSpec.IAMOD_MULTBASE_COL_MIX_NODE iamod_multbase_col_mix_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 1100) - iamod_multbase_col_mix_n.blend_type = "MULTIPLY" - iamod_multbase_col_mix_n.inputs['Fac'].default_value = 1 + iamod_multbase_col_mix_n.operation = "MULTIPLY" iamod_multbase_a_mix_n = node_tree.nodes.new("ShaderNodeMath") iamod_multbase_a_mix_n.name = DifSpecMultDifSpecIamodDifSpec.IAMOD_MULTBASE_A_MIX_NODE @@ -108,34 +108,45 @@ def init(node_tree): iamod_multbase_a_mix_n.operation = "MULTIPLY" # links creation - node_tree.links.new(iamod_tex_n.inputs['Vector'], third_geom_n.outputs['UV']) + node_tree.links.new(iamod_tex_n.inputs['Vector'], third_uvmap_n.outputs['UV']) node_tree.links.new(iamod_scale_col_n.inputs['Fac'], vcol_group_n.outputs['Vertex Color Alpha']) node_tree.links.new(iamod_scale_col_n.inputs['Color1'], iamod_tex_n.outputs['Color']) node_tree.links.new(iamod_scale_a_n.inputs['Fac'], vcol_group_n.outputs['Vertex Color Alpha']) - node_tree.links.new(iamod_scale_a_n.inputs['Color1'], iamod_tex_n.outputs['Value']) + node_tree.links.new(iamod_scale_a_n.inputs['Color1'], iamod_tex_n.outputs['Alpha']) - node_tree.links.new(iamod_multbase_col_mix_n.inputs['Color1'], mult_base_col_mix_n.outputs['Color']) - node_tree.links.new(iamod_multbase_col_mix_n.inputs['Color2'], iamod_scale_col_n.outputs['Color']) + node_tree.links.new(iamod_multbase_col_mix_n.inputs[0], mult_base_col_mix_n.outputs[0]) + node_tree.links.new(iamod_multbase_col_mix_n.inputs[1], iamod_scale_col_n.outputs['Color']) node_tree.links.new(iamod_multbase_a_mix_n.inputs[0], mult_base_a_mix_n.outputs['Value']) node_tree.links.new(iamod_multbase_a_mix_n.inputs[1], iamod_scale_a_n.outputs['Color']) - node_tree.links.new(vcol_mult_n.inputs['Color2'], iamod_multbase_col_mix_n.outputs['Color']) - node_tree.links.new(spec_mult_n.inputs['Color2'], iamod_multbase_a_mix_n.outputs['Value']) + node_tree.links.new(vcol_mult_n.inputs[1], iamod_multbase_col_mix_n.outputs[0]) + node_tree.links.new(spec_mult_n.inputs[1], iamod_multbase_a_mix_n.outputs['Value']) @staticmethod - def set_iamod_texture(node_tree, texture): + def set_iamod_texture(node_tree, image): """Set inverse alpha modulating texture to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assigned to iamod texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assigned to iamod texture node + :type image: bpy.types.Texture """ - node_tree.nodes[DifSpecMultDifSpecIamodDifSpec.IAMOD_TEX_NODE].texture = texture + node_tree.nodes[DifSpecMultDifSpecIamodDifSpec.IAMOD_TEX_NODE].image = image + + @staticmethod + def set_iamod_texture_settings(node_tree, settings): + """Set inverse alpha modulating texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + _material_utils.set_texture_settings_to_node(node_tree.nodes[DifSpecMultDifSpecIamodDifSpec.IAMOD_TEX_NODE], settings) @staticmethod def set_iamod_uv(node_tree, uv_layer): @@ -150,7 +161,7 @@ def set_iamod_uv(node_tree, uv_layer): if uv_layer is None or uv_layer == "": uv_layer = _MESH_consts.none_uv - node_tree.nodes[DifSpecMultDifSpecIamodDifSpec.THIRD_GEOM_NODE].uv_layer = uv_layer + node_tree.nodes[DifSpecMultDifSpecIamodDifSpec.THIRD_UVMAP_NODE].uv_map = uv_layer @staticmethod def set_alpha_test_flavor(node_tree, switch_on): @@ -187,3 +198,15 @@ def set_blend_add_flavor(node_tree, switch_on): """ pass # NOTE: no support for this flavor; overriding with empty function + + @staticmethod + def set_blend_mult_flavor(node_tree, switch_on): + """Set blend mult 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 mult should be switched on or off + :type switch_on: bool + """ + + pass # NOTE: no support for this flavor; overriding with empty function 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 6fc4a1e..be4917b 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 @@ -16,18 +16,15 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.consts import Mesh as _MESH_consts from io_scs_tools.internals.shaders.eut2.dif_spec import DifSpec -from io_scs_tools.internals.shaders.flavors import alpha_test -from io_scs_tools.internals.shaders.flavors import blend_add -from io_scs_tools.internals.shaders.flavors import blend_over +from io_scs_tools.utils import material as _material_utils class DifSpecOclu(DifSpec): - SEC_GEOM_NODE = "SecGeom" + SEC_UVMAP_NODE = "SecUVMap" OCLU_TEX_NODE = "OcclusionTexture" OCLU_SEPARATE_RGB_NODE = "OcclusionSeparateRGB" OCLU_A_MIX_NODE = "OcclusionAlphaMix" @@ -64,24 +61,24 @@ def init(node_tree): node.location.x += pos_x_shift * 2 # node creation - sec_geom_n = node_tree.nodes.new("ShaderNodeGeometry") - sec_geom_n.name = sec_geom_n.label = DifSpecOclu.SEC_GEOM_NODE - sec_geom_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1200) - sec_geom_n.uv_layer = _MESH_consts.none_uv + sec_uvmap_n = node_tree.nodes.new("ShaderNodeUVMap") + sec_uvmap_n.name = sec_uvmap_n.label = DifSpecOclu.SEC_UVMAP_NODE + sec_uvmap_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1100) + sec_uvmap_n.uv_map = _MESH_consts.none_uv - oclu_tex_n = node_tree.nodes.new("ShaderNodeTexture") + oclu_tex_n = node_tree.nodes.new("ShaderNodeTexImage") oclu_tex_n.name = oclu_tex_n.label = DifSpecOclu.OCLU_TEX_NODE oclu_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1200) + oclu_tex_n.width = 140 oclu_sep_rgb_n = node_tree.nodes.new("ShaderNodeSeparateRGB") oclu_sep_rgb_n.name = oclu_sep_rgb_n.label = DifSpecOclu.OCLU_SEPARATE_RGB_NODE oclu_sep_rgb_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1200) - oclu_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") + oclu_mix_n = node_tree.nodes.new("ShaderNodeVectorMath") oclu_mix_n.name = oclu_mix_n.label = DifSpecOclu.OCLU_MIX_NODE oclu_mix_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 1400) - oclu_mix_n.blend_type = "MULTIPLY" - oclu_mix_n.inputs['Fac'].default_value = 1 + oclu_mix_n.operation = "MULTIPLY" oclu_a_mix_n = node_tree.nodes.new("ShaderNodeMath") oclu_a_mix_n.name = oclu_a_mix_n.label = DifSpecOclu.OCLU_A_MIX_NODE @@ -89,35 +86,46 @@ def init(node_tree): oclu_a_mix_n.operation = "MULTIPLY" # links creation - node_tree.links.new(oclu_tex_n.inputs["Vector"], sec_geom_n.outputs["UV"]) + node_tree.links.new(oclu_tex_n.inputs["Vector"], sec_uvmap_n.outputs["UV"]) # pass 1 node_tree.links.new(oclu_sep_rgb_n.inputs["Image"], oclu_tex_n.outputs["Color"]) # pass 2 - node_tree.links.new(oclu_a_mix_n.inputs[0], base_tex_n.outputs["Value"]) + node_tree.links.new(oclu_a_mix_n.inputs[0], base_tex_n.outputs["Alpha"]) node_tree.links.new(oclu_a_mix_n.inputs[1], oclu_sep_rgb_n.outputs["R"]) - node_tree.links.new(oclu_mix_n.inputs["Color1"], base_tex_n.outputs["Color"]) - node_tree.links.new(oclu_mix_n.inputs["Color2"], oclu_sep_rgb_n.outputs["R"]) + node_tree.links.new(oclu_mix_n.inputs[0], base_tex_n.outputs["Color"]) + node_tree.links.new(oclu_mix_n.inputs[1], oclu_sep_rgb_n.outputs["R"]) # pass 3 - node_tree.links.new(spec_mult_n.inputs["Color2"], oclu_a_mix_n.outputs["Value"]) + node_tree.links.new(spec_mult_n.inputs[1], oclu_a_mix_n.outputs["Value"]) # pass 4 - node_tree.links.new(vcol_mult_n.inputs["Color2"], oclu_mix_n.outputs["Color"]) + node_tree.links.new(vcol_mult_n.inputs[1], oclu_mix_n.outputs[0]) @staticmethod - def set_oclu_texture(node_tree, texture): + def set_oclu_texture(node_tree, image): """Set occlusion texture to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assigned to oclu texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assigned to oclu texture node + :type image: bpy.types.Texture """ - node_tree.nodes[DifSpecOclu.OCLU_TEX_NODE].texture = texture + node_tree.nodes[DifSpecOclu.OCLU_TEX_NODE].image = image + + @staticmethod + def set_oclu_texture_settings(node_tree, settings): + """Set occlusion texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + _material_utils.set_texture_settings_to_node(node_tree.nodes[DifSpecOclu.OCLU_TEX_NODE], settings) @staticmethod def set_oclu_uv(node_tree, uv_layer): @@ -132,4 +140,4 @@ def set_oclu_uv(node_tree, uv_layer): if uv_layer is None or uv_layer == "": uv_layer = _MESH_consts.none_uv - node_tree.nodes[DifSpecOclu.SEC_GEOM_NODE].uv_layer = uv_layer + node_tree.nodes[DifSpecOclu.SEC_UVMAP_NODE].uv_map = uv_layer diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_oclu_add_env/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_oclu_add_env/__init__.py index 6d396ef..73c2081 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_oclu_add_env/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_oclu_add_env/__init__.py @@ -16,8 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.internals.shaders.eut2.dif_spec_oclu import DifSpecOclu from io_scs_tools.internals.shaders.eut2.std_passes.add_env import StdAddEnv @@ -43,6 +42,12 @@ def init(node_tree): StdAddEnv.add(node_tree, DifSpecOclu.GEOM_NODE, node_tree.nodes[DifSpecOclu.SPEC_COL_NODE].outputs['Color'], - node_tree.nodes[DifSpecOclu.BASE_TEX_NODE].outputs['Value'], - node_tree.nodes[DifSpecOclu.OUT_MAT_NODE].outputs['Normal'], + node_tree.nodes[DifSpecOclu.BASE_TEX_NODE].outputs['Alpha'], + node_tree.nodes[DifSpecOclu.LIGHTING_EVAL_NODE].outputs['Normal'], node_tree.nodes[DifSpecOclu.COMPOSE_LIGHTING_NODE].inputs['Env Color']) + + oclu_sep_n = node_tree.nodes[DifSpecOclu.OCLU_SEPARATE_RGB_NODE] + add_env_gn = node_tree.nodes[StdAddEnv.ADD_ENV_GROUP_NODE] + + # links creation + node_tree.links.new(add_env_gn.inputs['Strength Multiplier'], oclu_sep_n.outputs['R']) diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_oclu_weight_add_env/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_oclu_weight_add_env/__init__.py index e87e1c1..e89d475 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_oclu_weight_add_env/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_oclu_weight_add_env/__init__.py @@ -16,8 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2017: SCS Software - +# Copyright (C) 2017-2019: SCS Software from io_scs_tools.internals.shaders.eut2.dif_spec_oclu import DifSpecOclu from io_scs_tools.internals.shaders.eut2.std_passes.add_env import StdAddEnv @@ -44,25 +43,32 @@ def init(node_tree): spec_mult_n = node_tree.nodes[DifSpecOclu.SPEC_MULT_NODE] vcol_scale_n = node_tree.nodes[DifSpecOclu.VCOLOR_SCALE_NODE] - out_mat_n = node_tree.nodes[DifSpecOclu.OUT_MAT_NODE] + compose_lighting_n = node_tree.nodes[DifSpecOclu.COMPOSE_LIGHTING_NODE] # init nodes - vcol_spec_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") + vcol_spec_mult_n = node_tree.nodes.new("ShaderNodeVectorMath") vcol_spec_mult_n.name = vcol_spec_mult_n.label = DifSpecOcluWeightAddEnv.VCOLOR_SPEC_MULT_NODE vcol_spec_mult_n.location = (spec_mult_n.location.x + 185, spec_mult_n.location.y) - vcol_spec_mult_n.blend_type = "MULTIPLY" - vcol_spec_mult_n.inputs['Fac'].default_value = 1 + vcol_spec_mult_n.operation = "MULTIPLY" # make links - node_tree.links.new(vcol_spec_mult_n.inputs['Color1'], spec_mult_n.outputs['Color']) - node_tree.links.new(vcol_spec_mult_n.inputs['Color2'], vcol_scale_n.outputs['Color']) + node_tree.links.new(vcol_spec_mult_n.inputs[0], spec_mult_n.outputs[0]) + node_tree.links.new(vcol_spec_mult_n.inputs[1], vcol_scale_n.outputs[0]) - node_tree.links.new(out_mat_n.inputs['Spec'], vcol_spec_mult_n.outputs['Color']) + node_tree.links.new(compose_lighting_n.inputs['Specular Color'], vcol_spec_mult_n.outputs[0]) # init env pass StdAddEnv.add(node_tree, DifSpecOclu.GEOM_NODE, node_tree.nodes[DifSpecOclu.SPEC_COL_NODE].outputs['Color'], - node_tree.nodes[DifSpecOclu.BASE_TEX_NODE].outputs['Value'], - node_tree.nodes[DifSpecOclu.OUT_MAT_NODE].outputs['Normal'], + node_tree.nodes[DifSpecOclu.BASE_TEX_NODE].outputs['Alpha'], + node_tree.nodes[DifSpecOclu.LIGHTING_EVAL_NODE].outputs['Normal'], node_tree.nodes[DifSpecOclu.COMPOSE_LIGHTING_NODE].inputs['Env Color']) + + oclu_sep_n = node_tree.nodes[DifSpecOclu.OCLU_SEPARATE_RGB_NODE] + vcol_scale_n = node_tree.nodes[DifSpecOclu.VCOLOR_SCALE_NODE] + add_env_gn = node_tree.nodes[StdAddEnv.ADD_ENV_GROUP_NODE] + + # links creation + node_tree.links.new(add_env_gn.inputs['Weighted Color'], vcol_scale_n.outputs[0]) + node_tree.links.new(add_env_gn.inputs['Strength Multiplier'], oclu_sep_n.outputs['R']) diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_over_dif_opac/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_over_dif_opac/__init__.py index 879559e..ef534de 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_over_dif_opac/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_over_dif_opac/__init__.py @@ -16,15 +16,16 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.consts import Mesh as _MESH_consts from io_scs_tools.internals.shaders.eut2.dif_spec import DifSpec from io_scs_tools.internals.shaders.flavors import tg1 +from io_scs_tools.utils import material as _material_utils class DifSpecOverDifOpac(DifSpec): - SEC_GEOM_NODE = "SecGeomety" + SEC_UVMAP_NODE = "SecUVMap" OVER_TEX_NODE = "OverTex" OVER_MIX_NODE = "OverMix" @@ -57,16 +58,17 @@ def init(node_tree): opacity_mult_n.location.y -= 200 # nodes creation - sec_geom_n = node_tree.nodes.new("ShaderNodeGeometry") - sec_geom_n.name = DifSpecOverDifOpac.SEC_GEOM_NODE - sec_geom_n.label = DifSpecOverDifOpac.SEC_GEOM_NODE - sec_geom_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1200) - sec_geom_n.uv_layer = _MESH_consts.none_uv + sec_uv_n = node_tree.nodes.new("ShaderNodeUVMap") + sec_uv_n.name = DifSpecOverDifOpac.SEC_UVMAP_NODE + sec_uv_n.label = DifSpecOverDifOpac.SEC_UVMAP_NODE + sec_uv_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1200) + sec_uv_n.uv_map = _MESH_consts.none_uv - over_tex_n = node_tree.nodes.new("ShaderNodeTexture") + over_tex_n = node_tree.nodes.new("ShaderNodeTexImage") over_tex_n.name = DifSpecOverDifOpac.OVER_TEX_NODE over_tex_n.label = DifSpecOverDifOpac.OVER_TEX_NODE over_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1200) + over_tex_n.width = 140 over_mix_node = node_tree.nodes.new("ShaderNodeMixRGB") over_mix_node.name = DifSpecOverDifOpac.OVER_MIX_NODE @@ -75,13 +77,13 @@ def init(node_tree): over_mix_node.blend_type = "MIX" # links creation - node_tree.links.new(over_tex_n.inputs['Vector'], sec_geom_n.outputs['UV']) + node_tree.links.new(over_tex_n.inputs['Vector'], sec_uv_n.outputs['UV']) - node_tree.links.new(over_mix_node.inputs['Fac'], over_tex_n.outputs['Value']) + node_tree.links.new(over_mix_node.inputs['Fac'], over_tex_n.outputs['Alpha']) node_tree.links.new(over_mix_node.inputs['Color1'], base_tex_n.outputs['Color']) node_tree.links.new(over_mix_node.inputs['Color2'], over_tex_n.outputs['Color']) - node_tree.links.new(vcol_mult_n.inputs['Color2'], over_mix_node.outputs['Color']) + node_tree.links.new(vcol_mult_n.inputs[1], over_mix_node.outputs['Color']) @staticmethod def set_aux1(node_tree, aux_property): @@ -110,16 +112,27 @@ def set_reflection2(node_tree, value): pass # NOTE: reflection2 attribute doesn't change anything in rendered material, so pass it @staticmethod - def set_over_texture(node_tree, texture): + def set_over_texture(node_tree, image): """Set overlying texture to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assigned to over texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assigned to over texture node + :type image: bpy.types.Texture """ - node_tree.nodes[DifSpecOverDifOpac.OVER_TEX_NODE].texture = texture + node_tree.nodes[DifSpecOverDifOpac.OVER_TEX_NODE].image = image + + @staticmethod + def set_over_texture_settings(node_tree, settings): + """Set overlying texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + _material_utils.set_texture_settings_to_node(node_tree.nodes[DifSpecOverDifOpac.OVER_TEX_NODE], settings) @staticmethod def set_over_uv(node_tree, uv_layer): @@ -134,7 +147,7 @@ def set_over_uv(node_tree, uv_layer): if uv_layer is None or uv_layer == "": uv_layer = _MESH_consts.none_uv - node_tree.nodes[DifSpecOverDifOpac.SEC_GEOM_NODE].uv_layer = uv_layer + node_tree.nodes[DifSpecOverDifOpac.SEC_UVMAP_NODE].uv_map = uv_layer @staticmethod def set_tg1_flavor(node_tree, switch_on): @@ -148,13 +161,13 @@ def set_tg1_flavor(node_tree, switch_on): if switch_on and not tg1.is_set(node_tree): - out_node = node_tree.nodes[DifSpecOverDifOpac.SEC_GEOM_NODE] + out_node = node_tree.nodes[DifSpecOverDifOpac.GEOM_NODE] in_node = node_tree.nodes[DifSpecOverDifOpac.OVER_TEX_NODE] out_node.location.x -= 185 location = (out_node.location.x + 185, out_node.location.y) - tg1.init(node_tree, location, out_node.outputs["Global"], in_node.inputs["Vector"]) + tg1.init(node_tree, location, out_node.outputs["Position"], in_node.inputs["Vector"]) elif not switch_on: diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_weight/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_weight/__init__.py index 949bbf3..5092b8b 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_weight/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_weight/__init__.py @@ -16,8 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.internals.shaders.eut2.dif_spec import DifSpec @@ -46,27 +45,26 @@ def init(node_tree): # init parent DifSpec.init(node_tree) - out_mat_n = node_tree.nodes[DifSpec.OUT_MAT_NODE] + lighting_eval_n = node_tree.nodes[DifSpec.LIGHTING_EVAL_NODE] compose_lighting_n = node_tree.nodes[DifSpec.COMPOSE_LIGHTING_NODE] output_n = node_tree.nodes[DifSpec.OUTPUT_NODE] - vcol_mult_n = node_tree.nodes[DifSpec.VCOLOR_MULT_NODE] + vcol_scale_n = node_tree.nodes[DifSpec.VCOLOR_SCALE_NODE] spec_mult_n = node_tree.nodes[DifSpec.SPEC_MULT_NODE] # move existing - out_mat_n.location.x += pos_x_shift + lighting_eval_n.location.x += pos_x_shift compose_lighting_n.location.x += pos_x_shift output_n.location.x += pos_x_shift # node creation - spec_diff_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") + spec_diff_mult_n = node_tree.nodes.new("ShaderNodeVectorMath") spec_diff_mult_n.name = DifSpecWeight.SPEC_DIFF_MULT_NODE spec_diff_mult_n.label = DifSpecWeight.SPEC_DIFF_MULT_NODE spec_diff_mult_n.location = (start_pos_x + pos_x_shift * 5, start_pos_y + 1900) - spec_diff_mult_n.blend_type = "MULTIPLY" - spec_diff_mult_n.inputs['Fac'].default_value = 1.0 + spec_diff_mult_n.operation = "MULTIPLY" # links creation - node_tree.links.new(spec_diff_mult_n.inputs['Color1'], spec_mult_n.outputs['Color']) - node_tree.links.new(spec_diff_mult_n.inputs['Color2'], vcol_mult_n.outputs['Color']) + node_tree.links.new(spec_diff_mult_n.inputs[0], spec_mult_n.outputs[0]) + node_tree.links.new(spec_diff_mult_n.inputs[1], vcol_scale_n.outputs[0]) - node_tree.links.new(out_mat_n.inputs['Spec'], spec_diff_mult_n.outputs['Color']) + node_tree.links.new(compose_lighting_n.inputs['Specular Color'], spec_diff_mult_n.outputs[0]) diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_weight_add_env/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_weight_add_env/__init__.py index 82ea6a0..e1b63dc 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_weight_add_env/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_weight_add_env/__init__.py @@ -16,8 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.internals.shaders.eut2.dif_spec_weight import DifSpecWeight from io_scs_tools.internals.shaders.eut2.std_passes.add_env import StdAddEnv @@ -43,17 +42,11 @@ def init(node_tree): DifSpecWeight.GEOM_NODE, node_tree.nodes[DifSpecWeight.SPEC_COL_NODE].outputs['Color'], node_tree.nodes[DifSpecWeight.REMAP_ALPHA_GNODE].outputs['Weighted Alpha'], - node_tree.nodes[DifSpecWeight.OUT_MAT_NODE].outputs['Normal'], + node_tree.nodes[DifSpecWeight.LIGHTING_EVAL_NODE].outputs['Normal'], node_tree.nodes[DifSpecWeight.COMPOSE_LIGHTING_NODE].inputs['Env Color']) - @staticmethod - def set_reflection_texture(node_tree, texture): - """Set reflection texture on shader. - - :param node_tree: node tree of current shader - :type node_tree: bpy.types.NodeTree - :param texture: texture object which should be used for reflection - :type texture: bpy.types.Texture - """ + vcol_scale_n = node_tree.nodes[DifSpecWeight.VCOLOR_SCALE_NODE] + add_env_gn = node_tree.nodes[StdAddEnv.ADD_ENV_GROUP_NODE] - node_tree.nodes[DifSpecWeightAddEnv.REFL_TEX_NODE].texture = texture + # links creation + node_tree.links.new(add_env_gn.inputs['Weighted Color'], vcol_scale_n.outputs[0]) diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_weight_mult2/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_weight_mult2/__init__.py index c59e8bf..3522eef 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_weight_mult2/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_weight_mult2/__init__.py @@ -16,12 +16,12 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.internals.shaders.eut2.dif_spec import DifSpec -from io_scs_tools.internals.shaders.eut2.std_node_groups import mult2_mix +from io_scs_tools.internals.shaders.eut2.std_node_groups import mult2_mix_ng from io_scs_tools.internals.shaders.flavors import tg0 +from io_scs_tools.utils import material as _material_utils class DifSpecWeightMult2(DifSpec): @@ -51,16 +51,16 @@ def init(node_tree): # init parent DifSpec.init(node_tree, disable_remap_alpha=False) - geom_n = node_tree.nodes[DifSpec.GEOM_NODE] + first_uv_map = node_tree.nodes[DifSpec.UVMAP_NODE] base_tex_n = node_tree.nodes[DifSpec.BASE_TEX_NODE] spec_mult_n = node_tree.nodes[DifSpec.SPEC_MULT_NODE] vcol_scale_n = node_tree.nodes[DifSpec.VCOLOR_SCALE_NODE] vcol_mult_n = node_tree.nodes[DifSpec.VCOLOR_MULT_NODE] opacity_mult_n = node_tree.nodes[DifSpec.OPACITY_NODE] - out_mat_n = node_tree.nodes[DifSpec.OUT_MAT_NODE] + compose_lighting_n = node_tree.nodes[DifSpec.COMPOSE_LIGHTING_NODE] # move existing - geom_n.location.x -= pos_x_shift * 2 + first_uv_map.location.x -= pos_x_shift * 2 opacity_mult_n.location.y -= 100 for node in node_tree.nodes: if node.location.x > start_pos_x + pos_x_shift: @@ -69,63 +69,74 @@ def init(node_tree): # nodes creation uv_scale_n = node_tree.nodes.new("ShaderNodeMapping") uv_scale_n.name = uv_scale_n.label = DifSpecWeightMult2.UV_SCALE_NODE - uv_scale_n.location = (start_pos_x - pos_x_shift * 2, start_pos_y + 1200) + uv_scale_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1100) uv_scale_n.vector_type = "POINT" - uv_scale_n.translation = uv_scale_n.rotation = (0.0,) * 3 - uv_scale_n.scale = (1.0,) * 3 - uv_scale_n.use_min = uv_scale_n.use_max = False + uv_scale_n.inputs['Location'].default_value = uv_scale_n.inputs['Rotation'].default_value = (0.0,) * 3 + uv_scale_n.inputs['Scale'].default_value = (1.0,) * 3 + uv_scale_n.width = 140 - mult_tex_n = node_tree.nodes.new("ShaderNodeTexture") + mult_tex_n = node_tree.nodes.new("ShaderNodeTexImage") mult_tex_n.name = mult_tex_n.label = DifSpecWeightMult2.MULT_TEX_NODE mult_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1200) + mult_tex_n.width = 140 - mult2_mix_gn = node_tree.nodes.new("ShaderNodeGroup") - mult2_mix_gn.name = mult2_mix_gn.label = DifSpecWeightMult2.MULT2_MIX_GROUP_NODE - mult2_mix_gn.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1400) - mult2_mix_gn.node_tree = mult2_mix.get_node_group() + mult2_mix_n = node_tree.nodes.new("ShaderNodeGroup") + mult2_mix_n.name = mult2_mix_n.label = DifSpecWeightMult2.MULT2_MIX_GROUP_NODE + mult2_mix_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1400) + mult2_mix_n.node_tree = mult2_mix_ng.get_node_group() - spec_vcol_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") + spec_vcol_mult_n = node_tree.nodes.new("ShaderNodeVectorMath") spec_vcol_mult_n.name = spec_vcol_mult_n.label = DifSpecWeightMult2.SPEC_VCOL_MULT_NODE spec_vcol_mult_n.location = (start_pos_x + pos_x_shift * 5, start_pos_y + 1800) - spec_vcol_mult_n.blend_type = "MULTIPLY" - spec_vcol_mult_n.inputs["Fac"].default_value = 1.0 + spec_vcol_mult_n.operation = "MULTIPLY" # links creation - node_tree.links.new(uv_scale_n.inputs["Vector"], geom_n.outputs["UV"]) + node_tree.links.new(uv_scale_n.inputs["Vector"], first_uv_map.outputs["UV"]) node_tree.links.new(mult_tex_n.inputs["Vector"], uv_scale_n.outputs["Vector"]) # pass 1 - node_tree.links.new(mult2_mix_gn.inputs["Base Alpha"], base_tex_n.outputs["Value"]) - node_tree.links.new(mult2_mix_gn.inputs["Base Color"], base_tex_n.outputs["Color"]) - node_tree.links.new(mult2_mix_gn.inputs["Mult Alpha"], mult_tex_n.outputs["Value"]) - node_tree.links.new(mult2_mix_gn.inputs["Mult Color"], mult_tex_n.outputs["Color"]) + node_tree.links.new(mult2_mix_n.inputs["Base Alpha"], base_tex_n.outputs["Alpha"]) + node_tree.links.new(mult2_mix_n.inputs["Base Color"], base_tex_n.outputs["Color"]) + node_tree.links.new(mult2_mix_n.inputs["Mult Alpha"], mult_tex_n.outputs["Alpha"]) + node_tree.links.new(mult2_mix_n.inputs["Mult Color"], mult_tex_n.outputs["Color"]) # pass 2 - node_tree.links.new(spec_mult_n.inputs["Color2"], mult2_mix_gn.outputs["Mix Alpha"]) + node_tree.links.new(spec_mult_n.inputs[1], mult2_mix_n.outputs["Mix Alpha"]) - node_tree.links.new(opacity_mult_n.inputs[0], mult2_mix_gn.outputs["Mix Alpha"]) + node_tree.links.new(opacity_mult_n.inputs[0], mult2_mix_n.outputs["Mix Alpha"]) # pass 3 - node_tree.links.new(spec_vcol_mult_n.inputs["Color1"], spec_mult_n.outputs["Color"]) - node_tree.links.new(spec_vcol_mult_n.inputs["Color2"], vcol_scale_n.outputs["Color"]) + node_tree.links.new(spec_vcol_mult_n.inputs[0], spec_mult_n.outputs[0]) + node_tree.links.new(spec_vcol_mult_n.inputs[1], vcol_scale_n.outputs[0]) - node_tree.links.new(vcol_mult_n.inputs["Color2"], mult2_mix_gn.outputs["Mix Color"]) + node_tree.links.new(vcol_mult_n.inputs[1], mult2_mix_n.outputs["Mix Color"]) # pass 4 - node_tree.links.new(out_mat_n.inputs["Spec"], spec_vcol_mult_n.outputs["Color"]) + node_tree.links.new(compose_lighting_n.inputs["Specular Color"], spec_vcol_mult_n.outputs[0]) @staticmethod - def set_mult_texture(node_tree, texture): + def set_mult_texture(node_tree, image): """Set multiplication texture to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assigned to mult texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assigned to mult texture node + :type image: bpy.types.Texture """ - node_tree.nodes[DifSpecWeightMult2.MULT_TEX_NODE].texture = texture + node_tree.nodes[DifSpecWeightMult2.MULT_TEX_NODE].image = image + + @staticmethod + def set_mult_texture_settings(node_tree, settings): + """Set multiplication texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + _material_utils.set_texture_settings_to_node(node_tree.nodes[DifSpecWeightMult2.MULT_TEX_NODE], settings) @staticmethod def set_mult_uv(node_tree, uv_layer): @@ -149,8 +160,8 @@ def set_aux5(node_tree, aux_property): :type aux_property: bpy.types.IDPropertyGroup """ - node_tree.nodes[DifSpecWeightMult2.UV_SCALE_NODE].scale[0] = aux_property[0]["value"] - node_tree.nodes[DifSpecWeightMult2.UV_SCALE_NODE].scale[1] = aux_property[1]["value"] + node_tree.nodes[DifSpecWeightMult2.UV_SCALE_NODE].inputs['Scale'].default_value[0] = aux_property[0]["value"] + node_tree.nodes[DifSpecWeightMult2.UV_SCALE_NODE].inputs['Scale'].default_value[1] = aux_property[1]["value"] @staticmethod def set_tg0_flavor(node_tree, switch_on): @@ -171,8 +182,8 @@ def set_tg0_flavor(node_tree, switch_on): out_node.location.x -= 185 * 2 location = (out_node.location.x + 185, out_node.location.y) - tg0.init(node_tree, location, out_node.outputs["Global"], in_node.inputs["Vector"]) - tg0.init(node_tree, location, out_node.outputs["Global"], in_node2.inputs["Vector"]) + tg0.init(node_tree, location, out_node.outputs["Position"], in_node.inputs["Vector"]) + tg0.init(node_tree, location, out_node.outputs["Position"], in_node2.inputs["Vector"]) elif not switch_on: diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_weight_mult2_weight2/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_weight_mult2_weight2/__init__.py index 0626c91..d308976 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_weight_mult2_weight2/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_weight_mult2_weight2/__init__.py @@ -16,18 +16,18 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.consts import Mesh as _MESH_consts from io_scs_tools.internals.shaders.eut2.dif_spec_weight_mult2 import DifSpecWeightMult2 -from io_scs_tools.internals.shaders.eut2.std_node_groups import mult2_mix +from io_scs_tools.internals.shaders.eut2.std_node_groups import mult2_mix_ng from io_scs_tools.internals.shaders.flavors import tg1 from io_scs_tools.utils import convert as _convert_utils +from io_scs_tools.utils import material as _material_utils class DifSpecWeightMult2Weight2(DifSpecWeightMult2): - THRD_GEOM_NODE = "ThrdGeometry" + SEC_UVMAP_NODE = "ThrdGeometry" SEC_UV_SCALE_NODE = "SecUVScale" SEC_SPEC_COLOR_NODE = "SecSpecularColor" SPEC_COLOR_MIX_NODE = "SpecularColorMix" @@ -62,8 +62,8 @@ def init(node_tree): # init parent DifSpecWeightMult2.init(node_tree) + first_uv_n = node_tree.nodes[DifSpecWeightMult2.UVMAP_NODE] base_tex_n = node_tree.nodes[DifSpecWeightMult2.BASE_TEX_NODE] - geom_n = node_tree.nodes[DifSpecWeightMult2.GEOM_NODE] spec_col_n = node_tree.nodes[DifSpecWeightMult2.SPEC_COL_NODE] vcol_group_n = node_tree.nodes[DifSpecWeightMult2.VCOL_GROUP_NODE] mult2_mix_gn = node_tree.nodes[DifSpecWeightMult2.MULT2_MIX_GROUP_NODE] @@ -80,40 +80,42 @@ def init(node_tree): node.location.x += pos_x_shift # nodes creation - thrd_geom_n = node_tree.nodes.new("ShaderNodeGeometry") - thrd_geom_n.name = thrd_geom_n.label = DifSpecWeightMult2Weight2.THRD_GEOM_NODE - thrd_geom_n.location = (start_pos_x - pos_x_shift * 3, start_pos_y + 600) - thrd_geom_n.uv_layer = _MESH_consts.none_uv + sec_uv_n = node_tree.nodes.new("ShaderNodeUVMap") + sec_uv_n.name = sec_uv_n.label = DifSpecWeightMult2Weight2.SEC_UVMAP_NODE + sec_uv_n.location = (start_pos_x - pos_x_shift * 3, start_pos_y + 600) + sec_uv_n.uv_map = _MESH_consts.none_uv sec_uv_scale_n = node_tree.nodes.new("ShaderNodeMapping") sec_uv_scale_n.name = sec_uv_scale_n.label = DifSpecWeightMult2Weight2.SEC_UV_SCALE_NODE - sec_uv_scale_n.location = (start_pos_x - pos_x_shift * 2, start_pos_y + 600) + sec_uv_scale_n.location = (start_pos_x - pos_x_shift, start_pos_y + 600) sec_uv_scale_n.vector_type = "POINT" - sec_uv_scale_n.translation = sec_uv_scale_n.rotation = (0.0,) * 3 - sec_uv_scale_n.scale = (1.0,) * 3 - sec_uv_scale_n.use_min = sec_uv_scale_n.use_max = False + sec_uv_scale_n.inputs['Location'].default_value = sec_uv_scale_n.inputs['Rotation'].default_value = (0.0,) * 3 + sec_uv_scale_n.inputs['Scale'].default_value = (1.0,) * 3 + sec_uv_scale_n.width = 140 sec_spec_col_n = node_tree.nodes.new("ShaderNodeRGB") sec_spec_col_n.name = sec_spec_col_n.label = DifSpecWeightMult2Weight2.SEC_SPEC_COLOR_NODE sec_spec_col_n.location = (start_pos_x + pos_x_shift, start_pos_y + 2100) - base_1_tex_n = node_tree.nodes.new("ShaderNodeTexture") + base_1_tex_n = node_tree.nodes.new("ShaderNodeTexImage") base_1_tex_n.name = base_1_tex_n.label = DifSpecWeightMult2Weight2.BASE_1_TEX_NODE base_1_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 900) + base_1_tex_n.width = 140 - mult_1_tex_n = node_tree.nodes.new("ShaderNodeTexture") + mult_1_tex_n = node_tree.nodes.new("ShaderNodeTexImage") mult_1_tex_n.name = mult_1_tex_n.label = DifSpecWeightMult2Weight2.MULT_1_TEX_NODE mult_1_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 600) + mult_1_tex_n.width = 140 spec_col_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") spec_col_mix_n.name = spec_col_mix_n.label = DifSpecWeightMult2Weight2.SPEC_COLOR_MIX_NODE - spec_col_mix_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 2000) + spec_col_mix_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 2100) spec_col_mix_n.blend_type = "MIX" - sec_mult2_mix_gn = node_tree.nodes.new("ShaderNodeGroup") - sec_mult2_mix_gn.name = sec_mult2_mix_gn.label = DifSpecWeightMult2Weight2.SEC_MULT2_MIX_GROUP_NODE - sec_mult2_mix_gn.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 800) - sec_mult2_mix_gn.node_tree = mult2_mix.get_node_group() + sec_mult2_mix_n = node_tree.nodes.new("ShaderNodeGroup") + sec_mult2_mix_n.name = sec_mult2_mix_n.label = DifSpecWeightMult2Weight2.SEC_MULT2_MIX_GROUP_NODE + sec_mult2_mix_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 800) + sec_mult2_mix_n.node_tree = mult2_mix_ng.get_node_group() combined_a_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") combined_a_mix_n.name = combined_a_mix_n.label = DifSpecWeightMult2Weight2.COMBINED_ALPHA_MIX_NODE @@ -126,36 +128,36 @@ def init(node_tree): combined_mix_n.blend_type = "MIX" # links creation - node_tree.links.new(sec_uv_scale_n.inputs["Vector"], thrd_geom_n.outputs["UV"]) - node_tree.links.new(base_tex_n.inputs["Vector"], geom_n.outputs["UV"]) + node_tree.links.new(sec_uv_scale_n.inputs["Vector"], sec_uv_n.outputs["UV"]) + node_tree.links.new(base_tex_n.inputs["Vector"], first_uv_n.outputs["UV"]) node_tree.links.new(mult_1_tex_n.inputs["Vector"], sec_uv_scale_n.outputs["Vector"]) - node_tree.links.new(base_1_tex_n.inputs["Vector"], thrd_geom_n.outputs["UV"]) + node_tree.links.new(base_1_tex_n.inputs["Vector"], sec_uv_n.outputs["UV"]) # pass 1 node_tree.links.new(spec_col_mix_n.inputs["Fac"], vcol_group_n.outputs["Vertex Color Alpha"]) node_tree.links.new(spec_col_mix_n.inputs["Color1"], spec_col_n.outputs["Color"]) node_tree.links.new(spec_col_mix_n.inputs["Color2"], sec_spec_col_n.outputs["Color"]) - node_tree.links.new(sec_mult2_mix_gn.inputs["Base Alpha"], base_1_tex_n.outputs["Value"]) - node_tree.links.new(sec_mult2_mix_gn.inputs["Base Color"], base_1_tex_n.outputs["Color"]) - node_tree.links.new(sec_mult2_mix_gn.inputs["Mult Alpha"], mult_1_tex_n.outputs["Value"]) - node_tree.links.new(sec_mult2_mix_gn.inputs["Mult Color"], mult_1_tex_n.outputs["Color"]) + node_tree.links.new(sec_mult2_mix_n.inputs["Base Alpha"], base_1_tex_n.outputs["Alpha"]) + node_tree.links.new(sec_mult2_mix_n.inputs["Base Color"], base_1_tex_n.outputs["Color"]) + node_tree.links.new(sec_mult2_mix_n.inputs["Mult Alpha"], mult_1_tex_n.outputs["Alpha"]) + node_tree.links.new(sec_mult2_mix_n.inputs["Mult Color"], mult_1_tex_n.outputs["Color"]) # pass 2 node_tree.links.new(combined_a_mix_n.inputs["Fac"], vcol_group_n.outputs["Vertex Color Alpha"]) node_tree.links.new(combined_a_mix_n.inputs["Color1"], mult2_mix_gn.outputs["Mix Alpha"]) - node_tree.links.new(combined_a_mix_n.inputs["Color2"], sec_mult2_mix_gn.outputs["Mix Alpha"]) + node_tree.links.new(combined_a_mix_n.inputs["Color2"], sec_mult2_mix_n.outputs["Mix Alpha"]) node_tree.links.new(combined_mix_n.inputs["Fac"], vcol_group_n.outputs["Vertex Color Alpha"]) node_tree.links.new(combined_mix_n.inputs["Color1"], mult2_mix_gn.outputs["Mix Color"]) - node_tree.links.new(combined_mix_n.inputs["Color2"], sec_mult2_mix_gn.outputs["Mix Color"]) + node_tree.links.new(combined_mix_n.inputs["Color2"], sec_mult2_mix_n.outputs["Mix Color"]) # pass 3 - node_tree.links.new(spec_mult_n.inputs["Color1"], spec_col_mix_n.outputs["Color"]) - node_tree.links.new(spec_mult_n.inputs["Color2"], combined_a_mix_n.outputs["Color"]) + node_tree.links.new(spec_mult_n.inputs[0], spec_col_mix_n.outputs["Color"]) + node_tree.links.new(spec_mult_n.inputs[1], combined_a_mix_n.outputs["Color"]) - node_tree.links.new(vcol_mult_n.inputs["Color2"], combined_mix_n.outputs["Color"]) + node_tree.links.new(vcol_mult_n.inputs[1], combined_mix_n.outputs["Color"]) @staticmethod def set_reflection2(node_tree, value): @@ -196,8 +198,8 @@ def set_aux5(node_tree, aux_property): DifSpecWeightMult2.set_aux5(node_tree, aux_property) - node_tree.nodes[DifSpecWeightMult2Weight2.SEC_UV_SCALE_NODE].scale[0] = aux_property[2]["value"] - node_tree.nodes[DifSpecWeightMult2Weight2.SEC_UV_SCALE_NODE].scale[1] = aux_property[3]["value"] + node_tree.nodes[DifSpecWeightMult2Weight2.SEC_UV_SCALE_NODE].inputs['Scale'].default_value[0] = aux_property[2]["value"] + node_tree.nodes[DifSpecWeightMult2Weight2.SEC_UV_SCALE_NODE].inputs['Scale'].default_value[1] = aux_property[3]["value"] @staticmethod def set_base_uv(node_tree, uv_layer): @@ -212,16 +214,27 @@ def set_base_uv(node_tree, uv_layer): DifSpecWeightMult2.set_mult_uv(node_tree, uv_layer) @staticmethod - def set_base_1_texture(node_tree, texture): + def set_base_1_texture(node_tree, image): """Set base_1 texture to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assigned to base_1 texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assigned to base_1 texture node + :type image: bpy.types.Texture """ - node_tree.nodes[DifSpecWeightMult2Weight2.BASE_1_TEX_NODE].texture = texture + node_tree.nodes[DifSpecWeightMult2Weight2.BASE_1_TEX_NODE].image = image + + @staticmethod + def set_base_1_texture_settings(node_tree, settings): + """Set base_1 texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + _material_utils.set_texture_settings_to_node(node_tree.nodes[DifSpecWeightMult2Weight2.BASE_1_TEX_NODE], settings) @staticmethod def set_base_1_uv(node_tree, uv_layer): @@ -236,19 +249,30 @@ def set_base_1_uv(node_tree, uv_layer): if uv_layer is None or uv_layer == "": uv_layer = _MESH_consts.none_uv - node_tree.nodes[DifSpecWeightMult2Weight2.THRD_GEOM_NODE].uv_layer = uv_layer + node_tree.nodes[DifSpecWeightMult2Weight2.SEC_UVMAP_NODE].uv_map = uv_layer @staticmethod - def set_mult_1_texture(node_tree, texture): + def set_mult_1_texture(node_tree, image): """Set mult_1 texture to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assigned to mult_1 texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assigned to mult_1 texture node + :type image: bpy.types.Texture """ - node_tree.nodes[DifSpecWeightMult2Weight2.MULT_1_TEX_NODE].texture = texture + node_tree.nodes[DifSpecWeightMult2Weight2.MULT_1_TEX_NODE].image = image + + @staticmethod + def set_mult_1_texture_settings(node_tree, settings): + """Set mult_1 texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + _material_utils.set_texture_settings_to_node(node_tree.nodes[DifSpecWeightMult2Weight2.MULT_1_TEX_NODE], settings) @staticmethod def set_mult_1_uv(node_tree, uv_layer): @@ -298,6 +322,18 @@ def set_blend_add_flavor(node_tree, switch_on): pass # NOTE: no support for this flavor; overriding with empty function + @staticmethod + def set_blend_mult_flavor(node_tree, switch_on): + """Set blend mult 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 mult should be switched on or off + :type switch_on: bool + """ + + pass # NOTE: no support for this flavor; overriding with empty function + @staticmethod def set_tg1_flavor(node_tree, switch_on): """Set zero texture generation flavor to this shader. @@ -310,15 +346,15 @@ def set_tg1_flavor(node_tree, switch_on): if switch_on and not tg1.is_set(node_tree): - out_node = node_tree.nodes[DifSpecWeightMult2Weight2.THRD_GEOM_NODE] + out_node = node_tree.nodes[DifSpecWeightMult2Weight2.GEOM_NODE] in_node = node_tree.nodes[DifSpecWeightMult2Weight2.SEC_UV_SCALE_NODE] in_node2 = node_tree.nodes[DifSpecWeightMult2Weight2.BASE_1_TEX_NODE] out_node.location.x -= 185 * 2 location = (out_node.location.x + 185, out_node.location.y) - tg1.init(node_tree, location, out_node.outputs["Global"], in_node.inputs["Vector"]) - tg1.init(node_tree, location, out_node.outputs["Global"], in_node2.inputs["Vector"]) + tg1.init(node_tree, location, out_node.outputs["Position"], in_node.inputs["Vector"]) + tg1.init(node_tree, location, out_node.outputs["Position"], in_node2.inputs["Vector"]) elif not switch_on: diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_weight_weight_dif_spec_weight/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_weight_weight_dif_spec_weight/__init__.py index 16a4908..43c8bfa 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_spec_weight_weight_dif_spec_weight/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_spec_weight_weight_dif_spec_weight/__init__.py @@ -16,24 +16,22 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.consts import Mesh as _MESH_consts from io_scs_tools.internals.shaders.eut2.dif_spec import DifSpec -from io_scs_tools.internals.shaders.flavors import alpha_test -from io_scs_tools.internals.shaders.flavors import blend_over -from io_scs_tools.internals.shaders.flavors import blend_add from io_scs_tools.internals.shaders.flavors import tg1 -from io_scs_tools.utils import convert as _convert_utils +from io_scs_tools.utils import material as _material_utils class DifSpecWeightWeightDifSpecWeight(DifSpec): - SEC_GEOM_NODE = "SecondGeometry" + SEC_UVMAP_NODE = "SecondUVMap" OVER_TEX_NODE = "OverTex" BASE_OVER_MIX_NODE = "BaseOverColorMix" BASE_OVER_A_MIX_NODE = "BaseOverAlphaMix" SEC_SPEC_COL_NODE = "SecSpecularColor" SEC_SPEC_MIX_NODE = "SecSpecMix" + SEC_SHININESS_MIX_NODE = "SecShininnesMix" VCOL_SPEC_MULT_NODE = "VColSpecMult" @staticmethod @@ -63,7 +61,7 @@ def init(node_tree): spec_multi_n = node_tree.nodes[DifSpec.SPEC_MULT_NODE] vcol_scale_n = node_tree.nodes[DifSpec.VCOLOR_SCALE_NODE] vcol_multi_n = node_tree.nodes[DifSpec.VCOLOR_MULT_NODE] - out_mat_n = node_tree.nodes[DifSpec.OUT_MAT_NODE] + lighting_eval_n = node_tree.nodes[DifSpec.LIGHTING_EVAL_NODE] compose_lighting_n = node_tree.nodes[DifSpec.COMPOSE_LIGHTING_NODE] output_n = node_tree.nodes[DifSpec.OUTPUT_NODE] @@ -73,194 +71,158 @@ def init(node_tree): # move existing spec_multi_n.location.x += pos_x_shift spec_multi_n.location.y += 100 - out_mat_n.location.x += pos_x_shift + lighting_eval_n.location.x += pos_x_shift compose_lighting_n.location.x += pos_x_shift output_n.location.x += pos_x_shift # node creation - sec_geom_n = node_tree.nodes.new("ShaderNodeGeometry") - sec_geom_n.name = DifSpecWeightWeightDifSpecWeight.SEC_GEOM_NODE - sec_geom_n.label = DifSpecWeightWeightDifSpecWeight.SEC_GEOM_NODE - sec_geom_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1200) - sec_geom_n.uv_layer = _MESH_consts.none_uv + sec_geom_n = node_tree.nodes.new("ShaderNodeUVMap") + sec_geom_n.name = sec_geom_n.label = DifSpecWeightWeightDifSpecWeight.SEC_UVMAP_NODE + sec_geom_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1100) + sec_geom_n.uv_map = _MESH_consts.none_uv sec_spec_col_n = node_tree.nodes.new("ShaderNodeRGB") - sec_spec_col_n.name = DifSpecWeightWeightDifSpecWeight.SEC_SPEC_COL_NODE - sec_spec_col_n.label = DifSpecWeightWeightDifSpecWeight.SEC_SPEC_COL_NODE + sec_spec_col_n.name = sec_spec_col_n.label = DifSpecWeightWeightDifSpecWeight.SEC_SPEC_COL_NODE sec_spec_col_n.location = (start_pos_x + pos_x_shift, start_pos_y + 2100) - over_tex_n = node_tree.nodes.new("ShaderNodeTexture") - over_tex_n.name = DifSpecWeightWeightDifSpecWeight.OVER_TEX_NODE - over_tex_n.label = DifSpecWeightWeightDifSpecWeight.OVER_TEX_NODE + over_tex_n = node_tree.nodes.new("ShaderNodeTexImage") + over_tex_n.name = over_tex_n.label = DifSpecWeightWeightDifSpecWeight.OVER_TEX_NODE over_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1200) + over_tex_n.width = 140 sec_spec_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") - sec_spec_mix_n.name = DifSpecWeightWeightDifSpecWeight.SEC_SPEC_MIX_NODE - sec_spec_mix_n.label = DifSpecWeightWeightDifSpecWeight.SEC_SPEC_MIX_NODE + sec_spec_mix_n.name = sec_spec_mix_n.label = DifSpecWeightWeightDifSpecWeight.SEC_SPEC_MIX_NODE sec_spec_mix_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 2100) sec_spec_mix_n.blend_type = "MIX" + sec_shininess_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") + sec_shininess_mix_n.name = sec_shininess_mix_n.label = DifSpecWeightWeightDifSpecWeight.SEC_SHININESS_MIX_NODE + sec_shininess_mix_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 2300) + sec_shininess_mix_n.blend_type = "MIX" + base_over_a_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") - base_over_a_mix_n.name = DifSpecWeightWeightDifSpecWeight.BASE_OVER_A_MIX_NODE - base_over_a_mix_n.label = DifSpecWeightWeightDifSpecWeight.BASE_OVER_A_MIX_NODE + base_over_a_mix_n.name = base_over_a_mix_n.label = DifSpecWeightWeightDifSpecWeight.BASE_OVER_A_MIX_NODE base_over_a_mix_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1900) base_over_a_mix_n.blend_type = "MIX" base_over_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") - base_over_mix_n.name = DifSpecWeightWeightDifSpecWeight.BASE_OVER_MIX_NODE - base_over_mix_n.label = DifSpecWeightWeightDifSpecWeight.BASE_OVER_MIX_NODE + base_over_mix_n.name = base_over_mix_n.label = DifSpecWeightWeightDifSpecWeight.BASE_OVER_MIX_NODE base_over_mix_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1350) - vcol_spec_mul_n = node_tree.nodes.new("ShaderNodeMixRGB") - vcol_spec_mul_n.name = DifSpecWeightWeightDifSpecWeight.VCOL_SPEC_MULT_NODE - vcol_spec_mul_n.label = DifSpecWeightWeightDifSpecWeight.VCOL_SPEC_MULT_NODE + vcol_spec_mul_n = node_tree.nodes.new("ShaderNodeVectorMath") + vcol_spec_mul_n.name = vcol_spec_mul_n.label = DifSpecWeightWeightDifSpecWeight.VCOL_SPEC_MULT_NODE vcol_spec_mul_n.location = (start_pos_x + pos_x_shift * 5, start_pos_y + 1900) - vcol_spec_mul_n.blend_type = "MULTIPLY" - vcol_spec_mul_n.inputs["Fac"].default_value = 1 + vcol_spec_mul_n.operation = "MULTIPLY" # links creation node_tree.links.new(over_tex_n.inputs["Vector"], sec_geom_n.outputs["UV"]) # pass 1 + node_tree.links.new(sec_shininess_mix_n.inputs["Fac"], vcol_group_n.outputs["Vertex Color Alpha"]) + node_tree.links.new(sec_spec_mix_n.inputs["Fac"], vcol_group_n.outputs["Vertex Color Alpha"]) node_tree.links.new(sec_spec_mix_n.inputs["Color1"], spec_col_n.outputs["Color"]) node_tree.links.new(sec_spec_mix_n.inputs["Color2"], sec_spec_col_n.outputs["Color"]) node_tree.links.new(base_over_a_mix_n.inputs["Fac"], vcol_group_n.outputs["Vertex Color Alpha"]) - node_tree.links.new(base_over_a_mix_n.inputs["Color1"], base_tex_n.outputs["Value"]) - node_tree.links.new(base_over_a_mix_n.inputs["Color2"], over_tex_n.outputs["Value"]) + node_tree.links.new(base_over_a_mix_n.inputs["Color1"], base_tex_n.outputs["Alpha"]) + node_tree.links.new(base_over_a_mix_n.inputs["Color2"], over_tex_n.outputs["Alpha"]) node_tree.links.new(base_over_mix_n.inputs["Fac"], vcol_group_n.outputs["Vertex Color Alpha"]) node_tree.links.new(base_over_mix_n.inputs["Color1"], base_tex_n.outputs["Color"]) node_tree.links.new(base_over_mix_n.inputs["Color2"], over_tex_n.outputs["Color"]) # pass 2 - node_tree.links.new(spec_multi_n.inputs["Color1"], sec_spec_mix_n.outputs["Color"]) - node_tree.links.new(spec_multi_n.inputs["Color2"], base_over_a_mix_n.outputs["Color"]) + node_tree.links.new(spec_multi_n.inputs[0], sec_spec_mix_n.outputs["Color"]) + node_tree.links.new(spec_multi_n.inputs[1], base_over_a_mix_n.outputs["Color"]) - node_tree.links.new(vcol_multi_n.inputs["Color2"], base_over_mix_n.outputs["Color"]) + node_tree.links.new(vcol_multi_n.inputs[1], base_over_mix_n.outputs["Color"]) # pass 3 - node_tree.links.new(vcol_spec_mul_n.inputs["Color1"], spec_multi_n.outputs["Color"]) - node_tree.links.new(vcol_spec_mul_n.inputs["Color2"], vcol_scale_n.outputs["Color"]) + node_tree.links.new(vcol_spec_mul_n.inputs[0], spec_multi_n.outputs[0]) + node_tree.links.new(vcol_spec_mul_n.inputs[1], vcol_scale_n.outputs[0]) # pass 4 - node_tree.links.new(out_mat_n.inputs["Spec"], vcol_spec_mul_n.outputs["Color"]) - - @staticmethod - def set_over_texture(node_tree, texture): - """Set overlying texture to shader. - - :param node_tree: node tree of current shader - :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assigned to over texture node - :type texture: bpy.types.Texture - """ + node_tree.links.new(lighting_eval_n.inputs["Shininess"], sec_shininess_mix_n.outputs["Color"]) - node_tree.nodes[DifSpecWeightWeightDifSpecWeight.OVER_TEX_NODE].texture = texture + # pass 5 + node_tree.links.new(compose_lighting_n.inputs["Specular Color"], vcol_spec_mul_n.outputs[0]) + node_tree.links.new(compose_lighting_n.inputs['Alpha'], base_tex_n.outputs['Alpha']) @staticmethod - def set_over_uv(node_tree, uv_layer): - """Set UV layer to overlying texture in shader. + def set_shininess(node_tree, factor): + """Set shininess factor to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param uv_layer: uv layer string used for over texture - :type uv_layer: str + :param factor: shininess factor + :type factor: float """ - node_tree.nodes[DifSpecWeightWeightDifSpecWeight.SEC_GEOM_NODE].uv_layer = uv_layer + node_tree.nodes[DifSpecWeightWeightDifSpecWeight.SEC_SHININESS_MIX_NODE].inputs["Color1"].default_value = (factor,) * 4 @staticmethod - def set_aux3(node_tree, aux_property): - """Set secondary specular color to shader. + def set_over_texture(node_tree, image): + """Set overlying texture to shader. - NOTE: fourth component represents secondary specular shininess, which can not be used - because specular shininess can not be linked with nodes. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param aux_property: secondary specular color represented with property group - :type aux_property: bpy.types.IDPropertyGroup + :param image: texture image which should be assigned to over texture node + :type image: bpy.types.Texture """ - color = _convert_utils.aux_to_node_color(aux_property) - - node_tree.nodes[DifSpecWeightWeightDifSpecWeight.SEC_SPEC_COL_NODE].outputs["Color"].default_value = color + node_tree.nodes[DifSpecWeightWeightDifSpecWeight.OVER_TEX_NODE].image = image @staticmethod - def set_reflection2(node_tree, value): - """Set second reflection factor to shader. + def set_over_texture_settings(node_tree, settings): + """Set overlying texture settings to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param value: reflection factor - :type value: float + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str """ - - pass # NOTE: reflection attribute doesn't change anything in rendered material, so pass it@staticmethod + _material_utils.set_texture_settings_to_node(node_tree.nodes[DifSpecWeightWeightDifSpecWeight.OVER_TEX_NODE], settings) @staticmethod - def set_alpha_test_flavor(node_tree, switch_on): - """Set alpha test flavor to this shader. + def set_over_uv(node_tree, uv_layer): + """Set UV layer to overlying texture in 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 + :param uv_layer: uv layer string used for over texture + :type uv_layer: str """ - 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) + node_tree.nodes[DifSpecWeightWeightDifSpecWeight.SEC_UVMAP_NODE].uv_map = uv_layer @staticmethod - def set_blend_over_flavor(node_tree, switch_on): - """Set blend over flavor to this shader. + def set_aux3(node_tree, aux_property): + """Set secondary specular color to 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 + :param aux_property: secondary specular color represented with property group + :type aux_property: bpy.types.IDPropertyGroup """ - # remove alpha test flavor if it was set already. Because these two can not coexist - if alpha_test.is_set(node_tree): - DifSpecWeightWeightDifSpecWeight.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] + color = (aux_property[0]["value"], aux_property[1]["value"], aux_property[2]["value"], 1.0) + node_tree.nodes[DifSpecWeightWeightDifSpecWeight.SEC_SPEC_COL_NODE].outputs["Color"].default_value = color - if switch_on: - blend_over.init(node_tree, in_node.outputs['Vertex Color Alpha'], out_node.inputs['Alpha']) - else: - blend_over.delete(node_tree) + factor = aux_property[3]["value"] + node_tree.nodes[DifSpecWeightWeightDifSpecWeight.SEC_SHININESS_MIX_NODE].inputs["Color2"].default_value = (factor,) * 4 @staticmethod - def set_blend_add_flavor(node_tree, switch_on): - """Set blend add flavor to this shader. + def set_reflection2(node_tree, value): + """Set second reflection factor to 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 + :param value: reflection factor + :type value: float """ - # remove alpha test flavor if it was set already. Because these two can not coexist - if alpha_test.is_set(node_tree): - DifSpecWeightWeightDifSpecWeight.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) + pass # NOTE: reflection attribute doesn't change anything in rendered material, so pass it@staticmethod @staticmethod def set_tg1_flavor(node_tree, switch_on): @@ -274,13 +236,13 @@ def set_tg1_flavor(node_tree, switch_on): if switch_on and not tg1.is_set(node_tree): - out_node = node_tree.nodes[DifSpecWeightWeightDifSpecWeight.SEC_GEOM_NODE] + out_node = node_tree.nodes[DifSpec.GEOM_NODE] in_node = node_tree.nodes[DifSpecWeightWeightDifSpecWeight.OVER_TEX_NODE] out_node.location.x -= 185 location = (out_node.location.x + 185, out_node.location.y) - tg1.init(node_tree, location, out_node.outputs["Global"], in_node.inputs["Vector"]) + tg1.init(node_tree, location, out_node.outputs["Position"], in_node.inputs["Vector"]) elif not switch_on: diff --git a/addon/io_scs_tools/internals/shaders/eut2/dif_weight_dif/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/dif_weight_dif/__init__.py index d9207a6..2f7688f 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/dif_weight_dif/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/dif_weight_dif/__init__.py @@ -16,19 +16,20 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.consts import Mesh as _MESH_consts from io_scs_tools.internals.shaders.eut2.dif import Dif from io_scs_tools.internals.shaders.flavors import alpha_test from io_scs_tools.internals.shaders.flavors import blend_over from io_scs_tools.internals.shaders.flavors import blend_add +from io_scs_tools.internals.shaders.flavors import blend_mult from io_scs_tools.internals.shaders.flavors import tg1 +from io_scs_tools.utils import material as _material_utils class DifWeightDif(Dif): - SEC_GEOM_NODE = "SecGeom" + SEC_UVMAP_NODE = "SecUVMap" OVER_TEX_NODE = "OverTex" SPEC_MULT_NODE = "SpecMultiplier" BASE_OVER_MIX_NODE = "BaseOverMix" @@ -57,27 +58,28 @@ def init(node_tree): vcol_group_n = node_tree.nodes[Dif.VCOL_GROUP_NODE] base_tex_n = node_tree.nodes[Dif.BASE_TEX_NODE] spec_col_n = node_tree.nodes[Dif.SPEC_COL_NODE] + vcol_scale_n = node_tree.nodes[Dif.VCOLOR_SCALE_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.OPACITY_NODE]) # node creation - sec_geom_n = node_tree.nodes.new("ShaderNodeGeometry") - sec_geom_n.name = sec_geom_n.label = DifWeightDif.SEC_GEOM_NODE - sec_geom_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1200) - sec_geom_n.uv_layer = _MESH_consts.none_uv + sec_uv_n = node_tree.nodes.new("ShaderNodeUVMap") + sec_uv_n.name = sec_uv_n.label = DifWeightDif.SEC_UVMAP_NODE + sec_uv_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1100) + sec_uv_n.uv_map = _MESH_consts.none_uv - over_tex_n = node_tree.nodes.new("ShaderNodeTexture") + over_tex_n = node_tree.nodes.new("ShaderNodeTexImage") over_tex_n.name = over_tex_n.label = DifWeightDif.OVER_TEX_NODE over_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1200) + over_tex_n.width = 140 - spec_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") + spec_mult_n = node_tree.nodes.new("ShaderNodeVectorMath") spec_mult_n.name = spec_mult_n.label = DifWeightDif.SPEC_MULT_NODE spec_mult_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 1900) - spec_mult_n.blend_type = "MULTIPLY" - spec_mult_n.inputs['Fac'].default_value = 1 + spec_mult_n.operation = "MULTIPLY" base_over_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") base_over_mix_n.name = base_over_mix_n.label = DifWeightDif.BASE_OVER_MIX_NODE @@ -85,7 +87,7 @@ def init(node_tree): base_over_mix_n.blend_type = "MIX" # links creation - node_tree.links.new(over_tex_n.inputs['Vector'], sec_geom_n.outputs['UV']) + node_tree.links.new(over_tex_n.inputs['Vector'], sec_uv_n.outputs['UV']) # pass 1 node_tree.links.new(base_over_mix_n.inputs['Fac'], vcol_group_n.outputs['Vertex Color Alpha']) @@ -93,115 +95,103 @@ def init(node_tree): node_tree.links.new(base_over_mix_n.inputs['Color2'], over_tex_n.outputs['Color']) # pass 2 - node_tree.links.new(spec_mult_n.inputs['Color1'], spec_col_n.outputs['Color']) - node_tree.links.new(spec_mult_n.inputs['Color2'], vcol_group_n.outputs['Vertex Color']) + node_tree.links.new(spec_mult_n.inputs[0], spec_col_n.outputs[0]) + node_tree.links.new(spec_mult_n.inputs[1], vcol_scale_n.outputs[0]) - node_tree.links.new(vcol_mult_n.inputs['Color2'], base_over_mix_n.outputs['Color']) + node_tree.links.new(vcol_mult_n.inputs[1], base_over_mix_n.outputs['Color']) # pass to material - node_tree.links.new(out_mat_n.inputs['Spec'], spec_mult_n.outputs['Color']) + node_tree.links.new(compose_lighting_n.inputs['Specular Color'], spec_mult_n.outputs[0]) + node_tree.links.new(compose_lighting_n.inputs['Alpha'], base_tex_n.outputs['Alpha']) @staticmethod - def set_reflection2(node_tree, value): - """Set reflection2 factor to shader. + def finalize(node_tree, material): + """Finalize node tree and material settings. Should be called as last. - :param node_tree: node tree of current shader + :param node_tree: node tree on which this shader should be finalized :type node_tree: bpy.types.NodeTree - :param value: reflection factor - :type value: float + :param material: material used for this shader + :type material: bpy.types.Material """ - pass # NOTE: reflection attribute doesn't change anything in rendered material, so pass it + material.use_backface_culling = True + material.blend_method = "OPAQUE" - @staticmethod - def set_over_texture(node_tree, texture): - """Set over texture to shader. + # set proper blend method + if alpha_test.is_set(node_tree): + material.blend_method = "CLIP" + material.alpha_threshold = 0.05 - :param node_tree: node tree of current shader - :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assignet to over texture node - :type texture: bpy.types.Texture - """ + # add alpha test pass if multiply blend enabled, where alphed pixels shouldn't be multiplied as they are discarded + if blend_mult.is_set(node_tree): + compose_lighting_n = node_tree.nodes[Dif.COMPOSE_LIGHTING_NODE] + + # alpha test pass has to get fully opaque input, thus remove transparency linkage + if compose_lighting_n.inputs['Alpha'].links: + node_tree.links.remove(compose_lighting_n.inputs['Alpha'].links[0]) + + shader_from = compose_lighting_n.outputs['Shader'] + alpha_from = node_tree.nodes[Dif.BASE_TEX_NODE].outputs[0] + shader_to = compose_lighting_n.outputs['Shader'].links[0].to_socket - node_tree.nodes[DifWeightDif.OVER_TEX_NODE].texture = texture + alpha_test.add_pass(node_tree, shader_from, alpha_from, shader_to) + + if blend_add.is_set(node_tree): + material.blend_method = "BLEND" + if blend_mult.is_set(node_tree): + material.blend_method = "BLEND" + if blend_over.is_set(node_tree): + material.blend_method = "BLEND" @staticmethod - def set_over_uv(node_tree, uv_layer): - """Set UV layer to over texture in shader. + def set_reflection2(node_tree, value): + """Set reflection2 factor to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param uv_layer: uv layer string used for over texture - :type uv_layer: str + :param value: reflection factor + :type value: float """ - if uv_layer is None or uv_layer == "": - uv_layer = _MESH_consts.none_uv - - node_tree.nodes[DifWeightDif.SEC_GEOM_NODE].uv_layer = uv_layer + pass # NOTE: reflection attribute doesn't change anything in rendered material, so pass it @staticmethod - def set_alpha_test_flavor(node_tree, switch_on): - """Set alpha test flavor to this shader. + def set_over_texture(node_tree, image): + """Set over texture to 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 + :param image: texture image which should be assignet to over texture node + :type image: bpy.types.Texture """ - if switch_on and not blend_over.is_set(node_tree): - out_node = node_tree.nodes[Dif.OUT_MAT_NODE] - in_node = node_tree.nodes[Dif.BASE_TEX_NODE] - location = (out_node.location.x - 185 * 2, out_node.location.y - 500) - - alpha_test.init(node_tree, location, in_node.outputs['Value'], out_node.inputs['Alpha']) - else: - alpha_test.delete(node_tree) + node_tree.nodes[DifWeightDif.OVER_TEX_NODE].image = image @staticmethod - def set_blend_over_flavor(node_tree, switch_on): - """Set blend over flavor to this shader. + def set_over_texture_settings(node_tree, settings): + """Set over texture settings to 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 + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str """ - - # remove alpha test flavor if it was set already. Because these two can not coexist - if alpha_test.is_set(node_tree): - DifWeightDif.set_alpha_test_flavor(node_tree, False) - - out_node = node_tree.nodes[Dif.OUT_MAT_NODE] - in_node = node_tree.nodes[Dif.BASE_TEX_NODE] - - if switch_on: - blend_over.init(node_tree, in_node.outputs['Value'], out_node.inputs['Alpha']) - else: - blend_over.delete(node_tree) + _material_utils.set_texture_settings_to_node(node_tree.nodes[DifWeightDif.OVER_TEX_NODE], settings) @staticmethod - def set_blend_add_flavor(node_tree, switch_on): - """Set blend add flavor to this shader. + def set_over_uv(node_tree, uv_layer): + """Set UV layer to over texture in 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 + :param uv_layer: uv layer string used for over texture + :type uv_layer: str """ - # remove alpha test flavor if it was set already. Because these two can not coexist - if alpha_test.is_set(node_tree): - DifWeightDif.set_alpha_test_flavor(node_tree, False) - - out_node = node_tree.nodes[Dif.OUT_MAT_NODE] - in_node = node_tree.nodes[Dif.BASE_TEX_NODE] + if uv_layer is None or uv_layer == "": + uv_layer = _MESH_consts.none_uv - if switch_on: - blend_add.init(node_tree, in_node.outputs['Value'], out_node.inputs['Alpha']) - else: - blend_add.delete(node_tree) + node_tree.nodes[DifWeightDif.SEC_UVMAP_NODE].uv_map = uv_layer @staticmethod def set_tg1_flavor(node_tree, switch_on): @@ -215,13 +205,13 @@ def set_tg1_flavor(node_tree, switch_on): if switch_on and not tg1.is_set(node_tree): - out_node = node_tree.nodes[DifWeightDif.SEC_GEOM_NODE] + out_node = node_tree.nodes[Dif.GEOM_NODE] in_node = node_tree.nodes[DifWeightDif.OVER_TEX_NODE] out_node.location.x -= 185 location = (out_node.location.x + 185, out_node.location.y) - tg1.init(node_tree, location, out_node.outputs["Global"], in_node.inputs["Vector"]) + tg1.init(node_tree, location, out_node.outputs["Position"], in_node.inputs["Vector"]) elif not switch_on: diff --git a/addon/io_scs_tools/internals/shaders/eut2/fakeshadow/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/fakeshadow/__init__.py index e8e388a..0049857 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/fakeshadow/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/fakeshadow/__init__.py @@ -16,10 +16,14 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software +from io_scs_tools.internals.shaders.base import BaseShader -class Fakeshadow: + +class Fakeshadow(BaseShader): + WIREFRAME_NODE = "Wireframe" + MIX_NODE = "Mix" OUTPUT_NODE = "Output" @staticmethod @@ -38,24 +42,29 @@ def init(node_tree): start_pos_x = 0 start_pos_y = 0 + pos_x_shift = 185 + # node creation - output_n = node_tree.nodes.new("ShaderNodeOutput") - output_n.name = Fakeshadow.OUTPUT_NODE - output_n.label = Fakeshadow.OUTPUT_NODE - output_n.location = (start_pos_x, start_pos_y) - output_n.inputs['Color'].default_value = (0, 0, 0, 1) + wireframe_n = node_tree.nodes.new("ShaderNodeWireframe") + wireframe_n.name = wireframe_n.label = Fakeshadow.WIREFRAME_NODE + wireframe_n.location = (start_pos_x, start_pos_y) + wireframe_n.use_pixel_size = True + wireframe_n.inputs['Size'].default_value = 2.0 - @staticmethod - def set_material(node_tree, material): - """Set output material for this shader. + mix_n = node_tree.nodes.new("ShaderNodeMixRGB") + mix_n.name = mix_n.label = Fakeshadow.MIX_NODE + mix_n.location = (start_pos_x + pos_x_shift, start_pos_y) + mix_n.inputs['Color1'].default_value = (1, 1, 1, 1) # fakeshadow color + mix_n.inputs['Color2'].default_value = (0, 0, 0, 1) # wireframe color - :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 - """ + output_n = node_tree.nodes.new("ShaderNodeOutputMaterial") + output_n.name = output_n.label = Fakeshadow.OUTPUT_NODE + output_n.location = (start_pos_x + pos_x_shift * 2, start_pos_y) + + # links creation + node_tree.links.new(mix_n.inputs['Fac'], wireframe_n.outputs['Fac']) - pass # NOTE: fake shadows materials are not rendered in game so they don't need material + node_tree.links.new(output_n.inputs['Surface'], mix_n.outputs['Color']) @staticmethod def set_shadow_bias(node_tree, value): @@ -68,3 +77,16 @@ def set_shadow_bias(node_tree, value): """ pass # NOTE: shadow bias won't be visualized as game uses it's own implementation + + @staticmethod + def finalize(node_tree, material): + """Finalize node tree and material settings. Should be called as last. + + :param node_tree: node tree on which this shader should be finalized + :type node_tree: bpy.types.NodeTree + :param material: material used for this shader + :type material: bpy.types.Material + """ + + material.use_backface_culling = True + material.blend_method = "OPAQUE" diff --git a/addon/io_scs_tools/internals/shaders/eut2/flare/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/flare/__init__.py index 996fd32..ca6e876 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/flare/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/flare/__init__.py @@ -16,8 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.internals.shaders.eut2.unlit_vcol_tex import UnlitVcolTex @@ -41,18 +40,3 @@ def init(node_tree): # enable hardcoded flavors UnlitVcolTex.set_blend_add_flavor(node_tree, True) - - @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 - """ - - UnlitVcolTex.set_material(node_tree, material) - - material.use_transparency = True - material.transparency_method = "MASK" diff --git a/addon/io_scs_tools/internals/shaders/eut2/glass/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/glass/__init__.py index cc25f17..ec046ce 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/glass/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/glass/__init__.py @@ -16,53 +16,78 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from mathutils import Color from io_scs_tools.consts import Mesh as _MESH_consts -from io_scs_tools.internals.shaders.eut2.std_node_groups import compose_lighting -from io_scs_tools.internals.shaders.eut2.std_node_groups import add_env -from io_scs_tools.internals.shaders.eut2.std_node_groups import vcolor_input +from io_scs_tools.internals.shaders.base import BaseShader +from io_scs_tools.internals.shaders.eut2.std_node_groups import compose_lighting_ng +from io_scs_tools.internals.shaders.eut2.std_node_groups import add_env_ng +from io_scs_tools.internals.shaders.eut2.std_node_groups import lighting_evaluator_ng +from io_scs_tools.internals.shaders.eut2.std_node_groups import refl_normal_ng +from io_scs_tools.internals.shaders.eut2.std_node_groups import vcolor_input_ng from io_scs_tools.utils import convert as _convert_utils +from io_scs_tools.utils import material as _material_utils -class Glass: +class Glass(BaseShader): + # inputs DIFF_COL_NODE = "DiffuseColor" SPEC_COL_NODE = "SpecularColor" TINT_COL_NODE = "TintColor" TINT_OPACITY_NODE = "TintOpacity" - VCOL_GROUP_NODE = "VColorGroup" - GEOM_NODE = "Geometry" BASE_TEX_NODE = "BaseTex" + REFL_NORMAL_NODE = "ReflectionNormal" REFL_TEX_NODE = "ReflTex" ENV_COLOR_NODE = "EnvFactorColor" + + VCOL_GROUP_NODE = "VColorGroup" + GEOM_NODE = "Geometry" + UV_MAP_NODE = "UVMap" + + # pass 1 + VCOL_SCALE_NODE = "DiffVColScale" + + # pass 2 + GLASS_OPACITY_NODE = "GetGlassOpacity" + GLASS_TINT_NODE = "GetGlassTint" + + # pass 3 + OPAQUE_COLOR_NODE = "OpaqueColor" + + # pass 4 + FINAL_DIFFUSE = "FinalDiffuse" + FINAL_SPECULAR = "FinalSpecular" + + # pass 5 ADD_ENV_GROUP_NODE = "AddEnvGroup" - TINT_HSV_NODE = "TintColHSV" - TINT_OPACITY_COMBINE_NODE = "TintColCombine" - DIFF_VCOL_MULT_NODE = "DiffVColMultiplier" - OPACITY_VCOL_MULT_NODE = "DiffVColMultiplier" - SPEC_MULT_NODE = "SpecMultiplier" + # pass 6 + LIGHTING_EVAL_NODE = "LightingEvaluator" + FAKEOPAC_HSV_NODE = "FakeOpacityHSVSeparate" - TINT_SAT_SUBTRACT_NODE = "TintSaturationSubstract" - TINT_VCOL_MULT_NODE = "TintVColMultiplier" - DIFF_OPACITY_MULT_NODE = "DiffOpacityMultiplier" + # pass 7 + FAKEOPAC_SPEC_MIX_NODE = "FakeOpacitySpecMix" + FAKEOPAC_ADD_SV_NODE = "FakeOpacityAddSV" - TINT_VAL_SAT_MULT_NODE = "TintValueSaturationMultiplier" + # pass 8 + FAKEOPAC_SUB_SV_NODE = "FakeOpacitySubSV" + FAKEOPAC_V_INV_NODE = "FakeOpacityVInvert" - TINT_DIFF_MULT_NODE = "TintDiffMultiplier" + # pass 9 + FAKEOPAC_ADD_SPEC_MIX_NODE = "FakeOpacityAddSpec" + FAKEOPAC_MAX_SVV_NODE = "FakeOpacityMaxSVV" - OUT_MAT_NODE = "InputMaterial" - TINT_VAL_SUBTRACT_NODE = "TintValueSubtract" + # pass 10 + FAKEOPAC_MAX_ENV_SV_NODE = "FakeOpacityEnvSV" - COMPOSE_LIGHTING_NODE = "ComposeLighting" - OUT_ADD_SPEC_A_NODE = "OutputAddSpecAlpha" - MAX_TINT_VAL_OR_OPACITY_NODE = "MaxTintValueOpacity" + # pass 11 + FAKEOPAC_MAX_FINAL_NODE = "FakeOpacityFinal" - OUT_ADD_SPEC_NODE = "OutputAddSpec" - OUT_ADD_TINT_MAX_A_NODE = "OutputAddTintMaximum" + # pass 12 + COMPOSE_LIGHTING_NODE = "ComposeLighting" + # pass 13 OUTPUT_NODE = "Output" @staticmethod @@ -86,266 +111,264 @@ def init(node_tree): pos_x_shift = 185 # node creation - # vertex colors vcol_group_n = node_tree.nodes.new("ShaderNodeGroup") - vcol_group_n.name = Glass.VCOL_GROUP_NODE - vcol_group_n.label = Glass.VCOL_GROUP_NODE + vcol_group_n.name = vcol_group_n.label = Glass.VCOL_GROUP_NODE vcol_group_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1650) - vcol_group_n.node_tree = vcolor_input.get_node_group() + vcol_group_n.node_tree = vcolor_input_ng.get_node_group() - # geometry - geometry_n = node_tree.nodes.new("ShaderNodeGeometry") - geometry_n.name = Glass.GEOM_NODE - geometry_n.label = Glass.GEOM_NODE + geometry_n = node_tree.nodes.new("ShaderNodeNewGeometry") + geometry_n.name = geometry_n.label = Glass.GEOM_NODE geometry_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1500) - geometry_n.uv_layer = _MESH_consts.none_uv - # inputs - refl_tex_n = node_tree.nodes.new("ShaderNodeTexture") - refl_tex_n.name = Glass.REFL_TEX_NODE - refl_tex_n.label = Glass.REFL_TEX_NODE + uv_map_n = node_tree.nodes.new("ShaderNodeUVMap") + uv_map_n.name = uv_map_n.label = Glass.UV_MAP_NODE + uv_map_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1200) + uv_map_n.uv_map = _MESH_consts.none_uv + + refl_tex_n = node_tree.nodes.new("ShaderNodeTexEnvironment") + refl_tex_n.name = refl_tex_n.label = Glass.REFL_TEX_NODE refl_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 2700) + refl_tex_n.width = 140 env_col_n = node_tree.nodes.new("ShaderNodeRGB") - env_col_n.name = Glass.ENV_COLOR_NODE - env_col_n.label = Glass.ENV_COLOR_NODE + env_col_n.name = env_col_n.label = Glass.ENV_COLOR_NODE env_col_n.location = (start_pos_x + pos_x_shift, start_pos_y + 2400) tint_col_n = node_tree.nodes.new("ShaderNodeRGB") - tint_col_n.name = Glass.TINT_COL_NODE - tint_col_n.label = Glass.TINT_COL_NODE + tint_col_n.name = tint_col_n.label = Glass.TINT_COL_NODE tint_col_n.location = (start_pos_x + pos_x_shift, start_pos_y + 2200) tint_opacity_n = node_tree.nodes.new("ShaderNodeValue") - tint_opacity_n.name = Glass.TINT_OPACITY_NODE - tint_opacity_n.label = Glass.TINT_OPACITY_NODE + tint_opacity_n.name = tint_opacity_n.label = Glass.TINT_OPACITY_NODE tint_opacity_n.location = (start_pos_x + pos_x_shift, start_pos_y + 2000) spec_col_n = node_tree.nodes.new("ShaderNodeRGB") - spec_col_n.name = Glass.SPEC_COL_NODE - spec_col_n.label = Glass.SPEC_COL_NODE + spec_col_n.name = spec_col_n.label = Glass.SPEC_COL_NODE spec_col_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1900) diff_col_n = node_tree.nodes.new("ShaderNodeRGB") - diff_col_n.name = Glass.DIFF_COL_NODE - diff_col_n.label = Glass.DIFF_COL_NODE + diff_col_n.name = diff_col_n.label = Glass.DIFF_COL_NODE diff_col_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1700) - base_tex_n = node_tree.nodes.new("ShaderNodeTexture") - base_tex_n.name = Glass.BASE_TEX_NODE - base_tex_n.label = Glass.BASE_TEX_NODE + base_tex_n = node_tree.nodes.new("ShaderNodeTexImage") + base_tex_n.name = base_tex_n.label = Glass.BASE_TEX_NODE base_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1500) + base_tex_n.width = 140 + + # pass 0 + refl_norm_n = node_tree.nodes.new("ShaderNodeGroup") + refl_norm_n.name = refl_norm_n.label = Glass.REFL_NORMAL_NODE + refl_norm_n.location = (start_pos_x + pos_x_shift * 0, start_pos_y + 2500) + refl_norm_n.node_tree = refl_normal_ng.get_node_group() # pass 1 - add_env_gn = node_tree.nodes.new("ShaderNodeGroup") - add_env_gn.name = Glass.ADD_ENV_GROUP_NODE - add_env_gn.label = Glass.ADD_ENV_GROUP_NODE - add_env_gn.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 2500) - add_env_gn.node_tree = add_env.get_node_group() - add_env_gn.inputs['Apply Fresnel'].default_value = 1.0 - add_env_gn.inputs['Base Texture Alpha'].default_value = 1.0 - add_env_gn.inputs['Fresnel Scale'].default_value = 2.0 - add_env_gn.inputs['Fresnel Bias'].default_value = 1.0 - - tint_col_hsv_n = node_tree.nodes.new("ShaderNodeSeparateHSV") - tint_col_hsv_n.name = Glass.TINT_HSV_NODE - tint_col_hsv_n.label = Glass.TINT_HSV_NODE - tint_col_hsv_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 2100) - - tint_opacity_combine_n = node_tree.nodes.new("ShaderNodeCombineRGB") - tint_opacity_combine_n.name = Glass.TINT_OPACITY_COMBINE_NODE - tint_opacity_combine_n.label = Glass.TINT_OPACITY_COMBINE_NODE - tint_opacity_combine_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1800) - - diff_vcol_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") - diff_vcol_mult_n.name = Glass.DIFF_VCOL_MULT_NODE - diff_vcol_mult_n.label = Glass.DIFF_VCOL_MULT_NODE - diff_vcol_mult_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1650) - diff_vcol_mult_n.blend_type = "MULTIPLY" - diff_vcol_mult_n.inputs['Fac'].default_value = 1 - - spec_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") - spec_mult_n.name = Glass.SPEC_MULT_NODE - spec_mult_n.label = Glass.SPEC_MULT_NODE - spec_mult_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1450) - spec_mult_n.blend_type = "MULTIPLY" - spec_mult_n.inputs['Fac'].default_value = 1 + vcol_scale_n = node_tree.nodes.new("ShaderNodeVectorMath") + vcol_scale_n.name = vcol_scale_n.label = Glass.VCOL_SCALE_NODE + vcol_scale_n.location = (start_pos_x + pos_x_shift * 2, start_pos_y + 1650) + vcol_scale_n.operation = "MULTIPLY" + vcol_scale_n.inputs[1].default_value = (2.0,) * 3 # pass 2 - tint_sat_subtract_n = node_tree.nodes.new("ShaderNodeMath") - tint_sat_subtract_n.name = Glass.TINT_SAT_SUBTRACT_NODE - tint_sat_subtract_n.label = Glass.TINT_SAT_SUBTRACT_NODE - tint_sat_subtract_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 2200) - tint_sat_subtract_n.operation = "SUBTRACT" - tint_sat_subtract_n.inputs[0].default_value = 1.0 - - tint_vcol_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") - tint_vcol_mult_n.name = Glass.TINT_VCOL_MULT_NODE - tint_vcol_mult_n.label = Glass.TINT_VCOL_MULT_NODE - tint_vcol_mult_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 1950) - tint_vcol_mult_n.blend_type = "MULTIPLY" - tint_vcol_mult_n.inputs['Fac'].default_value = 1 - - diff_opacity_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") - diff_opacity_mult_n.name = Glass.DIFF_OPACITY_MULT_NODE - diff_opacity_mult_n.label = Glass.DIFF_OPACITY_MULT_NODE - diff_opacity_mult_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 1750) - diff_opacity_mult_n.blend_type = "MULTIPLY" - diff_opacity_mult_n.inputs['Fac'].default_value = 1 + glass_opacity_n = node_tree.nodes.new("ShaderNodeMath") + glass_opacity_n.name = glass_opacity_n.label = Glass.GLASS_OPACITY_NODE + glass_opacity_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1500) + glass_opacity_n.operation = "MULTIPLY" + + glass_tint_n = node_tree.nodes.new("ShaderNodeVectorMath") + glass_tint_n.name = glass_tint_n.label = Glass.GLASS_TINT_NODE + glass_tint_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1900) + glass_tint_n.operation = "MULTIPLY" # pass 3 - tint_val_sat_mult_n = node_tree.nodes.new("ShaderNodeMath") - tint_val_sat_mult_n.name = Glass.TINT_VAL_SAT_MULT_NODE - tint_val_sat_mult_n.label = Glass.TINT_VAL_SAT_MULT_NODE - tint_val_sat_mult_n.location = (start_pos_x + pos_x_shift * 5, start_pos_y + 2150) - tint_val_sat_mult_n.operation = "MULTIPLY" + opaque_col_n = node_tree.nodes.new("ShaderNodeVectorMath") + opaque_col_n.name = opaque_col_n.label = Glass.OPAQUE_COLOR_NODE + opaque_col_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 1900) + opaque_col_n.operation = "MULTIPLY" # pass 4 - tint_diff_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") - tint_diff_mult_n.name = Glass.TINT_DIFF_MULT_NODE - tint_diff_mult_n.label = Glass.TINT_DIFF_MULT_NODE - tint_diff_mult_n.location = (start_pos_x + pos_x_shift * 6, start_pos_y + 1900) - tint_diff_mult_n.blend_type = "MULTIPLY" - tint_diff_mult_n.inputs['Fac'].default_value = 1 + final_diff_n = node_tree.nodes.new("ShaderNodeVectorMath") + final_diff_n.name = final_diff_n.label = Glass.FINAL_DIFFUSE + final_diff_n.location = (start_pos_x + pos_x_shift * 5, start_pos_y + 1500) + final_diff_n.operation = "MULTIPLY" + final_diff_n.inputs[0].default_value = (1.0,) * 3 + + final_spec_n = node_tree.nodes.new("ShaderNodeVectorMath") + final_spec_n.name = final_spec_n.label = Glass.FINAL_SPECULAR + final_spec_n.location = (start_pos_x + pos_x_shift * 5, start_pos_y + 1700) + final_spec_n.operation = "MULTIPLY" # pass 5 - out_mat_n = node_tree.nodes.new("ShaderNodeExtendedMaterial") - out_mat_n.name = Glass.OUT_MAT_NODE - out_mat_n.label = Glass.OUT_MAT_NODE - if "SpecTra" in out_mat_n: - out_mat_n.inputs['SpecTra'].default_value = 0.0 - if "Refl" in out_mat_n: - out_mat_n.inputs['Refl'].default_value = 1.0 - elif "Reflectivity" in out_mat_n: - out_mat_n.inputs['Reflectivity'].default_value = 1.0 - out_mat_n.location = (start_pos_x + pos_x_shift * 7, start_pos_y + 2100) - - tint_val_subtract_n = node_tree.nodes.new("ShaderNodeMath") - tint_val_subtract_n.name = Glass.TINT_VAL_SUBTRACT_NODE - tint_val_subtract_n.label = Glass.TINT_VAL_SUBTRACT_NODE - tint_val_subtract_n.location = (start_pos_x + pos_x_shift * 7, start_pos_y + 1400) - tint_val_subtract_n.operation = "SUBTRACT" - tint_val_subtract_n.inputs[0].default_value = 1.0 + lighting_eval_n = node_tree.nodes.new("ShaderNodeGroup") + lighting_eval_n.name = lighting_eval_n.label = Glass.LIGHTING_EVAL_NODE + lighting_eval_n.location = (start_pos_x + pos_x_shift * 6, start_pos_y + 1800) + lighting_eval_n.node_tree = lighting_evaluator_ng.get_node_group() + + fakeopac_hsv_n = node_tree.nodes.new("ShaderNodeSeparateHSV") + fakeopac_hsv_n.name = fakeopac_hsv_n.label = Glass.FAKEOPAC_HSV_NODE + fakeopac_hsv_n.location = (start_pos_x + pos_x_shift * 6, start_pos_y + 1500) # pass 6 - compose_lighting_n = node_tree.nodes.new("ShaderNodeGroup") - compose_lighting_n.name = Glass.COMPOSE_LIGHTING_NODE - compose_lighting_n.label = Glass.COMPOSE_LIGHTING_NODE - compose_lighting_n.location = (start_pos_x + pos_x_shift * 8, start_pos_y + 2400) - compose_lighting_n.node_tree = compose_lighting.get_node_group() - - out_add_spec_a_n = node_tree.nodes.new("ShaderNodeMath") - out_add_spec_a_n.name = Glass.OUT_ADD_SPEC_A_NODE - out_add_spec_a_n.label = Glass.OUT_ADD_SPEC_A_NODE - out_add_spec_a_n.location = (start_pos_x + pos_x_shift * 8, start_pos_y + 1550) - out_add_spec_a_n.operation = "ADD" - - max_tint_n = node_tree.nodes.new("ShaderNodeMath") - max_tint_n.name = Glass.MAX_TINT_VAL_OR_OPACITY_NODE - max_tint_n.label = Glass.MAX_TINT_VAL_OR_OPACITY_NODE - max_tint_n.location = (start_pos_x + pos_x_shift * 8, start_pos_y + 1350) - max_tint_n.operation = "MAXIMUM" + add_env_n = node_tree.nodes.new("ShaderNodeGroup") + add_env_n.name = add_env_n.label = Glass.ADD_ENV_GROUP_NODE + add_env_n.location = (start_pos_x + pos_x_shift * 7, start_pos_y + 2500) + add_env_n.node_tree = add_env_ng.get_node_group() + add_env_n.inputs['Fresnel Scale'].default_value = 2.0 + add_env_n.inputs['Fresnel Bias'].default_value = 1.0 + add_env_n.inputs['Apply Fresnel'].default_value = 1.0 + add_env_n.inputs['Base Texture Alpha'].default_value = 1.0 + add_env_n.inputs['Weighted Color'].default_value = (1.0,) * 4 + add_env_n.inputs['Strength Multiplier'].default_value = 0.5 + + fakeopac_spec_mix_n = node_tree.nodes.new("ShaderNodeVectorMath") + fakeopac_spec_mix_n.name = fakeopac_spec_mix_n.label = Glass.FAKEOPAC_SPEC_MIX_NODE + fakeopac_spec_mix_n.location = (start_pos_x + pos_x_shift * 7, start_pos_y + 1900) + fakeopac_spec_mix_n.operation = "MULTIPLY" + + fakeopac_add_sv_n = node_tree.nodes.new("ShaderNodeMath") + fakeopac_add_sv_n.name = fakeopac_add_sv_n.label = Glass.FAKEOPAC_ADD_SV_NODE + fakeopac_add_sv_n.location = (start_pos_x + pos_x_shift * 7, start_pos_y + 1600) + fakeopac_add_sv_n.operation = "ADD" # pass 7 - out_add_spec_n = node_tree.nodes.new("ShaderNodeMixRGB") - out_add_spec_n.name = Glass.OUT_ADD_SPEC_NODE - out_add_spec_n.label = Glass.OUT_ADD_SPEC_NODE - out_add_spec_n.location = (start_pos_x + pos_x_shift * 9, start_pos_y + 2200) - out_add_spec_n.blend_type = "ADD" - out_add_spec_n.inputs['Fac'].default_value = 1 - - out_add_tint_max_a_n = node_tree.nodes.new("ShaderNodeMath") - out_add_tint_max_a_n.name = Glass.OUT_ADD_TINT_MAX_A_NODE - out_add_tint_max_a_n.label = Glass.OUT_ADD_TINT_MAX_A_NODE - out_add_tint_max_a_n.location = (start_pos_x + pos_x_shift * 9, start_pos_y + 1500) + fakeopac_sub_sv_n = node_tree.nodes.new("ShaderNodeMath") + fakeopac_sub_sv_n.name = fakeopac_sub_sv_n.label = Glass.FAKEOPAC_SUB_SV_NODE + fakeopac_sub_sv_n.location = (start_pos_x + pos_x_shift * 8, start_pos_y + 1500) + fakeopac_sub_sv_n.operation = "SUBTRACT" + fakeopac_sub_sv_n.use_clamp = True + + fakeopac_v_inv_n = node_tree.nodes.new("ShaderNodeMath") + fakeopac_v_inv_n.name = fakeopac_v_inv_n.label = Glass.FAKEOPAC_V_INV_NODE + fakeopac_v_inv_n.location = (start_pos_x + pos_x_shift * 8, start_pos_y + 1300) + fakeopac_v_inv_n.operation = "SUBTRACT" + + # pass 8 + fakeopac_add_spec_mix_n = node_tree.nodes.new("ShaderNodeMath") + fakeopac_add_spec_mix_n.name = fakeopac_add_spec_mix_n.label = Glass.FAKEOPAC_ADD_SPEC_MIX_NODE + fakeopac_add_spec_mix_n.location = (start_pos_x + pos_x_shift * 9, start_pos_y + 1800) + fakeopac_add_spec_mix_n.operation = "ADD" + + fakeopac_max_svv_n = node_tree.nodes.new("ShaderNodeMath") + fakeopac_max_svv_n.name = fakeopac_max_svv_n.label = Glass.FAKEOPAC_MAX_SVV_NODE + fakeopac_max_svv_n.location = (start_pos_x + pos_x_shift * 9, start_pos_y + 1600) + fakeopac_max_svv_n.operation = "MAXIMUM" + + # pass 9 + fakeopac_max_env_sv_n = node_tree.nodes.new("ShaderNodeMath") + fakeopac_max_env_sv_n.name = fakeopac_max_env_sv_n.label = Glass.FAKEOPAC_MAX_ENV_SV_NODE + fakeopac_max_env_sv_n.location = (start_pos_x + pos_x_shift * 10, start_pos_y + 1700) + fakeopac_max_env_sv_n.operation = "MAXIMUM" + + # pass 10 + fakeopac_max_final_n = node_tree.nodes.new("ShaderNodeMath") + fakeopac_max_final_n.name = fakeopac_max_final_n.label = Glass.FAKEOPAC_MAX_FINAL_NODE + fakeopac_max_final_n.location = (start_pos_x + pos_x_shift * 11, start_pos_y + 1500) + fakeopac_max_final_n.operation = "MAXIMUM" + + # pass 11 + compose_lighting_n = node_tree.nodes.new("ShaderNodeGroup") + compose_lighting_n.name = compose_lighting_n.label = Glass.COMPOSE_LIGHTING_NODE + compose_lighting_n.location = (start_pos_x + pos_x_shift * 12, start_pos_y + 2000) + compose_lighting_n.node_tree = compose_lighting_ng.get_node_group() + compose_lighting_n.inputs['Alpha'].default_value = 1.0 # output - output_n = node_tree.nodes.new("ShaderNodeOutput") - output_n.name = Glass.OUTPUT_NODE - output_n.label = Glass.OUTPUT_NODE - output_n.location = (start_pos_x + + pos_x_shift * 11, start_pos_y + 1900) + output_n = node_tree.nodes.new("ShaderNodeOutputMaterial") + output_n.name = output_n.label = Glass.OUTPUT_NODE + output_n.location = (start_pos_x + + pos_x_shift * 13, start_pos_y + 1800) # links creation - # input - node_tree.links.new(base_tex_n.inputs['Vector'], geometry_n.outputs['UV']) - node_tree.links.new(refl_tex_n.inputs['Vector'], geometry_n.outputs['Normal']) - - # pass 1 - node_tree.links.new(add_env_gn.inputs['Normal Vector'], geometry_n.outputs['Normal']) - node_tree.links.new(add_env_gn.inputs['View Vector'], geometry_n.outputs['View']) - node_tree.links.new(add_env_gn.inputs['Reflection Texture Color'], refl_tex_n.outputs['Color']) - node_tree.links.new(add_env_gn.inputs['Env Factor Color'], env_col_n.outputs['Color']) - node_tree.links.new(add_env_gn.inputs['Specular Color'], spec_col_n.outputs['Color']) - - node_tree.links.new(tint_col_hsv_n.inputs['Color'], tint_col_n.outputs['Color']) - - node_tree.links.new(tint_opacity_combine_n.inputs['R'], tint_opacity_n.outputs['Value']) - node_tree.links.new(tint_opacity_combine_n.inputs['G'], tint_opacity_n.outputs['Value']) - node_tree.links.new(tint_opacity_combine_n.inputs['B'], tint_opacity_n.outputs['Value']) + # inputs + node_tree.links.new(base_tex_n.inputs['Vector'], uv_map_n.outputs['UV']) + node_tree.links.new(refl_tex_n.inputs['Vector'], refl_norm_n.outputs['Reflection Normal']) - node_tree.links.new(diff_vcol_mult_n.inputs['Color1'], diff_col_n.outputs['Color']) - node_tree.links.new(diff_vcol_mult_n.inputs['Color2'], vcol_group_n.outputs['Vertex Color']) + # pass 0 + node_tree.links.new(refl_norm_n.inputs['Incoming'], geometry_n.outputs['Incoming']) + node_tree.links.new(refl_norm_n.inputs['Normal'], lighting_eval_n.outputs['Normal']) - node_tree.links.new(spec_mult_n.inputs['Color1'], spec_col_n.outputs['Color']) - node_tree.links.new(spec_mult_n.inputs['Color2'], base_tex_n.outputs['Value']) + # pass 1 + node_tree.links.new(vcol_scale_n.inputs[0], vcol_group_n.outputs['Vertex Color']) # pass 2 - node_tree.links.new(tint_sat_subtract_n.inputs[1], tint_col_hsv_n.outputs['S']) + node_tree.links.new(glass_opacity_n.inputs[0], tint_opacity_n.outputs[0]) + node_tree.links.new(glass_opacity_n.inputs[1], vcol_group_n.outputs['Vertex Color Alpha']) - node_tree.links.new(tint_vcol_mult_n.inputs['Color1'], tint_col_n.outputs['Color']) - node_tree.links.new(tint_vcol_mult_n.inputs['Color2'], vcol_group_n.outputs['Vertex Color']) - - node_tree.links.new(diff_opacity_mult_n.inputs['Color1'], tint_opacity_combine_n.outputs['Image']) - node_tree.links.new(diff_opacity_mult_n.inputs['Color2'], diff_vcol_mult_n.outputs['Color']) + node_tree.links.new(glass_tint_n.inputs[0], tint_col_n.outputs['Color']) + node_tree.links.new(glass_tint_n.inputs[1], vcol_scale_n.outputs[0]) # pass 3 - node_tree.links.new(tint_val_sat_mult_n.inputs[0], tint_sat_subtract_n.outputs[0]) - node_tree.links.new(tint_val_sat_mult_n.inputs[1], tint_col_hsv_n.outputs['V']) + node_tree.links.new(opaque_col_n.inputs[0], diff_col_n.outputs['Color']) + node_tree.links.new(opaque_col_n.inputs[1], glass_tint_n.outputs[0]) # pass 4 - node_tree.links.new(tint_diff_mult_n.inputs['Fac'], tint_val_sat_mult_n.outputs['Value']) - node_tree.links.new(tint_diff_mult_n.inputs['Color1'], tint_vcol_mult_n.outputs['Color']) - node_tree.links.new(tint_diff_mult_n.inputs['Color2'], diff_opacity_mult_n.outputs['Color']) + # node_tree.links.new(final_diff_n.inputs[0], glass_opacity_n.outputs[0]) + node_tree.links.new(final_diff_n.inputs[1], opaque_col_n.outputs[0]) + + node_tree.links.new(final_spec_n.inputs[0], spec_col_n.outputs['Color']) + node_tree.links.new(final_spec_n.inputs[1], base_tex_n.outputs['Alpha']) # pass 5 - node_tree.links.new(out_mat_n.inputs['Spec'], spec_mult_n.outputs['Color']) - node_tree.links.new(tint_val_subtract_n.inputs[1], vcol_group_n.outputs['Vertex Color Alpha']) + node_tree.links.new(lighting_eval_n.inputs['Normal Vector'], geometry_n.outputs['Normal']) + node_tree.links.new(lighting_eval_n.inputs['Incoming Vector'], geometry_n.outputs['Incoming']) + + node_tree.links.new(fakeopac_hsv_n.inputs['Color'], glass_tint_n.outputs[0]) # pass 6 - node_tree.links.new(compose_lighting_n.inputs['Diffuse Color'], tint_diff_mult_n.outputs['Color']) - node_tree.links.new(compose_lighting_n.inputs['Material Color'], tint_diff_mult_n.outputs['Color']) - node_tree.links.new(compose_lighting_n.inputs['Env Color'], add_env_gn.outputs['Environment Addition Color']) + node_tree.links.new(add_env_n.inputs['Normal Vector'], lighting_eval_n.outputs['Normal']) + node_tree.links.new(add_env_n.inputs['Reflection Normal Vector'], refl_norm_n.outputs['Reflection Normal']) + node_tree.links.new(add_env_n.inputs['Reflection Texture Color'], refl_tex_n.outputs['Color']) + node_tree.links.new(add_env_n.inputs['Env Factor Color'], env_col_n.outputs['Color']) + node_tree.links.new(add_env_n.inputs['Specular Color'], spec_col_n.outputs['Color']) - node_tree.links.new(out_add_spec_a_n.inputs[0], out_mat_n.outputs['Spec']) - node_tree.links.new(out_add_spec_a_n.inputs[1], add_env_gn.outputs['Environment Addition Color']) + node_tree.links.new(fakeopac_spec_mix_n.inputs[0], final_spec_n.outputs[0]) + node_tree.links.new(fakeopac_spec_mix_n.inputs[1], lighting_eval_n.outputs['Specular Lighting']) - node_tree.links.new(max_tint_n.inputs[0], tint_val_subtract_n.outputs[0]) - node_tree.links.new(max_tint_n.inputs[1], tint_opacity_n.outputs[0]) + node_tree.links.new(fakeopac_add_sv_n.inputs[0], fakeopac_hsv_n.outputs['S']) + node_tree.links.new(fakeopac_add_sv_n.inputs[1], fakeopac_hsv_n.outputs['V']) # pass 7 - node_tree.links.new(out_add_spec_n.inputs['Color1'], compose_lighting_n.outputs['Composed Color']) - node_tree.links.new(out_add_spec_n.inputs['Color2'], out_mat_n.outputs['Spec']) + node_tree.links.new(fakeopac_sub_sv_n.inputs[0], fakeopac_add_sv_n.outputs['Value']) + node_tree.links.new(fakeopac_sub_sv_n.inputs[1], fakeopac_hsv_n.outputs['V']) - node_tree.links.new(out_add_tint_max_a_n.inputs[0], out_add_spec_a_n.outputs[0]) - node_tree.links.new(out_add_tint_max_a_n.inputs[1], max_tint_n.outputs[0]) + node_tree.links.new(fakeopac_v_inv_n.inputs[1], fakeopac_hsv_n.outputs['V']) + + # pass 8 + node_tree.links.new(fakeopac_add_spec_mix_n.inputs[0], add_env_n.outputs['Environment Addition Color']) + node_tree.links.new(fakeopac_add_spec_mix_n.inputs[1], fakeopac_spec_mix_n.outputs[0]) + + node_tree.links.new(fakeopac_max_svv_n.inputs[0], fakeopac_sub_sv_n.outputs['Value']) + node_tree.links.new(fakeopac_max_svv_n.inputs[1], fakeopac_v_inv_n.outputs['Value']) + + # pass 9 + node_tree.links.new(fakeopac_max_env_sv_n.inputs[0], fakeopac_add_spec_mix_n.outputs['Value']) + node_tree.links.new(fakeopac_max_env_sv_n.inputs[1], fakeopac_max_svv_n.outputs['Value']) + + # pass 10 + node_tree.links.new(fakeopac_max_final_n.inputs[0], fakeopac_max_env_sv_n.outputs['Value']) + node_tree.links.new(fakeopac_max_final_n.inputs[1], glass_opacity_n.outputs['Value']) + + # pass 11 + node_tree.links.new(compose_lighting_n.inputs['Diffuse Color'], final_diff_n.outputs[0]) + node_tree.links.new(compose_lighting_n.inputs['Specular Color'], final_spec_n.outputs[0]) + node_tree.links.new(compose_lighting_n.inputs['Env Color'], add_env_n.outputs['Environment Addition Color']) + node_tree.links.new(compose_lighting_n.inputs['Diffuse Lighting'], lighting_eval_n.outputs['Diffuse Lighting']) + node_tree.links.new(compose_lighting_n.inputs['Specular Lighting'], lighting_eval_n.outputs['Specular Lighting']) + node_tree.links.new(compose_lighting_n.inputs['Alpha'], fakeopac_max_final_n.outputs['Value']) # output - node_tree.links.new(output_n.inputs['Color'], out_add_spec_n.outputs['Color']) - node_tree.links.new(output_n.inputs['Alpha'], out_add_tint_max_a_n.outputs[0]) + node_tree.links.new(output_n.inputs['Surface'], compose_lighting_n.outputs['Shader']) @staticmethod - def set_material(node_tree, material): - """Set output material for this shader. + def finalize(node_tree, material): + """Finalize node tree and material settings. Should be called as last. - :param node_tree: node tree of current shader + :param node_tree: node tree on which this shader should be finalized :type node_tree: bpy.types.NodeTree - :param material: blender material for used in this tree node as output + :param material: material used for this shader :type material: bpy.types.Material """ - material.use_transparency = True - material.transparency_method = "Z_TRANSPARENCY" - node_tree.nodes[Glass.OUT_MAT_NODE].material = material + material.use_backface_culling = True + material.blend_method = "BLEND" @staticmethod def set_add_ambient(node_tree, factor): @@ -372,7 +395,6 @@ def set_diffuse(node_tree, color): color = _convert_utils.to_node_color(color) node_tree.nodes[Glass.DIFF_COL_NODE].outputs['Color'].default_value = color - node_tree.nodes[Glass.OUT_MAT_NODE].material.diffuse_intensity = 0.7 @staticmethod def set_specular(node_tree, color): @@ -387,8 +409,6 @@ def set_specular(node_tree, color): color = _convert_utils.to_node_color(color) node_tree.nodes[Glass.SPEC_COL_NODE].outputs['Color'].default_value = color - # fix intensity each time if user might changed it by hand directly on material - node_tree.nodes[Glass.OUT_MAT_NODE].material.specular_intensity = 1.0 @staticmethod def set_shininess(node_tree, factor): @@ -400,7 +420,7 @@ def set_shininess(node_tree, factor): :type factor: float """ - node_tree.nodes[Glass.OUT_MAT_NODE].material.specular_hardness = factor + node_tree.nodes[Glass.LIGHTING_EVAL_NODE].inputs['Shininess'].default_value = factor @staticmethod def set_reflection(node_tree, value): @@ -415,16 +435,27 @@ def set_reflection(node_tree, value): pass # NOTE: reflection attribute doesn't change anything in rendered material, so pass it @staticmethod - def set_base_texture(node_tree, texture): + def set_base_texture(node_tree, image): """Set base texture to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assignet to base texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assignet to base texture node + :type image: bpy.types.Image """ - node_tree.nodes[Glass.BASE_TEX_NODE].texture = texture + node_tree.nodes[Glass.BASE_TEX_NODE].image = image + + @staticmethod + def set_base_texture_settings(node_tree, settings): + """Set base texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + _material_utils.set_texture_settings_to_node(node_tree.nodes[Glass.BASE_TEX_NODE], settings) @staticmethod def set_base_uv(node_tree, uv_layer): @@ -439,19 +470,30 @@ def set_base_uv(node_tree, uv_layer): if uv_layer is None or uv_layer == "": uv_layer = _MESH_consts.none_uv - node_tree.nodes[Glass.GEOM_NODE].uv_layer = uv_layer + node_tree.nodes[Glass.UV_MAP_NODE].uv_map = uv_layer @staticmethod - def set_reflection_texture(node_tree, texture): + def set_reflection_texture(node_tree, image): """Set reflection texture on shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture object which should be used for reflection - :type texture: bpy.types.Texture + :param image: texture image object which should be used for reflection + :type image: bpy.types.Image """ - node_tree.nodes[Glass.REFL_TEX_NODE].texture = texture + node_tree.nodes[Glass.REFL_TEX_NODE].image = image + + @staticmethod + def set_reflection_texture_settings(node_tree, settings): + """Set reflection texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + pass # reflection texture shouldn't use any custom settings @staticmethod def set_env_factor(node_tree, color): diff --git a/addon/io_scs_tools/internals/shaders/eut2/grass/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/grass/__init__.py index 0e3553a..cb0b3bd 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/grass/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/grass/__init__.py @@ -16,12 +16,14 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.internals.shaders.eut2.dif import Dif class Grass(Dif): + NORMAL_TRANS_NODE = "UpNormal" + @staticmethod def get_name(): """Get name of this shader file with full modules path.""" @@ -38,23 +40,20 @@ def init(node_tree): # init parent Dif.init(node_tree) - out_mat_n = node_tree.nodes[Dif.OUTPUT_NODE] - opacity_n = node_tree.nodes[Dif.OPACITY_NODE] - - # create links - node_tree.links.new(out_mat_n.inputs['Alpha'], opacity_n.outputs[0]) - - @staticmethod - def set_material(node_tree, material): - """Set output material for this shader. + geom_n = node_tree.nodes[Dif.GEOM_NODE] + lighting_eval_n = node_tree.nodes[Dif.LIGHTING_EVAL_NODE] - :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 - """ + # nodes creation + normal_trans_n = node_tree.nodes.new("ShaderNodeVectorTransform") + normal_trans_n.name = normal_trans_n.label = Grass.NORMAL_TRANS_NODE + normal_trans_n.location = (geom_n.location.x, geom_n.location.y - 250) + normal_trans_n.vector_type = "NORMAL" + normal_trans_n.convert_from = "OBJECT" + normal_trans_n.convert_to = "WORLD" + normal_trans_n.inputs['Vector'].default_value = (0, 0, 1) # up normal in object space - Dif.set_material(node_tree, material) + # links creation + node_tree.links.new(lighting_eval_n.inputs['Normal Vector'], normal_trans_n.outputs['Vector']) - material.use_transparency = True - material.transparency_method = "Z_TRANSPARENCY" + # enable hardcoded flavour + Dif.set_alpha_test_flavor(node_tree, True) diff --git a/addon/io_scs_tools/internals/shaders/eut2/lamp/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/lamp/__init__.py index ba1d7e1..d147413 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/lamp/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/lamp/__init__.py @@ -16,18 +16,21 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.consts import Mesh as _MESH_consts from io_scs_tools.internals.shaders.eut2.dif_spec import DifSpec -from io_scs_tools.internals.shaders.eut2.std_node_groups import lampmask_mixer +from io_scs_tools.internals.shaders.eut2.std_node_groups import lampmask_mixer_ng +from io_scs_tools.utils import material as _material_utils class Lamp(DifSpec): - SEC_GEOM_NODE = "SecondGeometry" + SEC_UVMAP_NODE = "SecondUVMap" MASK_TEX_NODE = "MaskTex" LAMPMASK_MIX_GROUP_NODE = "LampmaskMix" OUT_ADD_LAMPMASK_NODE = "LampmaskAdd" + OUT_ALPHA_INVERSE_NODE = "OutAlphaInverse" + OUT_MAT_NODE = "OutMat" @staticmethod def get_name(): @@ -67,56 +70,84 @@ def init(node_tree, init_dif_spec=True, start_pos_x=0, start_pos_y=0): output_n = node_tree.nodes[DifSpec.OUTPUT_NODE] # move existing - output_n.location.x += pos_x_shift + output_n.location.x += pos_x_shift * 3 # nodes creation - sec_geom_n = node_tree.nodes.new("ShaderNodeGeometry") - sec_geom_n.name = Lamp.SEC_GEOM_NODE - sec_geom_n.label = Lamp.SEC_GEOM_NODE - sec_geom_n.location = (start_pos_x - pos_x_shift, start_pos_y + 2300) - sec_geom_n.uv_layer = _MESH_consts.none_uv - - mask_tex_n = node_tree.nodes.new("ShaderNodeTexture") - mask_tex_n.name = Lamp.MASK_TEX_NODE - mask_tex_n.label = Lamp.MASK_TEX_NODE + sec_uvmap_n = node_tree.nodes.new("ShaderNodeUVMap") + sec_uvmap_n.name = sec_uvmap_n.label = Lamp.SEC_UVMAP_NODE + sec_uvmap_n.location = (start_pos_x - pos_x_shift, start_pos_y + 2300) + sec_uvmap_n.uv_map = _MESH_consts.none_uv + + mask_tex_n = node_tree.nodes.new("ShaderNodeTexImage") + mask_tex_n.name = mask_tex_n.label = Lamp.MASK_TEX_NODE mask_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 2400) + mask_tex_n.width = 140 + + lampmask_mixer_n = node_tree.nodes.new("ShaderNodeGroup") + lampmask_mixer_n.name = lampmask_mixer_n.label = Lamp.LAMPMASK_MIX_GROUP_NODE + lampmask_mixer_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 2300) + lampmask_mixer_n.node_tree = lampmask_mixer_ng.get_node_group() + + out_add_lampmask_n = node_tree.nodes.new("ShaderNodeVectorMath") + out_add_lampmask_n.name = out_add_lampmask_n.label = Lamp.OUT_ADD_LAMPMASK_NODE + out_add_lampmask_n.location = (output_n.location.x - pos_x_shift * 2, compose_lighting_n.location.y + 200) + out_add_lampmask_n.operation = "ADD" + + out_a_inv_n = node_tree.nodes.new("ShaderNodeMath") + out_a_inv_n.name = out_a_inv_n.label = Lamp.OUT_ALPHA_INVERSE_NODE + out_a_inv_n.location = (output_n.location.x - pos_x_shift * 2, output_n.location.y - 100) + out_a_inv_n.operation = "SUBTRACT" + out_a_inv_n.use_clamp = True + out_a_inv_n.inputs[0].default_value = 0.999999 # TODO: change back to 1.0 after bug is fixed: https://developer.blender.org/T71426 + + out_mat_n = node_tree.nodes.new("ShaderNodeEeveeSpecular") + out_mat_n.name = out_mat_n.label = Lamp.OUT_MAT_NODE + out_mat_n.location = (output_n.location.x - pos_x_shift, output_n.location.y) + out_mat_n.inputs["Base Color"].default_value = (0.0,) * 4 + out_mat_n.inputs["Specular"].default_value = (0.0,) * 4 - lampmask_mixr_gn = node_tree.nodes.new("ShaderNodeGroup") - lampmask_mixr_gn.name = Lamp.LAMPMASK_MIX_GROUP_NODE - lampmask_mixr_gn.label = Lamp.LAMPMASK_MIX_GROUP_NODE - lampmask_mixr_gn.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 2300) - lampmask_mixr_gn.node_tree = lampmask_mixer.get_node_group() + # links creation + node_tree.links.new(mask_tex_n.inputs['Vector'], sec_uvmap_n.outputs["UV"]) - out_add_lampmask_n = node_tree.nodes.new("ShaderNodeMixRGB") - out_add_lampmask_n.name = Lamp.OUT_ADD_LAMPMASK_NODE - out_add_lampmask_n.label = Lamp.OUT_ADD_LAMPMASK_NODE - out_add_lampmask_n.location = (output_n.location.x - pos_x_shift, start_pos_y + 1950) - out_add_lampmask_n.blend_type = "ADD" - out_add_lampmask_n.inputs['Fac'].default_value = 1 + node_tree.links.new(lampmask_mixer_n.inputs['Lampmask Tex Alpha'], mask_tex_n.outputs['Alpha']) + node_tree.links.new(lampmask_mixer_n.inputs['Lampmask Tex Color'], mask_tex_n.outputs['Color']) + node_tree.links.new(lampmask_mixer_n.inputs['UV Vector'], sec_uvmap_n.outputs['UV']) - # links creation - node_tree.links.new(mask_tex_n.inputs['Vector'], sec_geom_n.outputs["UV"]) + node_tree.links.new(out_add_lampmask_n.inputs[0], lampmask_mixer_n.outputs['Lampmask Addition Color']) + node_tree.links.new(out_add_lampmask_n.inputs[1], compose_lighting_n.outputs['Color']) - node_tree.links.new(lampmask_mixr_gn.inputs["Lampmask Tex Alpha"], mask_tex_n.outputs["Value"]) - node_tree.links.new(lampmask_mixr_gn.inputs["Lampmask Tex Color"], mask_tex_n.outputs["Color"]) - node_tree.links.new(lampmask_mixr_gn.inputs["UV Vector"], sec_geom_n.outputs["UV"]) + node_tree.links.new(out_a_inv_n.inputs[1], compose_lighting_n.outputs['Alpha']) - node_tree.links.new(out_add_lampmask_n.inputs["Color1"], lampmask_mixr_gn.outputs["Lampmask Addition Color"]) - node_tree.links.new(out_add_lampmask_n.inputs["Color2"], compose_lighting_n.outputs["Composed Color"]) + node_tree.links.new(out_mat_n.inputs['Emissive Color'], out_add_lampmask_n.outputs[0]) + node_tree.links.new(out_mat_n.inputs['Transparency'], out_a_inv_n.outputs['Value']) - node_tree.links.new(output_n.inputs["Color"], out_add_lampmask_n.outputs["Color"]) + node_tree.links.new(output_n.inputs["Surface"], out_mat_n.outputs['BSDF']) @staticmethod - def set_mask_texture(node_tree, texture): + def set_mask_texture(node_tree, image): """Set mask texture to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assigned to mask texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assigned to mask texture node + :type image: bpy.types.Image + """ + + node_tree.nodes[Lamp.MASK_TEX_NODE].image = image + + @staticmethod + def set_mask_texture_settings(node_tree, settings): + """Set mask texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str """ + _material_utils.set_texture_settings_to_node(node_tree.nodes[Lamp.MASK_TEX_NODE], settings) - node_tree.nodes[Lamp.MASK_TEX_NODE].texture = texture + # due the fact uvs get clamped in vertex shader, we have to manually switch repeat on, for effect to work correctly + node_tree.nodes[Lamp.MASK_TEX_NODE].extension = "REPEAT" @staticmethod def set_mask_uv(node_tree, uv_layer): @@ -131,4 +162,4 @@ def set_mask_uv(node_tree, uv_layer): if uv_layer is None or uv_layer == "": uv_layer = _MESH_consts.none_uv - node_tree.nodes[Lamp.SEC_GEOM_NODE].uv_layer = uv_layer + node_tree.nodes[Lamp.SEC_UVMAP_NODE].uv_map = uv_layer diff --git a/addon/io_scs_tools/internals/shaders/eut2/lamp/add_env.py b/addon/io_scs_tools/internals/shaders/eut2/lamp/add_env.py index a2b3a3e..67e4f13 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/lamp/add_env.py +++ b/addon/io_scs_tools/internals/shaders/eut2/lamp/add_env.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.internals.shaders.eut2.lamp import Lamp from io_scs_tools.internals.shaders.eut2.dif_spec_add_env import DifSpecAddEnv @@ -39,10 +39,3 @@ def init(node_tree): # init parents DifSpecAddEnv.init(node_tree) Lamp.init(node_tree, init_dif_spec=False, start_pos_x=0, start_pos_y=500) - - out_add_lampmask_n = node_tree.nodes[Lamp.OUT_ADD_LAMPMASK_NODE] - out_add_lampmask_n.location.y -= 300 - compose_lighting_n = node_tree.nodes[DifSpecAddEnv.COMPOSE_LIGHTING_NODE] - - # links fixing - node_tree.links.new(out_add_lampmask_n.inputs["Color2"], compose_lighting_n.outputs["Composed Color"]) diff --git a/addon/io_scs_tools/internals/shaders/eut2/light_tex/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/light_tex/__init__.py index 87ff69a..e92045a 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/light_tex/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/light_tex/__init__.py @@ -16,8 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.internals.shaders.eut2.dif import Dif @@ -49,18 +48,20 @@ def init(node_tree): # delete existing node_tree.nodes.remove(node_tree.nodes[Dif.OPACITY_NODE]) + node_tree.nodes.remove(node_tree.nodes[Dif.LIGHTING_EVAL_NODE]) base_tex_n = node_tree.nodes[Dif.BASE_TEX_NODE] v_col_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] + compose_lighting_n.inputs['Diffuse Lighting'].default_value = (1.0,) * 4 + compose_lighting_n.inputs['Specular Lighting'].default_value = (1.0,) * 4 # node creation - spec_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") + spec_mult_n = node_tree.nodes.new("ShaderNodeVectorMath") spec_mult_n.name = LightTex.SPEC_MULT_NODE spec_mult_n.label = LightTex.SPEC_MULT_NODE spec_mult_n.location = (start_pos_x + pos_x_shift * 5, start_pos_y + 1850) - spec_mult_n.blend_type = "MULTIPLY" - spec_mult_n.inputs['Fac'].default_value = 1 + spec_mult_n.operation = "MULTIPLY" rgb_to_bw_n = node_tree.nodes.new("ShaderNodeRGBToBW") rgb_to_bw_n.name = LightTex.RGB_TO_BW_ALPHA_NODE @@ -68,26 +69,37 @@ def init(node_tree): rgb_to_bw_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1300) # links creation - node_tree.links.new(spec_mult_n.inputs["Color1"], v_col_mult_n.outputs["Color"]) - node_tree.links.new(out_mat_n.inputs["Spec"], spec_mult_n.outputs["Color"]) + node_tree.links.new(spec_mult_n.inputs[0], v_col_mult_n.outputs[0]) + node_tree.links.new(compose_lighting_n.inputs["Specular Color"], spec_mult_n.outputs[0]) node_tree.links.new(rgb_to_bw_n.inputs["Color"], base_tex_n.outputs["Color"]) - node_tree.links.new(out_mat_n.inputs["Alpha"], rgb_to_bw_n.outputs["Val"]) + node_tree.links.new(compose_lighting_n.inputs["Alpha"], rgb_to_bw_n.outputs["Val"]) @staticmethod - def set_material(node_tree, material): - """Set output material for this shader. - NOTE: extended to ensure transparency for this shader, which gives - partial look of multiplying with underlying surfaces. + def finalize(node_tree, material): + """Finalize node tree and material settings. Should be called as last. - :param node_tree: node tree of current shader + :param node_tree: node tree on which this shader should be finalized :type node_tree: bpy.types.NodeTree - :param material: blender material for used in this tree node as output + :param material: material used for this shader :type material: bpy.types.Material """ - Dif.set_material(node_tree, material) + Dif.finalize(node_tree, material) + + # in game it gets added to framebuffer, however we don't have access to frame buffer thus make approximation with alpha blending + material.blend_method = "BLEND" + + @staticmethod + def set_shininess(node_tree, factor): + """Set shininess factor to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param factor: shininess factor + :type factor: float + """ - material.use_transparency = True + pass # NOTE: shininess in this case is envmap strength multiplier, but as we are faking with blend over, shininess is useless @staticmethod def set_alpha_test_flavor(node_tree, switch_on): @@ -125,6 +137,18 @@ def set_blend_add_flavor(node_tree, switch_on): pass # NOTE: no support for this flavor; overriding with empty function + @staticmethod + def set_blend_mult_flavor(node_tree, switch_on): + """Set blend mult 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 mult should be switched on or off + :type switch_on: bool + """ + + pass # NOTE: no support for this flavor; overriding with empty function + @staticmethod def set_aux0(node_tree, aux_property): """Set depth bias. diff --git a/addon/io_scs_tools/internals/shaders/eut2/lightmap/night.py b/addon/io_scs_tools/internals/shaders/eut2/lightmap/night.py index d96dc00..d6d904a 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/lightmap/night.py +++ b/addon/io_scs_tools/internals/shaders/eut2/lightmap/night.py @@ -18,7 +18,6 @@ # Copyright (C) 2015: SCS Software - from io_scs_tools.internals.shaders.eut2.light_tex import LightTex diff --git a/addon/io_scs_tools/internals/shaders/eut2/mlaaweight/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/mlaaweight/__init__.py index 6d79258..95b001e 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/mlaaweight/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/mlaaweight/__init__.py @@ -16,11 +16,12 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software +from io_scs_tools.internals.shaders.eut2.none import NNone -class MlaaWeight: - OUTPUT_NODE = "Output" + +class MlaaWeight(NNone): @staticmethod def get_name(): @@ -35,26 +36,9 @@ def init(node_tree): :type node_tree: bpy.types.NodeTree """ - start_pos_x = 0 - start_pos_y = 0 - - # node creation - output_n = node_tree.nodes.new("ShaderNodeOutput") - output_n.name = MlaaWeight.OUTPUT_NODE - output_n.label = MlaaWeight.OUTPUT_NODE - output_n.location = (start_pos_x, start_pos_y) - - @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 - """ - - material.use_transparency = True - material.transparency_method = "MASK" + # init parent + NNone.init(node_tree) - node_tree.nodes[MlaaWeight.OUTPUT_NODE].inputs['Alpha'].default_value = 0 + # change emissive color + shader_n = node_tree.nodes[NNone.SHADER_NODE] + shader_n.inputs['Emissive Color'].default_value = (0.0,) * 3 + (1.0,) diff --git a/addon/io_scs_tools/internals/shaders/eut2/none/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/none/__init__.py index 1b5c6eb..abfaba4 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/none/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/none/__init__.py @@ -16,10 +16,16 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software +from io_scs_tools.internals.shaders.base import BaseShader +from io_scs_tools.utils import convert as _convert_utils -class NNone: + +class NNone(BaseShader): + WIREFRAME_NODE = "Wire" + INVERT_NODE = "Invert" + SHADER_NODE = "Shader" OUTPUT_NODE = "Output" @staticmethod @@ -35,21 +41,44 @@ def init(node_tree): :type node_tree: bpy.types.NodeTree """ + pos_x_shift = 185 + # node creation - output_n = node_tree.nodes.new("ShaderNodeOutput") + wireframe_n = node_tree.nodes.new("ShaderNodeWireframe") + wireframe_n.name = wireframe_n.label = NNone.WIREFRAME_NODE + wireframe_n.use_pixel_size = True + wireframe_n.inputs['Size'].default_value = 2 + + invert_n = node_tree.nodes.new("ShaderNodeInvert") + invert_n.name = invert_n.label = NNone.INVERT_NODE + invert_n.location = (wireframe_n.location.x + pos_x_shift, wireframe_n.location.y) + invert_n.inputs['Fac'].default_value = 1 + + shader_n = node_tree.nodes.new("ShaderNodeEeveeSpecular") + shader_n.name = shader_n.label = NNone.SHADER_NODE + shader_n.location = (invert_n.location.x + pos_x_shift, invert_n.location.y) + shader_n.inputs['Base Color'].default_value = (0,) * 4 + shader_n.inputs['Specular'].default_value = (0,) * 4 + shader_n.inputs['Emissive Color'].default_value = _convert_utils.to_node_color((0.3,) * 3 + (1.0,)) + + output_n = node_tree.nodes.new("ShaderNodeOutputMaterial") output_n.name = output_n.label = NNone.OUTPUT_NODE - output_n.inputs['Color'].default_value = (0, 0, 0, 1) - output_n.inputs['Alpha'].default_value = 0.0 + output_n.location = (shader_n.location.x + pos_x_shift, shader_n.location.y) + + # links creation + node_tree.links.new(invert_n.inputs['Color'], wireframe_n.outputs['Fac']) + node_tree.links.new(shader_n.inputs['Transparency'], invert_n.outputs['Color']) + node_tree.links.new(output_n.inputs['Surface'], shader_n.outputs['BSDF']) @staticmethod - def set_material(node_tree, material): - """Set output material for this shader. + def finalize(node_tree, material): + """Finalize node tree and material settings. Should be called as last. - :param node_tree: node tree of current shader + :param node_tree: node tree on which this shader should be finalized :type node_tree: bpy.types.NodeTree - :param material: blender material for used in this tree node as output + :param material: material used for this shader :type material: bpy.types.Material """ - # only make sure that transparency is enabled on that material - material.use_transparency = True + # make sure that alpha clip is enabled on that material + material.blend_method = "CLIP" diff --git a/addon/io_scs_tools/internals/shaders/eut2/reflective/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/reflective/__init__.py index 5f3134a..7474f0d 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/reflective/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/reflective/__init__.py @@ -16,20 +16,26 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.consts import Mesh as _MESH_consts -from io_scs_tools.internals.shaders.eut2.std_node_groups import vcolor_input +from io_scs_tools.internals.shaders.base import BaseShader +from io_scs_tools.internals.shaders.eut2.std_node_groups import compose_lighting_ng +from io_scs_tools.internals.shaders.eut2.std_node_groups import lighting_evaluator_ng +from io_scs_tools.internals.shaders.eut2.std_node_groups import vcolor_input_ng +from io_scs_tools.utils import material as _material_utils -class Reflective: +class Reflective(BaseShader): GEOM_NODE = "Geometry" + UV_MAP_NODE = "UVMap" BASE_TEX_NODE = "BaseTex" VCOL_GROUP_NODE = "VColorGroup" OPACITY_NODE = "OpacityMultiplier" VCOLOR_MULT_NODE = "VertexColorMultiplier" VCOLOR_SCALE_NODE = "VertexColorScale" - OUT_MAT_NODE = "InputMaterial" + LIGHTING_EVAL_NODE = "LightingEvaluator" + COMPOSE_LIGHTING_NODE = "ComposeLighting" OUTPUT_NODE = "Output" @staticmethod @@ -52,104 +58,109 @@ def init(node_tree): # node creation vcol_group_n = node_tree.nodes.new("ShaderNodeGroup") - vcol_group_n.name = Reflective.VCOL_GROUP_NODE - vcol_group_n.label = Reflective.VCOL_GROUP_NODE + vcol_group_n.name = vcol_group_n.label = Reflective.VCOL_GROUP_NODE vcol_group_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1650) - vcol_group_n.node_tree = vcolor_input.get_node_group() + vcol_group_n.node_tree = vcolor_input_ng.get_node_group() - geometry_n = node_tree.nodes.new("ShaderNodeGeometry") - geometry_n.name = Reflective.GEOM_NODE - geometry_n.label = Reflective.GEOM_NODE - geometry_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1500) - geometry_n.uv_layer = _MESH_consts.none_uv + geom_n = node_tree.nodes.new("ShaderNodeNewGeometry") + geom_n.name = geom_n.label = Reflective.GEOM_NODE + geom_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1300) + + uv_map_n = node_tree.nodes.new("ShaderNodeUVMap") + uv_map_n.name = uv_map_n.label = Reflective.UV_MAP_NODE + uv_map_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1500) + uv_map_n.uv_map = _MESH_consts.none_uv opacity_n = node_tree.nodes.new("ShaderNodeMath") - opacity_n.name = Reflective.OPACITY_NODE - opacity_n.label = Reflective.OPACITY_NODE + opacity_n.name = opacity_n.label = Reflective.OPACITY_NODE opacity_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1300) opacity_n.operation = "MULTIPLY" - vcol_scale_n = node_tree.nodes.new("ShaderNodeMixRGB") - vcol_scale_n.name = Reflective.VCOLOR_SCALE_NODE - vcol_scale_n.label = Reflective.VCOLOR_SCALE_NODE + vcol_scale_n = node_tree.nodes.new("ShaderNodeVectorMath") + vcol_scale_n.name = vcol_scale_n.label = Reflective.VCOLOR_SCALE_NODE vcol_scale_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1750) - vcol_scale_n.blend_type = "MULTIPLY" - vcol_scale_n.inputs['Fac'].default_value = 1 - vcol_scale_n.inputs['Color2'].default_value = (2,) * 4 + vcol_scale_n.operation = "MULTIPLY" + vcol_scale_n.inputs[1].default_value = (2,) * 3 - base_tex_n = node_tree.nodes.new("ShaderNodeTexture") - base_tex_n.name = Reflective.BASE_TEX_NODE - base_tex_n.label = Reflective.BASE_TEX_NODE + base_tex_n = node_tree.nodes.new("ShaderNodeTexImage") + base_tex_n.name = base_tex_n.label = Reflective.BASE_TEX_NODE base_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1500) + base_tex_n.width = 140 - vcol_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") - vcol_mult_n.name = Reflective.VCOLOR_MULT_NODE - vcol_mult_n.label = Reflective.VCOLOR_MULT_NODE + vcol_mult_n = node_tree.nodes.new("ShaderNodeVectorMath") + vcol_mult_n.name = vcol_mult_n.label = Reflective.VCOLOR_MULT_NODE vcol_mult_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 1700) - vcol_mult_n.blend_type = "MULTIPLY" - vcol_mult_n.inputs['Fac'].default_value = 1 - - out_mat_n = node_tree.nodes.new("ShaderNodeExtendedMaterial") - out_mat_n.name = Reflective.OUT_MAT_NODE - out_mat_n.label = Reflective.OUT_MAT_NODE - if "SpecTra" in out_mat_n: - out_mat_n.inputs['SpecTra'].default_value = 0.0 - if "Refl" in out_mat_n: - out_mat_n.inputs['Refl'].default_value = 1.0 - elif "Reflectivity" in out_mat_n: - out_mat_n.inputs['Reflectivity'].default_value = 1.0 - out_mat_n.location = (start_pos_x + pos_x_shift * 7, start_pos_y + 1800) - - output_n = node_tree.nodes.new("ShaderNodeOutput") - output_n.name = Reflective.OUTPUT_NODE - output_n.label = Reflective.OUTPUT_NODE - output_n.location = (start_pos_x + + pos_x_shift * 8, start_pos_y + 1800) + vcol_mult_n.operation = "MULTIPLY" + + lighting_eval_n = node_tree.nodes.new("ShaderNodeGroup") + lighting_eval_n.name = lighting_eval_n.label = Reflective.LIGHTING_EVAL_NODE + lighting_eval_n.location = (start_pos_x + pos_x_shift * 7, start_pos_y + 1800) + lighting_eval_n.node_tree = lighting_evaluator_ng.get_node_group() + + compose_lighting_n = node_tree.nodes.new("ShaderNodeGroup") + compose_lighting_n.name = compose_lighting_n.label = Reflective.COMPOSE_LIGHTING_NODE + compose_lighting_n.location = (start_pos_x + pos_x_shift * 8, start_pos_y + 1800) + compose_lighting_n.node_tree = compose_lighting_ng.get_node_group() + compose_lighting_n.inputs['Specular Lighting'].default_value = (0.0,) * 4 + compose_lighting_n.inputs['Specular Color'].default_value = (0.0,) * 4 + + output_n = node_tree.nodes.new("ShaderNodeOutputMaterial") + output_n.name = output_n.label = Reflective.OUTPUT_NODE + output_n.location = (start_pos_x + + pos_x_shift * 9, start_pos_y + 1800) # links creation - node_tree.links.new(base_tex_n.inputs['Vector'], geometry_n.outputs['UV']) - node_tree.links.new(vcol_scale_n.inputs['Color1'], vcol_group_n.outputs['Vertex Color']) + node_tree.links.new(base_tex_n.inputs['Vector'], uv_map_n.outputs['UV']) + node_tree.links.new(vcol_scale_n.inputs[0], vcol_group_n.outputs['Vertex Color']) - node_tree.links.new(vcol_mult_n.inputs['Color1'], vcol_scale_n.outputs['Color']) - node_tree.links.new(vcol_mult_n.inputs['Color2'], base_tex_n.outputs['Color']) - node_tree.links.new(opacity_n.inputs[0], base_tex_n.outputs["Value"]) + node_tree.links.new(vcol_mult_n.inputs[0], vcol_scale_n.outputs[0]) + node_tree.links.new(vcol_mult_n.inputs[1], base_tex_n.outputs['Color']) + node_tree.links.new(opacity_n.inputs[0], base_tex_n.outputs["Alpha"]) node_tree.links.new(opacity_n.inputs[1], vcol_group_n.outputs["Vertex Color Alpha"]) - node_tree.links.new(out_mat_n.inputs['Color'], vcol_mult_n.outputs['Color']) - node_tree.links.new(out_mat_n.inputs['Alpha'], opacity_n.outputs['Value']) + node_tree.links.new(lighting_eval_n.inputs['Normal Vector'], geom_n.outputs['Normal']) + node_tree.links.new(lighting_eval_n.inputs['Incoming Vector'], geom_n.outputs['Incoming']) - node_tree.links.new(output_n.inputs['Color'], out_mat_n.outputs['Color']) - node_tree.links.new(output_n.inputs['Alpha'], out_mat_n.outputs['Alpha']) + node_tree.links.new(compose_lighting_n.inputs['Diffuse Lighting'], lighting_eval_n.outputs['Diffuse Lighting']) + node_tree.links.new(compose_lighting_n.inputs['Diffuse Color'], vcol_mult_n.outputs[0]) + node_tree.links.new(compose_lighting_n.inputs['Alpha'], opacity_n.outputs['Value']) + + node_tree.links.new(output_n.inputs['Surface'], compose_lighting_n.outputs['Shader']) @staticmethod - def set_material(node_tree, material): - """Set output material for this shader. + def finalize(node_tree, material): + """Finalize node tree and material settings. Should be called as last. - :param node_tree: node tree of current shader + :param node_tree: node tree on which this shader should be finalized :type node_tree: bpy.types.NodeTree - :param material: blender material for used in this tree node as output + :param material: material used for this shader :type material: bpy.types.Material """ - node_tree.nodes[Reflective.OUT_MAT_NODE].material = material - node_tree.nodes[Reflective.OUT_MAT_NODE].use_specular = False - material.use_transparency = True - material.transparency_method = "MASK" - - # make sure diffuse intensity and emit factors are properly set on material - material.diffuse_intensity = 0.7 - material.emit = 0 + material.use_backface_culling = True + material.blend_method = "BLEND" @staticmethod - def set_base_texture(node_tree, texture): + def set_base_texture(node_tree, image): """Set base texture to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assignet to base texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assignet to base texture node + :type image: bpy.types.Image """ - node_tree.nodes[Reflective.BASE_TEX_NODE].texture = texture + node_tree.nodes[Reflective.BASE_TEX_NODE].image = image + + @staticmethod + def set_base_texture_settings(node_tree, settings): + """Set base texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + _material_utils.set_texture_settings_to_node(node_tree.nodes[Reflective.BASE_TEX_NODE], settings) @staticmethod def set_base_uv(node_tree, uv_layer): @@ -164,4 +175,4 @@ def set_base_uv(node_tree, uv_layer): if uv_layer is None or uv_layer == "": uv_layer = _MESH_consts.none_uv - node_tree.nodes[Reflective.GEOM_NODE].uv_layer = uv_layer + node_tree.nodes[Reflective.UV_MAP_NODE].uv_map = uv_layer diff --git a/addon/io_scs_tools/internals/shaders/eut2/retroreflective/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/retroreflective/__init__.py index 343f843..8b1715e 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/retroreflective/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/retroreflective/__init__.py @@ -18,7 +18,6 @@ # 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 @@ -50,9 +49,12 @@ def 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] + lighting_eval_n = node_tree.nodes[Dif.LIGHTING_EVAL_NODE] compose_lighting_n = node_tree.nodes[Dif.COMPOSE_LIGHTING_NODE] + # modify exisiting + lighting_eval_n.inputs['Shininess'].default_value = 60 + # delete existing node_tree.nodes.remove(node_tree.nodes[Dif.SPEC_COL_NODE]) node_tree.nodes.remove(node_tree.nodes[Dif.DIFF_COL_NODE]) @@ -60,8 +62,7 @@ def init(node_tree): # 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.name = 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 @@ -69,25 +70,8 @@ def init(node_tree): # 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) + node_tree.links.new(compose_lighting_n.inputs['Diffuse Color'], vcol_mult_n.outputs[0]) + node_tree.links.new(compose_lighting_n.inputs['Specular Color'], spec_mult_n.outputs['Value']) @staticmethod def set_retroreflective_decal_flavor(node_tree, switch_on): @@ -100,11 +84,7 @@ def set_retroreflective_decal_flavor(node_tree, switch_on): :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']) + blend_over.init(node_tree) else: blend_over.delete(node_tree) diff --git a/addon/io_scs_tools/internals/shaders/eut2/shadowmap/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/shadowmap/__init__.py index bbd7fbb..053413a 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/shadowmap/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/shadowmap/__init__.py @@ -16,8 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.internals.shaders.eut2.unlit_tex import UnlitTex @@ -41,18 +40,3 @@ def init(node_tree): # enable hardcoded flavors: DEPTH, BLEND_OVER UnlitTex.set_blend_over_flavor(node_tree, True) - - @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 - """ - - UnlitTex.set_material(node_tree, material) - - material.use_transparency = True - material.transparency_method = "MASK" diff --git a/addon/io_scs_tools/internals/shaders/eut2/shadowonly/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/shadowonly/__init__.py index 2aa4027..3a1799a 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/shadowonly/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/shadowonly/__init__.py @@ -16,10 +16,13 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software +from io_scs_tools.internals.shaders.base import BaseShader -class Shadowonly: + +class Shadowonly(BaseShader): + COL_NODE = "Color" OUTPUT_NODE = "Output" @staticmethod @@ -38,24 +41,33 @@ def init(node_tree): start_pos_x = 0 start_pos_y = 0 + pos_x_shift = 185 + # node creation - output_n = node_tree.nodes.new("ShaderNodeOutput") - output_n.name = Shadowonly.OUTPUT_NODE - output_n.label = Shadowonly.OUTPUT_NODE - output_n.location = (start_pos_x, start_pos_y) - output_n.inputs['Color'].default_value = (0.01, 0, 0.01, 1) + col_n = node_tree.nodes.new("ShaderNodeRGB") + col_n.name = col_n.label = Shadowonly.COL_NODE + col_n.location = (start_pos_x, start_pos_y) + col_n.outputs['Color'].default_value = (0.01, 0, 0.01, 1.0) + + output_n = node_tree.nodes.new("ShaderNodeOutputMaterial") + output_n.name = output_n.label = Shadowonly.OUTPUT_NODE + output_n.location = (start_pos_x + pos_x_shift, start_pos_y) + + # links creation + node_tree.links.new(output_n.inputs['Surface'], col_n.outputs['Color']) @staticmethod - def set_material(node_tree, material): - """Set output material for this shader. + def finalize(node_tree, material): + """Finalize node tree and material settings. Should be called as last. - :param node_tree: node tree of current shader + :param node_tree: node tree on which this shader should be finalized :type node_tree: bpy.types.NodeTree - :param material: blender material for used in this tree node as output + :param material: material used for this shader :type material: bpy.types.Material """ - pass # NOTE: shadow casters are not rendered in game so they don't need material + material.use_backface_culling = True + material.blend_method = "OPAQUE" @staticmethod def set_shadow_bias(node_tree, value): diff --git a/addon/io_scs_tools/internals/shaders/eut2/sign/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/sign/__init__.py index 34a5b69..7594088 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/sign/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/sign/__init__.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.internals.shaders.eut2.dif import Dif @@ -42,17 +42,3 @@ def init(node_tree): # enable hardcoded flavors Dif.set_blend_over_flavor(node_tree, True) - - @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 - """ - - Dif.set_material(node_tree, material) - - material.use_transparency = True diff --git a/addon/io_scs_tools/internals/shaders/eut2/sky/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/sky/__init__.py index 1e22f59..28b1e86 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/sky/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/sky/__init__.py @@ -16,26 +16,26 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from mathutils import Color from io_scs_tools.consts import Mesh as _MESH_consts +from io_scs_tools.internals.shaders.base import BaseShader from io_scs_tools.internals.shaders.flavors import blend_over -from io_scs_tools.internals.shaders.eut2.std_node_groups import vcolor_input +from io_scs_tools.internals.shaders.eut2.std_node_groups import vcolor_input_ng from io_scs_tools.utils import convert as _convert_utils +from io_scs_tools.utils import material as _material_utils -class Sky: +class Sky(BaseShader): VCOL_GROUP_NODE = "VColorGroup" - GEOM_NODE = "Geometry" - SEC_GEOM_NODE = "SecGeometry" + UV_MAP_NODE = "UVMap" + SEC_UV_MAP_NODE = "SecUVMap" DIFF_COL_NODE = "DiffuseColor" BASE_TEX_NODE = "BaseTex" OVER_TEX_NODE = "OverTex" MASK_TEX_NODE = "MaskTex" BLEND_VAL_NODE = "BlendInput" - VCOLOR_SCALE_NODE = "VertexColorScale" MASK_TEX_SEP_NODE = "SeparateMask" MASK_FACTOR_MIX_NODE = "MaskFactorMix" MASK_FACTOR_BLEND_MULT_NODE = "MaskFactorBlendMultiplier" @@ -43,6 +43,8 @@ class Sky: BASE_OVER_A_MIX_NODE = "BaseOverAlphaMix" VCOLOR_MULT_NODE = "VertexColorMultiplier" DIFF_MULT_NODE = "DiffuseMultiplier" + ALPHA_INV_NODE = "AlphaInv" + OUT_SHADER_NODE = "OutShader" OUTPUT_NODE = "Output" @staticmethod @@ -67,45 +69,41 @@ def init(node_tree): vcol_group_n = node_tree.nodes.new("ShaderNodeGroup") vcol_group_n.name = vcol_group_n.label = Sky.VCOL_GROUP_NODE vcol_group_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1650) - vcol_group_n.node_tree = vcolor_input.get_node_group() + vcol_group_n.node_tree = vcolor_input_ng.get_node_group() - geometry_n = node_tree.nodes.new("ShaderNodeGeometry") - geometry_n.name = geometry_n.label = Sky.GEOM_NODE - geometry_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1500) - geometry_n.uv_layer = _MESH_consts.none_uv + uv_map_n = node_tree.nodes.new("ShaderNodeUVMap") + uv_map_n.name = uv_map_n.label = Sky.UV_MAP_NODE + uv_map_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1500) + uv_map_n.uv_map = _MESH_consts.none_uv - sec_geom_n = node_tree.nodes.new("ShaderNodeGeometry") - sec_geom_n.name = sec_geom_n.label = Sky.SEC_GEOM_NODE - sec_geom_n.location = (start_pos_x - pos_x_shift, start_pos_y + 900) - sec_geom_n.uv_layer = _MESH_consts.none_uv + sec_uv_map_n = node_tree.nodes.new("ShaderNodeUVMap") + sec_uv_map_n.name = sec_uv_map_n.label = Sky.SEC_UV_MAP_NODE + sec_uv_map_n.location = (start_pos_x - pos_x_shift, start_pos_y + 900) + sec_uv_map_n.uv_map = _MESH_consts.none_uv diff_col_n = node_tree.nodes.new("ShaderNodeRGB") diff_col_n.name = diff_col_n.label = Sky.DIFF_COL_NODE diff_col_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1700) - base_tex_n = node_tree.nodes.new("ShaderNodeTexture") + base_tex_n = node_tree.nodes.new("ShaderNodeTexImage") base_tex_n.name = base_tex_n.label = Sky.BASE_TEX_NODE base_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1500) + base_tex_n.width = 140 - over_tex_n = node_tree.nodes.new("ShaderNodeTexture") + over_tex_n = node_tree.nodes.new("ShaderNodeTexImage") over_tex_n.name = over_tex_n.label = Sky.OVER_TEX_NODE over_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1200) + over_tex_n.width = 140 - mask_tex_n = node_tree.nodes.new("ShaderNodeTexture") + mask_tex_n = node_tree.nodes.new("ShaderNodeTexImage") mask_tex_n.name = mask_tex_n.label = Sky.MASK_TEX_NODE mask_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 900) + mask_tex_n.width = 140 blend_input_n = node_tree.nodes.new("ShaderNodeValue") blend_input_n.name = blend_input_n.label = Sky.BLEND_VAL_NODE blend_input_n.location = (start_pos_x + pos_x_shift, start_pos_y + 600) - vcol_scale_n = node_tree.nodes.new("ShaderNodeMixRGB") - vcol_scale_n.name = vcol_scale_n.label = Sky.VCOLOR_SCALE_NODE - vcol_scale_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1600) - vcol_scale_n.blend_type = "MULTIPLY" - vcol_scale_n.inputs['Fac'].default_value = 1 - vcol_scale_n.inputs['Color2'].default_value = (2,) * 4 - mask_tex_sep_n = node_tree.nodes.new("ShaderNodeSeparateRGB") mask_tex_sep_n.name = mask_tex_sep_n.label = Sky.MASK_TEX_SEP_NODE mask_tex_sep_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 900) @@ -134,33 +132,41 @@ def init(node_tree): base_over_a_mix_n.location = (start_pos_x + pos_x_shift * 6, start_pos_y + 1200) base_over_a_mix_n.blend_type = "MIX" - vcol_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") - vcol_mult_n.name = Sky.VCOLOR_MULT_NODE - vcol_mult_n.label = Sky.VCOLOR_MULT_NODE + vcol_mult_n = node_tree.nodes.new("ShaderNodeVectorMath") + vcol_mult_n.name = vcol_mult_n.label = Sky.VCOLOR_MULT_NODE vcol_mult_n.location = (start_pos_x + pos_x_shift * 7, start_pos_y + 1500) - vcol_mult_n.blend_type = "MULTIPLY" - vcol_mult_n.inputs['Fac'].default_value = 1 + vcol_mult_n.operation = "MULTIPLY" - diff_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") - diff_mult_n.name = Sky.DIFF_MULT_NODE - diff_mult_n.label = Sky.DIFF_MULT_NODE + diff_mult_n = node_tree.nodes.new("ShaderNodeVectorMath") + diff_mult_n.name = diff_mult_n.label = Sky.DIFF_MULT_NODE diff_mult_n.location = (start_pos_x + pos_x_shift * 8, start_pos_y + 1650) - diff_mult_n.blend_type = "MULTIPLY" - diff_mult_n.inputs['Fac'].default_value = 1 - diff_mult_n.inputs['Color2'].default_value = (0, 0, 0, 1) - - output_n = node_tree.nodes.new("ShaderNodeOutput") - output_n.name = Sky.OUTPUT_NODE - output_n.label = Sky.OUTPUT_NODE - output_n.location = (start_pos_x + + pos_x_shift * 10, start_pos_y + 1500) + diff_mult_n.operation = "MULTIPLY" + diff_mult_n.inputs[1].default_value = (0, 0, 0) + + alpha_inv_n = node_tree.nodes.new("ShaderNodeMath") + alpha_inv_n.name = alpha_inv_n.label = Sky.ALPHA_INV_NODE + alpha_inv_n.location = (start_pos_x + pos_x_shift * 9, 1300) + alpha_inv_n.operation = "SUBTRACT" + alpha_inv_n.inputs[0].default_value = 0.999999 # TODO: change back to 1.0 after bug is fixed: https://developer.blender.org/T71426 + alpha_inv_n.inputs[1].default_value = 1.0 + alpha_inv_n.use_clamp = True + + out_shader_node = node_tree.nodes.new("ShaderNodeEeveeSpecular") + out_shader_node.name = out_shader_node.label = Sky.OUT_SHADER_NODE + out_shader_node.location = (start_pos_x + pos_x_shift * 10, 1500) + out_shader_node.inputs["Base Color"].default_value = (0.0,) * 4 + out_shader_node.inputs["Specular"].default_value = (0.0,) * 4 + + output_n = node_tree.nodes.new("ShaderNodeOutputMaterial") + output_n.name = output_n.label = Sky.OUTPUT_NODE + output_n.location = (start_pos_x + + pos_x_shift * 11, start_pos_y + 1500) # links creation - node_tree.links.new(base_tex_n.inputs['Vector'], geometry_n.outputs['UV']) - node_tree.links.new(over_tex_n.inputs['Vector'], geometry_n.outputs['UV']) - node_tree.links.new(mask_tex_n.inputs['Vector'], sec_geom_n.outputs['UV']) + node_tree.links.new(base_tex_n.inputs['Vector'], uv_map_n.outputs['UV']) + node_tree.links.new(over_tex_n.inputs['Vector'], uv_map_n.outputs['UV']) + node_tree.links.new(mask_tex_n.inputs['Vector'], sec_uv_map_n.outputs['UV']) # pass 1 - node_tree.links.new(vcol_scale_n.inputs['Color1'], vcol_group_n.outputs['Vertex Color']) node_tree.links.new(mask_tex_sep_n.inputs['Image'], mask_tex_n.outputs['Color']) # pass 2 @@ -176,31 +182,41 @@ def init(node_tree): node_tree.links.new(base_over_mix_n.inputs['Color2'], over_tex_n.outputs['Color']) node_tree.links.new(base_over_a_mix_n.inputs['Fac'], mask_factor_blend_mult_n.outputs['Color']) - node_tree.links.new(base_over_a_mix_n.inputs['Color1'], base_tex_n.outputs['Value']) - node_tree.links.new(base_over_a_mix_n.inputs['Color2'], over_tex_n.outputs['Value']) + node_tree.links.new(base_over_a_mix_n.inputs['Color1'], base_tex_n.outputs['Alpha']) + node_tree.links.new(base_over_a_mix_n.inputs['Color2'], over_tex_n.outputs['Alpha']) # pass 5 - node_tree.links.new(vcol_mult_n.inputs['Color1'], vcol_scale_n.outputs['Color']) - node_tree.links.new(vcol_mult_n.inputs['Color2'], base_over_mix_n.outputs['Color']) + node_tree.links.new(vcol_mult_n.inputs[0], vcol_group_n.outputs['Vertex Color']) + node_tree.links.new(vcol_mult_n.inputs[1], base_over_mix_n.outputs['Color']) # pass 6 - node_tree.links.new(diff_mult_n.inputs['Color1'], diff_col_n.outputs['Color']) - node_tree.links.new(diff_mult_n.inputs['Color2'], vcol_mult_n.outputs['Color']) + node_tree.links.new(diff_mult_n.inputs[0], diff_col_n.outputs['Color']) + node_tree.links.new(diff_mult_n.inputs[1], vcol_mult_n.outputs[0]) + + # pass 7 + node_tree.links.new(out_shader_node.inputs['Emissive Color'], diff_mult_n.outputs[0]) + node_tree.links.new(out_shader_node.inputs['Transparency'], alpha_inv_n.outputs['Value']) + + node_tree.links.new(alpha_inv_n.inputs[1], base_over_a_mix_n.outputs['Color']) # output pass - node_tree.links.new(output_n.inputs['Color'], diff_mult_n.outputs['Color']) + node_tree.links.new(output_n.inputs['Surface'], out_shader_node.outputs['BSDF']) @staticmethod - def set_material(node_tree, material): - """Set output material for this shader. + def finalize(node_tree, material): + """Finalize node tree and material settings. Should be called as last. - :param node_tree: node tree of current shader + :param node_tree: node tree on which this shader should be finalized :type node_tree: bpy.types.NodeTree - :param material: blender material for used in this tree node as output + :param material: material used for this shader :type material: bpy.types.Material """ - pass # we don't use any material as no shading is applied + material.use_backface_culling = True + material.blend_method = "OPAQUE" + + if blend_over.is_set(node_tree): + material.blend_method = "BLEND" @staticmethod def set_diffuse(node_tree, color): @@ -217,16 +233,27 @@ def set_diffuse(node_tree, color): node_tree.nodes[Sky.DIFF_COL_NODE].outputs['Color'].default_value = color @staticmethod - def set_base_texture(node_tree, texture): + def set_base_texture(node_tree, image): """Set base texture to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assignet to base texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assignet to base texture node + :type image: bpy.types.Image """ - node_tree.nodes[Sky.BASE_TEX_NODE].texture = texture + node_tree.nodes[Sky.BASE_TEX_NODE].image = image + + @staticmethod + def set_base_texture_settings(node_tree, settings): + """Set base texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + _material_utils.set_texture_settings_to_node(node_tree.nodes[Sky.BASE_TEX_NODE], settings) @staticmethod def set_base_uv(node_tree, uv_layer): @@ -241,19 +268,30 @@ def set_base_uv(node_tree, uv_layer): if uv_layer is None or uv_layer == "": uv_layer = _MESH_consts.none_uv - node_tree.nodes[Sky.GEOM_NODE].uv_layer = uv_layer + node_tree.nodes[Sky.UV_MAP_NODE].uv_map = uv_layer @staticmethod - def set_over_texture(node_tree, texture): + def set_over_texture(node_tree, image): """Set over texture to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assigned to over texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assigned to over texture node + :type image: bpy.types.Image """ - node_tree.nodes[Sky.OVER_TEX_NODE].texture = texture + node_tree.nodes[Sky.OVER_TEX_NODE].image = image + + @staticmethod + def set_over_texture_settings(node_tree, settings): + """Set over texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + _material_utils.set_texture_settings_to_node(node_tree.nodes[Sky.OVER_TEX_NODE], settings) @staticmethod def set_over_uv(node_tree, uv_layer): @@ -268,16 +306,27 @@ def set_over_uv(node_tree, uv_layer): Sky.set_base_uv(node_tree, uv_layer) @staticmethod - def set_mask_texture(node_tree, texture): + def set_mask_texture(node_tree, image): """Set mask texture to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assignet to mask texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assignet to mask texture node + :type image: bpy.types.Image """ - node_tree.nodes[Sky.MASK_TEX_NODE].texture = texture + node_tree.nodes[Sky.MASK_TEX_NODE].image = image + + @staticmethod + def set_mask_texture_settings(node_tree, settings): + """Set mask texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + _material_utils.set_texture_settings_to_node(node_tree.nodes[Sky.MASK_TEX_NODE], settings) @staticmethod def set_mask_uv(node_tree, uv_layer): @@ -292,7 +341,7 @@ def set_mask_uv(node_tree, uv_layer): if uv_layer is None or uv_layer == "": uv_layer = _MESH_consts.none_uv - node_tree.nodes[Sky.SEC_GEOM_NODE].uv_layer = uv_layer + node_tree.nodes[Sky.SEC_UV_MAP_NODE].uv_map = uv_layer @staticmethod def set_aux0(node_tree, aux_property): @@ -316,10 +365,7 @@ def set_blend_over_flavor(node_tree, switch_on): :type switch_on: bool """ - out_node = node_tree.nodes[Sky.OUTPUT_NODE] - in_node = node_tree.nodes[Sky.BASE_OVER_A_MIX_NODE] - if switch_on: - blend_over.init(node_tree, in_node.outputs['Color'], out_node.inputs['Alpha']) + blend_over.init(node_tree) else: blend_over.delete(node_tree) diff --git a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/add_env.py b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/add_env.py deleted file mode 100644 index 6cb0f69..0000000 --- a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/add_env.py +++ /dev/null @@ -1,219 +0,0 @@ -# ##### 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) 2015: SCS Software - -import bpy -from io_scs_tools.consts import Material as _MAT_consts -from io_scs_tools.internals.shaders.eut2.std_node_groups import refl_normal -from io_scs_tools.internals.shaders.eut2.std_node_groups import fresnel - -ADD_ENV_G = _MAT_consts.node_group_prefix + "AddEnvGroup" - -COMBINE_BASE_ALPHA_NODE = "CombineBaseAlpha" -ENV_SPEC_MULT_NODE = "EnvSpecMultiplier" -REFL_TEX_MULT_NODE = "ReflectionTexMultiplier" -REFL_TEX_COL_MULT_NODE = "ReflTexColorMultiplier" -TEX_FRESNEL_MULT_NODE = "TextureFresnelMultiplier" -GLOBAL_ENV_FACTOR_NODE = "GlobalEnvFactor" -GLOBAL_ENV_MULT_NODE = "GlobalEnvMultiplier" -FRESNEL_GNODE = "FresnelGroup" -REFL_NORMAL_GNODE = "ReflNormalGroup" - - -def get_node_group(): - """Gets node group for calcualtion of environment addition color. - - :return: node group which calculates environment addition color - :rtype: bpy.types.NodeGroup - """ - - if __group_needs_recreation__(): - __create_node_group__() - - return bpy.data.node_groups[ADD_ENV_G] - - -def set_global_env_factor(value): - """Sets global environment factor multiplication to the node group. - - NOTE: We are using global factor as we can not determinate if texture is generated one or static one and - based on that decide how much of envirnoment should be applied to result. - So for closest result to game this factor should be "env_static_mod" variable from "sun_profile" multiplied - with "env" variable from "sun_profile". - This way static cubemaps will be used as in game, however even generated cubemaps well mostl work well, - becuase expected values of that multiplication is from 0 to 1.0. - - :param value: global enironment factor (should come from sun profile) - :type value: float - """ - - get_node_group().nodes[GLOBAL_ENV_FACTOR_NODE].outputs[0].default_value = value - - -def __group_needs_recreation__(): - """Tells if group needs recreation. - - :return: True group isn't up to date and has to be (re)created; False if group doesn't need to be (re)created - :rtype: bool - """ - # current checks: - # 1. group existence in blender data block - # 2. existence of GLOBAL_ENV_FACTOR_NODE which was added in BT version 1.5 - return ADD_ENV_G not in bpy.data.node_groups or GLOBAL_ENV_FACTOR_NODE not in bpy.data.node_groups[ADD_ENV_G].nodes - - -def __create_node_group__(): - """Creates add env group. - - Inputs: Fresnel Scale, Fresnel Bias, Normal Vector, View Vector, Apply Fresnel, - Reflection Texture Color, Base Texture Alpha, Env Factor Color and Specular Color - Outputs: Environment Addition Color - """ - - start_pos_x = 0 - start_pos_y = 0 - - pos_x_shift = 185 - - if ADD_ENV_G not in bpy.data.node_groups: # creation - - add_env_g = bpy.data.node_groups.new(type="ShaderNodeTree", name=ADD_ENV_G) - - # inputs defining - add_env_g.inputs.new("NodeSocketFloat", "Fresnel Scale") - add_env_g.inputs.new("NodeSocketFloat", "Fresnel Bias") - add_env_g.inputs.new("NodeSocketVector", "Normal Vector") - add_env_g.inputs.new("NodeSocketVector", "View Vector") - add_env_g.inputs.new("NodeSocketFloat", "Apply Fresnel") - add_env_g.inputs.new("NodeSocketColor", "Reflection Texture Color") - add_env_g.inputs.new("NodeSocketFloat", "Base Texture Alpha") - add_env_g.inputs.new("NodeSocketColor", "Env Factor Color") - add_env_g.inputs.new("NodeSocketColor", "Specular Color") - - # outputs defining - add_env_g.outputs.new("NodeSocketColor", "Environment Addition Color") - - else: # recreation - - add_env_g = bpy.data.node_groups[ADD_ENV_G] - - # delete all old nodes and links as they will be recreated now with actual version - add_env_g.nodes.clear() - - # node creation - input_n = add_env_g.nodes.new("NodeGroupInput") - input_n.location = (start_pos_x - pos_x_shift, start_pos_y) - - output_n = add_env_g.nodes.new("NodeGroupOutput") - output_n.location = (start_pos_x + pos_x_shift * 6, start_pos_y) - - refl_normal_gn = add_env_g.nodes.new("ShaderNodeGroup") - refl_normal_gn.name = REFL_NORMAL_GNODE - refl_normal_gn.label = REFL_NORMAL_GNODE - refl_normal_gn.location = (start_pos_x + pos_x_shift, start_pos_y) - refl_normal_gn.node_tree = refl_normal.get_node_group() - - combine_base_alpha_n = add_env_g.nodes.new("ShaderNodeCombineRGB") - combine_base_alpha_n.name = COMBINE_BASE_ALPHA_NODE - combine_base_alpha_n.label = COMBINE_BASE_ALPHA_NODE - combine_base_alpha_n.location = (start_pos_x + pos_x_shift, start_pos_y - 150) - - env_spec_mult_n = add_env_g.nodes.new("ShaderNodeMixRGB") - env_spec_mult_n.name = ENV_SPEC_MULT_NODE - env_spec_mult_n.label = ENV_SPEC_MULT_NODE - env_spec_mult_n.location = (start_pos_x + pos_x_shift * 2, start_pos_y - 350) - env_spec_mult_n.blend_type = "MULTIPLY" - env_spec_mult_n.inputs['Fac'].default_value = 1 - - refl_tex_mult_n = add_env_g.nodes.new("ShaderNodeMixRGB") - refl_tex_mult_n.name = REFL_TEX_MULT_NODE - refl_tex_mult_n.label = REFL_TEX_MULT_NODE - refl_tex_mult_n.location = (start_pos_x + pos_x_shift * 2, start_pos_y - 150) - refl_tex_mult_n.blend_type = "MULTIPLY" - refl_tex_mult_n.inputs['Fac'].default_value = 1 - - refl_tex_col_mult_n = add_env_g.nodes.new("ShaderNodeMixRGB") - refl_tex_col_mult_n.name = REFL_TEX_COL_MULT_NODE - refl_tex_col_mult_n.label = REFL_TEX_COL_MULT_NODE - refl_tex_col_mult_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y - 200) - refl_tex_col_mult_n.blend_type = "MULTIPLY" - refl_tex_col_mult_n.inputs['Fac'].default_value = 1 - - fresnel_gn = add_env_g.nodes.new("ShaderNodeGroup") - fresnel_gn.name = FRESNEL_GNODE - fresnel_gn.label = FRESNEL_GNODE - fresnel_gn.location = (start_pos_x + pos_x_shift * 2, start_pos_y + 150) - fresnel_gn.node_tree = fresnel.get_node_group() - fresnel_gn.inputs['Scale'].default_value = 0.9 - fresnel_gn.inputs['Bias'].default_value = 0.2 - - tex_fresnel_mult_n = add_env_g.nodes.new("ShaderNodeMixRGB") - tex_fresnel_mult_n.name = TEX_FRESNEL_MULT_NODE - tex_fresnel_mult_n.label = TEX_FRESNEL_MULT_NODE - tex_fresnel_mult_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y) - tex_fresnel_mult_n.blend_type = "MULTIPLY" - tex_fresnel_mult_n.inputs['Fac'].default_value = 1 - - global_env_factor_n = add_env_g.nodes.new("ShaderNodeValue") - global_env_factor_n.name = GLOBAL_ENV_FACTOR_NODE - global_env_factor_n.label = GLOBAL_ENV_FACTOR_NODE - global_env_factor_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y - 200) - - global_env_mult_n = add_env_g.nodes.new("ShaderNodeMixRGB") - global_env_mult_n.name = GLOBAL_ENV_MULT_NODE - global_env_mult_n.label = GLOBAL_ENV_MULT_NODE - global_env_mult_n.location = (start_pos_x + pos_x_shift * 5, start_pos_y) - global_env_mult_n.blend_type = "MULTIPLY" - global_env_mult_n.inputs['Fac'].default_value = 1 - - # geometry links - add_env_g.links.new(refl_normal_gn.inputs['View Vector'], input_n.outputs['View Vector']) - add_env_g.links.new(refl_normal_gn.inputs['Normal Vector'], input_n.outputs['Normal Vector']) - - add_env_g.links.new(combine_base_alpha_n.inputs['R'], input_n.outputs['Base Texture Alpha']) - add_env_g.links.new(combine_base_alpha_n.inputs['G'], input_n.outputs['Base Texture Alpha']) - add_env_g.links.new(combine_base_alpha_n.inputs['B'], input_n.outputs['Base Texture Alpha']) - - # pass 1 - add_env_g.links.new(env_spec_mult_n.inputs['Color1'], input_n.outputs['Env Factor Color']) - add_env_g.links.new(env_spec_mult_n.inputs['Color2'], input_n.outputs['Specular Color']) - - add_env_g.links.new(refl_tex_mult_n.inputs['Color1'], input_n.outputs['Reflection Texture Color']) - add_env_g.links.new(refl_tex_mult_n.inputs['Color2'], combine_base_alpha_n.outputs['Image']) - - # pass 2 - add_env_g.links.new(fresnel_gn.inputs['Reflection Normal Vector'], refl_normal_gn.outputs['Reflection Normal']) - add_env_g.links.new(fresnel_gn.inputs['Normal Vector'], input_n.outputs['Normal Vector']) - add_env_g.links.new(fresnel_gn.inputs['Scale'], input_n.outputs['Fresnel Scale']) - add_env_g.links.new(fresnel_gn.inputs['Bias'], input_n.outputs['Fresnel Bias']) - - add_env_g.links.new(refl_tex_col_mult_n.inputs['Color1'], refl_tex_mult_n.outputs['Color']) - add_env_g.links.new(refl_tex_col_mult_n.inputs['Color2'], env_spec_mult_n.outputs['Color']) - - # pass 3 - add_env_g.links.new(tex_fresnel_mult_n.inputs['Fac'], input_n.outputs['Apply Fresnel']) - add_env_g.links.new(tex_fresnel_mult_n.inputs['Color1'], refl_tex_col_mult_n.outputs['Color']) - add_env_g.links.new(tex_fresnel_mult_n.inputs['Color2'], fresnel_gn.outputs['Fresnel Factor']) - - # pass 4 - add_env_g.links.new(global_env_mult_n.inputs['Color1'], tex_fresnel_mult_n.outputs['Color']) - add_env_g.links.new(global_env_mult_n.inputs['Color2'], global_env_factor_n.outputs['Value']) - - # output pass - add_env_g.links.new(output_n.inputs['Environment Addition Color'], global_env_mult_n.outputs['Color']) diff --git a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/add_env_ng.py b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/add_env_ng.py new file mode 100644 index 0000000..a29a5ed --- /dev/null +++ b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/add_env_ng.py @@ -0,0 +1,230 @@ +# ##### 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) 2015-2019: SCS Software + +import bpy +from io_scs_tools.consts import Material as _MAT_consts +from io_scs_tools.consts import SCSLigthing as _LIGHTING_consts +from io_scs_tools.internals.shaders.eut2.std_node_groups import fresnel_ng + +ADD_ENV_G = _MAT_consts.node_group_prefix + "AddEnvGroup" + +_ENV_STRENGTH_MULT_NODE = "EnvStrengthMultiplier" +_ENV_WEIGHT_MULT_NODE = "EnvWeightMultiplier" +_ENV_SPEC_MULT_NODE = "EnvSpecMultiplier" +_REFL_TEX_MULT_NODE = "ReflectionTexMultiplier" +_REFL_TEX_COL_MULT_NODE = "ReflTexColorMultiplier" +_TEX_FRESNEL_MULT_NODE = "TextureFresnelMultiplier" +_GLOBAL_ENV_FACTOR_NODE = "GlobalEnvFactor" +_GLOBAL_ENV_MULT_NODE = "GlobalEnvMultiplier" +_FRESNEL_GNODE = "FresnelGroup" + + +def get_node_group(): + """Gets node group for calcualtion of environment addition color. + + :return: node group which calculates environment addition color + :rtype: bpy.types.NodeGroup + """ + + if __group_needs_recreation__(): + __create_node_group__() + + return bpy.data.node_groups[ADD_ENV_G] + + +def set_global_env_factor(value): + """Sets global environment factor multiplication to the node group. + + NOTE: We are using global factor as we can not determinate if texture is generated one or static one and + based on that decide how much of envirnoment should be applied to result. + So for closest result to game this factor should be "env_static_mod" variable from "sun_profile" multiplied + with "env" variable from "sun_profile". + This way static cubemaps will be used as in game, however even generated cubemaps well mostl work well, + becuase expected values of that multiplication is from 0 to 1.0. + + :param value: global enironment factor (should come from sun profile) + :type value: float + """ + + get_node_group().nodes[_GLOBAL_ENV_FACTOR_NODE].outputs[0].default_value = value + + +def reset_lighting_params(): + """Resets lighting to default values. + """ + set_global_env_factor(_LIGHTING_consts.default_env) + + +def __group_needs_recreation__(): + """Tells if group needs recreation. + + :return: True group isn't up to date and has to be (re)created; False if group doesn't need to be (re)created + :rtype: bool + """ + # current checks: + # 1. group existence in blender data block + # 2. existence of GLOBAL_ENV_FACTOR_NODE which was added in BT version 1.5 + return (ADD_ENV_G not in bpy.data.node_groups or + _GLOBAL_ENV_FACTOR_NODE not in bpy.data.node_groups[ADD_ENV_G].nodes or + _ENV_STRENGTH_MULT_NODE not in bpy.data.node_groups[ADD_ENV_G].nodes or + _ENV_WEIGHT_MULT_NODE not in bpy.data.node_groups[ADD_ENV_G].nodes) + + +def __create_node_group__(): + """Creates add env group. + + Specular Color - needed because our env factor is always multiplied with material specular color + (eut2_effect_uniforms.cpp::uniform_setup_material_environment) + Weighted Color - needed for weighted variants of environment (eut2_forward_envmap.cg::ENVMAP_WEIGHT) + Strength Multiplier - multiplier modulating environment strength (eut2_forward_envmap.ch::ENVMAP_STRENGTH_MULTIPLIER) + + Inputs: Fresnel Scale, Fresnel Bias, Normal Vector, View Vector, Apply Fresnel, + Reflection Texture Color, Base Texture Alpha, Env Factor Color, Specular Color, Weighted Color, Strength Multiplier + Outputs: Environment Addition Color + """ + + start_pos_x = 0 + start_pos_y = 0 + + pos_x_shift = 185 + + if ADD_ENV_G not in bpy.data.node_groups: # creation + + add_env_g = bpy.data.node_groups.new(type="ShaderNodeTree", name=ADD_ENV_G) + + else: # recreation + + add_env_g = bpy.data.node_groups[ADD_ENV_G] + + # delete all inputs and outputs + add_env_g.inputs.clear() + add_env_g.outputs.clear() + + # delete all old nodes and links as they will be recreated now with actual version + add_env_g.nodes.clear() + + # inputs defining + add_env_g.inputs.new("NodeSocketFloat", "Fresnel Scale") + add_env_g.inputs.new("NodeSocketFloat", "Fresnel Bias") + add_env_g.inputs.new("NodeSocketVector", "Normal Vector") + add_env_g.inputs.new("NodeSocketVector", "Reflection Normal Vector") + add_env_g.inputs.new("NodeSocketFloat", "Apply Fresnel") + add_env_g.inputs.new("NodeSocketColor", "Reflection Texture Color") + add_env_g.inputs.new("NodeSocketFloat", "Base Texture Alpha") + add_env_g.inputs.new("NodeSocketColor", "Env Factor Color") + add_env_g.inputs.new("NodeSocketColor", "Specular Color") + add_env_g.inputs.new("NodeSocketColor", "Weighted Color") + add_env_g.inputs.new("NodeSocketFloat", "Strength Multiplier") + + # outputs defining + add_env_g.outputs.new("NodeSocketColor", "Environment Addition Color") + + # node creation + input_n = add_env_g.nodes.new("NodeGroupInput") + input_n.location = (start_pos_x - pos_x_shift, start_pos_y) + + output_n = add_env_g.nodes.new("NodeGroupOutput") + output_n.location = (start_pos_x + pos_x_shift * 7, start_pos_y) + + env_weight_mult_n = add_env_g.nodes.new("ShaderNodeVectorMath") + env_weight_mult_n.name = env_weight_mult_n.label = _ENV_WEIGHT_MULT_NODE + env_weight_mult_n.location = (start_pos_x + pos_x_shift * 1, start_pos_y - 500) + env_weight_mult_n.operation = "MULTIPLY" + + env_spec_mult_n = add_env_g.nodes.new("ShaderNodeVectorMath") + env_spec_mult_n.name = env_spec_mult_n.label = _ENV_SPEC_MULT_NODE + env_spec_mult_n.location = (start_pos_x + pos_x_shift * 2, start_pos_y - 350) + env_spec_mult_n.operation = "MULTIPLY" + + refl_tex_mult_n = add_env_g.nodes.new("ShaderNodeVectorMath") + refl_tex_mult_n.name = refl_tex_mult_n.label = _REFL_TEX_MULT_NODE + refl_tex_mult_n.location = (start_pos_x + pos_x_shift * 2, start_pos_y - 150) + refl_tex_mult_n.operation = "MULTIPLY" + + refl_tex_col_mult_n = add_env_g.nodes.new("ShaderNodeVectorMath") + refl_tex_col_mult_n.name = refl_tex_col_mult_n.label = _REFL_TEX_COL_MULT_NODE + refl_tex_col_mult_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y - 200) + refl_tex_col_mult_n.operation = "MULTIPLY" + + fresnel_n = add_env_g.nodes.new("ShaderNodeGroup") + fresnel_n.name = fresnel_n.label = _FRESNEL_GNODE + fresnel_n.location = (start_pos_x + pos_x_shift * 2, start_pos_y + 150) + fresnel_n.node_tree = fresnel_ng.get_node_group() + fresnel_n.inputs['Scale'].default_value = 0.9 + fresnel_n.inputs['Bias'].default_value = 0.2 + + tex_fresnel_mult_n = add_env_g.nodes.new("ShaderNodeMixRGB") + tex_fresnel_mult_n.name = tex_fresnel_mult_n.label = _TEX_FRESNEL_MULT_NODE + tex_fresnel_mult_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y) + tex_fresnel_mult_n.blend_type = "MULTIPLY" + + global_env_factor_n = add_env_g.nodes.new("ShaderNodeValue") + global_env_factor_n.name = global_env_factor_n.label = _GLOBAL_ENV_FACTOR_NODE + global_env_factor_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y - 200) + + global_env_mult_n = add_env_g.nodes.new("ShaderNodeVectorMath") + global_env_mult_n.name = global_env_mult_n.label = _GLOBAL_ENV_MULT_NODE + global_env_mult_n.location = (start_pos_x + pos_x_shift * 5, start_pos_y) + global_env_mult_n.operation = "MULTIPLY" + + env_strength_mult_n = add_env_g.nodes.new("ShaderNodeVectorMath") + env_strength_mult_n.name = env_strength_mult_n.label = _ENV_STRENGTH_MULT_NODE + env_strength_mult_n.location = (start_pos_x + pos_x_shift * 6, start_pos_y - 100) + env_strength_mult_n.operation = "MULTIPLY" + + # links + # pass 1 + add_env_g.links.new(env_weight_mult_n.inputs[0], input_n.outputs['Specular Color']) + add_env_g.links.new(env_weight_mult_n.inputs[1], input_n.outputs['Weighted Color']) + + # pass 2 + add_env_g.links.new(env_spec_mult_n.inputs[0], input_n.outputs['Env Factor Color']) + add_env_g.links.new(env_spec_mult_n.inputs[1], env_weight_mult_n.outputs[0]) + + add_env_g.links.new(refl_tex_mult_n.inputs[0], input_n.outputs['Reflection Texture Color']) + add_env_g.links.new(refl_tex_mult_n.inputs[1], input_n.outputs['Base Texture Alpha']) + + # pass 3 + add_env_g.links.new(fresnel_n.inputs['Reflection Normal Vector'], input_n.outputs['Reflection Normal Vector']) + add_env_g.links.new(fresnel_n.inputs['Normal Vector'], input_n.outputs['Normal Vector']) + add_env_g.links.new(fresnel_n.inputs['Scale'], input_n.outputs['Fresnel Scale']) + add_env_g.links.new(fresnel_n.inputs['Bias'], input_n.outputs['Fresnel Bias']) + + add_env_g.links.new(refl_tex_col_mult_n.inputs[0], refl_tex_mult_n.outputs[0]) + add_env_g.links.new(refl_tex_col_mult_n.inputs[1], env_spec_mult_n.outputs[0]) + + # pass 4 + add_env_g.links.new(tex_fresnel_mult_n.inputs['Fac'], input_n.outputs['Apply Fresnel']) + add_env_g.links.new(tex_fresnel_mult_n.inputs['Color1'], refl_tex_col_mult_n.outputs[0]) + add_env_g.links.new(tex_fresnel_mult_n.inputs['Color2'], fresnel_n.outputs['Fresnel Factor']) + + # pass 5 + add_env_g.links.new(global_env_mult_n.inputs[0], tex_fresnel_mult_n.outputs['Color']) + add_env_g.links.new(global_env_mult_n.inputs[1], global_env_factor_n.outputs['Value']) + + # pass 6 + add_env_g.links.new(env_strength_mult_n.inputs[0], global_env_mult_n.outputs[0]) + add_env_g.links.new(env_strength_mult_n.inputs[1], input_n.outputs['Strength Multiplier']) + + # output pass + add_env_g.links.new(output_n.inputs['Environment Addition Color'], env_strength_mult_n.outputs[0]) + + # set default lighting + reset_lighting_params() diff --git a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/alpha_remap.py b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/alpha_remap_ng.py similarity index 100% rename from addon/io_scs_tools/internals/shaders/eut2/std_node_groups/alpha_remap.py rename to addon/io_scs_tools/internals/shaders/eut2/std_node_groups/alpha_remap_ng.py diff --git a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/compose_lighting.py b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/compose_lighting.py deleted file mode 100644 index f8e5441..0000000 --- a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/compose_lighting.py +++ /dev/null @@ -1,139 +0,0 @@ -# ##### 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) 2016: SCS Software - -import bpy -from io_scs_tools.consts import Material as _MAT_consts -from io_scs_tools.utils import convert as _convert_utils - -COMPOSE_LIGHTING_G = _MAT_consts.node_group_prefix + "ComposeLighting" - -# D = lighting diffuse -# A = AddAmbient * SunProfileAmbient -# DL = D + A -# SL = lighting specular -# DC = attributes diffuse -# DS = attributes specular -# R = (DC * DL) + (SC * SL) --> R = DC * D + SC * SL + DC * A -# -# instead of implementing original formula we can just add (DC * A) to combined (DC * D + SC * SL) -# which is output of blender material. Additionally lest step is adding environment if specified. - -_ADD_AMBIENT_COL_NODE = "AdditionalAmbientColor" # sun profile ambient color -_MULT_A_NODE = "A=AddAmbient*SunProfileAmbient" -_MULT_DCA_NODE = "DC*A" -_MIX_MATERIAL_DCA_NODE = "Result=MaterialOut+DC*A" -_MIX_FINAL_NODE = "Result=Result+Env" - - -def get_node_group(): - """Gets node group for calcualtion of final lighting color. - - :return: node group which calculates finall shader output color - :rtype: bpy.types.NodeGroup - """ - - if COMPOSE_LIGHTING_G not in bpy.data.node_groups: - __create_node_group__() - - return bpy.data.node_groups[COMPOSE_LIGHTING_G] - - -def set_additional_ambient_col(color): - """Sets ambient color which should be used when material is using additional ambient. - - :param color: ambient color from sun profile (not converted to srgb) - :type color: list - """ - - color = _convert_utils.linear_to_srgb(color) - color = _convert_utils.to_node_color(color) - - get_node_group().nodes[_ADD_AMBIENT_COL_NODE].outputs[0].default_value = color - - -def __create_node_group__(): - """Creates compose lighting group. - - Inputs: AddAmbient, Diffuse Color, Material Color, Env Color - Outputs: Composed Color - """ - - start_pos_x = 0 - start_pos_y = 0 - - pos_x_shift = 185 - - compose_light_g = bpy.data.node_groups.new(type="ShaderNodeTree", name=COMPOSE_LIGHTING_G) - - # inputs defining - compose_light_g.inputs.new("NodeSocketFloat", "AddAmbient") - compose_light_g.inputs.new("NodeSocketColor", "Diffuse Color") - compose_light_g.inputs.new("NodeSocketColor", "Material Color") - compose_light_g.inputs.new("NodeSocketColor", "Env Color") - input_n = compose_light_g.nodes.new("NodeGroupInput") - input_n.location = (start_pos_x - pos_x_shift, start_pos_y) - - # outputs defining - compose_light_g.outputs.new("NodeSocketColor", "Composed Color") - output_n = compose_light_g.nodes.new("NodeGroupOutput") - output_n.location = (start_pos_x + pos_x_shift * 6, start_pos_y) - - # nodes creation - add_ambient_col_n = compose_light_g.nodes.new("ShaderNodeRGB") - add_ambient_col_n.name = add_ambient_col_n.label = _ADD_AMBIENT_COL_NODE - add_ambient_col_n.location = (start_pos_x + pos_x_shift * 1, start_pos_y + 400) - add_ambient_col_n.outputs[0].default_value = (0.0,) * 4 - - mult_a_n = compose_light_g.nodes.new("ShaderNodeMixRGB") - mult_a_n.name = mult_a_n.label = _MULT_A_NODE - mult_a_n.location = (start_pos_x + pos_x_shift * 2, start_pos_y + 300) - mult_a_n.blend_type = "MULTIPLY" - mult_a_n.inputs["Fac"].default_value = 1 - - mult_dca_n = compose_light_g.nodes.new("ShaderNodeMixRGB") - mult_dca_n.name = mult_dca_n.label = _MULT_DCA_NODE - mult_dca_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 200) - mult_dca_n.blend_type = "MULTIPLY" - mult_dca_n.inputs["Fac"].default_value = 1 - - mix_material_dca_n = compose_light_g.nodes.new("ShaderNodeVectorMath") - mix_material_dca_n.name = mix_material_dca_n.label = _MIX_MATERIAL_DCA_NODE - mix_material_dca_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 100) - mix_material_dca_n.operation = "ADD" - - mix_final_n = compose_light_g.nodes.new("ShaderNodeVectorMath") - mix_final_n.name = mix_final_n.label = _MIX_FINAL_NODE - mix_final_n.location = (start_pos_x + pos_x_shift * 5, start_pos_y) - mix_final_n.operation = "ADD" - - # links creation - compose_light_g.links.new(mult_a_n.inputs["Color1"], add_ambient_col_n.outputs["Color"]) - compose_light_g.links.new(mult_a_n.inputs["Color2"], input_n.outputs["AddAmbient"]) - - compose_light_g.links.new(mult_dca_n.inputs["Color1"], mult_a_n.outputs["Color"]) - compose_light_g.links.new(mult_dca_n.inputs["Color2"], input_n.outputs["Diffuse Color"]) - - compose_light_g.links.new(mix_material_dca_n.inputs[0], mult_dca_n.outputs["Color"]) - compose_light_g.links.new(mix_material_dca_n.inputs[1], input_n.outputs["Material Color"]) - - compose_light_g.links.new(mix_final_n.inputs[0], mix_material_dca_n.outputs["Vector"]) - compose_light_g.links.new(mix_final_n.inputs[1], input_n.outputs["Env Color"]) - - compose_light_g.links.new(output_n.inputs["Composed Color"], mix_final_n.outputs["Vector"]) diff --git a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/compose_lighting_ng.py b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/compose_lighting_ng.py new file mode 100644 index 0000000..277ffb2 --- /dev/null +++ b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/compose_lighting_ng.py @@ -0,0 +1,185 @@ +# ##### 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) 2016-2019: SCS Software + +import bpy +from io_scs_tools.consts import Material as _MAT_consts +from io_scs_tools.consts import SCSLigthing as _LIGHTING_consts +from io_scs_tools.utils import convert as _convert_utils + +COMPOSE_LIGHTING_G = _MAT_consts.node_group_prefix + "ComposeLighting" + +_ADD_AMBIENT_COL_NODE = "AdditionalAmbientColor" # sun profile ambient color +_MULT_AA_NODE = "AA=AddAmbient*AdditionalAmbientColor" +_SUM_DL_FINAL_NODE = "DLF=DL+AA" +_MULT_DIFFUSE_NODE = "Diffuse=DC*DLF" +_MULT_SPECULAR_NODE = "Specular=SC*SL" +_SUM_DIFF_SPEC_NODE = "DiffSpec=Diffuse+Specular" +_SUM_FINAL_NODE = "Result=DiffSpec+Env" +_ALPHA_INV_NODE = "AlphaInvert" +_OUT_MAT_NODE = "OutMaterial" + + +def get_node_group(): + """Gets node group for calcualtion of final lighting color. + + DL = lighting diffuse + SL = lighting specular + AAC = additional ambient color (which is environment ambient) + AC = attributes add ambient + DC = attributes diffuse color + SC = attirbutes specular color + ENV = provided environment pass color + + R = (DC * (DL + (AAC * AC)) + (SC * SL) + ENV + + :return: node group which calculates finall shader output color + :rtype: bpy.types.NodeGroup + """ + + if COMPOSE_LIGHTING_G not in bpy.data.node_groups: + __create_node_group__() + + return bpy.data.node_groups[COMPOSE_LIGHTING_G] + + +def set_additional_ambient_col(color): + """Sets ambient color which should be used when material is using additional ambient. + + :param color: ambient color from sun profile (not converted to srgb) + :type color: list[float] | tuple[float] + """ + get_node_group().nodes[_ADD_AMBIENT_COL_NODE].outputs[0].default_value = _convert_utils.to_node_color(color, from_linear=True) + + +def reset_lighting_params(): + """Resets lighting to default values. + """ + set_additional_ambient_col(_LIGHTING_consts.default_ambient) + + +def __create_node_group__(): + """Creates compose lighting group. + + Inputs: AddAmbient, Diffuse Color, Specular Color, Env Color, Diffuse Lighting, Specular Lighting, Alpha + Outputs: Shader, Color, Alpha + """ + + start_pos_x = 0 + + pos_x_shift = 185 + + compose_light_g = bpy.data.node_groups.new(type="ShaderNodeTree", name=COMPOSE_LIGHTING_G) + + # inputs defining + compose_light_g.inputs.new("NodeSocketFloat", "AddAmbient") + compose_light_g.inputs.new("NodeSocketColor", "Diffuse Color") + compose_light_g.inputs.new("NodeSocketColor", "Specular Color") + compose_light_g.inputs.new("NodeSocketColor", "Env Color") + compose_light_g.inputs.new("NodeSocketColor", "Diffuse Lighting") + compose_light_g.inputs.new("NodeSocketColor", "Specular Lighting") + compose_light_g.inputs.new("NodeSocketFloat", "Alpha") + input_n = compose_light_g.nodes.new("NodeGroupInput") + input_n.location = (start_pos_x - pos_x_shift, 0) + + # outputs defining + compose_light_g.outputs.new("NodeSocketShader", "Shader") + compose_light_g.outputs.new("NodeSocketColor", "Color") + compose_light_g.outputs.new("NodeSocketFloat", "Alpha") + output_n = compose_light_g.nodes.new("NodeGroupOutput") + output_n.location = (start_pos_x + pos_x_shift * 8, 0) + + # nodes creation + add_ambient_col_n = compose_light_g.nodes.new("ShaderNodeRGB") + add_ambient_col_n.name = add_ambient_col_n.label = _ADD_AMBIENT_COL_NODE + add_ambient_col_n.location = (start_pos_x + pos_x_shift * 1, 400) + + mult_aa_node = compose_light_g.nodes.new("ShaderNodeVectorMath") + mult_aa_node.name = mult_aa_node.label = _MULT_AA_NODE + mult_aa_node.location = (start_pos_x + pos_x_shift * 2, 350) + mult_aa_node.operation = "MULTIPLY" + + sum_dl_final_n = compose_light_g.nodes.new("ShaderNodeVectorMath") + sum_dl_final_n.name = sum_dl_final_n.label = _SUM_DL_FINAL_NODE + sum_dl_final_n.location = (start_pos_x + pos_x_shift * 3, 300) + sum_dl_final_n.operation = "ADD" + + mult_diffuse_n = compose_light_g.nodes.new("ShaderNodeVectorMath") + mult_diffuse_n.name = mult_diffuse_n.label = _MULT_DIFFUSE_NODE + mult_diffuse_n.location = (start_pos_x + pos_x_shift * 4, 250) + mult_diffuse_n.operation = "MULTIPLY" + + mult_specular_n = compose_light_g.nodes.new("ShaderNodeVectorMath") + mult_specular_n.name = mult_specular_n.label = _MULT_SPECULAR_NODE + mult_specular_n.location = (start_pos_x + pos_x_shift * 4, 50) + mult_specular_n.operation = "MULTIPLY" + + sum_diff_spec_n = compose_light_g.nodes.new("ShaderNodeVectorMath") + sum_diff_spec_n.name = sum_diff_spec_n.label = _SUM_DIFF_SPEC_NODE + sum_diff_spec_n.location = (start_pos_x + pos_x_shift * 5, 100) + sum_diff_spec_n.operation = "ADD" + + sum_final_n = compose_light_g.nodes.new("ShaderNodeVectorMath") + sum_final_n.name = sum_final_n.label = _SUM_FINAL_NODE + sum_final_n.location = (start_pos_x + pos_x_shift * 6, 0) + sum_final_n.operation = "ADD" + + alpha_inv_n = compose_light_g.nodes.new("ShaderNodeMath") + alpha_inv_n.name = alpha_inv_n.label = _ALPHA_INV_NODE + alpha_inv_n.location = (start_pos_x + pos_x_shift * 6, -200) + alpha_inv_n.operation = "SUBTRACT" + alpha_inv_n.inputs[0].default_value = 0.999999 # TODO: change back to 1.0 after bug is fixed: https://developer.blender.org/T71426 + alpha_inv_n.use_clamp = True + + out_mat_node = compose_light_g.nodes.new("ShaderNodeEeveeSpecular") + out_mat_node.name = out_mat_node.label = _OUT_MAT_NODE + out_mat_node.location = (start_pos_x + pos_x_shift * 7, 0) + out_mat_node.inputs["Base Color"].default_value = (0.0,) * 4 + out_mat_node.inputs["Specular"].default_value = (0.0,) * 4 + + # links creation + compose_light_g.links.new(mult_aa_node.inputs[0], add_ambient_col_n.outputs["Color"]) + compose_light_g.links.new(mult_aa_node.inputs[1], input_n.outputs["AddAmbient"]) + + compose_light_g.links.new(sum_dl_final_n.inputs[0], mult_aa_node.outputs[0]) + compose_light_g.links.new(sum_dl_final_n.inputs[1], input_n.outputs["Diffuse Lighting"]) + + compose_light_g.links.new(mult_diffuse_n.inputs[0], sum_dl_final_n.outputs["Vector"]) + compose_light_g.links.new(mult_diffuse_n.inputs[1], input_n.outputs["Diffuse Color"]) + + compose_light_g.links.new(mult_specular_n.inputs[0], input_n.outputs["Specular Color"]) + compose_light_g.links.new(mult_specular_n.inputs[1], input_n.outputs["Specular Lighting"]) + + compose_light_g.links.new(sum_diff_spec_n.inputs[0], mult_diffuse_n.outputs[0]) + compose_light_g.links.new(sum_diff_spec_n.inputs[1], mult_specular_n.outputs[0]) + + compose_light_g.links.new(sum_final_n.inputs[0], sum_diff_spec_n.outputs["Vector"]) + compose_light_g.links.new(sum_final_n.inputs[1], input_n.outputs["Env Color"]) + + compose_light_g.links.new(alpha_inv_n.inputs[1], input_n.outputs["Alpha"]) + + compose_light_g.links.new(out_mat_node.inputs["Emissive Color"], sum_final_n.outputs["Vector"]) + compose_light_g.links.new(out_mat_node.inputs["Transparency"], alpha_inv_n.outputs["Value"]) + + compose_light_g.links.new(output_n.inputs["Shader"], out_mat_node.outputs["BSDF"]) + compose_light_g.links.new(output_n.inputs["Color"], sum_final_n.outputs["Vector"]) + compose_light_g.links.new(output_n.inputs["Alpha"], input_n.outputs["Alpha"]) + + # set default lighting + reset_lighting_params() diff --git a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/fresnel.py b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/fresnel_ng.py similarity index 99% rename from addon/io_scs_tools/internals/shaders/eut2/std_node_groups/fresnel.py rename to addon/io_scs_tools/internals/shaders/eut2/std_node_groups/fresnel_ng.py index 75201d8..23059d9 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/fresnel.py +++ b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/fresnel_ng.py @@ -79,7 +79,7 @@ def __create_fresnel_group__(): add_mult_n = fresnel_g.nodes.new("ShaderNodeMath") add_mult_n.location = (185 * 4, 0) add_mult_n.operation = "ADD" - add_mult_n.use_clamp = False + add_mult_n.use_clamp = True add_mult_n.inputs[0].default_value = 1.0 # group links diff --git a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/lampmask_mixer.py b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/lampmask_mixer_ng.py similarity index 96% rename from addon/io_scs_tools/internals/shaders/eut2/std_node_groups/lampmask_mixer.py rename to addon/io_scs_tools/internals/shaders/eut2/std_node_groups/lampmask_mixer_ng.py index 8c466be..1eec233 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/lampmask_mixer.py +++ b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/lampmask_mixer_ng.py @@ -1,3 +1,23 @@ +# ##### 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 + import bpy from io_scs_tools.consts import LampTools as _LT_consts from io_scs_tools.consts import Material as _MAT_consts @@ -91,7 +111,7 @@ def __create_node_group__(): __init_uv_tile_bounding_nodes__(lampmask_g, uv_x_dot_n, uv_x_tile, pos_x_shift * 2, pos_y, max_x_uv) pos_y -= 50 - max_x_uv += 2 + max_x_uv += 1 max_y_uv = 1 pos_y = -400 @@ -100,7 +120,7 @@ def __create_node_group__(): __init_uv_tile_bounding_nodes__(lampmask_g, uv_y_dot_n, uv_y_tile, pos_x_shift * 2, pos_y, max_y_uv) pos_y -= 50 - max_y_uv += 2 + max_y_uv += 1 # init vehicle sides uv bounding mechanism pos_y = -50 diff --git a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/lighting_evaluator_ng.py b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/lighting_evaluator_ng.py new file mode 100644 index 0000000..2008440 --- /dev/null +++ b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/lighting_evaluator_ng.py @@ -0,0 +1,356 @@ +# ##### 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 + +import bpy +from math import cos, radians +from io_scs_tools.consts import Material as _MAT_consts +from io_scs_tools.consts import SCSLigthing as _LIGHTING_consts +from io_scs_tools.utils import convert as _convert_utils + +LIGHTING_EVALUATOR_G = _MAT_consts.node_group_prefix + "LightingEvaluator" + +_LIGHT_DIR_NODE = "LightDir" +_CAM_TO_WORLD_NODE = "CameraToWorld" +_CAM_NORMALIZE_NODE = "NormalizedCameraWorld" + +_LIGHT_AMB_COLOR_NODE = "AmbientLightColor" +_LIGHT_DIFF_COLOR_NODE = "DiffuseLightColor" +_LIGHT_SPEC_COLOR_NODE = "SpecularLightColor" + +_N_DOT_L_NODE = "NDotL=dot(NormalVector, NLight)" +_DIFF_FACTOR_NODE = "DiffFac=max(0, NDotL)" +_DIFF_MULT_NODE = "Diff=DiffuseLightColor*DiffFac" +_DIFF_AMB_ADD_NODE = "D=AmbientLight+Diff" # goes to Diffuse Lighting output + +_DIFF_FLAT_MULT_NODE = "DFlat=DiffuseLightColor*0.4" + +_DIFF_FLAT_NODE = "FlatFilter" + +_N_SUM_NODE = "NSum=IncomingVector+NLight" +_N_HALF_NODE = "NHalf=normalize(NSum)" +_N_DOT_H_NODE = "NDotH=dot(NormalVector, NHalf)" +_N_DOT_H_MAX_NODE = "NDotHMax=max(0.0, NDotH)" +_N_DOT_L_SPEC_CUT_NODE = "NDotLSpecCut=NDotL*10" +_SPEC_FACTOR_NODE = "SpecFac=pow(NDotHMax, Shininess)" +_SPEC_FACTOR_SMOOTH_NODE = "SpecFacSmooth=SpecFac*NDotLSpecCut" # clamp it! +_SPEC_MULT_NODE = "S=SpecularLightColor*SpecFacSmooth" # goes to Specular Lighting output + +_SPEC_FLAT_NODE = "FlatFilter" + + +def get_node_group(): + """Gets node group for end result of lighting evaluator functions from our effect library. + + It's actually approximation of eut2_pass_common::forward_lighting_evaluator_t: + 1. It calculates lighting factors from: cg_lib_light::light_directional_normalized + 2. Combines factors with environment lighting: eut2_library::add_forward_light + 3. Outputs diffuse and specular lighting factors, ready to be used by material. + + :return: node group which exposes vertex color input + :rtype: bpy.types.NodeGroup + """ + + if LIGHTING_EVALUATOR_G not in bpy.data.node_groups: + __create_group__() + + return bpy.data.node_groups[LIGHTING_EVALUATOR_G] + + +def set_light_direction(sun_obj): + """Sets light direction from sun object. + + :param sun_obj: blender object representing scs sun, from which direction will be read + :type sun_obj: bpy.types.Object + """ + light_dir_n = get_node_group().nodes[_LIGHT_DIR_NODE] + + rot_matrix = sun_obj.rotation_euler.to_matrix() + + light_dir_n.inputs[0].default_value = rot_matrix[0][2] + light_dir_n.inputs[1].default_value = rot_matrix[1][2] + light_dir_n.inputs[2].default_value = rot_matrix[2][2] + + cam_to_world_n = get_node_group().nodes[_CAM_TO_WORLD_NODE] + cam_to_world_n.convert_from = "WORLD" + + +def reset_light_direction(): + """Resets light direction and uses default one which is bound to current view matrix. + """ + light_dir_n = get_node_group().nodes[_LIGHT_DIR_NODE] + + light_dir_n.inputs[0].default_value = 0 + light_dir_n.inputs[1].default_value = cos(radians(60.0)) + light_dir_n.inputs[2].default_value = -cos(radians(60.0)) + + cam_to_world_n = get_node_group().nodes[_CAM_TO_WORLD_NODE] + cam_to_world_n.convert_from = "CAMERA" + + +def set_ambient_light(color): + """Sets ambient light color to evaluator. + + :param color: color to use as ambient + :type color: tuple[float] | list[float | mathutils.Color + """ + get_node_group().nodes[_LIGHT_AMB_COLOR_NODE].outputs[0].default_value = _convert_utils.to_node_color(color, from_linear=True) + + +def set_diffuse_light(color): + """Sets diffuse light color to evaluator. + + :param color: color to use as diffuse + :type color: tuple[float] | list[float | mathutils.Color + """ + get_node_group().nodes[_LIGHT_DIFF_COLOR_NODE].outputs[0].default_value = _convert_utils.to_node_color(color, from_linear=True) + + +def set_specular_light(color): + """Sets specular light color to evaluator. + + :param color: color to use as specular + :type color: tuple[float] | list[float | mathutils.Color + """ + get_node_group().nodes[_LIGHT_SPEC_COLOR_NODE].outputs[0].default_value = _convert_utils.to_node_color(color, from_linear=True) + + +def reset_lighting_params(): + """Resets lighting to default values. + """ + reset_light_direction() + set_ambient_light(_LIGHTING_consts.default_ambient) + set_diffuse_light(_LIGHTING_consts.default_diffuse) + set_specular_light(_LIGHTING_consts.default_specular) + + +def __create_group__(): + """Creates group and their nodes. + + Inputs: Normal Vector, Incoming Vector, Shininess + Outputs: Diffuse Lighting, Specular Lighting + """ + + start_pos_x = 0 + pos_x_shift = 185 + + if LIGHTING_EVALUATOR_G not in bpy.data.node_groups: # creation + + lighting_eval_g = bpy.data.node_groups.new(type="ShaderNodeTree", name=LIGHTING_EVALUATOR_G) + + # inputs defining + lighting_eval_g.inputs.new("NodeSocketVector", "Normal Vector") # n_normal + lighting_eval_g.inputs.new("NodeSocketVector", "Incoming Vector") # n_eye + lighting_eval_g.inputs.new("NodeSocketFloat", "Shininess") # specular_exponent + lighting_eval_g.inputs.new("NodeSocketFloat", "Flat Lighting") # flat lighting switch, should be 0 or 1 + + # outputs defining + lighting_eval_g.outputs.new("NodeSocketColor", "Diffuse Lighting") # final diffuse lighting + lighting_eval_g.outputs.new("NodeSocketColor", "Specular Lighting") # final specular lighting + lighting_eval_g.outputs.new("NodeSocketVector", "Normal") # bypassed normal, to have one access point to final normal + + else: # recreation + + lighting_eval_g = bpy.data.node_groups[LIGHTING_EVALUATOR_G] + + # delete all old nodes and links as they will be recreated now with actual version + lighting_eval_g.nodes.clear() + + # node creation + # 1. inputs and light infos + input_n = lighting_eval_g.nodes.new("NodeGroupInput") + input_n.location = (start_pos_x - pos_x_shift, -100) + + output_n = lighting_eval_g.nodes.new("NodeGroupOutput") + output_n.location = (start_pos_x + pos_x_shift * 11, 0) + + light_dir_n = lighting_eval_g.nodes.new("ShaderNodeCombineXYZ") + light_dir_n.name = light_dir_n.label = _LIGHT_DIR_NODE + light_dir_n.location = (start_pos_x - pos_x_shift * 2, 100) + + cam_to_world_n = lighting_eval_g.nodes.new("ShaderNodeVectorTransform") + cam_to_world_n.name = cam_to_world_n.label = _CAM_TO_WORLD_NODE + cam_to_world_n.location = (start_pos_x - pos_x_shift, 100) + cam_to_world_n.vector_type = "VECTOR" + cam_to_world_n.convert_from = "CAMERA" + cam_to_world_n.convert_to = "WORLD" + + cam_normalize_n = lighting_eval_g.nodes.new("ShaderNodeVectorMath") + cam_normalize_n.name = cam_normalize_n.label = _CAM_NORMALIZE_NODE + cam_normalize_n.location = (start_pos_x, 100) + cam_normalize_n.operation = "NORMALIZE" + + light_amb_n = lighting_eval_g.nodes.new("ShaderNodeRGB") + light_amb_n.name = light_amb_n.label = _LIGHT_AMB_COLOR_NODE + light_amb_n.location = (start_pos_x, 600) + + light_diff_n = lighting_eval_g.nodes.new("ShaderNodeRGB") + light_diff_n.name = light_diff_n.label = _LIGHT_DIFF_COLOR_NODE + light_diff_n.location = (start_pos_x, 400) + + light_spec_n = lighting_eval_g.nodes.new("ShaderNodeRGB") + light_spec_n.name = light_spec_n.label = _LIGHT_SPEC_COLOR_NODE + light_spec_n.location = (start_pos_x, -400) + + # 2. diffuse lighting + n_dot_l_n = lighting_eval_g.nodes.new("ShaderNodeVectorMath") + n_dot_l_n.name = n_dot_l_n.label = _N_DOT_L_NODE + n_dot_l_n.location = (start_pos_x + pos_x_shift * 2, 200) + n_dot_l_n.operation = "DOT_PRODUCT" + + diff_factor_n = lighting_eval_g.nodes.new("ShaderNodeMath") + diff_factor_n.name = diff_factor_n.label = _DIFF_FACTOR_NODE + diff_factor_n.location = (start_pos_x + pos_x_shift * 3, 300) + diff_factor_n.operation = "MAXIMUM" + diff_factor_n.inputs[1].default_value = 0.0 + + diff_mult_n = lighting_eval_g.nodes.new("ShaderNodeVectorMath") + diff_mult_n.name = diff_mult_n.label = _DIFF_MULT_NODE + diff_mult_n.location = (start_pos_x + pos_x_shift * 4, 450) + diff_mult_n.operation = "MULTIPLY" + + diff_flat_mult_n = lighting_eval_g.nodes.new("ShaderNodeVectorMath") + diff_flat_mult_n.name = diff_flat_mult_n.label = _DIFF_FLAT_MULT_NODE + diff_flat_mult_n.location = (start_pos_x + pos_x_shift * 4, 250) + diff_flat_mult_n.operation = "MULTIPLY" + diff_flat_mult_n.inputs[1].default_value = (0.4,) * 3 + + diff_flat_n = lighting_eval_g.nodes.new("ShaderNodeMixRGB") + diff_flat_n.name = diff_flat_n.label = _DIFF_FLAT_NODE + diff_flat_n.location = (start_pos_x + pos_x_shift * 5, 400) + diff_flat_n.blend_type = "MIX" + + diff_amb_add_n = lighting_eval_g.nodes.new("ShaderNodeVectorMath") + diff_amb_add_n.name = diff_amb_add_n.label = _DIFF_AMB_ADD_NODE + diff_amb_add_n.location = (start_pos_x + pos_x_shift * 6, 600) + diff_amb_add_n.operation = "ADD" + + # 3. specular lighting + n_sum_n = lighting_eval_g.nodes.new("ShaderNodeVectorMath") + n_sum_n.name = n_sum_n.label = _N_SUM_NODE + n_sum_n.location = (start_pos_x + pos_x_shift * 2, 0) + n_sum_n.operation = "ADD" + + n_half_n = lighting_eval_g.nodes.new("ShaderNodeVectorMath") + n_half_n.name = n_half_n.label = _N_HALF_NODE + n_half_n.location = (start_pos_x + pos_x_shift * 3, 0) + n_half_n.operation = "NORMALIZE" + + n_dot_h_n = lighting_eval_g.nodes.new("ShaderNodeVectorMath") + n_dot_h_n.name = n_dot_h_n.label = _N_DOT_H_NODE + n_dot_h_n.location = (start_pos_x + pos_x_shift * 4, -100) + n_dot_h_n.operation = "DOT_PRODUCT" + + n_dot_h_max_n = lighting_eval_g.nodes.new("ShaderNodeMath") + n_dot_h_max_n.name = n_dot_h_max_n.label = _N_DOT_H_MAX_NODE + n_dot_h_max_n.location = (start_pos_x + pos_x_shift * 5, -100) + n_dot_h_max_n.operation = "MAXIMUM" + n_dot_h_max_n.inputs[1].default_value = 0.0 + + n_dot_l_spec_cut_n = lighting_eval_g.nodes.new("ShaderNodeMath") + n_dot_l_spec_cut_n.name = n_dot_l_spec_cut_n.label = _N_DOT_L_SPEC_CUT_NODE + n_dot_l_spec_cut_n.location = (start_pos_x + pos_x_shift * 6, 0) + n_dot_l_spec_cut_n.operation = "MULTIPLY" + n_dot_l_spec_cut_n.inputs[1].default_value = 10.0 + n_dot_l_spec_cut_n.use_clamp = True + + spec_factor_n = lighting_eval_g.nodes.new("ShaderNodeMath") + spec_factor_n.name = spec_factor_n.label = _SPEC_FACTOR_NODE + spec_factor_n.location = (start_pos_x + pos_x_shift * 6, -200) + spec_factor_n.operation = "POWER" + + spec_factor_smooth_n = lighting_eval_g.nodes.new("ShaderNodeMath") + spec_factor_smooth_n.name = spec_factor_smooth_n.label = _SPEC_FACTOR_SMOOTH_NODE + spec_factor_smooth_n.location = (start_pos_x + pos_x_shift * 7, 0) + spec_factor_smooth_n.operation = "MULTIPLY" + + spec_mult_n = lighting_eval_g.nodes.new("ShaderNodeVectorMath") + spec_mult_n.name = spec_mult_n.label = _SPEC_MULT_NODE + spec_mult_n.location = (start_pos_x + pos_x_shift * 8, -200) + spec_mult_n.operation = "MULTIPLY" + + spec_flat_n = lighting_eval_g.nodes.new("ShaderNodeMixRGB") + spec_flat_n.name = spec_flat_n.label = _SPEC_FLAT_NODE + spec_flat_n.location = (start_pos_x + pos_x_shift * 9, -200) + spec_flat_n.blend_type = "MIX" + spec_flat_n.inputs["Color2"].default_value = (0.0,) * 4 + + # group links + # pass #-2 + lighting_eval_g.links.new(cam_to_world_n.inputs["Vector"], light_dir_n.outputs["Vector"]) + + # pass #-1 + lighting_eval_g.links.new(cam_normalize_n.inputs[0], cam_to_world_n.outputs["Vector"]) + + # pass #1 + lighting_eval_g.links.new(n_dot_l_n.inputs[0], cam_normalize_n.outputs["Vector"]) + lighting_eval_g.links.new(n_dot_l_n.inputs[1], input_n.outputs["Normal Vector"]) + + lighting_eval_g.links.new(n_sum_n.inputs[0], cam_normalize_n.outputs["Vector"]) + lighting_eval_g.links.new(n_sum_n.inputs[1], input_n.outputs["Incoming Vector"]) + + # pass #2 + lighting_eval_g.links.new(diff_factor_n.inputs[0], n_dot_l_n.outputs["Value"]) + + lighting_eval_g.links.new(n_half_n.inputs[0], n_sum_n.outputs["Vector"]) + + # pass #3 + lighting_eval_g.links.new(diff_mult_n.inputs[0], light_diff_n.outputs["Color"]) + lighting_eval_g.links.new(diff_mult_n.inputs[1], diff_factor_n.outputs["Value"]) + + lighting_eval_g.links.new(diff_flat_mult_n.inputs[0], light_diff_n.outputs["Color"]) + + lighting_eval_g.links.new(n_dot_h_n.inputs[0], n_half_n.outputs["Vector"]) + lighting_eval_g.links.new(n_dot_h_n.inputs[1], input_n.outputs["Normal Vector"]) + + # pass #4 + lighting_eval_g.links.new(diff_flat_n.inputs["Fac"], input_n.outputs["Flat Lighting"]) + lighting_eval_g.links.new(diff_flat_n.inputs["Color1"], diff_mult_n.outputs[0]) + lighting_eval_g.links.new(diff_flat_n.inputs["Color2"], diff_flat_mult_n.outputs[0]) + + lighting_eval_g.links.new(n_dot_h_max_n.inputs[0], n_dot_h_n.outputs["Value"]) + + # pass #5 + lighting_eval_g.links.new(diff_amb_add_n.inputs[0], light_amb_n.outputs["Color"]) + lighting_eval_g.links.new(diff_amb_add_n.inputs[1], diff_flat_n.outputs["Color"]) + + lighting_eval_g.links.new(n_dot_l_spec_cut_n.inputs[0], n_dot_l_n.outputs["Value"]) + + lighting_eval_g.links.new(spec_factor_n.inputs[0], n_dot_h_max_n.outputs["Value"]) + lighting_eval_g.links.new(spec_factor_n.inputs[1], input_n.outputs["Shininess"]) + + # pass #6 + lighting_eval_g.links.new(spec_factor_smooth_n.inputs[0], n_dot_l_spec_cut_n.outputs["Value"]) + lighting_eval_g.links.new(spec_factor_smooth_n.inputs[1], spec_factor_n.outputs["Value"]) + + # pass #7 + lighting_eval_g.links.new(spec_mult_n.inputs[0], spec_factor_smooth_n.outputs["Value"]) + lighting_eval_g.links.new(spec_mult_n.inputs[1], light_spec_n.outputs["Color"]) + + # pass #8 + lighting_eval_g.links.new(spec_flat_n.inputs["Fac"], input_n.outputs["Flat Lighting"]) + lighting_eval_g.links.new(spec_flat_n.inputs["Color1"], spec_mult_n.outputs[0]) + + # pass: out + lighting_eval_g.links.new(output_n.inputs["Diffuse Lighting"], diff_amb_add_n.outputs[0]) + lighting_eval_g.links.new(output_n.inputs["Specular Lighting"], spec_flat_n.outputs["Color"]) + lighting_eval_g.links.new(output_n.inputs["Normal"], input_n.outputs["Normal Vector"]) + + # set default lighting + reset_lighting_params() diff --git a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/linear_to_srgb_ng.py b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/linear_to_srgb_ng.py new file mode 100644 index 0000000..06b16f4 --- /dev/null +++ b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/linear_to_srgb_ng.py @@ -0,0 +1,161 @@ +# ##### 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 + +# Conversion done by formula: +# / L ≤ 0.0031308: L*12.92 +# S = - +# \ L > 0.0031308: 1.055*L^(1/2.4) − 0.055 + +import bpy +from io_scs_tools.consts import Material as _MAT_consts + +LINEAR_TO_SRGB_G = _MAT_consts.node_group_prefix + "LinearToSRGB" + +_SMALL_MULT_NODE = "SMALL_FINAL=L*12.92" + +_BIG_POWER_NODE = "BIG_LPOW=L^(1/2.4)" +_BIG_MULT_NODE = "BIG_LMULT=1.055*BIG_LPOW" +_BIG_SUBTRACT_NODE = "BIG_FINAL=BIG_LMULT-0.055" + +_IS_SMALL_NODE = "IS_SMALL=L < 0.0031308" +_IS_BIG_NODE = "IS_BIG=L > 0.0031308" + +_SMALL_FACTOR_NODE = "SMALL_FINAL*IS_SMALL" +_BIG_FACTOR_NODE = "BIG_FINAL*IS_BIG" + +_FINAL_MIX_NODE = "SMALL+BIG" + + +def get_node_group(): + """Gets node group for linear to srgb conversion. + + :return: node group which exposes linear to srgb conversion + :rtype: bpy.types.NodeGroup + """ + + if LINEAR_TO_SRGB_G not in bpy.data.node_groups: + __create_linear_to_srgb_group__() + + return bpy.data.node_groups[LINEAR_TO_SRGB_G] + + +def __create_linear_to_srgb_group__(): + """Creates linear to srgb coonversion group. + + Inputs: Value + Outputs: Value + """ + + lin_to_srgb_g = bpy.data.node_groups.new(type="ShaderNodeTree", name=LINEAR_TO_SRGB_G) + + start_pos_x = 0 + pos_x_shift = 185 + + # outputs defining + # inputs defining + lin_to_srgb_g.inputs.new("NodeSocketFloat", "Value") + input_n = lin_to_srgb_g.nodes.new("NodeGroupInput") + input_n.location = (start_pos_x - pos_x_shift, 0) + + # outputs defining + lin_to_srgb_g.outputs.new("NodeSocketFloat", "Value") + output_n = lin_to_srgb_g.nodes.new("NodeGroupOutput") + output_n.location = (start_pos_x + pos_x_shift * 7, 0) + + # group nodes + small_mult_n = lin_to_srgb_g.nodes.new("ShaderNodeMath") + small_mult_n.name = small_mult_n.label = _SMALL_MULT_NODE + small_mult_n.location = (start_pos_x + pos_x_shift * 3, 200) + small_mult_n.operation = 'MULTIPLY' + small_mult_n.inputs[1].default_value = 12.92 + + big_pow_n = lin_to_srgb_g.nodes.new("ShaderNodeMath") + big_pow_n.name = big_pow_n.label = _BIG_POWER_NODE + big_pow_n.location = (start_pos_x + pos_x_shift, -200) + big_pow_n.operation = 'POWER' + big_pow_n.inputs[1].default_value = 1 / 2.4 + + big_mult_n = lin_to_srgb_g.nodes.new("ShaderNodeMath") + big_mult_n.name = big_mult_n.label = _BIG_MULT_NODE + big_mult_n.location = (start_pos_x + pos_x_shift * 2, -200) + big_mult_n.operation = 'MULTIPLY' + big_mult_n.inputs[1].default_value = 1.055 + + big_sub_n = lin_to_srgb_g.nodes.new("ShaderNodeMath") + big_sub_n.name = big_sub_n.label = _BIG_SUBTRACT_NODE + big_sub_n.location = (start_pos_x + pos_x_shift * 3, -200) + big_sub_n.operation = 'SUBTRACT' + big_sub_n.inputs[1].default_value = 0.055 + + is_small_n = lin_to_srgb_g.nodes.new("ShaderNodeMath") + is_small_n.name = is_small_n.label = _IS_SMALL_NODE + is_small_n.location = (start_pos_x + pos_x_shift * 4, 400) + is_small_n.operation = 'LESS_THAN' + is_small_n.inputs[1].default_value = 0.0031308 + + is_big_n = lin_to_srgb_g.nodes.new("ShaderNodeMath") + is_big_n.name = is_big_n.label = _IS_BIG_NODE + is_big_n.location = (start_pos_x + pos_x_shift * 4, 0) + is_big_n.operation = 'GREATER_THAN' + is_big_n.inputs[1].default_value = 0.0031308 + + small_factor_n = lin_to_srgb_g.nodes.new("ShaderNodeMath") + small_factor_n.name = small_factor_n.label = _SMALL_FACTOR_NODE + small_factor_n.location = (start_pos_x + pos_x_shift * 5, 300) + small_factor_n.operation = 'MULTIPLY' + + big_factor_n = lin_to_srgb_g.nodes.new("ShaderNodeMath") + big_factor_n.name = big_factor_n.label = _BIG_FACTOR_NODE + big_factor_n.location = (start_pos_x + pos_x_shift * 5, -100) + big_factor_n.operation = 'MULTIPLY' + + final_mix_n = lin_to_srgb_g.nodes.new("ShaderNodeMath") + final_mix_n.name = final_mix_n.label = _FINAL_MIX_NODE + final_mix_n.location = (start_pos_x + pos_x_shift * 6, 0) + final_mix_n.operation = 'ADD' + + # group links + # pass: 1 + lin_to_srgb_g.links.new(big_pow_n.inputs[0], input_n.outputs['Value']) + + # pass: 2 + lin_to_srgb_g.links.new(big_mult_n.inputs[0], big_pow_n.outputs['Value']) + + # pass: 3 + lin_to_srgb_g.links.new(small_mult_n.inputs[0], input_n.outputs['Value']) + lin_to_srgb_g.links.new(big_sub_n.inputs[0], big_mult_n.outputs['Value']) + + # pass: 4 + lin_to_srgb_g.links.new(is_small_n.inputs[0], input_n.outputs['Value']) + lin_to_srgb_g.links.new(is_big_n.inputs[0], input_n.outputs['Value']) + + # pass: 5 + lin_to_srgb_g.links.new(small_factor_n.inputs[0], is_small_n.outputs['Value']) + lin_to_srgb_g.links.new(small_factor_n.inputs[1], small_mult_n.outputs['Value']) + + lin_to_srgb_g.links.new(big_factor_n.inputs[0], is_big_n.outputs['Value']) + lin_to_srgb_g.links.new(big_factor_n.inputs[1], big_sub_n.outputs['Value']) + + # pass: 6 + lin_to_srgb_g.links.new(final_mix_n.inputs[0], small_factor_n.outputs['Value']) + lin_to_srgb_g.links.new(final_mix_n.inputs[1], big_factor_n.outputs['Value']) + + # pass: out + lin_to_srgb_g.links.new(output_n.inputs['Value'], final_mix_n.outputs['Value']) diff --git a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/mult2_mix.py b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/mult2_mix_ng.py similarity index 92% rename from addon/io_scs_tools/internals/shaders/eut2/std_node_groups/mult2_mix.py rename to addon/io_scs_tools/internals/shaders/eut2/std_node_groups/mult2_mix_ng.py index b55a6ca..a9dbb63 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/mult2_mix.py +++ b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/mult2_mix_ng.py @@ -91,12 +91,11 @@ def __create_node_group__(): mult_green_mix_n.blend_type = "MIX" mult_green_mix_n.inputs["Color2"].default_value = (1.0,) * 4 - mult_base_mult_n = mult2_mix_g.nodes.new("ShaderNodeMixRGB") + mult_base_mult_n = mult2_mix_g.nodes.new("ShaderNodeVectorMath") mult_base_mult_n.name = _MULT_BASE_MULT_NODE mult_base_mult_n.label = _MULT_BASE_MULT_NODE mult_base_mult_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 400) - mult_base_mult_n.blend_type = "MULTIPLY" - mult_base_mult_n.inputs["Fac"].default_value = 1.0 + mult_base_mult_n.operation = "MULTIPLY" alpha_mix_n = mult2_mix_g.nodes.new("ShaderNodeMixRGB") alpha_mix_n.name = _ALPHA_MIX_NODE @@ -111,12 +110,12 @@ def __create_node_group__(): mult2_mix_g.links.new(mult_green_mix_n.inputs["Fac"], input_n.outputs["Base Alpha"]) mult2_mix_g.links.new(mult_green_mix_n.inputs["Color1"], mult_green_scale_n.outputs["Value"]) - mult2_mix_g.links.new(mult_base_mult_n.inputs["Color1"], input_n.outputs["Base Color"]) - mult2_mix_g.links.new(mult_base_mult_n.inputs["Color2"], mult_green_mix_n.outputs["Color"]) + mult2_mix_g.links.new(mult_base_mult_n.inputs[0], input_n.outputs["Base Color"]) + mult2_mix_g.links.new(mult_base_mult_n.inputs[1], mult_green_mix_n.outputs["Color"]) mult2_mix_g.links.new(alpha_mix_n.inputs["Fac"], input_n.outputs["Base Alpha"]) mult2_mix_g.links.new(alpha_mix_n.inputs["Color1"], input_n.outputs["Mult Alpha"]) mult2_mix_g.links.new(alpha_mix_n.inputs["Color2"], input_n.outputs["Base Alpha"]) - mult2_mix_g.links.new(output_n.inputs["Mix Color"], mult_base_mult_n.outputs["Color"]) + mult2_mix_g.links.new(output_n.inputs["Mix Color"], mult_base_mult_n.outputs[0]) mult2_mix_g.links.new(output_n.inputs["Mix Alpha"], alpha_mix_n.outputs["Color"]) diff --git a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/refl_normal.py b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/refl_normal.py deleted file mode 100644 index c15511e..0000000 --- a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/refl_normal.py +++ /dev/null @@ -1,91 +0,0 @@ -# ##### 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) 2015: SCS Software - -import bpy -from io_scs_tools.consts import Material as _MAT_consts - -REFL_NORMAL_G = _MAT_consts.node_group_prefix + "ReflectionNormalGroup" - - -def get_node_group(): - """Gets node group for calculation of reflection normal. - - :return: node group which calculates reflection normal - :rtype: bpy.types.NodeGroup - """ - - if REFL_NORMAL_G not in bpy.data.node_groups: - __create_refl_normal_group__() - - return bpy.data.node_groups[REFL_NORMAL_G] - - -def __create_refl_normal_group__(): - """Create reflection normal group. - - Inputs: View Vector, Normal Vector - Outputs: Reflection Normal Vector - """ - - refl_normal_g = bpy.data.node_groups.new(type="ShaderNodeTree", name=REFL_NORMAL_G) - - # inputs defining - refl_normal_g.inputs.new("NodeSocketVector", "Normal Vector") - refl_normal_g.inputs.new("NodeSocketVector", "View Vector") - input_n = refl_normal_g.nodes.new("NodeGroupInput") - input_n.location = (0, 0) - - # outputs defining - refl_normal_g.outputs.new("NodeSocketVector", "Reflection Normal") - output_n = refl_normal_g.nodes.new("NodeGroupOutput") - output_n.location = (185 * 5, 0) - - # group nodes - dot_n = refl_normal_g.nodes.new("ShaderNodeVectorMath") - dot_n.location = (185, 150) - dot_n.operation = "DOT_PRODUCT" - - dot_mult_2_n = refl_normal_g.nodes.new("ShaderNodeMath") - dot_mult_2_n.location = (185 * 2, 100) - dot_mult_2_n.operation = "MULTIPLY" - dot_mult_2_n.inputs[1].default_value = 2.0 - - cross_product_n = refl_normal_g.nodes.new("ShaderNodeVectorMath") - cross_product_n.location = (185 * 3, 50) - cross_product_n.operation = "CROSS_PRODUCT" - - substract_n = refl_normal_g.nodes.new("ShaderNodeVectorMath") - substract_n.location = (185 * 4, -50) - substract_n.operation = "SUBTRACT" - - # group links - # formula: view_v - 2*dot(view_v, normal_v)*normal_v - refl_normal_g.links.new(dot_n.inputs[0], input_n.outputs['View Vector']) - refl_normal_g.links.new(dot_n.inputs[1], input_n.outputs['Normal Vector']) - - refl_normal_g.links.new(dot_mult_2_n.inputs[0], dot_n.outputs['Value']) - - refl_normal_g.links.new(cross_product_n.inputs[0], input_n.outputs['Normal Vector']) - refl_normal_g.links.new(cross_product_n.inputs[1], dot_mult_2_n.outputs['Value']) - - refl_normal_g.links.new(substract_n.inputs[0], input_n.outputs['View Vector']) - refl_normal_g.links.new(substract_n.inputs[1], cross_product_n.outputs['Vector']) - - refl_normal_g.links.new(output_n.inputs['Reflection Normal'], substract_n.outputs['Vector']) diff --git a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/refl_normal_ng.py b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/refl_normal_ng.py new file mode 100644 index 0000000..15215bb --- /dev/null +++ b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/refl_normal_ng.py @@ -0,0 +1,104 @@ +# ##### 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 + +import bpy +from io_scs_tools.consts import Material as _MAT_consts + +REFL_NORMAL_G = _MAT_consts.node_group_prefix + "ReflectionNormalGroup" + + +def get_node_group(): + """Gets node group for calculation of reflection normal. + + :return: node group which calculates reflection normal + :rtype: bpy.types.NodeGroup + """ + + if REFL_NORMAL_G not in bpy.data.node_groups: + __create_refl_normal_group__() + + return bpy.data.node_groups[REFL_NORMAL_G] + + +def __create_refl_normal_group__(): + """Create reflection normal group. + + Inputs: Position, Normal + Outputs: Reflection Normal Vector + """ + + refl_normal_g = bpy.data.node_groups.new(type="ShaderNodeTree", name=REFL_NORMAL_G) + + # inputs defining + refl_normal_g.inputs.new("NodeSocketVector", "Incoming") + refl_normal_g.inputs.new("NodeSocketVector", "Normal") + input_n = refl_normal_g.nodes.new("NodeGroupInput") + input_n.location = (0, 0) + + # outputs defining + refl_normal_g.outputs.new("NodeSocketVector", "Reflection Normal") + output_n = refl_normal_g.nodes.new("NodeGroupOutput") + output_n.location = (185 * 7, 0) + + # group nodes + view_vector_n = refl_normal_g.nodes.new("ShaderNodeVectorMath") + view_vector_n.location = (185, 250) + view_vector_n.operation = "MULTIPLY" + view_vector_n.inputs[1].default_value = (-1,) * 3 + + view_vector_norm_n = refl_normal_g.nodes.new("ShaderNodeVectorMath") + view_vector_norm_n.location = (185 * 2, 250) + view_vector_norm_n.operation = "NORMALIZE" + + view_normal_dot_n = refl_normal_g.nodes.new("ShaderNodeVectorMath") + view_normal_dot_n.location = (185 * 3, 200) + view_normal_dot_n.operation = "DOT_PRODUCT" + + view_normal_dot_scaled_n = refl_normal_g.nodes.new("ShaderNodeMath") + view_normal_dot_scaled_n.location = (185 * 4, 200) + view_normal_dot_scaled_n.operation = "MULTIPLY" + view_normal_dot_scaled_n.inputs[1].default_value = 2.0 + + normal_mult_n = refl_normal_g.nodes.new("ShaderNodeVectorMath") + normal_mult_n.location = (185 * 5, 100) + normal_mult_n.operation = "MULTIPLY" + + view_normal_subtract_n = refl_normal_g.nodes.new("ShaderNodeVectorMath") + view_normal_subtract_n.location = (185 * 6, 350) + view_normal_subtract_n.operation = "SUBTRACT" + + # group links + # formula: view_v - 2*dot(view_v, normal_v) * normal_v as in GLSL reflect function + refl_normal_g.links.new(view_vector_n.inputs[0], input_n.outputs['Incoming']) + + refl_normal_g.links.new(view_vector_norm_n.inputs[0], view_vector_n.outputs['Vector']) + + refl_normal_g.links.new(view_normal_dot_n.inputs[0], view_vector_norm_n.outputs['Vector']) + refl_normal_g.links.new(view_normal_dot_n.inputs[1], input_n.outputs['Normal']) + + refl_normal_g.links.new(view_normal_dot_scaled_n.inputs[0], view_normal_dot_n.outputs['Value']) + + refl_normal_g.links.new(normal_mult_n.inputs[0], view_normal_dot_scaled_n.outputs['Value']) + refl_normal_g.links.new(normal_mult_n.inputs[1], input_n.outputs['Normal']) + + refl_normal_g.links.new(view_normal_subtract_n.inputs[0], view_vector_norm_n.outputs['Vector']) + refl_normal_g.links.new(view_normal_subtract_n.inputs[1], normal_mult_n.outputs[0]) + + refl_normal_g.links.new(output_n.inputs['Reflection Normal'], view_normal_subtract_n.outputs['Vector']) diff --git a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/scs_uvs_combine_ng.py b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/scs_uvs_combine_ng.py new file mode 100644 index 0000000..d71f541 --- /dev/null +++ b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/scs_uvs_combine_ng.py @@ -0,0 +1,85 @@ +# ##### 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 + +import bpy +from io_scs_tools.consts import Material as _MAT_consts + +SCS_UVS_COMBINE_G = _MAT_consts.node_group_prefix + "UVsCombine" + + +def get_node_group(): + """Gets node group for combining of SCS coordinate U and V into XYZ vector. + + :return: node group which calculates change factor + :rtype: bpy.types.NodeGroup + """ + + if SCS_UVS_COMBINE_G not in bpy.data.node_groups: + __create_node_group__() + + return bpy.data.node_groups[SCS_UVS_COMBINE_G] + + +def __create_node_group__(): + """Create group for combining of SCS U and V coordinates into XYZ vector. + + Inputs: U, V + Outputs: Vector + """ + + pos_x_shift = 185 + + scs_uvs_combine_g = bpy.data.node_groups.new(type="ShaderNodeTree", name=SCS_UVS_COMBINE_G) + + # inputs defining + scs_uvs_combine_g.inputs.new("NodeSocketFloat", "U") + scs_uvs_combine_g.inputs.new("NodeSocketFloat", "V") + input_n = scs_uvs_combine_g.nodes.new("NodeGroupInput") + input_n.location = (0, 0) + + # outputs defining + scs_uvs_combine_g.outputs.new("NodeSocketVector", "Vector") + output_n = scs_uvs_combine_g.nodes.new("NodeGroupOutput") + output_n.location = (pos_x_shift * 4, 0) + + # group nodes + v_to_scs_inv_n = scs_uvs_combine_g.nodes.new("ShaderNodeMath") + v_to_scs_inv_n.location = (pos_x_shift * 1, -100) + v_to_scs_inv_n.operation = "MULTIPLY" + v_to_scs_inv_n.inputs[1].default_value = -1 + + v_to_scs_add_n = scs_uvs_combine_g.nodes.new("ShaderNodeMath") + v_to_scs_add_n.location = (pos_x_shift * 2, -100) + v_to_scs_add_n.operation = "ADD" + v_to_scs_add_n.inputs[1].default_value = 1 + + combine_n = scs_uvs_combine_g.nodes.new("ShaderNodeCombineXYZ") + combine_n.location = (pos_x_shift * 3, 0) + combine_n.inputs['Z'].default_value = 0.0 + + # group links + scs_uvs_combine_g.links.new(v_to_scs_inv_n.inputs[0], input_n.outputs['V']) + + scs_uvs_combine_g.links.new(v_to_scs_add_n.inputs[0], v_to_scs_inv_n.outputs[0]) + + scs_uvs_combine_g.links.new(combine_n.inputs['X'], input_n.outputs['U']) + scs_uvs_combine_g.links.new(combine_n.inputs['Y'], v_to_scs_add_n.outputs[0]) + + scs_uvs_combine_g.links.new(output_n.inputs['Vector'], combine_n.outputs[0]) diff --git a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/scs_uvs_separate_ng.py b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/scs_uvs_separate_ng.py new file mode 100644 index 0000000..0bc4f90 --- /dev/null +++ b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/scs_uvs_separate_ng.py @@ -0,0 +1,84 @@ +# ##### 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 + +import bpy +from io_scs_tools.consts import Material as _MAT_consts + +SCS_UVS_SEPARATE_G = _MAT_consts.node_group_prefix + "UVsSeparate" + + +def get_node_group(): + """Gets node group for separation of UVs into SCS coordinate system U and V. + + :return: node group which calculates change factor + :rtype: bpy.types.NodeGroup + """ + + if SCS_UVS_SEPARATE_G not in bpy.data.node_groups: + __create_node_group__() + + return bpy.data.node_groups[SCS_UVS_SEPARATE_G] + + +def __create_node_group__(): + """Create group for separation of UVs to SCS coordinate system. + + Inputs: UV + Outputs: U, V + """ + + pos_x_shift = 185 + + scs_uvs_separate_g = bpy.data.node_groups.new(type="ShaderNodeTree", name=SCS_UVS_SEPARATE_G) + + # inputs defining + scs_uvs_separate_g.inputs.new("NodeSocketVector", "UV") + input_n = scs_uvs_separate_g.nodes.new("NodeGroupInput") + input_n.location = (0, 0) + + # outputs defining + scs_uvs_separate_g.outputs.new("NodeSocketFloat", "U") + scs_uvs_separate_g.outputs.new("NodeSocketFloat", "V") + output_n = scs_uvs_separate_g.nodes.new("NodeGroupOutput") + output_n.location = (pos_x_shift * 4, 0) + + # group nodes + separate_xyz_n = scs_uvs_separate_g.nodes.new("ShaderNodeSeparateXYZ") + separate_xyz_n.location = (pos_x_shift * 1, 0) + + v_to_scs_inv_n = scs_uvs_separate_g.nodes.new("ShaderNodeMath") + v_to_scs_inv_n.location = (pos_x_shift * 2, -100) + v_to_scs_inv_n.operation = "MULTIPLY" + v_to_scs_inv_n.inputs[1].default_value = -1 + + v_to_scs_add_n = scs_uvs_separate_g.nodes.new("ShaderNodeMath") + v_to_scs_add_n.location = (pos_x_shift * 3, -100) + v_to_scs_add_n.operation = "ADD" + v_to_scs_add_n.inputs[1].default_value = 1 + + # group links + scs_uvs_separate_g.links.new(separate_xyz_n.inputs[0], input_n.outputs['UV']) + + scs_uvs_separate_g.links.new(v_to_scs_inv_n.inputs[0], separate_xyz_n.outputs['Y']) + + scs_uvs_separate_g.links.new(v_to_scs_add_n.inputs[0], v_to_scs_inv_n.outputs[0]) + + scs_uvs_separate_g.links.new(output_n.inputs['U'], separate_xyz_n.outputs['X']) + scs_uvs_separate_g.links.new(output_n.inputs['V'], v_to_scs_add_n.outputs[0]) diff --git a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/vcolor_input.py b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/vcolor_input.py deleted file mode 100644 index 418109f..0000000 --- a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/vcolor_input.py +++ /dev/null @@ -1,131 +0,0 @@ -# ##### 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) 2015: SCS Software - -import bpy -from io_scs_tools.consts import Colors as _COL_consts -from io_scs_tools.consts import Material as _MAT_consts -from io_scs_tools.consts import Mesh as _MESH_consts - -VCOLOR_G = _MAT_consts.node_group_prefix + "VColorGroup" - -_VCOL_GEOM_N = "VertexColorGeom" -_VCOL_GEOM_A_N = "VertexColorAlphaGeom" -_VCOL_GAMMA_CORR_N = "VertexColorGamma" -_VCOL_GAMMA_CORR_A_N = "VertexColorAGamma" -_VCOL_SATURATE_N = "VertexColorSaturation" -_ALPHA_TO_BW_N = "VertexColAToBW" -_VCOL_NORM_N = "VertexColorNormalize" # some kind of HDR compensation -_VCOL_NORM_A_N = "VertexColorAlphaNormalize" - - -def get_node_group(): - """Gets node group for vertex color inputs. - - :return: node group which exposes vertex color input - :rtype: bpy.types.NodeGroup - """ - - if VCOLOR_G not in bpy.data.node_groups: - __create_vcolor_group__() - - return bpy.data.node_groups[VCOLOR_G] - - -def __create_vcolor_group__(): - """Creates vertex color input group. - - Inputs: None - Outputs: Vertex Color, Vertex Color Alpha - """ - - vcol_g = bpy.data.node_groups.new(type="ShaderNodeTree", name=VCOLOR_G) - - # outputs defining - vcol_g.outputs.new("NodeSocketColor", "Vertex Color") - vcol_g.outputs.new("NodeSocketFloat", "Vertex Color Alpha") - output_n = vcol_g.nodes.new("NodeGroupOutput") - output_n.location = (185 * 5, 0) - - # group nodes - vcol_geom_n = vcol_g.nodes.new("ShaderNodeGeometry") - vcol_geom_n.name = _VCOL_GEOM_N - vcol_geom_n.label = _VCOL_GEOM_N - vcol_geom_n.location = (185, 200) - vcol_geom_n.color_layer = _MESH_consts.default_vcol - - vcol_a_geom_n = vcol_g.nodes.new("ShaderNodeGeometry") - vcol_a_geom_n.name = _VCOL_GEOM_A_N - vcol_a_geom_n.label = _VCOL_GEOM_A_N - vcol_a_geom_n.location = (185, -100) - vcol_a_geom_n.color_layer = _MESH_consts.default_vcol + _MESH_consts.vcol_a_suffix - - vcol_gamma_corr_n = vcol_g.nodes.new("ShaderNodeGamma") - vcol_gamma_corr_n.name = _VCOL_GAMMA_CORR_N - vcol_gamma_corr_n.label = _VCOL_GAMMA_CORR_N - vcol_gamma_corr_n.location = (185 * 2, 100) - vcol_gamma_corr_n.inputs["Gamma"].default_value = 1 / _COL_consts.gamma - - vcol_gamma_corr_a_n = vcol_g.nodes.new("ShaderNodeGamma") - vcol_gamma_corr_a_n.name = _VCOL_GAMMA_CORR_A_N - vcol_gamma_corr_a_n.label = _VCOL_GAMMA_CORR_A_N - vcol_gamma_corr_a_n.location = (185 * 2, -100) - vcol_gamma_corr_a_n.inputs["Gamma"].default_value = 1 / _COL_consts.gamma - - vcol_saturate_n = vcol_g.nodes.new("ShaderNodeHueSaturation") - vcol_saturate_n.name = _VCOL_SATURATE_N - vcol_saturate_n.label = _VCOL_SATURATE_N - vcol_saturate_n.inputs['Hue'].default_value = 0.5 - vcol_saturate_n.inputs['Saturation'].default_value = _COL_consts.saturation - vcol_saturate_n.inputs['Value'].default_value = 1 - vcol_saturate_n.inputs['Fac'].default_value = 1 - vcol_saturate_n.location = (185 * 3, 100) - - alpha_to_bw_n = vcol_g.nodes.new("ShaderNodeRGBToBW") - alpha_to_bw_n.name = _ALPHA_TO_BW_N - alpha_to_bw_n.label = _ALPHA_TO_BW_N - alpha_to_bw_n.location = (185 * 3, -100) - - normalize_vcol_n = vcol_g.nodes.new("ShaderNodeMixRGB") - normalize_vcol_n.name = _VCOL_NORM_N - normalize_vcol_n.label = _VCOL_NORM_N - normalize_vcol_n.location = (185 * 4, 100) - normalize_vcol_n.blend_type = "MULTIPLY" - normalize_vcol_n.inputs["Fac"].default_value = 1 - normalize_vcol_n.inputs["Color2"].default_value = (2.0,) * 4 - - normalize_vcol_a_n = vcol_g.nodes.new("ShaderNodeMath") - normalize_vcol_a_n.name = _VCOL_NORM_A_N - normalize_vcol_a_n.label = _VCOL_NORM_A_N - normalize_vcol_a_n.location = (185 * 4, -100) - normalize_vcol_a_n.operation = "MULTIPLY" - normalize_vcol_a_n.inputs[1].default_value = 2.0 - - # group links - vcol_g.links.new(vcol_gamma_corr_n.inputs["Color"], vcol_geom_n.outputs["Vertex Color"]) - vcol_g.links.new(vcol_gamma_corr_a_n.inputs["Color"], vcol_a_geom_n.outputs["Vertex Color"]) - - vcol_g.links.new(vcol_saturate_n.inputs["Color"], vcol_gamma_corr_n.outputs["Color"]) - vcol_g.links.new(alpha_to_bw_n.inputs["Color"], vcol_gamma_corr_a_n.outputs["Color"]) - - vcol_g.links.new(normalize_vcol_n.inputs["Color1"], vcol_saturate_n.outputs["Color"]) - vcol_g.links.new(normalize_vcol_a_n.inputs[0], alpha_to_bw_n.outputs["Val"]) - - vcol_g.links.new(output_n.inputs["Vertex Color"], normalize_vcol_n.outputs["Color"]) - vcol_g.links.new(output_n.inputs["Vertex Color Alpha"], normalize_vcol_a_n.outputs["Value"]) diff --git a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/vcolor_input_ng.py b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/vcolor_input_ng.py new file mode 100644 index 0000000..2c333cd --- /dev/null +++ b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/vcolor_input_ng.py @@ -0,0 +1,139 @@ +# ##### 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) 2015-2019: SCS Software + +import bpy +from io_scs_tools.internals.shaders.eut2.std_node_groups import linear_to_srgb_ng +from io_scs_tools.consts import Material as _MAT_consts +from io_scs_tools.consts import Mesh as _MESH_consts + +VCOLOR_G = _MAT_consts.node_group_prefix + "VColorGroup" + +_VCOL_ATTRIBUTE_NODE = "VertexColor" +_VCOL_ATTRIBUTE_A_NODE = "VertexColorAlpha" + +_VCOL_SEPARATE_NODE = "VertexColorToRGB" +_ALPHA_TO_BW_NODE = "VertexColAToBW" + +_VCOL_R_LIN_TO_SRGB_NODE = "RedLinearToSRGB" +_VCOL_G_LIN_TO_SRGB_NODE = "GreenLinearToSRGB" +_VCOL_B_LIN_TO_SRGB_NODE = "BlueLinearToSRGB" +_ALPHA_LIN_TO_SRGB_NODE = "ALphaLinearToSRGB" + +_ALPHA_EXTEND_NODE = "AlphaExtend" + +_VCOL_COMBINE_NODE = "VertexColorRGBCombine" + + +def get_node_group(): + """Gets node group for vertex color inputs. + + :return: node group which exposes vertex color input + :rtype: bpy.types.NodeGroup + """ + + if VCOLOR_G not in bpy.data.node_groups: + __create_vcolor_group__() + + return bpy.data.node_groups[VCOLOR_G] + + +def __create_vcolor_group__(): + """Creates vertex color input group. + + Inputs: None + Outputs: Vertex Color, Vertex Color Alpha + """ + + pos_x_shift = 185 + + vcol_g = bpy.data.node_groups.new(type="ShaderNodeTree", name=VCOLOR_G) + + # outputs defining + vcol_g.outputs.new("NodeSocketColor", "Vertex Color") + vcol_g.outputs.new("NodeSocketFloat", "Vertex Color Alpha") + output_n = vcol_g.nodes.new("NodeGroupOutput") + output_n.location = (185 * 5, 0) + + # group nodes + vcol_n = vcol_g.nodes.new("ShaderNodeVertexColor") + vcol_n.name = vcol_n.label = _VCOL_ATTRIBUTE_NODE + vcol_n.location = (pos_x_shift, 200) + vcol_n.layer_name = _MESH_consts.default_vcol + + vcol_a_n = vcol_g.nodes.new("ShaderNodeVertexColor") + vcol_a_n.name = vcol_a_n.label = _VCOL_ATTRIBUTE_A_NODE + vcol_a_n.location = (pos_x_shift, -100) + vcol_a_n.layer_name = _MESH_consts.default_vcol + _MESH_consts.vcol_a_suffix + + vcol_separate_rgb_n = vcol_g.nodes.new("ShaderNodeSeparateRGB") + vcol_separate_rgb_n.name = vcol_separate_rgb_n.label = _VCOL_SEPARATE_NODE + vcol_separate_rgb_n.location = (pos_x_shift * 2, 200) + + alpha_to_bw_n = vcol_g.nodes.new("ShaderNodeRGBToBW") + alpha_to_bw_n.name = alpha_to_bw_n.label = _ALPHA_TO_BW_NODE + alpha_to_bw_n.location = (pos_x_shift * 2, -100) + + vcol_r_lin_to_srgb_n = vcol_g.nodes.new("ShaderNodeGroup") + vcol_r_lin_to_srgb_n.name = vcol_r_lin_to_srgb_n.label = _VCOL_R_LIN_TO_SRGB_NODE + vcol_r_lin_to_srgb_n.location = (pos_x_shift * 3, 350) + vcol_r_lin_to_srgb_n.node_tree = linear_to_srgb_ng.get_node_group() + + vcol_g_lin_to_srgb_n = vcol_g.nodes.new("ShaderNodeGroup") + vcol_g_lin_to_srgb_n.name = vcol_g_lin_to_srgb_n.label = _VCOL_G_LIN_TO_SRGB_NODE + vcol_g_lin_to_srgb_n.location = (pos_x_shift * 3, 200) + vcol_g_lin_to_srgb_n.node_tree = linear_to_srgb_ng.get_node_group() + + vcol_b_lin_to_srgb_n = vcol_g.nodes.new("ShaderNodeGroup") + vcol_b_lin_to_srgb_n.name = vcol_b_lin_to_srgb_n.label = _VCOL_B_LIN_TO_SRGB_NODE + vcol_b_lin_to_srgb_n.location = (pos_x_shift * 3, 50) + vcol_b_lin_to_srgb_n.node_tree = linear_to_srgb_ng.get_node_group() + + alpha_lin_to_srgb_n = vcol_g.nodes.new("ShaderNodeGroup") + alpha_lin_to_srgb_n.name = alpha_lin_to_srgb_n.label = _ALPHA_LIN_TO_SRGB_NODE + alpha_lin_to_srgb_n.location = (pos_x_shift * 3, -100) + alpha_lin_to_srgb_n.node_tree = linear_to_srgb_ng.get_node_group() + + alpha_extend_n = vcol_g.nodes.new("ShaderNodeMath") + alpha_extend_n.name = alpha_extend_n.label = _ALPHA_EXTEND_NODE + alpha_extend_n.location = (pos_x_shift * 4, -100) + alpha_extend_n.operation = "MULTIPLY" + alpha_extend_n.inputs[1].default_value = 2.0 + + vcol_combine_n = vcol_g.nodes.new("ShaderNodeCombineRGB") + vcol_combine_n.name = vcol_combine_n.label = _VCOL_COMBINE_NODE + vcol_combine_n.location = (pos_x_shift * 4, 200) + + # group links + vcol_g.links.new(vcol_separate_rgb_n.inputs['Image'], vcol_n.outputs['Color']) + vcol_g.links.new(alpha_to_bw_n.inputs["Color"], vcol_a_n.outputs['Color']) + + vcol_g.links.new(vcol_r_lin_to_srgb_n.inputs['Value'], vcol_separate_rgb_n.outputs['R']) + vcol_g.links.new(vcol_g_lin_to_srgb_n.inputs['Value'], vcol_separate_rgb_n.outputs['G']) + vcol_g.links.new(vcol_b_lin_to_srgb_n.inputs['Value'], vcol_separate_rgb_n.outputs['B']) + vcol_g.links.new(alpha_lin_to_srgb_n.inputs['Value'], alpha_to_bw_n.outputs['Val']) + + vcol_g.links.new(vcol_combine_n.inputs['R'], vcol_r_lin_to_srgb_n.outputs["Value"]) + vcol_g.links.new(vcol_combine_n.inputs['G'], vcol_g_lin_to_srgb_n.outputs["Value"]) + vcol_g.links.new(vcol_combine_n.inputs['B'], vcol_b_lin_to_srgb_n.outputs["Value"]) + + vcol_g.links.new(alpha_extend_n.inputs[0], alpha_lin_to_srgb_n.outputs['Value']) + + vcol_g.links.new(output_n.inputs['Vertex Color'], vcol_combine_n.outputs['Image']) + vcol_g.links.new(output_n.inputs['Vertex Color Alpha'], alpha_extend_n.outputs['Value']) diff --git a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/window_uv_factor.py b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/window_uv_factor.py deleted file mode 100644 index e7b41c8..0000000 --- a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/window_uv_factor.py +++ /dev/null @@ -1,127 +0,0 @@ -# ##### 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) 2015: SCS Software - -import bpy -from io_scs_tools.consts import Material as _MAT_consts - -WINDOW_UV_FACTOR_G = _MAT_consts.node_group_prefix + "WindowUVFactorGroup" - - -def get_node_group(): - """Gets node group for calculation of uv change factor. - For more information take a look at: eut2.window.cg - - :return: node group which calculates change factor - :rtype: bpy.types.NodeGroup - """ - - if WINDOW_UV_FACTOR_G not in bpy.data.node_groups: - __create_uv_factor_group__() - - return bpy.data.node_groups[WINDOW_UV_FACTOR_G] - - -def __create_uv_factor_group__(): - """Create UV factor group. - - Inputs: UV, Factor - Outputs: UV Offset - """ - - pos_x_shift = 185 - - uv_factor_g = bpy.data.node_groups.new(type="ShaderNodeTree", name=WINDOW_UV_FACTOR_G) - - # inputs defining - uv_factor_g.inputs.new("NodeSocketFloat", "UV") - uv_factor_g.inputs.new("NodeSocketFloat", "Factor") - input_n = uv_factor_g.nodes.new("NodeGroupInput") - input_n.location = (0, 0) - - # outputs defining - uv_factor_g.outputs.new("NodeSocketFloat", "UV Offset") - output_n = uv_factor_g.nodes.new("NodeGroupOutput") - output_n.location = (pos_x_shift * 6, 0) - - # group nodes - # FLOOR = floor(uv * 0.5) - half_scale_n = uv_factor_g.nodes.new("ShaderNodeMath") - half_scale_n.location = (pos_x_shift * 1, 100) - half_scale_n.operation = "MULTIPLY" - half_scale_n.inputs[1].default_value = 0.5 - - floor_add_n = uv_factor_g.nodes.new("ShaderNodeMath") - floor_add_n.location = (pos_x_shift * 2, 100) - floor_add_n.operation = "ADD" - floor_add_n.inputs[1].default_value = -0.5 - - floor_round_n = uv_factor_g.nodes.new("ShaderNodeMath") - floor_round_n.location = (pos_x_shift * 3, 100) - floor_round_n.operation = "ROUND" - - # MIN_MAX_FACTOR = min_max(factor * (-1.333)) - factor_expand_n = uv_factor_g.nodes.new("ShaderNodeMath") - factor_expand_n.location = (pos_x_shift * 1, -100) - factor_expand_n.operation = "MULTIPLY" - factor_expand_n.inputs[1].default_value = -1.333 - - max_factor_n = uv_factor_g.nodes.new("ShaderNodeMath") - max_factor_n.location = (pos_x_shift * 2, -100) - max_factor_n.operation = "MAXIMUM" - max_factor_n.inputs[1].default_value = -1.0 - - min_factor_n = uv_factor_g.nodes.new("ShaderNodeMath") - min_factor_n.location = (pos_x_shift * 3, -100) - min_factor_n.operation = "MINIMUM" - min_factor_n.inputs[1].default_value = 1.0 - - # 1/512 * FLOOR * MIN_MAX_FACTOR - offset_step_n = uv_factor_g.nodes.new("ShaderNodeMath") - offset_step_n.location = (pos_x_shift * 3, 300) - offset_step_n.operation = "DIVIDE" - offset_step_n.inputs[0].default_value = 1 - offset_step_n.inputs[1].default_value = 512 - - mult1_n = uv_factor_g.nodes.new("ShaderNodeMath") - mult1_n.location = (pos_x_shift * 4, 200) - mult1_n.operation = "MULTIPLY" - - mult2_n = uv_factor_g.nodes.new("ShaderNodeMath") - mult2_n.location = (pos_x_shift * 5, 0) - mult2_n.operation = "MULTIPLY" - - # group links - # formula: 1/512 * floor(uv * 0.5) * min_max(factor * -1.333) - uv_factor_g.links.new(half_scale_n.inputs[0], input_n.outputs['UV']) - uv_factor_g.links.new(factor_expand_n.inputs[0], input_n.outputs['Factor']) - - uv_factor_g.links.new(floor_add_n.inputs[0], half_scale_n.outputs[0]) - uv_factor_g.links.new(max_factor_n.inputs[0], factor_expand_n.outputs[0]) - - uv_factor_g.links.new(floor_round_n.inputs[0], floor_add_n.outputs[0]) - uv_factor_g.links.new(min_factor_n.inputs[0], max_factor_n.outputs[0]) - - uv_factor_g.links.new(mult1_n.inputs[0], offset_step_n.outputs[0]) - uv_factor_g.links.new(mult1_n.inputs[1], floor_round_n.outputs[0]) - - uv_factor_g.links.new(mult2_n.inputs[0], mult1_n.outputs[0]) - uv_factor_g.links.new(mult2_n.inputs[1], min_factor_n.outputs[0]) - - uv_factor_g.links.new(output_n.inputs[0], mult2_n.outputs[0]) diff --git a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/window_uv_offset.py b/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/window_uv_offset.py deleted file mode 100644 index fb9a071..0000000 --- a/addon/io_scs_tools/internals/shaders/eut2/std_node_groups/window_uv_offset.py +++ /dev/null @@ -1,187 +0,0 @@ -# ##### 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) 2015: SCS Software - -import bpy -from io_scs_tools.consts import Material as _MAT_consts -from io_scs_tools.internals.shaders.eut2.std_node_groups import window_uv_factor - -WINDOW_UV_OFFSET_G = _MAT_consts.node_group_prefix + "WindowUVOffsetGroup" - - -def get_node_group(): - """Gets node group for uv offseting based on view angle. - For more information take a look at: eut2.window.cg - - NOTE: Due to lack of data view angle is calclulated - based on normal of the mesh face. - :return: node group which calculates change factor - :rtype: bpy.types.NodeGroup - """ - - if WINDOW_UV_OFFSET_G not in bpy.data.node_groups: - __create_uv_offset_group__() - - return bpy.data.node_groups[WINDOW_UV_OFFSET_G] - - -def __create_uv_offset_group__(): - """Create UV factor group. - - Inputs: UV, Normal - Outputs: UV Final - """ - - pos_x_shift = 185 - - uv_offset_g = bpy.data.node_groups.new(type="ShaderNodeTree", name=WINDOW_UV_OFFSET_G) - - # inputs defining - uv_offset_g.inputs.new("NodeSocketVector", "UV") - uv_offset_g.inputs.new("NodeSocketVector", "Normal") - input_n = uv_offset_g.nodes.new("NodeGroupInput") - input_n.location = (0, 0) - - # outputs defining - uv_offset_g.outputs.new("NodeSocketVector", "UV Final") - output_n = uv_offset_g.nodes.new("NodeGroupOutput") - output_n.location = (pos_x_shift * 14, 0) - - # group nodes - separate_normal_n = uv_offset_g.nodes.new("ShaderNodeSeparateRGB") - separate_normal_n.location = (pos_x_shift * 1, 100) - - combine_normal_n = uv_offset_g.nodes.new("ShaderNodeCombineRGB") - combine_normal_n.location = (pos_x_shift * 2, -100) - combine_normal_n.inputs[1].default_value = 0.0 - combine_normal_n.inputs[2].default_value = 0.0 - - normal_cross_n = uv_offset_g.nodes.new("ShaderNodeVectorMath") - normal_cross_n.location = (pos_x_shift * 3, -100) - normal_cross_n.operation = "CROSS_PRODUCT" - - separate_cross_n = uv_offset_g.nodes.new("ShaderNodeSeparateRGB") - separate_cross_n.location = (pos_x_shift * 4, -100) - - inv_cross_z_n = uv_offset_g.nodes.new("ShaderNodeMath") - inv_cross_z_n.location = (pos_x_shift * 5, -200) - inv_cross_z_n.operation = "MULTIPLY" - inv_cross_z_n.inputs[1].default_value = -1.0 - - gt_normal_y_n = uv_offset_g.nodes.new("ShaderNodeMath") - gt_normal_y_n.location = (pos_x_shift * 5, -400) - gt_normal_y_n.operation = "GREATER_THAN" - gt_normal_y_n.inputs[1].default_value = 0.0 - - lt_normal_y_n = uv_offset_g.nodes.new("ShaderNodeMath") - lt_normal_y_n.location = (pos_x_shift * 5, -600) - lt_normal_y_n.operation = "LESS_THAN" - lt_normal_y_n.inputs[1].default_value = 0.0 - - max_cross_z_n = uv_offset_g.nodes.new("ShaderNodeMath") - max_cross_z_n.location = (pos_x_shift * 6, -100) - max_cross_z_n.operation = "MAXIMUM" - - mult_gt_n = uv_offset_g.nodes.new("ShaderNodeMath") - mult_gt_n.location = (pos_x_shift * 7, -200) - mult_gt_n.operation = "MULTIPLY" - - mult_lt_n = uv_offset_g.nodes.new("ShaderNodeMath") - mult_lt_n.location = (pos_x_shift * 7, -400) - mult_lt_n.operation = "MULTIPLY" - - inv_mult_gt_n = uv_offset_g.nodes.new("ShaderNodeMath") - inv_mult_gt_n.location = (pos_x_shift * 8, -200) - inv_mult_gt_n.operation = "MULTIPLY" - inv_mult_gt_n.inputs[1].default_value = -1.0 - - sum_lt_gt_n = uv_offset_g.nodes.new("ShaderNodeMath") - sum_lt_gt_n.location = (pos_x_shift * 9, -300) - sum_lt_gt_n.operation = "ADD" - - inv_y_factor_n = uv_offset_g.nodes.new("ShaderNodeMath") - inv_y_factor_n.location = (pos_x_shift * 10, -300) - inv_y_factor_n.operation = "MULTIPLY" - inv_y_factor_n.inputs[1].default_value = -1.0 - - separate_uv_n = uv_offset_g.nodes.new("ShaderNodeSeparateRGB") - separate_uv_n.location = (pos_x_shift * 10, 100) - - u_factor_gn = uv_offset_g.nodes.new("ShaderNodeGroup") - u_factor_gn.name = window_uv_factor.WINDOW_UV_FACTOR_G - u_factor_gn.label = window_uv_factor.WINDOW_UV_FACTOR_G - u_factor_gn.location = (pos_x_shift * 11, 300) - u_factor_gn.node_tree = window_uv_factor.get_node_group() - - v_factor_gn = uv_offset_g.nodes.new("ShaderNodeGroup") - v_factor_gn.name = window_uv_factor.WINDOW_UV_FACTOR_G - v_factor_gn.label = window_uv_factor.WINDOW_UV_FACTOR_G - v_factor_gn.location = (pos_x_shift * 11, -100) - v_factor_gn.node_tree = window_uv_factor.get_node_group() - - combine_uv_n = uv_offset_g.nodes.new("ShaderNodeCombineRGB") - combine_uv_n.location = (pos_x_shift * 12, 100) - combine_uv_n.inputs[2].default_value = 0.0 - - add_offset_n = uv_offset_g.nodes.new("ShaderNodeVectorMath") - add_offset_n.location = (pos_x_shift * 13, 100) - add_offset_n.operation = "ADD" - - # group links - uv_offset_g.links.new(separate_normal_n.inputs[0], input_n.outputs["Normal"]) - - uv_offset_g.links.new(combine_normal_n.inputs[0], separate_normal_n.outputs[0]) - - uv_offset_g.links.new(normal_cross_n.inputs[0], combine_normal_n.outputs[0]) - uv_offset_g.links.new(normal_cross_n.inputs[1], input_n.outputs["Normal"]) - - uv_offset_g.links.new(separate_cross_n.inputs[0], normal_cross_n.outputs[0]) - - uv_offset_g.links.new(inv_cross_z_n.inputs[0], separate_cross_n.outputs[2]) - uv_offset_g.links.new(gt_normal_y_n.inputs[0], separate_normal_n.outputs[1]) - uv_offset_g.links.new(lt_normal_y_n.inputs[0], separate_normal_n.outputs[1]) - - uv_offset_g.links.new(max_cross_z_n.inputs[0], separate_cross_n.outputs[2]) - uv_offset_g.links.new(max_cross_z_n.inputs[1], inv_cross_z_n.outputs[0]) - - uv_offset_g.links.new(mult_gt_n.inputs[0], max_cross_z_n.outputs[0]) - uv_offset_g.links.new(mult_gt_n.inputs[1], gt_normal_y_n.outputs[0]) - uv_offset_g.links.new(mult_lt_n.inputs[0], max_cross_z_n.outputs[0]) - uv_offset_g.links.new(mult_lt_n.inputs[1], lt_normal_y_n.outputs[0]) - - uv_offset_g.links.new(inv_mult_gt_n.inputs[0], mult_gt_n.outputs[0]) - - uv_offset_g.links.new(sum_lt_gt_n.inputs[0], inv_mult_gt_n.outputs[0]) - uv_offset_g.links.new(sum_lt_gt_n.inputs[1], mult_lt_n.outputs[0]) - - uv_offset_g.links.new(separate_uv_n.inputs[0], input_n.outputs["UV"]) - uv_offset_g.links.new(inv_y_factor_n.inputs[0], sum_lt_gt_n.outputs[0]) - - uv_offset_g.links.new(u_factor_gn.inputs["UV"], separate_uv_n.outputs[0]) - uv_offset_g.links.new(u_factor_gn.inputs["Factor"], separate_normal_n.outputs[0]) - uv_offset_g.links.new(v_factor_gn.inputs["UV"], separate_uv_n.outputs[1]) - uv_offset_g.links.new(v_factor_gn.inputs["Factor"], inv_y_factor_n.outputs[0]) - - uv_offset_g.links.new(combine_uv_n.inputs[0], u_factor_gn.outputs["UV Offset"]) - uv_offset_g.links.new(combine_uv_n.inputs[1], v_factor_gn.outputs["UV Offset"]) - - uv_offset_g.links.new(add_offset_n.inputs[0], combine_uv_n.outputs[0]) - uv_offset_g.links.new(add_offset_n.inputs[1], input_n.outputs["UV"]) - - uv_offset_g.links.new(output_n.inputs["UV Final"], add_offset_n.outputs[0]) diff --git a/addon/io_scs_tools/internals/shaders/eut2/std_passes/add_env.py b/addon/io_scs_tools/internals/shaders/eut2/std_passes/add_env.py index 97ee123..779aba7 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/std_passes/add_env.py +++ b/addon/io_scs_tools/internals/shaders/eut2/std_passes/add_env.py @@ -16,14 +16,15 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software - -from io_scs_tools.internals.shaders.eut2.std_node_groups import add_env +from io_scs_tools.internals.shaders.eut2.std_node_groups import add_env_ng +from io_scs_tools.internals.shaders.eut2.std_node_groups import refl_normal_ng from io_scs_tools.utils import convert as _convert_utils class StdAddEnv: + REFL_NORMAL_NODE = "ReflectionNormal" REFL_TEX_NODE = "ReflectionTex" ENV_COLOR_NODE = "EnvFactorColor" ADD_ENV_GROUP_NODE = "AddEnvGroup" @@ -34,7 +35,7 @@ def get_name(): return __name__ @staticmethod - def add(node_tree, geom_n_name, spec_col_socket, alpha_socket, out_mat_normal_socket, output_socket): + def add(node_tree, geom_n_name, spec_col_socket, alpha_socket, final_normal_socket, output_socket): """Add add env pass to node tree with links. :param node_tree: node tree on which this shader should be created @@ -45,8 +46,8 @@ def add(node_tree, geom_n_name, spec_col_socket, alpha_socket, out_mat_normal_so :type spec_col_socket: bpy.type.NodeSocket :param alpha_socket: socket from which alpha will be taken (if None it won't be used) :type alpha_socket: bpy.type.NodeSocket | None - :param out_mat_normal_socket: socket of output material node normal - :type out_mat_normal_socket: bpy.type.NodeSocket | None + :param final_normal_socket: socket of final normal, if not provided geometry normal is used + :type final_normal_socket: bpy.type.NodeSocket | None :param output_socket: output socket to which result will be given :type output_socket: bpy.type.NodeSocket """ @@ -59,53 +60,77 @@ def add(node_tree, geom_n_name, spec_col_socket, alpha_socket, out_mat_normal_so geometry_n = node_tree.nodes[geom_n_name] # node creation - refl_tex_n = node_tree.nodes.new("ShaderNodeTexture") + refl_normal_n = node_tree.nodes.new("ShaderNodeGroup") + refl_normal_n.name = refl_normal_n.label = StdAddEnv.REFL_NORMAL_NODE + refl_normal_n.location = (start_pos_x, start_pos_y + 2500) + refl_normal_n.node_tree = refl_normal_ng.get_node_group() + + refl_tex_n = node_tree.nodes.new("ShaderNodeTexEnvironment") refl_tex_n.name = refl_tex_n.label = StdAddEnv.REFL_TEX_NODE refl_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 2500) + refl_tex_n.width = 140 env_col_n = node_tree.nodes.new("ShaderNodeRGB") env_col_n.name = env_col_n.label = StdAddEnv.ENV_COLOR_NODE env_col_n.location = (start_pos_x + pos_x_shift, start_pos_y + 2200) - add_env_gn = node_tree.nodes.new("ShaderNodeGroup") - add_env_gn.name = add_env_gn.label = StdAddEnv.ADD_ENV_GROUP_NODE - add_env_gn.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 2300) - add_env_gn.node_tree = add_env.get_node_group() - add_env_gn.inputs['Apply Fresnel'].default_value = 1.0 - add_env_gn.inputs['Fresnel Scale'].default_value = 0.9 - add_env_gn.inputs['Fresnel Bias'].default_value = 0.2 - add_env_gn.inputs['Base Texture Alpha'].default_value = 0.5 + add_env_n = node_tree.nodes.new("ShaderNodeGroup") + add_env_n.name = add_env_n.label = StdAddEnv.ADD_ENV_GROUP_NODE + add_env_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 2300) + add_env_n.node_tree = add_env_ng.get_node_group() + add_env_n.inputs['Apply Fresnel'].default_value = 1.0 + add_env_n.inputs['Fresnel Scale'].default_value = 0.9 + add_env_n.inputs['Fresnel Bias'].default_value = 0.2 + add_env_n.inputs['Base Texture Alpha'].default_value = 0.5 + add_env_n.inputs['Weighted Color'].default_value = (1.0,) * 4 + add_env_n.inputs['Strength Multiplier'].default_value = 1.0 # geometry links - node_tree.links.new(add_env_gn.inputs['Normal Vector'], geometry_n.outputs['Normal']) - node_tree.links.new(add_env_gn.inputs['View Vector'], geometry_n.outputs['View']) - node_tree.links.new(refl_tex_n.inputs['Vector'], geometry_n.outputs['Normal']) + node_tree.links.new(refl_normal_n.inputs['Incoming'], geometry_n.outputs['Incoming']) + node_tree.links.new(refl_normal_n.inputs['Normal'], geometry_n.outputs['Normal']) + + node_tree.links.new(refl_tex_n.inputs['Vector'], refl_normal_n.outputs['Reflection Normal']) + + node_tree.links.new(add_env_n.inputs['Normal Vector'], geometry_n.outputs['Normal']) + node_tree.links.new(add_env_n.inputs['Reflection Normal Vector'], refl_normal_n.outputs['Reflection Normal']) # if out material node is really material node and has normal output, # use it as this normal might include normal maps - if out_mat_normal_socket is not None: - node_tree.links.new(refl_tex_n.inputs['Vector'], out_mat_normal_socket) + if final_normal_socket is not None: + node_tree.links.new(refl_normal_n.inputs['Normal'], final_normal_socket) + node_tree.links.new(add_env_n.inputs['Normal Vector'], final_normal_socket) - node_tree.links.new(add_env_gn.inputs['Env Factor Color'], env_col_n.outputs['Color']) - node_tree.links.new(add_env_gn.inputs['Reflection Texture Color'], refl_tex_n.outputs['Color']) + node_tree.links.new(add_env_n.inputs['Env Factor Color'], env_col_n.outputs['Color']) + node_tree.links.new(add_env_n.inputs['Reflection Texture Color'], refl_tex_n.outputs['Color']) - node_tree.links.new(add_env_gn.inputs['Specular Color'], spec_col_socket) - if add_env_gn and alpha_socket: - node_tree.links.new(add_env_gn.inputs['Base Texture Alpha'], alpha_socket) + node_tree.links.new(add_env_n.inputs['Specular Color'], spec_col_socket) + if add_env_n and alpha_socket: + node_tree.links.new(add_env_n.inputs['Base Texture Alpha'], alpha_socket) - node_tree.links.new(output_socket, add_env_gn.outputs['Environment Addition Color']) + node_tree.links.new(output_socket, add_env_n.outputs['Environment Addition Color']) @staticmethod - def set_reflection_texture(node_tree, texture): + def set_reflection_texture(node_tree, image): """Set reflection texture on shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture object which should be used for reflection - :type texture: bpy.types.Texture + :param image: texture image object which should be used for reflection + :type image: bpy.types.Image """ - node_tree.nodes[StdAddEnv.REFL_TEX_NODE].texture = texture + node_tree.nodes[StdAddEnv.REFL_TEX_NODE].image = image + + @staticmethod + def set_reflection_texture_settings(node_tree, settings): + """Set reflection texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + pass # reflection texture shouldn't use any custom settings @staticmethod def set_env_factor(node_tree, color): diff --git a/addon/io_scs_tools/internals/shaders/eut2/truckpaint/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/truckpaint/__init__.py index dc9af33..6107794 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/truckpaint/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/truckpaint/__init__.py @@ -16,17 +16,17 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.consts import Mesh as _MESH_consts from io_scs_tools.internals.shaders.eut2.dif_spec_add_env import DifSpecAddEnv from io_scs_tools.utils import convert as _convert_utils +from io_scs_tools.utils import material as _material_utils from io_scs_tools.utils import get_scs_globals as _get_scs_globals class Truckpaint(DifSpecAddEnv): - PAINT_GEOM_NODE = "PaintGeometry" + PAINT_UV_MAP_NODE = "PaintUVMap" PAINT_TEX_NODE = "PaintTex" PAINT_BASE_COL_NODE = "PaintjobBaseCol" @@ -48,7 +48,8 @@ class Truckpaint(DifSpecAddEnv): BLEND_MIX_NODE = "BlendMode" - PAINT_MULT_NODE = "PaintMultiplier" + PAINT_DIFFUSE_MULT_NODE = "PaintDiffuseMultiplier" + PAINT_SPECULAR_MULT_NODE = "PaintSpecularMultiplier" @staticmethod def get_name(): @@ -79,14 +80,14 @@ def init(node_tree): spec_mult_n = node_tree.nodes[DifSpecAddEnv.SPEC_MULT_NODE] vcol_scale_n = node_tree.nodes[DifSpecAddEnv.VCOLOR_SCALE_NODE] opacity_n = node_tree.nodes[DifSpecAddEnv.OPACITY_NODE] - out_mat_n = node_tree.nodes[DifSpecAddEnv.OUT_MAT_NODE] + lighting_eval_n = node_tree.nodes[DifSpecAddEnv.LIGHTING_EVAL_NODE] compose_lighting_n = node_tree.nodes[DifSpecAddEnv.COMPOSE_LIGHTING_NODE] output_n = node_tree.nodes[DifSpecAddEnv.OUTPUT_NODE] # move existing add_refl_gn.location.x += pos_x_shift * 3 spec_mult_n.location.x += pos_x_shift * 3 - out_mat_n.location.x += pos_x_shift * 2 + lighting_eval_n.location.x += pos_x_shift compose_lighting_n.location.x += pos_x_shift * 2 output_n.location.x += pos_x_shift * 2 @@ -94,68 +95,71 @@ def init(node_tree): node_tree.nodes[DifSpecAddEnv.ADD_ENV_GROUP_NODE].inputs['Apply Fresnel'].default_value = 1.0 # node creation - level 3 - env_vcol_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") - env_vcol_mix_n.name = Truckpaint.ENV_VCOL_MULT_NODE - env_vcol_mix_n.label = Truckpaint.ENV_VCOL_MULT_NODE + env_vcol_mix_n = node_tree.nodes.new("ShaderNodeVectorMath") + env_vcol_mix_n.name = env_vcol_mix_n.label = Truckpaint.ENV_VCOL_MULT_NODE env_vcol_mix_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 2000) - env_vcol_mix_n.blend_type = "MULTIPLY" - env_vcol_mix_n.inputs['Fac'].default_value = 1 + env_vcol_mix_n.operation = "MULTIPLY" - spec_vcol_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") - spec_vcol_mix_n.name = Truckpaint.SPEC_VCOL_MULT_NODE - spec_vcol_mix_n.label = Truckpaint.SPEC_VCOL_MULT_NODE + spec_vcol_mix_n = node_tree.nodes.new("ShaderNodeVectorMath") + spec_vcol_mix_n.name = spec_vcol_mix_n.label = Truckpaint.SPEC_VCOL_MULT_NODE spec_vcol_mix_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 1750) - spec_vcol_mix_n.blend_type = "MULTIPLY" - spec_vcol_mix_n.inputs['Fac'].default_value = 1 + spec_vcol_mix_n.operation = "MULTIPLY" opacity_vcol_n = node_tree.nodes.new("ShaderNodeMath") - opacity_vcol_n.name = Truckpaint.OPACITY_VCOL_MULT_NODE - opacity_vcol_n.label = Truckpaint.OPACITY_VCOL_MULT_NODE + opacity_vcol_n.name = opacity_vcol_n.label = Truckpaint.OPACITY_VCOL_MULT_NODE opacity_vcol_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 1300) opacity_vcol_n.operation = "MULTIPLY" + # node creation - level 4 + paint_spec_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") + paint_spec_mult_n.name = paint_spec_mult_n.label = Truckpaint.PAINT_SPECULAR_MULT_NODE + paint_spec_mult_n.location = (start_pos_x + pos_x_shift * 5, start_pos_y + 1950) + paint_spec_mult_n.blend_type = "MULTIPLY" + paint_spec_mult_n.inputs['Fac'].default_value = 0 + # node creation - level 5 base_paint_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") - base_paint_mult_n.name = Truckpaint.BASE_PAINT_MULT_NODE - base_paint_mult_n.label = Truckpaint.BASE_PAINT_MULT_NODE + base_paint_mult_n.name = base_paint_mult_n.label = Truckpaint.BASE_PAINT_MULT_NODE base_paint_mult_n.location = (start_pos_x + pos_x_shift * 6, start_pos_y + 1500) base_paint_mult_n.blend_type = "MULTIPLY" base_paint_mult_n.inputs['Fac'].default_value = 1 base_paint_mult_n.inputs['Color2'].default_value = _convert_utils.to_node_color(_get_scs_globals().base_paint_color) # node creation - level 6 - paint_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") - paint_mult_n.name = Truckpaint.PAINT_MULT_NODE - paint_mult_n.label = Truckpaint.PAINT_MULT_NODE - paint_mult_n.location = (start_pos_x + pos_x_shift * 7, start_pos_y + 1400) - paint_mult_n.blend_type = "MULTIPLY" - paint_mult_n.inputs['Fac'].default_value = 0 + paint_diff_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") + paint_diff_mult_n.name = paint_diff_mult_n.label = Truckpaint.PAINT_DIFFUSE_MULT_NODE + paint_diff_mult_n.location = (start_pos_x + pos_x_shift * 7, start_pos_y + 1400) + paint_diff_mult_n.blend_type = "MULTIPLY" + paint_diff_mult_n.inputs['Fac'].default_value = 0 # make links - level 2 - node_tree.links.new(env_vcol_mix_n.inputs['Color1'], env_color_n.outputs['Color']) - node_tree.links.new(env_vcol_mix_n.inputs['Color2'], vcol_scale_n.outputs['Color']) - - node_tree.links.new(spec_vcol_mix_n.inputs['Color1'], base_tex_n.outputs['Value']) - node_tree.links.new(spec_vcol_mix_n.inputs['Color2'], vcol_scale_n.outputs['Color']) + node_tree.links.new(env_vcol_mix_n.inputs[0], env_color_n.outputs['Color']) + node_tree.links.new(env_vcol_mix_n.inputs[1], vcol_scale_n.outputs[0]) - # make links - level 3 - node_tree.links.new(add_refl_gn.inputs['Env Factor Color'], env_vcol_mix_n.outputs['Color']) - - node_tree.links.new(spec_mult_n.inputs['Color1'], spec_color_n.outputs['Color']) - node_tree.links.new(spec_mult_n.inputs['Color2'], spec_vcol_mix_n.outputs['Color']) + node_tree.links.new(spec_vcol_mix_n.inputs[0], base_tex_n.outputs['Alpha']) + node_tree.links.new(spec_vcol_mix_n.inputs[1], vcol_scale_n.outputs[0]) - node_tree.links.new(opacity_vcol_n.inputs[0], vcol_scale_n.outputs['Color']) + node_tree.links.new(opacity_vcol_n.inputs[0], vcol_scale_n.outputs[0]) node_tree.links.new(opacity_vcol_n.inputs[1], opacity_n.outputs[0]) + # make links - level 3 + node_tree.links.new(paint_spec_mult_n.inputs['Color1'], spec_color_n.outputs['Color']) + # make links - level 4 - node_tree.links.new(base_paint_mult_n.inputs['Color1'], diff_mult_n.outputs['Color']) + node_tree.links.new(spec_mult_n.inputs[0], paint_spec_mult_n.outputs['Color']) + node_tree.links.new(spec_mult_n.inputs[1], spec_vcol_mix_n.outputs[0]) + + node_tree.links.new(base_paint_mult_n.inputs['Color1'], diff_mult_n.outputs[0]) # make links - level 5 - node_tree.links.new(paint_mult_n.inputs['Color1'], base_paint_mult_n.outputs['Color']) + node_tree.links.new(add_refl_gn.inputs['Env Factor Color'], env_vcol_mix_n.outputs[0]) + node_tree.links.new(add_refl_gn.inputs['Specular Color'], paint_spec_mult_n.outputs['Color']) + + node_tree.links.new(paint_diff_mult_n.inputs['Color1'], base_paint_mult_n.outputs['Color']) # make links - output - node_tree.links.new(out_mat_n.inputs['Color'], paint_mult_n.outputs['Color']) - node_tree.links.new(compose_lighting_n.inputs['Diffuse Color'], paint_mult_n.outputs['Color']) + # node_tree.links.new(compose_lighting_n.inputs['Specular Color'], paint_spec_mult_n.outputs['Color']) + node_tree.links.new(compose_lighting_n.inputs['Diffuse Color'], paint_diff_mult_n.outputs['Color']) @staticmethod def init_colormask_or_airbrush(node_tree): @@ -174,88 +178,78 @@ def init_colormask_or_airbrush(node_tree): if Truckpaint.BASE_PAINT_MULT_NODE in node_tree.nodes: node_tree.nodes[Truckpaint.BASE_PAINT_MULT_NODE].inputs["Fac"].default_value = 0 - paint_mult_n = node_tree.nodes[Truckpaint.PAINT_MULT_NODE] + paint_diff_mult_n = node_tree.nodes[Truckpaint.PAINT_DIFFUSE_MULT_NODE] + paint_spec_mult_n = node_tree.nodes[Truckpaint.PAINT_SPECULAR_MULT_NODE] # node creation - level 0 - paint_geom_n = node_tree.nodes.new("ShaderNodeGeometry") - paint_geom_n.name = Truckpaint.PAINT_GEOM_NODE - paint_geom_n.label = Truckpaint.PAINT_GEOM_NODE - paint_geom_n.location = (start_pos_x - pos_x_shift, start_pos_y + 950) - paint_geom_n.uv_layer = _MESH_consts.none_uv + paint_uv_map_n = node_tree.nodes.new("ShaderNodeUVMap") + paint_uv_map_n.name = paint_uv_map_n.label = Truckpaint.PAINT_UV_MAP_NODE + paint_uv_map_n.location = (start_pos_x - pos_x_shift, start_pos_y + 950) + paint_uv_map_n.uv_map = _MESH_consts.none_uv # node creation - level 1 paint_base_col_n = node_tree.nodes.new("ShaderNodeRGB") - paint_base_col_n.name = Truckpaint.PAINT_BASE_COL_NODE - paint_base_col_n.label = Truckpaint.PAINT_BASE_COL_NODE + paint_base_col_n.name = paint_base_col_n.label = Truckpaint.PAINT_BASE_COL_NODE paint_base_col_n.location = (start_pos_x + pos_x_shift, start_pos_y + 950) - paint_tex_n = node_tree.nodes.new("ShaderNodeTexture") - paint_tex_n.name = Truckpaint.PAINT_TEX_NODE - paint_tex_n.label = Truckpaint.PAINT_TEX_NODE + paint_tex_n = node_tree.nodes.new("ShaderNodeTexImage") + paint_tex_n.name = paint_tex_n.label = Truckpaint.PAINT_TEX_NODE paint_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 750) + paint_tex_n.width = 140 paint_b_col_n = node_tree.nodes.new("ShaderNodeRGB") - paint_b_col_n.name = Truckpaint.PAINT_B_COL_NODE - paint_b_col_n.label = Truckpaint.PAINT_B_COL_NODE + paint_b_col_n.name = paint_b_col_n.label = Truckpaint.PAINT_B_COL_NODE paint_b_col_n.location = (start_pos_x + pos_x_shift, start_pos_y + 450) paint_g_col_n = node_tree.nodes.new("ShaderNodeRGB") - paint_g_col_n.name = Truckpaint.PAINT_G_COL_NODE - paint_g_col_n.label = Truckpaint.PAINT_G_COL_NODE + paint_g_col_n.name = paint_g_col_n.label = Truckpaint.PAINT_G_COL_NODE paint_g_col_n.location = (start_pos_x + pos_x_shift, start_pos_y + 250) paint_r_col_n = node_tree.nodes.new("ShaderNodeRGB") - paint_r_col_n.name = Truckpaint.PAINT_R_COL_NODE - paint_r_col_n.label = Truckpaint.PAINT_R_COL_NODE + paint_r_col_n.name = paint_r_col_n.label = Truckpaint.PAINT_R_COL_NODE paint_r_col_n.location = (start_pos_x + pos_x_shift, start_pos_y + 50) # node creation - level 2 paint_tex_sep = node_tree.nodes.new("ShaderNodeSeparateRGB") - paint_tex_sep.name = Truckpaint.PAINT_TEX_SEP_NODE - paint_tex_sep.label = Truckpaint.PAINT_TEX_SEP_NODE + paint_tex_sep.name = paint_tex_sep.label = Truckpaint.PAINT_TEX_SEP_NODE paint_tex_sep.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 650) # node creation - level 3 airbrush_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") - airbrush_mix_n.name = Truckpaint.AIRBRUSH_MIX_NODE - airbrush_mix_n.label = Truckpaint.AIRBRUSH_MIX_NODE + airbrush_mix_n.name = airbrush_mix_n.label = Truckpaint.AIRBRUSH_MIX_NODE airbrush_mix_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 1000) airbrush_mix_n.blend_type = "MIX" col_mask_b_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") - col_mask_b_mix_n.name = Truckpaint.COL_MASK_B_MIX_NODE - col_mask_b_mix_n.label = Truckpaint.COL_MASK_B_MIX_NODE + col_mask_b_mix_n.name = col_mask_b_mix_n.label = Truckpaint.COL_MASK_B_MIX_NODE col_mask_b_mix_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 600) col_mask_b_mix_n.blend_type = "MIX" col_mask_g_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") - col_mask_g_mix_n.name = Truckpaint.COL_MASK_G_MIX_NODE - col_mask_g_mix_n.label = Truckpaint.COL_MASK_G_MIX_NODE + col_mask_g_mix_n.name = col_mask_g_mix_n.label = Truckpaint.COL_MASK_G_MIX_NODE col_mask_g_mix_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 400) col_mask_g_mix_n.blend_type = "MIX" col_mask_r_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") - col_mask_r_mix_n.name = Truckpaint.COL_MASK_R_MIX_NODE - col_mask_r_mix_n.label = Truckpaint.COL_MASK_R_MIX_NODE + col_mask_r_mix_n.name = col_mask_r_mix_n.label = Truckpaint.COL_MASK_R_MIX_NODE col_mask_r_mix_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 200) col_mask_r_mix_n.blend_type = "MIX" # node creation - level 4 blend_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") - blend_mix_n.name = Truckpaint.BLEND_MIX_NODE - blend_mix_n.label = Truckpaint.BLEND_MIX_NODE + blend_mix_n.name = blend_mix_n.label = Truckpaint.BLEND_MIX_NODE blend_mix_n.location = (start_pos_x + pos_x_shift * 5, start_pos_y + 800) blend_mix_n.inputs['Fac'].default_value = 1.0 blend_mix_n.blend_type = "MIX" # make links - level 0 - node_tree.links.new(paint_tex_n.inputs['Vector'], paint_geom_n.outputs['UV']) + node_tree.links.new(paint_tex_n.inputs['Vector'], paint_uv_map_n.outputs['UV']) # make links - level 1 node_tree.links.new(paint_tex_sep.inputs['Image'], paint_tex_n.outputs['Color']) # make links - level 2 - node_tree.links.new(airbrush_mix_n.inputs['Fac'], paint_tex_n.outputs['Value']) + node_tree.links.new(airbrush_mix_n.inputs['Fac'], paint_tex_n.outputs['Alpha']) node_tree.links.new(airbrush_mix_n.inputs['Color1'], paint_base_col_n.outputs['Color']) node_tree.links.new(airbrush_mix_n.inputs['Color2'], paint_tex_n.outputs['Color']) @@ -276,7 +270,8 @@ def init_colormask_or_airbrush(node_tree): node_tree.links.new(blend_mix_n.inputs['Color2'], col_mask_r_mix_n.outputs['Color']) # make links - level 5 - node_tree.links.new(paint_mult_n.inputs['Color2'], blend_mix_n.outputs['Color']) + node_tree.links.new(paint_diff_mult_n.inputs['Color2'], blend_mix_n.outputs['Color']) + node_tree.links.new(paint_spec_mult_n.inputs['Color2'], paint_tex_n.outputs['Alpha']) @staticmethod def set_base_paint_color(node_tree, color): @@ -295,13 +290,13 @@ def set_base_paint_color(node_tree, color): node_tree.nodes[Truckpaint.BASE_PAINT_MULT_NODE].inputs["Color2"].default_value = color @staticmethod - def set_paintjob_texture(node_tree, texture): + def set_paintjob_texture(node_tree, image): """Set paint texture to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assigned to paint texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assigned to paint texture node + :type image: bpy.types.Image """ # as this functions should be called from airbrush or colormask derivatives @@ -309,7 +304,18 @@ def set_paintjob_texture(node_tree, texture): if Truckpaint.PAINT_TEX_NODE not in node_tree.nodes: return - node_tree.nodes[Truckpaint.PAINT_TEX_NODE].texture = texture + node_tree.nodes[Truckpaint.PAINT_TEX_NODE].image = image + + @staticmethod + def set_paintjob_texture_settings(node_tree, settings): + """Set paintjob texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + _material_utils.set_texture_settings_to_node(node_tree.nodes[Truckpaint.PAINT_TEX_NODE], settings) @staticmethod def set_paintjob_uv(node_tree, uv_layer): @@ -329,7 +335,7 @@ def set_paintjob_uv(node_tree, uv_layer): if uv_layer is None or uv_layer == "": uv_layer = _MESH_consts.none_uv - node_tree.nodes[Truckpaint.PAINT_GEOM_NODE].uv_layer = uv_layer + node_tree.nodes[Truckpaint.PAINT_UV_MAP_NODE].uv_map = uv_layer @staticmethod def set_aux8(node_tree, color): diff --git a/addon/io_scs_tools/internals/shaders/eut2/truckpaint/airbrush.py b/addon/io_scs_tools/internals/shaders/eut2/truckpaint/airbrush.py index c85a162..b8474e8 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/truckpaint/airbrush.py +++ b/addon/io_scs_tools/internals/shaders/eut2/truckpaint/airbrush.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.internals.shaders.eut2.truckpaint import Truckpaint @@ -42,5 +42,8 @@ def init(node_tree): blend_mix_n = node_tree.nodes[Truckpaint.BLEND_MIX_NODE] blend_mix_n.inputs['Fac'].default_value = 0.0 - paint_mult_n = node_tree.nodes[Truckpaint.PAINT_MULT_NODE] - paint_mult_n.inputs['Fac'].default_value = 1.0 + paint_diff_mult_n = node_tree.nodes[Truckpaint.PAINT_DIFFUSE_MULT_NODE] + paint_diff_mult_n.inputs['Fac'].default_value = 1.0 + + paint_spec_mult_n = node_tree.nodes[Truckpaint.PAINT_SPECULAR_MULT_NODE] + paint_spec_mult_n.inputs['Fac'].default_value = 0.0 diff --git a/addon/io_scs_tools/internals/shaders/eut2/truckpaint/colormask.py b/addon/io_scs_tools/internals/shaders/eut2/truckpaint/colormask.py index b496556..31f0aaa 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/truckpaint/colormask.py +++ b/addon/io_scs_tools/internals/shaders/eut2/truckpaint/colormask.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.internals.shaders.eut2.truckpaint import Truckpaint @@ -42,5 +42,8 @@ def init(node_tree): blend_mix_n = node_tree.nodes[Truckpaint.BLEND_MIX_NODE] blend_mix_n.inputs['Fac'].default_value = 1.0 - paint_mult_n = node_tree.nodes[Truckpaint.PAINT_MULT_NODE] - paint_mult_n.inputs['Fac'].default_value = 1.0 + paint_diff_mult_n = node_tree.nodes[Truckpaint.PAINT_DIFFUSE_MULT_NODE] + paint_diff_mult_n.inputs['Fac'].default_value = 1.0 + + paint_spec_mult_n = node_tree.nodes[Truckpaint.PAINT_SPECULAR_MULT_NODE] + paint_spec_mult_n.inputs['Fac'].default_value = 1.0 diff --git a/addon/io_scs_tools/internals/shaders/eut2/unlit_tex/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/unlit_tex/__init__.py index 9159a1c..fd36f93 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/unlit_tex/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/unlit_tex/__init__.py @@ -16,23 +16,27 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from mathutils import Color from io_scs_tools.consts import Mesh as _MESH_consts +from io_scs_tools.internals.shaders.base import BaseShader from io_scs_tools.internals.shaders.flavors import alpha_test from io_scs_tools.internals.shaders.flavors import blend_over from io_scs_tools.internals.shaders.flavors import blend_add +from io_scs_tools.internals.shaders.flavors import blend_mult from io_scs_tools.internals.shaders.flavors import paint from io_scs_tools.utils import convert as _convert_utils +from io_scs_tools.utils import material as _material_utils -class UnlitTex: +class UnlitTex(BaseShader): DIFF_COL_NODE = "DiffuseColor" - GEOM_NODE = "Geometry" + UV_MAP_NODE = "UVMap" BASE_TEX_NODE = "BaseTex" TEX_MULT_NODE = "TextureMultiplier" + ALPHA_INV_NODE = "AlphaInv" + OUT_SHADER_NODE = "OutShader" OUTPUT_NODE = "Output" @staticmethod @@ -54,44 +58,58 @@ def init(node_tree): pos_x_shift = 185 # node creation - geometry_n = node_tree.nodes.new("ShaderNodeGeometry") - geometry_n.name = UnlitTex.GEOM_NODE - geometry_n.label = UnlitTex.GEOM_NODE - geometry_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1500) - geometry_n.uv_layer = _MESH_consts.none_uv + uv_map_n = node_tree.nodes.new("ShaderNodeUVMap") + uv_map_n.name = uv_map_n.label = UnlitTex.UV_MAP_NODE + uv_map_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1500) + uv_map_n.uv_map = _MESH_consts.none_uv diff_col_n = node_tree.nodes.new("ShaderNodeRGB") - diff_col_n.name = UnlitTex.DIFF_COL_NODE - diff_col_n.label = UnlitTex.DIFF_COL_NODE + diff_col_n.name = diff_col_n.label = UnlitTex.DIFF_COL_NODE diff_col_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1700) - base_tex_n = node_tree.nodes.new("ShaderNodeTexture") - base_tex_n.name = UnlitTex.BASE_TEX_NODE - base_tex_n.label = UnlitTex.BASE_TEX_NODE + base_tex_n = node_tree.nodes.new("ShaderNodeTexImage") + base_tex_n.name = base_tex_n.label = UnlitTex.BASE_TEX_NODE base_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1500) + base_tex_n.width = 140 - tex_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") - tex_mult_n.name = UnlitTex.TEX_MULT_NODE - tex_mult_n.label = UnlitTex.TEX_MULT_NODE + tex_mult_n = node_tree.nodes.new("ShaderNodeVectorMath") + tex_mult_n.name = tex_mult_n.label = UnlitTex.TEX_MULT_NODE tex_mult_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1500) - tex_mult_n.blend_type = "MULTIPLY" - tex_mult_n.inputs['Fac'].default_value = 1 - - output_n = node_tree.nodes.new("ShaderNodeOutput") - output_n.name = UnlitTex.OUTPUT_NODE - output_n.label = UnlitTex.OUTPUT_NODE - output_n.location = (start_pos_x + + pos_x_shift * 5, start_pos_y + 1500) + tex_mult_n.operation = "MULTIPLY" + + alpha_inv_n = node_tree.nodes.new("ShaderNodeMath") + alpha_inv_n.name = alpha_inv_n.label = UnlitTex.ALPHA_INV_NODE + alpha_inv_n.location = (start_pos_x + pos_x_shift * 4, 1300) + alpha_inv_n.operation = "SUBTRACT" + alpha_inv_n.inputs[0].default_value = 0.999999 # TODO: change back to 1.0 after bug is fixed: https://developer.blender.org/T71426 + alpha_inv_n.inputs[1].default_value = 1.0 + alpha_inv_n.use_clamp = True + + out_shader_node = node_tree.nodes.new("ShaderNodeEeveeSpecular") + out_shader_node.name = out_shader_node.label = UnlitTex.OUT_SHADER_NODE + out_shader_node.location = (start_pos_x + pos_x_shift * 5, 1500) + out_shader_node.inputs["Base Color"].default_value = (0.0,) * 4 + out_shader_node.inputs["Specular"].default_value = (0.0,) * 4 + + output_n = node_tree.nodes.new("ShaderNodeOutputMaterial") + output_n.name = output_n.label = UnlitTex.OUTPUT_NODE + output_n.location = (start_pos_x + + pos_x_shift * 6, start_pos_y + 1500) # links creation - node_tree.links.new(base_tex_n.inputs['Vector'], geometry_n.outputs['UV']) + node_tree.links.new(base_tex_n.inputs['Vector'], uv_map_n.outputs['UV']) + + node_tree.links.new(tex_mult_n.inputs[0], diff_col_n.outputs['Color']) + node_tree.links.new(tex_mult_n.inputs[1], base_tex_n.outputs['Color']) - node_tree.links.new(tex_mult_n.inputs['Color1'], diff_col_n.outputs['Color']) - node_tree.links.new(tex_mult_n.inputs['Color2'], base_tex_n.outputs['Color']) + node_tree.links.new(alpha_inv_n.inputs[1], base_tex_n.outputs['Alpha']) - node_tree.links.new(output_n.inputs['Color'], tex_mult_n.outputs['Color']) + node_tree.links.new(out_shader_node.inputs['Emissive Color'], tex_mult_n.outputs[0]) + node_tree.links.new(out_shader_node.inputs['Transparency'], alpha_inv_n.outputs['Value']) + + node_tree.links.new(output_n.inputs['Surface'], out_shader_node.outputs['BSDF']) @staticmethod - def set_material(node_tree, material): + def finalize(node_tree, material): """Set output material for this shader. :param node_tree: node tree of current shader @@ -100,7 +118,34 @@ def set_material(node_tree, material): :type material: bpy.types.Material """ - pass # NOTE: there is no material node for this shader, because no lightning is applied + material.use_backface_culling = True + material.blend_method = "OPAQUE" + + # set proper blend method + if alpha_test.is_set(node_tree): + material.blend_method = "CLIP" + material.alpha_threshold = 0.05 + + # add alpha test pass if multiply blend enabled, where alphed pixels shouldn't be multiplied as they are discarded + if blend_mult.is_set(node_tree): + out_shader_n = node_tree.nodes[UnlitTex.OUT_SHADER_NODE] + + # alpha test pass has to get fully opaque input, thus remove transparency linkage + if out_shader_n.inputs['Transparency'].links: + node_tree.links.remove(out_shader_n.inputs['Transparency'].links[0]) + + shader_from = out_shader_n.outputs['BSDF'] + alpha_from = node_tree.nodes[UnlitTex.BASE_TEX_NODE].outputs['Alpha'] + shader_to = out_shader_n.outputs['BSDF'].links[0].to_socket + + alpha_test.add_pass(node_tree, shader_from, alpha_from, shader_to) + + if blend_add.is_set(node_tree): + material.blend_method = "BLEND" + if blend_mult.is_set(node_tree): + material.blend_method = "BLEND" + if blend_over.is_set(node_tree): + material.blend_method = "BLEND" @staticmethod def set_diffuse(node_tree, color): @@ -117,16 +162,39 @@ def set_diffuse(node_tree, color): node_tree.nodes[UnlitTex.DIFF_COL_NODE].outputs['Color'].default_value = color @staticmethod - def set_base_texture(node_tree, texture): + def set_queue_bias(node_tree, value): + """Set queue bias attirbute for this shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param value: queue bias index + :type value: int + """ + + pass # NOTE: shadow bias won't be visualized as game uses it's own implementation + + @staticmethod + def set_base_texture(node_tree, image): """Set base texture to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assignet to base texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assignet to base texture node + :type image: bpy.types.Image """ - node_tree.nodes[UnlitTex.BASE_TEX_NODE].texture = texture + node_tree.nodes[UnlitTex.BASE_TEX_NODE].image = image + + @staticmethod + def set_base_texture_settings(node_tree, settings): + """Set base texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + _material_utils.set_texture_settings_to_node(node_tree.nodes[UnlitTex.BASE_TEX_NODE], settings) @staticmethod def set_base_uv(node_tree, uv_layer): @@ -141,7 +209,7 @@ def set_base_uv(node_tree, uv_layer): if uv_layer is None or uv_layer == "": uv_layer = _MESH_consts.none_uv - node_tree.nodes[UnlitTex.GEOM_NODE].uv_layer = uv_layer + node_tree.nodes[UnlitTex.UV_MAP_NODE].uv_map = uv_layer @staticmethod def set_alpha_test_flavor(node_tree, switch_on): @@ -153,12 +221,8 @@ def set_alpha_test_flavor(node_tree, switch_on): :type switch_on: bool """ - if switch_on and not blend_over.is_set(node_tree): - out_node = node_tree.nodes[UnlitTex.OUTPUT_NODE] - in_node = node_tree.nodes[UnlitTex.BASE_TEX_NODE] - location = (out_node.location.x - 185 * 2, out_node.location.y - 500) - - alpha_test.init(node_tree, location, in_node.outputs['Value'], out_node.inputs['Alpha']) + if switch_on: + alpha_test.init(node_tree) else: alpha_test.delete(node_tree) @@ -172,15 +236,8 @@ def set_blend_over_flavor(node_tree, switch_on): :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): - UnlitTex.set_alpha_test_flavor(node_tree, False) - if switch_on: - out_node = node_tree.nodes[UnlitTex.OUTPUT_NODE] - in_node = node_tree.nodes[UnlitTex.BASE_TEX_NODE] - - blend_over.init(node_tree, in_node.outputs['Value'], out_node.inputs['Alpha']) + blend_over.init(node_tree) else: blend_over.delete(node_tree) @@ -194,18 +251,44 @@ def set_blend_add_flavor(node_tree, switch_on): :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): - UnlitTex.set_alpha_test_flavor(node_tree, False) - if switch_on: + in_node = node_tree.nodes[UnlitTex.OUT_SHADER_NODE] out_node = node_tree.nodes[UnlitTex.OUTPUT_NODE] - in_node = node_tree.nodes[UnlitTex.BASE_TEX_NODE] - blend_add.init(node_tree, in_node.outputs['Value'], out_node.inputs['Alpha']) + # put it on location of output node & move output node for one slot to the right + location = tuple(out_node.location) + out_node.location.x += 185 + + blend_add.init(node_tree, location, in_node.outputs['BSDF'], out_node.inputs['Surface']) else: blend_add.delete(node_tree) + @staticmethod + def set_blend_mult_flavor(node_tree, switch_on): + """Set blend mult flavor to this shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param switch_on: flag indication if blending should be switched on or off + :type switch_on: bool + """ + + if switch_on: + out_node = node_tree.nodes[UnlitTex.OUTPUT_NODE] + in_node = node_tree.nodes[UnlitTex.OUT_SHADER_NODE] + + # break link to out shader node as mult uses DST_COLOR as source factor in blend function + if in_node.inputs['Transparency'].links: + node_tree.links.remove(in_node.inputs['Transparency'].links[0]) + + # put it on location of output node & move output node for one slot to the right + location = tuple(out_node.location) + out_node.location.x += 185 + + blend_mult.init(node_tree, location, in_node.outputs['BSDF'], out_node.inputs['Surface']) + else: + blend_mult.delete(node_tree) + @staticmethod def set_paint_flavor(node_tree, switch_on): """Set paint flavor to this shader. @@ -226,7 +309,7 @@ def set_paint_flavor(node_tree, switch_on): node.location.x += 185 location = (diff_mult_n.location.x - 185, diff_mult_n.location.y + 50) - paint.init(node_tree, location, diff_col_n.outputs["Color"], diff_mult_n.inputs["Color1"]) + paint.init(node_tree, location, diff_col_n.outputs["Color"], diff_mult_n.inputs[0]) else: paint.delete(node_tree) diff --git a/addon/io_scs_tools/internals/shaders/eut2/unlit_tex/a8.py b/addon/io_scs_tools/internals/shaders/eut2/unlit_tex/a8.py new file mode 100644 index 0000000..bb6c3f2 --- /dev/null +++ b/addon/io_scs_tools/internals/shaders/eut2/unlit_tex/a8.py @@ -0,0 +1,62 @@ +# ##### 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.unlit_tex import UnlitTex + + +class UnlitTexA8(UnlitTex): + + @staticmethod + def get_name(): + """Get name of this shader file with full modules path.""" + return __name__ + + @staticmethod + def init(node_tree): + """Initialize node tree with links for this shader. + + Color result in this shader is always (0,0,0) and transparency is donate by grayscale texture and not alpha channel. + + :param node_tree: node tree on which this shader should be created + :type node_tree: bpy.types.NodeTree + """ + + # init parent + UnlitTex.init(node_tree) + + # remove texture multiplier, which will break link for resulting color + node_tree.nodes.remove(node_tree.nodes[UnlitTex.TEX_MULT_NODE]) + + # then set output color to (0,0,0) + node_tree.nodes[UnlitTex.OUT_SHADER_NODE].inputs['Emissive Color'].default_value = (0,) * 4 + + # and instead alpha use color channel (as this shader requests grayscale) + node_tree.links.new(node_tree.nodes[UnlitTex.ALPHA_INV_NODE].inputs[1], node_tree.nodes[UnlitTex.BASE_TEX_NODE].outputs['Color']) + + @staticmethod + def set_paint_flavor(node_tree, switch_on): + """Set paint flavor to this shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param switch_on: flag indication if flavor should be switched on or off + :type switch_on: bool + """ + pass # no matter the color, we always output (0,0,0) opaque color in this shader, thus useless to do anything diff --git a/addon/io_scs_tools/internals/shaders/eut2/unlit_vcol_tex/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/unlit_vcol_tex/__init__.py index b20006b..d23e4ef 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/unlit_vcol_tex/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/unlit_vcol_tex/__init__.py @@ -16,12 +16,12 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from mathutils import Color from io_scs_tools.consts import Mesh as _MESH_consts -from io_scs_tools.internals.shaders.eut2.std_node_groups import vcolor_input +from io_scs_tools.internals.shaders.base import BaseShader +from io_scs_tools.internals.shaders.eut2.std_node_groups import vcolor_input_ng from io_scs_tools.internals.shaders.flavors import alpha_test from io_scs_tools.internals.shaders.flavors import awhite from io_scs_tools.internals.shaders.flavors import blend_add @@ -29,17 +29,19 @@ from io_scs_tools.internals.shaders.flavors import blend_over from io_scs_tools.internals.shaders.flavors import paint from io_scs_tools.utils import convert as _convert_utils +from io_scs_tools.utils import material as _material_utils -class UnlitVcolTex: +class UnlitVcolTex(BaseShader): DIFF_COL_NODE = "DiffuseColor" - GEOM_NODE = "Geometry" + UVMAP_NODE = "UVMap" VCOL_GROUP_NODE = "VColorGroup" OPACITY_NODE = "OpacityMultiplier" BASE_TEX_NODE = "BaseTex" DIFF_MULT_NODE = "DiffMultiplier" TEX_MULT_NODE = "TextureMultiplier" - VCOLOR_SCALE_NODE = "VertexColorScale" + ALPHA_INV_NODE = "AlphaInv" + OUT_SHADER_NODE = "OutShader" OUTPUT_NODE = "Output" @staticmethod @@ -62,86 +64,117 @@ def init(node_tree): # node creation vcol_group_n = node_tree.nodes.new("ShaderNodeGroup") - vcol_group_n.name = UnlitVcolTex.VCOL_GROUP_NODE - vcol_group_n.label = UnlitVcolTex.VCOL_GROUP_NODE + vcol_group_n.name = vcol_group_n.label = UnlitVcolTex.VCOL_GROUP_NODE vcol_group_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1650) - vcol_group_n.node_tree = vcolor_input.get_node_group() + vcol_group_n.node_tree = vcolor_input_ng.get_node_group() - geometry_n = node_tree.nodes.new("ShaderNodeGeometry") - geometry_n.name = UnlitVcolTex.GEOM_NODE - geometry_n.label = UnlitVcolTex.GEOM_NODE - geometry_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1500) - geometry_n.uv_layer = _MESH_consts.none_uv + uv_n = node_tree.nodes.new("ShaderNodeUVMap") + uv_n.name = uv_n.label = UnlitVcolTex.UVMAP_NODE + uv_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1500) + uv_n.uv_map = _MESH_consts.none_uv diff_col_n = node_tree.nodes.new("ShaderNodeRGB") - diff_col_n.name = UnlitVcolTex.DIFF_COL_NODE - diff_col_n.label = UnlitVcolTex.DIFF_COL_NODE + diff_col_n.name = diff_col_n.label = UnlitVcolTex.DIFF_COL_NODE diff_col_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1700) - vcol_scale_n = node_tree.nodes.new("ShaderNodeMixRGB") - vcol_scale_n.name = UnlitVcolTex.VCOLOR_SCALE_NODE - vcol_scale_n.label = UnlitVcolTex.VCOLOR_SCALE_NODE - vcol_scale_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1600) - vcol_scale_n.blend_type = "MULTIPLY" - vcol_scale_n.inputs['Fac'].default_value = 1 - vcol_scale_n.inputs['Color2'].default_value = (2,) * 4 - opacity_n = node_tree.nodes.new("ShaderNodeMath") - opacity_n.name = UnlitVcolTex.OPACITY_NODE - opacity_n.label = UnlitVcolTex.OPACITY_NODE + opacity_n.name = opacity_n.label = UnlitVcolTex.OPACITY_NODE opacity_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1300) opacity_n.operation = "MULTIPLY" - base_tex_n = node_tree.nodes.new("ShaderNodeTexture") - base_tex_n.name = UnlitVcolTex.BASE_TEX_NODE - base_tex_n.label = UnlitVcolTex.BASE_TEX_NODE + base_tex_n = node_tree.nodes.new("ShaderNodeTexImage") + base_tex_n.name = base_tex_n.label = UnlitVcolTex.BASE_TEX_NODE base_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1500) - - diff_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") - diff_mult_n.name = UnlitVcolTex.DIFF_MULT_NODE - diff_mult_n.label = UnlitVcolTex.DIFF_MULT_NODE - diff_mult_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 1700) - diff_mult_n.blend_type = "MULTIPLY" - diff_mult_n.inputs['Fac'].default_value = 1 - diff_mult_n.inputs['Color2'].default_value = (0, 0, 0, 1) - - tex_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") - tex_mult_n.name = UnlitVcolTex.TEX_MULT_NODE - tex_mult_n.label = UnlitVcolTex.TEX_MULT_NODE - tex_mult_n.location = (start_pos_x + pos_x_shift * 5, start_pos_y + 1500) - tex_mult_n.blend_type = "MULTIPLY" - tex_mult_n.inputs['Fac'].default_value = 1 - - output_n = node_tree.nodes.new("ShaderNodeOutput") - output_n.name = UnlitVcolTex.OUTPUT_NODE - output_n.label = UnlitVcolTex.OUTPUT_NODE - output_n.location = (start_pos_x + + pos_x_shift * 7, start_pos_y + 1500) + base_tex_n.width = 140 + + diff_mult_n = node_tree.nodes.new("ShaderNodeVectorMath") + diff_mult_n.name = diff_mult_n.label = UnlitVcolTex.DIFF_MULT_NODE + diff_mult_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1700) + diff_mult_n.operation = "MULTIPLY" + diff_mult_n.inputs[1].default_value = (0, 0, 0) + + tex_mult_n = node_tree.nodes.new("ShaderNodeVectorMath") + tex_mult_n.name = tex_mult_n.label = UnlitVcolTex.TEX_MULT_NODE + tex_mult_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 1500) + tex_mult_n.operation = "MULTIPLY" + + alpha_inv_n = node_tree.nodes.new("ShaderNodeMath") + alpha_inv_n.name = alpha_inv_n.label = UnlitVcolTex.ALPHA_INV_NODE + alpha_inv_n.location = (start_pos_x + pos_x_shift * 5, 1300) + alpha_inv_n.operation = "SUBTRACT" + alpha_inv_n.inputs[0].default_value = 0.999999 # TODO: change back to 1.0 after bug is fixed: https://developer.blender.org/T71426 + alpha_inv_n.inputs[1].default_value = 1.0 + alpha_inv_n.use_clamp = True + + out_shader_node = node_tree.nodes.new("ShaderNodeEeveeSpecular") + out_shader_node.name = out_shader_node.label = UnlitVcolTex.OUT_SHADER_NODE + out_shader_node.location = (start_pos_x + pos_x_shift * 6, 1500) + out_shader_node.inputs["Base Color"].default_value = (0.0,) * 4 + out_shader_node.inputs["Specular"].default_value = (0.0,) * 4 + + output_n = node_tree.nodes.new("ShaderNodeOutputMaterial") + output_n.name = output_n.label = UnlitVcolTex.OUTPUT_NODE + output_n.location = (start_pos_x + pos_x_shift * 7, start_pos_y + 1500) # links creation - node_tree.links.new(base_tex_n.inputs['Vector'], geometry_n.outputs['UV']) - node_tree.links.new(vcol_scale_n.inputs['Color1'], vcol_group_n.outputs['Vertex Color']) + node_tree.links.new(base_tex_n.inputs['Vector'], uv_n.outputs['UV']) - node_tree.links.new(diff_mult_n.inputs['Color1'], diff_col_n.outputs['Color']) - node_tree.links.new(diff_mult_n.inputs['Color2'], vcol_scale_n.outputs['Color']) - node_tree.links.new(opacity_n.inputs[0], base_tex_n.outputs["Value"]) + node_tree.links.new(diff_mult_n.inputs[0], diff_col_n.outputs['Color']) + node_tree.links.new(diff_mult_n.inputs[1], vcol_group_n.outputs['Vertex Color']) + + node_tree.links.new(opacity_n.inputs[0], base_tex_n.outputs["Alpha"]) node_tree.links.new(opacity_n.inputs[1], vcol_group_n.outputs["Vertex Color Alpha"]) - node_tree.links.new(tex_mult_n.inputs['Color1'], diff_mult_n.outputs['Color']) - node_tree.links.new(tex_mult_n.inputs['Color2'], base_tex_n.outputs['Color']) + node_tree.links.new(tex_mult_n.inputs[0], diff_mult_n.outputs[0]) + node_tree.links.new(tex_mult_n.inputs[1], base_tex_n.outputs['Color']) + + node_tree.links.new(alpha_inv_n.inputs[1], opacity_n.outputs['Value']) - node_tree.links.new(output_n.inputs['Color'], tex_mult_n.outputs['Color']) + node_tree.links.new(out_shader_node.inputs['Emissive Color'], tex_mult_n.outputs[0]) + node_tree.links.new(out_shader_node.inputs['Transparency'], alpha_inv_n.outputs['Value']) + + node_tree.links.new(output_n.inputs['Surface'], out_shader_node.outputs['BSDF']) @staticmethod - def set_material(node_tree, material): - """Set output material for this shader. + def finalize(node_tree, material): + """Finalize node tree and material settings. Should be called as last. - :param node_tree: node tree of current shader + :param node_tree: node tree on which this shader should be finalized :type node_tree: bpy.types.NodeTree - :param material: blender material for used in this tree node as output + :param material: material used for this shader :type material: bpy.types.Material """ - pass # NOTE: there is no material node for this shader, because no lightning is applied + material.use_backface_culling = True + material.blend_method = "OPAQUE" + + # set proper blend method + if alpha_test.is_set(node_tree): + material.blend_method = "CLIP" + material.alpha_threshold = 0.05 + + # add alpha test pass if: + # 1. awhite is enabled, as alpha test pass is called before awhite is aplied + # 2. multiply blend enabled, where alphed pixels shouldn't be multiplied as they are discarded + if awhite.is_set(node_tree) or blend_mult.is_set(node_tree): + out_shader_n = node_tree.nodes[UnlitVcolTex.OUT_SHADER_NODE] + + # alpha test pass has to get fully opaque input, thus remove transparency linkage + if out_shader_n.inputs['Transparency'].links: + node_tree.links.remove(out_shader_n.inputs['Transparency'].links[0]) + + shader_from = out_shader_n.outputs['BSDF'] + alpha_from = node_tree.nodes[UnlitVcolTex.OPACITY_NODE].outputs[0] + shader_to = out_shader_n.outputs['BSDF'].links[0].to_socket + + alpha_test.add_pass(node_tree, shader_from, alpha_from, shader_to) + + if blend_add.is_set(node_tree): + material.blend_method = "BLEND" + if blend_mult.is_set(node_tree): + material.blend_method = "BLEND" + if blend_over.is_set(node_tree): + material.blend_method = "BLEND" @staticmethod def set_diffuse(node_tree, color): @@ -158,16 +191,48 @@ def set_diffuse(node_tree, color): node_tree.nodes[UnlitVcolTex.DIFF_COL_NODE].outputs['Color'].default_value = color @staticmethod - def set_base_texture(node_tree, texture): + def set_queue_bias(node_tree, value): + """Set queue bias attirbute for this shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param value: queue bias index + :type value: int + """ + + pass # NOTE: shadow bias won't be visualized as game uses it's own implementation + + @staticmethod + def set_base_texture(node_tree, image): """Set base texture to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assignet to base texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assigned to base texture node + :type image: bpy.types.Texture """ - node_tree.nodes[UnlitVcolTex.BASE_TEX_NODE].texture = texture + opacity_n = node_tree.nodes[UnlitVcolTex.OPACITY_NODE] + base_tex_n = node_tree.nodes[UnlitVcolTex.BASE_TEX_NODE] + + # automatically pick color channel in case we operate with grayscale + if image and image.depth == 8: # grayscale + node_tree.links.new(opacity_n.inputs[0], base_tex_n.outputs["Color"]) + else: + node_tree.links.new(opacity_n.inputs[0], base_tex_n.outputs["Alpha"]) + + base_tex_n.image = image + + @staticmethod + def set_base_texture_settings(node_tree, settings): + """Set base texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + _material_utils.set_texture_settings_to_node(node_tree.nodes[UnlitVcolTex.BASE_TEX_NODE], settings) @staticmethod def set_base_uv(node_tree, uv_layer): @@ -182,7 +247,7 @@ def set_base_uv(node_tree, uv_layer): if uv_layer is None or uv_layer == "": uv_layer = _MESH_consts.none_uv - node_tree.nodes[UnlitVcolTex.GEOM_NODE].uv_layer = uv_layer + node_tree.nodes[UnlitVcolTex.UVMAP_NODE].uv_map = uv_layer @staticmethod def set_alpha_test_flavor(node_tree, switch_on): @@ -194,12 +259,8 @@ def set_alpha_test_flavor(node_tree, switch_on): :type switch_on: bool """ - if switch_on and not blend_over.is_set(node_tree): - out_node = node_tree.nodes[UnlitVcolTex.OUTPUT_NODE] - in_node = node_tree.nodes[UnlitVcolTex.OPACITY_NODE] - location = (out_node.location.x - 185 * 2, out_node.location.y - 500) - - alpha_test.init(node_tree, location, in_node.outputs['Value'], out_node.inputs['Alpha']) + if switch_on: + alpha_test.init(node_tree) else: alpha_test.delete(node_tree) @@ -213,77 +274,58 @@ def set_blend_over_flavor(node_tree, switch_on): :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): - UnlitVcolTex.set_alpha_test_flavor(node_tree, False) - if switch_on: - out_node = node_tree.nodes[UnlitVcolTex.OUTPUT_NODE] - in_node = node_tree.nodes[UnlitVcolTex.OPACITY_NODE] - - blend_over.init(node_tree, in_node.outputs['Value'], out_node.inputs['Alpha']) + blend_over.init(node_tree) else: blend_over.delete(node_tree) @staticmethod - def set_blend_mult_flavor(node_tree, switch_on): - """Set mult blending flavor to this shader. + 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 mult should be switched on or off + :param switch_on: flag indication if blend add should be switched on or off :type switch_on: bool """ - output_n = node_tree.nodes[UnlitVcolTex.OUTPUT_NODE] - - from_color_socket = awhite.get_out_socket(node_tree) - if not from_color_socket: - from_color_socket = node_tree.nodes[UnlitVcolTex.TEX_MULT_NODE].outputs['Color'] - to_color_socket = output_n.inputs['Color'] - - from_alpha_socket = node_tree.nodes[UnlitVcolTex.OPACITY_NODE].outputs[0] - to_alpha_socket = output_n.inputs['Alpha'] - - # remove alpha test flavor if it was set already. Because these two can not coexist - if alpha_test.is_set(node_tree): - UnlitVcolTex.set_alpha_test_flavor(node_tree, False) - if switch_on: + in_node = node_tree.nodes[UnlitVcolTex.OUT_SHADER_NODE] + out_node = node_tree.nodes[UnlitVcolTex.OUTPUT_NODE] - location = (output_n.location.x - 185, output_n.location.y) - output_n.location.x += 185 - blend_mult.init(node_tree, location, from_alpha_socket, to_alpha_socket, from_color_socket, to_color_socket) + # put it on location of output node & move output node for one slot to the right + location = tuple(out_node.location) + out_node.location.x += 185 + blend_add.init(node_tree, location, in_node.outputs['BSDF'], out_node.inputs['Surface']) else: - - output_n.location.x -= 185 - blend_mult.delete(node_tree) - - # make sure to reaply awhite if set - UnlitVcolTex.set_awhite_flavor(node_tree, awhite.is_set(node_tree)) + blend_add.delete(node_tree) @staticmethod - def set_blend_add_flavor(node_tree, switch_on): - """Set blend add flavor to this shader. + def set_blend_mult_flavor(node_tree, switch_on): + """Set blend mult 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 + :param switch_on: flag indication if blending 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): - UnlitVcolTex.set_alpha_test_flavor(node_tree, False) - if switch_on: out_node = node_tree.nodes[UnlitVcolTex.OUTPUT_NODE] - in_node = node_tree.nodes[UnlitVcolTex.OPACITY_NODE] + in_node = node_tree.nodes[UnlitVcolTex.OUT_SHADER_NODE] - blend_add.init(node_tree, in_node.outputs['Value'], out_node.inputs['Alpha']) + # break link to out shader node as mult uses DST_COLOR as source factor in blend function + if in_node.inputs['Transparency'].links: + node_tree.links.remove(in_node.inputs['Transparency'].links[0]) + + # put it on location of output node & move output node for one slot to the right + location = tuple(out_node.location) + out_node.location.x += 185 + + blend_mult.init(node_tree, location, in_node.outputs['BSDF'], out_node.inputs['Surface']) else: - blend_add.delete(node_tree) + blend_mult.delete(node_tree) @staticmethod def set_awhite_flavor(node_tree, switch_on): @@ -295,36 +337,19 @@ def set_awhite_flavor(node_tree, switch_on): :type switch_on: bool """ - output_n = node_tree.nodes[UnlitVcolTex.OUTPUT_NODE] - - from_mix_factor = node_tree.nodes[UnlitVcolTex.OPACITY_NODE].outputs[0] - from_color_socket = node_tree.nodes[UnlitVcolTex.TEX_MULT_NODE].outputs['Color'] - - # remove alpha test flavor if it was set already. Because these two can not coexist - if alpha_test.is_set(node_tree): - UnlitVcolTex.set_alpha_test_flavor(node_tree, False) - if switch_on: + out_shader_node = node_tree.nodes[UnlitVcolTex.OUT_SHADER_NODE] + from_mix_factor = node_tree.nodes[UnlitVcolTex.OPACITY_NODE].outputs[0] + from_color_socket = node_tree.nodes[UnlitVcolTex.TEX_MULT_NODE].outputs[0] - is_blend_mult_set = blend_mult.is_set(node_tree) - - # disable blend mult first - if is_blend_mult_set: - UnlitVcolTex.set_blend_mult_flavor(node_tree, False) - - location = (output_n.location.x - 185, output_n.location.y - 100) - output_n.location.x += 185 - awhite.init(node_tree, location, from_mix_factor, from_color_socket, output_n.inputs['Color']) - - # re-enable blend mult again - if is_blend_mult_set: - UnlitVcolTex.set_blend_mult_flavor(node_tree, True) + # remove link to transparency as awhite sets alpha to 1 + if out_shader_node.inputs['Transparency'].links: + node_tree.links.remove(out_shader_node.inputs['Transparency'].links[0]) + location = (out_shader_node.location.x - 185, out_shader_node.location.y) + awhite.init(node_tree, location, from_mix_factor, from_color_socket, out_shader_node.inputs['Emissive Color']) else: - - output_n.location.x -= 185 awhite.delete(node_tree) - node_tree.links.new(output_n.inputs['Color'], from_color_socket) @staticmethod def set_paint_flavor(node_tree, switch_on): @@ -346,7 +371,7 @@ def set_paint_flavor(node_tree, switch_on): node.location.x += 185 location = (diff_mult_n.location.x - 185 * 2, diff_mult_n.location.y + 50) - paint.init(node_tree, location, diff_col_n.outputs["Color"], diff_mult_n.inputs["Color1"]) + paint.init(node_tree, location, diff_col_n.outputs["Color"], diff_mult_n.inputs[0]) else: paint.delete(node_tree) diff --git a/addon/io_scs_tools/internals/shaders/eut2/water/__init__.py b/addon/io_scs_tools/internals/shaders/eut2/water/__init__.py index a83db07..7ea994b 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/water/__init__.py +++ b/addon/io_scs_tools/internals/shaders/eut2/water/__init__.py @@ -16,29 +16,41 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software import bpy +import math from io_scs_tools.internals.shaders.eut2.dif import Dif from io_scs_tools.internals.shaders.eut2.std_passes.add_env import StdAddEnv from io_scs_tools.internals.shaders.eut2.water import mix_factor_ng +from io_scs_tools.internals.shaders.eut2.water import water_stream_ng +from io_scs_tools.internals.shaders.flavors.nmap import scale_ng from io_scs_tools.utils import convert as _convert_utils +from io_scs_tools.utils import material as _material_utils class Water(Dif, StdAddEnv): + WATER_STREAM_NODE = "WaterStream" NEAR_COLOR_NODE = "NearColor" HORIZON_COLOR_NODE = "HorizonColor" MIX_FACTOR_GNODE = "WaterMixFactor" NEAR_MIX_NODE = "NearMix" HORIZON_MIX_NODE = "HorizonMix" - ADD_REFL_MIX_NODE = "AddRefl" + NEAR_HORIZON_ENV_MIX_NODE = "NearHorizonEnvLerpMix" NEAR_HORIZON_MIX_NODE = "NearHorizonLerpMix" - LAYER0_MAT_NODE = "Layer0Tex" - LAYER1_MAT_NODE = "Layer1Tex" + LAYER0_NMAP_UID = "Layer0" + LAYER1_NMAP_UID = "Layer1" LAY0_LAY1_NORMAL_MIX_NODE = "Layer0/Layer1NormalMix" + LAY0_LAY1_NORMAL_SCRAMBLE_NODE = "Layer0/Layer1NormalScramble" NORMAL_NORMALIZE_NODE = "Normalize" + POSTFIX_STREAM_MIX = "StreamMix" + POSTFIX_MAPPING_NODE = "Mapping" + POSTFIX_NMAP_TEX_NODE = "Tex" + POSTFIX_NMAP_NODE = "NormalMap" + POSTFIX_NMAP_SCALE_NODE = "NMapScaleGroup" + @staticmethod def get_name(): """Get name of this shader file with full modules path.""" @@ -48,8 +60,6 @@ def get_name(): def init(node_tree): """Initialize node tree with links for this shader. - NODE: this is fake representation only to utilize textures - :param node_tree: node tree on which this shader should be created :type node_tree: bpy.types.NodeTree """ @@ -62,28 +72,34 @@ def init(node_tree): # init parent Dif.init(node_tree) + geom_n = node_tree.nodes[Dif.GEOM_NODE] diff_col_n = node_tree.nodes[Dif.DIFF_COL_NODE] vcol_mult_n = node_tree.nodes[Dif.VCOLOR_MULT_NODE] - out_mat_n = node_tree.nodes[Dif.OUT_MAT_NODE] + lighting_eval_n = node_tree.nodes[Dif.LIGHTING_EVAL_NODE] + compose_lighting_n = node_tree.nodes[Dif.COMPOSE_LIGHTING_NODE] output_n = node_tree.nodes[Dif.OUTPUT_NODE] # delete existing - node_tree.nodes.remove(node_tree.nodes[Dif.COMPOSE_LIGHTING_NODE]) node_tree.nodes.remove(node_tree.nodes[Dif.BASE_TEX_NODE]) node_tree.nodes.remove(node_tree.nodes[Dif.OPACITY_NODE]) node_tree.nodes.remove(node_tree.nodes[Dif.DIFF_MULT_NODE]) # move existing - vcol_mult_n.location.y -= 0 - out_mat_n.location.x += pos_x_shift * 2 - output_n.location.x += pos_x_shift * 2 + lighting_eval_n.location.x += pos_x_shift * 3 + compose_lighting_n.location.x += pos_x_shift * 3 + output_n.location.x += pos_x_shift * 3 # nodes creation - mix_factor_gn = node_tree.nodes.new("ShaderNodeGroup") - mix_factor_gn.name = mix_factor_gn.label = Water.MIX_FACTOR_GNODE - mix_factor_gn.location = (start_pos_x + pos_x_shift, start_pos_y + 1500) - mix_factor_gn.node_tree = mix_factor_ng.get_node_group() + water_stream_n = node_tree.nodes.new("ShaderNodeGroup") + water_stream_n.name = water_stream_n.label = Water.WATER_STREAM_NODE + water_stream_n.location = (start_pos_x - pos_x_shift, start_pos_y + 700) + water_stream_n.node_tree = water_stream_ng.get_node_group() + + mix_factor_n = node_tree.nodes.new("ShaderNodeGroup") + mix_factor_n.name = mix_factor_n.label = Water.MIX_FACTOR_GNODE + mix_factor_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1500) + mix_factor_n.node_tree = mix_factor_ng.get_node_group() near_col_n = node_tree.nodes.new("ShaderNodeRGB") near_col_n.label = near_col_n.name = Water.NEAR_COLOR_NODE @@ -93,97 +109,175 @@ def init(node_tree): horizon_col_n.label = horizon_col_n.name = Water.HORIZON_COLOR_NODE horizon_col_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1100) - layer0_mat_n = node_tree.nodes.new("ShaderNodeMaterial") - layer0_mat_n.name = layer0_mat_n.label = Water.LAYER0_MAT_NODE - layer0_mat_n.location = (start_pos_x + pos_x_shift, start_pos_y + 900) - layer0_mat_n.use_diffuse = False - layer0_mat_n.use_specular = False + near_mix_n = node_tree.nodes.new("ShaderNodeVectorMath") + near_mix_n.name = near_mix_n.label = Water.NEAR_MIX_NODE + near_mix_n.location = (start_pos_x + pos_x_shift * 5, start_pos_y + 1400) + near_mix_n.operation = "MULTIPLY" - layer1_mat_n = node_tree.nodes.new("ShaderNodeMaterial") - layer1_mat_n.name = layer1_mat_n.label = Water.LAYER1_MAT_NODE - layer1_mat_n.location = (start_pos_x + pos_x_shift, start_pos_y + 500) - layer1_mat_n.use_diffuse = False - layer1_mat_n.use_specular = False + horizon_mix_n = node_tree.nodes.new("ShaderNodeVectorMath") + horizon_mix_n.name = horizon_mix_n.label = Water.HORIZON_MIX_NODE + horizon_mix_n.location = (start_pos_x + pos_x_shift * 5, start_pos_y + 1200) + horizon_mix_n.operation = "MULTIPLY" - lay0_lay1_normal_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") + lay0_lay1_normal_mix_n = node_tree.nodes.new("ShaderNodeVectorMath") lay0_lay1_normal_mix_n.name = lay0_lay1_normal_mix_n.label = Water.LAY0_LAY1_NORMAL_MIX_NODE - lay0_lay1_normal_mix_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 700) - lay0_lay1_normal_mix_n.blend_type = "ADD" - lay0_lay1_normal_mix_n.inputs['Fac'].default_value = 1.0 + lay0_lay1_normal_mix_n.location = (start_pos_x + pos_x_shift * 6, start_pos_y + 700) + lay0_lay1_normal_mix_n.operation = "ADD" normal_normalize_n = node_tree.nodes.new("ShaderNodeVectorMath") normal_normalize_n.name = normal_normalize_n.label = Water.LAY0_LAY1_NORMAL_MIX_NODE - normal_normalize_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 700) + normal_normalize_n.location = (start_pos_x + pos_x_shift * 7, start_pos_y + 700) normal_normalize_n.operation = "NORMALIZE" - near_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") - near_mix_n.name = near_mix_n.label = Water.NEAR_MIX_NODE - near_mix_n.location = (start_pos_x + pos_x_shift * 5, start_pos_y + 1400) - near_mix_n.blend_type = "MULTIPLY" - near_mix_n.inputs['Fac'].default_value = 1.0 - - horizon_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") - horizon_mix_n.name = horizon_mix_n.label = Water.HORIZON_MIX_NODE - horizon_mix_n.location = (start_pos_x + pos_x_shift * 5, start_pos_y + 1200) - horizon_mix_n.blend_type = "MULTIPLY" - horizon_mix_n.inputs['Fac'].default_value = 1.0 - - add_refl_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") - add_refl_mix_n.name = add_refl_mix_n.label = Water.ADD_REFL_MIX_NODE - add_refl_mix_n.location = (start_pos_x + pos_x_shift * 6, start_pos_y + 2000) - add_refl_mix_n.blend_type = "ADD" - add_refl_mix_n.inputs['Fac'].default_value = 1.0 + near_horizon_env_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") + near_horizon_env_mix_n.name = near_horizon_env_mix_n.label = Water.NEAR_HORIZON_ENV_MIX_NODE + near_horizon_env_mix_n.location = (start_pos_x + pos_x_shift * 7, start_pos_y + 2100) + near_horizon_env_mix_n.blend_type = "MIX" + near_horizon_env_mix_n.inputs['Color2'].default_value = (0.0,) * 4 # far horizon is without env, thus lerp to zero near_horizon_mix_n = node_tree.nodes.new("ShaderNodeMixRGB") near_horizon_mix_n.name = near_horizon_mix_n.label = Water.NEAR_HORIZON_MIX_NODE near_horizon_mix_n.location = (start_pos_x + pos_x_shift * 7, start_pos_y + 1700) near_horizon_mix_n.blend_type = "MIX" + normal_scramble_n = node_tree.nodes.new("ShaderNodeMixRGB") + normal_scramble_n.name = normal_scramble_n.label = Water.LAY0_LAY1_NORMAL_SCRAMBLE_NODE + normal_scramble_n.location = (start_pos_x + pos_x_shift * 8, start_pos_y + 1200) + normal_scramble_n.blend_type = "MIX" + normal_scramble_n.inputs['Color1'].default_value = (0.0, 0.0, 1.0, 0.0) # WATER_V_NORMAL + + # links creation + # pass 2 + node_tree.links.new(normal_normalize_n.inputs[0], lay0_lay1_normal_mix_n.outputs[0]) + node_tree.links.new(vcol_mult_n.inputs[1], diff_col_n.outputs['Color']) + + # pass 3 + node_tree.links.new(near_mix_n.inputs[0], vcol_mult_n.outputs[0]) + node_tree.links.new(near_mix_n.inputs[1], near_col_n.outputs['Color']) + + node_tree.links.new(horizon_mix_n.inputs[0], vcol_mult_n.outputs[0]) + node_tree.links.new(horizon_mix_n.inputs[1], horizon_col_n.outputs['Color']) + + node_tree.links.new(normal_scramble_n.inputs['Fac'], mix_factor_n.outputs['Scramble Mix Factor']) + node_tree.links.new(normal_scramble_n.inputs['Color2'], normal_normalize_n.outputs['Vector']) + + # pass 5 + node_tree.links.new(near_horizon_env_mix_n.inputs['Fac'], mix_factor_n.outputs['Mix Factor']) + node_tree.links.new(near_horizon_env_mix_n.inputs['Color1'], near_mix_n.outputs[0]) + + node_tree.links.new(near_horizon_mix_n.inputs['Fac'], mix_factor_n.outputs['Mix Factor']) + node_tree.links.new(near_horizon_mix_n.inputs['Color1'], near_mix_n.outputs[0]) + node_tree.links.new(near_horizon_mix_n.inputs['Color2'], horizon_mix_n.outputs[0]) + + # pass 6 + node_tree.links.new(lighting_eval_n.inputs['Normal Vector'], normal_scramble_n.outputs['Color']) + + # pass 7 + node_tree.links.new(compose_lighting_n.inputs['Env Color'], near_horizon_env_mix_n.outputs['Color']) + node_tree.links.new(compose_lighting_n.inputs['Diffuse Color'], near_horizon_mix_n.outputs['Color']) + # add environment pass and normal maps StdAddEnv.add(node_tree, Dif.GEOM_NODE, node_tree.nodes[Dif.SPEC_COL_NODE].outputs['Color'], None, - None, - node_tree.nodes[Water.ADD_REFL_MIX_NODE].inputs['Color1']) + node_tree.nodes[Water.LIGHTING_EVAL_NODE].outputs['Normal'], + node_tree.nodes[Water.NEAR_HORIZON_ENV_MIX_NODE].inputs['Color1']) + + node_tree.nodes[StdAddEnv.ADD_ENV_GROUP_NODE].inputs['Base Texture Alpha'].default_value = 1 # set full reflection strength + + Water.__init_nmap__(node_tree, + Water.LAYER0_NMAP_UID, + (start_pos_x + pos_x_shift, start_pos_y + 800), + geom_n.outputs['Position'], + water_stream_n.outputs['Stream0'], + geom_n.outputs['Normal'], + lay0_lay1_normal_mix_n.inputs[0]) + + Water.__init_nmap__(node_tree, + Water.LAYER1_NMAP_UID, + (start_pos_x + pos_x_shift, start_pos_y + 500), + geom_n.outputs['Position'], + water_stream_n.outputs['Stream1'], + geom_n.outputs['Normal'], + lay0_lay1_normal_mix_n.inputs[1]) - # links creation - # pass 1 - node_tree.links.new(lay0_lay1_normal_mix_n.inputs['Color1'], layer0_mat_n.outputs['Normal']) - node_tree.links.new(lay0_lay1_normal_mix_n.inputs['Color2'], layer1_mat_n.outputs['Normal']) + @staticmethod + def __init_nmap__(node_tree, uid, location, position_from, stream_from, normal_from, normal_to): + """Initialize nodes for normal map for given unique id water layer. - # pass 2 - node_tree.links.new(normal_normalize_n.inputs[0], lay0_lay1_normal_mix_n.outputs['Color']) - node_tree.links.new(vcol_mult_n.inputs['Color2'], diff_col_n.outputs['Color']) + :param node_tree: node tree on which this shader should be created + :type node_tree: bpy.types.NodeTree + :param uid: unique id of water layer to which all nmap nodes will be prefixed + :type uid: str + :param location: location where nmap should start creating it's nodes + :type location: tuple[int, int] + :param position_from: socket to take position vector from + :type position_from: bpy.types.NodeSocket + :param stream_from: socket to take water stream vector from + :type stream_from: bpy.types.NodeSocket + :param normal_from: socket from which original normal should be taken + :type normal_from: bpy.types.NodeSocket + :param normal_to: socket to which this water layer normal should be put to + :type normal_to: bpy.types.NodeSocket + """ - # pass 3 - node_tree.links.new(near_mix_n.inputs['Color1'], vcol_mult_n.outputs['Color']) - node_tree.links.new(near_mix_n.inputs['Color2'], near_col_n.outputs['Color']) + _STREAM_MIX_NODE = uid + Water.POSTFIX_STREAM_MIX + _MAPPING_NODE = uid + Water.POSTFIX_MAPPING_NODE + _NMAP_TEX_NODE = uid + Water.POSTFIX_NMAP_TEX_NODE + _NMAP_NODE = uid + Water.POSTFIX_NMAP_NODE + _NMAP_SCALE_NODE = uid + Water.POSTFIX_NMAP_SCALE_NODE - node_tree.links.new(horizon_mix_n.inputs['Color1'], vcol_mult_n.outputs['Color']) - node_tree.links.new(horizon_mix_n.inputs['Color2'], horizon_col_n.outputs['Color']) + # nodes + stream_mix_n = node_tree.nodes.new("ShaderNodeVectorMath") + stream_mix_n.name = stream_mix_n.label = Water.LAY0_LAY1_NORMAL_MIX_NODE + stream_mix_n.location = location + stream_mix_n.operation = "ADD" - # pass 4 - node_tree.links.new(add_refl_mix_n.inputs['Color2'], near_mix_n.outputs['Color']) + vector_mapping_n = node_tree.nodes.new("ShaderNodeMapping") + vector_mapping_n.name = vector_mapping_n.label = _MAPPING_NODE + vector_mapping_n.location = (location[0] + 185, location[1]) + vector_mapping_n.vector_type = "POINT" + vector_mapping_n.inputs['Location'].default_value = vector_mapping_n.inputs['Rotation'].default_value = (0.0,) * 3 + vector_mapping_n.inputs['Scale'].default_value = (1.0,) * 3 + vector_mapping_n.width = 140 - # pass 5 - node_tree.links.new(near_horizon_mix_n.inputs['Fac'], mix_factor_gn.outputs['Mix Factor']) - node_tree.links.new(near_horizon_mix_n.inputs['Color1'], add_refl_mix_n.outputs['Color']) - node_tree.links.new(near_horizon_mix_n.inputs['Color2'], horizon_mix_n.outputs['Color']) + nmap_tex_n = node_tree.nodes.new("ShaderNodeTexImage") + nmap_tex_n.name = nmap_tex_n.label = _NMAP_TEX_NODE + nmap_tex_n.location = (location[0] + 185 * 2, location[1]) + nmap_tex_n.width = 140 + + nmap_n = node_tree.nodes.new("ShaderNodeNormalMap") + nmap_n.name = nmap_n.label = _NMAP_NODE + nmap_n.location = (location[0] + 185 * 3, location[1] - 150) + nmap_n.space = "WORLD" + nmap_n.inputs["Strength"].default_value = 1 + + nmap_scale_n = node_tree.nodes.new("ShaderNodeGroup") + nmap_scale_n.name = nmap_scale_n.label = _NMAP_SCALE_NODE + nmap_scale_n.location = (location[0] + 185 * 4, location[1]) + nmap_scale_n.node_tree = scale_ng.get_node_group() + + # links + node_tree.links.new(stream_mix_n.inputs[0], position_from) + node_tree.links.new(stream_mix_n.inputs[1], stream_from) + + node_tree.links.new(vector_mapping_n.inputs['Vector'], stream_mix_n.outputs['Vector']) - # material pass - node_tree.links.new(out_mat_n.inputs['Color'], near_horizon_mix_n.outputs['Color']) - node_tree.links.new(out_mat_n.inputs['Normal'], normal_normalize_n.outputs['Vector']) + node_tree.links.new(nmap_tex_n.inputs['Vector'], vector_mapping_n.outputs['Vector']) - # output pass - node_tree.links.new(output_n.inputs['Color'], out_mat_n.outputs['Color']) + node_tree.links.new(nmap_n.inputs['Color'], nmap_tex_n.outputs['Color']) + + node_tree.links.new(nmap_scale_n.inputs['NMap Tex Color'], nmap_tex_n.outputs['Color']) + node_tree.links.new(nmap_scale_n.inputs['Original Normal'], normal_from) + node_tree.links.new(nmap_scale_n.inputs['Modified Normal'], nmap_n.outputs['Normal']) + + node_tree.links.new(normal_to, nmap_scale_n.outputs['Normal']) @staticmethod def set_aux0(node_tree, aux_property): """Set near distance, far distance and scramble factor. - NOTE: scramble factor is not implemented - :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree :param aux_property: near distance, far distance and scramble factor represented with property group @@ -192,6 +286,7 @@ def set_aux0(node_tree, aux_property): node_tree.nodes[Water.MIX_FACTOR_GNODE].inputs['Near Distance'].default_value = aux_property[0]['value'] node_tree.nodes[Water.MIX_FACTOR_GNODE].inputs['Far Distance'].default_value = aux_property[1]['value'] + node_tree.nodes[Water.MIX_FACTOR_GNODE].inputs['Scramble Distance'].default_value = aux_property[2]['value'] @staticmethod def set_aux1(node_tree, aux_property): @@ -221,119 +316,107 @@ def set_aux2(node_tree, aux_property): def set_aux3(node_tree, aux_property): """Set yaw, speed, texture scaleX and texture scaleY for layer0 texture. - NOTE: yaw and speed are not implemented - :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree :param aux_property: yaw, speed, texture scaleX and texture scaleY represented with property group :type aux_property: bpy.types.IDPropertyGroup """ - layer0_mat_n = node_tree.nodes[Water.LAYER0_MAT_NODE] + _LAYER0_NMAP_MAPPING_NODE = Water.LAYER0_NMAP_UID + Water.POSTFIX_MAPPING_NODE - # if there is no normal map material for layer0 yet - # force set of None texture so material will be created - if not layer0_mat_n.material: - Water.__set_texture__(node_tree, Water.LAYER0_MAT_NODE, None) + layer0_mapping_n = node_tree.nodes[_LAYER0_NMAP_MAPPING_NODE] + layer0_mapping_n.inputs['Scale'].default_value[0] = 1 / aux_property[2]['value'] + layer0_mapping_n.inputs['Scale'].default_value[1] = 1 / aux_property[3]['value'] - layer0_mat_n.material.texture_slots[0].scale.x = 1 / aux_property[2]['value'] - layer0_mat_n.material.texture_slots[0].scale.y = 1 / aux_property[3]['value'] + yaw = math.radians(aux_property[0]['value']) + water_stream_n = node_tree.nodes[Water.WATER_STREAM_NODE] + water_stream_n.inputs['Yaw0'].default_value = (math.sin(yaw), -math.cos(yaw), 0) + water_stream_n.inputs['Speed0'].default_value = aux_property[1]['value'] @staticmethod def set_aux4(node_tree, aux_property): """Set yaw, speed, texture scaleX and texture scaleY for layer1 texture. - NOTE: yaw and speed are not implemented - :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree :param aux_property: yaw, speed, texture scaleX and texture scaleY represented with property group :type aux_property: bpy.types.IDPropertyGroup """ - layer1_mat_n = node_tree.nodes[Water.LAYER1_MAT_NODE] + _LAYER1_NMAP_MAPPING_NODE = Water.LAYER1_NMAP_UID + Water.POSTFIX_MAPPING_NODE - # if there is no normal map material for layer0 yet - # force set of None texture so material will be created - if not layer1_mat_n.material: - Water.__set_texture__(node_tree, Water.LAYER1_MAT_NODE, None) + layer1_mapping_n = node_tree.nodes[_LAYER1_NMAP_MAPPING_NODE] + layer1_mapping_n.inputs['Scale'].default_value[0] = 1 / aux_property[2]['value'] + layer1_mapping_n.inputs['Scale'].default_value[1] = 1 / aux_property[3]['value'] - layer1_mat_n.material.texture_slots[0].scale.x = 1 / aux_property[2]['value'] - layer1_mat_n.material.texture_slots[0].scale.y = 1 / aux_property[3]['value'] + yaw = math.radians(aux_property[0]['value']) + water_stream_n = node_tree.nodes[Water.WATER_STREAM_NODE] + water_stream_n.inputs['Yaw1'].default_value = (math.sin(yaw), -math.cos(yaw), 0) + water_stream_n.inputs['Speed1'].default_value = aux_property[1]['value'] @staticmethod - def __set_texture__(node_tree, layer_mat_node_name, texture): - """Set texture to layer material. + def set_aux5(node_tree, aux_property): + """Enable/disable world space reflections. - :param node_tree: node tree + :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param layer_mat_node_name: name of the layer to set texture to - :type layer_mat_node_name: str - :param texture: texture which should be assigned to layer0 texture node - :type texture: bpy.types.Texture | None + :param aux_property: float enabling world space reflections + :type aux_property: bpy.types.IDPropertyGroup """ + pass # Enabling world space reflections doesn't do anything, thus just pass it. - # save currently active node to properly reset it on the end - # without reset of active node this material is marked as active which we don't want - old_active = node_tree.nodes.active - - # search possible existing materials and use it - material = None - i = 1 - while ".scs_nmap_" + str(i) in bpy.data.materials: - - curr_mat = bpy.data.materials[".scs_nmap_" + str(i)] - - # grab only material without any users and clear all texture slots - if curr_mat.users == 0: - material = curr_mat + @staticmethod + def set_layer0_texture(node_tree, image): + """Set texture to layer0 material. - for i in range(0, len(material.texture_slots)): - material.texture_slots.clear(i) + :param node_tree: node tree + :type node_tree: bpy.types.NodeTree + :param image: texture image which should be assigned to layer0 texture node + :type image: bpy.types.Image + """ - i += 1 + _LAYER0_MMAP_NODE = Water.LAYER0_NMAP_UID + Water.POSTFIX_NMAP_TEX_NODE - # if none is found create new one - if not material: - material = bpy.data.materials.new(".scs_nmap_" + str(i)) + node_tree.nodes[_LAYER0_MMAP_NODE].image = image - # finally set texture and it's properties to material - tex_slot = material.texture_slots.add() - tex_slot.texture_coords = "GLOBAL" - tex_slot.use_map_color_diffuse = False - tex_slot.use_map_normal = True - tex_slot.texture = texture - tex_slot.normal_map_space = "TANGENT" + @staticmethod + def set_layer0_texture_settings(node_tree, settings): + """Set layer0 texture settings to shader. - node_tree.nodes[layer_mat_node_name].material = material + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ - # trigger set methods for auxiliary items, just to make sure any previously set aux values don't get lost - # during material creation in this method - if layer_mat_node_name == Water.LAYER0_MAT_NODE: - Water.set_aux3(node_tree, node_tree.nodes[Dif.OUT_MAT_NODE].material.scs_props.shader_attribute_aux3) - elif layer_mat_node_name == Water.LAYER1_MAT_NODE: - Water.set_aux4(node_tree, node_tree.nodes[Dif.OUT_MAT_NODE].material.scs_props.shader_attribute_aux4) + _LAYER0_MMAP_NODE = Water.LAYER0_NMAP_UID + Water.POSTFIX_NMAP_TEX_NODE - node_tree.nodes.active = old_active + _material_utils.set_texture_settings_to_node(node_tree.nodes[_LAYER0_MMAP_NODE], settings) @staticmethod - def set_layer0_texture(node_tree, texture): - """Set texture to layer0 material. + def set_layer1_texture(node_tree, image): + """Set texture to layer1 material. :param node_tree: node tree :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assigned to layer0 texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assigned to layer1 texture node + :type image: bpy.types.Image """ - Water.__set_texture__(node_tree, Water.LAYER0_MAT_NODE, texture) + + _LAYER1_MMAP_NODE = Water.LAYER1_NMAP_UID + Water.POSTFIX_NMAP_TEX_NODE + + node_tree.nodes[_LAYER1_MMAP_NODE].image = image @staticmethod - def set_layer1_texture(node_tree, texture): - """Set texture to layer1 material. + def set_layer1_texture_settings(node_tree, settings): + """Set layer1 texture settings to shader. - :param node_tree: node tree + :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assigned to layer1 texture node - :type texture: bpy.types.Texture + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str """ - Water.__set_texture__(node_tree, Water.LAYER1_MAT_NODE, texture) + + _LAYER1_MMAP_NODE = Water.LAYER1_NMAP_UID + Water.POSTFIX_NMAP_TEX_NODE + + _material_utils.set_texture_settings_to_node(node_tree.nodes[_LAYER1_MMAP_NODE], settings) diff --git a/addon/io_scs_tools/internals/shaders/eut2/water/mix_factor_ng.py b/addon/io_scs_tools/internals/shaders/eut2/water/mix_factor_ng.py index 03df6b8..426ed10 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/water/mix_factor_ng.py +++ b/addon/io_scs_tools/internals/shaders/eut2/water/mix_factor_ng.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software import bpy from io_scs_tools.consts import Material as _MAT_consts @@ -57,13 +57,15 @@ def __create_group__(): # inputs defining detail_setup_g.inputs.new("NodeSocketFloat", "Near Distance") detail_setup_g.inputs.new("NodeSocketFloat", "Far Distance") + detail_setup_g.inputs.new("NodeSocketFloat", "Scramble Distance") input_n = detail_setup_g.nodes.new("NodeGroupInput") input_n.location = (start_pos_x, start_pos_y) # outputs defining detail_setup_g.outputs.new("NodeSocketFloat", "Mix Factor") + detail_setup_g.outputs.new("NodeSocketFloat", "Scramble Mix Factor") output_n = detail_setup_g.nodes.new("NodeGroupOutput") - output_n.location = (start_pos_x + pos_x_shift * 5, start_pos_y) + output_n.location = (start_pos_x + pos_x_shift * 7, start_pos_y) # group nodes camera_data_n = detail_setup_g.nodes.new("ShaderNodeCameraData") @@ -72,7 +74,8 @@ def __create_group__(): equation_nodes = [] for i, name in enumerate(("ZDeptInv", "ZDeptInv+NearDistance", "NearDistance-FarDistance", - "(ZDeptInv+NearDistance)/(NearDistance-FarDistance)")): + "(ZDeptInv+NearDistance)/(NearDistance-FarDistance)", "ZDeptInv/ScrambleDistance", + "1/(ZDeptInv/ScrambleDistance)")): # node creation equation_nodes.append(detail_setup_g.nodes.new("ShaderNodeMath")) @@ -107,3 +110,20 @@ def __create_group__(): detail_setup_g.links.new(equation_nodes[i].inputs[1], equation_nodes[i - 1].outputs[0]) detail_setup_g.links.new(output_n.inputs['Mix Factor'], equation_nodes[i].outputs[0]) + + elif i == 4: + + equation_nodes[i].operation = "DIVIDE" + equation_nodes[i].location.y -= 200 + detail_setup_g.links.new(equation_nodes[i].inputs[0], camera_data_n.outputs['View Distance']) + detail_setup_g.links.new(equation_nodes[i].inputs[1], input_n.outputs['Scramble Distance']) + + elif i == 5: + + equation_nodes[i].operation = "DIVIDE" + equation_nodes[i].location.y -= 200 + + equation_nodes[i].inputs[0].default_value = 1 + detail_setup_g.links.new(equation_nodes[i].inputs[1], equation_nodes[i - 1].outputs[0]) + + detail_setup_g.links.new(output_n.inputs['Scramble Mix Factor'], equation_nodes[i].outputs[0]) diff --git a/addon/io_scs_tools/internals/shaders/eut2/water/water_stream_ng.py b/addon/io_scs_tools/internals/shaders/eut2/water/water_stream_ng.py new file mode 100644 index 0000000..63041c2 --- /dev/null +++ b/addon/io_scs_tools/internals/shaders/eut2/water/water_stream_ng.py @@ -0,0 +1,127 @@ +# ##### 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 + +import bpy +from io_scs_tools.consts import Material as _MAT_consts + +WATER_STREAM_G = _MAT_consts.node_group_prefix + "WaterStream" + +_ANIM_TIME_NODE = "AnimTimeNode" + +_STREAM0_SPEED_MULT_NODE = "Stream0SpeedMultiply" +_STREAM0_TIME_ADD_NODE = "Stream0TimeAdd" +_STREAM1_SPEED_MULT_NODE = "Stream1SpeedMultiply" +_STREAM1_TIME_ADD_NODE = "Stream1TimeAdd" + + +def get_node_group(): + """Gets node group for water stream calculation. + + :return: node group which calculates water stream vectors + :rtype: bpy.types.NodeGroup + """ + + if WATER_STREAM_G not in bpy.data.node_groups: + __create_node_group__() + + return bpy.data.node_groups[WATER_STREAM_G] + + +def update_time(scene): + """Updates stream velocity value. + + :param scene: scene in which time for shaders is being updated + :type scene: bpy.types.Scene + """ + + if WATER_STREAM_G not in bpy.data.node_groups: + return + + time = scene.frame_current / (scene.render.fps / scene.render.fps_base) + bpy.data.node_groups[WATER_STREAM_G].nodes[_ANIM_TIME_NODE].outputs[0].default_value = time + + +def __create_node_group__(): + """Creates add env group. + + Inputs: Fresnel Scale, Fresnel Bias, Normal Vector, View Vector, Apply Fresnel, + Reflection Texture Color, Base Texture Alpha, Env Factor Color and Specular Color + Outputs: Environment Addition Color + """ + + start_pos_x = 0 + start_pos_y = 0 + + pos_x_shift = 185 + + water_stream_ng = bpy.data.node_groups.new(type="ShaderNodeTree", name=WATER_STREAM_G) + + # inputs defining + water_stream_ng.inputs.new("NodeSocketVector", "Yaw0") + water_stream_ng.inputs.new("NodeSocketFloat", "Speed0") + water_stream_ng.inputs.new("NodeSocketVector", "Yaw1") + water_stream_ng.inputs.new("NodeSocketFloat", "Speed1") + input_n = water_stream_ng.nodes.new("NodeGroupInput") + input_n.location = (start_pos_x - pos_x_shift, start_pos_y) + + # outputs defining + water_stream_ng.outputs.new("NodeSocketVector", "Stream0") + water_stream_ng.outputs.new("NodeSocketVector", "Stream1") + output_n = water_stream_ng.nodes.new("NodeGroupOutput") + output_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y) + + # node creation + stream0_speed_mult_n = water_stream_ng.nodes.new("ShaderNodeVectorMath") + stream0_speed_mult_n.name = stream0_speed_mult_n.label = _STREAM0_SPEED_MULT_NODE + stream0_speed_mult_n.location = (start_pos_x, start_pos_y + 100) + stream0_speed_mult_n.operation = "MULTIPLY" + + stream1_speed_mult_n = water_stream_ng.nodes.new("ShaderNodeVectorMath") + stream1_speed_mult_n.name = stream1_speed_mult_n.label = _STREAM1_SPEED_MULT_NODE + stream1_speed_mult_n.location = (start_pos_x, start_pos_y - 100) + stream1_speed_mult_n.operation = "MULTIPLY" + + anim_time_n = water_stream_ng.nodes.new("ShaderNodeValue") + anim_time_n.name = anim_time_n.label = _ANIM_TIME_NODE + anim_time_n.location = (start_pos_x, start_pos_y + 300) + + stream0_time_mult_n = water_stream_ng.nodes.new("ShaderNodeVectorMath") + stream0_time_mult_n.name = stream0_time_mult_n.label = _STREAM0_TIME_ADD_NODE + stream0_time_mult_n.location = (start_pos_x + pos_x_shift, start_pos_y + 100) + stream0_time_mult_n.operation = "MULTIPLY" + + stream1_time_mult_n = water_stream_ng.nodes.new("ShaderNodeVectorMath") + stream1_time_mult_n.name = stream1_time_mult_n.label = _STREAM1_TIME_ADD_NODE + stream1_time_mult_n.location = (start_pos_x + pos_x_shift, start_pos_y - 100) + stream1_time_mult_n.operation = "MULTIPLY" + + # links + water_stream_ng.links.new(stream0_speed_mult_n.inputs[0], input_n.outputs['Yaw0']) + water_stream_ng.links.new(stream0_speed_mult_n.inputs[1], input_n.outputs['Speed0']) + water_stream_ng.links.new(stream1_speed_mult_n.inputs[0], input_n.outputs['Yaw1']) + water_stream_ng.links.new(stream1_speed_mult_n.inputs[1], input_n.outputs['Speed1']) + + water_stream_ng.links.new(stream0_time_mult_n.inputs[0], anim_time_n.outputs[0]) + water_stream_ng.links.new(stream0_time_mult_n.inputs[1], stream0_speed_mult_n.outputs[0]) + water_stream_ng.links.new(stream1_time_mult_n.inputs[0], anim_time_n.outputs[0]) + water_stream_ng.links.new(stream1_time_mult_n.inputs[1], stream1_speed_mult_n.outputs[0]) + + water_stream_ng.links.new(output_n.inputs['Stream0'], stream0_time_mult_n.outputs[0]) + water_stream_ng.links.new(output_n.inputs['Stream1'], stream1_time_mult_n.outputs[0]) diff --git a/addon/io_scs_tools/internals/shaders/eut2/window/day.py b/addon/io_scs_tools/internals/shaders/eut2/window/day.py index c21f476..b718f49 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/window/day.py +++ b/addon/io_scs_tools/internals/shaders/eut2/window/day.py @@ -16,11 +16,10 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.internals.shaders.eut2.dif_spec_add_env import DifSpecAddEnv -from io_scs_tools.internals.shaders.eut2.std_node_groups import window_uv_offset +from io_scs_tools.internals.shaders.eut2.window import window_uv_offset_ng class WindowDay(DifSpecAddEnv): @@ -44,26 +43,28 @@ def init(node_tree): DifSpecAddEnv.init(node_tree) geom_n = node_tree.nodes[DifSpecAddEnv.GEOM_NODE] + uv_map_n = node_tree.nodes[DifSpecAddEnv.UVMAP_NODE] spec_col_n = node_tree.nodes[DifSpecAddEnv.SPEC_COL_NODE] base_tex_n = node_tree.nodes[DifSpecAddEnv.BASE_TEX_NODE] - out_mat_n = node_tree.nodes[DifSpecAddEnv.OUT_MAT_NODE] + compose_lighting_n = node_tree.nodes[DifSpecAddEnv.COMPOSE_LIGHTING_NODE] node_tree.nodes.remove(node_tree.nodes[DifSpecAddEnv.SPEC_MULT_NODE]) # create nodes - uv_recalc_gn = node_tree.nodes.new("ShaderNodeGroup") - uv_recalc_gn.name = window_uv_offset.WINDOW_UV_OFFSET_G - uv_recalc_gn.label = window_uv_offset.WINDOW_UV_OFFSET_G - uv_recalc_gn.location = (start_pos_x, start_pos_y + 1500) - uv_recalc_gn.node_tree = window_uv_offset.get_node_group() + uv_recalc_n = node_tree.nodes.new("ShaderNodeGroup") + uv_recalc_n.name = window_uv_offset_ng.WINDOW_UV_OFFSET_G + uv_recalc_n.label = window_uv_offset_ng.WINDOW_UV_OFFSET_G + uv_recalc_n.location = (start_pos_x, start_pos_y + 1500) + uv_recalc_n.node_tree = window_uv_offset_ng.get_node_group() # create links - node_tree.links.new(out_mat_n.inputs["Spec"], spec_col_n.outputs["Color"]) + node_tree.links.new(compose_lighting_n.inputs['Specular Color'], spec_col_n.outputs['Color']) - node_tree.links.new(uv_recalc_gn.inputs["UV"], geom_n.outputs["UV"]) - node_tree.links.new(uv_recalc_gn.inputs["Normal"], geom_n.outputs["Normal"]) + node_tree.links.new(uv_recalc_n.inputs['UV'], uv_map_n.outputs['UV']) + node_tree.links.new(uv_recalc_n.inputs['Normal'], geom_n.outputs['Normal']) + node_tree.links.new(uv_recalc_n.inputs['Incoming'], geom_n.outputs['Incoming']) - node_tree.links.new(base_tex_n.inputs["Vector"], uv_recalc_gn.outputs["UV Final"]) + node_tree.links.new(base_tex_n.inputs['Vector'], uv_recalc_n.outputs['UV Final']) @staticmethod def set_lightmap_texture(node_tree, texture): @@ -76,6 +77,17 @@ def set_lightmap_texture(node_tree, texture): """ pass # NOTE: light map texture is not used in day version of effect, so pass it + @staticmethod + def set_lightmap_texture_settings(node_tree, settings): + """Set lightmap texture settings to shader. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + pass # NOTE: light map texture is not used in day version of effect, so pass it + @staticmethod def set_lightmap_uv(node_tree, uv_layer): """Set UV layer to lightmap texture in shader. diff --git a/addon/io_scs_tools/internals/shaders/eut2/window/night.py b/addon/io_scs_tools/internals/shaders/eut2/window/night.py index 3df5aff..122e710 100644 --- a/addon/io_scs_tools/internals/shaders/eut2/window/night.py +++ b/addon/io_scs_tools/internals/shaders/eut2/window/night.py @@ -16,20 +16,19 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2015-2019: SCS Software from io_scs_tools.consts import Mesh as _MESH_consts from io_scs_tools.internals.shaders.eut2.dif_spec_add_env import DifSpecAddEnv -from io_scs_tools.internals.shaders.eut2.std_node_groups import window_uv_offset +from io_scs_tools.internals.shaders.eut2.window import window_uv_offset_ng +from io_scs_tools.utils import material as _material_utils class WindowNight(DifSpecAddEnv): - SEC_GEOM_NODE = "SecGeometry" + SEC_UV_MAP = "SecUVMap" LIGHTMAP_TEX_NODE = "LightmapTex" BASE_LIGHTMAP_MULT_NODE = "BaseLightmapMultiplier" BASE_ALPHA_MULT_NODE = "BaseAlphaMultiplier" - OUT_ADD_SPEC_NODE = "OutAddSpecular" @staticmethod def get_name(): @@ -52,17 +51,12 @@ def init(node_tree): # init parent DifSpecAddEnv.init(node_tree) + uv_map_n = node_tree.nodes[DifSpecAddEnv.UVMAP_NODE] geom_n = node_tree.nodes[DifSpecAddEnv.GEOM_NODE] spec_col_n = node_tree.nodes[DifSpecAddEnv.SPEC_COL_NODE] base_tex_n = node_tree.nodes[DifSpecAddEnv.BASE_TEX_NODE] diff_mult_n = node_tree.nodes[DifSpecAddEnv.DIFF_MULT_NODE] - out_mat_n = node_tree.nodes[DifSpecAddEnv.OUT_MAT_NODE] compose_lighting_n = node_tree.nodes[DifSpecAddEnv.COMPOSE_LIGHTING_NODE] - output_n = node_tree.nodes[DifSpecAddEnv.OUTPUT_NODE] - - # move existing - output_n.location.x += pos_x_shift - out_mat_n.location.y -= 150 # remove existing node_tree.nodes.remove(node_tree.nodes[DifSpecAddEnv.SPEC_MULT_NODE]) @@ -71,87 +65,76 @@ def init(node_tree): node_tree.nodes.remove(node_tree.nodes[DifSpecAddEnv.VCOLOR_MULT_NODE]) node_tree.nodes.remove(node_tree.nodes[DifSpecAddEnv.OPACITY_NODE]) - for link in node_tree.links: - if link.from_node == diff_mult_n and link.to_node == out_mat_n: - node_tree.links.remove(link) - break - # create nodes - uv_recalc_gn = node_tree.nodes.new("ShaderNodeGroup") - uv_recalc_gn.name = window_uv_offset.WINDOW_UV_OFFSET_G - uv_recalc_gn.label = window_uv_offset.WINDOW_UV_OFFSET_G - uv_recalc_gn.location = (start_pos_x, start_pos_y + 1500) - uv_recalc_gn.node_tree = window_uv_offset.get_node_group() - - sec_geom_n = node_tree.nodes.new("ShaderNodeGeometry") - sec_geom_n.name = sec_geom_n.label = WindowNight.SEC_GEOM_NODE - sec_geom_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1200) - sec_geom_n.uv_layer = _MESH_consts.none_uv - - lightmap_tex_n = node_tree.nodes.new("ShaderNodeTexture") + uv_recalc_n = node_tree.nodes.new("ShaderNodeGroup") + uv_recalc_n.name = window_uv_offset_ng.WINDOW_UV_OFFSET_G + uv_recalc_n.label = window_uv_offset_ng.WINDOW_UV_OFFSET_G + uv_recalc_n.location = (start_pos_x, start_pos_y + 1500) + uv_recalc_n.node_tree = window_uv_offset_ng.get_node_group() + + sec_uv_map_n = node_tree.nodes.new("ShaderNodeUVMap") + sec_uv_map_n.name = sec_uv_map_n.label = WindowNight.SEC_UV_MAP + sec_uv_map_n.location = (start_pos_x - pos_x_shift, start_pos_y + 1000) + sec_uv_map_n.uv_map = _MESH_consts.none_uv + + lightmap_tex_n = node_tree.nodes.new("ShaderNodeTexImage") lightmap_tex_n.name = lightmap_tex_n.label = WindowNight.LIGHTMAP_TEX_NODE lightmap_tex_n.location = (start_pos_x + pos_x_shift, start_pos_y + 1200) + lightmap_tex_n.width = 140 - base_lightmap_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") + base_lightmap_mult_n = node_tree.nodes.new("ShaderNodeVectorMath") base_lightmap_mult_n.name = base_lightmap_mult_n.label = WindowNight.BASE_LIGHTMAP_MULT_NODE base_lightmap_mult_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y + 1300) - base_lightmap_mult_n.blend_type = "MULTIPLY" - base_lightmap_mult_n.inputs["Fac"].default_value = 1.0 + base_lightmap_mult_n.operation = "MULTIPLY" - base_alpha_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") + base_alpha_mult_n = node_tree.nodes.new("ShaderNodeVectorMath") base_alpha_mult_n.name = base_alpha_mult_n.label = WindowNight.BASE_ALPHA_MULT_NODE base_alpha_mult_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y + 1500) - base_alpha_mult_n.blend_type = "MULTIPLY" - base_alpha_mult_n.inputs["Fac"].default_value = 1.0 - - out_add_spec_n = node_tree.nodes.new("ShaderNodeMixRGB") - out_add_spec_n.name = out_add_spec_n.label = WindowNight.BASE_ALPHA_MULT_NODE - out_add_spec_n.location = (start_pos_x + pos_x_shift * 9, start_pos_y + 1900) - out_add_spec_n.blend_type = "ADD" - out_add_spec_n.inputs["Fac"].default_value = 1.0 + base_alpha_mult_n.operation = "MULTIPLY" # create links - node_tree.links.new(uv_recalc_gn.inputs["UV"], geom_n.outputs["UV"]) - node_tree.links.new(uv_recalc_gn.inputs["Normal"], geom_n.outputs["Normal"]) + node_tree.links.new(uv_recalc_n.inputs['UV'], uv_map_n.outputs['UV']) + node_tree.links.new(uv_recalc_n.inputs['Normal'], geom_n.outputs['Normal']) + node_tree.links.new(uv_recalc_n.inputs['Incoming'], geom_n.outputs['Incoming']) - node_tree.links.new(base_tex_n.inputs["Vector"], uv_recalc_gn.outputs["UV Final"]) - node_tree.links.new(lightmap_tex_n.inputs["Vector"], sec_geom_n.outputs["UV"]) + node_tree.links.new(base_tex_n.inputs['Vector'], uv_recalc_n.outputs['UV Final']) + node_tree.links.new(lightmap_tex_n.inputs['Vector'], sec_uv_map_n.outputs['UV']) # pass 1 - node_tree.links.new(base_lightmap_mult_n.inputs["Color1"], base_tex_n.outputs["Color"]) - node_tree.links.new(base_lightmap_mult_n.inputs["Color2"], lightmap_tex_n.outputs["Color"]) + node_tree.links.new(base_lightmap_mult_n.inputs[0], base_tex_n.outputs['Color']) + node_tree.links.new(base_lightmap_mult_n.inputs[1], lightmap_tex_n.outputs['Color']) # pass 2 - node_tree.links.new(base_alpha_mult_n.inputs["Color1"], base_tex_n.outputs["Value"]) - node_tree.links.new(base_alpha_mult_n.inputs["Color2"], base_lightmap_mult_n.outputs["Color"]) + node_tree.links.new(base_alpha_mult_n.inputs[0], base_tex_n.outputs['Alpha']) + node_tree.links.new(base_alpha_mult_n.inputs[1], base_lightmap_mult_n.outputs[0]) # pass 3 - node_tree.links.new(diff_mult_n.inputs["Color2"], base_alpha_mult_n.outputs["Color"]) - - # output material - node_tree.links.new(out_mat_n.inputs["Spec"], spec_col_n.outputs["Color"]) + node_tree.links.new(diff_mult_n.inputs[1], base_alpha_mult_n.outputs[0]) - # post pass 1 - node_tree.links.new(compose_lighting_n.inputs["Diffuse Color"], diff_mult_n.outputs["Color"]) - node_tree.links.new(compose_lighting_n.inputs["Material Color"], diff_mult_n.outputs["Color"]) + # pass 4 + node_tree.links.new(compose_lighting_n.inputs['Specular Color'], spec_col_n.outputs['Color']) - # post pass 2 - node_tree.links.new(out_add_spec_n.inputs["Color1"], compose_lighting_n.outputs["Composed Color"]) - node_tree.links.new(out_add_spec_n.inputs["Color2"], out_mat_n.outputs["Spec"]) + @staticmethod + def set_lightmap_texture(node_tree, image): + """Set lightmap texture to shader. - # output pass - node_tree.links.new(output_n.inputs["Color"], out_add_spec_n.outputs["Color"]) + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param image: texture image which should be assignet to lightmap texture node + :type image: bpy.types.Image + """ + node_tree.nodes[WindowNight.LIGHTMAP_TEX_NODE].image = image @staticmethod - def set_lightmap_texture(node_tree, texture): - """Set lightmap texture to shader. + def set_lightmap_texture_settings(node_tree, settings): + """Set lightmap texture settings to shader. :param node_tree: node tree of current shader :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assignet to lightmap texture node - :type texture: bpy.types.Texture + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str """ - node_tree.nodes[WindowNight.LIGHTMAP_TEX_NODE].texture = texture + _material_utils.set_texture_settings_to_node(node_tree.nodes[WindowNight.LIGHTMAP_TEX_NODE], settings) @staticmethod def set_lightmap_uv(node_tree, uv_layer): @@ -166,4 +149,4 @@ def set_lightmap_uv(node_tree, uv_layer): if uv_layer is None or uv_layer == "": uv_layer = _MESH_consts.none_uv - node_tree.nodes[WindowNight.SEC_GEOM_NODE].uv_layer = uv_layer + node_tree.nodes[WindowNight.SEC_UV_MAP].uv_map = uv_layer diff --git a/addon/io_scs_tools/internals/shaders/eut2/window/window_final_uv_ng.py b/addon/io_scs_tools/internals/shaders/eut2/window/window_final_uv_ng.py new file mode 100644 index 0000000..ae85fbd --- /dev/null +++ b/addon/io_scs_tools/internals/shaders/eut2/window/window_final_uv_ng.py @@ -0,0 +1,104 @@ +# ##### 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 + +import bpy +from io_scs_tools.consts import Material as _MAT_consts + +WINDOW_FINAL_UV_G = _MAT_consts.node_group_prefix + "WindowFinalUV" + + +def get_node_group(): + """Gets node group for calculation of final uvs. + For more information take a look at: eut2.window.cg + + :return: node group which calculates change factor + :rtype: bpy.types.NodeGroup + """ + + if WINDOW_FINAL_UV_G not in bpy.data.node_groups: + __create_node_group__() + + return bpy.data.node_groups[WINDOW_FINAL_UV_G] + + +def __create_node_group__(): + """Create final UV calculation group. + + Inputs: UV, Factor + Outputs: Final UV + """ + + pos_x_shift = 185 + + final_uv_g = bpy.data.node_groups.new(type="ShaderNodeTree", name=WINDOW_FINAL_UV_G) + + # inputs defining + final_uv_g.inputs.new("NodeSocketFloat", "UV") + final_uv_g.inputs.new("NodeSocketFloat", "Factor") + input_n = final_uv_g.nodes.new("NodeGroupInput") + input_n.location = (0, 0) + + # outputs defining + final_uv_g.outputs.new("NodeSocketFloat", "Final UV") + output_n = final_uv_g.nodes.new("NodeGroupOutput") + output_n.location = (pos_x_shift * 6, 0) + + # group nodes + # UV_STEPS = floor(uv * 0.5) + half_scale_n = final_uv_g.nodes.new("ShaderNodeMath") + half_scale_n.location = (pos_x_shift * 1, 150) + half_scale_n.operation = "MULTIPLY" + half_scale_n.inputs[1].default_value = 0.5 + + floor_n = final_uv_g.nodes.new("ShaderNodeMath") + floor_n.location = (pos_x_shift * 2, 150) + floor_n.operation = "FLOOR" + + # OFFSET_STEP = 1/512 * UV_STEPS + offset_step_n = final_uv_g.nodes.new("ShaderNodeMath") + offset_step_n.location = (pos_x_shift * 3, 150) + offset_step_n.operation = "MULTIPLY" + offset_step_n.inputs[1].default_value = 1.0 / 512.0 + + # FINAL_OFFSET = OFFSET_STEP * FACTOR + final_offset_n = final_uv_g.nodes.new("ShaderNodeMath") + final_offset_n.location = (pos_x_shift * 4, -100) + final_offset_n.operation = "MULTIPLY" + + # FINAL_UVS = FINAL_OFFSET + UV + final_uvs_n = final_uv_g.nodes.new("ShaderNodeMath") + final_uvs_n.location = (pos_x_shift * 5, 50) + final_uvs_n.operation = "ADD" + + # group links + # formula: floor(uv * 0.5) * 1/512 * factor + uv + final_uv_g.links.new(half_scale_n.inputs[0], input_n.outputs['UV']) + + final_uv_g.links.new(floor_n.inputs[0], half_scale_n.outputs[0]) + + final_uv_g.links.new(offset_step_n.inputs[0], floor_n.outputs[0]) + + final_uv_g.links.new(final_offset_n.inputs[0], offset_step_n.outputs[0]) + final_uv_g.links.new(final_offset_n.inputs[1], input_n.outputs['Factor']) + + final_uv_g.links.new(final_uvs_n.inputs[1], final_offset_n.outputs[0]) + final_uv_g.links.new(final_uvs_n.inputs[0], input_n.outputs['UV']) + + final_uv_g.links.new(output_n.inputs['Final UV'], final_uvs_n.outputs[0]) diff --git a/addon/io_scs_tools/internals/shaders/eut2/window/window_offset_factor_ng.py b/addon/io_scs_tools/internals/shaders/eut2/window/window_offset_factor_ng.py new file mode 100644 index 0000000..1bd24c9 --- /dev/null +++ b/addon/io_scs_tools/internals/shaders/eut2/window/window_offset_factor_ng.py @@ -0,0 +1,115 @@ +# ##### 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 + +import bpy +from io_scs_tools.consts import Material as _MAT_consts + +WINDOW_OFFSET_FACTOR_G = _MAT_consts.node_group_prefix + "WindowOffsetFactor" + + +def get_node_group(): + """Gets node group for calculation of uv offset factor. + For more information take a look at: eut2.window.cg + + :return: node group which calculates change factor + :rtype: bpy.types.NodeGroup + """ + + if WINDOW_OFFSET_FACTOR_G not in bpy.data.node_groups: + __create_node_group__() + + return bpy.data.node_groups[WINDOW_OFFSET_FACTOR_G] + + +def __create_node_group__(): + """Create offset factor calculation group. + + Inputs: WndToEye Direction, WndToEye Up + Outputs: Offset Factor + """ + + pos_x_shift = 185 + + offset_factor_g = bpy.data.node_groups.new(type="ShaderNodeTree", name=WINDOW_OFFSET_FACTOR_G) + + # inputs defining + offset_factor_g.inputs.new("NodeSocketFloat", "WndToEye Up") + offset_factor_g.inputs.new("NodeSocketFloat", "WndToEye Direction") + input_n = offset_factor_g.nodes.new("NodeGroupInput") + input_n.location = (0, 0) + + # outputs defining + offset_factor_g.outputs.new("NodeSocketFloat", "Offset Factor") + output_n = offset_factor_g.nodes.new("NodeGroupOutput") + output_n.location = (pos_x_shift * 6, 0) + + # group nodes + # ANGLE_TO_ROTATION = saturate(acos(WndToEyeUp)) + arcos_n = offset_factor_g.nodes.new("ShaderNodeMath") + arcos_n.location = (pos_x_shift * 1, 200) + arcos_n.operation = "ARCCOSINE" + arcos_n.use_clamp = True + + # SIGN = -sign(WndToEyeDirection) + sign_gt_n = offset_factor_g.nodes.new("ShaderNodeMath") + sign_gt_n.location = (pos_x_shift * 1, 0) + sign_gt_n.operation = "GREATER_THAN" + sign_gt_n.inputs[1].default_value = 0.0 + + sign_lt_n = offset_factor_g.nodes.new("ShaderNodeMath") + sign_lt_n.location = (pos_x_shift * 1, -200) + sign_lt_n.operation = "LESS_THAN" + sign_lt_n.inputs[1].default_value = 0.0 + + sign_gt_mult_n = offset_factor_g.nodes.new("ShaderNodeMath") + sign_gt_mult_n.location = (pos_x_shift * 2, 0) + sign_gt_mult_n.operation = "MULTIPLY" + sign_gt_mult_n.inputs[1].default_value = 1.0 + + sign_lt_mult_n = offset_factor_g.nodes.new("ShaderNodeMath") + sign_lt_mult_n.location = (pos_x_shift * 2, -200) + sign_lt_mult_n.operation = "MULTIPLY" + sign_lt_mult_n.inputs[1].default_value = -1.0 + + final_sign_n = offset_factor_g.nodes.new("ShaderNodeMath") + final_sign_n.location = (pos_x_shift * 3, -100) + final_sign_n.operation = "ADD" + + # FINAL_FACTOR = SIGN * ANGLE_TO_ROTATION + final_factor_n = offset_factor_g.nodes.new("ShaderNodeMath") + final_factor_n.location = (pos_x_shift * 5, 100) + final_factor_n.operation = "MULTIPLY" + + # group links + # formula: -sign(WndToEyeDirection) * angle_to_rotation(WndToEyeUp) + offset_factor_g.links.new(arcos_n.inputs[0], input_n.outputs['WndToEye Up']) + offset_factor_g.links.new(sign_gt_n.inputs[0], input_n.outputs['WndToEye Direction']) + offset_factor_g.links.new(sign_lt_n.inputs[0], input_n.outputs['WndToEye Direction']) + + offset_factor_g.links.new(sign_gt_mult_n.inputs[0], sign_gt_n.outputs[0]) + offset_factor_g.links.new(sign_lt_mult_n.inputs[0], sign_lt_n.outputs[0]) + + offset_factor_g.links.new(final_sign_n.inputs[0], sign_gt_mult_n.outputs[0]) + offset_factor_g.links.new(final_sign_n.inputs[1], sign_lt_mult_n.outputs[0]) + + offset_factor_g.links.new(final_factor_n.inputs[0], arcos_n.outputs[0]) + offset_factor_g.links.new(final_factor_n.inputs[1], final_sign_n.outputs[0]) + + offset_factor_g.links.new(output_n.inputs['Offset Factor'], final_factor_n.outputs[0]) diff --git a/addon/io_scs_tools/internals/shaders/eut2/window/window_uv_offset_ng.py b/addon/io_scs_tools/internals/shaders/eut2/window/window_uv_offset_ng.py new file mode 100644 index 0000000..5bb8046 --- /dev/null +++ b/addon/io_scs_tools/internals/shaders/eut2/window/window_uv_offset_ng.py @@ -0,0 +1,262 @@ +# ##### 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 + +import bpy +from io_scs_tools.consts import Material as _MAT_consts +from io_scs_tools.internals.shaders.eut2.std_node_groups import scs_uvs_combine_ng +from io_scs_tools.internals.shaders.eut2.std_node_groups import scs_uvs_separate_ng +from io_scs_tools.internals.shaders.eut2.window import window_offset_factor_ng +from io_scs_tools.internals.shaders.eut2.window import window_final_uv_ng + +WINDOW_UV_OFFSET_G = _MAT_consts.node_group_prefix + "WindowUVOffsetGroup" + + +def get_node_group(): + """Gets node group for uv offseting based on view angle. + For more information take a look at: eut2.window.cg + + NOTE: Due to lack of data view angle is calclulated + based on normal of the mesh face. + :return: node group which calculates change factor + :rtype: bpy.types.NodeGroup + """ + + if WINDOW_UV_OFFSET_G not in bpy.data.node_groups: + __create_node_group__() + + return bpy.data.node_groups[WINDOW_UV_OFFSET_G] + + +def __create_node_group__(): + """Create UV factor group. + + Inputs: UV, Normal, Incoming + Outputs: UV Final + """ + + pos_x_shift = 185 + + uv_offset_g = bpy.data.node_groups.new(type="ShaderNodeTree", name=WINDOW_UV_OFFSET_G) + + # inputs defining + uv_offset_g.inputs.new("NodeSocketVector", "UV") + uv_offset_g.inputs.new("NodeSocketVector", "Normal") + uv_offset_g.inputs.new("NodeSocketVector", "Incoming") + input_n = uv_offset_g.nodes.new("NodeGroupInput") + input_n.location = (0, 0) + + # outputs defining + uv_offset_g.outputs.new("NodeSocketVector", "UV Final") + output_n = uv_offset_g.nodes.new("NodeGroupOutput") + output_n.location = (pos_x_shift * 12, 0) + + # group nodes + # pass 1 + es_world_up_n = uv_offset_g.nodes.new("ShaderNodeVectorTransform") + es_world_up_n.name = es_world_up_n.label = "ESWorldUp" + es_world_up_n.location = (pos_x_shift * 1, 200) + es_world_up_n.vector_type = "NORMAL" + es_world_up_n.convert_from = "WORLD" + es_world_up_n.convert_to = "CAMERA" + es_world_up_n.inputs[0].default_value = (0, 0, 1) + + es_normal_n = uv_offset_g.nodes.new("ShaderNodeVectorTransform") + es_normal_n.name = es_normal_n.label = "ESNormal" + es_normal_n.location = (pos_x_shift * 1, 0) + es_normal_n.vector_type = "NORMAL" + es_normal_n.convert_from = "WORLD" + es_normal_n.convert_to = "CAMERA" + + es_window_z_n = uv_offset_g.nodes.new("ShaderNodeVectorTransform") + es_window_z_n.name = es_window_z_n.label = "ESWindowZ" + es_window_z_n.location = (pos_x_shift * 1, -200) + es_window_z_n.vector_type = "NORMAL" + es_window_z_n.convert_from = "WORLD" + es_window_z_n.convert_to = "CAMERA" + + # pass 2 + es_window_x_n = uv_offset_g.nodes.new("ShaderNodeVectorMath") + es_window_x_n.name = es_window_x_n.label = "ESWindowX" + es_window_x_n.location = (pos_x_shift * 2, 200) + es_window_x_n.operation = "CROSS_PRODUCT" + + # pass 3 + es_window_y_n = uv_offset_g.nodes.new("ShaderNodeVectorMath") + es_window_y_n.name = es_window_y_n.label = "ESWindowY" + es_window_y_n.location = (pos_x_shift * 3, 100) + es_window_y_n.operation = "CROSS_PRODUCT" + + # pass 4 + wnd_to_eye_row1_mult_n = uv_offset_g.nodes.new("ShaderNodeVectorMath") + wnd_to_eye_row1_mult_n.name = wnd_to_eye_row1_mult_n.label = "WndToEyeRow1" + wnd_to_eye_row1_mult_n.location = (pos_x_shift * 5, 200) + wnd_to_eye_row1_mult_n.operation = "DOT_PRODUCT" + + wnd_to_eye_row2_mult_n = uv_offset_g.nodes.new("ShaderNodeVectorMath") + wnd_to_eye_row2_mult_n.name = wnd_to_eye_row2_mult_n.label = "WndToEyeRow2" + wnd_to_eye_row2_mult_n.location = (pos_x_shift * 5, 0) + wnd_to_eye_row2_mult_n.operation = "DOT_PRODUCT" + + wnd_to_eye_row3_mult_n = uv_offset_g.nodes.new("ShaderNodeVectorMath") + wnd_to_eye_row3_mult_n.name = wnd_to_eye_row3_mult_n.label = "WndToEyeRow3" + wnd_to_eye_row3_mult_n.location = (pos_x_shift * 5, -200) + wnd_to_eye_row3_mult_n.operation = "DOT_PRODUCT" + + # pass 5 + wnd_to_eye_xz_n = uv_offset_g.nodes.new("ShaderNodeCombineXYZ") + wnd_to_eye_xz_n.name = wnd_to_eye_xz_n.label = "WndToEyeXZ" + wnd_to_eye_xz_n.location = (pos_x_shift * 6, 200) + wnd_to_eye_xz_n.inputs[1].default_value = 0.0 + + wnd_to_eye_n = uv_offset_g.nodes.new("ShaderNodeCombineXYZ") + wnd_to_eye_n.name = wnd_to_eye_n.label = "WndToEye" + wnd_to_eye_n.location = (pos_x_shift * 6, 0) + + wnd_to_eye_yz_n = uv_offset_g.nodes.new("ShaderNodeCombineXYZ") + wnd_to_eye_yz_n.name = wnd_to_eye_yz_n.label = "WndToEyeYZ" + wnd_to_eye_yz_n.location = (pos_x_shift * 6, -200) + wnd_to_eye_yz_n.inputs[0].default_value = 0.0 + + # pass 6 + wnd_to_eye_xz_normalize_n = uv_offset_g.nodes.new("ShaderNodeVectorMath") + wnd_to_eye_xz_normalize_n.name = wnd_to_eye_xz_normalize_n.label = "WndToEyeXZNorm" + wnd_to_eye_xz_normalize_n.location = (pos_x_shift * 7, 200) + wnd_to_eye_xz_normalize_n.operation = "NORMALIZE" + + wnd_to_eye_yz_normalize_n = uv_offset_g.nodes.new("ShaderNodeVectorMath") + wnd_to_eye_yz_normalize_n.name = wnd_to_eye_yz_normalize_n.label = "WndToEyeYZNorm" + wnd_to_eye_yz_normalize_n.location = (pos_x_shift * 7, -200) + wnd_to_eye_yz_normalize_n.operation = "NORMALIZE" + + # pass 7 + wnd_to_eye_xz_sep_n = uv_offset_g.nodes.new("ShaderNodeSeparateXYZ") + wnd_to_eye_xz_sep_n.name = wnd_to_eye_xz_sep_n.label = "WndToEyeXZSeparate" + wnd_to_eye_xz_sep_n.location = (pos_x_shift * 8, 200) + + wnd_to_eye_sep_n = uv_offset_g.nodes.new("ShaderNodeSeparateXYZ") + wnd_to_eye_sep_n.name = wnd_to_eye_sep_n.label = "WndToEyeSeparate" + wnd_to_eye_sep_n.location = (pos_x_shift * 8, 0) + + wnd_to_eye_yz_sep_n = uv_offset_g.nodes.new("ShaderNodeSeparateXYZ") + wnd_to_eye_yz_sep_n.name = wnd_to_eye_yz_sep_n.label = "WndToEyeYZSeparate" + wnd_to_eye_yz_sep_n.location = (pos_x_shift * 8, -200) + + # pass 8 + u_factor_n = uv_offset_g.nodes.new("ShaderNodeGroup") + u_factor_n.name = u_factor_n.label = "UFactor" + u_factor_n.location = (pos_x_shift * 9, 200) + u_factor_n.node_tree = window_offset_factor_ng.get_node_group() + + uv_sep_n = uv_offset_g.nodes.new("ShaderNodeGroup") + uv_sep_n.name = uv_sep_n.label = "UVSeparate" + uv_sep_n.location = (pos_x_shift * 9, 0) + uv_sep_n.node_tree = scs_uvs_separate_ng.get_node_group() + + v_factor_n = uv_offset_g.nodes.new("ShaderNodeGroup") + v_factor_n.name = v_factor_n.label = "VFactor" + v_factor_n.location = (pos_x_shift * 9, -200) + v_factor_n.node_tree = window_offset_factor_ng.get_node_group() + + # pass 9 + u_final_n = uv_offset_g.nodes.new("ShaderNodeGroup") + u_final_n.name = u_final_n.label = "UFinal" + u_final_n.location = (pos_x_shift * 10, 100) + u_final_n.node_tree = window_final_uv_ng.get_node_group() + + v_final_n = uv_offset_g.nodes.new("ShaderNodeGroup") + v_final_n.name = v_final_n.label = "VFinal" + v_final_n.location = (pos_x_shift * 10, -100) + v_final_n.node_tree = window_final_uv_ng.get_node_group() + + # pass 10 + uv_combine_n = uv_offset_g.nodes.new("ShaderNodeGroup") + uv_combine_n.name = uv_combine_n.label = "UVFinal" + uv_combine_n.location = (pos_x_shift * 11, 0) + uv_combine_n.node_tree = scs_uvs_combine_ng.get_node_group() + + # group links + # pass 1 + uv_offset_g.links.new(es_normal_n.inputs[0], input_n.outputs['Normal']) + + uv_offset_g.links.new(es_window_z_n.inputs[0], input_n.outputs['Incoming']) + + # pass 2 + uv_offset_g.links.new(es_window_x_n.inputs[0], es_world_up_n.outputs[0]) + uv_offset_g.links.new(es_window_x_n.inputs[1], es_normal_n.outputs[0]) + + # pass 3 + uv_offset_g.links.new(es_window_y_n.inputs[0], es_window_x_n.outputs[0]) + uv_offset_g.links.new(es_window_y_n.inputs[1], es_normal_n.outputs[0]) + + # pass 4 + uv_offset_g.links.new(wnd_to_eye_row1_mult_n.inputs[0], es_window_x_n.outputs[0]) + uv_offset_g.links.new(wnd_to_eye_row1_mult_n.inputs[1], es_window_z_n.outputs[0]) + + uv_offset_g.links.new(wnd_to_eye_row2_mult_n.inputs[0], es_window_y_n.outputs[0]) + uv_offset_g.links.new(wnd_to_eye_row2_mult_n.inputs[1], es_window_z_n.outputs[0]) + + uv_offset_g.links.new(wnd_to_eye_row3_mult_n.inputs[0], es_normal_n.outputs[0]) + uv_offset_g.links.new(wnd_to_eye_row3_mult_n.inputs[1], es_window_z_n.outputs[0]) + + # pass 5 + uv_offset_g.links.new(wnd_to_eye_xz_n.inputs['X'], wnd_to_eye_row1_mult_n.outputs['Value']) + uv_offset_g.links.new(wnd_to_eye_xz_n.inputs['Z'], wnd_to_eye_row3_mult_n.outputs['Value']) + + uv_offset_g.links.new(wnd_to_eye_n.inputs['X'], wnd_to_eye_row1_mult_n.outputs['Value']) + uv_offset_g.links.new(wnd_to_eye_n.inputs['Y'], wnd_to_eye_row2_mult_n.outputs['Value']) + uv_offset_g.links.new(wnd_to_eye_n.inputs['Z'], wnd_to_eye_row3_mult_n.outputs['Value']) + + uv_offset_g.links.new(wnd_to_eye_yz_n.inputs['Y'], wnd_to_eye_row2_mult_n.outputs['Value']) + uv_offset_g.links.new(wnd_to_eye_yz_n.inputs['Z'], wnd_to_eye_row3_mult_n.outputs['Value']) + + # pass 6 + uv_offset_g.links.new(wnd_to_eye_xz_normalize_n.inputs[0], wnd_to_eye_xz_n.outputs[0]) + + uv_offset_g.links.new(wnd_to_eye_yz_normalize_n.inputs[0], wnd_to_eye_yz_n.outputs[0]) + + # pass 7 + uv_offset_g.links.new(wnd_to_eye_xz_sep_n.inputs[0], wnd_to_eye_xz_normalize_n.outputs[0]) + + uv_offset_g.links.new(wnd_to_eye_sep_n.inputs[0], wnd_to_eye_n.outputs[0]) + + uv_offset_g.links.new(wnd_to_eye_yz_sep_n.inputs[0], wnd_to_eye_yz_normalize_n.outputs[0]) + + # pass 8 + uv_offset_g.links.new(u_factor_n.inputs['WndToEye Up'], wnd_to_eye_xz_sep_n.outputs['Z']) + uv_offset_g.links.new(u_factor_n.inputs['WndToEye Direction'], wnd_to_eye_sep_n.outputs['X']) + + uv_offset_g.links.new(uv_sep_n.inputs[0], input_n.outputs['UV']) + + uv_offset_g.links.new(v_factor_n.inputs['WndToEye Up'], wnd_to_eye_yz_sep_n.outputs['Z']) + uv_offset_g.links.new(v_factor_n.inputs['WndToEye Direction'], wnd_to_eye_sep_n.outputs['Y']) + + # pass 10 + uv_offset_g.links.new(u_final_n.inputs['UV'], uv_sep_n.outputs['U']) + uv_offset_g.links.new(u_final_n.inputs['Factor'], u_factor_n.outputs['Offset Factor']) + + uv_offset_g.links.new(v_final_n.inputs['UV'], uv_sep_n.outputs['V']) + uv_offset_g.links.new(v_final_n.inputs['Factor'], v_factor_n.outputs['Offset Factor']) + + # pass 11 + uv_offset_g.links.new(uv_combine_n.inputs['U'], u_final_n.outputs['Final UV']) + uv_offset_g.links.new(uv_combine_n.inputs['V'], v_final_n.outputs['Final UV']) + + # output + uv_offset_g.links.new(output_n.inputs['UV Final'], uv_combine_n.outputs['Vector']) diff --git a/addon/io_scs_tools/internals/shaders/flavors/alpha_test.py b/addon/io_scs_tools/internals/shaders/flavors/alpha_test.py index 59aa646..a940e5f 100644 --- a/addon/io_scs_tools/internals/shaders/flavors/alpha_test.py +++ b/addon/io_scs_tools/internals/shaders/flavors/alpha_test.py @@ -16,51 +16,24 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software +from io_scs_tools.internals.shaders.std_node_groups import alpha_test_ng FLAVOR_ID = "alpha_test" -ALPHA_TEST_NODE = "AlphaTestCalc" +_ALPHA_TEST_PASS = "AlphaTestPass" -def __create_node__(node_tree): - """Create node for alpha test. - - :param node_tree: node tree on which alpha test will be used - :type node_tree: bpy.types.NodeTree - """ - alpha_test_n = node_tree.nodes.new("ShaderNodeMath") - alpha_test_n.name = ALPHA_TEST_NODE - alpha_test_n.label = ALPHA_TEST_NODE - alpha_test_n.operation = "GREATER_THAN" - alpha_test_n.inputs[1].default_value = 0.077 # actually it should be 0.05, however by visual comparison it turns out this value suits more - - -def init(node_tree, location, alpha_from, alpha_to): +def init(node_tree): """Initialize alpha test. :param node_tree: node tree on which alpha test will be used :type node_tree: bpy.types.NodeTree - :param location: x position in node tree - :type location (int, int) - :param alpha_from: node socket from which alpha test should be applierd - :type alpha_from: bpy.types.NodeSocket - :param alpha_to: node socket to which result of alpha test should be send - :type alpha_to: bpy.types.NodeSocket """ - if ALPHA_TEST_NODE not in node_tree.nodes: - __create_node__(node_tree) - - node_tree.nodes[ALPHA_TEST_NODE].location = location - - # links creation - nodes = node_tree.nodes - - node_tree.links.new(nodes[ALPHA_TEST_NODE].inputs[0], alpha_from) - node_tree.links.new(alpha_to, nodes[ALPHA_TEST_NODE].outputs[0]) - - node_tree[FLAVOR_ID] = True + # FIXME: move to old system after: https://developer.blender.org/T68406 is resolved + flavor_frame = node_tree.nodes.new(type="NodeFrame") + flavor_frame.name = flavor_frame.label = FLAVOR_ID def delete(node_tree): @@ -70,22 +43,11 @@ def delete(node_tree): :type node_tree: bpy.types.NodeTree """ - if ALPHA_TEST_NODE in node_tree.nodes: - node_tree.nodes.remove(node_tree.nodes[ALPHA_TEST_NODE]) - - if FLAVOR_ID in node_tree: - del node_tree[FLAVOR_ID] - + if _ALPHA_TEST_PASS in node_tree.nodes: + node_tree.nodes.remove(node_tree.nodes[_ALPHA_TEST_PASS]) -def get_node(node_tree): - """Gets alpha test flavor node. - - :param node_tree: node tree from which alpha test flavor node should be returned - :type node_tree: bpy.types.NodeTree - :return: node if it's set; otherwise None - :rtype: bpy.types.NodeTree | None - """ - return node_tree.nodes[ALPHA_TEST_NODE] if ALPHA_TEST_NODE in node_tree.nodes else None + if FLAVOR_ID in node_tree.nodes: + node_tree.nodes.remove(node_tree.nodes[FLAVOR_ID]) def is_set(node_tree): @@ -96,4 +58,37 @@ def is_set(node_tree): :return: True if flavor exists; False otherwise :rtype: bool """ - return FLAVOR_ID in node_tree and node_tree[FLAVOR_ID] + return FLAVOR_ID in node_tree.nodes + + +def add_pass(node_tree, shader_from, alpha_from, shader_to): + """Adds alpha test pass to simulate alpha cliping in combination with blending modes. + + :param node_tree: node tree to which pass should be added + :type node_tree: bpy.types.NodeTree + :param shader_from: where shader output should be taken from + :type shader_from: bpy.types.NodeSocket + :param alpha_from: where alpha output should be taken from + :type alpha_from: bpy.types.NodeSocket + :param shader_to: where should alpha tested pass output should go to + :type shader_to: bpy.types.NodeSocket + """ + node_from = shader_from.node + + # we will insert alpha pass, thus move right nodes for one slot to the right + for node in node_tree.nodes: + if node.location.x > node_from.location.x: + node.location.x += 185 + + # create alpha pass node + alpha_test_pass_n = node_tree.nodes.new("ShaderNodeGroup") + alpha_test_pass_n.name = alpha_test_pass_n.label = _ALPHA_TEST_PASS + alpha_test_pass_n.location = (node_from.location.x + 185, node_from.location.y) + alpha_test_pass_n.node_tree = alpha_test_ng.get_node_group() + alpha_test_pass_n.inputs['Alpha'].default_value = 1.0 + + # link it to the the node tree + node_tree.links.new(alpha_test_pass_n.inputs['Shader'], shader_from) + node_tree.links.new(alpha_test_pass_n.inputs['Alpha'], alpha_from) + + node_tree.links.new(shader_to, alpha_test_pass_n.outputs['Shader']) diff --git a/addon/io_scs_tools/internals/shaders/flavors/asafew.py b/addon/io_scs_tools/internals/shaders/flavors/asafew.py index 2b0c64f..7bfc866 100644 --- a/addon/io_scs_tools/internals/shaders/flavors/asafew.py +++ b/addon/io_scs_tools/internals/shaders/flavors/asafew.py @@ -36,7 +36,9 @@ def init(node_tree, alpha_remap_gn): alpha_remap_gn.inputs["Factor1"].default_value = 1.0 / 0.875 alpha_remap_gn.inputs["Factor2"].default_value = -0.125 * 1.0 / 0.875 - node_tree[FLAVOR_ID] = True + # FIXME: move to old system after: https://developer.blender.org/T68406 is resolved + flavor_frame = node_tree.nodes.new(type="NodeFrame") + flavor_frame.name = flavor_frame.label = FLAVOR_ID def delete(node_tree, alpha_remap_gn): @@ -48,11 +50,11 @@ def delete(node_tree, alpha_remap_gn): :type alpha_remap_gn: bpy.types.Node """ - if FLAVOR_ID in node_tree: + if FLAVOR_ID in node_tree.nodes: alpha_remap_gn.inputs["Factor1"].default_value = 1.0 alpha_remap_gn.inputs["Factor2"].default_value = 0.0 - del node_tree[FLAVOR_ID] + node_tree.nodes.remove(node_tree.nodes[FLAVOR_ID]) def is_set(node_tree): @@ -63,4 +65,4 @@ def is_set(node_tree): :return: True if flavor exists; False otherwise :rtype: bool """ - return FLAVOR_ID in node_tree and node_tree[FLAVOR_ID] + return FLAVOR_ID in node_tree.nodes diff --git a/addon/io_scs_tools/internals/shaders/flavors/awhite.py b/addon/io_scs_tools/internals/shaders/flavors/awhite.py index 42f6805..2ed9f80 100644 --- a/addon/io_scs_tools/internals/shaders/flavors/awhite.py +++ b/addon/io_scs_tools/internals/shaders/flavors/awhite.py @@ -67,7 +67,9 @@ def init(node_tree, location, mix_factor_from, color_from, color_to): node_tree.links.new(color_to, awhite_mix_n.outputs['Color']) - node_tree[FLAVOR_ID] = True + # FIXME: move to old system after: https://developer.blender.org/T68406 is resolved + flavor_frame = node_tree.nodes.new(type="NodeFrame") + flavor_frame.name = flavor_frame.label = FLAVOR_ID def delete(node_tree): @@ -77,9 +79,11 @@ def delete(node_tree): :type node_tree: bpy.types.NodeTree """ - if FLAVOR_ID in node_tree: + if _AWHITE_MIX_NODE in node_tree.nodes: node_tree.nodes.remove(node_tree.nodes[_AWHITE_MIX_NODE]) - del node_tree[FLAVOR_ID] + + if FLAVOR_ID in node_tree.nodes: + node_tree.nodes.remove(node_tree.nodes[FLAVOR_ID]) def is_set(node_tree): @@ -90,7 +94,7 @@ def is_set(node_tree): :return: True if flavor exists; False otherwise :rtype: bool """ - return FLAVOR_ID in node_tree and node_tree[FLAVOR_ID] + return FLAVOR_ID in node_tree.nodes def get_out_socket(node_tree): diff --git a/addon/io_scs_tools/internals/shaders/flavors/blend_add.py b/addon/io_scs_tools/internals/shaders/flavors/blend_add.py index 10f49d1..edab9ca 100644 --- a/addon/io_scs_tools/internals/shaders/flavors/blend_add.py +++ b/addon/io_scs_tools/internals/shaders/flavors/blend_add.py @@ -16,31 +16,41 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software +from io_scs_tools.internals.shaders.std_node_groups import blend_add_ng FLAVOR_ID = "blend_add" +_BLEND_ADD_GN = "BlendAddPass" -def init(node_tree, alpha_from, alpha_to): + +def init(node_tree, location, shader_from, shader_to): """Initialize blend add. - NOTE: this flavor is implemented the same as "blend_over" for now - because we don't have access to pixel color behind the material - using this flavor. So the closest solution is to use blend_over - as we at least get the opacity blending with alpha channel. :param node_tree: node tree on which blend over will be used :type node_tree: bpy.types.NodeTree - :param alpha_from: node socket from which blend over should be applierd - :type alpha_from: bpy.types.NodeSocket - :param alpha_to: node socket to which result of blend over should be send - :type alpha_to: bpy.types.NodeSocket + :param location: location where blend pass node should be created + :type location: tuple(int, int) + :param shader_from: node socket from which blend pass should take the shader result + :type shader_from: bpy.types.NodeSocket + :param shader_to: node socket to which result of blend pass should be sent + :type shader_to: bpy.types.NodeSocket """ - # links creation - node_tree.links.new(alpha_to, alpha_from) + # node creation + blend_add_n = node_tree.nodes.new("ShaderNodeGroup") + blend_add_n.name = blend_add_n.label = _BLEND_ADD_GN + blend_add_n.location = location + blend_add_n.node_tree = blend_add_ng.get_node_group() + + # link creation + node_tree.links.new(blend_add_n.inputs['Shader'], shader_from) + node_tree.links.new(shader_to, blend_add_n.outputs['Shader']) - node_tree[FLAVOR_ID] = True + # FIXME: move to old system after: https://developer.blender.org/T68406 is resolved + flavor_frame = node_tree.nodes.new(type="NodeFrame") + flavor_frame.name = flavor_frame.label = FLAVOR_ID def delete(node_tree): @@ -50,8 +60,11 @@ def delete(node_tree): :type node_tree: bpy.types.NodeTree """ - if FLAVOR_ID in node_tree: - del node_tree[FLAVOR_ID] + if _BLEND_ADD_GN in node_tree.nodes: + node_tree.nodes.remove(node_tree.nodes[_BLEND_ADD_GN]) + + if FLAVOR_ID in node_tree.nodes: + node_tree.nodes.remove(node_tree.nodes[FLAVOR_ID]) def is_set(node_tree): @@ -62,4 +75,4 @@ def is_set(node_tree): :return: True if flavor exists; False otherwise :rtype: bool """ - return FLAVOR_ID in node_tree and node_tree[FLAVOR_ID] + return FLAVOR_ID in node_tree.nodes diff --git a/addon/io_scs_tools/internals/shaders/flavors/blend_mult.py b/addon/io_scs_tools/internals/shaders/flavors/blend_mult.py index defd2f6..6912835 100644 --- a/addon/io_scs_tools/internals/shaders/flavors/blend_mult.py +++ b/addon/io_scs_tools/internals/shaders/flavors/blend_mult.py @@ -16,76 +16,41 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software +from io_scs_tools.internals.shaders.std_node_groups import blend_mult_ng FLAVOR_ID = "blend_mult" -_HUE_SAT_NODE = "BlendMultHueSat" -_ALPHA_POW_NODE = "BlendMultAlphaPower" +_BLEND_MULT_GN = "BlendMultPass" -def __create_nodes__(node_tree): - """Create node for alpha to white mixing. +def init(node_tree, location, shader_from, shader_to): + """Initialize blend mult. - :param node_tree: node tree on which this flavor will be used + :param node_tree: node tree on which blend over will be used :type node_tree: bpy.types.NodeTree - :return: alpha to white mixing node - :rtype: (bpy.types.Node, bpy.types.Node) + :param location: location where blend pass node should be created + :type location: tuple(int, int) + :param shader_from: node socket from which blend pass should take the shader result + :type shader_from: bpy.types.NodeSocket + :param shader_to: node socket to which result of blend pass should be sent + :type shader_to: bpy.types.NodeSocket """ - hue_sat_mix_n = node_tree.nodes.new("ShaderNodeHueSaturation") - hue_sat_mix_n.name = hue_sat_mix_n.label = _HUE_SAT_NODE - hue_sat_mix_n.inputs['Hue'].default_value = 0.5 - hue_sat_mix_n.inputs['Saturation'].default_value = 1.5 - hue_sat_mix_n.inputs['Value'].default_value = 0.025 - hue_sat_mix_n.inputs['Fac'].default_value = 1.0 + # node creation + blend_mult_n = node_tree.nodes.new("ShaderNodeGroup") + blend_mult_n.name = blend_mult_n.label = _BLEND_MULT_GN + blend_mult_n.location = location + blend_mult_n.node_tree = blend_mult_ng.get_node_group() - alpha_pow_n = node_tree.nodes.new("ShaderNodeMath") - alpha_pow_n.name = alpha_pow_n.label = _ALPHA_POW_NODE - alpha_pow_n.operation = "POWER" - alpha_pow_n.inputs[1].default_value = 1.5 + # link creation + node_tree.links.new(blend_mult_n.inputs['Shader'], shader_from) + node_tree.links.new(shader_to, blend_mult_n.outputs['Shader']) - return hue_sat_mix_n, alpha_pow_n - - -def init(node_tree, location, alpha_from, alpha_to, color_from, color_to): - """Initialize blend multiplication flavor. - - NOTE: this flavor implementation is fake aproximation to be - used in eut2.unlit.vcol.tex shader - - :param node_tree: node tree on which this flavor will be used - :type node_tree: bpy.types.NodeTree - :param location: position in node tree - :type location: (int, int) - :param alpha_from: node socket from which alpha should be taken - :type alpha_from: bpy.types.NodeSocket - :param alpha_to: node socket to which alpha should be send - :type alpha_to: bpy.types.NodeSocket - :param color_from: node socket from which color should be applierd - :type color_from: bpy.types.NodeSocket - :param color_to: node socket to which result of this flavor should be send - :type color_to: bpy.types.NodeSocket - """ - - if not is_set(node_tree): - (hue_sat_mix_n, alpha_pow_n) = __create_nodes__(node_tree) - hue_sat_mix_n.location = location - alpha_pow_n.location = location - alpha_pow_n.location.y -= 200 - else: - hue_sat_mix_n = node_tree.nodes[_HUE_SAT_NODE] - alpha_pow_n = node_tree.nodes[_ALPHA_POW_NODE] - - # links creation - node_tree.links.new(hue_sat_mix_n.inputs['Color'], color_from) - node_tree.links.new(alpha_pow_n.inputs[0], alpha_from) - - node_tree.links.new(color_to, hue_sat_mix_n.outputs['Color']) - node_tree.links.new(alpha_to, alpha_pow_n.outputs[0]) - - node_tree[FLAVOR_ID] = True + # FIXME: move to old system after: https://developer.blender.org/T68406 is resolved + flavor_frame = node_tree.nodes.new(type="NodeFrame") + flavor_frame.name = flavor_frame.label = FLAVOR_ID def delete(node_tree): @@ -95,10 +60,11 @@ def delete(node_tree): :type node_tree: bpy.types.NodeTree """ - if FLAVOR_ID in node_tree: - node_tree.nodes.remove(node_tree.nodes[_HUE_SAT_NODE]) - node_tree.nodes.remove(node_tree.nodes[_ALPHA_POW_NODE]) - del node_tree[FLAVOR_ID] + if _BLEND_MULT_GN in node_tree.nodes: + node_tree.nodes.remove(node_tree.nodes[_BLEND_MULT_GN]) + + if FLAVOR_ID in node_tree.nodes: + node_tree.nodes.remove(node_tree.nodes[FLAVOR_ID]) def is_set(node_tree): @@ -109,4 +75,4 @@ def is_set(node_tree): :return: True if flavor exists; False otherwise :rtype: bool """ - return FLAVOR_ID in node_tree and node_tree[FLAVOR_ID] + return FLAVOR_ID in node_tree.nodes diff --git a/addon/io_scs_tools/internals/shaders/flavors/blend_over.py b/addon/io_scs_tools/internals/shaders/flavors/blend_over.py index 0fd9bc1..ef6765a 100644 --- a/addon/io_scs_tools/internals/shaders/flavors/blend_over.py +++ b/addon/io_scs_tools/internals/shaders/flavors/blend_over.py @@ -16,27 +16,22 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software FLAVOR_ID = "blend_over" -def init(node_tree, alpha_from, alpha_to): +def init(node_tree): """Initialize blend over. :param node_tree: node tree on which blend over will be used :type node_tree: bpy.types.NodeTree - :param alpha_from: node socket from which blend over should be applierd - :type alpha_from: bpy.types.NodeSocket - :param alpha_to: node socket to which result of blend over should be send - :type alpha_to: bpy.types.NodeSocket """ - # links creation - node_tree.links.new(alpha_to, alpha_from) - - node_tree[FLAVOR_ID] = True + # FIXME: move to old system after: https://developer.blender.org/T68406 is resolved + flavor_frame = node_tree.nodes.new(type="NodeFrame") + flavor_frame.name = flavor_frame.label = FLAVOR_ID def delete(node_tree): @@ -46,8 +41,8 @@ def delete(node_tree): :type node_tree: bpy.types.NodeTree """ - if FLAVOR_ID in node_tree: - del node_tree[FLAVOR_ID] + if FLAVOR_ID in node_tree.nodes: + node_tree.nodes.remove(node_tree.nodes[FLAVOR_ID]) def is_set(node_tree): @@ -58,4 +53,4 @@ def is_set(node_tree): :return: True if flavor exists; False otherwise :rtype: bool """ - return FLAVOR_ID in node_tree and node_tree[FLAVOR_ID] + return FLAVOR_ID in node_tree.nodes diff --git a/addon/io_scs_tools/internals/shaders/flavors/nmap/__init__.py b/addon/io_scs_tools/internals/shaders/flavors/nmap/__init__.py index 3c61204..3d5abfb 100644 --- a/addon/io_scs_tools/internals/shaders/flavors/nmap/__init__.py +++ b/addon/io_scs_tools/internals/shaders/flavors/nmap/__init__.py @@ -16,22 +16,23 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software import bpy from io_scs_tools.consts import Mesh as _MESH_consts from io_scs_tools.internals.shaders.flavors.nmap import scale_ng from io_scs_tools.internals.shaders.flavors.nmap import dds16_ng +from io_scs_tools.utils import material as _material_utils NMAP_FLAVOR_FRAME_NODE = "TSNMap Flavor" -NMAP_NODE = "NormalMapMat" -NMAP_GEOM_NODE = "NMapGeom" +NMAP_UVMAP_NODE = "NormalMapUVs" +NMAP_NODE = "NormalMap" NMAP_TEX_NODE = "NMapTex" NMAP_DDS16_GNODE = "NMapDDS16Group" NMAP_SCALE_GNODE = "NMapScaleGroup" -def __create_nodes__(node_tree, location=None, normal_to=None): +def __create_nodes__(node_tree, location=None, normal_to=None, normal_from=None): """Create node for normal maps. :param node_tree: node tree on which normal map will be used @@ -43,24 +44,29 @@ def __create_nodes__(node_tree, location=None, normal_to=None): if NMAP_NODE in node_tree.nodes: location = node_tree.nodes[NMAP_NODE].location - # try to recover normal to link node socket - if not normal_to: - if NMAP_NODE in node_tree.nodes: - for link in node_tree.links: - if link.from_node == node_tree.nodes[NMAP_NODE] and link.from_socket.name == "Normal": - normal_to = link.to_socket + # try to recover normals to link node socket + if not normal_to and NMAP_NODE in node_tree.nodes: + for link in node_tree.links: + if link.from_node == node_tree.nodes[NMAP_NODE] and link.from_socket.name == "Normal": + normal_to = link.to_socket + + if not normal_from and NMAP_SCALE_GNODE in node_tree.nodes: + for link in node_tree.links: + if link.to_node == node_tree.nodes[NMAP_SCALE_GNODE] and link.to_socket.name == "Original Normal": + normal_from = link.from_socket frame = node_tree.nodes.new("NodeFrame") frame.name = frame.label = NMAP_FLAVOR_FRAME_NODE - nmap_geom_n = node_tree.nodes.new("ShaderNodeGeometry") - nmap_geom_n.parent = frame - nmap_geom_n.name = nmap_geom_n.label = NMAP_GEOM_NODE - nmap_geom_n.uv_layer = _MESH_consts.none_uv + nmap_uvs_n = node_tree.nodes.new("ShaderNodeUVMap") + nmap_uvs_n.parent = frame + nmap_uvs_n.name = nmap_uvs_n.label = NMAP_UVMAP_NODE + nmap_uvs_n.uv_map = _MESH_consts.none_uv - nmap_tex_n = node_tree.nodes.new("ShaderNodeTexture") + nmap_tex_n = node_tree.nodes.new("ShaderNodeTexImage") nmap_tex_n.parent = frame nmap_tex_n.name = nmap_tex_n.label = NMAP_TEX_NODE + nmap_tex_n.width = 140 nmap_n = node_tree.nodes.new("ShaderNodeNormalMap") nmap_n.parent = frame @@ -68,27 +74,28 @@ def __create_nodes__(node_tree, location=None, normal_to=None): nmap_n.space = "TANGENT" nmap_n.inputs["Strength"].default_value = 1 - nmap_scale_gn = node_tree.nodes.new("ShaderNodeGroup") - nmap_scale_gn.parent = frame - nmap_scale_gn.name = nmap_scale_gn.label = NMAP_SCALE_GNODE - nmap_scale_gn.node_tree = scale_ng.get_node_group() + nmap_scale_n = node_tree.nodes.new("ShaderNodeGroup") + nmap_scale_n.parent = frame + nmap_scale_n.name = nmap_scale_n.label = NMAP_SCALE_GNODE + nmap_scale_n.node_tree = scale_ng.get_node_group() # position nodes if location: - nmap_geom_n.location = (location[0] - 185 * 3, location[1]) + nmap_uvs_n.location = (location[0] - 185 * 3, location[1]) nmap_tex_n.location = (location[0] - 185 * 2, location[1]) nmap_n.location = (location[0] - 185, location[1] - 200) - nmap_scale_gn.location = (location[0], location[1]) + nmap_scale_n.location = (location[0], location[1]) # links creation nodes = node_tree.nodes - node_tree.links.new(nodes[NMAP_TEX_NODE].inputs["Vector"], nodes[NMAP_GEOM_NODE].outputs["UV"]) + node_tree.links.new(nodes[NMAP_UVMAP_NODE].outputs["UV"], nodes[NMAP_TEX_NODE].inputs["Vector"]) node_tree.links.new(nodes[NMAP_NODE].inputs["Color"], nodes[NMAP_TEX_NODE].outputs["Color"]) node_tree.links.new(nodes[NMAP_SCALE_GNODE].inputs["NMap Tex Color"], nodes[NMAP_TEX_NODE].outputs["Color"]) - node_tree.links.new(nodes[NMAP_SCALE_GNODE].inputs["Original Normal"], nodes[NMAP_GEOM_NODE].outputs["Normal"]) + if normal_from: + node_tree.links.new(nodes[NMAP_SCALE_GNODE].inputs["Original Normal"], normal_from) node_tree.links.new(nodes[NMAP_SCALE_GNODE].inputs["Modified Normal"], nodes[NMAP_NODE].outputs["Normal"]) # set normal only if we know where to @@ -96,32 +103,32 @@ def __create_nodes__(node_tree, location=None, normal_to=None): node_tree.links.new(normal_to, nodes[NMAP_SCALE_GNODE].outputs["Normal"]) -def __check_and_create_dds16_node__(node_tree, texture): +def __check_and_create_dds16_node__(node_tree, image): """Checks if given texture is composed '16-bit DDS' texture and properly create extra node for it's representation. On the contrary if texture is not 16-bit DDS and node exists clean that node and restore old connections. :param node_tree: node tree on which normal map will be used :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assignet to nmap texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assigned to nmap texture node + :type image: bpy.types.Image """ # in case of DDS simulating 16-bit normal maps create it's group and properly connect it, # on the other hand if group exists but shouldn't delete group and restore old connections - is_dds16 = texture and texture.image and texture.image.filepath.endswith(".dds") and texture.image.pixels[2] == 0.0 + is_dds16 = image and image.filepath.endswith(".dds") and image.pixels[2] == 0.0 if is_dds16 and NMAP_DDS16_GNODE not in node_tree.nodes: - nmap_dds16_gn = node_tree.nodes.new("ShaderNodeGroup") - nmap_dds16_gn.parent = node_tree.nodes[NMAP_FLAVOR_FRAME_NODE] - nmap_dds16_gn.name = nmap_dds16_gn.label = NMAP_DDS16_GNODE - nmap_dds16_gn.node_tree = dds16_ng.get_node_group() + nmap_dds16_n = node_tree.nodes.new("ShaderNodeGroup") + nmap_dds16_n.parent = node_tree.nodes[NMAP_FLAVOR_FRAME_NODE] + nmap_dds16_n.name = nmap_dds16_n.label = NMAP_DDS16_GNODE + nmap_dds16_n.node_tree = dds16_ng.get_node_group() location = node_tree.nodes[NMAP_NODE].location node_tree.nodes[NMAP_TEX_NODE].location[0] -= 185 - node_tree.nodes[NMAP_GEOM_NODE].location[0] -= 185 - nmap_dds16_gn.location = (location[0] - 185, location[1]) + node_tree.nodes[NMAP_UVMAP_NODE].location[0] -= 185 + nmap_dds16_n.location = (location[0] - 185, location[1]) node_tree.links.new(node_tree.nodes[NMAP_DDS16_GNODE].inputs["Color"], node_tree.nodes[NMAP_TEX_NODE].outputs["Color"]) @@ -133,33 +140,35 @@ def __check_and_create_dds16_node__(node_tree, texture): node_tree.nodes.remove(node_tree.nodes[NMAP_DDS16_GNODE]) node_tree.nodes[NMAP_TEX_NODE].location[0] += 185 - node_tree.nodes[NMAP_GEOM_NODE].location[0] += 185 + node_tree.nodes[NMAP_UVMAP_NODE].location[0] += 185 node_tree.links.new(node_tree.nodes[NMAP_NODE].inputs["Color"], node_tree.nodes[NMAP_TEX_NODE].outputs["Color"]) -def init(node_tree, location, normal_to): +def init(node_tree, location, normal_to, normal_from): """Initialize normal map nodes. :param node_tree: node tree on which normal map will be used :type node_tree: bpy.types.NodeTree :param location: x position in node tree :type location (int, int) - :param normal_to: node socket to which result of normal map material should be send + :param normal_to: node socket to which result of final commbined normal should be send :type normal_to: bpy.types.NodeSocket + :param normal_from: node socket from which original mesh normal should be taken + :type normal_from: bpy.types.NodeSocket """ if NMAP_FLAVOR_FRAME_NODE not in node_tree.nodes: - __create_nodes__(node_tree, location, normal_to) + __create_nodes__(node_tree, location, normal_to=normal_to, normal_from=normal_from) -def set_texture(node_tree, texture): +def set_texture(node_tree, image): """Set texture to normal map flavor. :param node_tree: node tree on which normal map is used :type node_tree: bpy.types.NodeTree - :param texture: texture which should be assignet to nmap texture node - :type texture: bpy.types.Texture + :param image: texture image which should be assignet to nmap texture node + :type image: bpy.types.Image """ # save currently active node to properly reset it on the end @@ -167,7 +176,7 @@ def set_texture(node_tree, texture): old_active = node_tree.nodes.active # ignore empty texture - if texture is None: + if image is None: delete(node_tree, True) return @@ -176,14 +185,25 @@ def set_texture(node_tree, texture): __create_nodes__(node_tree) # in case of DDS simulating 16-bit normal maps create it's group and properly connect it - __check_and_create_dds16_node__(node_tree, texture) + __check_and_create_dds16_node__(node_tree, image) # assign texture to texture node first - node_tree.nodes[NMAP_TEX_NODE].texture = texture + node_tree.nodes[NMAP_TEX_NODE].image = image node_tree.nodes.active = old_active +def set_texture_settings(node_tree, settings): + """Set texture settings to normal map flavor. + + :param node_tree: node tree of current shader + :type node_tree: bpy.types.NodeTree + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + _material_utils.set_texture_settings_to_node(node_tree.nodes[NMAP_TEX_NODE], settings) + + def set_uv(node_tree, uv_layer): """Set UV layer to texture in normal map flavor. @@ -201,7 +221,7 @@ def set_uv(node_tree, uv_layer): __create_nodes__(node_tree) # set uv layer to texture node and normal map node - node_tree.nodes[NMAP_GEOM_NODE].uv_layer = uv_layer + node_tree.nodes[NMAP_UVMAP_NODE].uv_map = uv_layer node_tree.nodes[NMAP_NODE].uv_map = uv_layer @@ -215,7 +235,6 @@ def delete(node_tree, preserve_node=False): """ if NMAP_NODE in node_tree.nodes and not preserve_node: - node_tree.nodes.remove(node_tree.nodes[NMAP_GEOM_NODE]) node_tree.nodes.remove(node_tree.nodes[NMAP_TEX_NODE]) node_tree.nodes.remove(node_tree.nodes[NMAP_NODE]) if NMAP_DDS16_GNODE in node_tree.nodes: diff --git a/addon/io_scs_tools/internals/shaders/flavors/paint.py b/addon/io_scs_tools/internals/shaders/flavors/paint.py index 6979023..dd97913 100644 --- a/addon/io_scs_tools/internals/shaders/flavors/paint.py +++ b/addon/io_scs_tools/internals/shaders/flavors/paint.py @@ -31,13 +31,12 @@ def __create_node__(node_tree): :param node_tree: node tree on which paint flavor will be used :type node_tree: bpy.types.NodeTree """ - paint_mult_n = node_tree.nodes.new("ShaderNodeMixRGB") + paint_mult_n = node_tree.nodes.new("ShaderNodeVectorMath") paint_mult_n.name = paint_mult_n.label = PAINT_MULT_NODE - paint_mult_n.blend_type = "MULTIPLY" - paint_mult_n.inputs['Fac'].default_value = 1 + paint_mult_n.operation = "MULTIPLY" color = _convert_utils.to_node_color(_get_scs_globals().base_paint_color) - paint_mult_n.inputs['Color2'].default_value = color + paint_mult_n.inputs[1].default_value = color[:3] def init(node_tree, location, diffuse_from, paint_to): @@ -61,10 +60,12 @@ def init(node_tree, location, diffuse_from, paint_to): # links creation nodes = node_tree.nodes - node_tree.links.new(nodes[PAINT_MULT_NODE].inputs["Color1"], diffuse_from) + node_tree.links.new(nodes[PAINT_MULT_NODE].inputs[0], diffuse_from) node_tree.links.new(paint_to, nodes[PAINT_MULT_NODE].outputs[0]) - node_tree[FLAVOR_ID] = True + # FIXME: move to old system after: https://developer.blender.org/T68406 is resolved + flavor_frame = node_tree.nodes.new(type="NodeFrame") + flavor_frame.name = flavor_frame.label = FLAVOR_ID def delete(node_tree): @@ -93,8 +94,8 @@ def delete(node_tree): if out_socket and in_socket: node_tree.links.new(out_socket, in_socket) - if FLAVOR_ID in node_tree: - del node_tree[FLAVOR_ID] + if FLAVOR_ID in node_tree.nodes: + node_tree.nodes.remove(node_tree.nodes[FLAVOR_ID]) def is_set(node_tree): @@ -105,7 +106,7 @@ def is_set(node_tree): :return: True if flavor exists; False otherwise :rtype: bool """ - return FLAVOR_ID in node_tree and node_tree[FLAVOR_ID] + return FLAVOR_ID in node_tree.nodes def set_color(node_tree, color): @@ -121,4 +122,4 @@ def set_color(node_tree, color): return color = _convert_utils.to_node_color(color) - node_tree.nodes[PAINT_MULT_NODE].inputs["Color2"].default_value = color + node_tree.nodes[PAINT_MULT_NODE].inputs[1].default_value = color diff --git a/addon/io_scs_tools/internals/shaders/flavors/tg0.py b/addon/io_scs_tools/internals/shaders/flavors/tg0.py index 9eba9ca..c0c2f83 100644 --- a/addon/io_scs_tools/internals/shaders/flavors/tg0.py +++ b/addon/io_scs_tools/internals/shaders/flavors/tg0.py @@ -32,9 +32,8 @@ def __create_node__(node_tree): vector_mapping_n = node_tree.nodes.new("ShaderNodeMapping") vector_mapping_n.name = vector_mapping_n.label = TG0_NODE vector_mapping_n.vector_type = "POINT" - vector_mapping_n.translation = vector_mapping_n.rotation = (0.0,) * 3 - vector_mapping_n.scale = (1.0,) * 3 - vector_mapping_n.use_min = vector_mapping_n.use_max = False + vector_mapping_n.inputs['Location'].default_value = vector_mapping_n.inputs['Rotation'].default_value = (0.0,) * 3 + vector_mapping_n.inputs['Scale'].default_value = (1.0,) * 3 def init(node_tree, location, geom_output, texture_input): @@ -61,7 +60,9 @@ def init(node_tree, location, geom_output, texture_input): node_tree.links.new(nodes[TG0_NODE].inputs[0], geom_output) node_tree.links.new(texture_input, nodes[TG0_NODE].outputs[0]) - node_tree[FLAVOR_ID] = True + # FIXME: move to old system after: https://developer.blender.org/T68406 is resolved + flavor_frame = node_tree.nodes.new(type="NodeFrame") + flavor_frame.name = flavor_frame.label = FLAVOR_ID def delete(node_tree): @@ -74,8 +75,8 @@ def delete(node_tree): if TG0_NODE in node_tree.nodes: node_tree.nodes.remove(node_tree.nodes[TG0_NODE]) - if FLAVOR_ID in node_tree: - del node_tree[FLAVOR_ID] + if FLAVOR_ID in node_tree.nodes: + node_tree.nodes.remove(node_tree.nodes[FLAVOR_ID]) def get_node(node_tree): @@ -97,7 +98,7 @@ def is_set(node_tree): :return: True if flavor exists; False otherwise :rtype: bool """ - return FLAVOR_ID in node_tree and node_tree[FLAVOR_ID] + return FLAVOR_ID in node_tree.nodes def set_scale(node_tree, scale_x, scale_y): @@ -114,5 +115,5 @@ def set_scale(node_tree, scale_x, scale_y): if vector_mapping_n: - vector_mapping_n.scale[0] = 1 / scale_x - vector_mapping_n.scale[1] = 1 / scale_y + vector_mapping_n.inputs['Scale'].default_value[0] = 1 / scale_x + vector_mapping_n.inputs['Scale'].default_value[1] = 1 / scale_y diff --git a/addon/io_scs_tools/internals/shaders/flavors/tg1.py b/addon/io_scs_tools/internals/shaders/flavors/tg1.py index 1efba52..2adae8d 100644 --- a/addon/io_scs_tools/internals/shaders/flavors/tg1.py +++ b/addon/io_scs_tools/internals/shaders/flavors/tg1.py @@ -32,9 +32,8 @@ def __create_node__(node_tree): vector_mapping_n = node_tree.nodes.new("ShaderNodeMapping") vector_mapping_n.name = vector_mapping_n.label = TG1_NODE vector_mapping_n.vector_type = "POINT" - vector_mapping_n.translation = vector_mapping_n.rotation = (0.0,) * 3 - vector_mapping_n.scale = (1.0,) * 3 - vector_mapping_n.use_min = vector_mapping_n.use_max = False + vector_mapping_n.inputs['Location'].default_value = vector_mapping_n.inputs['Rotation'].default_value = (0.0,) * 3 + vector_mapping_n.inputs['Scale'].default_value = (1.0,) * 3 def init(node_tree, location, geom_output, texture_input): @@ -61,7 +60,9 @@ def init(node_tree, location, geom_output, texture_input): node_tree.links.new(nodes[TG1_NODE].inputs[0], geom_output) node_tree.links.new(texture_input, nodes[TG1_NODE].outputs[0]) - node_tree[FLAVOR_ID] = True + # FIXME: move to old system after: https://developer.blender.org/T68406 is resolved + flavor_frame = node_tree.nodes.new(type="NodeFrame") + flavor_frame.name = flavor_frame.label = FLAVOR_ID def delete(node_tree): @@ -74,8 +75,8 @@ def delete(node_tree): if TG1_NODE in node_tree.nodes: node_tree.nodes.remove(node_tree.nodes[TG1_NODE]) - if FLAVOR_ID in node_tree: - del node_tree[FLAVOR_ID] + if FLAVOR_ID in node_tree.nodes: + node_tree.nodes.remove(node_tree.nodes[FLAVOR_ID]) def get_node(node_tree): @@ -97,7 +98,7 @@ def is_set(node_tree): :return: True if flavor exists; False otherwise :rtype: bool """ - return FLAVOR_ID in node_tree and node_tree[FLAVOR_ID] + return FLAVOR_ID in node_tree.nodes def set_scale(node_tree, scale_x, scale_y): @@ -114,5 +115,5 @@ def set_scale(node_tree, scale_x, scale_y): if vector_mapping_n: - vector_mapping_n.scale[0] = 1 / scale_x - vector_mapping_n.scale[1] = 1 / scale_y + vector_mapping_n.inputs['Scale'].default_value[0] = 1 / scale_x + vector_mapping_n.inputs['Scale'].default_value[1] = 1 / scale_y diff --git a/addon/io_scs_tools/internals/shaders/shader.py b/addon/io_scs_tools/internals/shaders/shader.py index 0ef35bc..91ca321 100644 --- a/addon/io_scs_tools/internals/shaders/shader.py +++ b/addon/io_scs_tools/internals/shaders/shader.py @@ -16,13 +16,13 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software +# Copyright (C) 2015-2019: SCS Software import bpy from io_scs_tools.utils.printout import lprint -def setup_nodes(material, effect, attr_dict, tex_dict, recreate): +def setup_nodes(material, effect, attr_dict, tex_dict, tex_settings_dict, recreate): """Setup material nodes to correctly present given shader from game engine. :param material: blender material which should be set for proper 3D view visualization @@ -33,28 +33,22 @@ def setup_nodes(material, effect, attr_dict, tex_dict, recreate): :type attr_dict: dict :param tex_dict: shader textures which should be set on given material; entry: (texture_type: texture object) :type tex_dict: dict + :param tex_settings_dict: shader texture settings which should be set on given material; entry: (texture_type: TOBJ settings string) + :type tex_settings_dict: dict :param recreate: flag indicating if shader nodes should be recreated. Should be triggered if effect name changes. :type recreate: bool """ - # disable transparency when resetting material - if recreate: - material.use_transparency = False - # gather possible flavors from effect name flavors = {} if effect.endswith(".a") or ".a." in effect: - flavors["alpha_test"] = material.use_transparency = True - material.transparency_method = "Z_TRANSPARENCY" + flavors["alpha_test"] = True - 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" + if (effect.endswith(".over") or ".over." in effect) and effect.rfind(".over") != effect.rfind(".over.dif") and ".retroreflective" not in effect: + flavors["blend_over"] = True if (effect.endswith(".mult") or ".mult." in effect) and ".mult.dif" not in effect and ".mult2" not in effect: - - flavors["blend_mult"] = material.use_transparency = True - material.transparency_method = "Z_TRANSPARENCY" + flavors["blend_mult"] = True if effect.endswith(".tg0") or ".tg0." in effect: flavors["tg0"] = True @@ -63,8 +57,7 @@ def setup_nodes(material, effect, attr_dict, tex_dict, recreate): flavors["tg1"] = True if (effect.endswith(".add") or ".add." in effect) and effect.rfind(".add.env") != effect.rfind(".add"): - flavors["blend_add"] = material.use_transparency = True - material.transparency_method = "MASK" + flavors["blend_add"] = True if effect.endswith(".tsnmapuv") or ".tsnmapuv." in effect: flavors["nmap"] = True @@ -100,10 +93,9 @@ def setup_nodes(material, effect, attr_dict, tex_dict, recreate): flavors["paint"] = True if effect.endswith(".decal.over") and ".retroreflective" in effect: - flavors["retroreflective_decal"] = material.use_transparency = True - material.transparency_method = "Z_TRANSPARENCY" + flavors["retroreflective_decal"] = True - __setup_nodes__(material, effect, attr_dict, tex_dict, {}, flavors, recreate) + __setup_nodes__(material, effect, attr_dict, tex_dict, tex_settings_dict, {}, flavors, recreate) def set_attribute(material, attr_type, attr_value): @@ -116,20 +108,33 @@ def set_attribute(material, attr_type, attr_value): :param attr_value: value which should be set to attribute in shader :type attr_value: object """ - __setup_nodes__(material, material.scs_props.mat_effect_name, {attr_type: attr_value}, {}, {}, {}, False) + __setup_nodes__(material, material.scs_props.mat_effect_name, {attr_type: attr_value}, {}, {}, {}, {}, False) -def set_texture(material, tex_type, texture): +def set_texture(material, tex_type, image): """Set texture of given type to material. :param material: blender material :type material: bpy.types.Material :param tex_type: type of SCS texture (one of: bpy.types.Material.scs_props.get_texture_types().keys()) :type tex_type: str - :param texture: blender texture object - :type texture: bpy.types.Texture + :param image: blender texture image object + :type image: bpy.types.Image + """ + __setup_nodes__(material, material.scs_props.mat_effect_name, {}, {tex_type: image}, {}, {}, {}, False) + + +def set_texture_settings(material, tex_type, settings): + """Set texture settings of given type to material. + + :param material: blender material + :type material: bpy.types.Material + :param tex_type: type of SCS texture (one of: bpy.types.Material.scs_props.get_texture_types().keys()) + :type tex_type: str + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str """ - __setup_nodes__(material, material.scs_props.mat_effect_name, {}, {tex_type: texture}, {}, {}, False) + __setup_nodes__(material, material.scs_props.mat_effect_name, {}, {}, {tex_type: settings}, {}, {}, False) def set_uv(material, tex_type, uv_layer, tex_coord): @@ -168,10 +173,10 @@ def set_uv(material, tex_type, uv_layer, tex_coord): is_valid_input = False if is_valid_input: - __setup_nodes__(material, material.scs_props.mat_effect_name, {}, {}, {tex_type: uv_layer}, {}, False) + __setup_nodes__(material, material.scs_props.mat_effect_name, {}, {}, {}, {tex_type: uv_layer}, {}, False) -def __setup_nodes__(material, effect, attr_dict, tex_dict, uvs_dict, flavors_dict, recreate): +def __setup_nodes__(material, effect, attr_dict, tex_dict, tex_settings_dict, uvs_dict, flavors_dict, recreate): """Wrapping setup of nodes for given material in central function. It properly setup nodes for 3D view visualization in real time. @@ -183,6 +188,8 @@ def __setup_nodes__(material, effect, attr_dict, tex_dict, uvs_dict, flavors_dic :type attr_dict: dict :param tex_dict: shader textures which should be set on given material; entry: (texture_type: texture object) :type tex_dict: dict + :param tex_settings_dict: shader texture settings which should be set on given material; entry: (texture_type: TOBJ settings string) + :type tex_settings_dict: dict :param uvs_dict: shader uv layers which should be set on given material; entry: (texture_type: string of uv layer) :type uvs_dict: dict :param flavors_dict: shader flavors which should be set on given material; entry: (flavor_type: flavor_data) @@ -195,7 +202,7 @@ def __setup_nodes__(material, effect, attr_dict, tex_dict, uvs_dict, flavors_dic shader_module = __get_shader__(effect, recreate, material.name) # prepare material if it's not prepared yet - if not material.node_tree: + if not material.node_tree or not material.use_nodes: material.use_nodes = True node_tree = material.node_tree @@ -204,7 +211,6 @@ def __setup_nodes__(material, effect, attr_dict, tex_dict, uvs_dict, flavors_dic if recreate: __clean_node_tree__(node_tree) shader_module.init(node_tree) - shader_module.set_material(node_tree, material) # set flavors first so any attributes changing flavor part of shader can take effect for flavor_type in flavors_dict: @@ -222,7 +228,7 @@ def __setup_nodes__(material, effect, attr_dict, tex_dict, uvs_dict, flavors_dic else: lprint("D Unsupported set_attribute with type %r called on shader %r", (attr_type, shader_module.get_name())) - # set textures, uv layers, flavors and vertex color + # set textures, texture settings, uv layers and flavors for tex_type in tex_dict: shader_set_texture = getattr(shader_module, "set_" + tex_type + "_texture", None) if shader_set_texture: @@ -230,6 +236,13 @@ def __setup_nodes__(material, effect, attr_dict, tex_dict, uvs_dict, flavors_dic else: lprint("D Unsupported set_texture with type %r called on shader %r", (tex_type, shader_module.get_name())) + for tex_type in tex_settings_dict: + shader_set_texture_settings = getattr(shader_module, "set_" + tex_type + "_texture_settings", None) + if shader_set_texture_settings: + shader_set_texture_settings(node_tree, tex_settings_dict[tex_type]) + else: + lprint("D Unsupported set_texture_settings with type %r called on shader %r", (tex_type, shader_module.get_name())) + for tex_type in uvs_dict: shader_set_uv = getattr(shader_module, "set_" + tex_type + "_uv", None) if shader_set_uv: @@ -237,6 +250,10 @@ def __setup_nodes__(material, effect, attr_dict, tex_dict, uvs_dict, flavors_dic else: lprint("D Unsupported set_uv with type %r called on shader %r", (tex_type, shader_module.get_name())) + # finalize shader on recreate (set material blending method, backface culling etc.) + if recreate: + shader_module.finalize(node_tree, material) + def __clean_node_tree__(node_tree): """Cleans material node tree of any nodes, custom properties. @@ -247,8 +264,10 @@ def __clean_node_tree__(node_tree): # clean nodes and custom props node_tree.nodes.clear() - for key in node_tree.keys(): - del node_tree[key] + + # FIXME: enable it again after: https://developer.blender.org/T68406 is resolved! + # for key in node_tree.keys(): + # del node_tree[key] def __get_shader__(effect, report_not_found, mat_name): diff --git a/addon/io_scs_tools/internals/shaders/std_node_groups/alpha_test_ng.py b/addon/io_scs_tools/internals/shaders/std_node_groups/alpha_test_ng.py new file mode 100644 index 0000000..344678d --- /dev/null +++ b/addon/io_scs_tools/internals/shaders/std_node_groups/alpha_test_ng.py @@ -0,0 +1,129 @@ +# ##### 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 + +import bpy +from io_scs_tools.consts import Material as _MAT_consts + +ALPHA_TEST_G = _MAT_consts.node_group_prefix + "AlphaTestPass" + +_SHADER_TO_RGB_NODE = "ShaderToRGB" +_ALPHA_TEST_NODE = "AlphaTest" +_ALPHA_INV_NODE = "AlphaInv" +_ALPHA_SHADER_NODE = "AlphaShader" + + +def get_node_group(): + """Gets node group for blending pass. + + :return: node group which adds blending pass + :rtype: bpy.types.NodeGroup + """ + + if __group_needs_recreation__(): + __create_node_group__() + + return bpy.data.node_groups[ALPHA_TEST_G] + + +def __group_needs_recreation__(): + """Tells if group needs recreation. + + :return: True group isn't up to date and has to be (re)created; False if group doesn't need to be (re)created + :rtype: bool + """ + # current checks: + # 1. group existence in blender data block + return ALPHA_TEST_G not in bpy.data.node_groups + + +def __create_node_group__(): + """Creates add blending group. + + Inputs: Shader, Alpha + Outputs: Shader + """ + + start_pos_x = 0 + start_pos_y = 0 + + pos_x_shift = 185 + + if ALPHA_TEST_G not in bpy.data.node_groups: # creation + + alpha_test_g = bpy.data.node_groups.new(type="ShaderNodeTree", name=ALPHA_TEST_G) + + else: # recreation + + alpha_test_g = bpy.data.node_groups[ALPHA_TEST_G] + + # delete all inputs and outputs + alpha_test_g.inputs.clear() + alpha_test_g.outputs.clear() + + # delete all old nodes and links as they will be recreated now with actual version + alpha_test_g.nodes.clear() + + # inputs defining + alpha_test_g.inputs.new("NodeSocketShader", "Shader") + alpha_test_g.inputs.new("NodeSocketFloat", "Alpha") + + # outputs defining + alpha_test_g.outputs.new("NodeSocketShader", "Shader") + + # node creation + input_n = alpha_test_g.nodes.new("NodeGroupInput") + input_n.location = (start_pos_x, start_pos_y) + + output_n = alpha_test_g.nodes.new("NodeGroupOutput") + output_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y) + + shader_to_rgb_n = alpha_test_g.nodes.new("ShaderNodeShaderToRGB") + shader_to_rgb_n.name = shader_to_rgb_n.label = _SHADER_TO_RGB_NODE + shader_to_rgb_n.location = (start_pos_x + pos_x_shift * 1, start_pos_y) + + alpha_test_n = alpha_test_g.nodes.new("ShaderNodeMath") + alpha_test_n.name = alpha_test_n.label = _ALPHA_TEST_NODE + alpha_test_n.location = (start_pos_x + pos_x_shift * 1, start_pos_y - 200) + alpha_test_n.operation = "GREATER_THAN" + alpha_test_n.inputs[1].default_value = 0.05 + + alpha_test_inv_n = alpha_test_g.nodes.new("ShaderNodeMath") + alpha_test_inv_n.name = alpha_test_inv_n.label = _ALPHA_INV_NODE + alpha_test_inv_n.location = (start_pos_x + pos_x_shift * 2, start_pos_y - 200) + alpha_test_inv_n.operation = "SUBTRACT" + alpha_test_inv_n.use_clamp = True + alpha_test_inv_n.inputs[0].default_value = 1 + + alpha_shader_n = alpha_test_g.nodes.new("ShaderNodeEeveeSpecular") + alpha_shader_n.name = alpha_shader_n.label = _ALPHA_SHADER_NODE + alpha_shader_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y) + alpha_shader_n.inputs["Base Color"].default_value = (0.0,) * 4 + alpha_shader_n.inputs["Specular"].default_value = (0.0,) * 4 + + # create extra links + alpha_test_g.links.new(shader_to_rgb_n.inputs['Shader'], input_n.outputs['Shader']) + alpha_test_g.links.new(alpha_test_n.inputs[0], input_n.outputs['Alpha']) + + alpha_test_g.links.new(alpha_test_inv_n.inputs[1], alpha_test_n.outputs[0]) + + alpha_test_g.links.new(alpha_shader_n.inputs['Emissive Color'], shader_to_rgb_n.outputs['Color']) + alpha_test_g.links.new(alpha_shader_n.inputs['Transparency'], alpha_test_inv_n.outputs[0]) + + alpha_test_g.links.new(output_n.inputs['Shader'], alpha_shader_n.outputs['BSDF']) diff --git a/addon/io_scs_tools/internals/shaders/std_node_groups/blend_add_ng.py b/addon/io_scs_tools/internals/shaders/std_node_groups/blend_add_ng.py new file mode 100644 index 0000000..b40ec0e --- /dev/null +++ b/addon/io_scs_tools/internals/shaders/std_node_groups/blend_add_ng.py @@ -0,0 +1,125 @@ +# ##### 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 + +import bpy +from io_scs_tools.consts import Material as _MAT_consts + +BLEND_ADD_G = _MAT_consts.node_group_prefix + "BlendAddPass" + +_SHADER_TO_RGB_NODE = "ShaderToRGB" +_EMISSION_SHADER_NODE = "Emission" +_TRANSPARENT_NODE = "Transparent" +_ADD_SHADER_NODE = "AddShader" + + +def get_node_group(): + """Gets node group for blending pass. + + :return: node group which adds blending pass + :rtype: bpy.types.NodeGroup + """ + + if __group_needs_recreation__(): + __create_node_group__() + + return bpy.data.node_groups[BLEND_ADD_G] + + +def __group_needs_recreation__(): + """Tells if group needs recreation. + + :return: True group isn't up to date and has to be (re)created; False if group doesn't need to be (re)created + :rtype: bool + """ + # current checks: + # 1. group existence in blender data block + return BLEND_ADD_G not in bpy.data.node_groups + + +def __create_node_group__(): + """Creates add blending group. + + Inputs: Shader + Outputs: Shader + """ + + start_pos_x = 0 + start_pos_y = 0 + + pos_x_shift = 185 + + if BLEND_ADD_G not in bpy.data.node_groups: # creation + + blend_add_g = bpy.data.node_groups.new(type="ShaderNodeTree", name=BLEND_ADD_G) + + else: # recreation + + blend_add_g = bpy.data.node_groups[BLEND_ADD_G] + + # delete all inputs and outputs + blend_add_g.inputs.clear() + blend_add_g.outputs.clear() + + # delete all old nodes and links as they will be recreated now with actual version + blend_add_g.nodes.clear() + + # inputs defining + blend_add_g.inputs.new("NodeSocketShader", "Shader") + + # outputs defining + blend_add_g.outputs.new("NodeSocketShader", "Shader") + + # node creation + input_n = blend_add_g.nodes.new("NodeGroupInput") + input_n.location = (start_pos_x, start_pos_y) + + output_n = blend_add_g.nodes.new("NodeGroupOutput") + output_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y) + + shader_to_rgb_n = blend_add_g.nodes.new("ShaderNodeShaderToRGB") + shader_to_rgb_n.name = shader_to_rgb_n.label = _SHADER_TO_RGB_NODE + shader_to_rgb_n.location = (start_pos_x + pos_x_shift * 1, start_pos_y) + + emission_shader_n = blend_add_g.nodes.new("ShaderNodeEmission") + emission_shader_n.name = emission_shader_n.label = _EMISSION_SHADER_NODE + emission_shader_n.location = (start_pos_x + pos_x_shift * 2, start_pos_y + 50) + emission_shader_n.inputs['Strength'].default_value = 1.0 + + transparent_shader_n = blend_add_g.nodes.new("ShaderNodeBsdfTransparent") + transparent_shader_n.name = transparent_shader_n.label = _TRANSPARENT_NODE + transparent_shader_n.location = (start_pos_x + pos_x_shift * 2, start_pos_y - 50) + + add_shader_n = blend_add_g.nodes.new("ShaderNodeAddShader") + add_shader_n.name = add_shader_n.label = _ADD_SHADER_NODE + add_shader_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y) + + # links + # input pass + blend_add_g.links.new(shader_to_rgb_n.inputs['Shader'], input_n.outputs['Shader']) + + # pass 1 + blend_add_g.links.new(emission_shader_n.inputs['Color'], shader_to_rgb_n.outputs['Color']) + + # pass 2 + blend_add_g.links.new(add_shader_n.inputs[0], emission_shader_n.outputs['Emission']) + blend_add_g.links.new(add_shader_n.inputs[1], transparent_shader_n.outputs['BSDF']) + + # output pass + blend_add_g.links.new(output_n.inputs['Shader'], add_shader_n.outputs['Shader']) diff --git a/addon/io_scs_tools/internals/shaders/std_node_groups/blend_mult_ng.py b/addon/io_scs_tools/internals/shaders/std_node_groups/blend_mult_ng.py new file mode 100644 index 0000000..3ece4d9 --- /dev/null +++ b/addon/io_scs_tools/internals/shaders/std_node_groups/blend_mult_ng.py @@ -0,0 +1,125 @@ +# ##### 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 + +import bpy +from io_scs_tools.consts import Material as _MAT_consts + +BLEND_MULT_G = _MAT_consts.node_group_prefix + "BlendMultPass" + +_SHADER_TO_RGB_NODE = "ShaderToRGB" +_TRANSPARENT_MULT_NODE = "TransparentMult" +_TRANSPARENT_ALPHA_NODE = "TransparentAlpha" +_MIX_SHADER_NODE = "MixShader" + + +def get_node_group(): + """Gets node group for blending pass. + + :return: node group which adds blending pass + :rtype: bpy.types.NodeGroup + """ + + if __group_needs_recreation__(): + __create_node_group__() + + return bpy.data.node_groups[BLEND_MULT_G] + + +def __group_needs_recreation__(): + """Tells if group needs recreation. + + :return: True group isn't up to date and has to be (re)created; False if group doesn't need to be (re)created + :rtype: bool + """ + # current checks: + # 1. group existence in blender data block + return BLEND_MULT_G not in bpy.data.node_groups + + +def __create_node_group__(): + """Creates add blending group. + + Inputs: Shader + Outputs: Shader + """ + + start_pos_x = 0 + start_pos_y = 0 + + pos_x_shift = 185 + + if BLEND_MULT_G not in bpy.data.node_groups: # creation + + blend_mult_g = bpy.data.node_groups.new(type="ShaderNodeTree", name=BLEND_MULT_G) + + else: # recreation + + blend_mult_g = bpy.data.node_groups[BLEND_MULT_G] + + # delete all inputs and outputs + blend_mult_g.inputs.clear() + blend_mult_g.outputs.clear() + + # delete all old nodes and links as they will be recreated now with actual version + blend_mult_g.nodes.clear() + + # inputs defining + blend_mult_g.inputs.new("NodeSocketShader", "Shader") + + # outputs defining + blend_mult_g.outputs.new("NodeSocketShader", "Shader") + + # node creation + input_n = blend_mult_g.nodes.new("NodeGroupInput") + input_n.location = (start_pos_x, start_pos_y) + + output_n = blend_mult_g.nodes.new("NodeGroupOutput") + output_n.location = (start_pos_x + pos_x_shift * 4, start_pos_y) + + shader_to_rgb_n = blend_mult_g.nodes.new("ShaderNodeShaderToRGB") + shader_to_rgb_n.name = shader_to_rgb_n.label = _SHADER_TO_RGB_NODE + shader_to_rgb_n.location = (start_pos_x + pos_x_shift * 1, start_pos_y) + + transparent_mult_n = blend_mult_g.nodes.new("ShaderNodeBsdfTransparent") + transparent_mult_n.name = transparent_mult_n.label = _TRANSPARENT_MULT_NODE + transparent_mult_n.location = (start_pos_x + pos_x_shift * 2, start_pos_y + 50) + + transparent_a_n = blend_mult_g.nodes.new("ShaderNodeBsdfTransparent") + transparent_a_n.name = transparent_a_n.label = _TRANSPARENT_ALPHA_NODE + transparent_a_n.location = (start_pos_x + pos_x_shift * 2, start_pos_y - 50) + + mix_shader_n = blend_mult_g.nodes.new("ShaderNodeMixShader") + mix_shader_n.name = mix_shader_n.label = _MIX_SHADER_NODE + mix_shader_n.location = (start_pos_x + pos_x_shift * 3, start_pos_y) + + # links + # input pass + blend_mult_g.links.new(shader_to_rgb_n.inputs['Shader'], input_n.outputs['Shader']) + + # pass 1 + blend_mult_g.links.new(transparent_mult_n.inputs['Color'], shader_to_rgb_n.outputs['Color']) + + # pass 2 + blend_mult_g.links.new(mix_shader_n.inputs['Fac'], shader_to_rgb_n.outputs['Alpha']) + blend_mult_g.links.new(mix_shader_n.inputs[1], transparent_mult_n.outputs['BSDF']) + blend_mult_g.links.new(mix_shader_n.inputs[2], transparent_a_n.outputs['BSDF']) + + # output pass + blend_mult_g.links.new(output_n.inputs['Shader'], mix_shader_n.outputs['Shader']) diff --git a/addon/io_scs_tools/internals/structure.py b/addon/io_scs_tools/internals/structure.py index 9418143..1f023f7 100755 --- a/addon/io_scs_tools/internals/structure.py +++ b/addon/io_scs_tools/internals/structure.py @@ -18,6 +18,7 @@ # Copyright (C) 2013-2017: SCS Software +import re from collections import OrderedDict from io_scs_tools.utils import convert as _convert_utils @@ -97,6 +98,27 @@ def set_prop_value(self, prop_key, value): return False + def remove_section(self, sec_type, sec_prop, regex_str): + """Remnoves section if section with given type, given property and given property value regex is found. + + :param sec_type: type of the section we are looking for + :type sec_type: str + :param sec_prop: type of property we are searching in section + :type sec_prop: str + :param regex_str: regex for matching property value + :type regex_str: str + :return: True if removed; False otherwise + :rtype: bool + """ + for sec in self.sections: + if sec.type == sec_type: + prop = sec.get_prop(sec_prop) + if prop and re.search(regex_str, prop[1]): + self.sections.remove(sec) + return True + + return False + class UnitData(object): """Unit data structure (SII files): @@ -173,7 +195,7 @@ def get_prop(self, prop_name, default=None): :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 + :rtype: None | any """ if prop_name not in self.props: diff --git a/addon/io_scs_tools/operators/__init__.py b/addon/io_scs_tools/operators/__init__.py index 1fc5c82..4325bc5 100644 --- a/addon/io_scs_tools/operators/__init__.py +++ b/addon/io_scs_tools/operators/__init__.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software from io_scs_tools.operators import material from io_scs_tools.operators import mesh @@ -24,3 +24,21 @@ from io_scs_tools.operators import scene from io_scs_tools.operators import wm from io_scs_tools.operators import world + + +def register(): + material.register() + mesh.register() + object.register() + scene.register() + wm.register() + world.register() + + +def unregister(): + material.unregister() + mesh.unregister() + object.unregister() + scene.unregister() + wm.unregister() + world.unregister() diff --git a/addon/io_scs_tools/operators/bases/export.py b/addon/io_scs_tools/operators/bases/export.py new file mode 100644 index 0000000..2952ee3 --- /dev/null +++ b/addon/io_scs_tools/operators/bases/export.py @@ -0,0 +1,171 @@ +# ##### 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 + +import bpy +from io_scs_tools import exp as _export +from io_scs_tools.utils import object as _object_utils +from io_scs_tools.utils import get_scs_globals as _get_scs_globals +from io_scs_tools.utils.printout import lprint + + +class SCSExportHelper: + """Class for implementation of SCS export routines, to take care of objects/collections visibilites and + implemenet convinient methods for usage as parent class in export operators. + """ + + def __init__(self): + self.cached_objects = None + """:type list[bpy.types.Object]: Used in selection mode to cache selected objects, to avoid multiple collecting.""" + self.objects_states = dict() + """:type dict[bpy.types.Object, boo]]: Hide & selection states for objects gathered on selection export. Used to restore + their states after export.""" + self.collection_visibilites = dict() + """:type dict[bpy.types.Object, bool]: Original collection visibilities used to restore their state after export.""" + self.scene = None + """:type bpy.types.Scene: Scene to which we will temporarly link objects that needs to be exported.""" + self.active_scene = None + """:type bpy.types.Scene: Scene which was active before export and should be recovered as active once export is done.""" + + def get_objects_for_export(self): + """Get objects for export, list filtered and extended depending on export scope. + + :return: list of objects to export calculated from selection + :rtype: list[bpy.types.Object] + """ + + # if cached, just return + if self.cached_objects: + return self.cached_objects + + objs_to_export = [] + + export_scope = _get_scs_globals().export_scope + if export_scope == "selection": + for root in _object_utils.gather_scs_roots(bpy.context.selected_objects): + objs_to_export.append(root) + + children = _object_utils.get_children(root) + children_unselected = [] + children_selected = [] + + for child_obj in children: + if child_obj.select_get(): + children_selected.append(child_obj) + else: + children_unselected.append(child_obj) + + # if any children was selected make sure, to export only them + if len(children_selected) > 0: + objs_to_export.extend(children_selected) + else: + objs_to_export.extend(children_unselected) + elif export_scope == "scene": + objs_to_export = tuple(bpy.context.scene.objects) + elif export_scope == "scenes": + objs_to_export = tuple(bpy.data.objects) + + # cache filtered list, to be able to retrive it quickly on second call + self.cached_objects = objs_to_export + + return self.cached_objects + + def init(self): + """Init routine to create export scene, link all exprt objects to it and unhide objects so they can be seen in preview and exported properly. + The problem is that depsgraph doesn't apply modifiers if object is hidden (directly or via collections). + + NOTE: Should be called before get_objects_for_export. + """ + + # we have to get object to export now, because creating new scene + # will invalidate context and selected objects within context are lost. + objs_to_export = self.get_objects_for_export() + + # create export scene + self.scene = bpy.data.scenes.new("SCS Export") + self.active_scene = bpy.context.window.scene + bpy.context.window.scene = self.scene + + # link objects to export scene and unhide all of them + for obj in objs_to_export: + self.scene.collection.objects.link(obj) + self.objects_states[obj] = obj.hide_viewport + obj.hide_viewport = False + + scs_globals = _get_scs_globals() + if scs_globals.export_scope == "selection" and scs_globals.preview_export_selection: + scs_globals.preview_export_selection_active = True + + def finish(self): + """Finish routine to restore objects & scene state after export. + + NOTE: Should be called after export has completed. + """ + _get_scs_globals().preview_export_selection_active = False + + for obj, state in self.objects_states.items(): + obj.hide_viewport = state + + # recover old active scene and remove temporary one + bpy.context.window.scene = self.active_scene + bpy.data.scenes.remove(self.scene) + self.scene = None + + def execute_export(self, context, without_preview): + """Executes export. + + :param context: operator context + :type context: bpy_struct + :param without_preview: is export run without preview? + :type without_preview: bool + :return: success of batch export + :rtype: {'FINISHED'} | {'CANCELLED'} + """ + + # show all collections, if normal export, so all modifiers will be applied correctly + if without_preview: + self.init() + + init_obj_list = self.get_objects_for_export() + + # check extension for EF format and properly assign it to name suffix + ef_name_suffix = "" + if _get_scs_globals().export_output_type == "EF": + ef_name_suffix = ".ef" + + try: + result = _export.batch_export(self, init_obj_list, name_suffix=ef_name_suffix) + except Exception as e: + + result = {"CANCELLED"} + context.window.cursor_modal_restore() + + import traceback + + trace_str = traceback.format_exc().replace("\n", "\n\t ") + lprint("E Unexpected %r accured during batch export:\n\t %s", + (type(e).__name__, trace_str), + report_errors=1, + report_warnings=1) + + # restore collections visiblities if normal export + if without_preview: + self.finish() + + return result diff --git a/addon/io_scs_tools/operators/bases/selection.py b/addon/io_scs_tools/operators/bases/selection.py index 1de609a..996e0ad 100644 --- a/addon/io_scs_tools/operators/bases/selection.py +++ b/addon/io_scs_tools/operators/bases/selection.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy from bpy.props import IntProperty @@ -36,7 +36,7 @@ class Selection: SHIFT_SELECT = _OP_consts.SelectionType.shift_select CTRL_SELECT = _OP_consts.SelectionType.ctrl_select - select_type = IntProperty() + select_type: IntProperty() def get_select_state(self): """Define selection state for objects depending on current select_type state. @@ -71,4 +71,4 @@ def invoke(self, context, event): elif event.ctrl: self.select_type = self.CTRL_SELECT - return self.execute(context) \ No newline at end of file + return self.execute(context) diff --git a/addon/io_scs_tools/operators/bases/view.py b/addon/io_scs_tools/operators/bases/view.py index 789310c..0ade534 100644 --- a/addon/io_scs_tools/operators/bases/view.py +++ b/addon/io_scs_tools/operators/bases/view.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy from bpy.props import IntProperty @@ -37,22 +37,22 @@ class View: SHIFT_VIEW = _OP_consts.ViewType.shift_view CTRL_VIEW = _OP_consts.ViewType.ctrl_view - view_type = IntProperty(default=VIEWONLY) + view_type: IntProperty(default=VIEWONLY) @staticmethod def get_objects(context): - if context.scene.scs_props.visibility_tools_scope == "Global": + if context.workspace.scs_props.visibility_tools_scope == "Global": objects = context.scene.objects else: - scs_root_object = _object_utils.get_scs_root(context.scene.objects.active) + scs_root_object = _object_utils.get_scs_root(context.view_layer.objects.active) if scs_root_object: objects = _object_utils.get_children(scs_root_object) - scs_root_object.hide = False - scs_root_object.select = True - bpy.context.scene.objects.active = scs_root_object + _object_utils.hide_set(scs_root_object, False) + _object_utils.select_set(scs_root_object, True) + bpy.context.view_layer.objects.active = scs_root_object else: # fallback don't do anything objects = [] @@ -70,7 +70,7 @@ def get_hide_state(self): # define type of (de)selection if self.view_type == self.VIEWONLY: object_hide = False - bpy.ops.object.hide_objects_within_root() + bpy.ops.object.scs_tools_hide_objects_within_scs_root() elif self.view_type == self.SHIFT_VIEW: object_hide = False @@ -91,4 +91,4 @@ def invoke(self, context, event): elif event.ctrl: self.view_type = self.CTRL_VIEW - return self.execute(context) \ No newline at end of file + return self.execute(context) diff --git a/addon/io_scs_tools/operators/material.py b/addon/io_scs_tools/operators/material.py index 01cd245..2279e27 100644 --- a/addon/io_scs_tools/operators/material.py +++ b/addon/io_scs_tools/operators/material.py @@ -16,14 +16,16 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2015: SCS Software +# Copyright (C) 2013-2019: SCS Software -from importlib.machinery import SourceFileLoader import bpy import os -from bpy.props import StringProperty, BoolProperty +import re +from bpy.props import StringProperty, BoolProperty, EnumProperty, IntProperty +from io_scs_tools.consts import Material as _MAT_consts from io_scs_tools.exp import tobj as _tobj_exp from io_scs_tools.internals import looks as _looks +from io_scs_tools.utils import convert as _convert_utils from io_scs_tools.utils import get_scs_globals as _get_scs_globals from io_scs_tools.utils import material as _material_utils from io_scs_tools.utils import object as _object_utils @@ -37,35 +39,23 @@ class Common: Wrapper class for better navigation in file """ - class ReloadMaterials(bpy.types.Operator): + class SCS_TOOLS_OT_ReloadMaterials(bpy.types.Operator): bl_label = "Reload SCS Materials" - bl_idname = "material.scs_reload_nodes" + bl_idname = "material.scs_tools_reload_materials" bl_description = "Reload node trees & any shader interface changes for all SCS materials in blend file." def execute(self, context): - # find group names created by Blender Tools with - # dynamic importing of all modules from "internals/shaders" folder - # and checking if module has "get_node_group" functions which indicates that - # module creates node group + # find group names created by Blender Tools with traversing all node groups and matching them by scs prefix groups_to_remove = [ "AddEnvGroup", # from v0.6 "FresnelGroup", # from v0.6 "LampmaskMixerGroup", # from v0.6 "ReflectionNormalGroup", # from v0.6 ] - for root, dirs, files in os.walk(_path_utils.get_addon_installation_paths()[0] + os.sep + "internals/shaders"): - - for file in files: - - if not file.endswith(".py"): - continue - - module = SourceFileLoader(root + os.sep + file, root + os.sep + file).load_module() - if "get_node_group" in dir(module): - - ng = module.get_node_group() - groups_to_remove.append(ng.name) + for group in bpy.data.node_groups: + if group.name.startswith(_MAT_consts.node_group_prefix): + groups_to_remove.append(group.name) # 1. clear nodes on materials for mat in bpy.data.materials: @@ -137,29 +127,434 @@ def execute(self, context): # without this call we might end up with outdated looks raising errors once user will switch to them for scs_root in scs_roots: _looks.update_look_from_material(scs_root, mat, True) + else: + # reset usage of nodes, to prevent crashing in no preset is found + mat.use_nodes = False return {'FINISHED'} - class SearchShaderPreset(bpy.types.Operator): + class SCS_TOOLS_OT_SearchShaderPreset(bpy.types.Operator): bl_label = "Search Shader Preset" - bl_idname = "material.scs_search_shader_preset" + bl_idname = "material.scs_tools_search_shader_preset" bl_description = "Quickly search trough shader presets with typing and assign selected shader preset to material." - bl_property = "shader_presets_list" + bl_property = "shader_presets" + + # NOTE: As shader preset list is "dynamic" property we can use that fact, + # to replicate same property inside this operator, but use functions from global scs props module. + # Second variable however is there because functions from global scs props requires it. + from io_scs_tools.properties.addon_preferences import SCSInventories + + shader_presets: EnumProperty( + name="Shader Presets", + description="Shader presets", + items=SCSInventories.retrieve_shader_presets_items, + get=SCSInventories.get_shader_presets_item, + set=SCSInventories.set_shader_presets_item, + ) + shader_presets_sorted: BoolProperty( + name="Shader Preset List Sorted Alphabetically", + description="Sort Shader preset list alphabetically", + default=True, + ) + + def execute(self, context): + lprint("D You searched & selected preset with name: %s", (self.shader_presets,)) + return {'FINISHED'} - from io_scs_tools.properties.world import GlobalSCSProps + def invoke(self, context, event): + wm = context.window_manager + wm.invoke_search_popup(self) + return {'FINISHED'} + + class SCS_TOOLS_OT_AdaptColorManagement(bpy.types.Operator): + bl_label = "Adapt Color Management" + bl_idname = "material.scs_tools_adapt_color_management" + bl_description = "Adapt current scene color management for proper colors in SCS Materials" + bl_options = {'INTERNAL'} - # Create reference to shader preset list and sorting property from global scs props. - # This way operator will be able to assign values directly to them once called. - shader_presets_list = GlobalSCSProps.shader_preset_list - shader_preset_list_sorted = GlobalSCSProps.shader_preset_list_sorted + adapt: IntProperty(name="Adapt Color Management?", default=-1) + + @classmethod + def poll(cls, context): + return not _material_utils.has_valid_color_management(context.scene) + + @staticmethod + def draw_popup_menu(self, context): + layout = self.layout + + display_settings = context.scene.display_settings + view_settings = context.scene.view_settings + + settings_texts = [] + if display_settings.display_device != 'sRGB': + settings_texts.append("%i. Display Device: %r (desired 'sRGB')" % (len(settings_texts) + 1, display_settings.display_device)) + if view_settings.view_transform != 'Standard': + settings_texts.append("%i. View Transform: %r (desired 'Standard')" % (len(settings_texts) + 1, view_settings.view_transform)) + if view_settings.look != 'None': + settings_texts.append("%i. Look: %r (desired 'None')" % (len(settings_texts) + 1, view_settings.look)) + if view_settings.exposure != 0.0: + settings_texts.append("%i. Exposure: '%.1f' (desired '0.0')" % (len(settings_texts) + 1, view_settings.exposure)) + if view_settings.gamma != 1.0: + settings_texts.append("%i. Gamma: '%.1f' (desired '1.0')" % (len(settings_texts) + 1, view_settings.gamma)) + + body = layout.column(align=True) + body.label(text="Some of scene color management settings are preventing proper visualization of SCS Materials:") + for settings_info in settings_texts: + body.label(text=settings_info) + body.label() + + footer = body.column(align=True) + footer.label(text="Do you want to automatically set desired values?") + props = footer.operator("material.scs_tools_adapt_color_management", text="Yes") + props.adapt = 1 + props = footer.operator("material.scs_tools_adapt_color_management", text="No") + props.adapt = 0 def execute(self, context): - lprint("D You searched & selected preset with name: %s", (self.shader_presets_list,)) + lprint("D Executed color management adoption!") + + if self.adapt != 1: # no adaption requested + return {'CANCELLED'} + + if not context.scene: + self.report({'ERROR'}, "Can't adapt color management, no scene!") + return {'CANCELLED'} + + display_settings = context.scene.display_settings + display_settings.display_device = "sRGB" + + view_settings = context.scene.view_settings + view_settings.view_transform = "Standard" + view_settings.look = "None" + view_settings.exposure = 0.0 + view_settings.gamma = 1.0 + + # reset adapt property, so next invocation will bring up popup again! + self.adapt = -1 + return {'FINISHED'} def invoke(self, context, event): wm = context.window_manager - wm.invoke_search_popup(self) + + # adapt state was set, do execution + if self.adapt != -1: + return self.execute(context) + + wm.popup_menu(self.draw_popup_menu, title="Color Management for SCS Materials!", icon='COLOR') + return {'FINISHED'} + + class SCS_TOOLS_OT_MergeMaterials(bpy.types.Operator): + bl_label = "Merge SCS Materials" + bl_idname = "material.scs_tools_merge_materials" + bl_description = "Collect same SCS materials and assign only unique instance to all objects in blend file." + + OBJ_SCOPE_SEL = "selected" + OBJ_SCOPE_ALL = "all" + + BY_N = "name" + BY_NE = "name_effect" + BY_NEA = "name_effect_attributes" + + scope_type: EnumProperty( + name="Objects Scope", + description="Which objects should take part in materials merging: selected or all", + items=[ + (OBJ_SCOPE_SEL, "Selected", "Materials will be merged on selected objects only", "", 1), + (OBJ_SCOPE_ALL, "All", "Materails will be merged on all objects in blend file", "", 2) + ], + default=OBJ_SCOPE_ALL + ) + + merge_type: EnumProperty( + name="Merge Type", + description="What should be taken in consideration while merging", + items=[ + (BY_N, "Name", "Merge by material name only", "", 1), + (BY_NE, "Name & Effect", "Merge by material name & effect", "", 2), + (BY_NEA, "Name & Effect & Attributes", "Merge by material name & effect & attributes in looks", "", 3), + ], + default=BY_NEA + ) + + def draw(self, context): + layout = self.layout + + body = layout.column(align=True) + body.label(text="Objects Scope:") + body.row().prop(self, "scope_type", expand=True) + body.label(text="Merge by:") + body.prop(self, "merge_type", expand=True) + + def merge_materials(self, mats_to_merge, base_mat_look_entries=None): + """Merge givem materials across all objects in blender scene. + If additional base material look entries is provided looks entries of material on current object + have to match to the base one, otherwise merging on particular object won't be done. + + :param mats_to_merge: materials to merge, dictonary with list of material duplicates per base material name + :type mats_to_merge: dict[str, list[bpy.types.Material]] + :param base_mat_look_entries: look entries from base material for in depth attributes check (finding perfect material copy) + :type base_mat_look_entries: dict[bpy.types.Material, dict] + """ + + reassigned_objs = set() + reassigned_mats = set() + mats_for_look_reassign = {} + + if self.scope_type == self.OBJ_SCOPE_SEL: + objs_to_check = bpy.context.selected_objects + elif self.scope_type == self.OBJ_SCOPE_ALL: + objs_to_check = bpy.data.objects + else: + self.report({'ERROR'}, "Invalid object scope selected!") + return {'CANCELLED'} + + for obj in objs_to_check: + root_obj = _object_utils.get_scs_root(obj) + + # make entry for looks reassignment map + if root_obj not in mats_for_look_reassign: + mats_for_look_reassign[root_obj] = set() + + for mat_slot in obj.material_slots: + mat = mat_slot.material + + # ignore empty slots + if not mat: + continue + + # iterate whole materials to merge dictionary, + # search for duplicate material and re-assign + for base_mat_name in mats_to_merge: + mats_list = mats_to_merge[base_mat_name] + + # only one material exists, means no duplicates thus continue + if len(mats_list) <= 1: + continue + + # first in the list is always base material + base_mat = mats_list[0] + + # base material already used, continue + if base_mat == mat: + continue + + # not found in materials duplicate list, continue + if mat not in mats_list: + continue + + # if base material look entries provided, skip the materials that don't have same looks setup as base material + if base_mat_look_entries: + mat_look_entries = _looks.get_material_entries(root_obj, mat) + if base_mat_look_entries[base_mat] != mat_look_entries: + continue + + # ressign material + mat_slot.material = base_mat + + # mark this material for reassignment in looks, instead of immidiate reassignment + # which would invalidate getter for material entries for other objects using current material on this root. + mats_for_look_reassign[root_obj].add((mat, base_mat)) + + reassigned_mats.add(mat) + reassigned_objs.add(obj) + break + + # reassign materials entries on looks of all roots + for root_obj in mats_for_look_reassign: + for mat, base_mat in mats_for_look_reassign[root_obj]: + _looks.reassign_material(root_obj, base_mat, mat) + + reassigned_mats_count = len(reassigned_mats) + reassigned_objs_count = len(reassigned_objs) + if reassigned_mats_count + reassigned_objs_count > 0: + self.report({'INFO'}, "Successfully merged %i materials on %i objects." % (reassigned_mats_count, reassigned_objs_count)) + else: + self.report({'WARNING'}, "No materials could be merged!. No duplicates found.") + + def execute(self, context): + + # flush any possible warnings not to interfer with the ones we might report here + lprint("", report_warnings=-1) + + # collect scs materials + scs_materials = [] + for mat in bpy.data.materials: + if mat.scs_props.mat_effect_name != "" and mat.scs_props.active_shader_preset_name != "": + scs_materials.append(mat) + + mats_to_merge = {} + + # 1. group materials by name + for mat in scs_materials: + name = mat.name + + if re.match(r".+((\.|_)\d{3})$", name): + base_name = name[:-4] + else: + base_name = name + + if base_name not in mats_to_merge: + mats_to_merge[base_name] = [] + + # order materials in the list, where first one is our original to merge to + if base_name == name: + mats_to_merge[base_name].insert(0, mat) + else: + mats_to_merge[base_name].append(mat) + + # filter out single instance materials as they can't be really merged + for mat_name in list(mats_to_merge.keys()): + if len(mats_to_merge[mat_name]) <= 1: + del mats_to_merge[mat_name] + + # report and filter out the ones without base material + for mat_name in list(mats_to_merge.keys()): + if mat_name not in bpy.data.materials: + # report + msg = "W Missing base material for (to merge them, rename one to %r):\n\t " % mat_name + for mat in mats_to_merge[mat_name]: + msg += "'" + mat.name + "', " + lprint(msg.strip(", ")) + + # remove + del mats_to_merge[mat_name] + + # now merge by name and finish + if self.merge_type == self.BY_N: + self.merge_materials(mats_to_merge) + lprint("", report_warnings=1) + return {'FINISHED'} + + # 2. group by effect name + for base_name, mat_list in mats_to_merge.copy().items(): + base_mat_effect = mat_list[0].scs_props.mat_effect_name + filtered_mat_list = [] + for mat in mat_list[1:]: + if mat.scs_props.mat_effect_name == base_mat_effect: + filtered_mat_list.append(mat) + + if filtered_mat_list: + # prepend base + filtered_mat_list.insert(0, mat_list[0]) + # update materials to merge dictonary + mats_to_merge[base_name] = filtered_mat_list + else: + del mats_to_merge[base_name] + + # now merge by name & effect and finish + if self.merge_type == self.BY_NE: + self.merge_materials(mats_to_merge) + lprint("", report_warnings=1) + return {'FINISHED'} + + # 3. complete copy + # go trough objects and collect all mergable base materials, material is mergable if one of the following: + # 1. is not used in any roots (no looks) + # 2. used only on one root (only one look series is available) + # 3. used in more roots where look entries are same for all of the roots (more looks series and all are the same) + mergable_base_mats = {} + unmergable_base_mats = set() + for obj in bpy.data.objects: + for mat_slot in obj.material_slots: + mat = mat_slot.material + + if not mat: + continue + + # ignore non-base materials + if mat.name not in mats_to_merge: + continue + + root_obj = _object_utils.get_scs_root(obj) + mat_look_entries = _looks.get_material_entries(root_obj, mat) + + # if no entry for current material in mergable and unmergable, save look setup from this root object + # otherwise move to unmergable if current root look entries doesn't match with saved one, + # this means we have multiple roots with different looks setup + if mat not in mergable_base_mats and mat not in unmergable_base_mats: + mergable_base_mats[mat] = mat_look_entries + elif mat in mergable_base_mats and mergable_base_mats[mat] != mat_look_entries: + del mergable_base_mats[mat] + unmergable_base_mats.add(mat) + + # remove unmergable base materials entries + for mat in unmergable_base_mats: + del mats_to_merge[mat.name] + + # report unmergable + if len(mats_to_merge) == 0 and len(unmergable_base_mats) > 0: + msg = ("No materials merging can be done. Found %i material candidates, " + "however they are used on multiple SCS Root objects with different looks setup!") % len(unmergable_base_mats) + self.report({'WARNING'}, msg) + lprint("W " + msg, report_warnings=1) + return {'CANCELLED'} + elif len(unmergable_base_mats) > 0: + msg = "W Due to different looks settings on multiple SCS Root objects, following materials are unmergable:\n\t " + for i, mat in enumerate(unmergable_base_mats): + msg += str(i + 1) + ". '" + mat.name + "'\n\t " + lprint(msg.strip("\n\t ")) + + # now merge by name & effect & attributes and finish + if self.merge_type == self.BY_NEA: + self.merge_materials(mats_to_merge, mergable_base_mats) + lprint("", report_warnings=1) + return {'FINISHED'} + + # invalid type, just cancel + return {'CANCELLED'} + + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_props_dialog(self, width=200) + + class SCS_TOOLS_OT_MaterialItemExtrasMenu(bpy.types.Operator): + bl_label = "More Options" + bl_idname = "material.scs_tools_material_item_extras" + bl_description = "Show more options for this material item (WT, Copy Linear Color etc.)" + bl_options = {'REGISTER', 'UNDO'} + + property_str: StringProperty( + description="String representing which property should be worked on.", + default="", + options={'HIDDEN'}, + ) + + __property_str = "" + """Static variable holding property name, for which context menu should be created.""" + + @staticmethod + def draw_context_menu(self, context): + layout = self.layout + property_str = Common.SCS_TOOLS_OT_MaterialItemExtrasMenu.__property_str + + body = layout.column(align=True) + props = body.operator("material.scs_tools_write_trough_looks", text="WT") + props.property_str = property_str + props.wt_type = Looks.SCS_TOOLS_OT_WriteThroughLooks.WT_TYPE_NORMAL + props = body.operator("material.scs_tools_write_trough_looks", text="WT - Same Look on All SCS Roots") + props.property_str = property_str + props.wt_type = Looks.SCS_TOOLS_OT_WriteThroughLooks.WT_TYPE_SAME_LOOK + props = body.operator("material.scs_tools_write_trough_looks", text="WT - All Looks on All SCS Roots") + props.property_str = property_str + props.wt_type = Looks.SCS_TOOLS_OT_WriteThroughLooks.WT_TYPE_ALL + + # show copy/paste only on color type of properties, as any other properties in our games (even aux ones) + # are not in any color space and can be copy/pasted normally as any other value in blender + material = context.active_object.active_material + if material.scs_props.bl_rna.properties[property_str].subtype == "COLOR": + body.separator(factor=0.25) + + props = body.operator("material.scs_tools_cpy_color_as_linear", icon="COPYDOWN") + props.property_str = property_str + props = body.operator("material.scs_tools_paste_color_from_linear", icon="PASTEDOWN") + props.property_str = property_str + + def execute(self, context): + # copy property name to static variable for usage in context menu + Common.SCS_TOOLS_OT_MaterialItemExtrasMenu.__property_str = self.property_str + + context.window_manager.popup_menu(self.draw_context_menu, title="Material Item Extras Menu", icon="LAYER_USED") return {'FINISHED'} @@ -168,9 +563,9 @@ class CustomMapping: Wrapper class for better navigation in file """ - class AddCustomMapping(bpy.types.Operator): + class SCS_TOOLS_OT_AddCustomTexCoordMapping(bpy.types.Operator): bl_label = "Add Custom TexCoord Mapping" - bl_idname = "material.add_custom_tex_coord_map" + bl_idname = "material.scs_tools_add_custom_tex_coord_mapping" bl_description = "Add custom TexCoord mapping to list" bl_options = {'INTERNAL'} @@ -183,9 +578,9 @@ def execute(self, context): return {'FINISHED'} - class RemoveCustomMapping(bpy.types.Operator): + class SCS_TOOLS_OT_RemoveCustomTexCoordMapping(bpy.types.Operator): bl_label = "Remove Custom TexCoord Mapping" - bl_idname = "material.remove_custom_tex_coord_map" + bl_idname = "material.scs_tools_remove_custom_tex_coord_mapping" bl_description = "Remove selected custom TexCoord mapping from list" bl_options = {'INTERNAL'} @@ -204,16 +599,26 @@ class Looks: Wrapper class for better navigation in file """ - class WriteThrough(bpy.types.Operator): + class SCS_TOOLS_OT_WriteThroughLooks(bpy.types.Operator): bl_label = "Write Through" - bl_idname = "material.scs_looks_wt" - bl_description = ("Write current material value through all currently defined looks within SCS Game Object " - "( 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_idname = "material.scs_tools_write_trough_looks" + bl_description = ("Write current material value through looks depending on given write trough type:\n" + "0 - All looks within same SCS Root object\n" + "1 - To same look on all SCS Root objects\n" + "2 - To all looks on all SCS Root objects") bl_options = {'REGISTER', 'UNDO'} - property_str = StringProperty( + WT_TYPE_NONE = -1 + WT_TYPE_NORMAL = 0 + WT_TYPE_SAME_LOOK = 1 + WT_TYPE_ALL = 2 + + wt_type: IntProperty( + name="Type of WT", + default=WT_TYPE_NONE + ) + + property_str: StringProperty( description="String representing which property should be written through.", default="", options={'HIDDEN'}, @@ -287,8 +692,22 @@ def execute(self, context): def invoke(self, context, event): - self.is_shift = event.shift - self.is_ctrl = event.ctrl + # decide which type of WT is it, prefer manually set type over ctrl and shift modifiers + if self.wt_type == self.WT_TYPE_NORMAL: + self.is_ctrl = False + self.is_shift = False + elif self.wt_type == self.WT_TYPE_SAME_LOOK: + self.is_ctrl = True + self.is_shift = False + elif self.wt_type == self.WT_TYPE_ALL: + self.is_ctrl = True + self.is_shift = True + else: + self.is_ctrl = False + self.is_shift = False + + # always reset type for next invoke + self.wt_type = -1 return self.execute(context) @@ -298,12 +717,12 @@ class Tobj: Wrapper class for better navigation in file """ - class ReloadTOBJ(bpy.types.Operator): + class SCS_TOOLS_ReloadTOBJ(bpy.types.Operator): bl_label = "Reload TOBJ settings" - bl_idname = "material.scs_reload_tobj" + bl_idname = "material.scs_tools_reload_tobj" bl_description = "Reload TOBJ file for this texture (if marked red TOBJ file is out of sync)" - texture_type = StringProperty( + texture_type: StringProperty( description="", default="", options={'HIDDEN'}, @@ -319,12 +738,12 @@ def execute(self, context): return {'FINISHED'} - class CreateTOBJ(bpy.types.Operator): + class SCS_TOOLS_OT_CreateTOBJ(bpy.types.Operator): bl_label = "Create TOBJ" - bl_idname = "material.scs_create_tobj" + bl_idname = "mmaterial.scs_tools_create_tobj" bl_description = "Create TOBJ file for this texture with default settings" - texture_type = StringProperty( + texture_type: StringProperty( description="", default="", options={'HIDDEN'}, @@ -358,12 +777,12 @@ class LampSwitcher: Wrapper class for better navigation in file """ - class SwitchLampMask(bpy.types.Operator): + class SCS_TOOLS_OT_SwitchLampmask(bpy.types.Operator): bl_label = "Switch Lamp Mask" - bl_idname = "material.scs_switch_lampmask" + bl_idname = "material.scs_tools_switch_lampmask" bl_description = "Show/Hide specific areas of lamp mask texture in lamp materials." - lamp_type = StringProperty( + lamp_type: StringProperty( description="", default="", options={'HIDDEN'}, @@ -371,7 +790,7 @@ class SwitchLampMask(bpy.types.Operator): def execute(self, context): - from io_scs_tools.internals.shaders.eut2.std_node_groups.lampmask_mixer import LAMPMASK_MIX_G + from io_scs_tools.internals.shaders.eut2.std_node_groups.lampmask_mixer_ng import LAMPMASK_MIX_G if LAMPMASK_MIX_G in bpy.data.node_groups: @@ -394,19 +813,23 @@ def execute(self, context): class Aliasing: - class LoadAliasedMaterial(bpy.types.Operator): + """ + Wrapper class for better navigation in file + """ + + class SCS_TOOLS_OT_LoadAliasedMaterial(bpy.types.Operator): bl_label = "Load Aliased Mat" - bl_idname = "material.load_aliased_material" + bl_idname = "material.scs_tools_load_aliased_material" bl_description = "Load values from aliased material." bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): - return context.material is not None + return context.active_object and context.active_object.active_material def execute(self, context): - material = context.material + material = context.active_object.active_material tex_raw_path = getattr(material.scs_props, "shader_texture_base", "") tex_raw_path = tex_raw_path.replace("\\", "/") @@ -487,7 +910,7 @@ def execute(self, context): "If you want to alias values from aliased material completely,\n" "select correct shader preset and flavor combination and execute aliasing again!") - bpy.ops.wm.show_warning_message("INVOKE_DEFAULT", icon="INFO", title="Aliasing partially successful!", message=msg) + bpy.ops.wm.scs_tools_show_message_in_popup("INVOKE_DEFAULT", icon='INFO', title="Aliasing partially successful!", message=msg) self.report({'WARNING'}, "Aliased partially succeded!") @@ -495,28 +918,32 @@ def execute(self, context): class Flavors: - class SwitchFlavor(bpy.types.Operator): + """ + Wrapper class for better navigation in file + """ + + class SCS_TOOLS_OT_SwitchFlavor(bpy.types.Operator): bl_label = "Switch Given Flavor" - bl_idname = "material.scs_switch_flavor" + bl_idname = "material.scs_tools_switch_flavor" bl_description = "Enable/disable this flavor for selected shader." - flavor_name = StringProperty( + flavor_name: StringProperty( options={'HIDDEN'}, ) - flavor_enabled = BoolProperty( + flavor_enabled: BoolProperty( options={"HIDDEN"} ) @classmethod def poll(cls, context): - return context.material + return context.active_object and context.active_object.active_material def execute(self, context): lprint("I " + self.bl_label + "...") from io_scs_tools.internals import shader_presets as _shader_presets - mat = context.material + mat = context.active_object.active_material mat_scs_props = mat.scs_props """:type: io_scs_tools.properties.material.MaterialSCSTools""" preset = _shader_presets.get_preset(mat_scs_props.active_shader_preset_name) @@ -561,8 +988,8 @@ def execute(self, context): # finally set new shader data to material section = _shader_presets.get_section(preset.name, flavors_suffix) - context.material.scs_props.mat_effect_name = preset.effect + flavors_suffix - _material_utils.set_shader_data_to_material(context.material, section) + mat.scs_props.mat_effect_name = preset.effect + flavors_suffix + _material_utils.set_shader_data_to_material(mat, section) # sync shader types on all scs roots by updating looks on them # to avoid different shader types on different scs roots for same material @@ -573,15 +1000,21 @@ def execute(self, context): class Texture: - class SelectShaderTextureFilePath(bpy.types.Operator): - """Universal operator for setting relative or absolute paths to shader texture files.""" - bl_label = "Select Shader Texture File" - bl_idname = "material.scs_select_shader_texture_filepath" + """ + Wrapper class for better navigation in file + """ + + class SCS_TOOLS_OT_SelectTexturePath(bpy.types.Operator): + """Universal operator for setting relative or absolute paths to material texture files.""" + bl_label = "Select Material Texture File" + bl_idname = "material.scs_tools_select_texture_path" bl_description = "Open a Texture file browser" bl_options = {'REGISTER', 'UNDO'} - shader_texture = bpy.props.StringProperty(options={'HIDDEN'}) - filepath = StringProperty( + shader_texture: StringProperty( + options={'HIDDEN'} + ) + filepath: StringProperty( name="Shader Texture File", description="Shader texture relative or absolute file path", # maxlen=1024, @@ -589,9 +1022,9 @@ class SelectShaderTextureFilePath(bpy.types.Operator): ) # NOTE: force blender to use thumbnails preview by default with display_type - display_type = get_filebrowser_display_type(is_image=True) + display_type: get_filebrowser_display_type(is_image=True) - filter_glob = StringProperty( + filter_glob: StringProperty( default="*.tobj;*.tga;*.png;", options={'HIDDEN'} ) @@ -628,3 +1061,125 @@ def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} + + +class Attribute: + """ + Wrapper class for better navigation in file + """ + + class SCS_TOOLS_OT_CopyColorAsLinear(bpy.types.Operator): + bl_label = "Copy Color as Linear" + bl_idname = "material.scs_tools_cpy_color_as_linear" + bl_description = "Copy this color to clipboard in linear colorspace (can be used for colors in SII and MAT files)" + + property_str: StringProperty( + description="String representing which property should be copied.", + default="", + options={'HIDDEN'}, + ) + + def execute(self, context): + material = context.active_object.active_material + + if material.scs_props.bl_rna.properties[self.property_str].subtype != "COLOR": + self.report({'ERROR'}, "Can not copy given attribute, it's not a COLOR property!") + return {'CANCELLED'} + + color = getattr(material.scs_props, self.property_str) + lin_color = list(_convert_utils.srgb_to_linear(color)) + + lin_color_str = "" + for col in lin_color: + lin_color_str += "%.6f, " % col + lin_color_str = lin_color_str[:-2] + + context.window_manager.clipboard = lin_color_str + + self.report({'INFO'}, "Color successfully copied as: (%.6f, %.6f, %.6f)" % (color[0], color[1], color[2])) + return {'FINISHED'} + + class SCS_TOOLS_OT_PasteColorFromLinear(bpy.types.Operator): + bl_label = "Paste Color from Linear" + bl_idname = "material.scs_tools_paste_color_from_linear" + bl_description = "Paste cliboard to this color from linear colorspace (can be used for colors in SII and MAT files)" + + property_str: StringProperty( + description="String representing which property should be copied.", + default="", + options={'HIDDEN'}, + ) + + def execute(self, context): + material = context.active_object.active_material + + if material.scs_props.bl_rna.properties[self.property_str].subtype != "COLOR": + self.report({'ERROR'}, "Can not paste to given attribute, it's not a COLOR property!") + return {'CANCELLED'} + + # paste from clipboard: + pasted_color_str = context.window_manager.clipboard + # 1. strip any whitespace ends and any brackets with spaces, + pasted_color_str = pasted_color_str.strip(" ").strip("(){}[]").strip(" ") + # 2. replace mid spaces with commas, + pasted_color_str = pasted_color_str.replace(" ", ",") + # 3. replace double commas with single, + pasted_color_str = pasted_color_str.replace(",,", ",") + + # validate string: 'X,X,X' + if not re.match(r"^\d+(\.\d*)?,\d+(\.\d*)?,\d+(\.\d*)?$", pasted_color_str): + self.report({'ERROR'}, ("Can not parse clipboard string," + "make sure format is 'X X X' or 'X, X, X' or variants enclosed with brackets.")) + return {'CANCELLED'} + + # convert into list of floats + pasted_color = [] + for num_str in pasted_color_str.split(","): + pasted_color.append(float(num_str)) + + # convert to srgb + color = _convert_utils.linear_to_srgb(pasted_color) + + # finally assign + setattr(material.scs_props, self.property_str, color) + + self.report({'INFO'}, "Color successfully pasted as: (%.6f, %.6f, %.6f)" % (color[0], color[1], color[2])) + return {'FINISHED'} + + +classes = ( + Aliasing.SCS_TOOLS_OT_LoadAliasedMaterial, + + Attribute.SCS_TOOLS_OT_CopyColorAsLinear, + Attribute.SCS_TOOLS_OT_PasteColorFromLinear, + + Common.SCS_TOOLS_OT_ReloadMaterials, + Common.SCS_TOOLS_OT_SearchShaderPreset, + Common.SCS_TOOLS_OT_AdaptColorManagement, + Common.SCS_TOOLS_OT_MergeMaterials, + Common.SCS_TOOLS_OT_MaterialItemExtrasMenu, + + CustomMapping.SCS_TOOLS_OT_AddCustomTexCoordMapping, + CustomMapping.SCS_TOOLS_OT_RemoveCustomTexCoordMapping, + + Flavors.SCS_TOOLS_OT_SwitchFlavor, + + LampSwitcher.SCS_TOOLS_OT_SwitchLampmask, + + Looks.SCS_TOOLS_OT_WriteThroughLooks, + + Texture.SCS_TOOLS_OT_SelectTexturePath, + + Tobj.SCS_TOOLS_OT_CreateTOBJ, + Tobj.SCS_TOOLS_ReloadTOBJ, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/addon/io_scs_tools/operators/mesh.py b/addon/io_scs_tools/operators/mesh.py index 793043f..f123437 100644 --- a/addon/io_scs_tools/operators/mesh.py +++ b/addon/io_scs_tools/operators/mesh.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015-2017: SCS Software +# Copyright (C) 2015-2019: SCS Software import bmesh import bpy @@ -36,24 +36,24 @@ class LampTool: Wrapper class for better navigation in file """ - class SetLampMaskUV(bpy.types.Operator): + class SCS_TOOLS_OT_SetLampmaskUV(bpy.types.Operator): bl_label = "Set UV to lamp mask" - bl_idname = "mesh.scs_set_lampmask_uv" + bl_idname = "mesh.scs_tools_set_lampmask_uv" bl_description = "Sets offset for lamp mask UV according to given vehicle side or auxiliary color." - vehicle_side = StringProperty( + vehicle_side: StringProperty( description="", default="", options={'HIDDEN'}, ) - aux_color = StringProperty( + aux_color: StringProperty( description="", default="", options={'HIDDEN'}, ) - traffic_light_color = StringProperty( + traffic_light_color: StringProperty( description="", default="", options={'HIDDEN'}, @@ -141,23 +141,23 @@ class VertexColorTools: Wrapper class for better navigation in file """ - class WrapVertexColors(bpy.types.Operator): + class SCS_TOOLS_OT_WrapVertexColors(bpy.types.Operator): bl_label = "Wrap" - bl_idname = "mesh.scs_wrap_vcol" + bl_idname = "mesh.scs_tools_wrap_vertex_colors" bl_description = "Wraps vertex colors to given interval." bl_options = {'REGISTER', 'UNDO'} - wrap_type = StringProperty( + wrap_type: StringProperty( options={'HIDDEN'}, ) - min = FloatProperty( + min: FloatProperty( name="Min Value", description="New minimal possible value for vertex colors.", default=0.4, max=0.5, min=0.0, ) - max = FloatProperty( + max: FloatProperty( name="Max Value", description="New maximal possible value for vertex colors.", default=0.6, @@ -195,20 +195,21 @@ def execute(self, context): # cache original vertex colors because of update on interval change if loop_i not in self.original_col: color = vcolor_layer.data[loop_i].color - self.original_col[loop_i] = (color[0], color[1], color[2]) + self.original_col[loop_i] = (color[0], color[1], color[2], color[3]) vcolor_layer.data[loop_i].color = ( self.original_col[loop_i][0] * interval + self.min, self.original_col[loop_i][1] * interval + self.min, - self.original_col[loop_i][2] * interval + self.min + self.original_col[loop_i][2] * interval + self.min, + self.original_col[loop_i][3] ) self.report({"INFO"}, "Vertex colors wrapped!") return {'FINISHED'} - class PrintVertexColorsStatistics(bpy.types.Operator): + class SCS_TOOLS_OT_PrintVertexColorsStats(bpy.types.Operator): bl_label = "Get Statistics" - bl_idname = "mesh.scs_get_vcol_stats" + bl_idname = "mesh.scs_tools_print_vertex_colors_stats" bl_description = "Prints out min, max and avarage vertex color for active vertex color layer." bl_options = {'REGISTER', 'UNDO'} @@ -251,9 +252,9 @@ def execute(self, context): return {'FINISHED'} - class AddVertexColorsToActive(bpy.types.Operator): + class SCS_TOOLS_OT_AddVertexColorsToActive(bpy.types.Operator): bl_label = "Add Vertex Colors To Active" - bl_idname = "mesh.scs_add_vcolors_to_active" + bl_idname = "mesh.scs_tools_add_vertex_colors_to_active" bl_description = "Adds missing vertex colors layers to active object." bl_options = {'REGISTER', 'UNDO'} @@ -275,13 +276,13 @@ def execute(self, context): # setting neutral value (0.5) to all colors for vertex_col_data in context.object.data.vertex_colors[curr_lay_name].data: - vertex_col_data.color = (0.5,) * 3 + vertex_col_data.color = (0.5,) * 3 + (1.0,) return {'FINISHED'} - class AddVertexColorsToAll(bpy.types.Operator): + class SCS_TOOLS_OT_AddVertexColorsToAll(bpy.types.Operator): bl_label = "Add Vertex Colors To All" - bl_idname = "mesh.scs_add_vcolors_to_all" + bl_idname = "mesh.scs_tools_add_vertex_colors_to_all" bl_description = "Adds missing vertex colors layers to all objects using this material." bl_options = {'REGISTER', 'UNDO'} @@ -315,18 +316,18 @@ def execute(self, context): # setting neutral value (0.5) to all colors for vertex_col_data in obj.data.vertex_colors[curr_lay_name].data: - vertex_col_data.color = (0.5,) * 3 + vertex_col_data.color = (0.5,) * 3 + (1.0,) return {'FINISHED'} - class VertexColoringEdit(bpy.types.Operator): + class SCS_TOOLS_OT_StartVColoring(bpy.types.Operator): bl_label = "VColoring - Edit" - bl_idname = "mesh.scs_vcoloring_edit" + bl_idname = "mesh.scs_tools_start_vcoloring" bl_description = "Enters complex vertex paint edit mode, where user can edit one of 4 vertex color layers: color, decal, ao, ao2.\n" \ "This layers are baked together by extra overlay functions designed for usage in map assets." bl_options = {'REGISTER', 'INTERNAL', 'UNDO'} - layer_name = StringProperty(default="", description="Name of the layer to edit currently.") + layer_name: StringProperty(default="", description="Name of the layer to edit currently.") __static_active_layer = _VCT_consts.ColoringLayersTypes.Color # by default start editing color """Stores currently active vertex color layer. Used for switching to other modes when user invokes this operator again.""" @@ -363,11 +364,11 @@ def initialize(self, context): wm = context.window_manager - self.__timer = wm.event_timer_add(0.15, context.window) + self.__timer = wm.event_timer_add(0.15, window=context.window) self.__active_object_name = context.active_object.name self.__active_object_mode = context.active_object.mode - VertexColorTools.VertexColoringEdit.__static_is_active = True + VertexColorTools.SCS_TOOLS_OT_StartVColoring.__static_is_active = True wm.modal_handler_add(self) @@ -382,23 +383,23 @@ def initialize(self, context): buffer = None if layer_name == _VCT_consts.ColoringLayersTypes.Color: - buffer = numpy.array([0.5] * (len(mesh.loops) * 3)) + buffer = numpy.array([0.5] * (len(mesh.loops) * 4)) elif layer_name == _VCT_consts.ColoringLayersTypes.Decal: - buffer = numpy.array([1.0] * (len(mesh.loops) * 3)) + buffer = numpy.array([1.0] * (len(mesh.loops) * 4)) elif layer_name == _VCT_consts.ColoringLayersTypes.AO: - buffer = numpy.array([0.5] * (len(mesh.loops) * 3)) + buffer = numpy.array([0.5] * (len(mesh.loops) * 4)) elif layer_name == _VCT_consts.ColoringLayersTypes.AO2: - buffer = numpy.array([0.5] * (len(mesh.loops) * 3)) + buffer = numpy.array([0.5] * (len(mesh.loops) * 4)) if buffer is not None: vcolor.data.foreach_set("color", buffer) # initialize buffers and hash self.__vcolors_buffer_arrays = [ - numpy.array([0.0] * (len(mesh.loops) * 3)), - numpy.array([0.0] * (len(mesh.loops) * 3)), - numpy.array([0.0] * (len(mesh.loops) * 3)), - numpy.array([0.0] * (len(mesh.loops) * 3)) + numpy.array([0.0] * (len(mesh.loops) * 4)), + numpy.array([0.0] * (len(mesh.loops) * 4)), + numpy.array([0.0] * (len(mesh.loops) * 4)), + numpy.array([0.0] * (len(mesh.loops) * 4)) ] self.__old_vcolors_array_hash = None @@ -493,10 +494,10 @@ def execute(self, context): # user requested layer change if self.layer_name != "" and self.layer_name in _VCT_consts.ColoringLayersTypes.as_list(): - VertexColorTools.VertexColoringEdit.__static_active_layer = self.layer_name + VertexColorTools.SCS_TOOLS_OT_StartVColoring.__static_active_layer = self.layer_name # already active abort another one - if VertexColorTools.VertexColoringEdit.__static_is_active: + if VertexColorTools.SCS_TOOLS_OT_StartVColoring.__static_is_active: return {'CANCELLED'} bpy.ops.object.mode_set(mode="VERTEX_PAINT") @@ -538,26 +539,26 @@ def cancel(self, context): self.__old_vcolors_array_hash = None self.__vcolors_buffer_arrays = [] - VertexColorTools.VertexColoringEdit.__static_is_active = False + VertexColorTools.SCS_TOOLS_OT_StartVColoring.__static_is_active = False lprint("D VColoring operator cleanup done, exiting now!") - class VertexColoringExit(bpy.types.Operator): + class SCS_TOOLS_OT_ExitVColoring(bpy.types.Operator): bl_label = "VColoring - Exit" - bl_idname = "mesh.scs_vcoloring_exit" + bl_idname = "mesh.scs_tools_exit_vcoloring" bl_description = "Exits complex vertex paint edit mode." bl_options = {'REGISTER', 'INTERNAL'} @classmethod def poll(cls, context): - return VertexColorTools.VertexColoringEdit.poll(context) + return VertexColorTools.SCS_TOOLS_OT_StartVColoring.poll(context) def execute(self, context): - VertexColorTools.VertexColoringEdit.abort() + VertexColorTools.SCS_TOOLS_OT_StartVColoring.abort() return {'FINISHED'} - class VertexColoringRebake(bpy.types.Operator): + class SCS_TOOLS_OT_RebakeVColoring(bpy.types.Operator): bl_label = "VColoring - Rebake" - bl_idname = "mesh.scs_vcoloring_rebake" + bl_idname = "mesh.scs_tools_rebake_vcoloring" bl_description = "Rebakes 4 vertex color layers (use it if you edited any of 4 extra vertex color layers by hand)." bl_options = {'REGISTER', 'UNDO'} @@ -582,10 +583,10 @@ def execute(self, context): # prepare buffers vcolors_buffer_arrays = [ - numpy.array([0.0] * (len(mesh.loops) * 3)), - numpy.array([0.0] * (len(mesh.loops) * 3)), - numpy.array([0.0] * (len(mesh.loops) * 3)), - numpy.array([0.0] * (len(mesh.loops) * 3)) + numpy.array([0.0] * (len(mesh.loops) * 4)), + numpy.array([0.0] * (len(mesh.loops) * 4)), + numpy.array([0.0] * (len(mesh.loops) * 4)), + numpy.array([0.0] * (len(mesh.loops) * 4)) ] # rebake @@ -605,3 +606,26 @@ def execute(self, context): self.report({'ERROR'}, message[2:]) return {'FINISHED'} + + +classes = ( + LampTool.SCS_TOOLS_OT_SetLampmaskUV, + + VertexColorTools.SCS_TOOLS_OT_AddVertexColorsToActive, + VertexColorTools.SCS_TOOLS_OT_AddVertexColorsToAll, + VertexColorTools.SCS_TOOLS_OT_PrintVertexColorsStats, + VertexColorTools.SCS_TOOLS_OT_StartVColoring, + VertexColorTools.SCS_TOOLS_OT_ExitVColoring, + VertexColorTools.SCS_TOOLS_OT_RebakeVColoring, + VertexColorTools.SCS_TOOLS_OT_WrapVertexColors, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/addon/io_scs_tools/operators/object.py b/addon/io_scs_tools/operators/object.py index b3bd6b2..2ac574a 100755 --- a/addon/io_scs_tools/operators/object.py +++ b/addon/io_scs_tools/operators/object.py @@ -16,13 +16,13 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bmesh import bpy import re -from bpy.props import BoolProperty, StringProperty, IntProperty +from bpy.props import BoolProperty, StringProperty, IntProperty, EnumProperty from io_scs_tools.consts import Look as _LOOK_consts from io_scs_tools.consts import Part as _PART_consts from io_scs_tools.consts import Variant as _VARIANT_consts @@ -31,15 +31,15 @@ from io_scs_tools.internals import inventory as _inventory from io_scs_tools.internals import looks as _looks from io_scs_tools.internals.icons import get_icon as _get_icon -from io_scs_tools.internals.connections.wrappers import group as _connection_group_wrapper +from io_scs_tools.internals.connections.wrappers import collection as _connections_wrapper from io_scs_tools.internals.open_gl.storage import terrain_points as _terrain_points_storage from io_scs_tools.operators.bases.selection import Selection as _BaseSelectionOperator from io_scs_tools.operators.bases.view import View as _BaseViewOperator from io_scs_tools.properties.object import ObjectSCSTools as _ObjectSCSTools from io_scs_tools.utils.printout import lprint from io_scs_tools.utils import convert as _convert_utils -from io_scs_tools.utils import object as _object_utils from io_scs_tools.utils import name as _name_utils +from io_scs_tools.utils import object as _object_utils from io_scs_tools.utils import material as _material_utils from io_scs_tools.utils import view3d as _view3d_utils from io_scs_tools.utils import path as _path_utils @@ -52,66 +52,110 @@ class ConvexCollider: Wrapper class for better navigation in file """ - class MakeConvex(bpy.types.Operator): + class SCS_TOOLS_OT_MakeConvexMesh(bpy.types.Operator): bl_label = "Make Convex From Selection" - bl_idname = "object.make_convex" + bl_idname = "object.scs_tools_make_convex_mesh" bl_description = "Create convex hull from selected objects" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): lprint('D Make Convex Geometry From Selection...') - objects, active_object = _object_utils.pick_objects_from_selection(self, needs_active_obj=True, obj_type='MESH') + objects, active_object = _object_utils.pick_objects_from_selection(self, needs_active_obj=False, obj_type='MESH') if objects: - geoms, convex_props, resulting_convex_object = _object_utils.create_convex_data(objects, create_hull=True) - _object_utils.make_objects_selected((resulting_convex_object,)) + _, _, resulting_convex_object = _object_utils.create_convex_data(objects, return_hull_object=True) + + if not resulting_convex_object: + self.report({'WARNING'}, "Could not create convex mesh, selected object seems to form a flat surface!") + return {'CANCELLED'} + + _object_utils.make_objects_selected((resulting_convex_object,), active_object=resulting_convex_object) return {'FINISHED'} - class ConvertToConvexLocator(bpy.types.Operator): + class SCS_TOOLS_OT_ConvertMeshToConvexLocator(bpy.types.Operator): bl_label = "Convert Meshes to Convex Collider" - bl_idname = "object.convert_meshes_to_convex_locator" + bl_idname = "object.scs_tools_convert_meshes_to_convex_locator" bl_description = "Convert selection to Convex Collision Locator" bl_options = {'REGISTER', 'UNDO'} - delete_mesh_objects = BoolProperty( + show_face_count_only: BoolProperty( + default=False + ) + + desired_face_count: IntProperty( + name="Desired Face Count", + description="Desired number of triangles that convex locator should have (less is better for game engine).", + max=256, + min=6, + step=1, + default=64 + ) + + delete_mesh_objects: BoolProperty( name="Delete Original Geometries", description="Delete all original Mesh Objects", default=False, ) - individual_objects = BoolProperty( + individual_objects: BoolProperty( name="Individual Objects", description="Make convex Locator from every individual Mesh Object in selection", default=False, ) + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + + layout.prop(self, "desired_face_count") + layout.prop(self, "delete_mesh_objects") + layout.prop(self, "individual_objects") + def execute(self, context): # print('Convert Selection to Convex Collision Locator...') - objects, active_object = _object_utils.pick_objects_from_selection(self, needs_active_obj=True, obj_type='MESH') + objects, active_object = _object_utils.pick_objects_from_selection(self, needs_active_obj=False, obj_type='MESH') if objects: if self.individual_objects: new_objects = [] for obj in objects: parent = obj.parent - geoms, convex_props, resulting_convex_object = _object_utils.create_convex_data((obj,), create_hull=False) - locator = _object_utils.create_collider_convex_locator(geoms, convex_props, (obj,), self.delete_mesh_objects) + geom, convex_props, _ = _object_utils.create_convex_data((obj,), max_face_count=self.desired_face_count) + + if not geom: + self.report({'WARNING'}, "Could not create convex mesh for %r, selected object seems to be a flat surface!" % obj.name) + continue + + locator = _object_utils.create_collider_convex_locator(geom, convex_props, (obj,), self.delete_mesh_objects) if locator: locator.parent = parent new_objects.append(locator) if new_objects: _object_utils.make_objects_selected(new_objects) else: - geoms, convex_props, resulting_convex_object = _object_utils.create_convex_data(objects, create_hull=False) + geom, convex_props, _ = _object_utils.create_convex_data(objects, max_face_count=self.desired_face_count) + + if not geom: + self.report({'WARNING'}, "Could not create convex mesh, selected object(s) seems to form a flat surface!") + return {'CANCELLED'} + parent = active_object.parent - locator = _object_utils.create_collider_convex_locator(geoms, convex_props, objects, self.delete_mesh_objects) + paernt_inverse_mat = active_object.matrix_parent_inverse + locator = _object_utils.create_collider_convex_locator(geom, convex_props, objects, self.delete_mesh_objects) if locator: locator.parent = parent # make sure to apply parent inverse matrix so object is oriented as origin object - locator.matrix_parent_inverse = active_object.matrix_parent_inverse + locator.matrix_parent_inverse = paernt_inverse_mat _object_utils.make_objects_selected((locator,)) return {'FINISHED'} - class ConvertFromConvex(bpy.types.Operator): + def invoke(self, context, event): + if self.show_face_count_only: + return context.window_manager.invoke_props_dialog(self) + else: + return self.execute(context) + + class SCS_TOOLS_OT_ConvertConvexLocatorToMesh(bpy.types.Operator): bl_label = "Convert Convex Collider to Mesh" - bl_idname = "object.convert_convex_locator_to_mesh" + bl_idname = "object.scs_tools_convert_convex_locator_to_mesh" bl_description = "Convert Convex Collision Locator to Mesh Object" bl_options = {'REGISTER', 'UNDO'} @@ -135,17 +179,7 @@ def execute(self, context): # else: # self.report({'ERROR_INVALID_CONTEXT'}, "No Collision Convex Locator in selection to operate on!") if new_active_object: - bpy.context.scene.objects.active = new_active_object - return {'FINISHED'} - - class UpdateConvex(bpy.types.Operator): - bl_label = "Update Convex Collider" - bl_idname = "object.update_convex" - bl_description = "Update Convex Collision Locator" - - def execute(self, context): - lprint('D Update Convex Collider Locator...') - _object_utils.update_convex_hull_margins(bpy.context.active_object) + bpy.context.view_layer.objects.active = new_active_object return {'FINISHED'} @@ -154,17 +188,17 @@ class Look: Wrapper class for better navigation in file """ - class AddLookOperator(bpy.types.Operator): + class SCS_TOOLS_OT_AddLook(bpy.types.Operator): """Add SCS Look to actual SCS Game Object.""" bl_label = "Add SCS Look" - bl_idname = "object.add_scs_look" + bl_idname = "object.scs_tools_add_look" bl_description = "Add new SCS Look to SCS Game Object" - look_name = StringProperty( + look_name: StringProperty( default=_LOOK_consts.default_name, description="Name of the new look" ) - instant_apply = BoolProperty( + instant_apply: BoolProperty( default=True, description="Flag indicating if new look should also be applied directly" "(used on import when applying of each look is useless)" @@ -206,10 +240,10 @@ def execute(self, context): return {'FINISHED'} - class RemoveActiveLookOperator(bpy.types.Operator): + class SCS_TOOLS_OT_RemoveActiveLook(bpy.types.Operator): """Remove SCS Look to actual SCS Game Object.""" bl_label = "Remove Active SCS Look" - bl_idname = "object.remove_scs_look" + bl_idname = "object.scs_tools_remove_active_look" bl_description = "Remove active SCS Look from SCS Game Object" @classmethod @@ -251,13 +285,13 @@ class Part: Wrapper class for better navigation in file """ - class SelectObjectsInPart(bpy.types.Operator, _BaseSelectionOperator): + class SCS_TOOLS_OT_DeSelectObjectsWithPart(bpy.types.Operator, _BaseSelectionOperator): """Switch select state for objects of given SCS Part.""" bl_label = "Select/Deselect All Objects In Part" - bl_idname = "object.switch_part_selection" + bl_idname = "object.scs_tools_de_select_objects_with_part" bl_description = "Switch selection for all objects in SCS Part" + _BaseSelectionOperator.bl_base_description - part_index = IntProperty() + part_index: IntProperty() def select_active_parts(self, objects, active_scs_part, outside_game_objects=False): @@ -268,17 +302,22 @@ def select_active_parts(self, objects, active_scs_part, outside_game_objects=Fal if outside_game_objects: if _object_utils.get_scs_root(obj): continue + + if not obj.visible_get(): # ignore invisible objects + continue + if obj.scs_props.scs_part == active_scs_part: # set actual select state if it's not set yet if actual_select_state is None: - actual_select_state = not obj.select + actual_select_state = not obj.select_get() + # deselect all in case any other object types were selected before bpy.ops.object.select_all(action='DESELECT') - obj.select = actual_select_state + _object_utils.select_set(obj, actual_select_state) elif self.select_type not in (self.SHIFT_SELECT, self.CTRL_SELECT): - obj.select = False + _object_utils.select_set(obj, False) return def execute(self, context): @@ -303,13 +342,13 @@ def execute(self, context): return {'FINISHED'} - class ViewObjectsInPart(bpy.types.Operator, _BaseViewOperator): + class SCS_TOOLS_OT_SwitchObjectsWithPart(bpy.types.Operator, _BaseViewOperator): """Switch view state for all objects of given Part.""" bl_label = "View/Hide Objects In Part" - bl_idname = "object.switch_part_visibility" + bl_idname = "object.scs_tools_switch_part_visibility" bl_description = "Switch view state for all objects in SCS Part" + _BaseViewOperator.bl_base_description - part_index = IntProperty() + part_index: IntProperty() def view_active_part(self, objects, active_scs_part, outside_game_objects=False): @@ -325,13 +364,13 @@ def view_active_part(self, objects, active_scs_part, outside_game_objects=False) # set actual hide state if it's not set yet if actual_hide_state is None: - actual_hide_state = not obj.hide + actual_hide_state = not obj.hide_get() - obj.hide = actual_hide_state + _object_utils.hide_set(obj, actual_hide_state) elif self.view_type == self.VIEWONLY: - obj.hide = True + _object_utils.hide_set(obj, True) return @@ -356,10 +395,10 @@ def execute(self, context): return {'FINISHED'} - class AddPartOperator(bpy.types.Operator): + class SCS_TOOLS_OT_AddPart(bpy.types.Operator): """Add SCS Part to actual SCS Game Object.""" bl_label = "Add SCS Part" - bl_idname = "object.add_scs_part" + bl_idname = "object.scs_tools_add_part" bl_description = "Add new SCS Part to SCS Game Object" @classmethod @@ -395,10 +434,10 @@ def execute(self, context): return {'FINISHED'} - class RemoveActivePartOperator(bpy.types.Operator): + class SCS_TOOLS_OT_RemoveActivePart(bpy.types.Operator): """Remove SCS Part to actual SCS Game Object.""" bl_label = "Remove Active SCS Part" - bl_idname = "object.remove_scs_part" + bl_idname = "object.scs_tools_remove_active_part" bl_description = "Remove active SCS Part from SCS Game Object (will be removed if none of the object uses it)" @classmethod @@ -454,10 +493,52 @@ def execute(self, context): return {'FINISHED'} - class CleanPartsOperator(bpy.types.Operator): + class SCS_TOOLS_OT_MoveActivePart(bpy.types.Operator): + """Move SCS Part in SCS Game Object down or up.""" + bl_label = "Move Active SCS Part" + bl_idname = "object.scs_tools_move_active_part" + bl_description = "Move active SCS Part from SCS Game Object up/down for one place" + + move_direction: StringProperty( + name="Move Direction", + default=_OP_consts.InventoryMoveType.move_down, + ) + + @classmethod + def poll(cls, context): + if _object_utils.get_scs_root(context.active_object): + return True + else: + return False + + def execute(self, context): + lprint('D Move SCS Part from SCS Game Object up/down for one place...') + + active_object = context.active_object + scs_root_object = _object_utils.get_scs_root(active_object) + active_scs_part = scs_root_object.scs_props.active_scs_part + part_inventory = scs_root_object.scs_object_part_inventory + + if 0 <= active_scs_part < len(part_inventory): + + if self.move_direction == _OP_consts.InventoryMoveType.move_down: + new_active_idx = _inventory.move_down(part_inventory, active_scs_part) + elif self.move_direction == _OP_consts.InventoryMoveType.move_up: + new_active_idx = _inventory.move_up(part_inventory, active_scs_part) + else: + self.report({'ERROR'}, "Invalid move direction! Aborting part moving!") + return {'CANCELLED'} + + scs_root_object.scs_props.active_scs_part = new_active_idx + else: + lprint("W No active 'SCS Part' to move!") + + return {'FINISHED'} + + class SCS_TOOLS_OT_CleanUnusedParts(bpy.types.Operator): """Removes all unused SCS Parts from SCS Game Object.""" bl_label = "Clean SCS Parts" - bl_idname = "object.clean_scs_parts" + bl_idname = "object.scs_tools_clean_unused_parts" bl_description = "Removes all unused SCS Parts from SCS Game Object" @classmethod @@ -513,10 +594,10 @@ def execute(self, context): return {'FINISHED'} - class AssignPartOperator(bpy.types.Operator): + class SCS_TOOLS_OT_AssignPart(bpy.types.Operator): """Assign active SCS Part to selected objects.""" bl_label = "Assign SCS Part" - bl_idname = "object.assign_scs_part" + bl_idname = "object.scs_tools_assign_part" bl_description = "Assign active SCS Part to selected objects" @classmethod @@ -561,10 +642,10 @@ def execute(self, context): return {"FINISHED"} - class PrintPartsOperator(bpy.types.Operator): + class SCS_TOOLS_OT_PrintParts(bpy.types.Operator): """Print SCS Parts to actual SCS Game Object.""" bl_label = "Print SCS Part" - bl_idname = "object.print_scs_parts" + bl_idname = "object.scs_tools_print_parts" bl_description = "Print SCS Parts to actual SCS Game Object" @classmethod @@ -586,13 +667,13 @@ class Variant: Wrapper class for better navigation in file """ - class SelectObjectsInVariant(bpy.types.Operator, _BaseSelectionOperator): + class SCS_TOOLS_OT_DeSelectObjectsWithVariant(bpy.types.Operator, _BaseSelectionOperator): """Switch selection for all objects in Variant.""" bl_label = "Select/Deselect All Objects In Variant" - bl_idname = "object.switch_variant_selection" + bl_idname = "object.scs_tools_de_select_objects_with_variant" bl_description = "Switch selection for all objects in SCS Variant" + _BaseSelectionOperator.bl_base_description - variant_index = IntProperty() + variant_index: IntProperty() def execute(self, context): lprint("D " + self.bl_label + "...") @@ -606,30 +687,35 @@ def execute(self, context): children = _object_utils.get_children(scs_root_object) for obj in children: + + if not obj.visible_get(): # ignore invisible objects + continue + if obj.scs_props.scs_part in parts: # set actual select state if it's not set yet if actual_select_state is None: - actual_select_state = not obj.select + actual_select_state = not obj.select_get() + # deselect all in case any object from other variants or outside root were selected bpy.ops.object.select_all(action='DESELECT') - obj.select = actual_select_state + _object_utils.select_set(obj, actual_select_state) elif self.select_type not in (self.SHIFT_SELECT, self.CTRL_SELECT): - obj.select = False + _object_utils.select_set(obj, False) else: lprint("W Given index for 'SCS Variant' is out of bounds: %i", (self.variant_index,)) return {'FINISHED'} - class ViewObjectsInVariant(bpy.types.Operator, _BaseViewOperator): + class SCS_TOOLS_OT_SwitchObjectsWithVariant(bpy.types.Operator, _BaseViewOperator): """Switch view state for all objects of given Variant.""" bl_label = "View/Hide Objects In Variant" - bl_idname = "object.switch_variant_visibility" + bl_idname = "object.scs_tools_switch_objects_with_variant" bl_description = "Switch view state for all objects in SCS Variant" + _BaseViewOperator.bl_base_description - variant_index = IntProperty() + variant_index: IntProperty() def execute(self, context): lprint("D " + self.bl_label + "...") @@ -647,21 +733,21 @@ def execute(self, context): if obj.scs_props.scs_part in parts: if actual_hide_state is None: - actual_hide_state = not obj.hide + actual_hide_state = not obj.hide_get() - obj.hide = actual_hide_state + _object_utils.hide_set(obj, actual_hide_state) elif self.view_type == self.VIEWONLY: - obj.hide = True + _object_utils.hide_set(obj, True) else: lprint("W Given index for 'SCS Variant' is out of bounds: %i", (self.variant_index,)) return {'FINISHED'} - class AddVariantOperator(bpy.types.Operator): + class SCS_TOOLS_OT_AddVariant(bpy.types.Operator): """Add SCS Variant to actual SCS Game Object.""" bl_label = "Add SCS Variant" - bl_idname = "object.add_scs_variant" + bl_idname = "object.scs_tools_add_variant" bl_description = "Add new SCS Variant to SCS Game Object" @classmethod @@ -694,10 +780,10 @@ def execute(self, context): return {'FINISHED'} - class RemoveActiveVariantOperator(bpy.types.Operator): + class SCS_TOOLS_OT_RemoveActiveVariant(bpy.types.Operator): """Remove SCS Variant to actual SCS Game Object.""" bl_label = "Remove Active SCS Variant" - bl_idname = "object.remove_scs_variant" + bl_idname = "object.scs_tools_remove_active_variant" bl_description = "Remove active SCS Variant from SCS Game Object" @classmethod @@ -728,10 +814,52 @@ def execute(self, context): return {'FINISHED'} - class PrintVariantsOperator(bpy.types.Operator): + class SCS_TOOLS_OT_MoveActiveVariant(bpy.types.Operator): + """Move SCS Variant in SCS Game Object down or up.""" + bl_label = "Move Active SCS Variant" + bl_idname = "object.scs_tools_move_active_variant" + bl_description = "Move active SCS Variant from SCS Game Object up/down for one place" + + move_direction: StringProperty( + name="Move Direction", + default=_OP_consts.InventoryMoveType.move_down, + ) + + @classmethod + def poll(cls, context): + if _object_utils.get_scs_root(context.active_object): + return True + else: + return False + + def execute(self, context): + lprint('D Move SCS Variant from SCS Game Object up/down for one place...') + + active_object = context.active_object + scs_root_object = _object_utils.get_scs_root(active_object) + active_scs_variant = scs_root_object.scs_props.active_scs_variant + variant_inventory = scs_root_object.scs_object_variant_inventory + + if 0 <= active_scs_variant < len(variant_inventory): + + if self.move_direction == _OP_consts.InventoryMoveType.move_down: + new_active_idx = _inventory.move_down(variant_inventory, active_scs_variant) + elif self.move_direction == _OP_consts.InventoryMoveType.move_up: + new_active_idx = _inventory.move_up(variant_inventory, active_scs_variant) + else: + self.report({'ERROR'}, "Invalid move direction! Aborting variant moving!") + return {'CANCELLED'} + + scs_root_object.scs_props.active_scs_variant = new_active_idx + else: + lprint("W No active 'SCS Variant' to move!") + + return {'FINISHED'} + + class SCS_TOOLS_OT_PrintVariants(bpy.types.Operator): """Print SCS Variants to actual SCS Game Object.""" bl_label = "Print SCS Variant" - bl_idname = "object.print_scs_variants" + bl_idname = "object.scs_tools_print_variants" bl_description = "Print SCS Variants to actual SCS Game Object" @classmethod @@ -761,10 +889,10 @@ class ModelObjects: Wrapper class for better navigation in file """ - class SelectModelObjects(bpy.types.Operator): + class SCS_TOOLS_OT_SelectModelObjects(bpy.types.Operator): """Selects all model objects.""" bl_label = "Select Model Objects" - bl_idname = "object.select_model_objects" + bl_idname = "object.scs_tools_select_model_objects" bl_description = "Select model objects" def execute(self, context): @@ -772,15 +900,15 @@ def execute(self, context): for obj in context.scene.objects: has_shadow, has_glass, has_static_collision, is_other = _material_utils.get_material_info(obj) if is_other: - obj.select = True + _object_utils.select_set(obj, True) else: - obj.select = False + _object_utils.select_set(obj, False) return {'FINISHED'} - class ViewModelObjects(bpy.types.Operator, _BaseViewOperator): + class SCS_TOOLS_OT_SwitchModelObjects(bpy.types.Operator, _BaseViewOperator): """Switch visibility of model objects.""" - bl_label = "Switch Visibility of Model Objects" - bl_idname = "object.switch_model_objects_visibility" + bl_label = "View Model Objects" + bl_idname = "object.scs_tools_switch_model_objects" bl_description = "View only model objects" + _BaseViewOperator.bl_base_description def execute(self, context): @@ -795,20 +923,20 @@ def execute(self, context): # set actual hide state if it's not set yet if actual_hide_state is None: - actual_hide_state = not obj.hide + actual_hide_state = not obj.hide_get() - obj.hide = actual_hide_state + _object_utils.hide_set(obj, actual_hide_state) elif self.view_type == self.VIEWONLY: - obj.hide = True + _object_utils.hide_set(obj, True) return {'FINISHED'} - class SearchDegeneratedPolys(bpy.types.Operator): + class SCS_TOOLS_OT_SearchDegeneratedPolys(bpy.types.Operator): """Switch visibility of model objects.""" bl_label = "SCS Geometry Check" - bl_idname = "object.scs_geometry_check" + bl_idname = "object.scs_tools_search_degenerated_polys" bl_description = "Searches selected objects for degenerated polygons which should be removed before exporting for game!" EPSILON = 0.001 * 0.01 @@ -885,7 +1013,7 @@ def execute(self, context): e.select = False for i, v in enumerate(obj.data.vertices): - v.select = i in degen_verts + v.select = (i in degen_verts) if len(degen_verts) > 0: degen_objs.append(obj.name) @@ -909,10 +1037,10 @@ class ShadowCasters: Wrapper class for better navigation in file """ - class SelectShadowCasters(bpy.types.Operator): + class SCS_TOOLS_OT_SelectShadowCasters(bpy.types.Operator): """Selects all shadow casters.""" bl_label = "Select Shadow Casters" - bl_idname = "object.select_shadow_casters" + bl_idname = "object.scs_tools_select_shadow_casters" bl_description = "Select shadow casters" def execute(self, context): @@ -920,15 +1048,15 @@ def execute(self, context): for obj in context.scene.objects: has_shadow, has_glass, has_static_collision, is_other = _material_utils.get_material_info(obj) if has_shadow: - obj.select = True + _object_utils.select_set(obj, True) else: - obj.select = False + _object_utils.select_set(obj, False) return {'FINISHED'} - class ViewShadowCasters(bpy.types.Operator, _BaseViewOperator): + class SCS_TOOL_OT_SwitchShadowCasters(bpy.types.Operator, _BaseViewOperator): """Switch visibility of shadow casters only.""" - bl_label = "Switch Visibility of Shadow Casters" - bl_idname = "object.switch_shadow_casters_visibility" + bl_label = "View Shadow Casters" + bl_idname = "object.scs_tools_switch_shadow_casters" bl_description = "View only Shadow Casters" + _BaseViewOperator.bl_base_description def execute(self, context): @@ -943,24 +1071,24 @@ def execute(self, context): # set actual hide state if it's not set yet if actual_hide_state is None: - actual_hide_state = not obj.hide + actual_hide_state = not obj.hide_get() # when hiding ignore flavored shadow casters if actual_hide_state is True and has_shadow and is_other: continue - obj.hide = actual_hide_state + _object_utils.hide_set(obj, actual_hide_state) elif self.view_type == self.VIEWONLY: - obj.hide = True + _object_utils.hide_set(obj, True) return {'FINISHED'} - class ShadowCasterObjectsInWireframes(bpy.types.Operator): + class SCS_TOOLS_OT_ShowShadowCastersAsWire(bpy.types.Operator): """Shadow Caster objects in wireframes.""" bl_label = "Shadow Caster Objects In Wireframes" - bl_idname = "object.shadow_caster_objects_in_wireframes" + bl_idname = "object.scs_tools_show_shadow_casters_as_wire" bl_description = "Display all shadow caster objects as wireframes" def execute(self, context): @@ -968,14 +1096,14 @@ def execute(self, context): for obj in context.scene.objects: has_shadow, has_glass, has_static_collision, is_other = _material_utils.get_material_info(obj) if has_shadow: - obj.draw_type = 'WIRE' + obj.display_type = 'WIRE' obj.show_all_edges = True return {'FINISHED'} - class ShadowCasterObjectsTextured(bpy.types.Operator): + class SCS_TOOLS_OT_ShowShadowCasterAsTextured(bpy.types.Operator): """Shadow Caster objects textured.""" bl_label = "Shadow Caster Objects Textured" - bl_idname = "object.shadow_caster_objects_textured" + bl_idname = "object.scs_tools_show_shadow_casters_as_textured" bl_description = "Display all shadow caster objects textured" def execute(self, context): @@ -983,7 +1111,7 @@ def execute(self, context): for obj in context.scene.objects: has_shadow, has_glass, has_static_collision, is_other = _material_utils.get_material_info(obj) if has_shadow: - obj.draw_type = 'TEXTURED' + obj.display_type = 'TEXTURED' return {'FINISHED'} @@ -992,10 +1120,10 @@ class Glass: Wrapper class for better navigation in file """ - class SelectGlassObjects(bpy.types.Operator): + class SCS_TOOLS_OT_SelectGlassObjects(bpy.types.Operator): """Selects all glass objects.""" bl_label = "Select Glass Objects" - bl_idname = "object.select_glass_objects" + bl_idname = "object.scs_tools_select_glass_objects" bl_description = "Select glass objects" def execute(self, context): @@ -1003,15 +1131,15 @@ def execute(self, context): for obj in context.scene.objects: has_shadow, has_glass, has_static_collision, is_other = _material_utils.get_material_info(obj) if has_glass: - obj.select = True + _object_utils.select_set(obj, True) else: - obj.select = False + _object_utils.select_set(obj, False) return {'FINISHED'} - class ViewGlassObjects(bpy.types.Operator, _BaseViewOperator): + class SCS_TOOLS_OT_SwitchGlassObjects(bpy.types.Operator, _BaseViewOperator): """Switch visibility of glass objects.""" - bl_label = "Switch Visibility of Glass Objects" - bl_idname = "object.switch_glass_objects_visibility" + bl_label = "View Glass Objects" + bl_idname = "object.scs_tools_switch_glass_objects" bl_description = "View only glass objects" + _BaseViewOperator.bl_base_description def execute(self, context): @@ -1026,20 +1154,20 @@ def execute(self, context): # set actual hide state if it's not set yet if actual_hide_state is None: - actual_hide_state = not obj.hide + actual_hide_state = not obj.hide_get() - obj.hide = actual_hide_state + _object_utils.hide_set(obj, actual_hide_state) elif self.view_type == self.VIEWONLY: - obj.hide = True + _object_utils.hide_set(obj, True) return {'FINISHED'} - class GlassObjectsInWireframes(bpy.types.Operator): + class SCS_TOOLS_OT_ShowGlassObjectsAsWire(bpy.types.Operator): """Glass objects in wireframes.""" bl_label = "Glass Objects In Wireframes" - bl_idname = "object.glass_objects_in_wireframes" + bl_idname = "object.scs_tools_show_glass_objects_as_wire" bl_description = "Display all glass objects as wireframes" def execute(self, context): @@ -1047,14 +1175,14 @@ def execute(self, context): for obj in context.scene.objects: has_shadow, has_glass, has_static_collision, is_other = _material_utils.get_material_info(obj) if has_glass: - obj.draw_type = 'WIRE' + obj.display_type = 'WIRE' obj.show_all_edges = True return {'FINISHED'} - class GlassObjectsTextured(bpy.types.Operator): + class SCS_TOOLS_OT_ShowGlassObjectsAsTextured(bpy.types.Operator): """Glass objects textured.""" bl_label = "Glass Objects Textured" - bl_idname = "object.glass_objects_textured" + bl_idname = "object.scs_tools_show_glass_objects_as_textured" bl_description = "Display all glass objects textured" def execute(self, context): @@ -1062,7 +1190,7 @@ def execute(self, context): for obj in context.scene.objects: has_shadow, has_glass, has_static_collision, is_other = _material_utils.get_material_info(obj) if has_glass: - obj.draw_type = 'TEXTURED' + obj.display_type = 'TEXTURED' return {'FINISHED'} @@ -1071,26 +1199,26 @@ class Collision: Wrapper class for better navigation in file """ - class SelectCollisionObjects(bpy.types.Operator): - """Selects all glass objects.""" - bl_label = "Select static collision objects" - bl_idname = "object.select_substance_objects" + class SCS_TOOLS_OT_SelectStaticCollisionObjects(bpy.types.Operator): + """Selects all static collision objects.""" + bl_label = "Select Static Collision Objects" + bl_idname = "object.scs_tools_select_static_collision_objects" bl_description = "Select objects with material using physics substance" def execute(self, context): - lprint('D Select Glass Objects...') + lprint('D Select Static Collision Objects...') for obj in context.scene.objects: has_shadow, has_glass, has_static_collision, is_other = _material_utils.get_material_info(obj) if has_static_collision: - obj.select = True + _object_utils.select_set(obj, True) else: - obj.select = False + _object_utils.select_set(obj, False) return {'FINISHED'} - class ViewCollisionObjects(bpy.types.Operator, _BaseViewOperator): - """Switch visibility of glass objects.""" - bl_label = "Switch Visibility of static collision objects" - bl_idname = "object.switch_substance_objects_visibility" + class SCS_TOOLS_OT_SwitchStaticCollisionObjects(bpy.types.Operator, _BaseViewOperator): + """Switch visibility of static collision objects.""" + bl_label = "View Static Collision Objects" + bl_idname = "object.scs_tools_switch_static_collision_objects" bl_description = "View only objects which SCS Part is prefixed with 'coll', marking it as static collision object" + \ _BaseViewOperator.bl_base_description @@ -1106,13 +1234,13 @@ def execute(self, context): # set actual hide state if it's not set yet if actual_hide_state is None: - actual_hide_state = not obj.hide + actual_hide_state = not obj.hide_get() - obj.hide = actual_hide_state + _object_utils.hide_set(obj, actual_hide_state) elif self.view_type == self.VIEWONLY: - obj.hide = True + _object_utils.hide_set(obj, True) return {'FINISHED'} @@ -1127,27 +1255,27 @@ class Model: Wrapper class for better navigation in file """ - class SelectModelLocators(bpy.types.Operator): + class SCS_TOOLS_OT_SelectModelLocators(bpy.types.Operator): """Selects all model locators.""" bl_label = "Select Model Locators" - bl_idname = "object.select_model_locators" + bl_idname = "object.scs_tools_select_model_locators" bl_description = "Select model locators" def execute(self, context): lprint('D Select Model Locators...') for obj in context.scene.objects: if obj.scs_props.locator_type == 'Model': - obj.select = True + _object_utils.select_set(obj, True) else: - obj.select = False + _object_utils.select_set(obj, False) return {'FINISHED'} - class SelectLocatorsWithSameHookup(bpy.types.Operator): + class SCS_TOOLS_OT_SelectModelLocatorsWithSameHookup(bpy.types.Operator): bl_label = "Select Locators With Same Hookup" - bl_idname = "object.select_model_locators_with_same_hookup" + bl_idname = "object.scs_tools_select_model_locators_with_same_hookup" bl_description = "Select all visible model locators with the same hookup value as this one." - source_object = StringProperty( + source_object: StringProperty( default="" ) @@ -1165,16 +1293,16 @@ def execute(self, context): for obj in context.visible_objects: if obj.scs_props.locator_type == 'Model' and obj.scs_props.locator_model_hookup == src_obj.scs_props.locator_model_hookup: - obj.select = True + _object_utils.select_set(obj, True) else: - obj.select = False + _object_utils.select_set(obj, False) return {"FINISHED"} - class ViewModelLocators(bpy.types.Operator, _BaseViewOperator): + class SCS_TOOLS_OT_SwitchModelLocators(bpy.types.Operator, _BaseViewOperator): """Switch visibility of model locators.""" - bl_label = "Switch Visibility of Model Locators" - bl_idname = "object.switch_model_locators_visibility" + bl_label = "View Model Locators" + bl_idname = "object.scs_tools_switch_model_locators" bl_description = "View only model locators" + _BaseViewOperator.bl_base_description def execute(self, context): @@ -1186,9 +1314,9 @@ def execute(self, context): hide_state=actual_hide_state, view_only=self.view_type == self.VIEWONLY) return {'FINISHED'} - class FixHookups(bpy.types.Operator): + class SCS_TOOLS_OT_FixModelLocatorHookups(bpy.types.Operator): bl_label = "Fix SCS Hookup Names on Model Locators" - bl_idname = "object.scs_fix_model_locator_hookups" + bl_idname = "object.scs_tools_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'} @@ -1237,10 +1365,10 @@ class ControlNodes: Wrapper class for better navigation in file """ - class SelectPrefabNodeLocators(bpy.types.Operator): + class SCS_TOOLS_OT_SelectPrefabNodeLocators(bpy.types.Operator): """Selects all prefab control node locators.""" bl_label = "Select Prefab Control Node Locators" - bl_idname = "object.select_prefab_nodes" + bl_idname = "object.scs_tools_select_prefab_node_locators" bl_description = "Select prefab control node locators" def execute(self, context): @@ -1248,17 +1376,17 @@ def execute(self, context): for obj in context.scene.objects: if obj.scs_props.locator_type == 'Prefab': if obj.scs_props.locator_prefab_type == 'Control Node': - obj.select = True + _object_utils.select_set(obj, True) else: - obj.select = False + _object_utils.select_set(obj, False) else: - obj.select = False + _object_utils.select_set(obj, False) return {'FINISHED'} - class ViewPrefabNodeLocators(bpy.types.Operator, _BaseViewOperator): + class SCS_TOOLS_OT_SwitchPrefabNodeLocators(bpy.types.Operator, _BaseViewOperator): """Switch visibility of prefab control node locators.""" - bl_label = "Switch Visibility of Prefab Control Node Locators" - bl_idname = "object.switch_prefab_nodes_visibility" + bl_label = "View Prefab Control Node Locators" + bl_idname = "object.scs_tools_switch_prefab_node_locators" bl_description = "View only prefab control node locators" + _BaseViewOperator.bl_base_description def execute(self, context): @@ -1270,14 +1398,14 @@ def execute(self, context): hide_state=actual_hide_state, view_only=self.view_type == self.VIEWONLY) return {'FINISHED'} - class AssignTerrainPoints(bpy.types.Operator): + class SCS_TOOLS_OT_AssignTerrainPoints(bpy.types.Operator): bl_label = "Assign Terrain Points" - bl_idname = "object.assign_terrain_points" + bl_idname = "object.scs_tools_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() + vg_name: StringProperty() """Name of the vertex group for terrain points. It consists of vertex group prefix and node index.""" @classmethod @@ -1291,7 +1419,7 @@ def execute(self, context): # ensure vertex group for current node if self.vg_name not in active_obj.vertex_groups: - active_obj.vertex_groups.new(self.vg_name) + active_obj.vertex_groups.new(name=self.vg_name) vg_instance = active_obj.vertex_groups[self.vg_name] @@ -1331,9 +1459,9 @@ def invoke(self, context, event): else: return wm.invoke_confirm(self, event) # ask user for overwrite - class ClearTerrainPointsOperator(bpy.types.Operator): + class SCS_TOOLS_OT_ClearTerrainPoints(bpy.types.Operator): bl_label = "Clear All Terrain Points" - bl_idname = "object.clear_all_terrain_points" + bl_idname = "object.scs_tools_clear_terrain_points" bl_description = "Clears all terrain points for currently selected prefab Control Node" bl_options = {'REGISTER', 'UNDO'} @@ -1353,8 +1481,8 @@ def execute(self, context): lprint("D " + self.bl_label + "...") # make sure to abort possible preview operator - if bpy.ops.object.abort_preview_terrain_points.poll(): - bpy.ops.object.abort_preview_terrain_points() + if bpy.ops.object.scs_tools_abort_terrain_points_preview.poll(): + bpy.ops.object.scs_tools_abort_terrain_points_preview() # if user is in assigning terrain points mode then node locator is other object if context.active_object.mode == "EDIT": @@ -1393,12 +1521,12 @@ def execute(self, context): return {'FINISHED'} - class PreviewTerrainPoints(bpy.types.Operator): + class SCS_TOOLS_OT_PreviewTerrainPoints(bpy.types.Operator): bl_label = "Preview Terrain Points" - bl_idname = "object.preview_terrain_points" + bl_idname = "object.scs_tools_preview_terrain_points" bl_description = "Preview terrain points for currently selected prefab Control Node " - preview_all = BoolProperty( + preview_all: BoolProperty( name="PreviewAll", description="If False only terrain points from visible meshes are shown. Otherwise all terrain points are shown.", default=False @@ -1426,7 +1554,7 @@ def execute_internal(self, context): for sibling in _object_utils.get_siblings(node_loc_obj): # ignore none mesh siblings or hidden siblings if previewing only visible - if sibling.type != "MESH" or (not self.preview_all and (sibling.hide or sibling not in context.visible_objects)): + if sibling.type != "MESH" or (not self.preview_all and (sibling.hide_get() or sibling not in context.visible_objects)): continue for vertex_group in sibling.vertex_groups: @@ -1458,9 +1586,10 @@ def execute_internal(self, context): co = v.co # finally add terrain point if it has correct vertex group - _terrain_points_storage.add(sibling.matrix_world * co, not sibling.hide) + _terrain_points_storage.add(sibling.matrix_world @ co, not sibling.hide_get()) - # force view refresh + # tag active object updated & force view refresh + context.active_object.update_tag(refresh={'OBJECT'}) _view3d_utils.tag_redraw_all_view3d_and_props() # cache active object matrix for possible terrain points desync check @@ -1485,7 +1614,7 @@ def poll(cls, context): def modal(self, context, event): - is_aborted = Locators.Prefab.ControlNodes.PreviewTerrainPoints.abort + is_aborted = Locators.Prefab.ControlNodes.SCS_TOOLS_OT_PreviewTerrainPoints.abort active_object_changed = self.__active_object != context.active_object is_object_mode_changed = self.__active_object_mode != context.active_object.mode is_transformed = self.__active_object_matrix and self.__active_object_matrix != str(context.active_object.matrix_world) @@ -1519,13 +1648,13 @@ def execute(self, context): # set event timer which will take care of aborting wm = context.window_manager - self.__timer = wm.event_timer_add(0.25, context.window) + self.__timer = wm.event_timer_add(time_step=0.25, window=context.window) self.__active_object = context.active_object self.__active_object_mode = context.active_object.mode self.__active_object_matrix = str(context.active_object.matrix_world) - Locators.Prefab.ControlNodes.PreviewTerrainPoints.is_active = True - Locators.Prefab.ControlNodes.PreviewTerrainPoints.abort = False + Locators.Prefab.ControlNodes.SCS_TOOLS_OT_PreviewTerrainPoints.is_active = True + Locators.Prefab.ControlNodes.SCS_TOOLS_OT_PreviewTerrainPoints.abort = False # use modal execution to abort operator if it gets aborted somehow wm.modal_handler_add(self) @@ -1533,8 +1662,11 @@ def execute(self, context): def cancel(self, context): - # clear terrain points storage and force redraw + # clear terrain points storage _terrain_points_storage.clear() + + # tag active object updated & force view refresh + self.__active_object.update_tag(refresh={'OBJECT'}) _view3d_utils.tag_redraw_all_view3d_and_props() wm = context.window_manager @@ -1544,19 +1676,19 @@ def cancel(self, context): self.__active_object = None self.__active_object_matrix = None - Locators.Prefab.ControlNodes.PreviewTerrainPoints.is_active = False + Locators.Prefab.ControlNodes.SCS_TOOLS_OT_PreviewTerrainPoints.is_active = False - class AbortPreviewTerrainPointsOperator(bpy.types.Operator): + class SCS_TOOLS_OT_AbortTerrainPointsPreview(bpy.types.Operator): bl_label = "Abort Preview Terrain Points" - bl_idname = "object.abort_preview_terrain_points" + bl_idname = "object.scs_tools_abort_terrain_points_preview" bl_description = "Abort preview of terrain points for currently selected prefab Control Node " @classmethod def poll(cls, context): - return Locators.Prefab.ControlNodes.PreviewTerrainPoints.is_active + return Locators.Prefab.ControlNodes.SCS_TOOLS_OT_PreviewTerrainPoints.is_active def execute(self, context): - Locators.Prefab.ControlNodes.PreviewTerrainPoints.abort = True + Locators.Prefab.ControlNodes.SCS_TOOLS_OT_PreviewTerrainPoints.abort = True return {'FINISHED'} class Signs: @@ -1564,10 +1696,10 @@ class Signs: Wrapper class for better navigation in file """ - class SelectPrefabSignLocators(bpy.types.Operator): + class SCS_TOOLS_OT_SelectPrefabSignLocators(bpy.types.Operator): """Selects all prefab sign locators.""" bl_label = "Select Prefab Sign Locators" - bl_idname = "object.select_prefab_signs" + bl_idname = "object.scs_tools_select_prefab_sign_locators" bl_description = "Select prefab sign locators" def execute(self, context): @@ -1575,17 +1707,17 @@ def execute(self, context): for obj in context.scene.objects: if obj.scs_props.locator_type == 'Prefab': if obj.scs_props.locator_prefab_type == 'Sign': - obj.select = True + _object_utils.select_set(obj, True) else: - obj.select = False + _object_utils.select_set(obj, False) else: - obj.select = False + _object_utils.select_set(obj, False) return {'FINISHED'} - class ViewPrefabSignLocators(bpy.types.Operator, _BaseViewOperator): + class SCS_TOOLS_OT_SwitchPrefabSignLocators(bpy.types.Operator, _BaseViewOperator): """Switch visibility of prefab sign locators.""" - bl_label = "Switch Visibility of Prefab Sign Locators" - bl_idname = "object.switch_prefab_signs_visibility" + bl_label = "View Prefab Sign Locators" + bl_idname = "object.scs_tools_switch_prefab_sign_locators" bl_description = "View only prefab sign locators" + _BaseViewOperator.bl_base_description def execute(self, context): @@ -1602,10 +1734,10 @@ class SpawnPoints: Wrapper class for better navigation in file """ - class SelectPrefabSpawnLocators(bpy.types.Operator): + class SCS_TOOLS_OT_SelectPrefabSpawnLocators(bpy.types.Operator): """Selects all prefab spawn locators.""" bl_label = "Select Prefab Spawn Locators" - bl_idname = "object.select_prefab_spawns" + bl_idname = "object.scs_tools_select_prefab_spawn_locators" bl_description = "Select prefab spawn locators" def execute(self, context): @@ -1613,17 +1745,17 @@ def execute(self, context): for obj in context.scene.objects: if obj.scs_props.locator_type == 'Prefab': if obj.scs_props.locator_prefab_type == 'Spawn Point': - obj.select = True + _object_utils.select_set(obj, True) else: - obj.select = False + _object_utils.select_set(obj, False) else: - obj.select = False + _object_utils.select_set(obj, False) return {'FINISHED'} - class ViewPrefabSpawnLocators(bpy.types.Operator, _BaseViewOperator): + class SCS_TOOLS_OT_SwitchPrefabSpawnLocators(bpy.types.Operator, _BaseViewOperator): """Switch visiblity of prefab spawn locators.""" - bl_label = "Switch Visibility of Prefab Spawn Locators" - bl_idname = "object.switch_prefab_spawns_visibility" + bl_label = "View Prefab Spawn Locators" + bl_idname = "object.scs_tools_switch_prefab_spawn_locators" bl_description = "View only prefab spawn locators" + _BaseViewOperator.bl_base_description def execute(self, context): @@ -1640,10 +1772,10 @@ class TrafficLights: Wrapper class for better navigation in file """ - class SelectPrefabTrafficLocators(bpy.types.Operator): + class SCS_TOOLS_OT_SelectPrefabTrafficLocators(bpy.types.Operator): """Selects all prefab traffic locators.""" bl_label = "Select Prefab Traffic Locators" - bl_idname = "object.select_prefab_traffics" + bl_idname = "object.scs_tools_select_prefab_traffic_locators" bl_description = "Select prefab traffic locators" def execute(self, context): @@ -1651,17 +1783,17 @@ def execute(self, context): for obj in context.scene.objects: if obj.scs_props.locator_type == 'Prefab': if obj.scs_props.locator_prefab_type == 'Traffic Semaphore': - obj.select = True + _object_utils.select_set(obj, True) else: - obj.select = False + _object_utils.select_set(obj, False) else: - obj.select = False + _object_utils.select_set(obj, False) return {'FINISHED'} - class ViewPrefabTrafficLocators(bpy.types.Operator, _BaseViewOperator): + class SCS_TOOLS_OT_SwitchPrefabTrafficLocators(bpy.types.Operator, _BaseViewOperator): """Switch visibility of prefab traffic locators.""" - bl_label = "Switch Visibility of Prefab Traffic Locators" - bl_idname = "object.switch_prefab_traffics_visibility" + bl_label = "View Prefab Traffic Locators" + bl_idname = "object.scs_tools_switch_prefab_traffic_locators" bl_description = "View only prefab traffic locators" + _BaseViewOperator.bl_base_description def execute(self, context): @@ -1678,10 +1810,10 @@ class NavigationPoints: Wrapper class for better navigation in file """ - class SelectPrefabNavigationLocators(bpy.types.Operator): + class SCS_TOOLS_OT_SelectPrefabNavLocators(bpy.types.Operator): """Selects all prefab navigation locators.""" bl_label = "Select Prefab Navigation Locators" - bl_idname = "object.select_prefab_navigations" + bl_idname = "object.scs_tools_select_prefab_nav_locators" bl_description = "Select prefab navigation locators" def execute(self, context): @@ -1689,17 +1821,17 @@ def execute(self, context): for obj in context.scene.objects: if obj.scs_props.locator_type == 'Prefab': if obj.scs_props.locator_prefab_type == 'Navigation Point': - obj.select = True + _object_utils.select_set(obj, True) else: - obj.select = False + _object_utils.select_set(obj, False) else: - obj.select = False + _object_utils.select_set(obj, False) return {'FINISHED'} - class ViewPrefabNavigationLocators(bpy.types.Operator, _BaseViewOperator): + class SCS_TOOLS_OT_SwitchPrefabNavLocators(bpy.types.Operator, _BaseViewOperator): """Switch visibility of prefab navigation locators.""" - bl_label = "Switch Visibility of Prefab Navigation Locators" - bl_idname = "object.switch_prefab_navigations_visibility" + bl_label = "View Prefab Navigation Locators" + bl_idname = "object.scs_tools_switch_prefab_nav_locators" bl_description = "View only prefab navigation locators" + _BaseViewOperator.bl_base_description def execute(self, context): @@ -1716,10 +1848,10 @@ class MapPoints: Wrapper class for better navigation in file """ - class SelectPrefabMapLocators(bpy.types.Operator): + class SCS_TOOLS_OT_SelectPrefabMapLocators(bpy.types.Operator): """Selects all prefab map locators.""" bl_label = "Select Prefab Map Locators" - bl_idname = "object.select_prefab_maps" + bl_idname = "object.scs_tools_select_prefab_map_locators" bl_description = "Select prefab map locators" def execute(self, context): @@ -1727,17 +1859,17 @@ def execute(self, context): for obj in context.scene.objects: if obj.scs_props.locator_type == 'Prefab': if obj.scs_props.locator_prefab_type == 'Map Point': - obj.select = True + _object_utils.select_set(obj, True) else: - obj.select = False + _object_utils.select_set(obj, False) else: - obj.select = False + _object_utils.select_set(obj, False) return {'FINISHED'} - class ViewPrefabMapLocators(bpy.types.Operator, _BaseViewOperator): + class SCS_TOOLS_OT_SwitchPrefabMapLocators(bpy.types.Operator, _BaseViewOperator): """Switch visibility of prefab map locators.""" - bl_label = "Switch Visibility of Prefab Map Locators" - bl_idname = "object.switch_prefab_maps_visibility" + bl_label = "View Prefab Map Locators" + bl_idname = "object.scs_tools_switch_prefab_map_locators" bl_description = "View only prefab map locators" + _BaseViewOperator.bl_base_description def execute(self, context): @@ -1754,10 +1886,10 @@ class TriggerPoints: Wrapper class for better navigation in file """ - class SelectPrefabTriggerLocators(bpy.types.Operator): + class SCS_TOOLS_OT_SelectPrefabTriggerLocators(bpy.types.Operator): """Selects all prefab trigger locators.""" bl_label = "Select Prefab Trigger Locators" - bl_idname = "object.select_prefab_triggers" + bl_idname = "object.scs_tools_select_prefab_trigger_locators" bl_description = "Select prefab trigger locators" def execute(self, context): @@ -1765,17 +1897,17 @@ def execute(self, context): for obj in context.scene.objects: if obj.scs_props.locator_type == 'Prefab': if obj.scs_props.locator_prefab_type == 'Trigger Point': - obj.select = True + _object_utils.select_set(obj, True) else: - obj.select = False + _object_utils.select_set(obj, False) else: - obj.select = False + _object_utils.select_set(obj, False) return {'FINISHED'} - class ViewPrefabTriggerLocators(bpy.types.Operator, _BaseViewOperator): + class SCS_TOOLS_OT_SwitchPrefabTriggerLocators(bpy.types.Operator, _BaseViewOperator): """Switch visibility of prefab trigger locators.""" - bl_label = "Switch Visibility of Prefab Trigger Locators" - bl_idname = "object.switch_prefab_triggers_visibility" + bl_label = "View Prefab Trigger Locators" + bl_idname = "object.scs_tools_switch_prefab_trigger_locators" bl_description = "View only prefab trigger locators" + _BaseViewOperator.bl_base_description def execute(self, context): @@ -1787,9 +1919,9 @@ def execute(self, context): hide_state=actual_hide_state, view_only=self.view_type == self.VIEWONLY) return {'FINISHED'} - class ConnectPrefabLocators(bpy.types.Operator): + class SCS_TOOLS_OT_ConnectPrefabLocators(bpy.types.Operator): bl_label = "Connect Prefab Locators" - bl_idname = "object.connect_prefab_locators" + bl_idname = "object.scs_tools_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'} @@ -1807,7 +1939,9 @@ def execute(self, context): self.report({'ERROR'}, "Selected objects are not correct prefab locators or they are not the same type.") return {'FINISHED'} - if _connection_group_wrapper.create_connection(obj0, obj1): + if _connections_wrapper.create_connection(obj0, obj1): + obj0.update_tag(refresh={'OBJECT'}) + obj1.update_tag(refresh={'OBJECT'}) _view3d_utils.tag_redraw_all_view3d() else: msg = str("Failed, because of one of following reasons:\n" @@ -1822,44 +1956,46 @@ def execute(self, context): return {'FINISHED'} - class DisconnectPrefabLocators(bpy.types.Operator): + class SCS_TOOLS_OT_DisconnectPrefabLocators(bpy.types.Operator): bl_label = "Disconnect Prefab Locators" - bl_idname = "object.disconnect_prefab_locators" + bl_idname = "object.scs_tools_disconnect_prefab_locators" bl_description = "To disconnect navigation points two connected prefab locators must be selected" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): if len(context.selected_objects) == 2: - obj0_name = context.selected_objects[0].name - obj1_name = context.selected_objects[1].name + obj0 = context.selected_objects[0] + obj1 = context.selected_objects[1] - if _connection_group_wrapper.delete_connection(obj0_name, obj1_name): + if _connections_wrapper.delete_connection(obj0.name, obj1.name): + obj0.update_tag(refresh={'OBJECT'}) + obj1.update_tag(refresh={'OBJECT'}) _view3d_utils.tag_redraw_all_view3d() else: self.report({'ERROR'}, "Connection between selected objects doesn't exists!") return {'FINISHED'} - class SelectPrefabLocators(bpy.types.Operator): + class SCS_TOOLS_OT_SelectPrefabLocators(bpy.types.Operator): """Selects all prefab locators.""" bl_label = "Select Prefab Locators" - bl_idname = "object.select_prefab_locators" + bl_idname = "object.scs_tools_select_prefab_locators" bl_description = "Select prefab locators" def execute(self, context): lprint('D Select Prefab Locators...') for obj in context.scene.objects: if obj.scs_props.locator_type == 'Prefab': - obj.select = True + _object_utils.select_set(obj, True) else: - obj.select = False + _object_utils.select_set(obj, False) return {'FINISHED'} - class ViewPrefabLocators(bpy.types.Operator, _BaseViewOperator): + class SCS_TOOLS_OT_SwitchPrefabLocators(bpy.types.Operator, _BaseViewOperator): """Switch visibility of prefab locators.""" - bl_label = "Switch Visibility of Prefab Locators" - bl_idname = "object.switch_prefab_locators_visibility" + bl_label = "View Prefab Locators" + bl_idname = "object.scs_tools_switch_prefab_locators" bl_description = "View only prefab locators" + _BaseViewOperator.bl_base_description def execute(self, context): @@ -1876,25 +2012,25 @@ class Collision: Wrapper class for better navigation in file """ - class SelectCollisionLocators(bpy.types.Operator): + class SCS_TOOLS_OT_SelectCollisionLocators(bpy.types.Operator): """Selects all collision locators.""" bl_label = "Select Collision Locators" - bl_idname = "object.select_collision_locators" + bl_idname = "object.scs_tools_select_collision_locators" bl_description = "Select collision locators" def execute(self, context): lprint('D Select Collision Locators...') for obj in context.scene.objects: if obj.scs_props.locator_type == 'Collision': - obj.select = True + _object_utils.select_set(obj, True) else: - obj.select = False + _object_utils.select_set(obj, False) return {'FINISHED'} - class ViewCollisionLocators(bpy.types.Operator, _BaseViewOperator): + class SCS_TOOLS_OT_SwitchCollisionLocators(bpy.types.Operator, _BaseViewOperator): """Switch visibility of collision locators.""" - bl_label = "Switch Visibility of Collision Locators" - bl_idname = "object.switch_collision_locators_visibility" + bl_label = "View Collision Locators" + bl_idname = "object.scs_tools_switch_collision_locators" bl_description = "View only collision locators" + _BaseViewOperator.bl_base_description def execute(self, context): @@ -1906,10 +2042,10 @@ def execute(self, context): hide_state=actual_hide_state, view_only=self.view_type == self.VIEWONLY) return {'FINISHED'} - class AllCollisionLocatorsWire(bpy.types.Operator): + class SCS_TOOLS_OT_EnableCollisionLocatorsWire(bpy.types.Operator): """All Collision Locators Wireframes ON.""" bl_label = "All Collision Locators Wireframes ON" - bl_idname = "object.all_collision_locators_wires" + bl_idname = "object.scs_tools_enable_collision_locators_wire" bl_description = "Turn ON wireframes draw in all collision locators" def execute(self, context): @@ -1918,12 +2054,16 @@ def execute(self, context): if obj.type == 'EMPTY': if obj.scs_props.locator_type == 'Collision': obj.scs_props.locator_collider_wires = True + obj.update_tag(refresh={'OBJECT'}) + + # force view refresh + _view3d_utils.tag_redraw_all_view3d() return {'FINISHED'} - class NoCollisionLocatorsWire(bpy.types.Operator): + class SCS_TOOLS_OT_DisableCollisionLocatorsWire(bpy.types.Operator): """All Collision Locators Wireframes OFF.""" bl_label = "All Collision Locators Wireframes OFF" - bl_idname = "object.no_collision_locators_wires" + bl_idname = "object.scs_tools_disable_collision_locators_wire" bl_description = "Turn OFF wireframes draw in all collision locators" def execute(self, context): @@ -1932,12 +2072,16 @@ def execute(self, context): if obj.type == 'EMPTY': if obj.scs_props.locator_type == 'Collision': obj.scs_props.locator_collider_wires = False + obj.update_tag(refresh={'OBJECT'}) + + # force view refresh + _view3d_utils.tag_redraw_all_view3d() return {'FINISHED'} - class AllCollisionLocatorsFaces(bpy.types.Operator): + class SCS_TOOLS_OT_EnableCollisionLocatorsFaces(bpy.types.Operator): """All Collision Locators Faces ON.""" bl_label = "All Collision Locators Faces ON" - bl_idname = "object.all_collision_locators_faces" + bl_idname = "object.scs_tools_enable_collision_locators_faces" bl_description = "Turn ON faces draw in all collision locators" def execute(self, context): @@ -1946,12 +2090,16 @@ def execute(self, context): if obj.type == 'EMPTY': if obj.scs_props.locator_type == 'Collision': obj.scs_props.locator_collider_faces = True + obj.update_tag(refresh={'OBJECT'}) + + # force view refresh + _view3d_utils.tag_redraw_all_view3d() return {'FINISHED'} - class NoCollisionLocatorsFaces(bpy.types.Operator): + class SCS_TOOLS_OT_DisableCollisionLocatorsFaces(bpy.types.Operator): """All Collision Locators Faces OFF.""" bl_label = "All Collision Locators Faces OFF" - bl_idname = "object.no_collision_locators_faces" + bl_idname = "object.scs_tools_disable_collision_locators_faces" bl_description = "Turn OFF faces draw in all collision locators" def execute(self, context): @@ -1960,6 +2108,10 @@ def execute(self, context): if obj.type == 'EMPTY': if obj.scs_props.locator_type == 'Collision': obj.scs_props.locator_collider_faces = False + obj.update_tag(refresh={'OBJECT'}) + + # force view refresh + _view3d_utils.tag_redraw_all_view3d() return {'FINISHED'} class Commons: @@ -1967,25 +2119,25 @@ class Commons: Wrapper class for better navigation in file """ - class SelectAllLocators(bpy.types.Operator): + class SCS_TOOLS_OT_SelectAllLocators(bpy.types.Operator): """Selects all locators.""" bl_label = "Select All Locators" - bl_idname = "object.select_all_locators" + bl_idname = "object.scs_tools_select_all_locators" bl_description = "Select all locators" def execute(self, context): lprint('D Select All Locators...') for obj in context.scene.objects: if obj.scs_props.locator_type != 'None': - obj.select = True + _object_utils.select_set(obj, True) else: - obj.select = False + _object_utils.select_set(obj, False) return {'FINISHED'} - class ViewAllLocators(bpy.types.Operator, _BaseViewOperator): + class SCS_TOOLS_OT_SwitchAllLocators(bpy.types.Operator, _BaseViewOperator): """Switch visibility all locators.""" - bl_label = "Switch Visibility of All Locators" - bl_idname = "object.switch_all_locators_visibility" + bl_label = "View All Locators" + bl_idname = "object.scs_tools_switch_all_locators" bl_description = "View only all locators" + _BaseViewOperator.bl_base_description def execute(self, context): @@ -1999,30 +2151,30 @@ def execute(self, context): # set actual hide state if it's not set yet if actual_hide_state is None: - actual_hide_state = not obj.hide + actual_hide_state = not obj.hide_get() - obj.hide = actual_hide_state + _object_utils.hide_set(obj, actual_hide_state) elif self.view_type == self.VIEWONLY: - obj.hide = True + _object_utils.hide_set(obj, True) return {'FINISHED'} - class PreviewModelPath(bpy.types.Operator): + class SCS_TOOLS_OT_SelectPreviewModelPath(bpy.types.Operator): """Operator for setting a relative path to Preview model file.""" bl_label = "Select Preview Model (*.pim)" - bl_idname = "object.select_preview_model_path" + bl_idname = "object.scs_tools_select_preview_model_path" bl_description = "Open a file browser" bl_options = {'REGISTER', 'UNDO'} - filepath = StringProperty( + filepath: StringProperty( name="Preview Model File Path", description="Relative path to a Preview model file", # maxlen=1024, subtype='FILE_PATH', ) - filter_glob = StringProperty(default="*.pim", options={'HIDDEN'}) + filter_glob: StringProperty(default="*.pim", options={'HIDDEN'}) def execute(self, context): """Set Traffic Semaphore Profile directory paths.""" @@ -2045,52 +2197,134 @@ class SCSRoot: Wrapper class for better navigation in file """ - class AddSCSRootObjectDialogOperator(bpy.types.Operator): - """Makes a dialog window allowing specifying a new Part name and adds it into Part list.""" - bl_idname = "object.add_scs_root_object_dialog_operator" - bl_label = "Add SCS Root Dialog" - bl_description = "Create dialog for adding new scs root (shouldn't be executed by user)" - bl_options = {"INTERNAL"} - - scs_root_object_string = StringProperty(name="Name for a new 'SCS Root Object':") - - def execute(self, context): - context.active_object.name = _name_utils.remove_diacritic(self.scs_root_object_string) - return {'FINISHED'} - - def invoke(self, context, event): - self.scs_root_object_string = context.active_object.name - return context.window_manager.invoke_props_dialog(self) - - class CreateSCSRootObject(bpy.types.Operator): + class SCS_TOOLS_OT_CreateSCSRoot(bpy.types.Operator): """Create New SCS Root Object.""" bl_label = "Add New SCS Root Object" - bl_idname = "object.create_scs_root_object" + bl_idname = "object.scs_tools_create_scs_root" 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'} + use_dialog: BoolProperty(name="Use Dialog", default=False) + scs_root_object_string: StringProperty(name="Name", default="game_object") + + def draw(self, context): + layout = self.layout + layout.prop(self, "scs_root_object_string") + def execute(self, context): lprint('D Create New SCS Root Object...') - _object_utils.make_scs_root_object(context, dialog=False) - return {'FINISHED'} - class CreateSCSRootObjectDialog(bpy.types.Operator): - """Create New SCS Root Object.""" - bl_label = "Add New SCS Root Object" - 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'} + # 1. save selected objects for re-parenting + objs_to_reparent = set(context.selected_objects) + + # 2. smart objects deselect, un-parent and keep transformations, so objects stays on same place when re-parenting to new root + if context.selected_objects: + + # make sure we have active object + context.view_layer.objects.active = context.selected_objects[0] + + # collect objects to deselect if it's a child of any selected parent to maintain multilevel inheritance on re-parent + objs_to_deselect = set() + for obj in context.selected_objects: + if not obj.parent: + continue + + # search for any selected parent, add it to deselect list and break if found + parent = obj.parent + while parent: + if parent.select_get(): + objs_to_deselect.add(obj) + break + + parent = parent.parent + + for obj in objs_to_deselect: + obj.select_set(False) + + # now collect parents that soon will be ex-parents + ex_parents = set() + for obj in context.selected_objects: + if obj.parent: + ex_parents.add(obj.parent) + + # un-parent + bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM') + + # fix ex parents + for obj in ex_parents: + obj.scs_cached_num_children = len(obj.children) + + ex_parent_scs_root = _object_utils.get_scs_root(obj) + if ex_parent_scs_root: + _looks.clean_unused(ex_parent_scs_root) + + # 3. add scs root empty object, set it's properties and fix scene objects count to avoid callback of new object + name = _name_utils.get_unique(self.scs_root_object_string, bpy.data.objects) + new_scs_root = bpy.data.objects.new(name, None) + new_scs_root.empty_display_size = 5.0 + new_scs_root.empty_display_type = "ARROWS" + new_scs_root.show_name = True + new_scs_root.location = context.scene.cursor.location + + new_scs_root.scs_props.object_identity = new_scs_root.name + new_scs_root.scs_props.scs_root_object_export_enabled = True + new_scs_root.scs_props.empty_object_type = 'SCS_Root' + + context.view_layer.active_layer_collection.collection.objects.link(new_scs_root) + context.view_layer.objects.active = new_scs_root + new_scs_root.select_set(True) + + context.scene.scs_cached_num_objects = len(context.scene.objects) + + # 4. re-parent objects to scs root object + part_inventory = new_scs_root.scs_object_part_inventory + if objs_to_reparent: + + new_scs_root_mats = [] + for obj in objs_to_reparent: + obj_has_old_parent = obj.parent and obj.parent not in objs_to_reparent + obj_needs_new_parent = obj_has_old_parent or not obj.parent + + # select and avoid parent loop check callback with set parent identity and new children number + if obj_needs_new_parent: + obj.select_set(True) + obj.scs_props.parent_identity = new_scs_root.name + obj.scs_cached_num_children = len(obj.children) + + # collect new materials from all objects which will belong to new scs root + for slot in obj.material_slots: + if slot.material and slot.material not in new_scs_root_mats: + new_scs_root_mats.append(slot.material) + + _looks.add_materials(new_scs_root, new_scs_root_mats) + + bpy.ops.object.parent_set(type='OBJECT', keep_transform=False) + bpy.ops.object.select_all(action='DESELECT') + new_scs_root.select_set(True) + + # fix children count to prevent persistent to hook up + new_scs_root.scs_cached_num_children = len(new_scs_root.children) + + # fix (recollect) parts on new scs root + for part_name in _object_utils.collect_parts_on_root(new_scs_root): + _inventory.add_item(part_inventory, part_name) + + # 5. if no part from re-parenting make default one, as scs root can't exists without part + if len(part_inventory) == 0: + _inventory.add_item(part_inventory, _PART_consts.default_name) - def execute(self, context): - lprint('D Create New SCS Root Object with Name dialog...') - _object_utils.make_scs_root_object(context, dialog=True) return {'FINISHED'} - class HideObjects(bpy.types.Operator): + def invoke(self, context, event): + if self.use_dialog: + return context.window_manager.invoke_props_dialog(self) + else: + return self.execute(context) + + class SCS_TOOLS_OT_HideObjectsWithinSCSRoot(bpy.types.Operator): bl_label = "Hide objects within current SCS Game Object" - bl_idname = "object.hide_objects_within_root" + bl_idname = "object.scs_tools_hide_objects_within_scs_root" bl_description = "Hide all the objects in active layers within current SCS Game Object" def execute(self, context): @@ -2101,14 +2335,14 @@ def execute(self, context): if scs_root_object: for obj in _object_utils.get_children(scs_root_object): if obj.type in ('EMPTY', 'MESH') and obj.scs_props.empty_object_type != 'SCS_Root': - obj.hide = True + _object_utils.hide_set(obj, True) return {'FINISHED'} - class ViewAllObjects(bpy.types.Operator): + class SCS_TOOLS_OT_ViewAllObjectsWithinSCSRoot(bpy.types.Operator): """Shows all the objects within current SCS Root object.""" bl_label = "View All Objects Within SCS Root" - bl_idname = "object.view_all_objects_within_root" + bl_idname = "object.scs_tools_view_all_objects_within_scs_root" bl_description = "Shows all the objects within current SCS Root object" @classmethod @@ -2123,16 +2357,16 @@ def execute(self, context): if scs_root_object: for obj in _object_utils.get_children(scs_root_object): if obj.type in ('EMPTY', 'MESH') and obj.scs_props.empty_object_type != 'SCS_Root': - obj.hide = False + _object_utils.hide_set(obj, False) - scs_root_object.hide = False + _object_utils.hide_set(scs_root_object, False) return {'FINISHED'} - class InvertVisibility(bpy.types.Operator): + class SCS_TOOLS_OT_InvertVisibilityWithinSCSRoot(bpy.types.Operator): """Invert visibility of the objects within current SCS Root object.""" bl_label = "Invert Visibility Within SCS Root" - bl_idname = "object.invert_visibility_within_root" + bl_idname = "object.scs_tools_invert_visibility_within_scs_root" bl_description = "Invert visibility of the objects within current SCS Root object" @classmethod @@ -2146,20 +2380,29 @@ def execute(self, context): scs_root_object = _object_utils.get_scs_root(active_object) if scs_root_object: - for obj in _object_utils.get_children(scs_root_object): + children = _object_utils.get_children(scs_root_object) + for obj in children: if obj.type in ('EMPTY', 'MESH') and obj.scs_props.empty_object_type != 'SCS_Root': - if obj.hide: - obj.hide = False - bpy.context.scene.objects.active = obj + if obj.hide_get(): + _object_utils.hide_set(obj, False) else: - obj.hide = True + _object_utils.hide_set(obj, True) + + # force first visible or root as active so user can invert visibility again, + # as active object got hidden and therfore this operator would be disabled + for obj in children: + if obj.visible_get(): + bpy.context.view_layer.objects.active = obj + break + else: + bpy.context.view_layer.objects.active = scs_root_object return {'FINISHED'} - class Isolate(bpy.types.Operator): + class SCS_TOOLS_OT_IsolateObjectsWithinSCSRoot(bpy.types.Operator): """Isolate all of the objects within current SCS Root object.""" bl_label = "Isolate Objects Within SCS Root" - bl_idname = "object.isolate_objects_within_root" + bl_idname = "object.scs_tools_isolate_objects_within_scs_root" bl_description = "Isolate all of the objects within current SCS Root object" @classmethod @@ -2176,15 +2419,70 @@ def execute(self, context): children = _object_utils.get_children(scs_root_object) for obj in bpy.context.scene.objects: if obj in children: - obj.hide = False - obj.select = True + _object_utils.hide_set(obj, False) + _object_utils.select_set(obj, True) else: - obj.hide = True - obj.select = False + _object_utils.hide_set(obj, True) + _object_utils.select_set(obj, False) - scs_root_object.hide = False - scs_root_object.select = True - bpy.context.scene.objects.active = scs_root_object + _object_utils.hide_set(scs_root_object, False) + scs_root_object.select_set(True) + bpy.context.view_layer.objects.active = scs_root_object + + return {'FINISHED'} + + class SCS_TOOLS_OT_RelocateSCSRoot(bpy.types.Operator): + """Re-locates SCS Root object to active object position and rotation, while keeping it's children untransformed.""" + bl_label = "Relocate Roots" + bl_idname = "object.scs_tools_relocate_scs_roots" + bl_description = "Re-locates SCS Root object to active object position and rotation, while keeping it's children untransformed." + + selected_objects = None + + @classmethod + def poll(cls, context): + return context.active_object and context.selected_objects + + def execute(self, context): + lprint("D " + self.bl_label + "...") + + active_object = context.active_object + + # get desired location/rotation and calculate translation diff + new_location = active_object.matrix_world.translation + new_rotation_matrix = active_object.matrix_world.to_quaternion().to_matrix().to_4x4() + + relocated_roots_count = 0 + for root_obj in context.selected_objects: + + # ignore non-root objects and active one + if root_obj.type != "EMPTY" or root_obj.scs_props.empty_object_type != "SCS_Root" or root_obj == active_object: + continue + + # calc translation diff + trans_diff = new_location - root_obj.matrix_world.translation + + # 1. gather original world matrices of children + children_matrices = {} + for child_obj in root_obj.children: + children_matrices[child_obj] = child_obj.matrix_world.to_4x4() # do 4x4 for a copy of original + + # 2. apply translation diff and rotation to root + root_obj.matrix_world.translation = root_obj.matrix_world.translation + trans_diff + inverse_rot_matrix = root_obj.matrix_world.to_quaternion().inverted().to_matrix().to_4x4() + root_obj.matrix_world = root_obj.matrix_world @ inverse_rot_matrix @ new_rotation_matrix + + # 3. apply original transformation matrices back to children + for child_obj in root_obj.children: + child_obj.matrix_world = children_matrices[child_obj] + + # 4. refresh counter + relocated_roots_count += 1 + + if relocated_roots_count == 0: + self.report({'WARNING'}, "No SCS Root Objects detected in selection. No relocation done!") + else: + self.report({'INFO'}, "Successfully relocated %i SCS Root objects." % relocated_roots_count) return {'FINISHED'} @@ -2194,10 +2492,10 @@ class Animation: Wrapper class for better navigation in file """ - class AddSCSAnimationOperator(bpy.types.Operator): + class SCS_TOOLS_OT_AddAnimation(bpy.types.Operator): """Add SCS Animation to actual SCS Game Object.""" bl_label = "Add SCS Animation" - bl_idname = "object.add_scs_animation" + bl_idname = "object.scs_tools_add_animation" bl_description = "Add SCS Animation to actual SCS Game Object" @classmethod @@ -2216,10 +2514,10 @@ def execute(self, context): return {'FINISHED'} - class RemoveSCSAnimationOperator(bpy.types.Operator): + class SCS_TOOLS_OT_RemoveAnimation(bpy.types.Operator): """Remove SCS Animation to actual SCS Game Object.""" bl_label = "Remove SCS Animation" - bl_idname = "object.remove_scs_animation" + bl_idname = "object.scs_tools_remove_animation" bl_description = "Remove SCS Animation to actual SCS Game Object" @classmethod @@ -2249,6 +2547,13 @@ def execute(self, context): lprint('S inventory[%i] - REMOVE item[%i] %r...', (len(inventory), active_scs_anim_i, active_scs_anim.name)) inventory.remove(active_scs_anim_i) + + # select active animation after remove if there is any left + if len(inventory) > 0: + if active_scs_anim_i - 1 >= 0: + scs_root_object.scs_props.active_scs_animation = active_scs_anim_i - 1 # one up + else: + scs_root_object.scs_props.active_scs_animation = 0 # first else: lprint("W No active 'SCS Animation' in list!") else: @@ -2256,10 +2561,10 @@ def execute(self, context): return {'FINISHED'} -class AddObject(bpy.types.Operator): +class SCS_TOOLS_OT_AddObject(bpy.types.Operator): """Creates and links locator or SCS Root to the scenes.""" bl_label = "Add SCS Object" - bl_idname = "object.scs_add_object" + bl_idname = "object.scs_tools_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'} @@ -2285,17 +2590,17 @@ def new_object_type_items(self, context): ), ] - new_object_type = bpy.props.EnumProperty( + new_object_type: EnumProperty( items=new_object_type_items ) - prefab_type = bpy.props.EnumProperty( + prefab_type: EnumProperty( name="Prefab Locator Type", description="Defines type of new prefab locator.", items=_ObjectSCSTools.locator_prefab_type_items ) - collider_type = bpy.props.EnumProperty( + collider_type: EnumProperty( name="Collision Locator Type", description="Defines type of new collision locator.", items=_ObjectSCSTools.locator_collider_type_items @@ -2325,17 +2630,18 @@ def execute(self, context): if self.new_object_type == "Root Object": - bpy.ops.object.create_scs_root_object() + bpy.ops.object.scs_tools_create_scs_root() else: + cursor = context.scene.cursor if self.new_object_type == "Prefab Locator": - new_loc = _object_utils.create_locator_empty("pl", context.scene.cursor_location, data_type="Prefab", blend_coords=True) + new_loc = _object_utils.create_locator_empty("pl", cursor.location, data_type="Prefab", blend_coords=True) new_loc.scs_props.locator_prefab_type = self.prefab_type elif self.new_object_type == "Model Locator": - new_loc = _object_utils.create_locator_empty("ml", context.scene.cursor_location, data_type="Model", blend_coords=True) + new_loc = _object_utils.create_locator_empty("ml", cursor.location, data_type="Model", blend_coords=True) else: - new_loc = _object_utils.create_locator_empty("cl", context.scene.cursor_location, data_type="Collision", blend_coords=True) + new_loc = _object_utils.create_locator_empty("cl", cursor.location, data_type="Collision", blend_coords=True) new_loc.scs_props.locator_collider_type = self.collider_type # if previous active object was SCS root then automatically parent new locator to it @@ -2345,15 +2651,19 @@ def execute(self, context): bpy.ops.object.select_all(action='DESELECT') # 2. prepare selection for parenting: select new locator, scs root and set scs root as active object - new_loc.select = True - active_obj.select = True - context.scene.objects.active = active_obj + new_loc.select_set(True) + active_obj.select_set(True) + context.view_layer.objects.active = active_obj # 3. execute parenting (selected -> active) bpy.ops.object.parent_set(type='OBJECT', keep_transform=True) - # 4. switch active to new locator so user can continue working on it - context.scene.objects.active = new_loc + # deselect all again & select only new object + bpy.ops.object.select_all(action='DESELECT') + new_loc.select_set(True) + + # switch active to new locator so user can continue working on it + context.view_layer.objects.active = new_loc return {'FINISHED'} @@ -2366,12 +2676,114 @@ def invoke(self, context, event): return self.execute(context) -class BlankOperator(bpy.types.Operator): +class SCS_TOOLS_OT_BlankOperator(bpy.types.Operator): """Blank Operator.""" bl_label = "Blank Operator" - bl_idname = "object.blank_operator" + bl_idname = "object.scs_tools_blank_operator" bl_description = "Blank operator" def execute(self, context): print('Blank Operator...') return {'FINISHED'} + + +classes = ( + Animation.SCS_TOOLS_OT_AddAnimation, + Animation.SCS_TOOLS_OT_RemoveAnimation, + + Collision.SCS_TOOLS_OT_SelectStaticCollisionObjects, + Collision.SCS_TOOLS_OT_SwitchStaticCollisionObjects, + + ConvexCollider.SCS_TOOLS_OT_ConvertConvexLocatorToMesh, + ConvexCollider.SCS_TOOLS_OT_ConvertMeshToConvexLocator, + ConvexCollider.SCS_TOOLS_OT_MakeConvexMesh, + + Glass.SCS_TOOLS_OT_ShowGlassObjectsAsWire, + Glass.SCS_TOOLS_OT_ShowGlassObjectsAsTextured, + Glass.SCS_TOOLS_OT_SelectGlassObjects, + Glass.SCS_TOOLS_OT_SwitchGlassObjects, + + Locators.Collision.SCS_TOOLS_OT_EnableCollisionLocatorsFaces, + Locators.Collision.SCS_TOOLS_OT_EnableCollisionLocatorsWire, + Locators.Collision.SCS_TOOLS_OT_DisableCollisionLocatorsFaces, + Locators.Collision.SCS_TOOLS_OT_DisableCollisionLocatorsWire, + Locators.Collision.SCS_TOOLS_OT_SelectCollisionLocators, + Locators.Collision.SCS_TOOLS_OT_SwitchCollisionLocators, + Locators.Commons.SCS_TOOLS_OT_SelectPreviewModelPath, + Locators.Commons.SCS_TOOLS_OT_SelectAllLocators, + Locators.Commons.SCS_TOOLS_OT_SwitchAllLocators, + Locators.Model.SCS_TOOLS_OT_FixModelLocatorHookups, + Locators.Model.SCS_TOOLS_OT_SelectModelLocatorsWithSameHookup, + Locators.Model.SCS_TOOLS_OT_SelectModelLocators, + Locators.Model.SCS_TOOLS_OT_SwitchModelLocators, + Locators.Prefab.ControlNodes.SCS_TOOLS_OT_AbortTerrainPointsPreview, + Locators.Prefab.ControlNodes.SCS_TOOLS_OT_AssignTerrainPoints, + Locators.Prefab.ControlNodes.SCS_TOOLS_OT_ClearTerrainPoints, + Locators.Prefab.ControlNodes.SCS_TOOLS_OT_PreviewTerrainPoints, + Locators.Prefab.ControlNodes.SCS_TOOLS_OT_SelectPrefabNodeLocators, + Locators.Prefab.ControlNodes.SCS_TOOLS_OT_SwitchPrefabNodeLocators, + Locators.Prefab.MapPoints.SCS_TOOLS_OT_SelectPrefabMapLocators, + Locators.Prefab.MapPoints.SCS_TOOLS_OT_SwitchPrefabMapLocators, + Locators.Prefab.NavigationPoints.SCS_TOOLS_OT_SelectPrefabNavLocators, + Locators.Prefab.NavigationPoints.SCS_TOOLS_OT_SwitchPrefabNavLocators, + Locators.Prefab.Signs.SCS_TOOLS_OT_SelectPrefabSignLocators, + Locators.Prefab.Signs.SCS_TOOLS_OT_SwitchPrefabSignLocators, + Locators.Prefab.SpawnPoints.SCS_TOOLS_OT_SelectPrefabSpawnLocators, + Locators.Prefab.SpawnPoints.SCS_TOOLS_OT_SwitchPrefabSpawnLocators, + Locators.Prefab.TrafficLights.SCS_TOOLS_OT_SelectPrefabTrafficLocators, + Locators.Prefab.TrafficLights.SCS_TOOLS_OT_SwitchPrefabTrafficLocators, + Locators.Prefab.TriggerPoints.SCS_TOOLS_OT_SelectPrefabTriggerLocators, + Locators.Prefab.TriggerPoints.SCS_TOOLS_OT_SwitchPrefabTriggerLocators, + Locators.Prefab.SCS_TOOLS_OT_ConnectPrefabLocators, + Locators.Prefab.SCS_TOOLS_OT_DisconnectPrefabLocators, + Locators.Prefab.SCS_TOOLS_OT_SelectPrefabLocators, + Locators.Prefab.SCS_TOOLS_OT_SwitchPrefabLocators, + + Look.SCS_TOOLS_OT_AddLook, + Look.SCS_TOOLS_OT_RemoveActiveLook, + + ModelObjects.SCS_TOOLS_OT_SearchDegeneratedPolys, + ModelObjects.SCS_TOOLS_OT_SelectModelObjects, + ModelObjects.SCS_TOOLS_OT_SwitchModelObjects, + + Part.SCS_TOOLS_OT_AddPart, + Part.SCS_TOOLS_OT_AssignPart, + Part.SCS_TOOLS_OT_CleanUnusedParts, + Part.SCS_TOOLS_OT_PrintParts, + Part.SCS_TOOLS_OT_RemoveActivePart, + Part.SCS_TOOLS_OT_MoveActivePart, + Part.SCS_TOOLS_OT_DeSelectObjectsWithPart, + Part.SCS_TOOLS_OT_SwitchObjectsWithPart, + + SCSRoot.SCS_TOOLS_OT_CreateSCSRoot, + SCSRoot.SCS_TOOLS_OT_HideObjectsWithinSCSRoot, + SCSRoot.SCS_TOOLS_OT_InvertVisibilityWithinSCSRoot, + SCSRoot.SCS_TOOLS_OT_IsolateObjectsWithinSCSRoot, + SCSRoot.SCS_TOOLS_OT_ViewAllObjectsWithinSCSRoot, + SCSRoot.SCS_TOOLS_OT_RelocateSCSRoot, + + ShadowCasters.SCS_TOOLS_OT_SelectShadowCasters, + ShadowCasters.SCS_TOOLS_OT_ShowShadowCastersAsWire, + ShadowCasters.SCS_TOOLS_OT_ShowShadowCasterAsTextured, + ShadowCasters.SCS_TOOL_OT_SwitchShadowCasters, + + Variant.SCS_TOOLS_OT_AddVariant, + Variant.SCS_TOOLS_OT_PrintVariants, + Variant.SCS_TOOLS_OT_RemoveActiveVariant, + Variant.SCS_TOOLS_OT_DeSelectObjectsWithVariant, + Variant.SCS_TOOLS_OT_SwitchObjectsWithVariant, + Variant.SCS_TOOLS_OT_MoveActiveVariant, + + SCS_TOOLS_OT_AddObject, + SCS_TOOLS_OT_BlankOperator, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/addon/io_scs_tools/operators/scene.py b/addon/io_scs_tools/operators/scene.py index 85e493e..73b74db 100644 --- a/addon/io_scs_tools/operators/scene.py +++ b/addon/io_scs_tools/operators/scene.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2017: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy import bmesh @@ -28,20 +28,22 @@ from sys import platform from time import time from bpy.props import StringProperty, CollectionProperty, EnumProperty, IntProperty, BoolProperty, FloatProperty, FloatVectorProperty +from bl_operators.presets import AddPresetBase from io_scs_tools.consts import ConvHlpr as _CONV_HLPR_consts from io_scs_tools.consts import Operators as _OP_consts from io_scs_tools.consts import PaintjobTools as _PT_consts from io_scs_tools.imp import pix as _pix_import from io_scs_tools.internals.structure import UnitData as _UnitData +from io_scs_tools.internals.containers import pix as _pix_container from io_scs_tools.internals.containers import sii as _sii_container from io_scs_tools.internals.containers.tobj import TobjContainer as _TobjContainer +from io_scs_tools.operators.bases.export import SCSExportHelper as _SCSExportHelper from io_scs_tools.utils import name as _name_utils from io_scs_tools.utils import object as _object_utils -from io_scs_tools.utils import view3d as _view3d_utils from io_scs_tools.utils import path as _path_utils from io_scs_tools.utils import get_scs_globals as _get_scs_globals from io_scs_tools.utils.printout import lprint -from io_scs_tools.utils.property import get_by_type as _get_bpy_prop +from io_scs_tools.utils.property import get_default as _get_default from io_scs_tools.utils.property import get_filebrowser_display_type from io_scs_tools import exp as _export from io_scs_tools import imp as _import @@ -52,21 +54,21 @@ class Import: Wrapper class for better navigation in file """ - class ImportAnimActions(bpy.types.Operator): + class SCS_TOOLS_OT_ImportAnimActions(bpy.types.Operator): bl_label = "Import SCS Animation (PIA)" - bl_idname = "scene.import_scs_anim_actions" + bl_idname = "scene.scs_tools_import_anim_actions" bl_description = "Import SCS Animation files (PIA) as a new SCS animations" - directory = StringProperty( + directory: StringProperty( name="Import PIA Directory", # maxlen=1024, subtype='DIR_PATH', ) - files = CollectionProperty( + files: CollectionProperty( type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'}, ) - filter_glob = StringProperty(default="*.pia", options={'HIDDEN'}) + filter_glob: StringProperty(default="*.pia", options={'HIDDEN'}) @classmethod def poll(cls, context): @@ -100,18 +102,26 @@ class Export: Wrapper class for better navigation in file """ - class ExportByScope(bpy.types.Operator): + class SCS_TOOLS_OT_ExportByScope(bpy.types.Operator, _SCSExportHelper): """Export selected operator.""" - bl_idname = "scene.export_scs_content_by_scope" + bl_idname = "scene.scs_tools_export_by_scope" bl_label = "Export By Used Scope" bl_description = "Export SCS models depending on selected export scope." bl_options = set() + def __init__(self): + super().__init__() + + self.can_mouse_rotate = False + """:type bool: Flag indiciating whether mouse move even will rotate view or no. Initiated by left mouse button press.""" + @staticmethod - def execute_rotation(rot_direction): + def execute_rotation(rot_direction, angle=0.05): """Uses Blender orbit rotation to rotate all 3D views :param rot_direction: "ORBITLEFT" | "ORBITRIGHT" | "ORBITUP" | "ORBITDOWN" :type rot_direction: str + :param angle: angle to use by rotation, in radians + :type angle: float """ for area in bpy.context.screen.areas: if area.type == "VIEW_3D": @@ -120,10 +130,10 @@ def execute_rotation(rot_direction): 'screen': bpy.context.screen, 'blend_data': bpy.context.blend_data, 'scene': bpy.context.scene, - 'region': area.regions[4], + 'region': area.regions[5], 'area': area } - bpy.ops.view3d.view_orbit(override, type=rot_direction) + bpy.ops.view3d.view_orbit(override, angle=angle, type=rot_direction) @staticmethod def execute_zoom(zoom_change): @@ -136,123 +146,10 @@ def execute_zoom(zoom_change): region = area.spaces[0].region_3d region.view_distance += zoom_change - def __init__(self): - """Shows all layer to be able to alter selection on the whole scene and - alter Blender selection the way that: - 1. if only child within root is selected -> selects root too - 2. if only root is selected -> select all children - 3. if root and some children are selected -> don't change selection - """ - - lprint("D Gathering object which visibility should be altered for export ...") - - self.layers_visibilities = _view3d_utils.switch_layers_visibility([], True) - self.last_active_obj = bpy.context.active_object - self.altered_objs = [] - self.altered_objs_visibilities = [] - self.not_root_objs = [] - - for obj in bpy.context.selected_objects: - root = _object_utils.get_scs_root(obj) - if root: - if root != obj: - if not root.select: - root.select = True - self.altered_objs.append(root.name) - else: - children = _object_utils.get_children(obj) - local_reselected_objs = [] - for child_obj in children: - local_reselected_objs.append(child_obj.name) - # if some child is selected this means we won't reselect nothing in this game objecdt - if child_obj.select: - local_reselected_objs = [] - break - self.altered_objs.extend(local_reselected_objs) - else: - obj.select = False - self.not_root_objs.append(obj.name) - - for obj_name in self.altered_objs: - self.altered_objs_visibilities.append(bpy.data.objects[obj_name].hide) - bpy.data.objects[obj_name].hide = False - bpy.data.objects[obj_name].select = True - - lprint("D Gathering object visibility done!") - - def __del__(self): - """Revert altered initial selection and layers visibilites - """ - - lprint("D Recovering object visibility after export ...") - - # safety check if it's not deleting last instance - if hasattr(self, "altered_objs"): - i = 0 - for obj_name in self.altered_objs: - bpy.data.objects[obj_name].hide = self.altered_objs_visibilities[i] - i += 1 - bpy.data.objects[obj_name].select = False - - for obj_name in self.not_root_objs: - bpy.data.objects[obj_name].select = True - - if self.last_active_obj is not None: - # call selection twice to actually change active object - self.last_active_obj.select = not self.last_active_obj.select - self.last_active_obj.select = not self.last_active_obj.select - - _view3d_utils.switch_layers_visibility(self.layers_visibilities, False) - - lprint("D Recovering object visibility done!") - - def execute_export(self, context, disable_local_view): - """Actually executes export of current selected objects (bpy.context.selected_objects) - - :param context: operator context - :type context: bpy_struct - :param disable_local_view: True if you want to disable local view after export - :type disable_local_view: bool - :return: succes of batch export - :rtype: {'FINISHED'} | {'CANCELLED'} - """ - - init_obj_list = () - export_scope = _get_scs_globals().export_scope - if export_scope == "selection": - init_obj_list = tuple(bpy.context.selected_objects) - elif export_scope == "scene": - init_obj_list = tuple(bpy.context.scene.objects) - elif export_scope == "scenes": - init_obj_list = tuple(bpy.data.objects) - - # check extension for EF format and properly assign it to name suffix - ef_name_suffix = "" - if _get_scs_globals().export_output_type == "EF": - ef_name_suffix = ".ef" - - try: - result = _export.batch_export(self, init_obj_list, name_suffix=ef_name_suffix) - except Exception as e: - - result = {"CANCELLED"} - context.window.cursor_modal_restore() - - import traceback - - trace_str = traceback.format_exc().replace("\n", "\n\t ") - lprint("E Unexpected %r accured during batch export:\n\t %s", - (type(e).__name__, trace_str), - report_errors=1, - report_warnings=1) - - if disable_local_view: - _view3d_utils.switch_local_view(False) - - return result - def modal(self, context, event): + ret_status = {'RUNNING_MODAL'} + if event.type in ('WHEELUPMOUSE', 'NUMPAD_PLUS'): self.execute_zoom(-1) elif event.type in ('WHEELDOWNMOUSE', 'NUMPAD_MINUS'): @@ -265,53 +162,63 @@ def modal(self, context, event): self.execute_rotation("ORBITUP") elif event.type == 'NUMPAD_2': self.execute_rotation("ORBITDOWN") + elif event.type == 'LEFTMOUSE' and event.value == 'PRESS': + self.can_mouse_rotate = True + elif (event.type == 'LEFTMOUSE' and event.value == 'RELEASE') or event.type == 'WINDOW_DEACTIVATE': + self.can_mouse_rotate = False + elif event.type == 'MOUSEMOVE' and self.can_mouse_rotate: + diff_x = event.mouse_x - event.mouse_prev_x + diff_y = event.mouse_y - event.mouse_prev_y + if abs(diff_x) > abs(diff_y): + self.execute_rotation("ORBITLEFT", angle=diff_x * 0.002) + else: + self.execute_rotation("ORBITDOWN", angle=diff_y * 0.002) elif event.type in ('RET', 'NUMPAD_ENTER'): - return self.execute_export(context, True) + ret_status = self.execute_export(context, False) elif event.type == 'ESC': - _view3d_utils.switch_local_view(False) - return {'CANCELLED'} + ret_status = {'CANCELLED'} - return {'RUNNING_MODAL'} + # make sure to finish preview, before ending the operator + if ret_status.issubset({'CANCELLED', 'FINISHED'}): + self.finish() + + return ret_status def invoke(self, context, event): # show preview or directly execute export - if _get_scs_globals().export_scope == "selection": - - if _get_scs_globals().preview_export_selection: - - if len(context.selected_objects) == 0: - print(self.not_root_objs) - if len(self.not_root_objs) > 0: - msg = "Selected objects are not part of any SCS Game Object!" - else: - msg = "Nothing selected!" - self.report({'ERROR'}, msg) - return {'FINISHED'} + if _get_scs_globals().export_scope == "selection" and _get_scs_globals().preview_export_selection: + if len(self.get_objects_for_export()) == 0: + if len(bpy.context.selected_objects) > 0: + msg = "Selected objects are not part of any SCS Game Object!" + else: + msg = "Nothing selected!" + self.report({'ERROR'}, msg) + return {'FINISHED'} - _view3d_utils.switch_local_view(True) - context.window_manager.modal_handler_add(self) - return {'RUNNING_MODAL'} + self.init() + context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} - return self.execute_export(context, False) + return self.execute_export(context, True) - class ExportAnimAction(bpy.types.Operator): + class SCS_TOOLS_OT_ExportAnimAction(bpy.types.Operator): bl_label = "Export SCS Animation (PIA)" - bl_idname = "scene.export_scs_anim_action" + bl_idname = "scene.scs_tools_export_anim_action" bl_description = "Select directory and export SCS animation (PIA) to it." bl_options = {'INTERNAL'} - index = IntProperty( + index: IntProperty( name="Anim Index", default=False, options={'HIDDEN'} ) - directory = StringProperty( + directory: StringProperty( name="Export PIA Directory", subtype='DIR_PATH', ) filename_ext = ".pia" - filter_glob = StringProperty(default=str("*" + filename_ext), options={'HIDDEN'}) + filter_glob: StringProperty(default=str("*" + filename_ext), options={'HIDDEN'}) @classmethod def poll(cls, context): @@ -355,22 +262,22 @@ class Paths: Wrapper class for better navigation in file """ - class SCSProjectPath(bpy.types.Operator): + class SCS_TOOLS_OT_SelectProjectPath(bpy.types.Operator): """Operator for setting an absolute path to SCS Project Directory.""" bl_label = "Select SCS Project Directory" - bl_idname = "scene.select_scs_project_path" + bl_idname = "scene.scs_tools_select_project_path" bl_description = "Open a directory browser" # always set default display type so blender won't be using "last used" - display_type = get_filebrowser_display_type() + display_type: get_filebrowser_display_type() - directory = StringProperty( + directory: StringProperty( name="SCS Project Directory Path", description="SCS project directory path", # maxlen=1024, subtype='DIR_PATH', ) - filter_glob = StringProperty(default="*.*", options={'HIDDEN'}) + filter_glob: StringProperty(default="*.*", options={'HIDDEN'}) def execute(self, context): """Set SCS project directory path.""" @@ -385,22 +292,22 @@ def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} - class ShaderPresetsFilePath(bpy.types.Operator): + class SCS_TOOLS_OT_SelectShaderPresetsPath(bpy.types.Operator): """Operator for setting relative or absolute path to Shader Presets Library file.""" bl_label = "Select Shader Presets Library File" - bl_idname = "scene.select_shader_presets_filepath" + bl_idname = "scene.scs_tools_select_shader_presets_path" bl_description = "Open a file browser" # always set default display type so blender won't be using "last used" - display_type = get_filebrowser_display_type() + display_type: get_filebrowser_display_type() - filepath = StringProperty( + filepath: StringProperty( name="Shader Presets Library File", description="Shader Presets library relative or absolute file path", # maxlen=1024, subtype='FILE_PATH', ) - filter_glob = StringProperty(default="*.txt", options={'HIDDEN'}) + filter_glob: StringProperty(default="*.txt", options={'HIDDEN'}) def execute(self, context): """Set Shader Presets library file path.""" @@ -419,22 +326,22 @@ def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} - class TriggerActionsRelativePath(bpy.types.Operator): + class SCS_TOOLS_OT_SelectTriggerActionsLibPath(bpy.types.Operator): """Operator for setting relative path to Trigger actions file.""" bl_label = "Select Trigger Actions File" - bl_idname = "scene.select_trigger_actions_rel_path" + bl_idname = "scene.scs_tools_select_trigger_actions_lib_path" bl_description = "Open a file browser" # always set default display type so blender won't be using "last used" - display_type = get_filebrowser_display_type() + display_type: get_filebrowser_display_type() - filepath = StringProperty( + filepath: StringProperty( name="Trigger Actions File", description="Trigger actions relative file path", # maxlen=1024, subtype='FILE_PATH', ) - filter_glob = StringProperty(default="*.sii", options={'HIDDEN'}) + filter_glob: StringProperty(default="*.sii", options={'HIDDEN'}) def execute(self, context): """Set Sign directory path.""" @@ -453,22 +360,22 @@ def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} - class SignLibraryRelativePath(bpy.types.Operator): + class SCS_TOOLS_OT_SelectSignLibPath(bpy.types.Operator): """Operator for setting relative path to Sign Library file.""" bl_label = "Select Sign Library File" - bl_idname = "scene.select_sign_library_rel_path" + bl_idname = "scene.scs_tools_select_sign_lib_path" bl_description = "Open a file browser" # always set default display type so blender won't be using "last used" - display_type = get_filebrowser_display_type() + display_type: get_filebrowser_display_type() - filepath = StringProperty( + filepath: StringProperty( name="Sign Library File", description="Sign library relative file path", # maxlen=1024, subtype='FILE_PATH', ) - filter_glob = StringProperty(default="*.sii", options={'HIDDEN'}) + filter_glob: StringProperty(default="*.sii", options={'HIDDEN'}) def execute(self, context): """Set Sign directory path.""" @@ -487,22 +394,22 @@ def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} - class TSemLibraryRelativePath(bpy.types.Operator): + class SCS_TOOLS_OT_SelectSemaphoreLibPath(bpy.types.Operator): """Operator for setting relative path to Traffic Semaphore Profile Library file.""" bl_label = "Select Traffic Semaphore Profile Library File" - bl_idname = "scene.select_tsem_library_rel_path" + bl_idname = "scene.scs_tools_select_semaphore_lib_path" bl_description = "Open a file browser" # always set default display type so blender won't be using "last used" - display_type = get_filebrowser_display_type() + display_type: get_filebrowser_display_type() - filepath = StringProperty( + filepath: StringProperty( name="Traffic Semaphore Profile Library File", description="Traffic Semaphore Profile library relative file path", # maxlen=1024, subtype='FILE_PATH', ) - filter_glob = StringProperty(default="*.sii", options={'HIDDEN'}) + filter_glob: StringProperty(default="*.sii", options={'HIDDEN'}) def execute(self, context): """Set Traffic Semaphore Profile library filepath.""" @@ -521,22 +428,22 @@ def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} - class TrafficRulesLibraryRelativePath(bpy.types.Operator): + class SCS_TOOLS_OT_SelectTrafficRulesLibPath(bpy.types.Operator): """Operator for setting relative path to Traffic Rules Library file.""" bl_label = "Select Traffic Rules Library File" - bl_idname = "scene.select_traffic_rules_library_rel_path" + bl_idname = "scene.scs_tools_select_traffic_rules_lib_path" bl_description = "Open a file browser" # always set default display type so blender won't be using "last used" - display_type = get_filebrowser_display_type() + display_type: get_filebrowser_display_type() - filepath = StringProperty( + filepath: StringProperty( name="Traffic Rules Library File", description="Traffic Rules library relative file path", # maxlen=1024, subtype='FILE_PATH', ) - filter_glob = StringProperty(default="*.sii", options={'HIDDEN'}) + filter_glob: StringProperty(default="*.sii", options={'HIDDEN'}) def execute(self, context): """Set Traffic Rules library filepath.""" @@ -555,22 +462,22 @@ def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} - class HookupLibraryRelativePath(bpy.types.Operator): - """Operator for setting relative path to Hookup files.""" + class SCS_TOOLS_OT_SelectHookupLibPath(bpy.types.Operator): + """Operator for setting path to Hookup files.""" bl_label = "Select Hookup Library Directory" - bl_idname = "scene.select_hookup_library_rel_path" + bl_idname = "scene.scs_tools_select_hookup_lib_path" bl_description = "Open a directory browser" # always set default display type so blender won't be using "last used" - display_type = get_filebrowser_display_type() + display_type: get_filebrowser_display_type() - directory = StringProperty( + directory: StringProperty( name="Hookup Library Directory Path", description="Hookup library directory path", # maxlen=1024, subtype='DIR_PATH', ) - filter_glob = StringProperty(default="*.sii", options={'HIDDEN'}) + filter_glob: StringProperty(default="*.sii", options={'HIDDEN'}) def execute(self, context): """Set Hookup directory path.""" @@ -589,22 +496,22 @@ def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} - class MatSubsLibraryRelativePath(bpy.types.Operator): - """Operator for setting relative path to Material Substance shader files.""" + class SCS_TOOLS_OT_SelectMatSubsLibPath(bpy.types.Operator): + """Operator for setting path to Material Substance file.""" bl_label = "Select Material Substance Library File" - bl_idname = "scene.select_matsubs_library_rel_path" + bl_idname = "scene.scs_tools_select_matsubs_lib_path" bl_description = "Open a file browser" # always set default display type so blender won't be using "last used" - display_type = get_filebrowser_display_type() + display_type: get_filebrowser_display_type() - filepath = StringProperty( + filepath: StringProperty( name="Material Substance Library File", description="Material Substance library relative file path", # maxlen=1024, subtype='FILE_PATH', ) - filter_glob = StringProperty(default="*.db", options={'HIDDEN'}) + filter_glob: StringProperty(default="*.db", options={'HIDDEN'}) def execute(self, context): """Set Material Substance library filepath.""" @@ -623,21 +530,21 @@ def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} - class SunProfilesLibraryPath(bpy.types.Operator): + class SCS_TOOLS_OT_SelectSunProfilesLibPath(bpy.types.Operator): """Operator for setting relative path to Material Substance shader files.""" bl_label = "Select Sun Profiles Library File" - bl_idname = "scene.select_sun_profiles_lib_path" + bl_idname = "scene.scs_tools_select_sun_profiles_lib_path" bl_description = "Open a file browser" # always set default display type so blender won't be using "last used" - display_type = get_filebrowser_display_type() + display_type: get_filebrowser_display_type() - filepath = StringProperty( + filepath: StringProperty( name="Sun Profiles Library File", description="Sun Profiles library relative/absolute file path", subtype='FILE_PATH', ) - filter_glob = StringProperty(default="*.sii", options={'HIDDEN'}) + filter_glob: StringProperty(default="*.sii", options={'HIDDEN'}) def execute(self, context): """Set Material Substance library filepath.""" @@ -656,22 +563,22 @@ def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} - class DirSelectorInsideBase(bpy.types.Operator): + class SCS_TOOLS_OT_SelectDirInsideBase(bpy.types.Operator): """Operator for setting relative or absolute path to Global Export file.""" bl_label = "Select Directory" - bl_idname = "scene.select_directory_inside_base" + bl_idname = "scene.scs_tools_select_dir_inside_base" bl_description = "Open a directory browser" # always set default display type so blender won't be using "last used" - display_type = get_filebrowser_display_type() + display_type: get_filebrowser_display_type() - directory = StringProperty( + directory: StringProperty( name="Directory", description="Directory inside SCS Project Base path", subtype='DIR_PATH', ) - type = EnumProperty( + type: EnumProperty( name="Type", description="Type of selection", items=( @@ -683,7 +590,7 @@ class DirSelectorInsideBase(bpy.types.Operator): options={'HIDDEN'} ) - filter_glob = StringProperty(default="*.pim", options={'HIDDEN'}) + filter_glob: StringProperty(default="*.pim", options={'HIDDEN'}) def execute(self, context): @@ -717,13 +624,13 @@ def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} - class ReloadLibraryPath(bpy.types.Operator): + class SCS_TOOLS_OT_ReloadLibrary(bpy.types.Operator): """Operator for reloading given library.""" bl_label = "Reload" - bl_idname = "scene.scs_reload_library" + bl_idname = "scene.scs_tools_reload_library" bl_description = "Reloads library and updates it with any possible new entries." - library_path_attr = StringProperty() + library_path_attr: StringProperty() def execute(self, context): scs_globals = _get_scs_globals() @@ -738,15 +645,46 @@ def execute(self, context): return {'FINISHED'} + class SCS_TOOLS_OT_AddPathPreset(bpy.types.Operator, AddPresetBase): + bl_label = "Add New Paths Preset" + bl_idname = "scene.scs_tools_add_path_preset" + bl_description = "Add or remove preset for SCS Project Base Path and rest of the libraries, to quickly switch between projects" + preset_menu = "SCS_TOOLS_PT_PathSettingsPresets" + + # variable used for all preset values + preset_defines = [ + "scs_globals = bpy.context.preferences.addons['io_scs_tools'].preferences.scs_globals" + ] + + # properties to store in the preset + preset_values = [ + "scs_globals.scs_project_path", + "scs_globals.use_alternative_bases", + "scs_globals.trigger_actions_rel_path", + "scs_globals.trigger_actions_use_infixed", + "scs_globals.sign_library_rel_path", + "scs_globals.sign_library_use_infixed", + "scs_globals.tsem_library_rel_path", + "scs_globals.tsem_library_use_infixed", + "scs_globals.traffic_rules_library_rel_path", + "scs_globals.traffic_rules_library_use_infixed", + "scs_globals.hookup_library_rel_path", + "scs_globals.matsubs_library_rel_path", + "scs_globals.shader_presets_filepath" + ] + + # where to store the preset + preset_subdir = "io_scs_tools/paths" + class Animation: """ Wraper class for better navigation in file """ - class IncreaseAnimationSteps(bpy.types.Operator): + class SCS_TOOLS_OT_IncreaseAnimationSteps(bpy.types.Operator): bl_label = "Increase Animation Steps" - bl_idname = "scene.increase_animation_steps" + bl_idname = "scene.scs_tools_increase_animation_steps" # TODO: better description... bl_description = "Scales the entire animation 2x in length, but compensate for playback and export so the result stays the same..." @@ -808,9 +746,9 @@ def execute(self, context): return {'FINISHED'} - class DecreaseAnimationSteps(bpy.types.Operator): + class SCS_TOOLS_OT_DecreaseAnimationSteps(bpy.types.Operator): bl_label = "Decrease Animation Steps" - bl_idname = "scene.decrease_animation_steps" + bl_idname = "scene.scs_tools_decrease_animation_steps" # TODO: better description... bl_description = "Scales down the entire animation to its half in length, but compensate for playback and export so the result stays the " \ "same..." @@ -879,9 +817,9 @@ class ConversionHelper: Wraper class for better navigation in file """ - class CleanRSRC(bpy.types.Operator): + class SCS_TOOLS_OT_CleanConversionRSRC(bpy.types.Operator): bl_label = "Clean RSRC" - bl_idname = "scene.scs_conv_hlpr_clean_rsrc" + bl_idname = "scene.scs_tools_clean_conversion_rsrc" bl_description = "Cleans-up converted data inside of conversion tools (empties 'rsrc' folder & removes symbolic links)." def execute(self, context): @@ -911,12 +849,12 @@ def execute(self, context): self.report({'INFO'}, "Successfully cleaned converted data in conversion tools folder!") return {'FINISHED'} - class AddConversionPath(bpy.types.Operator): + class SCS_TOOLS_OT_AddConversionPath(bpy.types.Operator): bl_label = "Add Path" - bl_idname = "scene.scs_conv_hlpr_add_path" + bl_idname = "scene.scs_tools_add_conversion_path" bl_description = "Adds new path to the stack of paths for conversion" - directory = StringProperty( + directory: StringProperty( name="Directory", description="Any directory on file system", subtype='DIR_PATH', @@ -939,9 +877,9 @@ def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} - class RemoveConversionPath(bpy.types.Operator): + class SCS_TOOLS_OT_RemoveConversionPath(bpy.types.Operator): bl_label = "Remove Path" - bl_idname = "scene.scs_conv_hlpr_remove_path" + bl_idname = "scene.scs_tools_remove_conversion_path" bl_description = "Removes path from the stack of paths for conversion" def execute(self, context): @@ -955,12 +893,12 @@ def execute(self, context): return {'FINISHED'} - class OrderConversionPath(bpy.types.Operator): + class SCS_TOOLS_OT_OrderConversionPath(bpy.types.Operator): bl_label = "Order Path" - bl_idname = "scene.scs_conv_hlpr_order_path" + bl_idname = "scene.scs_tools_order_conversion_path" bl_description = "Change order for the current path" - move_up = BoolProperty(default=True) + move_up: BoolProperty(default=True) def execute(self, context): scs_globals = _get_scs_globals() @@ -985,9 +923,9 @@ def execute(self, context): return {'FINISHED'} - class RunConversion(bpy.types.Operator): + class SCS_TOOLS_OT_RunConversion(bpy.types.Operator): bl_label = "Run Conversion" - bl_idname = "scene.scs_conv_hlpr_run" + bl_idname = "scene.scs_tools_run_conversion" bl_description = "Dry run of conversion tools without managing extra_mount.txt file (Should not be used from UI)" def execute(self, context): @@ -1040,9 +978,9 @@ def execute(self, context): return {'FINISHED'} - class ConvertCurrentBase(bpy.types.Operator): + class SCS_TOOLS_OT_ConvertCurrentBase(bpy.types.Operator): bl_label = "Convert Current Base" - bl_idname = "scene.scs_conv_hlpr_convert_current" + bl_idname = "scene.scs_tools_convert_current_base" bl_description = "Converts current SCS Base project (the one which is currently used by SCS Blender Tools) to binary files ready for packing." def execute(self, context): @@ -1066,14 +1004,14 @@ def execute(self, context): with open(extra_mount_path, mode="w", encoding="utf8") as f: f.write(link_hash) - return ConversionHelper.RunConversion.execute(self, context) + return ConversionHelper.SCS_TOOLS_OT_RunConversion.execute(self, context) - class ConvertCustomPaths(bpy.types.Operator): + class SCS_TOOLS_OT_ConvertCustomPaths(bpy.types.Operator): bl_label = "Convert Custom Paths" - bl_idname = "scene.scs_conv_hlpr_convert_custom" + bl_idname = "scene.scs_tools_convert_custom_paths" bl_description = "Converts all paths given in Custom Paths list (order is the same as they appear in the list)" - include_current_project = BoolProperty( + include_current_project: BoolProperty( default=False ) @@ -1120,11 +1058,11 @@ def execute(self, context): else: self.report({'WARNING'}, "None existing SCS Project Base Path detected, ignoring it!") - return ConversionHelper.RunConversion.execute(self, context) + return ConversionHelper.SCS_TOOLS_OT_RunConversion.execute(self, context) - class ConvertAllPaths(bpy.types.Operator): + class SCS_TOOLS_OT_ConvertAllPaths(bpy.types.Operator): bl_label = "Convert All" - bl_idname = "scene.scs_conv_hlpr_convert_all" + bl_idname = "scene.scs_tools_convert_all_paths" bl_description = "Converts all paths given in Custom Paths list + current SCS Project Base" def __init__(self): @@ -1135,14 +1073,14 @@ def poll(cls, context): return len(_get_scs_globals().conv_hlpr_custom_paths) > 0 def execute(self, context): - return ConversionHelper.ConvertCustomPaths.execute(self, context) + return ConversionHelper.SCS_TOOLS_OT_ConvertCustomPaths.execute(self, context) - class FindGameModFolder(bpy.types.Operator): + class SCS_TOOLS_OT_FindGameModFolder(bpy.types.Operator): bl_label = "Search SCS Game 'mod' Folder" - bl_idname = "scene.scs_conv_hlpr_find_mod_folder" + bl_idname = "scene.scs_tools_find_game_mod_folder" bl_description = "Search for given SCS game 'mod' folder and set it as mod destination" - game = EnumProperty( + game: EnumProperty( items=( ("Euro Truck Simulator 2", "Euro Truck Simulator 2", ""), ("American Truck Simulator", "American Truck Simulator", "") @@ -1231,9 +1169,9 @@ def draw(self, context): layout.prop(self, "game", expand=True) - class RunPacking(bpy.types.Operator): + class SCS_TOOLS_OT_RunPacking(bpy.types.Operator): bl_label = "Run Packing" - bl_idname = "scene.scs_conv_hlpr_pack" + bl_idname = "scene.scs_tools_run_packing" bl_description = "Pack converted sources to mod package and copy it to mod destination path.\n" \ "Depending on auto settings this operator will also execute clean, export and convert before packing." @@ -1272,7 +1210,7 @@ def execute(self, context): if scs_globals.conv_hlpr_clean_on_packing: try: - bpy.ops.scene.scs_conv_hlpr_clean_rsrc() + bpy.ops.scene.scs_tools_clean_conversion_rsrc() except RuntimeError as e: self.report({'ERROR'}, e.args[0]) return {'CANCELLED'} @@ -1280,7 +1218,7 @@ def execute(self, context): if scs_globals.conv_hlpr_export_on_packing: try: - bpy.ops.export_mesh.pim() + bpy.ops.scs_tools.export_pim() except RuntimeError as e: self.report({'ERROR'}, e.args[0]) return {'CANCELLED'} @@ -1289,9 +1227,9 @@ def execute(self, context): try: if scs_globals.conv_hlpr_use_custom_paths and len(scs_globals.conv_hlpr_custom_paths) > 0: - bpy.ops.scene.scs_conv_hlpr_convert_custom(include_current_project=True) + bpy.ops.scene.scs_tools_convert_custom_paths(include_current_project=True) else: - bpy.ops.scene.scs_conv_hlpr_convert_current() + bpy.ops.scene.scs_tools_convert_current_base() except RuntimeError as e: self.report({'ERROR'}, e.args[0]) return {'CANCELLED'} @@ -1383,27 +1321,15 @@ class Log: Wraper class for better navigation in file """ - class CopyLogToClipboard(bpy.types.Operator): + class SCS_TOOLS_OT_CopyLogToClipboard(bpy.types.Operator): bl_label = "Copy BT Log To Clipboard" - bl_idname = "scene.scs_copy_log" + bl_idname = "scene.scs_tools_copy_log_to_clipboard" bl_description = "Copies whole Blender Tools log to clipboard (log was captured since Blender startup)." def execute(self, context): from io_scs_tools.utils.printout import get_log - text = bpy.data.texts.new("SCS BT Log") - - override = { - 'window': bpy.context.window, - 'region': None, - 'area': None, - 'edit_text': text, - } - bpy.ops.text.insert(override, text=get_log()) - bpy.ops.text.select_all(override) - bpy.ops.text.copy(override) - - bpy.data.texts.remove(text, do_unlink=True) + context.window_manager.clipboard = get_log() self.report({'INFO'}, "Blender Tools log copied to clipboard!") return {'FINISHED'} @@ -1414,23 +1340,23 @@ class PaintjobTools: Wrapper class for better navigation in file """ - class ImportFromDataSII(bpy.types.Operator): + class SCS_TOOLS_OT_ImportFromDataSII(bpy.types.Operator): bl_label = "Import SCS Vehicle From data.sii" - bl_idname = "scene.scs_import_from_data_sii" + bl_idname = "scene.scs_tools_import_from_data_sii" 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( + directory: StringProperty( name="Import Vehicle", subtype='DIR_PATH', ) - filepath = StringProperty( + filepath: StringProperty( name="Vehicle 'data.sii' filepath", description="File path to vehicle 'data.sii", subtype='FILE_PATH', ) - filter_glob = StringProperty(default="*.sii", options={'HIDDEN'}) + filter_glob: StringProperty(default="*.sii", options={'HIDDEN'}) vehicle_type = _PT_consts.VehicleTypes.NONE start_time = None # saving start time when initialize is called @@ -1500,6 +1426,7 @@ def gather_model_paths(dir_path, unit_type, one_of_props): @staticmethod def import_and_clean_model(context, project_path, model_path): """Imports model from given model absolute path and removes all useless none paintable stuff. + If PIT file from given model doesn't have any truckpaint material, nothing is imported and None is returned. If no mesh remains in the model after cleaning, whole SCS Object is removed and None is returned. :param context: blender context used in PIX importing @@ -1512,6 +1439,29 @@ def import_and_clean_model(context, project_path, model_path): :rtype: bpy.types.Object """ + # ignore models without pit + if not os.path.isfile(model_path + ".pit"): + return None + + # load pit to search for truckpaint + pit_container = _pix_container.get_data_from_file(model_path + ".pit", ' ' * 4) + look = None + for section in pit_container: + if section.type == "Look": + look = section + break + + # ignore models with invalid pit (no look = invalid pit) + if not look: + return None + + # ignore models without truckpaint material + for mat_sec in look.get_sections("Material"): + if "eut2.truckpaint" in mat_sec.get_prop_value("Effect"): + break + else: # no truckpaint found, ignore this model + return None + # internally change project path for the sake of texture loading, path will be reset in finalize method call _get_scs_globals()["scs_project_path"] = project_path @@ -1521,7 +1471,6 @@ def import_and_clean_model(context, project_path, model_path): _get_scs_globals().import_in_progress = False curr_scs_root = context.active_object - curr_scs_root.hide = True # remove useless stuff (none truckpaint meshes & all locators except model locators without hookup) mesh_obj_count = 0 @@ -1552,8 +1501,6 @@ def import_and_clean_model(context, project_path, model_path): if remove_object: bpy.data.objects.remove(obj, do_unlink=True) - else: - obj.hide = True # if no mesh has left inside the model, then remove everything if removed_mesh_obj_count >= mesh_obj_count: @@ -1565,8 +1512,8 @@ def import_and_clean_model(context, project_path, model_path): return curr_scs_root @staticmethod - 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. + def add_model_to_collection(scs_root, model_type, model_name, linked_to_defs=set()): + """Adds model to collection so it can be distinguished amongs all other models. :param scs_root: blender object representing SCS Root :type scs_root: bpy.types.Object @@ -1591,12 +1538,12 @@ def add_model_to_group(scs_root, model_type, model_name, linked_to_defs=set()): 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 + # create collections 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 + # do not create collections for unused variants if variant_name not in used_variants_by_linked_defs: continue @@ -1604,40 +1551,31 @@ def add_model_to_group(scs_root, model_type, model_name, linked_to_defs=set()): override = bpy.context.copy() 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) + bpy.ops.object.scs_tools_de_select_objects_with_variant(override, select_type=_OP_consts.SelectionType.select, variant_index=i) - group_name = model_type + " | " + model_name + " | " + variant_name - group = bpy.data.groups.new(group_name) + collection_name = model_type + " | " + model_name + " | " + variant_name + collection = bpy.data.collections.new(collection_name) mesh_objects_count = 0 for obj in scs_root.children: - if not obj.select: + if not obj.select_get(): continue if obj.type == "MESH": mesh_objects_count += 1 - override = bpy.context.copy() - override['object'] = obj - bpy.ops.object.group_link(override, group=group.name) + collection.objects.link(obj) - # do not create groups for variant if no mesh objects inside + # do not create collections for variant if no mesh objects inside if mesh_objects_count <= 0: - bpy.data.groups.remove(group, do_unlink=True) + bpy.data.collections.remove(collection, 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) - obj.scs_props.object_identity = obj.name - obj.location = (0.0, 0.0, 0.0) - obj.use_fake_user = True # as we don't link object to the scene (we don't want user to interfere with it somehow) - obj.hide = True # as all groups are hidden by default make sure to hide object (this prevents group to get exported accidentally) + collection[_PT_consts.model_variant_prop] = variant_name + collection[_PT_consts.model_refs_to_sii] = list(linked_to_defs) - override = bpy.context.copy() - override['object'] = obj - bpy.ops.object.group_link(override, group=group.name) + # create new layer collection to make our collection visible in outliner + bpy.context.view_layer.layer_collection.collection.children.link(collection) @staticmethod def update_model_paths_dict(models_paths_dict, curr_models): @@ -1767,7 +1705,7 @@ def execute(self, context): lprint("S Vehicle Paths:\n%r" % vehicle_model_paths) - # import and properly group imported models + # import and properly collection 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_vehicle_models = set() # set of model paths found in multiple projects (for reporting purposes) @@ -1802,11 +1740,11 @@ 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_vehicle_scs_root, - self.vehicle_type, - os.path.basename(vehicle_model_path), - vehicle_model_paths[vehicle_model_path]) + # put imported model into it's own collections per variant + self.add_model_to_collection(curr_vehicle_scs_root, + self.vehicle_type, + os.path.basename(vehicle_model_path), + vehicle_model_paths[vehicle_model_path]) # if none vehicle models were properly imported it makes no sense to go forward on upgrades if len(already_imported) <= 0: @@ -1850,7 +1788,7 @@ def execute(self, context): 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 + # import models, position them properly and put them to collections already_imported = set() # set holding imported path of already imported model, to avoid double importing multiple_project_upgrade_models = set() # set of model paths found in multiple projects (for reporting purposes) for project_path in project_paths: @@ -1876,14 +1814,14 @@ def execute(self, context): # import model curr_upgrade_scs_root = self.import_and_clean_model(context, project_path, model_path) - if curr_upgrade_scs_root is None: # everything was removed, so prevent group creation etc... + if curr_upgrade_scs_root is None: # everything was removed, so prevent collection creation etc... 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), - upgrade_model_paths[upgrade_type][upgrade_model_path]) + # put imported model into it's own collections + self.add_model_to_collection(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 upgrade_locator = None @@ -1893,14 +1831,14 @@ def execute(self, context): continue # Now we are trying to find "perfect" match, which is found, - # when matched prefixed upgrade locator is also assigned to at least one group. + # when matched prefixed upgrade locator is also assigned to at least one collection. # This way we eliminate locators that are in variants # 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: upgrade_locator = possible_upgrade_locators[locator_name] - elif len(possible_upgrade_locators[locator_name].users_group) > 0: + elif len(possible_upgrade_locators[locator_name].users_collection) > 0: upgrade_locator = possible_upgrade_locators[locator_name] break @@ -1914,6 +1852,24 @@ def execute(self, context): curr_upgrade_scs_root.rotation_euler = (0,) * 3 curr_upgrade_scs_root.parent = upgrade_locator + ################################## + # + # 4. cleanup the scene and report problems + # + ################################## + + # remove any layer collection not created by us + cols_to_unlink = set() + for col in bpy.context.scene.collection.children: + if _PT_consts.model_refs_to_sii not in col: + cols_to_unlink.add(col) + for col in cols_to_unlink: + bpy.context.scene.collection.children.unlink(col) + + # make our layer collections hidden, as user should later select what to export + for layer_col in bpy.context.view_layer.layer_collection.children: + layer_col.hide_viewport = True + # on the end report multiple project model problems 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 " @@ -1935,50 +1891,50 @@ def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} - class ExportUVLayoutAndMesh(bpy.types.Operator): + class SCS_TOOLS_OT_ExportPaintjobUVLayoutAndMesh(bpy.types.Operator): bl_label = "Export SCS Paintjob UV Layout & Mesh" - bl_idname = "scene.scs_export_paintjob_uv_layout_and_mesh" + bl_idname = "scene.scs_tools_export_paintjob_uv_layout_and_mesh" bl_description = "Exports painjtob uv layout & mesh (OBJ) for currently visible objects in scene." bl_options = {'PRESET'} - directory = StringProperty( + directory: StringProperty( name="Export UV", subtype='DIR_PATH', ) - filepath = StringProperty( + filepath: StringProperty( name="Export UVs & mesh", description="File path to export paintjob uv layout & mesh too.", subtype='FILE_PATH', ) - config_meta_filepath = StringProperty( + config_meta_filepath: StringProperty( description="File path to paintjob configuration SII file." ) - layout_sii_selection_mode = BoolProperty( + layout_sii_selection_mode: BoolProperty( default=False, description="Use currently selected file as paintjob layout configuration file." ) - export_2nd_uvs = BoolProperty( + export_2nd_uvs: BoolProperty( name="Export 2nd UVs", description="Should 2nd UV set layout be exported?", default=True ) - export_3rd_uvs = BoolProperty( + export_3rd_uvs: BoolProperty( name="Export 3rd UVs", description="Should 3rd UV set layout be exported?", default=True ) - export_id_mask = BoolProperty( + export_id_mask: BoolProperty( name="Export ID Mask", description="Should be id mask marking texture portions be exported?", default=True ) - id_mask_alpha = FloatProperty( + 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.)", @@ -1987,7 +1943,7 @@ class ExportUVLayoutAndMesh(bpy.types.Operator): max=1.0 ) - export_mesh = BoolProperty( + export_mesh: BoolProperty( name="Export Mesh as OBJ", description="Should OBJ mesh also be exported?", default=True @@ -2072,9 +2028,14 @@ def cleanup_meshes(): """Cleanups any meshes with zero users that might be left-overs from join operator. """ + meshes_to_remove = [] for m in bpy.data.meshes: if m.users == 0: - bpy.data.meshes.remove(m) + meshes_to_remove.append(m.name) + + for mesh_name in meshes_to_remove: + if mesh_name in bpy.data.meshes: + bpy.data.meshes.remove(bpy.data.meshes[mesh_name]) def check(self, context): @@ -2088,13 +2049,13 @@ def draw(self, context): col = self.layout.column(align=True) - col.label("Paintjobs Layout META File:", icon='FILE_SCRIPT') + col.label(text="Paintjobs Layout META File:", icon='FILE_SCRIPT') col.prop(self, "config_meta_filepath", text="") - col.prop(self, "layout_sii_selection_mode", toggle=True, text="Select Current File from File Browser", icon='SCREEN_BACK') + col.prop(self, "layout_sii_selection_mode", toggle=True, text="Select Current File from File Browser", icon='PASTEDOWN') col.separator() - col.label("What to export?", icon='QUESTION') + col.label(text="What to export?", icon='QUESTION') col.prop(self, "export_2nd_uvs") col.prop(self, "export_3rd_uvs") col.prop(self, "export_id_mask") @@ -2122,57 +2083,52 @@ def execute(self, context): ################################## # - # 1. collect visible mesh & determinate which groups to export + # 1. collect visible mesh & determinate which collections to export # ################################## - visible_groups = [] - for group in bpy.data.groups: + visible_collections = [] + for layer_col in context.view_layer.layer_collection.children: - if _PT_consts.model_refs_to_sii not in group: + if layer_col.hide_viewport: continue - has_hidden_object = False - - # thanks to our dummy export tag object we can simply iterate trough group objects and - # once some object is hidden (either export tag object or any other) - # we decide that this group is not visible thus won't be exported - for obj in group.objects: - if obj.hide: - has_hidden_object = True - break + collection = layer_col.collection - if has_hidden_object: + if _PT_consts.model_refs_to_sii not in collection: continue - visible_groups.append(group) + # unhide all objects that are part of collections to export (in case user hid them for some reason), + # becasue hidden objects can not be selected/duplicated otherwise + for obj in collection.objects: + obj.hide_viewport = False + obj.hide_set(False) + + visible_collections.append(collection) - lprint("S Visible groups to export: %s", (visible_groups,)) + lprint("S Visible collections to export: %s", (visible_collections,)) merged_objects_to_export = {} - for group in visible_groups: + for collection in visible_collections: # start with selection clearing, use our implementation to deselect any possible selected object in hidden layers - for obj in context.scene.objects: - obj.select = False + for obj in context.view_layer.objects: + obj.select_set(False) selected_objects_count = 0 - for obj in group.objects: + for obj in collection.objects: if obj.type == "MESH": - obj.select = True + obj.select_set(True) selected_objects_count += 1 - # in case no mesh objects in this group, - # there is no data to be exported, so advance to next group + # in case no mesh objects in this collection, there is no data to be exported, so advance to next collection if selected_objects_count <= 0: continue bpy.ops.object.duplicate() if selected_objects_count > 1: - context.scene.objects.active = context.selected_objects[0] - override = context.copy() - override["selected_objects"] = context.selected_objects - bpy.ops.object.join(override) + context.view_layer.objects.active = context.selected_objects[0] + bpy.ops.object.join() self.cleanup_meshes() curr_merged_object = context.selected_objects[0] @@ -2183,7 +2139,7 @@ def execute(self, context): break if curr_truckpaint_mat is None: - self.do_report({'WARNING'}, "Group %r won't be exported as 'truckpaint' material wasn't found!" % group.name) + self.do_report({'WARNING'}, "Collection %r won't be exported as 'truckpaint' material wasn't found!" % collection.name) self.cleanup((curr_merged_object,)) continue @@ -2193,19 +2149,19 @@ def execute(self, context): curr_truckpaint_mat.scs_props.shader_texture_base_uv[1].value, curr_truckpaint_mat.scs_props.shader_texture_base_uv[2].value) - # remove all none needed & colliding data-blocks from object: materials, groups + # remove all none needed & colliding data-blocks from object: materials, collections while len(curr_merged_object.material_slots) > 0: override = context.copy() override["object"] = curr_merged_object bpy.ops.object.material_slot_remove(override) - while len(curr_merged_object.users_group) > 0: - override = context.copy() - override["object"] = curr_merged_object - override["group"] = curr_merged_object.users_group[0] - bpy.ops.object.group_remove(override) + while len(curr_merged_object.users_collection) > 0: + curr_merged_object.users_collection[0].objects.unlink(curr_merged_object) + + # linke merged objects to scene master collection so they can be handled properly by selection operators + context.view_layer.layer_collection.collection.objects.link(curr_merged_object) - merged_objects_to_export[curr_merged_object] = group + merged_objects_to_export[curr_merged_object] = collection if len(merged_objects_to_export) <= 0: self.do_report({'ERROR'}, "No objects to export!") @@ -2278,7 +2234,7 @@ def execute(self, context): unconfigured_objects = [] for obj in merged_objects_to_export: - group = merged_objects_to_export[obj] + collection = merged_objects_to_export[obj] # find texture portion belonging to export object texture_portion = None @@ -2290,7 +2246,7 @@ def execute(self, context): if not model_sii: continue - for reference_to_sii in group[_PT_consts.model_refs_to_sii]: + for reference_to_sii in collection[_PT_consts.model_refs_to_sii]: # yep we found possible sii of the model, but not quite yet if reference_to_sii.endswith(model_sii): @@ -2304,7 +2260,7 @@ def execute(self, context): 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[_PT_consts.model_variant_prop]: + if variant == collection[_PT_consts.model_variant_prop]: texture_portion = texture_portions[unit_id] break @@ -2313,12 +2269,12 @@ def execute(self, context): if not texture_portion: # texture portion not found help the user! referenced_siis = "" - for referenced_sii in sorted(group[_PT_consts.model_refs_to_sii]): + for referenced_sii in sorted(collection[_PT_consts.model_refs_to_sii]): referenced_siis += "-> %r\n\t " % referenced_sii 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)) + "SII files from which model was referenced:\n\t %s" % (collection.name, referenced_siis)) unconfigured_objects.append(obj) continue @@ -2333,17 +2289,24 @@ def execute(self, context): independent_export_objects[obj] = texture_portion # as last get trough objects with parent & put them in proper dictionary assigning PARENT texture portion already + parent_texture_portion = texture_portion while parent: - texture_portion = _sii_container.get_unit_by_id(pj_config_sii_container, parent, texture_portion.type) - parent = texture_portion.get_prop("parent") + parent_texture_portion = _sii_container.get_unit_by_id(pj_config_sii_container, parent, parent_texture_portion.type) + parent = parent_texture_portion.get_prop("parent") - if bool(texture_portion.get_prop("is_master")): + if bool(parent_texture_portion.get_prop("is_master")): master_child_export_objects[obj] = texture_portion else: - independent_export_objects[obj] = texture_portion # even if it has parent it's exported independent; no duplicates needed + independent_export_objects[obj] = parent_texture_portion # even if it has parent it's exported independent; no duplicates needed # cleanup unconfigured objects if len(unconfigured_objects) > 0: + + # make sure to remove uncofigured objects from merged, + # so that cleanup won't be done twice and possibly result in ReferenceError + for obj in unconfigured_objects: + del merged_objects_to_export[obj] + self.cleanup(unconfigured_objects) # nonsense to go further if nothing to export @@ -2363,21 +2326,25 @@ def execute(self, context): # duplicate all objects with master parent & merge them for obj in master_child_export_objects: + export_uvs_to = master_child_export_objects[obj].get_prop("export_uvs_to") for master_obj in master_export_objects: + # if there is extra field defining where we should export child of a master + # then ignore export for other master portions + if export_uvs_to and master_export_objects[master_obj].id not in export_uvs_to: + continue + bpy.ops.object.select_all(action="DESELECT") # duplicate - obj.select = True + obj.select_set(True) bpy.ops.object.duplicate() # merge with master object - master_obj.select = True - context.scene.objects.active = master_obj - override = context.copy() - override["selected_objects"] = context.selected_objects - bpy.ops.object.join(override) + master_obj.select_set(True) + context.view_layer.objects.active = master_obj + bpy.ops.object.join() self.cleanup_meshes() bpy.data.objects.remove(obj, do_unlink=True) @@ -2395,17 +2362,15 @@ def execute(self, context): # select all export objects first bpy.ops.object.select_all(action="DESELECT") for obj in independent_export_objects: - obj.select = True + obj.select_set(True) for obj in master_export_objects: - obj.select = True + obj.select_set(True) # merge them final_merged_object = context.selected_objects[0] if len(merged_objects_to_export) > 1: - context.scene.objects.active = context.selected_objects[0] - override = context.copy() - override["selected_objects"] = context.selected_objects - bpy.ops.object.join(override) + context.view_layer.objects.active = context.selected_objects[0] + bpy.ops.object.join() self.cleanup_meshes() ################################## @@ -2419,7 +2384,7 @@ def execute(self, context): if self.export_2nd_uvs: # set active uv layer so export will take proper - final_merged_object.data.uv_textures.active = final_merged_object.data.uv_textures[_PT_consts.uvs_name_2nd] + final_merged_object.data.uv_layers.active = final_merged_object.data.uv_layers[_PT_consts.uvs_name_2nd] override = context.copy() override["active_object"] = final_merged_object @@ -2442,7 +2407,7 @@ def execute(self, context): if self.export_3rd_uvs: # set active uv layer so export will take proper - final_merged_object.data.uv_textures.active = final_merged_object.data.uv_textures[_PT_consts.uvs_name_3rd] + final_merged_object.data.uv_layers.active = final_merged_object.data.uv_layers[_PT_consts.uvs_name_3rd] override = context.copy() override["active_object"] = final_merged_object @@ -2519,14 +2484,14 @@ def execute(self, context): # 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.alpha_mode = 'CHANNEL_PACKED' 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) + img.save_render(self.filepath + ".id_mask.png", scene=bpy.context.scene) # remove image data-block, as we don't need it anymore img.buffers_free() @@ -2542,9 +2507,9 @@ def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} - class GeneratePaintjob(bpy.types.Operator): + class SCS_TOOLS_OT_GeneratePaintjob(bpy.types.Operator): bl_label = "Generate SCS Paintjob From Common Texture" - bl_idname = "scene.scs_generate_paintjob" + bl_idname = "scene.scs_tools_generate_paintjob" bl_description = "Generates complete setup for given paintjob: definitions, TGAs & TOBJs." bl_options = {'INTERNAL'} @@ -2668,72 +2633,72 @@ def export_to_sii(self, op_inst, config_path): translate_node = None viewer_node = None - config_meta_filepath = StringProperty( + config_meta_filepath: StringProperty( description="File path to paintjob configuration SII file." ) - project_path = StringProperty( + project_path: StringProperty( description="Project to which this paintjob belongs. Could be usefull if PSD file is not within same project." ) - common_texture_path = StringProperty( + common_texture_path: StringProperty( description="File path to original common paintjob TGA texture." ) - export_alpha = BoolProperty( + export_alpha: BoolProperty( description="Flag defining if textures shall be exported with alpha or not." ) - preserve_common_texture = BoolProperty( + preserve_common_texture: BoolProperty( description="Should given common texture TGA be preserved and not deleted after generation is finished?" ) - optimize_single_color_textures = BoolProperty( + optimize_single_color_textures: BoolProperty( description="Export texture with size 4x4 if whole exported texture has all pixels with same color?" ) - export_configs_only = BoolProperty( + export_configs_only: BoolProperty( description="Should only configurations be exported (used for export of metallic like paintjobs without paintjob texture)?" ) # paint job settings exported to common SUI settings file - pjs_name = StringProperty(default="pj_name") - pjs_price = IntProperty(default=10000) - pjs_unlock = IntProperty(default=0) - pjs_icon = StringProperty(default="") + pjs_name: StringProperty(default="pj_name") + pjs_price: IntProperty(default=10000) + pjs_unlock: IntProperty(default=0) + pjs_icon: StringProperty(default="") - pjs_paint_job_mask = StringProperty(default="") + pjs_paint_job_mask: StringProperty(default="") - pjs_mask_r_color = FloatVectorProperty(default=(1, 0, 0)) - pjs_mask_r_locked = BoolProperty(default=True) + pjs_mask_r_color: FloatVectorProperty(default=(1, 0, 0)) + pjs_mask_r_locked: BoolProperty(default=True) - pjs_mask_g_color = FloatVectorProperty(default=(0, 1, 0)) - pjs_mask_g_locked = BoolProperty(default=True) + pjs_mask_g_color: FloatVectorProperty(default=(0, 1, 0)) + pjs_mask_g_locked: BoolProperty(default=True) - pjs_mask_b_color = FloatVectorProperty(default=(0, 0, 1)) - pjs_mask_b_locked = BoolProperty(default=True) + pjs_mask_b_color: FloatVectorProperty(default=(0, 0, 1)) + pjs_mask_b_locked: BoolProperty(default=True) - pjs_base_color = FloatVectorProperty(default=(1, 1, 1)) - pjs_base_color_locked = BoolProperty(default=True) + pjs_base_color: FloatVectorProperty(default=(1, 1, 1)) + pjs_base_color_locked: BoolProperty(default=True) - pjs_flip_color = FloatVectorProperty(default=(1, 0, 0)) - pjs_flip_color_locked = BoolProperty(default=True) + pjs_flip_color: FloatVectorProperty(default=(1, 0, 0)) + pjs_flip_color_locked: BoolProperty(default=True) - pjs_flip_strength = FloatProperty(default=0.27) + pjs_flip_strength: FloatProperty(default=0.27) - pjs_flake_color = FloatVectorProperty(default=(0, 1, 0)) - pjs_flake_color_locked = BoolProperty(default=True) + pjs_flake_color: FloatVectorProperty(default=(0, 1, 0)) + pjs_flake_color_locked: BoolProperty(default=True) - pjs_flake_uvscale = FloatProperty(default=32.0) - pjs_flake_vratio = FloatProperty(default=1.0) - pjs_flake_density = FloatProperty(default=1.0) - pjs_flake_shininess = FloatProperty(default=50.0) - pjs_flake_clearcoat_rolloff = FloatProperty(default=2.2) - pjs_flake_noise = StringProperty(default="/material/custom/flake_noise.tobj") + pjs_flake_uvscale: FloatProperty(default=32.0) + pjs_flake_vratio: FloatProperty(default=1.0) + pjs_flake_density: FloatProperty(default=1.0) + pjs_flake_shininess: FloatProperty(default=50.0) + pjs_flake_clearcoat_rolloff: FloatProperty(default=2.2) + pjs_flake_noise: StringProperty(default="/material/custom/flake_noise.tobj") - pjs_alternate_uvset = BoolProperty(default=False) - pjs_flipflake = BoolProperty(default=False) - pjs_airbrush = BoolProperty(default=False) + pjs_alternate_uvset: BoolProperty(default=False) + pjs_flipflake: BoolProperty(default=False) + pjs_airbrush: BoolProperty(default=False) @staticmethod def do_report(the_type, message, do_report=False): @@ -2844,15 +2809,25 @@ def export_texture(self, orig_img, tgas_dir_path, texture_portion): img_height) tga_path = os.path.join(tgas_dir_path, tga_name) - # export texture portion image by rendering compositor + # export texture portion image by properly reset scene settings and then render with compositor scene = bpy.context.scene + + scene.display_settings.display_device = "sRGB" + + scene.view_settings.view_transform = "Standard" + scene.view_settings.look = "None" + scene.view_settings.exposure = 0 + scene.view_settings.gamma = 1 + 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 + scene.render.dither_intensity = 0 + bpy.ops.render.render(write_still=True) # if no optimization or is master then we can skip optimization processing, @@ -3013,9 +2988,9 @@ def export_settings_sui(self, settings_sui_path): unit.props["unlock"] = self.pjs_unlock # now go trough all props and export the ones that are different from default value - for object_dir_entry in dir(self): - if object_dir_entry.startswith("pjs_"): - assert self.append_prop_if_not_default(unit, object_dir_entry) + for props_dir_entry in dir(self.properties): + if props_dir_entry.startswith("pjs_"): + assert self.append_prop_if_not_default(unit, props_dir_entry) return _sii_container.write_data_to_file(settings_sui_path, (unit,), is_sui=True, create_dirs=True) @@ -3036,12 +3011,12 @@ def append_prop_if_not_default(self, unit, prop_name, prop_value=None): _EPSILON = 0.0001 # float values max difference to be still equal _PJS_PREFIX = "pjs_" # prefix that marks setting as paint job setting - if not hasattr(PaintjobTools.GeneratePaintjob, prop_name): + if not hasattr(self, prop_name): lprint("E Invalid property for paintjob settings: %r, contact the developer!", (prop_name,)) return False # gather values - default_value = _get_bpy_prop(getattr(PaintjobTools.GeneratePaintjob, prop_name)) + default_value = _get_default(self.properties, prop_name) if prop_value is None: current_value = getattr(self, prop_name) @@ -3263,7 +3238,8 @@ 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 + common_tex_img.colorspace_settings.name = "sRGB" + common_tex_img.alpha_mode = 'STRAIGHT' if self.export_alpha else 'NONE' self.initialize_nodes(context, common_tex_img) @@ -3458,7 +3434,7 @@ def execute(self, context): # 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() + overrides[config_path] = PaintjobTools.SCS_TOOLS_OT_GeneratePaintjob.Overrides() # now add current accessory to overrides overrides[config_path].add_accessory(acc_type_token + "." + acc_id_token, pj_props) @@ -3482,3 +3458,54 @@ def execute(self, context): lprint("\nI Export of paintjobs took: %0.3f sec" % (time() - start_time)) return {'FINISHED'} + + +classes = ( + Animation.SCS_TOOLS_OT_IncreaseAnimationSteps, + Animation.SCS_TOOLS_OT_DecreaseAnimationSteps, + + ConversionHelper.SCS_TOOLS_OT_AddConversionPath, + ConversionHelper.SCS_TOOLS_OT_CleanConversionRSRC, + ConversionHelper.SCS_TOOLS_OT_ConvertAllPaths, + ConversionHelper.SCS_TOOLS_OT_ConvertCurrentBase, + ConversionHelper.SCS_TOOLS_OT_ConvertCustomPaths, + ConversionHelper.SCS_TOOLS_OT_FindGameModFolder, + ConversionHelper.SCS_TOOLS_OT_OrderConversionPath, + ConversionHelper.SCS_TOOLS_OT_RemoveConversionPath, + ConversionHelper.SCS_TOOLS_OT_RunConversion, + ConversionHelper.SCS_TOOLS_OT_RunPacking, + + Export.SCS_TOOLS_OT_ExportAnimAction, + Export.SCS_TOOLS_OT_ExportByScope, + + Import.SCS_TOOLS_OT_ImportAnimActions, + + Log.SCS_TOOLS_OT_CopyLogToClipboard, + + PaintjobTools.SCS_TOOLS_OT_ExportPaintjobUVLayoutAndMesh, + PaintjobTools.SCS_TOOLS_OT_GeneratePaintjob, + PaintjobTools.SCS_TOOLS_OT_ImportFromDataSII, + + Paths.SCS_TOOLS_OT_SelectProjectPath, + Paths.SCS_TOOLS_OT_SelectShaderPresetsPath, + Paths.SCS_TOOLS_OT_SelectTriggerActionsLibPath, + Paths.SCS_TOOLS_OT_SelectSignLibPath, + Paths.SCS_TOOLS_OT_SelectSemaphoreLibPath, + Paths.SCS_TOOLS_OT_SelectTrafficRulesLibPath, + Paths.SCS_TOOLS_OT_SelectHookupLibPath, + Paths.SCS_TOOLS_OT_SelectMatSubsLibPath, + Paths.SCS_TOOLS_OT_SelectSunProfilesLibPath, + Paths.SCS_TOOLS_OT_SelectDirInsideBase, + Paths.SCS_TOOLS_OT_ReloadLibrary, + Paths.SCS_TOOLS_OT_AddPathPreset, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/addon/io_scs_tools/operators/wm.py b/addon/io_scs_tools/operators/wm.py index 986ac5b..bf3ec93 100644 --- a/addon/io_scs_tools/operators/wm.py +++ b/addon/io_scs_tools/operators/wm.py @@ -16,48 +16,48 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2015: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy -from bgl import GL_NEAREST +import os from bpy.props import StringProperty, BoolProperty, IntProperty from io_scs_tools.consts import Operators as _OP_consts from io_scs_tools.utils import view3d as _view3d_utils from io_scs_tools.utils import path as _path_utils -class ShowWarningMessage(bpy.types.Operator): +class SCS_TOOLS_OT_ShowMessageInPopup(bpy.types.Operator): bl_label = "" bl_description = "Click for additional information." - bl_idname = "wm.show_warning_message" + bl_idname = "wm.scs_tools_show_message_in_popup" static_message = "" """Used for saving message inside class to be able to retrieve it on popup draw.""" - is_modal = BoolProperty(default=False) - icon = StringProperty(default="ERROR") - title = StringProperty(default="UNKWOWN") - message = StringProperty(default="NO MESSAGE") - width = IntProperty(default=300) - height = IntProperty(default=20) + is_modal: BoolProperty(default=False) + icon: StringProperty(default="ERROR") + title: StringProperty(default="UNKWOWN") + message: StringProperty(default="NO MESSAGE") + width: IntProperty(default=300) + height: IntProperty(default=20) @staticmethod def popup_draw(self, context): - lines = ShowWarningMessage.static_message.split("\n") + lines = SCS_TOOLS_OT_ShowMessageInPopup.static_message.split("\n") for line in lines: - self.layout.label(line) + self.layout.label(text=line) def draw(self, context): - row = self.layout.row().split(0.00001 * self.width) - row.label(" ") + row = self.layout.row().split(factor=0.00001 * self.width) + row.label(text=" ") col = row.column() - col.label(self.title, icon=self.icon) + col.label(text=self.title, icon=self.icon) col.separator() lines = self.message.split("\n") for line in lines: - col.label(line.strip()) + col.label(text=line.strip()) col.separator() col.separator() @@ -70,7 +70,7 @@ def execute(self, context): def invoke(self, context, event): - ShowWarningMessage.static_message = self.message + SCS_TOOLS_OT_ShowMessageInPopup.static_message = self.message if self.is_modal: return context.window_manager.invoke_props_dialog(self, width=self.width, height=self.height) @@ -78,17 +78,19 @@ def invoke(self, context, event): return self.execute_popup(context) -class Show3DViewReport(bpy.types.Operator): +class SCS_TOOLS_OT_Show3DViewReport(bpy.types.Operator): bl_label = "" bl_description = "Click for additional information." - bl_idname = "wm.show_3dview_report" + bl_idname = "wm.scs_tools_show_3dview_report" __static_is_shown = True """Used for indicating collapsed state of reporter.""" __static_hide_controls = False """Used for indicating controls visibility. Controls can be hidden if user shouldn't be able to abort report operator.""" - __static_running_instances = 0 - """Used for indication of already running operator.""" + __static_main_instance = None + """Used for indication of main instance and abortion of the rest.""" + __static_window_instance = None + """Used for indication on which window instance modal handler is currently added.""" __static_title = "" """Used for saving BT version inside class to be able to retrieve it on open gl draw.""" __static_message_l = [] @@ -99,6 +101,11 @@ class Show3DViewReport(bpy.types.Operator): """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.""" + __static_x_offset = 0 + __static_y_offset = 0 + """Used to designate X and Y offset from corner of drawing region (compensation for header and tools panels dynamic width))""" + __static_is_out_of_bounds = False + """Used for indicating whether all lines could be drawn into the region.""" esc_abort = 0 """Used for staging ESC key press in operator: @@ -107,11 +114,12 @@ class Show3DViewReport(bpy.types.Operator): 2 - ESC was released, ESC is finally captured """ - title = StringProperty(default="") - message = StringProperty(default="") - abort = BoolProperty(default=False) - hide_controls = BoolProperty(default=False) - is_progress_message = BoolProperty(default=False) + title: StringProperty(default="") + message: StringProperty(default="") + abort: BoolProperty(default=False) + hide_controls: BoolProperty(default=False) + is_progress_message: BoolProperty(default=False) + modal_handler_reassign: BoolProperty(default=False) @staticmethod def has_lines(): @@ -120,41 +128,51 @@ def has_lines(): :return: True if there is any lines; False otherwise :rtype: bool """ - return len(Show3DViewReport.__static_message_l) > 0 or len(Show3DViewReport.__static_progress_message_l) > 0 + return len(SCS_TOOLS_OT_Show3DViewReport.__static_message_l) > 0 or len(SCS_TOOLS_OT_Show3DViewReport.__static_progress_message_l) > 0 @staticmethod - def has_controls(): - """Tells if report operator currently has enabled controls. + def has_controls(window): + """Tells if report operator currently has enabled controls and given window is the one having model handler added. + :param window: window on which we should check for controls + :type window: bpy.type.Window :return: True if controls are enabled; False otherwise :rtype: bool """ - return not Show3DViewReport.__static_hide_controls + return window == SCS_TOOLS_OT_Show3DViewReport.__static_window_instance and not SCS_TOOLS_OT_Show3DViewReport.__static_hide_controls @staticmethod - def get_scs_logo_img_bindcode(): + def get_scs_banner_img_data(window): """Loads image to blender data block, loads it to gl memory and gets bindcode address that can be used in bgl module for image drawing. - :return: bindcode of scs bt logo image - :rtype: int + :param window: window for which we should get banner image + :type window: bpy.type.Window + :return: (bindcode of scs banner image, width of scs banner image, height of scs banner image + :rtype: (int, int, int) """ - if _OP_consts.View3DReport.BT_LOGO_IMG_NAME not in bpy.data.images: + if SCS_TOOLS_OT_Show3DViewReport.has_controls(window): + img_name = _OP_consts.View3DReport.BT_BANNER_WITH_CTRLS_IMG_NAME + else: + img_name = _OP_consts.View3DReport.BT_BANNER_IMG_NAME + + if img_name not in bpy.data.images: - img_path = _path_utils.get_addon_installation_paths()[0] + "/ui/icons/" + _OP_consts.View3DReport.BT_LOGO_IMG_NAME + img_path = os.path.join(_path_utils.get_addon_installation_paths()[0], "ui", "banners", img_name) img = bpy.data.images.load(img_path, check_existing=True) + img.colorspace_settings.name = 'Raw' else: - img = bpy.data.images[_OP_consts.View3DReport.BT_LOGO_IMG_NAME] + img = bpy.data.images[img_name] # ensure that image is loaded in GPU memory aka has proper bindcode, - # we have to that each time because if operator is shown for long time blender might free it on his own - if img.bindcode[0] == 0: - img.gl_load(0, GL_NEAREST, GL_NEAREST) + # we have to that each time because if operator is shown for long time blender might free it on it's own + if img.bindcode == 0: + img.gl_load() - return img.bindcode[0] + return img.bindcode, img.size[0], img.size[1] @staticmethod def get_lines(): @@ -164,12 +182,12 @@ def get_lines(): :rtype: list[str] """ lines = [] - lines.extend(Show3DViewReport.__static_progress_message_l) - lines.extend(Show3DViewReport.__static_message_l) + lines.extend(SCS_TOOLS_OT_Show3DViewReport.__static_progress_message_l) + lines.extend(SCS_TOOLS_OT_Show3DViewReport.__static_message_l) # do scrolling - lines_to_scroll = Show3DViewReport.__static_scroll_pos + lines_to_scroll = SCS_TOOLS_OT_Show3DViewReport.__static_scroll_pos while lines_to_scroll > 0: lines.pop(0) lines_to_scroll = lines_to_scroll - 1 @@ -183,7 +201,7 @@ def get_title(): :return: title which should be carrying info about Blender Tools version :rtype: str """ - return Show3DViewReport.__static_title + return SCS_TOOLS_OT_Show3DViewReport.__static_title @staticmethod def is_shown(): @@ -192,7 +210,7 @@ def is_shown(): :return: True if shown; False otherwise :rtype: bool """ - return Show3DViewReport.__static_is_shown + return SCS_TOOLS_OT_Show3DViewReport.__static_is_shown @staticmethod def is_in_btn_area(x, y, btn_area): @@ -207,8 +225,8 @@ def is_in_btn_area(x, y, btn_area): :return: True if x and y are inside button area; False otherwise :rtype: bool """ - return (btn_area[0] < x < btn_area[1] and - btn_area[2] < y < btn_area[3]) + return (btn_area[0] < (x - SCS_TOOLS_OT_Show3DViewReport.__static_x_offset) < btn_area[1] and + btn_area[2] < (y - SCS_TOOLS_OT_Show3DViewReport.__static_y_offset) < btn_area[3]) @staticmethod def is_scrolled(): @@ -217,32 +235,117 @@ def is_scrolled(): :return: True if we are not on zero scroll position; False otherwise :rtype: bool """ - return Show3DViewReport.__static_scroll_pos != 0 + return SCS_TOOLS_OT_Show3DViewReport.__static_scroll_pos != 0 or SCS_TOOLS_OT_Show3DViewReport.__static_is_out_of_bounds + + @staticmethod + def set_out_of_bounds(state): + """Sets state of the 3D view drawing buffer. If set to true it means, that not all lines could be displayed. + + :param state: True if not all lines could be displayed; False otherwise + :type state: bool + """ + SCS_TOOLS_OT_Show3DViewReport.__static_is_out_of_bounds = state + + @staticmethod + def set_btns_xy_offset(x, y): + """Sets offset for controls area inclusion calculation in is_in_btn_area method. + + :param x: X offset in pixels designating left margin for drawing of our controls + :type x: int + :param y: Y offset in pixels designating top margin for drawing of our controls + :type y: int + """ + SCS_TOOLS_OT_Show3DViewReport.__static_x_offset = x + SCS_TOOLS_OT_Show3DViewReport.__static_y_offset = y - def __init__(self): - Show3DViewReport.__static_running_instances += 1 + @staticmethod + def discard_drawing_data(): + """Discards drawing by cleaning static messages and removing banner image. + """ + + # free BT logo image resources + if _OP_consts.View3DReport.BT_BANNER_IMG_NAME in bpy.data.images: + + img = bpy.data.images[_OP_consts.View3DReport.BT_BANNER_IMG_NAME] + img.gl_free() - def __del__(self): - if Show3DViewReport.__static_running_instances > 0: + bpy.data.images.remove(img, do_unlink=True) + + SCS_TOOLS_OT_Show3DViewReport.__static_title = "" + SCS_TOOLS_OT_Show3DViewReport.__static_message_l.clear() + SCS_TOOLS_OT_Show3DViewReport.__static_progress_message_l.clear() - Show3DViewReport.__static_running_instances -= 1 + # trigger redraw so 3d view reports will be removed + _view3d_utils.tag_redraw_all_view3d() - # if user disables add-on, destructor is called again, so cleanup static variables - if Show3DViewReport.__static_running_instances <= 0: + @classmethod + def unregister(cls): + """Called on unregister of class. + """ + SCS_TOOLS_OT_Show3DViewReport.discard_drawing_data() - Show3DViewReport.__static_title = "" - Show3DViewReport.__static_message_l.clear() - Show3DViewReport.__static_progress_message_l.clear() + def fill_drawing_data(self): + """Fills drawing data into static variables, so they can be used by drawing methods of open gl. + """ + + # reset flags in static variables + SCS_TOOLS_OT_Show3DViewReport.__static_is_shown = True + SCS_TOOLS_OT_Show3DViewReport.__static_hide_controls = self.hide_controls + SCS_TOOLS_OT_Show3DViewReport.__static_scroll_pos = 0 + + # assign title to static variable + SCS_TOOLS_OT_Show3DViewReport.__static_title = self.title + + # split message by new lines + message_l = [] + for line in self.message.split("\n"): + + # remove tabulator simulated new lines from warnings and errors, written like: "\n\t " + line = line.replace("\t ", " " * 4) + + # remove tabulator simulated empty space before warning or error line of summaries e.g "\t > " + line = line.replace("\t ", "") + + message_l.append(line) + + # properly assign parsed message list depending on progress flag + if self.is_progress_message: + SCS_TOOLS_OT_Show3DViewReport.__static_progress_message_l = message_l + else: + SCS_TOOLS_OT_Show3DViewReport.__static_message_l = message_l + + def cancel(self, context): + # find oldest main window for possible re-call of the operator + oldest_main_window = None + if bpy.context.window_manager: + for window in bpy.context.window_manager.windows: + if not window.parent: + oldest_main_window = window + break + + # when operator get's cancelled, either user closed blender or window itself, in which our operator was handled. + # Thus make sure to re-call on exisiting oldest main window, so that 3d view opearator "reopens" + if oldest_main_window and SCS_TOOLS_OT_Show3DViewReport.__static_window_instance != oldest_main_window: + override = bpy.context.copy() + override["window"] = oldest_main_window + bpy.ops.wm.scs_tools_show_3dview_report(override, 'INVOKE_DEFAULT', modal_handler_reassign=True) + _view3d_utils.tag_redraw_all_view3d() + else: + SCS_TOOLS_OT_Show3DViewReport.discard_drawing_data() def modal(self, context, event): + # if not main instance finish + if SCS_TOOLS_OT_Show3DViewReport.__static_main_instance != self: + return {'FINISHED'} + # if global abort was requested finish this modal operator instance - if Show3DViewReport.__static_abort: + if SCS_TOOLS_OT_Show3DViewReport.__static_abort: return {'FINISHED'} # if operator doesn't have controls, then it can not be cancelled by user, # so we should simply pass trough - if not Show3DViewReport.has_controls(): + if not SCS_TOOLS_OT_Show3DViewReport.has_controls(SCS_TOOLS_OT_Show3DViewReport.__static_window_instance): return {'PASS_THROUGH'} # handle ESC press @@ -279,116 +382,90 @@ def modal(self, context, event): # NOTE: there is two stage exit on ESC because user might hit ESC unintentionally. # Plus in case some transformation was in progress (like translation) ESC will cancel it and # in worst case only hide 3d view logging operator, if stage ESC handling fails to capture that - if Show3DViewReport.__static_is_shown: + if SCS_TOOLS_OT_Show3DViewReport.__static_is_shown: - Show3DViewReport.__static_is_shown = False + SCS_TOOLS_OT_Show3DViewReport.__static_is_shown = False _view3d_utils.tag_redraw_all_view3d() return {'RUNNING_MODAL'} else: - self.cancel(context) + SCS_TOOLS_OT_Show3DViewReport.discard_drawing_data() return {'FINISHED'} # also exit/cancel operator if Close button area was clicked - if Show3DViewReport.is_in_btn_area(curr_x, curr_y, _OP_consts.View3DReport.CLOSE_BTN_AREA): # close - self.cancel(context) + if SCS_TOOLS_OT_Show3DViewReport.is_in_btn_area(curr_x, curr_y, _OP_consts.View3DReport.CLOSE_BTN_AREA): # close + SCS_TOOLS_OT_Show3DViewReport.discard_drawing_data() return {'FINISHED'} - if Show3DViewReport.is_in_btn_area(curr_x, curr_y, _OP_consts.View3DReport.HIDE_BTN_AREA): # show/hide + if SCS_TOOLS_OT_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 + SCS_TOOLS_OT_Show3DViewReport.__static_is_shown = not SCS_TOOLS_OT_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'} + # scroll up/down + if SCS_TOOLS_OT_Show3DViewReport.is_shown() and SCS_TOOLS_OT_Show3DViewReport.is_scrolled(): - elif Show3DViewReport.is_in_btn_area(curr_x, curr_y, _OP_consts.View3DReport.SCROLLDOWN_BTN_AREA): + if SCS_TOOLS_OT_Show3DViewReport.is_in_btn_area(curr_x, curr_y, _OP_consts.View3DReport.SCROLLUP_BTN_AREA): + new_position = SCS_TOOLS_OT_Show3DViewReport.__static_scroll_pos - 5 + min_position = 0 + SCS_TOOLS_OT_Show3DViewReport.__static_scroll_pos = max(new_position, min_position) + _view3d_utils.tag_redraw_all_view3d() + return {'RUNNING_MODAL'} - Show3DViewReport.__static_scroll_pos = min(Show3DViewReport.__static_scroll_pos + 5, len(Show3DViewReport.__static_message_l)) - _view3d_utils.tag_redraw_all_view3d() - return {'RUNNING_MODAL'} + if SCS_TOOLS_OT_Show3DViewReport.is_in_btn_area(curr_x, curr_y, _OP_consts.View3DReport.SCROLLDOWN_BTN_AREA): + new_position = SCS_TOOLS_OT_Show3DViewReport.__static_scroll_pos + 5 + max_position = ( + len(SCS_TOOLS_OT_Show3DViewReport.__static_message_l) + + len(SCS_TOOLS_OT_Show3DViewReport.__static_progress_message_l) - 1 + ) + SCS_TOOLS_OT_Show3DViewReport.__static_scroll_pos = min(new_position, max_position) + _view3d_utils.tag_redraw_all_view3d() + return {'RUNNING_MODAL'} return {'PASS_THROUGH'} - def cancel(self, context): - - # free BT logo image resources - if _OP_consts.View3DReport.BT_LOGO_IMG_NAME in bpy.data.images: - - img = bpy.data.images[_OP_consts.View3DReport.BT_LOGO_IMG_NAME] - img.gl_free() - - bpy.data.images.remove(img, do_unlink=True) - - Show3DViewReport.__static_message_l.clear() - Show3DViewReport.__static_progress_message_l.clear() - - _view3d_utils.tag_redraw_all_view3d() - def invoke(self, context, event): # propagate abort to all instances trough static variable - Show3DViewReport.__static_abort = self.abort + SCS_TOOLS_OT_Show3DViewReport.__static_abort = self.abort # if abort is requested just cancel operator if self.abort: if self.is_progress_message: - Show3DViewReport.__static_progress_message_l.clear() + SCS_TOOLS_OT_Show3DViewReport.__static_progress_message_l.clear() - if len(Show3DViewReport.__static_message_l) > 0: - Show3DViewReport.__static_hide_controls = False + if len(SCS_TOOLS_OT_Show3DViewReport.__static_message_l) > 0: + SCS_TOOLS_OT_Show3DViewReport.__static_hide_controls = False else: - self.cancel(context) + SCS_TOOLS_OT_Show3DViewReport.discard_drawing_data() else: - self.cancel(context) + SCS_TOOLS_OT_Show3DViewReport.discard_drawing_data() return {'CANCELLED'} - # reset flags in static variables - Show3DViewReport.__static_is_shown = True - Show3DViewReport.__static_hide_controls = self.hide_controls - - # assign title to static variable - Show3DViewReport.__static_title = self.title - - # split message by new lines - message_l = [] - for line in self.message.split("\n"): - - # remove tabulator simulated new lines from warnings and errors, written like: "\n\t " - line = line.replace("\t ", " " * 4) + # use current instance as main + SCS_TOOLS_OT_Show3DViewReport.__static_main_instance = self - # remove tabulator simulated empty space before warning or error line of summaries e.g "\t > " - line = line.replace("\t ", "") + # save window on which modal handler will be added, + # so we will be able to keep 3d view reports alive if user closes this window (see cancel method) + SCS_TOOLS_OT_Show3DViewReport.__static_window_instance = context.window - message_l.append(line) - - # properly assign parsed message list depending on progress flag - if self.is_progress_message: - Show3DViewReport.__static_progress_message_l = message_l - else: - Show3DViewReport.__static_message_l = message_l - - # if report operator is already running don't add new modal handler - if Show3DViewReport.__static_running_instances > 1: - return {'CANCELLED'} + # data shouldn't be changed if we are reassiging modal handler + if not self.modal_handler_reassign: + self.fill_drawing_data() context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} -class ShowDeveloperErrors(bpy.types.Operator): +class SCS_TOOLS_OT_ShowDeveloperErrors(bpy.types.Operator): bl_label = "" bl_description = "Show errors from stack. This was intended to be used only from batch import/export scripts." - bl_idname = "wm.show_dev_error_messages" + bl_idname = "wm.scs_tools_show_developer_errors" def execute(self, context): from io_scs_tools.utils.printout import dev_lprint @@ -396,3 +473,20 @@ def execute(self, context): dev_lprint() return {'FINISHED'} + + +classes = ( + SCS_TOOLS_OT_Show3DViewReport, + SCS_TOOLS_OT_ShowDeveloperErrors, + SCS_TOOLS_OT_ShowMessageInPopup +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/addon/io_scs_tools/operators/world.py b/addon/io_scs_tools/operators/world.py index caf7902..bcb06ae 100644 --- a/addon/io_scs_tools/operators/world.py +++ b/addon/io_scs_tools/operators/world.py @@ -16,60 +16,60 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy from math import pi -from time import time -from bpy.props import BoolProperty, IntProperty, StringProperty, CollectionProperty -from io_scs_tools.internals.shaders.eut2.std_node_groups import add_env as _add_env_node_group -from io_scs_tools.internals.shaders.eut2.std_node_groups import compose_lighting as _compose_lighting +from bpy.props import IntProperty +from io_scs_tools.internals.shaders.eut2.std_node_groups import add_env_ng as _add_env_ng +from io_scs_tools.internals.shaders.eut2.std_node_groups import compose_lighting_ng as _compose_lighting_ng +from io_scs_tools.internals.shaders.eut2.std_node_groups import lighting_evaluator_ng as _lighting_evaluator_ng from io_scs_tools.consts import SCSLigthing as _LIGHTING_consts -from io_scs_tools.utils import convert as _convert_utils -from io_scs_tools.utils import view3d as _view3d_utils from io_scs_tools.utils import get_scs_globals as _get_scs_globals +from io_scs_tools.utils import get_scs_inventories as _get_scs_inventories from io_scs_tools.utils.printout import lprint -class UseSunProfile(bpy.types.Operator): - bl_label = "Use Sun Profile" - bl_idname = "world.scs_use_sun_profile" - bl_description = "Setups lighting according to this sun profile." +class SCS_TOOLS_OT_SetupLighting(bpy.types.Operator): + bl_label = "Setup Lighting" + bl_idname = "world.scs_tools_setup_lighting" + bl_description = "Setups lighting according to given sun profile." __static_last_profile_i = -1 """Static variable saving index of sun profile currently used in lighting scene. Variable is used when operator is called with sun profile index -1, to determinate from which sun profile light data should be taken while seting up lighting scene.""" - sun_profile_index = IntProperty(default=-1) + sun_profile_index: IntProperty(default=-1) def execute(self, context): lprint('D ' + self.bl_label + "...") scs_globals = _get_scs_globals() + scs_inventories = _get_scs_inventories() # get sun profile index depending on input if self.sun_profile_index == -1: # if sun profile index is not given (equal to -1) then try to retrieve last used profile index # from static variable; otherwise use currently selected from list in UI - if UseSunProfile.__static_last_profile_i != -1: - sun_profile_i = UseSunProfile.__static_last_profile_i + if SCS_TOOLS_OT_SetupLighting.__static_last_profile_i != -1: + sun_profile_i = SCS_TOOLS_OT_SetupLighting.__static_last_profile_i else: - sun_profile_i = scs_globals.sun_profiles_inventory_active + sun_profile_i = scs_globals.sun_profiles_active else: sun_profile_i = self.sun_profile_index # validate sun profile index - if sun_profile_i < 0 or sun_profile_i > len(scs_globals.sun_profiles_inventory): + if sun_profile_i < 0 or sun_profile_i > len(scs_inventories.sun_profiles): lprint("E Using active sun profile failed!") return {'FINISHED'} # update static variable for sun profile index - UseSunProfile.__static_last_profile_i = sun_profile_i + SCS_TOOLS_OT_SetupLighting.__static_last_profile_i = sun_profile_i - sun_profile_item = scs_globals.sun_profiles_inventory[sun_profile_i] + sun_profile_item = scs_inventories.sun_profiles[sun_profile_i] """:type: io_scs_tools.properties.world.GlobalSCSProps.SunProfileInventoryItem""" # convert elevation into rotation direction vector for diffuse and specular lamps @@ -77,8 +77,8 @@ def execute(self, context): elevation_avg = (sun_profile_item.low_elevation + sun_profile_item.high_elevation) / 2 elevation_direction = 1 if sun_profile_item.sun_direction == 1 else -1 directed_lamps_rotation = ( - 0, elevation_direction * (pi / 2 - (pi * elevation_avg / 180)), # elevation + 0, pi * scs_globals.lighting_scene_east_direction / 180 # current BT user defined east direction ) @@ -88,386 +88,82 @@ def execute(self, context): else: lighting_scene = bpy.data.scenes[_LIGHTING_consts.scene_name] - lighting_scene.layers = [True] * 20 - - # 2. create ambient lights - _compose_lighting.set_additional_ambient_col(sun_profile_item.ambient * sun_profile_item.ambient_hdr_coef) - for ambient_lamp_name, ambient_lamp_direction, ambient_lamp_factor in _LIGHTING_consts.ambient_lamps: - - if ambient_lamp_name in bpy.data.lamps: - lamp_data = bpy.data.lamps[ambient_lamp_name] - else: - lamp_data = bpy.data.lamps.new(ambient_lamp_name, "HEMI") - - lamp_data.type = "HEMI" - lamp_data.use_specular = False - lamp_data.use_diffuse = True - lamp_data.energy = sun_profile_item.ambient_hdr_coef * ambient_lamp_factor - lamp_data.color = sun_profile_item.ambient - - if ambient_lamp_name in bpy.data.objects: - lamp_obj = bpy.data.objects[ambient_lamp_name] - lamp_obj.data = lamp_data - else: - lamp_obj = bpy.data.objects.new(ambient_lamp_name, lamp_data) - - lamp_obj.hide = True - lamp_obj.rotation_euler = ambient_lamp_direction - lamp_obj.rotation_euler[2] = directed_lamps_rotation[2] - lamp_obj.layers = [True] * 20 # enable it on all layers + # 2. create sun lamp empty and setup it's direction according elevation average - if lamp_obj.name not in lighting_scene.objects: - lighting_scene.objects.link(lamp_obj) - - # 3. create sun lamp for diffuse and setup it's direction according elevation average - if _LIGHTING_consts.diffuse_lamp_name in bpy.data.lamps: - lamp_data = bpy.data.lamps[_LIGHTING_consts.diffuse_lamp_name] + if _LIGHTING_consts.sun_lamp_name in bpy.data.objects: + lamp_obj = bpy.data.objects[_LIGHTING_consts.sun_lamp_name] else: - lamp_data = bpy.data.lamps.new(_LIGHTING_consts.diffuse_lamp_name, "SUN") - - lamp_data.type = "SUN" - lamp_data.use_specular = False - lamp_data.use_diffuse = True - lamp_data.energy = sun_profile_item.diffuse_hdr_coef - lamp_data.color = _convert_utils.linear_to_srgb(sun_profile_item.diffuse) + lamp_obj = bpy.data.objects.new(_LIGHTING_consts.sun_lamp_name, None) - if _LIGHTING_consts.diffuse_lamp_name in bpy.data.objects: - lamp_obj = bpy.data.objects[_LIGHTING_consts.diffuse_lamp_name] - lamp_obj.data = lamp_data - else: - lamp_obj = bpy.data.objects.new(_LIGHTING_consts.diffuse_lamp_name, lamp_data) + if lamp_obj.name not in lighting_scene.collection.objects: + lighting_scene.collection.objects.link(lamp_obj) - lamp_obj.hide = True + # lamp_obj.hide_viewport = True lamp_obj.rotation_euler = directed_lamps_rotation - lamp_obj.layers = [True] * 20 # enable it on all layers - - if lamp_obj.name not in lighting_scene.objects: - lighting_scene.objects.link(lamp_obj) - - # 4. create sun lamp for specular and setup it's direction according elevation average - if _LIGHTING_consts.specular_lamp_name in bpy.data.lamps: - lamp_data = bpy.data.lamps[_LIGHTING_consts.specular_lamp_name] - else: - lamp_data = bpy.data.lamps.new(_LIGHTING_consts.specular_lamp_name, "SUN") - - lamp_data.type = "SUN" - lamp_data.use_specular = True - lamp_data.use_diffuse = False - lamp_data.energy = sun_profile_item.specular_hdr_coef - lamp_data.color = _convert_utils.linear_to_srgb(sun_profile_item.specular) - - if _LIGHTING_consts.specular_lamp_name in bpy.data.objects: - lamp_obj = bpy.data.objects[_LIGHTING_consts.specular_lamp_name] - lamp_obj.data = lamp_data - else: - lamp_obj = bpy.data.objects.new(_LIGHTING_consts.specular_lamp_name, lamp_data) - lamp_obj.hide = True - lamp_obj.rotation_euler = directed_lamps_rotation - lamp_obj.layers = [True] * 20 # enable it on all layers + # 3. set ambient, diffuse and specular environment values + _lighting_evaluator_ng.set_ambient_light(sun_profile_item.ambient) + _lighting_evaluator_ng.set_diffuse_light(sun_profile_item.diffuse) + _lighting_evaluator_ng.set_specular_light(sun_profile_item.specular) + _lighting_evaluator_ng.set_light_direction(lamp_obj) - if lamp_obj.name not in lighting_scene.objects: - lighting_scene.objects.link(lamp_obj) + _compose_lighting_ng.set_additional_ambient_col(sun_profile_item.ambient) # 5. search for AddEnv node group and setup coefficient for environment accordingly - _add_env_node_group.set_global_env_factor(sun_profile_item.env * sun_profile_item.env_static_mod) - - # 6. set lighting scene as background scene of current scene - if context.scene and context.scene != lighting_scene: - context.scene.background_set = lighting_scene - else: - lprint("E Lighting scene created but not used, as currently there is no active scene!") + _add_env_ng.set_global_env_factor(sun_profile_item.env * sun_profile_item.env_static_mod) return {'FINISHED'} -class RemoveSCSLightingFromScene(bpy.types.Operator): - bl_label = "Switch Off SCS Lighting in Current Scene" - bl_idname = "world.scs_disable_lighting_in_scene" - bl_description = "Disables SCS lighting in current scene, giving you ability to setup your own lighting." +class SCS_TOOLS_OT_DisableLighting(bpy.types.Operator): + bl_label = "Disable Lighting" + bl_idname = "world.scs_tools_disable_lighting" + bl_description = "Disables SCS lighting, reseting lighting to generic one in camera space." def execute(self, context): lprint('D ' + self.bl_label + "...") old_scene = None - if context.scene and context.scene.background_set and context.scene.background_set.name == _LIGHTING_consts.scene_name: + if context.scene and context.scene != _LIGHTING_consts.scene_name: old_scene = context.scene - old_scene.background_set = None # if scs lighting scene is not present in blend file anymore, we are done if _LIGHTING_consts.scene_name not in bpy.data.scenes: return {'FINISHED'} - # otherwise check if current scene was the last using SCS lighting, - # if so then make sure to cleanup lamp objects and delete the scene - used_counter = 0 - for scene in bpy.data.scenes: - - if scene.name == _LIGHTING_consts.scene_name: - continue - - if scene.background_set == bpy.data.scenes[_LIGHTING_consts.scene_name]: - used_counter += 1 - - if used_counter == 0: - - # firstly remove the lighting scene - override = { - 'window': context.window, - 'screen': context.screen, - 'blend_data': context.blend_data, - 'scene': bpy.data.scenes[_LIGHTING_consts.scene_name], - 'region': None, - 'area': None, - 'edit_object': None, - 'active_object': None, - 'selected_objects': None, - } - bpy.ops.scene.delete(override, 'INVOKE_DEFAULT') - - # now gather all lamp objects names - lamp_names = [_LIGHTING_consts.diffuse_lamp_name, _LIGHTING_consts.specular_lamp_name] - for lamp_name, lamp_dir, lamp_factor in _LIGHTING_consts.ambient_lamps: - lamp_names.append(lamp_name) - - # lastly delete blend objects and lamps - for lamp_name in lamp_names: - - if lamp_name in bpy.data.objects: - bpy.data.objects.remove(bpy.data.objects[lamp_name], do_unlink=True) - - if lamp_name in bpy.data.lamps: - bpy.data.lamps.remove(bpy.data.lamps[lamp_name], do_unlink=True) - - # while we delete one scene blender might/will select first available as current, - # so we have to force our screen to be using old scene again - if old_scene and context.screen: - context.screen.scene = old_scene - - return {'FINISHED'} - - -class SCSPathsInitialization(bpy.types.Operator): - bl_label = "" - bl_description = "Initializes SCS Blender Tools filepaths asynchronously with proper reporting in 3D view." - bl_idname = "world.scs_paths_initialization" - - DUMP_LEVEL = 3 - """Constant for log level index according in SCS Globals, on which operator should printout extended report.""" - - __last_time = None - """Used for time tracking on each individual path initialization.""" - __path_in_progress = False - """Used as flag for indicating path being processed. So another execute method call shouldn't be triggered.""" - - # Static running variables - __static_timer = None - """Timer instance variable. We use timer to initilize paths gradually one by one.""" - __static_paths_count = 0 - """Static variable holding number of all paths that had to be processed. Used for reporting progress eg. 'X of Y paths done'.""" - __static_paths_done = 0 - """Static variable holding number of already processed paths. Used for reporting progress eg. 'X of Y paths done'.""" - __static_abort_instances = False - """Static variable holding existing operator instance alive until it's set to false. Used when multiple instance are invoked.""" - - # Static data storage - __static_message = "" - """Static variable holding printout extended message. This message used only if dump level is high enough.""" - __static_paths_list = [] - """Static variable holding list with dictonariy entries each of them representing Filepath class entry that needs in initialization. - Processed paths are removed on the fly. - """ - __static_callbacks = [] - """Static variable holding list of callbacks that will be executed once operator is finished or cancelled. - """ - - class Filepath(bpy.types.PropertyGroup): - """ - Entry for file paths collection that should be set on SCS globals asynchronously inside SCSToolsInitialization operator. - """ - name = StringProperty(description="Friendly name used for printouts.") - attr = StringProperty(description="Name of the property in SCSGlobalsProps class.") - path = StringProperty(description="Actual file/dir path that should be applied to the property.") - - paths_list = CollectionProperty( - type=Filepath, - description= - """ - List of paths that should be initialized, used only for passing filepaths data to operator. - During operator invoke this list extends static paths list. Accessing paths via static variable - is needed for multiple operator invoking from different places. As this enables us having common - up to date list for processing paths in order and none of it stays unproperly processed. - """ - ) - - @staticmethod - def is_running(): - """Tells if paths initialization is still in progress. - - :return: True if scs paths initialization is still in progress; False if none instances are running - :rtype: bool - """ - return len(SCSPathsInitialization.__static_paths_list) > 0 and SCSPathsInitialization.__static_timer - - @staticmethod - def append_callback(callback): - """Appends given callback function to callback list. Callbacks are called once paths initialization is done. - If operator is not running then False is returned and callback is not added to the list! - NOTE: there is no check if given callback is already in list. - - :param callback: callback function without arguments - :type callback: object - :return: True if operator is running and callback is added to the list properly; False if callback won't be added and executed - :rtype: bool - """ - if SCSPathsInitialization.is_running(): - SCSPathsInitialization.__static_callbacks.append(callback) - return True - - return False - - def execute(self, context): - - # do not proceed if list is already empty - if len(SCSPathsInitialization.__static_paths_list) <= 0: - return {'FINISHED'} - - self.__path_in_progress = True + # firstly remove the lighting scene + override = context.copy() + override['window'] = context.window_manager.windows[-1] + override['scene'] = bpy.data.scenes[_LIGHTING_consts.scene_name] + bpy.ops.scene.delete(override, 'INVOKE_DEFAULT') - # update message with current path and apply it - SCSPathsInitialization.__static_message += "Initializing " + SCSPathsInitialization.__static_paths_list[0]["name"] + "..." - setattr(_get_scs_globals(), SCSPathsInitialization.__static_paths_list[0]["attr"], SCSPathsInitialization.__static_paths_list[0]["path"]) - SCSPathsInitialization.__static_paths_list = SCSPathsInitialization.__static_paths_list[1:] # remove just processed item - SCSPathsInitialization.__static_message += " Done in %.2f s!\n" % (time() - self.__last_time) - SCSPathsInitialization.__static_paths_done += 1 + if _LIGHTING_consts.sun_lamp_name in bpy.data.objects: + bpy.data.objects.remove(bpy.data.objects[_LIGHTING_consts.sun_lamp_name], do_unlink=True) - # when executing last one, also print out hiding message - if len(SCSPathsInitialization.__static_paths_list) == 0: - SCSPathsInitialization.__static_message += "SCS Blender Tools are ready!" - _view3d_utils.tag_redraw_all_view3d() + # reset light direction & ambient, diffuse and specular environment values + _lighting_evaluator_ng.reset_lighting_params() + _compose_lighting_ng.reset_lighting_params() + _add_env_ng.reset_lighting_params() - self.__last_time = time() # reset last time for next path - - self.__path_in_progress = False - - # if debug then report whole progress message otherwise print out condensed message - if int(_get_scs_globals().dump_level) >= self.DUMP_LEVEL: - message = SCSPathsInitialization.__static_message - hide_controls = False - else: - message = "Paths and libraries initialization %s/%s ..." % (SCSPathsInitialization.__static_paths_done, - SCSPathsInitialization.__static_paths_count) - hide_controls = True - bpy.ops.wm.show_3dview_report('INVOKE_DEFAULT', message=message, hide_controls=hide_controls, is_progress_message=True) + # while we delete one scene blender might/will select first available as current, + # so we have to force our screen to be using old scene again + if old_scene and context.window: + context.window.scene = old_scene return {'FINISHED'} - def cancel(self, context): - - # reset static variables - SCSPathsInitialization.__static_message = "" - SCSPathsInitialization.__static_paths_list.clear() - - # try to reset timer if window manager is available - if len(bpy.data.window_managers) > 0: - wm = bpy.data.window_managers[0] - wm.event_timer_remove(SCSPathsInitialization.__static_timer) - SCSPathsInitialization.__static_timer = None - - # report finished progress to 3d view report operator - if int(_get_scs_globals().dump_level) < self.DUMP_LEVEL: - bpy.ops.wm.show_3dview_report('INVOKE_DEFAULT', abort=True, is_progress_message=True) - - # when done, tag everything for redraw in the case some UI components - # are reporting status of this operator - _view3d_utils.tag_redraw_all_regions() - - # as last invoke any callbacks and afterwards delete them - while len(SCSPathsInitialization.__static_callbacks) > 0: - - callback = SCSPathsInitialization.__static_callbacks[0] - - callback() - SCSPathsInitialization.__static_callbacks.remove(callback) - - lprint("D Paths initialization cancel invoked!") - - def modal(self, context, event): - - # if abort was requested finish immediately - if SCSPathsInitialization.__static_abort_instances: - self.cancel(context) - lprint("I Paths initialization aborted, deleting operator!") - return {'FINISHED'} - - if event.type == "TIMER": # process timer event - - if len(SCSPathsInitialization.__static_paths_list) <= 0: # once no more paths to process abort it - - # NOTE: canceling has to be done in timer event. - # Otherwise finishing operator with status 'FINISHED' eats event and - # stops event in this operator and cancels action which user wanted to do. - self.cancel(context) - lprint("I Paths initialization done, deleting operator!") - return {'FINISHED'} - - if not self.__path_in_progress: # if not in progress then trigger execute and process next - - self.execute(context) - - return {'PASS_THROUGH'} - - def invoke(self, context, event): - - self.__last_time = time() # reset last time now as everything starts again - SCSPathsInitialization.__static_paths_done = 0 # reset done paths counter as everything starts here - - # engage abortion of any running instances - SCSPathsInitialization.__abort_any_running_instances = True - - # now fill up new paths to static inventory - for filepath_prop in self.paths_list: - - # sort out only unique paths and merge them with current static path list - old_item = None - for item in SCSPathsInitialization.__static_paths_list: - if item["attr"] == filepath_prop.attr: - old_item = item - break - - # if old item is found just reuse it instead of adding new item to list - if old_item: - old_item["name"] = filepath_prop["name"] - old_item["path"] = filepath_prop["path"] - else: - SCSPathsInitialization.__static_paths_list.append( - { - "name": filepath_prop["name"], - "attr": filepath_prop["attr"], - "path": filepath_prop["path"] - } - ) - - # update paths counter to the current paths list length - SCSPathsInitialization.__static_paths_count = len(SCSPathsInitialization.__static_paths_list) - - # now as paths list is updated and we are about to run our instance - # release switch that should be aborting all the rest of operator instances - SCSPathsInitialization.__abort_any_running_instances = False - SCSPathsInitialization.__static_message = "Starting initialization...\n" - bpy.ops.wm.show_3dview_report('INVOKE_DEFAULT', message=SCSPathsInitialization.__static_message, - hide_controls=True, is_progress_message=True) +classes = ( + SCS_TOOLS_OT_DisableLighting, + SCS_TOOLS_OT_SetupLighting, +) - wm = bpy.data.window_managers[0] - window = wm.windows[0] - # in case any operator was previously invoked we have to remove timer before adding new - if SCSPathsInitialization.__static_timer: - wm.event_timer_remove(SCSPathsInitialization.__static_timer) +def register(): + for cls in classes: + bpy.utils.register_class(cls) - SCSPathsInitialization.__static_timer = wm.event_timer_add(0.2, window) - wm.modal_handler_add(self) - lprint("I Paths initialization started...") - return {'RUNNING_MODAL'} +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/addon/io_scs_tools/properties/__init__.py b/addon/io_scs_tools/properties/__init__.py index 1d16694..8fcfbce 100644 --- a/addon/io_scs_tools/properties/__init__.py +++ b/addon/io_scs_tools/properties/__init__.py @@ -16,14 +16,35 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software from io_scs_tools.properties import action +from io_scs_tools.properties import addon_preferences +from io_scs_tools.properties import dynamic from io_scs_tools.properties import material from io_scs_tools.properties import mesh from io_scs_tools.properties import object from io_scs_tools.properties import scene -from io_scs_tools.properties import world +from io_scs_tools.properties import workspace -from io_scs_tools.properties.dynamic import scene as scene_dynamic -from io_scs_tools.properties.dynamic import object as object_dynamic + +def register(): + action.register() + addon_preferences.register() + dynamic.register() + material.register() + mesh.register() + object.register() + scene.register() + workspace.register() + + +def unregister(): + action.unregister() + addon_preferences.unregister() + dynamic.unregister() + material.unregister() + mesh.unregister() + object.unregister() + scene.unregister() + workspace.unregister() diff --git a/addon/io_scs_tools/properties/action.py b/addon/io_scs_tools/properties/action.py index ddfa351..522a8b1 100644 --- a/addon/io_scs_tools/properties/action.py +++ b/addon/io_scs_tools/properties/action.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy from bpy.props import IntProperty @@ -28,7 +28,7 @@ class ActionSCSTools(bpy.types.PropertyGroup): :return: """ - anim_export_step = IntProperty( + anim_export_step: IntProperty( name="Export Step", description="Number of frames to step in action for each iteration trough exporting.", default=1, @@ -36,4 +36,18 @@ class ActionSCSTools(bpy.types.PropertyGroup): step=1, options={'HIDDEN'}, subtype='NONE', - ) \ No newline at end of file + ) + + +classes = ( + ActionSCSTools, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/addon/io_scs_tools/properties/world.py b/addon/io_scs_tools/properties/addon_preferences.py similarity index 70% rename from addon/io_scs_tools/properties/world.py rename to addon/io_scs_tools/properties/addon_preferences.py index 8c1ce28..1b71ced 100644 --- a/addon/io_scs_tools/properties/world.py +++ b/addon/io_scs_tools/properties/addon_preferences.py @@ -16,8 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2015: SCS Software - +# Copyright (C) 2019: SCS Software import bpy from bpy.props import (StringProperty, @@ -28,108 +27,101 @@ EnumProperty, FloatVectorProperty) from io_scs_tools.consts import ConvHlpr as _CONV_HLPR_consts +from io_scs_tools.consts import SCSLigthing as _LIGHTING_consts +from io_scs_tools.internals import icons as _icons from io_scs_tools.internals import preview_models as _preview_models from io_scs_tools.internals import shader_presets as _shader_presets from io_scs_tools.internals.callbacks import lighting_east_lock as _lighting_east_lock_callback from io_scs_tools.internals.containers import config as _config_container +from io_scs_tools.properties.dynamic import DynamicProps as _DynamicProps from io_scs_tools.utils import material as _material_utils from io_scs_tools.utils import path as _path_utils -from io_scs_tools.utils import get_scs_globals as _get_scs_globals - +from io_scs_tools.utils import view3d as _view3d_utils +from io_scs_tools.utils import get_scs_inventories as _get_scs_inventories -class GlobalSCSProps(bpy.types.PropertyGroup): - """ - SCS Tools Global Variables - ...World.scs_globals... - :return: - """ +class SCSInventories(bpy.types.PropertyGroup): # TRIGGER LOCATOR ACTIONS INVENTORY - class TriggerActionsInventory(bpy.types.PropertyGroup): + class TriggerActionsInventoryItem(bpy.types.PropertyGroup): """ Triger Actions Inventory. """ - item_id = StringProperty(default="") + item_id: StringProperty(default="") - scs_trigger_actions_inventory = CollectionProperty( - type=TriggerActionsInventory, - options={'SKIP_SAVE'}, + trigger_actions: CollectionProperty( + type=TriggerActionsInventoryItem, ) # SIGN LOCATOR MODEL INVENTORY - class SignModelInventory(bpy.types.PropertyGroup): + class SignModelInventoryItem(bpy.types.PropertyGroup): """ Sign Model Inventory. :return: """ - item_id = StringProperty(default="") - model_desc = StringProperty(default="") - look_name = StringProperty(default="") - category = StringProperty(default="") - dynamic = BoolProperty(default=False) + item_id: StringProperty(default="") + model_desc: StringProperty(default="") + look_name: StringProperty(default="") + category: StringProperty(default="") + dynamic: BoolProperty(default=False) - scs_sign_model_inventory = CollectionProperty( - type=SignModelInventory, - options={'SKIP_SAVE'}, + sign_models: CollectionProperty( + type=SignModelInventoryItem, ) # TRAFFIC SEMAPHORE LOCATOR PROFILE INVENTORY - class TSemProfileInventory(bpy.types.PropertyGroup): + class TSemProfileInventoryItem(bpy.types.PropertyGroup): """ Traffic Semaphore Profile Inventory. :return: """ - item_id = StringProperty(default="") - model = StringProperty(default="") + item_id: StringProperty(default="") + model: StringProperty(default="") - scs_tsem_profile_inventory = CollectionProperty( - type=TSemProfileInventory, - options={'SKIP_SAVE'}, + tsem_profiles: CollectionProperty( + type=TSemProfileInventoryItem, ) # TRAFFIC RULES PROFILE INVENTORY - class TrafficRulesInventory(bpy.types.PropertyGroup): + class TrafficRulesInventoryItem(bpy.types.PropertyGroup): """ Traffic Rules Inventory. :return: """ - # item_id = StringProperty(default="") - rule = StringProperty(default="") - num_params = StringProperty(default="") + # item_id: StringProperty(default="") + rule: StringProperty(default="") + num_params: StringProperty(default="") - scs_traffic_rules_inventory = CollectionProperty( - type=TrafficRulesInventory, - options={'SKIP_SAVE'}, + traffic_rules: CollectionProperty( + type=TrafficRulesInventoryItem, ) # HOOKUP INVENTORY - class HookupInventory(bpy.types.PropertyGroup): + class HookupInventoryItem(bpy.types.PropertyGroup): """ Hookup Inventory. :return: """ - item_id = StringProperty(default="") - model = StringProperty(default="") - brand_idx = IntProperty(default=0) - dir_type = StringProperty(default="") - low_poly_only = BoolProperty(default=False) + item_id: StringProperty(default="") + model: StringProperty(default="") + brand_idx: IntProperty(default=0) + dir_type: StringProperty(default="") + low_poly_only: BoolProperty(default=False) - scs_hookup_inventory = CollectionProperty( - type=HookupInventory, - options={'SKIP_SAVE'}, + hookups: CollectionProperty( + type=HookupInventoryItem, ) # MATERIAL SUBSTANCE INVENTORY - class MatSubsInventory(bpy.types.PropertyGroup): + class MatSubsInventoryItem(bpy.types.PropertyGroup): """ Material Substance Inventory. :return: """ - item_id = StringProperty(default="") - item_description = StringProperty(default="") + item_id: StringProperty(default="") + item_description: StringProperty(default="") - scs_matsubs_inventory = CollectionProperty( - type=MatSubsInventory, - options={'SKIP_SAVE'}, + matsubs: CollectionProperty( + type=MatSubsInventoryItem, ) def retrieve_shader_presets_items(self, context): @@ -142,14 +134,14 @@ def retrieve_shader_presets_items(self, context): # items = [("", "", "No SCS shader preset in use (may result in incorrect model output)", 'X', 0)] items = [] - for preset_name in _shader_presets.get_preset_names(self.shader_preset_list_sorted): + for preset_name in _shader_presets.get_preset_names(context.workspace.scs_props.shader_presets_sorted): if "spec" in preset_name: icon_str = 'MATERIAL' elif "glass" in preset_name: icon_str = 'MOD_LATTICE' elif "lamp" in preset_name: - icon_str = 'LAMP_SPOT' + icon_str = 'LIGHT_SPOT' elif "shadowonly" in preset_name: icon_str = 'MAT_SPHERE_SKY' elif "truckpaint" in preset_name: @@ -159,7 +151,7 @@ def retrieve_shader_presets_items(self, context): elif preset_name == "": icon_str = 'X' else: - icon_str = 'SOLID' + icon_str = 'SHADING_SOLID' preset_i = _shader_presets.get_preset_index(preset_name) items.append((preset_name, preset_name, "", icon_str, preset_i)) @@ -211,129 +203,208 @@ def set_shader_presets_item(self, value): else: print('''NO "preset_section"! (Shouldn't happen!)''') - shader_preset_list = EnumProperty( + shader_presets: EnumProperty( name="Shader Presets", description="Shader presets", items=retrieve_shader_presets_items, get=get_shader_presets_item, set=set_shader_presets_item, ) - shader_preset_list_sorted = BoolProperty( - name="Shader Preset List Sorted Alphabetically", - description="Sort Shader preset list alphabetically", - default=True, + + class SunProfileInventoryItem(bpy.types.PropertyGroup): + """ + Sun profile properties used to load climate profiles and create lighting scene from them. + """ + + def sun_profile_item_update(self, context): + """Update lighting in the scene according to currently set values in active sun profile inventory item. + + :param context: + :type context: + """ + if not self.is_blocked: + bpy.ops.world.scs_tools_setup_lighting() + + name: StringProperty(name="Name", default="") + + low_elevation: IntProperty( + name="Low Elevation", + options={'HIDDEN'}, + update=sun_profile_item_update + ) + high_elevation: IntProperty( + name="High Elevation", + options={'HIDDEN'}, + update=sun_profile_item_update + ) + + sun_direction: IntProperty( + name="Sun Elevation Direction", + options={'HIDDEN'}, + ) + + ambient: FloatVectorProperty( + name="Ambient", + options={'HIDDEN'}, + subtype='COLOR', + size=3, + min=-5, max=5, + soft_min=0, soft_max=1, + step=3, precision=2, + update=sun_profile_item_update + ) + + diffuse: FloatVectorProperty( + name="Diffuse", + options={'HIDDEN'}, + subtype='COLOR', + size=3, + min=-5, max=5, + soft_min=0, soft_max=1, + step=3, precision=2, + update=sun_profile_item_update + ) + + specular: FloatVectorProperty( + name="Specular", + options={'HIDDEN'}, + subtype='COLOR', + size=3, + min=-5, max=5, + soft_min=0, soft_max=1, + step=3, precision=2, + update=sun_profile_item_update + ) + + env: FloatProperty( + name="Env Factor", + options={'HIDDEN'}, + update=sun_profile_item_update + ) + env_static_mod: FloatProperty( + name="Env Static Modulator", + options={'HIDDEN'}, + update=sun_profile_item_update + ) + is_blocked: BoolProperty( + name="Is blocked?", + description="Tells if current sun profile is blocked and prevents lighting setup on any property update.", + options={'HIDDEN'}, + default=True + ) + + sun_profiles: CollectionProperty( + type=SunProfileInventoryItem, ) - # SCS TOOLS GLOBAL PATHS - def scs_project_path_update(self, context): - # Update all related paths so their libraries gets reloaded from new "SCS Project Path" location. - if not _get_scs_globals().config_update_lock: - _config_container.update_item_in_file('Paths.ProjectPath', self.scs_project_path) +class SCSGlobals(bpy.types.PropertyGroup): + """SCS Tools Global Variables""" - scs_globals = _get_scs_globals() + @staticmethod + def on_display_setting_update(context): + """Triggered on any display setting update. - # enable update lock because we only want to reload libraries, as their paths are not changed - _config_container.engage_config_lock() + :param context: blender context from which setting was updated + :type context: bpy.types.Context + """ - # trigger update functions via asynchronous operator for library paths initialization - bpy.ops.world.scs_paths_initialization('INVOKE_DEFAULT', paths_list=[ - {"name": "trigger actions library", "attr": "trigger_actions_rel_path", "path": scs_globals.trigger_actions_rel_path}, - {"name": "sign library", "attr": "sign_library_rel_path", "path": scs_globals.sign_library_rel_path}, - {"name": "traffic semaphore library", "attr": "tsem_library_rel_path", "path": scs_globals.tsem_library_rel_path}, - {"name": "traffic rules library", "attr": "traffic_rules_library_rel_path", "path": scs_globals.traffic_rules_library_rel_path}, - {"name": "hookups library", "attr": "hookup_library_rel_path", "path": scs_globals.hookup_library_rel_path}, - {"name": "material substance library", "attr": "matsubs_library_rel_path", "path": scs_globals.matsubs_library_rel_path}, - {"name": "sun profiles library", "attr": "sun_profiles_lib_path", "path": scs_globals.sun_profiles_lib_path}, - ]) + # mark scene for update, so that drawing buffers are refilled + if context and context.scene: + context.scene.update_tag() - # release lock as properties are applied - _config_container.release_config_lock(use_paths_init_callback=True) + def get_writtable_keys(self): + """Get writtable keys from scs globals (dynamic properties are excluded). + :return: set of writtable properties + :rtype: set + """ + return set(self.keys()) - _DynamicProps.get_registered_members(_DynamicProps.Scopes.SCS_GLOBALS) + + # DYNAMIC PROPS - UPDATE LOCKS (FOR AVOIDANCE OF RECURSION) + config_update_lock = _DynamicProps.register(_DynamicProps.Scopes.SCS_GLOBALS, "config_update_lock", bool, False) + import_in_progress = _DynamicProps.register(_DynamicProps.Scopes.SCS_GLOBALS, "import_in_progress", bool, False) + + # SCS TOOLS - PROJECT RELATED GLOBAL PATHS + def scs_project_path_update(self, context): + _config_container.update_scs_project_path(self.scs_project_path) return None def use_alternative_bases_update(self, context): _config_container.update_item_in_file('Paths.UseAlternativeBases', int(self.use_alternative_bases)) - self.scs_project_path_update(context) + # reload inventories, indirectly + _config_container.update_scs_project_path(self.scs_project_path, reload_only=True) return None def shader_presets_filepath_update(self, context): - _config_container.update_shader_presets_path(self.shader_presets_filepath) + shader_presets_path = self.shader_presets_filepath if self.shader_presets_use_custom else _path_utils.get_shader_presets_filepath() + _config_container.update_shader_presets_path(shader_presets_path) + return None + + def shader_presets_use_custom_update(self, context): + _config_container.update_item_in_file('Paths.ShaderPresetsUseCustom', int(self.shader_presets_use_custom)) + + # reload shader presets + shader_presets_path = self.shader_presets_filepath if self.shader_presets_use_custom else _path_utils.get_shader_presets_filepath() + _config_container.update_shader_presets_path(shader_presets_path, reload_only=True) return None def trigger_actions_rel_path_update(self, context): - _config_container.update_trigger_actions_rel_path(_get_scs_globals().scs_trigger_actions_inventory, - self.trigger_actions_rel_path) + _config_container.update_trigger_actions_rel_path(self.trigger_actions_rel_path) return None def trigger_actions_use_infixed_update(self, context): - - if not _get_scs_globals().config_update_lock: # prevent reloading library when blender is starting - _config_container.update_trigger_actions_rel_path(_get_scs_globals().scs_trigger_actions_inventory, - self.trigger_actions_rel_path, - readonly=True) - _config_container.update_item_in_file('Paths.TriggerActionsUseInfixed', int(self.trigger_actions_use_infixed)) + # reload inventory, as scope of gathered file paths has changed + _config_container.update_trigger_actions_rel_path(self.trigger_actions_rel_path, reload_only=True) + return None + def sign_library_rel_path_update(self, context): - _config_container.update_sign_library_rel_path(_get_scs_globals().scs_sign_model_inventory, - self.sign_library_rel_path) + _config_container.update_sign_library_rel_path(self.sign_library_rel_path) return None def sign_library_use_infixed_update(self, context): - - if not _get_scs_globals().config_update_lock: # prevent reloading library when blender is starting - _config_container.update_sign_library_rel_path(_get_scs_globals().scs_sign_model_inventory, - self.sign_library_rel_path, - readonly=True) - _config_container.update_item_in_file('Paths.SignUseInfixed', int(self.sign_library_use_infixed)) + # reload inventory, as scope of gathered file paths has changed + _config_container.update_sign_library_rel_path(self.sign_library_rel_path, reload_only=True) + return None + def tsem_library_rel_path_update(self, context): - _config_container.update_tsem_library_rel_path(_get_scs_globals().scs_tsem_profile_inventory, - self.tsem_library_rel_path) + _config_container.update_tsem_library_rel_path(self.tsem_library_rel_path) return None def tsem_library_use_infixed_update(self, context): - - if not _get_scs_globals().config_update_lock: # prevent reloading library when blender is starting - _config_container.update_tsem_library_rel_path(_get_scs_globals().scs_tsem_profile_inventory, - self.tsem_library_rel_path, - readonly=True) - _config_container.update_item_in_file('Paths.TSemProfileUseInfixed', int(self.tsem_library_use_infixed)) + # reload inventory, as scope of gathered file paths has changed + _config_container.update_tsem_library_rel_path(self.tsem_library_rel_path, reload_only=True) + return None + def traffic_rules_library_rel_path_update(self, context): - _config_container.update_traffic_rules_library_rel_path(_get_scs_globals().scs_traffic_rules_inventory, - self.traffic_rules_library_rel_path) + _config_container.update_traffic_rules_library_rel_path(self.traffic_rules_library_rel_path) return None def traffic_rules_library_use_infixed_update(self, context): - - if not _get_scs_globals().config_update_lock: # prevent reloading library when blender is starting - _config_container.update_traffic_rules_library_rel_path(_get_scs_globals().scs_traffic_rules_inventory, - self.traffic_rules_library_rel_path, - readonly=True) - _config_container.update_item_in_file('Paths.TrafficRulesUseInfixed', int(self.traffic_rules_library_use_infixed)) + + # reload inventory, as scope of gathered file paths has changed + _config_container.update_traffic_rules_library_rel_path(self.traffic_rules_library_rel_path, reload_only=True) return None def hookup_library_rel_path_update(self, context): - # print('Hookup Library Path UPDATE: "%s"' % self.hookup_library_rel_path) - _config_container.update_hookup_library_rel_path(_get_scs_globals().scs_hookup_inventory, - self.hookup_library_rel_path) + _config_container.update_hookup_library_rel_path(self.hookup_library_rel_path) return None def matsubs_library_rel_path_update(self, context): - # print('Material Substance Library Path UPDATE: "%s"' % self.matsubs_library_rel_path) - _config_container.update_matsubs_inventory(_get_scs_globals().scs_matsubs_inventory, - self.matsubs_library_rel_path) + _config_container.update_matsubs_inventory(self.matsubs_library_rel_path) return None os_rs = "//" # RELATIVE PATH SIGN - for all OSes we use // inside Blender Tools - scs_project_path = StringProperty( + scs_project_path: StringProperty( name="SCS Project Main Directory", description="SCS project main directory (absolute path)", default="", @@ -341,7 +412,7 @@ def matsubs_library_rel_path_update(self, context): subtype='NONE', update=scs_project_path_update ) - use_alternative_bases = BoolProperty( + use_alternative_bases: BoolProperty( name="Use SCS Resources and Libraries From Alternative Bases", description="When used, all resources with relative paths ('//') will also be searched for inside alternative 'base' directories.\n\n" "For example let's say we have:\n" @@ -360,74 +431,80 @@ def matsubs_library_rel_path_update(self, context): default=True, update=use_alternative_bases_update, ) - shader_presets_filepath = StringProperty( + shader_presets_filepath: StringProperty( name="Shader Presets Library", description="Shader Presets library file path (absolute file path; *.txt)", - default=_path_utils.get_shader_presets_filepath(), + default="", subtype='NONE', # subtype="FILE_PATH", update=shader_presets_filepath_update, ) - trigger_actions_rel_path = StringProperty( + shader_presets_use_custom: BoolProperty( + name="Use Custom Shader Presets", + description="Enables Usage of custom shader presets file", + default=False, + update=shader_presets_use_custom_update, + ) + trigger_actions_rel_path: StringProperty( name="Trigger Actions Lib", description="Trigger actions library (absolute or relative file path to 'SCS Project Base Directory'; *.sii)", default=str(os_rs + 'def/world/trigger_action.sii'), subtype='NONE', update=trigger_actions_rel_path_update, ) - trigger_actions_use_infixed = BoolProperty( + trigger_actions_use_infixed: BoolProperty( name="Trigger Actions Use Infixed Files", description="Also load library items from any infixed SII files from same directory.", default=True, update=trigger_actions_use_infixed_update, ) - sign_library_rel_path = StringProperty( + sign_library_rel_path: StringProperty( name="Sign Library", description="Sign library (absolute or relative file path to 'SCS Project Base Directory'; *.sii)", default=str(os_rs + 'def/world/sign.sii'), subtype='NONE', update=sign_library_rel_path_update, ) - sign_library_use_infixed = BoolProperty( + sign_library_use_infixed: BoolProperty( name="Sign Library Use Infixed Files", description="Also load library items from any infixed SII files from same directory.", default=True, update=sign_library_use_infixed_update, ) - tsem_library_rel_path = StringProperty( + tsem_library_rel_path: StringProperty( name="Traffic Semaphore Profile Library", description="Traffic Semaphore Profile library (absolute or relative file path to 'SCS Project Base Directory'; *.sii)", default=str(os_rs + 'def/world/semaphore_profile.sii'), subtype='NONE', update=tsem_library_rel_path_update, ) - tsem_library_use_infixed = BoolProperty( + tsem_library_use_infixed: BoolProperty( name="Traffic Semaphore Lib Use Infixed Files", description="Also load library items from any infixed SII files from same directory.", default=True, update=tsem_library_use_infixed_update, ) - traffic_rules_library_rel_path = StringProperty( + traffic_rules_library_rel_path: StringProperty( name="Traffic Rules Library", description="Traffic rules library (absolute or relative file path to 'SCS Project Base Directory'; *.sii)", default=str(os_rs + 'def/world/traffic_rules.sii'), subtype='NONE', update=traffic_rules_library_rel_path_update, ) - traffic_rules_library_use_infixed = BoolProperty( + traffic_rules_library_use_infixed: BoolProperty( name="Traffic Rules Lib Use Infixed Files", description="Also load library items from any infixed SII files from same directory.", default=True, update=traffic_rules_library_use_infixed_update, ) - hookup_library_rel_path = StringProperty( + hookup_library_rel_path: StringProperty( name="Hookup Library Dir", description="Hookup library directory (absolute or relative path to 'SCS Project Base Directory')", default=str(os_rs + 'unit/hookup'), subtype='NONE', update=hookup_library_rel_path_update, ) - matsubs_library_rel_path = StringProperty( + matsubs_library_rel_path: StringProperty( name="Material Substance Library", description="Material substance library (absolute or relative file path to 'SCS Project Base Directory'; *.db)", default=str(os_rs + 'material/material.db'), @@ -435,74 +512,58 @@ def matsubs_library_rel_path_update(self, context): update=matsubs_library_rel_path_update, ) - # UPDATE LOCKS (FOR AVOIDANCE OF RECURSION) - config_update_lock = BoolProperty( - name="Update Lock For Config Items", - description="Allows temporarily lock automatic updates for all items which are stored in config file", - default=False, - ) - import_in_progress = BoolProperty( - name="Indicator of import process", - description="Holds the state of SCS import process", - default=False, - ) - # DISPLAY SETTINGS def display_locators_update(self, context): + self.on_display_setting_update(context) _config_container.update_item_in_file('GlobalDisplay.DisplayLocators', int(self.display_locators)) - return None def locator_size_update(self, context): + self.on_display_setting_update(context) _config_container.update_item_in_file('GlobalDisplay.LocatorSize', self.locator_size) - return None def locator_empty_size_update(self, context): + self.on_display_setting_update(context) _config_container.update_item_in_file('GlobalDisplay.LocatorEmptySize', self.locator_empty_size) - return None def locator_prefab_wire_color_update(self, context): - _config_container.update_item_in_file('GlobalColors.PrefabLocatorsWire', - tuple(self.locator_prefab_wire_color)) - return None + self.on_display_setting_update(context) + _config_container.update_item_in_file('GlobalColors.PrefabLocatorsWire', tuple(self.locator_prefab_wire_color)) def locator_model_wire_color_update(self, context): - _config_container.update_item_in_file('GlobalColors.ModelLocatorsWire', - tuple(self.locator_model_wire_color)) - return None + self.on_display_setting_update(context) + _config_container.update_item_in_file('GlobalColors.ModelLocatorsWire', tuple(self.locator_model_wire_color)) def locator_coll_wire_color_update(self, context): - _config_container.update_item_in_file('GlobalColors.ColliderLocatorsWire', - tuple(self.locator_coll_wire_color)) - return None + self.on_display_setting_update(context) + _config_container.update_item_in_file('GlobalColors.ColliderLocatorsWire', tuple(self.locator_coll_wire_color)) def locator_coll_face_color_update(self, context): - _config_container.update_item_in_file('GlobalColors.ColliderLocatorsFace', - tuple(self.locator_coll_face_color)) - return None + self.on_display_setting_update(context) + _config_container.update_item_in_file('GlobalColors.ColliderLocatorsFace', tuple(self.locator_coll_face_color)) def display_connections_update(self, context): + self.on_display_setting_update(context) _config_container.update_item_in_file('GlobalDisplay.DisplayConnections', int(self.display_connections)) - return None def optimized_connections_drawing_update(self, context): + self.on_display_setting_update(context) _config_container.update_item_in_file('GlobalDisplay.OptimizedConnsDrawing', int(self.optimized_connections_drawing)) - return None def curve_segments_update(self, context): + self.on_display_setting_update(context) _config_container.update_item_in_file('GlobalDisplay.CurveSegments', self.curve_segments) - return None def np_curve_color_update(self, context): + self.on_display_setting_update(context) _config_container.update_item_in_file('GlobalColors.NavigationCurveBase', tuple(self.np_connection_base_color)) - return None def mp_line_color_update(self, context): + self.on_display_setting_update(context) _config_container.update_item_in_file('GlobalColors.MapLineBase', tuple(self.mp_connection_base_color)) - return None def tp_line_color_update(self, context): + self.on_display_setting_update(context) _config_container.update_item_in_file('GlobalColors.TriggerLineBase', tuple(self.tp_connection_base_color)) - return None def display_info_update(self, context): _config_container.update_item_in_file('GlobalDisplay.DisplayTextInfo', self.display_info) @@ -518,18 +579,10 @@ def drawing_mode_update(self, context): _open_gl_callback.enable(self.drawing_mode) def show_preview_models_update(self, context): - """ - :param context: - :return: - """ - for obj in bpy.data.objects: - if obj.type == 'EMPTY': - if self.show_preview_models: - if not _preview_models.load(obj): - _preview_models.unload(obj) - else: - _preview_models.unload(obj) - return None + _config_container.update_item_in_file('GlobalDisplay.DisplayPreviewModels', int(self.show_preview_models)) + + if not self.config_update_lock: + _preview_models.update() def base_paint_color_update(self, context): from io_scs_tools.internals.shaders import set_base_paint_color @@ -544,8 +597,51 @@ def base_paint_color_update(self, context): _config_container.update_item_in_file('GlobalColors.BasePaint', tuple(self.base_paint_color)) - drawing_mode = EnumProperty( - name="Custom Drawing Mode", + def retrieve_icon_theme_items(self, context): + + # no themes, just add none entry so user will know sth is wrong + if not _icons.has_loaded_themes(): + return [('none', '', '', -1)] + + # collect all loaded themes + items = [] + for theme_name in sorted(_icons.get_loaded_themes()): + items.append((theme_name, theme_name.capitalize(), "", _icons.get_theme_idx(theme_name))) + + return items + + def set_icon_theme_item(self, value): + + # no themes, nothing to remember or set + if not _icons.has_loaded_themes(): + return + + # set icons theme + theme_name = _icons.get_theme_name(value) + _icons.set_theme(theme_name) + + # update icon theme in config container + _config_container.update_item_in_file('GlobalDisplay.IconTheme', theme_name) + + # trigger regions redraw so new icon set will be used + _view3d_utils.tag_redraw_all_regions() + + self["current_icon_theme"] = value + + def get_icon_theme_item(self): + + # no themes, always return -1 as none value + if not _icons.has_loaded_themes(): + return -1 + + if "current_icon_theme" not in self: + default_theme_idx = _icons.get_current_theme_idx() + self["current_icon_theme"] = default_theme_idx + + return self["current_icon_theme"] + + drawing_mode: EnumProperty( + name="Drawing Mode", description="Drawing mode for custom elements (Locators and Connections)", items=( ('Normal', "Normal", "Use normal depth testing drawing"), @@ -554,13 +650,13 @@ def base_paint_color_update(self, context): update=drawing_mode_update ) - display_locators = BoolProperty( + display_locators: BoolProperty( name="Display Locators", description="Display locators in 3D views", default=True, update=display_locators_update, ) - locator_size = FloatProperty( + locator_size: FloatProperty( name="Locator Size", description="Locator display size in 3D views", default=1.0, @@ -570,7 +666,7 @@ def base_paint_color_update(self, context): unit='NONE', update=locator_size_update, ) - locator_empty_size = FloatProperty( + locator_empty_size: FloatProperty( name="Locator Empty Object Size", description="Locator Empty object display size in 3D views", default=0.05, @@ -581,7 +677,7 @@ def base_paint_color_update(self, context): unit='NONE', update=locator_empty_size_update, ) - locator_prefab_wire_color = FloatVectorProperty( + locator_prefab_wire_color: FloatVectorProperty( name="Prefab Loc Color", description="Color for prefab locators in 3D views", options={'HIDDEN'}, @@ -590,7 +686,7 @@ def base_paint_color_update(self, context): default=(0.5, 0.5, 0.5), update=locator_prefab_wire_color_update, ) - locator_model_wire_color = FloatVectorProperty( + locator_model_wire_color: FloatVectorProperty( name="Model Loc Color", description="Color for model locators in 3D views", options={'HIDDEN'}, @@ -600,7 +696,7 @@ def base_paint_color_update(self, context): default=(0.08, 0.12, 0.25), update=locator_model_wire_color_update, ) - locator_coll_wire_color = FloatVectorProperty( + locator_coll_wire_color: FloatVectorProperty( name="Collider Loc Wire Color", description="Color for collider locators' wireframe in 3D views", options={'HIDDEN'}, @@ -609,7 +705,7 @@ def base_paint_color_update(self, context): default=(0.5, 0.3, 0.1), update=locator_coll_wire_color_update, ) - locator_coll_face_color = FloatVectorProperty( + locator_coll_face_color: FloatVectorProperty( name="Collider Loc Face Color", description="Color for collider locators' faces in 3D views", options={'HIDDEN'}, @@ -619,13 +715,13 @@ def base_paint_color_update(self, context): # default=(0.065, 0.18, 0.3), update=locator_coll_face_color_update, ) - display_connections = BoolProperty( + display_connections: BoolProperty( name="Display Connections", description="Display connections in 3D views", default=True, update=display_connections_update, ) - curve_segments = IntProperty( + curve_segments: IntProperty( name="Curve Segments", description="Curve segment number for displaying in 3D views", default=16, @@ -635,13 +731,13 @@ def base_paint_color_update(self, context): subtype='NONE', update=curve_segments_update, ) - optimized_connections_drawing = BoolProperty( + optimized_connections_drawing: BoolProperty( name="Optimized Connections Draw", description="Draw connections only when data are updated ( switching this off might give you FPS )", default=False, update=optimized_connections_drawing_update, ) - np_connection_base_color = FloatVectorProperty( + np_connection_base_color: FloatVectorProperty( name="Nav Curves Color", description="Base color for navigation curves in 3D views", options={'HIDDEN'}, @@ -650,7 +746,7 @@ def base_paint_color_update(self, context): default=(0.0, 0.1167, 0.1329), update=np_curve_color_update, ) - mp_connection_base_color = FloatVectorProperty( + mp_connection_base_color: FloatVectorProperty( name="Map Line Color", description="Base color for map line connections in 3D views", options={'HIDDEN'}, @@ -659,7 +755,7 @@ def base_paint_color_update(self, context): default=(0.0, 0.2234, 0.0982), update=mp_line_color_update, ) - tp_connection_base_color = FloatVectorProperty( + tp_connection_base_color: FloatVectorProperty( name="Trigger Line Color", description="Base color for trigger line connections in 3D views", options={'HIDDEN'}, @@ -668,8 +764,8 @@ def base_paint_color_update(self, context): default=(0.1706, 0.0, 0.0593), update=tp_line_color_update, ) - display_info = EnumProperty( - name="Display Text Info", + display_info: EnumProperty( + name="Display Info Texts", description="Display additional text information in 3D views", items=( ('none', "None", "No additional information displayed in 3D view"), @@ -682,8 +778,8 @@ def base_paint_color_update(self, context): default='none', update=display_info_update, ) - info_text_color = FloatVectorProperty( - name="Info Text Color", + info_text_color: FloatVectorProperty( + name="Info Texts Color", description="Base color for information text in 3D views", options={'HIDDEN'}, subtype='COLOR', @@ -692,14 +788,14 @@ def base_paint_color_update(self, context): update=info_text_color_update, ) - show_preview_models = BoolProperty( + show_preview_models: BoolProperty( name="Show Preview Models", description="Show preview models for locators", default=True, update=show_preview_models_update ) - base_paint_color = FloatVectorProperty( + base_paint_color: FloatVectorProperty( name="Base Paint Color", description="Color used on shaders using paint flavor.\n" "This color is mixed in any shader using base paint color from game e.g. paint flavored and truckpaint shader", @@ -710,6 +806,15 @@ def base_paint_color_update(self, context): update=base_paint_color_update, ) + icon_theme: EnumProperty( + name="Icon Theme", + description="Theme of SCS Blender Tools custom icons.", + options={'HIDDEN'}, + items=retrieve_icon_theme_items, + get=get_icon_theme_item, + set=set_icon_theme_item, + ) + # IMPORT & EXPORT SETTINGS SAVED IN CONFIG def import_scale_update(self, context): _config_container.update_item_in_file('Import.ImportScale', float(self.import_scale)) @@ -852,7 +957,7 @@ def export_write_signature_update(self, context): return None # IMPORT OPTIONS - import_scale = FloatProperty( + import_scale: FloatProperty( name="Scale", description="Import scale of model", min=0.001, max=1000.0, @@ -860,75 +965,75 @@ def export_write_signature_update(self, context): default=1.0, update=import_scale_update, ) - import_preserve_path_for_export = BoolProperty( + import_preserve_path_for_export: BoolProperty( name="Preserve Path for Export", description="Automatically use and set custom export path on SCS Root to the same path that it was imported from.", default=False, update=import_preserve_path_for_export_update, ) - import_pim_file = BoolProperty( + import_pim_file: BoolProperty( name="Import Model (PIM)", description="Import Model data from PIM file", default=True, update=import_pim_file_update, ) - import_use_welding = BoolProperty( + import_use_welding: BoolProperty( name="Use Welding", description="Use automatic routine for welding of divided mesh surfaces", default=True, update=import_use_welding_update, ) - import_welding_precision = IntProperty( + import_welding_precision: IntProperty( name="Welding Precision", description="Number of decimals which has to be equal for welding to take place.", min=1, max=6, default=4, update=import_welding_precision_update ) - import_use_normals = BoolProperty( + import_use_normals: BoolProperty( name="Use Normals", description="When used meshes will get custom normals data from PIM file; " "otherwise Blender calculated normals will be used.", default=True, update=import_use_normals_update ) - import_pit_file = BoolProperty( + import_pit_file: BoolProperty( name="Import Trait (PIT)", description="Import Trait information from PIT file", default=True, update=import_pit_file_update, ) - import_load_textures = BoolProperty( + import_load_textures: BoolProperty( name="Load Textures", description="Load textures", default=True, update=import_load_textures_update, ) - import_pic_file = BoolProperty( + import_pic_file: BoolProperty( name="Import Collision (PIC)", description="Import Collision envelops from PIC file", default=True, update=import_pic_file_update, ) - import_pip_file = BoolProperty( + import_pip_file: BoolProperty( name="Import Prefab (PIP)", description="Import Prefab from PIP file", default=True, update=import_pip_file_update, ) - import_pis_file = BoolProperty( + import_pis_file: BoolProperty( name="Import Skeleton (PIS)", description="Import Skeleton from PIS file", default=True, update=import_pis_file_update, ) - import_connected_bones = BoolProperty( + import_connected_bones: BoolProperty( name="Create Connected Bones", description="Create connected Bones whenever possible", default=False, update=import_connected_bones_update, ) - import_bone_scale = FloatProperty( + import_bone_scale: FloatProperty( name="Bone Scale", description="Import scale for Bones", min=0.0001, max=10.0, @@ -936,13 +1041,13 @@ def export_write_signature_update(self, context): default=0.1, update=import_bone_scale_update, ) - import_pia_file = BoolProperty( + import_pia_file: BoolProperty( name="Import Animations (PIA)", description="Import Animations from all corresponding PIA files found in the same directory", default=True, update=import_pia_file_update, ) - import_include_subdirs_for_pia = BoolProperty( + import_include_subdirs_for_pia: BoolProperty( name="Search Subdirectories", description="Search also all subdirectories for animation files (PIA)", default=True, @@ -950,7 +1055,7 @@ def export_write_signature_update(self, context): ) # EXPORT OPTIONS - export_scope = EnumProperty( + export_scope: EnumProperty( name="Export Scope", items=( ('selection', "Selection", "Export selected objects only"), @@ -959,7 +1064,7 @@ def export_write_signature_update(self, context): ), default='scene', ) - export_scale = FloatProperty( + export_scale: FloatProperty( name="Export Scale", description="Export scale of model", min=0.01, max=1000.0, @@ -967,13 +1072,13 @@ def export_write_signature_update(self, context): default=1.0, update=export_scale_update, ) - export_apply_modifiers = BoolProperty( + export_apply_modifiers: BoolProperty( name="Apply Modifiers", description="Export meshes as modifiers were applied", default=True, update=export_apply_modifiers_update, ) - export_exclude_edgesplit = BoolProperty( + export_exclude_edgesplit: BoolProperty( name="Exclude 'Edge Split'", description="When you use Sharp Edge flags, then prevent 'Edge Split' modifier from " "dismemberment of the exported mesh - the correct smoothing will be still " @@ -981,33 +1086,33 @@ def export_write_signature_update(self, context): default=True, update=export_exclude_edgesplit_update, ) - export_include_edgesplit = BoolProperty( - name="Apply Only 'Edge Split'", + export_include_edgesplit: BoolProperty( + name="Apply 'Edge Split'", description="When you use Sharp Edge flags and don't want to apply modifiers, " "then use only 'Edge Split' modifier for dismemberment of the exported mesh " "- the only way to preserve correct smoothing", default=True, update=export_include_edgesplit_update, ) - export_active_uv_only = BoolProperty( + export_active_uv_only: BoolProperty( name="Only Active UVs", description="Export only active UV layer coordinates", default=False, update=export_active_uv_only_update, ) - export_vertex_groups = BoolProperty( + export_vertex_groups: BoolProperty( name="Vertex Groups", description="Export all existing 'Vertex Groups'", default=True, update=export_vertex_groups_update, ) - export_vertex_color = BoolProperty( + export_vertex_color: BoolProperty( name="Vertex Color", description="Export active vertex color layer", default=True, update=export_vertex_color_update, ) - export_vertex_color_type = EnumProperty( + export_vertex_color_type: EnumProperty( name="Vertex Color Type", description="Vertex color type", items=( @@ -1017,7 +1122,7 @@ def export_write_signature_update(self, context): default='rgbda', update=export_vertex_color_type_update, ) - export_vertex_color_type_7 = EnumProperty( + export_vertex_color_type_7: EnumProperty( name="Vertex Color Type", description="Vertex color type", items=( @@ -1028,13 +1133,13 @@ def export_write_signature_update(self, context): default='rgbda', update=export_vertex_color_type_7_update, ) - export_pim_file = BoolProperty( + export_pim_file: BoolProperty( name="Export Model", description="Export model automatically with file save", default=True, update=export_pim_file_update, ) - export_output_type = EnumProperty( + export_output_type: EnumProperty( name="Output Format", items=( ('5', "Game Data Format, ver. 5", "Export PIM (version 5) file formats for SCS Game Engine"), @@ -1044,37 +1149,37 @@ def export_write_signature_update(self, context): default='5', update=export_output_type_update, ) - export_pit_file = BoolProperty( + export_pit_file: BoolProperty( name="Export PIT", description="PIT...", default=True, update=export_pit_file_update, ) - export_pic_file = BoolProperty( + export_pic_file: BoolProperty( name="Export Collision", description="Export collision automatically with file save", default=True, update=export_pic_file_update, ) - export_pip_file = BoolProperty( + export_pip_file: BoolProperty( name="Export Prefab", description="Export prefab automatically with file save", default=True, update=export_pip_file_update, ) - export_pis_file = BoolProperty( + export_pis_file: BoolProperty( name="Export Skeleton", description="Export skeleton automatically with file save", default=True, update=export_pis_file_update, ) - export_pia_file = BoolProperty( + export_pia_file: BoolProperty( name="Export Animations", description="Export animations automatically with file save", default=True, update=export_pia_file_update, ) - export_write_signature = BoolProperty( + export_write_signature: BoolProperty( name="Write A Signature To Exported Files", description="Add a signature to the header of the output files with some additional information", default=False, @@ -1095,7 +1200,7 @@ def config_storage_place_update(self, context): return None - dump_level = EnumProperty( + dump_level: EnumProperty( name="Printouts", items=( ('0', "0 - Errors Only", "Print only Errors to the console"), @@ -1108,7 +1213,7 @@ def config_storage_place_update(self, context): default='2', update=dump_level_update, ) - config_storage_place = EnumProperty( + config_storage_place: EnumProperty( name="Use Global Settings", description="Defines place for storage of Global Settings. By default Common Config File is used for globals to be stored per machine.", items=( @@ -1120,17 +1225,17 @@ def config_storage_place_update(self, context): ) # COMMON SETTINGS - NOT SAVED IN CONFIG - preview_export_selection = BoolProperty( + preview_export_selection: BoolProperty( name="Preview selection", description="Preview selection which will be exported", default=True, ) - preview_export_selection_active = BoolProperty( + preview_export_selection_active: BoolProperty( name="Flag indication if selection preview is active", description="", default=False, ) - last_load_bt_version = StringProperty( + last_load_bt_version: StringProperty( name="Last Load BT Version", description="Version string of SCS Blender Tools on last file load (used for applying fixes on older versions)", default="0.0", @@ -1147,15 +1252,15 @@ def conv_hlpr_converters_path_update(self, context): _config_container.update_item_in_file('Paths.ConvertersPath', self.conv_hlpr_converters_path) - conv_hlpr_converters_path = StringProperty( + conv_hlpr_converters_path: StringProperty( name="Converters Path", description="Path to SCS conversion tools directory (needed only if you use Conversion Helper).", subtype="DIR_PATH", update=conv_hlpr_converters_path_update, - default="" ) - class ConvHlprCustomPathEntry(bpy.types.PropertyGroup): + class ConvHlprCustomPathInventoryItem(bpy.types.PropertyGroup): def path_update(self, context): @@ -1164,175 +1269,119 @@ def path_update(self, context): from io_scs_tools.utils.path import repair_path self.path = repair_path(self.path) - path = StringProperty(subtype='DIR_PATH') + path: StringProperty(subtype='DIR_PATH') - conversion_helper_expand = BoolProperty( - name="Expand Conversion Helper", - description="Expand Conversion Helper", - default=True, - ) - conv_hlpr_use_custom_paths = BoolProperty( + conv_hlpr_use_custom_paths: BoolProperty( name="Use Custom Paths", description="Enable/disable custom paths used for converting more targets at once", default=False ) - conv_hlpr_custom_paths = CollectionProperty( - type=ConvHlprCustomPathEntry, + conv_hlpr_custom_paths: CollectionProperty( + type=ConvHlprCustomPathInventoryItem, description="Custom paths used for converting more targets to one" ) - conv_hlpr_custom_paths_active = IntProperty( + conv_hlpr_custom_paths_active: IntProperty( description="Currently selected custom path" ) - conv_hlpr_mod_destination = StringProperty( + conv_hlpr_mod_destination: StringProperty( name="Mod Destination", description="Destination folder where mod will be packed to.", subtype="DIR_PATH", default="" ) - conv_hlpr_mod_name = StringProperty( + conv_hlpr_mod_name: StringProperty( name="Mod Name", description="Name of the packed mod zip file", default="" ) - conv_hlpr_clean_on_packing = BoolProperty( - name="Auto Clean", + conv_hlpr_clean_on_packing: BoolProperty( + name="Clean", description="Clean converted data directory before exporting & converting & packing (usually used when firstly packing a mod).", default=False ) - conv_hlpr_export_on_packing = BoolProperty( - name="Auto Export", + conv_hlpr_export_on_packing: BoolProperty( + name="Export", description="Export before converting & packing (it will use last successful batch export action).", default=True ) - conv_hlpr_convert_on_packing = BoolProperty( - name="Auto Convert", + conv_hlpr_convert_on_packing: BoolProperty( + name="Convert", description="Execute convert before packing (targets that will be converted: SCS Project Base Path + Custom Paths if enabled).", default=True ) - conv_hlpr_mod_compression = EnumProperty( + conv_hlpr_mod_compression: EnumProperty( + name="Packing Method", description="Compression method for mod packing", items=( - (_CONV_HLPR_consts.NoZip, "No Archive", "Create mod folder package instead of ZIP"), - (_CONV_HLPR_consts.StoredZip, "No Compression", "No compression done to package"), - (_CONV_HLPR_consts.DeflatedZip, "Deflated", "Use ZIP deflated compression method"), - (_CONV_HLPR_consts.Bzip2Zip, "BZIP2", "Uses bzip2 compression method"), + (_CONV_HLPR_consts.NoZip, "Folder", "Create mod folder package instead of ZIP"), + (_CONV_HLPR_consts.StoredZip, "ZIP (Uncompressed)", "No compression done to package"), + (_CONV_HLPR_consts.DeflatedZip, "ZIP (Moderate)", "Use ZIP deflated compression method"), + # (_CONV_HLPR_consts.Bzip2Zip, "ZIP (Best)", "Uses bzip2 compression method"), ), default=_CONV_HLPR_consts.DeflatedZip ) # SUN PROFILE SETTINGS - class SunProfileInventoryItem(bpy.types.PropertyGroup): - """ - Sun profile properties used to load climate profiles and create lighting scene from them. - """ - - name = StringProperty(name="Name", default="") - - low_elevation = IntProperty( - name="Low Elevation", - options={'HIDDEN'}, - ) - high_elevation = IntProperty( - name="High Elevation", - options={'HIDDEN'}, - ) - - sun_direction = IntProperty( - name="Sun Elevation Direction", - options={'HIDDEN'}, - ) - - ambient = FloatVectorProperty( - name="Ambient", - options={'HIDDEN'}, - subtype='COLOR', - size=3, - min=-5, max=5, - soft_min=0, soft_max=1, - step=3, precision=2, - ) - ambient_hdr_coef = FloatProperty( - name="Ambient HDR Coeficient", - options={'HIDDEN'}, - ) - - diffuse = FloatVectorProperty( - name="Diffuse", - options={'HIDDEN'}, - subtype='COLOR', - size=3, - min=-5, max=5, - soft_min=0, soft_max=1, - step=3, precision=2, - ) - diffuse_hdr_coef = FloatProperty( - name="Diffuse HDR Coeficient", - options={'HIDDEN'}, - ) - - specular = FloatVectorProperty( - name="Specular", - options={'HIDDEN'}, - subtype='COLOR', - size=3, - min=-5, max=5, - soft_min=0, soft_max=1, - step=3, precision=2, - ) - specular_hdr_coef = FloatProperty( - name="Specular HDR Coeficient", - options={'HIDDEN'}, - ) - - sun_color = FloatVectorProperty( - name="Sun Color", - options={'HIDDEN'}, - subtype='COLOR', - size=3, - min=-5, max=5, - soft_min=0, soft_max=1, - step=3, precision=2, - ) - sun_color_hdr_coef = FloatProperty( - name="Sun Color HDR Coeficient", - options={'HIDDEN'}, - ) - - env = FloatProperty( - name="Env Factor", - options={'HIDDEN'}, - ) - env_static_mod = FloatProperty( - name="Env Static Modulator", - options={'HIDDEN'}, - ) - sun_profiles_inventory = CollectionProperty( - type=SunProfileInventoryItem, - options={'SKIP_SAVE'}, - ) + def use_scs_lighting_update(self, context): + if self.use_scs_lighting: + bpy.ops.world.scs_tools_setup_lighting() + else: + bpy.ops.world.scs_tools_disable_lighting() - sun_profiles_inventory_active = IntProperty( - name="Currently Active Sun Profile", + use_scs_lighting: BoolProperty( + name="Use SCS Lighting", + description="When enabled SCS lighting setup from climate sun profiles will be used, otherwise generic lighting will be used.", + update=use_scs_lighting_update ) def sun_profiles_path_update(self, context): - _config_container.update_sun_profiles_library_path(_get_scs_globals().sun_profiles_inventory, - self.sun_profiles_lib_path) - return - - sun_profiles_lib_path = StringProperty( + _config_container.update_sun_profiles_library_path(self.sun_profiles_lib_path) + + # add default sun profile, if invalid path, so that user will be able to manipulate with lighting settings + if not _path_utils.is_valid_sun_profiles_library_path(): + sun_profile_item = _get_scs_inventories().sun_profiles.add() + sun_profile_item.name = "reference" + sun_profile_item.low_elevation = 60 + sun_profile_item.high_elevation = 60 + sun_profile_item.ambient = _LIGHTING_consts.default_ambient + sun_profile_item.diffuse = _LIGHTING_consts.default_diffuse + sun_profile_item.specular = _LIGHTING_consts.default_specular + sun_profile_item.env = _LIGHTING_consts.default_env + sun_profile_item.env_static_mod = _LIGHTING_consts.default_env + sun_profile_item.is_blocked = False + + # reset active sun profile and trigger sun profile update + self.sun_profiles_active = 0 + + sun_profiles_lib_path: StringProperty( name="Sun Profiles Library", description="Relative or absolute path to sun profiles definition file.", default="//def/climate/default/nice.sii", update=sun_profiles_path_update ) + def sun_profiles_active_update(self, context): + """Update lighting in the scene with newly active sun profile. + + :param context: + :type context: + """ + if self.use_scs_lighting: + bpy.ops.world.scs_tools_setup_lighting(sun_profile_index=self.sun_profiles_active) + else: + bpy.ops.world.scs_tools_disable_lighting() + + sun_profiles_active: IntProperty( + name="Currently Active Sun Profile", + update=sun_profiles_active_update + ) + def lighting_scene_east_direction_update(self, context): _lighting_east_lock_callback.set_lighting_east() return - lighting_scene_east_direction = IntProperty( + lighting_scene_east_direction: IntProperty( name="SCS Lighting East", description="Defines east position in lighting scene (changing it will change direction of diffuse and specular light).", default=0, min=0, max=360, step=10, subtype='ANGLE', @@ -1348,8 +1397,52 @@ def lighting_east_lock_update(self, context): return - lighting_east_lock = BoolProperty( + lighting_east_lock: BoolProperty( name="Lock East", description="Locks east side of SCS lighting to view. When used lamps will follow rotation of camera in 3D view.", update=lighting_east_lock_update, ) + + +class SCSAddonPrefs(bpy.types.AddonPreferences): + """ + SCS Addon Preferences + """ + bl_idname = "io_scs_tools" + + scs_inventories: bpy.props.PointerProperty( + name="SCS Inventories", + type=SCSInventories, + description="SCS Inventories" + ) + + scs_globals: bpy.props.PointerProperty( + name="SCS Globals", + type=SCSGlobals, + description="SCS Global Variables" + ) + + +classes = ( + SCSInventories.TriggerActionsInventoryItem, + SCSInventories.SignModelInventoryItem, + SCSInventories.TSemProfileInventoryItem, + SCSInventories.TrafficRulesInventoryItem, + SCSInventories.HookupInventoryItem, + SCSInventories.MatSubsInventoryItem, + SCSInventories.SunProfileInventoryItem, + SCSInventories, + SCSGlobals.ConvHlprCustomPathInventoryItem, + SCSGlobals, + SCSAddonPrefs, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/addon/io_scs_tools/properties/dynamic/__init__.py b/addon/io_scs_tools/properties/dynamic/__init__.py new file mode 100644 index 0000000..ece47ad --- /dev/null +++ b/addon/io_scs_tools/properties/dynamic/__init__.py @@ -0,0 +1,140 @@ +# ##### 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 + +import bpy + +from io_scs_tools.properties.dynamic import scene +from io_scs_tools.properties.dynamic import object + + +class DynamicProps: + class Scopes: + """Scopes to distingish variable names to avoid registration of dynamic properties with same names.""" + __prefix = "dynamic_" + + SCS_GLOBALS = __prefix + "scs_globals" + + __registered_props = {} + + @staticmethod + def __register_property__(scope, property_name): + """Registered given property name for given class type. + + :param scope: scope in which this porperty will be registered + :type scope: str + :param property_name: name of the property + :type property_name: str + :return: True if property can be registered; False if property is already registered for given class type + :rtype: bool + """ + + if scope not in DynamicProps.__registered_props: + DynamicProps.__registered_props[scope] = set() + + if property_name in DynamicProps.__registered_props: + return False + + DynamicProps.__registered_props[scope].add(property_name) + return True + + @staticmethod + def register(scope, property_name, property_type, default): + """Registers runtime property that won't be saved in blend file. + + :param scope: scope in which this porperty will be registered + :type scope: str + :param property_name: name of the property under which it will be saved + :type property_name: str + :param property_type: type of the property: str | float | bool | int | tuple | list + :type property_type: type + :param default: default value to be returned + :type default: any + :return: property implementation + :rtype: property + """ + + def getter(self): + """Returns property value. + + :param self: instance of object on which property will be registered + :type self: object + :return: default or saved value + :rtype: any + """ + assert self + + prefs = bpy.context.preferences.addons["io_scs_tools"].preferences + + if scope not in prefs: + return default + + scoped_prefs = prefs[scope] + + if property_name not in scoped_prefs: + return default + + return scoped_prefs[property_name] + + def setter(self, value): + """Sets value to the property. + + :param self: instance of object on which property will be registered + :type self: obj + :param value: value to save into this property + :type value: str | float | int | bool + """ + assert self + assert isinstance(value, property_type) + + prefs = bpy.context.preferences.addons["io_scs_tools"].preferences + + if scope not in prefs: + prefs[scope] = {} + + prefs[scope][property_name] = value + + # check for default value type + assert isinstance(default, property_type) + + # make sure registration is unique + assert DynamicProps.__register_property__(scope, property_name) + + return property(getter, setter) + + @staticmethod + def get_registered_members(scope): + """Gets property names as a tuple. + + :return: tuple of dynamic property names + :rtype: set[str] + """ + assert scope in DynamicProps.__registered_props + + return DynamicProps.__registered_props[scope] + + +def register(): + scene.register() + object.register() + + +def unregister(): + scene.register() + object.register() diff --git a/addon/io_scs_tools/properties/dynamic/scene.py b/addon/io_scs_tools/properties/dynamic/scene.py index acff7e4..9a59dcb 100644 --- a/addon/io_scs_tools/properties/dynamic/scene.py +++ b/addon/io_scs_tools/properties/dynamic/scene.py @@ -41,8 +41,8 @@ def _get_active_scs_root(self): if "scs_cached_active_scs_root" not in self: # if there is no active object set empty string - if self.objects.active: - name = self.objects.active.name + if bpy.context.active_object: + name = bpy.context.active_object.name else: name = "" diff --git a/addon/io_scs_tools/properties/material.py b/addon/io_scs_tools/properties/material.py index f883721..6334df1 100644 --- a/addon/io_scs_tools/properties/material.py +++ b/addon/io_scs_tools/properties/material.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2015: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy from bpy.props import (BoolProperty, @@ -110,6 +110,17 @@ def __update_shader_texture_tobj_file__(self, context, tex_type): texture_raw_path = getattr(self, shader_texture_str) texture_settings = getattr(self, shader_texture_str + "_settings") + # reflect updated settings in shaders + texture_settings_str = "" + texture_settings_str += "1" if "color_space_linear" in texture_settings else "0" + texture_settings_str += "1" if "tsnormal" in texture_settings else "0" + texture_settings_str += "1" if "u_repeat" in texture_settings else "0" + texture_settings_str += "1" if "v_repeat" in texture_settings else "0" + + material = _material_utils.get_material_from_context(context) + _shader.set_texture_settings(material, tex_type, texture_settings_str) + + # write settings to TOBJ tobj_file = _path_utils.get_tobj_path_from_shader_texture(texture_raw_path) if tobj_file: @@ -155,9 +166,9 @@ def __update_shader_texture__(self, context, tex_type): shader_texture_filepath = self[shader_texture_str] = _path_utils.get_scs_texture_str(shader_texture_filepath) # create texture and use it on shader - texture = _material_utils.get_texture(shader_texture_filepath, tex_type, report_invalid=True) + image = _material_utils.get_texture_image(shader_texture_filepath, tex_type, report_invalid=True) - _shader.set_texture(material, tex_type, texture) + _shader.set_texture(material, tex_type, image) # reload TOBJ settings _material_utils.reload_tobj_settings(material, tex_type) @@ -182,14 +193,14 @@ def update_value(self, context): if material: _shader.set_attribute(material, self.aux_type, getattr(material.scs_props, "shader_attribute_" + self.aux_type, None)) - value = FloatProperty( + value: FloatProperty( default=0.0, step=0.1, precision=4, options={'HIDDEN'}, update=update_value ) - aux_type = StringProperty() # used in update function to be able to identify which auxiliary is owning this item + aux_type: StringProperty() # used in update function to be able to identify which auxiliary is owning this item class UVMappingItem(bpy.types.PropertyGroup): """Class for saving uv map reference for SCS texture @@ -216,7 +227,7 @@ def update_value(self, context): if self.tex_coord != -1 and tex_mapping.tex_coord == self.tex_coord and tex_mapping.value != self.value: tex_mapping.value = self.value - value = StringProperty( + value: StringProperty( name="Texture UV Set", description="Mapping of UV Set to current texture type", default="", @@ -224,8 +235,8 @@ def update_value(self, context): update=update_value ) - tex_coord = IntProperty(default=-1) # used upon export for mapping uv to tex_coord field - texture_type = StringProperty() # used in update function to be able to identify which texture should be updated + tex_coord: IntProperty(default=-1) # used upon export for mapping uv to tex_coord field + texture_type: StringProperty() # used in update function to be able to identify which texture should be updated class TexCoordItem(bpy.types.PropertyGroup): """Property group holding data how to map uv maps to exported PIM file. It should be used only on material with imported shader. @@ -250,8 +261,8 @@ def update_name(self, context): if self.name != new_name: self.name = new_name - name = StringProperty(update=update_name) - value = StringProperty(description="UV map used for this tex_coord field", update=__update_look__) + name: StringProperty(update=update_name) + value: StringProperty(description="UV map used for this tex_coord field", update=__update_look__) @staticmethod def get_texture_types(): @@ -438,13 +449,13 @@ def update_shader_texture_reflection_settings(self, context): _cached_mat_num = -1 """Caching number of all materials to properly fix material ids when duplicating material""" - active_custom_tex_coord = IntProperty( + active_custom_tex_coord: IntProperty( name="Active TexCoord Mapping", description="Must have for showing the TexCoord mappings in the template list", default=0 ) - active_shader_preset_name = StringProperty( + active_shader_preset_name: StringProperty( name="Active Shader Preset Name", description="Active shader preset name for active Material", default="", @@ -452,25 +463,25 @@ def update_shader_texture_reflection_settings(self, context): update=update_active_shader_preset_name ) - custom_tex_coord_maps = CollectionProperty( + custom_tex_coord_maps: CollectionProperty( description="List of tex coord fields used for determinating order and number of exported uv layers. " "The number of the tex coord field specify order of exporting.", type=TexCoordItem, ) - enable_aliasing = BoolProperty( + enable_aliasing: BoolProperty( name="Aliasing", description="Enabling material aliasing on this material.", default=True ) - id = IntProperty( + id: IntProperty( name="Material ID", description="Material unique ID inside blend file, this way materials can be easily identified for looks", get=get_id ) - mat_effect_name = StringProperty( + mat_effect_name: StringProperty( name="Shader Effect Name", description="SCS shader effect name", default="", @@ -479,7 +490,7 @@ def update_shader_texture_reflection_settings(self, context): ) # SHADER ATTRIBUTES - shader_attribute_add_ambient = FloatProperty( + shader_attribute_add_ambient: FloatProperty( name="Add Ambient", description="SCS shader 'Add Ambient' value", default=0.0, @@ -491,70 +502,70 @@ def update_shader_texture_reflection_settings(self, context): update=update_shader_attribute_add_ambient, ) - shader_attribute_aux0 = CollectionProperty( + shader_attribute_aux0: CollectionProperty( name="Aux [0]", type=AuxiliaryItem, description="SCS shader 'Aux [0]' value", options={'HIDDEN'}, ) - shader_attribute_aux1 = CollectionProperty( + shader_attribute_aux1: CollectionProperty( name="Aux [1]", type=AuxiliaryItem, description="SCS shader 'Aux [1]' value", options={'HIDDEN'}, ) - shader_attribute_aux2 = CollectionProperty( + shader_attribute_aux2: CollectionProperty( name="Aux [2]", type=AuxiliaryItem, description="SCS shader 'Aux [2]' value", options={'HIDDEN'}, ) - shader_attribute_aux3 = CollectionProperty( + shader_attribute_aux3: CollectionProperty( name="Aux [3]", type=AuxiliaryItem, description="SCS shader 'Aux [3]' value", options={'HIDDEN'}, ) - shader_attribute_aux4 = CollectionProperty( + shader_attribute_aux4: CollectionProperty( name="Aux [4]", type=AuxiliaryItem, description="SCS shader 'Aux [4]' value", options={'HIDDEN'}, ) - shader_attribute_aux5 = CollectionProperty( + shader_attribute_aux5: CollectionProperty( name="Aux [5]", type=AuxiliaryItem, description="SCS shader 'Aux [5]' value", options={'HIDDEN'}, ) - shader_attribute_aux6 = CollectionProperty( + shader_attribute_aux6: CollectionProperty( name="Aux [6]", type=AuxiliaryItem, description="SCS shader 'Aux [6]' value", options={'HIDDEN'}, ) - shader_attribute_aux7 = CollectionProperty( + shader_attribute_aux7: CollectionProperty( name="Aux [7]", type=AuxiliaryItem, description="SCS shader 'Aux [7]' value", options={'HIDDEN'}, ) - shader_attribute_aux8 = CollectionProperty( + shader_attribute_aux8: CollectionProperty( name="Aux [8]", type=AuxiliaryItem, description="SCS shader 'Aux [8]' value", options={'HIDDEN'}, ) - shader_attribute_diffuse = FloatVectorProperty( + shader_attribute_diffuse: FloatVectorProperty( name="Diffuse", description="SCS shader 'Diffuse' value", default=(0.0, 0.0, 0.0), @@ -567,7 +578,7 @@ def update_shader_texture_reflection_settings(self, context): update=update_shader_attribute_diffuse, ) - shader_attribute_env_factor = FloatVectorProperty( + shader_attribute_env_factor: FloatVectorProperty( name="Env Factor", description="SCS shader 'Env Factor' value", default=(1.0, 1.0, 1.0), @@ -580,7 +591,7 @@ def update_shader_texture_reflection_settings(self, context): update=update_shader_attribute_env_factor, ) - shader_attribute_fresnel = FloatVectorProperty( + shader_attribute_fresnel: FloatVectorProperty( name="Fresnel", description="SCS shader 'Fresnel' value", default=(0.2, 0.9), @@ -593,7 +604,7 @@ def update_shader_texture_reflection_settings(self, context): update=update_shader_attribute_fresnel, ) - shader_attribute_reflection = FloatProperty( + shader_attribute_reflection: FloatProperty( name="Reflection", description="SCS shader 'Reflection' value", default=0.0, @@ -604,7 +615,7 @@ def update_shader_texture_reflection_settings(self, context): update=__update_look__, ) - shader_attribute_reflection2 = FloatProperty( + shader_attribute_reflection2: FloatProperty( name="Reflection 2", description="SCS shader 'Reflection 2' value", default=0.0, @@ -615,7 +626,7 @@ def update_shader_texture_reflection_settings(self, context): update=__update_look__, ) - shader_attribute_shadow_bias = FloatProperty( + shader_attribute_shadow_bias: FloatProperty( name="Shadow Bias", description="SCS shader 'Shadow Bias' value", default=0.0, @@ -626,7 +637,7 @@ def update_shader_texture_reflection_settings(self, context): update=__update_look__, ) - shader_attribute_shininess = FloatProperty( + shader_attribute_shininess: FloatProperty( name="Shininess", description="SCS shader 'Shininess' value", default=0.0, @@ -637,7 +648,7 @@ def update_shader_texture_reflection_settings(self, context): update=update_shader_attribute_shininess, ) - shader_attribute_specular = FloatVectorProperty( + shader_attribute_specular: FloatVectorProperty( name="Specular", description="SCS shader 'Specular' value", default=(0.0, 0.0, 0.0), @@ -650,7 +661,7 @@ def update_shader_texture_reflection_settings(self, context): update=update_shader_attribute_specular, ) - shader_attribute_tint = FloatVectorProperty( + shader_attribute_tint: FloatVectorProperty( name="Tint", description="SCS shader 'Tint' value", default=(1.0, 1.0, 1.0), @@ -663,7 +674,7 @@ def update_shader_texture_reflection_settings(self, context): update=update_shader_attribute_tint ) - shader_attribute_tint_opacity = FloatProperty( + shader_attribute_tint_opacity: FloatProperty( name="Tint Opacity", description="SCS shader 'Tint Opacity' value", default=0.0, @@ -673,16 +684,19 @@ def update_shader_texture_reflection_settings(self, context): update=update_shader_attribute_tint_opacity ) - shader_attribute_queue_bias = IntProperty( + shader_attribute_queue_bias: IntProperty( name="Queue Bias", description="SCS shader 'Queue Bias' value", - default=2, + default=0, + step=2, + min=0, + max=2, options={'HIDDEN'}, update=__update_look__ ) # TEXTURE: BASE - shader_texture_base = StringProperty( + shader_texture_base: StringProperty( name="Texture Base", description="Texture Base for active Material", default=_MAT_consts.unset_bitmap_filepath, @@ -690,23 +704,23 @@ def update_shader_texture_reflection_settings(self, context): subtype='NONE', update=update_shader_texture_base, ) - shader_texture_base_imported_tobj = StringProperty( + shader_texture_base_imported_tobj: StringProperty( name="Imported TOBJ Path", description="Use imported TOBJ path reference which will be exported into material (NOTE: export will not take care of any TOBJ files!)", default="", update=__update_look__ ) - shader_texture_base_locked = BoolProperty( + shader_texture_base_locked: BoolProperty( name="Texture Locked", description="Tells if texture is locked and should not be changed by user(intended for internal usage only)", default=False ) - shader_texture_base_map_type = StringProperty( + shader_texture_base_map_type: StringProperty( name="Texture Map Type", description="Stores texture mapping type and should not be changed by user(intended for internal usage only)", default="2d" ) - shader_texture_base_settings = EnumProperty( + shader_texture_base_settings: EnumProperty( name="Settings", description="TOBJ settings for this texture", items=__get_texture_settings__(), @@ -714,18 +728,18 @@ def update_shader_texture_reflection_settings(self, context): options={'ENUM_FLAG'}, update=update_shader_texture_base_settings ) - shader_texture_base_tobj_load_time = StringProperty( + shader_texture_base_tobj_load_time: StringProperty( name="Last TOBJ load time", description="Time string of last loading", default="", ) - shader_texture_base_use_imported = BoolProperty( + shader_texture_base_use_imported: BoolProperty( name="Use Imported", description="Use custom provided path for TOBJ reference", default=False, update=__update_look__ ) - shader_texture_base_uv = CollectionProperty( + shader_texture_base_uv: CollectionProperty( name="Texture Base UV Sets", description="Texture base UV sets for active Material", type=UVMappingItem, @@ -733,7 +747,7 @@ def update_shader_texture_reflection_settings(self, context): ) # TEXTURE: BASE_1 - shader_texture_base_1 = StringProperty( + shader_texture_base_1: StringProperty( name="Texture Base 1", description="Texture Base for active Material", default=_MAT_consts.unset_bitmap_filepath, @@ -741,23 +755,23 @@ def update_shader_texture_reflection_settings(self, context): subtype='NONE', update=update_shader_texture_base_1, ) - shader_texture_base_1_imported_tobj = StringProperty( + shader_texture_base_1_imported_tobj: StringProperty( name="Imported TOBJ Path", description="Use imported TOBJ path reference which will be exported into material (NOTE: export will not take care of any TOBJ files!)", default="", update=__update_look__ ) - shader_texture_base_1_locked = BoolProperty( + shader_texture_base_1_locked: BoolProperty( name="Texture Locked", description="Tells if texture is locked and should not be changed by user(intended for internal usage only)", default=False ) - shader_texture_base_1_map_type = StringProperty( + shader_texture_base_1_map_type: StringProperty( name="Texture Map Type", description="Stores texture mapping type and should not be changed by user(intended for internal usage only)", default="2d" ) - shader_texture_base_1_settings = EnumProperty( + shader_texture_base_1_settings: EnumProperty( name="Settings", description="TOBJ settings for this texture", items=__get_texture_settings__(), @@ -765,18 +779,18 @@ def update_shader_texture_reflection_settings(self, context): options={'ENUM_FLAG'}, update=update_shader_texture_base_1_settings ) - shader_texture_base_1_tobj_load_time = StringProperty( + shader_texture_base_1_tobj_load_time: StringProperty( name="Last TOBJ load time", description="Time string of last loading", default="", ) - shader_texture_base_1_use_imported = BoolProperty( + shader_texture_base_1_use_imported: BoolProperty( name="Use Imported", description="Use custom provided path for TOBJ reference", default=False, update=__update_look__ ) - shader_texture_base_1_uv = CollectionProperty( + shader_texture_base_1_uv: CollectionProperty( name="Texture Base UV Sets", description="Texture base UV sets for active Material", type=UVMappingItem, @@ -784,7 +798,7 @@ def update_shader_texture_reflection_settings(self, context): ) # TEXTURE: DETAIL - shader_texture_detail = StringProperty( + shader_texture_detail: StringProperty( name="Texture Detail", description="Texture Flakenoise for active Material", default=_MAT_consts.unset_bitmap_filepath, @@ -792,23 +806,23 @@ def update_shader_texture_reflection_settings(self, context): subtype='NONE', update=update_shader_texture_detail, ) - shader_texture_detail_imported_tobj = StringProperty( + shader_texture_detail_imported_tobj: StringProperty( name="Imported TOBJ Path", description="Use imported TOBJ path reference which will be exported into material (NOTE: export will not take care of any TOBJ files!)", default="", update=__update_look__ ) - shader_texture_detail_locked = BoolProperty( + shader_texture_detail_locked: BoolProperty( name="Texture Locked", description="Tells if texture is locked and should not be changed by user(intended for internal usage only)", default=False ) - shader_texture_detail_map_type = StringProperty( + shader_texture_detail_map_type: StringProperty( name="Texture Map Type", description="Stores texture mapping type and should not be changed by user(intended for internal usage only)", default="2d" ) - shader_texture_detail_settings = EnumProperty( + shader_texture_detail_settings: EnumProperty( name="Settings", description="TOBJ settings for this texture", items=__get_texture_settings__(), @@ -816,18 +830,18 @@ def update_shader_texture_reflection_settings(self, context): options={'ENUM_FLAG'}, update=update_shader_texture_detail_settings ) - shader_texture_detail_tobj_load_time = StringProperty( + shader_texture_detail_tobj_load_time: StringProperty( name="Last TOBJ load time", description="Time string of last loading", default="", ) - shader_texture_detail_use_imported = BoolProperty( + shader_texture_detail_use_imported: BoolProperty( name="Use Imported", description="Use custom provided path for TOBJ reference", default=False, update=__update_look__ ) - shader_texture_detail_uv = CollectionProperty( + shader_texture_detail_uv: CollectionProperty( name="Texture Detail UV Sets", description="Texture Detail UV sets for active Material", type=UVMappingItem, @@ -835,7 +849,7 @@ def update_shader_texture_reflection_settings(self, context): ) # TEXTURE: FLAKENOISE - shader_texture_flakenoise = StringProperty( + shader_texture_flakenoise: StringProperty( name="Texture Flakenoise", description="Texture Flakenoise for active Material", default=_MAT_consts.unset_bitmap_filepath, @@ -843,23 +857,23 @@ def update_shader_texture_reflection_settings(self, context): subtype='NONE', update=update_shader_texture_flakenoise, ) - shader_texture_flakenoise_imported_tobj = StringProperty( + shader_texture_flakenoise_imported_tobj: StringProperty( name="Imported TOBJ Path", description="Use imported TOBJ path reference which will be exported into material (NOTE: export will not take care of any TOBJ files!)", default="", update=__update_look__ ) - shader_texture_flakenoise_locked = BoolProperty( + shader_texture_flakenoise_locked: BoolProperty( name="Texture Locked", description="Tells if texture is locked and should not be changed by user(intended for internal usage only)", default=False ) - shader_texture_flakenoise_map_type = StringProperty( + shader_texture_flakenoise_map_type: StringProperty( name="Texture Map Type", description="Stores texture mapping type and should not be changed by user(intended for internal usage only)", default="2d" ) - shader_texture_flakenoise_settings = EnumProperty( + shader_texture_flakenoise_settings: EnumProperty( name="Settings", description="TOBJ settings for this texture", items=__get_texture_settings__(), @@ -867,18 +881,18 @@ def update_shader_texture_reflection_settings(self, context): options={'ENUM_FLAG'}, update=update_shader_texture_flakenoise_settings ) - shader_texture_flakenoise_tobj_load_time = StringProperty( + shader_texture_flakenoise_tobj_load_time: StringProperty( name="Last TOBJ load time", description="Time string of last loading", default="", ) - shader_texture_flakenoise_use_imported = BoolProperty( + shader_texture_flakenoise_use_imported: BoolProperty( name="Use Imported", description="Use custom provided path for TOBJ reference", default=False, update=__update_look__ ) - shader_texture_flakenoise_uv = CollectionProperty( + shader_texture_flakenoise_uv: CollectionProperty( name="Texture Flakenoise UV Sets", description="Texture Flakenoise UV sets for active Material", type=UVMappingItem, @@ -886,7 +900,7 @@ def update_shader_texture_reflection_settings(self, context): ) # TEXTURE: IAMOD - shader_texture_iamod = StringProperty( + shader_texture_iamod: StringProperty( name="Texture Iamod", description="Texture Iamod for active Material", default=_MAT_consts.unset_bitmap_filepath, @@ -894,23 +908,23 @@ def update_shader_texture_reflection_settings(self, context): subtype='NONE', update=update_shader_texture_iamod, ) - shader_texture_iamod_imported_tobj = StringProperty( + shader_texture_iamod_imported_tobj: StringProperty( name="Imported TOBJ Path", description="Use imported TOBJ path reference which will be exported into material (NOTE: export will not take care of any TOBJ files!)", default="", update=__update_look__ ) - shader_texture_iamod_locked = BoolProperty( + shader_texture_iamod_locked: BoolProperty( name="Texture Locked", description="Tells if texture is locked and should not be changed by user(intended for internal usage only)", default=False ) - shader_texture_iamod_map_type = StringProperty( + shader_texture_iamod_map_type: StringProperty( name="Texture Map Type", description="Stores texture mapping type and should not be changed by user(intended for internal usage only)", default="2d" ) - shader_texture_iamod_settings = EnumProperty( + shader_texture_iamod_settings: EnumProperty( name="Settings", description="TOBJ settings for this texture", items=__get_texture_settings__(), @@ -918,18 +932,18 @@ def update_shader_texture_reflection_settings(self, context): options={'ENUM_FLAG'}, update=update_shader_texture_iamod_settings ) - shader_texture_iamod_tobj_load_time = StringProperty( + shader_texture_iamod_tobj_load_time: StringProperty( name="Last TOBJ load time", description="Time string of last loading", default="", ) - shader_texture_iamod_use_imported = BoolProperty( + shader_texture_iamod_use_imported: BoolProperty( name="Use Imported", description="Use custom provided path for TOBJ reference", default=False, update=__update_look__ ) - shader_texture_iamod_uv = CollectionProperty( + shader_texture_iamod_uv: CollectionProperty( name="Texture Iamod UV Sets", description="Texture Iamod UV sets for active Material", type=UVMappingItem, @@ -937,7 +951,7 @@ def update_shader_texture_reflection_settings(self, context): ) # TEXTURE: LAYER0 - shader_texture_layer0 = StringProperty( + shader_texture_layer0: StringProperty( name="Texture Layer0", description="Texture Layer0 for active Material", default=_MAT_consts.unset_bitmap_filepath, @@ -945,23 +959,23 @@ def update_shader_texture_reflection_settings(self, context): subtype='NONE', update=update_shader_texture_layer0, ) - shader_texture_layer0_imported_tobj = StringProperty( + shader_texture_layer0_imported_tobj: StringProperty( name="Imported TOBJ Path", description="Use imported TOBJ path reference which will be exported into material (NOTE: export will not take care of any TOBJ files!)", default="", update=__update_look__ ) - shader_texture_layer0_locked = BoolProperty( + shader_texture_layer0_locked: BoolProperty( name="Texture Locked", description="Tells if texture is locked and should not be changed by user(intended for internal usage only)", default=False ) - shader_texture_layer0_map_type = StringProperty( + shader_texture_layer0_map_type: StringProperty( name="Texture Map Type", description="Stores texture mapping type and should not be changed by user(intended for internal usage only)", default="2d" ) - shader_texture_layer0_settings = EnumProperty( + shader_texture_layer0_settings: EnumProperty( name="Settings", description="TOBJ settings for this texture", items=__get_texture_settings__(), @@ -969,18 +983,18 @@ def update_shader_texture_reflection_settings(self, context): options={'ENUM_FLAG'}, update=update_shader_texture_layer0_settings ) - shader_texture_layer0_tobj_load_time = StringProperty( + shader_texture_layer0_tobj_load_time: StringProperty( name="Last TOBJ load time", description="Time string of last loading", default="", ) - shader_texture_layer0_use_imported = BoolProperty( + shader_texture_layer0_use_imported: BoolProperty( name="Use Imported", description="Use custom provided path for TOBJ reference", default=False, update=__update_look__ ) - shader_texture_layer0_uv = CollectionProperty( + shader_texture_layer0_uv: CollectionProperty( name="Texture Layer0 UV Sets", description="Texture Layer0 UV sets for active Material", type=UVMappingItem, @@ -988,7 +1002,7 @@ def update_shader_texture_reflection_settings(self, context): ) # TEXTURE: LAYER1 - shader_texture_layer1 = StringProperty( + shader_texture_layer1: StringProperty( name="Texture Layer1", description="Texture Layer1 for active Material", default=_MAT_consts.unset_bitmap_filepath, @@ -996,23 +1010,23 @@ def update_shader_texture_reflection_settings(self, context): subtype='NONE', update=update_shader_texture_layer1, ) - shader_texture_layer1_imported_tobj = StringProperty( + shader_texture_layer1_imported_tobj: StringProperty( name="Imported TOBJ Path", description="Use imported TOBJ path reference which will be exported into material (NOTE: export will not take care of any TOBJ files!)", default="", update=__update_look__ ) - shader_texture_layer1_locked = BoolProperty( + shader_texture_layer1_locked: BoolProperty( name="Texture Locked", description="Tells if texture is locked and should not be changed by user(intended for internal usage only)", default=False ) - shader_texture_layer1_map_type = StringProperty( + shader_texture_layer1_map_type: StringProperty( name="Texture Map Type", description="Stores texture mapping type and should not be changed by user(intended for internal usage only)", default="2d" ) - shader_texture_layer1_settings = EnumProperty( + shader_texture_layer1_settings: EnumProperty( name="Settings", description="TOBJ settings for this texture", items=__get_texture_settings__(), @@ -1020,18 +1034,18 @@ def update_shader_texture_reflection_settings(self, context): options={'ENUM_FLAG'}, update=update_shader_texture_layer1_settings ) - shader_texture_layer1_tobj_load_time = StringProperty( + shader_texture_layer1_tobj_load_time: StringProperty( name="Last TOBJ load time", description="Time string of last loading", default="", ) - shader_texture_layer1_use_imported = BoolProperty( + shader_texture_layer1_use_imported: BoolProperty( name="Use Imported", description="Use custom provided path for TOBJ reference", default=False, update=__update_look__ ) - shader_texture_layer1_uv = CollectionProperty( + shader_texture_layer1_uv: CollectionProperty( name="Texture Layer1 UV Sets", description="Texture Layer1 UV sets for active Material", type=UVMappingItem, @@ -1039,7 +1053,7 @@ def update_shader_texture_reflection_settings(self, context): ) # TEXTURE: LIGHTMAP - shader_texture_lightmap = StringProperty( + shader_texture_lightmap: StringProperty( name="Texture Lightmap", description="Texture Lightmap for active Material", default=_MAT_consts.unset_bitmap_filepath, @@ -1047,23 +1061,23 @@ def update_shader_texture_reflection_settings(self, context): subtype='NONE', update=update_shader_texture_lightmap, ) - shader_texture_lightmap_imported_tobj = StringProperty( + shader_texture_lightmap_imported_tobj: StringProperty( name="Imported TOBJ Path", description="Use imported TOBJ path reference which will be exported into material (NOTE: export will not take care of any TOBJ files!)", default="", update=__update_look__ ) - shader_texture_lightmap_locked = BoolProperty( + shader_texture_lightmap_locked: BoolProperty( name="Texture Locked", description="Tells if texture is locked and should not be changed by user(intended for internal usage only)", default=False ) - shader_texture_lightmap_map_type = StringProperty( + shader_texture_lightmap_map_type: StringProperty( name="Texture Map Type", description="Stores texture mapping type and should not be changed by user(intended for internal usage only)", default="2d" ) - shader_texture_lightmap_settings = EnumProperty( + shader_texture_lightmap_settings: EnumProperty( name="Settings", description="TOBJ settings for this texture", items=__get_texture_settings__(), @@ -1071,18 +1085,18 @@ def update_shader_texture_reflection_settings(self, context): options={'ENUM_FLAG'}, update=update_shader_texture_lightmap_settings ) - shader_texture_lightmap_tobj_load_time = StringProperty( + shader_texture_lightmap_tobj_load_time: StringProperty( name="Last TOBJ load time", description="Time string of last loading", default="", ) - shader_texture_lightmap_use_imported = BoolProperty( + shader_texture_lightmap_use_imported: BoolProperty( name="Use Imported", description="Use custom provided path for TOBJ reference", default=False, update=__update_look__ ) - shader_texture_lightmap_uv = CollectionProperty( + shader_texture_lightmap_uv: CollectionProperty( name="Texture Lightmap UV Sets", description="Texture Lightmap UV sets for active Material", type=UVMappingItem, @@ -1090,7 +1104,7 @@ def update_shader_texture_reflection_settings(self, context): ) # TEXTURE: MASK - shader_texture_mask = StringProperty( + shader_texture_mask: StringProperty( name="Texture Mask", description="Texture Mask for active Material", default=_MAT_consts.unset_bitmap_filepath, @@ -1098,23 +1112,23 @@ def update_shader_texture_reflection_settings(self, context): subtype='NONE', update=update_shader_texture_mask, ) - shader_texture_mask_imported_tobj = StringProperty( + shader_texture_mask_imported_tobj: StringProperty( name="Imported TOBJ Path", description="Use imported TOBJ path reference which will be exported into material (NOTE: export will not take care of any TOBJ files!)", default="", update=__update_look__ ) - shader_texture_mask_locked = BoolProperty( + shader_texture_mask_locked: BoolProperty( name="Texture Locked", description="Tells if texture is locked and should not be changed by user(intended for internal usage only)", default=False ) - shader_texture_mask_map_type = StringProperty( + shader_texture_mask_map_type: StringProperty( name="Texture Map Type", description="Stores texture mapping type and should not be changed by user(intended for internal usage only)", default="2d" ) - shader_texture_mask_settings = EnumProperty( + shader_texture_mask_settings: EnumProperty( name="Settings", description="TOBJ settings for this texture", items=__get_texture_settings__(), @@ -1122,18 +1136,18 @@ def update_shader_texture_reflection_settings(self, context): options={'ENUM_FLAG'}, update=update_shader_texture_mask_settings ) - shader_texture_mask_tobj_load_time = StringProperty( + shader_texture_mask_tobj_load_time: StringProperty( name="Last TOBJ load time", description="Time string of last loading", default="", ) - shader_texture_mask_use_imported = BoolProperty( + shader_texture_mask_use_imported: BoolProperty( name="Use Imported", description="Use custom provided path for TOBJ reference", default=False, update=__update_look__ ) - shader_texture_mask_uv = CollectionProperty( + shader_texture_mask_uv: CollectionProperty( name="Texture Mask UV Sets", description="Texture Mask UV sets for active Material", type=UVMappingItem, @@ -1141,7 +1155,7 @@ def update_shader_texture_reflection_settings(self, context): ) # TEXTURE: MULT - shader_texture_mult = StringProperty( + shader_texture_mult: StringProperty( name="Texture Mult", description="Texture Mult for active Material", default=_MAT_consts.unset_bitmap_filepath, @@ -1149,29 +1163,29 @@ def update_shader_texture_reflection_settings(self, context): subtype='NONE', update=update_shader_texture_mult, ) - shader_texture_mult_imported_tobj = StringProperty( + shader_texture_mult_imported_tobj: StringProperty( name="Imported TOBJ Path", description="Use imported TOBJ path reference which will be exported into material (NOTE: export will not take care of any TOBJ files!)", default="", update=__update_look__ ) - shader_texture_mult_use_imported = BoolProperty( + shader_texture_mult_use_imported: BoolProperty( name="Use Imported", description="Use custom provided path for TOBJ reference", default=False, update=__update_look__ ) - shader_texture_mult_locked = BoolProperty( + shader_texture_mult_locked: BoolProperty( name="Texture Locked", description="Tells if texture is locked and should not be changed by user(intended for internal usage only)", default=False ) - shader_texture_mult_map_type = StringProperty( + shader_texture_mult_map_type: StringProperty( name="Texture Map Type", description="Stores texture mapping type and should not be changed by user(intended for internal usage only)", default="2d" ) - shader_texture_mult_settings = EnumProperty( + shader_texture_mult_settings: EnumProperty( name="Settings", description="TOBJ settings for this texture", items=__get_texture_settings__(), @@ -1179,12 +1193,12 @@ def update_shader_texture_reflection_settings(self, context): options={'ENUM_FLAG'}, update=update_shader_texture_mult_settings ) - shader_texture_mult_tobj_load_time = StringProperty( + shader_texture_mult_tobj_load_time: StringProperty( name="Last TOBJ load time", description="Time string of last loading", default="", ) - shader_texture_mult_uv = CollectionProperty( + shader_texture_mult_uv: CollectionProperty( name="Texture Mult UV Sets", description="Texture Mult UV sets for active Material", type=UVMappingItem, @@ -1192,7 +1206,7 @@ def update_shader_texture_reflection_settings(self, context): ) # TEXTURE: MULT_1 - shader_texture_mult_1 = StringProperty( + shader_texture_mult_1: StringProperty( name="Texture Mult 1", description="Texture Mult for active Material", default=_MAT_consts.unset_bitmap_filepath, @@ -1200,29 +1214,29 @@ def update_shader_texture_reflection_settings(self, context): subtype='NONE', update=update_shader_texture_mult_1, ) - shader_texture_mult_1_imported_tobj = StringProperty( + shader_texture_mult_1_imported_tobj: StringProperty( name="Imported TOBJ Path", description="Use imported TOBJ path reference which will be exported into material (NOTE: export will not take care of any TOBJ files!)", default="", update=__update_look__ ) - shader_texture_mult_1_use_imported = BoolProperty( + shader_texture_mult_1_use_imported: BoolProperty( name="Use Imported", description="Use custom provided path for TOBJ reference", default=False, update=__update_look__ ) - shader_texture_mult_1_locked = BoolProperty( + shader_texture_mult_1_locked: BoolProperty( name="Texture Locked", description="Tells if texture is locked and should not be changed by user(intended for internal usage only)", default=False ) - shader_texture_mult_1_map_type = StringProperty( + shader_texture_mult_1_map_type: StringProperty( name="Texture Map Type", description="Stores texture mapping type and should not be changed by user(intended for internal usage only)", default="2d" ) - shader_texture_mult_1_settings = EnumProperty( + shader_texture_mult_1_settings: EnumProperty( name="Settings", description="TOBJ settings for this texture", items=__get_texture_settings__(), @@ -1230,12 +1244,12 @@ def update_shader_texture_reflection_settings(self, context): options={'ENUM_FLAG'}, update=update_shader_texture_mult_1_settings ) - shader_texture_mult_1_tobj_load_time = StringProperty( + shader_texture_mult_1_tobj_load_time: StringProperty( name="Last TOBJ load time", description="Time string of last loading", default="", ) - shader_texture_mult_1_uv = CollectionProperty( + shader_texture_mult_1_uv: CollectionProperty( name="Texture Mult UV Sets", description="Texture Mult UV sets for active Material", type=UVMappingItem, @@ -1243,7 +1257,7 @@ def update_shader_texture_reflection_settings(self, context): ) # TEXTURE: NMAP - shader_texture_nmap = StringProperty( + shader_texture_nmap: StringProperty( name="Texture NMap", description="Texture NMap for active Material", default=_MAT_consts.unset_bitmap_filepath, @@ -1251,23 +1265,23 @@ def update_shader_texture_reflection_settings(self, context): subtype='NONE', update=update_shader_texture_nmap, ) - shader_texture_nmap_imported_tobj = StringProperty( + shader_texture_nmap_imported_tobj: StringProperty( name="Imported TOBJ Path", description="Use imported TOBJ path reference which will be exported into material (NOTE: export will not take care of any TOBJ files!)", default="", update=__update_look__ ) - shader_texture_nmap_locked = BoolProperty( + shader_texture_nmap_locked: BoolProperty( name="Texture Locked", description="Tells if texture is locked and should not be changed by user(intended for internal usage only)", default=False ) - shader_texture_nmap_map_type = StringProperty( + shader_texture_nmap_map_type: StringProperty( name="Texture Map Type", description="Stores texture mapping type and should not be changed by user(intended for internal usage only)", default="2d" ) - shader_texture_nmap_settings = EnumProperty( + shader_texture_nmap_settings: EnumProperty( name="Settings", description="TOBJ settings for this texture", items=__get_texture_settings__(), @@ -1275,18 +1289,18 @@ def update_shader_texture_reflection_settings(self, context): options={'ENUM_FLAG'}, update=update_shader_texture_nmap_settings ) - shader_texture_nmap_tobj_load_time = StringProperty( + shader_texture_nmap_tobj_load_time: StringProperty( name="Last TOBJ load time", description="Time string of last loading", default="", ) - shader_texture_nmap_use_imported = BoolProperty( + shader_texture_nmap_use_imported: BoolProperty( name="Use Imported", description="Use custom provided path for TOBJ reference", default=False, update=__update_look__ ) - shader_texture_nmap_uv = CollectionProperty( + shader_texture_nmap_uv: CollectionProperty( name="Texture Base UV Sets", description="Texture base UV sets for active Material", type=UVMappingItem, @@ -1294,7 +1308,7 @@ def update_shader_texture_reflection_settings(self, context): ) # TEXTURE: NMAP_DETAIL - shader_texture_nmap_detail = StringProperty( + shader_texture_nmap_detail: StringProperty( name="Texture NMap Detail", description="Texture NMap Detail for active Material", default=_MAT_consts.unset_bitmap_filepath, @@ -1302,23 +1316,23 @@ def update_shader_texture_reflection_settings(self, context): subtype='NONE', update=update_shader_texture_nmap_detail, ) - shader_texture_nmap_detail_imported_tobj = StringProperty( + shader_texture_nmap_detail_imported_tobj: StringProperty( name="Imported TOBJ Path", description="Use imported TOBJ path reference which will be exported into material (NOTE: export will not take care of any TOBJ files!)", default="", update=__update_look__ ) - shader_texture_nmap_detail_locked = BoolProperty( + shader_texture_nmap_detail_locked: BoolProperty( name="Texture Locked", description="Tells if texture is locked and should not be changed by user(intended for internal usage only)", default=False ) - shader_texture_nmap_detail_map_type = StringProperty( + shader_texture_nmap_detail_map_type: StringProperty( name="Texture Map Type", description="Stores texture mapping type and should not be changed by user(intended for internal usage only)", default="2d" ) - shader_texture_nmap_detail_settings = EnumProperty( + shader_texture_nmap_detail_settings: EnumProperty( name="Settings", description="TOBJ settings for this texture", items=__get_texture_settings__(), @@ -1326,18 +1340,18 @@ def update_shader_texture_reflection_settings(self, context): options={'ENUM_FLAG'}, update=update_shader_texture_nmap_detail_settings ) - shader_texture_nmap_detail_tobj_load_time = StringProperty( + shader_texture_nmap_detail_tobj_load_time: StringProperty( name="Last TOBJ load time", description="Time string of last loading", default="", ) - shader_texture_nmap_detail_use_imported = BoolProperty( + shader_texture_nmap_detail_use_imported: BoolProperty( name="Use Imported", description="Use custom provided path for TOBJ reference", default=False, update=__update_look__ ) - shader_texture_nmap_detail_uv = CollectionProperty( + shader_texture_nmap_detail_uv: CollectionProperty( name="Texture Base UV Sets", description="Texture base UV sets for active Material", type=UVMappingItem, @@ -1345,7 +1359,7 @@ def update_shader_texture_reflection_settings(self, context): ) # TEXTURE: OCCLUSION - shader_texture_oclu = StringProperty( + shader_texture_oclu: StringProperty( name="Texture Oclu", description="Texture Oclu for active Material", default=_MAT_consts.unset_bitmap_filepath, @@ -1353,23 +1367,23 @@ def update_shader_texture_reflection_settings(self, context): subtype='NONE', update=update_shader_texture_oclu, ) - shader_texture_oclu_imported_tobj = StringProperty( + shader_texture_oclu_imported_tobj: StringProperty( name="Imported TOBJ Path", description="Use imported TOBJ path reference which will be exported into material (NOTE: export will not take care of any TOBJ files!)", default="", update=__update_look__ ) - shader_texture_oclu_locked = BoolProperty( + shader_texture_oclu_locked: BoolProperty( name="Texture Locked", description="Tells if texture is locked and should not be changed by user(intended for internal usage only)", default=False ) - shader_texture_oclu_map_type = StringProperty( + shader_texture_oclu_map_type: StringProperty( name="Texture Map Type", description="Stores texture mapping type and should not be changed by user(intended for internal usage only)", default="2d" ) - shader_texture_oclu_settings = EnumProperty( + shader_texture_oclu_settings: EnumProperty( name="Settings", description="TOBJ settings for this texture", items=__get_texture_settings__(), @@ -1377,18 +1391,18 @@ def update_shader_texture_reflection_settings(self, context): options={'ENUM_FLAG'}, update=update_shader_texture_oclu_settings ) - shader_texture_oclu_tobj_load_time = StringProperty( + shader_texture_oclu_tobj_load_time: StringProperty( name="Last TOBJ load time", description="Time string of last loading", default="", ) - shader_texture_oclu_use_imported = BoolProperty( + shader_texture_oclu_use_imported: BoolProperty( name="Use Imported", description="Use custom provided path for TOBJ reference", default=False, update=__update_look__ ) - shader_texture_oclu_uv = CollectionProperty( + shader_texture_oclu_uv: CollectionProperty( name="Texture Oclu UV Sets", description="Texture Oclu UV sets for active Material", type=UVMappingItem, @@ -1396,7 +1410,7 @@ def update_shader_texture_reflection_settings(self, context): ) # TEXTURE: OVER - shader_texture_over = StringProperty( + shader_texture_over: StringProperty( name="Texture Over", description="Texture Over for active Material", default=_MAT_consts.unset_bitmap_filepath, @@ -1404,23 +1418,23 @@ def update_shader_texture_reflection_settings(self, context): subtype='NONE', update=update_shader_texture_over, ) - shader_texture_over_imported_tobj = StringProperty( + shader_texture_over_imported_tobj: StringProperty( name="Imported TOBJ Path", description="Use imported TOBJ path reference which will be exported into material (NOTE: export will not take care of any TOBJ files!)", default="", update=__update_look__ ) - shader_texture_over_locked = BoolProperty( + shader_texture_over_locked: BoolProperty( name="Texture Locked", description="Tells if texture is locked and should not be changed by user(intended for internal usage only)", default=False ) - shader_texture_over_map_type = StringProperty( + shader_texture_over_map_type: StringProperty( name="Texture Map Type", description="Stores texture mapping type and should not be changed by user(intended for internal usage only)", default="2d" ) - shader_texture_over_settings = EnumProperty( + shader_texture_over_settings: EnumProperty( name="Settings", description="TOBJ settings for this texture", items=__get_texture_settings__(), @@ -1428,18 +1442,18 @@ def update_shader_texture_reflection_settings(self, context): options={'ENUM_FLAG'}, update=update_shader_texture_over_settings ) - shader_texture_over_tobj_load_time = StringProperty( + shader_texture_over_tobj_load_time: StringProperty( name="Last TOBJ load time", description="Time string of last loading", default="", ) - shader_texture_over_use_imported = BoolProperty( + shader_texture_over_use_imported: BoolProperty( name="Use Imported", description="Use custom provided path for TOBJ reference", default=False, update=__update_look__ ) - shader_texture_over_uv = CollectionProperty( + shader_texture_over_uv: CollectionProperty( name="Texture Over UV Sets", description="Texture Over UV sets for active Material", type=UVMappingItem, @@ -1447,7 +1461,7 @@ def update_shader_texture_reflection_settings(self, context): ) # TEXTURE: PAINTJOB - shader_texture_paintjob = StringProperty( + shader_texture_paintjob: StringProperty( name="Texture Paintjob", description="Texture Paintjob for active Material", default=_MAT_consts.unset_bitmap_filepath, @@ -1455,23 +1469,23 @@ def update_shader_texture_reflection_settings(self, context): subtype='NONE', update=update_shader_texture_paintjob, ) - shader_texture_paintjob_imported_tobj = StringProperty( + shader_texture_paintjob_imported_tobj: StringProperty( name="Imported TOBJ Path", description="Use imported TOBJ path reference which will be exported into material (NOTE: export will not take care of any TOBJ files!)", default="", update=__update_look__ ) - shader_texture_paintjob_locked = BoolProperty( + shader_texture_paintjob_locked: BoolProperty( name="Texture Locked", description="Tells if texture is locked and should not be changed by user(intended for internal usage only)", default=False ) - shader_texture_paintjob_map_type = StringProperty( + shader_texture_paintjob_map_type: StringProperty( name="Texture Map Type", description="Stores texture mapping type and should not be changed by user(intended for internal usage only)", default="2d" ) - shader_texture_paintjob_settings = EnumProperty( + shader_texture_paintjob_settings: EnumProperty( name="Settings", description="TOBJ settings for this texture", items=__get_texture_settings__(), @@ -1479,18 +1493,18 @@ def update_shader_texture_reflection_settings(self, context): options={'ENUM_FLAG'}, update=update_shader_texture_paintjob_settings ) - shader_texture_paintjob_tobj_load_time = StringProperty( + shader_texture_paintjob_tobj_load_time: StringProperty( name="Last TOBJ load time", description="Time string of last loading", default="", ) - shader_texture_paintjob_use_imported = BoolProperty( + shader_texture_paintjob_use_imported: BoolProperty( name="Use Imported", description="Use custom provided path for TOBJ reference", default=False, update=__update_look__ ) - shader_texture_paintjob_uv = CollectionProperty( + shader_texture_paintjob_uv: CollectionProperty( name="Texture Paintjob UV Sets", description="Texture Paintjob UV sets for active Material", type=UVMappingItem, @@ -1498,7 +1512,7 @@ def update_shader_texture_reflection_settings(self, context): ) # TEXTURE: REFLECTION - shader_texture_reflection = StringProperty( + shader_texture_reflection: StringProperty( name="Texture Reflection", description="Texture Reflection for active Material", default=_MAT_consts.unset_bitmap_filepath, @@ -1506,23 +1520,23 @@ def update_shader_texture_reflection_settings(self, context): subtype='NONE', update=update_shader_texture_reflection, ) - shader_texture_reflection_imported_tobj = StringProperty( + shader_texture_reflection_imported_tobj: StringProperty( name="Imported TOBJ Path", description="Use imported TOBJ path reference which will be exported into material (NOTE: export will not take care of any TOBJ files!)", default="", update=__update_look__ ) - shader_texture_reflection_locked = BoolProperty( + shader_texture_reflection_locked: BoolProperty( name="Texture Locked", description="Tells if texture is locked and should not be changed by user(intended for internal usage only)", default=False ) - shader_texture_reflection_map_type = StringProperty( + shader_texture_reflection_map_type: StringProperty( name="Texture Map Type", description="Stores texture mapping type and should not be changed by user(intended for internal usage only)", default="2d" ) - shader_texture_reflection_settings = EnumProperty( + shader_texture_reflection_settings: EnumProperty( name="Settings", description="TOBJ settings for this texture", items=__get_texture_settings__(), @@ -1530,18 +1544,18 @@ def update_shader_texture_reflection_settings(self, context): options={'ENUM_FLAG'}, update=update_shader_texture_reflection_settings ) - shader_texture_reflection_tobj_load_time = StringProperty( + shader_texture_reflection_tobj_load_time: StringProperty( name="Last TOBJ load time", description="Time string of last loading", default="", ) - shader_texture_reflection_use_imported = BoolProperty( + shader_texture_reflection_use_imported: BoolProperty( name="Use Imported", description="Use custom provided path for TOBJ reference", default=False, update=__update_look__ ) - shader_texture_reflection_uv = CollectionProperty( + shader_texture_reflection_uv: CollectionProperty( name="Texture Reflection UV Sets", description="Texture Reflection UV sets for active Material", type=UVMappingItem, @@ -1549,11 +1563,29 @@ def update_shader_texture_reflection_settings(self, context): ) # # Following String property is fed from MATERIAL SUBSTANCE data list, which is usually loaded from - # # "//base/material/material.db" file and stored at "scs_globals.scs_matsubs_inventory". - substance = StringProperty( + # # "//base/material/material.db" file and stored at "scs_inventories.matsubs". + substance: StringProperty( name="Substance", description="Substance", default=_MAT_consts.unset_substance, subtype='NONE', update=__update_look__ ) + + +classes = ( + MaterialSCSTools.TexCoordItem, + MaterialSCSTools.AuxiliaryItem, + MaterialSCSTools.UVMappingItem, + MaterialSCSTools +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/addon/io_scs_tools/properties/mesh.py b/addon/io_scs_tools/properties/mesh.py index 2a55f09..a00f2e6 100644 --- a/addon/io_scs_tools/properties/mesh.py +++ b/addon/io_scs_tools/properties/mesh.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy from bpy.props import StringProperty, FloatProperty @@ -27,18 +27,33 @@ class MeshSCSTools(bpy.types.PropertyGroup): SCS Tools Mesh Variables - ...Mesh.scs_props... :return: """ - locator_preview_model_path = StringProperty( + locator_preview_model_path: StringProperty( name="Preview Model", description="Preview model filepath", default="", subtype="FILE_PATH", # subtype='NONE', ) - vertex_color_multiplier = FloatProperty( + vertex_color_multiplier: FloatProperty( name="Vertex Color Multiplier", description="All of the vertices will have their color multiplied for this factor upon export.", default=1, min=0, max=10, step=0.1 - ) \ No newline at end of file + ) + + +classes = ( + MeshSCSTools, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/addon/io_scs_tools/properties/object.py b/addon/io_scs_tools/properties/object.py index 7e9d49b..4a8d349 100644 --- a/addon/io_scs_tools/properties/object.py +++ b/addon/io_scs_tools/properties/object.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy from bpy.props import (StringProperty, @@ -68,8 +68,8 @@ def name_update(self, context): if new_name != self.name: self.name = new_name - name = StringProperty(name="Look Name", default=_LOOK_consts.default_name, update=name_update) - id = IntProperty(name="Unique ID of this look") + name: StringProperty(name="Look Name", default=_LOOK_consts.default_name, update=name_update) + id: IntProperty(name="Unique ID of this look") class ObjectPartInventoryItem(bpy.types.PropertyGroup): @@ -126,14 +126,14 @@ def name_update(self, context): # backup current name for checking children on next renaming self["scs_part_old_name"] = self.name - name = StringProperty(name="Part Name", default=_PART_consts.default_name, update=name_update) + name: StringProperty(name="Part Name", default=_PART_consts.default_name, update=name_update) -class ObjectVariantPartInclusion(bpy.types.PropertyGroup): +class ObjectVariantPartInclusionItem(bpy.types.PropertyGroup): """ Variant to Part references, which gets saved within "SceneVariantInventory" into *.blend file. """ - include = BoolProperty(name="Part is included in the Variant", default=False) + include: BoolProperty(name="Part is included in the Variant", default=False) class ObjectVariantInventoryItem(bpy.types.PropertyGroup): @@ -165,8 +165,8 @@ def name_update(self, context): if new_name != self.name: self.name = new_name - name = StringProperty(name="Variant Name", default=_VARIANT_consts.default_name, update=name_update) - parts = CollectionProperty(type=ObjectVariantPartInclusion) + name: StringProperty(name="Variant Name", default=_VARIANT_consts.default_name, update=name_update) + parts: CollectionProperty(type=ObjectVariantPartInclusionItem) class ObjectAnimationInventoryItem(bpy.types.PropertyGroup): @@ -226,34 +226,34 @@ def anim_action_update(self, context): else: active_object.animation_data.action = None - name = StringProperty(name="Animation Name", default=_VARIANT_consts.default_name) - export = BoolProperty( + name: StringProperty(name="Animation Name", default=_VARIANT_consts.default_name) + export: BoolProperty( name="Export Inclusion", description="Include this SCS animation on Export and write it as SCS Animation file (PIA)", default=True, ) - action = StringProperty( + action: StringProperty( name="Animation Action", description="Action used for this SCS animation", default="", options={'HIDDEN'}, update=anim_action_update ) - anim_start = IntProperty( + anim_start: IntProperty( name="Animation Start", description="Action start frame for this SCS animation", default=1, options={'HIDDEN'}, update=anim_range_update ) - anim_end = IntProperty( + anim_end: IntProperty( name="Animation End", description="Action end frame for this SCS animation", default=250, options={'HIDDEN'}, update=anim_range_update ) - length = FloatProperty( + length: FloatProperty( name="Animation Length", description="Total length of this SCS Animation (in seconds)", default=10.0, @@ -272,8 +272,8 @@ class ObjectSCSTools(bpy.types.PropertyGroup): """ # HELPER VARIABLES - locators_orig_draw_size = FloatProperty(name="Locator Original Draw Size") - locators_orig_draw_type = StringProperty(name="Locator Original Draw Type") + locators_orig_display_size: FloatProperty(name="Locator Original Display Size") + locators_orig_display_type: StringProperty(name="Locator Original Display Type") def empty_object_type_items(self, context): """Returns object type items in real time, because of accessing to custom icons. @@ -289,16 +289,16 @@ def update_empty_object_type(self, context): if context.object: if self.empty_object_type == "SCS_Root": - context.object.empty_draw_size = 5.0 - context.object.empty_draw_type = "ARROWS" + context.object.empty_display_size = 5.0 + context.object.empty_display_type = "ARROWS" context.object.show_name = True else: - context.object.empty_draw_size = 1.0 - context.object.empty_draw_type = "PLAIN_AXES" + context.object.empty_display_size = 1.0 + context.object.empty_display_type = "PLAIN_AXES" context.object.show_name = False # EMPTY OBJECT TYPE - empty_object_type = EnumProperty( + empty_object_type: EnumProperty( name="SCS Object Type", description="Settings for Special SCS Objects", items=empty_object_type_items, @@ -306,24 +306,24 @@ def update_empty_object_type(self, context): ) # SCS SKELETON OBJECTS - scs_skeleton_custom_export_dirpath = StringProperty( + scs_skeleton_custom_export_dirpath: StringProperty( name="Skeleton Custom Export Path", description="Custom export file path for skeleton PIS file (if empty skeleton is exported beside PIM file).", default="", ) - scs_skeleton_custom_name = StringProperty( + scs_skeleton_custom_name: StringProperty( name="Skeleton Custom Name", description="Custom name for SCS Skeleton PIS file (if empty name of skeleton file is the same as PIM file).", default="", ) # SCS GAME OBJECTS - scs_root_object_export_enabled = BoolProperty( + scs_root_object_export_enabled: BoolProperty( name="Export", description="Enable / disable export of all object's children as single 'SCS Game Object'", default=True, ) - scs_root_animated = EnumProperty( + scs_root_animated: EnumProperty( name="Animation Output", description="Animation Output", items=( @@ -332,24 +332,24 @@ def update_empty_object_type(self, context): ), default='rigid', ) - scs_root_object_allow_custom_path = BoolProperty( + scs_root_object_allow_custom_path: BoolProperty( name="Allow Custom Export File Path", description="Allow use of custom export file path for this 'SCS Game Object'", default=False, ) - scs_root_object_export_filepath = StringProperty( + scs_root_object_export_filepath: StringProperty( name="Custom Export File Path", description="Custom export file path for this 'SCS Game Object'", default="//", # subtype="FILE_PATH", subtype='NONE', ) - scs_root_object_allow_anim_custom_path = BoolProperty( + scs_root_object_allow_anim_custom_path: BoolProperty( name="Allow Custom Animations Export File Path", description="Allow use of custom export file path for animations of this 'SCS Game Object'", default=False, ) - scs_root_object_anim_export_filepath = StringProperty( + scs_root_object_anim_export_filepath: StringProperty( name="Custom Animations Export File Path", description="Custom export file path for animations of this 'SCS Game Object'", default="//", @@ -395,7 +395,7 @@ def active_scs_part_get(self): def active_scs_part_set(self, value): self["active_scs_part_value"] = value - active_scs_part = IntProperty( + active_scs_part: IntProperty( name="Active SCS Part", description="Active SCS Part for current SCS Root Object", default=0, @@ -426,14 +426,14 @@ def set_active_scs_look(self, value): if scs_root: _looks.apply_active_look(scs_root) - active_scs_look = IntProperty( + active_scs_look: IntProperty( name="Active SCS Look", description="Active SCS Look for current SCS Game Object", options={'HIDDEN'}, get=get_active_scs_look, set=set_active_scs_look ) - active_scs_variant = IntProperty( + active_scs_variant: IntProperty( name="Active SCS Variant", description="Active SCS Variant for current SCS Game Object", default=0, @@ -493,7 +493,7 @@ def active_scs_animation_update(self, context): print('Wrong Animation index %i/%i!' % (active_scs_anim_i, len(scs_object_anim_inventory))) return None - active_scs_animation = IntProperty( + active_scs_animation: IntProperty( name="Active SCS Animation", description="Active SCS Animation for current SCS Game Object", default=0, @@ -504,7 +504,7 @@ def active_scs_animation_update(self, context): update=active_scs_animation_update, ) - scs_part = StringProperty( + scs_part: StringProperty( name="SCS Part", description="SCS Part for active object (type a new name to create new SCS Part)", default=_PART_consts.default_name, @@ -512,13 +512,13 @@ def active_scs_animation_update(self, context): subtype='NONE', ) - object_identity = StringProperty( + object_identity: StringProperty( name="Object Identity", default="", options={'HIDDEN'}, subtype='NONE', ) - parent_identity = StringProperty( + parent_identity: StringProperty( name="Parent Identity", default="", options={'HIDDEN'}, @@ -538,8 +538,8 @@ def locator_preview_model_path_update(self, context): 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 - bpy.context.scene.objects.active = obj + obj.select_set(True) + bpy.context.view_layer.objects.active = obj else: @@ -560,7 +560,7 @@ def locator_show_preview_model_update(self, context): _preview_models.unload(obj) return None - locator_preview_model_path = StringProperty( + locator_preview_model_path: StringProperty( name="Preview Model", description="Preview model filepath", default="", @@ -568,13 +568,13 @@ def locator_show_preview_model_update(self, context): subtype='NONE', update=locator_preview_model_path_update, ) - locator_show_preview_model = BoolProperty( + locator_show_preview_model: BoolProperty( name="Show Preview Model", description="Show preview model", default=True, update=locator_show_preview_model_update, ) - locator_preview_model_present = BoolProperty( + locator_preview_model_present: BoolProperty( name="Preview Model Exists", default=False, ) @@ -587,16 +587,16 @@ def locator_preview_model_type_update(self, context): if "scs_props" in child.data and child.data.scs_props.locator_preview_model_path != "": - child.draw_type = obj.scs_props.locator_preview_model_type + child.display_type = obj.scs_props.locator_preview_model_type - locator_preview_model_type = EnumProperty( + locator_preview_model_type: EnumProperty( name="Draw Type", description="Maximum draw type to display preview model with in viewport", items=( # ('TEXTURED', "Textured", "Draw the preview model with textures (if textures are enabled in the viewport)", 'TEXTURE_SHADED', 0), - ('SOLID', "Solid", "Draw the preview model as a solid (if solid drawing is enabled in the viewport)", 'SOLID', 1), - ('WIRE', "Wire", "Draw the bounds of the preview model", 'WIRE', 2), - ('BOUNDS', "Bounds", "Draw the preview model with textures (if textures are enabled in the viewport)", 'BBOX', 3), + ('SOLID', "Solid", "Draw the preview model as a solid (if solid drawing is enabled in the viewport)", 'SHADING_SOLID', 1), + ('WIRE', "Wire", "Draw the bounds of the preview model", 'SHADING_WIRE', 2), + ('BOUNDS', "Bounds", "Draw the preview model with textures (if textures are enabled in the viewport)", 'SHADING_BBOX', 3), ), default='WIRE', update=locator_preview_model_type_update @@ -651,14 +651,14 @@ def locator_type_update(self, context): obj.scs_props.scs_part = _PART_consts.default_name # SCS LOCATORS - locator_type = EnumProperty( + locator_type: EnumProperty( name="Type", description="Locator type", items=locator_type_items, update=locator_type_update, ) # LOCATORS - MODELS - locator_model_hookup = StringProperty( + locator_model_hookup: StringProperty( name="Hookup", description="Hookup", default="", @@ -675,27 +675,27 @@ def locator_collider_type_items(self, context): ] # LOCATORS - COLLIDERS - locator_collider_type = EnumProperty( + locator_collider_type: EnumProperty( name="Collider Type", description="Collider locator type", items=locator_collider_type_items, ) - locator_collider_wires = BoolProperty( + locator_collider_wires: BoolProperty( name="Collider Wireframes", description="Display wireframes for this collider", default=True, ) - locator_collider_faces = BoolProperty( + locator_collider_faces: BoolProperty( name="Collider Faces", description="Display faces for this collider", default=True, ) - locator_collider_centered = BoolProperty( + locator_collider_centered: BoolProperty( name="Locator Centered", description="Make locator centered", default=False, ) - locator_collider_mass = FloatProperty( + locator_collider_mass: FloatProperty( name="Mass Weight", description="Mass of the element", default=1.0, @@ -704,7 +704,7 @@ def locator_collider_type_items(self, context): unit='NONE', ) # TODO: MATERIAL could be probably loaded from file ".../root/maya/shared/AEModelLocatorTemplate.mel" - locator_collider_material = EnumProperty( + locator_collider_material: EnumProperty( name="Material", description="Collider's physical material", items=( @@ -713,7 +713,7 @@ def locator_collider_type_items(self, context): default='und', ) # LOCATORS - COLLIDER - BOX - locator_collider_box_x = FloatProperty( + locator_collider_box_x: FloatProperty( name="Box X Dimension", description="Box X dimension", default=1.0, @@ -722,7 +722,7 @@ def locator_collider_type_items(self, context): subtype='NONE', unit='NONE', ) - locator_collider_box_y = FloatProperty( + locator_collider_box_y: FloatProperty( name="Box Y Dimension", description="Box Y dimension", default=1.0, @@ -731,7 +731,7 @@ def locator_collider_type_items(self, context): subtype='NONE', unit='NONE', ) - locator_collider_box_z = FloatProperty( + locator_collider_box_z: FloatProperty( name="Box Z Dimension", description="Box Z dimension", default=1.0, @@ -741,7 +741,7 @@ def locator_collider_type_items(self, context): unit='NONE', ) # LOCATORS - COLLIDER - SPHERE, CAPSULE, CYLINDER - locator_collider_dia = FloatProperty( + locator_collider_dia: FloatProperty( name="Diameter", description="Collider diameter", default=2.0, @@ -750,7 +750,7 @@ def locator_collider_type_items(self, context): subtype='NONE', unit='NONE', ) - locator_collider_len = FloatProperty( + locator_collider_len: FloatProperty( name="Length", description="Collider length", default=2.0, @@ -772,7 +772,7 @@ def locator_collider_margin_update(self, context): if context.active_object: _object_utils.update_convex_hull_margins(context.active_object) - locator_collider_margin = FloatProperty( + locator_collider_margin: FloatProperty( name="Collision Margin", description="Collision margin of the element", default=0.0, @@ -781,7 +781,7 @@ def locator_collider_margin_update(self, context): unit='NONE', update=locator_collider_margin_update ) - locator_collider_vertices = IntProperty( + locator_collider_vertices: IntProperty( name="Number of Convex Hull Vertices", description="Number of convex hull vertices", default=0, @@ -806,7 +806,7 @@ def locator_prefab_type_update(self, context): self.locator_type_update(context) # LOCATORS - PREFABS - locator_prefab_type = EnumProperty( + locator_prefab_type: EnumProperty( name="Prefab Type", description="Prefab locator type", items=locator_prefab_type_items, @@ -816,7 +816,7 @@ def locator_prefab_type_update(self, context): enum_con_node_index_items = OrderedDict() for i in range(_PL_consts.PREFAB_NODE_COUNT_MAX): enum_con_node_index_items[i] = (str(i), str(i), "") - locator_prefab_con_node_index = EnumProperty( + locator_prefab_con_node_index: EnumProperty( name="Node Index", description="Node index", items=enum_con_node_index_items.values(), @@ -824,8 +824,8 @@ def locator_prefab_type_update(self, context): ) # LOCATORS - PREFAB - SIGNS # Following String property is fed from SIGN MODEL data list, which is usually loaded from - # "//base/def/world/sign.sii" file and stored at "scs_globals.scs_sign_model_inventory". - locator_prefab_sign_model = StringProperty( + # "//base/def/world/sign.sii" file and stored at "scs_inventories.sign_models". + locator_prefab_sign_model: StringProperty( name="Sign Model", description="Sign model", default="", @@ -861,7 +861,7 @@ def locator_prefab_type_update(self, context): (_PL_consts.PSP.WEIGHT_CAT_POS, (str(_PL_consts.PSP.WEIGHT_CAT_POS), "Weight Station CAT", "")), ]) # LOCATORS - PREFAB - SPAWN POINTS - locator_prefab_spawn_type = EnumProperty( + locator_prefab_spawn_type: EnumProperty( name="Spawn Type", description="Spawn type", items=enum_spawn_type_items.values(), @@ -871,15 +871,15 @@ def locator_prefab_type_update(self, context): enum_tsem_id_items = OrderedDict([(-1, ('-1', "None", ""))]) for i in range(_PL_consts.TSEM_COUNT_MAX): enum_tsem_id_items[i] = (str(i), str(i), "") - locator_prefab_tsem_id = EnumProperty( + locator_prefab_tsem_id: EnumProperty( name="ID", description="ID", items=enum_tsem_id_items.values(), default='-1', ) # Following String property is fed from TRAFFIC SEMAPHORE PROFILES data list, which is usually loaded from - # "//base/def/world/semaphore_profile.sii" file and stored at "scs_globals.scs_tsem_profile_inventory". - locator_prefab_tsem_profile = StringProperty( + # "//base/def/world/semaphore_profile.sii" file and stored at "scs_inventories.tsem_profiles". + locator_prefab_tsem_profile: StringProperty( name="Profile", description="Traffic Semaphore Profile", default="", @@ -898,41 +898,41 @@ def locator_prefab_type_update(self, context): (_PL_consts.TST.BARRIER_DISTANCE, (str(_PL_consts.TST.BARRIER_DISTANCE), "Barrier - Distance Activated", "")), (_PL_consts.TST.BARRIER_AUTOMATIC, (str(_PL_consts.TST.BARRIER_AUTOMATIC), "Barrier - Automatic", "")), ]) - locator_prefab_tsem_type = EnumProperty( + locator_prefab_tsem_type: EnumProperty( name="Type", description="Type", items=enum_tsem_type_items.values(), default=str(_PL_consts.TST.PROFILE), ) - locator_prefab_tsem_gs = FloatProperty( + locator_prefab_tsem_gs: FloatProperty( name="G", description="Time interval/Distance for Green light", default=15.0, min=-1.0, options={'HIDDEN'}, ) - locator_prefab_tsem_os1 = FloatProperty( + locator_prefab_tsem_os1: FloatProperty( name="O", description="Time interval/Distance for after-green Orange light", default=2.0, min=-1.0, options={'HIDDEN'}, ) - locator_prefab_tsem_rs = FloatProperty( + locator_prefab_tsem_rs: FloatProperty( name="R", description="Time interval/Distance for Red light", default=23.0, min=-1.0, options={'HIDDEN'}, ) - locator_prefab_tsem_os2 = FloatProperty( + locator_prefab_tsem_os2: FloatProperty( name="O", description="Time interval/Distance for after-red Orange light", default=2.0, min=-1.0, options={'HIDDEN'}, ) - locator_prefab_tsem_cyc_delay = FloatProperty( + locator_prefab_tsem_cyc_delay: FloatProperty( name="Cycle Delay", description="Number of seconds by which the whole traffic semaphore cycle is shifted.", default=0.0, @@ -942,22 +942,22 @@ def locator_prefab_type_update(self, context): unit='NONE', ) # LOCATORS - PREFAB - NAVIGATION POINTS - locator_prefab_np_low_probab = BoolProperty( + locator_prefab_np_low_probab: BoolProperty( name="Low Probability", description="Low probability", default=False, ) - locator_prefab_np_add_priority = BoolProperty( + locator_prefab_np_add_priority: BoolProperty( name="Additive Priority", description="Additive priority", default=False, ) - locator_prefab_np_limit_displace = BoolProperty( + locator_prefab_np_limit_displace: BoolProperty( name="Limit Displacement", description="Limit displacement", default=False, ) - locator_prefab_np_allowed_veh = EnumProperty( + locator_prefab_np_allowed_veh: EnumProperty( name="Allowed Vehicles", description="Allowed vehicles", items=( @@ -968,7 +968,7 @@ def locator_prefab_type_update(self, context): ), default=str(_PL_consts.PNCF.ALLOWED_VEHICLES_MASK), ) - locator_prefab_np_blinker = EnumProperty( + locator_prefab_np_blinker: EnumProperty( name="Blinker", description="Blinker", items=( @@ -986,7 +986,7 @@ def locator_prefab_type_update(self, context): for i in range(_PL_consts.PREFAB_LANE_COUNT_MAX): ii = _PL_consts.PREFAB_LANE_COUNT_MAX + 1 + i enum_np_boundary_items[ii] = (str(ii), "Output - Lane " + str(i), "") - locator_prefab_np_boundary = EnumProperty( + locator_prefab_np_boundary: EnumProperty( name="Boundary", description="Boundary", items=enum_np_boundary_items.values(), @@ -995,7 +995,7 @@ def locator_prefab_type_update(self, context): enum_np_boundary_node_items = OrderedDict() for i in range(_PL_consts.PREFAB_NODE_COUNT_MAX): enum_np_boundary_node_items[i] = (str(i), str(i), "") - locator_prefab_np_boundary_node = EnumProperty( + locator_prefab_np_boundary_node: EnumProperty( name="Boundary Node", description="Boundary node index", items=enum_np_boundary_node_items.values(), @@ -1004,15 +1004,15 @@ def locator_prefab_type_update(self, context): enum_np_traffic_semaphore_items = OrderedDict([(-1, ('-1', "None", ""))]) for i in range(_PL_consts.TSEM_COUNT_MAX): enum_np_traffic_semaphore_items[i] = (str(i), str(i), "") - locator_prefab_np_traffic_semaphore = EnumProperty( + locator_prefab_np_traffic_semaphore: EnumProperty( name="Traffic Semaphore", description="Traffic semaphore ID", items=enum_np_traffic_semaphore_items.values(), default='-1', ) # Following String property is fed from TRAFFIC SEMAPHORE PROFILES data list, which is usually loaded from - # "//base/def/world/traffic_rules.sii" file and stored at "scs_globals.scs_traffic_rules_inventory". - locator_prefab_np_traffic_rule = StringProperty( + # "//base/def/world/traffic_rules.sii" file and stored at "scs_inventories.traffic_rules". + locator_prefab_np_traffic_rule: StringProperty( name="Traffic Rule", description="Traffic rule", default="", @@ -1039,7 +1039,7 @@ def locator_prefab_type_update(self, context): enum_np_priority_modifier_items[i] = (str(i), name, "") - locator_prefab_np_priority_modifier = EnumProperty( + locator_prefab_np_priority_modifier: EnumProperty( name="Priority Modifier", description="Priority modifier", items=enum_np_priority_modifier_items.values(), @@ -1047,23 +1047,23 @@ def locator_prefab_type_update(self, context): ) # LOCATORS - PREFAB - MAP POINTS - locator_prefab_mp_road_over = BoolProperty( + locator_prefab_mp_road_over: BoolProperty( name="Road Over", description="The game tries to draw map point with this flag on top of all other map points", default=False, ) - locator_prefab_mp_no_outline = BoolProperty( + locator_prefab_mp_no_outline: BoolProperty( name="No Outline", description="Don't draw the black outline (useful for 'drawing' buildings etc.)", default=False, ) - locator_prefab_mp_no_arrow = BoolProperty( + locator_prefab_mp_no_arrow: BoolProperty( name="No Arrow", description="Prevents drawing of a green navigation arrow " "(useful for prefabs with more than 2 nodes, where path is clear also without an arrow)", default=False, ) - locator_prefab_mp_prefab_exit = BoolProperty( + locator_prefab_mp_prefab_exit: BoolProperty( name="Prefab Exit", description="Mark the approximate location of the prefab exit " "(useful for company prefabs where navigation will navigate from/to this point", @@ -1115,7 +1115,7 @@ def locator_prefab_type_update(self, context): "Polygon", "The map point is used to draw a polygon instead of a road." )), ]) - locator_prefab_mp_road_size = EnumProperty( + locator_prefab_mp_road_size: EnumProperty( name="Road Size", description="Size and type of the road for this map point", items=enum_mp_road_size_items.values(), @@ -1132,7 +1132,7 @@ def locator_prefab_type_update(self, context): (_PL_consts.MPVF.ROAD_OFFSET_25, (str(_PL_consts.MPVF.ROAD_OFFSET_25), "25 m", "")), # (_PL_consts.MPVF.ROAD_OFFSET_LANE, (str(_PL_consts.MPVF.ROAD_OFFSET_LANE), "", "")), ]) - locator_prefab_mp_road_offset = EnumProperty( + locator_prefab_mp_road_offset: EnumProperty( name="Road Offset", description="The center offset between the road lanes", items=enum_mp_road_offset_items.values(), @@ -1144,7 +1144,7 @@ def locator_prefab_type_update(self, context): (_PL_consts.MPVF.CUSTOM_COLOR2, (str(_PL_consts.MPVF.CUSTOM_COLOR2), "Dark", "Used for buildings")), (_PL_consts.MPVF.CUSTOM_COLOR3, (str(_PL_consts.MPVF.CUSTOM_COLOR3), "Green", "Used for grass and inaccessible areas")), ]) - locator_prefab_mp_custom_color = EnumProperty( + locator_prefab_mp_custom_color: EnumProperty( name="Custom Color", description="Colorizes map points when drawing them as polygons", items=enum_mp_custom_color_items.values(), @@ -1154,7 +1154,7 @@ def locator_prefab_type_update(self, context): for i in range(_PL_consts.PREFAB_NODE_COUNT_MAX): enum_mp_assigned_node_items[1 << i] = (str(1 << i), str(i), "") enum_mp_assigned_node_items[_PL_consts.MPNF.NAV_NODE_ALL] = (str(_PL_consts.MPNF.NAV_NODE_ALL), "All", "") - locator_prefab_mp_assigned_node = EnumProperty( + locator_prefab_mp_assigned_node: EnumProperty( name="Assigned Node", description="Must be set to corresponding prefab Control Node for start/end map points " "(for no navigation at all use 'All' option on all map points)", @@ -1164,7 +1164,7 @@ def locator_prefab_type_update(self, context): enum_mp_dest_nodes_items = OrderedDict() for i in range(_PL_consts.PREFAB_NODE_COUNT_MAX): enum_mp_dest_nodes_items[i] = (str(i), str(i), "Leads to Control Node with index %s" % i) - locator_prefab_mp_dest_nodes = EnumProperty( + locator_prefab_mp_dest_nodes: EnumProperty( name="Destination Nodes", description="Describes Control Nodes to which this map point can lead " "(only set if one of neighbour map points has more than 2 connected map points)", @@ -1174,13 +1174,13 @@ def locator_prefab_type_update(self, context): ) # LOCATORS - PREFAB - TRIGGER POINTS - locator_prefab_tp_action = StringProperty( + locator_prefab_tp_action: StringProperty( name="Action", description="Action", default="", subtype='NONE', ) - locator_prefab_tp_range = FloatProperty( + locator_prefab_tp_range: FloatProperty( name="Range", description="Range of the trigger point. In case trigger point is Sphere Trigger " "this is used as it's range otherwise it's used for up/down range.", @@ -1190,7 +1190,7 @@ def locator_prefab_type_update(self, context): subtype='NONE', unit='NONE', ) - locator_prefab_tp_reset_delay = FloatProperty( + locator_prefab_tp_reset_delay: FloatProperty( name="Reset Delay", description="Reset delay", default=0.0, @@ -1199,23 +1199,43 @@ def locator_prefab_type_update(self, context): subtype='NONE', unit='NONE', ) - locator_prefab_tp_sphere_trigger = BoolProperty( + locator_prefab_tp_sphere_trigger: BoolProperty( name="Sphere Trigger", description="Sphere trigger", default=False, ) - locator_prefab_tp_partial_activ = BoolProperty( + locator_prefab_tp_partial_activ: BoolProperty( name="Partial Activation", description="Partial activation", default=False, ) - locator_prefab_tp_onetime_activ = BoolProperty( + locator_prefab_tp_onetime_activ: BoolProperty( name="One-Time Activation", description="One-time activation", default=False, ) - locator_prefab_tp_manual_activ = BoolProperty( + locator_prefab_tp_manual_activ: BoolProperty( name="Manual Activation", description="Manual activation", default=False, ) + + +classes = ( + ObjectAnimationInventoryItem, + ObjectLooksInventoryItem, + ObjectPartInventoryItem, + ObjectVariantPartInclusionItem, + ObjectVariantInventoryItem, + ObjectSCSTools, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/addon/io_scs_tools/properties/scene.py b/addon/io_scs_tools/properties/scene.py index 524dad5..be7959a 100755 --- a/addon/io_scs_tools/properties/scene.py +++ b/addon/io_scs_tools/properties/scene.py @@ -16,13 +16,10 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy -from bpy.props import (StringProperty, - BoolProperty, - EnumProperty, - FloatProperty, ) +from bpy.props import StringProperty class SceneSCSProps(bpy.types.PropertyGroup): @@ -31,122 +28,7 @@ class SceneSCSProps(bpy.types.PropertyGroup): :return: """ - part_list_sorted = BoolProperty( - name="Part List Sorted Alphabetically", - description="Sort Part list alphabetically", - default=False, - ) - - variant_list_sorted = BoolProperty( - name="Variant List Sorted Alphabetically", - description="Sort Variant list alphabetically", - default=False, - ) - variant_views = EnumProperty( - name="Variant View Setting", - description="Variant view style options", - items=( - ('compact', "Compact", "Compact view (settings just for one Variant at a time)", 'FULLSCREEN', 0), - ('vertical', "Vertical", "Vertical long view (useful for larger number of Variants)", 'LONGDISPLAY', 1), - ('horizontal', "Horizontal", "Horizontal table view (best for smaller number of Variants)", 'SHORTDISPLAY', 2), - ('integrated', "Integrated", "Integrated in Variant list (best for smaller number of Parts)", 'LINENUMBERS_OFF', 3), - ), - default='horizontal', - ) - - # VISIBILITY VARIABLES - scs_look_panel_expand = BoolProperty( - name="Expand SCS Look Panel", - description="Expand SCS Look panel", - default=True, - ) - scs_part_panel_expand = BoolProperty( - name="Expand SCS Part Panel", - description="Expand SCS Part panel", - default=True, - ) - scs_variant_panel_expand = BoolProperty( - name="Expand SCS Variant Panel", - description="Expand SCS Variant panel", - default=True, - ) - empty_object_settings_expand = BoolProperty( - name="Expand SCS Object Settings", - description="Expand SCS Object settings panel", - default=True, - ) - locator_settings_expand = BoolProperty( - name="Expand Locator Settings", - description="Expand locator settings panel", - default=True, - ) - locator_display_settings_expand = BoolProperty( - name="Expand Locator Display Settings", - description="Expand locator display settings panel", - default=False, - ) - shader_item_split_percentage = FloatProperty( - name="UI Split Percentage", - description="This property defines percentage of UI space spliting between material item names and values.", - min=0.000001, max=1.0, - default=0.35, - ) - shader_attributes_expand = BoolProperty( - name="Expand SCS Material Attributes", - description="Expand SCS material attributes panel", - default=True, - ) - shader_textures_expand = BoolProperty( - name="Expand SCS Material Textures", - description="Expand SCS material textures panel", - default=True, - ) - shader_presets_expand = BoolProperty( - name="Expand Shader Presets", - description="Expand shader presets panel", - default=False, - ) - global_display_settings_expand = BoolProperty( - name="Expand Global Display Settings", - description="Expand global display settings panel", - default=True, - ) - global_paths_settings_expand = BoolProperty( - name="Expand Global Paths Settings", - description="Expand global paths settings panel", - default=True, - ) - scs_root_panel_settings_expand = BoolProperty( - name="Expand 'SCS Root Object' Settings", - description="Expand 'SCS Root Object' settings panel", - default=True, - ) - global_settings_expand = BoolProperty( - name="Expand Global Settings", - description="Expand global settings panel", - default=True, - ) - scene_settings_expand = BoolProperty( - name="Expand Scene Settings", - description="Expand scene settings panel", - default=True, - ) - scs_animation_settings_expand = BoolProperty( - name="Expand Animation Settings", - description="Expand animation settings panel", - default=True, - ) - scs_animplayer_panel_expand = BoolProperty( - name="Expand Animation Player Settings", - description="Expand animation player panel", - default=True, - ) - scs_skeleton_panel_expand = BoolProperty( - name="Expand Skeleton Settings", - description="Expand skeleton settings panel", - default=True, - ) - default_export_filepath = StringProperty( + default_export_filepath: StringProperty( name="Default Export Path", description="Relative export path (relative to SCS Project Base Path) for all SCS Game Objects without custom export path " "(this path does not apply when exporting via menu)", @@ -154,18 +36,18 @@ class SceneSCSProps(bpy.types.PropertyGroup): # subtype="FILE_PATH", subtype='NONE', ) - export_panel_expand = BoolProperty( - name="Expand Export Panel", - description="Expand Export panel", - default=True, - ) - # VISIBILITY TOOLS SCOPE - visibility_tools_scope = EnumProperty( - name="Visibility Tools Scope", - description="Selects the scope upon visibility tools are working", - items=( - ('Global', "Global", "Use scope of the whole scene"), - ('SCS Root', "SCS Root", "Use the scope of current SCS Root"), - ), - ) + +classes = ( + SceneSCSProps, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/addon/io_scs_tools/properties/workspace.py b/addon/io_scs_tools/properties/workspace.py new file mode 100644 index 0000000..b668d30 --- /dev/null +++ b/addon/io_scs_tools/properties/workspace.py @@ -0,0 +1,91 @@ +# ##### 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 + +import bpy +from bpy.props import (BoolProperty, + EnumProperty, ) + + +class WorkspaceSCSProps(bpy.types.PropertyGroup): + """ + SCS Tools Workspace Variables - ...Workspace.scs_props... + """ + + part_list_sorted: BoolProperty( + name="Part List Sorted Alphabetically", + description="Sort Part list alphabetically", + default=False, + ) + + variant_list_sorted: BoolProperty( + name="Variant List Sorted Alphabetically", + description="Sort Variant list alphabetically", + default=False, + ) + + variant_views: EnumProperty( + name="Variant View Setting", + description="Variant view style options", + items=( + ('compact', "Compact", "Compact view (settings just for one Variant at a time)", 'WINDOW', 0), + ('vertical', "Vertical", "Vertical long view (useful for larger number of Variants)", 'LONGDISPLAY', 1), + ('horizontal', "Horizontal", "Horizontal table view (best for smaller number of Variants)", 'SHORTDISPLAY', 2), + ('integrated', "Integrated", "Integrated in Variant list (best for smaller number of Parts)", 'LINENUMBERS_OFF', 3), + ), + default='horizontal', + ) + + shader_presets_sorted: BoolProperty( + name="Shader Preset List Sorted Alphabetically", + description="Sort Shader preset list alphabetically", + default=True, + ) + + # VISIBILITY VARIABLES + shader_presets_expand: BoolProperty( + name="Expand Shader Presets", + description="Expand shader presets panel", + default=False, + ) + + # VISIBILITY TOOLS SCOPE + visibility_tools_scope: EnumProperty( + name="Visibility Tools Scope", + description="Selects the scope upon visibility tools are working", + items=( + ('Global', "Global", "Use scope of the whole scene"), + ('SCS Root', "SCS Root", "Use the scope of current SCS Root"), + ), + ) + + +classes = ( + WorkspaceSCSProps, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/addon/io_scs_tools/shader_presets.txt b/addon/io_scs_tools/shader_presets.txt index 54bbcf8..cd33d00 100644 --- a/addon/io_scs_tools/shader_presets.txt +++ b/addon/io_scs_tools/shader_presets.txt @@ -219,7 +219,7 @@ Shader { Shader { PresetName: "dif" Effect: "eut2.dif" - Flavors: ( "FLAT" "SHADOW" "PAINT" "TEXGEN0" "TEXMTX0" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_ADD" ) + Flavors: ( "FLAT" "SHADOW" "PAINT" "TEXGEN0" "TEXMTX0" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_MULT|BLEND_ADD" ) Flags: 0 Attribute { Format: FLOAT3 @@ -256,7 +256,7 @@ Shader { PresetName: "dif.anim" Effect: "eut2.dif.anim" # NOTE: intentionally disabled TEXGEN0 flavor as it conflicts with usage of aux[0] attribute - Flavors: ( "ALPHA" "DEPTH" "BLEND_OVER|BLEND_ADD" ) + Flavors: ( "ALPHA" "BLEND_OVER|BLEND_MULT|BLEND_ADD" ) Flags: 0 Attribute { Format: FLOAT3 @@ -303,7 +303,7 @@ Shader { Shader { PresetName: "dif.lum" Effect: "eut2.dif.lum" - Flavors: ( "LUMMULVCOL" "SHADOW" "PAINT" "TEXGEN0" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_ADD" ) + Flavors: ( "LUMMULVCOL" "SHADOW" "PAINT" "TEXGEN0" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_MULT|BLEND_ADD" ) Flags: 0 Attribute { Format: FLOAT3 @@ -339,7 +339,7 @@ Shader { Shader { PresetName: "dif.lum.spec" Effect: "eut2.dif.lum.spec" - Flavors: ( "SHADOW" "PAINT" "TEXGEN0" "INVERTLUM" "LUMMULVCOL" "ASAFEW" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_ADD" ) + Flavors: ( "SHADOW" "PAINT" "TEXGEN0" "INVERTLUM" "LUMMULVCOL" "ASAFEW" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_MULT|BLEND_ADD" ) Flags: 0 Attribute { Format: FLOAT3 @@ -381,7 +381,7 @@ Shader { Shader { PresetName: "dif.spec" Effect: "eut2.dif.spec" - Flavors: ( "NMAP_TS|NMAP_TS_UV|NMAP_TS_16|NMAP_TS_UV_16" "SHADOW" "PAINT" "TEXGEN0" "ASAFEW" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_ADD" ) + Flavors: ( "NMAP_TS|NMAP_TS_UV|NMAP_TS_16|NMAP_TS_UV_16" "SHADOW" "PAINT" "TEXGEN0" "ASAFEW" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_MULT|BLEND_ADD" ) Flags: 0 Attribute { Format: FLOAT3 @@ -417,7 +417,7 @@ Shader { Shader { PresetName: "dif.spec.add.env" Effect: "eut2.dif.spec.add.env" - Flavors: ( "INDENV" "NMAP_TS|NMAP_TS_UV|NMAP_TS_16|NMAP_TS_UV_16" "SHADOW" "PAINT" "TEXGEN0" "INVERTOPAC" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_ADD" ) + Flavors: ( "INDENV" "NMAP_TS|NMAP_TS_UV|NMAP_TS_16|NMAP_TS_UV_16" "SHADOW" "PAINT" "TEXGEN0" "INVERTOPAC" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_MULT|BLEND_ADD" ) Flags: 0 Attribute { Format: FLOAT3 @@ -468,7 +468,7 @@ Shader { Shader { PresetName: "dif.spec.add.env.nofresnel" Effect: "eut2.dif.spec.add.env.nofresnel" - Flavors: ( "NMAP_TS|NMAP_TS_UV|NMAP_TS_16|NMAP_TS_UV_16" "SHADOW" "PAINT" "TEXGEN0" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_ADD" ) + Flavors: ( "NMAP_TS|NMAP_TS_UV|NMAP_TS_16|NMAP_TS_UV_16" "SHADOW" "PAINT" "TEXGEN0" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_MULT|BLEND_ADD" ) Flags: 0 Attribute { Format: FLOAT3 @@ -514,7 +514,7 @@ Shader { Shader { PresetName: "dif.spec.fade.dif.spec" Effect: "eut2.dif.spec.fade.dif.spec" - Flavors: ( "NMAP_DETAIL|NMAP_UV_DETAIL" "SHADOW" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_ADD" ) + Flavors: ( "NMAP_DETAIL|NMAP_UV_DETAIL" "SHADOW" "ALPHA" ) Flags: 0 Attribute { Format: FLOAT3 @@ -658,7 +658,7 @@ Shader { Shader { PresetName: "dif.spec.mult.dif.spec.iamod.dif.spec" Effect: "eut2.dif.spec.mult.dif.spec.iamod.dif.spec" - Flavors: ( "NMAP_TS|NMAP_TS_UV|NMAP_TS_16|NMAP_TS_UV_16" "DEPTH" "BLEND_OVER|BLEND_ADD" ) + Flavors: ( "NMAP_TS|NMAP_TS_UV|NMAP_TS_16|NMAP_TS_UV_16" ) Flags: 0 Attribute { Format: FLOAT3 @@ -903,7 +903,7 @@ Shader { Shader { PresetName: "dif.spec.weight" Effect: "eut2.dif.spec.weight" - Flavors: ( "SHADOW" "PAINT" "TEXGEN0" "ASAFEW" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_ADD" ) + Flavors: ( "SHADOW" "PAINT" "TEXGEN0" "ASAFEW" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_MULT|BLEND_ADD" ) Flags: 0 Attribute { Format: FLOAT3 @@ -939,7 +939,7 @@ Shader { Shader { PresetName: "dif.spec.weight.add.env" Effect: "eut2.dif.spec.weight.add.env" - Flavors: ( "NMAP_TS|NMAP_TS_UV|NMAP_TS_16|NMAP_TS_UV_16" "SHADOW" "PAINT" "TEXGEN0" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_ADD" ) + Flavors: ( "NMAP_TS|NMAP_TS_UV|NMAP_TS_16|NMAP_TS_UV_16" "SHADOW" "PAINT" "TEXGEN0" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_MULT|BLEND_ADD" ) Flags: 0 Attribute { Format: FLOAT3 @@ -990,7 +990,7 @@ Shader { Shader { PresetName: "dif.spec.weight.mult2" Effect: "eut2.dif.spec.weight.mult2" - Flavors: ( "SHADOW" "TEXGEN0" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_ADD" ) + Flavors: ( "SHADOW" "TEXGEN0" "ALPHA" ) Flags: 0 Attribute { Format: FLOAT3 @@ -1037,7 +1037,7 @@ Shader { Shader { PresetName: "dif.spec.weight.mult2.weight2" Effect: "eut2.dif.spec.weight.mult2.weight2" - Flavors: ( "SHADOW" "TEXGEN0" "TEXGEN1" "DEPTH" "BLEND_OVER|BLEND_ADD" ) + Flavors: ( "SHADOW" "TEXGEN0" "TEXGEN1" ) Flags: 0 Attribute { Format: FLOAT3 @@ -1105,7 +1105,7 @@ Shader { Shader { PresetName: "dif.spec.weight.weight.dif.spec.weight" Effect: "eut2.dif.spec.weight.weight.dif.spec.weight" - Flavors: ( "SHADOW" "TEXGEN0" "TEXGEN1" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_ADD" ) + Flavors: ( "SHADOW" "TEXGEN0" "TEXGEN1" "ALPHA" ) Flags: 0 Attribute { Format: FLOAT3 @@ -1157,7 +1157,7 @@ Shader { Shader { PresetName: "dif.weight.dif" Effect: "eut2.dif.weight.dif" - Flavors: ( "SHADOW" "PAINT" "TEXGEN0" "TEXGEN1" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_ADD" ) + Flavors: ( "SHADOW" "PAINT" "TEXGEN0" "TEXGEN1" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_MULT|BLEND_ADD" ) Flags: 0 Attribute { Format: FLOAT3 @@ -1579,7 +1579,7 @@ Shader { Shader { PresetName: "unlit.tex" Effect: "eut2.unlit.tex" - Flavors: ( "PAINT" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_ADD" ) + Flavors: ( "PAINT" "ALPHA" "DEPTH" "BLEND_OVER|BLEND_MULT|BLEND_ADD" ) Flags: 0 Attribute { Format: FLOAT3 @@ -1632,6 +1632,11 @@ Shader { Tag: "env_factor" Value: ( 1.0 1.0 1.0 ) } + Attribute { + Format: FLOAT3 + Tag: "fresnel" + Value: ( 0.2 0.9 ) + } Attribute { Format: FLOAT3 Tag: "aux[0]" @@ -1662,6 +1667,12 @@ Shader { FriendlyTag: "Layer1 (Yaw, Speed, ScaleX, ScaleY)" Value: ( 20.0, 0.05, 2.0, 2.0 ) } + Attribute { + Format: FLOAT + Tag: "aux[5]" + FriendlyTag: "Enable world reflection" + Value: ( 0.0 ) + } Texture { Tag: "texture[X]:texture_layer0" Value: "/material/terrain/water/waves_lake" diff --git a/addon/io_scs_tools/supported_effects.bin b/addon/io_scs_tools/supported_effects.bin index ef6efe16194d04130a2079cd3feab3194ddceaaa..76459280f4352b7820f1918cb8048703b974edc4 100644 GIT binary patch literal 213398 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 EKSXELPXGV_ diff --git a/addon/io_scs_tools/ui/__init__.py b/addon/io_scs_tools/ui/__init__.py index 6c256cd..ab7db57 100644 --- a/addon/io_scs_tools/ui/__init__.py +++ b/addon/io_scs_tools/ui/__init__.py @@ -16,12 +16,36 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software from io_scs_tools.ui import shared from io_scs_tools.ui import tool_shelf -from io_scs_tools.ui import scene +from io_scs_tools.ui import workspace from io_scs_tools.ui import object from io_scs_tools.ui import material from io_scs_tools.ui import mesh from io_scs_tools.ui import world +from io_scs_tools.ui import output + + +def register(): + # order matters for scs tools main menu items ordering, items registered soner will appear on top + shared.register() + tool_shelf.register() + workspace.register() + world.register() + object.register() + mesh.register() + material.register() + output.register() + + +def unregister(): + shared.unregister() + tool_shelf.unregister() + workspace.unregister() + world.unregister() + object.unregister() + mesh.unregister() + material.unregister() + output.unregister() diff --git a/addon/io_scs_tools/ui/banners/.scs_bt_banner.png b/addon/io_scs_tools/ui/banners/.scs_bt_banner.png new file mode 100644 index 0000000000000000000000000000000000000000..6e2d69cda09ee3cbef3af8798024f7915d762322 GIT binary patch literal 7598 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 VwLFiE*8VHuYN+Tc*DBdW{||Y@`e*e zSad^gZEa<4bO1wgWnpw>WFU8GbZ8()Nlj2!fese{01{3~L_t(&-tC)tlwC!g$3J!7 z>m}(|O~r!>5>R$Toe`8h2nq^OL6#9@>1rGVl}%?jpuhl!;KCvdh_Z|@A=OEUL184a z7-SJ;lTFzY3>YGWU8wH+I$OVYYyPO;?Y?<=uhR{}8RuB%-1EBctylL~zxplT-|}Fb zBXvfl&K#OLGnhIvOYW6ZXVf^?Q1bW0)S34co|TGotIj0N*644X*Vp|0{nJe{*f%o1!*vi@M- zn(r#Uf8`9*mjC^VTa(tYjH(-1quF~@i_N_YxDU7&_y*7qpsf$7GaCU<0$sooU?<=u zKxyljQP*hB6#A!TM!IAt))f&R{mM zx!k@tNjRkIaR15}=ve?v21*6KRDtOoa7w%Nb$~aVQEl*%0DlB7*VcbMb*7}{xd>(k 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 M07*qoM6N<$f~=k<*8l(j diff --git a/addon/io_scs_tools/ui/icons/black/.01_mesh_model_object.png b/addon/io_scs_tools/ui/icons/black/.01_mesh_model_object.png new file mode 100644 index 0000000000000000000000000000000000000000..4e626b530e892cb8b67cccb36615344185a87329 GIT binary patch literal 698 zcmV;r0!96aP)6YD(p(XQyhYx^$_MZvG#g8|6O~j}D~Lki3OPbRBsQ*5l4jSrCW4pE z#&O+{EAo}g?DzZ4pP69>_^vN5MRuSNg3af1bsw0js&hqAR-50r%#X9#EW21NT11o& z1NJ-*jYb1R@92^`VlgaD^2Y^^C z1^{TaT3g}11OVXT;sT@52<37awr%e$aCLPB#+Ve`m%zFTg+kz>`LG$=daTF^oSvS7 zbE!kpSBfHix(5q{DaP2&@JIzhi$~Q+QIzP`z|Kux!Cxk@mwXgW*6{}=aD06Hc>vb500RRu4=bdSq_Q%&FssuK-ArVQzfe&s(s;X{1*@y^?F%S`m2%K|p&XGtYU>F93 z5ci(vogEz=-3Rpu6u7&)3zbHtQUL%dxKBXSw5OMsm*1Y}{R~A1aB^}2*LCMIaNwPX g$!9%4zU#lOzZ5M3lkrs5#sB~S07*qoM6N<$f)i^xg8%>k literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/black/.02_object_with_shadow_caster_material.png b/addon/io_scs_tools/ui/icons/black/.02_object_with_shadow_caster_material.png new file mode 100644 index 0000000000000000000000000000000000000000..7cc3c4f98e65d546afb27b60a02bc7c0bcdc202b GIT binary patch literal 750 zcmV;qN6#RF+UZ`k?Y_9n$>Fc@bdB!?RFck>uL#SjG zz;rsr!NI}W{Z9lC(OUrDJow$+9Y&*(7MydAi;D|bmW9D!fOI;&5e@*4db5BxtJP{Q zEWA)Cpx^J~_V#uw{E>kF32!!=;9PI+6AeBm?P$*~zV2nX2h1qPjGkjSA0Fd(EWw~5NrBcyzmJ&)S z*tU&iGPyI{UjYEX>FFs9!_apQ0o*7cpU)#2jcR8vrGyXyjBP#9^#tgeloFhC2qBiO zdsYFQ^JNA0ZNtvq{?dE*&)NbnV=w!CTd#rzIe0@PrUhNFikTs&Tu$HG#Z6znjaWrUjdD`VBO%Y zo&$`rjqu52g27;bY&QFeG4^>Ge|2Lbkyw`0gGM5eMj_hA99>yqobo3{L{)oO2o*vu~i gFZBcRYAeR6#u<%zHi3a z87Je)24nq$pdg{Cuo8O7vUm|J^w>}XlF(c5luJw?mq3Cw+k-7hPq}Z}OH)c($VF%% zwU!D+;=v@OwH|aQMpyPv+?|=%OPu_3bv0>v=!0Q~?|tvR-}}AyzWIRvyU=!v%-jy( z3jp!A5hM)5B_YH)B3gM>C$}>Qf?)jo`SZs!nGA-8hTyvH8v*F=@5h-lXBfa|bpg;c z?Y&GU0|30dyqxoWKiqHdYPEWBdU{$DLUe7Xm$eLshK7*KdGpgZ$^PWYlivV1 zSW7QzDJ;vvfddElO@h}W+X7(bs!|F>B=_p@RUsm!lmatXw$sZRMn%Ig@bu}^ruO#s zsqOaIwhi01;kqtt+lJ#f(Kj>W$&)8YrBVREhG76ev2Fli7(RAg7gEaC##GZZn5GHa zw$a$w2-7rSSr$ywgi;EoY2y0z>j;7XmSw^BeTv892*c2;y97drhpnxxQKkT(l!`KT zT^EkyL@dN&F<6!r0SSTt0Prkxw6wH0D#%v-d^0gbqlSnt;px|&@>H7DHw(U z!!V-Ew}QV2=Je^)SYKa<<2V3--rin>Vfg!A0NhO`lNA7ZB9VZu>mVZNx*h>A3+3^HOUrjeRI0lNp7p`x$jC_4c!@+JdYYS?qjx+W zhY$kSu3f|C<|Y7ObaWK=?%iVm7#SIX5aP>Paca&v5#43xZwChlkKew18|&-q=;`T+ z=vPWX*L5hRP%f7N0FL9})TvWgTwFvxpQpjWK_rvOED=rDjDsB{pU=k|8XA5bA0O|Y znwo-?lDoUR>CmA=(R6G{p-{le$_gGlc)*oPg$4!&Fg7-JzoDVwLn2z+7XSb#l}a5- zsUL3KxbZ1)8 z8yn~D+_}>{J39-{^DsX@4*)oP_%ITQ1d_=lQmNFc<2d6&h_8vLT#x^{g_&c_{86=9 z&1`IJRMY7+r_*UJ6be;lzQW8y%pBW`PqCNHL{uW8AElI+bGcm9z{|_a62N65`l;5q zy%hj2NW14fKiXGUR{@}XKaTbbpzTEfD=RDTeZTFU0SJQNB?dgtgOu{!F+fCZb8~Yq z0RZstGr-J^%zTuH1|}ybw*tfSJOn{7z|2RPxp6N(cfE2ka~Csz6^7yaOG`_gj~+du zt5>fA0GgVb008sz^T=kism{*MnXaxb_I>{^BKoaTshl-U^UodG+(}t3mpcLc+}GFF zv9Pd!bUFD!g=IA)KZ5yU(qEINHTrNW?g=5E#VQg${k(pDmSZr}$4CuQ4`K3#j zIu;fdqBYyL(bo2Ixs_6oQo?mzR4Ns$uC8KgY6?WuapA&+2>`?UE`eoP7#@CZFQtU0 zX|L_`eIEcIgg`tVhvPWWC1u|wVCMEP46hJT&#s=fx4$ohIJ?^q=|7c!0IEH^XLZ%Z QUH||907*qoM6N<$f*uiOVgLXD literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/black/.04_object_with_physical_material.png b/addon/io_scs_tools/ui/icons/black/.04_object_with_physical_material.png new file mode 100644 index 0000000000000000000000000000000000000000..00138e0d85569e4ccfe22f227d89ecb770b3a5e4 GIT binary patch literal 1706 zcmV;b237fqP)({SO0>F(MH#!L+-vZFbaon$)nwmcPpArZLgCT}ts#uoY z9F0cH2_b*i00~hDA^nP?{Ia>Z`8FYht~h~kI4lnh4H=wHXY1p~k9hz;$P+q}&1TQf z&CR`g=+L35mjy6|WEMs7$D$}&7w@U6TGZ9m1wzOkLP!e$WV2Ze3=BY)<%OYEolf^F zj^m#8^z^*b)6-*nWd%f0{B}B>?&dgdoMD(oK@frfKq=MTzkgrhc^-bhUrr5s0 z^5x6W>-7)>0d;kCsI07f8ML2an76~>a8GAvr;=9zyUxmO7fS;Z}e;xo(Q&UrtPN%zNS;oPG2LS-GEF%~UVks*vErrA3000m|Qft?) z?FImLJ|Gkd{fH3qa{wYC#Fx!x??{q#&*5=>Pz#s$O!rT&Wxd2qETpJpS6*vuAgnK7G3L!Gi~Xsj8~_S11(vfKvML z)vH%Euy{N^4}^h%fhCKxEQ{^ix6eyY0lcH#h4~MM!*SARG`4!Z-b6`B30hiO!1H{u zAP8UY?Cku8EXy#NO!KV)0E~`~Vr*<|Ndk7e9jjNbo?qT>Zf+jY4!$_}$&)9>L?UrI zkw~<*w6rj>SS)A1wzd{tuUA`3k_5lsk4Pi}MNwW#;MlQa*tBUA0KjN8{;s^d{Es%9 z?E)c$<{d=C!^2+%@V3wA~_0JDwRSqnS@@i zhtKE3Rf zeslNk-S=+Zys2smM5EC!OOo_fGMW6&l1&xojZ37IW1TT+p%K@`uh4XI5>!8GD*wJ%bO1zIPgV}$Fq}Rn1;#8$uC7B5n?nN zzgt#Tb``(}ISTx@*52N(eIGu2m{3ZO@I3#nAP8?dozB-+ty=YZeSQ7+SeE_Cy?gg= zuebsX!%T!iA+@ow(I`n09zA-rQxwJB)z#I%2LPwj`A-0sK9jdn1QbOX2k^C9w{AVJ ztgJ+9Yb$g*-5E-$u29~J5nvcb<#~RXb%707*qoM6N<$f;xmM A`~Uy| literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/black/.05_locator_all_types.png b/addon/io_scs_tools/ui/icons/black/.05_locator_all_types.png new file mode 100644 index 0000000000000000000000000000000000000000..99a210c63eceba329385982cdf16f648bf81ec61 GIT binary patch literal 748 zcmV&58Vw|U1YX|7xNvcC z@rxusIyz!@cJ_w(%*+gjhljYk++906JGWM&QmG)xfuDiGznKrZ`-d#cSX)~oP176Z zOG`^Mn@!yP4`32_De2?+`8jiQb8R8!=jRC_AnC(rTelcvZDL~LTBmbqJT)~%wOU0I z?*1$A%|F1ujWNEJ#OCJat<|VjtGBmqA%s$vWjBJ7B%xd`b9Q!yq(6bzpQ+&Y?%t@^ z>x_(yw1v2}bqgVs@;ql?VBlK7*47r1kn}t7T?colDCu2{k(HH|&H+9J*A5O2NYj)w zP1)Vu#oYt&<6T}j;EB8c6<8n?-p0PipJbv!@Z_r&wn eCa6bsqQ3zjA9C5?^;J&*0000&r2&;6vyv`2}#_Q zAcps1?V{9DCOi8wSX?O5#zozB;o60(2kpL#{sHwb@E;I-5^$4lA|c!~ysm_|d4kCB zL<2&>g;TF{&!>wfX%h2eqIOf>7cR`rJ#)_Io^x~0IO-V$I7+Fd_Vf?Oad?W|)WJpM zz!-xu2A~Y2huEcoa>q_Y4%$FZ26$zRK`Dh&3avFFQt5>N(jrpPT6gW*u&A})YZidwCPh*U(R(rh*#tj7WX=A$TTXstVS zYXI|c06Li%-1$@hQ55N=r6rV7WHK4*^*Vqa2e7}tKQ!Rr+C+db3>Rv(TAzb_K97hH zhT*~}fFKBl?pkZjpdGN87zL0{r$6z1zwftJtJPf_m$_oGc%}8fiPBoDVzGEN%r-I3 zk?Xq3^SthSadA<_Q6iCe?fd?bF-93d==MjBEa3)xB)?S{To|4PuMm84T_u1XuB?y8}RzeY} zSZjX*yvGLr!diQAc6P?<>MEY+kxHdTLUcwlnPh8gi^Ic1!Z7S)=Rxb96#R1$`EY)I z&f3}zfTlF6vy9OZn;PS z#ffaVMJ$XlW-p;ob2%#riAO_WG#1np6gFDW+i4;358w|lQ3y6_b%fv^L@E*s#gNPD zfg~7{BQjyw_r5l89zX7O?}v@?C6mm~?9BHw^WMA}MQtzD58%&qJ|TtdymtrDG5xf^xZ>3={4Tf}r4e9*i*%kph7J zC;-0igPETIxCh`KGe56}&{_jPHdYUSnO|fwnM^jDrS9%-@;om?L@(OvyIQ8T9#~vl zJhp9)<3L29wLW#snsmYWdfPvEGO8`hf`|aXU>E?k8iW7_MFg(vRtCDR+cLn(V;B!} zMKYPBL?ThCM>Ws$sITwO!KsyqLIIjJ$8ji?N+9>VZM$Yh%VaW`o}Py9`w$T@vlfwe%=`#|+pPE%W-e`P zY+z_;2(IfQkw}E5SIe?6K0c1QxjAU9k9Mc#)m=;cvWR>u7K<1j9)|0>m6FY7BV97p zC7;g&fEP(zYki5C{rUNMjEsyN6CWKN#rF0#j4{aP^O&5RL^7F-1X1hiK4n1=c-5

)2^8WvC|D62> X+S?;@Xx%BC00000NkvXXu0mjfp&gKC literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/black/.08_locator_collision.png b/addon/io_scs_tools/ui/icons/black/.08_locator_collision.png new file mode 100644 index 0000000000000000000000000000000000000000..7ddc91ca85aa9f174d10892e24bfbb943b01cfa9 GIT binary patch literal 1032 zcmV+j1o!)iP)FCQlb@tR%qO~8@h1qLX<)5!u>2;80jD2pHRe=omE6$6J1v5W>V7E z*9mE;r4TVMCik4L8{Z=t^YW5WH>Dq39`~Mm@AtiX&b{Yo^gyW9YR2g3=nN6XiHMX^ z)kq}ro2F@hJQODp5dnB2B1a;EMxz1Swqe^gT-Sw&Ktx_&UtfRrVC2kvwY$3uBI?>U zH#Z?7e*t)U?*c^Rs291{wzsz-B1eP#*<%2Yd(ElUY9ES72a#o2z6%P4LWfO67{Ejr z@sv`kkk9AQY&Ibx1pre4bLSh4Mh7`Fe-Gd}fak4N>$_sH=-ZiD08I82pp?qb&d!2} zFf=p-%d!9)SWSXYmpP88% zlrt2M!X_AoLAtJ!uIpqN#+}Hzxgb=)eE~p>h)(MDx)qH^fA)efa>sq!aWOtV?spu= zp?Ez0B>NGbKXZQFCx)6+ym#LU|OzP&dAmzS4sMn*>b{=&inS(f!8kx0BCBC1p> zG&eWr+ciz|ZcN~jJrfaRG8yQ)4qexg$z;4c{H)uWexKr{uw)#s( zo`7^Zjc7F5mjF*hP&&SOet!gz^5*x7fpH?D)6>)TBlfoJ zioyOkY9vsJkh=7@05qZtb-vBV*m;4iEE|*FrEG{m>G)?Hb9vaorXcTK} zYdAPKXfM42Q+E*g6A^i9+cuV#mSCEu-{$A%Lq)vXa=Bb!>b?cnbw6fi=kV|l%gf6h zadCnD z{e5J!*}DmFT{rn|v=b4s*(@rR3IG%Vcvs2KXw3W)z$r5;5yAQSIo8+L2X}$%y2)m< ziHV5`fAR3}Fv{gJn7I@dNVQtc0Pr<4?~BOWn*bav2m$2J&d!iZrI1RcP$(3@%ywV? zyS-ODtb*>gXi7v1?dt3G`bUG3-x>YX|NrAZhkpSoHmh#9I|zmV0000vJkEF7gFU%#3oHDDLWol$`rfJnqtWOj2mDDqEJ7_y|4EyPq1K=V3e^SCmpH zl}d>Llv1$};z0=Ul5o862-nU5-}f(qAb4*!o7x%fbPi-PnSqotFbrcavq0-F*klwN nhOwJbJa>t`d`2lLDJdyG0007^Nklu}d3K9LIlm(Oz?X zi#QyWgHt3ZI2BuvTFl~Bl5T-^%I@OiAP()2U55w`t%zW|IJH|qX@)H34=|9Sq#z;) zM>umm?%wa)$;?O0>@hPE8Xhwrxvo2T zo2PQUVdk&pa(QWUbCW#JOXN_RrqSx^Dy^)nR8>{Y_5%1ZJ3FhLot-6`?8L+bot~b0 zs;WM`xi)g6Kgs2CFLrizA|UE?I9k9|i6!1R6JSy)(z4H4<)=jY*hUdMG^ zYoPZB;N9`@@z4O3N(EBNw*!Ab0Mb&*Z-qi(V2Egcp-=!be+TerU;rYbAPC+xn@x<5 zk3$H7bUNMBT+=iNAuu*JhDM_S0Iy@2hbbsQ5LDOK*ZWQ(+PAT>0RS}sN^F1-;z_&R zmYGbZCq%S9H8lm>wk3dPLzzb>nYb`NhRW z_`cr;@HjDmFom^Rt=2VCRrOYYYPAX}Z+ynfV0Oy8b(CFwWrBW%wSp{I3xM zP?-79wryNpU4fYohq-#i5{8CP!?vy{3gQC%1bG5Yeac@r^8f$<07*qoM6N<$f~(kC AtpET3 literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/black/.11_locator_prefab_spawn.png b/addon/io_scs_tools/ui/icons/black/.11_locator_prefab_spawn.png new file mode 100644 index 0000000000000000000000000000000000000000..5aa3e887e231a50c708f5b5e0ac8c0980f65adc2 GIT binary patch literal 876 zcmV-y1C#uTP)&ubG~6vw|e7;RIm zkX6wxgbd}iw9U4z6a+Wwx^4n82&J3if6?_@6*om*pp@4|X>=vHcGuKNe?XF<9Z={@ zA&Xd*o?SRsnkzSPQvCo2!hb_ym~5?A^_%cIg(r%i1Qd=D2k%( zWHQ-=Ft%-<0ma`20F%jNGfP7Q1BfB4!^6XR(@}?MDwRr|q}7e~wR4<}cswNT%7eEz2RVnr9JUi9mc*Xbq=LZJ| zL;L&ta2yAvQVGRk5n&h-U6$=kCWCA?i|OfUOifK;a&j`7ot=HHlv-@vm*_^cpD{8r za=DEp$4NdQ`9egNwbmQPn4<6dQS0GrpSEq&_x;EiQ`A~-h{!U@7qQlOU-Vab2ruLO zhif=DH+SYZ{=T|vcupZY&FAxH>wG@XqobqKD=9=BO7ZPIJC=^KY{=iFdr)Cs} z;oWwCFbwYwv>9F0jynOgX07jbO!9FM1Qd~-_6Nz&0Al+I5!nfXfFvJZ$(k+FMn4iZjrpzug-r0i@Vht9vcGy00007E=D5Nr(jNCk%&Eoj@7)M7(D3wZxMx#h15{N`1a2yBQ z+uJA>i%9^#w+h}8fDq!t*47p*>mPj`9v)(SeO>wsg#u1bPf;uuVVWkw;V^^{*xTEK z5aPpk0U)91j+&O#uMn@i_c`Km2|_Ow&Xt6mr=D04RzA$8nlse9P2U6aj#)>tKvQ2mvC( z%*>2rA`}XNbB^BLUb$Y=G;r=kULFCsN*6*v2!TW*fkvZ&d_E5V7#J9UX_}~3s}Mp+ zz#RkNmKza42m#JHCMG5@HZ}&^wo$26VB5Cz8-~$Y1`NZHDe!VBiUL*j6xw?VR8^H4 zux%T2b92%^I5>#;`FZJYCk38((liZXUCLcq3dEG;cbe>faQ zI-QpOcK!vPRy`LL5jAZN0G4H8cXwB^psFe~P3wFLUMj)I$HyiHyr$qq27Er>UjPsY z1fXde9LH(d2QDu!F*`d8pU>A6<5d6{V-^wJ3=IuI*L9T3We6dfO3<~Qo*q;x6}YD- z(=dC8zrPOv$mMc~#bV%` ze`*!q^RYai&!dMKIV{UU zqtR#rVHgJb`uZ?BI*LFb;CV3dY(I50`wEyJKb*dm{SCURI7Z!mL-7Cr002ovPDHLk FV1n7ku9*M; literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/black/.13_locator_prefab_navigation.png b/addon/io_scs_tools/ui/icons/black/.13_locator_prefab_navigation.png new file mode 100644 index 0000000000000000000000000000000000000000..42e630390436398634531a80209fb3b785461cc5 GIT binary patch literal 1116 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 literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/black/.15_locator_prefab_trigger.png b/addon/io_scs_tools/ui/icons/black/.15_locator_prefab_trigger.png new file mode 100644 index 0000000000000000000000000000000000000000..244a3ae0c14ee79db5d69c6e7a708b0157cde852 GIT binary patch literal 826 zcmV-A1I7G_P)p&LMmUy zP?a_^etx$@f%54%29$*(dGR~%o^wB*pA0k@6t>9BLt5*PzVBz_1UC$0QwZ^rh`z)! z5Cp-u$;ru{g@uK>#ryjDP%IWPF)^_srF?!JPlN^WJg;MMaS@xFn{f&N*LC4}UPl-w z5hYN2dpin+0+dpbgcFGbIy*ZfaYZE79inChdV6~TAaX{xYQQwjjbgFb66x}cPLdU0JT^#A}Y%YtDTr|&$^+rN~d5W7V5 zSxWhqh;}co(^cY1sjo9LGucw9g!T3H*x8MZjbVIz9IdUb=<4eFUaeMNq|@mip_`cb z)AaN-1Gw?i-`~&Wa+&An=XrT~nVGp;NC#P4TKJ1Q&SV9l+ap79;$QMiQ4&H zGC)K$Iy#Ck^rE!j{Gxn z9TJJe*-S9icGf&c&j07*qoM6N<$ EfK z_Ta!W!(=kww=5cRy)7pfiQ;1k2qChDVH|)HLO{q{$D*22 z8rp;qXqpBoC4Aq9X_~hY0DxMphC-o$)oO*&Xarr?!^Js=rfEo}QU{uIj(WWg&N-x% zi7Ie*3WmdBXsaDkNOEo(QiQcA41^L_{EK(ZQJ)D zaLxEBT`U&S>2y?@AGuua*G&YHj$gC_j4||jJ(c2T5CorYzL4bed6=dN#@Js+usWKR z%{d2S4B2e>>wgV`;G=1pucz`~B;a}8#Xd%5I-Mp4KS|(EzrF+ba1}a$YPE_W2sTN; lfipLvM_odn@|6Ehegl-3mLcih&uIVv002ovPDHLkV1lDW9S;Bi literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/black/.17_collider_sphere.png b/addon/io_scs_tools/ui/icons/black/.17_collider_sphere.png new file mode 100644 index 0000000000000000000000000000000000000000..01059ac7e54c2bb55b978af2e3f84929f9a073e8 GIT binary patch literal 859 zcmV-h1ElJx?216o#LB=i^$& z&=7W+3Q`MFv5gI-Qn*fdLsF9+rcHg9eCJ{;E~m zfmZ-aOG|8RZBeOI07xd2w70iojKT9fj4}AWk1>XsnHg4AR;Ad>4QjfW5svE-o&JqKITN8MoYV9301qA+**FRW6sw<#N_* z`Yr%J0pxNyqA0>~98#%N4B&YluIo0ySRf0P&*!bz;H>~^r`r>NVzEd^M+aS9T^M8H z`Foy+@B6r}i|e{y0BEf_KR>sYKQucrU0TTZeG-WTB0>~J062~l&)x#qPFNVbBXJ7` z2j#0fFf=q&20jAx_V&i@wjHp6^<%iM6G0GI%X6hv<+eNU9$aUf&u(DfJ)l2f*ayB-7K=4Nra*$bM?AnV6Vhbad2O{jQYyziAFFRQp4j2RJ@H zW@BT6%galYQePZE#u(=3=gDTX_UGG5sb6mLZwU~Fq2J%%|I$JLoSdAH&*v$XN&s|s zcazCv>SqqHeR_KOTRNS-Y?bj<5|LkPZ$7P56p?555_%0pN<002ovPDHLkV1mrphL8XN literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/black/.18_collider_capsule.png b/addon/io_scs_tools/ui/icons/black/.18_collider_capsule.png new file mode 100644 index 0000000000000000000000000000000000000000..85ba66e43e4bd626445f71a58694b3551cd0ace6 GIT binary patch literal 668 zcmV;N0%QG&P)!A}!G7)ReE+a@Jg zIFvM@B+!O%YM{D%s*p8A(`!kLK`)SC|EgN07mErvZiYjfc%qe-GY5z@8hV02ILzwj z!A=7d3@t@H^qppBXTJB_+3d_hpFVy5G3q9R%H^_ea&mI8lfrv@dnb;7} zFCt&1JAO9j=C#(dHx@SUzDwz!SQC-9RaBSdp-`v@+y^KYi)6D|YPA~2$HyJ5 zU@#bDW@d&=CWGs`ru#GCDy2@Y3?L#OfTsY9i;HY-ZgP5h%JA?oe!su#*Ev5wPoYpS zmupI?=WPMB)^nC+l>nBPm&xbz7m7?wOmrOpz;#_#S62b>Ja5+L^L+xa%mB->UIJ`y zZ?m*Xsa;!(qn8#%?V)tgo+Q+cv=C z)`P?E?;HpN?Cq z0l?wmA*E7@gM$Ok&dxfL1_A-b$H#G9mywYX(_H}`o!^Sv5Rt*=!O?5pUUe8Q-O+Ub0000%}XP97{@i z=MfQn-^cfTEX!hLWrfMf$sp~yi2QWRfa|(L@p$|%5wQSF)5Nyz2j97fu(7d0GMS{& zXmE9PbuFIH=b4+E18`i|owO|L9J+cS9*;jO6be@N-GpImY>ZSYh39#FH?zn$0EvgM)*tuC4;GzrRndR-;rZF+V>K@H8;c z-Gq;ikB`aaa>Qb>(A&_^5UPsr`v5F2FOy6rNhXs7jeK%)($NZb41jXEYyy^u5R1j= z@9z&?f#4cd1z>b^v=bW{83CYLtp=wC%jL2;H8s^T09vh<29be*0UC`)XaivhG!1}k zHrrNJ2e31l4E1_F^nyX-i3qC7>FFsG6B7WOot**DHQu#sHrwtPc+a1}lf}hFDwRrT z1I=a=%d&_@qj;W2u~@{mZG7M7@bD0TnVFd&{f}U$+rS&(NiLV8P$=9vO81l$fWE#y zJkO(6tFg1Q(}_o;(O?1sEud=(T-W^tZ~)fV*F#SMz~$v7hGAeBw?8p!YiphIUtC=L z8XVkw6+Z`l1Sprw92^|rIL?Es5Q#)EO|vrzzVDMrB*^7*WHOl`?JE)a`A!4xO}qrY z3D*JtP_Nh7-rlC&ZeyAz3kwU~rT;B5Rq^l-J?g3|B}A}vVULmMSHF@00000NkvXXu0mjfhzOOp literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/black/.20_collider_convex.png b/addon/io_scs_tools/ui/icons/black/.20_collider_convex.png new file mode 100644 index 0000000000000000000000000000000000000000..b1e1afe52f63f4bc97363a1fa5077a1462c35939 GIT binary patch literal 1105 zcmV-X1g`suP)bS7{-4y`D4aJ zO`>u2uqasTp@LC7>?(K@@zkmpSy~Fk#comdBHcZ;>m?P6dt7!wg~fySTypBtOOYN{ zM94vt68GT2&?crDotl}vdvStk{+Pwx!a^Sy2;cL)&+~lmJM#rTCT60T2f zEFvBe`Cdd0L?kI9Kc!NsrvKdd=;+8NB2Pr*t(4-eh&(wuI`aMN#>dCUts?SFMBZO^ zTwGj~#l^*sgS;1!XUE6Kt=Bd_IXU@4M3zJ(yRxz(fD8-_NGKGN=H_N`yWQe;yT$AE z%EH2eoS&ax4x)?5Z>OiHUsm%_DiO=e%fCdU(T5uw8_L<)SrK>Z)~)hlib7jk8{OUA zG&VNkbUNwo?iQ!h`Rnxb^rLb$_DTVonwsvct*sRT+H5uk2M22z7h79fsH)1nd-n(g z0*b@oxC1=8ve-f_7ON8x9S}e|J3GbtUF$P8HYSlsM558Ch=?AG#p-Gfkjv$Uv)QZw zVz=An{{8!+s%p*Ve!pMB;jo0m;lc}%%jJg3d8uXRVz&>ydi4qbx7&@Xs@%SPyXFIg zLLuySJMnlNUDpemH3tBOMn^}Po14Qh4C3)PKA(?ZFnFc;%*+fO9UUAV9+FO{+1uMI zX#SG`UjZB*9s;nvy-hluW?*1|rl!gl-p!jgnVg*D^z@Yd{e1v-cXus%xTXNQuHOLK z0IV_O^LbvsevN6GjE|334Dj^nQ}X#du~>|JJ`X@F7PIJ9UDvxlt||Ep>+0%;0Wz5k znM|gzp2=j`+1a7Lzn|IJ*^*B5_V(iU`$;4cWV6}J2l`n1%~d1#8sO#2mnB3z9!Eq_ z6a|mRQ}oo=*8`ADCQIt9aZu(xD0dOv0O;uGC?ULFFN&g&PNy%60-$Lcy}iA-T&|)H zD2hT$OG}Y<)eabj5dw^0Fv!xC#n`~mz3 zFgG{H#Kc5_^ZWe(oS&c96riuK@A9Yn!-aPB02ggr27U)vUtg!Er>77gpU;==z`J+v zFio=%AP@*JH8o|?8;YX*T+XY~Jpk|>@F&3L<|baRmxhK0Ow&X}N;VsYK`NC((=;?q zV}5?#qIQ69E16UbpeV|juIp34Tc^{>>gp;sn~hv9S21xinM74p?%us?Eu}tu_%Nv` zO14r>#lK?!P1F9$<#ONH?e<7#XQvIIp`oGTZD@_Ay}cd4Gz??f<#N5@x)71a;c!?U zJa{0cX-YLjA`v+_I1mweT#L3^%O>EtrfDxmMn*(ctCw_jb%}_)0G?m_U3V&jSMkjiym+qk)d&sJ*?t{M@KDJOjj& zCno@}fFBXBEs*49?C*(|_|X2U-MUV5HaQ$Sl= z8*_7W055@XGX;DJ6l=jH(b?HaBoYBA0$ZnlH`;0Ni#Wq5e_4X`l=$8qTI z?`LReh{?%GVzC$-8yh7Ne*&LAICJ-aZAm{%`c{&aOeS@DdRp$kxVX^y`ME61lF#Rp zZQJVT=#bCnli%-`-|sK`27^ImG8whBwEQjU7fC-DV~TfqRuz0u(t)J!Ha9n|si~<7 zxFn_1X&oOQE166x6bi}nJjLU2?dgtjt_4f8^eSKX$Jw1~2T2fKc2e+ZGCACX(^ZC3M78Z1MbtOr< zzP?s2ms2<#*7o+cB<&kxChuGGic-sUUAnuwiAJL=FE5wY^9uOt)<1lEI>t+T9!LOx z0j#dB5{tzM27?3w0Rn*lsZ{FW(yb(5W@ZN0b#Yx6&+|AuJS+wM4tRBZyaT`=07pkh zjE;`hC!n+h0Et9`L?Xe>%?&^S_!=<(1;3NW0N((GLZLt^mAVVxs})#X?;ik>dcpVP pi`?dTyZb}4Z#?hq2K8k1*gtH|NVDe{J@5bk002ovPDHLkV1i!>iiZFI literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/black/.22_scs_object_menu.png b/addon/io_scs_tools/ui/icons/black/.22_scs_object_menu.png new file mode 100644 index 0000000000000000000000000000000000000000..7fbe0860e476fd2c2c58d0ed59e962391d024c28 GIT binary patch literal 1676 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 IM6N<$g6h;om;e9( literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/white/.02_object_with_shadow_caster_material.png b/addon/io_scs_tools/ui/icons/white/.02_object_with_shadow_caster_material.png new file mode 100644 index 0000000000000000000000000000000000000000..2f41e6907c0afcf912db7a7b71c09de4b553f8ec GIT binary patch literal 765 zcmVkhn()-@BiNW z+m7KaUO7?*dlrDDCoqP9pY-qw@CVTIUg>mN16l}i#a$7J;AgwLyLxkTGZ7rnsZ>e> z`8bG}0k`+|_Es8=29-*MbUJOMT5HY0!2z~y-vNZ+_VzY*f0Te_;9CG+UtcGe%NfCa z-)Db+AKSJm6bf8kUe1KW!`UozS9v)pyxne7EEd_=*mx8^E#TSU+uPgtzM1iZ5iAHH zdbL_@X=i7LZnw+f;h|9ifQyR@9LJ$lDzUY-WekUZ9moMotE;OF1_Rpdws8WjHJ<0O zxVShs96~0PQUcIwwP-e*Mu(-8c%DbKT4gvK&JBkt6CNKQ=zlgY&B zpp+6RB}yr@*3TorG^CXHzK>GsesnJ?!1w+84m@=Xa|;N!KKT8jBM2%Es(<$V3`I=s9Yik<#Ua3^x#R*&hekKJ7 vA+!);)}G(>dc7D04u6=u>IcYMyjuJP`1Aee;^)=X00000NkvXXu0mjf1|nC- literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/white/.03_object_with_glass_material.png b/addon/io_scs_tools/ui/icons/white/.03_object_with_glass_material.png new file mode 100644 index 0000000000000000000000000000000000000000..44a7bb83b089112e7b6134b49230a395dc5cf237 GIT binary patch literal 1322 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 literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/white/.08_locator_collision.png b/addon/io_scs_tools/ui/icons/white/.08_locator_collision.png new file mode 100644 index 0000000000000000000000000000000000000000..1d0aa5468c3b3be614d70c739b2127d9850452f1 GIT binary patch literal 1058 zcmV+-1l{|IP)3~hPT;!0viG0;Wn zCb)TY?;r4x_CIJhLh(saTnOE{?bh@mN+_BUTs5%^U(0K4^3azWy3p83Omn(PrkVGW zN#c)9=?9nL-gEE$zB}jKdk*1&pzC_{^78UM0Am24s_Ls+t@bN`{6ldzVG&V^h$I{i zi)or-nx<%)CPXAe^lV^Y;KK(aXXf(q^0IWo`ue&M(H{UF-8(xGrGm(tSY2HeB1-kM z_h_Sags$sv6OoU|wr$S^@p#;4Bci_n40j_o6h(1nW@aQ53JDQ80E`ZEKMIG#K5}OM z2Ebzgk5yIu+OjOq&dhZHu|Rf3Q8GtIMA3$@pAw#0KDvWasYV5%&%TU~*hzh1@%I@y2tB6<$sb0YeVi2fuZiAJNcxVR{(RO)U5G)*&Kk9HA}R4OI0SWE!?j7DwW zfC|87<}U#pGINcHWO8y+Ha9o>cY&s9W+)Vr^Ye3Wak*TU(a}+1<`?J^NV={^0er^H z-x5)w5rBOMVP<-ITC&-!WV2a`$K%4xzxCw5H!+4T6?8ioMC7!p9~>MUzXkc7(Y5~n cAOAW01C*{gy+8DPRsaA107*qoM6N<$f)8@%(*OVf literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/white/.09_locator_prefab_node.png b/addon/io_scs_tools/ui/icons/white/.09_locator_prefab_node.png new file mode 100644 index 0000000000000000000000000000000000000000..b77d1362640db21f83e4c5ecec819951f9dd721c GIT binary patch literal 514 zcmV+d0{#7oP)y{~m_nIdHj)0uFRte`&Q^MlzX5Q!vvsvxZ?rPQESE0yLli94`ujAjs+ZSDm$54I#wZphAccxB*UAxE1gW zJOWAwG)>cPZQCyA^ZAtl)9Li=J{%75egDC>?cz^*APLky&l0Zd((m{0lu}h4C^(K& zZnxWWiM}JH#C6@7?Kn=2EJEod|v08}cKC@omaVhfZ~ zs_J=O!SlSOAp~-kWnE1T%d$u$5=9{dola+E0wF{wrM#9>-mh@a4dKQ;V4CKMVHi)% zW;4?3V8?qP2!g?6G8u+9=#BJ!%0K;`J@DBRcA7f86r>FFtkhK3vwF`DP}rv9O6nlEQ&W&i-pj7TKn zE7Z1a)M_;VfN7claM(`Wi|+33TA@%7rIaY8#4lcbu~?Me-d@pleXL#iPjho~feFmc z&I&VcbaHx+bzQ$aI5_Yv;nig_83~0#wqY1gIuQVXT+|rTuTXW{`mMbm}YcX#)<65ja!{=VqCUIOqaNC7bO(#Xh2Bgl2#8wEy3M}?V} z0u8SdV&-frmHOorynZT`5+ce229jaKvdd?P%U%SkjEtpXVS8v-Dr&kG9+4S~tY zNg*QB@B6fl3p5R-Qt53^PY<@Yw~&MdX_wf#w@gOI+8z1b~y16J)d5Yh!+3 zzlA|@FK{pLUkX@fXJ@#&x`OMva2y8!tbo1#2ocd{EEbbUBqBt#iCbK~{1y^`XZ7a{ c0P!pE1H3U@MW)57_W%F@07*qoM6N<$f?g(9nE(I) literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/white/.11_locator_prefab_spawn.png b/addon/io_scs_tools/ui/icons/white/.11_locator_prefab_spawn.png new file mode 100644 index 0000000000000000000000000000000000000000..887946d192420334a2c288ba167a4d739cde59d2 GIT binary patch literal 877 zcmV-z1CsoSP)Ou3iE7>6ZX`{2!K5 zegF{kSdS$V3CzyU0)XXV6Lez?`ysFx3WW}dr~qK9b5TlZ&CSihwrxl$yAhZoqCzMX zIs~xz5CJa)iKw)>9w@AFPSC%jGhcN+l+u?GE2H!!WLQc6NAZXy^pqyZBF7 zhG96&%{>tA&E;~R7{KnJk^_Ji2n3EBjYc!4%49MvLhiR>W^ObZ&0W$4NdN$>Xf*0H6L~;c zmgT_vT6RS@A)0W5SVR;PLRd;Et5&OxWV2bZv9ZxLP&S(tj^m67A@W3Y!ORy*sY?JC z?)cHouU$rW$H&J%PESv3Gcz-=Z5xS10`v3p2nK_U|CHRz%S)V`oZ$5I6lZ5=I6ptX zEtkum0eot=^Ae(gqgt)@Y&!wq9e_pPo+{kIXQXrSVGiIiWic}WQ#yD znPdP9k0Hffn~|nz(M|%IrbRK}X5`t9A0W`gK-%$=uIo%hC3HMUdLRJ&evp)OU1tDG z4<+(?6CPD6m84SY8-Tix0=_Gy-c~A=_W+Lk5V*U)zyInZaXae0#CS3(ekSltV2~J% z$K$Qe6pzOn07jph#FlBAccy9H0oZ!#2_oKqf<@}!|b00000NkvXXu0mjf DoLqog literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/white/.12_locator_prefab_semaphore.png b/addon/io_scs_tools/ui/icons/white/.12_locator_prefab_semaphore.png new file mode 100644 index 0000000000000000000000000000000000000000..0eb003f9d6feb79ad5afb5b922ca018f6332dd6e GIT binary patch literal 1069 zcmV+|1k(G7P)q=QT24&YQs}rJNK(TnHi5H_nBWauV=)b?P+&KL{cI z=7E-d;>8^?O%?L)lu{q~x>~#wXlQ6?1qcKJY;JC{wY3F6I-Mq$%aP0F&@_#Q zhldwu^2MEHS@?Xu5+C1!znsy5uInhJkWwOqU}Iy$jl{#l1Ga5*e}C^jZx{x)ZF_O; zjew(cDJ4=$c6WF2`~5_tQ2=gkZqPK1#>PgZlrG_#5pc>)2!WIm+qOxiQXCx}5ekI} z27`n`A=h3k7Hb=UVzKC^z{{~L3)3`fN5FCDXdn~{vAn$O+OMy#Sz20h?bW29QW*j- zqOvT@eZ$w619WtBFg`wB5&>P;JrSr%1Le@x*5+!!FpS!!z;WnYP!82rf#(z$hT(?z z`1ts%XEK>gNh9)_0#5{VU3WDggdmYfaCUa)+HY@fSy@?e?T%)=pz11cL^d-sQ_=&1 z!61P^;B6X_(^?%3RQj!XA9(#OaK8ET^YgAflgW@wCS7|q-vUn>cr7X+M9JjgE-TZrfCQvE~v08RGbNwQi1#X`=4X6 zm^rX>f4RA%$t0 zC40f#+#H>qolH+pv#_wh)zy`!vAn9qBpeQZjK||dA`$$4e+h|VvB=%s9jB+KJU%{p nUQDXNyRK&c#=9)1Z-RdSyM7OEsMkeR00000NkvXXu0mjf$wcjQ literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/white/.13_locator_prefab_navigation.png b/addon/io_scs_tools/ui/icons/white/.13_locator_prefab_navigation.png new file mode 100644 index 0000000000000000000000000000000000000000..210f61fc870b484bf6b451a999759a6ba0b4a408 GIT binary patch literal 1124 zcmV-q1e^PbP)oaV^V#jwnJA zD~%gn)rBydVx8h5BW?s?sAy^^=q|VkQftA&(5c$Ng;r*W&_%M$&Lkw^4>%JuljPkl z)ZZi~PGWRcJ@Dc3-hKC+``*3(@fhwG41n#@ehq-`nJ=H5F}5YkGRd+`jIkX6x=$(K z3t5&a91c@B9H##MeqxOM@hL^90u)8LcM%c*3xF>HI4{33#@Oq=zCKk%Bod*ap&5q;Iv)YRC~(SgRsM#SUsJg2U%F3ir(LaWsxo6TZwZVqc}YY`zt z^KAu402cLn{r6K-Q_|Sj7~0$0D+=IrIp1d_Dfxs3pJOkFBjOBoYZ^v)LPuAe~NQetsVN`}+|9 z-a`H%qtW=@?RL}h@-lJG0|oO|yV=M;X8GvzAScKQeS65fb=kt-< z?Y<^}bMBp)nIT0{NKq6TA0H8ewMj8M}r_)cTr>CE}TrOrX7!Z%gv9z>=L?ZFFwzgK+*4CyO z85x1yZdVtkQYip=lo{?5JG$h;LmcB1WFg71Yn@9t}fyAdes7SI2;6ExG)Ik zJV2MQSS;@W4BbS^bL{8QXmn&@Vc|nG8if$zYCH?zMKYQE>EPg?+++X%fk2?vVzK=8 zF%fytXf%&F=Yh-YALdMbJvcb1?m%yEFLBN{B}sav*Xtv)EGtH%Q8AfJZ|!zF<>JAE z2ZD3{q$2f2Fx=DAqY7|3owU8ZP31zX)uxICt>`YuITmbgZdRGk1^CBZN+X#}Vsdg4 zt*xz@=H})%CX?xn*=&B--QAt%*V)-g>2&&ORZ0O2I~)%62+q#VsJ^~F0bl?ZodO`g z3(Dz)+g!&0fR&XMoS&b&0lX;XW3^g81cSj50MGA4Kut}JYM-2(KnU?qp^p&au_Q@l z|A;CQkj-XQI~I!pfGFbjQV3x!H)cgenA-)pb~UJ~3;q{y7pSBNBcV{}>KaBU6as+A qeTXWB!pG(os(;v29N^BM+y4TMeKrG>QWci~000083#20 literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/white/.14_locator_prefab_map.png b/addon/io_scs_tools/ui/icons/white/.14_locator_prefab_map.png new file mode 100644 index 0000000000000000000000000000000000000000..6076e83deb0041a1ad052ce20d57eb9a8de20c70 GIT binary patch literal 1732 zcmV;#20QtQP)zR4@r%p|4}(deQ@ zSZD~N3qK~An6Po88^>svn2An|W+q|c#+XbD37c^+j)@D=nBqvrk17O=EFh9Mh?ziX z0~C;!+lSlUXBJLt%ZKvLI3H)#K0VL*|L%Ftc}_37( zFqKNps;b&+n&$n1fr07&B!J@c`C2U&%Oxr0hKh=cRAFJEWp;KpJw85eRaI3t4CA+N z-@g3=Aip($Qdd{^r#*Z2{QUa$>i~?7jw&PW zSKa6H{d(WNeZR_x3n7@9nIV}>l1wH6sH&>M;c)Q&{rjJml$0bQkx1yPPqj|*VvEJ{ z_uk%K2S6ke;n}li+`W4@H90w{l$4an{rmS@D=I2TBog%W^e{3qvKS7Bt0pEUX1)|# zT@Qe&s)wbN8vxSjG%sGfpslTqa5(%oA;fRO;qbH0&Q3-~Mi4@JO|{w4rY%1y<^#a1B%LI@@&CzWI}*`4c?PN%!)=jRo}Fj!hz!e+Bs0QWZmP*pX) zxVV@WLSPsMrKP1()3nAM7e!H&#(;G*vDs|yO`A5k3JMBb1qB5zo6Y8!o14Sy_2O_i*tl^cZnv9AB%E+qcu(+spj?e6|~I-@cuVedERr zUcY{g#bWusySw{`%z9o8NGW#!csw2yMZxKG;&!`nxm+ASew#2kx~?-jJ4;hj6D1`jIGs)uMZx3o0ALu#j$F;qMhbQmGUtPoB)QBSl3;Y~H*%Ph+hNcmcd#FPf&Y+^$}| zN`HSp;cz$)A{S(8YKp^$594yVvimem!|U~Cj?}v*fTAcntX8Y6s;XKc4iE?gsHv&p z>C>kx0E7^XkB?&*1{W`0T(P&hx*Dt1D!-Hg3$U%ax;k5!&!0c%=+UF>-o2ZKhEJW@ z(a}LP8fAERIIj*8i3A@%e&p=gv#Y`bR903hilWqz(|uWh`uh6s0DDzc6%QUf0Kn~b zbMM|gMn^{pg+33q=H_Mw2M38pquKO_!(r@pJ10(@SXDe64l^_~1YmDyXxO>}AkzU4 zA3mhHxtVwG-ceIigK3&fPfwFbBmih@Yva$g(;B!0-3}IS>fQ*49?J9y@U0fQ-drvbMHX*45R0)&KPA)AG%mH!=_i zNWb5Idj&us5EyK1Y&6%Uo@4j#-Oj~+2JG=$UXB%MyPu&_XRc{${C zf)(;-Kr)#`N{Q3yT=h0w%Z?p8mi_Epq*GpC^+^v;d^jXQ75EkySklqa5kTyU{?h5UWi$aL9>$;!XAJ6l& zF~(FX6|=UsW|o$gjOTf}lP82I1B?9(25rHC*7~_p>fTEs#0`*gT^At)#u$`Re`~GB zhOO|yoA7&pUOn&xcq@c>DW&|O%lKn}+uK{hF#IeeWXzxy!(-vB@M4Irgl%;)m}?CtH*XfzU;hhfP1`8ng`4v9U3vluS-eV%v5i^A?m+RI63A)>Nxi zc6WCh#+Xr{4qdk3`uduBy$-FHOQccOK5x1)1U-dUFQGw=vN2od{dq!*gaW_vR> wEX(>;YyGNM0Duv|{VLbqP{8f~q@4`^0H4+&rW%5@wg3PC07*qoM6N<$g3!dC3jhEB literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/white/.16_collider_box.png b/addon/io_scs_tools/ui/icons/white/.16_collider_box.png new file mode 100644 index 0000000000000000000000000000000000000000..8bcc150e92a6a227e89a9772af0f279237e811f0 GIT binary patch literal 691 zcmV;k0!;mhP)uwYx6dv=FH~6^%hjoMdLYC?ko@kCAKd#t(*bIGmjCn=?7X01rIy z#*hl+%B51NbPLITzuyC3w|ol#P*wE>z)t|Y=rT={o2Iz~AScyM3|^A^zW=?`>3nK7 zn}vkeYBd;!0mhiF>-tktPUaB^^5Jm!(r7dQ0L$evmzYwD$z%fCwlSa2VObV*UH{-X zj&qs&KCOtRX&*h$`xYfX9*-!O%emy^@ffykLzZP&mX(>GCO|d}NA(&35AcR1YuAA`8DQLIbXJ71)F$TsMD5YmV z8ZU}W0sVd-i^U>Fa(N6Q7G19xDKfZd{Lt;hrM zTXPMCLe0R3a+9H0|NtRL8goU{;^8t@pxS4ayh-fzprz- zoX%#mdShcludlD`wY4>!OeS@Ie}4tUWy!5ezK;NJ0LI71NhA{3woNb?1ii9~2?Yhz_)1)vxB0erg&fDOz7_VDm9V`F2qw6xIJ*ofme2qBPCBBewrReF|E z(%s!nKA&f6YYQL(ya(KBCXF=!o&g;Ii9~{?rY4k9oSdBCI1aw=qm-(cy^+?M(a}-! z`T%%(kx4CpCjf(kg9L*?EX(5b^b{dPsq|*{T5FV26%hyoh{a+iSN}}_9s$H+F)YhM zO3Cr@aS6clJbd4;fH6P@DjJQNT;Xd06nD4#0G*wk93CFBx3`B70wDyh>*9GHuIu9a zKECgt15irQ-rjCfwqLYkHr1#A0IuueI1UXB4Omv8XUjX^0GLV`7}Fz0>2z9OxdQ2Q z+5`3hwzs!S#qodO7%cHy(&Q?PRK{A;%x#ll2skIS(2VMg9!NCEMNTk$UR@;0N!;0T;Lb*cyn`;*49?Ky1FV#Y@SU6dY*^YnuUc0W@l$jp4Y(YO)3C| zJ^UfiySlo{?(QxyVj4DR1I2H%) l-Ie({{fkR!U3J~MegmxpX-mFvzt;c&002ovPDHLkV1m&$m45&L literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/white/.18_collider_capsule.png b/addon/io_scs_tools/ui/icons/white/.18_collider_capsule.png new file mode 100644 index 0000000000000000000000000000000000000000..6d33c59b17c3c7ce3e07720e7a5ef93ca3659961 GIT binary patch literal 685 zcmV;e0#f~nP){3L!S920NY3 zW#Ge{&_A^taP29MZGqG2Ts}TNHh=&?I2=f0&##`E=M#P#b&c{a&khi*PF?}s0xJwi9|w&IbawJ4G1B&lv1Aove_)j zWO6J?qtTe#0Dxf_q*5u(+Ehw?0T6nEQtBnZ#>NJbNMyLM+wIb7wQdF1d4s_qQcBGl zHvl{Xh{a;KTrL3G?KaJ3^VV>!cs!1j65y#G6VU(8rIZfA z2n6oLz~0{8D2p)zLWoyNsbsxgr&uhqzP`Rtc&Sw4@bFNxGD<1ndLeobJO|j`-p1$i zEd*YvRM^?s(cwGbg$}M7umcA05MXt6l~5>z+wEp)X=x^?X__1z9bp&-olZv&OW+A3 zrFfF;^vmU<52LqCIE-$T5aKcL258bhT4x3R1ik^Ug%FQ!;{3lqB$g~$`dj)9I@9KQoy+1DVMLOeyH0U=?yMB<;npM-OYT zy?Am#dMMpe4?XneUr?6zu%&zRFNhZlgFWgYRVl@TB32~?qlg*EWTL~2iLZy<7ouj@ zOqbG2zVP79`+eX0e&@a4@6EsvLk#ghLwO{w*|u%HkAUZSKG2Rk?@QnhAOgg{X}MfZ z%+Jq@d_La+%D@->gw#G`KO2Tos#dF?2_Zh%+}w=i+_Ef|mX;_K3S=@Fgb*t5Ij{=6 zxX-_T1mBL1j=rqd>vsmL)#^av=jZ2?N+pC4sH%#psx+HT*4NiLK0cPT=fJNI449_* zVXM_T5<;W^!Z5`1yn%!Pgb=K(tl+vX$z+m5B5@~PDwWvX-39P0%lgFkeHTEJgjTEd ztXM3jdfz4vPESv%*XwAS_COq9Zf;H*uzlZu2K;o#0Pu8qc^QEH{ry!MZEX#J+1Xjr=`^`qj-8zyfTz+xZxVi*&*yO*had=Ix68{*6h%Q*UoW|_v4QKl zxUP##q17ZuC7QXld%oNh1Apl_V)H7zys_`rGjaiu@{tyCxk#z6voHLIXO82U~Fs* zfS&ShywSXqgnt1~%H=W>6BDrw7=}T!*`(9yplKR~LIKb7P*s)b>1hBC4h|&!Pub}; z@Eh>NaU6=pV&77F7j#_*Ac`V1O(UI7v#_wxjdwa7SpiZDfUb&VS${`S^fh3(u1hwX zrQL21+yH_gpxthhNF=(|AhEi-%EiS6KqHk(eHDgb01vO?=fE!jQ&UsS%*Z z;D=s*zk~iA*abcYK6(r1t%#zC?d@%DZf?+ZokpW^TdUQ|z<0pQ`~1g#ND{Vfryk+F n6Q1WafzJD^F~kr<{HOQ_8{bT!Mm@h6tJP#J#~qSlxg6U~n|IbE2W z8gmmI+A;c%E74u|P|)UE)&0giz`f$ywVtK~nhYOz?X zfCmTw;=u|4o=nWYi;9YhTHrNsb-$>`;}IT@=b?}*;B_YFpTTT4*8x8O*8>9sLI@$& z*Vo1H@URF30wNlXifA+{LZOft8ygcPB_;QT#DU4Ova%0~)sPA~1_uX!ZfR+G(c9ZA zm6es{__nvVb#sIeoSvStx3|aD)fLHPlD)k>kxV9knV+BkS}#W_Bp@6PKkx7F&k9VX zQmn15726B|m6esWx3`l{r?D8Vzb$>+wGj4opE+{25=3`KaAJy0PrzDU0odjZEbB>tyVTR zHVB8q1&i`nVg)= z>xAF$Cm0OU(9nR{Y|h%XJ8+NtZP5+@T>w2jJ$VkN(}^TW2qB0@qdDv5=H_mjwfPfR zI}RZ4BI*qyC&1CsQJ&-S@)98gR;%?sDFCrp48Pw`JRZ;KfDnQ}Adtg-A9)>66y_Gi^Yh=VvLWEYpy?lFAEhZCLm@q znLYyo$z+m&fdNvf6eT4k1rus(Ye}cm%+1Ydrl8Si{FJ*XsEdNnF+e;XKT;Is3sqH@ z4-XFw0Fg+f;55{Z=H%oAAf+hE=ZQq(t#0r;;-%Z|7Sq$yLYC!+pI|x5<#GwT-7bKa zk0CDf7(K-A_4W0ighHVPpU+qDD+)l8B%!M6n@A+`;*s1>c*6U|U+YK*n={f!9smFU M07*qoM6N<$g4)IfumAu6 literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/white/.21_scs_root_object.png b/addon/io_scs_tools/ui/icons/white/.21_scs_root_object.png new file mode 100644 index 0000000000000000000000000000000000000000..c76146eb7a0de93e1b1d26bc2f58de891e3ece5a GIT binary patch literal 882 zcmV-&1C9KNP);y13{f7!aDjpkT1zq;3`$CA^`^3;62=Uwsmv58vsKm>Nmam`1z! zfeVj&&pqdR5BD9;g**NYPj=ioK42A)>{!jt-5M)XQ&S=$vcA49PN!3VmD>v54#;Je zmzOUx(SBWLH5CYG1lZf#%T0K_-n_M&768EJ=4S3&RnRApU)@2R<(ma26k0dEh)e>O_`gU6JQtUEXQ%Vm#_A#M za~t>qL;$l?27JJG;ML&Zpksc19=F?_XD~A}LpU6!zP=up%Z0A%EG;cDIy#D?DD?LB z=4=}p8WSHo}Q+;xfuYL%Z1nLC6P!lFfag_{_$CX%;&dSx~_|+X|lGqCXq~XU zs;cz#^jvI;1EmW=hBjac5Wn9qiA17o0g&lzB^HZGEEbcxx;g>Uz&i?G*kU+<_dvR~ zwpOCi=;h!gyXUL@{u@w%6QPZq&Ewz%9Sf@pou$o?y&Rc z&;JEL3V`CHM~`y;*qSwKXaKYRX~8G}X#ny8WB^D7ppJ};tgo)F7DQ1L0y|AhOS=HT zboT67h7dw-+_=F2s0HvA01JQ>lIAlZ2auDKlha}_81hq6QlL~S!Sg&GK70t8rZGA? z>brXNs@UD#je>##3ILLmlOr1&8yA+AmiCQ|j6?uHOG}G_qA10_efz#Q7z~`t<%&Ff z_%Q8oI6ee$AgqIV^XBz148xPZ2~iY@N~P+ksi`4^5K>%RJgU)XI)ZsEEiGJhbo6Io z4e0fHw-`KaFMzhTHXlV%+ZHcg>?VW|tyX&hz=pcII+11BDGjx@wsHWh;XEwO%F4Q2 zR#x_~yu6$?nM|U^Vj*_Bot!##isN}+2&ygC)YJ$RMO9;BNtIVrRPX>kko4Zn%*-6< z?d_d1ycHD{tTbaUgjfJ?BqSvK;c~f{K%pHwcJKh+3F2!pb>JdnVq%)lojd3A=M4@H zG5|h)Q3Iugg@yNqhK8neap}?}RxX$ClsIw#HlxvazpAQ=n9XJ(J3G4{fL@Brv|8-| z$8nR#q1Wr(&nq0SR4Q8=8X5#a5T?z=>eZ{C_&be@i~F*pqhpFepU=me&1N3J5=n3E z$&)81_r&(?+X;ZAaK+Pdb8~;cdGjV4T$qrF9Um1H^|i#!tE;OEIk-H}3;Fr^w*nR z(v(W&A?bqI84(fjMMg%(j{sIIU%vdlAPB*)D7k+9I+Ku)&=egVZJjxDW;=jSZ8n=& zSy{;gSR2Z)a?6%2A|Zs_x^*k?q5%N4LZR5Ze*JpJ;c&1|o;(q*UAsmoifRC`#Aq~* z_Vx9Ra~vl;e*Bm{bm$PL(P+8=#4K2_;Ae(m#OCH^J|`#VHh^k`8$g9ZVKW#EgDREk zD@=R^0l?cuMMaMShtlD2a7jr?rvb!E92J1~BO)RW0{8^LssPz{ckS9G5Hhj0FJ8RJ zS}Yb};lhQ#O2fQ5+z|r6I-AWl=-AP%g<=F+Mi-#VwwQF9wwIpU<9yNCX@Zp z*4D-a9}~;6d~+jT<*kNg5a!U}IxrzumNH6Hf>sUa!|@x7$fpR#q>7 zj{sBv*Z`n@(V|5|O-)S#P1D@*h7@^4_%S8jYs&>C>l^26&zq zbUNKF05eS{(^#;`(9qC$cX#)APfyR7$K#o@wmUmJSpb^>tXjHs>3yfu$@+cJG|i=_ zr=Oo@pscLyQJ_d;W1|3|SPE~ht*s5&O)&1>y~}22XI}#lE#<}nuw-OpIE_Z*@Z7m` zodD)eGoV(hTb)iPO9&x-eSJ)Fa`K5FN{L>tzuVs4&IMn){_{O}@E|{X_U!X`#@h8G z)Hi=5fcH{TQkHu>o^Akp0E`836aZGm#l@}3%gb93A0M9xK=gXOqb`^0GELLp05}bR fJkP)@{O9-!cvHFYPh~9700000NkvXXu0mjf;?WRB literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/icons/white/.icon_scs_bt_logo.png b/addon/io_scs_tools/ui/icons/white/.icon_scs_bt_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7140591d7c6455fb58e3fcff7aa35fa38824810c GIT binary patch literal 1685 zcmV;G25R|@w%6QPZq&Ewz%9Sf@pou$o?y&Rc z&;JEL3V`CHM~`y;*qSwKXaKYRX~8G}X#ny8WB^D7ppJ};tgo)F7DQ1L0y|AhOS=HT zboT67h7dw-+_=F2s0HvA01JQ>lIAlZ2auDKlha}_81hq6QlL~S!Sg&GK70t8rZGA? z>brXNs@UD#je>##3ILLmlOr1&8yA+AmiCQ|j6?uHOG}G_qA10_efz#Q7z~`t<%&Ff z_%Q8oI6ee$AgqIV^XBz148xPZ2~iY@N~P+ksi`4^5K>%RJgU)XI)ZsEEiGJhbo6Io z4e0fHw-`KaFMzhTHXlV%+ZHcg>?VW|tyX&hz=pcII+11BDGjx@wsHWh;XEwO%F4Q2 zR#x_~yu6$?nM|U^Vj*_Bot!##isN}+2&ygC)YJ$RMO9;BNtIVrRPX>kko4Zn%*-6< z?d_d1ycHD{tTbaUgjfJ?BqSvK;c~f{K%pHwcJKh+3F2!pb>JdnVq%)lojd3A=M4@H zG5|h)Q3Iugg@yNqhK8neap}?}RxX$ClsIw#HlxvazpAQ=n9XJ(J3G4{fL@Brv|8-| z$8nR#q1Wr(&nq0SR4Q8=8X5#a5T?z=>eZ{C_&be@i~F*pqhpFepU=me&1N3J5=n3E z$&)81_r&(?+X;ZAaK+Pdb8~;cdGjV4T$qrF9Um1H^|i#!tE;OEIk-H}3;Fr^w*nR z(v(W&A?bqI84(fjMMg%(j{sIIU%vdlAPB*)D7k+9I+Ku)&=egVZJjxDW;=jSZ8n=& zSy{;gSR2Z)a?6%2A|Zs_x^*k?q5%N4LZR5Ze*JpJ;c&1|o;(q*UAsmoifRC`#Aq~* z_Vx9Ra~vl;e*Bm{bm$PL(P+8=#4K2_;Ae(m#OCH^J|`#VHh^k`8$g9ZVKW#EgDREk zD@=R^0l?cuMMaMShtlD2a7jr?rvb!E92J1~BO)RW0{8^LssPz{ckS9G5Hhj0FJ8RJ zS}Yb};lhQ#O2fQ5+z|r6I-AWl=-AP%g<=F+Mi-#VwwQF9wwIpU<9yNCX@Zp z*4D-a9}~;6d~+jT<*kNg5a!U}IxrzumNH6Hf>sUa!|@x7$fpR#q>7 zj{sBv*Z`n@(V|5|O-)S#P1D@*h7@^4_%S8jYs&>C>l^26&zq zbUNKF05eS{(^#;`(9qC$cX#)APfyR7$K#o@wmUmJSpb^>tXjHs>3yfu$@+cJG|i=_ zr=Oo@pscLyQJ_d;W1|3|SPE~ht*s5&O)&1>y~}22XI}#lE#<}nuw-OpIE_Z*@Z7m` zodD)eGoV(hTb)iPO9&x-eSJ)Fa`K5FN{L>tzuVs4&IMn){_{O}@E|{X_U!X`#@h8G z)Hi=5fcH{TQkHu>o^Akp0E`836aZGm#l@}3%gb93A0M9xK=gXOqb`^0GELLp05}bR fJkP)@{O9-!cvHFYPh~9700000NkvXXu0mjf;?WRB literal 0 HcmV?d00001 diff --git a/addon/io_scs_tools/ui/material.py b/addon/io_scs_tools/ui/material.py index 64db22f..a8ece5e 100644 --- a/addon/io_scs_tools/ui/material.py +++ b/addon/io_scs_tools/ui/material.py @@ -16,685 +16,757 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy import os +from bpy.types import Panel from io_scs_tools.consts import Mesh as _MESH_consts from io_scs_tools.internals import shader_presets as _shader_presets +from io_scs_tools.utils import material as _material_utils from io_scs_tools.utils import object as _object_utils from io_scs_tools.utils import path as _path_utils from io_scs_tools.utils import get_scs_globals as _get_scs_globals +from io_scs_tools.utils import get_scs_inventories as _get_scs_inventories from io_scs_tools.ui import shared as _shared +_UI_SPLIT_PERC = 0.5 -class _MaterialPanelBlDefs(_shared.HeaderIconPanel): + +class _MaterialPanelBlDefs: bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" + bl_context = "material" + bl_ui_units_x = 15 + @classmethod + def poll(cls, context): + return hasattr(context, "active_object") and context.active_object and context.active_object.active_material -def _draw_shader_presets(layout, scs_props, scs_globals, is_imported_shader=False): - """Creates Shader Presets sub-panel.""" - layout_box = layout.box() - layout_box.enabled = not is_imported_shader - if scs_props.shader_presets_expand: - panel_header = layout_box.split(percentage=0.5) - panel_header_1 = panel_header.row() - panel_header_1.prop(scs_props, 'shader_presets_expand', text="Shader Presets:", icon='TRIA_DOWN', icon_only=True, emboss=False) - panel_header_2 = panel_header.row(align=True) - panel_header_2.alignment = 'RIGHT' - panel_header_2a = panel_header_2.row() - panel_header_2a.prop(scs_globals, 'shader_preset_list_sorted', text='', icon='SORTALPHA', expand=True, toggle=True) - - layout_box_row = layout_box.row(align=True) - if _path_utils.is_valid_shader_presets_library_path(): - layout_box_row.alert = False + def get_layout(self): + """Returns layout depending where it's drawn into. If popover create extra box to make it distinguisable between different sub-panels.""" + if self.is_popover: + layout = self.layout.box().column() else: - layout_box_row.alert = True - layout_box_row.prop(scs_globals, 'shader_presets_filepath', text='', icon='FILE_TEXT') - layout_box_row.operator('scene.select_shader_presets_filepath', text='', icon='FILESEL') + layout = self.layout + + return layout + - layout_box_row = layout_box.column() - layout_box_row.prop(scs_globals, 'shader_preset_list', expand=True, toggle=True) - else: - layout_box_row = layout_box.row().split(percentage=0.4) - layout_box_row.prop(scs_props, 'shader_presets_expand', text="Shader Presets:", icon='TRIA_RIGHT', icon_only=True, emboss=False) +class SCS_TOOLS_UL_MaterialCustomMappingSlot(bpy.types.UIList): + """ + Draw custom tex coord item slot within Custom Mapping list + """ - column = layout_box_row.column(align=True) + def draw_item(self, context, layout, data, item, icon, active_data, active_property, index): + line = layout.split(factor=0.4, align=False) + line.prop(item, "name", text="", emboss=False, icon_value=icon) - row = column.row(align=True) - if is_imported_shader: - row.prop(bpy.context.material.scs_props, "active_shader_preset_name", icon="COLOR", text="") + if item.value == "" or item.value not in bpy.context.active_object.data.uv_layers: + icon = 'ERROR' else: - row.prop(scs_globals, 'shader_preset_list', text='') - row.operator("material.scs_search_shader_preset", text="", icon="VIEWZOOM") + icon = 'GROUP_UVS' + + line.prop_search( + data=item, + property="value", + search_data=bpy.context.active_object.data, + search_property='uv_layers', + text="", + icon=icon, + ) + + +class SCS_TOOLS_PT_Material(_shared.HeaderIconPanel, _MaterialPanelBlDefs, Panel): + """Creates a Panel in the Material properties window""" + bl_label = "SCS Material" + + @staticmethod + def draw_shader_presets(layout, scs_props, scs_globals, scs_inventories, is_imported_shader=False): + """Creates Shader Presets sub-panel.""" + layout_box = layout.box() + layout_box.enabled = not is_imported_shader + if scs_props.shader_presets_expand: + panel_header = layout_box.split(factor=0.5) + panel_header_1 = panel_header.row() + panel_header_1.prop(scs_props, 'shader_presets_expand', text="Shader Presets:", icon='TRIA_DOWN', icon_only=True, emboss=False) + panel_header_2 = panel_header.row(align=True) + panel_header_2.alignment = 'RIGHT' + panel_header_2a = panel_header_2.row() + panel_header_2a.prop(scs_props, 'shader_presets_sorted', text="", icon='SORTALPHA', expand=True, toggle=True) + + layout_box_row = layout_box.row(align=True) + if _path_utils.is_valid_shader_presets_library_path(): + layout_box_row.alert = False + else: + layout_box_row.alert = True + layout_box_row.prop(scs_globals, 'shader_presets_filepath', text="", icon='FILE_TEXT') + layout_box_row.operator('scene.scs_tools_select_shader_presets_path', text="", icon='FILEBROWSER') + layout_box_row = layout_box.column() + layout_box_row.prop(scs_inventories, 'shader_presets', expand=True, toggle=True) + else: + layout_box_row = layout_box.row().split(factor=0.4) + layout_box_row.prop(scs_props, 'shader_presets_expand', text="Shader Presets:", icon='TRIA_RIGHT', icon_only=True, emboss=False) -def _draw_shader_flavors(layout, mat): - """Draws shader flavors if any. + column = layout_box_row.column(align=True) - :param layout: layout to draw on - :type layout: bpy.types.UILayout - :param mat: material for which flavors should be drawn - :type mat: bpy.types.Material - """ + row = column.row(align=True) + if is_imported_shader: + row.prop(bpy.context.material.scs_props, "active_shader_preset_name", icon='COLOR', text="") + else: + row.prop(scs_inventories, 'shader_presets', text="") + row.operator("material.scs_tools_search_shader_preset", text="", icon='VIEWZOOM') - if mat.scs_props.active_shader_preset_name == "": - return + @staticmethod + def draw_flavors(layout, mat): + """Draws shader flavors if any. - preset = _shader_presets.get_preset(mat.scs_props.active_shader_preset_name) - if preset: + :param layout: layout to draw on + :type layout: bpy.types.UILayout + :param mat: material for which flavors should be drawn + :type mat: bpy.types.Material + """ - # if there is no flavors in found preset, - # then we don't have to draw anything so exit drawing shader flavors right away - if len(preset.flavors) <= 0: + if mat.scs_props.active_shader_preset_name == "": return - # strip of base effect name, to avoid any flavor matching in base effect name - effect_flavor_part = mat.scs_props.mat_effect_name[len(preset.effect):] + preset = _shader_presets.get_preset(mat.scs_props.active_shader_preset_name) + if preset: - column = layout.column(align=True) - column.alignment = "LEFT" + # if there is no flavors in found preset, + # then we don't have to draw anything so exit drawing shader flavors right away + if len(preset.flavors) <= 0: + return - # draw switching operator for each flavor (if there is more variants draw operator for each variant) - enabled_flavors = {} # store enabled flavors, for later analyzing which rows should be disabled - """:type: dict[int, io_scs_tools.internals.shader_presets.ui_shader_preset_item.FlavorVariant]""" - flavor_rows = [] # store row UI layout of flavor operators, for later analyzing which rows should be disabled - for i, flavor in enumerate(preset.flavors): + # strip of base effect name, to avoid any flavor matching in base effect name + effect_flavor_part = mat.scs_props.mat_effect_name[len(preset.effect):] - row = column.row(align=True) - flavor_rows.append(row) + column = layout.column(align=True) + column.alignment = "LEFT" - for flavor_variant in flavor.variants: + # draw switching operator for each flavor (if there is more variants draw operator for each variant) + enabled_flavors = {} # store enabled flavors, for later analyzing which rows should be disabled + """:type: dict[int, io_scs_tools.internals.shader_presets.ui_shader_preset_item.FlavorVariant]""" + flavor_rows = [] # store row UI layout of flavor operators, for later analyzing which rows should be disabled + for i, flavor in enumerate(preset.flavors): - is_in_middle = "." + flavor_variant.suffix + "." in effect_flavor_part - is_on_end = effect_flavor_part.endswith("." + flavor_variant.suffix) - flavor_enabled = is_in_middle or is_on_end + row = column.row(align=True) + flavor_rows.append(row) - icon = "FILE_TICK" if flavor_enabled else "X" - props = row.operator("material.scs_switch_flavor", text=flavor_variant.suffix, icon=icon) - props.flavor_name = flavor_variant.suffix - props.flavor_enabled = flavor_enabled + for flavor_variant in flavor.variants: - if flavor_enabled: - enabled_flavors[i] = flavor_variant + is_in_middle = "." + flavor_variant.suffix + "." in effect_flavor_part + is_on_end = effect_flavor_part.endswith("." + flavor_variant.suffix) + flavor_enabled = is_in_middle or is_on_end - # now as we drawn the flavors and we know which ones are enabled, - # search the ones that are not compatible with currently enabled flavors and disable them in UI! - for i, flavor in enumerate(preset.flavors): + icon = _shared.get_on_off_icon(flavor_enabled) + props = row.operator("material.scs_tools_switch_flavor", text=flavor_variant.suffix, icon=icon, depress=flavor_enabled) + props.flavor_name = flavor_variant.suffix + props.flavor_enabled = flavor_enabled - # enabled flavors have to stay enabled so skip them - if i in enabled_flavors: - continue + if flavor_enabled: + enabled_flavors[i] = flavor_variant - for flavor_variant in flavor.variants: + # now as we drawn the flavors and we know which ones are enabled, + # search the ones that are not compatible with currently enabled flavors and disable them in UI! + for i, flavor in enumerate(preset.flavors): - # 1. construct proposed new flavor string: - # combine strings of enabled flavors and current flavor variant - new_flavor_str = "" - curr_flavor_added = False - for enabled_i in enabled_flavors.keys(): + # enabled flavors have to stay enabled so skip them + if i in enabled_flavors: + continue - if i < enabled_i and not curr_flavor_added: - new_flavor_str += "." + flavor_variant.suffix - curr_flavor_added = True + for flavor_variant in flavor.variants: - new_flavor_str += "." + enabled_flavors[enabled_i].suffix + # 1. construct proposed new flavor string: + # combine strings of enabled flavors and current flavor variant + new_flavor_str = "" + curr_flavor_added = False + for enabled_i in enabled_flavors.keys(): - if not curr_flavor_added: - new_flavor_str += "." + flavor_variant.suffix + if i < enabled_i and not curr_flavor_added: + new_flavor_str += "." + flavor_variant.suffix + curr_flavor_added = True - # 2. check if proposed new flavor combination exists in cache: - # if not then row on current flavor index has to be disabled - if not _shader_presets.has_section(preset.name, new_flavor_str): - flavor_rows[i].enabled = False - break + new_flavor_str += "." + enabled_flavors[enabled_i].suffix + if not curr_flavor_added: + new_flavor_str += "." + flavor_variant.suffix -def _draw_shader_attribute(layout, mat, split_perc, attribute): - """Draws one material attribute. + # 2. check if proposed new flavor combination exists in cache: + # if not then row on current flavor index has to be disabled + if not _shader_presets.has_section(preset.name, new_flavor_str): + flavor_rows[i].enabled = False + break + + @staticmethod + def draw_parameters(layout, mat, scs_inventories, split_perc, is_imported_shader=False): + """Creates Shader Parameters sub-panel.""" + shader_data = mat.get("scs_shader_attributes", {}) + # print(' shader_data: %s' % str(shader_data)) + + if len(shader_data) == 0: + info_box = layout.column() + info_box.label(text="Select a shader from the preset list.", icon='ERROR') + else: - :param layout: layout to draw attribute to - :type layout: bpy.types.UILayout - :param mat: material from which data should be displayed - :type mat: bpy.types.Material - :param split_perc: split percentage for attribute name/value - :type split_perc: float - :param attribute: attribute data - :type attribute: dict - """ + # MISSING VERTEX COLOR + active_vcolors = bpy.context.active_object.data.vertex_colors + is_valid_vcolor = _MESH_consts.default_vcol in active_vcolors + is_valid_vcolor_a = _MESH_consts.default_vcol + _MESH_consts.vcol_a_suffix in active_vcolors + if not is_valid_vcolor or not is_valid_vcolor_a: + + vcol_missing_box = layout.box() + + title_row = vcol_missing_box.row(align=True) + title_row.label(text="Vertex color layer(s) missing!", icon='ERROR') + + col = vcol_missing_box.column(align=True) + + info_msg = "Currently active object is missing vertex color layers with names:\n" + if not is_valid_vcolor: + info_msg += "-> '" + _MESH_consts.default_vcol + "'\n" + + if not is_valid_vcolor_a: + info_msg += "-> '" + _MESH_consts.default_vcol + _MESH_consts.vcol_a_suffix + "'\n" + + info_msg += "You can use 'Add Vertex Colors To (Active/All)' button to add needed layers or add layers manually." + _shared.draw_warning_operator(col, "Vertex Colors Missing", info_msg, text="More Info", icon='INFO') + + col.operator("mesh.scs_tools_add_vertex_colors_to_active") + col.operator("mesh.scs_tools_add_vertex_colors_to_all") + + global_mat_attr = layout.column(align=True) + # global_mat_attr.alignment = 'RIGHT' + global_mat_attr.enabled = not is_imported_shader + + # PRESET INFO + preset_info_row = global_mat_attr.row(align=True) + preset_info_space = preset_info_row.split(factor=split_perc, align=True) + preset_info_space.alignment = 'RIGHT' + preset_info_space.label(text="Effect") + preset_info_space.alignment = 'LEFT' + preset_info_space.prop(mat.scs_props, "mat_effect_name", emboss=False, text="") + + # MATERIAL ALIASING + alias_row = global_mat_attr.row() + alias_row = alias_row.split(factor=split_perc) + alias_row.alignment = 'RIGHT' + alias_row.label(text="Aliasing") + + alias_row = alias_row.row(align=True) + alias_text = "Enabled" if mat.scs_props.enable_aliasing else "Disabled" + alias_icon = _shared.get_on_off_icon(mat.scs_props.enable_aliasing) + alias_row.prop(mat.scs_props, "enable_aliasing", icon=alias_icon, text=alias_text, toggle=True) + + normalized_base_tex_path = mat.scs_props.shader_texture_base.replace("\\", "/") + is_aliasing_path = ("/material/road" in normalized_base_tex_path or + "/material/terrain" in normalized_base_tex_path or + "/material/custom" in normalized_base_tex_path) + + is_aliasable = ('textures' in shader_data and + ( + len(shader_data["textures"]) == 1 or + (len(shader_data["textures"]) == 2 and "dif.spec.weight.mult2" in mat.scs_props.mat_effect_name) + )) + + if mat.scs_props.enable_aliasing: + + if not (is_aliasing_path and is_aliasable): + + aliasing_info_msg = str("Material aliasing will work only for materials which 'Base' texture\n" + "is loaded from this directories and their subdirectories:\n" + "-> '/material/road'\n" + "-> '/material/terrain'\n" + "-> '/material/custom'\n" + "Additional requirement for aliasing is also single texture material or\n" + "exceptionally multi texture material of 'dif.spec.weight.mult2' family.\n\n" + "Currently aliasing can not be done because:") + + if not is_aliasing_path: + aliasing_info_msg += "\n-> Your 'Base' texture doesn't point to any of this (sub)directories." + if not is_aliasable: + aliasing_info_msg += "\n-> Current shader type use multiple textures or it's not 'dif.spec.weight.mult2' family type." + + _shared.draw_warning_operator(alias_row, "Aliasing Info", aliasing_info_msg, icon='INFO') + + alias_op_col = alias_row.column(align=True) + alias_op_col.enabled = is_aliasing_path and is_aliasable + alias_op_col.operator('material.scs_tools_load_aliased_material', icon='IMPORT', text="") + + # MATERIAL SUBSTANCE + substance_row = global_mat_attr.split(factor=split_perc) + tag_layout = substance_row.row() + tag_layout.alignment = 'RIGHT' + tag_layout.label(text="Substance") + tag_value = substance_row.row(align=True) + tag_value.prop_search(mat.scs_props, 'substance', scs_inventories, 'matsubs', icon='NONE', text="") + props = tag_value.operator("material.scs_tools_material_item_extras", text="", icon="LAYER_USED") + props.property_str = "substance" + + if len(shader_data['attributes']) == 0 and len(shader_data['textures']) == 0: + if shader_data['effect'].endswith('mlaaweight'): + layout.label(text="'Multi Level Anti-Aliasing' shader has no parameters.", icon='INFO') + else: + layout.label(text="No shader parameters!", icon='INFO') - linked_vals = False # DEBUG: Side by side display of values - - tag = attribute.get('Tag', "") - frendly_tag = attribute.get('FriendlyTag', None) - attribute_label = frendly_tag + ":" if frendly_tag else str(tag.replace('_', ' ').title() + ":") - hide_state = attribute.get('Hide', None) - lock_state = attribute.get('Lock', None) - preview_only_state = attribute.get('PreviewOnly', None) - - # ignore substance from attributes because it's drawn before already - if tag.lower() == "substance": - return - - if hide_state == 'True': - return - - attr_row = layout.row(align=True) - item_space = attr_row.split(percentage=split_perc, align=True) - - label_icon = 'ERROR' - if lock_state == 'True': - item_space.enabled = False - attribute_label = str(attribute_label[:-1] + " (locked):") - label_icon = 'LOCKED' - - tag_layout = item_space.row() - tag_layout.label(attribute_label) - - # create info operator for preview only attributes - if preview_only_state == "True": - _shared.draw_warning_operator(tag_layout, - "Preview Attribute Info", - "This attribute is used for preview only.\n" - "It won't be exported so it doesn't matter what value is used.", - icon="INFO") - - ''' - shader_attribute_id = str("shader_attribute_" + tag) - if shader_attribute_id in mat.scs_props: - item_space.prop(mat.scs_props, shader_attribute_id, text='') - else: - print(' %r is NOT defined in SCS Blender Tools...!' % shader_attribute_id) - ''' - - item_space = item_space.split(percentage=1 / (1 - split_perc + 0.000001) * 0.1, align=True) - props = item_space.operator("material.scs_looks_wt", text="WT") - props.property_str = "shader_attribute_" + tag - - if tag == 'diffuse': - item_space.prop(mat.scs_props, 'shader_attribute_diffuse', text='') - if linked_vals: - item_space.prop(mat, 'diffuse_color', text='') - elif tag == 'specular': - item_space.prop(mat.scs_props, 'shader_attribute_specular', text='') - if linked_vals: - item_space.prop(mat, 'specular_color', text='') - elif tag == 'shininess': - item_space.prop(mat.scs_props, 'shader_attribute_shininess', text='') - # if linked_vals: item_space.prop(mat, 'specular_intensity', text='') - if linked_vals: - item_space.prop(mat, 'specular_hardness', text='') - elif tag == 'add_ambient': - item_space.prop(mat.scs_props, 'shader_attribute_add_ambient', text='') - if linked_vals: - item_space.prop(mat, 'ambient', text='') - elif tag == 'reflection': - item_space.prop(mat.scs_props, 'shader_attribute_reflection', text='') - if linked_vals: - item_space.prop(mat.raytrace_mirror, 'reflect_factor', text='') - elif tag == 'reflection2': - item_space.prop(mat.scs_props, 'shader_attribute_reflection2', text='') - elif tag == 'shadow_bias': - item_space.prop(mat.scs_props, 'shader_attribute_shadow_bias', text='') - if linked_vals: - item_space.prop(mat, 'shadow_buffer_bias', text='') - elif tag == 'env_factor': - item_space.prop(mat.scs_props, 'shader_attribute_env_factor', text='') - elif tag == 'fresnel': - item_space.column().prop(mat.scs_props, 'shader_attribute_fresnel', text='') - elif tag == 'tint': - item_space.prop(mat.scs_props, 'shader_attribute_tint', text='') - elif tag == 'tint_opacity': - item_space.prop(mat.scs_props, 'shader_attribute_tint_opacity', text='') - elif tag == 'queue_bias': - item_space.prop(mat.scs_props, 'shader_attribute_queue_bias', text='') - elif tag.startswith("aux") and hasattr(mat.scs_props, "shader_attribute_" + tag): - - col = item_space.column().column(align=True) - - auxiliary_prop = getattr(mat.scs_props, "shader_attribute_" + tag, None) - - for item in auxiliary_prop: - col.prop(item, 'value', text='') - - else: - item_space.label('Undefined Shader Attribute Type!', icon=label_icon) - - -def _draw_shader_texture(layout, mat, split_perc, texture, read_only): - """Draws texture box with it's properties. - - :param layout: layout to draw attribute to - :type layout: bpy.types.UILayout - :param mat: material from which data should be displayed - :type mat: bpy.types.Material - :param split_perc: split percentage for attribute name/value - :type split_perc: float - :param texture: texture data - :type texture: dict - :param read_only: if texture should be read only - :type read_only: bool - """ + def draw_header_preset(self, context): + layout = self.layout + layout.operator("material.scs_tools_merge_materials", text="", emboss=True, icon="FULLSCREEN_EXIT") + layout.separator(factor=0.1) - tag = texture.get('Tag', None) - hide_state = texture.get('Hide', None) - tag_id = tag.split(':') - tag_id_string = tag_id[1] - texture_type = tag_id_string[8:] - shader_texture_id = str('shader_' + tag_id_string) + def draw(self, context): + """UI draw function.""" - if hide_state == 'True': - return + # draw minimalistict info, so user knows what's going on + if not self.poll(context): + self.layout.label(text="No active material!", icon="INFO") + return - texture_box = layout.box().column() # create column for compact display with alignment + layout = self.get_layout() + workspace = context.workspace + mat = context.active_object.active_material + scs_globals = _get_scs_globals() + scs_inventories = _get_scs_inventories() - header_split = texture_box.row(align=True).split(percentage=0.5) - header_split.label(texture_type.title(), icon="TEXTURE_SHADED") + # disable pane if config is being updated + layout.enabled = not scs_globals.config_update_lock - if hasattr(mat.scs_props, shader_texture_id): + if mat: + is_imported_shader = mat.scs_props.active_shader_preset_name == "" - shader_texture = mat.scs_props.get(shader_texture_id, "") - # imported tobj boolean switch (only for imported shaders) - use_imported_tobj = getattr(mat.scs_props, shader_texture_id + "_use_imported", False) - if read_only: - row = header_split.row(align=True) - row.alignment = 'RIGHT' - row.prop(mat.scs_props, shader_texture_id + "_use_imported") + # COLOR MANAGEMENT WARNING + if not _material_utils.has_valid_color_management(context.scene): + warning_box = layout.box() + warning_box.label(text="Scene color management invalid!", icon="ERROR") + warning_box.operator("material.scs_tools_adapt_color_management", icon="COLOR") - if use_imported_tobj: + # SHADER PRESETS PANEL + SCS_TOOLS_PT_Material.draw_shader_presets(layout, workspace.scs_props, scs_globals, scs_inventories, + is_imported_shader=is_imported_shader) - texture_row = texture_box.row(align=True) - item_space = texture_row.split(percentage=split_perc, align=True) - item_space.label("TOBJ Path:") + # FLAVORS PANEL + SCS_TOOLS_PT_Material.draw_flavors(layout, mat) - item_space = item_space.split(percentage=1 / (1 - split_perc + 0.000001) * 0.1, align=True) - props = item_space.operator("material.scs_looks_wt", text="WT") - props.property_str = shader_texture_id - item_space.prop(mat.scs_props, shader_texture_id + "_imported_tobj", text="") + # PARAMETERS PANEL + SCS_TOOLS_PT_Material.draw_parameters(layout, mat, scs_inventories, _UI_SPLIT_PERC, is_imported_shader=is_imported_shader) - # disable whole texture layout if it's locked - texture_box.enabled = not mat.scs_props.get(shader_texture_id + "_locked", False) - texture_row = texture_box.row(align=True) - item_space = texture_row.split(percentage=split_perc, align=True) +class SCS_TOOLS_PT_MaterialAttributes(_MaterialPanelBlDefs, Panel): + """Draws attributes of current material in sub-panel.""" + bl_parent_id = SCS_TOOLS_PT_Material.__name__ + bl_label = "Material Attributes" - # in case of custom tobj value texture is used only for preview - if read_only and use_imported_tobj: - item_space.label("Preview Tex:") - else: - item_space.label("Texture:") + @classmethod + def poll(cls, context): + if not _MaterialPanelBlDefs.poll(context): + return False - layout_box_col = item_space.column(align=True) - layout_box_row = layout_box_col.row(align=True) + mat = context.active_object.active_material + shader_data = mat.get("scs_shader_attributes", {}) - if shader_texture: - texture_icon = 'TEXTURE' - else: - texture_icon = 'MATPLANE' + if len(shader_data) == 0: + return False - # MARK INVALID SLOTS - if _path_utils.is_valid_shader_texture_path(shader_texture): - layout_box_row.alert = False - else: - layout_box_row.alert = True - texture_icon = 'NONE' # info operator will have icon, so texture path should use none + if 'attributes' not in shader_data: + return False - # MARK EMPTY SLOTS - if shader_texture == "": - layout_box_row.alert = True + if len(shader_data["attributes"]) == 0: + return False - layout_box_row = layout_box_row.split(percentage=1 / (1 - split_perc + 0.000001) * 0.1, align=True) - props = layout_box_row.operator("material.scs_looks_wt", text="WT") - props.property_str = shader_texture_id + cls.attributes_data = shader_data['attributes'] + cls.is_imported_shader = mat.scs_props.active_shader_preset_name == "" + return True - layout_box_row = layout_box_row.row(align=True) - layout_box_row.prop(mat.scs_props, shader_texture_id, text='', icon=texture_icon) + @staticmethod + def draw_attribute_item(layout, mat, split_perc, attribute): + """Draws one material attribute. - if layout_box_row.alert: # add info operator when texture path is invalid - _shared.draw_warning_operator(layout_box_row, - title="Texture Not Found", - message="Texture with given path doesn't exists or SCS Project Base Path is not properly set!") + :param layout: layout to draw attribute to + :type layout: bpy.types.UILayout + :param mat: material from which data should be displayed + :type mat: bpy.types.Material + :param split_perc: split percentage for attribute name/value + :type split_perc: float + :param attribute: attribute data + :type attribute: dict + """ - props = layout_box_row.operator('material.scs_select_shader_texture_filepath', text='', icon='FILESEL') - props.shader_texture = shader_texture_id # DYNAMIC ID SAVE (FOR FILE REQUESTER) + linked_vals = False # DEBUG: Side by side display of values - # ADDITIONAL TEXTURE SETTINGS - if (not read_only or (read_only and not use_imported_tobj)) and texture_box.enabled: + tag = attribute.get('Tag', "") + frendly_tag = attribute.get('FriendlyTag', None) + attribute_label = frendly_tag if frendly_tag else str(tag.replace('_', ' ').title()) + hide_state = attribute.get('Hide', None) + lock_state = attribute.get('Lock', None) + preview_only_state = attribute.get('PreviewOnly', None) - tobj_filepath = _path_utils.get_tobj_path_from_shader_texture(shader_texture) + # ignore substance from attributes because it's drawn before already + if tag.lower() == "substance": + return - tobj_settings_row = layout_box_col.row(align=True) - tobj_settings_row = tobj_settings_row.split(percentage=1 / (1 - split_perc + 0.000001) * 0.1, align=True) + if hide_state == 'True': + return - if not tobj_filepath: - props = tobj_settings_row.operator("material.scs_create_tobj", icon="NEW", text="") - props.texture_type = texture_type - # creating extra column->row so it can be properly disabled as tobj doesn't exists - tobj_settings_row = tobj_settings_row.column(align=True).row(align=True) + attr_row = layout.row(align=True) + item_space = attr_row.split(factor=split_perc, align=True) + + label_icon = 'ERROR' + if lock_state == 'True': + item_space.enabled = False + attribute_label = str(attribute_label + " (locked)") + label_icon = 'LOCKED' + + tag_layout = item_space.row() + tag_layout.alignment = 'RIGHT' + tag_layout.label(text=attribute_label) + + # create info operator for preview only attributes + if preview_only_state == "True": + _shared.draw_warning_operator(tag_layout, + "Preview Attribute Info", + "This attribute is used for preview only.\n" + "It won't be exported so it doesn't matter what value is used.", + icon='INFO') + + ''' + shader_attribute_id = str("shader_attribute_" + tag) + if shader_attribute_id in mat.scs_props: + item_space.prop(mat.scs_props, shader_attribute_id, text="") + else: + print(' %r is NOT defined in SCS Blender Tools...!' % shader_attribute_id) + ''' + value_layout = item_space.row(align=True) + + if tag == 'diffuse': + value_layout.prop(mat.scs_props, 'shader_attribute_diffuse', text="") + if linked_vals: + value_layout.prop(mat, 'diffuse_color', text="") + elif tag == 'specular': + value_layout.prop(mat.scs_props, 'shader_attribute_specular', text="") + if linked_vals: + value_layout.prop(mat, 'specular_color', text="") + elif tag == 'shininess': + value_layout.prop(mat.scs_props, 'shader_attribute_shininess', text="") + # if linked_vals: value_layout.prop(mat, 'specular_intensity', text="") + if linked_vals: + value_layout.prop(mat, 'specular_hardness', text="") + elif tag == 'add_ambient': + value_layout.prop(mat.scs_props, 'shader_attribute_add_ambient', text="") + if linked_vals: + value_layout.prop(mat, 'ambient', text="") + elif tag == 'reflection': + value_layout.prop(mat.scs_props, 'shader_attribute_reflection', text="") + if linked_vals: + value_layout.prop(mat.raytrace_mirror, 'reflect_factor', text="") + elif tag == 'reflection2': + value_layout.prop(mat.scs_props, 'shader_attribute_reflection2', text="") + elif tag == 'shadow_bias': + value_layout.prop(mat.scs_props, 'shader_attribute_shadow_bias', text="") + if linked_vals: + value_layout.prop(mat, 'shadow_buffer_bias', text="") + elif tag == 'env_factor': + value_layout.prop(mat.scs_props, 'shader_attribute_env_factor', text="") + elif tag == 'fresnel': + value_layout.column().prop(mat.scs_props, 'shader_attribute_fresnel', text="") + elif tag == 'tint': + value_layout.prop(mat.scs_props, 'shader_attribute_tint', text="") + elif tag == 'tint_opacity': + value_layout.prop(mat.scs_props, 'shader_attribute_tint_opacity', text="") + elif tag == 'queue_bias': + value_layout.prop(mat.scs_props, 'shader_attribute_queue_bias', text="") + elif tag.startswith("aux") and hasattr(mat.scs_props, "shader_attribute_" + tag): + + col = value_layout.column().column(align=True) + + auxiliary_prop = getattr(mat.scs_props, "shader_attribute_" + tag, None) + + for item in auxiliary_prop: + col.prop(item, 'value', text="") - # enable settings only if tobj exists and map type of the tobj is 2d - tobj_settings_row.enabled = tobj_filepath is not None and getattr(mat.scs_props, shader_texture_id + "_map_type", "") == "2d" + else: + value_layout.label(text="Undefined Shader Attribute Type!", icon=label_icon) - if tobj_filepath: - mtime = str(os.path.getmtime(tobj_filepath)) - tobj_settings_row.alert = (mtime != getattr(mat.scs_props, shader_texture_id + "_tobj_load_time", "NOT FOUND")) - else: - tobj_settings_row.alert = True + props = value_layout.operator("material.scs_tools_material_item_extras", text="", icon="LAYER_USED", emboss=True) + props.property_str = "shader_attribute_" + tag - props = tobj_settings_row.operator("material.scs_reload_tobj", icon="LOAD_FACTORY", text="") - props.texture_type = texture_type + def draw(self, context): + mat = context.active_object.active_material + + layout = self.get_layout() + layout.enabled = not self.is_imported_shader + + attrs_column = layout.column() # create column for compact display with alignment + for attribute_key in sorted(self.attributes_data.keys()): + self.draw_attribute_item(attrs_column, mat, _UI_SPLIT_PERC, self.attributes_data[attribute_key]) + + +class SCS_TOOLS_PT_MaterialTextures(_MaterialPanelBlDefs, Panel): + """Draws textures of current material in sub-panel.""" + bl_parent_id = SCS_TOOLS_PT_Material.__name__ + bl_label = "Material Textures" + + @classmethod + def poll(cls, context): + if not _MaterialPanelBlDefs.poll(context): + return False + + mat = context.active_object.active_material + shader_data = mat.get("scs_shader_attributes", {}) + + if len(shader_data) == 0: + return False + + if 'textures' not in shader_data: + return False + + if len(shader_data["textures"]) == 0: + return False + + cls.textures_data = shader_data['textures'] + cls.is_imported_shader = mat.scs_props.active_shader_preset_name == "" + return True + + @staticmethod + def draw_texture_item(layout, mat, split_perc, texture, read_only): + """Draws texture box with it's properties. + + :param layout: layout to draw attribute to + :type layout: bpy.types.UILayout + :param mat: material from which data should be displayed + :type mat: bpy.types.Material + :param split_perc: split percentage for attribute name/value + :type split_perc: float + :param texture: texture data + :type texture: dict + :param read_only: if texture should be read only + :type read_only: bool + """ + + tag = texture.get('Tag', None) + hide_state = texture.get('Hide', None) + tag_id = tag.split(':') + tag_id_string = tag_id[1] + texture_type = tag_id_string[8:] + shader_texture_id = str('shader_' + tag_id_string) + + if hide_state == 'True': + return - tobj_settings_row.prop_menu_enum( - mat.scs_props, - str('shader_' + tag_id_string + '_settings'), - icon='SETTINGS', - ) + texture_box = layout.box().column() # create column for compact display with alignment - # UV LAYERS FOR TEXTURE - uv_mappings = getattr(mat.scs_props, "shader_" + tag_id_string + "_uv", None) + header_split = texture_box.row(align=True) + header_split.label(text=texture_type.title(), icon='TEXTURE') - if len(uv_mappings) > 0: + if hasattr(mat.scs_props, shader_texture_id): - texture_row = texture_box.row(align=True) - item_space = texture_row.split(percentage=split_perc, align=True) + shader_texture = mat.scs_props.get(shader_texture_id, "") + # imported tobj boolean switch (only for imported shaders) + use_imported_tobj = getattr(mat.scs_props, shader_texture_id + "_use_imported", False) if read_only: - item_space.label("Preview UV Map:") - else: - item_space.label("Mapping:") - layout_box_col = item_space.column(align=True) + row = header_split.row(align=True) + row.prop(mat.scs_props, shader_texture_id + "_use_imported") - for mapping in uv_mappings: + if use_imported_tobj: - item_space_row = layout_box_col.row(align=True) + texture_row = texture_box.row(align=True) + texture_split = texture_row.split(factor=split_perc, align=True) + tag_layout = texture_split.row(align=True) + tag_layout.alignment = 'RIGHT' + tag_layout.label(text="TOBJ Path") - # add info about normal map uv mapping property in case of imported shader - if read_only and tag_id_string == "texture_nmap": - preview_nmap_msg = str("Maping value for normal maps is in the case of imported shader\n" - "also used for defining uv map layer for tangent calculations!\n" - "If the uv map is not provided first entry from Mappings list above will be used!") - _shared.draw_warning_operator(item_space_row, "Mapping Info", preview_nmap_msg, icon="INFO") + tag_value = texture_split.row(align=True) + tag_value.prop(mat.scs_props, shader_texture_id + "_imported_tobj", text="") - if mapping.value and mapping.value != "" and mapping.value in bpy.context.active_object.data.uv_layers: - icon = "GROUP_UVS" - else: - icon = "ERROR" - - item_space_row.prop_search( - data=mapping, - property="value", - search_data=bpy.context.active_object.data, - search_property='uv_layers', - text="", - icon=icon, - ) - - else: - texture_box.row().label('Unsupported Shader Texture Type!', icon="ERROR") + props = tag_value.operator("material.scs_tools_material_item_extras", text="", icon="LAYER_USED") + props.property_str = shader_texture_id + "_imported_tobj" + # disable whole texture layout if it's locked + texture_box.enabled = not mat.scs_props.get(shader_texture_id + "_locked", False) -def _draw_shader_parameters(layout, mat, scs_props, scs_globals, is_imported_shader=False): - """Creates Shader Parameters sub-panel.""" - split_perc = scs_props.shader_item_split_percentage - # panel_header = layout_box.split(percentage=0.5) - # panel_header_1 = panel_header.row() - # panel_header_1.prop(scene.scs_props, 'shader_presets_expand', text="Shader Parameters:", icon='TRIA_DOWN', icon_only=True, emboss=False) - # panel_header_1.label('Shader Parameters:', icon='NONE') - - shader_data = mat.get("scs_shader_attributes", {}) - # print(' shader_data: %s' % str(shader_data)) - - if len(shader_data) == 0: - info_box = layout.box() - info_box.label('Select a shader from the preset list.', icon='ERROR') - else: - - # MISSING VERTEX COLOR - active_vcolors = bpy.context.active_object.data.vertex_colors - is_valid_vcolor = _MESH_consts.default_vcol in active_vcolors - is_valid_vcolor_a = _MESH_consts.default_vcol + _MESH_consts.vcol_a_suffix in active_vcolors - if not is_valid_vcolor or not is_valid_vcolor_a: - - vcol_missing_box = layout.box() - - title_row = vcol_missing_box.row(align=True) - title_row.label("Vertex color layer(s) missing!", icon="ERROR") - - info_msg = "Currently active object is missing vertex color layers with names:\n" - if not is_valid_vcolor: - info_msg += "-> '" + _MESH_consts.default_vcol + "'\n" - - if not is_valid_vcolor_a: - info_msg += "-> '" + _MESH_consts.default_vcol + _MESH_consts.vcol_a_suffix + "'\n" - - info_msg += "You can use 'Add Vertex Colors To (Active/All)' button to add needed layers or add layers manually." - _shared.draw_warning_operator(title_row, "Vertex Colors Missing", info_msg, text="More Info", icon="INFO") - - col = vcol_missing_box.column(align=True) - col.operator("mesh.scs_add_vcolors_to_active") - col.operator("mesh.scs_add_vcolors_to_all") - - global_mat_attr = layout.column(align=False) - - # UI SPLIT PERCENTAGE PROPERTY - global_mat_attr.row().prop(scs_props, "shader_item_split_percentage", slider=True) - - # PRESET INFO - effect_name = mat.scs_props.mat_effect_name - effect_name = str('"' + effect_name + '"') - preset_info_row = global_mat_attr.row() - preset_info_space = preset_info_row.split(percentage=split_perc, align=True) - preset_info_space.label("Effect:", icon='NONE') - preset_info_space.label(effect_name, icon='NONE') - - # MATERIAL ALIASING - alias_row = global_mat_attr.row() - alias_row.enabled = not is_imported_shader - alias_row = alias_row.split(percentage=split_perc) - alias_row.label("Aliasing:") - - alias_row = alias_row.row(align=True) - alias_text = "Enabled" if mat.scs_props.enable_aliasing else "Disabled" - alias_icon = "FILE_TICK" if mat.scs_props.enable_aliasing else "X" - alias_row.prop(mat.scs_props, "enable_aliasing", icon=alias_icon, text=alias_text, toggle=True) - - normalized_base_tex_path = mat.scs_props.shader_texture_base.replace("\\", "/") - is_aliasing_path = ("/material/road" in normalized_base_tex_path or - "/material/terrain" in normalized_base_tex_path or - "/material/custom" in normalized_base_tex_path) - - is_aliasable = ('textures' in shader_data and - ( - len(shader_data["textures"]) == 1 or - (len(shader_data["textures"]) == 2 and "dif.spec.weight.mult2" in effect_name) - )) - - if mat.scs_props.enable_aliasing: - - if not (is_aliasing_path and is_aliasable): - - aliasing_info_msg = str("Material aliasing will work only for materials which 'Base' texture\n" - "is loaded from this directories and their subdirectories:\n" - "-> '/material/road'\n" - "-> '/material/terrain'\n" - "-> '/material/custom'\n" - "Additional requirement for aliasing is also single texture material or\n" - "exceptionally multi texture material of 'dif.spec.weight.mult2' family.\n\n" - "Currently aliasing can not be done because:") - - if not is_aliasing_path: - aliasing_info_msg += "\n-> Your 'Base' texture doesn't point to any of this (sub)directories." - if not is_aliasable: - aliasing_info_msg += "\n-> Current shader type use multiple textures or it's not 'dif.spec.weight.mult2' family type." - - _shared.draw_warning_operator(alias_row, "Aliasing Info", aliasing_info_msg, icon="INFO") - - alias_op_col = alias_row.column(align=True) - alias_op_col.enabled = is_aliasing_path and is_aliasable - alias_op_col.operator('material.load_aliased_material', icon="LOAD_FACTORY", text="") - - # MATERIAL SUBSTANCE - substance_row = global_mat_attr.row() - substance_row.enabled = not is_imported_shader - substance_row = substance_row.split(percentage=split_perc) - substance_row.label("Substance:") - substance_row = substance_row.split(percentage=1 / (1 - split_perc + 0.000001) * 0.1, align=True) - props = substance_row.operator("material.scs_looks_wt", text="WT") - props.property_str = "substance" - substance_row.prop_search(mat.scs_props, 'substance', scs_globals, 'scs_matsubs_inventory', icon='NONE', text="") - - if len(shader_data['attributes']) == 0 and len(shader_data['textures']) == 0: - info_box = layout.box() - if shader_data['effect'].endswith('mlaaweight'): - info_box.label('"Multi Level Anti-Aliasing" shader has no parameters.', icon='INFO') + texture_row = texture_box.row(align=True) + item_space = texture_row.split(factor=split_perc, align=True) + item_space.alignment = 'RIGHT' + + # in case of custom tobj value texture is used only for preview + if read_only and use_imported_tobj: + item_space.label(text="Preview Tex") else: - info_box.label('No shader parameters!', icon='INFO') - - if 'attributes' in shader_data and len(shader_data["attributes"]) > 0: - - attributes_box = layout.box() - attributes_box.enabled = not is_imported_shader - - if scs_props.shader_attributes_expand: - panel_header = attributes_box.split(percentage=0.5) - panel_header_1 = panel_header.row() - panel_header_1.prop( - scs_props, - 'shader_attributes_expand', - text="Material Attributes", - icon='TRIA_DOWN', - icon_only=True, - emboss=False - ) + item_space.label(text="Texture") - attributes_data = shader_data['attributes'] - if attributes_data: - attrs_column = attributes_box.column() # create column for compact display with alignment - for attribute_key in sorted(attributes_data.keys()): - _draw_shader_attribute(attrs_column, mat, split_perc, attributes_data[attribute_key]) + layout_box_col = item_space.column(align=True) + layout_box_row = layout_box_col.row(align=True) + if shader_texture: + texture_icon = 'TEXTURE' else: - panel_header = attributes_box.split(percentage=0.5) - panel_header_1 = panel_header.row() - panel_header_1.prop( - scs_props, - 'shader_attributes_expand', - text="Material Attributes", - icon='TRIA_RIGHT', - icon_only=True, - emboss=False - ) - - if 'textures' in shader_data and len(shader_data["textures"]) > 0: - textures_box = layout.box() - if scs_props.shader_textures_expand: - panel_header = textures_box.split(percentage=0.5) - panel_header_1 = panel_header.row() - panel_header_1.prop( - scs_props, - 'shader_textures_expand', - text="Material Textures", - icon='TRIA_DOWN', - icon_only=True, - emboss=False - ) + texture_icon = 'MATPLANE' - textures_data = shader_data['textures'] - if textures_data: - - if is_imported_shader: - - mappings_box = textures_box.box() - row = mappings_box.row() - row.label("Mappings:", icon="GROUP_UVS") - row = mappings_box.row() - row.template_list( - 'SCSMaterialCustomMappingSlot', - list_id="", - dataptr=mat.scs_props, - propname="custom_tex_coord_maps", - active_dataptr=mat.scs_props, - active_propname="active_custom_tex_coord", - rows=3, - maxrows=5, - type='DEFAULT', - columns=9 - ) - - col = row.column(align=True) - col.operator('material.add_custom_tex_coord_map', text="", icon='ZOOMIN') - col.operator('material.remove_custom_tex_coord_map', text="", icon='ZOOMOUT') - - for texture_key in sorted(textures_data.keys()): - _draw_shader_texture(textures_box, mat, split_perc, textures_data[texture_key], is_imported_shader) + # MARK INVALID SLOTS + if _path_utils.is_valid_shader_texture_path(shader_texture): + layout_box_row.alert = False else: - panel_header = textures_box.split(percentage=0.5) - panel_header_1 = panel_header.row() - panel_header_1.prop(scs_props, 'shader_textures_expand', text="Material Textures", icon='TRIA_RIGHT', icon_only=True, emboss=False) + layout_box_row.alert = True + texture_icon = 'NONE' # info operator will have icon, so texture path should use none + layout_box_row = layout_box_row.row(align=True) -def _draw_preset_shader_panel(layout, mat, scene_scs_props, scs_globals): - """Creates provisional Shader Preset sub-panel.""" + # MARK EMPTY SLOTS + layout_box_row.alert |= (shader_texture == "") - is_imported_shader = mat.scs_props.active_shader_preset_name == "" + if layout_box_row.alert: + _shared.draw_warning_operator(layout_box_row, + title="Texture Not Found", + message="Texture with given path doesn't exists or SCS Project Base Path is not properly set!") - # SHADER PRESETS PANEL - _draw_shader_presets(layout, scene_scs_props, scs_globals, is_imported_shader=is_imported_shader) + layout_box_row.prop(mat.scs_props, shader_texture_id, text="", icon=texture_icon) - # FLAVORS PANEL - _draw_shader_flavors(layout, mat) + props = layout_box_row.operator('material.scs_tools_select_texture_path', text="", icon='FILEBROWSER') + props.shader_texture = shader_texture_id # DYNAMIC ID SAVE (FOR FILE REQUESTER) - # PARAMETERS PANEL - _draw_shader_parameters(layout, mat, scene_scs_props, scs_globals, is_imported_shader=is_imported_shader) + props = layout_box_row.operator("material.scs_tools_material_item_extras", text="", icon="LAYER_USED") + props.property_str = shader_texture_id + # ADDITIONAL TEXTURE SETTINGS + if (not read_only or (read_only and not use_imported_tobj)) and texture_box.enabled: -class SCSMaterialCustomMappingSlot(bpy.types.UIList): - """ - Draw custom tex coord item slot within Custom Mapping list - """ + tobj_filepath = _path_utils.get_tobj_path_from_shader_texture(shader_texture) - def draw_item(self, context, layout, data, item, icon, active_data, active_property, index): - line = layout.split(percentage=0.4, align=False) - line.prop(item, "name", text="", emboss=False, icon_value=icon) + tobj_settings_row = layout_box_col.row(align=True) + + if not tobj_filepath: + props = tobj_settings_row.operator("mmaterial.scs_tools_create_tobj", icon='FILE_NEW', text="") + props.texture_type = texture_type + # creating extra column->row so it can be properly disabled as tobj doesn't exists + tobj_settings_row = tobj_settings_row.column(align=True).row(align=True) + + # enable settings only if tobj exists and map type of the tobj is 2d + tobj_settings_row.enabled = tobj_filepath is not None and getattr(mat.scs_props, shader_texture_id + "_map_type", "") == "2d" + + if tobj_filepath: + mtime = str(os.path.getmtime(tobj_filepath)) + tobj_settings_row.alert = (mtime != getattr(mat.scs_props, shader_texture_id + "_tobj_load_time", "NOT FOUND")) + props = tobj_settings_row.operator("material.scs_tools_reload_tobj", icon='FILE_REFRESH', text="") + props.texture_type = texture_type + else: + tobj_settings_row.alert = True + + tobj_settings_row.prop_menu_enum( + mat.scs_props, + str('shader_' + tag_id_string + '_settings'), + icon='SETTINGS', + ) + + # UV LAYERS FOR TEXTURE + uv_mappings = getattr(mat.scs_props, "shader_" + tag_id_string + "_uv", None) + + if len(uv_mappings) > 0: + + texture_row = texture_box.row(align=True) + item_space = texture_row.split(factor=split_perc, align=True) + item_space.alignment = 'RIGHT' + if read_only: + item_space.label(text="Preview UV Map") + else: + item_space.label(text="Mapping") + layout_box_col = item_space.column(align=True) + + for mapping in uv_mappings: + + item_space_row = layout_box_col.row(align=True) + + # add info about normal map uv mapping property in case of imported shader + if read_only and tag_id_string == "texture_nmap": + preview_nmap_msg = str("Maping value for normal maps is in the case of imported shader\n" + "also used for defining uv map layer for tangent calculations!\n" + "If the uv map is not provided first entry from Mappings list above will be used!") + _shared.draw_warning_operator(item_space_row, "Mapping Info", preview_nmap_msg, icon='INFO') + + if mapping.value and mapping.value != "" and mapping.value in bpy.context.active_object.data.uv_layers: + icon = 'GROUP_UVS' + else: + icon = 'ERROR' + + item_space_row.prop_search( + data=mapping, + property="value", + search_data=bpy.context.active_object.data, + search_property='uv_layers', + text="", + icon=icon, + ) - if item.value == "" or item.value not in bpy.context.active_object.data.uv_layers: - icon = "ERROR" else: - icon = "GROUP_UVS" + texture_box.row().label(text="Unsupported Shader Texture Type!", icon='ERROR') - line.prop_search( - data=item, - property="value", - search_data=bpy.context.active_object.data, - search_property='uv_layers', - text="", - icon=icon, - ) + def draw(self, context): + mat = context.active_object.active_material + + if self.is_imported_shader: + + mappings_box = self.layout.box() + row = mappings_box.row() + row.label(text="Mappings:", icon='GROUP_UVS') + row = mappings_box.row() + row.template_list( + SCS_TOOLS_UL_MaterialCustomMappingSlot.__name__, + list_id="", + dataptr=mat.scs_props, + propname="custom_tex_coord_maps", + active_dataptr=mat.scs_props, + active_propname="active_custom_tex_coord", + rows=3, + maxrows=5, + type='DEFAULT', + columns=9 + ) + col = row.column(align=True) + col.operator('material.scs_tools_add_custom_tex_coord_mapping', text="", icon='ADD') + col.operator('material.scs_tools_remove_custom_tex_coord_mapping', text="", icon='REMOVE') + + col = self.layout.column(align=True) + for texture_key in sorted(self.textures_data.keys()): + self.draw_texture_item(col, mat, _UI_SPLIT_PERC, self.textures_data[texture_key], self.is_imported_shader) -class SCSMaterialSpecials(_MaterialPanelBlDefs, bpy.types.Panel): - """Creates a Panel in the Material properties window""" - bl_label = "SCS Materials" - bl_idname = "MATERIAL_PT_SCS_materials" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "material" + +class SCS_TOOLS_PT_LooksOnMaterial(_shared.HeaderIconPanel, _MaterialPanelBlDefs, Panel): + """Draws SCS Looks panel on object tab.""" + + bl_label = "SCS Looks" + bl_options = {'DEFAULT_CLOSED'} + + @classmethod + def poll(cls, context): + if not _MaterialPanelBlDefs.poll(context): + return False + + cls.object = context.active_object + cls.scs_root = _object_utils.get_scs_root(cls.object) + return cls.object and cls.scs_root def draw(self, context): - """UI draw function.""" - layout = self.layout - scene = bpy.context.scene - mat = context.material - scs_globals = _get_scs_globals() + layout = self.get_layout() + _shared.draw_scs_looks_panel(layout, self.object, self.scs_root, without_box=True) - # disable pane if config is being updated - layout.enabled = not scs_globals.config_update_lock - if mat: - # PROVISIONAL SHADER PRESET PANEL - _draw_preset_shader_panel(layout.box(), mat, scene.scs_props, scs_globals) +classes = ( + SCS_TOOLS_PT_Material, + SCS_TOOLS_PT_MaterialAttributes, + SCS_TOOLS_PT_MaterialTextures, + SCS_TOOLS_PT_LooksOnMaterial, + SCS_TOOLS_UL_MaterialCustomMappingSlot, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + from io_scs_tools import SCS_TOOLS_MT_MainMenu + SCS_TOOLS_MT_MainMenu.append_props_entry("Material Properties", SCS_TOOLS_PT_Material.__name__) + - active_object = context.active_object - scs_root_object = _object_utils.get_scs_root(active_object) - if active_object and scs_root_object: - _shared.draw_scs_looks_panel(layout, scene, active_object, scs_root_object) +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/addon/io_scs_tools/ui/mesh.py b/addon/io_scs_tools/ui/mesh.py index 2fc4a6a..cd44e57 100644 --- a/addon/io_scs_tools/ui/mesh.py +++ b/addon/io_scs_tools/ui/mesh.py @@ -16,8 +16,9 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software +import bpy from bpy.types import Panel from io_scs_tools.ui import shared as _shared @@ -25,14 +26,27 @@ class _MeshPanelBlDefs(_shared.HeaderIconPanel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" + bl_ui_units_x = 15 + @classmethod + def poll(cls, context): + return hasattr(context, "active_object") and context.active_object and context.active_object.type == "MESH" -class SCSTools(_MeshPanelBlDefs, Panel): + def get_layout(self): + """Returns layout depending where it's drawn into. If popover create extra box to make it distinguisable between different sub-panels.""" + if self.is_popover: + layout = self.layout.box().column() + else: + layout = self.layout + + return layout + + +class SCS_TOOLS_PT_Mesh(_MeshPanelBlDefs, Panel): """ - Creates "SCS Object Specials" panel in the Object properties window. + Creates "SCS Mesh" panel in the Object properties window. """ - bl_label = "SCS Mesh Specials" - bl_idname = "MESH_PT_SCS_specials" + bl_label = "SCS Mesh" bl_context = "data" def draw(self, context): @@ -42,9 +56,34 @@ def draw(self, context): :type context: bpy.context """ - layout = self.layout - mesh = context.mesh + if not self.poll(context): + self.layout.label(text="No active mesh object!", icon="INFO") + return + + layout = self.get_layout() + mesh = context.active_object.data + + layout.use_property_split = True + layout.use_property_decorate = False # show this only for meshes not for empties and other kinda objects if mesh: layout.prop(mesh.scs_props, "vertex_color_multiplier") + + +classes = ( + SCS_TOOLS_PT_Mesh, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + from io_scs_tools import SCS_TOOLS_MT_MainMenu + SCS_TOOLS_MT_MainMenu.append_props_entry("Mesh Properties", SCS_TOOLS_PT_Mesh.__name__) + + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/addon/io_scs_tools/ui/object.py b/addon/io_scs_tools/ui/object.py index 9404a08..48129a2 100644 --- a/addon/io_scs_tools/ui/object.py +++ b/addon/io_scs_tools/ui/object.py @@ -16,1240 +16,1201 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import os import bpy from bpy.types import Panel from io_scs_tools.consts import Operators as _OP_consts from io_scs_tools.consts import PrefabLocators as _PL_consts -from io_scs_tools.internals.connections.wrappers import group as _connections_group_wrapper +from io_scs_tools.internals.connections.wrappers import collection as _connections_wrapper from io_scs_tools.utils import object as _object_utils from io_scs_tools.utils import get_scs_globals as _get_scs_globals +from io_scs_tools.utils import get_scs_inventories as _get_scs_inventories from io_scs_tools.ui import shared as _shared -class _ObjectPanelBlDefs(_shared.HeaderIconPanel): +class _ObjectPanelBlDefs: bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" + bl_ui_units_x = 15 + @classmethod + def poll(cls, context): + return hasattr(context, "active_object") and context.active_object -def _draw_locator_preview_panel(layout, obj, draw_box=True): - """Draw Locator Preview panel. + def get_layout(self): + """Returns layout depending where it's drawn into. If popover create extra box to make it distinguisable between different sub-panels.""" + if self.is_popover: + layout = self.layout.box().column() + else: + layout = self.layout - :param layout: Blender UI Layout to draw to - :type layout: bpy.types.UILayout - :param obj: SCS Locator Object - :type obj: bpy.types.Object - :param draw_box: Whether to draw a box around the Panel - :type draw_box: bool - """ - if draw_box: - box = layout.box() - else: - box = layout - - # Locator Preview Model Directory (FILE_PATH - relative) - row = box.row(align=True) - - # validity check for preview model path - row.alert = True - if obj.scs_props.locator_preview_model_path == "": - row.alert = False - elif os.path.isdir(_get_scs_globals().scs_project_path): - if os.path.isfile(_get_scs_globals().scs_project_path + os.sep + obj.scs_props.locator_preview_model_path): - if obj.scs_props.locator_preview_model_path.endswith(".pim"): - row.alert = False - - row.prop(obj.scs_props, 'locator_preview_model_path', expand=True) - row.operator('object.select_preview_model_path', text='', icon='FILESEL') - - # Locator Preview Model Controls - row = box.row() - row.prop(obj.scs_props, 'locator_show_preview_model', icon='NONE') - row.prop(obj.scs_props, 'locator_preview_model_type', icon='NONE') - - -def _draw_locator_panel(layout, context, scene, obj, enabled=True): - """Draw Locator Settings Panel. - - :param layout: Blender UI Layout to draw to - :type layout: bpy.types.UILayout - :param context: Blender Context - :type context: bpy.context - :param scene: Blender Scene - :type scene: bpy.types.Scene - :param obj: SCS Locator Object - :type obj: bpy.types.Object - :param enabled: if locator type selecting shall be disabled - :type enabled: bool - """ - - loc_set_split_percentage = 0.33 # the best value to match the rest of the layout in locators panel - - layout_box = layout.box() - if scene.scs_props.locator_settings_expand: - loc_header = layout_box.row() - loc_header.prop(scene.scs_props, 'locator_settings_expand', text="Locator Settings:", icon='TRIA_DOWN', icon_only=True, emboss=False) - loc_header.label('') - row = layout_box.row() - row.enabled = enabled - row.prop(obj.scs_props, 'locator_type', icon='NONE') + return layout - # MODEL LOCATORS - if obj.scs_props.locator_type == 'Model': - row = layout_box.row().split(percentage=loc_set_split_percentage) - col1 = row.column() - col2 = row.column() - - # locator name - col1.label("Name:") - col2.prop(obj, 'name', text="") - - # locator hookup - col1.label("Hookup:") - row = col2.row(align=True) - props = row.operator('scene.scs_reload_library', icon='FILE_REFRESH', text="") - props.library_path_attr = "hookup_library_rel_path" - props = row.operator("object.select_model_locators_with_same_hookup", icon="ZOOM_SELECTED", text="") - props.source_object = obj.name - row.prop_search(obj.scs_props, 'locator_model_hookup', _get_scs_globals(), 'scs_hookup_inventory', text="") - # (MODEL) LOCATOR PREVIEW PANEL - _draw_locator_preview_panel(layout_box, obj) - # COLLISION LOCATORS - elif obj.scs_props.locator_type == 'Collision': - row = layout_box.row() - row.prop(obj.scs_props, 'locator_collider_type', icon='NONE') - row = layout_box.row() - row_box = row.box() - display_row = row_box.row() - if obj.scs_props.locator_collider_wires: - icon = "FILE_TICK" - else: - icon = "X" - display_row.prop(obj.scs_props, 'locator_collider_wires', text='Wireframes', icon=icon, toggle=True) - if obj.scs_props.locator_collider_faces: - icon = "FILE_TICK" +class SCS_TOOLS_UL_ObjectPartSlot(bpy.types.UIList): + """Draw part item slot within SCS Parts list""" + + @staticmethod + def draw_icon_part_tools(layout, index): + """Draws Part Tools icons in a line. + + :param layout: Blender UI Layout to draw to + :type layout: bpy.types.UILayout + :param index: index of part in parts inventory + :type index: int + """ + props = layout.operator('object.scs_tools_de_select_objects_with_part', text="", emboss=False, icon='RESTRICT_SELECT_OFF') + props.part_index = index + props.select_type = _OP_consts.SelectionType.undecided + props = layout.operator('object.scs_tools_switch_part_visibility', text="", emboss=False, icon='HIDE_OFF') + props.part_index = index + props.view_type = _OP_consts.ViewType.undecided + + def draw_item(self, context, layout, data, item, icon, active_data, active_property, index): + if self.layout_type in {'DEFAULT', 'COMPACT'}: + if item: + line = layout.split(factor=0.6, align=False) + line.prop(item, "name", text="", emboss=False, icon_value=icon) + tools = line.row(align=True) + tools.alignment = 'RIGHT' + self.draw_icon_part_tools(tools, index) else: - icon = "X" - display_row.prop(obj.scs_props, 'locator_collider_faces', text='Faces', icon=icon, toggle=True) - if obj.scs_props.locator_collider_type != 'Convex': - if obj.scs_props.locator_collider_centered: - icon = "FILE_TICK" - else: - icon = "X" - row_box.prop(obj.scs_props, 'locator_collider_centered', icon=icon, toggle=True) - box_col = layout_box.column() - loc_set = box_col.column(align=True) - loc_set.prop(obj.scs_props, 'locator_collider_mass', icon='NONE') - if obj.scs_props.locator_collider_type == 'Convex': - loc_set.prop(obj.scs_props, 'locator_collider_margin', icon='NONE') - # loc_set = row_box.row() - # loc_set.prop(obj.scs_props, 'locator_collider_material', icon='NONE') - row_box_row = box_col.row() - if obj.scs_props.locator_collider_type == 'Box': - col = row_box_row.column(align=True) - col.prop(obj.scs_props, 'locator_collider_box_x', icon='NONE') - col.prop(obj.scs_props, 'locator_collider_box_y', icon='NONE') - col.prop(obj.scs_props, 'locator_collider_box_z', icon='NONE') - elif obj.scs_props.locator_collider_type == 'Sphere': - row = row_box_row.row() - row.prop(obj.scs_props, 'locator_collider_dia', text='Sphere Diameter', icon='NONE') - elif obj.scs_props.locator_collider_type == 'Capsule': - col = row_box_row.column(align=True) - col.prop(obj.scs_props, 'locator_collider_dia', text='Capsule Diameter', icon='NONE') - col.prop(obj.scs_props, 'locator_collider_len', text='Capsule Length', icon='NONE') - elif obj.scs_props.locator_collider_type == 'Cylinder': - col = row_box_row.column(align=True) - col.prop(obj.scs_props, 'locator_collider_dia', text='Cylinder Diameter', icon='NONE') - col.prop(obj.scs_props, 'locator_collider_len', text='Cylinder Length', icon='NONE') - elif obj.scs_props.locator_collider_type == 'Convex': - col_row = row_box_row.row() - col_row.label('%i hull vertices.' % obj.scs_props.locator_collider_vertices, icon='INFO') + layout.label(text="", icon_value=icon) + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon_value=icon) - # PREFAB LOCATORS - elif obj.scs_props.locator_type == 'Prefab': - box_row = layout_box.row() - box_row.enabled = enabled - box_row.prop(obj.scs_props, 'locator_prefab_type', icon='NONE') - box_row = layout_box.row() - box_row_box = box_row.box() - if obj.scs_props.locator_prefab_type == 'Control Node': - loc_set = box_row_box.row() - loc_set.prop(obj.scs_props, 'locator_prefab_con_node_index', icon='NONE') - - loc_set_col = box_row_box.column(align=True) - loc_set = loc_set_col.row(align=True) - - if not _object_utils.can_assign_terrain_points(context): - _shared.draw_warning_operator( - loc_set, - "Assigning Terrain Points", - str("To be able to assign terrain points you have to:\n" - "1. Select 'Control Node' locator and mesh object from which vertices you want to use as terrain points\n" - "2. Make sure that mesh object was selected last\n" - "3. Switch to 'Edit Mode'"), - icon="INFO" - ) - - loc_set.operator('object.assign_terrain_points') - - loc_set = loc_set_col.row(align=True) - loc_set.operator('object.clear_all_terrain_points') - - loc_set = box_row_box.row(align=True).split(percentage=0.5, align=True) - loc_set.label("Preview Terrain Points:", icon="VISIBLE_IPO_ON") - props = loc_set.operator('object.preview_terrain_points', text="Visible") - props.preview_all = False - props = loc_set.operator('object.preview_terrain_points', text="All") - props.preview_all = True - loc_set.operator('object.abort_preview_terrain_points', text="Abort") - - if obj.scs_props.locator_prefab_type == 'Sign': - loc_set = box_row_box.row().split(percentage=loc_set_split_percentage) - loc_set.label("Sign Model:") - row = loc_set.row(align=True) - props = row.operator('scene.scs_reload_library', icon='FILE_REFRESH', text="") - props.library_path_attr = "sign_library_rel_path" - row.prop_search(obj.scs_props, 'locator_prefab_sign_model', _get_scs_globals(), 'scs_sign_model_inventory', icon='NONE', text="") - if obj.scs_props.locator_prefab_type == 'Spawn Point': - loc_set = box_row_box.row() - loc_set.prop(obj.scs_props, 'locator_prefab_spawn_type', icon='NONE') - if obj.scs_props.locator_prefab_type == 'Traffic Semaphore': - loc_set = box_row_box.column() - row = loc_set.row(align=True).split(percentage=loc_set_split_percentage) - col1 = row.column() - col2 = row.column() - - # id - col1.label("ID:") - col2.prop(obj.scs_props, 'locator_prefab_tsem_id', icon='NONE', text="") - - # profile - col1.label("Profile:") - row = col2.row(align=True) - props = row.operator('scene.scs_reload_library', icon='FILE_REFRESH', text="") - props.library_path_attr = "tsem_library_rel_path" - row.prop_search(obj.scs_props, 'locator_prefab_tsem_profile', _get_scs_globals(), 'scs_tsem_profile_inventory', icon='NONE', text="") - - # type - col1.label("Type:") - col2.prop(obj.scs_props, 'locator_prefab_tsem_type', icon='NONE', text="") - - # interval distances and cycle delay - loc_set_col = loc_set.column() - if obj.scs_props.locator_prefab_tsem_type in ('0', '1'): - loc_set_col.enabled = False - else: - loc_set_col.enabled = True - loc_set = loc_set_col.row(align=True) - loc_set.label('Intervals/Distances:', icon='NONE') - loc_set = loc_set_col.row(align=True) - loc_set.prop(obj.scs_props, 'locator_prefab_tsem_gs', icon='NONE') - loc_set.prop(obj.scs_props, 'locator_prefab_tsem_os1', icon='NONE') - loc_set.prop(obj.scs_props, 'locator_prefab_tsem_rs', icon='NONE') - loc_set.prop(obj.scs_props, 'locator_prefab_tsem_os2', icon='NONE') - - loc_set = loc_set_col.row() - loc_set.prop(obj.scs_props, 'locator_prefab_tsem_cyc_delay', icon='NONE') - - if obj.scs_props.locator_prefab_type == 'Navigation Point': - loc_set = box_row_box.column(align=True) - - loc_bools_col = loc_set.column(align=True) - loc_set_row = loc_bools_col.row() - loc_set_row.prop(obj.scs_props, 'locator_prefab_np_low_probab', icon='NONE') - loc_set_row.prop(obj.scs_props, 'locator_prefab_np_add_priority', icon='NONE') - loc_set_row = loc_bools_col.row() - loc_set_row.prop(obj.scs_props, 'locator_prefab_np_limit_displace', icon='NONE') - - loc_set = box_row_box.row() - loc_set.prop(obj.scs_props, 'locator_prefab_np_allowed_veh', icon='NONE') - loc_set = box_row_box.row() - loc_set.prop(obj.scs_props, 'locator_prefab_np_blinker', icon='NONE', expand=True) - - loc_set = box_row_box.row().split(percentage=loc_set_split_percentage) - col1 = loc_set.column() - col2 = loc_set.column() - - # priority modifier - col1.label("Priority Modifier:") - col2.prop(obj.scs_props, 'locator_prefab_np_priority_modifier', icon='NONE', text="") - - # traffic semaphore - col1.label("Traffic Semaphore:") - col2.prop(obj.scs_props, 'locator_prefab_np_traffic_semaphore', icon='NONE', text="") - - # traffic rule - col1.label("Traffic Rule:") - row = col2.row(align=True) - props = row.operator('scene.scs_reload_library', icon='FILE_REFRESH', text="") - props.library_path_attr = "traffic_rules_library_rel_path" - row.prop_search(obj.scs_props, 'locator_prefab_np_traffic_rule', - _get_scs_globals(), 'scs_traffic_rules_inventory', - icon='NONE', text="") - - # boundary - col1.label("Boundary:") - col2.prop(obj.scs_props, 'locator_prefab_np_boundary', icon='NONE', text="") - - # boundary node - col1.label("Boundary Node:") - col2.prop(obj.scs_props, 'locator_prefab_np_boundary_node', icon='NONE', text="") - - loc_set = box_row_box.row() - if len(context.selected_objects) == 2: - loc0_obj = None - loc1_obj = context.active_object - - # check if both selected objects are navigation points and set not active object - is_mp = 0 - for obj_locator in context.selected_objects: - if obj_locator.scs_props.locator_type == 'Prefab' and obj_locator.scs_props.locator_prefab_type == 'Navigation Point': - is_mp += 1 - if obj_locator != context.active_object: - loc0_obj = obj_locator - if is_mp == 2: - if _connections_group_wrapper.has_connection(loc0_obj, loc1_obj): - loc_set.operator('object.disconnect_prefab_locators', text="Disconnect Navigation Points", icon='UNLINKED') - else: - loc_set.operator('object.connect_prefab_locators', text="Connect Navigation Points", icon='LINKED') - else: - loc_set.enabled = False - loc_set.operator('object.connect_prefab_locators', text="Connect / Disconnect Navigation Points", icon='LINKED') - else: - loc_set.enabled = False - loc_set.operator('object.connect_prefab_locators', text="Connect / Disconnect Navigation Points", icon='LINKED') - - # if obj.scs_props.locator_prefab_type == 'nac': - # loc_set = box_row_box.row() - # loc_set.label('?', icon='QUESTION') - if obj.scs_props.locator_prefab_type == 'Map Point': - - is_polygon = int(obj.scs_props.locator_prefab_mp_road_size) == _PL_consts.MPVF.ROAD_SIZE_MANUAL - - loc_set = box_row_box.column(align=True) - loc_set_row = loc_set.row() - loc_set_row.prop(obj.scs_props, 'locator_prefab_mp_road_over', icon='NONE') - loc_set_row.prop(obj.scs_props, 'locator_prefab_mp_no_outline', icon='NONE') - loc_set_row = loc_set.row() - loc_set_row.active = not is_polygon - loc_set_row.prop(obj.scs_props, 'locator_prefab_mp_no_arrow', icon='NONE') - loc_set_row.prop(obj.scs_props, 'locator_prefab_mp_prefab_exit', icon='NONE') - - loc_set = box_row_box.column() - loc_set_row = loc_set.row() - loc_set_row.prop(obj.scs_props, 'locator_prefab_mp_road_size', icon='NONE') - loc_set_row = loc_set.row() - loc_set_row.active = not is_polygon - loc_set_row.prop(obj.scs_props, 'locator_prefab_mp_road_offset', icon='NONE') - loc_set_row = loc_set.row() - loc_set_row.active = is_polygon - loc_set_row.prop(obj.scs_props, 'locator_prefab_mp_custom_color', icon='NONE') - loc_set_row = loc_set.row() - loc_set_row.active = not is_polygon - loc_set_row.prop(obj.scs_props, 'locator_prefab_mp_assigned_node', icon='NONE') - loc_set_row = loc_set.row() - loc_set_row.active = not is_polygon and obj.scs_props.locator_prefab_mp_assigned_node == "0" - loc_set_row.prop_menu_enum(obj.scs_props, 'locator_prefab_mp_dest_nodes') - - loc_set = box_row_box.row() - if len(context.selected_objects) == 2: - - # check if both selected objects are navigation points and set not active object - is_mp = 0 - for obj_locator in context.selected_objects: - if obj_locator.scs_props.locator_type == 'Prefab' and obj_locator.scs_props.locator_prefab_type == 'Map Point': - is_mp += 1 - if is_mp == 2: - if _connections_group_wrapper.has_connection(context.selected_objects[0], context.selected_objects[1]): - loc_set.operator('object.disconnect_prefab_locators', text="Disconnect Map Points", icon='UNLINKED') - else: - loc_set.operator('object.connect_prefab_locators', text="Connect Map Points", icon='LINKED') - else: - loc_set.enabled = False - loc_set.operator('object.connect_prefab_locators', text="Connect / Disconnect Map Points", icon='LINKED') - else: - loc_set.enabled = False - loc_set.operator('object.connect_prefab_locators', text="Connect / Disconnect Map Points", icon='LINKED') - - if obj.scs_props.locator_prefab_type == 'Trigger Point': - loc_set = box_row_box.row().split(percentage=loc_set_split_percentage) - loc_set.label("Action:") - row = loc_set.row(align=True) - props = row.operator('scene.scs_reload_library', icon='FILE_REFRESH', text="") - props.library_path_attr = "trigger_actions_rel_path" - row.prop_search(obj.scs_props, 'locator_prefab_tp_action', _get_scs_globals(), 'scs_trigger_actions_inventory', icon='NONE', text="") - loc_set = box_row_box.column(align=True) - loc_set.prop(obj.scs_props, 'locator_prefab_tp_range', icon='NONE') - loc_set.prop(obj.scs_props, 'locator_prefab_tp_reset_delay', icon='NONE') - loc_set = box_row_box.column(align=True) - loc_set_row = loc_set.row() - loc_set_row.prop(obj.scs_props, 'locator_prefab_tp_sphere_trigger', icon='NONE') - loc_set_row.prop(obj.scs_props, 'locator_prefab_tp_partial_activ', icon='NONE') - loc_set_row = loc_set.row() - loc_set_row.prop(obj.scs_props, 'locator_prefab_tp_onetime_activ', icon='NONE') - loc_set_row.prop(obj.scs_props, 'locator_prefab_tp_manual_activ', icon='NONE') - - loc_set = box_row_box.row() - if len(context.selected_objects) == 2: - - # check if both selected objects are navigation points and set not active object - is_tp = 0 - for obj_locator in context.selected_objects: - if obj_locator.scs_props.locator_type == 'Prefab' and obj_locator.scs_props.locator_prefab_type == 'Trigger Point': - is_tp += 1 - if is_tp == 2: - if _connections_group_wrapper.has_connection(context.selected_objects[0], context.selected_objects[1]): - loc_set.operator('object.disconnect_prefab_locators', text='Disconnect Trigger Points', icon='UNLINKED') - else: - loc_set.operator('object.connect_prefab_locators', text='Connect Trigger Points', icon='LINKED') - else: - loc_set.enabled = False - loc_set.operator('object.connect_prefab_locators', text='Connect Trigger Points', icon='LINKED') - else: - loc_set.enabled = False - loc_set.operator('object.connect_prefab_locators', text='Connect Trigger Points', icon='LINKED') - # (PREFAB) LOCATOR PREVIEW PANEL - _draw_locator_preview_panel(layout_box, obj) +class SCS_TOOLS_UL_ObjectVariantSlot(bpy.types.UIList): + """Draw variant item slot within SCS Variants list""" - # DISPLAY SETTINGS - # _draw_locator_display_settings_panel(layout_box, scene, obj) - else: - loc_set = layout_box.row() - loc_set.prop(scene.scs_props, 'locator_settings_expand', text="Locator Settings:", icon='TRIA_RIGHT', icon_only=True, emboss=False) - loc_set.label('') + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + if self.layout_type in {'DEFAULT', 'COMPACT'}: + if item: + if context.workspace.scs_props.variant_views == 'integrated': + layout.prop(item, "name", text="", emboss=False, icon_value=icon) + line = layout.row(align=True) + for part in item.parts: + line.prop(part, 'include', text=part.name, toggle=True) + else: + line = layout.split(factor=0.6, align=False) + line.prop(item, "name", text="", emboss=False, icon_value=icon) + tools = line.row(align=True) + tools.alignment = 'RIGHT' + SCS_TOOLS_PT_Variants.draw_icon_variant_tools(tools, index) + else: + layout.label(text="", icon_value=icon) + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon_value=icon) -def _draw_animation_section(layout): - """Draw Animation section. +class SCS_TOOLS_UL_ObjectAnimationSlot(bpy.types.UIList): + """Draw animation item slot within SCS Animation list""" - :param layout: Blender UI Layout to draw to - :type layout: bpy.types.UILayout - """ - context = bpy.context - active_object = context.active_object - scs_root_object = _object_utils.get_scs_root(active_object) - scs_object_animation_inventory = scs_root_object.scs_object_animation_inventory - active_scs_anim_i = scs_root_object.scs_props.active_scs_animation - if len(scs_object_animation_inventory) > active_scs_anim_i: + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + if self.layout_type in {'DEFAULT', 'COMPACT'}: + if item: - layout = layout.box() - layout.label("Active Animation Settings:", icon="ANIM_DATA") + row = layout.row(align=True) + row.prop(item, "export", text="") - active_scs_anim = scs_object_animation_inventory[active_scs_anim_i] + row2 = row.row(align=True) + row2.enabled = item.export + row2.prop(item, "name", text="", emboss=False) - action_col = layout.column(align=True) + extra_ops = row2.row(align=True) + extra_ops.alignment = 'RIGHT' - row = action_col.row(align=True) - icon = "NONE" if active_scs_anim.action in bpy.data.actions else "ERROR" - row.prop_search(active_scs_anim, 'action', bpy.data, 'actions', text="", icon=icon) + props = extra_ops.operator("scene.scs_tools_export_anim_action", text="", icon='EXPORT', emboss=True) + props.index = index - row = action_col.row(align=True) - row.enabled = active_scs_anim.action in bpy.data.actions - row.prop(active_scs_anim, 'anim_start', text="Start", icon='NONE') - row.prop(active_scs_anim, 'anim_end', text="End", icon='NONE') - row.prop(active_scs_anim, 'length', text="Length", icon='NONE') + else: + layout.label(text="", icon_value=icon) + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon_value=icon) - if active_object.animation_data and active_object.animation_data.action: - row = action_col.row(align=True) - row.operator('scene.increase_animation_steps', text='', icon='ZOOMIN') - row.operator('scene.decrease_animation_steps', text='', icon='ZOOMOUT') - row.prop(active_object.animation_data.action.scs_props, "anim_export_step") +class SCS_TOOLS_PT_Object(_shared.HeaderIconPanel, _ObjectPanelBlDefs, Panel): + """Creates "SCS Object" panel in the Object properties window.""" -def _draw_animplayer_panel(layout): - """Draw Animation Player panel. + bl_label = "SCS Object" + bl_context = "object" - :param layout: Blender UI Layout to draw to - :type layout: bpy.types.UILayout - """ - context = bpy.context - scene = context.scene - # active_object = context.active_object - # scs_root_object = utils.get_scs_root_object(active_object) - screen = context.screen + @staticmethod + def has_any_scs_subpanel(context): + """Returns sum of poll methods of all panels that has this one as a parent.""" + + return (SCS_TOOLS_PT_RootObject.poll(context) or + SCS_TOOLS_PT_Locator.poll(context) or + SCS_TOOLS_PT_Parts.poll(context) or + SCS_TOOLS_PT_Variants.poll(context) or + SCS_TOOLS_PT_LooksOnObject.poll(context) or + SCS_TOOLS_PT_Animations.poll(context) or + SCS_TOOLS_PT_Skeleton.poll(context)) + + @staticmethod + def draw_empty_object_panel(layout, obj, enabled=False): + """Creates 'Empty Object' settings sub-panel. + + :param layout: Blender UI Layout to draw to + :type layout: bpy.types.UILayout + :param obj: Blender Empty Object + :type obj: bpy.types.Object + :param enabled: Should empty object type property be enabled? + :type enabled: bool + """ + row = layout.row() + row.enabled = enabled + row.use_property_split = True + row.use_property_decorate = False + row.prop(obj.scs_props, 'empty_object_type', text="Object Type") - layout_box = layout.box() - if scene.scs_props.scs_animplayer_panel_expand: - row = layout_box.row() - row.prop(scene.scs_props, 'scs_animplayer_panel_expand', text="Animation Player:", icon='TRIA_DOWN', icon_only=True, emboss=False) - row.label('') + def draw(self, context): + """UI draw function.""" - # layout_box_row = layout_box.row() - # layout_row = layout_box_row.row(align=True) - # if not scene.use_preview_range: - # layout_row.prop(scene, "frame_start", text="Start") - # layout_row.prop(scene, "frame_end", text="End") - # else: - # layout_row.prop(scene, "frame_preview_start", text="Start") - # layout_row.prop(scene, "frame_preview_end", text="End") - # layout_row = layout_box_row.row() - # layout_row.scale_x = 0.7 - # layout_row.prop(scene, "frame_current", text='') + # draw minimalistict info, so user knows what's going on + if not self.poll(context): + self.layout.label(text="No active object!", icon="INFO") + return - layout_box_row = layout_box.row() - layout_col = layout_box_row.column(align=True) - layout_row = layout_col.row(align=True) - layout_row.prop(scene, "frame_preview_start", text="Start") - layout_row.prop(scene, "frame_preview_end", text="End") - layout_col = layout_box_row.column(align=True) - layout_col.prop(scene, "frame_current", text='Current') + obj = context.active_object - layout_box_row = layout_box.row() - layout_box_row.prop(scene, "use_preview_range", text='', toggle=True) - layout_row = layout_box_row.row(align=True) - layout_row.scale_x = 1.5 - layout_row.operator("screen.frame_jump", text='', icon='REW').end = False - layout_row.operator("screen.keyframe_jump", text='', icon='PREV_KEYFRAME').next = False - if not screen.is_animation_playing: - if scene.sync_mode == 'AUDIO_SYNC' and context.user_preferences.system.audio_device == 'JACK': - sub = layout_row.row(align=True) - sub.scale_x = 2.0 - sub.operator("screen.animation_play", text='', icon='PLAY') - else: - layout_row.operator("screen.animation_play", text='', icon='PLAY_REVERSE').reverse = True - layout_row.operator("screen.animation_play", text='', icon='PLAY') - else: - sub = layout_row.row(align=True) - sub.scale_x = 2.0 - sub.operator("screen.animation_play", text='', icon='PAUSE') - layout_row.operator("screen.keyframe_jump", text='', icon='NEXT_KEYFRAME').next = True - layout_row.operator("screen.frame_jump", text='', icon='FF').end = True - layout_row.prop(scene.render, "fps") - else: - row = layout_box.row() - row.prop(scene.scs_props, 'scs_animplayer_panel_expand', text="Animation Player:", icon='TRIA_RIGHT', icon_only=True, emboss=False) - row.label('') + if obj.type in 'MESH' and _object_utils.can_assign_terrain_points(context): + other_obj = _object_utils.get_other_object(context.selected_objects, obj) + self.draw_empty_object_panel(self.get_layout(), other_obj) + elif obj.type == 'EMPTY': + self.draw_empty_object_panel(self.get_layout(), obj, enabled=True) + elif not self.has_any_scs_subpanel(context): + self.layout.label(text="No SCS props for active object!", icon="INFO") -def _draw_scs_animation_panel(layout): - """Draw Animation settings panel. +class SCS_TOOLS_PT_RootObject(_ObjectPanelBlDefs, Panel): + """Creates 'SCS Root Object' settings sub-panel.""" - :param layout: Blender UI Layout to draw to - :type layout: bpy.types.UILayout - """ - context = bpy.context - scene = context.scene - active_object = context.active_object - scs_root_object = _object_utils.get_scs_root(active_object) + bl_parent_id = SCS_TOOLS_PT_Object.__name__ + bl_label = "Root Object" - layout_column = layout.column(align=True) - layout_box = layout_column.box() + @classmethod + def poll(cls, context): + if not _ObjectPanelBlDefs.poll(context): + return False - if scene.scs_props.scs_animation_settings_expand: + return context.active_object.scs_props.empty_object_type == 'SCS_Root' - layout_box_row = layout_box.row() - layout_box_row.prop(scene.scs_props, 'scs_animation_settings_expand', text="SCS Animations:", - icon='TRIA_DOWN', icon_only=True, emboss=False) - layout_box_row.label('') + def draw(self, context): + layout = self.get_layout() + obj = context.active_object - anims_box = layout_column.box() + layout.use_property_split = True + layout.use_property_decorate = False - # layout_box_row = layout_box.row() - # layout_box_row.prop(bpy.data, 'actions', text="Animation Actions:", icon='ACTION') + # export inclusion + layout.prop(obj.scs_props, 'scs_root_object_export_enabled', text="Export Inclusion") - if scs_root_object: + # model type + row = _shared.create_row(layout, use_split=False, enabled=obj.scs_props.scs_root_object_export_enabled) + row.prop(obj.scs_props, 'scs_root_animated', expand=True, toggle=True) - # ANIMATIONS CUSTOM EXPORT PATH - column = anims_box.column(align=True) - if scs_root_object.scs_props.scs_root_object_allow_anim_custom_path: - icon = "FILE_TICK" - text = "Custom Export Path Enabled" - else: - icon = "X" - text = "Custom Export Path Disabled" + # custom export path + col = layout.column(align=True) + col.enabled = obj.scs_props.scs_root_object_export_enabled - column.prop(scs_root_object.scs_props, 'scs_root_object_allow_anim_custom_path', text=text, icon=icon, toggle=True) - row2 = column.row(align=True) - row2.enabled = scs_root_object.scs_props.scs_root_object_allow_anim_custom_path - if row2.enabled: - root_anim_export_path = scs_root_object.scs_props.scs_root_object_anim_export_filepath - row2.alert = ((root_anim_export_path != "" and not root_anim_export_path.startswith("//")) or - not os.path.isdir(os.path.join(_get_scs_globals().scs_project_path, - scs_root_object.scs_props.scs_root_object_anim_export_filepath.strip("//")))) - if row2.alert: - _shared.draw_warning_operator( - row2, - "Custom Export Path Warning", - str("Current custom Animations filepath is unreachable, which may result into an error on export!\n" - "Make sure you did following:\n" - "1. Properly set \"SCS Project Base Path\"\n" - "2. Properly set this custom export path which must be relative on \"SCS Project Base Path\"") - ) - row2.prop(scs_root_object.scs_props, 'scs_root_object_anim_export_filepath', text='', icon='EXPORT') - props = row2.operator('scene.select_directory_inside_base', text='', icon='FILESEL') - props.type = "GameObjectAnimExportPath" + # Global Export Filepath (DIR_PATH - absolute) + row = col.row(align=True) + row.prop(obj.scs_props, 'scs_root_object_allow_custom_path', text="") + row2 = row.row(align=True) + row2.enabled = obj.scs_props.scs_root_object_allow_custom_path + if row2.enabled: + root_export_path = obj.scs_props.scs_root_object_export_filepath + row2.alert = ((root_export_path != "" and not root_export_path.startswith("//")) or + not os.path.isdir(os.path.join(_get_scs_globals().scs_project_path, + obj.scs_props.scs_root_object_export_filepath.strip("//")))) - # ANIMATION LIST - layout_setting = anims_box.row() - layout_setting.template_list( - 'SCSObjectAnimationSlots', - list_id="", - dataptr=scs_root_object, - propname="scs_object_animation_inventory", - active_dataptr=scs_root_object.scs_props, - active_propname="active_scs_animation", - rows=4, - maxrows=10, - type='DEFAULT', - columns=9, + row2.prop(obj.scs_props, 'scs_root_object_export_filepath', text="", icon='EXPORT') + if row2.alert: + _shared.draw_warning_operator( + row2, + "Custom Export Path Warning", + str("Current custom SCS Game Object filepath is unreachable, which may result into an error on export!\n" + "Make sure you did following:\n" + "1. Properly set \"SCS Project Base Path\"\n" + "2. Properly set this custom export path which must be relative on \"SCS Project Base Path\"") ) + props = row2.operator('scene.scs_tools_select_dir_inside_base', text="", icon='FILEBROWSER') + props.type = "GameObjectExportPath" - # LIST BUTTONS - list_buttons = layout_setting.column(align=True) - list_buttons.operator('object.add_scs_animation', text="", icon='ZOOMIN') - list_buttons.operator('object.remove_scs_animation', text="", icon='ZOOMOUT') - list_buttons.separator() - list_buttons.operator('scene.import_scs_anim_actions', text='', icon='IMPORT') - # ANIMATION SETTINGS - if len(scs_root_object.scs_object_animation_inventory) > 0: - _draw_animation_section(anims_box) +class SCS_TOOLS_PT_Locator(_ObjectPanelBlDefs, Panel): + """Draw Locator Settings Panel.""" + + bl_parent_id = SCS_TOOLS_PT_Object.__name__ + bl_label = "Locator Settings" + + @classmethod + def poll(cls, context): + if not _ObjectPanelBlDefs.poll(context): + return False + + return context.active_object.scs_props.empty_object_type == 'Locator' or _object_utils.can_assign_terrain_points(context) + + @staticmethod + def draw_model_locator(layout, obj): + layout.use_property_split = True + layout.use_property_decorate = False + + # locator name + row = layout.row() + row.prop(obj, 'name', text="Name") + + # locator hookup + row = layout.row(align=True) + _shared.compensate_aligning_bug(row, number_of_items=2) + row.prop_search(obj.scs_props, 'locator_model_hookup', _get_scs_inventories(), 'hookups', text="Hookup") + props = row.operator('scene.scs_tools_reload_library', icon='FILE_REFRESH', text="") + props.library_path_attr = "hookup_library_rel_path" + props = row.operator("object.scs_tools_select_model_locators_with_same_hookup", icon='ZOOM_SELECTED', text="") + props.source_object = obj.name + + @staticmethod + def draw_collision_locator(layout, obj): + layout.use_property_split = True + layout.use_property_decorate = False + + layout.prop(obj.scs_props, 'locator_collider_type') + + flow = layout.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False) + + col = flow.column() + col.prop(obj.scs_props, 'locator_collider_wires', text='Draw Wireframes') + col = flow.column() + col.prop(obj.scs_props, 'locator_collider_faces', text='Draw Faces') + if obj.scs_props.locator_collider_type != 'Convex': + layout.prop(obj.scs_props, 'locator_collider_centered') + + layout.prop(obj.scs_props, 'locator_collider_mass') + + if obj.scs_props.locator_collider_type == 'Box': + col = layout.column(align=True) + col.prop(obj.scs_props, 'locator_collider_box_x') + col.prop(obj.scs_props, 'locator_collider_box_y') + col.prop(obj.scs_props, 'locator_collider_box_z') + elif obj.scs_props.locator_collider_type == 'Sphere': + layout.prop(obj.scs_props, 'locator_collider_dia', text='Sphere Diameter') + elif obj.scs_props.locator_collider_type == 'Capsule': + col = layout.column(align=True) + col.prop(obj.scs_props, 'locator_collider_dia', text='Capsule Diameter') + col.prop(obj.scs_props, 'locator_collider_len', text='Capsule Length') + elif obj.scs_props.locator_collider_type == 'Cylinder': + col = layout.column(align=True) + col.prop(obj.scs_props, 'locator_collider_dia', text='Cylinder Diameter') + col.prop(obj.scs_props, 'locator_collider_len', text='Cylinder Length') + elif obj.scs_props.locator_collider_type == 'Convex': + layout.prop(obj.scs_props, 'locator_collider_margin') + layout.label(text="%i hull vertices." % obj.scs_props.locator_collider_vertices, icon='INFO') + + @staticmethod + def draw_prefab_control_node(layout, context, obj, enabled=True): + loc_set = _shared.create_row(layout, use_split=True, enabled=enabled) + loc_set.prop(obj.scs_props, 'locator_prefab_con_node_index') + + loc_set_col = layout.column(align=True) + loc_set = loc_set_col.row(align=True) + + if not _object_utils.can_assign_terrain_points(context): + _shared.draw_warning_operator( + loc_set, + "Assigning Terrain Points", + str("To be able to assign terrain points you have to:\n" + "1. Select 'Control Node' locator and mesh object from which vertices you want to use as terrain points\n" + "2. Make sure that mesh object was selected last\n" + "3. Switch to 'Edit Mode'"), + icon='INFO' + ) + + loc_set.operator('object.scs_tools_assign_terrain_points') + + loc_set = loc_set_col.row(align=True) + loc_set.operator('object.scs_tools_clear_terrain_points') + + loc_set = layout.row(align=True) + loc_set.label(text="Preview Terrain Points:", icon='HIDE_OFF') + loc_set = layout.row(align=True) + props = loc_set.operator('object.scs_tools_preview_terrain_points', text="Visible") + props.preview_all = False + props = loc_set.operator('object.scs_tools_preview_terrain_points', text="All") + props.preview_all = True + loc_set.operator('object.scs_tools_abort_terrain_points_preview', text="Abort") + + @staticmethod + def draw_prefab_sign(layout, obj): + layout.use_property_split = True + layout.use_property_decorate = False + + row = layout.row(align=True) + _shared.compensate_aligning_bug(row, number_of_items=1) + row.prop_search(obj.scs_props, 'locator_prefab_sign_model', _get_scs_inventories(), 'sign_models', text="Sign Model") + props = row.operator('scene.scs_tools_reload_library', icon='FILE_REFRESH', text="") + props.library_path_attr = "sign_library_rel_path" + + @staticmethod + def draw_prefab_spawn_point(layout, obj): + layout.use_property_split = True + layout.use_property_decorate = False + + layout.prop(obj.scs_props, 'locator_prefab_spawn_type') + + @staticmethod + def draw_prefab_semaphore(layout, obj): + layout.use_property_split = True + layout.use_property_decorate = False + + # id + layout.prop(obj.scs_props, 'locator_prefab_tsem_id') + + # profile + row = layout.row(align=True) + _shared.compensate_aligning_bug(row, number_of_items=1) + row.prop_search(obj.scs_props, 'locator_prefab_tsem_profile', _get_scs_inventories(), 'tsem_profiles') + props = row.operator('scene.scs_tools_reload_library', icon='FILE_REFRESH', text="") + props.library_path_attr = "tsem_library_rel_path" + + # type + layout.prop(obj.scs_props, 'locator_prefab_tsem_type', text="Type") + + # interval distances and cycle delay + loc_set_col = layout.column() + loc_set_col.use_property_split = False + loc_set_col.use_property_decorate = False + if obj.scs_props.locator_prefab_tsem_type in ('0', '1'): + loc_set_col.enabled = False + else: + loc_set_col.enabled = True + loc_set = loc_set_col.row(align=True) + loc_set.label(text="Intervals/Distances:") + loc_set = loc_set_col.row(align=True) + loc_set.prop(obj.scs_props, 'locator_prefab_tsem_gs') + loc_set.prop(obj.scs_props, 'locator_prefab_tsem_os1') + loc_set.prop(obj.scs_props, 'locator_prefab_tsem_rs') + loc_set.prop(obj.scs_props, 'locator_prefab_tsem_os2') + + loc_set = loc_set_col.row() + loc_set.prop(obj.scs_props, 'locator_prefab_tsem_cyc_delay') + + @staticmethod + def draw_prefab_navigation_point(layout, context, obj): + layout.use_property_split = True + layout.use_property_decorate = False + + flow = layout.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False) + + col = flow.column() + col.prop(obj.scs_props, 'locator_prefab_np_low_probab') + col = flow.column() + col.prop(obj.scs_props, 'locator_prefab_np_add_priority') + col = flow.column() + col.prop(obj.scs_props, 'locator_prefab_np_limit_displace') + + # allowed vehicles + layout.prop(obj.scs_props, 'locator_prefab_np_allowed_veh') + + # blinker + col = layout.column(align=True) + col.prop(obj.scs_props, 'locator_prefab_np_blinker', expand=True) + + # priority modifier + layout.prop(obj.scs_props, 'locator_prefab_np_priority_modifier') + + # traffic semaphore + layout.prop(obj.scs_props, 'locator_prefab_np_traffic_semaphore') + + # traffic rule + row = layout.row(align=True) + _shared.compensate_aligning_bug(row, number_of_items=1) + row.prop_search(obj.scs_props, 'locator_prefab_np_traffic_rule', _get_scs_inventories(), 'traffic_rules') + props = row.operator('scene.scs_tools_reload_library', icon='FILE_REFRESH', text="") + props.library_path_attr = "traffic_rules_library_rel_path" + + # boundary + layout.prop(obj.scs_props, 'locator_prefab_np_boundary') + + # boundary node + layout.prop(obj.scs_props, 'locator_prefab_np_boundary_node') + + loc_set = layout.row() + if len(context.selected_objects) == 2: + loc0_obj = None + loc1_obj = context.active_object + + # check if both selected objects are navigation points and set not active object + is_mp = 0 + for obj_locator in context.selected_objects: + if obj_locator.scs_props.locator_type == 'Prefab' and obj_locator.scs_props.locator_prefab_type == 'Navigation Point': + is_mp += 1 + if obj_locator != context.active_object: + loc0_obj = obj_locator + if is_mp == 2: + if _connections_wrapper.has_connection(loc0_obj, loc1_obj): + loc_set.operator('object.scs_tools_disconnect_prefab_locators', text="Disconnect Navigation Points", icon='UNLINKED') + else: + loc_set.operator('object.scs_tools_connect_prefab_locators', text="Connect Navigation Points", icon='LINKED') else: - anims_box.label('No Animation!', icon='INFO') + loc_set.enabled = False + loc_set.operator('object.scs_tools_connect_prefab_locators', text="Connect / Disconnect Navigation Points", icon='LINKED') else: - anims_box.label("No 'SCS Root Object'!", icon='INFO') + loc_set.enabled = False + loc_set.operator('object.scs_tools_connect_prefab_locators', text="Connect / Disconnect Navigation Points", icon='LINKED') + + @staticmethod + def draw_prefab_map_point(layout, context, obj): + # box_row_box = layout + layout.use_property_split = True + layout.use_property_decorate = False + + is_polygon = int(obj.scs_props.locator_prefab_mp_road_size) == _PL_consts.MPVF.ROAD_SIZE_MANUAL + + flow = layout.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False) + + col = flow.column() + col.prop(obj.scs_props, 'locator_prefab_mp_road_over') + col = flow.column() + col.prop(obj.scs_props, 'locator_prefab_mp_no_outline') + col = flow.column() + col.enabled = not is_polygon + col.prop(obj.scs_props, 'locator_prefab_mp_no_arrow') + col = flow.column() + col.enabled = not is_polygon + col.prop(obj.scs_props, 'locator_prefab_mp_prefab_exit') + + layout.prop(obj.scs_props, 'locator_prefab_mp_road_size') + row = _shared.create_row(layout, use_split=True, enabled=not is_polygon) + row.prop(obj.scs_props, 'locator_prefab_mp_road_offset') + row = _shared.create_row(layout, use_split=True, enabled=is_polygon) + row.prop(obj.scs_props, 'locator_prefab_mp_custom_color') + row = _shared.create_row(layout, use_split=True, enabled=not is_polygon) + row.prop(obj.scs_props, 'locator_prefab_mp_assigned_node') + row = _shared.create_row(layout, use_split=True, enabled=not is_polygon and obj.scs_props.locator_prefab_mp_assigned_node == "0") + row.prop_menu_enum(obj.scs_props, 'locator_prefab_mp_dest_nodes') + + loc_set = layout.row() + if len(context.selected_objects) == 2: + + # check if both selected objects are navigation points and set not active object + is_mp = 0 + for obj_locator in context.selected_objects: + if obj_locator.scs_props.locator_type == 'Prefab' and obj_locator.scs_props.locator_prefab_type == 'Map Point': + is_mp += 1 + if is_mp == 2: + if _connections_wrapper.has_connection(context.selected_objects[0], context.selected_objects[1]): + loc_set.operator('object.scs_tools_disconnect_prefab_locators', text="Disconnect Map Points", icon='UNLINKED') + else: + loc_set.operator('object.scs_tools_connect_prefab_locators', text="Connect Map Points", icon='LINKED') + else: + loc_set.enabled = False + loc_set.operator('object.scs_tools_connect_prefab_locators', text="Connect / Disconnect Map Points", icon='LINKED') + else: + loc_set.enabled = False + loc_set.operator('object.scs_tools_connect_prefab_locators', text="Connect / Disconnect Map Points", icon='LINKED') + + @staticmethod + def draw_prefab_trigger_point(layout, context, obj): + layout.use_property_split = True + layout.use_property_decorate = False + + row = layout.row(align=True) + _shared.compensate_aligning_bug(row, number_of_items=1) + row.prop_search(obj.scs_props, 'locator_prefab_tp_action', _get_scs_inventories(), 'trigger_actions') + props = row.operator('scene.scs_tools_reload_library', icon='FILE_REFRESH', text="") + props.library_path_attr = "trigger_actions_rel_path" + + layout.prop(obj.scs_props, 'locator_prefab_tp_range') + layout.prop(obj.scs_props, 'locator_prefab_tp_reset_delay') + + flow = layout.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False) + + col = flow.column() + col.prop(obj.scs_props, 'locator_prefab_tp_sphere_trigger') + col = flow.column() + col.prop(obj.scs_props, 'locator_prefab_tp_partial_activ') + col = flow.column() + col.prop(obj.scs_props, 'locator_prefab_tp_onetime_activ') + col = flow.column() + col.prop(obj.scs_props, 'locator_prefab_tp_manual_activ') + + loc_set = layout.row() + if len(context.selected_objects) == 2: + + # check if both selected objects are trigger points and set not active object + is_tp = 0 + for obj_locator in context.selected_objects: + if obj_locator.scs_props.locator_type == 'Prefab' and obj_locator.scs_props.locator_prefab_type == 'Trigger Point': + is_tp += 1 + if is_tp == 2: + if _connections_wrapper.has_connection(context.selected_objects[0], context.selected_objects[1]): + loc_set.operator('object.scs_tools_disconnect_prefab_locators', text='Disconnect Trigger Points', icon='UNLINKED') + else: + loc_set.operator('object.scs_tools_connect_prefab_locators', text='Connect Trigger Points', icon='LINKED') + else: + loc_set.enabled = False + loc_set.operator('object.scs_tools_connect_prefab_locators', text='Connect Trigger Points', icon='LINKED') + else: + loc_set.enabled = False + loc_set.operator('object.scs_tools_connect_prefab_locators', text='Connect Trigger Points', icon='LINKED') + + def draw(self, context): + layout = self.get_layout() + obj = context.active_object + can_select_loc_type = True + + if _object_utils.can_assign_terrain_points(context): + # if user can assign terrain points, then other object is locator + obj = _object_utils.get_other_object(context.selected_objects, obj) + can_select_loc_type = False + + row = _shared.create_row(layout, use_split=True, enabled=can_select_loc_type) + row.prop(obj.scs_props, 'locator_type') + + # MODEL LOCATORS + if obj.scs_props.locator_type == 'Model': + self.draw_model_locator(layout, obj) - # ANIMATION PLAYER PANEL - _draw_animplayer_panel(anims_box) + # COLLISION LOCATORS + elif obj.scs_props.locator_type == 'Collision': + self.draw_collision_locator(layout, obj) - else: - layout_box_row = layout_box.row() - layout_box_row.prop(scene.scs_props, 'scs_animation_settings_expand', text="SCS Animations:", - icon='TRIA_RIGHT', icon_only=True, emboss=False) - layout_box_row.label('') + # PREFAB LOCATORS + elif obj.scs_props.locator_type == 'Prefab': + box_row = _shared.create_row(layout, use_split=True, enabled=can_select_loc_type) + box_row.prop(obj.scs_props, 'locator_prefab_type') + if obj.scs_props.locator_prefab_type == 'Control Node': + self.draw_prefab_control_node(layout, context, obj, enabled=can_select_loc_type) + elif obj.scs_props.locator_prefab_type == 'Sign': + self.draw_prefab_sign(layout, obj) + elif obj.scs_props.locator_prefab_type == 'Spawn Point': + self.draw_prefab_spawn_point(layout, obj) + elif obj.scs_props.locator_prefab_type == 'Traffic Semaphore': + self.draw_prefab_semaphore(layout, obj) + elif obj.scs_props.locator_prefab_type == 'Navigation Point': + self.draw_prefab_navigation_point(layout, context, obj) + elif obj.scs_props.locator_prefab_type == 'Map Point': + self.draw_prefab_map_point(layout, context, obj) + elif obj.scs_props.locator_prefab_type == 'Trigger Point': + self.draw_prefab_trigger_point(layout, context, obj) + + +class SCS_TOOLS_PT_PreviewModel(_ObjectPanelBlDefs, Panel): + """Draw Locator Preview panel.""" + + bl_parent_id = SCS_TOOLS_PT_Locator.__name__ + bl_label = "Preview Model" + + @classmethod + def poll(cls, context): + if not _ObjectPanelBlDefs.poll(context): + return False + + return context.active_object.scs_props.empty_object_type == 'Locator' and context.active_object.scs_props.locator_type in ('Prefab', 'Model') + + def draw_header(self, context): + layout = self.layout + obj = context.active_object + layout.prop(obj.scs_props, 'locator_show_preview_model', text="") -def _draw_scs_skeleton_panel(layout): - scene = bpy.context.scene - active_object = bpy.context.active_object + def draw(self, context): + layout = self.get_layout() + obj = context.active_object - layout_column = layout.column(align=True) - layout_box = layout_column.box() + layout.enabled = obj.scs_props.locator_show_preview_model + layout.use_property_split = True + layout.use_property_decorate = False - if scene.scs_props.scs_skeleton_panel_expand: + # Locator Preview Model Directory (FILE_PATH - relative) + row = layout.row(align=True) - layout_box_row = layout_box.row() - layout_box_row.prop(scene.scs_props, 'scs_skeleton_panel_expand', text="SCS Skeleton:", - icon='TRIA_DOWN', icon_only=True, emboss=False) - layout_box_row.label('') + # validity check for preview model path + row.alert = True + if obj.scs_props.locator_preview_model_path == "": + row.alert = False + elif os.path.isdir(_get_scs_globals().scs_project_path): + if os.path.isfile(_get_scs_globals().scs_project_path + os.sep + obj.scs_props.locator_preview_model_path): + if obj.scs_props.locator_preview_model_path.endswith(".pim"): + row.alert = False - layout_box = layout_column.box() + row.prop(obj.scs_props, 'locator_preview_model_path', text="Path") + row.operator('object.scs_tools_select_preview_model_path', text="", icon='FILEBROWSER') - column_row = layout_box.row().split(percentage=0.3) + # Locator Preview Model Controls + layout.prop(obj.scs_props, 'locator_preview_model_type', ) - column1 = column_row.column() - column1.label("Custom Path:") - column1.label("Custom Name:") +class SCS_TOOLS_PT_Parts(_ObjectPanelBlDefs, Panel): + """Creates 'SCS Parts' settings sub-panel.""" - column2 = column_row.column() + bl_parent_id = SCS_TOOLS_PT_Object.__name__ + bl_label = "SCS Parts" + bl_options = {'DEFAULT_CLOSED'} - row = column2.row(align=True) - skeleton_export_path = active_object.scs_props.scs_skeleton_custom_export_dirpath - row.alert = ((skeleton_export_path != "" and not skeleton_export_path.startswith("//")) or - not os.path.isdir(os.path.join(_get_scs_globals().scs_project_path, - skeleton_export_path.strip("//")))) - if row.alert: - _shared.draw_warning_operator( - row, - "Skeleton Relative Export Path Warning", - str("Current relative export path is unreachable, which may result into an error on export!\n" - "Make sure you did following:\n" - "1. Properly set \"SCS Project Base Path\"\n" - "2. Properly set this relative export path for skeleton which must be relative on \"SCS Project Base Path\"") - ) + @classmethod + def poll(cls, context): + if not _ObjectPanelBlDefs.poll(context): + return False - row.prop(active_object.scs_props, "scs_skeleton_custom_export_dirpath", text="", icon="EXPORT") - props = row.operator('scene.select_directory_inside_base', text='', icon='FILESEL') - props.type = "SkeletonExportPath" + obj = context.active_object + + if obj.type in ('MESH', 'EMPTY'): + + cls.scs_root = _object_utils.get_scs_root(obj) + active_obj_has_part = not (obj.scs_props.empty_object_type == "Locator" and + obj.scs_props.locator_type == "Prefab" and + obj.scs_props.locator_prefab_type != "Sign") + return cls.scs_root and active_obj_has_part + + return False + + @staticmethod + def draw_part_list(layout, root_obj): + """Creates an editable Part list. + + :param layout: Blender UI Layout to draw to + :type layout: bpy.types.UILayout + :param root_obj: SCS Root Object + :type root_obj: bpy.types.Object + """ + row = layout.row() + row.template_list( + SCS_TOOLS_UL_ObjectPartSlot.__name__, + list_id="", + dataptr=root_obj, + propname="scs_object_part_inventory", + active_dataptr=root_obj.scs_props, + active_propname="active_scs_part", + rows=4, + maxrows=5, + type='DEFAULT', + columns=9 + ) - column2.prop(active_object.scs_props, "scs_skeleton_custom_name", text="", icon="FILE_TEXT") - else: - layout_box_row = layout_box.row() - layout_box_row.prop(scene.scs_props, 'scs_skeleton_panel_expand', text="SCS Skeleton:", - icon='TRIA_RIGHT', icon_only=True, emboss=False) - layout_box_row.label('') - - -def _draw_icon_part_tools(layout, index): - """Draws Part Tools icons in a line. - - :param layout: Blender UI Layout to draw to - :type layout: bpy.types.UILayout - :param index: index of part in parts inventory - :type index: int - """ - props = layout.operator('object.switch_part_selection', text="", emboss=False, icon='RESTRICT_SELECT_OFF') - props.part_index = index - props.select_type = _OP_consts.SelectionType.undecided - props = layout.operator('object.switch_part_visibility', text="", emboss=False, icon='RESTRICT_VIEW_OFF') - props.part_index = index - props.view_type = _OP_consts.ViewType.undecided - - -def _draw_icon_variant_tools(layout, index): - """Draws Variant Tools icons in a line. - - :param layout: Blender UI Layout to draw to - :type layout: bpy.types.UILayout - :param index: index of variant in variants inventory - :type index: int - """ - props = layout.operator('object.switch_variant_selection', text="", emboss=False, icon='RESTRICT_SELECT_OFF') - props.variant_index = index - props.select_type = _OP_consts.SelectionType.undecided - props = layout.operator('object.switch_variant_visibility', text="", emboss=False, icon='RESTRICT_VIEW_OFF') - props.variant_index = index - props.view_type = _OP_consts.ViewType.undecided - - -def _draw_icon_variant_tools_line(layout, index): - """Creates a line with minimalistic Variant Tools. - - :param layout: Blender UI Layout to draw to - :type layout: bpy.types.UILayout - :param index: index of active variant - :type index: int - """ - row = layout.row(align=True) - col = row.column() - col.alignment = 'LEFT' - col.label("Additional Variant Tools:") - col = row.column() - col_row = col.row(align=True) - col_row.alignment = 'RIGHT' - _draw_icon_variant_tools(col_row, index) - - -def _draw_part_list(layout, root_obj): - """Creates an editable Part list. - - :param layout: Blender UI Layout to draw to - :type layout: bpy.types.UILayout - :param root_obj: SCS Root Object - :type root_obj: bpy.types.Object - """ - row = layout.row() - row.template_list( - 'SCSObjectPartSlots', - list_id="", - dataptr=root_obj, - propname="scs_object_part_inventory", - active_dataptr=root_obj.scs_props, - active_propname="active_scs_part", - rows=3, - maxrows=5, - type='DEFAULT', - columns=9 - ) - - # LIST BUTTONS - col = row.column(align=True) - col.operator('object.add_scs_part', text="", icon='ZOOMIN') - col.operator('object.remove_scs_part', text="", icon='ZOOMOUT') - col.operator('object.clean_scs_parts', text="", icon='FILE_REFRESH') - - -def _draw_scs_part_panel(layout, scene, active_object, scs_root_object): - """Creates 'SCS Parts' settings sub-panel. - - :param layout: Blender UI Layout to draw to - :type layout: bpy.types.UILayout - :param scene: Blender Scene - :type scene: bpy.types.Scene - :param active_object: active object - :type active_object: bpy.types.Object - :param scs_root_object: SCS Root Object - :type scs_root_object: bpy.types.Object - """ - - layout_column = layout.column(align=True) - layout_box = layout_column.box() - - if scene.scs_props.scs_part_panel_expand: - - # HEADER (COLLAPSIBLE - OPENED) - row = layout_box.row() - row.prop(scene.scs_props, 'scs_part_panel_expand', text="SCS Parts:", icon='TRIA_DOWN', icon_only=True, emboss=False) - row.prop(scene.scs_props, 'scs_part_panel_expand', text=" ", icon='NONE', icon_only=True, emboss=False) - - layout_box = layout_column.box() # body box + # LIST BUTTONS + col = row.column(align=True) + col.operator('object.scs_tools_add_part', text="", icon='ADD') + col.operator('object.scs_tools_remove_active_part', text="", icon='REMOVE') + col.operator('object.scs_tools_clean_unused_parts', text="", icon='FILE_REFRESH') + col.operator('object.scs_tools_move_active_part', text="", icon='TRIA_UP').move_direction = _OP_consts.InventoryMoveType.move_up + col.operator('object.scs_tools_move_active_part', text="", icon='TRIA_DOWN').move_direction = _OP_consts.InventoryMoveType.move_down + + def draw(self, context): + layout = self.get_layout() + scs_root_object = self.scs_root + active_object = context.active_object if len(_object_utils.gather_scs_roots(bpy.context.selected_objects)) > 1 and active_object is not scs_root_object: - col = layout_box.box().column(align=True) - row = col.row() - row.label("WARNING", icon="ERROR") - row = col.row() - row.label("Can not edit parts! Selection has multiple game objects.") + warning_box = layout.column(align=True) + + header = warning_box.box() + header.label(text="WARNING", icon='ERROR') + body = warning_box.box() + col = body.column(align=True) + col.label(text="Can not edit parts!") + col.label(text="Selection has multiple game objects.") else: # more roots or active object is root object # DEBUG if int(_get_scs_globals().dump_level) > 2 and not active_object is scs_root_object: - row = layout_box.row(align=True) + row = layout.row(align=True) row.enabled = False - row.label("DEBUG - active obj part:") + row.label(text="DEBUG - active obj part:") row.prop(active_object.scs_props, 'scs_part', text="") # PART LIST - _draw_part_list(layout_box, scs_root_object) + self.draw_part_list(layout, scs_root_object) # ASSIGNEMENT CONTROLS - if not active_object is scs_root_object: + if active_object is not scs_root_object: - row = layout_box.row(align=True) - row.operator("object.assign_scs_part", text="Assign") - props = row.operator("object.switch_part_selection", text="Select") + row = layout.row(align=True) + row.operator("object.scs_tools_assign_part", text="Assign") + props = row.operator("object.scs_tools_de_select_objects_with_part", text="Select") props.part_index = scs_root_object.scs_props.active_scs_part props.select_type = _OP_consts.SelectionType.shift_select - props = row.operator("object.switch_part_selection", text="Deselect") + props = row.operator("object.scs_tools_de_select_objects_with_part", text="Deselect") props.part_index = scs_root_object.scs_props.active_scs_part props.select_type = _OP_consts.SelectionType.ctrl_select - else: - row = layout_box.row() - row.prop(scene.scs_props, 'scs_part_panel_expand', text="SCS Parts:", icon='TRIA_RIGHT', icon_only=True, emboss=False) - row.prop(scene.scs_props, 'scs_part_panel_expand', text=" ", icon='NONE', icon_only=True, emboss=False) - - -def _draw_part_list_for_variant(layout, scene, variant): - """Creates 'SCS Part' items for provided 'SCS Variant'. - - :param layout: Blender UI Layout to draw to - :type layout: bpy.types.UILayout - :param scene: Blender Scene - :type scene: bpy.types.Scene - :param variant: Variant from the SCS Root Object - :type variant: io_scs_tools.properties.object.ObjectVariantInventoryItem - """ - if scene.scs_props.part_list_sorted: - inventory_names = [] - for part in variant.parts: - inventory_names.append(part.name) - for name in sorted(inventory_names): - part = variant.parts[name] - layout.prop(part, 'include', text=part.name, toggle=True) - else: - for part in variant.parts: - layout.prop(part, 'include', text=part.name, toggle=True) - - -def _draw_vertical_variant_block(layout, scene, variant): - """Creates vertical 'SCS Variant' list. - - :param layout: Blender UI Layout to draw to - :type layout: bpy.types.UILayout - :param scene: Blender Scene - :type scene: bpy.types.Scene - :param variant: Variant from the SCS Root Object - :type variant: io_scs_tools.properties.object.ObjectVariantInventoryItem - """ - layout_box = layout.box() - row = layout_box.row() - row.alignment = 'CENTER' - # row.prop_enum(scene.scs_props, 'part_variants', variant.name) - # row.label('[ ' + variant.name + ' ]', icon='NONE') - row.label(variant.name, icon='NONE') - row = layout_box.row() - row.separator() - row.separator() - box = row.box() - col = box.column(align=True) - _draw_part_list_for_variant(col, scene, variant) - row.separator() - row.separator() - - -''' -def draw_horizontal_variant_block(layout, scene, variant): -"""Creates horizontal 'SCS Variant' list.""" - variantEntryBox = layout.box() - variantItem = variantEntryBox.row() - variantItem.alignment = 'CENTER' - variantItem.label(variant.name, icon='NONE') - variantEntryPart = variantEntryBox.column(align=True) - draw_part_list_for_variant(variantEntryPart, scene, variant) -''' - - -def _draw_horizontal_scs_variant_block(layout, variant): - """Creates horizontal 'SCS Variant' list. - - :param layout: Blender UI Layout to draw to - :type layout: bpy.types.UILayout - :param variant: Variant from the SCS Root Object - :type variant: io_scs_tools.properties.object.ObjectVariantInventoryItem - """ - box = layout.box() - row = box.row() - row.alignment = 'CENTER' - row.label(variant.name, icon='NONE') - col = box.column(align=True) - _draw_part_list_for_variant(col, bpy.context.scene, variant) - - -def _draw_scs_root_panel(layout, scene, obj): - """Creates 'SCS Root Object' settings sub-panel. - - :param layout: Blender UI Layout to draw to - :type layout: bpy.types.UILayout - :param scene: Blender Scene - :type scene: bpy.types.Scene - :param obj: SCS Root Object - :type obj: bpy.types.Object - """ - box = layout.box() - if scene.scs_props.scs_root_panel_settings_expand: - row = box.row() - row.prop(scene.scs_props, 'scs_root_panel_settings_expand', text="Root Object:", icon='TRIA_DOWN', icon_only=True, emboss=False) - row.label('') - col = box.column(align=True) - if obj.scs_props.scs_root_object_export_enabled: - icon = "FILE_TICK" - else: - icon = "X" - col.prop(obj.scs_props, 'scs_root_object_export_enabled', text="Export Inclusion", icon=icon, toggle=True) - row = col.row(align=True) - row.enabled = obj.scs_props.scs_root_object_export_enabled - row.prop(obj.scs_props, 'scs_root_animated', expand=True) - col = box.column() - col.enabled = obj.scs_props.scs_root_object_export_enabled - - # Global Export Filepath (DIR_PATH - absolute) - row = col.row(align=True) - row.prop(obj.scs_props, 'scs_root_object_allow_custom_path', text='', icon='NONE') - row2 = row.row(align=True) - row2.enabled = obj.scs_props.scs_root_object_allow_custom_path - if row2.enabled: - root_export_path = obj.scs_props.scs_root_object_export_filepath - row2.alert = ((root_export_path != "" and not root_export_path.startswith("//")) or - not os.path.isdir(os.path.join(_get_scs_globals().scs_project_path, - obj.scs_props.scs_root_object_export_filepath.strip("//")))) - if row2.alert: - _shared.draw_warning_operator( - row2, - "Custom Export Path Warning", - str("Current custom SCS Game Object filepath is unreachable, which may result into an error on export!\n" - "Make sure you did following:\n" - "1. Properly set \"SCS Project Base Path\"\n" - "2. Properly set this custom export path which must be relative on \"SCS Project Base Path\"") - ) - row2.prop(obj.scs_props, 'scs_root_object_export_filepath', text='', icon='EXPORT') - props = row2.operator('scene.select_directory_inside_base', text='', icon='FILESEL') - props.type = "GameObjectExportPath" - else: - row = box.row() - row.prop(scene.scs_props, 'scs_root_panel_settings_expand', text="Root Object:", icon='TRIA_RIGHT', icon_only=True, emboss=False) - row.label('') - - -def _draw_empty_object_panel(layout, context, scene, obj, enabled=True): - """Creates 'Empty Object' settings sub-panel. - - :param layout: Blender UI Layout to draw to - :type layout: bpy.types.UILayout - :param context: Blender Context - :type context: bpy.context - :param scene: Blender Scene - :type scene: bpy.types.Scene - :param obj: Blender Empty Object - :type obj: bpy.types.Object - :param enabled: if locator type selecting shall be disabled - :type enabled: bool - """ - layout_column = layout.column(align=True) - box = layout_column.box() - if scene.scs_props.empty_object_settings_expand: - row = box.row() - row.prop(scene.scs_props, 'empty_object_settings_expand', text="SCS Objects:", icon='TRIA_DOWN', icon_only=True, emboss=False) - row.label('') - box = layout_column.box() - row = box.row() - row.enabled = enabled - row.prop(obj.scs_props, 'empty_object_type', text="Object Type", icon='NONE') - # SCS ROOT PANEL - if obj.scs_props.empty_object_type == 'SCS_Root': - _draw_scs_root_panel(box, scene, obj) +class SCS_TOOLS_PT_Variants(_ObjectPanelBlDefs, Panel): + """Creates 'SCS Variants' settings sub-panel.""" - # LOCATOR PANEL - elif obj.scs_props.empty_object_type == 'Locator': - _draw_locator_panel(box, context, scene, obj, enabled=enabled) - else: - row = box.row() - row.prop(scene.scs_props, 'empty_object_settings_expand', text="SCS Objects:", icon='TRIA_RIGHT', icon_only=True, emboss=False) - row.label('') + bl_parent_id = SCS_TOOLS_PT_Object.__name__ + bl_label = "SCS Variants" + bl_options = {'DEFAULT_CLOSED'} + @classmethod + def poll(cls, context): + if not _ObjectPanelBlDefs.poll(context): + return False -def _draw_scs_variant_panel(layout, scene, scs_root_object): - """Creates 'SCS Variants' settings sub-panel. + return context.active_object.scs_props.empty_object_type == 'SCS_Root' - :param layout: Blender UI Layout to draw to - :type layout: bpy.types.UILayout - :param scene: Blender Scene - :type scene: bpy.types.Scene - :param scs_root_object: Blender SCS Root object - :type scs_root_object: bpy.types.Object - """ + @staticmethod + def draw_icon_variant_tools(layout, index): + """Draws Variant Tools icons in a line. - layout_column = layout.column(align=True) - box = layout_column.box() - - variant_inventory = scs_root_object.scs_object_variant_inventory + :param layout: Blender UI Layout to draw to + :type layout: bpy.types.UILayout + :param index: index of variant in variants inventory + :type index: int + """ + props = layout.operator('object.scs_tools_de_select_objects_with_variant', text="", emboss=False, icon='RESTRICT_SELECT_OFF') + props.variant_index = index + props.select_type = _OP_consts.SelectionType.undecided + props = layout.operator('object.scs_tools_switch_objects_with_variant', text="", emboss=False, icon='HIDE_OFF') + props.variant_index = index + props.view_type = _OP_consts.ViewType.undecided + + @staticmethod + def draw_icon_variant_tools_line(layout, index): + """Creates a line with minimalistic Variant Tools. + + :param layout: Blender UI Layout to draw to + :type layout: bpy.types.UILayout + :param index: index of active variant + :type index: int + """ + row = layout.row(align=True) + col = row.column() + col.alignment = 'LEFT' + col.label(text="Additional Variant Tools:") + col = row.column() + col_row = col.row(align=True) + col_row.alignment = 'RIGHT' + SCS_TOOLS_PT_Variants.draw_icon_variant_tools(col_row, index) + + @staticmethod + def draw_part_list_for_variant(layout, workspace, variant): + """Creates 'SCS Part' items for provided 'SCS Variant'. + + :param layout: Blender UI Layout to draw to + :type layout: bpy.types.UILayout + :param workspace: Blender WorkSpace + :type workspace: bpy.types.WorkSpace + :param variant: Variant from the SCS Root Object + :type variant: io_scs_tools.properties.object.ObjectVariantInventoryItem + """ + if workspace.scs_props.part_list_sorted: + inventory_names = [] + for part in variant.parts: + inventory_names.append(part.name) + for name in sorted(inventory_names): + part = variant.parts[name] + layout.prop(part, 'include', text=part.name, toggle=True) + else: + for part in variant.parts: + layout.prop(part, 'include', text=part.name, toggle=True) + + @staticmethod + def draw_vertical_variant_block(layout, workspace, variant): + """Creates vertical 'SCS Variant' list. + + :param layout: Blender UI Layout to draw to + :type layout: bpy.types.UILayout + :param workspace: Blender WorkSpace + :type workspace: bpy.types.WorkSpace + :param variant: Variant from the SCS Root Object + :type variant: io_scs_tools.properties.object.ObjectVariantInventoryItem + """ + layout_column = layout.column(align=True) + header = layout_column.box() + row = header.row(align=True) + row.alignment = 'CENTER' + row.label(text=variant.name) + + body = layout_column.box() + col = body.column(align=True) + SCS_TOOLS_PT_Variants.draw_part_list_for_variant(col, workspace, variant) + + @staticmethod + def draw_horizontal_scs_variant_block(layout, workspace, variant): + """Creates horizontal 'SCS Variant' list. + + :param layout: Blender UI Layout to draw to + :type layout: bpy.types.UILayout + :param workspace: Blender WorkSpace + :type workspace: bpy.types.WorkSpace + :param variant: Variant from the SCS Root Object + :type variant: io_scs_tools.properties.object.ObjectVariantInventoryItem + """ + box = layout.box() + row = box.row(align=True) + row.alignment = 'CENTER' + row.label(text=variant.name) - if scene.scs_props.scs_variant_panel_expand: + box = layout.box() + col = box.column(align=True) + SCS_TOOLS_PT_Variants.draw_part_list_for_variant(col, workspace, variant) - # HEADER (COLLAPSIBLE - OPENED) - split = box.split(percentage=0.5) - row = split.row() - row.prop(scene.scs_props, 'scs_variant_panel_expand', text="SCS Variants:", icon='TRIA_DOWN', icon_only=True, emboss=False) + def draw_header_preset(self, context): + layout = self.layout + workspace = context.workspace + layout.prop(workspace.scs_props, 'variant_views', text="", expand=True, toggle=True, emboss=True) - row = split.row(align=True) - row.alignment = 'RIGHT' - row.prop(scene.scs_props, 'variant_views', text='', icon='NONE', expand=True, toggle=True) + def draw(self, context): + layout = self.get_layout() + workspace = context.workspace + scs_root = context.active_object - box = layout_column.box() # body box + variant_inventory = scs_root.scs_object_variant_inventory # VARIANT LIST - row = box.row() + row = layout.row() col = row.column() col.template_list( - 'SCSObjectVariantSlots', + SCS_TOOLS_UL_ObjectVariantSlot.__name__, list_id="", - dataptr=scs_root_object, + dataptr=scs_root, propname="scs_object_variant_inventory", - active_dataptr=scs_root_object.scs_props, + active_dataptr=scs_root.scs_props, active_propname="active_scs_variant", - rows=1, - maxrows=5, + rows=3, + maxrows=6, type='DEFAULT', columns=9 ) - if scene.scs_props.variant_views == 'integrated': + if workspace.scs_props.variant_views == 'integrated': # VARIANT TOOLS FOR INTEGRATED LIST - _draw_icon_variant_tools_line(col, scs_root_object.scs_props.active_scs_variant) + self.draw_icon_variant_tools_line(col, scs_root.scs_props.active_scs_variant) # LIST BUTTONS col = row.column(align=True) - col.operator('object.add_scs_variant', text="", icon='ZOOMIN') - col.operator('object.remove_scs_variant', text="", icon='ZOOMOUT') + col.operator('object.scs_tools_add_variant', text="", icon='ADD') + col.operator('object.scs_tools_remove_active_variant', text="", icon='REMOVE') + col.operator('object.scs_tools_move_active_variant', text="", icon='TRIA_UP').move_direction = _OP_consts.InventoryMoveType.move_up + col.operator('object.scs_tools_move_active_variant', text="", icon='TRIA_DOWN').move_direction = _OP_consts.InventoryMoveType.move_down # VARIANT-PART LIST if len(variant_inventory) > 0: - if scene.scs_props.variant_views != 'integrated': + # VARIANT-PART LIST HEADER + if workspace.scs_props.variant_views != 'integrated': - box = box.box() - split = box.split(percentage=0.5) + col = layout.column() + split = col.split(factor=0.5) split1 = split.row() - split1.label("Variant-Part Table:", icon="MESH_GRID") + split1.label(text="Variant-Part Table:", icon='MESH_GRID') split2 = split.row(align=True) split2.alignment = 'RIGHT' - split2.prop(scene.scs_props, 'part_list_sorted', text='Parts', icon='SORTALPHA', expand=True, toggle=True) + split2.prop(workspace.scs_props, 'part_list_sorted', text='Parts', icon='SORTALPHA', expand=True, toggle=True) - if scene.scs_props.variant_views in ('vertical', 'horizontal'): - split2.prop(scene.scs_props, 'variant_list_sorted', text='Variants', icon='SORTALPHA', expand=True, toggle=True) + if workspace.scs_props.variant_views in ('vertical', 'horizontal'): + split2.prop(workspace.scs_props, 'variant_list_sorted', text='Variants', icon='SORTALPHA', expand=True, toggle=True) - if scene.scs_props.variant_views == 'compact': + if workspace.scs_props.variant_views == 'compact': # COMPACT LIST - if variant_inventory: + row = layout.row() + col = row.column(align=True) - row = box.row() - col = row.column(align=True) + active_scs_variant = scs_root.scs_props.active_scs_variant + self.draw_part_list_for_variant(col, workspace, variant_inventory[active_scs_variant]) - active_scs_variant = scs_root_object.scs_props.active_scs_variant - _draw_part_list_for_variant(col, scene, variant_inventory[active_scs_variant]) - else: + elif workspace.scs_props.variant_views == 'vertical': # VERTICAL LIST - if scene.scs_props.variant_views == 'vertical': - if scene.scs_props.variant_list_sorted: - inventory_names = [] - for variant in variant_inventory: - inventory_names.append(variant.name) - for name in sorted(inventory_names): - variant = variant_inventory[name] - _draw_vertical_variant_block(box, scene, variant) - else: - for variant in variant_inventory: - _draw_vertical_variant_block(box, scene, variant) + col = layout.column(align=False) + if workspace.scs_props.variant_list_sorted: + inventory_names = [] + for variant in variant_inventory: + inventory_names.append(variant.name) + for name in sorted(inventory_names): + variant = variant_inventory[name] + self.draw_vertical_variant_block(col, workspace, variant) + else: + for variant in variant_inventory: + self.draw_vertical_variant_block(col, workspace, variant) + + elif workspace.scs_props.variant_views == 'horizontal': # HORIZONTAL LIST - elif scene.scs_props.variant_views == 'horizontal': - row = box.row() - if scene.scs_props.variant_list_sorted: - inventory_names = [] - for variant in variant_inventory: - inventory_names.append(variant.name) - for name in sorted(inventory_names): - variant = variant_inventory[name] - _draw_horizontal_scs_variant_block(row, variant) - else: - for variant in variant_inventory: - _draw_horizontal_scs_variant_block(row, variant) - - else: - - # HEADER (COLLAPSIBLE - CLOSED) - row = box.row() - row.prop(scene.scs_props, 'scs_variant_panel_expand', text="SCS Variants:", icon='TRIA_RIGHT', icon_only=True, emboss=False) - row.label('') - - -class SCSObjectLookSlots(bpy.types.UIList): - """ - Draw look item slot within SCS Looks list - """ + row = layout.row(align=True) + if workspace.scs_props.variant_list_sorted: + inventory_names = [] + for variant in variant_inventory: + inventory_names.append(variant.name) + for name in sorted(inventory_names): + variant = variant_inventory[name] + self.draw_horizontal_scs_variant_block(row.column(align=True), workspace, variant) + else: + for variant in variant_inventory: + self.draw_horizontal_scs_variant_block(row.column(align=True), workspace, variant) - def draw_item(self, context, layout, data, item, icon, active_data, active_property, index): - layout.prop(item, "name", text="", emboss=False, icon_value=icon) - # DEBUG - if int(_get_scs_globals().dump_level) > 2: - layout.label("DEBUG - id: " + str(item.id)) +class SCS_TOOLS_PT_LooksOnObject(_ObjectPanelBlDefs, Panel): + """Draws SCS Looks panel on object tab.""" + bl_parent_id = SCS_TOOLS_PT_Object.__name__ + bl_label = "SCS Looks" + bl_options = {'DEFAULT_CLOSED'} -class SCSObjectPartSlots(bpy.types.UIList): - """ - Draw part item slot within SCS Parts list - """ + @classmethod + def poll(cls, context): + if not _ObjectPanelBlDefs.poll(context): + return False - def draw_item(self, context, layout, data, item, icon, active_data, active_property, index): - # assert(isinstance(item, bpy.types.MaterialSlot) - if self.layout_type in {'DEFAULT', 'COMPACT'}: - if item: - line = layout.split(percentage=0.6, align=False) - line.prop(item, "name", text="", emboss=False, icon_value=icon) - tools = line.row(align=True) - tools.alignment = 'RIGHT' - _draw_icon_part_tools(tools, index) - else: - layout.label(text="", icon_value=icon) - elif self.layout_type in {'GRID'}: - layout.alignment = 'CENTER' - layout.label(text="", icon_value=icon) + return context.active_object.scs_props.empty_object_type == 'SCS_Root' + def draw(self, context): + layout = self.get_layout() + obj = context.active_object -class SCSObjectVariantSlots(bpy.types.UIList): - """ - Draw variant item slot within SCS Variants list - """ + _shared.draw_scs_looks_panel(layout, obj, obj, without_box=True) - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - # assert(isinstance(item, bpy.types.MaterialSlot) - if self.layout_type in {'DEFAULT', 'COMPACT'}: - if item: - if context.scene.scs_props.variant_views == 'integrated': - layout.prop(item, "name", text="", emboss=False, icon_value=icon) - line = layout.row(align=True) - for part in item.parts: - line.prop(part, 'include', text=part.name, toggle=True) - else: - line = layout.split(percentage=0.6, align=False) - line.prop(item, "name", text="", emboss=False, icon_value=icon) - tools = line.row(align=True) - tools.alignment = 'RIGHT' - _draw_icon_variant_tools(tools, index) + +class SCS_TOOLS_PT_Animations(_ObjectPanelBlDefs, Panel): + """Draw Animation settings panel.""" + + bl_parent_id = SCS_TOOLS_PT_Object.__name__ + bl_label = "SCS Animations" + + @classmethod + def poll(cls, context): + if not _ObjectPanelBlDefs.poll(context): + return False + + cls.scs_root = _object_utils.get_scs_root(context.active_object) + return context.active_object.type == 'ARMATURE' + + def draw_animation_section(self, layout, active_obj): + """Draw Animation section. + + :param layout: Blender UI Layout to draw to + :type layout: bpy.types.UILayout + :param active_obj: operator context + :type active_obj: bpy.types.Object + """ + scs_object_animation_inventory = self.scs_root.scs_object_animation_inventory + active_scs_anim_i = self.scs_root.scs_props.active_scs_animation + if len(scs_object_animation_inventory) > active_scs_anim_i: + + layout.label(text="Active Animation Settings:", icon='ANIM_DATA') + + active_scs_anim = scs_object_animation_inventory[active_scs_anim_i] + + action_col = layout.column(align=True) + + row = action_col.row(align=True) + icon = 'NONE' if active_scs_anim.action in bpy.data.actions else 'ERROR' + row.prop_search(active_scs_anim, 'action', bpy.data, 'actions', text="", icon=icon) + + row = action_col.row(align=True) + row.enabled = active_scs_anim.action in bpy.data.actions + row.prop(active_scs_anim, 'anim_start', text="Start") + row.prop(active_scs_anim, 'anim_end', text="End") + row.prop(active_scs_anim, 'length', text="Length") + + if active_obj.animation_data and active_obj.animation_data.action: + row = action_col.row(align=True) + row.operator('scene.scs_tools_increase_animation_steps', text="", icon='ADD') + row.operator('scene.scs_tools_decrease_animation_steps', text="", icon='REMOVE') + row.prop(active_obj.animation_data.action.scs_props, "anim_export_step") + + def draw(self, context): + layout_column = self.layout + scs_root = self.scs_root + + # layout_column = layout.column(align=True) + + if scs_root: + + # ANIMATIONS CUSTOM EXPORT PATH + column = layout_column.column(align=True) + if self.scs_root.scs_props.scs_root_object_allow_anim_custom_path: + text = "Custom Export Path Enabled" else: - layout.label(text="", icon_value=icon) - elif self.layout_type in {'GRID'}: - layout.alignment = 'CENTER' - layout.label(text="", icon_value=icon) + text = "Custom Export Path Disabled" + icon = _shared.get_on_off_icon(self.scs_root.scs_props.scs_root_object_allow_anim_custom_path) + column.prop(scs_root.scs_props, 'scs_root_object_allow_anim_custom_path', text=text, icon=icon, toggle=True) + row2 = column.row(align=True) + row2.enabled = scs_root.scs_props.scs_root_object_allow_anim_custom_path + if row2.enabled: + root_anim_export_path = scs_root.scs_props.scs_root_object_anim_export_filepath + row2.alert = ((root_anim_export_path != "" and not root_anim_export_path.startswith("//")) or + not os.path.isdir(os.path.join(_get_scs_globals().scs_project_path, + scs_root.scs_props.scs_root_object_anim_export_filepath.strip("//")))) + row2.prop(scs_root.scs_props, 'scs_root_object_anim_export_filepath', text="", icon='EXPORT') -class SCSObjectAnimationSlots(bpy.types.UIList): - """ - Draw animation item slot within SCS Animation list - """ + if row2.alert: + _shared.draw_warning_operator( + row2, + "Custom Export Path Warning", + str("Current custom Animations filepath is unreachable, which may result into an error on export!\n" + "Make sure you did following:\n" + "1. Properly set \"SCS Project Base Path\"\n" + "2. Properly set this custom export path which must be relative on \"SCS Project Base Path\"") + ) - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - # assert(isinstance(item, bpy.types.MaterialSlot) - if self.layout_type in {'DEFAULT', 'COMPACT'}: - if item: - line = layout.split(percentage=0.6, align=False) + props = row2.operator('scene.scs_tools_select_dir_inside_base', text="", icon='FILEBROWSER') + props.type = "GameObjectAnimExportPath" - line.prop(item, "name", text="", emboss=False, icon_value=icon) + # ANIMATION LIST + layout_setting = layout_column.row() + layout_setting.template_list( + SCS_TOOLS_UL_ObjectAnimationSlot.__name__, + list_id="", + dataptr=scs_root, + propname="scs_object_animation_inventory", + active_dataptr=scs_root.scs_props, + active_propname="active_scs_animation", + rows=4, + maxrows=10, + type='DEFAULT', + columns=9, + ) - extra_ops = line.row(align=True) - extra_ops.alignment = 'RIGHT' + # LIST BUTTONS + list_buttons = layout_setting.column(align=True) + list_buttons.operator('object.scs_tools_add_animation', text="", icon='ADD') + list_buttons.operator('object.scs_tools_remove_animation', text="", icon='REMOVE') + list_buttons.separator() + list_buttons.operator('scene.scs_tools_import_anim_actions', text="", icon='IMPORT') - props = extra_ops.operator("scene.export_scs_anim_action", text="", icon="EXPORT", emboss=False) - props.index = index - extra_ops.prop(item, "export", text="Export") + # ANIMATION SETTINGS + if len(scs_root.scs_object_animation_inventory) > 0: + self.draw_animation_section(layout_column, context.active_object) else: - layout.label(text="", icon_value=icon) - elif self.layout_type in {'GRID'}: - layout.alignment = 'CENTER' - layout.label(text="", icon_value=icon) + layout_column.label(text="No Animation!", icon='INFO') + else: + layout_column.label(text="No 'SCS Root Object'!", icon='INFO') -class SCSTools(_ObjectPanelBlDefs, Panel): - """ - Creates "SCS Object Specials" panel in the Object properties window. - """ - bl_label = "SCS Object Specials" - bl_idname = "OBJECT_PT_SCS_specials" - bl_context = "object" +class SCS_TOOLS_PT_AnimPlayer(_ObjectPanelBlDefs, Panel): + """Draws Animation Player panel.""" - def draw(self, context): - """UI draw function. + bl_parent_id = SCS_TOOLS_PT_Animations.__name__ + bl_label = "Animation Player" + bl_options = {'DEFAULT_CLOSED'} - :param context: Blender Context - :type context: bpy.context - """ - layout = self.layout + def draw(self, context): + layout = self.get_layout() + context = bpy.context scene = context.scene - obj = context.object - other_obj = _object_utils.get_other_object(context.selected_objects, obj) + screen = context.screen - # print('obj: %s\t- %s' % (obj, context.object)) + # layout_box_row = layout_box.row() + # layout_row = layout_box_row.row(align=True) + # if not scene.use_preview_range: + # layout_row.prop(scene, "frame_start", text="Start") + # layout_row.prop(scene, "frame_end", text="End") + # else: + # layout_row.prop(scene, "frame_preview_start", text="Start") + # layout_row.prop(scene, "frame_preview_end", text="End") + # layout_row = layout_box_row.row() + # layout_row.scale_x = 0.7 + # layout_row.prop(scene, "frame_current", text="") - # ## SCS OBJECT SPECIALS PANEL ################ + layout_box_row = layout.row() + layout_col = layout_box_row.column(align=True) + layout_col.prop(scene, "frame_preview_start", text="Start") + layout_col.prop(scene, "frame_preview_end", text="End") + layout_col = layout_box_row.column(align=True) + layout_col.prop(scene, "frame_current", text='Current') + layout_col.prop(scene.render, "fps") - # TEST & DEBUG PANEL - # draw_test_panel(layout) + layout_box_row = layout.row() + layout_box_row.prop(scene, "use_preview_range", text="", toggle=True) + layout_row = layout_box_row.row(align=True) + layout_row.scale_x = 1.3 + layout_row.operator("screen.frame_jump", text="", icon='REW').end = False + layout_row.operator("screen.keyframe_jump", text="", icon='PREV_KEYFRAME').next = False + sub = layout_row.row(align=True) + sub.scale_x = 10 + if not screen.is_animation_playing: + if scene.sync_mode == 'AUDIO_SYNC' and context.user_preferences.system.audio_device == 'JACK': + sub.operator("screen.animation_play", text="", icon='PLAY') + else: + sub.operator("screen.animation_play", text="", icon='PLAY_REVERSE').reverse = True + sub.operator("screen.animation_play", text="", icon='PLAY') + else: + sub.operator("screen.animation_play", text="", icon='PAUSE') + layout_row.operator("screen.keyframe_jump", text="", icon='NEXT_KEYFRAME').next = True + layout_row.operator("screen.frame_jump", text="", icon='FF').end = True - if obj.type in ('MESH', 'EMPTY'): - scs_root_obj = _object_utils.get_scs_root(obj) +class SCS_TOOLS_PT_Skeleton(_ObjectPanelBlDefs, Panel): + """Draws skeleton properties panel.""" - active_obj_has_part = not (obj.scs_props.empty_object_type == "Locator" and - obj.scs_props.locator_type == "Prefab" and - obj.scs_props.locator_prefab_type != "Sign") - if scs_root_obj and active_obj_has_part: + bl_parent_id = SCS_TOOLS_PT_Object.__name__ + bl_label = "SCS Skeleton" - # SCS PART - _draw_scs_part_panel(layout, scene, obj, scs_root_obj) + @classmethod + def poll(cls, context): + if not _ObjectPanelBlDefs.poll(context): + return False + + return context.active_object.type == 'ARMATURE' + + def draw(self, context): + layout = self.get_layout() + active_object = bpy.context.active_object + + layout.use_property_split = True + layout.use_property_decorate = False + + row = layout.row(align=True) + skeleton_export_path = active_object.scs_props.scs_skeleton_custom_export_dirpath + row.alert = ((skeleton_export_path != "" and not skeleton_export_path.startswith("//")) or + not os.path.isdir(os.path.join(_get_scs_globals().scs_project_path, + skeleton_export_path.strip("//")))) + row.prop(active_object.scs_props, "scs_skeleton_custom_export_dirpath", text="Custom Path", icon='EXPORT') + if row.alert: + _shared.draw_warning_operator( + row, + "Skeleton Relative Export Path Warning", + str("Current relative export path is unreachable, which may result into an error on export!\n" + "Make sure you did following:\n" + "1. Properly set \"SCS Project Base Path\"\n" + "2. Properly set this relative export path for skeleton which must be relative on \"SCS Project Base Path\"") + ) + props = row.operator('scene.scs_tools_select_dir_inside_base', text="", icon='FILEBROWSER') + props.type = "SkeletonExportPath" - if obj == scs_root_obj: + layout.prop(active_object.scs_props, "scs_skeleton_custom_name", text="Custom Name", icon='FILE_TEXT') - # SCS VARIANT PANEL - _draw_scs_variant_panel(layout, scene, scs_root_obj) - # SCS LOOK PANEL - _shared.draw_scs_looks_panel(layout, scene, obj, scs_root_obj) +classes = ( + SCS_TOOLS_UL_ObjectPartSlot, + SCS_TOOLS_UL_ObjectVariantSlot, + SCS_TOOLS_UL_ObjectAnimationSlot, + SCS_TOOLS_PT_Object, + SCS_TOOLS_PT_RootObject, + SCS_TOOLS_PT_Locator, + SCS_TOOLS_PT_PreviewModel, + SCS_TOOLS_PT_Parts, + SCS_TOOLS_PT_Variants, + SCS_TOOLS_PT_LooksOnObject, + SCS_TOOLS_PT_Animations, + SCS_TOOLS_PT_AnimPlayer, + SCS_TOOLS_PT_Skeleton, +) - if _object_utils.can_assign_terrain_points(context): - _draw_empty_object_panel(layout, context, scene, other_obj, enabled=False) - # EMPTY OBJECT PANEL - if obj.type == 'EMPTY': - _draw_empty_object_panel(layout, context, scene, obj) +def register(): + for cls in classes: + bpy.utils.register_class(cls) - if obj.type == 'ARMATURE': + from io_scs_tools import SCS_TOOLS_MT_MainMenu + SCS_TOOLS_MT_MainMenu.append_props_entry("Object Properties", SCS_TOOLS_PT_Object.__name__) - # ANIMATION SETTINGS PANEL - _draw_scs_animation_panel(layout) - # SKELETON SETTINGS - _draw_scs_skeleton_panel(layout) +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/addon/io_scs_tools/ui/output.py b/addon/io_scs_tools/ui/output.py new file mode 100644 index 0000000..f11daf8 --- /dev/null +++ b/addon/io_scs_tools/ui/output.py @@ -0,0 +1,239 @@ +# ##### 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 + +import os +import bpy +from bpy.types import Panel, UIList +from io_scs_tools.utils import path as _path_utils +from io_scs_tools.utils import get_scs_globals as _get_scs_globals +from io_scs_tools.ui import shared as _shared + + +class _OutputPanelBlDefs(_shared.HeaderIconPanel): + """ + Defines class for showing in Blender Output Properties window + """ + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "output" + bl_ui_units_x = 15 + + layout = None # predefined Blender variable to avoid warnings in PyCharm + + +class SCS_TOOLS_PT_ExportPanel(_OutputPanelBlDefs, Panel): + """Draw Export panel.""" + + bl_label = "SCS Export" + + def draw(self, context): + layout = self.layout + scene = context.scene + scs_globals = _get_scs_globals() + + # scs tools main panel if config is being updated + layout.enabled = not scs_globals.config_update_lock + + if not scs_globals.preview_export_selection_active: + + col = layout.column(align=True) + row = col.row(align=True) + row.scale_y = 1.2 + + if scs_globals.export_scope == "selection": + icon = _shared.get_on_off_icon(scs_globals.preview_export_selection) + row.prop(scs_globals, 'preview_export_selection', text="", icon=icon, toggle=False) + + row.prop(scs_globals, 'export_scope', expand=True) + + col_row = col.row(align=True) + col_row.scale_y = 2 + col_row.operator('scene.scs_tools_export_by_scope', text="EXPORT") + + else: + + row = layout.box() + row.prop(scs_globals, "preview_export_selection_active", text="Export preview mode is active!", icon='ERROR', icon_only=True, + emboss=False) + row = row.column(align=True) + row.label(text="Rotate view:") + row.label(text="* Left Mouse Button + move") + row.label(text="* Numpad 2, 4, 6, 8") + row.label(text="Zoom in/out:") + row.label(text="* Mouse Scroll") + row.label(text="* Numpad '+', '-'") + row.separator() + row.label(text="Press ENTER to export selection!") + row.label(text="Press ESC to cancel export!") + + col = layout.column() + + # Fallback Export Path (FILE_PATH - relative) + col_row = _shared.create_row(col, use_split=True, enabled=True) + default_export_path = scene.scs_props.default_export_filepath + col_row.alert = ((default_export_path != "" and not default_export_path.startswith("//")) or + not os.path.isdir(os.path.join(scs_globals.scs_project_path, default_export_path.strip("//")))) + col_row.prop(scene.scs_props, 'default_export_filepath', icon='EXPORT') + if col_row.alert: + _shared.draw_warning_operator( + col_row, + "Default Export Path Warning", + str("Current Default Export Path is unreachable, which may result into an error on export!\n" + "Make sure you did following:\n" + "1. Properly set \"SCS Project Base Path\"\n" + "2. Properly set \"Default Export Path\" which must be relative on \"SCS Project Base Path\"") + ) + + props = col_row.operator('scene.scs_tools_select_dir_inside_base', text="", icon='FILEBROWSER') + props.type = "DefaultExportPath" + + _shared.draw_export_panel(layout, ignore_extra_boxes=True) + + +class SCS_TOOLS_UL_ConversionPathsSlot(UIList): + """ + Draw conversion path entry within ui list + """ + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + if item: + icon = 'ERROR' if not os.path.isdir(_path_utils.repair_path(item.path)) else 'NONE' + layout.prop(item, "path", text="", emboss=False, icon=icon) + else: + layout.label(text="", icon_value=icon) + + +class SCS_TOOLS_PT_ConversionHelper(_OutputPanelBlDefs, Panel): + """Draw Conversion Helper panel.""" + + bl_label = "SCS Conversion Helper" + + def draw_header_preset(self, context): + layout = self.layout + layout.operator('scene.scs_tools_clean_conversion_rsrc', text="", icon='SCULPTMODE_HLT') + layout.separator(factor=0.1) + + def draw(self, context): + layout = self.layout + scs_globals = _get_scs_globals() + + # scs tools main panel if config is being updated + layout.enabled = not scs_globals.config_update_lock + + # CONVERSION TOOLS PATH + col = layout.column(align=True) + col.label(text="Conversion Tools Path:", icon='FILE_FOLDER') + row = col.row(align=True) + valid = (os.path.isdir(scs_globals.conv_hlpr_converters_path) and + os.path.isfile(os.path.join(scs_globals.conv_hlpr_converters_path, "extra_mount.txt")) and + os.path.isfile(os.path.join(scs_globals.conv_hlpr_converters_path, "convert.cmd"))) + row.alert = not valid + row.prop(scs_globals, "conv_hlpr_converters_path", text="") + + # CUSTOM PATHS + cstm_paths_col = layout.column(align=not scs_globals.conv_hlpr_use_custom_paths) + row = cstm_paths_col.row(align=True) + row.scale_y = 1.1 + text = "Custom Paths: ON" if scs_globals.conv_hlpr_use_custom_paths else "Custom Paths: OFF" + icon = _shared.get_on_off_icon(scs_globals.conv_hlpr_use_custom_paths) + row.prop(scs_globals, "conv_hlpr_use_custom_paths", text=text, icon=icon, toggle=True) + + if scs_globals.conv_hlpr_use_custom_paths: + row = cstm_paths_col.row() + row.template_list( + SCS_TOOLS_UL_ConversionPathsSlot.__name__, + list_id="", + dataptr=scs_globals, + propname="conv_hlpr_custom_paths", + active_dataptr=scs_globals, + active_propname="conv_hlpr_custom_paths_active", + rows=4, + maxrows=5, + type='DEFAULT', + columns=9 + ) + + side_bar = row.column(align=True) + side_bar.operator("scene.scs_tools_add_conversion_path", text="", icon='ADD') + side_bar.operator("scene.scs_tools_remove_conversion_path", text="", icon='REMOVE') + side_bar.separator() + + props = side_bar.operator("scene.scs_tools_order_conversion_path", icon='TRIA_UP', text="") + props.move_up = True + props = side_bar.operator("scene.scs_tools_order_conversion_path", icon='TRIA_DOWN', text="") + props.move_up = False + + # CONVERT + row = cstm_paths_col.column(align=True) + row.scale_y = 1.1 + row.operator("scene.scs_tools_convert_current_base", text='CONVERT CURRENT SCS PROJECT') + if scs_globals.conv_hlpr_use_custom_paths: + row.operator("scene.scs_tools_convert_custom_paths", text='CONVERT CUSTOM PATHS') + row.operator("scene.scs_tools_convert_all_paths", text='CONVERT ALL') + + # PACKING + col = layout.column(align=True) + col.row().label(text="Mod Package Settings:", icon='PACKAGE') + + row = col.row(align=True) + row.prop(scs_globals, "conv_hlpr_mod_destination", text="") + row.operator("scene.scs_tools_find_game_mod_folder", text="", icon='ZOOM_SELECTED') + + row = col.row(align=True) + row.prop(scs_globals, "conv_hlpr_mod_name", text="") + + row = col.row(align=True) + row.prop(scs_globals, "conv_hlpr_mod_compression", text="") + + col.separator() + + row = col.row(align=True) + row.scale_y = 1.1 + icon = _shared.get_on_off_icon(scs_globals.conv_hlpr_clean_on_packing) + row.prop(scs_globals, "conv_hlpr_clean_on_packing", toggle=True, icon=icon) + icon = _shared.get_on_off_icon(scs_globals.conv_hlpr_export_on_packing) + row.prop(scs_globals, "conv_hlpr_export_on_packing", toggle=True, icon=icon) + icon = _shared.get_on_off_icon(scs_globals.conv_hlpr_convert_on_packing) + row.prop(scs_globals, "conv_hlpr_convert_on_packing", toggle=True, icon=icon) + + row = col.row(align=True) + row.scale_y = 1.5 + row.operator("scene.scs_tools_run_packing", text=">>>> PACK MOD <<<<") + + +classes = ( + SCS_TOOLS_PT_ExportPanel, + SCS_TOOLS_UL_ConversionPathsSlot, + SCS_TOOLS_PT_ConversionHelper, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + from io_scs_tools import SCS_TOOLS_MT_MainMenu + SCS_TOOLS_MT_MainMenu.append_output_entry("Output - Export", SCS_TOOLS_PT_ExportPanel.__name__) + SCS_TOOLS_MT_MainMenu.append_output_entry("Output - Conversion", SCS_TOOLS_PT_ConversionHelper.__name__) + + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/addon/io_scs_tools/ui/scene.py b/addon/io_scs_tools/ui/scene.py deleted file mode 100644 index 6091ad5..0000000 --- a/addon/io_scs_tools/ui/scene.py +++ /dev/null @@ -1,473 +0,0 @@ -# ##### 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) 2013-2014: SCS Software - -import os -from bpy.types import Panel, UIList -from io_scs_tools.utils import path as _path_utils -from io_scs_tools.utils import get_scs_globals as _get_scs_globals -from io_scs_tools.ui import shared as _shared - - -class _ScenePanelBlDefs(_shared.HeaderIconPanel): - """ - Defines class for showing in Blender Scene Properties window - """ - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "scene" - bl_label = "" - - layout = None # predefined Blender variable to avoid warnings in PyCharm - - -def _draw_path_settings_panel(scene, layout, scs_globals): - """Draw global path settings panel. - - :param layout: - :type layout: bpy.types.Layout - :return: - :rtype: - """ - layout_box = layout.box() - if scene.scs_props.global_paths_settings_expand: - layout_box_row = layout_box.row() - layout_box_row.prop( - scene.scs_props, - 'global_paths_settings_expand', - text="Path Settings:", - icon='TRIA_DOWN', - icon_only=True, - emboss=False - ) - layout_box_row.label('') - layout_box_col = layout_box.column(align=True) - - # SCS Project Path (DIR_PATH - absolute) - icon = 'SNAP_ON' if _get_scs_globals().use_alternative_bases else 'SNAP_OFF' - layout_box_col.label('SCS Project Base Path:', icon='FILE_FOLDER') - layout_box_row = layout_box_col.row(align=True) - layout_box_row.alert = not os.path.isdir(scs_globals.scs_project_path) - layout_box_row.prop(scs_globals, 'scs_project_path', text='', icon='PACKAGE') - layout_box_row.prop(scs_globals, 'use_alternative_bases', icon=icon, icon_only=True) - layout_box_row.operator('scene.select_scs_project_path', text='', icon='FILESEL') - - # Divide labels and sub paths to columns - sub_paths_layout = layout_box_col.row().split(percentage=0.3) - sub_paths_left_col = sub_paths_layout.column(align=True) - sub_paths_right_col = sub_paths_layout.column(align=True) - - # Trigger Actions File (FILE_PATH - relative) - icon = 'SNAP_ON' if _get_scs_globals().trigger_actions_use_infixed else 'SNAP_OFF' - sub_paths_left_col.label("Trigger Action Lib:") - sub_path_right_col_row = sub_paths_right_col.row(align=True) - sub_path_right_col_row.alert = not _path_utils.is_valid_trigger_actions_rel_path() - sub_path_right_col_row.prop(scs_globals, 'trigger_actions_rel_path', text='', icon='FILE_SCRIPT') - sub_path_right_col_row.prop(scs_globals, 'trigger_actions_use_infixed', icon=icon, icon_only=True) - sub_path_right_col_row.operator('scene.select_trigger_actions_rel_path', text='', icon='FILESEL') - - # Sign Library Directory (FILE_PATH - relative) - icon = 'SNAP_ON' if _get_scs_globals().sign_library_use_infixed else 'SNAP_OFF' - sub_paths_left_col.label("Sign Library:") - sub_path_right_col_row = sub_paths_right_col.row(align=True) - sub_path_right_col_row.alert = not _path_utils.is_valid_sign_library_rel_path() - sub_path_right_col_row.prop(scs_globals, 'sign_library_rel_path', text='', icon='FILE_SCRIPT') - sub_path_right_col_row.prop(scs_globals, 'sign_library_use_infixed', icon=icon, icon_only=True) - sub_path_right_col_row.operator('scene.select_sign_library_rel_path', text='', icon='FILESEL') - - # Traffic Semaphore Profile Library Directory (FILE_PATH - relative) - icon = 'SNAP_ON' if _get_scs_globals().tsem_library_use_infixed else 'SNAP_OFF' - sub_paths_left_col.label("Semaphore Lib:") - sub_path_right_col_row = sub_paths_right_col.row(align=True) - sub_path_right_col_row.alert = not _path_utils.is_valid_tsem_library_rel_path() - sub_path_right_col_row.prop(scs_globals, 'tsem_library_rel_path', text='', icon='FILE_SCRIPT') - sub_path_right_col_row.prop(scs_globals, 'tsem_library_use_infixed', icon=icon, icon_only=True) - sub_path_right_col_row.operator('scene.select_tsem_library_rel_path', text='', icon='FILESEL') - - # Traffic Rules Library Directory (FILE_PATH - relative) - icon = 'SNAP_ON' if _get_scs_globals().traffic_rules_library_use_infixed else 'SNAP_OFF' - sub_paths_left_col.label("Traffic Rules Lib:") - sub_path_right_col_row = sub_paths_right_col.row(align=True) - sub_path_right_col_row.alert = not _path_utils.is_valid_traffic_rules_library_rel_path() - sub_path_right_col_row.prop(scs_globals, 'traffic_rules_library_rel_path', text='', icon='FILE_SCRIPT') - sub_path_right_col_row.prop(scs_globals, 'traffic_rules_library_use_infixed', icon=icon, icon_only=True) - sub_path_right_col_row.operator('scene.select_traffic_rules_library_rel_path', text='', icon='FILESEL') - - # Hookup Library Directory (DIR_PATH - relative) - sub_paths_left_col.label("Hookup Lib Dir:") - sub_path_right_col_row = sub_paths_right_col.row(align=True) - sub_path_right_col_row.alert = not _path_utils.is_valid_hookup_library_rel_path() - sub_path_right_col_row.prop(scs_globals, 'hookup_library_rel_path', text='', icon='FILE_FOLDER') - sub_path_right_col_row.operator('scene.select_hookup_library_rel_path', text='', icon='FILESEL') - - # Material Substance Library Directory (FILE_PATH - relative) - sub_paths_left_col.label("Mat Substance Lib:") - sub_path_right_col_row = sub_paths_right_col.row(align=True) - sub_path_right_col_row.alert = not _path_utils.is_valid_matsubs_library_rel_path() - sub_path_right_col_row.prop(scs_globals, 'matsubs_library_rel_path', text='', icon='FILE_SCRIPT') - sub_path_right_col_row.operator('scene.select_matsubs_library_rel_path', text='', icon='FILESEL') - - layout_box_row = layout_box_col.row() - layout_box_row.separator() - - # Shader Presets File (FILE_PATH) - layout_box_col.label('Shader Presets Library:', icon='FILE_TEXT') - layout_box_row = layout_box_col.row(align=True) - if _path_utils.is_valid_shader_presets_library_path(): - layout_box_row.alert = False - else: - layout_box_row.alert = True - layout_box_row.prop(scs_globals, 'shader_presets_filepath', text='', icon='NONE') - layout_box_row.operator('scene.select_shader_presets_filepath', text='', icon='FILESEL') - - # CONVERSION TOOLS PATH - layout_box_col.label("Conversion Tools Path:", icon="FILE_FOLDER") - - layout_box_row = layout_box_col.row(align=True) - valid = (os.path.isdir(scs_globals.conv_hlpr_converters_path) and - os.path.isfile(os.path.join(scs_globals.conv_hlpr_converters_path, "extra_mount.txt")) and - os.path.isfile(os.path.join(scs_globals.conv_hlpr_converters_path, "convert.cmd"))) - layout_box_row.alert = not valid - layout_box_row.prop(scs_globals, "conv_hlpr_converters_path", text="") - - else: - layout_box_row = layout_box.row() - layout_box_row.prop( - scene.scs_props, - 'global_paths_settings_expand', - text="Path Settings:", - icon='TRIA_RIGHT', - icon_only=True, - emboss=False - ) - layout_box_row.label('') - - -def _draw_display_settings_panel(scene, layout, scs_globals): - """Draw global display settings panel.""" - layout_box = layout.box() - if scene.scs_props.global_display_settings_expand: - layout_box_row = layout_box.row() - layout_box_row.prop( - scene.scs_props, - 'global_display_settings_expand', - text="Display Settings:", - icon='TRIA_DOWN', - icon_only=True, - emboss=False - ) - layout_box_row.label('') - - layout_box_row = layout_box.row() - layout_box_row.label("Drawing Mode:") - layout_box_row.prop(scs_globals, 'drawing_mode', expand=True) - - layout_box_row = layout_box.row() - if scs_globals.display_locators: - icon = "FILE_TICK" - else: - icon = "X" - layout_box_row.prop(scs_globals, 'display_locators', icon=icon, toggle=True) - - if scs_globals.display_locators: - layout_box_row = layout_box.row() - layout_box_row.prop(scs_globals, 'locator_size', icon='NONE') - layout_box_row.prop(scs_globals, 'locator_empty_size', icon='NONE') - layout_box_col = layout_box.column() - layout_box_row = layout_box_col.row() - layout_box_row.prop(scs_globals, 'locator_prefab_wire_color', icon='NONE') - layout_box_row = layout_box_col.row() - layout_box_row.prop(scs_globals, 'locator_model_wire_color', icon='NONE') - layout_box_row = layout_box_col.row() - layout_box_row.prop(scs_globals, 'locator_coll_wire_color', icon='NONE') - layout_box_row = layout_box_col.row() - layout_box_row.prop(scs_globals, 'locator_coll_face_color', icon='NONE') - layout_box_row = layout_box.row() - - if scs_globals.display_connections: - icon = "FILE_TICK" - else: - icon = "X" - layout_box_row.prop(scs_globals, 'display_connections', icon=icon, toggle=True) - - if scs_globals.display_connections: - layout_box_row = layout_box.row() - layout_box_row.prop(scs_globals, 'optimized_connections_drawing') - layout_box_row = layout_box.row() - layout_box_row.prop(scs_globals, 'curve_segments', icon='NONE') - layout_box_col = layout_box.column() - layout_box_row = layout_box_col.row() - layout_box_row.prop(scs_globals, 'np_connection_base_color', icon='NONE') - layout_box_row = layout_box_col.row() - layout_box_row.prop(scs_globals, 'mp_connection_base_color', icon='NONE') - layout_box_row = layout_box_col.row() - layout_box_row.prop(scs_globals, 'tp_connection_base_color', icon='NONE') - - layout_box_row = layout_box.row() - layout_box_row.prop(scs_globals, 'display_info', icon='NONE') - layout_box_row = layout_box.row() - layout_box_row.prop(scs_globals, 'info_text_color', icon='NONE') - layout_box_row = layout_box.row() - if scs_globals.show_preview_models: - icon = "FILE_TICK" - else: - icon = "X" - layout_box_row.prop(scs_globals, 'show_preview_models', icon=icon, toggle=True) - layout_box_row = layout_box.row() - layout_box_row.prop(scs_globals, 'base_paint_color', icon='NONE') - else: - layout_box_row = layout_box.row() - layout_box_row.prop( - scene.scs_props, - 'global_display_settings_expand', - text="Display Settings:", - icon='TRIA_RIGHT', - icon_only=True, - emboss=False - ) - layout_box_row.label('') - - -def _draw_global_settings_panel(scene, layout, scs_globals): - """Draw global settings panel.""" - layout_column = layout.column(align=True) - layout_box = layout_column.box() # header - if scene.scs_props.global_settings_expand: - layout_box_row = layout_box.row() - layout_box_row.prop( - scene.scs_props, - 'global_settings_expand', - text="Global Settings:", - icon='TRIA_DOWN', - icon_only=True, - emboss=False - ) - layout_box_row.label('') - - # GLOBAL SETTINGS PANEL - # draw_scs_tools_settings_panel_box(layout_box.row()) - - layout_box = layout_column.box() # body - # PATH SETTINGS PANEL - _draw_path_settings_panel(scene, layout_box.row(), scs_globals) - - # DISPLAY SETTINGS PANEL - _draw_display_settings_panel(scene, layout_box.row(), scs_globals) - - _shared.draw_common_settings(layout_box) - - else: - layout_box_row = layout_box.row() - layout_box_row.prop( - scene.scs_props, - 'global_settings_expand', - text="Global Settings:", - icon='TRIA_RIGHT', - icon_only=True, - emboss=False - ) - layout_box_row.label('') - - -def _draw_export_panel(scene, layout, scs_globals): - """Draw Export panel.""" - layout_column = layout.column(align=True) - layout_box = layout_column.box() # header - if scene.scs_props.export_panel_expand: - box_row = layout_box.row() - box_row.prop(scene.scs_props, 'export_panel_expand', text="Export Panel:", icon='TRIA_DOWN', icon_only=True, emboss=False) - box_row.label('') - - layout_box = layout_column.box() # body - if not scs_globals.preview_export_selection_active: - - col = layout_box.column(align=True) - col.row(align=True).prop(scs_globals, 'export_scope', expand=True) - - col_row = col.row(align=True) - col_row.enabled = scs_globals.export_scope == "selection" - icon = "FILE_TICK" if scs_globals.preview_export_selection else "X" - col_row.prop(scs_globals, 'preview_export_selection', text="Preview Selection", icon=icon, toggle=True) - - col_row = col.row(align=True) - col_row.scale_y = 2 - col_row.operator('scene.export_scs_content_by_scope', text="EXPORT") - - else: - - row = layout_box.box() - row.prop(scs_globals, "preview_export_selection_active", text="Export preview mode is active!", icon='ERROR', icon_only=True, - emboss=False) - row = row.column(align=True) - row.label("Press ENTER to export selection!") - row.label("Press ESC to cancel export!") - - col = layout_box.column() - - # Default Export Path (FILE_PATH - relative) - col_row = col.row() - col_row.label("Default Export Path:", icon="FILE_FOLDER") - col_row = col.row(align=True) - default_export_path = scene.scs_props.default_export_filepath - col_row.alert = ((default_export_path != "" and not default_export_path.startswith("//")) or - not os.path.isdir(os.path.join(scs_globals.scs_project_path, default_export_path.strip("//")))) - if col_row.alert: - _shared.draw_warning_operator( - col_row, - "Default Export Path Warning", - str("Current Default Export Path is unreachable, which may result into an error on export!\n" - "Make sure you did following:\n" - "1. Properly set \"SCS Project Base Path\"\n" - "2. Properly set \"Default Export Path\" which must be relative on \"SCS Project Base Path\"") - ) - - col_row.prop(scene.scs_props, 'default_export_filepath', text='', icon='EXPORT') - props = col_row.operator('scene.select_directory_inside_base', text='', icon='FILESEL') - props.type = "DefaultExportPath" - - _shared.draw_export_panel(layout_box, ignore_extra_boxes=True) - else: - box_row = layout_box.row() - box_row.prop(scene.scs_props, 'export_panel_expand', text="Export Panel:", icon='TRIA_RIGHT', icon_only=True, emboss=False) - box_row.label('') - - -def _draw_conversion_panel(layout, scs_globals): - """Draw Conversion Helper panel.""" - layout_column = layout.column(align=True) - layout_box = layout_column.box() # header - if scs_globals.conversion_helper_expand: - header_split = layout_box.row().split() - header_split.prop(scs_globals, 'conversion_helper_expand', text="Conversion Helper:", icon='TRIA_DOWN', icon_only=True, emboss=False) - header_split.operator("scene.scs_conv_hlpr_clean_rsrc", text="Clean Converted Data", icon="SCULPTMODE_HLT") - - layout_box = layout_column.box() # body - - # CUSTOM PATHS - cstm_paths_col = layout_box.column(align=not scs_globals.conv_hlpr_use_custom_paths) - row = cstm_paths_col.row(align=True) - row.scale_y = 1.1 - text = "Custom Paths: ON" if scs_globals.conv_hlpr_use_custom_paths else "Custom Paths: OFF" - icon = "FILE_TICK" if scs_globals.conv_hlpr_use_custom_paths else "X" - row.prop(scs_globals, "conv_hlpr_use_custom_paths", text=text, icon=icon, toggle=True) - - if scs_globals.conv_hlpr_use_custom_paths: - row = cstm_paths_col.row() - row.template_list( - 'SCSConversionEntrySlots', - list_id="", - dataptr=scs_globals, - propname="conv_hlpr_custom_paths", - active_dataptr=scs_globals, - active_propname="conv_hlpr_custom_paths_active", - rows=4, - maxrows=5, - type='DEFAULT', - columns=9 - ) - - side_bar = row.column(align=True) - side_bar.operator("scene.scs_conv_hlpr_add_path", text="", icon="ZOOMIN") - side_bar.operator("scene.scs_conv_hlpr_remove_path", text="", icon="ZOOMOUT") - side_bar.separator() - - props = side_bar.operator("scene.scs_conv_hlpr_order_path", icon="TRIA_UP", text="") - props.move_up = True - props = side_bar.operator("scene.scs_conv_hlpr_order_path", icon="TRIA_DOWN", text="") - props.move_up = False - - # CONVERT - row = cstm_paths_col.column(align=True) - row.scale_y = 1.1 - row.operator("scene.scs_conv_hlpr_convert_current", text='CONVERT CURRENT SCS PROJECT') - if scs_globals.conv_hlpr_use_custom_paths: - row.operator("scene.scs_conv_hlpr_convert_custom", text='CONVERT CUSTOM PATHS') - row.operator("scene.scs_conv_hlpr_convert_all", text='CONVERT ALL') - - # PACKING - col = layout_box.column(align=True) - col.row().label("Mod Packing:", icon="PACKAGE") - - row = col.row(align=True) - row.prop(scs_globals, "conv_hlpr_mod_destination", text="", icon="FILE_FOLDER") - row.operator("scene.scs_conv_hlpr_find_mod_folder", text="", icon="RECOVER_AUTO") - - row = col.row(align=True) - row.prop(scs_globals, "conv_hlpr_mod_name", text="", icon="FILE") - - row = col.row(align=True) - row.scale_y = 1.2 - icon = "FILE_TICK" if scs_globals.conv_hlpr_clean_on_packing else "X" - row.prop(scs_globals, "conv_hlpr_clean_on_packing", toggle=True, icon=icon) - icon = "FILE_TICK" if scs_globals.conv_hlpr_export_on_packing else "X" - row.prop(scs_globals, "conv_hlpr_export_on_packing", toggle=True, icon=icon) - icon = "FILE_TICK" if scs_globals.conv_hlpr_convert_on_packing else "X" - row.prop(scs_globals, "conv_hlpr_convert_on_packing", toggle=True, icon=icon) - - row = col.row(align=True) - row.prop(scs_globals, "conv_hlpr_mod_compression", expand=True) - - row = col.row(align=True) - row.scale_y = 1.5 - row.operator("scene.scs_conv_hlpr_pack", text=">>>> PACK MOD <<<<") - - else: - box_row = layout_box.row() - box_row.prop(scs_globals, 'conversion_helper_expand', text="Conversion Helper:", icon='TRIA_RIGHT', icon_only=True, emboss=False) - box_row.label('') - - -class SCSConversionEntrySlots(UIList): - """ - Draw conversion path entry within ui list - """ - - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - if item: - icon = "ERROR" if not os.path.isdir(_path_utils.repair_path(item.path)) else "NONE" - layout.prop(item, "path", text="", emboss=False, icon=icon) - else: - layout.label(text="", icon_value=icon) - - -class SCSTools(_ScenePanelBlDefs, Panel): - """Creates a Panel in the Scene properties window""" - bl_label = "SCS Tools" - bl_idname = "OBJECT_PT_SCS_tools" - - def draw(self, context): - """UI draw function.""" - layout = self.layout - scene = context.scene - scs_globals = _get_scs_globals() - - # scs tools main panel if config is being updated - layout.enabled = not scs_globals.config_update_lock - - if scene: - - # GLOBAL SETTINGS PANEL - _draw_global_settings_panel(scene, layout, scs_globals) - - # EXPORT PANEL - if context.mode == 'OBJECT': - _draw_export_panel(scene, layout, scs_globals) - - # CONVERSION PANEL - _draw_conversion_panel(layout, scs_globals) diff --git a/addon/io_scs_tools/ui/shared.py b/addon/io_scs_tools/ui/shared.py index 3c0134d..efac339 100644 --- a/addon/io_scs_tools/ui/shared.py +++ b/addon/io_scs_tools/ui/shared.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy from io_scs_tools.consts import Icons as _ICONS_consts @@ -32,90 +32,97 @@ class HeaderIconPanel: Holds the function for drawing icon in header section of Panel """ + is_popover = None # predefined Blender variable to avoid warnings in PyCharm + def draw_header(self, context): - self.layout.label('', icon_value=get_icon(_ICON_TYPES.scs_logo)) + if not self.is_popover: + self.layout.label(text="", icon_value=get_icon(_ICON_TYPES.scs_logo)) + + +class SCS_TOOLS_UL_ObjectLookSlots(bpy.types.UIList): + """ + Draw look item slot within SCS Looks list + """ + + def draw_item(self, context, layout, data, item, icon, active_data, active_property, index): + layout.prop(item, "name", text="", emboss=False, icon_value=icon) + # DEBUG + if int(_get_scs_globals().dump_level) > 2: + layout.label(text="DEBUG - id: " + str(item.id)) -def draw_scs_looks_panel(layout, scene, active_object, scs_root_object): + +def draw_scs_looks_panel(layout, active_object, scs_root_object, without_box=False): """Creates 'SCS Looks' settings sub-panel. :param layout: Blender UI Layout to draw to :type layout: bpy.types.UILayout - :param scene: Blender Scene - :type scene: bpy.types.Scene :param active_object: active object :type active_object: bpy.types.Object :param scs_root_object: SCS Root Object :type scs_root_object: bpy.types.Object + :param without_box: draw without extra box layout? + :type without_box: bool """ layout_column = layout.column(align=True) - layout_box = layout_column.box() - if scene.scs_props.scs_look_panel_expand: + if without_box: + layout_box = layout_column + else: + layout_box = layout_column.box() - # HEADER (COLLAPSIBLE - OPENED) - row = layout_box.row() - row.prop(scene.scs_props, 'scs_look_panel_expand', text="SCS Looks:", icon='TRIA_DOWN', icon_only=True, emboss=False) - row.prop(scene.scs_props, 'scs_look_panel_expand', text=" ", icon='NONE', icon_only=True, emboss=False) - - layout_box = layout_column.box() # body box - - if len(_object_utils.gather_scs_roots(bpy.context.selected_objects)) > 1 and active_object is not scs_root_object: - - col = layout_box.box().column(align=True) - row = col.row() - row.label("WARNING", icon="ERROR") - row = col.row() - row.label("Can not edit looks! Selection has multiple game objects.") - - else: # more roots or active object is root object - - row = layout_box.row() - row.template_list( - 'SCSObjectLookSlots', - list_id="", - dataptr=scs_root_object, - propname="scs_object_look_inventory", - active_dataptr=scs_root_object.scs_props, - active_propname="active_scs_look", - rows=3, - maxrows=5, - type='DEFAULT', - columns=9 - ) - - # LIST BUTTONS - col = row.column(align=True) - col.operator('object.add_scs_look', text="", icon='ZOOMIN') - col.operator('object.remove_scs_look', text="", icon='ZOOMOUT') + if len(_object_utils.gather_scs_roots(bpy.context.selected_objects)) > 1 and active_object is not scs_root_object: + + warning_box = layout_box.column(align=True) + + header = warning_box.box() + header.label(text="WARNING", icon='ERROR') + body = warning_box.box() + col = body.column(align=True) + col.label(text="Can not edit looks!") + col.label(text="Selection has multiple game objects.") + + else: # more roots or active object is root object - else: row = layout_box.row() - row.prop(scene.scs_props, 'scs_look_panel_expand', text="SCS Looks:", icon='TRIA_RIGHT', icon_only=True, emboss=False) - row.prop(scene.scs_props, 'scs_look_panel_expand', text=" ", icon='NONE', icon_only=True, emboss=False) + row.template_list( + SCS_TOOLS_UL_ObjectLookSlots.__name__, + list_id="", + dataptr=scs_root_object, + propname="scs_object_look_inventory", + active_dataptr=scs_root_object.scs_props, + active_propname="active_scs_look", + rows=3, + maxrows=5, + type='DEFAULT', + columns=9 + ) + + # LIST BUTTONS + col = row.column(align=True) + col.operator('object.scs_tools_add_look', text="", icon='ADD') + col.operator('object.scs_tools_remove_active_look', text="", icon='REMOVE') def draw_export_panel(layout, ignore_extra_boxes=False): box1 = layout.box() if not ignore_extra_boxes else layout - row = box1.row() - row.prop(_get_scs_globals(), 'export_scale') - col = box1.column() - row = col.row() - row.prop(_get_scs_globals(), 'export_apply_modifiers') - row_2 = row.row() + + box1.use_property_split = True + box1.use_property_decorate = False + + box1.prop(_get_scs_globals(), 'export_scale') + + flow = box1.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False) + col = flow.column() + col.prop(_get_scs_globals(), 'export_apply_modifiers') + col = flow.column() if _get_scs_globals().export_output_type.startswith('EF'): - if int(_get_scs_globals().export_apply_modifiers): - row_2.enabled = True - else: - row_2.enabled = False - row_2.prop(_get_scs_globals(), 'export_exclude_edgesplit') + col.enabled = _get_scs_globals().export_apply_modifiers + col.prop(_get_scs_globals(), 'export_exclude_edgesplit') else: - if not int(_get_scs_globals().export_apply_modifiers): - row_2.enabled = True - else: - row_2.enabled = False - row_2.prop(_get_scs_globals(), 'export_include_edgesplit') + col.enabled = not _get_scs_globals().export_apply_modifiers + col.prop(_get_scs_globals(), 'export_include_edgesplit') ''' col.prop(_get_scs_globals(), 'export_active_uv_only') if not _get_scs_globals().export_output_type.startswith('EF'): @@ -131,6 +138,8 @@ def draw_export_panel(layout, ignore_extra_boxes=False): # row = box1.row() # row.prop(_get_scs_globals(), 'export_anim_file', expand=True) box2 = layout.box() if not ignore_extra_boxes else layout + box2.use_property_split = True + box2.use_property_decorate = False box2.prop(_get_scs_globals(), 'export_output_type') ''' col = box2.column() @@ -159,29 +168,35 @@ def draw_export_panel(layout, ignore_extra_boxes=False): ''' -def draw_common_settings(layout, log_level_only=False): +def draw_common_settings(layout, log_level_only=False, without_box=False): """Draw common settings panel featuring log level and usage type of global settings if requested :param layout: Blender UI layout to draw operator to :type layout: UILayout :param log_level_only: draw only log level option :type log_level_only: bool + :param without_box: draw without extra box layout? + :type without_box: bool """ - box4 = layout.box().column() + + if without_box: + sub_layout = layout.column() + else: + sub_layout = layout.box().column() + + sub_layout.use_property_split = True + sub_layout.use_property_decorate = False if not log_level_only: - row = box4.row(align=True) - row.operator("scene.scs_copy_log", icon="COPYDOWN") + sub_layout.operator("scene.scs_tools_copy_log_to_clipboard", icon='COPYDOWN') - row = box4.row(align=True) - row.prop(_get_scs_globals(), 'dump_level', text="Log Level", icon='MOD_EXPLODE') + sub_layout.prop(_get_scs_globals(), 'dump_level', text="Log Level", icon='MOD_EXPLODE') if not log_level_only: - row = box4.row(align=True) - row.prop(_get_scs_globals(), 'config_storage_place', icon='NONE') + sub_layout.prop(_get_scs_globals(), 'config_storage_place') -def draw_warning_operator(layout, title, message, text="", icon="ERROR"): +def draw_warning_operator(layout, title, message, text="", icon='ERROR'): """Draws operator for showing popup window with given title and message. :param layout: Blender UI layout to draw operator to @@ -195,8 +210,74 @@ def draw_warning_operator(layout, title, message, text="", icon="ERROR"): :param icon: blender icon string (optional) :type icon: str """ - props = layout.operator('wm.show_warning_message', text=text, icon=icon) + props = layout.operator('wm.scs_tools_show_message_in_popup', text=text, icon=icon) props.is_modal = False props.icon = icon props.title = title props.message = message + + +def create_row(layout, align=False, use_split=False, use_decorate=False, enabled=False): + """Creates a row layout with givent options of the layout. + + :param layout: + :type layout: + :param align: + :type align: + :param use_split: + :type use_split: + :param use_decorate: + :type use_decorate: + :param enabled: + :type enabled: + :return: + :rtype: + """ + row = layout.row(align=align) + row.use_property_split = use_split + row.use_property_decorate = use_decorate + row.enabled = enabled + return row + + +def compensate_aligning_bug(layout, number_of_items=0): + """Creates compensation UI label items to fix aliging, when prop_search is used. + + TODO: Remove this and it's usage once blender has this bug resolved. + + :param layout: + :type layout: + :param number_of_items: + :type number_of_items: + :return: + :rtype: + """ + if number_of_items > 0: + for _ in range(0, number_of_items): + layout.label(text="", icon='BLANK1') + + +def get_on_off_icon(is_state_on): + """Returns icon string for on/off state. Should be used everywhere where we indicate on/off state in buttons. + + :param is_state_on: if True then ON icon is returned, OFF icon returned otherwise + :type is_state_on: bool + :return: icon string + :rtype: str + """ + return 'CHECKBOX_HLT' if is_state_on else 'CHECKBOX_DEHLT' + + +classes = ( + SCS_TOOLS_UL_ObjectLookSlots, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/addon/io_scs_tools/ui/tool_shelf.py b/addon/io_scs_tools/ui/tool_shelf.py index 87da296..b7e7039 100644 --- a/addon/io_scs_tools/ui/tool_shelf.py +++ b/addon/io_scs_tools/ui/tool_shelf.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy from bpy.types import Panel @@ -36,53 +36,77 @@ class _ToolShelfBlDefs(_shared.HeaderIconPanel): Creating initial class with needed members to be registered in Blender 3D View Tool Shelf """ bl_space_type = "VIEW_3D" - bl_region_type = "TOOLS" + bl_region_type = "UI" bl_category = "SCS Tools" bl_label = "not set" layout = None # predefined Blender variable to avoid warnings in PyCharm -class SCSToolShelfBlDefs(_ToolShelfBlDefs, Panel): +class SCS_TOOLS_PT_ToolShelf(_ToolShelfBlDefs, Panel): """ Creates a Tool Shelf panel in the SCS Tools tab. """ bl_context = "objectmode" bl_label = "Tool Shelf" + @classmethod + def poll(cls, context): + return context.mode == "OBJECT" + def draw(self, context): + if not self.poll(context): + self.layout.label(text="Not in 'Object Mode'!", icon="INFO") + return + layout = self.layout scene = context.scene if scene: col = layout.column(align=True) row = col.row(align=True) - row.operator('object.create_scs_root_object', text='Add Root', icon_value=get_icon(_ICON_TYPES.scs_root)) - row.operator('object.create_scs_root_object_dialog', text='', icon='OUTLINER_DATA_FONT') + row.operator('object.scs_tools_create_scs_root', text='Add Root', icon_value=get_icon(_ICON_TYPES.scs_root)) + row.operator('object.scs_tools_create_scs_root', text="", icon='SYNTAX_OFF').use_dialog = True + row = col.row(align=True) + row.operator('object.scs_tools_relocate_scs_roots', icon="CON_LOCLIKE") + + col.separator(factor=0.5) + row = col.row(align=True) - row.operator('object.scs_geometry_check', text="Check Geometry", icon="ZOOM_SELECTED") + row.operator('object.scs_tools_search_degenerated_polys', text="Check Geometry", icon='ZOOM_SELECTED') row = col.row(align=True) - row.operator('mesh.scs_vcoloring_edit', text="VColoring", icon="GROUP_VCOL") - row.operator('mesh.scs_vcoloring_rebake', text="", icon="FILE_REFRESH") + row.operator('mesh.scs_tools_start_vcoloring', text="VColoring", icon='GROUP_VCOL') + row.operator('mesh.scs_tools_rebake_vcoloring', text="", icon='FILE_REFRESH') -class SCSToolsConvexBlDefs(_ToolShelfBlDefs, Panel): +class SCS_TOOLS_PT_ConvexBlDefs(_ToolShelfBlDefs, Panel): """ Creates a Convex panel in the SCS Tools tab. """ bl_context = "objectmode" bl_label = "Convex" + bl_options = {'DEFAULT_CLOSED'} + + @classmethod + def poll(cls, context): + return context.mode == "OBJECT" def draw(self, context): + + if not self.poll(context): + self.layout.label(text="Not in 'Object Mode'!", icon="INFO") + return + layout = self.layout scene = context.scene if scene: col = layout.column(align=True) - col.operator('object.make_convex', text='Make Convex', icon_value=get_icon(_ICON_TYPES.loc_collider_convex)) - col.operator('object.convert_meshes_to_convex_locator', text='Convert to Locator', icon_value=get_icon(_ICON_TYPES.loc)) - col.operator('object.convert_convex_locator_to_mesh', text='Convert to Mesh', icon='OUTLINER_OB_MESH') - # col.operator('object.update_convex', text='Update Convex') + col.operator('object.scs_tools_make_convex_mesh', text='Make Convex', icon_value=get_icon(_ICON_TYPES.loc_collider_convex)) + row = col.row(align=True) + row.operator('object.scs_tools_convert_meshes_to_convex_locator', text='Convert to Locator', icon_value=get_icon(_ICON_TYPES.loc)) + row.operator('object.scs_tools_convert_meshes_to_convex_locator', text='', icon='MODIFIER').show_face_count_only = True + col.operator('object.scs_tools_convert_convex_locator_to_mesh', text='Convert to Mesh', icon='MESH_DATA') ''' @@ -156,283 +180,341 @@ def draw(self, context): col = box.column(align=True) row1 = col.row(align=True) row1.alignment = 'CENTER' - row1.operator('object.select_model_objects', text='', icon='OBJECT_DATA') - row1.operator('object.select_shadow_casters', text='', icon='MAT_SPHERE_SKY') - row1.operator('object.select_glass_objects', text='', icon='MOD_LATTICE') - row1.operator('object.blank_operator', text='', icon='MOD_PHYSICS') # TODO: Material Physics - has it sense? + row1.operator('object.scs_tools_select_model_objects', text="", icon='OBJECT_DATA') + row1.operator('object.scs_tools_select_shadow_casters', text="", icon='MAT_SPHERE_SKY') + row1.operator('object.scs_tools_select_glass_objects', text="", icon='MOD_LATTICE') + row1.operator('object.scs_tools_blank_operator', text="", icon='MOD_PHYSICS') # TODO: Material Physics - has it sense? row2 = col.row(align=True) row2.alignment = 'CENTER' - row2.operator('object.select_all_locators', text='', icon='OUTLINER_OB_EMPTY') - row2.operator('object.select_model_locators', text='', icon='MONKEY') - row2.operator('object.select_prefab_locators', text='', icon='MOD_BUILD') - row2.operator('object.select_collision_locators', text='', icon='PHYSICS') + row2.operator('object.scs_tools_select_all_locators', text="", icon='OUTLINER_OB_EMPTY') + row2.operator('object.scs_tools_select_model_locators', text="", icon='MONKEY') + row2.operator('object.scs_tools_select_prefab_locators', text="", icon='MOD_BUILD') + row2.operator('object.scs_tools_select_collision_locators', text="", icon='PHYSICS') row2 = col.row(align=True) row2.alignment = 'CENTER' - row2.operator('object.select_prefab_nodes', text='', icon='FORCE_FORCE') - row2.operator('object.select_prefab_signs', text='', icon='QUESTION') - row2.operator('object.select_prefab_spawns', text='', icon='PARTICLE_DATA') - row2.operator('object.select_prefab_traffics', text='', icon='PMARKER_ACT') + row2.operator('object.scs_tools_select_prefab_node_locators', text="", icon='FORCE_FORCE') + row2.operator('object.scs_tools_select_prefab_sign_locators', text="", icon='QUESTION') + row2.operator('object.scs_tools_select_prefab_spawn_locators', text="", icon='PARTICLE_DATA') + row2.operator('object.scs_tools_select_prefab_traffic_locators', text="", icon='PMARKER_ACT') row2 = col.row(align=True) row2.alignment = 'CENTER' - row2.operator('object.select_prefab_navigations', text='', icon='AUTO') - row2.operator('object.select_prefab_maps', text='', icon='LAMP_SUN') - row2.operator('object.select_prefab_triggers', text='', icon='FORCE_TURBULENCE') - row2.operator('object.blank_operator', text='', icon='BLANK1') + row2.operator('object.scs_tools_select_prefab_nav_locators', text="", icon='AUTO') + row2.operator('object.scs_tools_select_prefab_map_locators', text="", icon='LIGHT_SUN') + row2.operator('object.scs_tools_select_prefab_trigger_locators', text="", icon='FORCE_TURBULENCE') + row2.operator('object.scs_tools_blank_operator', text="", icon='BLANK1') ''' -class SCSToolsVisibility(_ToolShelfBlDefs, Panel): +class SCS_TOOLS_PT_Visibility(_ToolShelfBlDefs, Panel): """ Creates a Visibility Tools panel in the SCS Tools tab. """ bl_context = "objectmode" bl_label = "Visibility Tools" + @classmethod + def poll(cls, context): + return context.mode == "OBJECT" + def draw(self, context): """UI draw function.""" - layout = self.layout - scene = context.scene + workspace = context.workspace - if not scene: + if not workspace: return + if not self.poll(context): + self.layout.label(text="Not in 'Object Mode'!", icon="INFO") + return + + layout = self.layout + scs_root_is_reachable = _object_utils.get_scs_root(context.active_object) is not None box = layout.box() row = box.row() - row.prop(scene.scs_props, "visibility_tools_scope", expand=True) + row.prop(workspace.scs_props, "visibility_tools_scope", expand=True) col = box.column(align=True) - col.enabled = scs_root_is_reachable or scene.scs_props.visibility_tools_scope == "Global" + col.enabled = scs_root_is_reachable or workspace.scs_props.visibility_tools_scope == "Global" row1 = col.row(align=True) - row1.alignment = 'CENTER' - props = row1.operator('object.switch_model_objects_visibility', text='', icon_value=get_icon(_ICON_TYPES.mesh)) + row1.scale_x = 100 # fake extending to full width + props = row1.operator('object.scs_tools_switch_model_objects', text="", icon_value=get_icon(_ICON_TYPES.mesh)) props.view_type = _OP_consts.ViewType.viewonly - props = row1.operator('object.switch_shadow_casters_visibility', text='', icon_value=get_icon(_ICON_TYPES.mesh_shadow_caster)) + props = row1.operator('object.scs_tools_switch_shadow_casters', text="", icon_value=get_icon(_ICON_TYPES.mesh_shadow_caster)) props.view_type = _OP_consts.ViewType.viewonly - props = row1.operator('object.switch_glass_objects_visibility', text='', icon_value=get_icon(_ICON_TYPES.mesh_glass)) + props = row1.operator('object.scs_tools_switch_glass_objects', text="", icon_value=get_icon(_ICON_TYPES.mesh_glass)) props.view_type = _OP_consts.ViewType.viewonly - props = row1.operator('object.switch_substance_objects_visibility', text='', icon_value=get_icon(_ICON_TYPES.mesh_with_physics)) + props = row1.operator('object.scs_tools_switch_static_collision_objects', text="", icon_value=get_icon(_ICON_TYPES.mesh_with_physics)) props.view_type = _OP_consts.ViewType.viewonly row2 = col.row(align=True) - row2.alignment = 'CENTER' - props = row2.operator('object.switch_all_locators_visibility', text='', icon_value=get_icon(_ICON_TYPES.loc)) + row2.scale_x = 100 # fake extending to full width + props = row2.operator('object.scs_tools_switch_all_locators', text="", icon_value=get_icon(_ICON_TYPES.loc)) props.view_type = _OP_consts.ViewType.viewonly - props = row2.operator('object.switch_model_locators_visibility', text='', icon_value=get_icon(_ICON_TYPES.loc_model)) + props = row2.operator('object.scs_tools_switch_model_locators', text="", icon_value=get_icon(_ICON_TYPES.loc_model)) props.view_type = _OP_consts.ViewType.viewonly - props = row2.operator('object.switch_prefab_locators_visibility', text='', icon_value=get_icon(_ICON_TYPES.loc_prefab)) + props = row2.operator('object.scs_tools_switch_prefab_locators', text="", icon_value=get_icon(_ICON_TYPES.loc_prefab)) props.view_type = _OP_consts.ViewType.viewonly - props = row2.operator('object.switch_collision_locators_visibility', text='', icon_value=get_icon(_ICON_TYPES.loc_collider)) + props = row2.operator('object.scs_tools_switch_collision_locators', text="", icon_value=get_icon(_ICON_TYPES.loc_collider)) props.view_type = _OP_consts.ViewType.viewonly col = box.column(align=True) - col.enabled = scs_root_is_reachable or scene.scs_props.visibility_tools_scope == "Global" + col.enabled = scs_root_is_reachable or workspace.scs_props.visibility_tools_scope == "Global" row2 = col.row(align=True) - row2.alignment = 'CENTER' - props = row2.operator('object.switch_prefab_nodes_visibility', text='', icon_value=get_icon(_ICON_TYPES.loc_prefab_node)) + row2.scale_x = 100 # fake extending to full width + props = row2.operator('object.scs_tools_switch_prefab_node_locators', text="", icon_value=get_icon(_ICON_TYPES.loc_prefab_node)) props.view_type = _OP_consts.ViewType.viewonly - props = row2.operator('object.switch_prefab_signs_visibility', text='', icon_value=get_icon(_ICON_TYPES.loc_prefab_sign)) + props = row2.operator('object.scs_tools_switch_prefab_sign_locators', text="", icon_value=get_icon(_ICON_TYPES.loc_prefab_sign)) props.view_type = _OP_consts.ViewType.viewonly - props = row2.operator('object.switch_prefab_spawns_visibility', text='', icon_value=get_icon(_ICON_TYPES.loc_prefab_spawn)) + props = row2.operator('object.scs_tools_switch_prefab_spawn_locators', text="", icon_value=get_icon(_ICON_TYPES.loc_prefab_spawn)) props.view_type = _OP_consts.ViewType.viewonly - props = row2.operator('object.switch_prefab_traffics_visibility', text='', icon_value=get_icon(_ICON_TYPES.loc_prefab_semaphore)) + props = row2.operator('object.scs_tools_switch_prefab_traffic_locators', text="", icon_value=get_icon(_ICON_TYPES.loc_prefab_semaphore)) props.view_type = _OP_consts.ViewType.viewonly row2 = col.row(align=True) - row2.alignment = 'CENTER' - props = row2.operator('object.switch_prefab_navigations_visibility', text='', - icon_value=get_icon(_ICONS_consts.Types.loc_prefab_navigation)) + row2.scale_x = 100 # fake extending to full width + props = row2.operator('object.scs_tools_switch_prefab_nav_locators', text="", icon_value=get_icon(_ICONS_consts.Types.loc_prefab_navigation)) props.view_type = _OP_consts.ViewType.viewonly - props = row2.operator('object.switch_prefab_maps_visibility', text='', icon_value=get_icon(_ICON_TYPES.loc_prefab_map)) + props = row2.operator('object.scs_tools_switch_prefab_map_locators', text="", icon_value=get_icon(_ICON_TYPES.loc_prefab_map)) props.view_type = _OP_consts.ViewType.viewonly - props = row2.operator('object.switch_prefab_triggers_visibility', text='', icon_value=get_icon(_ICON_TYPES.loc_prefab_trigger)) + props = row2.operator('object.scs_tools_switch_prefab_trigger_locators', text="", icon_value=get_icon(_ICON_TYPES.loc_prefab_trigger)) props.view_type = _OP_consts.ViewType.viewonly - row2.operator('object.blank_operator', text='', icon='BLANK1') + row2.operator('object.scs_tools_blank_operator', text="", icon='BLANK1') col = layout.column(align=True) - col.enabled = scs_root_is_reachable or scene.scs_props.visibility_tools_scope == "Global" - col.label("Current SCS Root:", icon_value=get_icon(_ICONS_consts.Types.scs_root)) - col.operator('object.invert_visibility_within_root', text='Invert Visibility') - col.operator('object.view_all_objects_within_root', text='View All') - col.operator('object.isolate_objects_within_root', text='Isolate') + col.enabled = scs_root_is_reachable or workspace.scs_props.visibility_tools_scope == "Global" + col.label(text="Current SCS Root:", icon_value=get_icon(_ICONS_consts.Types.scs_root)) + col.operator('object.scs_tools_invert_visibility_within_scs_root', text='Invert Visibility') + col.operator('object.scs_tools_view_all_objects_within_scs_root', text='View All') + col.operator('object.scs_tools_isolate_objects_within_scs_root', text='Isolate') -class SCSDisplayMethods(_ToolShelfBlDefs, Panel): +class SCS_TOOLS_PT_DisplayMethods(_ToolShelfBlDefs, Panel): """ Creates a Display Methods panel in the SCS Tools tab. """ bl_context = "objectmode" bl_label = "Display Methods" + bl_options = {'DEFAULT_CLOSED'} + + @classmethod + def poll(cls, context): + return context.mode == "OBJECT" def draw(self, context): """UI draw function.""" - layout = self.layout scene = context.scene if not scene: return + if not self.poll(context): + self.layout.label(text="Not in 'Object Mode'!", icon="INFO") + return + + layout = self.layout + # GLASS OBJECTS col = layout.column(align=True) - col.label(text='Glass Objects', icon_value=get_icon(_ICON_TYPES.mesh_glass)) + col.label(text="Glass Objects", icon_value=get_icon(_ICON_TYPES.mesh_glass)) row = col.row(align=True) - row.operator('object.glass_objects_in_wireframes', text='Wires') - row.operator('object.glass_objects_textured', text='Textured') + row.operator('object.scs_tools_show_glass_objects_as_wire', text='Wires') + row.operator('object.scs_tools_show_glass_objects_as_textured', text='Textured') # SHADOW CASTERS col = layout.column(align=True) - col.label(text='Shadow Casters', icon_value=get_icon(_ICON_TYPES.mesh_shadow_caster)) + col.label(text="Shadow Casters", icon_value=get_icon(_ICON_TYPES.mesh_shadow_caster)) row = col.row(align=True) - row.operator('object.shadow_caster_objects_in_wireframes', text='Wires') - row.operator('object.shadow_caster_objects_textured', text='Textured') + row.operator('object.scs_tools_show_shadow_casters_as_wire', text='Wires') + row.operator('object.scs_tools_show_shadow_casters_as_textured', text='Textured') # COLLISION LOCATORS col = layout.column(align=True) - col.label(text='Collision Locators', icon_value=get_icon(_ICON_TYPES.loc_collider)) + col.label(text="Collision Locators", icon_value=get_icon(_ICON_TYPES.loc_collider)) row = col.row(align=True) - row.operator('object.all_collision_locators_wires', text='All Wires') - row.operator('object.no_collision_locators_wires', text='No Wires') + row.operator('object.scs_tools_enable_collision_locators_wire', text='All Wires') + row.operator('object.scs_tools_disable_collision_locators_wire', text='No Wires') row = col.row(align=True) - row.operator('object.all_collision_locators_faces', text='All Faces') - row.operator('object.no_collision_locators_faces', text='No Faces') + row.operator('object.scs_tools_enable_collision_locators_faces', text='All Faces') + row.operator('object.scs_tools_disable_collision_locators_faces', text='No Faces') -class SCSLampSwitcher(_ToolShelfBlDefs, Panel): +class SCS_TOOLS_PT_LampSwitcher(_ToolShelfBlDefs, Panel): """ Creates Lamp Switcher panel for SCS Tools tab. """ bl_label = "Lamp Switcher" + bl_options = {'DEFAULT_CLOSED'} @classmethod def poll(cls, context): return not context.vertex_paint_object + @staticmethod + def get_lampmask_state_icon(lamp_type): + """Gets lampmast state icon for given lamp type, to designate whether lamp type is switched on or off. + + :param lamp_type: lamp type gotten from lamp types enumerator names + :type lamp_type: str + :return: radio button on or off icon + :rtype: str + """ + + from io_scs_tools.internals.shaders.eut2.std_node_groups.lampmask_mixer_ng import LAMPMASK_MIX_G + + lamp_type_enabled = False + + if LAMPMASK_MIX_G in bpy.data.node_groups: + lampmask_nodes = bpy.data.node_groups[LAMPMASK_MIX_G].nodes + if lamp_type in lampmask_nodes and lampmask_nodes[lamp_type].inputs[0].default_value == 1: + lamp_type_enabled = True + + return _shared.get_on_off_icon(lamp_type_enabled) + + def draw_lamp_type_switch(self, layout, lamp_type, text_override=None): + """Draws lamp type switcher operator. + + :param layout: UI to draw operator to + :type layout: bpy.types.UILayout + :param lamp_type: lamp type gotten from lamp types enumerator names + :type lamp_type: str + :param text_override: text for operator, if None lamp type string is used + :type text_override: str + """ + icon = self.get_lampmask_state_icon(lamp_type) + text = text_override if text_override else lamp_type + props = layout.operator("material.scs_tools_switch_lampmask", text=text, icon=icon) + props.lamp_type = lamp_type + def draw(self, context): + if not self.poll(context): + self.layout.label(text="Not in 'Vertex Paint' mode!", icon="INFO") + return + layout = self.layout # vehicle switcher body_col = layout.column(align=True) body_row = body_col.row(align=True) - body_row.label("Vehicle", icon="AUTO") + body_row.label(text="Vehicle", icon='AUTO') body_row = body_col.row(align=True) - props = body_row.operator("material.scs_switch_lampmask", text="Positional") - props.lamp_type = _LT_consts.VehicleLampTypes.Positional.name - props = body_row.operator("material.scs_switch_lampmask", text="DRL") - props.lamp_type = _LT_consts.VehicleLampTypes.DRL.name + self.draw_lamp_type_switch(body_row, _LT_consts.VehicleLampTypes.Positional.name, text_override="Positional") + self.draw_lamp_type_switch(body_row, _LT_consts.VehicleLampTypes.DRL.name, text_override="DRL") body_row = body_col.row(align=True) - props = body_row.operator("material.scs_switch_lampmask", text="Left Blinker") - props.lamp_type = _LT_consts.VehicleLampTypes.LeftTurn.name - props = body_row.operator("material.scs_switch_lampmask", text="Right Blinker") - props.lamp_type = _LT_consts.VehicleLampTypes.RightTurn.name + self.draw_lamp_type_switch(body_row, _LT_consts.VehicleLampTypes.LeftTurn.name, text_override="Left Blinker") + self.draw_lamp_type_switch(body_row, _LT_consts.VehicleLampTypes.RightTurn.name, text_override="Right Blinker") body_row = body_col.row(align=True) - props = body_row.operator("material.scs_switch_lampmask", text="Low Beam") - props.lamp_type = _LT_consts.VehicleLampTypes.LowBeam.name - props = body_row.operator("material.scs_switch_lampmask", text="High Beam") - props.lamp_type = _LT_consts.VehicleLampTypes.HighBeam.name + self.draw_lamp_type_switch(body_row, _LT_consts.VehicleLampTypes.LowBeam.name, text_override="Low Beam") + self.draw_lamp_type_switch(body_row, _LT_consts.VehicleLampTypes.HighBeam.name, text_override="High Beam") body_row = body_col.row(align=True) - props = body_row.operator("material.scs_switch_lampmask", text="Brake") - props.lamp_type = _LT_consts.VehicleLampTypes.Brake.name - props = body_row.operator("material.scs_switch_lampmask", text="Reverse") - props.lamp_type = _LT_consts.VehicleLampTypes.Reverse.name + self.draw_lamp_type_switch(body_row, _LT_consts.VehicleLampTypes.Brake.name, text_override="Brake") + self.draw_lamp_type_switch(body_row, _LT_consts.VehicleLampTypes.Reverse.name, text_override="Reverse") # auxiliary switcher body_col = layout.column(align=True) body_row = body_col.row(align=True) - body_row.label("Auxiliary", icon="LAMP_SPOT") + body_row.label(text="Auxiliary", icon='LIGHT_SPOT') body_row = body_col.row(align=True) - props = body_row.operator("material.scs_switch_lampmask", text="Dim") - props.lamp_type = _LT_consts.AuxiliaryLampTypes.Dim.name - props = body_row.operator("material.scs_switch_lampmask", text="Bright") - props.lamp_type = _LT_consts.AuxiliaryLampTypes.Bright.name + self.draw_lamp_type_switch(body_row, _LT_consts.AuxiliaryLampTypes.Dim.name, text_override="Dim") + self.draw_lamp_type_switch(body_row, _LT_consts.AuxiliaryLampTypes.Bright.name, text_override="Bright") # traffic light switcher body_col = layout.column(align=True) body_row = body_col.row(align=True) - body_row.label("Traffic Lights", icon="COLOR_RED") + body_row.label(text="Traffic Lights", icon_value=get_icon(_ICONS_consts.Types.loc_prefab_semaphore)) body_row = body_col.row(align=True) - props = body_row.operator("material.scs_switch_lampmask", text="Red") - props.lamp_type = _LT_consts.TrafficLightTypes.Red.name - props = body_row.operator("material.scs_switch_lampmask", text="Yellow") - props.lamp_type = _LT_consts.TrafficLightTypes.Yellow.name - props = body_row.operator("material.scs_switch_lampmask", text="Green") - props.lamp_type = _LT_consts.TrafficLightTypes.Green.name + self.draw_lamp_type_switch(body_row, _LT_consts.TrafficLightTypes.Red.name, text_override="Red") + self.draw_lamp_type_switch(body_row, _LT_consts.TrafficLightTypes.Yellow.name, text_override="Yellow") + self.draw_lamp_type_switch(body_row, _LT_consts.TrafficLightTypes.Green.name, text_override="Green") -class SCSLampTool(_ToolShelfBlDefs, Panel): +class SCS_TOOLS_PT_LampTool(_ToolShelfBlDefs, Panel): """ Creates Lamp Switcher panel for SCS Tools tab. """ bl_label = "Lamp UV Tool" bl_context = "mesh_edit" + @classmethod + def poll(cls, context): + return context.mode == "EDIT_MESH" + def draw(self, context): + if not self.poll(context): + self.layout.label(text="Not in 'Edit Mode'!", icon="INFO") + return + layout = self.layout body_col = layout.column(align=True) body_row = body_col.row(align=True) - body_row.label("Vehicle", icon="AUTO") + body_row.label(text="Vehicle", icon='AUTO') body_row = body_col.row(align=True) - props = body_row.operator("mesh.scs_set_lampmask_uv", text="Front Left") + props = body_row.operator("mesh.scs_tools_set_lampmask_uv", text="Front Left") props.vehicle_side = _LT_consts.VehicleSides.FrontLeft.name props.aux_color = props.traffic_light_color = "" - props = body_row.operator("mesh.scs_set_lampmask_uv", text="Front Right") + props = body_row.operator("mesh.scs_tools_set_lampmask_uv", text="Front Right") props.vehicle_side = _LT_consts.VehicleSides.FrontRight.name props.aux_color = props.traffic_light_color = "" body_row = body_col.row(align=True) - props = body_row.operator("mesh.scs_set_lampmask_uv", text="Rear Left") + props = body_row.operator("mesh.scs_tools_set_lampmask_uv", text="Rear Left") props.vehicle_side = _LT_consts.VehicleSides.RearLeft.name props.aux_color = props.traffic_light_color = "" - props = body_row.operator("mesh.scs_set_lampmask_uv", text="Rear Right") + props = body_row.operator("mesh.scs_tools_set_lampmask_uv", text="Rear Right") props.vehicle_side = _LT_consts.VehicleSides.RearRight.name props.aux_color = props.traffic_light_color = "" body_row = body_col.row(align=True) - props = body_row.operator("mesh.scs_set_lampmask_uv", text="Middle") + props = body_row.operator("mesh.scs_tools_set_lampmask_uv", text="Middle") props.vehicle_side = _LT_consts.VehicleSides.Middle.name props.aux_color = props.traffic_light_color = "" body_col = layout.column(align=True) body_row = body_col.row(align=True) - body_row.label("Auxiliary", icon="LAMP_SPOT") + body_row.label(text="Auxiliary", icon='LIGHT_SPOT') body_row = body_col.row(align=True) - props = body_row.operator("mesh.scs_set_lampmask_uv", text="White") + props = body_row.operator("mesh.scs_tools_set_lampmask_uv", text="White") props.aux_color = _LT_consts.AuxiliaryLampColors.White.name props.vehicle_side = props.traffic_light_color = "" - props = body_row.operator("mesh.scs_set_lampmask_uv", text="Orange") + props = body_row.operator("mesh.scs_tools_set_lampmask_uv", text="Orange") props.aux_color = _LT_consts.AuxiliaryLampColors.Orange.name props.vehicle_side = props.traffic_light_color = "" body_col = layout.column(align=True) body_row = body_col.row(align=True) - body_row.label("Traffic Lights", icon="COLOR_RED") + body_row.label(text="Traffic Lights", icon_value=get_icon(_ICONS_consts.Types.loc_prefab_semaphore)) body_row = body_col.row(align=True) - props = body_row.operator("mesh.scs_set_lampmask_uv", text="Red") + props = body_row.operator("mesh.scs_tools_set_lampmask_uv", text="Red") props.traffic_light_color = _LT_consts.TrafficLightTypes.Red.name props.vehicle_side = props.aux_color = "" - props = body_row.operator("mesh.scs_set_lampmask_uv", text="Yellow") + props = body_row.operator("mesh.scs_tools_set_lampmask_uv", text="Yellow") props.traffic_light_color = _LT_consts.TrafficLightTypes.Yellow.name props.vehicle_side = props.aux_color = "" - props = body_row.operator("mesh.scs_set_lampmask_uv", text="Green") + props = body_row.operator("mesh.scs_tools_set_lampmask_uv", text="Green") props.traffic_light_color = _LT_consts.TrafficLightTypes.Green.name props.vehicle_side = props.aux_color = "" -class SCSVColoring(_ToolShelfBlDefs, Panel): +class SCS_TOOLS_PT_VColoring(_ToolShelfBlDefs, Panel): bl_label = "VColoring" @classmethod def poll(cls, context): - return context.vertex_paint_object and bpy.ops.mesh.scs_vcoloring_edit.poll() + return context.vertex_paint_object and bpy.ops.mesh.scs_tools_start_vcoloring.poll() def draw(self, context): + if not self.poll(context): + self.layout.label(text="Vcoloring not in progress!", icon="INFO") + return + layout = self.layout body_col = layout.column(align=True) active_vcol_name = context.vertex_paint_object.data.vertex_colors.active.name @@ -452,14 +534,14 @@ def draw(self, context): body_row = body_col.row(align=True) icon = 'RADIOBUT_ON' if active_vcol_name == layer_name else 'RADIOBUT_OFF' - props = body_row.operator("mesh.scs_vcoloring_edit", text=text, icon=icon) + props = body_row.operator("mesh.scs_tools_start_vcoloring", text=text, icon=icon) props.layer_name = layer_name body_row = layout.row() - body_row.operator("mesh.scs_vcoloring_exit", text="Exit (ESC)", icon="QUIT") + body_row.operator("mesh.scs_tools_exit_vcoloring", text="Exit (ESC)", icon='QUIT') -class SCSVertexColorWrapTool(_ToolShelfBlDefs, Panel): +class SCS_TOOLS_PT_VertexColorWrapTool(_ToolShelfBlDefs, Panel): bl_label = "Wrap Tool" @classmethod @@ -467,19 +549,23 @@ def poll(cls, context): return context.vertex_paint_object def draw(self, context): + if not self.poll(context): + self.layout.label(text="Not in 'Vertex Paint' mode!", icon="INFO") + return + layout = self.layout body_col = layout.column(align=True) body_row = body_col.row(align=True) - props = body_row.operator("mesh.scs_wrap_vcol", text="Wrap All") + props = body_row.operator("mesh.scs_tools_wrap_vertex_colors", text="Wrap All") props.wrap_type = "all" body_row = body_col.row(align=True) - props = body_row.operator("mesh.scs_wrap_vcol", text="Wrap Selected") + props = body_row.operator("mesh.scs_tools_wrap_vertex_colors", text="Wrap Selected") props.wrap_type = "selected" -class SCSVertexColorStatsTool(_ToolShelfBlDefs, Panel): +class SCS_TOOLS_PT_VertexColorStatsTool(_ToolShelfBlDefs, Panel): bl_label = "Stats Tool" @classmethod @@ -487,7 +573,45 @@ def poll(cls, context): return context.vertex_paint_object def draw(self, context): + if not self.poll(context): + self.layout.label(text="Not in 'Vertex Paint' mode!", icon="INFO") + return + layout = self.layout body_row = layout.row(align=True) - body_row.operator("mesh.scs_get_vcol_stats") + body_row.operator("mesh.scs_tools_print_vertex_colors_stats") + + +classes = ( + SCS_TOOLS_PT_ToolShelf, + SCS_TOOLS_PT_ConvexBlDefs, + SCS_TOOLS_PT_Visibility, + SCS_TOOLS_PT_DisplayMethods, + SCS_TOOLS_PT_LampSwitcher, + SCS_TOOLS_PT_LampTool, + SCS_TOOLS_PT_VColoring, + SCS_TOOLS_PT_VertexColorStatsTool, + SCS_TOOLS_PT_VertexColorWrapTool, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + from io_scs_tools import SCS_TOOLS_MT_MainMenu + SCS_TOOLS_MT_MainMenu.append_sidebar_entry("Sidebar - Tool Shelf", SCS_TOOLS_PT_ToolShelf.__name__) + SCS_TOOLS_MT_MainMenu.append_sidebar_entry("Sidebar - Convex", SCS_TOOLS_PT_ConvexBlDefs.__name__) + SCS_TOOLS_MT_MainMenu.append_sidebar_entry("Sidebar - Visibility Tools", SCS_TOOLS_PT_Visibility.__name__) + SCS_TOOLS_MT_MainMenu.append_sidebar_entry("Sidebar - Display Methods", SCS_TOOLS_PT_DisplayMethods.__name__) + SCS_TOOLS_MT_MainMenu.append_sidebar_entry("Sidebar - Lamp Switcher", SCS_TOOLS_PT_LampSwitcher.__name__) + SCS_TOOLS_MT_MainMenu.append_sidebar_entry("Sidebar - Lamp Tool", SCS_TOOLS_PT_LampTool.__name__) + SCS_TOOLS_MT_MainMenu.append_sidebar_entry("Sidebar - VColoring", SCS_TOOLS_PT_VColoring.__name__) + SCS_TOOLS_MT_MainMenu.append_sidebar_entry("Sidebar - VColor Stats", SCS_TOOLS_PT_VertexColorStatsTool.__name__) + SCS_TOOLS_MT_MainMenu.append_sidebar_entry("Sidebar - VColor Wrap", SCS_TOOLS_PT_VertexColorWrapTool.__name__) + + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/addon/io_scs_tools/ui/workspace.py b/addon/io_scs_tools/ui/workspace.py new file mode 100644 index 0000000..e9f3c2a --- /dev/null +++ b/addon/io_scs_tools/ui/workspace.py @@ -0,0 +1,295 @@ +# ##### 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) 2013-2019: SCS Software + +import os +import bpy +from bpy.types import Panel +from bl_ui.utils import PresetPanel +from io_scs_tools.utils import path as _path_utils +from io_scs_tools.utils import get_scs_globals as _get_scs_globals +from io_scs_tools.ui import shared as _shared + + +class _WorkspacePanelBlDefs: + """ + Defines class for showing in Blender Scene Properties window + """ + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "Tool" + bl_ui_units_x = 15 + + layout = None # predefined Blender variable to avoid warnings in PyCharm + is_popover = None # predefined Blender variable to avoid warnings in PyCharm + + @classmethod + def poll(cls, context): + return context.region.type in ('WINDOW', 'HEADER') + + def get_layout(self): + """Returns layout depending where it's drawn into. If popover create extra box to make it distinguisable between different sub-panels.""" + if self.is_popover: + layout = self.layout.box().column() + else: + layout = self.layout + + return layout + + +class SCS_TOOLS_PT_GlobalSettings(_shared.HeaderIconPanel, _WorkspacePanelBlDefs, Panel): + """Draw global settings panel.""" + + bl_label = "SCS Global Settings" + + def draw(self, context): + pass + + +class SCS_TOOLS_PT_PathSettingsPresets(PresetPanel, Panel): + bl_label = "SCS Tools Paths Presets" + preset_subdir = "io_scs_tools/paths" + preset_operator = "script.execute_preset" + preset_add_operator = "scene.scs_tools_add_path_preset" + + +class SCS_TOOLS_PT_PathSettings(_WorkspacePanelBlDefs, Panel): + """Draw global path settings panel.""" + + bl_parent_id = SCS_TOOLS_PT_GlobalSettings.__name__ + bl_label = "Path Settings" + + def draw_header_preset(self, context): + SCS_TOOLS_PT_PathSettingsPresets.draw_panel_header(self.layout) + + def draw(self, context): + layout = self.get_layout() + scs_globals = _get_scs_globals() + + # scs tools main panel if config is being updated + layout.enabled = not scs_globals.config_update_lock + + # SCS Project Path (DIR_PATH - absolute) + icon = 'SNAP_ON' if _get_scs_globals().use_alternative_bases else 'SNAP_OFF' + layout.label(text="SCS Project Base Path:", icon='FILE_FOLDER') + row = layout.row(align=True) + row.alert = not os.path.isdir(scs_globals.scs_project_path) + row.prop(scs_globals, 'scs_project_path', text="") + row.prop(scs_globals, 'use_alternative_bases', icon=icon, icon_only=True) + row.operator('scene.scs_tools_select_project_path', text="", icon='FILEBROWSER') + + # Divide labels and sub paths to columns + sub_paths_layout = layout.row().split(factor=0.35) + sub_paths_left_col = sub_paths_layout.column(align=True) + sub_paths_right_col = sub_paths_layout.column(align=True) + + # Trigger Actions File (FILE_PATH - relative) + icon = 'SNAP_ON' if _get_scs_globals().trigger_actions_use_infixed else 'SNAP_OFF' + sub_paths_left_col.label(text="Trigger Action Lib:") + sub_path_right_col_row = sub_paths_right_col.row(align=True) + sub_path_right_col_row.alert = not _path_utils.is_valid_trigger_actions_rel_path() + sub_path_right_col_row.prop(scs_globals, 'trigger_actions_rel_path', text="", icon='FILE_CACHE') + sub_path_right_col_row.prop(scs_globals, 'trigger_actions_use_infixed', icon=icon, icon_only=True) + sub_path_right_col_row.operator('scene.scs_tools_select_trigger_actions_lib_path', text="", icon='FILEBROWSER') + + # Sign Library Directory (FILE_PATH - relative) + icon = 'SNAP_ON' if _get_scs_globals().sign_library_use_infixed else 'SNAP_OFF' + sub_paths_left_col.label(text="Sign Library:") + sub_path_right_col_row = sub_paths_right_col.row(align=True) + sub_path_right_col_row.alert = not _path_utils.is_valid_sign_library_rel_path() + sub_path_right_col_row.prop(scs_globals, 'sign_library_rel_path', text="", icon='FILE_CACHE') + sub_path_right_col_row.prop(scs_globals, 'sign_library_use_infixed', icon=icon, icon_only=True) + sub_path_right_col_row.operator('scene.scs_tools_select_sign_lib_path', text="", icon='FILEBROWSER') + + # Traffic Semaphore Profile Library Directory (FILE_PATH - relative) + icon = 'SNAP_ON' if _get_scs_globals().tsem_library_use_infixed else 'SNAP_OFF' + sub_paths_left_col.label(text="Semaphore Lib:") + sub_path_right_col_row = sub_paths_right_col.row(align=True) + sub_path_right_col_row.alert = not _path_utils.is_valid_tsem_library_rel_path() + sub_path_right_col_row.prop(scs_globals, 'tsem_library_rel_path', text="", icon='FILE_CACHE') + sub_path_right_col_row.prop(scs_globals, 'tsem_library_use_infixed', icon=icon, icon_only=True) + sub_path_right_col_row.operator('scene.scs_tools_select_semaphore_lib_path', text="", icon='FILEBROWSER') + + # Traffic Rules Library Directory (FILE_PATH - relative) + icon = 'SNAP_ON' if _get_scs_globals().traffic_rules_library_use_infixed else 'SNAP_OFF' + sub_paths_left_col.label(text="Traffic Rules Lib:") + sub_path_right_col_row = sub_paths_right_col.row(align=True) + sub_path_right_col_row.alert = not _path_utils.is_valid_traffic_rules_library_rel_path() + sub_path_right_col_row.prop(scs_globals, 'traffic_rules_library_rel_path', text="", icon='FILE_CACHE') + sub_path_right_col_row.prop(scs_globals, 'traffic_rules_library_use_infixed', icon=icon, icon_only=True) + sub_path_right_col_row.operator('scene.scs_tools_select_traffic_rules_lib_path', text="", icon='FILEBROWSER') + + # Hookup Library Directory (DIR_PATH - relative) + sub_paths_left_col.label(text="Hookup Lib Dir:") + sub_path_right_col_row = sub_paths_right_col.row(align=True) + sub_path_right_col_row.alert = not _path_utils.is_valid_hookup_library_rel_path() + sub_path_right_col_row.prop(scs_globals, 'hookup_library_rel_path', text="", icon='FILE_FOLDER') + sub_path_right_col_row.operator('scene.scs_tools_select_hookup_lib_path', text="", icon='FILEBROWSER') + + # Material Substance Library Directory (FILE_PATH - relative) + sub_paths_left_col.label(text="Mat Substance Lib:") + sub_path_right_col_row = sub_paths_right_col.row(align=True) + sub_path_right_col_row.alert = not _path_utils.is_valid_matsubs_library_rel_path() + sub_path_right_col_row.prop(scs_globals, 'matsubs_library_rel_path', text="", icon='FILE_CACHE') + sub_path_right_col_row.operator('scene.scs_tools_select_matsubs_lib_path', text="", icon='FILEBROWSER') + + row = layout.row() + row.separator() + + # Shader Presets File (FILE_PATH) + layout.label(text="Shader Presets Library:", icon='FILE_TEXT') + row = layout.row(align=True) + row.prop(scs_globals, 'shader_presets_use_custom', text="") + custom_path_row = row.row(align=True) + custom_path_row.enabled = scs_globals.shader_presets_use_custom + custom_path_row.alert = not _path_utils.is_valid_shader_presets_library_path() + custom_path_row.prop(scs_globals, 'shader_presets_filepath', text="") + custom_path_row.operator('scene.scs_tools_select_shader_presets_path', text="", icon='FILEBROWSER') + + +class SCS_TOOLS_PT_DisplaySettings(_WorkspacePanelBlDefs, Panel): + """Draw global display settings panel.""" + + bl_parent_id = SCS_TOOLS_PT_GlobalSettings.__name__ + bl_label = "Display Settings" + + def draw(self, context): + layout = self.get_layout() + scs_globals = _get_scs_globals() + + layout.use_property_split = True + layout.use_property_decorate = False + + # scs tools main panel if config is being updated + layout.enabled = not scs_globals.config_update_lock + + layout.prop(scs_globals, 'drawing_mode', expand=True) + + layout.prop(scs_globals, 'icon_theme') + layout.prop(scs_globals, 'display_info') + row = _shared.create_row(layout, use_split=True, align=True, enabled=scs_globals.display_info != "none") + row.prop(scs_globals, 'info_text_color') + layout.prop(scs_globals, 'base_paint_color') + layout.prop(scs_globals, 'show_preview_models') + + +class SCS_TOOLS_PT_LocatorsDisplay(_WorkspacePanelBlDefs, Panel): + """Draw locators display panel.""" + + bl_parent_id = SCS_TOOLS_PT_DisplaySettings.__name__ + bl_label = "Locators Display" + bl_options = {'DEFAULT_CLOSED'} + + def draw_header(self, context): + layout = self.layout + scs_globals = _get_scs_globals() + + layout.enabled = not scs_globals.config_update_lock + layout.prop(scs_globals, 'display_locators', text="") + + def draw(self, context): + layout = self.get_layout() + scs_globals = _get_scs_globals() + + # scs tools main panel if config is being updated + layout.enabled = not scs_globals.config_update_lock + + layout.use_property_split = True + layout.use_property_decorate = False + layout.enabled = scs_globals.display_locators and not scs_globals.config_update_lock + + layout.prop(scs_globals, 'locator_size') + layout.prop(scs_globals, 'locator_empty_size') + layout.prop(scs_globals, 'locator_prefab_wire_color') + layout.prop(scs_globals, 'locator_model_wire_color') + layout.prop(scs_globals, 'locator_coll_wire_color') + layout.prop(scs_globals, 'locator_coll_face_color') + + +class SCS_TOOLS_PT_ConnectionsDisplay(_WorkspacePanelBlDefs, Panel): + """Draw connections display panel.""" + + bl_parent_id = SCS_TOOLS_PT_DisplaySettings.__name__ + bl_label = "Connections Display" + bl_options = {'DEFAULT_CLOSED'} + + def draw_header(self, context): + layout = self.layout + scs_globals = _get_scs_globals() + + layout.enabled = not scs_globals.config_update_lock + layout.prop(scs_globals, 'display_connections', text="") + + def draw(self, context): + layout = self.get_layout() + scs_globals = _get_scs_globals() + + layout.use_property_split = True + layout.use_property_decorate = False + layout.enabled = scs_globals.display_connections and not scs_globals.config_update_lock + + layout.prop(scs_globals, 'optimized_connections_drawing') + layout.prop(scs_globals, 'curve_segments') + layout.prop(scs_globals, 'np_connection_base_color') + layout.prop(scs_globals, 'mp_connection_base_color') + layout.prop(scs_globals, 'tp_connection_base_color') + + +class SCS_TOOLS_PT_OtherSetttings(_WorkspacePanelBlDefs, Panel): + """Draw global settings panel.""" + + bl_parent_id = SCS_TOOLS_PT_GlobalSettings.__name__ + bl_label = "Other Settings" + + def draw_header(self, context): + pass # disable custom icon + + def draw(self, context): + """Draw global settings panel.""" + layout = self.get_layout() + scs_globals = _get_scs_globals() + + # scs tools main panel if config is being updated + layout.enabled = not scs_globals.config_update_lock + + _shared.draw_common_settings(layout, without_box=True) + + +classes = ( + SCS_TOOLS_PT_GlobalSettings, + SCS_TOOLS_PT_PathSettingsPresets, + SCS_TOOLS_PT_PathSettings, + SCS_TOOLS_PT_DisplaySettings, + SCS_TOOLS_PT_LocatorsDisplay, + SCS_TOOLS_PT_ConnectionsDisplay, + SCS_TOOLS_PT_OtherSetttings, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + from io_scs_tools import SCS_TOOLS_MT_MainMenu + SCS_TOOLS_MT_MainMenu.append_props_entry("Workspace Properties", SCS_TOOLS_PT_GlobalSettings.__name__) + + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/addon/io_scs_tools/ui/world.py b/addon/io_scs_tools/ui/world.py index 98d525e..4088157 100644 --- a/addon/io_scs_tools/ui/world.py +++ b/addon/io_scs_tools/ui/world.py @@ -16,26 +16,41 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software +import bpy from bpy.types import Panel, UIList -from io_scs_tools.consts import SCSLigthing as _LIGHTING_consts from io_scs_tools.utils import path as _path_utils from io_scs_tools.utils import view3d as _view_3d_utils from io_scs_tools.utils import get_scs_globals as _get_scs_globals +from io_scs_tools.utils import get_scs_inventories as _get_scs_inventories from io_scs_tools.ui import shared as _shared -class _WorldPanelBlDefs(_shared.HeaderIconPanel): +class _WorldPanelBlDefs: """ - Defines class for showing in Blender Scene Properties window + Defines class for showing in Blender World Properties window """ bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "world" + bl_ui_units_x = 15 + @classmethod + def poll(cls, context): + return context.region.type in ('WINDOW', 'HEADER') -class SCSSunProfileSlots(UIList): + def get_layout(self): + """Returns layout depending where it's drawn into. If popover create extra box to make it distinguisable between different sub-panels.""" + if self.is_popover: + layout = self.layout.box().column() + else: + layout = self.layout + + return layout + + +class SCS_TOOLS_UL_SunProfileSlot(UIList): """ Draw sun profile entry within ui list """ @@ -43,53 +58,45 @@ class SCSSunProfileSlots(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): if item: layout.prop(item, "name", text="", emboss=False) - props = layout.operator("world.scs_use_sun_profile", icon="SAVE_PREFS", text="", emboss=False) - props.sun_profile_index = index else: layout.label(text="", icon_value=icon) -class SCSLighting(_WorldPanelBlDefs, Panel): - """Creates a Panel in the Scene properties window""" +class SCS_TOOLS_PT_Lighting(_shared.HeaderIconPanel, _WorldPanelBlDefs, Panel): + """Creates a Panel in the world properties window""" bl_label = "SCS Lighting" - bl_idname = "WORLD_PT_SCS_Lighting" + + def draw_header(self, context): + if not self.is_popover: + scs_globals = _get_scs_globals() + self.layout.prop(scs_globals, "use_scs_lighting", text="") + + _shared.HeaderIconPanel.draw_header(self, context) def draw(self, context): """UI draw function.""" - layout = self.layout scs_globals = _get_scs_globals() + scs_inventories = _get_scs_inventories() - is_active_sun_profile_valid = (0 <= scs_globals.sun_profiles_inventory_active < len(scs_globals.sun_profiles_inventory)) - is_ligting_scene_used = (context.scene and context.scene.background_set and - context.scene.background_set.name == _LIGHTING_consts.scene_name) - - # draw operator for disabling lighting scene - if is_ligting_scene_used: - layout.operator("world.scs_disable_lighting_in_scene", icon="QUIT") + if self.is_popover: + header_row = self.layout.row() + header_row.prop(scs_globals, "use_scs_lighting", text="SCS Lighting") + layout = self.layout.box().column() + else: + layout = self.get_layout() - # draw warning if lighting scene is not used as background set in current scene - if is_active_sun_profile_valid and not is_ligting_scene_used: - _shared.draw_warning_operator(layout, "SCS Lighting Not Used", - "Current scene is not using SCS Ligthing.\n" - "If you want to enable it, you either press icon beside sun profile name in the list or\n" - "use 'Apply Values to SCS Lighting' button located on the bottom of selected sun profile details.", - text="SCS Lighting Not Used: Click For Info", - icon="INFO") + layout.enabled = _get_scs_globals().use_scs_lighting - # prepare main box containing header and body - col = layout.column(align=True) - header = col.box() - body = col.box() + # prepare main layout containing header and body + header = layout.column() + body = layout.column() # 1. header # library path - row = header.row(align=True) - split = row.split(percentage=0.35) - split.label("Sun Profiles Library:", icon="FILE_TEXT") - row = split.row(align=True) + row = _shared.create_row(header, use_split=True, enabled=True) row.alert = not _path_utils.is_valid_sun_profiles_library_path() - row.prop(scs_globals, "sun_profiles_lib_path", text="") - row.operator("scene.select_sun_profiles_lib_path", text="", icon='FILESEL') + row.prop(scs_globals, "sun_profiles_lib_path", icon='FILE_CACHE') + row.operator("scene.scs_tools_select_sun_profiles_lib_path", text="", icon='FILEBROWSER') # 2. body # lighting scene east direction @@ -97,12 +104,12 @@ def draw(self, context): left_col = row.row(align=True) left_col.enabled = not scs_globals.lighting_east_lock - left_col.label("", icon="LAMP_SPOT") - left_col.separator() + left_col.label(text="", icon='LIGHT_SPOT') left_col.prop(scs_globals, "lighting_scene_east_direction", slider=True) right_col = row.row(align=True) - right_col.prop(scs_globals, "lighting_east_lock", icon="LOCKED", icon_only=True) + icon = 'LOCKED' if scs_globals.lighting_east_lock else 'UNLOCKED' + right_col.prop(scs_globals, "lighting_east_lock", icon=icon, icon_only=True) # now if we have multiple 3D views locking has to be disabled, # as it can not work properly with multiple views because all views share same SCS Lighting lamps @@ -112,55 +119,72 @@ def draw(self, context): "SCS Lighting East Lock Disabled!", "East lock can not be used, because you are using multiple 3D views and\n" "tools can not decide on which view you want to lock the east.", - icon="INFO") + icon='INFO') # disable any UI from now on if active sun profile is not valid - body.enabled = is_active_sun_profile_valid + body.enabled = (0 <= scs_globals.sun_profiles_active < len(scs_inventories.sun_profiles)) # loaded sun profiles list body.template_list( - 'SCSSunProfileSlots', + SCS_TOOLS_UL_SunProfileSlot.__name__, list_id="", - dataptr=scs_globals, - propname="sun_profiles_inventory", + dataptr=scs_inventories, + propname="sun_profiles", active_dataptr=scs_globals, - active_propname="sun_profiles_inventory_active", + active_propname="sun_profiles_active", rows=3, maxrows=5, type='DEFAULT', columns=9 ) - # active/selected sun profile props - if is_active_sun_profile_valid: - layout = body.box().column() +class SCS_TOOLS_PT_ActiveSunProfileSettings(_WorldPanelBlDefs, Panel): + """Creates a sub panel in the world properties window for active lighting settings.""" + + bl_parent_id = SCS_TOOLS_PT_Lighting.__name__ + bl_label = "Active Sun Profile Settings" + + @classmethod + def poll(cls, context): + return 0 <= _get_scs_globals().sun_profiles_active < len(_get_scs_inventories().sun_profiles) + + def draw(self, context): + """UI draw function.""" + scs_globals = _get_scs_globals() + scs_inventories = _get_scs_inventories() + + layout = self.get_layout() + layout.enabled = scs_globals.use_scs_lighting + layout.use_property_split = True + layout.use_property_decorate = False - layout.label("Selected Sun Profile Details:", icon='LAMP') - layout.separator() + active_sun_profile = scs_inventories.sun_profiles[scs_globals.sun_profiles_active] - active_sun_profile = scs_globals.sun_profiles_inventory[scs_globals.sun_profiles_inventory_active] + layout.prop(active_sun_profile, "low_elevation") + layout.prop(active_sun_profile, "high_elevation") + layout.prop(active_sun_profile, "ambient") + layout.prop(active_sun_profile, "diffuse") + layout.prop(active_sun_profile, "specular") + layout.prop(active_sun_profile, "env") + layout.prop(active_sun_profile, "env_static_mod") - layout.row(align=True).prop(active_sun_profile, "low_elevation") - layout.row(align=True).prop(active_sun_profile, "high_elevation") - layout.row(align=True).prop(active_sun_profile, "ambient") - layout.row(align=True).prop(active_sun_profile, "ambient_hdr_coef") +classes = ( + SCS_TOOLS_PT_Lighting, + SCS_TOOLS_PT_ActiveSunProfileSettings, + SCS_TOOLS_UL_SunProfileSlot, +) - layout.row(align=True).prop(active_sun_profile, "diffuse") - layout.row(align=True).prop(active_sun_profile, "diffuse_hdr_coef") - layout.row(align=True).prop(active_sun_profile, "specular") - layout.row(align=True).prop(active_sun_profile, "specular_hdr_coef") +def register(): + for cls in classes: + bpy.utils.register_class(cls) - # layout.row(align=True).prop(active_sun_profile, "sun_color") - # layout.row(align=True).prop(active_sun_profile, "sun_color_hdr_coef") + from io_scs_tools import SCS_TOOLS_MT_MainMenu + SCS_TOOLS_MT_MainMenu.append_props_entry("World Properties", SCS_TOOLS_PT_Lighting.__name__) - layout.row(align=True).prop(active_sun_profile, "env") - layout.row(align=True).prop(active_sun_profile, "env_static_mod") - layout.separator() - row = layout.row() - row.scale_y = 1.5 - props = row.operator("world.scs_use_sun_profile", icon="SAVE_PREFS", text="Apply Values to SCS Lighting") - props.sun_profile_index = scs_globals.sun_profiles_inventory_active +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) diff --git a/addon/io_scs_tools/utils/__init__.py b/addon/io_scs_tools/utils/__init__.py index 487c04b..9a0cc1a 100644 --- a/addon/io_scs_tools/utils/__init__.py +++ b/addon/io_scs_tools/utils/__init__.py @@ -33,18 +33,36 @@ def __get_world__(): return bpy.data.worlds[0] +def __get_addon_prefs__(): + """Gets scs tools addon preferences. + + :return: scs tools addon preferences + :rtype: io_scs_tools.properties.addon_preferences.SCSAddonPrefs + """ + return bpy.context.preferences.addons["io_scs_tools"].preferences + + def get_scs_globals(): """Function for accessing SCS globals :return: global settings for SCS Blender Tools :rtype: io_scs_tools.properties.world.GlobalSCSProps """ - return __get_world__().scs_globals + return __get_addon_prefs__().scs_globals + +def get_scs_inventories(): + """Function for accessing SCS inventories -def ensure_scs_globals_save(): - """Function for ensuring that scs globals get's saved into the blend file, - even if world is unliked by the user. + :return: runtime loaded inventories for SCS Blender Tools + :rtype: io_scs_tools.properties.addon_preferences.SCSInventories + """ + return __get_addon_prefs__().scs_inventories + + +def save_scs_globals_to_blend(): + """Function saving scs globals into the blend file. + We take first world data block and write scs_globals dictionary of our properties into it. """ world = __get_world__() @@ -52,3 +70,27 @@ def ensure_scs_globals_save(): world.use_fake_user = True elif world.users >= 2: # multiple users, switch of fake one as data will get saved anyway world.use_fake_user = False + + global_props = {} + + scs_globals = get_scs_globals() + for prop in scs_globals.get_writtable_keys(): + global_props[prop] = scs_globals[prop] + + world['scs_globals'] = global_props + + +def load_scs_globals_from_blend(): + """Loads scs globals saved in the blend file and silently applies them, without triggering properties update functions. + If no entries is found nothing is loaded and no scs global property get changed. + """ + world = __get_world__() + + if "scs_globals" not in world: + return + + global_props = world['scs_globals'] + + scs_globals = get_scs_globals() + for prop in global_props.keys(): + scs_globals[prop] = global_props[prop] diff --git a/addon/io_scs_tools/utils/collection.py b/addon/io_scs_tools/utils/collection.py new file mode 100644 index 0000000..d2f7ba8 --- /dev/null +++ b/addon/io_scs_tools/utils/collection.py @@ -0,0 +1,46 @@ +# ##### 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 + + +def get_layer_collections(view_layer): + """Gets all children layer collections of given view layer (master collection is excluded)sas. + + :param view_layer: view layer to look up for layer collections + :type view_layer: bpy.types.ViewLayer + :return: list of layer collections in this view layer + :rtype: list[bpy.types.LayerCollection] + """ + + layer_collections = set() + + master_layer_coll = view_layer.layer_collection + collections_to_check = [master_layer_coll] + while len(collections_to_check) > 0: + coll = collections_to_check.pop(0) + + if coll.children: + collections_to_check.extend(coll.children) + + if coll == master_layer_coll: + continue + + layer_collections.add(coll) + + return list(layer_collections) diff --git a/addon/io_scs_tools/utils/convert.py b/addon/io_scs_tools/utils/convert.py index f2ccb33..0fb0b9a 100644 --- a/addon/io_scs_tools/utils/convert.py +++ b/addon/io_scs_tools/utils/convert.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import struct import math @@ -25,6 +25,7 @@ from io_scs_tools.utils.printout import lprint from io_scs_tools.utils import math as _math_utils from io_scs_tools.utils import get_scs_globals as _get_scs_globals +from io_scs_tools.utils import get_scs_inventories as _get_scs_inventories def linear_to_srgb(value): @@ -32,7 +33,7 @@ def linear_to_srgb(value): NOTE: taken from game code :param value: list of floats or float - :type value: float | list[float] + :type value: float | collections.Iterable[float] :return: converted color :rtype: float | list[float] """ @@ -56,6 +57,36 @@ def linear_to_srgb(value): return vals +def srgb_to_linear(value): + """Converts srgb color to linear colorspace. Function can convert single float or list of floats. + NOTE: taken from game code + + :param value: list of floats or float + :type value: float | collections.Iterable[float] + :return: converted color + :rtype: float | list[float] + """ + + is_float = isinstance(value, float) + if is_float: + vals = [value] + else: + vals = list(value) + + for i, v in enumerate(vals): + + if v <= 0.04045: + vals[i] = v / 12.92 + else: + a = 0.055 + vals[i] = ((v + a) / (1.0 + a)) ** 2.4 + + if is_float: + return vals[0] + else: + return vals + + def pre_gamma_corrected_col(color): """Pre applys gamma decoding to color. Usefull for preparation of color for Blender color pickers/nodes, @@ -75,7 +106,7 @@ def pre_gamma_corrected_col(color): return c -def to_node_color(color): +def to_node_color(color, from_linear=False): """Gets color ready for assigning to Blender nodes. 1. Sets minimal HSV value attribute for rendering 2. Applies pre gamma correction @@ -83,11 +114,17 @@ def to_node_color(color): :param color: color to be converted for usage in node :type color: mathutils.Color | collections.Iterable[float] + :param from_linear: should color first be converted from linear space? + :type from_linear: bool :return: RGBA as tuple of floats :rtype: tuple[float] """ - c = Color(color[:3]) # copy color so changes won't reflect on original passed color object + srgb_color = color + if from_linear: + srgb_color = linear_to_srgb(color) + + c = Color(srgb_color[:3]) # copy color so changes won't reflect on original passed color object c = pre_gamma_corrected_col(c) @@ -173,7 +210,7 @@ def hex_string_to_float(string): :return: Float value or Error string :rtype: float or str """ - if type(string) is str: + if isinstance(string, str): if string[0] == '&': if len(string) == 9: byte = bytearray.fromhex(string[1:]) @@ -210,7 +247,7 @@ def convert_location_to_scs(location, offset_matrix=Matrix()): :rtype: Vector """ scs_globals = _get_scs_globals() - return Matrix.Scale(scs_globals.export_scale, 4) * scs_to_blend_matrix().inverted() * (offset_matrix.inverted() * location) + return Matrix.Scale(scs_globals.export_scale, 4) @ scs_to_blend_matrix().inverted() @ (offset_matrix.inverted() @ location) def change_to_scs_xyz_coordinates(vec, scale): @@ -235,7 +272,7 @@ def change_to_blender_quaternion_coordinates(rot): :rtype: Quaternion """ quat = Quaternion((rot[0], rot[1], rot[2], rot[3])) - return (scs_to_blend_matrix() * quat.to_matrix().to_4x4() * scs_to_blend_matrix().inverted()).to_quaternion() + return (scs_to_blend_matrix() @ quat.to_matrix().to_4x4() @ scs_to_blend_matrix().inverted()).to_quaternion() def change_to_scs_quaternion_coordinates(rot): @@ -247,7 +284,7 @@ def change_to_scs_quaternion_coordinates(rot): :rtype: Quaternion """ quat = Quaternion((rot[0], rot[1], rot[2], rot[3])) - return (scs_to_blend_matrix().inverted() * quat.to_matrix().to_4x4() * scs_to_blend_matrix()).to_quaternion() + return (scs_to_blend_matrix().inverted() @ quat.to_matrix().to_4x4() @ scs_to_blend_matrix()).to_quaternion() def change_to_scs_uv_coordinates(uvs): @@ -293,7 +330,7 @@ def mat3_to_vec_roll(mat): # print(' mat_3x3[2]:\n%s' % str(mat_3x3.col[2])) zero_angle_matrix = vec_roll_to_mat3(axis, 0) - delta_matrix = zero_angle_matrix.inverted() * mat_3x3 + delta_matrix = zero_angle_matrix.inverted() @ mat_3x3 angle = math.atan2(delta_matrix.col[2][0], delta_matrix.col[2][2]) return axis, angle @@ -329,7 +366,7 @@ def vec_roll_to_mat3(axis, roll): b_matrix[3] = (0, 0, 0, 1) roll_matrix = Matrix.Rotation(roll, 4, nor) - return (roll_matrix * b_matrix).to_3x3() + return (roll_matrix @ b_matrix).to_3x3() def str_to_int(str_value): @@ -358,7 +395,7 @@ def hookup_id_to_hookup_name(hookup_id): :rtype: str """ hookup_name = hookup_id - for rec in _get_scs_globals().scs_hookup_inventory: + for rec in _get_scs_inventories().hookups: rec_id = rec.name.split(':', 1)[1].strip() if rec_id == hookup_id: hookup_name = rec.name diff --git a/addon/io_scs_tools/utils/material.py b/addon/io_scs_tools/utils/material.py index 852a8de..4cdfeb8 100644 --- a/addon/io_scs_tools/utils/material.py +++ b/addon/io_scs_tools/utils/material.py @@ -16,22 +16,24 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2015: SCS Software +# Copyright (C) 2013-2019: SCS Software +import bmesh import bpy import os +import tempfile +from math import pi from io_scs_tools.consts import Material as _MAT_consts from io_scs_tools.imp import tobj as _tobj_imp from io_scs_tools.internals import inventory as _invetory from io_scs_tools.internals import shader_presets as _shader_presets -from io_scs_tools.internals.containers import pix as _pix_container from io_scs_tools.internals.shaders import shader as _shader from io_scs_tools.utils import path as _path from io_scs_tools.utils.printout import lprint -def get_texture(texture_path, texture_type, report_invalid=False): - """Creates and setup Texture and Image data on active Material. +def get_texture_image(texture_path, texture_type, report_invalid=False): + """Creates and returns image for given texture path and type. :param texture_path: Texture path :type texture_path: str @@ -39,8 +41,14 @@ def get_texture(texture_path, texture_type, report_invalid=False): :type texture_type: str :param report_invalid: flag indicating if invalid texture should be reported in 3d view :type report_invalid: bool + :return: loaded image datablock to be used in SCS material + :rtype: bpy.types.Image """ + # get reflection image texture + if texture_path.endswith(".tobj") and texture_type == "reflection": + return get_reflection_image(texture_path, report_invalid=report_invalid) + # CREATE TEXTURE/IMAGE ID NAME teximag_id_name = _path.get_filename(texture_path, with_ext=False) @@ -76,29 +84,25 @@ def get_texture(texture_path, texture_type, report_invalid=False): return None - texture = None + image = None if abs_texture_filepath and os.path.isfile(abs_texture_filepath): - # find existing texture with this image - if teximag_id_name in bpy.data.textures: - - # reuse existing image texture if possible - postfix = 0 - postfixed_tex = teximag_id_name - while postfixed_tex in bpy.data.textures: + # reuse existing image texture if possible + postfix = 0 + postfixed_tex = teximag_id_name + while postfixed_tex in bpy.data.images: - img_exists = bpy.data.textures[postfixed_tex].image is not None - if img_exists and _path.repair_path(bpy.data.textures[postfixed_tex].image.filepath) == _path.repair_path(abs_texture_filepath): - texture = bpy.data.textures[postfixed_tex] - break + img_exists = postfixed_tex in bpy.data.images + if img_exists and _path.repair_path(bpy.data.images[postfixed_tex].filepath) == _path.repair_path(abs_texture_filepath): + image = bpy.data.images[postfixed_tex] + break - postfix += 1 - postfixed_tex = teximag_id_name + "." + str(postfix).zfill(3) + postfix += 1 + postfixed_tex = teximag_id_name + "." + str(postfix).zfill(3) - # if texture wasn't found create new one - if not texture: + # if image wasn't found create new one + if not image: - texture = bpy.data.textures.new(teximag_id_name, 'IMAGE') image = None # reuse existing image if possible @@ -117,6 +121,7 @@ def get_texture(texture_path, texture_type, report_invalid=False): if not image: image = bpy.data.images.load(abs_texture_filepath) image.name = teximag_id_name + image.alpha_mode = 'CHANNEL_PACKED' # try to get relative path to the Blender file and set it to the image if bpy.data.filepath != '': # empty file path means blender file is not saved @@ -128,28 +133,7 @@ def get_texture(texture_path, texture_type, report_invalid=False): if rel_path: image.filepath = rel_path - # finally link image to texture - texture.image = image - image.use_alpha = True - - # set proper color space depending on texture type - if texture_type == "nmap": - # For TGA and DDS normal maps texture use Non-Color color space as it should be, - # but for 16-bits PNG normal maps texture Linear has to be used - # otherwise Blender completely messes up normals calculation - if texture.image.filepath.endswith(".tga") or texture.image.filepath.endswith(".dds"): - texture.image.colorspace_settings.name = "Non-Color" - elif texture.image.filepath.endswith(".png") and texture.image.is_float: - texture.image.colorspace_settings.name = "Linear" - else: - texture.image.colorspace_settings.name = "sRGB" - else: - texture.image.colorspace_settings.name = "sRGB" - - # set usage of normal map if texture type is correct - texture.use_normal_map = (texture_type == "nmap") - - if texture is None and texture_path.endswith(".tobj"): + if image is None and texture_path.endswith(".tobj"): if report_invalid: lprint("", report_warnings=-1, report_errors=-1) @@ -159,7 +143,223 @@ def get_texture(texture_path, texture_type, report_invalid=False): if report_invalid: lprint("", report_warnings=1, report_errors=1) - return texture + return image + + +def get_reflection_image(texture_path, report_invalid=False): + """Gets reflection image for given texture path. + + 1. gets all textures names and check existance + 2. create image objects for all planes + 3. setup scene, create planes, create camera projector and assign images + 4. render & save image + 5. cleanup & scene restoring + 6. load temp image and pack it + 7. set filepath to TOBJ + + :param texture_path: Texture path + :type texture_path: str + :param report_invalid: flag indicating if invalid texture should be reported in 3d view + :type report_invalid: bool + :return: loaded image datablock to be used in SCS material + :rtype: bpy.types.Image + """ + + # CREATE TEXTURE/IMAGE ID NAME + teximag_id_name = _path.get_filename(texture_path, with_ext=False) + "_cubemap" + + # CREATE ABSOLUTE FILEPATH + abs_tobj_filepath = _path.get_abs_path(texture_path) + + # return None on non-existing TOBJ + if not abs_tobj_filepath or not os.path.isfile(abs_tobj_filepath): + return None + + # check existance of this cubemap + if teximag_id_name in bpy.data.images: + + if _path.get_abs_path(bpy.data.images[teximag_id_name].filepath) == abs_tobj_filepath: + return bpy.data.images[teximag_id_name] + + bpy.data.images.remove(bpy.data.images[teximag_id_name]) + + # 1. get all textures file paths and check their existance + + abs_texture_filepaths = _path.get_texture_paths_from_tobj(abs_tobj_filepath) + + # should be a cubemap with six images + if len(abs_texture_filepaths) != 6: + return None + + # all six images have to exist + for abs_texture_filepath in abs_texture_filepaths: + + if abs_texture_filepath[-4:] not in (".tga", ".png", ".dds"): # none supported file + + if report_invalid: + lprint("", report_warnings=-1, report_errors=-1) + + lprint("W Texture can't be displayed as TOBJ file: %r is referencing non texture file:\n\t %r", + (texture_path, _path.readable_norm(abs_texture_filepath))) + + if report_invalid: + lprint("", report_warnings=1, report_errors=1) + + return None + + elif not os.path.isfile(abs_texture_filepath): # none existing file + + if report_invalid: + lprint("", report_warnings=-1, report_errors=-1) + + # take care of none existing paths referenced in tobj texture names + lprint("W Texture can't be displayed as TOBJ file: %r is referencing non existing texture file:\n\t %r", + (texture_path, _path.readable_norm(abs_texture_filepath))) + + if report_invalid: + lprint("", report_warnings=1, report_errors=1) + + return None + + # 2. create image objects for all planes + + images = [] + for abs_texture_filepath in abs_texture_filepaths: + images.append(bpy.data.images.load(abs_texture_filepath)) + + # 3. setup scene, create planes, create camera projector and assign images + + old_scene = bpy.context.window.scene + tmp_scene = bpy.data.scenes.new("cubemap") + bpy.context.window.scene = tmp_scene + + meshes = [] + materials = [] + objects = [] + for i, plane in enumerate(("x+", "x-", "y+", "y-", "z+", "z-")): + # mesh creation + bm = bmesh.new(use_operators=True) + + bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=1, calc_uvs=True) + + mesh = bpy.data.meshes.new(plane) + bm.to_mesh(mesh) + bm.free() + + mesh.uv_layers.new() + + meshes.append(mesh) + + # material creation + material = bpy.data.materials.new(plane) + material.use_nodes = True + material.node_tree.nodes.clear() + + out_node = material.node_tree.nodes.new("ShaderNodeOutputMaterial") + emission_node = material.node_tree.nodes.new("ShaderNodeEmission") + tex_node = material.node_tree.nodes.new("ShaderNodeTexImage") + tex_node.image = images[i] + + material.node_tree.links.new(emission_node.inputs['Color'], tex_node.outputs['Color']) + material.node_tree.links.new(out_node.inputs['Surface'], emission_node.outputs['Emission']) + + mesh.materials.append(material) + + materials.append(material) + + # object creation + obj = bpy.data.objects.new(mesh.name, mesh) + obj.location = (0,) * 3 + obj.rotation_euler = (0,) * 3 + if plane == "x+": + obj.rotation_euler.x = pi * 0.5 + obj.location.y = 1 + elif plane == "x-": + obj.rotation_euler.x = pi * 0.5 + obj.rotation_euler.z = pi + obj.location.y = -1 + elif plane == "y+": + obj.rotation_euler.x = pi + obj.rotation_euler.z = pi * 0.5 + obj.location.z = 1 + elif plane == "y-": + obj.rotation_euler.z = pi * 0.5 + obj.location.z = -1 + elif plane == "z+": + obj.rotation_euler.x = pi * 0.5 + obj.rotation_euler.z = pi * 0.5 + obj.location.x = -1 + elif plane == "z-": + obj.rotation_euler.x = pi * 0.5 + obj.rotation_euler.z = -pi * 0.5 + obj.location.x = 1 + + tmp_scene.collection.objects.link(obj) + objects.append(obj) + + # camera creation + camera = bpy.data.cameras.new("projector") + camera.type = "PANO" + camera.lens = 5 + camera.sensor_width = 32 + camera.cycles.panorama_type = "EQUIRECTANGULAR" + camera.cycles.latitude_min = -pi * 0.5 + camera.cycles.latitude_max = pi * 0.5 + camera.cycles.longitude_min = pi + camera.cycles.longitude_max = -pi + + cam_obj = bpy.data.objects.new(camera.name, camera) + cam_obj.location = (0,) * 3 + cam_obj.rotation_euler = (pi * 0.5, 0, 0) + + tmp_scene.collection.objects.link(cam_obj) + + # 4. render & save image + + final_image_path = os.path.join(tempfile.gettempdir(), teximag_id_name + ".tga") + + tmp_scene.render.engine = "CYCLES" + tmp_scene.cycles.samples = 1 + tmp_scene.camera = cam_obj + tmp_scene.render.image_settings.file_format = "TARGA" + tmp_scene.render.image_settings.color_mode = "RGBA" + tmp_scene.render.resolution_percentage = 100 + tmp_scene.render.resolution_x = images[0].size[0] * 4 + tmp_scene.render.resolution_y = images[0].size[1] * 2 + tmp_scene.render.filepath = final_image_path + bpy.ops.render.render(write_still=True, scene=tmp_scene.name) + + # 5. cleanup & scene restoring + + for obj in objects: + bpy.data.objects.remove(obj) + + for mesh in meshes: + bpy.data.meshes.remove(mesh) + + for material in materials: + bpy.data.materials.remove(material) + + for image in images: + bpy.data.images.remove(image) + + bpy.data.objects.remove(cam_obj) + bpy.data.cameras.remove(camera) + + bpy.context.window.scene = old_scene + bpy.data.scenes.remove(tmp_scene) + + # 6. load temp image and pack it + + final_image = bpy.data.images.load(final_image_path) + final_image.name = teximag_id_name + final_image.alpha_mode = 'CHANNEL_PACKED' + final_image.pack() + + # 7. set filepath to original image + final_image.filepath = abs_tobj_filepath + + return final_image def get_material_from_context(context): @@ -366,6 +566,7 @@ def set_shader_data_to_material(material, section, is_import=False, override_bac # apply used textures created_textures = {} + created_tex_settings = {} created_tex_mappings = [] for tex_type in used_texture_types: @@ -439,7 +640,7 @@ def set_shader_data_to_material(material, section, is_import=False, override_bac # because otherwise texture from previous look might be applied if (scs_texture_str != "" and getattr(material.scs_props, "shader_texture_" + tex_type, "") == "") or is_import: material.scs_props["shader_texture_" + tex_type] = scs_texture_str - created_textures[tex_type] = get_texture(scs_texture_str, tex_type) + created_textures[tex_type] = get_texture_image(scs_texture_str, tex_type) if is_import: @@ -454,13 +655,20 @@ def set_shader_data_to_material(material, section, is_import=False, override_bac else: final_tex_str = getattr(material.scs_props, "shader_texture_" + tex_type, "") - created_textures[tex_type] = get_texture(final_tex_str, tex_type) + created_textures[tex_type] = get_texture_image(final_tex_str, tex_type) if is_import and not override_back_data: if created_textures[tex_type] is None: lprint("E Can't find texture nor TOBJ inside SCS Project Base Path: %r", (final_tex_str,)) + # now try to retrive settings for the textures from TOBJ + if tex_type in created_textures and created_textures[tex_type]: + final_tex_str = getattr(material.scs_props, "shader_texture_" + tex_type, "") + tobj_abs_path = _path.get_tobj_path_from_shader_texture(final_tex_str) + settings, map_type = _tobj_imp.get_settings_and_type(tobj_abs_path) + created_tex_settings[tex_type] = settings + # override shader data for identifying used attributes and textures in UI if override_back_data: @@ -470,7 +678,7 @@ def set_shader_data_to_material(material, section, is_import=False, override_bac material["scs_shader_attributes"] = shader_data # setup nodes for 3D view visualization - _shader.setup_nodes(material, preset_effect, created_attributes, created_textures, override_back_data) + _shader.setup_nodes(material, preset_effect, created_attributes, created_textures, created_tex_settings, override_back_data) # setup uv mappings to nodes later trough dedicated function, so proper validation is made on tex coord bindings for mapping_data in created_tex_mappings: @@ -505,6 +713,9 @@ def reload_tobj_settings(material, tex_type): setattr(material.scs_props, shader_texture_str + "_map_type", map_type) setattr(material.scs_props, shader_texture_str + "_tobj_load_time", str(os.path.getmtime(tobj_file))) + # apply reloaded settings to shader + _shader.set_texture_settings(material, tex_type, settings) + def find_preset(material_effect, material_textures): """Tries to find suitable Shader Preset (as defined in shader_presets.txt file) for imported shader. If it cannot be found, it will return None. @@ -562,3 +773,65 @@ def find_preset(material_effect, material_textures): return preset_section.get_prop_value("PresetName"), preset_section return None, None + + +def set_texture_settings_to_node(tex_node, settings): + """Sets TOBJ settings to given texture node and it's assigned image. + + :param tex_node: texture image node to which settings should be applied + :type tex_node: bpy.types.ShaderNodeTexImage + :param settings: binary string of TOBJ settings gotten from tobj import + :type settings: str + """ + + image = tex_node.image + + # linear colorspace + if settings[0] == "1" and image: + image.colorspace_settings.name = "Linear" + else: + image.colorspace_settings.name = "sRGB" + + # tsnormal option + if settings[1] == "1" and image: + if image.filepath[-4:] in (".tga", ".dds"): + image.colorspace_settings.name = "Non-Color" + elif image.filepath[-4:] == ".png" and image.is_float: + image.colorspace_settings.name = "Linear" + + # addr + if settings[2] == "1" and settings[3] == "1": + tex_node.extension = "REPEAT" + else: + tex_node.extension = "EXTEND" + + +def has_valid_color_management(scene): + """Gets validity of color management for rendering SCS object. + + :param scene: scene for which we are checking validity + :type scene: bpy.types.Scene + :return: True if scene colormanagement is valid; False otherwise + :rtype: bool + """ + + if not scene: + return False + + display_settings = scene.display_settings + view_settings = scene.view_settings + + is_proper_display_device = display_settings.display_device == "sRGB" + is_proper_view_transform = view_settings.view_transform == "Standard" + is_proper_look = view_settings.look == "None" + is_proper_exposure = view_settings.exposure == 0.0 + is_proper_gamma = view_settings.gamma == 1.0 + + valid_color_management = ( + is_proper_display_device and + is_proper_view_transform and + is_proper_look and + is_proper_exposure and + is_proper_gamma + ) + return valid_color_management diff --git a/addon/io_scs_tools/utils/mesh.py b/addon/io_scs_tools/utils/mesh.py index 54be0ff..3d089b8 100644 --- a/addon/io_scs_tools/utils/mesh.py +++ b/addon/io_scs_tools/utils/mesh.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy import bmesh @@ -113,7 +113,7 @@ def make_per_face_rgb_layer(mesh, rgb_layer, layer_name): def make_points_to_weld_list(mesh_vertices, mesh_normals, mesh_rgb, mesh_rgba, equal_decimals_count): - """Makes a list of duplicated vertices indices. Each item in lists represents indices of vertices with same position and normal.""" + """Makes a map of duplicated vertices indices into it's original counter part.""" # take first present vertex color data if mesh_rgb: @@ -124,7 +124,6 @@ def make_points_to_weld_list(mesh_vertices, mesh_normals, mesh_rgb, mesh_rgba, e mesh_final_rgba = {} posnorm_dict_tmp = {} - posnorm_dict = {} perc = 10 ** equal_decimals_count # represent precision for duplicates for val_i, val in enumerate(mesh_vertices): @@ -140,9 +139,18 @@ def make_points_to_weld_list(mesh_vertices, mesh_normals, mesh_rgb, mesh_rgba, e posnorm_dict_tmp[key] = [val_i] else: posnorm_dict_tmp[key].append(val_i) - posnorm_dict[key] = posnorm_dict_tmp[key] - return posnorm_dict.values() + # create map for quick access to original vertex index via double vertex indices: (key: double vert index; value: original vert index) + verts_map = {} + for indices in posnorm_dict_tmp.values(): + # ignore entry if only one vertex was added to it (means there was no duplicates for it) + if len(indices) <= 1: + continue + + for idx in indices[1:]: + verts_map[idx] = indices[0] # fist index is original, rest are duplicates + + return verts_map def set_sharp_edges(mesh, mesh_edges): @@ -312,10 +320,8 @@ def bm_make_faces(bm, faces, points_to_weld_list): new_f_idx = [] for v_idx in f_idx: new_v_idx = v_idx - for rec in points_to_weld_list: - if v_idx in rec: - new_v_idx = rec[0] - break + if v_idx in points_to_weld_list: + new_v_idx = points_to_weld_list[v_idx] new_f_idx.append(new_v_idx) try: @@ -429,12 +435,12 @@ def bm_make_vc_layer(pim_version, bm, vc_layer_name, vc_layer_data, multiplier=1 vcol = vc_layer_data[face_i][loop_i][:3] alpha = vc_layer_data[face_i][loop_i][3] - vcol = (vcol[0] / 2 / multiplier, vcol[1] / 2 / multiplier, vcol[2] / 2 / multiplier) + vcol = (vcol[0] / 2 / multiplier, vcol[1] / 2 / multiplier, vcol[2] / 2 / multiplier, 1.0) loop[color_lay] = vcol if alpha != -1.0: assert color_a_lay - vcol_a = (alpha / 2 / multiplier,) * 3 + vcol_a = (alpha / 2 / multiplier,) * 3 + (1.0,) loop[color_a_lay] = vcol_a @@ -502,6 +508,28 @@ def bm_prepare_mesh_for_export(mesh, transformation_matrix, triangulate=False, f return faces_mapping +def get_mesh_for_normals(mesh): + """Get mesh prepared to be used for normals export. + 1. enables auto smooth and sets angle to 180 degress so that split normaals can be calculated (but no normals gets splitted because of it) + 2. calculates split normals + + :param mesh: original mesh data block of object + :type mesh: bpy.types.Mesh + :return: mew mesh prepared for normals export + :rtype: bpy.types.Mesh + """ + new_mesh = mesh.copy() + + # if user is not using auto smooth, then apply it now just for the porpuse of proper normals split calculation. + if not new_mesh.use_auto_smooth: + new_mesh.use_auto_smooth = True + new_mesh.auto_smooth_angle = 3.14 + + new_mesh.calc_normals_split() + + return new_mesh + + def cleanup_mesh(mesh): """Frees normals split and removes mesh if possible. @@ -552,8 +580,8 @@ def vcoloring_rebake(mesh, vcolor_arrays, old_array_hash): # correct buffers size if needed for i in range(0, 4): - if len(vcolor_arrays[i]) != len(mesh.loops) * 3: - vcolor_arrays[i].resize((len(mesh.loops) * 3,)) + if len(vcolor_arrays[i]) != len(mesh.loops) * 4: + vcolor_arrays[i].resize((len(mesh.loops) * 4,)) color_loops = mesh_vcolors[_VCT_consts.ColoringLayersTypes.Color].data decal_loops = mesh_vcolors[_VCT_consts.ColoringLayersTypes.Decal].data diff --git a/addon/io_scs_tools/utils/object.py b/addon/io_scs_tools/utils/object.py index 9304147..d80a881 100644 --- a/addon/io_scs_tools/utils/object.py +++ b/addon/io_scs_tools/utils/object.py @@ -16,15 +16,12 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy import bmesh from bpy_extras import object_utils as bpy_object_utils from mathutils import Vector -from io_scs_tools.consts import Part as _PART_consts -from io_scs_tools.internals import looks as _looks -from io_scs_tools.internals import inventory as _inventory from io_scs_tools.utils.printout import lprint from io_scs_tools.utils import math as _math from io_scs_tools.utils import name as _name @@ -50,9 +47,9 @@ def gather_scs_roots(objs): """Gets all the SCS root objects from given objects. :param objs: Blender objects - :type objs: list of bpy.types.Object + :type objs: collections.Iterable[bpy.types.Object] :return: list of roots, if none are found then empty list - :rtype: list of bpy.types.Object + :rtype: list[bpy.types.Object] """ roots = {} for obj in objs: @@ -63,86 +60,6 @@ def gather_scs_roots(objs): return roots.values() -def make_scs_root_object(context, dialog=False): - # FAIRLY SMART SELECTION OF SCS GAME OBJECT CONTENT - scs_game_object_content = [] - for obj in context.selected_objects: - if not obj.parent: - scs_game_object_content.append(obj) - else: - if not obj.parent.select: - scs_game_object_content.append(obj) - - # UN-PARENT OBJECTS - un-parent and keep transformations, so objects stays on same place when re-parenting to new root - if context.active_object and len(context.selected_objects) > 0: - bpy.ops.object.parent_clear(type='CLEAR_KEEP_TRANSFORM') - - # ADD SCS ROOT OBJECT (EMPTY OBJECT) - bpy.ops.object.empty_add( - view_align=False, - location=context.scene.cursor_location, - ) - - # SET PROPERTIES - - name = _name.get_unique("game_object", bpy.data.objects) - bpy.context.active_object.name = name - new_scs_root = bpy.data.objects.get(name) - new_scs_root.scs_props.scs_root_object_export_enabled = True - new_scs_root.scs_props.empty_object_type = 'SCS_Root' - - # SET A NEW NAME DIALOG - if dialog: - bpy.ops.object.add_scs_root_object_dialog_operator('INVOKE_DEFAULT') - - # PARENT OBJECTS TO SCS ROOT OBJECT - part_inventory = new_scs_root.scs_object_part_inventory - if len(scs_game_object_content) > 0: - - bpy.ops.object.select_all(action='DESELECT') - - new_scs_root_mats = [] - # select content object for parenting later - for obj in scs_game_object_content: - obj.select = True - - # fix old parent with new children number and cleaned looks - if obj.parent: - ex_parent_obj = obj.parent - obj.parent = None - - ex_parent_obj.scs_cached_num_children = len(ex_parent_obj.children) - - ex_parent_scs_root = get_scs_root(ex_parent_obj) - if ex_parent_scs_root: - _looks.clean_unused(ex_parent_scs_root) - - obj.scs_props.parent_identity = new_scs_root.name - obj.scs_cached_num_children = len(obj.children) - - for slot in obj.material_slots: - if slot.material and slot.material not in new_scs_root_mats: - new_scs_root_mats.append(slot.material) - - _looks.add_materials(new_scs_root, new_scs_root_mats) - - bpy.ops.object.parent_set(type='OBJECT', keep_transform=False) - bpy.ops.object.select_all(action='DESELECT') - new_scs_root.select = True - - # fix children count to prevent persistent to hook up - new_scs_root.scs_cached_num_children = len(new_scs_root.children) - - for part_name in collect_parts_on_root(new_scs_root): - _inventory.add_item(part_inventory, part_name) - - # MAKE DEFAULT PART IF THERE IS NO PARTS - if len(part_inventory) == 0: - _inventory.add_item(part_inventory, _PART_consts.default_name) - - return new_scs_root - - def sort_out_game_objects_for_export(objects): """Takes initial selection (list of Blender Objects) and returns a dictionary where key is 'SCS Root Object' and its value is a list of Mesh and/or Locator Objects which @@ -355,7 +272,7 @@ def make_mesh_from_verts_and_faces(verts, faces, convex_props): new_bm.free() # Add the mesh as an object into the scene with this utility module. - new_object = bpy_object_utils.object_data_add(bpy.context, new_mesh).object + new_object = bpy_object_utils.object_data_add(bpy.context, new_mesh) new_object = set_collider_props(new_object, convex_props) new_object.location = convex_props['location'] new_object.rotation_euler = convex_props['rotation'] @@ -363,11 +280,13 @@ def make_mesh_from_verts_and_faces(verts, faces, convex_props): return new_object -def make_objects_selected(objects): +def make_objects_selected(objects, active_object=None): """Select only the given objects. Deselect all others. :param objects: list of objects to be selected :type objects: list, tuple + :param active_object: object to be made active; if None active object remains unchanged + :type active_object: bpy.types.Object :rtype: None """ bpy.ops.object.select_all(action='DESELECT') @@ -375,7 +294,10 @@ def make_objects_selected(objects): for obj in objects: # select only real objects if obj: - obj.select = True + obj.select_set(True) + + if active_object: + bpy.context.view_layer.objects.active = active_object def pick_objects_from_selection(operator_object, needs_active_obj=True, obj_type='MESH'): @@ -416,23 +338,38 @@ def pick_objects_from_selection(operator_object, needs_active_obj=True, obj_type return None, active_object -def create_convex_data(objects, convex_props={}, create_hull=False): - """Creates a convex hull from selected objects...""" +def create_convex_data(objects, convex_props={}, return_hull_object=False, max_face_count=256): + """Creates a convex hull from selected objects... - # SAVE ORIGINAL SELECTION + :param objects: list of blender objects to make convex hull from + :type objects: list[bpy.types.Object] + :param convex_props: existing convexr properties gotten with get_collider_props + :type convex_props: dict + :param return_hull_object: should convex data be as normal blender mesh object? + :type return_hull_object: bool + :param max_face_count: maximum number of faces that returned hull object can have, in interval (256, 6) + :type max_face_count: int + :return: tuple[(verts, faces, bb_size, bb_center), convex_props, returned_hull_object) + :rtype: tuple[tuple, dict, bpy.types.Object] + """ + + assert 6 <= max_face_count <= 256 + + # save original selection original_selection = [] for obj in bpy.context.selected_objects: original_selection.append(obj) - # MAKE A SELECTION FROM MESH OBJECTS + # save original active + original_active = bpy.context.active_object + + # make a selection from mesh objects make_objects_selected(objects) - # IF MULTIPLE OBJECTS, CREATE JOINED MESH FROM ALL SELECTED MESHES + # if multiple objects, create joined mesh from all selected meshes bpy.ops.object.duplicate_move() - original_active = None if bpy.context.active_object not in bpy.context.selected_objects: - original_active = bpy.context.active_object - bpy.context.scene.objects.active = bpy.context.selected_objects[0] + bpy.context.view_layer.objects.active = bpy.context.selected_objects[0] if len(original_selection) > 1: bpy.ops.object.join() @@ -442,123 +379,112 @@ def create_convex_data(objects, convex_props={}, create_hull=False): for modifier in obj.modifiers: bpy.ops.object.modifier_apply(modifier=modifier.name) - # if convex locator creation than decimate joined object to make sure - # mesh is not to big for convex locator - if not create_hull: - modifier = obj.modifiers.new("triangulate", "TRIANGULATE") - bpy.ops.object.modifier_apply(modifier=modifier.name) + # get properties from the resulting object + if not convex_props: + convex_props = {'name': obj.name} + convex_props = get_collider_props(obj, convex_props) - if len(obj.data.polygons) > 256: # actually decimate if face count is to big - initial_polycount = len(obj.data.polygons) + # create convex hull data + mesh = obj.data + bm = bmesh.new() + bm.from_mesh(mesh) - modifier = obj.modifiers.new("decimate", "DECIMATE") - while modifier.face_count > 256 or modifier.face_count < 200: + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.01) + + # delete everything except verts, otherwise convex hull doesn't work as expected + for face in bm.faces: + bm.faces.remove(face) + for edge in bm.edges: + bm.edges.remove(edge) + + convex_hull_res = bmesh.ops.convex_hull(bm, input=bm.verts) + + # now again remove everything except real convex hull geometry + for geom_type in ('geom_interior', 'geom_unused', 'geom_holes'): + for item in convex_hull_res[geom_type]: + try: + if isinstance(item, bmesh.types.BMVert): + bm.verts.remove(item) + elif isinstance(item, bmesh.types.BMEdge): + bm.edges.remove(item) + elif isinstance(item, bmesh.types.BMFace): + bm.faces.remove(item) + except ReferenceError: + continue + + # transform back to mesh + bm.normal_update() + bm.to_mesh(mesh) + bm.free() - # divide ratio by 2 until we get into desired count of faces - diff = abs(modifier.ratio) / 2.0 - if modifier.face_count > 256: - diff *= -1.0 + # if convex locator creation then decimate convex hull even more to make sure mesh is not to big for convex locator + if not return_hull_object and len(mesh.polygons) > max_face_count: - modifier.ratio += diff - bpy.context.scene.update() # invoke scene update to get updated modifier + initial_polycount = len(mesh.polygons) - bpy.ops.object.modifier_apply(modifier=modifier.name) + modifier = obj.modifiers.new("decimate", "DECIMATE") + bpy.context.view_layer.update() - lprint("W Mesh used for convex locator was decimated because it had to many faces for convex locator.\n\t " + - "Maximum triangles count is 256 but mesh had %s triangles.", (initial_polycount,)) + while (modifier.face_count > max_face_count or modifier.face_count < max(max_face_count - 10, 10)) and modifier.ratio > 0.01: - # GET PROPERTIES FROM THE RESULTING OBJECT - if not convex_props: - convex_props = {'name': obj.name} - convex_props = get_collider_props(obj, convex_props) + # divide ratio by 2 until we get into desired count of faces + diff = abs(modifier.ratio) / 2.0 + if modifier.face_count > max_face_count: + diff *= -1.0 - # CREATE CONVEX HULL DATA - geoms = {} - mesh = obj.data - bm = bmesh.new() - bm.from_mesh(mesh) - hull = None + modifier.ratio += diff + bpy.context.view_layer.update() # invoke depsgraph update to get updated modifier + print("WHILE:", modifier.face_count, modifier.ratio) + + bpy.ops.object.modifier_apply(modifier=modifier.name) + + lprint("I Mesh used for convex locator was decimated because it had to many faces for convex locator.\n\t " + + "Maximum triangles count is %i but mesh had %s triangles.", (max_face_count, initial_polycount)) + + # retrieve convex data + verts = [] + faces = [] + min_val = [None, None, None] + max_val = [None, None, None] + for vert in mesh.vertices: + verts.append(tuple(vert.co)) + + # Bounding Box data evaluation + min_val, max_val = _math.evaluate_minmax(verts[-1], min_val, max_val) + + for poly in mesh.polygons: + assert len(poly.vertices) == 3 + faces.append(tuple(poly.vertices)) + + # bbox data creation + bbox, bbcenter = _math.get_bb(min_val, max_val) + geom = (verts, faces, bbox, bbcenter) + + # should have volume, if it doesn't it's not really a shape + for axis_size in bbox: + if axis_size == 0: + geom = None + + # create convex hull object to return resulting_convex_object = None - try: - hull = bmesh.ops.convex_hull( - bm, - input=bm.verts, - use_existing_faces=False, - ) - except RuntimeError: - import traceback - - trace_str = traceback.format_exc().replace("\n", "\n\t ") - lprint("E Problem creating convex hull:\n\t %s", - (trace_str,), - report_errors=1, - report_warnings=1) - - # RETRIEVE CONVEX DATA - # print(' > hull: %s' % str(hull)) - if hull: - for geom in ('geom', 'geom_unused', 'geom_holes', 'geom_interior'): - geometry = hull[geom] - # print('\n > geometry: %r' % geom) - if geometry: - verts = [] - verts_index_correction = {} - vert_i = 0 - edges = [] - faces = [] - min_val = [None, None, None] - max_val = [None, None, None] - for item in geometry: - # print(' > item[%i]: %s' % (item.index, str(item))) - # print(' type: %s' % str(type(item))) - - # VERTICES - if isinstance(item, bmesh.types.BMVert): - vert_orig_index = item.index - verts.append(item.co) - - # Bounding Box data evaluation - min_val, max_val = _math.evaluate_minmax(item.co, min_val, max_val) - - verts_index_correction[vert_orig_index] = vert_i - vert_i += 1 - - # EDGES - # if isinstance(item, bmesh.types.BMEdge): - # edges.append(item) - # print(' > edge[%i]: %s' % (item.index, str(item.verts))) - - # FACES - if isinstance(item, bmesh.types.BMFace): - face_verts_correction = [] - for vert in item.verts: - face_verts_correction.append(verts_index_correction[vert.index]) - # print(' > face[%i]: %s - %s' % (item.index, str(face_verts), str(face_verts_correction))) - faces.append(face_verts_correction) - - # Bounding Box data creation - bbox, bbcenter = _math.get_bb(min_val, max_val) - geoms[geom] = (verts, faces, bbox, bbcenter) - - # CREATE CONVEX HULL MESH OBJECT - if geom == 'geom' and create_hull: - resulting_convex_object = make_mesh_from_verts_and_faces(verts, faces, convex_props) - - # make resulting object is on the same layers as joined object - resulting_convex_object.layers = obj.layers + if return_hull_object and geom: + resulting_convex_object = make_mesh_from_verts_and_faces(verts, faces, convex_props) - bm.free() + # make sure resulting object is on the same collections as joined object + for col in resulting_convex_object.users_collection: + col.objects.unlink(resulting_convex_object) + + for col in obj.users_collection: + col.objects.link(resulting_convex_object) make_objects_selected((object_to_delete,)) bpy.ops.object.delete(use_global=True) - # MAKE ORIGINAL ACTIVE OBJECT ACTIVE AGAIN - if original_active: - bpy.context.scene.objects.active = original_active - if original_selection: - make_objects_selected(original_selection) + # make original active object active again + if original_selection or original_active: + make_objects_selected(original_selection, active_object=original_active) - return geoms, convex_props, resulting_convex_object + return geom, convex_props, resulting_convex_object def add_collider_convex_locator(geom_data, convex_props, locator=None): @@ -566,8 +492,6 @@ def add_collider_convex_locator(geom_data, convex_props, locator=None): and creates new Convex Collision Locator.""" if not locator: bpy.ops.object.empty_add( - type='PLAIN_AXES', - view_align=False, location=convex_props['location'], rotation=convex_props['rotation'], ) @@ -603,12 +527,10 @@ def make_mesh_convex_from_locator(obj): return new_object -def create_collider_convex_locator(geoms, convex_props, mesh_objects, delete_mesh_objects): +def create_collider_convex_locator(geom, convex_props, mesh_objects, delete_mesh_objects): locator = None - if geoms: - for geom in geoms: - if geom == 'geom': - locator = add_collider_convex_locator(geoms[geom], convex_props) + if geom: + locator = add_collider_convex_locator(geom, convex_props) # DEBUG PRINTOUT # print('\n > geom: %s' % str(geom)) @@ -622,7 +544,7 @@ def create_collider_convex_locator(geoms, convex_props, mesh_objects, delete_mes make_objects_selected(mesh_objects) bpy.ops.object.delete(use_global=True) if locator: - locator.select = True + locator.select_set(True) update_convex_hull_margins(locator) return locator @@ -650,25 +572,25 @@ def show_loc_type(objects, loc_type, pref_type=None, hide_state=None, view_only= if hide_state is None: - hide_state = not obj.hide + hide_state = not obj.hide_get() - obj.hide = hide_state + hide_set(obj, hide_state) elif view_only: - obj.hide = True + hide_set(obj, True) else: if hide_state is None: - hide_state = not obj.hide + hide_state = not obj.hide_get() - obj.hide = hide_state + hide_set(obj, hide_state) elif view_only: - obj.hide = True + hide_set(obj, True) def get_object_materials(obj): @@ -712,9 +634,10 @@ def disable_modifier(modifier, disabled_modifiers): :param disabled_modifiers: :return: """ - if modifier.show_viewport: + if modifier.name not in disabled_modifiers: + disabled_modifiers[modifier.name] = (modifier.show_viewport, modifier.show_render) modifier.show_viewport = False - disabled_modifiers.append(modifier.name) + modifier.show_render = False return disabled_modifiers @@ -727,7 +650,7 @@ def disable_modifiers(obj, modifier_type_to_disable='EDGE_SPLIT', inverse=False) :param inverse: :return: """ - disabled_modifiers = [] + disabled_modifiers = dict() for modifier in obj.modifiers: if modifier_type_to_disable == 'ANY': disabled_modifiers = disable_modifier(modifier, disabled_modifiers) @@ -746,8 +669,7 @@ def get_mesh(obj): Returns Mesh data of provided Object (un)affected with specified Modifiers, which are evaluated as within provided Scene. """ - scene = bpy.context.scene - disabled_modifiers = [] + disabled_modifiers = dict() # always disable armature modifiers from SCS animations # to prevent vertex rest position change because of the animation @@ -759,20 +681,17 @@ 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.extend(disable_modifiers(obj, modifier_type_to_disable='EDGE_SPLIT')) - mesh = obj.to_mesh(scene, True, 'PREVIEW') - else: - mesh = obj.data + disabled_modifiers.update(disable_modifiers(obj, modifier_type_to_disable='EDGE_SPLIT')) else: - if _get_scs_globals().export_apply_modifiers: - mesh = obj.to_mesh(scene, True, 'PREVIEW') - else: + if not _get_scs_globals().export_apply_modifiers: if _get_scs_globals().export_include_edgesplit: - disabled_modifiers.extend(disable_modifiers(obj, modifier_type_to_disable='EDGE_SPLIT', inverse=True)) - mesh = obj.to_mesh(scene, True, 'PREVIEW') + disabled_modifiers.update(disable_modifiers(obj, modifier_type_to_disable='EDGE_SPLIT', inverse=True)) else: - disabled_modifiers.extend(disable_modifiers(obj, modifier_type_to_disable='ANY', inverse=True)) - mesh = obj.to_mesh(scene, True, 'PREVIEW') + disabled_modifiers.update(disable_modifiers(obj, modifier_type_to_disable='ANY', inverse=True)) + + depsgraph = bpy.context.evaluated_depsgraph_get() + obj_eval = obj.evaluated_get(depsgraph) + mesh = obj_eval.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph) restore_modifiers(obj, disabled_modifiers) @@ -786,9 +705,10 @@ def restore_modifiers(obj, disabled_modifiers): :param disabled_modifiers: :return: """ - for modifier in obj.modifiers: - if modifier.name in disabled_modifiers: - modifier.show_viewport = True + for modifier_name, show_states in disabled_modifiers.items(): + obj.modifiers[modifier_name].show_viewport = show_states[0] + obj.modifiers[modifier_name].show_render = show_states[1] + lprint("D Restored modifier with name: %r on object with name: %r", (modifier_name, obj.name)) def get_stream_vgs(obj): @@ -853,23 +773,30 @@ def create_locator_empty(name, loc, rot=(0, 0, 0), scale=(1, 1, 1), size=1.0, da else: location = _convert.change_to_scs_xyz_coordinates(loc, _get_scs_globals().import_scale) - bpy.ops.object.empty_add( - type='PLAIN_AXES', - view_align=False, - location=location, - rotation=rot, - ) - - bpy.context.active_object.name = _name.get_unique(name, bpy.data.objects, sep=".") - locator = bpy.context.active_object + unique_name = _name.get_unique(name, bpy.data.objects, sep=".") + locator = bpy.data.objects.new(unique_name, None) + locator.empty_display_type = 'PLAIN_AXES' locator.scs_props.object_identity = locator.name + # link to active layer and scene and make it active and selected + bpy.context.view_layer.active_layer_collection.collection.objects.link(locator) + bpy.context.view_layer.objects.active = locator + locator.select_set(True) + + # fix scene objects count to avoid callback of new object + bpy.context.scene.scs_cached_num_objects = len(bpy.context.scene.objects) + + locator.location = location + if rot_quaternion: locator.rotation_mode = 'QUATERNION' if blend_coords: locator.rotation_quaternion = rot_quaternion else: locator.rotation_quaternion = _convert.change_to_blender_quaternion_coordinates(rot_quaternion) + else: + locator.rotation_mode = 'XYZ' + locator.rotation_euler = rot locator.scale = scale locator.scs_props.empty_object_type = 'Locator' @@ -965,3 +892,126 @@ def get_other_object(obj_iterable, original_obj): return sel_obj return None + + +def set_locators_original_size_and_type(obj): + """Set original drawing style for Empty object. + + :param obj: Blender Object + :type obj: bpy.types.Object + """ + # _object.set_attr_if_different(obj, "empty_display_size", obj.scs_props.locators_orig_display_size) + if obj.empty_display_size != obj.scs_props.locators_orig_display_size: + obj.empty_display_size = obj.scs_props.locators_orig_display_size + obj.scs_props.locators_orig_display_size = 0.0 + if obj.empty_display_type != obj.scs_props.locators_orig_display_type: + obj.empty_display_type = obj.scs_props.locators_orig_display_type + obj.scs_props.locators_orig_display_type = '' + + +def store_locators_original_display_size_and_type(obj): + """Set Prefab Locator drawing style for Empty object. + + :param obj: Blender Object + :type obj: bpy.types.Object + """ + if obj.scs_props.locators_orig_display_size == 0.0: + set_attr_if_different(obj.scs_props, "locators_orig_display_size", obj.empty_display_size) + set_attr_if_different(obj.scs_props, "locators_orig_display_type", obj.empty_display_type) + + +def set_locators_prefab_display_size_and_type(obj): + """Set Prefab Locator drawing style for Empty object. + + :param obj: Blender Object + :type obj: bpy.types.Object + """ + new_draw_size = _get_scs_globals().locator_empty_size * _get_scs_globals().locator_size + set_attr_if_different(obj, "empty_display_size", new_draw_size) + set_attr_if_different(obj, "empty_display_type", 'PLAIN_AXES') + + +def set_locators_model_display_size_and_type(obj): + """Set Model Locator drawing style for Empty object. + + :param obj: Blender Object + :type obj: bpy.types.Object + """ + new_draw_size = _get_scs_globals().locator_empty_size * _get_scs_globals().locator_size + set_attr_if_different(obj, "empty_display_size", new_draw_size) + set_attr_if_different(obj, "empty_display_type", 'PLAIN_AXES') + + +def set_locators_coll_display_size_and_type(obj): + """Set Collision Locator drawing style for Empty object. + + :param obj: Blender Object + :type obj: bpy.types.Object + """ + new_draw_size = 0.5 + if obj.scs_props.locator_collider_type == 'Box': + bigest_value = max(obj.scs_props.locator_collider_box_x, obj.scs_props.locator_collider_box_y, obj.scs_props.locator_collider_box_z) + new_draw_size = bigest_value * 0.8 + elif obj.scs_props.locator_collider_type in ('Sphere', 'Capsule', 'Cylinder'): + new_draw_size = obj.scs_props.locator_collider_dia * 0.8 + elif obj.scs_props.locator_collider_type == 'Convex': + bbox = obj.scs_props.get("coll_convex_bbox", None) + bbcenter = obj.scs_props.get("coll_convex_bbcenter", None) + if bbox and bbcenter: + scaling = _math.scaling_width_margin(bbox, obj.scs_props.locator_collider_margin) + val = [] + for axis in range(3): + val.append((bbox[axis] + abs(bbcenter[axis])) * scaling[axis]) + new_draw_size = max(val) * 0.5 + set_attr_if_different(obj, "empty_display_size", new_draw_size) + set_attr_if_different(obj, "empty_display_type", 'PLAIN_AXES') + + +def hide_set(obj, state, view_layer=None): + """Sets hide state on the object taking into account it's membership in current or given view layer. + So in case object is not found in view layer, it's hide state doesn't change. + + :param obj: object to hide/unhide + :type obj: bpy.types.Object + :param state: True unhide; False hide + :type state: bool + :param view_layer: view layer on which hide state should be set; if None hide state is set on active view layer in the bpy context + :type view_layer: bpy.types.ViewLayer + :return: True if state was applied, False otherwise + :rtype: bool + """ + if view_layer: + if obj.name in view_layer.objects: + obj.hide_set(state, view_layer=view_layer) + return True + else: + if obj.name in bpy.context.view_layer.objects: + obj.hide_set(state) + return True + + return False + + +def select_set(obj, state, view_layer=None): + """Sets select state on the object taking into account it's membership in current or given view layer. + So in case object is not found in view layer, it's hide state doesn't change. + + :param obj: object to select/deselect + :type obj: bpy.types.Object + :param state: True select; False deselect + :type state: bool + :param view_layer: view layer on which select state should be set; if None select state is set on active view layer in the bpy context + :type view_layer: bpy.types.ViewLayer + :return: True if state was applied, False otherwise + :rtype: bool + """ + if view_layer: + if obj.name in view_layer.objects: + obj.select_set(state, view_layer=view_layer) + return True + else: + if obj.name in bpy.context.view_layer.objects: + obj.select_set(state) + return True + + return False diff --git a/addon/io_scs_tools/utils/path.py b/addon/io_scs_tools/utils/path.py index aab1c9d..488662c 100644 --- a/addon/io_scs_tools/utils/path.py +++ b/addon/io_scs_tools/utils/path.py @@ -16,7 +16,7 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy @@ -225,7 +225,15 @@ def is_valid_shader_texture_path(shader_texture): def is_valid_shader_presets_library_path(): """It returns True if there is valid "*.txt" file in the Shader Presets Library directory, otherwise False.""" - shader_presets_filepath = _get_scs_globals().shader_presets_filepath + + scs_globals = _get_scs_globals() + + # check if default presets path is valid + if not scs_globals.shader_presets_use_custom: + return get_shader_presets_filepath() != "" + + # check if set custom preset path is valid + shader_presets_filepath = scs_globals.shader_presets_filepath if shader_presets_filepath != "": if shader_presets_filepath.startswith("//"): # RELATIVE PATH shader_presets_abs_path = get_abs_path(shader_presets_filepath) @@ -384,6 +392,23 @@ def get_texture_path_from_tobj(tobj_filepath, raw_value=False): :rtype: str | None """ + return get_texture_paths_from_tobj(tobj_filepath, raw_value=raw_value, first_only=True)[0] + + +def get_texture_paths_from_tobj(tobj_filepath, raw_value=False, first_only=False): + """Get absolute path(s) of textures from given tobj filepath. + If raw value is requested returned path(s) are direct values written in TOBJ. + If first only is requested only first path is returned. + + :param tobj_filepath: absolute tobj file path + :type tobj_filepath: str + :param raw_value: flag for indicating if texture path(s) shall be returned as it's written in TOBJ + :type raw_value: bool + :param first_only: flag for requesting only first entry from TOBJ map names (only first texture) + :type first_only: bool + :return: absolute texture file path(s) if found or None + :rtype: tuple[str] | None + """ from io_scs_tools.internals.containers.tobj import TobjContainer container = TobjContainer.read_data_from_file(tobj_filepath, skip_validation=raw_value) @@ -393,12 +418,25 @@ def get_texture_path_from_tobj(tobj_filepath, raw_value=False): return None if raw_value: - return container.map_names[0] + if first_only: + return container.map_names[0], + + return tuple(container.map_names) - if container.map_names[0][0] == "/": - return get_abs_path("//" + container.map_names[0][1:]) + abs_texture_paths = [] + for map_name in container.map_names: + if map_name[0] == "/": + curr_abs_tobj_path = get_abs_path("//" + map_name[1:]) + else: + curr_abs_tobj_path = os.path.join(tobj_dir, map_name) + + # directly intercept and return first texture path + if first_only: + return curr_abs_tobj_path, + + abs_texture_paths.append(curr_abs_tobj_path) - return os.path.join(tobj_dir, container.map_names[0]) + return tuple(abs_texture_paths) def get_texture_extens_and_strip_path(texture_path): @@ -822,3 +860,23 @@ def copytree(src, dest): os.makedirs(dest_path) shutil.copyfile(os.path.join(root, file), os.path.join(dest_path, file)) + + +def get_tree_size(src): + """Return total size of files in given path and subdirs. + + :param src: source path to get size from + :param src: str + """ + total = 0 + + if not os.path.isdir(src) and not os.path.isfile(src): + return total + + for entry in os.scandir(src): + if entry.is_dir(follow_symlinks=False): + total += get_tree_size(entry.path) + else: + total += entry.stat(follow_symlinks=False).st_size + + return total diff --git a/addon/io_scs_tools/utils/printout.py b/addon/io_scs_tools/utils/printout.py index 03b8bdb..63445b6 100644 --- a/addon/io_scs_tools/utils/printout.py +++ b/addon/io_scs_tools/utils/printout.py @@ -204,7 +204,7 @@ def lprint(string, values=(), report_errors=0, report_warnings=0): print(text) file_logger.write(text + "\n") file_logger.flush() - bpy.ops.wm.show_3dview_report('INVOKE_DEFAULT', message=text) + bpy.ops.wm.scs_tools_show_3dview_report('INVOKE_DEFAULT', message=text) # make sure to tag any 3D view for redraw so errors OpenGL drawing is triggered from io_scs_tools.utils.view3d import tag_redraw_all_view3d diff --git a/addon/io_scs_tools/utils/property.py b/addon/io_scs_tools/utils/property.py index bb6285c..91cf5bb 100644 --- a/addon/io_scs_tools/utils/property.py +++ b/addon/io_scs_tools/utils/property.py @@ -16,26 +16,35 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software +import bpy from bpy.props import EnumProperty +from idprop.types import IDPropertyArray, IDPropertyGroup -def get_by_type(blender_property, from_object=None): - """Gets value of Blender ID property +def get_default(from_object, prop_name): + """Gets default value of Blender ID property - :param blender_property: any Blender ID property from "bpy.types" class - :type blender_property: tuple - :param from_object: object from which property value should be read - :type from_object: object - :return: default value or value from given object of Blender ID property; if given object doesn't have given property attribute None is returned - :rtype: object | None + :param from_object: instance of property object from which property value should be read + :type from_object: bpy.types.bpy_struct + :param prop_name: any Blender ID property name registred in given object + :type prop_name: str + :return: default value of Blender ID property; if given object doesn't have given property attribute or is None, then None is returned + :rtype: any | None """ if from_object is None: - return blender_property[1]['default'] + return None + + if prop_name not in from_object.bl_rna.properties: + return None + + bl_rna_prop = from_object.bl_rna.properties[prop_name] + if getattr(bl_rna_prop, "is_array", False): + return bl_rna_prop.default_array[:] else: - return getattr(from_object, blender_property[1]['attr'], None) + return bl_rna_prop.default def get_filebrowser_display_type(is_image=False): @@ -60,3 +69,29 @@ def get_filebrowser_display_type(is_image=False): default=default_value, options={'HIDDEN'} ) + + +def get_id_prop_as_py_object(prop_value): + """Get blender ID Property represented as with python native types. + Nested group and collection properties are properly converted too, with the help of recursive calls. + + :param prop_value: id property to convert + :type prop_value: IDProperty + :return: ID Property converted to native python object types + :rtype: any + """ + + if isinstance(prop_value, IDPropertyGroup): + prop_dict = {} + for sub_prop_key, sub_prop_value in prop_value.items(): + prop_dict[sub_prop_key] = get_id_prop_as_py_object(sub_prop_value) + return prop_dict + elif isinstance(prop_value, IDPropertyArray): + return tuple(prop_value) + elif isinstance(prop_value, list): + prop_list = [] + for sub_prop_value in prop_value: + prop_list.append(get_id_prop_as_py_object(sub_prop_value)) + return prop_list + else: + return prop_value diff --git a/addon/io_scs_tools/utils/view3d.py b/addon/io_scs_tools/utils/view3d.py index f8eb923..d5b0d51 100644 --- a/addon/io_scs_tools/utils/view3d.py +++ b/addon/io_scs_tools/utils/view3d.py @@ -16,64 +16,33 @@ # # ##### END GPL LICENSE BLOCK ##### -# Copyright (C) 2013-2014: SCS Software +# Copyright (C) 2013-2019: SCS Software import bpy -from io_scs_tools.utils import get_scs_globals as _get_scs_globals -from io_scs_tools.utils.printout import lprint -def switch_local_view(show): - """Switches first space in VIEW_3D areas to local or switches back to normal - - :param show: True if local view should be shown; False for switching back to normal view - :type show: bool - """ - for area in bpy.context.screen.areas: - if area.type == 'VIEW_3D': - if show and area.spaces[0].local_view is None: - lprint("D Going into local view!") - override = { - 'window': bpy.context.window, - 'screen': bpy.context.screen, - 'blend_data': bpy.context.blend_data, - 'scene': bpy.context.scene, - 'region': area.regions[4], - 'area': area - } - bpy.ops.view3d.localview(override) - elif not show and area.spaces[0].local_view is not None: - lprint("D Returning from local view!") - override = { - 'window': bpy.context.window, - 'screen': bpy.context.screen, - 'blend_data': bpy.context.blend_data, - 'scene': bpy.context.scene, - 'region': area.regions[4], - 'area': area - } - bpy.ops.view3d.localview(override) - - if _get_scs_globals().preview_export_selection_active != show: - _get_scs_globals().preview_export_selection_active = show - # redraw properties panels because of preview property change - for area in bpy.context.screen.areas: - if area.type == "PROPERTIES": - area.tag_redraw() - - -def get_all_spaces(): +def get_all_spaces(screen=None): """Gets all the spaces in Blender which are type of bpy.types.SpaceView3D + :param screen: blender screen; if None is given all screens are taken from window manager + :type screen: bpy.types.Screen :return: all the spaces of type bpy.types.SpaceView3D :rtype: list of bpy.types.SpaceView3D """ + screens = set() + if not screen: + for wnd in bpy.context.window_manager.windows: + screens.add(wnd.screen) + else: + screens.add(screen) + spaces = [] - for area in bpy.context.screen.areas: - if area.type == 'VIEW_3D': - for space in area.spaces: - if isinstance(space, bpy.types.SpaceView3D): - spaces.append(space) + for screen in screens: + for area in screen.areas: + if area.type == 'VIEW_3D': + for space in area.spaces: + if isinstance(space, bpy.types.SpaceView3D): + spaces.append(space) return spaces @@ -81,22 +50,21 @@ def get_all_spaces(): def has_multiple_view3d_spaces(screen=None): """Tells if given blender screen has multile 3D views or not. - :param screen: blender screen + :param screen: blender screen; if None is given all screens are taken from window manager :type screen: bpy.types.Screen :return: True if at leas one 3d view is present; False otherwise :rtype: bool """ - screens = {} - if screen is None: - if len(bpy.data.window_managers) > 0: - for wnd in bpy.data.window_managers[0].windows: - screens[wnd.screen.name] = wnd.screen + screens = set() + if not screen: + for wnd in bpy.context.window_manager.windows: + screens.add(wnd.screen) else: - screens[screen.name] = screen + screens.add(screen) spaces_count = 0 - for scr in screens.values(): + for scr in screens: for area in scr.areas: if area.type == 'VIEW_3D': @@ -127,40 +95,20 @@ def has_view3d_space(screen): return False -def switch_layers_visibility(storage_list, show): - """Switches visibility of layers in Blender. If show is True all layers on current scene - and local layers of current 3D spaces are shown. If storage_list is provided it tries - to restore visibilites from entries. - - WARNING: this function can restore only storage_list which was created with this function - otherwise it may raise error. +def get_spaces_with_local_view(screen=None): + """Gets all spaces of current screen that currently have active local view. - :param storage_list: list where previous layers visibility states are or should be stored into - :type storage_list: list - :param show: flag for indicating if all layers should be shown (value True) or restored (value False) - :type show: bool - :return: states of layers visibilites for scene and local layers of views - :rtype: list of list(bool*20) + :param screen: blender screen; if None is given all screens are taken from window manager + :type screen: bpy.types.Screen + :return: list of local view spaces + :rtype: list[bpy.types.SpaceView3D] """ + spaces = [] + for space in get_all_spaces(screen): + if space.local_view: + spaces.append(space) - for space in get_all_spaces(): - if show: - storage_list.append(list(space.layers)) - space.layers = [True] * 20 - lprint("D Layers visibility set to True") - elif len(storage_list) > 0: - space.layers = storage_list[0] - storage_list = storage_list[1:] - - scene = bpy.context.scene - if show: - storage_list.append(list(scene.layers)) - scene.layers = [True] * 20 - elif len(storage_list) > 0: - scene.layers = storage_list[0] - storage_list = storage_list[1:] - - return storage_list + return spaces def tag_redraw_all_view3d():