c7807a425a
- `bpy.types.Material.blend_method` aliases `bpy.types.Material.surface_render_method`. 'Opaque' and 'Alpha Clip' maps to deferred. - Renamed `show_transparent_back` to `use_transparency_overlap` - Renamed `use_screen_refraction` to `use_raytrace_refraction` - Deprecate `use_sss_translucency` and `use_sss_translucency` Related to: #113976 **NOTE** The light probe changes will be done in a different patch. Both patches should land just before we remove EEVEE Legacy from the code-base. Pull Request: https://projects.blender.org/blender/blender/pulls/122297
848 lines
31 KiB
Python
848 lines
31 KiB
Python
# SPDX-FileCopyrightText: 2018-2023 Blender Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
from mathutils import Color, Vector
|
|
|
|
__all__ = (
|
|
"PrincipledBSDFWrapper",
|
|
)
|
|
|
|
|
|
def _set_check(func):
|
|
from functools import wraps
|
|
|
|
@wraps(func)
|
|
def wrapper(self, *args, **kwargs):
|
|
if self.is_readonly:
|
|
assert not "Trying to set value to read-only shader!"
|
|
return
|
|
return func(self, *args, **kwargs)
|
|
return wrapper
|
|
|
|
|
|
def rgb_to_rgba(rgb):
|
|
return list(rgb) + [1.0]
|
|
|
|
|
|
def rgba_to_rgb(rgba):
|
|
return Color((rgba[0], rgba[1], rgba[2]))
|
|
|
|
|
|
# All clamping value shall follow Blender's defined min/max (check relevant node definition .c file).
|
|
def values_clamp(val, minv, maxv):
|
|
if hasattr(val, "__iter__"):
|
|
return tuple(max(minv, min(maxv, v)) for v in val)
|
|
else:
|
|
return max(minv, min(maxv, val))
|
|
|
|
# TODO: Consider moving node_input_value_set/node_input_value_get into a common utility module if
|
|
# more usage merits doing so. If that is done, abstract out the validity check and make it usable
|
|
# for node outputs as well. See PR #119354 for details.
|
|
|
|
|
|
def node_input_value_set(node, input, value):
|
|
if node is None or input not in node.inputs:
|
|
return
|
|
|
|
node.inputs[input].default_value = value
|
|
|
|
|
|
def node_input_value_get(node, input, default_value=None):
|
|
if node is None or input not in node.inputs:
|
|
return default_value
|
|
|
|
return node.inputs[input].default_value
|
|
|
|
|
|
class ShaderWrapper():
|
|
"""
|
|
Base class with minimal common ground for all types of shader interfaces we may want/need to implement.
|
|
"""
|
|
|
|
# The two mandatory nodes any children class should support.
|
|
NODES_LIST = (
|
|
"node_out",
|
|
|
|
"_node_texcoords",
|
|
)
|
|
|
|
__slots__ = (
|
|
"is_readonly",
|
|
"material",
|
|
"_textures",
|
|
"_grid_locations",
|
|
*NODES_LIST,
|
|
)
|
|
|
|
_col_size = 300
|
|
_row_size = 300
|
|
|
|
def _grid_to_location(self, x, y, dst_node=None, ref_node=None):
|
|
if ref_node is not None: # x and y are relative to this node location.
|
|
nx = round(ref_node.location.x / self._col_size)
|
|
ny = round(ref_node.location.y / self._row_size)
|
|
x += nx
|
|
y += ny
|
|
loc = None
|
|
while True:
|
|
loc = (x * self._col_size, y * self._row_size)
|
|
if loc not in self._grid_locations:
|
|
break
|
|
loc = (x * self._col_size, (y - 1) * self._row_size)
|
|
if loc not in self._grid_locations:
|
|
break
|
|
loc = (x * self._col_size, (y - 2) * self._row_size)
|
|
if loc not in self._grid_locations:
|
|
break
|
|
x -= 1
|
|
self._grid_locations.add(loc)
|
|
if dst_node is not None:
|
|
dst_node.location = loc
|
|
dst_node.width = min(dst_node.width, self._col_size - 20)
|
|
return loc
|
|
|
|
def __init__(self, material, is_readonly=True, use_nodes=True):
|
|
self.is_readonly = is_readonly
|
|
self.material = material
|
|
if not is_readonly:
|
|
self.use_nodes = use_nodes
|
|
self.update()
|
|
|
|
def update(self): # Should be re-implemented by children classes...
|
|
for node in self.NODES_LIST:
|
|
setattr(self, node, None)
|
|
self._textures = {}
|
|
self._grid_locations = set()
|
|
|
|
def use_nodes_get(self):
|
|
return self.material.use_nodes
|
|
|
|
@_set_check
|
|
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 ...:
|
|
# 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
|
|
# links = tree.links
|
|
|
|
node_texcoords = nodes.new(type='ShaderNodeTexCoord')
|
|
node_texcoords.label = "Texture Coords"
|
|
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)
|
|
|
|
|
|
class PrincipledBSDFWrapper(ShaderWrapper):
|
|
"""
|
|
Hard coded shader setup, based in Principled BSDF.
|
|
Should cover most common cases on import, and gives a basic nodal shaders support for export.
|
|
Supports basic: diffuse/spec/reflect/transparency/normal, with texturing.
|
|
"""
|
|
NODES_LIST = (
|
|
"node_out",
|
|
"node_principled_bsdf",
|
|
|
|
"_node_normalmap",
|
|
"_node_texcoords",
|
|
)
|
|
|
|
__slots__ = (
|
|
"is_readonly",
|
|
"material",
|
|
*NODES_LIST,
|
|
)
|
|
|
|
NODES_LIST = ShaderWrapper.NODES_LIST + NODES_LIST
|
|
|
|
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()
|
|
|
|
if not self.use_nodes:
|
|
return
|
|
|
|
tree = self.material.node_tree
|
|
|
|
nodes = tree.nodes
|
|
links = tree.links
|
|
|
|
# --------------------------------------------------------------------
|
|
# Main output and shader.
|
|
node_out = None
|
|
node_principled = None
|
|
for n in nodes:
|
|
if n.bl_idname == 'ShaderNodeOutputMaterial' and n.inputs[0].is_linked:
|
|
node_out = n
|
|
node_principled = n.inputs[0].links[0].from_node
|
|
elif n.bl_idname == 'ShaderNodeBsdfPrincipled' and n.outputs[0].is_linked:
|
|
node_principled = n
|
|
for lnk in n.outputs[0].links:
|
|
node_out = lnk.to_node
|
|
if node_out.bl_idname == 'ShaderNodeOutputMaterial':
|
|
break
|
|
if (
|
|
node_out is not None and node_principled is not None and
|
|
node_out.bl_idname == 'ShaderNodeOutputMaterial' and
|
|
node_principled.bl_idname == 'ShaderNodeBsdfPrincipled'
|
|
):
|
|
break
|
|
node_out = node_principled = None # Could not find a valid pair, let's try again
|
|
|
|
if node_out is not None:
|
|
self._grid_to_location(0, 0, ref_node=node_out)
|
|
elif not self.is_readonly:
|
|
node_out = nodes.new(type='ShaderNodeOutputMaterial')
|
|
node_out.label = "Material Out"
|
|
node_out.target = 'ALL'
|
|
self._grid_to_location(1, 1, dst_node=node_out)
|
|
self.node_out = node_out
|
|
|
|
if node_principled is not None:
|
|
self._grid_to_location(0, 0, ref_node=node_principled)
|
|
elif not self.is_readonly:
|
|
node_principled = nodes.new(type='ShaderNodeBsdfPrincipled')
|
|
node_principled.label = "Principled BSDF"
|
|
self._grid_to_location(0, 1, dst_node=node_principled)
|
|
# Link
|
|
links.new(node_principled.outputs["BSDF"], self.node_out.inputs["Surface"])
|
|
self.node_principled_bsdf = node_principled
|
|
|
|
# --------------------------------------------------------------------
|
|
# Normal Map, lazy initialization...
|
|
self._node_normalmap = ...
|
|
|
|
# --------------------------------------------------------------------
|
|
# Tex Coords, lazy initialization...
|
|
self._node_texcoords = ...
|
|
|
|
def node_normalmap_get(self):
|
|
if not self.use_nodes or self.node_principled_bsdf is None:
|
|
return None
|
|
node_principled = self.node_principled_bsdf
|
|
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"])
|
|
self._node_normalmap = node_normalmap
|
|
return self._node_normalmap
|
|
|
|
node_normalmap = property(node_normalmap_get)
|
|
|
|
# --------------------------------------------------------------------
|
|
# Base Color.
|
|
|
|
def base_color_get(self):
|
|
if not self.use_nodes or self.node_principled_bsdf is None:
|
|
return self.material.diffuse_color
|
|
return rgba_to_rgb(self.node_principled_bsdf.inputs["Base Color"].default_value)
|
|
|
|
@_set_check
|
|
def base_color_set(self, color):
|
|
color = values_clamp(color, 0.0, 1.0)
|
|
color = rgb_to_rgba(color)
|
|
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 = 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
|
|
return ShaderImageTextureWrapper(
|
|
self, self.node_principled_bsdf,
|
|
self.node_principled_bsdf.inputs["Base Color"],
|
|
grid_row_diff=1,
|
|
)
|
|
|
|
base_color_texture = property(base_color_texture_get)
|
|
|
|
# --------------------------------------------------------------------
|
|
# Specular.
|
|
|
|
def specular_get(self):
|
|
if not self.use_nodes or self.node_principled_bsdf is None:
|
|
return self.material.specular_intensity
|
|
return self.node_principled_bsdf.inputs["Specular IOR Level"].default_value
|
|
|
|
@_set_check
|
|
def specular_set(self, value):
|
|
value = values_clamp(value, 0.0, 1.0)
|
|
self.material.specular_intensity = value
|
|
if self.use_nodes and self.node_principled_bsdf is not None:
|
|
self.node_principled_bsdf.inputs["Specular IOR Level"].default_value = value
|
|
|
|
specular = property(specular_get, specular_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:
|
|
return None
|
|
return ShaderImageTextureWrapper(
|
|
self, self.node_principled_bsdf,
|
|
self.node_principled_bsdf.inputs["Specular IOR Level"],
|
|
grid_row_diff=0,
|
|
colorspace_name='Non-Color',
|
|
)
|
|
|
|
specular_texture = property(specular_texture_get)
|
|
|
|
# --------------------------------------------------------------------
|
|
# Specular Tint.
|
|
|
|
def specular_tint_get(self):
|
|
if not self.use_nodes or self.node_principled_bsdf is None:
|
|
return Color((0.0, 0.0, 0.0))
|
|
return rgba_to_rgb(self.node_principled_bsdf.inputs["Specular Tint"].default_value)
|
|
|
|
@_set_check
|
|
def specular_tint_set(self, color):
|
|
color = values_clamp(color, 0.0, 1.0)
|
|
color = rgb_to_rgba(color)
|
|
if self.use_nodes and self.node_principled_bsdf is not None:
|
|
self.node_principled_bsdf.inputs["Specular Tint"].default_value = color
|
|
|
|
specular_tint = property(specular_tint_get, specular_tint_set)
|
|
|
|
def specular_tint_texture_get(self):
|
|
if not self.use_nodes or self.node_principled_bsdf is None:
|
|
return None
|
|
return ShaderImageTextureWrapper(
|
|
self, self.node_principled_bsdf,
|
|
self.node_principled_bsdf.inputs["Specular Tint"],
|
|
grid_row_diff=0,
|
|
)
|
|
|
|
specular_tint_texture = property(specular_tint_texture_get)
|
|
|
|
# --------------------------------------------------------------------
|
|
# Roughness (also sort of inverse of specular hardness...).
|
|
|
|
def roughness_get(self):
|
|
if not self.use_nodes or self.node_principled_bsdf is None:
|
|
return self.material.roughness
|
|
return self.node_principled_bsdf.inputs["Roughness"].default_value
|
|
|
|
@_set_check
|
|
def roughness_set(self, value):
|
|
value = values_clamp(value, 0.0, 1.0)
|
|
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:
|
|
return None
|
|
return ShaderImageTextureWrapper(
|
|
self, self.node_principled_bsdf,
|
|
self.node_principled_bsdf.inputs["Roughness"],
|
|
grid_row_diff=0,
|
|
colorspace_name='Non-Color',
|
|
)
|
|
|
|
roughness_texture = property(roughness_texture_get)
|
|
|
|
# --------------------------------------------------------------------
|
|
# Metallic (a.k.a reflection, mirror).
|
|
|
|
def metallic_get(self):
|
|
if not self.use_nodes or self.node_principled_bsdf is None:
|
|
return self.material.metallic
|
|
return self.node_principled_bsdf.inputs["Metallic"].default_value
|
|
|
|
@_set_check
|
|
def metallic_set(self, value):
|
|
value = values_clamp(value, 0.0, 1.0)
|
|
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:
|
|
return None
|
|
return ShaderImageTextureWrapper(
|
|
self, self.node_principled_bsdf,
|
|
self.node_principled_bsdf.inputs["Metallic"],
|
|
grid_row_diff=0,
|
|
colorspace_name="Non-Color",
|
|
)
|
|
|
|
metallic_texture = property(metallic_texture_get)
|
|
|
|
# --------------------------------------------------------------------
|
|
# Transparency settings.
|
|
|
|
def ior_get(self):
|
|
if not self.use_nodes or self.node_principled_bsdf is None:
|
|
return 1.0
|
|
return self.node_principled_bsdf.inputs["IOR"].default_value
|
|
|
|
@_set_check
|
|
def ior_set(self, value):
|
|
value = values_clamp(value, 0.0, 1000.0)
|
|
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:
|
|
return None
|
|
return ShaderImageTextureWrapper(
|
|
self, self.node_principled_bsdf,
|
|
self.node_principled_bsdf.inputs["IOR"],
|
|
grid_row_diff=-1,
|
|
colorspace_name='Non-Color',
|
|
)
|
|
|
|
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
|
|
return self.node_principled_bsdf.inputs["Transmission Weight"].default_value
|
|
|
|
@_set_check
|
|
def transmission_set(self, value):
|
|
value = values_clamp(value, 0.0, 1.0)
|
|
if self.use_nodes and self.node_principled_bsdf is not None:
|
|
self.node_principled_bsdf.inputs["Transmission Weight"].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:
|
|
return None
|
|
return ShaderImageTextureWrapper(
|
|
self, self.node_principled_bsdf,
|
|
self.node_principled_bsdf.inputs["Transmission Weight"],
|
|
grid_row_diff=-1,
|
|
colorspace_name='Non-Color',
|
|
)
|
|
|
|
transmission_texture = property(transmission_texture_get)
|
|
|
|
def alpha_get(self):
|
|
if not self.use_nodes or self.node_principled_bsdf is None:
|
|
return 1.0
|
|
return self.node_principled_bsdf.inputs["Alpha"].default_value
|
|
|
|
@_set_check
|
|
def alpha_set(self, value):
|
|
value = values_clamp(value, 0.0, 1.0)
|
|
if self.use_nodes and self.node_principled_bsdf is not None:
|
|
self.node_principled_bsdf.inputs["Alpha"].default_value = value
|
|
|
|
alpha = property(alpha_get, alpha_set)
|
|
|
|
# Will only be used as gray-scale one...
|
|
def alpha_texture_get(self):
|
|
if not self.use_nodes or self.node_principled_bsdf is None:
|
|
return None
|
|
return ShaderImageTextureWrapper(
|
|
self, self.node_principled_bsdf,
|
|
self.node_principled_bsdf.inputs["Alpha"],
|
|
use_alpha=True,
|
|
grid_row_diff=-1,
|
|
colorspace_name='Non-Color',
|
|
)
|
|
|
|
alpha_texture = property(alpha_texture_get)
|
|
|
|
# --------------------------------------------------------------------
|
|
# Emission color.
|
|
|
|
def emission_color_get(self):
|
|
if not self.use_nodes or self.node_principled_bsdf is None:
|
|
return Color((0.0, 0.0, 0.0))
|
|
return rgba_to_rgb(self.node_principled_bsdf.inputs["Emission Color"].default_value)
|
|
|
|
@_set_check
|
|
def emission_color_set(self, color):
|
|
if self.use_nodes and self.node_principled_bsdf is not None:
|
|
color = values_clamp(color, 0.0, 1000000.0)
|
|
color = rgb_to_rgba(color)
|
|
self.node_principled_bsdf.inputs["Emission Color"].default_value = color
|
|
|
|
emission_color = property(emission_color_get, emission_color_set)
|
|
|
|
def emission_color_texture_get(self):
|
|
if not self.use_nodes or self.node_principled_bsdf is None:
|
|
return None
|
|
return ShaderImageTextureWrapper(
|
|
self, self.node_principled_bsdf,
|
|
self.node_principled_bsdf.inputs["Emission Color"],
|
|
grid_row_diff=1,
|
|
)
|
|
|
|
emission_color_texture = property(emission_color_texture_get)
|
|
|
|
def emission_strength_get(self):
|
|
if not self.use_nodes or self.node_principled_bsdf is None:
|
|
return 1.0
|
|
return self.node_principled_bsdf.inputs["Emission Strength"].default_value
|
|
|
|
@_set_check
|
|
def emission_strength_set(self, value):
|
|
value = values_clamp(value, 0.0, 1000000.0)
|
|
if self.use_nodes and self.node_principled_bsdf is not None:
|
|
self.node_principled_bsdf.inputs["Emission Strength"].default_value = value
|
|
|
|
emission_strength = property(emission_strength_get, emission_strength_set)
|
|
|
|
def emission_strength_texture_get(self):
|
|
if not self.use_nodes or self.node_principled_bsdf is None:
|
|
return None
|
|
return ShaderImageTextureWrapper(
|
|
self, self.node_principled_bsdf,
|
|
self.node_principled_bsdf.inputs["Emission Strength"],
|
|
grid_row_diff=-1,
|
|
colorspace_name='Non-Color',
|
|
)
|
|
|
|
emission_strength_texture = property(emission_strength_texture_get)
|
|
|
|
# --------------------------------------------------------------------
|
|
# Normal map.
|
|
|
|
def normalmap_strength_get(self):
|
|
if not self.use_nodes or self.node_normalmap is None:
|
|
return 0.0
|
|
return self.node_normalmap.inputs["Strength"].default_value
|
|
|
|
@_set_check
|
|
def normalmap_strength_set(self, value):
|
|
value = values_clamp(value, 0.0, 10.0)
|
|
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
|
|
return ShaderImageTextureWrapper(
|
|
self, self.node_normalmap,
|
|
self.node_normalmap.inputs["Color"],
|
|
grid_row_diff=-2,
|
|
colorspace_is_data=True,
|
|
)
|
|
|
|
normalmap_texture = property(normalmap_texture_get)
|
|
|
|
|
|
class ShaderImageTextureWrapper():
|
|
"""
|
|
Generic 'image texture'-like wrapper, handling image node, some mapping (texture coordinates transformations),
|
|
and texture coordinates source.
|
|
"""
|
|
|
|
# Note: this class assumes we are using nodes, otherwise it should never be used...
|
|
|
|
NODES_LIST = (
|
|
"node_dst",
|
|
"socket_dst",
|
|
|
|
"_node_image",
|
|
"_node_mapping",
|
|
)
|
|
|
|
__slots__ = (
|
|
"owner_shader",
|
|
"is_readonly",
|
|
"grid_row_diff",
|
|
"use_alpha",
|
|
"colorspace_is_data",
|
|
"colorspace_name",
|
|
*NODES_LIST,
|
|
)
|
|
|
|
def __new__(cls, owner_shader: ShaderWrapper, node_dst, socket_dst, *_args, **_kwargs):
|
|
instance = owner_shader._textures.get((node_dst, socket_dst), None)
|
|
if instance is not None:
|
|
return instance
|
|
instance = super(ShaderImageTextureWrapper, cls).__new__(cls)
|
|
owner_shader._textures[(node_dst, socket_dst)] = instance
|
|
return instance
|
|
|
|
def __init__(self, owner_shader: ShaderWrapper, node_dst, socket_dst, grid_row_diff=0,
|
|
use_alpha=False, colorspace_is_data=..., colorspace_name=...):
|
|
self.owner_shader = owner_shader
|
|
self.is_readonly = owner_shader.is_readonly
|
|
self.node_dst = node_dst
|
|
self.socket_dst = socket_dst
|
|
self.grid_row_diff = grid_row_diff
|
|
self.use_alpha = use_alpha
|
|
self.colorspace_is_data = colorspace_is_data
|
|
self.colorspace_name = colorspace_name
|
|
|
|
self._node_image = ...
|
|
self._node_mapping = ...
|
|
|
|
# tree = node_dst.id_data
|
|
# nodes = tree.nodes
|
|
# links = tree.links
|
|
|
|
if socket_dst.is_linked:
|
|
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"]
|
|
if socket_dst.is_linked:
|
|
from_node = socket_dst.links[0].from_node
|
|
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
|
|
|
|
tex.is_readonly = is_readonly_back
|
|
|
|
# --------------------------------------------------------------------
|
|
# Image.
|
|
|
|
def node_image_get(self):
|
|
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
|
|
|
|
node_image = tree.nodes.new(type='ShaderNodeTexImage')
|
|
self.owner_shader._grid_to_location(
|
|
-1, 0 + self.grid_row_diff,
|
|
dst_node=node_image, ref_node=self.node_dst,
|
|
)
|
|
|
|
tree.links.new(node_image.outputs["Alpha" if self.use_alpha else "Color"], self.socket_dst)
|
|
if self.use_alpha:
|
|
self.owner_shader.material.blend_method = 'BLEND'
|
|
self.owner_shader.material.use_transparency_overlap = False
|
|
|
|
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):
|
|
if self.colorspace_is_data is not ...:
|
|
if image.colorspace_settings.is_data != self.colorspace_is_data and image.users >= 1:
|
|
image = image.copy()
|
|
image.colorspace_settings.is_data = self.colorspace_is_data
|
|
if self.colorspace_name is not ...:
|
|
if image.colorspace_settings.name != self.colorspace_name and image.users >= 1:
|
|
image = image.copy()
|
|
image.colorspace_settings.name = self.colorspace_name
|
|
if self.use_alpha:
|
|
# Try to be smart, and only use image's alpha output if image actually has alpha data.
|
|
tree = self.owner_shader.material.node_tree
|
|
if image.channels < 4 or image.depth in {24, 8}:
|
|
tree.links.new(self.node_image.outputs["Color"], self.socket_dst)
|
|
else:
|
|
tree.links.new(self.node_image.outputs["Alpha"], self.socket_dst)
|
|
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.has_mapping_node() else self.node_image).inputs["Vector"]
|
|
if socket.is_linked:
|
|
return socket.links[0].from_socket.name
|
|
return 'UV'
|
|
|
|
@_set_check
|
|
def texcoords_set(self, texcoords):
|
|
# Image texture node already defaults to UVs, no extra node needed.
|
|
# 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.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)
|
|
|
|
def extension_get(self):
|
|
return self.node_image.extension if self.node_image is not None else 'REPEAT'
|
|
|
|
@_set_check
|
|
def extension_set(self, extension):
|
|
self.node_image.extension = extension
|
|
|
|
extension = property(extension_get, extension_set)
|
|
|
|
# --------------------------------------------------------------------
|
|
# Mapping.
|
|
|
|
def has_mapping_node(self):
|
|
return self._node_mapping not in {None, ...}
|
|
|
|
def node_mapping_get(self):
|
|
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:
|
|
node_mapping = self.node_image.inputs["Vector"].links[0].from_node
|
|
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"]
|
|
# 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.
|
|
tree.links.new(node_mapping.outputs["Vector"], socket_dst)
|
|
# 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 node_input_value_get(self.node_mapping, "Location", Vector((0.0, 0.0, 0.0)))
|
|
|
|
@_set_check
|
|
def translation_set(self, translation):
|
|
node_input_value_set(self.node_mapping, "Location", translation)
|
|
|
|
translation = property(translation_get, translation_set)
|
|
|
|
def rotation_get(self):
|
|
if self.node_mapping is None:
|
|
return Vector((0.0, 0.0, 0.0))
|
|
return self.node_mapping.inputs["Rotation"].default_value
|
|
|
|
@_set_check
|
|
def rotation_set(self, rotation):
|
|
self.node_mapping.inputs["Rotation"].default_value = rotation
|
|
|
|
rotation = property(rotation_get, rotation_set)
|
|
|
|
def scale_get(self):
|
|
if self.node_mapping is None:
|
|
return Vector((1.0, 1.0, 1.0))
|
|
return self.node_mapping.inputs["Scale"].default_value
|
|
|
|
@_set_check
|
|
def scale_set(self, scale):
|
|
self.node_mapping.inputs["Scale"].default_value = scale
|
|
|
|
scale = property(scale_get, scale_set)
|