インテル® INDE Media for Mobile チュートリアル – Android* 上での Unity3d* アプリケーションの高度なビデオ・キャプチャー

同カテゴリーの次の記事

プログラマーの生産性を高める

この記事は、インテル® デベロッパー・ゾーンに掲載されている「Intel® INDE Media for Mobile Tutorials – Advanced Video Capturing for Unity3d* Applications on Android*」の日本語参考訳です。


チュートリアル 1 の最後で、Unity* GUI レイヤーの問題に触れました。既存の GUI を多用する複雑な Unity* ゲームでは どうしたら良いのでしょうか? ここでは、そのための インテル® INDE Media for Mobile の高度な使用方法を示します。Unity* の無料バージョンを使って、フルスクリーン・イメージの後処理効果を使用しないアプローチを紹介します。

必要条件:

最初に、チュートリアル 1 で説明したように、インテル® INDE Media for Mobile をゲームに統合します。統合手順は、チュートリアル 1 を参照してください。ここでは、すでに統合されていることを前提に、必要な変更に注目して説明します。

Capturing.java ファイルを開きます。クラスは次のようになります。

package com.intel.inde.mp.samples.unity;

import com.intel.inde.mp.IProgressListener;
import com.intel.inde.mp.domain.Resolution;
import com.intel.inde.mp.android.graphics.FullFrameTexture;
import com.intel.inde.mp.android.graphics.FrameBuffer;
import com.intel.inde.mp.android.graphics.EglUtil;

import android.os.Environment;
import android.content.Context;

import java.io.IOException;
import java.io.File;

public class Capturing
{
    private static FullFrameTexture texture;
    private FrameBuffer frameBuffer;
    private VideoCapture videoCapture;
    
    private IProgressListener progressListener = new IProgressListener() {
        @Override
        public void onMediaStart() {
        }

        @Override
        public void onMediaProgress(float progress) {
        }

        @Override
        public void onMediaDone() {
        }

        @Override
        public void onMediaPause() {
        }

        @Override
        public void onMediaStop() {
        }

        @Override
        public void onError(Exception exception) {
        }
    };
    
    public Capturing(Context context, int width, int height)
    {
        videoCapture = new VideoCapture(context, progressListener);
        
        frameBuffer = new FrameBuffer(EglUtil.getInstance());
        frameBuffer.setResolution(new Resolution(width, height));

        texture = new FullFrameTexture();
    }

    public static String getDirectoryDCIM()
    {
        return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + File.separator;
    }

    public void initCapturing(int width, int height, int frameRate, 
        int bitRate)
    {
        VideoCapture.init(width, height, frameRate, bitRate);
    }

    public void startCapturing(String videoPath)
    {
        if (videoCapture == null) {
            return;
        }
        synchronized (videoCapture) {
            try {
                videoCapture.start(videoPath);
            } catch (IOException e) {
            }
        }
    }
    
    public void beginCaptureFrame()
    {
        frameBuffer.bind();
    }
    
    public void captureFrame(int textureID)
    {
        if (videoCapture == null) {
            return;
        }
        synchronized (videoCapture) {
            videoCapture.beginCaptureFrame();
            texture.draw(textureID);
            videoCapture.endCaptureFrame();
        }
    }
    
    public void endCaptureFrame()
    {
        frameBuffer.unbind();
        int textureID = frameBuffer.getTextureId();
        captureFrame(textureID);
        texture.draw(textureID);    
    }

    public void stopCapturing()
    {
        if (videoCapture == null) {
            return;
        }
        synchronized (videoCapture) {
            if (videoCapture.isStarted()) {
                videoCapture.stop();
            }
        }
    }

}

いくつかの変更が行われているのが分かります。主な変更点として、frameBuffer メンバーには、適切なサイズの FrameBuffer を作成するため、コンストラクターに width と height パラメーターが追加されています。また、次の 3 つの新しいパブリックメソッドも追加されています: frameBufferTexture()、beginCaptureFrame()、および endCaptureFrame()。これらのメソッドの役割は、後で C# 側で明らかになります。

何も変更せずに VideoCapture.java ファイルを閉じます。パッケージ名に注意してください。このパッケージ名は、Unity* のプレーヤー設定にある Bundle Identifier と同じでなければなりません。マニフェスト・ファイルも忘れないでください。必要なすべての権限と機能を設定します。

これで、/Plugins/Android 以下に AndroidManifest.xml と Java* ファイルが作成されます。Apache* Ant* スクリプトを作成し、すべてをビルドします。詳しい手順は、チュートリアル 1 を参照してください。ディレクトリーに Capturing.jar が作成されていることを確認します。

それでは、Unity* に移りましょう。Capture.cs を開き、その内容を次のコードに置き換えます。

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

[RequireComponent(typeof(Camera))]
public class Capture : MonoBehaviour
{
    public int videoWidth = 720;
    public int videoHeight = 1094;
    public int videoFrameRate = 30;
    public int videoBitRate = 3000;

    private string videoDir;
    public string fileName = "game_capturing-";
    
    private float nextCapture = 0.0f;
    public bool inProgress { get; private set; }
    private bool finalizeFrame = false;

    private AndroidJavaObject playerActivityContext = null;
    
    private static IntPtr constructorMethodID = IntPtr.Zero;
    private static IntPtr initCapturingMethodID = IntPtr.Zero;
    private static IntPtr startCapturingMethodID = IntPtr.Zero;
    private static IntPtr beginCaptureFrameMethodID = IntPtr.Zero;
    private static IntPtr endCaptureFrameMethodID = IntPtr.Zero;
    private static IntPtr stopCapturingMethodID = IntPtr.Zero;

    private static IntPtr getDirectoryDCIMMethodID = IntPtr.Zero;

    private IntPtr capturingObject = IntPtr.Zero;

    void Start()
    {
        if (!Application.isEditor) {
            // 最初に現在のアクティビティー・コンテキストを取得します
            using (AndroidJavaClass jc = 
                new AndroidJavaClass("com.unity3d.player.UnityPlayer")) {
                    playerActivityContext = 
                        jc.GetStatic<AndroidJavaObject>("currentActivity");
            }

            // クラスを探します
            IntPtr classID = 
                AndroidJNI.FindClass("com/intel/penelope/Capturing");

            // クラスのコンストラクターを探します
            constructorMethodID = 
                AndroidJNI.GetMethodID(classID, "<init>", 
                    "(Landroid/content/Context;II)V");

            // メソッドを登録します
            initCapturingMethodID = 
                AndroidJNI.GetMethodID(classID, "initCapturing", "(IIII)V");
            startCapturingMethodID = 
                AndroidJNI.GetMethodID(classID, "startCapturing", 
                    "(Ljava/lang/String;)V");
            beginCaptureFrameMethodID = 
                AndroidJNI.GetMethodID(classID, "beginCaptureFrame", "()V");
            endCaptureFrameMethodID = 
                AndroidJNI.GetMethodID(classID, "endCaptureFrame", "()V");
            stopCapturingMethodID = 
                AndroidJNI.GetMethodID(classID, "stopCapturing", "()V");

            // 静的メソッドを登録し呼び出します
            getDirectoryDCIMMethodID = 
                AndroidJNI.GetStaticMethodID(classID, "getDirectoryDCIM", 
                    "()Ljava/lang/String;");
            jvalue[] args = new jvalue[0];
            videoDir = 
                AndroidJNI.CallStaticStringMethod(classID, 
                    getDirectoryDCIMMethodID, args);

            // キャプチャー・オブジェクトを作成します
            jvalue[] constructorParameters = 
                AndroidJNIHelper.CreateJNIArgArray(new object [] 
                    { playerActivityContext, Screen.width, Screen.height });
            IntPtr local_capturingObject = 
                AndroidJNI.NewObject(classID, constructorMethodID, 
                    constructorParameters);
            if (local_capturingObject == IntPtr.Zero) {
                Debug.LogError("Can't create Capturing object");
                return;
            }

            // キャプチャー・オブジェクトのグローバル参照を保持します
            capturingObject = AndroidJNI.NewGlobalRef(local_capturingObject);
            AndroidJNI.DeleteLocalRef(local_capturingObject);

            AndroidJNI.DeleteLocalRef(classID);
        }
        inProgress = false;
        nextCapture = Time.time;
    }

    void OnPreRender()
    {
        if (inProgress && Time.time > nextCapture) {
            finalizeFrame = true;
            nextCapture += 1.0f / videoFrameRate;
            BeginCaptureFrame();
        }
    }

    public IEnumerator OnPostRender()
    {
        if (finalizeFrame) {
            finalizeFrame = false;
            yield return new WaitForEndOfFrame();
            EndCaptureFrame();
        } else {
            yield return null;
        }
    }

    public void StartCapturing()
    {
        if (capturingObject == IntPtr.Zero)
            return;

        jvalue[] videoParameters =  new jvalue[4];
        videoParameters[0].i = videoWidth;
        videoParameters[1].i = videoHeight;
        videoParameters[2].i = videoFrameRate;
        videoParameters[3].i = videoBitRate;
        AndroidJNI.CallVoidMethod(capturingObject, initCapturingMethodID, 
            videoParameters);
        DateTime date = DateTime.Now;
        string fullFileName = 
            fileName + date.ToString("ddMMyy-hhmmss.fff") + ".mp4";
        jvalue[] args = new jvalue[1];
        args[0].l = AndroidJNI.NewStringUTF(videoDir + fullFileName);
        AndroidJNI.CallVoidMethod(capturingObject, 
            startCapturingMethodID, args);

        inProgress = true;
    }

    private void BeginCaptureFrame()
    {
        if (capturingObject == IntPtr.Zero)
            return;

        jvalue[] args = new jvalue[0];
        AndroidJNI.CallVoidMethod(capturingObject, 
            beginCaptureFrameMethodID, args);
    }

    private void EndCaptureFrame()
    {
        if (capturingObject == IntPtr.Zero)
            return;

        jvalue[] args = new jvalue[0];
        AndroidJNI.CallVoidMethod(capturingObject, 
            endCaptureFrameMethodID, args);
    }

    public void StopCapturing()
    {
        inProgress = false;

        if (capturingObject == IntPtr.Zero)
            return;

        jvalue[] args = new jvalue[0];
        AndroidJNI.CallVoidMethod(capturingObject, 
            stopCapturingMethodID, args);
    }

}

これが最も大きな変更点ですが、ロジックは単純なものです。スクリーンサイズを Capturing.java コンストラクターに渡します。コンストラクターのシグネチャーが (Landroid/content/Context;II)V に変わっています。Java* で FrameBuffer を作成します。OnPreRender() は、カメラがシーンのレンダリングを開始する前に呼び出されます。ここで FrameBuffer をバインドします。シーンの実際のレンダリングはすべてオフスクリーンで行われます。OnPostRender() は、カメラがシーンのレンダリングを終了した後に呼び出されます。フレームの最後まで待ってから、デフォルトのオンスクリーンの FrameBuffer に切り替え、スクリーンに直接テクスチャーをコピーします (Capturing.java にある endCaptureFrame() メソッド)。Graphics.Blit() には Unity Pro* が必要なため、ここでは使用できません。同じテクスチャーを使用してフレームをキャプチャーします。

キャプチャー・アルゴリズムのゲーム・パフォーマンスへの影響を把握するため、単純な FPSCounter クラスを作成してみましょう。

using UnityEngine;
using System.Collections;

public class FPSCounter : MonoBehaviour
{
    public float updateRate = 4.0f; // 1 秒あたり 4 回更新

    private int frameCount = 0;
    private float nextUpdate = 0.0f;
    private float fps = 0.0f;
    private GUIStyle style = new GUIStyle();

    void Start()
    {
        style.fontSize = 48;
        style.normal.textColor = Color.white;

        nextUpdate = Time.time;
    }

    void Update()
    {
        frameCount++;
        if (Time.time > nextUpdate) {
            nextUpdate += 1.0f / updateRate;
            fps = frameCount * updateRate;
            frameCount = 0;
        }
    }

    void OnGUI()
    {
        GUI.Label(new Rect(10, 110, 300, 100), "FPS: " + fps, style);
    }
}

このスクリプトをシーンの任意のオブジェクトに追加します。

これだけです。それでは、Android* プラットフォーム向けにテスト・アプリケーションをビルドし実行してみましょう。キャプチャーしたビデオは、Android* デバイスの /mnt/sdcard/DCIM/ フォルダーに保存されます。

既知の問題:

  • このアプローチでは、オフスクリーン・レンダリング (ドロップシャドウ、遅延シェーディング、フルスクリーンの後処理効果) をキャプチャーできません。

関連記事