2.3 分形着色器

最近一些年有一门非常热门的几何学分支——分形几何,基于分形几何可以渲染出很多绚丽多彩的图案。本节将基于曼德布罗集(Mandelbrot set)和茱莉亚集(Julia Set)向读者简单介绍如何基于分形几何开发出具有吸引力的程序纹理着色器。

2.3.1 曼德布罗集简介

曼德布罗集是基于复数在复平面上迭代产生的,因此下面将首先简要介绍一下复数及复平面的知识。每个复数由两部分组成:实部和虚部。实部的基本单位是实数1,虚部的基本单位是i。i是一个很特殊的虚数,其是-1的平方根,即 2i =-1。

复数可以用a+bi的基本格式来表示,下面给出了两个复数相乘的规则。

x = a+bi y = c+di

xy = ac+adi+cbi-bd = (ac-bc)+(ad+bc)i

因为复数包含两个部分,所以每个复数都可以看做是二维平面上的一个点,用实部作为一个轴的坐标,虚部作为另一个轴的坐标。这个平面就称之为复平面,具体情况如图2-10所示。

介绍完了复数及复平面的基本知识后,就可介绍曼德布罗集了,其通过一个涉及复数的递归函数迭代产生,此递归函数如下。

从上述递归函数中可以看出,不同的常数c会导致不同的迭代结果。有些c值经过迭代可能会产生无穷大,有些可能不会。那些不会导致无穷大的c值就构成了曼德布罗集。

▲图2-10 复数在复数平面上的表示

▲图2-11 曼德布罗物集合图形

可以通过OpenGL ES 2.0的片元着色器进行上述的迭代计算,将迭代一定次数后达到无穷大(实际开发中指超过指定值)的片元采用一种颜色着色,迭代一定次数后小于指定值的片元采用另一种颜色着色。如果希望得到更绚丽的图案,则可以将迭代一定次数后超过指定值的片元,根据迭代次数的多少采用不同的颜色着色,图2-11就给出了一幅采用不同灰度进行着色的曼德布罗集图案。

2.3.2 曼德布罗集着色器的实现

上一小节介绍了曼德布罗集的基本原理,本小节将给出一个实现了曼德布罗集着色器的案例Sample2_7,其运行效果如图2-12所示。

▲图2-12 案例Sample2_7的运行效果图

说明

图2-12中的4幅图是采用不同的中心坐标和缩放位置绘制的,4幅图各自的绘制参数如表2-5所列。学习完本案例的代码后,读者可以自由改变这些参数以获得想要的渲染效果。另外,由于本书插图采用的是灰度印刷,因此可能看起来不是很漂亮,此时请读者自行用真机运行本案例观察就可以看到很漂亮的曼德布罗集了。

表2-5 缩放系数和中心坐标位置的值

了解了案例的运行效果后,下面对案例的开发进行简要的介绍。由于本案例中大部分代码与前面很多案例中的基本一致,因此这里仅介绍本案例中有代表性的着色器部分,具体内容如下。

(1)首先给出的是顶点着色器,其代码如下。

代码位置:见随书光盘中源代码/第2章/Sample2_7/assets目录下的vertex.sh。

1 uniform mat4 uMVPMatrix; //总变换矩阵

2 attribute vec3 aPosition; //顶点位置

3 attribute vec2 aTexCoor; //顶点纹理坐标

4 varying vec2 vTexPosition; //转换后传递给片元着色器的纹理坐标

5 void main(){

6 gl_Position = uMVPMatrix * vec4(aPosition,1); //根据总变换矩阵计算此次绘制此顶点的位置

7 vTexPosition =(aTexCoor-0.5)*5.0; //将纹理坐标转换后传递给片元着色器

8 }

提示

从上述代码中可以看出,此顶点着色器与普通纹理映射的顶点着色器基本一致。唯一的区别就是其不是将管线传入的纹理坐标直接传出的,而是将纹理坐标从0.0~1.0的范围转换到在-2.5~2.5的范围后再传出的,这样做是为了后面片元着色器迭代计算的方便。

(2)接着给出的是片元着色器,其代码如下。

