Click here to Skip to main content
Click here to Skip to main content

Magnet: A mind teaser in 3D

By , 17 Dec 2013
Rate this:
Please Sign up or sign in to vote.

Introduction 

This piece of work brings together the basic principles of Mathematics and Physics. This app is a mind teaser in three dimensional space, the idea is to push cubes in left/right, front/back, top/down directions in a 3D mesh using a yellow cube aka magnet and make a pattern as shown on the left side in fig. 1. Once a pattern is made the pattern vanishes and the game concludes.

Background 

Here is a video demonstration:

Using the code

Fig. 1

The above picture is a snapshot of the application, written in WPF using the MVVM pattern. The codes make use of a camera, lights (point, direction), custom geometry (modelvisual3d), delegatecommand, custom trigger action, dependency property, and many more..    

I will drive the article in the form of questions and answers (what the code does along with code piece).   

What is perspective-camera, position, lookdirection, fieldofView ? 

Perspective camera projects 3D object on 2D surface in more realistic way  

  • Position tell us where in 3D space camera is located, default value is Point3D(00, 125, 855).  
  • Look-direction is a 3DVector(magnitude/direction) tells us where to look from camera position, default value is Vector3D(0, -125, -855).    

  • FieldofView is a angle, define how enlarge the object in 3D view is projected, figure below show different field of view angles formed by same coloured lines, default value is 70, its like zoom-in and zoom-out feature, incrementing fieldofview cause zoom-in, decrementing fieldofview cause zoom-out.      

What pattern to make and controls to move magnet ? 

         

Build the pattern shown above by moving the magnet and pushing other cubes in any direction. Magnet when active attract other cubes from all direction in its line towards itself, second picture shows keyboard keys to move the magnet, to activate magnet press space/enter, use pageup/pagedown to move front and back, alternate keys are ( f - left, g - front, v - down, t - up, h - right, y - back, space - magnet).  

  private Model3DGroup TargetFigure();  

How to build Mesh ? 

 

Mesh is network of lines, connected together to form a structure with empty blocks, in these empty blocks magnet moves, these lines are actually a cylinder with radius 1, Mesh.cs is responsible for creating, rotating, translating and positioning lines/cylinder to form a 3d mesh.   

Sample code from Mesh.cs creates a cylinder3d. 

Cylinder3D cylinder = new Cylinder3D();
cylinder.Length = Math.Abs((Constants.NoofBlocksInXdirection) * cubeLength + 4);
cylinder.Radius = cylinderRadius;
cylinder.Material = new DiffuseMaterial(colorBrush);
cylinder.BackMaterial = new DiffuseMaterial(colorBrush);
 
TranslateTransform3D Transalte = new TranslateTransform3D();
Transalte.OffsetX = xCoordinate + 2;
Transalte.OffsetY = yCoordinate + cubeLength * levels;
Transalte.OffsetZ = zCoordinate;
 
RotateTransform3D ROTATE = new RotateTransform3D();
Vector3D vector3d = new Vector3D(0, 0, 1);
ROTATE.Rotation = new AxisAngleRotation3D(vector3d, 90);
 
Transform3DGroup myTransformGroup = new Transform3DGroup();
myTransformGroup.Children.Add(ROTATE);
myTransformGroup.Children.Add(Transalte);
cylinder._content.Transform = myTransformGroup;
modelGroup.Children.Add(cylinder._content);

How lines are made ?

This is a zoom image of a portion of line which actually is a cylinder, cylinder is MeshGeometry3D, mesh geometry is collection positions and triangles indices. 

Positions is collection of coordinates, divide the line length into parts , create circles around the line at part (such that line passes through the centre of circles and line is perpendicular to circle surface), divide the circle circumference into portions, calculate coordinates of points on circumference,

