The texture images we've been using are specified with red, green and blue planes. The display also supports a fourth "alpha" plane which can be used to implement transparency. The DirectX (and OpenGL) libraries actually support all kinds of rules for combining a new texture with what's already on the screen, but probably the most common is just to linearly combine them, multiplying by the alpha value.
For example, if alpha were 0.0, you'd get all of the existing screen pixel, and none of the new texture. This would be used to mask out unwanted parts of the texture and make it completely transparent. At the other extreme, a value of 1.0 would take none of the existing screen pixel, and all of the new texture, making it opaque. Values in the middle would give you a translucent effect.
The demo has used transparency from the very beginning. As mentioned in the last part, we draw the cursor on the screen by texturing a rectangle with the cursor image. That image is mostly transparent, since only the cross or arrow portion of the cursor pattern should be drawn.
We also use transparency for drawing the sky. There is a texture image in the demo file docs/textures/starpatch.jpg (Figure 1) which I cropped from one of the NASA Astronomy Picture of the Day images.
We could just repeat this image over the entire sky, but you would easily see the pattern (the eye is really good at picking up things like that!) So instead I splatter it over the entire sky at random a few hundred times. The rotation is random as well, and for good measure, I flip the texture horizontally and vertically at random (Figure 2). This seems to break up any pattern and produces a very starry sky (Figure 3).
I'm actually using this image as the alpha plane, not the RGB planes. In my first demo, I used it for both. I thought it was a nuisance to create a mask around each star so that the correct RGB value would show through. Then I realized that using the same image for RGB and alpha is wrong anyway. If you have a 50% gray for a faint star, then use 50% for the alpha as well, you are actually getting 25% gray on the screen.
Instead, I should use pure white as the RGB, and let the 50% gray of the image in alpha turn that into 50% gray on the screen. Once I realized that, I actually used a white-ish multicolored texture as the RGB, so that the stars have slightly different colors.
The sun and moon images are also textures with an alpha plane. For those textures, I did create a mask in alpha (see Figure 4), although since they are monochrome images, I could have used the same trick as with the stars.
Back at the beginning of 3D graphics, everything in the scene had to be sorted before it was drawn. The most distant objects would be drawn first, and then the nearer objects drawn on top of them. You had to be very careful when objects intersected, since you could get ordering errors that way (more on this later.) When hardware became fast and cheap enough, another technique became common -- the Z buffer.
The Z buffer is a chunk of memory which backs up every pixel on the screen. As triangles are drawn by the display, it not only sets the pixels on the screen, it sets the value of the Z buffer at that point. The buffer is set to the distance from the eye of the point in the triangle it is rendering. What's more, the display tests the value that's already there against the pixel it's about to draw. If the new pixel is farther away, it doesn't draw it. That allows us to draw our triangles in any order, and not worry about intersections.
In Figure 5, the right side is the image we are drawing. The left is a single scanline, seen from above. The yellow indicates the values that have been written into the Z buffer as the cube was drawn. Now when the circle (a slice of the spherical head of the avatar) is drawn, every point is tested against what is already in the Z buffer. The points that are behind what is already written are skipped.
Aside from saving us a lot of work figuring out which triangles are in front, this also has another benefit. If the triangles can be drawn in any order, we can just save the entire list of triangles in display memory. Then as the eye moves and we see the scene from different angles, we can reuse the same list of triangles over and over. The display will rotate them and paint them, and the Z buffer will keep track of what is in front from any angle.
My demo code does not do this. I regenerate the list of triangles (cube faces, with two triangles per face) every display cycle. I even eliminate the backwards-facing triangles that would not be drawn. That's the way we would have done it back before there were dedicated processors in the display. As commenter Florian Bösch pointed out, it would be quicker to leave the backwards-facing triangles in, and let the display processor take care of it. With a complete list of triangles already in the display, I would not have to regenerate them at all, saving time.
Transparency and the Z Buffer
Unfortunately, when it comes to transparency, the Z buffer doesn't help us in all situations. That's because the display isn't really implementing a "transparent" image. Instead, it has a rule for combining a new triangle with the image already on the screen. By using the alpha plane to blend them, we get the appearance of transparency. But we cannot draw triangles in any order.
Consider Figure 6. The translucent red cube must be drawn after the avatar. That's because after the cube is drawn, all we have left on the screen is a pattern of red and green pixels. If we drew the avatar second, there would be no way to treat some of these pixels as translucent. So when we deal with translucent data, we are back to the old world of sorting our triangles from back to front.
Unfortunately, this isn't always as simple as it sounds. In Figure 7, we have the pathological case. This looks strange in two dimensions, but imagine the triangles in three dimensions, all tilted towards you, but overlapping from your viewpoint. There is no way to sort this set of triangles. Any order you pick will result in them being drawn incorrectly, since red is on top of blue which is on top of green which is on top of red...
To draw these correctly, we have to break them into fragments where they intersect, and then sort and draw the fragments in the right order. See Figure 8. This has to be done on each refresh of the scene, which is slow and a real nuisance to code.
There is another technique we can use for transparent textures that still uses the Z buffer and avoids sorting. This is called "Alpha Testing". With this test on, the display will draw only pixels that pass the test. If we set the test to be "alpha greater than zero", then transparent pixels will not be drawn. This means they will not write any points into the Z buffer, and will not block later pixels. It will be as if we drew lots of small opaque shapes, not a single transparent texture.
In Figure 9, you can see this done correctly. The name tags are either opaque or transparent. The transparent pixels are not drawn, and so these can be done in any order. The Z buffer will allow later graphics (like the avatar) to be drawn under the name tag.
Unfortunately, this requires that we do no alpha blending at all. If we want to anti-alias the text, or even if we use one of the nicer scaling modes for images, that will blend in background pixels from the screen. The result is shown in Figure 10.
Look along the edge of the "O" where it intersects the face of the avatar. The staircase of pixels running through the eye are parts of the background terrain which was on the screen when the name tag was drawn. Those were blended with the name tag pixels and all got the Z buffer value of the plane containing the name tag. When later the avatar was drawn, its pixels were regarded as "behind" the name tag pixels, and were not drawn.
Drawing Transparent Cubes
We have an advantage due to the nature of the world we're drawing. The cubes in the world cannot intersect, so we can draw the terrain without worrying about it. We draw all the opaque cubes first. Those can be kept in display memory to speed things up. Then we sort the transparent cubes from back to front and draw them.
As a side note, since our world is all cubes kept in an Octree, we don't actually have to sort them. Each node of the Octree has eight children. Based on where the eye point is relative to the center, there is an order that sorts them from back to front (see figure 11.) For example, everything in cube 4, including all of its children, is behind everything in cube 8 and its children.
There are only eight possible cases (the eye can be in any of the eight octants) and we can decide between them easily. By applying this test on each node of the tree as we traverse it, we can read out all the cubes from back to front.
Note that this only works because we are storing cubes in the tree. If the leaves of the tree had objects that extended outside the leaf cell, this sort would fail.
So I sorted my transparent cubes, drew them after the opaque cubes, and got the image in Figure 12 -- oops. If you remember from part 1, each cube face is marked with a flag indicating whether it is visible. A face that touches another cube is not visible and is not drawn. That means that the cubes under the water were all turned off... and so when the water becomes transparent, there's nothing underneath.
We can fix this by redefining the visibility rule. Now a cube face is visible if it's touching air, or a transparent cube. That rule gets you Figure 13. Oops again! Since the cubes of water are touching other transparent cubes (more water), their sides are all visible.
So we change the rule to make a side visible if it's touching a transparent cube of a different type (or air.) That gets you the image in Figure 14, which is what we want.
To test that everything was being drawn in the right order, I built some translucent cube faces and piled them up in front of each other. (see Figure 15.) At first, I thought these looked right, but then I realized that there are no backs or tops to the cubes. This is because I'm still eliminating cube faces that point away from the eye. This is an optimization when drawing opaque cubes, but it makes a difference when drawing transparent ones (see Figure 16.)
I'm not actually sure this is worth doing, or gives the right effect. If the cubes were a uniform color (without the white frames), there would be no difference. The two-sided faces would just cause the cube to be darker, since you'd be seeing the background through two layers. That's not worth the trouble. If there were a complex texture there (a pattern of leaves, for example), you'd see it twice -- once on front and once on the back -- which would look strange.
Minecraft does not draw water or glass with the back sides showing (Figure 17.) When it draws blocks of leaves to make up a tree, it looks like it also skips the back faces. However, I think he does turn on an interior face when two blocks of leaves touch, unlike with water or glass. In Figure 18, the block at center clearly has no top or far side, but does have another face painted behind it, from the next block. Presumably he does this to make the trees "leafier". If no interior faces were turned on (as with glass or water), trees would look like big balloons with leaves painted on them.
That's all for this part. In the next part, we need some more infrastructure. We need a GUI.
The new demo is at The Part 4 Demo. It has been tested on Windows 7, 32-bit and 64-bit, and Windows XP, 32-bit.
Since we now have a help screen, just hit F1 for help. Hit ESC to exit the demo.
We're not doing anything ambitious with the graphics here, so if you can run any 3D game, you should be able to run this. If you get an error message about needing "d3dx9_42.dll", you need to update your DirectX version. Go to Microsoft DirectX Update for a page that does that.
The Source Code
Download The Part 4 Source for the C++ code, a roadmap to the source, and a build directory. This includes the executable demo and the files it needs. If you download this, you don't need to also download the demo zip above.
If you want to compile the code, the project is built with Microsoft Visual C++ 2010 Express. You will also need the DirectX Software Development Kit. It's possible you'll need the Windows 7 Software Development Kit too. These are all free from Microsoft.
Unfortunately, all three of these downloads are huge. Hopefully, if you are interested in game development, you already have them or an equivalent development environment. If not, I hope you have a fast internet connection! Download and install them in that order - Visual C++, then the DirectX SDK, then the Windows SDK.
There wasn't much new code this part, just a lot of experimentation with graphics and a lot of debugging.
blog comments powered by Disqus