android開発
Vulkan

Android VulkanでCamera画像を表示する。

Qiita初投稿です。開発自体まだ初心者で稚拙で間違った内容が含まれていることがございますが、その際はアドバイスおながいします。
また、この記事はVulkanよりもAndroidに関することの方が多いです。
というよりもVulkanに関する内容はほぼないです。
AndroidでもVulkanGraphics apiが使えるのでVulkanを用いてCamera画像を表示させてみます。
ソースコードはGithubに載せています。こちら

主な流れ

主な流れは以下のようになります。

Vulkan Pool.jpg

Activity

VulkanをAndroidで使用するにはNativeのANativeWindowが必要になります。そのためかGoogleのSampleはNative Activityを使用しています。
Native Activityからだとlayout.xmlを使用する場合はJavaでNative ActivityをOverrideしたりとActivityを2つ必要があり大変なので、今回はKotlinからのAppCompatActivityで実装します。

MainActivity.kt
class MainActivity : AppCompatActivity(){
    //省略
}

View ~SurfaceView or TextureView?~

先ほど書きましたが,VulkanにはANativeWindowが必要になります。
KotlinからANativeWindowを取得する方法にANativeWindow_fromSurfaceがあります。

VulkanJni.cpp
JNIEXPORT void JNICALL
Java_view_vulkan_vulkanrenderer_JniPart_jniInitVulkan(
        JNIEnv *env, jclass type, jlong jNativeHandle, jobject jSurface) {
    ANativeWindow *window = ANativeWindow_fromSurface(env, jSurface);
    if (window == nullptr) {
        LOGE("get window from surface fail!");
        return;
    }
    VulkanMain *vulkanMain = reinterpret_cast<VulkanMain *>(jNativeHandle);
    vulkanMain->init(window);
}

このjSurfaceはKotlinで取得したSurfaceになります。このSurfaceはSurfaceViewかTextureViewで生成しますが、このSurfaceView、TextureViewそれぞれ癖があります。
それを次に書いていきます。
今回はSurfaceViewを使ってますがソースコードにはTextureViewの簡単な例を載せています。

SurefaceViewの難点

SurfaceViewのLifeSicleはonResume時にsurfaceCreatedが呼ばれ,onPause時にsurfaceDestroyedが呼ばれるようになっています。
当然ながら破棄されたSurfaceで描画できないのでonResumeするたびにVulkanの初期化ををし、onPauseするたびにVulkanを終了する必要があります。
また、onPauseが描画途中に起こった場合はエラーになります。
上図の流れがさらにややこしくなります。
破棄を怠ると、再開するたびにメモリが増え続けることになります。
意外と怠ってるsampleが多かったので苦労しました。

TextureViewの難点

TextureViewはSurfaceViewと違い、onDestroyedの時に終了処理をすればいいので、onPauseでは描画処理をとめるだけで充分なので、描画途中かどうかなんて考えなくていいです。非常に簡単です。
GogleのSampleで TextureViewをつかっているのは理解させやすいためではないでしょうか。
しかし、このことが災いしてかSurfaceの破棄法がわからないのでTextureViewはメモリの消費が激しくメモリリークの危険性があります。
くわしくは、こちらを参考してください

SurfaceViewのSampleが多く、後々のことを考えるとSurfaceViewの方が良さそうなので、SurfaceViewを採用しました。

Camera api

今回使ったCamera api はAndroid SDK24から使えるNativeCameraを使用しています。
Camera2 apiでも実装ができると思うのですが、VulkanはAndroid Nからの適用なので、Native Cameraが使えます。
GoogleのNativeCamera sampleをコピペするだけでフレームを簡単に取れます。
JNIでのBuffer通信の手間とかを考えるとNativeCameraの方が楽です。

オフスクリーン

SurfaceViewで生成したSurfaceはVulkanに接続しているため、Cameraに接続ができないです。 Camera接続用のwindowを用意します。今回はSurfaceTextureを生成し、そこからSurfaceを用意するようにします。

VulkanRenderer.kt
    private fun createOffScreenSurface(){
        val textures = IntArray(1)
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0)
        GLES20.glGenTextures(1, textures, 0)
        checkGlError()

        val texture = textures[0]
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture)
        checkGlError()

        GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR)
        GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR)

        GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
        GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)

        checkGlError()
        mOffScreenSurfaceTexture = SurfaceTexture(texture)
    }

Vulkanの初期化

いよいよVulkanについてですがVulkanの初期化はgoogle sampleのtutorial6_textureとほぼ同じです。
違うところは、textureの初期化時にtexture imageを取得する必要がないことと、
頂点情報が違うことくらいです。
google sampleは頂点を三角形型にしてたので、今回は四角系型で登録します。

VulkanMain.cpp
struct VertexUV {
    float posX, posY, posZ, posW;  // Position data
    float u, v;                    // texture u,v
};

#define XYZ1(_x_, _y_, _z_) (_x_), (_y_), (_z_), 1.f
#define UV(_u_, _v_) (_u_), (_v_)

static const VertexUV vertexUV[] = {
        {XYZ1(-1.f, -1.f, 0),  UV(0.f, 0.f)},//left  - top
        {XYZ1( 1.f, -1.f, 0),  UV(1.f, 0.f)},//right - top
        {XYZ1(-1.f,  1.f, 0),  UV(0.f, 1.f)},//left  - under
        {XYZ1( 1.f,  1.f, 0),  UV(1.f, 1.f)},//right - under
        {XYZ1( 1.f, -1.f, 0),  UV(1.f, 0.f)},//right - top
        {XYZ1(-1.f,  1.f, 0),  UV(0.f, 1.f)},//left  - under
};

(x,y,z) は頂点座標、(u,v)はtextureの座標になります。
奥行きの情報は今回必要ないので、一律で0に設定します。xy座標は中心を原点にしているのに対して、uv座標は左上が原点となっています。
AndroidのCameraは90°傾いているのが一般的なのですが、幸いにもGoogleのNativeCamera sampleが傾きに合わせてCamera Frameを取得してくれている同じ座標を登録するようにします。

Shader Compiler

Shader CompilerはUsing Android Studioを使用しています。気をつけることとしては一回のBuild Apkでは.spvが作成されない場合があるので、その時は二回実行します。

update texture

ここまできたら、いよいよカメラ画像の描画に入ります。
方針としてはCamera Frameを取得→Camera frameをtextureとして更新→描画
という順になります。

VulkanMain.cpp
void VulkanMain::updateTexture(VulkanInfo &oVulkanInfo) {
    m_image = m_image_reader->GetLatestImage();
    if (m_image == nullptr)
        return;
    ANativeWindow_acquire(offscreenWindow);
    ANativeWindow_Buffer buf;
    if (ANativeWindow_lock(offscreenWindow, &buf, nullptr) < 0) {
        m_image_reader->DeleteImage(m_image);
        return;
    }

    m_image_reader->DisplayImage(&buf, m_image);
    ANativeWindow_unlockAndPost(offscreenWindow);
    ANativeWindow_release(offscreenWindow);

DisplayImageでCameraの傾きに合わせて取得しています。
次に取得したデータをtextureの更新として貼り付けるようにします。

VulkanMain.cpp
    //続き
    void *data;
    CALL_VK(vkMapMemory(oVulkanInfo.device_info.device_, oVulkanInfo.texture_object_info.mem, 0,
                        oVulkanInfo.texture_image_info.textureSize, 0, &data));

    for (int32_t y = 0; y < oVulkanInfo.texture_object_info.tex_height; y++) {
        unsigned char *row = reinterpret_cast<unsigned char *>((char *) data +
                                                               oVulkanInfo.texture_image_info.rowPitch *
                                                               y );
        unsigned char* src = reinterpret_cast<unsigned char *>((char*)buf.bits + 4 * buf.stride * y);
        size_t srcSize = (size_t) (4 * buf.stride);
        memcpy(row , src, srcSize);
    }
}

あとは描画命令をするだけでCamera画像が表示できるようになります。

感想とか

はじめはOpenGlの勉強をしようかと考えてたのですが、新しい方を勉強した方が得ではと軽い感じで始めてみましたがここまでくるのに意外と大変でした。
まだまだVulkanや全体の設定が甘いところがあるのですが、
修正しつつCamera Dataになりかしら手を加えていこうかと考えています。

参考にしたもの

  • VARTIP

    • 先に同じことをやってくれています。私の環境では綺麗に表示されなかったので、自分なりに改良するようにしました。
  • VulkanTutorial-Android

    • JavaからVulkanの使用方がかかれています。
  • Bor_Vulkan

    • SurfaceviewでのVulkanの取り扱いが一番しっかりとしています。