AViD is an Application for Visualizing Dendrimers. In a nutshell, it is designed to render a "Ball and Stick" model for dendrimers (highly branched molecules with regular repeating patterns). The intention behind it is to fill the need of displaying and communicating simplified models of these structures to others.
As a matter of fact, I'm not a chemist. By chance, my wife who is now nearing the end of her doctorate degree in Chemistry needed some help visualizing her molecules. This is where the software developer - me - came into the picture.
So, I set about trying to come up with an application that would suit her needs and, while I was at it, learn something new along the way.
It contains the following capabilities:
- Able to render dendrimers of the following core types:
- Linear - Straight core, bond angle 180°
- Trigonal - Three-point core, bond angle 120°
- Tetrahedral - Four-point core, bond angle 109.47°
- Able to grow up to six generations (levels of repeating patterns)
- A choice of 1-3 branch points (dendrons) per generation with appropriate bond angles
- 2D or 3D renderings
- Can show stepwise growth of generations
- Can freely rotate the dendrimer in 3D
- Can customize colors of elements
- Can truncate/hide branches
- Can produce image exports
- Can produce rotating animated GIF exports
- Can load/save dendrimers to the file system
- Can load files by drag/drop into the application
Code-Snippets Worthy of Reuse
- Converting mouse clicks to world space using a pick ray
- Capturing a rendered image from DirectX
- Updated/adapted version of a DirectX device settings form (see Ryan Cook reference below)
Useful Tid Bits for Future DirectX Developers
Since this was my first time to use DirectX, I dealt with several conceptual hurdles that for me were not directly obvious. But before I begin, some definitions:
- Device - The DirectX object that encapsulates all of the functionality for rendering using the DirectX libraries
- Camera - The point of view of the DirectX rendering for the client
- World space - The positioning system relative to the origin of the 3D world
- Object space - The positioning system relative to the origin of a 3D object
- Primitive object types - The basic objects to render in a 3D world on which all others are based (points, lines, and triangles)
- Meshes - A composition made up of primitive objects (namely, triangles) to represent a 3D object
- Ambient lighting - Lighting that exists in the entire world space that is direction-less
- Diffuse lighting - Lighting that exists in world space that has direction (either directional, point, or spot lights)
- Materials - The object that provides 3D surfaces with coloring information (ambient, diffuse, emissive, specular, and specular sharpness)
- Ambient color - The color applied to the surface when ambient lighting strikes the surface
- Diffuse color - The color applied to the surface when diffuse lighting strikes the surface
- Emissive color - The color applied to the surface even when no light strikes it (i.e., a glow effect)
- Specular color - The color applied to the surface as part of a shine from diffuse light striking it
- Specular dharpness - The level of the luster for the shine applied to the surface through the specular color (a higher number equals a shiny look, and a lower number equals a dull look)
- Pick rays - The vector that is translated from the client's screen to the 3D world with the intent of finding an object within it
Now, let's begin on some of the sticking points when working with DirectX. As a note, the examples I'm providing are purely for understanding the concepts. Many of the objects being used should be cached if possible, as they are expensive to produce.
Tid Bit #1 - Object Space vs. World Space
Initially, I believed that the difference between object space and world space was a formality to allow you to conceptually refer to something as either relative to the world's origin or the object's origin. In fact, it was far more important because of the structure of how objects are rendered in the DirectX world.
The primitive object types (points, lines, triangles) all inherently contain position information. So, when they are rendered, the world is expected to be centered back at the origin. This is done through the following:
device.Transform.World = Matrix.Identity;
After this, the primitive object can be rendered properly in the DirectX world. The
Transform property of the device is how we are able to move where an object is rendered in our 3D world.
For more complex objects (those that use meshes), a mesh does not contain any positional information. When you render these objects, they require you to use the world transform to shift the object to its proper location.
device.Transform.World = Matrix.Translation(new Vector3(12f, 14f, 8f));
Unfortunately, this doesn't solve all our issues. We will need the ability to rotate an object in space, and the very same world transform allows us to do so simply.
device.Transform.World = Matrix.RotationYawPitchRoll(0.1f, 0.5f, 0f)
* Matrix.Translation(new Vector3(12f, 14f, 8f));
Even the above, though, presents yet another problem, the rotation shown here will rotate about the object's origin. For many cases, the object's origin would not present the best point from which rotation would need to occur. Take for example, a cylinder. Rotating about the end is far different from rotating along a point in the middle of the length of the cylinder. So, for these cases, you must first translate the point of rotation, rotate, and then translate the rotated object to the correct position in world space.
device.Transform.World = Matrix.Translation(new Vector3(0f, 0f, 5f)
* Matrix.RotationYawPitchRoll(0.1f, 0.5f, 0f)
* Matrix.Translation(new Vector3(12f, 14f, 8f));
Tid Bit #2 - Meshes
As I was learning to develop in DirectX, I came across meshes as the defacto way of generating shapes. Unfortunately, because of my lack of knowledge within DirectX, I was unaware of the amount of overhead each mesh carries with it.
Because meshes carry only the blue-print of an object's shape, they are built to be reused. And should be. In a way, the separation of the various qualities of a 3D object (shape, location, material, etc.) actually allows for reuse because these components are not tied to each other. In the cases where it makes sense, they can quickly provide a performance boost to an application.
Tid Bit #3 - Producing a Pick Ray
Pick rays are important if you want to do any sort of interaction with your 3D world. In my case, I wanted the users to be able to select an object via a right mouse click. Capturing the mouse's click event, I could obtain the position of the mouse, but I still needed to translate that to my 3D world space. To do so, I used something similar to the following code:
int intMouseX, intMouseY;
Vector3 vecFar, vecPickRayPosition, vecPickRayDirection;
intMouseX = Math.Max(0, Math.Min(mouseLocation.X, this.m_device.Viewport.Width));
intMouseY = Math.Max(0, Math.Min(mouseLocation.Y, this.m_device.Viewport.Height));
vecNear = new Vector3(intMouseX, intMouseY, 0);
vecFar = new Vector3(intMouseX, intMouseY, 1);
vecPickRayPosition = vecNear;
vecPickRayDirection = Vector3.Subtract(vecFar, vecNear);
The near vector represents the origin of the pick ray, while the far vector provides the direction. You may have noticed that the Z component of the near and far vectors are 0 and 1, respectively. The reason for this is that this forces the near vector to the origin of the click (i.e., the camera position), and the far vector is forced to the farthest point in the world space upon unprojecting the two vectors.
Tid Bit #4 - Transforming Pick Rays
While meshes do contain a method called
Intersect which does most of the heavy lifting, it does not take into account how the mesh was rendered into the world. So, to overcome this, you must take the matrix used to translate the object into the world and invert it (i.e.,
Matrix.Invert(matrix)). With the matrix inverted, you apply it to the pick ray to orientate it correctly with the untransformed mesh.
bool blnIntersected = this.m_Mesh.Intersect(vecPosition, vecDirection);
In the above,
TransformCoordinate is used when you need to maintain the length of the resulting vector. For the direction where I don't care about the length, I used
TransformNormal, which maintains only the direction. After doing this, the mesh can now properly tell me whether or not the transformed pick ray did indeed intersect it.
Tid Bit #5 - Pick Rays and the Closest Object
Now that we are able to find if an object intersected with the pick ray, we have the issue of what if there are multiple objects. Because we are dealing with a 3D space, we certainly can have a situation where more than one object could fall within the pick ray. To overcome this, we must take into account the position of the pick ray and the object. Simply put, we find the closest object to the pick ray's origin:
fltTempFigureDistance = Vector3.Subtract(vecPosition, tempFigure.Position).Length();
if ((figure == null) || (fltTempFigureDistance < fltFigureDistance))
figure = tempFigure;
fltFigureDistance = fltTempFigureDistance;
Tid Bit #6 - Area Selection
I went through several iterations of algorithms to determine all of the figures within an area.
Attempt #1 - My first idea was to iterate through every pixel in the area selected and perform a pick ray. While it was easy to code and correct, it was horribly inefficient. For anything over a 40x40 pixel box, it would hang up the UI for more than a few seconds.
Attempt #2 - My next idea was to use the concept of collision detection to generate a 3D box of the region and find which figures intersected with this box. After toiling over whether or not I was going to use axis-aligned bounding boxes[^] or an oriented bounding box, I realized I still had a major hurtle looming over me. How do I pick the objects closest to the camera? Indeed, even if I were to find which figures were in the area, I could not easily tell which were visible.
The Solution - So, after some time, it struck me. I don't need to reinvent trying to figure out who's visible or not. The graphics engine already has done that for me. I just needed some way of determining which figures were in the area based on an image. It was here that my childhood days of paint by numbers struck me as a viable solution. Each figure would receive a unique color. Keeping one color in reserve for the background, I would be able to map all colors from 0xFF000001 to 0xFFFFFFFF (16,777,214 colors in all) to a specific figure. It should be noted that the colors will be so close that you can't determine their uniqueness based on the human eye. Below is an example of this technique:
Though, I did have to overcome one final hurdle. To improve the quality of the rendering, I have multisampling turned on in my settings. Unfortunately, this causes the rendering to 'blur' the edges with background causing the unique colors assumption to be invalidated. To disable this, I had to use the following code to on the fly force the rendering to not use multisampling.
this.m_device.RenderState.MultiSampleAntiAlias = false;
- Adapted DirectX settings control panel from Ryan Cook [^]
- NGif 184.108.40.206 - GIF animation code from gOODiDEA.NET [^]
- 220.127.116.11 - Released (March 1, 2009)
- Added ability to perform area selections
- Added ability to toggle on/off showing selected figures
- Added ability to toggle between rotation/selection of figures
- Added ability to print from within the form (plus a print preview)
- Selected figures now flash on all tabs and is controlled by the toggle mentioned above
- 18.104.22.168 - Released (February 22, 2009)
- Fixed issue with background color not loading properly
- 22.214.171.124 - Released (February 22, 2009)
- Fixed issue with image exports only saving in bitmap format
- Added ability to change the background color (on the Customization tab)
- Added ability to use control/shift to select/deselect items on the Customization tab
- Increased the rate of flashing for selected items to make it more visible
- 126.96.36.199 - First release (February 7, 2009)