うさがにっき

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

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を触ってみようかな

ViroCoreのSampleCodeを読み解く2

概要

  • 前回ViroCoreを試してだいぶよかった
  • 他に二つサンプルコードがあるので読み解く
  • 今回は3DModel(fbx)のload, 操作関係のメモ

詳細

とりあえずビルド、動かす

  • サンプルの中のARPlacingObjectsをimport、ビルド
    github.com https://raw.githubusercontent.com/viromedia/virocore/master/ARPlacingObjects/ViroARHitTestDemoActivity.gif

Projectを眺めてViroCoreの3DModelのloadや操作について確認

ProjectPackage構成

  • ViroActivity, ViroHelperについては中身同じなので前回の記事参照
  • ViroARObjectPlacementActivity.javaを読み解いていく
    f:id:tiro105:20180307150544p:plain:w200

ViroARObjectPlacementActivity.java

クラス変数

  • 3DModelを配置する際のカメラからの距離定義
  // Constants used to determine if plane or point is within bounds. Units in meters.
    static final float MIN_DISTANCE = .2f;
    static final float MAX_DISTANCE = 10f;
  • ARSceneと3DModelを保持する変数(Draggable3dObject)の宣言
    /*
    Reference to the arScene we will be creating within this activity
    */
    private ARScene mScene;

    /*
     List of draggable 3d objects in our scene.
     */
    private List<Draggable3dObject> mDraggableObjects;

コンストラクタ

  • Activityのコンストラクタで3DModelを保持するListをnew
    public ViroARObjectPlacementActivity() {
        mDraggableObjects = new ArrayList<Draggable3dObject>();
    }

onRendererStart()

  • 最初に全体の描画の流れを追って、最後にDraggable3dObjectについて説明
  • まずonCreate時の呼ばれるonRendererStart()
    @Override
    public void onRendererStart() {
        mScene = new ARScene();
        mScene.displayPointCloud(true);
        //add a listener to the scene so we can update 'AR Init' text.
        mScene.setListener(new ARSceneListener(this, mViroView));
        //add a light to the scene so our models can show up.
        mScene.getRootNode().addLight(new AmbientLight(Color.WHITE, 1000f));
        mViroView.setScene(mScene);
        View.inflate(this, R.layout.viro_view_ar_hit_test_hud, ((ViewGroup) mViroView));
    }
  • mScene.displayPointCloud(true);は自分で追加した、これがないと特徴点を認識しているかどうかがわからない
  • インスタンス化したmSceneに対してLinstenerや光を追加、mSceneをViewに設定

ARSceneListener

  • mScene(ARScene)に設定するListener
  • 前回の記事でいうTrackedPlanesController、実装すべきメソッドは以下を参照
    ARScene.Listener | Android Developers
  • 今回は初期化時に初期化が終わったTextを表示しているだけ
    private static class ARSceneListener implements ARScene.Listener {
        private WeakReference<Activity> mCurrentActivityWeak;

        public ARSceneListener(Activity activity, View rootView) {
            mCurrentActivityWeak = new WeakReference<Activity>(activity);
        }

        @Override
        public void onTrackingInitialized() {
            Activity activity = mCurrentActivityWeak.get();
            if (activity == null){
                return;
            }

            TextView initText = (TextView)activity.findViewById(R.id.initText);
            initText.setText("AR is initialized.");
        }

        @Override
        public void onAmbientLightUpdate(float v, float v1) {

        }

        @Override
        public void onAnchorFound(ARAnchor arAnchor, ARNode arNode) {

        }

        @Override
        public void onAnchorRemoved(ARAnchor arAnchor, ARNode arNode) {

        }

        @Override
        public void onAnchorUpdated(ARAnchor arAnchor, ARNode arNode) {

        }
    }

showPopup(View v)

  • layoutのonClick属性から呼び出されているメソッド
       <ImageButton
            android:id="@+id/imageButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="showPopup"
            android:background="@mipmap/viro_launcher" />
  • AlertDialogを表示して選択された3DModelをplaceObjectメソッドで配置、placeObjectメソッドについては後述
  • なおvrxとはViroMedia独自の3DModelファイル形式(ViroFBX)、以下のツールで作成することができる
    virocore.viromedia.com
   public void showPopup(View v) {

        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        CharSequence itemsList[] = {"Coffee mug", "Flowers", "Smile Emoji"};
        builder.setTitle("Choose an object")
                .setItems(itemsList, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int which) {
                        switch (which) {
                            case 0:
                                placeObject("file:///android_asset/object_coffee_mug.vrx");
                                break;
                            case 1:
                                placeObject("file:///android_asset/object_flowers.vrx");
                                break;
                            case 2:
                                placeObject("file:///android_asset/emoji_smile.vrx");
                                break;
                        }
                    }
                });


        Dialog d = builder.create();
        d.show();
    }

placeObject(final String fileName)

  • hitTestを行い、その結果から3DModelを配置する
    private void placeObject(final String fileName) {
        ViroViewARCore viewARView = (ViroViewARCore)mViroView;
        final Vector cameraPos  = viewARView.getLastCameraPositionRealtime();
        viewARView.performARHitTestWithRay(viewARView.getLastCameraForwardRealtime(), new ARHitTestListener() {
            @Override
            public void onHitTestFinished(ARHitTestResult[] arHitTestResults) {
                if(arHitTestResults != null ) {
                    if(arHitTestResults.length > 0) {
                        for (int i = 0; i < arHitTestResults.length; i++) {
                            ARHitTestResult result = arHitTestResults[i];
                            float distance = result.getPosition().distance(cameraPos);
                            if(distance > MIN_DISTANCE && distance < MAX_DISTANCE) {
                                // If we found a plane of feature point greater than .2 and less than 10 meters away
                                // then choose it!
                                add3dDraggableObject(fileName, result.getPosition());
                                return;
                            }
                        }
                    }
                }
                Toast.makeText(ViroARObjectPlacementActivity.this, "Unable to find suitable point or plane to place object!", Toast.LENGTH_LONG).show();
            }
        });
    }

add3dDraggableObject(String filename, Vector position)

  • Draggable3dObjectを作成し、listに追加して各種イベントを設定する、Draggable3dObjectについては後述
    private void add3dDraggableObject(String filename, Vector position) {
        Draggable3dObject draggable3dObject = new Draggable3dObject(filename);
        mDraggableObjects.add(draggable3dObject);
        draggable3dObject.addModelToPosition(position);
    }

Draggable3dObject

  • 3DModeのロード、移動、回転などのイベントを管理するクラス
クラス変数、コンストラクタ
        private String mFileName;
        private float rotateStart;
        private float scaleStart;


        public Draggable3dObject(String filename) {
            mFileName = filename;
        }
addModelToPosition(Vector position)
        private void addModelToPosition(Vector position) {
            final Object3D object3D = new Object3D();
            object3D.setPosition(position);
            // Shrink the objects as the original size is too large.
            object3D.setScale(new Vector(.2f, .2f, .2f));
  • Object3Dに対して、回転(setGestureRotateListener)、PinchInOut(setGesturePinchListener)、Drag(setDragListener)を設定
           // https://developer.viromedia.com/virocore/reference/com/viro/core/GestureRotateListener.html
            // nodeへのRotateイベントの口が予め用意されている
            object3D.setGestureRotateListener(new GestureRotateListener() {
                @Override
                public void onRotate(int i, Node node, float rotation, RotateState rotateState) {
                    if(rotateState == RotateState.ROTATE_START) {
                        rotateStart = object3D.getRotationEulerRealtime().y;
                    }
                    float totalRotationY = rotateStart + rotation;
                    object3D.setRotation(new Vector(0, totalRotationY, 0));
                }
            });

            // nodeへのPinch-InOutイベントの口が予め用意されている
            // https://developer.viromedia.com/virocore/reference/com/viro/core/GesturePinchListener.html
            object3D.setGesturePinchListener(new GesturePinchListener() {
                @Override
                public void onPinch(int i, Node node, float scale, PinchState pinchState) {
                    if(pinchState == PinchState.PINCH_START) {
                        scaleStart = object3D.getScaleRealtime().x;
                    } else {
                        object3D.setScale(new Vector(scaleStart * scale, scaleStart * scale, scaleStart * scale));
                    }
                }
            });

            object3D.setDragListener(new DragListener() {
                @Override
                public void onDrag(int i, Node node, Vector vector, Vector vector1) {

                }
            });
  • 非同期に3DModelをloadする
            // Load the Android model asynchronously.
            // loadできるモデルはFBX or OBJ
            // https://developer.viromedia.com/virocore/reference/com/viro/core/Object3D.html#loadModel(android.net.Uri, com.viro.core.Object3D.Type, com.viro.core.AsyncObject3DListener)
            object3D.loadModel(Uri.parse(mFileName), Object3D.Type.FBX, new AsyncObject3DListener() {
                @Override
                public void onObject3DLoaded(final Object3D object, final Object3D.Type type) {
                  //TODO: Display toast saying model loaded successfully.
                }

                @Override
                public void onObject3DFailed(String s) {
                    Toast.makeText(ViroARObjectPlacementActivity.this, "An error occured when loading the 3d Object!", Toast.LENGTH_LONG).show();
                }
            });
  • 3DModelのDragType定義、mSceneに配置
    // Make the object draggable.
            // https://developer.viromedia.com/virocore/reference/com/viro/core/Node.html#setDragType(com.viro.core.Node.DragType)
            // https://developer.viromedia.com/virocore/reference/com/viro/core/Node.DragType.html
            object3D.setDragType(Node.DragType.FIXED_TO_WORLD);
            mScene.getRootNode().addChildNode(object3D);

感想

  • fbxモデルも簡単にロードでき、簡単な操作(PinchInOut, Rotation)はデフォルトで定義されているので非常に扱いやすい
  • vrxなのはfbx sdkを使うとアプリサイズが大きくなりすぎてしまうらしい
  • glTFなどの形式も実装想定らしい
    github.com
  • やはりOpenGLESを扱うことを考えれば大変良い
  • 次は3つ目のサンプル読んで見てメモるべきことがあれば書く

ViroCoreの使い方、samplecode解説

概要

  • ViroMedia, ViroCoreの解説
  • ViroCoreを使ってみる
  • ViroCoreのsamplecodeを読んで何をしているか読み解く

詳細

ViroMediaとは

  • UnityやUnreal Engineを使わないMobileNative環境でAR/VR開発をするためのライブラリを作っている、利用料無料(2018/3/2現在)
    viromedia.com

  • 主なライブラリは以下の二つ

    • ViroReact

      • React NativeでAR/VRが簡単に開発できるライブラリ
        viromedia.com
    • ViroCore

      • Android NativeでAR/VR開発が簡単に開発できるライブラリ、XCodeのSceneKitのようにAndroidでAR/VR開発ができるようになる
        viromedia.com

ViroCoreとは

  • 今回はViroCoreを使ってARCoreを使ってみる
  • 上述の通り、ViroCoreではSceneKitのようにAndroidでAR/VR開発ができるようになるというのがポイントだが、ViroMedia曰く他にも以下のようなメリットがあるらしい
    • Android Platform(ARCore/Cardboard/GearVR/Daydream) Support
    • INTUITIVE JAVA DEVELOPMENT(SceneKitっぽく書ける)
    • POWERFUL RENDERER
    • PHYSICS ENGINE
    • DOCUMENTATION(確かにドキュメントはしっかりしている感じ)

とりあえずビルド、動かす

virocore.viromedia.com

            <meta-data
            android:name="com.viromedia.API_KEY"
            android:value="APIKEYを取得して入力" />

別のSampleを動かす

  • 上記のGetting-startedのものはARCore感がないのでもうちょっとちゃんとしたSampleを試してみる
    github.com

  • 「ARHelloWorldAndroid」プロジェクトをimport、上記と同じようにAPIKEYを設定すれば動くはず、もし動かない場合はBuild > Select Build Variants...からarcoreDebugを選択
    f:id:tiro105:20180302174247g:plain

Projectを眺めてViroCore雰囲気を掴む

Project Package構成

f:id:tiro105:20180302174914p:plain:w500

  • src/main/java/com.example.virosample下のソースがメイン
  • 3D Model(objとかtextureとか)はasset下に配置
  • gvr用ARCore用でAndroidManifest.xmlがある(productFlavorsで管理している)
  • BuildVariantでgvrDebugとかを選べば同じプロジェクトからGVR用のapkを出力できる、だがこのプロジェクトではARCore用のコードしかないのでGVRを選んでも真っ黒な画面が表示されるだけ

ViroHelper.java

  • assetから画像(obj用のtexture)を取得するメソッドのみ

ViroActivity.java

  • BaseActivity的に使っているクラス、実際の処理はこのActivityを継承して記述する
  • productFravorsを見て、適切なViewをActivityに配置
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (BuildConfig.VIRO_PLATFORM.equalsIgnoreCase("GVR")) {
            mViroView = createGVRView();

        } else if (BuildConfig.VIRO_PLATFORM.equalsIgnoreCase("OVR")) {
            mViroView = createOVRView();

        } else if (BuildConfig.VIRO_PLATFORM.equalsIgnoreCase("Scene")) {
            mViroView = createViroViewScene();
        } else if (BuildConfig.VIRO_PLATFORM.equalsIgnoreCase("ARCore")) {
            mViroView = createViroARCoreScene();
        }
        setContentView(mViroView);
    }
  • 今回はARCoreなのでcreateViroARCoreScene()で作ったViewが使われる
    private ViroView createViroARCoreScene() {
        RendererConfiguration config = new RendererConfiguration();
        config.setShadowsEnabled(true);
        config.setBloomEnabled(true);
        config.setHDREnabled(true);
        config.setPBREnabled(true);

        // ViroViewARCore
        // https://developer.viromedia.com/virocore/reference/com/viro/core/ViroViewARCore.html
        // RendererConfigurationを指定することができる
        ViroViewARCore viroView = new ViroViewARCore(this, new ViroViewARCore.StartupListener() {
            @Override
            public void onSuccess() {
                onRendererStart();
            }

            @Override
            public void onFailure(ViroViewARCore.StartupError error, String errorMessage) {
                onRendererFailed(error.toString(), errorMessage);
            }
        }, config);
        return viroView;
    }
  • なおonRendererStart()とonRendererFailed()はこのクラスで空で実装されており、継承クラスで実装する
    // 子クラスで実装してほしいらしい
    public void onRendererStart() {
        // Override this function to start building your scene here!
    }

    // 子クラスで実装してほしいらしい
    public void onRendererFailed(String error, String errorMessage) {
        // Fail as you wish!
    }

