Home   Donators   Contact Us       

<< Previous Tutorial     Next Tutorial >>

Tutorial 12 - Rubik's Cube
Rubik's Cube used by permission of Seven Towns Limited.
www.rubiks.com



Theory

Three rotation actions can be identified in Rubik's Cube, namely pitch, yaw and roll. In the previous tutorial, Tutorial 11 - Quaternions, we learned how to use quaternions to apply these three rotation actions to vectors, polygons and models.

  

To add rotation dynamics to Rubik's cube in a virtual 3D environment, we will make use of a technique known as grouping. By grouping together vertices or polygons, we can perform actions on the group instead of complicating algorithms by taking each individual element into account.

 

The first image above illustrates grouping of vertices. The blue vertices forms a vertex collection, as does the green and red vertices. The image on the right illustrates grouping of polygons. Similar to vertex collections, the red polygons forms a polygon collection, as does the blue and green polygons.

So how can we apply grouping techniques to add rotation dynamics to a virtual Rubik's Cube? We will track the user's selection of individual polygons, use this information to group together relevant polygons, and apply the correct rotation to the PolygonCollection. The image below illustrates this concept.

Imagine that the user presses down his/her left mouse button on polygon A. He/She then drags the mouse cursor to polygon B. We can use this user interaction information to determine which rotation action the user wants to perform (e.g. pitch, yaw or roll), as well as which polygons should be grouped together for the rotation.

IMPORTANT: The exact same technique can be used for touch input devices such as tablets and smartphones.

Tutorial Steps
Tutorial created with Real Studio 2011 Release 4.3.
1. Open Real Studio.
2. Choose the "Desktop" project template.
3. Tick the MaximizeButton property of Window1.
4. Add an OpenGLSurface control to Window1.
5. Resize and position OpenGLSurface1 to fill the whole form.
6. Tick the LockRight and LockBottom properties of OpenGLSurface1.
7. Save your project.
8. Download the following files below and save them next to your project file:
9. Import the classes and modules into your new project file. (Select File > Import... from the main menu)
10. Add a Timer control to Window1.
11. Change the Period property of Timer1 from 1000 to 50.
12. Add a property named "FromPoly" of type R3Polygon to Window1.
13. Add a property named "ToPoly" of type R3Polygon to Window1.
14. Add a property named "MousePrevX" of type Integer to Window1.
15. Add a property named "MousePrevY" of type Integer to Window1.
16. Add a property named "MoveInProgress" of type Boolean to Window1.
17. Add a property array named "PolyCols()" of type R3PolygonCollection to Window1.
18. Add a property named "RotationVector" of type R3Vector to Window1.
19. Add a property named "Scene" of type R3Scene to Window1.
20. Add a property named "TotDegrees" of type Integer to Window1.
21. Add a method named "RotateBlocks" to Window1.
22. Add the following code to the method RotateBlocks:
Dim i As Integer ' loop variable

' rotate the polygon collections

for i = 0 to polyCols.Ubound
  polyCols(i).Rotate(RotationVector)
next i

OpenGLSurface1.Render ' refresh the OpenGL surface

TotDegrees = TotDegrees + 15 ' increase amount of degrees rotated
23. Add a method named "SetRotationInfo" to Window1.
24. Add the following code to the method SetRotationInfo:
Dim fromCenter As R3Vector ' center of "from" polygon
Dim toCenter As R3Vector ' center of "to" polyon
Dim move As R3Vector ' temporary vector to hold move direction
Dim model As R3Model ' Rubik's Cube model

model = Scene.Model(0) ' get model from scene

fromCenter = FromPoly.Center
toCenter = ToPoly.Center

' calculate difference in "from" and "to" centers"

move = new R3Vector(fromCenter)
move.Subtract(toCenter)
move.Normalize

