SceneKit框架详细解析(八) —— SceneKit 3D编程入门(一)

版本记录

版本号 时间
V1.0 2021.07.20 星期二

前言

SceneKit使用高级场景描述创建3D游戏并将3D内容添加到应用程序。 轻松添加动画,物理模拟,粒子效果和逼真的基于物理性的渲染。接下来这几篇我们就详细的解析一下这个框架。感兴趣的看下面几篇文章。
1. SceneKit框架详细解析(一) —— 基本概览(一)
2. SceneKit框架详细解析(二) —— 基于SceneKit的简单游戏示例的实现(一)
3. SceneKit框架详细解析(三) —— 基于SceneKit的简单游戏示例的实现(二)
4. SceneKit框架详细解析(四) —— 基于SceneKit的简单游戏示例的实现(三)
5. SceneKit框架详细解析(五) —— 基于SceneKit的简单游戏示例的实现(四)
6. SceneKit框架详细解析(六) —— 基于SceneKit的简单游戏示例的实现(五)
7. SceneKit框架详细解析(七) —— 基于SceneKit的简单游戏示例的实现(六)

开始

首先我们看下主要内容:

通过构建对太阳系进行建模的应用程序,了解如何使用 SceneKit 进行 3D 图形编程。内容来自翻译

接着看下写作环境:

Swift 5, iOS 14, Xcode 12

下面就是正文了。

你有没有想过制作自己的视频游戏?或者您想通过添加漂亮的 3D 图形让您的 iOS 应用程序脱颖而出? 3D 图形编程的世界可能令人生畏。在着色器、采样器、mipmaptessellation之间,很难知道从哪里开始。幸运的是,在 iOS 上,您可以使用 Apple 用于 3D 编程的高级 API SceneKit 开始!

SceneKit 为您处理了许多繁琐的 3D 编程工作,让您可以专注于使您的游戏或应用程序独一无二的内容。在像 Metal 这样的低级 API 中,你需要处理物理和数学问题。另一方面,SceneKit 抽象了很多这种复杂性,使您可以轻松地用代码编写场景。

或者,您可以使用 Xcode 强大的Scene Editor来布置场景,类似于使用Interface Builder制作storyboards。更重要的是,SceneKit 与您创建大片视频游戏可能需要的一切顺利集成:Metal、GameplayKit、Model I/O 等等!

在本教程中,您将使用 SceneKit 将平淡无奇的 2D 太阳系应用程序推进到交互式 3D 世界。在此过程中,您将了解:

  • 3D 编程的基础。
  • 在代码或Scene Editor中创建和修改场景。
  • 节点、几何形状和材料。
  • 照明如何决定场景的外观。
  • 使用相机和约束改变用户看到的内容。

准备好进入 3D 图形世界!

下载并打开入门项目。在 Xcode 中打开 starter 项目,然后构建并运行。

Solar Scenes 是一款精美、内容丰富的应用程序,旨在向人们介绍他们所居住的太阳系的事实。至少,当您完成它时会这样。

现在,它缺少美丽的部分。 添加它是你的工作。

到目前为止,Solar Scenes仅涵盖五颗行星:

  • Mercury
  • Venus
  • Earth
  • Mars
  • Saturn

完成本教程后,您就可以添加缺失的行星了——也许还有一些您自己的额外创作!


Exploring Solar Scenes

在 Xcode 中,在项目导航器中查看应用程序的结构。

以下是每个group的工作:

  • Data Model:托管不同的行星和有关它们的一些事实。
  • UI:包含各种 SwiftUI 控件,用于设置当前 UI 的样式。
  • ContentView.swift:在 ViewModel.swift的帮助下,这是应用程序的主体。在这里,您将加载使应用程序栩栩如生的 SceneKit 场景。

Creating Your First Scene

要创建场景,您将使用 Xcode 的Scene Editor来管理scene file。 在 Xcode 的菜单栏中,选择 File ▸ New ▸ File... 并选择 SceneKit Scene File 模板。

单击Next,并将文件命名为 Solar Scene

然后,单击CreateXcodeScene Editor将加载您的第一个空白scene file

如果您没有看到左侧列出的相机camera节点,请通过选择场景左下角的Scene Graph View打开屏幕图(Screen graph)

SceneKit 使用scene graphs将场景组织为节点(nodes)树。 场景图(scene graphs)以根节点开始,您可以为场景添加任何内容作为该根节点下的子节点。

一个节点只不过是一个位置(location); 它没有行为或外观。 尽管如此,节点(nodes)仍是场景(scene)的核心。 要将任何内容添加到场景中,您需要将其附加到节点。

你的场景图已经带有一个子节点,它有一个附件:

相机节点(camera node)附有一个相机对象(camera object)。 您可以根据需要在场景中使用任意数量的摄像机,但您至少需要一台摄像机才能看到任何内容。

1. Loading a Scene

要在应用程序中查看您的场景(scene),您需要创建一个 SceneView。 打开 ContentView.swift

首先,在文件顶部导入 SceneKit

import SceneKit

接下来,在 ContentView中添加以下内容:

// 1
static func makeScene() -> SCNScene? {
  let scene = SCNScene(named: "Solar Scene.scn")
  return scene
}

// 2
var scene = makeScene()

在上面的代码中,您:

  • 1) 从场景文件(scene file)创建场景。
  • 2) 调用 makeScene() 加载场景。

现在,您需要获取对相机节点(camera node)的引用。 在body下方,就在 ContentView 的右大括号之前,添加以下内容:

func setUpCamera(planet: Planet?) -> SCNNode? {
  let cameraNode = scene?.rootNode
    .childNode(withName: "camera", recursively: false)
  return cameraNode
}

稍后您将对此进行扩展。 现在, setUpCamera(planet:)只是获取对相机节点的引用。

最后,您已准备好创建场景视图。 在body开始附近,移除 ColorPalette.secondary 及其视图修改器(view modifier)。 然后,添加以下内容:

SceneView(
  // 1
  scene: scene,
  // 2
  pointOfView: setUpCamera(planet: viewModel.selectedPlanet),
  // 3
  options: [.allowsCameraControl]
)
// 4
.background(ColorPalette.secondary)
.edgesIgnoringSafeArea(.all)

您已经用 SceneView 替换了应用程序的空白背景。 这是正在发生的事情,一步一步:

  • 1) 选择要显示的视图的场景。
  • 2) 场景视图的 pointOfView 是将显示场景的相机。 某些游戏通过使用此属性来更改当前视角,从而在多个摄像机之间进行交换。
  • 3) 您可以使用一组选项控制 SceneView 的行为。 在这里, .allowsCameraControl允许用户操纵相机。
  • 4) 设置场景视图的背景颜色,您将在场景加载时看到它,并将场景拉伸到整个窗口。

终于,你准备好看到你的第一个场景了! 构建并运行。

这里没什么,但考虑到您迄今为止编写的代码很少,这令人印象深刻。 Solar Scene.scn 提供了默认背景和相机,您的场景视图的 allowedCameraControl 选项可让您在场景周围自由滚动和缩放。 最重要的是,您无需编写任何代码即可完成此操作。

现在,你有一个相机,但没有什么可看的。 接下来,您将添加一个太阳来测试您的场景。

2. Adding Objects

打开 Solar Scene.scn。 在场景编辑器(Scene Editor)的右上角,单击加号(+)按钮以访问对象库(Object Library)

找到 Sphere 对象并将其拖到场景图中,就在相机节点(camera node)的下方。

注意:将对象拖到场景图中可能会很棘手。 如果您的新球体不会粘住,请在拖动之前尝试选择相机(camera)

选择sphere后,单击它并将其名称更改为 sun。 如果您在场景编辑器中看不到它,请放大或缩小以获得良好的太阳视图:

它看起来并不像。 现在,您的太阳是一个带有几何对象的简单节点。

请记住,默认情况下节点没有外观或行为。 SceneKit 包括各种几何形状,您可以使用它们为节点提供外观,但您不受限制。 使用 Metal 或外部 3D 建模工具,您可以创建自己的自定义几何图形。

当你制作了一个球体时,它看起来并不像一个太阳。 因此,为了让您的太阳外观更合身,您需要修改其材质(material)

3. Modifying Materials

Scene Editor右侧的“检查器”面板(Inspectors panel)中,选择“材料”检查器(Materials inspector)

对象的材质是一组属性,当与材质的光照模型结合时,决定如何渲染几何体的每个像素。 在这里,您可以更改决定太阳球体几何颜色的属性。

接下来,单击漫反射(Diffuse)颜色以调出颜色选择器。 导航到滑块(Sliders)选项卡,然后将下拉列表设置为 RGB Sliders。 最后,将Hex Color # 设置为 #F2FF2C

您可以将材质的漫反射视为对象的“基色”。

接下来,将照明(Illumination)的颜色设置为白色:#FFFFFF。 材质的照明让它定义了光如何照射到物体上。 即使几何体被光源遮挡,设置照明也会使材质自身着色,就好像它在接收光一样。

构建并运行。

如果您没有看到任何东西,请平移相机直到您的球体进入视野。

看哪,我们太阳系的中心:太阳!

然而,目前它有点粗糙。 接下来,您将使它更大,为其他行星提供一些规模标尺。

在检查器面板中,单击属性检查器(Attributes inspector)。 在这里,您可以控制节点及其任何附件的各种属性,例如球体的半径。

将半径更改为 10,这将使球体的大小增加十倍。 构建并运行。

等等,它去哪儿了? 这次平移相机无济于事。 你增加了太阳的大小,它吞没了相机。 您需要将相机移动到安全距离。


Setting Up the Camera Node

在场景图(scene graph)中,单击相机节点(camera node)。 然后,在检查器面板中,单击节点检查器(Node inspector)

在这里,您可以设置节点的位置、形状和方向。 您如何定向节点将影响连接到该节点的任何内容。 因此,更改相机节点的位置将更改与其相连的相机的位置。

场景中的每个节点都具有以下属性,节点检查器(Node inspector)可以编辑这些属性:

  • Identity:节点的名称,用于在代码中访问节点。
  • Position:节点相对于其父节点放置在场景中的位置。
  • Euler:节点相对于其父节点的旋转。
  • Scale:允许您变换节点,沿每个轴变换其大小。

要正确重新定位相机,请将Position更改为 x:-55、y:65 和 z:-68

然后,将 Euler 更改为 x:145、y:-13 和 z:-158。 这会将相机定向为指向您的太阳。 它还创造了一个空的空间,你的行星将填满。

现在,您的转换将如下所示:

缩小场景编辑器(Scene Editor),直到您可以清楚地看到相机和太阳。

您的相机正指向您的场景,但其观看深度(viewing depth)不足以看到您的太阳。 在检查器面板中,切换到属性检查器(Attributes inspector)。 在这里,您将找到 Z Clipping 属性,它可让您增加观看深度。

Z Clipping 下,将 Far 更改为 300

构建并运行

现在,您可以看到太阳,还有足够的空间容纳更多的行星!


Creating Planets

接下来,您将向太阳系添加五颗行星:

  • 1) Mercury
  • 2) Venus
  • 3) Earth
  • 4) Mars
  • 5) Saturn

每个都是一个球体,就像太阳一样。 您将更改每个行星的颜色、大小和位置等细节。

注意:以下大部分步骤重复您创建太阳时所做的操作。 如果您需要复习,请回顾Adding Objects部分。

1. Mercury

首先,从对象库(Objects Library)中添加一个新的球体几何体。

然后,在节点检查器中,将Name更改为mercury。 将位置更改为 x: 0、y: 0 和 z: 25

在材质检查器中,使用颜色选择器将漫反射(Diffuse)更改为 #BBBBBB。 然后,将粗糙度(Roughness)更改为 1。通过更改材质的粗糙度,您可以使其或多或少有光泽。 将粗糙度设置为接近0会使其发亮,将粗糙度设置为接近 1 会使其反射性降低。

构建并运行。

SceneKit 渲染你的第一颗行星,水星,靠近太阳。 接下来,您将按照对 Mercury 执行的步骤按顺序添加其余行星。

2. Venus