代码位置:见随书光盘中源代码/第2章/Sample2_7/assets目录下的frag.sh。

      1    precision mediump float;                          //给出默认的浮点精度
      2    varying vec2 vTexPosition;                        //从顶点着色器传递过来的纹理坐标
      3    const float maxIterations = 9999.0;              //最大迭代次数
      4    const float zoom = 1.0;                           //缩放系数
      5    const float xCenter = 0.0;                        //中心x坐标
      6    const float yCenter = 0.0;                        //中心y坐标
      7    const vec3 innerColor = vec3(0.0, 0.0, 1.0);     //内部颜色
      8    const vec3 outerColor1 = vec3(0.1, 0.0, 0.0);    //外部颜色1
      9    const vec3 outerColor2 = vec3(0.0, 1.0, 0.0);    //外部颜色2
      10   void main(){
      11         float real = vTexPosition.x * zoom + xCenter;   //变换当前位置
      12         float imag = vTexPosition.y * zoom + yCenter;
      13         float cReal = real;                              //c的实部
      14         float cImag = imag;                              //c的虚部
      15         float r2 = 0.0;                                  //半径的平方
      16         float i;                                          //迭代次数
      17         for(i=0.0; i<maxIterations && r2<4.0; i++){     //循环迭代
      18              float tmpReal = real;                      //保存当前实部值
      19              real =(tmpReal * tmpReal)-(imag * imag)+cReal;//计算下一次迭代后实部的值
      20              imag = 2.0 *tmpReal * imag +cImag;         //计算下一次迭代后虚部的值
      21              r2 =(real * real)+(imag * imag);        //计算半径的平方
      22         }
      23       vec3 color;                  //最终颜色
      24       if(r2 < 4.0){                //如果r2未达到4就退出了循环,表明迭代次数已达到最大值
      25         color = innerColor;       //此时采用内部颜色对此片元着色
      26       }else{                       //如果因r2大于4.0而退出循环,表明此位置在外部
      27         color = mix(outerColor1, outerColor2, fract(i * 0.07));
                                                        //按迭代次数采用不同的颜色着色
      28       }
      29         gl_FragColor = vec4(color, 1.0);     //将最终颜色传递给管线
      30   }

● 第17-28行是本片元着色器的关键,其实现了上一小节中介绍的曼德布罗集迭代生成算法。首先通过一个循环进行迭代,当迭代超过最大次数或指定值时停止迭代。停止迭代后根据终止迭代的原因采用不同的颜色对片元进行着色。

● 通过调整缩放系数(zoom),中心点坐标(xCenter, yCenter)可以得到不同的局部图案,有兴趣的读者可以自行修改这些参数并运行观察。

提示

像曼德布罗集这样大剂量高并发的运算用片元着色器实现最好不过,性能将大大优于CPU的实现。笔者自己也开发过用Java代码基于CPU实现的版本(也是Android手机版的),需要数十秒才能跑出结果,而本小节的案例仅需不到1秒。从这里也可以看出,在3D游戏开发中恰当运用计算能力较强的GPU可以很好地改善性能问题。

2.3.3 将曼德布罗集纹理应用到实际物体上

上一小节案例中的曼德布罗集纹理是应用到一个简单的纹理矩形上的,其实可以方便地将其应用到任意的物体上。本小节就给出一个将曼德布罗集纹理应用到茶壶上的案例Sample2_8,其运行效果如图2-13所示。

▲图2-13 案例Sample2_8的运行效果图

提示

由于本书插图采用的是灰度印刷,因此可能看起来不是很漂亮,此时请读者自行用真机运行本案例观察就可以看到很漂亮的曼德布罗集纹理茶壶了。

了解了案例的运行效果后,下面简要介绍一下案例的开发。由于本案例是复制并修改的上册第9章的案例Sample9_4,故没有变化的代码不再赘述,仅介绍着色器中有变化的部分,具体内容如下所列。

(1)首先需要将顶点着色器中直接将纹理坐标传入片元着色器的代码进行修改。修改为将纹理坐标在0.0~1.0的范围转换到在-2.5~2.5的范围后再传出的版本。这部分代码很简单,这里就不给出了,需要的读者请参考随书光盘里的源代码。

(2)接着将片元着色器进行修改,增加曼德布罗集迭代计算的相关代码。由于需要增加的代码与上一小节案例Sample2_7中的基本相同,因此这里不再重复给出,需要的读者也请参考随书光盘里的源代码。

提示