' "from" and "to" polygons on same plane?
if FromPoly.Normal.Equal(ToPoly.Normal) then

  ' yes, so determine the type of rotation using the "from" polygon normal and the move direction

  if R3T_AbsVectorEqualAndAbsValueOne(FromPoly.Normal, 0, 0, 1, move.y) then
  
    PolyCols = R3T_PolygonCollectionsWithXCenterAt(model, fromCenter.x) ' get polygon collections with same x value for center
    RotationVector.SetValue(FromPoly.Normal.z * move.y * -15, 0, 0) ' set pitch rotation
  
  elseif R3T_AbsVectorEqualAndAbsValueOne(FromPoly.Normal, 0, 1, 0, move.z) then
   
    PolyCols = R3T_PolygonCollectionsWithXCenterAt(model, fromCenter.x) ' get polygon collections with same x value for center
    RotationVector.SetValue(FromPoly.Normal.y * move.z * 15, 0, 0) ' set pitch rotation
  
  elseif R3T_AbsVectorEqualAndAbsValueOne(FromPoly.Normal, 0, 0, 1, move.x) then
  
    PolyCols = R3T_PolygonCollectionsWithYCenterAt(model, fromCenter.y) ' get polygon collections with same y value for center
    RotationVector.SetValue(0, FromPoly.Normal.z * move.x * 15, 0) ' set yaw rotation
  
  elseif R3T_AbsVectorEqualAndAbsValueOne(FromPoly.Normal, 1, 0, 0, move.z) then
  
    PolyCols = R3T_PolygonCollectionsWithYCenterAt(model, fromCenter.y) ' get polygon collections with same y value for center
    RotationVector.SetValue(0, FromPoly.Normal.x * move.z * -15, 0) ' set yaw rotation
  
  elseif R3T_AbsVectorEqualAndAbsValueOne(FromPoly.Normal, 0, 1, 0, move.x) then
  
    PolyCols = R3T_PolygonCollectionsWithZCenterAt(model, fromCenter.z) ' get polygon collections with same z value for center
    RotationVector.SetValue(0, 0, FromPoly.Normal.y * move.x * -15) ' set roll rotation
  
  elseif R3T_AbsVectorEqualAndAbsValueOne(FromPoly.Normal, 1, 0, 0, move.y) then
  
    PolyCols = R3T_PolygonCollectionsWithZCenterAt(model, fromCenter.z)  ' get polygon collections with same z value for center
    RotationVector.SetValue(0, 0, FromPoly.Normal.x * move.y * 15) ' set roll rotation
  
  end if

else ' "from" and "to" polygons are on different planes

  ' determine the type of rotation using the "from" and "to" polygon normals

  if R3T_AbsVectorsEqual(FromPoly.Normal, 0, 0, 1, ToPoly.Normal, 0, 1, 0) then
  
    PolyCols = R3T_PolygonCollectionsWithXCenterAt(model, fromCenter.x)  ' get polygon collections with same x value for center
    RotationVector.SetValue(FromPoly.Normal.z * ToPoly.Normal.y * 15, 0, 0) ' set pitch rotation
  
  elseif R3T_AbsVectorsEqual(FromPoly.Normal, 0, 1, 0, ToPoly.Normal, 0, 0, 1) then
  
    PolyCols = R3T_PolygonCollectionsWithXCenterAt(model, fromCenter.x) ' get polygon collections with same x value for center
    RotationVector.SetValue(FromPoly.Normal.y * ToPoly.Normal.z * -15, 0, 0) ' set pitch rotation
  
  elseif R3T_AbsVectorsEqual(FromPoly.Normal, 0, 0, 1, ToPoly.Normal, 1, 0, 0) then
  
    PolyCols = R3T_PolygonCollectionsWithYCenterAt(model, fromCenter.y) ' get polygon collections with same y value for center
    RotationVector.SetValue(0, FromPoly.Normal.z * ToPoly.Normal.x * -15, 0) ' set yaw rotation
  
  elseif R3T_AbsVectorsEqual(FromPoly.Normal, 1, 0, 0, ToPoly.Normal, 0, 0, 1) then
  
    PolyCols = R3T_PolygonCollectionsWithYCenterAt(model, fromCenter.y) ' get polygon collections with same y value for center
    RotationVector.SetValue(0, FromPoly.Normal.x * ToPoly.Normal.z * 15, 0) ' set yaw rotation
  
  elseif R3T_AbsVectorsEqual(FromPoly.Normal, 0, 1, 0, ToPoly.Normal, 1, 0, 0) then
  
    PolyCols = R3T_PolygonCollectionsWithZCenterAt(model, fromCenter.z) ' get polygon collections with same z value for center
    RotationVector.SetValue(0, 0, FromPoly.Normal.y * ToPoly.Normal.x * 15) ' set roll rotation
  
  elseif R3T_AbsVectorsEqual(FromPoly.Normal, 1, 0, 0, ToPoly.Normal, 0, 1, 0) then
  
    PolyCols = R3T_PolygonCollectionsWithZCenterAt(model, fromCenter.z) ' get polygon collections with same z value for center
    RotationVector.SetValue(0, 0, FromPoly.Normal.x * ToPoly.Normal.y * -15) ' set roll rotation
  
  end if 

