1、概述
在前面一篇文章 ARCore相关 里已经讨论过一些关于AR 和ARCore的概念了,并基于OpenGL ES实现了一个ARApp 的demo。OpenGL ES是内置在Android系统中的一个图像处理框架,这个框架相对来说学习曲线较陡,并且通过其来调用ARCore所要写的重复代码较多。官方也发现了这个问题,所以在2017年推出ARCore之后,在2018年的IO大会上正式推出了Sceneform这个全新的用于Java / Android开发人员的3D框架。这篇文章来简单讨论下如何使用Sceneform来构建ARApp。先来看一下最终效果:
如上面的gif所示,点击ADDBALL按钮会在地面上出现一个球体,并且这个球体在自动的旋转。下面来详细讨论下这个是怎么实现出来的。
2、前期准备
2.1 获取3D模型
为了在屏幕上显示上面的球体,首先需要拥有这个球体的3D模型。一般来说每个3D模型通常称为资产,它所承载的纹理或皮肤通常称为材质。有许多免费的3D模型网站可供使用。官方提供了一个很好的地方叫做Poly,在那里可以得到漂亮的3D模型。上面的球体模型就是从那里下载来的 球体模型(要梯子)。Sceneform支持渲染OBJ,FBX,glTF格式的模型。当然如果有专门的3D模型师根据你的需求帮你量身打造3D模型那就更棒啦。
2.2 在项目中配置Sceneform
要使用Sceneform需要在build.gradle的dependencies配置对其的依赖,到我写这篇文章时候其版本是1.5.0。
implementation"com.google.ar.sceneform.ux:sceneform-ux:1.5.0"
2.3 安装Sceneform工具插件
Android studio为我们提供了插件Google Sceneform Tools。安装步骤如下(这是OS X下的Android studio顺序,Windows下的类似):
① Android Studio- > Preferences- >Plugins
② 选择Browse repositories…- >搜索Google Sceneform Tools
③ 点击 Install
这个工具的作用如下:
① 以将所有支持的3D模型导出到我们的项目中。
② 为模型自动配置gradle。每当导出模型时,需要注入gradle中的几个配置。
sceneform.asset('sampledata/mosaicball.obj',
'default',
'sampledata/mosaicball.sfa',
'src/main/assets/mosaicball')
③ 可视化3D模型,这个工具将为提供了一个3D查看器,在导出后将能够看到模型在应用程序中的样子。
2.4 导出资产
有了heartform工具插件,就可以导出到项目中了。为了保存所有数据,可以在项目app层级下面新建一个sampledata文件夹。将所有下载的内容(mosaicball.mtl和mosaicball.obj)复制到该文件夹下。现在在项目中有模型,现在需要将它们导出到Sceneform资产和二进制文件。这时候就要使用上面下载的插件了。右键单击mosaicball.obj - >Import Sceneform Asset
通过导出,该插件将创建2个文件mosaicball.sfa(SceneForm资产描述)和mosaicball.sfb(SceneForm二进制)。二进制文件将随APK一起打包进去,并保留在assets文件夹中。转换后的描述文件sfa将不会被打包进apk。但这个文件可以用来更改对象属性,通过修改它来修改sfb。
如上图点击finish按钮后,gradle会自动构建资产,同时在sampledata文件夹会生成mosaicball.sfa,在assets文件夹会生成mosaicball.sfb。同时双击sfa或者sfb文件,Aandroid Studio 就会显示该模型。
3、代码实现
3.1 配置AndroidManifest
首先AR是显示与虚拟的结合,所以一定要获取相机权限,同时还需要添加一个关于AR的feature 来判断手机是否支持AR功能。同时要在<application>中添加元数据。
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera.ar" android:required="true" />
<application
...>
<meta-data android:name="com.google.ar.core" android:value="required" />
...
</application>
3.2 创建ARFragment
ARFragment 为开发者封装了权限查询请求等操作,使用起来非常简单,只要直接在布局文件中插入fragment 标签,并且对其name属性设置为 "com.google.ar.sceneform.ux.ArFragment" 其他的就可以像一个正常的fragment一样去设置其大小位置等属性了。
<fragment
android:id="@+id/ux_fragment"
android:name="com.google.ar.sceneform.ux.ArFragment"
...
/>
3.3 初次运行
如果不出意外这时候可以试着运行一下程序,此时会在支持ARCore的手机中看到以下操作ArFragment处理权限,ARCore会话创建和平面发现UI。同时对于初次运行会弹出相机许可请求对话框。
权限请求同意后,会看到检测平面的指示器。这时候需要左右前后移动一下手机,使其来检测具有一些“纹理”或图案的平面。这边说明一下对于纯白色的平面,就目前来讲检测效果并不理想。
当成功检测到平面就可以看到平面上散布着的白点,这些白点就是之前一篇文章里面说到的PointCloud。
至此已经成功将ARCore给运行起来了。
3.4 添加3D模型
前面的ArFragment已经检测到平面,这一步要在其上面放置之前获取到的3D模型。为了方便起见下面的代码实现了将3D对象添加到ArFragment中心,当然必须在中心位置有检测到平面即存在PointCloud。整个Activity代码如下所示:
public class MainActivityextends AppCompatActivity {
ArFragment fragment;
Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
fragment = (ArFragment)getSupportFragmentManager().findFragmentById(R.id.ux_fragment);
button = findViewById(R.id.button);
button.setOnClickListener(v -> addObject(Uri.parse("mosaicball.sfb")));
}
private void addObject(Uri parse) {
Frame frame =fragment.getArSceneView().getArFrame();
android.graphics.Point point = getScreenCenter();
if (frame !=null) {
List hits = frame.hitTest(point.x, point.y);
for (HitResult hit : hits) {
Trackable trackable = hit.getTrackable();
if (trackableinstanceof Plane && ((Plane)trackable).isPoseInPolygon(hit.getHitPose())) {
placeObject(fragment, hit.createAnchor(), parse);
break;
}
}
}
}
private void placeObject( ArFragment fragment, Anchor createAnchor, Uri model) {
ModelRenderable.builder()
.setSource(fragment.getContext(), model)
.build()
.thenAccept(modelRenderable -> addNodeToScene(fragment,createAnchor, modelRenderable))
.exceptionally(throwable -> {
AlertDialog.Builder builder =new AlertDialog.Builder(MainActivity.this);
builder.setMessage(throwable.getMessage())
.setTitle("error!");
AlertDialog dialog = builder.create();
dialog.show();
return null;
}) ;
}
private void addNodeToScene( ArFragment fragment, Anchor createAnchor, ModelRenderable renderable) {
AnchorNode anchorNode =new AnchorNode(createAnchor);
TransformableNode transformableNode =new TransformableNode(fragment.getTransformationSystem());
transformableNode.setRenderable(renderable);
transformableNode.setParent(anchorNode);
fragment.getArSceneView().getScene().addChild(anchorNode);
transformableNode.select();
}
private android.graphics.Point getScreenCenter() {
View vw = findViewById(R.id.ux_fragment);
return new Point(vw.getWidth() /2, vw.getHeight() /2);
}
}
这里对有些关键的步骤在下面进行说明。
3.4.1 addObject()
传入模型的Uri,然后识别屏幕中心并对该特定点执行命中测试。如果它是可追踪的,也就是说命中了PointCloud上的点,那么就调用placeObject()来进行下一步操作。命中测试说白来就是测试从屏幕中发出的点是不是能击中PointCloud上的点。
3.4.2 placeObject()
这个方法里面其实只执行了一行语句。在这里调用了Sceneform框架提供给我们的API,其作用是用来异步加载3D模型,并提供一种类似RXJava形式的函数式编程思想来处理错误情况。如果加载模型成功就调用addToNode()。
3.4.3 addNodeToScene()
该方法接受3个参数:Renderable,Anchor和ArFragment。其中Renderable是加载成功的3D模型,Anchor就是前面被击中的PointCloud上的点,在这里由上面讲到的可知是ArFragment的中心位置。
这边简单说明一下节点(Node)的概念,其包含Sceneform渲染,包括其位置,方向和可渲染对象以及与其交互(包括其碰撞形状和事件侦听器)所需的所有信息。节点可以添加到其他节点,形成父子关系。当一个节点是另一个节点的子节点时,它会随着它的父节点移动,旋转和缩放。一个节点可以有多个子节点,但只有一个父节点,从而形成一个树状结构,这种结构称为场景图(the scene graph)。
在该方法里面创建了两个节点AnchorNode和TransformableNode。其中AnchorNode用来使对象保持在特定的指示位置。而TransformableNode可以使用手势选择,移动,旋转和缩放的节点。
为TransformableNode设置renderable也就是3D模型,然后将父节点指定为AnchorNode。这意味着将模型置于该锚点位置,并且它是可放大缩小的并且可以进行移动。但移动的时候其实是TransformableNode在移动而AnchorNode其实还是不动的。
3.5 3D模型动起来
到上面最基础的一个ARApp已经基本完成了,相比最前面演示的只差最后一步就是让它动起来。这边我用的是自定义了一个Node,并用属性动画来使其不停的转动。自定义Node代码如下:
public class RotatingNode extends Node {
private ObjectAnimator rotationAnimation = null;
private float degreesPerSecond = 90.0f;
private float lastSpeedMultiplier = 1.0f;
private Float speedMultiplier = 1.0f;
private Long animationDuration =(long) ((1000 * 360) / (degreesPerSecond * speedMultiplier));
// 重载方法节点激活时调用
@Override
public void onActivate() {
super.onActivate();
startAnimation();
}
// 重载方法,节点取消激活状态时调用
@Override
public void onDeactivate() {
super.onDeactivate();
stopAnimation();
}
// ARCore 每一处理帧都会调用一次
@Override
public void onUpdate(FrameTime frameTime) {
super.onUpdate(frameTime);
if (rotationAnimation == null) {
return;
}
Float speedMultiplier = this.speedMultiplier;
// 如果速度没变就继续以之前速度运行.
if (lastSpeedMultiplier == speedMultiplier) {
return;
}
if (speedMultiplier == 0.0f) {
// 转速为0则停止旋转
rotationAnimation.pause();
} else {
// 速度改变重新运行属性动画
rotationAnimation.resume();
float animatedFraction = rotationAnimation.getAnimatedFraction();
rotationAnimation.setDuration(animationDuration);
rotationAnimation.setCurrentFraction(animatedFraction);
}
lastSpeedMultiplier = speedMultiplier;
}
// 设置速度
void setDegreesPerSecond( Float degreesPerSecond) {
this.degreesPerSecond = degreesPerSecond;
}
// 启动动画
private void startAnimation() {
if (rotationAnimation != null) {
return;
}
rotationAnimation = createAnimator();
rotationAnimation.setTarget( this);
rotationAnimation.setDuration(animationDuration);
rotationAnimation.start();
}
// 停止动画
private void stopAnimation() {
if (rotationAnimation == null) {
return;
}
rotationAnimation.cancel();
rotationAnimation = null;
}
// 返回一个 ObjectAnimator 用来使节点旋转起来
private ObjectAnimator createAnimator() {
// 节点的位置和角度信息设置通过Quaternion来设置
// 创建4个Quaternion 来设置四个关键位置
Quaternion orientation1 = Quaternion.axisAngle(new Vector3(0.0f, 1.0f, 0.0f), 0f);
Quaternion orientation2 = Quaternion.axisAngle(new Vector3(0.0f, 1.0f, 0.0f), 120f);
Quaternion orientation3 = Quaternion.axisAngle(new Vector3(0.0f, 1.0f, 0.0f), 240f);
Quaternion orientation4 = Quaternion.axisAngle(new Vector3(0.0f, 1.0f, 0.0f), 360f);
ObjectAnimator rotationAnimation =new ObjectAnimator();
rotationAnimation.setObjectValues(orientation1, orientation2, orientation3, orientation4);
// 设置属性动画修改的属性为localRotation
rotationAnimation.setPropertyName( "localRotation");
// 使用Sceneform 框架提供的估值器 QuaternionEvaluator 作为属性动画估值器
rotationAnimation.setEvaluator(new QuaternionEvaluator());
// 设置动画重复无限次播放。
rotationAnimation.setRepeatCount(ObjectAnimator.INFINITE);
rotationAnimation.setRepeatMode( ObjectAnimator.RESTART);
rotationAnimation.setInterpolator(new LinearInterpolator());
rotationAnimation.setAutoCancel(true);
return rotationAnimation;
}
}
有了上面的自定义旋转类以后只要稍稍修改一下addNodeToScene()方法就可以来,如下所示:
private void addNodeToScene( ArFragment fragment, Anchor createAnchor, ModelRenderable renderable) {
AnchorNode anchorNode =new AnchorNode(createAnchor);
RotatingNode rotatingNode =new RotatingNode();
TransformableNode transformableNode =new TransformableNode(fragment.getTransformationSystem());
// transformableNode.setRenderable(renderable);
// transformableNode.setParent(anchorNode);
rotatingNode.setRenderable(renderable);
rotatingNode.addChild(transformableNode);
rotatingNode.setParent(anchorNode);
fragment.getArSceneView().getScene().addChild(anchorNode);
transformableNode.select();
}
至此再运行程序就能看到一开始所展示的效果了。