15,034,644 members
Articles / Desktop Programming / WPF
Article
Posted 17 Dec 2013

41.5K views
43 bookmarked

# Magnet: A Mind Teaser in 3D

Rate me:
Can you solve this?

## 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 Figure 1. Once a pattern is made, the pattern vanishes and the game concludes.

## Background

Here is a video demonstration:

## Using the Code

Figure 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` tells us where in 3D space camera is located, default value is `Point3D(00, 125, 855)`.
• Look-direction is a `3DVector`(magnitude/direction) and tells us where to look from camera position, default value is `Vector3D(0, -125, -855)`.

• `FieldofView` is an angle, defines how enlarge the object in 3D view is projected. The figure below shows different fields 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 attracts other cubes from all directions in its line towards itself. The 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).

C#
```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`.

C#
```Cylinder3D cylinder = new Cylinder3D();
cylinder.Length = Math.Abs((Constants.NoofBlocksInXdirection) * cubeLength + 4);
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();
cylinder._content.Transform = myTransformGroup;

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 the image above, circle is divided into four portions and line is divided into two parts 0,1,2,3,4,5,6,7 are positions no (not coordinates). The below code calculates the coordinates:

C#
```for (int i =0; i <= lengthDivision; i++)
{
double y = minYCoor + i * dy;

for (int j = 0; j < circumferenceDivision; j++)
{
double t = j * dt;

}
}
```
C#
```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 joins them to form a surface and render them (by following right hand thumb rule to decide front and back surface), in the 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 connects positions to form triangles.

C#
```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

}
}```

### 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.

C#
```public static DependencyProperty StartingPointCubeProperty =
DependencyProperty.Register("StartingPointCube", typeof(Point3D), typeof(Base3D),

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.

C#
```PointLight light = new PointLight();
light.Position = new Point3D(point3d.X + widthHeightDepth,
point3d.Y + widthHeightDepth, point3d.Z + widthHeightDepth);
light.Color = Colors.Red;

### 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):

C#
```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, the below code does the same.

C#
```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 whose keys are floor no and values are `dictionary<int,cube>` keys of internal dictionary represents position on floor.

The code below creates `Cube` at specified coordinate in 3D space.

C#
```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);
}```

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

C#
```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 its movement, before magnet moves, we check if there is any vacant position in line of it movement.

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

Example 2: Let's say magnet moves 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's say magnet moves 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), the below code snippet does the same.

C#
```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 things take place.

1. `Camera position`, `lookdirection`, `fieldofview` are animated, the below code snippet does the same:
C#
```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 `position`s made empty and new `position`s are filled), the below code snippet does the same.
C#
```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, the below code snippet does the same:
C#
```MovingCube.BeginAnimation(Cube.StartingPointCubeProperty,
animationKeyFrames, HandoffBehavior.Compose); ```
4. When all `animation` stops, only then new movement of magnet can happen. The below code snippet ensures the same.
C#
```if (this.cubeAnimationCompleted == true &&
this.positionAnimationCompleted == true &&
this.cameraLookdirectionAnimationCompleted == true &&
this.fieldViewAnimationCompleted == true &&
CountMovingBlocks == 0)```

The following events ensure that `animation` has completed, only then new movements can take place:

C#
```private void AnimationKeyFramesCameraPosition_Completed(object sender, EventArgs e)
{
cameraLookdirectionAnimationCompleted = true;
} ```
C#
```private void AnimationKeyFramesCameraLookDirection_Completed(object sender, EventArgs e)
{
positionAnimationCompleted = true;
} ```
C#
```private void AnimationFieldView_Completed(object sender, EventArgs e)
{
fieldViewAnimationCompleted = true;
} ```
C#
```private void AnimationKeyFramesBox_Completed(object sender, EventArgs e)
{
cubeAnimationCompleted = true;
CheckCompletness();
} ```
5. `MagnetPositiononFloor`, `MagnetFloorNo` are updated on `magnet` movement.
• When `magnet` moves back.
C#
`MagnetPositionOnFloor = MagnetPositionOnFloor - Constants.BlocksInXdirection; `
• When `magnet` moves up.
C#
`MagnetFloorNo++;   `
• When `magnet` moves down.
C#
`MagnetFloorNo--; `
• When `magnet` move left.
C#
`MagnetPositionOnFloor--; `
• When `magnet` move right.
C#
`MagnetPositionOnFloor++;`
• When `magnet` move front.
C#
`MagnetPositionOnFloor = MagnetPositionOnFloor + Constants.BlocksInXdirection; `
6. Old events are unsubscribed and new events subscribed.
C#
```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 number of steps in direction (`Left`, `Right`, `Up`, `Down`, `Front`, `Back`).
C#
`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.
C#
`private void CheckCompletness(); `
9. `StepCount` is updated, tells how many steps magnet have moved, binded to view.
C#
```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 to invoke a function in `viewmodel` and pass key args, for that `Keyboard` event is binded to `Delegate` command using `System.Windows.Interactivity` DLL.

