3

If I have two points in SceneKit (e.g. (1,2,3) and (-1,-1,-1)). How do I draw a line between the two?

I see that there is a SCNBox object I may be able to use, but that only allows me to specify the center (e.g. via simdPosition). The other ways to modify it are the transform (which I don't know how to use), or the Euler angles (which I'm not sure how to calculate which ones I need to use).

Andy Jazz
  • 49,178
  • 17
  • 136
  • 220
Senseful
  • 86,719
  • 67
  • 308
  • 465

3 Answers3

5

You can draw a line between two points using the following approach:

import SceneKit

extension SCNGeometry {

    class func line(vector1: SCNVector3,
                    vector2: SCNVector3) -> SCNGeometry {

        let sources = SCNGeometrySource(vertices: [vector1,
                                                   vector2])
        let index: [Int32] = [0,1]

        let elements = SCNGeometryElement(indices: index,
                                    primitiveType: .line)

        return SCNGeometry(sources: [sources],
                          elements: [elements])
    }
}

...and then feed it to addLine function in ViewController:

class ViewController: UIViewController { 

    // Some code...

    func addLine(start: SCNVector3, end: SCNVector3) {
        let lineGeo = SCNGeometry.line(vector1: start,
                                       vector2: end)
        let lineNode = SCNNode(geometry: lineGeo)
        sceneView.scene.rootNode.addChildNode(lineNode)
    }
}

As we all know line's width can't be changed (cause there's no property to do it), so you can use cylinder primitive geometry instead of a line:

extension SCNGeometry {

    class func cylinderLine(from: SCNVector3, 
                              to: SCNVector3,
                        segments: Int) -> SCNNode {

        let x1 = from.x
        let x2 = to.x

        let y1 = from.y
        let y2 = to.y

        let z1 = from.z
        let z2 = to.z

        let distance =  sqrtf( (x2-x1) * (x2-x1) +
                               (y2-y1) * (y2-y1) +
                               (z2-z1) * (z2-z1) )

        let cylinder = SCNCylinder(radius: 0.005,
                                   height: CGFloat(distance))

        cylinder.radialSegmentCount = segments

        cylinder.firstMaterial?.diffuse.contents = UIColor.green

        let lineNode = SCNNode(geometry: cylinder)

        lineNode.position = SCNVector3(x: (from.x + to.x) / 2,
                                       y: (from.y + to.y) / 2,
                                       z: (from.z + to.z) / 2)

        lineNode.eulerAngles = SCNVector3(Float.pi / 2,
                                          acos((to.z-from.z)/distance),
                                          atan2((to.y-from.y),(to.x-from.x)))

        return lineNode
    }
}

...then feed it the same way to ViewController:

class ViewController: UIViewController { 

    // Some code...

    func addLine(start: SCNVector3, end: SCNVector3) {

        let cylinderLineNode = SCNGeometry.cylinderLine(from: start, 
                                                          to: end, 
                                                    segments: 3)

        sceneView.scene.rootNode.addChildNode(cylinderLineNode)
    }
}
Andy Jazz
  • 49,178
  • 17
  • 136
  • 220
2

First you'll need to calculate the heading and pitch between the two points. Full post is here and this answer explains how to do it between any arbitrary two points.

Once you have your two angles, if you attempt to use the Euler angles on an SCNBox, you'll notice that when you only modify the pitch (eulerAngles.x), or only modify the heading (eulerAngles.y), everything works fine. However, the moment you try to modify both, you'll run into issues. One solution is to wrap one node inside another.

This seemed like such a useful suggestion, that I create a handy wrapper node that should handle rotations upon all 3 axes:

import Foundation
import SceneKit

struct HeadingPitchBank {
    let heading: Float
    let pitch: Float
    let bank: Float

    /// returns the heading and pitch (bank is 0) represented by the vector
    static func from(vector: simd_float3) -> HeadingPitchBank {
        let heading = atan2f(vector.x, vector.z)
        let pitch = atan2f(sqrt(vector.x*vector.x + vector.z*vector.z), vector.y) - Float.pi / 2.0

        return HeadingPitchBank(heading: heading, pitch: pitch, bank: 0)
    }
}

class HeadingPitchBankWrapper: SCNNode {

    init(wrappedNode: SCNNode) {
        headingNode = SCNNode()
        pitchNode = SCNNode()
        bankNode = SCNNode()
        _wrappedNode = wrappedNode

        super.init()

        addChildNode(headingNode)
        headingNode.addChildNode(pitchNode)
        pitchNode.addChildNode(bankNode)
        bankNode.addChildNode(wrappedNode)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    var heading: Float {
        get {
            return headingNode.eulerAngles.y
        }
        set {
            headingNode.eulerAngles.y = newValue
        }
    }

    var pitch: Float {
        get {
            return pitchNode.eulerAngles.x
        }
        set {
            pitchNode.eulerAngles.x = newValue
        }
    }

    var bank: Float {
        get {
            return bankNode.eulerAngles.z
        }
        set {
            bankNode.eulerAngles.z = newValue
        }
    }

    var headingPitchBank: HeadingPitchBank {
        get {
            return HeadingPitchBank(heading: heading, pitch: pitch, bank: bank)
        }
        set {
            heading = newValue.heading
            pitch = newValue.pitch
            bank = newValue.bank
        }
    }

    var wrappedNode: SCNNode {
        return _wrappedNode
    }

    private var headingNode: SCNNode
    private var pitchNode: SCNNode
    private var bankNode: SCNNode
    private var _wrappedNode: SCNNode
}

You could then use this to easily draw a line between two points:

func createLine(start: simd_float3 = simd_float3(), end: simd_float3, color: UIColor, opacity: CGFloat? = nil, radius: CGFloat = 0.005) -> SCNNode {
    let length = CGFloat(simd_length(end-start))
    let box = SCNNode(geometry: SCNBox(width: radius, height: radius, length: length, chamferRadius: 0))
    box.geometry!.firstMaterial!.diffuse.contents = color

    let wrapper = HeadingPitchBankWrapper(wrappedNode: box)
    wrapper.headingPitchBank = HeadingPitchBank.from(vector: end - start)
    wrapper.simdPosition = midpoint(start, end)
    if let opacity = opacity {
        wrapper.opacity = opacity
    }
    return wrapper
}
Senseful
  • 86,719
  • 67
  • 308
  • 465
2

Just build a custom geometry using SCNGeometryPrimitiveType.line:

let vertices: [SCNVector3] = [
    SCNVector3(1, 2, 3),
    SCNVector3(-1, -1, -1)
]

let linesGeometry = SCNGeometry(
    sources: [
        SCNGeometrySource(vertices: vertices)
    ],
    elements: [
        SCNGeometryElement(
            indices: [Int32]([0, 1]),
            primitiveType: .line
        )
    ]
)

let line = SCNNode(geometry: linesGeometry)
scene.rootNode.addChildNode(line)
dirk
  • 494
  • 1
  • 3
  • 17