Notice
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 31 |
Tags
- rxjava hot observable
- 안드로이드 유닛 테스트
- 클래스
- 객체
- ANR이란
- 스택 자바 코드
- 서비스 쓰레드 차이
- 큐 자바 코드
- 안드로이드 유닛테스트란
- 안드로이드 유닛 테스트 예시
- ar vr 차이
- jvm이란
- 2022 플러터 안드로이드 스튜디오
- android ar 개발
- rxjava cold observable
- 안드로이드 레트로핏 crud
- 멤버변수
- 안드로이드 레트로핏 사용법
- 2022 플러터 설치
- Rxjava Observable
- 안드로이드 라이선스
- 안드로이드 라이선스 종류
- 안드로이드 os 구조
- rxjava disposable
- jvm 작동 원리
- 플러터 설치 2022
- android retrofit login
- 자바 다형성
- 서비스 vs 쓰레드
- 스택 큐 차이
Archives
- Today
- Total
나만을 위한 블로그
[Android] 화면에 생성한 AR core 객체와 ArFragment 화면을 함께 캡쳐하는 법 본문
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) + "};";
}
}
반응형
'Android' 카테고리의 다른 글
Comments