1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org
.chromium
.chromoting
;
7 import android
.app
.Activity
;
8 import android
.graphics
.Bitmap
;
9 import android
.graphics
.Point
;
10 import android
.graphics
.PointF
;
11 import android
.opengl
.GLES20
;
12 import android
.opengl
.Matrix
;
14 import com
.google
.vrtoolkit
.cardboard
.CardboardView
;
15 import com
.google
.vrtoolkit
.cardboard
.Eye
;
16 import com
.google
.vrtoolkit
.cardboard
.HeadTransform
;
17 import com
.google
.vrtoolkit
.cardboard
.Viewport
;
19 import org
.chromium
.chromoting
.jni
.JniInterface
;
21 import java
.nio
.ByteBuffer
;
22 import java
.nio
.ByteOrder
;
23 import java
.nio
.FloatBuffer
;
25 import javax
.microedition
.khronos
.egl
.EGLConfig
;
28 * Renderer for Cardboard view.
30 public class CardboardDesktopRenderer
implements CardboardView
.StereoRenderer
{
31 private static final String TAG
= "cr.CardboardRenderer";
33 private static final int BYTE_PER_FLOAT
= 4;
34 private static final int POSITION_DATA_SIZE
= 3;
35 private static final int TEXTURE_COORDINATE_DATA_SIZE
= 2;
36 private static final float Z_NEAR
= 0.1f
;
37 private static final float Z_FAR
= 100.0f
;
38 private static final float DESKTOP_POSITION_X
= 0.0f
;
39 private static final float DESKTOP_POSITION_Y
= 0.0f
;
40 private static final float DESKTOP_POSITION_Z
= -2.0f
;
41 private static final float HALF_SKYBOX_SIZE
= 100.0f
;
42 private static final float VIEW_POSITION_MIN
= -1.0f
;
43 private static final float VIEW_POSITION_MAX
= 3.0f
;
45 // Allows user to click even when looking outside the desktop
46 // but within edge margin.
47 private static final float EDGE_MARGIN
= 0.1f
;
49 // Distance to move camera each time.
50 private static final float CAMERA_MOTION_STEP
= 0.5f
;
52 private final Activity mActivity
;
54 private float mCameraPosition
;
56 // Lock to allow multithreaded access to mCameraPosition.
57 private final Object mCameraPositionLock
= new Object();
59 private float[] mCameraMatrix
;
60 private float[] mViewMatrix
;
61 private float[] mProjectionMatrix
;
63 // Make matrix member variable to avoid unnecessary initialization.
64 private float[] mDesktopModelMatrix
;
65 private float[] mDesktopCombinedMatrix
;
66 private float[] mEyePointModelMatrix
;
67 private float[] mEyePointCombinedMatrix
;
68 private float[] mSkyboxCombinedMatrix
;
70 // Direction that user is looking towards.
71 private float[] mForwardVector
;
73 // Eye position in desktop.
74 private float[] mEyePositionVector
;
76 private CardboardActivityDesktop mDesktop
;
77 private CardboardActivityEyePoint mEyePoint
;
78 private CardboardActivitySkybox mSkybox
;
80 // Flag to indicate whether reload the desktop texture or not.
81 private boolean mReloadTexture
;
83 /** Lock to allow multithreaded access to mReloadTexture. */
84 private final Object mReloadTextureLock
= new Object();
86 // Lock for eye position related operations.
87 // This protects access to mEyePositionVector.
88 private final Object mEyePositionLock
= new Object();
90 public CardboardDesktopRenderer(Activity activity
) {
92 mReloadTexture
= false;
93 mCameraPosition
= 0.0f
;
95 mCameraMatrix
= new float[16];
96 mViewMatrix
= new float[16];
97 mProjectionMatrix
= new float[16];
98 mDesktopModelMatrix
= new float[16];
99 mDesktopCombinedMatrix
= new float[16];
100 mEyePointModelMatrix
= new float[16];
101 mEyePointCombinedMatrix
= new float[16];
102 mSkyboxCombinedMatrix
= new float[16];
104 mForwardVector
= new float[3];
105 mEyePositionVector
= new float[3];
107 attachRedrawCallback();
110 // This can be called on any thread.
111 public void attachRedrawCallback() {
112 JniInterface
.provideRedrawCallback(new Runnable() {
115 synchronized (mReloadTextureLock
) {
116 mReloadTexture
= true;
123 public void onSurfaceCreated(EGLConfig config
) {
124 // Set the background clear color to black.
125 GLES20
.glClearColor(0.0f
, 0.0f
, 0.0f
, 0.0f
);
127 // Use culling to remove back faces.
128 GLES20
.glEnable(GLES20
.GL_CULL_FACE
);
130 // Enable depth testing.
131 GLES20
.glEnable(GLES20
.GL_DEPTH_TEST
);
133 mDesktop
= new CardboardActivityDesktop();
134 mEyePoint
= new CardboardActivityEyePoint();
135 mSkybox
= new CardboardActivitySkybox(mActivity
);
139 public void onSurfaceChanged(int width
, int height
) {
143 public void onNewFrame(HeadTransform headTransform
) {
144 // Position the eye at the origin.
148 synchronized (mCameraPositionLock
) {
149 eyeZ
= mCameraPosition
;
152 // We are looking toward the negative Z direction.
153 float lookX
= DESKTOP_POSITION_X
;
154 float lookY
= DESKTOP_POSITION_Y
;
155 float lookZ
= DESKTOP_POSITION_Z
;
157 // Set our up vector. This is where our head would be pointing were we holding the camera.
162 Matrix
.setLookAtM(mCameraMatrix
, 0, eyeX
, eyeY
, eyeZ
, lookX
, lookY
, lookZ
, upX
, upY
, upZ
);
164 headTransform
.getForwardVector(mForwardVector
, 0);
165 getLookingPosition();
166 maybeLoadDesktopTexture();
167 mSkybox
.maybeLoadTextureAndCleanImages();
171 public void onDrawEye(Eye eye
) {
172 GLES20
.glClear(GLES20
.GL_COLOR_BUFFER_BIT
| GLES20
.GL_DEPTH_BUFFER_BIT
);
174 // Apply the eye transformation to the camera.
175 Matrix
.multiplyMM(mViewMatrix
, 0, eye
.getEyeView(), 0, mCameraMatrix
, 0);
177 mProjectionMatrix
= eye
.getPerspective(Z_NEAR
, Z_FAR
);
185 public void onRendererShutdown() {
192 public void onFinishFrame(Viewport viewport
) {
195 private void drawDesktop() {
196 if (!mDesktop
.hasVideoFrame()) {
197 // This can happen if the client is connected, but a complete
198 // video frame has not yet been decoded.
202 Matrix
.setIdentityM(mDesktopModelMatrix
, 0);
203 Matrix
.translateM(mDesktopModelMatrix
, 0, DESKTOP_POSITION_X
,
204 DESKTOP_POSITION_Y
, DESKTOP_POSITION_Z
);
206 // Pass in Model View Matrix and Model View Project Matrix.
207 Matrix
.multiplyMM(mDesktopCombinedMatrix
, 0, mViewMatrix
, 0, mDesktopModelMatrix
, 0);
208 Matrix
.multiplyMM(mDesktopCombinedMatrix
, 0, mProjectionMatrix
,
209 0, mDesktopCombinedMatrix
, 0);
210 mDesktop
.setCombinedMatrix(mDesktopCombinedMatrix
);
215 private void drawEyePoint() {
216 if (!isLookingAtDesktop()) {
220 float eyePointX
= clamp(mEyePositionVector
[0], -mDesktop
.getHalfWidth(),
221 mDesktop
.getHalfWidth());
222 float eyePointY
= clamp(mEyePositionVector
[1], -mDesktop
.getHalfHeight(),
223 mDesktop
.getHalfHeight());
224 Matrix
.setIdentityM(mEyePointModelMatrix
, 0);
225 Matrix
.translateM(mEyePointModelMatrix
, 0, -eyePointX
, -eyePointY
,
227 Matrix
.multiplyMM(mEyePointCombinedMatrix
, 0, mViewMatrix
, 0, mEyePointModelMatrix
, 0);
228 Matrix
.multiplyMM(mEyePointCombinedMatrix
, 0, mProjectionMatrix
,
229 0, mEyePointCombinedMatrix
, 0);
231 mEyePoint
.setCombinedMatrix(mEyePointCombinedMatrix
);
235 private void drawSkybox() {
236 // Since we will always put the skybox center in the origin, so skybox
237 // model matrix will always be identity matrix which we could ignore.
238 Matrix
.multiplyMM(mSkyboxCombinedMatrix
, 0, mProjectionMatrix
,
240 mSkybox
.draw(mSkyboxCombinedMatrix
);
244 * Returns coordinates in units of pixels in the desktop bitmap.
245 * This can be called on any thread.
247 public PointF
getMouseCoordinates() {
248 PointF result
= new PointF();
249 Point shapePixels
= mDesktop
.getFrameSizePixels();
250 int heightPixels
= shapePixels
.x
;
251 int widthPixels
= shapePixels
.y
;
253 synchronized (mEyePositionLock
) {
254 // Due to the coordinate direction, we only have to inverse x.
255 result
.x
= (-mEyePositionVector
[0] + mDesktop
.getHalfWidth())
256 / (2 * mDesktop
.getHalfWidth()) * widthPixels
;
257 result
.y
= (mEyePositionVector
[1] + mDesktop
.getHalfHeight())
258 / (2 * mDesktop
.getHalfHeight()) * heightPixels
;
259 result
.x
= clamp(result
.x
, 0, widthPixels
);
260 result
.y
= clamp(result
.y
, 0, heightPixels
);
266 * Returns the passed in value if it resides within the specified range (inclusive). If not,
267 * it will return the closest boundary from the range. The ordering of the boundary values
270 * @param value The value to be compared against the range.
271 * @param a First boundary range value.
272 * @param b Second boundary range value.
273 * @return The passed in value if it is within the range, otherwise the closest boundary value.
275 private static float clamp(float value
, float a
, float b
) {
276 float min
= (a
> b
) ? b
: a
;
277 float max
= (a
> b
) ? a
: b
;
280 } else if (value
> max
) {
287 * Move the camera towards desktop.
288 * This method can be called on any thread.
290 public void moveTowardsDesktop() {
291 synchronized (mCameraPositionLock
) {
292 float newPosition
= mCameraPosition
- CAMERA_MOTION_STEP
;
293 if (newPosition
>= VIEW_POSITION_MIN
) {
294 mCameraPosition
= newPosition
;
300 * Move the camera away from desktop.
301 * This method can be called on any thread.
303 public void moveAwayFromDesktop() {
304 synchronized (mCameraPositionLock
) {
305 float newPosition
= mCameraPosition
+ CAMERA_MOTION_STEP
;
306 if (newPosition
<= VIEW_POSITION_MAX
) {
307 mCameraPosition
= newPosition
;
313 * Return true if user is looking at the desktop.
314 * This method can be called on any thread.
316 public boolean isLookingAtDesktop() {
317 synchronized (mEyePositionLock
) {
318 return Math
.abs(mEyePositionVector
[0]) <= (mDesktop
.getHalfWidth() + EDGE_MARGIN
)
319 && Math
.abs(mEyePositionVector
[1]) <= (mDesktop
.getHalfHeight() + EDGE_MARGIN
);
324 * Return true if user is looking at the space to the left of the desktop.
325 * This method can be called on any thread.
327 public boolean isLookingLeftOfDesktop() {
328 synchronized (mEyePositionLock
) {
329 return mEyePositionVector
[0] >= (mDesktop
.getHalfWidth() + EDGE_MARGIN
);
334 * Return true if user is looking at the space to the right of the desktop.
335 * This method can be called on any thread.
337 public boolean isLookingRightOfDesktop() {
338 synchronized (mEyePositionLock
) {
339 return mEyePositionVector
[0] <= -(mDesktop
.getHalfWidth() + EDGE_MARGIN
);
344 * Return true if user is looking at the space above the desktop.
345 * This method can be called on any thread.
347 public boolean isLookingAboveDesktop() {
348 synchronized (mEyePositionLock
) {
349 return mEyePositionVector
[1] <= -(mDesktop
.getHalfHeight() + EDGE_MARGIN
);
354 * Return true if user is looking at the space below the desktop.
355 * This method can be called on any thread.
357 public boolean isLookingBelowDesktop() {
358 synchronized (mEyePositionLock
) {
359 return mEyePositionVector
[1] >= (mDesktop
.getHalfHeight() + EDGE_MARGIN
);
364 * Get position on desktop where user is looking at.
366 private void getLookingPosition() {
367 synchronized (mEyePositionLock
) {
368 if (Math
.abs(mForwardVector
[2]) < 0.00001f
) {
369 mEyePositionVector
[0] = Math
.signum(mForwardVector
[0]) * Float
.MAX_VALUE
;
370 mEyePositionVector
[1] = Math
.signum(mForwardVector
[1]) * Float
.MAX_VALUE
;
372 mEyePositionVector
[0] = mForwardVector
[0] * DESKTOP_POSITION_Z
/ mForwardVector
[2];
373 mEyePositionVector
[1] = mForwardVector
[1] * DESKTOP_POSITION_Z
/ mForwardVector
[2];
375 mEyePositionVector
[2] = DESKTOP_POSITION_Z
;
380 * Link desktop texture with {@link CardboardActivityDesktop} if {@link mReloadTexture} is true.
381 * @param textureDataHandle the handle we want attach texture to
383 private void maybeLoadDesktopTexture() {
384 synchronized (mReloadTextureLock
) {
385 if (!mReloadTexture
) {
390 // TODO(shichengfeng): Record the time desktop drawing takes.
391 Bitmap bitmap
= JniInterface
.getVideoFrame();
393 if (bitmap
== null) {
394 // This can happen if the client is connected, but a complete video frame has not yet
399 mDesktop
.updateVideoFrame(bitmap
);
401 synchronized (mReloadTextureLock
) {
402 mReloadTexture
= false;
407 * Convert float array to a FloatBuffer for use in OpenGL calls.
409 public static FloatBuffer
makeFloatBuffer(float[] data
) {
410 FloatBuffer result
= ByteBuffer
411 .allocateDirect(data
.length
* BYTE_PER_FLOAT
)
412 .order(ByteOrder
.nativeOrder()).asFloatBuffer();
413 result
.put(data
).position(0);