SceneKit基础之实现一个太阳系场景(二)
在上一篇文章中,我们已经通过Scene Editor实现了场景的布局。接下来我们将通过代码的方式来给节点添加图片材质、场景背景以及切换相机的视角。
定义模型和实现切换
定义一个名为Planet的枚举用来管理节点数据:1
2
3
4
5
6
7
8
9
10
11
12enum Planet: String, CaseIterable {
case mercury
case venus
case earth
case mars
case saturn
var name:String {
// 名字为首字母大写
rawValue.prefix(1).capitalized + rawValue.dropFirst()
}
}
定义一个名ViewModel的类用来管理场景中选中的节点以及节点的切换事件:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41class ViewModel: NSObject,ObservableObject {
var selectedPlanet: Planet? // 当前选中的节点
// 当前选中的节点名称
var title:String {
selectedPlanet?.name ?? ""
}
// 选择下一个
func selectedNextPlanet() {
changeSelection(offset: 1)
}
// 选择上一个
func selectedPreviousPlanet() {
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
}
}
}
回到ContentView文件中,实例化一个ViewModel的对象:1
var viewModel = ViewModel()
然后在 body部分实现界面的完全布局:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46var body: some View {
ZStack {
SceneView(scene: scene,
pointOfView:setupCamera(planet: viewModel.selectedPlanet),
options: [.allowsCameraControl, .autoenablesDefaultLighting])
.background(.secondary)
VStack {
Spacer()
HStack {
HStack {
HStack {
Button(action: {
viewModel.selectedPreviousPlanet()
}, label: {
Image(systemName: "arrow.backward.circle.fill")
})
Button(action: {
viewModel.selectedPreviousPlanet()
}, label: {
Image(systemName: "arrow.forward.circle.fill")
})
}
Spacer()
Text(viewModel.title)
.foregroundStyle(.black)
Spacer()
// 不为空时显示清除按钮
if viewModel.selectedPlanet != nil {
Button(action: {
viewModel.clearSelection()
}, label: {
Image(systemName: "xmark.circle.fill")
})
}
}
}
.padding(8)
.background(.white)
.clipShape(RoundedRectangle(cornerRadius: 14))
.padding(20)
}
}
.ignoresSafeArea(.all)
}
在上面的代码中,SceneView多了一个pointOfView参数,它是一个SCNNode类型,它表示的是当前场景渲染时的视角点。我们可以通过切换视角点来切换当前场景中可见的内容。setupCamera函数的实现如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14// 根据选中的节点,改变相机的视点
func setupCamera(planet: Planet?) -> SCNNode? {
// 获取场景中的相机节点
let cameraNode = scene?.rootNode.childNode(withName: "camera", recursively: false)
if let planet = planet, let planetNode = scene?.rootNode.childNode(withName: planet.rawValue, recursively: false) {
let constraint = SCNLookAtConstraint(target: planetNode)
cameraNode?.constraints = [constraint]
let globalPosition = planetNode.convertPosition(SCNVector3(50, 10, 0), to: nil)
let move = SCNAction.move(to: globalPosition, duration: 1.0)
cameraNode?.runAction(move)
}
return cameraNode
}
它的实现如下:
- 根据节点名
camera获取相机节点cameraNode; - 根据传进来的
planet的rawValue获取当前选中的节点planetNode; - 然后设置
cameraNode的constraints; - 给
cameraNode添加一个移动的action; - 然后新的相机节点
cameraNode作为场景的pointOfView。
给星球节点添加图片材质
将准备好的星球图片素材拖入到Assets素材文件夹下:

同理,拖入准备好的场景背景素材添加到这个文件夹下:

注意,这里的场景背景图片需要放入六张图片。因为给 3D 场景背景添加背景图片需要同时设置 6 个方向,即 XYZ 三个轴的正负方向。
接着定义一个名为applyTextures的方法来添加材质和背景:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 给场景添加材质
static func applyTextures(to scene: SCNScene?) {
guard let scene = scene else { return }
// 给每个星球添加图片材质
for planet in Planet.allCases {
let identifier = planet.rawValue
let node = scene.rootNode.childNode(withName: identifier, recursively: false)
let texture = UIImage(named: identifier)
node?.geometry?.firstMaterial?.diffuse.contents = texture
}
// 给整个场景添加背景材质
let skyboxImages = (1...6).map { UIImage(named: "skybox\($0)") }
scene.background.contents = skyboxImages
}
给节点添加材质通过设置节点的geometry?.firstMaterial或者geometry?.materials。前者是只添加一个材质;后者添加多个材质。
给场景添加添加背景通过设置场景的background.contents。
最后,别忘了在makeScene方法中调用这个方法。