티스토리 뷰

728x90

ARCore관련해서 좀 더 공부를 하기 위하여 자료를 조사하던 중 GitHub에 올라온 것들 중 배우고 싶은 것이 있는 것들 위주로 공부를 하게되었습니다.

첫 번 째는 skydoves 님의 Pokedex-AR입니다.

https://github.com/skydoves/Pokedex-AR

 

GitHub - skydoves/Pokedex-AR: 🦄 Android Pokedex-AR using ARCore, Sceneform, Hilt, Coroutines, Flow, Jetpack (Room, ViewModel,

🦄 Android Pokedex-AR using ARCore, Sceneform, Hilt, Coroutines, Flow, Jetpack (Room, ViewModel, LiveData) based on MVVM architecture. - GitHub - skydoves/Pokedex-AR: 🦄 Android Pokedex-AR using ARCo...

github.com

 

ARCore를 활용하여 포켓몬 GO를 비슷하게 만든 프로젝트인 것 같습니다.
포켓몬을 눌렀을 때에는 점프하는 동작을 하고 몬스터볼을 눌렀을 때에는 몬스터볼이 날라가 포켓몬을 잡게 됩니다.

제가 이 프로젝트에서 배우고 싶었던 것은 모델의 움직임과 애니메이션을 주는 방법입니다.

 

  private fun initializeModels(arFragment: ArFragment, session: Session) {
    if (session.allAnchors.isEmpty() && !viewModel.isCaught) {
      val pose = Pose(floatArrayOf(0f, 0f, -1f), floatArrayOf(0f, 0f, 0f, 1f))
      session.createAnchor(pose).apply {
        val pokemon = PokemonModels.getRandomPokemon()
        ModelRenderer.renderObject(this@SceneActivity, pokemon) { renderable ->
          ModelRenderer.addPokemonOnScene(arFragment, this, renderable, pokemon)
        }

        val pokeBall = PokemonModels.getPokeball()
        ModelRenderer.renderObject(this@SceneActivity, pokeBall) { renderable ->
          ModelRenderer.addPokeBallOnScene(arFragment, this, this, renderable, pokeBall, pokemon) {
            viewModel.insertPokemonModel(pokemon)
          }
        }
      }
    }
  }

이 프로젝트에서는 앱을 최초 실행할 때 포켓몬과 몬스터볼의 위치를 지정하여 화면에 뿌려줍니다.

  private fun getEevee() = RenderingModel(
    name = "eevee",
    model = "eevee.sfb",
    localPosition = DEFAULT_POSITION_POKEMON
  )

getRandomPokemon()함수를 이용하면 위와 같이 정의된 함수중 한개가 랜덤으로 실행되게 됩니다.

data class RenderingModel(
  val name: String,
  val model: String,
  val direction: Vector3 = Vector3(0f, 0f, 1f),
  val scale: Float = 1f,
  val localPosition: Vector3 = Vector3(0.5f, 0.5f, 0.5f)
)

RenderingModel에서 Vector3 값으로 방향과 위치값을 설정하게 됩니다.

  fun addPokemonOnScene(
    fragment: ArFragment,
    anchor: Anchor,
    renderable: Renderable,
    renderingModel: RenderingModel
  ) {
    val anchorNode = AnchorNode(anchor)
    TransformableNode(fragment.transformationSystem).apply {
      
      .. (중략) ..
      
      setOnTouchListener { hitTestResult, motionEvent ->
        if (motionEvent.action == MotionEvent.ACTION_UP) {
          hitTestResult.node?.let { node ->
            node.setLookDirection(Vector3(0f, 0f, 1f))
            ModelAnimations.translateModel(
              anchorNode = node,
              targetPosition = Vector3(
                localPosition.x,
                localPosition.y + 0.25f,
                localPosition.z
              ),
              doWhenFinish = {
                val localPosition = renderingModel.localPosition
                ModelAnimations.translateModel(node, localPosition)
              }
            )
          }
        }
        true
      }
    }
  }

TransformableNode를 생성하고 터치 시에 ModelAnimations.translateModel() 함수를 실행하게 됩니다.

object ModelAnimations {
  inline fun translateModel(
    anchorNode: Node,
    targetPosition: Vector3,
    durationTime: Long = 150L,
    crossinline doWhenFinish: () -> Unit = {}
  ) {
    ObjectAnimator().apply {
      setAutoCancel(false)
      target = anchorNode
      duration = durationTime
      setObjectValues(
        anchorNode.localPosition,
        targetPosition
      )
      setPropertyName("localPosition")
      setEvaluator(Vector3Evaluator())
      interpolator = AccelerateDecelerateInterpolator()
      start()
    }.doWhenFinish { doWhenFinish() }
  }
}

ObjectAnimator를 이용하여 모델의 현위치 -> targetPosition 위치로 이동하게 됩니다.

inline fun ObjectAnimator.doWhenFinish(
  crossinline block: () -> Unit
) {
  addListener(object : Animator.AnimatorListener {
    override fun onAnimationEnd(animation: Animator?) = block()
    override fun onAnimationStart(animation: Animator?) = Unit
    override fun onAnimationCancel(animation: Animator?) = Unit
    override fun onAnimationRepeat(animation: Animator?) = Unit
  })
}

그 다음 doWhenFinish를 통해 변한 위치 -> 기존 위치의 값으로 다시 ModelAnimations.translateModel() 수행함으로써
점프하는 효과를 주었습니다.

val targetPosition = Vector3(
	pokemonPosition.x + getRandomPosition(),
	pokemonPosition.y + getRandomPosition(),
	pokemonPosition.z + getRandomPosition()
)

  private fun getRandomPosition(): Float {
    val position = Random.nextFloat()
    return if (position <= 0.5f) {
      position
    } else {
      position - 1
    }
  }

몬스터볼의 경우에는 getRandomPosition()함수를 활용해서 랜덤으로 위치 targetPosition을 설정하여 랜던방향으로 날아가게 됩니다.

 

두 번째는 SimonMarquis님의 AR-Toolbox입니다.

https://github.com/SimonMarquis/AR-Toolbox

 

GitHub - SimonMarquis/AR-Toolbox: 🧰 ARCore & Sceneform Playground

🧰 ARCore & Sceneform Playground. Contribute to SimonMarquis/AR-Toolbox development by creating an account on GitHub.

github.com

이 프로젝트에서는 ARCore로 할 수 있는 대부분의 기능이 다 구현되어 있었습니다.

그 중 제일 궁금했던 부분은 Color, Metallic, Roughness, Reflectance 와 같은 효과를 주는 방법과
AR 그리기, 거리재기 기능 구현입니다.

코드가 좀 어려워서 완벽하게 이해하진 못 했지만 제가 분석한 것 위주로 작성하겠습니다~

1) 모델에 [ Color, Metallic, Roughness, Reflectance ] 속성 값 주기

