You are on page 1of 48

'''

Copyright (C) 2019


rudy.michau@gmail.com

Created by RUDY MICHAU

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 3 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, see <http://www.gnu.org/licenses/>.
'''

import bpy
import os
from os.path import join, dirname, realpath
import bpy.utils.previews
from bpy_extras import view3d_utils
from bpy.props import IntProperty, FloatProperty, BoolProperty, StringProperty,
EnumProperty

from bpy.types import WindowManager, Operator, Menu, AddonPreferences

from bpy_extras.io_utils import ImportHelper

import rna_keymap_ui

import mathutils
from mathutils import Matrix, Vector, Euler

bl_info = {
"name": "Fluent : Materializer",
"description": "程序材质工具套装.",
"author": "Rudy MICHAU CG 资源网-CGZY.NET",
"version": (1, 1, 1),
"blender": (3, 2, 0),
"location": "Node Editor Toolbar",
"wiki_url": "",
"category": "Shader"}

temp = None

viewport_navigation_events = [
'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE',
'TRACKPADPAN', 'TRACKPADZOOM', 'MOUSEROTATE', 'MOUSESMARTZOOM',
'NDOF_MOTION'
]

viewport_navigation_keys = ['NUMPAD_1', 'NUMPAD_2', 'NUMPAD_3', 'NUMPAD_5',


'NUMPAD_7']

# Chargement des icones


fluent_icons_collection = {}
fluent_icons_loaded = False

icons_dir = join(dirname(realpath(__file__)))
image_size = (32, 32)

def load_icons():
global fluent_icons_collection
global fluent_icons_loaded

if fluent_icons_loaded: return fluent_icons_collection["main"]

custom_icons = bpy.utils.previews.new()

icons_dir = join(dirname(realpath(__file__)),'icons')

custom_icons.load("multiply", join(icons_dir, "multiply.png"), 'IMAGE')


custom_icons.load("difference", join(icons_dir, "difference.png"), 'IMAGE')

fluent_icons_collection["main"] = custom_icons
fluent_icons_loaded = True

return fluent_icons_collection["main"]

def clear_icons():
global fluent_icons_loaded
for icon in fluent_icons_collection.values():
bpy.utils.previews.remove(icon)
fluent_icons_collection.clear()
fluent_icons_loaded = False

main_dir = join(dirname(realpath(__file__)))
blender_dir = join(main_dir,'Blender_Files')
blender_file = join(blender_dir,'Material_Studying.blend')
file_path_node_tree = join(blender_file,'NodeTree')
file_path_shader = join(blender_file,'Material')

preview_collections = {}
preview_dir = join(dirname(realpath(__file__)),'thumbnails')
imperfections_dir = join(preview_dir,'imperfections')
grunges_dir = join(preview_dir,'grunges')
patterns_dir = join(preview_dir,'patterns')
liquid_dir = join(preview_dir,'liquid')
city_dir = join(preview_dir,'city')
normals_dir = join(preview_dir,'normals')
fabric_dir = join(preview_dir,'fabric')
metal_dir = join(preview_dir,'metal')
shaders_dir = join(preview_dir,'shaders')
screen_dir = join(preview_dir,'screen')
wood_dir = join(preview_dir,'wood')

pcoll = bpy.utils.previews.new()
pcoll.my_previews_dir = imperfections_dir
pcoll.my_previews = ()
preview_collections["imperfections"] = pcoll

pcoll = bpy.utils.previews.new()
pcoll.my_previews_dir = grunges_dir
pcoll.my_previews = ()
preview_collections["grunges"] = pcoll

pcoll = bpy.utils.previews.new()
pcoll.my_previews_dir = patterns_dir
pcoll.my_previews = ()
preview_collections["patterns"] = pcoll

pcoll = bpy.utils.previews.new()
pcoll.my_previews_dir = liquid_dir
pcoll.my_previews = ()
preview_collections["liquid"] = pcoll

pcoll = bpy.utils.previews.new()
pcoll.my_previews_dir = city_dir
pcoll.my_previews = ()
preview_collections["city"] = pcoll

pcoll = bpy.utils.previews.new()
pcoll.my_previews_dir = normals_dir
pcoll.my_previews = ()
preview_collections["normals"] = pcoll

pcoll = bpy.utils.previews.new()
pcoll.my_previews_dir = fabric_dir
pcoll.my_previews = ()
preview_collections["fabric"] = pcoll

pcoll = bpy.utils.previews.new()
pcoll.my_previews_dir = metal_dir
pcoll.my_previews = ()
preview_collections["metal"] = pcoll

pcoll = bpy.utils.previews.new()
pcoll.my_previews_dir = shaders_dir
pcoll.my_previews = ()
preview_collections["shaders"] = pcoll

pcoll = bpy.utils.previews.new()
pcoll.my_previews_dir = screen_dir
pcoll.my_previews = ()
preview_collections["screen"] = pcoll

pcoll = bpy.utils.previews.new()
pcoll.my_previews_dir = wood_dir
pcoll.my_previews = ()
preview_collections["wood"] = pcoll

def make_oops(msg):
global CR
def oops(self, context):
for m in msg:
self.layout.label(text=m)
return oops

def get_addon_preferences():
addon_key = __package__.split(".")[0]
addon_prefs = bpy.context.preferences.addons[addon_key].preferences
return addon_prefs

def active_object(obj = None, action = 'GET', solo = False):


if solo:
bpy.ops.object.select_all(action="DESELECT")
if action == 'SET':
if obj != None:
obj.hide_set(False)
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
return obj
elif action == 'GET':
return bpy.context.active_object

def select_objects(objs = [], action = 'GET'):


if action == 'SET':
for obj in objs :
obj.hide_set(False)
obj.select_set(True)
return True
elif action == 'GET':
return bpy.context.selected_objects

def deselect_objects(objs, not_actived = True):


active_obj = bpy.context.active_object

for obj in objs :


if not_actived and obj == active_obj:
continue

obj.hide_set(False)
obj.select_set(False)
return True

def link(material, from_node, from_slot_name, to_node, to_slot_name):


input = to_node.inputs[to_slot_name]
output = from_node.outputs[from_slot_name]
material.node_tree.links.new(input, output)

def get_material():
for o in bpy.context.scene.objects:
print(o.name)
return bpy.context.scene.objects

def get_node_tree(material):
return material.node_tree

def get_nodes(material):
return get_node_tree(material).nodes
def get_selected_nodes(nodes):
selected_nodes = []
for n in nodes:
if n.select:
selected_nodes.append(n)
return selected_nodes

def add_node_group(group_name):
nodes = get_nodes()
if not bpy.data.node_groups.get(group_name):
bpy.ops.wm.append(filename=group_name, directory=file_path_node_tree)
if bpy.data.node_groups.get(group_name):
new_group = nodes.new(type='ShaderNodeGroup')
new_group.node_tree = bpy.data.node_groups.get(group_name)
new_group.name = group_name
return new_group

def enum_previews_from_directory_items(pcoll):
global preview_collections
enum_items = []

if bpy.context is None:
return enum_items

directory = pcoll.my_previews_dir

if directory and os.path.exists(directory):


# Scan the directory for png files
image_paths = []
for fn in os.listdir(directory):
if fn.lower().endswith(".jpg"):
image_paths.append(fn)

for i, name in enumerate(image_paths):


# generates a thumbnail preview for a file.
filepath = os.path.join(directory, name)
icon = pcoll.get(name)
if not icon:
thumb = pcoll.load(name, filepath, 'IMAGE')
else:
thumb = pcoll[name]
enum_items.append((name.split('.jpg')[0], name.split('.jpg')[0], "",
thumb.icon_id, i))

pcoll.my_previews = enum_items
pcoll.my_previews_dir = directory
return pcoll.my_previews

def import_node_group(group_name):
material = bpy.context.active_object.active_material
tree = material.node_tree
nodes = tree.nodes
if not bpy.data.node_groups.get(group_name):
bpy.ops.wm.append(filename=group_name, directory=file_path_node_tree)
if bpy.data.node_groups.get(group_name):
new_group = nodes.new(type='ShaderNodeGroup')
new_group.node_tree = bpy.data.node_groups.get(group_name)
new_group.name = group_name
return new_group

def deselect_nodes(nodes):
for node in nodes:
node.select = False

def from_what(node, input_name):


a_node = node.inputs[input_name].links[0].from_node
a_socket = node.inputs[input_name].links[0].from_socket.name
return {'node':a_node, 'socket':a_socket}

def to_what(node, output_name):


node_tab = []
socket_tab = []
for l in node.outputs[output_name].links:
node_tab.append(l.to_node)
socket_tab.append(l.to_socket.name)
return {'node':node_tab, 'socket':socket_tab}

def click_on(mouse_x, mouse_y, ignore=False, search=['MESH']):


best_length_squared = -1.0
best_obj = None

visible_list = bpy.context.visible_objects
depsgraph = bpy.context.evaluated_depsgraph_get()

for obj in visible_list:


if obj.type in search and not obj.hide_get() and obj.display_type not in
['BOUNDS', 'WIRE'] and obj != ignore:
try:
eval_obj = obj.evaluated_get(depsgraph)
result = obj_ray_cast(eval_obj, mouse_x, mouse_y)
except:
result = {'success': False}

if result['success']:
length_squared = (obj.matrix_world @ result['hit'] -
result.get('ray_origin')).length_squared
if best_obj is None or length_squared < best_length_squared:
best_length_squared = length_squared
best_obj = obj

return best_obj

def obj_ray_cast(obj, mouse_x, mouse_y):


# get the context arguments
scene = bpy.context.scene
region = bpy.context.region
rv3d = bpy.context.region_data
coord = (mouse_x, mouse_y)

# get the ray from the viewport and mouse


view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord)
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord)

ray_target = ray_origin + view_vector

matrix = obj.matrix_world.copy()
# get the ray relative to the object
matrix_inv = matrix.inverted()
ray_origin_obj = matrix_inv @ ray_origin
ray_target_obj = matrix_inv @ ray_target
ray_direction_obj = ray_target_obj - ray_origin_obj

# cast the ray


success, location, normal, face_index = obj.ray_cast(ray_origin_obj,
ray_direction_obj)

return {
'success': success,
'hit': location,
'normal': normal,
'face_index': face_index,
'ray_origin': ray_origin,
'obj': obj
}

def cast_local2global(dico):
if dico['success']:
obj = dico['obj']
matrix = obj.matrix_world
converted = {
'success': dico['success'],
'hit': matrix @ dico['hit'],
'normal': matrix.to_3x3() @ dico['normal'],
'face_index': dico['face_index'],
'ray_origin': dico['ray_origin'],
'obj': dico['obj']
}
return converted
else:
print('Cast fail, impossible to convert.')

def pass_through(event):
return (event.type in viewport_navigation_events) \
or (event.type in viewport_navigation_keys and not event.shift) \
or (event.alt and event.type == 'LEFTMOUSE') \
or (event.alt and event.type == 'RIGHTMOUSE')

def make_single_user_node(node):
original = node.node_tree
single = original.copy()
node.node_tree = single
return single

class FLUENT_SHADER_OT_addNodes(Operator):
bl_idname = 'fluent.add_nodes'
bl_description = '添加节点设置'
bl_category = 'Node'
bl_label = '添加设置'

choice: bpy.props.StringProperty()

def invoke(self, context, event):


global temp
bpy.ops.object.mode_set(mode='OBJECT')
if event.ctrl and event.shift and event.alt:
bpy.context.window_manager.popup_menu(make_oops(['Add a node in the
material.',
'If nothing is selected, the new node is placed at the center of the
node tree.',
'If a node is selected, the new node is placed next to this one.']),
title="Node addition", icon='INFO')
return {'FINISHED'}
global file_path_node_tree
group_name = None
new_group = None
previous_node = None

self.grab_at_the_end = True

