分形

分形(Fractal)是一个有趣的数学现象,即存在这样一种图形,它的局部与全体具有一定的相似性。也就是说,对于一个具有分形性质的图形,它可以无穷放大而具有相似的图形模式,

如果有这么一样东西,不管你怎么放大它,看到的都是相似图案的循环,在放大10000倍的一个角落里,居然出现了和整个物体相同的花纹,这是多么美妙的图案!

Mandelbrot 集就是一个经典的分形图案,它由一个简单的多项式迭代生成:

\[z_{i+1}=z_i^2+c\]

这其中 $z,c$ 是复数,$z_0=0$,序列的值或者延伸到无限大,或者只停留在有限半径的圆盘内。Mandelbrot 集合就是使以上序列不延伸至无限大的所有 $c$ 的集合。

还有另一个经典的分形使用与上面一样的迭代公式和方法——Julia 集,不过在 Julia 集中,$c$ 是一个常复数,Julia 集合是使以上序列不延伸至无限大的所有 $z_0$ 的集合。

Mandelbrot 集合
Julia 集合

简单程序实现

由于这两种分形集合使用简单的公式和迭代方法,尤其适合程序实现。

通过设定图像宽度对应的 $x$ 轴区间长度和图像中心点对应的复平面位置,我们可以获得图片像素到复平面上的值的映射。

设图片宽 $w$,高 $h$,中心位置 $(x_c,y_c)$,宽度对应的区间长度 $l$,如图示:

映射示意

容易得到

\[\begin{gather} l_x=l\quad l_y=\frac {hl}w\\ x_l=x_c-\frac l2\quad x_h=x_c+\frac l2\\ y_l=y_c-\frac {hl}{2w}\quad y_h=y_c+\frac {hl}{2w}\\ x(i)=x_l+\frac{l_x}{w}i\\ y(j)=y_l+\frac{l_y}{h}j \end{gather}\]

这样一来我们就完成了图片上的像素到复平面上的值的映射。

在程序中,我们无法判断一个序列是否要发散到无穷大。所以,退而求其次,我们用 $|z|<2$ 作为迭代的继续条件。

通过对在不同迭代深度终止的点填充不同的颜色,我们就可以得到类似上面展示的美丽图片。