要创建 Venus,请执行以下操作:

  • 1) 向场景图(scene graph)中添加一个球体。
  • 2) 在节点检查器(Node inspector)中,将Name更改为 venus。 将 Positionz 值更改为 35
  • 3) 在属性检查器(Attributes inspector)中,将半径更改为 2
  • 4) 在材质检查器(Material inspector)中,将漫反射更改为 #59B1D6,将粗糙度(Roughness)更改为 1

3. Earth

对于我们称之为家的星球,请按照以下步骤操作:

  • 1) 向场景图中添加一个球体。
  • 2) 在节点检查器中,将Name更改为earth。 将 Position 的 z 值更改为 50
  • 3) 在属性检查器中,将半径更改为 2
  • 4) 在材质检查器中,将Diffuse更改为 #2F5CD6,将粗糙度更改为 1

4. Mars

接下来,对红色星球进行以下更改:

  • 1) 向场景图中添加一个球体。
  • 2) 在节点检查器中,将名称更改为 mars。 将 Positionz 更改为 75
  • 3) 在 Material检查器中,将 Diffuse 更改为 #C65B2C,将 Roughness 更改为 1

构建并运行。

你的太阳系正在形成,但它仍然有点乏味。 对于一些变化,您将添加每个人最喜欢的气体巨星:土星!


Adding Saturn’s Ring

土星遵循与之前行星类似的模式,但你会更进一步:土星需要一个环。

首先,创建行星的主体,就像您对以前的行星所做的一样:

  • 1) 向场景图中添加一个球体。
  • 2) 在节点检查器中,将名称更改为 saturn。 将 Positionz 更改为 150
  • 3) 在属性检查器中,将Radius更改为 5
  • 4) 在 Material 检查器中,将 Name 更改为 saturn,将 Diffuse 更改为 #D69D5F,将 Roughness 更改为 1。

注意:这次不要忘记节点检查器和材质检查器中的设置名称Name。 稍后您将使用它。

平移场景编辑器(Scene Editor)以仔细查看您最新的星球。

土星缺少它的标志环。 要添加环,您将使用 SceneKit 的另一个原始形状:环(tube)。

1. Adding the Ring

从对象库(Object Library)中,找到 Tube 并将其添加到场景图中。 然后,拖动tube使其嵌套在土星(saturn)下,如下所示:

虽然所有行星都是场景根节点的子节点,但您已将tube添加为土星saturn的子节点。 现在,您可以相对于其父级定位tube并为其设置动画。

将此视为将汽车座椅作为儿童添加到汽车节点:如果汽车移动,则子节点会继续行驶。

在节点检查器中,将tube的位置更改为 x: 0、y: 0 和 z: 0。这将使其以土星的位置为中心。

因为它比土星的球体小,所以你看不到管子(tube)。 要适当调整管的大小,请打开“属性”检查器。 然后,进行以下更改:

  • Inner radius: 7
  • Outer radius: 9
  • Height: 0.1

现在,您的环位置正确,但与星球的其他部分不匹配。 您可以修改材料以匹配,或者您可以重复使用您已经为 Saturn 设计的材料。

2. Reusing Material

当您制作大型场景时,重用对象的能力至关重要。 在 SceneKit 中,您可以重用节点、几何图形和材料。 这样,如果必须在整个场景中更改某些内容,您只需更改一次。

现在,如果你想改变土星的颜色或其他任何与其物质相关的东西,你必须复制土星和它的环的变化。 通过共享一个物质对象,行星及其环将共享相同的外观。

在场景图(scene graph)中选择管子(tube)后,打开材质检查器。 在顶部,您会找到一系列材料。

对于土星环,您只需要一种与行星体相匹配的材料。 因此,单击minus (—)按钮以移除tube材料。

然后,单击plus (+)按钮以调出材料选择器。

找到并单击土星材料,然后单击Done

现在,土星环与行星的身体共享相同的材料。

构建并运行以查看所有行星。

你的行星看起来很棒,但缺少一些东西。 你的场景有光,但看起来有点偏——没有来自太阳的光。 接下来,您将通过向太阳节点添加灯光来解决该问题。


Attaching a Light

当您从场景文件(scene file)创建场景时,就像您使用 Solar Scene.scn 所做的那样,SceneKit 通过为您提供默认相机、灯光和背景来为您提供一个运行开始。 您需要做的就是添加几何图形。

