- 投稿日:2020-11-08T22:35:18+09:00
【Swift】ミュージックライブラリから曲を再生する方法
ミュージックライブラリを使用する方法
ミュージックライブラリを使用する場合、ミュージックライブラリへのアクセスを許可させる必要があります。
info.plistPrivacy - Media Library Usage Description曲の再生は、
MediaPlayer
を使用してミュージックライブラリから曲を取得し、
MPMediaItemCollection
に分けることで、再生させるようにできます。import MediaPlayerミュージックライブラリから曲の一覧を取得する方法
ミュージックライブラリからの取得は曲以外に、アルバム、アーティストから一覧を取得することもできます。
// 曲の一覧を取得 let mPMediaQuery = MPMediaQuery.songs() // アルバムの一覧を取得 let mPMediaQuery = MPMediaQuery.albums() // アーティストの一覧を取得 let mPMediaQuery = MPMediaQuery.artists()曲を取得した後は、再生させるため曲ごとに分ける必要があります。
let mPMediaQuery = MPMediaQuery.songs() if let collections = mPMediaQuery.collections { for collection in collections { // タイトル print(collection.items[0].title!) } }再生する曲をセット
setQueue(with: MPMediaItemCollection)
でミュージックにある曲を指定して再生することができます。var player = MPMusicPlayerController.applicationMusicPlayer // 曲をセット player.setQueue(with: MPMediaItemCollection) // 曲を再生できるようにする player.prepareToPlay()再生
player.play()一時停止
player.pause()停止
player.stop()リピート
設定値によって、リピート方法を変更することができます。
// ユーザ好みのリピート player.repeatMode = .default // リピートしない player.repeatMode = .none // 現在の曲をリピート player.repeatMode = .one // 現在のプレイリストをリピート player.repeatMode = .oneシャッフル
設定値によって、シャッフル方法を変更できます。
// シャッフルしない player.shuffleMode = .off // ユーザ好みのシャッフル player.shuffleMode = .default // 曲ごとにシャッフル player.shuffleMode = .songs // アルバムごとにシャッフル player.shuffleMode = .albums参考
https://developer.apple.com/documentation/mediaplayer/mpmusicplayercontroller
- 投稿日:2020-11-08T20:22:24+09:00
【Swift5】画面遷移の方法
はじめに
学習でのアプリ開発で、
1 Storyboard — 1 ViewController
で設計しているのですが、画面遷移についての方法をいつもネットで検索していると気づきましたので、備忘録として投稿します。上記の方法で設計している理由としまして、1つの
ViewController
につき1つのStoryBoard
を用意する作り方は共同開発におけるコンフリクトが少なくなるとのことです。動作環境
【Xcode】Version 12.0.1
【Swift】Version 5.3実装方法 ( navigationController を使う)
// 遷移先のView(NextViewController)を取得 let storyboard = UIStoryboard(name: "Next", bundle: nil) let nextVC = storyboard.instantiateViewController(withIdentifier: "NextVC") as! NextViewController // NextViewControllerへ画面遷移 self.navigationController?.pushViewController(nextVC, animated: true)実装方法 ( navigationController を使わない)
// 遷移先のView(NextViewController)を取得 let storyboard = UIStoryboard(name: "Next", bundle: nil) let nextVC = storyboard.instantiateViewController(withIdentifier: "NextVC") as! NextViewController // NextViewControllerへ画面遷移 self.present(nextVC, animated: true, completion: nil)実装方法 ( storyboard の設定)
Storyboard
の画面をインスタンス化するので、Storyboard ID
をつけておく必要があります。Use Storyboard ID
にチェックをつけるのを忘れないように注意してください。まとめ
画面遷移の方法はまだまだありますので、随時更新できればなと思います。
- 投稿日:2020-11-08T19:45:21+09:00
【Swift5】TabBarControllerを用いたXLPagerTabStripの実装
はじめに
XLPagerTabStripというSwift製のiOSライブラリを使った際に、
TabBarController
と絡めた実装をしました。storyboard
での設定等で少し詰まったりもしましたので、備忘録として投稿します。動作環境
【Xcode】Version 12.0.1
【Swift】Version 5.3
【CocoaPods】version 1.9.3実装後の画面
実装手順
1.
TabBarController
の用意
Main.storyboard
の元々あるViewController
を削除して、TabBarController
を追加します。付属のViewController
も消しておいてください。(storyboard
1つに対してViewController
1つとする理由です。)
MainTabBarController.swift
を作成し、Main.storyboard
のClass
に割り当ててください。MainTabBarController.swiftclass MainTabBarController: UITabBarController { override func viewDidLoad() { super.viewDidLoad() // 各画面となるViewControllerを格納する変数を用意 var viewControllers = [UIViewController]() // それぞれの画面の設定(tabの画像など) let tab1VC = UIStoryboard(name: "Bookmarks", bundle: nil).instantiateInitialViewController() tab1VC?.tabBarItem = UITabBarItem(tabBarSystemItem: .bookmarks, tag: 0) viewControllers.append(tab1VC!) let tab2VC = UIStoryboard(name: "Favorites", bundle: nil).instantiateInitialViewController() tab2VC?.tabBarItem = UITabBarItem(tabBarSystemItem: .favorites, tag: 0) viewControllers.append(tab2VC!) self.setViewControllers(viewControllers, animated: false) } }2. 各画面となる
ViewController
の用意
- 上記の
tab1VC
をBookmarks.storyboard
、tab2VC
をFavorites.storyboard
として用意します。それぞれUINavigationController
を設定し、見分けがつくようにタイトルを付けます。3.
XLPagerTabStrip
を導入
- CocoaPodsを利用します。
- ターミナルを開いて
cd 対象ファイル
で移動します。pod init
でpodfile
作成podfile
に下記を追加し保存して、pod install
にて完了です。podfile.rbpod 'XLPagerTabStrip'4. 管理元クラス
ButtonBarPagerTabStripViewController
の用意
ViewController
たちを管理するButtonBarPagerTabStripViewController
を継承したクラスを用意します。今回はBookmarks
を管理元とします。BookmarksViewController.swift
を作成します。BookmarksViewController.swiftimport UIKit import XLPagerTabStrip // 継承元を書き換える ( UIViewController → ButtonBarPagerTabStripViewController ) class BookmarksViewController: ButtonBarPagerTabStripViewController { override func viewDidLoad() { super.viewDidLoad() } override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] { //管理されるViewControllerを返す処理 let firstVC = UIStoryboard(name: "First", bundle: nil).instantiateViewController(withIdentifier: "First") let secondVC = UIStoryboard(name: "Second", bundle: nil).instantiateViewController(withIdentifier: "Second") let thirdVC = UIStoryboard(name: "Third", bundle: nil).instantiateViewController(withIdentifier: "Third") let childViewControllers:[UIViewController] = [firstVC, secondVC, thirdVC] return childViewControllers } }1. ボタン部分になる
Collection View
の設置
- ボタン部分になる
Collection View
を配置し、下記画像のようにAutoLayout
を設定します。Cell
は消しておきましょう。
Collection View
のクラスをButtonBarView
と結びつけ、ModuleをXLPagerTabStrip
とします。
Collection View
を右クリックします。Referencing Outlets
のNew Referencing Outlet
をViewController
にドラッグし、ButtonBarView
を選択します。
2. 切り替え部分になる
ScrollView
の設置
- 切り替え部分になる
ScrollView
を配置し、下記画像のようにAutoLayout
を設定します。- あいまいなレイアウトと警告がでるので
Content Layout Guides
のチェックを外しましょう。
ScrollView
を右クリックします。Referencing Outlets
のNew Referencing Outlet
をViewController
にドラッグし、containerView
を選択します。
正しく繋ぐことができると、
ScrollView
の表示がContainerView
に変わります。5. 管理される
ViewController
たちの用意
- スワイプやボタンを押されて遷移する
ViewController
になります。IndicatorInfoProvider
プロトコルを実装したUIViewController
を下記3つそれぞれ用意します。
First.storyboard
、FirstViewController
を用意。(storyboard
の背景色は赤色
)Second.storyboard
、SecondViewController
を用意。(storyboard
の背景色は青色
)Third.storyboard
、ThirdViewController
を用意。(storyboard
の背景色は緑色
)FirstViewController.swiftimport UIKit import XLPagerTabStrip class FirstViewController: UIViewController { //ここがボタンのタイトルに利用されます var itemInfo: IndicatorInfo = "First" override func viewDidLoad() { super.viewDidLoad() } } extension FirstViewController: IndicatorInfoProvider { func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo { return itemInfo } }6. buttonBarの見た目と振る舞いを追加
MainTabBarController.swiftimport UIKit import XLPagerTabStrip class BookmarksViewController: ButtonBarPagerTabStripViewController { override func viewDidLoad() { // 画面UIについての処理 setupUI() super.viewDidLoad() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // 強制的に再選択し、changeCurrentIndexProgressiveを動作させる( 0番目 → 1番目 → 0番目 ) moveToViewController(at: 1, animated: false) moveToViewController(at: 0, animated: false) } func setupUI() { // ButtonBar全体の背景色 settings.style.buttonBarBackgroundColor = UIColor.white // ButtonBarItemの背景色 settings.style.buttonBarItemBackgroundColor = UIColor.white // ButtonBarItemの文字色 settings.style.buttonBarItemTitleColor = UIColor.lightGray // ButtonBarItemのフォントサイズ settings.style.buttonBarItemFont = .boldSystemFont(ofSize: 14) // 選択中のButtonBarインジケーターの色 settings.style.selectedBarBackgroundColor = UIColor.black // 選択中のButtonBarインジケーターの太さ settings.style.selectedBarHeight = 2.0 // ButtonBarの左端の余白 settings.style.buttonBarLeftContentInset = 8 // ButtonBarの右端の余白 settings.style.buttonBarRightContentInset = 8 // Button内の余白 settings.style.buttonBarItemLeftRightMargin = 32 // スワイプやButtonBarItemタップ等でページを切り替えた時の動作 changeCurrentIndexProgressive = { oldCell, newCell, progressPercentage, changeCurrentIndex, animated in // 変更されたか、選択前後のCellをアンラップ guard changeCurrentIndex, let oldCell = oldCell, let newCell = newCell else { return } // 選択前のセルの状態を指定 oldCell.label.textColor = UIColor.lightGray // 選択後のセルの状態を指定する newCell.label.textColor = UIColor.black } } override func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] { //管理されるViewControllerを返す処理 let firstVC = UIStoryboard(name: "First", bundle: nil).instantiateViewController(withIdentifier: "First") let secondVC = UIStoryboard(name: "Second", bundle: nil).instantiateViewController(withIdentifier: "Second") let thirdVC = UIStoryboard(name: "Third", bundle: nil).instantiateViewController(withIdentifier: "Third") let childViewControllers:[UIViewController] = [firstVC, secondVC, thirdVC] return childViewControllers } }参考
- 投稿日:2020-11-08T18:03:25+09:00
【Vision・Core ML・iOS・Swift】リアルタイム映像のオブジェクトを識別するPART2
この記事は何か?
リアルタイム映像のオブジェクトを識別するの続きです。ソースコードを読んでいきます。
カメラからリアルタイム映像をキャプチャするコード
実装を省略して、全体を見てみます。
サンプルコードには、下記のViewController
クラスを継承した「別のビューコントローラ」があります。実際のところは、そちらで物体認識が行われます。ViewController全体class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate { var bufferSize: CGSize = .zero var rootLayer: CALayer! = nil @IBOutlet weak private var previewView: UIView! private let session = AVCaptureSession() private var previewLayer: AVCaptureVideoPreviewLayer! = nil private let videoDataOutput = AVCaptureVideoDataOutput() private let videoDataOutputQueue = DispatchQueue(label: "VideoDataOutput", qos: .userInitiated, attributes: [], autoreleaseFrequency: .workItem) func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { } override func viewDidLoad() { super.viewDidLoad() setupAVCapture() } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } func setupAVCapture() {...} func startCaptureSession() {...} // Clean up capture setup func teardownAVCapture() {...} func captureOutput(_ captureOutput: AVCaptureOutput, didDrop didDropSampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { // print("frame dropped") } public func exifOrientationFromDeviceOrientation() -> CGImagePropertyOrientation {...} }プロパティ
UIとなるビューは、UIView型の
previewView
だけです。これが、カメラからの映像を表示します。メンバープロパティvar bufferSize: CGSize = .zero var rootLayer: CALayer! = nil @IBOutlet weak private var previewView: UIView! private let session = AVCaptureSession() private var previewLayer: AVCaptureVideoPreviewLayer! = nil private let videoDataOutput = AVCaptureVideoDataOutput() private let videoDataOutputQueue = DispatchQueue(label: "VideoDataOutput", qos: .userInitiated, attributes: [], autoreleaseFrequency: .workItem)メソッド
captureOutput(_:didOutput:from:) メソッド
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { // サブクラスで実装 }viewDidLoad()メソッドとdidReceiveMemoryWarning()メソッド
ビューコントローラのライフサイクルメソッドです。
override func viewDidLoad() { super.viewDidLoad() setupAVCapture() // session.startRunnning() // ビデオを表示するだけなら、ここでセッションを開始できるはず } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() }setupAVCapture()メソッド
事実上、アプリ起動後に呼ばれる最初のメソッドです。
セッション作成、入力と出力の追加、プレビュー設定func setupAVCapture() { var deviceInput: AVCaptureDeviceInput! // 入力デバイスは、メソッド全域にわたって参照できるようにする // デバイスから広角カメラを取得して、入力デバイスでラップする let videoDevice = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .back).devices.first do { deviceInput = try AVCaptureDeviceInput(device: videoDevice!) } catch { print("Could not create video device input: \(error)") return } /* ここからセッションの設定 */ session.beginConfiguration() session.sessionPreset = .vga640x480 // Visonモデルより少しだけ大きめにする // セッションに入力デバイスを追加する guard session.canAddInput(deviceInput) else { print("Could not add video device input to the session") session.commitConfiguration() return } session.addInput(deviceInput) // セッションにデータ出力を追加する if session.canAddOutput(videoDataOutput) { session.addOutput(videoDataOutput) videoDataOutput.alwaysDiscardsLateVideoFrames = true videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)] videoDataOutput.setSampleBufferDelegate(self, queue: videoDataOutputQueue) } else { print("Could not add video data output to the session") session.commitConfiguration() return } let captureConnection = videoDataOutput.connection(with: .video) // 常にフレームを処理する captureConnection?.isEnabled = true do { /* ここから、設定のためデバイスをロックする */ try videoDevice!.lockForConfiguration() let dimensions = CMVideoFormatDescriptionGetDimensions((videoDevice?.activeFormat.formatDescription)!) bufferSize.width = CGFloat(dimensions.width) bufferSize.height = CGFloat(dimensions.height) videoDevice!.unlockForConfiguration() /* デバイスのロックを解除する */ } catch { print(error) } session.commitConfiguration() /* セッションの設定はここまで */ // セッションのプレビューレイヤー previewLayer = AVCaptureVideoPreviewLayer(session: session) // セッションのプレビュー previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill // プレビューレイヤーの外寸 rootLayer = previewView.layer // UI上のプレビューを基底レイヤーとする previewLayer.frame = rootLayer.bounds // セッションプレビューの外寸をUIに合わせる rootLayer.addSublayer(previewLayer) // セッションプレビューを基底レイヤーに追加する }セッションの設定内容
セッションの設定は
beginConfiguration()
メソッドで開始して、commitConfiguration()
メソッドで完了します。setup()メソッドより抜粋/* ここからセッションの設定 */ session.beginConfiguration() session.sessionPreset = .vga640x480 // Visionモデルより、少しだけ大きめに // セッションに入力デバイスを追加する guard session.canAddInput(deviceInput) else { // 追加できなければ、メソッド終了 print("Could not add video device input to the session") session.commitConfiguration() return } session.addInput(deviceInput) // セッションにデータ出力を追加する if session.canAddOutput(videoDataOutput) { session.addOutput(videoDataOutput) videoDataOutput.alwaysDiscardsLateVideoFrames = true videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)] videoDataOutput.setSampleBufferDelegate(self, queue: videoDataOutputQueue) } else { // 追加できなければ、メソッド終了 print("Could not add video data output to the session") session.commitConfiguration() return } let captureConnection = videoDataOutput.connection(with: .video) // 常にフレームを処理する captureConnection?.isEnabled = true do { /* ここから、設定のためデバイスをロックする */ try videoDevice!.lockForConfiguration() let dimensions = CMVideoFormatDescriptionGetDimensions((videoDevice?.activeFormat.formatDescription)!) bufferSize.width = CGFloat(dimensions.width) bufferSize.height = CGFloat(dimensions.height) videoDevice!.unlockForConfiguration() /* デバイスのロックを解除する */ } catch { // ロックできなければエラー print(error) } session.commitConfiguration() /* セッションの設定はここまで */startCaptureSession()メソッド
セッションを開始します。
func startCaptureSession() { session.startRunning() }teardownAVCapture()メソッド
AVCaptureのプレビューレイヤーをまっさらな状態にします。
キャプチャ設定をクリーンアップするfunc teardownAVCapture() { previewLayer.removeFromSuperlayer() previewLayer = nil }captureOutput(_:didDrop:from:) {メソッド
func captureOutput(_ captureOutput: AVCaptureOutput, didDrop didDropSampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { // print("frame dropped") }exifOrientationFromDeviceOrientation() -> CGImagePropertyOrientationメソッド
デバイスの向きに基づいて、適切な画像方向を取得するpublic func exifOrientationFromDeviceOrientation() -> CGImagePropertyOrientation { let curDeviceOrientation = UIDevice.current.orientation let exifOrientation: CGImagePropertyOrientation switch curDeviceOrientation { case UIDeviceOrientation.portraitUpsideDown: exifOrientation = .left case UIDeviceOrientation.landscapeLeft: exifOrientation = .upMirrored case UIDeviceOrientation.landscapeRight: exifOrientation = .down case UIDeviceOrientation.portrait: exifOrientation = .up default: exifOrientation = .up } return exifOrientation }
- 投稿日:2020-11-08T16:19:02+09:00
【Vision・Core ML・iOS・Swift】リアルタイム映像のオブジェクトを識別する
この記事は何か?
Apple Developerのサンプルコード「Recognizing Objects in Live Capture」を独自に解説するものです。
環境
macOS 10.15.7
Xcode 12.1
Swift5.3
iOS 14.2概要
Visionフレームワークを使用すると、ライブキャプチャしたオブジェクトを識別できます。
Core MLモデルを使ったVisionのリクエストは、キャプチャしたシーンで見つかったオブジェクトを識別した結果をVNRecognizedObjectObservation
オブジェクトとして返します。このサンプルアプリでは、以下の方法を紹介しています。
- カメラをライブキャプチャ用に設定する
- Core MLモデルをVisionに組み込む
- 結果を解析して、オブジェクトを分類する
ライブキャプチャを設定する
AVライブキャプチャの実装は、他のキャプチャアプリと似ていますが、Visionアルゴリズムで最適に動作するようにカメラを設定するには、いくつかの微妙な違いがあります。
キャプチャに使用するカメラを設定する
AVFoundationフレームワークが出力するカメラ映像をメインビューコントローラに送信します。
まずは、AVCaptureSession
を設定するために、セッションのインスタンスを作成します。private let session = AVCaptureSession()重要なのは、アプリに最適な解像度を選択することです。
特に要件がない場合でも、最高解像度を安易に選択しないでください。
解像度が低ければ、Visionはより効率的に結果を処理できます。
Xcodeのモデルパラメータを見て、「アプリが640x480
ピクセル以下の解像度を必要とするか」を確認します。以下のコードは、カメラの解像度を「モデルで使用する画像の解像度以上で、最も近い解像度」に設定します。
デバイスとセッションの解像度を設定する//「背面にある広角のビデオカメラ」のインスタンスを作成 let videoDevice = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: .back).devices.first do { deviceInput = try AVCaptureDeviceInput(device: videoDevice!) // カメラからの入力 } catch { print("Could not create video device input: \(error)") return } session.beginConfiguration() // セッションの設定を開始する session.sessionPreset = .vga640x480 // モデル画像はより小さくするスケーリング手続きは、Visionが行います。
以下のコードは、カメラをデバイスとして追加して、セッションにビデオ入力を追加します。
セッションにカメラデバイスからの入力を追加するguard session.canAddInput(deviceInput) else { print("Could not add video device input to the session") session.commitConfiguration() return } session.addInput(deviceInput)以下のコードは、セッションにビデオ出力を追加し、ピクセルフォーマットを指定します。
if session.canAddOutput(videoDataOutput) { // ビデオ出力をセッションに追加できる場合 session.addOutput(videoDataOutput) // ビデオからの出力をセッションに追加する videoDataOutput.alwaysDiscardsLateVideoFrames = true videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)] videoDataOutput.setSampleBufferDelegate(self, queue: videoDataOutputQueue) } else { // ビデオ出力をセッションに追加できない場合は、セッション設定を終わる print("Could not add video data output to the session") session.commitConfiguration() return }すべてのフレームを処理するが、一度に複数のVisionリクエストを保持しない
バッファキューが使用可能なメモリを超えると、カメラは動作を停止します。
バッファ管理を簡単にするために、キャプチャ出力では、前のリクエストが必要とする限り、Visionは呼び出しをブロックします。
その結果、AVFoundationは必要に応じてフレームをドロップすることがあります。
サンプルアプリでは、キューサイズを1にしています。
別のリクエストが利用可能になったときに、Visionリクエストが処理のためにすでにキューに入っている場合、余分なものを保持する代わりにそれをスキップします。let captureConnection = videoDataOutput.connection(with: .video) // 全フレームを処理する captureConnection?.isEnabled = true do { try videoDevice!.lockForConfiguration() let dimensions = CMVideoFormatDescriptionGetDimensions((videoDevice?.activeFormat.formatDescription)!) bufferSize.width = CGFloat(dimensions.width) bufferSize.height = CGFloat(dimensions.height) videoDevice!.unlockForConfiguration() } catch { print(error) }以下のコードは、セッション設定を決定します。
セッション設定を決定するsession.commitConfiguration()以下のコードは、ビューコントローラにプレビューレイヤーを設定して、カメラがそのフレームをアプリのUIに送信できるようにします。
previewLayer = AVCaptureVideoPreviewLayer(session: session) previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill rootLayer = previewView.layer previewLayer.frame = rootLayer.bounds rootLayer.addSublayer(previewLayer)デバイスの向きを指定する
カメラの向きは、デバイスの向きを使用して適切に入力する必要があります。
Visionアルゴリズムは方向に依存しないので、リクエストを行う際には、キャプチャデバイスの方向と相対的な方向を使用してください。デバイスの向きに基づいて、画像方向を取得するlet curDeviceOrientation = UIDevice.current.orientation // 実際のデバイスの向き let exifOrientation: CGImagePropertyOrientation // 画像方向のメタ情報 switch curDeviceOrientation { case UIDeviceOrientation.portraitUpsideDown: // デバイスが縦向き(ホームボタンが上) exifOrientation = .left case UIDeviceOrientation.landscapeLeft: // デバイスが水平(ホームボタンが右) exifOrientation = .upMirrored case UIDeviceOrientation.landscapeRight: // デバイスが水平(ホームボタンが左 exifOrientation = .down case UIDeviceOrientation.portrait: // デバイスが縦向き(ホームボタンが下) exifOrientation = .up default: exifOrientation = .up }Core ML分類器を使って、ラベルを決定する
アプリに含めるCore MLモデルは、Visionのオブジェクト分類器に使用されるラベルを決定します。
このサンプルアプリのモデルは、Darknet YOLO(You Only Look Once)を使用してTuri Create 4.3.2でトレーニングされています。
Turi Createを使用して独自のモデルを生成する方法については、オブジェクト検出を参照してください。
Visionはこれらのモデルを解析し、オブザベーションをVNRecognizedObjectObservation
オブジェクトとして返します。以下のコードは、
VNCoreMLModel
を使用してモデルをロードします。モデルを作成するlet visionModel = try VNCoreMLModel(for: MLModel(contentsOf: modelURL))以下のコードは、導入したモデルから
VNCoreMLRequest
を作成します。リクエストを作成するlet objectRecognition = VNCoreMLRequest(model: visionModel, completionHandler: { (request, error) in DispatchQueue.main.async(execute: { // UIの更新はメインキューで実行する if let results = request.results { self.drawVisionRequestResults(results) } }) })完了ハンドラ自体はバックグラウンドキュー上で実行される可能性があります。
そのため、UIの更新をメインキュー上で実行して、即座に視覚的なフィードバックを提供します。
リクエストの完了ハンドラでは、requests
プロパティを介して結果にアクセスします。オブザベーションを分析する
結果プロパティはオブザベーションの配列です。
配列の要素は、ラベルとバウンディング・ボックスのセットです。以下のコードは、配列を反復して、これらのオブザベーションを解析します。
for observation in results where observation is VNRecognizedObjectObservation { guard let objectObservation = observation as? VNRecognizedObjectObservation else { continue } // 最も信頼度が高いラベルのみを選択する let topLabelObservation = objectObservation.labels[0] let objectBounds = VNImageRectForNormalizedRect(objectObservation.boundingBox, Int(bufferSize.width), Int(bufferSize.height)) let shapeLayer = self.createRoundedRectLayerWithBounds(objectBounds) let textLayer = self.createTextSubLayerInBounds(objectBounds, identifier: topLabelObservation.identifier, confidence: topLabelObservation.confidence) shapeLayer.addSublayer(textLayer) detectionOverlay.addSublayer(shapeLayer) }
labels
配列は、信頼度の高いものから低いものへと順に、各分類識別子とその信頼度の値をリストアップします。
サンプルアプリでは、要素0にある最も信頼度の高い分類のみをメモし、この分類と信頼度をテキストのオーバーレイで表示します。バウンディングボックスには、オブジェクトが観測された座標情報が含まれています。
サンプルでは、この座標情報を使用してオブジェクトの周囲にバウンディングボックスを描画します。このサンプルは、上位の分類のみを返すことで分類を単純化し、配列は信頼度スコアの小さい順に並べられます。
信頼度スコアを分析して複数の分類を表示すれば、検出されたオブジェクトをより詳細に説明したり、競合する分類を表示したりすることもできます。また、オブジェクトを識別した結果として得られる
VNRecognizedObjectObservation
を使用して、VNTrackObjectRequest
のようなオブジェクトトラッカーを初期化することもできます。
オブジェクト・トラッキングの詳細については、Tracking Multiple Objects or Rectangles in Videoを参照してください。
- 投稿日:2020-11-08T15:36:29+09:00
UINibでDI
要約
UINib.OptionsKey.externalObjects
を使ってDIします。やり方
- External Object用のクラスを用意する
- External Object付きのXibファイルを作成する
- instantiate(withOwner:options:)時にexternalObjectsキーでExternal Objectにオブジェクトを割り当てる
External Object用のクラスを用意する
今回は計算をさせたいので計算用オブジェクトを用意します。
これらのクラスは後ほど使います。class CalcObject: NSObject { func calc(num1: Int, num2: Int) -> Int { fatalError() } var operaterText: String { fatalError() } } class AdditionObject: CalcObject { override func calc(num1: Int, num2: Int) -> Int { num1 + num2 } override var operaterText: String { "+" } } class MultiplicationObject: CalcObject { override func calc(num1: Int, num2: Int) -> Int { num1 * num2 } override var operaterText: String { "×" } }External Object付きのXibファイルを作成する
まず、xibファイルを作成し、Interface Builderを開き、Object Libraryより
External Object
を追加します。
追加したらExternal Objectのインスペクタより設定を行います。
- Identifier → 今回はCalcObject
にします
- Class → 先ほど作成したクラスCalcObject
にしますついでにFile's Ownerも実装しておきます。
File's Ownerは、先ほどのExternal Objectと計算結果を表示するラベル、ボタンのアクションが認識できるよう、IBOutletやIBActionを設定しておきます。class NibOwner: NSObject { @IBOutlet var calcObject: CalcObject! @IBOutlet weak var label: UILabel! @IBInspectable var num1: Int = 0 @IBInspectable var num2: Int = 0 @IBAction func calc() { label.text = "\(num1) \(calcObject.operaterText) \(num2) = \(calcObject.calc(num1: num1, num2: num2))" } }instantiate(withOwner:options:)時にexternalObjectsキーでExternal Objectにオブジェクトを割り当てる
先ほど作成したxibファイルをコードから使います。
xibを使うときに、instantiate(withOwner:options:)
でインスタンス化しますが、その時のoptionsに. externalObjects
を付与します。
External ObjectのIdentifierをキーにして、実際にExternal Objectとして割り当てたいオブジェクトをvalueとします。
CalcObject()
の部分をAdditionObject
やMultiplicationObject
に差し替えると動作を変えることができます。
以下は画面にxibの内容を反映させるコードの例です。class ViewController: UIViewController { @IBOutlet weak var stackView: UIStackView! let nibOwner = NibOwner() override func viewDidLoad() { super.viewDidLoad() let nib = UINib(nibName: "View", bundle: nil) let externalObjects = ["CalcObject" : CalcObject()] // ここのオブジェクトを差し替える let instance = nib.instantiate(withOwner: nibOwner, options: [.externalObjects : externalObjects]) instance.compactMap { $0 as? UIView }.forEach { self.stackView.addArrangedSubview($0) } } }実際の動作
AdditionObjectの場合
MultiplicationObjectの場合
まとめ
つかおう!DI!
- 投稿日:2020-11-08T15:21:26+09:00
【Swift5】ARKit を始める 〜モーションキャプチャ〜
どうも、こんにちは。
ありきたりな、佐藤です。
そう、僕です。
最近急にARに興味を持ち出しました。
LIDARが触りたくて、でもLIDARが載ってる端末を所持していなくて。。
まずは、ARKitで、できることを学んでからだな!と思い、まずは勉強開始〜!
なぜ、モーションキャプチャからか、、、
動きある方が入り易いかな?って思うのでやってみる!
開発環境
- XCode12.1 (12A7403)
- Model: iPhone 11 Pro
- iOS 14.1 (18A8395)
ARKit
Appleの公式に説明があるので、こちらを確認してみてください^^
Apple ARKit4ARKit 4は、まったく新しいDepth APIを導入し、iPhone 12 Pro、iPhone 12 Pro Max、およびiPadProでLiDARスキャナーによって収集された詳細な深度情報にアクセスするための新しい方法を作成します。 Location Anchorsは、Apple Mapsの高解像度データを活用して、iPhoneおよびiPadアプリの世界の特定の場所にARエクスペリエンスを配置します。 また、顔追跡のサポートは、Apple Neural Engineと前面カメラを備えたすべてのデバイスに拡張されているため、さらに多くのユーザーが写真やビデオでARの楽しさを体験できます。
プロジェクトを作ろう!
新規作成
まずは、いつもの通り、Xcodeで
「Welcome to Xcode」の
「Create a new Xcode project」を選択
テンプレート選択
アプリケーション名を付ける
「MotionCapture」
今回はサンプルのモーションキャプチャのアプリなでこんな感じで作成して、「Next」を押し、
プロジェクトの保存先などは、任意の場所に。*下記の3項目はデフォルトでこうなってると思います
- Content Technology:「SceneKit」
- Interface:「Storyboard」
- Language:「Swift」
プロジェクトテンプレート
Xcodeが起動され、テンプレートのアプリが作成されましたね。
iPhoneをMacと接続し、「Run」してみましょう。
カメラへのアクセス許可を「OK」を選択し、iPhoneを動かし、ジェット機を探してみましょー!
発見したら、角度を変えるといろんな角度からジェット機が確認できますね。
長々と、説明をしましたね、、
念の為、必要な人がいるかもしれないので書いてみましたw
モーションキャプチャに突入
ARSCNViewDelegateもすでに入っている状態です。
ARSCVViewDelegateはARセッションにARAnchorが追加、更新、削除などの
イベントをハンドリングする為に用意されているものですが、
今回は動きをキャプチャしたいので、ARSessionDelegateを使用するので、
「ARSCNViewDelegate」を「ARSessionDelegate」に変更する。ViewController.swiftclass ViewController: UIViewController, ARSCNViewDelegate { ↓ class ViewController: UIViewController, ARSessionDelegate {テンプレートにすでにsceneViewが定義されていますね。
StroryBoardにももちろん用意されています。ViewController.swift@IBOutlet var sceneView: ARSCNView!sceneViewのdelegateを削除し、
sceneViewのsessitonのdelegateに、selfにセットし、ハンドリングできる様にするViewController.swiftsceneView.delegate = self ↓ sceneView.session.delegate = selfsceneViewのsceneに空のSCNSSceneをセットする
ViewController.swiftsceneView.scene = SCNScene()こちらは今回不要なので削除でOK
ViewController.swift// Show statistics such as fps and timing information // デフォルトはfalseだが、ジェット機に出した時の画面の下部に出ていた情報の表示のon/offフラグ sceneView.showsStatistics = true // Create a new scene // ジェット機の3Dオブジェクトを読み込む let scene = SCNScene(named: "art.scnassets/ship.scn")! // Set the scene to the view // sceneViewにジェット機を表示させる sceneView.scene = scene
人の動きを追跡する設定に変更する
viewWillAppear内で、元々ある
「ARWorldTrackingConfiguration」を「ARBodyTrackingConfiguration」に変更して、
sceneViewにセットする。*ARBodyTrackingConfigurationが使用できない端末の場合は、動作しない為isSupportedで判定し処理する
ARWorldTrackingConfiguration
ARBodyTrackingConfigurationViewController.swiftguard ARBodyTrackingConfiguration.isSupported else { fatalError("This feature is only supported on devices with an A12 chip") } // Create a session configuration // let configuration = ARWorldTrackingConfiguration() // ↓ let configuration = ARWorldTrackingConfiguration() // Run the view's session sceneView.session.run(configuration)
ARSessionDelegateのdelegateメソッドを実装する
今回使用するのはこの2つ
・ARAnchorが追加されたときに呼び出される、session(ARSession, didAdd: [ARAnchor])
・ARAnchorが更新されたときに呼び出される、session(ARSession, didUpdate: [ARAnchor])//developer.apple.com/documentation/arkit/arsessiondelegatefunc session(ARSession, didAdd: [ARAnchor]) func session(ARSession, didUpdate: [ARAnchor])
func session(ARSession, didAdd: [ARAnchor])を実装していく
anchorsのforループさせ、ARAnchorがARBodyAnchorか確認し、ARBodyAnchor以外は今回は処理しない為
returnする。ViewController.swiftfunc session(_ session: ARSession, didAdd anchors: [ARAnchor]) { for anchor in anchors { guard let bodyAnchor = anchor as? ARBodyAnchor else { return } // Next } }
func session(ARSession, didUpdate: [ARAnchor])も同じく、実装していく
didAddと同様にARAnchorを確認し対象だけ処理する様に実装する。
ViewController.swiftfunc session(_ session: ARSession, didUpdate anchors: [ARAnchor]) { for anchor in anchors { guard let bAnchor = anchor as? ARBodyAnchor else { return } // Next } }
ARBodyAnchorから情報を取り出し描画する
両方のdelegateメソッドから取得されたARBodyAnchorを使用し、画面に表示させよう。
・ARBodyAnchorからskeletonを取り出す。
skeletonは3Dの身体情報です。ViewController.swiftlet skeleton = anchor.skeleton
skeletonの、ARSkeletonDefinitionのjointNamesをforループでjointNameを取得する。
ARBodyAnchorの座標は「hips_joint」の位置が中心となっている。取得した、jointNameを使用し、modelTransformで、中心からの移動量と回転量を取得します。
modelTransformは、存在しないjointNameをセットするとnilが取得される。ViewController.swiftfor jointName in skeleton.definition.jointNames { let jointType = ARSkeleton.JointName(rawValue: jointName) if let transform = skeleton.modelTransform(for: jointType) {
備考:jointName
jointNameをデバッガで取得してみた。91パーツ。もっとあるのかな、、?
- 0 : "root"
- 1 : "hips_joint"
- 2 : "left_upLeg_joint"
- 3 : "left_leg_joint"
- 4 : "left_foot_joint"
- 5 : "left_toes_joint"
- 6 : "left_toesEnd_joint"
- 7 : "right_upLeg_joint"
- 8 : "right_leg_joint"
- 9 : "right_foot_joint"
- 10 : "right_toes_joint"
- 11 : "right_toesEnd_joint"
- 12 : "spine_1_joint"
- 13 : "spine_2_joint"
- 14 : "spine_3_joint"
- 15 : "spine_4_joint"
- 16 : "spine_5_joint"
- 17 : "spine_6_joint"
- 18 : "spine_7_joint"
- 19 : "left_shoulder_1_joint"
- 20 : "left_arm_joint"
- 21 : "left_forearm_joint"
- 22 : "left_hand_joint"
- 23 : "left_handIndexStart_joint"
- 24 : "left_handIndex_1_joint"
- 25 : "left_handIndex_2_joint"
- 26 : "left_handIndex_3_joint"
- 27 : "left_handIndexEnd_joint"
- 28 : "left_handMidStart_joint"
- 29 : "left_handMid_1_joint"
- 30 : "left_handMid_2_joint"
- 31 : "left_handMid_3_joint"
- 32 : "left_handMidEnd_joint"
- 33 : "left_handPinkyStart_joint"
- 34 : "left_handPinky_1_joint"
- 35 : "left_handPinky_2_joint"
- 36 : "left_handPinky_3_joint"
- 37 : "left_handPinkyEnd_joint"
- 38 : "left_handRingStart_joint"
- 39 : "left_handRing_1_joint"
- 40 : "left_handRing_2_joint"
- 41 : "left_handRing_3_joint"
- 42 : "left_handRingEnd_joint"
- 43 : "left_handThumbStart_joint"
- 44 : "left_handThumb_1_joint"
- 45 : "left_handThumb_2_joint"
- 46 : "left_handThumbEnd_joint"
- 47 : "neck_1_joint"
- 48 : "neck_2_joint"
- 49 : "neck_3_joint"
- 50 : "neck_4_joint"
- 51 : "head_joint"
- 52 : "jaw_joint"
- 53 : "chin_joint"
- 54 : "left_eye_joint"
- 55 : "left_eyeLowerLid_joint"
- 56 : "left_eyeUpperLid_joint"
- 57 : "left_eyeball_joint"
- 58 : "nose_joint"
- 59 : "right_eye_joint"
- 60 : "right_eyeLowerLid_joint"
- 61 : "right_eyeUpperLid_joint"
- 62 : "right_eyeball_joint"
- 63 : "right_shoulder_1_joint"
- 64 : "right_arm_joint"
- 65 : "right_forearm_joint"
- 66 : "right_hand_joint"
- 67 : "right_handIndexStart_joint"
- 68 : "right_handIndex_1_joint"
- 69 : "right_handIndex_2_joint"
- 70 : "right_handIndex_3_joint"
- 71 : "right_handIndexEnd_joint"
- 72 : "right_handMidStart_joint"
- 73 : "right_handMid_1_joint"
- 74 : "right_handMid_2_joint"
- 75 : "right_handMid_3_joint"
- 76 : "right_handMidEnd_joint"
- 77 : "right_handPinkyStart_joint"
- 78 : "right_handPinky_1_joint"
- 79 : "right_handPinky_2_joint"
- 80 : "right_handPinky_3_joint"
- 81 : "right_handPinkyEnd_joint"
- 82 : "right_handRingStart_joint"
- 83 : "right_handRing_1_joint"
- 84 : "right_handRing_2_joint"
- 85 : "right_handRing_3_joint"
- 86 : "right_handRingEnd_joint"
- 87 : "right_handThumbStart_joint"
- 88 : "right_handThumb_1_joint"
- 89 : "right_handThumb_2_joint"
- 90 : "right_handThumbEnd_joint"
取得した、パーツの情報とARBodyAnchorの中心点を乗算し
3D空間上での、パーツの位置・回転を取得する。ViewController.swift/// jointTypeの位置・回転をキャスト let partsPoint = SCNMatrix4(transform) /// 基準点 hipの位置・回転をキャスト let hipPoint = SCNMatrix4(anchor.transform) /// func SCNMatrix4Mult(_ a: SCNMatrix4, _ b: SCNMatrix4) -> SCNMatrix4で行列を合成するときは、左のaが後にやる方、右のbが先にやる方、という風に考えて合成します。 let matrix = SCNMatrix4Mult(partsPoint, hipPoint) let position = SCNVector3(matrix.m41, matrix.m42, matrix.m43)
すでにSceaneViewに同じものがあるかチェックし、あれば更新、なければ追加する
sceneViewから、nodeの名前をKeyに検索し、
存在していたら、nodeのpositionをセットして更新する。
存在していない場合は、「SCNSphere(radius: 0.02)」で球体を作成し
SCNNodeを作成、position、検索用のnameをセットし
作成した、SCNNodeを、sceneView.scene.rootNodeにaddChildNodeをする。ViewController.swiftif let nodeToUpdate = sceneView.scene.rootNode.childNode(withName: jointName, recursively: false) { /// 既に追加されているので、位置の更新のみ行う nodeToUpdate.isHidden = false nodeToUpdate.position = position } else { // GeoSphere // Radius 球の半径で初期値は 1。 let sphereGeometry = SCNSphere(radius: 0.02) // チェックすると三角ポリゴンを均等に面が構成される。 初期値はfalse sphereGeometry.isGeodesic = true // 球体Color sphereGeometry.firstMaterial?.diffuse.contents = UIColor.green // ノードに球体ジオメトリを設定 let sphereNode = SCNNode(geometry: sphereGeometry) // 表示位置設定 sphereNode.position = position // ノードにname設定 sphereNode.name = jointName // ルートノードに追加する sceneView.scene.rootNode.addChildNode(sphereNode) }さあ完成です。
実行してみましょう。
人がいない場合、youtubeなどの歩いている人の動画をiPhoneの画面越しにみて
見て下さい。全てのソースはGithubにPushしていますのでお取りくださいー。
https://github.com/Satoiosdevelop/ExampleMotionCapture
- 投稿日:2020-11-08T12:20:47+09:00
ARKit+SceneKit+Metal で 『君はラピュタ王の前にいるのだ』の再現
ARで壁や床から3Dモデルが現れる演出をよく見かけるので挑戦。
題材にしたのは『天空の城ラピュタ』。ラピュタの展望室でモウロ将軍の前にムスカとシータが天井から現れるシーン。(床に穴が空く仕掛けがある部屋を「展望室」?とは思うがwikiの表現に倣う)(モデルがレッサーパンダなのはおいて、、)完成イメージ
再現にあたり困ったのがキャラクターと天井の境界面の薄黄色の部分(ラピュタの技術では境界面は薄黄色になったりならなかったりしているが、ここでは薄黄色に統一)。以下、再現方法を解説します。再現方法
①ムスカとシータのノードを上下にアニメーションさせる
②天井境界の薄黄色面を作るための奥行き情報(以下、デプス)を作成する
次の3つを作る。
・天井の境界平面のデプス
・キャラクターを cullMode = back で描画したときのデプス
・キャラクターの cullMode = front で描画したときのデプス
③②の情報から境界面とキャラクターの断面部分を判定し、①の画像に薄黄色を加える。レンダリングパス(Xcodeの Capture GPU Frame)
以下、個別にみていきましょう。①ムスカとシータのノードを上下にアニメーションさせる
これは Xcode の Scene Editor で設定。
・キャラクターを並べる(キャラクターモデルはWWDC2017 SceneKit Demoから借用)。キャラクターは取りまとめノードchar_parent
の下にぶら下げる。
・境界面ノードslice_plane
をchar_parent
と同列に配置。この境界面ノードはアニメーションしない。→色はほぼ透明にする。
→Rendering Order の値を小さくして、キャラクターよりも先に描画することで、背景にキャラクターが描画されないようにする。
ここでCategory bit mask を設定している。後でデプスを生成する際に、境界面とキャラクターを区別するのに使う。境界面は4、キャラクターは2を設定する。②天井境界の薄黄色面を作るための奥行き情報を作成する
キャラクターの正面側(手前側)のデプス情報と、キャラクターの背面側(見えない側)のデプス情報を取得し、その差分でキャラクターの実体を得る。
1) キャラクターの背面部分のデプスを取得
→背面側のみ描画は cullMode=front という指定で行う(後述)
キャラクターの断面は、ここで得られたデプスよりも大きい値となる。
2) キャラクターの正面部分のデプスを取得
→正面側のみ描画は cullMode=back という指定で行う(後述)
キャラクターの断面は、ここで得られたデプスよりも小さい値となる。
3) 境界面(天井平面)のデプスを取得
上記1)2)のデプスでキャラクターの断面となるのは、この境界面のデプスの範囲とする。上記3つ各々のデプス情報を SCNTechnique によるマルチパスレンダリングで生成。マルチパスレンダリングの定義は次の通り。
tequnique.json{ "targets" : { "color_scene" : { "type" : "color" }, "depth_slice" : { "type" : "depth" }, "depth_cullback" : { "type" : "depth" }, "depth_cullfront" : { "type" : "depth" } }, "passes" : { "pass_scene" : { "draw" : "DRAW_SCENE", "outputs" : { "color" : "color_scene" } }, "pass_slice" : { "draw" : "DRAW_NODE", "includeCategoryMask" : 4, "outputs" : { "depth" : "depth_slice" }, "depthStates" : { "clear" : true, "func" : "less" } }, "pass_cullback" : { "draw" : "DRAW_NODE", "includeCategoryMask" : 2, "cullMode" : "back", "outputs" : { "depth" : "depth_cullback" }, "depthStates" : { "clear" : true, "func" : "less" } }, "pass_cullfront" : { "draw" : "DRAW_NODE", "includeCategoryMask" : 2, "cullMode" : "front", "outputs" : { "depth" : "depth_cullfront" }, "depthStates" : { "clear" : true, "func" : "less" } }, "pass_mix" : { "draw" : "DRAW_QUAD", "inputs" : { "colorScene" : "color_scene", "depthSlice" : "depth_slice", "depthCullBack" : "depth_cullback", "depthCullFront" : "depth_cullfront" }, "metalVertexShader" : "mix_vertex", "metalFragmentShader" : "mix_fragment", "outputs" : { "color" : "COLOR" }, "colorStates" : { "clear" : "true", "clearColor" : "0.0 0.0 0.0 0.0" } } }, "sequence" : [ "pass_scene", "pass_slice", "pass_cullback", "pass_cullfront", "pass_mix" ] }少しずつみていきましょう。
"pass_scene" : { "draw" : "DRAW_SCENE", "outputs" : { "color" : "color_scene" } },これは、シーン全体の描画の定義。
draw
にDRAW_SCENE
を指定することでカメラキャプチャ画像+キャラクターの描画を行なっている。描画結果は色情報のみで、color_scene
という名前のバッファに格納。"pass_slice" : { "draw" : "DRAW_NODE", "includeCategoryMask" : 4, "outputs" : { "depth" : "depth_slice" }, "depthStates" : { "clear" : true, "func" : "less" } },これは天井境界面の描画。
includeCategoryMask
に4
を指定しており、境界平面のみ描画する設定としている。この描画では色情報は不要でデプスのみdepth_slice
という名前のバッファに格納。"pass_cullback" : { "draw" : "DRAW_NODE", "includeCategoryMask" : 2, "cullMode" : "back", "outputs" : { "depth" : "depth_cullback" }, "depthStates" : { "clear" : true, "func" : "less" } },これは、正面からみたときのキャラクターのデプス情報を取得するための定義。
includeCategoryMask
に2
を指定しており、キャラクターのみ描画する設定としている。cullMode
にはback
を指定しており、見えている部分を描画し、見えていない(背面)は描画しないようにしている(デフォルトはback
)。この描画では色情報は不要でデプスのみdepth_cullback
という名前のバッファに格納。"pass_cullfront" : { "draw" : "DRAW_NODE", "includeCategoryMask" : 2, "cullMode" : "front", "outputs" : { "depth" : "depth_cullfront" }, "depthStates" : { "clear" : true, "func" : "less" } },これは、キャラクターの背面側のデプス情報を取得するための定義。"pass_cullback"と同様だが、
cullMode
にはfront
を指定している。"pass_mix" : { "draw" : "DRAW_QUAD", "inputs" : { "colorScene" : "color_scene", "depthSlice" : "depth_slice", "depthCullBack" : "depth_cullback", "depthCullFront" : "depth_cullfront" }, "metalVertexShader" : "mix_vertex", "metalFragmentShader" : "mix_fragment", "outputs" : { "color" : "COLOR" }, "colorStates" : { "clear" : "true", "clearColor" : "0.0 0.0 0.0 0.0" } }これは、最終的にカメラキャプチャ+キャラクター+キャラクター断面を表示させる定義。
inputs
に指定された各描画パスの出力結果(色情報、デプス)をmetalFragmentShader
で指定されたmix_fragment
フラグメントシェーダー(後述)で合成して、最終画像としている。outputs
のcolor
にCOLOR
と指定することで画面に描画される。③②の情報から境界面とキャラクターの断面部分を判定し、①の画像に薄黄色を加える
これは前述の
mix_fragment
シェーダーで行う。
処理内容はソース内のコメントにある通りで、デプス情報で薄い黄色を表示するかどうかを判定し、シーン全体の色に加算している。fragment half4 mix_fragment(MixColorInOut vert [[stage_in]], constant SCNSceneBuffer& scn_frame [[buffer(0)]], // 描画フレームの情報 texture2d<float, access::sample> colorScene [[texture(0)]], depth2d<float, access::sample> depthSlice [[texture(1)]], depth2d<float, access::sample> depthCullBack [[texture(2)]], depth2d<float, access::sample> depthCullFront [[texture(3)]]) { // 天井境界面のデプス float ds = depthSlice.sample(s, vert.uv); // 視点から見て手前を向いているポリゴンのデプス float db = depthCullBack.sample(s, vert.uv); // 視点から見て反対を向いたポリゴンのデプス float df = depthCullFront.sample(s, vert.uv); float4 sliceColor = float4(0.0, 0.0, 0.0, 0.0); if (df < ds) { // キャラクターの背面側より境界面が手前 if (ds < db) { // さらに、キャラクターの前面側より境界面が後ろ sliceColor = float4(0.5, 0.5, 0.0, 0.0); // 薄い黄色 } } // カメラキャプチャ画像を含むシーン全体の画像に境界面の色を足す float4 fragment_color = colorScene.sample(s, fract(vert.uv)); fragment_color += sliceColor; // ざつな処理だと思うが、色の取り扱いに詳しくないので機会があれば見直す。 return half4(fragment_color); }ここまでで説明は終わり。
ジオメトリとジオメトリの接触断面に色をつける方法はググっても見つからなかった。
今回、試行錯誤してなんとなく見れるようになったと思うが、この方法は境界面のデプスの他に、キャラクターの前面・背面の2つのデプス情報しか作っていないので、キャラクターの後ろに別のキャラクターがいる場合、後ろにいるキャラクターのデプスは手前のキャラクターのデプスで上書きされてしまい、境界面が描画されないという問題がある。
他に良い方法があると思うので、知っている方教えて欲しいです。
以下、試行錯誤過程で調べたり、試した内容です。再現にあたり試した方法
1) 大量のヒットテストをして境界面でのキャラクターの形を探る
境界面でSCNNode
のhitTestWithSegment(from:to:options:)
を横に100並べて、それをキャラクターの前面→背面、背面→前面でヒットテストすることで、キャラクターの表面位置を求めて、断面図を作ってみた。
→hitTestWithSegment の精度は期待するレベルではなくジオメトリの形とすこしずれた結果となり使えなかった。特に耳や足のように細かな箇所はヒット結果の位置が見た目と大きくずれた。本来の目的と全然異なる使い方なのでしょうがないとは思う。2) 境界面のジオメトリをリアルタイムに作成
・キャラクターのジオメトリを境界面でまっ平にしてその部分を薄黄色にする。
→例えば、足のようにジオメトリと境界面が複数箇所接しているときに、右足・左足のそれぞれでジオメトリを平にするのが相当面倒そう。試してはいない。
また、ジオメトリがローポリの場合、ガタガタすると思われるので、テッセレーション(?)でジオメトリを分割しておくとか必要になると思われる。
・キャラクターのジオメトリと境界面に接する部分で新たに平面ジオメトリ を作って境界面に配置する。
→これも、境界面の近くのジオメトリの頂点は取得できても、それから閉じた平面ジオメトリ を作るのが面倒と思われる(法線情報で頑張ればできるか??)。
→「mesh slicing」でぐぐるといくつか方法が見つかるが難しそうで手が止まった。
・Algorithm or software for slicing a mesh
→UE4だとリアルタイムでMesh Sliceができるっぽい。SceneKitにはなさそう。
・https://unrealengine.hatenablog.com/entry/2016/09/12/002115全体ソースコード
・swift
ViewController.swiftclass ViewController: UIViewController, ARSCNViewDelegate { @IBOutlet weak var scnView: ARSCNView! private let device = MTLCreateSystemDefaultDevice()! private var charNode: SCNNode! private var isTouching = false // タッチ検知中 override func viewDidLoad() { super.viewDidLoad() // キャラクター読み込み。WWDC2017 SceneKit Demoを借用 https://developer.apple.com/videos/play/wwdc2017/604/ guard let scene = SCNScene(named: "art.scnassets/scene.scn"), let charNode = scene.rootNode.childNode(withName: "char_node", recursively: true) else { return } self.charNode = charNode self.charNode.isHidden = true // Scene Technique セットアップ self.setupSCNTechnique() // AR Session 開始 self.scnView.delegate = self let configuration = ARWorldTrackingConfiguration() configuration.planeDetection = [.horizontal] self.scnView.session.run(configuration, options: [.removeExistingAnchors, .resetTracking]) } // // フレームごとに呼び出される // func renderer(_ renderer: SCNSceneRenderer, updateAtTime _: TimeInterval) { if isTouching { // 画面がタッチされた isTouching = false DispatchQueue.main.async { // 表示済みならスキップ guard self.charNode.isHidden else { return } let bounds = self.scnView.bounds let screenCenter = CGPoint(x: bounds.midX, y: bounds.midY) let results = self.scnView.hitTest(screenCenter, types: [.existingPlaneUsingGeometry]) guard let existingPlaneUsingGeometryResult = results.first(where: { $0.type == .existingPlaneUsingGeometry }), let _ = existingPlaneUsingGeometryResult.anchor as? ARPlaneAnchor else { // 画面中央に平面がないので何もしない return } // 画面中央にムスカとシータのノードを配置 let position = existingPlaneUsingGeometryResult.worldTransform.columns.3 self.scnView.scene.rootNode.addChildNode(self.charNode) self.charNode.simdPosition = SIMD3<Float>(position.x, position.y, position.z) self.charNode.isHidden = false } } } private func setupSCNTechnique() { guard let path = Bundle.main.path(forResource: "technique", ofType: "json") else { return } let url = URL(fileURLWithPath: path) guard let techniqueData = try? Data(contentsOf: url), let dict = try? JSONSerialization.jsonObject(with: techniqueData) as? [String: AnyObject] else { return } // マルチパスレンダリングを有効にする let technique = SCNTechnique(dictionary: dict) scnView.technique = technique } override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { guard let _ = touches.first else { return } isTouching = true } }・Metal
#include <metal_stdlib> using namespace metal; #include <SceneKit/scn_metal> // SceneKit -> Shader の受け渡し型 // 定義は https://developer.apple.com/documentation/scenekit/scnprogram 参照 struct VertexInput { float4 position [[attribute(SCNVertexSemanticPosition)]]; // 頂点座標 }; struct MixColorInOut { float4 position [[position]]; float2 uv; }; vertex MixColorInOut mix_vertex(VertexInput in [[stage_in]], constant SCNSceneBuffer& scn_frame [[buffer(0)]]) { MixColorInOut out; out.position = in.position; // 座標系を -1.0 ~ 1.0 -> 0.0 ~ 1.0 に変換。y軸は反転。 out.uv = float2((in.position.x + 1.0) * 0.5 , (in.position.y + 1.0) * -0.5); return out; } constexpr sampler s = sampler(coord::normalized, address::repeat, filter::nearest); fragment half4 mix_fragment(MixColorInOut vert [[stage_in]], constant SCNSceneBuffer& scn_frame [[buffer(0)]], // 描画フレームの情報 texture2d<float, access::sample> colorScene [[texture(0)]], depth2d<float, access::sample> depthSlice [[texture(1)]], depth2d<float, access::sample> depthCullBack [[texture(2)]], depth2d<float, access::sample> depthCullFront [[texture(3)]]) { // 天井境界面のデプス float ds = depthSlice.sample(s, vert.uv); // 視点から見て手前を向いているポリゴンのデプス float db = depthCullBack.sample(s, vert.uv); // 視点から見て反対を向いたポリゴンのデプス float df = depthCullFront.sample(s, vert.uv); float4 sliceColor = float4(0.0, 0.0, 0.0, 0.0); if (df < ds) { // キャラクターの背面側より境界面が手前 if (ds < db) { // さらに、キャラクターの前面側より境界面が後ろ sliceColor = float4(0.5, 0.5, 0.0, 0.0); // 薄い黄色 } } // カメラキャプチャ画像を含むシーン全体の画像に境界面の色を足す float4 fragment_color = colorScene.sample(s, fract(vert.uv)); fragment_color += sliceColor; // ざつな処理だと思うが、色の取り扱いに詳しくないので機会があれば見直す。 return half4(fragment_color); }
- 投稿日:2020-11-08T10:58:27+09:00
[Swift5]"lottie-ios"を使ってアニメーションを実装する方法
lottie-iosとは?
Lottie(ロッティー)は、Airbnbから登場したiOS、Android、React Native対応のアニメーションライブラリです。Adobe After Effectsで表示できるアニメーションをリアルタイムでレンダリングし、ネイティブアプリで静的なコンテンツを作るのと同じくらい簡単に面白い動きをするアニメーションを作成することができます。
▼詳しくはこちら
https://ferret-plus.com/6214▼公式ページはこちら
https://airbnb.design/lottie/実装環境
macOS Catalina 10.15.7
Xcode 12.1
Apple Swift version 5.3今回はTestAppというプロジェクトで実装します。
導入の流れ
①CocoaPodsのインストールとインポート
②JSONファイルのダウンロード
③ダウンロードしたファイルを読み込んでコード記述①CocoaPodsのインストールとインポート
必要とするPodsは
pod 'lottie-ios'
。Podfile.# Uncomment the next line to define a global platform for your project # platform :ios, '9.0' target 'TestApp' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! pod 'IBMWatsonToneAnalyzerV3', '~> 3.6.0' # Pods for TestApp target 'TestAppTests' do inherit! :search_paths # Pods for testing end target 'TestAppUITests' do # Pods for testing end //ここをインストール pod 'lottie-ios' end
pod install
が完了したらプロジェクトへimportします。②JSONファイルのダウンロード
それではLottieのアニメーション検索ページへ移動してお気に入りのアニメーションをダウンロードしていきます。
▼検索ページ
https://lottiefiles.com/featured?page=1お気に入りのアニメーションが決まったらクリックして下記のようなページを開きます。
右上にあるDownload JSONを選択し、Lottie JSONを選択してJSONファイルをダウンロードします。
ダウンロードが完了したらドラック&ドロップでプロジェクトファイル追に加します。
ドラックする場所はinfo.plist
の上あたりでOKです。③ダウンロードしたファイルを読み込んでコード記述
それではいよいよ実装です。
ViewController.swiftimport UIKit import Lottie //①でインポート済 class ViewController: UIViewController { //AnimationViewの宣言 var animationView = AnimationView() override func viewDidLoad() { super.viewDidLoad() //アニメーションの呼び出し addAnimationView() } //アニメーションの準備 func addAnimationView() { //アニメーションファイルの指定 animationView = AnimationView(name: "38280-man-chilling-on-electric-scooter") //ここに先ほどダウンロードしたファイル名を記述(拡張子は必要なし) //アニメーションの位置指定(画面中央) animationView.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.height) //アニメーションのアスペクト比を指定&ループで開始 animationView.contentMode = .scaleAspectFit animationView.loopMode = .loop animationView.play() //ViewControllerに配置 view.addSubview(animationView) }動作確認
コードの記述を終えたら動作確認を行います。
下記画像のように画面中央にアニメーションが表示されていれば成功です!
最後に
今回行ったようにLottieを使えばクオリティの高いアニメーションを短いコードで実装できます。
他にもオシャレなアニメーションがたくさんあるので一度みてみても良いかもしれませんね!最後までご覧いただきありがとうございます!
是非参考にしていただければと思います。
- 投稿日:2020-11-08T09:57:50+09:00
【Swift・駆け出し必見!】WebAPIの通信処理の実装を理解しよう
初学者がつまづく実装は大体決まっています。
・Delegate
・クロージャ
・通信周り
・エラーハンドリング
・アーキテクチャ
この辺りで「?」となる初学者は大勢いるのではないでしょうか?
そしてそのどれもが、開発をしていく上でとても重要なことです。ここでは、API通信の処理を理解するための内容にしたいと思います。
どこの記事にも書いてあるような説明は省いたりしています。Delegateについて理解したいなら
https://qiita.com/Sossiii/items/6c8916f96176991f19aa
こちらの記事を読んでいただきたいです。
Twitterでもなかなか好評でした。未経験から実務につくためのスキルマップ(ロードマップ)も公開しています。
https://qiita.com/Sossiii/items/e2c846d12f2f5db7c426API通信をするためのステップ
ステップ0:そもそもAPIとは何か
https://qiita.com/NagaokaKenichi/items/df4c8455ab527aeacf02
こちらの記事に詳細に書かれていますが、初学者だと難しいですね。
この記事を読み進む上でざっくり説明すると、
ある情報を通信によって外部から受信するためのものです。
例えばアプリを開発していると、イベント情報を管理画面で操作して
アプリ側に送信したい時があります。
そのためにいちいちアプリのアップデートをかけるわけにはいきません。
API通信を利用すると、
「イベント情報があったらアプリ内で表示する」
といったロジックを実装することができます。ステップ1:受け取るデータ構造を確認する
さて、APIについてざっくりと理解したところで、
次は利用するための手順を説明していきます。
まずやることは、どんなデータが送られてくるのかを確認することです。大まかなAPI通信処理の流れは、
1・リクエストを投げる
2・データを受け取る
3・受け取ったデータをマッピングする
4・マッピングしたデータを実際に利用する
といったものなのですが、マッピングの段階で
データの型が違っていたり、受け取り側の構造が違うと
マッピングが失敗してしまいます。
そのため、まずはどんなデータが送られてくるのかを確認します。確認する方法は、URLを検索するだけです。
https://covid19-japan-web-api.now.sh/api//v1/prefectures
例えばコロナウイルスの感染状況を公開しているこちらのURLに
アクセスしてみましょう。
※chrome拡張機能のJsonViewで見やすくしていますここで確認するのは「key」「valueとその型」「データ構造」です。
keyとは、画像で言うとid, name_ja, name_enなどにあたり
valueは1(Int), "北海道"(String), "Hokkaido"(String)にあたります
データ構造は、階層{ }や配列になっているか[ ]です。アウトプットしないとなぜこれらを確認しないといけないのかわからないので、
あ〜そんなものがあるのか、程度に読み進めてください。ステップ2:マッピングの準備、モデルを書く
送られてくるデータ構造を把握したら、マッピングするための
コードを書いていきます。マッピングにおいて、送られてくるデータから
利用したいデータを抜き取り、利用する側の言語で利用可能にします。https://covid19-japan-web-api.now.sh/api//v1/prefectures
で送られてくるデータから、id, name_ja, pcrを利用する場合を考えます。
この場合、知りたい部分のkey, value, 構造に着目すると以下のようになります。[ { id:1 name_ja: "北海道", ... pcr: 85892, }, { id:2 name_ja: "青森", ... pcr: 5350, }, ... ]それでは実際にマッピングするためのモデルを書いていきます。
Xcodeを開いたら、新しいファイルを作りましょう。
ファイル名はCovid.swiftとします。//Covid.swift file struct CovidInfo: Codable { let id: Int let nameJa: String let pcr: Int //2単語以上の変数をCodingKeyによってスネークケースに変換している //マッピングの際にはスネークケースが適用される enum CodingKeys: String, CodingKey { case nameJa = "name_ja" } }基本的に変数・定数名はローワーキャメルケース(https://wa3.i-3-i.info/word13956.html)
で書く必要があるので上記のように(nameJa)書いていますが、
返ってくるJSONデータではname_jaとなっていますね。マッピングの際はここの書き方が違うと失敗するので、
同じ形にしてあげる必要があります。
そこで活躍するのがCodingKeyです。
enum CodingKey: String, CodingKey {}
で一致させているのですね。さて、これでマッピングのための受け皿が完成したので、
次のステップで実際にリクエストを投げていきます。ステップ3:リクエストを投げる関数を作成する
API系の処理は、APIファイルを新たに作成して共通化します。
API.swiftとでもしておきましょう。
AlamofireやMoyaなどのライブラリを使いたいところですが、
駆け出しの方向けのためここではライブラリを使わないで進めていきます。struct CovidAPI { static func getPrefectures(completion: @escaping ([CovidInfo]) -> Void) { let url = URL(string: "https://covid19-japan-web-api.now.sh/api//v1/prefectures") let request = URLRequest(url: url!) URLSession.shared.dataTask(with: request) { (data, response, error) in if let data = data { let result = try! JSONDecoder().decode([CovidInfo].self, from: data) completion(result) } }.resume() } }上から順に解説していきます。
static func getPrefectures(completion: @escaping ([CovidInfo]) -> Void) //CovidInfoに[]がついているのはStep1でデータ構造を確認した時に[ ]がついていた(配列になっていた) //からです。複数のデータが飛んでくるので呼び出し処理では配列にしてあげます。 //staticをつけることで、CovidAPIをインスタンス化しなくても外部から //呼び出せるようにしています。 //staticがある場合 CovidAPI.getPrefectures() //staticがない場合 CovidAPI().getPrefectures() //引数にクロージャを使っています。 //関数内のデータはデフォルトで外部に持ち越すことができません。 //@escapingをつけることでそれを可能にしています。URLSession.shared.dataTask(with: request) { (data, response, error) in if let data = data { let result = try! JSONDecoder().decode([CovidInfo].self, from: data) completion(result) } }.resume() //URLSessionの行で実際にリクエストを投げていて、{()}内がレスポンスです。 //dataが私たちが欲しいデータになるので、if let文でdataが返ってきた場合にのみ //マッピングしていますね。 //dataをCovidInfo型に合わせて(マッピング)定数resultに代入し、 //completionに渡しています。(呼び出し側にデータを返している)ステップ4:実際に呼び出してみる
さて、これでAPI通信を利用する準備が整ったので、ViewControllerから呼び出してみましょう。
//ViewController.swift CovidAPI.getPrefectures(completion: {(result: [CovidInfo]) -> Void in DispatchQueue.main.async { idLabel.text = "\(result.id)" nameLabel.text = "\(result.nameJa)" pcrLabel.text = "\(result.pcr)" } })idLabel, nameLabel, pcrLabelは自分でUILabel()を用意して画面に配置してください。
データが反映されたらこの記事のステップは完了です。おわりに
通信周りの処理の流れは理解していただけたでしょうか。
1・リクエストを投げる
2・データを受け取る
3・受け取ったデータをマッピングする
4・マッピングしたデータを実際に利用する
の流れで行いますが、3のマッピングのために
1・返ってくるデータを確認(データ名とデータ構造)
2・受け取りたいデータを決めて受け皿を用意(型)
の2ステップが必要になります。
受け皿の型や変数名が違っているだけで通信処理に失敗するので、
ここの2ステップが最も重要と言っても過言ではありません。駆け出しの方が苦戦する実装の1つなので、少しでも参考になれば幸いです。
- 投稿日:2020-11-08T06:10:15+09:00
画像の上に文字を表示する
画像の上に文字を表示したかったのですが、すごくすごく頼りになったURL↓
https://qiita.com/taku/items/2c246d92bec494a1df5e上記URLをそのままコピーし他のですがSwift初心者のため、Swiftバージョンアップに伴う少しの変更で結構手間取ってしまったのでメモしておきます。。
こちらのソースで無事Swift12でも画像にテキストを表示できました。//画像の上にテキストを表示させる func drawText(image :UIImage) ->UIImage { let text = "Sample Text" let tmpImage = UIImage(named: "1") let font = UIFont.boldSystemFont(ofSize: 32) let imageRect = CGRect(x: 0, y: 0, width: tmpImage.size.width, height: tmpImage.size.height) UIGraphicsBeginImageContext(tmpImage.size); tmpImage.draw(in: imageRect) let textRect = CGRect(x: 5, y: 5, width: tmpImage.size.width - 5 , height: tmpImage.size.height - 5) let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle let textFontAttributes = [ NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: UIColor.black, NSAttributedString.Key.paragraphStyle: textStyle ] as [NSAttributedString.Key : Any] text.draw(in: textRect, withAttributes: textFontAttributes) let newImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext() return newImage! } 本当はAlamofire経由でJSONから取得したImageに動的にテキストを配置したいのですがそれはまだこれから・・・
- 投稿日:2020-11-08T05:24:39+09:00
【Swift】SCNNodeの色を変更する
SceneKit View上に描画をした際に、描画の色を変更したかったので調べました。
実行環境 バージョン Swift 5 Xcode 12.0 例としてballのnodeを作成しています。
let ball = SCNSphere(radius: 0.01) let node = SCNNode(geometry: ball) //色変更のコード ball.firstMaterial?.diffuse.contents = UIColor.red
UIColor.Red
の部分にお好みの色を入れると色変更が出来ます。