【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相关文档

实测效果

手机平板

ARCore
功能测试,非最终效果

功能测试,非最终效果

眼镜双屏

功能测试,非最终效果

实现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最明显的作用就是在菜单中显示所有程序图标,并控制相应程序的启动。

  1. 获取所有程序

通过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);
  1. 应用排序

这里安卓应用的安卓时间进行排序

/*按照应用安装时间进行排序*/
// 创建一个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);

  1. 应用隐藏

在实际需求中,可能需要涉及隐藏某些应用。这里不将在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效果。

  • 双屏显示

原理:将一个画面分为二,左右显示相同内容。
实现:略。

  • 合目显示

原理:将左画面右移,右画面左移。
实现:略。

相关推荐

  1. AWS ECSEC2、EKS 和 Fargate 之间的关系

    2024-02-02 17:02:01       51 阅读
  2. ECS如何安装可视化桌面

    2024-02-02 17:02:01       33 阅读
  3. AWS使用ECS时ecsTaskExecutionRole缺失

    2024-02-02 17:02:01       40 阅读
  4. AWS EKS使用Socket.IO

    2024-02-02 17:02:01       16 阅读
  5. 什么是ar.exe

    2024-02-02 17:02:01       11 阅读
  6. 在gen_server使用ets实例演示

    2024-02-02 17:02:01       32 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-02-02 17:02:01       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-02-02 17:02:01       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-02-02 17:02:01       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-02-02 17:02:01       20 阅读

热门阅读

  1. 02. 【Linux教程】GNU 项目简介

    2024-02-02 17:02:01       26 阅读
  2. 计算机网络(第六版)复习提纲21

    2024-02-02 17:02:01       32 阅读
  3. webpack环境配置

    2024-02-02 17:02:01       34 阅读
  4. 2024美赛C题思路/代码:网球中的动量

    2024-02-02 17:02:01       37 阅读
  5. OpenStack平台镜像优化

    2024-02-02 17:02:01       29 阅读
  6. Vue中的插槽Slot的使用说明

    2024-02-02 17:02:01       31 阅读
  7. 开源软件的未来

    2024-02-02 17:02:01       31 阅读
  8. Servlet基础之配置 Servlet 及其映射

    2024-02-02 17:02:01       33 阅读