Kudan AR SDK で AR Android アプリを作ってみよう〜マーカーレスで床に画像を表示

こんにちは。エクセルソフトの田淵です。

弊社取り扱いの Kudan AR SDK のエントリーです。

SDK のダウンロードは こちら からお申込みください。SDK を使った開発と、個人開発者のリリースは無料でご利用いただけます。企業の方は有料になりますので、@ytabuchi までご連絡ください。

前回の Android のエントリーではマーカーの上に動画を表示させるところまでをやりました。

今回は遂にマーカレスです。

現時点でのサンプルコードは ytabuchi の Github にアップしてあります。この後もコミット追加していくのでスナップショットです。

作業の流れ

今までは最初の MainActivity で作業していたので、アクティビティを分けたいと思います。

こんな感じですね。

  • 今までのマーカーの処理を MarkerActivity に移動する
  • マーカーレスの処理を ArbiActivity に新規に作成する
  • MainActivity は図のようにアクティビティに移動するだけのアクティビティにする

が作業内容です。ちょっと長いです。

また、今回使用する画像一式は、こちら からダウンロードしてください。

マーカー処理を MarkerActivity に移動

最初は既存のコードを移動しましょう。

activity_marker.xml を作成

メイン画面から「Marker」ボタンをタップした後で遷移するページを用意します。

Layout で右クリックして、「New>Layout Resource file」でアクティビティを作成します。

内容は前回までで作成した activity_main.xml から移行しましょう。

以下のようになります。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MarkerActivity">

    <Button
        android:id="@+id/showImageButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginStart="16dp"
        android:onClick="showImageButtonClicked"
        android:text="Image"
        app:layout_constraintBottom_toTopOf="@+id/clearButton"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/showModelButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginLeft="8dp"
        android:onClick="showModelButtonClicked"
        android:text="3D Model"
        app:layout_constraintBottom_toTopOf="@+id/clearButton"
        app:layout_constraintLeft_toRightOf="@+id/showImageButton" />

    <Button
        android:id="@+id/clearButton"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginBottom="16dp"
        android:layout_marginEnd="16dp"
        android:layout_marginStart="16dp"
        android:onClick="clearAllButtonClicked"
        android:text="Clear"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

    <Button
        android:id="@+id/showVideoButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginLeft="8dp"
        android:onClick="showVideoButtonClicked"
        android:text="Video"
        app:layout_constraintBottom_toTopOf="@+id/clearButton"
        app:layout_constraintLeft_toRightOf="@+id/showModelButton" />

</android.support.constraint.ConstraintLayout>

MarkerActivity を作成

次に MarkerActivity を作成して、MainActivity にあったキーをセットする部分と permissionsRequest メソッドを残して、その他を移行していきます。

class MarkerActivity : ARActivity() {

    private lateinit var imageTrackable: ARImageTrackable
    private lateinit var videoNode: ARVideoNode

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_marker)
    }

    override fun setup() {

        addImageTrackable()

        addImageNode()
        addModelNode()
        addVideoNode()
    }


    private fun addImageTrackable(){

        // ARImageTrackable をインスタンス化して画像を読み込み
        imageTrackable = ARImageTrackable("Lego")
        imageTrackable.loadFromAsset("lego.jpg")

        // ARImageTracker のインスタンスを取得して初期化
        val trackableManager = ARImageTracker.getInstance()
        trackableManager.initialise()

        // imageTrackable を ARImageTracker に追加
        trackableManager.addTrackable(imageTrackable)
    }

    
    private fun addImageNode(){

        // ARImageNode を画像を指定して初期化
        val imageNode = ARImageNode("cow.png")

        // imageNode のサイズを Trackable のサイズに合わせる
        val textureMaterial = imageNode.material as ARTextureMaterial
        val scale = imageTrackable.width / textureMaterial.texture.width
        imageNode.scaleByUniform(scale)

        // imageNode を trackable の world に追加
        imageTrackable.world.addChild(imageNode)

        // 初期状態で非表示
        imageNode.visible = false
    }

    private fun addModelNode(){

        // モデルのインポート
        val modelImporter = ARModelImporter()
        modelImporter.loadFromAsset("ben.jet")
        val modelNode = modelImporter.node as ARModelNode

        // モデルのテクスチャーを読み込み
        val texture2D = ARTexture2D()
        texture2D.loadFromAsset("bigBenTexture.png")

        // ARLightMaterial を作成してテクスチャーとアンビエントを指定
        val material = ARLightMaterial()
        material.setTexture(texture2D)
        material.setAmbient(0.8f, 0.8f, 0.8f)

        // モデルの全 meshNode に material を追加
        modelImporter.meshNodes.forEach { meshNode ->
            meshNode.material = material
        }

        // modelNode の向きと大きさを指定
        modelNode.rotateByDegrees(90f, 1f, 0f, 0f)
        modelNode.scaleByUniform(0.25f)

        // modelNode を trackable の world に追加
        imageTrackable.world.addChild(modelNode)

        // 初期状態で非表示
        modelNode.visible = false
    }

    private fun addVideoNode(){

        // ARVideoTexture を mp4 ファイルで初期化
        val videoTexture = ARVideoTexture()
        videoTexture.loadFromAsset("waves.mp4")

        // ARVideoTexture で ARVideoNode をインスタンス化
        videoNode = ARVideoNode(videoTexture)

        // videoNode のサイズを Trackable のサイズに合わせる
        val scale = imageTrackable.width / videoTexture.width
        videoNode.scaleByUniform(scale)

        // videoNode を trackable の world に追加
        imageTrackable.world.addChild(videoNode)

        // 初期状態で非表示
        videoNode.visible = false
    }

    // 全ての Node を非表示
    private fun hideAll(){
        val nodes = imageTrackable.world.children
        for (node in nodes)
            node.visible = false
    }

    fun clearAllButtonClicked(view: View){
        hideAll()
    }

    fun showImageButtonClicked(view: View){
        hideAll()
        imageTrackable.world.children[0].visible = true
    }

    fun showModelButtonClicked(view: View){
        hideAll()
        imageTrackable.world.children[1].visible = true
    }

    fun showVideoButtonClicked(view: View){
        hideAll()
        videoNode.videoTexture.reset()
        videoNode.videoTexture.start()
        imageTrackable.world.children[2].visible = true
    }

}

アクティビティを追加したら、AndroidManifest.xmlapplication 内に以下の activity を追加しましょう。

<activity
    android:name=".MarkerActivity"
    android:configChanges="orientation|screenSize"
    android:screenOrientation="fullSensor">
</activity>

configChangesscreenOrientation はアクティビティ毎に書くべきなのかいまいち分かっていませんが、これで動作はします()。

マーカーレスの処理を ArbiActivity に追加

コードが別れたので、マーカーレス用の画面と処理を追加していきます。

activity_arbi.xml を作成

先ほどと同様に、Layout を右クリックして、activity_arbi.xml を作成します。

画面下にオブジェクトを表示するためのボタンを作成したいので、以下のように記述します。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/changeTrackingModeButton"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:onClick="changeTrackingModeButtonClicked"
        android:text="Start Tracking"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
</android.support.constraint.ConstraintLayout>

ArbiActivity を作成

次に同様に ARActivity を継承した ArbiActivity を作成します。

ARActivity を継承しているので、setupoverride が必要です。

override fun setup() {
    super.setup()

}

そのまま setup 内で呼び出される、画像を表示するノードを作成します。trackingNode は後で使用するのでクラス変数 private lateinit var trackingNode: ARImageNode として定義しています。

private fun addTrackingNode() {

    // トラッキング(表示)するノードを用意
    trackingNode = ARImageNode("CowTracking.png")

    // ノードの画像を正しい向きにするために回転
    trackingNode.rotateByDegrees(90.0f, 1.0f, 0.0f, 0.0f)
    trackingNode.rotateByDegrees(180.0f, 0.0f, 1.0f, 0.0f)
    trackingNode.rotateByDegrees(-90.0f, 0.0f, 0.0f, 1.0f)

}

原理はまだ理解できていませんが、画像を読み込むと反転して回転された状態で表示されてしまうので、おまじない的に正しい向きに回転させています。

次にマーカーレスモードの View を作成します。

private fun setupArbiTrack() {

    // ArbiTrack を初期化
    val arbiTrack = ARArbiTrack.getInstance()
    arbiTrack.initialise()

    // ジャイロマネージャーを初期化
    val gyroPlaceManager = ARGyroPlaceManager.getInstance()
    gyroPlaceManager.initialise()


    // ターゲットとして使うノードを用意
    val targetNode = ARImageNode("CowTarget.png")

    // デバイスのジャイロでノードが動くようにノードを ARGyroPlaceManager に追加
    gyroPlaceManager.world.addChild(targetNode);

    // ノードの画像を正しい向きにするために回転し、サイズを調整
    targetNode.rotateByDegrees(90.0f, 1.0f, 0.0f, 0.0f)
    targetNode.rotateByDegrees(180.0f, 0.0f, 1.0f, 0.0f)
    targetNode.rotateByDegrees(-90.0f, 0.0f, 0.0f, 1.0f)
    targetNode.scaleByUniform(0.3f);

    // ARArbiTrack の targetNode に指定
    arbiTrack.targetNode = targetNode

    // ARArbiTrack の world に trackingNode を追加
    arbiTrack.world.addChild(trackingNode)

}

マーカーレスモードでは、ARArbiTrackARGyroPlaceManager を使用するため、2つのインスタンスを取得し、初期化しています。

ターゲットを ARGyroPlaceManager に追加して、最後に ARArbiTracktargetNode プロパティに指定すれば、起動時にターゲットが表示されます。

最後に表示するノード trackingNodeARArbiTrack に追加しています。前回までの記事と同じように、ノードをいくつか作っておけば、指定したノードの表示/非表示を切り替えられるのではないかと思いますので、次回試してみます。

そのままボタンの onClick イベントの処理を書いていきます。

