Draw SceneKit object between two points

前端 未结 7 1959
别跟我提以往
别跟我提以往 2020-12-07 23:51

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

相关标签:
7条回答
  • 2020-12-08 00:08

    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)
    }
    
    0 讨论(0)
  • 2020-12-08 00:11

    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.

    0 讨论(0)
  • 2020-12-08 00:15

    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
        }
    }
    
    0 讨论(0)
  • 2020-12-08 00:21

    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!

    0 讨论(0)
  • 2020-12-08 00:22

    @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)
        }
    }
    
    0 讨论(0)
  • 2020-12-08 00:33

    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.

    0 讨论(0)
提交回复
热议问题