diff --git a/release/scripts/modules/bpy_extras/node_shader_utils.py b/release/scripts/modules/bpy_extras/node_shader_utils.py index f8f0efdef8a..e97eac0a9e6 100644 --- a/release/scripts/modules/bpy_extras/node_shader_utils.py +++ b/release/scripts/modules/bpy_extras/node_shader_utils.py @@ -43,6 +43,7 @@ def rgb_to_rgba(rgb): def rgba_to_rgb(rgba): return Color((rgba[0], rgba[1], rgba[2])) + class ShaderWrapper(): """ Base class with minimal common ground for all types of shader interfaces we may want/need to implement. @@ -90,9 +91,10 @@ class ShaderWrapper(): dst_node.width = min(dst_node.width, self._col_size - 20) return loc - def __init__(self, material, is_readonly=True): + def __init__(self, material, is_readonly=True, use_nodes=True): self.is_readonly = is_readonly self.material = material + self.use_nodes = use_nodes self.update() def update(self): # Should be re-implemented by children classes... @@ -101,6 +103,7 @@ class ShaderWrapper(): self._textures = {} self._grid_locations = set() + def use_nodes_get(self): return self.material.use_nodes @@ -108,17 +111,22 @@ class ShaderWrapper(): def use_nodes_set(self, val): self.material.use_nodes = val self.update() + use_nodes = property(use_nodes_get, use_nodes_set) + def node_texcoords_get(self): if not self.use_nodes: return None - if self._node_texcoords is None: + if self._node_texcoords is ...: + # Running only once, trying to find a valid texcoords node. for n in self.material.node_tree.nodes: if n.bl_idname == 'ShaderNodeTexCoord': self._node_texcoords = n self._grid_to_location(0, 0, ref_node=n) break + if self._node_texcoords is ...: + self._node_texcoords = None if self._node_texcoords is None and not self.is_readonly: tree = self.material.node_tree nodes = tree.nodes @@ -129,6 +137,7 @@ class ShaderWrapper(): self._grid_to_location(-5, 1, dst_node=node_texcoords) self._node_texcoords = node_texcoords return self._node_texcoords + node_texcoords = property(node_texcoords_get) @@ -154,8 +163,9 @@ class PrincipledBSDFWrapper(ShaderWrapper): NODES_LIST = ShaderWrapper.NODES_LIST + NODES_LIST - def __init__(self, material, is_readonly=True): - super(PrincipledBSDFWrapper, self).__init__(material, is_readonly) + def __init__(self, material, is_readonly=True, use_nodes=True): + super(PrincipledBSDFWrapper, self).__init__(material, is_readonly, use_nodes) + def update(self): super(PrincipledBSDFWrapper, self).update() @@ -211,31 +221,42 @@ class PrincipledBSDFWrapper(ShaderWrapper): # -------------------------------------------------------------------- # Normal Map, lazy initialization... - self._node_normalmap = None + self._node_normalmap = ... # -------------------------------------------------------------------- # Tex Coords, lazy initialization... - self._node_texcoords = None + self._node_texcoords = ... + def node_normalmap_get(self): if not self.use_nodes: return None - if self._node_normalmap is None and self.node_principled_bsdf is not None: + if self.node_principled_bsdf is not None: node_principled = self.node_principled_bsdf - if node_principled.inputs["Normal"].is_linked: - node_normalmap = node_principled.inputs["Normal"].links[0].from_node - if node_normalmap.bl_idname == 'ShaderNodeNormalMap': - self._node_normalmap = node_normalmap - self._grid_to_location(0, 0, ref_node=node_normalmap) + if self._node_normalmap is ...: + # Running only once, trying to find a valid normalmap node. + if node_principled.inputs["Normal"].is_linked: + node_normalmap = node_principled.inputs["Normal"].links[0].from_node + if node_normalmap.bl_idname == 'ShaderNodeNormalMap': + self._node_normalmap = node_normalmap + self._grid_to_location(0, 0, ref_node=node_normalmap) + if self._node_normalmap is ...: + self._node_normalmap = None if self._node_normalmap is None and not self.is_readonly: + tree = self.material.node_tree + nodes = tree.nodes + links = tree.links + node_normalmap = nodes.new(type='ShaderNodeNormalMap') node_normalmap.label = "Normal/Map" self._grid_to_location(-1, -2, dst_node=node_normalmap, ref_node=node_principled) # Link links.new(node_normalmap.outputs["Normal"], node_principled.inputs["Normal"]) return self._node_normalmap + node_normalmap = property(node_normalmap_get) + # -------------------------------------------------------------------- # Base Color. @@ -249,8 +270,10 @@ class PrincipledBSDFWrapper(ShaderWrapper): self.material.diffuse_color = color if self.use_nodes and self.node_principled_bsdf is not None: self.node_principled_bsdf.inputs["Base Color"].default_value = rgb_to_rgba(color) + base_color = property(base_color_get, base_color_set) + def base_color_texture_get(self): if not self.use_nodes or self.node_principled_bsdf is None: return None @@ -259,8 +282,10 @@ class PrincipledBSDFWrapper(ShaderWrapper): self.node_principled_bsdf.inputs["Base Color"], grid_row_diff=1, ) + base_color_texture = property(base_color_texture_get) + # -------------------------------------------------------------------- # Specular. @@ -274,8 +299,10 @@ class PrincipledBSDFWrapper(ShaderWrapper): self.material.specular_intensity = value if self.use_nodes and self.node_principled_bsdf is not None: self.node_principled_bsdf.inputs["Specular"].default_value = value + specular = property(specular_get, specular_set) + def specular_tint_get(self): if not self.use_nodes or self.node_principled_bsdf is None: return 0.0 @@ -285,19 +312,24 @@ class PrincipledBSDFWrapper(ShaderWrapper): def specular_tint_set(self, value): if self.use_nodes and self.node_principled_bsdf is not None: self.node_principled_bsdf.inputs["Specular Tint"].default_value = rgb_to_rgba(value) + specular_tint = property(specular_tint_get, specular_tint_set) + # Will only be used as gray-scale one... def specular_texture_get(self): if not self.use_nodes or self.node_principled_bsdf is None: + print("NO NODES!") return None return ShaderImageTextureWrapper( self, self.node_principled_bsdf, self.node_principled_bsdf.inputs["Specular"], grid_row_diff=0, ) + specular_texture = property(specular_texture_get) + # -------------------------------------------------------------------- # Roughness (also sort of inverse of specular hardness...). @@ -311,8 +343,10 @@ class PrincipledBSDFWrapper(ShaderWrapper): self.material.roughness = value if self.use_nodes and self.node_principled_bsdf is not None: self.node_principled_bsdf.inputs["Roughness"].default_value = value + roughness = property(roughness_get, roughness_set) + # Will only be used as gray-scale one... def roughness_texture_get(self): if not self.use_nodes or self.node_principled_bsdf is None: @@ -322,8 +356,10 @@ class PrincipledBSDFWrapper(ShaderWrapper): self.node_principled_bsdf.inputs["Roughness"], grid_row_diff=0, ) + roughness_texture = property(roughness_texture_get) + # -------------------------------------------------------------------- # Metallic (a.k.a reflection, mirror). @@ -337,8 +373,10 @@ class PrincipledBSDFWrapper(ShaderWrapper): self.material.metallic = value if self.use_nodes and self.node_principled_bsdf is not None: self.node_principled_bsdf.inputs["Metallic"].default_value = value + metallic = property(metallic_get, metallic_set) + # Will only be used as gray-scale one... def metallic_texture_get(self): if not self.use_nodes or self.node_principled_bsdf is None: @@ -348,8 +386,10 @@ class PrincipledBSDFWrapper(ShaderWrapper): self.node_principled_bsdf.inputs["Metallic"], grid_row_diff=0, ) + metallic_texture = property(metallic_texture_get) + # -------------------------------------------------------------------- # Transparency settings. @@ -362,8 +402,10 @@ class PrincipledBSDFWrapper(ShaderWrapper): def ior_set(self, value): if self.use_nodes and self.node_principled_bsdf is not None: self.node_principled_bsdf.inputs["IOR"].default_value = value + ior = property(ior_get, ior_set) + # Will only be used as gray-scale one... def ior_texture_get(self): if not self.use_nodes or self.node_principled_bsdf is None: @@ -373,8 +415,10 @@ class PrincipledBSDFWrapper(ShaderWrapper): self.node_principled_bsdf.inputs["IOR"], grid_row_diff=-1, ) + ior_texture = property(ior_texture_get) + def transmission_get(self): if not self.use_nodes or self.node_principled_bsdf is None: return 0.0 @@ -384,8 +428,10 @@ class PrincipledBSDFWrapper(ShaderWrapper): def transmission_set(self, value): if self.use_nodes and self.node_principled_bsdf is not None: self.node_principled_bsdf.inputs["Transmission"].default_value = value + transmission = property(transmission_get, transmission_set) + # Will only be used as gray-scale one... def transmission_texture_get(self): if not self.use_nodes or self.node_principled_bsdf is None: @@ -395,8 +441,10 @@ class PrincipledBSDFWrapper(ShaderWrapper): self.node_principled_bsdf.inputs["Transmission"], grid_row_diff=-1, ) + transmission_texture = property(transmission_texture_get) + # TODO: Do we need more complex handling for alpha (allowing masking and such)? # Would need extra mixing nodes onto Base Color maybe, or even its own shading chain... @@ -412,8 +460,10 @@ class PrincipledBSDFWrapper(ShaderWrapper): def normalmap_strength_set(self, value): if self.use_nodes and self.node_normalmap is not None: self.node_normalmap.inputs["Strength"].default_value = value + normalmap_strength = property(normalmap_strength_get, normalmap_strength_set) + def normalmap_texture_get(self): if not self.use_nodes or self.node_normalmap is None: return None @@ -422,9 +472,11 @@ class PrincipledBSDFWrapper(ShaderWrapper): self.node_normalmap.inputs["Color"], grid_row_diff=-2, ) + normalmap_texture = property(normalmap_texture_get) + class ShaderImageTextureWrapper(): """ Generic 'image texture'-like wrapper, handling image node, some mapping (texture coordinates transformations), @@ -465,15 +517,17 @@ class ShaderImageTextureWrapper(): self.grid_row_diff = grid_row_diff self.use_alpha = use_alpha - self._node_image = None - self._node_mapping = None + self._node_image = ... + self._node_mapping = ... tree = node_dst.id_data nodes = tree.nodes links = tree.links if socket_dst.is_linked: - self._node_image = socket_dst.links[0].from_node + from_node = socket_dst.links[0].from_node + if from_node.bl_idname == 'ShaderNodeTexImage': + self._node_image = from_node if self.node_image is not None: socket_dst = self.node_image.inputs["Vector"] @@ -482,16 +536,67 @@ class ShaderImageTextureWrapper(): if from_node.bl_idname == 'ShaderNodeMapping': self._node_mapping = from_node + + def copy_from(self, tex): + # Avoid generating any node in source texture. + is_readonly_back = tex.is_readonly + tex.is_readonly = True + + if tex.node_image is not None: + self.image = tex.image + self.projection = tex.projection + self.texcoords = tex.texcoords + self.copy_mapping_from(tex) + + tex.is_readonly = is_readonly_back + + + def copy_mapping_from(self, tex): + # Avoid generating any node in source texture. + is_readonly_back = tex.is_readonly + tex.is_readonly = True + + if tex.node_mapping is None: # Used to actually remove mapping node. + if self.has_mapping_node(): + # We assume node_image can never be None in that case... + # Find potential existing link into image's Vector input. + socket_dst = socket_src = None + if self.node_mapping.inputs["Vector"].is_linked: + socket_dst = self.node_image.inputs["Vector"] + socket_src = self.node_mapping.inputs["Vector"].links[0].from_socket + + tree = self.owner_shader.material.node_tree + tree.nodes.remove(self.node_mapping) + self._node_mapping = None + + # If previously existing, re-link texcoords -> image + if socket_src is not None: + tree.links.new(socket_src, socket_dst) + elif self.node_mapping is not None: + self.translation = tex.translation + self.rotation = tex.rotation + self.scale = tex.scale + self.use_min = tex.use_min + self.use_max = tex.use_max + self.min = tex.min + self.max = tex.max + + tex.is_readonly = is_readonly_back + + # -------------------------------------------------------------------- # Image. def node_image_get(self): - if self._node_image is None: + if self._node_image is ...: + # Running only once, trying to find a valid image node. if self.socket_dst.is_linked: node_image = self.socket_dst.links[0].from_node if node_image.bl_idname == 'ShaderNodeTexImage': self._node_image = node_image self.owner_shader._grid_to_location(0, 0, ref_node=node_image) + if self._node_image is ...: + self._node_image = None if self._node_image is None and not self.is_readonly: tree = self.owner_shader.material.node_tree @@ -502,27 +607,33 @@ class ShaderImageTextureWrapper(): self._node_image = node_image return self._node_image + node_image = property(node_image_get) + def image_get(self): return self.node_image.image if self.node_image is not None else None @_set_check def image_set(self, image): self.node_image.image = image + image = property(image_get, image_set) + def projection_get(self): return self.node_image.projection if self.node_image is not None else 'FLAT' @_set_check def projection_set(self, projection): self.node_image.projection = projection + projection = property(projection_get, projection_set) + def texcoords_get(self): if self.node_image is not None: - socket = (self.node_mapping if self._node_mapping is not None else self.node_image).inputs["Vector"] + socket = (self.node_mapping if self.has_mapping_node() else self.node_image).inputs["Vector"] if socket.is_linked: return socket.links[0].from_socket.name return 'UV' @@ -530,20 +641,27 @@ class ShaderImageTextureWrapper(): @_set_check def texcoords_set(self, texcoords): # Image texture node already defaults to UVs, no extra node needed. - if texcoords == 'UV': + # ONLY in case we do not have any texcoords mapping!!! + if texcoords == 'UV' and not self.has_mapping_node(): return tree = self.node_image.id_data links = tree.links - node_dst = self.node_mapping if self._node_mapping is not None else self.node_image + node_dst = self.node_mapping if self.has_mapping_node() else self.node_image socket_src = self.owner_shader.node_texcoords.outputs[texcoords] links.new(socket_src, node_dst.inputs["Vector"]) + texcoords = property(texcoords_get, texcoords_set) + # -------------------------------------------------------------------- # Mapping. + def has_mapping_node(self): + return self._node_mapping not in {None, ...} + def node_mapping_get(self): - if self._node_mapping is None: + if self._node_mapping is ...: + # Running only once, trying to find a valid mapping node. if self.node_image is None: return None if self.node_image.inputs["Vector"].is_linked: @@ -551,78 +669,96 @@ class ShaderImageTextureWrapper(): if node_mapping.bl_idname == 'ShaderNodeMapping': self._node_mapping = node_mapping self.owner_shader._grid_to_location(0, 0 + self.grid_row_diff, ref_node=node_mapping) + if self._node_mapping is ...: + self._node_mapping = None if self._node_mapping is None and not self.is_readonly: # Find potential existing link into image's Vector input. socket_dst = self.node_image.inputs["Vector"] - socket_src = socket_dst.links[0].from_socket if socket_dst.is_linked else None + # If not already existing, we need to create texcoords -> mapping link (from UV). + socket_src = (socket_dst.links[0].from_socket if socket_dst.is_linked + else self.owner_shader.node_texcoords.outputs['UV']) tree = self.owner_shader.material.node_tree node_mapping = tree.nodes.new(type='ShaderNodeMapping') node_mapping.vector_type = 'TEXTURE' self.owner_shader._grid_to_location(-1, 0, dst_node=node_mapping, ref_node=self.node_image) - # link mapping -> image node + # Link mapping -> image node. tree.links.new(node_mapping.outputs["Vector"], socket_dst) - # And if already existing, re-link texcoords -> mapping - if socket_src is not None: - tree.links.new(socket_src, node_mapping.inputs["Vector"]) + # Link texcoords -> mapping. + tree.links.new(socket_src, node_mapping.inputs["Vector"]) self._node_mapping = node_mapping return self._node_mapping + node_mapping = property(node_mapping_get) + def translation_get(self): return self.node_mapping.translation if self.node_mapping is not None else Vector((0.0, 0.0, 0.0)) @_set_check def translation_set(self, translation): self.node_mapping.translation = translation + translation = property(translation_get, translation_set) + def rotation_get(self): return self.node_mapping.rotation if self.node_mapping is not None else Vector((0.0, 0.0, 0.0)) @_set_check def rotation_set(self, rotation): self.node_mapping.rotation = rotation + rotation = property(rotation_get, rotation_set) + def scale_get(self): return self.node_mapping.scale if self.node_mapping is not None else Vector((1.0, 1.0, 1.0)) @_set_check def scale_set(self, scale): self.node_mapping.scale = scale + scale = property(scale_get, scale_set) + def use_min_get(self): return self.node_mapping.use_min if self_mapping.node is not None else False @_set_check def use_min_set(self, use_min): self.node_mapping.use_min = use_min + use_min = property(use_min_get, use_min_set) + def use_max_get(self): return self.node_mapping.use_max if self_mapping.node is not None else False @_set_check def use_max_set(self, use_max): self.node_mapping.use_max = use_max + use_max = property(use_max_get, use_max_set) + def min_get(self): return self.node_mapping.min if self.node_mapping is not None else Vector((0.0, 0.0, 0.0)) @_set_check def min_set(self, min): self.node_mapping.min = min + min = property(min_get, min_set) + def max_get(self): return self.node_mapping.max if self.node_mapping is not None else Vector((0.0, 0.0, 0.0)) @_set_check def max_set(self, max): self.node_mapping.max = max + max = property(max_get, max_set)