티스토리 뷰

메타버스 스터디

ARCore 정리

알렌보이스 2021. 8. 6. 21:11
728x90

4주간 ARCore를 학습하면서 배운 내용들을 총 정리하는 글입니다.

0. 기본 세팅

 

AR 필수 앱과 AR 선택 앱을 구분해서 타깃 SDK와 Manifest 값을 다르게 설정해줘야 합니다.
AR 필수의 경우 minSdkVersion 24 이상 AR 선택의 경우 minSdkVersion 14 이상 설정해주고
AR 필수인 경우 Manifest에 아래의 값을 필수로 넣어줘야 합니다.

Manifest

<uses-feature android:name="android.hardware.camera.ar" />

 

app 수준의 build.gradle

// ARCore : https://developers.google.com/ar/develop/java/enable-arcore#ar-required_1
implementation 'com.google.ar:core:1.25.0'
implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.17.1'
implementation 'com.google.ar.sceneform:assets:1.17.1'

 

 

1. ARSceneView vs ARFragment

 

1) ArSceneView

    <com.google.ar.sceneform.ArSceneView
        android:id="@+id/arView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
val session = Session(this)
val config = Config(session)

config.updateMode = Config.UpdateMode.LATEST_CAMERA_IMAGE
session.configure(config)
arSecnceView.setupSession(session)

ArSceneView를 사용하기 위해 직접 Session과 Config를 설정해주어야 합니다.
또한 퍼미션 요청, session 오류 처리, ARCore 사용 가능 여부 등을 직접 구현해주어야 합니다.

 

2) ArFragment

    <fragment
        android:id="@+id/sceneFormFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        class="com.google.ar.sceneform.ux.ArFragment"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>
arFragment = supportFragmentManager.findFragmentById(R.id.sceneFormFragment) as ArFragment

ArSceneView의 번거로움을 간편하게 사용하기 위해 ArFragment를 지원해줍니다.
ArFragment에서 기본적으로 초록색 점선 영역을 구현해 두었기 때문에 간편하게 사용이 가능합니다.

    <fragment
        android:id="@+id/sceneFormFragment"
        android:name="com.example.arcorestudy.CustomArFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
class CustomArFragment : ArFragment() {

    override fun getSessionConfiguration(session: Session?): Config {
        planeDiscoveryController.setInstructionView(null)

        val config = super.getSessionConfiguration(session)
        config.cloudAnchorMode = Config.CloudAnchorMode.ENABLED
        config.focusMode = Config.FocusMode.AUTO
        config.depthMode = Config.DepthMode.AUTOMATIC
        config.lightEstimationMode= Config.LightEstimationMode.DISABLED
        config.planeFindingMode = Config.PlaneFindingMode.HORIZONTAL_AND_VERTICAL

        return config
    }

}

Config 설정을 바꾸기 위해서는 위와 같이 class를 정의해 주시면 됩니다.

 

2. Config

 

0) 종류

 

1) 기본 적인 사용 방법

config.updateMode = Config.UpdateMode.LATEST_CAMERA_IMAGE

 

2) FocusMode

왼쪽 : FIEXD / 오른쪽 : AUTO

FocusMode의 FIEXD의 경우 기종 차이가 있는 것 같습니다.
제가 사용 중인 Galaxy s20+의 경우 왼쪽 사진처럼 흐릿하게 나오지만
테스트폰인 Galaxy A31의 경우 오른쪽 사진처럼 뚜렷하게 잘 나왔습니다.

AUTO로 했을 때 둘 다 화질이 뚜렷하게 나오므로 AUTO 사용을 하는 것이 좋아 보입니다.

 

3) PlaneFindingMode

DISABLED, HORIZONTAL, VCERTICAL, HORIZONTAL_AND_VERTICAL 이렇게 4개 모드가 존재하는데
문자 그대로 Plan을 수평, 수직, 수평과 수직 이렇게 찾거나 탐색을 하지 않습니다.

 

4) LightEstimationMode

