我可以使 SCNBox 可点击和交互吗?
Can I make SCNBox tappable and interactive?
我正在尝试在 SwiftUI 中创建一个带有 SCNBox 的旋转立方体,您可以在其中点击框的每一侧,然后会出现一个不同的弹出屏幕/带有文本的视图等。我有一个旋转的 SCNBox 立方体,但是我该怎么做使其可点击和交互,如何使其重定向到另一个视图?
这就是我目前尝试将 Box Scene 插入 SwiftUI 视图的方式,然后该视图将进入现有应用程序的视图层次结构。
import SwiftUI
import SceneKit
class BoxNode: SCNNode {
override init() {
super.init()
self.geometry = SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0.0)
self.geometry?.firstMaterial?.shininess = 50
let action = SCNAction.rotate(by: 360 * CGFloat(Double.pi / 180), around: SCNVector3(x:0, y:1, z:0), duration: 8)
let repeatAction = SCNAction.repeatForever(action)
self.runAction(repeatAction)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
struct BoxScene: View {
var scene = SCNScene(named: "MyScene")
var cameraNode: SCNNode? {
scene?.rootNode.childNode(withName: "camera", recursively: false)
}
var boxNode: SCNNode? {
scene?.rootNode.addChildNode(BoxNode())
}
var body: some View {
SceneView(
scene: scene,
pointOfView: cameraNode,
options: []
)
}
}
但是,此代码无法编译 - 它表示“表达式类型不明确,下一行中没有更多上下文:
var boxNode: SCNNode? {
scene?.rootNode.addChildNode(BoxNode())
}
这是一个使用 SCNBox
.
可能会有所帮助的示例
BoxNode
是一个自定义 SCNNode
,它使用 SCNBox
作为其几何图形。每一面都设置为不同的颜色,以便于查看,如果您想要展示更精致的东西,可以使用 UIImage
s。由于我们使用 SceneKit
、UIKit
和 SwiftUI
,请确保在需要的地方导入它们。
class BoxNode: SCNNode {
override init() {
let length: CGFloat = 5
let box = SCNBox(width: length, height: length, length: length, chamferRadius: 0)
box.materials = [UIColor.red, .yellow, .green, .blue, .purple, .brown].map {
let material = SCNMaterial()
material.diffuse.contents = [=10=]
material.isDoubleSided = true
material.transparencyMode = .aOne
return material
}
super.init()
self.geometry = box
self.name = "boxNode"
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
然后在自定义 SCNScene
中使用它,它将 BoxNode
添加为根节点并设置旋转动画并旋转立方体。
class GameScene: SCNScene {
override init() {
super.init()
let cubeNode = BoxNode()
self.rootNode.addChildNode(cubeNode)
let xAngle = SCNMatrix4MakeRotation(.pi / 3.8, 1, 0, 0)
let zAngle = SCNMatrix4MakeRotation(-.pi / 4, 0, 0, 1)
cubeNode.pivot = SCNMatrix4Mult(SCNMatrix4Mult(xAngle, zAngle), cubeNode.transform)
// Rotate the cube
let animation = CAKeyframeAnimation(keyPath: "rotation")
animation.values = [SCNVector4(1, 1, 0.3, 0 * .pi),
SCNVector4(1, 1, 0.3, 1 * .pi),
SCNVector4(1, 1, 0.3, 2 * .pi)]
animation.duration = 5
animation.repeatCount = HUGE
cubeNode.addAnimation(animation, forKey: "rotation")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
然后 GameScene
设置在 UIViewController
内的 SCNView
内。 SCNView
固定在 ViewController 的边缘,我们覆盖 touchesBegan
以便我们可以与 SceneView
交互。我还创建了一个变量 sideTapped
,因为这将允许您注入一个回调,这样您就可以知道在您的立方体上点击了哪一侧。
class GameSceneViewController: UIViewController {
private let sceneView: SCNView = .init(frame: .zero)
private let scene = GameScene()
var sideTapped: ((Int) -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
sceneView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(sceneView)
NSLayoutConstraint.activate([
sceneView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
sceneView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
sceneView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
sceneView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
])
sceneView.scene = scene
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touchPoint = touches.first?.location(in: sceneView) else { return }
guard let hitTestResult = sceneView.hitTest(touchPoint, options: nil).first else { return }
guard hitTestResult.node is BoxNode else { return }
sideTapped?(hitTestResult.geometryIndex)
}
}
因为我们在 SwiftUI
中使用它,所以我们需要一个 UIViewControllerRepresentable
。这里我们将 sideTapped
函数传递给 GameSceneViewController
.
struct GSViewControllerRepresentable: UIViewControllerRepresentable {
let sideTapped: ((Int) -> Void)?
func makeUIViewController(context: Context) -> GameSceneViewController {
let vc = GameSceneViewController()
vc.sideTapped = sideTapped
return vc
}
func updateUIViewController(_ uiViewController: GameSceneViewController, context: Context) {
}
}
现在我们可以将其添加到 SwiftUI
视图中。
struct ContentView: View {
var body: some View {
GSViewControllerRepresentable { side in
print(side)
// Perform additional logic here
}
.frame(width: 200, height: 200)
}
}
最终结果应如下所示:
点击立方体的一侧将打印一个从 0 到 5 的数字,表示您点击的是哪一侧。根据使用的materials,数字应该对应于material的索引,所以在这种情况下:
- 0 是红色
- 1 是黄色
- 2 是绿色
- 3 是蓝色
- 4 是紫色
- 5 是棕色的
这应该会给你一些你应该能够关闭导航的东西。
我不是 SceneKit 方面的专家,但我周末在玩它,并且几乎完成了你所问的事情。希望这能给你一个想法,有很多方法可以实现你想做的事情,这是一种可能性。
如果你想滑动立方体来改变面,而不是让它自己旋转,你可能想看看这个 。
我正在尝试在 SwiftUI 中创建一个带有 SCNBox 的旋转立方体,您可以在其中点击框的每一侧,然后会出现一个不同的弹出屏幕/带有文本的视图等。我有一个旋转的 SCNBox 立方体,但是我该怎么做使其可点击和交互,如何使其重定向到另一个视图?
这就是我目前尝试将 Box Scene 插入 SwiftUI 视图的方式,然后该视图将进入现有应用程序的视图层次结构。
import SwiftUI
import SceneKit
class BoxNode: SCNNode {
override init() {
super.init()
self.geometry = SCNBox(width: 1, height: 1, length: 1, chamferRadius: 0.0)
self.geometry?.firstMaterial?.shininess = 50
let action = SCNAction.rotate(by: 360 * CGFloat(Double.pi / 180), around: SCNVector3(x:0, y:1, z:0), duration: 8)
let repeatAction = SCNAction.repeatForever(action)
self.runAction(repeatAction)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
struct BoxScene: View {
var scene = SCNScene(named: "MyScene")
var cameraNode: SCNNode? {
scene?.rootNode.childNode(withName: "camera", recursively: false)
}
var boxNode: SCNNode? {
scene?.rootNode.addChildNode(BoxNode())
}
var body: some View {
SceneView(
scene: scene,
pointOfView: cameraNode,
options: []
)
}
}
但是,此代码无法编译 - 它表示“表达式类型不明确,下一行中没有更多上下文:
var boxNode: SCNNode? {
scene?.rootNode.addChildNode(BoxNode())
}
这是一个使用 SCNBox
.
BoxNode
是一个自定义 SCNNode
,它使用 SCNBox
作为其几何图形。每一面都设置为不同的颜色,以便于查看,如果您想要展示更精致的东西,可以使用 UIImage
s。由于我们使用 SceneKit
、UIKit
和 SwiftUI
,请确保在需要的地方导入它们。
class BoxNode: SCNNode {
override init() {
let length: CGFloat = 5
let box = SCNBox(width: length, height: length, length: length, chamferRadius: 0)
box.materials = [UIColor.red, .yellow, .green, .blue, .purple, .brown].map {
let material = SCNMaterial()
material.diffuse.contents = [=10=]
material.isDoubleSided = true
material.transparencyMode = .aOne
return material
}
super.init()
self.geometry = box
self.name = "boxNode"
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
然后在自定义 SCNScene
中使用它,它将 BoxNode
添加为根节点并设置旋转动画并旋转立方体。
class GameScene: SCNScene {
override init() {
super.init()
let cubeNode = BoxNode()
self.rootNode.addChildNode(cubeNode)
let xAngle = SCNMatrix4MakeRotation(.pi / 3.8, 1, 0, 0)
let zAngle = SCNMatrix4MakeRotation(-.pi / 4, 0, 0, 1)
cubeNode.pivot = SCNMatrix4Mult(SCNMatrix4Mult(xAngle, zAngle), cubeNode.transform)
// Rotate the cube
let animation = CAKeyframeAnimation(keyPath: "rotation")
animation.values = [SCNVector4(1, 1, 0.3, 0 * .pi),
SCNVector4(1, 1, 0.3, 1 * .pi),
SCNVector4(1, 1, 0.3, 2 * .pi)]
animation.duration = 5
animation.repeatCount = HUGE
cubeNode.addAnimation(animation, forKey: "rotation")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
然后 GameScene
设置在 UIViewController
内的 SCNView
内。 SCNView
固定在 ViewController 的边缘,我们覆盖 touchesBegan
以便我们可以与 SceneView
交互。我还创建了一个变量 sideTapped
,因为这将允许您注入一个回调,这样您就可以知道在您的立方体上点击了哪一侧。
class GameSceneViewController: UIViewController {
private let sceneView: SCNView = .init(frame: .zero)
private let scene = GameScene()
var sideTapped: ((Int) -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
sceneView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(sceneView)
NSLayoutConstraint.activate([
sceneView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
sceneView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
sceneView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
sceneView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
])
sceneView.scene = scene
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touchPoint = touches.first?.location(in: sceneView) else { return }
guard let hitTestResult = sceneView.hitTest(touchPoint, options: nil).first else { return }
guard hitTestResult.node is BoxNode else { return }
sideTapped?(hitTestResult.geometryIndex)
}
}
因为我们在 SwiftUI
中使用它,所以我们需要一个 UIViewControllerRepresentable
。这里我们将 sideTapped
函数传递给 GameSceneViewController
.
struct GSViewControllerRepresentable: UIViewControllerRepresentable {
let sideTapped: ((Int) -> Void)?
func makeUIViewController(context: Context) -> GameSceneViewController {
let vc = GameSceneViewController()
vc.sideTapped = sideTapped
return vc
}
func updateUIViewController(_ uiViewController: GameSceneViewController, context: Context) {
}
}
现在我们可以将其添加到 SwiftUI
视图中。
struct ContentView: View {
var body: some View {
GSViewControllerRepresentable { side in
print(side)
// Perform additional logic here
}
.frame(width: 200, height: 200)
}
}
最终结果应如下所示:
点击立方体的一侧将打印一个从 0 到 5 的数字,表示您点击的是哪一侧。根据使用的materials,数字应该对应于material的索引,所以在这种情况下:
- 0 是红色
- 1 是黄色
- 2 是绿色
- 3 是蓝色
- 4 是紫色
- 5 是棕色的
这应该会给你一些你应该能够关闭导航的东西。
我不是 SceneKit 方面的专家,但我周末在玩它,并且几乎完成了你所问的事情。希望这能给你一个想法,有很多方法可以实现你想做的事情,这是一种可能性。
如果你想滑动立方体来改变面,而不是让它自己旋转,你可能想看看这个