393695bc8ceaffeb4b96bed10fb0c18cf3a2a70b
[blender.git] / release / scripts / modules / bpy_extras / node_shader_utils.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  This program is free software; you can redistribute it and/or
4 #  modify it under the terms of the GNU General Public License
5 #  as published by the Free Software Foundation; either version 2
6 #  of the License, or (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software Foundation,
15 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 # <pep8 compliant>
20
21 import bpy
22 from mathutils import Color, Vector
23
24 __all__ = (
25     "PrincipledBSDFWrapper",
26 )
27
28
29 def _set_check(func):
30     from functools import wraps
31
32     @wraps(func)
33     def wrapper(self, *args, **kwargs):
34         if self.is_readonly:
35             assert(not "Trying to set value to read-only shader!")
36             return
37         return func(self, *args, **kwargs)
38     return wrapper
39
40 def rgb_to_rgba(rgb):
41     return list(rgb) + [1.0]
42
43 def rgba_to_rgb(rgba):
44     return Color((rgba[0], rgba[1], rgba[2]))
45
46
47 class ShaderWrapper():
48     """
49     Base class with minimal common ground for all types of shader interfaces we may want/need to implement.
50     """
51
52     # The two mandatory nodes any children class should support.
53     NODES_LIST = (
54         "node_out",
55
56         "_node_texcoords",
57     )
58
59     __slots__ = (
60         "is_readonly",
61         "material",
62         "_textures",
63         "_grid_locations",
64         *NODES_LIST,
65     )
66
67     _col_size = 300
68     _row_size = 300
69
70     def _grid_to_location(self, x, y, dst_node=None, ref_node=None):
71         if ref_node is not None:  # x and y are relative to this node location.
72             nx = round(ref_node.location.x / self._col_size)
73             ny = round(ref_node.location.y / self._row_size)
74             x += nx
75             y += ny
76         loc = None
77         while True:
78             loc = (x * self._col_size, y * self._row_size)
79             if loc not in self._grid_locations:
80                 break
81             loc = (x * self._col_size, (y - 1) * self._row_size)
82             if loc not in self._grid_locations:
83                 break
84             loc = (x * self._col_size, (y - 2) * self._row_size)
85             if loc not in self._grid_locations:
86                 break
87             x -= 1
88         self._grid_locations.add(loc)
89         if dst_node is not None:
90             dst_node.location = loc
91             dst_node.width = min(dst_node.width, self._col_size - 20)
92         return loc
93
94     def __init__(self, material, is_readonly=True, use_nodes=True):
95         self.is_readonly = is_readonly
96         self.material = material
97         if not is_readonly:
98             self.use_nodes = use_nodes
99         self.update()
100
101     def update(self):  # Should be re-implemented by children classes...
102         for node in self.NODES_LIST:
103             setattr(self, node, None)
104         self._textures = {}
105         self._grid_locations = set()
106
107
108     def use_nodes_get(self):
109         return self.material.use_nodes
110
111     @_set_check
112     def use_nodes_set(self, val):
113         self.material.use_nodes = val
114         self.update()
115
116     use_nodes = property(use_nodes_get, use_nodes_set)
117
118
119     def node_texcoords_get(self):
120         if not self.use_nodes:
121             return None
122         if self._node_texcoords is ...:
123             # Running only once, trying to find a valid texcoords node.
124             for n in self.material.node_tree.nodes:
125                 if n.bl_idname == 'ShaderNodeTexCoord':
126                     self._node_texcoords = n
127                     self._grid_to_location(0, 0, ref_node=n)
128                     break
129             if self._node_texcoords is ...:
130                 self._node_texcoords = None
131         if self._node_texcoords is None and not self.is_readonly:
132             tree = self.material.node_tree
133             nodes = tree.nodes
134             links = tree.links
135
136             node_texcoords = nodes.new(type='ShaderNodeTexCoord')
137             node_texcoords.label = "Texture Coords"
138             self._grid_to_location(-5, 1, dst_node=node_texcoords)
139             self._node_texcoords = node_texcoords
140         return self._node_texcoords
141
142     node_texcoords = property(node_texcoords_get)
143
144
145 class PrincipledBSDFWrapper(ShaderWrapper):
146     """
147     Hard coded shader setup, based in Principled BSDF.
148     Should cover most common cases on import, and gives a basic nodal shaders support for export.
149     Supports basic: diffuse/spec/reflect/transparency/normal, with texturing.
150     """
151     NODES_LIST = (
152         "node_out",
153         "node_principled_bsdf",
154
155         "_node_normalmap",
156         "_node_texcoords",
157     )
158
159     __slots__ = (
160         "is_readonly",
161         "material",
162         *NODES_LIST,
163     )
164
165     NODES_LIST = ShaderWrapper.NODES_LIST + NODES_LIST
166
167     def __init__(self, material, is_readonly=True, use_nodes=True):
168         super(PrincipledBSDFWrapper, self).__init__(material, is_readonly, use_nodes)
169
170
171     def update(self):
172         super(PrincipledBSDFWrapper, self).update()
173
174         if not self.use_nodes:
175             return
176
177         tree = self.material.node_tree
178
179         nodes = tree.nodes
180         links = tree.links
181
182         # --------------------------------------------------------------------
183         # Main output and shader.
184         node_out = None
185         node_principled = None
186         for n in nodes:
187             if n.bl_idname == 'ShaderNodeOutputMaterial' and n.inputs[0].is_linked:
188                 node_out = n
189                 node_principled = n.inputs[0].links[0].from_node
190             elif n.bl_idname == 'ShaderNodeBsdfPrincipled' and n.outputs[0].is_linked:
191                 node_principled = n
192                 for lnk in n.outputs[0].links:
193                     node_out = lnk.to_node
194                     if node_out.bl_idname == 'ShaderNodeOutputMaterial':
195                         break
196             if (
197                     node_out is not None and node_principled is not None and
198                     node_out.bl_idname == 'ShaderNodeOutputMaterial' and
199                     node_principled.bl_idname == 'ShaderNodeBsdfPrincipled'
200             ):
201                 break
202             node_out = node_principled = None  # Could not find a valid pair, let's try again
203
204         if node_out is not None:
205             self._grid_to_location(0, 0, ref_node=node_out)
206         elif not self.is_readonly:
207             node_out = nodes.new(type='ShaderNodeOutputMaterial')
208             node_out.label = "Material Out"
209             node_out.target = 'ALL'
210             self._grid_to_location(1, 1, dst_node=node_out)
211         self.node_out = node_out
212
213         if node_principled is not None:
214             self._grid_to_location(0, 0, ref_node=node_principled)
215         elif not self.is_readonly:
216             node_principled = nodes.new(type='ShaderNodeBsdfPrincipled')
217             node_principled.label = "Principled BSDF"
218             self._grid_to_location(0, 1, dst_node=node_principled)
219             # Link
220             links.new(node_principled.outputs["BSDF"], self.node_out.inputs["Surface"])
221         self.node_principled_bsdf = node_principled
222
223         # --------------------------------------------------------------------
224         # Normal Map, lazy initialization...
225         self._node_normalmap = ...
226
227         # --------------------------------------------------------------------
228         # Tex Coords, lazy initialization...
229         self._node_texcoords = ...
230
231
232     def node_normalmap_get(self):
233         if not self.use_nodes or self.node_principled_bsdf is None:
234             return None
235         node_principled = self.node_principled_bsdf
236         if self._node_normalmap is ...:
237             # Running only once, trying to find a valid normalmap node.
238             if node_principled.inputs["Normal"].is_linked:
239                 node_normalmap = node_principled.inputs["Normal"].links[0].from_node
240                 if node_normalmap.bl_idname == 'ShaderNodeNormalMap':
241                     self._node_normalmap = node_normalmap
242                     self._grid_to_location(0, 0, ref_node=node_normalmap)
243             if self._node_normalmap is ...:
244                 self._node_normalmap = None
245         if self._node_normalmap is None and not self.is_readonly:
246             tree = self.material.node_tree
247             nodes = tree.nodes
248             links = tree.links
249
250             node_normalmap = nodes.new(type='ShaderNodeNormalMap')
251             node_normalmap.label = "Normal/Map"
252             self._grid_to_location(-1, -2, dst_node=node_normalmap, ref_node=node_principled)
253             # Link
254             links.new(node_normalmap.outputs["Normal"], node_principled.inputs["Normal"])
255             self._node_normalmap = node_normalmap
256         return self._node_normalmap
257
258     node_normalmap = property(node_normalmap_get)
259
260
261     # --------------------------------------------------------------------
262     # Base Color.
263
264     def base_color_get(self):
265         if not self.use_nodes or self.node_principled_bsdf is None:
266             return self.material.diffuse_color
267         return rgba_to_rgb(self.node_principled_bsdf.inputs["Base Color"].default_value)
268
269     @_set_check
270     def base_color_set(self, color):
271         self.material.diffuse_color = color
272         if self.use_nodes and self.node_principled_bsdf is not None:
273             self.node_principled_bsdf.inputs["Base Color"].default_value = rgb_to_rgba(color)
274
275     base_color = property(base_color_get, base_color_set)
276
277
278     def base_color_texture_get(self):
279         if not self.use_nodes or self.node_principled_bsdf is None:
280             return None
281         return ShaderImageTextureWrapper(
282             self, self.node_principled_bsdf,
283             self.node_principled_bsdf.inputs["Base Color"],
284             grid_row_diff=1,
285         )
286
287     base_color_texture = property(base_color_texture_get)
288
289
290     # --------------------------------------------------------------------
291     # Specular.
292
293     def specular_get(self):
294         if not self.use_nodes or self.node_principled_bsdf is None:
295             return self.material.specular_intensity
296         return self.node_principled_bsdf.inputs["Specular"].default_value
297
298     @_set_check
299     def specular_set(self, value):
300         self.material.specular_intensity = value
301         if self.use_nodes and self.node_principled_bsdf is not None:
302             self.node_principled_bsdf.inputs["Specular"].default_value = value
303
304     specular = property(specular_get, specular_set)
305
306
307     def specular_tint_get(self):
308         if not self.use_nodes or self.node_principled_bsdf is None:
309             return 0.0
310         return self.node_principled_bsdf.inputs["Specular Tint"].default_value
311
312     @_set_check
313     def specular_tint_set(self, value):
314         if self.use_nodes and self.node_principled_bsdf is not None:
315             self.node_principled_bsdf.inputs["Specular Tint"].default_value = value
316
317     specular_tint = property(specular_tint_get, specular_tint_set)
318
319
320     # Will only be used as gray-scale one...
321     def specular_texture_get(self):
322         if not self.use_nodes or self.node_principled_bsdf is None:
323             print("NO NODES!")
324             return None
325         return ShaderImageTextureWrapper(
326             self, self.node_principled_bsdf,
327             self.node_principled_bsdf.inputs["Specular"],
328             grid_row_diff=0,
329         )
330
331     specular_texture = property(specular_texture_get)
332
333
334     # --------------------------------------------------------------------
335     # Roughness (also sort of inverse of specular hardness...).
336
337     def roughness_get(self):
338         if not self.use_nodes or self.node_principled_bsdf is None:
339             return self.material.roughness
340         return self.node_principled_bsdf.inputs["Roughness"].default_value
341
342     @_set_check
343     def roughness_set(self, value):
344         self.material.roughness = value
345         if self.use_nodes and self.node_principled_bsdf is not None:
346             self.node_principled_bsdf.inputs["Roughness"].default_value = value
347
348     roughness = property(roughness_get, roughness_set)
349
350
351     # Will only be used as gray-scale one...
352     def roughness_texture_get(self):
353         if not self.use_nodes or self.node_principled_bsdf is None:
354             return None
355         return ShaderImageTextureWrapper(
356             self, self.node_principled_bsdf,
357             self.node_principled_bsdf.inputs["Roughness"],
358             grid_row_diff=0,
359         )
360
361     roughness_texture = property(roughness_texture_get)
362
363
364     # --------------------------------------------------------------------
365     # Metallic (a.k.a reflection, mirror).
366
367     def metallic_get(self):
368         if not self.use_nodes or self.node_principled_bsdf is None:
369             return self.material.metallic
370         return self.node_principled_bsdf.inputs["Metallic"].default_value
371
372     @_set_check
373     def metallic_set(self, value):
374         self.material.metallic = value
375         if self.use_nodes and self.node_principled_bsdf is not None:
376             self.node_principled_bsdf.inputs["Metallic"].default_value = value
377
378     metallic = property(metallic_get, metallic_set)
379
380
381     # Will only be used as gray-scale one...
382     def metallic_texture_get(self):
383         if not self.use_nodes or self.node_principled_bsdf is None:
384             return None
385         return ShaderImageTextureWrapper(
386             self, self.node_principled_bsdf,
387             self.node_principled_bsdf.inputs["Metallic"],
388             grid_row_diff=0,
389         )
390
391     metallic_texture = property(metallic_texture_get)
392
393
394     # --------------------------------------------------------------------
395     # Transparency settings.
396
397     def ior_get(self):
398         if not self.use_nodes or self.node_principled_bsdf is None:
399             return 1.0
400         return self.node_principled_bsdf.inputs["IOR"].default_value
401
402     @_set_check
403     def ior_set(self, value):
404         if self.use_nodes and self.node_principled_bsdf is not None:
405             self.node_principled_bsdf.inputs["IOR"].default_value = value
406
407     ior = property(ior_get, ior_set)
408
409
410     # Will only be used as gray-scale one...
411     def ior_texture_get(self):
412         if not self.use_nodes or self.node_principled_bsdf is None:
413             return None
414         return ShaderImageTextureWrapper(
415             self, self.node_principled_bsdf,
416             self.node_principled_bsdf.inputs["IOR"],
417             grid_row_diff=-1,
418         )
419
420     ior_texture = property(ior_texture_get)
421
422
423     def transmission_get(self):
424         if not self.use_nodes or self.node_principled_bsdf is None:
425             return 0.0
426         return self.node_principled_bsdf.inputs["Transmission"].default_value
427
428     @_set_check
429     def transmission_set(self, value):
430         if self.use_nodes and self.node_principled_bsdf is not None:
431             self.node_principled_bsdf.inputs["Transmission"].default_value = value
432
433     transmission = property(transmission_get, transmission_set)
434
435
436     # Will only be used as gray-scale one...
437     def transmission_texture_get(self):
438         if not self.use_nodes or self.node_principled_bsdf is None:
439             return None
440         return ShaderImageTextureWrapper(
441             self, self.node_principled_bsdf,
442             self.node_principled_bsdf.inputs["Transmission"],
443             grid_row_diff=-1,
444         )
445
446     transmission_texture = property(transmission_texture_get)
447
448
449     # TODO: Do we need more complex handling for alpha (allowing masking and such)?
450     #       Would need extra mixing nodes onto Base Color maybe, or even its own shading chain...
451
452     # --------------------------------------------------------------------
453     # Normal map.
454
455     def normalmap_strength_get(self):
456         if not self.use_nodes or self.node_normalmap is None:
457             return 0.0
458         return self.node_normalmap.inputs["Strength"].default_value
459
460     @_set_check
461     def normalmap_strength_set(self, value):
462         if self.use_nodes and self.node_normalmap is not None:
463             self.node_normalmap.inputs["Strength"].default_value = value
464
465     normalmap_strength = property(normalmap_strength_get, normalmap_strength_set)
466
467
468     def normalmap_texture_get(self):
469         if not self.use_nodes or self.node_normalmap is None:
470             return None
471         return ShaderImageTextureWrapper(
472             self, self.node_normalmap,
473             self.node_normalmap.inputs["Color"],
474             grid_row_diff=-2,
475         )
476
477     normalmap_texture = property(normalmap_texture_get)
478
479
480
481 class ShaderImageTextureWrapper():
482     """
483     Generic 'image texture'-like wrapper, handling image node, some mapping (texture coordinates transformations),
484     and texture coordinates source.
485     """
486
487     # Note: this class assumes we are using nodes, otherwise it should never be used...
488
489     NODES_LIST = (
490         "node_dst",
491         "socket_dst",
492
493         "_node_image",
494         "_node_mapping",
495     )
496
497     __slots__ = (
498         "owner_shader",
499         "is_readonly",
500         "grid_row_diff",
501         "use_alpha",
502         *NODES_LIST,
503     )
504
505     def __new__(cls, owner_shader: ShaderWrapper, node_dst, socket_dst, *args, **kwargs):
506         instance = owner_shader._textures.get((node_dst, socket_dst), None)
507         if instance is not None:
508             return instance
509         instance = super(ShaderImageTextureWrapper, cls).__new__(cls)
510         owner_shader._textures[(node_dst, socket_dst)] = instance
511         return instance
512
513     def __init__(self, owner_shader: ShaderWrapper, node_dst, socket_dst, grid_row_diff=0, use_alpha=False):
514         self.owner_shader = owner_shader
515         self.is_readonly = owner_shader.is_readonly
516         self.node_dst = node_dst
517         self.socket_dst = socket_dst
518         self.grid_row_diff = grid_row_diff
519         self.use_alpha = use_alpha
520
521         self._node_image = ...
522         self._node_mapping = ...
523
524         tree = node_dst.id_data
525         nodes = tree.nodes
526         links = tree.links
527
528         if socket_dst.is_linked:
529             from_node = socket_dst.links[0].from_node
530             if from_node.bl_idname == 'ShaderNodeTexImage':
531                 self._node_image = from_node
532
533         if self.node_image is not None:
534             socket_dst = self.node_image.inputs["Vector"]
535             if socket_dst.is_linked:
536                 from_node = socket_dst.links[0].from_node
537                 if from_node.bl_idname == 'ShaderNodeMapping':
538                     self._node_mapping = from_node
539
540
541     def copy_from(self, tex):
542         # Avoid generating any node in source texture.
543         is_readonly_back = tex.is_readonly
544         tex.is_readonly = True
545
546         if tex.node_image is not None:
547             self.image = tex.image
548             self.projection = tex.projection
549             self.texcoords = tex.texcoords
550             self.copy_mapping_from(tex)
551
552         tex.is_readonly = is_readonly_back
553
554
555     def copy_mapping_from(self, tex):
556         # Avoid generating any node in source texture.
557         is_readonly_back = tex.is_readonly
558         tex.is_readonly = True
559
560         if tex.node_mapping is None:  # Used to actually remove mapping node.
561             if self.has_mapping_node():
562                 # We assume node_image can never be None in that case...
563                 # Find potential existing link into image's Vector input.
564                 socket_dst = socket_src = None
565                 if self.node_mapping.inputs["Vector"].is_linked:
566                     socket_dst = self.node_image.inputs["Vector"]
567                     socket_src = self.node_mapping.inputs["Vector"].links[0].from_socket
568
569                 tree = self.owner_shader.material.node_tree
570                 tree.nodes.remove(self.node_mapping)
571                 self._node_mapping = None
572
573                 # If previously existing, re-link texcoords -> image
574                 if socket_src is not None:
575                     tree.links.new(socket_src, socket_dst)
576         elif self.node_mapping is not None:
577             self.translation = tex.translation
578             self.rotation = tex.rotation
579             self.scale = tex.scale
580             self.use_min = tex.use_min
581             self.use_max = tex.use_max
582             self.min = tex.min
583             self.max = tex.max
584
585         tex.is_readonly = is_readonly_back
586
587
588     # --------------------------------------------------------------------
589     # Image.
590
591     def node_image_get(self):
592         if self._node_image is ...:
593             # Running only once, trying to find a valid image node.
594             if self.socket_dst.is_linked:
595                 node_image = self.socket_dst.links[0].from_node
596                 if node_image.bl_idname == 'ShaderNodeTexImage':
597                     self._node_image = node_image
598                     self.owner_shader._grid_to_location(0, 0, ref_node=node_image)
599             if self._node_image is ...:
600                 self._node_image = None
601         if self._node_image is None and not self.is_readonly:
602             tree = self.owner_shader.material.node_tree
603
604             node_image = tree.nodes.new(type='ShaderNodeTexImage')
605             self.owner_shader._grid_to_location(-1, 0 + self.grid_row_diff, dst_node=node_image, ref_node=self.node_dst)
606
607             tree.links.new(node_image.outputs["Alpha" if self.use_alpha else "Color"], self.socket_dst)
608
609             self._node_image = node_image
610         return self._node_image
611
612     node_image = property(node_image_get)
613
614
615     def image_get(self):
616         return self.node_image.image if self.node_image is not None else None
617
618     @_set_check
619     def image_set(self, image):
620         self.node_image.image = image
621
622     image = property(image_get, image_set)
623
624
625     def projection_get(self):
626         return self.node_image.projection if self.node_image is not None else 'FLAT'
627
628     @_set_check
629     def projection_set(self, projection):
630         self.node_image.projection = projection
631
632     projection = property(projection_get, projection_set)
633
634
635     def texcoords_get(self):
636         if self.node_image is not None:
637             socket = (self.node_mapping if self.has_mapping_node() else self.node_image).inputs["Vector"]
638             if socket.is_linked:
639                 return socket.links[0].from_socket.name
640         return 'UV'
641
642     @_set_check
643     def texcoords_set(self, texcoords):
644         # Image texture node already defaults to UVs, no extra node needed.
645         # ONLY in case we do not have any texcoords mapping!!!
646         if texcoords == 'UV' and not self.has_mapping_node():
647             return
648         tree = self.node_image.id_data
649         links = tree.links
650         node_dst = self.node_mapping if self.has_mapping_node() else self.node_image
651         socket_src = self.owner_shader.node_texcoords.outputs[texcoords]
652         links.new(socket_src, node_dst.inputs["Vector"])
653
654     texcoords = property(texcoords_get, texcoords_set)
655
656
657     def extension_get(self):
658         return self.node_image.extension if self.node_image is not None else 'REPEAT'
659
660     @_set_check
661     def extension_set(self, extension):
662         self.node_image.extension = extension
663
664     extension = property(extension_get, extension_set)
665
666
667     # --------------------------------------------------------------------
668     # Mapping.
669
670     def has_mapping_node(self):
671         return self._node_mapping not in {None, ...}
672
673     def node_mapping_get(self):
674         if self._node_mapping is ...:
675             # Running only once, trying to find a valid mapping node.
676             if self.node_image is None:
677                 return None
678             if self.node_image.inputs["Vector"].is_linked:
679                 node_mapping = self.node_image.inputs["Vector"].links[0].from_node
680                 if node_mapping.bl_idname == 'ShaderNodeMapping':
681                     self._node_mapping = node_mapping
682                     self.owner_shader._grid_to_location(0, 0 + self.grid_row_diff, ref_node=node_mapping)
683             if self._node_mapping is ...:
684                 self._node_mapping = None
685         if self._node_mapping is None and not self.is_readonly:
686             # Find potential existing link into image's Vector input.
687             socket_dst = self.node_image.inputs["Vector"]
688             # If not already existing, we need to create texcoords -> mapping link (from UV).
689             socket_src = (socket_dst.links[0].from_socket if socket_dst.is_linked
690                                                           else self.owner_shader.node_texcoords.outputs['UV'])
691
692             tree = self.owner_shader.material.node_tree
693             node_mapping = tree.nodes.new(type='ShaderNodeMapping')
694             node_mapping.vector_type = 'TEXTURE'
695             self.owner_shader._grid_to_location(-1, 0, dst_node=node_mapping, ref_node=self.node_image)
696
697             # Link mapping -> image node.
698             tree.links.new(node_mapping.outputs["Vector"], socket_dst)
699             # Link texcoords -> mapping.
700             tree.links.new(socket_src, node_mapping.inputs["Vector"])
701
702             self._node_mapping = node_mapping
703         return self._node_mapping
704
705     node_mapping = property(node_mapping_get)
706
707
708     def translation_get(self):
709         return self.node_mapping.translation if self.node_mapping is not None else Vector((0.0, 0.0, 0.0))
710
711     @_set_check
712     def translation_set(self, translation):
713         self.node_mapping.translation = translation
714
715     translation = property(translation_get, translation_set)
716
717
718     def rotation_get(self):
719         return self.node_mapping.rotation if self.node_mapping is not None else Vector((0.0, 0.0, 0.0))
720
721     @_set_check
722     def rotation_set(self, rotation):
723         self.node_mapping.rotation = rotation
724
725     rotation = property(rotation_get, rotation_set)
726
727
728     def scale_get(self):
729         return self.node_mapping.scale if self.node_mapping is not None else Vector((1.0, 1.0, 1.0))
730
731     @_set_check
732     def scale_set(self, scale):
733         self.node_mapping.scale = scale
734
735     scale = property(scale_get, scale_set)
736
737
738     def use_min_get(self):
739         return self.node_mapping.use_min if self_mapping.node is not None else False
740
741     @_set_check
742     def use_min_set(self, use_min):
743         self.node_mapping.use_min = use_min
744
745     use_min = property(use_min_get, use_min_set)
746
747
748     def use_max_get(self):
749         return self.node_mapping.use_max if self_mapping.node is not None else False
750
751     @_set_check
752     def use_max_set(self, use_max):
753         self.node_mapping.use_max = use_max
754
755     use_max = property(use_max_get, use_max_set)
756
757
758     def min_get(self):
759         return self.node_mapping.min if self.node_mapping is not None else Vector((0.0, 0.0, 0.0))
760
761     @_set_check
762     def min_set(self, min):
763         self.node_mapping.min = min
764
765     min = property(min_get, min_set)
766
767
768     def max_get(self):
769         return self.node_mapping.max if self.node_mapping is not None else Vector((0.0, 0.0, 0.0))
770
771     @_set_check
772     def max_set(self, max):
773         self.node_mapping.max = max
774
775     max = property(max_get, max_set)