왼쪽 :  DISABLED / 가운데 : AMBIENT_INTENSITY / 오른쪽 : ENVIRONMENTAL_HDR

  • DISABLED : AR 개체와 장면 조명을 환경에 일치시키는 것이 중요하지 않거나 런타임 성능이 중요한 경우 사용
    기본적인 그림자나 3D느낌에는 큰 문제가 없이 사용 가능해 보입니다.
  • AMBIENT_INTENSITY : 주어진 이미지에 대한 평균 픽셀 강도와 조명 색상을 결정. 베이크인 조명이 있는 오브젝트와 같이 정확한 조명이 중요하지 않은 사용 사례를 위해 설계된 거친 설정
    DISABLED와 비슷해 보이긴 하는데 그림자가 주변과 비슷하게 따라간다는 차이가 있어 보입니다.
  • ENVIRONMENTAL_HDR : 방향성 조명, 그림자, 반사광 하이라이트 및 반사에 대한 세분화되고 사실적인 조명 추정을 허용하는 별도 API로 구성
    사무실에 형광등이 위에서 비춰서 오른쪽 사진과 같이 반영된 것 같습니다.

 

5) 그 외

 

InstantPlacementMode, UpdateMode, DepthMode의 경우 환경 문제인지 제 눈썰미 문제인지 차이점을 잘 모르겠습니다.

AugmentedFaceMode의 경우 얼굴 인식 관련은 많이 있기 때문에 ArCore에서는 넘어가기로 하였습니다.

CloudAnchorMode의 경우는 아래에 자세히 다루기 위하여 생략합니다.

 

3. Renderable

 

0) 종류

ModelRenderable, ViewRenderable

 

1) ModelRenderable

ModelRenderable을 생성하는 방법은 여러 가지 있겠지만 저는 2가지 방식을 이용하였습니다.

확장자가 sfb였을 때와 glb였을 경우 2가지가 있었습니다.

        ModelRenderable.builder()
            .setSource(fragment.context, uri)
            .build()
            .thenAccept { renderable->
                renderable = renderable
                addToScene(fragment, anchor, renderable)
            }
            .exceptionally { throwable ->
                alert {
                    title = "Error"
                    message = throwable.message.toString()
                }.show()
                return@exceptionally null
            }

sfb확장자의 경우 setSorce에 context와 uri를 넘겨주었습니다.

        val renderableSource =
            RenderableSource
                .builder()
                .setSource(this, Uri.parse(file.path), RenderableSource.SourceType.GLB)
                .setRecenterMode(RenderableSource.RecenterMode.ROOT)
                .build()

        ModelRenderable
            .builder()
            .setSource(this, renderableSource)
            .build()
            .thenAccept { renderable->
                addNodeToScene(arFragment, anchor, renderable)
            }
            .exceptionally { throwable ->
                alert {
                    title = "Error"
                    message = throwable.message.toString()
                }.show()
                return@exceptionally null
            }

glb확장자의 경우 RenderableSource를 만들어주고 setSource에 context와 RenderableSource를 넘겨주었습니다.

Builder 패턴으로 생성 성공 시에는 tehnAccpet에서 노드 생성과정으로 넘어가게 됩니다.
생성 실패 시에는 exceptionally에서 다이얼로그 띄우는 것으로 마무리합니다.

 

2) ViewRenderable

        ViewRenderable
            .builder()
            .setView(this, R.layout.name_card)
            .build()
            .thenAccept { viewRenderable ->
                addNodeToScene(arFragment, anchor, viewRenderable)
            }

ModelRenderable과 나머지는 다 동일하고 setSource대신 setView를 사용합니다.
미리 만들어 놓은 xml파일을 넣어서 사용하면 됩니다.

thenAccept의 viewRenderable.view를 통해 xml에 정의해둔 View에 접근이 가능합니다.
뷰 정의 시 주의점은 기본적으로 투명도가 어느 정도 들어가 있으므로 

 

4. Node

 

0) 종류

AnchorNode, AugmentedFaceNode, BaseTransformableNode, Camera, SkeletonNode, Sun

위와 같은 종류들이 있으며 ArSceneView의 Scene의 경우 최상위 Node로 인식합니다.

 

1) 흐름

    private fun settingArFragment() = with(binding){
        arFragment = supportFragmentManager.findFragmentById(R.id.sceneFormFragment) as ArFragment
        arFragment.setOnTapArPlaneListener { hitResult, plane, motionEvent ->
            settingInstantPlacement(hitResult)
        }
    }

    private fun settingInstantPlacement(hitResult: HitResult) {
        val anchorNode = AnchorNode(hitResult.createAnchor())
        anchorNode.renderable = renderable
        anchorNode.setOnTouchListener { _, _ ->
            anchorNode.setParent(null)
            return@setOnTouchListener true
        }
        arFragment.arSceneView.scene.addChild(anchorNode)
    }

기본적인 ArSceneView를 활용하여 모델을 표시하는 흐름입니다.
ArFragment에 setOnTapArPlaneListener를 등록하고
이벤트가 발생하면 연두색 점선 박스( settingInstantPlacement 함수) 동작 수행하여 모델을 표시하게 됩니다.

Renderable 객체를 생성 후 Node의 setRenderable 함수를 통해 Node에 담습니다.
그 후 ArSceneView의 scene의 addChild함수에 위의 Node를 담으면 모델이 표시가 됩니다.
필요에 따라 scene에 addOnUpdateListener를 등록하여 사용할 수 있습니다.

2) AnchorNode

기본적인 노드로 Anchor를 기반으로 배치를 하는 노드입니다. 
만약 Anchor를 추적하지 못하게 되면 하위 Node들이 제거됩니다.
구현하기에 따라 다르겠지만 기본적으로 AnchorNode의 경우 고정 배치가 되어있기 때문에
사용자가 크기, 위치, 방향 등의 동작이 필요한 경우가 아니라면 AnchorNode의 사용이 적합합니다.

 

3) TransformableNode

제스처를 통해 선택, 변환, 회전, 스케일 조정 등을 할 수 있는 노드입니다.
사용자의 컨트롤을 통해 보여줄 것이 있을 경우 사용하기 적합합니다.

 

4) SkeletonNode

골격, 백터, 매트릭스 등의 정보를 가지고 있는 노드입니다.
애니메이션이 있는 모델을 사용할 때 적합합니다.

 

5. Instant Placement

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

        initModelRecyclerView()
        bindModelRecyclerView()
        settingArFragment()

    }

    private fun initModelRecyclerView(){
        adapter = ModelAdapter(
            modelSelectListener = {
                renderable = it
            },
            downloadListener = { modelItem, position ->
                modelDownload(modelItem, position)
            }
        )

        binding.modelRecyclerView.adapter = adapter
    }

    private fun modelDownload(modelItem: ModelItem, position: Int) {
        settingDownloadStartState()

        try {
            val storageRef = Firebase.storage.reference
            val splitFileName = modelItem.fileName.split(".") // ex : [ "out", "glb" ]
            val file = File.createTempFile(splitFileName[0], splitFileName[1])

            storageRef.child(modelItem.fileName).getFile(file).addOnSuccessListener {
                buildModel(file, position)
                this.position = position
                renderableFileList[position] = file
            }

        }catch (e: IOException){
            e.printStackTrace()
            settingDownloadErrorState()
        }catch (e: Exception){
            e.printStackTrace()
            settingDownloadErrorState()
        }
    }

    private fun buildModel(file: File, position: Int){
        val renderableSource =
            RenderableSource
                .builder()
                .setSource(this, Uri.parse(file.path), RenderableSource.SourceType.GLB)
                .setRecenterMode(RenderableSource.RecenterMode.ROOT)
                .build()

        ModelRenderable
            .builder()
            .setSource(this, renderableSource)
            .setRegistryId(file.path)
            .build()
            .thenAccept { modelRenderable ->
                renderable = modelRenderable
                adapter.currentList[position].readable = modelRenderable
                adapter.notifyDataSetChanged()
                cardName = adapter.currentList[position].name
                Log.e("++++++", "${renderable?.animationDataCount}")
            }

        settingDownloadEndState()
    }

    private fun settingDownloadStartState(){
        binding.progressBar.isVisible = true
        toast("서버에서 이미지를 받아오고 있습니다. 잠시만 기다려주세요..")
    }

    private fun settingDownloadEndState(){
        binding.progressBar.isVisible = false
        toast("다운로드가 완료되었습니다.")
    }

    private fun settingDownloadErrorState(){
        binding.progressBar.isVisible = false
        toast("파일 다운로드중 오류가 발생하였습니다.")
    }

    private fun bindModelRecyclerView(){
        val modelList = mutableListOf<ModelItem>()
        modelList.add(ModelItem(imageResource = R.drawable.cat, name = "고양이", fileName = "cat.glb"))
        modelList.add(ModelItem(imageResource = R.drawable.ivysaur, name = "이상해풀", fileName = "Ivysaur.glb"))
        modelList.add(ModelItem(imageResource = R.drawable.out, name = "고우스트", fileName = "out.glb"))
        modelList.add(ModelItem(imageResource = R.drawable.spider, name = "스파이더맨", fileName = "spider.glb"))
        modelList.add(ModelItem(imageResource = R.drawable.clock, name = "시계", fileName = "Bee.glb"))

        adapter.submitList(modelList)
    }

    private fun settingArFragment() = with(binding){
        arFragment = supportFragmentManager.findFragmentById(R.id.sceneFormFragment) as ArFragment
        arFragment.setOnTapArPlaneListener { hitResult, plane, _ ->

            when (radioGroup.checkedRadioButtonId) {
                // 즉시 배치
                radioInstantPlacement.id -> {
                    settingInstantPlacement(hitResult)
                }
                // 이름표
                radioNameCard.id -> {
                    settingNameCard(hitResult)
                }
            }
        }
    }

    private fun settingInstantPlacement(hitResult: HitResult) {
        val anchorNode = AnchorNode(hitResult.createAnchor())
        anchorNode.renderable = renderable
        anchorNode.setOnTouchListener { _, _ ->
            anchorNode.setParent(null)
            return@setOnTouchListener true
        }
        arFragment.arSceneView.scene.addChild(anchorNode)
    }

    private fun settingNameCard(hitResult: HitResult){
        val anchorNode =AnchorNode(hitResult.createAnchor())
        anchorNode.setParent(arFragment.arSceneView.scene)
        val transFromAbleNode = TransformableNode(arFragment.transformationSystem)
        transFromAbleNode.setParent(anchorNode)
        transFromAbleNode.renderable = renderable
        transFromAbleNode.select()

        addName(anchorNode, transFromAbleNode, cardName)
    }

    private fun addName(anchorNode: AnchorNode, node: TransformableNode, name: String?) {
        ViewRenderable.builder().setView(this, R.layout.name_card)
            .build()
            .thenAccept { viewRenderable ->
                val nameView = TransformableNode(arFragment.transformationSystem)
                nameView.localPosition = Vector3(0f, node.localPosition.y + 0.5f, 0f)
                nameView.setParent(anchorNode)
                nameView.renderable = viewRenderable
                nameView.select()

                val txtName = viewRenderable.view as TextView
                txtName.text = name
                txtName.setOnClickListener {
                    anchorNode.setParent(null)
                }
            }
    }

Instant Placement의 경우 위에서 나온 Renderable과 Node을 같이 사용 것입니다.

Firebase Storage에 미리 glb파일을 저장해 놓고 다운로드하여서 사용하는 예제입니다.

위의 과정을 거쳐서 Glb 파일의 3D 모델이 Rnderable 객체에 담기게 됩니다.

이번 예제에서는 Instant Placement를 라디오 버튼에 따라 Node와 Renderable 다르게 사용하였습니다.

