Search code examples
iosswiftuiscenekitrubiks-cube

I need help putting the colors correctly in my Rubik's cube test


I am developing a 3D Rubik's Cube using SceneKit in Swift, and I’m encountering an issue where the colors of the cube’s faces are not rendering as solid 3x3 faces as expected. Instead of each face of the 3x3x3 cube (e.g., the front face in green, the left face in orange, etc.) being uniformly colored across all 9 cublets, the colors appear fragmented or misplaced, often showing up in rows or columns rather than covering the entire face. For example, I see the green color (intended for the front face, +Z) correctly on the top row, but it shifts to the bottom row or adjacent faces instead of forming a solid green 3x3 face.

Here’s an overview of my setup and the issue:

  • Objective: I want each face of the Rubik’s Cube (front, back, left, right, top, bottom) to be a solid color for the 9 cublets that make up that face (e.g., all cublets with z = 1 should have their front face painted green).

  • Current Behavior: Colors are applied partially or in fragmented patterns (e.g., a row of green at the top, then a row of green below, but not a solid 3x3 green face). This happens for all faces (red for +X, orange for -X, white for +Y, yellow for -Y, green for +Z, blue for -Z).

  • Suspected Issue: I suspect the cublets (SCNBox) might be rotated, causing the materials to apply to the wrong faces or in the wrong orientation, but I’ve been unable to confirm or fix this.

Here is my code:

CubeScene:

import SwiftUI
import SceneKit

class CuboScene: SCNScene {
    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
        
//        let light = SCNLight()
//        light.type = .omni
//        light.intensity = 2000
//        let lightNode = SCNNode()
//        lightNode.light = light
//        lightNode.position = SCNVector3(x: 5, y: 5, z: 5)
//        rootNode.addChildNode(lightNode)
        
        configureLights()
        createRubikCube()
    }
    
    private func createRubikCube() {
        let size: CGFloat = 0.8
        let spacing: CGFloat = 1.05
        
        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.0)  // No rounded edges
                    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)  // Ensure no rotation
                    print("Cubelet at position: (\(Float(x) * Float(spacing)), \(Float(y) * Float(spacing)), \(Float(z) * Float(spacing)) with materials: \(getFaceMaterial(x: x, y: y, z: z).map { $0.diffuse.contents as? UIColor ?? .gray }) and eulerAngles: \(cubeletNode.eulerAngles), rotation: \(cubeletNode.rotation)")
                    
                    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] = [
            // Right face (+X): red if x = 1, gray for x = 0 or -1
            (x == 1) ? .red : .gray,
            // Left face (-X): orange if x = -1, gray for x = 0 or 1
            (x == -1) ? .orange : .gray,
            // Top face (+Y): white if y = 1, gray for y = 0 or -1
            (y == 1) ? .white : .gray,
            // Bottom face (-Y): yellow if y = -1, gray for y = 0 or 1
            (y == -1) ? .yellow : .gray,
            // Front face (+Z): green if z = 1, gray for z = 0 or -1
            (z == 1) ? .green : .gray,
            // Back face (-Z): blue if z = -1, gray for z = 0 or 1
            (z == -1) ? .blue : .gray
        ]
//        print("Position (\(x), \(y), \(z)): Colors = \(colors)")  // Debugging
        return colors.map { createMaterial(color: $0) }
    }
    
    private func createMaterial(color: UIColor) -> SCNMaterial {
        let material = SCNMaterial()
        print("Applying material with color: \(color)")
        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
            ) // Orient toward the center
            rootNode.addChildNode(lightNode)
        }
    }
    
    required init(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

SceneKitView:

import SwiftUI
import SceneKit

struct SceneKitView: UIViewRepresentable {
    func makeUIView(context: Context) -> SCNView {
        let view = SCNView()
        view.scene = CuboScene()  // Use the created scene
        view.allowsCameraControl = true  // Allow camera movement with touch
        view.autoenablesDefaultLighting = false  // Disable default lighting
        view.backgroundColor = .clear
        
        // 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 cubes center
        view.pointOfView = cameraNode
        
        return view
    }

    func updateUIView(_ uiView: SCNView, context: Context) {}
}

#Preview {
    SceneKitView()
}

Here is how the Rubik's cube colors are generating:

enter image description here

I've tried a lot of different things, but I cannot make it right.


Solution

  • You've got the materials in the wrong order. It goes Front, Right, Back, Left, Top, Bottom. Update your materials array like this

    let colors: [UIColor] = [
       // Front face (+Z): green if z = 1, gray for z = 0 or -1
       (z == 1) ? .green : .gray,
       // Right face (+X): red if x = 1, gray for x = 0 or -1
       (x == 1) ? .red : .gray,
       // Back face (-Z): blue if z = -1, gray for z = 0 or 1
       (z == -1) ? .blue : .gray,
       // Left face (-X): orange if x = -1, gray for x = 0 or 1
       (x == -1) ? .orange : .gray,
       // Top face (+Y): white if y = 1, gray for y = 0 or -1
       (y == 1) ? .white : .gray,
       // Bottom face (-Y): yellow if y = -1, gray for y = 0 or 1
       (y == -1) ? .yellow : .gray
    ]