The other day I was browsing some archives on an old disk drive and came across the T64 formatted binaries of some demo software I wrote for the Commodore 64 in the 1980s. Demo software is fun to write and is more about tricks and optimizations than business rules or data models. I had written a few fun projects in Silverlight a few years back and decided to convert some over to Windows 8. This particular project involved using a plane projection on six different rectangles to provide the illusion of a cube (it is an illusion because it’s not true 3D rendering, but instead the manipulation of the rectangles to give the illusion of a 3D construct). This was overlaid on a classic sine-based plasma field.
Once again, I was amazed at how quickly and easily I was able to port the code over to Windows 8. Essentially, the only real work was changing the implementation of the plasma effect because the older version was able to manipulate pixels directly in the bitmap object, whereas in Windows 8 this must be done in a buffer and transferred over. By no means is this code optimized and efficient – you’ll certainly see the CPU spike – but it’s a neat and fun effect to watch and coding it was a nice break from typical line of business work.
The first step was creating the cube. A style defines the properties of a “side”:
<Style TargetType="Rectangle">
<Setter Property="Margin" Value="170,50"/>
<Setter Property="Height" Value="150"/>
<Setter Property="Width" Value="150"/>
<Setter Property="Opacity" Value="0.5"/>
</Style>
Next, the sides are positioned to simulate the actual surface of a cube – here are just two of the six sides as an example. Note two sides are wrapped in the grid to facilitate the necessarily transformations to project properly:
<Rectangle Fill="Orange">
<Rectangle.Projection>
<PlaneProjection x:Name="Rectangle4Projection"
CenterOfRotationZ="-75" RotationX="90"/>
</Rectangle.Projection>
</Rectangle>
<Grid Margin="170,50">
<Grid.Projection>
<PlaneProjection x:Name="Rectangle5Projection"
CenterOfRotationZ="-75" RotationY="-90"/>
</Grid.Projection>
<Rectangle Margin="0" Fill="Yellow" RenderTransformOrigin="0.5,0.5">
<Rectangle.RenderTransform>
<TransformGroup>
<ScaleTransform/>
<SkewTransform/>
<RotateTransform x:Name="Rectangle5Rotation" Angle="0"/>
<TranslateTransform/>
</TransformGroup>
</Rectangle.RenderTransform>
</Rectangle>
</Grid>
With the sides in place, the control itself wires into two events. The first event, the CompositionTarget.Rendering event, fires each time a frame is rendered for the application. This is one way to handle animations by “cheating” on the render loop rather than depending on a dispatcher timer. It also means the cube will move at different rates based on the speed of your machine. That event is used to update the projections for the sides:
private void CompositionTargetRendering(object sender, object e)
{
this.Rectangle1Projection.RotationY += ((this.pt.X - (this.LayoutRoot.ActualWidth / 2)) / this.LayoutRoot.ActualWidth) * 10;
this.Rectangle2Projection.RotationY += ((this.pt.X - (this.LayoutRoot.ActualWidth / 2)) / this.LayoutRoot.ActualWidth) * 10;
this.Rectangle3Projection.RotationY += ((this.pt.X - (this.LayoutRoot.ActualWidth / 2)) / this.LayoutRoot.ActualWidth) * 10;
this.Rectangle4Projection.RotationY += ((this.pt.X - (this.LayoutRoot.ActualWidth / 2)) / this.LayoutRoot.ActualWidth) * 10;
this.Rectangle5Projection.RotationY += ((this.pt.X - (this.LayoutRoot.ActualWidth / 2)) / this.LayoutRoot.ActualWidth) * 10;
this.Rectangle6Projection.RotationY += ((this.pt.X - (this.LayoutRoot.ActualWidth / 2)) / this.LayoutRoot.ActualWidth) * 10;
this.Rectangle1Projection.RotationX += ((this.pt.Y - (this.LayoutRoot.ActualHeight / 2)) / this.LayoutRoot.ActualHeight) * 10;
this.Rectangle2Projection.RotationX += ((this.pt.Y - (this.LayoutRoot.ActualHeight / 2)) / this.LayoutRoot.ActualHeight) * 10;
this.Rectangle3Projection.RotationX += ((this.pt.Y - (this.LayoutRoot.ActualHeight / 2)) / this.LayoutRoot.ActualHeight) * 10;
this.Rectangle4Projection.RotationX += ((this.pt.Y - (this.LayoutRoot.ActualHeight / 2)) / this.LayoutRoot.ActualHeight) * 10;
this.Rectangle5Rotation.Angle -= ((this.pt.Y - (this.LayoutRoot.ActualHeight / 2)) / this.LayoutRoot.ActualHeight) * 10;
this.Rectangle6Rotation.Angle += ((this.pt.Y - (this.LayoutRoot.ActualHeight / 2)) / this.LayoutRoot.ActualHeight) * 10;
}
You’ll notice there is a reference to a pt instance that impacts the offsets applied to the cube. This instance is updated by the PointerMoved event, and used to allow you to change the speed and direction of rotation of the cube by swiping your finger, mouse, or stylus across the cube.
private void LayoutRootPointerMoved(object sender,
Windows.UI.Xaml.Input.PointerRoutedEventArgs e)
{
this.pt = e.GetCurrentPoint(this.LayoutRoot).Position;
}
The plasma effect is also a control. An Image is used to host the plasma background:
<Grid>
<Image x:Name="PlasmaImage" Stretch="Fill"/>
</Grid>
Each frame, a new wave of sine data is computed, mapped into a pixel buffer, and applied to the Image. A virtual size of 320 x 200 is used and then simply expanded. This keeps the computation down but scales well to large screens because the constant movement and color shifts keep you from seeing individual pixels. A sine table is generated for the effect:
private void CreateSineTable()
{
for (var i = 0; i < 512; i++)
{
var rad = (i * 0.703125) * 0.0174532;
this.sine[i] = (int)(Math.Sin(rad) * 1024);
}
}
As well as a color palette:
private void CreatePalette()
{
for (var i = 0; i < 64; ++i)
{
var r = i << 2;
var g = 255 - ((i << 2) + 1);
this.palette[i] = Color.FromArgb(255, (byte)r, (byte)g, 0);
g = (i << 2) + 1;
this.palette[i + 64] = Color.FromArgb(255, 255, (byte)g, 0);
r = 255 - ((i << 2) + 1);
g = 255 - ((i << 2) + 1);
this.palette[i + 128] = Color.FromArgb(255, (byte)r, (byte)g, 0);
g = (i << 2) + 1;
this.palette[i + 192] = Color.FromArgb(255, 0, (byte)g, 0);
}
}
Then some math is applied to generate the effect (I’ll let you inspect the code for that, it’s fairly common although in the old days, optimizing it to run on slow processors was where a lot of the magic happened). One the buffer is set, it is rendered into a WriteableBitmap and set as the source of the Image:
var image = new WriteableBitmap(ScreenWidth, ScreenHeight);
using (var stream = image.PixelBuffer.AsStream())
{
stream.Seek(0, SeekOrigin.Begin);
stream.Write(this.pixelBuffer, 0, this.pixelBuffer.Length);
}
image.Invalidate();
this.plasmaImage = image;
Now there is a plasma effect “control” and a cube control available to drop on the canvas. If I had more hours in the day, I’d optimize this by updating the plasma field in a background thread so the UI thread would simply perform the buffer operation. There may be opportunity to use the parallel features of the TPL as well – just haven’t had time to dig into that. The effect works well as coded even if it does take up a few cycles. The main page simply overlays the cube on the plasma background and then uses a Viewbox to stretch the effects to fill the full screen:
<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
<local:Plasma HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
<Viewbox HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="50">
<local:Cube/>
</Viewbox>
</Grid>
It was a fun little side project. You can download the full source here or sideload with the package here.