본문 바로가기



관련 기술/AR(증강현실)

Sceneform을 적용하여 기본 AR앱을 만들어보자 (2)

지난 글에서 Sceneform을 적용하여 평면인식과 화면에 포인트 클라우드를 표시하는 기능까지 알아보았습니다.
이번 글에서는 인식한 평면을 기준으로 3D 객체를 배치하는 기능을 살펴보겠습니다.

 

 

 

 


지난 글에서 레이아웃에 Fragment 요소를 적용시킨 것을 기억하시죠?
이제 우리가 화면에 직접 무엇인가를 그려넣기 위해서는 해당 요소를 제어할 수 있는 변수를 만들고, 그 변수에 해당 요소를 할당해야 됩니다.

import com.google.ar.sceneform.ux.ArFragment;

...

    private ArFragment arFragment;
    
...

    @Override
    @SuppressWarnings({"AndroidApoChecker", "FutureReturnValueIgnored"})
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if(!checkIsSupportedDeviceOrFinish(this)){
            return;
        }
        setContentView(R.layout.activity_main);
        arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.ux_fragment);
    }

위의 코드와 같이 arFragment에 레이아웃의 요소를 할당하였습니다.

그럼 화면에 이것저것 그려넣기 위한 작업을 해야겠죠.

OpenGL을 이용하여 화면에 그림을 그리기 위해서는 렌더링 과정이 필요합니다.

그리고 화면에 배치할 3D 객체도 준비해야겠군요.

 

일단 화면에 그려넣을 3D 객체를 먼저 준비합시다.

3D 객체를 표현하기 위해서는 mtl, obj, sfa, sfb 파일이 필요합니다.

이 중에서 sfa, sfb 파일은 빌드하면서 생성되는 파일이니까 mtl, obj 파일을 준비해야되겠죠.

그럼 거의 표준처럼 사용되는 안드로보이 객체 데이터를 가져오도록 합시다.

 

