Tuesday, September 29, 2009

Fractal Rendering on the GPU - Mandelbrot and Julia Sets in an HLSL Shader

Julia Set Shader

I've been playing with HLSL shaders recently, and it occurred to me that the horsepower of the GPU could probably be harnessed to render fractals like the Mandelbrot Set. It turns out to be a perfect task for a shader, since it involves lots of completely localized calculations - exactly what graphics cards are good at.

A bit of web searching turned up the not surprising fact that I'm not the first person to have this realization. I found some nice code examples here and here. My version takes inspiration from both of these approaches, and adds a few tweaks of my own.

The image above is of the Julia set, rendered completely by the GPU. Below is, of course, the Mandelbrot Set.

Mandelbrot Set Shader

And another view of the Mandelbrot set zoomed in a bit:

Mandelbrot Set Shader

On my GeForce 8800GT, I can zoom and pan around at a rock-solid 60 frames per second. Here is a video exploring the Julia set. The warping you see is me transforming the seed used to render the set.



Here is the shader code I ended up with. It is pretty simple. I'm using the Normalized Iteration Count Algorithm to get nicely smoothed coloring. The parameters are set to sensible default values, but you will want to pass them in from your application and map them to some sort of input device. For rendering, simply draw a full screen quad using the shader. In my application I also gild the lily a bit and apply a bloom effect.

int Iterations = 128;
float2 Pan = float2(0.5, 0);
float Zoom = 3;
float Aspect = 1;
float2 JuliaSeed = float2(0.39, -0.2);
float3 ColorScale = float3(4, 5, 6);

float ComputeValue(float2 v, float2 offset)
{
float vxsquare = 0;
float vysquare = 0;

int iteration = 0;
int lastIteration = Iterations;

do
{
vxsquare = v.x * v.x;
vysquare = v.y * v.y;

v = float2(vxsquare - vysquare, v.x * v.y * 2) + offset;

iteration++;

if ((lastIteration == Iterations) && (vxsquare + vysquare) > 4.0)
{
lastIteration = iteration + 1;
}
}
while (iteration < lastIteration);

return (float(iteration) - (log(log(sqrt(vxsquare + vysquare))) / log(2.0))) / float(Iterations);
}

float4 Mandelbrot_PixelShader(float2 texCoord : TEXCOORD0) : COLOR0
{
float2 v = (texCoord - 0.5) * Zoom * float2(1, Aspect) - Pan;

float val = ComputeValue(v, v);

return float4(sin(val * ColorScale.x), sin(val * ColorScale.y), sin(val * ColorScale.z), 1);
}

float4 Julia_PixelShader(float2 texCoord : TEXCOORD0) : COLOR0
{
float2 v = (texCoord - 0.5) * Zoom * float2(1, Aspect) - Pan;

float val = ComputeValue(v, JuliaSeed);

return float4(sin(val * ColorScale.x), sin(val * ColorScale.y), sin(val * ColorScale.z), 1);
}

technique Mandelbrot
{
pass
{
PixelShader = compile ps_3_0 Mandelbrot_PixelShader();
}
}

technique Julia
{
pass
{
PixelShader = compile ps_3_0 Julia_PixelShader();
}
}

Tuesday, September 1, 2009

XNA BloomComponent and Multisampling

The Bloom Postprocess sample provided on the XNA website has issues if you use the component in a game that is using multisampling.

You'll get the following error:

"The active render target and depth stencil surface must have the same pixel size and multisampling type."

The issue is that the render targets created by the component do not respect the multisampling settings of the graphics device. This is easy to fix, however. In the LoadContent() method of BloomComponent.cs, find the code that is initializing renderTarget1 and renderTarget2.

Replace it with the following:

           renderTarget1 = new RenderTarget2D(GraphicsDevice, width, height, 1,
format, GraphicsDevice.DepthStencilBuffer.MultiSampleType, GraphicsDevice.PresentationParameters.MultiSampleQuality);
renderTarget2 = new RenderTarget2D(GraphicsDevice, width, height, 1,
format, GraphicsDevice.DepthStencilBuffer.MultiSampleType, GraphicsDevice.PresentationParameters.MultiSampleQuality);