Inexplicable loss of frame rate

Hi there,

I have a weird problem. I’m startig to work on an isometric game with a tile-based, changeable map. Each tile has it’s own renderer and for material I use an atlas, so i can switch for example from grass to road by changing the offset of the material. That all works fine. It even runs pretty solid, when I zoom all the way out so I have thousands of tiles on screen at once. The weird thing is: When I call a function, which makes changes to every tile the framerate drops dramatically. And I am not talking about the frame in wich the changes are made. That I would understand. It drops and stays down, even if i do not really change anything at all. For Example: I start the game. Zoom all the way out. Framerate is 40. Each Tile has a Material Offset of (0.25, 0.5). I call a function which sets all Material Offsets for testing purpose to (0.25, 0.5), so it doesn’t really change anything, but the framerate drops to 5 and stays there, as if the function was called every frame, but it’s not.

I have an Input_script…

function Update() {
	if (Input.GetKeyDown(KeyCode.C)) {
		SolidColors();
	}
}

function SolidColors() {
	for (var TmpParcel: ParcelManager_script in GameObject.FindObjectsOfType(ParcelManager_script)) {
		TmpParcel.ApplySolidColor();
	}
}

And here is the function I call in each “parcel”, wich manages 9 by 9 tiles:

function ApplySolidColor() {
	var TmpTile: GameObject;
	for (var t: int = 1; t < 82; t++) {
		TmpTile = transform.GetChild(t).gameObject;
TmpTile.GetComponent(MeshRenderer).material.mainTextureOffset = Vector2(0.25, 0.5);
	}
}

This is the first time I write a question, because I wouldn’t even know what to search for. I just don’t understand. To be clear: This does a lot in the frame it is called in, so I expect a drop in framerate, but this kills the framerate permanently. What am I missing?

As to the why this is I know the answer, as to how to fix it properly I still don’t know :frowning:

Calling Renderer.material creates a copy of the material and uses the copy instead. If you open the frame debugger window, you’ll probably see a huge difference at the moment you set the materials, as instead of all using the same material, from then on they use all seperate materials and they don’t get batched anymore.

If you want to change all the tiles in the same way, the fix is easy, as you only need to call the same thing with the Renderer.sharedMaterial for ONLY ONE of your renderers (sinse otherwise you would be setting the same material a lot of times) or simply by referencing the material through a public variable in your script and change that one. Once you update that one material, all renderers will render with the updated material. NOTE however that updating a sharedmaterial from code also updates your material in the project folder as it is the same material, changes to it in playmode persist outside of playmode. To fix that you could during initialization create a copy of the material in code, and use that one copy by setting all renderer’s sharedMaterial to be that copy, that way the original stays unaffected.

If you want to make individual changes to each material, then I don’t know what you should do, you could edit the uv of the meshes itself, as having 2 different meshes doesn’t break batching as long as they use the same material (and they can sinse changing the uv changes the visible texture anyway). But then you’ll perhaps sacrifice a lot of ram on meshes just for uv’s…

I once read somewhere while looking into this that you could use MaterialPropertyBlocks for that, but I never got that to work in a way that it actually improved my framerate so I’m not sure whether these things actually work for that purpose or that I just did something wrong…

A Couple of quick wins on performance would be to get rid of all the API calls you do

‘FindObjectsOfType’, ‘GetComponent’, MeshRenderer.material, transform.GetChild, transform.gameObject

These are the things i see at a glance which can all be replaced by a field holding a reference so you dont do that many API Calls.

for example in a single ‘ApplySolidColor’ you do 5*82 API calls, all just to get to the child’s material.

i suggest re-writing the ParcelManager_script so it caches the children’s material’s once (awake) if possible, it sounds trivial, and to some extent it is, but the performance gain is substantial

Like @troien explained you basically destroy the advantage of using an atlas by manipulating the material of each tile. The material offset is a global shader property. So when changing that you have to use seperate materials. When using seperate materials they can’t be batched since they don’t use the same material.

The solution is: Don’t change the material offset. You have to change the mesh itself. More precisely the UV coordinates of the mesh. The key is that the texture information has to be part of the vertex data which comes into the shader and not some global shader constant. So each tile is rendered with the same material but each tile can map to any tile in your atlas. The atlas material shouldn’t have any scale or offset in the first place. So a quad usually maps the the whole texture with UV coordinates like this:

(0,0)
(1,0)
(1,1)
(0,1)

Now you have to set UV coordinates of the tile you actually want. I guess your atlas is an evenly tiled 4x4 texture? (so 16 different tile textures in the atlas?)

If you think about the Uv coords as a Rect you can simply say:

public static Rect GetUVAtlasCoord(int aXTile, int aYTile, int aIndex)
{
    int x = aIndex % aXTile;
    int y = aIndex / aXTile;
    float xSize = 1f / aXTile;
    float ySize = 1f / aYTile;
    return new Rect(xSize * x, ySize*y, xSize, ySize);
}

This will calculate the UV coordinates of an evenly tiled atlas. You pass the tile count in each direction and a simple texture index in the range of (0 to (aXTile*aYTile-1)) so in the case of 4x4 it’s (0 - 15)

The returned Rect can simply be used to set the UV coordinates of a quad. Just use xMin, yMin and xMax, yMax. Keep in mind that Uv coordinates usually start at the bottom left of the texture.