How to calculate normal vector of a SCNPlane? - ios

Goal: Define the normal vector for a plane represented by SCNPlane.
As mentioned in Apple's documentation Working with Vectors - Calculate the Normal of a Triangle, I can calculate the normal vector based on 3 points from the plane. The issue here is that I don't know how I can get 3 points that they make proper triangle. I've noticed that SCNPlane has a property boundingBox and it can represent 2 of the triangle vertices (min and max). How can I find the 3rd vertex that is placed on the SCNPlane? I cannot use the center of the SCNPlane, because it creates a line along with min and max points from boundingBox.
Is there any other way that can help me to retrieve a normal vector for the SCNPlane?

From the documentation we learn that
The surface is one-sided. Its surface normal vectors point in the positive z-axis direction of its local coordinate space, so it is only visible from that direction by default.
The normal of a SCNPlane is always (0, 0, 1) in local space and that cannot change.
When it is attached to a node, the orientation of that node determines the normal in an another coordinate system. You can use simdConvertVector:toNode: to convert between coordinate spaces:
// normal expressed in world space
let normal = simd_normalize(node.simdConvertVector(simd_float3(0, 0, 1), to: nil))

To add to the accepted answer, in order to retrieve the points that define the plane, you can query the plane's geometry sources.
let plane = SCNPlane(width: 100, height: 20)
print("Sources for normal: \(vertices(sources: plane.sources(for: .normal)))")
print("Sources for vertex: \(vertices(sources: plane.sources(for: .vertex)))")
extension UnsafeRawPointer {
func loadUnaligned<T>(as: T.Type, count: Int) -> [T] {
assert(_isPOD(T.self)) // relies on the type being POD (no refcounting or other management)
let buffer = UnsafeMutablePointer<T>.allocate(capacity: count)
defer { buffer.deallocate() }
memcpy(buffer, self, MemoryLayout<T>.size * count)
return (0..<count).map({ index in buffer.advanced(by: index).pointee })
}
}
func vertices(sources: [SCNGeometrySource]) -> [[SCNVector3]] {
var result = [[SCNVector3]]()
result.reserveCapacity(sources.count)
for source in sources {
precondition(source.usesFloatComponents == true, "SCNVector3 can handle only three-component vectors whose components are floating-point values, i.e., floats or doubles")
precondition(source.componentsPerVector == 3, "SCNVector3 can only be used for three components per vector")
let shouldUseFloatNotDouble: Bool
if source.bytesPerComponent == 4 {
shouldUseFloatNotDouble = true
}
else if source.bytesPerComponent == 8 {
shouldUseFloatNotDouble = false
}
else {
assert(false, "The SCNGeometrySource has reported an unexpected byte size for its vector components, not 4 bytes (float) or 8 bytes (double) but \(source.bytesPerComponent). I am not equipped for this so I am going to use floats and hope for the best. This will probably not work. Sorry.")
shouldUseFloatNotDouble = true
}
let vectors = source.data.withUnsafeBytes {
(p: UnsafeRawBufferPointer) -> [SCNVector3] in
if (shouldUseFloatNotDouble) {
let simdArray = (p.baseAddress! + source.dataOffset).loadUnaligned(as: SIMD3<Float>.self, count: source.vectorCount)
return simdArray.map { simd in SCNVector3(simd)}
} else {
let simdArray = (p.baseAddress! + source.dataOffset).loadUnaligned(as: SIMD3<Double>.self, count: source.vectorCount)
return simdArray.map { simd in SCNVector3(simd)}
}
}
result.append(vectors)
}
return result
}
Output:
Sources for normal: [[__C.SCNVector3(x: 0.0, y: 0.0, z: 1.0), __C.SCNVector3(x: 1.0, y: 0.5, z: -0.5), __C.SCNVector3(x: 0.0, y: 0.0, z: 1.0), __C.SCNVector3(x: 1.0, y: -0.5, z: 0.5)]]
Sources for vertex: [[__C.SCNVector3(x: -0.5, y: -0.5, z: 0.0), __C.SCNVector3(x: 0.0, y: 1.0, z: 0.0), __C.SCNVector3(x: 0.5, y: -0.5, z: 0.0), __C.SCNVector3(x: 0.0, y: 1.0, z: 1.0)]]

Related

How do I create a PKDrawing programmatically, from CGPoints?

