Building efficient content in Away3D 4.0: Sharing Materials & Geometries

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.

Reusing Materials

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.

Sharing materials allows more renderables (here SubGeometry objects) to be rendered with less switches

Sharing materials allows more renderables (here SubGeometry objects) to be rendered with less switches

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!

Reusing Geometries

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!

Material/Geometry Tandems

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.

Rendering objects with one-on-one Material/Geometry combinations

Rendering objects with one-on-one Material/Geometry combinations

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 :)

For example

Here’s a comparison:

Demo NOT reusing anything (warning, can be very slow!)

Demo reusing both Geometry and Materials

Source for both

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? :-)

 

Update

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

15 thoughts on “Building efficient content in Away3D 4.0: Sharing Materials & Geometries

  1. nice post!
    You’re doing great stuff at away3d and such kind of posts are going to help very much the community to leverage your framework.
    Suggesting the best practices will produce a lot of great stuff in the long run, well done :)

    thank you for sharing,
    ciao

  2. Thanks for that Post!

    Do you know how a Engine like Frostbite handles this “restriction” and can you tell me what happens if I call view.dispose()?

    Will a objects disposed or not?

    Thx

  3. Cheers for the wonderful post David!

    One thing though, shouldnt this line
    return new Plane(createMaterial());
    at createMesh() -> if(SHARE_MATERIAL) be
    return new Plane(_referenceMaterial);
    instead?

  4. Christian: Depends on what restriction you’re talking about. In case of the amount of VertexBuffer3D and Texture instances limits, on native code that’s usually much less of an issue. Nonetheless, any game engine does high-end resource management and shares geometry, materials, textures (any type of resource, really) as much as it can. Check out the book “Game Engine Architecture” by Jason Gregory if you’re really interested in native game engine architectures.
    About View::dispose, it should free up the Context3D used by the disposed view, along with all the resources created from that context. The scene itself, materials, geometries, etc, are not cleared, since it’s independent of the view (it could be set as the scene for other views, for example).

    Li: Ah, of course, blame my short attention span :) Thx for the heads up!

  5. Thanks for all you do, David. I’ve been using Away3D for over a year now, and this is probably the most practical design strategy tip I’ve seen. It’s simple things like this that can escape your typical user who doesn’t have “the big picture” on how Away3D works like the developers do, so it’s great to get these foundation strategies

  6. Thanks alot. This is what I am looking for.
    I am very happy using away3d and it is powerful. My only concern is performance. Can you please give more talk on performance? Also where can I get more tutorial about away3d performance tuning?

  7. Does this trick also work for Away3D 3.6 or is it 4 only? I ask as we really can’t move to 4 yet due to needing to support flash player 10 for quite some time to come. (Enterprise apps)

  8. Simon: I wouldn’t consider it a trick, I would consider it common sense in memory usage ;) But yes, while in 3.6 it doesn’t impact the actual performance, sharing geometries and materials wherever possible will lead to decreased memory usage, so it’s recommended.

  9. ADDITION (comment up): In my source , in function createMesh(), should be
    return Mesh(_referenceMesh.clone()); ,
    but still not help.

  10. It would help if you could explain what error(s) you’re getting. Possible guesses are that you aren’t using the dev branch for which this article’s source was recently adapted, or you don’t specify to compile to swf-version 13 (how that would work in CS5.5, I wouldn’t know, I don’t use that nightmare ;) )

  11. Hi, I use swf-version 13, there is no errors (I have flash setting correctly for AWAY 3D 4 and flash 11.2), but it looks like its not cloned plane, but create new one,.. if I merged all of plane to one mesh, fps is still full it OK. But I dont want merged it to 1 mesh, but I want many cloned plane with full fps like in these example,.. there is my .as http://webmaking.sk/develop/temp/ReuseDemo.as.txt .
    Thanks for help, maybe I doing somethiong wrong in my .AS,
    THANKS FOR REPLY ;)

  12. Oh, I gues I got it, in this exmaple is “driver” HW,…and I have Directx9, How I change it to HW ?

  13. Hi David,

    Thats a good explanation, but I think this example doesn’t show enough.

    Maybe you can post an example showing how to dispose the elements that you don’t us (killed for example). Because I’m trying to do that but the debug stats showing my ram increasing…

Leave a Reply

Your email address will not be published. Required fields are marked *