使用 SceneKit 在两点之间绘制一条线

Posted

技术标签:

【中文标题】使用 SceneKit 在两点之间绘制一条线【英文标题】:SceneKit – Drawing a line between two points 【发布时间】:2014-03-20 02:55:50 【问题描述】:

我有两个 SCNVector3 类型的点(我们称它们为 pointA 和 pointB)。我想在他们之间画一条线。似乎应该很容易,但找不到方法。

我看到两个选项,都有问题:

使用半径非常小的SCNCylinder,长度为|pointA-pointB|然后定位/旋转它。

使用自定义 SCNGeometry 但不确定如何;也许必须定义两个三角形来形成一个非常薄的矩形?

似乎应该有一种更简单的方法可以做到这一点,但我似乎找不到。

编辑:使用三角形方法可以在 (0,0,0) 和 (10,10,10) 之间画一条线:

CGFloat delta = 0.1;
SCNVector3 positions[] =   SCNVector3Make(0,0,0),
    SCNVector3Make(10, 10, 10),
    SCNVector3Make(0+delta, 0+delta, 0+delta),
    SCNVector3Make(10+delta, 10+delta, 10+delta);
int indicies[] = 
    0,2,1,
    1,2,3
;

SCNGeometrySource *vertexSource = [SCNGeometrySource geometrySourceWithVertices:positions count:4];
NSData *indexData = [NSData dataWithBytes:indicies length:sizeof(indicies)];
SCNGeometryElement *element = [SCNGeometryElement geometryElementWithData:indexData primitiveType:SCNGeometryPrimitiveTypeTriangles primitiveCount:2 bytesPerIndex:sizeof(int)];
SCNGeometry *line = [SCNGeometry geometryWithSources:@[vertexSource] elements:@[element]];

SCNNode *lineNode = [SCNNode nodeWithGeometry:line];
[root addChildNode:lineNode];

但是有问题:由于法线,你只能从一侧看到这条线!从另一边是看不见的。此外,如果“delta”太小,您根本看不到这条线。事实上,从技术上讲,它是一个矩形,而不是我想要的线,如果我想绘制多条连接线,这可能会导致小的图形故障。

【问题讨论】:

这可能对你有帮助:ronnqvi.st/custom-scenekit-geometry 万岁!一个 SceneKit 问题! 说真的,SceneKit 太棒了,我不知道为什么越来越多的人不使用它。这是我的第一个 Objective-C 项目/应用程序(尽管我第二次尝试这样做),鉴于我完全没有 OpenGL 背景,SceneKit 让我的事情变得如此简单。希望它最终会被移植到 ios 上,并且会看到更多的使用...... 天哪!我什至没有时间回家回答这个问题:( 该死,我只想从 A 点到 B 点画一条 3D 线。 【参考方案1】:

这是一个简单的 Swift 扩展:

extension SCNGeometry 
    class func lineFrom(vector vector1: SCNVector3, toVector vector2: SCNVector3) -> SCNGeometry 
        let indices: [Int32] = [0, 1]

        let source = SCNGeometrySource(vertices: [vector1, vector2])
        let element = SCNGeometryElement(indices: indices, primitiveType: .Line)

        return SCNGeometry(sources: [source], elements: [element])

    

【讨论】:

您认为可以在此函数中调整线条粗细吗? 线条粗细在 2D 空间中是有意义的,但在 3D 空间中没有那么大。您可以在彼此靠近的点之间绘制多条线,但最终会得到类似于平面的东西,或者一个气缸。更简单的方法是使用 SCNPlane 或 SCNCylinder 对象来实现这一点。 您可以在 OpenGL 中调整线条粗细,但它在 iOS 上会被忽略(并且仅适用于 macOS 上的某些值)。我认为 Metal 中没有线宽调用(但我可能错了)。 我希望有一个更粗线的函数。 如何绘制多条线并填充它们?【参考方案2】:

有很多方法可以做到这一点。

如上所述,您的自定义几何方法有一些缺点。您应该能够通过为其材质赋予doubleSided 属性来纠正它从一侧不可见的问题。不过,您仍然可能会遇到二维问题。

您还可以修改自定义几何图形以包含更多三角形,从而获得具有三个或更多边的管形,而不是扁平矩形。或者只在几何源中有两个点,然后使用 SCNGeometryPrimitiveTypeLine 几何元素类型让 Scene Kit 在它们之间绘制一条线段。 (尽管使用线条绘制渲染样式时不会像使用阴影多边形那样灵活。)

您还可以使用您提到的SCNCylinder 方法(或任何其他内置原始形状)。请记住,几何图形是在它们自己的本地(又名模型)坐标空间中定义的,Scene Kit 相对于节点定义的坐标空间解释该坐标空间。换句话说,您可以定义一个在所有维度上都是 1.0 单位宽的圆柱体(或盒子、胶囊或平面或其他任何东西),然后使用包含该几何图形的 SCNNode 的旋转/缩放/位置或变换使其变长,薄,并在你想要的两点之间伸展。 (另请注意,由于您的线条会很细,您可以减少所使用的任何内置几何图形的 segmentCounts,因为这些细节将不可见。)

另一个选项是SCNShape 类,它允许您从 2D Bézier 路径创建拉伸的 3D 对象。计算出正确的变换来得到一个连接任意两个点的平面听起来像是一些有趣的数学运算,但是一旦你这样做了,你就可以轻松地将你的点与你选择的任何形状的线连接起来。

【讨论】:

SCNGeometryPrimitiveTypeLine 正是我想要的!谢谢。不知道我怎么错过了。对于其他查找此内容的人,我在下面添加了新代码。在 1.0 单位宽的圆柱体上的提示然后缩放节点也非常有用。 嗨,我对“圆柱体”解决方案或更好的形状解决方案很感兴趣。我能够创建长度等于点之间距离的圆柱体,但我无法定位它们。关于如何获得两点之间方向的任何线索? “两点之间的方向”实际上与“我站在这里,我想看看那里”是一样的——构造一个look-at matrix 并将其用作线节点的变换。当然,观察方向是沿着局部 z 轴,而 SCNCylinder 沿着局部 y 轴,所以你需要一个额外的位来进行变换——也许转动圆柱体的 pivot 所以它沿局部 z 轴移动。 改造 SCNShape 是一场噩梦。【参考方案3】:

下面从 (0, 0, 0) 到 (10, 10, 10) 的行的新代码。 我不确定它是否可以进一步改进。

SCNVector3 positions[] = 
    SCNVector3Make(0.0, 0.0, 0.0),
    SCNVector3Make(10.0, 10.0, 10.0)
;

int indices[] = 0, 1;

SCNGeometrySource *vertexSource = [SCNGeometrySource geometrySourceWithVertices:positions
                                                                          count:2];

NSData *indexData = [NSData dataWithBytes:indices
                                   length:sizeof(indices)];

SCNGeometryElement *element = [SCNGeometryElement geometryElementWithData:indexData
                                                            primitiveType:SCNGeometryPrimitiveTypeLine
                                                           primitiveCount:1
                                                            bytesPerIndex:sizeof(int)];

SCNGeometry *line = [SCNGeometry geometryWithSources:@[vertexSource]
                                            elements:@[element]];

SCNNode *lineNode = [SCNNode nodeWithGeometry:line];

[root addChildNode:lineNode];

【讨论】:

这是一条 3d 线。注意位置中的 z 坐标。【参考方案4】:

这里有一个解决方案

class func lineBetweenNodeA(nodeA: SCNNode, nodeB: SCNNode) -> SCNNode 
    let positions: [Float32] = [nodeA.position.x, nodeA.position.y, nodeA.position.z, nodeB.position.x, nodeB.position.y, nodeB.position.z]
    let positionData = NSData(bytes: positions, length: MemoryLayout<Float32>.size*positions.count)
    let indices: [Int32] = [0, 1]
    let indexData = NSData(bytes: indices, length: MemoryLayout<Int32>.size * indices.count)

    let source = SCNGeometrySource(data: positionData as Data, semantic: SCNGeometrySource.Semantic.vertex, vectorCount: indices.count, usesFloatComponents: true, componentsPerVector: 3, bytesPerComponent: MemoryLayout<Float32>.size, dataOffset: 0, dataStride: MemoryLayout<Float32>.size * 3)
    let element = SCNGeometryElement(data: indexData as Data, primitiveType: SCNGeometryPrimitiveType.line, primitiveCount: indices.count, bytesPerIndex: MemoryLayout<Int32>.size)

    let line = SCNGeometry(sources: [source], elements: [element])
    return SCNNode(geometry: line)

如果您想更新线宽或与修改绘制线的属性相关的任何内容,您需要在 SceneKit 的渲染回调中使用其中一个 openGL 调用:

func renderer(aRenderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: NSTimeInterval) 
    //Makes the lines thicker
    glLineWidth(20)

【讨论】:

如何设置线条的颜色?我试过:line.firstMaterial?.diffuse.contents = UIColor.orangeColor() 但没用。 上述方法依赖于OpenGL绘图调用。我会更新以上内容以反映您的问题 我已经反映了改变线宽,因为这是我从头顶上知道的。我确信有一种方法可以将颜色绑定到 OpenGL 上下文以进行线条绘制。你可以在那里调用它。 有没有办法为每条线提供不同的厚度,例如,如果我有 10 条线,每条线的厚度都不同?我看了很多,一种方法是在渲染节点方法中编写自己的自定义绘图逻辑?还有其他简单的方法吗? @SonuVR 因为 iOS 现在对 SceneKit 使用 Metal 而不是 OpenGL。【参考方案5】:

这是一个 swift5 版本:

func lineBetweenNodes(positionA: SCNVector3, positionB: SCNVector3, inScene: SCNScene) -> SCNNode 
    let vector = SCNVector3(positionA.x - positionB.x, positionA.y - positionB.y, positionA.z - positionB.z)
    let distance = sqrt(vector.x * vector.x + vector.y * vector.y + vector.z * vector.z)
    let midPosition = SCNVector3 (x:(positionA.x + positionB.x) / 2, y:(positionA.y + positionB.y) / 2, z:(positionA.z + positionB.z) / 2)

    let lineGeometry = SCNCylinder()
    lineGeometry.radius = 0.05
    lineGeometry.height = distance
    lineGeometry.radialSegmentCount = 5
    lineGeometry.firstMaterial!.diffuse.contents = GREEN

    let lineNode = SCNNode(geometry: lineGeometry)
    lineNode.position = midPosition
    lineNode.look (at: positionB, up: inScene.rootNode.worldUp, localFront: lineNode.worldUp)
    return lineNode

【讨论】:

这并没有提供问题的答案。您可以search for similar questions,或参考页面右侧的相关和链接问题找到答案。如果您有一个相关但不同的问题,ask a new question,并包含指向此问题的链接以帮助提供上下文。见:Ask questions, get answers, no distractions 它确实回答了“使用 SceneKit 在两点之间画一条线”的问题。所以没关系。【参考方案6】:

所以在你的 ViewController.cs 中定义你的向量点并调用一个 Draw 函数,然后在最后一行 - 它只是旋转它以查看点 b。

       var a = someVector3;
       var b = someOtherVector3;
       nfloat cLength = (nfloat)Vector3Helper.DistanceBetweenPoints(a, b);
       var cyclinderLine = CreateGeometry.DrawCylinderBetweenPoints(a, b, cLength, 0.05f, 10);
       ARView.Scene.RootNode.Add(cyclinderLine);
       cyclinderLine.Look(b, ARView.Scene.RootNode.WorldUp, cyclinderLine.WorldUp);

创建一个静态 CreateGeomery 类并将这个静态方法放在那里

        public static SCNNode DrawCylinderBetweenPoints(SCNVector3 a,SCNVector3 b, nfloat length, nfloat radius, int radialSegments)

         SCNNode cylinderNode;
         SCNCylinder cylinder = new SCNCylinder();
         cylinder.Radius = radius;
         cylinder.Height = length;
         cylinder.RadialSegmentCount = radialSegments;
         cylinderNode = SCNNode.FromGeometry(cylinder);
         cylinderNode.Position = Vector3Helper.GetMidpoint(a,b);

         return cylinderNode;
        

您可能还希望在静态帮助程序类中使用这些实用程序方法

        public static double DistanceBetweenPoints(SCNVector3 a, SCNVector3 b)
        
         SCNVector3 vector = new SCNVector3(a.X - b.X, a.Y - b.Y, a.Z - b.Z);
         return Math.Sqrt(vector.X * vector.X + vector.Y * vector.Y + vector.Z * vector.Z);
        


    public static SCNVector3 GetMidpoint(SCNVector3 a, SCNVector3 b)

        float x = (a.X + b.X) / 2;
        float y = (a.Y + b.Y) / 2;
        float z = (a.Z + b.Z) / 2;

        return new SCNVector3(x, y, z);
    

感谢我所有的 Xamarin c# 朋友。

【讨论】:

【参考方案7】:

这是一个使用独立于直线方向的三角形的解决方案。 它是使用叉积构造的,以获得垂直于线的点。所以你需要一个小的 SCNVector3 扩展,但它在其他情况下也可能会派上用场。

private func makeRect(startPoint: SCNVector3, endPoint: SCNVector3, width: Float ) -> SCNGeometry 
    let dir = (endPoint - startPoint).normalized()
    let perp = dir.cross(SCNNode.localUp) * width / 2

    let firstPoint = startPoint + perp
    let secondPoint = startPoint - perp
    let thirdPoint = endPoint + perp
    let fourthPoint = endPoint - perp
    let points = [firstPoint, secondPoint, thirdPoint, fourthPoint]

    let indices: [UInt16] = [
        1,0,2,
        1,2,3
    ]
    let geoSource = SCNGeometrySource(vertices: points)
    let geoElement = SCNGeometryElement(indices: indices, primitiveType: .triangles)

    let geo = SCNGeometry(sources: [geoSource], elements: [geoElement])
    geo.firstMaterial?.diffuse.contents = UIColor.blue.cgColor
    return geo

SCNVector3 扩展:

import Foundation
import SceneKit

extension SCNVector3

    /**
     * Returns the length (magnitude) of the vector described by the SCNVector3
     */
    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()
    

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

【讨论】:

是否有意让矩形的深度随着长度的增加而增加? edit 我用SCNVector3(x: 0, y: 0, z: 1) 替换了SCNNode.localUp 似乎由于某种原因使它正常工作。不知道为什么。 Xcode 12.3 let dir = (endPoint - startPoint).normalized() 中的两个错误。 return self / length() gist.github.com/jeremyconkin/a3909b2d3276d1b6fbff02cefecd561a

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

使用jQuery在谷歌地图上的两点之间画一条线?

在两点之间绘制 SceneKit 对象

需要在网页上的两点之间画一条线

查找 2 条折线之间的角度

在绘制的点之间绘制一条线

如何使用 plotly.graph_objects 绘制 3D 线?