
Introduction
I find the Rubik's Cube to be a really captivating and fascinating puzzle and since WPF has 3-D capabilities, I decided to try making a Rubik's cube application that would closely replicate the real thing, both visually and interactively.
Background
3-D support in WPF is not designed to provide full-featured game development capabilities but it is possible to emulate some simple games/toys, like the Rubik's cube. Please note that this article is not an introduction to 3-D graphics support in WPF, so I hope that you are at least conversant with the relevant details in this area. (I'm not exactly familiar with all the intricate details of 3-D support in WPF but I know enough to have made this application). If you are unfamiliar with the details of WPF 3-D support, or want to know more, then you can take a look at the various resources that are available online, like here on CodeProject.
Another thing that I hope you are also familiar with is some of the cubing terms. Just in case, this is how you denote the faces of a cube,

Using the Application
To turn a layer of the cube hold down the left mouse button and swipe in the direction you intend to rotate the layer. Once you let go of the left-mouse button the layer will rotate as intended.

Rotating the U layer anti-clockwise

You can also rotate the whole cube using the right-mouse button. Just hold down the right-mouse button and swipe across the intended axis of rotation. The cube will rotate once you let go off the right-mouse button.

Rotating the cube around the Z-axis

Note that to rotate a layer, or the whole cube; you only interact with the F and R faces of the cube. Also note that rotations will only occur if the swipe is along a particular layer, horizontally or vertically, and crosses two or more cubies/cubelets.
To scramble the cube, just click on the Scramble button.
Design & Layout
Designing the cube by hand-coding XAML would have been torture. Instead I opted to model the cube in Blender. After studying a few important Blender details; like moving, rotating, scaling, and applying materials to objects, I played around with the application for a while and ended up with a model that looks like a Rubik's cube.

Modelling of the cube in progress

The final result
After modeling of the cube was complete I exported the model as a Wavefront (.obj) file and then added the object (.obj) file and the material (.mtl) file that was generated to the WPF project, in Expression Blend. The object file and the material file can be found in the 3D_Cube folder.

The material and object files
Once the material and object file were added to the project it was just a matter of right-clicking the object file and selecting Insert, from the context-menu, to add the 3-D content to the artboard, in Expression Blend. The Viewport3D object that was created contained quite a number of ModelVisual3D objects, including a PerspectiveCamera and several lights.
In order for the cube not to appear like just a black mass with coloured stickers, I added a good number of lights for adequate illumination.

"All of the lights, all of the lights..."
The Viewport3D object is overlaid with 18 Path objects that are used to respond to mouse events. I could have opted to use ModelUIElement3D objects; that support input, focus, and events, but opted to stick with ModelVisual3D objects.

