前言
Wolvic 的首页中有许多的组件,比如标题栏,托盘,系统设置界面等等,而这些如果用纯 opengl 去绘制会非常的繁琐,所以在 wolvic 中使用了原生安卓的UI框架去设计界面,在绘制时将内容绘制到自己给到的 canvas 上面,传递给 opengl 进行绘制上屏,通过这种方式,实现了用原生安卓的开发过程去创建 VR 环境的平面组件。
原理分析
绘制分析
Wolvic 中的UI组件是用 Widget 这个概念去承载,所有组件都会实现 Widget 这个接口,并基本都会继承自 UIWidget。首先对 Widget 有个概念,一个 Widget 是一个组件,它可以由一个或多个 view 组成。比如 TrayWidget 就是一个托盘组件,具体样式如图:
相信大多数人对于如何绘制这样一个托盘是比较熟悉的,简单来说可以用一个 xml 描述文件,依次按照自己熟悉的布局绘制出来。那么他是怎么绘制到 VR 空间的呢?先来看看 Widget 的结构关系。
上图简略描述了 UIWidget 类的基本结构,省略了监听,逻辑处理的方法和成员,仅保留了和绘制,事件相关的处理代码。可以看到,首页展现出来的每一个界面,基本都是由 UIWidget 组成的,浏览器界面是由 WidowsWidget 绘制,托盘是由 TrayWidget 绘制,键盘是由 KeyboardWidget 绘制等等。由于 UIWidget 继承了安卓的 FrameLayout, 所以他本身就是一个 View,可以当作孩子被插入到其他 View 当中,也可以接收其他 View 插入其中。
-
UIWidget 组合了一系列的类,是整个组件的核心。其子类需要实现 initializeWidgetPlacement 方法来设置自己的大小位置信息等等。
-
WidgetPlacement 表示一个 UIWidget 的描述信息,包含宽高,偏移位置,分辨率,名称,是否可见等等信息。
-
UISurfaceTextureRenderer 用来准备绘制的画布,他包装了从 native 传递过来的 SurfaceTexture ,创建了 Surface,并在绘制的时候由此创建出一个 Canvas 用于 UIWidget 的绘制。
-
WidgetManagerDelegate 负责组件的创建,更新,添加,删除等,并拥有控制是否可见,监听各种状态的任务,在 wolvic 中实际是 VRBrowserActivity。
在 wolvic 中,UIWidget 相关的类,大多数都会有一个 native 的类进行对应,比如 UIWidget.java 对应 Widget.cpp, WidgetPlacement.java 对应 WidgetPlacement.cpp 等等诸如此类,在创建 java 类的同时,会调用一个 createNative 的方法同时也创建对应的 C++ 类,C++ 类更多的是一个 java 类的映射,保存了 java 类对应的基本信息,供native 侧进行调用。其中 Widget.cpp 有一个重要的功能,就是创建 TextureSurface 或者 Surface ,具体什么时候创建 TextureSurface,什么时候创建 Surface,此处先不表,后文揭晓。
为了理解的更加具体,我们就挑 TrayWidget 和 WindowWidget 来看看具体的实现。为什么挑这两个,因为其比较有代表性,TrayWidget 代表了大多数的组件形态,而 WindowWidget 表示的是整个 GeckoView(等同于安卓的Webview) 界面的绘制,比较重要。
View 树挂载
在讲具体实现之前,先不着急,先来看看 wolvic 当中是如何挂载 view 的,毕竟 wolvic 当中的 activity 的界面不是由 contentView 渲染出来的,所以根本不会挂载 contentView。这部分逻辑主要都在 VrBrowserActivity 中。
@Override
protected void onCreate(Bundle savedInstanceState) {
……
mWidgetContainer = new FrameLayout(this);
……
queueRunnable(() -> {
createOffscreenDisplay();
……
});
……
}
其中 mWidgetContainer 是整个 View 树的根节点,所有的 UIWidget 都会通过 addView 的方式挂载到这个 View 节点上。queueRunnable 将在 native 运行的线程执行任务,也就是创建 OffscreenDisplay.
void createOffscreenDisplay() {
final SurfaceTexture texture = createSurfaceTexture();
runOnUiThread(() -> {
mOffscreenDisplay = new OffscreenDisplay(VRBrowserActivity.this, texture, 16, 16);
mOffscreenDisplay.setContentView(mWidgetContainer);
});
}
private SurfaceTexture createSurfaceTexture() {
int[] ids = new int[1];
GLES20.glGenTextures(1, ids, 0);
……
return new SurfaceTexture(ids[0]);
}
在 createOffscreenDisplay 方法中,可以看到先创建了一个 SurfaceTexture,这就是为什么必须要 queueRunnable,因为需要在 native 运行的线程生成 SurfaceTexture.之后再抛到主线程创建 OffscreenDisplay。再来看一下 OffscreenDisplay 类是怎么实现的。
可以看出这个类实际上是对 VirtualDisplay 和 Presenation 的一个封装,这两个类的具体用法可以参考安卓官方文档。所以上述的调用会执行以下两部分逻辑:
public void onResume() {
if (mVirtualDisplay == null) {
……
int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
……
mVirtualDisplay = manager.createVirtualDisplay("OffscreenViews Overlay", mWidth, mHeight,
mDefaultMetrics.densityDpi, mSurface, flags);
}
if (mPresentation == null) {
mPresentation = new OffscreenPresentation(mContext, mVirtualDisplay.getDisplay());
mPresentation.show();
if (mContentView != null) {
mPresentation.setContentView(mContentView);
}
}
}
public void setContentView(View aView) {
mContentView = aView;
if (mPresentation != null) {
mPresentation.setContentView(aView);
}
}
首先创建了 mVirtualDisplay 和 mPresentation,并将 mWidgetContainer 设置为了 mPresentation 的 contentView。通过此种方式,就相当于在虚拟屏上挂载了一颗 view 树,整个渲染流程都是和正常的 activity 上面挂载一棵树是一样的,只不过不在当前屏幕当中。
TrayWidget 实现
Wolvic 中使用了 Databinding,如果不熟悉,可以先查看相关文档。首先在构造时进行了 View 的初始化:
public void updateUI() {
removeAllViews();
LayoutInflater inflater = LayoutInflater.from(getContext());
// Inflate this data binding layout
mBinding = DataBindingUtil.inflate(inflater, R.layout.tray, this, true);
……
}
TrayWidget 的视图是由 R.layout.tray 描述。通过 DataBinding 的方式 attach 到了当前 View 当中。在 TrayWidget 中没有重写 draw 相关的方法,所以没有绘制的特殊逻辑,所以看其父类 UIWidget 是如何执行绘制即可。 上述就是 UIWidget 的 draw 方法的执行过程。可以看到view的绘制被拦截到了 renderer 生成的 canvas 上面。如果 widget 设置了 proxifyLayer,则会再绘制一次到 proxyRenderer 的 canvas 上面。通过这种方式,当前的 Widget 就已经被绘制到了 renderer 里面的 surface 上面了。proxifyLayer 目前看起来是用于做动画防止闪烁的,无动画的 UIWidget 暂时用不到。
WindowWidget 实现
WindowWidget 代表了一个 GeckoView 绘制界面。 他重写了 UIWidget 的 draw 方法,没有使用父类的 draw 的方式,而是走了原生的 draw 方式:
@Override
public void draw(Canvas aCanvas) {
if (mView != null) {
super.draw(aCanvas);
}
}
mView 是 GeckoView 上面的覆盖层,比如弹窗,比如设置界面等等。正常情况下为空,则 draw 的时候,什么都不执行。那么他是怎么绘制 GeckoView 的网页界面呢?大致流程如下图:
在代码中查找会发现,在 setSurface 和 setSurfaceTexture 的时候会调用一个 callSurfaceChanged 方法,把 surface 对象传递到了 mSession 当中。mSession 中又会把这个 surface 传递给 GeckoSession 中,交给 GeckoView 内核进行使用,具体的绘制都在 GeckoView 内核中实现,此处不再继续深究。
Texture(Surface) 生成过程
上面分析了 UI 的绘制过程,这一节说一下画布是怎么生成的。上文提到过 UIWidget 的两个方法, setSurface 和 setSurfaceTexture,这两个方法就是画布生成的关键。先来看看 UIWidget 创建过程中,native的执行逻辑:
从上图可以看出,在主线程创建 Widget 的时候,native 也需要创建对应的 native 对象,并且会区分是否是 layer,目前只有 oculcus 打开了 layer 的设置选项,其他设备统统都没有走 layer 类型。这个layer 类型是 OpenXR 的概念,相关链接见这里。XrCompositionLayerQuad 层对于渲染到虚拟世界中的用户界面元素或 2D 内容很有用。该层的 XrSwapchainSubImage::swapchain 图像应用于虚拟世界空间中的四边形。
在 Widget的 UpdateSurface 方法中,也判断了是否是 layer,是的话等待 surfaceChanged 事件再执行下一步,否则就会创建一个 TextureSurface 类型,注意此时仅仅是创建了一个壳 TextureSurface ,其中的 opengl 的 texture 还未真正创建,此类会在 SurfaceTextureFactory 中记录自己需要一个 texture,等到 SurfaceTextureFactory 根据记录创建好了 texture 之后,会回设给 TextureSurface。和 layer 类型等待 surfaceChanged 事件一致,等到合适的时机才会去创建真正的 texture 或者 surface。后面就是给 quad 绑定上面创建的 textureSurface,更新给 opengl 的 program,因为可能会有一些关键字不一样,需要去动态修改 program。
native 的 Widget 创建完成了,再来看看当不是 layer 类型的时候具体是什么时候生成 texture 或者 surface:
已经讲过 StartFrame 方法了,此处着重看 RenderContext::Update 方法,此方法中会去遍历前文 TextureSurface::Create 留下的需要创建 texture 集合,逐个进行 glGenTexture 创建真正的 texture.并且会根据这个 texture 去创建 java 层的 SurfaceTexture 对象,并把它分发给 java 层的 UIWidget 对象。到这里,就看到了绘制分析中提到的 surface 和 surfaceTexture 是从何而来的了。最后再通过 updateTexImage 将当前的图像提交到纹理中,就实现了当前 Widget 的绘制。
再来看一下如果是 Layer 类型,是什么过程:
在 layer 打开时,从上图可以看到,每一个平面的 UIWidget 都会去创建一个对应的 OpenXRLayerQuad 对象,他会去初始化 surface,他是通过 OpenXR 的 xrCreateSwapchainAndroidSurfaceKHR** **方法去创建的。再通过 DispatchCreateWidgetLayer 方法传递到 java 层,并设置给 UIWidget。*和非 layer 类型的区别就在于一个是通过 jni 的方式手动去创建的 surface, 一个是通过 OpenXR 的方式去创建的 surface。*有了这个 surface,上文的渲染就水到渠成了。
结束
结合 wolvic 浏览器openxr 支持现状 可以大致了解 wolvic 中是如何将安卓的组件渲染到 3D 空间了。