2018年11月15日木曜日

タッチ座標の色情報を得る

やりたいこと

UIImageViewをタッチしたピクセルの色情報とアルファ値を得る。
たとえば赤いピクセルをタッチしたら
r:255.0 g:0.0 b:0.0 a:255.0
などと出るように。

やり方

タッチ位置に相当するImageViewに貼られたImageのアドレスを参照し、色情報を得る。
ImageViewに表示したImageはたいてい縮小か拡大されてるので、画面のタッチ位置からImage上の位置を計算する。

Image上の位置から画像データのメモリ上のアドレスを計算する。
アドレス計算時は1ピクセルを何バイトで扱っているのかの考慮が必要。(4バイトだった)
Retinaディスプレイの場合、縦横ともデータが2.0倍とかなるので(最近は3.0倍もあるんだっけ?)それも考慮に入れてアドレス計算要。
(注:Retinaで2.0のscaleを得て計算したらかえってうまくいかなかったので、以下のコードでは1.0を代入してある。おそらく画面比率の計算とかを二重にやっちゃってるんじゃないかと思うんだけど、よくわからねぇ…(^^;)ゞ
画面のタッチ位置とオリジナルImage上の位置の変換をしなければ、Retina解像度を考慮に入れたやり方でうまくいくんじゃないかと思う…)

サイトを参考にUIImageを拡張して機能を付けた。

画像の本当の大きさとImageView上に貼られた画像の比率を計算する過程でAVFoundationをimport要。

注意点

CGImageは必ずしもRGBAの順番で並んでいるわけじゃないそうな。
CGImageのCGImageAlphaInfo.rawValueでどういう並びなのか調べられる
以下のコードではRGBAの並びとして書いている。

コード

Swift4.1
import UIKit
import AVFoundation

let pixelDataBytesSize = 4; //1ピクセルのバイト数

extension UIImage {
func getColor(pos: CGPoint) -> (color:UIColor, r:CGFloat, g:CGFloat, b:CGFloat, a:CGFloat) {
let imageData = self.cgImage?.dataProvider?.data //画像データ
let data:UnsafePointer = CFDataGetBytePtr(imageData) //画像データのポインタ(配列扱い)を得るらしい
let scale:CGFloat = 1.0 //UIScreen.main.scale //画面のスケール(Retinaなら多分2.0〜3.0)を得る(コメントアウト中)
//任意のピクセルアドレスを計算
let address:Int = ((Int(self.size.width) * Int(pos.y * scale)) + Int(pos.x * scale)) * pixelDataBytesSize
//RGBαの各値を得る
let r = CGFloat(data[address])
let g = CGFloat(data[address + 1])
let b = CGFloat(data[address + 2])
let a = CGFloat(data[address + 3])
return (UIColor(red: r / 255, green: g / 255, blue: b / 255, alpha: a / 255), r, g, b, a)
}
}

class ViewController: UIViewController {
@IBOutlet weak var myLabel: UILabel!
@IBOutlet weak var myImageView: UIImageView!

override func viewDidLoad() {
super.viewDidLoad()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//imageView上の実表示フレーム
let presentedFrame = AVMakeRect(aspectRatio: (myImageView.image?.size)!, insideRect: myImageView.bounds)
let marginX = presentedFrame.origin.x //上部のマージン
let marginY = presentedFrame.origin.y //左のマージン
if let touch = touches.first as UITouch? {
let loc = touch.location(in: myImageView) //タッチ座標
let image = myImageView.image
//画面上のタップ位置とimage上のタップ位置の変換
let tapX = (image?.size.width)! / presentedFrame.size.width * (loc.x - marginX)
let tapY = (image?.size.height)! / presentedFrame.size.height * (loc.y - marginY)
let color = image?.getColor(pos: CGPoint(x: tapX, y: tapY))
myLabel.textColor = color?.color
myLabel.text = "r:\(color!.r.description) g:\(color!.g.description) b:\(color!.b.description) a:\(color!.a.description)"
}
}
}

参考サイト

ImageViewに表示されたImageの、表示部分だけのframeを求める

ただし、AspectFitで表示された場合のみ有効。

ImageViewにImageを表示する際、contentModeの設定次第でImageを拡大/縮小して収めてくれるわけだが、Imageが実際に表示されている部分のframeサイズを得たいことがある。
そんな時に使うのが以下の関数。
ただしImageの縦横比を保ったまま拡大/縮小する .scaleAspectFit に設定された場合のみ。(他の設定にしても同じ結果しか得られなかった)
以下はすでにImageViewに表示中のImageについてframeサイズを得たが、表示前のImageでも大丈夫だろう。

Swift4.1
let presentedFrame = AVMakeRect(aspectRatio: (myImageView.image?.size)!, insideRect: myImageView.bounds)
print("表示サイズ \(presentedFrame)")

あらかじめAVFoundationをimportして使う。

ずっとわからず、比率を自前で計算してやってたんだけど、誤差が出たりとってもめんどうだった。
他のcontentModeでも使える関数ないかしら?

参考サイト:AS blind side/UIImagwViewにAspect Fitで配置されたUIImageの位置・サイズを求める

