在Shady中控制逐帧绘制

时间:2019-05-28 19:45:41

标签: python shady

我正在努力了解如何控制Shady在屏幕上绘制的内容的流程。我习惯了Psychtoolbox,在其中您通过在后缓冲区上进行绘制来不断添加帧,然后通过调用flip()将其显式推入屏幕。使用Shady时,事情似乎会自动发生,因此,当您向World添加Stimulus对象时,它会尽快绘制。但是,如果我必须添加几个刺激,如何保证在绘制所有内容之前,屏幕上没有任何更新?

例如,假设我要绘制一个空白屏幕,并在其左上角的顶部绘制一个小方块。我可以做类似的事情:

w = Shady.World()
s1 = w.Stimulus(None, 'blank', envelopeSize=[1920, 1200], backgroundColor=[0, 0, 0], z=0)
s2 = w.Stimulus(None, 'square', envelopeSize=[20, 20], x=-950, y=590, backgroundColor=[1, 1, 1], z=1)

但是如何保证我不会以从帧n开始绘制s1和从帧n + 1开始绘制s2结束呢?也许可以将它们组合成一个Stimulus对象,但我想将它们分开(在我的真实案例中,小方块实际上是用来触发光电管的,所以我需要能够将它闪烁一次框架,在任务执行期间的不同时间。

1 个答案:

答案 0 :(得分:1)

首先,最好阅读Concurrency上的Shady文档。前两个示例(在“正在运行单线程”标题下)不存在您担心的问题,因为Stimulus实例是在实际渲染单个帧之前创建的。这是一个简单的示例:

import Shady
w = Shady.World(threaded=False)
# s1 = ...    
# s2 = ...
# ...    
w.Run()

超越这一点,您说得很对,Shady固有的范式转换之一就是您永远不会自己叫任何flip()等价物。一开始可能并不熟悉,但请放心:在我们基于Shady构建的应用程序中,我们不会错过自己调用flip()的日子。刺激参数的更新在回调中完成,例如:

  • 您可以选择将其安装到每个WorldStimulus实例中的所谓的“动画回调”;

  • 您可以分配给任何WorldStimulus属性的
  • 动态(可调用)值(在所有动画回调之后 立即获得这些值);

  • 响应按键等的事件回调。

确保您在同一回调中所做的更改可以在同一帧上呈现-这就是即使在 not 运行单线程时我们也能严格控制时间的答案。的确,如果您运行多线程并依次发出一个w.Stimulus()调用,则不能保证它们出现在同一帧上(实际上,可以保证它们出现在不同上)帧,因为非绘图线程中的.Stimulus()调用实际上将刺激创建的实际工作推迟到绘图线程中,然后等待直到返回之前完成)。可能的解毒剂是(1)单线程运行; (2)按照文档的第二个示例,以专用的w.Stimulus()方法执行所有Prepare()创建调用;或(3)确保刺激物在创建时具有visible=False,并且仅在以后显示。在所有这些情况下,我们都小心地将创建刺激(可能很慢)与对其属性的操纵分开。

前两种回调类型与您所描述的最相关。它们在"Making properties dynamic"的Shady的文档中有所介绍。在它们提供的框架内,(一如往常,在Python和Shady中)有许多不同的方法可以实现您描述的目标。就个人而言,我喜欢使用StateMachine实例作为动画回调。这是我创建一个简单的重复呈现的刺激的方法,该刺激的发作是由传感器补丁闪烁一帧预示的:

import random
import Shady

w = Shady.World(canvas=True, gamma=2.2)


# STIMULI

Shady.Stimulus.SetDefault(visible=False)
# let all stimuli be invisible by default when created

gabor = w.Sine(pp=0)
sensorPatch = w.Stimulus(
    size = 100,  # small,
    color = 1,   # bright,
    anchor = Shady.UPPER_LEFT, # with its top-left corner stuck...
    position = w.Place(Shady.UPPER_LEFT),  # to the top-left corner of the screen
)


# STATE MACHINE

sm = Shady.StateMachine()

@sm.AddState
class InterTrialInterval(sm.State):
    # state names should be descriptive but can be anything you want

    def duration(self):
        return random.uniform(1.0, 3.0)

    next = 'PresentGabor'

@sm.AddState
class PresentGabor(sm.State):

    def onset(self):
        gabor.visible = True
        sensorPatch.visible = Shady.Impulse() # a dynamic object: returns 1.0 the first time it is evaluated, then 0.0 thereafter

    duration = 2.0

    def offset(self):
        gabor.visible = False

    next = 'InterTrialInterval'


w.SetAnimationCallback( sm )
# now sm(t) will be called at every new time `t`, i.e. on every frame,
# and this will in turn call `onset()` and `offset()` whenever appropriate