Files
test/scripts/startup/bl_ui/properties_material.py
Habib Gahbiche 1b4daf9d2e Nodes: remove "Use Nodes" in Shader Editor for Object Materials
"Use Nodes" was removed in the compositor to simplify the compositing
workflow. This introduced a slight inconsistency with the Shader Node
Editor.

This PR removes "Use Nodes" for object materials.

For Line Style, no changes are planned (not sure how to preserve
compatibility yet).
This simplifies the state of objects; either they have a material or
they don't.

Backward compatibility:
- If Use Nodes is turned Off, new nodes are added to the node tree to
simulate the same material:
- DNA: Only `use_nodes` is marked deprecated
- Python API:
  - `material.use_nodes` is marked deprecated and will be removed in
6.0. Reading it always returns `True` and setting it has no effect.
  - `material.diffuse_color`, `material.specular` etc.. Are not used by
EEVEE anymore but are kept because they are used by Workbench.

Forward compatibility:
Always enable 'Use Nodes' when writing blend files.

Known Issues:
Some UI tests are failing on macOS

Pull Request: https://projects.blender.org/blender/blender/pulls/141278
2025-09-14 17:53:54 +02:00

461 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")
layout.operator("object.material_slot_remove_all")
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 ma:
layout.prop(ma, "name", text="", emboss=False, icon_value=icon)
else:
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'}
def draw(self, context):
self.layout.template_preview(context.material)
class MATERIAL_PT_custom_props(MaterialButtonsPanel, PropertyPanel, Panel):
COMPAT_ENGINES = {
'BLENDER_RENDER',
'BLENDER_EEVEE',
'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',
'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'}
def draw(self, context):
layout = self.layout
mat = context.material
layout.use_property_split = True
panel_node_draw(layout, mat.node_tree, 'OUTPUT_MATERIAL', "Surface")
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'}
@classmethod
def poll(cls, context):
engine = context.engine
mat = context.material
return mat 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'}
@classmethod
def poll(cls, context):
engine = context.engine
mat = context.material
return mat 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'}
@classmethod
def poll(cls, context):
engine = context.engine
mat = context.material
return mat 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)
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_MATERIAL_PT_settings(MaterialButtonsPanel, Panel):
bl_label = "Settings"
bl_context = "material"
COMPAT_ENGINES = {'BLENDER_EEVEE'}
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_MATERIAL_PT_settings_surface(MaterialButtonsPanel, Panel):
bl_label = "Surface"
bl_context = "material"
bl_parent_id = "EEVEE_MATERIAL_PT_settings"
COMPAT_ENGINES = {'BLENDER_EEVEE'}
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_MATERIAL_PT_settings_volume(MaterialButtonsPanel, Panel):
bl_label = "Volume"
bl_context = "material"
bl_parent_id = "EEVEE_MATERIAL_PT_settings"
COMPAT_ENGINES = {'BLENDER_EEVEE'}
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',
'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_MATERIAL_PT_settings_surface,
EEVEE_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)