65.9K
CodeProject 正在变化。 阅读更多。
Home

在 WPF 中使用 PixelShader 渲染 Mandelbrot 集

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (42投票s)

2011年7月15日

CPOL

7分钟阅读

viewsIcon

49625

downloadIcon

2927

本文解释了如何使用像素着色器快速生成 Mandelbrot 集。

引言

本文解释了如何使用像素着色器快速生成 Mandelbrot 和 Julia 集。通过显卡加速,像素值可以实时异步计算。每个单元计算分配给它的像素。不幸的是,一些显卡不支持双精度浮点数(包括我的显卡),所以我们无法获得高倍放大。但速度是一个优势。

要求

背景

我从这篇文章中学到了 HLSL。它解释了如何使用像素着色器操纵亮度和对比度。如果你想了解更多关于 Mandelbrot 集的信息,请查看维基百科

编写着色器文件

让我们在项目中添加一个文本文件并将其命名为“mandel.txt”。它将是 HLSL 中着色器的源代码。现在我们来编写着色器的框架。

sampler2D input : register(s0);
// Image to be processed, loaded from Sampler Register 0.

float4 main(float2 uv : TEXCOORD) : COLOR
// uv are the coordinates of the pixel to be processed
{
    float4 color = tex2D(input, uv); // Getting pixel uv from input.
    return color; // Returning new color of processed pixel.
}
// As you see HLSL is similar to C.

着色器通过 `Effect` 属性应用于 WPF 元素。WPF 元素的渲染图像被传递给着色器包装器,包装器将编译后的着色器程序发送到显卡。包装器还将数据(以及渲染的元素)发送到显卡中指定的寄存器。在 WPF 中,寄存器由包装器分配,并且可以由着色器读取。如果您想更全面地了解寄存器,请查阅维基百科

input 是从采样器寄存器 0 加载的图像。将来,包装器(WPF 会自动完成)会将渲染的元素(例如 Grid)放置在此寄存器中。Mandelbrot 不会转换图像,它不是从其像素生成的,因此我们可以在 Mandelbrot 着色器中删除输入。

让我们回到项目。我们必须编译着色器文件。为此,请转到项目属性和生成事件部分。

现在我们添加预构建事件命令

"C:\Program Files\Microsoft DirectX SDK (June 2010)\Utilities\bin\x86\fxc" 
    /T ps_3_0 /E main /Fo"$(ProjectDir)mandel.ps" "$(ProjectDir)mandel.txt"

这将编译我们的着色器。现在让我们运行项目。

编译过程中可能会出现错误。这可能是着色器文件编码问题。FXC 编译器只支持 ANSI 代码页,而 Visual Studio 默认创建 UTF-8 编码的文件。要在 Visual Studio 中将 UTF-8 更改为 ANSI:

  1. 在解决方案资源管理器中选择 mandel.txt
  2. 打开“文件”菜单并选择“将 mandel.txt 另存为...
  3. 不要更改路径 - 文件将被覆盖。
  4. 点击“保存并编码...
  5. 当出现覆盖消息时,点击“是”。
  6. 选择“中欧 (Windows) - 代码页 1250”并点击“确定”。

如果一切顺利,您应该看到一个空白的 `MainWindow` 窗口。目前,我们不会在项目中使用着色器。让我们将编译后的着色器文件添加到项目中。编译后的程序应该在项目目录中,名为“mandel.ps”。现在该文件将在每次重新构建时被覆盖。让我们实现一个简单的 Mandelbrot。为此,我们必须有一个复数库。我已经创建了它,您可以从本文顶部下载。下载后,解压文件并将“complex.txt”添加到您的 Mandelbrot 项目中。在 mandel.txt 中 `main` 之前添加以下行:

#include "complex.txt"

现在我们可以轻松地实现 Mandelbrot。删除所有内部的 `main`。然后让我们定义变量:

float2 z = float2(uv.x, uv.y); // Complex number z
float i = 0; // Iterations
float maxIter = 32; // Maximal iterations
float2 power = float2(2, 0); // Exponent. For standard Mandelbrot it is (2, 0).
float bailout = 4; // Bailout

现在编写循环

while (i < maxIter && c_abs(z) <= bailout) // While loop
{
    z = c_add(c_pow(z, power), uv); // Recalculating z
    i++; // Iterations + 1
}

迭代次数(i)将由颜色表示。函数 c_absc_addc_pow... 是复数库中用于计算绝对值、加法、幂等的函数。如果 z ≤ bailout 的绝对值,则循环结束。在 Mandelbrot 的中心,迭代次数将是无限的,因此有一个 maxIter 变量 - 它抑制无限循环的形成。计算 z 新值的行在数学上表示为:z(n+1) = zna + b,其中 apowerbuv

现在为了测试,如果 `i == maxIter` 则返回红色,否则返回黑色。将来,这将用复杂的调色板渲染。

if (i == maxIter)
    return float4(1.0, 0.0, 0.0, 1.0); //float4(red, green, blue, alpha)
