As soon as Google announced possibility to create custom watch faces with new Android 5.0 for Android Wear, we’ve ordered brand new ASUS ZenWatch to develop watch face (I believe nothing can beat quality of this device for its price). We decided not to port any of existing live wallpapers to Android Wear but to create some completely new scene for it. This resulted in creating Axiom Watch Faces app which consists of 5 digital watch faces, implemented in 3D with OpenGL ES 2.0.
Original concept
First, watch face was inspired by “Numbers” video by beeple - https://vimeo.com/18876537. It looks pretty cool and doesn’t seem to be very complicated at a first glance. However, we’ve found out that there are some significant technical limitations which prevented us from creating this scene as seen in video. It is not that easy to implement such wireframe numbers which at the same time correctly occludes itself. In short, it will require too much draw calls to draw such seemingly simple scene, and quite limited hardware of watches may not be capable of handling that much draw calls. So we had to develop some other concept which resulted in idea of adding depth to pixel fonts from old computers with low-res graphics.
OpenGL ES config w/ missing depth component
Gles2WatchFaceService from Android Wear API doesn’t provide access to all features needed to create a 3D watch face. The major problem we’ve encountered is that it doesn’t allow you to pick a desired OpenGL ES config. In fact, it provides no access to EGL at all. It is enough to run 'Tilt' sample watch face from official examples without any flexibility in mind - that example has no overlapping geometry and thus doesn’t need z-buffer at all. So Gles2WatchFaceService chooses EGL config without depth buffer - EGL_DEPTH_SIZE is simply not specified in it, and there's no way to specify own EGL config too.
Because of this limitation we had to decompile its source code and create custom implementation of Gles2WatchFaceService which fills in EGL config with necessary values.
Also other well-known developer of live wallpapers and watch faces Kittehface Software reported that Moto360 doesn’t work with valid 16-bit color so we use failsafe 32-bit only for all devices. We express a huge gratitude to Kittehface for explaining this bug and saving possibly days of fixing it for us and other developers.
We won't provide full decompiled code of Gles2WatchFaceService here because its code can be changed in any upcoming update of API. Here are all changes we've made to decompiled Gles2WatchFaceService:
1. Update EGL config to request depth component:
private static final int[] EGL_CONFIG_ATTRIB_LIST = new int[]{
EGL14.EGL_RENDERABLE_TYPE, 4,
EGL14.EGL_RED_SIZE, 8,
EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_ALPHA_SIZE, 8,
EGL14.EGL_DEPTH_SIZE, 16, // this was missing
EGL14.EGL_NONE};
2. Make mInsetBottom and mInsetLeft variables accessible from subclasses. These values will be used to properly update viewport. Either add getters or make them protected:
protected int mInsetBottom = 0;
protected int mInsetLeft = 0;
We won't provide full decompiled code of Gles2WatchFaceService here because its code can be changed in any upcoming update of API. Here are all changes we've made to decompiled Gles2WatchFaceService:
1. Update EGL config to request depth component:
private static final int[] EGL_CONFIG_ATTRIB_LIST = new int[]{
EGL14.EGL_RENDERABLE_TYPE, 4,
EGL14.EGL_RED_SIZE, 8,
EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_ALPHA_SIZE, 8,
EGL14.EGL_DEPTH_SIZE, 16, // this was missing
EGL14.EGL_NONE};
2. Make mInsetBottom and mInsetLeft variables accessible from subclasses. These values will be used to properly update viewport. Either add getters or make them protected:
protected int mInsetBottom = 0;
protected int mInsetLeft = 0;
glViewport()
Official documentation has some very brief and vague mentioning of handling devices with different screens using onApplyWindowInsets() method. It is said that you should use this method to adapt for displays with bottom “chin”. This is necessary to properly adjust view on “flat tire” display of Moto 360.
Without having Moto360 in hands it was unclear how to use it so our first attempt to run watch face on this watch resulted in misplaced viewport - shifted up by bottom inset size. Pretty strange is that this was not the case with Tilt watch face - it was centered properly. To find the cause of this issue we had to take a look at decompiled source code of Gles2WatchFaceService again. It was caused by usage of glViewport(). In our renderer we use off-screen rendering for bloom effect, and use glViewport() to switch between small off-screen render target and on-screen viewport. In order to avoid misplaced viewport it is necessary to take into account bottom inset when setting glViewport().
Opaque mode in normal and ambient mode
For reasons unknown it is not possible to render opaque peek cards in ambient mode. You can alter appearance of peek cards in normal mode but in ambient mode they are 100% translucent. You will have to render some black rectangle underneath peek card using getPeekCardPosition() method. In our case it is enough to use small pick card sizes so they don’t overlap with digits but in general it is a good idea to resize viewport according to received dimensions of peek card.
Sending messages with settings update to watch
In our companion settings activity on watch we use HoloColorPicker to choose colors. Color of watch face gets updated immediately on every change of color in color picker. However, this leads to certain problems. It is possible to update colors too fast by continuously swiping color picker, and queue of messages to watch can't handle updates fast enough. It is simply not designed to work this way and last messages in really long queue may not be delivered to watch. Nothing won't crash but Data Layer API may simply skip last messages from queue. To fix this problem we're throttling updates of color picker by using Handler with 500 ms interval:
Handler handlerUpdateColorBackground = new Handler();
Runnable runnableUpdateColorBackground = new Runnable() {
@Override
public void run() {
// update only if color was changed
if (pickedColorBackground != lastSentColorBackground) {
sendConfigUpdateMessage(KEY_BACKGROUND_COLOR, pickedColorBackground);
}
handlerUpdateColorBackground.postDelayed(this, 500);
// update last color sent to watch
lastSentColorBackground = pickedColorBackground;
}
};
Handler handlerUpdateColorBackground = new Handler();
Runnable runnableUpdateColorBackground = new Runnable() {
@Override
public void run() {
// update only if color was changed
if (pickedColorBackground != lastSentColorBackground) {
sendConfigUpdateMessage(KEY_BACKGROUND_COLOR, pickedColorBackground);
}
handlerUpdateColorBackground.postDelayed(this, 500);
// update last color sent to watch
lastSentColorBackground = pickedColorBackground;
}
};
Overall impression of Android Wear hardware
Most of Android Wear smart watches on market have Snapdragon 400 in them - ASUS, Sony, Samsung and LG uses it in all of their devices. This SoC has quite impressive quad-core CPU with clock speeds up to 1.2 GHz which is more than enough for smartwatch. Its Adreno 305 GPU may seem to be out-of-date at a glance. But watches have quite low resolution of 320x320 pixels so its power is enough to render quite complex 3D scenes at 60 fps. Even Moto 360 with its aged OMAP3630 SoC (as revealed in this iFixit teardown) has powerful enough PowerVR SGX530 in it which also provides good 3D performance at required resolution.
For example, applying bloom rendered at 128x128 resolution and blurred 4x times doesn’t seem to affect performance at all - Adreno 305 easily handles this additional task. However, for these watch faces it is enough to use even quite low-res texture of 64x64 size with 2 blur cycles to achieve good visual quality.
There are numerous videos that showcase games like Temple Run 2 and GTA running on Android Wear without any lag which also showcases performance of GPUs found in smart watches.
There are numerous videos that showcase games like Temple Run 2 and GTA running on Android Wear without any lag which also showcases performance of GPUs found in smart watches.
Shaders used in this app
To reduce the amount of draw calls and CPU work, digits animation is done in vertex shader. To illustrate it, here is the model of transition between ‘5’ and ‘0’:As you can see, its vertices are divided into three different groups:
- yellow parts are not animated - they represent parts which are in common for ‘5’ and ‘0’;
- dark grey represent parts which will move down during animation - they are not used in ‘0’ digit;
- light grey represent parts which will move up during animation - they are used in ‘0’ but not in ‘5’.
Pixel shader is very simple - the lower Z coordinate of model gets, the closer to black fragment is rendered. This represents transition between digits ‘3’ and ‘4’ (yes it is incorrectly mirrored in RenderMonkey) - you can see parts of ‘4’ being shifted downwards and parts of ‘3’ popped to the top:
Here is the final result with black background which fades with bottom black of the model:
Below is the code of vertex shader:
uniform mat4 view_proj_matrix;
uniform float uAnim;
uniform float uHeight;
uniform float uHeightColor;
uniform float uHeightOffset;
varying vec2 Texcoord;
varying float vColor;
void main( void )
{
vec2 uv = gl_MultiTexCoord0.xy;
vec4 pos = rm_Vertex;
pos.z += uHeight * uv.y * clamp(uAnim + uv.x, 0.0, 1.0);
gl_Position = view_proj_matrix * pos;
Texcoord = uv;
vColor = (uHeightColor + pos.z + uHeightOffset) / uHeightColor;
}
uAnim is responsible for animation - it is set in range of [-1..1]
uHeight is a multiplier for height animation - it differs for different digits ‘fonts’ and basically represents the height of single ‘pillar’ of a digit - 40 units for font in pictures shown.
uHeightColor and uHeightOffset are used to better control fading colors to black. uHeightColor = 40 and uHeightOffset = 0 are used in the pictures above but they are different for other fonts.
Pixel shader’s code:
uniform vec4 uColor;
uniform vec4 uColorBottom;
varying float vColor;
void main( void )
{
gl_FragColor = mix(uColorBottom, uColor, clamp(vColor, 0.0, 1.0));
}
As you can see, pretty much everything is calculated in fragment shader already.
Archive containing RenderMonkey project with shaders: shaders_watchface.zip
Publishing
When publishing app to Google Play, you will need to tick the “Distribute your app on Android Wear” checkbox. This initiates review of your app for compliance with Wear App Quality guidelines. In our case it took just an hour or two to receive a “green light” email informing that app conforms to guidelines for watch face apps.
You can get our Axiom Watch Faces app on Google Play:
Hi,
ReplyDeleteNice article. I wonder how you achieved so great performance. On my ZenWatch2 a simple glClear() and glFinish() takes about 10 - 11 ms.
You might have chosen slow EGL config. Otherwise, it is hard to tell what could possibly be wrong with performance of your implementation.
DeleteI use the default config. The interesting thing is that when I draw a 1024 vertex triangle strip too, that doesn't do a measurable increase in the render time. I think this 10-11ms could be related to GL command queue handling or the render pipeline.
DeleteHi great reeading your blog
ReplyDelete