end if

' clear the selected polygons

FromPoly = nil
ToPoly = nil
25. Add the following code to the Action event of Timer1:
RotateBlocks ' perform partial rotation

' did we complete a full 90 degrees rotation?
if TotDegrees >= 90 then

  Timer1.Enabled = false ' stop rotation animation
  Redim PolyCols(-1) ' clear the selected polygon collections
  RotationVector.SetValue(0, 0, 0) ' set the rotation vector to zero
  TotDegrees = 0 ' set the total amount of degrees rotated to zero

end if
26. Add the following code to the Open event of Window1:
Dim model As R3Model ' tempory model object

' instantiate the Scene object

Scene = new R3Scene
Scene.BackgroundColor.SetValue(1, 1, 1) ' white background

' instantiate a new Rubik's cube model

model = R3_Model_RubiksCube
Scene.Model.Append model ' add the model to our scene

MoveInProgress = False ' we are not busy making a move

RotationVector  = new R3Vector(0, 0, 0) ' set the rotation vector to zero
TotDegrees = 0 ' set the total amount of degrees rotated to zero

Timer1.Enabled = false
27. Add the following code to the Paint event of Window1:
' refresh the OpenGL surface

OpenGLSurface1.Render
28. Add the following code to the Open event of OpenGLSurface1:
R3_OGLInitialize ' initalize OpenGL environment

' instantiate memory blocks used to configure our light with

Dim light_position As new MemoryBlock(16)
Dim light_ambience As new MemoryBlock(16)
Dim light_diffANDspec As new MemoryBlock(16)

' define position of the light

light_position.SingleValue(0) = -1.0
light_position.SingleValue(4) = 0.0
light_position.SingleValue(8) = 3.0
light_position.SingleValue(12) = 0.0

' define ambience of the light

light_ambience.SingleValue(0) = 0.1
light_ambience.SingleValue(4) = 0.1
light_ambience.SingleValue(8) = 0.1
light_ambience.SingleValue(12) = 1.0

' define diffuse and specular of the light

light_diffANDspec.SingleValue(0) = 0.5
light_diffANDspec.SingleValue(4) = 0.5
light_diffANDspec.SingleValue(8) = 0.5
light_diffANDspec.SingleValue(12) = 1

' apply light settings

OpenGL.glLightfv OpenGL.GL_LIGHT0, OpenGL.GL_POSITION, light_position ' set position
OpenGL.glLightfv OpenGL.GL_LIGHT0, OpenGL.GL_AMBIENT, light_ambience ' set ambience
OpenGL.glLightfv OpenGL.GL_LIGHT0, OpenGL.GL_DIFFUSE, light_diffANDspec ' set diffuse
OpenGL.glLightfv OpenGL.GL_LIGHT0, OpenGL.GL_SPECULAR, light_diffANDspec ' set specular

OpenGL.glEnable OpenGL.GL_LIGHT0 ' enable our light source

' enable overwriting of material properties with vertex colors

OpenGL.glEnable OpenGL.GL_COLOR_MATERIAL
29. Add the following code to the Resized event of OpenGLSurface1:
' set the viewport rectangle

OpenGL.glViewport 0, 0, OpenGLSurface1.Width, OpenGLSurface1.Height

' set up the perspective projection settings

OpenGL.glMatrixMode OpenGL.GL_PROJECTION
OpenGL.glLoadIdentity
OpenGL.gluPerspective 60.0, OpenGLSurface1.Width / OpenGLSurface1.Height, 1, 100.0

' select and reset the modelview matrix

OpenGL.glMatrixMode OpenGL.GL_MODELVIEW
OpenGL.glLoadIdentity
30. Add the following code to the Render event of OpenGLSurface1:
' render the Scene with OpenGL

R3_OGLRenderScene Scene
31. Add the following code to the MouseUp event of OpenGLSurface1:
MoveInProgress = False ' completed move
32. Add the following code to the MouseDown event of OpenGLSurface1:
FromPoly = R3_OGLPickPolygon(Scene, x, OpenGLSurface1.Height - y) ' pick the polygon at mouse coordinate (x, y)

' did the user click on a polygon?
if FromPoly <> nil then
  MoveInProgress = True ' indicate that we started making a move
end if

' save the mouse position

MousePrevX = x
MousePrevY = y

