球体上两点之间的圆柱方向、Scenekit、四元数 IOS

Cylinder Orientation between two points on a sphere, Scenekit, Quaternions IOS

我一直在尝试使用 SceneKit 在球体外缘的两点之间绘制一个圆柱体。我已经使用原始几何图形和带有 SCNRendering Delegate 的 openGL 在这两点之间生成了一条线,但是现在我需要在这两个点之间生成一个圆柱体(好吧,不仅仅是两个,而是位于球体表面的任意两个 3D 矢量).我已经为此连续工作了大约 3 天,并且我已经完成了我能找到的关于实现四元数的所有内容以实现这一点,但就目前而言,我无法让它工作。学术文章、科学研究,什么都没有,没有任何东西可以重新调整两个固定点之间的圆柱体。我需要一个算法来做到这一点。

无论如何,这是我最近的代码,但它不起作用,但这只是我到目前为止已经完成的近 2k 行代码中的一小段,但没有达到预期的结果。我知道我可以转向更高级的东西,比如构建我自己的 SCNProgram and/or SCNRenderer,然后访问 GLSL、OpenGL 和 Metal 复杂性,但这似乎应该可以使用 Scenekit 并在 GLKit 矢量结构和 之间转换来自 SCNVector 结构,但到目前为止这是不可能的:

代码:

以下代码获取经度和纬度坐标并将它们投影到 3D 球体的表面上。这些坐标是通过我构建的专有函数返回的,在该函数中我收到了 {x,y,z} 坐标的 SCNVector3,该坐标在我的 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 and/or OpenGL。谢谢!

编辑:

变量 "cooridnateSetOne" 和 "cooridnateSetTwo" 是由另一个函数生成的 SCNNodes,该函数强制原始线几何图形进入此节点,然后 returns 它成为 SCNScene 的子类实现。

这是一个快速演示,它使用节点层次结构(使圆柱体位于其末端位于一个点且其长度沿局部 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。)

谢谢你,里克斯特!我把它更进一步,并从中制作了一个 class:

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 应用程序中成功测试了这个 class,使用这个功能, 绘制 100 行(oops cylinders :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)
        }
    }

问候。 (顺便说一句。我只能看到 3D 对象。我这辈子从没见过 "line" :o)

这是使用 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% 准确度所需的数字,这就是它的工作原理。显然,您需要将其更改为场景的正确尺寸,但方法如下。这是对 http://www.gdal.org/index.html. An explanation an be found here: http://www.gdal.org/osr_tutorial.html 使用的方法的改编。我很快就把它放在一起了,但它很有效而且很准确,你可以根据自己的喜好随意更改数字格式。

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

我一直在寻找在两点之间制作圆柱体的解决方案,感谢 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
}

仅供参考,一个更优雅的 SCNCyclinder 实现以给定半径连接开始和结束位置:

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

我使用 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
}