在球面上两点之间的圆柱方向,Scenekit,Quaternions IOS

7
我一直在使用SceneKit尝试在一个球的外边缘的两点之间绘制一个圆柱体。我已经使用基本几何和SCNRendering Delegate中的OpenGL生成了连接这两个点的线,但现在我需要在这两个点之间(不仅仅是两个点,而是任意位于球面上的3D向量)产生一个圆柱体。我已经连续工作了大约三天,浏览了所有有关实现四元数的资料,但目前为止,我无法让它正常工作。学术论文、科学研究,以及其他任何东西都不能让圆柱体在两个固定点之间重新对齐。我需要一种算法来实现这一点。
以下是我的最新代码,虽然它无法正常工作,但这只是我经过近2000行代码的小片段,没有达到预期的结果。我知道我可以采取更高级的方法,比如构建自己的SCNProgram和/或SCNRenderer来访问GLSL、OpenGL和Metal复杂性,但这似乎是可以使用Scenekit并在GLKit向量结构和SCNVector结构之间进行转换的东西,但到目前为止,这是不可能的。
以下代码将经度和纬度坐标投影到3D球面的表面上,并通过我构建的专有函数返回这些坐标的SCNVector3 {x,y,z}坐标,这些坐标在我的3D球上可以准确显示。我在两组经度和纬度坐标之间绘制一条线,使用基本几何绘制的线穿过了球的中心。所以,正如我上面提到的,我希望实现同样的功能,但是用圆柱体代替线(顺便说一下,这里列出的经度和纬度坐标是虚假的,它们是随机生成的,但都位于地球表面上)。
drawLine = [self lat1:37.76830 lon1:-30.40096 height1:tall lat2:3.97620 lon2:63.73095 height2:tall];

float cylHeight = GLKVector3Distance(SCNVector3ToGLKVector3(cooridnateSetOne.position), SCNVector3ToGLKVector3(coordinateSetTwo.position));

SCNCylinder * cylTest = [SCNCylinder cylinderWithRadius:0.2 height:cylHeight];
SCNNode * test = [SCNNode nodeWithGeometry:cylTest];

SCNMaterial *material = [SCNMaterial material];
[[material diffuse] setContents:[SKColor whiteColor]];
material.diffuse.intensity = 60;
material.emission.contents = [SKColor whiteColor];

material.lightingModelName = SCNLightingModelConstant;
[cylTest setMaterials:@[material]];

GLKVector3 u = SCNVector3ToGLKVector3(cooridnateSetOne.position);
GLKVector3 v = SCNVector3ToGLKVector3(cooridnateSetTwo.position);

GLKVector3 w = GLKVector3CrossProduct(u, v);

GLKQuaternion q = GLKQuaternionMakeWithAngleAndVector3Axis(GLKVector3DotProduct(u,v), GLKVector3Normalize(w));
q.w += GLKQuaternionLength(q);
q = GLKQuaternionNormalize(q);
SCNVector4 final = SCNVector4FromGLKVector4(GLKVector4Make(q.x, q.y, q.z, q.w));

test.orientation = final;

我尝试过其他代码,包括相同的方法。事实上,我甚至在Objective-C中构建了自己的SCNVector3和SCNVector4数学库,以查看我的数学方法是否产生与使用GLKit数学不同的值,但是两种方法都得到了相同的结果。任何帮助都将是很棒的,但现在,我不想涉足比SceneKit更复杂的东西。我会在一个月或两个月后才会深入研究Metal和/或OpenGL。谢谢!
编辑:
变量“cooridnateSetOne”和“cooridnateSetTwo”是由另一个函数产生的SCNNodes,该函数将原始线几何体强制转换为此节点,然后将其返回到SCNScene的子类实现中。
6个回答

9
这是一个使用节点层次结构的快速演示(将圆柱体定位在一个点处,使其端部与本地z轴重合),并使用约束(使该z轴朝向另一个点)的示例。
let root = view.scene!.rootNode

// visualize a sphere
let sphere = SCNSphere(radius: 1)
sphere.firstMaterial?.transparency = 0.5
let sphereNode = SCNNode(geometry: sphere)
root.addChildNode(sphereNode)

// some dummy points opposite each other on the sphere
let rootOneThird = CGFloat(sqrt(1/3.0))
let p1 = SCNVector3(x: rootOneThird, y: rootOneThird, z: rootOneThird)
let p2 = SCNVector3(x: -rootOneThird, y: -rootOneThird, z: -rootOneThird)

// height of the cylinder should be the distance between points
let height = CGFloat(GLKVector3Distance(SCNVector3ToGLKVector3(p1), SCNVector3ToGLKVector3(p2)))

// add a container node for the cylinder to make its height run along the z axis
let zAlignNode = SCNNode()
zAlignNode.eulerAngles.x = CGFloat(M_PI_2)
// and position the zylinder so that one end is at the local origin
let cylinder = SCNNode(geometry: SCNCylinder(radius: 0.1, height: height))
cylinder.position.y = -height/2
zAlignNode.addChildNode(cylinder)

// put the container node in a positioning node at one of the points
p2Node.addChildNode(zAlignNode)
// and constrain the positioning node to face toward the other point
p2Node.constraints = [ SCNLookAtConstraint(target: p1Node) ]

非常抱歉,如果你在寻找一种特定于 ObjC 的解决方案,但是对我来说,在 OS X Swift 演练场中原型化这个问题更快。 (此外,在 iOS 中需要的 CGFloat 转换较少,因为 SCNVector3 的元素类型仅为 Float。)


谢谢你的回答!我已经将它用作SCNNode扩展,并添加了一些条件,以获取所有可能点坐标之间的正确连接。 - VYT
代码末尾的 p1Nodep2Node 是什么? - rmaddy

5

以下是一种更优雅的SCNCylinder实现方式,可用于连接给定半径的起始和结束位置:

func makeCylinder(from: SCNVector3, to: SCNVector3, radius: CGFloat) -> SCNNode
{
    let lookAt = to - from
    let height = lookAt.length()

    let y = lookAt.normalized()
    let up = lookAt.cross(vector: to).normalized()
    let x = y.cross(vector: up).normalized()
    let z = x.cross(vector: y).normalized()
    let transform = SCNMatrix4(x: x, y: y, z: z, w: from)

    let geometry = SCNCylinder(radius: radius, 
                               height: CGFloat(height))
    let childNode = SCNNode(geometry: geometry)
    childNode.transform = SCNMatrix4MakeTranslation(0.0, height / 2.0, 0.0) * 
      transform

    return childNode
}

需要安装以下扩展:

extension SCNVector3 {
    /**
     * Calculates the cross product between two SCNVector3.
     */
    func cross(vector: SCNVector3) -> SCNVector3 {
        return SCNVector3Make(y * vector.z - z * vector.y, z * vector.x - x * vector.z, x * vector.y - y * vector.x)
    }

    func length() -> Float {
        return sqrtf(x*x + y*y + z*z)
    }

    /**
     * Normalizes the vector described by the SCNVector3 to length 1.0 and returns
     * the result as a new SCNVector3.
     */
    func normalized() -> SCNVector3 {
        return self / length()
    }
}

extension SCNMatrix4 {
    public init(x: SCNVector3, y: SCNVector3, z: SCNVector3, w: SCNVector3) {
        self.init(
            m11: x.x,
            m12: x.y,
            m13: x.z,
            m14: 0.0,

            m21: y.x,
            m22: y.y,
            m23: y.z,
            m24: 0.0,

            m31: z.x,
            m32: z.y,
            m33: z.z,
            m34: 0.0,

            m41: w.x,
            m42: w.y,
            m43: w.z,
            m44: 1.0)
    }
}

/**
 * Divides the x, y and z fields of a SCNVector3 by the same scalar value and
 * returns the result as a new SCNVector3.
 */
func / (vector: SCNVector3, scalar: Float) -> SCNVector3 {
    return SCNVector3Make(vector.x / scalar, vector.y / scalar, vector.z / scalar)
}

func * (left: SCNMatrix4, right: SCNMatrix4) -> SCNMatrix4 {
    return SCNMatrix4Mult(left, right)
}

这可能是被接受的答案,因为它做到了其他答案没有做到的——在正确的位置和方向上创建一个单一的 SCNNode 圆柱体。不幸的是,它有缺陷,因为它漏掉了它正在使用的两个扩展,所以它不能编译——即使用4个 SCNVector3SCNMatrix4init 和他用于翻译的两个 SCNMatrix4 的乘法。它们足够简单,但不明显。如果 @ChristopherOezbek 没有发帖,我会发布它们... - Grimxn
谢谢,Christopher!我同意,一个标准的向量和矩阵库是必不可少的(我们都写过一个),但我认为真正的问题是开发人员缺乏数学知识... - Grimxn
上面的代码无法编译,变量“result”未定义。此外,“SCNVector4”扩展似乎没有在任何地方使用。 - sammy-sc
1
它应该说“return childNode”。他也未使用“radius”和“color”参数。尽管如此,我可以证明Oezbek先生提供的解决方案有效。同样的Grimxn,如果我能的话,我会进一步点赞。这一切都归结于对概念良好的“数学知识”。 - oneiros
我在“lookAt = to - from”这一行上得到了一个“二元运算符'-'不能应用于两个'SCNVector3'操作数”的错误提示。 我认为你忘记了SCNVector3的func -函数。 - user3064538

4

谢谢你,Rickster!我将其进一步处理,并制作成了一个类:

class LineNode: SCNNode
{
    init( parent: SCNNode,     // because this node has not yet been assigned to a parent.
              v1: SCNVector3,  // where line starts
              v2: SCNVector3,  // where line ends
          radius: CGFloat,     // line thicknes
      radSegmentCount: Int,    // number of sides of the line
        material: [SCNMaterial] )  // any material.
    {
        super.init()
        let  height = v1.distance(v2)

        position = v1

        let ndV2 = SCNNode()

        ndV2.position = v2
        parent.addChildNode(ndV2)

        let ndZAlign = SCNNode()
        ndZAlign.eulerAngles.x = Float(M_PI_2)

        let cylgeo = SCNCylinder(radius: radius, height: CGFloat(height))
        cylgeo.radialSegmentCount = radSegmentCount
        cylgeo.materials = material

        let ndCylinder = SCNNode(geometry: cylgeo )
        ndCylinder.position.y = -height/2
        ndZAlign.addChildNode(ndCylinder)

        addChildNode(ndZAlign)

        constraints = [SCNLookAtConstraint(target: ndV2)]
    }

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

我已经在iOS应用程序中成功地测试了这个类,使用这个函数绘制了100条线(糟糕的是,是圆柱:o)。
    func linesTest3()
    {
        let mat = SCNMaterial()
        mat.diffuse.contents  = UIColor.whiteColor()
        mat.specular.contents = UIColor.whiteColor()

        for _ in 1...100    // draw 100 lines (as cylinders) between random points.
        {
            let v1 =  SCNVector3( x: Float.random(min: -50, max: 50),
                                  y: Float.random(min: -50, max: 50),
                                  z: Float.random(min: -50, max: 50) )

            let v2 =  SCNVector3( x: Float.random(min: -50, max: 50),
                                  y: Float.random(min: -50, max: 50),
                                  z: Float.random(min: -50, max: 50) )

            // Just for testing, add two little spheres to check if lines are drawn correctly:
            // each line should run exactly from a green sphere to a red one:

            root.addChildNode(makeSphere(v1, radius: 0.5, color: UIColor.greenColor()))
            root.addChildNode(makeSphere(v2, radius: 0.5, color: UIColor.redColor()))

            // Have to pass the parentnode because 
            // it is not known during class instantiation of LineNode.

            let ndLine = LineNode(
                         parent: scene.rootNode, // ** needed
                             v1: v1,    // line (cylinder) starts here
                             v2: v2,    // line ends here
                         radius: 0.2,   // line thickness
                radSegmentCount: 6,     // hexagon tube
                       material: [mat] )  // any material

            root.addChildNode(ndLine)
        }
    }

100个随机的线条 问候。 (顺便说一句,我只能看到3D物体.. 我从来没有在我的生命中看过“线”:o)


2
这是一个使用Objective-C的完整方法。
首先,这是如何使用它的:
SCNNode * testNode = [self lat1:-35 lon1:108 height1:tall lat2:-35 lon2:30 height2:0];

输入:
第一个位置 lat1 = 第一个位置的纬度 lon1 = 第一个位置的经度 height1 = 第一个位置距离地球的高度 lat2 = 第二个位置的纬度 lon2 = 第二个位置的经度 height2 = 第二个位置距离地球的高度
第二种方法创建了每个位置的SCNVector3点。
-(SCNNode *)lat1:(double)lat1 lon1:(double)lon1 height1:(float)height1 lat2:(double)lat2 lon2:(double)lon2 height2:(float)height2 {
    SCNVector3 positions[] = {[self lat:lat1 lon:lon1 height:height1], [self lat:lat2 lon:lon2 height:height2]};
    
    float cylHeight = GLKVector3Distance(SCNVector3ToGLKVector3(positions[0]), SCNVector3ToGLKVector3(positions[1]))/4;
    
    SCNCylinder * masterCylinderNode = [SCNCylinder cylinderWithRadius:0.05 height:cylHeight];

    SCNMaterial *material = [SCNMaterial material];
    [[material diffuse] setContents:[SKColor whiteColor]];
    material.lightingModelName = SCNLightingModelConstant;
    material.emission.contents = [SKColor whiteColor];
    [masterCylinderNode setMaterials:@[material]];

    SCNNode *mainLocationPointNodeTestA = [mainLocationPointNode clone];
    SCNNode *mainLocationPointNodeTestB = [mainLocationPointNode clone];
    
    mainLocationPointNodeTestA.position = positions[0];
    mainLocationPointNodeTestB.position = positions[1];
    
    SCNNode * mainParentNode = [SCNNode node];
    SCNNode * tempNode2 =[SCNNode nodeWithGeometry:masterCylinderNode];
    
    [mainParentNode addChildNode:mainLocationPointNodeTestA];
    [mainParentNode addChildNode:mainLocationPointNodeTestB];
    [mainParentNode addChildNode:tempNode2];
    
    [mainParentNode setName:@"parentToLineNode"];
    
    tempNode2.position = SCNVector3Make((positions[0].x+positions[1].x)/2, (positions[0].y+positions[1].y)/2, (positions[0].z+positions[1].z)/2);
    tempNode2.pivot = SCNMatrix4MakeTranslation(0, cylHeight*1.5, 0);
    
    GLKVector3 normalizedVectorStartingPosition = GLKVector3Make(0.0, 1.0, 0.0);
    GLKVector3 magicAxis = GLKVector3Normalize(GLKVector3Subtract(GLKVector3Make(positions[0].x/2, positions[0].y/2, positions[0].z/2), GLKVector3Make(positions[1].x/2, positions[1].y/2, positions[1].z/2)));
    
    GLKVector3 rotationAxis = GLKVector3CrossProduct(normalizedVectorStartingPosition, magicAxis);
    CGFloat rotationAngle = GLKVector3DotProduct(normalizedVectorStartingPosition, magicAxis);
    
    GLKVector4 rotation = GLKVector4MakeWithVector3(rotationAxis, acos(rotationAngle));
    tempNode2.rotation = SCNVector4FromGLKVector4(rotation);
    
    return mainParentNode;
}

这种方法使用硬编码的地球半径和曲率值,我展示这个只是为了展示实现100%准确性所需的数字。你显然需要根据你场景的正确尺寸进行更改,但这是方法。这是Link使用的方法的改编版。可以在此处找到解释:Link。我很快就把它拼凑起来了,但它是准确的,可以随意更改数字格式。
-(SCNVector3)lat:(double)lat lon:(double)lon height:(float)height {
    double latd = 0.0174532925;
    double latitude = latd*lat;
    double longitude = latd*lon;
    
    Float64 rad = (Float64)(6378137.0);
    Float64 f = (Float64)(1.0/298.257223563);
    
    double cosLat = cos(latitude);
    
    double sinLat = sin(latitude);
    
    double FF = pow((1.0-f), 2);
    double C = 1/(sqrt(pow(cosLat,2) + FF * pow(sinLat,2)));
    double S = C * FF;
    
    double x = ((rad * C)*cosLat * cos(longitude))/(1000000/(1+height));
    double y = ((rad * C)*cosLat * sin(longitude))/(1000000/(1+height));
    double z = ((rad * S)*sinLat)/(1000000/(1+height));
    
    return SCNVector3Make(y+globeNode.position.x, z+globeNode.position.y, x+globeNode.position.z);
}

哈哈,就在我修复自己的方法并使它正常工作的时候,我看到了这个方法,而且它比我的方法更好。我把这些方法放进我的代码里,它现在已经能够正常运行,而且比使用 lookatconstraints 方法更好。非常不错! - Larry Pickles
太好了!我很高兴它起作用了,我之前做了这个项目,最近开始搜索相关问题,想看看是否还能帮到你,非常不错。 - Larry Pickles

2
我使用 SCNVector3 扩展,代码如下:
 func cylVector(from : SCNVector3, to : SCNVector3) -> SCNNode {
    let vector = to - from,
        length = vector.length()

    let cylinder = SCNCylinder(radius: cylsRadius, height: CGFloat(length))
    cylinder.radialSegmentCount = 6
    cylinder.firstMaterial = material

    let node = SCNNode(geometry: cylinder)

    node.position = (to + from) / 2
    node.eulerAngles = SCNVector3Make(CGFloat(Double.pi/2), acos((to.z-from.z)/length), atan2((to.y-from.y), (to.x-from.x) ))

    return node
}

0

我一直在寻找一种方法,在两个点之间制作圆柱体,感谢rickster,我使用了他的答案来制作SCNNode扩展。在那里,我添加了缺失的条件,以避免可能的圆柱体方向错误相反。

func makeCylinder(positionStart: SCNVector3, positionEnd: SCNVector3, radius: CGFloat , color: NSColor, transparency: CGFloat) -> SCNNode
{
    let height = CGFloat(GLKVector3Distance(SCNVector3ToGLKVector3(positionStart), SCNVector3ToGLKVector3(positionEnd)))
    let startNode = SCNNode()
    let endNode = SCNNode()

    startNode.position = positionStart
    endNode.position = positionEnd

    let zAxisNode = SCNNode()
    zAxisNode.eulerAngles.x = CGFloat(M_PI_2)

    let cylinderGeometry = SCNCylinder(radius: radius, height: height)
    cylinderGeometry.firstMaterial?.diffuse.contents = color
    let cylinder = SCNNode(geometry: cylinderGeometry)

    cylinder.position.y = -height/2
    zAxisNode.addChildNode(cylinder)

    let returnNode = SCNNode()

    if (positionStart.x > 0.0 && positionStart.y < 0.0 && positionStart.z < 0.0 && positionEnd.x > 0.0 && positionEnd.y < 0.0 && positionEnd.z > 0.0)
    {
        endNode.addChildNode(zAxisNode)
        endNode.constraints = [ SCNLookAtConstraint(target: startNode) ]
        returnNode.addChildNode(endNode)

    }
    else if (positionStart.x < 0.0 && positionStart.y < 0.0 && positionStart.z < 0.0 && positionEnd.x < 0.0 && positionEnd.y < 0.0 && positionEnd.z > 0.0)
    {
        endNode.addChildNode(zAxisNode)
        endNode.constraints = [ SCNLookAtConstraint(target: startNode) ]
        returnNode.addChildNode(endNode)

    }
    else if (positionStart.x < 0.0 && positionStart.y > 0.0 && positionStart.z < 0.0 && positionEnd.x < 0.0 && positionEnd.y > 0.0 && positionEnd.z > 0.0)
    {
        endNode.addChildNode(zAxisNode)
        endNode.constraints = [ SCNLookAtConstraint(target: startNode) ]
        returnNode.addChildNode(endNode)

    }
    else if (positionStart.x > 0.0 && positionStart.y > 0.0 && positionStart.z < 0.0 && positionEnd.x > 0.0 && positionEnd.y > 0.0 && positionEnd.z > 0.0)
    {
        endNode.addChildNode(zAxisNode)
        endNode.constraints = [ SCNLookAtConstraint(target: startNode) ]
        returnNode.addChildNode(endNode)

    }
    else
    {
        startNode.addChildNode(zAxisNode)
        startNode.constraints = [ SCNLookAtConstraint(target: endNode) ]
        returnNode.addChildNode(startNode)
    }

    return returnNode
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接