https://github.com/rectinajh/ScenkitDemo
Scene Kit是一个苹果Cocoa风格的3D渲染框架,该框架被引入OS X是在WWDC 2012 (那时 OS X 系统还在用喵系命名)。在第一版通用 3D 渲染器发布后,一年内又陆续增加了像 shader (着色器) 修改器、节点约束、骨骼动画等几个强大的特性 (随 Mavericks 发布)。 今年(2014年),Scene Kit 变的更加强大,支持了粒子效果、物理引擎、脚本事件以及多通道分层渲染等多种技术,以及(也许对许多人来说最重要的)它被引入iOS,而苹果现在指的是把它当成“休闲游戏”的套件。
从一开始,我发现Scene Kit的最大优势和差异在于与其他图形框架,如Core Image,Core Animation,Sprite Kit的集成。 这在其他游戏引擎中可不常见,但是如果你是一个业余爱好者,或者主要是Cocoa 或 Cocoa Touch 框架下的开发者,那么这意味着很多东西应该很亲切了。
Scene Kit 概要
Scene Kit 构建在OpenGL之上,其中灯光,几何图形,材料和相机等高级引擎特性,这些组件都是面向对象的,你可以用熟悉的 Objective-C 或 Swift 语言来编写代码。 如果用过最早的版本的OpenGL,那时还没有 shader,只能苦逼的使用各种底层受限制的 API 开发。 幸运的是 Scene Kit 就好了很多,高级配置对于大多数常见任务是足够的 - 甚至更先进的功能,如动态阴影和景深效果,使用它提供的上层 API 来配置,就已经足够了。 不仅如此,Scene Kit 还允许你直接调用底层 API,或自己写 shader 进行手动渲染 (GLSL)。
节点 (Nodes)
除了灯光,几何图形,材料和相机这几个具体的对象之外,Scene Kit还使用节点层次来组织其内容。 每个节点相对于其父节点具有位置,旋转和缩放,而父节点又相对于其父节点,一直向上,直到根节点。 假如要给一个节点确定一个位置,就必须将它挂载到节点树中的某个节点上。 使用以下方法管理节点层次结构:
addChildNode(_:)
insertChildNode(_: atIndex:)
removeFromParentNode()
这些方法与 iOS 和 OS X 中管理 view 和 layer 层级方法如出一辙。
几何模型对象
Scene Kit 内建了几种简单的几何模型,如盒子、球体、平面、圆锥体等,对于游戏,一般都会从文件中加载3D模型。你可以通过制定文件名来导入 (或导出) COLLADA 格式的模型文件:
let chessPieces = SCNScene(named: "chess pieces") // SCNScene?
如果一个从文件里加载的场景可以全部显示时,将其设置成SCNView 的 scene 就好了。 如果场景包含多个对象,但是屏幕上只能显示一些对象,则可以它们的名称找到它,并将其添加到SCNView中呈现的Scene中:
if let knight = chessPieces.rootNode.childNodeWithName("Knight", recursively: true) {
sceneView.scene?.rootNode.addChildNode(knight)
}
这是一个对导入文件原始节点的引用,其中包含了任一和每一个子节点,也包括了模型对象 (包括其材质),光照,以及绑定在这些节点上的摄像机。只要传入的名字一样,不论调用多少次,返回的都是对同一个对象的引用。
若需要在场景中拥有一个节点的多个拷贝,如在一个国际象棋棋盘上显示两个马,你可以对马这个节点进行 copy 或 clone (递归的copy)。这将会拷贝一份节点的引用,但两份引用所指向的材质对象和模型对象仍然是原来那个。所以,想要单独改变副本材质的话,需要再copy一份模型对象,并对这个新的模型对象设置新材质。copy一个模型对象的速度仍然很快,开销也不高,因为副本引用的顶点数据还是同一份。
带有骨骼动画的模型对象也会拥有一个皮肤对象,它提供了对骨骼中各个节点的访问接口,以及管理骨骼和模型间连接的功能。每个单独的骨骼都可以被移动和旋转,而复杂的动画需要同时对多块骨骼进行操作,如一个角色走路的动画,很可能就是从文件读取并加到对象上的 (而不是用代码一根骨头一根骨头的写)。
光照
Scene Kit 中完全都是动态光照,使用起来一般会很简单,但也意味着与完整的游戏引擎相比,光照这块进步并不明显。Scene Kit 提供四种类型的光照:环境光、定向光源、点光源和聚光灯。
通常来说,旋转坐标轴和变换角度并不是设定光照的最佳方法。下面的例子表示一个光照对象通过一个节点对象来设置空间坐标,再通过 "look at" 约束,将光照对象约束到了目标对象上,即使它移动,光照也会一直朝向目标对象。
let spot = SCNLight()
spot.type = SCNLightTypeSpot
spot.castsShadow = true
let spotNode = SCNNode()
spotNode.light = spot
spotNode.position = SCNVector3(x: 4, y: 7, z: 6)
let lookAt = SCNLookAtConstraint(target: knight)
spotNode.constraints = [lookAt]
动画
Scene Kit 的对象中绝大多数属性都是可以进行动画的,就像 Cocoa (或 Cocoa Touch) 框架一样,你可以创建一个 CAAnimation 对象,并指定一个 key path (甚至可以 "position.x") ,然后向一个对象施加这个动画。同样的,你可以在 SCNTransaction 的 "begin" 和 "commit" 调用间去改变值,和刚才的 CAAnimation 非常相似:
let move = CABasicAnimation(keyPath: "position.x")
move.byValue = 10
move.duration = 1.0
knight.addAnimation(move, forKey: "slide right")
Scene Kit 也提供了像 Sprite Kit 那样的 action 形式的动画 API,你可以创建串行的动画组,也支持自定义 action 来协同使用。与 Core Animation 不同的是,这些 action 作为游戏循环的一部分执行,在每一帧都更新模型对象的值,而不只是更新表现层的节点。
假如你之前用过 Sprite Kit,会发现 Scene Kit 除了变成了 3D 之外,没有太多陌生的东西。目前,在 iOS8 (首次支持 Scene Kit) 和 OS X 10.10 下,Scene Kit 和 Sprite Kit 可以协同工作:对 Sprite Kit 来说,3D 模型可以与 2D 精灵混合使用;对 Scene Kit 来说,Sprite Kit 中的场景和纹理可以作为 Scene Kit 的纹理贴图,而且 Sprite Kit 的场景可以作为 Scene Kit 场景的蒙层 (如3D游戏中的2D菜单面板,译者注。两套非常像的API和概念 (像场景啊,节点啊,约束啊两边都有), 让人容易混淆。
开始用 Scene Kit 写游戏
不仅是动作和纹理,Scene Kit 和 Sprite Kit 还有很多相同之处。当开始写游戏的时候,Scene Kit 和它 2D 版本的小伙伴非常相似,它们的游戏循环步骤完全一致,使用下面几个代理回调:
1,更新场景
2,应用动画/动作
3,模拟物理效果
4,应用约束
5,渲染
这些回调在每帧被调用,并用来执行游戏相关的逻辑,如用户输入,AI (人工智能) 和游戏脚本。
处理用户输入
Scene Kit 与普通 Cocoa 或 Cocoa Touch 应用使用一样的机制来处理用户输入,如键盘事件、鼠标事件、触摸事件和手势识别,而主要区别在于 Scene Kit 中只有一个视图,场景视图 (scene view) 。像键盘事件或如捏取、滑动、旋转的手势,只要知道事件的发生就好了,但像鼠标点击,或触碰、拖动手势等就需要知道具体的事件信息了。
这些情况下,scene view 可以使用 -hitTest(_: options:) 来做点击测试。与通常的视图只返回被点击的子 view 或子 layer 不同,Scene Kit 返回一个数组,里面存有每个相交的模型对象以及从摄像机投向这个测试点的射线。每个 hit test 的结果包含被击中模型的节点对象,也包含了交点的详细信息 (交点坐标、交点表面法线,交点的纹理坐标)。多数情况下,知道第一个被击中的节点就足够了:
if let firstHit = sceneView.hitTest(tapLocation, options: nil)?.first as? SCNHitTestResult {
let hitNode = firstHit.node
// do something with the node that was hit...
}
扩展默认渲染流程
光照和材质的配置方法很易用,但有局限性。假如你有写好的 OpenGL 着色器 (shader),可以用于完全自定制的进行材质渲染;如果你只想修改下默认的渲染,Scene Kit 暴露了 4 个入口用于插入 shader代码 (GLSL) 来改变默认渲染。Scene Kit 在不同入口点分别提供了对旋转矩阵、模型数据、样本贴图及渲染后输出的色值的访问。
比如,下面的 GLSL 代码被用在模型数据的入口点中,可以将模型对象上所有点沿 x 轴扭曲。这是通过定义一个函数来创建一个旋转变换,并将其应用在模型的位置和法线上。同时,也自定义了一个 "uniform" 变量来决定对象该如何被扭曲。
// a function that creates a rotation transform matrix around X
mat4 rotationAroundX(float angle)
{
return mat4(1.0, 0.0, 0.0, 0.0,
0.0, cos(angle), -sin(angle), 0.0,
0.0, sin(angle), cos(angle), 0.0,
0.0, 0.0, 0.0, 1.0);
}
#pragma body
uniform float twistFactor = 1.0;
float rotationAngle = _geometry.position.x * twistFactor;
mat4 rotationMatrix = rotationAroundX(rotationAngle);
// position is a vec4
_geometry.position *= rotationMatrix;
// normal is a vec3
vec4 twistedNormal = vec4(_geometry.normal, 1.0) * rotationMatrix;
_geometry.normal = twistedNormal.xyz;
着色修改器 (Shader modifier) 既可以绑定在模型对象上,也可以绑定在它的材质对象上。这两个类都完全支持 key-value coding (KVC),你可以指定任意 key 进行赋值。在 shader 中声明的 "twistFactor" uniform 变量使得 Scene Kit 在这个值改变时自动重新绑定 uniform,这使得你也可以用 KVC 来实现:
torus.setValue(5.0, forKey: "twistFactor")
使用这个 key path 的 CAAnimation 也 ok:
let twist = CABasicAnimation(keyPath: "twistFactor")
twist.fromValue = 5
twist.toValue = 0
twist.duration = 2.0
torus.addAnimation(twist, forKey: "Twist the torus")
延时着色
即使在纯 OpenGL 环境下,有些图像效果也无法通过一次渲染 pass 完成,我们可以将不同 shader 进行序列操作,以达到后续处理的目的,称为延时着色。Scene Kit 使用 SCNTechnique 类来表示这种技术。它使用字典来创建,字典中定义了绘图步骤、输入输出、shader 文件、符号等等。
第一个渲染 pass 永远是 Scene Kit 的默认渲染,它输出场景的颜色和景深。如果你不想这时计算色值,可以将材质设置成"恒定"的光照模型,或者将场景里所有光照都设置成环境光。
比如,从 Scene Kit 渲染流程的第一个 pass 获取景深,第二个获取法线,第三个对其执行边界检测,你即可以沿轮廓也可以沿边缘画粗线:
参考资料:
https://developer.apple.com/documentation/scenekit
https://developer.apple.com/videos/play/wwdc2014/609/
https://developer.apple.com/videos/play/wwdc2013/500/
https://www.objc.io/issues/18-games/scenekit/