在 WPF 中使用 PixelShader 渲染 Mandelbrot 集
本文解释了如何使用像素着色器快速生成 Mandelbrot 集。
引言
本文解释了如何使用像素着色器快速生成 Mandelbrot 和 Julia 集。通过显卡加速,像素值可以实时异步计算。每个单元计算分配给它的像素。不幸的是,一些显卡不支持双精度浮点数(包括我的显卡),所以我们无法获得高倍放大。但速度是一个优势。
要求
- Visual Studio Express 或 Standard 2010 - 用于创建项目
- DirectX SDK (2010 年 6 月) - 用于编译 HLSL - 着色器语言
背景
我从这篇文章中学到了 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:
- 在解决方案资源管理器中选择 mandel.txt。
- 打开“文件”菜单并选择“将 mandel.txt 另存为...”
- 不要更改路径 - 文件将被覆盖。
- 点击“保存并编码...”
- 当出现覆盖消息时,点击“是”。
- 选择“中欧 (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_abs
、c_add
、c_pow
... 是复数库中用于计算绝对值、加法、幂等的函数。如果 z ≤ bailout
的绝对值,则循环结束。在 Mandelbrot 的中心,迭代次数将是无限的,因此有一个 maxIter
变量 - 它抑制无限循环的形成。计算 z
新值的行在数学上表示为:z(n+1) = zna + b
,其中 a
是 power
,b
是 uv
。
现在为了测试,如果 `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。