node_shader_utils: Proper fix for node_normalmap accessor returning ellipsis value.
[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         return self._node_normalmap
256
257     node_normalmap = property(node_normalmap_get)
258
259
260     # --------------------------------------------------------------------
261     # Base Color.
262
263     def base_color_get(self):
264         if not self.use_nodes or self.node_principled_bsdf is None:
265             return self.material.diffuse_color
266         return rgba_to_rgb(self.node_principled_bsdf.inputs["Base Color"].default_value)
267
268     @_set_check
269     def base_color_set(self, color):
270         self.material.diffuse_color = color
271         if self.use_nodes and self.node_principled_bsdf is not None:
272             self.node_principled_bsdf.inputs["Base Color"].default_value = rgb_to_rgba(color)
273
274     base_color = property(base_color_get, base_color_set)
275
276
277     def base_color_texture_get(self):
278         if not self.use_nodes or self.node_principled_bsdf is None:
279             return None
280         return ShaderImageTextureWrapper(
281             self, self.node_principled_bsdf,
282             self.node_principled_bsdf.inputs["Base Color"],
283             grid_row_diff=1,
284         )
285
286     base_color_texture = property(base_color_texture_get)
287
288
289     # --------------------------------------------------------------------
290     # Specular.
291
292     def specular_get(self):
293         if not self.use_nodes or self.node_principled_bsdf is None:
294             return self.material.specular_intensity
295         return self.node_principled_bsdf.inputs["Specular"].default_value
296
297     @_set_check
298     def specular_set(self, value):
299         self.material.specular_intensity = value
300         if self.use_nodes and self.node_principled_bsdf is not None:
301             self.node_principled_bsdf.inputs["Specular"].default_value = value
302
303     specular = property(specular_get, specular_set)
304
305
306     def specular_tint_get(self):
307         if not self.use_nodes or self.node_principled_bsdf is None:
308             return 0.0
309         return rgba_to_rgb(self.node_principled_bsdf.inputs["Specular Tint"].default_value)
310
311     @_set_check
312     def specular_tint_set(self, value):
313         if self.use_nodes and self.node_principled_bsdf is not None:
314             self.node_principled_bsdf.inputs["Specular Tint"].default_value = rgb_to_rgba(value)
315
316     specular_tint = property(specular_tint_get, specular_tint_set)
317
318
319     # Will only be used as gray-scale one...
320     def specular_texture_get(self):
321         if not self.use_nodes or self.node_principled_bsdf is None:
322             print("NO NODES!")
323             return None
324         return ShaderImageTextureWrapper(
325             self, self.node_principled_bsdf,
326             self.node_principled_bsdf.inputs["Specular"],
327             grid_row_diff=0,
328         )
329
330     specular_texture = property(specular_texture_get)
331
332
333     # --------------------------------------------------------------------
334     # Roughness (also sort of inverse of specular hardness...).
335
336     def roughness_get(self):
337         if not self.use_nodes or self.node_principled_bsdf is None:
338             return self.material.roughness
339         return self.node_principled_bsdf.inputs["Roughness"].default_value
340
341     @_set_check
342     def roughness_set(self, value):
343         self.material.roughness = value
344         if self.use_nodes and self.node_principled_bsdf is not None:
345             self.node_principled_bsdf.inputs["Roughness"].default_value = value
346
347     roughness = property(roughness_get, roughness_set)
348
349
350     # Will only be used as gray-scale one...
351     def roughness_texture_get(self):
352         if not self.use_nodes or self.node_principled_bsdf is None:
353             return None
354         return ShaderImageTextureWrapper(
355             self, self.node_principled_bsdf,
356             self.node_principled_bsdf.inputs["Roughness"],
357             grid_row_diff=0,
358         )
359
360     roughness_texture = property(roughness_texture_get)
361
362
363     # --------------------------------------------------------------------
364     # Metallic (a.k.a reflection, mirror).
365
366     def metallic_get(self):
367         if not self.use_nodes or self.node_principled_bsdf is None:
368             return self.material.metallic
369         return self.node_principled_bsdf.inputs["Metallic"].default_value
370
371     @_set_check
372     def metallic_set(self, value):
373         self.material.metallic = value
374         if self.use_nodes and self.node_principled_bsdf is not None:
375             self.node_principled_bsdf.inputs["Metallic"].default_value = value
376
377     metallic = property(metallic_get, metallic_set)
378
379
380     # Will only be used as gray-scale one...
381     def metallic_texture_get(self):
382         if not self.use_nodes or self.node_principled_bsdf is None:
383             return None
384         return ShaderImageTextureWrapper(
385             self, self.node_principled_bsdf,
386             self.node_principled_bsdf.inputs["Metallic"],
387             grid_row_diff=0,
388         )
389
390     metallic_texture = property(metallic_texture_get)
391
392
393     # --------------------------------------------------------------------
394     # Transparency settings.
395
396     def ior_get(self):
397         if not self.use_nodes or self.node_principled_bsdf is None:
398             return 1.0
399         return self.node_principled_bsdf.inputs["IOR"].default_value
400
401     @_set_check
402     def ior_set(self, value):
403         if self.use_nodes and self.node_principled_bsdf is not None:
404             self.node_principled_bsdf.inputs["IOR"].default_value = value
405
406     ior = property(ior_get, ior_set)
407
408
409     # Will only be used as gray-scale one...
410     def ior_texture_get(self):
411         if not self.use_nodes or self.node_principled_bsdf is None:
412             return None
413         return ShaderImageTextureWrapper(
414             self, self.node_principled_bsdf,
415             self.node_principled_bsdf.inputs["IOR"],
416             grid_row_diff=-1,
417         )
418
419     ior_texture = property(ior_texture_get)
420
421
422     def transmission_get(self):
423         if not self.use_nodes or self.node_principled_bsdf is None:
424             return 0.0
425         return self.node_principled_bsdf.inputs["Transmission"].default_value
426
427     @_set_check
428     def transmission_set(self, value):
429         if self.use_nodes and self.node_principled_bsdf is not None:
430             self.node_principled_bsdf.inputs["Transmission"].default_value = value
431
432     transmission = property(transmission_get, transmission_set)
433
434
435     # Will only be used as gray-scale one...
436     def transmission_texture_get(self):
437         if not self.use_nodes or self.node_principled_bsdf is None:
438             return None
439         return ShaderImageTextureWrapper(
440             self, self.node_principled_bsdf,
441             self.node_principled_bsdf.inputs["Transmission"],
442             grid_row_diff=-1,
443         )
444
445     transmission_texture = property(transmission_texture_get)
446
447
448     # TODO: Do we need more complex handling for alpha (allowing masking and such)?
449     #       Would need extra mixing nodes onto Base Color maybe, or even its own shading chain...
450
451     # --------------------------------------------------------------------
452     # Normal map.
453
454     def normalmap_strength_get(self):
455         if not self.use_nodes or self.node_normalmap is None:
456             return 0.0
457         return self.node_normalmap.inputs["Strength"].default_value
458
459     @_set_check
460     def normalmap_strength_set(self, value):
461         if self.use_nodes and self.node_normalmap is not None:
462             self.node_normalmap.inputs["Strength"].default_value = value
463
464     normalmap_strength = property(normalmap_strength_get, normalmap_strength_set)
465
466
467     def normalmap_texture_get(self):
468         if not self.use_nodes or self.node_normalmap is None:
469             return None
470         return ShaderImageTextureWrapper(
471             self, self.node_normalmap,
472             self.node_normalmap.inputs["Color"],
473             grid_row_diff=-2,
474         )
475
476     normalmap_texture = property(normalmap_texture_get)
477
478
479
480 class ShaderImageTextureWrapper():
481     """
482     Generic 'image texture'-like wrapper, handling image node, some mapping (texture coordinates transformations),
483     and texture coordinates source.
484     """
485
486     # Note: this class assumes we are using nodes, otherwise it should never be used...
487
488     NODES_LIST = (
489         "node_dst",
490         "socket_dst",
491
492         "_node_image",
493         "_node_mapping",
494     )
495
496     __slots__ = (
497         "owner_shader",
498         "is_readonly",
499         "grid_row_diff",
500         "use_alpha",
501         *NODES_LIST,
502     )
503
504     def __new__(cls, owner_shader: ShaderWrapper, node_dst, socket_dst, *args, **kwargs):
505         instance = owner_shader._textures.get((node_dst, socket_dst), None)
506         if instance is not None:
507             return instance
508         instance = super(ShaderImageTextureWrapper, cls).__new__(cls)
509         owner_shader._textures[(node_dst, socket_dst)] = instance
510         return instance
511
512     def __init__(self, owner_shader: ShaderWrapper, node_dst, socket_dst, grid_row_diff=0, use_alpha=False):
513         self.owner_shader = owner_shader
514         self.is_readonly = owner_shader.is_readonly
515         self.node_dst = node_dst
516         self.socket_dst = socket_dst
517         self.grid_row_diff = grid_row_diff
518         self.use_alpha = use_alpha
519
520         self._node_image = ...
521         self._node_mapping = ...
522
523         tree = node_dst.id_data
524         nodes = tree.nodes
525         links = tree.links
526
527         if socket_dst.is_linked:
528             from_node = socket_dst.links[0].from_node
529             if from_node.bl_idname == 'ShaderNodeTexImage':
530                 self._node_image = from_node
531
532         if self.node_image is not None:
533             socket_dst = self.node_image.inputs["Vector"]
534             if socket_dst.is_linked:
535                 from_node = socket_dst.links[0].from_node
536                 if from_node.bl_idname == 'ShaderNodeMapping':
537                     self._node_mapping = from_node
538
539
540     def copy_from(self, tex):
541         # Avoid generating any node in source texture.
542         is_readonly_back = tex.is_readonly
543         tex.is_readonly = True
544
545         if tex.node_image is not None:
546             self.image = tex.image
547             self.projection = tex.projection
548             self.texcoords = tex.texcoords
549             self.copy_mapping_from(tex)
550
551         tex.is_readonly = is_readonly_back
552
553
554     def copy_mapping_from(self, tex):
555         # Avoid generating any node in source texture.
556         is_readonly_back = tex.is_readonly
557         tex.is_readonly = True
558
559         if tex.node_mapping is None:  # Used to actually remove mapping node.
560             if self.has_mapping_node():
561                 # We assume node_image can never be None in that case...
562                 # Find potential existing link into image's Vector input.
563                 socket_dst = socket_src = None
564                 if self.node_mapping.inputs["Vector"].is_linked:
565                     socket_dst = self.node_image.inputs["Vector"]
566                     socket_src = self.node_mapping.inputs["Vector"].links[0].from_socket
567
568                 tree = self.owner_shader.material.node_tree
569                 tree.nodes.remove(self.node_mapping)
570                 self._node_mapping = None
571
572                 # If previously existing, re-link texcoords -> image
573                 if socket_src is not None:
574                     tree.links.new(socket_src, socket_dst)
575         elif self.node_mapping is not None:
576             self.translation = tex.translation
577             self.rotation = tex.rotation
578             self.scale = tex.scale
579             self.use_min = tex.use_min
580             self.use_max = tex.use_max
581             self.min = tex.min
582             self.max = tex.max
583
584         tex.is_readonly = is_readonly_back
585
586
587     # --------------------------------------------------------------------
588     # Image.
589
590     def node_image_get(self):
591         if self._node_image is ...:
592             # Running only once, trying to find a valid image node.
593             if self.socket_dst.is_linked:
594                 node_image = self.socket_dst.links[0].from_node
595                 if node_image.bl_idname == 'ShaderNodeTexImage':
596                     self._node_image = node_image
597                     self.owner_shader._grid_to_location(0, 0, ref_node=node_image)
598             if self._node_image is ...:
599                 self._node_image = None
600         if self._node_image is None and not self.is_readonly:
601             tree = self.owner_shader.material.node_tree
602
603             node_image = tree.nodes.new(type='ShaderNodeTexImage')
604             self.owner_shader._grid_to_location(-1, 0 + self.grid_row_diff, dst_node=node_image, ref_node=self.node_dst)
605
606             tree.links.new(node_image.outputs["Alpha" if self.use_alpha else "Color"], self.socket_dst)
607
608             self._node_image = node_image
609         return self._node_image
610
611     node_image = property(node_image_get)
612
613
614     def image_get(self):
615         return self.node_image.image if self.node_image is not None else None
616
617     @_set_check
618     def image_set(self, image):
619         self.node_image.image = image
620
621     image = property(image_get, image_set)
622
623
624     def projection_get(self):
625         return self.node_image.projection if self.node_image is not None else 'FLAT'
626
627     @_set_check
628     def projection_set(self, projection):
629         self.node_image.projection = projection
630
631     projection = property(projection_get, projection_set)
632
633
634     def texcoords_get(self):
635         if self.node_image is not None:
636             socket = (self.node_mapping if self.has_mapping_node() else self.node_image).inputs["Vector"]
637             if socket.is_linked:
638                 return socket.links[0].from_socket.name
639         return 'UV'
640
641     @_set_check
642     def texcoords_set(self, texcoords):
643         # Image texture node already defaults to UVs, no extra node needed.
644         # ONLY in case we do not have any texcoords mapping!!!
645         if texcoords == 'UV' and not self.has_mapping_node():
646             return
647         tree = self.node_image.id_data
648         links = tree.links
649         node_dst = self.node_mapping if self.has_mapping_node() else self.node_image
650         socket_src = self.owner_shader.node_texcoords.outputs[texcoords]
651         links.new(socket_src, node_dst.inputs["Vector"])
652
653     texcoords = property(texcoords_get, texcoords_set)
654
655
656     def extension_get(self):
657         return self.node_image.extension if self.node_image is not None else 'REPEAT'
658
659     @_set_check
660     def extension_set(self, extension):
661         self.node_image.extension = extension
662
663     extension = property(extension_get, extension_set)
664
665
666     # --------------------------------------------------------------------
667     # Mapping.
668
669     def has_mapping_node(self):
670         return self._node_mapping not in {None, ...}
671
672     def node_mapping_get(self):
673         if self._node_mapping is ...:
674             # Running only once, trying to find a valid mapping node.
675             if self.node_image is None:
676                 return None
677             if self.node_image.inputs["Vector"].is_linked:
678                 node_mapping = self.node_image.inputs["Vector"].links[0].from_node
679                 if node_mapping.bl_idname == 'ShaderNodeMapping':
680                     self._node_mapping = node_mapping
681                     self.owner_shader._grid_to_location(0, 0 + self.grid_row_diff, ref_node=node_mapping)
682             if self._node_mapping is ...:
683                 self._node_mapping = None
684         if self._node_mapping is None and not self.is_readonly:
685             # Find potential existing link into image's Vector input.
686             socket_dst = self.node_image.inputs["Vector"]
687             # If not already existing, we need to create texcoords -> mapping link (from UV).
688             socket_src = (socket_dst.links[0].from_socket if socket_dst.is_linked
689                                                           else self.owner_shader.node_texcoords.outputs['UV'])
690
691             tree = self.owner_shader.material.node_tree
692             node_mapping = tree.nodes.new(type='ShaderNodeMapping')
693             node_mapping.vector_type = 'TEXTURE'
694             self.owner_shader._grid_to_location(-1, 0, dst_node=node_mapping, ref_node=self.node_image)
695
696             # Link mapping -> image node.
697             tree.links.new(node_mapping.outputs["Vector"], socket_dst)
698             # Link texcoords -> mapping.
699             tree.links.new(socket_src, node_mapping.inputs["Vector"])
700
701             self._node_mapping = node_mapping
702         return self._node_mapping
703
704     node_mapping = property(node_mapping_get)
705
706
707     def translation_get(self):
708         return self.node_mapping.translation if self.node_mapping is not None else Vector((0.0, 0.0, 0.0))
709
710     @_set_check
711     def translation_set(self, translation):
712         self.node_mapping.translation = translation
713
714     translation = property(translation_get, translation_set)
715
716
717     def rotation_get(self):
718         return self.node_mapping.rotation if self.node_mapping is not None else Vector((0.0, 0.0, 0.0))
719
720     @_set_check
721     def rotation_set(self, rotation):
722         self.node_mapping.rotation = rotation
723
724     rotation = property(rotation_get, rotation_set)
725
726
727     def scale_get(self):
728         return self.node_mapping.scale if self.node_mapping is not None else Vector((1.0, 1.0, 1.0))
729
730     @_set_check
731     def scale_set(self, scale):
732         self.node_mapping.scale = scale
733
734     scale = property(scale_get, scale_set)
735
736
737     def use_min_get(self):
738         return self.node_mapping.use_min if self_mapping.node is not None else False
739
740     @_set_check
741     def use_min_set(self, use_min):
742         self.node_mapping.use_min = use_min
743
744     use_min = property(use_min_get, use_min_set)
745
746
747     def use_max_get(self):
748         return self.node_mapping.use_max if self_mapping.node is not None else False
749
750     @_set_check
751     def use_max_set(self, use_max):
752         self.node_mapping.use_max = use_max
753
754     use_max = property(use_max_get, use_max_set)
755
756
757     def min_get(self):
758         return self.node_mapping.min if self.node_mapping is not None else Vector((0.0, 0.0, 0.0))
759
760     @_set_check
761     def min_set(self, min):
762         self.node_mapping.min = min
763
764     min = property(min_get, min_set)
765
766
767     def max_get(self):
768         return self.node_mapping.max if self.node_mapping is not None else Vector((0.0, 0.0, 0.0))
769
770     @_set_check
771     def max_set(self, max):
772         self.node_mapping.max = max
773
774     max = property(max_get, max_set)