Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 962
Larry Pickles Avatar asked Jun 14 '15 08:06

Larry Pickles


3 Answers

Here's a quick demo using node hierarchy (to get the cylinder situated such that its end is at one point and its length is along the local z-axis) and a constraint (to make that z-axis look at another point).

let root = view.scene!.rootNode

// visualize a sphere
let sphere = SCNSphere(radius: 1)
sphere.firstMaterial?.transparency = 0.5
let sphereNode = SCNNode(geometry: sphere)
root.addChildNode(sphereNode)

// some dummy points opposite each other on the sphere
let rootOneThird = CGFloat(sqrt(1/3.0))
let p1 = SCNVector3(x: rootOneThird, y: rootOneThird, z: rootOneThird)
let p2 = SCNVector3(x: -rootOneThird, y: -rootOneThird, z: -rootOneThird)

// height of the cylinder should be the distance between points
let height = CGFloat(GLKVector3Distance(SCNVector3ToGLKVector3(p1), SCNVector3ToGLKVector3(p2)))

// add a container node for the cylinder to make its height run along the z axis
let zAlignNode = SCNNode()
zAlignNode.eulerAngles.x = CGFloat(M_PI_2)
// and position the zylinder so that one end is at the local origin
let cylinder = SCNNode(geometry: SCNCylinder(radius: 0.1, height: height))
cylinder.position.y = -height/2
zAlignNode.addChildNode(cylinder)

// put the container node in a positioning node at one of the points
p2Node.addChildNode(zAlignNode)
// and constrain the positioning node to face toward the other point
p2Node.constraints = [ SCNLookAtConstraint(target: p1Node) ]

Sorry if you were looking for an ObjC-specific solution, but it was quicker for me to prototype this in an OS X Swift playground. (Also, less CGFloat conversion is needed in iOS, because the element type of SCNVector3 is just Float there.)

like image 90
rickster Avatar answered Sep 22 '22 12:09

rickster


Just for reference a more elegant SCNCyclinder implementation to connect a start and end position with a given radius:

func makeCylinder(from: SCNVector3, to: SCNVector3, radius: CGFloat) -> SCNNode
{
    let lookAt = to - from
    let height = lookAt.length()

    let y = lookAt.normalized()
    let up = lookAt.cross(vector: to).normalized()
    let x = y.cross(vector: up).normalized()
    let z = x.cross(vector: y).normalized()
    let transform = SCNMatrix4(x: x, y: y, z: z, w: from)

    let geometry = SCNCylinder(radius: radius, 
                               height: CGFloat(height))
    let childNode = SCNNode(geometry: geometry)
    childNode.transform = SCNMatrix4MakeTranslation(0.0, height / 2.0, 0.0) * 
      transform

    return childNode
}

Needs the following extension:

extension SCNVector3 {
    /**
     * Calculates the cross product between two SCNVector3.
     */
    func cross(vector: SCNVector3) -> SCNVector3 {
        return SCNVector3Make(y * vector.z - z * vector.y, z * vector.x - x * vector.z, x * vector.y - y * vector.x)
    }

    func length() -> Float {
        return sqrtf(x*x + y*y + z*z)
    }

    /**
     * Normalizes the vector described by the SCNVector3 to length 1.0 and returns
     * the result as a new SCNVector3.
     */
    func normalized() -> SCNVector3 {
        return self / length()
    }
}

extension SCNMatrix4 {
    public init(x: SCNVector3, y: SCNVector3, z: SCNVector3, w: SCNVector3) {
        self.init(
            m11: x.x,
            m12: x.y,
            m13: x.z,
            m14: 0.0,

            m21: y.x,
            m22: y.y,
            m23: y.z,
            m24: 0.0,

            m31: z.x,
            m32: z.y,
            m33: z.z,
            m34: 0.0,

            m41: w.x,
            m42: w.y,
            m43: w.z,
            m44: 1.0)
    }
}

/**
 * Divides the x, y and z fields of a SCNVector3 by the same scalar value and
 * returns the result as a new SCNVector3.
 */
func / (vector: SCNVector3, scalar: Float) -> SCNVector3 {
    return SCNVector3Make(vector.x / scalar, vector.y / scalar, vector.z / scalar)
}

func * (left: SCNMatrix4, right: SCNMatrix4) -> SCNMatrix4 {
    return SCNMatrix4Mult(left, right)
}
like image 37
Christopher Oezbek Avatar answered Sep 22 '22 12:09

Christopher Oezbek


Thank you, Rickster! I have taken it a little further and made a class out of it:

class LineNode: SCNNode
{
    init( parent: SCNNode,     // because this node has not yet been assigned to a parent.
              v1: SCNVector3,  // where line starts
              v2: SCNVector3,  // where line ends
          radius: CGFloat,     // line thicknes
      radSegmentCount: Int,    // number of sides of the line
        material: [SCNMaterial] )  // any material.
    {
        super.init()
        let  height = v1.distance(v2)

        position = v1

        let ndV2 = SCNNode()

        ndV2.position = v2
        parent.addChildNode(ndV2)

        let ndZAlign = SCNNode()
        ndZAlign.eulerAngles.x = Float(M_PI_2)

        let cylgeo = SCNCylinder(radius: radius, height: CGFloat(height))
        cylgeo.radialSegmentCount = radSegmentCount
        cylgeo.materials = material

        let ndCylinder = SCNNode(geometry: cylgeo )
        ndCylinder.position.y = -height/2
        ndZAlign.addChildNode(ndCylinder)

        addChildNode(ndZAlign)

        constraints = [SCNLookAtConstraint(target: ndV2)]
    }

    override init() {
        super.init()
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
 }

I have tested this class successfully in an iOS app, using this function, which draws 100 lines (oops cylinders :o).

    func linesTest3()
    {
        let mat = SCNMaterial()
        mat.diffuse.contents  = UIColor.whiteColor()
        mat.specular.contents = UIColor.whiteColor()

        for _ in 1...100    // draw 100 lines (as cylinders) between random points.
        {
            let v1 =  SCNVector3( x: Float.random(min: -50, max: 50),
                                  y: Float.random(min: -50, max: 50),
                                  z: Float.random(min: -50, max: 50) )

            let v2 =  SCNVector3( x: Float.random(min: -50, max: 50),
                                  y: Float.random(min: -50, max: 50),
                                  z: Float.random(min: -50, max: 50) )

            // Just for testing, add two little spheres to check if lines are drawn correctly:
            // each line should run exactly from a green sphere to a red one:

            root.addChildNode(makeSphere(v1, radius: 0.5, color: UIColor.greenColor()))
            root.addChildNode(makeSphere(v2, radius: 0.5, color: UIColor.redColor()))

            // Have to pass the parentnode because 
            // it is not known during class instantiation of LineNode.

            let ndLine = LineNode(
                         parent: scene.rootNode, // ** needed
                             v1: v1,    // line (cylinder) starts here
                             v2: v2,    // line ends here
                         radius: 0.2,   // line thickness
                radSegmentCount: 6,     // hexagon tube
                       material: [mat] )  // any material

            root.addChildNode(ndLine)
        }
    }

100 random lines Regards. (btw. I can only see 3D objects.. I have never seen a "line" in my life :o)

like image 22
Ted van Gaalen Avatar answered Sep 21 '22 12:09

Ted van Gaalen