在这个世界,有非常多奇妙的事情。分形,就是其中之一。
分形艺术(fractal art)由IBM研究室的数学家曼德布洛特(Benoit.Mandelbrot,1924-2010)提出。其维度并非整数的几何图形,而是在越来越细微的尺度上不断自我重复,是一项研究不规则性的科学。
图1 是 mandelbrot 分形图中的一部分。一个完整的 mandelbort 图如图 2 所示。
本文的目的,是使用 go 语言来绘制一幅这样的图。想想是不是觉得很酷?其实绘制这样的图非常简单,你只需要知道一点点数学知识即可。下面一步一步介绍。
有以下迭代公式:
其中 z0=0 z 0 = 0 , c c 为任意复数(complex). 也有叫虚数,这个随你啦。
若 limn→+∞zn lim n → + ∞ z n 收敛,则 c∈M c ∈ M 。 M M 表示 Mandelbrot 集合。
从上面的定义可得知,Mandelbrot 就是所有那些使得 limn→+∞zn lim n → + ∞ z n 收敛的 c c 的集合。
这里我只介绍对我们有用的。
这个特性能很快帮我们排除掉一大波非 Mandelbrot 集合里的数。对于 |c|>2 | c | > 2 的那些数来说,它一定不属于 Mandelbrot 集合。
值得一提的是,上面的特性反过来是不一定成立的。也就是说如果 |c|≤2 | c | ≤ 2 ,则 c 不一定就属于 Mandelbrot 集合。
上面这句话的意思是说,对于 Mandelbrot 集合中的任意一个复数 c c ,都能使用 |zn|≤2 | z n | ≤ 2 成立。反过来说,如果某个复数 c c 使得 |zn|>2 | z n | > 2 ,则该数一定不属于 Mandelbrot 集合。
如果写程序的话,给定一个数 c c ,判断它是不是 mandelbrot 集合就非常简单了,我们只要判断 zn z n 和 2 之间的大小就行了。
当然我们不可能在程序里判断每一个迭代中的 z z ,比如你只要计算 z0 z 0 到 z199 z 199 这 200 个 z z ,如果这 200 个数都小于等于 2,则认为它属于 Mandelbrot 集合(我们只能说它大概率属于 Mandelbrot 集合)。如果计算出来某个 z z 大于 2 了,那它一定不属于 Mandelbrot 集合。
当然你可以提高迭代精度,比如迭代 1000 次,10000 次甚至更多次。
我们只需要计算 |c|≤2 | c | ≤ 2 的那些复数就行了。
对于一个复数 c=x+yi c = x + y i ,我们只需要计算 −2≤x≤2 − 2 ≤ x ≤ 2 , −2≤y≤2 − 2 ≤ y ≤ 2 这个范围内的复数就行了。
假设我们有一幅大小为 1000×1000 1000 × 1000 的图,左上角第一个像素坐标为 (0,0) ( 0 , 0 ) ,右下角为 (999,999) ( 999 , 999 ) 。我们需要把每一个像素 (px,py) ( p x , p y ) 映射到一个复数 c=x+yi c = x + y i 上,其中 −2≤x≤2 − 2 ≤ x ≤ 2 , −2≤y≤2 − 2 ≤ y ≤ 2 。映射公式如下:
这样我们就能把坐标 (px,py) ( p x , p y ) 映射到一个复数 c c 上啦。
最后的问题就可以转换为判断像素 (px,py) ( p x , p y ) 是否属于 Mandelbrot 集合。
如果某个像素 p∈M p ∈ M ,则将其颜色设置为黑色。
如果某个像素 p∉M p ∉ M ,则如果确定颜色呢?前面已经讲了确定一个复数 c c 的方法,主要是计算每一个 z z 的大小。
假设我们在计算第 n n 个数 zn z n 的时候,发现 |zn|>2 | z n | > 2 ,则我们把对应的像素点颜色设置为 f(n) f ( n ) . f(n) f ( n ) 是一种把迭代次数转换为特定颜色的方法。举个例子,假设我们生成的是灰度图,我们可以设置:
即把迭代次数设置为像素值。如果 n>255 n > 255 怎么办?没关系,你可以对 255 取模,这样就可以把像素值控制到 255 以内了。
一切理论都准备好了,就差实践了。但是好像还差点什么。没错,复数在 go 语言里难道还需要自己构造一个吗?不用了!go 语言已经对复数提供了支持。
go 语言原生支持复数类型。
在 go 里,有两种复数类型,一种是 complex64
,另一种是 complex128
。这两种复数类型精度分别对应 float32
和 float64
。
下面的例子简单演示了声明复数的方法。
// demo01.go
package main
import "fmt"
import "math/cmplx"
func main() {
var x complex64 = complex(3, 4) // 使用 complex 内建函数
var y complex64 = complex(6, 8)
var z complex128 = complex(1, 2)
fmt.Println(x)
fmt.Printf("%v\n", y)
// 在 go 里,复数可以直接做四则运行
fmt.Println(x + y)
fmt.Println(x * y)
fmt.Println(x / y)
fmt.Println()
a := 1 + 2i // 也可以使用字面量。如果不指定类型,则推导类型是 complex128
b := 2 + 3i
c := cmplx.Sqrt(-4.41) // 对负数开根号,也可以得到复数。
fmt.Println(a * b)
fmt.Println(c)
fmt.Println(real(a)) // 计算实部
fmt.Println(imag(a)) // 计算虚部
}
// demo02.go
package main
import (
"image"
"image/color"
"image/png"
"io"
"math/cmplx"
"net/http"
)
func handle(w http.ResponseWriter, r *http.Request) {
draw(w)
}
func draw(w io.Writer) {
const size = 1000
rec := image.Rect(0, 0, size, size)
img := image.NewRGBA(rec)
for y := 0; y < size; y++ {
yy := 4 * (float64(y)/size - 0.5) // [-2, 2]
for x := 0; x < size; x++ {
xx := 4 * (float64(x)/size - 0.5) // [-2, 2]
c := complex(xx, yy)
img.Set(x, y, mandelbrot(c))
}
}
png.Encode(w, img)
}
// z := z^2 + c
// 特点,如果 c in M,则 |c| <= 2; 反过来不一定成立
// 如果 c in M,则 |z| <= 2. 这个特性可以用来发现 c 是否属于 M
func mandelbrot(c complex128) color.Color {
var z complex128
const iterator = 254
// 如果迭代 200 次发现 z 还是小于 2,则认为 c 属于 M
for i := uint8(0); i < iterator; i++ {
if cmplx.Abs(z) > 2 {
return getColor(i)
}
z = z*z + c
}
return color.Black
}
// 根据迭代次数计算一个合适的像素值
func getColor(n uint8) color.Color {
// 这里乘以 15 是为了提高颜色的区分度,即对比度
return color.Gray{n * 15}
}
func main() {
http.HandleFunc("/", handle)
http.ListenAndServe(":8080", nil)
}
最后的结果如下:
练习:
图 2 中的 getColor
函数定义如下:
func getColor(n uint8) color.Color {
paletted := [16]color.Color{
color.RGBA{66, 30, 15, 255}, // # brown 3
color.RGBA{25, 7, 26, 255}, // # dark violett
color.RGBA{9, 1, 47, 255}, //# darkest blue
color.RGBA{4, 4, 73, 255}, //# blue 5
color.RGBA{0, 7, 100, 255}, //# blue 4
color.RGBA{12, 44, 138, 255}, //# blue 3
color.RGBA{24, 82, 177, 255}, //# blue 2
color.RGBA{57, 125, 209, 255}, //# blue 1
color.RGBA{134, 181, 229, 255}, // # blue 0
color.RGBA{211, 236, 248, 255}, // # lightest blue
color.RGBA{241, 233, 191, 255}, // # lightest yellow
color.RGBA{248, 201, 95, 255}, // # light yellow
color.RGBA{255, 170, 0, 255}, // # dirty yellow
color.RGBA{204, 128, 0, 255}, // # brown 0
color.RGBA{153, 87, 0, 255}, // # brown 1
color.RGBA{106, 52, 3, 255}, // # brown 2
}
return paletted[n%16]
}