[Diving into WWDC 2017] SceneKit: What's New

SceneKit 的前世今生

背景

SceneKit是苹果 2012 年引入的一个处理 3D 场景的高层框架,底层技术从开始的OpenGL,迁移到后来的Metal。一开始只支持 macOS 平台,在 2014 年的开发者大会上正式通过 iOS 8 引入移动平台,后来随着 watchOS 和 tvOS 的陆续问世,SceneKit 成为了一项全平台 3D 图形处理及场景渲染技术。

概念

3D 场景的处理通常出现在游戏开发中,我们在 App 开发中比较少用到。想要在 iOS 中展现一个 3D 场景,还可以基于以下技术实现:

  • OpenGL ES
  • Unity 3D

OpenGL 是比较底层的图形程序接口集,ES是其三维图形子集,需要对图形学、图像处理技术等专业知识有一定了解,学习门槛较高。而Unity 3D是专门的游戏开发引擎,虽然功能强大但针对简单的 3D 场景,显得过于复杂且上手比较慢。


SceneKit 框架位于底层图形库和CocoaTouch之间,与CoreAnimationCoreImage等位于同一层。它对图形渲染等底层技术做了高层的接口封装,并抽象出几个核心概念。场景图、镜头、光源、几何体、物质、物理系统等 3D 场景中的真实概念,都被抽象成独立的对象并提供了可读性良好的状态获取和设置方法,开发者只需与这些概念打交道就能实现设计稿中的 3D 场景。

SceneKit 简介

首先介绍下 SceneKit 框架中的几个核心概念:

  • SCNView: 3D 画布
  • SCNScene: 3D 场景图
  • SCNNode: 结点
  • SCNCamera: 镜头
  • SCNLight: 光源
  • SCNGeometry: 几何形状
  • SCNMaterial: 物质

说到 3D 场景首先想到的可能是三维空间中的一个物体,比如宇宙中的一个球状的行星。在 SceneKit 中,包含这个行星的宇宙就是一个SCNScene,而这个行星就是一个节点(SCNNode)当然,宇宙中不可能只有一个行星,还有恒星、卫星、流星等,这些都可以通过一个SCNNode对象来描述。而在 SceneKit 的世界中,一个 SCNNode 往往是最基本的单元,可能只够表示一个简单的几何形状(比如一个三角形),所以考虑到有土星这样自带光环的行星,或者月球这样表面凹凸不平、布满环形山的卫星,它们实际需要很多这样的基本几何单元进行组合来描述。

这就涉及到节点间的组合。SCNNode 对象通过addChildNode方法添加孩子节点,每个节点也都根据需要,可以作为另一个节点的孩子节点。这些节点形成一个类似 UIVIew hierarchy 的树形结构,其根节点是固定的,即 SCNScene 对象的rootNode属性。这里 rootNode 是一个虚拟概念,是 readOnly 的,存在的意义就是添加 childNode,连接起当前场景中的所有节点。

SCNScene *scene = [SCNScene scene];  
SCNNode *someNode = [SCNNode node];  
[scene.rootNode addChildNode:someNode];

这样设计的好处是简化节点的处理。假设土星这个 parentNode 由 1000 个形状大小各异的 childNode 组成,如果想给它添加“自转”动作,只需对 parentNode 进行相应操作即可,所有子节点都会执行相同动作,不需要逐个处理。这里可以类比到 UIView 的 addSubView,道理是一样的。

因此可以说在 SceneKit 中,一切皆节点。那么节点本质上是什么东西呢?每个 SCNNode 对象定义了很多属性,比较重要的有:

  • SCNLight:光源
  • SCNCamera:镜头
  • SCNGeometry:几何形状

首先说几何形状,这是比较通俗易懂的。表示这个节点是一个由SCNGeometry类来描述的几何体,是有具象的。比如上面例子中的土星球体。SCNGeometry 对象最重要的属性是 materials 数组,数组元素是SCNMaterial对象。后者是对一种物质或材料的抽象封装。数组就意味着一个几何体可以由多个 SCNMaterial 对象来描述,但是一般在 SCNGeometry 划分粒度较小的情况下,一个 SCNMaterial 对象足矣描述这个几何体的物质特征,SCNGeometry 甚至直接提供了firstMaterial便捷属性来获取它。

SCNGeometry *box = [SCNBox boxWithWidth:5.f height:5.f length:5.f chamferRadius:1.f];  
SCNMaterial *material = [box firstMaterial];  

