관리 메뉴

나만을 위한 블로그

[Android] 화면에 생성한 AR core 객체와 ArFragment 화면을 함께 캡쳐하는 법 본문

Android

[Android] 화면에 생성한 AR core 객체와 ArFragment 화면을 함께 캡쳐하는 법

참깨빵위에참깨빵 2020. 7. 3. 19:00
728x90
반응형

※ 아래 예제는 갤럭시 S8+에서 정상 작동을 확인했지만 상위 핸드폰에서 작동하는지는 확인하지 않았다

 

XML 파일

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
    android:id="@+id/main_ar_layout"
    tools:context=".DrawAR.OtherDrawAr.OtherDrawActivity">

    <fragment
        android:id="@+id/sceneform_fragment"
        android:name="com.google.ar.sceneform.ux.ArFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true" />

    <LinearLayout
        android:id="@+id/controlsPanel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="30dp"
        android:background="@drawable/panel"
        android:orientation="horizontal"
        android:padding="10dp">

        <ImageView
            android:id="@+id/clearButton"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:layout_gravity="center"
            android:contentDescription="@string/clear_drawing"
            android:src="@drawable/ic_delete" />

        <ImageView
            android:id="@+id/colorPickerIcon"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:layout_gravity="center"
            android:layout_marginStart="14dp"
            android:contentDescription="@string/select_color"
            android:src="@drawable/ic_selected_white" />

        <ImageView
            android:id="@+id/undoButton"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:layout_gravity="center"
            android:layout_marginStart="14dp"
            android:contentDescription="@string/undo_last_drawing_element"
            android:src="@drawable/ic_undo" />

        <ImageView
            android:id="@+id/other_ar_camera"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:layout_gravity="center"
            android:layout_marginStart="14dp"
            android:contentDescription="@string/undo_last_drawing_element"
            android:src="@drawable/ic_camera" />

    </LinearLayout>

    <LinearLayout
        android:id="@+id/colorPanel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="30dp"
        android:background="@drawable/panel"
        android:orientation="horizontal"
        android:padding="10dp">

        <ImageView
            android:id="@+id/whiteCircle"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:layout_marginEnd="14dp"
            android:contentDescription="@string/white_color"
            android:src="@drawable/ic_white_circle" />

        <ImageView
            android:id="@+id/redCircle"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:layout_marginEnd="14dp"
            android:contentDescription="@string/red_color"
            android:src="@drawable/ic_red_circle" />

        <ImageView
            android:id="@+id/greenCircle"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:layout_marginEnd="14dp"
            android:contentDescription="@string/green_color"
            android:src="@drawable/ic_green_circle" />

        <ImageView
            android:id="@+id/blueCircle"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:layout_marginEnd="14dp"
            android:contentDescription="@string/blue_color"
            android:src="@drawable/ic_blue_circle" />

        <ImageView
            android:id="@+id/blackCircle"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:layout_marginEnd="14dp"
            android:contentDescription="@string/black_color"
            android:src="@drawable/ic_black_circle" />

        <ImageView
            android:id="@+id/rainbowCircle"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:contentDescription="@string/rainbow_color"
            android:src="@drawable/ic_rainbow_circle" />

    </LinearLayout>

</RelativeLayout>

 

자바 파일, 액티비티 자바 파일 1개 + 클래스 파일 4개다

액티비티 파일 먼저 올림

 

import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.PixelCopy;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import com.google.ar.core.Pose;
import com.google.ar.core.TrackingState;
import com.google.ar.sceneform.AnchorNode;
import com.google.ar.sceneform.ArSceneView;
import com.google.ar.sceneform.Camera;
import com.google.ar.sceneform.FrameTime;
import com.google.ar.sceneform.HitTestResult;
import com.google.ar.sceneform.Scene;
import com.google.ar.sceneform.collision.Ray;
import com.google.ar.sceneform.math.Vector3;
import com.google.ar.sceneform.rendering.Color;
import com.google.ar.sceneform.rendering.Material;
import com.google.ar.sceneform.rendering.MaterialFactory;
import com.google.ar.sceneform.rendering.Texture;
import com.google.ar.sceneform.rendering.Texture.Sampler;
import com.google.ar.sceneform.rendering.Texture.Sampler.WrapMode;
import com.google.ar.sceneform.ux.ArFragment;
import com.vocabulary.childvoca.R;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.concurrent.CompletionException;

public class OtherDrawActivity extends AppCompatActivity implements Scene.OnUpdateListener, Scene.OnPeekTouchListener
{
    private static final String TAG = OtherDrawActivity.class.getSimpleName();
    private static final double MIN_OPENGL_VERSION = 3.0;
    private static final float DRAW_DISTANCE = 0.13f;
    private static final Color WHITE = new Color(android.graphics.Color.WHITE);
    private static final Color RED = new Color(android.graphics.Color.RED);
    private static final Color GREEN = new Color(android.graphics.Color.GREEN);
    private static final Color BLUE = new Color(android.graphics.Color.BLUE);
    private static final Color BLACK = new Color(android.graphics.Color.BLACK);

