From 6a6d383970a67aff4a683af62cdbc419bc253d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 30 Dec 2020 17:52:27 +0100 Subject: [PATCH 01/63] Update "Armory Props" object panel to Blender 2.9 layouts --- blender/arm/props_ui.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index d58b3012..8c1e3498 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -21,8 +21,9 @@ from arm.lightmapper.utility import icon from arm.lightmapper.properties.denoiser import oidn, optix import importlib -# Menu in object region + class ARM_PT_ObjectPropsPanel(bpy.types.Panel): + """Menu in object region.""" bl_label = "Armory Props" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" @@ -37,12 +38,13 @@ class ARM_PT_ObjectPropsPanel(bpy.types.Panel): if obj == None: return - layout.prop(obj, 'arm_export') + col = layout.column() + col.prop(obj, 'arm_export') if not obj.arm_export: return - layout.prop(obj, 'arm_spawn') - layout.prop(obj, 'arm_mobile') - layout.prop(obj, 'arm_animation_enabled') + col.prop(obj, 'arm_spawn') + col.prop(obj, 'arm_mobile') + col.prop(obj, 'arm_animation_enabled') if obj.type == 'MESH': layout.prop(obj, 'arm_instanced') From a647263d1a12ec8cb47d45a839829619a6d6597b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 30 Dec 2020 17:53:35 +0100 Subject: [PATCH 02/63] Update "Armory Proxy" object panel to Blender 2.9 layouts --- blender/arm/props_ui.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 8c1e3498..7f51d8de 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -2114,20 +2114,27 @@ class ARM_PT_ProxyPanel(bpy.types.Panel): layout.use_property_split = True layout.use_property_decorate = False layout.operator("arm.make_proxy") + obj = bpy.context.object - if obj != None and obj.proxy != None: - layout.label(text="Sync") - layout.prop(obj, "arm_proxy_sync_loc") - layout.prop(obj, "arm_proxy_sync_rot") - layout.prop(obj, "arm_proxy_sync_scale") - layout.prop(obj, "arm_proxy_sync_materials") - layout.prop(obj, "arm_proxy_sync_modifiers") - layout.prop(obj, "arm_proxy_sync_traits") - row = layout.row() + if obj is not None and obj.proxy is not None: + col = layout.column(heading="Sync") + col.prop(obj, "arm_proxy_sync_loc") + col.prop(obj, "arm_proxy_sync_rot") + col.prop(obj, "arm_proxy_sync_scale") + col.separator() + + col.prop(obj, "arm_proxy_sync_materials") + col.prop(obj, "arm_proxy_sync_modifiers") + col.separator() + + col.prop(obj, "arm_proxy_sync_traits") + row = col.row() row.enabled = obj.arm_proxy_sync_traits row.prop(obj, "arm_proxy_sync_trait_props") - layout.operator("arm.proxy_toggle_all") - layout.operator("arm.proxy_apply_all") + + row = layout.row(align=True) + row.operator("arm.proxy_toggle_all") + row.operator("arm.proxy_apply_all") class ArmMakeProxyButton(bpy.types.Operator): '''Create proxy from linked object''' From 257f295b275aa545f22f3fa536bd1c19796d10f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 30 Dec 2020 17:54:44 +0100 Subject: [PATCH 03/63] Update "Armory Lod" object panel to Blender 2.9 layouts --- blender/arm/props_lod.py | 8 +++----- blender/arm/props_ui.py | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/blender/arm/props_lod.py b/blender/arm/props_lod.py index ef1b2e86..b483007c 100755 --- a/blender/arm/props_lod.py +++ b/blender/arm/props_lod.py @@ -34,24 +34,22 @@ class ArmLodListItem(bpy.types.PropertyGroup): class ARM_UL_LodList(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - # We could write some code to decide which icon to use here... - custom_icon = 'OBJECT_DATAMODE' + layout.use_property_split = False - # Make sure your code supports all 3 layout types if self.layout_type in {'DEFAULT', 'COMPACT'}: layout.prop(item, "enabled_prop") name = item.name if name == '': name = 'None' row = layout.row() - row.label(text=name, icon=custom_icon) + row.label(text=name, icon='OBJECT_DATAMODE') col = row.column() col.alignment = 'RIGHT' col.label(text="{:.2f}".format(item.screen_size_prop)) elif self.layout_type in {'GRID'}: layout.alignment = 'CENTER' - layout.label(text="", icon = custom_icon) + layout.label(text="", icon='OBJECT_DATAMODE') class ArmLodListNewItem(bpy.types.Operator): # Add a new item to the list diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 7f51d8de..de871621 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -1850,7 +1850,7 @@ class ARM_PT_BakePanel(bpy.types.Panel): layout.label(text="Warning! Overflow not yet supported") class ArmGenLodButton(bpy.types.Operator): - '''Automatically generate LoD levels''' + """Automatically generate LoD levels.""" bl_idname = 'arm.generate_lod' bl_label = 'Auto Generate' From 52cee7f0ce7d0070f50d35563598f92872f9339e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 30 Dec 2020 17:56:34 +0100 Subject: [PATCH 04/63] Update "Armory Traits" object panel to Blender 2.9 layouts --- blender/arm/props_traits.py | 57 +++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index 377ed796..47fa91f8 100755 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -2,6 +2,7 @@ import json import os import shutil import subprocess +from typing import Union import webbrowser from bpy.types import NodeTree @@ -82,7 +83,10 @@ class ArmTraitListItem(bpy.types.PropertyGroup): arm_traitpropswarnings: CollectionProperty(type=ArmTraitPropWarning) class ARM_UL_TraitList(bpy.types.UIList): + """List of traits.""" def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + layout.use_property_split = False + custom_icon = "NONE" custom_icon_value = 0 if item.type_prop == "Haxe Script": @@ -96,7 +100,6 @@ class ARM_UL_TraitList(bpy.types.UIList): elif item.type_prop == "Logic Nodes": custom_icon = 'NODETREE' - # Make sure your code supports all 3 layout types if self.layout_type in {'DEFAULT', 'COMPACT'}: layout.prop(item, "enabled_prop") # Display " " for props without a name to right-align the @@ -635,7 +638,7 @@ class ARM_PT_TraitPanel(bpy.types.Panel): layout.use_property_split = True layout.use_property_decorate = False obj = bpy.context.object - draw_traits(layout, obj, is_object=True) + draw_traits_panel(layout, obj, is_object=True) class ARM_PT_SceneTraitPanel(bpy.types.Panel): bl_label = "Armory Scene Traits" @@ -648,7 +651,7 @@ class ARM_PT_SceneTraitPanel(bpy.types.Panel): layout.use_property_split = True layout.use_property_decorate = False obj = bpy.context.scene - draw_traits(layout, obj, is_object=False) + draw_traits_panel(layout, obj, is_object=False) class ARM_OT_CopyTraitsFromActive(bpy.types.Operator): bl_label = 'Copy Traits from Active Object' @@ -724,21 +727,24 @@ class ARM_OT_CopyTraitsFromActive(bpy.types.Operator): return {'INTERFACE'} -def draw_traits(layout, obj, is_object): - rows = 2 + +def draw_traits_panel(layout: bpy.types.UILayout, obj: Union[bpy.types.Object, bpy.types.Scene], + is_object: bool) -> None: + # Make the list bigger when there are a few traits + num_rows = 2 if len(obj.arm_traitlist) > 1: - rows = 4 + num_rows = 4 row = layout.row() - row.template_list("ARM_UL_TraitList", "The_List", obj, "arm_traitlist", obj, "arm_traitlist_index", rows=rows) + row.template_list("ARM_UL_TraitList", "The_List", obj, "arm_traitlist", obj, "arm_traitlist_index", rows=num_rows) col = row.column(align=True) op = col.operator("arm_traitlist.new_item", icon='ADD', text="") op.is_object = is_object if is_object: - op = col.operator("arm_traitlist.delete_item", icon='REMOVE', text="")#.all = False + op = col.operator("arm_traitlist.delete_item", icon='REMOVE', text="") else: - op = col.operator("arm_traitlist.delete_item_scene", icon='REMOVE', text="")#.all = False + op = col.operator("arm_traitlist.delete_item_scene", icon='REMOVE', text="") op.is_object = is_object if len(obj.arm_traitlist) > 1: @@ -750,35 +756,30 @@ def draw_traits(layout, obj, is_object): op.direction = 'DOWN' op.is_object = is_object + # Draw trait specific content if obj.arm_traitlist_index >= 0 and len(obj.arm_traitlist) > 0: item = obj.arm_traitlist[obj.arm_traitlist_index] if item.type_prop == 'Haxe Script' or item.type_prop == 'Bundled Script': + row = layout.row(align=True) + row.alignment = 'EXPAND' + if item.type_prop == 'Haxe Script': - row = layout.row(align=True) - row.alignment = 'EXPAND' column = row.column(align=True) column.alignment = 'EXPAND' if item.class_name_prop == '': column.enabled = False - op = column.operator("arm.edit_script", icon="FILE_SCRIPT") - op.is_object = is_object - op = row.operator("arm.new_script") - op.is_object = is_object - op = row.operator("arm.refresh_scripts", text="Refresh") - else: # Bundled + column.operator("arm.edit_script", icon="FILE_SCRIPT").is_object = is_object + row.operator("arm.new_script").is_object = is_object + row.operator("arm.refresh_scripts", text="Refresh") + + # Bundled scripts + else: if item.class_name_prop == 'NavMesh': - row = layout.row(align=True) - row.alignment = 'EXPAND' - op = layout.operator("arm.generate_navmesh") - row = layout.row(align=True) - row.alignment = 'EXPAND' - column = row.column(align=True) - column.alignment = 'EXPAND' - if not item.class_name_prop == 'NavMesh': - op = column.operator("arm.edit_bundled_script", icon="FILE_SCRIPT") - op.is_object = is_object - op = row.operator("arm.refresh_scripts", text="Refresh") + row.operator("arm.generate_navmesh") + else: + row.operator("arm.edit_bundled_script", icon="FILE_SCRIPT").is_object = is_object + row.operator("arm.refresh_scripts", text="Refresh") # Default props item.name = item.class_name_prop From 4862054bcfb1761a085f1430b2984ea44e5f3e33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 30 Dec 2020 17:56:58 +0100 Subject: [PATCH 05/63] Reorder trait type selection --- blender/arm/props_traits.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index 47fa91f8..d1c25c9a 100755 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -119,13 +119,14 @@ class ArmTraitListNewItem(bpy.types.Operator): is_object: BoolProperty(name="Object Trait", description="Whether this is an object or scene trait", default=False) type_prop: EnumProperty( - items = [('Haxe Script', 'Haxe', 'Haxe Script'), - ('WebAssembly', 'Wasm', 'WebAssembly'), - ('UI Canvas', 'UI', 'UI Canvas'), - ('Bundled Script', 'Bundled', 'Bundled Script'), - ('Logic Nodes', 'Nodes', 'Logic Nodes') - ], - name = "Type") + name="Type", + items=[ + ('Haxe Script', 'Haxe', 'Haxe Script'), + ('Logic Nodes', 'Nodes', 'Logic Nodes'), + ('UI Canvas', 'UI', 'UI Canvas'), + ('Bundled Script', 'Bundled', 'Bundled Script'), + ('WebAssembly', 'Wasm', 'WebAssembly') + ]) def invoke(self, context, event): wm = context.window_manager From 14bca0ec80563898db665ea1751bf7b7fa0369ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 30 Dec 2020 17:57:23 +0100 Subject: [PATCH 06/63] Shorten too long node trait operator labels --- blender/arm/props_traits.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index d1c25c9a..b0836246 100755 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -834,7 +834,7 @@ def draw_traits_panel(layout: bpy.types.UILayout, obj: Union[bpy.types.Object, b # New column = row.column(align=True) column.alignment = 'EXPAND' - op = column.operator("arm.new_treenode", text="New Node Tree", icon="ADD") + op = column.operator("arm.new_treenode", text="New Tree", icon="ADD") op.is_object = is_object # At least one check is active Logic Node Editor is_check_logic_node_editor = False @@ -857,7 +857,7 @@ def draw_traits_panel(layout: bpy.types.UILayout, obj: Union[bpy.types.Object, b column.enabled = False else: column.enabled = is_check_logic_node_editor - op = column.operator("arm.edit_treenode", text="Edit Node Tree", icon="NODETREE") + op = column.operator("arm.edit_treenode", text="Edit Tree", icon="NODETREE") op.is_object = is_object # Get from Node Tree Editor column = row.column(align=True) @@ -866,7 +866,7 @@ def draw_traits_panel(layout: bpy.types.UILayout, obj: Union[bpy.types.Object, b column.enabled = False else: column.enabled = is_check_logic_node_editor - op = column.operator("arm.get_treenode", text="From Node Editor", icon="IMPORT") + op = column.operator("arm.get_treenode", text="From Editor", icon="IMPORT") op.is_object = is_object # Row for search From cab20d0a64bdacfae7f4d97bd8a091c5b4287ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 4 Jan 2021 19:34:42 +0100 Subject: [PATCH 07/63] Principled BSDF: update input socket indices to Blender 2.9 --- blender/arm/material/cycles_nodes/nodes_shader.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/blender/arm/material/cycles_nodes/nodes_shader.py b/blender/arm/material/cycles_nodes/nodes_shader.py index d6114454..4e593bd0 100644 --- a/blender/arm/material/cycles_nodes/nodes_shader.py +++ b/blender/arm/material/cycles_nodes/nodes_shader.py @@ -41,7 +41,7 @@ def parse_addshader(node: bpy.types.ShaderNodeAddShader, out_socket: NodeSocket, def parse_bsdfprincipled(node: bpy.types.ShaderNodeBsdfPrincipled, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: - c.write_normal(node.inputs[19]) + c.write_normal(node.inputs[20]) state.out_basecol = c.parse_vector_input(node.inputs[0]) # subsurface = c.parse_vector_input(node.inputs[1]) # subsurface_radius = c.parse_vector_input(node.inputs[2]) @@ -62,11 +62,12 @@ def parse_bsdfprincipled(node: bpy.types.ShaderNodeBsdfPrincipled, out_socket: N if node.inputs[17].is_linked or node.inputs[17].default_value[0] != 0.0: state.out_emission = '({0}.x)'.format(c.parse_vector_input(node.inputs[17])) state.emission_found = True - # clearcoar_normal = c.parse_vector_input(node.inputs[20]) - # tangent = c.parse_vector_input(node.inputs[21]) + # clearcoar_normal = c.parse_vector_input(node.inputs[21]) + # tangent = c.parse_vector_input(node.inputs[22]) if state.parse_opacity: - if len(node.inputs) > 20: - state.out_opacity = c.parse_value_input(node.inputs[18]) + if len(node.inputs) > 21: + state.out_opacity = c.parse_value_input(node.inputs[19]) + def parse_bsdfdiffuse(node: bpy.types.ShaderNodeBsdfDiffuse, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: From c5e95224425e00b0277b4875a6778230aaa7c41e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 6 Jan 2021 17:23:21 +0100 Subject: [PATCH 08/63] Blender 2.9: Update operator options --- blender/arm/logicnode/arm_nodes.py | 8 ++++++++ blender/arm/logicnode/input/LN_on_swipe.py | 2 ++ blender/arm/nodes_logic.py | 7 ++++--- blender/arm/props_exporter.py | 2 +- blender/arm/props_ui.py | 6 +++--- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/blender/arm/logicnode/arm_nodes.py b/blender/arm/logicnode/arm_nodes.py index 64c23121..d7920b40 100644 --- a/blender/arm/logicnode/arm_nodes.py +++ b/blender/arm/logicnode/arm_nodes.py @@ -112,6 +112,7 @@ class ArmNodeAddInputButton(bpy.types.Operator): """Add a new input socket to the node set by node_index.""" bl_idname = 'arm.node_add_input' bl_label = 'Add Input' + bl_options = {'UNDO', 'INTERNAL'} node_index: StringProperty(name='Node Index', default='') socket_type: StringProperty(name='Socket Type', default='NodeSocketShader') @@ -135,6 +136,7 @@ class ArmNodeAddInputValueButton(bpy.types.Operator): """Add new input""" bl_idname = 'arm.node_add_input_value' bl_label = 'Add Input' + bl_options = {'UNDO', 'INTERNAL'} node_index: StringProperty(name='Node Index', default='') socket_type: StringProperty(name='Socket Type', default='NodeSocketShader') @@ -148,6 +150,7 @@ class ArmNodeRemoveInputButton(bpy.types.Operator): """Remove last input""" bl_idname = 'arm.node_remove_input' bl_label = 'Remove Input' + bl_options = {'UNDO', 'INTERNAL'} node_index: StringProperty(name='Node Index', default='') def execute(self, context): @@ -163,6 +166,7 @@ class ArmNodeRemoveInputValueButton(bpy.types.Operator): """Remove last input""" bl_idname = 'arm.node_remove_input_value' bl_label = 'Remove Input' + bl_options = {'UNDO', 'INTERNAL'} node_index: StringProperty(name='Node Index', default='') def execute(self, context): @@ -178,6 +182,7 @@ class ArmNodeAddOutputButton(bpy.types.Operator): """Add a new output socket to the node set by node_index""" bl_idname = 'arm.node_add_output' bl_label = 'Add Output' + bl_options = {'UNDO', 'INTERNAL'} node_index: StringProperty(name='Node Index', default='') socket_type: StringProperty(name='Socket Type', default='NodeSocketShader') @@ -201,6 +206,7 @@ class ArmNodeRemoveOutputButton(bpy.types.Operator): """Remove last output""" bl_idname = 'arm.node_remove_output' bl_label = 'Remove Output' + bl_options = {'UNDO', 'INTERNAL'} node_index: StringProperty(name='Node Index', default='') def execute(self, context): @@ -216,6 +222,7 @@ class ArmNodeAddInputOutputButton(bpy.types.Operator): """Add new input and output""" bl_idname = 'arm.node_add_input_output' bl_label = 'Add Input Output' + bl_options = {'UNDO', 'INTERNAL'} node_index: StringProperty(name='Node Index', default='') in_socket_type: StringProperty(name='In Socket Type', default='NodeSocketShader') @@ -246,6 +253,7 @@ class ArmNodeRemoveInputOutputButton(bpy.types.Operator): """Remove last input and output""" bl_idname = 'arm.node_remove_input_output' bl_label = 'Remove Input Output' + bl_options = {'UNDO', 'INTERNAL'} node_index: StringProperty(name='Node Index', default='') def execute(self, context): diff --git a/blender/arm/logicnode/input/LN_on_swipe.py b/blender/arm/logicnode/input/LN_on_swipe.py index cfd9b17c..ed104a41 100644 --- a/blender/arm/logicnode/input/LN_on_swipe.py +++ b/blender/arm/logicnode/input/LN_on_swipe.py @@ -5,6 +5,7 @@ class NodeAddOutputButton(bpy.types.Operator): """Add 4 States""" bl_idname = 'arm.add_output_4_parameters' bl_label = 'Add 4 States' + bl_options = {'UNDO', 'INTERNAL'} node_index: StringProperty(name='Node Index', default='') socket_type: StringProperty(name='Socket Type', default='NodeSocketShader') name_format: StringProperty(name='Name Format', default='Output {0}') @@ -31,6 +32,7 @@ class NodeRemoveOutputButton(bpy.types.Operator): """Remove 4 last states""" bl_idname = 'arm.remove_output_4_parameters' bl_label = 'Remove 4 States' + bl_options = {'UNDO', 'INTERNAL'} node_index: StringProperty(name='Node Index', default='') def execute(self, context): diff --git a/blender/arm/nodes_logic.py b/blender/arm/nodes_logic.py index 3a44971f..4a61c1f7 100755 --- a/blender/arm/nodes_logic.py +++ b/blender/arm/nodes_logic.py @@ -58,13 +58,14 @@ class ARM_OT_AddNodeOverride(bpy.types.Operator): bl_idname = "arm.add_node_override" bl_label = "Add Node" bl_property = "type" + bl_options = {'INTERNAL'} type: StringProperty(name="NodeItem type") use_transform: BoolProperty(name="Use Transform") def invoke(self, context, event): bpy.ops.node.add_node('INVOKE_DEFAULT', type=self.type, use_transform=self.use_transform) - return {"FINISHED"} + return {'FINISHED'} @classmethod def description(cls, context, properties): @@ -261,7 +262,7 @@ class ARM_PT_Variables(bpy.types.Panel): setN.ntype = ID class ARMAddVarNode(bpy.types.Operator): - '''Add a linked node of that Variable''' + """Add a linked node of that Variable""" bl_idname = 'arm.add_var_node' bl_label = 'Add Get' bl_options = {'GRAB_CURSOR', 'BLOCKING'} @@ -296,7 +297,7 @@ class ARMAddVarNode(bpy.types.Operator): return({'FINISHED'}) class ARMAddSetVarNode(bpy.types.Operator): - '''Add a node to set this Variable''' + """Add a node to set this Variable""" bl_idname = 'arm.add_setvar_node' bl_label = 'Add Set' bl_options = {'GRAB_CURSOR', 'BLOCKING'} diff --git a/blender/arm/props_exporter.py b/blender/arm/props_exporter.py index 2b19a543..0dc2f143 100644 --- a/blender/arm/props_exporter.py +++ b/blender/arm/props_exporter.py @@ -358,7 +358,7 @@ class ArmExporterSpecialsMenu(bpy.types.Menu): layout.operator("arm.exporter_gpuprofile") class ArmoryExporterOpenFolderButton(bpy.types.Operator): - '''Open published folder''' + """Open published folder""" bl_idname = 'arm.exporter_open_folder' bl_label = 'Open Folder' diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index de871621..82b69bc4 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -270,7 +270,7 @@ class ARM_PT_ScenePropsPanel(bpy.types.Panel): row.prop(scene, 'arm_export') class InvalidateCacheButton(bpy.types.Operator): - '''Delete cached mesh data''' + """Delete cached mesh data""" bl_idname = "arm.invalidate_cache" bl_label = "Invalidate Cache" @@ -279,7 +279,7 @@ class InvalidateCacheButton(bpy.types.Operator): return{'FINISHED'} class InvalidateMaterialCacheButton(bpy.types.Operator): - '''Delete cached material data''' + """Delete cached material data""" bl_idname = "arm.invalidate_material_cache" bl_label = "Invalidate Cache" @@ -2246,7 +2246,7 @@ class ARM_OT_ShowFileVersionInfo(bpy.types.Operator): bl_idname = 'arm.show_old_file_version_info' bl_description = ('Displays an info panel that warns about opening a file' 'which was created in a previous version of Armory') - # bl_options = {'INTERNAL'} + bl_options = {'INTERNAL'} wrd = None From 80ea09671c4aca0e27ddf0edcd3c9a886c74af64 Mon Sep 17 00:00:00 2001 From: QuantumCoderQC Date: Wed, 6 Jan 2021 20:26:43 +0100 Subject: [PATCH 09/63] Fix Exporter and conversions for Physics World in Blender 2.9.X --- .../armory/trait/physics/bullet/PhysicsWorld.hx | 14 +++++++++----- blender/arm/exporter.py | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Sources/armory/trait/physics/bullet/PhysicsWorld.hx b/Sources/armory/trait/physics/bullet/PhysicsWorld.hx index 22cf1cc5..581f7dda 100644 --- a/Sources/armory/trait/physics/bullet/PhysicsWorld.hx +++ b/Sources/armory/trait/physics/bullet/PhysicsWorld.hx @@ -53,7 +53,6 @@ class PhysicsWorld extends Trait { public var rbMap: Map; public var conMap: Map; public var timeScale = 1.0; - var timeStep = 1 / 60; var maxSteps = 1; public var solverIterations = 10; public var hitPointWorld = new Vec4(); @@ -68,7 +67,7 @@ class PhysicsWorld extends Trait { public static var physTime = 0.0; #end - public function new(timeScale = 1.0, timeStep = 1 / 60, solverIterations = 10) { + public function new(timeScale = 1.0, maxSteps = 10, solverIterations = 10) { super(); if (nullvec) { @@ -82,8 +81,7 @@ class PhysicsWorld extends Trait { sceneRemoved = false; this.timeScale = timeScale; - this.timeStep = timeStep; - maxSteps = timeStep < 1 / 60 ? 10 : 1; + this.maxSteps = maxSteps; this.solverIterations = solverIterations; // First scene @@ -270,7 +268,13 @@ class PhysicsWorld extends Trait { if (preUpdates != null) for (f in preUpdates) f(); - world.stepSimulation(timeStep, maxSteps, t); + //Bullet physics fixed timescale + var fixedTime = 1.0 / 60; + + //This condition must be satisfied to not loose time + var currMaxSteps = t < (fixedTime * maxSteps) ? maxSteps : 1; + + world.stepSimulation(t, currMaxSteps, fixedTime); updateContacts(); for (rb in rbMap) @:privateAccess rb.physicsUpdate(); diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index 5ee3a07f..f58039cf 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -2596,7 +2596,7 @@ class ArmoryExporter: rbw = self.scene.rigidbody_world if rbw is not None and rbw.enabled: - out_trait['parameters'] = [str(rbw.time_scale), str(1 / rbw.steps_per_second), str(rbw.solver_iterations)] + out_trait['parameters'] = [str(rbw.time_scale), str(rbw.substeps_per_frame), str(rbw.solver_iterations)] self.output['traits'].append(out_trait) From c69774402c1ed675dc9de48d3c3e61c2f4757186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 11 Jan 2021 19:51:15 +0100 Subject: [PATCH 10/63] Blender 2.9: Fix tilesheet operator exceptions when using the search menu --- blender/arm/props_tilesheet.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/blender/arm/props_tilesheet.py b/blender/arm/props_tilesheet.py index eeea1b4f..c6c58d42 100644 --- a/blender/arm/props_tilesheet.py +++ b/blender/arm/props_tilesheet.py @@ -60,6 +60,8 @@ class ArmTilesheetActionListDeleteItem(bpy.types.Operator): def poll(self, context): """ Enable if there's something in the list """ wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_tilesheetlist) == 0: + return False trait = wrd.arm_tilesheetlist[wrd.arm_tilesheetlist_index] return len(trait.arm_tilesheetactionlist) > 0 @@ -81,6 +83,8 @@ class ArmTilesheetActionListMoveItem(bpy.types.Operator): # Move an item in the list bl_idname = "arm_tilesheetactionlist.move_item" bl_label = "Move an item in the list" + bl_options = {'INTERNAL'} + direction: EnumProperty( items=( ('UP', 'Up', ""), @@ -90,6 +94,8 @@ class ArmTilesheetActionListMoveItem(bpy.types.Operator): def poll(self, context): """ Enable if there's something in the list. """ wrd = bpy.data.worlds['Arm'] + if len(wrd.arm_tilesheetlist) == 0: + return False trait = wrd.arm_tilesheetlist[wrd.arm_tilesheetlist_index] return len(trait.arm_tilesheetactionlist) > 0 @@ -203,6 +209,8 @@ class ArmTilesheetListMoveItem(bpy.types.Operator): # Move an item in the list bl_idname = "arm_tilesheetlist.move_item" bl_label = "Move an item in the list" + bl_options = {'INTERNAL'} + direction: EnumProperty( items=( ('UP', 'Up', ""), From 4f25af45ba6d74ee15142dd9867ca9c784953f68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 11 Jan 2021 19:56:40 +0100 Subject: [PATCH 11/63] Cleanup --- blender/arm/props_tilesheet.py | 87 ++++++++++++++++------------------ 1 file changed, 42 insertions(+), 45 deletions(-) diff --git a/blender/arm/props_tilesheet.py b/blender/arm/props_tilesheet.py index c6c58d42..1f7d53b6 100644 --- a/blender/arm/props_tilesheet.py +++ b/blender/arm/props_tilesheet.py @@ -1,30 +1,26 @@ -import shutil import bpy -import os -import json -from bpy.types import Menu, Panel, UIList from bpy.props import * class ArmTilesheetActionListItem(bpy.types.PropertyGroup): name: StringProperty( - name="Name", - description="A name for this item", - default="Untitled") + name="Name", + description="A name for this item", + default="Untitled") start_prop: IntProperty( - name="Start", - description="A name for this item", - default=0) + name="Start", + description="A name for this item", + default=0) end_prop: IntProperty( - name="End", - description="A name for this item", - default=0) + name="End", + description="A name for this item", + default=0) loop_prop: BoolProperty( - name="Loop", - description="A name for this item", - default=True) + name="Loop", + description="A name for this item", + default=True) class ARM_UL_TilesheetActionList(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): @@ -52,13 +48,13 @@ class ArmTilesheetActionListNewItem(bpy.types.Operator): return{'FINISHED'} class ArmTilesheetActionListDeleteItem(bpy.types.Operator): - # Delete the selected item from the list + """Delete the selected item from the list""" bl_idname = "arm_tilesheetactionlist.delete_item" bl_label = "Deletes an item" @classmethod def poll(self, context): - """ Enable if there's something in the list """ + """Enable if there's something in the list""" wrd = bpy.data.worlds['Arm'] if len(wrd.arm_tilesheetlist) == 0: return False @@ -80,19 +76,20 @@ class ArmTilesheetActionListDeleteItem(bpy.types.Operator): return{'FINISHED'} class ArmTilesheetActionListMoveItem(bpy.types.Operator): - # Move an item in the list + """Move an item in the list""" bl_idname = "arm_tilesheetactionlist.move_item" bl_label = "Move an item in the list" bl_options = {'INTERNAL'} direction: EnumProperty( - items=( - ('UP', 'Up', ""), - ('DOWN', 'Down', ""),)) + items=( + ('UP', 'Up', ""), + ('DOWN', 'Down', "") + )) @classmethod def poll(self, context): - """ Enable if there's something in the list. """ + """Enable if there's something in the list""" wrd = bpy.data.worlds['Arm'] if len(wrd.arm_tilesheetlist) == 0: return False @@ -135,27 +132,27 @@ class ArmTilesheetActionListMoveItem(bpy.types.Operator): class ArmTilesheetListItem(bpy.types.PropertyGroup): name: StringProperty( - name="Name", - description="A name for this item", - default="Untitled") + name="Name", + description="A name for this item", + default="Untitled") tilesx_prop: IntProperty( - name="Tiles X", - description="A name for this item", - default=0) + name="Tiles X", + description="A name for this item", + default=0) tilesy_prop: IntProperty( - name="Tiles Y", - description="A name for this item", - default=0) + name="Tiles Y", + description="A name for this item", + default=0) framerate_prop: FloatProperty( - name="Frame Rate", - description="A name for this item", - default=4.0) + name="Frame Rate", + description="A name for this item", + default=4.0) arm_tilesheetactionlist: CollectionProperty(type=ArmTilesheetActionListItem) - arm_tilesheetactionlist_index: IntProperty(name="Index for my_list", default=0) + arm_tilesheetactionlist_index: IntProperty(name="Index for arm_tilesheetactionlist", default=0) class ARM_UL_TilesheetList(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): @@ -168,10 +165,10 @@ class ARM_UL_TilesheetList(bpy.types.UIList): elif self.layout_type in {'GRID'}: layout.alignment = 'CENTER' - layout.label(text="", icon = custom_icon) + layout.label(text="", icon=custom_icon) class ArmTilesheetListNewItem(bpy.types.Operator): - # Add a new item to the list + """Add a new item to the list""" bl_idname = "arm_tilesheetlist.new_item" bl_label = "Add a new item" @@ -182,7 +179,7 @@ class ArmTilesheetListNewItem(bpy.types.Operator): return{'FINISHED'} class ArmTilesheetListDeleteItem(bpy.types.Operator): - # Delete the selected item from the list + """Delete the selected item from the list""" bl_idname = "arm_tilesheetlist.delete_item" bl_label = "Deletes an item" @@ -206,15 +203,16 @@ class ArmTilesheetListDeleteItem(bpy.types.Operator): return{'FINISHED'} class ArmTilesheetListMoveItem(bpy.types.Operator): - # Move an item in the list + """Move an item in the list""" bl_idname = "arm_tilesheetlist.move_item" bl_label = "Move an item in the list" bl_options = {'INTERNAL'} direction: EnumProperty( - items=( - ('UP', 'Up', ""), - ('DOWN', 'Down', ""),)) + items=( + ('UP', 'Up', ""), + ('DOWN', 'Down', "") + )) @classmethod def poll(self, context): @@ -255,7 +253,6 @@ class ArmTilesheetListMoveItem(bpy.types.Operator): return{'FINISHED'} def register(): - bpy.utils.register_class(ArmTilesheetActionListItem) bpy.utils.register_class(ARM_UL_TilesheetActionList) bpy.utils.register_class(ArmTilesheetActionListNewItem) @@ -269,7 +266,7 @@ def register(): bpy.utils.register_class(ArmTilesheetListMoveItem) bpy.types.World.arm_tilesheetlist = CollectionProperty(type=ArmTilesheetListItem) - bpy.types.World.arm_tilesheetlist_index = IntProperty(name="Index for my_list", default=0) + bpy.types.World.arm_tilesheetlist_index = IntProperty(name="Index for arm_tilesheetlist", default=0) def unregister(): bpy.utils.unregister_class(ArmTilesheetListItem) From 492009883105150913850f0d28f4221e458d0be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 11 Jan 2021 19:57:29 +0100 Subject: [PATCH 12/63] Blender 2.9: Fix another exception when using the search menu --- blender/arm/logicnode/arm_nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/logicnode/arm_nodes.py b/blender/arm/logicnode/arm_nodes.py index d7920b40..e99b8bd2 100644 --- a/blender/arm/logicnode/arm_nodes.py +++ b/blender/arm/logicnode/arm_nodes.py @@ -273,7 +273,7 @@ class ArmNodeRemoveInputOutputButton(bpy.types.Operator): class ArmNodeSearch(bpy.types.Operator): bl_idname = "arm.node_search" bl_label = "Search..." - bl_options = {"REGISTER"} + bl_options = {"REGISTER", "INTERNAL"} bl_property = "item" def get_search_items(self, context): From 1b9f010c575f735b75ec061dff2db1e9b9efa24d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 14 Jan 2021 20:56:04 +0100 Subject: [PATCH 13/63] Blender 2.9: Improve project window panel UI --- blender/arm/props_ui.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 82b69bc4..1e89521e 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -775,11 +775,15 @@ class ARM_PT_ProjectWindowPanel(bpy.types.Panel): layout.use_property_decorate = False wrd = bpy.data.worlds['Arm'] layout.prop(wrd, 'arm_winmode') - layout.prop(wrd, 'arm_winresize') - col = layout.column() - col.enabled = wrd.arm_winresize - col.prop(wrd, 'arm_winmaximize') - layout.prop(wrd, 'arm_winminimize') + + col = layout.column(align=True) + col.prop(wrd, 'arm_winresize') + sub = col.column() + sub.enabled = wrd.arm_winresize + sub.prop(wrd, 'arm_winmaximize') + col.enabled = True + col.prop(wrd, 'arm_winminimize') + layout.prop(wrd, 'arm_vsync') class ARM_PT_ProjectModulesPanel(bpy.types.Panel): From a98559ea7fbf5c243dfb4fd325e79b2be2cae94f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 14 Jan 2021 20:57:44 +0100 Subject: [PATCH 14/63] Blender 2.9: Improve material blending panel UI --- blender/arm/props.py | 6 +++--- blender/arm/props_ui.py | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/blender/arm/props.py b/blender/arm/props.py index ae6aa0a9..ab91f129 100755 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -391,7 +391,7 @@ def init_properties(): ('destination_color', 'Destination Color', 'Destination Color'), ('inverse_source_color', 'Inverse Source Color', 'Inverse Source Color'), ('inverse_destination_color', 'Inverse Destination Color', 'Inverse Destination Color')], - name='Source', default='blend_one', description='Blending factor', update=assets.invalidate_shader_cache) + name='Source (Alpha)', default='blend_one', description='Blending factor', update=assets.invalidate_shader_cache) bpy.types.Material.arm_blending_destination_alpha = EnumProperty( items=[('blend_one', 'One', 'One'), ('blend_zero', 'Zero', 'Zero'), @@ -403,14 +403,14 @@ def init_properties(): ('destination_color', 'Destination Color', 'Destination Color'), ('inverse_source_color', 'Inverse Source Color', 'Inverse Source Color'), ('inverse_destination_color', 'Inverse Destination Color', 'Inverse Destination Color')], - name='Destination', default='blend_one', description='Blending factor', update=assets.invalidate_shader_cache) + name='Destination (Alpha)', default='blend_one', description='Blending factor', update=assets.invalidate_shader_cache) bpy.types.Material.arm_blending_operation_alpha = EnumProperty( items=[('add', 'Add', 'Add'), ('subtract', 'Subtract', 'Subtract'), ('reverse_subtract', 'Reverse Subtract', 'Reverse Subtract'), ('min', 'Min', 'Min'), ('max', 'Max', 'Max')], - name='Operation', default='add', description='Blending operation', update=assets.invalidate_shader_cache) + name='Operation (Alpha)', default='add', description='Blending operation', update=assets.invalidate_shader_cache) # For scene bpy.types.Scene.arm_export = BoolProperty(name="Export", description="Export scene data", default=True) bpy.types.Scene.arm_terrain_textures = StringProperty(name="Textures", description="Set root folder for terrain assets", default="//Bundled/", subtype="DIR_PATH") diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 1e89521e..d0db4f08 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -369,7 +369,7 @@ class ARM_PT_MaterialBlendingPropsPanel(bpy.types.Panel): bl_parent_id = "ARM_PT_MaterialPropsPanel" def draw_header(self, context): - if context.material == None: + if context.material is None: return self.layout.prop(context.material, 'arm_blending', text="") @@ -378,16 +378,18 @@ class ARM_PT_MaterialBlendingPropsPanel(bpy.types.Panel): layout.use_property_split = True layout.use_property_decorate = False mat = bpy.context.material - if mat == None: + if mat is None: return flow = layout.grid_flow() flow.enabled = mat.arm_blending - col = flow.column() + col = flow.column(align=True) col.prop(mat, 'arm_blending_source') col.prop(mat, 'arm_blending_destination') col.prop(mat, 'arm_blending_operation') - col = flow.column() + flow.separator() + + col = flow.column(align=True) col.prop(mat, 'arm_blending_source_alpha') col.prop(mat, 'arm_blending_destination_alpha') col.prop(mat, 'arm_blending_operation_alpha') From 4c0f0f416115c40492dc5ead0e36d63a5ccb5526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 14 Jan 2021 21:06:37 +0100 Subject: [PATCH 15/63] Blender 2.9: slightly improve renderpath "Renderer" panel UI --- blender/arm/props_ui.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index d0db4f08..763641fa 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -1106,17 +1106,23 @@ class ARM_PT_RenderPathRendererPanel(bpy.types.Panel): layout.prop(rpdat, 'arm_tess_shadows_outer') layout.prop(rpdat, 'arm_particles') - layout.prop(rpdat, 'arm_skin') - row = layout.row() - row.enabled = rpdat.arm_skin == 'On' - row.prop(rpdat, 'arm_skin_max_bones_auto') - row = layout.row() + layout.separator(factor=0.1) + + col = layout.column() + col.prop(rpdat, 'arm_skin') + col = col.column() + col.enabled = rpdat.arm_skin == 'On' + col.prop(rpdat, 'arm_skin_max_bones_auto') + row = col.row() row.enabled = not rpdat.arm_skin_max_bones_auto row.prop(rpdat, 'arm_skin_max_bones') - layout.prop(rpdat, "rp_hdr") - layout.prop(rpdat, "rp_stereo") - layout.prop(rpdat, 'arm_culling') - layout.prop(rpdat, 'rp_pp') + layout.separator(factor=0.1) + + col = layout.column() + col.prop(rpdat, "rp_hdr") + col.prop(rpdat, "rp_stereo") + col.prop(rpdat, 'arm_culling') + col.prop(rpdat, 'rp_pp') class ARM_PT_RenderPathShadowsPanel(bpy.types.Panel): bl_label = "Shadows" From 406d48eb7c04778219f96a1dd5b739f2b9351a29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 14 Jan 2021 21:13:02 +0100 Subject: [PATCH 16/63] Blender 2.9: Improve renderpath world panel UI --- blender/arm/props_ui.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 763641fa..fe71b35b 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -1218,16 +1218,22 @@ class ARM_PT_RenderPathWorldPanel(bpy.types.Panel): rpdat = wrd.arm_rplist[wrd.arm_rplist_index] layout.prop(rpdat, "rp_background") - layout.prop(rpdat, 'arm_irradiance') + col = layout.column() - col.enabled = rpdat.arm_irradiance - col.prop(rpdat, 'arm_radiance') + col.prop(rpdat, 'arm_irradiance') colb = col.column() - colb.enabled = rpdat.arm_radiance - colb.prop(rpdat, 'arm_radiance_size') + colb.enabled = rpdat.arm_irradiance + colb.prop(rpdat, 'arm_radiance') + sub = colb.row() + sub.enabled = rpdat.arm_radiance + sub.prop(rpdat, 'arm_radiance_size') + layout.separator() + layout.prop(rpdat, 'arm_clouds') - layout.prop(rpdat, "rp_water") + col = layout.column(align=True) + col.prop(rpdat, "rp_water") + col = col.column(align=True) col.enabled = rpdat.rp_water col.prop(rpdat, 'arm_water_level') col.prop(rpdat, 'arm_water_density') From 133777f7e57bef1f718258c228ecb6bcdad6d7dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 14 Jan 2021 21:18:46 +0100 Subject: [PATCH 17/63] Blender 2.9: Improve renderpath postprocess panel UI --- blender/arm/props_ui.py | 61 ++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index fe71b35b..6e791618 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -1269,27 +1269,35 @@ class ARM_PT_RenderPathPostProcessPanel(bpy.types.Panel): rpdat = wrd.arm_rplist[wrd.arm_rplist_index] layout.enabled = rpdat.rp_render_to_texture - row = layout.row() - row.prop(rpdat, "rp_antialiasing") - layout.prop(rpdat, "rp_supersampling") - layout.prop(rpdat, 'arm_rp_resolution') + col = layout.column() + col.prop(rpdat, "rp_antialiasing") + col.prop(rpdat, "rp_supersampling") + + col = layout.column() + col.prop(rpdat, 'arm_rp_resolution') if rpdat.arm_rp_resolution == 'Custom': - layout.prop(rpdat, 'arm_rp_resolution_size') - layout.prop(rpdat, 'arm_rp_resolution_filter') - layout.prop(rpdat, 'rp_dynres') + col.prop(rpdat, 'arm_rp_resolution_size') + col.prop(rpdat, 'arm_rp_resolution_filter') + col.prop(rpdat, 'rp_dynres') layout.separator() - row = layout.row() - row.prop(rpdat, "rp_ssgi") + col = layout.column() - col.enabled = rpdat.rp_ssgi != 'Off' - col.prop(rpdat, 'arm_ssgi_half_res') - col.prop(rpdat, 'arm_ssgi_rays') - col.prop(rpdat, 'arm_ssgi_radius') - col.prop(rpdat, 'arm_ssgi_strength') - col.prop(rpdat, 'arm_ssgi_max_steps') + col.prop(rpdat, "rp_ssgi") + sub = col.column() + sub.enabled = rpdat.rp_ssgi != 'Off' + sub.prop(rpdat, 'arm_ssgi_half_res') + sub.prop(rpdat, 'arm_ssgi_rays') + sub.prop(rpdat, 'arm_ssgi_radius') + sub.prop(rpdat, 'arm_ssgi_strength') + sub.prop(rpdat, 'arm_ssgi_max_steps') + layout.separator(factor=0.5) + + layout.prop(rpdat, 'arm_micro_shadowing') layout.separator() - layout.prop(rpdat, "rp_ssr") + col = layout.column() + col.prop(rpdat, "rp_ssr") + col = col.column() col.enabled = rpdat.rp_ssr col.prop(rpdat, 'arm_ssr_half_res') col.prop(rpdat, 'arm_ssr_ray_step') @@ -1298,33 +1306,42 @@ class ARM_PT_RenderPathPostProcessPanel(bpy.types.Panel): col.prop(rpdat, 'arm_ssr_falloff_exp') col.prop(rpdat, 'arm_ssr_jitter') layout.separator() - layout.prop(rpdat, 'arm_ssrs') + col = layout.column() + col.prop(rpdat, 'arm_ssrs') + col = col.column() col.enabled = rpdat.arm_ssrs col.prop(rpdat, 'arm_ssrs_ray_step') - layout.prop(rpdat, 'arm_micro_shadowing') layout.separator() - layout.prop(rpdat, "rp_bloom") + col = layout.column() + col.prop(rpdat, "rp_bloom") + col = col.column() col.enabled = rpdat.rp_bloom col.prop(rpdat, 'arm_bloom_threshold') col.prop(rpdat, 'arm_bloom_strength') col.prop(rpdat, 'arm_bloom_radius') layout.separator() - layout.prop(rpdat, "rp_motionblur") + col = layout.column() + col.prop(rpdat, "rp_motionblur") + col = col.column() col.enabled = rpdat.rp_motionblur != 'Off' col.prop(rpdat, 'arm_motion_blur_intensity') layout.separator() - layout.prop(rpdat, "rp_volumetriclight") + col = layout.column() + col.prop(rpdat, "rp_volumetriclight") + col = col.column() col.enabled = rpdat.rp_volumetriclight col.prop(rpdat, 'arm_volumetric_light_air_color') col.prop(rpdat, 'arm_volumetric_light_air_turbidity') col.prop(rpdat, 'arm_volumetric_light_steps') layout.separator() - layout.prop(rpdat, "rp_chromatic_aberration") + col = layout.column() + col.prop(rpdat, "rp_chromatic_aberration") + col = col.column() col.enabled = rpdat.rp_chromatic_aberration col.prop(rpdat, 'arm_chromatic_aberration_type') col.prop(rpdat, 'arm_chromatic_aberration_strength') From b4f0df6367985d0d479be40492ae9628a10e2b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 14 Jan 2021 21:21:01 +0100 Subject: [PATCH 18/63] Blender 2.9: Redesign renderpath compositor panel UI --- blender/arm/props_ui.py | 74 +++++++++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 6e791618..4de624e3 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -1374,45 +1374,49 @@ class ARM_PT_RenderPathCompositorPanel(bpy.types.Panel): layout.enabled = rpdat.rp_compositornodes layout.prop(rpdat, 'arm_tonemap') - layout.prop(rpdat, 'arm_letterbox') + col = layout.column() - col.enabled = rpdat.arm_letterbox - col.prop(rpdat, 'arm_letterbox_size') - layout.prop(rpdat, 'arm_sharpen') + draw_conditional_prop(col, 'Letterbox', rpdat, 'arm_letterbox', 'arm_letterbox_size') + draw_conditional_prop(col, 'Sharpen', rpdat, 'arm_sharpen', 'arm_sharpen_strength') + draw_conditional_prop(col, 'Vignette', rpdat, 'arm_vignette', 'arm_vignette_strength') + draw_conditional_prop(col, 'Film Grain', rpdat, 'arm_grain', 'arm_grain_strength') + layout.separator() + col = layout.column() - col.enabled = rpdat.arm_sharpen - col.prop(rpdat, 'arm_sharpen_strength') - layout.prop(rpdat, 'arm_fisheye') - layout.prop(rpdat, 'arm_vignette') - col = layout.column() - col.enabled = rpdat.arm_vignette - col.prop(rpdat, 'arm_vignette_strength') - layout.prop(rpdat, 'arm_lensflare') - layout.prop(rpdat, 'arm_grain') - col = layout.column() - col.enabled = rpdat.arm_grain - col.prop(rpdat, 'arm_grain_strength') - layout.prop(rpdat, 'arm_fog') - col = layout.column(align=True) + col.prop(rpdat, 'arm_fog') + col = col.column(align=True) col.enabled = rpdat.arm_fog col.prop(rpdat, 'arm_fog_color') col.prop(rpdat, 'arm_fog_amounta') col.prop(rpdat, 'arm_fog_amountb') layout.separator() - layout.prop(rpdat, "rp_autoexposure") + col = layout.column() - col.enabled = rpdat.rp_autoexposure - col.prop(rpdat, 'arm_autoexposure_strength', text='Strength') - col.prop(rpdat, 'arm_autoexposure_speed', text='Speed') - layout.prop(rpdat, 'arm_lens_texture') + col.prop(rpdat, "rp_autoexposure") + sub = col.column(align=True) + sub.enabled = rpdat.rp_autoexposure + sub.prop(rpdat, 'arm_autoexposure_strength', text='Strength') + sub.prop(rpdat, 'arm_autoexposure_speed', text='Speed') + layout.separator() + + col = layout.column() + col.prop(rpdat, 'arm_lensflare') + col.prop(rpdat, 'arm_fisheye') + + col = layout.column() + col.prop(rpdat, 'arm_lens_texture') if rpdat.arm_lens_texture != "": - layout.prop(rpdat, 'arm_lens_texture_masking') + col.prop(rpdat, 'arm_lens_texture_masking') if rpdat.arm_lens_texture_masking: - layout.prop(rpdat, 'arm_lens_texture_masking_centerMinClip') - layout.prop(rpdat, 'arm_lens_texture_masking_centerMaxClip') - layout.prop(rpdat, 'arm_lens_texture_masking_luminanceMin') - layout.prop(rpdat, 'arm_lens_texture_masking_luminanceMax') - layout.prop(rpdat, 'arm_lens_texture_masking_brightnessExp') + sub = col.column(align=True) + sub.prop(rpdat, 'arm_lens_texture_masking_centerMinClip') + sub.prop(rpdat, 'arm_lens_texture_masking_centerMaxClip') + sub = col.column(align=True) + sub.prop(rpdat, 'arm_lens_texture_masking_luminanceMin') + sub.prop(rpdat, 'arm_lens_texture_masking_luminanceMax') + col.prop(rpdat, 'arm_lens_texture_masking_brightnessExp') + layout.separator() + layout.prop(rpdat, 'arm_lut_texture') class ARM_PT_BakePanel(bpy.types.Panel): @@ -2580,6 +2584,18 @@ def draw_custom_node_menu(self, context): layout.prop(context.active_node, 'arm_material_param', text='Armory: Material Parameter') +def draw_conditional_prop(layout: bpy.types.UILayout, heading: str, data: bpy.types.AnyType, prop_condition: str, prop_value: str) -> None: + """Draws a property row with a checkbox that enables a value field. + The function fails when prop_condition is not a boolean property. + """ + col = layout.column(heading=heading) + row = col.row() + row.prop(data, prop_condition, text='') + sub = row.row() + sub.enabled = getattr(data, prop_condition) + sub.prop(data, prop_value, expand=True) + + def register(): bpy.utils.register_class(ARM_PT_ObjectPropsPanel) bpy.utils.register_class(ARM_PT_ModifiersPropsPanel) From 40efd58214b61b7805920ac95726d6f695590495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 14 Jan 2021 21:24:42 +0100 Subject: [PATCH 19/63] Blender 2.9: Improve trait list UI and fix checkbox position --- blender/arm/props_traits.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index b0836246..9e2943dc 100755 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -101,16 +101,20 @@ class ARM_UL_TraitList(bpy.types.UIList): custom_icon = 'NODETREE' if self.layout_type in {'DEFAULT', 'COMPACT'}: - layout.prop(item, "enabled_prop") + row = layout.row() + row.separator(factor=0.1) + row.prop(item, "enabled_prop") # Display " " for props without a name to right-align the # fake_user button - layout.label(text=item.name if item.name != "" else " ", icon=custom_icon, icon_value=custom_icon_value) + row.label(text=item.name if item.name != "" else " ", icon=custom_icon, icon_value=custom_icon_value) elif self.layout_type in {'GRID'}: layout.alignment = 'CENTER' layout.label(text="", icon=custom_icon, icon_value=custom_icon_value) - layout.prop(item, "fake_user", text="", icon="FAKE_USER_ON" if item.fake_user else "FAKE_USER_OFF") + row = layout.row(align=True) + row.scale_x = 1.2 + row.prop(item, "fake_user", text="", icon="FAKE_USER_ON" if item.fake_user else "FAKE_USER_OFF") class ArmTraitListNewItem(bpy.types.Operator): bl_idname = "arm_traitlist.new_item" From b7903dbef1b27e1be00cd2a55634387f18eccb64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 14 Jan 2021 22:14:10 +0100 Subject: [PATCH 20/63] Change canvas icon according to the canvas node category --- blender/arm/props_traits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index 9e2943dc..044b0c0f 100755 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -94,7 +94,7 @@ class ARM_UL_TraitList(bpy.types.UIList): elif item.type_prop == "WebAssembly": custom_icon_value = icons_dict["wasm"].icon_id elif item.type_prop == "UI Canvas": - custom_icon = "OBJECT_DATAMODE" + custom_icon = "NODE_COMPOSITING" elif item.type_prop == "Bundled Script": custom_icon_value = icons_dict["bundle"].icon_id elif item.type_prop == "Logic Nodes": From fb88361c5bb9672418bc0ade3febf969e278c622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 14 Jan 2021 22:14:50 +0100 Subject: [PATCH 21/63] Blender 2.9: Simplify and improve trait panel UI --- blender/arm/props_traits.py | 104 +++++++++++++----------------------- 1 file changed, 36 insertions(+), 68 deletions(-) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index 044b0c0f..ae475444 100755 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -765,26 +765,25 @@ def draw_traits_panel(layout: bpy.types.UILayout, obj: Union[bpy.types.Object, b if obj.arm_traitlist_index >= 0 and len(obj.arm_traitlist) > 0: item = obj.arm_traitlist[obj.arm_traitlist_index] - if item.type_prop == 'Haxe Script' or item.type_prop == 'Bundled Script': - row = layout.row(align=True) - row.alignment = 'EXPAND' + row = layout.row(align=True) + row.alignment = 'EXPAND' + row.scale_y = 1.2 + if item.type_prop == 'Haxe Script' or item.type_prop == 'Bundled Script': if item.type_prop == 'Haxe Script': + row.operator("arm.new_script", icon="FILE_NEW").is_object = is_object column = row.column(align=True) - column.alignment = 'EXPAND' - if item.class_name_prop == '': - column.enabled = False + column.enabled = item.class_name_prop != '' column.operator("arm.edit_script", icon="FILE_SCRIPT").is_object = is_object - row.operator("arm.new_script").is_object = is_object - row.operator("arm.refresh_scripts", text="Refresh") # Bundled scripts else: if item.class_name_prop == 'NavMesh': - row.operator("arm.generate_navmesh") + row.operator("arm.generate_navmesh", icon="UV_VERTEXSEL") else: row.operator("arm.edit_bundled_script", icon="FILE_SCRIPT").is_object = is_object - row.operator("arm.refresh_scripts", text="Refresh") + + row.operator("arm.refresh_scripts", text="Refresh", icon="FILE_REFRESH") # Default props item.name = item.class_name_prop @@ -799,86 +798,56 @@ def draw_traits_panel(layout: bpy.types.UILayout, obj: Union[bpy.types.Object, b elif item.type_prop == 'WebAssembly': item.name = item.webassembly_prop + + row.operator("arm.new_wasm", icon="FILE_NEW") + row.operator("arm.refresh_scripts", text="Refresh", icon="FILE_REFRESH") + row = layout.row() row.prop_search(item, "webassembly_prop", bpy.data.worlds['Arm'], "arm_wasm_list", text="Module") - row = layout.row(align=True) - row.alignment = 'EXPAND' - column = row.column(align=True) - column.alignment = 'EXPAND' - if item.class_name_prop == '': - column.enabled = False - # op = column.operator("arm.edit_script", icon="FILE_SCRIPT") - # op.is_object = is_object - op = row.operator("arm.new_wasm") - # op.is_object = is_object - op = row.operator("arm.refresh_scripts", text="Refresh") elif item.type_prop == 'UI Canvas': item.name = item.canvas_name_prop - row = layout.row(align=True) - row.alignment = 'EXPAND' + row.operator("arm.new_canvas", icon="FILE_NEW").is_object = is_object column = row.column(align=True) - column.alignment = 'EXPAND' - if item.canvas_name_prop == '': - column.enabled = False - op = column.operator("arm.edit_canvas", icon="FILE_SCRIPT") - op.is_object = is_object - op = row.operator("arm.new_canvas") - op.is_object = is_object - op = row.operator("arm.refresh_canvas_list", text="Refresh") + column.enabled = item.canvas_name_prop != '' + column.operator("arm.edit_canvas", icon="NODE_COMPOSITING").is_object = is_object + row.operator("arm.refresh_canvas_list", text="Refresh", icon="FILE_REFRESH") row = layout.row() row.prop_search(item, "canvas_name_prop", bpy.data.worlds['Arm'], "arm_canvas_list", text="Canvas") elif item.type_prop == 'Logic Nodes': - # Row for buttons - row = layout.row(align=True) - row.alignment = 'EXPAND' - # New - column = row.column(align=True) - column.alignment = 'EXPAND' - op = column.operator("arm.new_treenode", text="New Tree", icon="ADD") - op.is_object = is_object - # At least one check is active Logic Node Editor - is_check_logic_node_editor = False - context_screen = bpy.context.screen - # Loop for all spaces - if context_screen is not None: - areas = context_screen.areas + # Check if there is at least one active Logic Node Editor + is_editor_active = False + if bpy.context.screen is not None: + areas = bpy.context.screen.areas for area in areas: for space in area.spaces: if space.type == 'NODE_EDITOR': if space.tree_type == 'ArmLogicTreeType' and space.node_tree is not None: - is_check_logic_node_editor = True + is_editor_active = True break - if is_check_logic_node_editor: + if is_editor_active: break - # Edit - column = row.column(align=True) - column.alignment = 'EXPAND' - if item.node_tree_prop is None: - column.enabled = False - else: - column.enabled = is_check_logic_node_editor - op = column.operator("arm.edit_treenode", text="Edit Tree", icon="NODETREE") - op.is_object = is_object - # Get from Node Tree Editor - column = row.column(align=True) - column.alignment = 'EXPAND' - if item is None: - column.enabled = False - else: - column.enabled = is_check_logic_node_editor - op = column.operator("arm.get_treenode", text="From Editor", icon="IMPORT") - op.is_object = is_object - # Row for search + row.operator("arm.new_treenode", text="New Tree", icon="ADD").is_object = is_object + + column = row.column(align=True) + column.enabled = is_editor_active and item.node_tree_prop is not None + column.operator("arm.edit_treenode", text="Edit Tree", icon="NODETREE").is_object = is_object + + column = row.column(align=True) + column.enabled = is_editor_active and item is not None + column.operator("arm.get_treenode", text="From Editor", icon="IMPORT").is_object = is_object + row = layout.row() row.prop_search(item, "node_tree_prop", bpy.data, "node_groups", text="Tree") + # ===================== + # Draw trait properties if item.type_prop == 'Haxe Script' or item.type_prop == 'Bundled Script': - # Props + if item.arm_traitpropslist: layout.label(text="Trait Properties:") if item.arm_traitpropswarnings: @@ -888,7 +857,6 @@ def draw_traits_panel(layout: bpy.types.UILayout, obj: Union[bpy.types.Object, b for warning in item.arm_traitpropswarnings: box.label(text=warning.warning) - propsrow = layout.row() propsrows = max(len(item.arm_traitpropslist), 6) row = layout.row() row.template_list("ARM_UL_PropList", "The_List", item, "arm_traitpropslist", item, "arm_traitpropslist_index", rows=propsrows) From 820de42e8397ef54efc234d636614e6b47ead61c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 14 Jan 2021 22:17:42 +0100 Subject: [PATCH 22/63] Blender 2.9: Fix lod list layout --- blender/arm/props_lod.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/blender/arm/props_lod.py b/blender/arm/props_lod.py index b483007c..e1da2a82 100755 --- a/blender/arm/props_lod.py +++ b/blender/arm/props_lod.py @@ -37,11 +37,12 @@ class ARM_UL_LodList(bpy.types.UIList): layout.use_property_split = False if self.layout_type in {'DEFAULT', 'COMPACT'}: - layout.prop(item, "enabled_prop") + row = layout.row() + row.separator(factor=0.1) + row.prop(item, "enabled_prop") name = item.name if name == '': name = 'None' - row = layout.row() row.label(text=name, icon='OBJECT_DATAMODE') col = row.column() col.alignment = 'RIGHT' From 04fdcc550005140f7806d07cb46f1e9da1fba859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 14 Jan 2021 22:28:58 +0100 Subject: [PATCH 23/63] Cleanup --- blender/arm/props_traits.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index ae475444..fa78b108 100755 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -639,11 +639,8 @@ class ARM_PT_TraitPanel(bpy.types.Panel): bl_context = "object" def draw(self, context): - layout = self.layout - layout.use_property_split = True - layout.use_property_decorate = False obj = bpy.context.object - draw_traits_panel(layout, obj, is_object=True) + draw_traits_panel(self.layout, obj, is_object=True) class ARM_PT_SceneTraitPanel(bpy.types.Panel): bl_label = "Armory Scene Traits" @@ -652,11 +649,8 @@ class ARM_PT_SceneTraitPanel(bpy.types.Panel): bl_context = "scene" def draw(self, context): - layout = self.layout - layout.use_property_split = True - layout.use_property_decorate = False obj = bpy.context.scene - draw_traits_panel(layout, obj, is_object=False) + draw_traits_panel(self.layout, obj, is_object=False) class ARM_OT_CopyTraitsFromActive(bpy.types.Operator): bl_label = 'Copy Traits from Active Object' @@ -735,6 +729,9 @@ class ARM_OT_CopyTraitsFromActive(bpy.types.Operator): def draw_traits_panel(layout: bpy.types.UILayout, obj: Union[bpy.types.Object, bpy.types.Scene], is_object: bool) -> None: + layout.use_property_split = True + layout.use_property_decorate = False + # Make the list bigger when there are a few traits num_rows = 2 if len(obj.arm_traitlist) > 1: From fa7f58e4dd3925c144dda38d66d1b6d9fd9aa6ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 14 Jan 2021 22:46:22 +0100 Subject: [PATCH 24/63] Ensure that only logic node trees show up as node traits --- blender/arm/props_traits.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index fa78b108..7dd5c429 100755 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -62,6 +62,10 @@ def update_trait_group(self, context): pass class ArmTraitListItem(bpy.types.PropertyGroup): + def poll_node_trees(self, tree: NodeTree): + """Ensure that only logic node trees show up as node traits""" + return tree.bl_idname == 'ArmLogicTreeType' + name: StringProperty(name="Name", description="A name for this item", default="") enabled_prop: BoolProperty(name="", description="A name for this item", default=True, update=trigger_recompile) is_object: BoolProperty(name="", default=True) @@ -77,7 +81,7 @@ class ArmTraitListItem(bpy.types.PropertyGroup): class_name_prop: StringProperty(name="Class", description="A name for this item", default="", update=update_trait_group) canvas_name_prop: StringProperty(name="Canvas", description="A name for this item", default="", update=update_trait_group) webassembly_prop: StringProperty(name="Module", description="A name for this item", default="", update=update_trait_group) - node_tree_prop: PointerProperty(type=NodeTree, update=update_trait_group) + node_tree_prop: PointerProperty(type=NodeTree, update=update_trait_group, poll=poll_node_trees) arm_traitpropslist: CollectionProperty(type=ArmTraitPropListItem) arm_traitpropslist_index: IntProperty(name="Index for my_list", default=0) arm_traitpropswarnings: CollectionProperty(type=ArmTraitPropWarning) From ee1b55184cb7d0c8d004ad1f829ddcaa044bd6fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 15 Jan 2021 19:26:31 +0100 Subject: [PATCH 25/63] Move icon code into module This, combined with lazy loading, has the advantage of using icons in property definitions before the actual registration code runs --- blender/arm/nodes_logic.py | 3 ++- blender/arm/props_traits.py | 16 +++++----------- blender/arm/props_ui.py | 3 ++- blender/arm/ui_icons.py | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 blender/arm/ui_icons.py diff --git a/blender/arm/nodes_logic.py b/blender/arm/nodes_logic.py index 4a61c1f7..12f605fb 100755 --- a/blender/arm/nodes_logic.py +++ b/blender/arm/nodes_logic.py @@ -8,6 +8,7 @@ import arm.logicnode.arm_nodes as arm_nodes import arm.logicnode.replacement import arm.logicnode import arm.props_traits +import arm.ui_icons as ui_icons import arm.utils registered_nodes = [] @@ -174,7 +175,7 @@ class ARM_PT_LogicNodePanel(bpy.types.Panel): layout.operator('arm.open_node_documentation', icon='HELP') column = layout.column(align=True) column.operator('arm.open_node_python_source', icon='FILE_SCRIPT') - column.operator('arm.open_node_haxe_source', icon_value=arm.props_traits.icons_dict['haxe'].icon_id) + column.operator('arm.open_node_haxe_source', icon_value=ui_icons.get_id("haxe")) class ArmOpenNodeHaxeSource(bpy.types.Operator): diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index 7dd5c429..2353c1da 100755 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -11,6 +11,7 @@ import bpy.utils.previews import arm.make as make from arm.props_traits_props import * import arm.proxy as proxy +import arm.ui_icons as ui_icons import arm.utils import arm.write_data as write_data @@ -94,13 +95,13 @@ class ARM_UL_TraitList(bpy.types.UIList): custom_icon = "NONE" custom_icon_value = 0 if item.type_prop == "Haxe Script": - custom_icon_value = icons_dict["haxe"].icon_id + custom_icon_value = ui_icons.get_id("haxe") elif item.type_prop == "WebAssembly": - custom_icon_value = icons_dict["wasm"].icon_id + custom_icon_value = ui_icons.get_id("wasm") elif item.type_prop == "UI Canvas": custom_icon = "NODE_COMPOSITING" elif item.type_prop == "Bundled Script": - custom_icon_value = icons_dict["bundle"].icon_id + custom_icon_value = ui_icons.get_id("bundle") elif item.type_prop == "Logic Nodes": custom_icon = 'NODETREE' @@ -862,8 +863,8 @@ def draw_traits_panel(layout: bpy.types.UILayout, obj: Union[bpy.types.Object, b row = layout.row() row.template_list("ARM_UL_PropList", "The_List", item, "arm_traitpropslist", item, "arm_traitpropslist_index", rows=propsrows) + def register(): - global icons_dict bpy.utils.register_class(ArmTraitListItem) bpy.utils.register_class(ARM_UL_TraitList) bpy.utils.register_class(ArmTraitListNewItem) @@ -891,14 +892,8 @@ def register(): bpy.types.Scene.arm_traitlist = CollectionProperty(type=ArmTraitListItem) bpy.types.Scene.arm_traitlist_index = IntProperty(name="Index for arm_traitlist", default=0) - icons_dict = bpy.utils.previews.new() - icons_dir = os.path.join(os.path.dirname(__file__), "custom_icons") - icons_dict.load("haxe", os.path.join(icons_dir, "haxe.png"), 'IMAGE') - icons_dict.load("wasm", os.path.join(icons_dir, "wasm.png"), 'IMAGE') - icons_dict.load("bundle", os.path.join(icons_dir, "bundle.png"), 'IMAGE') def unregister(): - global icons_dict bpy.utils.unregister_class(ARM_OT_CopyTraitsFromActive) bpy.utils.unregister_class(ArmTraitListItem) bpy.utils.unregister_class(ARM_UL_TraitList) @@ -920,4 +915,3 @@ def unregister(): bpy.utils.unregister_class(ArmRefreshCanvasListButton) bpy.utils.unregister_class(ARM_PT_TraitPanel) bpy.utils.unregister_class(ARM_PT_SceneTraitPanel) - bpy.utils.previews.remove(icons_dict) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 4de624e3..304f635f 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -15,6 +15,7 @@ import arm.props_properties import arm.props_traits import arm.nodes_logic import arm.proxy +import arm.ui_icons as ui_icons import arm.utils from arm.lightmapper.utility import icon @@ -2574,7 +2575,7 @@ def draw_custom_node_menu(self, context): layout = self.layout layout.separator() layout.operator("arm.open_node_documentation", text="Show documentation for this node", icon='HELP') - layout.operator("arm.open_node_haxe_source", text="Open .hx source in the browser", icon_value=arm.props_traits.icons_dict['haxe'].icon_id) + layout.operator("arm.open_node_haxe_source", text="Open .hx source in the browser", icon_value=ui_icons.get_id("haxe")) layout.operator("arm.open_node_python_source", text="Open .py source in the browser", icon='FILE_SCRIPT') elif context.space_data.tree_type == 'ShaderNodeTree': diff --git a/blender/arm/ui_icons.py b/blender/arm/ui_icons.py new file mode 100644 index 00000000..f9364dd7 --- /dev/null +++ b/blender/arm/ui_icons.py @@ -0,0 +1,34 @@ +""" +Blender user interface icon handling. +""" +import os.path +from typing import Optional + +import bpy.utils.previews + +_icons_dict: Optional[bpy.utils.previews.ImagePreviewCollection] = None +"""Dictionary of all loaded icons, or `None` if not loaded""" + +_icons_dir = os.path.join(os.path.dirname(__file__), "custom_icons") +"""Directory of the icon files""" + + +def _load_icons() -> None: + """(Re)loads all icons""" + global _icons_dict + + if _icons_dict is not None: + bpy.utils.previews.remove(_icons_dict) + + _icons_dict = bpy.utils.previews.new() + _icons_dict.load("armory", os.path.join(_icons_dir, "armory.png"), 'IMAGE') + _icons_dict.load("bundle", os.path.join(_icons_dir, "bundle.png"), 'IMAGE') + _icons_dict.load("haxe", os.path.join(_icons_dir, "haxe.png"), 'IMAGE') + _icons_dict.load("wasm", os.path.join(_icons_dir, "wasm.png"), 'IMAGE') + + +def get_id(identifier: str) -> int: + """Returns the icon ID from the given identifier""" + if _icons_dict is None: + _load_icons() + return _icons_dict[identifier].icon_id From 7b55f3d8f57894d87f8518b742e20af2c8f166ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 17 Jan 2021 16:53:17 +0100 Subject: [PATCH 26/63] Blender 2.9: Improve `Add Trait` operator UI --- blender/arm/props_traits.py | 47 ++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index 2353c1da..2b1c0115 100755 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -15,7 +15,19 @@ import arm.ui_icons as ui_icons import arm.utils import arm.write_data as write_data -icons_dict: bpy.utils.previews.ImagePreviewCollection +ICON_HAXE = ui_icons.get_id('haxe') +ICON_NODES = 'NODETREE' +ICON_CANVAS = 'NODE_COMPOSITING' +ICON_BUNDLED = ui_icons.get_id('bundle') +ICON_WASM = ui_icons.get_id('wasm') + +PROP_TYPES_ENUM = [ + ('Haxe Script', 'Haxe', 'Haxe script', ICON_HAXE, 0), + ('Logic Nodes', 'Nodes', 'Logic nodes (visual scripting)', ICON_NODES, 1), + ('UI Canvas', 'UI', 'User interface', ICON_CANVAS, 2), + ('Bundled Script', 'Bundled', 'Premade script with common functionality', ICON_BUNDLED, 3), + ('WebAssembly', 'Wasm', 'WebAssembly', ICON_WASM, 4) +] def trigger_recompile(self, context): @@ -71,14 +83,7 @@ class ArmTraitListItem(bpy.types.PropertyGroup): enabled_prop: BoolProperty(name="", description="A name for this item", default=True, update=trigger_recompile) is_object: BoolProperty(name="", default=True) fake_user: BoolProperty(name="Fake User", description="Export this trait even if it is deactivated", default=False) - type_prop: EnumProperty( - items = [('Haxe Script', 'Haxe', 'Haxe Script'), - ('WebAssembly', 'Wasm', 'WebAssembly'), - ('UI Canvas', 'UI', 'UI Canvas'), - ('Bundled Script', 'Bundled', 'Bundled Script'), - ('Logic Nodes', 'Nodes', 'Logic Nodes') - ], - name = "Type") + type_prop: EnumProperty(name="Type", items=PROP_TYPES_ENUM) class_name_prop: StringProperty(name="Class", description="A name for this item", default="", update=update_trait_group) canvas_name_prop: StringProperty(name="Canvas", description="A name for this item", default="", update=update_trait_group) webassembly_prop: StringProperty(name="Module", description="A name for this item", default="", update=update_trait_group) @@ -118,34 +123,28 @@ class ARM_UL_TraitList(bpy.types.UIList): layout.label(text="", icon=custom_icon, icon_value=custom_icon_value) row = layout.row(align=True) - row.scale_x = 1.2 row.prop(item, "fake_user", text="", icon="FAKE_USER_ON" if item.fake_user else "FAKE_USER_OFF") class ArmTraitListNewItem(bpy.types.Operator): bl_idname = "arm_traitlist.new_item" - bl_label = "New Trait Item" + bl_label = "Add Trait" bl_description = "Add a new trait item to the list" - is_object: BoolProperty(name="Object Trait", description="Whether this is an object or scene trait", default=False) - type_prop: EnumProperty( - name="Type", - items=[ - ('Haxe Script', 'Haxe', 'Haxe Script'), - ('Logic Nodes', 'Nodes', 'Logic Nodes'), - ('UI Canvas', 'UI', 'UI Canvas'), - ('Bundled Script', 'Bundled', 'Bundled Script'), - ('WebAssembly', 'Wasm', 'WebAssembly') - ]) + is_object: BoolProperty(name="Is Object Trait", description="Whether this trait belongs to an object or a scene", default=False) + type_prop: EnumProperty(name="Type", items=PROP_TYPES_ENUM) def invoke(self, context, event): wm = context.window_manager - return wm.invoke_props_dialog(self) + return wm.invoke_props_dialog(self, width=400) def draw(self, context): layout = self.layout # Todo: show is_object property when called from operator search menu # layout.prop(self, "is_object") - layout.prop(self, "type_prop", expand=True) + + row = layout.row() + row.scale_y = 1.3 + row.prop(self, "type_prop", expand=True) def execute(self, context): if self.is_object: @@ -162,7 +161,7 @@ class ArmTraitListNewItem(bpy.types.Operator): class ArmTraitListDeleteItem(bpy.types.Operator): """Delete the selected item from the list""" bl_idname = "arm_traitlist.delete_item" - bl_label = "Deletes an item" + bl_label = "Remove Trait" bl_options = {'INTERNAL'} is_object: BoolProperty(name="", description="A name for this item", default=False) From ffd3d6e82434db4f840cb59f410693439f1218ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 17 Jan 2021 16:54:08 +0100 Subject: [PATCH 27/63] Show is_object property when `Add Trait` is not invoked from the UI --- blender/arm/props_traits.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index 2b1c0115..e10bf283 100755 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -133,14 +133,19 @@ class ArmTraitListNewItem(bpy.types.Operator): is_object: BoolProperty(name="Is Object Trait", description="Whether this trait belongs to an object or a scene", default=False) type_prop: EnumProperty(name="Type", items=PROP_TYPES_ENUM) + # Show more options when invoked from the operator search menu + invoked_by_search: BoolProperty(name="", default=True) + def invoke(self, context, event): wm = context.window_manager return wm.invoke_props_dialog(self, width=400) def draw(self, context): layout = self.layout - # Todo: show is_object property when called from operator search menu - # layout.prop(self, "is_object") + + if self.invoked_by_search: + row = layout.row() + row.prop(self, "is_object") row = layout.row() row.scale_y = 1.3 @@ -590,8 +595,6 @@ class ArmNewCanvasDialog(bpy.types.Operator): self.canvas_name = self.canvas_name.replace(' ', '') write_data.write_canvasjson(self.canvas_name) arm.utils.fetch_script_names() - # Todo: create new trait item when called from operator search - # menu, then remove 'INTERNAL' from bl_options item = obj.arm_traitlist[obj.arm_traitlist_index] item.canvas_name_prop = self.canvas_name return {'FINISHED'} @@ -746,6 +749,7 @@ def draw_traits_panel(layout: bpy.types.UILayout, obj: Union[bpy.types.Object, b col = row.column(align=True) op = col.operator("arm_traitlist.new_item", icon='ADD', text="") + op.invoked_by_search = False op.is_object = is_object if is_object: op = col.operator("arm_traitlist.delete_item", icon='REMOVE', text="") From 562d39c2035fa46f499d6aaeafe8954ab43b5aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 17 Jan 2021 20:24:43 +0100 Subject: [PATCH 28/63] Blender 2.9: Improve Armory player panel UI --- blender/arm/props_ui.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 304f635f..fc31a31d 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -406,20 +406,25 @@ class ARM_PT_ArmoryPlayerPanel(bpy.types.Panel): layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Arm'] + row = layout.row(align=True) row.alignment = 'EXPAND' + row.scale_y = 1.3 if state.proc_play is None and state.proc_build is None: row.operator("arm.play", icon="PLAY") else: row.operator("arm.stop", icon="MESH_PLANE") - row.operator("arm.clean_menu") - layout.prop(wrd, 'arm_runtime') - layout.prop(wrd, 'arm_play_camera') - layout.prop(wrd, 'arm_play_scene') + row.operator("arm.clean_menu", icon="BRUSH_DATA") + + box = layout.box() + box.prop(wrd, 'arm_runtime') + box.prop(wrd, 'arm_play_camera') + box.prop(wrd, 'arm_play_scene') if log.num_warnings > 0: box = layout.box() - # Less spacing between lines + box.alert = True + col = box.column(align=True) col.label(text=f'{log.num_warnings} warnings occurred during compilation!', icon='ERROR') # Blank icon to achieve the same indentation as the line before From 22d3530863e659e81f4df74494d7a9c1b9a2b816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 21 Jan 2021 16:12:32 +0100 Subject: [PATCH 29/63] Blender 2.9: Add Nishita sky model implementation --- Shaders/std/sky.glsl | 118 ++++++++++++++++++ .../material/cycles_nodes/nodes_texture.py | 27 +++- 2 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 Shaders/std/sky.glsl diff --git a/Shaders/std/sky.glsl b/Shaders/std/sky.glsl new file mode 100644 index 00000000..21c64ea0 --- /dev/null +++ b/Shaders/std/sky.glsl @@ -0,0 +1,118 @@ +/* Various sky functions + * + * Nishita model is based on https://github.com/wwwtyro/glsl-atmosphere (Unlicense License) + * Changes to the original implementation: + * - r and pSun parameters of nishita_atmosphere() are already normalized + */ + +#ifndef _SKY_GLSL_ +#define _SKY_GLSL_ + +#define PI 3.141592 + +#define nishita_iSteps 16 +#define nishita_jSteps 8 + +/* ray-sphere intersection that assumes + * the sphere is centered at the origin. + * No intersection when result.x > result.y */ +vec2 nishita_rsi(const vec3 r0, const vec3 rd, const float sr) { + float a = dot(rd, rd); + float b = 2.0 * dot(rd, r0); + float c = dot(r0, r0) - (sr * sr); + float d = (b*b) - 4.0*a*c; + + if (d < 0.0) return vec2(1e5,-1e5); + return vec2( + (-b - sqrt(d))/(2.0*a), + (-b + sqrt(d))/(2.0*a) + ); +} + +vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const float iSun, const float rPlanet, const float rAtmos, const vec3 kRlh, const float kMie, const float shRlh, const float shMie, const float g) { + // r and pSun must be already normalized! + + // Calculate the step size of the primary ray. + vec2 p = nishita_rsi(r0, r, rAtmos); + if (p.x > p.y) return vec3(0,0,0); + p.y = min(p.y, nishita_rsi(r0, r, rPlanet).x); + float iStepSize = (p.y - p.x) / float(nishita_iSteps); + + // Initialize the primary ray time. + float iTime = 0.0; + + // Initialize accumulators for Rayleigh and Mie scattering. + vec3 totalRlh = vec3(0,0,0); + vec3 totalMie = vec3(0,0,0); + + // Initialize optical depth accumulators for the primary ray. + float iOdRlh = 0.0; + float iOdMie = 0.0; + + // Calculate the Rayleigh and Mie phases. + float mu = dot(r, pSun); + float mumu = mu * mu; + float gg = g * g; + float pRlh = 3.0 / (16.0 * PI) * (1.0 + mumu); + float pMie = 3.0 / (8.0 * PI) * ((1.0 - gg) * (mumu + 1.0)) / (pow(1.0 + gg - 2.0 * mu * g, 1.5) * (2.0 + gg)); + + // Sample the primary ray. + for (int i = 0; i < nishita_iSteps; i++) { + + // Calculate the primary ray sample position. + vec3 iPos = r0 + r * (iTime + iStepSize * 0.5); + + // Calculate the height of the sample. + float iHeight = length(iPos) - rPlanet; + + // Calculate the optical depth of the Rayleigh and Mie scattering for this step. + float odStepRlh = exp(-iHeight / shRlh) * iStepSize; + float odStepMie = exp(-iHeight / shMie) * iStepSize; + + // Accumulate optical depth. + iOdRlh += odStepRlh; + iOdMie += odStepMie; + + // Calculate the step size of the secondary ray. + float jStepSize = nishita_rsi(iPos, pSun, rAtmos).y / float(nishita_jSteps); + + // Initialize the secondary ray time. + float jTime = 0.0; + + // Initialize optical depth accumulators for the secondary ray. + float jOdRlh = 0.0; + float jOdMie = 0.0; + + // Sample the secondary ray. + for (int j = 0; j < nishita_jSteps; j++) { + + // Calculate the secondary ray sample position. + vec3 jPos = iPos + pSun * (jTime + jStepSize * 0.5); + + // Calculate the height of the sample. + float jHeight = length(jPos) - rPlanet; + + // Accumulate the optical depth. + jOdRlh += exp(-jHeight / shRlh) * jStepSize; + jOdMie += exp(-jHeight / shMie) * jStepSize; + + // Increment the secondary ray time. + jTime += jStepSize; + } + + // Calculate attenuation. + vec3 attn = exp(-(kMie * (iOdMie + jOdMie) + kRlh * (iOdRlh + jOdRlh))); + + // Accumulate scattering. + totalRlh += odStepRlh * attn; + totalMie += odStepMie * attn; + + // Increment the primary ray time. + iTime += iStepSize; + } + + // Calculate and return the final color. + return iSun * (pRlh * kRlh * totalRlh + pMie * kMie * totalMie); +} + +#endif diff --git a/blender/arm/material/cycles_nodes/nodes_texture.py b/blender/arm/material/cycles_nodes/nodes_texture.py index b9f57daf..f3f9c070 100644 --- a/blender/arm/material/cycles_nodes/nodes_texture.py +++ b/blender/arm/material/cycles_nodes/nodes_texture.py @@ -293,6 +293,21 @@ def parse_tex_sky(node: bpy.types.ShaderNodeTexSky, out_socket: bpy.types.NodeSo # Pass through return c.to_vec3([0.0, 0.0, 0.0]) + if node.sky_type == 'PREETHAM' or node.sky_type == 'HOSEK_WILKIE': + if node.sky_type == 'PREETHAM': + log.warn('Preetham sky model is not supported, using Hosek Wilkie sky model instead') + + return parse_sky_hosekwilkie(node, state) + + elif node.sky_type == 'NISHITA': + return parse_sky_nishita(node, state) + + else: + log.error(f'Unsupported sky model: {node.sky_type}!') + return c.to_vec3([0.0, 0.0, 0.0]) + + +def parse_sky_hosekwilkie(node: bpy.types.ShaderNodeTexSky, state: ParserState) -> vec3str: world = state.world curshader = state.curshader @@ -312,10 +327,10 @@ def parse_tex_sky(node: bpy.types.ShaderNodeTexSky, out_socket: bpy.types.NodeSo curshader.add_uniform('vec3 I', link="_hosekI") curshader.add_uniform('vec3 Z', link="_hosekZ") curshader.add_uniform('vec3 hosekSunDirection', link="_hosekSunDirection") - curshader.add_function('''vec3 hosekWilkie(float cos_theta, float gamma, float cos_gamma) { + curshader.add_function("""vec3 hosekWilkie(float cos_theta, float gamma, float cos_gamma) { \tvec3 chi = (1 + cos_gamma * cos_gamma) / pow(1 + H * H - 2 * cos_gamma * H, vec3(1.5)); \treturn (1 + A * exp(B / (cos_theta + 0.01))) * (C + D * exp(E * gamma) + F * (cos_gamma * cos_gamma) + G * chi + I * sqrt(cos_theta)); -}''') +}""") world.arm_envtex_sun_direction = [node.sun_direction[0], node.sun_direction[1], node.sun_direction[2]] world.arm_envtex_turbidity = node.turbidity @@ -353,6 +368,14 @@ def parse_tex_sky(node: bpy.types.ShaderNodeTexSky, out_socket: bpy.types.NodeSo return 'Z * hosekWilkie(cos_theta, gamma_val, cos_gamma) * envmapStrength;' +def parse_sky_nishita(node: bpy.types.ShaderNodeTexSky, state: ParserState) -> vec3str: + curshader = state.curshader + curshader.add_include('std/sky.glsl') + curshader.add_uniform('vec3 sunDir', link='_sunDirection') + + return 'nishita_atmosphere(n, vec3(0,0,6372e3), sunDir, 22.0, 6371e3, 6471e3, vec3(5.5e-6,13.0e-6,22.4e-6), 21e-6, 8e3, 1.2e3, 0.758)' + + def parse_tex_environment(node: bpy.types.ShaderNodeTexEnvironment, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: if state.context == ParserContext.OBJECT: log.warn('Environment Texture node is not supported for object node trees, using default value') From 396e60574ad2d17b8e1012e66a5994767d4c6b0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 21 Jan 2021 20:36:03 +0100 Subject: [PATCH 30/63] Optimize Nishita sky shader --- Shaders/std/sky.glsl | 42 +++++++++++++------ .../material/cycles_nodes/nodes_texture.py | 5 ++- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/Shaders/std/sky.glsl b/Shaders/std/sky.glsl index 21c64ea0..310ed2ad 100644 --- a/Shaders/std/sky.glsl +++ b/Shaders/std/sky.glsl @@ -3,6 +3,7 @@ * Nishita model is based on https://github.com/wwwtyro/glsl-atmosphere (Unlicense License) * Changes to the original implementation: * - r and pSun parameters of nishita_atmosphere() are already normalized + * - Some original parameters of nishita_atmosphere() are replaced with pre-defined values */ #ifndef _SKY_GLSL_ @@ -13,6 +14,18 @@ #define nishita_iSteps 16 #define nishita_jSteps 8 +// The values here are taken from Cycles code if they +// exist there, otherwise they are taken from the example +// in the glsl-atmosphere repo +#define nishita_sun_intensity 22.0 +#define nishita_atmo_radius 6420e3 +#define nishita_rayleigh_scale 8e3 +#define nishita_rayleigh_coeff vec3(5.5e-6, 13.0e-6, 22.4e-6) +#define nishita_mie_scale 1.2e3 +#define nishita_mie_coeff 2e-5 +#define nishita_mie_dir 0.76 // Aerosols anisotropy ("direction") +#define nishita_mie_dir_sq 0.5776 // Squared aerosols anisotropy + /* ray-sphere intersection that assumes * the sphere is centered at the origin. * No intersection when result.x > result.y */ @@ -29,11 +42,15 @@ vec2 nishita_rsi(const vec3 r0, const vec3 rd, const float sr) { ); } -vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const float iSun, const float rPlanet, const float rAtmos, const vec3 kRlh, const float kMie, const float shRlh, const float shMie, const float g) { - // r and pSun must be already normalized! - +/* + * r: normalized ray direction + * r0: ray origin + * pSun: normalized sun direction + * rPlanet: planet radius + */ +vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const float rPlanet) { // Calculate the step size of the primary ray. - vec2 p = nishita_rsi(r0, r, rAtmos); + vec2 p = nishita_rsi(r0, r, nishita_atmo_radius); if (p.x > p.y) return vec3(0,0,0); p.y = min(p.y, nishita_rsi(r0, r, rPlanet).x); float iStepSize = (p.y - p.x) / float(nishita_iSteps); @@ -52,9 +69,8 @@ vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const floa // Calculate the Rayleigh and Mie phases. float mu = dot(r, pSun); float mumu = mu * mu; - float gg = g * g; float pRlh = 3.0 / (16.0 * PI) * (1.0 + mumu); - float pMie = 3.0 / (8.0 * PI) * ((1.0 - gg) * (mumu + 1.0)) / (pow(1.0 + gg - 2.0 * mu * g, 1.5) * (2.0 + gg)); + float pMie = 3.0 / (8.0 * PI) * ((1.0 - nishita_mie_dir_sq) * (mumu + 1.0)) / (pow(1.0 + nishita_mie_dir_sq - 2.0 * mu * nishita_mie_dir, 1.5) * (2.0 + nishita_mie_dir_sq)); // Sample the primary ray. for (int i = 0; i < nishita_iSteps; i++) { @@ -66,15 +82,15 @@ vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const floa float iHeight = length(iPos) - rPlanet; // Calculate the optical depth of the Rayleigh and Mie scattering for this step. - float odStepRlh = exp(-iHeight / shRlh) * iStepSize; - float odStepMie = exp(-iHeight / shMie) * iStepSize; + float odStepRlh = exp(-iHeight / nishita_rayleigh_scale) * iStepSize; + float odStepMie = exp(-iHeight / nishita_mie_scale) * iStepSize; // Accumulate optical depth. iOdRlh += odStepRlh; iOdMie += odStepMie; // Calculate the step size of the secondary ray. - float jStepSize = nishita_rsi(iPos, pSun, rAtmos).y / float(nishita_jSteps); + float jStepSize = nishita_rsi(iPos, pSun, nishita_atmo_radius).y / float(nishita_jSteps); // Initialize the secondary ray time. float jTime = 0.0; @@ -93,15 +109,15 @@ vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const floa float jHeight = length(jPos) - rPlanet; // Accumulate the optical depth. - jOdRlh += exp(-jHeight / shRlh) * jStepSize; - jOdMie += exp(-jHeight / shMie) * jStepSize; + jOdRlh += exp(-jHeight / nishita_rayleigh_scale) * jStepSize; + jOdMie += exp(-jHeight / nishita_mie_scale) * jStepSize; // Increment the secondary ray time. jTime += jStepSize; } // Calculate attenuation. - vec3 attn = exp(-(kMie * (iOdMie + jOdMie) + kRlh * (iOdRlh + jOdRlh))); + vec3 attn = exp(-(nishita_mie_coeff * (iOdMie + jOdMie) + nishita_rayleigh_coeff * (iOdRlh + jOdRlh))); // Accumulate scattering. totalRlh += odStepRlh * attn; @@ -112,7 +128,7 @@ vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const floa } // Calculate and return the final color. - return iSun * (pRlh * kRlh * totalRlh + pMie * kMie * totalMie); + return nishita_sun_intensity * (pRlh * nishita_rayleigh_coeff * totalRlh + pMie * nishita_mie_coeff * totalMie); } #endif diff --git a/blender/arm/material/cycles_nodes/nodes_texture.py b/blender/arm/material/cycles_nodes/nodes_texture.py index f3f9c070..1cb4eb98 100644 --- a/blender/arm/material/cycles_nodes/nodes_texture.py +++ b/blender/arm/material/cycles_nodes/nodes_texture.py @@ -373,7 +373,10 @@ def parse_sky_nishita(node: bpy.types.ShaderNodeTexSky, state: ParserState) -> v curshader.add_include('std/sky.glsl') curshader.add_uniform('vec3 sunDir', link='_sunDirection') - return 'nishita_atmosphere(n, vec3(0,0,6372e3), sunDir, 22.0, 6371e3, 6471e3, vec3(5.5e-6,13.0e-6,22.4e-6), 21e-6, 8e3, 1.2e3, 0.758)' + planet_radius = 6360e3 # Earth radius used in Blender + ray_origin_z = planet_radius + node.altitude * 1000 + + return f'nishita_atmosphere(n, vec3(0, 0, {ray_origin_z}), sunDir, {planet_radius})' def parse_tex_environment(node: bpy.types.ShaderNodeTexEnvironment, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: From 288ead64dc8e657078cd7848591d7d91dd8e3dba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 21 Jan 2021 21:14:05 +0100 Subject: [PATCH 31/63] Nishita sky: implement air and dust density --- Shaders/std/sky.glsl | 22 +++++++++++-------- blender/arm/material/cycles.py | 6 ++++- .../material/cycles_nodes/nodes_texture.py | 8 ++++++- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/Shaders/std/sky.glsl b/Shaders/std/sky.glsl index 310ed2ad..921eb7e8 100644 --- a/Shaders/std/sky.glsl +++ b/Shaders/std/sky.glsl @@ -1,9 +1,12 @@ /* Various sky functions + * ===================== * - * Nishita model is based on https://github.com/wwwtyro/glsl-atmosphere (Unlicense License) - * Changes to the original implementation: - * - r and pSun parameters of nishita_atmosphere() are already normalized - * - Some original parameters of nishita_atmosphere() are replaced with pre-defined values + * Nishita model is based on https://github.com/wwwtyro/glsl-atmosphere(Unlicense License) + * + * Changes to the original implementation: + * - r and pSun parameters of nishita_atmosphere() are already normalized + * - Some original parameters of nishita_atmosphere() are replaced with pre-defined values + * - Implemented air and dust density node parameters (see Blender source) */ #ifndef _SKY_GLSL_ @@ -47,8 +50,9 @@ vec2 nishita_rsi(const vec3 r0, const vec3 rd, const float sr) { * r0: ray origin * pSun: normalized sun direction * rPlanet: planet radius + * density: (air density, dust density) */ -vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const float rPlanet) { +vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const float rPlanet, const vec2 density) { // Calculate the step size of the primary ray. vec2 p = nishita_rsi(r0, r, nishita_atmo_radius); if (p.x > p.y) return vec3(0,0,0); @@ -82,8 +86,8 @@ vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const floa float iHeight = length(iPos) - rPlanet; // Calculate the optical depth of the Rayleigh and Mie scattering for this step. - float odStepRlh = exp(-iHeight / nishita_rayleigh_scale) * iStepSize; - float odStepMie = exp(-iHeight / nishita_mie_scale) * iStepSize; + float odStepRlh = exp(-iHeight / nishita_rayleigh_scale) * density.x * iStepSize; + float odStepMie = exp(-iHeight / nishita_mie_scale) * density.y * iStepSize; // Accumulate optical depth. iOdRlh += odStepRlh; @@ -109,8 +113,8 @@ vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const floa float jHeight = length(jPos) - rPlanet; // Accumulate the optical depth. - jOdRlh += exp(-jHeight / nishita_rayleigh_scale) * jStepSize; - jOdMie += exp(-jHeight / nishita_mie_scale) * jStepSize; + jOdRlh += exp(-jHeight / nishita_rayleigh_scale) * density.x * jStepSize; + jOdMie += exp(-jHeight / nishita_mie_scale) * density.y * jStepSize; // Increment the secondary ray time. jTime += jStepSize; diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index 71cf6f84..b587226c 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -640,8 +640,12 @@ def to_vec1(v): return str(v) +def to_vec2(v): + return f'vec2({v[0]}, {v[1]})' + + def to_vec3(v): - return 'vec3({0}, {1}, {2})'.format(v[0], v[1], v[2]) + return f'vec3({v[0]}, {v[1]}, {v[2]})' def rgb_to_bw(res_var: vec3str) -> floatstr: diff --git a/blender/arm/material/cycles_nodes/nodes_texture.py b/blender/arm/material/cycles_nodes/nodes_texture.py index 1cb4eb98..4aef494c 100644 --- a/blender/arm/material/cycles_nodes/nodes_texture.py +++ b/blender/arm/material/cycles_nodes/nodes_texture.py @@ -376,7 +376,13 @@ def parse_sky_nishita(node: bpy.types.ShaderNodeTexSky, state: ParserState) -> v planet_radius = 6360e3 # Earth radius used in Blender ray_origin_z = planet_radius + node.altitude * 1000 - return f'nishita_atmosphere(n, vec3(0, 0, {ray_origin_z}), sunDir, {planet_radius})' + d_air = node.air_density + d_dust = node.dust_density + # Todo: Implement ozone density (ignored for now) + # d_ozone = node.ozone_density + density = c.to_vec2((d_air, d_dust)) + + return f'nishita_atmosphere(n, vec3(0, 0, {ray_origin_z}), sunDir, {planet_radius}, {density})' def parse_tex_environment(node: bpy.types.ShaderNodeTexEnvironment, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: From 6b6dc6264fd235bfc8b801978acbca13efb761b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 24 Jan 2021 20:42:18 +0100 Subject: [PATCH 32/63] Blender 2.9: Improve exporter settings UI --- blender/arm/props.py | 2 +- blender/arm/props_ui.py | 185 ++++++++++++++++++---------------------- 2 files changed, 86 insertions(+), 101 deletions(-) diff --git a/blender/arm/props.py b/blender/arm/props.py index ab91f129..e9c6b183 100755 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -178,7 +178,7 @@ def init_properties(): ('ErrorsOnly', 'Errors Only', 'Show only errors')], name="Compile Log Parameter", update=assets.invalidate_compiler_cache, default="Summary") - bpy.types.World.arm_project_win_build_cpu = IntProperty(name="Count CPU", description="Specifies the maximum number of concurrent processes to use when building", default=1, min=1, max=multiprocessing.cpu_count()) + bpy.types.World.arm_project_win_build_cpu = IntProperty(name="CPU Count", description="Specifies the maximum number of concurrent processes to use when building", default=1, min=1, max=multiprocessing.cpu_count()) bpy.types.World.arm_project_win_build_open = BoolProperty(name="Open Build Directory", description="Open the build directory after successfully assemble", default=False) bpy.types.World.arm_project_icon = StringProperty(name="Icon (PNG)", description="Exported project icon, must be a PNG image", default="", subtype="FILE_PATH", update=assets.invalidate_compiler_cache) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index fc31a31d..a767aba7 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -494,45 +494,55 @@ class ARM_PT_ArmoryExporterPanel(bpy.types.Panel): col.prop(wrd, 'arm_asset_compression') col.prop(wrd, 'arm_single_data_file') -class ARM_PT_ArmoryExporterAndroidSettingsPanel(bpy.types.Panel): - bl_label = "Android Settings" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "render" - bl_options = { 'HIDE_HEADER' } - bl_parent_id = "ARM_PT_ArmoryExporterPanel" + +class ExporterTargetSettingsMixin: + """Mixin for common exporter setting subpanel functionality. + + Panels that inherit from this mixin need to have a arm_target + variable for polling.""" + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = 'render' + bl_parent_id = 'ARM_PT_ArmoryExporterPanel' + + # Override this in sub classes + arm_panel = '' @classmethod def poll(cls, context): wrd = bpy.data.worlds['Arm'] if (len(wrd.arm_exporterlist) > 0) and (wrd.arm_exporterlist_index >= 0): item = wrd.arm_exporterlist[wrd.arm_exporterlist_index] - return item.arm_project_target == 'android-hl' - else: - return False + return item.arm_project_target == cls.arm_target + return False + + def draw_header(self, context): + self.layout.label(text='', icon='SETTINGS') + + +class ARM_PT_ArmoryExporterAndroidSettingsPanel(ExporterTargetSettingsMixin, bpy.types.Panel): + bl_label = "Android Settings" + arm_target = 'android-hl' # See ExporterTargetSettingsMixin def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Arm'] - # Options - layout.label(text='Android Settings', icon='SETTINGS') - row = layout.row() - row.prop(wrd, 'arm_winorient') - row = layout.row() - row.prop(wrd, 'arm_project_android_sdk_compile') - row = layout.row() - row.prop(wrd, 'arm_project_android_sdk_min') - row = layout.row() - row.prop(wrd, 'arm_project_android_sdk_target') + + col = layout.column() + col.prop(wrd, 'arm_winorient') + col.prop(wrd, 'arm_project_android_sdk_compile') + col.prop(wrd, 'arm_project_android_sdk_min') + col.prop(wrd, 'arm_project_android_sdk_target') + class ARM_PT_ArmoryExporterAndroidPermissionsPanel(bpy.types.Panel): bl_label = "Permissions" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" - bl_options = { 'DEFAULT_CLOSED' } + bl_options = {'DEFAULT_CLOSED'} bl_parent_id = "ARM_PT_ArmoryExporterAndroidSettingsPanel" def draw(self, context): @@ -590,7 +600,7 @@ class ARM_PT_ArmoryExporterAndroidBuildAPKPanel(bpy.types.Panel): bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" - bl_options = { 'DEFAULT_CLOSED'} + bl_options = {'DEFAULT_CLOSED'} bl_parent_id = "ARM_PT_ArmoryExporterAndroidSettingsPanel" def draw(self, context): @@ -598,109 +608,84 @@ class ARM_PT_ArmoryExporterAndroidBuildAPKPanel(bpy.types.Panel): layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Arm'] - row = layout.row() - row.prop(wrd, 'arm_project_android_build_apk') path = arm.utils.get_android_sdk_root_path() + + col = layout.column() + + row = col.row() row.enabled = len(path) > 0 - row = layout.row() - row.prop(wrd, 'arm_project_android_rename_apk') + row.prop(wrd, 'arm_project_android_build_apk') + + row = col.row() row.enabled = wrd.arm_project_android_build_apk - row = layout.row() + row.prop(wrd, 'arm_project_android_rename_apk') + row = col.row() + row.enabled = wrd.arm_project_android_build_apk and len(arm.utils.get_android_apk_copy_path()) > 0 row.prop(wrd, 'arm_project_android_copy_apk') - row.enabled = (wrd.arm_project_android_build_apk) and (len(arm.utils.get_android_apk_copy_path()) > 0) - row = layout.row() + + row = col.row(align=True) row.prop(wrd, 'arm_project_android_list_avd') - col = row.column(align=True) - col.operator('arm.update_list_android_emulator', text='', icon='FILE_REFRESH') - col.enabled = len(path) > 0 - col = row.column(align=True) - col.operator('arm.run_android_emulator', text='', icon='PLAY') - col.enabled = len(path) > 0 and len(arm.utils.get_android_emulator_name()) > 0 - row = layout.row() - row.prop(wrd, 'arm_project_android_run_avd') + sub = row.column(align=True) + sub.enabled = len(path) > 0 + sub.operator('arm.update_list_android_emulator', text='', icon='FILE_REFRESH') + sub = row.column(align=True) + sub.enabled = len(path) > 0 and len(arm.utils.get_android_emulator_name()) > 0 + sub.operator('arm.run_android_emulator', text='', icon='PLAY') + + row = col.row() row.enabled = arm.utils.get_project_android_build_apk() and len(arm.utils.get_android_emulator_name()) > 0 + row.prop(wrd, 'arm_project_android_run_avd') -class ARM_PT_ArmoryExporterHTML5SettingsPanel(bpy.types.Panel): + +class ARM_PT_ArmoryExporterHTML5SettingsPanel(ExporterTargetSettingsMixin, bpy.types.Panel): bl_label = "HTML5 Settings" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "render" - bl_options = { 'HIDE_HEADER' } - bl_parent_id = "ARM_PT_ArmoryExporterPanel" - - @classmethod - def poll(cls, context): - wrd = bpy.data.worlds['Arm'] - if (len(wrd.arm_exporterlist) > 0) and (wrd.arm_exporterlist_index >= 0): - item = wrd.arm_exporterlist[wrd.arm_exporterlist_index] - return item.arm_project_target == 'html5' - else: - return False + arm_target = 'html5' # See ExporterTargetSettingsMixin def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Arm'] - # Options - layout.label(text='HTML5 Settings', icon='SETTINGS') - row = layout.row() - row.prop(wrd, 'arm_project_html5_popupmenu_in_browser') - row = layout.row() - row.prop(wrd, 'arm_project_html5_copy') + + col = layout.column() + col.prop(wrd, 'arm_project_html5_popupmenu_in_browser') + row = col.row() row.enabled = len(arm.utils.get_html5_copy_path()) > 0 - row = layout.row() + row.prop(wrd, 'arm_project_html5_copy') + row = col.row() + row.enabled = len(arm.utils.get_html5_copy_path()) > 0 and wrd.arm_project_html5_copy and len(arm.utils.get_link_web_server()) > 0 row.prop(wrd, 'arm_project_html5_start_browser') - row.enabled = (len(arm.utils.get_html5_copy_path()) > 0) and (wrd.arm_project_html5_copy) and (len(arm.utils.get_link_web_server()) > 0) -class ARM_PT_ArmoryExporterWindowsSettingsPanel(bpy.types.Panel): + +class ARM_PT_ArmoryExporterWindowsSettingsPanel(ExporterTargetSettingsMixin, bpy.types.Panel): bl_label = "Windows Settings" - bl_space_type = "PROPERTIES" - bl_region_type = "WINDOW" - bl_context = "render" - bl_options = { 'HIDE_HEADER' } - bl_parent_id = "ARM_PT_ArmoryExporterPanel" - - @classmethod - def poll(cls, context): - wrd = bpy.data.worlds['Arm'] - if (len(wrd.arm_exporterlist) > 0) and (wrd.arm_exporterlist_index >= 0): - item = wrd.arm_exporterlist[wrd.arm_exporterlist_index] - return item.arm_project_target == 'windows-hl' - else: - return False + arm_target = 'windows-hl' # See ExporterTargetSettingsMixin def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Arm'] - # Options - layout.label(text='Windows Settings', icon='SETTINGS') - row = layout.row() + + col = layout.column() + row = col.row(align=True) row.prop(wrd, 'arm_project_win_list_vs') - col = row.column(align=True) - col.operator('arm.update_list_installed_vs', text='', icon='FILE_REFRESH') - col.enabled = arm.utils.get_os_is_windows() - row = layout.row() - row.prop(wrd, 'arm_project_win_build') + sub = row.column(align=True) + sub.enabled = arm.utils.get_os_is_windows() + sub.operator('arm.update_list_installed_vs', text='', icon='FILE_REFRESH') + + row = col.row() row.enabled = arm.utils.get_os_is_windows() - is_enable = arm.utils.get_os_is_windows() and wrd.arm_project_win_build != '0' and wrd.arm_project_win_build != '1' - row = layout.row() - row.prop(wrd, 'arm_project_win_build_mode') - row.enabled = is_enable - row = layout.row() - row.prop(wrd, 'arm_project_win_build_arch') - row.enabled = is_enable - row = layout.row() - row.prop(wrd, 'arm_project_win_build_log') - row.enabled = is_enable - row = layout.row() - row.prop(wrd, 'arm_project_win_build_cpu') - row.enabled = is_enable - row = layout.row() - row.prop(wrd, 'arm_project_win_build_open') - row.enabled = is_enable + row.prop(wrd, 'arm_project_win_build', text='After Publish') + layout.separator() + + col = layout.column() + col.enabled = arm.utils.get_os_is_windows() and wrd.arm_project_win_build != '0' and wrd.arm_project_win_build != '1' + col.prop(wrd, 'arm_project_win_build_mode') + col.prop(wrd, 'arm_project_win_build_arch') + col.prop(wrd, 'arm_project_win_build_log') + col.prop(wrd, 'arm_project_win_build_cpu') + col.prop(wrd, 'arm_project_win_build_open') class ARM_PT_ArmoryProjectPanel(bpy.types.Panel): bl_label = "Armory Project" @@ -2737,4 +2722,4 @@ def unregister(): bpy.utils.unregister_class(ArmProxyApplyAllButton) bpy.utils.unregister_class(ArmSyncProxyButton) bpy.utils.unregister_class(ArmPrintTraitsButton) - bpy.utils.unregister_class(ARM_PT_MaterialNodePanel) \ No newline at end of file + bpy.utils.unregister_class(ARM_PT_MaterialNodePanel) From 6ae76bfdf6fed53f8c593dd8a780aa9dc4e7ac62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 25 Jan 2021 13:53:28 +0100 Subject: [PATCH 33/63] Blender 2.9: Improve exporter panel UI --- blender/arm/props_ui.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index a767aba7..4179e939 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -442,12 +442,14 @@ class ARM_PT_ArmoryExporterPanel(bpy.types.Panel): layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Arm'] + row = layout.row(align=True) row.alignment = 'EXPAND' - row.operator("arm.build_project") + row.scale_y = 1.3 + row.enabled = wrd.arm_exporterlist_index >= 0 and len(wrd.arm_exporterlist) > 0 + row.operator("arm.build_project", icon="MOD_BUILD") # row.operator("arm.patch_project") row.operator("arm.publish_project", icon="EXPORT") - row.enabled = wrd.arm_exporterlist_index >= 0 and len(wrd.arm_exporterlist) > 0 rows = 2 if len(wrd.arm_exporterlist) > 1: @@ -480,16 +482,24 @@ class ARM_PT_ArmoryExporterPanel(bpy.types.Panel): box.prop_search(item, 'arm_project_scene', bpy.data, 'scenes', text='Scene') layout.separator() - col = layout.column() + col = layout.column(align=True) col.prop(wrd, 'arm_project_name') col.prop(wrd, 'arm_project_package') col.prop(wrd, 'arm_project_bundle') + + col = layout.column(align=True) col.prop(wrd, 'arm_project_version') col.prop(wrd, 'arm_project_version_autoinc') + + col = layout.column() col.prop(wrd, 'arm_project_icon') + + col = layout.column(heading='Code Output') col.prop(wrd, 'arm_dce') col.prop(wrd, 'arm_compiler_inline') col.prop(wrd, 'arm_minify_js') + + col = layout.column(heading='Data') col.prop(wrd, 'arm_optimize_data') col.prop(wrd, 'arm_asset_compression') col.prop(wrd, 'arm_single_data_file') From d7e70c4c0a0f25d41e79550db15c613cab99802b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 25 Jan 2021 13:58:47 +0100 Subject: [PATCH 34/63] Blender 2.9: Improve project flags panel UI --- blender/arm/props.py | 4 ++-- blender/arm/props_ui.py | 34 +++++++++++++++++++++------------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/blender/arm/props.py b/blender/arm/props.py index e9c6b183..16693a93 100755 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -212,11 +212,11 @@ def init_properties(): bpy.types.World.arm_khafile = PointerProperty(name="Khafile", description="Source appended to khafile.js", update=assets.invalidate_compiler_cache, type=bpy.types.Text) bpy.types.World.arm_texture_quality = FloatProperty(name="Texture Quality", default=1.0, min=0.0, max=1.0, subtype='FACTOR', update=assets.invalidate_compiler_cache) bpy.types.World.arm_sound_quality = FloatProperty(name="Sound Quality", default=0.9, min=0.0, max=1.0, subtype='FACTOR', update=assets.invalidate_compiler_cache) - bpy.types.World.arm_minimize = BoolProperty(name="Minimize Data", description="Export scene data in binary", default=True, update=assets.invalidate_compiled_data) + bpy.types.World.arm_minimize = BoolProperty(name="Binary Scene Data", description="Export scene data in binary", default=True, update=assets.invalidate_compiled_data) bpy.types.World.arm_minify_js = BoolProperty(name="Minify JS", description="Minimize JavaScript output when publishing", default=True) bpy.types.World.arm_optimize_data = BoolProperty(name="Optimize Data", description="Export more efficient geometry and shader data, prolongs build times", default=True, update=assets.invalidate_compiled_data) bpy.types.World.arm_deinterleaved_buffers = BoolProperty(name="Deinterleaved Buffers", description="Use deinterleaved vertex buffers", default=False, update=assets.invalidate_compiler_cache) - bpy.types.World.arm_export_tangents = BoolProperty(name="Export Tangents", description="Precompute tangents for normal mapping, otherwise computed in shader", default=True, update=assets.invalidate_compiled_data) + bpy.types.World.arm_export_tangents = BoolProperty(name="Precompute Tangents", description="Precompute tangents for normal mapping, otherwise computed in shader", default=True, update=assets.invalidate_compiled_data) bpy.types.World.arm_batch_meshes = BoolProperty(name="Batch Meshes", description="Group meshes by materials to speed up rendering", default=False, update=assets.invalidate_compiler_cache) bpy.types.World.arm_batch_materials = BoolProperty(name="Batch Materials", description="Marge similar materials into single pipeline state", default=False, update=assets.invalidate_shader_cache) bpy.types.World.arm_stream_scene = BoolProperty(name="Stream Scene", description="Stream scene content", default=False, update=assets.invalidate_compiler_cache) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 4179e939..3ca9a6e2 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -500,6 +500,7 @@ class ARM_PT_ArmoryExporterPanel(bpy.types.Panel): col.prop(wrd, 'arm_minify_js') col = layout.column(heading='Data') + col.prop(wrd, 'arm_minimize') col.prop(wrd, 'arm_optimize_data') col.prop(wrd, 'arm_asset_compression') col.prop(wrd, 'arm_single_data_file') @@ -724,19 +725,26 @@ class ARM_PT_ProjectFlagsPanel(bpy.types.Panel): layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Arm'] - layout.prop(wrd, 'arm_verbose_output') - layout.prop(wrd, 'arm_cache_build') - layout.prop(wrd, 'arm_live_patch') - layout.prop(wrd, 'arm_stream_scene') - layout.prop(wrd, 'arm_batch_meshes') - layout.prop(wrd, 'arm_batch_materials') - layout.prop(wrd, 'arm_write_config') - layout.prop(wrd, 'arm_minimize') - layout.prop(wrd, 'arm_deinterleaved_buffers') - layout.prop(wrd, 'arm_export_tangents') - layout.prop(wrd, 'arm_loadscreen') - layout.prop(wrd, 'arm_texture_quality') - layout.prop(wrd, 'arm_sound_quality') + + col = layout.column(heading='Debug') + col.prop(wrd, 'arm_verbose_output') + col.prop(wrd, 'arm_cache_build') + + col = layout.column(heading='Runtime') + col.prop(wrd, 'arm_live_patch') + col.prop(wrd, 'arm_stream_scene') + col.prop(wrd, 'arm_loadscreen') + col.prop(wrd, 'arm_write_config') + + col = layout.column(heading='Renderer') + col.prop(wrd, 'arm_batch_meshes') + col.prop(wrd, 'arm_batch_materials') + col.prop(wrd, 'arm_deinterleaved_buffers') + col.prop(wrd, 'arm_export_tangents') + + col = layout.column(heading='Quality') + col.prop(wrd, 'arm_texture_quality') + col.prop(wrd, 'arm_sound_quality') class ARM_PT_ProjectFlagsDebugConsolePanel(bpy.types.Panel): bl_label = "Debug Console" From ba62ba0285e4137c518931389034ceddc1dfc6e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 25 Jan 2021 14:01:53 +0100 Subject: [PATCH 35/63] Blender 2.9: Fix/Improve collision filter mask UI --- blender/arm/props_collision_filter_mask.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/blender/arm/props_collision_filter_mask.py b/blender/arm/props_collision_filter_mask.py index d5b52910..6c4b93b5 100644 --- a/blender/arm/props_collision_filter_mask.py +++ b/blender/arm/props_collision_filter_mask.py @@ -1,31 +1,31 @@ import bpy -from bpy.props import * -from bpy.types import Panel + class ARM_PT_RbCollisionFilterMaskPanel(bpy.types.Panel): bl_label = "Armory Collision Filter Mask" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "physics" - def draw(self, context): layout = self.layout - layout.use_property_split = True + layout.use_property_split = False layout.use_property_decorate = False obj = bpy.context.object - if obj == None: + if obj is None: return - if obj.rigid_body != None: + if obj.rigid_body is not None: layout.prop(obj, 'arm_rb_collision_filter_mask') + def register(): bpy.utils.register_class(ARM_PT_RbCollisionFilterMaskPanel) bpy.types.Object.arm_rb_collision_filter_mask = bpy.props.BoolVectorProperty( name="Collision Filter Mask", - default=(True, False, False,False,False,False, False, False,False,False,False, False, False,False,False,False, False, False,False,False), + default=[True] + [False] * 19, size=20, subtype='LAYER') + def unregister(): bpy.utils.unregister_class(ARM_PT_RbCollisionFilterMaskPanel) From 1d79708b22d1ba700038eab011d0406a6b24e6dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 25 Jan 2021 16:23:03 +0100 Subject: [PATCH 36/63] Fix trait type backward compatibility with pre-Blender2.9 files --- blender/arm/props_traits.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/blender/arm/props_traits.py b/blender/arm/props_traits.py index e10bf283..8e3ddbc0 100755 --- a/blender/arm/props_traits.py +++ b/blender/arm/props_traits.py @@ -21,12 +21,15 @@ ICON_CANVAS = 'NODE_COMPOSITING' ICON_BUNDLED = ui_icons.get_id('bundle') ICON_WASM = ui_icons.get_id('wasm') +# Pay attention to the ID number parameter for backward compatibility! +# This is important if the enum is reordered or the string identifier +# is changed as the number is what's stored in the blend file PROP_TYPES_ENUM = [ ('Haxe Script', 'Haxe', 'Haxe script', ICON_HAXE, 0), - ('Logic Nodes', 'Nodes', 'Logic nodes (visual scripting)', ICON_NODES, 1), + ('Logic Nodes', 'Nodes', 'Logic nodes (visual scripting)', ICON_NODES, 4), ('UI Canvas', 'UI', 'User interface', ICON_CANVAS, 2), ('Bundled Script', 'Bundled', 'Premade script with common functionality', ICON_BUNDLED, 3), - ('WebAssembly', 'Wasm', 'WebAssembly', ICON_WASM, 4) + ('WebAssembly', 'Wasm', 'WebAssembly', ICON_WASM, 1) ] From d55f889a8408cb39a4a372d4324b97e0f8bed0b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 25 Jan 2021 16:45:30 +0100 Subject: [PATCH 37/63] Another small compositor panel UI improvement --- blender/arm/props_ui.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 3ca9a6e2..d216a027 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -1383,6 +1383,7 @@ class ARM_PT_RenderPathCompositorPanel(bpy.types.Panel): layout.enabled = rpdat.rp_compositornodes layout.prop(rpdat, 'arm_tonemap') + layout.separator() col = layout.column() draw_conditional_prop(col, 'Letterbox', rpdat, 'arm_letterbox', 'arm_letterbox_size') @@ -1411,6 +1412,7 @@ class ARM_PT_RenderPathCompositorPanel(bpy.types.Panel): col = layout.column() col.prop(rpdat, 'arm_lensflare') col.prop(rpdat, 'arm_fisheye') + layout.separator() col = layout.column() col.prop(rpdat, 'arm_lens_texture') From 05307817ee05c31c96f4673604462f32a04fea11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 25 Jan 2021 16:47:51 +0100 Subject: [PATCH 38/63] Fix grammar in warnings report --- blender/arm/make.py | 2 +- blender/arm/props_ui.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/blender/arm/make.py b/blender/arm/make.py index 0700afed..fe1e7f8c 100755 --- a/blender/arm/make.py +++ b/blender/arm/make.py @@ -433,7 +433,7 @@ def compilation_server_done(): def build_done(): print('Finished in ' + str(time.time() - profile_time)) if log.num_warnings > 0: - log.print_warn(f'{log.num_warnings} warnings occurred during compilation') + log.print_warn(f'{log.num_warnings} warning{"s" if log.num_warnings > 1 else ""} occurred during compilation') if state.proc_build is None: return result = state.proc_build.poll() diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index d216a027..32f3c719 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -426,7 +426,7 @@ class ARM_PT_ArmoryPlayerPanel(bpy.types.Panel): box.alert = True col = box.column(align=True) - col.label(text=f'{log.num_warnings} warnings occurred during compilation!', icon='ERROR') + col.label(text=f'{log.num_warnings} warning{"s" if log.num_warnings > 1 else ""} occurred during compilation!', icon='ERROR') # Blank icon to achieve the same indentation as the line before col.label(text='Please open the console to get more information.', icon='BLANK1') From e3f992b1f350b6149d579e20974997f20b8bf2ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 25 Jan 2021 16:48:03 +0100 Subject: [PATCH 39/63] Whitespace cleanup --- blender/arm/make.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/blender/arm/make.py b/blender/arm/make.py index fe1e7f8c..e2d8bff5 100755 --- a/blender/arm/make.py +++ b/blender/arm/make.py @@ -211,7 +211,7 @@ def export_data(fp, sdk_path): resx, resy = arm.utils.get_render_resolution(arm.utils.get_active_scene()) if wrd.arm_write_config: write_data.write_config(resx, resy) - + # Change project version (Build, Publish) if (not state.is_play) and (wrd.arm_project_version_autoinc): wrd.arm_project_version = arm.utils.arm.utils.change_version_project(wrd.arm_project_version) @@ -654,12 +654,12 @@ def build_success(): state.proc_publish_build = run_proc(cmd, done_gradlew_build) else: print('\nBuilding APK Warning: ANDROID_SDK_ROOT is not specified in environment variables and "Android SDK Path" setting is not specified in preferences: \n- If you specify an environment variable ANDROID_SDK_ROOT, then you need to restart Blender;\n- If you specify the setting "Android SDK Path" in the preferences, then repeat operation "Publish"') - + # HTML5 After Publish if target_name.startswith('html5'): if len(arm.utils.get_html5_copy_path()) > 0 and (wrd.arm_project_html5_copy): project_name = arm.utils.safesrc(wrd.arm_project_name +'-'+ wrd.arm_project_version) - dst = os.path.join(arm.utils.get_html5_copy_path(), project_name) + dst = os.path.join(arm.utils.get_html5_copy_path(), project_name) if os.path.exists(dst): shutil.rmtree(dst) try: @@ -673,10 +673,10 @@ def build_success(): link_html5_app = arm.utils.get_link_web_server() +'/'+ project_name print("Running a browser with a link " + link_html5_app) webbrowser.open(link_html5_app) - + # Windows After Publish if target_name.startswith('windows'): - list_vs = [] + list_vs = [] err = '' # Print message project_name = arm.utils.safesrc(wrd.arm_project_name +'-'+ wrd.arm_project_version) @@ -707,18 +707,18 @@ def build_success(): for vs in list_vs: print('- ' + vs[1] + ' (version ' + vs[3] +')') return - # Current VS + # Current VS vs_path = '' for vs in list_vs: if vs[0] == wrd.arm_project_win_list_vs: vs_path = vs[2] break - # Open in Visual Studio + # Open in Visual Studio if int(wrd.arm_project_win_build) == 1: cmd = os.path.join('start "' + vs_path, 'Common7', 'IDE', 'devenv.exe" "' + os.path.join(project_path, project_name + '.sln"')) subprocess.Popen(cmd, shell=True) # Compile - if int(wrd.arm_project_win_build) > 1: + if int(wrd.arm_project_win_build) > 1: bits = '64' if wrd.arm_project_win_build_arch == 'x64' else '32' # vcvars cmd = os.path.join(vs_path, 'VC', 'Auxiliary', 'Build', 'vcvars' + bits + '.bat') @@ -792,7 +792,7 @@ def done_vs_vars(): # MSBuild wrd = bpy.data.worlds['Arm'] list_vs, err = arm.utils.get_list_installed_vs(True, True, True) - # Current VS + # Current VS vs_path = '' vs_name = '' for vs in list_vs: @@ -851,7 +851,7 @@ def done_vs_build(): os.chdir(res_path) # set work folder subprocess.Popen(cmd, shell=True) # Open Build Directory - if wrd.arm_project_win_build_open: + if wrd.arm_project_win_build_open: arm.utils.open_folder(path) state.redraw_ui = True else: From a1bbd76de74e7b72f83f5486b4732ca5eaaf9c0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 25 Jan 2021 16:51:28 +0100 Subject: [PATCH 40/63] Fix: Remove unused/non-existing armory icon --- blender/arm/ui_icons.py | 1 - 1 file changed, 1 deletion(-) diff --git a/blender/arm/ui_icons.py b/blender/arm/ui_icons.py index f9364dd7..ed2649f5 100644 --- a/blender/arm/ui_icons.py +++ b/blender/arm/ui_icons.py @@ -21,7 +21,6 @@ def _load_icons() -> None: bpy.utils.previews.remove(_icons_dict) _icons_dict = bpy.utils.previews.new() - _icons_dict.load("armory", os.path.join(_icons_dir, "armory.png"), 'IMAGE') _icons_dict.load("bundle", os.path.join(_icons_dir, "bundle.png"), 'IMAGE') _icons_dict.load("haxe", os.path.join(_icons_dir, "haxe.png"), 'IMAGE') _icons_dict.load("wasm", os.path.join(_icons_dir, "wasm.png"), 'IMAGE') From 75bb88831c85c887fdf766144bf15bb3f6af964d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 29 Jan 2021 21:38:14 +0100 Subject: [PATCH 41/63] Blender 2.9: Cycles attribute node - support alpha output and new attribute_type prop --- blender/arm/material/cycles.py | 35 ++++++++++++ .../arm/material/cycles_nodes/nodes_input.py | 54 +++++++++++-------- 2 files changed, 67 insertions(+), 22 deletions(-) diff --git a/blender/arm/material/cycles.py b/blender/arm/material/cycles.py index 71cf6f84..be92b3ce 100644 --- a/blender/arm/material/cycles.py +++ b/blender/arm/material/cycles.py @@ -644,6 +644,41 @@ def to_vec3(v): return 'vec3({0}, {1}, {2})'.format(v[0], v[1], v[2]) +def cast_value(val: str, from_type: str, to_type: str) -> str: + """Casts a value that is already parsed in a glsl string to another + value in a string. + + vec2 types are not supported (not used in the node editor) and there + is no cast towards int types. If casting from vec3 to vec4, the w + coordinate/alpha channel is filled with a 1. + + If this function is called with invalid parameters, a TypeError is + raised. + """ + if from_type == to_type: + return val + + if from_type in ('int', 'float'): + if to_type in ('int', 'float'): + return val + elif to_type in ('vec2', 'vec3', 'vec4'): + return f'{to_type}({val})' + + elif from_type == 'vec3': + if to_type == 'float': + return rgb_to_bw(val) + elif to_type == 'vec4': + return f'vec4({val}, 1.0)' + + elif from_type == 'vec4': + if to_type == 'float': + return rgb_to_bw(val) + elif to_type == 'vec3': + return f'{val}.xyz' + + raise TypeError("Invalid type cast in shader!") + + def rgb_to_bw(res_var: vec3str) -> floatstr: return f'((({res_var}.r * 0.3 + {res_var}.g * 0.59 + {res_var}.b * 0.11) / 3.0) * 2.5)' diff --git a/blender/arm/material/cycles_nodes/nodes_input.py b/blender/arm/material/cycles_nodes/nodes_input.py index 60d86799..69accb45 100644 --- a/blender/arm/material/cycles_nodes/nodes_input.py +++ b/blender/arm/material/cycles_nodes/nodes_input.py @@ -10,42 +10,52 @@ import arm.utils def parse_attribute(node: bpy.types.ShaderNodeAttribute, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: - # Color - if out_socket == node.outputs[0]: - # Vertex colors only for now - state.con.add_elem('col', 'short4norm') - return 'vcolor' + out_type = 'float' if out_socket.type == 'VALUE' else 'vec3' - # Vector - elif out_socket == node.outputs[1]: - # UV maps only for now - state.con.add_elem('tex', 'short2norm') + if node.attribute_name == 'time': + state.curshader.add_uniform('float time', link='_time') + + if out_socket == node.outputs[3]: + return '1.0' + return c.cast_value('time', from_type='float', to_type=out_type) + + # UV maps (higher priority) and vertex colors + if node.attribute_type == 'GEOMETRY': + + # Alpha output. Armory doesn't support vertex colors with alpha + # values yet and UV maps don't have an alpha channel + if out_socket == node.outputs[3]: + return '1.0' + + # UV maps mat = c.mat_get_material() mat_users = c.mat_get_material_users() if mat_users is not None and mat in mat_users: mat_user = mat_users[mat][0] - # No UV layers for Curve + # Curves don't have uv layers, so check that first if hasattr(mat_user.data, 'uv_layers'): lays = mat_user.data.uv_layers + # First UV map referenced + if node.attribute_name == lays[0].name: + state.con.add_elem('tex', 'short2norm') + return c.cast_value('vec3(texCoord.x, 1.0 - texCoord.y, 0.0)', from_type='vec3', to_type=out_type) + # Second UV map referenced - if len(lays) > 1 and node.attribute_name == lays[1].name: + elif len(lays) > 1 and node.attribute_name == lays[1].name: state.con.add_elem('tex1', 'short2norm') - return 'vec3(texCoord1.x, 1.0 - texCoord1.y, 0.0)' + return c.cast_value('vec3(texCoord1.x, 1.0 - texCoord1.y, 0.0)', from_type='vec3', to_type=out_type) - return 'vec3(texCoord.x, 1.0 - texCoord.y, 0.0)' + # Vertex colors + # TODO: support multiple vertex color sets + state.con.add_elem('col', 'short4norm') + return c.cast_value('vcolor', from_type='vec3', to_type=out_type) - # Fac - else: - if node.attribute_name == 'time': - state.curshader.add_uniform('float time', link='_time') - return 'time' - - # Return 0.0 till drivers are implemented - else: - return '0.0' + if out_socket == node.outputs[3]: + return '1.0' + return c.cast_value('0.0', from_type='float', to_type=out_type) def parse_rgb(node: bpy.types.ShaderNodeRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: From 121d449c35535586777fb4da19a4819eb8ffc242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 29 Jan 2021 22:15:21 +0100 Subject: [PATCH 42/63] Blender 2.9: Cycles attribute node - support object/custom properties --- .../arm/material/cycles_nodes/nodes_input.py | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/blender/arm/material/cycles_nodes/nodes_input.py b/blender/arm/material/cycles_nodes/nodes_input.py index 69accb45..2a79dffd 100644 --- a/blender/arm/material/cycles_nodes/nodes_input.py +++ b/blender/arm/material/cycles_nodes/nodes_input.py @@ -1,6 +1,8 @@ -import bpy from typing import Union +import bpy +import mathutils + import arm.log as log import arm.material.cycles as c import arm.material.cycles_functions as c_functions @@ -53,6 +55,35 @@ def parse_attribute(node: bpy.types.ShaderNodeAttribute, out_socket: bpy.types.N state.con.add_elem('col', 'short4norm') return c.cast_value('vcolor', from_type='vec3', to_type=out_type) + # Check object properties + # see https://developer.blender.org/rB6fdcca8de6 for reference + mat = c.mat_get_material() + mat_users = c.mat_get_material_users() + if mat_users is not None and mat in mat_users: + # Use first material user for now... + mat_user = mat_users[mat][0] + + val = None + # Custom properties first + if node.attribute_name in mat_user: + val = mat_user[node.attribute_name] + # Blender properties + elif hasattr(mat_user, node.attribute_name): + val = getattr(mat_user, node.attribute_name) + + if val is not None: + if isinstance(val, float): + return c.cast_value(str(val), from_type='float', to_type=out_type) + elif isinstance(val, int): + return c.cast_value(str(val), from_type='int', to_type=out_type) + elif isinstance(val, mathutils.Vector) and len(val) <= 4: + out = val.to_4d() + + if out_socket == node.outputs[3]: + return c.to_vec1(out[3]) + return c.cast_value(c.to_vec3(out), from_type='vec3', to_type=out_type) + + # Default values, attribute name did not match if out_socket == node.outputs[3]: return '1.0' return c.cast_value('0.0', from_type='float', to_type=out_type) From f2c16097d4a62e832b7f84d7a7bde121a2f8b2c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sat, 30 Jan 2021 20:21:31 +0100 Subject: [PATCH 43/63] Fix lod operator polling and add some bl_options --- blender/arm/props_lod.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/blender/arm/props_lod.py b/blender/arm/props_lod.py index e1da2a82..44bfdca9 100755 --- a/blender/arm/props_lod.py +++ b/blender/arm/props_lod.py @@ -56,6 +56,7 @@ class ArmLodListNewItem(bpy.types.Operator): # Add a new item to the list bl_idname = "arm_lodlist.new_item" bl_label = "Add a new item" + bl_options = {'UNDO'} def execute(self, context): mdata = bpy.context.object.data @@ -68,10 +69,13 @@ class ArmLodListDeleteItem(bpy.types.Operator): # Delete the selected item from the list bl_idname = "arm_lodlist.delete_item" bl_label = "Deletes an item" + bl_options = {'INTERNAL', 'UNDO'} @classmethod - def poll(self, context): + def poll(cls, context): """ Enable if there's something in the list """ + if bpy.context.object is None: + return False mdata = bpy.context.object.data return len(mdata.arm_lodlist) > 0 @@ -97,6 +101,7 @@ class ArmLodListMoveItem(bpy.types.Operator): # Move an item in the list bl_idname = "arm_lodlist.move_item" bl_label = "Move an item in the list" + bl_options = {'INTERNAL', 'UNDO'} direction: EnumProperty( items=( ('UP', 'Up', ""), From 28011bcc009814df7558830fb9fd1c4aff5c56a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sat, 13 Feb 2021 18:23:37 +0100 Subject: [PATCH 44/63] Nishita sky: add sun disk drawing --- Shaders/std/sky.glsl | 17 +++++++++++++++ .../material/cycles_nodes/nodes_texture.py | 21 ++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/Shaders/std/sky.glsl b/Shaders/std/sky.glsl index 921eb7e8..6b85b4cc 100644 --- a/Shaders/std/sky.glsl +++ b/Shaders/std/sky.glsl @@ -29,6 +29,8 @@ #define nishita_mie_dir 0.76 // Aerosols anisotropy ("direction") #define nishita_mie_dir_sq 0.5776 // Squared aerosols anisotropy +#define sun_limb_darkening_col vec3(0.397, 0.503, 0.652) + /* ray-sphere intersection that assumes * the sphere is centered at the origin. * No intersection when result.x > result.y */ @@ -135,4 +137,19 @@ vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const floa return nishita_sun_intensity * (pRlh * nishita_rayleigh_coeff * totalRlh + pMie * nishita_mie_coeff * totalMie); } +vec3 sun_disk(const vec3 n, const vec3 light_dir, const float disk_size, const float intensity) { + // Normalized SDF + float dist = distance(n, light_dir) / disk_size; + + // Darken the edges of the sun + // Reference: https://media.contentapi.ea.com/content/dam/eacom/frostbite/files/s2016-pbs-frostbite-sky-clouds-new.pdf + // (Physically Based Sky, Atmosphere and Cloud Rendering in Frostbite by Sebastien Hillaire) + // Page 28, Page 60 (Code from [Nec96]) + float invDist = 1.0 - dist; + float mu = sqrt(invDist * invDist); + vec3 limb_darkening = 1.0 - (1.0 - pow(vec3(mu), sun_limb_darkening_col)); + + return 1 + (1.0 - step(1.0, dist)) * nishita_sun_intensity * intensity * limb_darkening; +} + #endif diff --git a/blender/arm/material/cycles_nodes/nodes_texture.py b/blender/arm/material/cycles_nodes/nodes_texture.py index 4aef494c..b095785d 100644 --- a/blender/arm/material/cycles_nodes/nodes_texture.py +++ b/blender/arm/material/cycles_nodes/nodes_texture.py @@ -1,3 +1,4 @@ +import math import os from typing import Union @@ -382,7 +383,25 @@ def parse_sky_nishita(node: bpy.types.ShaderNodeTexSky, state: ParserState) -> v # d_ozone = node.ozone_density density = c.to_vec2((d_air, d_dust)) - return f'nishita_atmosphere(n, vec3(0, 0, {ray_origin_z}), sunDir, {planet_radius}, {density})' + sun = '' + if node.sun_disc: + # The sun size is calculated relative in terms of the distance + # between the sun position and the sky dome normal at every + # pixel (see sun_disk() in sky.glsl). + # + # An isosceles triangle is created with the camera at the + # opposite side of the base with node.sun_size being the vertex + # angle from which the base angle theta is calculated. Iron's + # skydome geometry roughly resembles a unit sphere, so the leg + # size is set to 1. The base size is the doubled normal-relative + # target size. + + # sun_size is already in radians despite being degrees in the UI + theta = 0.5 * (math.pi - node.sun_size) + size = math.cos(theta) + sun = f'* sun_disk(n, sunDir, {size}, {node.sun_intensity})' + + return f'nishita_atmosphere(n, vec3(0, 0, {ray_origin_z}), sunDir, {planet_radius}, {density}){sun}' def parse_tex_environment(node: bpy.types.ShaderNodeTexEnvironment, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: From 702436e2a10361e1bea124f8ed54281919de1c86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sat, 13 Feb 2021 19:01:14 +0100 Subject: [PATCH 45/63] Add artistic option for darkening clouds at night --- blender/arm/make_world.py | 16 +++++++++++++--- blender/arm/props.py | 4 ++++ blender/arm/props_ui.py | 1 + 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index 8c3dcedd..98ac282c 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -293,7 +293,7 @@ def frag_write_clouds(world: bpy.types.World, frag: Shader): }''' frag.add_function(func_cloud_radiance) - frag.add_function('''vec3 traceClouds(vec3 sky, vec3 dir) { + func_trace_clouds = '''vec3 traceClouds(vec3 sky, vec3 dir) { \tconst float step_size = 0.5 / float(cloudsSteps); \tfloat T = 1.0; \tfloat C = 0.0; @@ -312,6 +312,16 @@ def frag_write_clouds(world: bpy.types.World, frag: Shader): \t\t} \t\tuv += (dir.xy / dir.z) * step_size * cloudsUpper; \t} +''' + if world.arm_darken_clouds: + func_trace_clouds += '\t// Darken clouds when the sun is low\n' -\treturn vec3(C) + sky * T; -}''') + # Nishita sky + if 'vec3 sunDir' in frag.uniforms: + func_trace_clouds += '\tC *= smoothstep(-0.02, 0.25, sunDir.z);\n' + # Hosek + else: + func_trace_clouds += '\tC *= smoothstep(0.04, 0.32, hosekSunDirection.z);\n' + + func_trace_clouds += '\treturn vec3(C) + sky * T;\n}' + frag.add_function(func_trace_clouds) diff --git a/blender/arm/props.py b/blender/arm/props.py index ae6aa0a9..1c5b1c5b 100755 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -435,6 +435,10 @@ def init_properties(): bpy.types.World.compo_defs = StringProperty(name="Compositor Shader Defs", default='') bpy.types.World.arm_use_clouds = BoolProperty(name="Clouds", default=False, update=assets.invalidate_shader_cache) + bpy.types.World.arm_darken_clouds = BoolProperty( + name="Darken Clouds at Night", + description="Darkens the clouds when the sun is low. This setting is for artistic purposes and is not physically correct", + default=False, update=assets.invalidate_shader_cache) bpy.types.World.arm_clouds_lower = FloatProperty(name="Lower", default=1.0, min=0.1, max=10.0, update=assets.invalidate_shader_cache) bpy.types.World.arm_clouds_upper = FloatProperty(name="Upper", default=1.0, min=0.1, max=10.0, update=assets.invalidate_shader_cache) bpy.types.World.arm_clouds_wind = FloatVectorProperty(name="Wind", default=[1.0, 0.0], size=2, update=assets.invalidate_shader_cache) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 82b69bc4..64435781 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -244,6 +244,7 @@ class ARM_PT_WorldPropsPanel(bpy.types.Panel): layout.prop(world, 'arm_use_clouds') col = layout.column(align=True) col.enabled = world.arm_use_clouds + col.prop(world, 'arm_darken_clouds') col.prop(world, 'arm_clouds_lower') col.prop(world, 'arm_clouds_upper') col.prop(world, 'arm_clouds_precipitation') From 92554876f191265791892e77e4b085f47978cce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 21 Feb 2021 01:12:15 +0100 Subject: [PATCH 46/63] Fix nishita sky altitude The scale was changed in recent Blender builds --- blender/arm/material/cycles_nodes/nodes_texture.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/material/cycles_nodes/nodes_texture.py b/blender/arm/material/cycles_nodes/nodes_texture.py index b095785d..6d62526f 100644 --- a/blender/arm/material/cycles_nodes/nodes_texture.py +++ b/blender/arm/material/cycles_nodes/nodes_texture.py @@ -375,7 +375,7 @@ def parse_sky_nishita(node: bpy.types.ShaderNodeTexSky, state: ParserState) -> v curshader.add_uniform('vec3 sunDir', link='_sunDirection') planet_radius = 6360e3 # Earth radius used in Blender - ray_origin_z = planet_radius + node.altitude * 1000 + ray_origin_z = planet_radius + node.altitude d_air = node.air_density d_dust = node.dust_density From 742b9ce1e1e6bf0e0f9920715d236cce3739d043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sun, 21 Feb 2021 17:00:29 +0100 Subject: [PATCH 47/63] Nishita sky: add support for ozone density --- Shaders/std/sky.glsl | 46 ++++++++++++++----- .../material/cycles_nodes/nodes_texture.py | 6 +-- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/Shaders/std/sky.glsl b/Shaders/std/sky.glsl index 6b85b4cc..9ad9964c 100644 --- a/Shaders/std/sky.glsl +++ b/Shaders/std/sky.glsl @@ -6,7 +6,14 @@ * Changes to the original implementation: * - r and pSun parameters of nishita_atmosphere() are already normalized * - Some original parameters of nishita_atmosphere() are replaced with pre-defined values - * - Implemented air and dust density node parameters (see Blender source) + * - Implemented air, dust and ozone density node parameters (see Blender source) + * + * Reference for the sun's limb darkening and ozone calculations: + * [Hill] Sebastien Hillaire. Physically Based Sky, Atmosphere and Cloud Rendering in Frostbite + * (https://media.contentapi.ea.com/content/dam/eacom/frostbite/files/s2016-pbs-frostbite-sky-clouds-new.pdf) + * + * Cycles code used for reference: blender/intern/sky/source/sky_nishita.cpp + * (https://github.com/blender/blender/blob/4429b4b77ef6754739a3c2b4fabd0537999e9bdc/intern/sky/source/sky_nishita.cpp) */ #ifndef _SKY_GLSL_ @@ -29,8 +36,21 @@ #define nishita_mie_dir 0.76 // Aerosols anisotropy ("direction") #define nishita_mie_dir_sq 0.5776 // Squared aerosols anisotropy +// The ozone absorption coefficients are taken from Cycles code. +// Because Cycles calculates 21 wavelengths, we use the coefficients +// which are closest to the RGB wavelengths (645nm, 510nm, 440nm). +// Precalculating values by simulating Blender's spec_to_xyz() function +// to include all 21 wavelengths gave unrealistic results +#define nishita_ozone_coeff vec3(1.59051840791988e-6, 0.00000096707041180970, 0.00000007309568762914) + +// Values from [Hill: 60] #define sun_limb_darkening_col vec3(0.397, 0.503, 0.652) +/* Approximates the density of ozone for a given sample height. Values taken from Cycles code. */ +float nishita_density_ozone(const float height) { + return (height < 10000.0 || height >= 40000.0) ? 0.0 : (height < 25000.0 ? (height - 10000.0) / 15000.0 : -((height - 40000.0) / 15000.0)); +} + /* ray-sphere intersection that assumes * the sphere is centered at the origin. * No intersection when result.x > result.y */ @@ -52,9 +72,9 @@ vec2 nishita_rsi(const vec3 r0, const vec3 rd, const float sr) { * r0: ray origin * pSun: normalized sun direction * rPlanet: planet radius - * density: (air density, dust density) + * density: (air density, dust density, ozone density) */ -vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const float rPlanet, const vec2 density) { +vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const float rPlanet, const vec3 density) { // Calculate the step size of the primary ray. vec2 p = nishita_rsi(r0, r, nishita_atmo_radius); if (p.x > p.y) return vec3(0,0,0); @@ -102,8 +122,7 @@ vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const floa float jTime = 0.0; // Initialize optical depth accumulators for the secondary ray. - float jOdRlh = 0.0; - float jOdMie = 0.0; + vec3 jODepth = vec3(0.0); // (Rayleigh, Mie, ozone) // Sample the secondary ray. for (int j = 0; j < nishita_jSteps; j++) { @@ -115,15 +134,22 @@ vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const floa float jHeight = length(jPos) - rPlanet; // Accumulate the optical depth. - jOdRlh += exp(-jHeight / nishita_rayleigh_scale) * density.x * jStepSize; - jOdMie += exp(-jHeight / nishita_mie_scale) * density.y * jStepSize; + jODepth += vec3( + exp(-jHeight / nishita_rayleigh_scale) * density.x * jStepSize, + exp(-jHeight / nishita_mie_scale) * density.y * jStepSize, + nishita_density_ozone(jHeight) * density.z * jStepSize + ); // Increment the secondary ray time. jTime += jStepSize; } // Calculate attenuation. - vec3 attn = exp(-(nishita_mie_coeff * (iOdMie + jOdMie) + nishita_rayleigh_coeff * (iOdRlh + jOdRlh))); + vec3 attn = exp(-( + nishita_mie_coeff * (iOdMie + jODepth.y) + + (nishita_rayleigh_coeff) * (iOdRlh + jODepth.x) + + nishita_ozone_coeff * jODepth.z + )); // Accumulate scattering. totalRlh += odStepRlh * attn; @@ -142,9 +168,7 @@ vec3 sun_disk(const vec3 n, const vec3 light_dir, const float disk_size, const f float dist = distance(n, light_dir) / disk_size; // Darken the edges of the sun - // Reference: https://media.contentapi.ea.com/content/dam/eacom/frostbite/files/s2016-pbs-frostbite-sky-clouds-new.pdf - // (Physically Based Sky, Atmosphere and Cloud Rendering in Frostbite by Sebastien Hillaire) - // Page 28, Page 60 (Code from [Nec96]) + // [Hill: 28, 60] (code from [Nec96]) float invDist = 1.0 - dist; float mu = sqrt(invDist * invDist); vec3 limb_darkening = 1.0 - (1.0 - pow(vec3(mu), sun_limb_darkening_col)); diff --git a/blender/arm/material/cycles_nodes/nodes_texture.py b/blender/arm/material/cycles_nodes/nodes_texture.py index 6d62526f..b3b81d1f 100644 --- a/blender/arm/material/cycles_nodes/nodes_texture.py +++ b/blender/arm/material/cycles_nodes/nodes_texture.py @@ -377,11 +377,7 @@ def parse_sky_nishita(node: bpy.types.ShaderNodeTexSky, state: ParserState) -> v planet_radius = 6360e3 # Earth radius used in Blender ray_origin_z = planet_radius + node.altitude - d_air = node.air_density - d_dust = node.dust_density - # Todo: Implement ozone density (ignored for now) - # d_ozone = node.ozone_density - density = c.to_vec2((d_air, d_dust)) + density = c.to_vec3((node.air_density, node.dust_density, node.ozone_density)) sun = '' if node.sun_disc: From 5f55b00710fd780b2adce9373e1609acd30aa028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 11 Mar 2021 23:16:44 +0100 Subject: [PATCH 48/63] Begin with Nishita LUT implementation for better performance --- Shaders/std/sky.glsl | 17 +++--- Sources/armory/math/Helper.hx | 9 ++++ Sources/armory/object/Uniforms.hx | 6 ++- Sources/armory/renderpath/Nishita.hx | 80 ++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 Sources/armory/renderpath/Nishita.hx diff --git a/Shaders/std/sky.glsl b/Shaders/std/sky.glsl index 9ad9964c..61f93c84 100644 --- a/Shaders/std/sky.glsl +++ b/Shaders/std/sky.glsl @@ -19,6 +19,9 @@ #ifndef _SKY_GLSL_ #define _SKY_GLSL_ +// OpenGl ES doesn't support 1D textures so we use a 1 px height sampler2D here... +uniform sampler2D nishitaLUT; + #define PI 3.141592 #define nishita_iSteps 16 @@ -46,6 +49,8 @@ // Values from [Hill: 60] #define sun_limb_darkening_col vec3(0.397, 0.503, 0.652) +#define heightToLUT(h) (textureLod(nishitaLUT, vec2(clamp(h * (1 / 60000.0), 0.0, 1.0), 0.0), 0.0).xyz * 10.0) + /* Approximates the density of ozone for a given sample height. Values taken from Cycles code. */ float nishita_density_ozone(const float height) { return (height < 10000.0 || height >= 40000.0) ? 0.0 : (height < 25000.0 ? (height - 10000.0) / 15000.0 : -((height - 40000.0) / 15000.0)); @@ -108,8 +113,9 @@ vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const floa float iHeight = length(iPos) - rPlanet; // Calculate the optical depth of the Rayleigh and Mie scattering for this step. - float odStepRlh = exp(-iHeight / nishita_rayleigh_scale) * density.x * iStepSize; - float odStepMie = exp(-iHeight / nishita_mie_scale) * density.y * iStepSize; + vec3 iLookup = heightToLUT(iHeight); + float odStepRlh = iLookup.x * iStepSize; + float odStepMie = iLookup.y * iStepSize; // Accumulate optical depth. iOdRlh += odStepRlh; @@ -134,11 +140,8 @@ vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const floa float jHeight = length(jPos) - rPlanet; // Accumulate the optical depth. - jODepth += vec3( - exp(-jHeight / nishita_rayleigh_scale) * density.x * jStepSize, - exp(-jHeight / nishita_mie_scale) * density.y * jStepSize, - nishita_density_ozone(jHeight) * density.z * jStepSize - ); + vec3 jLookup = heightToLUT(jHeight); + jODepth += jLookup * jStepSize; // Increment the secondary ray time. jTime += jStepSize; diff --git a/Sources/armory/math/Helper.hx b/Sources/armory/math/Helper.hx index 03351492..d3579d1e 100644 --- a/Sources/armory/math/Helper.hx +++ b/Sources/armory/math/Helper.hx @@ -72,4 +72,13 @@ class Helper { if (value <= leftMin) return rightMin; return map(value, leftMin, leftMax, rightMin, rightMax); } + + /** + Return the sign of the given value represented as `1.0` (positive value) + or `-1.0` (negative value). The sign of `0` is `0`. + **/ + public static inline function sign(value: Float): Float { + if (value == 0) return 0; + return (value < 0) ? -1.0 : 1.0; + } } diff --git a/Sources/armory/object/Uniforms.hx b/Sources/armory/object/Uniforms.hx index 91c55f30..64d20af2 100644 --- a/Sources/armory/object/Uniforms.hx +++ b/Sources/armory/object/Uniforms.hx @@ -19,8 +19,12 @@ class Uniforms { } public static function textureLink(object: Object, mat: MaterialData, link: String): kha.Image { + if (link == "_nishitaLUT") { + if (armory.renderpath.Nishita.data == null) armory.renderpath.Nishita.recompute(Scene.active.world); + return armory.renderpath.Nishita.data.optDepthLUT; + } #if arm_ltc - if (link == "_ltcMat") { + else if (link == "_ltcMat") { if (armory.data.ConstData.ltcMatTex == null) armory.data.ConstData.initLTC(); return armory.data.ConstData.ltcMatTex; } diff --git a/Sources/armory/renderpath/Nishita.hx b/Sources/armory/renderpath/Nishita.hx new file mode 100644 index 00000000..c968798d --- /dev/null +++ b/Sources/armory/renderpath/Nishita.hx @@ -0,0 +1,80 @@ +package armory.renderpath; + +import kha.FastFloat; +import kha.graphics4.TextureFormat; +import kha.graphics4.Usage; + +import iron.data.WorldData; + +import armory.math.Helper; + +class Nishita { + + public static var data: NishitaData = null; + + public static function recompute(world: WorldData) { + if (world == null || world.raw.sun_direction == null) return; + if (data == null) data = new NishitaData(); + + // TODO + data.recompute(1.0, 1.0, 1.0); + } +} + +class NishitaData { + static inline var LUT_WIDTH = 16; + + /** Maximum ray height as defined by Cycles **/ + static inline var MAX_HEIGHT = 60000; + + static inline var RAYLEIGH_SCALE = 8e3; + static inline var MIE_SCALE = 1.2e3; + + public var optDepthLUT: kha.Image; + + public function new() {} + + function getOzoneDensity(height: FastFloat): FastFloat { + if (height < 10000.0 || height >= 40000.0) { + return 0.0; + } + if (height < 25000.0) { + return (height - 10000.0) / 15000.0; + } + return -((height - 40000.0) / 15000.0); + } + + /** + The RGBA texture layout looks as follows: + R = Rayleigh optical depth at height \in [0, 60000] + G = Mie optical depth at height \in [0, 60000] + B = Ozone optical depth at height \in [0, 60000] + A = Unused + **/ + public function recompute(densityFacAir: FastFloat, densityFacDust: FastFloat, densityFacOzone: FastFloat) { + optDepthLUT = kha.Image.create(LUT_WIDTH, 1, TextureFormat.RGBA32, Usage.StaticUsage); + + var textureData = optDepthLUT.lock(); + for (i in 0...LUT_WIDTH) { + // Get the height for each LUT pixel i (in [-1, 1] range) + var height = (i / LUT_WIDTH) * 2 - 1; + + // Use quadratic height for better horizon precision + // See https://sebh.github.io/publications/egsr2020.pdf (5.3) + height = 0.5 + 0.5 * Helper.sign(height) * Math.sqrt(Math.abs(height)); + height *= MAX_HEIGHT; // Denormalize + + // Make sure we use 32 bit floats + var optDepthRayleigh: FastFloat = Math.exp(-height / RAYLEIGH_SCALE) * densityFacAir; + var optDepthMie: FastFloat = Math.exp(-height / MIE_SCALE) * densityFacDust; + var optDepthOzone: FastFloat = getOzoneDensity(height) * densityFacOzone; + + // 10 is the maximum density, so we divide by it to be able to use normalized values + textureData.set(i * 4 + 0, Std.int(optDepthRayleigh * 255 / 10)); + textureData.set(i * 4 + 1, Std.int(optDepthMie * 255 / 10)); + textureData.set(i * 4 + 2, Std.int(optDepthOzone * 255 / 10)); + textureData.set(i * 4 + 3, 255); // Unused + } + optDepthLUT.unlock(); + } +} From ef8fb21536353120495d47115b305a54002e698f Mon Sep 17 00:00:00 2001 From: Alexander Kleemann Date: Thu, 18 Mar 2021 18:49:30 +0100 Subject: [PATCH 49/63] Update lightmapper to Blender 2.9+ Finalized update to support Blender 2.9+ as well as new features, fixes and more stability --- blender/arm/lightmapper/__init__.py | 2 +- .../arm/lightmapper/assets/TLM_Overlay.png | Bin 0 -> 22962 bytes blender/arm/lightmapper/assets/tlm_data.blend | Bin 126952 -> 126639 bytes blender/arm/lightmapper/keymap/__init__.py | 7 - blender/arm/lightmapper/keymap/keymap.py | 2 + blender/arm/lightmapper/operators/__init__.py | 5 +- .../arm/lightmapper/operators/imagetools.py | 172 +++++- .../lightmapper/operators/installopencv.py | 5 +- blender/arm/lightmapper/operators/tlm.py | 436 +++++++++++-- blender/arm/lightmapper/panels/__init__.py | 0 blender/arm/lightmapper/panels/image.py | 66 ++ blender/arm/lightmapper/panels/light.py | 17 + blender/arm/lightmapper/panels/object.py | 118 ++++ blender/arm/lightmapper/panels/scene.py | 582 ++++++++++++++++++ blender/arm/lightmapper/panels/world.py | 17 + .../arm/lightmapper/preferences/__init__.py | 16 + .../preferences/addon_preferences.py | 75 +++ .../arm/lightmapper/properties/__init__.py | 14 +- blender/arm/lightmapper/properties/atlas.py | 34 +- blender/arm/lightmapper/properties/image.py | 26 +- blender/arm/lightmapper/properties/object.py | 17 +- .../lightmapper/properties/renderer/cycles.py | 30 +- .../properties/renderer/octanerender.py | 10 + blender/arm/lightmapper/properties/scene.py | 105 +++- blender/arm/lightmapper/utility/build.py | 415 +++++++++---- .../arm/lightmapper/utility/cycles/cache.py | 95 ++- .../lightmapper/utility/cycles/lightmap.py | 51 +- .../arm/lightmapper/utility/cycles/nodes.py | 113 +++- .../arm/lightmapper/utility/cycles/prepare.py | 385 +++++++----- .../utility/denoiser/integrated.py | 2 +- blender/arm/lightmapper/utility/encoding.py | 237 +++++-- .../lightmapper/utility/filtering/opencv.py | 2 +- .../arm/lightmapper/utility/gui/Viewport.py | 67 ++ .../arm/lightmapper/utility/luxcore/setup.py | 259 ++++++++ .../lightmapper/utility/octane/configure.py | 243 ++++++++ .../lightmapper/utility/octane/lightmap2.py | 71 +++ blender/arm/lightmapper/utility/pack.py | 14 +- blender/arm/lightmapper/utility/utility.py | 146 +++-- blender/arm/props_bake.py | 6 +- blender/arm/props_ui.py | 446 +------------- 40 files changed, 3348 insertions(+), 960 deletions(-) create mode 100644 blender/arm/lightmapper/assets/TLM_Overlay.png create mode 100644 blender/arm/lightmapper/panels/__init__.py create mode 100644 blender/arm/lightmapper/panels/image.py create mode 100644 blender/arm/lightmapper/panels/light.py create mode 100644 blender/arm/lightmapper/panels/object.py create mode 100644 blender/arm/lightmapper/panels/scene.py create mode 100644 blender/arm/lightmapper/panels/world.py create mode 100644 blender/arm/lightmapper/preferences/__init__.py create mode 100644 blender/arm/lightmapper/preferences/addon_preferences.py create mode 100644 blender/arm/lightmapper/utility/gui/Viewport.py create mode 100644 blender/arm/lightmapper/utility/luxcore/setup.py create mode 100644 blender/arm/lightmapper/utility/octane/configure.py create mode 100644 blender/arm/lightmapper/utility/octane/lightmap2.py diff --git a/blender/arm/lightmapper/__init__.py b/blender/arm/lightmapper/__init__.py index eaf15cec..8df717b4 100644 --- a/blender/arm/lightmapper/__init__.py +++ b/blender/arm/lightmapper/__init__.py @@ -1 +1 @@ -__all__ = ('Operators', 'Properties', 'Utility', 'Keymap') \ No newline at end of file +__all__ = ('Operators', 'Panels', 'Properties', 'Preferences', 'Utility', 'Keymap') \ No newline at end of file diff --git a/blender/arm/lightmapper/assets/TLM_Overlay.png b/blender/arm/lightmapper/assets/TLM_Overlay.png new file mode 100644 index 0000000000000000000000000000000000000000..50bef87f4166b81ee1d05a9799ffe32b4904144e GIT binary patch literal 22962 zcmd42by!?q&nP;$yHngL?(R_Bix!8$-QBIYyK8YT?(Po7ix+pdJN>=yC+D8;+TIPf+P|=K0E*bK$4acR{;RP;y}MgV8KEErQJxH001a)i?5o_ zn(}hI#&$N0h9-7KKt^{Pdk`D|;1_bYH#D{aI+GXy%`9vM$S&Hu$w(|r1jsZv z4~nyu0GXz|5{a0dBanoHk%N($Oc0)g-_gXBS4I5GzXyY^1jx*to$Yye@Xm{hB(m4*wMn?*}~42L1o3a7{x2;2#{-Akk9qwA_{*fV%U>-~v$X(;h4r5& z{{zzeFPH!s7b{3V|LV>^Ag%uq1X?&=d60&i7&;q@8#;sL%F4{j!obYNz{35Ng`1b1 zo0o-|j+uq`->mhb@E{a33=_(Ayc^1Mm#V7*9sYIouS;u- ze{}q>mjA^!H2%w-0GYj`or#Mv(BvO3{@DY6p-y(D&TfW|KoK+0A`6g-n3`IEdjH~$g@KjjD=RB6D;F;t+rN2Z`X6WWkKX+sXY()U z|A*P|{MT&$HRiw1=6{m(zh}eG^w$dfhp70O{x@O%3*o<+4Gv+CDIRk1feBW= zt#?7q>5McSkpwax2f8Wcj*OqS! zAt)Pq)n$j93uDk8)?iI3)C?n5Q;_;O!wQGM%HC0L*65pk8#1Ar<=04GSxyHlGO%gM z8Fm<()R|hP^Ob4Aux3=PM_P5A?(R=xE&;TrDRFKE>$2hlJl%6zCf*eweE zITF%tia1eeQ4%;fN>Wry*iIbd9s_G@>ve|F>@z6~GSgY?qh)vcugF)=X;WWq5I0B6u&V*x@6=(;cO|F>Tx1Q%cg#O}g#zKlu{ zfq*Q<-~OO^vooJTB0!1!M6s9tBF+FG3=11hEA9WoDkWy>3YY>;p~B%bq;CsT zV8J5?;MI3;*md*cNWoUP{R0|r#5M_uAbfmA`+U@y^3>4eR#!s#{lh(@z0;gUe!T~T zvBR1jbsqc}%Wj(0ObUF{KFiJ$%q8U-*Hoi@SfjQz=jG+qPE!b^q@yRndf$US695nJ z-#WtlZlKi=)!qrmo&7%Uj7Eovl{G<}FM%c*8q3>FM<;2d#uv^1}%-u7}UksmYVV1f^eU9f(^jD2wU@%z&%-&2R4 zR!UGH6ai`vyPf)_BL=G!At51>oX=l^lky&0-Fi~8v%f-E4(>c@i-S^ywc;nKnK;zd zn8U0JpvwyjF=ERYHN#aTW1B1$Hjy+eF81!+xsAic5@`qbS+TLxQYKQ{J*f?nK-^@c z?F5E|2Ta8T68n9ME*N5rD0uW1Btvv7Q^N&gp30qs0E=e41|dpj)4pwx|FJu(&8}rW z8AE7tyY}Q~UR4SCVQW8CpblL3ZWlP7i;H9~lK zheOI<294#2>@hc=yH~wN014BQ(!obq000?GZFcLIk?{lHk6FB;n3yTew2Z)zq-V=x zwyCW+$77jjUCrFiyXx-QB$GU4fvP?KEpERQ?n2GF6UGY=;eaw)&4~+`fr4B=MX%KJ zQa#(36~YcX|6kU#Q`@$&2CU!IvwENj{M%s4Vy5a$;o-627rY*&4zLctVXIl3_5oXW zFFs5Id@q)=!J~7K(w!h{E5Cg&fQ=$-LOc2M$J`j6>Qa?}QS6K7EK^rar|bjyblbhR zEh?K2jOAtEpF`5mS&o<8pvPOVb$NN6wC2&W`U#oUul+q1$z7`()ZnOsK7f~cTdpkB>UmhO{9|Hd2=&lmls2w zRyyCcYyOUNnv85^DSflheTSyH^DXqdgoq4QDc{U0a&Y()&rlzK9rspTJTm0|rmwzO zWOtcb^$8pH!Q28S=A4SVu&7M>)`gup8tYubGCddeA)Re( zceV8KA25IceWS`3ahW$HhB<%209;x4kXuZmI3?7*oGxLrX3aP=-LF7;I?*7J<3HDg z7T1eQ5P5ZtsHmY`L*Y_Vc~z=J_PCJ<^f#uTWD&v#3x2_POXB-*+(q;Q=^4nV$ce2E zhy!~_XI7_li0YW9*&8OlYD*;~wl1Goncs6Yx?3GvB*@c`KutA`V5^LQgKfoAQ9YFn zT4pFmg=ev_L+U%mwmN>f&PWVy#B6Pz{-!#uL!^}UAVrQMl|^TyUZR;+TE_{G4>`Dh zzsvzaVwRAWOMPI<;%8ueA#B9D2q}Bs{3#OQ#_qZ8p3$b;uSeBs zr*G42H9tSL!OBXv*c*~$SvVq{T9Qk0EUx{@XV!Bbx|cE6 zuL;G}Lp?q7iaVdLKlwBviF$qp_!o>QxJ@lHHPdJHL#SM^)wDbiv6_^;l%JSuJFawg zkF~fLlCt^yvGwh?I@~;1yC8Hw5mS|rHXoEn#eF!f;nYw|+ISgXU{gof*2NsuSev<8 zmgKwmXpT!a9T|O#xdfM_!y<0?JruQy5$yMa~ zI94on+YI~?PqlVhV_V zQ>%pPJsHjRe)}ndV9{O>93;r$x7hS?x>kxHVbIW*S>&~Z5CFfU5p*ED{mpeqXaWEE zoki?pI(OQzEd3D3oKkx5IlT5~ zJ3DnLGpmFKJXF6;p){iwvLpsP1anA$F5= zmq^MmOHO?|N-aBCUefU`T-dN0k=m7c-9o_(7WTq%YBgQ|$jq|SLAxRlQ~z>Md}yNQ zsc>@fzWRF89&(B(X8PsSg7(H;+Rzr{Qu2FA9#&p*yu|SApFep9H|~|Y<*+BGMserL z*00Z}dW@zbwQ2!K4)$-m?}<`@QG1R2fx>2rW6!GBT~6|~M-|nHFQ@b3wIy-0ge)^l zhS^*~hoAIkH_Pt0{M4bd7A)a(`zhMEY=8igFKT3iXPitKc6vXB&jsHDX0qypCEd0a zKbrU4xMG#)4(^~$JHgmO`&$m%zxG7Z&0t+0^}L1X0{&z>e9F8TVQ3vS9Z zQ8xMkVKtB@zcRKFiR-oMY-V{ zJLpsfNwe2oa5#}bh|FeFJy+YyaI=fvb80>3i}ecbtor`wB&AHsw?HH)p;yyONK%2O zM-47D{d+=?Ryt4RaYhcSX0~5dgfs{5WO+9sW&6D}H~-Wdk(us#MxXAowB-D9GW7-q z*&o&YnvoU_AbGfd5c~*r8ZV7lBQK&T&hBJCEZ?AB<0t(BdvYVGSE>&gKiiesb)1n3}@7puR zE?zs&1|)ie?wf^9JzhOlwi_AKjK6jL>U;q}o%EE%s!Z!Z0EQAoZ%!Q#m!`#Kv$6>o z$3-n2=OQS3OT3nSmtzD(zj&VNsfL!3X+lEC+l~ey6Hny(H|C zeKLu{R?|*jJ_{ZXrny{mNtz1K#(y7lb;^2$z` zo+fY33^)u}ifJR>IDIuAKE)w|ca;{h^H~j5J-e`G!M0k>_?;lvtHX_(ymB{PdSdt5 zx^u6T84Uw25G%-py@x*@QGn+Oex|!}sB84C4FK@W@vPg1X@=3}n=z;O67SJLh$58Y zdSH>P_)cm5fv%EJU-OjPztwGfuSf*1u#%PQ?Yeg&bag5a9w>hbi-|O=2Zg>8G)^PY8hvCM|YNZAM0QOigWN9#`vDKf4=KlP+BM#08 zA6)pj<{Pj^N4SYff6AFgDc1$L_2%w7!&f@KjgoZWPYfLB%p8bCnQrp+9PXy{&w+y? zM1PnbXPX-z>}|F-9{Ms%BIe?|N26XIh$1#Llqq%tS3-Bd%Zjrtrx3W6VZTmHTfA=9 z?7xGx`t@jPbN31PQ>_iWW}V~YHoSw*fgcDw^Heo@OAkAl_~Tl#gc}y)3i8@bn1HtT zJL|h9u8eD0x&2}tWkCEmJRp{(iA605(hO2JR`8q8Vsq3=NXnCwXuDlFVCG|qZ!vXK z>27Ja_ofQL^f1f8J%(GeWUQNWdtk2$6pnGg{|YdQ}~?j?zSrc9S&vk|Wg^%i&^+;D8Yd6gAf9T@w; z6VX2pe(?PC{`|0Jk-&LFL%Oo8gYNCpwER~4w(a-Jlu0;NLm5tg2isbtZ11%`~xX_@ji`5buG0D*iO^R6{nF5>5`nGhl zAnWL7o}Jldn5~&K-66=}CY;D}Q-K5UZ*J?_e(7f4$HZfu7dt4zN7&NAB2{!Mhd0XWW!em`dz(v zjLq4p)ELfRA9kzU-Wc3xzUn2o3_iV=s54}M1D2o~=9swYoKwSy>BSVNZUzxmCBCi`o$Ry*8SlqD2z|G~&N7mN>s7&Z@_-*k4>tzw zh7YR?+;Q12(+K9C5f=?_o3U>2p?`idhmiW;-6o>_{_}|K%hFPyQ~?~ov3tbR8IgNf zTtMk)r*!Z6s%-h0PDRI-VR^dkb^X?98A5p6=3(T`Rm?Ue<&8V}H?;b&{yXK$3u3Dl zns*7TN@CMPx<(L#m_muoiYVZZ_oei7SJ2RP!tAFEodo%i;a>tP?EzQ4n$~@5;!?0%^B~D?p59suA)DfS=xc>pMljWFM6lt=~S=YL`f&y@p*cD$GGH$c2tgYCRWyGr%9r#N zjQHT;%&ERj+KgT9h5ImdNZUxy`l5$VlDRy1RGb9Li`in!%f}pe(Yo9hWwrI`@Q^$2 zA8uVPa)RRFg7b7{+YyqI|ZOXVx5O0q5!x_5BTG`C$yP?&mu&9 zjRJSLV0yR6Q4S-wPZ=>zxckgc0%t)xG#58q-`u8f-hd(YwP9m{d>bm#!$&+X%Up#dO6NO)H<4lM4(m6Vw}$F zRhE)1=CoSCOPMClX)C-aZvMW4={2FFK2VwUp1P%6lkTt$?f*x}xbTnHwmb_t-;nj! z5jiKDt+NUnSWbqU&&K*nc=j1%vT|$@LGr^Tv1vj8e4XYJ#P1B6Qe`zi1ws3XJ|AD_ zYWrRI#O}jrWFpETCN6pV%{kWXM4=b=jg2sl*8eEDb>d7q$=i3q@@kQ*OkPX@+B}EO zLAt)XZY56O=U`sMNuqAVJYhc_x-S)hdmMI$)^r1FjQKUhsH)I64_Ize$k$V4Nq z8>wmH!sh<_t8i_4#{q@0mcs%K0$^oZPPOu0T__5Un&NH66Z}7^7=MnI>cO}U@R&JgT7Qi&`jHk;+<&MiTu`YAHzkOiYM zG{JUVwDJ%P{R9aOpN={Ez*36=^Uu|yQE4=vUd*s@{^kVaXOlLet(ung40V<8@ISBa zVXJby4)I2A1E&~$Pus5O0(xZD6P&{biVGzkrs@2@qs)hP62#|KCY(&6Gh&>_m0W-Ue7?XKv59g#|U z<>G`#CO|^ z@Fi<`ywi0J4&=YZv^M#@kR3}n)D6etVD%fMNGLZ`Ku0(4Y|HnMt7Wd{O^5EX4tm=? z0YKg?jobaB>nSDb+x#dGOl?kU3e{TUr=S?RAIdpjcI|8n)yE$vC?^ihh-yM>^$@q3 zlb=s<{XVQ7-3v?>b|MOft^oe5zZZyd{R>1KJQk*-;^IT`2NaGQ#(i9kqPj%_3TPgX zez>~bWWkdPgh->^^J0~bf7L4C^pV6bo8+xR)QtNPD8W247vE^0JhjX?zgf25>}TpL zw3y}+OTv8MdEPUMo^Nk1z4_*Y2QcZ812%K|zI|wTx#AqsWwsH$ zmX>uSSw*QG=!{4q5R@loDHwg{{ccQDLxLOCFrMuDVv?Rk4sIN=hGBkm30kD8YLLl$ z%G7?HJPY{gTGn7jz|)Zi%4G@Q%D3#gx_NF4>EF-67cCQiOZ3pHw{l^VxCzq;oitFiFe8{xk0qWL~o$=n7p1nFPu2! z!rUv*!OQ7{DULaRH0`C81Rp|2M>n^Y)@oqgEr?#_b2d^m3 zN+#h#xaqJnR&`p7*aauf2~eB$bL{<(lh_kyEQS>5fczF+MT0+3;Js#XQ%OC6bK@AU zEM5pEfrchsFE2&tbV+{moHoqo+j}jmlOTOty9aBJ{Q9ROSI5wt6(4$Dg4Q;6$Jv7A z2Nc{?ZBl?zHU%j|GSy1jmZEVNL93s?Z2gi6t~95gtXE#cw~0}3z<7sNp`UvCyiGYh z+cZ2+*R$oPp+o(IKNNWeEL4115<;KEdPSOt&puW|jx@Zr-XGaqe<-{gWY#z1b5nb) zppsAWkWL)k+pcB1PI+?3xlx3}o24XTf2Vwk=*J>|wKpP#1#F5n4Oou0UH47UO*J#5 z*9b%!>R_gZJ-zO_DIs?D8*Ahi8s!>B;Yx6rYdLtNEptI%Q3gX%0B5)&N`C#A?Ar-p zXQ_#GstOdfQP;QW+zmQQ$FYM1s7k;!X}9$C?P3EO07XLR=OZhZJ#uAuc&B8 z6VlOPR=VSmFG5M`Erc7es_ z!#E0QbIvOo<+6xz;K(HQp?aI1&qWI?wn}-6Ko|Ml(P1T1`ytK{+z9(L78GX528_`y z)KH}W>@HeM_`?AKRD{u=!YK$a#?kPMXi``m<*$ASMy%+GES3``V`)npkRHOW7ppj! zl|@*x^pWMD1PhXuA#FY-t%v*waA9laRNyN$!2>(#a1bO0eKpNGG0o#ubCa6`Z#HA< zxCyI6X(r!#58i5JsFNa>wIuKwZf0fohayQJ)V$VeoOH+fp#XlS^qg*9m3mv)3#^&OdiTuRaWK&1tp z!kvAt(?4tKBp_Lo{VdKW(Ci|Y??rX4TFw=vNH8ER2Dx4~#wV4zG0Igh7Z*2#@-qyu zIQ!VtWlk_klXv=g>Bs>qrWAc&uFJjg>ryW%S1QxuvUE8m)Fi=nKR@~Fh+@{=_|7;~ zJbwpYN_qK%1b$nwX?I~0-_hk?yt~uQ`@SzSceRJYF2lJW`|s8@-DUE}M+o@KsLCl> zp+%LiA1}|pG$Ip?g$&n&#}UZD9cOqU;AQ52O@0`QA$>+XhsYR%PZ!GjOn&oX;TfTc zD|5$j`K~^=j~&u+bTAM2-X#0Oy<>nTrYxsn!}h_wv+vI&eqaxe-Gxz8v^h)Uhga2I z>&v9uMpU|mwI730MW`* z-NN5e-;WJ?fgx533{~0jA|Y0atR`kZ(y90K+Fh=6Sh)Cp-JY%r+2*#0QyU7lOO;(n z8Q;64rXPMaKkehbI6wNHr!IygA-t!6!w1T2lwA02JYPc$5$mBSa8+1<3YAi9b3S|z z&>3q+$+ZveP=&Mg-tlv0#JNn5?!t(TE~_c@H1BsPi^fg*FCx1(-Pn=W3{GZn=|?p9 zF$vqe0dUY1w0euMK(YevPzKH+Wz>a0;fUq&j;`+`k%UBF&9#(()ii2B*y+=$pseF% z-MZOBv#YthH}Ad_YOiS0%c+r`>(!Z1l;1UpkB^UF?sVRMg9NRcS5ki%%c$w|+2j(= zIgWzBceUjkUl{3nsWaWK>=zpGW*t;;z>tI*#VGW!eS_Bd+4S=m)UjFfSuBdn>u!7z zPoxp>Ix|$M&)c_HXm3R}v1-6{HfJu9?Mu`b4$3+sHyVXz*P3t^M7EU!cOlHLQc(q@ zLXr-_e`}AET^FEOrlw5~r&1aTeaFsX@}rll@gp~_;P&=5ykMkhI*WF{^R2QZlu_5f z=iT>4pc#C(GC!Fv;wE~MfMm!>CgpX>g-$-QBI$jk$u)*IqTixABi&<#Cm-x~h40*N zB25>QfgcJ0aGyzYvHrp>Eljaa9M>jV;bO!NPpf?M8XYQ&F8>qmD4&66MKG_W(IYqp zjg(`>%bey1Gw8h3+hTigj~jVi6)9>Fue&B>Ms(5mZ5bs2mg$r7@j07@`kv3>=du7$*XeR)BK{@}=>ComY5o?>8YGs|!OZk2o- z9MB2jI$!CSr5x4!W0~bGh!!kYpGQZ&e5|U5-=yoF_d?RHO@EC(e3qOPR}wJ3y3frS zbpB{aR0Hg_ZaS)}mJ|u^MBl5IV<>4!=PV_X`ffy=@ib?Wc~8lfYLa0>SZ~Oal{bQM z6&VZ;B&xyxN^@`sOFOEQkTz^Z4_Bs^*E#-nbU0r$;v`Ph`3<61*s0y-Y9|~jtZ>$Q zx2l>US93X6W-Rh(?#a2=8ZgD@)w=dc2Yav^l1#}&GJ~0~|M_x*)p6SDqS*T>>c_bh zEGV^q3=9p_IlmhmDR6QAO@I`rMSdyvAH4t`_cQN755g|%16UKgYC!WQn9 zJu?YPnezlUFy?7Owd+=9U>2hFa7ZYhF%fP+2~EOpf=~bstWK{(B7L8R3^EUV)+12K zbYY8YH=nQ3Z*raQ!S!ulxp4Bh`vDC=fi>v70&cjE-j7!y<$>n@R@ne!qBU=(faqX9{*FgfnSAC-fjdbv1Tw3JU78&8Z|~Hh=8IP`=vgdAoDo6QK{^S==YmENg$Waf zv6`GdV`2=ZcA8Z%#?k>xTFhilUUF=*ix4?JVbls&l8{(bRhn}T*O@~JMom)ke)2#IPOOA&^dULRDo4F7JAi zfxA%&iE2lfO_NRs92?C4H2SU?S2!9qh|3aDE(><^oVDgAuOo*jUk|GD5)xY@a7Gu0 zAijcf2C5$BGW+Y-N9;PYVQj5Xkq#@{G_NaeO%J&4X0jjScIl=m$jMKQ+ENmO1b^a&=>nZA3Gfg?bP z$*GuNvf?YYJOuHH>GWR#@-j0tV*);#KBT?%6O5n=+z&Whqn=l=j121>T_S5xSqf5{ z?}traJvs~>*7+*2HIy!1aO1*nCe^&E`A)0uI6)sIP3Ck^+T@DRVx-01c&v=B@xij| zOi@&3Y-C@Ld5TIPwU!^xeF3V8(iWsNGL1^u;$LwSfjsx+r~tVe(*otJKWVs@oE2a20L&Um zb^b!DNdX=m3zm}|uj_Dxb0%M;r0zx{>&uI=<0us^Xe?WQ>tH^q zz=4WwaXojrST`OS%}{DiMiU7+h+wTV9~sV0+ewniyp~hxH6mhS!9%bk47w{nAy(vb z^QOYyvwVyFj3^F~t_BS@`m)QXtKX%^qagysj*D)5VDjrYL)tll?^M*>h?kiKCtvB73feiN}W5xulJ<+w4=xfDqw4hCvxwd-l>z`JtaU_q$4m)6O-Fl|igzbGmsh{!j9(mB((D72y z8D`lT7VD0(=y^{Bsv#V9-G1GSUjE^e>HF?Acs8r2dLD^N^p&QF0>(P z;TzS)iKM7^bTr#}tGt(}gh$V;>$F%&0_z#+V|+E4u>zH*+$QnXs5alE-y>}}^yUgg zzYReaOZS#tjItdvsKlktgmGQpB9zxzJ)O3_u9+oN`?R~nck#C{6FB_&{VpttZTXh} zDzK&+E?yHytvI(?@Mg{_aEcFU>!WyBkL{-ssw!+Xv#~|~;SGdj^~co&^*I8OoL+&T!2nkE;}fwoUz6+*sh zz$EIVm32SeBrt@=#dQsPV!U&E?2#H}?zN$PKgV}`Jzc*sCcGeTQpPOh`Qi=dq9OC* z1M9%#YAWZlEjXM;8hw|5rF^`7=PtZhVrY!@;$A@1_Z|mdMUH#7q<$x5?V@QaH@j_n z;Wwb6np@~3;s#Vt5=Q)3TJ*cX>HP3nH2Hkp>|);8J2Qtql<4fd7r}j=8XZfXIB?16 z(B`DYIbC(?d+lM=kR)U;c}Y^2HrM6s*~lm_)Y))$-koly7G=}*3RMXflngHxrexnm75>q>ZfaDfX~C9VSFx zRQ$uTm|JJ@^-_ zHbzqxGWAx&M0fs9CuW-vi}OpAlmzP>4~@G5p(3!*+PrdDls7^ia2znHOFB=To2B}k zlb?wteP(`;fFGV3RBewT(4)MiJltM^l|J7kn(%Odn)PX=W4gex=H@zbFRXZ9w2d0% z#Os?4gRmf5r_b$dWrZ##CdQ{^_1f3tvf%*hCr)R-*lLdo&m})j>}=Vls{!H%&Umts zpiGRIi~x*P=L>RtxGUaNx{!e`|FWhIAcAa-uv$^*FvjTp-l%6+id;NR-lCC(~C| zFhEa}&BJCQ6SGlXo`sW{iahd-CEJ3PjM;-cTf1=gq5R!5=MH+Dq}#c z<3INt><|TYMRD4b_-E=;vqTO}P6z2D{3?*Q_x zj@B3bX@%*YJ2W}6-UQyZLFy2_nE$%jR`b2l;CUYH{6;AlRs3wW z)5`Wl*FE}~z-dkQ^2c*~iiaA8T0^E2b3_sMmfMLyQKM9ceUM`dHjRY1mNJDjIuz-B zv^+>l1BlvLQL`Wbb=sj-*Un85@|W^#v13;5ubW&%alyBkPHMo~BRlK(_80RpcKIE_ zKxq?aNfF${?``|{FTsts->cGwajCws3?95qG41KTh}Q7|0I1>FDX_}%z?+1povd^@ zF?9eTA=b(^YYJwD84}gmT9f{9gW?UUXtFegCLqsea_jB!27=fQ1L1~holU|pEphM# z#+z%4P);2p3uC~4i)q-|LqkWw*fwT=kJ8a>x^DEknvEmw1x7_dx6C#P#rhc*s@yL) zP>!*84i-hKs{1W6E!ijP;H+NGG)3mZ8jk;wot7SQD3I~O0X!d160p0j<-N-HaukK- zv^en_f^qzPh?A5@T7MNrTB`Th`;qqH-N$IN4xJu!^72^e5_%OAbcelh@^=Vf0mVQk z8Ys#K>z=DJ#vX97Igb*O)C5FvhkzTJ&8KhObP-&@Wq z#4+Zmtg?yRe#vrc(A zRc7-;FW}zTh<#h;FMQRkP(w|GgT2~%0$d#L`nXCcdrp`(%of%DNT5uckWtKy%PmAm zQP4@FfTJBPGs{M|mdO@~VaT<7!paqtBO6v`$1S>77~KE<>=g*xGZ?62#7UUh@&pSD zesOmA24R4Ts$>Xn#vf`0rGYD757(+SENSS14jcF$s=iiG-b&2>iGK#95ltGtTRI%7 zxrSwO1$fx06r|+xpjsU-#x?-=Gp>_$74;m~ z$D-SJpf=7O{v^fUH5|}9DH&}jn8R`7)kmZdKW-=#E}Mjh~6C_r0p(+NtCN~b}oU@QJzQ!2Bf%d#eb;=y;AeE?Av*&*sY{!-u;kEPm)hVG|5i! zJ(f`u)Z9xj^jbwY*!?2C8tU>^)79?{b~B-J0w)Gl;7R0{f6-loMI0HfgFA_v`p}~uCkwdLIU8( zu_?YS_EZ9gQNN8h=PTAfoEKD~WH_sb>Wva|gIZS%*d1R07w6jygr^~H?vi#3`MOR- z*atAgbWEL-LN(u6F)@AbtMXGXPr9K9E$#Jq1pG`oGK0!)*mh6AT>dNJxzrfQ*e@g(nzc|*tm>$DE;(& z+R>c5%zk}b625V?%-Hj~F4c+G%$Hx2`*43Fu-GAREhOT9x$;;2o{MRQ1 zDp?$H!Rg)YH;4{Df3D`6?69Hq1&O>C1FCcZ2wC{n7KOe04Fm8Q|(hH zcUc&rIo=#JD(=KDwWy=7D`N6NO96(S<*V_{uHxgTVGT0Db~-U39{tEA#Rl5&UsLAQ z#*?_X=sr9!UeVymjXY?}5Y?JYh<)&PgZNJ=5jQA#cCNS|U&~)erf7(Kpt3Zk+%zPK zhOQ+Fr_I(}2l1cAFupkVdhC1K*)-nNF)NNo8o^j4;cV`-BgdZVDa(w>?mDYu-XGcS zciUYL#l#n8^C3hTt+&H$FG_}L^b zDsr;Def+F;DbH9~)g4mh76byy@CDW@Qzw~XNXi4Zm@3Kr6{0?@!eA6)au2TlHYoUO3T1(KkmXV=`Z{0o3jqaAHtCPR#U;`Y z-#xf^rhWVRAP(mbpJt;U_gs9^qVc90S?e(LUEW?vi{({cMfCui*KzjmIeS?_eGr zd$}7JuidqZf?TiHF9s|S0Np0JLWAw!h_WjkYE8z+kkV;bGmm+Pxh%I{i)JVqbXfO0 zUe=)I|B$TK=U>rwl_b|eLD+bSXcc5~^|j?>kN(RXmWV3kkq6MkwHcmWW+Tjs>hwep zQcv4oo{F2pz9i?em7Jeu+gJ}DTB@qW!I||i2wk7f_}s7~W#%+#zom^Tc2~YfTn-`c zH+1I9V(lAC7x{$F{&<07F^ic`q-D^=8#Os1YZ?@Mq`3A)I<&q~Z;}|3rLv2L6a7^k z#BAP483Z*Mmd$GZ#cK;oaF6wbtZ!n7iCCW!ImX$eg_{hY-ZG|g6+mr7 zq@WBAI1&eDNynQ*BiqW)&aoXu4;%6=uQV&66nKt?@-!RW=Ed%G&uQWoaJ`6{q{wTV% z99DL=WT3t0uA8(hA-_J}RpH13;@RF#l+jG!XH7w$P>uvzf&S098Pg5VU;g}*6qskC z_4ADCeC{n$r-Ln)Qz<1f`64}YUnGh>4Y%skGCmpGXiggyDtiD$e3{~E?+NhNK*_j- zC8Zng{*1V+x;ZDfbY5g_?i9yG#FtC%xGF97aCRPDlg!z0Y}}hYh1NE~_p&%_-eis+ zfQn9keg{UXsUxZh|UH7rjfu-w7JW?M}YO_%#J}nSkEhHh3*MYGzCu9xK zGE~TcC^RjM5)lTU1-*PDU#rw)NIP-ao>|*y{)eYV@qM|$dft^`IXywnqIYaciTGwV zx5z zT21x1yD`GrPcCgYmoPWT(;Em!xn8o;Y6@8xpGnvN3cA(M^a@QI0k6A$Tn*Ux^m4(d zN0!|HKm;`I3!?1f(0*D5>nUx6^y835QUU+IK9gnJLXxozg8EXNLBIyfDNn$hRY4n6 zy~gIJErq9Gj4T$HH8mJI0_R>BfRv(&K<%%t@AyxXYwS=Y~}iMoH2=>Cff^LYlzSYSpowgvdLJ-hob zpBi;yCs58H>Yoqnm4COR^pww}U~BOT{WKT^Vbe}`a~u?Fp@!6Lyd;_F1y%nUk_6a$ zxr;k>adp-4lXaA-b$x6Zhc6(+S|W<0FJ0zFw?sNSX^zAXe|ns?6JsKd*YiN$!2OIT z#`KHiB#>kAPjmloPnbCsn!q7?6h%V}9LUDs)V3xrE>l5fRD6igXG*&*pCD zf26OlnPKOk8NqkNsYUty-6<@`7|~s7-{G8I4TF65&S5cP0N0H>-_QSHWre!EspYq0 zn3eg5o_dRq{{xD!BJtWSoL%I_G{c7D@?%N=SjyRHzoHmYkip}oOMaQW=Vv?ONa9wu zi9k-?J~T9&pEJg>)Y=`M+}6v26JtFS&QGn&VFMXX`&Swx5tL~;@PMKMOoRrbiEpK! zRkdo}KLE(*s)nof-vGv9vlP7;W3qw7A>7;>9UNn&d$^u-EoKj(^_+cMA9;tP~r)Cs@_%rt?JfD($&8ejKVidHbFsK5eDM1V9zGv~kW@UwCf7>c}^ zhf~)2Gqok(U7X7fW2glb3VKmE>c#TYX{o5=qpysa#Ry;eAjge}u1;$w)MM&k)y4ZX zl6J>`CI6P&$HRWHhtcU^xh z9RGN*y<>eJxlKGSa6SIM>y}#qMftnt2ak<;e$S`1A^WDXzOOlkBDtzh<}?dAl>%_| zg>r}he;NYG)3wi`s z#@9qwXqh=etY@&Hr+K#cS`5vaqDD5c&Tmnle27Fm(N|iuACI^O{a#(2YuVvs`Nypa zdFK@jOBxNpY7e*6&1TJ;V1?QZ_xe!d^?r(2#Vr^0ZErK=Z)w*5h`_{xB)-YUN7MwT zbQ0~#=qP1c#TG=7HM)X=$j5+~N^TD0mpIZYHAf6FTyw369X&14%u$#a9XJ3&-b zmS1Ab-)dujlw>4=hePdhNVi`cG%>`|oY?DN>$?b~o*){_|G!GPu6H=RXggYr7NSd{ z1W_`&Aj;^y_Y!S%q6ZO%QG;lMs3C-CLDVQ=^xmWQUPl;=-p2L&2kyhYujhRG<$U{` zv-etSuj#P@{>V#8o22rv@MjgcqSr22o>wA4Gw>PKOfOKlz?@DQi}Qs+G!Slvt1S&7Onh z_{9pk6B3D=6Q8rdL8p5}EKn(uj=juU`w<+7yZhG8uij)^KwIqV_tUSqM^G*|Fw{9N z{Two*=qMXj=O>=JId(EaY(WS}zK|lYe6^lM=)X7c(9X@~Z8z%*2bO8VkmzoY+Aps2 z1L8M*pHIU7@euiJGf7=r##J~*^Q+*+=qn806Y)vJOEC=K+_#^;_E91hoIqYZ+ufbO z91}O64we*slx@2(=tOp=a=(*+xb%(ZUO(Zlqx6##KSYt>5tFfksH=>6D2(3ZhF7?< z*a-0dePv9RFt`1yWCy(D@d|qP<5JVLI#1##++$;k2`H_7Ie_QUjkne^u!w@V2j*{K z0&ZY+&X&6h1lV9G{Zi#)N|U%5EP&l$9TK_ySP!NG1bpYaDvwN_5mq>xJHg+K%&}$+ zVn97gB&%GCl%_k+yM&QQZdqZCu98+d!YbQM!-re@*8lL2WC?Kaw&J%Wuz>6x-l;2( z)}a#bF_+r28VzrSY6t2uSc{Sx^*d&mPN_LmWf0+G1sd#)0VpGg^iFA_ppf~CeMGnD zjkoXZ#3L;R|JkY151U{eN_Hj?9MLB1+O6Q_p~%n4(}sACIzGBCq>}Ii0Lb0`^98WC zSZJ$55sF!$JAEW*v-jvz-^4K%+vCKyFPwGLrk$s?nTkb3EKxc%Mbe9r-x40L;o!h6 z{cge_(?@?Z#7T*p)XZ6G!c$=go4-);;U4(wBNhMv(G2y&CtVq1Z zH1?@X*tA_3)ySkVFN@ks`Pl6+Od?7LJ0g1jnXb1UJci%i{(HyJ)j(Ar;dYAA>_;)F z?b!Hna`LQ7GP*nM_-G6SEz^EW$et0Y(vqv1Uc-n005BEI@QUY}dZQ=(w!M}AE3%Fz z|Bui#^z}`^D5Y;y3<>HhbpDv3^H`cvTDP!>J!YheDNnECQX03*CZoecK+wtFxR;h2 zBP1ravCXkZv~$f{I`F#E3c0(hPW8W`75~pYHVuC|*}fhQ1y$CJi&HrVQuo6+9b{%! zBe6^Bs!fY4>0$I&$>glsSNfBK{~Qhj$T5^W-@^QIx%v@xpBbMUI(>67qc6L|2e_OR ztxoFW0ldbKyRM#OYf~;S55r$Om#{&o`;nkYYUoy&6+s6m#?x@F^5o$0`C2(JjI*zU zLUu0iYR%t*z8Ou(Ji^t1X672`4K+((T^<`=Xu8bqMEn80tJqz=CFG(nOCDPs@pV>y z_!wC*HR2b&C6>I}S?1AppNzBMf96)tk4D^LKBjp9J5JNI z;|s#OPaJiX%{kyDsMDLzjf3vBW9qI`;uT zZcN#mkjHFeM#}+~bd+5FBXq{e6~|k}%HkJZ29>}EKxrbb$SNPbahq3@BC_Hz>OM|k z^KvqYEHOEa7Is9kMzb$mQK|`Y)lCWBv#WXv@(Q z5laab0P?oP1NrB)@nh@mM6ilrCt;-L2guP54VK_IA3Ko;$1ka$CHZOY`V7#kOX9;A zGT_m9`&sf_I32Px*@$fSx;8!S<;vAxn$gdyMmz@D{#4h!x~{ar7Ct;+sQG2EiJV}C z6JVr@BWEZF2Nw(Q#kIq?BE{_wv}{J|nu&0w^(&VNpLf@WosY(?G7hZ*7q@PVsEuZ- z$HYv&>7nS}mHnR?d{pAZ0c5Iz%z}~eTbOs{CBKYE41ptsnzP~jh|zzuG*f&dSw(u_o2(ar-ZJf``MIi(!&W2v|6e(&8wS&K>ltaOKOC#%tt;JHl#C4MQLNlVq6s#_i zMaVgQY@XDNk^Cq-{j&N4o^kA3DD${lYuSjcpVtDg78}rV77&EU|--!?NiOaCf1!Qf$~_*!A?kj&-MQ9o z_7vbCGtk{n*-)Ti0+0A1L1}eLn%`~2`{XtJe_J;gp;MLGbmV;&c*L2%Fo0a(*^en% z;qth^MnX|PodyzR*yh}mcT`XJ*r6uo8xTfT2Zgx>Fxy|=Z{&+oSO9IxD@-p7&&4cJ zW`m6f@*V$|iPtGlC4?b%NbM(`SUbB4IPNH2$z?1z(uHPA*sbXKbgGR+he~(88F>$+ zvP}$dbUnTCCgI293ODPV0|O%w?*o+qmQi@`yfxV@=x|KW@KM~wLptnV`PRBbZxG16 zO!kAaub4jbDQ-IVEX|;I+MGHIm^tQ>i6z=DhR8t5SDCAw`Cn?TYvsAEo^#jAeYm3E@xcqP*!JL0a|j;I zxG5klF`^*z04z^!hy^GtNH_z{l`{4mM9#?x8rE@#lFs~nljEo7w>`{H>Vn-?=6%M$ zfOd)%Op%h}7p2em=!JjOt~#ceXyKcPZVOU;r#EC>KM{>YV1W%JcS#^Dh7r#@!|Ig+r){IpCuwB-t_=iAB^uxafbHqoKQer`)iY zw<`1gx+DRD$IQ!9X?vO03cXB|0#Tjh6+19_AYc-eeEV|iXCWed`*WdOeNp?71ezih z@apM8R@mipGL?&M{|ksCV~wtOf(RzHSYGndyl#ZJ7W8?-DY>$pRErv!cB_LcDdKjT14ObE9QYn&d&HK*+-UK%RJk|Di=BY%> ztF>p%^mmHSbXM{amo$yJ!J9+AfBqn2p~2hS`J~*oN?O(CH-|g*X8@pTyDp_&uja3a z#0`GQh@p%Sy`&J`qzuhESAz@sbp^X0jyA#48*;P!`388djRQZ?HaTNH@CM{T#PW9L)8Z(jT6{RG@z{Th%8^L7vGhsL!{^CQymmA z^nsqq@1#Eok5!OfmIPxB{0-qO)H1YO=1&y@ap1)e9V`4Y^dj|G7MDkRxthYTaYJ+$ zC`wbj;v^A~+yu;ubpB#;6kIs_s|0U%&-Ix_PiDot3qyUT1hG29-|!Kp=E>GOScDt} z4{Z|@j=4mI7UELmb^FLw)=wa<1euBRd*{cHHdx7uU|9*Q6-6<>(-*L#d^pA83T^=;qYVxl(fzWthK@R|!amEaS6nEzdMQaRyZ=_>HOX>O zF3mUa{-%@BZxThNq%4#5cQqzzV{-+cTCL1}d%n+3OBPofAo-y934w`jt-I1-OpL<| zgH+YW%q}RrrK%}fs0WK%*OB1RF9ld~^1fn;ahNE|X*))p3VEBLjH$U|8iW?ZjAoqd z)X~OgG>Z5LjuXELZSn9GQ!j+jd`MIF-wui9ypr2e_gg7;5D!jKlaWH|mh^@BMBzc> z%*(<{)l}I+{Dw4sX}1rwH`?jKb4sS@=E>j%3SjtZ5oS=5V@&v?`Vbt<#b(UF@^n<^ zv*Q>(Fdfp^646gE|DLwhete0Rin#-cR(KmHVzyhmFC;&YsvKQP6tmra4HTiLK6Y=N z7C^A}8DqKM0sdzN!XQwV&@1*=KzEn6frS#qcQi zL&b0pHZ=uTQesKUr=rDOCsTmOync<~>LS-&0!U_qL4{zcnt_-=WwxbL@+&D}OY0;( z*!hPok~Ba_@9^p-JaTMm5K$ikWRIzd92Aza>MnRoN3o%@EKxD+F@S3gTqj`ekln9v zuWvndZWMsq7JkXfurPg5@Ydva9o(+h7eqkK)=M$E$==hPS7>n(%}R%~J$}wxM0+2s zbe*htg4@ml)QqZazJBJb`C!Vn>QEXOjwYe@9Bv;qY#KOK0;y*T)$_~AKQ_sT6hsi; z?P?i0W9j~gzibQHO8sS> zPkiw1;*R((m@KmeEKbBQX&L^;HM#}l`qxBP>D%`#Z>6^Wh{;5&-1Ow&3@2}=a!K|B z{mY1Z>oQEdcgP~QA;@T#wdbV_7p??F~5>bYl1>1HaIvyt{;d$k@UOsQR=NQh| z@vq~z&bsT~nIw(5Y6UG3q_6*Q(W>w*N-)$PUULRQtPxIxK>!asbtzv@rXuT?cALJV z4O*FG4$!Uo{cnM{>LlLcSWooGk%E~~&cy$6hOt5e-i`FI08L;dOb@X}Wsy-jj)BOm% zH%gh;+c%imS@B4J#;h$1*c$-F#zAY)@+mDAqD1lTCbClyYA11g1wx!Z&zKsmR5p8O z=o@IeP(22wqqMHSbHQ<0W^#MO^jh8*%ZSqFP-fxFzX%u7es%VY7JJlOQSWRUu3CW3 zpd{yne>^S!$oZmPWl9V+ek@~=(%=eewO4bjmoU=BE0}DGq->YWymi2!J6YvC&64bw zYu+7oM5L8^^08eHsFh(*Eu1VrbcR8a?ymm$B{_^SFl_EEYhpcOM*$*Qh zBNu2h3J5x}?^@mAttO3D(69*}8I2yirs4vuD!tMVt+(nFh{H*G zC|8=D??(c*_C0|sD(V;VN<|Gm&*t5Z_rzegv80jqo@WJ-+R;AD_$J)*LC~g&kYkpYg`j>;x>ublhGy zVC(&EF1e64(OGJ{H_{r{J>`zR_+tvY6f7u-FB@e4+5g5VbXt(A{xFIt&1B3L19DT93$g7973Tn z9ZzDJu)D>$F!So}e7A^(tFckXw03_KFQ@s2KnnJ?Y_Lf5M8fUu%vhWTB0o8&9O)DJ zG2l>f@Y7MlZT3Wvs4&-DGa&`o!q#(uqk@h|Q1m zj8K)&>kCBdNkufu`nY7cJz+fjCdNQbt9cp!3odhM?NvszYB(3CHi Hg@pbO-W@#% literal 0 HcmV?d00001 diff --git a/blender/arm/lightmapper/assets/tlm_data.blend b/blender/arm/lightmapper/assets/tlm_data.blend index 409c894b33630b82edc77541f1c8f99829627679..8dabd2cbe1259c85907a61bc7a600fb494148cdf 100644 GIT binary patch literal 126639 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 z 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

0jVp<00ydE#4~LtO2xDMxVV6yMOtBr+C9A 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 1 or dim[1] > 1: + + if active_image.TLM_ImageProperties.tlm_image_scale_method == "Nearest": + interp = cv2.INTER_NEAREST + elif active_image.TLM_ImageProperties.tlm_image_scale_method == "Area": + interp = cv2.INTER_AREA + elif active_image.TLM_ImageProperties.tlm_image_scale_method == "Linear": + interp = cv2.INTER_LINEAR + elif active_image.TLM_ImageProperties.tlm_image_scale_method == "Cubic": + interp = cv2.INTER_CUBIC + elif active_image.TLM_ImageProperties.tlm_image_scale_method == "Lanczos": + interp = cv2.INTER_LANCZOS4 + + resized = cv2.resize(basefile, dim, interpolation = interp) + + #resizedFile = os.path.join(dir_path, basename + "_" + str(width) + "_" + str(height) + extension) + resizedFile = os.path.join(dir_path, basename + extension) + + cv2.imwrite(resizedFile, resized) + + active_image.filepath_raw = resizedFile + bpy.ops.image.reload() + + print(newfile) + print(img_path) + + else: + + print("Please save image") + + print("Upscale") + + return {'RUNNING_MODAL'} + +class TLM_ImageSwitchUp(bpy.types.Operator): + bl_idname = "tlm.image_switchup" + bl_label = "Quickswitch Up" + bl_description = "Switches to a cached upscaled image" + bl_options = {'REGISTER', 'UNDO'} + + def invoke(self, context, event): + + for area in bpy.context.screen.areas: + if area.type == "IMAGE_EDITOR": + active_image = area.spaces.active.image + + if active_image.source == "FILE": + img_path = active_image.filepath_raw + filename = os.path.basename(img_path) + + print("Switch up") + + return {'RUNNING_MODAL'} + +class TLM_ImageSwitchDown(bpy.types.Operator): + bl_idname = "tlm.image_switchdown" + bl_label = "Quickswitch Down" + bl_description = "Switches to a cached downscaled image" + bl_options = {'REGISTER', 'UNDO'} + + def invoke(self, context, event): + + for area in bpy.context.screen.areas: + if area.type == "IMAGE_EDITOR": + active_image = area.spaces.active.image + + if active_image.source == "FILE": + img_path = active_image.filepath_raw + filename = os.path.basename(img_path) + + print("Switch Down") return {'RUNNING_MODAL'} \ No newline at end of file diff --git a/blender/arm/lightmapper/operators/installopencv.py b/blender/arm/lightmapper/operators/installopencv.py index b3d173a6..21f5e37f 100644 --- a/blender/arm/lightmapper/operators/installopencv.py +++ b/blender/arm/lightmapper/operators/installopencv.py @@ -21,7 +21,10 @@ class TLM_Install_OpenCV(bpy.types.Operator): print("Module OpenCV") - pythonbinpath = bpy.app.binary_path_python + if (2, 91, 0) > bpy.app.version: + pythonbinpath = bpy.app.binary_path_python + else: + pythonbinpath = sys.executable if platform.system() == "Windows": pythonlibpath = os.path.join(os.path.dirname(os.path.dirname(pythonbinpath)), "lib") diff --git a/blender/arm/lightmapper/operators/tlm.py b/blender/arm/lightmapper/operators/tlm.py index ace239d7..a46f3604 100644 --- a/blender/arm/lightmapper/operators/tlm.py +++ b/blender/arm/lightmapper/operators/tlm.py @@ -1,9 +1,36 @@ -import bpy, os, time, blf, webbrowser, platform +import bpy, os, time, blf, webbrowser, platform, numpy, bmesh import math, subprocess, multiprocessing +from .. utility import utility from .. utility import build from .. utility.cycles import cache from .. network import server +def setObjectLightmapByWeight(minimumRes, maximumRes, objWeight): + + availableResolutions = [32,64,128,256,512,1024,2048,4096,8192] + + minRes = minimumRes + minResIdx = availableResolutions.index(minRes) + maxRes = maximumRes + maxResIdx = availableResolutions.index(maxRes) + + exampleWeight = objWeight + + if minResIdx == maxResIdx: + pass + else: + + increment = 1.0/(maxResIdx-minResIdx) + + assortedRange = [] + + for a in numpy.arange(0.0, 1.0, increment): + assortedRange.append(round(a, 2)) + + assortedRange.append(1.0) + nearestWeight = min(assortedRange, key=lambda x:abs(x - exampleWeight)) + return (availableResolutions[assortedRange.index(nearestWeight) + minResIdx]) + class TLM_BuildLightmaps(bpy.types.Operator): bl_idname = "tlm.build_lightmaps" bl_label = "Build Lightmaps" @@ -52,13 +79,13 @@ class TLM_CleanLightmaps(bpy.types.Operator): for file in os.listdir(dirpath): os.remove(os.path.join(dirpath + "/" + file)) - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: cache.backup_material_restore(obj) - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: cache.backup_material_rename(obj) @@ -75,8 +102,8 @@ class TLM_CleanLightmaps(bpy.types.Operator): if image.name.endswith("_baked"): bpy.data.images.remove(image, do_unlink=True) - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: if obj.TLM_ObjectProperties.tlm_postpack_object: @@ -92,14 +119,17 @@ class TLM_CleanLightmaps(bpy.types.Operator): bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) bpy.context.view_layer.objects.active = obj - #print(x) uv_layers = obj.data.uv_layers + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + for i in range(0, len(uv_layers)): - if uv_layers[i].name == 'UVMap_Lightmap': + if uv_layers[i].name == uv_channel: uv_layers.active_index = i - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Lightmap shift A") break bpy.ops.object.mode_set(mode='EDIT') @@ -111,9 +141,11 @@ class TLM_CleanLightmaps(bpy.types.Operator): bpy.ops.object.mode_set(mode='OBJECT') if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - #print(obj.name + ": Active UV: " + obj.data.uv_layers[obj.data.uv_layers.active_index].name) print("Resized for obj: " + obj.name) + if "Lightmap" in obj: + del obj["Lightmap"] + return {'FINISHED'} class TLM_ExploreLightmaps(bpy.types.Operator): @@ -153,63 +185,285 @@ class TLM_ExploreLightmaps(bpy.types.Operator): return {'FINISHED'} -class TLM_EnableSelection(bpy.types.Operator): - """Enable for selection""" - bl_idname = "tlm.enable_selection" - bl_label = "Enable for selection" - bl_description = "Enable for selection" +class TLM_EnableSet(bpy.types.Operator): + """Enable for set""" + bl_idname = "tlm.enable_set" + bl_label = "Enable for set" + bl_description = "Enable for set" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): scene = context.scene - for obj in bpy.context.selected_objects: - obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = True + weightList = {} #ObjName : [Dimension,Weight] + max = 0 - if scene.TLM_SceneProperties.tlm_override_object_settings: - obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution = scene.TLM_SceneProperties.tlm_mesh_lightmap_resolution - obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode = scene.TLM_SceneProperties.tlm_mesh_lightmap_unwrap_mode - obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin = scene.TLM_SceneProperties.tlm_mesh_unwrap_margin - obj.TLM_ObjectProperties.tlm_postpack_object = scene.TLM_SceneProperties.tlm_postpack_object + if bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Scene": + for obj in bpy.context.scene.objects: + if obj.type == "MESH": - if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": - obj.TLM_ObjectProperties.tlm_atlas_pointer = scene.TLM_SceneProperties.tlm_atlas_pointer + print("Enabling for scene: " + obj.name) + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = True + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode = bpy.context.scene.TLM_SceneProperties.tlm_mesh_lightmap_unwrap_mode + obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin = bpy.context.scene.TLM_SceneProperties.tlm_mesh_unwrap_margin + obj.TLM_ObjectProperties.tlm_postpack_object = bpy.context.scene.TLM_SceneProperties.tlm_postpack_object - obj.TLM_ObjectProperties.tlm_postatlas_pointer = scene.TLM_SceneProperties.tlm_postatlas_pointer + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + obj.TLM_ObjectProperties.tlm_atlas_pointer = bpy.context.scene.TLM_SceneProperties.tlm_atlas_pointer + obj.TLM_ObjectProperties.tlm_postatlas_pointer = bpy.context.scene.TLM_SceneProperties.tlm_postatlas_pointer + + if bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Single": + obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution = scene.TLM_SceneProperties.tlm_mesh_lightmap_resolution + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Dimension": + obj_dimensions = obj.dimensions.x * obj.dimensions.y * obj.dimensions.z + weightList[obj.name] = [obj_dimensions, 0] + if obj_dimensions > max: + max = obj_dimensions + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Surface": + bm = bmesh.new() + bm.from_mesh(obj.data) + area = sum(f.calc_area() for f in bm.faces) + weightList[obj.name] = [area, 0] + if area > max: + max = area + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Volume": + bm = bmesh.new() + bm.from_mesh(obj.data) + volume = float( bm.calc_volume()) + weightList[obj.name] = [volume, 0] + if volume > max: + max = volume + + elif bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Selection": + for obj in bpy.context.selected_objects: + if obj.type == "MESH": + + print("Enabling for selection: " + obj.name) + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = True + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode = bpy.context.scene.TLM_SceneProperties.tlm_mesh_lightmap_unwrap_mode + obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin = bpy.context.scene.TLM_SceneProperties.tlm_mesh_unwrap_margin + obj.TLM_ObjectProperties.tlm_postpack_object = bpy.context.scene.TLM_SceneProperties.tlm_postpack_object + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + obj.TLM_ObjectProperties.tlm_atlas_pointer = bpy.context.scene.TLM_SceneProperties.tlm_atlas_pointer + + obj.TLM_ObjectProperties.tlm_postatlas_pointer = bpy.context.scene.TLM_SceneProperties.tlm_postatlas_pointer + + if bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Single": + obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution = scene.TLM_SceneProperties.tlm_mesh_lightmap_resolution + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Dimension": + obj_dimensions = obj.dimensions.x * obj.dimensions.y * obj.dimensions.z + weightList[obj.name] = [obj_dimensions, 0] + if obj_dimensions > max: + max = obj_dimensions + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Surface": + bm = bmesh.new() + bm.from_mesh(obj.data) + area = sum(f.calc_area() for f in bm.faces) + weightList[obj.name] = [area, 0] + if area > max: + max = area + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Volume": + bm = bmesh.new() + bm.from_mesh(obj.data) + volume = float( bm.calc_volume()) + weightList[obj.name] = [volume, 0] + if volume > max: + max = volume + + else: #Enabled + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + print("Enabling for designated: " + obj.name) + + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = True + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode = bpy.context.scene.TLM_SceneProperties.tlm_mesh_lightmap_unwrap_mode + obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin = bpy.context.scene.TLM_SceneProperties.tlm_mesh_unwrap_margin + obj.TLM_ObjectProperties.tlm_postpack_object = bpy.context.scene.TLM_SceneProperties.tlm_postpack_object + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + obj.TLM_ObjectProperties.tlm_atlas_pointer = bpy.context.scene.TLM_SceneProperties.tlm_atlas_pointer + + obj.TLM_ObjectProperties.tlm_postatlas_pointer = bpy.context.scene.TLM_SceneProperties.tlm_postatlas_pointer + + if bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Single": + obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution = scene.TLM_SceneProperties.tlm_mesh_lightmap_resolution + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Dimension": + obj_dimensions = obj.dimensions.x * obj.dimensions.y * obj.dimensions.z + weightList[obj.name] = [obj_dimensions, 0] + if obj_dimensions > max: + max = obj_dimensions + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Surface": + bm = bmesh.new() + bm.from_mesh(obj.data) + area = sum(f.calc_area() for f in bm.faces) + weightList[obj.name] = [area, 0] + if area > max: + max = area + elif bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight == "Volume": + bm = bmesh.new() + bm.from_mesh(obj.data) + volume = float( bm.calc_volume()) + weightList[obj.name] = [volume, 0] + if volume > max: + max = volume + + if bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Scene": + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + + if bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight != "Single": + for key in weightList: + weightList[obj.name][1] = weightList[obj.name][0] / max + a = setObjectLightmapByWeight(int(bpy.context.scene.TLM_SceneProperties.tlm_resolution_min), int(bpy.context.scene.TLM_SceneProperties.tlm_resolution_max), weightList[obj.name][1]) + print(str(a) + "/" + str(weightList[obj.name][1])) + print("Scale: " + str(weightList[obj.name][0])) + print("Obj: " + obj.name) + obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution = str(a) + + elif bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Selection": + for obj in bpy.context.selected_objects: + if obj.type == "MESH": + + if bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight != "Single": + for key in weightList: + weightList[obj.name][1] = weightList[obj.name][0] / max + a = setObjectLightmapByWeight(int(bpy.context.scene.TLM_SceneProperties.tlm_resolution_min), int(bpy.context.scene.TLM_SceneProperties.tlm_resolution_max), weightList[obj.name][1]) + print(str(a) + "/" + str(weightList[obj.name][1])) + print("Scale: " + str(weightList[obj.name][0])) + print("Obj: " + obj.name) + obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution = str(a) + + + else: #Enabled + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + if bpy.context.scene.TLM_SceneProperties.tlm_resolution_weight != "Single": + for key in weightList: + weightList[obj.name][1] = weightList[obj.name][0] / max + a = setObjectLightmapByWeight(int(bpy.context.scene.TLM_SceneProperties.tlm_resolution_min), int(bpy.context.scene.TLM_SceneProperties.tlm_resolution_max), weightList[obj.name][1]) + print(str(a) + "/" + str(weightList[obj.name][1])) + print("Scale: " + str(weightList[obj.name][0])) + print("Obj: " + obj.name) + print("") + obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution = str(a) + return{'FINISHED'} class TLM_DisableSelection(bpy.types.Operator): - """Disable for selection""" + """Disable for set""" bl_idname = "tlm.disable_selection" - bl_label = "Disable for selection" + bl_label = "Disable for set" bl_description = "Disable for selection" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): - for obj in bpy.context.selected_objects: - obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = False + scene = context.scene + + weightList = {} #ObjName : [Dimension,Weight] + max = 0 + + if bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Scene": + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = False + + elif bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Selection": + for obj in bpy.context.selected_objects: + if obj.type == "MESH": + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = False + + + else: #Enabled + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + obj.TLM_ObjectProperties.tlm_mesh_lightmap_use = False + return{'FINISHED'} class TLM_RemoveLightmapUV(bpy.types.Operator): - """Remove Lightmap UV for selection""" + """Remove Lightmap UV for set""" bl_idname = "tlm.remove_uv_selection" bl_label = "Remove Lightmap UV" - bl_description = "Remove Lightmap UV for selection" + bl_description = "Remove Lightmap UV for set" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): - for obj in bpy.context.selected_objects: - if obj.type == "MESH": - uv_layers = obj.data.uv_layers + if bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Scene": + for obj in bpy.context.scene.objects: + if obj.type == "MESH": - for uvlayer in uv_layers: - if uvlayer.name == "UVMap_Lightmap": - uv_layers.remove(uvlayer) + uv_layers = obj.data.uv_layers + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + for uvlayer in uv_layers: + if uvlayer.name == uv_channel: + uv_layers.remove(uvlayer) + + elif bpy.context.scene.TLM_SceneProperties.tlm_utility_set == "Selection": + for obj in bpy.context.selected_objects: + if obj.type == "MESH": + + uv_layers = obj.data.uv_layers + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + for uvlayer in uv_layers: + if uvlayer.name == uv_channel: + uv_layers.remove(uvlayer) + + else: #Enabled + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + uv_layers = obj.data.uv_layers + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + for uvlayer in uv_layers: + if uvlayer.name == uv_channel: + uv_layers.remove(uvlayer) return{'FINISHED'} @@ -222,8 +476,8 @@ class TLM_SelectLightmapped(bpy.types.Operator): def execute(self, context): - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: obj.select_set(True) @@ -278,7 +532,7 @@ class TLM_AtlastListDeleteItem(bpy.types.Operator): list = scene.TLM_AtlasList index = scene.TLM_AtlasListItem - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: atlasName = scene.TLM_AtlasList[index].name @@ -310,7 +564,7 @@ class TLM_PostAtlastListDeleteItem(bpy.types.Operator): list = scene.TLM_PostAtlasList index = scene.TLM_PostAtlasListItem - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: atlasName = scene.TLM_PostAtlasList[index].name @@ -437,7 +691,7 @@ class TLM_BuildEnvironmentProbes(bpy.types.Operator): def invoke(self, context, event): - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: if obj.type == "LIGHT_PROBE": if obj.data.type == "CUBEMAP": @@ -500,7 +754,7 @@ class TLM_BuildEnvironmentProbes(bpy.types.Operator): cam.rotation_euler = positions[val] filename = os.path.join(directory, val) + "_" + camobj_name + ".hdr" - bpy.data.scenes['Scene'].render.filepath = filename + bpy.context.scene.render.filepath = filename print("Writing out: " + val) bpy.ops.render.render(write_still=True) @@ -642,7 +896,7 @@ class TLM_BuildEnvironmentProbes(bpy.types.Operator): subprocess.call([envpipe3], shell=True) - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: obj.select_set(False) cam_obj.select_set(True) @@ -686,7 +940,92 @@ class TLM_MergeAdjacentActors(bpy.types.Operator): scene = context.scene - + return {'FINISHED'} + +class TLM_PrepareUVMaps(bpy.types.Operator): + bl_idname = "tlm.prepare_uvmaps" + bl_label = "Prepare UV maps" + bl_description = "Prepare UV lightmaps for selected objects" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + scene = context.scene + + + + return {'FINISHED'} + +class TLM_LoadLightmaps(bpy.types.Operator): + bl_idname = "tlm.load_lightmaps" + bl_label = "Load Lightmaps" + bl_description = "Load lightmaps from selected folder" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + scene = context.scene + + utility.transfer_load() + + build.finish_assemble() + + return {'FINISHED'} + +class TLM_ToggleTexelDensity(bpy.types.Operator): + bl_idname = "tlm.toggle_texel_density" + bl_label = "Toggle Texel Density" + bl_description = "Toggle visualize lightmap texel density for selected objects" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + + scene = context.scene + + for obj in bpy.context.selected_objects: + if obj.type == "MESH": + uv_layers = obj.data.uv_layers + + #if the object has a td_vis in the uv maps, toggle off + #else toggle on + + if obj.TLM_ObjectProperties.tlm_use_default_channel: + + for i in range(0, len(uv_layers)): + if uv_layers[i].name == 'UVMap_Lightmap': + uv_layers.active_index = i + break + else: + + for i in range(0, len(uv_layers)): + if uv_layers[i].name == obj.TLM_ObjectProperties.tlm_uv_channel: + uv_layers.active_index = i + break + + #filepath = r"C:\path\to\image.png" + + #img = bpy.data.images.load(filepath) + + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + space_data = area.spaces.active + bpy.ops.screen.area_dupli('INVOKE_DEFAULT') + new_window = context.window_manager.windows[-1] + + area = new_window.screen.areas[-1] + area.type = 'VIEW_3D' + #bg = space_data.background_images.new() + print(bpy.context.object) + bpy.ops.object.bake_td_uv_to_vc() + + #bg.image = img + break + + + #set active uv_layer to + + + print("TLM_Viz_Toggle") return {'FINISHED'} @@ -698,7 +1037,4 @@ def TLM_HalfResolution(): pass def TLM_DivideLMGroups(): - pass - -def TLM_LoadFromFolder(): pass \ No newline at end of file diff --git a/blender/arm/lightmapper/panels/__init__.py b/blender/arm/lightmapper/panels/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/blender/arm/lightmapper/panels/image.py b/blender/arm/lightmapper/panels/image.py new file mode 100644 index 00000000..929907e2 --- /dev/null +++ b/blender/arm/lightmapper/panels/image.py @@ -0,0 +1,66 @@ +import bpy, os, math, importlib + +from bpy.types import Menu, Operator, Panel, UIList + +from bpy.props import ( + StringProperty, + BoolProperty, + IntProperty, + FloatProperty, + FloatVectorProperty, + EnumProperty, + PointerProperty, +) + +class TLM_PT_Imagetools(bpy.types.Panel): + bl_label = "TLM Imagetools" + bl_space_type = "IMAGE_EDITOR" + bl_region_type = 'UI' + bl_category = "TLM Imagetools" + + def draw_header(self, _): + layout = self.layout + row = layout.row(align=True) + row.label(text ="Image Tools") + + def draw(self, context): + layout = self.layout + + activeImg = None + + for area in bpy.context.screen.areas: + if area.type == 'IMAGE_EDITOR': + activeImg = area.spaces.active.image + + if activeImg is not None and activeImg.name != "Render Result" and activeImg.name != "Viewer Node": + + cv2 = importlib.util.find_spec("cv2") + + if cv2 is None: + row = layout.row(align=True) + row.label(text ="OpenCV not installed.") + else: + + row = layout.row(align=True) + row.label(text ="Method") + row = layout.row(align=True) + row.prop(activeImg.TLM_ImageProperties, "tlm_image_scale_engine") + row = layout.row(align=True) + row.prop(activeImg.TLM_ImageProperties, "tlm_image_cache_switch") + row = layout.row(align=True) + row.operator("tlm.image_upscale") + if activeImg.TLM_ImageProperties.tlm_image_cache_switch: + row = layout.row(align=True) + row.label(text ="Switch up.") + row = layout.row(align=True) + row.operator("tlm.image_downscale") + if activeImg.TLM_ImageProperties.tlm_image_cache_switch: + row = layout.row(align=True) + row.label(text ="Switch down.") + if activeImg.TLM_ImageProperties.tlm_image_scale_engine == "OpenCV": + row = layout.row(align=True) + row.prop(activeImg.TLM_ImageProperties, "tlm_image_scale_method") + + else: + row = layout.row(align=True) + row.label(text ="Select an image") \ No newline at end of file diff --git a/blender/arm/lightmapper/panels/light.py b/blender/arm/lightmapper/panels/light.py new file mode 100644 index 00000000..fd576af1 --- /dev/null +++ b/blender/arm/lightmapper/panels/light.py @@ -0,0 +1,17 @@ +import bpy +from bpy.props import * +from bpy.types import Menu, Panel + +class TLM_PT_LightMenu(bpy.types.Panel): + bl_label = "The Lightmapper" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "light" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + scene = context.scene + obj = bpy.context.object + layout.use_property_split = True + layout.use_property_decorate = False \ No newline at end of file diff --git a/blender/arm/lightmapper/panels/object.py b/blender/arm/lightmapper/panels/object.py new file mode 100644 index 00000000..7f0e7abe --- /dev/null +++ b/blender/arm/lightmapper/panels/object.py @@ -0,0 +1,118 @@ +import bpy +from bpy.props import * +from bpy.types import Menu, Panel + +class TLM_PT_ObjectMenu(bpy.types.Panel): + bl_label = "The Lightmapper" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + scene = context.scene + obj = bpy.context.object + layout.use_property_split = True + layout.use_property_decorate = False + + if obj.type == "MESH": + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_use") + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_use_default_channel") + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + + row = layout.row() + row.prop_search(obj.TLM_ObjectProperties, "tlm_uv_channel", obj.data, "uv_layers", text='UV Channel') + + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_resolution") + if obj.TLM_ObjectProperties.tlm_use_default_channel: + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_unwrap_mode") + row = layout.row() + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + + if scene.TLM_AtlasListItem >= 0 and len(scene.TLM_AtlasList) > 0: + row = layout.row() + item = scene.TLM_AtlasList[scene.TLM_AtlasListItem] + row.prop_search(obj.TLM_ObjectProperties, "tlm_atlas_pointer", scene, "TLM_AtlasList", text='Atlas Group') + row = layout.row() + else: + row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") + row = layout.row() + + else: + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_postpack_object") + row = layout.row() + + + if obj.TLM_ObjectProperties.tlm_postpack_object and obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode != "AtlasGroupA": + if scene.TLM_PostAtlasListItem >= 0 and len(scene.TLM_PostAtlasList) > 0: + row = layout.row() + item = scene.TLM_PostAtlasList[scene.TLM_PostAtlasListItem] + row.prop_search(obj.TLM_ObjectProperties, "tlm_postatlas_pointer", scene, "TLM_PostAtlasList", text='Atlas Group') + row = layout.row() + + else: + row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") + row = layout.row() + + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_unwrap_margin") + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filter_override") + row = layout.row() + if obj.TLM_ObjectProperties.tlm_mesh_filter_override: + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_mode") + row = layout.row(align=True) + if obj.TLM_ObjectProperties.tlm_mesh_filtering_mode == "Gaussian": + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_gaussian_strength") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") + elif obj.TLM_ObjectProperties.tlm_mesh_filtering_mode == "Box": + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_box_strength") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") + elif obj.TLM_ObjectProperties.tlm_mesh_filtering_mode == "Bilateral": + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_bilateral_diameter") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_bilateral_color_deviation") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_bilateral_coordinate_deviation") + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") + else: + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_median_kernel", expand=True) + row = layout.row(align=True) + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") + + +class TLM_PT_MaterialMenu(bpy.types.Panel): + bl_label = "The Lightmapper" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "material" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + scene = context.scene + obj = bpy.context.object + layout.use_property_split = True + layout.use_property_decorate = False + + mat = bpy.context.material + if mat == None: + return + + if obj.type == "MESH": + + row = layout.row() + row.prop(mat, "TLM_ignore") \ No newline at end of file diff --git a/blender/arm/lightmapper/panels/scene.py b/blender/arm/lightmapper/panels/scene.py new file mode 100644 index 00000000..56c05cb7 --- /dev/null +++ b/blender/arm/lightmapper/panels/scene.py @@ -0,0 +1,582 @@ +import bpy, importlib, math +from bpy.props import * +from bpy.types import Menu, Panel +from .. utility import icon +from .. properties.denoiser import oidn, optix + +class TLM_PT_Settings(bpy.types.Panel): + bl_label = "Settings" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_BakePanel" + + @classmethod + def poll(self, context): + scene = context.scene + return scene.arm_bakemode == "Lightmap" + + def draw(self, context): + layout = self.layout + scene = context.scene + layout.use_property_split = True + layout.use_property_decorate = False + sceneProperties = scene.TLM_SceneProperties + + row = layout.row(align=True) + + #We list LuxCoreRender as available, by default we assume Cycles exists + row.prop(sceneProperties, "tlm_lightmap_engine") + + if sceneProperties.tlm_lightmap_engine == "Cycles": + + #CYCLES SETTINGS HERE + engineProperties = scene.TLM_EngineProperties + + row = layout.row(align=True) + row.label(text="General Settings") + row = layout.row(align=True) + row.operator("tlm.build_lightmaps") + row = layout.row(align=True) + row.operator("tlm.clean_lightmaps") + row = layout.row(align=True) + row.operator("tlm.explore_lightmaps") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_apply_on_unwrap") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_headless") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_alert_on_finish") + + if sceneProperties.tlm_alert_on_finish: + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_alert_sound") + + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_verbose") + #row = layout.row(align=True) + #row.prop(sceneProperties, "tlm_compile_statistics") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_override_bg_color") + if sceneProperties.tlm_override_bg_color: + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_override_color") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_reset_uv") + + row = layout.row(align=True) + try: + if bpy.context.scene["TLM_Buildstat"] is not None: + row.label(text="Last build completed in: " + str(bpy.context.scene["TLM_Buildstat"][0])) + except: + pass + + row = layout.row(align=True) + row.label(text="Cycles Settings") + + row = layout.row(align=True) + row.prop(engineProperties, "tlm_mode") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_quality") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_resolution_scale") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_bake_mode") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_target") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_lighting_mode") + # if scene.TLM_EngineProperties.tlm_lighting_mode == "combinedao" or scene.TLM_EngineProperties.tlm_lighting_mode == "indirectao": + # row = layout.row(align=True) + # row.prop(engineProperties, "tlm_premultiply_ao") + if scene.TLM_EngineProperties.tlm_bake_mode == "Background": + row = layout.row(align=True) + row.label(text="Warning! Background mode is currently unstable", icon_value=2) + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_network_render") + if sceneProperties.tlm_network_render: + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_network_paths") + #row = layout.row(align=True) + #row.prop(sceneProperties, "tlm_network_dir") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_caching_mode") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_directional_mode") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_lightmap_savedir") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_dilation_margin") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_exposure_multiplier") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_setting_supersample") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_metallic_clamp") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_texture_interpolation") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_texture_extrapolation") + + + + # elif sceneProperties.tlm_lightmap_engine == "LuxCoreRender": + + # engineProperties = scene.TLM_Engine2Properties + # row = layout.row(align=True) + # row.prop(engineProperties, "tlm_luxcore_dir") + # row = layout.row(align=True) + # row.operator("tlm.build_lightmaps") + # #LUXCORE SETTINGS HERE + # #luxcore_available = False + + # #Look for Luxcorerender in the renderengine classes + # # for engine in bpy.types.RenderEngine.__subclasses__(): + # # if engine.bl_idname == "LUXCORE": + # # luxcore_available = True + # # break + + # # row = layout.row(align=True) + # # if not luxcore_available: + # # row.label(text="Please install BlendLuxCore.") + # # else: + # # row.label(text="LuxCoreRender not yet available.") + + elif sceneProperties.tlm_lightmap_engine == "OctaneRender": + + engineProperties = scene.TLM_Engine3Properties + + #LUXCORE SETTINGS HERE + octane_available = True + + + + row = layout.row(align=True) + row.operator("tlm.build_lightmaps") + row = layout.row(align=True) + row.operator("tlm.clean_lightmaps") + row = layout.row(align=True) + row.operator("tlm.explore_lightmaps") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_verbose") + row = layout.row(align=True) + row.prop(engineProperties, "tlm_lightmap_savedir") + row = layout.row(align=True) + +class TLM_PT_Denoise(bpy.types.Panel): + bl_label = "Denoise" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_BakePanel" + + @classmethod + def poll(self, context): + scene = context.scene + return scene.arm_bakemode == "Lightmap" + + def draw_header(self, context): + scene = context.scene + sceneProperties = scene.TLM_SceneProperties + self.layout.prop(sceneProperties, "tlm_denoise_use", text="") + + def draw(self, context): + layout = self.layout + scene = context.scene + layout.use_property_split = True + layout.use_property_decorate = False + sceneProperties = scene.TLM_SceneProperties + layout.active = sceneProperties.tlm_denoise_use + + row = layout.row(align=True) + + #row.prop(sceneProperties, "tlm_denoiser", expand=True) + #row = layout.row(align=True) + row.prop(sceneProperties, "tlm_denoise_engine", expand=True) + row = layout.row(align=True) + + if sceneProperties.tlm_denoise_engine == "Integrated": + row.label(text="No options for Integrated.") + elif sceneProperties.tlm_denoise_engine == "OIDN": + denoiseProperties = scene.TLM_OIDNEngineProperties + row.prop(denoiseProperties, "tlm_oidn_path") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_oidn_verbose") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_oidn_threads") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_oidn_maxmem") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_oidn_affinity") + # row = layout.row(align=True) + # row.prop(denoiseProperties, "tlm_denoise_ao") + elif sceneProperties.tlm_denoise_engine == "Optix": + denoiseProperties = scene.TLM_OptixEngineProperties + row.prop(denoiseProperties, "tlm_optix_path") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_optix_verbose") + row = layout.row(align=True) + row.prop(denoiseProperties, "tlm_optix_maxmem") + #row = layout.row(align=True) + #row.prop(denoiseProperties, "tlm_denoise_ao") + +class TLM_PT_Filtering(bpy.types.Panel): + bl_label = "Filtering" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_BakePanel" + + @classmethod + def poll(self, context): + scene = context.scene + return scene.arm_bakemode == "Lightmap" + + def draw_header(self, context): + scene = context.scene + sceneProperties = scene.TLM_SceneProperties + self.layout.prop(sceneProperties, "tlm_filtering_use", text="") + + def draw(self, context): + layout = self.layout + scene = context.scene + layout.use_property_split = True + layout.use_property_decorate = False + sceneProperties = scene.TLM_SceneProperties + layout.active = sceneProperties.tlm_filtering_use + #row = layout.row(align=True) + #row.label(text="TODO MAKE CHECK") + #row = layout.row(align=True) + #row.prop(sceneProperties, "tlm_filtering_engine", expand=True) + row = layout.row(align=True) + + if sceneProperties.tlm_filtering_engine == "OpenCV": + + cv2 = importlib.util.find_spec("cv2") + + if cv2 is None: + row = layout.row(align=True) + row.label(text="OpenCV is not installed. Install it through preferences.") + else: + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_mode") + row = layout.row(align=True) + if scene.TLM_SceneProperties.tlm_filtering_mode == "Gaussian": + row.prop(scene.TLM_SceneProperties, "tlm_filtering_gaussian_strength") + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") + elif scene.TLM_SceneProperties.tlm_filtering_mode == "Box": + row.prop(scene.TLM_SceneProperties, "tlm_filtering_box_strength") + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") + + elif scene.TLM_SceneProperties.tlm_filtering_mode == "Bilateral": + row.prop(scene.TLM_SceneProperties, "tlm_filtering_bilateral_diameter") + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_bilateral_color_deviation") + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_bilateral_coordinate_deviation") + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") + else: + row.prop(scene.TLM_SceneProperties, "tlm_filtering_median_kernel", expand=True) + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") + else: + row = layout.row(align=True) + row.prop(scene.TLM_SceneProperties, "tlm_numpy_filtering_mode") + + +class TLM_PT_Encoding(bpy.types.Panel): + bl_label = "Encoding" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_BakePanel" + + @classmethod + def poll(self, context): + scene = context.scene + return scene.arm_bakemode == "Lightmap" + + def draw_header(self, context): + scene = context.scene + sceneProperties = scene.TLM_SceneProperties + self.layout.prop(sceneProperties, "tlm_encoding_use", text="") + + def draw(self, context): + layout = self.layout + scene = context.scene + layout.use_property_split = True + layout.use_property_decorate = False + sceneProperties = scene.TLM_SceneProperties + layout.active = sceneProperties.tlm_encoding_use + + sceneProperties = scene.TLM_SceneProperties + row = layout.row(align=True) + + if scene.TLM_EngineProperties.tlm_bake_mode == "Background": + row.label(text="Encoding options disabled in background mode") + row = layout.row(align=True) + + else: + + row.prop(sceneProperties, "tlm_encoding_device", expand=True) + row = layout.row(align=True) + + if sceneProperties.tlm_encoding_device == "CPU": + row.prop(sceneProperties, "tlm_encoding_mode_a", expand=True) + else: + row.prop(sceneProperties, "tlm_encoding_mode_b", expand=True) + + if sceneProperties.tlm_encoding_device == "CPU": + if sceneProperties.tlm_encoding_mode_a == "RGBM": + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_encoding_range") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_decoder_setup") + if sceneProperties.tlm_encoding_mode_a == "RGBD": + pass + if sceneProperties.tlm_encoding_mode_a == "HDR": + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_format") + else: + + if sceneProperties.tlm_encoding_mode_b == "RGBM": + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_encoding_range") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_decoder_setup") + + if sceneProperties.tlm_encoding_mode_b == "LogLuv" and sceneProperties.tlm_encoding_device == "GPU": + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_decoder_setup") + if sceneProperties.tlm_encoding_mode_b == "HDR": + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_format") + +class TLM_PT_Utility(bpy.types.Panel): + bl_label = "Utilities" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_BakePanel" + + @classmethod + def poll(self, context): + scene = context.scene + return scene.arm_bakemode == "Lightmap" + + def draw(self, context): + layout = self.layout + scene = context.scene + layout.use_property_split = True + layout.use_property_decorate = False + sceneProperties = scene.TLM_SceneProperties + + row = layout.row(align=True) + row.label(text="Enable Lightmaps for set") + row = layout.row(align=True) + row.operator("tlm.enable_set") + row = layout.row(align=True) + row.prop(sceneProperties, "tlm_utility_set") + row = layout.row(align=True) + #row.label(text="ABCD") + row.prop(sceneProperties, "tlm_mesh_lightmap_unwrap_mode") + + if sceneProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + + if scene.TLM_AtlasListItem >= 0 and len(scene.TLM_AtlasList) > 0: + row = layout.row() + item = scene.TLM_AtlasList[scene.TLM_AtlasListItem] + row.prop_search(sceneProperties, "tlm_atlas_pointer", scene, "TLM_AtlasList", text='Atlas Group') + else: + row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") + + else: + + row = layout.row() + row.prop(sceneProperties, "tlm_postpack_object") + row = layout.row() + + if sceneProperties.tlm_postpack_object and sceneProperties.tlm_mesh_lightmap_unwrap_mode != "AtlasGroupA": + + if scene.TLM_PostAtlasListItem >= 0 and len(scene.TLM_PostAtlasList) > 0: + row = layout.row() + item = scene.TLM_PostAtlasList[scene.TLM_PostAtlasListItem] + row.prop_search(sceneProperties, "tlm_postatlas_pointer", scene, "TLM_PostAtlasList", text='Atlas Group') + row = layout.row() + + else: + row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") + row = layout.row() + + row.prop(sceneProperties, "tlm_mesh_unwrap_margin") + row = layout.row() + row.prop(sceneProperties, "tlm_resolution_weight") + + if sceneProperties.tlm_resolution_weight == "Single": + row = layout.row() + row.prop(sceneProperties, "tlm_mesh_lightmap_resolution") + else: + row = layout.row() + row.prop(sceneProperties, "tlm_resolution_min") + row = layout.row() + row.prop(sceneProperties, "tlm_resolution_max") + + row = layout.row() + row.operator("tlm.disable_selection") + row = layout.row(align=True) + row.operator("tlm.select_lightmapped_objects") + row = layout.row(align=True) + row.operator("tlm.remove_uv_selection") + row = layout.row(align=True) + + + row.label(text="Environment Probes") + row = layout.row() + row.operator("tlm.build_environmentprobe") + row = layout.row() + row.operator("tlm.clean_environmentprobe") + row = layout.row() + row.prop(sceneProperties, "tlm_environment_probe_engine") + row = layout.row() + row.prop(sceneProperties, "tlm_cmft_path") + row = layout.row() + row.prop(sceneProperties, "tlm_environment_probe_resolution") + row = layout.row() + row.prop(sceneProperties, "tlm_create_spherical") + + if sceneProperties.tlm_create_spherical: + + row = layout.row() + row.prop(sceneProperties, "tlm_invert_direction") + row = layout.row() + row.prop(sceneProperties, "tlm_write_sh") + row = layout.row() + row.prop(sceneProperties, "tlm_write_radiance") + + row = layout.row(align=True) + row.label(text="Load lightmaps") + row = layout.row() + row.prop(sceneProperties, "tlm_load_folder") + row = layout.row() + row.operator("tlm.load_lightmaps") + +class TLM_PT_Additional(bpy.types.Panel): + bl_label = "Additional" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "render" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "ARM_PT_BakePanel" + + @classmethod + def poll(self, context): + scene = context.scene + return scene.arm_bakemode == "Lightmap" + + def draw(self, context): + layout = self.layout + scene = context.scene + sceneProperties = scene.TLM_SceneProperties + atlasListItem = scene.TLM_AtlasListItem + atlasList = scene.TLM_AtlasList + postatlasListItem = scene.TLM_PostAtlasListItem + postatlasList = scene.TLM_PostAtlasList + + layout.label(text="Network Rendering") + row = layout.row() + row.operator("tlm.start_server") + layout.label(text="Atlas Groups") + row = layout.row() + row.prop(sceneProperties, "tlm_atlas_mode", expand=True) + + if sceneProperties.tlm_atlas_mode == "Prepack": + + rows = 2 + if len(atlasList) > 1: + rows = 4 + row = layout.row() + row.template_list("TLM_UL_AtlasList", "Atlas List", scene, "TLM_AtlasList", scene, "TLM_AtlasListItem", rows=rows) + col = row.column(align=True) + col.operator("tlm_atlaslist.new_item", icon='ADD', text="") + col.operator("tlm_atlaslist.delete_item", icon='REMOVE', text="") + + if atlasListItem >= 0 and len(atlasList) > 0: + item = atlasList[atlasListItem] + layout.prop(item, "tlm_atlas_lightmap_unwrap_mode") + layout.prop(item, "tlm_atlas_lightmap_resolution") + layout.prop(item, "tlm_atlas_unwrap_margin") + + amount = 0 + + for obj in bpy.context.scene.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + if obj.TLM_ObjectProperties.tlm_atlas_pointer == item.name: + amount = amount + 1 + + layout.label(text="Objects: " + str(amount)) + + else: + + layout.label(text="Postpacking is unstable.") + + cv2 = importlib.util.find_spec("cv2") + + if cv2 is None: + + row = layout.row(align=True) + row.label(text="OpenCV is not installed. Install it through preferences.") + + else: + + rows = 2 + if len(atlasList) > 1: + rows = 4 + row = layout.row() + row.template_list("TLM_UL_PostAtlasList", "PostList", scene, "TLM_PostAtlasList", scene, "TLM_PostAtlasListItem", rows=rows) + col = row.column(align=True) + col.operator("tlm_postatlaslist.new_item", icon='ADD', text="") + col.operator("tlm_postatlaslist.delete_item", icon='REMOVE', text="") + + if postatlasListItem >= 0 and len(postatlasList) > 0: + item = postatlasList[postatlasListItem] + layout.prop(item, "tlm_atlas_lightmap_resolution") + + #Below list object counter + amount = 0 + utilized = 0 + atlasUsedArea = 0 + atlasSize = item.tlm_atlas_lightmap_resolution + + for obj in bpy.context.scene.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + if obj.TLM_ObjectProperties.tlm_postpack_object: + if obj.TLM_ObjectProperties.tlm_postatlas_pointer == item.name: + amount = amount + 1 + + atlasUsedArea += int(obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution) ** 2 + + row = layout.row() + row.prop(item, "tlm_atlas_repack_on_cleanup") + + #TODO SET A CHECK FOR THIS! ADD A CV2 CHECK TO UTILITY! + cv2 = True + + if cv2: + row = layout.row() + row.prop(item, "tlm_atlas_dilation") + layout.label(text="Objects: " + str(amount)) + + utilized = atlasUsedArea / (int(atlasSize) ** 2) + layout.label(text="Utilized: " + str(utilized * 100) + "%") + + if (utilized * 100) > 100: + layout.label(text="Warning! Overflow not yet supported") \ No newline at end of file diff --git a/blender/arm/lightmapper/panels/world.py b/blender/arm/lightmapper/panels/world.py new file mode 100644 index 00000000..b3c5d294 --- /dev/null +++ b/blender/arm/lightmapper/panels/world.py @@ -0,0 +1,17 @@ +import bpy +from bpy.props import * +from bpy.types import Menu, Panel + +class TLM_PT_WorldMenu(bpy.types.Panel): + bl_label = "The Lightmapper" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "world" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + scene = context.scene + obj = bpy.context.object + layout.use_property_split = True + layout.use_property_decorate = False \ No newline at end of file diff --git a/blender/arm/lightmapper/preferences/__init__.py b/blender/arm/lightmapper/preferences/__init__.py new file mode 100644 index 00000000..94cdfaea --- /dev/null +++ b/blender/arm/lightmapper/preferences/__init__.py @@ -0,0 +1,16 @@ +import bpy +from bpy.utils import register_class, unregister_class +from . import addon_preferences +#from . import build, clean, explore, encode, installopencv + +classes = [ + addon_preferences.TLM_AddonPreferences +] + +def register(): + for cls in classes: + register_class(cls) + +def unregister(): + for cls in classes: + unregister_class(cls) \ No newline at end of file diff --git a/blender/arm/lightmapper/preferences/addon_preferences.py b/blender/arm/lightmapper/preferences/addon_preferences.py new file mode 100644 index 00000000..173ac71a --- /dev/null +++ b/blender/arm/lightmapper/preferences/addon_preferences.py @@ -0,0 +1,75 @@ +import bpy, platform +from os.path import basename, dirname +from bpy.types import AddonPreferences +from .. operators import installopencv +import importlib + +class TLM_AddonPreferences(AddonPreferences): + + bl_idname = "thelightmapper" + + def draw(self, context): + + layout = self.layout + + box = layout.box() + row = box.row() + row.label(text="OpenCV") + + cv2 = importlib.util.find_spec("cv2") + + if cv2 is not None: + row.label(text="OpenCV installed") + else: + if platform.system() == "Windows": + row.label(text="OpenCV not found - Install as administrator!", icon_value=2) + else: + row.label(text="OpenCV not found - Click to install!", icon_value=2) + row = box.row() + row.operator("tlm.install_opencv_lightmaps", icon="PREFERENCES") + + box = layout.box() + row = box.row() + row.label(text="Blender Xatlas") + if "blender_xatlas" in bpy.context.preferences.addons.keys(): + row.label(text="Blender Xatlas installed and available") + else: + row.label(text="Blender Xatlas not installed", icon_value=2) + row = box.row() + row.label(text="Github: https://github.com/mattedicksoncom/blender-xatlas") + + box = layout.box() + row = box.row() + row.label(text="RizomUV Bridge") + row.label(text="Coming soon") + + box = layout.box() + row = box.row() + row.label(text="UVPackmaster") + row.label(text="Coming soon") + + texel_density_addon = False + for addon in bpy.context.preferences.addons.keys(): + if addon.startswith("Texel_Density"): + texel_density_addon = True + + box = layout.box() + row = box.row() + row.label(text="Texel Density Checker") + if texel_density_addon: + row.label(text="Texel Density Checker installed and available") + else: + row.label(text="Texel Density Checker", icon_value=2) + row.label(text="Coming soon") + row = box.row() + row.label(text="Github: https://github.com/mrven/Blender-Texel-Density-Checker") + + box = layout.box() + row = box.row() + row.label(text="LuxCoreRender") + row.label(text="Coming soon") + + box = layout.box() + row = box.row() + row.label(text="OctaneRender") + row.label(text="Coming soon") \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/__init__.py b/blender/arm/lightmapper/properties/__init__.py index 052a173b..0a011de9 100644 --- a/blender/arm/lightmapper/properties/__init__.py +++ b/blender/arm/lightmapper/properties/__init__.py @@ -1,7 +1,7 @@ import bpy from bpy.utils import register_class, unregister_class -from . import scene, object, atlas -from . renderer import cycles, luxcorerender +from . import scene, object, atlas, image +from . renderer import cycles, luxcorerender, octanerender from . denoiser import oidn, optix classes = [ @@ -9,12 +9,14 @@ classes = [ object.TLM_ObjectProperties, cycles.TLM_CyclesSceneProperties, luxcorerender.TLM_LuxCoreSceneProperties, + octanerender.TLM_OctanerenderSceneProperties, oidn.TLM_OIDNEngineProperties, optix.TLM_OptixEngineProperties, atlas.TLM_AtlasListItem, atlas.TLM_UL_AtlasList, atlas.TLM_PostAtlasListItem, - atlas.TLM_UL_PostAtlasList + atlas.TLM_UL_PostAtlasList, + image.TLM_ImageProperties ] def register(): @@ -25,12 +27,14 @@ def register(): bpy.types.Object.TLM_ObjectProperties = bpy.props.PointerProperty(type=object.TLM_ObjectProperties) bpy.types.Scene.TLM_EngineProperties = bpy.props.PointerProperty(type=cycles.TLM_CyclesSceneProperties) bpy.types.Scene.TLM_Engine2Properties = bpy.props.PointerProperty(type=luxcorerender.TLM_LuxCoreSceneProperties) + bpy.types.Scene.TLM_Engine3Properties = bpy.props.PointerProperty(type=octanerender.TLM_OctanerenderSceneProperties) bpy.types.Scene.TLM_OIDNEngineProperties = bpy.props.PointerProperty(type=oidn.TLM_OIDNEngineProperties) bpy.types.Scene.TLM_OptixEngineProperties = bpy.props.PointerProperty(type=optix.TLM_OptixEngineProperties) bpy.types.Scene.TLM_AtlasListItem = bpy.props.IntProperty(name="Index for my_list", default=0) bpy.types.Scene.TLM_AtlasList = bpy.props.CollectionProperty(type=atlas.TLM_AtlasListItem) bpy.types.Scene.TLM_PostAtlasListItem = bpy.props.IntProperty(name="Index for my_list", default=0) bpy.types.Scene.TLM_PostAtlasList = bpy.props.CollectionProperty(type=atlas.TLM_PostAtlasListItem) + bpy.types.Image.TLM_ImageProperties = bpy.props.PointerProperty(type=image.TLM_ImageProperties) bpy.types.Material.TLM_ignore = bpy.props.BoolProperty(name="Skip material", description="Ignore material for lightmapped object", default=False) @@ -42,9 +46,11 @@ def unregister(): del bpy.types.Object.TLM_ObjectProperties del bpy.types.Scene.TLM_EngineProperties del bpy.types.Scene.TLM_Engine2Properties + del bpy.types.Scene.TLM_Engine3Properties del bpy.types.Scene.TLM_OIDNEngineProperties del bpy.types.Scene.TLM_OptixEngineProperties del bpy.types.Scene.TLM_AtlasListItem del bpy.types.Scene.TLM_AtlasList del bpy.types.Scene.TLM_PostAtlasListItem - del bpy.types.Scene.TLM_PostAtlasList \ No newline at end of file + del bpy.types.Scene.TLM_PostAtlasList + del bpy.types.Image.TLM_ImageProperties \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/atlas.py b/blender/arm/lightmapper/properties/atlas.py index baccd926..8154f72c 100644 --- a/blender/arm/lightmapper/properties/atlas.py +++ b/blender/arm/lightmapper/properties/atlas.py @@ -34,12 +34,16 @@ class TLM_PostAtlasListItem(bpy.types.PropertyGroup): max=1.0, subtype='FACTOR') - tlm_atlas_lightmap_unwrap_mode : EnumProperty( - items = [('Lightmap', 'Lightmap', 'TODO'), - ('SmartProject', 'Smart Project', 'TODO'), - ('Xatlas', 'Xatlas', 'TODO')], + unwrap_modes = [('Lightmap', 'Lightmap', 'Use Blender Lightmap Pack algorithm'), + ('SmartProject', 'Smart Project', 'Use Blender Smart Project algorithm')] + + if "blender_xatlas" in bpy.context.preferences.addons.keys(): + unwrap_modes.append(('Xatlas', 'Xatlas', 'Use Xatlas addon packing algorithm')) + + tlm_postatlas_lightmap_unwrap_mode : EnumProperty( + items = unwrap_modes, name = "Unwrap Mode", - description="TODO", + description="Atlas unwrapping method", default='SmartProject') class TLM_UL_PostAtlasList(bpy.types.UIList): @@ -51,7 +55,7 @@ class TLM_UL_PostAtlasList(bpy.types.UIList): #In list object counter amount = 0 - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: if obj.TLM_ObjectProperties.tlm_postpack_object: if obj.TLM_ObjectProperties.tlm_postatlas_pointer == item.name: @@ -69,9 +73,6 @@ class TLM_UL_PostAtlasList(bpy.types.UIList): layout.alignment = 'CENTER' layout.label(text="", icon = custom_icon) - - - class TLM_AtlasListItem(bpy.types.PropertyGroup): obj: PointerProperty(type=bpy.types.Object, description="The object to bake") tlm_atlas_lightmap_resolution : EnumProperty( @@ -95,12 +96,17 @@ class TLM_AtlasListItem(bpy.types.PropertyGroup): max=1.0, subtype='FACTOR') + unwrap_modes = [('Lightmap', 'Lightmap', 'Use Blender Lightmap Pack algorithm'), + ('SmartProject', 'Smart Project', 'Use Blender Smart Project algorithm'), + ('Copy', 'Copy existing', 'Use the existing UV channel')] + + if "blender_xatlas" in bpy.context.preferences.addons.keys(): + unwrap_modes.append(('Xatlas', 'Xatlas', 'Use Xatlas addon packing algorithm')) + tlm_atlas_lightmap_unwrap_mode : EnumProperty( - items = [('Lightmap', 'Lightmap', 'TODO'), - ('SmartProject', 'Smart Project', 'TODO'), - ('Xatlas', 'Xatlas', 'TODO')], + items = unwrap_modes, name = "Unwrap Mode", - description="TODO", + description="Atlas unwrapping method", default='SmartProject') class TLM_UL_AtlasList(bpy.types.UIList): @@ -111,7 +117,7 @@ class TLM_UL_AtlasList(bpy.types.UIList): amount = 0 - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": if obj.TLM_ObjectProperties.tlm_atlas_pointer == item.name: diff --git a/blender/arm/lightmapper/properties/image.py b/blender/arm/lightmapper/properties/image.py index e6e75766..a81169c4 100644 --- a/blender/arm/lightmapper/properties/image.py +++ b/blender/arm/lightmapper/properties/image.py @@ -1,10 +1,26 @@ import bpy from bpy.props import * -class TLM_ObjectProperties(bpy.types.PropertyGroup): - tlm_image_scale_method : EnumProperty( - items = [('Native', 'Native', 'TODO'), - ('OpenCV', 'OpenCV', 'TODO')], +class TLM_ImageProperties(bpy.types.PropertyGroup): + tlm_image_scale_engine : EnumProperty( + items = [('OpenCV', 'OpenCV', 'TODO')], name = "Scaling engine", description="TODO", - default='Native') \ No newline at end of file + default='OpenCV') + + #('Native', 'Native', 'TODO'), + + tlm_image_scale_method : EnumProperty( + items = [('Nearest', 'Nearest', 'TODO'), + ('Area', 'Area', 'TODO'), + ('Linear', 'Linear', 'TODO'), + ('Cubic', 'Cubic', 'TODO'), + ('Lanczos', 'Lanczos', 'TODO')], + name = "Scaling method", + description="TODO", + default='Lanczos') + + tlm_image_cache_switch : BoolProperty( + name="Cache for quickswitch", + description="Caches scaled images for quick switching", + default=True) \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/object.py b/blender/arm/lightmapper/properties/object.py index 7738add9..c5c2fe47 100644 --- a/blender/arm/lightmapper/properties/object.py +++ b/blender/arm/lightmapper/properties/object.py @@ -7,7 +7,7 @@ class TLM_ObjectProperties(bpy.types.PropertyGroup): tlm_atlas_pointer : StringProperty( name = "Atlas Group", - description = "Atlas Lightmap Group", + description = "", default = "") tlm_postatlas_pointer : StringProperty( @@ -51,8 +51,7 @@ class TLM_ObjectProperties(bpy.types.PropertyGroup): unwrap_modes = [('Lightmap', 'Lightmap', 'TODO'), ('SmartProject', 'Smart Project', 'TODO'), - ('CopyExisting', 'Copy Existing', 'TODO'), - ('AtlasGroupA', 'Atlas Group (Prepack)', 'TODO')] + ('AtlasGroupA', 'Atlas Group (Prepack)', 'Attaches the object to a prepack Atlas group. Will overwrite UV map on build.')] tlm_postpack_object : BoolProperty( #CHECK INSTEAD OF ATLASGROUPB name="Postpack object", @@ -145,4 +144,14 @@ class TLM_ObjectProperties(bpy.types.PropertyGroup): name="Median kernel", default=3, min=1, - max=5) \ No newline at end of file + max=5) + + tlm_use_default_channel : BoolProperty( + name="Use default UV channel", + description="Will either use or create the default UV Channel 'UVMap_Lightmap' upon build.", + default=True) + + tlm_uv_channel : StringProperty( + name = "UV Channel", + description = "Use any custom UV Channel for the lightmap", + default = "UVMap") \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/renderer/cycles.py b/blender/arm/lightmapper/properties/renderer/cycles.py index ad99a664..d3763400 100644 --- a/blender/arm/lightmapper/properties/renderer/cycles.py +++ b/blender/arm/lightmapper/properties/renderer/cycles.py @@ -21,6 +21,16 @@ class TLM_CyclesSceneProperties(bpy.types.PropertyGroup): description="Select baking quality", default="0") + targets = [('texture', 'Image texture', 'Build to image texture')] + if (2, 92, 0) >= bpy.app.version: + targets.append(('vertex', 'Vertex colors', 'Build to vertex colors')) + + tlm_target : EnumProperty( + items = targets, + name = "Build Target", + description="Select target to build to", + default="texture") + tlm_resolution_scale : EnumProperty( items = [('1', '1/1', '1'), ('2', '1/2', '2'), @@ -45,10 +55,12 @@ class TLM_CyclesSceneProperties(bpy.types.PropertyGroup): description="Select bake mode", default="Foreground") + caching_modes = [('Copy', 'Copy', 'More overhead; allows for network.')] + + #caching_modes.append(('Cache', 'Cache', 'Cache in separate blend'),('Node', 'Node restore', 'EXPERIMENTAL! Use with care')) + tlm_caching_mode : EnumProperty( - items = [('Copy', 'Copy', 'More overhead; allows for network.'), - ('Cache', 'Cache', 'Cache in separate blend'), - ('Node', 'Node restore', 'EXPERIMENTAL! Use with care')], + items = caching_modes, name = "Caching mode", description="Select cache mode", default="Copy") @@ -88,8 +100,16 @@ class TLM_CyclesSceneProperties(bpy.types.PropertyGroup): tlm_lighting_mode : EnumProperty( items = [('combined', 'Combined', 'Bake combined lighting'), + ('combinedao', 'Combined+AO', 'Bake combined lighting with Ambient Occlusion'), ('indirect', 'Indirect', 'Bake indirect lighting'), - ('ao', 'AO', 'Bake only Ambient Occlusion')], +# ('indirectao', 'Indirect+AO', 'Bake indirect lighting with Ambient Occlusion'), + ('ao', 'AO', 'Bake only Ambient Occlusion'), + ('complete', 'Complete', 'Bake complete map')], name = "Lighting mode", description="TODO.", - default="combined") \ No newline at end of file + default="combined") + + tlm_premultiply_ao : BoolProperty( + name="Premultiply AO", + description="Ambient Occlusion will be premultiplied together with lightmaps, requiring less textures.", + default=True) \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/renderer/octanerender.py b/blender/arm/lightmapper/properties/renderer/octanerender.py index e69de29b..8c66cf13 100644 --- a/blender/arm/lightmapper/properties/renderer/octanerender.py +++ b/blender/arm/lightmapper/properties/renderer/octanerender.py @@ -0,0 +1,10 @@ +import bpy +from bpy.props import * + +class TLM_OctanerenderSceneProperties(bpy.types.PropertyGroup): + + tlm_lightmap_savedir : StringProperty( + name="Lightmap Directory", + description="TODO", + default="Lightmaps", + subtype="FILE_PATH") \ No newline at end of file diff --git a/blender/arm/lightmapper/properties/scene.py b/blender/arm/lightmapper/properties/scene.py index 6652b548..36c74fbc 100644 --- a/blender/arm/lightmapper/properties/scene.py +++ b/blender/arm/lightmapper/properties/scene.py @@ -1,11 +1,19 @@ -import bpy +import bpy, os from bpy.props import * +from .. utility import utility + +def transfer_load(): + load_folder = bpy.context.scene.TLM_SceneProperties.tlm_load_folder + lightmap_folder = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + print(load_folder) + print(lightmap_folder) + #transfer_assets(True, load_folder, lightmap_folder) class TLM_SceneProperties(bpy.types.PropertyGroup): engines = [('Cycles', 'Cycles', 'Use Cycles for lightmapping')] - engines.append(('LuxCoreRender', 'LuxCoreRender', 'Use LuxCoreRender for lightmapping')) + #engines.append(('LuxCoreRender', 'LuxCoreRender', 'Use LuxCoreRender for lightmapping')) #engines.append(('OctaneRender', 'Octane Render', 'Use Octane Render for lightmapping')) tlm_atlas_pointer : StringProperty( @@ -112,7 +120,7 @@ class TLM_SceneProperties(bpy.types.PropertyGroup): #FILTERING SETTINGS GROUP tlm_filtering_use : BoolProperty( - name="Enable Filtering", + name="Enable denoising", description="Enable denoising for lightmaps", default=False) @@ -182,6 +190,17 @@ class TLM_SceneProperties(bpy.types.PropertyGroup): min=1, max=5) + tlm_clamp_hdr : BoolProperty( + name="Enable HDR Clamp", + description="Clamp HDR Value", + default=False) + + tlm_clamp_hdr_value : IntProperty( + name="HDR Clamp value", + default=10, + min=0, + max=20) + #Encoding properties tlm_encoding_use : BoolProperty( name="Enable encoding", @@ -197,12 +216,13 @@ class TLM_SceneProperties(bpy.types.PropertyGroup): encoding_modes_1 = [('RGBM', 'RGBM', '8-bit HDR encoding. Good for compatibility, good for memory but has banding issues.'), ('RGBD', 'RGBD', '8-bit HDR encoding. Similar to RGBM.'), - ('HDR', 'HDR', '32-bit HDR encoding. Best quality, but high memory usage and not compatible with all devices.')] + ('HDR', 'HDR', '32-bit HDR encoding. Best quality, but high memory usage and not compatible with all devices.'), + ('SDR', 'SDR', '8-bit flat encoding.')] - encoding_modes_2 = [('RGBM', 'RGBM', '8-bit HDR encoding. Good for compatibility, good for memory but has banding issues.'), - ('RGBD', 'RGBD', '8-bit HDR encoding. Similar to RGBM.'), + encoding_modes_2 = [('RGBD', 'RGBD', '8-bit HDR encoding. Similar to RGBM.'), ('LogLuv', 'LogLuv', '8-bit HDR encoding. Different.'), - ('HDR', 'HDR', '32-bit HDR encoding. Best quality, but high memory usage and not compatible with all devices.')] + ('HDR', 'HDR', '32-bit HDR encoding. Best quality, but high memory usage and not compatible with all devices.'), + ('SDR', 'SDR', '8-bit flat encoding.')] tlm_encoding_mode_a : EnumProperty( items = encoding_modes_1, @@ -275,8 +295,7 @@ class TLM_SceneProperties(bpy.types.PropertyGroup): tlm_mesh_lightmap_unwrap_mode : EnumProperty( items = [('Lightmap', 'Lightmap', 'TODO'), ('SmartProject', 'Smart Project', 'TODO'), - ('CopyExisting', 'Copy Existing', 'TODO'), - ('AtlasGroupA', 'Atlas Group (Prepack)', 'TODO'), + ('AtlasGroupA', 'Atlas Group (Prepack)', 'Attaches the object to a prepack Atlas group. Will overwrite UV map on build.'), ('Xatlas', 'Xatlas', 'TODO')], name = "Unwrap Mode", description="TODO", @@ -317,12 +336,30 @@ class TLM_SceneProperties(bpy.types.PropertyGroup): tlm_metallic_clamp : EnumProperty( items = [('ignore', 'Ignore', 'Ignore clamping'), + ('skip', 'Skip', 'Skip baking metallic materials'), ('zero', 'Zero', 'Set zero'), ('limit', 'Limit', 'Clamp to 0.9')], name = "Metallic clamping", description="TODO.", default="ignore") + tlm_texture_interpolation : EnumProperty( + items = [('Smart', 'Smart', 'Bicubic when magnifying.'), + ('Cubic', 'Cubic', 'Cubic interpolation'), + ('Closest', 'Closest', 'No interpolation'), + ('Linear', 'Linear', 'Linear')], + name = "Texture interpolation", + description="Texture interpolation.", + default="Linear") + + tlm_texture_extrapolation : EnumProperty( + items = [('REPEAT', 'Repeat', 'Repeat in both direction.'), + ('EXTEND', 'Extend', 'Extend by repeating edge pixels.'), + ('CLIP', 'Clip', 'Clip to image size')], + name = "Texture extrapolation", + description="Texture extrapolation.", + default="EXTEND") + tlm_verbose : BoolProperty( name="Verbose", description="Verbose console output", @@ -409,4 +446,52 @@ class TLM_SceneProperties(bpy.types.PropertyGroup): ('CYCLES', 'Cycles', 'TODO')], name = "Probe Render Engine", description="TODO", - default='BLENDER_EEVEE') \ No newline at end of file + default='BLENDER_EEVEE') + + tlm_load_folder : StringProperty( + name="Load Folder", + description="Load existing lightmaps from folder", + subtype="DIR_PATH") + + tlm_utility_set : EnumProperty( + items = [('Scene', 'Scene', 'Set for all objects in the scene.'), + ('Selection', 'Selection', 'Set for selected objects.'), + ('Enabled', 'Enabled', 'Set for objects that has been enabled for lightmapping.')], + name = "Set", + description="Utility selection set", + default='Scene') + + tlm_resolution_weight : EnumProperty( + items = [('Single', 'Single', 'Set a single resolution for all objects.'), + ('Dimension', 'Dimension', 'Distribute resolutions based on object dimensions.'), + ('Surface', 'Surface', 'Distribute resolutions based on mesh surface area.'), + ('Volume', 'Volume', 'Distribute resolutions based on mesh volume.')], + name = "Resolution weight", + description="Method for setting resolution value", + default='Single') + #Todo add vertex color option + + tlm_resolution_min : EnumProperty( + items = [('32', '32', 'TODO'), + ('64', '64', 'TODO'), + ('128', '128', 'TODO'), + ('256', '256', 'TODO'), + ('512', '512', 'TODO'), + ('1024', '1024', 'TODO'), + ('2048', '2048', 'TODO'), + ('4096', '4096', 'TODO')], + name = "Minimum resolution", + description="Minimum distributed resolution", + default='32') + + tlm_resolution_max : EnumProperty( + items = [('64', '64', 'TODO'), + ('128', '128', 'TODO'), + ('256', '256', 'TODO'), + ('512', '512', 'TODO'), + ('1024', '1024', 'TODO'), + ('2048', '2048', 'TODO'), + ('4096', '4096', 'TODO')], + name = "Maximum resolution", + description="Maximum distributed resolution", + default='256') \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/build.py b/blender/arm/lightmapper/utility/build.py index a8ad80dd..891ebcf4 100644 --- a/blender/arm/lightmapper/utility/build.py +++ b/blender/arm/lightmapper/utility/build.py @@ -1,17 +1,20 @@ import bpy, os, subprocess, sys, platform, aud, json, datetime, socket -import threading + from . import encoding, pack from . cycles import lightmap, prepare, nodes, cache +from . luxcore import setup +from . octane import configure, lightmap2 from . denoiser import integrated, oidn, optix from . filtering import opencv +from . gui import Viewport from .. network import client + from os import listdir from os.path import isfile, join from time import time, sleep from importlib import util previous_settings = {} - postprocess_shutdown = False def prepare_build(self=0, background_mode=False, shutdown_after_build=False): @@ -21,15 +24,31 @@ def prepare_build(self=0, background_mode=False, shutdown_after_build=False): print("Building lightmaps") + if bpy.context.scene.TLM_EngineProperties.tlm_lighting_mode == "combinedao": + + scene = bpy.context.scene + + if not "tlm_plus_mode" in bpy.app.driver_namespace or bpy.app.driver_namespace["tlm_plus_mode"] == 0: + filepath = bpy.data.filepath + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), scene.TLM_EngineProperties.tlm_lightmap_savedir) + if os.path.isdir(dirpath): + for file in os.listdir(dirpath): + os.remove(os.path.join(dirpath + "/" + file)) + bpy.app.driver_namespace["tlm_plus_mode"] = 1 + print("Plus Mode") + if bpy.context.scene.TLM_EngineProperties.tlm_bake_mode == "Foreground" or background_mode==True: global start_time start_time = time() + bpy.app.driver_namespace["tlm_start_time"] = time() scene = bpy.context.scene sceneProperties = scene.TLM_SceneProperties - #Timer start here bound to global + if not background_mode and bpy.context.scene.TLM_EngineProperties.tlm_lighting_mode != "combinedao": + #pass + setGui(1) if check_save(): print("Please save your file first") @@ -52,12 +71,6 @@ def prepare_build(self=0, background_mode=False, shutdown_after_build=False): self.report({'INFO'}, "Error:Filtering - OpenCV not installed") return{'FINISHED'} - #TODO DO some resolution change - #if checkAtlasSize(): - # print("Error: AtlasGroup overflow") - # self.report({'INFO'}, "Error: AtlasGroup overflow - Too many objects") - # return{'FINISHED'} - setMode() dirpath = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) @@ -67,19 +80,6 @@ def prepare_build(self=0, background_mode=False, shutdown_after_build=False): #Naming check naming_check() - # if sceneProperties.tlm_reset_uv or sceneProperties.tlm_atlas_mode == "Postpack": - # for obj in bpy.data.objects: - # if obj.type == "MESH": - # uv_layers = obj.data.uv_layers - - - - #for uvlayer in uv_layers: - # if uvlayer.name == "UVMap_Lightmap": - # uv_layers.remove(uvlayer) - - ## RENDER DEPENDENCY FROM HERE - if sceneProperties.tlm_lightmap_engine == "Cycles": prepare.init(self, previous_settings) @@ -90,18 +90,14 @@ def prepare_build(self=0, background_mode=False, shutdown_after_build=False): if sceneProperties.tlm_lightmap_engine == "OctaneRender": - pass - - #Renderer - Store settings - - #Renderer - Set settings - - #Renderer - Config objects, lights, world + configure.init(self, previous_settings) begin_build() else: + print("Baking in background") + filepath = bpy.data.filepath bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath) @@ -111,22 +107,7 @@ def prepare_build(self=0, background_mode=False, shutdown_after_build=False): scene = bpy.context.scene sceneProperties = scene.TLM_SceneProperties - #We dynamically load the renderer and denoiser, instead of loading something we don't use - - if sceneProperties.tlm_lightmap_engine == "Cycles": - - pass - - if sceneProperties.tlm_lightmap_engine == "LuxCoreRender": - - pass - - if sceneProperties.tlm_lightmap_engine == "OctaneRender": - - pass - #Timer start here bound to global - if check_save(): print("Please save your file first") self.report({'INFO'}, "Please save your file first") @@ -168,23 +149,12 @@ def prepare_build(self=0, background_mode=False, shutdown_after_build=False): client.connect_client(HOST, PORT, bpy.data.filepath, 0) - # with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - # s.connect((HOST, PORT)) - # message = { - # "call" : 1, - # "command" : 1, - # "enquiry" : 0, - # "args" : bpy.data.filepath - # } - - # s.sendall(json.dumps(message).encode()) - # data = s.recv(1024) - # print(data.decode()) - finish_assemble() else: + print("Background driver process") + bpy.app.driver_namespace["alpha"] = 0 bpy.app.driver_namespace["tlm_process"] = False @@ -196,6 +166,8 @@ def prepare_build(self=0, background_mode=False, shutdown_after_build=False): def distribute_building(): + print("Distributing lightmap building") + #CHECK IF THERE'S AN EXISTING SUBPROCESS if not os.path.isfile(os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir, "process.tlm")): @@ -215,8 +187,16 @@ def distribute_building(): with open(os.path.join(write_directory, "process.tlm"), 'w') as file: json.dump(process_status, file, indent=2) - bpy.app.driver_namespace["tlm_process"] = subprocess.Popen([sys.executable,"-b", blendPath,"--python-expr",'import bpy; import thelightmapper; thelightmapper.addon.utility.build.prepare_build(0, True);'], shell=False, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) - + if (2, 91, 0) > bpy.app.version: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + bpy.app.driver_namespace["tlm_process"] = subprocess.Popen([sys.executable,"-b", blendPath,"--python-expr",'import bpy; import thelightmapper; thelightmapper.addon.utility.build.prepare_build(0, True);'], shell=False, stdout=subprocess.PIPE) + else: + bpy.app.driver_namespace["tlm_process"] = subprocess.Popen([sys.executable,"-b", blendPath,"--python-expr",'import bpy; import thelightmapper; thelightmapper.addon.utility.build.prepare_build(0, True);'], shell=False, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) + else: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + bpy.app.driver_namespace["tlm_process"] = subprocess.Popen([bpy.app.binary_path,"-b", blendPath,"--python-expr",'import bpy; import thelightmapper; thelightmapper.addon.utility.build.prepare_build(0, True);'], shell=False, stdout=subprocess.PIPE) + else: + bpy.app.driver_namespace["tlm_process"] = subprocess.Popen([bpy.app.binary_path,"-b", blendPath,"--python-expr",'import bpy; import thelightmapper; thelightmapper.addon.utility.build.prepare_build(0, True);'], shell=False, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) if bpy.context.scene.TLM_SceneProperties.tlm_verbose: print("Started process: " + str(bpy.app.driver_namespace["tlm_process"]) + " at " + str(datetime.datetime.now())) @@ -269,6 +249,10 @@ def finish_assemble(self=0): if sceneProperties.tlm_lightmap_engine == "OctaneRender": pass + if not 'start_time' in globals(): + global start_time + start_time = time() + manage_build(True) def begin_build(): @@ -288,7 +272,8 @@ def begin_build(): pass if sceneProperties.tlm_lightmap_engine == "OctaneRender": - pass + + lightmap2.bake() #Denoiser if sceneProperties.tlm_denoise_use: @@ -429,7 +414,7 @@ def begin_build(): print("Encoding:" + str(file)) encoding.encodeImageRGBMCPU(img, sceneProperties.tlm_encoding_range, dirpath, 0) - if sceneProperties.tlm_encoding_mode_b == "RGBD": + if sceneProperties.tlm_encoding_mode_a == "RGBD": if bpy.context.scene.TLM_SceneProperties.tlm_verbose: print("ENCODING RGBD") @@ -455,6 +440,36 @@ def begin_build(): print("Encoding:" + str(file)) encoding.encodeImageRGBDCPU(img, sceneProperties.tlm_encoding_range, dirpath, 0) + if sceneProperties.tlm_encoding_mode_a == "SDR": + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("EXR Format") + + ren = bpy.context.scene.render + ren.image_settings.file_format = "PNG" + #ren.image_settings.exr_codec = "scene.TLM_SceneProperties.tlm_exr_codec" + + end = "_baked" + + baked_image_array = [] + + if sceneProperties.tlm_denoise_use: + + end = "_denoised" + + if sceneProperties.tlm_filtering_use: + + end = "_filtered" + + #For each image in folder ending in denoised/filtered + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + for file in dirfiles: + if file.endswith(end + ".hdr"): + + img = bpy.data.images.load(os.path.join(dirpath,file)) + img.save_render(img.filepath_raw[:-4] + ".png") + else: if sceneProperties.tlm_encoding_mode_b == "HDR": @@ -562,6 +577,33 @@ def begin_build(): print("Encoding:" + str(file)) encoding.encodeImageRGBDGPU(img, sceneProperties.tlm_encoding_range, dirpath, 0) + if sceneProperties.tlm_encoding_mode_b == "PNG": + + ren = bpy.context.scene.render + ren.image_settings.file_format = "PNG" + #ren.image_settings.exr_codec = "scene.TLM_SceneProperties.tlm_exr_codec" + + end = "_baked" + + baked_image_array = [] + + if sceneProperties.tlm_denoise_use: + + end = "_denoised" + + if sceneProperties.tlm_filtering_use: + + end = "_filtered" + + #For each image in folder ending in denoised/filtered + dirfiles = [f for f in listdir(dirpath) if isfile(join(dirpath, f))] + + for file in dirfiles: + if file.endswith(end + ".hdr"): + + img = bpy.data.images.load(os.path.join(dirpath,file)) + img.save_render(img.filepath_raw[:-4] + ".png") + manage_build() def manage_build(background_pass=False): @@ -610,6 +652,10 @@ def manage_build(background_pass=False): formatEnc = "_encoded.png" + if sceneProperties.tlm_encoding_mode_a == "SDR": + + formatEnc = ".png" + else: print("GPU Encoding") @@ -632,6 +678,10 @@ def manage_build(background_pass=False): formatEnc = "_encoded.png" + if sceneProperties.tlm_encoding_mode_b == "SDR": + + formatEnc = ".png" + if not background_pass: nodes.exchangeLightmapsToPostfix("_baked", end, formatEnc) @@ -653,13 +703,13 @@ def manage_build(background_pass=False): filepath = bpy.data.filepath dirpath = os.path.join(os.path.dirname(bpy.data.filepath), scene.TLM_EngineProperties.tlm_lightmap_savedir) - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: cache.backup_material_restore(obj) - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: cache.backup_material_rename(obj) @@ -672,77 +722,181 @@ def manage_build(background_pass=False): if "_Original" in mat.name: bpy.data.materials.remove(mat) - for obj in bpy.data.objects: - - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: - img_name = obj.name + '_baked' - Lightmapimage = bpy.data.images[img_name] - obj["Lightmap"] = Lightmapimage.filepath_raw + + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + atlasName = obj.TLM_ObjectProperties.tlm_atlas_pointer + img_name = atlasName + '_baked' + Lightmapimage = bpy.data.images[img_name] + obj["Lightmap"] = Lightmapimage.filepath_raw + elif obj.TLM_ObjectProperties.tlm_postpack_object: + atlasName = obj.TLM_ObjectProperties.tlm_postatlas_pointer + img_name = atlasName + '_baked' + ".hdr" + Lightmapimage = bpy.data.images[img_name] + obj["Lightmap"] = Lightmapimage.filepath_raw + else: + img_name = obj.name + '_baked' + Lightmapimage = bpy.data.images[img_name] + obj["Lightmap"] = Lightmapimage.filepath_raw for image in bpy.data.images: if image.name.endswith("_baked"): bpy.data.images.remove(image, do_unlink=True) - total_time = sec_to_hours((time() - start_time)) - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print(total_time) + if "tlm_plus_mode" in bpy.app.driver_namespace: #First DIR pass - bpy.context.scene["TLM_Buildstat"] = total_time + if bpy.app.driver_namespace["tlm_plus_mode"] == 1: #First DIR pass - reset_settings(previous_settings["settings"]) + filepath = bpy.data.filepath + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), scene.TLM_EngineProperties.tlm_lightmap_savedir) - if sceneProperties.tlm_lightmap_engine == "LuxCoreRender": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + cache.backup_material_restore(obj) - pass + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + cache.backup_material_rename(obj) - if sceneProperties.tlm_lightmap_engine == "OctaneRender": + for mat in bpy.data.materials: + if mat.users < 1: + bpy.data.materials.remove(mat) - pass + for mat in bpy.data.materials: + if mat.name.startswith("."): + if "_Original" in mat.name: + bpy.data.materials.remove(mat) - if bpy.context.scene.TLM_EngineProperties.tlm_bake_mode == "Background": - pass + for image in bpy.data.images: + if image.name.endswith("_baked"): + bpy.data.images.remove(image, do_unlink=True) - if scene.TLM_SceneProperties.tlm_alert_on_finish: + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) - alertSelect = scene.TLM_SceneProperties.tlm_alert_sound + files = os.listdir(dirpath) + + for index, file in enumerate(files): + + filename = extension = os.path.splitext(file)[0] + extension = os.path.splitext(file)[1] + + os.rename(os.path.join(dirpath, file), os.path.join(dirpath, filename + "_dir" + extension)) + + print("First DIR pass complete") + + bpy.app.driver_namespace["tlm_plus_mode"] = 2 + + prepare_build(self=0, background_mode=False, shutdown_after_build=False) + + if not background_pass and bpy.context.scene.TLM_EngineProperties.tlm_lighting_mode != "combinedao": + #pass + setGui(0) + + elif bpy.app.driver_namespace["tlm_plus_mode"] == 2: + + filepath = bpy.data.filepath + + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + + files = os.listdir(dirpath) + + for index, file in enumerate(files): + + filename = os.path.splitext(file)[0] + extension = os.path.splitext(file)[1] + + if not filename.endswith("_dir"): + os.rename(os.path.join(dirpath, file), os.path.join(dirpath, filename + "_ao" + extension)) + + print("Second AO pass complete") + + total_time = sec_to_hours((time() - start_time)) + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(total_time) + + bpy.context.scene["TLM_Buildstat"] = total_time + + reset_settings(previous_settings["settings"]) + + bpy.app.driver_namespace["tlm_plus_mode"] = 0 + + if not background_pass: + + #TODO CHANGE! + + nodes.exchangeLightmapsToPostfix(end, end + "_dir", formatEnc) + + nodes.applyAOPass() - if alertSelect == "dash": - soundfile = "dash.ogg" - elif alertSelect == "pingping": - soundfile = "pingping.ogg" - elif alertSelect == "gentle": - soundfile = "gentle.ogg" else: - soundfile = "noot.ogg" - scriptDir = os.path.dirname(os.path.realpath(__file__)) - sound_path = os.path.abspath(os.path.join(scriptDir, '..', 'assets/'+soundfile)) + total_time = sec_to_hours((time() - start_time)) + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(total_time) - device = aud.Device() - sound = aud.Sound.file(sound_path) - device.play(sound) + bpy.context.scene["TLM_Buildstat"] = total_time - print("Lightmap building finished") + reset_settings(previous_settings["settings"]) - if bpy.app.background: + print("Lightmap building finished") - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Writing background process report") - - write_directory = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + if sceneProperties.tlm_lightmap_engine == "LuxCoreRender": - if os.path.exists(os.path.join(write_directory, "process.tlm")): + pass - process_status = json.loads(open(os.path.join(write_directory, "process.tlm")).read()) + if sceneProperties.tlm_lightmap_engine == "OctaneRender": - process_status[1]["completed"] = True + pass - with open(os.path.join(write_directory, "process.tlm"), 'w') as file: - json.dump(process_status, file, indent=2) + if bpy.context.scene.TLM_EngineProperties.tlm_bake_mode == "Background": + pass - if postprocess_shutdown: - sys.exit() + if not background_pass and bpy.context.scene.TLM_EngineProperties.tlm_lighting_mode != "combinedao": + #pass + setGui(0) + + if scene.TLM_SceneProperties.tlm_alert_on_finish: + + alertSelect = scene.TLM_SceneProperties.tlm_alert_sound + + if alertSelect == "dash": + soundfile = "dash.ogg" + elif alertSelect == "pingping": + soundfile = "pingping.ogg" + elif alertSelect == "gentle": + soundfile = "gentle.ogg" + else: + soundfile = "noot.ogg" + + scriptDir = os.path.dirname(os.path.realpath(__file__)) + sound_path = os.path.abspath(os.path.join(scriptDir, '..', 'assets/'+soundfile)) + + device = aud.Device() + sound = aud.Sound.file(sound_path) + device.play(sound) + + if bpy.app.background: + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Writing background process report") + + write_directory = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + + if os.path.exists(os.path.join(write_directory, "process.tlm")): + + process_status = json.loads(open(os.path.join(write_directory, "process.tlm")).read()) + + process_status[1]["completed"] = True + + with open(os.path.join(write_directory, "process.tlm"), 'w') as file: + json.dump(process_status, file, indent=2) + + if postprocess_shutdown: + sys.exit() #TODO - SET BELOW TO UTILITY @@ -770,9 +924,8 @@ def reset_settings(prev_settings): def naming_check(): - for obj in bpy.data.objects: - - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: @@ -829,6 +982,9 @@ def check_save(): def check_denoiser(): + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Checking denoiser path") + scene = bpy.context.scene if scene.TLM_SceneProperties.tlm_denoise_use: @@ -847,8 +1003,8 @@ def check_denoiser(): return 0 def check_materials(): - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: for slot in obj.material_slots: mat = slot.material @@ -868,15 +1024,39 @@ def check_materials(): def sec_to_hours(seconds): a=str(seconds//3600) b=str((seconds%3600)//60) - c=str((seconds%3600)%60) + c=str(round((seconds%3600)%60,1)) d=["{} hours {} mins {} seconds".format(a, b, c)] return d def setMode(): + + obj = bpy.context.scene.objects[0] + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.mode_set(mode='OBJECT') #TODO Make some checks that returns to previous selection +def setGui(mode): + + if mode == 0: + + context = bpy.context + driver = bpy.app.driver_namespace + + if "TLM_UI" in driver: + driver["TLM_UI"].remove_handle() + + if mode == 1: + + #bpy.context.area.tag_redraw() + context = bpy.context + driver = bpy.app.driver_namespace + driver["TLM_UI"] = Viewport.ViewportDraw(context, "Building Lightmaps") + + bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) + def checkAtlasSize(): overflow = False @@ -897,7 +1077,7 @@ def checkAtlasSize(): utilized = 0 atlasUsedArea = 0 - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: if obj.TLM_ObjectProperties.tlm_postpack_object: if obj.TLM_ObjectProperties.tlm_postatlas_pointer == atlas.name: @@ -912,4 +1092,5 @@ def checkAtlasSize(): if overflow == True: return True else: - return False \ No newline at end of file + return False + diff --git a/blender/arm/lightmapper/utility/cycles/cache.py b/blender/arm/lightmapper/utility/cycles/cache.py index 50542c6e..07f068b7 100644 --- a/blender/arm/lightmapper/utility/cycles/cache.py +++ b/blender/arm/lightmapper/utility/cycles/cache.py @@ -2,7 +2,6 @@ import bpy #Todo - Check if already exists, in case multiple objects has the same material - def backup_material_copy(slot): material = slot.material dup = material.copy() @@ -16,25 +15,49 @@ def backup_material_cache_restore(slot, path): if bpy.context.scene.TLM_SceneProperties.tlm_verbose: print("Restore cache") -def backup_material_rename(obj): - if "TLM_PrevMatArray" in obj: - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Renaming material for: " + obj.name) +# def backup_material_restore(obj): #?? +# if bpy.context.scene.TLM_SceneProperties.tlm_verbose: +# print("Restoring material for: " + obj.name) - for slot in obj.material_slots: +#Check if object has TLM_PrevMatArray +# if yes +# - check if array.len is bigger than 0: +# if yes: +# for slot in object: +# originalMaterial = TLM_PrevMatArray[index] +# +# +# if no: +# - In which cases are these? - if slot.material is not None: - if slot.material.name.endswith("_Original"): - newname = slot.material.name[1:-9] - if newname in bpy.data.materials: - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Removing material: " + bpy.data.materials[newname].name) - bpy.data.materials.remove(bpy.data.materials[newname]) - slot.material.name = newname +# if no: +# - In which cases are there not? +# - If a lightmapped material was applied to a non-lightmap object? - del obj["TLM_PrevMatArray"] -def backup_material_restore(obj): + # if bpy.data.materials[originalMaterial].users > 0: #TODO - Check if all lightmapped + + # print("Material has multiple users") + + # if originalMaterial in bpy.data.materials: + # slot.material = bpy.data.materials[originalMaterial] + # slot.material.use_fake_user = False + # elif "." + originalMaterial + "_Original" in bpy.data.materials: + # slot.material = bpy.data.materials["." + originalMaterial + "_Original"] + # slot.material.use_fake_user = False + + # else: + + # print("Material has one user") + + # if "." + originalMaterial + "_Original" in bpy.data.materials: + # slot.material = bpy.data.materials["." + originalMaterial + "_Original"] + # slot.material.use_fake_user = False + # elif originalMaterial in bpy.data.materials: + # slot.material = bpy.data.materials[originalMaterial] + # slot.material.use_fake_user = False + +def backup_material_restore(obj): #?? if bpy.context.scene.TLM_SceneProperties.tlm_verbose: print("Restoring material for: " + obj.name) @@ -59,9 +82,43 @@ def backup_material_restore(obj): originalMaterial = "" if slot.material is not None: - #slot.material.user_clear() Seems to be bad; See: https://developer.blender.org/T49837 - bpy.data.materials.remove(slot.material) + #if slot.material.users < 2: + #slot.material.user_clear() #Seems to be bad; See: https://developer.blender.org/T49837 + #bpy.data.materials.remove(slot.material) if "." + originalMaterial + "_Original" in bpy.data.materials: slot.material = bpy.data.materials["." + originalMaterial + "_Original"] - slot.material.use_fake_user = False \ No newline at end of file + slot.material.use_fake_user = False + + else: + + print("No previous material for " + obj.name) + + else: + + print("No previous material for " + obj.name) + +def backup_material_rename(obj): #?? + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Renaming material for: " + obj.name) + + + if "TLM_PrevMatArray" in obj: + + for slot in obj.material_slots: + + if slot.material is not None: + if slot.material.name.endswith("_Original"): + newname = slot.material.name[1:-9] + if newname in bpy.data.materials: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Removing material: " + bpy.data.materials[newname].name) + #if bpy.data.materials[newname].users < 2: + #bpy.data.materials.remove(bpy.data.materials[newname]) #TODO - Maybe remove this + slot.material.name = newname + + del obj["TLM_PrevMatArray"] + + else: + + print("No Previous material array for: " + obj.name) \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/cycles/lightmap.py b/blender/arm/lightmapper/utility/cycles/lightmap.py index 8588bee8..0d0c56e1 100644 --- a/blender/arm/lightmapper/utility/cycles/lightmap.py +++ b/blender/arm/lightmapper/utility/cycles/lightmap.py @@ -1,25 +1,26 @@ import bpy, os +from .. import build +from time import time, sleep -def bake(): +def bake(plus_pass=0): - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: bpy.ops.object.select_all(action='DESELECT') obj.select_set(False) iterNum = 0 currentIterNum = 0 - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: iterNum = iterNum + 1 if iterNum > 1: iterNum = iterNum - 1 - for obj in bpy.data.objects: - if obj.type == 'MESH': - + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: scene = bpy.context.scene @@ -32,19 +33,45 @@ def bake(): obj.hide_render = False scene.render.bake.use_clear = False - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Baking " + str(currentIterNum) + "/" + str(iterNum) + " (" + str(round(currentIterNum/iterNum*100, 2)) + "%) : " + obj.name) + #os.system("cls") - if scene.TLM_EngineProperties.tlm_lighting_mode == "combined" or scene.TLM_EngineProperties.tlm_lighting_mode == "combinedAO": + #if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Baking " + str(currentIterNum) + "/" + str(iterNum) + " (" + str(round(currentIterNum/iterNum*100, 2)) + "%) : " + obj.name) + #elapsed = build.sec_to_hours((time() - bpy.app.driver_namespace["tlm_start_time"])) + #print("Baked: " + str(currentIterNum) + " | Left: " + str(iterNum-currentIterNum)) + elapsedSeconds = time() - bpy.app.driver_namespace["tlm_start_time"] + bakedObjects = currentIterNum + bakedLeft = iterNum-currentIterNum + if bakedObjects == 0: + bakedObjects = 1 + averagePrBake = elapsedSeconds / bakedObjects + remaining = averagePrBake * bakedLeft + #print(time() - bpy.app.driver_namespace["tlm_start_time"]) + print("Elapsed time: " + str(round(elapsedSeconds, 2)) + "s | ETA remaining: " + str(round(remaining, 2)) + "s") #str(elapsed[0]) + #print("Averaged: " + str(averagePrBake)) + #print("Remaining: " + str(remaining)) + + if scene.TLM_EngineProperties.tlm_target == "vertex": + scene.render.bake_target = "VERTEX_COLORS" + + if scene.TLM_EngineProperties.tlm_lighting_mode == "combined": bpy.ops.object.bake(type="DIFFUSE", pass_filter={"DIRECT","INDIRECT"}, margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) - elif scene.TLM_EngineProperties.tlm_lighting_mode == "indirect" or scene.TLM_EngineProperties.tlm_lighting_mode == "indirectAO": + elif scene.TLM_EngineProperties.tlm_lighting_mode == "indirect": bpy.ops.object.bake(type="DIFFUSE", pass_filter={"INDIRECT"}, margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) elif scene.TLM_EngineProperties.tlm_lighting_mode == "ao": bpy.ops.object.bake(type="AO", margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) + elif scene.TLM_EngineProperties.tlm_lighting_mode == "combinedao": + + if bpy.app.driver_namespace["tlm_plus_mode"] == 1: + bpy.ops.object.bake(type="DIFFUSE", pass_filter={"DIRECT","INDIRECT"}, margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) + elif bpy.app.driver_namespace["tlm_plus_mode"] == 2: + bpy.ops.object.bake(type="AO", margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) + elif scene.TLM_EngineProperties.tlm_lighting_mode == "complete": - bpy.ops.object.bake(type="COMBINED", margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) + bpy.ops.object.bake(type="COMBINED", margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) else: bpy.ops.object.bake(type="DIFFUSE", pass_filter={"DIRECT","INDIRECT"}, margin=scene.TLM_EngineProperties.tlm_dilation_margin, use_clear=False) + bpy.ops.object.select_all(action='DESELECT') currentIterNum = currentIterNum + 1 diff --git a/blender/arm/lightmapper/utility/cycles/nodes.py b/blender/arm/lightmapper/utility/cycles/nodes.py index bb42a895..9eb24d88 100644 --- a/blender/arm/lightmapper/utility/cycles/nodes.py +++ b/blender/arm/lightmapper/utility/cycles/nodes.py @@ -1,8 +1,8 @@ import bpy, os def apply_lightmaps(): - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: for slot in obj.material_slots: mat = slot.material @@ -32,8 +32,12 @@ def apply_lightmaps(): def apply_materials(): - for obj in bpy.data.objects: - if obj.type == "MESH": + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Applying materials") + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: uv_layers = obj.data.uv_layers @@ -90,7 +94,7 @@ def apply_materials(): for node in nodes: if node.name == "Baked Image": lightmapNode = node - lightmapNode.location = -800, 300 + lightmapNode.location = -1200, 300 lightmapNode.name = "TLM_Lightmap" foundBakedNode = True @@ -98,9 +102,10 @@ def apply_materials(): if not foundBakedNode: lightmapNode = node_tree.nodes.new(type="ShaderNodeTexImage") - lightmapNode.location = -300, 300 + lightmapNode.location = -1200, 300 lightmapNode.name = "TLM_Lightmap" - lightmapNode.interpolation = "Smart" + lightmapNode.interpolation = bpy.context.scene.TLM_SceneProperties.tlm_texture_interpolation + lightmapNode.extension = bpy.context.scene.TLM_SceneProperties.tlm_texture_extrapolation if (obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA" and obj.TLM_ObjectProperties.tlm_atlas_pointer != ""): lightmapNode.image = bpy.data.images[obj.TLM_ObjectProperties.tlm_atlas_pointer + "_baked"] @@ -118,37 +123,32 @@ def apply_materials(): #Find mainnode mainNode = outputNode.inputs[0].links[0].from_node - #Clamp metallic - - # if scene.TLM_SceneProperties.tlm_metallic_clamp != "ignore": - # if mainNode.type == "BSDF_PRINCIPLED": - - # if len(mainNode.inputs[4].links) == 0: - - # if scene.TLM_SceneProperties.tlm_metallic_clamp == "zero": - # mainNode.inputs[4].default_value = 0.0 - # else: - # mainNode.inputs[4].default_value = 0.99 - - # else: - - # pass - #Add all nodes first #Add lightmap multipliction texture mixNode = node_tree.nodes.new(type="ShaderNodeMixRGB") mixNode.name = "Lightmap_Multiplication" - mixNode.location = -300, 300 + mixNode.location = -800, 300 if scene.TLM_EngineProperties.tlm_lighting_mode == "indirect" or scene.TLM_EngineProperties.tlm_lighting_mode == "indirectAO": mixNode.blend_type = 'ADD' else: mixNode.blend_type = 'MULTIPLY' - mixNode.inputs[0].default_value = 1.0 + + if scene.TLM_EngineProperties.tlm_lighting_mode == "complete": + mixNode.inputs[0].default_value = 0.0 + else: + mixNode.inputs[0].default_value = 1.0 UVLightmap = node_tree.nodes.new(type="ShaderNodeUVMap") - UVLightmap.uv_map = "UVMap_Lightmap" + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + UVLightmap.uv_map = uv_channel + UVLightmap.name = "Lightmap_UV" - UVLightmap.location = -1000, 300 + UVLightmap.location = -1500, 300 if(scene.TLM_SceneProperties.tlm_decoder_setup): if scene.TLM_SceneProperties.tlm_encoding_device == "CPU": @@ -196,7 +196,7 @@ def apply_materials(): baseColorValue = mainNode.inputs[0].default_value baseColorNode = node_tree.nodes.new(type="ShaderNodeRGB") baseColorNode.outputs[0].default_value = baseColorValue - baseColorNode.location = ((mainNode.location[0] - 500, mainNode.location[1] - 300)) + baseColorNode.location = ((mainNode.location[0] - 1100, mainNode.location[1] - 300)) baseColorNode.name = "Lightmap_BasecolorNode_A" else: baseColorNode = mainNode.inputs[0].links[0].from_node @@ -235,13 +235,19 @@ def apply_materials(): mat.node_tree.links.new(mixNode.outputs[0], mainNode.inputs[0]) #Connect mixnode to pbr node mat.node_tree.links.new(UVLightmap.outputs[0], lightmapNode.inputs[0]) #Connect uvnode to lightmapnode + #If skip metallic + if scene.TLM_SceneProperties.tlm_metallic_clamp == "skip": + if mainNode.inputs[4].default_value > 0.1: #DELIMITER + moutput = mainNode.inputs[0].links[0].from_node + mat.node_tree.links.remove(moutput.outputs[0].links[0]) + def exchangeLightmapsToPostfix(ext_postfix, new_postfix, formatHDR=".hdr"): if bpy.context.scene.TLM_SceneProperties.tlm_verbose: print(ext_postfix, new_postfix, formatHDR) - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: for slot in obj.material_slots: mat = slot.material @@ -267,6 +273,53 @@ def exchangeLightmapsToPostfix(ext_postfix, new_postfix, formatHDR=".hdr"): for image in bpy.data.images: image.reload() +def applyAOPass(): + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + for slot in obj.material_slots: + mat = slot.material + node_tree = mat.node_tree + nodes = mat.node_tree.nodes + + for node in nodes: + if node.name == "Baked Image" or node.name == "TLM_Lightmap": + + filepath = bpy.data.filepath + dirpath = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + + LightmapPath = node.image.filepath_raw + + filebase = os.path.basename(LightmapPath) + filename = os.path.splitext(filebase)[0] + extension = os.path.splitext(filebase)[1] + AOImagefile = filename[:-4] + "_ao" + AOImagePath = os.path.join(dirpath, AOImagefile + extension) + + AOMap = nodes.new('ShaderNodeTexImage') + AOMap.name = "TLM_AOMap" + AOImage = bpy.data.images.load(AOImagePath) + AOMap.image = AOImage + AOMap.location = -800, 0 + + AOMult = nodes.new(type="ShaderNodeMixRGB") + AOMult.name = "TLM_AOMult" + AOMult.blend_type = 'MULTIPLY' + AOMult.inputs[0].default_value = 1.0 + AOMult.location = -300, 300 + + multyNode = nodes["Lightmap_Multiplication"] + mainNode = nodes["Principled BSDF"] + UVMapNode = nodes["Lightmap_UV"] + + node_tree.links.remove(multyNode.outputs[0].links[0]) + + node_tree.links.new(multyNode.outputs[0], AOMult.inputs[1]) + node_tree.links.new(AOMap.outputs[0], AOMult.inputs[2]) + node_tree.links.new(AOMult.outputs[0], mainNode.inputs[0]) + node_tree.links.new(UVMapNode.outputs[0], AOMap.inputs[0]) + def load_library(asset_name): scriptDir = os.path.dirname(os.path.realpath(__file__)) diff --git a/blender/arm/lightmapper/utility/cycles/prepare.py b/blender/arm/lightmapper/utility/cycles/prepare.py index 6e5bd70f..6222d18c 100644 --- a/blender/arm/lightmapper/utility/cycles/prepare.py +++ b/blender/arm/lightmapper/utility/cycles/prepare.py @@ -1,4 +1,4 @@ -import bpy +import bpy, math from . import cache from .. utility import * @@ -31,13 +31,16 @@ def configure_lights(): def configure_meshes(self): - for obj in bpy.data.objects: - if obj.type == "MESH": + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Configuring meshes") + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: cache.backup_material_restore(obj) - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: cache.backup_material_rename(obj) @@ -59,9 +62,18 @@ def configure_meshes(self): scene = bpy.context.scene - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + if scene.TLM_SceneProperties.tlm_apply_on_unwrap: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Applying transform to: " + obj.name) + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) + + obj.hide_select = False #Remember to toggle this back for slot in obj.material_slots: if "." + slot.name + '_Original' in bpy.data.materials: if bpy.context.scene.TLM_SceneProperties.tlm_verbose: @@ -77,33 +89,35 @@ def configure_meshes(self): bpy.ops.object.select_all(action='DESELECT') - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": uv_layers = obj.data.uv_layers - if not "UVMap_Lightmap" in uv_layers: + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + if not uv_channel in uv_layers: if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("UVMap made A") - uvmap = uv_layers.new(name="UVMap_Lightmap") + print("UV map created for object: " + obj.name) + uvmap = uv_layers.new(name=uv_channel) uv_layers.active_index = len(uv_layers) - 1 else: - print("Existing found...skipping") + print("Existing UV map found for object: " + obj.name) for i in range(0, len(uv_layers)): if uv_layers[i].name == 'UVMap_Lightmap': uv_layers.active_index = i - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Lightmap shift A") break atlas_items.append(obj) obj.select_set(True) - if scene.TLM_SceneProperties.tlm_apply_on_unwrap: - bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) - if atlasgroup.tlm_atlas_lightmap_unwrap_mode == "SmartProject": if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Smart Project A for: " + str(atlas_items)) + print("Atlasgroup Smart Project for: " + str(atlas_items)) for obj in atlas_items: print(obj.name + ": Active UV: " + obj.data.uv_layers[obj.data.uv_layers.active_index].name) @@ -112,7 +126,12 @@ def configure_meshes(self): bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') - bpy.ops.uv.smart_project(angle_limit=45.0, island_margin=atlasgroup.tlm_atlas_unwrap_margin, user_area_weight=1.0, use_aspect=True, stretch_to_bounds=False) + #API changes in 2.91 causes errors: + if (2, 91, 0) > bpy.app.version: + bpy.ops.uv.smart_project(angle_limit=45.0, island_margin=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin, user_area_weight=1.0, use_aspect=True, stretch_to_bounds=False) + else: + angle = math.radians(45.0) + bpy.ops.uv.smart_project(angle_limit=angle, island_margin=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin, area_weight=1.0, correct_aspect=True, scale_to_bounds=False) bpy.ops.mesh.select_all(action='DESELECT') bpy.ops.object.mode_set(mode='OBJECT') elif atlasgroup.tlm_atlas_lightmap_unwrap_mode == "Lightmap": @@ -123,8 +142,9 @@ def configure_meshes(self): bpy.ops.object.mode_set(mode='OBJECT') elif atlasgroup.tlm_atlas_lightmap_unwrap_mode == "Xatlas": + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Temporary skip: COPYING SMART PROJECT") + print("Using Xatlas on Atlas Group: " + atlas) for obj in atlas_items: obj.select_set(True) @@ -139,176 +159,213 @@ def configure_meshes(self): else: if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Copied Existing A") + print("Copied Existing UV Map for Atlas Group: " + atlas) - for obj in bpy.data.objects: - if obj.type == "MESH": + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: iterNum = iterNum + 1 - for obj in bpy.data.objects: - if obj.type == "MESH": - if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + for obj in bpy.context.scene.objects: + if obj.name in bpy.context.view_layer.objects: #Possible fix for view layer error + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: - objWasHidden = False + objWasHidden = False - #For some reason, a Blender bug might prevent invisible objects from being smart projected - #We will turn the object temporarily visible - obj.hide_viewport = False - obj.hide_set(False) + #For some reason, a Blender bug might prevent invisible objects from being smart projected + #We will turn the object temporarily visible + obj.hide_viewport = False + obj.hide_set(False) - currentIterNum = currentIterNum + 1 + currentIterNum = currentIterNum + 1 - #Configure selection - bpy.ops.object.select_all(action='DESELECT') - bpy.context.view_layer.objects.active = obj - obj.select_set(True) - obs = bpy.context.view_layer.objects - active = obs.active + #Configure selection + bpy.ops.object.select_all(action='DESELECT') + bpy.context.view_layer.objects.active = obj + obj.select_set(True) - #Provide material if none exists - preprocess_material(obj, scene) + obs = bpy.context.view_layer.objects + active = obs.active - #UV Layer management here - if not obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": - uv_layers = obj.data.uv_layers - if not "UVMap_Lightmap" in uv_layers: - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("UVMap made B") - uvmap = uv_layers.new(name="UVMap_Lightmap") - uv_layers.active_index = len(uv_layers) - 1 + #Provide material if none exists + preprocess_material(obj, scene) - #If lightmap - if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "Lightmap": - if scene.TLM_SceneProperties.tlm_apply_on_unwrap: - bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) - bpy.ops.uv.lightmap_pack('EXEC_SCREEN', PREF_CONTEXT='ALL_FACES', PREF_MARGIN_DIV=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin) - - #If smart project - elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "SmartProject": + #UV Layer management here + if not obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + uv_layers = obj.data.uv_layers + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + if not uv_channel in uv_layers: if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Smart Project B") - if scene.TLM_SceneProperties.tlm_apply_on_unwrap: - bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) - bpy.ops.object.select_all(action='DESELECT') - obj.select_set(True) - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.select_all(action='DESELECT') - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.uv.smart_project(angle_limit=45.0, island_margin=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin, user_area_weight=1.0, use_aspect=True, stretch_to_bounds=False) - - elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "Xatlas": + print("UV map created for obj: " + obj.name) + uvmap = uv_layers.new(name=uv_channel) + uv_layers.active_index = len(uv_layers) - 1 - if scene.TLM_SceneProperties.tlm_apply_on_unwrap: - bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) + #If lightmap + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "Lightmap": + bpy.ops.uv.lightmap_pack('EXEC_SCREEN', PREF_CONTEXT='ALL_FACES', PREF_MARGIN_DIV=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin) - #import blender_xatlas - #blender_xatlas.Unwrap_Lightmap_Group_Xatlas_2(bpy.context) + #If smart project + elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "SmartProject": - #bpy.ops.object.setup_unwrap() - Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj) - - elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": - - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("ATLAS GROUP: " + obj.TLM_ObjectProperties.tlm_atlas_pointer) - - else: #if copy existing - - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Copied Existing B") - - else: - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Existing found...skipping") - for i in range(0, len(uv_layers)): - if uv_layers[i].name == 'UVMap_Lightmap': - uv_layers.active_index = i if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Lightmap shift B") - break + print("Smart Project B") + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + #API changes in 2.91 causes errors: + if (2, 91, 0) > bpy.app.version: + bpy.ops.uv.smart_project(angle_limit=45.0, island_margin=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin, user_area_weight=1.0, use_aspect=True, stretch_to_bounds=False) + else: + angle = math.radians(45.0) + bpy.ops.uv.smart_project(angle_limit=angle, island_margin=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin, area_weight=1.0, correct_aspect=True, scale_to_bounds=False) + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.mode_set(mode='OBJECT') + + elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "Xatlas": + + Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj) - #Sort out nodes - for slot in obj.material_slots: + elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": - nodetree = slot.material.node_tree + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("ATLAS GROUP: " + obj.TLM_ObjectProperties.tlm_atlas_pointer) + + else: #if copy existing - outputNode = nodetree.nodes[0] #Presumed to be material output node + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Copied Existing UV Map for object: " + obj.name) - if(outputNode.type != "OUTPUT_MATERIAL"): + else: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Existing UV map found for obj: " + obj.name) + for i in range(0, len(uv_layers)): + if uv_layers[i].name == uv_channel: + uv_layers.active_index = i + break + + #print(x) + + #Sort out nodes + for slot in obj.material_slots: + + nodetree = slot.material.node_tree + + outputNode = nodetree.nodes[0] #Presumed to be material output node + + if(outputNode.type != "OUTPUT_MATERIAL"): + for node in nodetree.nodes: + if node.type == "OUTPUT_MATERIAL": + outputNode = node + break + + mainNode = outputNode.inputs[0].links[0].from_node + + if mainNode.type not in ['BSDF_PRINCIPLED','BSDF_DIFFUSE','GROUP']: + + #TODO! FIND THE PRINCIPLED PBR + self.report({'INFO'}, "The primary material node is not supported. Seeking first principled.") + + if len(find_node_by_type(nodetree.nodes, Node_Types.pbr_node)) > 0: + mainNode = find_node_by_type(nodetree.nodes, Node_Types.pbr_node)[0] + else: + self.report({'INFO'}, "No principled found. Seeking diffuse") + if len(find_node_by_type(nodetree.nodes, Node_Types.diffuse)) > 0: + mainNode = find_node_by_type(nodetree.nodes, Node_Types.diffuse)[0] + else: + self.report({'INFO'}, "No supported nodes. Continuing anyway.") + + if mainNode.type == 'GROUP': + if mainNode.node_tree != "Armory PBR": + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("The material group is not supported!") + + if (mainNode.type == "BSDF_PRINCIPLED"): + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("BSDF_Principled") + if scene.TLM_EngineProperties.tlm_directional_mode == "None": + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Directional mode") + if not len(mainNode.inputs[19].links) == 0: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("NOT LEN 0") + ninput = mainNode.inputs[19].links[0] + noutput = mainNode.inputs[19].links[0].from_node + nodetree.links.remove(noutput.outputs[0].links[0]) + + #Clamp metallic + if bpy.context.scene.TLM_SceneProperties.tlm_metallic_clamp == "limit": + MainMetNodeSocket = mainNode.inputs[4] + if not len(MainMetNodeSocket.links) == 0: + nodes = nodetree.nodes + MetClampNode = nodes.new('ShaderNodeClamp') + MetClampNode.location = (-200,150) + MetClampNode.inputs[2].default_value = 0.9 + minput = mainNode.inputs[4].links[0] #Metal input socket + moutput = mainNode.inputs[4].links[0].from_node #Metal output node + nodetree.links.remove(moutput.outputs[0].links[0]) #Works + nodetree.links.new(moutput.outputs[0], MetClampNode.inputs[0]) #minput node to clamp node + nodetree.links.new(MetClampNode.outputs[0],MainMetNodeSocket) #clamp node to metinput + else: + if mainNode.inputs[4].default_value > 0.9: + mainNode.inputs[4].default_value = 0.9 + elif bpy.context.scene.TLM_SceneProperties.tlm_metallic_clamp == "zero": + MainMetNodeSocket = mainNode.inputs[4] + if not len(MainMetNodeSocket.links) == 0: + nodes = nodetree.nodes + MetClampNode = nodes.new('ShaderNodeClamp') + MetClampNode.location = (-200,150) + MetClampNode.inputs[2].default_value = 0.0 + minput = mainNode.inputs[4].links[0] #Metal input socket + moutput = mainNode.inputs[4].links[0].from_node #Metal output node + nodetree.links.remove(moutput.outputs[0].links[0]) #Works + nodetree.links.new(moutput.outputs[0], MetClampNode.inputs[0]) #minput node to clamp node + nodetree.links.new(MetClampNode.outputs[0],MainMetNodeSocket) #clamp node to metinput + else: + mainNode.inputs[4].default_value = 0.0 + + if (mainNode.type == "BSDF_DIFFUSE"): + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("BSDF_Diffuse") + + # if (mainNode.type == "BSDF_DIFFUSE"): + # if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + # print("BSDF_Diffuse") + + #TODO FIX THIS PART! + #THIS IS USED IN CASES WHERE FOR SOME REASON THE USER FORGETS TO CONNECT SOMETHING INTO THE OUTPUT MATERIAL + for slot in obj.material_slots: + + nodetree = bpy.data.materials[slot.name].node_tree + nodes = nodetree.nodes + + #First search to get the first output material type for node in nodetree.nodes: if node.type == "OUTPUT_MATERIAL": - outputNode = node + mainNode = node break - mainNode = outputNode.inputs[0].links[0].from_node + #Fallback to get search + if not mainNode.type == "OUTPUT_MATERIAL": + mainNode = nodetree.nodes.get("Material Output") - if mainNode.type not in ['BSDF_PRINCIPLED','BSDF_DIFFUSE','GROUP']: + #Last resort to first node in list + if not mainNode.type == "OUTPUT_MATERIAL": + mainNode = nodetree.nodes[0].inputs[0].links[0].from_node - #TODO! FIND THE PRINCIPLED PBR - self.report({'INFO'}, "The primary material node is not supported. Seeking first principled.") + # for node in nodes: + # if "LM" in node.name: + # nodetree.links.new(node.outputs[0], mainNode.inputs[0]) - if len(find_node_by_type(nodetree.nodes, Node_Types.pbr_node)) > 0: - mainNode = find_node_by_type(nodetree.nodes, Node_Types.pbr_node)[0] - else: - self.report({'INFO'}, "No principled found. Seeking diffuse") - if len(find_node_by_type(nodetree.nodes, Node_Types.diffuse)) > 0: - mainNode = find_node_by_type(nodetree.nodes, Node_Types.diffuse)[0] - else: - self.report({'INFO'}, "No supported nodes. Continuing anyway.") - - if mainNode.type == 'GROUP': - if mainNode.node_tree != "Armory PBR": - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("The material group is not supported!") - - if (mainNode.type == "BSDF_PRINCIPLED"): - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("BSDF_Principled") - if scene.TLM_EngineProperties.tlm_directional_mode == "None": - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("Directional mode") - if not len(mainNode.inputs[19].links) == 0: - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("NOT LEN 0") - ninput = mainNode.inputs[19].links[0] - noutput = mainNode.inputs[19].links[0].from_node - nodetree.links.remove(noutput.outputs[0].links[0]) - - #Clamp metallic - if(mainNode.inputs[4].default_value == 1): - mainNode.inputs[4].default_value = 0.0 - - if (mainNode.type == "BSDF_DIFFUSE"): - if bpy.context.scene.TLM_SceneProperties.tlm_verbose: - print("BSDF_Diffuse") - - for slot in obj.material_slots: - - nodetree = bpy.data.materials[slot.name].node_tree - nodes = nodetree.nodes - - #First search to get the first output material type - for node in nodetree.nodes: - if node.type == "OUTPUT_MATERIAL": - mainNode = node - break - - #Fallback to get search - if not mainNode.type == "OUTPUT_MATERIAL": - mainNode = nodetree.nodes.get("Material Output") - - #Last resort to first node in list - if not mainNode.type == "OUTPUT_MATERIAL": - mainNode = nodetree.nodes[0].inputs[0].links[0].from_node - - for node in nodes: - if "LM" in node.name: - nodetree.links.new(node.outputs[0], mainNode.inputs[0]) - - for node in nodes: - if "Lightmap" in node.name: - nodes.remove(node) + # for node in nodes: + # if "Lightmap" in node.name: + # nodes.remove(node) def preprocess_material(obj, scene): if len(obj.material_slots) == 0: @@ -537,7 +594,7 @@ def store_existing(prev_container): selected = [] - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: if obj.select_get(): selected.append(obj.name) diff --git a/blender/arm/lightmapper/utility/denoiser/integrated.py b/blender/arm/lightmapper/utility/denoiser/integrated.py index e3061f30..38c1cabc 100644 --- a/blender/arm/lightmapper/utility/denoiser/integrated.py +++ b/blender/arm/lightmapper/utility/denoiser/integrated.py @@ -22,7 +22,7 @@ class TLM_Integrated_Denoise: bpy.ops.object.camera_add() #Just select the first camera we find, needed for the compositor - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: if obj.type == "CAMERA": bpy.context.scene.camera = obj return diff --git a/blender/arm/lightmapper/utility/encoding.py b/blender/arm/lightmapper/utility/encoding.py index 92a8e8d5..f716f342 100644 --- a/blender/arm/lightmapper/utility/encoding.py +++ b/blender/arm/lightmapper/utility/encoding.py @@ -332,6 +332,205 @@ def encodeImageRGBDGPU(image, maxRange, outDir, quality): #Todo - Find a way to save #bpy.ops.image.save_all_modified() +#TODO - FINISH THIS +def encodeImageRGBMGPU(image, maxRange, outDir, quality): + input_image = bpy.data.images[image.name] + image_name = input_image.name + + offscreen = gpu.types.GPUOffScreen(input_image.size[0], input_image.size[1]) + + image = input_image + + vertex_shader = ''' + + uniform mat4 ModelViewProjectionMatrix; + + in vec2 texCoord; + in vec2 pos; + out vec2 texCoord_interp; + + void main() + { + //gl_Position = ModelViewProjectionMatrix * vec4(pos.xy, 0.0f, 1.0f); + //gl_Position.z = 1.0; + gl_Position = vec4(pos.xy, 100, 100); + texCoord_interp = texCoord; + } + + ''' + fragment_shader = ''' + in vec2 texCoord_interp; + out vec4 fragColor; + + uniform sampler2D image; + + //Code from here: https://github.com/BabylonJS/Babylon.js/blob/master/src/Shaders/ShadersInclude/helperFunctions.fx + + const float PI = 3.1415926535897932384626433832795; + const float HALF_MIN = 5.96046448e-08; // Smallest positive half. + + const float LinearEncodePowerApprox = 2.2; + const float GammaEncodePowerApprox = 1.0 / LinearEncodePowerApprox; + const vec3 LuminanceEncodeApprox = vec3(0.2126, 0.7152, 0.0722); + + const float Epsilon = 0.0000001; + #define saturate(x) clamp(x, 0.0, 1.0) + + float maxEps(float x) { + return max(x, Epsilon); + } + + float toLinearSpace(float color) + { + return pow(color, LinearEncodePowerApprox); + } + + vec3 toLinearSpace(vec3 color) + { + return pow(color, vec3(LinearEncodePowerApprox)); + } + + vec4 toLinearSpace(vec4 color) + { + return vec4(pow(color.rgb, vec3(LinearEncodePowerApprox)), color.a); + } + + vec3 toGammaSpace(vec3 color) + { + return pow(color, vec3(GammaEncodePowerApprox)); + } + + vec4 toGammaSpace(vec4 color) + { + return vec4(pow(color.rgb, vec3(GammaEncodePowerApprox)), color.a); + } + + float toGammaSpace(float color) + { + return pow(color, GammaEncodePowerApprox); + } + + float square(float value) + { + return value * value; + } + + // Check if configurable value is needed. + const float rgbdMaxRange = 255.0; + + vec4 toRGBM(vec3 color) { + + vec4 rgbm; + color *= 1.0/6.0; + rgbm.a = saturate( max( max( color.r, color.g ), max( color.b, 1e-6 ) ) ); + rgbm.a = clamp(floor(D) / 255.0, 0., 1.); + rgbm.rgb = color / rgbm.a; + + return + + float maxRGB = maxEps(max(color.r, max(color.g, color.b))); + float D = max(rgbdMaxRange / maxRGB, 1.); + D = clamp(floor(D) / 255.0, 0., 1.); + vec3 rgb = color.rgb * D; + + // Helps with png quantization. + rgb = toGammaSpace(rgb); + + return vec4(rgb, D); + } + + vec3 fromRGBD(vec4 rgbd) { + // Helps with png quantization. + rgbd.rgb = toLinearSpace(rgbd.rgb); + + // return rgbd.rgb * ((rgbdMaxRange / 255.0) / rgbd.a); + + return rgbd.rgb / rgbd.a; + } + + void main() + { + + fragColor = toRGBM(texture(image, texCoord_interp).rgb); + + } + + ''' + + x_screen = 0 + off_x = -100 + off_y = -100 + y_screen_flip = 0 + sx = 200 + sy = 200 + + vertices = ( + (x_screen + off_x, y_screen_flip - off_y), + (x_screen + off_x, y_screen_flip - sy - off_y), + (x_screen + off_x + sx, y_screen_flip - sy - off_y), + (x_screen + off_x + sx, y_screen_flip - off_x)) + + if input_image.colorspace_settings.name != 'Linear': + input_image.colorspace_settings.name = 'Linear' + + # Removing .exr or .hdr prefix + if image_name[-4:] == '.exr' or image_name[-4:] == '.hdr': + image_name = image_name[:-4] + + target_image = bpy.data.images.get(image_name + '_encoded') + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(image_name + '_encoded') + if not target_image: + target_image = bpy.data.images.new( + name = image_name + '_encoded', + width = input_image.size[0], + height = input_image.size[1], + alpha = True, + float_buffer = False + ) + + shader = gpu.types.GPUShader(vertex_shader, fragment_shader) + batch = batch_for_shader( + shader, 'TRI_FAN', + { + "pos": vertices, + "texCoord": ((0, 1), (0, 0), (1, 0), (1, 1)), + }, + ) + + if image.gl_load(): + raise Exception() + + with offscreen.bind(): + bgl.glActiveTexture(bgl.GL_TEXTURE0) + bgl.glBindTexture(bgl.GL_TEXTURE_2D, image.bindcode) + + shader.bind() + shader.uniform_int("image", 0) + batch.draw(shader) + + buffer = bgl.Buffer(bgl.GL_BYTE, input_image.size[0] * input_image.size[1] * 4) + bgl.glReadBuffer(bgl.GL_BACK) + bgl.glReadPixels(0, 0, input_image.size[0], input_image.size[1], bgl.GL_RGBA, bgl.GL_UNSIGNED_BYTE, buffer) + + offscreen.free() + + target_image.pixels = [v / 255 for v in buffer] + input_image = target_image + + #Save LogLuv + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print(input_image.name) + input_image.filepath_raw = outDir + "/" + input_image.name + ".png" + #input_image.filepath_raw = outDir + "_encoded.png" + input_image.file_format = "PNG" + bpy.context.scene.render.image_settings.quality = quality + #input_image.save_render(filepath = input_image.filepath_raw, scene = bpy.context.scene) + input_image.save() + + #Todo - Find a way to save + #bpy.ops.image.save_all_modified() + def encodeImageRGBMCPU(image, maxRange, outDir, quality): input_image = bpy.data.images[image.name] image_name = input_image.name @@ -431,21 +630,6 @@ def encodeImageRGBDCPU(image, maxRange, outDir, quality): result_pixel[i+1] = math.pow(result_pixel[i+1] * D, 1/2.2) result_pixel[i+2] = math.pow(result_pixel[i+2] * D, 1/2.2) result_pixel[i+3] = D - - - # for i in range(0,num_pixels,4): - - # m = saturate(max(result_pixel[i], result_pixel[i+1], result_pixel[i+2], 1e-6)) - # d = max(maxRange / m, 1) - # #d = saturate(math.floor(d) / 255.0) - # d = np.clip((math.floor(d) / 255.0), 0.0, 1.0) - - # #TODO TO GAMMA SPACE - - # result_pixel[i] = math.pow(result_pixel[i] * d * 255 / maxRange, 1/2.2) - # result_pixel[i+1] = math.pow(result_pixel[i+1] * d * 255 / maxRange, 1/2.2) - # result_pixel[i+2] = math.pow(result_pixel[i+2] * d * 255 / maxRange, 1/2.2) - # result_pixel[i+3] = d target_image.pixels = result_pixel @@ -457,25 +641,4 @@ def encodeImageRGBDCPU(image, maxRange, outDir, quality): input_image.filepath_raw = outDir + "/" + input_image.name + ".png" input_image.file_format = "PNG" bpy.context.scene.render.image_settings.quality = quality - input_image.save() - - # const float rgbdMaxRange = 255.0; - - # vec4 toRGBD(vec3 color) { - # float maxRGB = maxEps(max(color.r, max(color.g, color.b))); - # float D = max(rgbdMaxRange / maxRGB, 1.); - # D = clamp(floor(D) / 255.0, 0., 1.); - # vec3 rgb = color.rgb * D; - - # // Helps with png quantization. - # rgb = toGammaSpace(rgb); - - # return vec4(rgb, D); - # } - - # const float Epsilon = 0.0000001; - # #define saturate(x) clamp(x, 0.0, 1.0) - - # float maxEps(float x) { - # return max(x, Epsilon); - # } \ No newline at end of file + input_image.save() \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/filtering/opencv.py b/blender/arm/lightmapper/utility/filtering/opencv.py index 501b2b3d..c6b1b557 100644 --- a/blender/arm/lightmapper/utility/filtering/opencv.py +++ b/blender/arm/lightmapper/utility/filtering/opencv.py @@ -62,7 +62,7 @@ class TLM_CV_Filtering: #SEAM TESTING# ##################### - if obj_name in bpy.data.objects: + if obj_name in bpy.context.scene.objects: override = bpy.data.objects[obj_name].TLM_ObjectProperties.tlm_mesh_filter_override elif obj_name in scene.TLM_AtlasList: override = False diff --git a/blender/arm/lightmapper/utility/gui/Viewport.py b/blender/arm/lightmapper/utility/gui/Viewport.py new file mode 100644 index 00000000..be1e8d2b --- /dev/null +++ b/blender/arm/lightmapper/utility/gui/Viewport.py @@ -0,0 +1,67 @@ +import bpy, blf, bgl, os, gpu +from gpu_extras.batch import batch_for_shader + +class ViewportDraw: + + def __init__(self, context, text): + + bakefile = "TLM_Overlay.png" + scriptDir = os.path.dirname(os.path.realpath(__file__)) + bakefile_path = os.path.abspath(os.path.join(scriptDir, '..', '..', 'assets/' + bakefile)) + + image_name = "TLM_Overlay.png" + + bpy.ops.image.open(filepath=bakefile_path) + + print("Self path: " + bakefile_path) + + image = bpy.data.images[image_name] + + x = 15 + y = 15 + w = 400 + h = 200 + + self.shader = gpu.shader.from_builtin('2D_IMAGE') + self.batch = batch_for_shader( + self.shader, 'TRI_FAN', + { + "pos": ((x, y), (x+w, y), (x+w, y+h), (x, y+h)), + "texCoord": ((0, 0), (1, 0), (1, 1), (0, 1)), + }, + ) + + if image.gl_load(): + raise Exception() + + self.text = text + self.image = image + #self.handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_text_callback, (context,), 'WINDOW', 'POST_PIXEL') + self.handle2 = bpy.types.SpaceView3D.draw_handler_add(self.draw_image_callback, (context,), 'WINDOW', 'POST_PIXEL') + + def draw_text_callback(self, context): + + font_id = 0 + blf.position(font_id, 15, 15, 0) + blf.size(font_id, 20, 72) + blf.draw(font_id, "%s" % (self.text)) + + def draw_image_callback(self, context): + + if self.image: + bgl.glEnable(bgl.GL_BLEND) + bgl.glActiveTexture(bgl.GL_TEXTURE0) + bgl.glBindTexture(bgl.GL_TEXTURE_2D, self.image.bindcode) + + self.shader.bind() + self.shader.uniform_int("image", 0) + self.batch.draw(self.shader) + bgl.glDisable(bgl.GL_BLEND) + + def update_text(self, text): + + self.text = text + + def remove_handle(self): + #bpy.types.SpaceView3D.draw_handler_remove(self.handle, 'WINDOW') + bpy.types.SpaceView3D.draw_handler_remove(self.handle2, 'WINDOW') \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/luxcore/setup.py b/blender/arm/lightmapper/utility/luxcore/setup.py new file mode 100644 index 00000000..13bfc57b --- /dev/null +++ b/blender/arm/lightmapper/utility/luxcore/setup.py @@ -0,0 +1,259 @@ +import bpy + +from .. utility import * + +def init(self, prev_container): + + #TODO - JSON classes + export.scene = """scene.camera.cliphither = 0.1 +scene.camera.clipyon = 100 +scene.camera.shutteropen = 0 +scene.camera.shutterclose = 1 +scene.camera.autovolume.enable = 1 +scene.camera.lookat.orig = 7.358891 -6.925791 4.958309 +scene.camera.lookat.target = 6.707333 -6.31162 4.513038 +scene.camera.up = -0.3240135 0.3054208 0.8953956 +scene.camera.screenwindow = -1 1 -0.5625 0.5625 +scene.camera.lensradius = 0 +scene.camera.focaldistance = 10 +scene.camera.autofocus.enable = 0 +scene.camera.type = "perspective" +scene.camera.oculusrift.barrelpostpro.enable = 0 +scene.camera.fieldofview = 39.59776 +scene.camera.bokeh.blades = 0 +scene.camera.bokeh.power = 3 +scene.camera.bokeh.distribution.type = "NONE" +scene.camera.bokeh.scale.x = 0.7071068 +scene.camera.bokeh.scale.y = 0.7071068 +scene.lights.__WORLD_BACKGROUND_LIGHT__.gain = 2e-05 2e-05 2e-05 +scene.lights.__WORLD_BACKGROUND_LIGHT__.transformation = 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 +scene.lights.__WORLD_BACKGROUND_LIGHT__.id = 0 +scene.lights.__WORLD_BACKGROUND_LIGHT__.temperature = -1 +scene.lights.__WORLD_BACKGROUND_LIGHT__.temperature.normalize = 0 +scene.lights.__WORLD_BACKGROUND_LIGHT__.visibility.indirect.diffuse.enable = 1 +scene.lights.__WORLD_BACKGROUND_LIGHT__.visibility.indirect.glossy.enable = 1 +scene.lights.__WORLD_BACKGROUND_LIGHT__.visibility.indirect.specular.enable = 1 +scene.lights.__WORLD_BACKGROUND_LIGHT__.type = "sky2" +scene.lights.__WORLD_BACKGROUND_LIGHT__.dir = 0 0 1 +scene.lights.__WORLD_BACKGROUND_LIGHT__.turbidity = 2.2 +scene.lights.__WORLD_BACKGROUND_LIGHT__.groundalbedo = 0.5 0.5 0.5 +scene.lights.__WORLD_BACKGROUND_LIGHT__.ground.enable = 0 +scene.lights.__WORLD_BACKGROUND_LIGHT__.ground.color = 0.5 0.5 0.5 +scene.lights.__WORLD_BACKGROUND_LIGHT__.ground.autoscale = 1 +scene.lights.__WORLD_BACKGROUND_LIGHT__.distribution.width = 512 +scene.lights.__WORLD_BACKGROUND_LIGHT__.distribution.height = 256 +scene.lights.__WORLD_BACKGROUND_LIGHT__.visibilitymapcache.enable = 0 +scene.lights.2382361116072.gain = 1 1 1 +scene.lights.2382361116072.transformation = -0.2908646 0.9551712 -0.05518906 0 -0.7711008 -0.1998834 0.6045247 0 0.5663932 0.2183912 0.7946723 0 4.076245 1.005454 5.903862 1 +scene.lights.2382361116072.id = 0 +scene.lights.2382361116072.temperature = -1 +scene.lights.2382361116072.temperature.normalize = 0 +scene.lights.2382361116072.type = "sphere" +scene.lights.2382361116072.color = 1 1 1 +scene.lights.2382361116072.power = 0 +scene.lights.2382361116072.normalizebycolor = 0 +scene.lights.2382361116072.efficency = 0 +scene.lights.2382361116072.position = 0 0 0 +scene.lights.2382361116072.radius = 0.1 +scene.materials.Material2382357175256.type = "disney" +scene.materials.Material2382357175256.basecolor = "0.7 0.7 0.7" +scene.materials.Material2382357175256.subsurface = "0" +scene.materials.Material2382357175256.roughness = "0.2" +scene.materials.Material2382357175256.metallic = "0" +scene.materials.Material2382357175256.specular = "0.5" +scene.materials.Material2382357175256.speculartint = "0" +scene.materials.Material2382357175256.clearcoat = "0" +scene.materials.Material2382357175256.clearcoatgloss = "1" +scene.materials.Material2382357175256.anisotropic = "0" +scene.materials.Material2382357175256.sheen = "0" +scene.materials.Material2382357175256.sheentint = "0" +scene.materials.Material2382357175256.transparency.shadow = 0 0 0 +scene.materials.Material2382357175256.id = 3364224 +scene.materials.Material2382357175256.emission.gain = 1 1 1 +scene.materials.Material2382357175256.emission.power = 0 +scene.materials.Material2382357175256.emission.normalizebycolor = 1 +scene.materials.Material2382357175256.emission.efficency = 0 +scene.materials.Material2382357175256.emission.theta = 90 +scene.materials.Material2382357175256.emission.id = 0 +scene.materials.Material2382357175256.emission.importance = 1 +scene.materials.Material2382357175256.emission.temperature = -1 +scene.materials.Material2382357175256.emission.temperature.normalize = 0 +scene.materials.Material2382357175256.emission.directlightsampling.type = "AUTO" +scene.materials.Material2382357175256.visibility.indirect.diffuse.enable = 1 +scene.materials.Material2382357175256.visibility.indirect.glossy.enable = 1 +scene.materials.Material2382357175256.visibility.indirect.specular.enable = 1 +scene.materials.Material2382357175256.shadowcatcher.enable = 0 +scene.materials.Material2382357175256.shadowcatcher.onlyinfinitelights = 0 +scene.materials.Material2382357175256.photongi.enable = 1 +scene.materials.Material2382357175256.holdout.enable = 0 +scene.materials.Material__0012382357172440.type = "disney" +scene.materials.Material__0012382357172440.basecolor = "0.7 0.7 0.7" +scene.materials.Material__0012382357172440.subsurface = "0" +scene.materials.Material__0012382357172440.roughness = "0.2" +scene.materials.Material__0012382357172440.metallic = "0" +scene.materials.Material__0012382357172440.specular = "0.5" +scene.materials.Material__0012382357172440.speculartint = "0" +scene.materials.Material__0012382357172440.clearcoat = "0" +scene.materials.Material__0012382357172440.clearcoatgloss = "1" +scene.materials.Material__0012382357172440.anisotropic = "0" +scene.materials.Material__0012382357172440.sheen = "0" +scene.materials.Material__0012382357172440.sheentint = "0" +scene.materials.Material__0012382357172440.transparency.shadow = 0 0 0 +scene.materials.Material__0012382357172440.id = 6728256 +scene.materials.Material__0012382357172440.emission.gain = 1 1 1 +scene.materials.Material__0012382357172440.emission.power = 0 +scene.materials.Material__0012382357172440.emission.normalizebycolor = 1 +scene.materials.Material__0012382357172440.emission.efficency = 0 +scene.materials.Material__0012382357172440.emission.theta = 90 +scene.materials.Material__0012382357172440.emission.id = 0 +scene.materials.Material__0012382357172440.emission.importance = 1 +scene.materials.Material__0012382357172440.emission.temperature = -1 +scene.materials.Material__0012382357172440.emission.temperature.normalize = 0 +scene.materials.Material__0012382357172440.emission.directlightsampling.type = "AUTO" +scene.materials.Material__0012382357172440.visibility.indirect.diffuse.enable = 1 +scene.materials.Material__0012382357172440.visibility.indirect.glossy.enable = 1 +scene.materials.Material__0012382357172440.visibility.indirect.specular.enable = 1 +scene.materials.Material__0012382357172440.shadowcatcher.enable = 0 +scene.materials.Material__0012382357172440.shadowcatcher.onlyinfinitelights = 0 +scene.materials.Material__0012382357172440.photongi.enable = 1 +scene.materials.Material__0012382357172440.holdout.enable = 0 +scene.objects.23823611086320.material = "Material2382357175256" +scene.objects.23823611086320.ply = "mesh-00000.ply" +scene.objects.23823611086320.camerainvisible = 0 +scene.objects.23823611086320.id = 1326487202 +scene.objects.23823611086320.appliedtransformation = 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 1 +scene.objects.23823611279760.material = "Material__0012382357172440" +scene.objects.23823611279760.ply = "mesh-00001.ply" +scene.objects.23823611279760.camerainvisible = 0 +scene.objects.23823611279760.id = 3772660237 +scene.objects.23823611279760.appliedtransformation = 5 0 0 0 0 5 0 0 0 0 5 0 0 0 0 1 +""" + + export.config = """context.verbose = 1 +accelerator.type = "AUTO" +accelerator.instances.enable = 1 +accelerator.motionblur.enable = 1 +accelerator.bvh.builder.type = "EMBREE_BINNED_SAH" +accelerator.bvh.treetype = 4 +accelerator.bvh.costsamples = 0 +accelerator.bvh.isectcost = 80 +accelerator.bvh.travcost = 10 +accelerator.bvh.emptybonus = 0.5 +scene.epsilon.min = "1e-05" +scene.epsilon.max = "0.1" +scene.file = "scene.scn" +images.scale = 1 +lightstrategy.type = "LOG_POWER" +native.threads.count = 8 +renderengine.type = "BAKECPU" +path.pathdepth.total = "7" +path.pathdepth.diffuse = "5" +path.pathdepth.glossy = "5" +path.pathdepth.specular = "6" +path.hybridbackforward.enable = "0" +path.hybridbackforward.partition = "0.8" +path.hybridbackforward.glossinessthreshold = "0.049" +path.russianroulette.depth = 3 +path.russianroulette.cap = 0.5 +path.clamping.variance.maxvalue = 0 +path.forceblackbackground.enable = "0" +sampler.type = "SOBOL" +sampler.imagesamples.enable = 1 +sampler.sobol.adaptive.strength = "0.9" +sampler.sobol.adaptive.userimportanceweight = 0.75 +sampler.sobol.bucketsize = "16" +sampler.sobol.tilesize = "16" +sampler.sobol.supersampling = "1" +sampler.sobol.overlapping = "1" +path.photongi.sampler.type = "METROPOLIS" +path.photongi.photon.maxcount = 100000000 +path.photongi.photon.maxdepth = 4 +path.photongi.photon.time.start = 0 +path.photongi.photon.time.end = -1 +path.photongi.visibility.lookup.radius = 0 +path.photongi.visibility.lookup.normalangle = 10 +path.photongi.visibility.targethitrate = 0.99 +path.photongi.visibility.maxsamplecount = 1048576 +path.photongi.glossinessusagethreshold = 0.05 +path.photongi.indirect.enabled = 0 +path.photongi.indirect.maxsize = 0 +path.photongi.indirect.haltthreshold = 0.05 +path.photongi.indirect.lookup.radius = 0 +path.photongi.indirect.lookup.normalangle = 10 +path.photongi.indirect.usagethresholdscale = 8 +path.photongi.indirect.filter.radiusscale = 3 +path.photongi.caustic.enabled = 0 +path.photongi.caustic.maxsize = 100000 +path.photongi.caustic.updatespp = 8 +path.photongi.caustic.updatespp.radiusreduction = 0.96 +path.photongi.caustic.updatespp.minradius = 0.003 +path.photongi.caustic.lookup.radius = 0.15 +path.photongi.caustic.lookup.normalangle = 10 +path.photongi.debug.type = "none" +path.photongi.persistent.file = "" +path.photongi.persistent.safesave = 1 +film.filter.type = "BLACKMANHARRIS" +film.filter.width = 2 +opencl.platform.index = -1 +film.width = 960 +film.height = 600 +film.safesave = 1 +film.noiseestimation.step = "32" +film.noiseestimation.warmup = "8" +film.noiseestimation.filter.scale = 4 +batch.haltnoisethreshold = 0.01 +batch.haltnoisethreshold.step = 64 +batch.haltnoisethreshold.warmup = 64 +batch.haltnoisethreshold.filter.enable = 1 +batch.haltnoisethreshold.stoprendering.enable = 1 +batch.halttime = "0" +batch.haltspp = 32 +film.outputs.safesave = 1 +film.outputs.0.type = "RGB_IMAGEPIPELINE" +film.outputs.0.filename = "RGB_IMAGEPIPELINE_0.png" +film.outputs.0.index = "0" +film.imagepipelines.000.0.type = "NOP" +film.imagepipelines.000.1.type = "TONEMAP_LINEAR" +film.imagepipelines.000.1.scale = "1" +film.imagepipelines.000.2.type = "GAMMA_CORRECTION" +film.imagepipelines.000.2.value = "2.2" +film.imagepipelines.000.radiancescales.0.enabled = "1" +film.imagepipelines.000.radiancescales.0.globalscale = "1" +film.imagepipelines.000.radiancescales.0.rgbscale = "1" "1" "1" +periodicsave.film.outputs.period = 0 +periodicsave.film.period = 0 +periodicsave.film.filename = "film.flm" +periodicsave.resumerendering.period = 0 +periodicsave.resumerendering.filename = "rendering.rsm" +resumerendering.filesafe = 1 +debug.renderconfig.parse.print = 0 +debug.scene.parse.print = 0 +screen.refresh.interval = 100 +screen.tool.type = "CAMERA_EDIT" +screen.tiles.pending.show = 1 +screen.tiles.converged.show = 0 +screen.tiles.notconverged.show = 0 +screen.tiles.passcount.show = 0 +screen.tiles.error.show = 0 +bake.minmapautosize = 64 +bake.maxmapautosize = 1024 +bake.powerof2autosize.enable = 1 +bake.skipexistingmapfiles = 1 +film.imagepipelines.1.0.type = "NOP" +bake.maps.0.type = "COMBINED" +bake.maps.0.filename = "23823611086320.exr" +bake.maps.0.imagepipelineindex = 1 +bake.maps.0.width = 512 +bake.maps.0.height = 512 +bake.maps.0.autosize.enabled = 1 +bake.maps.0.uvindex = 0 +bake.maps.0.objectnames = "23823611086320" +bake.maps.1.type = "COMBINED" +bake.maps.1.filename = "23823611279760.exr" +bake.maps.1.imagepipelineindex = 1 +bake.maps.1.width = 512 +bake.maps.1.height = 512 +bake.maps.1.autosize.enabled = 1 +bake.maps.1.uvindex = 0 +bake.maps.1.objectnames = "23823611279760" +""" \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/octane/configure.py b/blender/arm/lightmapper/utility/octane/configure.py new file mode 100644 index 00000000..ba6641ab --- /dev/null +++ b/blender/arm/lightmapper/utility/octane/configure.py @@ -0,0 +1,243 @@ +import bpy, math + +#from . import cache +from .. utility import * + +def init(self, prev_container): + + #store_existing(prev_container) + + #set_settings() + + configure_world() + + configure_lights() + + configure_meshes(self) + +def configure_world(): + pass + +def configure_lights(): + pass + +def configure_meshes(self): + + for mat in bpy.data.materials: + if mat.users < 1: + bpy.data.materials.remove(mat) + + for mat in bpy.data.materials: + if mat.name.startswith("."): + if "_Original" in mat.name: + bpy.data.materials.remove(mat) + + for image in bpy.data.images: + if image.name.endswith("_baked"): + bpy.data.images.remove(image, do_unlink=True) + + iterNum = 1 + currentIterNum = 0 + + scene = bpy.context.scene + + for obj in scene.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + obj.hide_select = False #Remember to toggle this back + + currentIterNum = currentIterNum + 1 + + obj.octane.baking_group_id = 1 + currentIterNum #0 doesn't exist, 1 is neutral and 2 is first baked object + + print("Obj: " + obj.name + " set to baking group: " + str(obj.octane.baking_group_id)) + + for slot in obj.material_slots: + if "." + slot.name + '_Original' in bpy.data.materials: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("The material: " + slot.name + " shifted to " + "." + slot.name + '_Original') + slot.material = bpy.data.materials["." + slot.name + '_Original'] + + + objWasHidden = False + + #For some reason, a Blender bug might prevent invisible objects from being smart projected + #We will turn the object temporarily visible + obj.hide_viewport = False + obj.hide_set(False) + + #Configure selection + bpy.ops.object.select_all(action='DESELECT') + bpy.context.view_layer.objects.active = obj + obj.select_set(True) + obs = bpy.context.view_layer.objects + active = obs.active + + uv_layers = obj.data.uv_layers + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + if not uv_channel in uv_layers: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("UV map created for obj: " + obj.name) + uvmap = uv_layers.new(name=uv_channel) + uv_layers.active_index = len(uv_layers) - 1 + print("Setting active UV to: " + uv_layers.active_index) + + #If lightmap + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "Lightmap": + bpy.ops.uv.lightmap_pack('EXEC_SCREEN', PREF_CONTEXT='ALL_FACES', PREF_MARGIN_DIV=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin) + + #If smart project + elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "SmartProject": + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Smart Project B") + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + #API changes in 2.91 causes errors: + if (2, 91, 0) > bpy.app.version: + bpy.ops.uv.smart_project(angle_limit=45.0, island_margin=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin, user_area_weight=1.0, use_aspect=True, stretch_to_bounds=False) + else: + angle = math.radians(45.0) + bpy.ops.uv.smart_project(angle_limit=angle, island_margin=obj.TLM_ObjectProperties.tlm_mesh_unwrap_margin, area_weight=1.0, correct_aspect=True, scale_to_bounds=False) + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.mode_set(mode='OBJECT') + + elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "Xatlas": + + Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj) + + elif obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("ATLAS GROUP: " + obj.TLM_ObjectProperties.tlm_atlas_pointer) + + else: #if copy existing + + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Copied Existing UV Map for object: " + obj.name) + + else: + if bpy.context.scene.TLM_SceneProperties.tlm_verbose: + print("Existing UV map found for obj: " + obj.name) + for i in range(0, len(uv_layers)): + if uv_layers[i].name == uv_channel: + uv_layers.active_index = i + break + + set_camera() + +def set_camera(): + + cam_name = "TLM-BakeCam" + + if not cam_name in bpy.context.scene: + camera = bpy.data.cameras.new(cam_name) + camobj_name = "TLM-BakeCam-obj" + cam_obj = bpy.data.objects.new(camobj_name, camera) + bpy.context.collection.objects.link(cam_obj) + cam_obj.location = ((0,0,0)) + + bpy.context.scene.camera = cam_obj + +def set_settings(): + + scene = bpy.context.scene + cycles = scene.cycles + scene.render.engine = "CYCLES" + sceneProperties = scene.TLM_SceneProperties + engineProperties = scene.TLM_EngineProperties + cycles.device = scene.TLM_EngineProperties.tlm_mode + + if cycles.device == "GPU": + scene.render.tile_x = 256 + scene.render.tile_y = 256 + else: + scene.render.tile_x = 32 + scene.render.tile_y = 32 + + if engineProperties.tlm_quality == "0": + cycles.samples = 32 + cycles.max_bounces = 1 + cycles.diffuse_bounces = 1 + cycles.glossy_bounces = 1 + cycles.transparent_max_bounces = 1 + cycles.transmission_bounces = 1 + cycles.volume_bounces = 1 + cycles.caustics_reflective = False + cycles.caustics_refractive = False + elif engineProperties.tlm_quality == "1": + cycles.samples = 64 + cycles.max_bounces = 2 + cycles.diffuse_bounces = 2 + cycles.glossy_bounces = 2 + cycles.transparent_max_bounces = 2 + cycles.transmission_bounces = 2 + cycles.volume_bounces = 2 + cycles.caustics_reflective = False + cycles.caustics_refractive = False + elif engineProperties.tlm_quality == "2": + cycles.samples = 512 + cycles.max_bounces = 2 + cycles.diffuse_bounces = 2 + cycles.glossy_bounces = 2 + cycles.transparent_max_bounces = 2 + cycles.transmission_bounces = 2 + cycles.volume_bounces = 2 + cycles.caustics_reflective = False + cycles.caustics_refractive = False + elif engineProperties.tlm_quality == "3": + cycles.samples = 1024 + cycles.max_bounces = 256 + cycles.diffuse_bounces = 256 + cycles.glossy_bounces = 256 + cycles.transparent_max_bounces = 256 + cycles.transmission_bounces = 256 + cycles.volume_bounces = 256 + cycles.caustics_reflective = False + cycles.caustics_refractive = False + elif engineProperties.tlm_quality == "4": + cycles.samples = 2048 + cycles.max_bounces = 512 + cycles.diffuse_bounces = 512 + cycles.glossy_bounces = 512 + cycles.transparent_max_bounces = 512 + cycles.transmission_bounces = 512 + cycles.volume_bounces = 512 + cycles.caustics_reflective = True + cycles.caustics_refractive = True + else: #Custom + pass + +def store_existing(prev_container): + + scene = bpy.context.scene + cycles = scene.cycles + + selected = [] + + for obj in bpy.context.scene.objects: + if obj.select_get(): + selected.append(obj.name) + + prev_container["settings"] = [ + cycles.samples, + cycles.max_bounces, + cycles.diffuse_bounces, + cycles.glossy_bounces, + cycles.transparent_max_bounces, + cycles.transmission_bounces, + cycles.volume_bounces, + cycles.caustics_reflective, + cycles.caustics_refractive, + cycles.device, + scene.render.engine, + bpy.context.view_layer.objects.active, + selected, + [scene.render.resolution_x, scene.render.resolution_y] + ] \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/octane/lightmap2.py b/blender/arm/lightmapper/utility/octane/lightmap2.py new file mode 100644 index 00000000..ad843276 --- /dev/null +++ b/blender/arm/lightmapper/utility/octane/lightmap2.py @@ -0,0 +1,71 @@ +import bpy, os + +def bake(): + + cam_name = "TLM-BakeCam-obj" + + if cam_name in bpy.context.scene.objects: + + print("Camera found...") + + camera = bpy.context.scene.objects[cam_name] + + camera.data.octane.baking_camera = True + + for obj in bpy.context.scene.objects: + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(False) + + iterNum = 2 + currentIterNum = 1 + + for obj in bpy.context.scene.objects: + if obj.type == "MESH": + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + iterNum = iterNum + 1 + + if iterNum > 1: + iterNum = iterNum - 1 + + for obj in bpy.context.scene.objects: + if obj.type == 'MESH' and obj.name in bpy.context.view_layer.objects: + if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: + + currentIterNum = currentIterNum + 1 + + scene = bpy.context.scene + + print("Baking obj: " + obj.name) + + print("Baking ID: " + str(currentIterNum) + " out of " + str(iterNum)) + + bpy.ops.object.select_all(action='DESELECT') + + camera.data.octane.baking_group_id = currentIterNum + + savedir = os.path.dirname(bpy.data.filepath) + user_dir = scene.TLM_Engine3Properties.tlm_lightmap_savedir + directory = os.path.join(savedir, user_dir) + + image_settings = bpy.context.scene.render.image_settings + image_settings.file_format = "HDR" + image_settings.color_depth = '32' + + filename = os.path.join(directory, "LM") + "_" + obj.name + ".hdr" + bpy.context.scene.render.filepath = filename + + resolution = int(obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution) + + bpy.context.scene.render.resolution_x = resolution + bpy.context.scene.render.resolution_y = resolution + + bpy.ops.render.render(write_still=True) + + else: + + print("No baking camera found") + + + + + print("Baking in Octane!") \ No newline at end of file diff --git a/blender/arm/lightmapper/utility/pack.py b/blender/arm/lightmapper/utility/pack.py index 3c2768c7..c2e1f15c 100644 --- a/blender/arm/lightmapper/utility/pack.py +++ b/blender/arm/lightmapper/utility/pack.py @@ -106,7 +106,7 @@ def postpack(): rect = [] #For each object that targets the atlas - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: if obj.TLM_ObjectProperties.tlm_postpack_object: if obj.TLM_ObjectProperties.tlm_postatlas_pointer == atlas.name: @@ -156,7 +156,13 @@ def postpack(): obj = bpy.data.objects[aob] for idx, layer in enumerate(obj.data.uv_layers): - if layer.name == "UVMap_Lightmap": + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + uv_channel = obj.TLM_ObjectProperties.tlm_uv_channel + else: + uv_channel = "UVMap_Lightmap" + + if layer.name == uv_channel: obj.data.uv_layers.active_index = idx print("UVLayer set to: " + str(obj.data.uv_layers.active_index)) @@ -194,7 +200,7 @@ def postpack(): print("Written: " + str(os.path.join(lightmap_directory, atlas.name + end + formatEnc))) #Change the material for each material, slot - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: if obj.TLM_ObjectProperties.tlm_postpack_object: if obj.TLM_ObjectProperties.tlm_postatlas_pointer == atlas.name: @@ -219,7 +225,7 @@ def postpack(): existing_image.user_clear() #Add dilation map here... - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: if obj.TLM_ObjectProperties.tlm_postpack_object: if obj.TLM_ObjectProperties.tlm_postatlas_pointer == atlas.name: diff --git a/blender/arm/lightmapper/utility/utility.py b/blender/arm/lightmapper/utility/utility.py index 75929157..b206c773 100644 --- a/blender/arm/lightmapper/utility/utility.py +++ b/blender/arm/lightmapper/utility/utility.py @@ -1,5 +1,5 @@ import bpy.ops as O -import bpy, os, re, sys, importlib, struct, platform, subprocess, threading, string, bmesh +import bpy, os, re, sys, importlib, struct, platform, subprocess, threading, string, bmesh, shutil, glob, uuid from io import StringIO from threading import Thread from queue import Queue, Empty @@ -81,15 +81,8 @@ def save_image(image): image.filepath_raw = savepath - # if "Normal" in image.name: - # bpy.context.scene.render.image_settings.quality = 90 - # image.save_render( filepath = image.filepath_raw, scene = bpy.context.scene ) - # else: image.save() - - - def get_file_size(filepath): size = "Unpack Files" try: @@ -141,7 +134,7 @@ def check_is_org_material(self,material): def clean_empty_materials(self): - for obj in bpy.data.objects: + for obj in bpy.context.scene.objects: for slot in obj.material_slots: mat = slot.material if mat is None: @@ -319,6 +312,11 @@ def lightmap_to_ao(material,lightmap_node): # https://github.com/mattedicksoncom/blender-xatlas/ ########################################################### +def gen_safe_name(): + genId = uuid.uuid4().hex + # genId = "u_" + genId.replace("-","_") + return "u_" + genId + def Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj): blender_xatlas = importlib.util.find_spec("blender_xatlas") @@ -330,32 +328,54 @@ def Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj): packOptions = bpy.context.scene.pack_tool chartOptions = bpy.context.scene.chart_tool + sharedProperties = bpy.context.scene.shared_properties + #sharedProperties.unwrapSelection context = bpy.context - - if obj.type == 'MESH': - context.view_layer.objects.active = obj - if obj.data.users > 1: - obj.data = obj.data.copy() #make single user copy - uv_layers = obj.data.uv_layers - #setup the lightmap uvs - uvName = "UVMap_Lightmap" - if sharedProperties.lightmapUVChoiceType == "NAME": - uvName = sharedProperties.lightmapUVName - elif sharedProperties.lightmapUVChoiceType == "INDEX": - if sharedProperties.lightmapUVIndex < len(uv_layers): - uvName = uv_layers[sharedProperties.lightmapUVIndex].name + #save whatever mode the user was in + startingMode = bpy.context.object.mode + selected_objects = bpy.context.selected_objects - if not uvName in uv_layers: - uvmap = uv_layers.new(name=uvName) - uv_layers.active_index = len(uv_layers) - 1 - else: - for i in range(0, len(uv_layers)): - if uv_layers[i].name == uvName: - uv_layers.active_index = i - obj.select_set(True) + #check something is actually selected + #external function/operator will select them + if len(selected_objects) == 0: + print("Nothing Selected") + self.report({"WARNING"}, "Nothing Selected, please select Something") + return {'FINISHED'} + + #store the names of objects to be lightmapped + rename_dict = dict() + safe_dict = dict() + + #make sure all the objects have ligthmap uvs + for obj in selected_objects: + if obj.type == 'MESH': + safe_name = gen_safe_name(); + rename_dict[obj.name] = (obj.name,safe_name) + safe_dict[safe_name] = obj.name + context.view_layer.objects.active = obj + if obj.data.users > 1: + obj.data = obj.data.copy() #make single user copy + uv_layers = obj.data.uv_layers + + #setup the lightmap uvs + uvName = "UVMap_Lightmap" + if sharedProperties.lightmapUVChoiceType == "NAME": + uvName = sharedProperties.lightmapUVName + elif sharedProperties.lightmapUVChoiceType == "INDEX": + if sharedProperties.lightmapUVIndex < len(uv_layers): + uvName = uv_layers[sharedProperties.lightmapUVIndex].name + + if not uvName in uv_layers: + uvmap = uv_layers.new(name=uvName) + uv_layers.active_index = len(uv_layers) - 1 + else: + for i in range(0, len(uv_layers)): + if uv_layers[i].name == uvName: + uv_layers.active_index = i + obj.select_set(True) #save all the current edges if sharedProperties.packOnly: @@ -381,8 +401,11 @@ def Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj): bpy.ops.object.mode_set(mode='OBJECT') + #Create a fake obj export to a string + #Will strip this down further later fakeFile = StringIO() blender_xatlas.export_obj_simple.save( + rename_dict=rename_dict, context=bpy.context, filepath=fakeFile, mainUVChoiceType=sharedProperties.mainUVChoiceType, @@ -393,20 +416,26 @@ def Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj): use_mesh_modifiers=True, use_edges=True, use_smooth_groups=False, - use_smooth_groups_bitflags=False, + use_smooth_groups_bitflags=False, use_normals=True, use_uvs=True, use_materials=False, use_triangles=False, - use_nurbs=False, - use_vertex_groups=False, + use_nurbs=False, + use_vertex_groups=False, use_blen_objects=True, group_by_object=False, group_by_material=False, keep_vertex_order=False, ) - file_path = os.path.dirname(os.path.abspath(blender_xatlas.__file__)) + #print just for reference + # print(fakeFile.getvalue()) + + #get the path to xatlas + #file_path = os.path.dirname(os.path.abspath(__file__)) + scriptsDir = bpy.utils.user_resource('SCRIPTS', "addons") + file_path = os.path.join(scriptsDir, "blender_xatlas") if platform.system() == "Windows": xatlas_path = os.path.join(file_path, "xatlas", "xatlas-blender.exe") elif platform.system() == "Linux": @@ -458,6 +487,8 @@ def Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj): shell=True ) + print(xatlas_path) + #shove the fake file in stdin stdin = xatlas_process.stdin value = bytes(fakeFile.getvalue() + "\n", 'UTF-8') #The \n is needed to end the input properly @@ -482,17 +513,17 @@ def Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj): obName: string = "" uvArray: List[float] = field(default_factory=list) faceArray: List[int] = field(default_factory=list) - + convertedObjects = [] uvArrayComplete = [] - + #search through the out put for STARTOBJ #then start reading the objects obTest = None startRead = False for line in outObj.splitlines(): - + line_split = line.split() if not line_split: @@ -504,14 +535,14 @@ def Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj): print("Start reading the objects----------------------------------------") startRead = True # obTest = uvObject() - + if startRead: #if it's a new obj if line_start == 'o': #if there is already an object append it if obTest is not None: convertedObjects.append(obTest) - + obTest = uvObject() #create new uv object obTest.obName = line_split[1] @@ -536,9 +567,9 @@ def Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj): #append the final object convertedObjects.append(obTest) - # print(convertedObjects) - - + print(convertedObjects) + + #apply the output------------------------------------------------------------- #copy the uvs to the original objects # objIndex = 0 @@ -548,7 +579,7 @@ def Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj): bpy.ops.object.select_all(action='DESELECT') obTest = importObject - + obTest.obName = safe_dict[obTest.obName] #probably shouldn't just replace it bpy.context.scene.objects[obTest.obName].select_set(True) context.view_layer.objects.active = bpy.context.scene.objects[obTest.obName] bpy.ops.object.mode_set(mode = 'OBJECT') @@ -563,7 +594,7 @@ def Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj): nFaces = len(bm.faces) #need to ensure lookup table for some reason? - if hasattr(bm.faces, "ensure_lookup_table"): + if hasattr(bm.faces, "ensure_lookup_table"): bm.faces.ensure_lookup_table() #loop through the faces @@ -601,7 +632,7 @@ def Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj): currentObject = bpy.context.scene.objects[edgeList['object']] bm = bmesh.new() bm.from_mesh(currentObject.data) - if hasattr(bm.edges, "ensure_lookup_table"): + if hasattr(bm.edges, "ensure_lookup_table"): bm.edges.ensure_lookup_table() #assume that all the triangulated edges come after the original edges @@ -617,6 +648,27 @@ def Unwrap_Lightmap_Group_Xatlas_2_headless_call(obj): bm.free() bpy.ops.object.mode_set(mode='EDIT') - #End setting the quads back again------------------------------------------------------------ + #End setting the quads back again------------------------------------------------------------- - print("Finished Xatlas----------------------------------------") \ No newline at end of file + #select the original objects that were selected + for objectName in rename_dict: + if objectName[0] in bpy.context.scene.objects: + current_object = bpy.context.scene.objects[objectName[0]] + current_object.select_set(True) + context.view_layer.objects.active = current_object + + bpy.ops.object.mode_set(mode=startingMode) + + print("Finished Xatlas----------------------------------------") + return {'FINISHED'} + +def transfer_assets(copy, source, destination): + for filename in glob.glob(os.path.join(source, '*.*')): + shutil.copy(filename, destination) + +def transfer_load(): + load_folder = bpy.path.abspath(os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_SceneProperties.tlm_load_folder)) + lightmap_folder = os.path.join(os.path.dirname(bpy.data.filepath), bpy.context.scene.TLM_EngineProperties.tlm_lightmap_savedir) + print(load_folder) + print(lightmap_folder) + transfer_assets(True, load_folder, lightmap_folder) \ No newline at end of file diff --git a/blender/arm/props_bake.py b/blender/arm/props_bake.py index 25bff93a..e433e682 100644 --- a/blender/arm/props_bake.py +++ b/blender/arm/props_bake.py @@ -3,7 +3,7 @@ import arm.assets import bpy from bpy.types import Menu, Panel, UIList from bpy.props import * -from arm.lightmapper import operators, properties, utility, keymap +from arm.lightmapper import operators, properties, utility class ArmBakeListItem(bpy.types.PropertyGroup): obj: PointerProperty(type=bpy.types.Object, description="The object to bake") @@ -361,7 +361,6 @@ def register(): operators.register() properties.register() - keymap.register() def unregister(): bpy.utils.unregister_class(ArmBakeListItem) @@ -380,5 +379,4 @@ def unregister(): #Unregister lightmapper operators.unregister() - properties.unregister() - keymap.unregister() \ No newline at end of file + properties.unregister() \ No newline at end of file diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 32f3c719..d8531192 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -20,6 +20,7 @@ import arm.utils from arm.lightmapper.utility import icon from arm.lightmapper.properties.denoiser import oidn, optix +from arm.lightmapper.panels import scene import importlib @@ -73,9 +74,18 @@ class ARM_PT_ObjectPropsPanel(bpy.types.Panel): if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: row = layout.row() - row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_resolution") + row.prop(obj.TLM_ObjectProperties, "tlm_use_default_channel") + + if not obj.TLM_ObjectProperties.tlm_use_default_channel: + + row = layout.row() + row.prop_search(obj.TLM_ObjectProperties, "tlm_uv_channel", obj.data, "uv_layers", text='UV Channel') + row = layout.row() - row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_unwrap_mode") + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_resolution") + if obj.TLM_ObjectProperties.tlm_use_default_channel: + row = layout.row() + row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_unwrap_mode") row = layout.row() if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": @@ -93,7 +103,6 @@ class ARM_PT_ObjectPropsPanel(bpy.types.Panel): row.prop(obj.TLM_ObjectProperties, "tlm_postpack_object") row = layout.row() - if obj.TLM_ObjectProperties.tlm_postpack_object and obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode != "AtlasGroupA": if scene.TLM_PostAtlasListItem >= 0 and len(scene.TLM_PostAtlasList) > 0: row = layout.row() @@ -1482,423 +1491,6 @@ class ARM_PT_BakePanel(bpy.types.Panel): layout.prop(item, "res_x") layout.prop(item, "res_y") - else: - - scene = context.scene - sceneProperties = scene.TLM_SceneProperties - row = layout.row(align=True) - - row = layout.row(align=True) - - #We list LuxCoreRender as available, by default we assume Cycles exists - row.prop(sceneProperties, "tlm_lightmap_engine") - - if sceneProperties.tlm_lightmap_engine == "Cycles": - - #CYCLES SETTINGS HERE - engineProperties = scene.TLM_EngineProperties - - row = layout.row(align=True) - row.label(text="General Settings") - row = layout.row(align=True) - row.operator("tlm.build_lightmaps") - row = layout.row(align=True) - row.operator("tlm.clean_lightmaps") - row = layout.row(align=True) - row.operator("tlm.explore_lightmaps") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_apply_on_unwrap") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_headless") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_alert_on_finish") - - if sceneProperties.tlm_alert_on_finish: - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_alert_sound") - - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_verbose") - #row = layout.row(align=True) - #row.prop(sceneProperties, "tlm_compile_statistics") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_override_bg_color") - if sceneProperties.tlm_override_bg_color: - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_override_color") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_reset_uv") - - row = layout.row(align=True) - try: - if bpy.context.scene["TLM_Buildstat"] is not None: - row.label(text="Last build completed in: " + str(bpy.context.scene["TLM_Buildstat"][0])) - except: - pass - - row = layout.row(align=True) - row.label(text="Cycles Settings") - - row = layout.row(align=True) - row.prop(engineProperties, "tlm_mode") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_quality") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_resolution_scale") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_bake_mode") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_lighting_mode") - - if scene.TLM_EngineProperties.tlm_bake_mode == "Background": - row = layout.row(align=True) - row.label(text="Warning! Background mode is currently unstable", icon_value=2) - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_network_render") - if sceneProperties.tlm_network_render: - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_network_paths") - #row = layout.row(align=True) - #row.prop(sceneProperties, "tlm_network_dir") - row = layout.row(align=True) - row = layout.row(align=True) - row.prop(engineProperties, "tlm_caching_mode") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_directional_mode") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_lightmap_savedir") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_dilation_margin") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_exposure_multiplier") - row = layout.row(align=True) - row.prop(engineProperties, "tlm_setting_supersample") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_metallic_clamp") - - elif sceneProperties.tlm_lightmap_engine == "LuxCoreRender": - - #LUXCORE SETTINGS HERE - luxcore_available = False - - #Look for Luxcorerender in the renderengine classes - for engine in bpy.types.RenderEngine.__subclasses__(): - if engine.bl_idname == "LUXCORE": - luxcore_available = True - break - - row = layout.row(align=True) - if not luxcore_available: - row.label(text="Please install BlendLuxCore.") - else: - row.label(text="LuxCoreRender not yet available.") - - elif sceneProperties.tlm_lightmap_engine == "OctaneRender": - - #LUXCORE SETTINGS HERE - octane_available = False - - row = layout.row(align=True) - row.label(text="Octane Render not yet available.") - - - ################## - #DENOISE SETTINGS! - row = layout.row(align=True) - row.label(text="Denoise Settings") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_denoise_use") - row = layout.row(align=True) - - if sceneProperties.tlm_denoise_use: - row.prop(sceneProperties, "tlm_denoise_engine", expand=True) - row = layout.row(align=True) - - if sceneProperties.tlm_denoise_engine == "Integrated": - row.label(text="No options for Integrated.") - elif sceneProperties.tlm_denoise_engine == "OIDN": - denoiseProperties = scene.TLM_OIDNEngineProperties - row.prop(denoiseProperties, "tlm_oidn_path") - row = layout.row(align=True) - row.prop(denoiseProperties, "tlm_oidn_verbose") - row = layout.row(align=True) - row.prop(denoiseProperties, "tlm_oidn_threads") - row = layout.row(align=True) - row.prop(denoiseProperties, "tlm_oidn_maxmem") - row = layout.row(align=True) - row.prop(denoiseProperties, "tlm_oidn_affinity") - # row = layout.row(align=True) - # row.prop(denoiseProperties, "tlm_denoise_ao") - elif sceneProperties.tlm_denoise_engine == "Optix": - denoiseProperties = scene.TLM_OptixEngineProperties - row.prop(denoiseProperties, "tlm_optix_path") - row = layout.row(align=True) - row.prop(denoiseProperties, "tlm_optix_verbose") - row = layout.row(align=True) - row.prop(denoiseProperties, "tlm_optix_maxmem") - - - ################## - #FILTERING SETTINGS! - row = layout.row(align=True) - row.label(text="Filtering Settings") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_filtering_use") - row = layout.row(align=True) - - if sceneProperties.tlm_filtering_use: - - if sceneProperties.tlm_filtering_engine == "OpenCV": - - cv2 = importlib.util.find_spec("cv2") - - if cv2 is None: - row = layout.row(align=True) - row.label(text="OpenCV is not installed. Install it below.") - row = layout.row(align=True) - row.label(text="It is recommended to install as administrator.") - row = layout.row(align=True) - row.operator("tlm.install_opencv_lightmaps") - else: - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_filtering_mode") - row = layout.row(align=True) - if scene.TLM_SceneProperties.tlm_filtering_mode == "Gaussian": - row.prop(scene.TLM_SceneProperties, "tlm_filtering_gaussian_strength") - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") - elif scene.TLM_SceneProperties.tlm_filtering_mode == "Box": - row.prop(scene.TLM_SceneProperties, "tlm_filtering_box_strength") - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") - - elif scene.TLM_SceneProperties.tlm_filtering_mode == "Bilateral": - row.prop(scene.TLM_SceneProperties, "tlm_filtering_bilateral_diameter") - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_filtering_bilateral_color_deviation") - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_filtering_bilateral_coordinate_deviation") - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") - else: - row.prop(scene.TLM_SceneProperties, "tlm_filtering_median_kernel", expand=True) - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_filtering_iterations") - else: - row = layout.row(align=True) - row.prop(scene.TLM_SceneProperties, "tlm_numpy_filtering_mode") - - - ################## - #ENCODING SETTINGS! - row = layout.row(align=True) - row.label(text="Encoding Settings") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_encoding_use") - row = layout.row(align=True) - - if sceneProperties.tlm_encoding_use: - - - if scene.TLM_EngineProperties.tlm_bake_mode == "Background": - row.label(text="Encoding options disabled in background mode") - row = layout.row(align=True) - - row.prop(sceneProperties, "tlm_encoding_device", expand=True) - row = layout.row(align=True) - - if sceneProperties.tlm_encoding_device == "CPU": - row.prop(sceneProperties, "tlm_encoding_mode_a", expand=True) - else: - row.prop(sceneProperties, "tlm_encoding_mode_b", expand=True) - - if sceneProperties.tlm_encoding_device == "CPU": - if sceneProperties.tlm_encoding_mode_a == "RGBM": - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_encoding_range") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_decoder_setup") - if sceneProperties.tlm_encoding_mode_a == "RGBD": - pass - if sceneProperties.tlm_encoding_mode_a == "HDR": - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_format") - else: - - if sceneProperties.tlm_encoding_mode_b == "RGBM": - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_encoding_range") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_decoder_setup") - - if sceneProperties.tlm_encoding_mode_b == "LogLuv" and sceneProperties.tlm_encoding_device == "GPU": - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_decoder_setup") - if sceneProperties.tlm_encoding_mode_b == "HDR": - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_format") - - ################## - #SELECTION OPERATORS! - - row = layout.row(align=True) - row.label(text="Selection Operators") - row = layout.row(align=True) - - row = layout.row(align=True) - row.operator("tlm.enable_selection") - row = layout.row(align=True) - row.operator("tlm.disable_selection") - row = layout.row(align=True) - row.prop(sceneProperties, "tlm_override_object_settings") - - if sceneProperties.tlm_override_object_settings: - - row = layout.row(align=True) - row = layout.row() - row.prop(sceneProperties, "tlm_mesh_lightmap_unwrap_mode") - row = layout.row() - - if sceneProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": - - if scene.TLM_AtlasListItem >= 0 and len(scene.TLM_AtlasList) > 0: - row = layout.row() - item = scene.TLM_AtlasList[scene.TLM_AtlasListItem] - row.prop_search(sceneProperties, "tlm_atlas_pointer", scene, "TLM_AtlasList", text='Atlas Group') - else: - row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") - - else: - row = layout.row() - row.prop(sceneProperties, "tlm_postpack_object") - row = layout.row() - - if sceneProperties.tlm_postpack_object and sceneProperties.tlm_mesh_lightmap_unwrap_mode != "AtlasGroupA": - if scene.TLM_PostAtlasListItem >= 0 and len(scene.TLM_PostAtlasList) > 0: - row = layout.row() - item = scene.TLM_PostAtlasList[scene.TLM_PostAtlasListItem] - row.prop_search(sceneProperties, "tlm_postatlas_pointer", scene, "TLM_PostAtlasList", text='Atlas Group') - row = layout.row() - - else: - row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") - row = layout.row() - - if sceneProperties.tlm_mesh_lightmap_unwrap_mode != "AtlasGroupA": - row.prop(sceneProperties, "tlm_mesh_lightmap_resolution") - row = layout.row() - row.prop(sceneProperties, "tlm_mesh_unwrap_margin") - - row = layout.row(align=True) - row.operator("tlm.remove_uv_selection") - row = layout.row(align=True) - row.operator("tlm.select_lightmapped_objects") - row = layout.row(align=True) - - ################## - #Additional settings - row = layout.row(align=True) - row.label(text="Additional options") - sceneProperties = scene.TLM_SceneProperties - atlasListItem = scene.TLM_AtlasListItem - atlasList = scene.TLM_AtlasList - postatlasListItem = scene.TLM_PostAtlasListItem - postatlasList = scene.TLM_PostAtlasList - - layout.label(text="Atlas Groups") - row = layout.row() - row.prop(sceneProperties, "tlm_atlas_mode", expand=True) - - if sceneProperties.tlm_atlas_mode == "Prepack": - - rows = 2 - if len(atlasList) > 1: - rows = 4 - row = layout.row() - row.template_list("TLM_UL_AtlasList", "Atlas List", scene, "TLM_AtlasList", scene, "TLM_AtlasListItem", rows=rows) - col = row.column(align=True) - col.operator("tlm_atlaslist.new_item", icon='ADD', text="") - col.operator("tlm_atlaslist.delete_item", icon='REMOVE', text="") - #col.menu("ARM_MT_BakeListSpecials", icon='DOWNARROW_HLT', text="") - - # if len(scene.TLM_AtlasList) > 1: - # col.separator() - # op = col.operator("arm_bakelist.move_item", icon='TRIA_UP', text="") - # op.direction = 'UP' - # op = col.operator("arm_bakelist.move_item", icon='TRIA_DOWN', text="") - # op.direction = 'DOWN' - - if atlasListItem >= 0 and len(atlasList) > 0: - item = atlasList[atlasListItem] - #layout.prop_search(item, "obj", bpy.data, "objects", text="Object") - #layout.prop(item, "res_x") - layout.prop(item, "tlm_atlas_lightmap_unwrap_mode") - layout.prop(item, "tlm_atlas_lightmap_resolution") - layout.prop(item, "tlm_atlas_unwrap_margin") - - amount = 0 - - for obj in bpy.data.objects: - if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: - if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": - if obj.TLM_ObjectProperties.tlm_atlas_pointer == item.name: - amount = amount + 1 - - layout.label(text="Objects: " + str(amount)) - - # layout.use_property_split = True - # layout.use_property_decorate = False - # layout.label(text="Enable for selection") - # layout.label(text="Disable for selection") - # layout.label(text="Something...") - - else: - - layout.label(text="Postpacking is unstable.") - rows = 2 - if len(atlasList) > 1: - rows = 4 - row = layout.row() - row.template_list("TLM_UL_PostAtlasList", "PostList", scene, "TLM_PostAtlasList", scene, "TLM_PostAtlasListItem", rows=rows) - col = row.column(align=True) - col.operator("tlm_postatlaslist.new_item", icon='ADD', text="") - col.operator("tlm_postatlaslist.delete_item", icon='REMOVE', text="") - - if postatlasListItem >= 0 and len(postatlasList) > 0: - item = postatlasList[postatlasListItem] - layout.prop(item, "tlm_atlas_lightmap_resolution") - - #Below list object counter - amount = 0 - utilized = 0 - atlasUsedArea = 0 - atlasSize = item.tlm_atlas_lightmap_resolution - - for obj in bpy.data.objects: - if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: - if obj.TLM_ObjectProperties.tlm_postpack_object: - if obj.TLM_ObjectProperties.tlm_postatlas_pointer == item.name: - amount = amount + 1 - - atlasUsedArea += int(obj.TLM_ObjectProperties.tlm_mesh_lightmap_resolution) ** 2 - - row = layout.row() - row.prop(item, "tlm_atlas_repack_on_cleanup") - - #TODO SET A CHECK FOR THIS! ADD A CV2 CHECK TO UTILITY! - cv2 = True - - if cv2: - row = layout.row() - row.prop(item, "tlm_atlas_dilation") - layout.label(text="Objects: " + str(amount)) - - utilized = atlasUsedArea / (int(atlasSize) ** 2) - layout.label(text="Utilized: " + str(utilized * 100) + "%") - - if (utilized * 100) > 100: - layout.label(text="Warning! Overflow not yet supported") - class ArmGenLodButton(bpy.types.Operator): """Automatically generate LoD levels.""" bl_idname = 'arm.generate_lod' @@ -2671,6 +2263,13 @@ def register(): bpy.utils.register_class(ArmoryUpdateListAndroidEmulatorRunButton) bpy.utils.register_class(ArmoryUpdateListInstalledVSButton) + bpy.utils.register_class(scene.TLM_PT_Settings) + bpy.utils.register_class(scene.TLM_PT_Denoise) + bpy.utils.register_class(scene.TLM_PT_Filtering) + bpy.utils.register_class(scene.TLM_PT_Encoding) + bpy.utils.register_class(scene.TLM_PT_Utility) + bpy.utils.register_class(scene.TLM_PT_Additional) + bpy.types.VIEW3D_HT_header.append(draw_view3d_header) bpy.types.VIEW3D_MT_object.append(draw_view3d_object_menu) bpy.types.NODE_MT_context_menu.append(draw_custom_node_menu) @@ -2743,3 +2342,10 @@ def unregister(): bpy.utils.unregister_class(ArmSyncProxyButton) bpy.utils.unregister_class(ArmPrintTraitsButton) bpy.utils.unregister_class(ARM_PT_MaterialNodePanel) + + bpy.utils.unregister_class(scene.TLM_PT_Settings) + bpy.utils.unregister_class(scene.TLM_PT_Denoise) + bpy.utils.unregister_class(scene.TLM_PT_Filtering) + bpy.utils.unregister_class(scene.TLM_PT_Encoding) + bpy.utils.unregister_class(scene.TLM_PT_Utility) + bpy.utils.unregister_class(scene.TLM_PT_Additional) \ No newline at end of file From 5fe816d16a35fec61ee866c04df36fb20d348a9a Mon Sep 17 00:00:00 2001 From: Alexander Kleemann Date: Thu, 18 Mar 2021 19:25:02 +0100 Subject: [PATCH 50/63] Add in-menu option for OpenCV installation --- blender/arm/lightmapper/panels/scene.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/blender/arm/lightmapper/panels/scene.py b/blender/arm/lightmapper/panels/scene.py index 56c05cb7..de36f22f 100644 --- a/blender/arm/lightmapper/panels/scene.py +++ b/blender/arm/lightmapper/panels/scene.py @@ -259,7 +259,9 @@ class TLM_PT_Filtering(bpy.types.Panel): if cv2 is None: row = layout.row(align=True) - row.label(text="OpenCV is not installed. Install it through preferences.") + row.label(text="OpenCV is not installed. Please install it as an administrator.") + row = layout.row(align=True) + row.operator("tlm.install_opencv_lightmaps") else: row = layout.row(align=True) row.prop(scene.TLM_SceneProperties, "tlm_filtering_mode") From eb6d23fab353aecfa923cd8218736092edd5a28e Mon Sep 17 00:00:00 2001 From: Alexander Kleemann Date: Mon, 22 Mar 2021 20:26:01 +0100 Subject: [PATCH 51/63] Fix material translucency in Blender 2.9+ The extra input node in the principled is now taken into consideration --- blender/arm/material/mat_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/blender/arm/material/mat_utils.py b/blender/arm/material/mat_utils.py index 9d6eb1f0..b1567a72 100644 --- a/blender/arm/material/mat_utils.py +++ b/blender/arm/material/mat_utils.py @@ -73,7 +73,8 @@ def is_transluc_type(node): node.type == 'BSDF_TRANSPARENT' or \ node.type == 'BSDF_TRANSLUCENT' or \ (node.type == 'GROUP' and node.node_tree.name.startswith('Armory PBR') and (node.inputs[1].is_linked or node.inputs[1].default_value != 1.0)) or \ - (node.type == 'BSDF_PRINCIPLED' and len(node.inputs) > 20 and (node.inputs[18].is_linked or node.inputs[18].default_value != 1.0)): + (node.type == 'BSDF_PRINCIPLED' and len(node.inputs) > 20 and (node.inputs[18].is_linked or node.inputs[18].default_value != 1.0)) or \ + (node.type == 'BSDF_PRINCIPLED' and len(node.inputs) > 21 and (node.inputs[19].is_linked or node.inputs[19].default_value != 1.0)): return True return False From 05c14238f25fa83d2679a479bbdac5c2d44b8278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 25 Mar 2021 23:03:08 +0100 Subject: [PATCH 52/63] shader.py: add API to set texture params --- blender/arm/material/shader.py | 36 +++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/blender/arm/material/shader.py b/blender/arm/material/shader.py index 7b00ed6d..b094a734 100644 --- a/blender/arm/material/shader.py +++ b/blender/arm/material/shader.py @@ -119,16 +119,29 @@ class ShaderContext: c['link'] = link self.constants.append(c) - def add_texture_unit(self, ctype, name, link=None, is_image=None): + def add_texture_unit(self, name, link=None, is_image=None, + addr_u=None, addr_v=None, + filter_min=None, filter_mag=None, mipmap_filter=None): for c in self.tunits: if c['name'] == name: return - c = { 'name': name } - if link != None: + c = {'name': name} + if link is not None: c['link'] = link - if is_image != None: + if is_image is not None: c['is_image'] = is_image + if addr_u is not None: + c['addressing_u'] = addr_u + if addr_v is not None: + c['addressing_v'] = addr_v + if filter_min is not None: + c['filter_min'] = filter_min + if filter_mag is not None: + c['filter_mag'] = filter_mag + if mipmap_filter is not None: + c['mipmap_filter'] = mipmap_filter + self.tunits.append(c) def make_vert(self, custom_name: str = None): @@ -213,7 +226,10 @@ class Shader: if s not in self.outs: self.outs.append(s) - def add_uniform(self, s, link=None, included=False): + def add_uniform(self, s, link=None, included=False, + tex_addr_u=None, tex_addr_v=None, + tex_filter_min=None, tex_filter_mag=None, + tex_mipmap_filter=None): ar = s.split(' ') # layout(RGBA8) image3D voxels utype = ar[-2] @@ -224,9 +240,15 @@ class Shader: # Add individual units - mySamplers[0], mySamplers[1] for i in range(int(uname[-2])): uname_array = uname[:-2] + str(i) + ']' - self.context.add_texture_unit(utype, uname_array, link=link, is_image=is_image) + self.context.add_texture_unit( + uname_array, link, is_image, + tex_addr_u, tex_addr_v, + tex_filter_min, tex_filter_mag, tex_mipmap_filter) else: - self.context.add_texture_unit(utype, uname, link=link, is_image=is_image) + self.context.add_texture_unit( + uname, link, is_image, + tex_addr_u, tex_addr_v, + tex_filter_min, tex_filter_mag, tex_mipmap_filter) else: # Prefer vec4[] for d3d to avoid padding if ar[0] == 'float' and '[' in ar[1]: From 8d812548c431e8820586c10fffc7d6afcfdec547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 25 Mar 2021 23:25:41 +0100 Subject: [PATCH 53/63] Use 2D LUT for Nishita skies --- Shaders/std/sky.glsl | 63 ++++--- Sources/armory/object/Uniforms.hx | 2 +- Sources/armory/renderpath/Nishita.hx | 158 ++++++++++++++---- .../material/cycles_nodes/nodes_texture.py | 2 + 4 files changed, 159 insertions(+), 66 deletions(-) diff --git a/Shaders/std/sky.glsl b/Shaders/std/sky.glsl index 61f93c84..5a58734f 100644 --- a/Shaders/std/sky.glsl +++ b/Shaders/std/sky.glsl @@ -1,12 +1,13 @@ /* Various sky functions * ===================== * - * Nishita model is based on https://github.com/wwwtyro/glsl-atmosphere(Unlicense License) + * Nishita model is based on https://github.com/wwwtyro/glsl-atmosphere (Unlicense License) * * Changes to the original implementation: * - r and pSun parameters of nishita_atmosphere() are already normalized * - Some original parameters of nishita_atmosphere() are replaced with pre-defined values * - Implemented air, dust and ozone density node parameters (see Blender source) + * - Replaced the inner integral calculation with a LUT lookup * * Reference for the sun's limb darkening and ozone calculations: * [Hill] Sebastien Hillaire. Physically Based Sky, Atmosphere and Cloud Rendering in Frostbite @@ -19,15 +20,18 @@ #ifndef _SKY_GLSL_ #define _SKY_GLSL_ -// OpenGl ES doesn't support 1D textures so we use a 1 px height sampler2D here... uniform sampler2D nishitaLUT; -#define PI 3.141592 +#ifndef PI + #define PI 3.141592 +#endif +#ifndef HALF_PI + #define HALF_PI 1.570796 +#endif #define nishita_iSteps 16 -#define nishita_jSteps 8 -// The values here are taken from Cycles code if they +// These values are taken from Cycles code if they // exist there, otherwise they are taken from the example // in the glsl-atmosphere repo #define nishita_sun_intensity 22.0 @@ -49,7 +53,17 @@ uniform sampler2D nishitaLUT; // Values from [Hill: 60] #define sun_limb_darkening_col vec3(0.397, 0.503, 0.652) -#define heightToLUT(h) (textureLod(nishitaLUT, vec2(clamp(h * (1 / 60000.0), 0.0, 1.0), 0.0), 0.0).xyz * 10.0) +float random(vec2 coords) { + return fract(sin(dot(coords.xy, vec2(12.9898,78.233))) * 43758.5453); +} + +vec3 nishita_lookupLUT(const float height, const float sunTheta) { + vec2 coords = vec2( + sqrt(height * (1 / nishita_atmo_radius)), + 0.5 + 0.5 * sign(sunTheta - HALF_PI) * sqrt(abs(sunTheta * (1 / HALF_PI) - 1)) + ); + return textureLod(nishitaLUT, coords, 0.0).rgb; +} /* Approximates the density of ozone for a given sample height. Values taken from Cycles code. */ float nishita_density_ozone(const float height) { @@ -112,40 +126,21 @@ vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const floa // Calculate the height of the sample. float iHeight = length(iPos) - rPlanet; - // Calculate the optical depth of the Rayleigh and Mie scattering for this step. - vec3 iLookup = heightToLUT(iHeight); - float odStepRlh = iLookup.x * iStepSize; - float odStepMie = iLookup.y * iStepSize; + // Calculate the optical depth of the Rayleigh and Mie scattering for this step + float odStepRlh = exp(-iHeight / nishita_rayleigh_scale) * density.x * iStepSize; + float odStepMie = exp(-iHeight / nishita_mie_scale) * density.y * iStepSize; // Accumulate optical depth. iOdRlh += odStepRlh; iOdMie += odStepMie; - // Calculate the step size of the secondary ray. - float jStepSize = nishita_rsi(iPos, pSun, nishita_atmo_radius).y / float(nishita_jSteps); + // Idea behind this: "Rotate" everything by iPos (-> iPos is the new zenith) and then all calculations for the + // inner integral only depend on the sample height (iHeight) and sunTheta (angle between sun and new zenith). + float sunTheta = acos(dot(normalize(iPos), normalize(pSun))); + vec3 jODepth = nishita_lookupLUT(iHeight, sunTheta);// * vec3(14000000 / 255, 14000000 / 255, 2000000 / 255); - // Initialize the secondary ray time. - float jTime = 0.0; - - // Initialize optical depth accumulators for the secondary ray. - vec3 jODepth = vec3(0.0); // (Rayleigh, Mie, ozone) - - // Sample the secondary ray. - for (int j = 0; j < nishita_jSteps; j++) { - - // Calculate the secondary ray sample position. - vec3 jPos = iPos + pSun * (jTime + jStepSize * 0.5); - - // Calculate the height of the sample. - float jHeight = length(jPos) - rPlanet; - - // Accumulate the optical depth. - vec3 jLookup = heightToLUT(jHeight); - jODepth += jLookup * jStepSize; - - // Increment the secondary ray time. - jTime += jStepSize; - } + // Apply dithering to reduce visible banding + jODepth += mix(-1000, 1000, random(r.xy)); // Calculate attenuation. vec3 attn = exp(-( diff --git a/Sources/armory/object/Uniforms.hx b/Sources/armory/object/Uniforms.hx index 64d20af2..e492f2fb 100644 --- a/Sources/armory/object/Uniforms.hx +++ b/Sources/armory/object/Uniforms.hx @@ -21,7 +21,7 @@ class Uniforms { public static function textureLink(object: Object, mat: MaterialData, link: String): kha.Image { if (link == "_nishitaLUT") { if (armory.renderpath.Nishita.data == null) armory.renderpath.Nishita.recompute(Scene.active.world); - return armory.renderpath.Nishita.data.optDepthLUT; + return armory.renderpath.Nishita.data.lut; } #if arm_ltc else if (link == "_ltcMat") { diff --git a/Sources/armory/renderpath/Nishita.hx b/Sources/armory/renderpath/Nishita.hx index c968798d..1a4cac92 100644 --- a/Sources/armory/renderpath/Nishita.hx +++ b/Sources/armory/renderpath/Nishita.hx @@ -5,36 +5,77 @@ import kha.graphics4.TextureFormat; import kha.graphics4.Usage; import iron.data.WorldData; +import iron.math.Vec2; +import iron.math.Vec3; import armory.math.Helper; +/** + Utility class to control the Nishita sky model. +**/ class Nishita { public static var data: NishitaData = null; + /** + Recompute the nishita lookup table. Call this function after updating + the sky density settings. + **/ public static function recompute(world: WorldData) { if (world == null || world.raw.sun_direction == null) return; if (data == null) data = new NishitaData(); // TODO - data.recompute(1.0, 1.0, 1.0); + data.computeLUT(new Vec3(1.0, 1.0, 1.0)); } } +/** + This class holds the precalculated result of the inner scattering integral + of the Nishita sky model. The outer integral is calculated in + [`armory/Shaders/std/sky.glsl`](https://github.com/armory3d/armory/blob/master/Shaders/std/sky.glsl). + + @see `armory.renderpath.Nishita` +**/ class NishitaData { - static inline var LUT_WIDTH = 16; - /** Maximum ray height as defined by Cycles **/ - static inline var MAX_HEIGHT = 60000; + public var lut: kha.Image; - static inline var RAYLEIGH_SCALE = 8e3; - static inline var MIE_SCALE = 1.2e3; + /** + The amount of individual sample heights stored in the LUT (and the width + of the LUT image). + **/ + public static var lutHeightSteps = 128; + /** + The amount of individual sun angle steps stored in the LUT (and the + height of the LUT image). + **/ + public static var lutAngleSteps = 128; - public var optDepthLUT: kha.Image; + /** + Amount of steps for calculating the inner scattering integral. Heigher + values are more precise but take longer to compute. + **/ + public static var jSteps = 8; + + /** Radius of the atmosphere in meters. **/ + public static var radiusAtmo = 6420000; + /** + Radius of the planet in meters. The default value is the earth radius as + defined in Cycles. + **/ + public static var radiusPlanet = 6360000; + + /** Rayleigh scattering scale parameter. **/ + public static var rayleighScale = 8e3; + /** Mie scattering scale parameter. **/ + public static var mieScale = 1.2e3; public function new() {} + /** Approximates the density of ozone for a given sample height. **/ function getOzoneDensity(height: FastFloat): FastFloat { + // Values are taken from Cycles code if (height < 10000.0 || height >= 40000.0) { return 0.0; } @@ -45,36 +86,91 @@ class NishitaData { } /** - The RGBA texture layout looks as follows: - R = Rayleigh optical depth at height \in [0, 60000] - G = Mie optical depth at height \in [0, 60000] - B = Ozone optical depth at height \in [0, 60000] - A = Unused + Ray-sphere intersection test that assumes the sphere is centered at the + origin. There is no intersection when result.x > result.y. Otherwise + this function returns the distances to the two intersection points, + which might be equal. **/ - public function recompute(densityFacAir: FastFloat, densityFacDust: FastFloat, densityFacOzone: FastFloat) { - optDepthLUT = kha.Image.create(LUT_WIDTH, 1, TextureFormat.RGBA32, Usage.StaticUsage); + function raySphereIntersection(rayOrigin: Vec3, rayDirection: Vec3, sphereRadius: Int): Vec2 { + // Algorithm is described here: https://en.wikipedia.org/wiki/Line%E2%80%93sphere_intersection + var a = rayDirection.dot(rayDirection); + var b = 2.0 * rayDirection.dot(rayOrigin); + var c = rayOrigin.dot(rayOrigin) - (sphereRadius * sphereRadius); + var d = (b * b) - 4.0 * a * c; - var textureData = optDepthLUT.lock(); - for (i in 0...LUT_WIDTH) { - // Get the height for each LUT pixel i (in [-1, 1] range) - var height = (i / LUT_WIDTH) * 2 - 1; + // Ray does not intersect the sphere + if (d < 0.0) return new Vec2(1e5, -1e5); + + return new Vec2( + (-b - Math.sqrt(d)) / (2.0 * a), + (-b + Math.sqrt(d)) / (2.0 * a) + ); + } + + /** + Computes the LUT texture for the given density values. + @param density 3D vector of air density, dust density, ozone density + **/ + public function computeLUT(density: Vec3) { + var imageData = new haxe.io.Float32Array(lutHeightSteps * lutAngleSteps * 4); + + for (x in 0...lutHeightSteps) { + var height = (x / (lutHeightSteps - 1)); // Use quadratic height for better horizon precision - // See https://sebh.github.io/publications/egsr2020.pdf (5.3) - height = 0.5 + 0.5 * Helper.sign(height) * Math.sqrt(Math.abs(height)); - height *= MAX_HEIGHT; // Denormalize + height *= height; + height *= radiusAtmo; // Denormalize - // Make sure we use 32 bit floats - var optDepthRayleigh: FastFloat = Math.exp(-height / RAYLEIGH_SCALE) * densityFacAir; - var optDepthMie: FastFloat = Math.exp(-height / MIE_SCALE) * densityFacDust; - var optDepthOzone: FastFloat = getOzoneDensity(height) * densityFacOzone; + for (y in 0...lutAngleSteps) { + var sunTheta = y / (lutAngleSteps - 1) * 2 - 1; - // 10 is the maximum density, so we divide by it to be able to use normalized values - textureData.set(i * 4 + 0, Std.int(optDepthRayleigh * 255 / 10)); - textureData.set(i * 4 + 1, Std.int(optDepthMie * 255 / 10)); - textureData.set(i * 4 + 2, Std.int(optDepthOzone * 255 / 10)); - textureData.set(i * 4 + 3, 255); // Unused + // Improve horizon precision + // See https://sebh.github.io/publications/egsr2020.pdf (5.3) + sunTheta = Helper.sign(sunTheta) * sunTheta * sunTheta; + sunTheta = sunTheta * Math.PI / 2 + Math.PI / 2; // Denormalize + + var jODepth = sampleSecondaryRay(height, sunTheta, density); + + var pixelIndex = (x + y * lutHeightSteps) * 4; + imageData[pixelIndex + 0] = jODepth.x; + imageData[pixelIndex + 1] = jODepth.y; + imageData[pixelIndex + 2] = jODepth.z; + imageData[pixelIndex + 3] = 1.0; // Unused + } } - optDepthLUT.unlock(); + + lut = kha.Image.fromBytes(imageData.view.buffer, lutHeightSteps, lutAngleSteps, TextureFormat.RGBA128, Usage.StaticUsage); + } + + /** + Calculates the integral for the secondary ray. + **/ + public function sampleSecondaryRay(height: FastFloat, sunTheta: FastFloat, density: Vec3): Vec3 { + // Reconstruct values from the shader + var iPos = new Vec3(0, 0, height + radiusPlanet); + var pSun = new Vec3(0.0, Math.sin(sunTheta), Math.cos(sunTheta)).normalize(); + + var jTime: FastFloat = 0.0; + var jStepSize: FastFloat = raySphereIntersection(iPos, pSun, radiusAtmo).y / jSteps; + + // Optical depth accumulators for the secondary ray (Rayleigh, Mie, ozone) + var jODepth = new Vec3(); + + for (i in 0...jSteps) { + + // Calculate the secondary ray sample position and height + var jPos = iPos.clone().add(pSun.clone().mult(jTime + jStepSize * 0.5)); + var jHeight = jPos.length() - radiusPlanet; + + // Accumulate optical depth + var optDepthRayleigh = Math.exp(-jHeight / rayleighScale) * density.x; + var optDepthMie = Math.exp(-jHeight / mieScale) * density.y; + var optDepthOzone = getOzoneDensity(jHeight) * density.z; + jODepth.addf(optDepthRayleigh, optDepthMie, optDepthOzone); + + jTime += jStepSize; + } + + return jODepth.mult(jStepSize); } } diff --git a/blender/arm/material/cycles_nodes/nodes_texture.py b/blender/arm/material/cycles_nodes/nodes_texture.py index b3b81d1f..c88a8373 100644 --- a/blender/arm/material/cycles_nodes/nodes_texture.py +++ b/blender/arm/material/cycles_nodes/nodes_texture.py @@ -373,6 +373,8 @@ def parse_sky_nishita(node: bpy.types.ShaderNodeTexSky, state: ParserState) -> v curshader = state.curshader curshader.add_include('std/sky.glsl') curshader.add_uniform('vec3 sunDir', link='_sunDirection') + curshader.add_uniform('sampler2D nishitaLUT', link='_nishitaLUT', included=True, + tex_addr_u='clamp', tex_addr_v='clamp') planet_radius = 6360e3 # Earth radius used in Blender ray_origin_z = planet_radius + node.altitude From 845d2aff9376074d11ddee443a24d5d2ecbda647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 26 Mar 2021 15:39:18 +0100 Subject: [PATCH 54/63] Use switch/case for uniform links --- Sources/armory/object/Uniforms.hx | 305 +++++++++++++++--------------- 1 file changed, 156 insertions(+), 149 deletions(-) diff --git a/Sources/armory/object/Uniforms.hx b/Sources/armory/object/Uniforms.hx index e492f2fb..e87f809b 100644 --- a/Sources/armory/object/Uniforms.hx +++ b/Sources/armory/object/Uniforms.hx @@ -19,172 +19,179 @@ class Uniforms { } public static function textureLink(object: Object, mat: MaterialData, link: String): kha.Image { - if (link == "_nishitaLUT") { - if (armory.renderpath.Nishita.data == null) armory.renderpath.Nishita.recompute(Scene.active.world); - return armory.renderpath.Nishita.data.lut; + switch (link) { + case "_nishitaLUT": { + if (armory.renderpath.Nishita.data == null) armory.renderpath.Nishita.recompute(Scene.active.world); + return armory.renderpath.Nishita.data.lut; + } + #if arm_ltc + case "_ltcMat": { + if (armory.data.ConstData.ltcMatTex == null) armory.data.ConstData.initLTC(); + return armory.data.ConstData.ltcMatTex; + } + case "_ltcMag": { + if (armory.data.ConstData.ltcMagTex == null) armory.data.ConstData.initLTC(); + return armory.data.ConstData.ltcMagTex; + } + #end } - #if arm_ltc - else if (link == "_ltcMat") { - if (armory.data.ConstData.ltcMatTex == null) armory.data.ConstData.initLTC(); - return armory.data.ConstData.ltcMatTex; - } - else if (link == "_ltcMag") { - if (armory.data.ConstData.ltcMagTex == null) armory.data.ConstData.initLTC(); - return armory.data.ConstData.ltcMagTex; - } - #end + var target = iron.RenderPath.active.renderTargets.get(link.endsWith("_depth") ? link.substr(0, link.length - 6) : link); return target != null ? target.image : null; } public static function vec3Link(object: Object, mat: MaterialData, link: String): iron.math.Vec4 { var v: Vec4 = null; - #if arm_hosek - if (link == "_hosekA") { - if (armory.renderpath.HosekWilkie.data == null) { - armory.renderpath.HosekWilkie.recompute(Scene.active.world); + switch (link) { + #if arm_hosek + case "_hosekA": { + if (armory.renderpath.HosekWilkie.data == null) { + armory.renderpath.HosekWilkie.recompute(Scene.active.world); + } + if (armory.renderpath.HosekWilkie.data != null) { + v = iron.object.Uniforms.helpVec; + v.x = armory.renderpath.HosekWilkie.data.A.x; + v.y = armory.renderpath.HosekWilkie.data.A.y; + v.z = armory.renderpath.HosekWilkie.data.A.z; + } } - if (armory.renderpath.HosekWilkie.data != null) { + case "_hosekB": { + if (armory.renderpath.HosekWilkie.data == null) { + armory.renderpath.HosekWilkie.recompute(Scene.active.world); + } + if (armory.renderpath.HosekWilkie.data != null) { + v = iron.object.Uniforms.helpVec; + v.x = armory.renderpath.HosekWilkie.data.B.x; + v.y = armory.renderpath.HosekWilkie.data.B.y; + v.z = armory.renderpath.HosekWilkie.data.B.z; + } + } + case "_hosekC": { + if (armory.renderpath.HosekWilkie.data == null) { + armory.renderpath.HosekWilkie.recompute(Scene.active.world); + } + if (armory.renderpath.HosekWilkie.data != null) { + v = iron.object.Uniforms.helpVec; + v.x = armory.renderpath.HosekWilkie.data.C.x; + v.y = armory.renderpath.HosekWilkie.data.C.y; + v.z = armory.renderpath.HosekWilkie.data.C.z; + } + } + case "_hosekD": { + if (armory.renderpath.HosekWilkie.data == null) { + armory.renderpath.HosekWilkie.recompute(Scene.active.world); + } + if (armory.renderpath.HosekWilkie.data != null) { + v = iron.object.Uniforms.helpVec; + v.x = armory.renderpath.HosekWilkie.data.D.x; + v.y = armory.renderpath.HosekWilkie.data.D.y; + v.z = armory.renderpath.HosekWilkie.data.D.z; + } + } + case "_hosekE": { + if (armory.renderpath.HosekWilkie.data == null) { + armory.renderpath.HosekWilkie.recompute(Scene.active.world); + } + if (armory.renderpath.HosekWilkie.data != null) { + v = iron.object.Uniforms.helpVec; + v.x = armory.renderpath.HosekWilkie.data.E.x; + v.y = armory.renderpath.HosekWilkie.data.E.y; + v.z = armory.renderpath.HosekWilkie.data.E.z; + } + } + case "_hosekF": { + if (armory.renderpath.HosekWilkie.data == null) { + armory.renderpath.HosekWilkie.recompute(Scene.active.world); + } + if (armory.renderpath.HosekWilkie.data != null) { + v = iron.object.Uniforms.helpVec; + v.x = armory.renderpath.HosekWilkie.data.F.x; + v.y = armory.renderpath.HosekWilkie.data.F.y; + v.z = armory.renderpath.HosekWilkie.data.F.z; + } + } + case "_hosekG": { + if (armory.renderpath.HosekWilkie.data == null) { + armory.renderpath.HosekWilkie.recompute(Scene.active.world); + } + if (armory.renderpath.HosekWilkie.data != null) { + v = iron.object.Uniforms.helpVec; + v.x = armory.renderpath.HosekWilkie.data.G.x; + v.y = armory.renderpath.HosekWilkie.data.G.y; + v.z = armory.renderpath.HosekWilkie.data.G.z; + } + } + case "_hosekH": { + if (armory.renderpath.HosekWilkie.data == null) { + armory.renderpath.HosekWilkie.recompute(Scene.active.world); + } + if (armory.renderpath.HosekWilkie.data != null) { + v = iron.object.Uniforms.helpVec; + v.x = armory.renderpath.HosekWilkie.data.H.x; + v.y = armory.renderpath.HosekWilkie.data.H.y; + v.z = armory.renderpath.HosekWilkie.data.H.z; + } + } + case "_hosekI": { + if (armory.renderpath.HosekWilkie.data == null) { + armory.renderpath.HosekWilkie.recompute(Scene.active.world); + } + if (armory.renderpath.HosekWilkie.data != null) { + v = iron.object.Uniforms.helpVec; + v.x = armory.renderpath.HosekWilkie.data.I.x; + v.y = armory.renderpath.HosekWilkie.data.I.y; + v.z = armory.renderpath.HosekWilkie.data.I.z; + } + } + case "_hosekZ": { + if (armory.renderpath.HosekWilkie.data == null) { + armory.renderpath.HosekWilkie.recompute(Scene.active.world); + } + if (armory.renderpath.HosekWilkie.data != null) { + v = iron.object.Uniforms.helpVec; + v.x = armory.renderpath.HosekWilkie.data.Z.x; + v.y = armory.renderpath.HosekWilkie.data.Z.y; + v.z = armory.renderpath.HosekWilkie.data.Z.z; + } + } + #end + #if rp_voxelao + case "_cameraPositionSnap": { v = iron.object.Uniforms.helpVec; - v.x = armory.renderpath.HosekWilkie.data.A.x; - v.y = armory.renderpath.HosekWilkie.data.A.y; - v.z = armory.renderpath.HosekWilkie.data.A.z; + var camera = iron.Scene.active.camera; + v.set(camera.transform.worldx(), camera.transform.worldy(), camera.transform.worldz()); + var l = camera.lookWorld(); + var e = Main.voxelgiHalfExtents; + v.x += l.x * e * 0.9; + v.y += l.y * e * 0.9; + var f = Main.voxelgiVoxelSize * 8; // Snaps to 3 mip-maps range + v.set(Math.floor(v.x / f) * f, Math.floor(v.y / f) * f, Math.floor(v.z / f) * f); } + #end } - else if (link == "_hosekB") { - if (armory.renderpath.HosekWilkie.data == null) { - armory.renderpath.HosekWilkie.recompute(Scene.active.world); - } - if (armory.renderpath.HosekWilkie.data != null) { - v = iron.object.Uniforms.helpVec; - v.x = armory.renderpath.HosekWilkie.data.B.x; - v.y = armory.renderpath.HosekWilkie.data.B.y; - v.z = armory.renderpath.HosekWilkie.data.B.z; - } - } - else if (link == "_hosekC") { - if (armory.renderpath.HosekWilkie.data == null) { - armory.renderpath.HosekWilkie.recompute(Scene.active.world); - } - if (armory.renderpath.HosekWilkie.data != null) { - v = iron.object.Uniforms.helpVec; - v.x = armory.renderpath.HosekWilkie.data.C.x; - v.y = armory.renderpath.HosekWilkie.data.C.y; - v.z = armory.renderpath.HosekWilkie.data.C.z; - } - } - else if (link == "_hosekD") { - if (armory.renderpath.HosekWilkie.data == null) { - armory.renderpath.HosekWilkie.recompute(Scene.active.world); - } - if (armory.renderpath.HosekWilkie.data != null) { - v = iron.object.Uniforms.helpVec; - v.x = armory.renderpath.HosekWilkie.data.D.x; - v.y = armory.renderpath.HosekWilkie.data.D.y; - v.z = armory.renderpath.HosekWilkie.data.D.z; - } - } - else if (link == "_hosekE") { - if (armory.renderpath.HosekWilkie.data == null) { - armory.renderpath.HosekWilkie.recompute(Scene.active.world); - } - if (armory.renderpath.HosekWilkie.data != null) { - v = iron.object.Uniforms.helpVec; - v.x = armory.renderpath.HosekWilkie.data.E.x; - v.y = armory.renderpath.HosekWilkie.data.E.y; - v.z = armory.renderpath.HosekWilkie.data.E.z; - } - } - else if (link == "_hosekF") { - if (armory.renderpath.HosekWilkie.data == null) { - armory.renderpath.HosekWilkie.recompute(Scene.active.world); - } - if (armory.renderpath.HosekWilkie.data != null) { - v = iron.object.Uniforms.helpVec; - v.x = armory.renderpath.HosekWilkie.data.F.x; - v.y = armory.renderpath.HosekWilkie.data.F.y; - v.z = armory.renderpath.HosekWilkie.data.F.z; - } - } - else if (link == "_hosekG") { - if (armory.renderpath.HosekWilkie.data == null) { - armory.renderpath.HosekWilkie.recompute(Scene.active.world); - } - if (armory.renderpath.HosekWilkie.data != null) { - v = iron.object.Uniforms.helpVec; - v.x = armory.renderpath.HosekWilkie.data.G.x; - v.y = armory.renderpath.HosekWilkie.data.G.y; - v.z = armory.renderpath.HosekWilkie.data.G.z; - } - } - else if (link == "_hosekH") { - if (armory.renderpath.HosekWilkie.data == null) { - armory.renderpath.HosekWilkie.recompute(Scene.active.world); - } - if (armory.renderpath.HosekWilkie.data != null) { - v = iron.object.Uniforms.helpVec; - v.x = armory.renderpath.HosekWilkie.data.H.x; - v.y = armory.renderpath.HosekWilkie.data.H.y; - v.z = armory.renderpath.HosekWilkie.data.H.z; - } - } - else if (link == "_hosekI") { - if (armory.renderpath.HosekWilkie.data == null) { - armory.renderpath.HosekWilkie.recompute(Scene.active.world); - } - if (armory.renderpath.HosekWilkie.data != null) { - v = iron.object.Uniforms.helpVec; - v.x = armory.renderpath.HosekWilkie.data.I.x; - v.y = armory.renderpath.HosekWilkie.data.I.y; - v.z = armory.renderpath.HosekWilkie.data.I.z; - } - } - else if (link == "_hosekZ") { - if (armory.renderpath.HosekWilkie.data == null) { - armory.renderpath.HosekWilkie.recompute(Scene.active.world); - } - if (armory.renderpath.HosekWilkie.data != null) { - v = iron.object.Uniforms.helpVec; - v.x = armory.renderpath.HosekWilkie.data.Z.x; - v.y = armory.renderpath.HosekWilkie.data.Z.y; - v.z = armory.renderpath.HosekWilkie.data.Z.z; - } - } - #end - #if rp_voxelao - if (link == "_cameraPositionSnap") { - v = iron.object.Uniforms.helpVec; - var camera = iron.Scene.active.camera; - v.set(camera.transform.worldx(), camera.transform.worldy(), camera.transform.worldz()); - var l = camera.lookWorld(); - var e = Main.voxelgiHalfExtents; - v.x += l.x * e * 0.9; - v.y += l.y * e * 0.9; - var f = Main.voxelgiVoxelSize * 8; // Snaps to 3 mip-maps range - v.set(Math.floor(v.x / f) * f, Math.floor(v.y / f) * f, Math.floor(v.z / f) * f); - } - #end return v; } public static function floatLink(object: Object, mat: MaterialData, link: String): Null { - #if rp_dynres - if (link == "_dynamicScale") { - return armory.renderpath.DynamicResolutionScale.dynamicScale; + switch (link) { + #if rp_dynres + case "_dynamicScale": { + return armory.renderpath.DynamicResolutionScale.dynamicScale; + } + #end + #if arm_debug + case "_debugFloat": { + return armory.trait.internal.DebugConsole.debugFloat; + } + #end + #if rp_voxelao + case "_voxelBlend": { // Blend current and last voxels + var freq = armory.renderpath.RenderPathCreator.voxelFreq; + return (armory.renderpath.RenderPathCreator.voxelFrame % freq) / freq; + } + #end } - #end - #if arm_debug - if (link == "_debugFloat") { - return armory.trait.internal.DebugConsole.debugFloat; - } - #end - #if rp_voxelao - if (link == "_voxelBlend") { // Blend current and last voxels - var freq = armory.renderpath.RenderPathCreator.voxelFreq; - return (armory.renderpath.RenderPathCreator.voxelFrame % freq) / freq; - } - #end return null; } } From 420033c86dcb79ef783a25971660a8ba2d84c57d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 26 Mar 2021 20:59:26 +0100 Subject: [PATCH 55/63] Add API to set Nishita density parameters --- Shaders/std/sky.glsl | 8 +++---- Sources/armory/object/Uniforms.hx | 23 +++++++++++++++--- Sources/armory/renderpath/Nishita.hx | 24 +++++++++++++++---- blender/arm/exporter.py | 1 + .../material/cycles_nodes/nodes_texture.py | 5 ++-- blender/arm/props.py | 1 + 6 files changed, 48 insertions(+), 14 deletions(-) diff --git a/Shaders/std/sky.glsl b/Shaders/std/sky.glsl index 5a58734f..6fca94dd 100644 --- a/Shaders/std/sky.glsl +++ b/Shaders/std/sky.glsl @@ -21,6 +21,7 @@ #define _SKY_GLSL_ uniform sampler2D nishitaLUT; +uniform vec2 nishitaDensity; #ifndef PI #define PI 3.141592 @@ -91,9 +92,8 @@ vec2 nishita_rsi(const vec3 r0, const vec3 rd, const float sr) { * r0: ray origin * pSun: normalized sun direction * rPlanet: planet radius - * density: (air density, dust density, ozone density) */ -vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const float rPlanet, const vec3 density) { +vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const float rPlanet) { // Calculate the step size of the primary ray. vec2 p = nishita_rsi(r0, r, nishita_atmo_radius); if (p.x > p.y) return vec3(0,0,0); @@ -127,8 +127,8 @@ vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const floa float iHeight = length(iPos) - rPlanet; // Calculate the optical depth of the Rayleigh and Mie scattering for this step - float odStepRlh = exp(-iHeight / nishita_rayleigh_scale) * density.x * iStepSize; - float odStepMie = exp(-iHeight / nishita_mie_scale) * density.y * iStepSize; + float odStepRlh = exp(-iHeight / nishita_rayleigh_scale) * nishitaDensity.x * iStepSize; + float odStepMie = exp(-iHeight / nishita_mie_scale) * nishitaDensity.y * iStepSize; // Accumulate optical depth. iOdRlh += odStepRlh; diff --git a/Sources/armory/object/Uniforms.hx b/Sources/armory/object/Uniforms.hx index e87f809b..9416e79c 100644 --- a/Sources/armory/object/Uniforms.hx +++ b/Sources/armory/object/Uniforms.hx @@ -11,14 +11,14 @@ class Uniforms { public static function register() { iron.object.Uniforms.externalTextureLinks = [textureLink]; - iron.object.Uniforms.externalVec2Links = []; + iron.object.Uniforms.externalVec2Links = [vec2Link]; iron.object.Uniforms.externalVec3Links = [vec3Link]; iron.object.Uniforms.externalVec4Links = []; iron.object.Uniforms.externalFloatLinks = [floatLink]; iron.object.Uniforms.externalIntLinks = []; } - public static function textureLink(object: Object, mat: MaterialData, link: String): kha.Image { + public static function textureLink(object: Object, mat: MaterialData, link: String): Null { switch (link) { case "_nishitaLUT": { if (armory.renderpath.Nishita.data == null) armory.renderpath.Nishita.recompute(Scene.active.world); @@ -40,7 +40,7 @@ class Uniforms { return target != null ? target.image : null; } - public static function vec3Link(object: Object, mat: MaterialData, link: String): iron.math.Vec4 { + public static function vec3Link(object: Object, mat: MaterialData, link: String): Null { var v: Vec4 = null; switch (link) { #if arm_hosek @@ -173,6 +173,23 @@ class Uniforms { return v; } + public static function vec2Link(object: Object, mat: MaterialData, link: String): Null { + var v: Vec4 = null; + switch (link) { + case "_nishitaDensity": { + v = iron.object.Uniforms.helpVec; + var w = Scene.active.world; + if (w != null) { + // We only need Rayleigh and Mie density in the sky shader -> Vec2 + v.x = w.raw.nishita_density[0]; + v.y = w.raw.nishita_density[1]; + } + } + } + + return v; + } + public static function floatLink(object: Object, mat: MaterialData, link: String): Null { switch (link) { #if rp_dynres diff --git a/Sources/armory/renderpath/Nishita.hx b/Sources/armory/renderpath/Nishita.hx index 1a4cac92..7dcba4a2 100644 --- a/Sources/armory/renderpath/Nishita.hx +++ b/Sources/armory/renderpath/Nishita.hx @@ -1,6 +1,7 @@ package armory.renderpath; import kha.FastFloat; +import kha.arrays.Float32Array; import kha.graphics4.TextureFormat; import kha.graphics4.Usage; @@ -18,15 +19,28 @@ class Nishita { public static var data: NishitaData = null; /** - Recompute the nishita lookup table. Call this function after updating - the sky density settings. + Recomputes the nishita lookup table after the density settings changed. + Do not call this method on every frame (it's slow)! **/ public static function recompute(world: WorldData) { - if (world == null || world.raw.sun_direction == null) return; + if (world == null || world.raw.nishita_density == null) return; if (data == null) data = new NishitaData(); - // TODO - data.computeLUT(new Vec3(1.0, 1.0, 1.0)); + var density = world.raw.nishita_density; + data.computeLUT(new Vec3(density[0], density[1], density[2])); + } + + /** Sets the sky's density parameters and calls `recompute()` afterwards. **/ + public static function setDensity(world: WorldData, densityAir: FastFloat, densityDust: FastFloat, densityOzone: FastFloat) { + if (world == null) return; + + if (world.raw.nishita_density == null) world.raw.nishita_density = new Float32Array(3); + var density = world.raw.nishita_density; + density[0] = Helper.clamp(densityAir, 0, 10); + density[1] = Helper.clamp(densityDust, 0, 10); + density[2] = Helper.clamp(densityOzone, 0, 10); + + recompute(world); } } diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index f58039cf..e3841a28 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -2843,6 +2843,7 @@ class ArmoryExporter: out_world['sun_direction'] = list(world.arm_envtex_sun_direction) out_world['turbidity'] = world.arm_envtex_turbidity out_world['ground_albedo'] = world.arm_envtex_ground_albedo + out_world['nishita_density'] = list(world.arm_nishita_density) disable_hdr = world.arm_envtex_name.endswith('.jpg') diff --git a/blender/arm/material/cycles_nodes/nodes_texture.py b/blender/arm/material/cycles_nodes/nodes_texture.py index c88a8373..f9c35dd8 100644 --- a/blender/arm/material/cycles_nodes/nodes_texture.py +++ b/blender/arm/material/cycles_nodes/nodes_texture.py @@ -375,11 +375,12 @@ def parse_sky_nishita(node: bpy.types.ShaderNodeTexSky, state: ParserState) -> v curshader.add_uniform('vec3 sunDir', link='_sunDirection') curshader.add_uniform('sampler2D nishitaLUT', link='_nishitaLUT', included=True, tex_addr_u='clamp', tex_addr_v='clamp') + curshader.add_uniform('vec2 nishitaDensity', link='_nishitaDensity', included=True) planet_radius = 6360e3 # Earth radius used in Blender ray_origin_z = planet_radius + node.altitude - density = c.to_vec3((node.air_density, node.dust_density, node.ozone_density)) + state.world.arm_nishita_density = [node.air_density, node.dust_density, node.ozone_density] sun = '' if node.sun_disc: @@ -399,7 +400,7 @@ def parse_sky_nishita(node: bpy.types.ShaderNodeTexSky, state: ParserState) -> v size = math.cos(theta) sun = f'* sun_disk(n, sunDir, {size}, {node.sun_intensity})' - return f'nishita_atmosphere(n, vec3(0, 0, {ray_origin_z}), sunDir, {planet_radius}, {density}){sun}' + return f'nishita_atmosphere(n, vec3(0, 0, {ray_origin_z}), sunDir, {planet_radius}){sun}' def parse_tex_environment(node: bpy.types.ShaderNodeTexEnvironment, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: diff --git a/blender/arm/props.py b/blender/arm/props.py index 1c5b1c5b..b286cd5e 100755 --- a/blender/arm/props.py +++ b/blender/arm/props.py @@ -326,6 +326,7 @@ def init_properties(): bpy.types.World.arm_envtex_sun_direction = FloatVectorProperty(name="Sun Direction", size=3, default=[0,0,0]) bpy.types.World.arm_envtex_turbidity = FloatProperty(name="Turbidity", default=1.0) bpy.types.World.arm_envtex_ground_albedo = FloatProperty(name="Ground Albedo", default=0.0) + bpy.types.World.arm_nishita_density = FloatVectorProperty(name="Nishita Density", size=3, default=[1, 1, 1]) bpy.types.Material.arm_cast_shadow = BoolProperty(name="Cast Shadow", default=True) bpy.types.Material.arm_receive_shadow = BoolProperty(name="Receive Shadow", description="Requires forward render path", default=True) bpy.types.Material.arm_overlay = BoolProperty(name="Overlay", default=False) From ffcc5fcceb6e1c9e12614dccb89e75e513ac47c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 31 Mar 2021 20:31:31 +0200 Subject: [PATCH 56/63] Make clouds work with Nishita sky model --- blender/arm/make_world.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index 98ac282c..08eea1ca 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -279,7 +279,12 @@ def frag_write_clouds(world: bpy.types.World, frag: Shader): func_cloud_radiance = 'float cloudRadiance(vec3 p, vec3 dir) {\n' if '_EnvSky' in world.world_defs: - func_cloud_radiance += '\tvec3 sun_dir = hosekSunDirection;\n' + # Nishita sky + if 'vec3 sunDir' in frag.uniforms: + func_cloud_radiance += '\tvec3 sun_dir = sunDir;\n' + # Hosek + else: + func_cloud_radiance += '\tvec3 sun_dir = hosekSunDirection;\n' else: func_cloud_radiance += '\tvec3 sun_dir = vec3(0, 0, -1);\n' func_cloud_radiance += '''\tconst int steps = 8; @@ -313,6 +318,7 @@ def frag_write_clouds(world: bpy.types.World, frag: Shader): \t\tuv += (dir.xy / dir.z) * step_size * cloudsUpper; \t} ''' + if world.arm_darken_clouds: func_trace_clouds += '\t// Darken clouds when the sun is low\n' From c5be90d0b0cdfaaa4543900dbb1b9a236c9338e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Wed, 31 Mar 2021 20:33:52 +0200 Subject: [PATCH 57/63] Cleanup --- Shaders/std/sky.glsl | 44 +++++++------------ Sources/armory/object/Uniforms.hx | 2 +- .../material/cycles_nodes/nodes_texture.py | 2 +- 3 files changed, 17 insertions(+), 31 deletions(-) diff --git a/Shaders/std/sky.glsl b/Shaders/std/sky.glsl index 6fca94dd..d3d04e53 100644 --- a/Shaders/std/sky.glsl +++ b/Shaders/std/sky.glsl @@ -66,25 +66,15 @@ vec3 nishita_lookupLUT(const float height, const float sunTheta) { return textureLod(nishitaLUT, coords, 0.0).rgb; } -/* Approximates the density of ozone for a given sample height. Values taken from Cycles code. */ -float nishita_density_ozone(const float height) { - return (height < 10000.0 || height >= 40000.0) ? 0.0 : (height < 25000.0 ? (height - 10000.0) / 15000.0 : -((height - 40000.0) / 15000.0)); -} - -/* ray-sphere intersection that assumes - * the sphere is centered at the origin. - * No intersection when result.x > result.y */ +/* See raySphereIntersection() in armory/Sources/renderpath/Nishita.hx */ vec2 nishita_rsi(const vec3 r0, const vec3 rd, const float sr) { float a = dot(rd, rd); float b = 2.0 * dot(rd, r0); float c = dot(r0, r0) - (sr * sr); float d = (b*b) - 4.0*a*c; - if (d < 0.0) return vec2(1e5,-1e5); - return vec2( - (-b - sqrt(d))/(2.0*a), - (-b + sqrt(d))/(2.0*a) - ); + // If d < 0.0 the ray does not intersect the sphere + return (d < 0.0) ? vec2(1e5,-1e5) : vec2((-b - sqrt(d))/(2.0*a), (-b + sqrt(d))/(2.0*a)); } /* @@ -94,43 +84,41 @@ vec2 nishita_rsi(const vec3 r0, const vec3 rd, const float sr) { * rPlanet: planet radius */ vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const float rPlanet) { - // Calculate the step size of the primary ray. + // Calculate the step size of the primary ray vec2 p = nishita_rsi(r0, r, nishita_atmo_radius); - if (p.x > p.y) return vec3(0,0,0); + if (p.x > p.y) return vec3(0.0); p.y = min(p.y, nishita_rsi(r0, r, rPlanet).x); float iStepSize = (p.y - p.x) / float(nishita_iSteps); - // Initialize the primary ray time. + // Primary ray time float iTime = 0.0; - // Initialize accumulators for Rayleigh and Mie scattering. + // Accumulators for Rayleigh and Mie scattering. vec3 totalRlh = vec3(0,0,0); vec3 totalMie = vec3(0,0,0); - // Initialize optical depth accumulators for the primary ray. + // Optical depth accumulators for the primary ray float iOdRlh = 0.0; float iOdMie = 0.0; - // Calculate the Rayleigh and Mie phases. + // Calculate the Rayleigh and Mie phases float mu = dot(r, pSun); float mumu = mu * mu; float pRlh = 3.0 / (16.0 * PI) * (1.0 + mumu); float pMie = 3.0 / (8.0 * PI) * ((1.0 - nishita_mie_dir_sq) * (mumu + 1.0)) / (pow(1.0 + nishita_mie_dir_sq - 2.0 * mu * nishita_mie_dir, 1.5) * (2.0 + nishita_mie_dir_sq)); - // Sample the primary ray. + // Sample the primary ray for (int i = 0; i < nishita_iSteps; i++) { - // Calculate the primary ray sample position. + // Calculate the primary ray sample position and height vec3 iPos = r0 + r * (iTime + iStepSize * 0.5); - - // Calculate the height of the sample. float iHeight = length(iPos) - rPlanet; // Calculate the optical depth of the Rayleigh and Mie scattering for this step float odStepRlh = exp(-iHeight / nishita_rayleigh_scale) * nishitaDensity.x * iStepSize; float odStepMie = exp(-iHeight / nishita_mie_scale) * nishitaDensity.y * iStepSize; - // Accumulate optical depth. + // Accumulate optical depth iOdRlh += odStepRlh; iOdMie += odStepMie; @@ -142,22 +130,20 @@ vec3 nishita_atmosphere(const vec3 r, const vec3 r0, const vec3 pSun, const floa // Apply dithering to reduce visible banding jODepth += mix(-1000, 1000, random(r.xy)); - // Calculate attenuation. + // Calculate attenuation vec3 attn = exp(-( nishita_mie_coeff * (iOdMie + jODepth.y) + (nishita_rayleigh_coeff) * (iOdRlh + jODepth.x) + nishita_ozone_coeff * jODepth.z )); - // Accumulate scattering. + // Accumulate scattering totalRlh += odStepRlh * attn; totalMie += odStepMie * attn; - // Increment the primary ray time. iTime += iStepSize; } - // Calculate and return the final color. return nishita_sun_intensity * (pRlh * nishita_rayleigh_coeff * totalRlh + pMie * nishita_mie_coeff * totalMie); } @@ -166,7 +152,7 @@ vec3 sun_disk(const vec3 n, const vec3 light_dir, const float disk_size, const f float dist = distance(n, light_dir) / disk_size; // Darken the edges of the sun - // [Hill: 28, 60] (code from [Nec96]) + // [Hill: 28, 60] (according to [Nec96]) float invDist = 1.0 - dist; float mu = sqrt(invDist * invDist); vec3 limb_darkening = 1.0 - (1.0 - pow(vec3(mu), sun_limb_darkening_col)); diff --git a/Sources/armory/object/Uniforms.hx b/Sources/armory/object/Uniforms.hx index 9416e79c..70751dc6 100644 --- a/Sources/armory/object/Uniforms.hx +++ b/Sources/armory/object/Uniforms.hx @@ -177,9 +177,9 @@ class Uniforms { var v: Vec4 = null; switch (link) { case "_nishitaDensity": { - v = iron.object.Uniforms.helpVec; var w = Scene.active.world; if (w != null) { + v = iron.object.Uniforms.helpVec; // We only need Rayleigh and Mie density in the sky shader -> Vec2 v.x = w.raw.nishita_density[0]; v.y = w.raw.nishita_density[1]; diff --git a/blender/arm/material/cycles_nodes/nodes_texture.py b/blender/arm/material/cycles_nodes/nodes_texture.py index f9c35dd8..75e0ffb6 100644 --- a/blender/arm/material/cycles_nodes/nodes_texture.py +++ b/blender/arm/material/cycles_nodes/nodes_texture.py @@ -296,7 +296,7 @@ def parse_tex_sky(node: bpy.types.ShaderNodeTexSky, out_socket: bpy.types.NodeSo if node.sky_type == 'PREETHAM' or node.sky_type == 'HOSEK_WILKIE': if node.sky_type == 'PREETHAM': - log.warn('Preetham sky model is not supported, using Hosek Wilkie sky model instead') + log.info('Info: Preetham sky model is not supported, using Hosek Wilkie sky model instead') return parse_sky_hosekwilkie(node, state) From 656b018e5f73240074f7bbf08514617f6d95f94b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 2 Apr 2021 01:59:28 +0200 Subject: [PATCH 58/63] Render (ir)radiance probes if no other probes are set --- blender/arm/exporter.py | 11 +-- blender/arm/make_world.py | 63 +++++++++++---- .../material/cycles_nodes/nodes_texture.py | 3 +- blender/arm/write_probes.py | 80 +++++++++++++++++-- 4 files changed, 126 insertions(+), 31 deletions(-) diff --git a/blender/arm/exporter.py b/blender/arm/exporter.py index e3841a28..1c5d9ec3 100755 --- a/blender/arm/exporter.py +++ b/blender/arm/exporter.py @@ -2858,16 +2858,9 @@ class ArmoryExporter: rpdat = arm.utils.get_rp() solid_mat = rpdat.arm_material_model == 'Solid' arm_irradiance = rpdat.arm_irradiance and not solid_mat - arm_radiance = False - radtex = world.arm_envtex_name.rsplit('.', 1)[0] + arm_radiance = rpdat.arm_radiance + radtex = world.arm_envtex_name.rsplit('.', 1)[0] # Remove file extension irrsharmonics = world.arm_envtex_irr_name - - # Radiance - if '_EnvTex' in world.world_defs: - arm_radiance = rpdat.arm_radiance - elif '_EnvSky' in world.world_defs: - arm_radiance = rpdat.arm_radiance - radtex = 'hosek' num_mips = world.arm_envtex_num_mips strength = world.arm_envtex_strength diff --git a/blender/arm/make_world.py b/blender/arm/make_world.py index 08eea1ca..71dac33b 100755 --- a/blender/arm/make_world.py +++ b/blender/arm/make_world.py @@ -17,17 +17,59 @@ shader_datas = [] def build(): + """Builds world shaders for all exported worlds.""" global shader_datas - bpy.data.worlds['Arm'].world_defs = '' + wrd = bpy.data.worlds['Arm'] + rpdat = arm.utils.get_rp() + + mobile_mat = rpdat.arm_material_model == 'Mobile' or rpdat.arm_material_model == 'Solid' + envpath = os.path.join(arm.utils.get_fp_build(), 'compiled', 'Assets', 'envmaps') + + wrd.world_defs = '' worlds = [] shader_datas = [] - for scene in bpy.data.scenes: - # Only export worlds from enabled scenes - if scene.arm_export and scene.world is not None and scene.world not in worlds: - worlds.append(scene.world) - create_world_shaders(scene.world) + with write_probes.setup_envmap_render(): + + for scene in bpy.data.scenes: + world = scene.world + + # Only export worlds from enabled scenes and only once per world + if scene.arm_export and world is not None and world not in worlds: + worlds.append(world) + + world.arm_envtex_name = '' + create_world_shaders(world) + + if rpdat.arm_irradiance: + # Plain background color + if '_EnvCol' in world.world_defs: + world_name = arm.utils.safestr(world.name) + # Irradiance json file name + world.arm_envtex_name = world_name + world.arm_envtex_irr_name = world_name + write_probes.write_color_irradiance(world_name, world.arm_envtex_color) + + # Render world to envmap for (ir)radiance, if no + # other probes are exported + elif world.arm_envtex_name == '': + write_probes.render_envmap(envpath, world) + + filename = f'env_{arm.utils.safesrc(world.name)}' + image_file = f'{filename}.jpg' + image_filepath = os.path.join(envpath, image_file) + + world.arm_envtex_name = image_file + world.arm_envtex_irr_name = os.path.basename(image_filepath).rsplit('.', 1)[0] + + write_radiance = rpdat.arm_radiance and not mobile_mat + mip_count = write_probes.write_probes(image_filepath, True, world.arm_envtex_num_mips, write_radiance) + world.arm_envtex_num_mips = mip_count + + if write_radiance: + # Set world def, everything else is handled by write_probes() + wrd.world_defs += '_Rad' def create_world_shaders(world: bpy.types.World): @@ -131,14 +173,7 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader, con: Sha col = world.color world.arm_envtex_color = [col[0], col[1], col[2], 1.0] world.arm_envtex_strength = 1.0 - - # Irradiance/Radiance: clear to color if no texture or sky is provided - if rpdat.arm_irradiance or rpdat.arm_irradiance: - if '_EnvSky' not in world.world_defs and '_EnvTex' not in world.world_defs and '_EnvImg' not in world.world_defs: - # Irradiance json file name - world.arm_envtex_name = world_name - world.arm_envtex_irr_name = world_name - write_probes.write_color_irradiance(world_name, world.arm_envtex_color) + world.world_defs += '_EnvCol' # Clouds enabled if rpdat.arm_clouds and world.arm_use_clouds: diff --git a/blender/arm/material/cycles_nodes/nodes_texture.py b/blender/arm/material/cycles_nodes/nodes_texture.py index 75e0ffb6..516d555b 100644 --- a/blender/arm/material/cycles_nodes/nodes_texture.py +++ b/blender/arm/material/cycles_nodes/nodes_texture.py @@ -294,6 +294,8 @@ def parse_tex_sky(node: bpy.types.ShaderNodeTexSky, out_socket: bpy.types.NodeSo # Pass through return c.to_vec3([0.0, 0.0, 0.0]) + state.world.world_defs += '_EnvSky' + if node.sky_type == 'PREETHAM' or node.sky_type == 'HOSEK_WILKIE': if node.sky_type == 'PREETHAM': log.info('Info: Preetham sky model is not supported, using Hosek Wilkie sky model instead') @@ -315,7 +317,6 @@ def parse_sky_hosekwilkie(node: bpy.types.ShaderNodeTexSky, state: ParserState) # Match to cycles world.arm_envtex_strength *= 0.1 - world.world_defs += '_EnvSky' assets.add_khafile_def('arm_hosek') curshader.add_uniform('vec3 A', link="_hosekA") curshader.add_uniform('vec3 B', link="_hosekB") diff --git a/blender/arm/write_probes.py b/blender/arm/write_probes.py index 11529394..608deab5 100644 --- a/blender/arm/write_probes.py +++ b/blender/arm/write_probes.py @@ -1,23 +1,89 @@ -import bpy +from contextlib import contextmanager +import math import multiprocessing import os -import sys -import subprocess -import json import re -import arm.utils +import subprocess + +import bpy + import arm.assets as assets +import arm.log as log +import arm.utils + def add_irr_assets(output_file_irr): assets.add(output_file_irr + '.arm') + def add_rad_assets(output_file_rad, rad_format, num_mips): assets.add(output_file_rad + '.' + rad_format) for i in range(0, num_mips): assets.add(output_file_rad + '_' + str(i) + '.' + rad_format) -# Generate probes from environment map -def write_probes(image_filepath, disable_hdr, cached_num_mips, arm_radiance=True): + +@contextmanager +def setup_envmap_render(): + """Creates a background scene for rendering environment textures. + Use it as a context manager to automatically clean up on errors. + """ + rpdat = arm.utils.get_rp() + radiance_size = int(rpdat.arm_radiance_size) + + # TODO: Add world option to render .hdr in the UI + + # Render worlds in a different scene so that there are no other + # objects. The actual scene might be called differently if the name + # is already taken + scene = bpy.data.scenes.new("_arm_envmap_render") + scene.render.engine = "CYCLES" + scene.render.image_settings.file_format = "JPEG" + scene.render.image_settings.quality = 100 + scene.render.resolution_x = radiance_size + scene.render.resolution_y = radiance_size // 2 + + # Set GPU as rendering device if the user enabled it + if bpy.context.preferences.addons["cycles"].preferences.compute_device_type == "CUDA": + scene.cycles.device = "GPU" + else: + log.info('Armory: Using CPU for environment render (might be slow). Enable CUDA if possible.') + + # One sample is enough for world background only + scene.cycles.samples = 1 + + # Setup scene + cam = bpy.data.cameras.new("_arm_cam_envmap_render") + cam_obj = bpy.data.objects.new("_arm_cam_envmap_render", cam) + scene.collection.objects.link(cam_obj) + scene.camera = cam_obj + + cam_obj.location = [0.0, 0.0, 0.0] + cam.type = "PANO" + cam.cycles.panorama_type = "EQUIRECTANGULAR" + cam_obj.rotation_euler = [math.radians(90), 0, math.radians(-90)] + + try: + yield + finally: + bpy.data.objects.remove(cam_obj) + bpy.data.cameras.remove(cam) + bpy.data.scenes.remove(scene) + + +def render_envmap(target_dir: str, world: bpy.types.World): + """Renders an environment texture for the given world into the + target_dir. Use in combination with setup_envmap_render().""" + scene = bpy.data.scenes["_arm_envmap_render"] + scene.world = world + + render_path = os.path.join(target_dir, f"env_{arm.utils.safesrc(world.name)}.jpg") + scene.render.filepath = render_path + + bpy.ops.render.render(write_still=True, scene=scene.name) + + +def write_probes(image_filepath: str, disable_hdr: bool, cached_num_mips: int, arm_radiance=True) -> int: + """Generate probes from environment map and returns the mipmap count""" envpath = arm.utils.get_fp_build() + '/compiled/Assets/envmaps' if not os.path.exists(envpath): From 34f08e6922d47978ce8ab3d83936a3ae84a7776a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 2 Apr 2021 01:59:55 +0200 Subject: [PATCH 59/63] Fix irradiance brightness See write_sky_irradiance() for reference --- blender/arm/write_probes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blender/arm/write_probes.py b/blender/arm/write_probes.py index 608deab5..496981f1 100644 --- a/blender/arm/write_probes.py +++ b/blender/arm/write_probes.py @@ -317,6 +317,8 @@ def sh_to_json(sh_file): parse_band_floats(irradiance_floats, band0_line) parse_band_floats(irradiance_floats, band1_line) parse_band_floats(irradiance_floats, band2_line) + for i in range(0, len(irradiance_floats)): + irradiance_floats[i] /= 2 sh_json = {'irradiance': irradiance_floats} ext = '.arm' if bpy.data.worlds['Arm'].arm_minimize else '' From 899411dea4005594caa42b9f62faaa76c1e63d8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Fri, 2 Apr 2021 02:18:11 +0200 Subject: [PATCH 60/63] Cleanup write_probes.py --- blender/arm/write_probes.py | 79 +++++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/blender/arm/write_probes.py b/blender/arm/write_probes.py index 496981f1..f6ca036e 100644 --- a/blender/arm/write_probes.py +++ b/blender/arm/write_probes.py @@ -30,8 +30,6 @@ def setup_envmap_render(): rpdat = arm.utils.get_rp() radiance_size = int(rpdat.arm_radiance_size) - # TODO: Add world option to render .hdr in the UI - # Render worlds in a different scene so that there are no other # objects. The actual scene might be called differently if the name # is already taken @@ -129,7 +127,7 @@ def write_probes(image_filepath: str, disable_hdr: bool, cached_num_mips: int, a scaled_file = output_file_rad + '.' + rad_format if arm.utils.get_os() == 'win': - output = subprocess.check_output([ \ + subprocess.check_output([ kraffiti_path, 'from=' + input_file, 'to=' + scaled_file, @@ -137,35 +135,35 @@ def write_probes(image_filepath: str, disable_hdr: bool, cached_num_mips: int, a 'width=' + str(target_w), 'height=' + str(target_h)]) else: - output = subprocess.check_output([ \ - kraffiti_path + \ - ' from="' + input_file + '"' + \ - ' to="' + scaled_file + '"' + \ - ' format=' + rad_format + \ - ' width=' + str(target_w) + \ - ' height=' + str(target_h)], shell=True) + subprocess.check_output([ + kraffiti_path + + ' from="' + input_file + '"' + + ' to="' + scaled_file + '"' + + ' format=' + rad_format + + ' width=' + str(target_w) + + ' height=' + str(target_h)], shell=True) # Irradiance spherical harmonics if arm.utils.get_os() == 'win': - subprocess.call([ \ + subprocess.call([ cmft_path, '--input', scaled_file, '--filter', 'shcoeffs', '--outputNum', '1', '--output0', output_file_irr]) else: - subprocess.call([ \ - cmft_path + \ - ' --input ' + '"' + scaled_file + '"' + \ - ' --filter shcoeffs' + \ - ' --outputNum 1' + \ - ' --output0 ' + '"' + output_file_irr + '"'], shell=True) + subprocess.call([ + cmft_path + + ' --input ' + '"' + scaled_file + '"' + + ' --filter shcoeffs' + + ' --outputNum 1' + + ' --output0 ' + '"' + output_file_irr + '"'], shell=True) sh_to_json(output_file_irr) add_irr_assets(output_file_irr) # Mip-mapped radiance - if arm_radiance == False: + if not arm_radiance: return cached_num_mips # 4096 = 256 face @@ -266,37 +264,37 @@ def write_probes(image_filepath: str, disable_hdr: bool, cached_num_mips: int, a if disable_hdr is True: for f in generated_files: if arm.utils.get_os() == 'win': - subprocess.call([ \ + subprocess.call([ kraffiti_path, 'from=' + f + '.hdr', 'to=' + f + '.jpg', 'format=jpg']) else: - subprocess.call([ \ - kraffiti_path + \ - ' from="' + f + '.hdr"' + \ - ' to="' + f + '.jpg"' + \ - ' format=jpg'], shell=True) + subprocess.call([ + kraffiti_path + + ' from="' + f + '.hdr"' + + ' to="' + f + '.jpg"' + + ' format=jpg'], shell=True) os.remove(f + '.hdr') # Scale from (4x2 to 1x1> - for i in range (0, 2): + for i in range(0, 2): last = generated_files[-1] out = output_file_rad + '_' + str(mip_count + i) if arm.utils.get_os() == 'win': - subprocess.call([ \ + subprocess.call([ kraffiti_path, 'from=' + last + '.' + rad_format, 'to=' + out + '.' + rad_format, 'scale=0.5', 'format=' + rad_format], shell=True) else: - subprocess.call([ \ - kraffiti_path + \ - ' from=' + '"' + last + '.' + rad_format + '"' + \ - ' to=' + '"' + out + '.' + rad_format + '"' + \ - ' scale=0.5' + \ - ' format=' + rad_format], shell=True) + subprocess.call([ + kraffiti_path + + ' from=' + '"' + last + '.' + rad_format + '"' + + ' to=' + '"' + out + '.' + rad_format + '"' + + ' scale=0.5' + + ' format=' + rad_format], shell=True) generated_files.append(out) mip_count += 2 @@ -305,6 +303,7 @@ def write_probes(image_filepath: str, disable_hdr: bool, cached_num_mips: int, a return mip_count + def sh_to_json(sh_file): """Parse sh coefs produced by cmft into json array""" with open(sh_file + '.c') as f: @@ -327,15 +326,26 @@ def sh_to_json(sh_file): # Clean up .c os.remove(sh_file + '.c') + def parse_band_floats(irradiance_floats, band_line): string_floats = re.findall(r'[-+]?\d*\.\d+|\d+', band_line) - string_floats = string_floats[1:] # Remove 'Band 0/1/2' number + string_floats = string_floats[1:] # Remove 'Band 0/1/2' number for s in string_floats: irradiance_floats.append(float(s)) + def write_sky_irradiance(base_name): # Hosek spherical harmonics - irradiance_floats = [1.5519331988822218,2.3352207154503266,2.997277451988076,0.2673894962434794,0.4305630474135794,0.11331825259716752,-0.04453633521758638,-0.038753175134160295,-0.021302768541875794,0.00055858020486499,0.000371654770334503,0.000126606145406403,-0.000135708721978705,-0.000787399554583089,-0.001550090690860059,0.021947399048903773,0.05453650591711572,0.08783641266630278,0.17053593578630663,0.14734127083304463,0.07775404698816404,-2.6924363189795e-05,-7.9350169701934e-05,-7.559914435231e-05,0.27035455385870993,0.23122918445556914,0.12158817295211832] + irradiance_floats = [ + 1.5519331988822218, 2.3352207154503266, 2.997277451988076, + 0.2673894962434794, 0.4305630474135794, 0.11331825259716752, + -0.04453633521758638, -0.038753175134160295, -0.021302768541875794, + 0.00055858020486499, 0.000371654770334503, 0.000126606145406403, + -0.000135708721978705, -0.000787399554583089, -0.001550090690860059, + 0.021947399048903773, 0.05453650591711572, 0.08783641266630278, + 0.17053593578630663, 0.14734127083304463, 0.07775404698816404, + -2.6924363189795e-05, -7.9350169701934e-05, -7.559914435231e-05, + 0.27035455385870993, 0.23122918445556914, 0.12158817295211832] for i in range(0, len(irradiance_floats)): irradiance_floats[i] /= 2 @@ -350,6 +360,7 @@ def write_sky_irradiance(base_name): assets.add(output_file + '.arm') + def write_color_irradiance(base_name, col): """Constant color irradiance""" # Adjust to Cycles From 97b578d0ed59f5b26fe8cbad56c63d1f37500bd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Mon, 19 Apr 2021 22:21:23 +0200 Subject: [PATCH 61/63] Fix export of attribute node if no UV map exists --- blender/arm/material/cycles_nodes/nodes_input.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blender/arm/material/cycles_nodes/nodes_input.py b/blender/arm/material/cycles_nodes/nodes_input.py index 2a79dffd..69072bcd 100644 --- a/blender/arm/material/cycles_nodes/nodes_input.py +++ b/blender/arm/material/cycles_nodes/nodes_input.py @@ -41,7 +41,7 @@ def parse_attribute(node: bpy.types.ShaderNodeAttribute, out_socket: bpy.types.N lays = mat_user.data.uv_layers # First UV map referenced - if node.attribute_name == lays[0].name: + if len(lays) > 0 and node.attribute_name == lays[0].name: state.con.add_elem('tex', 'short2norm') return c.cast_value('vec3(texCoord.x, 1.0 - texCoord.y, 0.0)', from_type='vec3', to_type=out_type) From 22a557162fd907ef57d34e09636b2f6a408950b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Sat, 1 May 2021 00:04:25 +0200 Subject: [PATCH 62/63] Update Blender version information to 2.93 LTS --- blender/arm/props_ui.py | 6 +++--- blender/arm/utils.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 1fcd6de0..9f32d353 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -1024,7 +1024,7 @@ class ArmoryPlayButton(bpy.types.Operator): # Compare version Blender and Armory (major, minor) if not arm.utils.compare_version_blender_arm(): - self.report({'INFO'}, 'For Armory to work correctly, you need Blender 2.83 LTS.') + self.report({'INFO'}, 'For Armory to work correctly, you need Blender 2.93 LTS.') if not arm.utils.check_saved(self): return {"CANCELLED"} @@ -1063,7 +1063,7 @@ class ArmoryBuildProjectButton(bpy.types.Operator): def execute(self, context): # Compare version Blender and Armory (major, minor) if not arm.utils.compare_version_blender_arm(): - self.report({'INFO'}, 'For Armory to work correctly, you need Blender 2.83 LTS.') + self.report({'INFO'}, 'For Armory to work correctly, you need Blender 2.93 LTS.') if not arm.utils.check_saved(self): return {"CANCELLED"} @@ -1103,7 +1103,7 @@ class ArmoryPublishProjectButton(bpy.types.Operator): def execute(self, context): # Compare version Blender and Armory (major, minor) if not arm.utils.compare_version_blender_arm(): - self.report({'INFO'}, 'For Armory to work correctly, you need Blender 2.83 LTS.') + self.report({'INFO'}, 'For Armory to work correctly, you need Blender 2.93 LTS.') if not arm.utils.check_saved(self): return {"CANCELLED"} diff --git a/blender/arm/utils.py b/blender/arm/utils.py index 88db6888..7b3af738 100755 --- a/blender/arm/utils.py +++ b/blender/arm/utils.py @@ -946,7 +946,7 @@ def get_link_web_server(): return '' if not hasattr(addon_prefs, 'link_web_server') else addon_prefs.link_web_server def compare_version_blender_arm(): - return not (bpy.app.version[0] != 2 or bpy.app.version[1] != 83) + return not (bpy.app.version[0] != 2 or bpy.app.version[1] != 93) def type_name_to_type(name: str) -> bpy.types.bpy_struct: """Return the Blender type given by its name, if registered.""" From 2c761b2ff6ccd04e98ca1c464aab5ba81f0fd629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Br=C3=BCckner?= Date: Thu, 6 May 2021 21:29:59 +0200 Subject: [PATCH 63/63] Slightly improve debug console panel UI --- blender/arm/props_ui.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/blender/arm/props_ui.py b/blender/arm/props_ui.py index 9f32d353..00b13940 100644 --- a/blender/arm/props_ui.py +++ b/blender/arm/props_ui.py @@ -934,23 +934,20 @@ class ARM_PT_ProjectFlagsDebugConsolePanel(bpy.types.Panel): bl_options = {'DEFAULT_CLOSED'} bl_parent_id = "ARM_PT_ProjectFlagsPanel" + def draw_header(self, context): + wrd = bpy.data.worlds['Arm'] + self.layout.prop(wrd, 'arm_debug_console', text='') + def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Arm'] - row = layout.row() - row.enabled = wrd.arm_ui != 'Disabled' - row.prop(wrd, 'arm_debug_console') - row = layout.row() - row.enabled = wrd.arm_debug_console - row.prop(wrd, 'arm_debug_console_position') - row = layout.row() - row.enabled = wrd.arm_debug_console - row.prop(wrd, 'arm_debug_console_scale') - row = layout.row() - row.enabled = wrd.arm_debug_console - row.prop(wrd, 'arm_debug_console_visible') + col = layout.column() + col.enabled = wrd.arm_debug_console + col.prop(wrd, 'arm_debug_console_position') + col.prop(wrd, 'arm_debug_console_scale') + col.prop(wrd, 'arm_debug_console_visible') class ARM_PT_ProjectWindowPanel(bpy.types.Panel): bl_label = "Window"