HLSL shader for infinite background only works on square textures

I’m trying to write a shader that draws a seamless background infinitely in Monogame. The code is based on: http://www.david-gouveia.com/portfolio/scrolling-textures-with-zoom-and-rotation/

Here are the textures I’m using for the demonstration: http://imgur.com/a/n9WrG

sampler TextureSampler : register(s0);
float2 ViewportSize;
float4x4 ScrollMatrix;
struct VertexToPixel {
    float4 Position : SV_Position0;
    float4 TexCoord : TEXCOORD0;
    float4 Color : COLOR0;
};
VertexToPixel SpriteVertexShader(float4 color : COLOR0, float4 texCoord : TEXCOORD0, float4 position : POSITION0) {
    VertexToPixel Output = (VertexToPixel)0;

    // Half pixel offset for correct texel centering.
    position.xy -= 0.5;

    // Viewport adjustment.
    position.xy = position.xy / ViewportSize;
    position.xy *= float2(2, -2);
    position.xy -= float2(1, -1);

    // Transform our texture coordinates to account for camera
    texCoord = mul(texCoord, ScrollMatrix);
    
    //pass position and color to PS
    Output.Color = color;
    Output.Position = position;
    Output.TexCoord = texCoord;

    return Output;
}

technique SpriteBatch {
    pass {
        VertexShader = compile vs_2_0 SpriteVertexShader();
    }
}

I’m using Monogame.Extended so I can use their Camera2D class. I have this function to draw my texture:

public static void drawSeamlessBackground(SpriteBatch s, Texture2D t, GraphicsDevice gd, float parallax, Camera2D cam) {
    //TODO: Make this work with non square textures.
    Vector2 textureSize = new Vector2(t.Width, t.Height);
    Rectangle view = gd.Viewport.Bounds;
    Matrix m = Matrix.CreateTranslation(new Vector3(-cam.Origin / textureSize, 0.0f)) *
                Matrix.CreateScale(1f / cam.Zoom) *
                Matrix.CreateRotationZ(-cam.Rotation) *
                Matrix.CreateTranslation(new Vector3(cam.Origin / textureSize, 0.0f)) *
                Matrix.CreateTranslation(new Vector3((cam.Position * parallax) / textureSize, 0.0f));

    infiniteShader.Parameters["ScrollMatrix"].SetValue(m);
    //Normally this next line would not be here since the viewport doesn't usually change.
    infiniteShader.Parameters["ViewportSize"].SetValue(new Vector2(view.Width, view.Height));

    s.Begin(samplerState: SamplerState.LinearWrap, effect: infiniteShader);
    s.Draw(t, new Vector2(0, 0), view, Color.White);
    s.End();
}

This whole thing works fine for square textures, but when the width and the height are different, there’s distortion that happens when I rotate my camera.

In the next image, there are two textures, one that is square and one that is a rectangle.

Here is what it looks like with no rotation:
No Camera Rotation

Here is what it looks like with some rotation:
With some Camera Rotation

The red texture has square dimensions (500x500). The white texture has rectangular dimensions (1280x720).

I’m sure this is a simple fix, but I can’t figure it out.

After working on this for a while, I found a solution. A shear happens when scaling is done before a rotation. It turns out my texCoord had the wrong ratio. It needed to use the texture’s ratio. The shader is perfectly fine, but I need to apply the texture’s ratio to my matrix before the rotation.

public static void drawSeamlessBackground(SpriteBatch s, Texture2D t, GraphicsDevice gd, float parallax, Camera2D cam) {
    Vector2 textureSize = new Vector2(t.Width, t.Height);
    Rectangle view = gd.Viewport.Bounds;

    Vector2 textureRatio = new Vector2(t.Width / t.Height, 1);
    if (t.Width < t.Height) {
        textureRatio = new Vector2(1, t.Height / t.Width);
    }

    Matrix m = Matrix.CreateTranslation(new Vector3(-cam.Origin / textureSize, 0.0f)) *
                Matrix.CreateScale(1f / cam.Zoom) *
                Matrix.CreateScale(textureRatio.X, textureRatio.Y, 1) *
                Matrix.CreateRotationZ(-cam.Rotation) *
                Matrix.CreateScale(1f / textureRatio.X, 1f /textureRatio.Y, 1) *
                Matrix.CreateTranslation(new Vector3(cam.Origin / textureSize, 0.0f)) *
                Matrix.CreateTranslation(new Vector3((cam.Position * parallax) / textureSize, 0.0f));

    infiniteShader.Parameters["ScrollMatrix"].SetValue(m);
    infiniteShader.Parameters["ViewportSize"].SetValue(new Vector2(view.Width, view.Height));

    s.Begin(samplerState: SamplerState.LinearWrap, effect: infiniteShader);
    s.Draw(t, new Vector2(0, 0), view, Color.White);
    s.End();
}

Because of rounding errors though, it’s best to use textures that have nice ratios. For example, if I have a texture that is 1920x1080 in size (ratio of 1,777777777777778), I will still get shearing, but if I make that 2000x1000 (ratio of 2), I don’t get any.

Using a custom shader for something like a repeating background is unnecessary. Just use this method and you won’t need to use a custom shader, and it’s quite a bit simpler, and you can use any size or ratio of texture that you want.