    private ArFragment fragment;
    private AnchorNode anchorNode;
    private final ArrayList<Stroke> strokes = new ArrayList<>();
    private Material material;
    private Stroke currentStroke;

    LinearLayout colorPanel;
    LinearLayout controlPanel;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        if (!checkIsSupportedDeviceOrFinish(this))
        {
            return;
        }
        setContentView(R.layout.activity_other_draw);

        colorPanel = (LinearLayout) findViewById(R.id.colorPanel);
        controlPanel = (LinearLayout) findViewById(R.id.controlsPanel);

        MaterialFactory.makeOpaqueWithColor(this, WHITE).thenAccept(material1 -> material = material1.makeCopy()).exceptionally(throwable -> {
            displayError(throwable);
            throw new CompletionException(throwable);
        });

        fragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.sceneform_fragment);
        fragment.getArSceneView().getPlaneRenderer().setEnabled(false);
        fragment.getArSceneView().getScene().addOnUpdateListener(this);
        fragment.getArSceneView().getScene().addOnPeekTouchListener(this);

        // 그린 선들 전부 지우기
        ImageView clearButton = (ImageView) findViewById(R.id.clearButton);
        clearButton.setOnClickListener(view -> {
            for (Stroke stroke : strokes)
            {
                stroke.clear();
            }
            strokes.clear();
        });

        // 한 획 지우기
        ImageView undoButton = (ImageView) findViewById(R.id.undoButton);
        undoButton.setOnClickListener(view -> {
            if (strokes.size() < 1)
            {
                return;
            }
            int lastIndex = strokes.size() - 1;
            strokes.get(lastIndex).clear();
            strokes.remove(lastIndex);
        });

        // 사진 찍기
        ImageView cameraButton = (ImageView) findViewById(R.id.other_ar_camera);
        cameraButton.setOnClickListener(view -> {
            takePhoto();
            Toast.makeText(this, "사진이 앨범에 성공적으로 저장되었습니다", Toast.LENGTH_SHORT).show();
        });

        setUpColorPickerUi();
    }

    private void setUpColorPickerUi()
    {
        ImageView colorPickerIcon = (ImageView) findViewById(R.id.colorPickerIcon);
        colorPanel.setVisibility(View.GONE);
        colorPickerIcon.setOnClickListener(view -> {
            if (controlPanel.getVisibility() == View.VISIBLE)
            {
                controlPanel.setVisibility(View.GONE);
                colorPanel.setVisibility(View.VISIBLE);
            }
        });

        ImageView whiteCircle = (ImageView) findViewById(R.id.whiteCircle);
        whiteCircle.setOnClickListener((onClick) -> {
            setColor(WHITE);
            colorPickerIcon.setImageResource(R.drawable.ic_selected_white);
        });
        ImageView redCircle = (ImageView) findViewById(R.id.redCircle);
        redCircle.setOnClickListener((onClick) -> {
            setColor(RED);
            colorPickerIcon.setImageResource(R.drawable.ic_selected_red);
        });

        ImageView greenCircle = (ImageView) findViewById(R.id.greenCircle);
        greenCircle.setOnClickListener((onClick) -> {
            setColor(GREEN);
            colorPickerIcon.setImageResource(R.drawable.ic_selected_green);
        });

        ImageView blueCircle = (ImageView) findViewById(R.id.blueCircle);
        blueCircle.setOnClickListener((onClick) -> {
            setColor(BLUE);
            colorPickerIcon.setImageResource(R.drawable.ic_selected_blue);
        });

        ImageView blackCircle = (ImageView) findViewById(R.id.blackCircle);
        blackCircle.setOnClickListener((onClick) -> {
            setColor(BLACK);
            colorPickerIcon.setImageResource(R.drawable.ic_selected_black);
        });

        ImageView rainbowCircle = (ImageView) findViewById(R.id.rainbowCircle);
        rainbowCircle.setOnClickListener((onClick) -> {
            setTexture(R.drawable.rainbow_texture);
            colorPickerIcon.setImageResource(R.drawable.ic_selected_rainbow);
        });
    }

    private String generateFilename()
    {
        // 현재시간을 기준으로 파일명 생성
        String date = new SimpleDateFormat("yyyyMMddHHmmss", java.util.Locale.getDefault()).format(new Date());
        return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + File.separator + "arcapture/" + date + "_screenshot.jpg";
    }

    private void saveBitmapToDisk(Bitmap bitmap, String filename) throws IOException
    {
        // 사용자의 갤러리에 arcapture 디렉토리 생성 및 Bitmap을 JPEG 형식으로 갤러리에 저장
        File out = new File(filename);
        if (!out.getParentFile().exists())
        {
            out.getParentFile().mkdirs();
        }
        try (FileOutputStream outputStream = new FileOutputStream(filename); ByteArrayOutputStream outputData = new ByteArrayOutputStream())
        {
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputData);
            outputData.writeTo(outputStream);
            outputStream.flush();
        } catch (IOException ex)
        {
            throw new IOException("Failed to save bitmap to disk", ex);
        }
    }

    private void takePhoto()
    {
        // PixelCopy를 사용하여 카메라 화면과 object를 bitmap으로 생성
        final String filename = generateFilename();
        ArSceneView view = fragment.getArSceneView();

        final Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);

        final HandlerThread handlerThread = new HandlerThread("PixelCopier");
        handlerThread.start();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
        {
            PixelCopy.request(view, bitmap, (copyResult) -> {
                if (copyResult == PixelCopy.SUCCESS)
                {
                    try
                    {
                        saveBitmapToDisk(bitmap, filename);

                        // Media Scanning 실시
                        Uri uri = Uri.parse("file://" + filename);
                        Intent i = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
                        i.setData(uri);
                        sendBroadcast(i);

                    } catch (IOException e)
                    {
                        Toast toast = Toast.makeText(OtherDrawActivity.this, e.toString(), Toast.LENGTH_LONG);
                        toast.show();
                        Log.e("takePhoto()", e.toString());
                        return;
                    }
                }
                else
                {
                    Toast toast = Toast.makeText(OtherDrawActivity.this, "스크린샷 저장 실패! : " + copyResult, Toast.LENGTH_LONG);
                    toast.show();
                }
                handlerThread.quitSafely();
            }, new Handler(handlerThread.getLooper()));
        }
    }

    @SuppressWarnings({"FutureReturnValueIgnored"})
    private void setTexture(int resourceId)
    {
        Texture.builder().setSource(fragment.getContext(), resourceId).setSampler(Sampler.builder().setWrapMode(WrapMode.REPEAT).build()).build().thenCompose(texture -> MaterialFactory.makeOpaqueWithTexture(fragment.getContext(), texture)).thenAccept(material1 -> material = material1.makeCopy()).exceptionally(throwable -> {
            displayError(throwable);
            throw new CompletionException(throwable);
        });

        colorPanel.setVisibility(View.GONE);
        controlPanel.setVisibility(View.VISIBLE);
    }

    @SuppressWarnings({"FutureReturnValueIgnored"})
    private void setColor(Color color)
    {
        MaterialFactory.makeOpaqueWithColor(fragment.getContext(), color).thenAccept(material1 -> material = material1.makeCopy()).exceptionally(throwable -> {
            displayError(throwable);
            throw new CompletionException(throwable);
        });
        colorPanel.setVisibility(View.GONE);
        controlPanel.setVisibility(View.VISIBLE);
    }

    @Override
    public void onPeekTouch(HitTestResult hitTestResult, MotionEvent tap)
    {
        int action = tap.getAction();
        Camera camera = fragment.getArSceneView().getScene().getCamera();
        Ray ray = camera.screenPointToRay(tap.getX(), tap.getY());
        Vector3 drawPoint = ray.getPoint(DRAW_DISTANCE);
        if (action == MotionEvent.ACTION_DOWN)
        {
            if (anchorNode == null)
            {
                ArSceneView arSceneView = fragment.getArSceneView();
                com.google.ar.core.Camera coreCamera = arSceneView.getArFrame().getCamera();
                if (coreCamera.getTrackingState() != TrackingState.TRACKING)
                {
                    return;
                }
                Pose pose = coreCamera.getPose();
                anchorNode = new AnchorNode(arSceneView.getSession().createAnchor(pose));
                anchorNode.setParent(arSceneView.getScene());
            }
            currentStroke = new Stroke(anchorNode, material);
            strokes.add(currentStroke);
            currentStroke.add(drawPoint);
        }
        else if (action == MotionEvent.ACTION_MOVE && currentStroke != null)
        {
            currentStroke.add(drawPoint);
        }
    }

    @Override
    public void onUpdate(FrameTime frameTime)
    {
        com.google.ar.core.Camera camera = fragment.getArSceneView().getArFrame().getCamera();
        if (camera.getTrackingState() == TrackingState.TRACKING)
        {
            fragment.getPlaneDiscoveryController().hide();
        }
    }


    private void displayError(Throwable throwable)
    {
        Log.e(TAG, "Unable to create material", throwable);
        Toast toast = Toast.makeText(this, "Unable to create material", Toast.LENGTH_LONG);
        toast.setGravity(Gravity.CENTER, 0, 0);
        toast.show();
    }

    /**
     * Returns false and displays an error message if Sceneform can not run, true if Sceneform can run on this device.
     * Sceneform을 실행할 수 없으면 false를 반환하고 Sceneform을 이 장치에서 실행할 수 있으면 true를 표시
     * <p>
     * Sceneform requires Android N on the device as well as OpenGL 3.0 capabilities.
     * Sceneform에는 장치의 Android N 및 OpenGL 3.0 기능이 필요하다
     * <p>
     * Finishes the activity if Sceneform can not run
     * Sceneform을 실행할 수 없는 경우 액티비티를 종료
     */
    public static boolean checkIsSupportedDeviceOrFinish(final Activity activity)
    {
        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;
        }
        String openGlVersionString = ((ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE)).getDeviceConfigurationInfo().getGlEsVersion();
        if (Double.parseDouble(openGlVersionString) < MIN_OPENGL_VERSION)
        {
            Log.e(TAG, "Sceneform은 OpenGL ES 3.0 이상의 버전을 요구합니다");
            Toast.makeText(activity, "Sceneform requires OpenGL ES 3.0 or later", Toast.LENGTH_LONG).show();
            activity.finish();
            return false;
        }
        return true;
    }

}
// CaptureUtil.java

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Environment;
import android.util.LruCache;
import android.view.View;
import android.widget.Toast;

