格式
一般 3D 素材都是外部工具开发的,Xcode 支持的常见类型有 .dae 和 .obj 格式,其中 dae 格式支持多模型和动画,最为好用。
异常
在 3D 素材使用的过程,我们也经常遇到各种问题,常见的有下面几种:
尺寸和坐标
3D 文件由外部工具创建,再导出,然后放在 Xcode 中使用。由于不同工具的特性,常常遇到 3D 模型尺寸过大/过小,坐标原点偏离,或者旋转。比如下图中的模型,原始坐标 y 轴是水平的,而且尺寸过大,1 米左右:
一般我们可以在模型中添加一个名为 base 的 Node,再把模型放在这个结点下面,成为它的子结点,这样就相当于把耳机模型 y 轴竖起来了:
尺寸过大/过小问题,也可以通过再套一层 Node 并设置 Scale 来解决。真是应了那句名言:没有什么是加一层封装解决不了的,如果有,那就加两层。
内存过大
Xcode 对 3D 格式的支持并不完美,有时一个特殊的 3D 模型文件就可以造成 Xcode 卡死,或者 app 运行时卡死。
最常见的是对 .dae 文件的复杂动画支持不完美,一个 50M 大小的 dae 文件,占用内存 就可能高达 1.2G,一下子就把 app 就卡死了。
如果判断这种问题呢?其实很简单,只要把 .dae 文件转换成 .scn,再看一下它的大小就知道了。
莫名崩溃
有时还有遇到的情况时,3D 模型在添加或移除时,又或者 ARSCNView 销毁时,偶然会遇到崩溃。一般堆栈信息是 C3DAnimation 之类的,很可能就是因为 3D 模型存在问题。
解决的办法,要么重新导出模型,要么将模型转为 .scn 格式。毕竟 .scn 格式的兼容性,要好得多。
加载
要使用 3D 模型,我们还需要将模型加载到内存中,然后显示在屏幕上
直接加载
简单常用
SCNScene *scene = [SCNScene sceneNamed:@"mode.dae"];
SCNNode *modelNode = [scene.rootNode childNodeWithName:@"base" recursively:YES];
按设置加载
除了直接加载,也可以根据设置来加载,比如说,加载但不播放动画:
NSURL *url = [[NSBundle mainBundle] URLForResource:self.ballModel.model3DName withExtension:nil];
SCNScene *scene = [SCNScene sceneWithURL:url options:@{SCNSceneSourceAnimationImportPolicyKey:SCNSceneSourceAnimationImportPolicyDoNotPlay} error:nil];
SCNNode *modelNode = [scene.rootNode childNodeWithName:@"base" recursively:YES];
如果感觉不够用的话,还可以使用SCNSceneSource类,它的加载方法可以监听加载进度
- (nullable SCNScene *)sceneWithOptions:(nullable NSDictionary<SCNSceneSourceLoadingOption, id> *)options statusHandler:(nullable SCNSceneSourceStatusHandler)statusHandler;
加载卡顿优化
加载的卡顿一般分为两个方面:
- 从磁盘读取文件,并创建对象,加载到内存中;
- 将内存中的模型对象和贴图纹理,加载到 GPU 上,以供显示;
所以我们可以从这两方面来入手优化:
- 用后台线程来读取文件,减少主线程压力;
- 提前将模型和贴图加载到 GPU 上,减少显示时压力;
第一步就是常规的多线程就可以了,第二步我们使用 ARSCNView 提供的方法来优化:
// 切换后台线程,加载模型到内存
dispatch_async(dispatch_get_global_queue(0, 0), ^{
SCNScene *scene = [SCNScene sceneNamed:@"mode.dae"];
SCNNode *modelNode = [scene.rootNode childNodeWithName:@"base" recursively:YES];
dispatch_async(dispatch_get_main_queue(), ^{
// 这个方法必须在主线程调用。它的作用是:当 GPU 相对空闲时,用 CPU 的后台线程将模型和贴图等,从内存传输到 GPU 上,然后在主线程回调。这样减小了 GPU 带宽压力 和 CPU 主线程的压力
[scnView prepareObjects:@[modelNode] withCompletionHandler:^(BOOL success) {
if (success) {
[self addChildNode:modelNode];
}
}];
});
});
ps:
prepareObjects方法并不全能,当你的贴图中有 SKScene 时,就会崩溃;另外,贴图中有 CALayer 的话,虽然可以正常使用prepareObjects方法,但在后台线程加载模型可能会导致显示不出来。