Pick material under mouse cursor

Hey, I’m really new to python, but I would like to make a little script and need some tips, where I should start and if it is possible at all.

Sometimes its really difficult to find the right material of an object with more than 10 materials.

One way is to go to edit mode and select the polys of the material selection, but sometimes you have to iterate through all materials to find the right one. Another way is to separate the object by material and then select the object with the desired material. But you have to rejoin the meshes after.

My plan is to make a script, which will select the material below the mouse cursor if some hotkey is pressed. Almost something like an eyedropper.

Are there any functions in python that could help with this task?

My thoughts are, that something like this could be an approach to find the material.

1 Trace a ray from the mouse position and project it on to the mesh
(Would VBH a good way to calculate this efficiently or is the VBH generation to slow?
the same goes for KDtrees I think)

2 Calculate the nearest vertex to the vector where the ray is intersecting the mesh

3 Get the applied material from the selected vertex

Thanks for every help and tips :slight_smile:

1 Like

So ok, after some research I found out, that the sharerslot of the selected face is also selected automatically… Works only in editmode, but should do the trick for most of the cases. :smiley:

Strange that I never noticed that after over 2 years of daily blender usage

BVH is probably only useful if you plan on eyedropping a specific object. If you want raycast to work with any arbitrary object in the scene, you might need to use scene.ray_cast(). It’s about 8 times slower (still in the milliseconds), but for the purpose of checking one face, once, it’s likely more than fast enough.

With scene.ray_cast(), the return values include face index and the object, which is enough to determine the material.

Suppose you get the return values from a raycast:

  • find the material index using obj.data.polygons[face_index].material_index
  • lookup the material using obj.material_slots.find(material_index)

Edit: I should add that this would be for object mode!

1 Like

Thank you so much, worked so far.

The only problem is, that the face index has an out of index when modifiers are used, so the face index is higher than the number of faces of the base mesh.

Is there a workaround to check the generated polys for their applied material? Is bgl maybe the right direction I have to look?

Which I also wanted to ask, is there a way to improve my script?

import bpy
from bpy_extras import view3d_utils


def main(context, event):
    # get the context arguments
    scene = context.scene
    region = context.region
    rv3d = context.region_data
    coord = event.mouse_region_x, event.mouse_region_y
    scene = bpy.context.scene 
    viewlayer = bpy.context.view_layer

    # 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 *-10)
    ray_goal = ray_origin + (view_vector *1000)

    rayresult = scene.ray_cast(viewlayer, ray_target, ray_goal)

    print (rayresult)
    print (ray_goal)
    print (ray_target)

    # get and select object
    obj = rayresult[4]   
    bpy.ops.object.select_all(action='DESELECT')
    context.view_layer.objects.active = obj

    # select material slot
    MatInd = obj.data.polygons[rayresult[3]].material_index
    bpy.context.object.active_material_index = MatInd


class MaterialPicker(bpy.types.Operator):
    bl_idname = "view3d.material_picker"
    bl_label = "Material Picker"

    def modal(self, context, event):
        if event.type in {'MIDDLEMOUSE', 'WHEELUPMOUSE', 
'WHEELDOWNMOUSE'}:
            # allow navigation
            return {'PASS_THROUGH'}
        elif event.type == 'LEFTMOUSE':
            main(context, event)
            return {'RUNNING_MODAL'}
        elif event.type in {'RIGHTMOUSE', 'ESC'}:
            return {'CANCELLED'}

        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        if context.space_data.type == 'VIEW_3D':
            context.window_manager.modal_handler_add(self)
            return {'RUNNING_MODAL'}
        else:
            self.report({'WARNING'}, "Active space must be a View3d")
            return {'CANCELLED'}


def register():
    bpy.utils.register_class(MaterialPicker)


def unregister():
    bpy.utils.unregister_class(MaterialPicker)


if __name__ == "__main__":
    register()

In this case you can just access the same object data used in the raycast.

So, if you have a bpy.data.objects['Cube'] that uses a procedural modifier, look for the evaluated version under context.depsgraph.scene_eval.objects['Cube']. Its data should correspond to what you see in the viewport with modifiers on. Try finding the material there.

So far the script looks good to me!

Edit: Forgot to mention there’s an elegant way of getting the scene evalulated object like so:

dg = context.depsgraph
eval_obj = dg.id_eval_get(obj)

MatInd = eval_obj.data.polygons[rayresult[3]].material_index
2 Likes

Thank you so much, this helped me a lot.

It works now, but I have to test it in bigger scenes too.
Something is really strange, the ray_cast is much more precise if I increase the multiplier to 10000 instead of 1000, so the tracing goal is really far away.

Do you see a way how to improve the code further?

import bpy
from bpy_extras import view3d_utils


def main(context, event):
    # get the context arguments
    scene = context.scene
    region = context.region
    rv3d = context.region_data
    coord = event.mouse_region_x, event.mouse_region_y
    scene = bpy.context.scene 
    viewlayer = bpy.context.view_layer

    # 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 *-10)
    ray_goal = ray_origin + (view_vector *10000)

    result, location, normal, index, object, matrix = scene.ray_cast(viewlayer, ray_target, ray_goal)

    # get and select object
    obj = object  
    bpy.ops.object.select_all(action='DESELECT')
    context.view_layer.objects.active = obj
    
    dg = context.depsgraph
    eval_obj = dg.id_eval_get(obj)
    
    if obj:
        # select material slot
        MatInd = eval_obj.data.polygons[index].material_index
        
        print (MatInd)
        bpy.context.object.active_material_index = MatInd

    else:
        return {'CANCELLED'}


class MaterialPicker(bpy.types.Operator):
    bl_idname = "view3d.material_picker"
    bl_label = "Material Picker"

    def modal(self, context, event):
        if event.type in {'MIDDLEMOUSE', 'WHEELUPMOUSE', 
'WHEELDOWNMOUSE'}:
            # allow navigation
            return {'PASS_THROUGH'}
        elif event.type == 'LEFTMOUSE':
            main(context, event)
            return {'RUNNING_MODAL'}
        elif event.type in {'RIGHTMOUSE', 'ESC'}:
            return {'CANCELLED'}

        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        if context.space_data.type == 'VIEW_3D':
            context.window_manager.modal_handler_add(self)
            return {'RUNNING_MODAL'}
        else:
            self.report({'WARNING'}, "Active space must be a View3d")
            return {'CANCELLED'}


def register():
    bpy.utils.register_class(MaterialPicker)


def unregister():
    bpy.utils.unregister_class(MaterialPicker)


if __name__ == "__main__":
    register()

Cheers Daniel

You were passing ray_target in place of ray_origin to scene.ray_cast() which caused weird results. I simplified some of them and made some adjustments elsewhere. I left the original lines commented out. You can remove them.

import bpy
from bpy_extras import view3d_utils


def main(context, event):
    # get the context arguments
    scene = context.scene
    region = context.region
    rv3d = context.region_data
    coord = event.mouse_region_x, event.mouse_region_y
#    scene = bpy.context.scene 
#    viewlayer = bpy.context.view_layer
    viewlayer = context.view_layer

    # 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 *-10)
#    ray_goal = ray_origin + (view_vector *1000)

#    result, location, normal, index, object, matrix = scene.ray_cast(viewlayer, ray_target, ray_goal)
    result, location, normal, index, object, matrix = scene.ray_cast(viewlayer, ray_origin, view_vector)

    # get and select object
#    obj = object  
#    bpy.ops.object.select_all(action='DESELECT')
#    context.view_layer.objects.active = obj

    if result:
        for o in context.selected_objects:
            o.select_set(False)
        dg = context.depsgraph
#        eval_obj = dg.id_eval_get(obj)
        eval_obj = dg.id_eval_get(object)
        viewlayer.objects.active = object.original
    
#    if obj:
        # select material slot
        MatInd = eval_obj.data.polygons[index].material_index
        
        print (MatInd)
        object.original.active_material_index = MatInd

#    else:
#        return {'CANCELLED'}
    return {'FINISHED'}


class MaterialPicker(bpy.types.Operator):
    bl_idname = "view3d.material_picker"
    bl_label = "Material Picker"

    def modal(self, context, event):
        if event.type in {'MIDDLEMOUSE', 'WHEELUPMOUSE', 
'WHEELDOWNMOUSE'}:
            # allow navigation
            return {'PASS_THROUGH'}
#        elif event.type == 'LEFTMOUSE':
        elif event.type == 'LEFTMOUSE' and event.value == 'PRESS':
            main(context, event)
            return {'RUNNING_MODAL'}
        elif event.type in {'RIGHTMOUSE', 'ESC'}:
            return {'CANCELLED'}

        return {'RUNNING_MODAL'}

    def invoke(self, context, event):
        if context.space_data.type == 'VIEW_3D':
            context.window_manager.modal_handler_add(self)
            return {'RUNNING_MODAL'}
        else:
            self.report({'WARNING'}, "Active space must be a View3d")
            return {'CANCELLED'}


def register():
    bpy.utils.register_class(MaterialPicker)


def unregister():
    bpy.utils.unregister_class(MaterialPicker)


if __name__ == "__main__":
    register()

Edit: Simplified some more.

1 Like

That’s awesome thanks! Was wondering why the event was triggered twice :smiley:
elif event.type == ‘LEFTMOUSE’ and event.value == ‘PRESS’:

Thank you a lot!

Useful. Hope addon or script release…

You can get the script “material_picker.py” here. https://bit.ly/2Wilqug
There are other small scripts in here, just copy them in your folder:
“Foundation\Blender\2.80\scripts\startup” Then you will find them with search

1 Like

WoW. some many useful scripts. Thanks so much !

Drop to floor !!! C4D had it plugins.

1 Like

Does anyone know if this can be done, but detect which modifier is under the mouse cursor?