Thx for all the replies! I have to be able to interact with the map runtime so I don't think splitting it to textures would work. I was able to make some progress though. I dropped the whole idea of separate tile class and instead have the data in simple byte[x,y,z] array. I'm not really sure what was causing the slowdown before but now I can run steady 60 fps as long as I don't zoom too far out.
I probably have to change to SpriteSortMode.BackToFront so that I can utilize layerdepth for sorting. Even though I'm already naturally sorting the map itself, I have to somehow get depth sorted with moving objects that might be behind parts of the map.
Here is the draw/culling, not pretty but does the job:
public void Draw(SpriteBatch spriteBatch)
// Start tile of horizontal row to be drawn.
int dx = topLeft.X;
int dy = topLeft.Y;
// Tile to be drawn.
int x, y;
bool swap = true;
spriteBatch.Begin(SpriteSortMode.Deferred, null, SamplerState.PointClamp, null, null, null, camera.GetViewMatrix());
// Update tile to start coordinates of new row.
x = dx;
y = dy;
// Make sure we are within map limits.
if (x >= 0 && x < Map.X && y >= 0 && y < Map.Y)
for (int z = 0; z < Map.Z; z++)
if (data[x, y, z] > 0)
tilePosition = IsoTransform(x, y);
GetTileData(data[x, y, z], z, ref sourceRect, ref tileAlpha);
spriteBatch.Draw(tileSheet, tilePosition, sourceRect, tileColor * tileAlpha, 0, tileOrigin, 1f, SpriteEffects.None, 0);
// Add to draw our next horizontal tile.
} while (x <= dx + maxX); // Horizontal row done.
// Add to either dx or dy to get to the next row.
dx += 1;
swap = false;
dy += 1;
swap = true;
} while (dy <= topLeft.Y + maxY); // We are done drawing once we reach maxY.
private void UpdateLimits()
// Get view rect coordinates.
Vector2 topLeft = Vector2.Transform(new Vector2(0, 0), camera.InverseViewMatrix);
Vector2 bottomRight = Vector2.Transform(screenSize, camera.InverseViewMatrix);
// Calculate how many tiles fit in to this view.
maxX = (int)(bottomRight.X - topLeft.X) / Tile.X;
maxY = (int)(bottomRight.Y - topLeft.Y) / Tile.Y;
// Get first tile to be drawn.
this.topLeft = WorldToTile(topLeft, false);
// Add a bit extra around the view to make sure half tiles get drawn.
maxX += 3;
maxY += 3;
this.topLeft.X -= 2;