当你有一个 normalNSOpenGLView
时,你可以简单地通过 OpenGL 绘制一些东西,然后调用-flushBuffer
使NSOpenGLContext
渲染出现在屏幕上。如果你的上下文不是双缓冲的,如果你渲染到一个窗口,这不是必需的,因为在 MacOS X 中所有窗口都已经被自己双缓冲了,调用glFlush()
也足够了(仅对于真正的全屏 OpenGL 渲染,您需要双缓冲以避免伪影)。然后,OpenGL 将直接渲染到视图的像素存储(实际上是窗口的后备存储),或者在双缓冲的情况下,它将渲染到后缓冲区,然后与前缓冲区交换;因此,新内容立即在屏幕上可见(实际上不是在下一次屏幕刷新之前,但这种刷新每秒至少发生 50-60 次)。
NSOpenGLView
如果是分层支持的,情况会有所不同。当你调用-flushBuffer
orglFlush()
时,渲染确实像之前那样反复进行,图像直接渲染到视图的像素存储中,但是,这个像素存储不再是窗口的后备存储,它是视图的“支持层”。因此,您的 OpenGL 图像已更新,您只是看不到它的发生,因为“绘制到图层”和“在屏幕上显示图层”是两个完全不同的事情!要使新图层内容可见,您必须调用setNeedsDisplay:YES
layer-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:
- 创建你自己的子类
NSOpenGLLayer
,我们称之为MyOpenGLLayer
- 创建你自己的子类
NSView
,我们称之为MyGLView
- 覆盖
- (CALayer *)makeBackingLayer
以返回一个自动释放的实例MyOpenGLLayer
- 设置
wantsLayer:YES
为MyGLView
您现在拥有自己的图层支持视图,它由您的 NSOpenGLLayer 子类支持。由于它是layer backed的,因此添加子视图(例如按钮、文本字段等)绝对没问题。
对于您的支持层,您基本上有两种选择。
选项 1
正确且官方支持的方法是将渲染保持在主线程上。因此,您必须执行以下操作:
- 覆盖
canDrawInContext:...
以返回YES
/ NO
,具体取决于您是否可以/想要绘制下一帧。
- 覆盖
drawInContext:...
以执行您的实际 OpenGL 渲染。
- 使图层异步 (
setAsynchronous:YES
)
- 确保图层在调整大小时“更新”
setNeedsDisplayOnBoundsChange:YES
(
苹果会为你创建一个,每次触发时都会CVDisplayLink
调用主线程,如果这个方法返回,它会调用. 这是你应该做的方式。canDrawInContext:...
YES
drawInContext:...
如果您的渲染太昂贵而无法在主线程上进行,您可以执行以下技巧: 覆盖openGLContextForPixelFormat:...
以创建与您之前创建的另一个上下文(上下文 A)共享的上下文(上下文 B)。在上下文 A 中创建一个帧缓冲区(您可以在创建上下文 B 之前或之后执行此操作,这并不重要);如果需要(您选择的位深度),附加深度和/或模板渲染缓冲区,但是不是颜色渲染缓冲区,而是附加“纹理”(纹理 X)作为颜色附件(glFramebufferTexture()
)。现在,在渲染到该帧缓冲区时,所有颜色渲染输出都将写入该纹理。在您选择的任何线程上使用上下文 A 执行对此帧缓冲区的所有渲染!渲染完成后,canDrawInContext:...
返回YES
并drawInContext:...
绘制一个简单的四边形填充整个活动帧缓冲区(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 内部没有任何人反对。