ViroARHelloWorldActivity.java

  • メインの処理を行っているクラス、ここにSceneKitっぽくいろいろ書いてある
  • onRendererStart()の実装
    • ARSceneを作ってViewに設定している
    • ARSceneに対してARScene.Listenerを実装したTrackedPlanesControllerを設定
    • anchorから平面認識して、planeの描画などはTrackedPlanesControllerが担当
    /**
     * Create an AR scene that tracks planes. Clicking on a plane places a 3D Object on that spot.
     */
    @Override
    public void onRendererStart() {
        // Create the 3d ar scene, and display the point clouds.
        mScene = new ARScene();
        mScene.displayPointCloud(true);

        // Create an TrackedPlanesController to visually display tracked planes
        TrackedPlanesController controller = new TrackedPlanesController(this, mViroView);

        // Spawn a 3D Droid on the position where the user has clicked on a tracked plane.
        // plane click時の処理はcontrollerに委譲
        controller.addOnPlaneClickListener(new ClickListener() {
            @Override
            public void onClick(int i, Node node, Vector clickPosition) {
                // Droidくん配置
                createDroidAtPosition(clickPosition);
            }

            @Override
            public void onClickState(int i, Node node, ClickState clickState, Vector vector) {
                //No-op
            }
        });

        mScene.setListener(controller);
        mViroView.setScene(mScene);
    }
  • TrackedPlanesControllerの実装
    • 平面を認識してplaneを作成して配置するなどを行う
    • onAnchorFound()でarAnchor.getType()がplaneなら平面と認識していろいろ処理をしている
    • 以下のドキュメントにもある通り、ARScene.Listenerを実装していることによりARSceneの各イベント時の挙動を定義できる
      ARScene.Listener | Android Developers
    /**
     * An TrackedPlanesController that tracks planes and renders a surface on them.
     */
    private static class TrackedPlanesController implements ARScene.Listener {
        // WeakReference
        // https://qiita.com/yyyske/items/daa5c844647604e27e4f
        // why does this code use WeakReference
        // http://ishiitakeru-programing-memo.blogspot.jp/2015/10/android-weakreference.html
        private WeakReference<Activity> mCurrentActivityWeak;
        private boolean searchingForPlanesLayoutIsVisible = false;
        private HashMap<String, Node> surfaces = new HashMap<String, Node>();
        private Set<ClickListener> mPlaneClickListeners = new HashSet<ClickListener>();

        public TrackedPlanesController(Activity activity, View rootView){
            mCurrentActivityWeak = new WeakReference<Activity>(activity);

            // Inflate viro_view_hud.xml layout to display a "Searching for surfaces" text view.
            View.inflate(activity, R.layout.viro_view_hud, ((ViewGroup) rootView));
        }

        /**
         * Register click listener for other components to listen for click events that occur
         * on tracked planes. In this example, a listener is registered during scene creation,
         * so as spawn 3d droids on a click.
         */
        public void addOnPlaneClickListener(ClickListener listener){
            mPlaneClickListeners.add(listener);
        }

        public void removeOnPlaneClickListener(ClickListener listener){
            if (mPlaneClickListeners.contains(listener)){
                mPlaneClickListeners.remove(listener);
            }
        }

        /**
         * Once a Tracked plane is found, we can hide the our "Searching for Surfaces" UI.
         */
        private void hideIsTrackingLayoutUI(){
            if (searchingForPlanesLayoutIsVisible){
                return;
            }
            searchingForPlanesLayoutIsVisible = true;

            Activity activity = mCurrentActivityWeak.get();
            if (activity == null){
                return;
            }

            View isTrackingFrameLayout = activity.findViewById(R.id.viro_view_hud);
            isTrackingFrameLayout.animate().alpha(0.0f).setDuration(2000);
        }

        @Override
        public void onAnchorFound(ARAnchor arAnchor, ARNode arNode) {
            // Spawn a visual plane if a PlaneAnchor was found
            if (arAnchor.getType() == ARAnchor.Type.PLANE){
                ARPlaneAnchor planeAnchor = (ARPlaneAnchor)arAnchor;

                // Create the visual geometry representing this plane
                Vector dimensions = planeAnchor.getExtent();
                Surface plane = new Surface(1,1);
                plane.setWidth(dimensions.x);
                plane.setHeight(dimensions.z);

                // Set a default material for this plane.
                Material material = new Material();
                material.setDiffuseColor(Color.parseColor("#BF000000"));
                plane.setMaterials(Arrays.asList(material));

                // Attach it to the node
                Node planeNode = new Node();
                planeNode.setGeometry(plane);
                planeNode.setRotation(new Vector(-Math.toRadians(90.0), 0, 0));
                planeNode.setPosition(planeAnchor.getCenter());

                // Attach this planeNode to the anchor's arNode
                arNode.addChildNode(planeNode);
                surfaces.put(arAnchor.getAnchorId(), planeNode);

                // Attach click listeners to be notified upon a plane onClick.
                planeNode.setClickListener(new ClickListener() {
                    @Override
                    public void onClick(int i, Node node, Vector vector) {
                        for (ClickListener listener : mPlaneClickListeners){
                            listener.onClick(i, node, vector);
                        }
                    }

                    @Override
                    public void onClickState(int i, Node node, ClickState clickState, Vector vector) {
                        //No-op
                    }
                });

                // Finally, hide isTracking UI if we haven't done so already.
                hideIsTrackingLayoutUI();
            }
        }

        @Override
        public void onAnchorUpdated(ARAnchor arAnchor, ARNode arNode) {
            if (arAnchor.getType() == ARAnchor.Type.PLANE){
                ARPlaneAnchor planeAnchor = (ARPlaneAnchor)arAnchor;

                // Update the mesh surface geometry
                Node node = surfaces.get(arAnchor.getAnchorId());
                Surface plane = (Surface) node.getGeometry();
                Vector dimensions = planeAnchor.getExtent();
                plane.setWidth(dimensions.x);
                plane.setHeight(dimensions.z);
            }
        }

        @Override
        public void onAnchorRemoved(ARAnchor arAnchor, ARNode arNode) {
            surfaces.remove(arAnchor.getAnchorId());
        }

        @Override
        public void onTrackingInitialized() {
            //No-op
        }

        @Override
        public void onAmbientLightUpdate(float v, float v1) {
            //No-op
        }
    }
  • ちなみにobjファイルロードして、texture貼るのはこんな感じ
        object3D.loadModel(Uri.parse("file:///android_asset/andy.obj"), Object3D.Type.OBJ, new AsyncObject3DListener() {
            @Override
            public void onObject3DLoaded(final Object3D object, final Object3D.Type type) {
                // When the model is loaded, set the texture associated with this OBJ.
                Texture objectTexture = new Texture(bot, Texture.Format.RGBA8, false, false);
                Material material = new Material();
                material.setDiffuseTexture(objectTexture);
                object3D.getGeometry().setMaterials(Arrays.asList(material));
            }

            @Override
            public void onObject3DFailed(String s) {
            }
        });

感想

  • OpenGLESと比べてはるかに簡単に記述できる、Unity, SceneKitを触ったことがある人なら概念は理解しやすいと感じる
  • NativeでのARCoreではRajawaliを検討していたのだが、ARCore対応のIssueはでているもののまだ取り込まれていない github.com
  • また最近Rajawaliが動いてない感じなので期待している github.com
  • スター数はViroReactが126、ViroCoreが26なのでいまのところかなりマイナーな感じは否めない
  • ARCoreを触りたい、かつSceneKitを触ったことがある人は実はあまりいないのか・・・?

参考

Android EmulatorでARCoreを動かしてみた

概要

  • ARCoreが1.0になってEmulatorで動くようになったので試してみた
  • Emulatorなので以下のような様々な制約があるが、とりあえず試してみるには良いがこれだけで開発は無理
    • AndroidStudio(AndroidNative)で動作する、Unityでは無理(Unrealは試してない)
    • PCカメラを使った平面認識は無理(特徴点検出はできた)
    • Emulatorに用意されているvirtualSceneであれば平面認識まで可能(UnityでのTangoのデバッグ時に出てくる部屋みたいなやつ)

詳細

開発環境

  • AndroidStudio3.1(2018/2/27時点でBeta)
    • Emulatorなど全部入っているのでとりあえずこれ落としておけばOK
    • 実はARCoreを動かすだけなら3.0以上で良いのだがEmulatorを使う場合は3.1が必要

開発環境構築

AndroidStudio 3.1(beta)のダウンロード

Get the Android Studio Preview | Android Studio

からダウンロードしたいのだが日本語だと3.1がなぜか表示されないので言語を英語にする
ページの一番下で言語を変更、日本語からEnglishに

f:id:tiro105:20180227161601p:plain:w450

すると以下の画面が表示されるので3.1をDL

f:id:tiro105:20180227161657p:plain:w450

インストール

  • ダウンロードしたzipを解凍し、良き場所に配置して起動

サンプルプロジェクトダウンロード, import

  • ARCore SDKをDL
    github.com
  • File > Openから/samples/hello_ar_javaを開く
    f:id:tiro105:20180227163919p:plain:w450

  • ビルドするときにSDK Managerから足りないものあれば指摘されるので良きようにDL、もしわからなければこちらを参考に
    多分7.0が入ってなければDL(Emulator使うなら8.1も必要だけど後述)、最新のSDKにアップデートなどが走る
    IDE および SDK ツールの更新 | Android Studio

Emulator環境構築

  • サンプルプロジェクトを動かすEmulator環境を構築する、動作対象端末を持ってる人は端末使ったほうが早いのでそちら推奨

必要なものDL

  • AndroidStudioのメニューからTools > SDK Manager
  • SDK Platformsタブで右下の「Show Package Details」にチェック、Android8.1(Oreo)のGoogle APIs Intel x86 Atom System Image (Level 27, Version 4).を選択、OKでインストール
    f:id:tiro105:20180227172927p:plain:w450

  • SDK ToolsタブからAndroid Emulator (Version 27.1.10).を選択、ダウンロード
    f:id:tiro105:20180227173134p:plain:w450

EmulatorとなるVirtual Device作成

  • AndroidStudioのメニューからTools > AVD Manager
  • 左下のCreate Virtual Device
    f:id:tiro105:20180227173736p:plain:w450

  • Pixel or Pixel2選択
    f:id:tiro105:20180227173801p:plain:w450

  • OSはOreo: API Level 27: x86: Android 8.1 (Google APIs)
    f:id:tiro105:20180227173830p:plain:w450

  • Show Advanced SettingsからCamera BackをVirtualSceneに設定(WebcamにしたらPCのカメラが使える、PCカメラに設定した場合私の環境だと特徴点検出はできたが平面検出はできなかった)
    f:id:tiro105:20180227173849p:plain:w450
    f:id:tiro105:20180227173859p:plain:w450

  • 私の環境だけなのかわからないのだが、$ANDROID_SDK_ROOTを設定していないとEmulatorが起動しなかった、必要な場合は以下を参照 stackoverflow.com

Emulatorで実行

  • 作成したEmulatorを指定してimportしたプロジェクト実行
    f:id:tiro105:20180227174852p:plain:w450

  • こんな感じで仮想空間が広がる
    f:id:tiro105:20180227175213p:plain

  • 最初に操作方法が出るが、Alt押しながらマウスとかキーボードさわれば視点の回転や移動ができる、詳しくは以下を参照
    Run AR Apps in Android Emulator  |  ARCore  |  Google Developers
    Control the AR Simulation参照

感想

  • 今ARCoreを動かせる端末が少ないのでとりあえず動かすには良い
  • AndroidSDKとAndroidIDEをAndroidStudioとして一括で扱うのは便利な面もあり、Emulator動かすだけなのに新しいバージョンのAndroidStudio必要なのが不思議だったりした(やってないけどSDK Managerから必要なEmulatorだけDLすれば3.0系でも動くのか?)
  • Emulator起動してUnityから繋げばUnityでも使えるんじゃね?と思ったが実行すると「DllNotFoundException: arcore_unity_api」となった、近いうちに対応してくれると嬉しい
  • たまにカメラ権限を与えるにもかかわらずカメラが使えないよエラー(ログ取り忘れた)が帰ってくるがエミュレーターを再起動すれば正常動作する、再起動はエミュレータに表示されてる電源ボタン長押し

参考

僕の考えるARVRの現状

概要

この記事はRecruit Engineers Advent Calendar 2017の23日目の記事です adventar.org

  • 今年もARVR色々ありました
  • 今私の考えるARVRのふわっとしたまとめを作ってみた

詳細

市場規模

2017年、AR/VR支出が最も多いのは米国(32億ドル)であり、次いで日本を除くアジア/太平洋地域(APeJ)(30億ドル)、西欧(20億ドル)の順となります。
しかし興味深いことに、APeJが2018年と2019年にトップに立つもその後減速、2020年からは支出が加速する米国が再びトップに立つことが予測されます。
そしてその間に、西欧がAPeJを追い抜き、2021年には2位に浮上すると予測しています。2016年から2021年にかけて最も急速にAR/VR支出が成長するのはカナダ(CAGR 145.2%)で、次いで中東欧(同133.5%)、西欧(同121.2%)および米国(同120.5%)とIDCは予測しています
一方、日本でもAR/VR関連市場の成長は堅調なものの、その成長率は2016年から2021年においては年間平均67.1%であり、世界の113.2%に比べるとやや見劣りすると予測されます。
「視覚による情報行動に革新的な影響をもたらすAR/VR技術の導入は、イノベーションを実現する上での重要な鍵となることは明らかである」とIDC Japan PC,携帯端末&クライアントソリューション シニアマーケットアナリストの菅原 啓は述べています。
さらに「現段階ではコスト面やコンテンツ内容での懸念材料が根強いのも事実だが、だからこそ、今この段階で採用・導入を進めることは、今後の技術革新やハードウェアの普及を考えると、企業にとって極めて大きなアドバンテージとなるだろう」と提言しています。

とのことなので、日本頑張っていこうな

体験機器

AR

  • AR HMD(HMDじゃない気がする)
    • HoloLensの体験は未だ圧倒的(価格含め)
    • Meta2やってみたいけどHoloLensを超えるとかの声はあまり聞かない
    • Magic Leap one「サイバーーーーパンク!!!、超かっこいい!!!!!」と思ったけど、自分の価値観が揺らぐ世間との違いだった
  • AR スマホ

VR

  • VR HMD
    • Windows Mixed Reality Immersive headsetsの登場が熱かった、USBでつながれば即使えるのはやはり強い
    • 8K HMDなど解像度の向上や、嗅覚触覚に訴えかけるデバイスが登場するなどよりイマーシブルなデバイスが登場しており体験としてはさらに良くなりそう
    • Windows headesetsで普及、8Kなどでさらに体験の向上など理想的な登り方をしている気がする
    • Daydream HMDはまだかな???
  • VRスマホ
    • Daydream Viewがついに日本発売されました
    • iOSは相変わらず動く気を感じられない
    • NOLOとかZapboxとかHTC LINKとか面白いのでましたね、使ってみたい

体験機器まとめ

  • スマホOSのAR対応とWindows Mixed Reality Immersive headsetsが普及に繋がりそうで大変嬉しい

開発環境

  • 開発環境でもまとめてみた f:id:tiro105:20171222204043p:plain

まとめた感想

  • Unity, UnrealEngineが相変わらず強い
  • 既存スマホAppにAR/VR機能を追加しようとするとNativeでがんばるしかない、SceneKit, rajawaliがよくできているけどUnityでやった後にやるとやはりしんどい
  • Amazon Sumerian, Lens Studio, Frame Studioなどプレイヤーが増えてきて嬉しい

来年の妄想

  • Oculus GO、Daydream HMDなどスタンドアロンHMDがいよいよ登場、PC不要かつゴーグルはめてすぐにVRに入れるのはかなり魅力的
  • ARKit, ARCoreの登場によりそれらに対応するサードパーティSDKが増えてきそう(Vuforia, Lens Studio)、いろんな体験が簡単に作れるようになるといいな
  • HoloLensにおいつくデバイスの登場が待たれる

ARCoreでプロ生ちゃんだしてみた

概要

ARCore Preview2も出たことだしプロ生ちゃんだしてみた (この記事はある程度Unityが触れる人を対象にしています)
この記事はプロ生ちゃんAdvent Calendar 2017の記事です
adventar.org

詳細

ARCoreとは

プロ生ちゃんを出すまで

adb install arcore-preview2.apk

結果

f:id:tiro105:20171218191714p:plain
なんか重力が上むいてる・・・

感想

  • アニメーション、音声とかつけると夢広がりそう
  • デフォルトだと端末が熱くなる、改善できそう
  • 重力問題なんとかしたい、スパッツ必須
  • ARKitを違って一回Pauseしちゃうと全てがresetされる、保存するのに何か必要なのかな?
    • known issueで上がってた ArSession_configure() does not apply settings changes if called while in a resumed state.

読書感想_人工知能は人間を超えるか

人工知能は人間を超えるか

