SceneKit的那些坑

简介

SceneKit是苹果爸爸自带的一个3D引擎,应该是高度封装了OpenGL ES相关的内容,使用起来比OpenGL方便了不止100倍,当然功能上肯定不能和手撸OpenGL相比较啦,但是一些简单的3D场景用它做起来还是比较方便的。比如加载显示个3D模型啥的。

需求

产品想在咱的app里面加一套勋章体系,这个也不是啥新鲜功能啦,纵观各大直播app,勋章体系做的都是66的,但是呢,咱们产品大大看上了QQ的勋章展示方式,如下图:

第一反应是OpenGL ES,后来同事推荐了SceneKit这个框架,于是有了这一篇踩坑之旅。

学习资料

这位同学 的入门教程还不错,不过现在他把教程转为了收费项目,但是代码还是可以在他的Github上下载到的,而且看他的提交记录.md文件里面会有惊喜哦。

so 我这里就直说我踩过的一些坑啦,想学习整个流程的童鞋可以参考上面的教程。在这里先谢谢这位同学啦~~~

坑s

SceneKit是支持两个格式的文件的.scn和.dae 但是 这两种格式好像并没有太大的区别,都可以通过下面2中方式加载到程序中:

通过记事本打开文件,就会发现每个模型(node)都有一个id和name

通过id加载

1
2
3
4
5
NSURL *bundlePathUrl = [[NSBundle mainBundle] bundleURL];
bundlePathUrl = [bundlePathUrl URLByAppendingPathComponent:@"art.scnassets/medle1.dae"];
SCNSceneSource *sceneSource = [SCNSceneSource sceneSourceWithURL:bundlePathUrl options:nil];
SCNNode *myNode = [sceneSource entryWithIdentifier:@"ID4" withClass:[SCNNode class]];

通过name加载

1
2
SCNScene *scene = [SCNScene sceneNamed:@"art.scnassets/medle1.dae"];
SCNNode *myNode = [scene.rootNode childNodeWithName:@"medle1" recursively:YES];

自定义材质(Material)

想要自定义模型的材质,就需要获取到模型的几何属性(geometry),but 自定义的模型加载进来的时候默认的geometry属性是nil的你敢信吗。。。google了一番之后,找到了一个解决方案 先遍历一下这个模型的childNodes 然后修改child的geometry的firstMaterial。。。这也算曲线救国了

这里还有一个坑,美工导出的模型可能会有好几个child我擦嘞。。。这个时候咋办呢 ,取哪个都不对啊, 每个child可能都只是整个模型的一部分而已,让美工的同学先把这几个child合并为一个child,然后再导出试试吧。

材质叠加

美工同学可能建好了模型,并成功导出正确的模型格式,还把材质也一起导出来了,是不是很完美。拿来直接用就可以啦。but 我要再添加一个材质到模型上该如何实现呢(比如我要贴一张图片到模型上),如果我直接修改firstMaterial的话,那么不好意思哦,原来美工导出的材质就被remove掉了。

这个时候就不能直接修改firstMaterial啦,需要修改firstMaterial的diffuse和multiply属性来实现,美工同学导出的材质图片= diffuse.contents 自定义贴图 = multiply.contents。 然后就是微调图片的位置啥的了,通过修改contentsTransform这个属性来实现。

添加射灯

可以看到,qq的勋章在滑动的时候,上面有高亮的金属光泽,这个应该是可以通过添加灯光来实现的。SceneKit有提供4中灯光,分别是环境光,点光源,方向光和聚光灯(我这里叫他射灯)。

首先 环境光和点光源都是必须的 实现高光的话,还需要再添加一个射灯,然后调整照射的位置到你满意就可以啦。见如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (SCNNode *)spotNode{
if (!_spotNode) {
SCNLight *spotLight = [SCNLight light];// 创建光对象
spotLight.type = SCNLightTypeSpot;// 设置类型
spotLight.color = [UIColor whiteColor]; // 设置光的颜色
spotLight.castsShadow = TRUE;// 捕捉阴影
spotLight.attenuationStartDistance = 0;
spotLight.attenuationEndDistance = 100;
spotLight.attenuationFalloffExponent = 2;
spotLight.spotInnerAngle = 0;
spotLight.spotOuterAngle = 30;
_spotNode = [SCNNode node];
_spotNode.position = SCNVector3Make(0, 2, 10); //设置光源节点的位置
_spotNode.light = spotLight;
}
return _spotNode;
}

