SceneKit make Lightning Bolts
This here gives you another approach how to create randomized, full 3D Lightning Bolts in SceneKit (thank you Harry!).
Create a new SceneKit project (for iOS) in Xcode using the default Game template - the one that shows the aircraft in 3D space - delete the aircraft and create an empty scene with a black background. Also globally define your sceneView
(to be able to access it from other classes).
Add the following classes and extensions to a new Swift file (import SceneKit
):
Classes
class LightningStrike:
class LightningStrike : Geometry {
var bolt:[Lightning] = []
var start = SCNVector3() // stores start position of the Bolt
var end = SCNVector3() // stores end position of the Bolt
static var delayTime = 0.0
override init() {
start = SCNVector3(0.0, +5.0, 0.0) // default, to be changed
end = SCNVector3(0.0, -5.0, 0.0) // default, to be changed
print("Lightning Strike initialized")
}
private func fadeOutBolt() {
for b in bolt {
SCNTransaction.begin()
SCNTransaction.animationDuration = 2.0
b.face.geometry?.firstMaterial?.transparency = 0.0
SCNTransaction.commit()
}
}
func strike() {
for b in bolt { b.face.removeFromParentNode() }
bolt.removeAll()
// Create Main Bolt
bolt.append(Lightning())
bolt[0].createBolt(start,end)
sceneView.scene?.rootNode.addChildNode(bolt[0].face)
// Create child Bolts
for _ in 0 ..< 15 { // number of child bolts
// let parent = Int.random(in: 0 ..< bolt.count) // random parent bolt, an other method
let parent : Int = 0
let start = bolt[parent].centerLine[10 + Int.random(in: 0 ..< 15)] // random node to start from off of parent, pay attention to: numSegments - changing numbers here can cause out of index crash
let length:SCNVector3 = bolt[parent].end.minus(start) // length from our start to end of parent
var end = SCNVector3()
end.x = start.x + length.x / 1.5 + Float.random(in: 0 ... abs(length.x) / 3) // adjust by playing with this numbers
end.y = start.y + length.y / 1.5 + Float.random(in: 0 ... abs(length.y) / 3) // adjust by playing with this numbers
end.z = start.z + length.z / 1.5 + Float.random(in: 0 ... abs(length.z) / 3) // adjust by playing with this numbers
bolt.append(Lightning())
let index = bolt.count-1
bolt[index].width = bolt[parent].width * 0.2
bolt[index].deviation = bolt[parent].deviation * 0.3
bolt[index].createBolt(start,end)
sceneView.scene?.rootNode.addChildNode(bolt[0].face)
}
// Reset delay time and schedule fadeOut
LightningStrike.delayTime = 0.0 // reset delay time
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { self.fadeOutBolt() }
// Here you can add a Sound Effect
}
deinit {
for b in bolt { b.face.removeFromParentNode() }
bolt.removeAll()
print("Lightning Strike deinitialized")
}
}
class Lightning:
class Lightning : Geometry {
let UNASSIGNED:Float = 999
var start = SCNVector3()
var end = SCNVector3()
var numSegments = Int() // use => 3,5,9,17,33,65
var width = Float()
var deviation = Float()
var vertices:[SCNVector3] = []
var normals:[SCNVector3] = []
var indices:[Int32] = []
var centerLine:[SCNVector3] = []
var face:SCNNode! = nil
override init() {
numSegments = 33 // 17
width = 0.1
deviation = 1.5
centerLine = Array(repeating: SCNVector3(), count: numSegments)
// indexed indices never change
var j:Int = 0
for i in 0 ..< numSegments-1 {
j = i * 3
indices.append(Int32(j + 0)) // 2 triangles on side #1
indices.append(Int32(j + 2))
indices.append(Int32(j + 3))
indices.append(Int32(j + 2))
indices.append(Int32(j + 5))
indices.append(Int32(j + 3))
indices.append(Int32(j + 2)) // side #2
indices.append(Int32(j + 1))
indices.append(Int32(j + 5))
indices.append(Int32(j + 1))
indices.append(Int32(j + 4))
indices.append(Int32(j + 5))
indices.append(Int32(j + 1)) // side #3
indices.append(Int32(j + 0))
indices.append(Int32(j + 4))
indices.append(Int32(j + 0))
indices.append(Int32(j + 3))
indices.append(Int32(j + 4))
}
}
func createNode() -> SCNGeometry {
for i in 0 ..< numSegments { centerLine[i].x = UNASSIGNED }
centerLine[0] = start
centerLine[numSegments-1] = end
var hop:Int = max(numSegments / 2,1)
var currentDeviation = deviation
while true {
for i in stride(from:0, to: numSegments, by:hop) {
if centerLine[i].x != UNASSIGNED { continue }
let p1 = centerLine[i-hop]
let p2 = centerLine[i+hop]
centerLine[i] = SCNVector3(
(p1.x + p2.x)/2 + Float.random(in: -currentDeviation ... currentDeviation),
(p1.y + p2.y)/2 + Float.random(in: -currentDeviation ... currentDeviation),
(p1.z + p2.z)/2 + Float.random(in: -currentDeviation ... currentDeviation))
}
if hop == 1 { break }
hop /= 2
currentDeviation *= 0.6
}
vertices.removeAll()
normals.removeAll()
// triangle of vertices at each centerLine node on XZ plane
let ss:[Float] = [ sin(0), sin(Float.pi * 2/3), sin(Float.pi * 4/3)]
let cc:[Float] = [ cos(0), cos(Float.pi * 2/3), cos(Float.pi * 4/3)]
var w = width
for i in 0 ..< numSegments {
for j in 0 ..< 3 {
vertices.append(SCNVector3(centerLine[i].x + cc[j] * w, centerLine[i].y, centerLine[i].z + ss[j] * w))
}
w *= 0.90 // bolt gets thinner towards endings
}
// normal for each vertex: position vs. position of neighbor on next node
var index1 = Int()
var index2 = Int()
func norm(_ v: SCNVector3) -> SCNVector3 {
let d = max(sqrt(v.x * v.x + v.y * v.y + v.z * v.z), 0.0001)
return SCNVector3(v.x / d, v.y / -d, v.z / d)
}
for i in 0 ..< numSegments {
for j in 0 ..< 3 {
index1 = i * 3 + j // point on current node
index2 = index1 + 3 // neighboring point on next node
if index2 >= vertices.count { index2 -= 6 } // last node references previous node instead
normals.append(norm(vertices[index1].minus(vertices[index2])))
}
}
let geoBolt = self.createGeometry(
vertices: vertices,
normals: normals,
indices: indices,
primitiveType: SCNGeometryPrimitiveType.triangles)
let boltMaterial : SCNMaterial = {
let material = SCNMaterial()
material.name = "bolt"
material.diffuse.contents = UIColor.init(hex: "#BAB1FFFF") // this is a very clear, almost white purple
material.roughness.contents = 1.0
material.emission.contents = UIColor.init(hex: "#BAB1FFFF") // this is a very clear, almost white purple
material.lightingModel = .physicallyBased
material.isDoubleSided = true
material.transparency = 0.0
return material
}()
geoBolt.firstMaterial = boltMaterial
// this makes the bolt not appearing all geometry at the same time - it's an animation effect
DispatchQueue.main.asyncAfter(deadline: .now() + LightningStrike.delayTime) {
boltMaterial.transparency = 1.0
}
LightningStrike.delayTime += 0.01665
// geoBolt.subdivisionLevel = 1 // give it a try or not...
return geoBolt
}
// Creates a Branch of the entire Bolt
func createBolt(_ nstart:SCNVector3, _ nend:SCNVector3) {
start = nstart
end = nend
face = SCNNode(geometry:createNode())
// This will add some glow around the Bolt,
// but it is **enourmous** performence and memory intense,
// you could try to add some SCNTechnique instead
// let gaussianBlur = CIFilter(name: "CIGaussianBlur")
// gaussianBlur?.name = "blur"
// gaussianBlur?.setValue(2, forKey: "inputRadius")
// face.filters = [gaussianBlur] as? [CIFilter]
sceneView.scene?.rootNode.addChildNode(face)
}
}
class Geometry:
class Geometry : NSObject {
internal func createGeometry(
vertices:[SCNVector3],
normals:[SCNVector3],
indices:[Int32],
primitiveType:SCNGeometryPrimitiveType) -> SCNGeometry
{
// Computed property that indicates the number of primitives to create based on primitive type
var primitiveCount:Int {
get {
switch primitiveType {
case SCNGeometryPrimitiveType.line:
return indices.count / 2
case SCNGeometryPrimitiveType.point:
return indices.count
case SCNGeometryPrimitiveType.triangles,
SCNGeometryPrimitiveType.triangleStrip:
return indices.count / 3
default : return 0
}
}
}
//------------------------
let vdata = NSData(bytes: vertices, length: MemoryLayout<SCNVector3>.size * vertices.count)
let vertexSource = SCNGeometrySource(
data: vdata as Data,
semantic: SCNGeometrySource.Semantic.vertex,
vectorCount: vertices.count,
usesFloatComponents: true,
componentsPerVector: 3,
bytesPerComponent: MemoryLayout<Float>.size,
dataOffset: 0,
dataStride: MemoryLayout<SCNVector3>.size)
//------------------------
let ndata = NSData(bytes: normals, length: MemoryLayout<SCNVector3>.size * normals.count)
let normalSource = SCNGeometrySource(
data: ndata as Data,
semantic: SCNGeometrySource.Semantic.normal,
vectorCount: normals.count,
usesFloatComponents: true,
componentsPerVector: 3,
bytesPerComponent: MemoryLayout<Float>.size,
dataOffset: 0,
dataStride: MemoryLayout<SCNVector3>.size)
let indexData = NSData(bytes: indices, length: MemoryLayout<Int32>.size * indices.count)
let element = SCNGeometryElement(
data: indexData as Data, primitiveType: primitiveType,
primitiveCount: primitiveCount, bytesPerIndex: MemoryLayout<Int32>.size)
return SCNGeometry(sources: [vertexSource, normalSource], elements: [element])
}
}
Extensions:
for SCNVector3:
extension SCNVector3
{
func length() -> Float { return sqrtf(x*x + y*y + z*z) }
func minus(_ other:SCNVector3) -> SCNVector3 { return SCNVector3(x - other.x, y - other.y, z - other.z) }
func normalized() -> SCNVector3 {
let len = length()
var ans = SCNVector3()
ans.x = self.x / len
ans.y = self.y / len
ans.z = self.z / len
return ans
}
}
for UIColor:
extension UIColor {
public convenience init?(hex: String) {
let r, g, b, a: CGFloat
if hex.hasPrefix("#") {
let start = hex.index(hex.startIndex, offsetBy: 1)
let hexColor = String(hex[start...])
if hexColor.count == 8 {
let scanner = Scanner(string: hexColor)
var hexNumber: UInt64 = 0
if scanner.scanHexInt64(&hexNumber) {
r = CGFloat((hexNumber & 0xff000000) >> 24) / 255
g = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255
b = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255
a = CGFloat(hexNumber & 0x000000ff) / 255
self.init(red: r, green: g, blue: b, alpha: a)
return
}
}
}
return nil
}
}
Usage:
Init the class in your ViewController like so:
let lightningStrike = LightningStrike()
Also add a tap gesture recogniser (in viewDidLoad) for easy testing:
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
sceneView.addGestureRecognizer(tapGesture)
And the corresponding function that will trigger the Lightning Bolt:
@objc func handleTap(_ gestureRecognize: UIGestureRecognizer) {
lightningStrike.strike() // will fire a Lighting Bolt
}
Results:




Have fun with it.