티스토리 뷰

메타버스 스터디

ARCore 2주차

알렌보이스 2021. 7. 4. 19:56
728x90

 

    <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"/>

지난 글에서는 fragment에서 class를 통해 google에서 제공하는 ArFragment를 사용하여 제작하였습니다.

    <com.google.ar.sceneform.ArSceneView
        android:id="@+id/arView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

이번에는 ARScenceView를 통해 ARCore를 사용해보려고 합니다.

    private fun checkCameraPermission(){
        TedPermission.with(this)
            .setPermissionListener(object: PermissionListener {
                override fun onPermissionGranted() {
                	createSession()
                }
                override fun onPermissionDenied(deniedPermissions: MutableList<String>?) {
                    finish()
                }
            })
            .setDeniedMessage(R.string.permission_denied_message)
            .setPermissions(Manifest.permission.CAMERA)
            .check()
    }

ArFragment와 달리 자동으로 권한 요청을 해주지 않기 때문에 우선 권한 요청을 먼저 진행합니다.
편하게 사용하기 위해 TedPermission을 이용하였습니다.

https://github.com/ParkSangGwon/TedPermission

 

ParkSangGwon/TedPermission

Easy check permission library for Android Marshmallow - ParkSangGwon/TedPermission

github.com

 

    private fun createSession(){
        try {
            session = Session(this)

            configSession()

            binding.arView.setupSession(session)
            session.resume()
            binding.arView.resume()

        }catch (e : UnavailableArcoreNotInstalledException){
            e.printStackTrace()
        }catch (e : UnavailableApkTooOldException){
            e.printStackTrace()
        }catch (e : UnavailableSdkTooOldException){
            e.printStackTrace()
        }catch (e : UnavailableDeviceNotCompatibleException){
            e.printStackTrace()
        }catch (e : CameraNotAvailableException){
            e.printStackTrace()
        }catch (e : Exception){
            e.printStackTrace()
        }

    }

    private fun configSession(){
        val config = Config(session)
        config.updateMode = Config.UpdateMode.LATEST_CAMERA_IMAGE
        config.instantPlacementMode = Config.InstantPlacementMode.LOCAL_Y_UP
        session.configure(config)
    }
    
        override fun onPause() {
        super.onPause()

        if(this::session.isInitialized){
            session.pause()
            binding.arView.pause()
        }
    }

    override fun onDestroy() {
        super.onDestroy()

        session.close()
    }

카메라 권한 요청 후 Session을 구성해줍니다.
Session 생성 후 binding.arView.setupSession(session) 를 해주면 기본적인 ARCore사용 준비는 완료가 됩니다.
Confing를 통해 [조명 추정, 클라우드 앵커, Augmented Images, Agumented Faces, 깊이 API, 즉시 배치]와 같은 각종 기능을 추가 설정이 가능합니다.

이번 예제에서는 업데이트 모드와 즉시 배치 모드를 활성화하였습니다.

    private fun getAugmentImage() : Bitmap? {
        try {
            val inputStream = assets.open("qr.png")
            val bitmap = BitmapFactory.decodeStream(inputStream)
            binding.img.setImageBitmap(bitmap)
            return bitmap
        }catch (e : Exception){
            e.printStackTrace()
        }
        return null
    }

이번 예제에서는 Augmented Images를 진행할 예정이기 때문에 Bitmap 이미지를 준비합니다.

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

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

        return true
    }

