Pages

Monday, March 7, 2011

Unity3d + C#: Reducing draw calls and gaining some performances with CombineChildren.cs and Texture2D.PackTexture

I realize a lot of Unity tutorial out there focus more on the gameplay issue and less in the graphical issue, probably due to the fact that they want to guide/teach you into creating a better game with more kickass functions... Well, in this post, I'm doing a note on ways to reduce draw calls in order for better performance in lower-end PCs. 

Usually a game with high draw calls means it has a ton of meshes in its scene with different textures, materials and shaders, and the PC will have to loads them up and render one after another causing it to gets laggy, unless you have a high-end PC like mine... You can read a more detail explanation in this article at Unity Answers by RobotDuck.

So, a way to reduce this is, as the article explained, by creating a large texture atlas, which is made up of all the textures of the meshes in your scene, and then assign the texture atlas back to the meshes, and then position them to their corresponding textures on the atlas according to their designated UV coordinates. I'm kinda bad at explaining stuff... so I'll just show you this:


This is an example of a texture atlas (taken from the link above), where by you have all the textures of the 3d objects in your scene and then assign the texture to them according to their corresponding UV coordinates...

So let's say you had already created all the textures for your game project and was too lazy to create the texture atlas for time's sake, cuz it can get pretty messy assuming you have like 100 different textures and you would like to have say... 6 different shaders for all of them. Instead of opening up your Photoshop to make those 6 texture atlases (for different shaders), you can pack those textures up into atlas straight way using scripts... 

I found one well-written script, written by "phantom" from the Unity forum which uses the Unity built in functions called Texture2D.PackTexture() to create a texture atlas. I've sorta tested it out, tweaked it a little, and exported it as a unity package... you can download it from here (for Unity vers. 3.x): TexturePacker.unitypackage

And now let me guide you through on how you can use the script... 

Import all the assets into your project folder. Some of the script inside the package, especially CombineChildren and MeshCombineUtility originated from Unity's own Standard Assets. 


Go to the Assets/Scenes folder and open up the "TexAtlasTest" scene, and you'll see 4 cubes inside the scene. Don't touch anything yet, but go to the Game window, and select stats...



The stats window should show a Draw Calls of 4, which make sense, consider we have 4 cubes in the scene with different textures and materials, but uses the same shaders, thus there's a chance here we could combine their textures together and use the same material and texture atlas for all the meshes in the scene. 

Next, in the Hierarchy, you should see a gameObject named "Objs" which contains all the blocks in the scene. When you select it, there should be two scripts attached to it: CombineChildren.cs and TexturePacker.cs 


What this CombineChildren.cs script does is, it combine all the meshes of the children of the gameObject (which was using the script) into one big mesh. And then TexturePacker.cs will combine all the textures of the children to create one huge texture atlas, which would then be auto-UV to all the meshes inside the scene. 

There's two parameters over at the inspector which you could toy around with: 
"Shader Type": the name (in string) of the shader you want to use, and 
"Texture Size": the size of the texture atlas you want to create... it's 1024 x 1024 by default...
As for the "Packed Texture" variable, just leave it alone... It's actually for storing the texture atlas created when you click "play"... so it'll remain empty for now, and then a new texture will appear during play mode...

Those were the two important scripts to make the draw calls reduction possible... Now if you could just click "Play", and look at stats again:


Now we have for the scene, ONE draw calls. (Yeah! :D) 

More explanation about the script used is inside the TexturePacker.cs script itself (those comments in green-colored font)... 

NOTE:
* Important thing to be noted about the scripts when using them is that... these scripts will only turn their children into one huge meshes and one huge texture atlas... Thus, always make sure that you have an empty game object that stores all the meshes which uses the same shaders that you wanna combine into, before using them.

* In the process of packing texture using PackTextures() functions, you may face a problem of getting an error like: "Texture atlas needs textures to have Readable flag set." Don't worry, it's not a bug or anything, you can solve it by going to your texture's folder, click on the textures you're doing the packing on, and in the inspector, look for "texture type" and select "Advanced". Doing so will bring out a number of new parameters. Look for the one called "Read/Write Enabled", check it (for all textures used), and the problem's solved.

What's happening here is that if the "Read/Write Enabled" is not enabled, the texture is never stored in ram, but only vram, as such you cannot use it for packing as the cpu can't access it.

Reference (some cool articles you could also check out):





13 comments:

Phil said...

Dude.. This tutorial is amazing! Great Job and keep them coming!

rabside said...

Great job!
only one question: how can I use it to reduce the drawcalls of my GUITexture?

thx a lot

JakeL168 said...

@rabside: sorry, I don't usually use GUITexture, mostly OnGUI...

rabside said...

@JakeL168: I know with OnGUI the performance is really low. If you don't use GUITexture, what do you use?
we're developing an Android game.

JakeL168 said...

@rabside: I just said it... I use OnGUI... I'm also developing an iOS and Android game too, but not a very fun one though...

naju said...

great! i'm currently following this tutorial now!

jake, do you mind if i give your blog url to some of the VR juniors who might need help? i won't give it to them if you don't let me

naju said...
This comment has been removed by the author.
funcdesigns said...

This is really helpful Jake, I just got some issues displayed on my console. Here's the list:

!Failed setting triangles. Some indeces are referencig out of bounds vertices. line177

!Mesh.vertices is too large. A mesh may not have more than 65000 vertices. line170

!Mesh normals is out of bounds. The supplied array needs to be the same size as the Mesh.vertices array. line171

This 3 errors are displayed in my console several times. Any remarks about this?

Thanks!

JakeL168 said...

@funcdesigns I'm not sure, I've never encounter the problem b4... Perhaps you could show me the script where the problem occur?

barzoon said...

How would you use this with a scene that changes at runtime (walls getting destroyed and so on).

JakeL168 said...

Like I've mention, this only apply to static object, means the object which won't move or have anything happen on its surface, because this method is applying the texture on the object's (wall) surface using its UV information... Destroying the wall might causes a change in the mesh's UV (same thing if you try doing that in a 3d modelling program), and the texture would goes haywire...

ippo said...

I made a script that works for dynamic objects ie gameobjects that move as well. It is here:

http://ippomed.com/wordpress/one-draw-call-for-each-shader-with-dynamic-meshes-in-unity-3d-the-bob-script/

Panda Man said...

In the last note you said that you must check the box to make it accessible to CPU by allowing it to go to RAM. Does that mean your textures will now reside in RAM and not VRAM?

Isn't that a bit counterproductive? Or does the script put it back in VRAM after the initializations?