import androidx.recyclerview.widget.RecyclerView;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class CaptureUtil
{
    // 캡쳐한 사진이 저장될 외부 저장소
    private static final String CAPTURE_PATH = "/arcapture";

    /**
     * 특정 뷰만 캡쳐
     *
     * @param View
     */
    public static void captureView(View View)
    {
        View.buildDrawingCache();
        Bitmap captureView = View.getDrawingCache();
        FileOutputStream fos;

        String strFolderPath = Environment.getExternalStorageDirectory().getAbsolutePath() + CAPTURE_PATH;
        File folder = new File(strFolderPath);
        if (!folder.exists())
        {
            folder.mkdirs();
        }

        String strFilePath = strFolderPath + "/" + System.currentTimeMillis() + ".png";
        File fileCacheItem = new File(strFilePath);

        try
        {
            fos = new FileOutputStream(fileCacheItem);
            captureView.compress(Bitmap.CompressFormat.PNG, 100, fos);
        } catch (FileNotFoundException e)
        {
            e.printStackTrace();
        }
    }

    /**
     * 액티비티 전체 캡쳐
     *
     * @param context
     */
    public static void captureActivity(Activity context)
    {
        if (context == null)
        {
            return;
        }
        View root = context.getWindow().getDecorView().getRootView();
        root.setDrawingCacheEnabled(true);
        root.buildDrawingCache();
        // 루트뷰의 캐시를 가져옴
        Bitmap screenshot = root.getDrawingCache();

        // get view coordinates
        int[] location = new int[2];
        root.getLocationInWindow(location);

        // 이미지를 자를 수 있으나 전체 화면을 캡쳐 하도록 함
        Bitmap bmp = Bitmap.createBitmap(screenshot, location[0], location[1], root.getWidth(), root.getHeight(), null, false);
        String strFolderPath = Environment.getExternalStorageDirectory().getAbsolutePath() + CAPTURE_PATH;
        File folder = new File(strFolderPath);
        if (!folder.exists())
        {
            folder.mkdirs();
        }

        String strFilePath = strFolderPath + "/" + System.currentTimeMillis() + ".png";
        File fileCacheItem = new File(strFilePath);
        OutputStream out = null;
        try
        {
            fileCacheItem.createNewFile();
            out = new FileOutputStream(fileCacheItem);
            bmp.compress(Bitmap.CompressFormat.PNG, 100, out);
        } catch (Exception e)
        {
            e.printStackTrace();
        } finally {
            try
            {
                out.close();
            } catch (IOException e)
            {
                e.printStackTrace();
            }
        }
        Toast.makeText(context, "촬영완료", Toast.LENGTH_SHORT).show();
    }

    /**
     * 리사이클러뷰 전체스크롤 캡쳐
     *
     * @param view
     * @param bgColor
     */
    public static void captureRecyclerView(RecyclerView view, int bgColor)
    {
        if (view == null)
        {
            return;
        }
        captureMyRecyclerView(view, bgColor, 0, view.getAdapter().getItemCount() - 1);
    }

    /**
     * 리사이클러뷰 범위 캡쳐
     *
     * @param view
     * @param bgColor
     * @param startPosition
     * @param endPosition
     */
    public static void captureRecyclerView(RecyclerView view, int bgColor, int startPosition, int endPosition)
    {
        if (view == null)
        {
            return;
        }
        captureMyRecyclerView(view, bgColor, startPosition, endPosition);
    }


    private static void captureMyRecyclerView(RecyclerView view, int bgColor, int startPosition, int endPosition)
    {
        RecyclerView.Adapter adapter = view.getAdapter();
        Bitmap bigBitmap = null;
        if (adapter != null)
        {

            if (startPosition > endPosition)
            {
                int tmp = endPosition;
                endPosition = startPosition;
                startPosition = tmp;
            }

            int size = endPosition - startPosition;
            int height = 0;
            Paint paint = new Paint();
            int iHeight = 0;
            final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

            final int cacheSize = maxMemory / 8;
            LruCache<String, Bitmap> bitmaCache = new LruCache<>(cacheSize);
            for (int i = startPosition; i < endPosition + 1; i++)
            {
                RecyclerView.ViewHolder holder = adapter.createViewHolder(view, adapter.getItemViewType(i));
                adapter.onBindViewHolder(holder, i);
                holder.itemView.measure(View.MeasureSpec.makeMeasureSpec(view.getWidth(), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
                holder.itemView.layout(0, 0, holder.itemView.getMeasuredWidth(), holder.itemView.getMeasuredHeight());
                holder.itemView.setDrawingCacheEnabled(true);
                holder.itemView.buildDrawingCache();
                if (bgColor != 0)
                {
                    holder.itemView.setBackgroundColor(bgColor);
                }
                Bitmap drawingCache = holder.itemView.getDrawingCache();
                if (drawingCache != null)
                {

                    bitmaCache.put(String.valueOf(i), drawingCache);
                }

                height += holder.itemView.getMeasuredHeight();
            }

            bigBitmap = Bitmap.createBitmap(view.getMeasuredWidth(), height, Bitmap.Config.ARGB_8888);
            Canvas bigCanvas = new Canvas(bigBitmap);
            bigCanvas.drawColor(Color.WHITE);

            for (int i = 0; i < size + 1; i++)
            {
                Bitmap bitmap = bitmaCache.get(String.valueOf(i));
                bigCanvas.drawBitmap(bitmap, 0f, iHeight, paint);
                iHeight += bitmap.getHeight();
                bitmap.recycle();
            }

        }

        String strFolderPath = Environment.getExternalStorageDirectory().getAbsolutePath() + CAPTURE_PATH;
        File folder = new File(strFolderPath);
        if (!folder.exists())
        {
            folder.mkdirs();
        }

        String strFilePath = strFolderPath + "/" + System.currentTimeMillis() + ".png";
        File fileCacheItem = new File(strFilePath);
        OutputStream out = null;
        try
        {
            fileCacheItem.createNewFile();
            out = new FileOutputStream(fileCacheItem);
            bigBitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
        } catch (Exception e)
        {
            e.printStackTrace();
        } finally
        {
            try
            {
                out.close();
            } catch (IOException e)
            {
                e.printStackTrace();
            }
        }
    }
}
// ExtrudedCylinder.java

import com.google.ar.sceneform.math.MathHelper;
import com.google.ar.sceneform.math.Quaternion;
import com.google.ar.sceneform.math.Vector3;
import com.google.ar.sceneform.rendering.Material;
import com.google.ar.sceneform.rendering.ModelRenderable;
import com.google.ar.sceneform.rendering.RenderableDefinition;
import com.google.ar.sceneform.rendering.RenderableDefinition.Submesh;
import com.google.ar.sceneform.rendering.Vertex;
import com.google.ar.sceneform.rendering.Vertex.UvCoordinate;
import com.google.ar.sceneform.utilities.AndroidPreconditions;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Utility class used to dynamically construct {@link ModelRenderable}s for extruded cylinders.
 */
public class ExtrudedCylinder
{
    private static final String TAG = ExtrudedCylinder.class.getSimpleName();
    private static final int NUMBER_OF_SIDES = 8;

    private enum Direction
    {
        UP, DOWN
    }

    /**
     * Creates a {@link ModelRenderable} in the shape of a cylinder with the give specifications.
     * 주기 지정을 사용하여 실린더 모양으로 {@link ModelRenderable}을 만든다
     *
     * @param radius   the radius of the constructed cylinder
     * @param points   the list of points the extruded cylinder will be constructed around
     * @param material the material to use for rendering the cylinder
     * @return renderable representing a cylinder with the given parameters
     */
    @SuppressWarnings("AndroidApiChecker")
    // CompletableFuture requires api level 24
    public static RenderableDefinition makeExtrudedCylinder(float radius, List<Vector3> points, Material material)
    {
        AndroidPreconditions.checkMinAndroidApiLevel();

        if (points.size() < 2)
        {
            return null;
        }

        ArrayList<Vertex> vertices = new ArrayList<>();
        ArrayList<Integer> triangleIndices = new ArrayList<>();
        ArrayList<Quaternion> rotations = new ArrayList<>();
        Vector3 desiredUp = Vector3.up();

        for (int point = 0; point < points.size() - 1; point++)
        {
            generateVerticesFromPoints(desiredUp, vertices, rotations, points.get(point + 1), points.get(point), radius);
        }

        updateConnectingPoints(vertices, points, rotations, radius);
        generateTriangleIndices(triangleIndices, points.size());
        updateEndPointUV(vertices);

        // Add start cap
        makeDisk(vertices, triangleIndices, points, 0, Direction.UP);
        // Add end cap
        makeDisk(vertices, triangleIndices, points, points.size() - 1, Direction.DOWN);

        Submesh submesh = Submesh.builder().setTriangleIndices(triangleIndices).setMaterial(material).build();

        RenderableDefinition renderableDefinition = RenderableDefinition.builder().setVertices(vertices).setSubmeshes(Arrays.asList(submesh)).build();

        return renderableDefinition;
    }

    private static void generateVerticesFromPoints(Vector3 desiredUp, List<Vertex> vertices, List<Quaternion> rotations, Vector3 firstPoint, Vector3 secondPoint, float radius)
    {
        final Vector3 difference = Vector3.subtract(firstPoint, secondPoint);
        Vector3 directionFromTopToBottom = difference.normalized();
        Quaternion rotationFromAToB = Quaternion.lookRotation(directionFromTopToBottom, desiredUp);

        // cosTheta0 provides the angle between the rotations
        if (!rotations.isEmpty())
        {
            double cosTheta0 = dot(rotations.get(rotations.size() - 1), rotationFromAToB);
            // Flip end rotation to get shortest path if needed
            if (cosTheta0 < 0.0)
            {
                rotationFromAToB = negated(rotationFromAToB);
            }
        }
        rotations.add(rotationFromAToB);

        directionFromTopToBottom = Quaternion.rotateVector(rotationFromAToB, Vector3.forward()).normalized();
        Vector3 rightDirection = Quaternion.rotateVector(rotationFromAToB, Vector3.right()).normalized();
        Vector3 upDirection = Quaternion.rotateVector(rotationFromAToB, Vector3.up()).normalized();
        desiredUp.set(upDirection);

        List<Vertex> bottomVertices = new ArrayList<>();

        final float halfHeight = difference.length() / 2;
        final Vector3 center = Vector3.add(firstPoint, secondPoint).scaled(.5f);

        final float thetaIncrement = (float) (2 * Math.PI) / NUMBER_OF_SIDES;
        float theta = 0;
        float cosTheta = (float) Math.cos(theta);
        float sinTheta = (float) Math.sin(theta);
        float uStep = (float) 1.0 / NUMBER_OF_SIDES;

        // Generate edge vertices along the sides of the cylinder.
        for (int edgeIndex = 0; edgeIndex <= NUMBER_OF_SIDES; edgeIndex++)
        {
            // Create top edge vertex
            Vector3 topPosition = Vector3.add(directionFromTopToBottom.scaled(-halfHeight), Vector3.add(rightDirection.scaled(radius * cosTheta), upDirection.scaled(radius * sinTheta)));
            Vector3 normal = Vector3.subtract(topPosition, directionFromTopToBottom.scaled(-halfHeight)).normalized();
            topPosition = Vector3.add(topPosition, center);
            UvCoordinate uvCoordinate = new UvCoordinate(uStep * edgeIndex, 0);

            Vertex vertex = Vertex.builder().setPosition(topPosition).setNormal(normal).setUvCoordinate(uvCoordinate).build();
            vertices.add(vertex);

            // Create bottom edge vertex
            Vector3 bottomPosition = Vector3.add(directionFromTopToBottom.scaled(halfHeight), Vector3.add(rightDirection.scaled(radius * cosTheta), upDirection.scaled(radius * sinTheta)));
            normal = Vector3.subtract(bottomPosition, directionFromTopToBottom.scaled(halfHeight)).normalized();
            bottomPosition = Vector3.add(bottomPosition, center);
            float vHeight = halfHeight * 2;
            uvCoordinate = new UvCoordinate(uStep * edgeIndex, vHeight);

            vertex = Vertex.builder().setPosition(bottomPosition).setNormal(normal).setUvCoordinate(uvCoordinate).build();
            bottomVertices.add(vertex);

            theta += thetaIncrement;
            cosTheta = (float) Math.cos(theta);
            sinTheta = (float) Math.sin(theta);
        }
        vertices.addAll(bottomVertices);
    }

    private static void updateConnectingPoints(List<Vertex> vertices, List<Vector3> points, List<Quaternion> rotations, float radius)
    {
        // Loop over each segment of cylinder, connecting the ends of this segment to start of the next.
        int currentSegmentVertexIndex = NUMBER_OF_SIDES + 1;
        int nextSegmentVertexIndex = currentSegmentVertexIndex + NUMBER_OF_SIDES + 1;

        for (int segmentIndex = 0; segmentIndex < points.size() - 2; segmentIndex++)
        {
            Vector3 influencePoint = points.get(segmentIndex + 1);

            Quaternion averagedRotation = lerp(rotations.get(segmentIndex), rotations.get(segmentIndex + 1), .5f);
            Vector3 rightDirection = Quaternion.rotateVector(averagedRotation, Vector3.right()).normalized();
            Vector3 upDirection = Quaternion.rotateVector(averagedRotation, Vector3.up()).normalized();

            for (int edgeIndex = 0; edgeIndex <= NUMBER_OF_SIDES; edgeIndex++)
            {
                // Connect bottom vertex of current edge to the top vertex of the edge on next segment.
                float theta = (float) (2 * Math.PI) * edgeIndex / NUMBER_OF_SIDES;
                float cosTheta = (float) Math.cos(theta);
                float sinTheta = (float) Math.sin(theta);

                // Create new position
                Vector3 position = Vector3.add(rightDirection.scaled(radius * cosTheta), upDirection.scaled(radius * sinTheta));
                Vector3 normal = position.normalized();
                position.set(Vector3.add(position, influencePoint));

                // Update position, UV, and normals of connecting vertices
                int previousSegmentVertexIndex = currentSegmentVertexIndex - NUMBER_OF_SIDES - 1;
                Vertex updatedVertex = Vertex.builder().setPosition(position).setNormal(normal).setUvCoordinate(new UvCoordinate(vertices.get(currentSegmentVertexIndex).getUvCoordinate().x, (Vector3.subtract(position, vertices.get(previousSegmentVertexIndex).getPosition()).length() + vertices.get(previousSegmentVertexIndex).getUvCoordinate().y))).build();

                vertices.set(currentSegmentVertexIndex, updatedVertex);
                vertices.remove(nextSegmentVertexIndex);
                currentSegmentVertexIndex++;
            }
            currentSegmentVertexIndex = nextSegmentVertexIndex;
            nextSegmentVertexIndex += NUMBER_OF_SIDES + 1;
        }
    }

    private static void updateEndPointUV(List<Vertex> vertices)
    {
        // Update UV coordinates of ending vertices
        for (int edgeIndex = 0; edgeIndex <= NUMBER_OF_SIDES; edgeIndex++)
        {
            int vertexIndex = vertices.size() - edgeIndex - 1;
            Vertex currentVertex = vertices.get(vertexIndex);
            currentVertex.setUvCoordinate(new UvCoordinate(currentVertex.getUvCoordinate().x, (Vector3.subtract(vertices.get(vertexIndex).getPosition(), vertices.get(vertexIndex - NUMBER_OF_SIDES - 1).getPosition()).length() + vertices.get(vertexIndex - NUMBER_OF_SIDES - 1).getUvCoordinate().y)));
        }
    }

    private static void generateTriangleIndices(List<Integer> triangleIndices, int numberOfPoints)
    {
        // Create triangles along the sides of cylinder part
        for (int segment = 0; segment < numberOfPoints - 1; segment++)
        {
            int segmentVertexIndex = segment * (NUMBER_OF_SIDES + 1);
            for (int side = 0; side < NUMBER_OF_SIDES; side++)
            {
                int topLeft = side + segmentVertexIndex;
                int topRight = side + segmentVertexIndex + 1;
                int bottomLeft = side + NUMBER_OF_SIDES + segmentVertexIndex + 1;
                int bottomRight = side + NUMBER_OF_SIDES + segmentVertexIndex + 2;

                // First triangle of side.
                triangleIndices.add(topLeft);
                triangleIndices.add(bottomRight);
                triangleIndices.add(topRight);

                // Second triangle of side.
                triangleIndices.add(topLeft);
                triangleIndices.add(bottomLeft);
                triangleIndices.add(bottomRight);
            }
        }
    }

    private static void makeDisk(List<Vertex> vertices, List<Integer> triangleIndices, List<Vector3> points, int centerPointIndex, Direction direction)
    {

        Vector3 centerPoint = points.get(centerPointIndex);
        Vector3 nextPoint = points.get(centerPointIndex + (direction == Direction.UP ? 1 : -1));
        Vector3 normal = Vector3.subtract(centerPoint, nextPoint).normalized();
        Vertex center = Vertex.builder().setPosition(centerPoint).setNormal(normal).setUvCoordinate(new UvCoordinate(.5f, .5f)).build();
        int centerIndex = vertices.size();
        vertices.add(center);

        int vertexPosition = centerPointIndex * (NUMBER_OF_SIDES + 1);
        for (int edge = 0; edge <= NUMBER_OF_SIDES; edge++)
        {
            Vertex edgeVertex = vertices.get(vertexPosition + edge);
            float theta = (float) (2 * Math.PI * edge / NUMBER_OF_SIDES);
            UvCoordinate uvCoordinate = new UvCoordinate((float) (Math.cos(theta) + 1f) / 2, (float) (Math.sin(theta) + 1f) / 2);
            Vertex topVertex = Vertex.builder().setPosition(edgeVertex.getPosition()).setNormal(normal).setUvCoordinate(uvCoordinate).build();
            vertices.add(topVertex);

            if (edge != NUMBER_OF_SIDES)
            {
                // Add disk triangle, using direction to check which side the triangles should face
                if (direction == Direction.UP)
                {
                    triangleIndices.add(centerIndex);
                    triangleIndices.add(centerIndex + edge + 1);
                    triangleIndices.add(centerIndex + edge + 2);
                }
                else
                {
                    triangleIndices.add(centerIndex);
                    triangleIndices.add(centerIndex + edge + 2);
                    triangleIndices.add(centerIndex + edge + 1);
                }
            }
        }
    }

    /**
     * The dot product of two Quaternions.
     */
    private static float dot(Quaternion lhs, Quaternion rhs)
    {
        return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z + lhs.w * rhs.w;
    }

    private static Quaternion negated(Quaternion quat)
    {
        return new Quaternion(-quat.x, -quat.y, -quat.z, -quat.w);
    }

    private static Quaternion lerp(Quaternion a, Quaternion b, float ratio)
    {
        return new Quaternion(MathHelper.lerp(a.x, b.x, ratio), MathHelper.lerp(a.y, b.y, ratio), MathHelper.lerp(a.z, b.z, ratio), MathHelper.lerp(a.w, b.w, ratio));
    }
}
// LineSimplifier.java

import com.google.ar.sceneform.math.Vector3;

import java.util.ArrayList;
import java.util.List;

/**
 * Smooths a given list of points
 */
public class LineSimplifier
{
    private static final String TAG = LineSimplifier.class.getSimpleName();
    private static final float MAXIMUM_SMOOTHING_DISTANCE = 0.005f;
    private static final int POINT_SMOOTHING_INTERVAL = 10;

    private final ArrayList<Vector3> points = new ArrayList<>();
    private final ArrayList<Vector3> smoothedPoints = new ArrayList<>();

    public LineSimplifier()
    {
    }

    public void add(Vector3 point)
    {
        points.add(point);
        if (points.size() - smoothedPoints.size() > POINT_SMOOTHING_INTERVAL)
        {
            smoothPoints();
        }
    }

    private void smoothPoints()
    {
        List<Vector3> pointsToSmooth = points.subList(points.size() - POINT_SMOOTHING_INTERVAL - 1, points.size() - 1);
        ArrayList<Vector3> newlySmoothedPoints = smoothPoints(pointsToSmooth);
        points.subList(points.size() - POINT_SMOOTHING_INTERVAL - 1, points.size() - 1).clear();
        points.addAll(points.size() - 1, newlySmoothedPoints);
        smoothedPoints.addAll(newlySmoothedPoints);
    }

    // Line smoothing using the Ramer-Douglas-Peucker algorithm, modified for 3D smoothing.
    private ArrayList<Vector3> smoothPoints(List<Vector3> pointsToSmooth)
    {
        ArrayList<Vector3> results = new ArrayList<>();
        float maxDistance = 0.0f;
        int index = 0;
        float distance;
        int endIndex = pointsToSmooth.size() - 1;
        for (int i = 0; i < endIndex - 1; i++)
        {
            distance = getPerpendicularDistance(points.get(0), points.get(endIndex), points.get(i));
            if (distance > maxDistance)
            {
                index = i;
                maxDistance = distance;
            }
        }
        if (maxDistance > MAXIMUM_SMOOTHING_DISTANCE)
        {
            ArrayList<Vector3> result1 = smoothPoints(pointsToSmooth.subList(0, index));
            ArrayList<Vector3> result2 = smoothPoints(pointsToSmooth.subList(index + 1, endIndex));
            results.addAll(result1);
            results.addAll(result2);
        }
        else
        {
            results.addAll(pointsToSmooth);
        }
        return results;
    }

    private float getPerpendicularDistance(Vector3 start, Vector3 end, Vector3 point)
    {
        Vector3 crossProduct = Vector3.cross(Vector3.subtract(point, start), Vector3.subtract(point, end));
        float result = crossProduct.length() / Vector3.subtract(end, start).length();
        return result;
    }

    public List<Vector3> getPoints()
    {
        return points;
    }
}
// Stroke.java

import com.google.ar.sceneform.AnchorNode;
import com.google.ar.sceneform.Node;
import com.google.ar.sceneform.math.Vector3;
import com.google.ar.sceneform.rendering.Material;
import com.google.ar.sceneform.rendering.ModelRenderable;
import com.google.ar.sceneform.rendering.RenderableDefinition;

import java.util.List;

/**
 * Collects points to be drawn
 */
public class Stroke
{
    private static final float CYLINDER_RADIUS = 0.005f;
    private static final float MINIMUM_DISTANCE_BETWEEN_POINTS = 0.005f;
    private static final String TAG = Stroke.class.getSimpleName();


    private final Node node = new Node();
    private final Material material;
    private final LineSimplifier lineSimplifier = new LineSimplifier();

    private AnchorNode anchorNode;
    private ModelRenderable shape;

    public Stroke(AnchorNode anchorNode, Material material)
    {
        this.material = material;
        this.anchorNode = anchorNode;
        node.setParent(anchorNode);
    }

    public void add(Vector3 pointInWorld)
    {
        Vector3 pointInLocal = anchorNode.worldToLocalPoint(pointInWorld);
        List<Vector3> points = lineSimplifier.getPoints();
        if (getNumOfPoints() < 1)
        {
            lineSimplifier.add(pointInLocal);
            return;
        }

        Vector3 prev = points.get(points.size() - 1);
        Vector3 diff = Vector3.subtract(prev, pointInLocal);
        if (diff.length() < MINIMUM_DISTANCE_BETWEEN_POINTS)
        {
            return;
        }

        lineSimplifier.add(pointInLocal);

        RenderableDefinition renderableDefinition = ExtrudedCylinder.makeExtrudedCylinder(CYLINDER_RADIUS, points, material);
        if (shape == null)
        {
            shape = ModelRenderable.builder().setSource(renderableDefinition).build().join();
            node.setRenderable(shape);
        }
        else
        {
            shape.updateFromDefinition(renderableDefinition);
        }
    }

    public void clear()
    {
        lineSimplifier.getPoints().clear();
        node.setParent(null);
    }

    public int getNumOfPoints()
    {
        return lineSimplifier.getPoints().size();
    }

    @Override
    public String toString()
    {
        String result = "Vector3[] strokePoints = {";
        for (Vector3 vector3 : lineSimplifier.getPoints())
        {
            result += ("new Vector3(" + vector3.x + "f, " + vector3.y + "f, " + vector3.z + "f),\n ");
        }
        return result.substring(0, result.length() - 3) + "};";
    }
}
반응형
Comments