The Interactive Renderer: GraphicsWindow
See Also: Class GraphicsWindow, Class Mesh, Class Light, Class HitRegion.
Overview
The display of geometric structures in 3ds max is handled through a class called GraphicsWindow. An instance of a GraphicsWindow sets up access to underlying drivers that allow 2D and 3D primitives to be rasterized and appear on-screen.
The methods in the GraphicsWindow class are designed to provide a fast, low-impedance pipeline to the underlying driver, and are optimized for mesh rendering.
If you wish to display a geometric object that is represented as a triangular mesh (consisting of a vertex list and a connectivity list), you should not use the GraphicsWindow methods directly. Rather, you should create a 3ds max Mesh, and then call that mesh's render() method. This assures that your mesh is rendered using the optimal path through the display subsystem.
A GraphicsWindow provides the following services:
Window Access Services
When the system creates an instance of a GraphicsWindow, the constructor opens a window for output and connects that window to an instance of a low-level rasterizer (driver). There are methods to resize and move the window, and to configure or change drivers.
Note: Since the driver has complete control over the window's palette (if one exists), each GraphicsWindow within 3ds max must be connected to the same driver.
The physical window consists of two parts: an image buffer (containing the colored pixels, as they appear on the screen) and a Z buffer. The Z buffer is used to record depth at each pixel as geometric primitives are rasterized.
Most drivers control two image buffers. One is displayed on the screen, and the other is used to rasterize geometric primitives. When rasterization of a complete frame is done, the off-screen buffer is blitted onto the display screen.
There are methods within the GraphicsWindow class that can be used to read/write the image and Z buffers.
Rendering Modes
A GraphicsWindow instance can be in any of several different rasterizing modes. The various rendering modes may be combined in a large number of ways. For example, it is possible to request Gouraud-shaded, z-buffered lines, or textured, flat-shaded triangles. (The actual output will depend on the capabilities of the underlying device driver.)
Each GraphicsWindow has a master mode (called the rendering limit) and a current mode. The current mode is always a "subset" of the master mode, in that any limits imposed by the master mode are forced onto the current mode.
For example, if the master mode restricts primitives to wireframe rendering, then setting the current mode to filled polygons will have no effect. On the other hand, if the master mode restricts rendering to flat shading, then the current mode can be set to wireframe to force polygons to render at the wireframe level.
In MAX, the rendering limits for each viewport can be set in the Rendering Method page of the Views / Viewport Configuration dialog box. Note that even when the level is set to "Smooth + Highlights", some primitives (for example lights and cameras) appear in wireframe. This is done by setting their private mode to wireframe.
The various rendering modes may be combined in a large number of ways. For example, it is possible to get Gouraud-shaded, z-buffered lines, or textured, flat-shaded triangles.
Coordinate Systems
There are two primary coordinate systems within a GraphicsWindow: device-independent 3D eye coordinates and device-dependent window coordinates.
Device-dependent coordinates are integer triples that have their origin at the lower-left corner of the window and increase toward the upper right. One unit corresponds to a single pixel. Z values closest to the user are 0, and they increase into the screen. The maximum Z value is driver-dependent. This coordinate system is left-handed.
Eye coordinates are floating point triples that correspond to relative distances from the camera (or "eye") associated with the window. In eye coordinates, x increases toward the right, y increases upward, and z decreases away (and in front of) the eye point. This coordinate system is right-handed.
There are two ways to set up eye coordinates. In the first, the camera is assumed to be sitting at the origin looking down the negative z axis. You specify camera parameters (perspective/orthographic, field-of-view, etc.) and then provide a transformation matrix for an object's position relative to the camera. This is most easily accomplished by concatenating the inverse affine transformation for the camera's position in world space with the (forward) affine transformation of the object in world space.
In the second method, a camera matrix is specified that includes both the world position (affine transformation) and projection information. This allows for a simple camera "look at" transformation model.
Note that the system stores two matrices: a camera matrix (which can be a pure projection), and an affine transformation matrix. When deriving device coordinates for a geometric primitive, each position is logically transformed through the affine transformation first, followed by the camera transformation. This allows each geometric primitive to be represented in its own local coordinates.
In addition to providing a method for transforming points from local, model coordinates to floating point device coordinates, a method is provided that transforms model coordinates into integer window coordinates with the origin at the upper left. These integer coordinates correspond to the coordinates used by GDI and the Windows mouse routines.
Both model-to-device coordinate methods return clipping values indicating whether the transformed point lies outside of the view volume. Flags are provided for each of the six planes bounding the viewing frustum.
Primitives
The geometric primitives supported by the GraphicsWindow class were designed to provide optimal support for rendering triangular meshes. As noted earlier, the preferred way to render such meshes is through the render() method of the Mesh class.
If you need to use the geometric primitives in the GraphicsWindow class directly, you should be aware that there is no direct support within GraphicsWindow for lit primitives, so rendering illuminated surfaces is a two-step process.
There are two levels of primitive support within the GraphicsWindow class: one set uses device-independent 3D eye coordinates and the other uses device-dependent window coordinates. At each level there are routines for polylines, polygons (triangles), markers, and annotation text.
The device-dependent routines do not provide any support for clipping or range testing! For speed, these routines assume that all coordinate values passed in are valid. If invalid values are present, the results are unpredictable, but a program crash or corruption of the 3ds max system is almost guaranteed.
The higher-level routines transform their coordinate lists through the current affine and camera transformations, clipping when necessary, and then call the corresponding low-level routines.
These routines are designed to optimize the rendering of meshes composed of vertex and connectivity lists. This process works as follows:
Each vertex is transformed using the model-to-device transformation routine, and the resulting device coordinates and clip flags are cached.
For each triangle in the connectivity list, the clip flags are examined to classify the triangle into one of the following three cases:
If all vertices lie outside the viewing frustrum, the triangle is trivially rejected.
If all vertices lie inside the viewing region, it is rasterized using the cached device coordinates and the low-level primitive routines.
If the triangle is clipped by the viewing region, its model coordinates are sent to the high-level routines where the triangle is appropriately clipped and rasterized.
Note that this approach only transforms and clip-checks most vertices once. (Only in the rare case that a primitive is clipped do the associated vertices have to be transformed again.) Since most vertices in a mesh are shared, this provides an efficient means to rasterize the entire mesh.
Materials and Lighting
As mentioned above, the geometric primitives in a GraphicsWindow do not directly support lighting. Rather, polylines and polygons can be provided with colors for each vertex. If provided (and the rendering limit allows for it), the device driver will interpolate the vertex colors as the primitive is rasterized.
To facilitate the lighting process, there are methods to specify light properties (type, color, angles, etc.), position and direction, and to specify material properties (ambient, diffuse, and specular colors, etc.) Once a material and a set of lights are specified, a given vertex / normal vector pair may be "rendered" by calling the lightVertex() method. This method uses the position and normal vector direction to produce an RGB triple representing the lit color.
Logically speaking, for lit primitives the pipeline described above is modified by having each vertex lit at the time it is transformed. The resulting RGB triples can then be cached and later used during rasterization of the primitive. In the mesh class, the actual renderer uses a lazy evaluation algorithm so that only vertices that may appear on-screen get lit.
Hit Testing
It is also possible to test each primitive for intersection or containment within a specified "hit region". The region may be a point (+/- epsilon), a rectangle, a circle, or a fence (an arbitrary polygon region).
Hit testing is done through a side-effect mechanism. When hit-testing is enabled, primitives set a flag if they pass through or are fully contained in the hit region. (When in this mode, the primitives do not cause any visual side-effects -- in particular, they do not change the image or Z buffers.)
By clearing the flag before a primitive is rendered, and checking it immediately afterwards, a hit-check for that particular primitive is made. Hit testing on vertices, edges, entire meshes, etc., can be accomplished by rendering only the desired primitives and checking the hit flag at the desired granularity.
When the hit region is a single point, each "hit" primitive also sets a distance variable (representing distance from the hit point to the line in wireframe mode, and Z distance in filled mode), so that "smart" hit testing -- i.e. choosing the closest primitive to the hit point -- may be implemented.