"Use Nodes" was removed in the compositor to simplify the compositing workflow. This introduced a slight inconsistency with the Shader Node Editor. This PR removes "Use Nodes" for object materials. For Line Style, no changes are planned (not sure how to preserve compatibility yet). This simplifies the state of objects; either they have a material or they don't. Backward compatibility: - If Use Nodes is turned Off, new nodes are added to the node tree to simulate the same material: - DNA: Only `use_nodes` is marked deprecated - Python API: - `material.use_nodes` is marked deprecated and will be removed in 6.0. Reading it always returns `True` and setting it has no effect. - `material.diffuse_color`, `material.specular` etc.. Are not used by EEVEE anymore but are kept because they are used by Workbench. Forward compatibility: Always enable 'Use Nodes' when writing blend files. Known Issues: Some UI tests are failing on macOS Pull Request: https://projects.blender.org/blender/blender/pulls/141278
830 lines
30 KiB
Python
830 lines
30 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):
|
|
self.is_readonly = is_readonly
|
|
self.material = material
|
|
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 node_texcoords_get(self):
|
|
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):
|
|
super(PrincipledBSDFWrapper, self).__init__(material, is_readonly)
|
|
|
|
def update(self):
|
|
super(PrincipledBSDFWrapper, self).update()
|
|
|
|
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 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 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.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 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 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.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 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 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.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 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 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.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 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 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.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 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 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.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 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 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.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 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 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.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 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 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.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 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 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.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 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 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.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 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.surface_render_method = 'DITHERED'
|
|
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)
|