SCNMaterial 对象描述了某种物质或材料的一些特性,比如光学特性(吸收光特性、漫反射或镜面反射特性、发光、透明等)、表面纹理、物理特性(粗糙度、金属特性等),总之是一些看不太懂的很专业的名词。其目的就是为了使这个 SCNGeometry 对象更拟物,更贴近真实场景中的物体。这些特性都通过SCNMaterialProperty对象表征,其最重要的属性 contents,是一个id类型对象,可以通过 color、image、layer、string、URL、number 等类型的对象进行赋值,来表示具体取值。更详细的可以参考 SCNMaterialProperty 接口文档。

box.firstMaterial.specular.contents = [UIColor whiteColor];  

光源和镜头是比较特殊的两种节点属性。当给一个 SCNNode 对象的light属性赋值时,这个节点也就成为了一个光源节点,是和一般几何体节点平级的。camera类似,通过给 camera 属性赋值得到一个镜头节点。在一个 3D 场景中,这两类节点是必不可少的,否则整个场景不能正常显示或执行预定的动作。这两类节点本身没有太多要说的,SCNLight最重要的是SCNLightType属性,用来指定光源类型,如点光、泛光、平行光等;SCNCamera则暴露出了几乎你能想到的任何数码相机参数,如光圈、广角、HDR 等。

SCNNode *lightNode = [SCNNode node];  
lightNode.light = [SCNLight light];  
lightNode.light.type = SCNLightTypeOmni;  

此外 SCNNode 还有一些简单属性,如namepositionrotation等,这些概念顾名思义。position 是一个SCNVector3(x, y, z)类型的结构体,表示节点在3D场景中的位置。ScenceKit 中 z 轴沿设备垂直方向延伸,屏幕向外是 +z,向内是 -z。rotation 是一个SCNVector4(x, y, z, w)结构体,多了一个表示旋转弧度的参数。此外位置、旋转等属性是 animatable 的,即支持动画效果,这个后面再详述。

回到最初的 SCNScene 宇宙概念,一个 scene 中包含很多节点。其中有一个 rootNode 节点作为这些节点的顶端节点,被 scene 本身持有。那 scene 是否是唯一的?答案是否定的。SCNScene 可以理解成一个容器,承载着很多节点。这样的容器也可以有很多,在上面的例子里,考虑到爱因斯坦的相对论,有无数多个平行宇宙,就有无数多个 SCNScene 对象。但不同的是,SCNScene 对象逻辑上可 merge,方式是节点的集合进行 merge,不能直接 merge SCNScene 对象。在 SceneKit 开发中,scene 可以用于实际场景的划分,比如一个游戏中的主角、关卡、敌人及分数统计显示等,就可以用不同的 SCNScene 对象来定义,分而治之互不干涉,最终通过 merge 其中的节点,展示在根 SCNView 场景中。

通过上面的描述把场景构建完成后运行程序,并不能看到效果。原因是 SCNScene 只是一种描述,必须将这种描述落实到 3D 画布上,才能进行展示。SCNView就是这样的画布,通过设置其 scene 属性在画布上布置设计好的场景,用playpausestop方法来控制其展示。SCNView 的父类是UIView,所以一切我们熟悉的UI设置、层级管理、手势识别等都是通用的,当然也能直接把它设置为一个 UIViewController.view 来展示 3D 场景。

SCNView *scnView = [SCNView new];  
scnView.scene = scene;  
[scnView play:nil];

ok,3D 场景构建完并可以显示出来,但是如何让场景中的物体动起来呢?这就涉及到 SceneKit 的动画(animation)和行动(action)系统。所有 SCNNode 对象都是遵从SCNAnimatableSCNActionable协议的,前者实际上对 CoreAnimation 做了一些封装或桥接,对于节点的 animatable 属性,可以执行CAAnimation类似的动画,如上面提到的 position、rotation,还有opacitytransform等。后者是一个新的动作的概念,通常针对高层节点进行设置,直接作用于动画系统中的 presentation layer,不依赖于节点的某个 animatable 属性。比如让一个土星节点绕 y 轴无限旋转,可以用一行代码实现:

[saturnNode runAction:[SCNAction repeatActionForever:[SCNAction rotateByX:0 y:2 z:0 duration:1]]];

SCNAction 的 repeatActionForever 类方法提供了一个简洁明了的将某个动作封装为无限循环执行的接口,SCNActionable 定义的runAction方法在当前节点上执行这个动作。SceneKit 就是通过这样的动画和动作的方式,让整个 3D 场景动起来,构建出各种各样生动活泼的游戏效果。

最后提一下 SceneKit 复杂且实用的物理系统。通过底层的 Metal 物理引擎,3D 场景中的物体可以模拟现实中的物理效果。比如最简单粗暴的重力效果。SCNScene 对象有个自动创建的 SCNPhysicsWorld 属性,表征这个场景的物理系统。里面定义了重力加速度、速度、碰撞等属性或代理对象,作用于当前场景的所有节点。还可以通过addBehavior 方法添加一些符合物理规律的行为:

