十一、RGB颜色模型和真彩色
在继续修改程序以前,我们先要讨论一下和颜色相关的计算机语言知识。
在程序的第(9)部分给像素绘色时,其中表示颜色的参数rgb
是一个32比特的整数。这个颜色代码使用的是通常所说的RGB颜色模型或红绿蓝颜色模型,并分别用8比特的数据来表示某颜色中的红、绿、蓝分量,即总共用24比特的数据来表示一种颜色,故可表示的不同颜色总共有224约为1677万种,即通常所说的“真彩色”方式。因为每种颜色分量都用8比特数据来表示,所以都有从0到255这256种可能性。数字越小,相应分量越小,反之亦然。
在上述方式下,一个32比特整数如何表示一种色彩?最高位的8比特通常被忽略(也有用作Alpha通道的,即表示此像素的透明程度,但在这里和我们无关)。接下去的8比特用来表示此颜色中的红色分量,然后是8比特的绿色分量,最后是8比特的蓝色分量。因为8比特数据恰好可以用两个十六进制数来表示,所以每种颜色也可用6位十六进制数来表示。
所以当我们拿到一个32比特整数rgb,想知道它代表了红绿蓝颜色分量分别为多少的颜色,只需将其表示为32位二进制形式,忽略其最高8位,然后依次每8位表示的就是此颜色的红绿蓝色分量(或者将其表示为8位十六进制形式,忽略其最高2位,然后依次每2位表示的就是此颜色的红绿蓝色分量)。比如十六进制数ABCDEF表示的颜色中,红色分量为AB(十进制数171),绿色分量为CD(十进制数205),蓝色分量为EF(十进制数239)。如果用Java程序写出来:
int rgb = 0xABCDEF;
int r = (rgb >> 16) & 0xFF; // = 0xAB
int g = (rgb >> 8) & 0xFF; // = 0xCD
int b = rgb & 0xFF; // = 0xEF
里面计算所得的r
,g
,b
就是整数rgb
所代表的颜色的红、绿、蓝分量。(&为按位与运算,“& 0xFF”即十六进制数只取最低两位。)
反之,如果我们知道了某颜色的红绿蓝分量(数字都在0和255之间),如何得到其颜色的整数表示?反过来即可,把分量写成二进制(或十六进制)形式,分段拼好:
int r = 0xAB;
int g = 0xCD;
int b = 0xEF;
int rgb = (r << 16) | (g << 8) | b; // = 0xABCDEF
(|为按位或运算。)
十二、颜色渐变
颜色渐变也叫色彩梯度。给定两种颜色,可以非常不同(比如红色和绿色),如何实现从第一种颜色到第二种颜色的渐变,也就是在它们间插入一系列颜色,使得每两种相邻的颜色间的区别都不大?最简单的方式大概就是分别对两种颜色的红绿蓝分量进行线性插值,然后将得到的分量合成就得到了颜色(函数)。
比如说我们取一种颜色为红色(十六进制数FF0000,红色分量全满,其它两分量为0),另一种为绿色(十六进制数00FF00)。那么下面的函数,当变量f
取0到1之间的实数时,我们就得到了从红色渐变为绿色过程的一系列颜色:
int interpolation(double f) {
int red = 0xFF0000;
int green = 0x00FF00;
// 将颜色拆成红绿蓝分量
int r0 = (red >> 16) & 0xFF;
int g0 = (red >> 8) & 0xFF;
int b0 = red & 0xFF;
int r1 = (green >> 16) & 0xFF;
int g1 = (green >> 8) & 0xFF;
int b1 = green & 0xFF;
// 线性插值
int r = (int) ((1 - f) * r0 + f * r1 + 0.5);
int g = (int) ((1 - f) * g0 + f * g1 + 0.5);
int b = (int) ((1 - f) * b0 + f * b1 + 0.5);
// 将红绿蓝分量拼合
return (r << 16) | (g << 8) | b;
}
代码很容易懂:先是把红色red
和绿色green
拆成各分量,然后再以变量f
为各分量作线性插值并四舍五入取整,算出所得颜色的各分量,最后返回拼合的颜色。下图即用此函数计算所得的颜色,从左到右f
从0线性地变到1:
十三、根据迭代次数选择颜色的调色盘
回过头来看我们绘制Mandelbrot集合的程序,我们改写一下调色盘函数。
Java版本:
// (8)调色盘
int getColor(double d) {
if (d >= 0) {
return interpolation(d / MAXITERNUMBER, 0xFFFFFF, 0x202020);
} else {
return 0x000000;
}
}
int interpolation(double f, int c0, int c1) {
int r0 = (c0 >> 16) & 0xFF;
int g0 = (c0 >> 8) & 0xFF;
int b0 = c0 & 0xFF;
int r1 = (c1 >> 16) & 0xFF;
int g1 = (c1 >> 8) & 0xFF;
int b1 = c1 & 0xFF;
int r = (int) ((1 - f) * r0 + f * r1 + 0.5);
int g = (int) ((1 - f) * g0 + f * g1 + 0.5);
int b = (int) ((1 - f) * b0 + f * b1 + 0.5);
return (r << 16) | (g << 8) | b;
}
JavaScript版本:
// (8)调色盘
function getColor(d) {
if (d >= 0) {
return interpolation(d / MAXITERNUMBER, 0xFFFFFF, 0x202020);
} else {
return 0x000000;
}
}
function interpolation(f, c0, c1) {
var r0 = (c0 >> 16) & 0xFF;
var g0 = (c0 >> 8) & 0xFF;
var b0 = c0 & 0xFF;
var r1 = (c1 >> 16) & 0xFF;
var g1 = (c1 >> 8) & 0xFF;
var b1 = c1 & 0xFF;
var r = Math.floor((1 - f) * r0 + f * r1 + 0.5);
var g = Math.floor ((1 - f) * g0 + f * g1 + 0.5);
var b = Math.floor ((1 - f) * b0 + f * b1 + 0.5);
return (r << 16) | (g << 8) | b;
}
我们得到
-0.743030 + 0.126433i @ 0.016110 /0.75
终于见到了海马尾和孔雀羽眼的形状,虽然还缺点颜色。
容易看出,上面新添的interpolation()
函数计算的正是给定两颜色参数c0
和c1
的线性插值。而在新的调色盘函数getColor()
中,我们不再作非黑即白的划分,而是根据超过逃逸半径所需的迭代次数(更精确地说,是根据“迭代次数/迭代次数上限”值,此值总是在0和1之间)来选择颜色:次数越少,像素的颜色就越靠近白色(十六进制数FFFFFF);次数越多,像素的颜色就越靠近深灰色(十六进制数202020)。
和开始的非白即黑图相比,在新图中多出来的那些像素全都不是黑色,而是某种程度的灰色,也即代表它们的中心点都不属于Mandelbrot集合。所以在这里要纠正的一个误解,当我在前面说绘出的图是“Mandelbrot集合的图像”,这话并不精确。那些图像更象是Mandelbrot集合的图像再加上在Mandelbrot集合附近但并不属于Mandelbrot集合的点的某种结构的图像,而所谓的“某种结构”是通过对不同迭代次数的区别对待而显现出来的。
但反过来说,之所以在复平面的某个地方,不属于Mandelbrot集合的点会有复杂的结构,也是因为在那附近有属于Mandelbrot集合的点。粗略地说,一个点虽然不属于Mandelbrot集合,也就是在迭代过程中数列最终还是超出了逃逸半径,但是用了比其他点更多的次数,那是因为它比其他点更靠近Mandelbrot集合。此时虽然我们在取代表点的过程中没能直接取中属于Mandelbrot集合的点,但通过观测到离它很近的集合外的点,也算间接观测到了集合本身的形状。所以在这种意义下,我们说这个图像就是“Mandelbrot集合的图像”,也未尝不可。
当然,我们完全可以用其他的调色盘函数来取代前面举的例子。比如说换成(此处只提供Java版本,很容易改写成JavaScript版本)
// (8)调色盘
int getColor(double d) {
if (d >= 0) {
int q = (int) d % 3;
if (q == 0) {
return 0xFF0000;
} else if (q == 1) {
return 0x0000FF;
} else {
return 0x00FF00;
}
} else {
return 0x000000;
}
}
-0.743030 + 0.126433i @ 0.016110 /0.75
这是以超过逃逸半径所需的迭代次数的除以3的余数来决定颜色。如果嫌细节部分乱糟糟,还可以分段考虑,迭代次数100以内(也即离Mandelbrot集合较远的那些点)考虑除以2的余数,超过100则用前面的线性插值,做到两种风格兼具:
// (8)调色盘
int getColor(double d) {
if (d >= 0) {
if (d <= 100) {
int q = (int) d % 2;
if (q == 0) {
return 0x0000FF;
} else {
return 0x00FF00;
}
}
return interpolation(d / MAXITERNUMBER, 0xFFFF00, 0xFF0000);
} else {
return 0x000000;
}
}
-0.743030 + 0.126433i @ 0.016110 /0.75
或者试试sin函数?
// (8)调色盘
int getColor(double d) {
if (d >= 0) {
return interpolation(Math.sin(d * 50), 0xFFFF00, 0xFF0000);
} else {
return 0x000000;
}
}
-0.743030 + 0.126433i @ 0.016110 /0.75
可能性无穷无尽。应该说这更是一个艺术问题或个人口味问题,已经和数学不太相关了。读者完全可以自己试着写调色盘函数,看看可以创造出什么样的画面来。
(待续)
网友评论
======================
据我所知,连选出哪些算法在迭代次数有限时能尽量好地去除这些附加结构都是个很困难的任务,因为曼德布罗集的补不是递归的。