Gamasutra is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Gamasutra: The Art & Business of Making Gamesspacer
Dynamic 2D Imposters: A Simple, Efficient DirectX 9 Implementation
arrowPress Releases
November 22, 2019
Games Press
View All     RSS







If you enjoy reading this site, you might also want to check out these UBM Tech sites:


 

Dynamic 2D Imposters: A Simple, Efficient DirectX 9 Implementation


January 5, 2006 Article Start Previous Page 2 of 3 Next
 

Runtime Efficiency Concerns

Aren't imposters meant to make rendering more efficient? The answer is yes, however if an imposter system is implemented naïvely you may find that it doesn't quite solve your efficiency problems.

The naïve implementation is the simplistic approach of using one render texture for each imposter. This approach should only ever be used during prototyping (when solving visual issues) as it has a detrimental impact on performance. The naïve implementation requires a render target change for each imposter that is generated and requires a DirectX draw call for each imposter that is rendered. Changing render targets is an expensive operation. Executing a draw call for a small amount of geometry is also an expensive operation. To get the most out of imposters, multiple imposter textures need to be packed onto each render texture. The bigger the render texture and the smaller the imposter texture, the better, as we can fit more imposters in the same render texture. Packing texture in this way means that only one render target change is needed to generate multiple imposters and only a single draw call is required to render multiple imposters. Therefore, the more imposters that can be squeezed into a render texture, the more efficient the system becomes.

Regenerating imposters, although more efficient when using texture packing, is still the most expensive part of this system. Therefore we want to regenerate imposters as little as possible. It is important to tweak threshold values so that imposters are only regenerated when imposter error becomes obviously noticeable by the user.

As imposters are regularly regenerated, a dynamic DirectX vertex buffer is used to pack imposter vertices for draw calls. A dynamic vertex buffer is a vertex buffer that is created with flags that specify that DirectX should optimize the vertex buffer for usage where the vertex buffer is updated each frame.

Finally, it should be mentioned that the imposter render texture should never be locked. Locking a render texture can cause the graphics system to flush and stall which will really kill performance.

Step-by-step Implementation

The following section describes the code necessary for efficient imposter generation and rendering. The code listings that are included are from the sample application that comes with the article. The sample code has been compiled with the DirectX 9 October 2005 SDK and it makes heavy use of the DirectX Extensions library (D3DX).

The code listings are presented in a step-by-step fashion and introduced in order of the most important techniques in the imposter system. The first steps involve usage of the render textures. Then we look at the techniques for building imposter billboards and the matrices required for render to texture. Next, for the purposes of demonstration and example, the naïve inefficient imposter implementation is presented. Subsequently, the efficient approach using texture packing is discussed. Finally, there is a section that demonstrates how to determine when an imposter requires regeneration.

1. Render Texture Allocation

The first thing needed in an imposter system is a render texture. The code in Listing 1 creates the render texture using D3DXCreateTexture and D3DXCreateRenderToSurface. The DirectX code is encapsulated in the function RenderTexture::Init which is called to initialize the render texture. For runtime efficiency this function should only be called when the game starts up. Parameters to Init specify the resolution of the texture. The texture is created with the format D3DFMT_A8R8G8B8 which has 8-bits each for red, green, blue, and alpha channels. The alpha channel is required as it is the alpha mask that defines the area of the texture that contains the imposter. To create a render texture rather than a normal texture, D3DUSAGE_RENDERTARGET is specified as the Usage parameter. For the Pool parameter, D3DPOOL_DEFAULT is used and this places the texture in the memory pool that is most efficient for its use: video memory.

2. Render to Texture

Rendering to a texture is similar to rendering a normal scene. When rendering a DirectX scene, the rendering needs to be executed between calls to the IDirect3DDevice9 functions, BeginScene and EndScene. BeginScene is called first. Rendering is then performed using DirectX API functions. EndScene is called when rendering is complete. When rendering to texture, the ID3DXRenderToSurface functions BeginScene and EndScene are called instead of those in IDirect3DDevice9. In Listing 2, BeginScene and EndScene functions are added to the RenderTexture class to encapsulate the DirectX versions of these functions.

We have now covered the basics of render texture usage and created the RenderTexture class that simplifies the use of render textures. Listing 3 presents a simple example of using the RenderTexture class.

