27

I have two points (let's call them pointA and pointB) of type SCNVector3. I want to draw a line between them. Seems like it should be easy, but can't find a way to do it.

I see two options, both have issues:

  • Use a SCNCylinder with a small radius, with length |pointA-pointB| and then position it/rotate it.

  • Use a custom SCNGeometry but not sure how; would have to define two triangles to form a very thin rectangle perhaps?

It seems like there should be an easier way of doing this, but I can't seem to find one.

Edit: Using the triangle method gives me this for drawing a line between (0,0,0) and (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];

But there are problems: due to the normals, you can only see this line from one side! It's invisible from the other side. Also, if "delta" is too small you can't see the line at all. As it is, it's technically a rectangle, rather than the line I was going for, which might result in small graphical glitches if I want to draw multiple joined up lines.

Andy Jazz
  • 49,178
  • 17
  • 136
  • 220
Matthew
  • 1,366
  • 2
  • 15
  • 28

8 Answers8

21

Here's a simple extension in 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])

    }
}
Praveen Gowda I V
  • 9,569
  • 4
  • 41
  • 49
Jovan Stankovic
  • 4,661
  • 4
  • 27
  • 16
  • You think it'd be possible to adjust line thickness from within this function? – Gabriel Garrett Jun 30 '16 at 15:22
  • 1
    Line thickness makes sense in 2D space, but not so much in 3D.You could draw multiple lines between points that are close to each other, but you would end up with something that resembles a plane, or a cylinder. Easier way is to use SCNPlane or SCNCylinder objects to achieve this. – Jovan Stankovic Jul 01 '16 at 20:11
  • 1
    You can adjust line thickness in OpenGL but it’s ignored on iOS (and works for only some values on macOS). I don’t think there’s a line thickness call in Metal (but I might be wrong). – Wil Shipley Jul 19 '17 at 01:26
  • I hope there is a function that has a thicker line though. – Lim Thye Chean Aug 07 '17 at 12:36
  • How to draw multiple lines and fill them? – khunshan Jan 23 '18 at 10:37
  • You can't adjust thickness in Metal, but if you are using OpenGL as a backing layer you can us glLineWidth(5.0); to set your line thickness by pixel thickness. – Andrew Zimmer Sep 18 '18 at 17:28
15

There are lots of ways to do this.

As noted, your custom geometry approach has some disadvantages. You should be able to correct the problem of it being invisible from one side by giving its material the doubleSided property. You still may have issues with it being two-dimensional, though.

You could also modify your custom geometry to include more triangles, so you get a tube shape with three or more sides instead of a flat rectangle. Or just have two points in your geometry source, and use the SCNGeometryPrimitiveTypeLine geometry element type to have Scene Kit draw a line segment between them. (Though you won't get as much flexibility in rendering styles with line drawing as with shaded polygons.)

You can also use the SCNCylinder approach you mentioned (or any of the other built-in primitive shapes). Remember that geometries are defined in their own local (aka Model) coordinate space, which Scene Kit interprets relative to the coordinate space defined by a node. In other words, you can define a cylinder (or box or capsule or plane or whatever) that's 1.0 units wide in all dimensions, then use the rotation/scale/position or transform of the SCNNode containing that geometry to make it long, thin, and stretching between the two points you want. (Also note that since your line is going to be pretty thin, you can reduce the segmentCounts of whichever built-in geometry you're using, because that much detail won't be visible.)

Yet another option is the SCNShape class that lets you create an extruded 3D object from a 2D Bézier path. Working out the right transform to get a plane connecting two arbitrary points sounds like some fun math, but once you do it you could easily connect your points with any shape of line you choose.

rickster
  • 124,678
  • 26
  • 272
  • 326
  • SCNGeometryPrimitiveTypeLine was exactly what I was looking for! Thanks. Don't know how I missed it. For anyone else looking this up, I've added the new code below. The tip on a 1.0 unit wide cylinder and then scaling the node is also really useful. – Matthew Feb 19 '14 at 20:29
  • Hi, i'm intrested in the "cylinder" solution or, even better, the shape solution. I'm able to create cylinders with length equal to the distance between points, but i can't orientate them. Any clue about how to get the orientation between two points? – Dodgson86 Jan 28 '15 at 15:35
  • 1
    "Orientation between two points" is effectively the same thing as "I'm standing here and I want to look over there" — construct a [look-at matrix](https://developer.apple.com/library/ios/documentation/GLkit/Reference/GLKMatrix4/index.html#//apple_ref/c/func/GLKMatrix4MakeLookAt) and use that as the line node's transform. Of course, the look-at direction is along the local z-axis, and an `SCNCylinder` goes along the local y-axis, so you'll need an extra bit for your transform — maybe turn the cylinder's `pivot` so it goes along the local z-axis. – rickster Jan 28 '15 at 19:31
  • 1
    Transforming SCNShape is nightmare. – khunshan Jan 26 '18 at 12:40
10

New code for a line from (0, 0, 0) to (10, 10, 10) below. I'm not sure if it could be improved further.

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];
Pietro Saccardi
  • 2,602
  • 34
  • 41
Matthew
  • 1,366
  • 2
  • 15
  • 28
10

Here's one solution

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

if you would like to update the line width or anything related to modifying properties of the drawn line, you'll want to use one of the openGL calls in SceneKit's rendering callback:

func renderer(aRenderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: NSTimeInterval) {
    //Makes the lines thicker
    glLineWidth(20)
}
TheCodingArt
  • 3,436
  • 4
  • 30
  • 53
  • 2
    How can we set the color of the line? I tried: line.firstMaterial?.diffuse.contents = UIColor.orangeColor() but didn't work. – zumzum Feb 05 '15 at 18:39
  • The above method relies on OpenGL drawing calls. I'll update the above to reflect your questions – TheCodingArt Feb 05 '15 at 18:53
  • I've reflected changing the line width because that's what I knew off the top of my head. I'm sure there's a way to bind a color to the openGL context for line drawing though. You would just call that there. – TheCodingArt Feb 05 '15 at 18:59
  • Is there any way to provide different thickness for each line, for example if i have 10 lines each of different thickness? I looked a lot, one way is to write own custom drawing logic in render node method? is there any other easy way? – Chandan Shetty SP Oct 03 '16 at 05:26
  • How to you change the line thickness in Obj C ? – toto_tata Sep 28 '17 at 09:09
  • glLineWidth(20) doesnt work for some reason. any insights why ? @TheCodingArt – Alejandro Vargas Oct 02 '17 at 16:07
  • 2
    @SonuVR because iOS now uses Metal instead of OpenGL for SceneKit. – khunshan Jan 23 '18 at 10:37
8

Here is a swift5 version:

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
}
silberz
  • 105
  • 1
  • 5
  • 1
    This does not provide an answer to the question. You can [search for similar questions](//stackoverflow.com/search), or refer to the related and linked questions on the right-hand side of the page to find an answer. If you have a related but different question, [ask a new question](//stackoverflow.com/questions/ask), and include a link to this one to help provide context. See: [Ask questions, get answers, no distractions](//stackoverflow.com/tour) – Dharman Aug 30 '19 at 09:54
  • 3
    It does answer the question "Drawing a line between two points using SceneKit". So it's fine. – Softlion Oct 02 '20 at 03:54
2

So inside your ViewController.cs define your vector points and call a Draw function, then on the last line there - it's just rotating it to look at point 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);

Create a static CreateGeomery class and put this static method in there

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

you may also want these utility methods in a static helper class

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

For all my Xamarin c# homies out there.

Andrew
  • 121
  • 8
2

Here's a solution using triangles that works independent of the direction of the line. It's constructed using the cross product to get points perpendicular to the line. So you'll need a small SCNVector3 extension, but it'll probably come in handy in other cases, too.

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)
    }
}
Jaykob
  • 323
  • 2
  • 10
  • Is it intentional that the depth of the rectangle increases as it's length increases? – zakdances Sep 19 '19 at 06:02
  • **edit** I replaced `SCNNode.localUp` with `SCNVector3(x: 0, y: 0, z: 1)` seems to make this work correctly for some reason. Not sure why. – zakdances Sep 19 '19 at 06:33
  • Two errors in Xcode 12.3 `let dir = (endPoint - startPoint).normalized()`. <-- "'Binary operator '-' cannot be applied to two 'SCNVector3' operands'". AND `return self / length()` <-- "Binary operator '/' cannot be applied to operands of type 'SCNVector3' and 'Float'" – The Way Feb 23 '21 at 16:13
  • https://gist.github.com/jeremyconkin/a3909b2d3276d1b6fbff02cefecd561a – Silvering Apr 02 '21 at 14:49
2

enter image description here

Swift version

To generate a line in a form of cylinder with a certain position and an orientation, let's implement the SCNGeometry extension with a cylinderLine() class method inside. The toughest part here is a trigonometry (for defining cylinder's direction). Here it is:

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

The rest is easy.

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)
    }
}
Andy Jazz
  • 49,178
  • 17
  • 136
  • 220