下面的程序是我大一练习 C 语言的时候写的,我把它捡出来并简单加了一些注释。这个程序实现使用CPU计算上面两个集合的图像并输出到BMP文件。(关于输出到BMP文件,可以参考 用C++画图(一):图片文件

#include<stdio.h>

//复数
struct complex
{
    double Real;
    double Imag;
};

//颜色
struct BGR
{
    unsigned char B;
    unsigned char G;
    unsigned char R;
};

int Width, Height, max_Iterations, mode;
double X_max, X_min, Y_max, Y_min, Delta, xC, yC;
struct BGR IMG[2000 * 3000], Color[257];

//生成颜色序列
void Build_color_array()
{
    int i;
    for (i = 1;i <= 256;i++)
    {
        Color[(i) % 256 + 1].B = i % 256 / 64 == 0 ? 3 * i : i % 256 / 64 == 1 ? 128 + i : i % 256 / 64 == 2 ? -4 * i - 1 : 0;
        Color[(i) % 256 + 1].G = i % 256 / 32 == 0 ? i : i % 256 / 32 == 1 || i % 256 / 32 == 2 ? -64 + 3 * i : i % 256 / 32 == 3 ? 128 + i : i % 256 / 32 == 4 ? 383 - i : i % 256 / 32 == 5 || i % 256 / 32 == 6 ? 703 - 3 * i : 255 - i;
        Color[(i) % 256 + 1].R = i % 256 / 64 == 0 ? i : i % 256 / 64 == 1 ? -128 + 3 * i : i % 256 / 64 == 2 ? 383 - i : 768 - 3 * i;
    }
    Color[0].B = Color[0].G = Color[0].R = 0;
}

struct complex plus(struct complex A, struct complex B)
{
    struct complex C;
    C.Real = A.Real + B.Real;
    C.Imag = A.Imag + B.Imag;
    return C;
}

struct complex multi(struct complex A, struct complex B)
{
    struct complex C;
    C.Real = A.Real * B.Real - A.Imag * B.Imag;
    C.Imag = A.Real * B.Imag + A.Imag * B.Real;
    return C;
}

struct complex power(struct complex A, int n)
{
    struct complex C = A;
    int i;
    for (i = 2; i <= n; i++)
        C = multi(C, A);
    return C;
}

//迭代函数
struct complex f(struct complex Z, struct complex C)
{
    return plus(power(Z, 2), C);
}

//绘制图片
void draw_img()
{
    struct complex C, Z;
    double Delta_X = (X_max - X_min) / Width;
    double Delta_Y = (Y_max - Y_min) / Height;

    double a, b;
    if (!mode)
    {
        printf("Julia Pos: ");
        scanf("%lf%lf", &a, &b);
    }

    int r, c, i, s = 0;
    system("cls");
    printf("Processing ");
    for (i = 1; i <= 100; i++) putchar(' ');
    printf("|%2d%%", s);
    for (r = 1; r <= Height; r++)
    {
        for (c = 1; c <= Width; c++)
        {
            if (mode)
            {
                C.Real = X_min + c * Delta_X;
                C.Imag = Y_min + r * Delta_Y;
                Z.Real = Z.Imag = 0;
            }
            else
            {
                C.Real = a;
                C.Imag = b;
                Z.Real = X_min + c * Delta_X;
                Z.Imag = Y_min + r * Delta_Y;
            }
            for (i = 1; i <= max_Iterations; i++)
            {
                Z = f(Z, C);
                if (Z.Real * Z.Real + Z.Imag * Z.Imag > 4.0)
                {
                    IMG[(r - 1) * Width + c - 1] = Color[i % 256 + 1];
                    break;
                }
            }
        }
        if (r == (int)((Height / 100.0) * (int)(r / (Height / 100.0))) + 1)
        {
            s++;
            system("cls");
            printf("Processing ");
            for (i = 1;i <= s;i++) putchar('.');
            for (i = s + 1;i <= 100;i++) putchar(' ');
            printf("|%2d%%", s);
        }
    }
}

//输出到BMP
void Save_to_Bmp(const char* filename)
{
    unsigned int size = Width * Height * 3 + 54;
    unsigned short head[] = {
        0x4D42,size % 0x10000,size / 0x10000,0,0,0x36,0,0x28,
        0,Width % 0x10000,Width / 0x10000,Height % 0x10000,Height / 0x10000,0x10,0x18,0,
        0,0,0,0,0,0,0,0,0,0,0
    };
    printf("\nExporting...\n");
    FILE* fp = fopen(filename, "wb");
    if (!fp)
    {
        printf("ERROR: cannot open file.\n");
        return;
    }
    fwrite(head, 1, sizeof(head), fp);
    fwrite(IMG, 1, size, fp);
    fclose(fp);
}

main()
{
    printf("Fractal\n");

    printf("Select Mode (1 for Mandelbrot; 0 for Julia): ");
    scanf("%d", &mode);

    printf("Size: ");
    scanf("%d %d", &Width, &Height);

    printf("Zone Width: ");
    scanf("%lf", &Delta);

    printf("Center: ");
    scanf("%lf%lf", &xC, &yC);

    printf("Max Iteration: ");
    scanf("%d", &max_Iterations);

    X_max = xC + Delta / 2;
    X_min = xC - Delta / 2;
    Y_max = yC + Delta / 2 * Height / Width;
    Y_min = yC - Delta / 2 * Height / Width;

    Build_color_array();
    draw_img();
    Save_to_Bmp("../data/Picture.bmp");
    printf("Done.\n");
    getchar();
    while (getchar() != 10);
}

并行计算

如果你编译并运行了上面的代码,你会发现,运行所消耗的时间非常漫长,尽管生成的美丽图片使得它不那么难以忍受。

如何加快运行的速度呢?不难发现,在上面两种分形集合的生成中,像素间是两两独立的,他们不使用相同的计算资源也不存在逻辑上的前后关系。也就是说,将两个像素的计算任务交给不同的计算单元同时运算,结果和简单的让一个计算单元依次计算并不会有什么不同。显然,如果能同时使用多个计算单元来计算,速度将大大提高,这也就是并行计算的思想。

现代计算机的CPU通常拥有多于一个的核心(比如我的笔记本使用的 i5-8250U 就拥有四个核心),如果能将其全部用上,就能提高运行的速度。但是,计算机上还有一个模块拥有更多的计算单元——GPU。

GPU指图形处理单元,也就是显卡。在计算机图形的渲染中,对每个像素的着色也是独立进行的,具有高度的并行性。作为图形渲染的专用硬件,GPU理所应当的具有强大的并行处理能力。事实上,GPU便是由成百上千的小计算单元组成。

现代的GPU通常拥有可编程计算的能力,可以用来计算用户指定的任务,我们可以使用它的并行计算能力来加速我们的计算。

并行程序实现

CUDA(Compute Unified Device Architecture),是显卡厂商 NVIDIA 推出的运算平台。我们使用它来对分形问题进行并行加速。

此外,为了使用 GUI 和鼠标交互,我使用 OpenGL 来生成窗口并渲染计算生成的图片。

有关资料:

LearnOpenGL CN

CUDA编程入门极简教程 - 知乎

CUDA与OpenGL交互开发_ruby97的专栏-CSDN博客

CUDA 部分:

#include "kernel.h"
#include "complex.h"

__global__ void MandelbrotKernel(uchar4* img, uchar3* palette, int2 size, double4 zone, int max_Iterations)
{
    int index = blockIdx.x * blockDim.x + threadIdx.x;
    if (index >= size.x * size.y) return;
    int xIdx = index % size.x;
    int yIdx = index / size.x;
    double x = zone.x * (size.x - xIdx) / size.x + zone.z * xIdx / size.x;
    double y = zone.y * (size.y - yIdx) / size.y + zone.w * yIdx / size.y;
    double2 z = { 0, 0 };
    double2 c = { x, y };
    for (int i = 0; i < max_Iterations; i++)
    {
        z = complexPlus(complexSquare(z), c);
        if (complexLength2(z) > 4.0)
        {
            img[index].x = palette[i % 256].x;
            img[index].y = palette[i % 256].y;
            img[index].z = palette[i % 256].z;
            img[index].w = 255;
            return;
        }
    }
    img[index].x = 0;
    img[index].y = 0;
    img[index].z = 0;
    img[index].w = 255;
}

__global__ void JuliaKernel(uchar4* img, uchar3* palette, int2 size, double2 con, double4 zone, int max_Iterations)
{
    int index = blockIdx.x * blockDim.x + threadIdx.x;
    if (index >= size.x * size.y) return;
    int xIdx = index % size.x;
    int yIdx = index / size.x;
    double x = zone.x * (size.x - xIdx) / size.x + zone.z * xIdx / size.x;
    double y = zone.y * (size.y - yIdx) / size.y + zone.w * yIdx / size.y;
    double2 z = { x, y };
    double2 c = { con.x, con.y };
    for (int i = 0; i < max_Iterations; i++)
    {
        z = complexPlus(complexSquare(z), c);
        if (complexLength2(z) > 4.0)
        {
            img[index].x = palette[i % 256].x;
            img[index].y = palette[i % 256].y;
            img[index].z = palette[i % 256].z;
            img[index].w = 255;
            return;
        }
    }
    img[index].x = 0;
    img[index].y = 0;
    img[index].z = 0;
    img[index].w = 255;
}


void Mandelbrot(uchar4* img, uchar3* palette, int2 size, double4 zone, int max_Iterations)
{
    MandelbrotKernel << <ceil(size.x * size.y / 1024), 1024 >> > (img, palette, size, zone, max_Iterations);
}

void Julia(uchar4* img, uchar3* palette, int2 size, double2 con, double4 zone, int max_Iterations)
{
    JuliaKernel << <ceil(size.x * size.y / 1024), 1024 >> > (img, palette, size, con, zone, max_Iterations);
}

C++ 部分:

#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <iostream>
#include <vector>

#include "cuda_runtime.h"
#include <cuda_gl_interop.h>  
#include "device_launch_parameters.h"
#include "kernel.h"

int2 size = { 1920, 1080 };                                     //窗口(图像)尺寸
double diagonal = sqrt(size.x * size.x + size.y * size.y);      //对角线

typedef struct
{
    double zoom;
    double xC;
    double yC;
} Zone;

std::vector<Zone> zones;                                        //可视区域栈
bool mode;                                                      //模式
int max_Iterations;                                             //最大迭代数
double2 JuliaPos;                                               //Julia集位置参数

uchar3 palette[256];                                            //调色板
uchar3* palette_d;                                              //GPU端调色板数据指针

GLuint PBO;                                                     //OpenGL像素缓冲对象
cudaGraphicsResource* resource;                                 //CUDA图形资源对象

bool mousePressed = false;                                      //鼠标状态flag
double2 start, end;                                             //鼠标控制点位置

void refreshImage(GLFWwindow* window);

//窗口大小改变回调
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    //更新数据
    size.x = width;
    size.y = height;
    diagonal = sqrt(width * width + height * height);

    //更新视口和像素缓冲对象
    glViewport(0, 0, width, height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0, size.x, size.y, 0, -1, 1);
    glBufferData(GL_PIXEL_UNPACK_BUFFER, size.x * size.y * 4, NULL, GL_DYNAMIC_DRAW);

    refreshImage(window);
}