3. Generating Imposter Billboards

The first stage of imposter generation is to generate geometry for the imposter billboard. The billboard that is required is that which in screen-space fully covers the 3D object from the current viewpoint.


Figure 5. Image showing the 3D object (bounding box in yellow) projected onto the imposter billboard (outlined in red).

The billboard geometry is derived from the bounding box of the 3D object. First, the bounding box is projected into screen-space. Then, working in screen-space, a 2D bounding box is generated that encloses the points of the 3D bounding box. This 2D bounding box is the imposter billboard in screen-space. While determining the 2D bounding box, the depth values of each point are compared to determine the minimum depth value of all points. Finally the screen-space points and the minimum depth value are unprojected into world-space and are used as the positions of the imposter billboard.

Listing 4 presents the ImposterVertex and Imposter structures. These store the data required by a 2D imposter. ImposterVertex represents each vertex of the imposter billboard. Imposter vertices have position, color, and texture-coordinates. The color is used for alpha-fade transitions from 3D object to imposter. The Imposter structure represents an individual imposter billboard. In addition to other useful data, it contains the vertices that make up the billboard.

Listing 5 contains the CreateBillboard function that enscapsulates the code to compute an imposter billboard. D3DXVec3ProjectArray is called to perform the projection into screen-space. Subsequently D3DXVec3UnprojectArray is called to unproject from screen-space into world-space. Also computed are the center point of the billboard and the distances to the nearest and furthest points on the bounding box. This data is used in the next section for computing the matrices required for rendering to the texture. Calculated from the imposter center is the direction vector to the camera position. The imposter center and camera direction are used to help determine when an imposter requires regeneration.

4. Generation View and Projection Matrices

View and projection matrices are required in order to render to the texture. The matrices are computed for a particular imposter in CreateMatrices in Listing 6. The matrices are generated using the functions D3DXMatrixLookAtLH and D3DXMatrixPerspectiveLH. The view matrix is computed with the current camera position and the camera is reorientated to look directly at the center of the imposter billboard. The projection matrix is computed using the billboard as the projection plane and the near and far plane distances that were calculated in Listing 5.

5. Rendering a Mesh to the Imposter Texture

With an imposter billboard and the right matrices we can now take a look at how to render an imposter to a texture. This section presents the naïve approach to imposter generation. The approach presented here is sufficient for prototyping an imposter system, however it is inefficient as it changes render target for each imposter that is generated.

The code for rendering a mesh to the imposter texture is presented in Listing 7. BeginScene is called to begin rendering to render texture. The imposter is then rendered into the texture and EndScene is called. It is important to note that this is inefficient as it is changing render target for each imposter that is generated. When rendering the imposter to the texture, the billboard is first constructed by calling CreateImposterBillboard. Next CreateMatrices is called to compute view and projection matrices. These matrices are plugged into DirectX by calling SetTransform. Before rendering, the mesh render state is initialized and the background is cleared. It is important to note that fog is disabled as it is the imposter billboard that will be fogged and not the mesh that is rendered into the imposter texture. Also, alpha-blending is disabled as this is a feature cannot be used when rendering imposters (see Visual Quality Concerns).

In later sections, a more efficient implementation using texture packing is developed.

6. Rendering an Imposter Billboard

Continuing on with the naïve imposter implementation, Listing 8 presents code that renders a single billboard. SetTexture is called to bind the render texture and DrawPrimitiveUP is called to render the billboard.

Before calling DrawPrimitveUP render-state is initialized. It should be noted that fog is enabled here. The mesh that was rendered to the texture was not fogged, so fog needs to be enabled when the imposter is rendered. Note also the use of alpha-testing so that only the area of the render texture where the imposter exists (where alpha is equal to 1.0) will be blended into the scene. Alpha-blending is enabled here for the alpha-fade transitions that smoothly blend from 3D object to 2D imposter.

Again, this approach is very inefficient but is useful for demonstration and prototyping. It is inefficient not only due to the call to DrawPrimitiveUP, but because the biggest expense is that there is one draw call required for each imposter.

7. Using Texture Packing for Efficient Imposter Generation and Rendering