I've watched this WWDC session as well as its sample project: https://developer.apple.com/documentation/pencilkit/inspecting_modifying_and_constructing_pencilkit_drawings
However, when I try to plot CGPoints on my drawing canvas, nothing shows up.
Here's my setup:
var points: [CGPoint] = []
(500...1000).forEach { x in
(500...1000).forEach { y in
points.append(CGPoint(x: x, y: y))
}
}
let strokePoints = points.map {
PKStrokePoint(location: $0, timeOffset: 0, size: CGSize(uniform: 1), opacity: 2, force: 1, azimuth: 1, altitude: 1)
}
let strokePath = PKStrokePath(controlPoints: strokePoints, creationDate: Date())
let stroke = PKStroke(ink: PKInk(.pen, color: .systemGreen), path: strokePath)
canvasView.drawing = PKDrawing(strokes: [ stroke ])
I figured that the problem was on the size of my stroke. This will work:
PKStrokePoint(location: point, timeOffset: 0, size: CGSize(uniform: 3), opacity: 1, force: 0, azimuth: 0, altitude: 0)
Note that it must be at least 3. If it's 1, it's invisible, if it's 2 it's semi-transparent. This doesn't seem to be related to the screen's scale (my iPad Pro's UIScreen.main.scale = 2.0) so I just hardcoded it at 3, to represent a single pixel.
To achieve this result, I drew a single pixel on the screen using the pencil and logged its content, which showed me these parameters:
▿ PencilKit.PKStrokePoint
- strokePoint: <PKStrokePoint: 0x600001ad8f60 location={445, 333.5} timeOffset=0.000000 size={3.084399938583374, 3.084399938583374} opacity=0.999985 azimuth=-3.141593 force=0.000000 altitude=1.570796> #0
- super: NSObject
So then I played around with those values (for size, opacity, azimuth, force and altitude), and figured that none of those except the size and opacity matter. That's why I set them as all as zero values in my code.

Pixel to Vertex coordinate during off-screen rendering

I'm drawing a set of triangles filled with plain color to off-screen texture.
My problem is that I get too big triangles in a result image, it looks like this:
My vertex coordinates are in pixels. (I simply generate them as a random float in (0, outTexture.width/height)). I do not multiply them by any projection in my vertex function (maybe that is my mistake?)
So my question is how does vertex coordinates correlate with pixel coordinate?
The solution was to make ortho projection and pass it as a uniform.
Here is the code that worked for me:
func makeOrthographicMatrix(left: Float, right: Float, bottom: Float, top: Float, near: Float, far: Float) -> [Float] {
let ral = right + left
let rsl = right - left
let tab = top + bottom
let tsb = top - bottom
let fan = far + near
let fsn = far - near
return [2.0 / rsl, 0.0, 0.0, 0.0,
0.0, 2.0 / tsb, 0.0, 0.0,
0.0, 0.0, -2.0 / fsn, 0.0,
-ral / rsl, -tab / tsb, -fan / fsn, 1.0]
}
And I called that like so :
var projection = makeOrthographicMatrix(0.0, right: Float(inTexture.width), bottom: 0.0, top: Float(inTexture.height), near: -1.0, far: 1.0)

SceneKit - Draw 3D Parabola

I'm given three points and need to draw a smooth 3D parabola. The trouble is that curved line is choppy and has some weird divots in it
Here is my code...
func drawJump(jump: Jump){
let halfDistance = jump.distance.floatValue/2 as Float
let tup = CalcParabolaValues(0.0, y1: 0.0, x2: halfDistance, y2: jump.height.floatValue, x3: jump.distance.floatValue, y3: 0)
println("tuple \tup")
var currentX = 0 as Float
var increment = jump.distance.floatValue / Float(50)
while currentX < jump.distance.floatValue - increment {
let x1 = Float(currentX)
let x2 = Float((currentX+increment))
let y1 = calcParabolaYVal(tup.a, b: tup.b, c: tup.c, x: x1)
let y2 = calcParabolaYVal(tup.a, b: tup.b, c: tup.c, x: x2)
drawLine(x1, y1: y1, x2: x2, y2: y2)
currentX += increment
}
}
func CalcParabolaValues(x1: Float, y1: Float, x2: Float, y2: Float, x3: Float, y3: Float) -> (a: Float, b: Float, c: Float) {
println(x1, y1, x2, y2, x3, y3)
let a = y1/((x1-x2)*(x1-x3)) + y2/((x2-x1)*(x2-x3)) + y3/((x3-x1)*(x3-x2))
let b = (-y1*(x2+x3)/((x1-x2)*(x1-x3))-y2*(x1+x3)/((x2-x1)*(x2-x3))-y3*(x1+x2)/((x3-x1)*(x3-x2)))
let c = (y1*x2*x3/((x1-x2)*(x1-x3))+y2*x1*x3/((x2-x1)*(x2-x3))+y3*x1*x2/((x3-x1)*(x3-x2)))
return (a, b, c)
}
func calcParabolaYVal(a:Float, b:Float, c:Float, x:Float)->Float{
return a * x * x + b * x + c
}
func drawLine(x1: Float, y1: Float,x2: Float, y2: Float) {
println("drawLine \(x1) \(y1) \(x2) \(y2)")
let positions: [Float32] = [
x1, y1, 0,
x2, y2, 0
]
let positionData = NSData(bytes: positions, length: sizeof(Float32)*positions.count)
let indices: [Int32] = [0, 1]
let indexData = NSData(bytes: indices, length: sizeof(Int32) * indices.count)
let source = SCNGeometrySource(data: positionData, semantic: SCNGeometrySourceSemanticVertex, vectorCount: indices.count, floatComponents: true, componentsPerVector: 3, bytesPerComponent: sizeof(Float32), dataOffset: 0, dataStride: sizeof(Float32) * 3)
let element = SCNGeometryElement(data: indexData, primitiveType: SCNGeometryPrimitiveType.Line, primitiveCount: indices.count, bytesPerIndex: sizeof(Int32))
let line = SCNGeometry(sources: [source], elements: [element])
self.rootNode.addChildNode( SCNNode(geometry: line))
}
func renderer(aRenderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: NSTimeInterval) {
glLineWidth(20)
}
I also have to figure out how to animate the arc from left to right. Can someone help me out? Swift or Objective C is fine. Any help is appreciated. Thanks!
I'd recommend using SCNShape to create your parabola. To start, you'll need to represent your parabola as a Bézier curve. You can use UIBezierPath for that. For animation, I personally find shader modifiers are a nice fit for cases like this.
The Parabola
Watch out, though — you probably want a path that represents just the open stroke of the arc. If you do something like this:
let path = UIBezierPath()
path.moveToPoint(CGPointZero)
path.addQuadCurveToPoint(CGPoint(x: 100, y: 0), controlPoint: CGPoint(x: 50, y: 200))
You'll get a filled-in parabola, like this (seen in 2D in the debugger quick look, then extruded in 3D with SCNShape):
To create a closed shape that's just the arc, you'll need to trace back over the curve, a little bit away from the original:
let path = UIBezierPath()
path.moveToPoint(CGPointZero)
path.addQuadCurveToPoint(CGPoint(x: 100, y: 0), controlPoint: CGPoint(x: 50, y: 200))
path.addLineToPoint(CGPoint(x: 99, y: 0))
path.addQuadCurveToPoint(CGPoint(x: 1, y: 0), controlPoint: CGPoint(x: 50, y: 198))
That's better.
... in Three-Dee!
How to actually make it 3D? Just make an SCNShape with the extrusion depth you like:
let shape = SCNShape(path: path, extrusionDepth: 10)
And set it in your scene:
shape.firstMaterial?.diffuse.contents = SKColor.blueColor()
let shapeNode = SCNNode(geometry: shape)
shapeNode.pivot = SCNMatrix4MakeTranslation(50, 0, 0)
shapeNode.eulerAngles.y = Float(-M_PI_4)
root.addChildNode(shapeNode)
Here I'm using a pivot to make the shape rotate around the major axis of the parabola, instead of the y = 0 axis of the planar Bézier curve. And making it blue. Also, root is just a shortcut I made for the view's scene's root node.
Animating
The shape of the parabola doesn't really need to change through your animation — you just need a visual effect that progressively reveals it along its x-axis. Shader modifiers are a great fit for that, because you can make the animated effect per-pixel instead of per-vertex and do all the expensive work on the GPU.
Here's a shader snippet that uses a progress parameter, varying from 0 to 1, to set opacity based on x-position:
// declare a variable we can set from SceneKit code
uniform float progress;
// tell SceneKit this shader uses transparency so we get correct draw order
#pragma transparent
// get the position in model space
vec4 mPos = u_inverseModelViewTransform * vec4(_surface.position, 1.0);
// a bit of math to ramp the alpha based on a progress-adjusted position
_surface.transparent.a = clamp(1.0 - ((mPos.x + 50.0) - progress * 200.0) / 50.0, 0.0, 1.0);
Set that as a shader modifier for the Surface entry point, and then you can animate the progress variable:
let modifier = "uniform float progress;\n #pragma transparent\n vec4 mPos = u_inverseModelViewTransform * vec4(_surface.position, 1.0);\n _surface.transparent.a = clamp(1.0 - ((mPos.x + 50.0) - progress * 200.0) / 50.0, 0.0, 1.0);"
shape.shaderModifiers = [ SCNShaderModifierEntryPointSurface: modifier ]
shape.setValue(0.0, forKey: "progress")
SCNTransaction.begin()
SCNTransaction.setAnimationDuration(10)
shape.setValue(1.0, forKey: "progress")
SCNTransaction.commit()
Further Considerations
Here's the whole thing in a form you can paste into a (iOS) playground. A few things left as exercises to the reader, plus other notes:
Factor out the magic numbers and make a function or class so you can alter the size/shape of your parabola. (Remember that you can scale SceneKit nodes relative to other scene elements, so they don't have to use the same units.)
Position the parabola relative to other scene elements. If you take away my line that sets the pivot, the shapeNode.position is the left end of the parabola. Change the parabola's length (or scale it), then rotate it around its y-axis, and you can make the other end line up with some other node. (For you to fire ze missiles at?)
I threw this together with Swift 2 beta, but I don't think there's any Swift-2-specific syntax in there — porting back to 1.2 if you need to deploy soon should be straightforward.
If you also want to do this on OS X, it's a bit trickier — there, SCNShape uses NSBezierPath, which unlike UIBezierPath doesn't support quadratic curves. Probably an easy way out would be to fake it with an elliptical arc.
I don't think your table has enough points, assuming the renderer is connecting them with straight line segments. On top of this, the thickness and dashing of the line make it difficult to see that. Try getting a smooth curve with a thin solid line first.
If you want to animate the progression of the curve, as if it were showing the flight of a projectile, it will probably be easiest to just write your function for the motion: y = k*x^2, and just render from x=0 to x=T for increasing values of T.

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
}

Could someone explain why/how this sample scene kit code works?

I am working with scene kit and am trying to make a first person game. I found this sample code for making the first person camera with a pan gesture. Everything works but I do not understand what is going on here. Could someone explain what is happening?
func lookGestureRecognized(gesture: UIPanGestureRecognizer) {
//get translation and convert to rotation
let translation = gesture.translationInView(self.view)
let hAngle = acos(Float(translation.x) / 200) - Float(M_PI_2)
let vAngle = acos(Float(translation.y) / 200) - Float(M_PI_2)
//rotate hero
heroNode.physicsBody?.applyTorque(SCNVector4(x: 0, y: 1, z: 0, w: hAngle), impulse: true)
//tilt camera
elevation = max(Float(-M_PI_4), min(Float(M_PI_4), elevation + vAngle))
camNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: elevation)
//reset translation
gesture.setTranslation(CGPointZero, inView: self.view)
}
Here is the same code, with a few additional comments:
func lookGestureRecognized(gesture: UIPanGestureRecognizer) {
// Create Translation variable, containing the
// "distance" traveled by the finger since the last event
let translation = gesture.translationInView(self.view)
// From this distance, calculate how much the camera should move
// 1) horizontally, 2) vertically using angles (FPS controls)
let hAngle = acos(Float(translation.x) / 200) - Float(M_PI_2)
let vAngle = acos(Float(translation.y) / 200) - Float(M_PI_2)
// Apply the horizontal angle to the Hero node as a force to
// Make it rotate accordingly (physics body use forces to move)
heroNode.physicsBody?.applyTorque(SCNVector4(x: 0, y: 1, z: 0, w: hAngle), impulse: true)
// Use the other angle to look up and down, clamped to ±pi/4
elevation = max(Float(-M_PI_4), min(Float(M_PI_4), elevation + vAngle))
// Apply the new angle to teh camera on the X axis
camNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: elevation)
// Set the translation to 0 to avoid accumulation
// the next time the event is triggered
gesture.setTranslation(CGPointZero, inView: self.view)
}
This should help understand, let me know if you need more details on how it works!
(Note: the "Distance" is actually a 2D Vector)
EDIT: here's a better explanation of the angle:
let hAngle = acos(Float(translation.x) / 200) - Float(M_PI_2)
First, the translation (so pixel distance on x) is divided by 200. This is to both slow down the movement and (unsafely) keep x between -1 and 1.
Acos gives the arc cosinus of a value. The result is between 0 to pi and (to simplify) only works for x from -1 to 1. Here's a graph for it: http://www.wolframalpha.com/input/?i=acos%28x%29-pi%2F2
Since we want to move in positive and negative directions, we remove half of the max value (M_PI_2, which is pi/2) to keep the result within -pi/2 to pi/2
In the end, if you move your finger 200 pixels in a direction, you would look pi/2=90° on your screen.

Resources