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:
I've tried a lot of different things, but I cannot make it right.
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
]