I've recently ported the hexasphere implementation by Rob Scanlon at https://github.com/arscan/hexasphere.js to objective-c for use in a project I'm exploring.

As a port, it works very well, and I can render the hexasphere using SceneKit on iOS however it's terribly inefficient. Because I want to dynamically animate individual facets of the hexasphere, I originally implemented each facet as a single SCNNode with it's own SCNGeometry object.

This produces a very slow rendering, with frame rates well below 30fps because of the high draw count.

Here is an image of the output after the original port:

enter image description here

With some advice from another developer, I've improved the rendering by creating a single SCNGeometry object that represents a single hexagon, and then using that single geometry multiple times, once for every hexagon in the hexasphere.

Whilst this brought down the draw count to an amazing 8 draws, it introduced a new problem.

By taking the multiple geometries generated by the original hexasphere code, each hexasphere was oriented in 3D so that they fit into the hexasphere appropriately.

Moving to the new design where I am positioning each SCNNode on the centrepoint of each hexagon, left me with a single geometry, correctly position in 3D, but incorrectly oriented, as this image shows:

Not Right

After some experimentation I worked out that if I use the latitude and longitude of each hexagon, these would appear to give the correct orientation in the X and Y axes.

Applying this, I got a much better result:

Almost there

But there is still a problem. The hexagons, whilst correctly positioned, are not oriented correctly on the face of the hexasphere.

So, finally, my question.

I'm trying to work out how to rotate those hexagons on the surface of the hexasphere, but I can't see how to. I had thought it was about rotating it on the Z axis, but that doesn't appear to work. I think I somehow need to calculate a set of 3 angles, 2 of which much be close to the latitude and longitude.

My current thought is that given that I have the boundary points of the original hexagon generated by the original code, I should be able to calculate (somehow) a 3D vector that represents the slope/orientation of the plane formed by those points, and use that to get the hexagons looking the way they are supposed to look.

Can someone help me get this right. Now I know I'm working with SceneKit and Objective-C, but I'm open to solutions in other languages. What I'm hoping is that I don't just get told to read a site with arcane maths because believe me, I've tried, and I just don't get it.

[UPDATE] I've completed my project now, and have made my Objective-C implementation of the hexasphere public on git at: https://github.com/pkclsoft/HexasphereDemo

  • 2
    There's going to be a limit to how far you can go with this method, because the hexagons that make up a Goldberg polyhedron are not all the same size & shape (they're irregular). Already in your image you can see large gaps between the tiles near the center of the view, and tighter bunching approaching the icosahedral vertices. Is this acceptable? Also, are you comfortable with using quaternions or rotation matrices to describe an orientation, rather than angles? This problem is a bit easier to address directly in these alternative forms. – DMGregory Jul 9 '16 at 11:40
  • Thanks @DMGregory, SCNNodes offer several mechanisms to adjust the orientation, etc and A quaternion is one of them. There are also transmformation matrices. I understand the concepts of angle far more, though I accept I need to learn about the other options. Regarding the separations, I can see what you mean, and I think I'm going to have to accept the limitations in order to keep the performance. – PKCLsoft Jul 9 '16 at 11:54
up vote 4 down vote accepted

As I mentioned above, there won't be a perfect solution here, because the hexagons that make up a Goldberg polyhedron tend to be irregular and non-congruent with each other. From one part of the sphere to another they distort in different ways. In general there's no rotation that will align a regular source hexagon with a given tile perfectly.

But, there are lots of ways to get a close match, particularly in areas where the distortion is low. I'll present one below, but I make no guarantees it's optimal. ;)

Here's an outline of the method:

Diagram of the steps below

First we want to identify the longest corner-to-corner axis of the hexagon - on the grounds that it likely dominates the perceived orientation of the tile.

Next we want to find the midpoint of this line. That's the position in space where we'll position our hex tile (which might be different from the coordinates of the geodesic vertex it came from), so from here on I'm going to call it the "center."

