支持图层的OpenGLView仅在调整窗口大小时才重绘 [英] Layer-backed OpenGLView redraws only if window is resized

查看:134
本文介绍了支持图层的OpenGLView仅在调整窗口大小时才重绘的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个窗口,其主视图类型为 NSView 和一个子视图,该子视图是 NSOpenGLView 的子类。名称为 CustomOpenGLView
NSOpenGLView 的子类是通过Interface Builder中的 Custom View 并将其类设置为 CustomOpenGLView
这是根据Apple示例代码层支持的OpenGLView

I have a window with a main view of type NSView and a subview which is a subclass of NSOpenGLView whose name is CustomOpenGLView. The subclass of NSOpenGLView is obtained through a Custom View in Interface Builder and by setting its class to CustomOpenGLView. This is made according to the Apple Sample Code Layer Backed OpenGLView.

该应用程序每隔0.05秒就会在OpenGLContext上绘制一些内容。禁用了核心动画层的
已禁用,我能够在视图中看到运动对象,这是不断重绘视图的结果。

The app is made to draw something to the OpenGLContext every, let's say, 0.05 seconds. With Core Animation Layer disabled I am able to see the moving object in the view, which is the consequence of the continuous redrawing of the view. And everything works flawlessly.

我现在想在 CustomOpenGLView 上方有一个半透明的视图,以容纳诸如

I now want to have a semitransparent view on top of CustomOpenGLView to house control buttons like play/stop/ecc..

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

To do this I have add a subview to CustomOpenGLView and I have enabled Core Animation Layer on CustomOpenGLView. Control buttons are placed in this new subview.

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

This way the view with control buttons correctly appears on top of CustomOpenGLView but now the view doesn't redraw. It draws only if I resize the window containing all these views.

结果是我看不到任何动画 ...我只看到了静止图像,表示绘制循环开始时绘制的第一帧。
如果我调整窗口大小,则会重新绘制openGLContext,直到我停止调整窗口大小为止。之后,我再次看到在调整大小时出现了最后一张绘图的静止图像。

The result is that I do not see any "animation"...I only see a still image which represents the first frame which gets drawn when the drawing loop starts. If I resize the window, openGLContext gets redrawn until I stop resizing the window. After that I see once again a still image with the last drawing occurred during the resize.

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

In addition, when the drawing loop starts, only the first "frame" appears on screen and if I resize the window, let's say, 5 seconds later, I see in the view exactly what it should have been drawn 5 seconds after the starting of drawing loop. It seems like I need to set [glView setNeedsDisplay:TRUE]. I did that but nothing has changed.

哪里出了错?为什么添加核心动画层会破坏重绘?

Where is the mistake? Why does adding Core Animation Layer break the redraw? Does it imply something I'm not getting?

推荐答案

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

When you have a normal NSOpenGLView, you can simply draw something via OpenGL and then call -flushBuffer of the NSOpenGLContext to make the rendering appear on screen. If your context is not double buffered, which is not necessary if you render to a window, since all windows are already double buffered by themselves in MacOS X, calling glFlush() is sufficient as well (only for real fullscreen OpenGL rendering, you'll need double buffering to avoid artifacts). OpenGL will then render directly into the pixel storage of your view (which is in fact the backing storage of the window) or in case of double buffering, it will render to the back-buffer and then swap it with the front-buffer; thus the new content is immediately visible on screen (actually not before the next screen refresh but such a refresh takes place at least 50-60 times a second).

如果 NSOpenGLView 是层支持的,则有些不同。当您调用 -flushBuffer glFlush()时,渲染实际上就像之前和之后一样进行,图像直接渲染到视图的像素存储中,但是,此像素存储不再是窗口的后备存储,它是视图的后备层。因此,您的OpenGL图像已更新,您根本看不到它的发生,因为绘制到图层和在屏幕上显示图层是完全不同的两件事!要使新图层内容可见,您必须在支持图层的 NSOpenGLView 上调用 setNeedsDisplay:YES

Things are a bit different if the NSOpenGLView is layer-backed. When you call -flushBuffer or glFlush(), the rendering does actually take place just as it did before and again, the image is directly rendered to the pixel storage of the view, however, this pixel storage is not the backing storage of the window any longer, it is the "backing layer" of the view. So your OpenGL image is updated, you just don't see it happening since "drawing into a layer" and "displaying a layer on screen" are two completely different things! To make the new layer content visible, you'll have to call setNeedsDisplay:YES on your layer-backed NSOpenGLView.

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

Why didn't it work for you when you called setNeedsDisplay:YES? First of all, make sure you perform this call on the main thread. You can perform this call on any thread you like, it will for sure mark the view dirty, yet only when performing this call on the main thread, it will also schedule a redraw call for it (without that call it is marked dirty but it won't be redrawn until any other parent/child view of it is redrawn). Another problem could be the drawRect: method. When you mark the view as dirty and it is redrawn, this method is being called and whatever this method "draws" overwrites whatever content is currently within the layer. As long as your view wasn't layer-backed, it didn't matter where you rendered your OpenGL content but for a layer-backed view, this is actually the method where you should perform all your drawings.

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

Try the following: Create a NSTimer on your main thread that fires every 20 ms and calls a method that calls setNeedsDisplay:YES on your layer-backed NSOpenGLView. Move all your OpenGL render code into the drawRect: method of your layer-backed NSOpenGLView. That should work pretty well. If you need something more reliably than a NSTimer, try a CVDisplayLink (CV = CoreVideo). A CVDisplayLink is like a timer, yet it fires every time the screen has just been redrawn.

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

