本模块内容绝大部分是在慕课上看中国农业大学网客时的笔记,因此算作转载,在此鸣谢赵明、李振波两位老师,感谢他们录制该门课程供大家学习!
光栅图形学
为适应光栅显示器,需发展一套和他相适应的算法来处理、显示图形。光栅图形学算法大多数属于计算机图形的底层算法,虽然编程过程中我们一般不会亲自去写它,但它包含很多图形学的基本概念和思想,了解这些算法会对更高级、更深层次内容的理解有很大帮助,亦是本模块重要的第一课。直线段的扫描转换算法
计算机上不像数学,直线的显示是由有限个像素点来拟合直线,因此要使用一些算法来进行图形的显示。
最容易想到的直线绘制算法
首先需要知道这些离散的像素点的坐标
采用斜截式方程表示直线 y=kx+b,已知直线始末两点坐标,可得k和b,然后求x和y。
此时我们令x在x0~x1之间变化,求y的值。(像素坐标都是整数,所以y值要进行取整处理)
这里我们为了将数学上的点扫描成屏幕上的像素点使用了四舍五入法,即x、y各加0.5,然后取整。大概的代码如下:
public class TraditionalMethod extends JFrame{private int x1,x2,y1,y2;private float k,b;public TraditionalMethod( int x1 , int y1 , int x2 , int y2 ){super();setTitle("最易想到的直线绘制算法");setSize(800,600);setVisible(true);this.x1=x1;this.x2=x2;this.y1=y1;this.y2=y2;traditionalDrawLine(x1,y1,x2,y2);}private void traditionalDrawLine( int x1 , int y1 , int x2 , int y2 ){if (x1==x2){for (int i=y1;i<y2;i++){drawPixel(x1,i);}}else {if (x1>x2){//保证x1对应的点在x2对应点的左边int temp=x1;x1=x2;x2=temp;temp=y1;y1=y2;y2=temp;}k=(y2-y1)/(x2-x1);b=y1-k*x1;for (int i=x1;i<x2;i++){int j=(int)(k*i+b+0.5);drawPixel(i,j);}}}private void drawPixel(int x1,int y1) {//暂时没找到Java中的点绘制方法,虽然可以用drawLine或drawOval等代替但懒得搞了}}
直线是最基本的图形,一幅图往往要画成千上万条直线,因此直线算法的好坏会直接影响图形的质量和显示速度。上面的算法虽然直观,但效率实在感人,且许多细节未考虑。于是考虑是否能将乘法取消掉从而提高效率?答案当然是可以,所以有下面三种经典直线绘制算法之一——DDA(Digital Differential Analyzer,数值微分法)
三种经典直线绘制算法
1.数值微分法(DDA)
介绍DDA之前需要先引进图形学中一个重要的“增量思想”:
在直线中,x值每次运算固定+1,由于x,y的一次线性关系,所以有像y4=y3+k这样的递推关系,就可以取消掉相对加法慢一些的乘法运算,而用加法代替它达到相同效果。
运用了该思想的DDA算法步骤如下:
计算k值,并与1比较(*)四舍五入由x算y递推循环
*注意,这并不适合任意斜率的直线,k<1比较适用,但k>1时,光栅点会随k变大而变稀,直线看上去就像“断了”。此时可以算斜率的倒数,让y每次运算固定+1,然后算x。
public class DDADrawLine extends JFrame {private int x1,x2,y1,y2;private float k;public DDADrawLine( int x1 , int y1 , int x2 , int y2 ){super();setTitle("DDA直线绘制算法");setSize(800,600);setVisible(true);this.x1=x1;this.x2=x2;this.y1=y1;this.y2=y2;DDA(x1,y1,x2,y2);}private void DDA( int x1 , int y1 , int x2 , int y2 ){if (x1==x2){for (int i=y1;i<y2;i++){drawPixel(x1,i);}}else if (y1==y2){//对于特殊斜率无需计算k值for (int i=x1;i<x2;i++){drawPixel(i,y1);}}else {if (x1>x2){ //令x1在x2左边int temp=x1;x1=x2;x2=temp;temp=y1;y1=y2;y2=temp;}k=(y2-y1)/(x2-x1);if (k<1&&k>-1){int j=y1;for (int i=x1;i<x2;i++){drawPixel(i,j);j+=k;}}else { //按y步进,因y1和y2大小关系未知所以需要分别进行k=1/k;if (k>0){int i=x1;for (int j=y1;j<y2;j++) {drawPixel(i,j);i+=k;}}else {int i=x1;for (int j=y1;j>y2;j--){drawPixel(i,j);i+=k;}}}}}private void drawPixel(int x1,int y1) {//暂时没找到Java中的点绘制方法,虽然可以用drawLine或drawOval等代替但懒得搞了}}
其实,这样还不是最优,进一步改进的方向如下:
改进效率:将浮点运算改进为整数运算修改直线方程类型-->中点画线法
2.中点画线法
中点画线法使用了直线的一般式方程F(x,y)=0或Ax+By+C=0,其中A=-△y,B=△x,C=-B△x此时,直线将坐标系分为3部分:直线上方(F>0),直线下方(F<0),直线(F=0)。我们每次在最大位移方向上走一步,在另一个方向上走还是不走取决于中间误差项的判断。比如0≤|k|≤1时,x++后,y++或者y不变要看与中点M的y值大小
判断方法:
将M代入F,(x=xi+1,y=yi+0.5)得到一个值d,看d与0的大小关系就能判断中点在直线的上面还是下面了
中点画线法的计算量:
目前情况,为得到d,需要2个乘法,4个加法
public class MidPoint extends JFrame {private int x1,x2,y1,y2,d,A,B,C;public MidPoint( int x1 , int y1 , int x2 , int y2 ){super();setTitle("Mid Point直线绘制算法");setSize(800,600);setVisible(true);this.x1 = x1;this.x2 = x2;this.y1 = y1;this.y2 = y2;this.A = y2 - y1;this.B = x1 - x2;this.C = x1*(y1 - y2) + y1*(x2 - x1);midPointDrawLine(x1 , y1 , x2 , y2 , A , B , C);//基于直线的一般方程Ax+By+C=0}private void midPointDrawLine(int x1 , int y1 , int x2 , int y2 , int A , int B , int C){if (abs(A) < abs(B)){ //判断按x还是y步进的依据,此处为按x步进if (x1 > x2){int temp = x1;x1 = x2;x2 = temp;temp = y1;y1 = y2;y2 = temp;}int j = y1;if (y1 < y2){ //判断从x1递增还是从x2递减,递增情况:for (int i = x1 ; i < x2 ; i++){d=2*A*i+B*(2*j+1)+2*C; //扩大2倍消去浮点数0.5后再与0比较if (d >= 0){j++;drawPixel(i,j);}else {drawPixel(i,j);}}}else { //递减情况:for (int i = x2 ; i > x1 ; i--){d = 2*A*i + B*(2*j + 1) + 2*C;if (d >= 0){j++;drawPixel(i,j);}else {drawPixel(i,j);}}}}else{ //按y步进情况if (y1 >y2){int temp = y1;y1 = y2;y2 = temp;temp = x1;x1 = x2;x2 = temp;}int i = x1;if (x1 < x2) {for (int j = y1; j < y2 ; j++){d = 2*A*i + B*(2*j + 1) + 2*C;if (d >= 0){i++;drawPixel(i,j);}else {drawPixel(i,j);}}}else {for (int j = y2 ; j > y1 ; j--){d = 2*A*i + B*(2*j + 1) + 2*C;if (d < 0){i++;drawPixel(i,j);}else {drawPixel(i,j);}}}}}private void drawPixel(int x,int y){//暂时没找到Java中的点绘制方法,虽然可以用drawLine或drawOval等代替但懒得搞了}}
优化:
类比前面,可以使用增量计算提高效率
推导d的递推公式如下图所示:
d<0时
d>=0时
步骤:
1)计算d的初始值F(x0+1,y=y0+0.5),有d0=Ax0+By0+C+A+0.5B=>d0=A+0.5B
2)d2=d1+A+B,d<0;d2=d1+A,d≥0……此时,效率至少与DDA算法一样高
**可以用2d0=2A+B,解决掉浮点数运算,全部转化为整数加法
该改进较为简单,偷个懒代码省略嘿嘿
其实,即使已经简化到这样,仍然有很多人愿意进一步去改进它。因为这种很基础的算法每次处理图像都会被几千、几万,甚至几百万次地被调用,该算法每一点点的改进都会被n倍地放大。我看过的一个思路是根据“沿x方向和沿y方向描点是有规律的,找出该规律并循环即可连增量加法也不必要算了”。更多对这些算法的优化读者可以自行去中国知网关键字搜索,只需要一点编程基础,一点初中数学基础和一点点线性代数基础,然后会熟练使用搜索引擎就能看懂绝大多数文章。