Relative imports are intentionally avoided in UI code (`scripts/startup/bl_ui`) because it makes it impossible to run individual files in isolation. Common workflow for making mock-ups and quick UI edits is to load the UI file in Blender's text editor with "Edit Source" operator, make changes, and run the script to see the changes. But in the current version it's impossible because files include relative imports, which don't allow Python to run scripts individually. Pull Request: https://projects.blender.org/blender/blender/pulls/138246
483 lines
14 KiB
Python
483 lines
14 KiB
Python
# SPDX-FileCopyrightText: 2009-2023 Blender Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
import bpy
|
|
from bpy.types import Menu, Panel, UIList
|
|
from bpy.app.translations import contexts as i18n_contexts
|
|
from rna_prop_ui import PropertyPanel
|
|
from bpy_extras.node_utils import find_node_input
|
|
|
|
from bl_ui.space_properties import PropertiesAnimationMixin
|
|
|
|
|
|
class MATERIAL_MT_context_menu(Menu):
|
|
bl_label = "Material Specials"
|
|
|
|
def draw(self, _context):
|
|
layout = self.layout
|
|
|
|
layout.operator("material.copy", icon='COPYDOWN')
|
|
layout.operator("object.material_slot_copy")
|
|
layout.operator("material.paste", icon='PASTEDOWN')
|
|
layout.operator("object.material_slot_remove_unused")
|
|
|
|
|
|
class MATERIAL_UL_matslots(UIList):
|
|
|
|
def draw_item(self, _context, layout, _data, item, icon, _active_data, _active_propname, _index):
|
|
# assert(isinstance(item, bpy.types.MaterialSlot)
|
|
# ob = data
|
|
slot = item
|
|
ma = slot.material
|
|
|
|
layout.context_pointer_set("id", ma)
|
|
layout.context_pointer_set("material_slot", slot)
|
|
|
|
if self.layout_type in {'DEFAULT', 'COMPACT'}:
|
|
if ma:
|
|
layout.prop(ma, "name", text="", emboss=False, icon_value=icon)
|
|
else:
|
|
layout.label(text="", icon_value=icon)
|
|
elif self.layout_type == 'GRID':
|
|
layout.alignment = 'CENTER'
|
|
layout.label(text="", icon_value=icon)
|
|
|
|
|
|
class MaterialButtonsPanel:
|
|
bl_space_type = 'PROPERTIES'
|
|
bl_region_type = 'WINDOW'
|
|
bl_context = "material"
|
|
# COMPAT_ENGINES must be defined in each subclass, external engines can add themselves here
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
mat = context.material
|
|
return mat and (context.engine in cls.COMPAT_ENGINES) and not mat.grease_pencil
|
|
|
|
|
|
class MATERIAL_PT_preview(MaterialButtonsPanel, Panel):
|
|
bl_label = "Preview"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
COMPAT_ENGINES = {'BLENDER_EEVEE_NEXT'}
|
|
|
|
def draw(self, context):
|
|
self.layout.template_preview(context.material)
|
|
|
|
|
|
class MATERIAL_PT_custom_props(MaterialButtonsPanel, PropertyPanel, Panel):
|
|
COMPAT_ENGINES = {
|
|
'BLENDER_RENDER',
|
|
'BLENDER_EEVEE_NEXT',
|
|
'BLENDER_WORKBENCH',
|
|
}
|
|
_context_path = "material"
|
|
_property_type = bpy.types.Material
|
|
|
|
|
|
class EEVEE_MATERIAL_PT_context_material(MaterialButtonsPanel, Panel):
|
|
bl_label = ""
|
|
bl_context = "material"
|
|
bl_options = {'HIDE_HEADER'}
|
|
COMPAT_ENGINES = {
|
|
'BLENDER_EEVEE_NEXT',
|
|
'BLENDER_WORKBENCH',
|
|
}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
ob = context.object
|
|
mat = context.material
|
|
|
|
if mat and mat.grease_pencil:
|
|
return False
|
|
if ob and ob.type == 'GREASEPENCIL':
|
|
return False
|
|
|
|
return (
|
|
(ob or mat) and
|
|
(context.engine in cls.COMPAT_ENGINES)
|
|
)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
mat = context.material
|
|
ob = context.object
|
|
slot = context.material_slot
|
|
space = context.space_data
|
|
|
|
if ob:
|
|
is_sortable = len(ob.material_slots) > 1
|
|
rows = 3
|
|
if is_sortable:
|
|
rows = 5
|
|
|
|
row = layout.row()
|
|
|
|
row.template_list("MATERIAL_UL_matslots", "", ob, "material_slots", ob, "active_material_index", rows=rows)
|
|
|
|
col = row.column(align=True)
|
|
col.operator("object.material_slot_add", icon='ADD', text="")
|
|
col.operator("object.material_slot_remove", icon='REMOVE', text="")
|
|
|
|
col.separator()
|
|
|
|
col.menu("MATERIAL_MT_context_menu", icon='DOWNARROW_HLT', text="")
|
|
|
|
if is_sortable:
|
|
col.separator()
|
|
|
|
col.operator("object.material_slot_move", icon='TRIA_UP', text="").direction = 'UP'
|
|
col.operator("object.material_slot_move", icon='TRIA_DOWN', text="").direction = 'DOWN'
|
|
|
|
row = layout.row()
|
|
|
|
if ob:
|
|
row.template_ID(ob, "active_material", new="material.new")
|
|
|
|
if slot:
|
|
row.prop(slot, "link", icon_only=True)
|
|
|
|
if ob.mode == 'EDIT':
|
|
row = layout.row(align=True)
|
|
row.operator("object.material_slot_assign", text="Assign")
|
|
row.operator("object.material_slot_select", text="Select")
|
|
row.operator("object.material_slot_deselect", text="Deselect")
|
|
|
|
elif mat:
|
|
row.template_ID(space, "pin_id")
|
|
|
|
|
|
def panel_node_draw(layout, ntree, _output_type, input_name):
|
|
node = ntree.get_output_node('EEVEE')
|
|
|
|
if node:
|
|
input = find_node_input(node, input_name)
|
|
if input:
|
|
layout.template_node_view(ntree, node, input)
|
|
else:
|
|
layout.label(text="Incompatible output node")
|
|
else:
|
|
layout.label(text="No output node")
|
|
|
|
|
|
class EEVEE_MATERIAL_PT_surface(MaterialButtonsPanel, Panel):
|
|
bl_label = "Surface"
|
|
bl_context = "material"
|
|
COMPAT_ENGINES = {'BLENDER_EEVEE_NEXT'}
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
mat = context.material
|
|
|
|
if mat.use_nodes:
|
|
layout.use_property_split = True
|
|
panel_node_draw(layout, mat.node_tree, 'OUTPUT_MATERIAL', "Surface")
|
|
else:
|
|
layout.prop(mat, "use_nodes", icon='NODETREE')
|
|
layout.use_property_split = True
|
|
layout.prop(mat, "diffuse_color", text="Base Color")
|
|
layout.prop(mat, "metallic")
|
|
layout.prop(mat, "specular_intensity", text="Specular")
|
|
layout.prop(mat, "roughness")
|
|
|
|
|
|
class EEVEE_MATERIAL_PT_volume(MaterialButtonsPanel, Panel):
|
|
bl_label = "Volume"
|
|
bl_translation_context = i18n_contexts.id_id
|
|
bl_context = "material"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
COMPAT_ENGINES = {'BLENDER_EEVEE_NEXT'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
engine = context.engine
|
|
mat = context.material
|
|
return mat and mat.use_nodes and (engine in cls.COMPAT_ENGINES) and not mat.grease_pencil
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
layout.use_property_split = True
|
|
|
|
mat = context.material
|
|
|
|
panel_node_draw(layout, mat.node_tree, 'OUTPUT_MATERIAL', "Volume")
|
|
|
|
|
|
class EEVEE_MATERIAL_PT_displacement(MaterialButtonsPanel, Panel):
|
|
bl_label = "Displacement"
|
|
bl_context = "material"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
COMPAT_ENGINES = {'BLENDER_EEVEE_NEXT'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
engine = context.engine
|
|
mat = context.material
|
|
return mat and mat.use_nodes and (engine in cls.COMPAT_ENGINES) and not mat.grease_pencil
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
layout.use_property_split = True
|
|
|
|
mat = context.material
|
|
|
|
panel_node_draw(layout, mat.node_tree, 'OUTPUT_MATERIAL', "Displacement")
|
|
|
|
|
|
class EEVEE_MATERIAL_PT_thickness(MaterialButtonsPanel, Panel):
|
|
bl_label = "Thickness"
|
|
bl_translation_context = i18n_contexts.id_material
|
|
bl_context = "material"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
COMPAT_ENGINES = {'BLENDER_EEVEE_NEXT'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
engine = context.engine
|
|
mat = context.material
|
|
return mat and mat.use_nodes and (engine in cls.COMPAT_ENGINES) and not mat.grease_pencil
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
layout.use_property_split = True
|
|
|
|
mat = context.material
|
|
|
|
panel_node_draw(layout, mat.node_tree, 'OUTPUT_MATERIAL', "Thickness")
|
|
|
|
|
|
def draw_material_surface_settings(layout, mat, is_eevee=True):
|
|
col = layout.column(heading="Backface Culling")
|
|
col.prop(mat, "use_backface_culling", text="Camera")
|
|
col.prop(mat, "use_backface_culling_shadow", text="Shadow")
|
|
col.prop(mat, "use_backface_culling_lightprobe_volume", text="Light Probe Volume")
|
|
|
|
col = layout.column(align=True)
|
|
|
|
if is_eevee:
|
|
col.prop(mat, "displacement_method", text="Displacement")
|
|
col = col.column(align=True)
|
|
|
|
col.enabled = mat.displacement_method != 'BUMP'
|
|
# Clarify that this is for displacement if the displacement method setting is not above.
|
|
max_diplacement_text = "Max Distance" if is_eevee else "Max Displacement"
|
|
col.prop(mat, "max_vertex_displacement", text=max_diplacement_text)
|
|
|
|
if mat.displacement_method == 'DISPLACEMENT':
|
|
layout.label(text="Unsupported displacement method", icon='ERROR')
|
|
|
|
if is_eevee:
|
|
layout.prop(mat, "use_transparent_shadow")
|
|
|
|
col = layout.column()
|
|
col.prop(mat, "surface_render_method", text="Render Method")
|
|
if mat.surface_render_method == 'BLENDED':
|
|
col.prop(mat, "use_transparency_overlap", text="Transparency Overlap")
|
|
elif mat.surface_render_method == 'DITHERED':
|
|
col.prop(mat, "use_raytrace_refraction", text="Raytraced Transmission")
|
|
|
|
col = layout.column()
|
|
col.prop(mat, "thickness_mode", text="Thickness")
|
|
if mat.surface_render_method == 'DITHERED':
|
|
col.prop(mat, "use_thickness_from_shadow", text="From Shadow")
|
|
|
|
|
|
def draw_material_volume_settings(layout, mat, is_eevee=True):
|
|
layout.prop(mat, "volume_intersection_method", text="Intersection" if is_eevee else "Volume Intersection")
|
|
|
|
|
|
def draw_material_settings(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
layout.use_property_decorate = False
|
|
|
|
mat = context.material
|
|
|
|
draw_material_surface_settings(layout, mat, False)
|
|
draw_material_volume_settings(layout, mat, False)
|
|
|
|
|
|
# TODO: used by `./scripts/addons_core/hydra_storm/ui.py`, move to `EEVEE_NEXT_MATERIAL_PT_settings`.
|
|
class EEVEE_MATERIAL_PT_settings(MaterialButtonsPanel, Panel):
|
|
bl_label = "Settings"
|
|
bl_context = "material"
|
|
COMPAT_ENGINES = {'BLENDER_EEVEE'}
|
|
|
|
def draw(self, context):
|
|
draw_material_settings(self, context)
|
|
|
|
|
|
class EEVEE_MATERIAL_PT_viewport_settings(MaterialButtonsPanel, Panel):
|
|
bl_label = "Settings"
|
|
bl_context = "material"
|
|
bl_parent_id = "MATERIAL_PT_viewport"
|
|
COMPAT_ENGINES = {'BLENDER_RENDER'}
|
|
|
|
def draw(self, context):
|
|
draw_material_settings(self, context)
|
|
|
|
|
|
class EEVEE_NEXT_MATERIAL_PT_settings(MaterialButtonsPanel, Panel):
|
|
bl_label = "Settings"
|
|
bl_context = "material"
|
|
COMPAT_ENGINES = {'BLENDER_EEVEE_NEXT'}
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
layout.use_property_decorate = False
|
|
|
|
mat = context.material
|
|
|
|
layout.prop(mat, "pass_index")
|
|
|
|
|
|
class EEVEE_NEXT_MATERIAL_PT_settings_surface(MaterialButtonsPanel, Panel):
|
|
bl_label = "Surface"
|
|
bl_context = "material"
|
|
bl_parent_id = "EEVEE_NEXT_MATERIAL_PT_settings"
|
|
COMPAT_ENGINES = {'BLENDER_EEVEE_NEXT'}
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
layout.use_property_decorate = False
|
|
|
|
mat = context.material
|
|
|
|
draw_material_surface_settings(layout, mat)
|
|
|
|
|
|
class EEVEE_NEXT_MATERIAL_PT_settings_volume(MaterialButtonsPanel, Panel):
|
|
bl_label = "Volume"
|
|
bl_context = "material"
|
|
bl_parent_id = "EEVEE_NEXT_MATERIAL_PT_settings"
|
|
COMPAT_ENGINES = {'BLENDER_EEVEE_NEXT'}
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
layout.use_property_decorate = False
|
|
|
|
mat = context.material
|
|
|
|
draw_material_volume_settings(layout, mat)
|
|
|
|
|
|
class MATERIAL_PT_viewport(MaterialButtonsPanel, Panel):
|
|
bl_label = "Viewport Display"
|
|
bl_context = "material"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
bl_order = 10
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
mat = context.material
|
|
return mat and not mat.grease_pencil
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
|
|
mat = context.material
|
|
|
|
col = layout.column()
|
|
col.prop(mat, "diffuse_color", text="Color")
|
|
col.prop(mat, "metallic")
|
|
col.prop(mat, "roughness")
|
|
|
|
|
|
class MATERIAL_PT_lineart(MaterialButtonsPanel, Panel):
|
|
bl_label = "Line Art"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
bl_order = 10
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
mat = context.material
|
|
return mat and not mat.grease_pencil
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
|
|
mat = context.material
|
|
lineart = mat.lineart
|
|
|
|
layout.prop(lineart, "use_material_mask", text="Material Mask")
|
|
|
|
col = layout.column(align=True)
|
|
col.active = lineart.use_material_mask
|
|
row = col.row(align=True, heading="Masks")
|
|
for i in range(8):
|
|
row.prop(lineart, "use_material_mask_bits", text=" ", index=i, toggle=True)
|
|
if i == 3:
|
|
row = col.row(align=True)
|
|
|
|
row = layout.row(align=True, heading="Custom Occlusion")
|
|
row.prop(lineart, "mat_occlusion", text="Levels")
|
|
|
|
row = layout.row(heading="Intersection Priority")
|
|
row.prop(lineart, "use_intersection_priority_override", text="")
|
|
subrow = row.row()
|
|
subrow.active = lineart.use_intersection_priority_override
|
|
subrow.prop(lineart, "intersection_priority", text="")
|
|
|
|
|
|
class MATERIAL_PT_animation(MaterialButtonsPanel, Panel, PropertiesAnimationMixin):
|
|
COMPAT_ENGINES = {
|
|
'BLENDER_RENDER',
|
|
'BLENDER_EEVEE_NEXT',
|
|
'BLENDER_WORKBENCH',
|
|
}
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
layout.use_property_decorate = False
|
|
|
|
# MaterialButtonsPanel.poll ensures this is not None.
|
|
material = context.material
|
|
|
|
col = layout.column(align=True)
|
|
col.label(text="Material")
|
|
self.draw_action_and_slot_selector(context, col, material)
|
|
|
|
if node_tree := material.node_tree:
|
|
col = layout.column(align=True)
|
|
col.label(text="Shader Node Tree")
|
|
self.draw_action_and_slot_selector(context, col, node_tree)
|
|
|
|
|
|
classes = (
|
|
MATERIAL_MT_context_menu,
|
|
MATERIAL_UL_matslots,
|
|
MATERIAL_PT_preview,
|
|
EEVEE_MATERIAL_PT_context_material,
|
|
EEVEE_MATERIAL_PT_surface,
|
|
EEVEE_MATERIAL_PT_volume,
|
|
EEVEE_MATERIAL_PT_displacement,
|
|
EEVEE_MATERIAL_PT_thickness,
|
|
EEVEE_MATERIAL_PT_settings,
|
|
EEVEE_NEXT_MATERIAL_PT_settings,
|
|
EEVEE_NEXT_MATERIAL_PT_settings_surface,
|
|
EEVEE_NEXT_MATERIAL_PT_settings_volume,
|
|
MATERIAL_PT_lineart,
|
|
MATERIAL_PT_viewport,
|
|
EEVEE_MATERIAL_PT_viewport_settings,
|
|
MATERIAL_PT_animation,
|
|
MATERIAL_PT_custom_props,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__": # only for live edit.
|
|
from bpy.utils import register_class
|
|
for cls in classes:
|
|
register_class(cls)
|