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
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.
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!
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:
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()
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()
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
How Can we use similar concent to make object “active” on mouseover?
Any idea what lines of code would work?
I tried but I am not that much into programming.
So you can assign hotkey to it.
Also it has very powerful tool “Cursor Fit & Align” to align 3d cursor by vertex/edge/face(s) - recommend! (P.S. it can store cursors position&rotation like bookmarks!)
Other thing: Blender has build in addon called “Material Utilities”, it store a list of all your materials in scene and with “Shift+Q” you call a menu with it