In image above circle is divided into 4 portions and line is divided into two parts 0,1,2,3,4,5,6,7 are positions no(not coordinates), below code calculates the coordinates.  

 for (int i =0; i <= lengthDivision; i++)
            {
                double y = minYCoor + i * dy;
 
                for (int j = 0; j < circumferenceDivision; j++)
                {
                    double t = j * dt;
 
                    mesh.Positions.Add(GetPosition(t, y));
                }
            } 										 
 Point3D GetPosition(double t, double y)
        {
            double x = Radius * Math.Cos(t);
            double z = Radius * Math.Sin(t);
            return new Point3D(x, y, z);
        }

TriangleIndices is collection of positions, wpf rendering system picks 3 positions from triangles indices in continuation and join them to form a surface and render them(by following right hand  thumb rule to decide front and back surface), in figure above  0 4 1 1 4 5 1 5 2 2 5 6 2 6 3 3 6 7 3 7 0 0 7 4  are triangle indices, by connecting positions in triangle indices surfaces are made. This code below connect positions to form triangles.

for (int i = 0; i < lengthDivision; i++)
            {
                for (int j = 0; j < circumferenceDivision; j++)
                {
                   int x0 = j % circumferenceDivision + i * circumferenceDivision;//0
                   int x1 = (j + 1) % circumferenceDivision + i * circumferenceDivision;//1
                    int x2 = j + circumferenceDivision + i * circumferenceDivision;//4

                    int x3 = x1;//1
                    int x4 = x3 + circumferenceDivision;//5
                    int x5 = x2;//4
                   
                    mesh.TriangleIndices.Add(x0);
                    mesh.TriangleIndices.Add(x2);
                    mesh.TriangleIndices.Add(x1);
 
                    mesh.TriangleIndices.Add(x3);
                    mesh.TriangleIndices.Add(x5);
                    mesh.TriangleIndices.Add(x4);
               }
            }

How to make cubes ?

   

Cube is MeshGeometry3D formed by positions and triangle-indices, 0,1,2,3,4,5,6,7 are positions on a cube every point is relative to starting coordinate, just need to provide starting coordinates and widthheightdepth to cube.cs it will draw itself.   

public static DependencyProperty StartingPointCubeProperty = 					DependencyProperty.Register("StartingPointCube", typeof(Point3D), typeof(Base3D), 	new PropertyMetadata(OnPoint3dChanged));																	public Point3D StartingPointCube
        {
            get { 
		   return (Point3D)GetValue(StartingPointCubeProperty); 
		}
            set {
           
                    SetValue(StartingPointCubeProperty, value);
                }
        }

 The magnet is illuminated in special way, there is point light inside it to give illumination. 

PointLight light = new PointLight();
light.Position = new Point3D(point3d.X + widthHeightDepth, 							     point3d.Y + widthHeightDepth, 							     point3d.Z + widthHeightDepth);
light.Color = Colors.Red;
modelGroup.Children.Add(light);

Placement of cubes how ?

In mesh there are 125 empty blocks where cubes can be placed, imagine there are 5 floors, on each floor there are 25 blocks(5 blocks in x direction * 5 blocks in z direction),  

public static int BlocksInXdirection = 5;
public static int BlocksInZdirection = 5;
public static int NoofFloor = 5; 

There are 12 cubes, 4 of each color(red,blue,green) and one moving cube magnet, these cubes are placed randomly in any of the 125 blocks, below code does the same.     

int xcoor, ycoor, zcoor;
int floorNo = -1;
int positionOnFloor = randomCube.Next(0, cubesPerFloor);
 
Random randomSteps = new Random();
 
for (int i = 1; i <= TotalCubes; i++)
    {
          positionOnFloor = randomCube.Next(0, cubesPerFloor);
 
          Color color = ColorsCollection[i % 3];
 
          floorNo = (floorNo + 3) % Constants.NoofFloor;
 
          //This position is unoccupied
      if (position[floorNo][positionOnFloor] == null)
      {
        xcoor = (int)(positionOnFloor % (Constants.BlocksInXdirection));
        ycoor = floorNo;
        zcoor = (int)(positionOnFloor / (Constants.BlocksInZdirection));
 
 position[floorNo][positionOnFloor] = PlaceCube(xcoor, floorNo, zcoor, color);
      }        
    }

We maintain each placed cube in data structure position, position is a dictionary who's keys are floor no and values are dictionary<int,cube> keys of internal dictionary represents position on floor. 

 

The code below create Cube at specified coordinate in 3d space. 

private Cube PlaceCube(int xCoor, int yCoor, int zCoor, Color color)
{
      double cubeLength =Constants.CubeLength;
      Cube cube3d = new Cube();
      cube3d.Transform = Translate;
      cube3d.color = color;
      cube3d.WidthHeightDepth = Constants.CubeLength;
      cube3d.opacity = 1;
      cube3d.StartingPointCube = new Point3D(cubeLength * xCoor, cubeLength * yCoor,cubeLength * zCoor);
      cubesCollection.Add(cube3d);
}

By default the chosen position of magnet is floor 2 and 22 is position on floor. 

this.Magnet = PlaceCube(Constants.MagnetBlockXDirection,Constants.MagnetBlockYDirection, Constants.MagnetBlockZDirection, Colors.Yellow); 
           
this.MagnetFloorNo = Constants.MagnetBlockYDirection;
this.MagnetPositionOnFloor = Constants.BlocksInXdirection * Constants.MagnetBlockZDirection + Constants.MagnetBlockXDirection;
         
this.position[this.MagnetFloorNo][this.MagnetPositionOnFloor] = this.Magnet;
this.Magnet.IsMovingCube = true;

Movement of magnet how ?

Magnet can push other cubes in direction of it movement, before magnet moves we check if there is any vacant position in line of it movement.   

Example 1 :  let say magnet move left from current position 24 on floor no 1, it can move( if any one of positions 23,22,21,20 on same floor is vacant). 

Example 2 :  let say magnet move back from current position 22 on floor no 1, it can move( if any one of positions 2,7,12,17 on same floor is vacant). 

Example 3 :  let say magnet move up from current position 10 on floor no 1, it can move( if any one of position 10 on floor no 2,3,4,5 is vacant), below code snippet does the same.   

case Direction.Up:
for (counter = MagnetFloorNo + 1; counter < Constants.NoofFloor; counter++)
 {
    if (position[counter][MagnetPositionOnFloor] == null)
          {
            emptyPositionOrFloor = counter;
            canMove = true;
            break;
          }
 }   

 if magnet can move many thing take place. 

1)  Camera position, lookdirection, fieldofview are animated, below code snippet does the same. 

this.ViewModelCamera.BeginAnimation(PerspectiveCamera.PositionProperty, animationKeyFramesCameraPosition, HandoffBehavior.Compose);
this.ViewModelCamera.BeginAnimation(PerspectiveCamera.LookDirectionProperty, animationKeyFramesCameraLookDirection, HandoffBehavior.Compose);
this.ViewModelCamera.BeginAnimation(PerspectiveCamera.FieldOfViewProperty, animationKeyFramesFieldofView, HandoffBehavior.Compose);   
2)  On magnet movement datastructure  position is updated (old positions made empty and new positions are filled), below code snippet does the same.  
case Direction.Left:
if (cubeLocation.X >= 0)
 {
for (counter = emptyPositionOrFloor + 1; counter < MagnetPositionOnFloor; counter++)
     {
           this.MoveBlocks(position[MagnetFloorNo][counter], 1, Direction.Left);
           position[MagnetFloorNo][counter - 1] = position[MagnetFloorNo][counter];
     }
 
position[MagnetFloorNo][MagnetPositionOnFloor - 1] = position[MagnetFloorNo][MagnetPositionOnFloor];
            position[MagnetFloorNo][MagnetPositionOnFloor] = null;
            MagnetPositionOnFloor--;
            goto default;
 }....  

3) Magnet movement is animated, below code snippet does the same.  