sealed class MaterialNode(
    name: String,
    val properties: MaterialProperties,
    coordinator: Coordinator,
    settings: Settings
) : Nodes(name, coordinator, settings) {

    init {
        update()
    }

    fun update(block: (MaterialProperties.() -> Unit) = {}) {
        properties.update(renderable?.material, block)
    }

}

Nodes, Coordinator, Settings 모두 프로젝트 내부에서 정의된 클래스이지만 이 기능과는 관계가 별로 없습니다.
update 함수를 통해 MaterialProperties의 속성을 update 해줍니다.

class MaterialProperties(
    @field:ColorInt var color: Int = DEFAULT_COLOR,
    @field:IntRange(from = 0, to = 100) var metallic: Int = DEFAULT_METALLIC,
    @field:IntRange(from = 0, to = 100) var roughness: Int = DEFAULT_ROUGHNESS,
    @field:IntRange(from = 0, to = 100) var reflectance: Int = DEFAULT_REFLECTANCE
) {
    companion object {

        private const val DEFAULT_COLOR = Color.WHITE
        private const val DEFAULT_METALLIC = 0
        private const val DEFAULT_ROUGHNESS = 40
        private const val DEFAULT_REFLECTANCE = 50

        val DEFAULT = MaterialProperties()

    }

    fun update(material: Material?, block: (MaterialProperties.() -> Unit) = {}) {
        block(this)
        material?.apply {
            setFloat3(MATERIAL_COLOR, color.toArColor())
            setFloat(MATERIAL_METALLIC, metallic / 100F)
            setFloat(MATERIAL_ROUGHNESS, roughness / 100F)
            setFloat(MATERIAL_REFLECTANCE, reflectance / 100F)
        }
    }

}

setFloat와 setFloat3 함수 매개변수로 MATERIAL_ 로 시작하는 값을 확인할 수 있습니다.

package com.google.ar.sceneform.rendering;

import android.content.Context;
import androidx.annotation.RequiresApi;
import com.google.ar.sceneform.rendering.R.raw;
import java.util.concurrent.CompletableFuture;

