How to avoid momentary stretching on autorotation of iOS OpenGL ES apps
Asked Answered
S

1

8

This has been bugging me recently. It is quite straightforward to put together an OpenGL ES app that supports both portrait and landscape. But during autorotation, the system seems to just forcibly stretch the render buffer to the new dimensions once, autorotate, and then call the usual -layoutSubviews -> -resizeFromLayer: etc. so the drawables can be adjusted to the new viewport dimensions.

Most apps I've seen that support both portrait and landscape seem to settle for this easy approach. But I wonder if I can do better...

Perhaps I should intercept the autorotation before it happens (using UIViewController's usual methods), "expand" once the render buffer to a perfect square of the longest screen size (e.g., 1136px x 1136px on an iPhone 5) - so that it 'bleeds' off screen, perform the autorrotation (no renderbuffer size change and hence no stretching, just as when you switch between e.g. two landscape orientations), and finally adjust the framebuffer again to dispose the invisible, "wasted" margins off screen? (of course I could have a square framebuffer all along, but that would be inefficient fillrate-wise)

Any suggestions? Any better way to accomplish this that I haven't thought about?

EDIT

I modified my -resizeFromLayer: code as follows:

CGRect bounds = [layer bounds];

// Enlarge layer to square of the longest side:
if (bounds.size.width < bounds.size.height) {
    bounds.size.width = bounds.size.height;
}
else{
    bounds.size.height = bounds.size.width;
}

[layer setBounds:bounds];


// Adjust size to match view's layer:
[_mainContext renderbufferStorage:GL_RENDERBUFFER
                         fromDrawable:(CAEAGLLayer*) [_view layer]];

// Query the new size:
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH,  &_backingWidth);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_backingHeight);

// (etc., you know the drill...)

...And it works! As it stands right now, I have a wasteful, off-screen-bleeding square renderbuffer all along, but this is just a proof of concept. On production code, I would perform said resizing-to-square just before autorotation, and resize back to screen dimensions afterwards.

Shopper answered 9/9, 2013 at 3:4 Comment(1)
I haven't had time to try the approach mentioned above, yet. As soon as I can test it, I'll post the results here.Shopper
B
6

I managed to fix this fairly easily in my 3D app by fetching the view size from the GLKView presentationLayer. This will give the current view size during the rotation animation, and the correct projection matrix can be calculated based on this size during the update step. This prevents any incorrect aspect ratio distortion.

If you want to try this, you can add the following lines to the OpenGL Game template in Xcode:

- (void)update
{
    // get the size from the presentation layer, so that animating does not squish
    CALayer *presentationLayer = [self.view.layer presentationLayer];
    CGSize layerSize = presentationLayer.bounds.size;
    float aspect = fabsf(layerSize.width / layerSize.height);
    GLKMatrix4 projectionMatrix = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(65.0f), aspect, 0.1f, 100.0f);

    ...
}
Blackguard answered 11/9, 2013 at 13:5 Comment(10)
I'm not using GLKit, and I'm not familiar with this -update method; does it get called every frame?Shopper
Yes. I assume you have an update and render loop for this app, so all you need to do is to get the size of the presentation layer of your CAEAGLLayer in the update loop and use this to calculate your projection matrix.Blackguard
Thank you. Although I'm not using GLKit right now, and my own solution seems to do the trick, I'm accepting your answer because it looks clean, complying with the frameworks, and it might come in handy in the near future!Shopper
Nice trick. And the core idea stands regardless of whether you use GLKit: every layer has a presentationLayer, so whatever your draw code is doing it can use that layer's size to figure out the aspect ratio it should draw at. However, Calculating your projection matrix every frame is needless -- if not performance-killing -- computation. You might consider conditionalizing this so it only happens during rotation (the beginning and end of which you can catch in your view controller).Telegraphy
@Telegraphy I agree that it is needless to calculate the projection matrix every update step, this is taken directly from the Xcode template as a simple example. I dispute it is a performance killer though, it is just a single tanf and a few divides and multiplies. If you want a simple way to check for animations before adjusting the matrix, you can check if the layer.animationKeys property is non nil.Blackguard
@Blackguard I also agree that calculating the projection matrix in itself shouldn't be a performance killer; However, I'd rather skip uploading an extra uniform to all my shaders every frame (not that I've benchmarked it or anything...).Shopper
Anyway, I'm trying this approach. I'm not using GLKit, but I'm still using EAGLLayer and I have an -update: method so I guess it should work.Shopper
@Blackguard So, I tried it. If I create my textures using GL_LINEAR, the seams (=dark grid lines) between each quad of my 2D tile map layer (a regular quad mesh, tile size = 32 points) "show" for a while during rotation (like a flickering grid). But if I set the filter to GL_NEAREST, the artefact disappears. Since you mentioned your game is 3D, perhaps you can't help me here; I guess all your UI is single textured quads...Shopper
OK, that was on the low-res simulators. On retina devices, I still get the "seams" even with GL_LINEAR...Shopper
@NicolasMiari This sounds like a float rounding issue caused by the projection matrix. There are a bunch of other posts with similar issues, they can probably help, eg #1913336Blackguard

© 2022 - 2024 — McMap. All rights reserved.