2019年3月13日水曜日

ARKitサンプルにジェットのパーティクルを追加しよう



ARKitサンプルにジェットのパーティクルを追加しよう



ARKitのサンプル飛行機に下の絵のようなジェットのパーティクルを追加してみましょう。




パーティクルとは小さな画像を粒子に見立てて、加速度や出現頻度、範囲などの粒子運動を指定することで炎や煙、雪などをシミュレートするコンピューターグラフィックス技術です。

Web上でパーティクルを作成できるParticle2dxのようなサービスもありますが、Xcodeでも標準でパーティクルを作成することができます。


ジェットのパーティクルを作る


まず始めにXcodeの左のペインより適当な場所を選択し、右クリックで「New File...」を選択します。




作成するファイル形式の一覧が表示されますので、その中から「SceneKit Particle System File」を選択します。



Particle System Template の確認がありますがここはSmokeを選びましょう。

ファイル名はBoostFireという名前を指定します。

BoostFire.scnptというファイルが作成されるので、それを選択するとパーティクルの編集を行うことができます。

様々なパラメータがあって迷ってしまいますが、下記のような値を設定してください。


炎を演出するために重要なのは赤枠で囲った部分です。

  • Acceleration  → y の値に1をセットすることで炎の勢いを演出することができます
  • Color → 色を指定することで炎の赤を演出することができます
  • Custom Animation → 炎の形を作ることができます

Custom Animationの部分はパーティクルの広がり方を指定します。

デフォルトの状態では上に登るに従って発散する形なので右肩上がりの線になりますが、始まりから一度膨らんで萎む炎の形は下の絵のように山形になります。





パーティクルを飛行機に追加する


パーティクルが作れたので、早速飛行機に追加しましょう。

追加する場所は飛行機のお尻部分になります。

自作の3Dオブジェクトであれば追加したい場所に空のノードを作成しておく必要がありますが、サンプルの飛行機には既にemitterというノードが用意されています。




コード上の追加はとても簡単ですが、追加時にディレクトリを指定する必要があるので先に先程作成したBoostFire.scnpとsmoke.pngをart.scnassetsへ移動しておきましょう。




移動後はこのようなフォルダ構成になるはずです。

次にViewController.swiftを開き、ViewDidLoad()内でshipをロードしている箇所に次のコードを追加します。

if let particle = SCNParticleSystem(named: "BoostFire.scnp", inDirectory: "art.scnassets") {
    particle.particleSize = 0.3
    if let emitter = ship.childNode(withName: "emitter", recursively: true) {
        emitter.addParticleSystem(particle)
    }
}

このようにすることでファイルからロードされたパーティクルがemitterの場所に配置されます。


実際の動作





修正後のコード

import UIKit
import SceneKit
import ARKit

class ViewController: UIViewController, ARSCNViewDelegate, UIGestureRecognizerDelegate {

    @IBOutlet var sceneView: ARSCNView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Set the view's delegate
        sceneView.delegate = self
        
        // Show statistics such as fps and timing information
        sceneView.showsStatistics = true
        
        // Create a new scene
        let scene = SCNScene(named: "art.scnassets/ship.scn")!
        
        if let ship = scene.rootNode.childNode(withName: "ship", recursively: true) {
            if let particle = SCNParticleSystem(named: "BoostFire.scnp", inDirectory: "art.scnassets") {
                particle.particleSize = 0.3
                if let emitter = ship.childNode(withName: "emitter", recursively: true) {
                    emitter.addParticleSystem(particle)
                }
            }
            ship.opacity = 0
        }
        
        // Setup Omni Light
        let light = SCNLight()
        light.type = .omni

        let LightNode = SCNNode()
        LightNode.light = light
        LightNode.name = "light"
        LightNode.position = SCNVector3(-1,2,-1)
        scene.rootNode.addChildNode(LightNode)
        
        // Set the scene to the view
        sceneView.scene = scene
        
        let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tap(_:)))
        tapRecognizer.delegate = self
        sceneView.addGestureRecognizer(tapRecognizer)
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        // Create a session configuration
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = [.horizontal]
        
        // Run the view's session
        sceneView.session.run(configuration)
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        // Pause the view's session
        sceneView.session.pause()
    }

    @objc func tap(_ tapRecognizer: UITapGestureRecognizer) {
        let touchPoint = tapRecognizer.location(in: self.sceneView)
        
        let results = self.sceneView.hitTest(touchPoint, options: [SCNHitTestOption.searchMode : SCNHitTestSearchMode.all.rawValue])
        
        if let result = results.first {
            guard let hitNodeName = result.node.name else { return }
            guard hitNodeName == "shipMesh" else { return }
            
            if let ship = result.node.parent {
                let actMove = SCNAction.move(by: SCNVector3(0, 0, 0.1), duration: 0.2)
                ship.runAction(actMove)
            }
        }
    }
    
    // MARK: - ARSCNViewDelegate
    
/*
    // Override to create and configure nodes for anchors added to the view's session.
    func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
        let node = SCNNode()
     
        return node
    }
*/
    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        
        if let ship = sceneView.scene.rootNode.childNode(withName: "ship", recursively: true) {
            ship.removeFromParentNode()
            node.addChildNode(ship)
            
            let configuration = ARWorldTrackingConfiguration()
            sceneView.session.pause()
            sceneView.session.run(configuration)
            
            let completion = {
                let rotationAction = SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 0, z: 1, duration: 1))
                ship.runAction(rotationAction)
            }
            
            SCNTransaction.begin()
            SCNTransaction.animationDuration = 1.5
            SCNTransaction.completionBlock = completion
            ship.opacity = 1
            SCNTransaction.commit()
        }
    }
    
    func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
        guard let lightEstimate = self.sceneView.session.currentFrame?.lightEstimate else { return }
        let ambientIntensity = lightEstimate.ambientIntensity
        let ambientColorTemperature = lightEstimate.ambientColorTemperature
        
        if let lightNode = self.sceneView.scene.rootNode.childNode(withName: "light", recursively: true) {
            guard let light = lightNode.light else { return }
            light.intensity = ambientIntensity
            light.temperature = ambientColorTemperature
        }
    }
    
    func session(_ session: ARSession, didFailWithError error: Error) {
        // Present an error message to the user
        
    }
    
    func sessionWasInterrupted(_ session: ARSession) {
        // Inform the user that the session has been interrupted, for example, by presenting an overlay
        
    }
    
    func sessionInterruptionEnded(_ session: ARSession) {
        // Reset tracking and/or remove existing anchors if consistent tracking is required
        
    }
}


0 件のコメント:

コメントを投稿