- (void)addBehavior:(SCNPhysicsBehavior *)behavior;

如果只是想对特定节点添加物理特效,需要设置 SCNNode 的physicsBody属性,里面定义了很多和自然界中真实物体相关的属性,如反弹特性、表面曲率等,具体可以参考SCNPhysicsBody类接口文档。

Demo time!

通过 Xcode 新建一个 SceneKit 示例工程是非常简单的,苹果已经为我们做好了一切。

新建一个 Game 工程,technology 选择 SceneKit,不修改任何代码的情况下直接 run,会看到一个自动旋转的小飞机。通过 pan、pinch 等手势,可以控制场景中的镜头节点相应调整视角。这里我们做一处修改,给飞机添加最简单的物理特性:重力效果。飞机就会产生缓缓降落的效果,如图所示:

查看 GameViewController 类实现,代码注释写的比较详细,可以参考 3D 场景一步步建立并运行起来的过程:

// 从 scn 文件取出 SCNScene 实例
SCNScene *scene = [SCNScene sceneNamed:@"art.scnassets/ship.scn"];

// 添加一个镜头节点
SCNNode *cameraNode = [SCNNode node];  
cameraNode.camera = [SCNCamera camera];  
[scene.rootNode addChildNode:cameraNode];

// 放置镜头
cameraNode.position = SCNVector3Make(0, 0, 15);

// 添加一个泛光源节点
SCNNode *lightNode = [SCNNode node];  
lightNode.light = [SCNLight light];  
lightNode.light.type = SCNLightTypeOmni;  
lightNode.position = SCNVector3Make(0, 10, 10);  
[scene.rootNode addChildNode:lightNode];

// 添加一个环境光源节点
SCNNode *ambientLightNode = [SCNNode node];  
ambientLightNode.light = [SCNLight light];  
ambientLightNode.light.type = SCNLightTypeAmbient;  
ambientLightNode.light.color = [UIColor darkGrayColor];  
[scene.rootNode addChildNode:ambientLightNode];

// 通过遍历节点树找到主节点:一架飞机
SCNNode *ship = [scene.rootNode childNodeWithName:@"ship" recursively:YES];

// 添加重力效果
ship.physicsBody.type = SCNPhysicsBodyTypeDynamic;  
ship.physicsBody.affectedByGravity = YES;  
ship.physicsBody.mass = 1;

// 获取 SCNView
SCNView *scnView = (SCNView *)self.view;

// 设置SCNView的场景图
scnView.scene = scene;

// 允许用户对镜头进行默认控制
scnView.allowsCameraControl = YES;

// 显示运行时帧率、时间等信息
scnView.showsStatistics = YES;

// 作为UIView,设置其背景色
scnView.backgroundColor = [UIColor blackColor];  

以上只是一个简单的示例 demo。SceneKit 的能力远不止如此,它可以对节点做非常丰富的表面渲染、动态物理效果等设置,让一切场景中的 3D 元素看起来“栩栩如生”。感兴趣的读者可以参照文末的相关资料写个 demo 细细把玩。

回到 demo 工程,art.scnassets是一种 SceneKit 专用的资源打包形式,类似 image assets,统一管理 3D 场景图和图片素材等资料。其中场景文件 ship.scn 其实就是 SCNScene 对象归档化后的磁盘文件,可以进入 Xcode scene 编辑器进行设计。类似于 xib 或 storyBoard,scene 编辑器很强大,可以方便的拖拽 object library 中预置的各种节点到当前 scene 中进行设置。整个 scene 设计通过预览功能可以实现所见即所得:

WWDC 2017 SceneKit 新特性

总体来讲,今年 WWDC 上关于 SceneKit 的 session 没有提出革命性的改进,只是做了一些锦上添花的事情。关键词有四个:

Camera : 镜头

  • 改进效果:增加了一些新的镜头特效,比如运动模糊
  • 提高易用性:主要从开发角度,让开发者更容易设置 camera 参数(因为以往的镜头特性 coding 被吐槽过于复杂,接口易用性差)

Animation Improvement : 动画改进

  • 提出了新的 animation 管理对象,方便控制动画执行过程中的参数

Developer Tools : 开发者工具

  • 工具功能更强大,可以进行 SceneKit 运行时性能 debug,找到掉帧的原因

Related Technologies : 相关技术

  • 与 ARKit 等技术的结合,在真实场景中展现虚拟 3D 物体

下面详细阐述相关技术细节

Camera

