仅在调整窗口大小时才会重新绘制图层支持的OpenGLView

时间:2011-09-30 12:08:29

标签: core-animation calayer nsview redraw nsopenglview

我有一个窗口,其主视图类型为NSView,子窗口是NSOpenGLView的子类,其名称为CustomOpenGLViewNSOpenGLView的子类是通过Interface Builder中的Custom View获得的,并将其类设置为CustomOpenGLView。 这是根据Apple示例代码Layer Backed OpenGLView制作的。

该应用程序每次都会向OpenGLContext绘制一些东西,比方说,0.05秒。 使用核心动画层禁用,我能够在视图中看到移动对象,这是连续重绘视图的结果。一切都完美无瑕。

我现在希望在CustomOpenGLView之上设置一个半透明视图来容纳控制按钮,如play / stop / ecc ..

为此,我向CustomOpenGLView添加了子视图,并在CustomOpenGLView上启用了核心动画层。控制按钮位于此新子视图中。

这样,带有控制按钮的视图正确显示在CustomOpenGLView之上,但现在视图不会重绘。仅当我调整包含所有这些视图的窗口时,它才会绘制。

结果是我没有看到任何“动画”......我只看到一个静止图像,它代表绘图循环开始时绘制的第一帧。 如果我调整窗口大小,openGLContext会重新绘制,直到我停止调整窗口大小。之后,我再次看到一张静止图像,在调整大小期间发生了最后一次绘图。

此外,当绘图循环开始时,屏幕上只显示第一个“框架”,如果我调整窗口大小,让我们说,5秒钟之后,我会在视图中看到应该在5秒之后绘制的内容。绘制循环的开始。 好像我需要设置[glView setNeedsDisplay:TRUE]。我这样做但没有任何改变。

错误在哪里?为什么添加核心动画层会破坏重绘?这是否意味着我没有得到什么?

1 个答案:

答案 0 :(得分:23)

当你有一个正常的NSOpenGLView时,你可以通过OpenGL简单地绘制一些东西,然后调用-flushBuffer的{​​{1}}来使渲染出现在屏幕上。如果你的上下文没有双缓冲,如果你渲染到一个窗口就没有必要,因为所有的窗口都已经在MacOS X中自己进行了双重缓冲,调用NSOpenGLContext也足够了(仅适用于真正的全屏OpenGL渲染,你需要双缓冲以避免伪影)。然后OpenGL将直接渲染到视图的像素存储中(实际上是窗口的后备存储),或者在双缓冲的情况下,它将渲染到后缓冲区,然后将其与前缓冲区交换;因此,新内容立即在屏幕上可见(实际上不是在下一次屏幕刷新之前,但这样的刷新每秒至少发生50-60次)。

如果glFlush()是图层支持的,情况会有所不同。当您调用NSOpenGLView-flushBuffer时,渲染实际上就像之前和之后一样,图像直接渲染到视图的像素存储区,但是,此像素存储不是不再支持窗口的存储,它是"支持层"的观点。因此,您的OpenGL图像会更新,您只是看不到它发生的事情,因为"绘制到一个图层"和"在屏幕上显示一个图层"是两个完全不同的东西!要使新图层内容可见,您必须在图层支持的glFlush()上调用setNeedsDisplay:YES