else
    return float4(0.0, 0.0, 0.0, 1.0);

现在你的代码应该像这样

#include "complex.txt"

float4 main(float2 uv : TEXCOORD) : COLOR
{
    float2 z = float2(uv.x, uv.y);
    float i = 0;
    float maxIter = 32;
    float2 power = float2(2, 0);
    float bailout = 4;

    while (i < maxIter && c_abs(z) <= bailout)
    {
        z = c_add(c_pow(z, power), uv);
        i++;
    }

    if (i == maxIter)
        return float4(1.0, 0.0, 0.0, 1.0);
    else
        return float4(0.0, 0.0, 0.0, 1.0);
}

您可以重新编译您的项目。一切都应该没问题。

着色器包装器

我们编译好的着色器应该添加到项目中。如果我们要让包装器能够加载着色器,我们必须将着色器文件的“生成操作”设置为“资源”。

现在向项目中添加一个新类,并将其命名为“MandelbrotEffect”。这将是我们的包装器。向类中添加以下 using 语句:

using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Effects;

我们的类必须继承自 ShaderEffect。让我们加载着色器:

PixelShader m_shader = new PixelShader() { UriSource = 
            new Uri(@"mandel.ps", UriKind.RelativeOrAbsolute) };

此行必须添加到 `MandelbrotEffect` 类中。现在添加构造函数:

public MandelbrotEffect()
{
    PixelShader = m_shader;
}

构造函数将着色器源设置为 `m_shader`。我们不必实现 `input`,因为我们在着色器中没有使用它。如果您将来想创建一个需要 `input` 的效果,请参阅此处

现在您可以重新编译您的项目。

使用着色器

现在让我们测试我们的着色器。转到 MainPage.xaml。它应该看起来像这样:

<Window x:Class="ShaderMandelbrot.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        <!-- HERE -->

        Title="MainWindow" Height="350" Width="525">

在“HERE”注释处添加此行

xmlns:my="clr-namespace:ShaderMandelbrot"

并将 `ShaderMandelbrot` 替换为包含您的 `MandelbrotEffect` 的命名空间。现在将 `Grid` 的背景设置为任何可见的颜色(例如 `Red`),因为只有可见的像素会被像素着色器转换。所以让我们将 `MandelbrotEffect` 添加到 `Grid` 中:

<Grid.Effect>
    <my:MandelbrotEffect />
</Grid.Effect>

现在重新构建项目并运行!

添加参数

要更改最大迭代次数、幂或跳出值,我们必须修改像素着色器文件。让我们将这些更改为参数。我们将修改像素着色器。首先,从 `main` 中删除以下行:

float maxIter = 32;
float2 power = float2(2, 0);
float bailout = 4;

其次,在文件的顶部、input 之后添加这些行:

float maxIter : register(c0);
float2 power : register(c1);
float bailout : register(c2);

现在参数是从像素着色器常量寄存器加载的。

第三,在包装器中实现这些参数

public static readonly DependencyProperty MaxIterProperty = 
  DependencyProperty.Register("MaxIter", typeof(double), typeof(MandelbrotEffect), 
  new UIPropertyMetadata(32.0, PixelShaderConstantCallback(0))); // register(c0)
public double MaxIter
{
    get { return (double)GetValue(MaxIterProperty); }
    set { SetValue(MaxIterProperty, value); }
}

public static readonly DependencyProperty PowerProperty = 
  DependencyProperty.Register("Power", typeof(Point), 
  typeof(MandelbrotEffect), new UIPropertyMetadata(new Point(2, 0), 
  PixelShaderConstantCallback(1))); // register(c1)
public Point Power
{
    get { return (Point)GetValue(PowerProperty); }
    set { SetValue(PowerProperty, value); }
}

public static readonly DependencyProperty BailoutProperty = 
  DependencyProperty.Register("Bailout", typeof(double), 
  typeof(MandelbrotEffect), 
  new UIPropertyMetadata(4.0, PixelShaderConstantCallback(2))); // register(c2)
public double Bailout
{
    get { return (double)GetValue(BailoutProperty); }
    set { SetValue(BailoutProperty, value); }
}

第四,更新着色器值 - 将这些行添加到 `MandelbrotEffect` 构造函数中

UpdateShaderValue(MaxIterProperty);
UpdateShaderValue(PowerProperty);
UpdateShaderValue(BailoutProperty);

完成。现在重新构建项目,您可以在 XAML 编辑器中设置参数。

<my:MandelbrotEffect Power="6,0" />

这创建了一个 Mandelbrot 的变体。

偏移和大小

目前,我们只能看到分形的一部分。让我们重新缩放它。将 `offset` 和 `size` 参数添加到着色器中:

float2 offset : register(c3);
float2 size : register(c4);

在包装器中实现参数

public static readonly DependencyProperty OffsetProperty = 
  DependencyProperty.Register("Offset", typeof(Point), 
  typeof(MandelbrotEffect), new UIPropertyMetadata(new Point(-3, -2), 
  PixelShaderConstantCallback(3)));
public Point Offset
{
    get { return (Point)GetValue(OffsetProperty); }
    set { SetValue(OffsetProperty, value); }
}

public static readonly DependencyProperty SizeProperty = 
  DependencyProperty.Register("Size", typeof(Point), 
  typeof(MandelbrotEffect), new UIPropertyMetadata(new Point(0.25, 0.25), 
  PixelShaderConstantCallback(4)));
public Point Size
{
    get { return (Point)GetValue(SizeProperty); }
    set { SetValue(SizeProperty, value); }
}

并更新着色器值

UpdateShaderValue(OffsetProperty);
UpdateShaderValue(SizeProperty);

现在我们必须在着色器中重新缩放 `uv`

float2 xy = float2(uv.x / size.x + offset.x, uv.y / size.y + offset.y);

将着色器中的所有 `uv` 更改为 `xy` 并运行。

调色板

我们的 Mandelbrot 集颜色并不丰富。为了获得漂亮的颜色,我们必须创建一个调色板。这是一个简单但效果非常好的调色板代码:

float4 getColor(float i)
{
    float k = 1.0 / 3.0;
    float k2 = 2.0 / 3.0;
    float cr = 0.0;
    float cg = 0.0;
    float cb = 0.0;
    if (i >= k2)
    {
        cr = i - k2;
        cg = (k-1) - cr;
    }
    else if (i >= k)
    {
        cg = i - k;
        cb = (k-1) - cg;
    }
    else
    {
        cb = i;
    }
    return float4(cr * 3, cg * 3, cb * 3, 1.0);
}

将其粘贴到着色器中的“include line”之后。现在替换此行:

if (i == maxIter)
    return float4(1.0, 0.0, 0.0, 1.0);
else
    return float4(0.0, 0.0, 0.0, 1.0);

用这个:

if (i < maxIter)
    return getColor(i / maxIter);
else
    return float4(0.0, 0.0, 0.0, 1.0);

这是结果。

连续(平滑)着色

归一化迭代计数算法可以消除难看的颜色阈值。我们可以很容易地实现它,如下所示。替换这个:

return getColor(i / maxIter);

用这个:

{
    i -= log(log(c_abs(z))) / log(c_abs(power));
    return getColor(i / maxIter);
}

就是这样!我们的 Mandelbrot 着色器文件已完成。这就是效果:

Mandelbrot 截图

Julia

Julia 公式与 Mandelbrot 类似。只有一个变化。这是 Julia 的公式:z(n+1) = zna + b,其中 a 是幂,而 b 不是 uv ——在 Julia 中,它是种子。最简单的选择方法是从 Mandelbrot 切换——在示例应用程序中,我就是这样做的。

这是 Julia 着色器的代码

float maxIter : register(c0);
float2 power : register(c1);
float bailout : register(c2);
float2 offset : register(c3);
float2 size : register(c4);
float2 seed : register(c5);

#include "complex.txt"

float4 getColor(float i)
{
    float k = 1.0 / 3.0;
    float k2 = 2.0 / 3.0;
    float cr = 0.0;
    float cg = 0.0;
    float cb = 0.0;
    if (i >= k2)
    {
        cr = i - k2;
        cg = (k-1) - cr;
    }
    else if (i >= k)
    {
        cg = i - k;
        cb = (k-1) - cg;
    }
    else
    {
        cb = i;
    }
    return float4(cr * 3, cg * 3, cb * 3, 1.0);
}

float4 main(float2 uv : TEXCOORD) : COLOR
{
    float2 xy = float2(uv.x / size.x + offset.x, uv.y / size.y + offset.y);
    float2 z = float2(xy.x, xy.y);
    float i = 0;

    while (i < maxIter && c_abs(z) <= bailout)
    {
        z = c_add(c_pow(z, power), seed);
        i++;
    }
    if (i < maxIter)
    {
        i -= log(log(c_abs(z))) / log(c_abs(power));
        return getColor(i / maxIter);
    }
    else
        return float4(0.0, 0.0, 0.0, 1.0);
}

您必须自行实现包装器或下载源代码。请记住:您必须将编译行添加到预构建命令中。

Julia 截图

其他功能

在示例应用程序(及其源代码)中,我在 `MainWindow` 中应用了一些其他功能。这些功能包括用鼠标移动、缩放分形,切换到 Julia,以及用于调节参数的滑块。

下一步?

您可以尝试自己实现其他分形。

结论

Mandelbrot 集用复数实现非常简单。Julia 的公式与 Mandelbrot 的公式类似。使用像素着色器,我们可以获得快速分形,但由于存在双精度数的问题,我们无法获得高倍放大。像素着色器是快速图像处理的好解决方案,而使用 WPF 和 HLSL,这非常容易。

历史

  • 2011-07-19 - 修正,添加 Julia。
© . All rights reserved.