return true
33. Add the following code to the MouseDrag event of OpenGLSurface1:
Dim model As R3Model ' temporary model object

model = Scene.Model(0) ' get the first model in the scene

' do we have a "from" polygon selected?
if FromPoly <> nil then

  ' yes, so lets find the index of the polygon the cursor is currently on 

  ToPoly = R3_OGLPickPolygon(Scene, x, OpenGLSurface1.Height - y)  ' pick the polygon at mouse coordinate (x, y)

  ' is the mouse cursor over on a polygon?
  if ToPoly <> nil then
  
    ' yes, is it a different polygon than the "from" polygon
  
    if FromPoly.Index <> ToPoly.Index then
	
	  ' yes, so lets set up the rotation information
	
	  SetRotationInfo
	
	  FromPoly = nil
	  ToPoly = nil
	
	  Timer1.Enabled = true ' start rotation animation
	
    end if
  
  end if

else

  'so rotate the whole Rubik's Cube since we are not busy making a move

  if not MoveInProgress then
  
    ' apply a pitch and yaw rotation using the mouse x and y movement
    model.Rotation.RotateProjection((y - MousePrevY), (x - MousePrevX))
  
    OpenGLSurface1.Render ' refresh the OpenGL surface
  
  end if

end if

' save the mouse position

MousePrevX = x
MousePrevY = y
34. Save and run the project.
35. Click down on the white background with your left mouse button and hold down the button. Drag the mouse cursor around to rotate the cube. Click down on a colored block with your left mouse button and hold down the button. Drag the mouse cursor to another colored block to rotate the individual blocks.

Code Analysis

If you worked through the previous tutorials, you might have noticed the change in naming convention used for classes. All classes are now abbreviated with R3 instead of R3DT.

Prior to this tutorial, polygons were added to a model by simply appending it directly to the Polygon() array of the model. A new method named AppendPolygon is added to the R3Model class. Unless you are comfortable with the inner workings of the relationship between R3Model and R3Polygon, it is recommended to never append new polygons directly to the model's polygon array, but rather use the AppendPolygon method.

Two new classes are introduced in this tutorial, R3VertexCollection and R3PolygonCollection. As their names imply, these classes are used to respectively store collections of vertices and polygons. Once you have grouped polygons into a R3PolygonCollection, rotating all the polygons in the collection simultaneously is done with a single call to the Rotate method. You can imagine how this simplifies algorithms when working with millions of polygons in a scene or model.

With computers being "binary calculators", using Double values can become complicated, especially when comparing two values with each other. Without going into the legacy details of why this is so, an important helper function is added to the R3_Core module, named R3_DoublesEqual. Whenever you need to compare two double values with each other, ALWAYS use this function rather than comparing them directly using the "=" operator. The R3_DoublesEqual function has built in tolerance values that makes the comparison of doubles more accurate.

The purpose of the helper functions, R3T_AbsVectorEqualAndAbsValueOne and R3T_AbsVectorsEqual, in the R3_Tut00012 module provides a shortcut to compare the equality of vectors and values in a very specific way. R3T_AbsVectorEqualAndAbsValueOne returns true if the absolute values of the coordinates of a given vector is equal to a set of values and when the absolute value of a variable is equal to one. R3T_AbsVectorsEqual returns true if the absolute values of the coordinates of two given vectors are equal to a certain set of values.

The SetRotationInfo method is the hart of determining the correct rotation action to take and also responsible for grouping the correct polygons together. Lets take a closer look at line 22 in SetRotationInfo:

if R3T_AbsVectorEqualAndAbsValueOne(FromPoly.Normal, 0, 0, 1, move.y) then

What this line is really saying, is if the normal vector of the "from" polygon is either facing towards us or away from us, and the mouse movement was either up or down along the y-axis, then perform a pitch rotation. The image below makes it easy to visualize the "if" statement.

Now run the Rubik's Cube simulator again, shuffle up the cube with a couple of random rotations, and see how long it takes you to un-shuffle the cube to its starting position.

Project Downloads

<< Previous Tutorial     Next Tutorial >>


 
All the content on Real 3D Tutorials, with the exception of the SyntaxHighlighter which is licensed under the MIT License, is provided to the public domain and everyone is free to use, modify, republish, sell or give away this work without prior consent from anybody. Content is provided without warranty of any kind. Under no circumstances shall the author(s) or contributor(s) be liable for damages resulting directly or indirectly from the use or non-use of the content.
Should you find the content useful and would like to make a contribution, you can show your support by making a donation.