listener) {
+ this.listener = listener;
+ }
+
+
+ private final double distance(Point p1, Point p2) {
+ double xDiff = (double)(p1.x - p2.x);
+ double yDiff = (double)(p1.y - p2.y);
+ return Math.sqrt(xDiff * xDiff + yDiff * yDiff);
+ }
+
+ public MapTouchWrapper(Context context) {
+ super(context);
+ setup(context);
+ }
+
+ public MapTouchWrapper(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setup(context);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (listener == null) {
+ return false;
+ }
+ int x = (int) ev.getX();
+ int y = (int) ev.getY();
+ Point tapped = new Point(x, y);
+
+ switch (ev.getAction()){
+ case MotionEvent.ACTION_DOWN:
+ down = tapped;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (down != null && distance(down, tapped) >= touchSlop) {
+ down = null;
+ break;
+ }
+ case MotionEvent.ACTION_UP:
+ if (down != null && distance(down, tapped) < touchSlop) {
+ this.listener.apply(tapped);
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/app/src/main/java/upb/airdocs/arhelpers/MapView.java b/app/src/main/java/upb/airdocs/arhelpers/MapView.java
new file mode 100644
index 0000000..20f15c2
--- /dev/null
+++ b/app/src/main/java/upb/airdocs/arhelpers/MapView.java
@@ -0,0 +1,109 @@
+package upb.airdocs.arhelpers;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LightingColorFilter;
+import android.graphics.Paint;
+
+import androidx.annotation.NonNull;
+
+import com.google.android.gms.maps.CameraUpdateFactory;
+import com.google.android.gms.maps.GoogleMap;
+import com.google.android.gms.maps.OnMapReadyCallback;
+import com.google.android.gms.maps.internal.IGoogleMapDelegate;
+import com.google.android.gms.maps.model.BitmapDescriptorFactory;
+import com.google.android.gms.maps.model.CameraPosition;
+import com.google.android.gms.maps.model.LatLng;
+import com.google.android.gms.maps.model.Marker;
+import com.google.android.gms.maps.model.MarkerOptions;
+
+import upb.airdocs.ARActivity;
+import upb.airdocs.R;
+
+public class MapView {
+ private int CAMERA_MARKER_COLOR = Color.argb(255, 0, 255, 0);
+ private int EARTH_MARKER_COLOR = Color.argb(255, 125, 125, 125);
+
+ boolean setInitialCameraPosition = false;
+ Marker cameraMarker;
+ boolean cameraIdle = true;
+ ARActivity activity;
+ GoogleMap googleMap;
+ public Marker earthMarker;
+
+
+
+ MapView(ARActivity activity, GoogleMap googleMap) {
+ googleMap.getUiSettings().setMapToolbarEnabled(false);
+ googleMap.getUiSettings().setIndoorLevelPickerEnabled(false);
+ googleMap.getUiSettings().setZoomControlsEnabled(false);
+ googleMap.getUiSettings().setTiltGesturesEnabled(false);
+ googleMap.getUiSettings().setScrollGesturesEnabled(false);
+
+ googleMap.setOnMarkerClickListener(unused -> false);
+
+ // Add listeners to keep track of when the GoogleMap camera is moving.
+ googleMap.setOnCameraMoveListener(() -> cameraIdle = false); // TODO: does it work like this?
+ googleMap.setOnCameraIdleListener(() -> cameraIdle = false);
+ cameraMarker = createMarker(CAMERA_MARKER_COLOR);
+ earthMarker = createMarker(EARTH_MARKER_COLOR);
+ this.activity = activity;
+ }
+
+
+ void updateMapPosition(Double latitude, Double longitude, Double heading) {
+ LatLng position = new LatLng(latitude, longitude);
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // If the map is already in the process of a camera update, then don't move it.
+ if (!cameraIdle) {
+ return;
+ }
+ cameraMarker.setVisible(true);
+ cameraMarker.setPosition(position);
+ cameraMarker.setRotation(heading.floatValue());
+
+ CameraPosition.Builder cameraPositionBuilder;
+ if (!setInitialCameraPosition) {
+ // Set the camera position with an initial default zoom level.
+ setInitialCameraPosition = true;
+ cameraPositionBuilder = new CameraPosition.Builder().zoom(21f).target(position);
+ } else {
+ // Set the camera position and keep the same zoom level.
+ cameraPositionBuilder = new CameraPosition.Builder()
+ .zoom(googleMap.getCameraPosition().zoom)
+ .target(position);
+ }
+ googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPositionBuilder.build()));
+ }
+ });
+
+ }
+
+ /** Creates and adds a 2D anchor marker on the 2D map view. */
+ private Marker createMarker(int color) {
+ MarkerOptions markersOptions = new MarkerOptions()
+ .position(new LatLng(0.0,0.0))
+ .draggable(false)
+ .anchor(0.5f, 0.5f)
+ .flat(true)
+ .visible(false)
+ .icon(BitmapDescriptorFactory.fromBitmap(createColoredMarkerBitmap(color)));
+ return googleMap.addMarker(markersOptions);
+ }
+
+ private Bitmap createColoredMarkerBitmap(int color) {
+ BitmapFactory.Options opt = new BitmapFactory.Options();
+ opt.inMutable = true;
+ Bitmap navigationIcon = BitmapFactory.decodeResource(activity.getResources(), R.drawable.ic_navigation_white_48dp, opt);
+ Paint p = new Paint();
+ p.setColorFilter(new LightingColorFilter(color, /* add= */1));
+ Canvas canvas = new Canvas(navigationIcon);
+ canvas.drawBitmap(navigationIcon, /* left= */0f, /* top= */0f, p);
+ return navigationIcon;
+ }
+
+}
diff --git a/app/src/main/java/upb/airdocs/common/helpers/DisplayRotationHelper.java b/app/src/main/java/upb/airdocs/common/helpers/DisplayRotationHelper.java
new file mode 100644
index 0000000..958bb3c
--- /dev/null
+++ b/app/src/main/java/upb/airdocs/common/helpers/DisplayRotationHelper.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package upb.airdocs.common.helpers;
+
+import android.app.Activity;
+import android.content.Context;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.DisplayListener;
+import android.view.Display;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import com.google.ar.core.Session;
+
+/**
+ * Helper to track the display rotations. In particular, the 180 degree rotations are not notified
+ * by the onSurfaceChanged() callback, and thus they require listening to the android display
+ * events.
+ */
+public final class DisplayRotationHelper implements DisplayListener {
+ private boolean viewportChanged;
+ private int viewportWidth;
+ private int viewportHeight;
+ private final Display display;
+ private final DisplayManager displayManager;
+ private final CameraManager cameraManager;
+
+ /**
+ * Constructs the DisplayRotationHelper but does not register the listener yet.
+ *
+ * @param context the Android {@link Context}.
+ */
+ public DisplayRotationHelper(Context context) {
+ displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
+ cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
+ WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ display = windowManager.getDefaultDisplay();
+ }
+
+ /** Registers the display listener. Should be called from {@link Activity#onResume()}. */
+ public void onResume() {
+ displayManager.registerDisplayListener(this, null);
+ }
+
+ /** Unregisters the display listener. Should be called from {@link Activity#onPause()}. */
+ public void onPause() {
+ displayManager.unregisterDisplayListener(this);
+ }
+
+ /**
+ * Records a change in surface dimensions. This will be later used by {@link
+ * #updateSessionIfNeeded(Session)}. Should be called from {@link
+ * android.opengl.GLSurfaceView.Renderer
+ * #onSurfaceChanged(javax.microedition.khronos.opengles.GL10, int, int)}.
+ *
+ * @param width the updated width of the surface.
+ * @param height the updated height of the surface.
+ */
+ public void onSurfaceChanged(int width, int height) {
+ viewportWidth = width;
+ viewportHeight = height;
+ viewportChanged = true;
+ }
+
+ /**
+ * Updates the session display geometry if a change was posted either by {@link
+ * #onSurfaceChanged(int, int)} call or by {@link #onDisplayChanged(int)} system callback. This
+ * function should be called explicitly before each call to {@link Session#update()}. This
+ * function will also clear the 'pending update' (viewportChanged) flag.
+ *
+ * @param session the {@link Session} object to update if display geometry changed.
+ */
+ public void updateSessionIfNeeded(Session session) {
+ if (viewportChanged) {
+ int displayRotation = display.getRotation();
+ session.setDisplayGeometry(displayRotation, viewportWidth, viewportHeight);
+ viewportChanged = false;
+ }
+ }
+
+ /**
+ * Returns the aspect ratio of the GL surface viewport while accounting for the display rotation
+ * relative to the device camera sensor orientation.
+ */
+ public float getCameraSensorRelativeViewportAspectRatio(String cameraId) {
+ float aspectRatio;
+ int cameraSensorToDisplayRotation = getCameraSensorToDisplayRotation(cameraId);
+ switch (cameraSensorToDisplayRotation) {
+ case 90:
+ case 270:
+ aspectRatio = (float) viewportHeight / (float) viewportWidth;
+ break;
+ case 0:
+ case 180:
+ aspectRatio = (float) viewportWidth / (float) viewportHeight;
+ break;
+ default:
+ throw new RuntimeException("Unhandled rotation: " + cameraSensorToDisplayRotation);
+ }
+ return aspectRatio;
+ }
+
+ /**
+ * Returns the rotation of the back-facing camera with respect to the display. The value is one of
+ * 0, 90, 180, 270.
+ */
+ public int getCameraSensorToDisplayRotation(String cameraId) {
+ CameraCharacteristics characteristics;
+ try {
+ characteristics = cameraManager.getCameraCharacteristics(cameraId);
+ } catch (CameraAccessException e) {
+ throw new RuntimeException("Unable to determine display orientation", e);
+ }
+
+ // Camera sensor orientation.
+ int sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
+
+ // Current display orientation.
+ int displayOrientation = toDegrees(display.getRotation());
+
+ // Make sure we return 0, 90, 180, or 270 degrees.
+ return (sensorOrientation - displayOrientation + 360) % 360;
+ }
+
+ private int toDegrees(int rotation) {
+ switch (rotation) {
+ case Surface.ROTATION_0:
+ return 0;
+ case Surface.ROTATION_90:
+ return 90;
+ case Surface.ROTATION_180:
+ return 180;
+ case Surface.ROTATION_270:
+ return 270;
+ default:
+ throw new RuntimeException("Unknown rotation " + rotation);
+ }
+ }
+
+ @Override
+ public void onDisplayAdded(int displayId) {}
+
+ @Override
+ public void onDisplayRemoved(int displayId) {}
+
+ @Override
+ public void onDisplayChanged(int displayId) {
+ viewportChanged = true;
+ }
+}
diff --git a/app/src/main/java/upb/airdocs/common/helpers/FullScreenHelper.java b/app/src/main/java/upb/airdocs/common/helpers/FullScreenHelper.java
new file mode 100644
index 0000000..b9b66a9
--- /dev/null
+++ b/app/src/main/java/upb/airdocs/common/helpers/FullScreenHelper.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package upb.airdocs.common.helpers;
+
+import android.app.Activity;
+import android.view.View;
+
+/** Helper to set up the Android full screen mode. */
+public final class FullScreenHelper {
+ /**
+ * Sets the Android fullscreen flags. Expected to be called from {@link
+ * Activity#onWindowFocusChanged(boolean hasFocus)}.
+ *
+ * @param activity the Activity on which the full screen mode will be set.
+ * @param hasFocus the hasFocus flag passed from the {@link Activity#onWindowFocusChanged(boolean
+ * hasFocus)} callback.
+ */
+ public static void setFullScreenOnWindowFocusChanged(Activity activity, boolean hasFocus) {
+ if (hasFocus) {
+ // https://developer.android.com/training/system-ui/immersive.html#sticky
+ activity
+ .getWindow()
+ .getDecorView()
+ .setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
+ }
+ }
+}
diff --git a/app/src/main/java/upb/airdocs/common/helpers/SnackbarHelper.java b/app/src/main/java/upb/airdocs/common/helpers/SnackbarHelper.java
new file mode 100644
index 0000000..bc4813c
--- /dev/null
+++ b/app/src/main/java/upb/airdocs/common/helpers/SnackbarHelper.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package upb.airdocs.common.helpers;
+
+import android.app.Activity;
+import android.view.View;
+import android.widget.TextView;
+
+import com.google.android.material.snackbar.BaseTransientBottomBar;
+import com.google.android.material.snackbar.Snackbar;
+
+/**
+ * Helper to manage the sample snackbar. Hides the Android boilerplate code, and exposes simpler
+ * methods.
+ */
+public final class SnackbarHelper {
+ private static final int BACKGROUND_COLOR = 0xbf323232;
+ private Snackbar messageSnackbar;
+ private enum DismissBehavior { HIDE, SHOW, FINISH };
+ private int maxLines = 2;
+ private String lastMessage = "";
+ private View snackbarView;
+
+ public boolean isShowing() {
+ return messageSnackbar != null;
+ }
+
+ /** Shows a snackbar with a given message. */
+ public void showMessage(Activity activity, String message) {
+ if (!message.isEmpty() && (!isShowing() || !lastMessage.equals(message))) {
+ lastMessage = message;
+ show(activity, message, DismissBehavior.HIDE);
+ }
+ }
+
+ /** Shows a snackbar with a given message, and a dismiss button. */
+ public void showMessageWithDismiss(Activity activity, String message) {
+ show(activity, message, DismissBehavior.SHOW);
+ }
+
+ /**
+ * Shows a snackbar with a given error message. When dismissed, will finish the activity. Useful
+ * for notifying errors, where no further interaction with the activity is possible.
+ */
+ public void showError(Activity activity, String errorMessage) {
+ show(activity, errorMessage, DismissBehavior.FINISH);
+ }
+
+ /**
+ * Hides the currently showing snackbar, if there is one. Safe to call from any thread. Safe to
+ * call even if snackbar is not shown.
+ */
+ public void hide(Activity activity) {
+ if (!isShowing()) {
+ return;
+ }
+ lastMessage = "";
+ Snackbar messageSnackbarToHide = messageSnackbar;
+ messageSnackbar = null;
+ activity.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ messageSnackbarToHide.dismiss();
+ }
+ });
+ }
+
+ public void setMaxLines(int lines) {
+ maxLines = lines;
+ }
+
+ /**
+ * Sets the view that will be used to find a suitable parent view to hold the Snackbar view.
+ *
+ * To use the root layout ({@link android.R.id.content}), pass in {@code null}.
+ *
+ * @param snackbarView the view to pass to {@link
+ * Snackbar#make(…)} which will be used to find a
+ * suitable parent, which is a {@link androidx.coordinatorlayout.widget.CoordinatorLayout}, or
+ * the window decor's content view, whichever comes first.
+ */
+ public void setParentView(View snackbarView) {
+ this.snackbarView = snackbarView;
+ }
+
+ private void show(
+ final Activity activity, final String message, final DismissBehavior dismissBehavior) {
+ activity.runOnUiThread(
+ new Runnable() {
+ @Override
+ public void run() {
+ messageSnackbar =
+ Snackbar.make(
+ snackbarView == null
+ ? activity.findViewById(android.R.id.content)
+ : snackbarView,
+ message,
+ Snackbar.LENGTH_INDEFINITE);
+ messageSnackbar.getView().setBackgroundColor(BACKGROUND_COLOR);
+ if (dismissBehavior != DismissBehavior.HIDE) {
+ messageSnackbar.setAction(
+ "Dismiss",
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ messageSnackbar.dismiss();
+ }
+ });
+ if (dismissBehavior == DismissBehavior.FINISH) {
+ messageSnackbar.addCallback(
+ new BaseTransientBottomBar.BaseCallback() {
+ @Override
+ public void onDismissed(Snackbar transientBottomBar, int event) {
+ super.onDismissed(transientBottomBar, event);
+ activity.finish();
+ }
+ });
+ }
+ }
+ ((TextView)
+ messageSnackbar
+ .getView()
+ .findViewById(com.google.android.material.R.id.snackbar_text))
+ .setMaxLines(maxLines);
+ messageSnackbar.show();
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/upb/airdocs/common/helpers/TapHelper.java b/app/src/main/java/upb/airdocs/common/helpers/TapHelper.java
new file mode 100644
index 0000000..c5b95ef
--- /dev/null
+++ b/app/src/main/java/upb/airdocs/common/helpers/TapHelper.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package upb.airdocs.common.helpers;
+
+import android.content.Context;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+
+/**
+ * Helper to detect taps using Android GestureDetector, and pass the taps between UI thread and
+ * render thread.
+ */
+public final class TapHelper implements OnTouchListener {
+ private final GestureDetector gestureDetector;
+ private final BlockingQueue queuedSingleTaps = new ArrayBlockingQueue<>(16);
+
+ /**
+ * Creates the tap helper.
+ *
+ * @param context the application's context.
+ */
+ public TapHelper(Context context) {
+ gestureDetector =
+ new GestureDetector(
+ context,
+ new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ // Queue tap if there is space. Tap is lost if queue is full.
+ queuedSingleTaps.offer(e);
+ return true;
+ }
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Polls for a tap.
+ *
+ * @return if a tap was queued, a MotionEvent for the tap. Otherwise null if no taps are queued.
+ */
+ public MotionEvent poll() {
+ return queuedSingleTaps.poll();
+ }
+
+ @Override
+ public boolean onTouch(View view, MotionEvent motionEvent) {
+ return gestureDetector.onTouchEvent(motionEvent);
+ }
+}
diff --git a/app/src/main/java/upb/airdocs/common/helpers/TrackingStateHelper.java b/app/src/main/java/upb/airdocs/common/helpers/TrackingStateHelper.java
new file mode 100644
index 0000000..4b28569
--- /dev/null
+++ b/app/src/main/java/upb/airdocs/common/helpers/TrackingStateHelper.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package upb.airdocs.common.helpers;
+
+import android.app.Activity;
+import android.view.WindowManager;
+
+import com.google.ar.core.Camera;
+import com.google.ar.core.TrackingFailureReason;
+import com.google.ar.core.TrackingState;
+
+/** Gets human readibly tracking failure reasons and suggested actions. */
+public final class TrackingStateHelper {
+ private static final String INSUFFICIENT_FEATURES_MESSAGE =
+ "Can't find anything. Aim device at a surface with more texture or color.";
+ private static final String EXCESSIVE_MOTION_MESSAGE = "Moving too fast. Slow down.";
+ private static final String INSUFFICIENT_LIGHT_MESSAGE =
+ "Too dark. Try moving to a well-lit area.";
+ private static final String INSUFFICIENT_LIGHT_ANDROID_S_MESSAGE =
+ "Too dark. Try moving to a well-lit area."
+ + " Also, make sure the Block Camera is set to off in system settings.";
+ private static final String BAD_STATE_MESSAGE =
+ "Tracking lost due to bad internal state. Please try restarting the AR experience.";
+ private static final String CAMERA_UNAVAILABLE_MESSAGE =
+ "Another app is using the camera. Tap on this app or try closing the other one.";
+ private static final int ANDROID_S_SDK_VERSION = 31;
+
+ private final Activity activity;
+
+ private TrackingState previousTrackingState;
+
+ public TrackingStateHelper(Activity activity) {
+ this.activity = activity;
+ }
+
+ /** Keep the screen unlocked while tracking, but allow it to lock when tracking stops. */
+ public void updateKeepScreenOnFlag(TrackingState trackingState) {
+ if (trackingState == previousTrackingState) {
+ return;
+ }
+
+ previousTrackingState = trackingState;
+ switch (trackingState) {
+ case PAUSED:
+ case STOPPED:
+ activity.runOnUiThread(
+ () -> activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON));
+ break;
+ case TRACKING:
+ activity.runOnUiThread(
+ () -> activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON));
+ break;
+ }
+ }
+
+ public static String getTrackingFailureReasonString(Camera camera) {
+ TrackingFailureReason reason = camera.getTrackingFailureReason();
+ switch (reason) {
+ case NONE:
+ return "";
+ case BAD_STATE:
+ return BAD_STATE_MESSAGE;
+ case INSUFFICIENT_LIGHT:
+ if (android.os.Build.VERSION.SDK_INT < ANDROID_S_SDK_VERSION) {
+ return INSUFFICIENT_LIGHT_MESSAGE;
+ } else {
+ return INSUFFICIENT_LIGHT_ANDROID_S_MESSAGE;
+ }
+ case EXCESSIVE_MOTION:
+ return EXCESSIVE_MOTION_MESSAGE;
+ case INSUFFICIENT_FEATURES:
+ return INSUFFICIENT_FEATURES_MESSAGE;
+ case CAMERA_UNAVAILABLE:
+ return CAMERA_UNAVAILABLE_MESSAGE;
+ }
+ return "Unknown tracking failure reason: " + reason;
+ }
+}
diff --git a/app/src/main/java/upb/airdocs/common/samplerender/Framebuffer.java b/app/src/main/java/upb/airdocs/common/samplerender/Framebuffer.java
new file mode 100644
index 0000000..0232aad
--- /dev/null
+++ b/app/src/main/java/upb/airdocs/common/samplerender/Framebuffer.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package upb.airdocs.common.samplerender;
+
+import android.opengl.GLES30;
+import android.util.Log;
+
+import java.io.Closeable;
+
+/** A framebuffer associated with a texture. */
+public class Framebuffer implements Closeable {
+ private static final String TAG = Framebuffer.class.getSimpleName();
+
+ private final int[] framebufferId = {0};
+ private final Texture colorTexture;
+ private final Texture depthTexture;
+ private int width = -1;
+ private int height = -1;
+
+ /**
+ * Constructs a {@link Framebuffer} which renders internally to a texture.
+ *
+ * In order to render to the {@link Framebuffer}, use {@link SampleRender#draw(Mesh, Shader,
+ * Framebuffer)}.
+ */
+ public Framebuffer(SampleRender render, int width, int height) {
+ try {
+ colorTexture =
+ new Texture(
+ render,
+ Texture.Target.TEXTURE_2D,
+ Texture.WrapMode.CLAMP_TO_EDGE,
+ /*useMipmaps=*/ false);
+ depthTexture =
+ new Texture(
+ render,
+ Texture.Target.TEXTURE_2D,
+ Texture.WrapMode.CLAMP_TO_EDGE,
+ /*useMipmaps=*/ false);
+
+ // Set parameters of the depth texture so that it's readable by shaders.
+ GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, depthTexture.getTextureId());
+ GLError.maybeThrowGLException("Failed to bind depth texture", "glBindTexture");
+ GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_COMPARE_MODE, GLES30.GL_NONE);
+ GLError.maybeThrowGLException("Failed to set texture parameter", "glTexParameteri");
+ GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_NEAREST);
+ GLError.maybeThrowGLException("Failed to set texture parameter", "glTexParameteri");
+ GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_NEAREST);
+ GLError.maybeThrowGLException("Failed to set texture parameter", "glTexParameteri");
+
+ // Set initial dimensions.
+ resize(width, height);
+
+ // Create framebuffer object and bind to the color and depth textures.
+ GLES30.glGenFramebuffers(1, framebufferId, 0);
+ GLError.maybeThrowGLException("Framebuffer creation failed", "glGenFramebuffers");
+ GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, framebufferId[0]);
+ GLError.maybeThrowGLException("Failed to bind framebuffer", "glBindFramebuffer");
+ GLES30.glFramebufferTexture2D(
+ GLES30.GL_FRAMEBUFFER,
+ GLES30.GL_COLOR_ATTACHMENT0,
+ GLES30.GL_TEXTURE_2D,
+ colorTexture.getTextureId(),
+ /*level=*/ 0);
+ GLError.maybeThrowGLException(
+ "Failed to bind color texture to framebuffer", "glFramebufferTexture2D");
+ GLES30.glFramebufferTexture2D(
+ GLES30.GL_FRAMEBUFFER,
+ GLES30.GL_DEPTH_ATTACHMENT,
+ GLES30.GL_TEXTURE_2D,
+ depthTexture.getTextureId(),
+ /*level=*/ 0);
+ GLError.maybeThrowGLException(
+ "Failed to bind depth texture to framebuffer", "glFramebufferTexture2D");
+
+ int status = GLES30.glCheckFramebufferStatus(GLES30.GL_FRAMEBUFFER);
+ if (status != GLES30.GL_FRAMEBUFFER_COMPLETE) {
+ throw new IllegalStateException("Framebuffer construction not complete: code " + status);
+ }
+ } catch (Throwable t) {
+ close();
+ throw t;
+ }
+ }
+
+ @Override
+ public void close() {
+ if (framebufferId[0] != 0) {
+ GLES30.glDeleteFramebuffers(1, framebufferId, 0);
+ GLError.maybeLogGLError(Log.WARN, TAG, "Failed to free framebuffer", "glDeleteFramebuffers");
+ framebufferId[0] = 0;
+ }
+ colorTexture.close();
+ depthTexture.close();
+ }
+
+ /** Resizes the framebuffer to the given dimensions. */
+ public void resize(int width, int height) {
+ if (this.width == width && this.height == height) {
+ return;
+ }
+ this.width = width;
+ this.height = height;
+
+ // Color texture
+ GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, colorTexture.getTextureId());
+ GLError.maybeThrowGLException("Failed to bind color texture", "glBindTexture");
+ GLES30.glTexImage2D(
+ GLES30.GL_TEXTURE_2D,
+ /*level=*/ 0,
+ GLES30.GL_RGBA,
+ width,
+ height,
+ /*border=*/ 0,
+ GLES30.GL_RGBA,
+ GLES30.GL_UNSIGNED_BYTE,
+ /*pixels=*/ null);
+ GLError.maybeThrowGLException("Failed to specify color texture format", "glTexImage2D");
+
+ // Depth texture
+ GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, depthTexture.getTextureId());
+ GLError.maybeThrowGLException("Failed to bind depth texture", "glBindTexture");
+ GLES30.glTexImage2D(
+ GLES30.GL_TEXTURE_2D,
+ /*level=*/ 0,
+ GLES30.GL_DEPTH_COMPONENT32F,
+ width,
+ height,
+ /*border=*/ 0,
+ GLES30.GL_DEPTH_COMPONENT,
+ GLES30.GL_FLOAT,
+ /*pixels=*/ null);
+ GLError.maybeThrowGLException("Failed to specify depth texture format", "glTexImage2D");
+ }
+
+ /** Returns the color texture associated with this framebuffer. */
+ public Texture getColorTexture() {
+ return colorTexture;
+ }
+
+ /** Returns the depth texture associated with this framebuffer. */
+ public Texture getDepthTexture() {
+ return depthTexture;
+ }
+
+ /** Returns the width of the framebuffer. */
+ public int getWidth() {
+ return width;
+ }
+
+ /** Returns the height of the framebuffer. */
+ public int getHeight() {
+ return height;
+ }
+
+ /* package-private */
+ int getFramebufferId() {
+ return framebufferId[0];
+ }
+}
diff --git a/app/src/main/java/upb/airdocs/common/samplerender/GLError.java b/app/src/main/java/upb/airdocs/common/samplerender/GLError.java
new file mode 100644
index 0000000..20df50c
--- /dev/null
+++ b/app/src/main/java/upb/airdocs/common/samplerender/GLError.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package upb.airdocs.common.samplerender;
+
+import android.opengl.GLES30;
+import android.opengl.GLException;
+import android.opengl.GLU;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/** Module for handling OpenGL errors. */
+public class GLError {
+ /** Throws a {@link GLException} if a GL error occurred. */
+ public static void maybeThrowGLException(String reason, String api) {
+ List errorCodes = getGlErrors();
+ if (errorCodes != null) {
+ throw new GLException(errorCodes.get(0), formatErrorMessage(reason, api, errorCodes));
+ }
+ }
+
+ /** Logs a message with the given logcat priority if a GL error occurred. */
+ public static void maybeLogGLError(int priority, String tag, String reason, String api) {
+ List errorCodes = getGlErrors();
+ if (errorCodes != null) {
+ Log.println(priority, tag, formatErrorMessage(reason, api, errorCodes));
+ }
+ }
+
+ private static String formatErrorMessage(String reason, String api, List errorCodes) {
+ StringBuilder builder = new StringBuilder(String.format("%s: %s: ", reason, api));
+ Iterator iterator = errorCodes.iterator();
+ while (iterator.hasNext()) {
+ int errorCode = iterator.next();
+ builder.append(String.format("%s (%d)", GLU.gluErrorString(errorCode), errorCode));
+ if (iterator.hasNext()) {
+ builder.append(", ");
+ }
+ }
+ return builder.toString();
+ }
+
+ private static List getGlErrors() {
+ int errorCode = GLES30.glGetError();
+ // Shortcut for no errors
+ if (errorCode == GLES30.GL_NO_ERROR) {
+ return null;
+ }
+ List errorCodes = new ArrayList<>();
+ errorCodes.add(errorCode);
+ while (true) {
+ errorCode = GLES30.glGetError();
+ if (errorCode == GLES30.GL_NO_ERROR) {
+ break;
+ }
+ errorCodes.add(errorCode);
+ }
+ return errorCodes;
+ }
+
+ private GLError() {}
+}
diff --git a/app/src/main/java/upb/airdocs/common/samplerender/GpuBuffer.java b/app/src/main/java/upb/airdocs/common/samplerender/GpuBuffer.java
new file mode 100644
index 0000000..c706de1
--- /dev/null
+++ b/app/src/main/java/upb/airdocs/common/samplerender/GpuBuffer.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package upb.airdocs.common.samplerender;
+
+import android.opengl.GLES30;
+import android.util.Log;
+
+import java.nio.Buffer;
+
+/* package-private */
+class GpuBuffer {
+ private static final String TAG = GpuBuffer.class.getSimpleName();
+
+ // These values refer to the byte count of the corresponding Java datatypes.
+ public static final int INT_SIZE = 4;
+ public static final int FLOAT_SIZE = 4;
+
+ private final int target;
+ private final int numberOfBytesPerEntry;
+ private final int[] bufferId = {0};
+ private int size;
+ private int capacity;
+
+ public GpuBuffer(int target, int numberOfBytesPerEntry, Buffer entries) {
+ if (entries != null) {
+ if (!entries.isDirect()) {
+ throw new IllegalArgumentException("If non-null, entries buffer must be a direct buffer");
+ }
+ // Some GPU drivers will fail with out of memory errors if glBufferData or glBufferSubData is
+ // called with a size of 0, so avoid this case.
+ if (entries.limit() == 0) {
+ entries = null;
+ }
+ }
+
+ this.target = target;
+ this.numberOfBytesPerEntry = numberOfBytesPerEntry;
+ if (entries == null) {
+ this.size = 0;
+ this.capacity = 0;
+ } else {
+ this.size = entries.limit();
+ this.capacity = entries.limit();
+ }
+
+ try {
+ // Clear VAO to prevent unintended state change.
+ GLES30.glBindVertexArray(0);
+ GLError.maybeThrowGLException("Failed to unbind vertex array", "glBindVertexArray");
+
+ GLES30.glGenBuffers(1, bufferId, 0);
+ GLError.maybeThrowGLException("Failed to generate buffers", "glGenBuffers");
+
+ GLES30.glBindBuffer(target, bufferId[0]);
+ GLError.maybeThrowGLException("Failed to bind buffer object", "glBindBuffer");
+
+ if (entries != null) {
+ entries.rewind();
+ GLES30.glBufferData(
+ target, entries.limit() * numberOfBytesPerEntry, entries, GLES30.GL_DYNAMIC_DRAW);
+ }
+ GLError.maybeThrowGLException("Failed to populate buffer object", "glBufferData");
+ } catch (Throwable t) {
+ free();
+ throw t;
+ }
+ }
+
+ public void set(Buffer entries) {
+ // Some GPU drivers will fail with out of memory errors if glBufferData or glBufferSubData is
+ // called with a size of 0, so avoid this case.
+ if (entries == null || entries.limit() == 0) {
+ size = 0;
+ return;
+ }
+ if (!entries.isDirect()) {
+ throw new IllegalArgumentException("If non-null, entries buffer must be a direct buffer");
+ }
+ GLES30.glBindBuffer(target, bufferId[0]);
+ GLError.maybeThrowGLException("Failed to bind vertex buffer object", "glBindBuffer");
+
+ entries.rewind();
+
+ if (entries.limit() <= capacity) {
+ GLES30.glBufferSubData(target, 0, entries.limit() * numberOfBytesPerEntry, entries);
+ GLError.maybeThrowGLException("Failed to populate vertex buffer object", "glBufferSubData");
+ size = entries.limit();
+ } else {
+ GLES30.glBufferData(
+ target, entries.limit() * numberOfBytesPerEntry, entries, GLES30.GL_DYNAMIC_DRAW);
+ GLError.maybeThrowGLException("Failed to populate vertex buffer object", "glBufferData");
+ size = entries.limit();
+ capacity = entries.limit();
+ }
+ }
+
+ public void free() {
+ if (bufferId[0] != 0) {
+ GLES30.glDeleteBuffers(1, bufferId, 0);
+ GLError.maybeLogGLError(Log.WARN, TAG, "Failed to free buffer object", "glDeleteBuffers");
+ bufferId[0] = 0;
+ }
+ }
+
+ public int getBufferId() {
+ return bufferId[0];
+ }
+
+ public int getSize() {
+ return size;
+ }
+}
diff --git a/app/src/main/java/upb/airdocs/common/samplerender/IndexBuffer.java b/app/src/main/java/upb/airdocs/common/samplerender/IndexBuffer.java
new file mode 100644
index 0000000..2cdb002
--- /dev/null
+++ b/app/src/main/java/upb/airdocs/common/samplerender/IndexBuffer.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package upb.airdocs.common.samplerender;
+
+import android.opengl.GLES30;
+
+import java.io.Closeable;
+import java.nio.IntBuffer;
+
+/**
+ * A list of vertex indices stored GPU-side.
+ *
+ * When constructing a {@link Mesh}, an {@link IndexBuffer} may be passed to describe the
+ * ordering of vertices when drawing each primitive.
+ *
+ * @see glDrawElements
+ */
+public class IndexBuffer implements Closeable {
+ private final GpuBuffer buffer;
+
+ /**
+ * Construct an {@link IndexBuffer} populated with initial data.
+ *
+ *
The GPU buffer will be filled with the data in the direct buffer {@code entries},
+ * starting from the beginning of the buffer (not the current cursor position). The cursor will be
+ * left in an undefined position after this function returns.
+ *
+ *
The {@code entries} buffer may be null, in which case an empty buffer is constructed
+ * instead.
+ */
+ public IndexBuffer(SampleRender render, IntBuffer entries) {
+ buffer = new GpuBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, GpuBuffer.INT_SIZE, entries);
+ }
+
+ /**
+ * Populate with new data.
+ *
+ *
The entire buffer is replaced by the contents of the direct buffer {@code entries}
+ * starting from the beginning of the buffer, not the current cursor position. The cursor will be
+ * left in an undefined position after this function returns.
+ *
+ *
The GPU buffer is reallocated automatically if necessary.
+ *
+ *
The {@code entries} buffer may be null, in which case the buffer will become empty.
+ */
+ public void set(IntBuffer entries) {
+ buffer.set(entries);
+ }
+
+ @Override
+ public void close() {
+ buffer.free();
+ }
+
+ /* package-private */
+ int getBufferId() {
+ return buffer.getBufferId();
+ }
+
+ /* package-private */
+ int getSize() {
+ return buffer.getSize();
+ }
+}
diff --git a/app/src/main/java/upb/airdocs/common/samplerender/Mesh.java b/app/src/main/java/upb/airdocs/common/samplerender/Mesh.java
new file mode 100644
index 0000000..99eb6ef
--- /dev/null
+++ b/app/src/main/java/upb/airdocs/common/samplerender/Mesh.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package upb.airdocs.common.samplerender;
+
+import android.opengl.GLES30;
+import android.util.Log;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+
+import de.javagl.obj.Obj;
+import de.javagl.obj.ObjData;
+import de.javagl.obj.ObjReader;
+import de.javagl.obj.ObjUtils;
+
+/**
+ * A collection of vertices, faces, and other attributes that define how to render a 3D object.
+ *
+ *
To render the mesh, use {@link SampleRender#draw()}.
+ */
+public class Mesh implements Closeable {
+ private static final String TAG = Mesh.class.getSimpleName();
+
+ /**
+ * The kind of primitive to render.
+ *
+ *
This determines how the data in {@link VertexBuffer}s are interpreted. See here for more on how primitives
+ * behave.
+ */
+ public enum PrimitiveMode {
+ POINTS(GLES30.GL_POINTS),
+ LINE_STRIP(GLES30.GL_LINE_STRIP),
+ LINE_LOOP(GLES30.GL_LINE_LOOP),
+ LINES(GLES30.GL_LINES),
+ TRIANGLE_STRIP(GLES30.GL_TRIANGLE_STRIP),
+ TRIANGLE_FAN(GLES30.GL_TRIANGLE_FAN),
+ TRIANGLES(GLES30.GL_TRIANGLES);
+
+ /* package-private */
+ final int glesEnum;
+
+ private PrimitiveMode(int glesEnum) {
+ this.glesEnum = glesEnum;
+ }
+ }
+
+ private final int[] vertexArrayId = {0};
+ private final PrimitiveMode primitiveMode;
+ private final IndexBuffer indexBuffer;
+ private final VertexBuffer[] vertexBuffers;
+
+ /**
+ * Construct a {@link Mesh}.
+ *
+ *
The data in the given {@link IndexBuffer} and {@link VertexBuffer}s does not need to be
+ * finalized; they may be freely changed throughout the lifetime of a {@link Mesh} using their
+ * respective {@code set()} methods.
+ *
+ *
The ordering of the {@code vertexBuffers} is significant. Their array indices will
+ * correspond to their attribute locations, which must be taken into account in shader code. The
+ * layout qualifier must
+ * be used in the vertex shader code to explicitly associate attributes with these indices.
+ */
+ public Mesh(
+ SampleRender render,
+ PrimitiveMode primitiveMode,
+ IndexBuffer indexBuffer,
+ VertexBuffer[] vertexBuffers) {
+ if (vertexBuffers == null || vertexBuffers.length == 0) {
+ throw new IllegalArgumentException("Must pass at least one vertex buffer");
+ }
+
+ this.primitiveMode = primitiveMode;
+ this.indexBuffer = indexBuffer;
+ this.vertexBuffers = vertexBuffers;
+
+ try {
+ // Create vertex array
+ GLES30.glGenVertexArrays(1, vertexArrayId, 0);
+ GLError.maybeThrowGLException("Failed to generate a vertex array", "glGenVertexArrays");
+
+ // Bind vertex array
+ GLES30.glBindVertexArray(vertexArrayId[0]);
+ GLError.maybeThrowGLException("Failed to bind vertex array object", "glBindVertexArray");
+
+ if (indexBuffer != null) {
+ GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, indexBuffer.getBufferId());
+ }
+
+ for (int i = 0; i < vertexBuffers.length; ++i) {
+ // Bind each vertex buffer to vertex array
+ GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vertexBuffers[i].getBufferId());
+ GLError.maybeThrowGLException("Failed to bind vertex buffer", "glBindBuffer");
+ GLES30.glVertexAttribPointer(
+ i, vertexBuffers[i].getNumberOfEntriesPerVertex(), GLES30.GL_FLOAT, false, 0, 0);
+ GLError.maybeThrowGLException(
+ "Failed to associate vertex buffer with vertex array", "glVertexAttribPointer");
+ GLES30.glEnableVertexAttribArray(i);
+ GLError.maybeThrowGLException(
+ "Failed to enable vertex buffer", "glEnableVertexAttribArray");
+ }
+ } catch (Throwable t) {
+ close();
+ throw t;
+ }
+ }
+
+ /**
+ * Constructs a {@link Mesh} from the given Wavefront OBJ file.
+ *
+ *
The {@link Mesh} will be constructed with three attributes, indexed in the order of local
+ * coordinates (location 0, vec3), texture coordinates (location 1, vec2), and vertex normals
+ * (location 2, vec3).
+ */
+ public static Mesh createFromAsset(SampleRender render, String assetFileName) throws IOException {
+ try (InputStream inputStream = render.getAssets().open(assetFileName)) {
+ Obj obj = ObjUtils.convertToRenderable(ObjReader.read(inputStream));
+
+ // Obtain the data from the OBJ, as direct buffers:
+ IntBuffer vertexIndices = ObjData.getFaceVertexIndices(obj, /*numVerticesPerFace=*/ 3);
+ FloatBuffer localCoordinates = ObjData.getVertices(obj);
+ FloatBuffer textureCoordinates = ObjData.getTexCoords(obj, /*dimensions=*/ 2);
+ FloatBuffer normals = ObjData.getNormals(obj);
+
+ VertexBuffer[] vertexBuffers = {
+ new VertexBuffer(render, 3, localCoordinates),
+ new VertexBuffer(render, 2, textureCoordinates),
+ new VertexBuffer(render, 3, normals),
+ };
+
+ IndexBuffer indexBuffer = new IndexBuffer(render, vertexIndices);
+
+ return new Mesh(render, PrimitiveMode.TRIANGLES, indexBuffer, vertexBuffers);
+ }
+ }
+
+ @Override
+ public void close() {
+ if (vertexArrayId[0] != 0) {
+ GLES30.glDeleteVertexArrays(1, vertexArrayId, 0);
+ GLError.maybeLogGLError(
+ Log.WARN, TAG, "Failed to free vertex array object", "glDeleteVertexArrays");
+ }
+ }
+
+ /**
+ * Draws the mesh. Don't call this directly unless you are doing low level OpenGL code; instead,
+ * prefer {@link SampleRender#draw}.
+ */
+ public void lowLevelDraw() {
+ if (vertexArrayId[0] == 0) {
+ throw new IllegalStateException("Tried to draw a freed Mesh");
+ }
+
+ GLES30.glBindVertexArray(vertexArrayId[0]);
+ GLError.maybeThrowGLException("Failed to bind vertex array object", "glBindVertexArray");
+ if (indexBuffer == null) {
+ // Sanity check for debugging
+ int numberOfVertices = vertexBuffers[0].getNumberOfVertices();
+ for (int i = 1; i < vertexBuffers.length; ++i) {
+ if (vertexBuffers[i].getNumberOfVertices() != numberOfVertices) {
+ throw new IllegalStateException("Vertex buffers have mismatching numbers of vertices");
+ }
+ }
+ GLES30.glDrawArrays(primitiveMode.glesEnum, 0, numberOfVertices);
+ GLError.maybeThrowGLException("Failed to draw vertex array object", "glDrawArrays");
+ } else {
+ GLES30.glDrawElements(
+ primitiveMode.glesEnum, indexBuffer.getSize(), GLES30.GL_UNSIGNED_INT, 0);
+ GLError.maybeThrowGLException(
+ "Failed to draw vertex array object with indices", "glDrawElements");
+ }
+ }
+}
diff --git a/app/src/main/java/upb/airdocs/common/samplerender/SampleRender.java b/app/src/main/java/upb/airdocs/common/samplerender/SampleRender.java
new file mode 100644
index 0000000..78b60e6
--- /dev/null
+++ b/app/src/main/java/upb/airdocs/common/samplerender/SampleRender.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package upb.airdocs.common.samplerender;
+
+import android.content.res.AssetManager;
+import android.opengl.GLES30;
+import android.opengl.GLSurfaceView;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+/** A SampleRender context. */
+public class SampleRender {
+ private static final String TAG = SampleRender.class.getSimpleName();
+
+ private final AssetManager assetManager;
+
+ private int viewportWidth = 1;
+ private int viewportHeight = 1;
+
+ /**
+ * Constructs a SampleRender object and instantiates GLSurfaceView parameters.
+ *
+ * @param glSurfaceView Android GLSurfaceView
+ * @param renderer Renderer implementation to receive callbacks
+ * @param assetManager AssetManager for loading Android resources
+ */
+ public SampleRender(GLSurfaceView glSurfaceView, Renderer renderer, AssetManager assetManager) {
+ this.assetManager = assetManager;
+ glSurfaceView.setPreserveEGLContextOnPause(true);
+ glSurfaceView.setEGLContextClientVersion(3);
+ glSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0);
+ glSurfaceView.setRenderer(
+ new GLSurfaceView.Renderer() {
+ @Override
+ public void onSurfaceCreated(GL10 gl, EGLConfig config) {
+ GLES30.glEnable(GLES30.GL_BLEND);
+ GLError.maybeThrowGLException("Failed to enable blending", "glEnable");
+ renderer.onSurfaceCreated(SampleRender.this);
+ }
+
+ @Override
+ public void onSurfaceChanged(GL10 gl, int w, int h) {
+ viewportWidth = w;
+ viewportHeight = h;
+ renderer.onSurfaceChanged(SampleRender.this, w, h);
+ }
+
+ @Override
+ public void onDrawFrame(GL10 gl) {
+ clear(/*framebuffer=*/ null, 0f, 0f, 0f, 1f);
+ renderer.onDrawFrame(SampleRender.this);
+ }
+ });
+ glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
+ glSurfaceView.setWillNotDraw(false);
+ }
+
+ /** Draw a {@link Mesh} with the specified {@link Shader}. */
+ public void draw(Mesh mesh, Shader shader) {
+ draw(mesh, shader, /*framebuffer=*/ null);
+ }
+
+ /**
+ * Draw a {@link Mesh} with the specified {@link Shader} to the given {@link Framebuffer}.
+ *
+ *
The {@code framebuffer} argument may be null, in which case the default framebuffer is used.
+ */
+ public void draw(Mesh mesh, Shader shader, Framebuffer framebuffer) {
+ useFramebuffer(framebuffer);
+ shader.lowLevelUse();
+ mesh.lowLevelDraw();
+ }
+
+ /**
+ * Clear the given framebuffer.
+ *
+ *
The {@code framebuffer} argument may be null, in which case the default framebuffer is
+ * cleared.
+ */
+ public void clear(Framebuffer framebuffer, float r, float g, float b, float a) {
+ useFramebuffer(framebuffer);
+ GLES30.glClearColor(r, g, b, a);
+ GLError.maybeThrowGLException("Failed to set clear color", "glClearColor");
+ GLES30.glDepthMask(true);
+ GLError.maybeThrowGLException("Failed to set depth write mask", "glDepthMask");
+ GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT | GLES30.GL_DEPTH_BUFFER_BIT);
+ GLError.maybeThrowGLException("Failed to clear framebuffer", "glClear");
+ }
+
+ /** Interface to be implemented for rendering callbacks. */
+ public static interface Renderer {
+ /**
+ * Called by {@link SampleRender} when the GL render surface is created.
+ *
+ *
See {@link GLSurfaceView.Renderer#onSurfaceCreated}.
+ */
+ public void onSurfaceCreated(SampleRender render);
+
+ /**
+ * Called by {@link SampleRender} when the GL render surface dimensions are changed.
+ *
+ *
See {@link GLSurfaceView.Renderer#onSurfaceChanged}.
+ */
+ public void onSurfaceChanged(SampleRender render, int width, int height);
+
+ /**
+ * Called by {@link SampleRender} when a GL frame is to be rendered.
+ *
+ *
See {@link GLSurfaceView.Renderer#onDrawFrame}.
+ */
+ public void onDrawFrame(SampleRender render);
+ }
+
+ /* package-private */
+ AssetManager getAssets() {
+ return assetManager;
+ }
+
+ private void useFramebuffer(Framebuffer framebuffer) {
+ int framebufferId;
+ int viewportWidth;
+ int viewportHeight;
+ if (framebuffer == null) {
+ framebufferId = 0;
+ viewportWidth = this.viewportWidth;
+ viewportHeight = this.viewportHeight;
+ } else {
+ framebufferId = framebuffer.getFramebufferId();
+ viewportWidth = framebuffer.getWidth();
+ viewportHeight = framebuffer.getHeight();
+ }
+ GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, framebufferId);
+ GLError.maybeThrowGLException("Failed to bind framebuffer", "glBindFramebuffer");
+ GLES30.glViewport(0, 0, viewportWidth, viewportHeight);
+ GLError.maybeThrowGLException("Failed to set viewport dimensions", "glViewport");
+ }
+}
diff --git a/app/src/main/java/upb/airdocs/common/samplerender/Shader.java b/app/src/main/java/upb/airdocs/common/samplerender/Shader.java
new file mode 100644
index 0000000..8610f5e
--- /dev/null
+++ b/app/src/main/java/upb/airdocs/common/samplerender/Shader.java
@@ -0,0 +1,650 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package upb.airdocs.common.samplerender;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.content.res.AssetManager;
+import android.opengl.GLES30;
+import android.opengl.GLException;
+import android.util.Log;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+
+/**
+ * Represents a GPU shader, the state of its associated uniforms, and some additional draw state.
+ */
+public class Shader implements Closeable {
+ private static final String TAG = Shader.class.getSimpleName();
+
+ /**
+ * A factor to be used in a blend function.
+ *
+ * @see glBlendFunc
+ */
+ public static enum BlendFactor {
+ ZERO(GLES30.GL_ZERO),
+ ONE(GLES30.GL_ONE),
+ SRC_COLOR(GLES30.GL_SRC_COLOR),
+ ONE_MINUS_SRC_COLOR(GLES30.GL_ONE_MINUS_SRC_COLOR),
+ DST_COLOR(GLES30.GL_DST_COLOR),
+ ONE_MINUS_DST_COLOR(GLES30.GL_ONE_MINUS_DST_COLOR),
+ SRC_ALPHA(GLES30.GL_SRC_ALPHA),
+ ONE_MINUS_SRC_ALPHA(GLES30.GL_ONE_MINUS_SRC_ALPHA),
+ DST_ALPHA(GLES30.GL_DST_ALPHA),
+ ONE_MINUS_DST_ALPHA(GLES30.GL_ONE_MINUS_DST_ALPHA),
+ CONSTANT_COLOR(GLES30.GL_CONSTANT_COLOR),
+ ONE_MINUS_CONSTANT_COLOR(GLES30.GL_ONE_MINUS_CONSTANT_COLOR),
+ CONSTANT_ALPHA(GLES30.GL_CONSTANT_ALPHA),
+ ONE_MINUS_CONSTANT_ALPHA(GLES30.GL_ONE_MINUS_CONSTANT_ALPHA);
+
+ /* package-private */
+ final int glesEnum;
+
+ private BlendFactor(int glesEnum) {
+ this.glesEnum = glesEnum;
+ }
+ }
+
+ private int programId = 0;
+ private final Map uniforms = new HashMap<>();
+ private int maxTextureUnit = 0;
+
+ private final Map uniformLocations = new HashMap<>();
+ private final Map uniformNames = new HashMap<>();
+
+ private boolean depthTest = true;
+ private boolean depthWrite = true;
+ private BlendFactor sourceRgbBlend = BlendFactor.ONE;
+ private BlendFactor destRgbBlend = BlendFactor.ZERO;
+ private BlendFactor sourceAlphaBlend = BlendFactor.ONE;
+ private BlendFactor destAlphaBlend = BlendFactor.ZERO;
+
+ /**
+ * Constructs a {@link Shader} given the shader code.
+ *
+ * @param defines A map of shader precompiler symbols to be defined with the given names and
+ * values
+ */
+ public Shader(
+ SampleRender render,
+ String vertexShaderCode,
+ String fragmentShaderCode,
+ Map defines) {
+ int vertexShaderId = 0;
+ int fragmentShaderId = 0;
+ String definesCode = createShaderDefinesCode(defines);
+ try {
+ vertexShaderId =
+ createShader(
+ GLES30.GL_VERTEX_SHADER, insertShaderDefinesCode(vertexShaderCode, definesCode));
+ fragmentShaderId =
+ createShader(
+ GLES30.GL_FRAGMENT_SHADER, insertShaderDefinesCode(fragmentShaderCode, definesCode));
+
+ programId = GLES30.glCreateProgram();
+ GLError.maybeThrowGLException("Shader program creation failed", "glCreateProgram");
+ GLES30.glAttachShader(programId, vertexShaderId);
+ GLError.maybeThrowGLException("Failed to attach vertex shader", "glAttachShader");
+ GLES30.glAttachShader(programId, fragmentShaderId);
+ GLError.maybeThrowGLException("Failed to attach fragment shader", "glAttachShader");
+ GLES30.glLinkProgram(programId);
+ GLError.maybeThrowGLException("Failed to link shader program", "glLinkProgram");
+
+ final int[] linkStatus = new int[1];
+ GLES30.glGetProgramiv(programId, GLES30.GL_LINK_STATUS, linkStatus, 0);
+ if (linkStatus[0] == GLES30.GL_FALSE) {
+ String infoLog = GLES30.glGetProgramInfoLog(programId);
+ GLError.maybeLogGLError(
+ Log.WARN, TAG, "Failed to retrieve shader program info log", "glGetProgramInfoLog");
+ throw new GLException(0, "Shader link failed: " + infoLog);
+ }
+ } catch (Throwable t) {
+ close();
+ throw t;
+ } finally {
+ // Shader objects can be flagged for deletion immediately after program creation.
+ if (vertexShaderId != 0) {
+ GLES30.glDeleteShader(vertexShaderId);
+ GLError.maybeLogGLError(Log.WARN, TAG, "Failed to free vertex shader", "glDeleteShader");
+ }
+ if (fragmentShaderId != 0) {
+ GLES30.glDeleteShader(fragmentShaderId);
+ GLError.maybeLogGLError(Log.WARN, TAG, "Failed to free fragment shader", "glDeleteShader");
+ }
+ }
+ }
+
+ /**
+ * Creates a {@link Shader} from the given asset file names.
+ *
+ * The file contents are interpreted as UTF-8 text.
+ *
+ * @param defines A map of shader precompiler symbols to be defined with the given names and
+ * values
+ */
+ public static Shader createFromAssets(
+ SampleRender render,
+ String vertexShaderFileName,
+ String fragmentShaderFileName,
+ Map defines)
+ throws IOException {
+ AssetManager assets = render.getAssets();
+ return new Shader(
+ render,
+ inputStreamToString(assets.open(vertexShaderFileName)),
+ inputStreamToString(assets.open(fragmentShaderFileName)),
+ defines);
+ }
+
+ @Override
+ public void close() {
+ if (programId != 0) {
+ GLES30.glDeleteProgram(programId);
+ programId = 0;
+ }
+ }
+
+ /**
+ * Sets depth test state.
+ *
+ * @see glEnable(GL_DEPTH_TEST).
+ */
+ public Shader setDepthTest(boolean depthTest) {
+ this.depthTest = depthTest;
+ return this;
+ }
+
+ /**
+ * Sets depth write state.
+ *
+ * @see glDepthMask.
+ */
+ public Shader setDepthWrite(boolean depthWrite) {
+ this.depthWrite = depthWrite;
+ return this;
+ }
+
+ /**
+ * Sets blending function.
+ *
+ * @see glBlendFunc
+ */
+ public Shader setBlend(BlendFactor sourceBlend, BlendFactor destBlend) {
+ this.sourceRgbBlend = sourceBlend;
+ this.destRgbBlend = destBlend;
+ this.sourceAlphaBlend = sourceBlend;
+ this.destAlphaBlend = destBlend;
+ return this;
+ }
+
+ /**
+ * Sets blending functions separately for RGB and alpha channels.
+ *
+ * @see glBlendFunc
+ */
+ public Shader setBlend(
+ BlendFactor sourceRgbBlend,
+ BlendFactor destRgbBlend,
+ BlendFactor sourceAlphaBlend,
+ BlendFactor destAlphaBlend) {
+ this.sourceRgbBlend = sourceRgbBlend;
+ this.destRgbBlend = destRgbBlend;
+ this.sourceAlphaBlend = sourceAlphaBlend;
+ this.destAlphaBlend = destAlphaBlend;
+ return this;
+ }
+
+ /** Sets a texture uniform. */
+ public Shader setTexture(String name, Texture texture) {
+ // Special handling for Textures. If replacing an existing texture uniform, reuse the texture
+ // unit.
+ int location = getUniformLocation(name);
+ Uniform uniform = uniforms.get(location);
+ int textureUnit;
+ if (!(uniform instanceof UniformTexture)) {
+ textureUnit = maxTextureUnit++;
+ } else {
+ UniformTexture uniformTexture = (UniformTexture) uniform;
+ textureUnit = uniformTexture.getTextureUnit();
+ }
+ uniforms.put(location, new UniformTexture(textureUnit, texture));
+ return this;
+ }
+
+ /** Sets a {@code bool} uniform. */
+ public Shader setBool(String name, boolean v0) {
+ int[] values = {v0 ? 1 : 0};
+ uniforms.put(getUniformLocation(name), new UniformInt(values));
+ return this;
+ }
+
+ /** Sets an {@code int} uniform. */
+ public Shader setInt(String name, int v0) {
+ int[] values = {v0};
+ uniforms.put(getUniformLocation(name), new UniformInt(values));
+ return this;
+ }
+
+ /** Sets a {@code float} uniform. */
+ public Shader setFloat(String name, float v0) {
+ float[] values = {v0};
+ uniforms.put(getUniformLocation(name), new Uniform1f(values));
+ return this;
+ }
+
+ /** Sets a {@code vec2} uniform. */
+ public Shader setVec2(String name, float[] values) {
+ if (values.length != 2) {
+ throw new IllegalArgumentException("Value array length must be 2");
+ }
+ uniforms.put(getUniformLocation(name), new Uniform2f(values.clone()));
+ return this;
+ }
+ /** Sets a {@code vec3} uniform. */
+ public Shader setVec3(String name, float[] values) {
+ if (values.length != 3) {
+ throw new IllegalArgumentException("Value array length must be 3");
+ }
+ uniforms.put(getUniformLocation(name), new Uniform3f(values.clone()));
+ return this;
+ }
+
+ /** Sets a {@code vec4} uniform. */
+ public Shader setVec4(String name, float[] values) {
+ if (values.length != 4) {
+ throw new IllegalArgumentException("Value array length must be 4");
+ }
+ uniforms.put(getUniformLocation(name), new Uniform4f(values.clone()));
+ return this;
+ }
+
+ /** Sets a {@code mat2} uniform. */
+ public Shader setMat2(String name, float[] values) {
+ if (values.length != 4) {
+ throw new IllegalArgumentException("Value array length must be 4 (2x2)");
+ }
+ uniforms.put(getUniformLocation(name), new UniformMatrix2f(values.clone()));
+ return this;
+ }
+
+ /** Sets a {@code mat3} uniform. */
+ public Shader setMat3(String name, float[] values) {
+ if (values.length != 9) {
+ throw new IllegalArgumentException("Value array length must be 9 (3x3)");
+ }
+ uniforms.put(getUniformLocation(name), new UniformMatrix3f(values.clone()));
+ return this;
+ }
+
+ /** Sets a {@code mat4} uniform. */
+ public Shader setMat4(String name, float[] values) {
+ if (values.length != 16) {
+ throw new IllegalArgumentException("Value array length must be 16 (4x4)");
+ }
+ uniforms.put(getUniformLocation(name), new UniformMatrix4f(values.clone()));
+ return this;
+ }
+
+ /** Sets a {@code bool} array uniform. */
+ public Shader setBoolArray(String name, boolean[] values) {
+ int[] intValues = new int[values.length];
+ for (int i = 0; i < values.length; ++i) {
+ intValues[i] = values[i] ? 1 : 0;
+ }
+ uniforms.put(getUniformLocation(name), new UniformInt(intValues));
+ return this;
+ }
+
+ /** Sets an {@code int} array uniform. */
+ public Shader setIntArray(String name, int[] values) {
+ uniforms.put(getUniformLocation(name), new UniformInt(values.clone()));
+ return this;
+ }
+
+ /** Sets a {@code float} array uniform. */
+ public Shader setFloatArray(String name, float[] values) {
+ uniforms.put(getUniformLocation(name), new Uniform1f(values.clone()));
+ return this;
+ }
+
+ /** Sets a {@code vec2} array uniform. */
+ public Shader setVec2Array(String name, float[] values) {
+ if (values.length % 2 != 0) {
+ throw new IllegalArgumentException("Value array length must be divisible by 2");
+ }
+ uniforms.put(getUniformLocation(name), new Uniform2f(values.clone()));
+ return this;
+ }
+ /** Sets a {@code vec3} array uniform. */
+ public Shader setVec3Array(String name, float[] values) {
+ if (values.length % 3 != 0) {
+ throw new IllegalArgumentException("Value array length must be divisible by 3");
+ }
+ uniforms.put(getUniformLocation(name), new Uniform3f(values.clone()));
+ return this;
+ }
+
+ /** Sets a {@code vec4} array uniform. */
+ public Shader setVec4Array(String name, float[] values) {
+ if (values.length % 4 != 0) {
+ throw new IllegalArgumentException("Value array length must be divisible by 4");
+ }
+ uniforms.put(getUniformLocation(name), new Uniform4f(values.clone()));
+ return this;
+ }
+
+ /** Sets a {@code mat2} array uniform. */
+ public Shader setMat2Array(String name, float[] values) {
+ if (values.length % 4 != 0) {
+ throw new IllegalArgumentException("Value array length must be divisible by 4 (2x2)");
+ }
+ uniforms.put(getUniformLocation(name), new UniformMatrix2f(values.clone()));
+ return this;
+ }
+
+ /** Sets a {@code mat3} array uniform. */
+ public Shader setMat3Array(String name, float[] values) {
+ if (values.length % 9 != 0) {
+ throw new IllegalArgumentException("Values array length must be divisible by 9 (3x3)");
+ }
+ uniforms.put(getUniformLocation(name), new UniformMatrix3f(values.clone()));
+ return this;
+ }
+
+ /** Sets a {@code mat4} uniform. */
+ public Shader setMat4Array(String name, float[] values) {
+ if (values.length % 16 != 0) {
+ throw new IllegalArgumentException("Value array length must be divisible by 16 (4x4)");
+ }
+ uniforms.put(getUniformLocation(name), new UniformMatrix4f(values.clone()));
+ return this;
+ }
+
+ /**
+ * Activates the shader. Don't call this directly unless you are doing low level OpenGL code;
+ * instead, prefer {@link SampleRender#draw}.
+ */
+ public void lowLevelUse() {
+ // Make active shader/set uniforms
+ if (programId == 0) {
+ throw new IllegalStateException("Attempted to use freed shader");
+ }
+ GLES30.glUseProgram(programId);
+ GLError.maybeThrowGLException("Failed to use shader program", "glUseProgram");
+ GLES30.glBlendFuncSeparate(
+ sourceRgbBlend.glesEnum,
+ destRgbBlend.glesEnum,
+ sourceAlphaBlend.glesEnum,
+ destAlphaBlend.glesEnum);
+ GLError.maybeThrowGLException("Failed to set blend mode", "glBlendFuncSeparate");
+ GLES30.glDepthMask(depthWrite);
+ GLError.maybeThrowGLException("Failed to set depth write mask", "glDepthMask");
+ if (depthTest) {
+ GLES30.glEnable(GLES30.GL_DEPTH_TEST);
+ GLError.maybeThrowGLException("Failed to enable depth test", "glEnable");
+ } else {
+ GLES30.glDisable(GLES30.GL_DEPTH_TEST);
+ GLError.maybeThrowGLException("Failed to disable depth test", "glDisable");
+ }
+ try {
+ // Remove all non-texture uniforms from the map after setting them, since they're stored as
+ // part of the program.
+ ArrayList obsoleteEntries = new ArrayList<>(uniforms.size());
+ for (Map.Entry entry : uniforms.entrySet()) {
+ try {
+ entry.getValue().use(entry.getKey());
+ if (!(entry.getValue() instanceof UniformTexture)) {
+ obsoleteEntries.add(entry.getKey());
+ }
+ } catch (GLException e) {
+ String name = uniformNames.get(entry.getKey());
+ throw new IllegalArgumentException("Error setting uniform `" + name + "'", e);
+ }
+ }
+ uniforms.keySet().removeAll(obsoleteEntries);
+ } finally {
+ GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
+ GLError.maybeLogGLError(Log.WARN, TAG, "Failed to set active texture", "glActiveTexture");
+ }
+ }
+
+ private static interface Uniform {
+ public void use(int location);
+ }
+
+ private static class UniformTexture implements Uniform {
+ private final int textureUnit;
+ private final Texture texture;
+
+ public UniformTexture(int textureUnit, Texture texture) {
+ this.textureUnit = textureUnit;
+ this.texture = texture;
+ }
+
+ public int getTextureUnit() {
+ return textureUnit;
+ }
+
+ @Override
+ public void use(int location) {
+ if (texture.getTextureId() == 0) {
+ throw new IllegalStateException("Tried to draw with freed texture");
+ }
+ GLES30.glActiveTexture(GLES30.GL_TEXTURE0 + textureUnit);
+ GLError.maybeThrowGLException("Failed to set active texture", "glActiveTexture");
+ GLES30.glBindTexture(texture.getTarget().glesEnum, texture.getTextureId());
+ GLError.maybeThrowGLException("Failed to bind texture", "glBindTexture");
+ GLES30.glUniform1i(location, textureUnit);
+ GLError.maybeThrowGLException("Failed to set shader texture uniform", "glUniform1i");
+ }
+ }
+
+ private static class UniformInt implements Uniform {
+ private final int[] values;
+
+ public UniformInt(int[] values) {
+ this.values = values;
+ }
+
+ @Override
+ public void use(int location) {
+ GLES30.glUniform1iv(location, values.length, values, 0);
+ GLError.maybeThrowGLException("Failed to set shader uniform 1i", "glUniform1iv");
+ }
+ }
+
+ private static class Uniform1f implements Uniform {
+ private final float[] values;
+
+ public Uniform1f(float[] values) {
+ this.values = values;
+ }
+
+ @Override
+ public void use(int location) {
+ GLES30.glUniform1fv(location, values.length, values, 0);
+ GLError.maybeThrowGLException("Failed to set shader uniform 1f", "glUniform1fv");
+ }
+ }
+
+ private static class Uniform2f implements Uniform {
+ private final float[] values;
+
+ public Uniform2f(float[] values) {
+ this.values = values;
+ }
+
+ @Override
+ public void use(int location) {
+ GLES30.glUniform2fv(location, values.length / 2, values, 0);
+ GLError.maybeThrowGLException("Failed to set shader uniform 2f", "glUniform2fv");
+ }
+ }
+
+ private static class Uniform3f implements Uniform {
+ private final float[] values;
+
+ public Uniform3f(float[] values) {
+ this.values = values;
+ }
+
+ @Override
+ public void use(int location) {
+ GLES30.glUniform3fv(location, values.length / 3, values, 0);
+ GLError.maybeThrowGLException("Failed to set shader uniform 3f", "glUniform3fv");
+ }
+ }
+
+ private static class Uniform4f implements Uniform {
+ private final float[] values;
+
+ public Uniform4f(float[] values) {
+ this.values = values;
+ }
+
+ @Override
+ public void use(int location) {
+ GLES30.glUniform4fv(location, values.length / 4, values, 0);
+ GLError.maybeThrowGLException("Failed to set shader uniform 4f", "glUniform4fv");
+ }
+ }
+
+ private static class UniformMatrix2f implements Uniform {
+ private final float[] values;
+
+ public UniformMatrix2f(float[] values) {
+ this.values = values;
+ }
+
+ @Override
+ public void use(int location) {
+ GLES30.glUniformMatrix2fv(location, values.length / 4, /*transpose=*/ false, values, 0);
+ GLError.maybeThrowGLException("Failed to set shader uniform matrix 2f", "glUniformMatrix2fv");
+ }
+ }
+
+ private static class UniformMatrix3f implements Uniform {
+ private final float[] values;
+
+ public UniformMatrix3f(float[] values) {
+ this.values = values;
+ }
+
+ @Override
+ public void use(int location) {
+ GLES30.glUniformMatrix3fv(location, values.length / 9, /*transpose=*/ false, values, 0);
+ GLError.maybeThrowGLException("Failed to set shader uniform matrix 3f", "glUniformMatrix3fv");
+ }
+ }
+
+ private static class UniformMatrix4f implements Uniform {
+ private final float[] values;
+
+ public UniformMatrix4f(float[] values) {
+ this.values = values;
+ }
+
+ @Override
+ public void use(int location) {
+ GLES30.glUniformMatrix4fv(location, values.length / 16, /*transpose=*/ false, values, 0);
+ GLError.maybeThrowGLException("Failed to set shader uniform matrix 4f", "glUniformMatrix4fv");
+ }
+ }
+
+ private int getUniformLocation(String name) {
+ Integer locationObject = uniformLocations.get(name);
+ if (locationObject != null) {
+ return locationObject;
+ }
+ int location = GLES30.glGetUniformLocation(programId, name);
+ GLError.maybeThrowGLException("Failed to find uniform", "glGetUniformLocation");
+ if (location == -1) {
+ throw new IllegalArgumentException("Shader uniform does not exist: " + name);
+ }
+ uniformLocations.put(name, Integer.valueOf(location));
+ uniformNames.put(Integer.valueOf(location), name);
+ return location;
+ }
+
+ private static int createShader(int type, String code) {
+ int shaderId = GLES30.glCreateShader(type);
+ GLError.maybeThrowGLException("Shader creation failed", "glCreateShader");
+ GLES30.glShaderSource(shaderId, code);
+ GLError.maybeThrowGLException("Shader source failed", "glShaderSource");
+ GLES30.glCompileShader(shaderId);
+ GLError.maybeThrowGLException("Shader compilation failed", "glCompileShader");
+
+ final int[] compileStatus = new int[1];
+ GLES30.glGetShaderiv(shaderId, GLES30.GL_COMPILE_STATUS, compileStatus, 0);
+ if (compileStatus[0] == GLES30.GL_FALSE) {
+ String infoLog = GLES30.glGetShaderInfoLog(shaderId);
+ GLError.maybeLogGLError(
+ Log.WARN, TAG, "Failed to retrieve shader info log", "glGetShaderInfoLog");
+ GLES30.glDeleteShader(shaderId);
+ GLError.maybeLogGLError(Log.WARN, TAG, "Failed to free shader", "glDeleteShader");
+ throw new GLException(0, "Shader compilation failed: " + infoLog);
+ }
+
+ return shaderId;
+ }
+
+ private static String createShaderDefinesCode(Map defines) {
+ if (defines == null) {
+ return "";
+ }
+ StringBuilder builder = new StringBuilder();
+ for (Map.Entry entry : defines.entrySet()) {
+ builder.append("#define " + entry.getKey() + " " + entry.getValue() + "\n");
+ }
+ return builder.toString();
+ }
+
+ private static String insertShaderDefinesCode(String sourceCode, String definesCode) {
+ String result =
+ sourceCode.replaceAll(
+ "(?m)^(\\s*#\\s*version\\s+.*)$", "$1\n" + Matcher.quoteReplacement(definesCode));
+ if (result.equals(sourceCode)) {
+ // No #version specified, so just prepend source
+ return definesCode + sourceCode;
+ }
+ return result;
+ }
+
+ private static String inputStreamToString(InputStream stream) throws IOException {
+ InputStreamReader reader = new InputStreamReader(stream, UTF_8.name());
+ char[] buffer = new char[1024 * 4];
+ StringBuilder builder = new StringBuilder();
+ int amount = 0;
+ while ((amount = reader.read(buffer)) != -1) {
+ builder.append(buffer, 0, amount);
+ }
+ reader.close();
+ return builder.toString();
+ }
+}
diff --git a/app/src/main/java/upb/airdocs/common/samplerender/Texture.java b/app/src/main/java/upb/airdocs/common/samplerender/Texture.java
new file mode 100644
index 0000000..fd40c28
--- /dev/null
+++ b/app/src/main/java/upb/airdocs/common/samplerender/Texture.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package upb.airdocs.common.samplerender;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.opengl.GLES11Ext;
+import android.opengl.GLES30;
+import android.util.Log;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/** A GPU-side texture. */
+public class Texture implements Closeable {
+ private static final String TAG = Texture.class.getSimpleName();
+
+ private final int[] textureId = {0};
+ private final Target target;
+
+ /**
+ * Describes the way the texture's edges are rendered.
+ *
+ * @see GL_TEXTURE_WRAP_S.
+ */
+ public enum WrapMode {
+ CLAMP_TO_EDGE(GLES30.GL_CLAMP_TO_EDGE),
+ MIRRORED_REPEAT(GLES30.GL_MIRRORED_REPEAT),
+ REPEAT(GLES30.GL_REPEAT);
+
+ /* package-private */
+ final int glesEnum;
+
+ private WrapMode(int glesEnum) {
+ this.glesEnum = glesEnum;
+ }
+ }
+
+ /**
+ * Describes the target this texture is bound to.
+ *
+ * @see glBindTexture.
+ */
+ public enum Target {
+ TEXTURE_2D(GLES30.GL_TEXTURE_2D),
+ TEXTURE_EXTERNAL_OES(GLES11Ext.GL_TEXTURE_EXTERNAL_OES),
+ TEXTURE_CUBE_MAP(GLES30.GL_TEXTURE_CUBE_MAP);
+
+ final int glesEnum;
+
+ private Target(int glesEnum) {
+ this.glesEnum = glesEnum;
+ }
+ }
+
+ /**
+ * Describes the color format of the texture.
+ *
+ * @see glTexImage2d.
+ */
+ public enum ColorFormat {
+ LINEAR(GLES30.GL_RGBA8),
+ SRGB(GLES30.GL_SRGB8_ALPHA8);
+
+ final int glesEnum;
+
+ private ColorFormat(int glesEnum) {
+ this.glesEnum = glesEnum;
+ }
+ }
+
+ /**
+ * Construct an empty {@link Texture}.
+ *
+ * Since {@link Texture}s created in this way are not populated with data, this method is
+ * mostly only useful for creating {@link Target.TEXTURE_EXTERNAL_OES} textures. See {@link
+ * #createFromAsset} if you want a texture with data.
+ */
+ public Texture(SampleRender render, Target target, WrapMode wrapMode) {
+ this(render, target, wrapMode, /*useMipmaps=*/ true);
+ }
+
+ public Texture(SampleRender render, Target target, WrapMode wrapMode, boolean useMipmaps) {
+ this.target = target;
+
+ GLES30.glGenTextures(1, textureId, 0);
+ GLError.maybeThrowGLException("Texture creation failed", "glGenTextures");
+
+ int minFilter = useMipmaps ? GLES30.GL_LINEAR_MIPMAP_LINEAR : GLES30.GL_LINEAR;
+
+ try {
+ GLES30.glBindTexture(target.glesEnum, textureId[0]);
+ GLError.maybeThrowGLException("Failed to bind texture", "glBindTexture");
+ GLES30.glTexParameteri(target.glesEnum, GLES30.GL_TEXTURE_MIN_FILTER, minFilter);
+ GLError.maybeThrowGLException("Failed to set texture parameter", "glTexParameteri");
+ GLES30.glTexParameteri(target.glesEnum, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);
+ GLError.maybeThrowGLException("Failed to set texture parameter", "glTexParameteri");
+
+ GLES30.glTexParameteri(target.glesEnum, GLES30.GL_TEXTURE_WRAP_S, wrapMode.glesEnum);
+ GLError.maybeThrowGLException("Failed to set texture parameter", "glTexParameteri");
+ GLES30.glTexParameteri(target.glesEnum, GLES30.GL_TEXTURE_WRAP_T, wrapMode.glesEnum);
+ GLError.maybeThrowGLException("Failed to set texture parameter", "glTexParameteri");
+ } catch (Throwable t) {
+ close();
+ throw t;
+ }
+ }
+
+ /** Create a texture from the given asset file name. */
+ public static Texture createFromAsset(
+ SampleRender render, String assetFileName, WrapMode wrapMode, ColorFormat colorFormat)
+ throws IOException {
+ Texture texture = new Texture(render, Target.TEXTURE_2D, wrapMode);
+ Bitmap bitmap = null;
+ try {
+ // The following lines up to glTexImage2D could technically be replaced with
+ // GLUtils.texImage2d, but this method does not allow for loading sRGB images.
+
+ // Load and convert the bitmap and copy its contents to a direct ByteBuffer. Despite its name,
+ // the ARGB_8888 config is actually stored in RGBA order.
+ bitmap =
+ convertBitmapToConfig(
+ BitmapFactory.decodeStream(render.getAssets().open(assetFileName)),
+ Bitmap.Config.ARGB_8888);
+ ByteBuffer buffer = ByteBuffer.allocateDirect(bitmap.getByteCount());
+ bitmap.copyPixelsToBuffer(buffer);
+ buffer.rewind();
+
+ GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, texture.getTextureId());
+ GLError.maybeThrowGLException("Failed to bind texture", "glBindTexture");
+ GLES30.glTexImage2D(
+ GLES30.GL_TEXTURE_2D,
+ /*level=*/ 0,
+ colorFormat.glesEnum,
+ bitmap.getWidth(),
+ bitmap.getHeight(),
+ /*border=*/ 0,
+ GLES30.GL_RGBA,
+ GLES30.GL_UNSIGNED_BYTE,
+ buffer);
+ GLError.maybeThrowGLException("Failed to populate texture data", "glTexImage2D");
+ GLES30.glGenerateMipmap(GLES30.GL_TEXTURE_2D);
+ GLError.maybeThrowGLException("Failed to generate mipmaps", "glGenerateMipmap");
+ } catch (Throwable t) {
+ texture.close();
+ throw t;
+ } finally {
+ if (bitmap != null) {
+ bitmap.recycle();
+ }
+ }
+ return texture;
+ }
+
+ @Override
+ public void close() {
+ if (textureId[0] != 0) {
+ GLES30.glDeleteTextures(1, textureId, 0);
+ GLError.maybeLogGLError(Log.WARN, TAG, "Failed to free texture", "glDeleteTextures");
+ textureId[0] = 0;
+ }
+ }
+
+ /** Retrieve the native texture ID. */
+ public int getTextureId() {
+ return textureId[0];
+ }
+
+ /* package-private */
+ Target getTarget() {
+ return target;
+ }
+
+ private static Bitmap convertBitmapToConfig(Bitmap bitmap, Bitmap.Config config) {
+ // We use this method instead of BitmapFactory.Options.outConfig to support a minimum of Android
+ // API level 24.
+ if (bitmap.getConfig() == config) {
+ return bitmap;
+ }
+ Bitmap result = bitmap.copy(config, /*isMutable=*/ false);
+ bitmap.recycle();
+ return result;
+ }
+}
diff --git a/app/src/main/java/upb/airdocs/common/samplerender/VertexBuffer.java b/app/src/main/java/upb/airdocs/common/samplerender/VertexBuffer.java
new file mode 100644
index 0000000..f2dc3f7
--- /dev/null
+++ b/app/src/main/java/upb/airdocs/common/samplerender/VertexBuffer.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package upb.airdocs.common.samplerender;
+
+import android.opengl.GLES30;
+
+import java.io.Closeable;
+import java.nio.FloatBuffer;
+
+/**
+ * A list of vertex attribute data stored GPU-side.
+ *
+ *
One or more {@link VertexBuffer}s are used when constructing a {@link Mesh} to describe vertex
+ * attribute data; for example, local coordinates, texture coordinates, vertex normals, etc.
+ *
+ * @see glVertexAttribPointer
+ */
+public class VertexBuffer implements Closeable {
+ private final GpuBuffer buffer;
+ private final int numberOfEntriesPerVertex;
+
+ /**
+ * Construct a {@link VertexBuffer} populated with initial data.
+ *
+ *
The GPU buffer will be filled with the data in the direct buffer {@code entries},
+ * starting from the beginning of the buffer (not the current cursor position). The cursor will be
+ * left in an undefined position after this function returns.
+ *
+ *
The number of vertices in the buffer can be expressed as {@code entries.limit() /
+ * numberOfEntriesPerVertex}. Thus, The size of the buffer must be divisible by {@code
+ * numberOfEntriesPerVertex}.
+ *
+ *
The {@code entries} buffer may be null, in which case an empty buffer is constructed
+ * instead.
+ */
+ public VertexBuffer(SampleRender render, int numberOfEntriesPerVertex, FloatBuffer entries) {
+ if (entries != null && entries.limit() % numberOfEntriesPerVertex != 0) {
+ throw new IllegalArgumentException(
+ "If non-null, vertex buffer data must be divisible by the number of data points per"
+ + " vertex");
+ }
+
+ this.numberOfEntriesPerVertex = numberOfEntriesPerVertex;
+ buffer = new GpuBuffer(GLES30.GL_ARRAY_BUFFER, GpuBuffer.FLOAT_SIZE, entries);
+ }
+
+ /**
+ * Populate with new data.
+ *
+ *
The entire buffer is replaced by the contents of the direct buffer {@code entries}
+ * starting from the beginning of the buffer, not the current cursor position. The cursor will be
+ * left in an undefined position after this function returns.
+ *
+ *
The GPU buffer is reallocated automatically if necessary.
+ *
+ *
The {@code entries} buffer may be null, in which case the buffer will become empty.
+ * Otherwise, the size of {@code entries} must be divisible by the number of entries per vertex
+ * specified during construction.
+ */
+ public void set(FloatBuffer entries) {
+ if (entries != null && entries.limit() % numberOfEntriesPerVertex != 0) {
+ throw new IllegalArgumentException(
+ "If non-null, vertex buffer data must be divisible by the number of data points per"
+ + " vertex");
+ }
+ buffer.set(entries);
+ }
+
+ @Override
+ public void close() {
+ buffer.free();
+ }
+
+ /* package-private */
+ int getBufferId() {
+ return buffer.getBufferId();
+ }
+
+ /* package-private */
+ int getNumberOfEntriesPerVertex() {
+ return numberOfEntriesPerVertex;
+ }
+
+ /* package-private */
+ int getNumberOfVertices() {
+ return buffer.getSize() / numberOfEntriesPerVertex;
+ }
+}
diff --git a/app/src/main/java/upb/airdocs/common/samplerender/arcore/BackgroundRenderer.java b/app/src/main/java/upb/airdocs/common/samplerender/arcore/BackgroundRenderer.java
new file mode 100644
index 0000000..6f3f0fe
--- /dev/null
+++ b/app/src/main/java/upb/airdocs/common/samplerender/arcore/BackgroundRenderer.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package upb.airdocs.common.samplerender.arcore;
+
+import android.media.Image;
+import android.opengl.GLES30;
+
+import com.google.ar.core.Coordinates2d;
+import com.google.ar.core.Frame;
+import upb.airdocs.common.samplerender.Framebuffer;
+import upb.airdocs.common.samplerender.Mesh;
+import upb.airdocs.common.samplerender.SampleRender;
+import upb.airdocs.common.samplerender.Shader;
+import upb.airdocs.common.samplerender.Texture;
+import upb.airdocs.common.samplerender.VertexBuffer;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.util.HashMap;
+
+/**
+ * This class both renders the AR camera background and composes the a scene foreground. The camera
+ * background can be rendered as either camera image data or camera depth data. The virtual scene
+ * can be composited with or without depth occlusion.
+ */
+public class BackgroundRenderer {
+ private static final String TAG = BackgroundRenderer.class.getSimpleName();
+
+ // components_per_vertex * number_of_vertices * float_size
+ private static final int COORDS_BUFFER_SIZE = 2 * 4 * 4;
+
+ private static final FloatBuffer NDC_QUAD_COORDS_BUFFER =
+ ByteBuffer.allocateDirect(COORDS_BUFFER_SIZE).order(ByteOrder.nativeOrder()).asFloatBuffer();
+
+ private static final FloatBuffer VIRTUAL_SCENE_TEX_COORDS_BUFFER =
+ ByteBuffer.allocateDirect(COORDS_BUFFER_SIZE).order(ByteOrder.nativeOrder()).asFloatBuffer();
+
+ static {
+ NDC_QUAD_COORDS_BUFFER.put(
+ new float[] {
+ /*0:*/ -1f, -1f, /*1:*/ +1f, -1f, /*2:*/ -1f, +1f, /*3:*/ +1f, +1f,
+ });
+ VIRTUAL_SCENE_TEX_COORDS_BUFFER.put(
+ new float[] {
+ /*0:*/ 0f, 0f, /*1:*/ 1f, 0f, /*2:*/ 0f, 1f, /*3:*/ 1f, 1f,
+ });
+ }
+
+ private final FloatBuffer cameraTexCoords =
+ ByteBuffer.allocateDirect(COORDS_BUFFER_SIZE).order(ByteOrder.nativeOrder()).asFloatBuffer();
+
+ private final Mesh mesh;
+ private final VertexBuffer cameraTexCoordsVertexBuffer;
+ private Shader backgroundShader;
+ private Shader occlusionShader;
+ private final Texture cameraDepthTexture;
+ private final Texture cameraColorTexture;
+
+ private boolean useDepthVisualization;
+ private boolean useOcclusion;
+ private float aspectRatio;
+
+ /**
+ * Allocates and initializes OpenGL resources needed by the background renderer. Must be called
+ * during a {@link SampleRender.Renderer} callback, typically in {@link
+ * SampleRender.Renderer#onSurfaceCreated()}.
+ */
+ public BackgroundRenderer(SampleRender render) {
+ cameraColorTexture =
+ new Texture(
+ render,
+ Texture.Target.TEXTURE_EXTERNAL_OES,
+ Texture.WrapMode.CLAMP_TO_EDGE,
+ /*useMipmaps=*/ false);
+ cameraDepthTexture =
+ new Texture(
+ render,
+ Texture.Target.TEXTURE_2D,
+ Texture.WrapMode.CLAMP_TO_EDGE,
+ /*useMipmaps=*/ false);
+
+ // Create a Mesh with three vertex buffers: one for the screen coordinates (normalized device
+ // coordinates), one for the camera texture coordinates (to be populated with proper data later
+ // before drawing), and one for the virtual scene texture coordinates (unit texture quad)
+ VertexBuffer screenCoordsVertexBuffer =
+ new VertexBuffer(render, /* numberOfEntriesPerVertex=*/ 2, NDC_QUAD_COORDS_BUFFER);
+ cameraTexCoordsVertexBuffer =
+ new VertexBuffer(render, /*numberOfEntriesPerVertex=*/ 2, /*entries=*/ null);
+ VertexBuffer virtualSceneTexCoordsVertexBuffer =
+ new VertexBuffer(render, /* numberOfEntriesPerVertex=*/ 2, VIRTUAL_SCENE_TEX_COORDS_BUFFER);
+ VertexBuffer[] vertexBuffers = {
+ screenCoordsVertexBuffer, cameraTexCoordsVertexBuffer, virtualSceneTexCoordsVertexBuffer,
+ };
+ mesh =
+ new Mesh(render, Mesh.PrimitiveMode.TRIANGLE_STRIP, /*indexBuffer=*/ null, vertexBuffers);
+ }
+
+ /**
+ * Sets whether the background camera image should be replaced with a depth visualization instead.
+ * This reloads the corresponding shader code, and must be called on the GL thread.
+ */
+ public void setUseDepthVisualization(SampleRender render, boolean useDepthVisualization)
+ throws IOException {
+ if (backgroundShader != null) {
+ if (this.useDepthVisualization == useDepthVisualization) {
+ return;
+ }
+ backgroundShader.close();
+ backgroundShader = null;
+ this.useDepthVisualization = useDepthVisualization;
+ }
+ if (useDepthVisualization) {
+ backgroundShader =
+ Shader.createFromAssets(
+ render,
+ "shaders/background_show_depth_color_visualization.vert",
+ "shaders/background_show_depth_color_visualization.frag",
+ /*defines=*/ null)
+ .setTexture("u_CameraDepthTexture", cameraDepthTexture)
+ .setDepthTest(false)
+ .setDepthWrite(false);
+ } else {
+ backgroundShader =
+ Shader.createFromAssets(
+ render,
+ "shaders/background_show_camera.vert",
+ "shaders/background_show_camera.frag",
+ /*defines=*/ null)
+ .setTexture("u_CameraColorTexture", cameraColorTexture)
+ .setDepthTest(false)
+ .setDepthWrite(false);
+ }
+ }
+
+ /**
+ * Sets whether to use depth for occlusion. This reloads the shader code with new {@code
+ * #define}s, and must be called on the GL thread.
+ */
+ public void setUseOcclusion(SampleRender render, boolean useOcclusion) throws IOException {
+ if (occlusionShader != null) {
+ if (this.useOcclusion == useOcclusion) {
+ return;
+ }
+ occlusionShader.close();
+ occlusionShader = null;
+ this.useOcclusion = useOcclusion;
+ }
+ HashMap defines = new HashMap<>();
+ defines.put("USE_OCCLUSION", useOcclusion ? "1" : "0");
+ occlusionShader =
+ Shader.createFromAssets(render, "shaders/occlusion.vert", "shaders/occlusion.frag", defines)
+ .setDepthTest(false)
+ .setDepthWrite(false)
+ .setBlend(Shader.BlendFactor.SRC_ALPHA, Shader.BlendFactor.ONE_MINUS_SRC_ALPHA);
+ if (useOcclusion) {
+ occlusionShader
+ .setTexture("u_CameraDepthTexture", cameraDepthTexture)
+ .setFloat("u_DepthAspectRatio", aspectRatio);
+ }
+ }
+
+ /**
+ * Updates the display geometry. This must be called every frame before calling either of
+ * BackgroundRenderer's draw methods.
+ *
+ * @param frame The current {@code Frame} as returned by {@link Session#update()}.
+ */
+ public void updateDisplayGeometry(Frame frame) {
+ if (frame.hasDisplayGeometryChanged()) {
+ // If display rotation changed (also includes view size change), we need to re-query the UV
+ // coordinates for the screen rect, as they may have changed as well.
+ frame.transformCoordinates2d(
+ Coordinates2d.OPENGL_NORMALIZED_DEVICE_COORDINATES,
+ NDC_QUAD_COORDS_BUFFER,
+ Coordinates2d.TEXTURE_NORMALIZED,
+ cameraTexCoords);
+ cameraTexCoordsVertexBuffer.set(cameraTexCoords);
+ }
+ }
+
+ /** Update depth texture with Image contents. */
+ public void updateCameraDepthTexture(Image image) {
+ // SampleRender abstraction leaks here
+ GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, cameraDepthTexture.getTextureId());
+ GLES30.glTexImage2D(
+ GLES30.GL_TEXTURE_2D,
+ 0,
+ GLES30.GL_RG8,
+ image.getWidth(),
+ image.getHeight(),
+ 0,
+ GLES30.GL_RG,
+ GLES30.GL_UNSIGNED_BYTE,
+ image.getPlanes()[0].getBuffer());
+ if (useOcclusion) {
+ aspectRatio = (float) image.getWidth() / (float) image.getHeight();
+ occlusionShader.setFloat("u_DepthAspectRatio", aspectRatio);
+ }
+ }
+
+ /**
+ * Draws the AR background image. The image will be drawn such that virtual content rendered with
+ * the matrices provided by {@link com.google.ar.core.Camera#getViewMatrix(float[], int)} and
+ * {@link com.google.ar.core.Camera#getProjectionMatrix(float[], int, float, float)} will
+ * accurately follow static physical objects.
+ */
+ public void drawBackground(SampleRender render) {
+ render.draw(mesh, backgroundShader);
+ }
+
+ /**
+ * Draws the virtual scene. Any objects rendered in the given {@link Framebuffer} will be drawn
+ * given the previously specified {@link OcclusionMode}.
+ *
+ * Virtual content should be rendered using the matrices provided by {@link
+ * com.google.ar.core.Camera#getViewMatrix(float[], int)} and {@link
+ * com.google.ar.core.Camera#getProjectionMatrix(float[], int, float, float)}.
+ */
+ public void drawVirtualScene(
+ SampleRender render, Framebuffer virtualSceneFramebuffer, float zNear, float zFar) {
+ occlusionShader.setTexture(
+ "u_VirtualSceneColorTexture", virtualSceneFramebuffer.getColorTexture());
+ if (useOcclusion) {
+ occlusionShader
+ .setTexture("u_VirtualSceneDepthTexture", virtualSceneFramebuffer.getDepthTexture())
+ .setFloat("u_ZNear", zNear)
+ .setFloat("u_ZFar", zFar);
+ }
+ render.draw(mesh, occlusionShader);
+ }
+
+ /** Return the camera color texture generated by this object. */
+ public Texture getCameraColorTexture() {
+ return cameraColorTexture;
+ }
+
+ /** Return the camera depth texture generated by this object. */
+ public Texture getCameraDepthTexture() {
+ return cameraDepthTexture;
+ }
+}
diff --git a/app/src/main/java/upb/airdocs/hellogeospatial/HelloGeoActivity.kt b/app/src/main/java/upb/airdocs/hellogeospatial/HelloGeoActivity.kt
new file mode 100644
index 0000000..eb2828c
--- /dev/null
+++ b/app/src/main/java/upb/airdocs/hellogeospatial/HelloGeoActivity.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package upb.airdocs.hellogeospatial
+
+import android.content.ServiceConnection
+import android.os.Bundle
+import android.util.Log
+import android.widget.Button
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import com.google.ar.core.Config
+import com.google.ar.core.Session
+import upb.airdocs.hellogeospatial.helpers.ARCoreSessionLifecycleHelper
+import upb.airdocs.hellogeospatial.helpers.GeoPermissionsHelper
+import upb.airdocs.hellogeospatial.helpers.HelloGeoView
+import upb.airdocs.common.helpers.FullScreenHelper
+import upb.airdocs.common.samplerender.SampleRender
+import com.google.ar.core.exceptions.CameraNotAvailableException
+import com.google.ar.core.exceptions.UnavailableApkTooOldException
+import com.google.ar.core.exceptions.UnavailableDeviceNotCompatibleException
+import com.google.ar.core.exceptions.UnavailableSdkTooOldException
+import com.google.ar.core.exceptions.UnavailableUserDeclinedInstallationException
+import upb.airdocs.R
+
+class HelloGeoActivity : AppCompatActivity() {
+ companion object {
+ private const val TAG = "HelloGeoActivity"
+ }
+
+ lateinit var arCoreSessionHelper: ARCoreSessionLifecycleHelper
+ lateinit var view: HelloGeoView
+ lateinit var renderer: HelloGeoRenderer
+// lateinit var postARDoc: Button
+
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // Setup ARCore session lifecycle helper and configuration.
+ arCoreSessionHelper = ARCoreSessionLifecycleHelper(this)
+ // If Session creation or Session.resume() fails, display a message and log detailed
+ // information.
+ arCoreSessionHelper.exceptionCallback =
+ { exception ->
+ val message =
+ when (exception) {
+ is UnavailableUserDeclinedInstallationException ->
+ "Please install Google Play Services for AR"
+ is UnavailableApkTooOldException -> "Please update ARCore"
+ is UnavailableSdkTooOldException -> "Please update this app"
+ is UnavailableDeviceNotCompatibleException -> "This device does not support AR"
+ is CameraNotAvailableException -> "Camera not available. Try restarting the app."
+ else -> "Failed to create AR session: $exception"
+ }
+ Log.e(TAG, "ARCore threw an exception", exception)
+ view.snackbarHelper.showError(this, message)
+ }
+
+ // Configure session features.
+ arCoreSessionHelper.beforeSessionResume = ::configureSession
+ lifecycle.addObserver(arCoreSessionHelper)
+
+// TODO: get documents
+
+
+
+ // Set up the Hello AR renderer.
+ renderer = HelloGeoRenderer(this)
+ lifecycle.addObserver(renderer)
+
+ // Set up Hello AR UI.
+ view = HelloGeoView(this)
+ lifecycle.addObserver(view)
+ setContentView(view.root)
+
+ // Sets up an example renderer using our HelloGeoRenderer.
+ SampleRender(view.surfaceView, renderer, assets)
+ }
+
+ // Configure the session, setting the desired options according to your usecase.
+ fun configureSession(session: Session) {
+ session.configure(
+ session.config.apply {
+ // Enable Geospatial Mode.
+ geospatialMode = Config.GeospatialMode.ENABLED
+ }
+ )
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ results: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, results)
+ if (!GeoPermissionsHelper.hasGeoPermissions(this)) {
+ // Use toast instead of snackbar here since the activity will exit.
+ Toast.makeText(this, "Camera and location permissions are needed to run this application", Toast.LENGTH_LONG)
+ .show()
+ if (!GeoPermissionsHelper.shouldShowRequestPermissionRationale(this)) {
+ // Permission denied with checking "Do not ask again".
+ GeoPermissionsHelper.launchPermissionSettings(this)
+ }
+ finish()
+ }
+ }
+
+ override fun onWindowFocusChanged(hasFocus: Boolean) {
+ super.onWindowFocusChanged(hasFocus)
+ FullScreenHelper.setFullScreenOnWindowFocusChanged(this, hasFocus)
+ }
+}
diff --git a/app/src/main/java/upb/airdocs/hellogeospatial/HelloGeoRenderer.kt b/app/src/main/java/upb/airdocs/hellogeospatial/HelloGeoRenderer.kt
new file mode 100644
index 0000000..a7b013a
--- /dev/null
+++ b/app/src/main/java/upb/airdocs/hellogeospatial/HelloGeoRenderer.kt
@@ -0,0 +1,332 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package upb.airdocs.hellogeospatial
+
+import android.location.Location
+import android.opengl.Matrix
+import android.util.Log
+import android.widget.Toast
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import com.google.android.gms.maps.model.LatLng
+import com.google.ar.core.*
+import com.google.ar.core.exceptions.CameraNotAvailableException
+import upb.airdocs.Document
+import upb.airdocs.common.helpers.DisplayRotationHelper
+import upb.airdocs.common.helpers.TrackingStateHelper
+import upb.airdocs.common.samplerender.*
+import upb.airdocs.common.samplerender.Mesh
+import upb.airdocs.common.samplerender.arcore.BackgroundRenderer
+import java.io.IOException
+import kotlin.math.abs
+import kotlin.math.absoluteValue
+
+
+class HelloGeoRenderer(val activity: HelloGeoActivity) :
+ SampleRender.Renderer, DefaultLifecycleObserver {
+ //
+ companion object {
+ val TAG = "HelloGeoRenderer"
+
+ private val Z_NEAR = 0.1f
+ private val Z_FAR = 1000f
+ var staticLatitude = 0.0;
+ var staticLongitude = 0.0;
+ var staticAltitude = 0.0;
+ var future : VpsAvailabilityFuture? = null
+ var vpsDone = false;
+ var vpsString = "";
+
+ }
+
+ lateinit var backgroundRenderer: BackgroundRenderer
+ lateinit var virtualSceneFramebuffer: Framebuffer
+ var hasSetTextureNames = false
+ private val earthAnchors = ArrayList()
+
+ // Virtual object (ARCore pawn)
+ lateinit var virtualObjectMesh: Mesh
+ lateinit var virtualObjectShader: Shader
+ lateinit var virtualObjectTexture: Texture
+
+ // Temporary matrix allocated here to reduce number of allocations for each frame.
+ val modelMatrix = FloatArray(16)
+ val viewMatrix = FloatArray(16)
+ val projectionMatrix = FloatArray(16)
+ val modelViewMatrix = FloatArray(16) // view x model
+
+
+ val modelViewProjectionMatrix = FloatArray(16) // projection x view x model
+
+ val session
+ get() = activity.arCoreSessionHelper.session
+
+ val displayRotationHelper = DisplayRotationHelper(activity)
+ val trackingStateHelper = TrackingStateHelper(activity)
+
+ override fun onResume(owner: LifecycleOwner) {
+ displayRotationHelper.onResume()
+ hasSetTextureNames = false
+ }
+
+ override fun onPause(owner: LifecycleOwner) {
+ displayRotationHelper.onPause()
+ }
+
+ override fun onSurfaceCreated(render: SampleRender) {
+ println("onSurfaceCreated")
+ // Prepare the rendering objects.
+ // This involves reading shaders and 3D model files, so may throw an IOException.
+ try {
+ backgroundRenderer = BackgroundRenderer(render)
+ virtualSceneFramebuffer = Framebuffer(render, /*width=*/ 1, /*height=*/ 1)
+
+ // Virtual object to render (Geospatial Marker)
+ virtualObjectTexture =
+ Texture.createFromAsset(
+ render,
+ "models/spatial_marker_baked.png",
+ Texture.WrapMode.CLAMP_TO_EDGE,
+ Texture.ColorFormat.SRGB
+ )
+
+ virtualObjectMesh = Mesh.createFromAsset(render, "models/geospatial_marker.obj");
+ virtualObjectShader =
+ Shader.createFromAssets(
+ render,
+ "shaders/ar_unlit_object.vert",
+ "shaders/ar_unlit_object.frag",
+ /*defines=*/ null)
+ .setTexture("u_Texture", virtualObjectTexture)
+
+ backgroundRenderer.setUseDepthVisualization(render, false) // TODO??
+ backgroundRenderer.setUseOcclusion(render, false) // TODO set to true for occlusion
+ } catch (e: IOException) {
+ Log.e(TAG, "Failed to read a required asset file", e)
+ showError("Failed to read a required asset file: $e")
+ }
+// onMapClick(LatLng(44.434958, 26.047685))
+ }
+
+ override fun onSurfaceChanged(render: SampleRender, width: Int, height: Int) {
+ displayRotationHelper.onSurfaceChanged(width, height)
+ virtualSceneFramebuffer.resize(width, height)
+ }
+ //
+
+
+ override fun onDrawFrame(render: SampleRender) {
+ val session = session ?: return
+
+
+
+
+
+ //
+ // Texture names should only be set once on a GL thread unless they change. This is done during
+ // onDrawFrame rather than onSurfaceCreated since the session is not guaranteed to have been
+ // initialized during the execution of onSurfaceCreated.
+ if (!hasSetTextureNames) {
+ session.setCameraTextureNames(intArrayOf(backgroundRenderer.cameraColorTexture.textureId))
+ hasSetTextureNames = true
+ }
+
+ // -- Update per-frame state
+
+ // Notify ARCore session that the view size changed so that the perspective matrix and
+ // the video background can be properly adjusted.
+ displayRotationHelper.updateSessionIfNeeded(session)
+
+ // Obtain the current frame from ARSession. When the configuration is set to
+ // UpdateMode.BLOCKING (it is by default), this will throttle the rendering to the
+ // camera framerate.
+ val frame =
+ try {
+ session.update()
+ } catch (e: CameraNotAvailableException) {
+ Log.e(TAG, "Camera not available during onDrawFrame", e)
+ showError("Camera not available. Try restarting the app.")
+ return
+ }
+
+ val camera = frame.camera
+
+ // BackgroundRenderer.updateDisplayGeometry must be called every frame to update the coordinates
+ // used to draw the background camera image.
+ backgroundRenderer.updateDisplayGeometry(frame)
+
+ // Keep the screen unlocked while tracking, but allow it to lock when tracking stops.
+ trackingStateHelper.updateKeepScreenOnFlag(camera.trackingState)
+
+ // -- Draw background
+ if (frame.timestamp != 0L) {
+ // Suppress rendering if the camera did not produce the first frame yet. This is to avoid
+ // drawing possible leftover data from previous sessions if the texture is reused.
+ backgroundRenderer.drawBackground(render)
+ }
+
+ // If not tracking, don't draw 3D objects.
+ if (camera.trackingState == TrackingState.PAUSED) {
+ return
+ }
+
+ // Get projection matrix.
+ camera.getProjectionMatrix(projectionMatrix, 0, Z_NEAR, Z_FAR)
+
+ // Get camera matrix and draw.
+ camera.getViewMatrix(viewMatrix, 0)
+
+ render.clear(virtualSceneFramebuffer, 0f, 0f, 0f, 0f)
+ //
+
+
+
+ // TODO: Obtain Geospatial information and display it on the map.
+ val earth = session.earth
+
+ if (earth?.trackingState != TrackingState.TRACKING) {
+ println("VLAD Earth not tracking: ${earth?.trackingState}")
+ return
+ }
+ // TODO: the Earth object may be used here.
+ val cameraGeospatialPose = earth.cameraGeospatialPose
+ activity.view.mapView?.updateMapPosition(
+ latitude = cameraGeospatialPose.latitude,
+ longitude = cameraGeospatialPose.longitude,
+ heading = cameraGeospatialPose.heading
+ )
+
+ staticLatitude = cameraGeospatialPose.latitude
+ staticLongitude = cameraGeospatialPose.longitude
+ staticAltitude = cameraGeospatialPose.altitude
+ activity.view.updateStatusText(earth, earth.cameraGeospatialPose, vpsString)
+
+
+ // Obtain a VpsAvailabilityFuture and store it somewhere.
+ if (future == null) {
+ future = session.checkVpsAvailabilityAsync(staticLatitude, staticLongitude, null)
+ }
+//
+ // Poll VpsAvailabilityFuture later, for example, in a render loop.
+ if (future!!.state != FutureState.DONE && !vpsDone) {
+ println("VLAD: NOT DONE. ${future!!.state} Returning...")
+ return
+ }
+ if (!vpsDone) {
+ when (future!!.result) {
+ VpsAvailability.AVAILABLE -> {
+ // VPS is available at this location.
+ println("VLAD: VPS available")
+ activity.runOnUiThread { Toast.makeText(activity, "VPS Available", Toast.LENGTH_SHORT).show() }
+ vpsString = "Available"
+ }
+ VpsAvailability.UNAVAILABLE -> {
+ // VPS is unavailable at this location.
+ activity.runOnUiThread { Toast.makeText(activity, "VPS Unavailable", Toast.LENGTH_SHORT).show() }
+ println("VLAD: VPS unavailable")
+ vpsString = "Unavailable"
+ }
+ VpsAvailability.ERROR_NETWORK_CONNECTION -> {
+ // The external service could not be reached due to a network connection error.
+ activity.runOnUiThread { Toast.makeText(activity, "VPS ERROR_NETWORK_CONNECTION", Toast.LENGTH_SHORT).show() }
+ println("VLAD: error network conn")
+ vpsString = "Error network connection"
+ }
+ else -> {
+ activity.runOnUiThread { Toast.makeText(activity, "VPS ELSE???", Toast.LENGTH_SHORT).show() }
+ println("VLAD: else??")
+ vpsString = "Error"
+ }
+ }
+ vpsDone = true
+ }
+
+ var toViewDoc: Document? = null;
+ activity.view.documents?.forEach { document ->
+// TODO: document.altitude
+ val anchor = onMapClick(LatLng(document.latitude, document.longitude), document.altitude) ?: return
+ render.renderCompassAtAnchor(anchor)
+
+ val currLocation = Location("")
+ currLocation.latitude = staticLatitude
+ currLocation.longitude = staticLongitude
+
+ val anchorLocation = Location("")
+ anchorLocation.latitude = document.latitude
+ anchorLocation.longitude = document.longitude
+
+ if (currLocation.distanceTo(anchorLocation) < 5) {
+ toViewDoc = document
+ }
+ }
+
+ if (toViewDoc != null) {
+ activity.view.updateViewDocument(toViewDoc!!)
+ } else {
+ activity.view.clearViewDocument()
+ }
+
+ // Compose the virtual scene with the background.
+ backgroundRenderer.drawVirtualScene(render, virtualSceneFramebuffer, Z_NEAR, Z_FAR)
+ }
+
+
+ fun onMapClick(latLng: LatLng, alt: Double): Anchor? {
+ // TODO: place an anchor at the given position.
+ val earth = session?.earth ?: return null
+ if (earth.trackingState != TrackingState.TRACKING) {
+ return null
+ }
+// earthAnchor?.detach()
+ // Place the earth anchor at the same altitude as that of the camera to make it easier to view.
+// TODO: if altitude within [-3,+3m] -> then move closer
+
+ var altitude = alt
+// if (abs(earth.cameraGeospatialPose.altitude - alt) < 3) {
+// altitude = earth.cameraGeospatialPose.altitude
+// }
+
+ var quats = earth.cameraGeospatialPose.eastUpSouthQuaternion
+ // The rotation quaternion of the anchor in the East-Up-South (EUS) coordinate system.
+ val qx = 0f
+ val qy = 0f
+ val qz = 0f
+ val qw = 1f
+ activity.view.mapView?.earthMarker?.apply {
+ position = latLng
+ isVisible = true
+ }
+ return earth.createAnchor(latLng.latitude, latLng.longitude, altitude, quats[0], quats[1], quats[2], quats[3])
+ }
+
+ private fun SampleRender.renderCompassAtAnchor(anchor: Anchor) {
+ // Get the current pose of the Anchor in world space. The Anchor pose is updated
+ // during calls to session.update() as ARCore refines its estimate of the world.
+ anchor.pose.toMatrix(modelMatrix, 0)
+
+ // Calculate model/view/projection matrices
+ Matrix.multiplyMM(modelViewMatrix, 0, viewMatrix, 0, modelMatrix, 0)
+ Matrix.multiplyMM(modelViewProjectionMatrix, 0, projectionMatrix, 0, modelViewMatrix, 0)
+
+ // Update shader properties and draw
+ virtualObjectShader.setMat4("u_ModelViewProjection", modelViewProjectionMatrix)
+ // actual shader
+ draw(virtualObjectMesh, virtualObjectShader, virtualSceneFramebuffer)
+ }
+
+ private fun showError(errorMessage: String) =
+ activity.view.snackbarHelper.showError(activity, errorMessage)
+}
diff --git a/app/src/main/java/upb/airdocs/hellogeospatial/helpers/ARCoreSessionLifecycleHelper.kt b/app/src/main/java/upb/airdocs/hellogeospatial/helpers/ARCoreSessionLifecycleHelper.kt
new file mode 100644
index 0000000..f4e3c75
--- /dev/null
+++ b/app/src/main/java/upb/airdocs/hellogeospatial/helpers/ARCoreSessionLifecycleHelper.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package upb.airdocs.hellogeospatial.helpers
+
+import android.app.Activity
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import com.google.ar.core.ArCoreApk
+import com.google.ar.core.Session
+import com.google.ar.core.exceptions.CameraNotAvailableException
+
+/**
+ * Manages an ARCore Session using the Android Lifecycle API. Before starting a Session, this class
+ * requests installation of Google Play Services for AR if it's not installed or not up to date and
+ * asks the user for required permissions if necessary.
+ */
+class ARCoreSessionLifecycleHelper(
+ val activity: Activity,
+ val features: Set = setOf()
+) : DefaultLifecycleObserver {
+ var installRequested = false
+ var session: Session? = null
+ private set
+
+ /**
+ * Creating a session may fail. In this case, session will remain null, and this function will be
+ * called with an exception.
+ *
+ * See
+ * [the `Session` constructor](https://developers.google.com/ar/reference/java/com/google/ar/core/Session#Session(android.content.Context)
+ * ) for more details.
+ */
+ var exceptionCallback: ((Exception) -> Unit)? = null
+
+ /**
+ * Before `Session.resume()` is called, a session must be configured. Use
+ * [`Session.configure`](https://developers.google.com/ar/reference/java/com/google/ar/core/Session#configure-config)
+ * or
+ * [`setCameraConfig`](https://developers.google.com/ar/reference/java/com/google/ar/core/Session#setCameraConfig-cameraConfig)
+ */
+ var beforeSessionResume: ((Session) -> Unit)? = null
+
+ /**
+ * Attempts to create a session. If Google Play Services for AR is not installed or not up to
+ * date, request installation.
+ *
+ * @return null when the session cannot be created due to a lack of the CAMERA permission or when
+ * Google Play Services for AR is not installed or up to date, or when session creation fails for
+ * any reason. In the case of a failure, [exceptionCallback] is invoked with the failure
+ * exception.
+ */
+ private fun tryCreateSession(): Session? {
+ // The app must have been given the CAMERA permission. If we don't have it yet, request it.
+ if (!GeoPermissionsHelper.hasGeoPermissions(activity)) {
+ GeoPermissionsHelper.requestPermissions(activity)
+ return null
+ }
+
+ return try {
+ // Request installation if necessary.
+ when (ArCoreApk.getInstance().requestInstall(activity, !installRequested)!!) {
+ ArCoreApk.InstallStatus.INSTALL_REQUESTED -> {
+ installRequested = true
+ // tryCreateSession will be called again, so we return null for now.
+ return null
+ }
+ ArCoreApk.InstallStatus.INSTALLED -> {
+ // Left empty; nothing needs to be done.
+ }
+ }
+
+ // Create a session if Google Play Services for AR is installed and up to date.
+ Session(activity, features)
+ } catch (e: Exception) {
+ exceptionCallback?.invoke(e)
+ null
+ }
+ }
+
+ override fun onResume(owner: LifecycleOwner) {
+ val session = this.session ?: tryCreateSession() ?: return
+ try {
+ beforeSessionResume?.invoke(session)
+ session.resume()
+ this.session = session
+ } catch (e: CameraNotAvailableException) {
+ exceptionCallback?.invoke(e)
+ }
+ }
+
+ override fun onPause(owner: LifecycleOwner) {
+ session?.pause()
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ // Explicitly close the ARCore session to release native resources.
+ // Review the API reference for important considerations before calling close() in apps with
+ // more complicated lifecycle requirements:
+ // https://developers.google.com/ar/reference/java/arcore/reference/com/google/ar/core/Session#close()
+ session?.close()
+ session = null
+ }
+}
diff --git a/app/src/main/java/upb/airdocs/hellogeospatial/helpers/GeoPermissionsHelper.kt b/app/src/main/java/upb/airdocs/hellogeospatial/helpers/GeoPermissionsHelper.kt
new file mode 100644
index 0000000..0e6d9a4
--- /dev/null
+++ b/app/src/main/java/upb/airdocs/hellogeospatial/helpers/GeoPermissionsHelper.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package upb.airdocs.hellogeospatial.helpers
+
+import android.Manifest
+import android.app.Activity
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.provider.Settings
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+
+/** Helper to ask camera permission. */
+object GeoPermissionsHelper {
+ private val PERMISSIONS = arrayOf(Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION)
+
+ /** Check to see we have the necessary permissions for this app. */
+ fun hasGeoPermissions(activity: Activity): Boolean {
+ return PERMISSIONS.all {
+ ContextCompat.checkSelfPermission(activity, it) == PackageManager.PERMISSION_GRANTED
+ }
+ }
+
+ /** Check to see we have the necessary permissions for this app, and ask for them if we don't. */
+ fun requestPermissions(activity: Activity?) {
+ ActivityCompat.requestPermissions(
+ activity!!, PERMISSIONS, 0)
+ }
+
+ /** Check to see if we need to show the rationale for this permission. */
+ fun shouldShowRequestPermissionRationale(activity: Activity): Boolean {
+ return PERMISSIONS.any { ActivityCompat.shouldShowRequestPermissionRationale(activity, it) }
+ }
+
+ /** Launch Application Setting to grant permission. */
+ fun launchPermissionSettings(activity: Activity) {
+ val intent = Intent()
+ intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
+ intent.data = Uri.fromParts("package", activity.packageName, null)
+ activity.startActivity(intent)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/upb/airdocs/hellogeospatial/helpers/HelloGeoView.kt b/app/src/main/java/upb/airdocs/hellogeospatial/helpers/HelloGeoView.kt
new file mode 100644
index 0000000..6e79927
--- /dev/null
+++ b/app/src/main/java/upb/airdocs/hellogeospatial/helpers/HelloGeoView.kt
@@ -0,0 +1,297 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package upb.airdocs.hellogeospatial.helpers
+
+import android.content.*
+import android.opengl.GLSurfaceView
+import android.os.*
+import android.util.Log
+import android.view.View
+import android.widget.Button
+import android.widget.TextView
+import android.widget.Toast
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import com.google.ar.core.Earth
+import com.google.ar.core.GeospatialPose
+import com.indooratlas.android.sdk.*
+import upb.airdocs.Document
+import upb.airdocs.PostDocumentActivity
+import upb.airdocs.R
+import upb.airdocs.ScanService
+import upb.airdocs.common.helpers.SnackbarHelper
+import upb.airdocs.hellogeospatial.HelloGeoActivity
+import upb.airdocs.hellogeospatial.HelloGeoRenderer
+
+/** Contains UI elements for Hello Geo. */
+class HelloGeoView(val activity: HelloGeoActivity) : DefaultLifecycleObserver, IALocationListener,
+ IARegion.Listener {
+ val root = View.inflate(activity, R.layout.activity_aractivity, null)
+ val surfaceView = root.findViewById(R.id.surfaceview)
+ val getARDocs = root.findViewById
192.168.142.123
preference_file
+
+ LAT/LNG: %.6f˚, %.6f˚\n\t\t\tACCURACY: %.2fm\nALTITUDE: %.2fm\n\t\t\tACCURACY: %.2fm\nHEADING: %.1f˚\n\t\t\tACCURACY: %.1f˚
+ EarthState: %1$s\nTrackingState: %2$s\nVPS: %3$s\n%4$s
diff --git a/build.gradle b/build.gradle
index 2426268..5a58ead 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,13 +1,14 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
-
+ ext.kotlin_version = '1.4.20'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.1'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong