vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【Unity5】【iOS】カメラロールで選んだ画像をMaterialにセットする

前回までAndroidで作成していたプラグインを、iOSでやってみた、というお話です。

masanori840816/SetMaterialsFromGallery · GitHub

やったこと

  • ボタンを押したらカメラロール(Photo Library)を開いて画像を選択できるようにする
  • 選択した画像をUnityに渡して、3DオブジェクトのMaterialとしてセットする

準備

  1. 以前プラグインを作成した時と同じように、Assets > Plugins、Assets > Plugins > iOSに以下のファイルを作成します。
  2. Assets > Plugins > CtrlIosPlugin.cs
  3. Assets > Plugins > iOS > PluginConnector.mm

  4. 今回はこれまで作成したAndroid用のプロジェクトに、iOS用のプラグインスクリプトを追加するため、「#if」を使って特定のOSに特化したコードが別のOSで実行されないようにします。

    例:CtrlMain.cs

〜省略〜
void Start ()
{
#if UNITY_ANDROID
  _ctrAndroidPlugin = _gmoAndroidPlugin.GetComponent();
  // DCIMディレクトリのパスを取得する.
  _strDcimPath = _ctrAndroidPlugin.GetText();
  // DCIMディレクトリ内にあるファイルを取得.
  _strFileNames = Directory.GetDirectories(_strDcimPath);

#elif UNITY_IPHONE
  CtrlIosPlugin.Init();

#endif
}
〜省略〜

特にAndroidプラグインを呼び出す「CtrlAndroidPlugin.cs(CtrlPlugins.csから改名)」で、 「AndroidJavaClass」を使う部分などはiOSでビルドする際に読み込まれないようにしないとエラーになるため注意が必要です。

呼び出す側

CtrlMain.cs

〜省略〜
  void Start ()
  {
〜省略〜

#elif UNITY_IPHONE
    CtrlIosPlugin.Init();

#endif
  }
〜省略〜
  void OnGUI()
  {
〜省略〜

#elif UNITY_IPHONE
  if(GUI.Button(new Rect(300f, 20f, 200f, 200f), "GetData"))
  {
    // Open Camera roll.
    CtrlIosPlugin.OpenPhotoLibrary();
  }
#endif
}
  • Start()で初期化、OnGUI()でボタンが押されたタイミングでカメラロールを表示するコードを呼びます。

プラグイン

カメラロールを開く

「UIImagePickerController」を使ってカメラロールを開きます。

PhoneSelector.m

〜省略〜
@interface PhotoSelector()
@property (strong, nonatomic)UIViewController *vwcUnityView;
@property (strong, nonatomic)NSMutableString *mstSelectedImage;
@end
@implementation PhotoSelector
〜省略〜
- (void)Initialize
{
    _vwcUnityView = UnityGetGLViewController();
}
- (void)OpenPhotoLibrary
{
    UIImagePickerController *imgPic = [[UIImagePickerController alloc]init];
    imgPic.delegate = self;
    // フォトライブラリから画像を取得する.
    [imgPic setSourceType:UIImagePickerControllerSourceTypePhotoLibrary];
    [_vwcUnityView presentViewController: imgPic animated:YES completion:nil];
}
〜省略〜
  • 「UINavigationControllerDelegate」、「UIImagePickerControllerDelegate」のプロトコルをセットする必要があります。
  • Unity側のViewControllerを取得して、「UIImagePickerController」を遷移先のViewControllerとしてセットしてカメラロールを開きます。

選択された画像を取得する

画像を選択すると「- (void)imagePickerController〜」が呼ばれるので、該当の画像データを取得します。

PhoneSelector.m

〜省略〜
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
  
  // ここでデータを取得します.
  
〜省略〜
  // カメラロールを閉じる.
  [picker dismissViewControllerAnimated:YES completion:nil];
}
〜省略〜

Unity側にデータを渡す

ここまでは比較的楽にできたのですが、Unity側に選択したデータを渡すところでつまづきました。

Androidでは画像のパスを取得して、それを渡してやればOKでした。
しかし、iOSではファイルのパスを取得するのが難しいようで、「asset-library://〜」のような内容でしか取得できませんでした。

