【问题标题】:Layer-backed OpenGLView redraws only if window is resized仅当调整窗口大小时,支持图层的 OpenGLView 才会重绘
【发布时间】:2011-11-28 10:26:45
【问题描述】:

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

应用程序会每隔 0.05 秒向 OpenGLContext 绘制一些内容。 使用 Core Animation Layer disabled 我可以在视图中看到移动的对象,这是视图不断重绘的结果。一切都完美无缺。

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

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

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

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

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

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

【问题讨论】:

  • 有什么原因不能使用 CAOpenGLLayer 代替吗?那么您就不必担心图层支持,因为您的内容将直接托管在图层中。像你一样,我在尝试使用 CALayer 支持 NSOpenGLView 时看到了伪像。
  • @BradLarson 我在使用 CAOpenGLLayer 时遇到了很多问题。我是一名新手开发人员,我已经阅读了一些关于从 NSOpenGLView 到 CAOpenGLLayer 的过渡的教程,但我无法让它工作。该应用程序使用CVDisplayLink,并且绘制由 displaylink 回调控制。我知道 CAOpenGLLayer 不需要 CVDisplayLink 之类的东西,但我遇到了一些我无法解决的问题。该应用程序实际上是一个电影播放器​​,通过 displaylink 回调我检查在给定时间内是否有新帧可用;如果有新的框架可用,它就会被绘制。
  • @BradLarson(我在这里继续,因为我已经达到了上一条评论中的最大字符数)使用 NSOpenGLView 可以按预期工作。使用 CAOpenGLLayer,我在检查新图像是否可用时遇到问题,因为实例变量始终为 nil
  • 使用 CAOpenGLLayer,我在检查新图像是否可用时遇到问题,因为实例变量(应该调用检查新帧可用性的方法)始终是 nil。经过多天的挣扎,我因此放弃了 CAOpenGLLayer..
  • 我在这里描述了我如何在一个应用程序中使用 CAOpenGLLayer:stackoverflow.com/questions/6113922/…。 CVDisplayLink 和 CAOpenGLLayer 不能很好地配合使用,正如我在这里提到的:stackoverflow.com/questions/5316474/…,但我听说有一些方法可以使用它来更新 CAOpenGLLayer 而不会出现我看到的工件。内部 CAOpenGLLayer 异步回调在大多数情况下应该可以正常工作,但 50 FPS 视频会导致问题。

标签: core-animation calayer nsview redraw nsopenglview


【解决方案1】:

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

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

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

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

更新

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

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

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

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

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

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

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

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

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

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

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

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

【讨论】:

  • 非常感谢您的详细回答,您向我解释了很多微妙的方面!在实施您的建议之前,我还有一个问题。实际上,我已经将 CVDisplayLink 与我的 NON-layered-back NSOpenGLView一起使用,因为我想在一个不是主线程的线程中执行所有绘图(我需要以 60fps 的速度显示具有高 UI 响应性的内容)。如果我切换到支持层的NSOpenGLView,是否仍然可以从单独的线程中绘制?
  • 很遗憾,答案是否定的。您可以从您喜欢的任何线程绘制到 CALayer(无论是它自己的层还是任何视图的支持层,包括 NSOpenGLView),但是除了使用 main 之外,没有可靠的方法使该层在屏幕上可见线。 CALayer 确实比方面更糟糕。我尝试的所有方法要么会导致伪影,甚至可能会以随机间隔使应用程序崩溃。您可以做的根本不是使用 NSOpenGLView,而是使用 NSOpenGLLayer,您可以将其作为支持层放置到您喜欢的任何视图中(只需首先使用 setLayer: 和 then setWantsLayer: ...跨度>
  • ... 这两个调用的顺序很关键!)。使用 NSOpenGLLayer,您必须使用渲染方式:使图层异步并通过 canDrawInContext... 和 drawInContext... 方法控制渲染,您必须覆盖或不将其设置为异步,使 canDraw... 始终返回 YES并通过直接调用图层上的display 来更新其内容,然后通过[CATransaction flush] 将图层带到屏幕上来控制另一个线程的渲染(都来自另一个线程)。这似乎可行,尽管它没有官方记录的做事方式。
  • @Andrea3000 正如您已经注意到的,NSOpenGLLayer 和 CAOpenGLLayer 之间的唯一区别是第一个使用 AppKit 数据类型。 NSOpenGLLayer 的实现基本上将 AppKit 类型“翻译”为更原生的 CAOpenGLLayer 类型,然后使用翻译后的类型调用 [self ...]。当然,子类化 CAOpenGLLayer 可以为您节省一点处理时间,但类型的转换和对self 的额外调用并没有那么昂贵,并且在大多数情况下几乎无法检测到差异。如果你更喜欢使用 AppKit 类型,子类 NSOpenGLLayer
  • @GitSyncApp 因为 CVDisplayLink 将确保渲染与您的屏幕刷新率和图形子系统同步。渲染比显示器物理显示或子系统实际处理能力更多的图像是没有意义的。此外,计时器不会以精确的间隔触发,如果您仅错过 0.01 毫秒的屏幕刷新,则整个图像将被全屏刷新延迟(导致动画延迟并减少每秒显示的渲染图像数量)。 CVDisplayLink 就像一个高优先级优化计时器。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-10
  • 1970-01-01
  • 2015-10-26
相关资源
最近更新 更多