XML
```<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 To 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.

C#
```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;
}
}
}

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 otherwise decreased.

Opacity is animated not changed in one go.

C#
``` 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);

}
}

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's 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.

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

C#
```this.ViewModelCamera.BeginAnimation(PerspectiveCamera.PositionProperty,
animationKeyFramesCameraPosition, HandoffBehavior.Compose);```
C#
```this.ViewModelCamera.BeginAnimation(PerspectiveCamera.LookDirectionProperty,
animationKeyFramesCameraLookDirection, HandoffBehavior.Compose); ```
C#
```this.ViewModelCamera.BeginAnimation(PerspectiveCamera.FieldOfViewProperty,
animationKeyFramesFieldofView, HandoffBehavior.Compose); ```

Following properties in view model are binded to dependency property of Perspective `Camera`.

C#
```public Point3D CameraPosition
{
get
{
return this.cameraPosition;
}
set
{
this.cameraPosition = value;
NotifyPropertyChanged("CameraPosition");
}
}
```
C#
```public double FieldofView
{
get
{
return this.fieldofView;
}
set
{
this.fieldofView = value;
NotifyPropertyChanged("FieldofView");
}
}
```
C#
```public Vector3D CameraLookDirection
{
get
{
return this.cameraLookDirection;
}
set
{
this.cameraLookDirection = value;
NotifyPropertyChanged("CameraLookDirection");
}
}```

`Camera` property encapsulates the above properties and registers a `Changed` event.

C#
```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;
}
}```

Whenever 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`.

C#
```MagnetViewModel viewModel = new MagnetViewModel();
```
C#
```this.DataContext = viewModel;
```

The 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 a one time activity, removal of cube from `ViewPort3D` happens once a pattern is made and cubes need to be removed from `Viewport3D`.

C#
```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

• There could be many variants to this game, highlighted only the basic one.
• 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 ended up with something else.

## History

• 17th December, 2013: Initial version

## Share

 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/

 First Prev Next
 My vote of 5 Programm3r13-Jan-14 19:37 Programm3r 13-Jan-14 19:37
 entrée en.png hack2root9-Jan-14 19:59 hack2root 9-Jan-14 19:59
 Re: entrée en.png ATUL_LOONA9-Jan-14 20:30 ATUL_LOONA 9-Jan-14 20:30
 Re: entrée en.png hack2root12-Jan-14 5:37 hack2root 12-Jan-14 5:37
 Nice concept Meshack Musundi4-Jan-14 4:41 Meshack Musundi 4-Jan-14 4:41
 Re: Nice concept ATUL_LOONA5-Jan-14 20:56 ATUL_LOONA 5-Jan-14 20:56
 Re: Nice very Nice ATUL_LOONA2-Jan-14 19:23 ATUL_LOONA 2-Jan-14 19:23
 My vote of 5 Ramsin23-Dec-13 8:39 Ramsin 23-Dec-13 8:39
 Re: My vote of 5 ATUL_LOONA2-Jan-14 19:23 ATUL_LOONA 2-Jan-14 19:23
 Excellent idea ! Perić Željko19-Dec-13 3:41 Perić Željko 19-Dec-13 3:41
 Re: Excellent idea ! ATUL_LOONA20-Dec-13 0:22 ATUL_LOONA 20-Dec-13 0:22
 My vote of 5 fredatcodeproject19-Dec-13 2:44 fredatcodeproject 19-Dec-13 2:44
 source code fredatcodeproject17-Dec-13 23:44 fredatcodeproject 17-Dec-13 23:44
 Re: source code ATUL_LOONA18-Dec-13 0:11 ATUL_LOONA 18-Dec-13 0:11
 Where is the code???? Sacha Barber17-Dec-13 23:21 Sacha Barber 17-Dec-13 23:21
 Re: Where is the code???? ATUL_LOONA17-Dec-13 23:55 ATUL_LOONA 17-Dec-13 23:55
 The link states, "Download the source code, but there is only an executable image. Bill_Hallahan17-Dec-13 15:08 Bill_Hallahan 17-Dec-13 15:08
 great! Southmountain17-Dec-13 9:49 Southmountain 17-Dec-13 9:49
 please keep it up... diligent hands rule....
 Last Visit: 31-Dec-99 18:00     Last Update: 21-Sep-21 22:27 Refresh 1