Search code examples
swiftscenekit

How to bend a SCNShape “line”?


I’m trying to create a lighting bolt using scenekit and I’m following this guide. So far I’ve got a vertical line in my scene using UIBezierPath with an extrusion to make it 3d but I’m not sure how to bend the “line” at the midpoint as described in the link.

func createBolt() {
     let path = UIBezierPath()
     path.move(to: CGPoint(x: 0, y: 0))
     path.addLine(to: CGPoint(x: 0, y: 1))
     path.close()


    let shape = SCNShape(path: path, extrusionDepth 0.2)
    let color = UIColor.red
    shape.firstMaterial?.diffuse.contents = color


    let boltNode = SCNNode(geometry: shape)
    boltNode.position.z = 0
    sceneView.scene.rootNode.addChildNode(boltNode)
}

Solution

  • Algorithm is pretty straightforward:
    You start with list of 1 segment from A to B, then on each generation you split each segment on 2 segments by shifting middle point on random offset on his norm

    struct Segment {
        let start: CGPoint
        let end: CGPoint
    }
    
    /// Calculate norm of 2d vector
    func norm(_ v: CGPoint) -> CGPoint {
        let d = max(sqrt(v.x * v.x + v.y * v.y), 0.0001)
        return CGPoint(x: v.x / d, y: v.y / -d)
    }
    
    /// Splitting segment on 2 segments with middle point be shifted by `offset` on norm
    func split(_ segment: Segment, by offset: CGFloat) -> [Segment] {
        var midPoint = (segment.start + segment.end) / 2
        midPoint = norm(segment.end - segment.start) * offset + midPoint
        return [
            Segment(start: segment.start, end: midPoint),
            Segment(start: midPoint, end: segment.end)
        ]
    }
    
    /// Generate bolt-like line from `start` to `end` with maximal started frequence of `maxOffset`
    /// and `generation` of split loops
    func generate(from start: CGPoint, to end: CGPoint, withOffset maxOffset: CGFloat, generations: Int = 6) -> UIBezierPath {
        var segments = [Segment(start: start, end: end)]
        var offset = maxOffset
        for _ in 0 ..< generations {
            segments = segments.flatMap { split($0, by: CGFloat.random(in: -offset...offset)) }
            offset /= 2
        }
        let path = UIBezierPath()
        path.move(to: start)
        segments.forEach { path.addLine(to: $0.end) }
        return path
    }
    
    // MARK: - Example
    let start = CGPoint(x: 10, y: 10)
    let end = CGPoint(x: 90, y: 90)
    let path = generate(from: start, to: end, withOffset: 30, generations: 5)
    
    // MARK: - Helpers
    func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
        return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
    }
    func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
        return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
    }
    func / (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
        return CGPoint(x: lhs.x / rhs, y: lhs.y / rhs)
    }
    func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
        return CGPoint(x: lhs.x * rhs, y: lhs.y * rhs)
    }
    

    enter image description here