3
$\begingroup$

I am trying to write a script that does various things to a mesh object from within a loop, and updates the 3D window each time, based on this script. This would be for explanatory and instructive purposes.

I would like to trigger a screen capture (to PNG) of only the 3D window each time as well.

I'm experimenting with this:

bpy.ops.screen.screenshot(filepath=path, full=False)

which I found in the documentation.

I have to include ...blah.png in the path name in order to get a PNG, or else it writes a .blend file each time. That's OK I guess.

But with full=False so far I don't seem to have good control getting my mouse-hover to tell the already-running-script which window I want to be active in order for the script to capture it.

Q: Is there some way I can just explicitly specify in the script which window should be captured?

(I made this GIF by hand from 34 manual screen shots, I'd like to automate this kind of thing)

sequential vertex selection animation

$\endgroup$
7
  • 1
    $\begingroup$ Did you try to pass a custom context? blender.org/api/blender_python_api_2_75_release/… $\endgroup$
    – CodeManX
    Commented Aug 2, 2015 at 16:18
  • $\begingroup$ Wow, that looks like a very, very useful item @CoDEmanX. I will try it and then update here, $\endgroup$
    – uhoh
    Commented Aug 2, 2015 at 16:46
  • $\begingroup$ So far no luck @CoDEmaX. Apparently my search skills are not sufficient. I think I need to do something with bpy.context.window but when I do dir() on that, the only setter I find seems to be .cursor_set()which is not what I need. Where can I find or look up information like "the identity of the current active window can be read or set using the following attribute...". Where is this stuff documented in a way that is easy to look-up? Thanks! $\endgroup$
    – uhoh
    Commented Aug 3, 2015 at 6:14
  • $\begingroup$ Since there happens to be a relevant example in that link, I can at least see how to sort of "get at" the area. [area.type for area in windows[0].screen.areas if "3" in area.type] returns ['VIEW 3D']. But I really don't see how to make it "the active area". $\endgroup$
    – uhoh
    Commented Aug 3, 2015 at 6:37
  • $\begingroup$ Have found this nugget "The active Area for example, can only be set inside of WindowManager", way back in 2.5 Developer Architecture here near the end. $\endgroup$
    – uhoh
    Commented Aug 3, 2015 at 11:48

1 Answer 1

7
$\begingroup$

To clear up some misunderstandings:

There are no get and set methods to modify the context, and mostly not for other purposes either. It's simply not the Python way of coding. ob.attr is your getter, and ob.attr = 123 the setter. Magic methods can intercept get and set operations to customize object behavior, but that's advanced stuff.

There's also a programmatic way to get and set any attribute, but you only ever need it if the attribute name is dynamic or not always present: hasattr(ob, "attr"), getattr(ob, "attr"), setattr(ob, "attr", 123).

All immediate members of bpy.context are read-only. You can't modify anything here. You can do something like bpy.context.window.screen = bpy.data.screens["Default"] however. screen is not a direct context attribute and editable. On the other hand, bpy.context.screen is read-only. bpy.context.window_manager.windows[#].screen is editable again.

To run an operator in a custom context means, that you construct a Python dictionary with context member names as keys, and datablock references as values. You do not modify the actual context:

ob = bpy.context.scene.object_bases["Cube"]
ctx = {"selected_bases": [ob]} # dict to store custom context
bpy.ops.object.delete(ctx) # run operator with custom context

Above code deletes the object Cube, even if completely different objects (or rather object bases) are selected without changing the selection temporarily.

It also prints a bunch of PyContext warnings to console, because the delete operator expects a couple more context members. We did not provide them in above code, we only set selected_bases. A nice, although limited trick is to run an operator with an empty context to figure out (some of the) context members:

bpy.ops.object.delete({})

It's sometimes easier to derive the context from the actual context, and only modify the necessary dictionary keys:

ctx = bpy.context.copy()
ctx["selected_bases"] = [bpy.context.scene.object_bases["Cube"]]
bpy.ops.object.delete(ctx)

Above code should not cause any PyContext warnings.

We can apply this to the screenshot operator now:

context = bpy.context
for area in context.screen.areas:
    if area.type == 'VIEW_3D':
        ctx = {
            "window": context.window, # current window, could also copy context
            "area": area, # our 3D View (the first found only actually)
            "region": None # just to suppress PyContext warning, doesn't seem to have any effect
        }
        bpy.ops.screen.screenshot(ctx, filepath="...", full=False)
        break # limit to first 3D View (optional)

Don't ever rely on the area index, it's not a stable property.


If you want an screenshot of a certain region (exactly as seen on screen), you can do the following:

import bpy
import bgl

context = bpy.context

def screenshot_region(region):
    x = region.x
    y = region.y
    width = region.width
    height = region.height
    
    buf = bgl.Buffer(bgl.GL_FLOAT, width * height * 4)
    bgl.glReadPixels(x, y, width, height, bgl.GL_RGBA, bgl.GL_FLOAT, buf)
    img = bpy.data.images.new("Screenshot", width, height, alpha=True)
    img.pixels[:] = buf

for area in context.screen.areas:
    if area.type == 'VIEW_3D':
        for region in area.regions:
            if region.type == 'WINDOW':
                screenshot_region(region)
                break

It creates an image datablock with the screenshot. You may wanna save it using Image.save_as(), which will use the scene's render settings. Or use struct.pack() to create some sort of bitmap file directly.

If you want to take a series of screenshots, you could do the following:

                scene = context.scene
                frame_orig = scene.frame_current

                for frame in range(scene.frame_start, scene.frame_end + 1):
                    scene.frame_set(frame)
                    bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
                    screenshot_region(region)

                scene.frame_set(frame_orig)

Also see Can I redraw during the script?.

Here's a more sophisticated example:

import bpy
import bgl

context = bpy.context
scene = context.scene

def screenshot_region(region):
    x = region.x
    y = region.y
    width = region.width
    height = region.height
    
    view_settings(area.spaces.active)
    bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
    
    buf = bgl.Buffer(bgl.GL_FLOAT, width * height * 4)
    bgl.glReadPixels(x, y, width, height, bgl.GL_RGBA, bgl.GL_FLOAT, buf)
    img = bpy.data.images.new("Screenshot", width, height, alpha=True)
    img.pixels[:] = buf
    
    view_settings(area.spaces.active, True)
    
def view_settings(view3d, revert=False):
    pref = context.user_preferences.view
    if not revert:    
        view_settings.ob_info = pref.show_object_info
        view_settings.view_name = pref.show_view_name
        view_settings.mini_axis = pref.show_mini_axis
        #view_settings.only_render = view3d.show_only_render
        view_settings.manipulator = view3d.show_manipulator
        
        pref.show_object_info = False
        pref.show_view_name = False
        pref.show_mini_axis = False
        #view3d.show_only_render = False
        view3d.show_manipulator = False
    else:        
        pref.show_object_info = view_settings.ob_info
        pref.show_view_name = view_settings.view_name
        pref.show_mini_axis = view_settings.mini_axis
        #view3d.show_only_render = view_settings.only_render
        view3d.show_manipulator = view_settings.manipulator

    
for area in context.screen.areas:
    if area.type == 'VIEW_3D':
        for region in area.regions:
            if region.type == 'WINDOW':
                for frame in range(scene.frame_start, scene.frame_end + 1):
                    scene.frame_set(frame)
                    screenshot_region(region)
                break

Related reads:

$\endgroup$
9
  • $\begingroup$ This is a fantastically useful answer and tutorial @CoDEmanX. Thank you for taking the time to explain everything clearly and add examples! Something that was confusing: In the documentation for operators, positional arguments are missing. ditto using python help(). While their existence is mentioned elsewhere, isn't it good practice to list positional arguments explicitly in documenting the operators? It's not like we need to save paper. $\endgroup$
    – uhoh
    Commented Aug 4, 2015 at 2:11
  • $\begingroup$ I don't think it's necessary to mention the 3 positional arguments that every operator supports in every operator description. That would be really redundant and might look like they were required, although it's an advanced and not very often used feature. Another more detailed example wouldn't hurt though. $\endgroup$
    – CodeManX
    Commented Aug 4, 2015 at 4:52
  • $\begingroup$ What is wrong with redundancy in documentation? What is wrong with being complete? How can incomplete be correct? Docs are used differently by different people. I go to the documentation, look up this operator, and I see 18 arguments, but no context. I believe this to be true. I don't start flipping through screens, count the total number of ops, and then rationalize "Aha! There are more than N ops, that means that there could be additional arguments mentioned only somewhere else." Anyway, your answer is fantastic. Thanks! $\endgroup$
    – uhoh
    Commented Aug 4, 2015 at 5:57
  • 1
    $\begingroup$ Added example how to use bgl.glReadPixels() to take a screenshot of a certain region. $\endgroup$
    – CodeManX
    Commented Aug 4, 2015 at 10:01
  • 1
    $\begingroup$ It's just the 3 positional args to specify a custom context (dict), an execution context (string, e.g. 'INVOKE_DEFAULT') and if pushing to undo stack should be disabled (bool). The undo option doesn't seem to work the reverse way (=True), to enable pushing to undo stack if the operator itself does not set the bl_options flag 'UNDO'. All operator properties should be listed as keyword args in the docs. $\endgroup$
    – CodeManX
    Commented Aug 5, 2015 at 4:24

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .