Having made some progress in the geometry side of things I\'m moving on to putting together an entire scene. That scene has a couple dozen objects, each defined by a boundin
Draw the line between two nodes:
func generateLine( startPoint: SCNVector3, endPoint: SCNVector3) -> SCNGeometry {
let vertices: [SCNVector3] = [startPoint, endPoint]
let data = NSData(bytes: vertices, length: MemoryLayout<SCNVector3>.size * vertices.count) as Data
let vertexSource = SCNGeometrySource(data: data,
semantic: .vertex,
vectorCount: vertices.count,
usesFloatComponents: true,
componentsPerVector: 3,
bytesPerComponent: MemoryLayout<Float>.size,
dataOffset: 0,
dataStride: MemoryLayout<SCNVector3>.stride)
let indices: [Int32] = [ 0, 1]
let indexData = NSData(bytes: indices, length: MemoryLayout<Int32>.size * indices.count) as Data
let element = SCNGeometryElement(data: indexData,
primitiveType: .line,
primitiveCount: indices.count/2,
bytesPerIndex: MemoryLayout<Int32>.size)
return SCNGeometry(sources: [vertexSource], elements: [element])
}
How To Use
let line = generateLine(startPoint: SCNVector3Make(1, 1, 1), endPoint: SCNVector3Make(8, 8, 8))
let lineNode = SCNNode(geometry: line)
lineNode.position = SCNVector3Make(15, 15, 10)
scene.rootNode.addChildNode(lineNode)
The thickness of the line requires implementing the SCNSceneRendererDelegate, in particular:
func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval){
glLineWidth(10)
}
Both solutions mentioned above work very well and I can contribute third solution to this question.
//extension code starts
func normalizeVector(_ iv: SCNVector3) -> SCNVector3 {
let length = sqrt(iv.x * iv.x + iv.y * iv.y + iv.z * iv.z)
if length == 0 {
return SCNVector3(0.0, 0.0, 0.0)
}
return SCNVector3( iv.x / length, iv.y / length, iv.z / length)
}
extension SCNNode {
func buildLineInTwoPointsWithRotation(from startPoint: SCNVector3,
to endPoint: SCNVector3,
radius: CGFloat,
color: UIColor) -> SCNNode {
let w = SCNVector3(x: endPoint.x-startPoint.x,
y: endPoint.y-startPoint.y,
z: endPoint.z-startPoint.z)
let l = CGFloat(sqrt(w.x * w.x + w.y * w.y + w.z * w.z))
if l == 0.0 {
// two points together.
let sphere = SCNSphere(radius: radius)
sphere.firstMaterial?.diffuse.contents = color
self.geometry = sphere
self.position = startPoint
return self
}
let cyl = SCNCylinder(radius: radius, height: l)
cyl.firstMaterial?.diffuse.contents = color
self.geometry = cyl
//original vector of cylinder above 0,0,0
let ov = SCNVector3(0, l/2.0,0)
//target vector, in new coordination
let nv = SCNVector3((endPoint.x - startPoint.x)/2.0, (endPoint.y - startPoint.y)/2.0,
(endPoint.z-startPoint.z)/2.0)
// axis between two vector
let av = SCNVector3( (ov.x + nv.x)/2.0, (ov.y+nv.y)/2.0, (ov.z+nv.z)/2.0)
//normalized axis vector
let av_normalized = normalizeVector(av)
let q0 = Float(0.0) //cos(angel/2), angle is always 180 or M_PI
let q1 = Float(av_normalized.x) // x' * sin(angle/2)
let q2 = Float(av_normalized.y) // y' * sin(angle/2)
let q3 = Float(av_normalized.z) // z' * sin(angle/2)
let r_m11 = q0 * q0 + q1 * q1 - q2 * q2 - q3 * q3
let r_m12 = 2 * q1 * q2 + 2 * q0 * q3
let r_m13 = 2 * q1 * q3 - 2 * q0 * q2
let r_m21 = 2 * q1 * q2 - 2 * q0 * q3
let r_m22 = q0 * q0 - q1 * q1 + q2 * q2 - q3 * q3
let r_m23 = 2 * q2 * q3 + 2 * q0 * q1
let r_m31 = 2 * q1 * q3 + 2 * q0 * q2
let r_m32 = 2 * q2 * q3 - 2 * q0 * q1
let r_m33 = q0 * q0 - q1 * q1 - q2 * q2 + q3 * q3
self.transform.m11 = r_m11
self.transform.m12 = r_m12
self.transform.m13 = r_m13
self.transform.m14 = 0.0
self.transform.m21 = r_m21
self.transform.m22 = r_m22
self.transform.m23 = r_m23
self.transform.m24 = 0.0
self.transform.m31 = r_m31
self.transform.m32 = r_m32
self.transform.m33 = r_m33
self.transform.m34 = 0.0
self.transform.m41 = (startPoint.x + endPoint.x) / 2.0
self.transform.m42 = (startPoint.y + endPoint.y) / 2.0
self.transform.m43 = (startPoint.z + endPoint.z) / 2.0
self.transform.m44 = 1.0
return self
}
}
//extension ended.
//in your code, you can like this.
let twoPointsNode1 = SCNNode()
scene.rootNode.addChildNode(twoPointsNode1.buildLineInTwoPointsWithRotation(
from: SCNVector3(1,-1,3), to: SCNVector3( 7,11,7), radius: 0.2, color: .cyan))
//end
you can reference http://danceswithcode.net/engineeringnotes/quaternions/quaternions.html
BTW, you will get same result when you use a cylinder to make a line between two points from above 3 methods. But indeed, they will have different normal lines. In another words, if you use box between two points, sides of box, except top and bottom, will face different direction from above 3 methods.
let me know pls if you need further explanation.
Here's a solution using simd and quaternions for the rotation. I based the extension off of the answer by @Bersaelor.
I used this derivation (https://stackoverflow.com/a/1171995/6693924) to create the quaternion from two vectors. Hope this helps.
extension SCNNode {
static func lineNode(from: simd_float3, to: simd_float3, radius : CGFloat = 0.25) -> SCNNode
{
let vector = to - from
let height = simd_length(vector)
//cylinder
let cylinder = SCNCylinder(radius: radius, height: CGFloat(height))
cylinder.firstMaterial?.diffuse.contents = UIColor.white
//line node
let lineNode = SCNNode(geometry: cylinder)
//adjust line position
let line_axis = simd_float3(0, height/2, 0)
lineNode.simdPosition = from + line_axis
let vector_cross = simd_cross(line_axis, vector)
let qw = simd_length(line_axis) * simd_length(vector) + simd_dot(line_axis, vector)
let q = simd_quatf(ix: vector_cross.x, iy: vector_cross.y, iz: vector_cross.z, r: qw).normalized
lineNode.simdRotate(by: q, aroundTarget: from)
return lineNode
}
}
Sprout's (wow, the autocorrect will not allow me to actually type in his name!) post is indeed a solution, but I have implemented a very different solution in my code.
What I do is calculate the length of the line and the two endpoints, based on the X, Y and Z locations from the two ends:
let w = SCNVector3(x: CGFloat(x2m-x1m), y: CGFloat(y2m-y1m), z: CGFloat(z2m-z1m))
let l = w.length()
The length is simply pythag. Now I make an SCNNode
that will hold the SCNCylinder
, and position it in the middle of the line:
let node = SCNNode(geometry: cyl)
node.position = SCNVector3(x: CGFloat((x1m+x2m)/2.0), y: CGFloat((y1m+y2m)/2.0), z: CGFloat((z1m+z2m)/2.0))
And now the nasty part, where we calculate the Euler angles and rotate the node:
let lxz = (Double(w.x)**2 + Double(w.z)**2)**0.5
var pitch, pitchB: Double
if w.y < 0 {
pitchB = M_PI - asin(Double(lxz)/Double(l))
} else {
pitchB = asin(Double(lxz)/Double(l))
}
if w.z == 0 {
pitch = pitchB
} else {
pitch = sign(Double(w.z)) * pitchB
}
var yaw: Double
if w.x == 0 && w.z == 0 {
yaw = 0
} else {
let inner = Double(w.x) / (Double(l) * sin (pitch))
if inner > 1 {
yaw = M_PI_2
} else if inner < -1 {
yaw = M_PI_2
} else {
yaw = asin(inner)
}
}
node.eulerAngles = SCNVector3(CGFloat(pitch), CGFloat(yaw), 0)
I suspect there is a much simpler way to do this using one of the other rotation inputs, but this works and working is a feature!
@maury-markowitz's answer worked for me, here is the latest (Swift4) version of it.
To anyone working with SCNVector3
in Swift I can only recommend to add the +-*/
operator overloads somewhere in your code (e.g. from here).
extension SCNNode {
static func lineNode(from: SCNVector3, to: SCNVector3, radius: CGFloat = 0.25) -> SCNNode {
let vector = to - from
let height = vector.length()
let cylinder = SCNCylinder(radius: radius, height: CGFloat(height))
cylinder.radialSegmentCount = 4
let node = SCNNode(geometry: cylinder)
node.position = (to + from) / 2
node.eulerAngles = SCNVector3.lineEulerAngles(vector: vector)
return node
}
}
extension SCNVector3 {
static func lineEulerAngles(vector: SCNVector3) -> SCNVector3 {
let height = vector.length()
let lxz = sqrtf(vector.x * vector.x + vector.z * vector.z)
let pitchB = vector.y < 0 ? Float.pi - asinf(lxz/height) : asinf(lxz/height)
let pitch = vector.z == 0 ? pitchB : sign(vector.z) * pitchB
var yaw: Float = 0
if vector.x != 0 || vector.z != 0 {
let inner = vector.x / (height * sinf(pitch))
if inner > 1 || inner < -1 {
yaw = Float.pi / 2
} else {
yaw = asinf(inner)
}
}
return SCNVector3(CGFloat(pitch), CGFloat(yaw), 0)
}
}
For the sake of another method, I achieved this through trigonometry. This made the code very minimal. Here is the end result:
In my case the nodes are always placed on a fixed plane that slices the Y-Axis.
// Create Cylinder Geometry
let line = SCNCylinder(radius: 0.002, height: node1.distance(to: node2))
// Create Material
let material = SCNMaterial()
material.diffuse.contents = UIColor.red
material.lightingModel = .phong
line.materials = [material]
// Create Cylinder(line) Node
let newLine = SCNNode()
newLine.geometry = line
newLine.position = posBetween(first: node1, second: node2)
// This is the change in x,y and z between node1 and node2
let dirVector = SCNVector3Make(node2.x - node1.x, node2.y - node1.y, node2.z - node1.z)
// Get Y rotation in radians
let yAngle = atan(dirVector.x / dirVector.z)
// Rotate cylinder node about X axis so cylinder is laying down
currentLine.eulerAngles.x = .pi / 2
// Rotate cylinder node about Y axis so cylinder is pointing to each node
currentLine.eulerAngles.y = yAngle
This is the function to get the position between two nodes, place it within your class:
func posBetween(first: SCNVector3, second: SCNVector3) -> SCNVector3 {
return SCNVector3Make((first.x + second.x) / 2, (first.y + second.y) / 2, (first.z + second.z) / 2)
}
This is the extension to get the distance between nodes for the cylinder height, place it somewhere outside of your class:
extension SCNVector3 {
func distance(to destination: SCNVector3) -> CGFloat {
let dx = destination.x - x
let dy = destination.y - y
let dz = destination.z - z
return CGFloat(sqrt(dx*dx + dy*dy + dz*dz))
}
}
If you don't have one fixed axis like myself then you could do the extra trig to use this method.