How to reliably get pixels of a material preview?

It seems that material.preview.image_size is (0, 0) most of the time, despite the material preview clearly being shown in the Blender UI. Is there a way to reliably get a preview’s pixel data and size?

import bpy
obj = bpy.context.object
for ms in obj.material_slots:
    mat = ms.material
    if not mat: continue
    size = tuple(mat.preview.image_size)
    some_pixels = tuple(mat.preview.image_pixels[:16])
    print(mat, size, some_pixels)
1 Like

Motivation for this question: I’m trying to make a panel that displays a grid of image-based materials, but currently it seems I’m out of luck. None of the solutions I can think of work well:

  1. I wouldn’t mind using the material’s original preview in a “flat” (“plane”) mode, but it doesn’t respect the image aspect ratio (the material is always stretched to a square), and blender UI refuses to distort the layout.template_icon() elements via layout.scale_* or layout.ui_units_*.
  2. My next approach was to generate a custom resized icon from a material preview (the topic of this post), but it seems to not work most of the time. No idea why.
  3. Theoretically, I could try to use GPUOffScreen.draw_view3d() to render a temporary object to a texture, but preliminary experiments have shown that it only works from within a POST_PIXEL callback. Trying to use it elsewhere either results in inexplicable glitches and distortions, or just crashes Blender. And trying to modify the scene inside a POST_PIXEL callback appears to be futile and/or dangerous.

Any ideas?

@Hypersomniac, is this your area of expertise? Hope I’m not bothering you too much :slight_smile:

I would like to have such function to colorize materials to their material previews just instantly, for imported scenes with textures to control material distribution.

Hmm… well, it seems like material’s preview data updates (and becomes available to python) after preview.icon_id was displayed via layout.template_icon() somewhere in the Blender UI. Is there a way to force it to update immediately? material.preview.reload() seems to actually erase all pixel data.