Search code examples
iosswiftshadermetaltexture2d

Centered fill/fit 2D texture in Shader Metal


I need to center it on a 2D texture when adjusting fit/fill texture in the view, but I can't configure uv coords.

Original image

When adjust fill, show the first part of the image not the center:

Fill image

when adjust fit, not get the correct center:

Fit image

float2 adjustPos(float2 size,
                 float2 uv) {
    uv.x /= size.x;
    uv.y /= size.y;
    uv.y = 1.0f - uv.y;
    
    return uv;
}

float2 scaleTexture(texture2d<float, access::sample> tex2d,
                    float2 size,
                    float2 uv,
                    int mode) {
    int width = tex2d.get_width();
    int height = tex2d.get_height();
    float widthRatio  = size.x/width;
    float heightRatio = size.y/height;
    float2 pos;
    
    if (mode == 0) { // Aspect Fit
        int2 newSize = int2(width*widthRatio, height*widthRatio);
        pos = adjustPos(float2(newSize), uv);
        float y = (uv.y/size.y) / 2.0;
        y = y-pos.y;
        y = 1.0f-y;
        pos.y = y;
    } else if (mode == 1) { // Aspect Fill
        int2 newSize = int2(width*heightRatio, height*heightRatio);
        pos = adjustPos(float2(newSize), uv);
        
        if (newSize.x != size.x) {
            pos.x  = 0.5f + ((pos.x - 0.5f) * (1.0f - (heightRatio/100)));
        }
    } else {
        float scale = min(widthRatio, heightRatio);
        int2 newSize = int2(width*scale, height*scale);
        pos = adjustPos(float2(newSize), uv);
    }
    
    return pos;
}

Solution

  • You can use this shader and customize it as per your requirements. This solution will let you make a texture fit fill within a given size.

    The following metal shader takes a texture, an output size, expected content mode, and returns a fit/fill image within the given 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);
    }
    

    *Tips: This MSL uses some struct of MetalPetal.