3

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

uhoh
  • 2,667
  • 2
  • 27
  • 56
  • 1
    Did you try to pass a custom context? http://www.blender.org/api/blender_python_api_2_75_release/bpy.ops.html#overriding-context – CodeManX Aug 02 '15 at 16:18
  • Wow, that looks like a very, very useful item @CoDEmanX. I will try it and then update here, – uhoh Aug 02 '15 at 16:46
  • 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! – uhoh Aug 03 '15 at 06:14
  • 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". – uhoh Aug 03 '15 at 06:37
  • 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. – uhoh Aug 03 '15 at 11:48
  • These appear related: from @CoDEmanX here... – uhoh Aug 03 '15 at 12:05
  • ...and from @frisee here. – uhoh Aug 03 '15 at 12:05

1 Answers1

7

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:

CodeManX
  • 29,298
  • 3
  • 89
  • 128
  • 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. – uhoh Aug 04 '15 at 02:11
  • 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. – CodeManX Aug 04 '15 at 04:52
  • 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! – uhoh Aug 04 '15 at 05:57
  • 18 arguments? They are unlikely all actual properties - see bpy.ops.wm.link() for example. All filter_* attributes are used internally in the modal file select dialog, but do not influence the actual operation. This is hard to distinguish, especially in auto-complete (not so much in docs as there's more space). How to communicate positional optional args, positional args with default, mandatory and internal keyword args? Also see https://developer.blender.org/T34014#151430 – CodeManX Aug 04 '15 at 09:19
  • 1
    Added example how to use bgl.glReadPixels() to take a screenshot of a certain region. – CodeManX Aug 04 '15 at 10:01
  • wow Wow WOW this is all really good stuff, and all in one place! Thank you again @CoDEmanX, this will keep my head spinning for a while! – uhoh Aug 04 '15 at 11:00
  • Just to double check, for bpy.ops.screen.screen_shot() there are 18 optional keyword arguments here and 3 optional named arguments here not mentioned explicitly in the first link. Are there others out there too, or is that it? – uhoh Aug 05 '15 at 03:29
  • 1
    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. – CodeManX Aug 05 '15 at 04:24
  • Apart from these arguments, there are no other that I would know about. Operators written in C might require custom data (see e.g. this, it's not possible to provide the knife operator with a list of cut points via Python), and maybe use some sort of internal properties at rare occasions, but it shouldn't matter to a Python scripter at all. – CodeManX Aug 05 '15 at 04:25