ARKit Convert 3d object position to UIView coordinates - ios

I have created measure demo which allow to put multiple points and show distance between them. which works fine
I want to show preview that what so far has been drawn in real world to the UIView using UIBezierPath . Just like http://armeasure.com/
I have tried many things to achieve this but I couldn't find any right way to do it.
if self.linkList.count == 1 {
bezierPath.removeAllPoints()
bezierPath.move(to: CGPoint(x: 10,y: 10))
} else {
guard self.linkList.count > 1 ,let object2 = self.linkList.lastNode, let object1 = self.linkList.lastNode?.previous else {return}
let value = self.getMeasurementXandYBetween(vector1: object1.node.mainNode.position, and: object2.node.mainNode.position)
print(value)
let x = Double((object1.node.mainNode.position.x + value ) * 377.9527559055 )
let y = Double((object1.node.mainNode.position.y + value) * 377.9527559055)
let pointCoordinates = CGPoint(x: x , y: y)
print("x : Y ",x,y)
bezierPath.addLine(to: pointCoordinates)
}
shapeLayer.removeFromSuperlayer()
shapeLayer.path = bezierPath.cgPath
shapeLayer.lineWidth = 0.5
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.position = CGPoint(x: 0, y: 0)
self.viewToDraw.layer.addSublayer(shapeLayer)
func getMeasurementXandYBetween(vector1:SCNVector3, and vector2:SCNVector3) -> Float {
return sqrtf((vector1.x - vector2.x) * (vector1.x - vector2.x) + (vector1.y - vector2.y) * (vector1.y - vector2.y))
}
The logic I used (which is not working is) Location of previous node + distance I got from getMeasurementXandYBetween multiply by 377.
Please suggest a hint or any other solution

You can get the coordinates of any point in screen-space by using the projectPoint() on your SCNSceneRenderer.
This will give you a vector with 3 elements, build your CGPoint using the first two and build your shape from those points.

Related

How to restrict Drag/Pan of a View between 2 CGPoints (Like a Slider)

I have 2 independent set of data, one is used to draw a line(a Path), and the other is used to put a small view somewhere on the line(Circle).
You can imagine it just like a Slider(in fact the UX should be something like that)
I wanted to add a Drag Gesture on the Red Circle View, so that the user can slide it any where between the line end points. But the way i've implemented or thought it to be isn't working at all or is pretty bad.
The know set of data is:
Start of the Line - CGPoint
End of the Line - CGPoint
Position of the Circle View - CGPoint
For simplicity of the example, i've added some simple points namely Top & Bottom, but in reality, what i'm trying to achieve is, establish a perimeter on a Map Layer, by fetching the Lat/Long Coordinates, converting it to Screen Coordinates space and plotting a Path(line) for those 2 points and thereafter giving the ability to the user to label the Perimeter and giving the freedom to drag it along the perimeter line as per the user's need. (One label per line, no complications on that).
That being said, Slider wasn't an option.
Which looks something like:
Sample Code:
struct TestView: View {
#State var position = CGPoint(x: 60, y: 60)
let top = CGPoint(x: 50, y: 50)
let bottom = CGPoint(x: 300, y: 300)
var body: some View {
ZStack {
Path { path in
path.move(to: self.top)
path.addLine(to: self.bottom)
}
.stroke(Color.red, lineWidth: 5)
Circle()
.foregroundColor(.red)
.frame(width: 20)
.position(self.position)
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged { drag in
print(drag.location)
if
self.top.x <= drag.location.x,
self.bottom.x >= drag.location.x,
self.top.y <= drag.location.y,
self.bottom.y >= drag.location.y,
self.pointOnLine(point: drag.location)
{
self.position = drag.location
}
}
)
}
}
}
Helper Method to check if point is on line:
func pointOnLine(point: CGPoint) -> Bool {
let dxc = point.x - top.x
let dyc = point.y - top.y
let dxl = bottom.x - top.x
let dyl = bottom.y - top.y
let cross = dxc * dyl - dyc * dxl
return cross == 0
}
Any help appreciated. Thanks in advance.
Got it Working
Updated the helper method to get the closest intersection point of drag location and a point on line.
Assigned in to self.position which keeps the View sticking to the line.
Gesture:
DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged { drag in
self.position = self.pointFrom(drag.location,
toLineSegment: self.top, self.bottom)
}
Helper Method to get Point on Line:
private func pointFrom(_ point: CGPoint, toLineSegment start: CGPoint, _ end: CGPoint) -> CGPoint {
let pointAndStartXDiff = point.x - start.x
let pointAndStartYDiff = point.y - start.y
let startAndEndXDiff = end.x - start.x
let startAndEndYDiff = end.y - start.y
let dotProduct = pointAndStartXDiff * startAndEndXDiff + pointAndStartYDiff * startAndEndYDiff
let lengthSquare = startAndEndXDiff * startAndEndXDiff + startAndEndYDiff * startAndEndYDiff
let param = dotProduct / lengthSquare
// intersection of normal to start, end that goes through point
var xIntersection, yIntersection: CGFloat
if param < 0 || (start.x == end.x && start.y == end.y) {
xIntersection = start.x
yIntersection = start.y
} else if param > 1 {
xIntersection = end.x
yIntersection = end.y
} else {
xIntersection = start.x + param * startAndEndXDiff
yIntersection = start.y + param * startAndEndYDiff
}
return CGPoint(x: xIntersection, y: yIntersection)
}
Rest everything is same.
Results in something like this:
You could try using position along the line, that is, using the angle of the line.
you have some sort of drag view to get the current position:
var drag: some Gesture {
DragGesture().onChanged { value in self.pos = CGPoint(x: value.location.x, y: value.location.y)}
}
you know the initial and final positions of the line, so you know the angle "atan(dx,dy)".
So staring from the start position you can get the next position for the circle, something like
x = dcos(angle)+x0, y=dsin(angle)+y0 ...