通过一个小游戏的例子来体现 SceneKit 对 camera 特性的改进,主要包括三个方面:

  • 更自然的体验 游戏镜头可以“平滑”跟踪主角的移动,平滑体现在两者相对位置需要发生改变时,镜头移动的加减速度改变较平缓、不突兀

  • 更丰富 随着游戏场景切换,镜头追踪主角的方式也会变化,比如主角和敌人战斗时,镜头会被压低以追踪角色动作细节。触发任务提示时,镜头会切换到主场景以外的远处等

  • 更有代入感 跳跃障碍时,镜头不再跟随角色移动,保持让障碍物在游戏场景正中以提高玩家操作的稳定性

SCNCamera 对象本身也增加了一些特性,使其更接近真实的录像设备。如对焦、景深、锐度、运动模糊等参数设置,以及一些光学细节处理,如物体表面曲率影响对环境光的反射率等。

易用性方面,SCNCamera 接口变得更加直观:

  • 镜头追踪角度的设置,从两个坐标轴的度数,变成长度+高度。xfovyfov -> focalLength,sensorHeight

  • 以往通过设置 sceneView 的allowsCameraControl属性控制 camera,但只有一些默认设置。本次给 sceneView 增加了 SCNCameraController 属性,提供更精细的控制

  • 增加了一些 SCNNode 约束类型。这里科普一下,每个 SCNNode 都有SCNConstraint对象构成的数组这么一个 constraints 属性, 用于表示和其他 node 的约束关系。SCNConstraint 对象有很多派生对象,表示不同的约束类型。在现场的演示 demo 中,给 cameraNode 增加对 characterNode 的 lookAt 约束后,镜头就会自动跟踪主角的位置,类似于第一视角游戏,效果可以自行脑补

  • SIMD。给 SCNNode 的一些参数计算提供了更加易用的工具方法

Surface Subdivision and Tessellation

直译就是表面细分和网格化?这部分生词较多,概念也比较专业,理解的比较粗浅。总之就是对 3D 场景中的几何物体划分更小的粒度去控制和渲染,使整体效果看上去更加真实、细腻。实现上是通过底层 Metal 框架的改进来达到效果的。具体到类层面,有如下改进:

  • SCNGeomerty 增加tessellation属性,控制几何体表面的网格化。这里有相当多的专业属性暴露给开发者去设置
  • SCNMaterial 增加displaceMent属性,表示物质表面细节单元的位移。material 层面也扩展了属性集,给物质建模增加了更多的想象空间

Animation

定义了新的SCNAnimationPlayer对象,功能类似原 SCNAnimatable 协议。但是因为实例化了,动画参数设置起来更加直观。 此外底层实现还提高了动画性能,动画开始、暂停等状态切换变得更快。

Developer Tools

之前版本的 Xcode 有专门检查 SceneKit 运行性能的Instruments工具,如 cpu 占用率、帧率、节点数等,但是没有好的调试和定位性能瓶颈的方法。这次对性能参数做了更加细致的划分,并提供类似于其他 Instruments 工具一样定位到代码级别的服务。

  • frame:帧检查
  • rendering time:渲染时间
  • updating:状态更新
  • resource loading:资源加载

Relative Technologies

本次 WWDC 的重头ARKit,其实和老牌的 Scenekit 关系密切。都是渲染 3D 场景,只是前者把它带到了诸如摄像头的真实场景中。负责展示增强现实场景的ARSCNView,本身就是 SCNView 的 subClass,因此所有 SceneKit 特性可用!

现场演示了把 Scenekit 小游戏中的主角,一只可爱的小浣熊,置于手机摄像头拍摄的真实场景中。每个 SCNScene 对象都有一个名为background的 SCNMaterialProperty 属性,之前介绍过 SCNMaterialProperty 属性可以通过设置其contents参数来定制物质特性。一般的 SCNScene 对象,background.contents 设置为 image 贴图或纯颜色,这里通过把浣熊 SCNScene 对象的 background.contents 设置成 ARKit 的场景,实现了 demo 演示的3D增强现实效果:

除了 ARKit,session 中还简略介绍了 Scenekit 与GamekitModel I/OUIFocus等框架的关系,此处不再赘述。

总结

SceneKit 作为苹果 5 年前推出的老牌 3D 渲染技术,经过几年的发展已经变得相对成熟。但是 Unity 等游戏引擎的流行使得 SceneKit 在游戏开发中的地位略显尴尬。随着今年 ARKit 等热门技术的诞生,SceneKit 除了进一步强化自身武功,也期望在未来能与这些热点技术相结合,在更加丰富的应用场景中,被更多开发者认可和使用。

想进一步了解 SceneKit,请参考

WWDC session

Apple documents

SceneKit tutorial

《3D Graphics with Scene Kit》