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?
Answer by troien · Mar 13, 2016 at 10:27 AM
As to the why this is I know the answer, as to how to fix it properly I still don't know :(
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...
Answer by Riaan-Walters · Mar 13, 2016 at 03:32 PM
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
Are you sure you read the question carefully? All he does in that script is only executed once when he presses the key "C". That code isn't executed every frame.
From the question:
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
Also it's true that some method can be quite heavy, but when used in the editor or in a PC build you can still use several of those without any problems. Also there are huge differences between the methods you've mentioned. $$anonymous$$eshRenderer.material, transform.GetChild and transform.gameObject are neglectable as they just return an already internally cached value. $$anonymous$$eshRenderer.material might create a duplicate of the material if the renderer doesn't have it's own instance yet, but that's a one time event. GetComponent is a bit heavier but you can still do several hundreds per frame without any trouble. FindObjectsOfType is a magnitude slower than GetComponent, but it also depends on the actual project. If there are many objects loaded(scene objects as well as assets) it's quite slow. If there are only a few it's pretty fast.
Answer by Bunny83 · Mar 13, 2016 at 02:29 PM
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.
Your answer
Follow this Question
Related Questions
How do I change the color of a material in mesh render? 0 Answers
Problem to read an array from another script but on the same object 1 Answer
Unity3d Script does not work anymore 1 Answer
How dose getcomponentwork ? 0 Answers
NullReferenceException: Object Reference not set to an instance of an object 1 Answer