9

我有一个带有类型主视图NSView和一个子视图的窗口,该子视图是NSOpenGLView其名称的子类CustomOpenGLView。的子类NSOpenGLView是通过Custom ViewInterface Builder 中的 a 并将其类设置为 来获得的CustomOpenGLView。这是根据 Apple Sample Code Layer Backed OpenGLView 制作的。

该应用程序每隔 0.05 秒向 OpenGLContext 绘制一些内容。禁用核心动画层后,我可以在视图中看到移动的对象,这是视图连续重绘的结果。一切都完美无缺。

我现在想在顶部有一个半透明的视图CustomOpenGLView来容纳播放/停止/ecc 等控制按钮。

为此,我添加了一个子视图CustomOpenGLView并启用了核心动画层CustomOpenGLView。控制按钮放置在这个新的子视图中。

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

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

此外,当绘图循环开始时,只有第一个“帧”出现在屏幕上,如果我调整窗口大小,比方说 5 秒后,我在视图中看到它应该在开始 5 秒后绘制的确切内容绘图循环。看来我需要设置[glView setNeedsDisplay:TRUE]. 我这样做了,但没有任何改变。

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

4

1 回答 1

25

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

NSOpenGLView如果是分层支持的,情况会有所不同。当你调用-flushBufferorglFlush()时,渲染确实像之前那样反复进行,图像直接渲染到视图的像素存储中,但是,这个像素存储不再是窗口的后备存储,它是视图的“支持层”。因此,您的 OpenGL 图像已更新,您只是看不到它的发生,因为“绘制到图层”和“在屏幕上显示图层”是两个完全不同的事情!要使新图层内容可见,您必须调用setNeedsDisplay:YESlayer-backed NSOpenGLView

为什么你打电话时它对你不起作用setNeedsDisplay:YES?首先,确保在主线程上执行此调用。您可以在您喜欢的任何线程上执行此调用,它肯定会将视图标记为脏,但只有在主线程上执行此调用时,它才会为其安排重绘调用(没有该调用它被标记为脏但它在重绘它的任何其他父/子视图之前不会重绘)。另一个问题可能是drawRect:方法。当您将视图标记为脏并重新绘制时,将调用此方法,并且该方法“绘制”的任何内容都会覆盖图层中当前的任何内容。只要您的视图没有图层支持,那么渲染 OpenGL 内容的位置并不重要,但对于图层支持的视图,

尝试以下操作:NSTimer在您的主线程上创建一个每 20 毫秒触发一次的方法,并调用一个调用setNeedsDisplay:YES您的 layer-backed的方法NSOpenGLView。将所有 OpenGL 渲染代码移动到drawRect:layer-backed 的方法中NSOpenGLView。那应该工作得很好。如果您需要比 a 更可靠的东西NSTimer,请尝试 a CVDisplayLink(CV = CoreVideo)。ACVDisplayLink就像一个计时器,但每次屏幕刚刚重绘时它都会触发。

更新

分层 NSOpenGLView 有点过时了,从 10.6 开始,它们不再需要了。当您将其分层时,NSOpenGLView 在内部会创建一个 NSOpenGLLayer,因此您也可以自己直接使用这样的层并“构建”您自己的 NSOpenGLView:

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

您现在拥有自己的图层支持视图,它由您的 NSOpenGLLayer 子类支持。由于它是layer backed的,因此添加子视图(例如按钮、文本字段等)绝对没问题。

对于您的支持层,您基本上有两种选择。

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

  • 覆盖canDrawInContext:...以返回YES/ NO,具体取决于您是否可以/想要绘制下一帧。
  • 覆盖drawInContext:...以执行您的实际 OpenGL 渲染。
  • 使图层异步 ( setAsynchronous:YES)
  • 确保图层在调整大小时“更新” setNeedsDisplayOnBoundsChange:YES

苹果会为你创建一个,每次触发时都会CVDisplayLink调用主线程,如果这个方法返回,它会调用. 这是你应该做的方式。canDrawInContext:...YESdrawInContext:...

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

选项 2
另一个选项不受 Apple 官方支持,可能适合您,也可能不适合您:

  • 不要覆盖canDrawInContext:...,默认实现总是返回YES,这就是你想要的。
  • 覆盖drawInContext:...以执行您的实际 OpenGL 渲染,所有这一切。
  • 不要使图层异步。
  • 不要设置needsDisplayOnBoundsChange

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

虽然不推荐这种方法,因为它可能会导致其他视图的重绘问题(因为这些也可能在主线程以外的线程上重绘,并且并非所有视图都支持),它也不被禁止,它不使用私有 API 并且它已在 Apple 邮件列表中提出建议,而 Apple 内部没有任何人反对。

于 2012-06-26T18:07:44.330 回答