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编程入门,感兴趣的给个赞或者关注~~~

推荐阅读更多精彩内容