AnchorNode와 ModelRenderable, TransformableNode와 ModelRenderable과 ViewRenderable을 사용하였습니다.

 

6. Augmented Image

class CustomArFragment : ArFragment() {

    override fun getSessionConfiguration(session: Session?): Config {
        planeDiscoveryController.setInstructionView(null)

        val config = super.getSessionConfiguration(session)
        config.focusMode = Config.FocusMode.AUTO
        config.depthMode = Config.DepthMode.AUTOMATIC

        if(!buildDatabase(config, session)){
            Log.e("CustomArFragment", "Error database")
        }

        return config
    }

    private fun buildDatabase(config: Config, session: Session?) : Boolean{
        val bitmap = loadImage() ?: return false
        val augmentedImageDatabase = AugmentedImageDatabase(session)

        augmentedImageDatabase.addImage("qr", bitmap)
        config.augmentedImageDatabase = augmentedImageDatabase

        return true
    }

    private fun loadImage() : Bitmap? {
        try {
            val inputStream = context?.assets?.open("qr.png")
            return BitmapFactory.decodeStream(inputStream)
        }catch (e : IOException){
            e.printStackTrace()
        }
        return null
    }

}

우선 ArFragment를 약간 커스텀을 하였습니다. 
ArFragment에는 Config에 기본적으로 UpdateMode만 LATEST_CAMERA_IMAGE로 설정되어 있습니다.
이번에 Agumented Image를 사용할 것이기 때문에 AgumentedImageDatabase를 지정해 주어야 합니다.


AgumentedImageDatabase를 만들기 위해서는 Bitmap과 이름만 있으면 됩니다.
이번 예제의 경우 asstes에 저장해둔 이미지를 사용하였습니다.

이미지는 특징만 구분 가능하면 상관없지만 저는 qr코드를 사용하였습니다.

    private fun settingArFragment() = with(binding){
        arFragment = supportFragmentManager.findFragmentById(R.id.sceneFormFragment) as CustomArFragment2
        arFragment.arSceneView.scene.addOnUpdateListener(this@ARCoreActivity::onUpdateFrame)

		.... 중략 ....
        
    }

    private fun onUpdateFrame(frameTime: FrameTime){
        if(binding.radioAugmentedImage.isChecked){
            settingAugmentedImage()
        }
    }

    private fun settingAugmentedImage() {
        val frame = arFragment.arSceneView.arFrame ?: return

        frame.getUpdatedTrackables(AugmentedImage::class.java).forEach { augmentedImage ->
            if(augmentedImage.trackingState == TrackingState.TRACKING){
                if(augmentedImage.name == "qr"){
                    if(shouldConfigureSession.not()){
                        shouldConfigureSession = true
                        val node = MyARNode(this, renderableFileList[position])
                        node.setImage(augmentedImage)
                        arFragment.arSceneView.scene.addChild(node)
                    }
                }
            }
        }
    }

scene가 업데이트마다 getTrackingMethod()를 사용하여 현재 카메라에서 이미지를 추적하고 있는지 확인합니다.
AgumentedImageDatabase에 등록한 같은 이미지를 발견하게 되면 그 이미지 위에 모델을 표시하게 됩니다.

