Aspect Fit and Aspect Fill content mode with OpenGL ES 2.0

Viewed 1407

I need to add two new content modes to display my textures with OpenGL ES 2.0 : "Aspect Fit" and "Aspect Fill'.

Here's an image explaining the different content modes :

I already have the "Scale to fill" content mode, which is the default behavior I guess.

Here's my basic vertex shader code for textures :

precision highp float;

attribute vec2 aTexCoordinate;
varying vec2 vTexCoordinate;
uniform mat4 uMVPMatrix;
attribute vec4 vPosition;

void main() {
    vTexCoordinate = aTexCoordinate;
    gl_Position = uMVPMatrix * vPosition;
}

And here's my fragment shader for textures :

precision mediump float;

uniform vec4 vColor;
uniform sampler2D uTexture;
varying vec2 vTexCoordinate;

void main() {
    // premultiplied alpha
    vec4 texColor = texture2D(uTexture, vTexCoordinate);
    // Scale the texture RGB by the vertex color
    texColor.rgb *= vColor.rgb;
    // Scale the texture RGBA by the vertex alpha to reinstate premultiplication
    gl_FragColor = texColor * vColor.a;
    //gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}

For the "Aspect Fill" mode, I thought I could play with the texture coordinates to crop the image. But for the "Aspect Fit" mode, I don't have a clear idea on how I could do it in the shaders, and even better with a red background color like in the screenshot.

3 Answers

You have to scale the texture coordinates. I recommend to add a uniform for scaling the texture:

uniform vec2 textureScale;

void main()
{
    vTexCoordinate = textureScale * (aTexCoordinate - 0.5) + 0.5;
    // [...]
}

Compute the aspect ratio of the texture:

aspect = textrueHeight / textureWidth

The the textureScale has to be set dependent on the mode:

  • "Scale To Fill": vec2(1.0, 1.0).
  • landscape "Aspect Fit": vec2(1.0, aspect).
  • landscape "Aspect Fill": vec2(1.0/aspect, 1.0).
  • portrait "Aspect Fit": vec2(1.0/aspect, 1.0).
  • portrait "Aspect Fill": vec2(1.0, aspect).

You have to set the texture wrap parameters (GL_TEXTURE_WRAP_S, GL_TEXTURE_WRAP_T) mode to GL_CLAMP_TO_EDGE. See glTexParameter.

Alternatively you can discard superfluous fragments in the fragment shader:

void main() {
    if (vTexCoordinate.x < 0.0 || vTexCoordinate.x > 1.0 ||
        vTexCoordinate.y < 0.0 || vTexCoordinate.y > 1.0) {
    
        discard;
    }

    // [...]
}   

Thanks to @Rabbid76 and his answer here , I managed to do it after adding this part to his answer :

float textureAspect = (float)textureSize.width / (float)textureSize.height;
float frameAspect = (float)frameSize.width / (float)frameSize.height;

float scaleX = 1, scaleY = 1;
float textureFrameRatio = textureAspect / frameAspect;
BOOL portraitTexture = textureAspect < 1;
BOOL portraitFrame = frameAspect < 1;

if(contentMode == AspectFill) {
    if(portraitFrame)
        scaleX = 1.f / textureFrameRatio;
    else
        scaleY = textureFrameRatio;
}
else if(contentMode == AspectFit) {
    if(portraitFrame)
        scaleY = textureFrameRatio;
    else
        scaleX = 1.f / textureFrameRatio;
}

Then I do vec2(scaleX, scaleY)

A metal fragment shader, For making a texture aspect fill/fit within an expected size.

fragment float4 fragment_aspect_fitfill(
                               VertexOut vertexIn [[stage_in]],
                               texture2d<float, access::sample> sourceTexture [[texture(0)]],
                               sampler sourceSampler [[sampler(0)]],
                               constant float2 &size [[ buffer(0) ]],
                               constant float &contentMode [[ buffer(1) ]])
{
    float2 uv = vertexIn.textureCoordinate;
    
    //Calculate Aspect Ration for both Texture and Expected output texture
    float textureAspect = (float)sourceTexture.get_width() / (float)sourceTexture.get_height();
    float frameAspect = (float)size.x / (float)size.y;
    
    
    float scaleX = 1, scaleY = 1;
    float textureFrameRatio = textureAspect / frameAspect;
    bool portraitTexture = textureAspect < 1;
    bool portraitFrame = frameAspect < 1;
    
    
    // Content mode 0 is for aspect Fill, 1 is for Aspect Fit
    if(contentMode == 0.0) {
        if(portraitFrame)
            scaleX = 1.f / textureFrameRatio;
        else
            scaleY = textureFrameRatio;
    } else if(contentMode == 1.0) {
        if(portraitFrame)
            scaleY = textureFrameRatio;
        else
            scaleX = 1.f / textureFrameRatio;
    }
    
    float2 textureScale = float2(scaleX, scaleY);
    float2 vTexCoordinate = textureScale * (uv - 0.5) + 0.5;
    

    return sourceTexture.sample(sourceSampler, vTexCoordinate);
}

** Note: Use MetalPetal for better understanding some struct like VertexOut etc.

** This fragment shader algorithm may not be perfect, but this works and worked for me.

Related