AR增强现实:ARCore基础、3D模型渲染、平面检测、光照估计

AR增强现实,说白了就是把虚拟物体塞进真实世界里。你拿着手机,摄像头对着桌面,然后一只3D小恐龙就站在桌面上——这就是AR最直观的样子。

我最早接触ARCore是在2018年,那时候Google刚推出1.0版本。说实话,第一版bug不少,平面检测经常飘。但到了现在,ARCore已经非常成熟了。我个人习惯把它当作Android平台上AR开发的标配。

ARCore的核心能力

ARCore不是魔法,它靠三个核心能力来欺骗你的眼睛:

  • 运动追踪:手机怎么移动,虚拟物体就怎么跟着动
  • 环境理解:检测水平面、垂直面,知道哪里能放东西
  • 光照估计:分析真实环境的光线,让虚拟物体有正确的阴影和亮度

这三个能力缺一不可。我在项目中遇到过一个问题:光照估计没做好,虚拟物体在暗光环境下亮得像灯泡,一眼就看穿是假的。

ARCore的工作流程

先看一张整体流程图,帮你快速建立认知:

ARCore 核心工作流程 1. 初始化ARCore 创建Session 2. 运动追踪 跟踪手机位姿 3. 平面检测 识别水平/垂直面 4. 渲染 光照估计(并行) 虚拟物体叠加到真实场景 3D模型 + 正确光照 + 正确位置

嗯,这张图把流程讲清楚了。核心就是:先初始化,然后不断追踪、检测、估计,最后把模型渲染上去。

项目配置

要开始用ARCore,首先得在build.gradle里加依赖:

dependencies {
    implementation 'com.google.ar:core:1.38.0'
    implementation 'com.google.ar.sceneform:sceneform-base:1.17.1'
    implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.17.1'
}

注意,SceneForm从1.15.0开始就不在Google官方维护了。我个人建议用SceneForm的社区分支,或者直接用OpenGL/Metal自己渲染。不过对于初学者,SceneForm还是最友好的选择。

AndroidManifest里也要声明:

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera.ar" />

<meta-data android:name="com.google.ar.core" android:value="required" />

这里有个坑:如果你把ARCore设为required,那不支持AR的设备直接装不了你的App。我曾经因为这个被产品经理骂了一顿——用户反馈说在旧手机上搜不到我们的App。后来改成了optional,自己代码里判断是否支持。

创建AR Session

AR Session是ARCore的入口。说白了,它就是AR世界的上下文:

// 检查ARCore是否可用
ArCoreApk.Availability availability = ArCoreApk.getInstance().checkAvailability(this);
if (availability.isSupported()) {
    // 创建Session
    session = new Session(this);
    // 配置Session
    Config config = new Config(session);
    config.setUpdateMode(Config.UpdateMode.LATEST_CAMERA_IMAGE);
    session.configure(config);
}

你想想看,Session就像是一个画布,所有AR数据都在上面绘制。每次摄像头帧进来,Session都会更新追踪状态、平面列表、光照信息。

平面检测

平面检测是AR里最常用的功能。用户点击屏幕,我们就在那个位置放一个虚拟物体。但前提是——得先找到平面。

// 获取所有检测到的平面
Collection<Plane> planes = session.getAllTrackables(Plane.class);
for (Plane plane : planes) {
    if (plane.getTrackingState() == TrackingState.TRACKING) {
        // 获取平面的中心点和法向量
        float[] center = plane.getCenterPose().getTranslation();
        float[] normal = plane.getCenterPose().getRotationQuaternion();
        
        // 判断是水平面还是垂直面
        int type = plane.getType(); // HORIZONTAL_UPWARD_FACING 或 VERTICAL
    }
}

我记得有一次做AR家具展示,用户想把沙发放在墙上(垂直面)。结果平面检测一直识别不到垂直面,后来发现是手机离墙太近了。ARCore对距离有要求,太近反而检测不准。

小技巧: 平面检测时,让用户缓慢移动手机,扫描不同角度。这样ARCore能更快地建立环境地图。别让用户拿着手机不动——那样永远检测不到平面。

3D模型渲染

有了平面,接下来就是把3D模型放上去。SceneForm里用ModelRenderable来加载模型:

ModelRenderable.builder()
    .setSource(this, Uri.parse("models/robot.sfb"))
    .build()
    .thenAccept(renderable -> {
        // 创建AnchorNode,锚定到平面
        AnchorNode anchorNode = new AnchorNode(anchor);
        anchorNode.setRenderable(renderable);
        
        // 添加到Scene
        arFragment.getArSceneView().getScene().addChild(anchorNode);
    })
    .exceptionally(throwable -> {
        Log.e("AR", "模型加载失败", throwable);
        return null;
    });

这里要注意,SceneForm支持的是.sfb格式(SceneForm Binary),不是.obj或.gltf。你需要用Google提供的工具把模型转成.sfb。或者,你也可以直接用Filament引擎加载.gltf,但代码量会大很多。

