Time to dig deeper into Away3D 4.0 and see how you can structure and manage your content to get the best performance! If you haven’t read the previous posts, it wouldn’t hurt to do so now, since this one will be all about Materials (on the Mesh side of things) and Geometries.
In any sort of project, if you have data collections that use a lot of memory, it makes sense not to duplicate them if you want to share the same data across several clients (clients simply meaning “users of the data”). Instead, you make all clients refer to the same data object, or model. Similarly, you probably wouldn’t want to perform the same expensive operations on this data for every client. The shared model can take care of that only once for all clients. In the Away3D scene graph structure, the Geometry is essentially your model, and a Mesh refers to it. Many Meshes can make use of the same Geometry. Similarly, Materials can be shared across Meshes as well. This all sounds very logical, yet for some reason I can’t fathom, this is a principle that’s often violated against. Even though Away3D internally tries to minimize memory use and tries to share things as best as it can (it caches Program3D instances, Texture objects, etc., across material instances), I can’t stress this enough: reuse Material and Geometry instances whenever possible. Not only does it limit CPU and GPU memory usage, it’s also essential for performance. I’ll explain why.
Every material uses a “shader program”, which is represented in Actionscript by a Program3D object. It’s a combination of a vertex shader and a fragment shader. I won’t go into the subject of shader programs in detail, but if you’re interested, check wikipedia for starters. A large task of the shader program is to define the colour of every pixel that’s being drawn. In other words, it controls how your rendered object will be coloured, textured, lit, shaded, … If it’s on your screen, it passed through a shader program. As a result, a different material means it needs a different Program3D. When drawing objects, the gpu needs to be told to use a different program every time for every material. Unfortunately, this switch is typically very expensive and something you’d want to prevent as much as possible. To this end, Away3D sorts all the objects that need to be rendered (the so called “renderables”) primarily according to their materials so all objects with the same material will be rendered consecutively. If you’re using different material instances for things that look exactly the same, it means more programs need to be switched needlessly.
Okay, I’ll admit, that isn’t entirely true. For those interested, I’ll explain what really happens. Whenever a material is created or changed so that it needs to create a new shader program, the vertex and fragment code is regenerated. A Program3D is requested from a central cache, but if a Program3D object already exists for that exact code combination, the existing object is returned. Different materials using the same code also use the same programs, which means there won’t be any program switching. However, it’s easy enough to accidentally have small differences between materials so that the code – and in turn the program instance – changes. And the following is still true regardless:
Every time a material changes, certain data needs to be uploaded to the GPU: Textures, lighting properties, … No caching of Program3D objects is going to prevent that. If you use a new material instance for every object, these uploads may occur for every object. Share a material, and it only happens once for all the renderables using it.
Note: if you really need several material instances, but you’re using the same BitmapData objects as textures or normal maps, at least try to share those across materials unless you have a very good reason not to. RAM’s not for wasting! Furthermore, there’s a limit to the amount of Texture objects (a buffer object representing the image data on the GPU) that can be created, and for every BitmapData, a Texture is created. You wouldn’t want to go over the limit!
Similarly, Meshes that use the same data should all refer to a single Geometry instance. A very common example is a game in which there are several occurences of the same monster type spread throughout the game map. You can have one “beer demon” Geometry, and place many beer demons in your level by making the different Mesh objects refer to that single Geometry instance. Not only does this use much less memory, it also causes less instances of VertexBuffer3D to be created (these are buffers representing the vertex data on the GPU side of things). There’s a limit on how many can exist at any given time and if you exceed it, an error will be thrown and your game will crash. Sharing Geometry objects can also result in better performance in some cases, because if the same Geometry was used for the previously rendered object, it may not have to reupload its data to the GPU!
In many cases, especially in games, you’ll have a collection of game object “types”, such as different monsters, a barrel, weapon/health/power-up pick-ups. Each of these objects will probably have their own specific Geometry and Material objects. Makes sense, since you typically wouldn’t want to use a barrel geometry for a monster, or a shotgun texture on a health boost pick-up, would you? This one-on-one mapping is a pretty fortunate situation, because it means that the rendering for these objects can be batched very efficiently. Only when different objects will be rendered, does any material or geometry data need to be uploaded, with the exception of scene-graph-specific data such as transformation matrices.
Making sure that the same Geometry and Material instances are used across all instances for the same game object type is easy. The Mesh class contains a clone() method that creates a new Mesh instance that refers to the same Geometry and Material. This way, you can build a library of reference Meshes that are not actually added to the scene. Instead, you populate your scene with these clone objects. You can dispose of them whenever they’re picked up (health pack) or killed (monster), while the reference mesh and its material and geometry can be disposed when everything is unloaded. For this reference library, you can use Away3D’s AssetLibrary, which will take care of all your loading and management needs :)
Here’s a comparison:
I’ll admit, a large cost in the first demo is the creation of the objects rather than their use. This creation is a constant cost, however, and you can see the performance drop drastically over a short time. So… Enough incentive? :-)
In the “dev” branch (and when we go Beta it will also be in the master branch), BitmapMaterial was removed in favour of TextureMaterial, accepting a texture rather than a BitmapData. It goes without saying that you should share these texture objects whenever possible rather than creating new textures with the same content.
All sources for this post have been updated to use said dev branch: https://github.com/away3d/away3d-core-fp11/tree/dev