在两点之间绘制 SceneKit 对象

Posted

技术标签:

【中文标题】在两点之间绘制 SceneKit 对象【英文标题】:Draw SceneKit object between two points 【发布时间】:2016-05-02 07:56:31 【问题描述】:

在geometry side 方面取得了一些进展,我正着手将整个场景组合在一起。该场景有几十个对象,每个对象由一个边界立方体定义,其角由两个 SCNVector3(最初是两组 x、y、z)指定。

这是我目前所拥有的一个示例 - 它是一个 11 元素对数周期天线,就像 70 年代的老式电视天线一样。每条灰线都是一个“元素”,通常由铝棒制成。我使用了从 +ve 到 -ve Y 的 SCNCylinders,整个内容不到 100 行(SK 非常棒)。

问题是如果元素在 X 上不对称并且因此必须旋转 SCNCylinder 会发生什么情况。我找到了this example,但我无法理解细节......它似乎利用了球体是对称的这一事实,因此角度有点“消失”。

有没有人有一个通用的函数,它将获取两个 3D 点并返回适合设置节点的 eulerAngle 的 SCNVector3,或类似的解决方案?

【问题讨论】:

您找到解决方案了吗?谢谢! 恐怕运气不好。我转到了应用程序的其他部分。 【参考方案1】:

上面提到的两种解决方案都非常有效,我可以为这个问题提供第三个解决方案。

//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

可以参考http://danceswithcode.net/engineeringnotes/quaternions/quaternions.html

顺便说一句,当您使用圆柱在上述 3 种方法中的两点之间画一条线时,您将得到相同的结果。但实际上,它们会有不同的法线。换句话说,如果你在两点之间使用盒子,盒子的边,除了顶部和底部,将面向与上述3种方法不同的方向。

如果您需要进一步解释,请告诉我。

【讨论】:

当我使用 ARImageTrackingConfiguration 而不是 ARWorldTrackingConfiguration 时,是否将线路放在固定位置?【参考方案2】:

编辑:对于低于或等于 ios 11

我要告诉你一个好消息!您可以链接两个点并在此 Vector 上放置一个 SCNNode !

拿着这个,享受在两点之间画线吧!

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)
        
    

【讨论】:

您可以使用scene.rootNode.addChildNode() 将此节点添加到您的scenekit 视图中!我已经使用这个类来模拟两个原子之间的蛋白质连接。【参考方案3】:

@maury-markowitz 的回答对我有用,这是它的最新 (Swift4) 版本。 对于在 Swift 中使用 SCNVector3 的任何人,我只能建议在代码中的某处添加 +-*/ 运算符重载 (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)
    

【讨论】:

请注意,如果您正在使用 Swift 4(因此可能针对 iOS 11 / macOS 10.13 / 等),您可以更轻松地使用 SIMD 向量类型而不是 SCNVector,从而得到东西像免费的运算符重载(因为它们使用 CPU 内在函数而更快)。此外,SCNNode.look(at:) 等新的便利功能也可能是相关的。【参考方案4】:

为了另一种方法,我通过三角函数实现了这一点。这使得代码非常少。这是最终结果:

在我的例子中,节点总是放置在一个固定的平面上,该平面切割 Y 轴。

// 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

这是获取两个节点之间位置的函数,将它放在你的类中:

func posBetween(first: SCNVector3, second: SCNVector3) -> SCNVector3 
        return SCNVector3Make((first.x + second.x) / 2, (first.y + second.y) / 2, (first.z + second.z) / 2)

这是获取圆柱高度节点之间距离的扩展,将其放置在类之外的某个位置:

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))
    

如果你没有像我这样的固定轴,那么你可以做额外的三角来使用这种方法。

【讨论】:

【参考方案5】:

这是一个使用 simd 和四元数进行旋转的解决方案。我基于@Bersaelor 的答案扩展。

我使用这个推导 (https://***.com/a/1171995/6693924) 从两个向量创建四元数。希望这会有所帮助。

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
    

【讨论】:

我需要使用 lineNode.simdRotate(by: q, aroundTarget: (to+from)/2) 但除此之外,这对我来说非常有效,而且非常简洁!谢谢!【参考方案6】:

Sprout 的(哇,自动更正不允许我实际输入他的名字!)帖子确实是一个解决方案,但我在我的代码中实现了一个非常不同的解决方案。

我所做的是根据两端的 X、Y 和 Z 位置计算直线和两个端点的长度:

let w = SCNVector3(x: CGFloat(x2m-x1m), y: CGFloat(y2m-y1m), z: CGFloat(z2m-z1m))
let l = w.length()

长度只是 pythag。现在我创建一个SCNNode 来保存SCNCylinder,并将它放在行的中间:

    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))

现在是讨厌的部分,我们计算欧拉角并旋转节点:

    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)

我怀疑有一种更简单的方法可以使用其他旋转输入之一来执行此操作,但这很有效,而且工作是一项功能!

【讨论】:

【参考方案7】:

在两个节点之间画线:

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])

    

如何使用

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)

线条的粗细需要实现SCNSceneRendererDelegate,特别是:

func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval)
        glLineWidth(10)

【讨论】:

'glLineWidth' 已弃用:首先在 iOS 12.0 中弃用 - OpenGLES API 已弃用。【参考方案8】:

Winchill 答案的 Objective-C 版本:

-(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);
    

【讨论】:

以上是关于在两点之间绘制 SceneKit 对象的主要内容,如果未能解决你的问题,请参考以下文章

在 SceneKit/modelIO 中测量 3D-Obj 文件上两点之间的距离并显示它们

如何在 iOS 11 中将 SKVideoNode 添加到 ARKit-SceneKit?

在 SceneKit 中,如何在不同大小的对象上平铺纹理,同时保持最小的绘制调用?

在两点之间绘制平滑曲线 arc()

SceneKit - 在3D对象上绘图

R语言segments函数绘制两点之间的线段实战