Gear VR FrameworkでCanvasを使ったテクスチャー描画を行う

Androidには[Canvas](https://developer.android.com/reference/android/graphics/Canvas.html)という2D描画の仕組みがありますが、これをGear VR Frameworkの3Dシーンの描画にも応用できたら便利だなと思って、やってみました。3Dシーンの描画というより、3Dシーンの中にあるオブジェクトのテクスチャーをCanvasで描画するというアプローチです。

まず、テクスチャーを作成します。CanvasTextureと名付けました。

```java
import android.graphics.Canvas;
import android.graphics.SurfaceTexture;
import android.view.Surface;

import org.gearvrf.GVRContext;
import org.gearvrf.GVRDrawFrameListener;
import org.gearvrf.GVRExternalTexture;

import java.lang.ref.WeakReference;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

public class CanvasTexture extends GVRExternalTexture {

    private final Renderer renderer;

    public CanvasTexture(GVRContext gvrContext, Renderer renderer) {
        super(gvrContext);
        this.renderer = renderer;
        gvrContext.registerDrawFrameListener(new Listener(this));
    }

    public interface Renderer {
        boolean isDirty();

        void draw(Canvas canvas);

        int width();

        int height();
    }

    private static class Listener implements GVRDrawFrameListener {
        private final GVRContext context;
        private final WeakReference<CanvasTexture> ref;
        private SurfaceTexture surfaceTexture;
        private Surface surface;

        private Listener(CanvasTexture canvasTexture) {
            this.context = canvasTexture.getGVRContext();
            this.ref = new WeakReference<>(canvasTexture);
        }

        @Override
        public void onDrawFrame(float v) {

            CanvasTexture canvasTexture = ref.get();

            if (canvasTexture == null) {
                context.unregisterDrawFrameListener(this);
                release();
                return;
            }

            Future<Integer> futureId = canvasTexture.getFutureId();
            if (!futureId.isDone()) return;

            try {
                int id = futureId.get();

                if (surfaceTexture == null) {
                    surfaceTexture = new SurfaceTexture(id);
                }

                if (surface == null) {
                    surface = new Surface(surfaceTexture);
                }

                Renderer renderer = canvasTexture.renderer;
                if (renderer.isDirty()) {
                    surfaceTexture.setDefaultBufferSize(renderer.width(), renderer.height());

                    Canvas canvas = surface.lockCanvas(null);

                    if (canvas != null) {
                        renderer.draw(canvas);
                        surface.unlockCanvasAndPost(canvas);
                        surfaceTexture.updateTexImage();
                    }
                }

            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }

        private void release() {
            if (surfaceTexture != null) {
                surfaceTexture.release();
                surfaceTexture = null;
            }

            if (surface != null) {
                surface.release();
                surface = null;
            }
        }
    }
}
```

CanvasTextureはコンストラクターにRendererインターフェースを受け取ります。Rendererの実装クラスは`draw`メソッドでCanvasを受け取り、テクスチャーの描画を行います。必要無いのに毎フレーム再描画を行うのは処理負荷が大きくなりすぎてしまうので、`isDirty`メソッドで再描画が必要かどうかをチェックして必要な時だけ描画処理が行われるようになっています。

```java:実装例
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;

public class TestRenderer implements CanvasTexture.Renderer {

    private final Paint paint = new Paint();
    private boolean dirty = true;

    public TestRenderer() {
        paint.setColor(Color.WHITE);
    }

    @Override
    public boolean isDirty() {
        return dirty;
    }

    @Override
    public void draw(Canvas canvas) {
        canvas.drawColor(0, PorterDuff.Mode.CLEAR);
        canvas.drawText(System.currentTimeMillis() + "s", 0, 64, paint);
//        dirty = false; // falseに設定しないとisDirtyがtrueを返し続け、毎フレーム描画を行う
    }

    @Override
    public int width() {
        return 128; // Canvasの幅を指定する
    }

    @Override
    public int height() {
        return 128; // Canvasの高さを指定する
    }
}
```

上記の`TestRenderer`を`CanvasTexture`のコンストラクタに渡してテクスチャーを作成し、それを使って`GVRSceneObject`を作成します。注意点として`CanvasTexture`は`GVRExternalTexture`を継承していて、描画をするためにはOESシェーダーを指定しなければならないということです。

```java
GVRContext ctx;
CanvasTexture.Renerer renderer = new TestRenderer();
CanvasTexture texture = new CanvasTexture(ctx, renderer);
GVRSceneObject obj = new GVRSceneObject(ctx, 1, 1, texture, GVRMaterial.GVRShaderType.OES.ID);
obj.getTransform().setPosition(0, 0, -5);
ctx.getMainScene().addSceneObject(obj);
```

これで、Canvasで描画されたテクスチャー(現在時刻のミリ秒が白文字で表示される)を持ったオブジェクトが3Dシーンに現れます。