2018年10月31日水曜日

Xcode10.1アップデート

10月30日のアップルのスペシャルイベントで新しいMacBook Air、Mac mini、iPad Proなどが発表され、それらに対応したXcodeもリリースされた。

リリースノートによればSwift 4.2.1および、iOS12.1、WatchOS 5.1、tvOS 12.1、macOS MojaveのそれぞれのOSのSDKに対応ということだ。

2018年10月24日水曜日

次期macOSから全てのアプリに署名と公証が必要に?

情報ソース:AAPL. Ch.

iOSアプリはAppStore経由で流通するしか方法がないが、macOSのアプリは昔ながらに一般サイトなどで流通することができる。これは野良アプリと通称されるが、製作者を信頼するか自分で確認しない限り、マルウェアなどである可能性があるわけだ。
それを防止するため、macOS Mojaveではアップルによる事前チェック(公証サービス/Notary Service)を行い、アプリにマルウェアが含まれていないというお墨付きのチケットを発行できるようになった(んだって? 知らんかった)。

この検証結果を、悪意のあるアプリをブロックするGatekeeper機能と連携させ、実行時に「Appleによるチェックで悪質なソフトウェアは検出されませんでした。」とダイアログ表示させるのに使う。
(従来の野良アプリの場合、「このファイルは"Safariにより XXXX からダウンロードされました。」などと出る)
現在は希望したアプリだけだが、これを次期macOSから全てのアプリに適用させる考えだという。

んー、まあ安全であることに越したことはないけど、いちいち管理されるのもめんどいな。まさか安全性検証のために検査料を払えなんてことにならないといいが。

2018年9月28日金曜日

Mojaveにしたらビルドが超遅い


macOS 10.14 Mojaveに上げたところ、Xcode10.0で実機のビルドが途中で止まってしまうようになった。(シミュレータでも同じになるかはわからんが)
Building xx of yy tasksって出て、xxが25で止まったまま動かない。
しょうがないからビルドを止めようとSTOPボタンを押してもダメ。強制終了するしかなく、Xcodeの再起動にもまたえらい時間がかかる。
しょうがないのでそのまま数分間我慢してたら、いつもと同じようにビルドできるようになったけど、他のプロジェクトをビルドすると同じように途中で長々と止まる。

その後、Macを再起動したところ、このやたら時間のかかるビルドは解消されたっぽい。

Mojave自体、日本語入力がかなりもたつくなど、10.14.0の完成度は高くなさげ。
まあしばらくしたらアップデートされるだろうけど、もうちょっとこう、最初からちゃんとしててほしい。

2018年9月13日木曜日

ARのお勉強/Sprite版

ARSessionの開始手順

AR空間に表示するSpriteや3Dオブジェクトは別として、とりあえずARSessionだけ開始する方法

SKSceneを読み込み、SKSceneViewのsceneとして表示

@IBOutlet var sceneView: ARSKView! //StoryboardのSceneViewと接続されてる

if let scene = SKScene(fileNamed: "Scene") {
sceneView.presentScene(scene)
}

Scene.swiftへのアクセスは以下のようにすればできる。
let s = sceneView.scene asScene

AR空間を認識させる

バックカメラを使い、デバイスの位置、向き、AR空間の認識などを行う。開始時に行われ、数秒間待たされる。

let configuration = ARWorldTrackingConfiguration()

ARSession開始

ARWorldTrackingConfigurationで認識したAR設定に基づいてARSessionを開始する。
sceneView.session.run(configuration)

AR空間にオブジェクトを表示させる

ARAnchor

ARのオブジェクトを表示する位置を指定するのがARAnchor。
ARAnchorをAR空間に貼り付けた後、動かせるのか、動かせないのかはまだ不明。

行列でできている

ARAnchorの値は4*4の行列(matrix_identity_float4x4)で示される。
例)以下は変数に作られたばかりの行列の値
simd_float4x4([[1.0, 0.0, 0.0, 0.0)], [0.0, 1.0, 0.0, 0.0)], [0.0, 0.0, 1.0, 0.0)], [0.0, 0.0, 0.0, 1.0)]])

その行列の4個目(0オリジンなのでindexなら3)の要素のうちx, y, zがそれぞれAR空間の軸に対応しており、デバイスを中心にしたARAnchorの位置を1m = 1.0としてFloatで指定する。
  • xは0がデバイス位置、プラスが下↓、マイナスが上↑
  • yは0がデバイス位置、プラスが右方向→、マイナスが左方向←
  • zは0がデバイス位置、プラスが後方(液晶モニタ側)、マイナスが前方(カメラの先)

指定方法

//世界座標と向きを表す4*4の行列
var transform = matrix_identity_float4x4
//行列の3列目のz値に-0.2で前方0.2mを指定
transform.columns.3.x = 0
transform.columns.3.y = 0
transform.columns.3.z = -0.2

ARSKViewDelegate

AR空間に表示するものには、2DのSpriteと3Dのオブジェクトの2つあるが、今回は2DのSprite。
ARAnchorに実際にSpriteを貼るには以下のDelegateメソッドを利用する。

新規ARAnchor追加で呼ばれる

