Newer
Older
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
bl_info = {
'name': "Nodes Efficiency Tools",
'author': "Bartek Skorupa",
Bartek Skorupa
committed
'version': (2, 27),
'blender': (2, 6, 6),
'location': "Node Editor Properties Panel (Ctrl-SPACE)",
'description': "Nodes Efficiency Tools",
'warning': "",
'wiki_url': "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Nodes/Nodes_Efficiency_Tools",
'tracker_url': "http://projects.blender.org/tracker/index.php?func=detail&aid=33543&group_id=153&atid=469",
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
'category': "Node",
}
import bpy
from bpy.types import Operator, Panel, Menu
from bpy.props import FloatProperty, EnumProperty, BoolProperty
#################
# rl_outputs:
# list of outputs of Input Render Layer
# with attributes determinig if pass is used,
# and MultiLayer EXR outputs names and corresponding render engines
#
# rl_outputs entry = (render_pass, rl_output_name, exr_output_name, in_internal, in_cycles)
rl_outputs = (
('use_pass_ambient_occlusion', 'AO', 'AO', True, True),
('use_pass_color', 'Color', 'Color', True, False),
('use_pass_combined', 'Image', 'Combined', True, True),
('use_pass_diffuse', 'Diffuse', 'Diffuse', True, False),
('use_pass_diffuse_color', 'Diffuse Color', 'DiffCol', False, True),
('use_pass_diffuse_direct', 'Diffuse Direct', 'DiffDir', False, True),
('use_pass_diffuse_indirect', 'Diffuse Indirect', 'DiffInd', False, True),
('use_pass_emit', 'Emit', 'Emit', True, False),
('use_pass_environment', 'Environment', 'Env', True, False),
('use_pass_glossy_color', 'Glossy Color', 'GlossCol', False, True),
('use_pass_glossy_direct', 'Glossy Direct', 'GlossDir', False, True),
('use_pass_glossy_indirect', 'Glossy Indirect', 'GlossInd', False, True),
('use_pass_indirect', 'Indirect', 'Indirect', True, False),
('use_pass_material_index', 'IndexMA', 'IndexMA', True, True),
('use_pass_mist', 'Mist', 'Mist', True, False),
('use_pass_normal', 'Normal', 'Normal', True, True),
('use_pass_object_index', 'IndexOB', 'IndexOB', True, True),
('use_pass_reflection', 'Reflect', 'Reflect', True, False),
('use_pass_refraction', 'Refract', 'Refract', True, False),
('use_pass_shadow', 'Shadow', 'Shadow', True, True),
('use_pass_specular', 'Specular', 'Spec', True, False),
('use_pass_transmission_color', 'Transmission Color', 'TransCol', False, True),
('use_pass_transmission_direct', 'Transmission Direct', 'TransDir', False, True),
('use_pass_transmission_indirect', 'Transmission Indirect', 'TransInd', False, True),
('use_pass_uv', 'UV', 'UV', True, True),
('use_pass_vector', 'Speed', 'Vector', True, True),
('use_pass_z', 'Z', 'Depth', True, True),
)
# list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
blend_types = [
('MIX', 'Mix', 'Mix Mode'),
('ADD', 'Add', 'Add Mode'),
('MULTIPLY', 'Multiply', 'Multiply Mode'),
('SUBTRACT', 'Subtract', 'Subtract Mode'),
('SCREEN', 'Screen', 'Screen Mode'),
('DIVIDE', 'Divide', 'Divide Mode'),
('DIFFERENCE', 'Difference', 'Difference Mode'),
('DARKEN', 'Darken', 'Darken Mode'),
('LIGHTEN', 'Lighten', 'Lighten Mode'),
('OVERLAY', 'Overlay', 'Overlay Mode'),
('DODGE', 'Dodge', 'Dodge Mode'),
('BURN', 'Burn', 'Burn Mode'),
('HUE', 'Hue', 'Hue Mode'),
('SATURATION', 'Saturation', 'Saturation Mode'),
('VALUE', 'Value', 'Value Mode'),
('COLOR', 'Color', 'Color Mode'),
('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'),
('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'),
]
# list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty.
operations = [
('ADD', 'Add', 'Add Mode'),
('MULTIPLY', 'Multiply', 'Multiply Mode'),
('SUBTRACT', 'Subtract', 'Subtract Mode'),
('DIVIDE', 'Divide', 'Divide Mode'),
('SINE', 'Sine', 'Sine Mode'),
('COSINE', 'Cosine', 'Cosine Mode'),
('TANGENT', 'Tangent', 'Tangent Mode'),
('ARCSINE', 'Arcsine', 'Arcsine Mode'),
('ARCCOSINE', 'Arccosine', 'Arccosine Mode'),
('ARCTANGENT', 'Arctangent', 'Arctangent Mode'),
('POWER', 'Power', 'Power Mode'),
('LOGARITHM', 'Logatithm', 'Logarithm Mode'),
('MINIMUM', 'Minimum', 'Minimum Mode'),
('MAXIMUM', 'Maximum', 'Maximum Mode'),
('ROUND', 'Round', 'Round Mode'),
('LESS_THAN', 'Less Than', 'Less Thann Mode'),
('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
]
# in BatchChangeNodes additional types/operations in a form that can be used as 'items' for EnumProperty.
navs = [
('CURRENT', 'Current', 'Leave at current state'),
('NEXT', 'Next', 'Next blend type/operation'),
('PREV', 'Prev', 'Previous blend type/operation'),
]
# list of mixing shaders
Bartek Skorupa
committed
merge_shaders_types = ('MIX', 'ADD')
# list of regular shaders. Entry: (identified, type, name for humans). Will be used in SwapShaders and menus.
# Keeping mixed case to avoid having to translate entries when adding new nodes in SwapNodes.
regular_shaders = (
('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF'),
('ShaderNodeBsdfGlossy', 'BSDF_GLOSSY', 'Glossy BSDF'),
('ShaderNodeBsdfGlass', 'BSDF_GLASS', 'Glass BSDF'),
('ShaderNodeBsdfDiffuse', 'BSDF_DIFFUSE', 'Diffuse BSDF'),
('ShaderNodeSubsurfaceScattering', 'SUBSURFACE_SCATTERING', 'Subsurface Scattering'),
('ShaderNodeEmission', 'EMISSION', 'Emission'),
('ShaderNodeBsdfVelvet', 'BSDF_VELVET', 'Velvet BSDF'),
('ShaderNodeBsdfTranslucent', 'BSDF_TRANSLUCENT', 'Translucent BSDF'),
('ShaderNodeAmbientOcclusion', 'AMBIENT_OCCLUSION', 'Ambient Occlusion'),
('ShaderNodeBackground', 'BACKGROUND', 'Background'),
('ShaderNodeBsdfRefraction', 'BSDF_REFRACTION', 'Refraction BSDF'),
('ShaderNodeBsdfAnisotropic', 'BSDF_ANISOTROPIC', 'Anisotropic BSDF'),
('ShaderNodeHoldout', 'HOLDOUT', 'Holdout'),
)
Bartek Skorupa
committed
merge_shaders = (
('ShaderNodeAddShader', 'ADD_SHADER', 'Add Shader'),
('ShaderNodeMixShader', 'MIX_SHADER', 'Mix Shader'),
)
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
def get_nodes_links(context):
space = context.space_data
tree = space.node_tree
nodes = tree.nodes
links = tree.links
active = nodes.active
context_active = context.active_node
# check if we are working on regular node tree or node group is currently edited.
# if group is edited - active node of space_tree is the group
# if context.active_node != space active node - it means that the group is being edited.
# in such case we set "nodes" to be nodes of this group, "links" to be links of this group
# if context.active_node == space.active_node it means that we are not currently editing group
is_main_tree = True
if active:
is_main_tree = context_active == active
if not is_main_tree: # if group is currently edited
tree = active.node_tree
nodes = tree.nodes
links = tree.links
return nodes, links
class NodeToolBase:
@classmethod
def poll(cls, context):
space = context.space_data
return space.type == 'NODE_EDITOR' and space.node_tree is not None
class MergeNodes(Operator, NodeToolBase):
bl_idname = "node.merge_nodes"
bl_label = "Merge Nodes"
bl_description = "Merge Selected Nodes"
bl_options = {'REGISTER', 'UNDO'}
mode = EnumProperty(
name="mode",
description="All possible blend types and math operations",
items=blend_types + [op for op in operations if op not in blend_types],
)
merge_type = EnumProperty(
name="merge type",
description="Type of Merge to be used",
items=(
('AUTO', 'Auto', 'Automatic Output Type Detection'),
('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
('MIX', 'Mix Node', 'Merge using Mix Nodes'),
('MATH', 'Math Node', 'Merge using Math Nodes'),
),
)
def execute(self, context):
tree_type = context.space_data.node_tree.type
if tree_type == 'COMPOSITING':
node_type = 'CompositorNode'
elif tree_type == 'SHADER':
node_type = 'ShaderNode'
nodes, links = get_nodes_links(context)
mode = self.mode
merge_type = self.merge_type
selected_mix = [] # entry = [index, loc]
selected_shader = [] # entry = [index, loc]
selected_math = [] # entry = [index, loc]
for i, node in enumerate(nodes):
if node.select and node.outputs:
if merge_type == 'AUTO':
for (type, types_list, dst) in (
Bartek Skorupa
committed
('SHADER', merge_shaders_types, selected_shader),
('RGBA', [t[0] for t in blend_types], selected_mix),
('VALUE', [t[0] for t in operations], selected_math),
):
output_type = node.outputs[0].type
valid_mode = mode in types_list
# When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
# Cheat that output type is 'RGBA',
# and that 'MIX' exists in math operations list.
# This way when selected_mix list is analyzed:
# Node data will be appended even though it doesn't meet requirements.
if output_type != 'SHADER' and mode == 'MIX':
output_type = 'RGBA'
valid_mode = True
if output_type == type and valid_mode:
dst.append([i, node.location.x, node.location.y])
else:
for (type, types_list, dst) in (
Bartek Skorupa
committed
('SHADER', merge_shaders_types, selected_shader),
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
('MIX', [t[0] for t in blend_types], selected_mix),
('MATH', [t[0] for t in operations], selected_math),
):
if merge_type == type and mode in types_list:
dst.append([i, node.location.x, node.location.y])
# When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
# use only 'Mix' nodes for merging.
# For that we add selected_math list to selected_mix list and clear selected_math.
if selected_mix and selected_math and merge_type == 'AUTO':
selected_mix += selected_math
selected_math = []
for nodes_list in [selected_mix, selected_shader, selected_math]:
if nodes_list:
count_before = len(nodes)
# sort list by loc_x - reversed
nodes_list.sort(key=lambda k: k[1], reverse=True)
# get maximum loc_x
loc_x = nodes_list[0][1] + 350.0
nodes_list.sort(key=lambda k: k[2], reverse=True)
loc_y = nodes_list[len(nodes_list) - 1][2]
offset_y = 40.0
if nodes_list == selected_shader:
offset_y = 150.0
the_range = len(nodes_list) - 1
do_hide = True
if len(nodes_list) == 1:
the_range = 1
do_hide = False
for i in range(the_range):
if nodes_list == selected_mix:
add_type = node_type + 'MixRGB'
add = nodes.new(add_type)
add.blend_type = mode
add.show_preview = False
add.hide = do_hide
first = 1
second = 2
add.width_hidden = 100.0
elif nodes_list == selected_math:
add_type = node_type + 'Math'
add = nodes.new(add_type)
add.operation = mode
add.hide = do_hide
first = 0
second = 1
add.width_hidden = 100.0
elif nodes_list == selected_shader:
if mode == 'MIX':
add_type = node_type + 'MixShader'
add = nodes.new(add_type)
first = 1
second = 2
add.width_hidden = 100.0
elif mode == 'ADD':
add_type = node_type + 'AddShader'
add = nodes.new(add_type)
first = 0
second = 1
add.width_hidden = 100.0
add.location = loc_x, loc_y
loc_y += offset_y
add.select = True
count_adds = i + 1
count_after = len(nodes)
index = count_after - 1
Bartek Skorupa
committed
first_selected = nodes[nodes_list[0][0]]
# "last" node has been added as first, so its index is count_before.
last_add = nodes[count_before]
# add links from last_add to all links 'to_socket' of out links of first selected.
for fs_link in first_selected.outputs[0].links:
links.new(last_add.outputs[0], fs_link.to_socket)
# add link from "first" selected and "first" add node
Bartek Skorupa
committed
links.new(first_selected.outputs[0], nodes[count_after - 1].inputs[first])
# add links between added ADD nodes and between selected and ADD nodes
for i in range(count_adds):
if i < count_adds - 1:
links.new(nodes[index - 1].inputs[first], nodes[index].outputs[0])
if len(nodes_list) > 1:
links.new(nodes[index].inputs[second], nodes[nodes_list[i + 1][0]].outputs[0])
index -= 1
# set "last" of added nodes as active
Bartek Skorupa
committed
nodes.active = last_add
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
for i, x, y in nodes_list:
nodes[i].select = False
return {'FINISHED'}
class BatchChangeNodes(Operator, NodeToolBase):
bl_idname = "node.batch_change"
bl_label = "Batch Change"
bl_description = "Batch Change Blend Type and Math Operation"
bl_options = {'REGISTER', 'UNDO'}
blend_type = EnumProperty(
name="Blend Type",
items=blend_types + navs,
)
operation = EnumProperty(
name="Operation",
items=operations + navs,
)
def execute(self, context):
nodes, links = get_nodes_links(context)
blend_type = self.blend_type
operation = self.operation
for node in context.selected_nodes:
if node.type == 'MIX_RGB':
if not blend_type in [nav[0] for nav in navs]:
node.blend_type = blend_type
else:
if blend_type == 'NEXT':
index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
#index = blend_types.index(node.blend_type)
if index == len(blend_types) - 1:
node.blend_type = blend_types[0][0]
else:
node.blend_type = blend_types[index + 1][0]
if blend_type == 'PREV':
index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
if index == 0:
node.blend_type = blend_types[len(blend_types) - 1][0]
else:
node.blend_type = blend_types[index - 1][0]
if node.type == 'MATH':
if not operation in [nav[0] for nav in navs]:
node.operation = operation
else:
if operation == 'NEXT':
index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
#index = operations.index(node.operation)
if index == len(operations) - 1:
node.operation = operations[0][0]
else:
node.operation = operations[index + 1][0]
if operation == 'PREV':
index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
#index = operations.index(node.operation)
if index == 0:
node.operation = operations[len(operations) - 1][0]
else:
node.operation = operations[index - 1][0]
return {'FINISHED'}
class ChangeMixFactor(Operator, NodeToolBase):
bl_idname = "node.factor"
bl_label = "Change Factor"
bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
bl_options = {'REGISTER', 'UNDO'}
# option: Change factor.
# If option is 1.0 or 0.0 - set to 1.0 or 0.0
# Else - change factor by option value.
option = FloatProperty()
def execute(self, context):
nodes, links = get_nodes_links(context)
option = self.option
selected = [] # entry = index
for si, node in enumerate(nodes):
if node.select:
if node.type in {'MIX_RGB', 'MIX_SHADER'}:
selected.append(si)
for si in selected:
fac = nodes[si].inputs[0]
nodes[si].hide = False
if option in {0.0, 1.0}:
fac.default_value = option
else:
fac.default_value += option
return {'FINISHED'}
class NodesCopySettings(Operator):
bl_idname = "node.copy_settings"
bl_label = "Copy Settings"
bl_description = "Copy Settings of Active Node to Selected Nodes"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
space = context.space_data
valid = False
if (space.type == 'NODE_EDITOR' and
space.node_tree is not None and
context.active_node is not None and
context.active_node.type is not 'FRAME'
):
valid = True
return valid
def execute(self, context):
nodes, links = get_nodes_links(context)
selected = [n for n in nodes if n.select]
reselect = [] # duplicated nodes will be selected after execution
active = nodes.active
if active.select:
reselect.append(active)
for node in selected:
if node.type == active.type and node != active:
# duplicate active, relink links as in 'node', append copy to 'reselect', delete node
bpy.ops.node.select_all(action='DESELECT')
nodes.active = active
active.select = True
bpy.ops.node.duplicate()
copied = nodes.active
# Copied active should however inherit some properties from 'node'
attributes = (
'hide', 'show_preview', 'mute', 'label',
'use_custom_color', 'color', 'width', 'width_hidden',
)
for attr in attributes:
setattr(copied, attr, getattr(node, attr))
# Handle scenario when 'node' is in frame. 'copied' is in same frame then.
if copied.parent:
bpy.ops.node.parent_clear()
locx = node.location.x
locy = node.location.y
# get absolute node location
parent = node.parent
while parent:
locx += parent.location.x
locy += parent.location.y
parent = parent.parent
copied.location = [locx, locy]
# reconnect links from node to copied
for i, input in enumerate(node.inputs):
if input.links:
link = input.links[0]
links.new(link.from_socket, copied.inputs[i])
for out, output in enumerate(node.outputs):
if output.links:
out_links = output.links
for link in out_links:
links.new(copied.outputs[out], link.to_socket)
bpy.ops.node.select_all(action='DESELECT')
node.select = True
bpy.ops.node.delete()
reselect.append(copied)
else: # If selected wasn't copied, need to reselect it afterwards.
reselect.append(node)
# clean up
bpy.ops.node.select_all(action='DESELECT')
for node in reselect:
node.select = True
nodes.active = active
return {'FINISHED'}
class NodesCopyLabel(Operator, NodeToolBase):
bl_idname = "node.copy_label"
bl_label = "Copy Label"
bl_options = {'REGISTER', 'UNDO'}
option = EnumProperty(
name="option",
description="Source of name of label",
items=(
('FROM_ACTIVE', 'from active', 'from active node',),
('FROM_NODE', 'from node', 'from node linked to selected node'),
('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
)
)
def execute(self, context):
nodes, links = get_nodes_links(context)
option = self.option
active = nodes.active
if option == 'FROM_ACTIVE':
if active:
src_label = active.label
for node in [n for n in nodes if n.select and nodes.active != n]:
node.label = src_label
elif option == 'FROM_NODE':
selected = [n for n in nodes if n.select]
for node in selected:
for input in node.inputs:
if input.links:
src = input.links[0].from_node
node.label = src.label
break
elif option == 'FROM_SOCKET':
selected = [n for n in nodes if n.select]
for node in selected:
for input in node.inputs:
if input.links:
src = input.links[0].from_socket
node.label = src.name
break
return {'FINISHED'}
class NodesClearLabel(Operator, NodeToolBase):
bl_idname = "node.clear_label"
bl_label = "Clear Label"
bl_options = {'REGISTER', 'UNDO'}
option = BoolProperty()
def execute(self, context):
nodes, links = get_nodes_links(context)
for node in [n for n in nodes if n.select]:
node.label = ''
return {'FINISHED'}
def invoke(self, context, event):
if self.option:
return self.execute(context)
else:
return context.window_manager.invoke_confirm(self, event)
class NodesAddTextureSetup(Operator):
bl_idname = "node.add_texture"
bl_label = "Texture Setup"
bl_description = "Add Texture Node Setup to Selected Shaders"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
space = context.space_data
valid = False
if space.type == 'NODE_EDITOR':
if space.tree_type == 'ShaderNodeTree' and space.node_tree is not None:
valid = True
return valid
def execute(self, context):
nodes, links = get_nodes_links(context)
active = nodes.active
valid = False
if active:
if active.select:
if active.type in {
'BSDF_ANISOTROPIC',
'BSDF_DIFFUSE',
'BSDF_GLOSSY',
'BSDF_GLASS',
'BSDF_REFRACTION',
'BSDF_TRANSLUCENT',
'BSDF_TRANSPARENT',
'BSDF_VELVET',
'EMISSION',
'AMBIENT_OCCLUSION',
}:
if not active.inputs[0].is_linked:
valid = True
if valid:
locx = active.location.x
locy = active.location.y
tex = nodes.new('ShaderNodeTexImage')
tex.location = [locx - 200.0, locy + 28.0]
map = nodes.new('ShaderNodeMapping')
map.location = [locx - 490.0, locy + 80.0]
coord = nodes.new('ShaderNodeTexCoord')
coord.location = [locx - 700, locy + 40.0]
active.select = False
nodes.active = tex
links.new(tex.outputs[0], active.inputs[0])
links.new(map.outputs[0], tex.inputs[0])
links.new(coord.outputs[2], map.inputs[0])
return {'FINISHED'}
class NodesAddReroutes(Operator, NodeToolBase):
bl_idname = "node.add_reroutes"
bl_label = "Add Reroutes"
bl_description = "Add Reroutes to Outputs"
bl_options = {'REGISTER', 'UNDO'}
option = EnumProperty(
name="option",
items=[
('ALL', 'to all', 'Add to all outputs'),
('LOOSE', 'to loose', 'Add only to loose outputs'),
('LINKED', 'to linked', 'Add only to linked outputs'),
]
)
def execute(self, context):
tree_type = context.space_data.node_tree.type
option = self.option
nodes, links = get_nodes_links(context)
# output valid when option is 'all' or when 'loose' output has no links
valid = False
post_select = [] # nodes to be selected after execution
# create reroutes and recreate links
for node in [n for n in nodes if n.select]:
if node.outputs:
x = node.location.x
y = node.location.y
width = node.width
# unhide 'REROUTE' nodes to avoid issues with location.y
if node.type == 'REROUTE':
node.hide = False
# When node is hidden - width_hidden not usable.
# Hack needed to calculate real width
if node.hide:
bpy.ops.node.select_all(action='DESELECT')
helper = nodes.new('NodeReroute')
helper.select = True
node.select = True
# resize node and helper to zero. Then check locations to calculate width
bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
width = 2.0 * (helper.location.x - node.location.x)
# restore node location
node.location = x, y
# delete helper
node.select = False
# only helper is selected now
bpy.ops.node.delete()
x = node.location.x + width + 20.0
if node.type != 'REROUTE':
y -= 35.0
y_offset = -22.0
loc = x, y
reroutes_count = 0 # will be used when aligning reroutes added to hidden nodes
for out_i, output in enumerate(node.outputs):
pass_used = False # initial value to be analyzed if 'R_LAYERS'
# if node is not 'R_LAYERS' - "pass_used" not needed, so set it to True
if node.type != 'R_LAYERS':
pass_used = True
else: # if 'R_LAYERS' check if output represent used render pass
node_scene = node.scene
node_layer = node.layer
# If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
if output.name == 'Alpha':
pass_used = True
else:
# check entries in global 'rl_outputs' variable
for render_pass, out_name, exr_name, in_internal, in_cycles in rl_outputs:
if output.name == out_name:
pass_used = getattr(node_scene.render.layers[node_layer], render_pass)
break
if pass_used:
valid = ((option == 'ALL') or
(option == 'LOOSE' and not output.links) or
(option == 'LINKED' and output.links))
# Add reroutes only if valid, but offset location in all cases.
if valid:
n = nodes.new('NodeReroute')
nodes.active = n
for link in output.links:
links.new(n.outputs[0], link.to_socket)
links.new(output, n.inputs[0])
n.location = loc
post_select.append(n)
reroutes_count += 1
y += y_offset
loc = x, y
# disselect the node so that after execution of script only newly created nodes are selected
node.select = False
# nicer reroutes distribution along y when node.hide
if node.hide:
y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
for reroute in [r for r in nodes if r.select]:
reroute.location.y -= y_translate
for node in post_select:
node.select = True
return {'FINISHED'}
class NodesSwap(Operator, NodeToolBase):
bl_idname = "node.swap_nodes"
bl_label = "Swap Nodes"
bl_options = {'REGISTER', 'UNDO'}
option = EnumProperty(
items=[
('CompositorNodeSwitch', 'Switch', 'Switch'),
('NodeReroute', 'Reroute', 'Reroute'),
('NodeMixRGB', 'Mix Node', 'Mix Node'),
('NodeMath', 'Math Node', 'Math Node'),
('CompositorNodeAlphaOver', 'Alpha Over', 'Alpha Over'),
('ShaderNodeBsdfTransparent', 'Transparent BSDF', 'Transparent BSDF'),
('ShaderNodeBsdfGlossy', 'Glossy BSDF', 'Glossy BSDF'),
('ShaderNodeBsdfGlass', 'Glass BSDF', 'Glass BSDF'),
('ShaderNodeBsdfDiffuse', 'Diffuse BSDF', 'Diffuse BSDF'),
('ShaderNodeSubsurfaceScattering', 'SUBSURFACE_SCATTERING', 'Subsurface Scattering'),
('ShaderNodeEmission', 'Emission', 'Emission'),
('ShaderNodeBsdfVelvet', 'Velvet BSDF', 'Velvet BSDF'),
('ShaderNodeBsdfTranslucent', 'Translucent BSDF', 'Translucent BSDF'),
('ShaderNodeAmbientOcclusion', 'Ambient Occlusion', 'Ambient Occlusion'),
('ShaderNodeBackground', 'Background', 'Background'),
('ShaderNodeBsdfRefraction', 'Refraction BSDF', 'Refraction BSDF'),
('ShaderNodeBsdfAnisotropic', 'Anisotropic BSDF', 'Anisotropic BSDF'),
('ShaderNodeHoldout', 'Holdout', 'Holdout'),
Bartek Skorupa
committed
('ShaderNodeAddShader', 'Add Shader', 'Add Shader'),
('ShaderNodeMixShader', 'Mix Shader', 'Mix Shader'),
]
)
def execute(self, context):
nodes, links = get_nodes_links(context)
tree_type = context.space_data.tree_type
if tree_type == 'CompositorNodeTree':
prefix = 'Compositor'
elif tree_type == 'ShaderNodeTree':
prefix = 'Shader'
option = self.option
selected = [n for n in nodes if n.select]
reselect = []
mode = None # will be used to set proper operation or blend type in new Math or Mix nodes.
# regular_shaders - global list. Entry: (identifier, type, name for humans)
# example: ('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF')
swap_shaders = option in (s[0] for s in regular_shaders)
Bartek Skorupa
committed
swap_merge_shaders = option in (s[0] for s in merge_shaders)
if swap_shaders or swap_merge_shaders:
# replace_types - list of node types that can be replaced using selected option
Bartek Skorupa
committed
shaders = regular_shaders + merge_shaders
replace_types = [type[1] for type in shaders]
new_type = option
elif option == 'CompositorNodeSwitch':
replace_types = ('REROUTE', 'MIX_RGB', 'MATH', 'ALPHAOVER')
new_type = option
elif option == 'NodeReroute':
replace_types = ('SWITCH')
new_type = option
elif option == 'NodeMixRGB':
replace_types = ('REROUTE', 'SWITCH', 'MATH', 'ALPHAOVER')
new_type = prefix + option
elif option == 'NodeMath':
replace_types = ('REROUTE', 'SWITCH', 'MIX_RGB', 'ALPHAOVER')
new_type = prefix + option
elif option == 'CompositorNodeAlphaOver':
replace_types = ('REROUTE', 'SWITCH', 'MATH', 'MIX_RGB')
new_type = option
for node in selected:
if node.type in replace_types:
hide = node.hide
if node.type == 'REROUTE':
hide = True
new_node = nodes.new(new_type)
# if swap Mix to Math of vice-verca - try to set blend type or operation accordingly
Bartek Skorupa
committed
if new_node.type in {'MIX_RGB', 'ALPHAOVER'}:
new_node.inputs[0].default_value = 1.0
if node.type == 'MATH':
if node.operation in [entry[0] for entry in blend_types]:
Bartek Skorupa
committed
if hasattr(new_node, 'blend_type'):
new_node.blend_type = node.operation
for i in range(2):
new_node.inputs[i+1].default_value = [node.inputs[i].default_value] * 3 + [1.0]
elif node.type in {'MIX_RGB', 'ALPHAOVER'}:
for i in range(3):
new_node.inputs[i].default_value = node.inputs[i].default_value
elif new_node.type == 'MATH':
Bartek Skorupa
committed
if node.type in {'MIX_RGB', 'ALPHAOVER'}:
if hasattr(node, 'blend_type'):
if node.blend_type in [entry[0] for entry in operations]:
new_node.operation = node.blend_type
for i in range(2):
channels = []
for c in range(3):
channels.append(node.inputs[i+1].default_value[c])
new_node.inputs[i].default_value = max(channels)
old_inputs_count = len(node.inputs)
new_inputs_count = len(new_node.inputs)
Bartek Skorupa
committed
replace = [] # entries - pairs: old input index, new input index.
if swap_shaders:
for old_i, old_input in enumerate(node.inputs):
for new_i, new_input in enumerate(new_node.inputs):
if old_input.name == new_input.name:
replace.append((old_i, new_i))
Bartek Skorupa
committed
new_input.default_value = old_input.default_value
Bartek Skorupa
committed
elif option == 'ShaderNodeAddShader':
if node.type == 'ADD_SHADER':
replace = ((0, 0), (1, 1))
elif node.type == 'MIX_SHADER':
replace = ((1, 0), (2, 1))
elif option == 'ShaderNodeMixShader':
if node.type == 'ADD_SHADER':
replace = ((0, 1), (1, 2))
elif node.type == 'MIX_SHADER':
replace = ((1, 1), (2, 2))
elif new_inputs_count == 1:
Bartek Skorupa
committed
replace = ((0, 0), )
elif new_inputs_count == 2:
if old_inputs_count == 1:
replace = ((0, 0), )
elif old_inputs_count == 2:
replace = ((0, 0), (1, 1))
elif old_inputs_count == 3:
replace = ((1, 0), (2, 1))
elif new_inputs_count == 3:
if old_inputs_count == 1:
replace = ((0, 1), )
elif old_inputs_count == 2:
replace = ((0, 1), (1, 2))
elif old_inputs_count == 3:
replace = ((0, 0), (1, 1), (2, 2))
if replace:
for old_i, new_i in replace:
if node.inputs[old_i].links:
in_link = node.inputs[old_i].links[0]
links.new(in_link.from_socket, new_node.inputs[new_i])
for out_link in node.outputs[0].links:
links.new(new_node.outputs[0], out_link.to_socket)
Bartek Skorupa
committed
for attr in {'location', 'label', 'mute', 'show_preview', 'width_hidden', 'use_clamp'}:
if hasattr(node, attr) and hasattr(new_node, attr):
setattr(new_node, attr, getattr(node, attr))
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
new_node.hide = hide
nodes.active = new_node
reselect.append(new_node)
bpy.ops.node.select_all(action="DESELECT")
node.select = True
bpy.ops.node.delete()
else:
reselect.append(node)
for node in reselect:
node.select = True
return {'FINISHED'}
class NodesLinkActiveToSelected(Operator):
bl_idname = "node.link_active_to_selected"
bl_label = "Link Active Node to Selected"
bl_options = {'REGISTER', 'UNDO'}
replace = BoolProperty()
use_node_name = BoolProperty()
use_outputs_names = BoolProperty()
@classmethod
def poll(cls, context):
space = context.space_data
valid = False
if space.type == 'NODE_EDITOR':
if space.node_tree is not None and context.active_node is not None:
if context.active_node.select:
valid = True
return valid
def execute(self, context):
nodes, links = get_nodes_links(context)
replace = self.replace
use_node_name = self.use_node_name
use_outputs_names = self.use_outputs_names
active = nodes.active
selected = [node for node in nodes if node.select and node != active]
outputs = [] # Only usable outputs of active nodes will be stored here.
for out in active.outputs:
if active.type != 'R_LAYERS':
outputs.append(out)
else:
# 'R_LAYERS' node type needs special handling.
# outputs of 'R_LAYERS' are callable even if not seen in UI.
# Only outputs that represent used passes should be taken into account
# Check if pass represented by output is used.
# global 'rl_outputs' list will be used for that
for render_pass, out_name, exr_name, in_internal, in_cycles in rl_outputs:
pass_used = False # initial value. Will be set to True if pass is used
if out.name == 'Alpha':
# Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
pass_used = True
elif out.name == out_name:
# example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
pass_used = getattr(active.scene.render.layers[active.layer], render_pass)
break
if pass_used:
outputs.append(out)
doit = True # Will be changed to False when links successfully added to previous output.
for out in outputs:
if doit:
for node in selected:
dst_name = node.name # Will be compared with src_name if needed.
# When node has label - use it as dst_name
if node.label:
dst_name = node.label
valid = True # Initial value. Will be changed to False if names don't match.
src_name = dst_name # If names not used - this asignment will keep valid = True.
if use_node_name:
# Set src_name to source node name or label
src_name = active.name
if active.label:
src_name = active.label
elif use_outputs_names:
src_name = (out.name, )
for render_pass, out_name, exr_name, in_internal, in_cycles in rl_outputs:
if out.name in {out_name, exr_name}:
src_name = (out_name, exr_name)
if dst_name not in src_name:
valid = False
if valid:
for input in node.inputs:
if input.type == out.type or node.type == 'REROUTE':
if replace or not input.is_linked:
links.new(out, input)
if not use_node_name and not use_outputs_names:
doit = False
break
return {'FINISHED'}
class AlignNodes(Operator, NodeToolBase):
bl_idname = "node.align_nodes"
bl_label = "Align nodes"
bl_options = {'REGISTER', 'UNDO'}
# option: 'Vertically', 'Horizontally'
option = EnumProperty(
name="option",
description="Direction",
items=(
('AXIS_X', "Align Vertically", 'Align Vertically'),
('AXIS_Y', "Aligh Horizontally", 'Aligh Horizontally'),
)
)
def execute(self, context):
nodes, links = get_nodes_links(context)
selected = [] # entry = [index, loc.x, loc.y, width, height]
frames_reselect = [] # entry = frame node. will be used to reselect all selected frames
active = nodes.active
for i, node in enumerate(nodes):
if node.select:
if node.type == 'FRAME':
node.select = False
frames_reselect.append(i)
else:
locx = node.location.x
locy = node.location.y
parent = node.parent
while parent is not None:
locx += parent.location.x
locy += parent.location.y
parent = parent.parent
selected.append([i, locx, locy])
count = len(selected)
# add reroute node then scale all to 0.0 and calculate widths and heights of nodes
if count > 1: # aligning makes sense only if at least 2 nodes are selected
helper = nodes.new('NodeReroute')
helper.select = True
bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
# store helper's location for further calculations
zero_x = helper.location.x
zero_y = helper.location.y
nodes.remove(helper)
# helper is deleted but its location is stored
# helper's width and height are 0.0.
# Check loc of other nodes in relation to helper to calculate their dimensions
# and append them to entries of "selected"
total_w = 0.0 # total width of all nodes. Will be calculated later.
total_h = 0.0 # total height of all nodes. Will be calculated later
for j, [i, x, y] in enumerate(selected):
locx = nodes[i].location.x
locy = nodes[i].location.y
# take node's parent (frame) into account. Get absolute location
parent = nodes[i].parent
while parent is not None:
locx += parent.location.x
locy += parent.location.y