You can download this article's sample files from our Web site as part of the file oct98.zip. Go to www.zdjournals.com/ivb, then click the Source Code hyperlink.
Last month, in the first part of this two-part series, we introduced you to the basic concepts of three-dimensional drawing in Visual Basic. We began working on a 3D drawing project, laying the groundwork code, and writing a procedure to generate 3D lines.
This month, we'll expand our project to draw 3D spheres, cubes, and triangles, as well as filled wireframe shapes like that shown in Figure A. We'll build on the code we wrote last month, and we assume you've either downloaded or created the sample project.
Figure A: Nifty 3D images like this one will add sophistication to your applications.
Setup steps At present, the threedee form in the threedee project contains a picture box to display 3D drawings and a single command button. Let's begin by opening the project and adding two more buttons named filled and unfilled. Give these buttons the captions Filled Wireframe and Unfilled Wireframe. Now we'll get to work drawing 3D cubes and spheres. 3D cubes and spheres Your first new 3D shape is a wireframe cube. Our cube procedure, shown in Listing A, uses the ln3d procedure we presented last month to draw a rectangular prism. It takes minimum and maximum X, Y, and Z values as arguments, then draws the 12 three-dimensional lines required to make a cube. (As with last month's code, we've removed most comment lines to save space. However, the sample files contain comprehensive comments.)
Listing A: The cube procedure
Sub cube(ByVal xmin As Double, ByVal xmax As _
Double, ByVal ymin As Double, ByVal ymax As _
Double, ByVal zmin As Double, ByVal zmax As Double)
ln3d xmin, ymin, zmax, xmax, ymin, zmax 'back face
ln3d xmin, ymin, zmax, xmin, ymax, zmax
ln3d xmax, ymin, zmax, xmax, ymax, zmax
ln3d xmin, ymax, zmax, xmax, ymax, zmax
ln3d xmin, ymin, zmax, xmin, ymin, zmin 'side edges
ln3d xmax, ymin, zmax, xmax, ymin, zmin
ln3d xmin, ymax, zmax, xmin, ymax, zmin
ln3d xmax, ymax, zmax, xmax, ymax, zmin
ln3d xmin, ymin, zmin, xmax, ymin, zmin 'front face
ln3d xmin, ymin, zmin, xmin, ymax, zmin
ln3d xmax, ymin, zmin, xmax, ymax, zmin
ln3d xmin, ymax, zmin, xmax, ymax, zmin
End Sub
Next, you'll enter the code to create a sphere. Because this implementation
doesn't perform shading, representing a sphere is simply a matter of drawing a
circle with a translated center point and a perspective-corrected radius. Enter
into the frm3d form the sp3d subprocedure shown in Listing B, which
takes four arguments: the X, Y, and Z coordinate of its center, and its radius:
sp3d x, y, z, radius
Listing B: The sp3d subprocedure
Sub sp3d(ByVal x1 As Double, ByVal y1 As _
Double, ByVal z1 As Double, ByVal r As Double)
Dim xa As Double 'translated x
Dim ya As Double 'translated y
Dim xb As Double 'translated x for radius
Dim cr As Double 'corrected radius length
xa = xtrans(x1, z1)
ya = ytrans(y1, z1)
xb = xtrans(x1 + r, z1)
cr = xb - xa
pb.Circle (xa, ya), cr
End Sub
The procedure uses xtrans and ytrans we created last month to translate the 3D
center point to a 2D coordinate, then adds the radius to the X coordinate of
the center point to calculate a point on the right edge of the circle. The
difference between the two translated X coordinates is the
perspective-corrected radius.VB determines the circle's line width and color from the picture box's DrawWidth and ForeColor properties. Similarly, the fill style and color come from the picture box's FillStyle and FillColor properties.
Let's update the code in the Basic 3D drawing button to display 3D cubes and spheres as well as lines, as shown in Figure B. Add the code shown in color in Listing C at the beginning and end of the Basic 3D Drawing button's Click event.
Figure B: Our code generates these sample spheres, cubes, and lines.
Listing C: basic_click()
Private Sub basic_Click()
Dim lp As Double
Dim ra(1 To 8, 1 To 3) As Double
Dim lp2 As Integer
Dim x As Integer
Dim y As Integer
Dim z As Integer
.
.
.
For lp = 700 To 100 Step -100 'spheres
sp3d 0, 10, lp, 10
DoEvents
Next lp
pb.ForeColor = QBColor(12) `cubes
For lp = 0 To 900 Step 100
cube 140, 180, 100, 140, lp, lp + 40
DoEvents
Next lp
pb.ForeColor = RGB(0, 0, 0)
End Sub
3D filled triangles
For your final primitive shape, you'll create filled and unfilled 3D triangles.
But first, you must make a couple of additions to the existing code. Add the
following line to the global variables in the form's general declarations:
Dim spr As Double
This
variable provides a scale unit-to-pixel ratio for the triangle fill you'll
create later.
Now, add this line to the init3d procedure:
spr = (pbt - pbb) / pbhpx
This code calculates the ratio you just defined.
The code for the tr3d procedure appears in Listing D, along with the
code for simple min and max functions the procedure uses. The tr3d procedure
takes as arguments the X, Y, and Z coordinates of the three corners:
tr3d x1, y1, z1, x2, y2, z2, x3, y3, z3
Listing D: The tr3d procedure
Sub tr3d(ByVal x1 As Double, ByVal y1 As Double, _
ByVal z1 As Double, ByVal x2 As Double, _
ByVal y2 As Double, ByVal z2 As Double, _
ByVal x3 As Double, ByVal y3 As Double, _
ByVal z3 As Double)
Dim xa As Double 'translated coords for 3 points
Dim ya As Double
Dim xb As Double
Dim yb As Double
Dim xc As Double
Dim yc As Double
Dim cury As Double 'current y
Dim ymin As Double 'minimum y for triangle
Dim ymax As Double 'max y for triangle
Dim xmin As Double 'min x for scan line
Dim xmax As Double 'max x for scan line
Dim stp As Double 'y step value
Dim b As Boolean 'boolean flag for line comparison
Dim iy As Double 'scratch vars for line comparison
Dim ay As Double
Dim ix As Double
Dim ax As Double
Dim xtmp As Double 'x intercept on current line
'if dens > 1, scan line step will be <1 pixel
'if dens = .5 step will be 2 pixels, etc.
Const dens = 1.1
xa = xtrans(x1, z1)
ya = ytrans(y1, z1)
xb = xtrans(x2, z2)
yb = ytrans(y2, z2)
xc = xtrans(x3, z3)
yc = ytrans(y3, z3)
If pb.FillStyle = 1 Then 'not filled. _
draw edge lines and exit
pb.Line (xa, ya)-(xb, yb)
pb.Line (xa, ya)-(xc, yc)
pb.Line (xb, yb)-(xc, yc)
Exit Sub
End If
ymin = min(ya, yb) 'scan line fill. _
Horizontal lines only.
ymin = min(ymin, yc)
ymax = max(ya, yb)
ymax = max(ymax, yc)
If ymin < min(pbb, pbt) Then ymin = min(pbb, pbt)
If ymax > max(pbb, pbt) Then ymax = max(pbb, pbt)
stp = spr / dens
For cury = ymin To ymax Step stp _
'scan from top to bottom
b = False 'determine whether to assign or compare
iy = min(ya, yb) 'line1: a-b. Set min/max x and y.
If iy = ya Then
ay = yb
ix = xa
ax = xb
Else
ay = ya
ix = xb
ax = xa
End If
If (cury >= iy) And (cury <= ay) Then
xtmp = ix + ((ax - ix) * ((cury - iy) / _
(ay - iy)))
xmin = xtmp
xmax = xtmp
b = True
End If
iy = min(ya, yc)
If iy = ya Then
ay = yc
ix = xa
ax = xc
Else
ay = ya
ix = xc
ax = xa
End If
If (cury >= iy) And (cury <= ay) Then
xtmp = ix + ((ax - ix) * ((cury - iy) / (ay - iy)))
If b Then
xmin = min(xtmp, xmin)
xmax = max(xtmp, xmax)
Else
xmin = xtmp
xmax = xtmp
End If
b = True
End If
iy = min(yb, yc)
If iy = yb Then
ay = yc
ix = xb
ax = xc
Else
ay = yb
ix = xc
ax = xb
End If
'if in y range
If (cury >= iy) And (cury <= ay) Then
xtmp = ix + ((ax - ix) * ((cury - iy) / (ay - iy)))
If b Then
xmin = min(xtmp, xmin)
xmax = max(xtmp, xmax)
Else
xmin = xtmp
xmax = xtmp
End If
End If
If (xmin >= min(pbl, pbr)) Or _
(xmax <= max(pbl, pbr)) Then
pb.Line (xmin, cury)-(xmax, cury), pb.FillColor
End If
Next cury
'outlines, just in case. (catches rounding errors)
pb.Line (xa, ya)-(xb, yb), pb.FillColor
pb.Line (xa, ya)-(xc, yc), pb.FillColor
pb.Line (xb, yb)-(xc, yc), pb.FillColor
End Sub
Function min(ByVal a As Double, ByVal b As Double)
If a < b Then
min = a
Else
min = b
End If
End Function
Function max(ByVal a As Double, ByVal b As Double)
If a > b Then
max = a
Else
max = b
End If
End Function
This procedure will draw either filled or unfilled triangles. If the picture
box's FillStyle property is set to 1 (Transparent), the procedure
translates the three endpoints, draws the three lines, and exits. If
FillStyle is set to 0 (Solid) or any other value, the procedure draws a
filled triangle in the picture box's fill color.
The filled triangle code uses a scan-line fill to fill a 2D triangle using only horizontal lines. The code determines the minimum and maximum Y value of the three endpoints, then increments from one to the other in one-pixel increments. At least two of the lines will have intersection points on each pixel row. All three lines will have a point on a pixel row only when the triangle includes a horizontal line.
The code determines the intersection points, draws a horizontal line from the minimum X point to the maximum X point, then continues to the next pixel row. When the code reaches the last Y value, it will have filled the triangle using horizontal lines, with no unfilled pixels and no pixel filled more than once.
When developing this code, I noticed slight rounding errors that resulted in occasional unfilled pixels at the edge of the triangle. I added a few lines of code to draw the triangle edges, but be aware that the result still misses a single pixel once in a great while.
Beyond triangles Filled triangles are the basis for all polygon fills. You can break down a rectangle into two triangles; more complex shapes require more triangles. Our filled triangle procedure takes you out of the world of wireframe models and a little bit closer to 3D rendering. If you're careful to draw your filled shapes in reverse Z-order (drawing the farthest shape first), you can make your wireframe models opaque by performing hidden surface removal.
Figure A shows an example of a filled wireframe model. The lumps and swoops are the graph of the interaction of two sine waves, varying along the X and Z axes. Listing E contains the wireframe1 procedure that yields this result.
Listing E: The wireframe1 procedure
Sub wireframe1()
Dim x As Integer
Dim z As Integer
Const pi = 3.14159265
Const xmin = -400
Const xmax = 500
Const zmax = 800
Const zmin = 375
Const stp = 20 'grid size
Const fac1 = 250 'factor 1. sin rate of change
Const fac2 = 120 'factor 2. sin multiplier
Const fac3 = -170 'factor 3. grid y offset
pb.Cls
vpy = pbt 'set vanishing point to top of picture box
pb.FillColor = QBColor(11) 'set properties
pb.ForeColor = QBColor(0)
pb.DrawWidth = 1
For z = zmax To zmin Step (stp * -1) _
'draw from back to front
For x = xmin To xmax Step stp _
'draw from left to right
If (x < xmax) And (z > zmin) And _
(pb.FillStyle <> 1) Then
tr3d x, (Sin(x * pi / fac1) + Sin(z * pi / fac1)) _
* fac2 + fac3, z, x + stp, (Sin((x + stp) * pi / _
fac1) + Sin(z * pi / fac1)) * fac2 + fac3, z, x, _
(Sin(x * pi / fac1) + _
Sin((z - stp) * pi / fac1)) * fac2 + fac3, z - stp
tr3d x + stp, (Sin((x + stp) * pi / fac1) + _
Sin(z * pi / fac1)) * fac2 + fac3, z, x, _
(Sin(x * pi / fac1) + _
Sin((z - stp) * pi / fac1)) * fac2 + fac3, _
z - stp, x + stp, (Sin((x + stp) * pi / fac1) + _
Sin((z - stp) * pi / fac1)) * fac2 + fac3, z - stp
End If
If x < xmax Then
ln3d x, (Sin(x * pi / fac1) + Sin(z * pi / _
fac1)) * fac2 + fac3, z, x + stp, _
(Sin((x + stp) * pi / fac1) _
+ Sin(z * pi / fac1)) * fac2 + fac3, z
End If
If z > zmin Then
ln3d x, (Sin(x * pi / fac1) _
+ Sin(z * pi / fac1)) * fac2 _
+ fac3, z, x, (Sin(x * pi / fac1) _
+ Sin((z - stp) * pi / _
fac1)) * fac2 + fac3, z - stp
End If
DoEvents
Next x
Next z
End Sub
Miscellaneous functions
With the basic framework in place, it's easy to extend your 3D drawing
capabilities. For instance, the sample code available from our Web site
includes a simple extension. The horizon procedure takes "ground color" and
"sky color" as arguments. It clears the picture box, then colors everything
above the vanishing point's Y value "sky" and everything at or below the
vanishing point "ground." You'll soon find yourself developing similar tools.
Stones left unturned
Is there really such a thing as a vanishing point? No--if there were, all the
stars in the night sky would converge to a single bright point. A vanishing
point is just a convenient construction to use when implementing perspective,
and it works only when it lies beyond the farthest point you draw.
Also note that we didn't discuss color in these articles. Our sample code uses only the standard 16 colors available through the qbcolor function, but you can use any colors. However, you may get strange results on 16- or 256-color systems, especially with line widths of 1.
Conclusion After working through our examples, are you ready to write your own rendering engine? Probably not. But you're bound to think of many ways to use the techniques we've demonstrated to deliver 3D graphs and wireframes in your VB applications. We hope you'll use our examples as starting points to take your applications to the next visual level.
Copyright © 1998, ZD
Inc. All rights reserved. ZD Journals and the ZD Journals logo are trademarks of ZD
Inc. Reproduction in whole or in part in any form or medium without
express written permission of ZD Inc. is prohibited. All other product
names and logos are trademarks or registered trademarks of their
respective owners.