func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? { }

ARKitのテンプレートで使われてるのがこちら。
新しいARAnchorが追加されると呼ばれ、そこに貼りたいSpriteをreturnしてやればいい。

ARAnchorにSKNodeが貼られると呼ばれる

func view(_ view: ARSKView, didAdd node: SKNode, for anchor: ARAnchor) { }

SpriteのActionを使う

view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? で返すSpriteNodeにActionを設定してやればいいのだが、直接そのSpriteNodeを返してもARAnchorに固定されているため、思ったような動きをしてくれない。
そこで空のSKNodeを作り(仮にreturnNodeとする)、それにaddChildeした上で、そのreturnNodeを返してやればいい。

let returnNode = SKNode()
中略
returnNode.addChild(spriteNode)
return returnNode

Spriteを消す時には、このダミーの親ノードも消さないとゴミが残っちゃうぞ。

2018年9月10日月曜日

Spriteの衝突検出

衝突判定検出用delegateの設定

class GameScene: SKScene, SKPhysicsContactDelegate { }

SKPhysicsWorldはSprite世界に関してのクラスで、GameSceneのdidMove()などで以下のように設定しておく。
self.physicsWorld.contactDelegate = self

これによって衝突時に didBegin()メソッド が呼ばれるようになる。

エッジベースの物理体とは

ゲーム中の障害物など、重力の影響を受けないスプライト。
縁(エッジ)しか使われないということか?

GameSceneの場合、範囲として自分のframeを与えてやればいい。
self.physicsBody = SKPhysicsBody(edgeLoopFrom: self.frame)

ほかのSpriteは中心位置をoriginにした矩形になっているため、以下のようにwidthとheightの1/2だけ左上にずらしたCGRectで指定する。ちとめんどい。
self.label?.physicsBody = SKPhysicsBody(edgeLoopFrom: 
                  CGRect(x: -((self.label?.frame.size.width)! / 2),
                         y: -((self.label?.frame.size.height)! / 2),
                     width: self.frame.size.width,
                    height: self.frame.size.height))

ボリュームベースの物理体とは

ゲーム中のキャラクターなど、重力の影響を与えるスプライト。

categoryBitMaskの設定

操作する自分のキャラなのか、敵のキャラなのか、壁などの障害物なのかなどをざっくりカテゴライズすることで、衝突の影響を受けるのか、衝突時に didBegin()メソッドでチェックをするのかなどを細かに設定する。
32bitの任意のビットのみを1にすることで設定する。
以下のようにあらかじめカテゴライズ用定数を用意しておくと便利。
let rocketCate:UInt32 = 0x1 << 0
let labelCate:UInt32  = 0x1 << 1
let frameCate:UInt32  = 0x1 << 2

self.physicsBody?.categoryBitMask = frameCate
self.label?.physicsBody?.categoryBitMask = labelCate
sprite.physicsBody?.categoryBitMask = rocketCate

collisionBitMaskの設定

spriteのカテゴリーどうしの衝突(collision)の有無を決める。
これも32bitのプロパティで、categoryBitMaskの値を使って指定し、ビットが1になっているカテゴリーと衝突する。
デフォルトではすべてのカテゴリーどうしが衝突することになり、0に設定するとすり抜ける事になる。
2カテゴリー以上指定する場合は、カテゴリーどうしをor(|)でつないで指定する。
sprite.physicsBody?.collisionBitMask = frameCate | labelCate

contactTestBitMaskの設定

collisionBitMaskの設定だけでは、衝突して跳ね返ったりするものの、その検知がされないので、delegateメソッドのdidBegin()で検知する相手のカテゴリーを指定する。
sprite.physicsBody?.contactTestBitMask = labelCate

応用

collisionBitMaskを設定せず、contactTestBitMaskだけ設定すれば、すり抜けるけど衝突は検知するなんてこともできるわけだ。

衝突時のdelegateメソッド

SKPhysicsContactDelegateのメソッド。
引数のcontactで、どのspriteどうしが衝突したのか検出できる。
衝撃の値と衝突した位置も。
これを使って敵キャラを消したりするわけですな。

func didBegin(_ contact: SKPhysicsContact) {
    let bodyA = contact.bodyA
    let bodyB = contact.bodyB
    print("bodyA = \(String(describing: bodyA.node?.name)), bodyB = \(String(describing: bodyB.node?.name))")
    print("衝撃値 = \(contact.collisionImpulse)")
    print("衝突位置 = \(contact.contactPoint)")
}

撃ち落としたら消す

敵機を発射したミサイルが当たったら撃ち落とす場合は、最後に以下のようなコードを書いておく。あらかじめ敵機のnodeにenemyとでも名前をつけておく。

    if bodyA.node?.name == "enemy" || bodyB.node?.name == "enemy" {
        bodyA.node?.parent?.removeFromParent()
        bodyB.node?.parent?.removeFromParent()
    }

bodyAとbodyBのどっちが敵機、ミサイルになるのかは不定なので、どちらかが敵機だったら双方をremoveするようにする。
爆発の表現(particle)は未実装だが、remove処理の直前に処理を書けばいいだろう。