So far we have developed several useful functions for generation and rendering of 2D imposters. In the previous two sections, a simplistic and naïve technique for imposter generation and rendering was presented. Using a single render texture for each imposter is very expensive. When generating and rendering multiple imposters per frame, the performance costs quickly add up. This section presents an efficient implementation that packs multiple imposters into a single texture. Texture packing has the dual effect of reducing render target changes and reducing draw calls required to generate and render imposters.

The render texture is divided up into regions each of which is used as an imposter texture. When regenerating multiple imposters that are in the same texture only one render target change is required. When rendering imposter billboards we can copy all billboard vertices to a single dynamic DirectX vertex buffer. Imposter billboards that are contained in the same texture are all rendered with a single draw call.

The first step is to divide up the render texture and generate texture coordinates for each imposter. The texture coordinates map the imposter to the region of the render texture that it occupies. Listing 9 presents the AllocImposters function. Parameters to the function specify the number of imposters that are required in the U and V axis of the render texture. The imposter texture is divided up and texture coordinates are assigned to each imposter. Note that the Imposter objects are pre-allocated as a array. It makes sense to allocate in this way as computer games are typically limited in the amount of memory they can consume, so it is better to pre-allocate memory when possible.

The texture coordinates generated in Listing 9 are used both when generating and rendering imposters. When generating imposters, we need to call the IDirect3DDevice9 function SetViewport to set the renderable area of the render texture. The function InitImposterViewport in Listing 10 demonstrates how the texture coordinates are used to set the viewport. After calling SetViewport subsequent rendering will only be output to the specified region of the render texture.

With the ability to render to regions of the render texture, we are now able to pack multiple imposters into a single texture. To do this we take the GenerateImposter function presented in Listing 7 and modifiy it to handle multiple imposters. The modified version of the function presented in Listing 11 is now called GenerateImposters. The important thing to note is that there is only a single call to BeginScene after which a loop is executed that generates multiple imposters. Note that before rendering the 3D object to the texture InitImposterViewport is called to render to the correct region of the texture.

With multiple imposter packed into a single texture, we now look at a more efficient method of rendering imposter billboards. First, a dynamic DirectX vertex buffer is created. A dynamic vertex buffer is the right choice for imposter rendering. As we are going to be depth-sorting imposters, they are potentially rendered in a different order each frame. Therefore, we need to copy vertices to a dynamic vertex buffer each frame. It is worth noting that if you don't use depth-sorting, you could experiment with using a static vertex buffer that is only updated as imposters are periodically regenerated, however the caching code required is beyond the scope of this article. Example code for creating a dynamic vertex buffer is presented in Listing 12.

The RenderImposterBillboards function is presented in Listing 13. This functions renders all imposters that are packed in a texture. For alpha-blending to work, the imposters are first sorted so that they are rendered in back-to-front order. The vertices of the sorted billboards are then coped into the dynamic vertex buffer. Last, the render state is initialized and a single call to DrawPrimitive is executed to render the billboards.

8. Testing for Imposter Regeneration

Finally, we need to determine when imposters require regeneration. It is impractical to regenerate all imposters every frame. This would be more expensive than rendering the 3D objects in the first place. Cached imposters should be reused over as many frames as possible and not regenerated until imposter error becomes noticeable by the user. Listing 14 presents two tests that determine when an imposter requires regeneration. The first test checks the time since the imposter was last regenerated. If this time is greater than the threshold value, then the imposter needs regeneration. The second test examines the angle between the current camera vector and the camera vector that was computed when the imposter was last generated. If this angle is greater than the threshold angle, then the imposter needs regeneration. In either case, when the imposter is to be regenerated the requiresRegenerate member of the Imposter structure is set to true.


Article Start Previous Page 2 of 3 Next

Related Jobs

Sanzaru Games Inc.
Sanzaru Games Inc. — Dublin, California, United States
[11.21.19]

Rendering Core Technology Engineer
Sanzaru Games Inc.
Sanzaru Games Inc. — Dublin, California, United States
[11.21.19]

Junior Gameplay Engineer
LOKO AI
LOKO AI — Los Angeles, California, United States
[11.20.19]

Senior Unreal Engine Developer
Free Range Games
Free Range Games — Sausalito, California, United States
[11.20.19]

Senior Engineer (Unreal)





Loading Comments

loader image