티스토리 뷰

메타버스 스터디

ARCore

알렌보이스 2021. 6. 27. 12:40
728x90

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

 

환경 설정

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

<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'

// anko library : https://github.com/Kotlin/anko
implementation "org.jetbrains.anko:anko:0.10.8"

// firebase
implementation platform('com.google.firebase:firebase-bom:28.1.0')
implementation 'com.google.firebase:firebase-analytics-ktx'
implementation 'com.google.firebase:firebase-storage-ktx'

 

3D 모델 준비

다른 글에서는 fbx 또는 obj 파일을 다운로드 후에

'Goole Sceneform Tools' 플러그인을 받아서 스튜디오 안에서 3D 파일 확인 및 사용이 가능하다고 하는데
저는 안드로이드 스튜디오 4.2.1 버전을 사용 하고 있는데 플러그인이 설치가 안됩니다.

그래서 이 글에서는 glb 파일을 이용해서 진행할 예정입니다.

https://poly.google.com/

 

Poly

3D 세계를 탐험해 보세요

poly.google.com

파일은 위의 페이지에서 다운로드하였는데 2021.06.30 기준 서비스 종료된다고 합니다.
나중에 안건대 Window에 기본 설치가 되어있던 3D 뷰어에서 기본 제공하는 라이브러리도 glb확장자 파일로 저장이 가능합니다.

 

ARFragment 설정

activity_arcore.xml

    <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를 사용할 예정입니다.
그러기 위해서 fragment에 class="com.google.ar.sceneform.ux.ArFragment" 를 사용합니다.

ARCoreActivity.kt

val arFragment = supportFragmentManager.findFragmentById(R.id.sceneFormFragment) as ArFragment

이렇게 연결해준 뒤 실행을 해주면

ArFragement에서 카메라 권한 요청을 하고 오른쪽과 같은 가이드 애니메이션이 나오게 됩니다.

카메라를 이동하다 보면 위의 사진과 같인 점들이 찍히는 걸 확인할 수 있습니다.
나주에 저 점이 있는 곳에 3D 모델을 올립니다.

 

리사이클러뷰 설정

이번 예제에서는 5개의 3D 파일을 Firebase Storage에 저장해 놓았습니다.

위와 같이 리사이클러뷰를 만들어서 사용 전에 다운로드한 뒤 사용할 예정입니다.
이미지는 따로 drawable에 넣어 두었습니다

ModelItem.kt

data class ModelItem(
    val imageResource : Int,				// drawable Resource id
    val name : String,						// 이름표에 쓰일 이름
    val fileName : String,					// Firebase Storage에 있는 파일 이름
    var readable: ModelRenderable?= null,	// model 객체
    var isSelected : Boolean = false		// 리사이클러뷰에서 선택 여부
)

ModelAdapter.kt

class ModelAdapter(val modelSelectListener : (ModelRenderable?) -> Unit, val downloadListener : (ModelItem, Int) -> Unit) : ListAdapter<ModelItem, ModelAdapter.ModelViewHolder>(diffUtil) {

    inner class ModelViewHolder(private val binding : CellModelBinding) : RecyclerView.ViewHolder(binding.root){
        fun bind(modelItem: ModelItem){
            binding.imageModel.setImageResource(modelItem.imageResource)
            binding.btnDownload.isVisible = modelItem.readable == null
            binding.layoutBackground.backgroundResource = if(modelItem.isSelected) R.drawable.bg_round_teal else R.drawable.bg_round_white

            binding.root.setOnClickListener {
                setCheckedPosition(layoutPosition)

                if(modelItem.isSelected){
                    if(modelItem.readable == null){
                        downloadListener(modelItem, layoutPosition)
                    }else{
                        modelSelectListener(modelItem.readable)
                    }
                }else{
                    modelItem.isSelected = true
                }
            }
        }
    }

    private fun setCheckedPosition(position: Int){
        currentList.forEachIndexed { index, modelItem ->
            modelItem.isSelected = index == position
        }
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ModelViewHolder {
        return ModelViewHolder(CellModelBinding.inflate(LayoutInflater.from(parent.context), parent, false))
    }

    override fun onBindViewHolder(holder: ModelViewHolder, position: Int) {
        holder.bind(currentList[position])
    }

    companion object {
        val diffUtil = object : DiffUtil.ItemCallback<ModelItem>(){
            override fun areItemsTheSame(oldItem: ModelItem, newItem: ModelItem): Boolean = oldItem == newItem

            override fun areContentsTheSame(oldItem: ModelItem, newItem: ModelItem): Boolean = oldItem.fileName == newItem.fileName
        }
    }
}

ARCoreActivity.kt

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

        binding.modelRecyclerView.adapter = adapter
    }

처음 선택했을 시에는 downloadLister를 통해 파일을 받아 온 뒤 renderable 값을 채워줍니다.
다운로드한 뒤에는 저장해둔 renderable을 받아서 사용합니다.

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

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

        }catch (e: IOException){
            e.printStackTrace()
            binding.progressBar.isVisible = false
            toast("파일 다운로드중 오류가 발생하였습니다.")
        }catch (e: Exception){
            e.printStackTrace()
            binding.progressBar.isVisible = false
            toast("파일 다운로드중 오류가 발생하였습니다.")
        }
    }

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

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

이 부분은 리사이클러뷰 설정과 파이어베이스 다운로드 부분입니다.
임시 파일을 만든 뒤 Firebase에서 파일을 받아와서 내용을 채웁니다.

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

        downloadEndMessage()
    }

Firebase에서 받아온 file을 RederableSource builder를 통해 ModelRenderable에 사용할 수 있는 source를 만들어 줍니다.
setSource에서 지원하는 확장자는 GLB와 GLTF2 확장자를 지원해줍니다.

ModelRenderable builder를 통해 fragment에서 사용할 ModelRenderable 객체를 생성합니다.

    private fun settingArFragment(){
        arFragment = supportFragmentManager.findFragmentById(R.id.sceneFormFragment) as ArFragment
        arFragment.setOnTapArPlaneListener { hitResult, _, _ ->
            val anchorNode = AnchorNode(hitResult.createAnchor())
            anchorNode.renderable = renderable
            arFragment.arSceneView.scene.addChild(anchorNode)
        }
    }

위에 하얀 점 위에 3D 모델을 올리는 코드입니다.
AnchorNode에 위에서 받은 renderable 담아줍니다.
그 뒤에 ArFragement(화면)에 3D모델을 표시해줍니다.

 

ARCoreActivity.kt 전체 코드

class ARCoreActivity : AppCompatActivity() {

    private val binding : ActivityArcoreBinding by lazy { ActivityArcoreBinding.inflate(layoutInflater) }
    private lateinit var arFragment : ArFragment

    private lateinit var adapter: ModelAdapter
    private var renderable : ModelRenderable? = null

    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) {
        downloadStartMessage()

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

        }catch (e: IOException){
            e.printStackTrace()
            binding.progressBar.isVisible = false
            toast("파일 다운로드중 오류가 발생하였습니다.")
        }catch (e: Exception){
            e.printStackTrace()
            binding.progressBar.isVisible = false
            toast("파일 다운로드중 오류가 발생하였습니다.")
        }
    }

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

        downloadEndMessage()
    }

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

    private fun downloadEndMessage(){
        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 = "clock.glb"))

        adapter.submitList(modelList)
    }

    private fun settingArFragment(){
        arFragment = supportFragmentManager.findFragmentById(R.id.sceneFormFragment) as ArFragment
        arFragment.setOnTapArPlaneListener { hitResult, _, _ ->
            val anchorNode = AnchorNode(hitResult.createAnchor())
            anchorNode.renderable = renderable
            arFragment.arSceneView.scene.addChild(anchorNode)
        }
    }
}

 

결과 화면

 

 

진행 중 & 아쉬운 점 & 추후 진행

1. 애니메이션이 있는 파일 진행 중입니다만 애니메이션이 없다고 나와서 원인을 분석 중입니다.

2. 결과 사진에 나온 이상해풀도 다운로드할 때에는 색이 있는 걸 받은 건데 왜 인지는 모르겠지만 색이 안 나오네요 ㅠㅠ

3. 3D 객체 여러 개 꺼내면 핸드폰 발열과 배터리 소모가 크게 느껴집니다. (간단한 게임 돌리고 할 때는 발열 못 느끼는데 체감이 확실히 납니다.)

4. 이름표 표시하는 거랑 몇 개 더 진행해 보고 ArFragment말고 상세 설정해서 진행하는 걸 해봐야겠습니다.

728x90

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

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