エイバースの中の人

アプリとWEBサービスを作っているABARSのBLOGです。

UnityアプリのiPhoneX対応

UnityアプリはXcode9でビルドした場合は互換モードでは動作せず、全画面モードで動作します。Unity 5.5など、古いバージョンのUnityでビルドした場合でも同様です。そのため、縦画面想定で、Canvas Scalerでmatch=1.0などと設定していると、UIの左右が見切れることになります。

この問題を解決するには、画面のアスペクト比を見て、match=0.0に設定する必要があります。具体的に、以下のようなスクリプトを全てのCanvas Scalerを含むGameObjectにアタッチします。尚、Canvas Scalerの初期化よりも前に走らせるために、Project SettingのScript Execution OrderでCanvasScreenAutoFixの実行順を早くしておきます。

//iPhoneXの縦長画面に対応
//Canvasにアタッチ

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class CanvasScreenAutoFix : MonoBehaviour
{
	private void Awake()
	{
		if(1.0f*Screen.width/Screen.height<9/16.0f){
			GetComponent<CanvasScaler>().matchWidthOrHeight=0.0f;
		}else{
			GetComponent<CanvasScaler>().matchWidthOrHeight=1.0f;
		}
	}
}

スクリプトのアタッチを手動で行うと大変なので、Editor Scriptで自動化します。

//iPhoneX対応のため、全てのCanvasにCanvasScreenAutoFixをアタッチ

using UnityEngine;
using UnityEditor;
using UnityEngine.Networking;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEditor.SceneManagement;
using UnityEngine.SceneManagement;

public class iPhoneXSupport : MonoBehaviour {
    public static List<T> FindObjectsOfTypeAll<T>()
    {
        List<T> results = new List<T>();
        var s = SceneManager.GetActiveScene();
        if (s.isLoaded)
        {
            var allGameObjects = s.GetRootGameObjects();
            for (int j = 0; j < allGameObjects.Length; j++)
            {
                var go = allGameObjects[j];
                results.AddRange(go.GetComponentsInChildren<T>(true));
            }
        }
        return results;
    }

    [MenuItem ("iPhoneX/Add canvas scaler")]
    static void ChangeToProduction() {
        GameObject[] allObjects = (GameObject[])FindObjectsOfTypeAll( typeof(GameObject) );
        foreach ( GameObject obj in allObjects ){
            if(obj.GetComponent<Canvas>()!=null){
                obj.AddComponent<CanvasScreenAutoFix>();
                Debug.Log(obj.name);
                EditorSceneManager.MarkSceneDirty(SceneManager.GetActiveScene());
            }
        }
    }
}

UIは互換表示のように黒帯を表示し、メインゲームは全画面で動作させる場合、UIのRectTransformのAnchorがCenter以外だとレイアウトが崩れる場合があるため、手動で全てCenterに書き換えます。

また、カメラの画角も変わり、大きく表示されるため、補正します。

//iPhoneXの縦長画面に対応
//Cameraにアタッチ

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraScreenAutoFix : MonoBehaviour {

	// Use this for initialization
	void Awake () {
		float initial_field_of_view=GetComponent<Camera>().fieldOfView;
		float ratio=(9.0f/16.0f)/(1.0f*Screen.width/Screen.height);
		if(ratio<1.0f){
			ratio=1.0f;
		}
		GetComponent<Camera>().fieldOfView=initial_field_of_view*ratio;
	}
}

中央寄せではなく、上寄せしているAnchorを持つUIに対しては、セーフエリアの44pxを加算する必要があるため、以下のスクリプトを当てます。

//上寄せのUIのセーフエリアを適用

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class SafeArea : MonoBehaviour {
	private void Awake()
	{
		if(1.0f*Screen.width/Screen.height<9/19.0f){
			Transform target=transform;
			while(target!=null){
				if(target.GetComponent<CanvasScaler>()!=null){
					break;
				}
				target=target.parent;
			}

			CanvasScaler canvas_scaler=target.GetComponent<CanvasScaler>();
			float safe_area_y=44.0f;
			safe_area_y=safe_area_y*canvas_scaler.referenceResolution.y/Screen.height;
			Vector3 pos=GetComponent<RectTransform>().anchoredPosition;
			pos.y=pos.y-safe_area_y;
			GetComponent<RectTransform>().anchoredPosition=pos;
		}
	}
}

新規開発のアプリからは、20:9でUIの背景を作成すると共に、端寄せでUIを配置する必要があるかなと思います。

iOSアプリのiPhoneX対応

iPhoneXのノッチに対応するため、StoryboardにSafe Areaが追加されました。従来は、Viewを基準にConstraintsを指定していましたが、Xcode9以降はSafe Areaを基準にConstraintsを指定することになります。

Safe Areaを使用するには、Safe Area Relative Marginsのチェックボックスを有効にします。その後、ConstraintsのAlign Top to:をViewからSafe Areaに変更します。

storyboard

