9

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 bounding cube whose corners are specified by two SCNVector3s (originally two sets of x,y,z).

Here's an example of what I have so far - it's an 11-element log-periodic antenna, like the old school TV antennas from the 70s. Each of the grey lines is an "element", typically made of aluminum rod. I used SCNCylinders from +ve to -ve Y and the entire thing is less than 100 lines (SK is pretty amazing).

olde schoole

The problem is what happens if the elements are not symmetrical across X and thus the SCNCylinder has to be rotated. I found this example, but I can't understand the specifics... it appears to take advantage of the fact that a sphere is symmetric so angles kind of "go away".

Does anyone have a general function that will take two 3D points and return the SCNVector3 suitable for setting the node's eulerAngle, or a similar solution?

Community
  • 1
  • 1
Maury Markowitz
  • 9,082
  • 11
  • 46
  • 98

8 Answers8

22

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.

xudesheng
  • 1,082
  • 11
  • 25
  • do this put the line on fixed place as I'm using ARImageTrackingConfiguration instead of ARWorldTrackingConfiguration ? – aznelite89 Dec 03 '18 at 02:27
7

EDIT: For under or equal to IOS 11

I've good news for you ! You can link two points and put a SCNNode on this Vector !

Take this and enjoy drawing line between two point !

class   CylinderLine: SCNNode
{
    init( parent: SCNNode,//Needed to add destination point of your line
        v1: SCNVector3,//source
        v2: SCNVector3,//destination
        radius: CGFloat,//somes option for the cylinder
        radSegmentCount: Int, //other option
        color: UIColor )// color of your node object
    {
        super.init()

        //Calcul the height of our line
        let  height = v1.distance(v2)

        //set position to v1 coordonate
        position = v1

        //Create the second node to draw direction vector
        let nodeV2 = SCNNode()

        //define his position
        nodeV2.position = v2
        //add it to parent
        parent.addChildNode(nodeV2)

        //Align Z axis
        let zAlign = SCNNode()
        zAlign.eulerAngles.x = Float(M_PI_2)

        //create our cylinder
        let cyl = SCNCylinder(radius: radius, height: CGFloat(height))
        cyl.radialSegmentCount = radSegmentCount
        cyl.firstMaterial?.diffuse.contents = color

        //Create node with cylinder
        let nodeCyl = SCNNode(geometry: cyl )
        nodeCyl.position.y = -height/2
        zAlign.addChildNode(nodeCyl)

        //Add it to child
        addChildNode(zAlign)

        //set contrainte direction to our vector
        constraints = [SCNLookAtConstraint(target: nodeV2)]
    }

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

private extension SCNVector3{
    func distance(receiver:SCNVector3) -> Float{
        let xd = receiver.x - self.x
        let yd = receiver.y - self.y
        let zd = receiver.z - self.z
        let distance = Float(sqrt(xd * xd + yd * yd + zd * zd))

        if (distance < 0){
            return (distance * -1)
        } else {
            return (distance)
        }
    }
}
E. Spiroux
  • 430
  • 6
  • 15
  • You can add this node with scene.rootNode.addChildNode() to your scenekit view ! I've used this class to modelize protein link between two atoms. – E. Spiroux Oct 04 '17 at 09:03
5

@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)
    }
}
Bersaelor
  • 2,517
  • 34
  • 58
  • 1
    Note that if you're working with Swift 4 (and thus likely targeting iOS 11 / macOS 10.13 / etc), you can more easily use SIMD vector types instead of `SCNVector`, and thus get things like operator overloads (that are faster because they use CPU intrinsics) for free. Also, new convenience functions like `SCNNode.look(at:)` might be relevant. – rickster Jan 03 '18 at 20:04
4

For the sake of another method, I achieved this through trigonometry. This made the code very minimal. Here is the end result:

enter image description here

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.

Steve
  • 4,372
  • 26
  • 37