val augmentedImageDatabase = AugmentedImageDatabase(session) 를 통해 비어있는 데이터베이스를 생성합니다.
.addImage("qr", bitmap) 를 통해 새 이미지를 등록합니다. "qr"은 나중에 이미지 이름으로 사용할 것입니다.
그 후 confing에 추가 후 위에처럼 session에 등록하면 됩니다.

    private fun initSceneView(){
        binding.arView.scene.addOnUpdateListener(this)
    }

    override fun onUpdate(frameTime: FrameTime?) {
        val frame = binding.arView.arFrame ?: return

        frame.getUpdatedTrackables(AugmentedImage::class.java).forEach { augmentedImage ->
            if(augmentedImage.trackingState == TrackingState.TRACKING){
                if(augmentedImage.name == "qr"){
                    if(isAgumentedImageVisible.not()){
                        isAgumentedImageVisible = true
                        val anchorNode = AnchorNode()
                        anchorNode.renderable = renderable

                        binding.arView.scene.addChild(anchorNode)
                    }
                }
            }
        }
    }

scence의 업데이트 정보를 받기 위해 Scene.OnUpdateListener를 상속받아서 등록해줍니다.

augmentedImage가 추적 중 데이터베이스에 저장된 "qr"과 3D 모델이 나오도록 설정했습니다.
저는 고정 이미지를 사용했기 때문에 [ augmentedImage.trackingState == TrackingState.TRACKING ]이 조건만 넣었는데 
움직이는 이미지 사용할 경우에는 아래의 조건을 추가해주면 됩니다.
if(augmentedImage.trackingMethod == AugmentedImage.TrackingMethod.FULL_TRACKING)

전체 코드

더보기

activity_arcore_augmented.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.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=".ARCoreAugmentedActivity">

    <com.google.ar.sceneform.ArSceneView
        android:id="@+id/arView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <ImageView
        android:id="@+id/img"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:src="@color/white"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

ARCoreAugmentedActivity

class ARCoreAugmentedActivity : AppCompatActivity(), Scene.OnUpdateListener{

    private val binding : ActivityArcoreAugmentedBinding by lazy { ActivityArcoreAugmentedBinding.inflate(layoutInflater) }

    private lateinit var session : Session
    private val fileName = "out.glb"
    private var isAgumentedImageVisible : Boolean = false
    private var renderable : ModelRenderable? = null


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

        // 카메라 권환 확인
        checkCameraPermission()

    }

    private fun checkCameraPermission(){
        TedPermission.with(this)
            .setPermissionListener(object: PermissionListener {
                override fun onPermissionGranted() {
                    initRenderableFile()
                    createSession()
                    initSceneView()
                }
                override fun onPermissionDenied(deniedPermissions: MutableList<String>?) {
                    finish()
                }
            })
            .setDeniedMessage(R.string.permission_denied_message)
            .setPermissions(Manifest.permission.CAMERA)
            .check()
    }

    // 3D 모델 파일 설정
    private fun initRenderableFile(){
        try {
            val splitFileName = fileName.split(".")
            val file = File.createTempFile(splitFileName[0], splitFileName[1])

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

            toast("3D 모델 준비 완료")
        }catch (e: IOException){
            e.printStackTrace()
            toast("파일 다운로드중 오류가 발생하였습니다.")
        }catch (e: java.lang.Exception){
            e.printStackTrace()
            toast("파일 다운로드중 오류가 발생하였습니다.")
        }
    }

    private fun createSession(){
        try {
            // Create a new ARCore session
            session = Session(this)

            // Configure the session
            configSession()

            binding.arView.setupSession(session)
            session.resume()
            binding.arView.resume()

        }catch (e : UnavailableArcoreNotInstalledException){
            e.printStackTrace()
        }catch (e : UnavailableApkTooOldException){
            e.printStackTrace()
        }catch (e : UnavailableSdkTooOldException){
            e.printStackTrace()
        }catch (e : UnavailableDeviceNotCompatibleException){
            e.printStackTrace()
        }catch (e : CameraNotAvailableException){
            e.printStackTrace()
        }catch (e : Exception){
            e.printStackTrace()
        }

    }

    private fun configSession(){
        val config = Config(session)
        if(!buildDatabase(config)){
            Toast.makeText(this, "Error database", Toast.LENGTH_SHORT).show()
        }

        config.updateMode = Config.UpdateMode.LATEST_CAMERA_IMAGE
        // 즉시 배치 모드를 설정합니다.
        config.instantPlacementMode = Config.InstantPlacementMode.LOCAL_Y_UP

        session.configure(config)
    }

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

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

        return true
    }

    private fun getAugmentImage() : Bitmap? {
        try {
            val inputStream = assets.open("qr.png")
            val bitmap = BitmapFactory.decodeStream(inputStream)
            binding.img.setImageBitmap(bitmap)
            return bitmap
        }catch (e : Exception){
            e.printStackTrace()
        }
        return null
    }

    private fun initSceneView(){
        binding.arView.scene.addOnUpdateListener(this)
    }

    override fun onUpdate(frameTime: FrameTime?) {
        val frame = binding.arView.arFrame ?: return

        frame.getUpdatedTrackables(AugmentedImage::class.java).forEach { augmentedImage ->
            if(augmentedImage.trackingState == TrackingState.TRACKING){
                if(augmentedImage.name == "qr"){
                    if(isAgumentedImageVisible.not()){
                        isAgumentedImageVisible = true
                        val anchorNode = AnchorNode()
                        anchorNode.renderable = renderable

                        binding.arView.scene.addChild(anchorNode)
                    }
                }
            }
        }
    }

    private fun buildModel(file: File){
        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 ->
                toast("다운로드 완료")
                renderable = modelRenderable
            }
            .exceptionally {
                toast("${it.message}")
                return@exceptionally null
            }
    }

    override fun onPause() {
        super.onPause()

        if(this::session.isInitialized){
            session.pause()
            binding.arView.pause()
        }
    }

    override fun onDestroy() {
        super.onDestroy()

        session.close()
    }

}

등록한 이미지 (qr코드)를 발견하면 오른쪽 사진과 같이 모델이 나오게 됩니다.
화면 오른쪽 화단은 그냥 예시 이미지로 넣어 둔 거고 필수는 아닙니다.
근데 이게 한 번 인식이 안되면 쫌 잘 안 되는 느낌이 있습니다.

 

저번 글에 올린 내용에 추가 제작을 하였습니다.
https://alanboyce.tistory.com/4

 

ARCore

이번 주는 ARCore에 대해서 공부를 진행하였습니다. 이름에서도 알 수 있듯이 ARCore는 AR의 필수 기능(모션 추적, 환경 이해 및 조명 추정 등)의 API를 제공해주는 SDK입니다. https://developers.google.com/ar.

alanboyce.tistory.com

지난 글에서 즉시 배치만 진행하였고 이번에 이름표 단것과 위에서 한 Agumented Image를 하려고 하였으나
Agumented Image는 아직 테스트 진행 중입니다.

시작하기 전에 앞서 지난번의 ArFragment 설정하는 부분을 수정하였습니다.

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

anchorNode.setOnTouchListener를 추가하여 등록한 anchor를 누르면 지울 수 있도록 설정하였습니다.

<?xml version="1.0" encoding="utf-8"?>
<TextView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/txtName"
    android:background="@drawable/bg_round_teal"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:padding="20dp"
    android:textSize="18sp"
    android:textStyle="bold"
    android:textColor="@color/white"
    tools:text="고양이"/>

이름표를 위해 위의 xml을 만들었습니다.

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

이번에는 기본 AnchorNode 위에 TransformableNode를 올렸습니다.
이름 그대로 움직임이 가능한 노드입니다.

저 하얀색 점을 인식한 곳이면 이동이 가능한 AnchorNode입니다.

 

다음 주에는 AnchorNode 종류들과 Config 셋팅들 테스트 진행과 클라우드 앵커 부분 진행할 계획입니다.

728x90

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

ARCore 정리  (0) 2021.08.06
AR Core 4주차  (0) 2021.07.16
ARCore 3주차  (0) 2021.07.11
ARCore  (0) 2021.06.27
구글 글라스 학습 (구글 문서)  (0) 2021.06.20
댓글