You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

问题原因与修复方案

问题根源

  1. 临时节点过早移除:在旋转完成的闭包中,每次循环都执行tempNode.removeFromParentNode(),导致后续小方块无法正确获取世界坐标,节点状态混乱。
  2. 欧拉角手动累加错误:通过临时节点旋转后,小方块的自身欧拉角已经被旋转操作修改,手动累加角度会导致旋转状态错误,进而影响材质的显示(因为材质绑定到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)
        }
    }
}

修复说明

  1. 将tempNode.removeFromParentNode()移到循环外部,确保所有小方块在获取世界坐标时临时节点仍在场景中。
  2. 使用convertEulerAngles(_:from:)将小方块在临时节点中的欧拉角转换为根节点的本地欧拉角,避免手动累加导致的错误。
  3. 移除了不必要的原始位置和欧拉角存储,简化逻辑。

内容的提问来源于stack exchange,提问作者mafa-

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 04:03:10