blender/scripts/modules/bpy_extras/node_shader_utils.py
Sergey Sharybin 03806d0b67 Re-design of submodules used in blender.git
This commit implements described in the #104573.

The goal is to fix the confusion of the submodule hashes change, which are not
ideal for any of the supported git-module configuration (they are either always
visible causing confusion, or silently staged and committed, also causing
confusion).

This commit replaces submodules with a checkout of addons and addons_contrib,
covered by the .gitignore, and locale and developer tools are moved to the
main repository.

This also changes the paths:
- /release/scripts are moved to the /scripts
- /source/tools are moved to the /tools
- /release/datafiles/locale is moved to /locale

This is done to avoid conflicts when using bisect, and also allow buildbot to
automatically "recover" wgen building older or newer branches/patches.

Running `make update` will initialize the local checkout to the changed
repository configuration.

Another aspect of the change is that the make update will support Github style
of remote organization (origin remote pointing to thy fork, upstream remote
pointing to the upstream blender/blender.git).

Pull Request #104755
2023-02-21 16:39:58 +01:00

815 lines
30 KiB
Python

# 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))
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"].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"].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
return self.node_principled_bsdf.inputs["Specular Tint"].default_value
@_set_check
def specular_tint_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["Specular Tint"].default_value = 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,
colorspace_name='Non-Color',
)
specular_texture = property(specular_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"].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"].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"],
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"].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"].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"],
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._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):
if self.node_mapping is None:
return Vector((0.0, 0.0, 0.0))
return self.node_mapping.inputs['Location'].default_value
@_set_check
def translation_set(self, translation):
self.node_mapping.inputs['Location'].default_value = 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)