How to draw a bitmap element using OpenGL


Summary
ArcGIS 9.2 has three display systems that use OpenGL as their underlying drawing pipe: ArcGlobe/GlobeControl, ArcScene/SceneControl, and MapControl's dynamic display.
One of the advantages of the ArcGIS platform is that it is open for different levels of customization, which allows the developer to choose the appropriate application programming interface (API) and development environment for a given task. One such task might be drawing a bitmap element in a specific way that is not supported by the standard API (such as billboard image or an image with special effects).
This document will guide you through the steps required to open a bitmap image, create an OpenGL texture for that image, create a display list for a geometry to which the texture will be mapped, and draw the item in the display.

In this topic


Drawing a bitmap element using OpenGL

This topic is for advanced users who need a high degree of customization. Therefore, this article assumes you are familiar with the following topics:
 
The idea behind this technique is to create OpenGL geometry; bind it with the texture of the image resource; translate, rotate, and scale it; and draw it in the right geographic location. Ideally, each display list and texture should get created once in the lifetime of the application, unless something changes and forces you to regenerate it (new OpenGL rendering context, new image for the texture, and so on).
 
You should keep in mind that OpenGL is a state machine, and therefore, you must make sure that OpenGL is ready to execute your commands without harming the application. Not doing so might lead to unexpected results. This means that you can only make calls to OpenGL where it is safe that the projection matrix is not modified, the transformation stack is reset, and so on. In addition, you must make sure that any OpenGL flags (such as LIGHTING, ALPHA_TEST, and ENABLE_TEXTURE) that you modify are switched back to their original state to prevent unexpected results.

Compiling a display list for the element's geometry

The first task is to create the display list for the geometry of the element. The display list can be any type of geometry that you prefer. In this case for the sake of simplicity, create an OpenGL quad. Drawing in OpenGL is done in a geocentric coordinate system whose values are dependent on the relevant application. It can range from 0.0 to 1.0 or be mapped to the application window size.
 
For that reason, it is convenient to use one unit size when creating the geometry and later on scale it to the desired scale when you are about to draw the geometry. This approach makes it easy to set different scales for items that get rendered on different scales (which might occur at the same scene in the case of a 3D application).
  1. Use OpenGL to generate a new display list. Use a unit class member to store the display list ID. See the following:
[Java]
m_itemList = GL.glGenLists(1);
  1. Start compiling OpenGL commands for the display list. OpenGL display lists are eventually a cached sequence of commands. See the following:
[Java]
GL.glNewList(m_itemList, GL.GL_COMPILE);
  1. Since you are about to use a one-unit size for your display list, you will have to push and later on pop the matrix stack to retain its current state. See the following:
[Java]
GL.glPushMatrix();
  1. In the following code example, use a quad geometry whose origin is set to the lower left corner. Therefore, you need to translate one-unit to the right, then up to draw the quad around its center.
[Java]
GL.glTranslatef( - 0.5f,  - 0.5f, 0.0f);
  1. Since you need to bind a texture to the geometry, be sure to enable it. Ultimately, you need to test whether it is enabled (use glIsEnabled ) to disable it in the end of the display list in cases where it wasn't enabled. If you want, you can also set the way texture values are interpreted when a fragment is textured. See the following:
[Java]
GL.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, (int)GL.GL_MODULATE);
GL.glEnable(GL.GL_TEXTURE_2D);
  1. Create the OpenGL geometry (quad) and specify the texture coordinates. See the following:
[Java]
GL.glBegin(GL.GL_QUADS);
GL.glTexCoord2f(0.0f, 0.0f);
GL.glVertex2f(0.0f, 0.0f);
GL.glTexCoord2f(0.0f, 1.0f);
GL.glVertex2f(0.0f, 1.0f);
GL.glTexCoord2f(1.0f, 1.0f);
GL.glVertex2f(1.0f, 1.0f);
GL.glTexCoord2f(1.0f, 0.0f);
GL.glVertex2f(1.0f, 0.0f);
GL.glEnd();
  1. Pop the matrix stack to retain its current state. See the following:
[Java]
GL.glPopMatrix();
  1. End the list. See the following:
[Java]
GL.glEndList();
Now that the display list of the geometry exists, you can bind any texture to it. This way, you can use this geometry to draw multiple picture items in different locations. This means you can create multiple textures and bind the relevant texture to the geometry right before you are about to draw it .
In cases where you need to bind only a single texture to your geometry, you can do the texture binding inside the display list. This makes your drawing sequence simpler since you do not have to bind the texture each time before drawing and, in addition, improves the performance (see sample Globe digitize tool). In cases where you intend to bind the texture as part of the display list, be sure to create the texture before you start compiling the display list; otherwise, it will heavily degrade the performance.

Opening the bitmap and creating the OpenGL texture for that image

In this section of the document, you will implement a new helper method to get an input bitmap and return the generated OpenGL texture ID. The OpenGL code for this sample was formed using an OpenGL wrapper class written by Colin P. Fahey (refer to C# wrapper for OpenGL for the Windows operating system).
 
This library does not fully support .NET calls. Therefore, the following code includes the use of unsafe code blocks, which allows the use of C pointers:
  1. Use GDI+ bitmap to open your image. See the following:
[Java]
Bitmap bitmap = new Bitmap(@ "FullImagePath");
  1. Create a new helper method that gets the bitmap and returns the OpenGL texture ID. See the following:
[Java]
private uint CreateTexture(Bitmap bitmap){}
To generate a valid texture, OpenGL requires that the image size (on each dimension) be a power of 2. The first section of the method is dedicated to calculating the closest size that matches the original image size, which is a power of 2, and you scaling the image to match that size.
 
  1. Get the image size. In the following code, make the resized image square (for simplicity):
[Java]
int h = bitmap.Height;
int w = bitmap.Width;
int s = Math.Max(h, w);
  1. Calculate the closest power of 2 to match the image size and, accordingly, set the size of the buffer that will be used for the resized image. See the following:
[Java]
double x = Math.Log(Convert.ToDouble(s)) / Math.Log(2.0);
s = Convert.ToInt32(Math.Pow(2.0, Convert.ToDouble(Math.Ceiling(x))));
int bufferSizeInPixels = s * s;
  1. Get the bitmap's raw data (the bitmap's underlying data structure). You have to flip the image to get the right pixel order. See the following:
[Java]
Rectangle rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
bitmap.RotateFlip(RotateFlipType.RotateNoneFlipY);
System.Drawing.Imaging.BitmapData bitmapData;
bitmapData = bitmap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadOnly,
    System.Drawing.Imaging.PixelFormat.Format24bppRgb);
  1. Create a new buffer to hold the BGR (blue, green, and red) values of the scaled image. See the following:
[Java]
byte[] bgrBuffer = new byte[bufferSizeInPixels * 3];
  1. Scale the image. See the following:
[Java]
unsafe{
    fixed(byte * pBgrBuffer = bgrBuffer){
        GLU.gluScaleImage(GL.GL_BGR_EXT, bitmap.Size.Width, bitmap.Size.Height,
            GL.GL_UNSIGNED_BYTE, bitmapData.Scan0.ToPointer(), s, s,
            GL.GL_UNSIGNED_BYTE, pBgrBuffer);
    }
}
The next step in creating the texture, is to set a transparency color for the image. This means that you have to set an alpha value to each pixel and assign a value of 0 in case the pixel color is the transparency color; any other pixel gets an alpha value of 1.
 
To accomplish this, you have to create additional buffers of BGRA values (blue, green, red, and alpha), populate them with the BGR values, and set the alpha values according to the transparency color.
In this example, the transparency color is set to white (255).
  1. Create the BGRA buffer. See the following:
[Java]
byte[] bgraBuffer = new byte[bufferSizeInPixels * 4];
  1. Populate the buffer. See the following:
[Java]
int posBgr = 0;
int posBgra = 0;
for (int i = 0; i < bufferSizeInPixels; i++){
    bgraBuffer[posBgra] = bgrBuffer[posBgr]; //This is the buffer value of B.
    bgraBuffer[posBgra + 1] = bgrBuffer[posBgr + 1]; //This is the buffer value of G.
    bgraBuffer[posBgra + 2] = bgrBuffer[posBgr + 2]; //This is the buffer value of R.

    //Sets the alpha buffer value.
    if (255 == bgrBuffer[posBgr] && 255 == bgrBuffer[posBgr + 1] && 255 ==
        bgrBuffer[posBgr + 2]){
        bgraBuffer[posBgra + 3] = 0;
    }
    else{
        bgraBuffer[posBgra + 3] = 255;
    }
    posBgr += 3;
    posBgra += 4;
}
  1. Use OpenGL to generate a new texture and bind it to the 2D textured target. See the following:
[Java]
uint[] texture = new uint[1];
GL.glEnable(GL.GL_TEXTURE_2D);
GL.glGenTextures(1, texture);
GL.glBindTexture(GL.GL_TEXTURE_2D, texture[0]);
  1. Set the texture parameters. See the following:
[Java]
GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, (int)GL.GL_LINEAR);
GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, (int)GL.GL_LINEAR);
GL.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, (int)GL.GL_MODULATE);
  1. Specify the 2D texture image buffer for the newly created texture. See the following:
[Java]
unsafe{
    fixed(byte * pBgraBuffer = bgraBuffer){
        GL.glTexImage2D(GL.GL_TEXTURE_2D, 0, (int)GL.GL_RGBA, s, s, 0,
            GL.GL_BGRA_EXT, GL.GL_UNSIGNED_BYTE, pBgraBuffer);
    }
}
  1. Unlock the bitmap from memory. See the following:
[Java]
bitmap.UnlockBits(bitmapData);
  1. Return the new texture ID. See the following:
[Java]
return texture[0];

Binding the texture to the OpenGL geometry and drawing it in the right place

Now that the geometry display list and the image texture exist, you can use them to draw the textured element in the application display. Inside the application relevant draw method, add the following code in the next group of steps.
 
  1. Since you have to translate the item into the right place, you have to push the matrix stack so that you can retain it once you are done drawing. See the following:
[Java]
GL.glPushMatrix();
  1. Translate the transformation matrix into the right geocentric coordinates where you want to draw your item. This means that if your item is a geographic item, you need to know the transformation between world coordinates and geocentric coordinates (OpenGL). The transformation is dependent on the application in which you are drawing. For example, if you are drawing inside ArcGlobe/GlobeControl, you can use the interface IGlobeViewUtil. GeographicToGeocentric() to transform from geographic to geocentric. If you are using the dynamic display, you can use the method IDisplayTransformation. FromMapPoint(). See the following:
[Java]
GL.glTranslatef((float)m_deviceFrame.left + 70.0f, (float)m_deviceFrame.top + 70.0f,
    0.0f);
  1. Scale the geometry to the appropriate size. You have constructed the geometry inside the display list and used a size of one unit. You need to scale the geometry to render it according to its real-world size or to a size that looks good when displayed. There are several ways to calculate the scale ratio. For example, in the case of ArcGlobe/GlobeControl, it is possible to get the value of the globe radius in geographic units (IGlobeDisplayRendering). Knowing that the globe radius in geocentric units is 1, you can conclude the scale ratio as double scale = 1.0/globeRadiusMeters. See the following:
[Java]
GL.glScalef(scale, scale, 0.0f);
  1. Rotate the geometry. In the case of a 2D display, rotate the geometry around the z-axis. In a 3D environment, the rotation can be around any of the axes. See the following:
[Java]
GL.glRotatef((float)rotation, 0.0f, 0.0f, 1.0f);
  1. Bind the texture to the target geometry. See the following:
[Java]
GL.glBindTexture(GL.GL_TEXTURE_2D, (uint)m_textureId);
  1. You must set the color to white to allow for the texture to display correctly. See the following:
[Java]
GL.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
  1. Call the display list to draw the geometry. See the following:
[Java]
GL.glCallList(m_itemList);
  1. Finally, you have to pop back the matrix stack to retain the original state. See the following:
[Java]
GL.glPopMatrix();

Putting it all together

Now that you have all the pieces together, your drawing method should look similar to the following:
[Java]
void DrawMethod(� � �){
    if (m_bOnce){
        CreateDisplayLists();
        Bitmap bitmap = new Bitmap(m_imagePath);
        m_textureId = CreateTexture(bitmap);

        m_bOnce = false;
    }

    //Draw the element.
    GL.glPushMatrix();
    GL.glTranslatef((float)m_deviceFrame.left + 70.0f, (float)m_deviceFrame.top +
        70.0f, 0.0f);
    GL.glScalef(scale, scale, 0.0f);
    GL.glRotatef((float)Display.DisplayTransformation.Rotation, 0.0f, 0.0f, 1.0f);
    GL.glBindTexture(GL.GL_TEXTURE_2D, (uint)m_textureId);
    GL.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
    GL.glCallList(m_itemList);
    GL.glPopMatrix();
}


See Also:

Dynamic Drawing APIs
Rendering Dynamic Content using IDynamicLayer
Dynamic Map Events
Sample:Add OpenGL Heads Up Display




Additional Requirements
  • The technique described in this document relies on a third-party OpenGL library since there is no direct interface to OpenGL in .NET. Open source wrapper libraries are available for download; however, the current library used by the code in this document (written by Colin P. Fahey) requires you to use unsafe code sections. Therefore, the code cannot be used in VB.NET.

Development licensingDeployment licensing
Engine Developer KitEngine: 3D Analyst
ArcGIS for Desktop Basic: 3D AnalystArcGIS for Desktop Basic: 3D Analyst
ArcGIS for Desktop Standard: 3D AnalystArcGIS for Desktop Standard: 3D Analyst
ArcGIS for Desktop Advanced: 3D AnalystArcGIS for Desktop Advanced: 3D Analyst