MovingCube.BeginAnimation(Cube.StartingPointCubeProperty, animationKeyFrames, HandoffBehavior.Compose); 

4) When all animation stop only then new movement of magnet can happen , below code snippet ensure the same.   

if (this.cubeAnimationCompleted == true && 
    this.positionAnimationCompleted == true && 
    this.cameraLookdirectionAnimationCompleted == true && 
    this.fieldViewAnimationCompleted == true && 
    CountMovingBlocks == 0)

following events ensure that animation has completed only then new movements can take place.

private void AnimationKeyFramesCameraPosition_Completed(object sender, EventArgs e)
 {
     cameraLookdirectionAnimationCompleted = true;
 } 
private void AnimationKeyFramesCameraLookDirection_Completed(object sender, EventArgs e)
 {
      positionAnimationCompleted = true;
 } 
private void AnimationFieldView_Completed(object sender, EventArgs e)
 {
      fieldViewAnimationCompleted = true;
 } 
private void AnimationKeyFramesBox_Completed(object sender, EventArgs e)
 {
      cubeAnimationCompleted = true;
      CheckCompletness();
 } 

5)  MagnetPositiononFloor, MagnetFloorNo are updated on magnet movement.

  •  When magnet moves back. 
MagnetPositionOnFloor = MagnetPositionOnFloor - Constants.BlocksInXdirection; 
  • When magnet moves up. 
MagnetFloorNo++;   
  • When magnet moves down.  
MagnetFloorNo--; 
  •  When magnet move left.      
MagnetPositionOnFloor--; 
  • When magnet move right.  
MagnetPositionOnFloor++;
  • When magnet move front.  
MagnetPositionOnFloor = MagnetPositionOnFloor + Constants.BlocksInXdirection; 

6) Old events are unsubscribed and new events subscribed. 

if (animationFieldView != null)
    {
      animationFieldView.Completed -= new EventHandler(AnimationFieldView_Completed);
    }
             
animationFieldView = new DoubleAnimationUsingKeyFrames();
animationFieldView.Completed += new EventHandler(AnimationFieldView_Completed);...	

7) Magnet pushes other cubes, here cube3d is instance of cube that is pushed by magnet by no. of steps   in direction( Left, Right, Up, Down, Front,Back)  

private void MoveBlocks(Cube cube3d, int step, Direction direction).... 

8) When magnet move pattern completeness is checked on completion of  magnet animation as shown in event above. 

private void CheckCompletness(); 
 9) StepCount is updated, tells how many steps magnet have moved, binded to view.
 public int StepCount
        {
            get
            {
                return stepCount;
            }
            set
            {
                this.stepCount = value;
                NotifyPropertyChanged("StepCount");
            }
        }

How to pass keyboard event with keyargs to ViewModel ? 

On keydown event in main window we want invoke a function in viewmodel  and pass key args, for that Keyboard event is binded to Delegate command using System.Windows.Interactivity dll 

<i:Interaction.Triggers> 
<i:EventTrigger EventName="KeyDown">
<local:InvokeDelegateCommandAction 
 Command="{Binding KeyDownCommand}"
 CommandName="KeyDownCommand"
 CommandParameter="{Binding RelativeSource={RelativeSource Self}, Path=InvokeParameter}" />
</i:EventTrigger>									   </i:Interaction.Triggers>

InvokeDelegateCommandAction is custom trigger action for passing keyargs, taken help from this Post

How do make sure cubes are visible when they fall in line of sight ? 

On every move of magnet, we calculate distance of others cubes from camera position and maintain old distance. 

private List<Cube> CalculateDistance(PerspectiveCamera camera)
     {
          Cube cube;
            Vector3D vector;
            List<Cube> cubeCollection = new List<Cube>();
            for (int floor = 0; floor < Constants.NoofFloor; floor++)
            {
                for (int i = 0; i < Constants.NoofBlocksInXdirection * Constants.NoofBlocksInZdirection; i++)
                {
                    if (!((position[floor][i] == null) || (floor == MagnetFloorNo && i == MagnetPositionOnFloor)))
                    {
                        cube = position[floor][i] as Cube;
                        vector = Point3D.Subtract(camera.Position, cube.Point3DCircuit);
                        cube.OldDistanceFromViewer = cube.NewDistanceFromViewer;
                        cube.NewDistanceFromViewer = vector.Length;
                        cubeCollection.Add(cube);
                    }
                }
            }
 
            return cubeCollection.OrderByDescending(x => x.NewDistanceFromViewer).ToList();
        }

by differentiating between old distance and new distance opacity is changed, if cube has move farther from old position opacity is increased other wise decreased.   

 Opacity is animated not changed in one go.  

 if ((cubeCollection[i].NewDistanceFromViewer - cubeCollection[i].OldDistanceFromViewer) > 0)
                {
                    oldOpacity = .8;
                    delta = (.1) / factor;
                }
                else
                {
                    oldOpacity = 1;
                    delta = -(.2) / factor;
                }
 
                for (int count = 1; count < factor; count++)
                {
                    newOpacity = oldOpacity + delta * count;
                    if (newOpacity > 0 && newOpacity <= 1)
                    {
                        LinearDoubleKeyFrame linearkeyFrame = new LinearDoubleKeyFrame(newOpacity);
 
                        opacitykeyFrame.KeyFrames.Add(linearkeyFrame);
                    }
                }
 
cubeCollection[i].BeginAnimation(Cube.opacityProperty, opacitykeyFrame, HandoffBehavior.SnapshotAndReplace); 

Why do we need to animate position,look-direction,fieldofView property of camera ? 

To give perfect view of moving magnet in 3D mesh, there is need to animate camera,when magnet moves left/right/up/down/front/back camera is moved left/right/up/down/front/back respectively to stay focused on magnet.  

There is special handling on front movement of magnet fieldofview is decreased and on back movement of cube fieldofview is increased by fixed coordinates.  

e.g Let say cube has moved to left mesh block from current position , camera.position.x coordinate is decreased by delta but camera.lookdirection.x coordinate is increased by delta. 

Note: We ensure the camera.lookdirection.magnitude is constant while animating camera.position, camera.lookdirection and camera.fieldofview.   

How do we animate position,look-direction,fieldofView property of camera ? 

Point3DAnimationUsingKeyFrames, Vector3DAnimationUsingKeyFrames, DoubleAnimationUsingKeyFrames are used to animated position,look-direction,fieldofView respectively. 

Which ever way the magnet moves e.g cube moves in up direction by x coordinates same as cube length,              

this.ViewModelCamera.BeginAnimation(PerspectiveCamera.PositionProperty, animationKeyFramesCameraPosition, HandoffBehavior.Compose);
this.ViewModelCamera.BeginAnimation(PerspectiveCamera.LookDirectionProperty, animationKeyFramesCameraLookDirection, HandoffBehavior.Compose); 
this.ViewModelCamera.BeginAnimation(PerspectiveCamera.FieldOfViewProperty, animationKeyFramesFieldofView, HandoffBehavior.Compose); 
Following properties in view model are binded to dependency property of Perspective Camera.     
 public Point3D CameraPosition
        {
            get
            {
                return this.cameraPosition;
            }
            set
            {
                this.cameraPosition = value;
                NotifyPropertyChanged("CameraPosition");
            }
        } 
 public double FieldofView
        {
            get
            {
                return this.fieldofView;
            }
            set
            {
                this.fieldofView = value;
                NotifyPropertyChanged("FieldofView");
            }
        }	
public Vector3D CameraLookDirection
        {
            get
            {
                return this.cameraLookDirection;
            }
            set
            {
                this.cameraLookDirection = value;
                NotifyPropertyChanged("CameraLookDirection");
            }
        }

Camera property encapsulates above properties and registers a Changed event. 

private PerspectiveCamera ViewModelCamera
        {
            get
            {
                if (camera == null)
                {
                    camera = new PerspectiveCamera(CameraPosition, CameraLookDirection, new Vector3D(0, 0, 0), FieldofView);
                    camera.Changed += new EventHandler(Camera_changed);
                }
 
                return camera;
            }
        }

When ever there is change in any of ViewModelCamera properties Changed event is raised and we notify the MainWindow.xml to update itself.   

MVVM Pattern ?

Code is based on MVVM pattern, instance of  MagnetViewModel  is set as datacontext. 

  MagnetViewModel viewModel = new MagnetViewModel(); 
  this.DataContext = viewModel; 

Entire code was initially written in code behind but later moved to MVVM pattern to make code fully testable but still there is a little code in codebehind file which caters to addition and removal of cubes in ViewPort3d. Addition of all cube to ViewPort3D is one time activity, removal of cube from ViewPort3D happens once a pattern is made and cubes need to be removed from Viewport3D. 

void ViewModel_CollectionChangedChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            List<Cube> removeCubes = sender as  List<Cube>;
 
            for (int k = 0; k < removeCubes.Count; k++)
            {
                this.ViewPort3dPentagon.Children.Remove(removeCubes[k]);
            }
        } 

CollectionChanged events is raised from view model, (registered and listened) in code behind file to remove cube. 

Points of Interest

1) There could be many variants to this game, highlighted only the basic one. 

2) Most important thing to note performance of app in term of usability and system resource. 

The most fun part is, it started with something and end up something else.  

Do vote if you like this article cheers. 

License

This article, along with any associated source code and files, is licensed under Microsoft Reciprocal License

About the Author

ATUL_LOONA

India India
"It's impossible," said pride. "It's risky," said experience . "It's pointless," said reason "Give it a try." whispered the heart
 

Another piece of work here are links.
 
1) http://monk.parseapp.com/index.htm
 
2) http://picassa.parseapp.com/index.htm
 
3) http://circles.parseapp.com/
Follow on   LinkedIn

Comments and Discussions

 
GeneralMy vote of 5 PinmemberProgramm3r13-Jan-14 19:37 
Questionentrée en.png Pinmemberhack2root9-Jan-14 19:59 
AnswerRe: entrée en.png PingroupATUL_LOONA9-Jan-14 20:30 
GeneralRe: entrée en.png Pinmemberhack2root12-Jan-14 5:37 
GeneralNice concept PinprofessionalMeshack Musundi4-Jan-14 4:41 
GeneralRe: Nice concept PingroupATUL_LOONA5-Jan-14 20:56 
QuestionNice very Nice PinmvpShivprasad koirala30-Dec-13 16:10 
AnswerRe: Nice very Nice PingroupATUL_LOONA2-Jan-14 19:23 
GeneralMy vote of 5 PinmemberRamsin23-Dec-13 8:39 
GeneralRe: My vote of 5 PingroupATUL_LOONA2-Jan-14 19:23 
SuggestionExcellent idea ! [modified] PinmemberPerić Željko19-Dec-13 3:41 
GeneralRe: Excellent idea ! PingroupATUL_LOONA20-Dec-13 0:22 
GeneralMy vote of 5 Pinmemberfredatcodeproject19-Dec-13 2:44 
Questionsource code Pinmemberfredatcodeproject17-Dec-13 23:44 
AnswerRe: source code PingroupATUL_LOONA18-Dec-13 0:11 
QuestionWhere is the code???? PinmvpSacha Barber17-Dec-13 23:21 
AnswerRe: Where is the code???? PingroupATUL_LOONA17-Dec-13 23:55 
QuestionThe link states, "Download the source code, but there is only an executable image. PinmemberBill_Hallahan17-Dec-13 15:08 
Generalgreat! PinmemberSouthmountain17-Dec-13 9:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140421.2 | Last Updated 17 Dec 2013
Article Copyright 2013 by ATUL_LOONA
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid