SceneKit - 在两点之间绘制一条线

27
我有两个类型为SCNVector3的点(称为pointA和pointB)。我想在它们之间画一条线。这似乎很简单,但找不到方法来实现它。
我看到有两种选择,但都有问题:
1. 使用带有小半径的SCNCylinder,长度为| pointA-pointB |,然后将其定位/旋转。 2. 使用自定义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”太小,你根本看不到这条线。目前为止,它在技术上是一个矩形,而不是我想要的线,这可能会导致小的图形故障,如果我想画多个连接线。


1
这可能会对您有所帮助:http://ronnqvi.st/custom-scenekit-geometry/ - A J
6
太好了!一个SceneKit的问题! - David Rönnqvist
说真的,SceneKit非常棒,我不知道为什么更多的人不使用它。这是我第一个Objective-C项目/应用程序(尽管是第二次尝试),鉴于我完全没有OpenGL背景,SceneKit让我的工作变得非常容易。希望它最终能够移植到iOS上,并且会看到更多的使用... - Matthew
哎呀!我甚至都没时间回家回答这个问题 :( - David Rönnqvist
4
该死,我想做的就是从点A到点B画一条3D线。 - μολὼν.λαβέ
8个回答

21

这是一个简单的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])

    }
}

你觉得能否在这个函数内部调整线条的粗细? - Gabriel Garrett
1
线条粗细在二维空间中很有意义,但在三维空间中就没那么实用了。你可以在靠近的点之间画多条线,但最终结果可能类似于一个平面或圆柱体。更简单的方法是使用SCNPlane或SCNCylinder对象来实现此目的。 - Jovan Stankovic
1
你可以在OpenGL中调整线条的粗细,但在iOS上会被忽略(并且在macOS上仅适用于某些值)。我不认为Metal中有线条粗细的调用(但我可能是错的)。 - Wil Shipley
我希望有一个函数可以画出更粗的线条。 - Lim Thye Chean
如何绘制多条线并填充它们? - khunshan
在Metal中无法调整线条的粗细,但如果您使用OpenGL作为后备层,则可以使用glLineWidth(5.0);按像素粗细设置线条粗细。 - Andrew Zimmer

15

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

注意,您的自定义几何方法存在一些缺点。您应该能够通过给其材质设置 doubleSided属性来解决它从一侧不可见的问题。但是,您仍可能遇到它是二维形状的问题。

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

您还可以使用您提到的 SCNCylinder 方法(或任何其他内置基本形状)。请记住,几何体是在它们自己的本地坐标空间中定义的,Scene Kit 将其相对于节点定义的坐标空间进行解释。换句话说,您可以定义一个在所有尺寸上宽为 1.0 的圆柱体(或盒子、胶囊体、平面或其他任何形状),然后使用包含该几何体的 SCNNode 的旋转、缩放、位置或变换将其变长、细长,并延伸到您想要的两个点之间。(还要注意,由于您的线条会非常细,因此您可以降低使用的任何内置几何体的 segmentCount,因为这么多的细节是看不到的。)

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


SCNGeometryPrimitiveTypeLine正是我一直在寻找的!谢谢。不知道我怎么错过了它。对于其他正在查找此内容的人,我已经添加了下面的新代码。关于使用1.0单位宽度的圆柱体进行缩放的提示也非常有用。 - Matthew
嗨,我对“圆柱体”解决方案或更好的“形状”解决方案感兴趣。我能够创建长度等于两点之间距离的圆柱体,但我无法确定它们的方向。你有关于如何获取两点之间方向的线索吗? - Dodgson86
1
“两点之间的方向” 实际上就是 “我站在这里,想看那边”,需要构建一个观察矩阵,并将其用作线节点的变换。当然,观察方向是沿着本地 z 轴的,而 SCNCylinder 沿着本地 y 轴,所以您需要为变换增加额外的部分,也许要转动圆柱体的 pivot 使其沿着本地 z 轴运动。 - rickster
1
转换SCNShape是一场噩梦。 - khunshan

10

这里有一个解决方案

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

2
我们如何设置线条的颜色?我尝试过:line.firstMaterial?.diffuse.contents = UIColor.orangeColor(),但没有起作用。 - zumzum
有没有办法为每条线提供不同的线条粗细,例如如果我有10条线,每条线都有不同的粗细?我找了很多资料,一种方法是在渲染节点方法中编写自己的自定义绘图逻辑?还有其他简单的方法吗? - Chandan Shetty SP
如何在Obj C中更改线条粗细? - toto_tata
glLineWidth(20)出现问题,有何见解?@TheCodingArt - Alejandro Vargas
2
@SonuVR 因为 iOS 现在使用 Metal 而不是 OpenGL 作为 SceneKit 的渲染引擎。 - khunshan
显示剩余2条评论

10

以下是从(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];

这是一条三维线。请注意位置中的z坐标。 - coneybeare

8

以下是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
}

1
这并没有提供问题的答案。您可以搜索类似的问题,或者参考页面右侧的相关和链接问题来找到答案。如果您有一个相关但不同的问题,请提出新问题,并包含此问题的链接以帮助提供上下文。请参阅:提问,获取答案,无干扰 - Dharman
3
可以,这个回答解答了“如何在SceneKit中连接两个点并画出一条线”的问题。所以没问题。 - Softlion

3

enter image description here

Swift版本

为了生成一个带有特定位置和方向的圆柱形状的线条,我们将在SCNGeometry扩展中实现一个cylinderLine()类方法。这里最困难的部分是三角函数(用于定义圆柱体的方向)。下面是代码:

import SceneKit

extension SCNGeometry {
    
    class func cylinderLine(from: SCNVector3, to: SCNVector3,
                                              segments: Int = 5) -> SCNNode {
        let x1 = from.x; let x2 = to.x
        let y1 = from.y; let y2 = to.y
        let z1 = from.z; let z2 = to.z
        
        let subExpr01 = Float((x2-x1) * (x2-x1))
        let subExpr02 = Float((y2-y1) * (y2-y1))
        let subExpr03 = Float((z2-z1) * (z2-z1))
        
        let distance = CGFloat(sqrtf(subExpr01 + subExpr02 + subExpr03))
        
        let cylinder = SCNCylinder(radius: 0.005, height: CGFloat(distance))
        cylinder.radialSegmentCount = segments
        cylinder.firstMaterial?.diffuse.contents = NSColor.systemYellow
        
        let lineNode = SCNNode(geometry: cylinder)
        
        lineNode.position = SCNVector3((x1+x2)/2, (y1+y2)/2, (z1+z2)/2)
        
        lineNode.eulerAngles = SCNVector3(x: CGFloat.pi / 2,
                                      y: acos((to.z-from.z)/CGFloat(distance)),
                                      z: atan2((to.y-from.y), (to.x-from.x)))
        return lineNode
    }
}

剩下的很容易。
class ViewController: NSViewController {
    
    @IBOutlet var sceneView: SCNView!
    let scene = SCNScene()
    var startingPoint: SCNVector3!
    var endingPoint: SCNVector3!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        sceneView.scene = scene
        sceneView.backgroundColor = NSColor.black
        sceneView.allowsCameraControl = true                
        self.startingPoint = SCNVector3Zero
        self.endingPoint = SCNVector3(1,1,1)
        self.lineInBetween()
    }   
    func lineInBetween() {
        self.addSphereDot(position: startingPoint)
        self.addSphereDot(position: endingPoint)
        self.addLine(start: startingPoint, end: endingPoint)
    }   
    func addSphereDot(position: SCNVector3) {
        let sphere = SCNSphere(radius: 0.03)
        sphere.firstMaterial?.diffuse.contents = NSColor.red
        let node = SCNNode(geometry: sphere)
        node.position = position
        scene.rootNode.addChildNode(node)
    }   
    func addLine(start: SCNVector3, end: SCNVector3) {
        let lineNode = SCNGeometry.cylinderLine(from: start, to: end)
        scene.rootNode.addChildNode(lineNode)
    }
}

2

在您的ViewController.cs文件中定义您的向量点并调用绘制函数,然后在最后一行代码中,将其旋转以查看点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#开发者。


2

这是一种使用三角形解决方案,可以独立于线条的方向而工作。它使用叉积得到与该线垂直的点。因此,您需要一个小的SCNVector3扩展程序,但在其他情况下也可能会派上用场。

这里有一个使用三角形的解决方案,可以独立于线条的方向而工作。它使用叉积来获取与该线垂直的点。因此,您需要一个小的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 extension:

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

矩形的深度随着长度的增加而增加,这是有意为之吗? - zakdances
我将 SCNNode.localUp 替换为 SCNVector3(x: 0, y: 0, z: 1),看起来能够正确地工作,但不确定原因。 - zakdances
Xcode 12.3 中有两个错误:let dir = (endPoint - startPoint).normalized()。错误信息为“'Binary operator '-' cannot be applied to two 'SCNVector3' operands'”。以及 return self / length(),错误信息为“Binary operator '/' cannot be applied to operands of type 'SCNVector3' and 'Float'”。 - The Way
https://gist.github.com/jeremyconkin/a3909b2d3276d1b6fbff02cefecd561a - Silvering

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