How to draw a bitmap element using OpenGL


Summary
ArcGIS versions 9.2 and later have the following 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 topic guides you through the required steps 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, then draw the item in the display.


Drawing a bitmap element using OpenGL

This topic is for advanced users who need a high degree of customization; therefore, this topic assumes you are familiar with the following:
  • Visual C# programming language.
  • ArcGIS Engine and ArcGIS for Desktop architecture—This means that you know exactly when and where to plug in the OpenGL drawing (the custom layer drawing methods, Before/After draw event handlers, and so on). For more information, see RSS weather 3D layer.  
  • Working knowledge of OpenGL—If you need to refresh your OpenGL knowledge, the following are some popular OpenGL Web sites:
The idea behind this technique is to create OpenGL geometry; bind it with the texture of the image resource; translate, rotate, and scale it, then 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).
Remember, OpenGL is a state machine; therefore, 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, 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, then later 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 code example:
[C#]
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 code example:
[C#]
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, then later pop the matrix stack to retain its current state. See the following code example:
[C#]
GL.glPushMatrix();
  1. In the following code example, use a quad geometry whose origin is set to the lower left corner; therefore, translate one unit to the right, then up to draw the quad around its center:
[C#]
GL.glTranslatef( - 0.5f,  - 0.5f, 0.0f);
  1. Since you need to bind a texture to the geometry, enable it. Ultimately, test whether it is enabled (use glIsEnabled ) to disable it in the end of the display list in cases where it was not enabled. If you want, you can also set the way texture values are interpreted when a fragment is textured. See the following code example:
[C#]
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 code example:
[C#]
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 code example:
[C#]
GL.glPopMatrix();
  1. End the list. See the following code example:
[C#]
GL.glEndList();
Now that the display list of the geometry exists, you can bind any texture to it. This way, you can use the geometry to draw multiple picture items in different locations. This means you can create multiple textures and bind the relevant texture to the geometry before you draw it (see the previously mentioned RSS weather 3D layer sample).
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 the previously mentioned Globe digitize tool sample). In cases where you intend to bind the texture as part of the display list, create the texture before you start compiling the display list; otherwise, it heavily degrades the performance.

Opening a bitmap and creating the OpenGL texture for that image

In this section, 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 (see C# wrapper for OpenGL for the Windows operating system). This library does not fully support .NET calls; therefore , the following code example 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 code example:
[C#]
Bitmap bitmap = new Bitmap(@"FullImagePath");
  1. Create a helper method that gets the bitmap and returns the OpenGL texture ID. See the following code example:
[C#]
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 scaling the image to match that size.
  1. Get the image size. In the following code example, make the resized image square (for simplicity):
[C#]
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 code example:
[C#]
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). Flip the image to get the correct pixel order. See the following code example:
[C#]
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 buffer to hold the BGR (blue, green, and red) values of the scaled image. See the following code example:
[C#]
byte[] bgrBuffer = new byte[bufferSizeInPixels * 3];
  1. Scale the image. See the following code example:
[C#]
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, 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 code example:
[C#]
byte[] bgraBuffer = new byte[bufferSizeInPixels * 4];
  1. Populate the buffer. See the following code example:
[C#]
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 code example:
[C#]
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 code example:
[C#]
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 created texture. See the following code example:
[C#]
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 code example:
[C#]
bitmap.UnlockBits(bitmapData);
  1. Return the new texture ID. See the following code example:
[C#]
return texture[0];

Binding the texture to the OpenGL geometry and drawing it in the correct location

Now that the geometry display list and the image texture exist, use them to draw the textured element in the application display. Inside the application relevant draw method, add the following code example in the next group of steps.
  1. Since you have to translate the item into the correct location, push the matrix stack so you can retain it once you are done drawing. See the following code example:
[C#]
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 IGlobeViewUtil.GeographicToGeocentric() to transform from geographic to geocentric. If you are using the dynamic display, you can use IDisplayTransformation.FromMapPoint(). See the following code example:
[C#]
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. 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 code example:
[C#]
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 code example:
[C#]
GL.glRotatef((float)rotation, 0.0f, 0.0f, 1.0f);
  1. Bind the texture to the target geometry. See the following code example:
[C#]
GL.glBindTexture(GL.GL_TEXTURE_2D, (uint)m_textureId);
  1. Set the color to white to allow the texture to display correctly. See the following code example:
[C#]
GL.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
  1. Call the display list to draw the geometry. See the following code example:
[C#]
GL.glCallList(m_itemList);
  1. Pop back the matrix stack to retain the original state. See the following code example:
[C#]
GL.glPopMatrix();

Putting it all together

Now that you have all the pieces together, your drawing method resembles the following code example:
[C#]
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:

How to get and install an OpenGL wrapper for .NET
Sample: RSS weather 3D layer




Additional Requirements
  • The technique described in this topic 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 topic (written by Colin P. Fahey) requires you to use unsafe code sections. Therefore, the code cannot be used in VB .NET.

Development licensing Deployment licensing
Engine Developer Kit Engine: 3D Analyst
ArcGIS for Desktop Basic: 3D Analyst ArcGIS for Desktop Basic: 3D Analyst
ArcGIS for Desktop Standard: 3D Analyst ArcGIS for Desktop Standard: 3D Analyst
ArcGIS for Desktop Advanced: 3D Analyst ArcGIS for Desktop Advanced: 3D Analyst