Introduction
This article presents an algorithm for wrapping scrolling text around certain 3D solids, and gives an example implementation in Direct3D. The technique described aims to be as efficient as possible in terms of runtime performance, and the code provided strives to be brief and comprehensible. Two still pictures of the article demo in action are shown below:

The text-wrapping effect can be impressive, particularly when combined with lighting and presented at a high rate of speed. However, a few qualifying statements are appropriate before going further.
First, this article does not present a general technique for wrapping any arbitrary string of text around any arbitrary solid. Rather, certain restrictions are placed on these parameters, which are explored below. This was deemed acceptable for a game or simulation, where a large number of superficial special effects must be quickly combined to form a plausible reality.
Second, the code presented here targets DirectX 9, even though newer versions of DirectX are available. The use of DirectX 9 allows the program to run on Windows XP, which does not run DirectX 10+.
Background
In addition to Windows XP, use of DirectX 9 enables the use a key non-Microsoft development tool: MinGW, which was used to develop the demo program. MinGW is the “Minimalist GNU Compiler for Windows,” a free port of the GNU Compiler Collection (GCC) that runs on – and targets – Windows.
Beyond its free price, the small size and unobtrusive nature of MinGW make it possible for this article to cover 3D development in greater detail than would otherwise be the case. Taken together with its predecessor article, the article describes not just the central text-wrapping technique, but the complete knowledge base associated with the spinning cube demo around which it is built, from tool setup to application development. While it is certainly possible to use the code presented here in a Visual Studio project, the article and its predecessor article offer a complete and self-contained path to free 3D development with MinGW.
The basic Direct3D techniques used here are identical to those in the predecessor article. So, unlike that article – which is really a brief but complete Direct3D tutorial – this article will not discuss every line of its source.
To build the code, it is necessary to install the DirectX SDK, along with MinGW. The links and procedures necessary to do these things are available from the predecessor article.
Using the Code
The source code archive provided with this article consists of two files, named “TextWrap.cpp" and "TextWrap.png". Once all the prerequisites are installed, it should be possible to build “TextWrap.cpp” into a demo executable by running a single command in the Windows Command Prompt.
To do this, it is advisable to first use the "cd" command to change the working directory to the folder where "TextWrap.cpp" is located. After changing to the correct folder (i.e., wherever you extracted the source code archive for this article), the following command should be executed:
c:\mingw\bin\g++ textwrap.CPP -fcheck-new -o textwrap.exe -mwindows -I
"C:\Program Files\Microsoft DirectX SDK (August 2007)\Include" -I c:\mingw\include
-L c:\mingw\lib\ -ld3d9 "C:\Program Files\Microsoft DirectX SDK
(August 2007)\Lib\x86\d3dx9.lib"
-lwinmm
Note that, if the command text shown above is used to build an actual command using cut-and-paste, it is necessary to convert the carriage returns into spaces, i.e. the text shown above should be entered as a single, unbroken line. One approach is to paste the command from this page into Windows Notepad, edit it in Notepad to reside on a single line, and then paste the resultant text into a Command Prompt window. (The context menu of a Command Prompt window contains an "Edit" menu item which offers a "Paste" command.)
This build command is a key piece of information, and is explained clause-by-clause in the predecessor article. The only changes compared to that article are the use of different .CPP and .EXE file names, and the inclusion of linker option “-lwinmm.” This option includes the Windows Multimedia libraries, which contain some timing functions necessary for frame rate calculation.
The output of the build command is Textwrap.exe. The image file “Textwrap.png” must be present in the folder from which this executable is run.
When run without any parameters, “Textwrap.exe” renders as quickly as possible. On most hardware, this should translate to a rendering speed of at least 60 frames-per-second (fps), a rate beyond which the human eye is typically not prepared to process data (see here). This should even be possible on low-performance systems, e.g. budget laptops with single-core CPUs, shared RAM, etc.
However, the executable can also be parameterized with a target frame rate if desired, in which case the demo will throttle its rendering speed down to a user-specified rate. This is done using the “-fps” option.
For example, one might run the demo executable using the command “Textwrap.exe –fps30” to force a 30 frame-per-second rate. This might be desirable on certain slower hardware; a frame rate of 30fps is still acceptable for most purposes, e.g. television, motion pictures, and many games. On more advanced hardware, one might want to explicitly specify the 60fps rate. This might be useful if, for example, the default (unlimited) option was allowing the CPU to overwhelm the GPU with frames, resulting in uneven performance.
Finally, note that Direct3D text rendering, an integral component of the demo, requires 32-bit color depth or better. In rare cases, this may necessitate a change in the “Display” section of Control Panel.
Algorithm
The challenge of wrapping text around a solid is a somewhat tricky one. Even a simple cube exposes 6 faces. If a continuous band of text is to be wrapped around the solid, then the impact of this wrapping on each of these faces must be considered. In Direct3D, logic will need to be built to generate dynamic textures containing the rendered text for display on each face. If the text is to scroll, then these textures need to be updated dynamically, in real time.
As stated, though, this article simplifies this daunting task by placing some restrictions on the nature of the solids and the scrolling text. First, the faces around which the text scrolls must all be identical in size. Second, the faces must be basically rectangular in shape, at least in the areas where the text is scrolling.
These restrictions, while quite specific, still leave a great deal of room for creativity. In the demo program as provided, the scrolling area consists of four faces of a six-faced cube. The top and bottom of the cube do not have scrolling text. Rather, text is wrapped around the other four faces of the cube, oriented in parallel with the top and bottom of the cube.
This wrapping pattern is a logical one that is extensible to many similar solids. Text can be wrapped around octagonal solids like those shown below this paragraph, for example. In such an application, the text will flow around the rectangular sides of the solid only; the top and bottom of these solids are not rectangular, and cannot have scrolling text per the restrictions articulated above.