4

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
    }
}
  • 1
    I needed to use lineNode.simdRotate(by: q, aroundTarget: (to+from)/2) but other than that this worked perfectly for me, and is really concise! Thanks! – Chris McElroy Oct 11 '20 at 20:52
1

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
  • 9,082
  • 11
  • 46
  • 98
0

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)
}
Niraj Paul
  • 1,498
  • 14
  • 22
0

Objective-C version of Winchill's answer:

-(void)lineNodeFrom:(SCNVector3)to to:(SCNVector3)from radius:(float)radius{
    
    
    SCNVector3 w = SCNVector3Make(to.x - from.x, to.y - from.y, from.z - to.z);
    float l = sqrtf(powf(w.x, 2) + powf(w.y, 2) + powf(w.z, 2.0f));
    
    SCNCylinder * cylinder = [SCNCylinder cylinderWithRadius:radius height:l];
    SCNMaterial * material = [SCNMaterial material];
    material.diffuse.contents = [[UIColor darkGrayColor] colorWithAlphaComponent:0.75f];
    cylinder.materials = @[material];
    
    [self setGeometry:cylinder];
    
    //original vector of cylinder above 0,0,0
    SCNVector3 ov = SCNVector3Make(0, l/2.0,0);
    //target vector, in new coordination
    SCNVector3 nv = SCNVector3Make((from.x - to.x)/2.0, (from.y - to.y)/2.0, (from.z-to.z)/2.0);
    // axis between two vector
    SCNVector3 av = SCNVector3Make((ov.x + nv.x)/2.0, (ov.y+nv.y)/2.0, (ov.z+nv.z)/2.0);
    
    //normalized axis vector
    SCNVector3 av_normalized = [self normaliseVector:av];
    float q0 = 0.0f; //cos(angel/2), angle is always 180 or M_PI
    float q1 = av_normalized.x; // x' * sin(angle/2)
    float q2 = av_normalized.y; // y' * sin(angle/2)
    float q3 = av_normalized.z; // z' * sin(angle/2)
    
    float r_m11 = q0 * q0 + q1 * q1 - q2 * q2 - q3 * q3;
    float r_m12 = 2 * q1 * q2 + 2 * q0 * q3;
    float r_m13 = 2 * q1 * q3 - 2 * q0 * q2;
    float r_m21 = 2 * q1 * q2 - 2 * q0 * q3;
    float r_m22 = q0 * q0 - q1 * q1 + q2 * q2 - q3 * q3;
    float r_m23 = 2 * q2 * q3 + 2 * q0 * q1;
    float r_m31 = 2 * q1 * q3 + 2 * q0 * q2;
    float r_m32 = 2 * q2 * q3 - 2 * q0 * q1;
    float r_m33 = q0 * q0 - q1 * q1 - q2 * q2 + q3 * q3;
    
    SCNMatrix4 transform;
    transform.m11 = r_m11;
    transform.m12 = r_m12;
    transform.m13 = r_m13;
    transform.m14 = 0.0;
    
    transform.m21 = r_m21;
    transform.m22 = r_m22;
    transform.m23 = r_m23;
    transform.m24 = 0.0;
    
    transform.m31 = r_m31;
    transform.m32 = r_m32;
    transform.m33 = r_m33;
    transform.m34 = 0.0;
    
    transform.m41 = (to.x + from.x) / 2.0;
    transform.m42 = (to.y + from.y) / 2.0;
    transform.m43 = (to.z + from.z) / 2.0;
    transform.m44 = 1.0;
 
    self.transform = transform;
    
}

-(SCNVector3)normaliseVector:(SCNVector3)iv{
    
    float length = sqrt(iv.x * iv.x + iv.y * iv.y + iv.z * iv.z);
    if (length == 0){
        return SCNVector3Make(0.0, 0.0, 0.0);
    }
    
    return SCNVector3Make(iv.x / length, iv.y / length, iv.z / length);
    
}
Johnny Rockex
  • 4,136
  • 3
  • 35
  • 55