Now we need to form a basis for our orientation. We'll take the vector from the center to one of the corners on our long axis and call it "up." (The top blue arrow in the diagram above)

Next, we'll take the normal of the face (the green arrow sticking out of the hexagon in an attempt to show 3D above). I expect you can get this from the hexasphere code, but you can also normalize the offset from the center of the sphere to the face position to get something similar (depending on how hexasphere builds its polyhedra).

Take the cross product of the up and normal vectors to get a "right" vector perpendicular to both.

Now we can construct a transformation matrix something like this:

┌                            ┐
| right  up   normal  center | (Treating each named vector as a 3x1 column
|   0     0     0        1   |  so the matrix is actually 4x4)
└                            ┘

Depending on your matrix multiplication conventions, you may want the transpose of this, and you may need to negate one of the axes to get the handedness right for your coordinate system / winding. I haven't used SceneKit so I'm not familiar with its particular conventions.

If you scale all three vectors to the length of the "up" vector, you'll match the furthest corners but usually overshoot the rest, but you could use some fraction of this length to get a slightly inset hexagon face, which may help hide the inaccuracies.

If your setup supports non-uniform scale and skew, we can get a little closer:

Diagram of a variant, non-uniform method

Here we calculate the right vector a bit differently.

We take the offset from the center to the midpoint of each edge across from it. Subtract the left offset from the right and divide by two to get an "average" right vector.

To make this a drop-in replacement, we change our source hexagon so it's as wide as it is tall, so the side edges end up where we expect without a compensating magic number. ;)

Now we construct the transformation matrix the same way, but we don't normalize or rescale the lengths of the up or right vectors - their relative lengths describe how broad or skinny the hexagon should be.(You can still multiply them by a fraction if you want to inset the whole face proportionately though)

This still won't be perfect, but it may be close enough for the eye in many cases.

  • OK, I think I understand the gist of what you're saying though I'm still rough on how to implement it. I'll spend the next day or so experimenting. I am grateful though for the long and explanatory answer, especially the highlighting of the fact that the hexagons are not all the same. I had thought they were. Some tests today show that they most certainly aren't, so your second set of steps to stretch each node to improve the visualisation will be needed. I've found that some hexagons are large enough to cause even my so-far solution to look quite bad. – PKCLsoft Jul 10 '16 at 7:41
  • I think the thing I'm most unsure about right now is the construction of that matrix. So the other 3 rows are the xyz of the vectors that make up those points? – PKCLsoft Jul 10 '16 at 7:42
  • Yes, that's correct. There's more detail on transformation matrix structure here if you'd like to see it broken out further. – DMGregory Jul 10 '16 at 13:05
  • SceneKit uses right handed orientation, and column-major matrices. Having had a go at the first half (i.e. no scaling), I found I had to configure the matrix as: SCNMatrix4 transform = { right.x, right.y, right.z, 0, up.x, up.y, up.z, 0, normal.x, normal.y, normal.z, 0, center.x, center.y, center.z, 1 }; This places the hexagons in the right positions, however they are about an 8th of the size they need to be. – PKCLsoft Jul 10 '16 at 14:54
  • 1
    Assuming your source hexagon is sized so the top point is at (0, 1, 0) and the bottom is at (0, -1, 0), then scaling it by the length of "up" will turn that corner-to-corner distance into the length of the longest diagonal in the hex you're trying to match. You can get the center and normal vectors to match this scale by normalizing them then multiplying by the length of up. – DMGregory Jul 14 '16 at 14:33

Updated: to show my final result.

This answer is actually my implementation of the answer provided by @DMGregory above.

For the most part, I found that answer worked very well, so I thought it worthwhile to post some SceneKit code.

Before I do, here is an image showing what the answer provides as a result. It's definitely not perfect, and as @DMGregory states, it's not expected to be.

Pretty Darn Good

The (accepted) answer provided by @DMGregory above has two main components to it.

  • The first component deals with forming a transformation matrix that will position and orient each of the faces on the polyhedron as desired.
  • The second component deals with an attempt to scale each face of the polyhedron so that it closely resembles the original image in the question.

What I have, is a working first component, and now, a working second component.

First Component - position and orientation

The polyhedron in question involves 12 pentagons, and a computed number of hexagons. The answer above only really discusses hexagonal faces, however I found that I could apply the same rules to the 12 pentagons with one key point.

For each face I needed to determine the centroid, and the coordinate of the "top" vertice. I called this "top" vertice, the keyPoint of the face.

Four SCNVector3 objects are defined, center, up, normal and right. Given the above, I calculated these as follows:

SCNVector3 center = tile.centrePoint;
SCNVector3 up = SCNVector3FromGLKVector3(
                   GLKVector3Subtract(
                      SCNVector3ToGLKVector3(center),
                      SCNVector3ToGLKVector3(tile.keyPoint)));
SCNVector3 normal = SCNVector3FromGLKVector3(
                       SCNVector3ToGLKVector3(tile.centrePoint));
SCNVector3 right = SCNVector3FromGLKVector3(
                      GLKVector3CrossProduct(
                         SCNVector3ToGLKVector3(normal),
                         SCNVector3ToGLKVector3(up)));

Now calculate the length of up and scale both right and normal as directed.

float upLength = GLKVector3Length(SCNVector3ToGLKVector3(up));
right = SCNVector3FromGLKVector3(
           GLKVector3MultiplyScalar(
               GLKVector3Normalize(SCNVector3ToGLKVector3(right)), upLength));
normal = SCNVector3FromGLKVector3(
            GLKVector3MultiplyScalar(
               GLKVector3Normalize(
                  SCNVector3ToGLKVector3(normal)), upLength));

Noting that the hex.centrePoint has been calculated by the original hexasphere code, and that calculating the midpoint along the long axis of the hexagon gave essentially the same answer.

I then configured the transformation matrix as directed:

GLKMatrix4 transform =  {
  right.x, right.y, right.z, 0,
  up.x, up.y, up.z, 0,
  normal.x, normal.y, normal.z, 0,
  center.x, center.y, center.z, 1
};

node.transform = SCNMatrix4FromGLKMatrix4(transform);

This provided the following:

Scaled Once

There is still the second component of the scaling to be done. This looks promising though.

Second Component - Scaling

I then set about to implement the second component with the following modification to the above (for hexagonal faces only):

In addition to the keyPoint, I now needed both a midLeft and midRight which are the center points of the two "sides" of the hexagon.

I then applied the extra steps as directed to produce the average 'width' of the face:

GLKVector3 offsetLeft = GLKVector3Subtract(
                           SCNVector3ToGLKVector3(center), midLeft);

GLKVector3 offsetRight = GLKVector3Subtract(
                            SCNVector3ToGLKVector3(center), midRight);

SCNVector3 average = SCNVector3FromGLKVector3(
                        GLKVector3DivideScalar(
                           GLKVector3Subtract(offsetLeft, offsetRight), 2.0));

The matrix then became:

GLKMatrix4 transform =  {
  up.x, up.y, up.z, 0,
  average.x, average.y, average.z, 0,
  normal.x, normal.y, normal.z, 0,
  center.x, center.y, center.z, 1
};

When I first wrote up this answer, I had skipped a crucial step:

To make this a drop-in replacement, we change our source hexagon so it's as wide as it is tall, so the side edges end up where we expect without a compensating magic number. ;)

At the time it didn't make sense, but having gone back to the problem I realised my mistake, and with this done, the result is what I've now placed at the top of this answer (and again below). I suspect is as good as this solution is going to get. All told, the answer provided by @DMGregory is great; it just needed time for me to understand it.

Scaled Twice

Your Answer

 
discard

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.