fun changeTrackingModeButtonClicked(view: View) {

    val arbiTrack = ARArbiTrack.getInstance()
    val b = findViewById

ARArbiTrack のインスタンスに isTracking のプロパティがあるので、これでボタンのテキストと targetNodevisible を制御しています。trackingNode は、ARArbiTrack をスタート/ストップすると自動で表示/非表示されます。

さあ!完成です。

ArbiActivity は次のようになっています。

class ArbiActivity : ARActivity() {

    private lateinit var trackingNode: ARImageNode


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_arbi)
    }


    override fun setup() {
        super.setup()

        addTrackingNode()
        setupArbiTrack()
    }


    private fun addTrackingNode() {

        // トラッキング(表示)するノードを用意
        trackingNode = ARImageNode("CowTracking.png")

        // ノードの画像を正しい向きにするために回転
        trackingNode.rotateByDegrees(90.0f, 1.0f, 0.0f, 0.0f)
        trackingNode.rotateByDegrees(180.0f, 0.0f, 1.0f, 0.0f)
        trackingNode.rotateByDegrees(-90.0f, 0.0f, 0.0f, 1.0f)

    }

    private fun setupArbiTrack() {

        // ArbiTrack を初期化
        val arbiTrack = ARArbiTrack.getInstance()
        arbiTrack.initialise()

        // ジャイロマネージャーを初期化
        val gyroPlaceManager = ARGyroPlaceManager.getInstance()
        gyroPlaceManager.initialise()


        // ターゲットとして使うノードを用意
        val targetNode = ARImageNode("CowTarget.png")

        // デバイスのジャイロでノードが動くようにノードを ARGyroPlaceManager に追加
        gyroPlaceManager.world.addChild(targetNode);

        // ノードの画像を正しい向きにするために回転し、サイズを調整
        targetNode.rotateByDegrees(90.0f, 1.0f, 0.0f, 0.0f)
        targetNode.rotateByDegrees(180.0f, 0.0f, 1.0f, 0.0f)
        targetNode.rotateByDegrees(-90.0f, 0.0f, 0.0f, 1.0f)
        targetNode.scaleByUniform(0.3f);

        // ARArbiTrack の targetNode に指定
        arbiTrack.targetNode = targetNode

        // ARArbiTracker の world に trackingNode を追加
        arbiTrack.world.addChild(trackingNode)

    }


    fun changeTrackingModeButtonClicked(view: View) {

        val arbiTrack = ARArbiTrack.getInstance()
        val b = findViewById

Activity を追加したので、AndroidManifest.xml への追加も忘れずにやっておきましょう。

<activity
    android:name=".ArbiActivity"
    android:configChanges="orientation|screenSize"
    android:screenOrientation="fullSensor">
</activity>

MainActivity を移動するだけのアクティビティにする

MainActivity に必要なのは ARAPIKeysetAPIKey でキーをセットする部分とパーミッションの処理だけですので、それぞれ次のように修正します。

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/markerPageButton"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:onClick="markerPageButtonClicked"
        android:text="Marker AR"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:textAllCaps="false" />

    <Button
        android:id="@+id/arbiPageButton"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:onClick="arbiPageButtonClicked"
        android:text="Markerless AR"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/markerPageButton"
        tools:textAllCaps="false" />
</android.support.constraint.ConstraintLayout>

MainActivity

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)

        val key = ARAPIKey.getInstance()
        key.setAPIKey("API_KEY")

        permissionsRequest()

    }

    fun markerPageButtonClicked(view: View) {
        val markerIntent = Intent(this, MarkerActivity::class.java)
        startActivity(markerIntent)
    }

    fun arbiPageButtonClicked(view: View) {
        val arbiIntent = Intent(this, ArbiActivity::class.java)
        startActivity(arbiIntent)
    }

    // Permission のリクエストを OS 標準の requestPermissions メソッドで行う
    private fun permissionsRequest(){
        if (ContextCompat.checkSelfPermission(this,
                        Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this,
                        Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this,
                        Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this,
                    arrayOf(Manifest.permission.INTERNET, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA), 111)

        }
    }

    // ダイアログを表示して、本サンプルアプリの設定画面に遷移する
    private fun permissionsNotSelected() {
        val builder = AlertDialog.Builder(this)
        builder.setTitle("Permissions required")
        builder.setMessage("Please enable the requested permissions in the app settings in order to use this demo app")
        builder.setPositiveButton("Set permission", DialogInterface.OnClickListener { dialog, id ->
            dialog.cancel()
            val intent = Intent()
            intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
            intent.data = Uri.parse("package:eu.kudan.ar")
            startActivity(intent)
        })
        val noPermission = builder.create()
        noPermission.show()
    }

    // requestPermissions ダイアログの結果を受け、全て許可されていなければ、permissionsNotSelected を呼び出し
    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        when (requestCode) {
            111 -> {
                if (grantResults.isNotEmpty() &&
                        grantResults[0] == PackageManager.PERMISSION_GRANTED &&
                        grantResults[1] == PackageManager.PERMISSION_GRANTED &&
                        grantResults[2] == PackageManager.PERMISSION_GRANTED &&
                        grantResults[3] == PackageManager.PERMISSION_GRANTED) {
                    // パーミッションが必要な処理
                } else {
                    permissionsNotSelected()
                }
            }
        }
    }
}

こんな感じで一度配置したら割とピタッと配置される感じを味わってみてください。

https://platform.twitter.com/widgets.js

この後は

次はマーカーモードと同じく、マーカーレスで 3D モデルを配置することにチャレンジしてみたいと思います。

Kudan AR SDK エントリー一覧

Kudan AR SDK チュートリアル記事まとめ | エクセルソフト ブログ

をご覧ください。

以上です。

タイトルとURLをコピーしました