また、Xcode8までは、ステータスバーの背景色を設定することができないため、ダークモードでは以下のように背景色となるビューを追加していました。

var h : CGFloat=20.0
dark_view = UIView(frame: CGRect(x: 0.0, y: 0.0, width: w, height: h))
dark_view!.backgroundColor=bg_color
self.window!.rootViewController!.view.addSubview(dark_view!)

しかし、iPhoneXではステータスバーの高さが44pxに上がっているため、以下のようにサイズを調整する必要があります。

var h : CGFloat=20.0
if #available(iOS 11.0, *) {
    if(UIApplication.shared.windows[0].safeAreaInsets != UIEdgeInsets.zero){
        h=44.0
    }
}

ただ、実際にやってみると、ポートレートモードでは問題ないのですが、iPhoneXのランドスケープモードではステータスバーが表示されなくなったため、追加したサブビューがUIの上に被ってしまいます。そのため、サブビューを追加する方法ではなく、ステータスバーの背景色を直接、変えるようにした方がよいようです。(参考:Changing the Status Bar Color for specific ViewControllers using Swift in iOS8

extension UIApplication {
    class var statusBarBackgroundColor: UIColor? {
        get {
            return (shared.value(forKey: "statusBar") as? UIView)?.backgroundColor
        } set {
            (shared.value(forKey: "statusBar") as? UIView)?.backgroundColor = newValue
        }
    }
}

UIApplication.statusBarBackgroundColor = bg_color;

また、ランドスケープモード時には左右のunsafeエリアが白で塗りつぶされます。この色を変えるには、viewDidLoadでview.backgroundColorに色を設定します。(参考:Changing the background color of the view, Swift

override func viewDidLoad() {
    super.viewDidLoad()
    if(dark_mode){
        let bg_color:UIColor=UIColor(red: r/255, green: r/255, blue: r/255, alpha: 1.0)
        view.backgroundColor = bg_color
    }
}

Xcodeでdouble freeを検出する

XcodeのProduct -> SchemeからAddress Sanitizerにチェックを入れると、mallocで確保したメモリのdouble freeを検出することができます。スタック破損チェックなどに便利です。

sanitizer

Swift3のNSAttributedStringでunrecognized selector例外が起きる

Swift2からコンバートしたSwift3のNSAttributedStringでunrecognized selector例外が発生します。

let attributedOptions : [String: AnyObject] = [
    NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType as AnyObject,
    NSCharacterEncodingDocumentAttribute: String.Encoding.utf8 as AnyObject
]
attributedString = try NSAttributedString(data: encodedData, options: attributedOptions, documentAttributes: nil)


String.Encoding.utf8だとダメで、String.Encoding.utf8.rawValueにする必要があるようです。

let attributedOptions : [String: AnyObject] = [
    NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType as AnyObject,
    NSCharacterEncodingDocumentAttribute: String.Encoding.utf8.rawValue as AnyObject
]
attributedString = try NSAttributedString(data: encodedData, options: attributedOptions, documentAttributes: nil)


参考:Swift-3 error: '-[_SwiftValue unsignedIntegerValue]: unrecognized selector

Android NDK r8でビルドしたバイナリがAndroid6で動作しない

Android NDK r8でビルドした.soが、Android6でDllNotFoundExceptionになることがあります。logcatを見てみると、hoge.so: has text relocations、というエラーが発生しています。

調査したところ、Android 6.0 Marshmallow (API23)はテキスト再配置をサポートしておらず、この機能を持つライブラリは実行できないようです。

暫定対処としては、targetSDKVersionを23ではなく、22にするとよいようです。

抜本的には、Android NDK r10eでのリビルドを推奨なようです。

第28回IGポート株主総会レポート(2017/08/29)

場所は武蔵野劇場でした。100人程度の参加人数。最初に招集通知の説明、その後に来期の説明。来期は亜人の実写版、魔法陣グルグルなど。魔法使いの嫁は権利窓口として、グループシナジーの最大化を図る。

IMG_2414


以下、質疑応答。

ネットフリックスの制作費が10倍ということで話題だが、ネットフリックスは29期の売上か?ネットフリックス向けは制作単価を上げて質を上げる方向なのか。


B:the BeginningはPerfect Bonesから名前変更。ネットフリックス向けは会計処理が複雑。配信売上と相殺されていく。単価の記事の10倍というのは間違い。実際はそこまで高くない。

製作委員会ではなく、サブライセンスの契約になっている。売れたら売れただけ入る。短期ではなく、長期の構造。

製作委員会方式だとアニメーターに入らないという話がある。今後の方向性は?魔法使いの嫁なども一例だと思うが。


WitStudioのケース。いいものを作る。委員会方式も高度化。条件交渉や、よりよいパートナーを見つけるなど、体制を作る。

30年、基礎を作るべく積み上げてきた。基礎の上に100年続く家を建てる。皆様にわかりやすく見せるのが大事。

株主優待について。クオカードはタチコマの横に飾っている。


クオカードは、攻殻機動隊だけがアニメじゃないという苦情もきた。万人が満足する優待はできない。人的リソースがかかる。機関投資家からは使い道がないので、株価を上げて配当するのが筋ではないかとのコメントを頂いている。

スマホアプリの展開は?魔法使いの嫁など。ジョーカーゲームの続編は?


アプリ市場伸長も認識しているが、作品によりけり。ネクソンさんとやっている攻殻機動隊がCloseした。IGの作品はストーリー性が高いため、キャラがバトルして、ガチャを引くというのに向いていない。やるなら、それに向けて作品を作らないといけない。

原作元の意向があるので、作品の続編は協議で決まる。黒子のバスケは第1話から全て映像化できた。

受賞歴を見やすくできないか?


貴重な意見、ありがとうございます。IRのページを充実させていく。

IGストアとLineスタンプにSACの2ndが欲しい。


貴重な意見、ありがとうございます。

eコマース戦略、商品数が少ない。客単価設定は?


IGストアは収益性が高い方に特化している。売上高は非開示だが、ジョーカーゲームの売上がダントツ。設定資料集、原画集など。海外の人とeコマースで繋がる。WitStudioも進撃のフェアを行なって数字を残した。

GoogleやAppleのストアで過去のライブラリを活かせないか?コンテンツを探しにくいのでIGでまとめて欲しい。


昔の作品は配信の窓口権を保持していない。パッケージメーカーの事情により配信できないケースがある。USのプラットフォームは日本に不利なケースもある。

アーカイブの事業部署がある。アニメ会社の中では珍しい。大事にしており、強みであると考えている。

ファフナーはいつ見れる?


守秘義務がある。

タテアニメと、乙女向けのトキメキレストランについて。


数字は非公開。ローンチから3ヶ月、バグも解消。数字を伸ばしている。システム周りが落ち着いたので、キラーコンテンツを入れる。控えていた宣伝に力を入れる。

初の試み。DVD、BDは低迷。パッケージ市場ではアイドルものが売れている。アイドリッシュセブンなど、社内に知見が溜まっている。

孤独のグルメをタテアニメでやります。

スチームでサイコパスのゲームを見た。海外で魔法使いの嫁をどう売るか。


出資比率に応じた海外権の確保は重要。IGストアでマーケティングリサーチ中。高くて粗利の高いものでいきたい。世界がeコマースでつながる。魔法使いの嫁は海外権を持っており、クランチロールとYoukuなどと組む。昨日も商品の監修が来ていた。OVAを制作したことで、OVAの段階からプリセールスができた。

IG USAが20年。フリクリやNetflixは北米の本社とIG USAおよびIGで契約している。

出版の減収、減益の理由


あまんちゅのアニメなどで重版したが、思ったよりも返品が増えた。大阪は編集1、営業1。書店周り。大学と専門学校の新人発掘。

銀英伝の進捗


9/20にお披露目イベントをやる。ファンが多いので、ぱらぱら出すと、必ず叩かれる。ヤマトでの経験がある。戦略的に情報を抑制して、まとめて情報を出す。

BSのコンテンツ資産の中身


映像マスターは製作委員会に出資した金額。大体、1年で減価償却。昔はテープだったので固定資産。

コンテンツ資産はIGポート100%で制作したもの、もしくは多くを出資したもの。魔法使いの嫁のOVAと、魔法使いの嫁のTVシリーズがこれに当たる。他社が出資すると映像制作収入。最近はデータ納品になるため、無形固定資産になる。監査法人とも調整の結果。


その後、魔法使いの嫁のビジネスモデル説明会と第一話の上映会がありました。魔法使いの嫁への出資は60%に及び、限定コミックの付属するBDの予約は好調で、今期、魔法使いの嫁はIG全体の版権収入の35%近い値を期待しているとのことです。このビジネスモデルは全ての作品に適用できるものではなく、MAG gardenとの2007年の経営統合から10年かかって、ようやくこのビジネスモデルを適用できる作品がでてきた、という話をされていました。

おみやげ。魔法使いの嫁を期待していましたが、真田幸村でした。

IMG_2415

UnityWebRequest.Postで32KB以上のデータが送信できない

UnityWebRequest.Postに32KB以上のデータを与えると、UriFormatException: Uri is longer than the maximum {0} characters.が発生します。これは、UnityWebRequestのPostの内部でUri.EscapeDataStringを使用しており、本来は制約がないはずのPostのデータについても、Uriの32768文字制約が適用されるためです。具体的に、UnityのソースコードをデコンパイルしたSerializeSimpleFormにおいて、Uri.EscapeDataString(current.Value)を呼んでいることがわかります。

この問題を回避するには、Uri.EscapeDataStringを細かく呼ぶ以下のようなクラスを自作して、UnityWebRequest.Postを置き換える必要があります。

尚、検証に使用したUnityは5.5.4f1です。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine.Scripting;
using UnityEngineInternal;

public class UnityWebRequestUnlimited : MonoBehaviour {
	private static string EscapeLongDataString(string stringToEscape)
	{
		var sb = new StringBuilder();
		var length = stringToEscape.Length;

		// 32767以上だとエラーになるため分割する
		var limit = 32767 - 1;
		for (int i = 0; i < length; i += limit)
		{
			sb.Append(Uri.EscapeDataString(stringToEscape.Substring(i, Math.Min(limit, length - i))));
		}

		return sb.ToString();
	}

	public static byte[] SerializeSimpleForm(Dictionary formFields)
	{
		string text = "";
		foreach (KeyValuePair current in formFields)
		{
			if (text.Length > 0)
			{
				text += "&";
			}
			text = text + Uri.EscapeDataString(current.Key) + "=" + EscapeLongDataString(current.Value);
		}
		return Encoding.UTF8.GetBytes(text);
	}

	public static UnityWebRequest Post(string uri, Dictionary formFields)
	{
		UnityWebRequest unityWebRequest = new UnityWebRequest(uri, "POST");
		byte[] data = null;
		if (formFields != null && formFields.Count != 0)
		{
			data = UnityWebRequestUnlimited.SerializeSimpleForm(formFields);
		}
		unityWebRequest.uploadHandler = new UploadHandlerRaw(data)
		{
			contentType = "application/x-www-form-urlencoded"
		};
		unityWebRequest.downloadHandler = new DownloadHandlerBuffer();
		return unityWebRequest;
	}
}

Unity Cloud BuildでUnable to merge android manifestsが出る

Unity 5.5.4f1にFirebase 4を入れようとした際、 Unable to merge android manifestsのエラーが発生しました。ローカルでビルドした場合と、Unity 5.6.3f1を使用した場合はエラーは発生しません。Unity 5.6からAndroid 4.4未満はサポートしなくなったため、Unity Cloud Buildで使用しているAndroid SDKのバージョンの問題かと考え、Androidのビルド設定のMinimum API Levelをデフォルトの2.3.1から4.4に上げたところ、問題は解消されました。

1820: [Unity] AndroidSDKToolsException: Unable to merge android manifests. See the Console for more details. 
1821: [Unity]   at UnityEditor.Android.AndroidSDKTools.DetectErrorsAndWarnings (System.String logMessages, System.String errorMsg) [0x00000] in :0 

UnityにおけるiOS版のアプリサイズの削減

AppStoreでは100MBを超えるとLTEでアプリをダウンロードできなくなります。ダウンロード数を増やすには、アプリサイズ(圧縮後)を100MB以下に抑える必要があります。

しかし、Unityを使用して開発していると、iOSのアプリサイズがAndroidよりも大幅に大きくなります。その原因の多くは、テクスチャの容量です。

Androidでアルファ付き画像を使用すると、デフォルトでETC2+ALPHAが選択され、1画素8bitになります。また、非正方形の画像も4x4画素の倍数であれば圧縮可能です。対して、iOSの場合はデフォルトでPVRTC 4bitが選択されますが、PVRTCは正方形かつ2の乗数しか扱えないため、往々にしてRGBA無圧縮が選択され、1画素32bitになります。その結果、iOSのアプリサイズがAndroidの4倍になります。

この問題を解決するには、UnityのSpritePackerを使用して、2の乗数の解像度に複数の画像をまとめる必要があります。具体的に、テクスチャのSprite ModeにPolygonに指定した上で、Packing Tagを指定すると、自動的にパッキングすることができます。

Packingによって、テクスチャの解像度は正方形になります。その結果、PVRTCでの圧縮が有効になります。アルファ値が存在しない場合は、PVRTC 4bitでも十分な画質が確保できるため、これだけで問題は解消です。しかし、UI画像など、アルファ値が存在する場合は、PVRTC 4bitが非常に汚いため、実用的ではありません。

アルファ値が存在する画像を使用する場合は、iOSのTexture CompressionでASTC 4x4を指定します。これを使用すると、1画素8bitになり、とても綺麗です。しかし、ASTC 4x4はiPhone6以降にのみ対応しており、iPhone5SではランタイムでRGBA無圧縮に展開されるため、1枚につき数百ミリ秒程度の展開コストがかかります。そのため、全ての画像にASTC 4x4を使用すると非常に重いです。そのため、全てのUI画像に適用するのではなく、Sprite Packingする画像にのみ適用することを推奨します。

推奨設定:
・画像は4x4の倍数で作成する (ETC2およびASTCの制約)
・テクスチャはSpritePackerでまとめる (PVRTCを使用できるようにする)
・アルファなしの場合はPVRTCを使用する (iPhone5Sのパフォーマンス向上のため)
・アルファ付きの場合はASTC 4x4を使用する (画質を上げるため)

注意:
・iPhone5SではASTCがソフトデコードになるので使いすぎない

YOLOの出力ベクトルからバウンディングボックスを計算

YOLOは、シンプルなCNNでオブジェクトのカテゴリとバウンディングボックスを検出することができるネットワークです。概要はChainerでYOLOからどうぞ。

YOLOの入力は448x448ピクセルの画像を、RRR...GGG...BBB...で並べて与えます。その際、入力画素のレンジは+-1.0に正規化する必要があるので、RGBが8bitの場合は、normalizedRGB=RGB/255.0f*2-1.0fとします。また、Bitmapの上下のスキャン順にも注意します。

対して、YOLOの出力は1470次元のベクトルであり、バウンディングボックスを計算するには、やや複雑な計算が必要です。

バウンディングボックスの計算方法はYOLOtiny_chainer/predict.pyのコードを参考にさせて頂きました。

YOLOの出力は、順に以下の項目で構成されます。

  probs=ans[0:980].reshape((7,7,20))     # class probabilities
  confs=ans[980:1078].reshape((7,7,2))   # confidence score for Bounding Boxes
  boxes = ans[1078:].reshape((7,7,2,4))  # Bounding Boxes positions (x,y,w,h)


probsは64x64ピクセルを1グリッドとして、画像を7x7個のグリッドに区切ったそれぞれで、以下の20カテゴリの推定確率が入っています。

  classes = ["aeroplane", "bicycle", "bird", "boat", "bottle",
             "bus", "car", "cat", "chair", "cow",
             "diningtable", "dog", "horse", "motorbike", "person",
             "pottedplant", "sheep", "sofa", "train","tvmonitor"]


confsには7x7個のグリッドにつき、2つのバウンディングボックスの信頼度が入っています。

boxesには7x7個のグリッドの2つのバウンディングボックスの座標が、(x,y,w,h)の順に入っています。wとhはsqrtがかかっています。そのため、有効なバウンディングボックスの座標は以下で計算することができます。

for(int by=0;by<7;by++){
  for(int bx=0;bx<7;bx++){
    for(int box=0;box<2;box++){
       for(int category=0;category<20;category++){
         if(confs[by][bx][box]*probs[by][bx][category]>0.1f){
           x= (bx+boxes[by][bx][box][0])*(448/7);
           y = (by+boxes[by][bx][box][1])*(448/7);
           w = pow(box[by][bx][box][2],2)*448;
           h = pow(box[by][bx][box][3],2)*448;
           x1 = x - w/2;
           x2 = x + w/2;
           y1 = y - h/2;
           y2 = y + h/2;
           class = classes[category];
        }
     }
  }
}


尚、このままでは、1つのオブジェクトに対して複数のバウンディングボックスが描画されてしまいます。この問題を解決するには、You Only Look Once:Unified, 統合されたリアルタイムオブジェクトの検出を参考に、計算したバウンディングボックスに対してNon-Maximum suppressionを適用する必要があります。簡易的には、バウンディングボックスを確率順でソートして、重なっているバウンディングボックスを削除してもよいかと思います。

BigQueryのStreamingAPIで日付分割する

AppEngineからBigQueryにinsertする際、templateSuffixに日付を指定すると、TABLE_IDのSchemaを使用して、自動的にテーブルを日付分割することができます。

    SUFFIX_ID = "_"+datetime.datetime.now().strftime("%Y%m%d")
    body = { 'rows':[{'json':data_hash}] ,'ignoreUnknownValues':True ,'templateSuffix':SUFFIX_ID}
...
    response = bigquery.tabledata().insertAll(projectId=PROJECT_ID,
                                              datasetId=DATASET_ID,
                                              tableId=TABLE_ID,
                                              body=body).execute()

ディープラーニングの重みを圧縮するDeep Compression

DEEP COMPRESSION: COMPRESSING DEEP NEURAL NETWORKS WITH PRUNING, TRAINED QUANTIZATION AND HUFFMAN CODING

ディープラーニングの重みの圧縮に関する論文です。Network pruning(刈り込み)と非線形量子化によってAlexNetの係数を240MBから6.9MBまで圧縮します。

以下、論文からアーキテクチャを意訳してみました。

architecture


Network Pruning

Network pruningはCNNモデルの圧縮において広く利用されています。1989年にLeCunによって、ネットワークの複雑さの削減とオーバーフィッティングのために有効であると証明されました。また、2015年にHanによって、近代のState-of-the-artなCNN modelsに対して適用されました。

Pruningでは、まず、connectivityを通常のネットワークトレーニングによって学習します。次に、小さい重みのconnectionを刈り込みます。全てのしきい値以下の重みがネットワークから取り除かれます。最後に、残っているスパースなconnectionに対して、再学習を行います。Pruningによって、AlexNetのパラメータは1/9〜1/13まで削減されます。

Pruningによってスパースになった重みは、圧縮行格納方式 CSR(compressed sparse row)もしくは圧縮列格納方式 CSC(compressed sparse column)フォーマットで符号化します。その結果、2a+n+1個の値が残ります。aは非0の要素数、nはrowかcolumnの数です。

さらに圧縮するため、非0重みの出現位置を、絶対位置ではなく相対位置インデックスで符号化します。畳み込み層に対しては8bit、全結合層に対しては5bitで符号化します。8bitもしくは5bitを超えた値が出現した場合、0を符号化することで、残差が8bitもしくは3bitを超えないようにします。

padding


Trained Quantization and Weight Sharing

ネットワークの量子化と重みの共有化によって、刈り込まれたネットワークの重みをさらに圧縮します。保存すべき複数のconnectionで共有される効果的な重みを見つけ、共有された重みをfine-tuneします。

quantization


Weight Sharingの概念を図3に示します。4入力、4出力のニューロンがあり、重みは4x4行列となっています。左上が係数の行列、左下が勾配の行列です。重みは量子化され、4つのbinsに区分けされます。各binを色分けして示しています。同じbinに格納された重みは、共通の重み(セントロイド)に量子化されます。その結果、重みは共有された重みテーブルのインデックスとなります。updateによって、勾配はグループごとに平均化され、学習率を乗じ、セントロイドを更新します。

刈り込みされたAlexNetにおいて、1つの畳込み層につき、重みは256 shared weightsまで非線形量子化され、各重みは8bitとなります。また、1つの全結合層につき、重みは32 shared weightsまで非線形量子化され、各重みは5bitになります。

quantized_result


量子化によって、4x4行列が、4セントロイドと2bitインデックスになるため、データサイズは1/3まで削減されます。

ハフマン符号化

量子化された重みと、疎行列のインデックスをハフマン符号化することで、データサイズは20%〜30%削減されます。

LoadSceneMode.AdditiveによるuGUIのメモリ削減

UnityではPrefabのネストが使用できないため、複雑なUIのシーンでは、シーン内に多くのuGUI要素が含まれることになります。uGUIのテクスチャは、クオリティの問題から、高解像度かつRGBA32で格納されることが多いため、すぐにメモリ不足になります。

この問題は、UIごとに個別のシーンを作成し、LoadSceneMode.Additiveを使用することで解決することができます。

//シーンの追加読み込み
UnityEngine.SceneManagement.SceneManager.LoadScene( additive_scene, UnityEngine.SceneManagement.LoadSceneMode.Additive );
before_scene=additive_scene;

//シーンの破棄
if(before_scene!=""){
	SceneManager.UnloadSceneAsync(before_scene);
	before_scene="";
}

シーンを追加読み込みした場合も、各シーンからGameObject.Findを行い、シーン間のオブジェクトを参照することができます。また、シーンの追加読み込みは、iOS、Android共に数100ms程度で行うことができるため、通常のシーン読み込みが数秒かかることに比べると、かなり高速です。

ただし、UnityEngine.SceneManagement.LoadSceneMode.AdditiveはブロッキングAPIにもかかわらず、シーンの読み込みが1フレーム遅延するという問題があります。そのため、LoadSceneを実行した直後にGameObject.Findすると失敗するので、注意する必要があります。

UnityのIAPButtonの異常系の対策

UnityのIAPButtonには以下の2つの問題があります。

(1) たまにボタンを押しても反応しない
(2) 連打すると複数の決済が走る

(1)が起きる場合、IAPButton.IAPButtonStoreManager.InitiatePurchaseにおいて、controllerがnullになっており、Null Pointer Exceptionが発生しています。これは、IAPButtonがActiveになったタイミングでストアへの問い合わせが発生しており、問い合わせが完了するまでIAPButtonが有効でないことに起因しています。しかし、このエラーを通知するコールバックは存在しないため、IAPButton.csを書き換えず、クライアントからcontroller==nullを検出するには、GetProductの戻り値をnullチェックするのが良さそうです。

(2)は、Editorでは即時にストアが立ち上がるのですが、iOSではストアの立ち上がりが遅延するため、その間にもう一回同じボタンが押せてしまうのが問題です。IAPButtonにはonPurchaseCompleteもしくはonPurchaseFailedしかなく、onPurchaseStartが存在しません。IAPButton.csを書き換えず、クライアントから購入の開始を検出するには、Button側にAddListenerすると良さそうです。ButtonのonClickでis_purchasingフラグを設定して、他のUIを無効化するアプローチです。尚、その際、(1)の対策をしていないと、異常系で全くUIが押せなくなり、悲惨なことになるので注意です。

以上を踏まえて、以下のような実装が良さそうです。

button.GetComponent<Button>().onClick.AddListener(
	() => {
		if(UnityEngine.Purchasing.IAPButton.IAPButtonStoreManager.Instance.GetProduct(cash.GetComponent<UnityEngine.Purchasing.IAPButton>().productId)==null){
			//ストア情報の取得に失敗するなど、IAPの初期化に失敗した場合、
			//PurchaseFailureEventが呼ばれないまま、NullPointerExceptionになるので、
			//自前でケアする必要がある
			invalid_state(info.StoreID);
		}else{
			//ボタンの連打を防ぐ
			button.GetComponent<Button>().interactable=false;
			is_purchasing=true;
		}
	}
);

button.gameObject.GetComponent<UnityEngine.Purchasing.IAPButton>().onPurchaseComplete.AddListener(
(UnityEngine.Purchasing.Product product) => {
	success(product);
}
);

button.gameObject.GetComponent<UnityEngine.Purchasing.IAPButton>().onPurchaseFailed.AddListener(
(UnityEngine.Purchasing.Product product,UnityEngine.Purchasing.PurchaseFailureReason reason) => {
	button.GetComponent<Button>().interactable=true;	//購入キャンセルした場合はボタンを有効化
	is_purchasing=false;
	
	failed(product,reason);
}
);


尚、アプリ決済直後にアプリを終了した場合、onPurchaseCompleteが呼ばれませんが、次回のアプリ起動時、IAPButtonがActiveになった際に、onPurchaseCompleteが呼ばれます。ただし、IAPButton.csのPurchaseProcessingResultはデフォルトで以下のような実装になっています。TODOに記載されているように、このままの実装では、決済に失敗したIAPButtonがInactiveの際に、別のIAPButtonを表示した場合に、復帰処理が正しく行われず、IAPButtonに設定したPurchaseCompleteコールバックが呼ばれないまま、IAPがCOMPLETEしてしまいます。

public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs e)
{
	foreach (var button in activeButtons) {
		if (button.productId == e.purchasedProduct.definition.id) {
			return button.ProcessPurchase(e);
		}
	}
	return PurchaseProcessingResult.Complete; // TODO: Maybe this shouldn't return complete
}

この問題を回避するために、PurchaseProcessingResult.Pendingを返すことで、決済の復帰シーケンスで対象のIAPButtonが有効になる前にCompleteが呼ばれるのを防止した方がよいです。Pendingを返しておくと、次回のアプリ起動時に、再度、決済の終了処理が走ります。

public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs e)
{
	foreach (var button in activeButtons) {
		if (button.productId == e.purchasedProduct.definition.id) {
			return button.ProcessPurchase(e);
		}
	}
	return PurchaseProcessingResult.Pending;
}

シーンロード直後のタップイベントを無効化する

UnityのEventSystemはシーンロード中のタップが、シーンロード直後に発火する問題があります。そのため、ロード中にタップを連打していると、次のシーンに遷移した直後に大量のタップイベントが発生します。この問題を解消するには、EventSystemに以下のようにスクリプトを当て、シーン開始直後、一定期間はEventSystemを無効化する必要があります。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class EventSystemUpdater : MonoBehaviour {
	private float t;

	private const float INVALIDATE_T=0.33f;

	void Awake () {
		t=0.0f;
		GetComponent().enabled=false;
	}

	// Use this for initialization
	void Start () {
	}
	
	// Update is called once per frame
	void Update () {
		t=t+Time.deltaTime;
		if(t>=INVALIDATE_T){
			if(GetComponent().enabled==false){
				GetComponent().enabled=true;
			}
		}
	}
}

Firebaseに格納した進行度をBigQueryで集計する

Firebaseのカスタムイベントに対して、カスタムキーでイベントを複数回送信すると、1ユーザに対して複数のイベントがレコードに登録されます。このデータを、FirebaseからBigQueryに転送した後、Redashで解析することを考えます。

このイベントが進行度の場合で、カスタムキーに対してint値を保つ場合に、各進行度に対して、何人のユーザがいるかを計測したい場合があります。その際、そのままカウントしてしまうと、1ユーザが複数回イベントを発行してしまっているため、ユーザ数が実際のユーザ数よりも多くなってしまいます。1ユーザが複数のイベントを発行していた場合、ユーザごとに最大の進行度を計算してから、カウントしたいです。

この目的を達成するには、以下のようなクエリを実行します。GROUP BYで user_dim.app_info.app_instance_idを指定して、MAX(event_dim.params.value.int_value) でまとめた後、COUNTします。

SELECT 
  some_int_value,
  EXACT_COUNT_DISTINCT( user_dim.app_info.app_instance_id ) as users
FROM
(
SELECT user_dim.app_info.app_instance_id,
       MAX(event_dim.params.value.int_value) AS some_int_value
FROM [xxx-xxx:xxx_ANDROID.app_events_xxx]
WHERE event_dim.name = "some_event_name"
  AND event_dim.params.key = "some_event_key"
GROUP BY user_dim.app_info.app_instance_id
)
GROUP BY
  1
ORDER BY 
  some_int_value

また、複数の日付のテーブルを結合して集計する場合は、FROMを以下のように書き換えます。

FROM 
(TABLE_DATE_RANGE([xxx-xxx:xxx_ANDROID.app_events_], 
                    TIMESTAMP('2017-07-04'), 
                    TIMESTAMP('2017-07-09')))

参考:BigQuery と Firebase Analytics によるモバイル アプリのカスタム分析

Gear VR 2016年版と2017年版の比較

Gear VR 2016年版(SM-R323)と2017年版(SM-R324)を比較してみました。

IMG_2323


こちらが2016年版です。アタッチメントが黒です。Galaxy S8は指で押すと、若干ですが、上下に動きます。

IMG_2322


こちらが2017年版です。アタッチメントが灰色です。Galaxy S8は、かっちりと固定され、指で押しても上下に動きません。

IMG_2321


当初は、USB-Cアタッチメントの差だけかと思っていたのですが、2017年版のUSB-Cアタッチメントを2016年版に装着してみても、上下のゆらぎは改善しませんでした。どうやら、スマートフォンを上下から挟み込む3連の出っ張りの高さも違うようです。

結論として、2016年版と2017年版は、筐体もアタッチメントも差分があるため、2017年版を購入した方がよいようです。

Galaxy S8でGear VRの熱問題は解決したか

Gear VRの最大の問題は熱です。Galaxy S6で動画を見ていると、15分程度で熱の警告が表示され、フレームレートが大きく低下します。

この問題は、Galaxy S6をファンで冷却することで改善することができます。具体的には、扇風機の前でプレイするか、サンワダイレクトのスマートフォンクーラーで冷却することで、改善します。

IMG_2300


しかし、サンワダイレクトのスマートフォンクーラーは、ホールド力が弱く、うまくGear VRに固定できず、ストラップを通して無理やり固定したため、視力調整が難しくなってしまいました。また、クーラー自体もMicro USBで充電しないといけないのが不便です。その結果、ほとんど使わず、扇風機で対処していました。

Galaxy S8では、SOCのプロセスの進化の恩恵で、劇的に熱問題が改善しています。以下の記事では、GearVRをサーマルカメラで計測したところ、同条件で、Galaxy S7が50度、Galaxy S8が39度まで改善することが示されています。

Galaxy S8 vs. Galaxy S7: Which is best for VR?

実際、Galaxy S8を使い始めた以降、動画・ゲーム・インターネットのいずれでも、一度も熱警告は発生していません。

Galaxy S6では負荷の低いアプリでも熱警告が頻発するため、Gear VRを使用する場合、Galaxy S6ではなく、Galaxy S8を選択するのがよさそうです。

Gear VRの2016年版をGalaxy S8で使えるか

Gear VRには2016年版と2017年版があります。2016年版の型番はSM-R323、2017年版の型番はSM-R324です。2017年版にはコントローラが付属します。2016年版のGear VRはGalaxy S8でも使用できるのでしょうか。

そもそも、Galaxy S8はUSB-Cです。Gear VRは、USB端子がアタッチメント方式になっており、Micro USBとUSB-Cを切り替えられるようになっているため、USB-Cアタッチメントがあれば接続できそうです。

2016年版には、USB-Cアタッチメントが付属するものと、付属しないものがあります。もともと、USB-CはGalaxy Note 7用のものであり、Galaxy Note 7のバッテリー問題以後、USB-Cのアタッチメントは同梱されなくなりました。そのため、Amazonやメルカリには、Gear VRの2016年版が安価に出品されていますが、USB-Cのアタッチメントが付属するかどうかは、事前に確認が必要です。

USB-Cのアタッチメントがあれば、接続できそうです。ということで、実際に接続してみました。

IMG_2314


接続したところ、Galaxy S8はGear VRを認識し、問題なく動作しました。視野的にも問題は感じず、Galaxy S6と同等の見栄えです。ただ、Gear VR本体とGalaxy S8のホルダーに若干、遊びがあるためか、Gear VRを強く降ると、若干、がたつきがあるように感じます。

海外のフォーラムで調査してみると、2016年版と2017年版では、数ミリcm程度、アタッチメントのサイズが異なるという情報がありました。

比較画像

若干のがたつきは、この数ミリが影響しているのかもしれません。

公式には、Galaxy S8で使用できるのはSM-R324だけとなっているため、基本的には2017年版の新型を使った方が良さそうです。

Galaxy S8の縦長画面におけるuGUIの動作

Galaxy S8では16:9を超える縦長になっています。縦長アプリの場合、uGUIのMatch Width Or Heightに1を設定し、横長になった場合に左右に余白を追加する方向に調整することが多いため、Galaxy S8では逆にマイナスの余白が発生し、見切れてしまうのではないかと心配していました。

しかし、その心配は杞憂で、実際にGalaxy S8でUnityで開発したアプリを動作させたところ、自動的にアプリは16:9の解像度で起動し、上下に黒の余白が入るようになっていました。Galaxy S8側で、アプリの互換性を保つよう、ホワイトリストで16:9表示にフォールバックするようです。

Galaxy S8:18.5:9の縦長ディスプレイはアプリごとに表示を変えられるので安心
Search
Profile
Twitter
TopHatenar
HotEntry
Counter

アクセス解析付きカウンター。あなたのBLOGにもどうですか?登録はこちらから。

TOP/ BLOG/ LECTURE/ ONLINE/ RUINA/ ADDON/ THREAD/ METHUSELAYZE/ IPHONE/ MET_IPHONE/ ENGLISH/ RANKING