Improving the rendering of a large image in a small UIImageView with an one liner

It is not always that an image view and its image have the same size. Often times the size of the images we obtain from a web service don’t match the size of the image views where they are displayed. Or you might present an image picker controller so the user can take a photo and then display this photo in a thumbnail before proceeding to a next step.

Consider the following photo:

Mercedes AMG GTS

This is what you get when you display it in a small image view:

The result is grainy, noisy. You could obviously resize the original image and use the smaller version in this small image view instead. That would give you a smoother result. That sounds like a bit of work though, especially considering that you can obtain an equivalent result with one simple line of code:

[code lang=”objc”]
imageView.layer.minificationFilter = kCAFilterTrilinear;
[/code]

And we get:

Now that doesn’t hurt the eyes like the previous image. It looks as smooth as we would like.

Now let’s discuss how all this works.

To draw an image on the screen, the pixels of the image must be mapped to the pixels of the screen. This process is known as filtering. The image can be filtered in a variety of ways and the most popular algorithms have a quality vs speed tradeoff as usual. This filtering, however, is performed by the hardware and the hardware only implements the most basic algorithms that are usually good enough and very fast.

The simplest one is the nearest filter (kCAFilterNearest). For each pixel on the screen it just picks the color of one of the pixels of the image that is located under that screen pixel, usually the image pixel that is closest to the middle of the screen pixel. The following image shows the same photo scaled down to 32×24 pixels using a nearest filter:

The result is a mess, especially because the original image is much bigger than the resized image.

The default filter on iOS is the bilinear filter (kCAFilterLinear). It selects 4 pixels in the image that are closest to the center of the screen pixel and computes an weighted average of the color of these pixels as the output. The article Bilinear Texture Filtering in the Direct3D documentation gives a nice explanation on bilinear filtering. If we scale down the same photo to 32×24 using a bilinear filter we get this:

 The result is very similar to the nearest filter because there is a big difference between the original image size and the filtered image size. The bilinear filter only does a really nice job if the filtered image is not much smaller than half the size of the original image. The filtered image becomes grainer as it shrinks.

The filter this post highlights is the trilinear filter (kCAFilterTrilinear). Before the trilinear filter can be used, a mipmap must be generated for the original image. The mipmap is a sequence of smaller versions of the original image, where each subsequent image has half the width and height of the original. The last item in the sequence is a 1×1 image. The mipmap is generated for you by the hardware.

The trilinear filter dynamically selects one image in the mipmap array that is more appropriate for the image view size and then applies a bilinear filter on it. That also makes images look perfect at all times in image views where you run scaling animations. Here is the same photo scaled down to 32×24 using a trilinear filter:

It looks really smooth even for such extreme down scaling.

Storing mipmaps for your images requires more memory, of course. However, that is usually not a problem since the mipmap adds about 1/3 of the size of the original image to the memory. That would only become significant if you’re working with really large images.

Note that if you’re displaying a large image on a small UIButton, you should set the minificationFilter property to kCAFilterTrilinear on its imageView.layer, not on the button itself.

Melhorando a aparência de imagens grandes em UIImageViews pequenos com uma linha de código

Nem sempre as image views têm o mesmo tamanho das imagens que exibimos nelas. Geralmente o tamanho das imagens que obtemos de algum web service é diferente do tamanho das image views onde mostramos essas imagens. Também, muitas vezes apresentamos um image picker para o usuário tirar uma foto e em seguida mostramos essa foto grande em um thumbnail pequeno antes de prosseguir.

Considere a seguinte foto:

Mercedes AMG GTS

Esse é o resultado que obtemos ao exibí-la numa image view pequena:

O resultado é muito serrilhado, granulado. Podemos fazer um resize nessa imagem e utilizar essa imagem menor nessa pequena image view para obter um resultado melhor. Mas isso requer um pouco mais de código e trabalho. Que tal a seguinte linha de código:

[code lang=”objc”]
imageView.layer.minificationFilter = kCAFilterTrilinear;
[/code]

Que resulta em:

Uma imagem bem suave que não vai causar má impressão em ninguém.

Agora vejamos como isso funciona.

Para desenhar uma imagem na tela, os pixels da imagem precisam ser mapeados sobre os pixels da tela. Esse processo é conhecido como filtragem. A imagem pode ser filtrada de várias maneiras e quanto melhor a qualidade do filtro mais tempo ele leva pra ser processado. Essa filtragem é realizada pelo hardware e este apenas implementa algoritmos básicos que geralmente são bons o suficiente e também são muito rápidos.

O filtro mais simples é o nearest filter (kCAFilterNearest). Para cada pixel na tela ele apenas seleciona a cor do pixel da imagem que estiver localizado sob esse pixel na tela, geralmente o pixel da imagem mais próximo do centro do pixel da tela. A imagem a seguir mostra uma foto filtrada na resolução de 32×24 usando o nearest filter:

O resultado deixa muito a desejar, principalmente porque a imagem original é muito maior do que a imagem filtrada.

Por padrão, o filtro utilizado no iOS é o bilinear filter (kCAFilterLinear). Ele seleciona 4 pixels na imagem que estão mais próximos do centro do pixel na tela e calcula uma média ponderada da cor desses pixels como resultado. O artigo Bilinear Texture Filtering na documentação do Direct3D apresenta uma ótima explicação sobre o bilinear filter. Se filtramos a mesma imagem anterior numa resolução de 32×24 utilizando o bilinear filter obtemos:

O resultado é bastante parecido com o do nearest filter porque a imagem original é muito maior do que a imagem filtrada. O bilinear filter apenas dá ótimos resultados quando o tamanho da imagem filtrada não é muito menor que a metade do tamanho da imagem original. A imagem filtrada fica cada vez mais granulada ao ser mais reduzida.

O filtro de destaque desse post é o trilinear filter (kCAFilterTrilinear). Antes que o trilinear filter possa ser utilizado, um mipmap tem que ser gerado para a imagem original. O mipmap é uma seqüência de versões menores da imagem original, onde cada imagem subsequente tem metade da largura e altura da original. O último item da seqüência é uma imagem 1×1. O mipmap é gerado pra nós pelo hardware.

O trilinear filter seleciona uma imagem no mipmap que é mais apropriada para o tamanho da image view dinamicamente, e em seguida aplica um bilinear filter nessa imagem. Isso também faz com que as imagens apareçam perfeitamente em qualquer instante durante uma animação em que a escala da image view é alterada. A seguir a mesma foto filtrada em 32×24 com o trilinear filter:

O resultado fica bem suave mesmo nesse exemplo extremo de redução de tamanho.

Armazenar mipmaps para suas imagens requer mais memória, obviamente. Porém, isso geralmente não é um problema já que os mipmaps adicionam em torno de 1/3 do tamanho da imagem original ao consumo de memória. Isso apenas será significativo se estiver trabalhando com imagens muito grandes.

Note que se estiver exibindo imagens grandes num UIButton pequeno, você deve atribuir o kCAFilterTrilinear na propriedade minificationFilter do imageView.layer desse botão, e não diretamente na instância do botão.

UIView – Frame e Transform

Precisamos ter cuidado ao modificar a propriedade transform de uma view. Geralmente, quando alteramos o transform não devemos mexer no frame da view (a não ser que você saiba o que está fazendo), pois o frame é o axis-aligned bounding rectangle (AABR) da view (também conhecido como axis-aligned bounding box). O frame muda sempre que o transform mudar, e isso pode parecer estranho a princípio portanto, irei ilustrar como isso funciona a seguir.

Primeiramente, o AABR é o menor retângulo alinhado com os eixos coordenados (os eixos x (horizontal) e y (vertical)) que contém todos os pontos de uma forma geométrica, que nesse caso, é uma instância de UIView. Assim, se a view rotacionar um pouco, o AABR anterior não mais deve conter a view completamente, portanto ele deve mudar. Para demonstrar essa relação entre o frame e o transform da UIView, criei um projeto de exemplo bem simples no Github que você pode encontrar aqui https://github.com/xissburg/FrameTransform. A seguir, dois screenshots do exemplo:

Basicamente, ele contém uma view na frente (cinza escuro) e outra no fundo (cinza claro). A view da frente é transformada através de gestos (pan, pinch e rotação) e depois que a transformação é aplicada o frame da view da frente é atribuido ao frame da view do fundo. Na imagem anterior, nós temos à esquerda a view da frente em sua configuração inicial, com transformação identidade. À direita, uma pequena rotação foi aplicada e podemos ver como o seu frame ficou (retângulo cinza claro). Ele aumentou para conter toda a view. Quando a escala da view muda, a mesma coisa acontece, o frame muda para conter toda a view novamente.

A conclusão disso tudo é que temos que ter cautela ao tentar mudar o transform e o frame da view. Não quero dizer que você nunca deve fazer isso, pois existem algumas poucas situações em que é necessário mudar ambos para obter o resultado desejado. É por isso que na maioria dos casos você deve utilizar o bounds em vez do frame, pois o bounds não muda após alterações do transform.

Apresentando múltiplos View Controllers de uma só vez

Não é muito comum, mas eventualmente você pode precisar de apresentar múltiplos View Controllers de uma vez, onde o último View Controller (VC) apresentado aparece no topo. Os outros devem aparecer após este último ser removido da tela (dismiss). Por exemplo, pode ser necessário apresentar um UIImagePickerController para escolher ou tirar uma foto e em seguida mostrar outro VC para editar a imagem depois que o UIImagePickerController for removido.

A forma trivial para tentar conseguir esse efeito é criar os VCs que deseja apresentar, e simplesmente apresentar um após o outro, com ou sem animação. Infelizmente, isso não funciona. Geralmente, o primeiro VC que foi apresentado vai aparecer na tela, e os outros vão para algum lugar ainda desconhecido. Mudando a ordem das operações pode dar resultados diferentes, porém nenhum deles é o que desejamos. Entretanto, existe uma técnica um pouco mais complicada que nos dá o resultado desejado.