In addition, for solids that do happen to be cubic in shape, a wider variety of wrapping patterns is also available. The text could potentially be wrapped vertically across the top, bottom, front, and back faces of a cube, for example. In fact, nothing prevents this from being done in conjunction with the horizontal text demonstrated here. This application would wrap all faces of the cube with text.
Scrolling text can even be wrapped around urban landscapes like the one presented in Wolfenstein 3D, or in the maze game shown here. Note that basically any surface in these games can be wrapped in scrolling text using the technique described here. This is true because they are based around 90-degree angles and large, discrete blocks having constant, symmetrical dimensions.
Even in more advanced simulations, selectively designed polyhedra such as walls or buildings can still benefit from the effect, so long as the faces around which the text must scroll are regular in size and (basically) rectangular in shape. In such a rendering, text might meander like a scrolling marquee around the top of several faces, each at some right angle to the last, giving an effect similar to the stock ticker-style displays sometimes installed on the lobby walls in office buildings. Eventually, in such an effect, faces might (or might not) meet to form a contiguous circle of text as is seen on the prisms. The algorithm presented accommodates all of these modes of operation, provided that the restrictions already described are observed.
As stated, there are limitations associated with the choice of text as well. In particular, the basic scrolling text string as rendered must be sized to approximately span the width (or height, if the text is vertical) of the texture. This basic string will be repeated exactly N times over the entire solid, where N is the number of faces. Any large difference between the width of the basic string and the width of the texture will therefore result in either overlaps or gaps in the overall band of text. Some of these N instances of the string will necessarily span texture boundaries, i.e. will consist of joined portions from two faces; some of the faces will be obscured and as a result will not be rendered. To the user, though, the effect presented will be that of N instances of the basic string wrapped in a continuous band across the N faces.
Finally, it is required that the stream of text be drawn at right angles to the texture sides. That is, the bottom of the text must be parallel to two of the four sides and perpendicular to the other two. Wrapping text at oblique angles is not supported here.
One important effect of these various restrictions is to allow all of the dynamic textures rendered onto the faces of the solid to be identical at all points in time. As evidence, consider the first image presented at the top of this article, which was a picture of the demo in action. Both visible faces of the cube upon which text has been drawn are in fact identically textured. Only the top and bottom faces have a different texture, which is static and devoid of rendered text.
The construction of this single texture is not so difficult. It is based on always drawing two instances of the basic string onto the texture surface for each frame, at appropriate coordinates, and allowing Direct3D to clip off the unseen portion of each string.
At the start of the demo, the first of these two scrolling strings simply gets rendered at the extreme left side of each face, i.e. at X coordinate 0 of the dynamic texture. The second string is rendered immediately to the right of this string, i.e. starting at coordinate TEXTURE_SIZE. This second string is not visible in the first frame, since the actual, viewable portion of the texture ranges from 0 to TEXTURE_SIZE-1 in both dimensions.
In the next frame, the first string is drawn at a position slightly more to the left, leaving some empty space near the right side of the texture. This space is filled in by the second string, which is also shifted to the left, by an identical number of pixels. Note that this movement does not have to be to the left. In other applications, the text could move toward the right, or even toward the top or bottom of the texture.
Whatever its direction, movement continues in this manner until the second string’s X coordinate becomes negative. At that point the entire process repeats itself, i.e. the first string gets drawn at X coordinate 0 once more, the second string at TEXTURE_SIZE, and so on. From the user's perspective, though, this will not be perceived as any sort of reset. The net effect will be one where multiple instances of the basic string stream continuously - and contiguously - around the solid in a single direction.
Throughout this process, the end of the first string (which starts out at somewhere near TEXTURE_SIZE-1 in the first frame, and moves left) joins nicely to the beginning of the second (which starts out at TEXTURE_SIZE and moves left), so long as the scrolling string has been well-matched to TEXTURE_SIZE. This is diagrammed in the figure below:
In this figure, the gray rectangle represents the viewable surface of the dynamic texture. The scrolling string of text is shown plotted twice, with portions of each plot falling outside the texture surface. At runtime, these will be clipped by the graphics system, but they are included in the figure for illustrative purposes.
At this example point in time, the text strings have been moved to the left by some constant K. (In the coordinate system used for Direct3D textures, the origin is at the left, and movement to the left is negative). The length of each string of text is just slightly less than TEXTURE_SIZE, and the first (left) string thus spans from X coordinate 0-K to some point just left of (TEXTURE_SIZE-1-K). Just to the right of that position, the second string begins at (TEXTURE_SIZE-K).
The figure further explains the behavior of the algorithm for differently-sized strings. The string scrolled in the figure was selected- in its content, font details, and point size – to span approximately TEXTURE_SIZE pixels. Consider what would happen if the string were slightly longer, and were plotted using the same font size. The string would still be plotted twice in its entirety, and this would be done using the same coordinates. As promised above, this would cause overlap.
What is less obvious than all of this is that, at any given point in time, the text clipped off at the left side of the texture will naturally complete the text at the right side. The little bit of the first letter that scrolls off the left side of the texture in the second frame will consist of the exact same pixels that scroll in at the right side, as the beginning of the second string, for example. In the last figure shown, the second string consists basically of “Stream of Text for Scr” and the first string of “olling demo.”
This neat side-to-side matching behavior is the reason for the restriction against scrolling text on non-rectangular faces. Consider the generation of the first frame's dynamic texture, as it was explained above. This texture has the scrolling string plotted from the extreme left of the texture to its right. Since the texture is square, portions of it - and of the string - would not even be present on the surface of, for example, a triangular face.
Even if the algorithm as presented thus far is modified to draw this first instance of the string within the portion of the texture that ends up on a triangular face, there is no way to scroll this text to the left (or right) in a way that allows side-by-side matching of faces. The portions of the text clipped off at the left side do not correspond to the portions that would enter from the right side as part of a second string. Joining sides will thus result in what amounts to missing pixels.
Implementation
The algorithm presented thus far is easy enough. As always, implementation nevertheless presents some difficulties. In the next portion of the article, all of the additions / changes necessary to derive the demo code from the predecessor article’s code are shown and discussed. This is done, simply, from the top of the source code file to the bottom. These changes should be essentially similar to the changes required to integrate the scrolling text technique into most Direct3D applications.
First, near the top of the file, some new headers are #included. Both of these are necessitated by the “-fps” option:
#include <Mmsystem.h> //For timing functions
#include <stdio.h> //To parse command line
Next, in the section where globals are declared, some additions are made:
LPDIRECT3DTEXTURE9 top_bottom_texture = NULL;
IDirect3DSurface9 * mesh = NULL;
LPD3DXFONT main_font=NULL;
From top to bottom, these new variables are:
- the second, non-dynamic texture used for the top and bottom of the cube;
- a surface holding the background image of the main texture, which is used for erasing text between frames and
- the Direct3D font instance used to do the text rendering
These follow a common garbage collection pattern shared with the globals from the previous article. The variables are all pointers to COM objects, and are initialized to NULL. When it is time to reclaim their storage, they are checked for null before IUnknown::Release() is called on them. Making these variables global allows the function WindowProc(), and all of the various functions it calls, to use the variables. For example, WindowProc() gets called after another application has taken control over the display hardware. Certain method calls involving the demo’s Direct3D objects must be made at this point. All of the texture and surface objects are invalidated at this point, and must be re-instantiated. Even the Direct3D font object must be remade, since it is associated with the lost 3D device instance.
Unfortunately, the parameters of WindowProc() are set by Microsoft, and these parameters do not include the necessary Direct3D pointers to deal with these control-related events. Making the pointers global allows access from the necessary functions. In keeping with good design practice, this set of global variables has been kept as small as possible.
Next, in the section where program constants are #defined, some existing constants have been given new values. In particular, TEXTURE_SIZE is now 512 (from 1024). This should improve performance on budget hardware, and in testing did not seem to have any negative effect in terms of appearance. The name of the image file has also been changed to match the new executable name.
Then, some new constants are declared:
#define DEMO_TEXT_COLOR D3DCOLOR_ARGB(255,108,108,228)
#define TEXT_SCROLL_SPEED 5
#define WRAP_STRING "Fast Dynamic Texture"
#define TEXT_POINTS 70
#define DEMO_FONT "Arial Narrow"
#define TEXT_TOP 210
These all relate to the new text portion of the display. A color is #defined, which was selected to be identical in hue and saturation to the shades of blue used in the “Textwrap.png.” The scrolling speed of the text, in pixels per frame, is supplied, followed by the content of the string, its size in points, and the font name. Finally, the top coordinate of the texture stream (in the 0-511 Y dimension of the 512x512 texture) is #defined.
All of these constants are freely adjustable during development, subject to the caveats already given about text size. It is possible to use a longer string of text in conjunction with a smaller point size, for example, or to draw the text at a different Y coordinate, simply by changing these constants.
The use of constants like TEXT_POINTS and TEXT_TOP could be replaced with calls to Win32 functions like GetFontMetrics() to abstract away details like text size in pixels. Going even further, something like WPF - an abstraction based on real numbers and vectors - could presumably be used to draw the texture.
None of this was considered desirable here, for several reasons. WPF is not compatible with MinGW - or any standard C++ compiler - and this imposes additional costs on the developer in terms of knowledge and tool set. Also, resolution independence in drawing the 2D texture is less important than one might think, since the texture can subsequently be applied to any Direct3D primitive for display. The fact that the 2D rendering done here ends up on a Direct3D texture gives it a very good form of resolution independence, and this was deemed to be more than sufficient for the scope of this article.
Continuing further down in the new code, function do_2D_work() is now declared alongside some existing prototypes. This will be called by the rendering loop to generate the necessary dynamic texture for each frame.
At the top of main(), a new block of code checks the command line for the “-fps” option. If this is found, the user-supplied frame rate is converted into a per-frame elapsed time in milliseconds:
DWORD limit_fps = 0;
if (strstr (command_line, "-fps"))
{
sscanf (strstr (command_line, "-fps") + strlen ("-fps"), "%d", &limit_fps);
limit_fps = 1000 / limit_fps; }
The addition performed on the results of strstr() and strlen() serves to create a pointer to the character right after the "-fps" part of the command line. This pointer is passed to sscanf() to obtain the integer frame rate.
Immediately below, the process’s core affinity is tied to a single logical processor, using a call to SetProcessAffinityMask()
SetProcessAffinityMask (GetCurrentProcess (), 1);
This function interprets its second parameter as a 64-bit binary mask. The mask, in this case, consists of 63 zeroes followed by a 1, indicating that only the first of 64 possible logical cores should be enabled. A single core ought to be more than capable of doing the limited CPU work necessary to achieve 60fps; it is the GPU that will do the bulk of the work, and we do not want a fast, multi-core CPU to overwhelm the GPU by requesting greater than 60fps.
Finally, immediately below the central call to render(), some code is inserted to selectively Sleep() in order to achieve the user-requested frame rate:
if (limit_fps)
{
static DWORD last_time = timeGetTime ();
DWORD current_time = timeGetTime ();
DWORD delta_time = current_time - last_time; last_time = current_time;
if (delta_time < limit_fps)
{
Sleep (limit_fps - delta_time);
}
}
This code operates only when the program was run with the "-fps" option, in which case limit_fps will be non-zero. Specifically, limit_fps holds the hypothetical render time, in milliseconds, of each frame at the rate requested by the user (e.g. 20ms for "-fps50"). The code checks the render time of each frame after rendering is complete. If this time span is less than would be appropriate for the user-entered frame rate, then this difference is passed to Sleep() in an effort to get back on the desired schedule.
Further down, a new block of code initializes some of the new Direct3D COM objects:
device->CreateOffscreenPlainSurface
(TEXTURE_SIZE, TEXTURE_SIZE, D3DFMT_X8R8G8B8, D3DPOOL_DEFAULT, &mesh, NULL);
D3DXLoadSurfaceFromFile
(mesh, NULL, NULL, TEXTURE_FILE_NAME,NULL,
D3DX_DEFAULT, 0, NULL);
D3DXCreateFont( device, TEXT_POINTS, 0, FW_NORMAL, 0, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, DEFAULT_QUALITY,
DEFAULT_PITCH | FF_DONTCARE, DEMO_FONT, &main_font );
The first two statements set up mesh, a buffer that holds the background image onto which text is drawn. The portion of this over which text is written must be restored with each frame, to erase the last frame’s text. The parameters used for this call begin with its dimensions, followed by the pixel format (24 bits of RGB data in a 32-bit DWORD with 8 bits wasted – a very typical Direct3D format in cases where an alpha channel is not needed).
Note that allocation option D3DX_DEFAULT is used, which instructs Direct3D to store this data in video RAM. Generally, option D3DX_MANAGED is used for allocating surfaces in the demo code. That option gives Direct3D the option to relocate storage between main RAM and video RAM. However, this was found to cause failures for this buffer, which for architectural reasons must reside in video RAM.
The final parameters to CreateOffscreenPlainSurface() are the desired surface address, followed by an optional parameter set to NULL in this application. The statement after that - a call to D3DXLoadSurfaceFromFile() - does the work of filling the surface just allocated with image data from the demo file. The options used are almost entirely Direct3D defaults, although file name and surface address must, of course, be passed as well.
The third statement creates the font used to draw the text (main_font). Most of the values passed here are defaults, although the code controls font family, size, and weight explicitly.
Down a bit further, in render(), a new call to function do_2D_work() is made, in order to create the dynamic texture necessary for the scrolling text effect. This function is treated in detail later in this article. After the new call to do_2D_work(), the next change in render() related to the scrolling text feature involves the call to D3DXMatrixRotationYawPitchRoll():
D3DXMatrixRotationYawPitchRoll (&matRotateX, index, index / 4.0,
index / 4.0);
This differs from the equivalent statement in the predecessor program in its two additional divisions-by-four. During testing, it was found that rotating more dramatically about the yaw axis, and less so about the pitch and roll axes, allowed the actual text to be more visible in the demo display.
Just slightly further down in render(), some changes have been made to the code that actually renders the 3D solid:
device->SetSamplerState (0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
device->SetSamplerState (0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
device->SetTexture (0, top_bottom_texture);
device->DrawPrimitive (D3DPT_TRIANGLESTRIP, 8, 2);
device->DrawPrimitive (D3DPT_TRIANGLESTRIP, 16, 2);
device->SetTexture (0, texture_1);
device->DrawPrimitive (D3DPT_TRIANGLESTRIP, 0, 2);
device->DrawPrimitive (D3DPT_TRIANGLESTRIP, 4, 2);
device->DrawPrimitive (D3DPT_TRIANGLESTRIP, 12, 2);
device->DrawPrimitive (D3DPT_TRIANGLESTRIP, 20, 2);
First, the calls to SetSamplerState() are new. These specify that a basic (linear) form of anti-aliasing should be applied to the texture as it gets magnified or reduced in size to cover the various faces of the cube. In the predecessor article, this anti-aliasing was not used. However, jagged edge effects were controlled somewhat by the higher texture resolution (1024x1024) used in that article’s demo.
Now, instead of selecting texture_1 and rendering all 6 faces (as was done in the predecessor article), the first two faces (the top and the bottom) are first rendered using top_bottom_texture. The rendering is performed using calls to IDirect3DDevice9::DrawPrimitive(). This happens before texture_1 is even selected.
The DrawPrimitive() calls that render each face can be distinguished based on their second, integer parameter. In our array of vertices vertex_buffer, each group of four elements forms a pair of triangle strips, which corresponds to one cube face. DrawPrimitive() takes three parameters: first, we pass the constant D3DPT_TRIANGLESTRIP, and then we pass an index into vertex_buffer, which will be a multiple of four; finally, we pass the number of triangles in the strip (2). As indicated by the comments in the declaration of vertex_buffer, the top face vertices begin at element 8 of vertex_buffer, and the bottom face vertices begin at element 16. All of these are, of course, spelled out in much greater detail in the predecessor article.
The next code change related to the scrolling text effect is the addition of some new garbage collection code in cleanup():
RELEASE_COM_OBJECT(main_font)
RELEASE_COM_OBJECT(top_bottom_texture)
RELEASE_COM_OBJECT(mesh)
This code deals with the new globals introduction in this demo. Recall that all globals follow the same pattern of allocation / deallocation. As such, these calls are very similar to the other calls already present in cleanup().
The next change comes in setup function init_graphics().
D3DXCreateTextureFromFileEx (
device,
TEXTURE_FILE_NAME,
TEXTURE_SIZE,
TEXTURE_SIZE,
D3DX_DEFAULT, D3DUSAGE_RENDERTARGET, D3DFMT_X8R8G8B8, D3DPOOL_DEFAULT, D3DX_DEFAULT, D3DX_DEFAULT, 0, NULL, NULL,
&texture_1
);
The sixth and eighth parameters are different. In the sixth parameter, D3DUSAGE_RENDERTARGET is passed, which tells Direct3D that the application will render into this texture. This is necessary to achieve the texture dynamicity necessary for the scrolling text effect. In the eighth parameter, D3DPOOL_DEFAULT is now used instead of D3DPOOL_MANAGED. This instructs Direct3D to allocate the necessary image storage in video memory. This is a prerequisite for dynamic textures.
After that, a similar call is made for top_bottom_texture:
D3DXCreateTextureFromFileEx (
device,
TEXTURE_FILE_NAME,
TEXTURE_SIZE,
TEXTURE_SIZE,
D3DX_DEFAULT, 0,
D3DFMT_X8R8G8B8, D3DPOOL_MANAGED,
D3DX_DEFAULT, D3DX_DEFAULT, 0, NULL, NULL,
&top_bottom_texture
);
The main differences are the absence of D3DUSAGE_RENDERTARGET (since it will not be necessary to draw anything dynamically on the top and bottom faces) and the related use of D3DPOOL_MANAGED instead of D3DPOOL_DEFAULT. This second change allows Direct3D to use either video RAM or system RAM to store data associated with the texture. This indicates that top_bottom_texture should not be allowed to crowd, for example, texture_1 out of video memory.
Finally, the source code changes conclude with the body of do_2D_work() function built to dynamically render the main texture. It begins like this:
void do_2D_work () {
IDirect3DSurface9 * surface = NULL;
texture_1->GetSurfaceLevel (0, &surface);
static RECT redraw_rect={0};
if (!(redraw_rect.right))
{
redraw_rect.left = 0;
redraw_rect.top = TEXT_TOP;
redraw_rect.right = TEXTURE_SIZE;
redraw_rect.bottom = TEXT_POINTS + TEXT_TOP;
}
D3DXLoadSurfaceFromSurface
(
surface, NULL, &redraw_rect,
mesh, NULL,
&redraw_rect,
D3DX_FILTER_NONE, 0
);
This code first declares and acquires a pointer to the Direct3D surface object that corresponds to the dynamic texture texture_1. This will be drawn upon to render the texture for each frame.
Then, an area of the surface is re-painted from buffer mesh, by way of a call to D3DXLoadSurfaceFromSurface(). This area includes the area upon which text is being rendered, which spans the entire width of the texture from 0 to TEXTURE_SIZE, and occupies the vertical portion of the texture between TEXT_TOP and TEXT_POINTS+TEXT_TOP. This area must be cleared (i.e. re-painted from a copy of the background bitmap, mesh) with each frame, or the text will blur into a blue smudge. Note that static allocation is used for the necessary RECT, allowing initialization to happen only once, for maximum efficiency.
After all of this, some code related to text appearance is executed:
static D3DCOLOR font_color = DEMO_TEXT_COLOR;
static int text_left = 0;
static int text_left2 = TEXTURE_SIZE - 1;
text_left -= TEXT_SCROLL_SPEED;
text_left2 -= TEXT_SCROLL_SPEED;
if (text_left <= -TEXTURE_SIZE)
text_left = 0;
if (text_left2 < 0)
text_left2 = TEXTURE_SIZE - 1;
RECT text_rect;
text_rect.left=text_left;
text_rect.right=text_left+TEXTURE_SIZE;
text_rect.top=TEXT_TOP;
text_rect.bottom= text_rect.top+TEXT_POINTS;
Variables text_left and text_left2 hold the position of the left- and right-side text strings drawn onto the texture, respectively. These are static in order to preserve their values between calls, and the four statements after these variables are declared serve to implement the scrolling logic already described. Once this arithmetic is done, the resultant coordinates are loaded into a bounding rectangle for the first (left) string of text.
Now, the first of the two text strings can be drawn:
IDirect3DSurface9 *saved_surface;
device->GetRenderTarget(0, &saved_surface);
device->SetRenderTarget(0, surface);
main_font->DrawText(NULL, WRAP_STRING, -1, & text_rect, 0, font_color );
Line by line, this code first declares two surfaces, then saves the current rendering target, then sets the rendering target to the dynamic texture’s surface. (Direct3D supports chains of multiple rendering targets, but we are only using one at a time – target 0, the first parameter in two of these calls). Finally, the scrolling text string – WRAP_STRING – is drawn, using the appropriate font, color, and bounding rectangle. The first, NULL parameter could hold an instance of type Sprite, but this is not required. The -1 parameter is normally a string length parameter; the -1 indicates a null-terminated string.
All that remains is the following:
text_rect.left = text_left2;
text_rect.right=text_left2+TEXTURE_SIZE;
main_font->DrawText(NULL, WRAP_STRING, -1, & text_rect, 0, font_color );
device->SetRenderTarget(0, saved_surface);
saved_surface->Release();
surface->Release();
To summarize, the bounding rectangle is set up with the coordinate for the second string (text_left2). Then, another call to DrawText() is made, and the render target is returned to its previous value. Finally, the two surface objects that got referenced earlier are released.
At this point, the text-wrapping algorithm has been explained in detail, along with its Direct3D implementation. As promised, it is a quick and convincing technique, though it is not an all-encompassing one. The technique presented here is also noteworthy for its efficiency. In particular, the use of only a single dynamic texture offers size and speed advantages compared to other techniques.
Extending the Code
For reasons of performance and simplicity, the implementation provided here has been packaged as a free-standing demo. However, it should not be difficult to apply the techniques described here to any suitable polyhedron in any existing Direct3D application. In all cases, the game loop will need to call something like do_2D_work() before rendering the scene proper. This call constructs the dynamic texture as it will appear in each frame.
If the entire polyhedron is using a single, common texture, then a second, non-dynamic texture like top_bottom_texture will have to be added for the text-free faces; the steps necessary to do this are also spelled out in detail above. Then, code will have to be altered to render the top and bottom faces separately, using this new texture. Again, the required steps have all been covered in the last section.
In cases where an existing application is already rendering a polyhedron with dynamic surfaces, much work is already done. Something like do_2D_work() will likely already exist, and retrofitting dynamic text will largely be a matter of making the two DrawText() calls work within this existing 2D framework, along with the call to D3DXLoadSurfaceFromSurface().
The extension of the algorithm provided here to work with other classes of polyhedrons and with a wider variety of text strings is a more complex topic. The primary performance advantage of this article's algorithm is its reliance on a single dynamic texture, and any attempt to generalize the algorithm to other sorts of solids would seem to threaten this optimization. In some cases, this will be unavoidable, and it is hoped that this article can at least provide a starting point for that work. If nothing else, the Direct3D framework around which one might build something more clever is at least given here.
History
- 29th June, 2010: This is the third major version of this article. The second version added a demo link, and improved the lighting for some 3D hardware. This third version of the article re-words some passages for clarity, especially in the introductory passages explaining the algorithm.