티스토리 뷰

메타버스 스터디

AR Core 4주차

알렌보이스 2021. 7. 16. 21:45
728x90

애니메이션 효과

모델 애니메이션 테스트 영상

애니메이션을 위해 3D 모델 파일을 저장합니다.
저의 경우 안드로이드 스튜디오 4.2.2 버전을 사용하고 있는데 아래의 플러그인이 설치가 안됩니다.

그래서 우선 다른 분이 깃허브에 올려주신 파일을 다운로드 받아서 사용하기로 하였습니다.

sampledata와 assets에 받아서 파일을 저장하였습니다.
원래는 sampledata에 있는 fbx파일을 위의 플러그인으로 sfb파일로 변환후에 사용한다고 합니다.

    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: HitResult, plane: Plane, motionEvent: MotionEvent ->
            if (plane.type != Plane.Type.HORIZONTAL_UPWARD_FACING) {
                return@setOnTapArPlaneListener
            }
            val anchor = hitResult.createAnchor()
            placeObject(arFragment, anchor, uri)
        }

    }

    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)
    }

우선 모델을 배치하는 코드를 작성해줍니다. (이 부분은 이전 글에서도 계속 한 부분이라 생략합니다.)

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

		... 중략 ...

        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()
        }
    }

이번 예제에서는 name을 통해서 애니메이션 데이터를 얻고 애니메이션의 동작을 수행했는데

modelRenderable.animationDataCount
modelRenderable.getAnimationData(0)

위와 같이 개수 파악후에 원하는 동작의 index 값으로도 애니메이션의 데이터를 얻을 수 있습니다.

animator?.repeatCount = 3

start 전에 repeatCount를 통해 반복 설정이 가능한데 3을 했을때 4번 동작을 합니다.

 

 

Cloud Anchor

시작하기전에 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에 등록해줍니다.

    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값을 이용하여 생성합니다. 

 

 

출처 ( 원본 코드에서 약간씩만 수정하였습니다.)

https://medium.com/@ardeploy/build-shared-augmented-reality-experience-for-android-using-sceneform-and-arcore-cloud-anchors-29ae1c55bea7

 

Build Shared Augmented Reality Experience for Android using Sceneform and ARCore Cloud Anchors

In Google I/O 2018, some of the most exciting features with respect to Augmented Reality were ARCore, Sceneform, and Cloud Anchors…

medium.com

https://github.com/Kristina-Simakova/arcore_model_animation

 

Kristina-Simakova/arcore_model_animation

How to animate 3D models in ARCore Sceneform + Mixamo - Kristina-Simakova/arcore_model_animation

github.com

 

728x90

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

ARCore 관련 프로젝트 분석하기  (0) 2021.10.24
ARCore 정리  (0) 2021.08.06
ARCore 3주차  (0) 2021.07.11
ARCore 2주차  (0) 2021.07.04
ARCore  (0) 2021.06.27
댓글