当你致电NSOpenGLView时,为什么它不起作用?首先,确保在主线程上执行此调用。您可以在任何您喜欢的线程上执行此调用,它肯定会将视图标记为脏,但只有在主线程上执行此调用时,它还会为其调度重绘调用(没有该调用它被标记为脏但是它在重新绘制任何其他父/子视图之前,不会重绘。另一个问题可能是setNeedsDisplay:YES方法。当您将视图标记为脏并且重新绘制时,将调用此方法,无论此方法如何"绘制"覆盖图层中当前的任何内容。只要您的视图不是图层支持的,在您渲染OpenGL内容的位置并不重要,但对于图层支持的视图,这实际上是您应该执行所有绘图的方法。

尝试以下操作:在主线程上创建一个drawRect:,每20毫秒触发一次,并调用一个在图层支持的NSTimer上调用setNeedsDisplay:YES的方法。将所有OpenGL渲染代码移动到图层支持的NSOpenGLView的{​​{1}}方法中。这应该工作得很好。如果您需要比drawRect:更可靠的内容,请尝试NSOpenGLView(CV = CoreVideo)。 NSTimer就像一个计时器,但每次重新绘制屏幕时它都会触发。

更新

分层的NSOpenGLView有点过时了,从10.6开始它们不再是真正需要的了。在内部,当你使NSOpenGLLay成为分层时,它会创建一个NSOpenGLLayer,所以你也可以自己直接使用这样一个层来构建"构建"你自己的NSOpenGLView:

  1. 创建自己的CVDisplayLink子类,让我们称之为CVDisplayLink
  2. 创建自己的NSOpenGLLayer子类,让我们称之为MyOpenGLLayer
  3. 覆盖NSView以返回自动释放的MyGLView
  4. 实例
  5. - (CALayer *)makeBackingLayer
  6. 设置MyOpenGLLayer

    您现在拥有自己的图层支持视图,它由NSOpenGLLayer子类支持。由于它是图层支持,因此可以添加子视图(例如按钮,文本字段等)。

    对于您的支持层,您基本上有两个选项。

    选项1
    正确且官方支持的方法是将渲染保留在主线程上。因此,您必须执行以下操作:

    • 覆盖wantsLayer:YES以返回MyGLView / canDrawInContext:...,具体取决于您是否可以/想要绘制下一帧。
    • 覆盖YES以执行实际的OpenGL渲染。
    • 使图层异步(NO
    • 确保图层已更新"每当调整大小(drawInContext:...)时,否则在调整图层大小时不会调整OpenGL背景表面的大小(并且每次重绘图层时都必须拉伸/缩小渲染的OpenGL上下文)

    Apple将为您创建setAsynchronous:YES,每次触发时都会在主线程上调用setNeedsDisplayOnBoundsChange:YES,如果此方法返回CVDisplayLink,则会调用canDrawInContext:...。这就是你应该怎么做的方式。

    如果您的渲染在主线程上太昂贵,您可以执行以下操作:覆盖YES以创建与您之前创建的另一个上下文共享的上下文(上下文B)(上下文A)。在Context A中创建一个帧缓冲区(你可以在创建Context B之前或之后创建它,它真的很重要);如果需要,附加深度和/或模板渲染缓冲区(您选择的位深度),但是不要使用颜色渲染缓冲区,而是附加一个"纹理" (纹理X)作为颜色附件(drawInContext:...)。现在,渲染到该帧缓冲区时,所有颜色渲染输出都会写入该纹理。在您选择的任何线程上使用上下文A对此帧缓冲执行所有渲染!渲染完成后,让openGLContextForPixelFormat:...返回glFramebufferTexture(),然后在canDrawInContext:...中绘制一个简单的 quad ,填充整个活动帧缓冲区(Apple已将其设置为你和完全填充它的视口)和纹理X纹理。这是可能的,因为共享上下文也共享所有对象(例如像纹理,帧缓冲等)。因此,您的YES方法永远不会比绘制单个简单的纹理四边形更重要。所有其他(可能是昂贵的渲染)都发生在后台线程上的这个纹理上,而且没有阻塞你的主线程。

    选项2
    另一个选项未经Apple正式支持,可能适用于您,也可能不适用于您:

    • 请勿覆盖drawInContext:...,默认实施始终返回drawInContext:...,这就是您想要的内容。
    • 覆盖canDrawInContext:...以执行实际的OpenGL渲染,所有这些。
    • 不要使图层异步。
    • 不要设置YES

    每当您想重新绘制此图层时,请直接致电drawInContext:... needsDisplayOnBoundsChange!这是真的,Apple说你不应该叫它但是,"不应该"不是"不是")并且在致电display后,请致电setNeedsDisplay。这将工作,即使从后台线程调用!您的display方法是从调用[CATransaction flush]的同一个线程调用的,该线程可以是任何线程。直接调用drawInContext:...将确保您的OpenGL渲染代码执行,但新呈现的内容仍然只在图层的后备存储中可见,要将其带到屏幕,您必须强制系统执行图层合成和{{ 1}}就是这样做的。 CATransaction类只有类方法(你永远不会创建它的实例)是隐式线程安全的,并且可以随时从任何线程使用它(它随时随地执行锁定)。

    虽然不建议使用此方法,但由于它可能会导致其他视图的重绘问题(因为这些问题也可能会在主线程以外的线程上重新绘制而不是所有视图都支持),所以也不会被禁止,它没有使用私有API,并且已经在Apple邮件列表上建议,没有任何人在Apple反对它。