Browse Source

prevent texture bleeding with UV inset and point sampling

Inset UV coordinates by 0.5 texels to prevent sampling outside tile boundaries
Set SamplerState to PointClamp in BeginDraw() and DrawWorld() methods
Christopher Whitley 3 weeks ago
parent
commit
2ce7f8b6a3
1 changed files with 25 additions and 5 deletions
  1. 25 5
      source/MonoGame.Extended/Tilemaps/Rendering/TilemapRenderer.cs

+ 25 - 5
source/MonoGame.Extended/Tilemaps/Rendering/TilemapRenderer.cs

@@ -379,13 +379,27 @@ public sealed class TilemapRenderer : IDisposable
     /// <param name="flipFlags">The flip transformation flags.</param>
     /// <param name="texture">The texture to calculate UVs from.</param>
     /// <returns>An array of 4 UV coordinates: [top-left, top-right, bottom-left, bottom-right].</returns>
+    /// <remarks>
+    /// UV coordinates are inset by 0.5 texels to prevent texture bleeding and visible seams
+    /// between tiles. This is a common technique to avoid sampling pixels outside the intended
+    /// tile boundary when texture filtering is applied.
+    /// </remarks>
     private Vector2[] CalculateTextureCoordinates(Rectangle sourceRect, TilemapTileFlipFlags flipFlags, Texture2D texture)
     {
-        // Normalize coordinates to 0-1 range
-        float left = (float)sourceRect.Left / texture.Width;
-        float right = (float)sourceRect.Right / texture.Width;
-        float top = (float)sourceRect.Top / texture.Height;
-        float bottom = (float)sourceRect.Bottom / texture.Height;
+        // Calculate texel size for UV inset
+        float texelWidth = 1.0f / texture.Width;
+        float texelHeight = 1.0f / texture.Height;
+
+        // Inset by 0.5 texels to prevent sampling outside tile boundaries
+        // This eliminates visible seams between tiles
+        float insetU = texelWidth * 0.5f;
+        float insetV = texelHeight * 0.5f;
+
+        // Normalize coordinates to 0-1 range with inset applied
+        float left = (sourceRect.Left + 0.5f) / texture.Width;
+        float right = (sourceRect.Right - 0.5f) / texture.Width;
+        float top = (sourceRect.Top + 0.5f) / texture.Height;
+        float bottom = (sourceRect.Bottom - 0.5f) / texture.Height;
 
         // Apply horizontal flip
         if ((flipFlags & TilemapTileFlipFlags.FlipHorizontally) != 0)
@@ -687,6 +701,9 @@ public sealed class TilemapRenderer : IDisposable
         // Save GraphicsDevice state for SpriteBatch mixing
         SaveGraphicsDeviceState();
 
+        // Configure sampler state to prevent tile seams
+        _graphicsDevice.SamplerStates[0] = SamplerState.PointClamp;
+
         // Set up matrices
         _viewMatrix = camera.GetViewMatrix();
         _projectionMatrix = Matrix.CreateOrthographicOffCenter(
@@ -924,6 +941,9 @@ public sealed class TilemapRenderer : IDisposable
         if (!_isWorldMode)
             throw new InvalidOperationException("Not in world mode. Use LoadWorld() first, or use Draw() for single tilemap.");
 
+        // Configure sampler state to prevent tile seams
+        _graphicsDevice.SamplerStates[0] = SamplerState.PointClamp;
+
         // Set up matrices
         _viewMatrix = camera.GetViewMatrix();
         _projectionMatrix = Matrix.CreateOrthographicOffCenter(