特别需要注意的是,本案例中迭代次数需要从上一小节的9999改为99,否则用手指在屏幕上滑动以旋转茶壶时就会很卡。

2.3.4 茱莉亚集着色器的实现

将曼德布罗集中与片元位置挂钩的常量 c 替换为固定常量后就可以产生茱莉亚集的分形图案,本小节的第一个案例Sample2_9就是实现了茱莉亚集分形图案的纹理矩形,其运行效果如图2-14所示。

▲图2-14 Sample2_9运行效果图

说明

图2-14中的4幅图是采用不同的固定常量c绘制的,4幅图各自的绘制参数如表2-6所列。学习完本案例的代码后,读者可以自由改变这些参数以获得想要的渲染效果。另外,由于本书插图采用的是灰度印刷,因此可能看起来不是很漂亮,此时请读者自行用真机运行本案例观察就可以看到很漂亮的茱莉亚集了。

表2-6 不同的参数c

了解了案例的运行效果后,下面对案例的开发进行简要的介绍。由于本案例是由前面的案例Sample2_7修改而来,而且仅修改了片元着色器。故这里仅给出修改后片元着色器的代码,具体内容如下。

代码位置:见随书光盘中源代码/第2章/Sample2_9/assets目录下的frag.sh。

      1    precision mediump float;                          //给出默认的浮点精度
      2    varying vec2 vTexPosition;                        //从顶点着色器传递过来的纹理坐标
      3    const float maxIterations = 9999.0;              //最大迭代次数
      4    const float zoom = 0.6;                           //缩放系数
      5    const float xCenter = 0.0;                        //中心x坐标
      6    const float yCenter = 0.0;                        //中心y坐标
      7    const vec3 innerColor = vec3(0.0, 0.0, 1.0);     //内部颜色
      8    const vec3 outerColor1 = vec3(0.1, 0.0, 0.0);    //外部颜色1
      9    const vec3 outerColor2 = vec3(0.0, 1.0, 0.0);    //外部颜色2
      10         void main(){
      11            float real = vTexPosition.x * zoom + xCenter;    //变换当前位置
      12            float imag = vTexPosition.y * zoom + yCenter;
      13            float cReal = 0.32;                      //c的实部
      14            float cImag =0.043;                      //c的虚部
      15            float r2 = 0.0;                          //半径的平方
      16            float i;                                 //迭代次数
      17            for(i=0.0; i<maxIterations && r2<4.0; i++){ //循环迭代
      18                  float tmpReal = real;                   //保存当前实部值
      19                  real =(tmpReal * tmpReal)-(imag * imag)+cReal;//计算下一次迭代后实部的值
      20                  imag = 2.0 *tmpReal * imag +cImag;     //计算下一次迭代后虚部的值
      21                  r2 =(real * real)+(imag * imag);    //计算半径的平方
      22            }
      23       vec3 color;                  //最终颜色
      24       if(r2 < 4.0){                //如果r2未达到4就退出了循环,表明迭代次数已达到最大值
      25         color = innerColor;       //此时采用内部颜色对此片元着色
      26       }else{                       //如果因r2大于4.0而退出循环,表明此位置在外部
      27         color = mix(outerColor1, outerColor2, fract(i * 0.07));
                                            //按迭代次数采用不同的颜色着色
      28       }
      29         gl_FragColor = vec4(color, 1.0);                     //将最终颜色传递给管线
      30   }

提示

从上述代码中可以看出,最大的变化就是第13行与第14行常数c的实部与虚部。修改前实部与虚部是与位置挂钩的,修改后变成固定的常量了。

同样也可以将茱莉亚集分形纹理应用到茶壶上,只要将前面的案例Sample2_8复制一份并对片元着色器进行相应的修改即可得到案例Sample2_10,其运行效果如图2-15所示。

▲图2-15 Sample2_10运行效果图

提示

由于本书插图采用的是灰度印刷,因此可能看起来不是很漂亮,此时请读者自行用真机运行本案例观察就可以看到很漂亮的茱莉亚集纹理茶壶了。

由于本案例中的代码主要是来自案例Sample2_8,仅仅是将原来曼德布罗集的片元着色器修改成了茱莉亚集的片元着色器。而茱莉亚集片元着色器的代码前面也已经给出,故这里就不再给出本案例的代码了,需要的读者请参考随书光盘。