iosswiftrotationscenekitrubiks-cube

Colors Turn Black After Combining Multiple Layer Rotations in SceneKit Rubik’s Cube (Swift), But Not Individually or in Specific Combinations


I’m encountering an issue where the colors of the cubelets turn black after combining multiple layer rotations, but not after individual rotations or the specific combination of left layer rotation + top layer rotation. Individually, each layer rotation (top, bottom, right, left, front, back) works correctly, maintaining the colors (green for front, red for right, blue for back, orange for left, white for top, yellow for bottom). However, when I perform sequences like top + right, front + right, or bottom + left, the colors of some or all cubelets turn black after the second rotation, but the combination of left + top rotation preserves colors.

Here is my code so far:

CubeScene:

import SwiftUI
import SceneKit

class CuboScene: SCNScene, ObservableObject {
    override init() {
        super.init()
        rootNode.eulerAngles = SCNVector3(0, 0, 0)  // Ensure rootNode is not rotated
        rootNode.position = SCNVector3(0, 0, 0)  // Ensure rootNode is at the origin
        rootNode.scale = SCNVector3(1, 1, 1)  // Ensure no unexpected scaling
        
        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,  // Front face (+Z)
            (x == 1) ? .red : .black,    // Right face (+X)
            (z == -1) ? .blue : .black,  // Back face (-Z)
            (x == -1) ? .orange : .black, // Left face (-X)
            (y == 1) ? .white : .black,  // Top face (+Y)
            (y == -1) ? .yellow : .black // Bottom face (-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() {
        // Ambient light for base illumination
        let ambientLight = SCNLight()
        ambientLight.type = .ambient
        ambientLight.intensity = 500
        let ambientLightNode = SCNNode()
        ambientLightNode.light = ambientLight
        rootNode.addChildNode(ambientLightNode)

        // Array of positions and directions for the 6 lights
        let positions: [(SCNVector3, SCNVector3)] = [
            (SCNVector3(0, 10, 0), SCNVector3(0, -1, 0)),  // Top
            (SCNVector3(0, -10, 0), SCNVector3(0, 1, 0)),  // Bottom
            (SCNVector3(10, 0, 0), SCNVector3(-1, 0, 0)),  // Right
            (SCNVector3(-10, 0, 0), SCNVector3(1, 0, 0)),  // Left
            (SCNVector3(0, 0, 10), SCNVector3(0, 0, -1)),  // Front
            (SCNVector3(0, 0, -10), SCNVector3(0, 0, 1))   // Back
        ]

        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 eje.x != 0 {
                    cubelet.eulerAngles = SCNVector3(cubelet.eulerAngles.x + Float(angle), cubelet.eulerAngles.y, cubelet.eulerAngles.z)
                } else if eje.y != 0 {
                    cubelet.eulerAngles = SCNVector3(cubelet.eulerAngles.x, cubelet.eulerAngles.y + Float(angle), cubelet.eulerAngles.z)
                } else if eje.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:

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
        
        // Configure an initial camera
        let cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        cameraNode.position = SCNVector3(x: 5, y: 5, z: 5)  // Diagonal position to see all faces
        cameraNode.look(at: SCNVector3(0, 0, 0))  // Point to the cube's center
        view.pointOfView = cameraNode
        
        return view
    }
    
    func updateUIView(_ uiView: SCNView, context: Context) {
        uiView.scene = cubeScene
    }
}

#Preview {
    SceneKitView(cubeScene: CuboScene())  // Or RubikCubeScene if preferred
}

ContenView:

import SwiftUI

struct ContentView: View {
    @StateObject private var cubeScene = CuboScene()  // Or use RubikCubeScene if preferred
    
    var body: some View {
        VStack {
            SceneKitView(cubeScene: cubeScene)
                .frame(height: 400)
                .ignoresSafeArea()
        }
        Button("Rotate Top Layer") {
            cubeScene.rotateTopLayer()
        }
        .padding()
        .background(Color.blue)
        .foregroundColor(.white)
        .cornerRadius(10)
        
        Button("Rotate Right Layer") {
            cubeScene.rotateRightLayer()
        }
        .padding()
        .background(Color.blue)
        .foregroundColor(.white)
        .cornerRadius(10)
        
        Button("Rotate Left Layer") {
            cubeScene.rotateLeftLayer()
        }
        .padding()
        .background(Color.blue)
        .foregroundColor(.white)
        .cornerRadius(10)
        
        Button("Rotate Bottom Layer") {
            cubeScene.rotateBottomLayer()
        }
        .padding()
        .background(Color.blue)
        .foregroundColor(.white)
        .cornerRadius(10)
        
        Button("Rotate Front Layer") {
            cubeScene.rotateFrontLayer()
        }
        .padding()
        .background(Color.blue)
        .foregroundColor(.white)
        .cornerRadius(10)
        
        Button("Rotate Back Layer") {
            cubeScene.rotateBackLayer()
        }
        .padding()
        .background(Color.blue)
        .foregroundColor(.white)
        .cornerRadius(10)
    }
}

#Preview {
    ContentView()
}

Here is a video of the issue:

https://www.youtube.com/shorts/7q8XcdjdoCE


Solution

  • The colors are not "turning black" ... it looks like it, because the rotations are not correct -- revealing the "black" faces.

    You may be able to significantly simplify your rotation code. Give this a try:

    func rotateLayer(cubelets: [SCNNode], axis: SCNVector3, angle: CGFloat) {
        let tempNode = SCNNode()
        
        for cubelet in cubelets {
            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
                
                // move cublet to 0,0,0 center
                cubelet.position = .init(x: 0, y: 0, z: 0)
                
                // applying worldTransform handles the cublet rotation
                cubelet.transform = cubelet.worldTransform
                
                // move cublet back to its original position
                cubelet.position = localPosition
                
                self.rootNode.addChildNode(cubelet)
            }
            tempNode.removeFromParentNode()
        }
        
    }