7. Model Animation

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

        arFragment = supportFragmentManager.findFragmentById(R.id.sceneFormFragment) as ArFragment
        uri = Uri.parse("model_fight.sfb")

        arFragment.setOnTapArPlaneListener { hitResult, plane, _ ->
            if (plane.type != Plane.Type.HORIZONTAL_UPWARD_FACING) {
                return@setOnTapArPlaneListener
            }
            val anchor = hitResult.createAnchor()
            placeObject(arFragment, anchor, uri)
        }

        binding.animateKickButton.setOnClickListener { animateModel("Character|Kick") }
        binding.animateIdleButton.setOnClickListener { animateModel("Character|Idle") }
        binding.animateBoxingButton.setOnClickListener { animateModel("Character|Boxing") }
    }

    private fun animateModel(name: String) {
        animator?.let { it ->
            if (it.isRunning) {
                it.end()
            }
        }
        renderable?.let { modelRenderable ->
            val data = modelRenderable.getAnimationData(name)
            animator = ModelAnimator(data, modelRenderable)
            animator?.start()
        }
    }

    private fun placeObject(fragment: ArFragment, anchor: Anchor, uri: Uri) {
        ModelRenderable.builder()
            .setSource(fragment.context, uri)
            .build()
            .thenAccept {
                renderable = it
                addToScene(fragment, anchor, it)
            }
            .exceptionally {
                val builder = AlertDialog.Builder(this)
                builder.setMessage(it.message).setTitle("Error")
                val dialog = builder.create()
                dialog.show()
                return@exceptionally null
            }
    }

    private fun addToScene(fragment: ArFragment, anchor: Anchor, renderable: Renderable) {
        val anchorNode = AnchorNode(anchor)

        val skeletonNode = SkeletonNode()
        skeletonNode.renderable = renderable

        val node = TransformableNode(fragment.transformationSystem)
        node.addChild(skeletonNode)
        node.setParent(anchorNode)

        fragment.arSceneView.scene.addChild(anchorNode)
    }

여기서 가장 중요한 부분은 다음과 같습니다.

        renderable?.let { modelRenderable ->
            val data = modelRenderable.getAnimationData(name)
            animator = ModelAnimator(data, modelRenderable)
            animator?.start()
        }

Rendrable 객체에서 getAnimationData(name : String) 또는  getAnimationData(index : Int)를 통해 애니메이션 데이터를 가져옵니다.

얻은 AnimationData와 Rendrable을 이용하여 ModelAnimator를 생성하고 start()를 활용하여 애니메이션을 작동합니다.

// Renderable 객체에 애니메이션의 개수 파악
modelRenderable.animationDataCount
// 애니메이션 반복 설정 - 0이 1회작동
animator?.repeatCount = 3

위의 방법으로 애니메이션 개수 파악과 반복 설정도 가능합니다.

 

8. Cloud Anchors

시작하기전에 Google Api를 등록해야합니다.

https://console.cloud.google.com/apis/api/arcorecloudanchor.googleapis.com/overview?project=maptest-278808 

 

Google Cloud Platform

하나의 계정으로 모든 Google 서비스를 Google Cloud Platform을 사용하려면 로그인하세요.

accounts.google.com

<meta-data
    android:name="com.google.android.ar.API_KEY"
    android:value="api-key" />

Api를 등록하고 Api key를 Menifest에 등록해줍니다.

Api를 등록하고 Api key를 Menifest에 등록해줍니다.

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

        fragment = supportFragmentManager.findFragmentById(R.id.sceneFormFragment) as CustomArFragment
        fragment.arSceneView.scene.addOnUpdateListener(this::onUpdateFrame)

        fragment.setOnTapArPlaneListener { hitResult, plane, motionEvent ->
            if(plane.type != Plane.Type.HORIZONTAL_UPWARD_FACING){
                return@setOnTapArPlaneListener
            }

            val newAnchor = fragment.arSceneView.session?.hostCloudAnchor(hitResult.createAnchor())
            setCloudAnchor(newAnchor)

            appAnchorState = AppAnchorState.HOSTING
            binding.textView.text = "Now hosting anchor..."

            placeObject(fragment, cloudAnchor, "out.glb")
        }

        storageManager = StorageManager(this)
    }

    private fun setCloudAnchor(newAnchor: Anchor?){
        if(cloudAnchor != null){
            cloudAnchor?.detach()
        }

        cloudAnchor = newAnchor
        appAnchorState = AppAnchorState.NONE
    }

    private fun placeObject(arFragment: ArFragment, anchor : Anchor?, fileName : String){
        val splitFileName = fileName.split(".")
        val file = File.createTempFile(splitFileName[0], splitFileName[1])

        assets.open(fileName).use { input->
            file.outputStream().use { output ->
                input.copyTo(output)
            }
        }

        val renderableSource =
            RenderableSource
                .builder()
                .setSource(this, Uri.parse(file.path), RenderableSource.SourceType.GLB)
                .setRecenterMode(RenderableSource.RecenterMode.ROOT)
                .build()

        ModelRenderable
            .builder()
            .setSource(this, renderableSource)
            .build()
            .thenAccept { renderable->
                addNodeToScene(arFragment, anchor, renderable)
            }
            .exceptionally { throwable ->
                alert {
                    title = "Error"
                    message = throwable.message.toString()
                }.show()
                return@exceptionally null
            }
    }
    
    private fun addNodeToScene(arFragment: ArFragment, anchor: Anchor?, renderable: ModelRenderable) {
        val anchorNode = AnchorNode(anchor)
        val node = TransformableNode(arFragment.transformationSystem)
        node.renderable = renderable
        node.setParent(anchorNode)
        arFragment.arSceneView.scene.addChild(anchorNode)
        node.select()
    }