Se apresentarmos o primeiro VC sem animação e em seguida apresentarmos o segundo VC com animação, o primeiro vai aparecer repentinamente (pois está sem animação) e o segundo será apresentado com animação como esperado. Mas é claro que não queremos que o primeiro VC apareça. Para isso, podemos atribuir zero à sua propriedade alpha antes de apresentá-lo, e atribuir 1 no completion block do método presentViewController:animated:completion: que vamos usar para apresentar o segundo VC. Agora, temos uma tela preta durante a animação da apresentação do segundo VC. Isso acontece devido à uma otimização do UIKit onde ele esconde os VCs que estão por baixo do VC que está no topo, e ele não leva o alpha em consideração. Assim, vemos a cor de fundo da window ou de seu view controller raíz ou outra coisa, dependendo da estrutura da sua app. Mas não é isso que queremos ver, nós queremos que o conteúdo da tela permaneça durante a animação da apresentação do segundo VC. Portanto, o que podemos fazer é criar uma imagem com esse conteúdo e colocar uma UIImageView com essa imagem no fundo da tela até que a animação termine.

Dessa forma, esse efeito pode ser atingido com os seguintes passos:

  1. Criar os VCs A e B, onde B vai ficar no topo.
  2. Desenhar o VC atual em uma imagem através do método -[CALayer renderInContext:].
  3. Criar uma imageView com essa imagem e inserí-la na window no índice 0 para que ela fique atrás de nossos VCs.
  4. Atribuir zero para o alpha da view do VC A.
  5. Apresentar B utilizando presentViewController:animated:completion: e atribuir 1 para o alpha da view de A no completion block e também remover a imageView de sua superview (a imagem será desalocada nesse passo).

A seguir um código de exemplo:

[code lang=”obj-c”]
UIViewController *aViewController = /*_____*/; // Criar VCs de alguma maneira (alloc init, carregar de um xib, ou instanciar de um storyboard)
UIViewController *bViewController = /*______*/;

// Criar imagem
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
[self.view.layer renderInContext:context]; // Aqui desenhamos a view na imagem
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.view.frame];
imageView.image = image
[self.window insertSubview:imageView atIndex:0];

// Apresentar os VCs
aViewController.view.alpha = 0; // Atribuir zero ao alpha para que possamos ver a imageView
[self presentViewController:aViewController animated:NO completion:nil];
[self presentViewController:bViewController animated:YES completion:^{
aViewController.view.alpha = 1;
[imageView removeFromSuperview];
}];
[/code]

Depois, após remover o bViewController (dismiss), o aViewControler deve aparecer na tela.

Presenting Multiple Modal View Controllers at Once

It’s quite unusual, but eventually you might need to present multiple modal View Controllers at once, where it looks like only the top most View Controller (VC) is being presented. The others will appear as the user dismisses the topmost VC. For instance, you might want to present an UIImagePickerController to take/choose a picture and show another VC to edit the image after the UIImagePickerController is dismissed.

The trivial way to attempt to achieve that is to just create the VCs you want to present modally and present one after the other. Unfortunately, this is not gonna work. What you get is just the first VC presented, all others just go nowhere. UIKit just won’t cooperate with you here. You might try some variations of this, but I am almost certain none of them are going to work as you would expect. However, there’s still one trick that will make that happen…

If you present the first VC without animation and then present the second with animation, the first will appear suddenly and then the second will be presented with animation. But we don’t want for the first VC to appear. We want for it to be transparent until the animation of the presentation of the second VC completes. So, let’s set the alpha property of its view to zero before presenting it, and then set it back to 1 in the completion block of the presentViewController:animated:completion:. Now what we get is a black background during the presentation of the second VC. This happens because of an optimization in UIKit where it hides the VCs that are under the topmost VC, and it does not take the alpha into account. Since we are presenting a VC modally without animation, we see either the window background color or whatever root view controller that might be in it, since the VC’s view alpha is zero. So what we can do to keep our current VC on the background while the second VC is animating into the screen? Yes, you might have thought of that: put an image with the contents of the current VC on the background.

So the outline of the method should be as follows:

  1. Create VCs A and B, where B will be on the top.
  2. Draw the current VC on an image through -[CALayer renderInContext:].
  3. Create an imageView with this image and insert it in the main window at index 0 so it will be behind everything.
  4. Set A’s view alpha to zero.
  5. Present B using presentViewController:animated:completion: and set A’s view alpha to one in the completion block and also remove the imageView from its superview (the image will be deallocated in this step).

And here is some sample code:

[code lang=”obj-c”]
UIViewController *aViewController = /*_____*/; // Create your VC somehow
UIViewController *bViewController = /*______*/;

// Create the image
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
[self.view.layer renderInContext:context];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.view.frame];
imageView.image = image
[self.window insertSubview:imageView atIndex:0];

// Present the VCs
aViewController.view.alpha = 0; // Set alpha to zero so that we can see the imageView
[self presentViewController:aViewController animated:NO completion:nil];
[self presentViewController:bViewController animated:YES completion:^{
aViewController.view.alpha = 1;
[imageView removeFromSuperview];
}];
[/code]

Then, after dismissing bViewController, you should see aViewController on the screen.

Faster Gaussian Blur in GLSL

The popular technique to make a gaussian blur in GLSL is a two-pass blur, in the horizontal and then in the vertical direction (or vice-versa), also known as a convolution. This is a classic post on this matter http://www.gamerendering.com/2008/10/11/gaussian-blur-filter-shader/, and many others you will find around the web do exactly the same. But the performance of this shader can be improved quite a lot.

