うさがにっき

読書感想文とプログラムのこと書いてきます

ViroCoreのSampleCodeを読み解き、描画周りを整理する

tiro105.hateblo.jp の続き

概要

  • 3つめのサンプルも割とよかったのでまとめることにした
    • このサンプルは公式が解説を載せているのでこっちをみる方が良いです
      blog.viromedia.com
  • 今回は光と描画関係
  • 二、三個Issue投げたら素早く、丁寧に対応してもらえてすごくよかった

詳細

とりあえずビルド動かす

  • プロジェクトARRetailをimport, AndroidManifest.xmlAPIキー修正して実行
    github.com
    f:id:tiro105:20180313165125g:plain

  • 最後のシャッターボタンが動かないのはStorageへのRuntime Permissionが設定されていないため、Issue投げたら対応してくれたのでそのうちmergeされると思う マージされました github.com

ProjectPackage構成

  • 今回はメインのクラスに絞って確認、他は前回と同じ
    f:id:tiro105:20180313165533p:plain:w300
  • assetに3dmodel(vrx), いくつかのactivityとmodelがある

読み解き対象

  • 画面遷移としては以下のようになる
    • ProductDetailActivity → ProductARActivity → ProductSelectionActivity
  • ProductDetailActivity, ProductSelectionActivityは通常のAndroidの画面のため割愛、ProductARActivityを読んでいく

ProductARActivity

クラス変数

  • ログに使うTAG、前の画面で選択した3DModelをIntentから取り出すためのkey、画面の要素とbindするための変数を宣言
    private static final String TAG = ProductARActivity.class.getSimpleName();
    final public static String INTENT_PRODUCT_KEY = "product_key";

    private ViroView mViroView;
    private View mHudGroupView;
    private TextView mHUDInstructions;
    private ImageView mCameraButton;
    private View mIconShakeView;
  • 現在の状態を保持するための列挙体を定義、宣言
    /*
     The Tracking status is used to coordinate the displaying of our 3D controls and HUD
     UI as the user looks around the tracked AR Scene.
     */
    private enum TRACK_STATUS{
        FINDING_SURFACE,
        SURFACE_NOT_FOUND,
        SURFACE_FOUND,
        SELECTED_SURFACE;
    }

    private TRACK_STATUS mStatus = TRACK_STATUS.SURFACE_NOT_FOUND;
  • ARに使うViroCore周りの変数宣言
    private Product mSelectedProduct = null;
    private Node mProductModelGroup = null;
    private Node mCrosshairModel = null;
    private AmbientLight mMainLight = null;
    private Vector mLastProductRotation = new Vector();
    private Vector mSavedRotateToRotation = new Vector();
    private ARHitTestListenerCrossHair mCrossHairHitTest = null;

Activityのライフサイクル系

  • これまでのSampleCodeはライフサイクル系に手を加えてなかったかが、これはいろいろしている、Android触ったことあるマンとしてはこっちの方がしっくりくる
onCreate(Bundle savedInstanceState)
  • RendererConfigurationを使ってRenderingの設定、詳しくは以下を参照
    RendererConfiguration | Android Developers
  • ViroViewARCoreがいい感じに起動できたらdisplayScene()で初期設定(後述)
  • INTENT_PRODUCT_KEYから取得したmSelectedProductはdisplayScene()から呼ばれるinit3DModelProduct(arScene)で非同期にLoadされる(後述)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        RendererConfiguration config = new RendererConfiguration();
        config.setShadowsEnabled(true);
        config.setBloomEnabled(true);
        config.setHDREnabled(true);
        config.setPBREnabled(true);

        mViroView = new ViroViewARCore(this, new ViroViewARCore.StartupListener() {
            @Override
            public void onSuccess() {
                displayScene();
            }

            @Override
            public void onFailure(ViroViewARCore.StartupError error, String errorMessage) {
                Log.e(TAG, "Failed to load AR Scene [" + errorMessage + "]");
            }
        }, config);
        setContentView(mViroView);

        Intent intent = getIntent();
        String key = intent.getStringExtra(INTENT_PRODUCT_KEY);
        ProductApplicationContext context = (ProductApplicationContext)getApplicationContext();
        mSelectedProduct = context.getProductDB().getProductByName(key);

        View.inflate(this, R.layout.ar_hud, ((ViewGroup) mViroView));
        mHudGroupView = (View) findViewById(R.id.main_hud_layout);
        mHudGroupView.setVisibility(View.GONE);
    }
