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

版本记录

版本号 时间
V1.0 2021.07.21 星期三

前言

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

源码

1. Swift

首先看下工程组织结构

下面就是源码了。

1. Planet.swift
import Foundation

enum Planet: String, CaseIterable {
  case mercury
  case venus
  case earth
  case mars
  case saturn

  var name: String {
    rawValue.prefix(1).capitalized + rawValue.dropFirst()
  }
}
2. Planet+Info.swift
import Foundation

extension Planet {
  var moonCount: Int {
    switch self {
    case .mercury:
      return 0
    case .venus:
      return 0
    case .earth:
      return 1
    case .mars:
      return 2
    case .saturn:
      return 62
    }
  }

  var yearLength: String {
    switch self {
    case .mercury:
      return "88 Earth days"
    case .venus:
      return "225 Earth days"
    case .earth:
      return "365.25 days"
    case .mars:
      return "1.88 Earth years"
    case .saturn:
      return "29.45 earth years"
    }
  }

  var namesake: String {
    switch self {
    case .mercury:
      return "Roman god of speed"
    case .venus:
      return "Roman goddess of love"
    case .earth:
      return "The ground"
    case .mars:
      return "Roman god of war"
    case .saturn:
      return "Roman god of agriculture"
    }
  }
}
3. PlanetInfoRow.swift
import SwiftUI

struct PlanetInfoRow: View {
  let title: String
  let value: String

  var body: some View {
    HStack {
      VStack(alignment: .leading) {
        Text("\(title):").fontWeight(.thin).font(.title2)
        Text(value).fontWeight(.semibold)
      }
      .foregroundColor(.white)
      Spacer()
    }
  }
}

struct PlanetInfoRow_Previews: PreviewProvider {
  static var previews: some View {
    PlanetInfoRow(title: "Number of moons", value: "3")
  }
}
4. ColorPalette.swift
import SwiftUI

enum ColorPalette {
  static let primary = Color(
    red: 29 / 255,
    green: 17 / 255,
    blue: 53 / 255,
    opacity: 1)

  static let secondary = Color(
    red: 186 / 255,
    green: 30 / 255,
    blue: 104 / 255,
    opacity: 1)

  static let accent = Color(
    red: 252 / 255,
    green: 251 / 255,
    blue: 254 / 255,
    opacity: 1)
}
5. SolarButtonStyle.swift
import SwiftUI

struct SolarButtonStyle: ButtonStyle {
  func makeBody(configuration: Configuration) -> some View {
    configuration
      .label
      .foregroundColor(configuration.isPressed ? .gray : .white)
      .padding(8)
      .background(ColorPalette.secondary)
      .cornerRadius(8)
  }
}
6. ProgressViewWithBackgroundStyle.swift
import SwiftUI

struct ProgressViewWithBackgroundStyle: ProgressViewStyle {
  func makeBody(configuration: Configuration) -> some View {
    ColorPalette.accent
      .opacity(0.8)
      .overlay(ProgressView(configuration))
      .cornerRadius(5)
  }
}
7. AppMain.swift
import SwiftUI

@main
struct AppMain: App {
  var body: some Scene {
    WindowGroup {
      ContentView()
        .buttonStyle(SolarButtonStyle())
    }
  }
}
8. ContentView.swift
import SwiftUI
import SceneKit

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

  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
    let skyboxImages = (1...6).map { UIImage(named: "skybox\($0)") }
    // 2
    scene?.background.contents = skyboxImages
  }

  // 2
  var scene = makeScene()

  @ObservedObject var viewModel = ViewModel()

  var body: some View {
    ZStack {
      SceneView(
        // 1
        scene: scene,
        // 2
        pointOfView: setUpCamera(planet: viewModel.selectedPlanet),
        // 3
        options: viewModel.selectedPlanet == nil ? [.allowsCameraControl] : [])
        // 4
        .background(ColorPalette.secondary)
        .edgesIgnoringSafeArea(.all)

      VStack {
        if let planet = viewModel.selectedPlanet {
          VStack {
            PlanetInfoRow(title: "Length of year", value: planet.yearLength)
            PlanetInfoRow(title: "Number of moons", value: "\(planet.moonCount)")
            PlanetInfoRow(title: "Namesake", value: planet.namesake)
          }
          .padding(8)
          .background(ColorPalette.primary)
          .cornerRadius(14)
          .padding(12)
        }

        Spacer()

        HStack {
          HStack {
            Button(action: viewModel.selectPreviousPlanet) {
              Image(systemName: "arrow.backward.circle.fill")
            }
            Button(action: viewModel.selectNextPlanet) {
              Image(systemName: "arrow.forward.circle.fill")
            }
          }

          Spacer()
          Text(viewModel.title).foregroundColor(.white)
          Spacer()

          if viewModel.selectedPlanet != nil {
            Button(action: viewModel.clearSelection) {
              Image(systemName: "xmark.circle.fill")
            }
          }
        }
        .padding(8)
        .background(ColorPalette.primary)
        .cornerRadius(14)
        .padding(12)
      }
    }
  }

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

    // 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)
    }

    return cameraNode
  }

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

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}
9. ViewModel.swift
import Foundation

class ViewModel: NSObject, ObservableObject {
  @Published var selectedPlanet: Planet?

  var title: String {
    selectedPlanet?.name ?? ""
  }

  func selectNextPlanet() {
    changeSelection(offset: 1)
  }

  func selectPreviousPlanet() {
    changeSelection(offset: -1)
  }

  func clearSelection() {
    selectedPlanet = nil
  }

  private func changeSelection(offset: Int) {
    guard
      let selectedPlanet = selectedPlanet,
      let index = Planet.allCases.firstIndex(of: selectedPlanet)
    else {
      selectedPlanet = Planet.allCases.first
      return
    }

    let newIndex = index + offset

    if newIndex < 0 {
      self.selectedPlanet = Planet.allCases.last
    } else if newIndex < Planet.allCases.count {
      self.selectedPlanet = Planet.allCases[newIndex]
    } else {
      self.selectedPlanet = Planet.allCases.first
    }
  }
}

后记

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容