public static void DrawSeamlessBackground(SpriteBatch s, Texture2D t, GraphicsDevice gd, Camera2D cam)
{
    Rectangle destination = cam.BoundingRectangle.ToRectangle();
    PresentationParameters parameters = gd.PresentationParameters;
    destination.Width = parameters.BackBufferWidth * 4;
    destination.Height = parameters.BackBufferHeight * 4;
    destination.X -= parameters.BackBufferWidth * 2;
    destination.Y -= parameters.BackBufferHeight * 2;
    Rectangle source = destination;
        

    s.Begin(samplerState: SamplerState.LinearWrap, transformMatrix: cam.GetViewMatrix());
    s.Draw(t, destination, source, Color.White);
    s.End();
}

Note that the parts using the BackBufferWidth and BackBufferHeight are a bit hacky. I couldn’t be bothered to properly calculate the size of the destination and source rectangles, so I just set them to widths and heights that will be big enough no matter how the camera is rotated. This will not work right if you zoom the camera out. You’ll have to be smarter about the size of the destination and source rectangle if you want to allow zooming out.

Also note that I have next to no experience with MonoGame.Extended and the Camera2D class, I just quickly cobbled together this code to show that you don’t need a custom shader to have a repeating background.

I was actually using this before. It works with scaling and will always cover the whole window:

public static void drawSeamlessBackground(SpriteBatch s, Matrix transforMatrix, Rectangle area, Texture2D texture) {
    Point backgroundTopLeft = Vector2.Transform(new Vector2(area.X - 1, area.Y - 1), Matrix.Invert(transforMatrix)).ToPoint();
    Point backgroundBottomRight = Vector2.Transform(new Vector2(area.Width + 2, area.Height + 2), Matrix.Invert(transforMatrix)).ToPoint();
    Point backgroundTopRight = Vector2.Transform(new Vector2(area.Width + 2, area.X - 1), Matrix.Invert(transforMatrix)).ToPoint();
    Point backgroundBottomLeft = Vector2.Transform(new Vector2(area.Y - 1, area.Height + 2), Matrix.Invert(transforMatrix)).ToPoint();

    int leftPoint = Math.Min(Math.Min(backgroundTopRight.X, backgroundTopLeft.X), Math.Min(backgroundBottomRight.X, backgroundBottomLeft.X));
    int topPoint = Math.Min(Math.Min(backgroundTopRight.Y, backgroundTopLeft.Y), Math.Min(backgroundBottomRight.Y, backgroundBottomLeft.Y));
    int rightPoint = Math.Max(Math.Max(backgroundTopRight.X, backgroundTopLeft.X), Math.Max(backgroundBottomRight.X, backgroundBottomLeft.X));
    int bottomPoint = Math.Max(Math.Max(backgroundTopRight.Y, backgroundTopLeft.Y), Math.Max(backgroundBottomRight.Y, backgroundBottomLeft.Y));

    Point realBackground = new Point(rightPoint - leftPoint, bottomPoint - topPoint);

    s.Begin(transformMatrix: transforMatrix, samplerState: SamplerState.LinearWrap);
    s.Draw(texture: texture, position: new Vector2(leftPoint, topPoint), sourceRectangle: new Rectangle(leftPoint, topPoint, realBackground.X, realBackground.Y), color: Color.White);
    s.End();
}

I’ll compare what I have with what you have, maybe I can simplify my version.

I thought doing it with a shader was more efficient.

I don’t know whether a shader would be more efficient or not, but I suspect that just using SpriteBatch and a transform matrix will be performant enough.

Just to provide some closure to this. Last time I must have been tired. Turns out I was breaking my head on the image ratio for no reason. In the shader, the texCoord maps from 0 to 1 (on both the X and Y axis). I can multiply that value by the texture size and divide it back later. When I create my matrix, I apply and remove the texture size before and after the rotation. This version now works with any texture ratios.

public static void drawSeamlessBackground(SpriteBatch s, Texture2D t, GraphicsDevice gd, float parallax, Camera2D cam) {
    Vector2 textureSize = new Vector2(t.Width, t.Height);
    Rectangle view = gd.Viewport.Bounds;

    Matrix m = Matrix.CreateTranslation(new Vector3(-cam.Origin / textureSize, 0.0f)) *
                Matrix.CreateScale(1f / cam.Zoom) *
                Matrix.CreateScale(textureSize.X, textureSize.Y, 1) *
                Matrix.CreateRotationZ(-cam.Rotation) *
                Matrix.CreateScale(1f / textureSize.X, 1f /textureSize.Y, 1) *
                Matrix.CreateTranslation(new Vector3(cam.Origin / textureSize, 0.0f)) *
                Matrix.CreateTranslation(new Vector3((cam.Position * parallax) / textureSize, 0.0f));

    infiniteShader.Parameters["ScrollMatrix"].SetValue(m);
    infiniteShader.Parameters["ViewportSize"].SetValue(new Vector2(view.Width, view.Height));

    s.Begin(samplerState: SamplerState.LinearWrap, effect: infiniteShader);
    s.Draw(t, new Vector2(0, 0), view, Color.White);
    s.End();
}