The bottleneck is in the dependent texture reads, which are any texture reads in a fragment shader where the coordinates depend on some calculation, or any texture reads in a vertex shader. A non-dependent texture read is one where the coordinates are either a constant or a varying. According to the PowerVR Performance Recommendations, a dependent texture read adds a lot of additional steps in the processing of your shader because the texture coordinates cannot be known ahead of time. When you use a constant or a varying, the hardware is able to pre-fetch the texture data before it gets to run your fragment shader, hence it becomes way more efficient.

In this gaussian blur shader we can get completely rid of the dependent reads. The key is to compute all the texture coordinates in the vertex shader and store them in varyings. They will be automatically interpolated as usual and you will have the benefits of the texture pre-fetch that occurs before the GPU runs the fragment shader. In GLSL ES we can use up to 32 floats in our varyings, which allow us to pre-compute up to 16 vec2 texture coordinates in the vertex shader.

We need two vertex shaders, one for the first pass which computes the horizontal texture coordinates and another for the second pass which computes the vertical texture coordinates. And we need a single fragment shader that we can use in both passes. This is what the first vertex shader looks like:

[code lang=”C”]
/* HBlurVertexShader.glsl */
attribute vec4 a_position;
attribute vec2 a_texCoord;

varying vec2 v_texCoord;
varying vec2 v_blurTexCoords[14];

void main()
{
gl_Position = a_position;
v_texCoord = a_texCoord;
v_blurTexCoords[ 0] = v_texCoord + vec2(-0.028, 0.0);
v_blurTexCoords[ 1] = v_texCoord + vec2(-0.024, 0.0);
v_blurTexCoords[ 2] = v_texCoord + vec2(-0.020, 0.0);
v_blurTexCoords[ 3] = v_texCoord + vec2(-0.016, 0.0);
v_blurTexCoords[ 4] = v_texCoord + vec2(-0.012, 0.0);
v_blurTexCoords[ 5] = v_texCoord + vec2(-0.008, 0.0);
v_blurTexCoords[ 6] = v_texCoord + vec2(-0.004, 0.0);
v_blurTexCoords[ 7] = v_texCoord + vec2( 0.004, 0.0);
v_blurTexCoords[ 8] = v_texCoord + vec2( 0.008, 0.0);
v_blurTexCoords[ 9] = v_texCoord + vec2( 0.012, 0.0);
v_blurTexCoords[10] = v_texCoord + vec2( 0.016, 0.0);
v_blurTexCoords[11] = v_texCoord + vec2( 0.020, 0.0);
v_blurTexCoords[12] = v_texCoord + vec2( 0.024, 0.0);
v_blurTexCoords[13] = v_texCoord + vec2( 0.028, 0.0);
}
[/code]

As you can see, the v_blurTexCoords varying is an array of vec2 where we store a bunch of vectors around the texture coordinate of the vertex a_texCoord. Each vec2 in this array will be interpolated along the triangles being rendered, as expected. We do the same for the other vertex shader which pre-computes the vertical blur texture coordinates:

[code lang=”c”]
/* VBlurVertexShader.glsl */
attribute vec4 a_position;
attribute vec2 a_texCoord;

varying vec2 v_texCoord;
varying vec2 v_blurTexCoords[14];

void main()
{
gl_Position = a_position;
v_texCoord = a_texCoord;
v_blurTexCoords[ 0] = v_texCoord + vec2(0.0, -0.028);
v_blurTexCoords[ 1] = v_texCoord + vec2(0.0, -0.024);
v_blurTexCoords[ 2] = v_texCoord + vec2(0.0, -0.020);
v_blurTexCoords[ 3] = v_texCoord + vec2(0.0, -0.016);
v_blurTexCoords[ 4] = v_texCoord + vec2(0.0, -0.012);
v_blurTexCoords[ 5] = v_texCoord + vec2(0.0, -0.008);
v_blurTexCoords[ 6] = v_texCoord + vec2(0.0, -0.004);
v_blurTexCoords[ 7] = v_texCoord + vec2(0.0, 0.004);
v_blurTexCoords[ 8] = v_texCoord + vec2(0.0, 0.008);
v_blurTexCoords[ 9] = v_texCoord + vec2(0.0, 0.012);
v_blurTexCoords[10] = v_texCoord + vec2(0.0, 0.016);
v_blurTexCoords[11] = v_texCoord + vec2(0.0, 0.020);
v_blurTexCoords[12] = v_texCoord + vec2(0.0, 0.024);
v_blurTexCoords[13] = v_texCoord + vec2(0.0, 0.028);
}
[/code]

The fragment shader just computes the gaussian-weighted average of the color of the texels on these pre-computed texture coordinates. Since it uses the generic v_blurTexCoords array, we can use this same fragment shader in both passes.

[code lang=”c”]
/* BlurFragmentShader.glsl */
precision mediump float;

uniform sampler2D s_texture;

varying vec2 v_texCoord;
varying vec2 v_blurTexCoords[14];

void main()
{
gl_FragColor = vec4(0.0);
gl_FragColor += texture2D(s_texture, v_blurTexCoords[ 0])*0.0044299121055113265;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[ 1])*0.00895781211794;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[ 2])*0.0215963866053;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[ 3])*0.0443683338718;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[ 4])*0.0776744219933;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[ 5])*0.115876621105;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[ 6])*0.147308056121;
gl_FragColor += texture2D(s_texture, v_texCoord )*0.159576912161;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[ 7])*0.147308056121;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[ 8])*0.115876621105;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[ 9])*0.0776744219933;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[10])*0.0443683338718;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[11])*0.0215963866053;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[12])*0.00895781211794;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[13])*0.0044299121055113265;
}
[/code]

As you can see, all the texture reads (calls to the built-in texture2D function) receive a pre-computed texture coordinate, that is why the hardware is able to pre-fetch the texture data for these texture reads, which is a huge improvement. For more details about what happens in the hardware, check out the section 5 of the PowerVR SGX Architecture Guide for Developers.

Then, to render the blurred image we draw into an off-screen buffer (Render toTexture) the first pass using the HBlurVertexShader and the BlurFragmentShader and the second pass using the VBlurVertexShader and the BlurFragmentShader. Of course the second pass might be rendered directly into the main frame buffer if no more post processing is going to take place.

Obviously, this technique can also be applied in any other similar scenarios. You can find an implementation of blur and sharpen filters in XBImageFilters.

Desfoque Gaussiano Mais Rápido em GLSL

Uma técnica popular para se fazer o desfoque gaussiano utilizando OpenGL é um desfoque em dois passos utilizando shaders, onde fazemos um passo com desfoque na horizontal e em seguida fazermos um segundo passo com desfoque na vertical, também conhecido como convolução. Este é um post clássico sobre essa técnica http://www.gamerendering.com/2008/10/11/gaussian-blur-filter-shader/, e muitos outros disponíveis na web fazem exatamente a mesma coisa. Porém, a performance do shader pode ser melhorada e muito.

O gargalo nesse shader comumente usado para conseguir o desfoque gaussiano são as leituras de textura dependentes (dependent texture reads), que são quaisquer leituras de textura (utilizando as funções texture1D, texture2D ou texture3D) em um fragment shader onde as coordenadas dependem de algum cálculo, ou quaisquer leituras de textura em um vertex shader. Uma leitura de textura independente é uma em que as coordenadas são ou uma constante, ou uma variável varying. De acordo com o PowerVR Performance Recommendations, uma leitura de textura dependente adiciona vários passos a mais no processamento do shader, pois os valores das coordenadas de textura não são conhecidos antes da execução do mesmo. Quando fazemos apenas leituras independentes, a GPU consegue fazer um pre-fetch(leitura com antecedência) do valor da textura na coordenada que ela já sabe qual vai ser, e isso pode melhorar a performance de forma significativa.

Nesse novo shader de desfoque gaussiano eliminamos as leituras dependentes completamente. O segredo é calcular todas as coordenadas de textura no vertex shader e armazená-las em varyings. Elas serão interpoladas ao longo dos triângulos automaticamente e a GPU será capaz de fazer as leituras de textura antes de executar o fragment shader. No GLSL ES podemos utilizar até 32 floats como varyings, o que nos permite pré-calcular até 16 coordenadas de textura 2D utilizando arrays de vec2 no vertex shader.

Precisamos de dois vertex shaders, um para o primeiro passo onde calculamos as coordenadas de textura horizontais, e outro para o segundo passo onde calculamos as coordenadas de textura verticais. Por fim, precisamos de um único fragment shader que pode ser utilizado em ambos os passos. A seguir, o primeiro vertex shader:

[code lang=”C”]
/* HBlurVertexShader.glsl */
attribute vec4 a_position;
attribute vec2 a_texCoord;

varying vec2 v_texCoord;
varying vec2 v_blurTexCoords[14];

void main()
{
gl_Position = a_position;
v_texCoord = a_texCoord;
v_blurTexCoords[ 0] = v_texCoord + vec2(-0.028, 0.0);
v_blurTexCoords[ 1] = v_texCoord + vec2(-0.024, 0.0);
v_blurTexCoords[ 2] = v_texCoord + vec2(-0.020, 0.0);
v_blurTexCoords[ 3] = v_texCoord + vec2(-0.016, 0.0);
v_blurTexCoords[ 4] = v_texCoord + vec2(-0.012, 0.0);
v_blurTexCoords[ 5] = v_texCoord + vec2(-0.008, 0.0);
v_blurTexCoords[ 6] = v_texCoord + vec2(-0.004, 0.0);
v_blurTexCoords[ 7] = v_texCoord + vec2( 0.004, 0.0);
v_blurTexCoords[ 8] = v_texCoord + vec2( 0.008, 0.0);
v_blurTexCoords[ 9] = v_texCoord + vec2( 0.012, 0.0);
v_blurTexCoords[10] = v_texCoord + vec2( 0.016, 0.0);
v_blurTexCoords[11] = v_texCoord + vec2( 0.020, 0.0);
v_blurTexCoords[12] = v_texCoord + vec2( 0.024, 0.0);
v_blurTexCoords[13] = v_texCoord + vec2( 0.028, 0.0);
}
[/code]

O v_blurTexCoords é uma varying com tipo array de vec2 onde colocamos um grupo de vetores em torno da coordenada de textura do vértice a_texCoord. Cada vec2 nesse array será interpolado ao longo do triângulo que estiver sendo renderizado. Fazemos o mesmo no outro vertex shader, só que dessa vez variando a coordenada de textura na vertical:

[code lang=”c”]
/* VBlurVertexShader.glsl */
attribute vec4 a_position;
attribute vec2 a_texCoord;

varying vec2 v_texCoord;
varying vec2 v_blurTexCoords[14];

void main()
{
gl_Position = a_position;
v_texCoord = a_texCoord;
v_blurTexCoords[ 0] = v_texCoord + vec2(0.0, -0.028);
v_blurTexCoords[ 1] = v_texCoord + vec2(0.0, -0.024);
v_blurTexCoords[ 2] = v_texCoord + vec2(0.0, -0.020);
v_blurTexCoords[ 3] = v_texCoord + vec2(0.0, -0.016);
v_blurTexCoords[ 4] = v_texCoord + vec2(0.0, -0.012);
v_blurTexCoords[ 5] = v_texCoord + vec2(0.0, -0.008);
v_blurTexCoords[ 6] = v_texCoord + vec2(0.0, -0.004);
v_blurTexCoords[ 7] = v_texCoord + vec2(0.0, 0.004);
v_blurTexCoords[ 8] = v_texCoord + vec2(0.0, 0.008);
v_blurTexCoords[ 9] = v_texCoord + vec2(0.0, 0.012);
v_blurTexCoords[10] = v_texCoord + vec2(0.0, 0.016);
v_blurTexCoords[11] = v_texCoord + vec2(0.0, 0.020);
v_blurTexCoords[12] = v_texCoord + vec2(0.0, 0.024);
v_blurTexCoords[13] = v_texCoord + vec2(0.0, 0.028);
}
[/code]

O fragment shader apenas calcula a média ponderada pela função gaussiana da cor dos texels (texture pixels) nas coordenadas de textura pré-calculadas. Utilizamos o nome v_blurTexCoords para o array de coordenadas de textura em ambos vertex shaders, portanto podemos utilizar esse fragment shader em ambos os passos sem problemas.

[code lang=”c”]
/* BlurFragmentShader.glsl */
precision mediump float;

uniform sampler2D s_texture;

varying vec2 v_texCoord;
varying vec2 v_blurTexCoords[14];

void main()
{
gl_FragColor = vec4(0.0);
gl_FragColor += texture2D(s_texture, v_blurTexCoords[ 0])*0.0044299121055113265;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[ 1])*0.00895781211794;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[ 2])*0.0215963866053;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[ 3])*0.0443683338718;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[ 4])*0.0776744219933;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[ 5])*0.115876621105;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[ 6])*0.147308056121;
gl_FragColor += texture2D(s_texture, v_texCoord )*0.159576912161;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[ 7])*0.147308056121;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[ 8])*0.115876621105;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[ 9])*0.0776744219933;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[10])*0.0443683338718;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[11])*0.0215963866053;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[12])*0.00895781211794;
gl_FragColor += texture2D(s_texture, v_blurTexCoords[13])*0.0044299121055113265;
}
[/code]

Todas as leituras de textura (chamadas à função texture2D) recebem uma coordenada de textura pré-calculada, e por isso a GPU não precisa fazer a leitura da textura durante a execução do fragment shader. Para conhecer em detalhes o que acontece no hardware, dê uma olhada na seção 5 do PowerVR SGX Architecture Guide for Developers.

Então, para desenhar uma imagem desfocada desenhamos o primeiro passo numa outra textura (Render to Texture) utilizando o HBlurVertexShader e o BlurFragmentShader, e em seguida desenhamos em outro alvo utilizando o VBlurVertexShader e o BlurFragmentShader. Claro que o segundo passo pode ser renderizado diretamente no frame buffer principal se não houver mais pós-processamento a ser feito.

Obviamente esta técnica pode também ser aplicada em situações similares. Você pode encontrar uma implementação de shaders de desfoque e sharpen no XBImageFilters.

Eliminando condicionais em shaders

Talvez você já deve ter lido em algum lugar que as GPUs odeiam condicionais (if-elses, for e while loops dinâmicos). Maiores detalhes podem ser encontrados aqui http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter34.html. Em vez de tentar achar a melhor configuração para seus condicionais, que tal se livrar completamente deles? Isso é possível e não é tão difícil de fazer na maioria dos casos.

A seguir vou mostrar como eliminar condicionais em shaders com um exemplo. Isso usualmente é feito utilizando built-ins do GLSL como mix() (lerp() em HLSL), clamp(), sign() e abs(). Neste exemplo vou escrever um fragment shader que faz um “overlay” de uma textura sobre outra. Nesse caso o overlay é o Photoshop Overlay Blending Mode, utilizando as fórmulas desse post clássico http://mouaif.wordpress.com/2009/01/05/photoshop-math-with-glsl-shaders/. A implementação padrão do overlay requer um condicional para cada um dos canais RGB. Basicamente, ele faz o seguinte: para da canal, se o valor na imagem base for menor que 0.5, retorne 2 * base * blend; caso contrário, retorne 1 - 2 * (1 - base) * (1 - blend). Onde blend é a imagem que está sobre a imagem base. Em outras palavras, se o valor for menor que 0.5, faça um multiply, senão faça um screen. Então, o fragment shader fica dessa forma:

[code lang=”c”]
// Obs.: Isto é GLSL ES
precision highp float;

uniform sampler2D s_texture; // textura Base
uniform sampler2D s_overlay; // textura Overlay

varying vec2 v_texCoord;

float overlayf(float base, float blend)
{
if (base < 0.5) {
return 2.0 * base * blend;
}
else {
return 1.0 – 2.0 * (1.0 – base) * (1.0 – blend);
}
}

vec3 overlay(vec3 base, vec3 blend)
{
return vec3(overlayf(base.r, blend.r), overlayf(base.g, blend.g), overlayf(base.b, blend.b));
}

void main()
{
vec4 base = texture2D(s_texture, v_texCoord);
vec4 blend = texture2D(s_overlay, v_texCoord);
vec3 overlay = overlay(base.rgb, blend.rgb); // cor Overlay
// Interpolar linearmente entre a cor base e a cor overlay pois
// a textura de blend pode ter transparência. A função mix é a ideal
// para a situação pois mix(x, y, a) = x*(1.0 – a) + y*a
vec3 finalColor = mix(base.rgb, overlay.rgb, blend.a);
gl_FragColor = vec4(finalColor, base.a);
}
[/code]

Podemos deixar esse código mais simples empregando o uso de built-ins. De modo geral, temos que converter a comparação (base < 0.5) em um float que é zero se base for menor que 0.5, e é 1 caso contrário. Depois usamos esse float como o parâmetro de interpolação (o terceiro parâmetro) na função mix(). Isto vai nos permitir selecionar entre o primeiro e o segundo parâmetro já que esse float é zero (mix() retorna primeiro parâmetro) ou um (mix() retorna o segundo parâmetro). Por exemplo:

[code lang=”c”]
float a = 0.0;
float x = 1.0;
float y = 2.0;
float z = mix(x, y, a); // z será igual a x
float b = 1.0;
float k = mix(x, y, b); // k será igual a y
[/code]

Então, o shader final deve ficar assim:

[code lang=”c”]
precision highp float;

uniform sampler2D s_texture;
uniform sampler2D s_overlay;

varying vec2 v_texCoord;
varying vec2 v_rawTexCoord;

void main()
{
vec4 base = texture2D(s_texture, v_texCoord);
vec4 blend = texture2D(s_overlay, v_texCoord);
// Este é o nosso parâmetro seletor. Para cada uma dos componentes rgb, subtraímos 0.5
// e obtemos o sign() do resultado. Ele vai retornar -1.0 se menor que 0.0, 0.0 se 0.0,
// ou 1.0 se maior que 0.0. Nesse caso, nós queremos que o resultado final seja 0.0 quando
// a subtração for menor que zero, e 1.0 caso contrário, portanto utilizamos clamp() para
// manter o valor no intervalo [0.0, 1.0]. Assim, quando menor que zero, ela vai
// retornar -1.0 e este valor será jogado para 0.0 devido ao clamp.
vec3 br = clamp(sign(base.rgb – vec3(0.5)), vec3(0.0), vec3(1.0));
vec3 multiply = 2.0 * base.rgb * blend.rgb;
vec3 screen = vec3(1.0) – 2.0 * (vec3(1.0) – base.rgb)*(vec3(1.0) – blend.rgb);
// Se br for 0.0, overlay será um multiply (equivalente a (base < 0.5) { return multiply; }).
// Se bt for 1.0, overlay será um screen (equivalente a (base >= 0.5) { return screen; }).
vec3 overlay = mix(multiply, screen, br);
vec3 finalColor = mix(base.rgb, overlay, blend.a);
gl_FragColor = vec4(finalColor, base.a);
}
[/code]

Agora o código está mais curto e limpo e faz exatamente a mesma coisa. A diferença é que calculamos ambos os valores do if e else e depois selecionamos um deles. Quando utilizamos condicionais, nós acabamos não perdendo tempo calculando valores que não serão utilizados, em que nesse caso possui um custo insignificante. Do outro lado, não temos condicional algum no código, o que mantém a GPU mais feliz. Dependendo do hardware e da complexidade do shader pode haver uma melhora na performance.

Eliminating branches in shaders

You might have heard GPUs hate branches (if-elses, and consequently dynamic for- and while-loops). See more details here http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter34.html. Instead of trying to figure out how to best setup your branches, how about getting completely rid of them? That is possible and easy to do in most cases.

I am gonna show you how to eliminate branches in shaders with one example. That is usually done using GLSL built-in functions like mix() (lerp() in HLSL), clamp(), sign() and abs(). In this example I will write a fragment shader that “overlays” one texture on top of another. In this case the overlay is the Photoshop Overlay Blending Mode. I am using the formulas from this classic post http://mouaif.wordpress.com/2009/01/05/photoshop-math-with-glsl-shaders/. The naïve implementation of the Overlay blending requires one branch for each of the RGB channels. Basically, what it does is the following: for each channel, if the value in the base image is smaller than 0.5, return 2 * base * blend; otherwise, return 1 - 2 * (1 - base) * (1 - blend). Where blend is the image on top of the base image. Conceptually, if the value is lower than 0.5, multiply, else screen it. Then, the fragment shader would end up looking like this:

[code lang=”c”]
// Note: This is GLSL ES
precision highp float;

uniform sampler2D s_texture; // Base texture
uniform sampler2D s_overlay; // Overlay texture

varying vec2 v_texCoord;

float overlayf(float base, float blend)
{
if (base < 0.5) {
return 2.0 * base * blend;
}
else {
return 1.0 – 2.0 * (1.0 – base) * (1.0 – blend);
}
}

vec3 overlay(vec3 base, vec3 blend)
{
return vec3(overlayf(base.r, blend.r), overlayf(base.g, blend.g), overlayf(base.b, blend.b));
}

void main()
{
vec4 base = texture2D(s_texture, v_texCoord);
vec4 blend = texture2D(s_overlay, v_texCoord);
vec3 overlay = overlay(base.rgb, blend.rgb); // Overlay’d color
// Linearly interpolate between the base color and overlay’d color
// because the blend texture might have transparency. The built-in
// mix does the job since mix(x, y, a) = x*(1.0 – a) + y*a
vec3 finalColor = mix(base.rgb, overlay.rgb, blend.a);
gl_FragColor = vec4(finalColor, base.a);
}
[/code]

That’s quite a lot of code.. It’s possible to make it much simpler with the smart use of the built-in functions. The general technique is to convert the comparison (base < 0.5) into a float which is zero if base is below 0.5, and is one otherwise. Then we use this float as the interpolation parameter (the third) in the mix() built-in. It will allow us to select between the first and the second parameters since it is either zero (then mix() returns the first parameter) or one (mix() returns the second parameter), for example:

[code lang=”c”]
float a = 0.0;
float x = 1.0;
float y = 2.0;
float z = mix(x, y, a); // z will be equals to x
float b = 1.0;
float k = mix(x, y, b); // k will be equals to y
[/code]

Then, our final overlay shader code will be:

[code lang=”c”]
precision highp float;

uniform sampler2D s_texture;
uniform sampler2D s_overlay;

varying vec2 v_texCoord;
varying vec2 v_rawTexCoord;

void main()
{
vec4 base = texture2D(s_texture, v_texCoord);
vec4 blend = texture2D(s_overlay, v_texCoord);
// This is our ‘selection parameter’. For each of the rgb components, we subtract 0.5
// and take the sign() of the result. It will return -1.0 if smaller than 0.0, 0.0 if 0.0,
// and 1.0 if greater than 0.0. In this case, we want for the final result to be 0.0
// when the subtraction is smaller than zero and 1.0 otherwise, then we clamp() the value
// in the [0.0, 1.0] interval. Hence, when smaller than zero, it will return -1.0 and this
// value will be clamped to 0.0.
vec3 br = clamp(sign(base.rgb – vec3(0.5)), vec3(0.0), vec3(1.0));
vec3 multiply = 2.0 * base.rgb * blend.rgb;
vec3 screen = vec3(1.0) – 2.0 * (vec3(1.0) – base.rgb)*(vec3(1.0) – blend.rgb);
// If br is 0.0, overlay will be multiply (which translates to if (base < 0.5) { return multiply; }).
// if bt is 1.0, overlay will be screen (which translates to if (base >= 0.5) { return screen; }).
vec3 overlay = mix(multiply, screen, br);
vec3 finalColor = mix(base.rgb, overlay, blend.a);
gl_FragColor = vec4(finalColor, base.a);
}
[/code]

Now the code is much shorter and cleaner and performs exactly the same function. The difference is that we compute both the values of the if and the else and then select one of them. When branching, we avoid wasting the computation of the unused value, which in this example has a negligible cost. On the other hand, we have no branches, which keeps the GPU happy. Depending on your hardware and shader complexity, you might notice a performance improvement.

UIView – Frame and Transform

You should be careful when messing around with the transform property of a view. Usually, when doing it you should not touch the frame property of the view (unless you know what you’re doing), because the frame is the axis-aligned bounding rectangle (AABR) of a view (also known as axis-aligned bounding box). Then, it changes whenever you change the transform of your view, and this change might look really weird for you if you don’t know how this works, which is what I am gonna show in this post.

The AABR is the smallest rectangle aligned with the coordinate axes (the x (horizontal) and y (vertical) axes) that contains a given shape, in this case, an UIView instance. So, if you rotate the view a little, the previous AABR won’t anymore contain the view completely, then it must change. I have setup a simple example project on Github to display this relationship between the frame and the transform of an UIView, which you can find here https://github.com/xissburg/FrameTransform. Here we can see two screenshots of it:

Basically, it contains one view in the front (dark gray) and another in the back (light gray). The view in the front is transformed by gestures (pan, pinch and rotate) and after the transform is applied the frame of the front view is assigned to the frame of the back view. In the previous image, we have on the left the front view in its initial configuration, no transforms applied. On the right, a small rotation was applied and we can see what its frame looks like now (it is the light gray rectangle). It has increased to accommodate the new shape of the front view. When it is scaled the same thing happens, the frame is also scaled to accommodate the view again.

The conclusion is, be careful when changing both the transform and the frame of a view. I am not saying you should not do that, because there are a few situations where it is handy to change both to obtain the result you want. That is why in most cases you should use the bounds of the view instead, because the bounds don’t change under transforms.