Hi guys,
I have a bit of a problem. I have a script that advances the frame number, does some stuff (like a project from view in the viewport, baking a texture, etc). The script works well when I run it many times manually, but when I put a loop in the script, the viewport isn’t updating. So my question is, how can I simply force the view to update in my script. I googled it already and found suggestions like using the frame_change handers, but that seems like doing it the wrong way around to me. Any other suggestions?
Thanks!
Frame Change Handlers do work. Play the timeline to execute your loop.
But there is also a mechanism called a Modal Timer. An example script resides under the Templates menu of the text editor.
You could put the inner loop code inside the timer event.
Remember, python for Blender is a single core operation. Your code runs, then the system updates itself.
Thans for your quick answer. It makes sense to me now. If I use the frame change handlers, I would need to make sure that the timeline plays only once and doesn’t loop. How can I do that?
You can check Scene.frame_current in a frame_change handler and abort the playback if the frame number is equal to frame_end, but a modal timer is much more appropriate - the user could interfere with the playback, but he can’t if you use a modal operator that returns RUNNING_MODAL in its modal() method:
import bpy
class ModalTimerOperator(bpy.types.Operator):
"""Operator which runs its self from a timer"""
bl_idname = "wm.modal_timer_operator"
bl_label = "Modal Timer Operator"
_timer = None
frame_current = 0
frame_prev = 0
def modal(self, context, event):
if (event.type in {'RIGHTMOUSE', 'ESC'} or
self.frame_current > context.scene.frame_end):
return self.cancel(context)
if event.type == 'TIMER':
context.scene.frame_set(self.frame_current)
self.frame_current += 1
# some action
context.object.location.z += 0.1
return {'RUNNING_MODAL'}
def execute(self, context):
wm = context.window_manager
sce = context.scene
self.frame_prev = sce.frame_current
self.frame_current = sce.frame_start
self._timer = wm.event_timer_add(0.3, context.window)
wm.modal_handler_add(self)
return {'RUNNING_MODAL'}
def cancel(self, context):
wm = context.window_manager
wm.event_timer_remove(self._timer)
print(self.frame_prev)
context.scene.frame_set(self.frame_prev)
return {'FINISHED'}
def register():
bpy.utils.register_class(ModalTimerOperator)
def unregister():
bpy.utils.unregister_class(ModalTimerOperator)
if __name__ == "__main__":
register()
# test call
bpy.ops.wm.modal_timer_operator()
Sorry I didn’t come back to this in such a while. Just an update. I finally had some time to look at this, but I guess I’m not doing it right, as I get a ton of crashes. I will just have to be a bit more careful and check some more stuff. My script at the moment is really sloppy. I don’t do any error checking. I guess I will now have to. I will be back when I figure out why it is crashing exactly.
Ah! I figured it out already. I actually did two things wrong
- I had the indentation level of my own code wrong, so that ran every time modal() was called instead of every time event.type was “TIMER”
- After I fixed that, it still crashed, but that problem stopped when I increased the delay in event_timer_add from 0.3 seconds to 5.0 seconds.
Apparently, something goes very wrong when the routine gets called when the previous call didn’t finish yet. Is there a standard way to check this, or should
I just write a flag somewhere to check whether the last call is finished.
Thanks for all the help, I’m really getting somewhere now!
AFAIK, modal() is not called again until a previous call returned? But could be wrong… maybe an issue to set scene_frame in a modal timer op… Can you provide a test script?
My script is not a secret, so I can just post the actual thing. I can’t figure out how to post code while maintainging the indentation levels, so they are screwed up. Sorry for that.
It is not very flexible at the moment, it assume you have an object called Walls which has a mesh Cube.002.
Most importantly, it is textured with a movie using project from view, the corresponding UVmap is called projected
in the script. It is also textured with a blank image using an unwrapping. The scripts than runs through the timeline, bakes the projected texture to the unwrapped one for each time and saves the unwrapped texture to a file. I am open to all comments. I know that I’m an absolute ass when it comes to checking error messages and such. This is because my python scripts usually do much more trivial things, like open same data and make a plot. But I would really like to learn how one should do these things!
script work when there are two texture in the uv image editor. One is the movie and
it is associated with a uvmap that has been projected from view. The other one is
an empty texture associated with a uvmap that has been regularly unwrapped. This last
one is the one we will bake to.
import bpy
class ModalTimerOperator(bpy.types.Operator):
“”“Operator which runs its self from a timer”""
bl_idname = “wm.modal_timer_operator”
bl_label = “Modal Timer Operator”
_timer = None
frame_current = 0
frame_prev = 0
def modal(self, context, event):
if (event.type in {'RIGHTMOUSE', 'ESC'} or
self.frame_current > context.scene.frame_end):
return self.cancel(context)
if event.type == 'TIMER':
context.scene.frame_set(self.frame_current)
self.frame_current += 1
print("frame number is now:",bpy.data.scenes["Scene"].frame_current)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
object_wall=bpy.context.scene.objects["Walls"]
bpy.context.scene.objects.active=object_wall
uvmap_projected=bpy.data.meshes["Cube.002"].uv_textures["projected"]
uvmap_unwrapped=bpy.data.meshes["Cube.002"].uv_textures["unwrapped"]
# set active texture to projected and project from view
bpy.data.meshes["Cube.002"].uv_textures.active=uvmap_projected
# to get the right context for project_from_view, we first need to find the 3D_view
filtered=next(filter(lambda x:x.type=="VIEW_3D",bpy.context.screen.areas))
# now we need to find the window region
window_region=next(filter(lambda x:x.type=="WINDOW",filtered.regions))
override=bpy.context.copy()
override['area']=filtered
override['region']=window_region
override['edit_object']=bpy.context.edit_object
bpy.ops.uv.project_from_view(override,orthographic=False, camera_bounds=True,correct_aspect=False, clip_to_bounds=False, scale_to_bounds=False)
bpy.data.meshes["Cube.002"].uv_textures.active=uvmap_unwrapped
bpy.ops.object.bake_image()
bpy.ops.object.mode_set(mode='OBJECT')
filename='//automated_wall_textures_1/wall_texture_baked_frame_'+'{:03d}'.format(bpy.data.scenes["Scene"].frame_current)
print(filename)
bpy.data.images['wall_texture'].filepath_raw = filename
bpy.data.images['wall_texture'].file_format = 'PNG'
bpy.data.images['wall_texture'].save()
return {'RUNNING_MODAL'}
def execute(self, context):
wm = context.window_manager
sce = context.scene
self.frame_prev = sce.frame_current
self.frame_current = sce.frame_start
self._timer = wm.event_timer_add(5.0, context.window)
wm.modal_handler_add(self)
return {'RUNNING_MODAL'}
def cancel(self, context):
wm = context.window_manager
wm.event_timer_remove(self._timer)
print(self.frame_prev)
context.scene.frame_set(self.frame_prev)
return {'FINISHED'}
def register():
bpy.utils.register_class(ModalTimerOperator)
def unregister():
bpy.utils.unregister_class(ModalTimerOperator)
if name == “main”:
register()
# test call
bpy.ops.wm.modal_timer_operator()
I think the amount of work you are doing inside the timer event exceeds the timer interval, thus recursion. I get a ‘Circular Reference to texture stack error’ when I run your script.
Do you get that error with the script as is, or only when you shorten the timer interval? With me, the script runs without error like this, but crashes without error messages on the console when I shorten the timer interval. The circular reference to texture stack error are an error I get when I don’t set up the textures like the scripts expects them.