GitHub의 Sceneform에 있는 hellosceneform 예제에서 파일을 가져옵니다. ( https://github.com/google-ar/sceneform-android-sdk/tree/v1.15.0/samples/hellosceneform/app/sampledata/models )

해당 위치에 가 보면 아래와 같이 4개의 파일을 볼 수 있습니다.

GitHub - google-ar/sceneform-android-sdk 의 3d모델 샘플 파일

파일을 모두 받아와서 필요한 위치에 복사하여 사용해도 되고 andy.mtl 파일과 andy.obj 파일만 받아와서 빌드한 후 사용해도 됩니다.

 

ARCore에서는 Sceneform에서 obj 파일과 같은 3D 자원을 사용할 때 Sceneform에서 사용하는 고유의 바이너리 파일 형태로 변환해서 사용합니다.

sfa, sfb 파일이 그것인데 sfa 파일 SceneForm Asset Definition 파일을 말하고, sfb 파일 SceneForm Binary 파일이라는 의미입니다.

이렇게 sfb 파일로 변환하기 위해서 Sceneform에서는 변환용 도구를 플러그인의 형태로 제공합니다.

이 플러그인을 안드로이드 스튜디오에 설치해서 사용하면 됩니다.

플러그인 설치 방법은 개발자 문서( https://developers.google.com/sceneform/develop/getting-started#import-sceneform-plugin )를 참고하시기 바랍니다.

 

Sceneform 도구 플러그인 설치

 

자.. 이제 플러그인도 설치 되었고, 3D 객체 파일이 준비되면 프로젝트에 포함을 시켜야겠죠.

이 플러그인을 프로젝트에 사용하기 위해서 build.gradle (Project: xxxxx) 파일을 열고 플러그인의 Classpath를 등록해 줍니다. (Gradle의 Classpath는 아마 프로젝트 생성 시 자동으로 등록이 되어 있을겁니다.)

    dependencies {
        classpath "com.android.tools.build:gradle:4.0.0"
        classpath 'com.google.ar.sceneform:plugin:1.15.0'
    }

 

다음으로 build.gradle (Module: app) 파일을 열어서 아래의 내용을 제일 아래에 추가하겠습니다.

apply plugin: 'com.google.ar.sceneform.plugin'

sceneform.asset('sampledata/models/andy.obj',	// 사용하고자 하는 3D 객체 파일의 경로
        'default',				// 재질(Material) 경로
        'sampledata/models/andy.sfa',		// sfa 파일의 출력 경로
        'src/main/res/raw/andy')		// sfb 파일의 출력 경로

build.gradle (Module: app) 파일에 조금 전에 추가한 플러그인을 적용시키는데 com.google.ar.sceneform.plugin 이 바로 그 플러그인 입니다.

 

그리고 변환을 위하여 sceneform 자산 정보를 위의 코드에 정의된 순서대로 설정하여 변환작업을 수행하게 합니다.

그럼 위에서 지정한 sampledata/models/ 폴더에 GitHub에서 받아온 andy.obj 파일과 andy.mtl 파일을 저장해 두고 빌드하면 파일들이 변환됩니다.

sampledata/models/ 폴더는 지금 개발중인 프로젝트의 app 폴더를 기준으로 합니다.

( 위의 설정에 따르면 {project path}/app/sampledata/models/ 의 경로가 되겠죠)

 

설정이 끝난 후에 역시 "Sync Now" 버튼을 눌러주면 아래와 같이 설정이 성공적으로 완료되었다는 메시지가 나옵니다.

Java로 개발하고 있는데 내부적으로는 Kotlin과 뭔가 연관이 있나봅니다.

뭔가 문제가 없나... 빌드를 시켜 보았습니다.

Sceneform적용, 3D객체 설정 적용 후의 AR 앱화면

문제 없이 잘 나옵니다.

 

빌드 후에 안드로이드 스튜디오의 왼쪽에 있는 프로젝트 탭을 보면 빌드된 andy.sfb 파일이 res/raw/ 폴더에 저장되어 있는 것을 확인할 수 있습니다.

andy.sfb 파일 빌드 성공

 

 

 

 

 

그럼 이제 평면위에 3D 객체를 배치하는 작업을 시작하죠.

 

지금까지 준비한 안드로보이 3D 객체를 AR 앱의 화면에 출력하기 위하여 먼저 렌더링 가능한 모델의 형태로 바꾸어주어야 합니다.

이를 위해서 ModelRenderable 의 형태로 andyRenderable 이라는 변수를 하나 만들어줍시다.

그리고 ModelRenderable 객체를 빌드시키기 위한 빌더를 설정합니다.

이 작업들도 onCreate() 메소드에 적용하겠습니다.

ModelRenderable.builder()
        .setSource(this, R.raw.andy)
        .build()
        .thenAccept(renderable -> andyRenderable = renderable)
        .exceptionally(
                throwable -> {
                    Toast toast = Toast.makeText(this, "Unable to load andy renderable", Toast.LENGTH_LONG);
                    toast.setGravity(Gravity.CENTER, 0, 0);
                    toast.show();
                    return null;
                });

코드의 내용은 위에서부터 순서대로 읽어내려가면 됩니다.

    - ModelRenderable 객체의 빌더를 설정하는데
    - 소스데이터는 R.raw.andy를 선택하고
    - 빌드를 한다.
    - 그리고 접근할때는 renderable의 형태인 andyRenderable 변수를 대상으로 한다.
    - 예외가 발행하면 throwable 블럭의 내용을 실시한다.

와 같은 식으로 읽으면 쉽게 이해가 됩니다.

 

3D 객체를 렌더링 가능한 모델로 빌드하고 나면 arFragment에 할당하고 Listner를 통해서 계속적으로 감시, 갱신을 수행하도록 합니다.

감시, 갱신의 대상은 화면에 출력되는 arFragment 요소에 터치(또는 탭) 이벤트가 발생하는지 여부입니다.

화면을 손가락으로 탭하면 해당 위치를 읽고, 소속된 평면이 있는지, 있다면 어떤 평면인지, 또 다른 모션 이벤트는 무엇이 발생했는지 등을 확인합니다

그리고 읽어들인 정보를 기준으로 앵커를 생성, 설치하고 해당 앵커를 기준으로 각각 필요한 값을 설정하며, 설정된 정보(위치, 방향, 소속 평면 등)를 기반으로 3D 객체를 배치합니다.  

arFragment.setOnTapArPlaneListener(
        (HitResult hitResult, Plane plane, MotionEvent motionEvent) -> {
            if(andyRenderable == null){
                return;
            }

            Anchor anchor = hitResult.createAnchor();
            AnchorNode anchorNode = new AnchorNode(anchor);
            anchorNode.setParent(arFragment.getArSceneView().getScene());

            TransformableNode andy = new TransformableNode(arFragment.getTransformationSystem());
            andy.setParent(anchorNode);
            andy.setRenderable(andyRenderable);
            andy.select();
        }
);

이것으로 화면을 탭(터치)하면 해당 위치에 안드로보이를 그려주도록 하는 작업이 끝났습니다.

그럼 빌드하고 실행시켜보겠습니다.

디스플레이에 탭하면 안드로보이를 그려주는 AR앱 화면

잘 동작합니다.

(그런데... 왜 흔한 초록색 안드로보이가 아니라.. 흰색 안드로보이가 그려지는지 모르겠군요. 예전엔 안그랬는데..)

 

지금까지의 내용이 헷갈리지 않게 MainActivity.java 파일 전체를 올려보겠습니다.

package com.aidalab.proparsf;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.widget.Toast;

import com.google.ar.core.Anchor;
import com.google.ar.core.HitResult;
import com.google.ar.core.Plane;
import com.google.ar.sceneform.AnchorNode;
import com.google.ar.sceneform.rendering.ModelRenderable;
import com.google.ar.sceneform.ux.ArFragment;
import com.google.ar.sceneform.ux.TransformableNode;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();
    private static final double MIN_OPENGL_VERSION = 3.0;

    private ArFragment arFragment;

    private ModelRenderable andyRenderable;

    @Override
    @SuppressWarnings({"AndroidApoChecker", "FutureReturnValueIgnored"})
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if(!checkIsSupportedDeviceOrFinish(this)){
            return;
        }
        setContentView(R.layout.activity_main);
        arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.ux_fragment);

        // 사용할 3D 객체를 렌더링 가능한 모델로 빌드하도록 설정하고 빌드시킴 
        ModelRenderable.builder()
                .setSource(this, R.raw.andy)
                .build()
                .thenAccept(renderable -> andyRenderable = renderable)
                .exceptionally(
                        throwable -> {
                            Toast toast = Toast.makeText(this, "Unable to load andy renderable", Toast.LENGTH_LONG);
                            toast.setGravity(Gravity.CENTER, 0, 0);
                            toast.show();
                            return null;
                        });

        // 디스플레이의 한 점을 탭(터치)하면 그 위치에 3D 객체를 그려주도록 리스너 설치
        arFragment.setOnTapArPlaneListener(
                (HitResult hitResult, Plane plane, MotionEvent motionEvent) -> {
                    if(andyRenderable == null){
                        return;
                    }

                    Anchor anchor = hitResult.createAnchor();
                    AnchorNode anchorNode = new AnchorNode(anchor);
                    anchorNode.setParent(arFragment.getArSceneView().getScene());

                    TransformableNode andy = new TransformableNode(arFragment.getTransformationSystem());
                    andy.setParent(anchorNode);
                    andy.setRenderable(andyRenderable);
                    andy.select();
                }
        );
    }

    @Override
    protected void onResume() {
        super.onResume();
        requestCameraPermission();

    }

    /**
     * Sceneform을 실행할 수 없으면 false 반환
     *
     * @param activity
     * @return
     */
    public static boolean checkIsSupportedDeviceOrFinish(final Activity activity){
        // SDK 최소 버전을 만족하지 못하면 False Return
        if(Build.VERSION.SDK_INT < Build.VERSION_CODES.N){
            Log.e(TAG, "Sceneform requires Android N or later");
            Toast.makeText(activity, "Sceneform requires Android N or later.", Toast.LENGTH_LONG).show();
            activity.finish();
            return false;
        }

        // OpenGL 최소 버전을 만족하지 못하면 False Return
        String openGlVersionString =
                ((ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE))
                        .getDeviceConfigurationInfo()
                        .getGlEsVersion();

        if (Double.parseDouble(openGlVersionString) < MIN_OPENGL_VERSION) {
            Log.e(TAG, "Sceneform requires OpenGL ES 3.0 later");
            Toast.makeText(activity, "Sceneform requires OpenGL ES 3.0 or later", Toast.LENGTH_LONG)
                    .show();
            activity.finish();
            return false;
        }
        return true;
    }

    private void requestCameraPermission(){
        if(ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED){
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, 0);
        }
    }
}

여기까지 해서 Sceneform을 이용한 기본적인 AR 앱을 만들어보았습니다.

직접 제어해서 만드는 것보다 확실히 쉽기도 하고 작업량도 줄었고.. 그리고 성능도 괜찮군요.

 

다음 글에서는 나의 위치 또는 디스플레이에 탭한 위치 등에 대한 좌표 확인, 활용 등에 대해서 알아보도록 하겠습니다.

 

 

 

 

반응형