Saturday, May 30, 2009

WPF Black Holes


My journey into WPF is still in its infancy, but I want to document some of the ups and downs I've discovered working with the framework, ending in a big UP!

I've been googling like mad and absorbing knowledge as I ramp up the WPF learning curve. Some of my early research left me feeling pessimistic about the prospects of writing a game using WPF. First, there was the message group post (I cannot locate the link anymore, but I promise this was for real) where the poor programmer was experiencing poor render performance drawing lines in WPF (more on that later!) The suggestion he finally adopted was to run a WinForms Picture control hosted in his WPF application! Astonishingly, the GDI+ based WinForms outperformed WPF in the line drawing heavy application!

Second was the promosing blog entitled "WPF for Games". The author was literally gushing about WPF in his early blog entries. By the end of the blog, this grim topic appeared - "Throwing in the Towel", where the author is fed up fighting against the WPF framework and decides to write his game in XNA.

I love a good challenge, and this just makes me want to try harder to get a decent game out of WPF ;)

My first concrete project in WPF is a port of my TurboSprite sprite engine. I have debated internally about whether to port TurboSprite to WPF, or to try and use WPF's framework as the basis for my sprite animation. I concluded that porting TurboSprite would save me alot of headaches in the long run. My feeling is that my lightweight sprite engine will allow me to realize the graphics performance I want without pushing this burden onto the WPF framework. My sprites descend from Object - and there's nothing more lightweight than that! I will use the WPF framework to try and make a cool user interface surrounding the main SpriteSurface that contains the game's action.

But how to translate TurboSprite to WPF? The main component of TurboSprite is the SpriteSurface, and for this I derive from FrameworkElement, and drawing in an overridden OnRender method, using the supplied DrawingContext. In my next blog post I plan to go into some of the early technical details of my implementation, but for now I can describe some of the early results.

I've found that rendering images in the DrawingContext using DrawImage is blazingly fast, and perforing rotations using Transforms seems to not impact rendering performance. I was able to create many very large ImageSprites and add them to the SpriteSurface without and degredation in FPS. This is good news for Scenarios that like to use many large background graphics!

Next I tried to tackle porting beat2k's SpiralSprite, which is the basis for black holes in the SV graphics. The code ported easily enough, but when I created a SpiralSprite in my new WPF TurboSprite, the animation rate went down the toilet! Adding 2 or 3 SpiralSprites caused a complete bottleneck and the FPS was down in the single digit range.

But I was determined to find a better solution than hosting a WinForms control for my SpriteSurface!

After some research I discovered that creating a Geometry was the way to go. Apparently, the Geometry can be rendered with graphics acceleration all in one shot, while rendering the individual lines was very time consuming. The problem with this approach is that it sacrificed the ability to color each segment of the spiral, an effect that beat2k implemented in his code. I solved the problem by rendering the SpiralSprite using a RadialGradientBrush, with the end color set to Transparent.

The end result is a new SpiralSprite class that still has the customizability, the rotation (now leveraging the existing Sprite Spin and SpinSpeed properties), and that can be rendered extremely quickly on the SpriteSurface.

The code for the modified SpiralSprite


//SpiralSprite class by Jason Milliser

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;

namespace SCG.TurboSprite
{
public class SpiralSprite : Sprite
{
//constructor
public SpiralSprite(int arms, float radiusmin, int radiusmax, int armslope, int slopelength, bool isSpinningClockwise, Color CenterColor)
{
//arm slope is the number of lines drawn to the center of the spiral. Also controls the tightness of teh spiral.
//slopelength is the length of each line segment of each spiral. longer lines can tighten spiral, but quality will go down.
slength = slopelength;
_centerColor = CenterColor;
_direction = isSpinningClockwise;
_radiusMax = radiusmax;
_radiusMin = radiusmin;
_armSlope = armslope;
_arms = arms;
Shape = new Rect(0-_radiusMax, 0-_radiusMax, _radiusMax * 2, _radiusMax * 2);
}

//calculate points of spiral
protected internal override void Process()
{
//we now need to create points only once, rotation performed by transformed
if (_points != null)
return;

int theta = 0;
float sinvalue = 0;
float cosvalue = 0;
double altitude = 0;
float faltitude = 0;
double radiusscale = 0;
double dradiusmin = 0;
int rdegrees = 0;

//preliminary math calculations
theta = 360 / _arms;

if (_points == null)
_points = new Point[_arms * _armSlope + 1];

for (int arm = 0; arm < _arms; arm++)
{
for (int seg = 0; seg < _armSlope; seg++)
{
if (_direction == true)
rdegrees = (seg * slength) + (arm * theta);
else
rdegrees = -(seg * slength) - (arm * theta);
while (rdegrees < 0) rdegrees = rdegrees + 360;
while (rdegrees >= 360) rdegrees = rdegrees - 360;

radiusscale = _radiusMax - _radiusMin;
dradiusmin = _armSlope;
altitude = (seg + 1) / dradiusmin;
altitude = radiusscale * altitude;
dradiusmin = _radiusMin;
altitude = dradiusmin + altitude;
faltitude = Convert.ToSingle(altitude);

sinvalue = LookupTables.Sin(rdegrees) * faltitude;
cosvalue = LookupTables.Cos(rdegrees) * faltitude;

_points[(arm * _armSlope) + seg].X = sinvalue;
_points[(arm * _armSlope) + seg].Y = cosvalue;
}
}
}

//Render the sprite
protected internal override void Render(DrawingContext dc)
{
if (_points != null)
{
//Create the Geometry if it doesn't yet exist
//this needs to be done on the same thread, so it's done here and not in Process
if (_geom == null)
{
_geom = new StreamGeometry();
StreamGeometryContext sgc = _geom.Open();
sgc.BeginFigure(_points[0], false, false);
List points = new List();
for (int i = 1; i < _points.Length; i++)
points.Add(_points[i]);
sgc.PolyLineTo(points, true, true);
sgc.Close();
}

//Transform the location to match the sprite's position
Transform t = new TranslateTransform(X - Surface.OffsetX, Y - Surface.OffsetY);
dc.PushTransform(t);

//Rotate sprite
Transform rotate = new RotateTransform(FacingAngle);
dc.PushTransform(rotate);

//Create gradient brush so spiral fades out
RadialGradientBrush rgb = new RadialGradientBrush(_centerColor, Colors.Transparent);

//Draw the spiral geometry
dc.DrawGeometry(null, new Pen(rgb, 2), _geom);

//Draw the central back hole
dc.DrawEllipse(Brushes.Black, null, new Point(0, 0), 7, 7);
dc.Pop();
dc.Pop();
}
}

//private members
private Point[] _points;
private int _arms;
private float _radiusMax;
private float _radiusMin;
private int _armSlope;
private Color _centerColor;
private bool _direction;
private int slength;
private StreamGeometry _geom = null;
private Pen _pen = new Pen(Brushes.Yellow, 1);
}
}

No comments:

Post a Comment