Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Draw SceneKit object between two points

Tags:

swift

scenekit

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?

like image 991
Maury Markowitz Avatar asked Jan 25 '16 20:01

Maury Markowitz


4 Answers

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.

like image 199
xudesheng Avatar answered Nov 17 '22 01:11

xudesheng


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)
        }
    }
}
like image 7
E. Spiroux Avatar answered Nov 17 '22 00:11

E. Spiroux


@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)
    }
}
like image 5
Bersaelor Avatar answered Nov 16 '22 23:11

Bersaelor


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.

like image 3
Steve Avatar answered Nov 16 '22 23:11

Steve