概要

  • 近年メディアを騒がせている人工知能について「本当にすごいこと」か「実はそんなにすごくないこと」を判断するために、人工知能の研究の歴史を読み解く

人工知能とは何か

  • この本における人工知能は「人のように考えるコンピュータ」
    • この定義にのっとるコンピュータはまだ完成していない
    • 専門家における定義は専門家によって異なる
  • 人間の脳の活動は神経細胞を使った電気信号のやり取りなんだから、電気回路で再現できるというのが本書の基本的な考え方
  • 世間で言われている人工知能は以下の4レベルに分けることができる
    • レベル1:単純な制御プログラム
    • レベル2:古典的な人工知能
      ふるまいのパターンが極めて多彩なもの
    • レベル3:機械学習を取り入れたもの
      推論の仕組みがデータをもとに学習されているもの、機械学習アルゴリズムの一種でありサンプルとなるデータをもとに、る0るや知識を自ら学習するもの
    • レベル4:ディープラーニングを取り入れたもの
      機械学習をする際の特徴量をコンピュータ自身で学習するもの

「推論」と「検索」の時代

  • ここから人工知能の歴史の話、人工知能はこれまでブームと冬の時代を繰り返してきた
  • ブームをまとめると以下の感じ
  • 「推論・探索」で探索木で迷路を解く、ハノイの塔などの問題が一見高速で解決できることからAIへの期待が高まった
  • 将棋、チェスなど限られた場所では活躍したが現実の問題は非常に複雑でありいわゆる「トイ・プログラム」しか解けないのではないか??という失望感が広がりいったんブーム終焉

Microsoft AzureのCognativeって推察のことだったんだ

「知識」を入れると賢くなる

  • 非常に複雑なら、それに対応するだけのデータ(知識)をコンピュータに事前に入れておけばいいんじゃないか?という考え
  • 現実すべては不可能だが専門分野の知識を入れて利用する「エキスパートシステム」の利用が期待された
  • 分野にもよるが、やはり必要になる情報の量が膨大、エキスパートシステムとなると情報を取得することにもコストがかかった
  • フレーム問題(あるタスクを実行するのに「関係のある知識だけを取り出して使う」)、シンボルグラウンディング問題(シンボルに対して意味を自分自身で紐づけることができない)
  • これらの問題からやはり人工知能無理じゃないか?となり再びブーム終焉

二回目のブームが終わるとさすがに心折れそうだなって思った

機械学習」の静かな広がり

  • 「知識」をいれる「学習」を「分ける(そのデータがある条件に対してイエスかノーか?)」ことの精度を上げていくこととして捉えた場合、コンピュータが大量のデータを処理しながら、この分け方を自動的に収集するのが「機械学習
  • いったん分け方が定義されればコンピュータはかなり精度高く「知識」を使える
  • この分け方がいくつかあるが人間の脳を模した方法が「ニューラルネットワーク
  • ニューラルネットワークは非常に優秀だが、データを分ける際の閾値的なもの、「特徴量」が必要になる
  • 「世界からどの特徴に注目して情報を取り出すべきか」をコンピュータ自身が決定できないことが人工知能への大きな壁だった

ニューラルネットワークについてはゼロからわかるDeepLearning(参考書籍にリンクあり)がよかった

静寂を破る「ディープラーニング

  • データをもとに、コンピュータ自身が特徴量を自らつくりだせる
  • ディープラーニングの考え方自体は昔からあったが、最近になって適切な使い方が見えてきた
  • ディープラーニングがすべてに利用できるわけではないが、これは人工知能の課題であった「世界からどの特徴に注目して情報を取り出すべきか」に対する一つの可能性を提示した

本章に対する作者の並々ならぬ思いが見えてきて非常に読み応えのある章

人工知能は人間を超えるか

  • 筆者が考えるこれからのディープラーニングから発生する流れ
    • 画像特徴の抽象化ができるAI
    • マルチモーダルな抽象化ができるよAI
    • 行動と結果の抽象化ができるAI
    • 行動を通じた特徴量を獲得できるAI
    • 言語理解・自動翻訳ができるAI
    • 知識獲得ができるAI
  • この流れで進めばコンピュータが今人間が持っている創造性などを持つことも可能だろう

最近Googleから発表された学習モデルを自ら作り出すロジックは大きな一歩なんじゃ?
http://news.livedoor.com/article/detail/13984245/

変わりゆく世界

  • ディープラーニングの出現によりさまざまなものへのAI利用が考えられる
  • 一般普及には時間がかかるかもしれないが確実に変化は訪れるだろう
  • 仕事の置き換えも起こるだろうが、そこから新たに生まれる職業もある

感想

関連書籍