Search code examples
iosobjective-copengl-es3dscenekit

Cylinder Orientation between two points on a sphere, Scenekit, Quaternions IOS


I've been trying to draw a cylinder between two points on the outer edge of a sphere using SceneKit. I have already produced a line between these two points using primitive geometry and openGL with SCNRendering Delegate, but now I need to produce a cylinder between these two (well, not just two, but any two 3D vectors that sit on the surface of the sphere). I've been working on this for about 3 days straight now, and I've gone through everything I could find on implementing Quaternions to make this happen, but as it stands, I can't get it to work. Academic articles, scientific studies, and nothing, nothing is working to realign a cylinder between two fixed points. I need an algorithm to do this.

Anyway, here's my most recent code that doesn't work, but this is just a small snippet of nearly 2k lines of code I've worked through so far without the intended result. I know I can move to something more advanced like building my own SCNProgram and/or SCNRenderer to then access GLSL, OpenGL, and Metal complexity, but this seems like something that should be possible using Scenekit and converting between GLKit vector structs to and from SCNVector structs, but so far it's impossible:

Code:

The following code ingests Longitude and Latitude coordinates and projects them onto the surface of a 3D sphere. These coordinates are returned through a proprietary function I build where I received a SCNVector3 of {x,y,z} coordinates that display accurately on my 3D sphere. I draw a line between two sets of Longitude and Latitude coordinates where the lines that are drawn using primitives shoot through the center of the sphere. So, as I mentioned above, I want this same functionality but with cylinders, not lines (by the way, the longitude and latitude coordinates listed here are bogus, they are randomly generated but both fall on the Earth's surface).

drawLine = [self lat1:37.76830 lon1:-30.40096 height1:tall lat2:3.97620 lon2:63.73095 height2:tall];

float cylHeight = GLKVector3Distance(SCNVector3ToGLKVector3(cooridnateSetOne.position), SCNVector3ToGLKVector3(coordinateSetTwo.position));

SCNCylinder * cylTest = [SCNCylinder cylinderWithRadius:0.2 height:cylHeight];
SCNNode * test = [SCNNode nodeWithGeometry:cylTest];

SCNMaterial *material = [SCNMaterial material];
[[material diffuse] setContents:[SKColor whiteColor]];
material.diffuse.intensity = 60;
material.emission.contents = [SKColor whiteColor];

material.lightingModelName = SCNLightingModelConstant;
[cylTest setMaterials:@[material]];

GLKVector3 u = SCNVector3ToGLKVector3(cooridnateSetOne.position);
GLKVector3 v = SCNVector3ToGLKVector3(cooridnateSetTwo.position);

GLKVector3 w = GLKVector3CrossProduct(u, v);

GLKQuaternion q = GLKQuaternionMakeWithAngleAndVector3Axis(GLKVector3DotProduct(u,v), GLKVector3Normalize(w));
q.w += GLKQuaternionLength(q);
q = GLKQuaternionNormalize(q);
SCNVector4 final = SCNVector4FromGLKVector4(GLKVector4Make(q.x, q.y, q.z, q.w));

test.orientation = final;

Other code I've tried includes this same sort of method, in fact, I even built my own SCNVector3 and SCNVector4 Math libraries in Objective-C to see if my math methods produced different values than using GLKit maths, but I get the same results with both methods. Any help would be awesome, but for now, I'm not looking to jump into anything more complicated than SceneKit. I won't be diving into Metal and/or OpenGL for another month or two. Thanks!

EDIT:

The variables "cooridnateSetOne" and "cooridnateSetTwo" are SCNNodes that are produced by another function that forces a primitive line geometry into this node and then returns it to a subclass implementation of SCNScene.


Solution

  • Here's an entire method using Objective-C

    First, here's how you use it:

    SCNNode * testNode = [self lat1:-35 lon1:108 height1:tall lat2:-35 lon2:30 height2:0];
    

    Inputs:

    1rst location lat1 = latitude of 1rst location lon1 = longitude of 1rst location height1 = distance from earth for 1rst location lat2 = latitude of 2nd location lon2 = latitude of 2nd location height2 = distance from earth for 2nd location

    The second method creates the SCNVector3 points for each location in question above:

    -(SCNNode *)lat1:(double)lat1 lon1:(double)lon1 height1:(float)height1 lat2:(double)lat2 lon2:(double)lon2 height2:(float)height2 {
        SCNVector3 positions[] = {[self lat:lat1 lon:lon1 height:height1], [self lat:lat2 lon:lon2 height:height2]};
        
        float cylHeight = GLKVector3Distance(SCNVector3ToGLKVector3(positions[0]), SCNVector3ToGLKVector3(positions[1]))/4;
        
        SCNCylinder * masterCylinderNode = [SCNCylinder cylinderWithRadius:0.05 height:cylHeight];
    
        SCNMaterial *material = [SCNMaterial material];
        [[material diffuse] setContents:[SKColor whiteColor]];
        material.lightingModelName = SCNLightingModelConstant;
        material.emission.contents = [SKColor whiteColor];
        [masterCylinderNode setMaterials:@[material]];
    
        SCNNode *mainLocationPointNodeTestA = [mainLocationPointNode clone];
        SCNNode *mainLocationPointNodeTestB = [mainLocationPointNode clone];
        
        mainLocationPointNodeTestA.position = positions[0];
        mainLocationPointNodeTestB.position = positions[1];
        
        SCNNode * mainParentNode = [SCNNode node];
        SCNNode * tempNode2 =[SCNNode nodeWithGeometry:masterCylinderNode];
        
        [mainParentNode addChildNode:mainLocationPointNodeTestA];
        [mainParentNode addChildNode:mainLocationPointNodeTestB];
        [mainParentNode addChildNode:tempNode2];
        
        [mainParentNode setName:@"parentToLineNode"];
        
        tempNode2.position = SCNVector3Make((positions[0].x+positions[1].x)/2, (positions[0].y+positions[1].y)/2, (positions[0].z+positions[1].z)/2);
        tempNode2.pivot = SCNMatrix4MakeTranslation(0, cylHeight*1.5, 0);
        
        GLKVector3 normalizedVectorStartingPosition = GLKVector3Make(0.0, 1.0, 0.0);
        GLKVector3 magicAxis = GLKVector3Normalize(GLKVector3Subtract(GLKVector3Make(positions[0].x/2, positions[0].y/2, positions[0].z/2), GLKVector3Make(positions[1].x/2, positions[1].y/2, positions[1].z/2)));
        
        GLKVector3 rotationAxis = GLKVector3CrossProduct(normalizedVectorStartingPosition, magicAxis);
        CGFloat rotationAngle = GLKVector3DotProduct(normalizedVectorStartingPosition, magicAxis);
        
        GLKVector4 rotation = GLKVector4MakeWithVector3(rotationAxis, acos(rotationAngle));
        tempNode2.rotation = SCNVector4FromGLKVector4(rotation);
        
        return mainParentNode;
    }
    

    This second method uses hard coded numbers for earth's radius and curvature, I'm showing this just to show the numbers required for total 100% accuracy, this is how it works. You'll want to change this to the correct dimensions for your scene, obviously, but here's the method. This is an adaptation of methods used by Link. An explanation an be found here: Link. I put this together very quickly but it works and is accurate, feel free to change the number formats to your liking.

    -(SCNVector3)lat:(double)lat lon:(double)lon height:(float)height {
        double latd = 0.0174532925;
        double latitude = latd*lat;
        double longitude = latd*lon;
        
        Float64 rad = (Float64)(6378137.0);
        Float64 f = (Float64)(1.0/298.257223563);
        
        double cosLat = cos(latitude);
        
        double sinLat = sin(latitude);
        
        double FF = pow((1.0-f), 2);
        double C = 1/(sqrt(pow(cosLat,2) + FF * pow(sinLat,2)));
        double S = C * FF;
        
        double x = ((rad * C)*cosLat * cos(longitude))/(1000000/(1+height));
        double y = ((rad * C)*cosLat * sin(longitude))/(1000000/(1+height));
        double z = ((rad * S)*sinLat)/(1000000/(1+height));
        
        return SCNVector3Make(y+globeNode.position.x, z+globeNode.position.y, x+globeNode.position.z);
    }