SceneKit魔方(Swift)多层旋转组合后色块变黑问题求助
SceneKit魔方组合旋转后色块变黑问题
单独旋转任意层(顶、底、右、左、前、后)或左层+顶层组合旋转时,色块颜色正常(前绿、右红、后蓝、左橙、顶白、底黄);但执行顶层+右层、前层+右层、底层+左层等组合旋转后,部分或全部色块会变黑。
相关代码
CuboScene.swift
import SwiftUI import SceneKit class CuboScene: SCNScene, ObservableObject { override init() { super.init() rootNode.eulerAngles = SCNVector3(0, 0, 0) // 确保根节点未旋转 rootNode.position = SCNVector3(0, 0, 0) // 确保根节点在原点 rootNode.scale = SCNVector3(1, 1, 1) // 确保无意外缩放 configureLights() createRubikCube() } private func createRubikCube() { let size: CGFloat = 0.8 let spacing: CGFloat = 0.78 for x in -1...1 { for y in -1...1 { for z in -1...1 { let cubelet = SCNBox(width: size, height: size, length: size, chamferRadius: 0.10) let cubeletNode = SCNNode(geometry: cubelet) cubeletNode.position = SCNVector3(Float(x) * Float(spacing), Float(y) * Float(spacing), Float(z) * Float(spacing)) cubeletNode.eulerAngles = SCNVector3(0, 0, 0) cubelet.materials = getFaceMaterial(x: x, y: y, z: z) rootNode.addChildNode(cubeletNode) } } } } private func getFaceMaterial(x: Int, y: Int, z: Int) -> [SCNMaterial] { let colors: [UIColor] = [ (z == 1) ? .green : .black, // 前面 (+Z) (x == 1) ? .red : .black, // 右面 (+X) (z == -1) ? .blue : .black, // 后面 (-Z) (x == -1) ? .orange : .black, // 左面 (-X) (y == 1) ? .white : .black, // 顶面 (+Y) (y == -1) ? .yellow : .black // 底面 (-Y) ] return colors.map { createMaterial(color: $0) } } private func createMaterial(color: UIColor) -> SCNMaterial { let material = SCNMaterial() material.diffuse.contents = color.withAlphaComponent(1.0) material.specular.contents = UIColor.white material.shininess = 100 material.lightingModel = .phong return material } private func configureLights() { // 环境光提供基础照明 let ambientLight = SCNLight() ambientLight.type = .ambient ambientLight.intensity = 500 let ambientLightNode = SCNNode() ambientLightNode.light = ambientLight rootNode.addChildNode(ambientLightNode) // 6个方向的定向光位置和方向 let positions: [(SCNVector3, SCNVector3)] = [ (SCNVector3(0, 10, 0), SCNVector3(0, -1, 0)), // 顶部 (SCNVector3(0, -10, 0), SCNVector3(0, 1, 0)), // 底部 (SCNVector3(10, 0, 0), SCNVector3(-1, 0, 0)), // 右侧 (SCNVector3(-10, 0, 0), SCNVector3(1, 0, 0)), // 左侧 (SCNVector3(0, 0, 10), SCNVector3(0, 0, -1)), // 前面 (SCNVector3(0, 0, -10), SCNVector3(0, 0, 1)) // 后面 ] for (position, direction) in positions { let light = SCNLight() light.type = .directional light.intensity = 1000 light.color = UIColor.white let lightNode = SCNNode() lightNode.light = light lightNode.position = position lightNode.eulerAngles = SCNVector3( atan2(direction.y, direction.z), atan2(direction.x, direction.z), 0) rootNode.addChildNode(lightNode) } } func rotateLayer(cubelets: [SCNNode], axis: SCNVector3, angle: CGFloat) { let tempNode = SCNNode() var originalMaterials: [SCNNode: [SCNMaterial]] = [:] var originalPositions: [SCNNode: SCNVector3] = [:] var originalEulerAngles: [SCNNode: SCNVector3] = [:] for cubelet in cubelets { if let geometry = cubelet.geometry as? SCNBox { originalMaterials[cubelet] = geometry.materials } originalPositions[cubelet] = cubelet.position originalEulerAngles[cubelet] = cubelet.eulerAngles cubelet.removeFromParentNode() tempNode.addChildNode(cubelet) } rootNode.addChildNode(tempNode) let rotationAction = SCNAction.rotate(by: angle, around: axis, duration: 0.2) tempNode.runAction(rotationAction) { for cubelet in cubelets { let worldPosition = cubelet.worldPosition tempNode.removeFromParentNode() let localPosition = self.rootNode.convertPosition(worldPosition, from: nil) cubelet.position = localPosition if axis.x != 0 { cubelet.eulerAngles = SCNVector3(cubelet.eulerAngles.x + Float(angle), cubelet.eulerAngles.y, cubelet.eulerAngles.z) } else if axis.y != 0 { cubelet.eulerAngles = SCNVector3(cubelet.eulerAngles.x, cubelet.eulerAngles.y + Float(angle), cubelet.eulerAngles.z) } else if axis.z != 0 { cubelet.eulerAngles = SCNVector3(cubelet.eulerAngles.x, cubelet.eulerAngles.y, cubelet.eulerAngles.z + Float(angle)) } if let geometry = cubelet.geometry as? SCNBox { geometry.materials = originalMaterials[cubelet] ?? geometry.materials } self.rootNode.addChildNode(cubelet) } tempNode.removeFromParentNode() } } func rotateTopLayer() { let axis = SCNVector3(x: 0, y: 1, z: 0) let angle: CGFloat = .pi / 2 let topCubelets = rootNode.childNodes.filter { node in abs(node.position.y - 0.78) < 0.01 } rotateLayer(cubelets: topCubelets, axis: axis, angle: angle) } func rotateBottomLayer() { let axis = SCNVector3(x: 0, y: 1, z: 0) let angle: CGFloat = -.pi / 2 let bottomCubelets = rootNode.childNodes.filter { node in abs(node.position.y + 0.78) < 0.01 } rotateLayer(cubelets: bottomCubelets, axis: axis, angle: angle) } func rotateRightLayer() { let axis = SCNVector3(x: 1, y: 0, z: 0) let angle: CGFloat = .pi / 2 let rightCubelets = rootNode.childNodes.filter { node in abs(node.position.x - 0.78) < 0.02 } rotateLayer(cubelets: rightCubelets, axis: axis, angle: angle) } func rotateLeftLayer() { let axis = SCNVector3(x: 1, y: 0, z: 0) let angle: CGFloat = -.pi / 2 let leftCubelets = rootNode.childNodes.filter { node in abs(node.position.x + 0.78) < 0.02 } rotateLayer(cubelets: leftCubelets, axis: axis, angle: angle) } func rotateFrontLayer() { let axis = SCNVector3(x: 0, y: 0, z: 1) let angle: CGFloat = .pi / 2 let frontCubelets = rootNode.childNodes.filter { node in abs(node.position.z - 0.78) < 0.01 } rotateLayer(cubelets: frontCubelets, axis: axis, angle: angle) } func rotateBackLayer() { let axis = SCNVector3(x: 0, y: 0, z: 1) let angle: CGFloat = -.pi / 2 let backCubelets = rootNode.childNodes.filter { node in abs(node.position.z + 0.78) < 0.01 } rotateLayer(cubelets: backCubelets, axis: axis, angle: angle) } required init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
SceneKitView.swift
import SwiftUI import SceneKit struct SceneKitView: UIViewRepresentable { @ObservedObject var cubeScene: CuboScene func makeUIView(context: Context) -> SCNView { let view = SCNView() view.scene = cubeScene view.allowsCameraControl = true view.autoenablesDefaultLighting = true view.backgroundColor = .gray // 配置初始相机 let cameraNode = SCNNode() cameraNode.camera = SCNCamera() cameraNode.position = SCNVector3(x: 5, y: 5, z: 5) // 对角线位置以查看所有面 cameraNode.look(at: SCNVector3(0, 0, 0)) // 指向魔方中心 view.pointOfView = cameraNode return view } func updateUIView(_ uiView: SCNView, context: Context) { uiView.scene = cubeScene } } #Preview { SceneKitView(cubeScene: CuboScene()) }
ContentView.swift
import SwiftUI struct ContentView: View { @StateObject private var cubeScene = CuboScene() var body: some View { VStack { SceneKitView(cubeScene: cubeScene) .frame(height: 400) .ignoresSafeArea() Button("旋转顶层") { cubeScene.rotateTopLayer() } .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(10) Button("旋转右层") { cubeScene.rotateRightLayer() } .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(10) Button("旋转左层") { cubeScene.rotateLeftLayer() } .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(10) Button("旋转底层") { cubeScene.rotateBottomLayer() } .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(10) Button("旋转前层") { cubeScene.rotateFrontLayer() } .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(10) Button("旋转后层") { cubeScene.rotateBackLayer() } .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(10) } } } #Preview { ContentView() }
问题原因与修复方案
问题根源
- 临时节点过早移除:在旋转完成的闭包中,每次循环都执行
tempNode.removeFromParentNode(),导致后续小方块无法正确获取世界坐标,节点状态混乱。 - 欧拉角手动累加错误:通过临时节点旋转后,小方块的自身欧拉角已经被旋转操作修改,手动累加角度会导致旋转状态错误,进而影响材质的显示(因为材质绑定到SCNBox的面,欧拉角错误会让原本有颜色的面转向背光或被遮挡)。
修复后的rotateLayer方法
func rotateLayer(cubelets: [SCNNode], axis: SCNVector3, angle: CGFloat) { let tempNode = SCNNode() var originalMaterials: [SCNNode: [SCNMaterial]] = [:] for cubelet in cubelets { if let geometry = cubelet.geometry as? SCNBox { originalMaterials[cubelet] = geometry.materials } cubelet.removeFromParentNode() tempNode.addChildNode(cubelet) } rootNode.addChildNode(tempNode) let rotationAction = SCNAction.rotate(by: angle, around: axis, duration: 0.2) tempNode.runAction(rotationAction) { // 先将临时节点从根节点移除,确保所有小方块的世界坐标正确 tempNode.removeFromParentNode() for cubelet in cubelets { // 将小方块的世界坐标转换为根节点的本地坐标 let localPosition = self.rootNode.convertPosition(cubelet.worldPosition, from: nil) cubelet.position = localPosition // 将小方块的世界欧拉角转换为根节点的本地欧拉角,无需手动累加 let localEuler = self.rootNode.convertEulerAngles(cubelet.eulerAngles, from: tempNode) cubelet.eulerAngles = localEuler // 恢复材质 if let geometry = cubelet.geometry as? SCNBox { geometry.materials = originalMaterials[cubelet] ?? geometry.materials } self.rootNode.addChildNode(cubelet) } } }
修复说明
- 将
tempNode.removeFromParentNode()移到循环外部,确保所有小方块在获取世界坐标时临时节点仍在场景中。 - 使用
convertEulerAngles(_:from:)将小方块在临时节点中的欧拉角转换为根节点的本地欧拉角,避免手动累加导致的错误。 - 移除了不必要的原始位置和欧拉角存储,简化逻辑。
内容的提问来源于stack exchange,提问作者mafa-
相关产品推荐
相关产品推荐

