Awesome work! I was planning to make an add-on for this myself, but never got around to. Thankfully now I don’t have to!
I have tried installing the addon but I cannot locate it inside blender (v2.81)/
In the node editor, T key menu > Custom/Normal buttons.
Also make sure, what you downloaded was actually a python file. GitHub is funny about that and gives you HTML files when you say “Save Link as…”
So if you cannot find it in your installed add-ons, make sure you have something that can be installed.
You can also just copy this code-block and save it with a .py extension:
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
bl_info = {
"name": "Normal Map nodes to Custom",
"author": "Spooky spooky Ghostman, Kamikaze, crute",
"description": "Replace Normal Nodes for better EEVEE Viewport-Performance",
"blender": (2, 80, 0),
"version": (0, 1, 0),
"location": "Tools Panel (T) in Shader Editor",
"warning": "",
"category": "Material",
}
from mathutils import Color, Vector
import bpy
class MAT_OT_custom_normal(bpy.types.Operator):
bl_description = "Switch normal map nodes to a faster custom node"
bl_idname = 'node.normal_map_group'
bl_label = "Normal Map nodes to Custom"
bl_options = {'UNDO'}
@classmethod
def poll(self, context):
return (bpy.data.materials or bpy.data.node_groups)
def execute(self, context):
def mirror(new, old):
"""Copy attributes of the old node to the new node"""
new.parent = old.parent
new.label = old.label
new.mute = old.mute
new.hide = old.hide
new.select = old.select
new.location = old.location
# inputs
for (name, point) in old.inputs.items():
input = new.inputs.get(name)
if input:
input.default_value = point.default_value
for link in point.links:
new.id_data.links.new(link.from_socket, input)
# outputs
for (name, point) in old.outputs.items():
output = new.outputs.get(name)
if output:
output.default_value = point.default_value
for link in point.links:
new.id_data.links.new(output, link.to_socket)
def get_custom():
name = 'Normal Map Optimized'
group = bpy.data.node_groups.get(name)
if not group and self.custom:
group = default_custom_nodes()
return group
def set_custom(nodes):
group = get_custom()
if not group:
return
for node in nodes:
new = None
if self.custom:
if isinstance(node, bpy.types.ShaderNodeNormalMap):
new = nodes.new(type='ShaderNodeGroup')
new.node_tree = group
else:
if isinstance(node, bpy.types.ShaderNodeGroup):
if node.node_tree == group:
new = nodes.new(type='ShaderNodeNormalMap')
if new:
name = node.name
mirror(new, node)
nodes.remove(node)
new.name = name
for mat in bpy.data.materials:
set_custom(getattr(mat.node_tree, 'nodes', []))
for group in bpy.data.node_groups:
set_custom(group.nodes)
if (not self.custom) and get_custom():
bpy.data.node_groups.remove(get_custom())
return {'FINISHED'}
custom: bpy.props.BoolProperty(
name="To Custom",
description="Set all normals to custom group, or revert back to normal",
default=True,
)
class MUT_PT_normal_map_nodes(bpy.types.Panel):
bl_category = ""
bl_label = ""
bl_options = {'HIDE_HEADER'}
bl_region_type = 'TOOLS'
bl_space_type = 'NODE_EDITOR'
@classmethod
def poll(self, context):
return True
def draw(self, context):
layout = self.layout
col = layout.column(align=True)
tog = MAT_OT_custom_normal.bl_idname
col.operator(tog, text="Custom").custom = True
col.operator(tog, text="Normal").custom = False
def default_custom_nodes():
use_new_nodes = (bpy.app.version >= (2, 81))
group = bpy.data.node_groups.new('Normal Map Optimized', 'ShaderNodeTree')
nodes = group.nodes
links = group.links
# Input
input = group.inputs.new('NodeSocketFloat', 'Strength')
input.default_value = 1.0
input.min_value = 0.0
input.max_value = 1.0
input = group.inputs.new('NodeSocketColor', 'Color')
input.default_value = ((0.5, 0.5, 1.0, 1.0))
# Output
group.outputs.new('NodeSocketVector', 'Normal')
# Add Nodes
frame = nodes.new('NodeFrame')
frame.name = 'Matrix * Normal Map'
frame.label = 'Matrix * Normal Map'
frame.location = Vector((540.0, -80.0))
frame.hide = False
frame.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node = nodes.new('ShaderNodeVectorMath')
node.name = 'Vector Math'
node.label = ''
node.parent = frame
node.hide = True
node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node.location = Vector((-60.0, 20.0))
node.operation = 'DOT_PRODUCT'
node.inputs[0].default_value = (0.5, 0.5, 0.5) # Vector
node.inputs[1].default_value = (0.5, 0.5, 0.5) # Vector
if use_new_nodes:
node.inputs[2].default_value = 1.0 # Scale
node = nodes.new('ShaderNodeVectorMath')
node.name = 'Vector Math.001'
node.label = ''
node.parent = frame
node.hide = True
node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node.location = Vector((-60.0, -20.0))
node.operation = 'DOT_PRODUCT'
node.inputs[0].default_value = (0.5, 0.5, 0.5) # Vector
node.inputs[1].default_value = (0.5, 0.5, 0.5) # Vector
if use_new_nodes:
node.inputs[2].default_value = 1.0 # Scale
node = nodes.new('ShaderNodeVectorMath')
node.name = 'Vector Math.002'
node.label = ''
node.parent = frame
node.hide = True
node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node.location = Vector((-60.0, -60.0))
node.inputs[0].default_value = (0.5, 0.5, 0.5) # Vector
node.inputs[1].default_value = (0.5, 0.5, 0.5) # Vector
if use_new_nodes:
node.inputs[2].default_value = 1.0 # Scale
node.operation = 'DOT_PRODUCT'
node = nodes.new('ShaderNodeCombineXYZ')
node.name = 'Combine XYZ'
node.label = ''
node.parent = frame
node.hide = True
node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node.location = Vector((100.0, -20.0))
node.inputs[0].default_value = 0.0 # X
node.inputs[1].default_value = 0.0 # Y
node.inputs[2].default_value = 0.0 # Z
frame = nodes.new('NodeFrame')
frame.name = 'Generate TBN from Bump Node'
frame.label = 'Generate TBN from Bump Node'
frame.location = Vector((-192.01412963867188, -77.50459289550781))
frame.hide = False
frame.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node = nodes.new('ShaderNodeUVMap')
node.name = 'UV Map'
node.label = ''
node.parent = frame
node.hide = True
node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node.location = Vector((-247.98587036132812, -2.4954071044921875))
node = nodes.new('ShaderNodeSeparateXYZ')
node.name = 'UV Gradients'
node.label = 'UV Gradients'
node.parent = frame
node.hide = True
node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node.location = Vector((-87.98587036132812, -2.4954071044921875))
node.inputs[0].default_value = (0.0, 0.0, 0.0) # Vector
# node.outputs.remove((node.outputs['Z']))
node = nodes.new('ShaderNodeNewGeometry')
node.name = 'Normal'
node.label = 'Normal'
node.parent = frame
node.hide = True
node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node.location = Vector((72.01412963867188, -62.49540710449219))
# for out in node.outputs:
# if out.name not in ['Normal']:
# node.outputs.remove(out)
node = nodes.new('ShaderNodeBump')
node.name = 'Bi-Tangent'
node.label = 'Bi-Tangent'
node.parent = frame
node.hide = True
node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node.location = Vector((72.01412963867188, -22.495407104492188))
node.invert = True
node.inputs[0].default_value = 1.0 # Strength
node.inputs[1].default_value = 1000.0 # Distance
node.inputs[2].default_value = 1.0 # Height
if use_new_nodes:
node.inputs[3].default_value = 1.0 # Height_dx
node.inputs[4].default_value = 1.0 # Height_dy
node.inputs[5].default_value = (0.0, 0.0, 0.0) # Normal
else:
node.inputs[3].default_value = (0.0, 0.0, 0.0) # Normal
# for inp in node.inputs:
# if inp.name not in ['Height']:
# node.inputs.remove(inp)
node = nodes.new('ShaderNodeBump')
node.name = 'Tangent'
node.label = 'Tangent'
node.parent = frame
node.hide = True
node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node.location = Vector((72.01412963867188, 17.504592895507812))
node.invert = True
# for inp in node.inputs:
# if inp.name not in ['Height']:
# node.inputs.remove(inp)
frame = nodes.new('NodeFrame')
frame.name = 'Node'
frame.label = 'Normal Map Processing'
frame.location = Vector((180.0, -260.0))
frame.hide = False
frame.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node = nodes.new('NodeGroupInput')
node.name = 'Group Input'
node.label = ''
node.parent = frame
node.hide = True
node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node.location = Vector((-400.0, 20.0))
node = nodes.new('ShaderNodeMixRGB')
node.name = 'Influence'
node.label = ''
node.parent = frame
node.hide = True
node.location = Vector((-240.0, 20.0))
node.inputs[1].default_value = (0.5, 0.5, 1.0, 1.0) # Color1
node = nodes.new('ShaderNodeVectorMath')
node.name = 'Vector Math.003'
node.label = ''
node.parent = frame
node.hide = True
node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node.location = Vector((-80.0, 20.0))
node.operation = 'SUBTRACT'
node.inputs[0].default_value = (0.5, 0.5, 0.5) # Vector
node.inputs[1].default_value = (0.5, 0.5, 0.5) # Vector
if use_new_nodes:
node.inputs[2].default_value = 1.0 # Scale
# node.inputs.remove(node.inputs[1])
node = nodes.new('ShaderNodeVectorMath')
node.name = 'Vector Math.004'
node.label = ''
node.parent = frame
node.hide = True
node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node.location = Vector((80.0, 20.0))
node.inputs[0].default_value = (0.5, 0.5, 0.5) # Vector
node.inputs[1].default_value = (0.5, 0.5, 0.5) # Vector
if use_new_nodes:
node.inputs[2].default_value = 1.0 # Scale
frame = nodes.new('NodeFrame')
frame.name = 'Transpose Matrix'
frame.label = 'Transpose Matrix'
frame.location = Vector((180.0, -80.0))
frame.hide = False
frame.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node = nodes.new('ShaderNodeCombineXYZ')
node.name = 'Combine XYZ.001'
node.label = ''
node.parent = frame
node.hide = True
node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node.location = Vector((80.0, 20.0))
node.inputs[0].default_value = 0.0 # X
node.inputs[1].default_value = 0.0 # Y
node.inputs[2].default_value = 0.0 # Z
node = nodes.new('ShaderNodeCombineXYZ')
node.name = 'Combine XYZ.002'
node.label = ''
node.parent = frame
node.hide = True
node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node.location = Vector((80.0, -20.0))
node.inputs[0].default_value = 0.0 # X
node.inputs[1].default_value = 0.0 # Y
node.inputs[2].default_value = 0.0 # Z
node = nodes.new('ShaderNodeCombineXYZ')
node.name = 'Combine XYZ.003'
node.label = ''
node.parent = frame
node.hide = True
node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node.location = Vector((80.0, -60.0))
node.inputs[0].default_value = 0.0 # X
node.inputs[1].default_value = 0.0 # Y
node.inputs[2].default_value = 0.0 # Z
node = nodes.new('ShaderNodeSeparateXYZ')
node.name = 'Separate XYZ.001'
node.label = ''
node.parent = frame
node.hide = True
node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node.location = Vector((-80.0, 20.0))
node.inputs[0].default_value = (0.0, 0.0, 0.0) # Vector
node = nodes.new('ShaderNodeSeparateXYZ')
node.name = 'Separate XYZ.002'
node.label = ''
node.parent = frame
node.hide = True
node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node.location = Vector((-80.0, -20.0))
node.inputs[0].default_value = (0.0, 0.0, 0.0) # Vector
node = nodes.new('ShaderNodeSeparateXYZ')
node.name = 'Separate XYZ.003'
node.label = ''
node.parent = frame
node.hide = True
node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node.location = Vector((-80.0, -60.0))
node.inputs[0].default_value = (0.0, 0.0, 0.0) # Vector
node = nodes.new('NodeGroupOutput')
node.name = 'Group Output'
node.label = ''
node.location = Vector((840.0, -80.0))
node.hide = False
node.color = Color((0.6079999804496765, 0.6079999804496765, 0.6079999804496765))
node.inputs[0].default_value = (0.0, 0.0, 0.0) # Normal
# Connect the nodes
links.new(nodes['Group Input'].outputs['Strength'], nodes['Influence'].inputs[0])
links.new(nodes['Group Input'].outputs['Color'], nodes['Influence'].inputs[2])
links.new(nodes['Influence'].outputs['Color'], nodes['Vector Math.003'].inputs[0])
links.new(nodes['UV Gradients'].outputs['X'], nodes['Tangent'].inputs['Height'])
links.new(nodes['UV Gradients'].outputs['Y'], nodes['Bi-Tangent'].inputs['Height'])
links.new(nodes['UV Map'].outputs['UV'], nodes['UV Gradients'].inputs['Vector'])
links.new(nodes['Tangent'].outputs['Normal'], nodes['Separate XYZ.001'].inputs[0])
links.new(nodes['Bi-Tangent'].outputs['Normal'], nodes['Separate XYZ.002'].inputs[0])
links.new(nodes['Normal'].outputs['Normal'], nodes['Separate XYZ.003'].inputs[0])
links.new(nodes['Vector Math.004'].outputs['Vector'], nodes['Vector Math'].inputs[1])
links.new(nodes['Combine XYZ.001'].outputs['Vector'], nodes['Vector Math'].inputs[0])
links.new(nodes['Vector Math.004'].outputs['Vector'], nodes['Vector Math.001'].inputs[1])
links.new(nodes['Combine XYZ.002'].outputs['Vector'], nodes['Vector Math.001'].inputs[0])
links.new(nodes['Vector Math.004'].outputs['Vector'], nodes['Vector Math.002'].inputs[1])
links.new(nodes['Combine XYZ.003'].outputs['Vector'], nodes['Vector Math.002'].inputs[0])
links.new(nodes['Vector Math.003'].outputs['Vector'], nodes['Vector Math.004'].inputs[0])
links.new(nodes['Vector Math.003'].outputs['Vector'], nodes['Vector Math.004'].inputs[1])
links.new(nodes['Vector Math'].outputs['Value'], nodes['Combine XYZ'].inputs['X'])
links.new(nodes['Vector Math.001'].outputs['Value'], nodes['Combine XYZ'].inputs['Y'])
links.new(nodes['Vector Math.002'].outputs['Value'], nodes['Combine XYZ'].inputs['Z'])
links.new(nodes['Separate XYZ.001'].outputs['X'], nodes['Combine XYZ.001'].inputs['X'])
links.new(nodes['Separate XYZ.002'].outputs['X'], nodes['Combine XYZ.001'].inputs['Y'])
links.new(nodes['Separate XYZ.003'].outputs['X'], nodes['Combine XYZ.001'].inputs['Z'])
links.new(nodes['Separate XYZ.001'].outputs['Y'], nodes['Combine XYZ.002'].inputs['X'])
links.new(nodes['Separate XYZ.002'].outputs['Y'], nodes['Combine XYZ.002'].inputs['Y'])
links.new(nodes['Separate XYZ.003'].outputs['Y'], nodes['Combine XYZ.002'].inputs['Z'])
links.new(nodes['Separate XYZ.001'].outputs['Z'], nodes['Combine XYZ.003'].inputs['X'])
links.new(nodes['Separate XYZ.002'].outputs['Z'], nodes['Combine XYZ.003'].inputs['Y'])
links.new(nodes['Separate XYZ.003'].outputs['Z'], nodes['Combine XYZ.003'].inputs['Z'])
links.new(nodes['Combine XYZ'].outputs['Vector'], nodes['Group Output'].inputs['Normal'])
return group
def register():
bpy.utils.register_class(MAT_OT_custom_normal)
bpy.utils.register_class(MUT_PT_normal_map_nodes)
def unregister():
bpy.utils.unregister_class(MAT_OT_custom_normal)
bpy.utils.unregister_class(MUT_PT_normal_map_nodes)
if __name__ == "__main__":
register()
Okay I got it working… I was installing via zip. So I extracted the zip and directlty installed the .py file.
Thanks for the pointers @lucky and @Benjay
This is a fantastic workaround until they fix it properly (which I am certain they will). The script really is the icing on the cake! Thank you so much, the difference is night and day.
Thanks for the add on, it work really well
Thanks helped me texture paint in Eevee render mode without much struggle!
Thank you so much, I was straggling with 5 fps and now I am at 26-30!!
Thank you so much for this plugin! I was losing my mind trying to figure out why I’m getting 2fps with Eevee and 12fps with Cycles. It works, now Eevee performance more or less matches viewport shading.
you should be able to send this to the blender foundation and just have them integrate it right? Isnt that the point of open source software?
From what I understand, this doesn’t fix the core issue. It’s just a temporary fix to make the viewport useable. You’re supposed to switch the normals back before rendering. So the Blender team would still need to tackle the heart of the issue rather than applying a bandage.
Thank you, thank you, to all you beautiful bastards!
I have some legacy blender users (2.79.6) that would like this addon
would it be possible to backport? @Benjay
Great addon. In my scene FPS is raised from 4-5 up to 15-20!
Do you have a forecast when it will be available for Blender 3.2?
thanks
I think I saw a fix for this in oldbens repo
does this scheme work with procedural maps or only with image node?
With procedural maps you wouldn’t really need it, as most of Blender’s procedurals are greyscale and you can use a bump node for those, which is cheaper (and it’s also what this setup uses).
Blender 3.2 support is fixed in my fork:
Will propose the fix to oldben as well