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