WWWで「asset-library://〜」のパスにアクセスしてみるなど、方法を探ってみたもののうまくいかず。

結局、取得したデータからUIImageを生成して、それをUnity側に渡すことにしました。

処理の流れは以下の通りです。

  1. 「didFinishPickingMediaWithInfo:(NSDictionary *)info」からUIImageを取得
  2. Documentsフォルダのパスを取得
  3. Documentsフォルダに任意の名前をつけて画像を書き出す
  4. 3で書きだした画像のパスを「UnitySendMessage」でUnity側に渡す
  5. Androidと同じように、「File.ReadAllBytes」で4のパスから画像データを読み込み、MaterialのmainTextureとして設定する

PhoneSelector.m

〜省略〜
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    // カメラロールから選んだ写真のURLを取得.
    UIImage *myUIImage = [info objectForKey:UIImagePickerControllerOriginalImage];
    NSData *imageData = UIImagePNGRepresentation(myUIImage);
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    // 適当なファイル名をつける.
    NSString *filePath = [documentsDirectory stringByAppendingPathComponent:@"tmp.jpg"];
    [imageData writeToFile:filePath atomically:YES];
    
    _mstSelectedImage = (NSMutableString *)filePath;
    // 取得したパスをUnity側に送信する.
    UnitySendMessage("CtrlSetTexture", "OnCallbackIos", MakeStringCopy([_mstSelectedImage UTF8String]));
    // カメラロールを閉じる.
    [picker dismissViewControllerAnimated:YES completion:nil];
}
char* MakeStringCopy (const char* string)
{
    if (string == NULL)
        return NULL;
    
    char* res = (char*)malloc(strlen(string) + 1);
    strcpy(res, string);
    return res;
}
@end

CtrlSetTexture.cs

using UnityEngine;
using System.Collections;
using System.IO;

public class CtrlSetTexture : MonoBehaviour
{
    public Material _mtrCube;

    public void SetNewTexture(string strPath, int intWidth, int intHeight)
    {
        if (File.Exists(strPath))
        {
            // 取得したパスから画像を読み込んでマテリアルとして設定する.
            _mtrCube.mainTexture = ReadTexture(strPath, intWidth, intHeight);
        }
    }
    Texture2D ReadTexture(string strPath, int intWidth, int intHeight)
    {
    byte[] bytReadBinary = File.ReadAllBytes(strPath);
    Texture2D txtNewImage = new Texture2D(intWidth, intHeight);
        txtNewImage.LoadImage(bytReadBinary);

    return txtNewImage;
    }
    public void OnCallbackIos(string strGotData)
    {
        // called from iOS plugin.
        this.SetNewTexture(strGotData, 2048, 1536);
    }
}
  • Androidでは「CtrlAndroidPlugin.cs」を呼び出していましたが、iOSでは「CtrlIosPlugin.cs」をSceneに置いていないため、このスクリプトを直接呼び出しています。
  • AndroidiOSで画面の比率を変更できるように、「SetNewTexture」の引数としてTextureの縦横のサイズを渡すように変更しています。

Unityに渡すデータ

「UnitySendMessage」の第3引数は、AndroidではString型でしたが、iOSでの型はconst char*です。

この型を戻り値として以下のようなメソッドを作ってやれば、プラグインにアクセスしている「CtrlIosPlugin.cs」でもstring型として戻り値を受け取ることができます。

- (const char *)GetSelectedImage
{
  return MakeStringCopy([_mstSelectedImage UTF8String]);
}

受け取る側(CtrlIosPlugin.cs)

[DllImport("__Internal")]
private static extern string getSelectedImage_();

public static string GetSelectedImage()
{
  return getSelectedImage_();
}

終わりに

これでMaterialに画像をセットすることはできるのですが、特に初回は画像を選択してから実際にMaterialに反映されるまでに少し時間がかかります。

呼び出しまでに少なくとも1フレームかかる「UnitySendMessage」を使っていたり、UIImageを一度書き出してから読み込んでいるところが原因かもしれません。
実際に使うときはもう少し素早く反映させる、または反映までの間にアニメーションを加えるなどの工夫が必要になりそうです。

参考

プラグイン

カメラロール