Newer
Older
# DISPLACEMENT NODES
if sname[0] == 'Displacement':
disp_texture = nodes.new(type='ShaderNodeTexImage')
img = bpy.data.images.load(path.join(import_path, sname[2]))
disp_texture.image = img
disp_texture.label = 'Displacement'
if disp_texture.image:
disp_texture.image.colorspace_settings.is_data = True
# Add displacement offset nodes
disp_node = nodes.new(type='ShaderNodeDisplacement')
# Align the Displacement node under the active Principled BSDF node
disp_node.location = active_node.location + Vector((100, -700))
link = connect_sockets(disp_node.inputs[0], disp_texture.outputs[0])
# TODO Turn on true displacement in the material
# Too complicated for now
# Find output node
output_node = [n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial']
if output_node:
if not output_node[0].inputs[2].is_linked:
link = connect_sockets(output_node[0].inputs[2], disp_node.outputs[0])
continue
# BUMP NODES
elif sname[0] == 'Bump':
# Test if new texture node is bump map
fname_components = split_into_components(sname[2])
match_bump = set(bump_abbr).intersection(set(fname_components))
if match_bump:
# If Bump add bump node in between
bump_node_texture = nodes.new(type='ShaderNodeTexImage')
img = bpy.data.images.load(path.join(import_path, sname[2]))
img.colorspace_settings.is_data = True
bump_node_texture.image = img
bump_node_texture.label = 'Bump'
# Add bump node
bump_node = nodes.new(type='ShaderNodeBump')
link = connect_sockets(bump_node.inputs[2], bump_node_texture.outputs[0])
link = connect_sockets(active_node.inputs['Normal'], bump_node.outputs[0])
continue
# NORMAL NODES
elif sname[0] == 'Normal':
# Test if new texture node is normal map
fname_components = split_into_components(sname[2])
match_normal = set(normal_abbr).intersection(set(fname_components))
if match_normal:
# If Normal add normal node in between
normal_node_texture = nodes.new(type='ShaderNodeTexImage')
img = bpy.data.images.load(path.join(import_path, sname[2]))
img.colorspace_settings.is_data = True
normal_node_texture.image = img
normal_node_texture.label = 'Normal'
# Add normal node
normal_node = nodes.new(type='ShaderNodeNormalMap')
link = connect_sockets(normal_node.inputs[1], normal_node_texture.outputs[0])
# Connect to bump node if it was created before, otherwise to the BSDF
if bump_node is None:
link = connect_sockets(active_node.inputs[sname[0]], normal_node.outputs[0])
else:
link = connect_sockets(bump_node.inputs[sname[0]], normal_node.outputs[sname[0]])
continue
# AMBIENT OCCLUSION TEXTURE
elif sname[0] == 'Ambient Occlusion':
ao_texture = nodes.new(type='ShaderNodeTexImage')
img = bpy.data.images.load(path.join(import_path, sname[2]))
ao_texture.image = img
ao_texture.label = sname[0]
if ao_texture.image:
ao_texture.image.colorspace_settings.is_data = True
continue
if not active_node.inputs[sname[0]].is_linked:
# No texture node connected -> add texture node with new image
texture_node = nodes.new(type='ShaderNodeTexImage')
img = bpy.data.images.load(path.join(import_path, sname[2]))
texture_node.image = img
if sname[0] == 'Roughness':
# Test if glossy or roughness map
fname_components = split_into_components(sname[2])
match_rough = set(rough_abbr).intersection(set(fname_components))
match_gloss = set(gloss_abbr).intersection(set(fname_components))
if match_rough:
# If Roughness nothing to to
link = connect_sockets(active_node.inputs[sname[0]], texture_node.outputs[0])
elif match_gloss:
# If Gloss Map add invert node
invert_node = nodes.new(type='ShaderNodeInvert')
link = connect_sockets(invert_node.inputs[1], texture_node.outputs[0])
link = connect_sockets(active_node.inputs[sname[0]], invert_node.outputs[0])
roughness_node = texture_node
else:
# This is a simple connection Texture --> Input slot
link = connect_sockets(active_node.inputs[sname[0]], texture_node.outputs[0])
# Use non-color for all but 'Base Color' Textures
if not sname[0] in ['Base Color', 'Emission'] and texture_node.image:
texture_node.image.colorspace_settings.is_data = True
else:
# If already texture connected. add to node list for alignment
texture_node = active_node.inputs[sname[0]].links[0].from_node
# This are all connected texture nodes
texture_nodes.append(texture_node)
texture_node.label = sname[0]
if disp_texture:
texture_nodes.append(disp_texture)
if bump_node_texture:
texture_nodes.append(bump_node_texture)
if normal_node_texture:
texture_nodes.append(normal_node_texture)
if ao_texture:
# We want the ambient occlusion texture to be the top most texture node
texture_nodes.insert(0, ao_texture)
# Alignment
for i, texture_node in enumerate(texture_nodes):
offset = Vector((-550, (i * -280) + 200))
texture_node.location = active_node.location + offset
if normal_node:
# Extra alignment if normal node was added
normal_node.location = normal_node_texture.location + Vector((300, 0))
if bump_node:
# Extra alignment if bump node was added
bump_node.location = bump_node_texture.location + Vector((300, 0))
if roughness_node:
# Alignment of invert node if glossy map
invert_node.location = roughness_node.location + Vector((300, 0))
# Add texture input + mapping
mapping = nodes.new(type='ShaderNodeMapping')
mapping.location = active_node.location + Vector((-1050, 0))
if len(texture_nodes) > 1:
# If more than one texture add reroute node in between
reroute = nodes.new(type='NodeReroute')
texture_nodes.append(reroute)
tex_coords = Vector((texture_nodes[0].location.x,
sum(n.location.y for n in texture_nodes) / len(texture_nodes)))
reroute.location = tex_coords + Vector((-50, -120))
for texture_node in texture_nodes:
link = connect_sockets(texture_node.inputs[0], reroute.outputs[0])
link = connect_sockets(reroute.inputs[0], mapping.outputs[0])
else:
link = connect_sockets(texture_nodes[0].inputs[0], mapping.outputs[0])
# Connect texture_coordinates to mapping node
texture_input = nodes.new(type='ShaderNodeTexCoord')
texture_input.location = mapping.location + Vector((-200, 0))
link = connect_sockets(mapping.inputs[0], texture_input.outputs[2])
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
# Create frame around tex coords and mapping
frame = nodes.new(type='NodeFrame')
frame.label = 'Mapping'
mapping.parent = frame
texture_input.parent = frame
frame.update()
# Create frame around texture nodes
frame = nodes.new(type='NodeFrame')
frame.label = 'Textures'
for tnode in texture_nodes:
tnode.parent = frame
frame.update()
# Just to be sure
active_node.select = False
nodes.update()
links.update()
force_update(context)
return {'FINISHED'}
class NWAddReroutes(Operator, NWBase):
"""Add Reroute Nodes and link them to outputs of selected nodes"""
bl_idname = "node.nw_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 + node.width + 20.0
y = node.location.y
new_node_reroutes = []
# unhide 'REROUTE' nodes to avoid issues with location.y
if node.type == 'REROUTE':
node.hide = False
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):
if output.is_unavailable:
continue
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
pass_used = False # initial value to be analyzed if 'R_LAYERS'
# if node != '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 rlo in rl_outputs:
if output.name in {rlo.output_name, rlo.exr_output_name}:
pass_used = getattr(node_scene.view_layers[node_layer], rlo.render_pass)
break
if pass_used:
valid = ((option == 'ALL') or
(option == 'LOOSE' and not output.links) or
(option == 'LINKED' and output.links))
if valid:
# Add reroutes only if valid.
n = nodes.new('NodeReroute')
nodes.active = n
for link in output.links:
connect_sockets(n.outputs[0], link.to_socket)
connect_sockets(output, n.inputs[0])
n.location = loc
new_node_reroutes.append(n)
post_select.append(n)
if valid or not output.hide:
# Offset reroutes for all outputs, except hidden ones.
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 new_node_reroutes:
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
reroute.location.y -= y_translate
for node in post_select:
node.select = True
return {'FINISHED'}
class NWLinkActiveToSelected(Operator, NWBase):
"""Link active node to selected nodes basing on various criteria"""
bl_idname = "node.nw_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):
valid = False
if nw_check(context):
if 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 rlo 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 in {rlo.output_name, rlo.exr_output_name}:
# example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
pass_used = getattr(active.scene.view_layers[active.layer], rlo.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 assignment 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 rlo in rl_outputs:
if out.name in {rlo.output_name, rlo.exr_output_name}:
src_name = (rlo.output_name, rlo.exr_output_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:
connect_sockets(out, input)
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
if not use_node_name and not use_outputs_names:
doit = False
break
return {'FINISHED'}
class NWAlignNodes(Operator, NWBase):
'''Align the selected nodes neatly in a row/column'''
bl_idname = "node.nw_align_nodes"
bl_label = "Align Nodes"
bl_options = {'REGISTER', 'UNDO'}
margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes')
def execute(self, context):
nodes, links = get_nodes_links(context)
margin = self.margin
selection = []
for node in nodes:
if node.select and node.type != 'FRAME':
selection.append(node)
# If no nodes are selected, align all nodes
active_loc = None
if not selection:
selection = nodes
elif nodes.active in selection:
active_loc = copy(nodes.active.location) # make a copy, not a reference
# Check if nodes should be laid out horizontally or vertically
# use dimension to get center of node, not corner
x_locs = [n.location.x + (n.dimensions.x / 2) for n in selection]
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
y_locs = [n.location.y - (n.dimensions.y / 2) for n in selection]
x_range = max(x_locs) - min(x_locs)
y_range = max(y_locs) - min(y_locs)
mid_x = (max(x_locs) + min(x_locs)) / 2
mid_y = (max(y_locs) + min(y_locs)) / 2
horizontal = x_range > y_range
# Sort selection by location of node mid-point
if horizontal:
selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2))
else:
selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
# Alignment
current_pos = 0
for node in selection:
current_margin = margin
current_margin = current_margin * 0.5 if node.hide else current_margin # use a smaller margin for hidden nodes
if horizontal:
node.location.x = current_pos
current_pos += current_margin + node.dimensions.x
node.location.y = mid_y + (node.dimensions.y / 2)
else:
node.location.y = current_pos
current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment
node.location.x = mid_x - (node.dimensions.x / 2)
# If active node is selected, center nodes around it
if active_loc is not None:
active_loc_diff = active_loc - nodes.active.location
for node in selection:
node.location += active_loc_diff
else: # Position nodes centered around where they used to be
locs = ([n.location.x + (n.dimensions.x / 2) for n in selection]
) if horizontal else ([n.location.y - (n.dimensions.y / 2) for n in selection])
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
new_mid = (max(locs) + min(locs)) / 2
for node in selection:
if horizontal:
node.location.x += (mid_x - new_mid)
else:
node.location.y += (mid_y - new_mid)
return {'FINISHED'}
class NWSelectParentChildren(Operator, NWBase):
bl_idname = "node.nw_select_parent_child"
bl_label = "Select Parent or Children"
bl_options = {'REGISTER', 'UNDO'}
option: EnumProperty(
name="option",
items=(
('PARENT', 'Select Parent', 'Select Parent Frame'),
('CHILD', 'Select Children', 'Select members of selected frame'),
)
)
def execute(self, context):
nodes, links = get_nodes_links(context)
option = self.option
selected = [node for node in nodes if node.select]
if option == 'PARENT':
for sel in selected:
parent = sel.parent
if parent:
parent.select = True
else: # option == 'CHILD'
for sel in selected:
children = [node for node in nodes if node.parent == sel]
for kid in children:
kid.select = True
return {'FINISHED'}
class NWDetachOutputs(Operator, NWBase):
"""Detach outputs of selected node leaving inputs linked"""
bl_idname = "node.nw_detach_outputs"
bl_label = "Detach Outputs"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
nodes, links = get_nodes_links(context)
selected = context.selected_nodes
bpy.ops.node.duplicate_move_keep_inputs()
new_nodes = context.selected_nodes
bpy.ops.node.select_all(action="DESELECT")
for node in selected:
node.select = True
bpy.ops.node.delete_reconnect()
for new_node in new_nodes:
new_node.select = True
bpy.ops.transform.translate('INVOKE_DEFAULT')
return {'FINISHED'}
class NWLinkToOutputNode(Operator):
"""Link to Composite node or Material Output node"""
bl_idname = "node.nw_link_out"
bl_label = "Connect to Output"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
valid = False
if nw_check(context):
if context.active_node is not None:
for out in context.active_node.outputs:
if is_visible_socket(out):
valid = True
break
return valid
def execute(self, context):
nodes, links = get_nodes_links(context)
active = nodes.active
output_index = None
tree_type = context.space_data.tree_type
shader_outputs = {'OBJECT': 'ShaderNodeOutputMaterial',
'WORLD': 'ShaderNodeOutputWorld',
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
'LINESTYLE': 'ShaderNodeOutputLineStyle'}
output_type = {
'ShaderNodeTree': shader_outputs[context.space_data.shader_type],
'CompositorNodeTree': 'CompositorNodeComposite',
'TextureNodeTree': 'TextureNodeOutput',
'GeometryNodeTree': 'NodeGroupOutput',
}[tree_type]
for node in nodes:
# check whether the node is an output node and,
# if supported, whether it's the active one
if node.rna_type.identifier == output_type \
and (node.is_active_output if hasattr(node, 'is_active_output')
else True):
output_node = node
break
else: # No output node exists
bpy.ops.node.select_all(action="DESELECT")
output_node = nodes.new(output_type)
output_node.location.x = active.location.x + active.dimensions.x + 80
output_node.location.y = active.location.y
if active.outputs:
for i, output in enumerate(active.outputs):
if is_visible_socket(output):
output_index = i
break
for i, output in enumerate(active.outputs):
if output.type == output_node.inputs[0].type and is_visible_socket(output):
output_index = i
break
out_input_index = 0
if tree_type == 'ShaderNodeTree':
if active.outputs[output_index].name == 'Volume':
out_input_index = 1
elif active.outputs[output_index].name == 'Displacement':
out_input_index = 2
elif tree_type == 'GeometryNodeTree':
if active.outputs[output_index].type != 'GEOMETRY':
return {'CANCELLED'}
connect_sockets(active.outputs[output_index], output_node.inputs[out_input_index])
force_update(context) # viewport render does not update
return {'FINISHED'}
class NWMakeLink(Operator, NWBase):
"""Make a link from one socket to another"""
bl_idname = 'node.nw_make_link'
bl_label = 'Make Link'
bl_options = {'REGISTER', 'UNDO'}
from_socket: IntProperty()
to_socket: IntProperty()
def execute(self, context):
nodes, links = get_nodes_links(context)
n1 = nodes[context.scene.NWLazySource]
n2 = nodes[context.scene.NWLazyTarget]
connect_sockets(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
force_update(context)
return {'FINISHED'}
class NWCallInputsMenu(Operator, NWBase):
"""Link from this output"""
bl_idname = 'node.nw_call_inputs_menu'
bl_label = 'Make Link'
bl_options = {'REGISTER', 'UNDO'}
from_socket: IntProperty()
def execute(self, context):
nodes, links = get_nodes_links(context)
context.scene.NWSourceSocket = self.from_socket
n1 = nodes[context.scene.NWLazySource]
n2 = nodes[context.scene.NWLazyTarget]
if len(n2.inputs) > 1:
bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
elif len(n2.inputs) == 1:
connect_sockets(n1.outputs[self.from_socket], n2.inputs[0])
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
return {'FINISHED'}
class NWAddSequence(Operator, NWBase, ImportHelper):
"""Add an Image Sequence"""
bl_idname = 'node.nw_add_sequence'
bl_label = 'Import Image Sequence'
bl_options = {'REGISTER', 'UNDO'}
directory: StringProperty(
subtype="DIR_PATH"
)
filename: StringProperty(
subtype="FILE_NAME"
)
files: CollectionProperty(
type=bpy.types.OperatorFileListElement,
options={'HIDDEN', 'SKIP_SAVE'}
)
relative_path: BoolProperty(
name='Relative Path',
description='Set the file path relative to the blend file, when possible',
default=True
)
def draw(self, context):
layout = self.layout
layout.alignment = 'LEFT'
layout.prop(self, 'relative_path')
def execute(self, context):
nodes, links = get_nodes_links(context)
directory = self.directory
filename = self.filename
files = self.files
tree = context.space_data.node_tree
# DEBUG
# print ("\nDIR:", directory)
# print ("FN:", filename)
# print ("Fs:", list(f.name for f in files), '\n')
if tree.type == 'SHADER':
node_type = "ShaderNodeTexImage"
elif tree.type == 'COMPOSITING':
node_type = "CompositorNodeImage"
else:
self.report({'ERROR'}, "Unsupported Node Tree type!")
return {'CANCELLED'}
if not files[0].name and not filename:
self.report({'ERROR'}, "No file chosen")
return {'CANCELLED'}
elif files[0].name and (not filename or not path.exists(directory + filename)):
# User has selected multiple files without an active one, or the active one is non-existent
filename = files[0].name
if not path.exists(directory + filename):
self.report({'ERROR'}, filename + " does not exist!")
return {'CANCELLED'}
without_ext = '.'.join(filename.split('.')[:-1])
# if last digit isn't a number, it's not a sequence
if not without_ext[-1].isdigit():
self.report({'ERROR'}, filename + " does not seem to be part of a sequence")
return {'CANCELLED'}
extension = filename.split('.')[-1]
reverse = without_ext[::-1] # reverse string
count_numbers = 0
for char in reverse:
if char.isdigit():
count_numbers += 1
else:
break
without_num = without_ext[:count_numbers * -1]
files = sorted(glob(directory + without_num + "[0-9]" * count_numbers + "." + extension))
num_frames = len(files)
nodes_list = [node for node in nodes]
if nodes_list:
nodes_list.sort(key=lambda k: k.location.x)
xloc = nodes_list[0].location.x - 220 # place new nodes at far left
yloc = 0
for node in nodes:
node.select = False
yloc += node_mid_pt(node, 'y')
else:
xloc = 0
yloc = 0
name_with_hashes = without_num + "#" * count_numbers + '.' + extension
bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type=node_type)
node = nodes.active
node.label = name_with_hashes
filepath = directory + (without_ext + '.' + extension)
if self.relative_path:
if bpy.data.filepath:
try:
filepath = bpy.path.relpath(filepath)
except ValueError:
pass
img = bpy.data.images.load(filepath)
img.source = 'SEQUENCE'
img.name = name_with_hashes
node.image = img
image_user = node.image_user if tree.type == 'SHADER' else node
# separate the number from the file name of the first file
image_user.frame_offset = int(files[0][len(without_num) + len(directory):-1 * (len(extension) + 1)]) - 1
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
image_user.frame_duration = num_frames
return {'FINISHED'}
class NWAddMultipleImages(Operator, NWBase, ImportHelper):
"""Add multiple images at once"""
bl_idname = 'node.nw_add_multiple_images'
bl_label = 'Open Selected Images'
bl_options = {'REGISTER', 'UNDO'}
directory: StringProperty(
subtype="DIR_PATH"
)
files: CollectionProperty(
type=bpy.types.OperatorFileListElement,
options={'HIDDEN', 'SKIP_SAVE'}
)
def execute(self, context):
nodes, links = get_nodes_links(context)
xloc, yloc = context.region.view2d.region_to_view(context.area.width / 2, context.area.height / 2)
if context.space_data.node_tree.type == 'SHADER':
node_type = "ShaderNodeTexImage"
elif context.space_data.node_tree.type == 'COMPOSITING':
node_type = "CompositorNodeImage"
else:
self.report({'ERROR'}, "Unsupported Node Tree type!")
return {'CANCELLED'}
new_nodes = []
for f in self.files:
fname = f.name
node = nodes.new(node_type)
new_nodes.append(node)
node.label = fname
node.hide = True
node.width_hidden = 100
node.location.x = xloc
node.location.y = yloc
yloc -= 40
img = bpy.data.images.load(self.directory + fname)
node.image = img
# shift new nodes up to center of tree
list_size = new_nodes[0].location.y - new_nodes[-1].location.y
for node in nodes:
if node in new_nodes:
node.select = True
node.location.y += (list_size / 2)
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
else:
node.select = False
return {'FINISHED'}
class NWViewerFocus(bpy.types.Operator):
"""Set the viewer tile center to the mouse position"""
bl_idname = "node.nw_viewer_focus"
bl_label = "Viewer Focus"
x: bpy.props.IntProperty()
y: bpy.props.IntProperty()
@classmethod
def poll(cls, context):
return nw_check(context) and context.space_data.tree_type == 'CompositorNodeTree'
def execute(self, context):
return {'FINISHED'}
def invoke(self, context, event):
render = context.scene.render
space = context.space_data
percent = render.resolution_percentage * 0.01
nodes, links = get_nodes_links(context)
viewers = [n for n in nodes if n.type == 'VIEWER']
if viewers:
mlocx = event.mouse_region_x
mlocy = event.mouse_region_y
select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
if 'FINISHED' not in select_node: # only run if we're not clicking on a node
region_x = context.region.width
region_y = context.region.height
region_center_x = context.region.width / 2
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
region_center_y = context.region.height / 2
bd_x = render.resolution_x * percent * space.backdrop_zoom
bd_y = render.resolution_y * percent * space.backdrop_zoom
backdrop_center_x = (bd_x / 2) - space.backdrop_offset[0]
backdrop_center_y = (bd_y / 2) - space.backdrop_offset[1]
margin_x = region_center_x - backdrop_center_x
margin_y = region_center_y - backdrop_center_y
abs_mouse_x = (mlocx - margin_x) / bd_x
abs_mouse_y = (mlocy - margin_y) / bd_y
for node in viewers:
node.center_x = abs_mouse_x
node.center_y = abs_mouse_y
else:
return {'PASS_THROUGH'}
return self.execute(context)
class NWSaveViewer(bpy.types.Operator, ExportHelper):
"""Save the current viewer node to an image file"""
bl_idname = "node.nw_save_viewer"
bl_label = "Save This Image"
filepath: StringProperty(subtype="FILE_PATH")
filename_ext: EnumProperty(
name="Format",
description="Choose the file format to save to",
items=(('.bmp', "BMP", ""),
('.rgb', 'IRIS', ""),
('.png', 'PNG', ""),
('.jpg', 'JPEG', ""),
('.jp2', 'JPEG2000', ""),
('.tga', 'TARGA', ""),
('.cin', 'CINEON', ""),
('.dpx', 'DPX', ""),
('.exr', 'OPEN_EXR', ""),
('.hdr', 'HDR', ""),
('.tif', 'TIFF', "")),
default='.png',
)
@classmethod
def poll(cls, context):
valid = False
if nw_check(context):
if context.space_data.tree_type == 'CompositorNodeTree':
if "Viewer Node" in [i.name for i in bpy.data.images]:
if sum(bpy.data.images["Viewer Node"].size) > 0: # False if not connected or connected but no image
valid = True
return valid
def execute(self, context):
fp = self.filepath
if fp:
formats = {
'.bmp': 'BMP',
'.rgb': 'IRIS',
'.png': 'PNG',
'.jpg': 'JPEG',
'.jpeg': 'JPEG',
'.jp2': 'JPEG2000',
'.tga': 'TARGA',
'.cin': 'CINEON',
'.dpx': 'DPX',
'.exr': 'OPEN_EXR',
'.hdr': 'HDR',
'.tiff': 'TIFF',
'.tif': 'TIFF'}
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
basename, ext = path.splitext(fp)
old_render_format = context.scene.render.image_settings.file_format
context.scene.render.image_settings.file_format = formats[self.filename_ext]
context.area.type = "IMAGE_EDITOR"
context.area.spaces[0].image = bpy.data.images['Viewer Node']
context.area.spaces[0].image.save_render(fp)
context.area.type = "NODE_EDITOR"
context.scene.render.image_settings.file_format = old_render_format
return {'FINISHED'}
class NWResetNodes(bpy.types.Operator):
"""Reset Nodes in Selection"""
bl_idname = "node.nw_reset_nodes"
bl_label = "Reset Nodes"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
space = context.space_data
return space.type == 'NODE_EDITOR'
def execute(self, context):
node_active = context.active_node
node_selected = context.selected_nodes
Soeren Schmidt-Clausen
committed
node_ignore = ["FRAME", "REROUTE", "GROUP", "SIMULATION_INPUT", "SIMULATION_OUTPUT"]
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
# Check if one node is selected at least
if not (len(node_selected) > 0):
self.report({'ERROR'}, "1 node must be selected at least")
return {'CANCELLED'}
active_node_name = node_active.name if node_active.select else None
valid_nodes = [n for n in node_selected if n.type not in node_ignore]
# Create output lists
selected_node_names = [n.name for n in node_selected]
success_names = []
# Reset all valid children in a frame
node_active_is_frame = False
if len(node_selected) == 1 and node_active.type == "FRAME":
node_tree = node_active.id_data
children = [n for n in node_tree.nodes if n.parent == node_active]
if children:
valid_nodes = [n for n in children if n.type not in node_ignore]
selected_node_names = [n.name for n in children if n.type not in node_ignore]
node_active_is_frame = True
# Check if valid nodes in selection
if not (len(valid_nodes) > 0):
# Check for frames only
frames_selected = [n for n in node_selected if n.type == "FRAME"]
if (len(frames_selected) > 1 and len(frames_selected) == len(node_selected)):
self.report({'ERROR'}, "Please select only 1 frame to reset")
else:
self.report({'ERROR'}, "No valid node(s) in selection")
return {'CANCELLED'}
# Report nodes that are not valid
if len(valid_nodes) != len(node_selected) and node_active_is_frame is False:
valid_node_names = [n.name for n in valid_nodes]
not_valid_names = list(set(selected_node_names) - set(valid_node_names))
self.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names)))
# Deselect all nodes
for i in node_selected:
i.select = False
# Run through all valid nodes
for node in valid_nodes:
parent = node.parent if node.parent else None
node_loc = [node.location.x, node.location.y]
node_tree = node.id_data
props_to_copy = 'bl_idname name location height width'.split(' ')
reconnections = []
mappings = chain.from_iterable([node.inputs, node.outputs])
for i in (i for i in mappings if i.is_linked):
for L in i.links:
reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
props = {j: getattr(node, j) for j in props_to_copy}
new_node = node_tree.nodes.new(props['bl_idname'])
props_to_copy.pop(0)
for prop in props_to_copy:
setattr(new_node, prop, props[prop])
nodes = node_tree.nodes
nodes.remove(node)
new_node.name = props['name']
if parent:
new_node.parent = parent
new_node.location = node_loc
for str_from, str_to in reconnections:
connect_sockets(eval(str_from), eval(str_to))
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
new_node.select = False
success_names.append(new_node.name)
# Reselect all nodes
if selected_node_names and node_active_is_frame is False:
for i in selected_node_names:
node_tree.nodes[i].select = True
if active_node_name is not None:
node_tree.nodes[active_node_name].select = True
node_tree.nodes.active = node_tree.nodes[active_node_name]
self.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names)))
return {'FINISHED'}
classes = (
NWLazyMix,
NWLazyConnect,
NWDeleteUnused,