Each of the ModelVisual3D objects that represent cubies is named according to the colors of its 'stickers' e.g. WGR_Cubie is the White-Green-Red corner piece.
The Code
The Location enumeration contains members representing the location of a cubie in 3-D space.
Enum Location
FUL
FU
FUR
RU
BUR
BU
BUL
LU
UC
FL
FC
FR
RC
BR
BC
BL
LC
FDL
FD
FDR
RD
BDR
BD
BDL
LD
DC
End Enum
Each cubie in the Viewport3D is associated with an object of class Cubie, which contains methods for rotating the cubelet around a particular axis,
Imports System.Windows.Media.Media3D
Imports System.Windows.Media.Animation
Public Class Cubie
Public cubelet As ModelVisual3D
Private _cubieLocation As Location
Private axisPoint As New Point3D(0, 0, 0)
Private axisAngleRtn3D As New AxisAngleRotation3D(New Vector3D(0, 0, 1), 0)
Private dblAnim As DoubleAnimation
Private rtnTrans3D As RotateTransform3D
Private transGroup As New Transform3DGroup
Private milliSec As Short = 280
</span> ''' Location of the cubie in 3D space; in terms
''' of an enum Location value.
''' <span class="code-SummaryComment"></summary>
</span> Friend Property CubieLocation() As Location
Get
Return _cubieLocation
End Get
Set(ByVal value As Location)
_cubieLocation = value
End Set
End Property
''' <span class="code-SummaryComment"><summary>
</span> ''' Rotate cubie around the X-axis.
''' <span class="code-SummaryComment"></summary>
</span> ''' <span class="code-SummaryComment"><param name="angle">The angle of rotation; -90 or 90.</param>
</span> Public Sub RotateAround_X_Axis(ByVal angle As Double)
axisAngleRtn3D = New AxisAngleRotation3D(New Vector3D(1, 0, 0), 0)
rtnTrans3D = New RotateTransform3D(axisAngleRtn3D, axisPoint)
dblAnim = New DoubleAnimation(CDbl(angle), TimeSpan.FromMilliseconds(milliSec), FillBehavior.HoldEnd)
axisAngleRtn3D.BeginAnimation(AxisAngleRotation3D.AngleProperty, dblAnim)
transGroup.Children.Add(rtnTrans3D)
cubelet.Transform = transGroup
' Change value of _cubeLocation accordingly.
ChangeLocationOn_X_AxisRtn(angle)
End Sub
''' <span class="code-SummaryComment"><summary>
</span> ''' Rotate cubie around the Y-axis.
''' <span class="code-SummaryComment"></summary>
</span> ''' <span class="code-SummaryComment"><param name="angle">The angle of rotation; -90 or 90.</param>
</span> Public Sub RotateAround_Y_Axis(ByVal angle As Double)
axisAngleRtn3D = New AxisAngleRotation3D(New Vector3D(0, 1, 0), 0)
rtnTrans3D = New RotateTransform3D(axisAngleRtn3D, axisPoint)
dblAnim = New DoubleAnimation(CDbl(angle), TimeSpan.FromMilliseconds(milliSec), FillBehavior.HoldEnd)
axisAngleRtn3D.BeginAnimation(AxisAngleRotation3D.AngleProperty, dblAnim)
transGroup.Children.Add(rtnTrans3D)
cubelet.Transform = transGroup
' Change value of _cubeLocation accordingly.
ChangeLocationOn_Y_AxisRtn(angle)
End Sub
''' <span class="code-SummaryComment"><summary>
</span> ''' Rotate cubie around the Z-axis.
''' <span class="code-SummaryComment"></summary>
</span> ''' <span class="code-SummaryComment"><param name="angle">The angle of rotation; -90 or 90.</param>
</span> Public Sub RotateAround_Z_Axis(ByVal angle As Double)
axisAngleRtn3D = New AxisAngleRotation3D(New Vector3D(0, 0, 1), 0)
rtnTrans3D = New RotateTransform3D(axisAngleRtn3D, axisPoint)
dblAnim = New DoubleAnimation(CDbl(angle), TimeSpan.FromMilliseconds(milliSec), FillBehavior.HoldEnd)
axisAngleRtn3D.BeginAnimation(AxisAngleRotation3D.AngleProperty, dblAnim)
transGroup.Children.Add(rtnTrans3D)
cubelet.Transform = transGroup
' Change value of _cubeLocation accordingly.
ChangeLocationOn_Z_AxisRtn(angle)
End Sub
Private Sub ChangeLocationOn_X_AxisRtn(ByVal angle As Integer)
' Looking from R-to-L
' ===================
' 90 degree angle (anti-clockwise rotation).
If angle >
The ModelVisual3D objects that make up a particular cubie are associated with a Cubie object when the PopulateList() method in class Rotater is called by its constructor.
Private Sub PopulateList()
cubiesList.Add(New Cubie With {.CubieLocation = Location.FUL, .cubelet = mainWin.WGO_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.FU, .cubelet = mainWin.WG_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.FUR, .cubelet = mainWin.WGR_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.RU, .cubelet = mainWin.WR_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.BUR, .cubelet = mainWin.WRB_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.BU, .cubelet = mainWin.WB_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.BUL, .cubelet = mainWin.WBO_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.LU, .cubelet = mainWin.WO_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.UC, .cubelet = mainWin.WC_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.FL, .cubelet = mainWin.GO_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.FC, .cubelet = mainWin.GC_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.FR, .cubelet = mainWin.GR_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.RC, .cubelet = mainWin.RC_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.BR, .cubelet = mainWin.RB_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.BC, .cubelet = mainWin.BC_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.BL, .cubelet = mainWin.BO_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.LC, .cubelet = mainWin.OC_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.FDL, .cubelet = mainWin.YGO_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.FD, .cubelet = mainWin.YG_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.FDR, .cubelet = mainWin.YGR_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.RD, .cubelet = mainWin.YR_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.BDR, .cubelet = mainWin.YRB_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.BD, .cubelet = mainWin.YB_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.BDL, .cubelet = mainWin.YBO_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.LD, .cubelet = mainWin.YO_Cubie})
cubiesList.Add(New Cubie With {.CubieLocation = Location.DC, .cubelet = mainWin.YC_Cubie})
End Sub
As I mentioned earlier, the Path objects that overlay the Viewport3D are used to detect mouse events in order to carry out the required rotation.
Private Sub Paths_PreviewMouseLeftButtonDown(ByVal sender As Object, _
ByVal e As System.Windows.Input.MouseButtonEventArgs) _
Handles FU_1.PreviewMouseLeftButtonDown, FU_2.PreviewMouseLeftButtonDown, FU_3.PreviewMouseLeftButtonDown, _
FM_1.PreviewMouseLeftButtonDown, FM_2.PreviewMouseLeftButtonDown, FM_3.PreviewMouseLeftButtonDown, _
FD_1.PreviewMouseLeftButtonDown, FD_2.PreviewMouseLeftButtonDown, FD_3.PreviewMouseLeftButtonDown, _
RU_1.PreviewMouseLeftButtonDown, RU_2.PreviewMouseLeftButtonDown, RU_3.PreviewMouseLeftButtonDown, _
RM_1.PreviewMouseLeftButtonDown, RM_2.PreviewMouseLeftButtonDown, RM_3.PreviewMouseLeftButtonDown, _
RD_1.PreviewMouseLeftButtonDown, RD_2.PreviewMouseLeftButtonDown, RD_3.PreviewMouseLeftButtonDown
cPath_1 = CType(sender, Path)
End Sub
Private Sub Paths_PreviewMouseLeftButtonUp(ByVal sender As Object, _
ByVal e As System.Windows.Input.MouseButtonEventArgs) _
Handles FU_1.PreviewMouseLeftButtonUp, FU_2.PreviewMouseLeftButtonUp, FU_3.PreviewMouseLeftButtonUp, _
FM_1.PreviewMouseLeftButtonUp, FM_2.PreviewMouseLeftButtonUp, FM_3.PreviewMouseLeftButtonUp, _
FD_1.PreviewMouseLeftButtonUp, FD_2.PreviewMouseLeftButtonUp, FD_3.PreviewMouseLeftButtonUp, _
RU_1.PreviewMouseLeftButtonUp, RU_2.PreviewMouseLeftButtonUp, RU_3.PreviewMouseLeftButtonUp, _
RM_1.PreviewMouseLeftButtonUp, RM_2.PreviewMouseLeftButtonUp, RM_3.PreviewMouseLeftButtonUp, _
RD_1.PreviewMouseLeftButtonUp, RD_2.PreviewMouseLeftButtonUp, RD_3.PreviewMouseLeftButtonUp
cPath_2 = CType(sender, Path)
If (cPath_1 IsNot cPath_2) Then
rt.RotateLayer(cPath_1, cPath_2)
End If
End Sub
Private Sub Paths_PreviewMouseRightButtonDown(ByVal sender As Object, _
ByVal e As System.Windows.Input.MouseButtonEventArgs) _
Handles FU_1.PreviewMouseRightButtonDown, FU_2.PreviewMouseRightButtonDown, FU_3.PreviewMouseRightButtonDown, _
FM_1.PreviewMouseRightButtonDown, FM_2.PreviewMouseRightButtonDown, FM_3.PreviewMouseRightButtonDown, _
FD_1.PreviewMouseRightButtonDown, FD_2.PreviewMouseRightButtonDown, FD_3.PreviewMouseRightButtonDown, _
RU_1.PreviewMouseRightButtonDown, RU_2.PreviewMouseRightButtonDown, RU_3.PreviewMouseRightButtonDown, _
RM_1.PreviewMouseRightButtonDown, RM_2.PreviewMouseRightButtonDown, RM_3.PreviewMouseRightButtonDown, _
RD_1.PreviewMouseRightButtonDown, RD_2.PreviewMouseRightButtonDown, RD_3.PreviewMouseRightButtonDown
cPath_1 = CType(sender, Path)
End Sub
Private Sub Paths_PreviewMouseRightButtonUp(ByVal sender As Object, _
ByVal e As System.Windows.Input.MouseButtonEventArgs) _
Handles FU_1.PreviewMouseRightButtonUp, FU_2.PreviewMouseRightButtonUp, FU_3.PreviewMouseRightButtonUp, _
FM_1.PreviewMouseRightButtonUp, FM_2.PreviewMouseRightButtonUp, FM_3.PreviewMouseRightButtonUp, _
FD_1.PreviewMouseRightButtonUp, FD_2.PreviewMouseRightButtonUp, FD_3.PreviewMouseRightButtonUp, _
RU_1.PreviewMouseRightButtonUp, RU_2.PreviewMouseRightButtonUp, RU_3.PreviewMouseRightButtonUp, _
RM_1.PreviewMouseRightButtonUp, RM_2.PreviewMouseRightButtonUp, RM_3.PreviewMouseRightButtonUp, _
RD_1.PreviewMouseRightButtonUp, RD_2.PreviewMouseRightButtonUp, RD_3.PreviewMouseRightButtonUp
cPath_2 = CType(sender, Path)
If (cPath_1 IsNot cPath_2) Then
rt.RotateCube(cPath_1, cPath_2)
End If
End Sub
The RotateLayer() method in class Rotater calls several methods which determine which Path objects the user interacted with so as to respond accordingly.
Public Sub RotateLayer(ByVal path_1 As Path, ByVal path_2 As Path)
HorizontalSwipeAcross_F_Paths(path_1, path_2)
HorizontalSwipeAcross_R_Paths(path_1, path_2)
HorizontalSwipeAcross_FandR_Paths(path_1, path_2)
VerticalSwipeOn_F_Paths(path_1, path_2)
VerticalSwipeOn_R_Paths(path_1, path_2)
End Sub
</span> ''' Check which horizontal layer the user intends to rotate when
''' the user swipes horizontally across the front face of the
''' cube, and rotate the layer.
''' <span class="code-SummaryComment"></summary>
</span> Private Sub HorizontalSwipeAcross_F_Paths(ByVal path_1 As Path, ByVal path_2 As Path)
' Horizontal swipe on FU... Paths; L-to-R mouse swipe.
If (path_1 Is mainWin.FU_1 AndAlso path_2 Is mainWin.FU_2) Then
RotateFU_RU_LayerAroundY_Axis(ANTICLOCKWISE_ANGLE)
ElseIf (path_1 Is mainWin.FU_1 AndAlso path_2 Is mainWin.FU_3) Then
RotateFU_RU_LayerAroundY_Axis(ANTICLOCKWISE_ANGLE)
ElseIf (path_1 Is mainWin.FU_2 AndAlso path_2 Is mainWin.FU_3) Then
RotateFU_RU_LayerAroundY_Axis(ANTICLOCKWISE_ANGLE)
End If
' Horizontal swipe on FU... Paths; R-to-L mouse swipe.
If (path_1 Is mainWin.FU_3 AndAlso path_2 Is mainWin.FU_2) Then
RotateFU_RU_LayerAroundY_Axis(CLOCKWISE_ANGLE)
ElseIf (path_1 Is mainWin.FU_3 AndAlso path_2 Is mainWin.FU_1) Then
RotateFU_RU_LayerAroundY_Axis(CLOCKWISE_ANGLE)
ElseIf (path_1 Is mainWin.FU_2 AndAlso path_2 Is mainWin.FU_1) Then
RotateFU_RU_LayerAroundY_Axis(CLOCKWISE_ANGLE)
End If
...
End Sub
''' <span class="code-SummaryComment"><summary>
</span> ''' Check which horizontal layer the user intends to rotate when
''' the user swipes horizontally across the right face of the
''' cube, and rotate the layer.
''' <span class="code-SummaryComment"></summary>
</span> Private Sub HorizontalSwipeAcross_R_Paths(ByVal path_1 As Path, ByVal path_2 As Path)
...
' =======================================================
' Horizontal swipe on RM... Paths; L-to-R mouse swipe.
If (path_1 Is mainWin.RM_1 AndAlso path_2 Is mainWin.RM_2) Then
RotateFM_RM_LayerAroundY_Axis(ANTICLOCKWISE_ANGLE)
ElseIf (path_1 Is mainWin.RM_1 AndAlso path_2 Is mainWin.RM_3) Then
RotateFM_RM_LayerAroundY_Axis(ANTICLOCKWISE_ANGLE)
ElseIf (path_1 Is mainWin.RM_2 AndAlso path_2 Is mainWin.RM_3) Then
RotateFM_RM_LayerAroundY_Axis(ANTICLOCKWISE_ANGLE)
End If
' Horizontal swipe on RM... Paths; R-to-L mouse swipe.
If (path_1 Is mainWin.RM_3 AndAlso path_2 Is mainWin.RM_2) Then
RotateFM_RM_LayerAroundY_Axis(CLOCKWISE_ANGLE)
ElseIf (path_1 Is mainWin.RM_3 AndAlso path_2 Is mainWin.RM_1) Then
RotateFM_RM_LayerAroundY_Axis(CLOCKWISE_ANGLE)
ElseIf (path_1 Is mainWin.RM_2 AndAlso path_2 Is mainWin.RM_1) Then
RotateFM_RM_LayerAroundY_Axis(CLOCKWISE_ANGLE)
End If
...
End Sub
''' <span class="code-SummaryComment"><summary>
</span> ''' Check for horizontal swipes that cross from the front face of
''' the cube to the right face, and vice-versa, and rotate the
''' appopriate layer.
''' <span class="code-SummaryComment"></summary>
</span> Private Sub HorizontalSwipeAcross_FandR_Paths(ByVal path_1 As Path, ByVal path_2 As Path)
' Horizontal swipe crossing from FU Paths to RU Paths
' and vice-versa.
If (path_1 Is mainWin.FU_2 AndAlso path_2 Is mainWin.RU_1) Then
RotateFU_RU_LayerAroundY_Axis(ANTICLOCKWISE_ANGLE)
ElseIf (path_1 Is mainWin.FU_3 AndAlso path_2 Is mainWin.RU_1) Then
RotateFU_RU_LayerAroundY_Axis(ANTICLOCKWISE_ANGLE)
ElseIf (path_1 Is mainWin.RU_2 AndAlso path_2 Is mainWin.FU_3) Then
RotateFU_RU_LayerAroundY_Axis(CLOCKWISE_ANGLE)
ElseIf (path_1 Is mainWin.RU_1 AndAlso path_2 Is mainWin.FU_3) Then
RotateFU_RU_LayerAroundY_Axis(CLOCKWISE_ANGLE)
End If
...
End Sub
The methods called by the various methods highlighted above determine which cubies to rotate to create the illusion of a layer rotation e.g. the following methods lead to layer rotation around the Y-axis,
Public Sub RotateFU_RU_LayerAroundY_Axis(ByVal angle As Double)
Dim cubiesToMove = cubiesList.Where _
(Function(c) c.CubieLocation = Location.FUL Or c.CubieLocation = Location.FU Or _
c.CubieLocation = Location.FUR Or c.CubieLocation = Location.RU Or _
c.CubieLocation = Location.BUR Or c.CubieLocation = Location.BU Or _
c.CubieLocation = Location.BUL Or c.CubieLocation = Location.LU Or _
c.CubieLocation = Location.UC)
For Each c In cubiesToMove
c.RotateAround_Y_Axis(angle)
Next
End Sub
Private Sub RotateFM_RM_LayerAroundY_Axis(ByVal angle As Double)
Dim cubiesToMove = cubiesList.Where _
(Function(c) c.CubieLocation = Location.FL Or c.CubieLocation = Location.FC Or _
c.CubieLocation = Location.FR Or c.CubieLocation = Location.RC Or _
c.CubieLocation = Location.BR Or c.CubieLocation = Location.BC Or _
c.CubieLocation = Location.BL Or c.CubieLocation = Location.LC)
For Each c In cubiesToMove
c.RotateAround_Y_Axis(angle)
Next
End Sub
Public Sub RotateFD_RD_LayerAroundY_Axis(ByVal angle As Double)
Dim cubiesToMove = cubiesList.Where _
(Function(c) c.CubieLocation = Location.FDL Or c.CubieLocation = Location.FD Or _
c.CubieLocation = Location.FDR Or c.CubieLocation = Location.RD Or _
c.CubieLocation = Location.BDR Or c.CubieLocation = Location.BD Or _
c.CubieLocation = Location.BDL Or c.CubieLocation = Location.LD Or _
c.CubieLocation = Location.DC)
For Each c In cubiesToMove
c.RotateAround_Y_Axis(angle)
Next
End Sub
The class Scrambler deals with the scrambling of the cube.
Imports System.Windows.Threading
Public Class Scrambler
Private rtr As Rotater
Private mainWin As MainWindow
Private rotations() As String = {"F", "F'", "F2", "R", "R'", "R2", _
"B", "B'", "B2", "L", "L'", "L2", _
"U", "U'", "U2", "D", "D'", "D2"}
Private rndIndex As Integer = -1
Private index As Integer
Private listIndex As Integer
Private isScrambling As Boolean
Private Const CLOCKWISE_ANGLE As Double = -90
Private Const ANTICLOCKWISE_ANGLE As Double = 90
Private rnd As New Random
Private scrambleTimer As DispatcherTimer
Private rotationsList As New List(Of String)
Public Sub New(ByRef rt As Rotater, ByRef win As MainWindow)
rtr = rt
mainWin = win
scrambleTimer = New DispatcherTimer
scrambleTimer.Interval = New TimeSpan(0, 0, 0, 0, 400)
AddHandler scrambleTimer.Tick, AddressOf Timer_Tick
End Sub
Public Sub ScrambleCube()
If (isScrambling = False) Then
rotationsList.Clear()
For i As Integer = 0 To 24
PopulateRotationsList()
Next
mainWin.ViewportAndCanvasGrid.IsEnabled = False
isScrambling = True
scrambleTimer.Start()
Else
MessageBox.Show("Scrambling of cube already in progress", _
"WPF Rubiks", MessageBoxButton.OK, MessageBoxImage.Exclamation)
End If
End Sub
</span> ''' Populate rotationsList with disimillar Strings following
''' each other, and the next String in the List is not an
''' inverse or double of the preceding String e.g. F' does
''' not come after F, or F2 after F.
''' <span class="code-SummaryComment"></summary>
</span> Private Sub PopulateRotationsList()
Dim n As Integer = rnd.Next(0, 18)
Dim lastString As String = String.Empty
If (rotationsList.Count >
Conclusion
That's it. I hope you'll have a fun time solving the WPF Rubik's Cube. I have scrambled and solved it several times and it is quite as thrilling as the real thing.
History
- 1st Feb, 2012: Initial post