Apple Engine

Apple, iPhone, iOS, その周辺のことについて

SceneKit で画像の色情報を元にジオメトリを配置する

f:id:x67x6fx74x6f:20190312154610j:plain
画像の色情報とともにジオメトリを配置

上の画像の様な感じでピクセル分だけジオメトリを複製してマテリアルに色を付ける。

各ジオメトリのマテリアルの色が変わる可能性があり、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)
                ])
        )
    )
}

 

サンプルコード

github.com

 

まとめ

SceneKit の場合、テクスチャの色からパーティクルの色を設定することができないため、試しにジオメトリでやってみたが、
総ピクセルが 〜3K ぐらいまでなら XS では行けそうな感じではある。

また、同じ色であるならジオメトリを再利用できるので、色から調べジオメトリを新規に作成しない様にすると負荷が軽くなると思われる。