我个人的经验是:模型面数不要太高。移动端GPU扛不住几百万面的模型。一个AR场景里,模型控制在1-3万面比较合适。纹理贴图也尽量用512x512或1024x1024。

光照估计

光照估计是AR里最容易忽略但又最重要的环节。没有光照估计,虚拟物体就像P上去的贴纸,毫无立体感。

// 获取当前帧的光照估计
Frame frame = session.update();
LightEstimate lightEstimate = frame.getLightEstimate();

// 获取环境光强度
float pixelIntensity = lightEstimate.getPixelIntensity();

// 获取环境光颜色
float[] colorCorrection = lightEstimate.getColorCorrection();

// 获取环境球谐光照(用于反射)
float[] sphericalHarmonics = lightEstimate.getEnvironmentalHdrEnvironmentalLighting();

SceneForm会自动应用光照估计到模型上。但如果你用OpenGL自己渲染,就需要手动处理了。

核心要点: 光照估计包含三个部分:环境光强度(亮度)、颜色校正(色温)、球谐光照(方向光)。三者结合,才能让虚拟物体融入真实环境。

我曾经踩过一个坑:在室内暖黄光下,虚拟物体偏蓝,看起来特别假。后来发现是颜色校正没处理好。ARCore返回的颜色校正是针对当前场景的,你得把它应用到模型的材质上。

用户交互:点击放置

用户点击屏幕,我们在点击位置放置模型。这个逻辑看起来简单,但实现起来有几个细节:

arFragment.setOnTapArPlaneListener((hitResult, plane, motionEvent) -> {
    // 1. 创建Anchor
    Anchor anchor = hitResult.createAnchor();
    
    // 2. 创建AnchorNode
    AnchorNode anchorNode = new AnchorNode(anchor);
    anchorNode.setParent(arFragment.getArSceneView().getScene());
    
    // 3. 创建TransformableNode(支持手势缩放旋转)
    TransformableNode transformableNode = new TransformableNode(
        arFragment.getTransformationSystem());
    transformableNode.setParent(anchorNode);
    transformableNode.setRenderable(renderable);
    transformableNode.select();
});

嗯,这里有个细节:hitResult.createAnchor()创建的Anchor会随着ARCore的追踪优化而自动调整位置。但如果你用hitResult.createAnchor(pose)指定位置,那模型就固定死了,不会自动修正。

注意: Anchor不是永久的。如果ARCore丢失追踪(比如手机快速移动),Anchor会进入TrackingState.STOPPED状态。这时候模型会消失。你需要监听状态变化,必要时重新创建。

性能优化建议

AR应用对性能要求极高。30fps是底线,低于这个帧率用户会明显感到眩晕。我总结了几条优化经验:

  • 控制模型面数:单个模型不超过5万面
  • 纹理压缩:使用ETC2或ASTC格式
  • 减少Draw Call:合并材质,减少渲染批次
  • 降低分辨率:如果设备发热,可以降低图形分辨率
  • 及时清理:不再需要的Anchor和Node要及时移除

我记得有一次在低端机上跑AR,手机烫得能煎鸡蛋。后来把模型从3万面降到1万面,纹理从1024降到512,帧率从18fps提到了30fps。效果虽然差了点,但至少能用了。

完整示例代码

最后,给一个完整的Activity骨架。你直接拿去改改就能用:

public class ArActivity extends AppCompatActivity {
    private ArFragment arFragment;
    private ModelRenderable renderable;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_ar);
        
        arFragment = (ArFragment) getSupportFragmentManager()
            .findFragmentById(R.id.ar_fragment);
        
        // 加载模型
        ModelRenderable.builder()
            .setSource(this, Uri.parse("models/robot.sfb"))
            .build()
            .thenAccept(renderable -> this.renderable = renderable);
        
        // 点击放置
        arFragment.setOnTapArPlaneListener((hitResult, plane, motionEvent) -> {
            if (renderable == null) return;
            
            Anchor anchor = hitResult.createAnchor();
            AnchorNode anchorNode = new AnchorNode(anchor);
            anchorNode.setParent(arFragment.getArSceneView().getScene());
            
            TransformableNode node = new TransformableNode(
                arFragment.getTransformationSystem());
            node.setParent(anchorNode);
            node.setRenderable(renderable);
        });
    }
}

这个代码虽然简单,但已经包含了ARCore的核心流程:初始化、平面检测、模型渲染、用户交互。光照估计是SceneForm自动处理的,你不需要额外写代码。

AR开发的门槛其实不高,难的是做出让人信服的体验。光照、阴影、遮挡、物理碰撞——这些细节才是决定AR应用好坏的关键。我建议你从最简单的模型放置开始,慢慢加上交互、动画、音效,一步步把体验做丰富。

好了,这一章的内容就到这里。ARCore的世界很大,平面检测只是第一步。后面还有云锚点、多人共享AR、深度API等等,都是很有意思的方向。


公众号:蓝海资料掘金营,微信deep3321