In Swift, suppose I have an SCNVector3 variable called p and I want to be able to set p by the following code:
p = [a, b, c]
no matter [a, b, c] is either a [Float] or a [CGFloat].
How can I do that?
You can't make it generic, because you do need Float values to initialize SCNVector3, but you can unite a number of types with a protocol.
Here I made SCNVector3 ExpressibleByArrayLiteral with array elements that conform to protocol ConvertibleToFloat. That protocol handles the ability to covert the value to Float. I've implemented it for Float, CGFloat, Double, and Int and it can be extended to other types as needed:
import UIKit
import SceneKit
public protocol ConvertibleToFloat {
var floatValue: Float { get }
}
extension Float: ConvertibleToFloat {
public var floatValue: Float { return self }
}
extension CGFloat: ConvertibleToFloat {
public var floatValue: Float { return Float(self) }
}
extension Double: ConvertibleToFloat {
public var floatValue: Float { return Float(self) }
}
extension Int: ConvertibleToFloat {
public var floatValue: Float { return Float(self) }
}
extension SCNVector3: ExpressibleByArrayLiteral {
public typealias ArrayLiteralElement = ConvertibleToFloat
public init(arrayLiteral: ConvertibleToFloat...) {
let values = arrayLiteral.map { $0.floatValue } + [0, 0, 0]
self.init(x: values[0], y: values[1], z: values[2])
}
}
Testing it out:
var sv = SCNVector3(0, 0, 0)
// Initialize with array literal of CGFloat
sv = [CGFloat(1.1), CGFloat(2.2), CGFloat(3.3)]
print(sv) // SCNVector3(x: 1.1, y: 2.2, z: 3.3)
// Initialize with array literal of Float
sv = [Float(1.2), Float(2.3), Float(3.4)]
print(sv) // SCNVector3(x: 1.2, y: 2.3, z: 3.4)
// Not enough values, rest default to 0
sv = [CGFloat(3)]
print(sv) // SCNVector3(x: 3.0, y: 0.0, z: 0.0)
// Initialize with empty array
sv = []
print(sv) // SCNVector3(x: 0.0, y: 0.0, z: 0.0)
// Initilize with [Double]
sv = [1.4, 2.6, 3.7]
print(sv) // SCNVector3(x: 1.4, y: 2.6, z: 3.7)
// Initialize with [Int]
sv = [8, 9, 10]
print(sv) // SCNVector3(x: 8.0, y: 9.0, z: 10.0)
// As in the question
let a: CGFloat = 3.1
let b: CGFloat = 4.2
let c: CGFloat = 5.3
sv = [a, b, c]
print(sv) // SCNVector3(x: 3.1, y: 4.2, z: 5.3)
My little extension from #vacawama ‘s code:
import UIKit
import SceneKit
// MARK: - RealNumber (protocol)
public protocol RealNumber {
var realValue: Float { get }
}
// MARK: - RealNumber (operations)
// -a (additive inverse)
public prefix func - (a:RealNumber) -> Float {
return -a.realValue
}
// a + b (addition)
public func + <U:RealNumber, V:RealNumber>(a:U, b:V) -> Float {
return a.realValue + b.realValue
}
// a - b (substraction)
public func - <U:RealNumber, V:RealNumber>(a:U, b:V) -> Float {
return a.realValue - b.realValue
}
// a * b (multiplication)
public func * <U:RealNumber, V:RealNumber>(a:U, b:V) -> Float {
return a.realValue * b.realValue
}
// a / b (division)
public func / <U:RealNumber, V:RealNumber>(a:U, b:V) -> Float {
return a.realValue / b.realValue
}
// MARK: - RealNumber (conforming types)
extension Float: RealNumber { public var realValue: Float { return self } }
extension CGFloat: RealNumber { public var realValue: Float { return Float(self) } }
extension Double: RealNumber { public var realValue: Float { return Float(self) } }
extension Int: RealNumber { public var realValue: Float { return Float(self) } }
// MARK: - test RealNumber
let c = CGFloat(3) // CGFloat
let f = Float(2) // Float
let d = 1.5 // Double
let i = 4 // Int
// test operations
i + d // 5.5
c - f // 1.0
i * d // 6.0
i / d // 2.667
// MARK: - RealVector (protocol)
public protocol RealVector: ExpressibleByArrayLiteral {
// must have dimension (of the vector space)
static var dimension: Int { get }
// can get/set coords of the vector
var coordinates: [Float] { get set }
// must have default init
init()
// can init with [RealNumber]
init(arrayLiteral: RealNumber...)
// must have vector addition, additive inverse, and scalar multiplication
static func + (u:Self, v:Self) -> Self // vector addition
static prefix func - (u:Self) -> Self // additive inverse
static func * (a:RealNumber, v:Self) -> Self // scalar multiplication
// can get/set coordinate by subscript index
subscript(i:Int) -> Float { get set }
}
// MARK: - RealVector (default behaviors)
extension RealVector {
// no default get/set coordinates
// you have to implement them yourself ‼️
// default subscript implementation
public subscript(i:Int) -> Float {
get { return coordinates[i] }
set {
var coords = self.coordinates
coords[i] = newValue
self.coordinates = coords
}
}
// default u + v (vector addition)
public static func + (u:Self, v:Self) -> Self {
var coords = (0...(Self.dimension - 1)).map { u[$0] + v[$0] }
var vec = Self.init()
vec.coordinates = coords
return vec
}
// default u + a (convenient vector addition)
public static func + (u:Self, a:RealNumber) -> Self {
var coords = (0...(Self.dimension - 1)).map { u[$0] + a.realValue }
var vec = Self.init()
vec.coordinates = coords
return vec
}
// default -v (additive inverse)
static public prefix func - (u:Self) -> Self {
var coords = (0...(Self.dimension - 1)).map { -u[$0] }
var vec = Self.init()
vec.coordinates = coords
return vec
}
// default * (scalar multiplication)
public static func * (a:RealNumber, v:Self) -> Self {
var coords = (0...(Self.dimension - 1)).map { a.realValue * v[$0] }
var vec = Self.init()
vec.coordinates = coords
return vec
}
// default / (scalar division)
public static func / (v:Self, a:RealNumber) -> Self {
return (1/a.realValue) * v
}
// other default operations ( u-v, v*a, u+=v, u-=v, u*=a, ... )
public static func - (u:Self, v:Self) -> Self { return u + (-v) }
public static func - (u:Self, a:RealNumber) -> Self { return u + (-a) }
public static func * (v:Self, a:RealNumber) -> Self { return a * v }
public static func += (u:inout Self, v:Self) { u = u + v }
public static func -= (u:inout Self, v:Self) { u = u - v }
public static func += (u:inout Self, a:RealNumber) { u = u + a }
public static func -= (u:inout Self, a:RealNumber) { u = u - a }
public static func *= (u:inout Self, a:RealNumber) { u = u * a }
public static func /= (u:inout Self, a:RealNumber) { u = u / a }
// default init with [RealNumber]
public init(arrayLiteral elements: RealNumber...) {
var coords = elements.map { $0.realValue } + [Float](repeating: 0, count: Self.dimension)
self.init()
self.coordinates = coords
}
// default init with another RealVector
public init<V:RealVector>(_ v:V) {
self.init()
self.coordinates = v.coordinates
}
}
// vector operations of two (different types of) RealVector
// U + V -> U
public func + <U:RealVector, V:RealVector>(u:U, v:V) -> U {
return u + U.init(v)
}
// U - V -> U
public func - <U:RealVector, V:RealVector>(u:U, v:V) -> U {
return u - U.init(v)
}
// MARK: - RealVector (conforming types)
// SCNVector3
extension SCNVector3: RealVector {
// 3-dimensional
public static var dimension: Int { return 3 }
// custom get/set coordinates
public var coordinates: [Float] {
get { return [x, y, z] }
set {
var coords = newValue + [0,0,0]
x = coords[0]
y = coords[1]
z = coords[2]
}
}
}// end: SCNVector3: RealVector
// CGPoint
extension CGPoint: RealVector {
// 2-dimensional
public static var dimension: Int { return 2 }
// custom get/set coordinates
public var coordinates: [Float] {
get { return [x, y].map { $0.realValue } }
set {
var coords = newValue + [0,0]
x = CGFloat(coords[0])
y = CGFloat(coords[1])
}
}
}// end: CGPoint
// CGVector
extension CGVector: RealVector {
// 2-dimensional
public static var dimension: Int { return 2 }
// custom get/set coordinates
public var coordinates: [Float] {
get { return [dx, dy].map { $0.realValue } }
set {
var coords = newValue + [0,0]
dx = CGFloat(coords[0])
dy = CGFloat(coords[1])
}
}
}// end: CGVector
// CGSize
extension CGSize: RealVector {
// 2-dimensional
public static var dimension: Int { return 2 }
// custiom get/set coordinates
public var coordinates: [Float] {
get { return [width, height].map { $0.realValue } }
set {
var coords = newValue + [0,0]
width = CGFloat(coords[0])
height = CGFloat(coords[1])
}
}
}
// SCNVector4
extension SCNVector4: RealVector {
// 4-dimensional
public static var dimension: Int { return 4 }
// custom get/set coordinates
public var coordinates: [Float] {
get { return [x, y, z, w] }
set {
var coords = newValue + [0,0,0,0]
x = coords[0]
y = coords[1]
z = coords[2]
w = coords[3]
}
}
}// end: SCNVector4
// CGRect
extension CGRect: RealVector {
// 4-dimensional
public static var dimension: Int { return 4}
// custom get/set coordinates
public var coordinates: [Float] {
get {
return [origin.x, origin.y, width, height].map { $0.realValue }
}
set {
var v = newValue + [0,0,0,0]
origin = [v[0], v[1]]
size = [v[2], v[3]]
}
}
}// end: CGRect
// MARK: - test RealVector
var p: CGPoint = [5, 6]
p.coordinates
CGPoint.dimension
// test init
p = CGPoint(SCNVector3(1,2,3)) // init with SCNVector3
p = [CGFloat(1.1), CGFloat(2.2), CGFloat(3.3)] // init with [CGFloat]
p = [Float(1.2), Float(2.3), Float(3.4)] // init with [Float]
p = [1.2, 3.4, 5.6] // init with [Double]
p = [1, 2, 3] // init with [Int]
p = [3] // Not enough values (rest default to 0)
p = [] // init with empty array
// test vector subscript
p[1] // get: 0.0
p[1] = 3 // set: 3.0
p // (0.0, 3.0)
// test vector operations
let u: SCNVector3 = [1,2,3]
let v: SCNVector3 = [4,5,6]
var w = SCNVector3(p) // init with CGPoint
w = -u
w = u + [1,2,3]
w = u + v
w = u - v
w = 2 * u
w = u * 2
w = u / 2
w += 0.5 // (1.0, 1.5, 2.0)
w -= 2 // (-1.0, -0.5, 0)
w *= 4 // (-4, -2, 0)
w /= 2 // (-2, -1, 0)
w += [7,8,9]// (5, 7, 9)
// vector linear combinations
w = 4*u - 3*v // (-8, -7, -6)
// vector operations of two different conforming types
w + p // (-8, -4, -6)
w - p // (-8, -10, -6)
Related
I'm getting crashes from a fixed size array even though I thought I'd protected the index from going out of range. The array updates in a loop, like a ring buffer.
This crashes reliably when the array size is 1000, but I've not managed to get it to crash when it's 500 or fewer.
EDIT: It has now crashed with an array size of 500, so the previous statement is no longer true.
I'm wondering whether this is compiler 'optimisation'... and what to do if it is. Any ideas gratefully received.
struct ZeroCrossing {
//The integer index of the second sample (cross over is between two samples)
var index: UInt
//The highest amplitude peak (negative or positive) between this and the previous crossing
let previousPeak: Float
//The interpolated crossover point between the two sample indices
let indexWithOffset: Double
}
class CrossingBuffer {
private var array: [ZeroCrossing]
private let size: Int
private var nextWriteIndex = 0
private var full: Bool {
nextWriteIndex >= size
}
init(size: Int) {
self.size = size
array = [ZeroCrossing](repeating: ZeroCrossing(index: 0, previousPeak: 0, indexWithOffset: 0), count: size)
array.reserveCapacity(size)
}
public func write(_ val: ZeroCrossing) {
array[nextWriteIndex % size] = val
nextWriteIndex += 1
}
public func getAfterIndex(_ refIndex: Double) -> [ZeroCrossing]? {
if !full { return nil }
var subArray = [ZeroCrossing]()
let lastElementIndex = nextWriteIndex - 1
for i in 0...size - 1 {
// CRASHES ON NEXT LINE !!!
let thisCrossing = array[(lastElementIndex - i) % size]
if thisCrossing.indexWithOffset > refIndex {
subArray.append(thisCrossing)
} else {
break
}
}
return subArray.reversed()
}
public func reset() {
array = [ZeroCrossing](repeating: ZeroCrossing(index: 0, previousPeak: 0, indexWithOffset: 0), count: size)
nextWriteIndex = 0
}
}
The backtrace ends with the following
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x1016dc008)
frame #0: 0x000000018bf5c090 libswiftCore.dylib`swift_retain + 60
frame #1: 0x000000018bf9c704 libswiftCore.dylib`swift_bridgeObjectRetain + 56
* frame #2: 0x0000000100838e50 CrossingBuffer.getAfterIndex(refIndex=431975.76999999583, self=0x00000002800a86f0) at CrossingBuffer.swift:97:31
frame #3: 0x00000001007cd704 correlate(self=0x000000010130e5c0) at PitchEngine.swift:146:52
frame #4: 0x00000001007a155c correlate(self=0x0000000283b91200) at TunerEngine.swift:57:39
frame #5: 0x00000001007a1bd8 #objc correlate() at <compiler-generated>:0
But what's strange is that I can access the element from the debug console using the same index reference as the line it crashed on:
(lldb) print (lastElementIndex - i) % size
(Int) $R4 = 838
(lldb) print array.count
(Int) $R5 = 1000
(lldb) print array[(lastElementIndex - i) % size]
(ZeroCrossing) $R6 = (index = 438691, previousPeak = 0.0251232013, indexWithOffset = 438690.12000000477)
Update: additional code to illustrate crashing, it seems that in my case it only crashes when the array size is > 638. There may need to be some noise for the mic to pick up to instigate the crash:
import Foundation
import AVKit
final class AudioTap {
private var audioEngine = AVAudioEngine()
private var windowIndex: UInt = 0
private var lastPeak: Float = 0
private var lastSample: Sample?
public var minPeakSize: Float = 0.005
// Size of crossingBuffer changes crashing behavior
private var crossingBuffer = CrossingBuffer(size: 2000)
private var lastSeekIndex: Double = 0.0
private var timer: Timer?
init() {
installTunerTap()
audioEngine.prepare()
do {
try audioEngine.start()
} catch let error as NSError {
print("AVAudioEngine error on start: \(error.domain), \(error)")
}
timer = Timer.scheduledTimer(
timeInterval: 0.01,
target: self,
selector: #selector(getNext),
userInfo: nil,
repeats: true)
}
private func installTunerTap() {
let inputNode = audioEngine.inputNode
inputNode.installTap( onBus: 0,
bufferSize: 1000,
format: nil,
block: { buffer, when in
let sampleCount = Int(buffer.frameLength)
var sampleIndex = 0
while (sampleIndex < sampleCount) {
if let val = buffer.floatChannelData?.pointee[sampleIndex]{
let sample = Sample(index: self.windowIndex, val: val)
self.update(sample: sample)
}
self.windowIndex += 1
sampleIndex += 1
}
})
}
private func update(sample: Sample) {
lastPeak = abs(sample.val) > abs(lastPeak) ? sample.val : lastPeak
if let last = lastSample {
if last.val * sample.val < 0 && abs(lastPeak) > minPeakSize { // this is a zero crossing
let offset = Double(sample.index) + Double(round((sample.val/(last.val - sample.val)) * 100) / 100)
let crossing = ZeroCrossing(
index: sample.index,
previousPeak: lastPeak,
indexWithOffset: offset
)
crossingBuffer.write(crossing)
lastPeak = 0
}
}
lastSample = sample
}
#objc func getNext() {
if let arr = crossingBuffer.getAfterIndex(lastSeekIndex) {
if let s = arr.last {
lastSeekIndex = s.indexWithOffset
}
}
}
}
It's not crashed after many uses so I'm going to call this one fixed. Here's the updated CrossingBuffer code with the NSLock implemented. Thanks for the help #JoakimDainelson:
class CrossingBuffer {
private var array: [ZeroCrossing]
// NSLock Added
var lock = NSLock()
private let size: Int
private var nextWriteIndex = 0
private var full: Bool {
nextWriteIndex >= size
}
init(size: Int) {
self.size = size
array = [ZeroCrossing](repeating: ZeroCrossing(index: 0, previousPeak: 0, indexWithOffset: 0), count: size)
array.reserveCapacity(size)
}
public func write(_ val: ZeroCrossing) {
//Lock before write
lock.lock()
array[nextWriteIndex % size] = val
//Unlock
lock.unlock()
nextWriteIndex += 1
}
public func getAfterIndex(_ refIndex: Double) -> [ZeroCrossing]? {
if !full { return nil }
var subArray = [ZeroCrossing]()
let lastElementIndex = nextWriteIndex - 1
for i in 0...size - 1 {
// Was Crashing here, now wrapped in lock / unlock
lock.lock()
let thisCrossing = array[(lastElementIndex - i) % size]
lock.unlock()
if thisCrossing.indexWithOffset > refIndex {
subArray.append(thisCrossing)
} else {
break
}
}
return subArray.reversed()
}
public func reset() {
array = [ZeroCrossing](repeating: ZeroCrossing(index: 0, previousPeak: 0, indexWithOffset: 0), count: size)
nextWriteIndex = 0
}
}
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)
}
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)
}
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.
Is it possible to create an SKAction for SKSpriteNode in SpriteKit that generates the same effect as "Photoshop" with the Edit->Transform->Distort option?
Example:
I solve with this implementation:
Swift 5
extension SKSpriteNode {
func addSkew(value: CGFloat = -1){
var effectNode = SKEffectNode()
effectNode.shouldRasterize = true
effectNode.shouldEnableEffects = true
effectNode.addChild(SKSpriteNode(texture: texture))
effectNode.zPosition = 1
let transform = CGAffineTransform(a: 1 , b: 0,
c: value, d: 1,
tx: 0 , ty: 0)
let transformFilter = CIFilter(name: "CIAffineTransform")!
transformFilter.setValue(transform, forKey: "inputTransform")
effectNode.filter = transformFilter
addChild(effectNode)
texture = nil
}
}
You can create a skew using a 1x1 warp mesh. This is supported in iOS10.0+.
This extension receives the skew angle in degrees, and distorts around the anchor point of the given sprite.
Swift 4.2
extension SKWarpGeometryGrid {
public static var skewPosGridZero:[float2] {
get {
return [float2(0.0, 0.0), float2(1.0, 0.0),
float2(0.0, 1.0), float2(1.0, 1.0)]
}
}
public static func skewXPosGrid(_ skewX: CGFloat, node:SKSpriteNode? = nil) -> [float2] {
let anchorY:Float = Float(node?.anchorPoint.y ?? 0.5)
var skewPosGrid = skewPosGridZero
let offsetX = Float(tan(skewX.degToRad()) * (node == nil ? 1.0 : (node!.size.height/node!.size.width)) )
skewPosGrid[2][0] += offsetX * (1.0 - anchorY)
skewPosGrid[3][0] += offsetX * (1.0 - anchorY)
skewPosGrid[0][0] -= offsetX * anchorY
skewPosGrid[1][0] -= offsetX * anchorY
return skewPosGrid
}
public static func skewYPosGrid(_ skewY: CGFloat, node:SKSpriteNode? = nil) -> [float2] {
let anchorX:Float = Float(node?.anchorPoint.x ?? 0.5)
var skewPosGrid = skewPosGridZero
let offsetY = Float(tan(skewY.degToRad()) * (node == nil ? 1.0 : (node!.size.width/node!.size.height)) )
skewPosGrid[1][1] += offsetY * (1.0 - anchorX)
skewPosGrid[3][1] += offsetY * (1.0 - anchorX)
skewPosGrid[0][1] -= offsetY * anchorX
skewPosGrid[2][1] -= offsetY * anchorX
return skewPosGrid
}
public static func skewX(_ angle: CGFloat, node:SKSpriteNode? = nil) -> SKWarpGeometryGrid {
return SKWarpGeometryGrid(columns: 1, rows: 1, sourcePositions: skewPosGridZero, destinationPositions: skewXPosGrid(angle, node:node))
}
public static func skewY(_ angle: CGFloat, node:SKSpriteNode? = nil) -> SKWarpGeometryGrid {
return SKWarpGeometryGrid(columns: 1, rows: 1, sourcePositions: skewPosGridZero, destinationPositions: skewYPosGrid(angle, node:node))
}
public static func skewZero() -> SKWarpGeometryGrid {
return SKWarpGeometryGrid(columns: 1, rows: 1)
}
}
Example animation:
let spriteNode = SKSpriteNode(imageNamed: "tex")
spriteNode.anchorPoint = CGPoint(x:0.25, y:1.0)
let skewA = SKWarpGeometryGrid.skewX(-45.0, node: spriteNode)
let skewB = SKWarpGeometryGrid.skewX(45.0, node: spriteNode)
spriteNode.warpGeometry = skewB
if let skewActionA = SKAction.warp(to: skewA, duration: 3.0),
let skewActionB = SKAction.warp(to: skewB, duration: 3.0){
// Individual easing
skewActionA.timingMode = .easeInEaseOut
skewActionB.timingMode = .easeInEaseOut
spriteNode.run(SKAction.repeatForever(SKAction.sequence([skewActionA,skewActionB])))
}
The list of available SKAction's is here: https://developer.apple.com/reference/spritekit/skaction
There is none to do exactly what you describe. Instead, you can export multiple sprite images from a photo editing tool like Photoshop, and use an animation action like class func animate(with: [SKTexture], timePerFrame: TimeInterval).
This is a little more work, but should achieve the desired effect.
I want to calculate area rectangle form point x and y
class Point{
let x: Int
let y: Int
init(x: Int,y: Int){
self.x = x
self.y = y
}
}
class Rectangle{
func area() -> Int{
//I have no idea
}
}
class Point {
let x: Int
let y: Int
init(x: Int, y: Int) {
self.x = x
self.y = y
}
}
class Rectangle {
let nw: Point
let se: Point
init(nw: Point, se: Point) {
self.nw = nw
self.se = se
}
func area() -> Int {
return (se.y - nw.y) * (se.x - nw.x)
}
func containsPoint(_ p: Point) -> Bool {
let isContainHorizontal = (nw.x <= p.x) && (p.x <= se.x )
let isContainVertical = (nw.y <= p.y) && (p.y <= se.y)
return isContainHorizontal && isContainVertical
}
func combine(_ rect: Rectangle) -> Rectangle {
return Rectangle(nw: Point(x: max(nw.x, rect.nw.x), y: max(nw.y, rect.nw.y)), se: Point(x: min(se.x, rect.se.x), y: min(se.y, rect.se.y)))
}
}
output
2. 6
3. false, true
4. 1
I'm trying to implement a verlet rope in swift. I'm running into the problem where when trying to fix the position of the point masses from constraints, they very rapidly grow apart and then the coordinates become NaNs. Any idea what could be wrong in my code?
import Foundation
private let DEF_POINT_COUNT = 10
private let CONST_ITERATIONS = 5
#objc class PointMass: DebugPrintable {
var point: NSPoint
private var oldPoint: NSPoint
var x: Double {
get { return Double(point.x) }
}
var y: Double {
get { return Double(point.y) }
}
var debugDescription: String {
get { return "\(point)" }
}
init(_ point: NSPoint) {
self.point = point
self.oldPoint = point
}
func updatePosition() {
let dx = point.x - oldPoint.x
let dy = (point.y - oldPoint.y)
oldPoint = point
point = NSPoint(x: point.x + dx, y: point.y + dy)
}
func updatePosition(point: NSPoint) {
let dx = point.x - self.point.x
let dy = point.y - self.point.y
self.oldPoint = NSPoint(x: oldPoint.x + dx, y: oldPoint.y + dy)
self.point = point
}
}
struct Constraint {
var p1: PointMass
var p2: PointMass
var len: Double
func fixPoints() {
let dx = p2.x - p1.x
let dy = p2.y - p1.y
let dist = sqrt(dx*dx + dy*dy)
let diff = (dist - len)/len
p2.updatePosition(NSPoint(x: p2.x - diff*dx*0.5, y: p2.y - diff*dy*0.5))
p1.updatePosition(NSPoint(x: p1.x + diff*dx*0.5, y: p1.y + diff*dy*0.5))
}
}
#objc class Rope: NSObject {
let points: [PointMass]
let constraints: [Constraint]
init(anchor: NSPoint, end: NSPoint, length: Double, count: Int = DEF_POINT_COUNT) {
let anchorPoint = PointMass(anchor)
let endPoint = PointMass(end)
let dx = (anchorPoint.x - endPoint.x)/Double(count)
let dy = (anchorPoint.y - endPoint.y)/Double(count)
let constraintLength = length/Double(count)
var points = [endPoint]
var constraints: [Constraint] = []
for i in 1...count {
let prevPoint = points[i-1]
let newPoint = PointMass(NSPoint(x: prevPoint.x + dx, y: prevPoint.y + dy))
let constraint = Constraint(p1: prevPoint, p2: newPoint, len: constraintLength)
points.append(newPoint)
constraints.append(constraint)
}
self.points = points
self.constraints = constraints
}
func update(anchor: NSPoint, endPoint: NSPoint) {
points.first?.updatePosition(endPoint)
points.last?.updatePosition(anchor)
for point in points {
point.updatePosition()
}
for i in 0...CONST_ITERATIONS {
for constraint in constraints {
constraint.fixPoints()
}
}
}
}
Found it. The problem was in the fixPoints() method, as I suspected.
The line
let diff = (dist - len)/len
should be instead
let diff = (len - dist)/dist