そのほかのライフサイクル系
  • onActivityXXX(this)メソッドが並んでいるが、これ必要なのだろうか・・・?、普通にライフサイクル通りに動いているのでいらない気がしている
    @Override
    protected void onStart() {
        super.onStart();
        mViroView.onActivityStarted(this);
    }

    @Override
    protected void onResume() {
        super.onResume();
        mViroView.onActivityResumed(this);
    }

    @Override
    protected void onPause(){
        super.onPause();
        mViroView.onActivityPaused(this);
    }

    @Override
    protected void onStop() {
        super.onStop();
        mViroView.onActivityStopped(this);
    }

    @Override
    protected void onDestroy(){
        ((ViroViewARCore)mViroView).setCameraARHitTestListener(null);
        mViroView.onActivityDestroyed(this);
        super.onDestroy();
    }

displayScene()

  • ARScene、mainとなるmMainLightの設定
  • mMainLightの影響する3DModelを定義するためにsetInfluenceBitMaskの設定、詳しくは以下を参照、結構詳しく設定できる
    virocore.viromedia.com
  • initARCrosshair(arScene)でフォーカスの初期化、 init3DModelProduct(arScene)で3DModelの初期化(いろいろ用意してあるように見せて実はobject_lamp.vrxしか呼んでないので何選んでもlampが表示される)、 initARHud()で画面のUI要素の初期設定を行う
    private void displayScene() {
        // Create the ARScene within which to load our ProductAR Experience
        ARScene arScene = new ARScene();
        mMainLight = new AmbientLight(Color.parseColor("#606060"), 400);
        mMainLight.setInfluenceBitMask(3);
        arScene.getRootNode().addLight(mMainLight);

        // Setup our 3D and HUD controls
        initARCrosshair(arScene);
        init3DModelProduct(arScene);
        initARHud();

        // Start our tracking UI when the scene is ready to be tracked
        arScene.setListener(new ARSceneListener());

        // Finally set the arScene on the renderer
        mViroView.setScene(arScene);
    }
init3DModelProduct(ARScene scene)
  • これは結構面白いことやってるのでメモっておく
  • spotLight(spotLight), shadowNode(Node), productModel(Object3D)をmProductModelGroupにaddXXXしている、これにより一つのNodeに光、影、3DModelを扱えるようにする
    private void init3DModelProduct(ARScene scene){
        // Create our group node containing the light, shadow plane, and 3D models
        mProductModelGroup = new Node();

        // Create a light to be shined on the model.
        Spotlight spotLight = new Spotlight();
        spotLight.setInfluenceBitMask(1);
        spotLight.setPosition(new Vector(0,5,0));
        spotLight.setCastsShadow(true);
        spotLight.setAttenuationEndDistance(7);
        spotLight.setAttenuationStartDistance(4);
        spotLight.setDirection(new Vector(0,-1,0));
        spotLight.setIntensity(6000);
        spotLight.setShadowOpacity(0.35f);
        mProductModelGroup.addLight(spotLight);

        // Create a mock shadow plane in AR
        Node shadowNode = new Node();
        Surface shadowSurface = new Surface(20,20);
        Material material = new Material();
        material.setShadowMode(Material.ShadowMode.TRANSPARENT);
        material.setLightingModel(Material.LightingModel.LAMBERT);
        shadowSurface.setMaterials(Arrays.asList(material));
        shadowNode.setGeometry(shadowSurface);
        shadowNode.setLightReceivingBitMask(1);
        shadowNode.setPosition(new Vector(0,-0.01,0));
        shadowNode.setRotation(new Vector(-1.5708,0,0));
        mProductModelGroup.addChildNode(shadowNode);

        // Load the model from the given mSelected Product
        final Object3D productModel = new Object3D();
        productModel.loadModel(Uri.parse(mSelectedProduct.m3DModelUri), Object3D.Type.FBX, new AsyncObject3DListener() {
            @Override
            public void onObject3DLoaded(Object3D object3D, Object3D.Type type) {
                object3D.setLightReceivingBitMask(1);
                mProductModelGroup.setOpacity(0);
                mProductModelGroup.setScale(new Vector(0.9, 0.9, 0.9));
                mLastProductRotation = object3D.getRotationEulerRealtime();
            }

            @Override
            public void onObject3DFailed(String error) {
                Log.e("Viro"," Model load failed : " + error);
            }
        });

        // Make this 3D Product object draggable.
        mProductModelGroup.setDragType(Node.DragType.FIXED_TO_WORLD);
        mProductModelGroup.setDragListener(new DragListener() {
            @Override
            public void onDrag(int i, Node node, Vector vector, Vector vector1) {
                // No-op
            }
        });

        // Set click listeners on this 3D product
        productModel.setClickListener(new ClickListener() {
            @Override
            public void onClick(int i, Node node, Vector vector) {
                // No-op
            }

            @Override
            public void onClickState(int i, Node node, ClickState clickState, Vector vector) {
                onModelClick(clickState);
            }
        });

        // Set gesture listeners such that the user can rotate this model.
        productModel.setGestureRotateListener(new GestureRotateListener() {
            @Override
            public void onRotate(int source, Node node, float radians, RotateState rotateState) {
                Vector rotateTo = new Vector(mLastProductRotation.x, mLastProductRotation.y + radians, mLastProductRotation.z);
                productModel.setRotation(rotateTo);
                mSavedRotateToRotation = rotateTo;
            }
        });

        mProductModelGroup.setOpacity(0);
        mProductModelGroup.addChildNode(productModel);
        scene.getRootNode().addChildNode(mProductModelGroup);
    }

ARSceneListener

  • いつもの
    protected class ARSceneListener implements ARScene.Listener {
        @Override
        public void onTrackingInitialized() {
            // The Renderer is ready - turn everything visible.
            mHudGroupView.setVisibility(View.VISIBLE);

            // Update our UI views to the finding surface state.
            setTrackingStatus(TRACK_STATUS.FINDING_SURFACE);
        }

        @Override
        public void onAmbientLightUpdate(float lightIntensity, float colorTemperature) {
            // no-op
        }

        @Override
        public void onAnchorFound(ARAnchor anchor, ARNode arNode) {
            // no-op
        }

        @Override
        public void onAnchorUpdated(ARAnchor anchor, ARNode arNode) {
            // no-op
        }

        @Override
        public void onAnchorRemoved(ARAnchor anchor, ARNode arNode) {
            // no-op
        }
    }
setTrackingStatus(TRACK_STATUS status)
  • 平面認識時にいろいろアップデートする
  • updateUIHud()はUIの文言など、update3DARCrosshair()はフォーカスの制御を、update3DModelProduct()は3DModelの制御を行う
  • 中身はそんな難しくなくmStatusの中身見てopacity変えてるだけ
    private void setTrackingStatus(TRACK_STATUS status) {
        if (mStatus == TRACK_STATUS.SELECTED_SURFACE || mStatus == status){
            return;
        }

        // If the surface has been selected, we no longer need our cross hair listener.
        if (status == TRACK_STATUS.SELECTED_SURFACE){
            ((ViroViewARCore)mViroView).setCameraARHitTestListener(null);
        }

        mStatus = status;
        updateUIHud();
        update3DARCrosshair();
        update3DModelProduct();
    }

ARHitTestListenerCrossHair

  • 平面認識時にの挙動を定義できるListenerを実装したもの
    ARHitTestListener | Android Developers
  • 平面を認識した際のフォーカスの動きを制御している、画面のUIを制御しているのは上述のsetTrackingStatus
    private class ARHitTestListenerCrossHair implements ARHitTestListener {
        @Override
        public void onHitTestFinished(ARHitTestResult[] arHitTestResults) {
            if( arHitTestResults == null || arHitTestResults.length <=0) {
                return;
            }

            // If we have found intersected AR Hit points, update views as needed, reset miss count.
            ViroViewARCore viewARView = (ViroViewARCore)mViroView;
            final Vector cameraPos  = viewARView.getLastCameraPositionRealtime();

            // Grab the closest ar hit target
            float closestDistance = Float.MAX_VALUE;
            ARHitTestResult result = null;
            for (int i = 0; i < arHitTestResults.length; i++) {
                ARHitTestResult currentResult = arHitTestResults[i];

                float distance = currentResult.getPosition().distance(cameraPos);
                if (distance < closestDistance && distance > .3 && distance < 5){
                    result = currentResult;
                    closestDistance = distance;
                }
            }

            // Update the cross hair target location with the closest target.
            animateCrossHairToPosition(result);

            // Update State based on hit target
            if (result != null){
                setTrackingStatus(TRACK_STATUS.SURFACE_FOUND);
            } else {
                setTrackingStatus(TRACK_STATUS.FINDING_SURFACE);
            }
        }

        private void animateCrossHairToPosition(ARHitTestResult result){
            if (result == null) {
                return;
            }

            AnimationTransaction.begin();
            AnimationTransaction.setAnimationDuration(70);
            AnimationTransaction.setTimingFunction(AnimationTimingFunction.EaseOut);
            mCrosshairModel.setPosition(result.getPosition());
            mCrosshairModel.setRotation(result.getRotation());
            AnimationTransaction.commit();
        }
    }

感想

  • LightやMaterialまわりのドキュメントがしっかり作ってあり、かなり使いやすい
    • RendererConfigurationだけでもOpen GLESでやろうと思うと気が滅入りそうだけど大事なことが揃っており嬉しい
  • Issueに対する返事が早く、丁寧で嬉しい
    github.com
  • ドキュメントがかなりしっかりしていて助かる
  • 次はPhysicsを触ってみようかな