//鼠标点击回调
void mouse_button_callback(GLFWwindow* window, int button, int action, int mods)
{
    if (button == GLFW_MOUSE_BUTTON_LEFT)
    {
        if (action == GLFW_PRESS && !mousePressed)
        {
            //左键按下:初始化控制点
            mousePressed = true;
            glfwGetCursorPos(window, &start.x, &start.y);
            end = start;
        }
        else if (action == GLFW_RELEASE && mousePressed)
        {
            //左键抬起:计算新视口,刷新
            Zone temp = zones.back();
            mousePressed = false;
            double xC = temp.xC + (start.x - size.x / 2.0) / size.x * temp.zoom;
            double yC = temp.yC - (start.y - size.y / 2.0) / size.x * temp.zoom;
            double zoom = temp.zoom * sqrt(pow(start.x - end.x, 2) + pow(start.y - end.y, 2)) * 2 / diagonal;
            zones.push_back({ zoom, xC, yC });
            refreshImage(window);
        }
    }
    if (button == GLFW_MOUSE_BUTTON_RIGHT && action == GLFW_RELEASE)
    {
        if (!mousePressed && zones.size() > 1)
        {
            //右键:回退视口
            zones.pop_back();
            refreshImage(window);
        }
        else
        {
            //右键:取消控制
            mousePressed = false;
            refreshImage(window);
        }
    }
}

//鼠标移动回调
static void cursor_position_callback(GLFWwindow* window, double xpos, double ypos)
{
    if (mousePressed)
    {
        //更新控制点
        end.x = xpos;
        end.y = ypos;

        //计算示意框
        double len = sqrt(pow(start.x - end.x, 2) + pow(start.y - end.y, 2));
        double x = len * size.x / diagonal;
        double y = len * size.y / diagonal;

        //绘制示意框
        glDrawPixels(size.x, size.y, GL_RGBA, GL_UNSIGNED_BYTE, 0);
        glBegin(GL_LINE_LOOP);
        glColor4f(1, 0, 0, 1);
        glVertex2d(start.x + x, start.y + y);
        glVertex2d(start.x + x, start.y - y);
        glVertex2d(start.x - x, start.y - y);
        glVertex2d(start.x - x, start.y + y);
        glEnd();
        glFlush();
        glfwSwapBuffers(window);
    }
}

//ESC退出
void processInput(GLFWwindow* window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

//初始化调色板
void initPalette()
{
    for (int i = 0; i < 256; i++)
    {
        palette[i % 256].x = i % 256 / 64 == 0 ? i : i % 256 / 64 == 1 ? -128 + 3 * i : i % 256 / 64 == 2 ? 383 - i : 768 - 3 * i;
        palette[i % 256].y = i % 256 / 32 == 0 ? i : i % 256 / 32 == 1 || i % 256 / 32 == 2 ? -64 + 3 * i : i % 256 / 32 == 3 ? 128 + i : i % 256 / 32 == 4 ? 383 - i : i % 256 / 32 == 5 || i % 256 / 32 == 6 ? 703 - 3 * i : 255 - i;
        palette[i % 256].z = i % 256 / 64 == 0 ? 3 * i : i % 256 / 64 == 1 ? 128 + i : i % 256 / 64 == 2 ? -4 * i - 1 : 0;
    }
    cudaMalloc((void**)&palette_d, 256 * sizeof(uchar3));
    cudaMemcpy(palette_d, palette, 256 * sizeof(uchar3), cudaMemcpyHostToDevice);
}

//刷新图像
void refreshImage(GLFWwindow* window)
{
    //CUDA映射GPU端图形资源
    uchar4* img_d;
    cudaGraphicsGLRegisterBuffer(&resource, PBO, cudaGraphicsMapFlagsNone);
    cudaGraphicsMapResources(1, &resource, NULL);
    cudaGraphicsResourceGetMappedPointer((void**)&img_d, NULL, resource);

    //计算可视区域
    Zone z = zones.back();
    double4 zone;
    zone.x = z.xC - z.zoom / 2;
    zone.z = z.xC + z.zoom / 2;
    zone.y = z.yC - z.zoom / 2 * size.y / size.x;
    zone.w  = z.yC + z.zoom / 2 * size.y / size.x;

    //CUDA计算
    if (mode)
        Mandelbrot(img_d, palette_d, size, zone, max_Iterations);
    else
        Julia(img_d, palette_d, size, JuliaPos, zone, max_Iterations);

    // Check for any errors launching the kernel
    cudaError_t cudaStatus = cudaGetLastError();
    if (cudaStatus != cudaSuccess)
    {
        std::cerr << "addKernel launch failed: " << cudaGetErrorString(cudaStatus) << std::endl;
        exit(-1);
    }

    //CUDA图形资源解映射
    cudaGraphicsUnmapResources(1, &resource, NULL);

    //OpenGL绘制像素
    glDrawPixels(size.x, size.y, GL_RGBA, GL_UNSIGNED_BYTE, 0);
    glfwSwapBuffers(window);
}

int main()
{
    //初始区域
    zones.push_back({ 4, 0, 0 });

    std::cout << "Fractal with OpenGL and CUDA\n";

    std::cout << "Select Mode (1 for Mandelbrot; 0 for Julia): ";
    std::cin >> mode;

    std::cout << "Max Iteration: ";
    std::cin >> max_Iterations;

    if (!mode)
    {
        std::cout << "Julia Pos: ";
        std::cin >> JuliaPos.x >> JuliaPos.y;
    }

    //初始化OpenGL到兼容模式
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE);

    //创建窗口
    GLFWwindow* window = glfwCreateWindow(size.x, size.y, "Fractal", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);

    //初始化OpenGL API
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    //初始化视口
    glViewport(0, 0, size.x, size.y);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0, size.x, size.y, 0, -1, 1);

    //绑定事件处理回调
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    glfwSetMouseButtonCallback(window, mouse_button_callback);
    glfwSetCursorPosCallback(window, cursor_position_callback);

    //创建像素缓冲区
    glGenBuffers(1, &PBO);
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, PBO);
    glBufferData(GL_PIXEL_UNPACK_BUFFER, size.x * size.y * 4, NULL, GL_DYNAMIC_DRAW);

    //初始化调色盘
    initPalette();

    //刷新图像
    refreshImage(window);

    //循环处理事件
    while (!glfwWindowShouldClose(window))
    {
        processInput(window);
        glfwPollEvents();
    }

    glfwTerminate();
    return 0;
}

更多

本文的代码保存在:JeffreyXiang/Fractal - GitHub

如果你对这个话题仍感兴趣,不妨看看下面几个链接。

什么是分形? - 知乎

神奇的分形艺术: Mandelbrot集和Julia集_亨利王的博客-CSDN博客

再谈Julia集与Mandelbrot集 | Matrix67: The Aha Moments