@RequiresApi(
    api = 24
)
public final class MaterialFactory {
    public static final String MATERIAL_COLOR = "color";
    public static final String MATERIAL_TEXTURE = "texture";
    public static final String MATERIAL_METALLIC = "metallic";
    public static final String MATERIAL_ROUGHNESS = "roughness";
    public static final String MATERIAL_REFLECTANCE = "reflectance";
	
    .. 이하 생략 ..
}

MaterialFactory에 정의되어있는 name과 setFloat와 setFloat3 Material 속성 값을 변경할 수 있습니다.

Material 속성을 가지고 있다면 위의 다섯가지 속성을 언제든 바꿀 수 있습니다.
가장 간단하게 Material 속성을 적용하려면 ShapeFactory을 이용하여 ModelRenderable을 생성 하는 방법입니다.
ShapeFactory에서는 makeCube, makeSphere, makeCylinder 이 3가지 함수를 제공합니다.
그래서 이 프로젝트에서도 큐브, 구, 원기둥을 활용해서 속성을 적용하는걸 확인할 수 있습니다.

 

2) 드로잉

when (motionEvent.action) {
	MotionEvent.ACTION_DOWN -> drawing = Drawing.create(x, y, true, materialProperties(), this, coordinator, settings)
	MotionEvent.ACTION_MOVE -> drawing?.extend(x, y)
	MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> drawing = drawing?.deleteIfEmpty().let { null }
}

모션 이벤트를 통해서

DOWN 상태일때 Drawing을 생성

MOVE 상태일때 extend

UP, CANCEL 상태일때 마치게 됩니다.

class Drawing(
    val isFromTouch: Boolean,
    private val plane: CollisionPlane?,
    properties: MaterialProperties,
    coordinator: Coordinator,
    settings: Settings
) : MaterialNode("Drawing", properties, coordinator, settings) {

    companion object {

        ..(중략)..

        fun create(
            x: Float,
            y: Float,
            fromTouch: Boolean,
            properties: MaterialProperties,
            ar: ArSceneView,
            coordinator: Coordinator,
            settings: Settings
        ): Drawing? {
            
            ..(중략)..

            return Drawing(fromTouch, plane, properties, coordinator, settings).apply {
                makeOpaqueWithColor(context.applicationContext, properties.color.toArColor()).thenAccept { material = it }
                attach(anchor, scene)
                extend(x, y)
            }
        }
    }
}

makerOpaueWitchColor은 MaterialFactory에 정의된 함수로 Material 생성과 동시에 색상을 지정할 수 있는 함수입니다.

    private val line = LineSimplifier()
    private var material: Material? = null
        set(value) {
            field = value?.apply { properties.update(this) }
            render()
        }

    private fun append(pointInWorld: Vector3) {
        val pointInLocal = (parent as AnchorNode).worldToLocalPoint(pointInWorld)
        line.append(pointInLocal)
        render()
    }

    private fun render() {
        val definition = ExtrudedCylinder.makeExtrudedCylinder(RADIUS, line.points, material ?: return) ?: return
        if (renderable == null) {
            ModelRenderable.builder().setSource(definition).build().thenAccept { renderable = it }
        } else {
            renderable?.updateFromDefinition(definition)
        }
    }

    fun extend(x: Float, y: Float) {
        val ray = scene?.camera?.screenPointToRay(x, y) ?: return
        if (plane != null) {
            val rayHit = RayHit()
            if (plane.rayIntersection(ray, rayHit)) {
                append(rayHit.point)
            }
        } else {
            append(ray.getPoint(DEFAULT_DRAWING_DISTANCE))
        }
    }

    fun deleteIfEmpty() = if (line.points.size < 2) detach() else Unit

LineSimplifier와 ExtrudedCylinder은 프로젝트에서 정의된 클래스인데 이 부분은 아직 분석 중입니다.

ACTION_DOWN이 호출될 때 위의 과정을 거쳐서 Drawing 객체와 모델을 생성하게 됩니다.

ACTION_MOVE가 호출될 때 위의 과정을 거쳐 기존의 ExtrudedCylinder에 연장해서 그리게 됩니다.

ACTION_UP, ACTION_CANCEL이 호출 될 때 deleteIfEmpty가 호출되어 그리기를 종료하게 됩니다.

 

 

Measure 일차적으로 분석은 하였으나 정리할 필요가 있어서 다음에 올리도록 하겠습니다~

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
ARCore  (0) 2021.06.27
댓글