티스토리 뷰
애니메이션 효과
애니메이션을 위해 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를 등록해야합니다.
<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://github.com/Kristina-Simakova/arcore_model_animation
'메타버스 스터디' 카테고리의 다른 글
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 |
- Total
- Today
- Yesterday
- Compose ConstraintLayout
- Android
- Compose 네이버 지도 api
- WebView
- 웹뷰
- Duplicate class found error
- 안드로이드
- Compose 네이버 지도
- Duplicate class fond 에러
- 포켓몬 도감
- Worker
- Compose BottomSheet
- Retrofit
- Compose BottomSheetDialog
- compose
- LazyColumn
- 안드로이드 구글 지도
- Compose ModalBottomSheetLayout
- Kotlin
- column
- WorkManager
- Android Compose
- Compose QRCode Scanner
- Compose Naver Map
- Pokedex
- Compose BottomSheetScaffold
- Row
- Compose MotionLayout
- Fast api
- Gradient
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |