node_shader_utils: several fixes, improvements and cleanups.

Fix broken behavior in case of texcoords mapping (we do need texcoords
node in all cases, then, even for UV coords...).

Use nodes by default when generating new write-allowed wrapper around a
material.

Do not try to find nodes in existing tree all the time, do it only once,
even for lazy-initialized nodes (through accessors).

Fix ugly spacing in property accessors (since it looks like some people
do not like a single 'block' with both getters, setters and prop
definition, at least use one sep line everywhere (and two sep lines to
separate properties)...
This commit is contained in:
Bastien Montagne 2018-10-13 19:27:12 +02:00
parent b11515332b
commit dd6bf3f84a
1 changed files with 161 additions and 25 deletions

View File

@ -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)