Orientation of the cylinder between two points on a sphere, Scenekit, Quaternions iOS

I tried to draw a cylinder between two points on the outer edge of a sphere using SceneKit. I already created a line between these two points using primitive geometry and openGL with the SCNRendering delegate, but now I need to create a cylinder between these two (well, not just two, but any two 3D vectors that sit on the surface of the sphere). I have been working on this for about 3 days right now, and I went through everything I could find when implementing Quaternions to make this happen, but since it is, I can't get it to work. Academic articles, scientific research and nothing, nothing works to rebuild a cylinder between two fixed points. I need an algorithm for this.

In any case, here is my latest code that does not work, but this is just a small fragment of almost 2k lines of code that I have worked with so far without the expected result. I know that I can move on to something more advanced, for example, create my own SCNProgram and / or SCNRenderer, so that I can then access the complexities of GLSL, OpenGL and Metal, but this looks like it should be possible with Scenekit and conversion between the GLKit vector structures in the SCNVector and from structures from it, but so far this is not possible:

code:

The following code processes the coordinates of longitude and latitude and projects them onto the surface of a 3D sphere. These coordinates are returned through the proprietary function that I built, where I got the SCNVector3 coordinates {x, y, z}, which are accurately displayed on my 3D sphere. I draw a line between two sets of longitude and latitude coordinates, where the lines that are drawn using primitives make their way through the center of the sphere. So, as I mentioned above, I need the same functionality, but with cylinders and not with lines (by the way, the longitude and latitude coordinates listed here are fictitious, they are randomly generated, but both fall to the surface of the Earth).

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)); qw += GLKQuaternionLength(q); q = GLKQuaternionNormalize(q); SCNVector4 final = SCNVector4FromGLKVector4(GLKVector4Make(qx, qy, qz, qw)); test.orientation = final; 

Another code I tried includes the same method, in fact, I even built my own math libraries SCNVector3 and SCNVector4 in Objective-C to see if my math methods produce different values โ€‹โ€‹than using math GLKit, but I I get the same results with both methods. Any help would be awesome, but for now I don't want to jump into something more complicated than SceneKit. I will not dive into Metal and / or OpenGL for another month or two. Thanks!

EDIT:

The variables "cooridnateSetOne" and "cooridnateSetTwo" are SCNNodes, which are created by another function that forces the primitive linear geometry in this node, and then returns it to the SCNScene subclass implementation.

+6
source share
5 answers

Here's the whole method using Objective-C

First, here is how you use it:

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

Inputs

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

The second method creates SCNVector3 points for each of the above locations:

 -(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 the Earth's radius and curvature. I show this to show the numbers needed for full 100 percent accuracy, this is how it works. Obviously, you will want to change this to the correct size for your scene, but here is the method. This is an adaptation of the methods used by http://www.gdal.org/index.html . An explanation can be found here: http://www.gdal.org/osr_tutorial.html . I put it 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); } 
+2
source

Here's a demo using the node hierarchy (to get the cylinder so that its end is at one point and its length is along the local z axis) and the constraint (to get this z axis to 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 a solution specific to ObjC, but for me it was faster to prototype it on the OS X Swift playground. (Also, on iOS, less CGFloat conversion is CGFloat , because the element type SCNVector3 is just Float there.)

+8
source

Thank Rickster I took 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 successfully tested this class in an iOS application 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 Sincerely. (By the way, I can only see 3D objects .. I have never seen a โ€œlineโ€ in my life: o)

+4
source

For reference only, a more elegant implementation of SCNCyclinder for connecting the start and end positions with a given radius:

 func makeCylinder(from: SCNVector3, to: SCNVector3, radius: CGFloat, color: NSColor) -> 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: CGFloat(self.radius), height: CGFloat(height)) let childNode = SCNNode(geometry: geometry) childNode.transform = SCNMatrix4MakeTranslation(0.0, height / 2.0, 0.0) * transform return result } 

The following extension is required:

 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: xx, m12: xy, m13: xz, m14: 0.0, m21: yx, m22: yy, m23: yz, m24: 0.0, m31: zx, m32: zy, m33: zz, m34: 0.0, m41: wx, m42: wy, m43: wz, 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) } 
+2
source

I was looking for a solution to make a cylinder between two points and thanks to rickster , I used his answer to make the SCNNode extension. There I added the missing conditions for the possible orientation of the cylinder in order to avoid its wrong opposite direction.

 func makeCylinder(positionStart: SCNVector3, positionEnd: SCNVector3, radius: CGFloat , color: NSColor, transparency: CGFloat) -> SCNNode { let height = CGFloat(GLKVector3Distance(SCNVector3ToGLKVector3(positionStart), SCNVector3ToGLKVector3(positionEnd))) let startNode = SCNNode() let endNode = SCNNode() startNode.position = positionStart endNode.position = positionEnd let zAxisNode = SCNNode() zAxisNode.eulerAngles.x = CGFloat(M_PI_2) let cylinderGeometry = SCNCylinder(radius: radius, height: height) cylinderGeometry.firstMaterial?.diffuse.contents = color let cylinder = SCNNode(geometry: cylinderGeometry) cylinder.position.y = -height/2 zAxisNode.addChildNode(cylinder) let returnNode = SCNNode() if (positionStart.x > 0.0 && positionStart.y < 0.0 && positionStart.z < 0.0 && positionEnd.x > 0.0 && positionEnd.y < 0.0 && positionEnd.z > 0.0) { endNode.addChildNode(zAxisNode) endNode.constraints = [ SCNLookAtConstraint(target: startNode) ] returnNode.addChildNode(endNode) } else if (positionStart.x < 0.0 && positionStart.y < 0.0 && positionStart.z < 0.0 && positionEnd.x < 0.0 && positionEnd.y < 0.0 && positionEnd.z > 0.0) { endNode.addChildNode(zAxisNode) endNode.constraints = [ SCNLookAtConstraint(target: startNode) ] returnNode.addChildNode(endNode) } else if (positionStart.x < 0.0 && positionStart.y > 0.0 && positionStart.z < 0.0 && positionEnd.x < 0.0 && positionEnd.y > 0.0 && positionEnd.z > 0.0) { endNode.addChildNode(zAxisNode) endNode.constraints = [ SCNLookAtConstraint(target: startNode) ] returnNode.addChildNode(endNode) } else if (positionStart.x > 0.0 && positionStart.y > 0.0 && positionStart.z < 0.0 && positionEnd.x > 0.0 && positionEnd.y > 0.0 && positionEnd.z > 0.0) { endNode.addChildNode(zAxisNode) endNode.constraints = [ SCNLookAtConstraint(target: startNode) ] returnNode.addChildNode(endNode) } else { startNode.addChildNode(zAxisNode) startNode.constraints = [ SCNLookAtConstraint(target: endNode) ] returnNode.addChildNode(startNode) } return returnNode } 
0
source

Source: https://habr.com/ru/post/989082/


All Articles