다른 부분은 기존과 다 동일하고 Anchor 생성 부분만 hostCloudAnchor 로 생성해줍니다.

    private fun onUpdateFrame(frameTime: FrameTime){
        checkUpdatedAnchor()
    }

    @Synchronized private fun checkUpdatedAnchor(){
        if (appAnchorState !== AppAnchorState.HOSTING) {
            return
        }
        val cloudState = cloudAnchor!!.cloudAnchorState
        if (appAnchorState === AppAnchorState.HOSTING) {
            if (cloudState.isError) {
                binding.textView.text = "Error hosting anchor.. $cloudState"
                appAnchorState = AppAnchorState.NONE
            } else if (cloudState == CloudAnchorState.SUCCESS) {
                storageManager.nextShortCode(object : StorageManager.ShortCodeListener{
                    override fun onShortCodeAvailable(shortCode: Int?) {
                        if (shortCode == null) {
                            binding.textView.text = "Could not get shortCode"
                            return
                        }
                        storageManager.storeUsingShortCode(shortCode, cloudAnchor!!.cloudAnchorId)
                        binding.textView.text = "Anchor hosted! Cloud Short Code: $shortCode"
                    }

                })
                binding.textView.text = "Anchor hosted with id ${cloudAnchor?.cloudAnchorId}"
                appAnchorState = AppAnchorState.HOSTED
            }
        }
    }

    private enum class AppAnchorState{
        NONE,
        HOSTING,
        HOSTED,
        RESOLVING,
        RESOLVED
    }
internal class StorageManager(context: Context?) {
    internal interface CloudAnchorIdListener {
        fun onCloudAnchorIdAvailable(cloudAnchorId: String?)
    }

    internal interface ShortCodeListener {
        fun onShortCodeAvailable(shortCode: Int?)
    }

    private val rootRef: DatabaseReference

    fun nextShortCode(listener: ShortCodeListener) {
        rootRef
            .child(KEY_NEXT_SHORT_CODE)
            .runTransaction(
                object : Transaction.Handler {
                    override fun doTransaction(currentData: MutableData): Transaction.Result {
                        var shortCode: Int? = currentData.getValue(Int::class.java)
                        if (shortCode == null) {
                            shortCode = INITIAL_SHORT_CODE - 1
                        }
                        currentData.value = shortCode + 1
                        return Transaction.success(currentData)
                    }

                    override fun onComplete(error: DatabaseError?, committed: Boolean, currentData: DataSnapshot?) {
                        if (!committed) {
                            Log.e(TAG, "Firebase Error", error?.toException())
                            listener.onShortCodeAvailable(null)
                        } else {
                            listener.onShortCodeAvailable(currentData?.getValue(Int::class.java))
                        }
                    }
                })
    }

    fun storeUsingShortCode(shortCode: Int, cloudAnchorId: String?) {
        rootRef.child(KEY_PREFIX + shortCode).setValue(cloudAnchorId)
    }

    fun getCloudAnchorID(shortCode: Int, listener: CloudAnchorIdListener) {
        rootRef
            .child(KEY_PREFIX + shortCode)
            .addListenerForSingleValueEvent(
                object : ValueEventListener {
                    override fun onDataChange(dataSnapshot: DataSnapshot) {
                        listener.onCloudAnchorIdAvailable(java.lang.String.valueOf(dataSnapshot.getValue()))
                    }

                    override fun onCancelled(error: DatabaseError) {
                        Log.e(
                            TAG, "The database operation for getCloudAnchorID was cancelled.",
                            error.toException()
                        )
                        listener.onCloudAnchorIdAvailable(null)
                    }
                })
    }

    companion object {
        private val TAG = StorageManager::class.java.name
        private const val KEY_ROOT_DIR = "shared_anchor_codelab_root"
        private const val KEY_NEXT_SHORT_CODE = "next_short_code"
        private const val KEY_PREFIX = "anchor;"
        private const val INITIAL_SHORT_CODE = 142
    }

    init {
        val firebaseApp = FirebaseApp.initializeApp(context!!)
        rootRef = FirebaseDatabase.getInstance(firebaseApp!!).reference.child(KEY_ROOT_DIR)
        DatabaseReference.goOnline()
    }
}

영상에 약간 오류가 있어서 저 가이드 손모양이 안사라지는데 원인 분석 중입니다.
정상적으로 작동한다는 가정하에 진행 순서는


Plan 터치 이벤트 > AnchorNode 생성 (hostCloudAnchor) > ModelRenderable 생성 > 
TransformableNode 생성 및 ModelRenderable  > TransformableNode를 AnchorNode >
AnchorNode를 Scene에 등록 > Scene의 OnUpdateListener 이벤트 > AppAnchorState 상태에 따른 분기 >
StorageManger를 이용하여 Firebase Realtime Database에 저장

위의 과정을 거쳐 다음과 같이 저장이 됩니다!

 

이제 저장을 했으니 불러오는 과정을 진행해 보겠습니다.

    private fun onResolveOkPressed(dialogValue: String) {
        val shortCode = dialogValue.toInt()
        storageManager.getCloudAnchorID(shortCode, object : CloudAnchorIdListener {
            override fun onCloudAnchorIdAvailable(cloudAnchorId: String?) {
                val resolvedAnchor = fragment.arSceneView.session!!.resolveCloudAnchor(cloudAnchorId)
                setCloudAnchor(resolvedAnchor)
                placeObject(fragment, cloudAnchor, "out.glb")
                binding.textView.text = "Now Resolving Anchor.."
                appAnchorState = AppAnchorState.RESOLVING
            }
        })
    }
    
    @Synchronized private fun checkUpdatedAnchor(){
        if (appAnchorState !== AppAnchorState.HOSTING && appAnchorState !== AppAnchorState.RESOLVING) {
            return
        }
        val cloudState = cloudAnchor!!.cloudAnchorState
        if (appAnchorState === AppAnchorState.HOSTING) {
           ... 중략 ...
        }else if (appAnchorState === AppAnchorState.RESOLVING) {
            if (cloudState.isError) {
                binding.textView.text = "Error resolving anchor : $cloudState"

                appAnchorState = AppAnchorState.NONE
            } else if (cloudState == CloudAnchorState.SUCCESS) {
                binding.textView.text = "Anchor resolved successfully"
                appAnchorState = AppAnchorState.RESOLVED
            }
        }
    }

위에서 hostCloudAnchor  대신에 resolveCloudAnchor를 생성해주는 거 외에는 동일합니다.
resolveCloudAnchor를 만들때 host에서 만든 id값을 이용하여 생성합니다. 

 

728x90

'메타버스 스터디' 카테고리의 다른 글

ARCore 관련 프로젝트 분석하기  (0) 2021.10.24
AR Core 4주차  (0) 2021.07.16
ARCore 3주차  (0) 2021.07.11
ARCore 2주차  (0) 2021.07.04
ARCore  (0) 2021.06.27
댓글