在场景图中,右键单击太阳(sun)节点。

在此菜单中,您可以在一系列附件之间进行选择:灯光、相机、物理实体等等。 现在,单击Add Light

太阳节点旁边的小太阳图标(多么合适!)表示该节点有一个光附件。 同样,立方体图标表示几何附件。

单击太阳(sun)节点。 然后,在“检查器”面板中,单击“属性”检查器。

之前,您只有适用于附加几何体的属性,但现在您还有一个用于附加灯光的新部分。

首先,将灯光的Type更改为 Omni,即omnidirectional的缩写。 这是一种向四面八方照射的光型。 然后,将 Intensity 更改为 20000。毕竟是太阳!

构建并运行。

在那次变化之后,你的整个太阳系都沐浴在太阳温暖的光芒中。

您已经在 Solar Scene.scn 中达到了您需要的程度。 要真正发挥 SceneKit 的威力,是时候开始在 Swift 代码中添加场景了。


Adding Textures

到目前为止,您已经用单一的简单颜色为每个行星着色。 尽管被称为“蓝色星球”,但地球看起来还是有点平淡。 为了让事情更上一层楼,您将对每个行星应用纹理(texture)而不是颜色。

纹理是环绕几何体的图像,例如行星球体。 还记得材质检查器中的各种颜色属性吗? 您也可以将纹理图像应用于每个属性。

在 Xcode 中,打开 Assets.xcassets 并查看planets组。

每个行星都有其表面的人造地图。 创建场景时,您将使用这些图像将纹理应用于每个行星。

1. Replacing Colors with Textures

首先,打开 ContentView.swift。 然后,在 makeScene() 下添加:

static func applyTextures(to scene: SCNScene?) {
  // 1
  for planet in Planet.allCases {
    // 2
    let identifier = planet.rawValue
    // 3
    let node = scene?.rootNode
      .childNode(withName: identifier, recursively: false)

    // Images courtesy of Solar System Scope https://bit.ly/3fAWUzi
    // 4
    let texture = UIImage(named: identifier)

    // 5
    node?.geometry?.firstMaterial?.diffuse.contents = texture
  }
}

这是正在发生的事情,一步一步:

  • 1) Planet枚举列出了应用程序中的所有行星。
  • 2) 每个行星的 rawValue 与您一直应用于行星的标识符相同:水星、金星等。
  • 3) 使用标识符,获取对行星节点的引用。
  • 4) Assets.xcassets 中的纹理名称也与行星标识符匹配。 这会为适当的行星创建一个 UIImage
  • 5) 最后,将图像设置为节点材质上的行星漫反射(diffuse),从而替换作为其基本外观的颜色。

要应用您的纹理,请在返回场景之前将其添加到 makeScene()中:

applyTextures(to: scene)

这会在创建场景时调用 applyTextures(to:)

构建并运行。 然后,仔细看看每个行星:

那看起来好多了。 纹理是为场景添加真实感的好方法。 接下来,要真正获得那种“空间”的感觉,您将再添加一种纹理。

2. Using Skybox Textures

你的行星状态很好,但看看你场景的其余部分,它看起来不太像太空。 在 Xcode 中,打开 Assets.xcassets并查看天空盒(skybox)组。

不是一张,不是两张,而是六张星空的图像。 非常适合场景背景! 但是,为什么是六个? 好吧,SceneKit 可以将这六个图像用于一种称为天空盒的常见 3D 编程技术。 通过使用这些图像作为场景的背景,SceneKit 从六个图像中制作一个立方体,并将整个场景放入立方体中。

applyTextures(to:)的底部,添加:

// 1
let skyboxImages = (1...6).map { UIImage(named: "skybox\($0)") }
// 2
scene?.background.contents = skyboxImages

这是做的事情:

  • 1) 创建一个包含六个天空盒图像的数组。
  • 2) 使用这些图像作为场景的背景内容。

SceneKit 将从这里开始处理其余的工作,因为框架知道将六个图像的数组应用为天空盒。

最后,构建并运行看看这两行代码能做什么。

无论您如何滚动或平移图像,您都会被深邃的星空所包围。通过使用天空盒纹理,您已将场景拍摄到外太空!

你的场景终于成型了。剩下的就是对应用程序如何与 SceneKit 交互的一些调整。例如,显示行星信息的原始应用程序与 SceneKit 场景没有真正的链接。是时候改变它了。


Working With the Camera

当您在 Solar Scenes 的行星切换器中选择不同的行星时,如果场景集中在选定的行星上,那就太好了。目前,SceneKit 控制相机,但您也可以通过编程方式移动它。要将相机聚焦在选定的行星上,您将使用约束(constraint)

1. Using Constraints

与自动布局约束类似,SceneKitSNCConstraint 及其子类允许您指定规则以应用于任何节点的位置和方向。

例如, SCNDistanceConstraint 可以强制一个节点与另一个节点保持指定的距离。使用 SCNBillboardConstraint,您可以强制一个节点面向相机,就像一幅令人毛骨悚然的画,无论您站在哪里,它似乎都跟着您。

对于选定的行星,您将使用 SCNLookAtConstraint 强制相机查看选定的行星节点。为了平滑相机的方向变化,您将使用动作(action)为其设置动画。

2. Animating With Actions

动作是一种执行简单动画的方法。通过运行一个动作,一个节点可以平滑地改变它的位置和方向,缩放到不同的大小等等。

接下来,您将在“太阳场景”(Solar Scenes)中切换行星时使用动作为相机的位置变化设置动画,并且您将使用约束来聚焦于行星。

3. Focusing on the Selected Planet

首先,将它添加到 ContentView.swift的右大括号之前:

func planetNode(planet: Planet) -> SCNNode? {
  scene?.rootNode.childNode(withName: planet.rawValue, recursively: false)
}

这会抓取对应于Planet 模型对象的 Planet 节点。

接下来,找到 setUpCamera(planet:)。 在最后的 return 语句之前添加:

// 1
if let planetNode = planet.flatMap(planetNode(planet:)) {
  // 2
  let constraint = SCNLookAtConstraint(target: planetNode)
  cameraNode?.constraints = [constraint]
  // 3
  let globalPosition = planetNode
    .convertPosition(SCNVector3(x: 50, y: 10, z: 0), to: nil)
  // 4
  let move = SCNAction.move(to: globalPosition, duration: 1.0)
  cameraNode?.runAction(move)
}

这是正在发生的事情:

  • 1) ContentView 在设置视图主体时调用 setUpCamera(planet:),从视图模型中传递当前选定的行星。 在这里,您将使用planetNode(planet:)获得对该行星节点的引用。
  • 2) 创建一个 SCNLookAtConstraint,它侧重于planetNode,然后将其应用于cameraNode 的约束。
  • 3) 节点定义自己的坐标空间。 因此,使用 convertPosition(_:to:)将靠近行星的位置转换为全局坐标空间中的相同位置。
  • 4) 使用全局位置,创建并运行一个动作来更新 cameraNode 的位置。

最后,当相机聚焦在一个星球上时,你应该禁用 SceneKit 的自动相机控制。 转到bodySceneView的初始化。 查找options并将其替换为:

options: viewModel.selectedPlanet == nil ? [.allowsCameraControl] : []

在这里,如果您选择了一颗行星,您将取消对相机的自动控制。

构建并运行。 翻转行星切换器中的行星,并在相机聚焦所选行星时享受流畅的动画。

SceneKit 是一个功能强大且易于访问的 3D 编程入口点。 如果您想了解有关 SceneKit的更多信息,Apple 的documentation and WWDC videos将是您的指南。

在本教程中,您使用了一个简单的应用程序并添加了一个完全交互式的 3D 场景。 您为太阳系中的行星制作了视觉表示,而不仅仅是使用普通的旧 2D 界面。

接下来,为什么不将缺失的行星添加到太阳场景中? 或者,您可以通过支持增强现实(augmented reality)将场景带入现实世界。 查看我们的书,Apple Augmented Reality by Tutorials,了解如何操作。

后记

本篇主要讲述了SceneKit 3D编程入门,感兴趣的给个赞或者关注~~~

推荐阅读更多精彩内容