【EQ-R】使用EQ-Renderer实现AR桌面
EQ-R
简介
EQ-Renderer是EQ基于sceneform(filament)扩展的一个用于安卓端的三维AR渲染器。
主要功能
它包含sceneform_v1.16.0中九成接口(剔除了如sfb资源加载等已弃用的内容),扩展了视频背景视图、解决了sceneform模型加载的内存泄漏问题、集成了AREngine和ORB-SLAM3、添加了场景坐标与地理坐标系(CGCS-2000)的转换方法。
注:由于精力有限,文档和示例都不完善。sceneform相关请直接参考谷歌官方文档,扩展部分接口说明请移步git联系。
相关链接
Git仓库
码云
EQ-R相关文档
实测效果
手机平板
眼镜双屏
实现Launcher
与普通应用的异同
Launcher本质上就是一个app,用于管理其它应用程序。在开发时,当在AndroidManifest.xml中配置了“HOME”属性时,那在系统启动时或点击“Home”键时,就会跳转到这个应用界面。若一台设备具有多个launcher,则会提示用户选择launcher。
<activity
android:name=".StartActivity"
android:screenOrientation="landscape">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
示例应用可参考AOSP(安卓开源工程)源码中packages/apps/Launcher3。
部分功能实现
实现程序菜单
launcher最明显的作用就是在菜单中显示所有程序图标,并控制相应程序的启动。
- 获取所有程序
通过PackageManager获取所有程序的包名和Activity名称
PackageManager packageManager = context.getPackageManager();
//使用queryIntentActivities方法查询具有Launcher图标的应用程序的主Activity,
// 并获取了它们的包名和Activity名称。这些信息可以用于启动特定应用程序的主Activity。
Intent intent = new Intent().setAction(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER);
List<ResolveInfo> resolveInfoList = packageManager.queryIntentActivities(intent, 0);
- 应用排序
这里安卓应用的安卓时间进行排序
/*按照应用安装时间进行排序*/
// 创建一个Comparator用于比较ResolveInfo对象
Comparator<ResolveInfo> installTimeComparator = new Comparator<ResolveInfo>() {
@Override
public int compare(ResolveInfo resolveInfo1, ResolveInfo resolveInfo2) {
// 获取应用的包名
String packageName1 = resolveInfo1.activityInfo.packageName;
String packageName2 = resolveInfo2.activityInfo.packageName;
// 获取应用的安装时间
long installTime1 = getInstallTime(packageManager, packageName1);
long installTime2 = getInstallTime(packageManager, packageName2);
// 比较应用的安装时间
return Long.compare(installTime1, installTime2);
}
};
// 使用Comparator对ResolveInfo列表进行排序
Collections.sort(resolveInfoList, installTimeComparator);
- 应用隐藏
在实际需求中,可能需要涉及隐藏某些应用。这里不将在excludePackagesList中的包名添加进appList
for (ResolveInfo resolveInfo : resolveInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
if (excludePackagesList != null && excludePackagesList.contains(packageName)) {
continue;
}
//...
}
实现时间显示
日期和时间显示,可直接使用TextView,这里给出个简单示例。
public class DateTextView extends TextView {
private String mDateFormat;
private BroadcastReceiver receiver;
private IntentFilter filter;
public DateTextView(Context context) {
super(context);
init(context);
}
public DateTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public DateTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
registerReceiver();
updateText();
}
public void setDateFormat(String format) {
mDateFormat = format;
updateText();
}
private void registerReceiver() {
filter = new IntentFilter();
filter.addAction(Intent.ACTION_TIME_TICK);
filter.addAction(Intent.ACTION_TIME_CHANGED);
filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
updateText();
}
};
getContext().registerReceiver(receiver, filter);
}
private void updateText() {
SimpleDateFormat sdf = new SimpleDateFormat(mDateFormat, Locale.getDefault());
String date = sdf.format(new Date());
setText(date);
setTextColor(Color.WHITE);
setTextSize(16);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (receiver != null) {
getContext().unregisterReceiver(receiver);
receiver = null;
}
}
@Override
public void dispatchWindowVisibilityChanged(int visibility) {
super.dispatchWindowVisibilityChanged(visibility);
if (visibility == VISIBLE) {
updateText();
if (receiver == null){
getContext().registerReceiver(receiver, filter);
}
} else if (visibility == GONE || visibility == INVISIBLE) {
if (receiver != null){
getContext().unregisterReceiver(receiver);
receiver = null;
}
}
}
}
实现电量显示
这里通过广播的方式获取电量,示例如下。
private void registReceiver(){
//电量监听
IntentFilter batteryFilter = new IntentFilter();
batteryFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
getContext().registerReceiver(batteryReceiver,batteryFilter);
}
private void unregistReceiver(){
getContext().unregisterReceiver(batteryReceiver);
}
//</editor-fold>
//<editor-fold> - 电量管理
private BroadcastReceiver batteryReceiver = new BroadcastReceiver() {
@SuppressLint("SetTextI18n")
@Override
public void onReceive(Context context, Intent intent) {
int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0);
int levelPercent = (int)(((float)level / scale) * 100);
if (batteryValue != null){
batteryValue.setText(levelPercent + " %");
}
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN);
if (status == BatteryManager.BATTERY_STATUS_CHARGING) {
//充电状态,显示的图标
batteryIcon.setBackground(context.getDrawable(R.drawable.battery_charging));
}else {
batteryIcon.setBackground(context.getDrawable(R.drawable.battery_bg));
}
}
};
实体组件
基础组件
由于在后面的三维场景渲染中,是直接将安卓View渲染在三维场景中。
因此这里将不同功能实现的安卓View封装成不同的实体组件。
在此基础上,抽象出基类BaseComponent,便于后续数据管理和功能扩展。
在当前实现中,Node类是一个包含三维空间位置、姿态信息的实体类。
BaseComponent类具有Node属性,包含load、resume、pause、destroy方法。
程序菜单组件
作用:用于显示所有程序图标
实现功能:
- 获取本机所有程序
- 隐藏指定程序
- 应用安装、卸载提示
- 添加快捷方式
程序快捷栏组件
作用:用于显示程序的快捷方式,便于常用应用启动。
实现功能:
- 程序快捷方式显示
- 时间日期显示
- 剩余电量显示
- 通讯服务商类型显示
图片组件
作用:装饰场景
视频组件
作用:装饰场景
参数化配置
组件配置
在将功能组件化后,实现通过配置文件控制相应组件的加载。 这样可以简化开发,便于维护。
下面的配置“EntityData.xml”,会在场景中显示程序菜单组件、快捷栏组件和一张名为“bg”的图片
<?xml version="1.0" encoding="utf-8"?>
<entities>
<AppMenuView>
<position>
<x>0</x>
<y>0</y>
<z>0</z>
</position>
<rotation>
<x>0</x>
<y>0</y>
<z>0</z>
<w>1.0</w>
</rotation>
<scale>
<x>0.3</x>
<y>0.3</y>
<z>0.3</z>
</scale>
</AppMenuView>
<ShortcutView>
<position>
<x>0</x>
<y>-0.23</y>
<z>0.00</z>
</position>
<rotation>
<x>0</x>
<y>0</y>
<z>0</z>
<w>1.0</w>
</rotation>
<scale>
<x>0.3</x>
<y>0.3</y>
<z>0.3</z>
</scale>
</ShortcutView>
<Image3DView>
<drawable>bg</drawable>
<position>
<x>0</x>
<y>0.25</y>
<z>-0.1</z>
</position>
<rotation>
<x>0</x>
<y>0</y>
<z>0</z>
<w>1.0</w>
</rotation>
<scale>
<x>0.2</x>
<y>0.2</y>
<z>0.2</z>
</scale>
</Image3DView>
</entities>
其它配置
此外,如隐藏应用、推广信息等内容也应该实现通过配置信息的方式进行加载,便于后期修改。
实现AR
场景渲染
Filament渲染器
这里使用filament作为渲染器,用于AR场景中三维内容的渲染。 filament与sceneform有一定的渊源。早期谷歌的sceneform方便了移动端AR的应用开发。
EQ-Renderer
EQ-Renderer为在filament的基础上封装的一套渲染接口,简化了filament的调用。
implementation project(path: ':eq-renderer')
针对手机与平板
支持ARCore、AREngine的设备
做过移动端AR开发的朋友或多或少都使用过ARCore、AREngine(华为的),这里EQ-Renderer集成了ARCore和AREngine,直接使用ARSceneLayout控件即可。
public class XrActivity extends BaseSceneActivity {
private Node sceneNode;
private ARSceneLayout sceneLayout;
/**
*
*/
@SuppressLint("UseCompatLoadingForDrawables")
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Config.useAR = false;
// 隐藏状态栏
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
);
//读取设置
Config.readConfig(this);
setContentView(R.layout.activity_main);
sceneNode = new Node();
//注意:sceneNode此时还没有和场景中的节点建立联系
sceneLayout = findViewById(R.id.scene_layout);
layout.getCamera().setVerticalFovDegrees(47);
sceneNode.setParent(layout.getRootNode());
//根据Assets/EntityData.xml文件来加载实体对象
EntityManager.getInstance().load(this,sceneNode);
}
@Override
protected void onResume() {
super.onResume();
EntityManager.getInstance().resume();
}
@Override
protected void onPause() {
EntityManager.getInstance().pause();
super.onPause();
}
@Override
protected void onDestroy() {
EntityManager.getInstance().destroy();
super.onDestroy();
}
}
不支持ARCore、AREngine的设备
某些定制机未在ARCore支持列表内,则需要使用EQ-Renderer中的SceneLayout组件。 SceneLayout组件默认加载一个三维场景,其场景相机获取的方式如下:
Camera camera = sceneLayout.getCamera();
camera 这里可以设置垂直(水平)FOV,设置近(远)裁剪平面的距离。
这里FOV要与物理设备的相机参数保持一致。 注:若已知内参fx,fy,Cx,Cy,则需要换算一下。
参考:
- 通过三方SLAM算法获取6dof位姿
- 通过设备自身传感器获取方位角、俯仰角、翻滚角
针对MR眼镜
VST方案
针对使用VST(Video See Through,视频透视)方案的眼镜设备,与手机端一致,虚拟场景相机与真实物理相机参数保持一致即可。
OST方案
针对使用OST(Optical See Through,光学透视)方案的眼镜设备,这类设备有个特点,就是黑色画面背景看不见(换句话说,带上眼镜,若显示屏投射的画面为黑色,那么用户将看不见任何画面内容)。因此,将SceneLayout的背景设置为黑色,即是AR既视感。
基于此,若要结合3dof、6dof数据,则与上面的方式一致。若不结合(“类似于固定视角”),则到此即可。
注意事项
需要注意的是: MR眼镜通常是双屏显示,因此与手机(平板)端不同的是,这里需要实现左右双屏画面显示。 而在实际的体验中,会发现显示有重影。这是由于左边画面显示内容和右边画面显示内容一样,而人的左右眼位置不一样,这样看到画面就是左右重影。因此,还需要做一个合目的操作,实现伪3D效果。
- 双屏显示
原理:将一个画面分为二,左右显示相同内容。 实现:略。
- 合目显示
原理:将左画面右移,右画面左移。 实现:略。
转载自:https://juejin.cn/post/7370962530051833891