import bpy, bmesh
import random
from mathutils import Color
from . import infobar
from . materials.capaint import carpaint_material
from . materials.emission_glow import emission_glow_material
from ... utility import addon
from ... ui_framework.master import Master
from ... utility.base_modal_controls import Base_Modal_Controls
# Cursor Warp imports
from ... utils.toggle_view3d_panels import collapse_3D_view_panels
from ... utils.modal_frame_drawing import draw_modal_frame
from ... utils.cursor_warp import mouse_warp
from ... utility import method_handler
class HOPS_OT_MaterialScroll(bpy.types.Operator):
bl_idname = "hops.material_scroll"
bl_label = "Material Scroll"
bl_options = {'REGISTER', 'UNDO', 'GRAB_CURSOR'}
bl_description = """Interactively scroll through materials
Ctrl + LMB - Blank material scroll
(Generate new random material each scroll)
Shift + LMB - Non-Destructive material scroll
(non-destructive / keeps indice connections)
Press H for help
"""
@classmethod
def poll(cls, context):
obj = context.active_object
return obj and obj.type in {'MESH', 'CURVE', 'SURFACE', 'META', 'FONT'} and obj.mode in {'OBJECT', 'EDIT'}
def invoke(self, context, event):
self.copy_view = addon.preference().behavior.mat_viewport
#self.scroll_modes = {'NORMAL', 'BLANK', 'DESTRUCTIVE'} enum prop placeholder
self.scroll_mode = 'NORMAL'
if event.ctrl:
self.scroll_mode = 'BLANK'
elif event.shift:
self.scroll_mode = 'DESTRUCTIVE'
hide_flag = not context.preferences.filepaths.show_hidden_files_datablocks
self.materials = [mat.name for mat in bpy.data.materials if not mat.grease_pencil and (not mat.name.startswith(".") or hide_flag)]
if self.scroll_mode != 'BLANK' and not self.materials:
self.report({'INFO'}, "No materials found")
return {'CANCELLED'}
self.overlay_orig = context.space_data.overlay.show_overlays
context.space_data.overlay.show_overlays = False
self.mode = context.mode
self.active = context.active_object
self.edit_mode_init = False
if self.mode == 'EDIT_MESH':
self.active.select_set(True)
self.edit_mode_init = True
bpy.ops.object.mode_set(mode = 'OBJECT')
self.objects = [obj for obj in context.selected_objects if obj.type in {'MESH', 'CURVE', 'SURFACE', 'META', 'FONT'} ]
if not self.objects:
self.report({'INFO'}, "No valid object is selected")
return {'CANCELLED'}
#grab whatever object if active was excluded from valid objects, for whatever reason
if self.active not in self.objects:
self.active = self.objects[0]
#backup and junk
self.slot_backup = dict()
self.created_mats = set()
self.created_slots = []
self.data_index_back = dict()
#sroll stuff
self.material_types = ['PRINCIPLED', 'CARPAINT', 'EMISSION']
self.material_type = 'PRINCIPLED'
self.force_enable = set()
self.slot_modes = ['ACTIVE', 'ALL', 'NOT ACTIVE']
self.slot_index =0
self.slot_mode = 'ACTIVE'
self.enable_color = False
self.obj_slots = {}
#mappring for material slots to object index, their original material, and current material index
for o in self.objects:
back = [s.material.name if s.material else None for s in o.material_slots ]
self.slot_backup.update ( {o:back} )
if self.mode == 'EDIT_MESH' and self.scroll_mode == 'BLANK' :
self.data_index_back.update( {o:[p.material_index for p in o.data.polygons]} )
self.random_mat()
o.data.materials.append(bpy.data.materials[ self.materials[-1] ])
index = len (o.data.materials) -1
for polygon in o.data.polygons:
if polygon.select:
polygon.material_index = index
o.active_material_index = index
self.created_slots.append(o)
if not o.material_slots:
o.data.materials.append(None)
self.created_slots.append(o)
piece = [self.materials.index(s.material.name) if s.material else 0 for s in o.material_slots ]
self.obj_slots.update({o:piece})
# filepathprefs = bpy.context.preferences.filepaths
# for material in [mat for mat in bpy.data.materials if not mat.name.startswith('.')] if filepathprefs.show_hidden_files_datablocks else bpy.data.materials:
# self.materials.append(material)
self.exit_to_map_scroll = False
# Base Systems
self.master = Master(context=context)
self.master.only_use_fast_ui = True
self.base_controls = Base_Modal_Controls(context, event)
self.original_tool_shelf, self.original_n_panel = collapse_3D_view_panels()
self.draw_handle = bpy.types.SpaceView3D.draw_handler_add(self.safe_draw_shader, (context,), 'WINDOW', 'POST_PIXEL')
context.window_manager.modal_handler_add(self)
#infobar.initiate(self)
return {"RUNNING_MODAL"}
def modal(self, context, event):
# Base Systems
self.master.receive_event(event=event)
self.base_controls.update(context, event)
mouse_warp(context, event)
if self.base_controls.pass_through:
return {'PASS_THROUGH'}
elif event.type == 'Z' and (event.shift or event.alt):
return {'PASS_THROUGH'}
elif self.base_controls.confirm:
if self.overlay_orig:
context.space_data.overlay.show_overlays = True
self.remove_shader()
collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
self.master.run_fade()
self.clean_mats()
self.report({'INFO'}, "Finished")
if self.edit_mode_init:
bpy.ops.object.mode_set(mode = 'EDIT')
if self.exit_to_map_scroll:
bpy.ops.hops.map_scroll('INVOKE_DEFAULT')
return {'FINISHED'}
elif self.base_controls.cancel:
self.restore_slots()
if self.overlay_orig:
context.space_data.overlay.show_overlays = True
self.remove_shader()
collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
self.master.run_fade()
self.clean_mats()
self.report({'INFO'}, "Cancelled")
if self.edit_mode_init:
bpy.ops.object.mode_set(mode = 'EDIT')
return {'CANCELLED'}
elif self.base_controls.scroll or (event.type in {'Z', 'X'} and event.value == 'PRESS'):
direction = -1 if (event.type in {'X'} or self.base_controls.scroll == -1) else 1
if event.shift:
if event.type in {'NUMPAD_PLUS', 'EQUAL','NUMPAD_MINUS', 'MINUS'}:
direction*=-1
for o in self.objects:
max_index = len (o.material_slots) -1
index = o.active_material_index
index -= direction
index = 0 if index > max_index else index
index = max_index if index < 0 else index
o.active_material_index = index
else:
self.material_scroll( direction = direction)
elif event.type == 'T' and event.value == 'PRESS': #and self.blank_material_scroll:
if self.scroll_mode == 'NORMAL':
self.scroll_mode = 'BLANK'
#types = ['PRINCIPLED', 'EMISSION', 'GLASS', 'CARPAINT']
types = self.material_types
self.material_type = types[(types.index(self.material_type) + 1) % len(types)]
self.report({'INFO'}, f"Material Type: {self.material_type.capitalize()}")
elif event.type == 'B' and event.value == 'PRESS':
self.scroll_mode = 'BLANK' if self.scroll_mode != 'BLANK' else 'NORMAL'
self.report({'INFO'}, f"Scroll Mode: {self.scroll_mode}")
elif event.type == 'D' and event.value == 'PRESS':
self.scroll_mode = 'DESTRUCTIVE' if self.scroll_mode != 'DESTRUCTIVE' else 'NORMAL'
self.report({'INFO'}, f"Scroll Mode: {self.scroll_mode}")
elif event.type == 'R' and event.value == 'PRESS':
if self.scroll_mode != 'DESTRUCTIVE':
self.randomize_mats()
self.report({'INFO'}, "Randmized ALL")
elif event.type == 'A' and event.value == 'PRESS':
self.slot_index = self.slot_index +1 if self.slot_index<2 else 0
self.slot_mode = self.slot_modes[self.slot_index]
self.report({'INFO'}, F"Affected slots: {self.slot_mode}")
elif self.scroll_mode != 'NORMAL' and event.type == 'C' and event.value == 'PRESS':
self.enable_color = not self.enable_color
msg = "ON" if self.enable_color else "OFF"
self.report({'INFO'}, F"COLOR: {msg}")
elif self.base_controls.tilde and event.shift == True:
context.space_data.overlay.show_overlays = not context.space_data.overlay.show_overlays
elif event.type == 'ONE' and event.value == 'PRESS':
if 0 not in self.force_enable :
self.force_enable.add(0)
else :
self.force_enable.discard(0)
elif event.type == 'TWO' and event.value == 'PRESS':
if 1 not in self.force_enable :
self.force_enable.add(1)
else :
self.force_enable.discard(1)
elif event.type == 'THREE' and event.value == 'PRESS':
if 2 not in self.force_enable :
self.force_enable.add(2)
else :
self.force_enable.discard(2)
elif event.type == 'FOUR' and event.value == 'PRESS':
if 3 not in self.force_enable :
self.force_enable.add(3)
else :
self.force_enable.discard(3)
elif event.type == 'V' and event.value == 'PRESS':
self.clean_mats()
context.area.header_text_set(text=None)
self.remove_shader()
collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
self.master.run_fade()
bpy.ops.hops.adjust_viewport('INVOKE_DEFAULT')
return {'FINISHED'}
# Map Scroll
elif event.type == 'M' and event.value == 'PRESS':
self.exit_to_map_scroll = not self.exit_to_map_scroll
msg = 'Exit to Map Scroll : ON' if self.exit_to_map_scroll else 'Exit to Map Scroll : OFF'
bpy.ops.hops.display_notification(info=msg)
self.draw_master(context=context)
context.area.tag_redraw()
return {"RUNNING_MODAL"}
def safe_draw_shader(self, context):
method_handler(self.draw_shader,
arguments = (context,),
identifier = 'UI Framework',
exit_method = self.remove_shader)
def remove_shader(self):
'''Remove shader handle.'''
if self.draw_handle:
self.draw_handle = bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle, "WINDOW")
def draw_shader(self, context):
'''Draw shader handle.'''
draw_modal_frame(context)
# def remove_slot(self):
# if self.active.mode == 'EDIT':
# bpy.ops.object.editmode_toggle()
# bpy.ops.object.material_slot_remove()
# bpy.ops.object.editmode_toggle()
def material_scroll(self, direction =0):
max_index = len(self.materials) -1
if max_index < 0:
if self.scroll_mode == 'BLANK' and direction > 0:
max_index = 0
else:
return
dup_filter =set()
for obj, slot_data in self.obj_slots.items():
allowed = set( range( len(obj.material_slots) ) )
if self.slot_mode == 'ACTIVE':
allowed = {obj.active_material_index}
allowed.update(self.force_enable)
elif self.slot_mode == "NOT ACTIVE" :
allowed.discard (obj.active_material_index)
for index , slot in enumerate(obj.material_slots):
if index not in allowed:
continue
if self.scroll_mode == 'DESTRUCTIVE':
if slot.material:
if slot.material.name not in dup_filter:
dup_filter.add (slot.material.name)
else:
continue
self.random_mat(slot.material)
else:
self.random_mat()
slot_data[index] = len(self.materials) -1
slot.material = bpy.data.materials[self.materials[-1]]
continue
if self.scroll_mode == 'BLANK' and (slot_data[index]+direction>max_index):# direction ==1:
self.random_mat()
slot_data[index] = len(self.materials) -1
slot.material = bpy.data.materials[self.materials[-1]]
continue
scroll = slot_data[index] +direction
scroll = 0 if scroll > max_index else scroll
scroll = max_index if scroll<0 else scroll
slot_data[index] = scroll
slot.material = bpy.data.materials[self.materials[scroll]]
def randomize_mats(self):
max_index = len(self.materials) -1
for obj, slot_data in self.obj_slots.items():
allowed = set( range( len(obj.material_slots) ) )
if self.slot_mode == 'ACTIVE':
allowed = {obj.active_material_index}
allowed.update(self.force_enable)
elif self.slot_mode == "NOT ACTIVE" :
allowed.discard (obj.active_material_index)
for index , slot in enumerate(obj.material_slots):
if index not in allowed:
continue
mat_index = random.randint(0, max_index)
slot_data[index] = mat_index
slot.material = bpy.data.materials[ self.materials[mat_index] ]
def clean_mats(self):
for mat_name in self.created_mats:
material = bpy.data.materials[mat_name]
if not material.users:
bpy.data.materials.remove(material )
del material
del self.slot_backup
del self.created_mats
del self.created_slots
del self.materials
del self.data_index_back
del self.objects
def restore_slots(self):
for obj in self.objects:
back_list = self.slot_backup [obj]
for slot, mat_name in zip(obj.material_slots , back_list ):
slot.material = bpy.data.materials[mat_name] if mat_name else None
if self.data_index_back:
for obj, index_list in self.data_index_back.items():
for poly , index in zip (obj.data.polygons , index_list):
poly.material_index = index
for obj in self.created_slots:
obj.data.materials.pop()
def draw_master(self, context):
# Start
self.master.setup()
########################
# Fast UI
########################
if self.master.should_build_fast_ui():
mods_list = list(reversed([(i+1, mat.name) for i, mat in enumerate(self.active.material_slots)]))
active_mod = self.active.active_material_index +1
active_slot = F"{active_mod}/{len(self.active.material_slots)}"
force_enabld = ''
if not len (self.force_enable):
force_enabld = None
else:
force_enabld = sorted([e+1 for e in self.force_enable])
#Main
win_list = []
if addon.preference().ui.Hops_modal_fast_ui_loc_options != 1: #Fast Floating
win_list.append(len(self.materials))
if self.scroll_mode != 'NORMAL':
win_list.append(self.material_type)
else:
win_list.append(self.scroll_mode)
win_list.append(F'{self.slot_mode} : {active_slot}')
win_list.append(force_enabld)
else:
win_list.append(F'Materials: {len(self.materials)}')
win_list.append(self.scroll_mode)
if self.scroll_mode != 'NORMAL':
win_list.append(F"Type : {self.material_type} ")
if self.material_type == 'PRINCIPLED':
msg = "ON" if self.enable_color else "OFF"
win_list.append(F"Color :{msg} ")
win_list.append(F"Slots: {self.slot_mode}")
win_list.append(F"Active Slot: {active_slot}")
if self.slot_mode != 'ALL':
win_list.append(F"Force Enabled: {force_enabld}" )
#win_list.append(f"{self.index}")
# win_list.append(f"{self.materials[self.index].name}")
# Help
help_items = {"GLOBAL" : [], "STANDARD" : []}
help_items["GLOBAL"] = [
("M", "Toggle mods list"),
("H", "Toggle help"),
("~", "Toggle UI Display Type"),
("O", "Toggle viewport rendering")]
help_items["STANDARD"] = [
("Shift+Scroll / Z / X", "Increment Active Slot"),
("Scroll", "Increment Material"),
("Z / X", "Increment Material"),
("M", "Map Scroll Exit"),
("V", "Viewport Scroll Exit"),
("A", "Toggle affected slots"),
("R", "Randomize ALL"),
("C", "Toggle Color"),
("D", "Toggle Non-Destructive Blank Scroll"),
("B", "Toggle Blank Material Scroll"),
("T", "Cycle Blank Material Type")]
if self.slot_mode == 'ACTIVE':
help_items["STANDARD"].append(["1-4", "Force Enable Slots"])
# Mods
# active_mod = ""
# filepathprefs = bpy.context.preferences.filepaths
# for material in [mat for mat in bpy.data.materials if not mat.name.startswith('.')] if filepathprefs.show_hidden_files_datablocks else bpy.data.materials:
# if self.materials[self.index] == material:
# active_mod = self.materials[self.index].name
# mods_list.append([material.name_full, ""])
self.master.receive_fast_ui(
win_list=win_list,
help_list=help_items,
image="InteractiveBoolean",
mods_list=mods_list,
active_mod_name=active_mod,
mods_label_text="Materials",
number_mods=False)
# Finished
self.master.finished()
def random_mat(self , material = None):
color = 1 if self.enable_color else 0
if self.material_type == 'PRINCIPLED':
new_mat = random_principled(material=material, color_prob=color, copy_view = self.copy_view)
elif self.material_type == 'CARPAINT':
new_mat = random_carpaint(material=material, copy_view = self.copy_view)
elif self.material_type == 'EMISSION':
new_mat = random_emit(material=material, copy_view = self.copy_view)
#new_mat.node_tree.nodes.update()
if self.scroll_mode != 'DESTRUCTIVE':
self.created_mats.add(new_mat.name)
self.materials.append(new_mat.name)
def random_principled (material = None, name = 'Material', metal_prob = 0.8, rough_min = 0.01,
rough_max = 0.4, color_prob = 0.2, clear_prob = 0.2,
val_min = 0.1, val_max =0.8, copy_view = False ):
material, principled = add_pricipled(material=material)
metal = 1 if roll(metal_prob) else 0
clearcoat = 1 if roll(clear_prob) else 0
clearcoat_rough = random.uniform(rough_min, rough_max) if clearcoat else 0
roughness = random.uniform(rough_min, rough_max)
roll_color = roll (color_prob)
if roll_color:
color = random_color (col_min = 0.1, col_max = 0.7)
else:
color = random_color (color= False, grey_min = 0.1, grey_max= 0.4 )
#viewport
if copy_view:
material.diffuse_color = color
material.metallic = metal
material.roughness = roughness
else:
material.diffuse_color = random_color()
principled.inputs['Base Color'].default_value = color
principled.inputs['Metallic'].default_value = metal
principled.inputs['Roughness'].default_value = roughness
if bpy.app.version[0] < 4:
principled.inputs['Clearcoat'].default_value = clearcoat
principled.inputs['Clearcoat Roughness'].default_value = clearcoat_rough
else:
principled.inputs['Coat Weight'].default_value = clearcoat
principled.inputs['Coat Roughness'].default_value = clearcoat_rough
return material
def random_carpaint(material = None, name = 'Carpaint', metal_prob = 0.8, rough_min = 0.01,
rough_max = 0.4, color_prob = 1, clear_prob = 0.2, copy_view = False):
material, carpaint_shader = carpaint_material(material = material)
metal = 1 if roll(metal_prob) else 0
clearcoat = 1 if roll(clear_prob) else 0
clearcoat_rough = random.uniform(rough_min, rough_max) if clearcoat else 0
roughness = random.uniform(rough_min, rough_max)
color = random_color ()
#viewport
if copy_view:
material.diffuse_color = color
material.metallic = metal
material.roughness = roughness
else:
material.diffuse_color = random_color()
carpaint_shader.inputs["Hue Variation"].default_value = random.uniform(0,1)
carpaint_shader.inputs["Hue Shift Base Value"].default_value = 0.5
carpaint_shader.inputs["Saturation Variation"].default_value = random.uniform(0,1)
carpaint_shader.inputs["Saturation Base Value"].default_value = 1.0
carpaint_shader.inputs["Brightness Variation"].default_value = random.uniform(0,1)
carpaint_shader.inputs["Brightness Value"].default_value = random.uniform(0,1)
carpaint_shader.inputs["Metallic"].default_value = random.randint(0,1)
carpaint_shader.inputs["Flake Roughness Minimum"].default_value = 0.23999999463558197
carpaint_shader.inputs["Flake Roughness Maximum"].default_value = 0.8799999952316284
carpaint_shader.inputs["Flake Scale"].default_value = 4000.0
carpaint_shader.inputs["Clearcoat"].default_value = clearcoat
carpaint_shader.inputs["Clearcoat Roughness"].default_value = clearcoat_rough
carpaint_shader.inputs["Randomness"].default_value = 0.0
return material
def random_emit(material = None, name = 'Emission', pulse = True ,
cycle_count = 4, copy_view = False):
material, emission_glow = emission_glow_material(material=material)
cycle_count = cycle_count if pulse else 0
col = Color()
h = random.uniform(0,1)
s = random.uniform(0,1)
v = 1
col.hsv = (h,s,v)
color1 =[col.r, col.g, col.b, 1]
emit_multi = random.uniform(1, 10)
#viewport
if copy_view:
material.diffuse_color = color1
material.metallic = 0
material.roughness = 0
else:
material.diffuse_color = random_color()
emission_glow.inputs["Cycle Count"].default_value = cycle_count
emission_glow.inputs["Transition Sharp"].default_value = 1.0
emission_glow.inputs["Emit Multiplier"].default_value = emit_multi
emission_glow.inputs["Color1"].default_value = color1
emission_glow.inputs["Color2"].default_value = [0.0, 0.0, 0.0, 1.0]
emission_glow.inputs["Func Offset (deg)"].default_value = 0.0
emission_glow.inputs["Emit Offset"].default_value = 0.0
emission_glow.inputs["Color Blend"].default_value = 0.0
return material
def add_pricipled (material = None, name = "Material"):
if not material:
material = bpy.data.materials.new (name)
material.use_nodes = True
material_nodes = material.node_tree.nodes
material_nodes.clear()
output = material_nodes.new(type="ShaderNodeOutputMaterial")
principled = material_nodes.new("ShaderNodeBsdfPrincipled")
material.node_tree.links.new(principled.outputs[0], output.inputs[0])
principled.location = [-300, output.location[1]]
return (material, principled)
def roll (chance):
return random.uniform(0,1) <=chance
def random_color(color = True, col_min = 0.1, col_max = 0.8, grey_min = 0.1 , grey_max = 0.4):
if color :
r = random.uniform(col_min, col_max)
g = random.uniform(col_min, col_max)
b = random.uniform(col_min, col_max)
else:
r = g = b = random.uniform (grey_min, grey_max)
return [r , g, b, 1]