勋章的旋转

勋章需要支持手势,主要是通过滑动手势旋转勋章,而且需要在不同滑动力度时旋转不同的角度,当然,最后旋转的角度都是M_PI的整数倍,所以勋章才会始终保持正面或者背面面向屏幕而不是侧面面向屏幕了。

手势的选择

手势识别我尝试了2种方案,第一种是touchBegan系列响应事件,还有一种是添加UIPanGestureRecognizer手势,其实二者的处理方案是差不多的,但是使用touch系列方法不能检测到滑动的速度,而UIPanGestureRecognizer有提供检测速率的方法,so 当然是选择UIPanGestureRecognizer啦~

属性的选择

然后就是选择改变node的哪个属性来实现旋转了,SCNNode有好几个属性都是可以旋转node,比如transform,pivot,rotation等,不同点是前两个是矩阵运算,后一个是向量运算。这里我也做了2种尝试,最初我使用的是pivot,在处理旋转角度小于M_PI时还行,但是如果想让勋章旋转一圈或者几圈时,无论我的角度设置的是多大,最终效果都只能旋转M_PI。好吧,然后我就转向使用rotation旋转勋章,这里还有一点需要注意的,要保证勋章是平滑的转动而不会出现突出旋转到某个方向,必须要在勋章的初始角度上进行加减,并保存勋章旋转后的角度以便下一次旋转时计算角度值。

动画

当手指离开屏幕后,勋章会根据手指滑动的速率和方向做一个旋转的弹簧动画,因为SCNNode本身是支持CA动画的,所以我首先想到的就是使用CA动画去做,ok,第一次旋转是木有问题的,但是动画结束之后,如果你再去旋转勋章你会发现,勋章完全不动了,不管你的旋转角度是多少。。。我想可能是因为CA动画本身并没有改变rotation的值,所以当我再次旋转时计算的角度值并不是从当前勋章的其实角度开始的。

CA动画不行,还好Scenekit提供了另外一种动画的实现方式,SCNTransaction动画,类似于CATransaction的使用方式。好吧 动画这块的效果终于差不多搞定了。

自定义贴图

勋章的背面需要显示用户的相关信息,这些信息都是动态的,不可能是美工同学在建模的时候加,只能是通过代码的方式动态的加上去。

因为图片可以直接作为材质的contents,so 我将需要显示的信息放到一个label上,然后对这个label做截图操作就可以得到我想要的图片文件了。

SCNNode有很多的材质相关的属性,而且还有一个材质列表,模型所有使用的材质都在这个列表中可以找到,所以我的做法就是取出你需要改变的材质,将自定义贴图和当前材质做一个合并,然后使用合并后的材质替换之前的。

这里有点需要注意的就是调试贴图到正确的位置,因为图片的坐标系是01坐标系,所以在调试的时候参数需要尽量小,慢慢调,如果你合并材质和替换都成功了但是在模型上面看不到图片,可能就是图片的位置不对。

模型的动态加载

Xcode在build的时候,会把项目中引用了的.dae模型(这里指本地模型)通过一个脚本copy到.ipa中,copy的过程中可能对模型的格式进行了特殊处理,因为copy后的文件比原文件小了很多。

如果我们要动态加载模型的话,Xcode是没有对模型进行copy操作的,我们直接使用设计给的模型文件,在模型被保存到沙盒中后,通过文件路径加载无法正常加载出模型,后来在网上找到这个解决方案这里还有一篇中文的,内容差不多,先将模型用Xcode在build时使用的脚步copy一份,然后把copy的模型文件给服务器,下载后就可以正常显示了。

相当于我们帮Xcode做了copy的操作。

这是我的demo