SceneKit で画像の色情報を元にジオメトリを配置する
上の画像の様な感じでピクセル分だけジオメトリを複製してマテリアルに色を付ける。
各ジオメトリのマテリアルの色が変わる可能性があり、GPU による Geometry Instancing が働かず、
ドローコールが画像のピクセル分だけ増えるので注意。
iPhone 6s では 2500 ピクセルで 25 FPS 切るため、今回のものが実用的なものではなく実験というものだと思ってほい。
(iPhone XS ではなんとか 60 FPS を出すことができるが……)
原理的には CGImage の dataProvider からピクセル情報を取得して色を取り、その色をジオメトリに渡し配置するだけ。
UIImage から画像の色を取得する
今回は UIImage のエクステンションを作成。
UIImage を CGImage にして dataProvider からものデータを取る。
あとは CFDataGetBytes からデータを配列に変換する。
そのままだと配列に値が順番で詰め込まれる。
わかりやすくするため RGBA でごとに分けて多次元配列にしている。
extension UIImage { func getColorArray() -> [[UInt8]] { let imageRef = self.cgImage! let data = imageRef.dataProvider!.data let length = CFDataGetLength(data) var rawData = [UInt8](repeating: 0, count: length) CFDataGetBytes(data, CFRange(location: 0, length: length), &rawData) var colorArray = [[UInt8]]() let sepalatorLength = rawData.count / 4 for i in 0..<sepalatorLength { let firstNumber = 4 * i let LastNumber = 4 * i + 3 colorArray.append( Array(rawData[firstNumber...LastNumber]) ) } return colorArray } }
ちなみに 0〜255 で値が変えるため、UIColor などで使用する際は 255 で割って 0 〜 1 の値にする必要がある。
また、RGBA の羅列となってしまうため、縦横のピクセル数は覚えていく必要がある。
あと、NSImage の場合はまた設定が異なるの注意。
色を渡し配置する
先ほどの配列を順番に読むと左上から横方向に色情報が取得できるため、それを適応するだけ。
今回は SCNNode の派生クラス SCNImageColorNode のイニシャライズで設定し、設定すると X, Z 座標で表示される。
設定するパラメータは UIImage、画像の縦横のピクセル数、ジオメトリ、ジオメトリ配置の際のマージンを設定している。
class SCNImageColorNode: SCNNode { required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } init(image:UIImage, imageWidth:UInt, imageHeight:UInt, geometry:SCNGeometry, geometoryOffsetX:Float = 0, geometoryOffsetZ:Float = 0){ super.init() var array = image.getColorArray() let w = imageWidth let h = imageHeight let dx = (geometry.boundingBox.max.x - geometry.boundingBox.min.x) let dz = (geometry.boundingBox.max.z - geometry.boundingBox.min.z) for j in 0..<w { for k in 0..<h { let index = Int((j * w) + k) let node = SCNNode(geometry: geometry.copy() as? SCNGeometry) let material = SCNMaterial() material.lightingModel = .physicallyBased material.diffuse.contents = UIColor( red: CGFloat(array[index][0]) / 255, green: CGFloat(array[index][1]) / 255, blue: CGFloat(array[index][2]) / 255, alpha: CGFloat(array[index][3]) / 255 ) node.geometry?.materials = [material] let posX = Float(k) * (dx + geometoryOffsetX) let posZ = Float(j) * (dz + geometoryOffsetZ) node.simdPosition = float3(posX, 0, posZ) self.addChildNode(node) } } } }
UIViewController 側の設定例
let image = UIImage(named: "logo50")! let box = SCNBox(width: 0.05, height: 0.05, length: 0.05, chamferRadius: 0) let imageToGeoNode = SCNImageColorNode(image: image, imageWidth: 50, imageHeight: 50, geometry: box, geometoryOffsetX: 0.05, geometoryOffsetZ: 0.05) scene.rootNode.addChildNode(imageToGeoNode)
雑につくっているので適当に手直しした方が良いかと思われる。
おまけ:アニメーションを設定する
本来は SCNImageColorNode の色を取得するループ内でアニメーションを設定した方が無駄な処理がないのだが、
汎用性を考えて UIViewController の SCNImageColorNode の小ノード調べ SCNAction を設定している。
var countUp = 0 for i in 0 ..< imageToGeoNode.childNodes.count { let chiledNode = imageToGeoNode.childNodes[i] if i % 50 == 49 { countUp += 1 } let count = i % 50 + countUp let posX = chiledNode.position.x let posZ = chiledNode.position.z let firstWait:TimeInterval = Double(count) * 0.01 let endWait:TimeInterval = Double((50*50) - count) * 0.0025 let move1 = SCNAction.move(to: SCNVector3(posX, 3.0, posZ), duration: 0.5) let move2 = SCNAction.move(to: SCNVector3(posX, 1.0, posZ), duration: 0.5) let move3 = SCNAction.move(to: SCNVector3(posX, 2.0, posZ), duration: 0.5) let move4 = SCNAction.move(to: SCNVector3(posX, 0.0, posZ), duration: 0.5) move1.timingMode = .easeOut move2.timingMode = .easeIn move3.timingMode = .easeOut move4.timingMode = .easeIn chiledNode.runAction( SCNAction.repeatForever( SCNAction.sequence([ SCNAction.wait(duration:firstWait), move1, move2, move3, SCNAction.wait(duration: endWait), move4, SCNAction.wait(duration: endWait) ]) ) ) }
サンプルコード
まとめ
SceneKit の場合、テクスチャの色からパーティクルの色を設定することができないため、試しにジオメトリでやってみたが、
総ピクセルが 〜3K ぐらいまでなら XS では行けそうな感じではある。
また、同じ色であるならジオメトリを再利用できるので、色から調べジオメトリを新規に作成しない様にすると負荷が軽くなると思われる。