Understanding the Duals
You may recall from the first article in this series (Platonic Solids Part 1: What are the Platonic solids?) that each platonic solid has a dual. You saw in the post Platonic Solids Part 5: Cube and octahedron that the cube and octahedron are duals of each other.
Similarly the icosahedron and dodecahedron are duals of each other.
To make a dual from a platonic solid, consider each of the solid’s vertices. Suppose faces A, B, C, … share a vertex in the original solid. Then place new vertices a, b, c, … in the middle of each of those faces and use them to define a new face in the dual. (To find the middle of a face, simply average the X, Y, and Z coordinates of the face’s vertices.)
Ideally this example program would make an icosahedron and a dodecahedron with edge lengths 1 centered at the origin. Unfortunately if you do that the icosahedron fits entirely inside the dodecahedron so you can’t see it. The problem is that using an edge length of 1 makes the icosahedron too small.
To fix this, we’ll define two values for each platonic solid. Let the inner radius be the distance from the center of the solid to the center of one of its faces. Let the outer radius be the distance from the center of the solid to its vertices. You can use a solid’s geometry to calculate the values or you can perform some simple calculations.
To find a solid’s outer radius, you can use the distance formula to find the distance from one of the vertices to the origin.
To find the inner radius you can average the coordinates of the vertices on a face to find the point in the center of the face. Then you can use the distance formula.
For a dodecahedron the values are:
For an icosahedron the values are:
Having calculated the solids’ radii, you can make the icosahedron stick out nicely as in the picture at the top of the post by scaling it so it has the same inner radius as the dodecahedron.
There’s one more change you need to make to get the final picture above. In my earlier posts, the icosahedron and dodecahedron were both arranged so key vertices lay along the X axis. That gave the solids orientations that did not show their duality. You can fix that by rotating one of the solids by 180 degrees.
The Example Program
There are a couple of approaches you can take to scale and rotate the icosahedron. First you can scale and rotate the vertices themselves before you use them to build the icosahedron’s face and edge models. Second you can build the models with the original vertices and then scale and rotate the models.
Transforming the Vertices
The following code shows how the program can transform the icosahedron’s vertices before it builds the models.
... Load the icosa_points array with the vertex coordinates ... // Scale and rotate the icosahedron's // vertices before making the models. // Rotate the icosahedron 180 degrees around the Y axis. Matrix3D transform = Matrix3D.Identity; Quaternion quaternion = new Quaternion( new Vector3D(0,1,0), 180); transform.RotateAt(quaternion, new Point3D(0, 0, 0)); // Scale the icosahedron to give it the // same inner radius as the dodecahedron. double icosa_scale = DodecaInnerRadius / IcosaInnerRadius; transform.ScaleAt( new Vector3D(icosa_scale, icosa_scale, icosa_scale), new Point3D(0, 0, 0)); // Transform the points. transform.Transform(icosa_points);
This code creates a Matrix3D object that represents the rotation followed by the scaling. (Actually in this example you could perform the operations in either order. Here you could perform the scaling before the rotation. That’s only true in this example because we’re transforming the vertices around the origin. In general the order of the transformations is not interchangeable.)
The code starts by creating a Rotation3D to represent the identity transformation. If you applied this initial matrix to the vertices, they would not be changed.
Next the code creates a quaternion to represent the rotation. Don’t worry about exactly what a quaternion is. (It’s sort of a point in four-dimensional space with X, Y, Z, and W components.) All you need to know is that one can represent a rotation’s axis of rotation and the angle of rotation. In this example, it represents rotation by 180 degrees around a vertical axis of rotation.
Note that the axis is defined as a vector so it has a direction but not a location. For example, a quaternion rotating by 90 degrees around a vertical axis is the same whether the axis passes through the point (0, 0, 0) or some other point such as (1, 2, 3).
Next the code passes the quaternion into the transformation matrix’s RotateAt method. It also specifies the point (0, 0, 0) so the rotation represents rotation around the Y axis.
The code then calculates the scale factor necessary to make the icosahedron have the same inner radius as the dodecahedron. It calls the transformation matrix’s ScaleAt method to append a scaling transformation to the matrix. Now the matrix represents the rotation followed by the scaling.
Finally the code calls the matrix’s Transform method, passing it icosahedron’s vertex array to make the matrix transform them.
After this point the vertices have been rotated and scaled so the program creates the icosahedron’s models as in the previous examples.
Transforming the Models
The second approach is to build the icosahedron’s face and edge models as in the previous examples and then apply rotation and scaling transformations to the models. With that approach, the models use the original calculated vertices and WPF transforms them when it needs to draw them.
The following code shows this approach.
// Scale and rotate the icosahedron's face and edge models. // Rotate the icosahedron 180 degrees around the Y axis. Rotation3D rotation = new AxisAngleRotation3D( new Vector3D(0, 1, 0), 180); RotateTransform3D rotate_transform = new RotateTransform3D(rotation, 0, 0, 0); // Scale the icosahedron to give it the // same inner radius as the dodecahedron. double icosa_scale = DodecaInnerRadius / IcosaInnerRadius; ScaleTransform3D scale_transform = new ScaleTransform3D( icosa_scale, icosa_scale, icosa_scale, 0, 0, 0); // Transform the models. Transform3DGroup transform_group = new Transform3DGroup(); transform_group.Children.Add(rotate_transform); transform_group.Children.Add(scale_transform); icosa_face_model.Transform = transform_group; icosa_edge_model.Transform = transform_group;
First the code creates a Rotation3D object that represents a rotation by 180 degrees around the vector <0, 1, 0>. (This basically encapsulates the quaternion used by the earlier approach.)
The code then uses the Rotation3D object to create a RotateTransform3D to rotate around the origin. (This corresponds to calling a transformation matrix’s RotateAt method, passing it the quaternion.)
Next the code calculates the necessary scale factor and uses it to create a ScaleTransform3D object to represent scaling by that factor in the X, Y, and Z directions, scaled relative to the origin.
Because a model can have only one transformation, the code next combines the rotation and scaling transformations into a single Transform3DGroup. It creates the group and adds the two transformations to its Children collection.
Finally the code sets the icosahedron’s face and edge models’ Transform properties to the group. Now WPF will automatically transform the vertices as needed.
The first approach is fairly intuitive (assuming you understand how quaternions and transformation matrices work). After you transform the vertices, they have their final values so you don’t need to worry about how WPF manipulates them. In this example, both the icosahedron’s face and edge models use the transformed vertices.
One nice feature of the second approach is that it leaves the original data unchanged so you can manipulate it if necessary. For example, suppose you’re drawing a graph and the vertices represent data points. You use a transformation to scale and translate the data so it looks nice on the screen. Now if you change a data value at run time, WPF automatically scales and translates the new value for you so the graph immediately updates.
Another nice feature of the second approach is that you can apply the same transformation to multiple models. This example uses the same transformation for the icosahedron’s face and edge models. If you modify the models’ transformations, WPF automatically updates the display. That makes this approach more flexible than the first approach, although it may be a bit less intuitive.
The example program includes code that uses both approaches with the first approach initially commented out. You can comment and uncomment the two approaches to see that they produce the same result. (Or comment them both out to see that the initial icosahedron lies completely inside the dodecahedron.)
For more details about how the program works, see the earlier examples or download this example and look at the code.