Working with Bitmaps
See Also: BitmapManager, BitmapInfo, Bitmap, BitmapIO, BitmapStorage.
Overview
This section presents information about working with bitmap images. It discusses the main classes used, and presents information about concepts such as creating, loading and saving bitmaps, memory management when working with bitmaps, palettes, alpha, gamma, the geometry/graphics buffer (G-buffer), working with multi-frame images, handling errors that occur, and pixel storage formats. This section also documents a few utility functions in the API that are not part of any class.
Overview of the Principal Classes
The following three classes are the main ones used when working with bitmaps:
There is a global object defined by 3ds max called TheManager. This is an instance of the class BitmapManager. This object manages and enables developers to work with bitmaps in MAX. For example, this class provides methods for creating and loading bitmaps. It also has methods for displaying some common dialogs that let users interactively specify files and devices to work with, and set options for bitmaps.
BitmapInfo is the class used to describe the properties of a bitmap. The developer can declare an instance of this class and use its methods such as SetWidth(), SetHeight(), SetGamma(), and SetName() to describe the bitmap properties. The other classes related to bitmaps then use this information. Thus BitmapInfo is the heart of all image input/output. For example, all but a few methods in the BitmapManager use a BitmapInfo object as the main argument. For instance, if you wish to create a bitmap, you declare an instance of BitmapInfo and use its methods to establish the bitmap properties. Then when you call the BitmapManager method Create(), you pass the BitmapInfo object. The BitmapManager uses this information to determine how much memory to allocate based on the width, height and color depth.
This class also has methods to get and set the number of frames used in multi-frame bitmaps, and to define 'custom' properties of the bitmap. Custom properties let you specify only a portion of the main bitmap, such as a smaller, sub-region of the original, or fewer frames than the original (different begin and end settings or a different frame increment step size for multi-frame bitmaps). These custom properties are read and used by bitmap copying operations for example.
The Bitmap class is the bitmap itself. All image access is done through this class. This class provides standard methods to retrieve and store pixels from the image. The Bitmap class has methods to retrieve parameters of the bitmap such as its width, height, whether it is dithered, or has an alpha channel. Additional methods allow developers to open bitmaps for output, write multi-frame images, and to copy pixel data between bitmaps.
There are other bitmap related classes used only by developers who create 3ds max plug-ins used to load and save new bitmap file formats. The methods of these classes are called by the BitmapManager and Bitmap classes and not by developers themselves. The primary classes used for creating image loading and saving plug-ins are:
This is the main plug-in class used by developers creating image loader / saver plug-ins. For example, a developer creating a plug-in to support the PCX file format would derive their plug-in class from BitmapIO. This class is used for both files and devices (devices are items such as digital disk recorders). Developers implement pure virtual methods of this class to provide information about the image loader / saver they are creating. This is information such as the plug-in author name, copyright data, image format description, filename extension(s) used, and other capabilities of the image loader / saver. The developer also implements methods to load the image from disk, prepare it for output, write image data to it, and close it. Image loader / saver plug-ins use the BitmapStorage class described below to load and save their actual pixel data.
When an image is loaded or created, the buffer that will hold the image data is an instance of the BitmapStorage class. This class allows developers to access the image in a uniform manner even though the underlying storage might be 1, 8, 16, 32 or 48 bit. For example, a paletted 8-bit format is perfect for loading GIF files but not for loading 32-bit Targa files. The inverse is also true. There is no point in creating a true color 64-bit storage to load a GIF file that only has 8-bit color information.
Thus the BitmapStorage mechanism was created so images could be kept in memory in a more efficient way. Instead of loading everything in MAX's internal 64-bit format, bitmaps are loaded and/or created using the most efficient storage for their type. The BitmapStorage class provides an uniform set of pixel access methods to hide these different formats from the developer using bitmaps. For example, standard methods are available for getting and putting pixels at various color depths. In this way, even though an image may be a 1-bit monochrome line art bitmap, pixels may be retrieved or stored at other color depths. For example the BitmapStorage methods Get16Gray()/Put16Gray(), GetTruePixels()/PutTruePixels(), and GetIndexPixels()/PutIndexPixels()provide access to pixel data at various color depths.
Again, note that the BitmapStorage class is a low level access mechanism used by image loader / saver (BitmapIO) plug-ins. Developers wanting to access an image use the methods in the Bitmap class instead.
Needed #include and LIB files
When working with bitmaps, make sure you add the bitmap include file to your source file, i.e. use the statement:
#include "bmmlib.h"
Also be sure to set your MS VC++ project settings to link to \MAXSDK\LIB\BMM.LIB
Using bmmlib.h and bmm.lib will ensure you have access to all the bitmap classes, methods, and functions.
Creating, Opening, Writing, and Closing Bitmaps
This section shows how the BitmapInfo and BitmapManager classes may be used to create, open, and write to bitmaps. Following this is code that demonstrates how to open an existing bitmap using the standard 3ds max Open File dialog box. Both these examples show how to properly delete the bitmaps after they are used.
To create a bitmap from scratch, we need to declare a pointer to point to it:
Bitmap *bmap;
Remember that the Bitmap class represents the bitmap itself. Next, we need to declare an instance of the BitmapInfo class to describe the properties of the bitmap to create:
BitmapInfo bi;
Then we initialize the BitmapInfo with the properties of the bitmap we wish to create. This is done using the methods of BitmapInfo such as SetType(), SetWidth(), etc.:
// Initialize the BitmapInfo instance
bi.SetType(BMM_TRUE_64);
bi.SetWidth(320);
bi.SetHeight(200);
bi.SetFlags(MAP_HAS_ALPHA);
bi.SetCustomFlag(0);
Note in the above code how the type is set. The type describes the number of bits per pixel used to describe the image, whether the images is paletted (has a color map), and if it has an alpha channel. To see a list of the types of bitmaps that may be created see List of Bitmap Types. In this example, we use BMM_TRUE_64. This format has 16-bits for each color (RGB) and alpha component. This is the format that 3ds max uses internally in the renderer.
Once the BitmapInfo is initialized, we can call a method of the bitmap manager to create the bitmap. A global instance of the BitmapManager class exists called TheManager. This is what we use to call methods of BitmapManager as shown below:
// Create a new bitmap
bmap = TheManager->Create(&bi);
Note that we pass it our BitmapInfo instance. This is where the bitmap manager gets the information on the bitmap to create. If the pointer returned is NULL, an error has occurred in creating the bitmap. If the pointer is non-NULL, it's valid and we can use it to call methods of the Bitmap class to work with the bitmap. The code below shows how the PutPixels() method is used to write data to the bitmap. This code sets every pixel of the image to the color and opacity specified by r, g, b and alpha.
...
BMM_Color_64 *line, *lp, color = {r, g, b, alpha};
int bmapWidth = bmap->Width(), bmapHeight = bmap->Height();
if ((line = (BMM_Color_64 *)calloc(bmapWidth, sizeof(BMM_Color_64)))) {
int ix, iy;
for(ix = 0, lp = line; ix < bmapWidth; ix++, lp++)
*lp = color;
for(iy = 0; iy < bmapHeight; iy++)
int res = bmap->PutPixels(0, iy, bmapWidth, line);
free(line);
}
When we are done using the bitmap, we need to delete it. This is done by simply using the delete operator:
if (bmap) bmap->DeleteThis();
That's all that's required to create a bitmap from scratch.
The following code demonstrates two other methods of the bitmap manager -- SelectFileInput() to allow the user to choose a file to load (and initialize the BitmapInfo instance passed to it) and Load() to create a bitmap to hold the image. Again, we declare a Bitmap pointer and a BitmapInfo instance.
Bitmap *bmap;
BitmapInfo bi;
Next, we use a method of the bitmap manager to allow the user to choose an image file. This method returns FALSE if the user cancels and TRUE if they select an image. If you want to directly indicate which file to load, just set the fields of BitmapInfo yourself (for example bi.SetName(_T("TEST.JPG"));)
// Let the user choose the file to open
BOOL res = TheManager->SelectFileInput(&bi, ip->GetMAXHWnd(),
_T("Open File"));
if (!res) return; // User cancelled...
After SelectFileInput() returns, the BitmapInfo instance passed to it contains the necessary information about the bitmap to load. To load the bitmap, the Load() method of the bitmap manager is used.
// Load the selected image
BMMRES status;
bmap = TheManager->Load(&bi, &status);
if (status != BMMRES_SUCCESS) {
MessageBox(ip->GetMAXHWnd(), _T("Error loading bitmap."),
_T("Error"), MB_ICONSTOP);
}
Once the image is loaded, you can work with it like any other bitmap. For example to display the bitmap in a window, use the Bitmap::Display() method.
// Display the opened bitmap
bmap->Display(title, BMM_CN, FALSE, TRUE);
A few more notes on the bitmap manager Load() method -- Additional options may be set by calling BitmapManager::ImageInputOptions() before calling Load(). This method will ask the user for special details such as custom positioning of smaller/larger images, etc. This method sets the proper fields in BitmapInfo.
The examples above show how to create and load a bitmap. Once you have the bitmap what if you want to save it to disk? Also, how do you deal with multi-frame files (like a FLC or AVI)? The example below demonstrates both these things for a multi-frame file. Again, we declare a Bitmap pointer and a BitmapInfo instance.
Bitmap *bmap;
BitmapInfo bi;
To allow the user to choose an output file type, use the BitmapManager method SelectFileOutput(). This brings up the 'Browse Images for Output' dialog box. This method returns TRUE if the user selected a file; it returns FALSE if they cancel the dialog box. This method also handles checking if the filename chosen already exists, and if so provides an overwrite question dialog. This method will fill in the proper fields of the BitmapInfo passed.
BOOL gotIt = TheManager->SelectFileOutput(&bi, ip->GetMAXHWnd());
if (!gotIt) return; // User cancelled…
Next we need to set the image size and the properties of BitmapInfo related to multi-frame images. Below we do this for a 30 frame sequence.
bi.SetWidth(640)
bi.SetHeight(480)
bi.SetFirstFrame(0)
bi.SetLastFrame(29)
With the BitmapInfo setup, we can call the BitmapManager to create the sequence:
bmap = TheManager->Create(&bi);
The next code is used to open the bitmap for output. This indicates to the system that the bitmap is open for output and we can write to.
bmap->OpenOutput(&bi);
Next we simply write the images for each frame:
for (frame = 0; frame < 30; frame++) {
// Do something to the image
// ...
// Write the image
bmap->Write(&bi, frame);
}
When we are done we need to close the image:
bmap->Close(&bi)
Note: You can add any number of outputs to a bitmap. Just keep calling bmap->OpenOutput() with different outputs (for instance a TGA file and Frame Buffer). To write or close a specific output, use Write() and Close(). To write and close them all at once, use the Bitmap methods WriteAll() and CloseAll(). It is okay to use WriteAll() and CloseAll() if you have just one output defined.
Multi-frame Files
Certain file types (and most devices) are multi-frame. AVI, IFL, FLI, and FLC are all formats that support multiple frames. The Bitmap class has methods for dealing with these multi-frame bitmaps. For example, you can set the current frame to load or save. Also, the BitmapInfo class has methods that will let you work with only a subset of frames of a multi-frame image, for example, SetStartFrame(), SetEndFrame(), and SetCustomStep() let you specify a different start, end and frame increment to be used.
When loading files, BitmapInfo defaults to frame 0. For multi-frame files you should specify the frame number you want to load. This is done by using bi.SetCurrentFrame(f) before calling Load().
High Dynamic Range Bitmaps
Newly added to the bitmap system in R4 are High Dynamic Range bitmaps. This was accomplished by adding methods to get and put floating point color values into the Bitmap and BitmapStorage classes. Conversions between floating point and fixed point representations are handled by the BitmapStorage, include clamping and scaling of floating point values.
There are four high dynamic range BitmapStorage formats:
· LogLUV32: This format uses a logarithmic encoding of luminance and U’ and V’ in the CIE perceptively uniform space. It spans 38 orders of magnitude from 5.43571´10-20 to 1.84467´1019 in steps of about 0.3% luminance steps. It includes both positive and negative colors. A separate 16 bit channel is kept for alpha values.
· LogLUV24: This format is similar to LogLUV32 except is uses smaller values to give a span of 5 order of magnitude from 1/4096 to 16 in 1.1% luminance steps. A separate 8 bit channel is kept for alpha values.
· LogLUV24A: This format is identical to LogLUV24, except the 8 bit alpha value is kept with the 24 bit color value in a single 32 bit word.
· RealPixel: This format encodes the exponent, e, of the largest rgb component of the pixel, and the ratio of each component with 2e.
Added BMM_Color_fl for foating point access to bitmaps.
There are new methods in Bitmap and BitmapStorage that allow data to be retrieved in floating point values rather then integer values so 3ds max can get the high dynamic range data without clamping.
BMM_Color_fl uses floats to hold the RGBA color components, rather than 16 bit integers which are used in BMM_Color_64. To convert from BMM_Color_64 to BMM_Color_fl you divide each component by 65535.0 and to go back you multiply each component by 65535.0. High Dynamic Range bitmaps are not restricted to the range 0.0 to 1.0. Some formats allow negative values and all of the formats can go well above 1.0.
There is a problem is when converting from BMM_Color_fl to BMM_Color_64, since the value may exceed 65535. The bitmap will either clamp the values to 0 to 65535, or scale the values by the largest component value so all of the components are in the range 0 to 65535.
The class hierarchy of BitmapStorage was changed a little. Two new classes BitmapStorageLDR and BitmapStorageHDR are derived from BitmapStorage and should be used as the base class for BitmapStorage implementations. BitmapStorageLDR provides default implementations of the new floating point BitmapStorage methods and BitmapStorageHDR provides default implementations of the 64 bit pixel BitmapStorage methods.
Implemented converters between AColor and BMM_Color_fl.
Implemented converter between Color and high dynamic range pixel formats.
Added method IsHighDynamicRange to Texmap so we can determine when a texmap is returning high dynamic range data.
PreShade and PostShade store high dynamic range data when a Texmap indicates that it returns high dynamic range data.
Added interface method UseHighDynamicRange to allow StdMirror and StdCubic to use high dynamic range bitmaps.
Custom Bitmap Properties
You may occasionally want to work with only a portion of a bitmap or series of images. For example, when copying one file to another, if you had a 640x480, 30 frame FLC file, you might want to work with the lower right corner 160x100 portion, using every other frame, perhaps starting at frame 5 and ending at frame 15. The 'Custom' methods of the BitmapInfo class let you specify these options. Methods such as SetCustomWidth(), SetCustomX(), and SetCustomStep() let you specify the part of the source image that should be manipulated. The method that copied the image would see that these custom properties were set and would act accordingly. See the BitmapInfo class for details on these methods.
You may use the BitmapManager::ImageInputOptions() method to allow the user to specify these options via the standard 3ds max Input Image Options dialog. This dialog simply sets the appropriate data members in BitmapInfo based on the user's choices.
Memory Management for Plug-Ins that work with Bitmaps
Memory is allocated and de-allocated by the bitmap classes in various ways. It is important to understand when memory needs to be freed by the developer, and when the system will take care of freeing it. This section discusses this issue.
The bitmap manager methods Create() and Load() both return pointers to bitmaps that need to be freed when the developer is done working with them. This is accomplished by simply using the delete operator on the pointer returned from the methods. The pseudo code below shows both of these cases.
This is the BitmapManager::Create() case:
Bitmap *bmap;
BitmapInfo bi;
bmap = TheManager->Create(&bi);
// Work with the bitmap...
// ...
// Free the bitmap when done
if (bmap) bmap->DeleteThis();
This is the BitmapManager::Load() case:
BMMRES status;
Bitmap *bmap;
BitmapInfo bi;
BOOL res = TheManager->SelectFileInput(&bi, ip->GetMAXHWnd(),
_T("Open File"));
bmap = TheManager->Load(&bi, &status);
// Work with the bitmap...
// ...
// Free the bitmap when done
if (bmap) bmap->DeleteThis();
As the code above shows, the developer is responsible for freeing memory from both these methods.
Another example of getting a pointer to a bitmap and needing to free it is the pointer returned from the Bitmap method ToDIB(). This method creates a new Windows Device Independent Bitmap (DIB) and returns a pointer to it. The DIB bitmap is created from the bitmap whose method is called. The DIB is allocated internally using LocalAlloc() and must be freed by the developer using LocalFree(). The pseudo-code below show how this is done.
PBITMAPINFO pDib;
pDib = bmap->ToDib();
// Work with the bitmap...
// ...
// Free the bitmap when done
LocalFree(pDib);
There is a different case where the developer receives a pointer to a bitmap, but is NOT responsible for deleting it when done. If you have a video post Image Filter plug-in, it receives a pointer to the video post bitmap queue. This bitmap should not be deleted as video post is using it internally. For example, in the ImageFilter::Render() method, the filter plug-in has access to a source bitmap named srcmap. The methods of this pointer may be called, but the bitmap itself should not be deleted by the plug-in. Video post will take care of it.
Memory Management for Image Loader/Saver Plug-Ins
This section discusses how memory is managed internally by image loader / saver plug-ins. These are the plug-ins derived from class BitmapIO. Examples of this type of plug-in are the GIF, FLC and JPG IO modules. Developers who are not creating these types of plug-ins do not need to be concerned with these details.
The memory allocated to a bitmap is managed internally by an instance of BitmapStorage. This class provides access to the pixels through a uniform interface.
When a developer creates an image, the memory is allocated by the system. As long as the bitmap is being used within the system, the bitmap remains allocated. This is handled internally by a usage counter that tracks if a bitmap is still being used. If another use of the bitmap takes place, the usage count is incremented. If the use of a bitmap ends, the usage count is decremented. When the usage count for the bitmap goes to zero, the system frees the memory. This happens automatically without intervention from the plug-in.
For example, once a bitmap is loaded (this is never the case when a bitmap is created), a storage for the image is created to hold the actual bitmap. If a second attempt to open this same bitmap is made (the same file and the same frame), instead of creating a new storage, the bitmap is created pointing to the existing storage. In the storage a counter is incremented to tell how many bitmaps are using it. When a developer deletes the bitmap (i.e. delete MyMap;) the destructor calls the storage and asks to be unlinked from the storage. The storage decrements the usage count and if it reaches 0, the storage itself is also deleted.
Note that this is only the case when a bitmap is loaded because when you create a bitmap, it doesn't yet exist in the file system. There is no way for someone else to "open" it. This is why when you create a bitmap, it is said this is a "WRITE ONLY" bitmap, and when you load a bitmap it is said this is a "READ ONLY" bitmap. In order to read and write a bitmap you must load the original bitmap, create a second, copy the data (doing whatever processing in between), and then write the newly created bitmap.
Bitmap Adjustment - Changes to Resolution and Color Depth
There are several ways to change the resolution of a bitmap. In each case this involves creating a new bitmap and copying the existing bitmap to the new. To change the image size without any scaling of the pixel data, use Bitmap::CopyImage() and specify the COPY_IMAGE_CROP operation. To resize the bitmap you use Bitmap::CopyImage() as well. This method lets you specify either a low quality (faster) or a higher quality (slower) copy. See List of Copy Image Operations for examples of each.
To change the color depth (number of bits per pixel), you must create a new bitmap of the desired color depth, and copy the original to the new using Bitmap::CopyImage().
Palettes
Some color bitmaps use only 8-bits per pixel. These images, unlike true color images where every pixel can be a unique color, are limited by the colors stored in a palette. For example, an 8-bit GIF file has a 256 color palette. The color values stored by the palette can be any color, but the actual bitmap image can only be comprised of colors from the palette. And since the palette is limited to 256 colors, the image has a maximum of 256 unique colors. For paletted bitmaps, each pixel in the image is actually an index into the palette (sometimes called a color lookup table). So the pixel value tells the system which palette slot to look in, and the value in that palette slot determines the exact color.
In contrast, true color images, for example a 24-bit TGA file, store the color of the pixel directly in the pixel value. There is no palette used.
In MAX, every bitmap has storage for a palette even if it is not used. There are methods of the Bitmap class used to work with palettes. A developer may use Bitmap::IsPaletted() to determine if an image is indeed paletted. To access the palette of a bitmap, Bitmap::GetPalette() and Bitmap::SetPalette() may be used.
Palettes are primarily a concern for image loader plug-in derived from BitmapIO. When loading an 8 bit image, the loader would create an 8-bit storage and set the palette through BitmapStorage::SetPalette(). Sample code is available showing how this is done in \MAXSDK\SAMPLES\IO\BMP\BMP.CPP in the Load() method.
Pixel Storage
There are several in-memory storage formats for pixel data. For a list of the available formats see the section Pixel Storage Types. Also see Class PixelBuf, and Template Class PixelBufT for some useful utility classes that manage single scanline buffers.
Pre-Multiplied Alpha
The following is a discussion of the concept of pre-multiplied alpha as used by MAX. A 32-bit bitmap file contains four channels of data: red, green, blue, and alpha. The first three provide color information to the pixels, while the alpha channel provides transparency information. There are two methods of storing alpha in a bitmap -- pre-multiplied and non-pre-multiplied.
To composite an image that is in non-pre-multiplied format, the alpha must be multiplied by each of the R,G, and B channels before adding it in to the color of the background image. This provides the correct transparency effect, but must be done each time you composite. With pre-multiplied alpha, you store the R,G, and B components with the alpha already multiplied in, so compositing is more efficient.
However, this is not the only reason that 3ds max stores images in the pre-multiplied format. When you render an image, you typically want the edges of the objects to be anti-aliased. This effect is achieved by determining the fractional coverage of pixels on the edge of the object, and then adjusting the alpha of the pixel to reflect this. For example, a pixel which is 30% covered by the object will have an alpha of 0.30. To anti-alias the edges, the alpha must be pre-multiplied to darken these edge pixels. (This is basically equivalent to compositing the image over a black image). So it is natural, in a sense, for rendered images to have pre-multiplied alpha. If you didn't pre-multiply in the alpha of a rendered image, then just looking at the RGB you would see jaggies on the edges of objects: you'd have to composite it against black using the alpha channel whenever you wanted to display it.
Pre-multiplied alpha works as follows: If you have an image A which is partially transparent, and you want to composite it over an image B, the alpha channel of A tells you at each pixel how much of A and B to mix in. If A's alpha is pre-multiplied (as it always is in MAX) then the formula is:
color = A + (1-A.alpha)*B
alpha can also be thought of as the "opacity" of A at a given pixel.
If image A was stored with Non-Pre-Multiplied Alpha (NPMA) then the formula for compositing would be:
color = A.alpha*A + (1-A.alpha)*B
To understand why pre-multiplied alpha is used consider an anti-aliased edge of an object, rendered against a black background. The pixels along the edge will have an alpha less than 1.0, and when the alpha is multiplied in, it will make the edge look smooth (i.e. anti-aliased). If you don't pre-multiply the alpha, the RGB image displayed as-is (with out taking into account alpha) looks jagged: multiplying by the alpha is what anti-aliases the image.
This begs the question "Why would you ever use non-pre-multiplied alpha?". Say you have an image without an alpha channel. You want to create an alpha channel to mask out all but a certain object, but want to leave the original image unchanged: you may be using the same image at other places in your rendering. In this case, you would want to paint an alpha channel using an image editor (such as Photoshop) which would mask out the image, and combine it using the non-pre-multiplied alpha formula.
MAX's Mask texture map allows you to do this, in fact. Basically Mask lets you combine a texture with an mask channel, where the mask channel acts as a NON pre-multiplied alpha channel. What the mask texture actually does is multiply all four channels of the map with the mask channel, so what is passed up the pipeline is RGBA with pre-multiplied alpha.
Dithering and Filtering
When converting images with a palette of a greater number of colors to an image with a palette of fewer colors, dithering is a means of simulating colors not in the more limited palette by mixing different colored pixels together.
Dithering is also a method of smoothing the edges between two color regions by mixing their pixels so the edges appear to blend together.
In MAX, you have the option of setting dithering if you are rendering for the limited colors of an 8-bit display (256 colors). It can help prevent a banding effect in color gradients. Dithering does increase the size of 8-bit files and slows down the playback speed of animations.
3ds max is designed to render 64-bit color output. Consequently, you also have the option of setting dithering for true color (24 or 32-bit color). The Dither True Color option ensures that you get the best quality on true-color displays.
Users turn dithering on and off in the Rendering page of the Preferences dialog. Users can also set dithering for scene motion blur in Video Post. Here, dithering provides a smoothing effect between the separate images making up the "blur". Video Post dither is set as a percentage of total dither.
A developer can determine if a bitmap is dithered by calling Bitmap::IsDithered(). A developer can ask the system to dither an image at render time using the method Bitmap::SetDither().
Normally, a developer is not directly concerned with the dithering or filtering of bitmaps. These two operations are performed by the renderer, and the 3ds max user sets these characteristics of the bitmaps using the 3ds max user interface. A developer can however determine if a bitmap will be filtered by calling Bitmap::HasFilter(). A developer can also ask the system to filter an image when it's rendered. This is done using the method Bitmap::SetFilter(). See List of Bitmap Filter Types for more details on the types of filtering available. There is also a method of the Bitmap class called GetFiltered() to compute averaged colors over a specified area of the bitmap using the bitmap's current filtering algorithm.
Gamma Correction
This section contains an overview of the concept of gamma followed by a discussion of the methods available to developers in dealing with bitmaps and gamma correction.
Gamma correction compensates for the differences in color display on different output devices so that images look the same when viewed on different monitors.
A gamma value of 1.0 corresponds to an "ideal" monitor; that is, one that has a perfectly linear progression from white through gray to black. However, the ideal display device doesn't exist. Computer monitors are "nonlinear" devices. The higher the gamma value, the greater the degree of nonlinearity. The standard gamma value for NTSC video is 2.2. For computer monitors, gamma values in the range of 1.5 to 2.0 are common.
When you create an image on your computer, you base your color values and intensities on what you see on your monitor. Thus, when you save an image that looks perfect on your own monitor, you're compensating for the variance caused by the monitor gamma. The same image displayed on another monitor (or recorded to another media affected by gamma) will look different, depending on that media's gamma values.
Two basic procedures are required to compensate for changes in gamma:
1. Calibrate your output display devices so that the midtones generated by 3ds max are accurately duplicated on your display device. You do this in the Gamma panel of the Preferences dialog (Display Gamma).
2. Determine the gamma value to be applied to files output by the 3ds max renderer and files input into 3ds max, such as texture maps. This control is also in the Gamma panel of the Preferences dialog (Files Gamma).
The most important rule about gamma correction is to do it only once. If you do it twice, the image quality is over bright and loses color resolution.
With regard to output file gamma, video devices such as video tape recorders have their own hardware gamma-correction circuitry. Therefore, you need to decide whether to let 3ds max do the output gamma correction or to let the output device handle it.
Gamma correction is not required for hardcopy print media.
Files coming into 3ds max from programs such as Adobe Photoshop will have been gamma-corrected already. If you've been viewing the files on the same monitor and they look good, you won't need to set input file gamma in 3ds max.
A developer can indicate that a bitmap should have a custom gamma setting using the method BitmapInfo::SetGamma(). To retrieve the gamma setting stored with the bitmap use Bitmap::Gamma().
There are several ways to access pixel values in a bitmap. Some of these methods return gamma corrected pixels while other do not. Normally, a plug-in that access the pixels directly (for example an image filter plug-in that modifies the pixels) should use the method GetLinearPixels(). This method returns pixels that are NOT gamma corrected. The method GetPixels() is employed to access pixels that are gamma corrected.
Developers may also use the methods of Class GammaMgr to gamma correct colors.
Aspect Ratio
Aspect ratio is usually expressed either as a ratio of bitmap width over bitmap height (for example, 4:3) or as a real value relative to 1 (for example, 1.333).
Methods are available in the BitmapInfo class to get and set the aspect ratio property of the BitmapInfo (BitmapInfo::Aspect() and SetAspect()). Methods are available from the Bitmap and BitmapStorage classes to return the value of the BitmapInfo instance associated with the Bitmap or BitmapStorage.
3ds max also allows users to set the pixel aspect ratio, so that there is a different value for the distance covered by one pixel measured horizontally and one pixel measured vertically. A developer can check the pixel aspect ratio setting that is being used by the renderer using the method Interface::GetRendAspect().
Hot Check Utilities
There are functions that may be used to correct a pixel with RGB values that will give "unsafe" values of chrominance signal or composite signal amplitude when encoded into an NTSC or PAL color signal. This happens for certain high-intensity, high-saturation colors that are rare in real scenes, but can easily be present in computer generated images. See: List of Video Color Check Utilities.
G-Buffer Image Channels
Image Filter and Image Layer events in Video Post can use masks that are based on geometry/graphics buffer (G-Buffer) channels instead of the more widely used RGB and alpha channels. Also, some kinds of Filter and Layer events can post-process objects or materials designated by these channels. The 3ds max G-Buffer system allows developers to access additional data about rendered objects.
A G-buffer is used to store, at every pixel, information about the geometry at that pixel. All plug-ins in video post can request various components of the G-buffer. When video post calls the renderer it takes the sum of all the requests and asks the renderer to produce the G-buffer.
This allows a developer writing an image processing plug-in to locate parts of the image using a specific material, locate a specific node in the scene, and access UV coordinates, surface normals, and unclamped color values. This also allows the developer to access the Z (depth) buffer. In release 3.0 and later developers can access, color, transparency and weight for sub-pixel fragments as well as velocity (for motion blur).
A filter plug-in (derived from class ImageFilter) implements a method ChannelsRequired() to indicate what channels it needs. Then, at the time the filter's Render() method is called it will have access to these channels. See List of Image Channels for details on the available channels.
Error Reporting
When an image loader/saver plug-in (derived from class BitmapIO) encounters an error during a bitmap operation (for example "No Disk Space" when attempting to write a bitmap) there is a system flag that controls how the error should be reported to the user.
The image loader/saver base class BitmapIO provides a method for reporting errors named ProcessImageIOError(). This presents a standard dialog that provides the user with options to cancel or retry the operation. So, when a developer runs into an error condition while processing a bitmap, they would use the BitmapIO::ProcessImageIOError() method to report it. Some "common" error messages are already defined, and for those you would simply use one of the error codes defined in BITMAP.H. These are:
BMMRES_MEMORYERROR Generic memory error.
BMMRES_CANTSTORAGE Generic can't create storage error.
BMMRES_BADFRAME Generic Invalid Frame Number Requested.
More of these exist but are for internal use only. To send your own message, simply pass a TCHAR string instead. The file name and/or device name are taken from the given BitmapInfo object.
The ProcessImageIOError() returns either BMMRES_ERRORRETRY or BMMRES_ERRORTAKENCARE depending on the users selection from the dialog box. Normally a developer doesn't care about the return value and simply returns it and exits.
The idea of a standard error processing dialog is to control the display of dialogs. For example, when 3ds max is running in network rendering mode, no dialogs should be displayed. That would cause the machine to just sit there since there would be no user to respond to the dialog.
If you must handle the error yourself (that is, if you want to display your own error dialog), you should first check to see if dialogs are allowed by checking the BitmapManager::SilentMode() method. This method returns a value indicating if dialogs should indeed be displayed or not.
Utility Functions for Use with Bitmaps.
The following functions are general utility routines for dealing with bitmap files (but are not methods of a specific class):
Prototype:
BOOL BMMCreateNumberedFilename( const TCHAR *namein,DWORD frame, TCHAR *nameout );
Remarks:
Implemented by the System.
This appends a 4 digit frame number string to the end of the name passed. For example, this will convert bigfile.tga to bigfile0000.tga (or bigf0000.tga). This function checks the file system to see if it supports long file names and manages the length appropriately.
Parameters:
const TCHAR *namein
The input name to append the numbers to.
DWORD frame
The frame number to append.
TCHAR *nameout
The output string.
Return Value:
TRUE if the function succeeded; otherwise FALSE.
Prototype:
BOOL BMMGetFullFilename(BitmapInfo *bi);
Remarks:
This function will search the system for a bitmap. The BitmapInfo pointer contains the name of the bitmap that is searched for (bi->Name()). If the filename found in the BitmapInfo is incorrect, and the bitmap is found somewhere else, this function will replace bi->Name() with the correct path.
The order of the search is as follows:
- The full UNC path/filename saved in the BitmapInfo object.
- The path where the current 3ds max file was loaded from.
- The directory tree under the directory where the current Max files was loaded.
- The Map path.
Parameters:
BitmapInfo *bi
Describes the bitmap to find (using bi->Name()). This name is updated if the bitmap is found in a different location.
Return Value:
TRUE if the file was found; otherwise FALSE.
Prototype:
BOOL BMMIsFile(const TCHAR *filename);
Remarks:
Returns TRUE if the specified filename is indeed an existing file; otherwise FALSE.
Parameters:
const TCHAR *filename
The filename to check.
Prototype:
void BMMSplitFilename(const TCHAR *name, TCHAR *p, TCHAR *f, TCHAR *e);
Remarks:
This function will break the specified filename into path, file and extension components. *p, *f, and/or *e can be NULL. It is possible, for example, to call with just *e to collect just the file extension.
Parameters:
const TCHAR *name
The filename to split apart.
TCHAR *p
The path name is stored here.
TCHAR *f
The file name is stored here.
TCHAR *e
The file name extension is stored here. The name includes the period character (.).
Prototype:
void BMMAppendSlash(TCHAR *path);
Remarks:
This function appends a slash character to the end of the path passed unless one already exists.
Parameters:
TCHAR *path
The path name to append.
Prototype:
BOOL BMMGetUniversalName(TCHAR *out_uncname, const TCHAR* in_path, BOOL nolocal = FALSE);
Remarks:
This function is available in release 4.0 and later only.
Given a path (E:\path\filename.ext), the function will check and see if this drive is mapped to a network share. If successful, the full UNC version will be returned in out_uncname ("\\computer\share\path\file.ext"). If the function returns FALSE, out_uncname will be undefined.
This function has been enhanced to also return an UNC for a local drive that happens to be shared. For instance, if you pass in something like d:\data\images\maps\background\rottenredmond.tga and it happens that d:\data is shared as "Image Data", the function will return:
\\computername\Image Data\images\rottenredmond.tga.
Parameters:
TCHAR *out_uncname
This is a buffer you pass to it to receive the UNC path (if any). It must be at least MAX_PATH long.
const TCHAR *in_path
The path for which to obtain the UNC name.
BOOL nolocal = FALSE
Pass this as TRUE if you just want to see if this is a network share (don't check if this local drive is shared).
Return Value:
TRUE if successful, otherwise FALSE.
Prototype:
BOOL BMMFindNetworkShare(const TCHAR* in_localpath, TCHAR* out_sharename, TCHAR* out_sharepath);
Remarks:
This method is available in release 4.0 and later only.
Given a path (E:\path\filename.ext) this function will check and see if this [local] path is shared. If successful, it will return both the share name and the path of the share.
Parameters:
TCHAR *in_localpath
The local path provided.
TCHAR *out_sharename
The share name which is returned if the provided path is shared.
TCHAR *out_sharepath
The path of the share which is returned if the provided path is shared..
Return Value:
TRUE if successful, otherwise FALSE.
Prototype:
BOOL BMMGetLocalShare(const TCHAR *local_path, TCHAR *share);
Remarks:
This method is available in release 4.0 and later only.
This method represents the "second half" of BMMGetUniversalName() above. This method will check local paths only and return a UNC version if a share exists somewhere up in the path hierarchy.
Parameters:
const TCHAR *local_path
The local path provided.
TCHAR *share
The share name which is returned.
Return Value:
TRUE if successful, otherwise FALSE.
Prototype:
LPTSTR BMMGetLastErrorText(LPTSTR lpszBuf, DWORD dwSize);
Remarks:
This function is available in release 2.0 and later only.
Whenever you call a Win32 function and there is an error, this method may be used to return the descriptive string associated with the error.
Parameters:
LPTSTR lpszBuf
This is the string that is updated.
DWORD dwSize
The maximum length of the string that may be returned in lpszBuf.