Layered NSOpenGLView are somewhat outdated, starting with 10.6 they are not really needed any longer. Internally a NSOpenGLView creates a NSOpenGLLayer when you make it layered, so you can as well use such a layer directly yourself and "building" your own NSOpenGLView:


  1. 创建您自己的 NSOpenGLLayer 子类,我们将其称为 MyOpenGLLayer

  2. 创建您自己的 NSView 子类,我们将其称为 MyGLView

  3. 覆盖-(CALayer *)makeBackingLayer 返回自动释放的 MyOpenGLLayer

  4. 实例为 MyGLView

  1. Create your own subclass of NSOpenGLLayer, let's call it MyOpenGLLayer
  2. Create your own subclass of NSView, let's call it MyGLView
  3. Override - (CALayer *)makeBackingLayer to return an autoreleased instance of MyOpenGLLayer
  4. Set wantsLayer:YES for MyGLView


$ b设置 wantsLayer:YES $ b

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

You now have your own layer backed view and it is layer backed by your NSOpenGLLayer subclass. Since it is layer backed, it is absolutely okay to add sub-views to it (e.g. buttons, textfields, etc.).

您的支持层,基本上有两个选择。

For your backing layer, you have basically two options.

选项1

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

Option 1
The correct and officially supported way is to keep your rendering on the main thread. Therefor you must do the following:


  • 覆盖 canDrawInContext:... 返回 / ,具体取决于您是否可以/是否希望绘制下一帧。

  • 覆盖 drawInContext:... 以执行实际的OpenGL渲染。

  • 使图层异步( setAsynchronous:YES

  • 只要调整图层大小,请确保更新图层( setNeedsDisplayOnBoundsChange:YES ),否则在调整图层大小时不会调整OpenGL底面的大小(并且每次重新绘制图层时都必须拉伸/收缩渲染的OpenGL上下文)

  • Override canDrawInContext:... to return YES/NO, depending on whether you can/want to draw the next frame or not.
  • Override drawInContext:... to perform your actual OpenGL rendering.
  • Make the layer asynchronous (setAsynchronous:YES)
  • Be sure the layer is "updated" whenever its resized (setNeedsDisplayOnBoundsChange:YES), otherwise the OpenGL backing surface is not resized when the layer is resized (and the rendered OpenGL context must be stretched/shrunk each time the layer redraws)

Apple将为您创建一个 CVDisplayLink ,该调用会调用 canDrawInContext:... 在主线程上每次触发时,如果此方法返回 YES ,它将调用 drawInContext:...

Apple will create a CVDisplayLink for you, that calls canDrawInContext:... on main thread each time it fires and if this method returns YES, it calls drawInContext:.... This is the way how you should do it.

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

If your rendering is too expensive to happen on main thread, you can do the following trick: Override openGLContextForPixelFormat:... to create a context (Context B) that is shared with another context you created earlier (Context A). Create a framebuffer in Context A (you can do that before or after creating Context B, it won't really matter); attach depth and/or stencil renderbuffers if required (of a bit depth of your choice), however instead of a color renderbuffer, attach a "texture" (Texture X) as color attachments (glFramebufferTexture()). Now all color render output is written to that texture when rendering to that framebuffer. Perform all rendering to this framebuffer using Context A on any thread of your choice! Once the rendering is done, make canDrawInContext:... return YES and in drawInContext:... just draw a simple quad that fills the whole active framebuffer (Apple has already set it for you and also the viewport to fill it completely) and that is textured with the Texture X. This is possible, since shared contexts share also all objects (e.g. like textures, framebuffers, etc.). So your drawInContext:... method will never do more than drawing a single, simple textured quad, that's all. All other (possibly expensive rendering) happens to this texture on a background thread and without ever blocking your main thread.

选项2

苹果公司未正式支持另一种选择,并且可能对您不起作用:

Option 2
The other option is not officially supported by Apple and may or may not work for you:


  • 请勿覆盖 canDrawInContext:... ,默认实现始终返回 YES ,这就是您想要的。

  • 覆盖 drawInContext:... 来执行实际的OpenGL渲染,所有这些。

  • 不要制作图层

  • 不要设置 needsDisplayOnBoundsChange

  • Don't override canDrawInContext:..., the default implementation always returns YES and that's what you want.
  • Override drawInContext:... to perform your actual OpenGL rendering, all of it.
  • Don't make the layer asynchronous.
  • Don't set needsDisplayOnBoundsChange.

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

Whenever you want to redraw this layer, call display directly (NOT setNeedsDisplay! It's true, Apple says you shouldn't call it, but "shouldn't" is not "mustn't") and after calling display, call [CATransaction flush]. This will work, even when called from a background thread! Your drawInContext:... method is called from the same thread that calls display which can be any thread. Calling display directly will make sure your OpenGL render code executes, yet the newly rendered content is still only visible in the backing storage of the layer, to bring it to screen you must force the system to perform layer compositing and [CATransaction flush] will do exactly that. The class CATransaction, which has only class methods (you will never create an instance of it) is implicitly thread-safe and may always be used from any thread at any time (it performs locking on its own whenever and wherever required).

虽然不建议使用此方法,因为它可能会导致其他视图的重绘问题(因为这些视图也可能在主线程以外的其他线程上重绘,并且并非所有视图都支持), 也不禁止,它不使用私有API,并且已在Apple邮件列表中建议,但苹果公司没有人反对。

While this method is not recommend, since it may cause redraw issues for other views (since those may also be redrawn on threads other than main thread and not all views support that), it is not forbidden either, it uses no private API and it has been suggested on the Apple mailing list without anyone at Apple opposing it.

这篇关于支持图层的OpenGLView仅在调整窗口大小时才重绘的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