1 /* 2 * Copyright 2018 Google LLC All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.skar.examples.helloskar.app; 18 19 import android.animation.PropertyValuesHolder; 20 import android.animation.ValueAnimator; 21 import android.graphics.Canvas; 22 import android.graphics.Color; 23 import android.graphics.PorterDuff; 24 import android.opengl.GLES20; 25 import android.opengl.GLSurfaceView; 26 import android.opengl.Matrix; 27 import android.os.Bundle; 28 import android.support.annotation.NonNull; 29 30 import android.support.design.widget.BottomNavigationView; 31 import android.support.v7.app.AppCompatActivity; 32 import android.support.v7.widget.Toolbar; 33 import android.util.Log; 34 import android.view.Menu; 35 import android.view.MenuInflater; 36 import android.view.MenuItem; 37 import android.view.MotionEvent; 38 import android.view.SurfaceHolder; 39 import android.view.View; 40 import android.view.WindowManager; 41 import android.widget.Toast; 42 43 import com.google.ar.core.Anchor; 44 import com.google.ar.core.ArCoreApk; 45 import com.google.ar.core.Camera; 46 import com.google.ar.core.Frame; 47 import com.google.ar.core.HitResult; 48 import com.google.ar.core.Plane; 49 import com.google.ar.core.Point; 50 import com.google.ar.core.Point.OrientationMode; 51 import com.google.ar.core.PointCloud; 52 import com.google.ar.core.Session; 53 import com.google.ar.core.Trackable; 54 import com.google.ar.core.TrackingState; 55 56 import com.google.ar.core.examples.java.helloskar.R; 57 import com.google.skar.examples.helloskar.helpers.CameraPermissionHelper; 58 import com.google.skar.examples.helloskar.helpers.DisplayRotationHelper; 59 import com.google.skar.examples.helloskar.helpers.FullScreenHelper; 60 import com.google.skar.examples.helloskar.helpers.GestureHelper; 61 import com.google.skar.examples.helloskar.helpers.SnackbarHelper; 62 63 import com.google.skar.examples.helloskar.rendering.BackgroundRenderer; 64 import com.google.skar.examples.helloskar.rendering.DrawManager; 65 66 import com.google.ar.core.exceptions.CameraNotAvailableException; 67 import com.google.ar.core.exceptions.UnavailableApkTooOldException; 68 import com.google.ar.core.exceptions.UnavailableArcoreNotInstalledException; 69 import com.google.ar.core.exceptions.UnavailableDeviceNotCompatibleException; 70 import com.google.ar.core.exceptions.UnavailableSdkTooOldException; 71 import com.google.ar.core.exceptions.UnavailableUserDeclinedInstallationException; 72 73 import java.io.IOException; 74 import java.util.ArrayList; 75 76 import javax.microedition.khronos.egl.EGLConfig; 77 import javax.microedition.khronos.opengles.GL10; 78 79 /** 80 * This is a simple example that shows how to create an augmented reality (AR) application using the 81 * ARCore API. The application will display any detected planes and will allow the user to tap on a 82 * plane to place 2D objects 83 */ 84 85 public class HelloCanvasAR extends AppCompatActivity implements GLSurfaceView.Renderer { 86 private static final String TAG = HelloCanvasAR.class.getSimpleName(); 87 private final int MAX_NUMBER_DRAWABLES = 50; // Arbitrary limit to the # of anchors to store 88 89 // Simple SurfaceView used to draw 2D objects on top of the GLSurfaceView 90 private CanvasARSurfaceView arSurfaceView; 91 private SurfaceHolder holder; 92 93 // GLSurfaceView used to draw 3D objects & camera input 94 private GLSurfaceView glSurfaceView; 95 96 // ARSession 97 private Session session; 98 99 // OpenGL background renderer 100 private final BackgroundRenderer backgroundRenderer = new BackgroundRenderer(); 101 102 // 2D Renderer 103 private DrawManager drawManager = new DrawManager(); 104 105 private boolean installRequested; 106 private final SnackbarHelper messageSnackbarHelper = new SnackbarHelper(); 107 private DisplayRotationHelper displayRotationHelper; 108 private GestureHelper tapHelper; 109 110 // Temporary matrix allocated here to reduce number of allocations for each frame. 111 private final float[] anchorMatrix = new float[16]; 112 113 // Anchors created from taps used for object placing. 114 private final ArrayList<Anchor> anchors = new ArrayList<>(); 115 116 // Animation fields 117 float radius; 118 String PROPERTY_RADIUS = "radius"; 119 ValueAnimator animator; 120 121 @Override onCreate(Bundle savedInstanceState)122 protected void onCreate(Bundle savedInstanceState) { 123 super.onCreate(savedInstanceState); 124 setContentView(R.layout.activity_main); 125 126 // Menu tool bar set up 127 Toolbar toolbar = findViewById(R.id.main_toolbar); 128 setSupportActionBar(toolbar); 129 130 // Hide notifications bar 131 getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 132 WindowManager.LayoutParams.FLAG_FULLSCREEN); 133 134 // Canvas Surface View set up 135 arSurfaceView = findViewById(R.id.canvas_surfaceview); 136 glSurfaceView = findViewById(R.id.gl_surfaceview); 137 arSurfaceView.bringToFront(); 138 arSurfaceView.setLayerType(View.LAYER_TYPE_HARDWARE, null); 139 holder = arSurfaceView.getHolder(); 140 141 // Set up tap listener. 142 tapHelper = new GestureHelper(this); 143 144 glSurfaceView.setOnTouchListener(tapHelper); 145 146 // Set up renderer. 147 glSurfaceView.setPreserveEGLContextOnPause(true); 148 glSurfaceView.setEGLContextClientVersion(2); 149 glSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0); // Alpha used for plane blending. 150 glSurfaceView.setRenderer(this); 151 glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); 152 displayRotationHelper = new DisplayRotationHelper(this); 153 installRequested = false; 154 155 // Set up finger painting palette bar 156 BottomNavigationView bottomNav = findViewById(R.id.palette); 157 bottomNav.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener() { 158 @Override 159 public boolean onNavigationItemSelected(@NonNull MenuItem item) { 160 switch (item.getItemId()) { 161 case R.id.palette_green: 162 drawManager.fingerPainting.setColor(Color.GREEN); 163 return true; 164 case R.id.palette_red: 165 drawManager.fingerPainting.setColor(Color.RED); 166 return true; 167 case R.id.palette_reset: 168 drawManager.fingerPainting.reset(); 169 return true; 170 default: 171 return true; 172 } 173 } 174 }); 175 176 // Value Animator set up 177 PropertyValuesHolder propertyRadius = PropertyValuesHolder.ofFloat(PROPERTY_RADIUS, 0, 0.5f); 178 animator = new ValueAnimator(); 179 animator.setValues(propertyRadius); 180 animator.setDuration(1000); 181 animator.setRepeatCount(ValueAnimator.INFINITE); 182 animator.setRepeatMode(ValueAnimator.REVERSE); 183 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 184 @Override 185 public void onAnimationUpdate(ValueAnimator animation) { 186 radius = (float) animation.getAnimatedValue(PROPERTY_RADIUS); 187 } 188 }); 189 animator.start(); 190 } 191 192 @Override onResume()193 protected void onResume() { 194 super.onResume(); 195 196 if (session == null) { 197 Exception exception = null; 198 String message = null; 199 try { 200 switch (ArCoreApk.getInstance().requestInstall(this, !installRequested)) { 201 case INSTALL_REQUESTED: 202 installRequested = true; 203 return; 204 case INSTALLED: 205 break; 206 } 207 208 // ARCore requires camera permissions to operate. If we did not yet obtain runtime 209 // permission on Android M and above, now is a good time to ask the user for it. 210 if (!CameraPermissionHelper.hasCameraPermission(this)) { 211 CameraPermissionHelper.requestCameraPermission(this); 212 return; 213 } 214 215 // Create the session. 216 session = new Session(/* context= */ this); 217 218 } catch (UnavailableArcoreNotInstalledException 219 | UnavailableUserDeclinedInstallationException e) { 220 message = "Please install ARCore"; 221 exception = e; 222 } catch (UnavailableApkTooOldException e) { 223 message = "Please update ARCore"; 224 exception = e; 225 } catch (UnavailableSdkTooOldException e) { 226 message = "Please update this app"; 227 exception = e; 228 } catch (UnavailableDeviceNotCompatibleException e) { 229 message = "This device does not support AR"; 230 exception = e; 231 } catch (Exception e) { 232 message = "Failed to create AR session"; 233 exception = e; 234 } 235 236 if (message != null) { 237 messageSnackbarHelper.showError(this, message); 238 Log.e(TAG, "Exception creating session", exception); 239 return; 240 } 241 } 242 243 // Note that order matters - see the note in onPause(), the reverse applies here. 244 try { 245 session.resume(); 246 } catch (CameraNotAvailableException e) { 247 messageSnackbarHelper.showError(this, "Camera not available. Please restart the app."); 248 session = null; 249 return; 250 } 251 252 glSurfaceView.onResume(); 253 displayRotationHelper.onResume(); 254 messageSnackbarHelper.showMessage(this, "Searching for surfaces..."); 255 } 256 257 @Override onPause()258 public void onPause() { 259 super.onPause(); 260 if (session != null) { 261 displayRotationHelper.onPause(); 262 glSurfaceView.onPause(); 263 session.pause(); 264 } 265 } 266 267 @Override onRequestPermissionsResult(int requestCode, String[] permissions, int[] results)268 public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) { 269 if (!CameraPermissionHelper.hasCameraPermission(this)) { 270 Toast.makeText(this, "Camera permission is needed to run this application", Toast.LENGTH_LONG) 271 .show(); 272 if (!CameraPermissionHelper.shouldShowRequestPermissionRationale(this)) { 273 // Permission denied with checking "Do not ask again". 274 CameraPermissionHelper.launchPermissionSettings(this); 275 } 276 finish(); 277 } 278 } 279 280 @Override onWindowFocusChanged(boolean hasFocus)281 public void onWindowFocusChanged(boolean hasFocus) { 282 super.onWindowFocusChanged(hasFocus); 283 FullScreenHelper.setFullScreenOnWindowFocusChanged(this, hasFocus); 284 } 285 286 /************** GLSurfaceView Methods ****************************/ 287 @Override onSurfaceCreated(GL10 gl, EGLConfig config)288 public void onSurfaceCreated(GL10 gl, EGLConfig config) { 289 GLES20.glClearColor(0.1f, 0.1f, 0.1f, 1.0f); 290 291 // Prepare the rendering objects. This involves reading shaders, so may throw an IOException. 292 try { 293 // Create the texture and pass it to ARCore session to be filled during update(). 294 backgroundRenderer.createOnGlThread( this); 295 drawManager.initializePlaneShader(this, "models/trigrid.png"); 296 } catch (IOException e) { 297 Log.e(TAG, "Failed to read an asset file", e); 298 } 299 } 300 301 @Override onSurfaceChanged(GL10 gl, int width, int height)302 public void onSurfaceChanged(GL10 gl, int width, int height) { 303 displayRotationHelper.onSurfaceChanged(width, height); 304 GLES20.glViewport(0, 0, width, height); 305 306 // Send viewport information to 2D AR drawing manager 307 drawManager.updateViewport(width, height); 308 } 309 310 @Override onDrawFrame(GL10 gl)311 public void onDrawFrame(GL10 gl) { 312 Canvas canvas = null; 313 314 // Clear screen to notify driver it should not load any pixels from previous frame. 315 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); 316 317 if (session == null) { 318 return; 319 } 320 321 // Notify ARCore session that the view size changed so that the perspective matrix and 322 // the video background can be properly adjusted. 323 displayRotationHelper.updateSessionIfNeeded(session); 324 325 try { 326 session.setCameraTextureName(backgroundRenderer.getTextureId()); 327 Frame frame = session.update(); 328 Camera camera = frame.getCamera(); 329 330 // Query information from single tap gestures to get anchors 331 handleSingleTaps(frame, camera); 332 333 // Draw background with OpenGL. 334 // TODO: possibly find a way to extract texture and draw on Canvas 335 backgroundRenderer.draw(frame); 336 337 // If not tracking, don't draw objects 338 if (camera.getTrackingState() == TrackingState.PAUSED) { 339 return; 340 } 341 342 // Get projection matrix. 343 float[] projMatrix = new float[16]; 344 camera.getProjectionMatrix(projMatrix, 0, 0.1f, 100.0f); 345 drawManager.updateProjectionMatrix(projMatrix); 346 347 // Get camera matrix and draw. 348 float[] viewMatrix = new float[16]; 349 camera.getViewMatrix(viewMatrix, 0); 350 drawManager.updateViewMatrix(viewMatrix); 351 352 final float[] colorCorrectionRgba = new float[4]; 353 frame.getLightEstimate().getColorCorrection(colorCorrectionRgba, 0); 354 drawManager.updateLightColorFilter(colorCorrectionRgba); 355 356 // Query information from scrolling gestures to build finger paintings 357 handleHoldTaps(frame, camera); 358 359 // Drawing on Canvas (SurfaceView) 360 if (arSurfaceView.isRunning()) { 361 // Lock canvas 362 canvas = holder.lockHardwareCanvas(); 363 canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); 364 365 // Draw point cloud 366 PointCloud pointCloud = frame.acquirePointCloud(); 367 drawPointCloud(canvas, pointCloud); 368 pointCloud.release(); 369 370 // Draw planes 371 // Check if we detected at least one plane. If so, hide the loading message. 372 if (messageSnackbarHelper.isShowing()) { 373 for (Plane plane : session.getAllTrackables(Plane.class)) { 374 if (plane.getType() == com.google.ar.core.Plane.Type.HORIZONTAL_UPWARD_FACING 375 && plane.getTrackingState() == TrackingState.TRACKING) { 376 messageSnackbarHelper.hide(this); 377 break; 378 } 379 } 380 } 381 382 // Draw planes 383 drawPlanes(canvas, camera); 384 385 // Draw models 386 drawModels(canvas); 387 388 // Draw finger painting 389 drawFingerPainting(canvas); 390 391 // Unlock canvas 392 holder.unlockCanvasAndPost(canvas); 393 } 394 } catch (Throwable t) { 395 // Avoid crashing the application due to unhandled exceptions. 396 if (canvas != null) { 397 holder.unlockCanvasAndPost(canvas); 398 } 399 Log.e(TAG, "Exception on the OpenGL thread", t); 400 } 401 } 402 403 /**************************** Gesture helpers ******************************/ 404 /** 405 * Given a Frame and a Camera, perform hit tests on stored UI touch events. If a hit test is 406 * successful, construct an Anchor at the hit position and add it to the set of anchors. 407 * @param frame Frame of this update() call 408 * @param camera Camera of this update() call 409 */ handleSingleTaps(Frame frame, Camera camera)410 private void handleSingleTaps(Frame frame, Camera camera) { 411 MotionEvent tap = tapHelper.poll(); 412 if (tap != null && camera.getTrackingState() == TrackingState.TRACKING) { 413 for (HitResult hit : frame.hitTest(tap)) { 414 // Check if any plane was hit, and if it was hit inside the plane polygon 415 Trackable trackable = hit.getTrackable(); 416 // Creates an anchor if a plane or an oriented point was hit. 417 if ((trackable instanceof Plane 418 && ((Plane) trackable).isPoseInPolygon(hit.getHitPose()) 419 && (DrawManager.calculateDistanceToPlane(hit.getHitPose(), camera.getPose()) 420 > 0)) 421 || (trackable instanceof Point 422 && ((Point) trackable).getOrientationMode() 423 == OrientationMode.ESTIMATED_SURFACE_NORMAL)) { 424 if (anchors.size() >= MAX_NUMBER_DRAWABLES) { 425 anchors.get(0).detach(); 426 anchors.remove(0); 427 } 428 anchors.add(hit.createAnchor()); 429 break; 430 } 431 } 432 } 433 } 434 435 /** 436 * Given a Frame and a Camera, perform hit tests on stored UI touch events. If a hit test is 437 * successful, construct an Anchor at the hit position and add it to the set of anchors. 438 * @param frame Frame of this update() call 439 * @param camera Camera of this update() call 440 */ handleHoldTaps(Frame frame, Camera camera)441 private void handleHoldTaps(Frame frame, Camera camera) { 442 // Building finger painting 443 GestureHelper.ScrollEvent holdTap = tapHelper.holdPoll(); 444 if (holdTap != null && camera.getTrackingState() == TrackingState.TRACKING) { 445 for (HitResult hit : frame.hitTest(holdTap.event)) { 446 // Check if any plane was hit, and if it was hit inside the plane polygon 447 Trackable trackable = hit.getTrackable(); 448 // Creates an anchor if a plane or an oriented point was hit. 449 if ((trackable instanceof Plane 450 && ((Plane) trackable).isPoseInPolygon(hit.getHitPose()) 451 && (DrawManager.calculateDistanceToPlane(hit.getHitPose(), camera.getPose()) 452 > 0)) 453 || (trackable instanceof Point 454 && ((Point) trackable).getOrientationMode() 455 == OrientationMode.ESTIMATED_SURFACE_NORMAL)) { 456 457 // Get hit point transform, apply it to the origin --> point is not in hit 458 // location on the plane 459 float[] modelMatrix = new float[16]; 460 hit.getHitPose().toMatrix(modelMatrix, 0); 461 float[] hitLocation = {0, 0, 0, 1}; 462 Matrix.multiplyMV(hitLocation, 0, modelMatrix, 0, 463 hitLocation, 0); 464 465 if (! drawManager.fingerPainting.computeNextPoint(hitLocation, modelMatrix, holdTap)) { 466 // Try to add the next point to the finger painting. If return value 467 // is false, then keep looping 468 continue; 469 } 470 471 break; 472 } 473 } 474 } 475 } 476 477 /**************************** Drawing helpers ******************************/ 478 // Helper drawing functions that invoke drawManager drawPlanes(Canvas canvas, Camera camera)479 private void drawPlanes(Canvas canvas, Camera camera) { 480 drawManager.drawPlanes(canvas, camera.getPose(), session.getAllTrackables(Plane.class)); 481 } 482 drawPointCloud(Canvas canvas, PointCloud cloud)483 private void drawPointCloud(Canvas canvas, PointCloud cloud) { 484 drawManager.drawPointCloud(canvas, cloud); 485 } 486 drawModels(Canvas canvas)487 private void drawModels(Canvas canvas) { 488 for (Anchor anchor : anchors) { 489 if (anchor.getTrackingState() != TrackingState.TRACKING) { 490 continue; 491 } 492 // Get the current pose of an Anchor in world space 493 anchor.getPose().toMatrix(anchorMatrix, 0); 494 drawManager.modelMatrices.add(0, anchorMatrix); 495 496 switch (drawManager.currentDrawabletype) { 497 case circle: 498 drawManager.drawCircle(canvas); 499 break; 500 case rect: 501 drawManager.drawRect(canvas); 502 break; 503 case animation: 504 drawManager.drawAnimatedRoundRect(canvas, radius); 505 break; 506 case text: 507 drawManager.drawText(canvas, "Android"); 508 break; 509 default: 510 drawManager.drawCircle(canvas); 511 break; 512 } 513 } 514 } 515 drawFingerPainting(Canvas canvas)516 private void drawFingerPainting(Canvas canvas) { 517 drawManager.fingerPainting.setSmoothness(drawManager.drawSmoothPainting); 518 drawManager.fingerPainting.buildPath(); 519 drawManager.drawFingerPainting(canvas); 520 } 521 522 /**************************** UI helpers ******************************/ 523 // Tool bar functions onCreateOptionsMenu(Menu menu)524 public boolean onCreateOptionsMenu(Menu menu) { 525 MenuInflater inflater = getMenuInflater(); 526 inflater.inflate(R.menu.main_menu, menu); 527 528 menu.setGroupCheckable(R.id.menu_drawables, true, true); 529 return true; 530 } 531 onOptionsItemSelected(MenuItem item)532 public boolean onOptionsItemSelected(MenuItem item) { 533 item.setChecked(!item.isChecked()); 534 switch (item.getItemId()) { 535 case R.id.smooth_paint: 536 drawManager.drawSmoothPainting = item.isChecked(); 537 return true; 538 case R.id.draw_circle: 539 drawManager.currentDrawabletype = DrawManager.DrawingType.circle; 540 return true; 541 case R.id.draw_rect: 542 drawManager.currentDrawabletype = DrawManager.DrawingType.rect; 543 return true; 544 case R.id.draw_text: 545 drawManager.currentDrawabletype = DrawManager.DrawingType.text; 546 return true; 547 case R.id.draw_animation: 548 drawManager.currentDrawabletype = DrawManager.DrawingType.animation; 549 return true; 550 default: 551 return super.onOptionsItemSelected(item); 552 } 553 } 554 } 555