Camera is not following the airplane in Scenekit

I have a flying aircraft which I am following and I am also showing the path the aircraft has followed. I am drawing cylinders as a line for drawing the path. Its kind of drawing a line between 2 points. I have a cameraNode which is set to (0,200,200) initially. At that point I can see the aircraft. But when I start my flight. It goes out of the screen. I want 2 things :
Follow just the aircraft (Path won't matter).
Show whole path and also the aircraft.
I tried finding the min ad max x,y and z and taking average but it din't work. If you see below gif its too zoomed and aircraft has moved out of the screen
Here is how I set my camera:
- (void)setUpCamera {
SCNScene *workingScene = [self getWorkingScene];
_cameraNode = [[SCNNode alloc] init];
_cameraNode.camera = [SCNCamera camera];
_cameraNode.camera.zFar = 500;
_cameraNode.position = SCNVector3Make(0, 60, 50);
[workingScene.rootNode addChildNode:_cameraNode];
SCNNode *frontCameraNode = [SCNNode node];
frontCameraNode.position = SCNVector3Make(0, 100, 50);
frontCameraNode.camera = [SCNCamera camera];
frontCameraNode.camera.xFov = 75;
frontCameraNode.camera.zFar = 500;
[_assetActivity addChildNode:frontCameraNode]; //_assetActivity is the aircraft node.
}
Here is how I am changing camera position which is not working:
- (void)showRealTimeFlightPath {
DAL3DPoint *point = [self.aircraftLocation convertCooridnateTo3DPoint];
DAL3DPoint *previousPoint = [self.previousAircraftLocation convertCooridnateTo3DPoint];
self.minCoordinate = [self.minCoordinate findMinPoint:self.minCoordinate currentPoint:point];
self.maxCoordinate = [self.minCoordinate findMaxPoint:self.maxCoordinate currentPoint:point];
DAL3DPoint *averagePoint = [[DAL3DPoint alloc] init];
averagePoint = [averagePoint averageBetweenCoordiantes:self.minCoordinate maxPoint:self.maxCoordinate];
SCNVector3 positions[] = {
SCNVector3Make(point.x,point.y,point.z) ,
SCNVector3Make(previousPoint.x,previousPoint.y,previousPoint.z)
};
SCNScene *workingScene = [self getWorkingScene];
DALLineNode *lineNodeA = [[DALLineNode alloc] init];
[lineNodeA init:workingScene.rootNode v1:positions[0] v2:positions[1] radius:0.1 radSegementCount:6 lineColor:[UIColor greenColor]] ;
[workingScene.rootNode addChildNode:lineNodeA];
self.previousAircraftLocation = [self.aircraftLocation mutableCopy];
self.cameraNode.position = SCNVector3Make(averagePoint.x, averagePoint.y, z);
self.pointOfView = self.cameraNode;
}
Code in swift or objective c are welcomed.
Thanks!!
The first behavior you describe would most easily be achieved by chaining a look-at constraint and a distance constraint, both targeting the aircraft.
let lookAtConstraint = SCNLookAtConstraint(target: aircraft)
let distanceConstraint = SCNDistanceConstraint(target: aircraft)
distanceConstraint.minimumDistance = 10 // set to whatever minimum distance between the camera and aircraft you'd like
distanceConstraint.maximumDistance = 10 // set to whatever maximum distance between the camera and aircraft you'd like
camera.constraints = [lookAtConstraint, distanceConstraint]
For iOS 10 and earlier, you can implement a distance constraint using SCNTransformConstraint. Here's a basic (though slightly ugly 😛) implementation that uses linear interpolation to update the node's position.
func normalize(_ value: Float, in range: ClosedRange<Float>) -> Float {
return (value - range.lowerBound) / (range.upperBound - range.lowerBound)
}
func interpolate(from start: Float, to end: Float, alpha: Float) -> Float {
return (1 - alpha) * start + alpha * end
}
let target = airplane
let minimumDistance: Float = 10
let maximumDistance: Float = 15
let distanceConstraint = SCNTransformConstraint(inWorldSpace: false) { (node, transform) -> SCNMatrix4 in
let distance = abs(sqrt(pow(target.position.x - node.position.x, 2) + pow(target.position.y - node.position.y, 2) + pow(target.position.z - node.position.z, 2)))
let normalizedDistance: Float
switch distance {
case ...minimumDistance:
normalizedDistance = self.normalize(minimumDistance, in: 0 ... distance)
case maximumDistance...:
normalizedDistance = self.normalize(maximumDistance, in: 0 ... distance)
default:
return transform
}
node.position.x = self.interpolate(from: target.position.x, to: node.position.x, alpha: normalizedDistance)
node.position.y = self.interpolate(from: target.position.y, to: node.position.y, alpha: normalizedDistance)
node.position.z = self.interpolate(from: target.position.z, to: node.position.z, alpha: normalizedDistance)
return transform
}
The second behavior could be implemented by determining the bounding box of your aircraft and all of its path segments in the camera's local coordinate space, then updating the camera's distance from the center of that bounding box to frame all of those nodes in the viewport. frameNodes(_:), a convenience method that implements this functionality, was introduced in iOS 11 and is defined on SCNCameraController. I'd recommend using it if possible, unless you want to dive into the trigonometry yourself. You could use your scene view's default camera controller or create a temporary instance, whichever suits the needs of your app.
You need to calculate the angle of the velocity so that the camera points in the direction of the moving SCNNode.
This code will point you in the right direction.
func renderer(_ aRenderer: SCNSceneRenderer, didSimulatePhysicsAtTime time: TimeInterval) {
// get velocity angle using velocity of vehicle
var degrees = convertVectorToAngle(vector: vehicle.chassisBody.velocity)
// get rotation of current camera on X and Z axis
let eX = cameraNode.eulerAngles.x
let eZ = cameraNode.eulerAngles.z
// offset rotation on y axis by 90 degrees
// this needs work, buggy
let ninety = deg2rad(90)
// default camera Y Euler angle facing north at 0 degrees
var eY : Float = 0.0
if degrees != 0 {
eY = Float(-degrees) - Float(ninety)
}
// rotate camera direction using cameraNode.eulerAngles and direction of velocity as eY
cameraNode.eulerAngles = SCNVector3Make(eX, eY, eZ)
// put camera 25 points behind vehicle facing direction of velocity
let dir = calculateCameraDirection(cameraNode: vehicleNode)
let pos = pointInFrontOfPoint(point: vehicleNode.position, direction:dir, distance: 25)
// camera follows driver view from 25 points behind, and 10 points above vehicle
cameraNode.position = SCNVector3Make(pos.x, vehicleNode.position.y + 10, pos.z)
}
func convertVectorToAngle(vector: SCNVector3) -> CGFloat {
let degrees = atan2(vector.z, vector.x)
return CGFloat(degrees)
}
func pointInFrontOfPoint(point: SCNVector3, direction: SCNVector3, distance: Float) -> SCNVector3 {
var x = Float()
var y = Float()
var z = Float()
x = point.x + distance * direction.x
y = point.y + distance * direction.y
z = point.z + distance * direction.z
let result = SCNVector3Make(x, y, z)
return result
}
func calculateCameraDirection(cameraNode: SCNNode) -> SCNVector3 {
let x = -cameraNode.rotation.x
let y = -cameraNode.rotation.y
let z = -cameraNode.rotation.z
let w = cameraNode.rotation.w
let cameraRotationMatrix = GLKMatrix3Make(cos(w) + pow(x, 2) * (1 - cos(w)),
x * y * (1 - cos(w)) - z * sin(w),
x * z * (1 - cos(w)) + y*sin(w),
y*x*(1-cos(w)) + z*sin(w),
cos(w) + pow(y, 2) * (1 - cos(w)),
y*z*(1-cos(w)) - x*sin(w),
z*x*(1 - cos(w)) - y*sin(w),
z*y*(1 - cos(w)) + x*sin(w),
cos(w) + pow(z, 2) * ( 1 - cos(w)))
let cameraDirection = GLKMatrix3MultiplyVector3(cameraRotationMatrix, GLKVector3Make(0.0, 0.0, -1.0))
return SCNVector3FromGLKVector3(cameraDirection)
}
func deg2rad(_ number: Double) -> Double {
return number * .pi / 180
}

How to calculate the correct position for a matrix of circles to fit inside given view with proportional gaps?

I am pretty new to iOS development and I am trying to display a 10x10 grid inside a UIView respecting its bounds and I would like that the circles would be calculated based on the available width/height of the device.
What I tried so far without luck:
func setUpPoints() {
let matrixSize = 10
let diameter = (min(painelView.frame.size.width, painelView.frame.size.height) / CGFloat(matrixSize + 1)).rounded(.down)
let radius = diameter / 2
for i in 0...matrixSize {
for j in 0...matrixSize {
let x = CGFloat(i) * diameter + radius
let y = CGFloat(j) * diameter + radius
let frame = CGRect(x: x, y: y, width: diameter, height: diameter)
let circle = Circle(frame: frame)
circle.tag = j * matrixSize + i + 1
painelView.addSubview(circle)
}
}
}
My goal is to distribute the circles inside the gray rectangle proportionally so it will look like the Android pattern lock screen:
Can someone please give me some pointers?
Thanks.
If I understand what you are trying to do, then the following line:
let radius = (painelView.frame.size.width + painelView.frame.size.height) / CGFloat(matrixSize * 2)
should be:
let radius = (min(painelView.frame.size.width, painelView.frame.size.height) / CGFloat(matrixSize + 1)).rounded(.down)
The above change will allow the "square" of circles fit within whichever is smaller - the view's width or height, allowing for a gap around the "square" equal to half the diameter of each circle.
You also need to change both loops to start with 0.
for i in 0..<matrixSize {
for j in 0..<matrixSize {
BTW - your radius variable is really the diameter. And gap is really the radius.
The following code provides a border around the square of circles and it includes some space between the circles. Adjust as needed.
func setUpPoints() {
let matrixSize = 10
let borderRatio = CGFloat(0.5) // half a circle diameter - change as desired
let gapRatio = CGFloat(0.25) // quarter circle diameter - change as desired
let squareSize = min(painelView.frame.size.width, painelView.frame.size.height)
let diameter = (squareSize / (CGFloat(matrixSize) + 2 * borderRatio + CGFloat(matrixSize - 1) * gapRatio)).rounded(.down)
let centerToCenter = (diameter + diameter * gapRatio).rounded(.down)
let borderSize = (diameter * borderRatio).rounded()
for i in 0..<matrixSize {
for j in 0..<matrixSize {
let x = CGFloat(i) * centerToCenter + borderSize
let y = CGFloat(j) * centerToCenter + borderSize
let frame = CGRect(x: x, y: y, width: diameter, height: diameter)
let circle = Circle(frame: frame)
circle.tag = j * matrixSize + i + 1
painelView.addSubview(circle)
}
}
}

Add block between two points. SpriteKit

I am trying to add a SKNode between two points like picture below.
What I have:
I count the distance between those two points with this code (works fine):
func distanceCount(_ point: CGPoint) -> CGFloat {
return abs(CGFloat(hypotf(Float(point.x - x), Float(point.y - y)))) }
Then I count the middle point(also works fine)
func middlePointCount(_ point: CGPoint) -> CGPoint {
return CGPoint(x: CGFloat((point.x + x) / 2), y: CGFloat((point.y + y) / 2))
}
Finally this function adds my object (SKNode) :
func addBlock(_ size:CGSize, rotation:CGFloat, point: CGPoint) -> SKNode{
let block = SKSpriteNode(color: UIColor.lightGray , size: size)
block.physicsBody = SKPhysicsBody(rectangleOf: block.frame.size)
block.position = point //This is my middle point
block.physicsBody!.affectedByGravity = false
block.physicsBody!.isDynamic = false
block.zRotation = rotation
return block
}
Summary: My addBlock function adds object with right width and centred on the right place , but angle is wrong.
Note: I have tried to create functions which should count the angle but they were all wrong :/ .
My question: How can I get the right angle , or is there some other how can I reach my goal?
If you need more details just let me know.
Thank you :)
To get the angle between two points you'll need to use the following
atan2(p2.y-p1.y, p2.x-p1.x)
Midpoint
The midpoint between 2 points A and B is defined as
midpoint = {(A.x + B.x) / 2, (A.y + B.y) / 2}
CGPoint Extension
So let's create and extension of CGPoint to easily build a Midpoint starting from 2 points
extension CGPoint {
init(midPointBetweenA a: CGPoint, andB b: CGPoint) {
self.x = (a.x + b.x) / 2
self.y = (a.y + b.y) / 2
}
}
Test
Now let's test it
let a = CGPoint(x: 1, y: 4)
let b = CGPoint(x: 2, y: 3)
let c = CGPoint(midPointBetweenA: a, andB: b) // {x 1,5 y 3,5}
Looks good right?
Wrap up
Now given your 2 points you just need to calculate the midpoint and assign it to the position of your SKNode.
let nodeA: SKNode = ...
let nodeB: SKNode = ...
let nodeC: SKNode = ...
nodeC.position = CGPoint(midPointBetweenA: nodeA.position, andB: nodeB.position)

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.
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.)
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)
}
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)
}
}
Regards. (btw. I can only see 3D objects.. I have never seen a "line" in my life :o)
i use SCNVector3 extensions with:
func cylVector(from : SCNVector3, to : SCNVector3) -> SCNNode {
let vector = to - from,
length = vector.length()
let cylinder = SCNCylinder(radius: cylsRadius, height: CGFloat(length))
cylinder.radialSegmentCount = 6
cylinder.firstMaterial = material
let node = SCNNode(geometry: cylinder)
node.position = (to + from) / 2
node.eulerAngles = SCNVector3Make(CGFloat(Double.pi/2), acos((to.z-from.z)/length), atan2((to.y-from.y), (to.x-from.x) ))
return node
}
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);
}
I have been looking for a solution to make cylinder between two points and thanks to rickster, I have used his answer to make SCNNode extension. There, I have added missing conditions for a possible cylinder orientation 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
}

Resources