diff --git a/node_wrangler/__init__.py b/node_wrangler/__init__.py index b1baa24c9..df2a76759 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, 52), + "version": (3, 53), "blender": (4, 0, 0), "location": "Node Editor Toolbar or Shift-W", "description": "Various tools to enhance and speed up node-based workflow", diff --git a/node_wrangler/operators.py b/node_wrangler/operators.py index d719925ad..24e8402cf 100644 --- a/node_wrangler/operators.py +++ b/node_wrangler/operators.py @@ -499,6 +499,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): @@ -519,10 +520,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""" @@ -565,6 +568,46 @@ class NWPreviewNode(Operator, NWBase): groupout.is_active_output = True return groupout + @staticmethod + def get_scene_intensity(context): + """Calculate intensity compensation based on scene exposure""" + # 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 + 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""" @@ -771,11 +814,23 @@ class NWPreviewNode(Operator, NWBase): if active_node_socket_index is None: return {'CANCELLED'} - 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)