try:
material = bpy.context.active_object.active_material
tree = material.node_tree
nodes = tree.nodes
selected_nodes = bpy.context.selected_nodes
except:
obj = bpy.context.active_object
mat = bpy.data.materials.new(name="F_Material")
mat.use_nodes = True
if obj.data.materials:
obj.data.materials[0] = mat
else:
# no slots
obj.data.materials.append(mat)
material = bpy.context.active_object.active_material
tree = material.node_tree
nodes = tree.nodes
selected_nodes = []
for node in nodes:
if node.type == 'BSDF_PRINCIPLED':
node.select = True
nodes.active = node
selected_nodes.append(node)
else:
node.select = False

try:
if nodes.active and nodes.active.select:
previous_node = nodes.active
else:
previous_node = None
# previous_node = bpy.context.selected_nodes[0]
except:
previous_node = None
if self.choice == 'imperfections':
group_name = bpy.context.scene.FluentShaderProps.f_imperfections
elif self.choice == 'grunges':
group_name = bpy.context.scene.FluentShaderProps.f_grunges
elif self.choice == 'patterns':
group_name = bpy.context.scene.FluentShaderProps.f_patterns
elif self.choice == 'liquid':
group_name = bpy.context.scene.FluentShaderProps.f_liquid
elif self.choice == 'city':
group_name = bpy.context.scene.FluentShaderProps.f_city
elif self.choice == 'normals':
group_name = bpy.context.scene.FluentShaderProps.f_normals
elif self.choice == 'fabric':
group_name = bpy.context.scene.FluentShaderProps.f_fabric
elif self.choice == 'metal':
group_name = bpy.context.scene.FluentShaderProps.f_metal
elif self.choice == 'shaders':
group_name = bpy.context.scene.FluentShaderProps.f_shaders
elif self.choice == 'wood':
group_name = bpy.context.scene.FluentShaderProps.f_wood
elif self.choice == 'screen':
group_name = bpy.context.scene.FluentShaderProps.f_screen
elif self.choice in ['Smart Shader 2 layers', 'Smart Shader 3 layers']:
try:
if previous_node.type == 'BSDF_PRINCIPLED':
group_name = '混合图层'
else:
bpy.context.window_manager.popup_menu(make_oops(['Select a
Principled BSDF before to call this function.']), title="How to use ?",
icon='INFO')
return{'FINISHED'}
except:
bpy.context.window_manager.popup_menu(make_oops(['Select a
Principled BSDF before to call this function.']), title="How to use ?",
icon='INFO')
return{'FINISHED'}

if group_name == None:
group_name = self.choice

new_group = import_node_group(group_name)

if previous_node:
new_group.location = previous_node.location
new_group.location[0] -= previous_node.dimensions[0] *
(1/context.preferences.system.ui_scale) + 50

if self.choice == 'Mix Layers':


if previous_node and previous_node.type == 'BSDF_PRINCIPLED':
deselect_nodes(nodes)
research_previous = None
try:
# recherche si un mix est déja connecté
color_socket = previous_node.inputs['Base
Color'].links[0].from_socket.name
color_node = previous_node.inputs['Base
Color'].links[0].from_node
metallic_socket =
previous_node.inputs['Metallic'].links[0].from_socket.name
metallic_node =
previous_node.inputs['Metallic'].links[0].from_node
roughness_socket =
previous_node.inputs['Roughness'].links[0].from_socket.name
roughness_node =
previous_node.inputs['Roughness'].links[0].from_node
normal_socket =
previous_node.inputs['Normal'].links[0].from_socket.name
normal_node = previous_node.inputs['Normal'].links[0].from_node
try:
emission_s_socket = previous_node.inputs['Emission
Strength'].links[0].from_socket.name
emission_s_node = previous_node.inputs['Emission
Strength'].links[0].from_node
emission_c_socket = previous_node.inputs['Emission
Color'].links[0].from_socket.name
emission_c_node = previous_node.inputs['Emission
Color'].links[0].from_node
except:pass
research_previous = 'SUCCESS'
except:
research_previous = 'FAIL'
pass
if research_previous == 'SUCCESS' and 'Mix Layers' in
color_node.name and 'Mix Layers' in metallic_node.name and 'Mix Layers' in
roughness_node.name and 'Mix Layers' in normal_node.name:
# connecte le nouveaux mix au BSDF
link(material, new_group, 'Color', previous_node, 'Base Color')
link(material, new_group, 'Metallic', previous_node,
'Metallic')
link(material, new_group, 'Roughness', previous_node,
'Roughness')
link(material, new_group, 'Normal', previous_node, 'Normal')
try:
link(material, new_group, 'Emission Strength',
previous_node, 'Emission Strength')
link(material, new_group, 'Emission Color', previous_node,
'Emission')
except:pass
# connecte les sorties du mix précédent aux entrée du nouveau
mix
link(material, color_node, color_socket, new_group, 'Color 1')
link(material, metallic_node, metallic_socket, new_group,
'Metallic 1')
link(material, roughness_node, roughness_socket, new_group,
'Roughness 1')
link(material, normal_node, normal_socket, new_group, 'Normal
1')
try:
link(material, emission_node, emission_socket, new_group,
'Emission Strength 1')
except:pass
new_group.location =
[(color_node.location[0]+previous_node.location[0])/2,
(color_node.location[1]+previous_node.location[1])/2]
else:
link(material, new_group, 'Color', previous_node, 'Base Color')
link(material, new_group, 'Metallic', previous_node,
'Metallic')
link(material, new_group, 'Roughness', previous_node,
'Roughness')
link(material, new_group, 'Normal', previous_node, 'Normal')
try:
link(material, new_group, 'Emission Strength',
previous_node, 'Emission Strength')
link(material, new_group, 'Emission Color', previous_node,
'Emission')
except:pass
new_group.select = True
self.grab_at_the_end = False
elif previous_node and previous_node.type != 'BSDF_PRINCIPLED' and 'Mix
Layers' in previous_node.name:
new_group = bpy.context.selected_nodes[0]
bpy.ops.node.select_all(action='DESELECT')
research_previous = None
try:
previous_color_link = previous_node.outputs['Color'].links[0]
previous_metallic_link =
previous_node.outputs['Metallic'].links[0]
previous_roughness_link =
previous_node.outputs['Roughness'].links[0]
previous_normal_link = previous_node.outputs['Normal'].links[0]
try:
previous_emission_link = previous_node.outputs['Emission
Strength'].links[0]
previous_emission_color_link =
previous_node.outputs['Emission Color'].links[0]
except:pass
research_previous = 'SUCCESS'
except:
research_previous = 'FAIL'
# connecte l'ancien mix avec le nouveau
link(material, previous_node, 'Color', new_group, 'Color 1')
link(material, previous_node, 'Metallic', new_group, 'Metallic 1')
link(material, previous_node, 'Roughness', new_group, 'Roughness
1')
link(material, previous_node, 'Normal', new_group, 'Normal 1')
try:
link(material, previous_node, 'Emission Strength', new_group,
'Emission Strength 1')
except:pass
if research_previous == 'SUCCESS':
# connecte le nouveau mix au bsdf
link(material, new_group, 'Color', previous_color_link.to_node,
previous_color_link.to_socket.name)
link(material, new_group, 'Metallic',
previous_metallic_link.to_node, previous_metallic_link.to_socket.name)
link(material, new_group, 'Roughness',
previous_roughness_link.to_node, previous_roughness_link.to_socket.name)
link(material, new_group, 'Normal',
previous_normal_link.to_node, previous_normal_link.to_socket.name)
try:
link(material, new_group, 'Emission Strength',
previous_emission_link.to_node, previous_emission_link.to_socket.name)
link(material, new_group, 'Emission Color',
previous_emission_color_link.to_node, previous_emission_color_link.to_socket.name)
except:pass
bsdf_node = previous_color_link.to_node
new_group.location =
[(bsdf_node.location[0]+previous_node.location[0])/2,
(bsdf_node.location[1]+previous_node.location[1])/2]
else:
new_group.location.x += 400
self.grab_at_the_end = False
elif previous_node and 'Layer' in previous_node.name:
deselect_nodes(nodes)
research_previous = None
try:
# recherche ce à quoi est connecté le layer
color_socket =
previous_node.outputs['Color'].links[0].to_socket.name
color_node = previous_node.outputs['Color'].links[0].to_node
metallic_socket =
previous_node.outputs['Metallic'].links[0].to_socket.name
metallic_node =
previous_node.outputs['Metallic'].links[0].to_node
roughness_socket =
previous_node.outputs['Roughness'].links[0].to_socket.name
roughness_node =
previous_node.outputs['Roughness'].links[0].to_node
normal_socket =
previous_node.outputs['Normal'].links[0].to_socket.name
normal_node = previous_node.outputs['Normal'].links[0].to_node
emission_socket = previous_node.outputs['Emission
Strength'].links[0].to_socket.name
emission_node = previous_node.outputs['Emission
Strength'].links[0].to_node
research_previous = 'SUCCESS'
except:
research_previous = 'FAIL'
pass
if research_previous == 'SUCCESS':
# connecte le previous layer aux entrées du mix
link(material, previous_node, 'Color', new_group, 'Color 1')
link(material, previous_node, 'Metallic', new_group, 'Metallic
1')
link(material, previous_node, 'Roughness', new_group,
'Roughness 1')
link(material, previous_node, 'Normal', new_group, 'Normal 1')
try:
link(material, previous_node, 'Emission Strength',
new_group, 'Emission Strength 1')
except:pass
# connecte le nouveaux mix à ce qui était connecté au layer
link(material, new_group, 'Color', color_node, color_socket)
link(material, new_group, 'Metallic', metallic_node,
metallic_socket)
link(material, new_group, 'Roughness', roughness_node,
roughness_socket)
link(material, new_group, 'Normal', normal_node, normal_socket)
try:
link(material, new_group, 'Emission Strength',
emission_node, emission_socket)
except:pass
try:
link(material, new_group, 'Color', emission_node,
'Emission')
except:pass
new_group.location.x = (previous_node.location.x +
color_node.location.x)/2
new_group.location.y = (previous_node.location.y +
color_node.location.y)/2
elif self.choice == 'Layer':
if previous_node and 'Mix Layers' in previous_node.name:
if not previous_node.inputs['Color 1'].links:
link(material, new_group, 'Color', previous_node, 'Color 1')
link(material, new_group, 'Metallic', previous_node, 'Metallic
1')
link(material, new_group, 'Roughness', previous_node,
'Roughness 1')
link(material, new_group, 'Normal', previous_node, 'Normal 1')
try:
link(material, new_group, 'Emission Strength',
previous_node, 'Emission Strength 1')
except:pass
elif not previous_node.inputs['Color 2'].links:
link(material, new_group, 'Color', previous_node, 'Color 2')
link(material, new_group, 'Metallic', previous_node, 'Metallic
2')
link(material, new_group, 'Roughness', previous_node,
'Roughness 2')
link(material, new_group, 'Normal', previous_node, 'Normal 2')
try:
link(material, new_group, 'Emission Strength',
previous_node, 'Emission Strength 2')
except:pass
elif previous_node and previous_node.type == 'BSDF_PRINCIPLED':
link(material, new_group, 'Color', previous_node, 'Base Color')
link(material, new_group, 'Metallic', previous_node, 'Metallic')
link(material, new_group, 'Roughness', previous_node, 'Roughness')
link(material, new_group, 'Normal', previous_node, 'Normal')
try:
link(material, new_group, 'Emission Strength', previous_node,
'Emission Strength')
link(material, new_group, 'Emission Color', previous_node,
'Emission')
except:pass
elif self.choice in {'Edges', 'Cavity', 'All edges', 'Directional Mask',
'Painted Mask', 'Local mask'} and previous_node:
# vérifie si la node sélectionnée à une entrée mask si oui on la
connecte à la sortie mask de la node tout juste ajoutée
for input in selected_nodes[0].inputs:
if input.name == 'Mask' and not input.links:
link(material, bpy.context.selected_nodes[0], 'Mask',
selected_nodes[0], 'Mask')
elif previous_node and self.choice == 'Smart Shader 2 layers' and
previous_node.type == 'BSDF_PRINCIPLED':
mix_1 = new_group
deselect_nodes(nodes)
link(material, mix_1, 'Color', previous_node, 'Base Color')
link(material, mix_1, 'Metallic', previous_node, 'Metallic')
link(material, mix_1, 'Roughness', previous_node, 'Roughness')
link(material, mix_1, 'Normal', previous_node, 'Normal')
try:
link(material, mix_1, 'Emission Strength', previous_node, 'Emission
Strength')
link(material, mix_1, 'Emission Color', previous_node, 'Emission')
except:pass

layer_1 = import_node_group('Layer')
link(material, layer_1, 'Color', mix_1, 'Color 1')
link(material, layer_1, 'Metallic', mix_1, 'Metallic 1')
link(material, layer_1, 'Roughness', mix_1, 'Roughness 1')
link(material, layer_1, 'Normal', mix_1, 'Normal 1')
try:
link(material, layer_1, 'Emission Strength', mix_1, 'Emission
Strength 1')
except:pass

layer_2 = import_node_group('Layer')
link(material, layer_2, 'Color', mix_1, 'Color 2')
link(material, layer_2, 'Metallic', mix_1, 'Metallic 2')
link(material, layer_2, 'Roughness', mix_1, 'Roughness 2')
link(material, layer_2, 'Normal', mix_1, 'Normal 2')
try:
link(material, layer_2, 'Emission Strength', mix_1, 'Emission
Strength 2')
except:pass

layer_1.location = [mix_1.location[0]-200, mix_1.location[1] + 120]


layer_2.location = [mix_1.location[0]-200, mix_1.location[1] - 320]
self.grab_at_the_end = False
elif previous_node and self.choice == 'Smart Shader 3 layers' and
previous_node.type == 'BSDF_PRINCIPLED':
mix_2 = new_group
deselect_nodes(nodes)
link(material, mix_2, 'Color', previous_node, 'Base Color')
link(material, mix_2, 'Metallic', previous_node, 'Metallic')
link(material, mix_2, 'Roughness', previous_node, 'Roughness')
link(material, mix_2, 'Normal', previous_node, 'Normal')
try:
link(material, mix_2, 'Emission Strength', previous_node, 'Emission
Strength')
link(material, mix_2, 'Emission Color', previous_node, 'Emission')
except:pass

mix_1 = import_node_group('Mix Layers')


link(material, mix_1, 'Color', mix_2, 'Color 1')
link(material, mix_1, 'Metallic', mix_2, 'Metallic 1')
link(material, mix_1, 'Roughness', mix_2, 'Roughness 1')
link(material, mix_1, 'Normal', mix_2, 'Normal 1')
try:
link(material, mix_1, 'Emission Strength', mix_2, 'Emission
Strength 1')
except:pass

layer_1 = import_node_group('Layer')
link(material, layer_1, 'Color', mix_1, 'Color 1')
link(material, layer_1, 'Metallic', mix_1, 'Metallic 1')
link(material, layer_1, 'Roughness', mix_1, 'Roughness 1')
link(material, layer_1, 'Normal', mix_1, 'Normal 1')
try:
link(material, layer_1, 'Emission Strength', mix_1, 'Emission
Strength 1')
except:pass
layer_2 = import_node_group('Layer')
link(material, layer_2, 'Color', mix_1, 'Color 2')
link(material, layer_2, 'Metallic', mix_1, 'Metallic 2')
link(material, layer_2, 'Roughness', mix_1, 'Roughness 2')
link(material, layer_2, 'Normal', mix_1, 'Normal 2')
try:
link(material, layer_2, 'Emission Strength', mix_1, 'Emission
Strength 2')
except:pass

layer_3 = import_node_group('Layer')
link(material, layer_3, 'Color', mix_2, 'Color 2')
link(material, layer_3, 'Metallic', mix_2, 'Metallic 2')
link(material, layer_3, 'Roughness', mix_2, 'Roughness 2')
link(material, layer_3, 'Normal', mix_2, 'Normal 2')
try:
link(material, layer_3, 'Emission Strength', mix_2, 'Emission
Strength 2')
except:pass

mix_1.location = [mix_2.location[0] - 200, mix_2.location[1]]


layer_3.location = [mix_1.location[0], mix_1.location[1] - 480]
layer_1.location = [mix_1.location[0]-200, mix_1.location[1] + 120]
layer_2.location = [mix_1.location[0]-200, mix_1.location[1] - 320]
self.grab_at_the_end = False
elif new_group.node_tree.name == 'Pixel':
new_screen_node_tree = make_single_user_node(new_group)
for n in new_screen_node_tree.nodes:
if n.type == 'GROUP' and n.node_tree.name == 'Image_Instance':
new_image_instance_node_tree = make_single_user_node(n)
break
for n in new_screen_node_tree.nodes:
if n.type == 'GROUP' and n.node_tree.name == 'Glitcher':
new_glitcher_node_tree = make_single_user_node(n)
for nn in new_glitcher_node_tree.nodes:
if nn.type == 'GROUP' and nn.node_tree.name ==
'Image_Instance':
nn.node_tree = new_image_instance_node_tree
break

deselect_nodes(nodes)
new_group.select = True
nodes.active = new_group
self.new_group = new_group
temp = {'last_addition':new_group, 'previous_node':previous_node}

if 'Scale' in new_group.inputs:
new_group.inputs['Scale'].default_value *=
context.scene.FluentShaderProps.scale_scale

return {'FINISHED'}

class FLUENT_SHADER_PT_ViewPanel(bpy.types.Panel):
"Fluent Shader Editor"
bl_label = "着色器"
bl_name = "Shader"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = 'Fluent'

def draw(self, context):


layout = self.layout
box = layout.box()
row = box.row()
row.prop(context.scene.FluentShaderProps, 'show_bake_setting',
icon="TRIA_DOWN" if
context.scene.FluentShaderProps.show_bake_setting else "TRIA_RIGHT",
icon_only=True, emboss=False
)
row.label(text="烘焙设置")
if context.scene.FluentShaderProps.show_bake_setting:
selection_col = box.column(align=True)
selection_col.prop(context.scene.FluentShaderProps, 'map_size')
selection_col.prop(context.scene.FluentShaderProps, 'bake_sample')

box = layout.box()
row = box.row()
row.prop(context.scene.FluentShaderProps, 'show_bake_pbr',
icon="TRIA_DOWN" if context.scene.FluentShaderProps.show_bake_pbr
else "TRIA_RIGHT",
icon_only=True, emboss=False
)
row.label(text="烘焙 PBR")
if context.scene.FluentShaderProps.show_bake_pbr:
selection_col = box.column(align=True)
selection_col.prop(context.scene.FluentShaderProps, 'bake_make_color')
selection_col.prop(context.scene.FluentShaderProps,
'bake_make_metallic')
selection_col.prop(context.scene.FluentShaderProps,
'bake_make_roughness')
selection_col_row = selection_col.row()
selection_col_row.prop(context.scene.FluentShaderProps,
'bake_make_normal')
selection_col_row.prop(context.scene.FluentShaderProps,
'bake_normal_directx')
selection_col.prop(context.scene.FluentShaderProps,
'bake_make_emission')
selection_col.prop(context.scene.FluentShaderProps, 'bake_make_ao')
# layout.prop(context.scene.FluentShaderProps, 'bake_make_alpha')
selection_col.prop(context.scene.FluentShaderProps,
'bake_make_selected_to_active')
if context.scene.FluentShaderProps.bake_make_selected_to_active:
selection_col.prop(context.scene.FluentShaderProps,
'bake_make_selected_to_active_extrusion')
selection_col.operator('fluent.bakepbrmaps', text='烘焙 PBR 贴图')

box = layout.box()
row = box.row()
row.prop(context.scene.FluentShaderProps, 'show_bake_mask',
icon="TRIA_DOWN" if context.scene.FluentShaderProps.show_bake_mask
else "TRIA_RIGHT",
icon_only=True, emboss=False
)
row.label(text="烘焙蒙版")
if context.scene.FluentShaderProps.show_bake_mask:
selection_col = box.column(align=True)
selection_col.operator(FLUENT_OT_BakeMask.bl_idname, text='所有边缘和型
腔').choice = 'ALL'
selection_col.operator(FLUENT_OT_BakeMask.bl_idname, text='只有边
缘').choice = 'EDGES'
selection_col.operator(FLUENT_OT_BakeMask.bl_idname, text='只有凹
洞').choice = 'CAVITY'
selection_col.operator(FLUENT_OT_BakeMask.bl_idname, text='仅选择节
点').choice = 'SELECTED'

# box = layout.box()
# row = box.row()
# row.prop(context.scene.FluentShaderProps, 'show_bake_decal',
# icon="TRIA_DOWN" if
context.scene.FluentShaderProps.show_bake_decal else "TRIA_RIGHT",
# icon_only=True, emboss=False
# )
# row.label(text="贴花")
# if context.scene.FluentShaderProps.show_bake_decal:
# selection_col = box.column(align=True)
# selection_col.operator(FLUENT_SHADER_OT_MoveDecal.bl_idname, text='移
动贴花')

layout.operator(FLUENT_SHADER_OT_FindHelp.bl_idname, text='帮助',
icon='HELP')

class FLUENT_SHADER_PT_Library(bpy.types.Panel):
"Fluent 节点资源库"
bl_space_type = 'NODE_EDITOR'
bl_region_type = 'UI'
bl_category = 'Fluent'
bl_label = '资源库 CGZY.NET'

@classmethod
def poll(cls, context):
if (context.space_data.tree_type == "ShaderNodeTree" and
context.space_data.shader_type == "OBJECT"):
return True
else:
return False

def draw(self, context):


layout = self.layout
row = layout.row()
if context.scene.render.engine not in ["CYCLES", "BLENDER_EEVEE"]:
row.label(text="仅支持 Cycles / Eevee")
else:
if context.space_data.tree_type == "ShaderNodeTree" and
context.space_data.shader_type == "OBJECT":
if not context.object:
row.label(text="选择对象")
elif not context.object.active_material:
row.label(text="添加材质")
elif not context.object.active_material.node_tree:
row.label(text="启用节点")
else:
layout = self.layout
layout.prop(context.scene.FluentShaderProps, 'scale_scale')
layout.prop(context.scene.FluentShaderProps, 'sections')
layout.template_icon_view(context.scene.FluentShaderProps,
'f_'+str(context.scene.FluentShaderProps.sections), show_labels=True)
layout.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="添
加").choice = context.scene.FluentShaderProps.sections

#####################################################
class FLUENT_OT_BakeMaps(Operator):
"烘焙 PBR 贴图\nCTRL+SHIFT+ALT 寻求帮助"
bl_idname = "fluent.bakepbrmaps"
bl_label="烘焙 PBR 贴图"
bl_options = {'REGISTER', 'UNDO'}

@classmethod
def poll(cls, context):
if active_object('GET'):
return True
else:
return False

def bake_process(self, context):


# On récupère le nom de l'objet sélectionné
obj = bpy.context.active_object
obj_name = obj.name

# sauvgarde les paramètres rendu de l'utilisateur


self.previous_settings['engine'] = bpy.context.scene.render.engine
self.previous_settings['preview_samples'] =
bpy.context.scene.cycles.preview_samples
self.previous_settings['render_samples'] = bpy.context.scene.cycles.samples
self.previous_settings['shading_type'] =
bpy.context.space_data.shading.type
self.previous_settings['baking_use_selected_to_active'] =
bpy.context.scene.render.bake.use_selected_to_active
# configure le moteur de rendu pour le baking
bpy.context.scene.render.bake.margin = 2
bpy.context.scene.cycles.samples =
int(bpy.context.scene.FluentShaderProps.bake_sample)
bpy.context.scene.render.engine = 'CYCLES'
bpy.context.scene.view_settings.view_transform = 'Standard'
bpy.context.scene.render.bake.use_clear = False
# besoin de forcer use_selected_to_active à false pour les bakes basiques
bpy.context.scene.render.bake.use_selected_to_active = False

try:
file_path = bpy.data.filepath
file_name = file_path.split('\\')[-1]
path = file_path.split(file_name)[0]
path_texture = path+'Textures\\'
if not os.path.exists(path_texture):
os.makedirs(path_texture)
folder_made = True
except:
folder_made = False
# ajoute une node image dans chaque matériau
img_nodes = []
for m in self.object.material_slots:
nodes = get_nodes(m.material)
img_node = nodes.new(type='ShaderNodeTexImage')
img_node.name = '#imgtemp'
img_nodes.append([nodes, img_node])

if context.scene.FluentShaderProps.bake_make_color:
# créer l'image Color vide
color_img = bpy.data.images.new(self.object.name + '_' + "Color",
width=self.size, height=self.size)
color_img.file_format = 'PNG'
if folder_made:
color_img.filepath_raw = path_texture + color_img.name + '.png'

# Connecte la Color qui rentre dans le PS à la sortie du shader


self.link_socket_to_materials_outputs('color')

# Assign l'image recevant le bake à la node image de chaque materiaux


for i in img_nodes:
i[1].image = color_img
i[0].active = i[1]

bpy.ops.object.bake(type='COMBINED',
use_selected_to_active=self.selected_to_active, cage_extrusion=self.extrusion)
self.restore_materials_outputs()

try:
color_img.save()
except:
self.report({'WARNING'}, "Blender fails to save the files on your
system. Save it manually.")

if context.scene.FluentShaderProps.bake_make_roughness:
# créer l'image Roughness vide
roughness_img = bpy.data.images.new(self.object.name+'_'+"Roughness",
width=self.size, height=self.size, is_data=True)
roughness_img.file_format = 'PNG'
if folder_made:
roughness_img.filepath_raw = path_texture+roughness_img.name+'.png'

self.link_socket_to_materials_outputs('roughness')

# Assign l'image recevant le bake à la node image de chaque materiaux


for i in img_nodes:
i[1].image = roughness_img
i[0].active = i[1]

bpy.ops.object.bake(type='COMBINED',
use_selected_to_active=self.selected_to_active, cage_extrusion=self.extrusion)
self.restore_materials_outputs()

try:
roughness_img.save()
except:
self.report({'WARNING'}, "Blender fails to save the files on your
system. Save it manually.")
if context.scene.FluentShaderProps.bake_make_metallic:
# créer l'image metallic vide
metallic_img = bpy.data.images.new(self.object.name+'_'+"Metallic",
width=self.size, height=self.size, is_data=True)
metallic_img.file_format = 'PNG'
if folder_made:
metallic_img.filepath_raw = path_texture+metallic_img.name+'.png'

self.link_socket_to_materials_outputs('metallic')

# Assign l'image recevant le bake à la node image de chaque materiaux


for i in img_nodes:
i[1].image = metallic_img
i[0].active = i[1]

bpy.ops.object.bake(type='COMBINED',
use_selected_to_active=self.selected_to_active, cage_extrusion=self.extrusion)
self.restore_materials_outputs()

try:
metallic_img.save()
except:
self.report({'WARNING'}, "Blender fails to save the files on your
system. Save it manually.")

if context.scene.FluentShaderProps.bake_make_normal:
# créer l'image normal vide
normal_img = bpy.data.images.new(self.object.name+'_'+"Normal",
width=self.size, height=self.size, is_data=True, float_buffer=True)
normal_img.file_format = 'PNG'
if folder_made:
normal_img.filepath_raw = path_texture+normal_img.name+'.png'

# Assign l'image recevant le bake à la node image de chaque materiaux


for i in img_nodes:
i[1].image = normal_img
i[0].active = i[1]

# Optimisation des weighted normal si il y en a


if self.highpoly_objects:
objects = self.highpoly_objects
else:
objects = [self.object]
i_was_false = []
for o in objects:
for m in o.modifiers:
if m.type == 'WEIGHTED_NORMAL' and not m.keep_sharp:
m.keep_sharp = True
i_was_false.append(m)

bpy.ops.object.bake(type='NORMAL',
use_selected_to_active=self.selected_to_active, cage_extrusion=self.extrusion)

if context.scene.FluentShaderProps.bake_normal_directx:
normal_directx_img = bpy.data.images.new(self.object.name + '_' +
"DirectX_Normal", width=self.size, height=self.size, is_data=True,
float_buffer=True)
normal_directx_img.file_format = 'PNG'
if folder_made:
normal_directx_img.filepath_raw = path_texture +
normal_directx_img.name + '.png'
for i, slot in enumerate(self.object.material_slots):
mat = slot.material
nodes = get_nodes(mat)
material_output = nodes.get('Material Output')
i_node = img_nodes[i][1]
separate_node = nodes.new(type='ShaderNodeSeparateRGB')
separate_node.name = '#temp_separate'
invert_node = nodes.new(type='ShaderNodeInvert')
invert_node.name = '#temp_invert'
combine_node = nodes.new(type='ShaderNodeCombineRGB')
combine_node.name = '#temp_combine'
link(mat, i_node, i_node.outputs[0].name, separate_node,
separate_node.inputs[0].name)
link(mat, separate_node, separate_node.outputs['R'].name,
combine_node, combine_node.inputs['R'].name)
link(mat, separate_node, separate_node.outputs['B'].name,
combine_node, combine_node.inputs['B'].name)
link(mat, separate_node, separate_node.outputs['G'].name,
invert_node, invert_node.inputs['Color'].name)
link(mat, invert_node, invert_node.outputs[0].name,
combine_node, combine_node.inputs['G'].name)
link(mat, combine_node, combine_node.outputs[0].name,
material_output, 0)

# ajoute une node image


img_directx_node = nodes.new(type='ShaderNodeTexImage')
img_directx_node.name = '#imgtemp'
img_directx_node.image = normal_directx_img
nodes.active = img_directx_node

selected_objects = select_objects()
deselect_objects(selected_objects)
bpy.ops.object.bake(type='COMBINED')
select_objects(selected_objects, 'SET')

try:
bpy.context.scene.render.image_settings.color_depth = '16'
bpy.context.scene.render.image_settings.file_format = 'PNG'
if not context.scene.FluentShaderProps.bake_normal_directx:
normal_img.save_render(filepath=normal_img.filepath_raw)
else:

normal_directx_img.save_render(filepath=normal_directx_img.filepath_raw)
bpy.context.scene.render.image_settings.color_depth = '8'
except:
self.report({'WARNING'}, "Blender fails to save the files on your
system. Save it manually.")

self.restore_materials_outputs()

for m in i_was_false:
m.keep_sharp = False

if context.scene.FluentShaderProps.bake_make_ao:
# créer l'image Roughness vide
ao_img = bpy.data.images.new(self.object.name+'_'+"AO",
width=self.size, height=self.size, is_data=True, float_buffer=True)
ao_img.file_format = 'PNG'
if folder_made:
ao_img.filepath_raw = path_texture+ao_img.name+'.png'

# Assign l'image recevant le bake à la node image de chaque materiaux


for i in img_nodes:
i[1].image = ao_img
i[0].active = i[1]

bpy.ops.object.bake(type='AO',
use_selected_to_active=self.selected_to_active, cage_extrusion=self.extrusion)
try:
ao_img.save()
except:
self.report({'WARNING'}, "Blender fails to save the files on your
system. Save it manually.")

if context.scene.FluentShaderProps.bake_make_emission:
# créer l'image Emission Color vide ###################################
emission_color_img = bpy.data.images.new(self.object.name + '_' +
"Emission_Color", width=self.size, height=self.size)
emission_color_img.file_format = 'PNG'
if folder_made:
emission_color_img.filepath_raw = path_texture +
emission_color_img.name + '.png'

# Connecte la Color qui rentre dans le PS à la sortie du shader


self.link_socket_to_materials_outputs('emission')

# Assign l'image recevant le bake à la node image de chaque materiaux


for i in img_nodes:
i[1].image = emission_color_img
i[0].active = i[1]

bpy.ops.object.bake(type='COMBINED',
use_selected_to_active=self.selected_to_active, cage_extrusion=self.extrusion)
self.restore_materials_outputs()

try:
emission_color_img.save()
except:
self.report({'WARNING'}, "Blender fails to save the files on your
system. Save it manually.")

# créer l'image Emission Strength vide


###################################
emission_strength_img = bpy.data.images.new(self.object.name + '_' +
"Emission_Strength", width=self.size,
height=self.size)
emission_strength_img.file_format = 'PNG'
if folder_made:
emission_strength_img.filepath_raw = path_texture +
emission_strength_img.name + '.png'

# Connecte la Color qui rentre dans le PS à la sortie du shader


self.link_socket_to_materials_outputs('emission strength')

# Assign l'image recevant le bake à la node image de chaque materiaux


for i in img_nodes:
i[1].image = emission_strength_img
i[0].active = i[1]

bpy.ops.object.bake(type='COMBINED',
use_selected_to_active=self.selected_to_active,
cage_extrusion=self.extrusion)
self.restore_materials_outputs()

try:
emission_strength_img.save()
except:
self.report({'WARNING'}, "Blender fails to save the files on your
system. Save it manually.")

# combine roughness metallic AO


if context.scene.FluentShaderProps.bake_make_roughness and
context.scene.FluentShaderProps.bake_make_metallic and
context.scene.FluentShaderProps.bake_make_ao :
combine_img =
bpy.data.images.new(self.object.name+'_AO_ROUGHNESS_METALLIC', width=self.size,
height=self.size)
combine_img.file_format = 'PNG'
if folder_made:
combine_img.filepath_raw = path_texture + combine_img.name + '.png'

for slot in self.object.material_slots:


mat = slot.material
nodes = get_nodes(mat)
material_output = nodes.get('Material Output')
img_R_AO_node = nodes.new(type='ShaderNodeTexImage')
img_R_AO_node.name = '#imgtemp'
img_G_ROUGHNESS_node = nodes.new(type='ShaderNodeTexImage')
img_G_ROUGHNESS_node.name = '#imgtemp'
img_B_METALLIC_node = nodes.new(type='ShaderNodeTexImage')
img_B_METALLIC_node.name = '#imgtemp'
img_R_AO_node.image = ao_img
img_G_ROUGHNESS_node.image = roughness_img
img_B_METALLIC_node.image = metallic_img
combine_node = nodes.new(type='ShaderNodeCombineRGB')
combine_node.name = '#imgtemp'
link(mat, img_R_AO_node, img_R_AO_node.outputs[0].name,
combine_node, combine_node.inputs[0].name)
link(mat, img_G_ROUGHNESS_node,
img_G_ROUGHNESS_node.outputs[0].name, combine_node, combine_node.inputs[1].name)
link(mat, img_B_METALLIC_node, img_B_METALLIC_node.outputs[0].name,
combine_node, combine_node.inputs[2].name)
link(mat, combine_node, combine_node.outputs[0].name,
material_output, 0)

img_COMBINED_node = nodes.new(type='ShaderNodeTexImage')
img_COMBINED_node.name = '#imgtemp'
img_COMBINED_node.image = combine_img
nodes.active = img_COMBINED_node

bpy.ops.object.bake(type='COMBINED')
try:
combine_img.save()
except:
self.report({'WARNING'}, "Blender fails to save the files on your
system. Save it manually.")

self.restore_materials_outputs()

# supprime toutes les nodes images utilisées pour le bake


for m in self.object.material_slots:
nodes = get_nodes(m.material)
for node in nodes:
if '#imgtemp' in node.name:
nodes.remove(node)

bpy.context.scene.view_settings.view_transform = 'Filmic'
bpy.context.scene.render.engine = self.previous_settings['engine']
bpy.context.scene.cycles.preview_samples =
self.previous_settings['preview_samples']
bpy.context.scene.cycles.samples = self.previous_settings['render_samples']
bpy.context.space_data.shading.type =
self.previous_settings['shading_type']
bpy.context.scene.render.bake.use_selected_to_active =
self.previous_settings['baking_use_selected_to_active']

return{'FINISHED'}

def get_selected_objects_materials(self):
objects = select_objects()
materials = dict()
for object in objects :
if object != active_object():
materials[object.active_material.name] = object.active_material

return materials

def link_socket_to_materials_outputs(self, type) :


# for material in self.selected_objects_material.values() :
if self.highpoly_objects:
objects = self.highpoly_objects
else:
objects = [self.object]
for obj in objects:
for slot in obj.material_slots:
material = slot.material
nodes = get_nodes(material)

# trouve le principled shader


material_output = nodes.get('Material Output')
PS = nodes.get('Principled BSDF')

if PS and PS.outputs['BSDF'].links and


PS.outputs['BSDF'].links[0].to_node == material_output :
socket = node = None
# node_created = None

if type == 'color' :
# trouve ce qui est connecté à l'entrée Color
try:
socket = PS.inputs['Base
Color'].links[0].from_socket.name
node = PS.inputs['Base Color'].links[0].from_node
except:
node = nodes.new(type='ShaderNodeRGB')
node.name = '#temp_color'
node.outputs[0].default_value = PS.inputs['Base
Color'].default_value
socket = node.outputs[0].name
# node_created = node

if type == 'roughness' :
# trouve ce qui est connecté à l'entrée Roughness
try:
socket =
PS.inputs['Roughness'].links[0].from_socket.name
node = PS.inputs['Roughness'].links[0].from_node
except:
node = nodes.new(type='ShaderNodeValue')
node.name = '#temp_roughness'
node.outputs[0].default_value =
PS.inputs['Roughness'].default_value
socket = node.outputs[0].name
node_created = node

if type == 'metallic' :
# trouve ce qui est connecté à l'entrée Metallic
try:
socket =
PS.inputs['Metallic'].links[0].from_socket.name
node = PS.inputs['Metallic'].links[0].from_node
except:
node = nodes.new(type='ShaderNodeValue')
node.name = '#temp_metallic'
node.outputs[0].default_value =
PS.inputs['Metallic'].default_value
socket = node.outputs[0].name
node_created = node

if type == 'emission' :
# trouve ce qui est connecté à l'entrée Emission
try:
socket =
PS.inputs['Emission'].links[0].from_socket.name
node = PS.inputs['Emission'].links[0].from_node
except:
node = nodes.new(type='ShaderNodeRGB')
node.name = '#temp_emission_color'
node.outputs[0].default_value =
PS.inputs['Emission'].default_value
socket = node.outputs[0].name

if type == 'emission strength' :


# trouve ce qui est connecté à l'entrée Emission Strength
try:
socket = PS.inputs['Emission
Strength'].links[0].from_socket.name
node = PS.inputs['Emission
Strength'].links[0].from_node
except:
node = nodes.new(type='ShaderNodeValue')
node.name = '#temp_emission_strength'
node.outputs[0].default_value = PS.inputs['Emission
Strength'].default_value
socket = node.outputs[0].name

if node == None or socket == None :


self.report({'ERROR'}, 'Type "'+type+'" not supported.')

link(material, node, socket, material_output, 0)

def restore_materials_outputs(self):
if self.highpoly_objects:
objects = self.highpoly_objects
else:
objects = [self.object]
for obj in objects:
for slot in obj.material_slots:
material = slot.material
nodes = get_nodes(material)
material_output = nodes.get('Material Output')
PS = nodes.get('Principled BSDF')
for node in nodes:
if '#temp' in node.name:
nodes.remove(node)
link(material, PS, 0, material_output, 0)

def invoke(self, context, event):


if event.ctrl and event.shift and event.alt:
bpy.context.window_manager.popup_menu(make_oops(['This function bakes
what you designed with Fluent Materializer.',
'The baking function bakes what is connected to the principled BSDF,
which is connected to the material output.',
'Be sure the principled BSDF is connected to the material output, then
select your object and bake.',
'The program will try to save the images in a folder called "Textures"
next to the blend file,',
'if it can\'t, you have to save your files manualy',
'The image name follows this pattern : ObjectName_MAP']), title="About
PBR baking", icon='INFO')
return{'FINISHED'}
self.previous_settings = {}
self.size = int(context.scene.FluentShaderProps.map_size)
self.object = active_object('GET')
self.highpoly_objects = None
self.selected_to_active =
context.scene.FluentShaderProps.bake_make_selected_to_active
self.extrusion = 0

if self.selected_to_active:
self.extrusion =
context.scene.FluentShaderProps.bake_make_selected_to_active_extrusion

# Vérification
if self.object.type != 'MESH':
bpy.context.window_manager.popup_menu(
make_oops(['You try to bake a non mesh thing.']),
title="Problem detected",
icon='INFO'
)
return {'FINISHED'}
if not self.object or len(bpy.context.selected_objects) == 0:
bpy.context.window_manager.popup_menu(
make_oops(['No active object.']),
title="Problem detected",
icon='INFO'
)
return{'FINISHED'}
elif not len(self.object.data.materials):
bpy.context.window_manager.popup_menu(
make_oops(['No material for the active object.']),
title="Problem detected",
icon='INFO'
)
return {'FINISHED'}
elif not len(self.object.data.uv_layers):
bpy.context.window_manager.popup_menu(
make_oops(['No UV map detected.','Unwrap the model before to
bake.']),
title="Problem detected",
icon='INFO'
)
return {'FINISHED'}
# bake un objet
if self.object and not self.selected_to_active and not
len(bpy.context.selected_objects) >= 2:
self.bake_process(context)
elif not self.selected_to_active and len(bpy.context.selected_objects) >=
2:
bpy.context.window_manager.popup_menu(
make_oops(['No selected to active option but more than one object
selected.']),
title="Problem detected",
icon='INFO'
)
# bake high to low
if self.object and self.selected_to_active and
len(bpy.context.selected_objects) >= 2:
self.highpoly_objects = [o for o in bpy.context.selected_objects if o !
= self.object and o.type == 'MESH']
self.bake_process(context)
elif self.selected_to_active and len(bpy.context.selected_objects) < 2:
bpy.context.window_manager.popup_menu(
make_oops(['Select to active option but less than 2 objects
selected.']),
title="Problem detected",
icon='INFO'
)

return{'FINISHED'}

class FLUENT_OT_BakeMask(Operator):
"烘焙节点树的边缘和空腔遮罩\nCTRL+SHIFT+ALT 寻求帮助"
bl_idname = "fluent.bakemask"
bl_label="烘焙 PBR 贴图"
bl_options = {'REGISTER', 'UNDO'}

choice: bpy.props.StringProperty()

@classmethod
def poll(cls, context):
if active_object('GET'):
return True
else:
return False

def bake_process(self):
PS = None
obj_name = None
# trouve le principled shader
node_tree = get_node_tree(self.baked_material)
nodes = get_nodes(self.baked_material)
if self.choice == 'ALL':
mask_nodes = [node for node in nodes if 'Edges' in node.name or 'edges'
in node.name or 'Cavity' in node.name]
elif self.choice == 'EDGES':
mask_nodes = [node for node in nodes if 'Edges' in node.name or 'edges'
in node.name]
elif self.choice == 'CAVITY':
mask_nodes = [node for node in nodes if 'Cavity' in node.name]
elif self.choice == 'SELECTED':
mask_nodes = [node for node in nodes if ('Edges' in node.name or
'Cavity' in node.name or 'edges' in node.name) and node.select]
# On récupère le nom de l'objet sélectionné
obj = bpy.context.active_object
obj_name = obj.name
if mask_nodes and obj.select_get():
# sauvgarde les paramètres rendu de l'utilisateur
self.previous_settings['engine'] = bpy.context.scene.render.engine
self.previous_settings['preview_samples'] =
bpy.context.scene.cycles.preview_samples
self.previous_settings['render_samples'] =
bpy.context.scene.cycles.samples
self.previous_settings['shading_type'] =
bpy.context.space_data.shading.type
# configure le moteur de rendu pour le baking
bpy.context.scene.render.bake.margin = 2
bpy.context.scene.cycles.samples =
int(bpy.context.scene.FluentShaderProps.bake_sample)
bpy.context.scene.render.engine = 'CYCLES'
bpy.context.scene.view_settings.view_transform = 'Standard'

# Ajout une node emission et la connect à la sortie du matériau


emission_node = nodes.new('ShaderNodeEmission')
material_output = nodes.get('Material Output')
link(self.baked_material, emission_node, 0, material_output, 0)

try:
file_path = bpy.data.filepath
file_name = file_path.split('\\')[-1]
path = file_path.split(file_name)[0]
path_texture = path+'Textures\\'
if not os.path.exists(path_texture):
os.makedirs(path_texture)
folder_made = True
except:
folder_made = False
self.report({'INFO'}, "The blend file must be saved for automatic
image saving.")
for mask_node in mask_nodes:
# mute le mask mixer
for n in mask_node.node_tree.nodes:
if n.bl_idname == 'ShaderNodeGroup' and 'Mask Texture Mixer' in
n.node_tree.name:
n.mute = True

# ajoute une node image


img_node = nodes.new(type='ShaderNodeTexImage')

# créer l'image Mask vide


mask_img =
bpy.data.images.new(obj_name+'_'+self.baked_material.name+'_Mask_'+mask_node.name,
width=self.size, height=self.size)
if folder_made:
mask_img.filepath_raw =
path_texture+obj_name+'_'+mask_node.name+'_Mask.png'
mask_img.file_format = 'PNG'

# Node image utilise l'image Mask


img_node.image = mask_img

# Connecte le mask à l'entrée Color de l'émission


link(self.baked_material, mask_node, 'Mask', emission_node, 0)
# Node image utilise l'image Mask
img_node.image = mask_img
# Bake
# bpy.context.scene.sequencer_colorspace_settings.name = 'sRGB'
active_object(obj = obj, action = 'SET', solo = True)
node_tree.nodes.active = img_node
bpy.ops.object.bake(type='EMIT')
# Enregistre l'image
if folder_made:
try:
mask_img.save()
except:
self.report({'INFO'}, "Blender fails to save the files on
your system. Save it manually.")
# Réactive le mask mixer
# mute le mask mixer
# for n in mask_node.node_tree.nodes:
# if n.bl_idname == 'ShaderNodeGroup' and 'Mask Texture Mixer'
in n.node_tree.name:
# n.mute = False
# Ajoute la node bake dans l'arbre
new_group = import_node_group('Baked')
original = new_group.node_tree
single = original.copy()
new_group.node_tree = single
new_group.label = 'Baked '+mask_node.name
img_node_baked = new_group.node_tree.nodes.get('Image Texture')
img_node_baked.image = mask_img

nodes.remove(img_node)

nodes.remove(emission_node)
bpy.context.scene.view_settings.view_transform = 'Filmic'
bpy.context.scene.render.engine = self.previous_settings['engine']
bpy.context.scene.cycles.preview_samples =
self.previous_settings['preview_samples']
bpy.context.scene.cycles.samples =
self.previous_settings['render_samples']
bpy.context.space_data.shading.type =
self.previous_settings['shading_type']

else:
self.report({'ERROR'}, 'No selected object.')

return{'FINISHED'}

def invoke(self, context, event):


if event.ctrl and event.shift and event.alt:
bpy.context.window_manager.popup_menu(make_oops(['This function bake
edge and cavity masks from Fluent MC.',
'The program searchs and uses the edge and cavity masks from Fluent MC
in the node tree.',
'The program will try to save the images in a folder called "Textures"
next to the blend file if it saved.',
'The image name follows this pattern :
ObjectName_MaterialName_Mask_NodeName']), title="About masks baking", icon='INFO')
return{'FINISHED'}
self.previous_settings = {}
self.size = int(context.scene.FluentShaderProps.map_size)
self.baked_material = active_object('GET').active_material

if not len(active_object('GET').data.uv_layers):
bpy.context.window_manager.popup_menu(
make_oops(['No UV map detected.','Unwrap the model before to
bake.']),
title="Problem detected",
icon='INFO'
)
return {'FINISHED'}

self.bake_process()

return{'FINISHED'}

#####################################################
class FluentShaderProps(bpy.types.PropertyGroup):

def import_imperfections(self, context):


bpy.ops.fluent.add_nodes('INVOKE_DEFAULT', choice='imperfections')

def import_grunges(self, context):


bpy.ops.fluent.add_nodes('INVOKE_DEFAULT', choice='grunges')

def import_normals(self, context):


bpy.ops.fluent.add_nodes('INVOKE_DEFAULT', choice='normals')

def import_patterns(self, context):


bpy.ops.fluent.add_nodes('INVOKE_DEFAULT', choice='patterns')
def import_liquid(self, context):
bpy.ops.fluent.add_nodes('INVOKE_DEFAULT', choice='liquid')

def import_city(self, context):


bpy.ops.fluent.add_nodes('INVOKE_DEFAULT', choice='city')

def import_fabric(self, context):


bpy.ops.fluent.add_nodes('INVOKE_DEFAULT', choice='fabric')

def import_metal(self, context):


bpy.ops.fluent.add_nodes('INVOKE_DEFAULT', choice='metal')

def import_shaders(self, context):


bpy.ops.fluent.add_nodes('INVOKE_DEFAULT', choice='shaders')

def import_screen(self, context):


bpy.ops.fluent.add_nodes('INVOKE_DEFAULT', choice='screen')

def import_wood(self, context):


bpy.ops.fluent.add_nodes('INVOKE_DEFAULT', choice='wood')

show_bake_setting: bpy.props.BoolProperty(
default=False
)

show_bake_pbr: bpy.props.BoolProperty(
default=False
)

show_bake_mask: bpy.props.BoolProperty(
default=False
)

show_bake_decal: bpy.props.BoolProperty(
default=False
)

map_size : bpy.props.EnumProperty(
name='贴图大小',
items=(
("1024", "1024", "1024"),
("2048", "2048", "2048"),
("4096", "4096", "4096"),
("8192", "8192", "8192")
),
default='2048'
)
bake_sample : bpy.props.EnumProperty(
name='Sample',
items=(
("1", "1", "1"),
("2", "2", "2"),
("4", "4", "4"),
("8", "8", "8"),
("16", "16", "16"),
("32", "32", "32"),
("64", "64", "64")
),
default='16'
)
sections : bpy.props.EnumProperty(
name='分类',
items=(
("imperfections", "瑕疵", "imperfections"),
("grunges", "垃圾", "grunges"),
("patterns", "Patterns", "patterns"),
("liquid", "Liquid", "liquid"),
("city", "城市", "city"),
("normals", "Normals", "normals"),
("fabric", "织物", "fabric"),
("metal", "Metal", "metal"),
("shaders", "着色器", "shaders"),
("wood", "Wood", "wood"),
("screen", "Screen", "screen"),
),
default='imperfections'
)
scale_scale : bpy.props.FloatProperty(
description = "乘以默认添加的节点比例.用于自动调整对象大小",
name = "Scale",
default = 1,
min = 0,
step = 0.01,
precision = 2
)
bake_make_color : BoolProperty(
default = True,
name = '颜色',
description = "烘焙颜色贴图"
)
bake_make_metallic : BoolProperty(
default = True,
name = '金属度',
description = "烘焙金属贴图"
)
bake_make_roughness : BoolProperty(
default = True,
name = '粗糙度',
description = "烘焙粗糙度贴图"
)
bake_make_normal : BoolProperty(
default = True,
name = '法线',
description = "烘焙法线贴图"
)
bake_make_emission : BoolProperty(
default = False,
name = '自发光(发射)',
description = "烘焙自发光(发射)贴图"
)
bake_make_ao : BoolProperty(
default = False,
name = 'AO',
description = "烘焙 AO 环境遮挡贴图"
)
bake_make_alpha : BoolProperty(
default = False,
name = 'Alpha',
description = "烘焙 alpha 贴图"
)
bake_normal_opengl : BoolProperty(
default = True,
name = 'OpenGL',
description = "保存 opengl 法线贴图"
)
bake_normal_directx : BoolProperty(
default = False,
name = 'DirectX',
description = "保存 DirectX 法线贴图"
)
bake_make_selected_to_active : BoolProperty(
default = False,
name = '将高多边形烘焙成低多边形',
description = "将一个或多个对象烘焙为一个\n 警告:您需要选择所有要烘焙的对象并保持目标
对象处于活动状态"
)
bake_make_selected_to_active_extrusion : FloatProperty(
default = 0.01,
name = '挤出',
description = "用于在选择到活动的烘焙模式下烘焙的挤出.\n 注意:此设置将取决于您的对象的
大小.如果您得到奇怪的结果,请降低它"
)

global preview_collections

f_imperfections : bpy.props.EnumProperty(

items=enum_previews_from_directory_items(preview_collections["imperfections"]),
update = import_imperfections
)
f_grunges : bpy.props.EnumProperty(
items=enum_previews_from_directory_items(preview_collections["grunges"]),
update = import_grunges
)
f_patterns : bpy.props.EnumProperty(
items=enum_previews_from_directory_items(preview_collections["patterns"]),
update = import_patterns
)
f_liquid : bpy.props.EnumProperty(
items=enum_previews_from_directory_items(preview_collections["liquid"]),
update = import_liquid
)
f_city : bpy.props.EnumProperty(
items=enum_previews_from_directory_items(preview_collections["city"]),
update = import_city
)
f_normals : bpy.props.EnumProperty(
items=enum_previews_from_directory_items(preview_collections["normals"]),
update = import_normals
)
f_fabric : bpy.props.EnumProperty(
items=enum_previews_from_directory_items(preview_collections["fabric"]),
update = import_fabric
)
f_metal : bpy.props.EnumProperty(
items=enum_previews_from_directory_items(preview_collections["metal"]),
update = import_metal
)
f_shaders : bpy.props.EnumProperty(
items=enum_previews_from_directory_items(preview_collections["shaders"]),
update = import_shaders
)
f_wood: bpy.props.EnumProperty(
items=enum_previews_from_directory_items(preview_collections["wood"]),
update=import_wood
)
f_screen: bpy.props.EnumProperty(
items=enum_previews_from_directory_items(preview_collections["screen"]),
update=import_screen
)
#####################################################

class FLUENT_SHADER_MT_Pie_Menu(bpy.types.Menu):
bl_label = 'Fluent MZ '+str(bl_info['version'][0])+'.'+str(bl_info['version']
[1])+'.'+str(bl_info['version'][2])
bl_options = {'REGISTER'}

@classmethod
def poll(cls, context):
return (bpy.context.area.ui_type == 'ShaderNodeTree')

def draw(self, context):


icons = load_icons()
multiply_ico = icons.get("multiply")
difference_ico = icons.get("difference")

layout = self.layout
pie = layout.menu_pie()
LEFT = pie.column()
LEFT.label(text='图层')
box = LEFT.column()
box.scale_x = 1.2
box.scale_y = 1.5
box.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="混合图层").choice =
"Mix Layers"
row = box.row(align=True)
row.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="新建图层").choice =
"Layer"
row.operator(FLUENT_SHADER_OT_SwapLayers.bl_idname, text="",
icon="FILE_REFRESH")
row.operator(FLUENT_SHADER_OT_LayerMixLayersConnect.bl_idname, text="",
icon="LINKED")
row = LEFT.row(align=True)
row.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="2 layers").choice =
"Smart Shader 2 layers"
row.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="3 layers").choice =
"Smart Shader 3 layers"
RIGHT = pie.column()
RIGHT.label(text='蒙版')
row = RIGHT.row(align=True)
row.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="所有边缘").choice =
"All edges"
row.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="边缘").choice =
"Edges"
RIGHT.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="凹洞").choice =
"Cavity"
row = RIGHT.row(align=True)
row.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="定向",
icon="EMPTY_SINGLE_ARROW").choice = "Directional Mask"
row.operator(FLUENT_SHADER_OT_Localmask.bl_idname, text="局部")
row = RIGHT.row(align=True)
row.operator(FLUENT_SHADER_OT_NewPaintedMask.bl_idname, text="绘制",
icon="BRUSH_DATA").option = "Painted Mask"
row.operator(FLUENT_SHADER_OT_NewPaintedMask.bl_idname, text="",
icon_value=multiply_ico.icon_id).option = 'MULTIPLY'
row.operator(FLUENT_SHADER_OT_NewPaintedMask.bl_idname, text="",
icon_value=difference_ico.icon_id).option = 'DIFFERENCE'
row.operator(FLUENT_SHADER_OT_EditPaintedMask.bl_idname, text="",
icon="GP_MULTIFRAME_EDITING")
RIGHT.label(text='贴花')
row = RIGHT.row(align=True)
row.operator(FLUENT_SHADER_OT_OpenFilebrowser.bl_idname, text='添加',
icon='PLUS').continue_to = 'new_decal'
row.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text='混合器',
icon='SELECT_EXTEND').choice = 'Decal Mixer'
row.operator(FLUENT_SHADER_OT_NewDecal.bl_idname, text='复制',
icon='DUPLICATE').duplicate = True
BOTTOM = pie.column()
BOTTOM.label(text='数学')
colonneA = BOTTOM.row(align=True)
colonneA.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="屏幕").choice
= "Screen"
colonneA.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="变亮").choice
= "Lighten"
colonneB = BOTTOM.row(align=True)
colonneB.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="覆盖").choice
= "Overlay"
colonneB.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="差异").choice
= "Difference"
colonneC = BOTTOM.row(align=True)
colonneC.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="相乘").choice
= "Multiply"
colonneC.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="数学混
合").choice = "Math Mix"
colonneD = BOTTOM.row(align=True)
colonneD.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="移位/锐
化").choice = "Shift/Sharpen"
colonneD.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="反转").choice
= "Invert"
pie.menu("FLUENT_MT_Color_Menu")

class FLUENT_MT_Color_Menu(Menu):
bl_label = "金属颜色"

def draw(self, context):


layout = self.layout
colonne = layout.column()
ligne = colonne.row(align=True)
layout.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="铁").choice =
"Iron"
layout.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="铝").choice =
"Aluminium"
layout.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="金子").choice =
"Gold"
layout.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="铂金").choice =
"Platinium"
ligne2 = colonne.row(align=True)
layout.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="铜").choice =
"Copper"
layout.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="氧化铜").choice =
"CuO"
layout.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="黄铜").choice =
"Brass"
layout.operator(FLUENT_SHADER_OT_addNodes.bl_idname, text="银").choice =
"Silver"

class FLUENT_SHADER_OT_NodeCounter(Operator):
bl_idname = 'fluent.node_counter'
bl_description = '计算节点树中的节点'
bl_category = 'Node'
bl_label = '节点计数器'

def count_inside(self, node):


count = 0
nodes = node.node_tree.nodes
for n in nodes:
if n.type == 'GROUP':
count += self.count_inside(n)
else:
if n.type not in ['GROUP_INPUT', 'GROUP_OUTPUT']:
count +=1
return count

def invoke(self, context, event):


active_material = active_object('GET').active_material
nodes = get_nodes(active_material)
count_total = 0
for n in nodes:
if n.type == 'GROUP':
count_total += self.count_inside(n)
else:
count_total +=1
ratio = round(((len(nodes)-1)/count_total - 1) * 100,1) * -1
bpy.context.window_manager.popup_menu(make_oops([str(count_total-1)+" nodes
in your material."]), title="INFO", icon='INFO')
return{'FINISHED'}

class FLUENT_SHADER_OT_ImageExtractor(Operator):
bl_idname = 'fluent.imageextractor'
bl_description = '将图像转换为组以提取粗糙度和法线'
bl_category = 'Node'
bl_label = '图像提取器'
def execute(self, context):
active_material = active_object('GET').active_material
nodes = get_nodes(active_material)

if nodes.active and nodes.active.select:


img_node = nodes.active
img = img_node.image
bpy.ops.fluent.add_nodes('INVOKE_DEFAULT', choice='Image Data
Extractor')
new_group = temp['last_addition']
new_group.location = img_node.location
for n in new_group.node_tree.nodes:
if n.type == 'TEX_IMAGE':
n.image = img
nodes.remove(img_node)
return{'FINISHED'}

class FLUENT_SHADER_PT_Tool(bpy.types.Panel):
"Fluent Shader Panel"
bl_space_type = 'NODE_EDITOR'
bl_region_type = 'UI'
bl_category = 'Fluent'
bl_label = '工具'

@classmethod
def poll(cls, context):
if (context.space_data.tree_type == "ShaderNodeTree" and
context.space_data.shader_type == "OBJECT"):
return True
else:
return False

def draw(self, context):


layout = self.layout
if not context.object.active_material:
layout.label(text="添加材质")
else:
layout.operator(FLUENT_SHADER_OT_NodeCounter.bl_idname)
layout.operator(FLUENT_SHADER_OT_ImageExtractor.bl_idname)

class FLUENT_SHADER_OT_SwapLayers(Operator):
"""交换所选混合图层的所有条目"""
bl_idname = 'fluent.swaplayers'
bl_category = 'Node'
bl_label = '交换图层'

def execute(self, context):


active_material = active_object('GET').active_material
nodes = get_nodes(active_material)
the_mix_layers = None

if len(bpy.context.selected_nodes) == 2:
if not 'Mix' in bpy.context.selected_nodes[0].name and not 'Mix' in
bpy.context.selected_nodes[1].name:
if 'Layer' in bpy.context.selected_nodes[0].name and 'Layer' in
bpy.context.selected_nodes[1].name:
layer_1 = bpy.context.selected_nodes[0]
layer_2 = bpy.context.selected_nodes[1]
else:
bpy.context.window_manager.popup_menu(make_oops(['Select two Layers
node.']), title="INFO", icon='INFO')
return{'FINISHED'}

if layer_1 and layer_2:


color_1 = to_what(layer_1, 'Color')
color_2 = to_what(layer_2, 'Color')
metallic_1 = to_what(layer_1, 'Metallic')
metallic_2 = to_what(layer_2, 'Metallic')
roughness_1 = to_what(layer_1, 'Roughness')
roughness_2 = to_what(layer_2, 'Roughness')
normal_1 = to_what(layer_1, 'Normal')
normal_2 = to_what(layer_2, 'Normal')
try:
emission_1 = to_what(layer_1, 'Emission Strength')
emission_2 = to_what(layer_2, 'Emission Strength')
except:pass

link(active_material, layer_1, layer_1.outputs['Color'].name,


color_2['node'][0], color_2['socket'][0])
link(active_material, layer_2, layer_2.outputs['Color'].name,
color_1['node'][0], color_1['socket'][0])

link(active_material, layer_1, layer_1.outputs['Metallic'].name,


metallic_2['node'][0], metallic_2['socket'][0])
link(active_material, layer_2, layer_2.outputs['Metallic'].name,
metallic_1['node'][0], metallic_1['socket'][0])

link(active_material, layer_1, layer_1.outputs['Roughness'].name,


roughness_2['node'][0], roughness_2['socket'][0])
link(active_material, layer_2, layer_2.outputs['Roughness'].name,
roughness_1['node'][0], roughness_1['socket'][0])

link(active_material, layer_1, layer_1.outputs['Normal'].name,


normal_2['node'][0], normal_2['socket'][0])
link(active_material, layer_2, layer_2.outputs['Normal'].name,
normal_1['node'][0], normal_1['socket'][0])

try:
link(active_material, layer_1, layer_1.outputs['Emission
Strength'].name, emission_2['node'][0], emission_2['socket'][0])
link(active_material, layer_2, layer_2.outputs['Emission
Strength'].name, emission_1['node'][0], emission_1['socket'][0])
except:pass

return{'FINISHED'}

class FLUENT_SHADER_OT_LayerMixLayersConnect(Operator):
"""将图层连接到混合图层"""
bl_idname = 'fluent.layermixlayersconnect'
bl_category = 'Node'
bl_label = '图层连接器'

def execute(self, context):


active_material = active_object('GET').active_material
nodes = get_nodes(active_material)

selected_nodes = get_selected_nodes(nodes)
layer_node = None
mix_node = None

if len(selected_nodes) == 2:
if selected_nodes[0].type == 'GROUP' and
selected_nodes[0].node_tree.name == 'Layer':
layer_node = selected_nodes[0]
elif selected_nodes[1].type == 'GROUP' and
selected_nodes[1].node_tree.name == 'Layer':
layer_node = selected_nodes[1]
if selected_nodes[0].type == 'GROUP' and
selected_nodes[0].node_tree.name == 'Mix Layers':
mix_node = selected_nodes[0]
elif selected_nodes[1].type == 'GROUP' and
selected_nodes[1].node_tree.name == 'Mix Layers':
mix_node = selected_nodes[1]

if layer_node and mix_node:


if not mix_node.inputs['Color 1'].links:
link(active_material, layer_node, 'Color', mix_node, 'Color 1')
link(active_material, layer_node, 'Metallic', mix_node,
'Metallic 1')
link(active_material, layer_node, 'Roughness', mix_node,
'Roughness 1')
link(active_material, layer_node, 'Normal', mix_node, 'Normal
1')
try:
link(active_material, layer_node, 'Emission Strength',
mix_node, 'Emission Strength 1')
except:pass
elif not mix_node.inputs['Color 2'].links:
link(active_material, layer_node, 'Color', mix_node, 'Color 2')
link(active_material, layer_node, 'Metallic', mix_node,
'Metallic 2')
link(active_material, layer_node, 'Roughness', mix_node,
'Roughness 2')
link(active_material, layer_node, 'Normal', mix_node, 'Normal
2')
try:
link(active_material, layer_node, 'Emission Strength',
mix_node, 'Emission Strength 2')
except:pass

return{'FINISHED'}

class FLUENT_SHADER_OT_NewPaintedMask(Operator):
"""绘制蒙版"""
bl_idname = 'fluent.newpaintedmask'
bl_category = 'Node'
bl_label = '绘制蒙版'

size : IntProperty(
name="Size",
description="Image size.",
default=4096
)
the_node : StringProperty()
option : StringProperty()

def invoke(self, context, event):


# Vérification avant lancement
try:
material = bpy.context.active_object.active_material
tree = material.node_tree
nodes = tree.nodes
material_output = nodes.get('Material Output')
previous_node = nodes.active
uv_layers = bpy.context.active_object.data.uv_layers
if not len(uv_layers):
bpy.context.window_manager.popup_menu(make_oops(['No UV map.',
'Please, unwrap your model.', 'The paint process isn\'t a procedural workflow but
an image based workflow. So UVs needed.']), title="INFO", icon='INFO')
return {'FINISHED'}
masks_list = ['Edges', 'All edges', 'Cavity', 'Directional Mask',
'Painted Mask', 'Math Mix', 'Multiply', 'Difference', 'Overlay', 'Lighten',
'Screen']
test = [a for a in masks_list if a in previous_node.name]
if self.option in ['MULTIPLY', 'DIFFERENCE'] and len(test) == 0:
bpy.context.window_manager.popup_menu(make_oops(['Currently, this
function work only with another mask.']), title="INFO", icon='INFO')
return {'FINISHED'}
except:
return {'FINISHED'}
# Fin vérification avant lancement
global temp
active_obj = bpy.context.active_object
active_material = bpy.context.active_object.active_material
tree = active_material.node_tree
nodes = tree.nodes
bpy.ops.fluent.add_nodes('INVOKE_DEFAULT', choice = 'Painted Mask')
new_group = temp['last_addition']
previous_node = temp['previous_node']
# créer l'image
mask_img =
bpy.data.images.new(active_obj.name+'_'+active_material.name+'_'+"painted",
width=self.size, height=self.size)
mask_img.file_format = 'PNG'
# copie indépendante
original = new_group.node_tree
single = original.copy()
new_group.node_tree = single
# récupération de la node image
for node in new_group.node_tree.nodes:
if node.type == 'TEX_IMAGE':
img_node = node
img_node.image = mask_img
# auto mix si demandé
previous_node = temp['previous_node']
if previous_node and self.option != 'Painted Mask':
for o in previous_node.outputs:
if len(o.links):
previous_node_linked_to = {'output':o,
'target':to_what(previous_node, o.name)}
break
# shift → multiply
if self.option == 'MULTIPLY':
operation_node = import_node_group('Multiply')
# alt → substract
if self.option == 'DIFFERENCE':
operation_node = import_node_group('Difference')
# relie la previous node au multiply
link(active_material, previous_node, previous_node.outputs[0].name,
operation_node, operation_node.inputs[0].name)
# relie le multiply à ce à quoi la previous était reliée
link(active_material, operation_node, operation_node.outputs[0].name,
previous_node_linked_to['target']['node'][0], previous_node_linked_to['target']
['socket'][0])
# relie le painted mask au multiply
link(active_material, new_group, 'Mask', operation_node,
operation_node.inputs[1].name)
# repositionnement
operation_node.location = previous_node.location
# previous_node.location[1] -=
previous_node.dimensions[1]*(1/context.preferences.system.ui_scale)
previous_node.location[1] +=
previous_node.dimensions[1]*(1/context.preferences.system.ui_scale)
new_group.location[1] = previous_node.location[1]
deselect_nodes(nodes)
nodes.active = img_node
img_node.select = True
area = next(area for area in bpy.context.screen.areas if area.type ==
'VIEW_3D')
space = next(space for space in area.spaces if space.type == 'VIEW_3D')
space.shading.type = 'MATERIAL'
bpy.ops.object.mode_set(mode='TEXTURE_PAINT')
# sélectionne le slot à peindre
for idx, img in enumerate(active_material.texture_paint_images):
if img == mask_img:
active_material.paint_active_slot = idx
# connect directement la node painted mask à la sortie pour visualisation
et chargement rapide.
link(active_material, new_group, 'Mask', material_output, 0)
return{'FINISHED'}

class FLUENT_SHADER_OT_EditPaintedMask(Operator):
"""编辑选择的绘制蒙版"""
bl_idname = 'fluent.editpaintedmask'
bl_category = 'Node'
bl_label = '编辑已绘制的蒙版'

def execute(self, context):


active_obj = active_object('GET')
material = bpy.context.active_object.active_material
tree = material.node_tree
nodes = tree.nodes
material_output = nodes.get('Material Output')
if nodes.active and nodes.active.select:
previous_node = nodes.active
else:
bpy.context.window_manager.popup_menu(make_oops(['Painted mask edition
function.', 'Select a painted mask node before.']), title="INFO", icon='INFO')
return{'FINISHED'}
if 'Painted Mask' in previous_node.name:
for n in previous_node.node_tree.nodes:
if n.type == 'TEX_IMAGE':
img_node = n
if img_node:
for idx, img in enumerate(material.texture_paint_images):
if img == img_node.image:
material.paint_active_slot = idx
# bpy.ops.node.nw_preview_node('INVOKE_DEFAULT')
link(material, previous_node, 'Mask', material_output, 0)
area = next(area for area in bpy.context.screen.areas if area.type ==
'VIEW_3D')
space = next(space for space in area.spaces if space.type == 'VIEW_3D')
space.shading.type = 'MATERIAL'
bpy.ops.object.mode_set(mode='TEXTURE_PAINT')

return{'FINISHED'}

class FLUENT_SHADER_OT_Refresh(Operator):
"""编辑选择的绘制蒙版"""
bl_idname = 'fluent.refresh'
bl_label = '刷新 Cycles 视口'

def execute(self, context):


bpy.context.space_data.shading.type = 'SOLID'
bpy.context.space_data.shading.type = 'RENDERED'
return{'FINISHED'}

class FLUENT_SHADER_OT_NewDecal(Operator):
"""添加贴花节点"""
bl_idname = 'fluent.newdecal'
bl_label = '添加贴花节点'

image_path: bpy.props.StringProperty()
duplicate: bpy.props.BoolProperty()

def invoke(self, context, event):


# vérifications
obj = active_object(action='GET')
if obj:
decal_image = None
active_material = obj.active_material
nodes = get_nodes(active_material)
if self.duplicate:
active_node = nodes.active
if 'Decal' in active_node.node_tree.name and not 'Mixer' in
active_node.node_tree.name:
decal_image = active_node.node_tree.nodes['Image
Texture'].image
previous_empty = active_node.node_tree.nodes['Texture
Coordinate'].object
else:
return{'FINISHED'}
bpy.ops.fluent.add_nodes('INVOKE_DEFAULT', choice='Decal')

global temp
decal_node = temp['last_addition']
original = decal_node.node_tree
single = original.copy()
decal_node.node_tree = single

bpy.ops.object.empty_add(type='SINGLE_ARROW', align='WORLD')
empty = active_object(action='GET')
empty['fluent_type'] = 'decal_projector'
if 'previous_empty' in locals():
empty.scale = previous_empty.scale
empty.rotation_euler = previous_empty.rotation_euler
empty.location = previous_empty.location

# set l'empty dans l'uv coordinate


decal_node.node_tree.nodes['Texture Coordinate'].object = empty

if not decal_image:
decal_image = bpy.data.images.load(filepath=self.image_path,
check_existing=False)
decal_node.node_tree.nodes['Image Texture'].image = decal_image

if not self.duplicate:
ratio = decal_image.size[1]/decal_image.size[0]
empty.scale[0] = 1/ratio

active_object(action='SET', obj=empty, solo=True)


context.scene.tool_settings.snap_elements = {'FACE'}
context.scene.tool_settings.snap_target = 'CENTER'
context.scene.tool_settings.use_snap_align_rotation = True

return{'FINISHED'}

class FLUENT_SHADER_OT_Localmask(Operator):
"""添加本地蒙版节点"""
bl_idname = 'fluent.localmask'
bl_label = '添加本地蒙版节点'

def invoke(self, context, event):


# vérifications
obj = active_object(action='GET')
if obj:
active_material = obj.active_material
nodes = get_nodes(active_material)
bpy.ops.fluent.add_nodes('INVOKE_DEFAULT', choice='Local mask')

global temp
decal_node = temp['last_addition']
original = decal_node.node_tree
single = original.copy()
decal_node.node_tree = single

bpy.ops.object.empty_add(type='SINGLE_ARROW', align='WORLD')
empty = active_object(action='GET')
empty['fluent_type'] = 'decal_projector'

# set l'empty dans l'uv coordinate


decal_node.node_tree.nodes['Texture Coordinate'].object = empty
active_object(action='SET', obj=empty, solo=True)
context.scene.tool_settings.snap_elements = {'FACE'}
context.scene.tool_settings.snap_target = 'CENTER'
context.scene.tool_settings.use_snap_align_rotation = True

return{'FINISHED'}

#################################################################################
class FluentAddonPreferences(AddonPreferences):
bl_idname = __name__

def draw(self, context):


wm = context.window_manager
layout = self.layout
AddonKeymaps.draw_keymap_items(wm, layout)

class AddonKeymaps:
_addon_keymaps = []
_keymaps = {}

@classmethod
def new_keymap(cls, name, kmi_name, kmi_value=None, km_name='3D View',
space_type="VIEW_3D", region_type="WINDOW",
event_type=None, event_value=None, ctrl=False, shift=False,
alt=False, key_modifier="NONE"):
"""
Adds a new keymap
:param name: str, Name that will be displayed in the panel preferences
:param kmi_name: str
- bl_idname for the operators (exemple: 'object.cube_add')
- 'wm.call_menu' for menu
- 'wm.call_menu_pie' for pie menu
:param kmi_value: str
- class name for Menu or Pie Menu
- None for operators
:param km_name: str, keymap name (exemple: '3D View Generic')
:param space_type: str, space type keymap is associated with, see:
https://docs.blender.org/api/current/bpy.types.KeyMap.html?
highlight=space_type#bpy.types.KeyMap.space_type
:param region_type: str, region type keymap is associated with, see:
https://docs.blender.org/api/current/bpy.types.KeyMap.html?
highlight=region_type#bpy.types.KeyMap.region_type
:param event_type: str, see:
https://docs.blender.org/api/current/bpy.types.Event.html?
highlight=event#bpy.types.Event.type
:param event_value: str, type of the event, see:
https://docs.blender.org/api/current/bpy.types.Event.html?
highlight=event#bpy.types.Event.value
:param ctrl: bool
:param shift: bool
:param alt: bool
:param key_modifier: str, regular key pressed as a modifier
https://docs.blender.org/api/current/bpy.types.KeyMapItem.html?
highlight=modifier#bpy.types.KeyMapItem.key_modifier
:return:
"""
cls._keymaps.update({name: [kmi_name, kmi_value, km_name, space_type,
region_type, event_type, event_value,
ctrl, shift, alt, key_modifier]
})

@classmethod
def add_hotkey(cls, kc, keymap_name):

items = cls._keymaps.get(keymap_name)
if not items:
return

kmi_name, kmi_value, km_name, space_type, region_type = items[:5]


event_type, event_value, ctrl, shift, alt, key_modifier = items[5:]
km = kc.keymaps.new(name=km_name, space_type=space_type,
region_type=region_type)

kmi = km.keymap_items.new(kmi_name, event_type, event_value,


ctrl=ctrl,
shift=shift, alt=alt,
key_modifier=key_modifier
)
if kmi_value:
kmi.properties.name = kmi_value

kmi.active = True

cls._addon_keymaps.append((km, kmi))

@staticmethod
def register_keymaps():
wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
# In background mode, there's no such thing has keyconfigs.user,
# because headless mode doesn't need key combos.
# So, to avoid error message in background mode, we need to check if
# keyconfigs is loaded.
if not kc:
return

for keymap_name in AddonKeymaps._keymaps.keys():


AddonKeymaps.add_hotkey(kc, keymap_name)

@classmethod
def unregister_keymaps(cls):
kmi_values = [item[1] for item in cls._keymaps.values() if item]
kmi_names = [item[0] for item in cls._keymaps.values() if
item not in ['wm.call_menu', 'wm.call_menu_pie']]

for km, kmi in cls._addon_keymaps:


# remove addon keymap for menu and pie menu
if hasattr(kmi.properties, 'name'):
if kmi_values:
if kmi.properties.name in kmi_values:
km.keymap_items.remove(kmi)

# remove addon_keymap for operators


else:
if kmi_names:
if kmi.idname in kmi_names:
km.keymap_items.remove(kmi)

cls._addon_keymaps.clear()

@staticmethod
def get_hotkey_entry_item(name, kc, km, kmi_name, kmi_value, col):

# for menus and pie_menu


if kmi_value:
for km_item in km.keymap_items:
if km_item.idname == kmi_name and km_item.properties.name ==
kmi_value:
col.context_pointer_set('keymap', km)
rna_keymap_ui.draw_kmi([], kc, km, km_item, col, 0)
return

col.label(text=f"No hotkey entry found for {name}")


col.operator(TEMPLATE_OT_restore_hotkey.bl_idname,
text="恢复键盘映射",
icon='ADD').km_name = km.name

# for operators
else:
if km.keymap_items.get(kmi_name):
col.context_pointer_set('keymap', km)
rna_keymap_ui.draw_kmi([], kc, km, km.keymap_items[kmi_name],
col, 0)

else:
col.label(text=f"No hotkey entry found for {name}")
col.operator(TEMPLATE_OT_restore_hotkey.bl_idname,
text="恢复键盘映射",
icon='ADD').km_name = km.name

@staticmethod
def draw_keymap_items(wm, layout):
kc = wm.keyconfigs.user

box = layout.box()
for name, items in AddonKeymaps._keymaps.items():
kmi_name, kmi_value, km_name = items[:3]
split = box.split()
col = split.column()
# col.label(text=name)
# col.separator()
km = kc.keymaps[km_name]
AddonKeymaps.get_hotkey_entry_item(name, kc, km, kmi_name,
kmi_value, col)

class TEMPLATE_OT_restore_hotkey(Operator):
bl_idname = "template.restore_hotkey"
bl_label = "恢复热键"
bl_options = {'REGISTER', 'INTERNAL'}

km_name: StringProperty()

def execute(self, context):


context.preferences.active_section = 'KEYMAP'
wm = context.window_manager
kc = wm.keyconfigs.addon
km = kc.keymaps.get(self.km_name)
if km:
km.restore_to_default()
context.preferences.is_dirty = True
context.preferences.active_section = 'ADDONS'
return {'FINISHED'}

class FLUENT_SHADER_OT_FindHelp(Operator):
"""如何寻求帮助"""
bl_idname = 'fluent.findhelp'
bl_label = '如何寻求帮助'

def execute(self, context):


bpy.context.window_manager.popup_menu(make_oops(['Hold Shift+Ctrl+Alt when
you click on button to display the documentation about it.']), title="Find help.",
icon='INFO')
return{'FINISHED'}
#################################################################################

class FLUENT_SHADER_OT_OpenFilebrowser(Operator, ImportHelper):


bl_idname = "fluent.open_filebrowser"
bl_label = "打开文件浏览器 (yay)"

filter_glob: StringProperty(
default='*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp',
options={'HIDDEN'}
)

continue_to: StringProperty(
default='',
options={'HIDDEN'}
)

# some_boolean: BoolProperty(
# name='Do a thing',
# description='Do a thing with the file you\'ve selected',
# default=True,
# )

def execute(self, context):


"""选择一个文件"""

filename, extension = os.path.splitext(self.filepath)

if self.continue_to == 'new_decal':
bpy.ops.fluent.newdecal('INVOKE_DEFAULT', image_path=self.filepath)

return {'FINISHED'}

classes = (
FluentShaderProps,
FLUENT_SHADER_PT_ViewPanel,
FLUENT_SHADER_PT_Library,
FLUENT_SHADER_MT_Pie_Menu,
FLUENT_MT_Color_Menu,
FLUENT_SHADER_OT_addNodes,
FLUENT_OT_BakeMaps,
FLUENT_OT_BakeMask,
FLUENT_SHADER_OT_NodeCounter,
FLUENT_SHADER_OT_ImageExtractor,
FLUENT_SHADER_PT_Tool,
FLUENT_SHADER_OT_SwapLayers,
FLUENT_SHADER_OT_LayerMixLayersConnect,
FLUENT_SHADER_OT_NewPaintedMask,
FLUENT_SHADER_OT_EditPaintedMask,
FLUENT_SHADER_OT_NewDecal,
FLUENT_SHADER_OT_Localmask,
FLUENT_SHADER_OT_Refresh,
FLUENT_SHADER_OT_FindHelp,
FLUENT_SHADER_OT_OpenFilebrowser,
FluentAddonPreferences
)

def register():
global preview_collections
from bpy.utils import register_class
for cls in classes:
register_class(cls)

bpy.types.Scene.FluentShaderProps = bpy.props.PointerProperty( type =


FluentShaderProps )

AddonKeymaps.new_keymap('Fluent Shader', 'wm.call_menu_pie',


'FLUENT_SHADER_MT_Pie_Menu',
'Node Generic', 'NODE_EDITOR', 'WINDOW', 'F',
'PRESS', False, False, False, 'NONE'
)
AddonKeymaps.new_keymap('Refresh', 'fluent.refresh', None,
'3D View Generic', 'VIEW_3D', 'WINDOW', 'GRLESS',
'PRESS', True, False, False, 'NONE'
)
AddonKeymaps.register_keymaps()

def unregister():
AddonKeymaps.unregister_keymaps()
from bpy.utils import unregister_class
for cls in reversed(classes):
unregister_class(cls)

for pcoll in preview_collections.values():


bpy.utils.previews.remove(pcoll)
preview_collections.clear()

if __name__ == "__main__":
register()

You might also like