From ff9e38f1e89f817c1ed214e6ee494dd3971622bc Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Wed, 17 Jan 2024 17:59:39 +0100 Subject: [PATCH 1/3] Fix #105065: NW: add back exposure compensation for Preview Node Exposure compensation was a useful feature for better previewing colors when scene exposure was not neutral. It was removed in 4bf15a06f3 - [D15350](https://archive.blender.org/developer/D15350) after any socket could be connected to the shader output. This commit reintroduce this feature, but makes it disabled by default as this workflow is not always wanted. In the case of an NPR scene where every material is emissive so there are no actual lighting calculations, the user may want to use the preview feature to quickly select a texture without having it exposure-compensated. --- node_wrangler/operators.py | 56 +++++++++++++++++++++++++++++++++++- node_wrangler/preferences.py | 6 ++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/node_wrangler/operators.py b/node_wrangler/operators.py index 6bd7de7ee..65cde0e36 100644 --- a/node_wrangler/operators.py +++ b/node_wrangler/operators.py @@ -491,6 +491,7 @@ class NWPreviewNode(Operator, NWBase): def __init__(self): self.shader_output_type = "" self.shader_output_ident = "" + self.shader_viewer_ident = "" @classmethod def poll(cls, context): @@ -513,10 +514,12 @@ class NWPreviewNode(Operator, NWBase): else: self.shader_output_type = "OUTPUT_MATERIAL" self.shader_output_ident = "ShaderNodeOutputMaterial" + self.shader_viewer_ident = "ShaderNodeEmission" elif shader_type == 'WORLD': self.shader_output_type = "OUTPUT_WORLD" self.shader_output_ident = "ShaderNodeOutputWorld" + self.shader_viewer_ident = "ShaderNodeBackground" def ensure_viewer_socket(self, node_tree, socket_type, connect_socket=None): """Check if a viewer output already exists in a node group, otherwise create it""" @@ -559,6 +562,44 @@ class NWPreviewNode(Operator, NWBase): groupout.is_active_output = True return groupout + @staticmethod + def get_scene_intensity(context): + """Calculate intensity compensation based on scene exposure""" + intensity = 1.0 + if context.scene.render.engine == 'CYCLES' and hasattr(context.scene, 'cycles'): + # Film exposure is a multiplier + intensity /= context.scene.cycles.film_exposure + # CM exposure is measured in stops/EVs (2^x) + intensity /= 2.0 ** context.scene.view_settings.exposure + return intensity + + @staticmethod + def get_emission_node(base_node_tree): + for node in base_node_tree.nodes: + if "Emission Viewer" in node.name: + return node + + def ensure_emission_node(self, context, base_node_tree, output_node): + emission_node = self.get_emission_node(base_node_tree) + if emission_node is None: + emission_node = base_node_tree.nodes.new(self.shader_viewer_ident) + emission_node.hide = True + emission_node.location = output_node.location + Vector((0, 40)) + emission_node.label = "Viewer" + emission_node.name = "Emission Viewer" + emission_node.use_custom_color = True + emission_node.color = (0.6, 0.5, 0.4) + emission_node.select = False + + # If Viewer is connected to output by user, don't change those connections (patch by gandalf3) + if (len(emission_node.outputs[0].links) == 0 + or emission_node.outputs[0].links[0].to_node != output_node): + connect_sockets(emission_node.outputs[0], output_node.inputs[0]) + + # Set brightness of viewer to compensate for Film and CM exposure + emission_node.inputs[1].default_value = self.get_scene_intensity(context) + return emission_node + @classmethod def search_sockets(cls, node, sockets, index=None): """Recursively scan nodes for viewer sockets and store them in a list""" @@ -759,11 +800,24 @@ class NWPreviewNode(Operator, NWBase): active_node_socket_index = self.get_output_index( active, output_node, base_node_tree == active_tree, 'SHADER' ) - if active.outputs[active_node_socket_index].name == "Volume": + + node_output = active.outputs[active_node_socket_index] + if node_output.name == "Volume": output_node_socket_index = 1 else: output_node_socket_index = 0 + # Use an emission node if needed to compensate for scene exposure + settings = context.preferences.addons[__package__].preferences + if (settings.use_viewer_exposure_compensation + and self.get_scene_intensity(context) != 1.0 + and node_output.type != 'SHADER'): + output_node = self.ensure_emission_node(context, base_node_tree, output_node) + output_node_socket_index = 0 + else: + if (emission_node := self.get_emission_node(base_node_tree)) is not None: + base_node_tree.nodes.remove(emission_node) + # If there are no nested node groups, the link starts at the active node node_output = active.outputs[active_node_socket_index] if len(path) > 1: diff --git a/node_wrangler/preferences.py b/node_wrangler/preferences.py index 6ce4c57bc..e3e759d7a 100644 --- a/node_wrangler/preferences.py +++ b/node_wrangler/preferences.py @@ -85,6 +85,11 @@ class NWNodeWrangler(bpy.types.AddonPreferences): ), default='CENTER', description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes") + use_viewer_exposure_compensation: BoolProperty( + name="Use Viewer Exposure Compensation", + default=False, + description="When using the viewer to preview colors in the shader editor, take the scene exposure into account to display the original color" + ) show_hotkey_list: BoolProperty( name="Show Hotkey List", @@ -109,6 +114,7 @@ class NWNodeWrangler(bpy.types.AddonPreferences): col = layout.column() col.prop(self, "merge_position") col.prop(self, "merge_hide") + col.prop(self, "use_viewer_exposure_compensation") box = layout.box() col = box.column(align=True) -- 2.30.2 From f6a9afce41fd743e9530c73a50f6cc5ef811cd17 Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Wed, 17 Jan 2024 18:21:59 +0100 Subject: [PATCH 2/3] Bump NW version --- node_wrangler/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node_wrangler/__init__.py b/node_wrangler/__init__.py index 13a3178bd..b1baa24c9 100644 --- a/node_wrangler/__init__.py +++ b/node_wrangler/__init__.py @@ -5,7 +5,7 @@ bl_info = { "name": "Node Wrangler", "author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer", - "version": (3, 51), + "version": (3, 52), "blender": (4, 0, 0), "location": "Node Editor Toolbar or Shift-W", "description": "Various tools to enhance and speed up node-based workflow", -- 2.30.2 From 6268ab0ce80d188f376235470dc249bd3a9f4983 Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Wed, 17 Jan 2024 18:42:19 +0100 Subject: [PATCH 3/3] Avoid divide by zero error --- node_wrangler/operators.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/node_wrangler/operators.py b/node_wrangler/operators.py index 65cde0e36..03c7cd8e1 100644 --- a/node_wrangler/operators.py +++ b/node_wrangler/operators.py @@ -565,12 +565,14 @@ class NWPreviewNode(Operator, NWBase): @staticmethod def get_scene_intensity(context): """Calculate intensity compensation based on scene exposure""" - intensity = 1.0 + # CM exposure is measured in stops/EVs (2^x) + intensity = 1.0 / (2.0 ** context.scene.view_settings.exposure) if context.scene.render.engine == 'CYCLES' and hasattr(context.scene, 'cycles'): + if context.scene.cycles.film_exposure == 0.0: + # Avoid divide by zero error + return 1.0 # Film exposure is a multiplier intensity /= context.scene.cycles.film_exposure - # CM exposure is measured in stops/EVs (2^x) - intensity /= 2.0 ** context.scene.view_settings.exposure return intensity @staticmethod -- 2.30.2