计算机图形学笔记二——圆形、圆弧段的绘制算法
圆形的绘制
绘制圆形,我们要利用好圆形的对称性,我们可以通过绘制\(\frac{1}{8}\)圆,然后进行一些变换完成整个圆的绘制。
上一篇:绘制直线的算法
下一篇:橡皮筋技术
中点画圆法
和中点画线法相似,首先构造圆的隐式方程:
在圆上的点是0,在圆外面的是F(x,y)>0,在圆里面的是F(x,y)<0,我们绘制第一卦象的\(\frac{1}{8}\)个圆[\(\frac{\pi}{2}\),\(\frac{\pi}{4}\)](往x轴正方向迭代),构造判别式:
当d$>=$0时,最佳逼近点取 \((x_i+1,y_i-1)\)
当d$<$0,最佳逼近点取 \((x_i,y_i)\)
(这里书上似乎写错了,书中写成了\(F(x_i+2,y_i-1.5)\),在此予以纠正。)
得到迭代公式,我们可以写出这样的代码:这份代码与书中的有所不同,书中的代码没有使用数组来存储循环的部分。
void Mid_Circle(int Rx, int Ry, int R) {
glBegin(GL_POINTS);
int x, y, d;
x = 0, y = R;
d = 5 - 4 * R;
int dir[4][2] = {
{ 1, 1 }, { 1,-1 },
{ -1,1}, {-1,-1 }
}; //使用循环来计算对称的区域
for (int i = 0; i < 2; i++) {//这里x=0所以只需要循环2次就行了。
glVertex2i(Rx + x * dir[i][0], Ry + y * dir[i][1]);
glVertex2i(Rx + y * dir[i][0], Ry + x * dir[i][1]);
}
while (x <= y) {
if (d >= 0) {
x += 1;
y -= 1;
d += 8 * (x - y) + 20;
}
else {
x += 1;
d += 8 * x + 12;
}
for (int i = 0; i < 4; i++) {
glVertex2i(Rx + x * dir[i][0], Ry + y * dir[i][1]);
glVertex2i(Rx + y * dir[i][0], Ry + x * dir[i][1]);
}
}
glFlush();
glEnd();
}
Bresenham画圆算法
和画线算法很相似,也是通过计算残差来进行近似,只是迭代方程有所不同,我们直接开始推导:
设当前最佳逼近点为 \((x_i,y_i)\),那么下一个最佳逼近点为\((x_i+1,y_i)\)或者 \((x_i+1,y_i-1)\),残差分别为:
若 \(p_i>0\)则取 \((x_i+1,y_i-1)\)作为取样点,得到迭代方程:
若 \((p_i<=0)\)则取点 \((x_i+1,y_i)\)作为最佳取样点,得到迭代方程:
在点 \((0,R)\)可以得到判别式:
我们可以写出这样的代码
void BRESENHAM_Circle(int Rx, int Ry, int R) {
glBegin(GL_POINTS);
int x, y;
x = 0, y = R;
int d = 3 - 2 * R;
int dir[4][2] = {
{ 1, 1 }, { 1,-1 },
{ -1,1}, {-1,-1 }
}; //使用循环来计算对称的区域
for (int i = 0; i < 2; i++) {
glVertex2i(Rx, Ry + y*dir[i][1]);
glVertex2i(Rx + y * dir[i][0], Ry);
}
while (x < y) {
if (d >= 0) {
x += 1;
y -= 1;
d += 4*(x - y) + 10;
}
else {
x += 1;
d += 4 * x + 6;
}
for (int i = 0; i < 4; i++) {
glVertex2i(Rx + x * dir[i][0], Ry + y * dir[i][1]);
glVertex2i(Rx + y * dir[i][0], Ry + x * dir[i][1]);
}
}
glFlush();
glEnd();
}
辅助画圆函数
再为两个算法重载两个使用两个点作为参数的画圆函数,来丰富画圆的方式:
void Mid_Circle(int Rx, int Ry, int endx, int endy) {
int distance = static_cast<int>(sqrt((endx - Rx) * (endx - Rx) + (endy - Ry) * (endy - Ry)));
Mid_Circle(Rx, Ry, distance);
}
void BRESENHAM_Circle(int Rx, int Ry, int endx, int endy) {
int distance = static_cast<int>(sqrt((endx - Rx) * (endx - Rx) + (endy - Ry) * (endy - Ry)));
BRESENHAM_Circle(Rx, Ry, distance);
}
圆弧段扫描算法
完成了画圆的扫描算法,接下来学习的是圆弧的扫描算法,为了方便绘制,规定:画的圆弧是起始点在圆上沿顺时针行走得到。
先来对坐标轴进行分区:
我们只绘制0区域的这 \(\frac{1}{8}\)段圆弧,然后通过对称来绘制其他的区域。因为我们需要绘制的不再是所有的区域,所以我们需要对区域的位置进行辨别,当然,使用的位置是相对于圆心的位置。写一个函数来返回我们需要的区域id:
FIndIndexOfArc
GLint FIndIndexOfArc(GLint x, GLint y) {
if (x > 0 && y >= 0) {
if (x < y)return 0;
else return 1;
}
else if (x >= 0&&y<0) {
if (x > -y)return 2;
else return 3;
}
else if (x < 0 && y <=0) {
if (x > y)return 4;
else return 5;
}
else {
if (-x > y)return 6;
else return 7;
}
}
然后根据区域id我们计算需要的变换矩阵这里的变化是对称,不是旋转那么就会面临一个问题——起始段的绘制和结尾段的绘制会有所不同,这个体现在分段处理函数DrawCircleArc
中:
GetMatrixT
void GetMatrixT(GLint T[2][2], GLint index) {
int i = index % 8;
if (i == 0) {
T[0][0] = 1, T[0][1] = 0,
T[1][0] = 0, T[1][1] = 1;
}
else if (i == 1) {
T[0][0] = 0, T[0][1] = 1,
T[1][0] = 1, T[1][1] = 0;
}
else if (i == 2) {
T[0][0] = 0, T[0][1] = 1,
T[1][0] = -1, T[1][1] = 0;
}
else if (i == 3) {
T[0][0] = 1, T[0][1] = 0,
T[1][0] = 0, T[1][1] = -1;
}
else if (i == 4) {
T[0][0] = -1, T[0][1] = 0,
T[1][0] = 0, T[1][1] = -1;
}
else if (i == 5) {
T[0][0] = 0, T[0][1] = -1,
T[1][0] = -1, T[1][1] = 0;
}
else if (i == 6) {
T[0][0] = 0, T[0][1] = -1,
T[1][0] = 1, T[1][1] = 0;
}
else if (i == 7) {
T[0][0] = -1, T[0][1] = 0,
T[1][0] = 0, T[1][1] = 1;
}
}
拿到两个点之后,我们首先获得他们的区域id,然后判断开始点的id是否小于结束点,如果大于,那么将结束点的id+8,也就是保证开始点的id小于结束点的id。再使用id去获取变换矩阵的时候,我们也要对id进行取模操作。假设我们得到的开始区间的id是a,结束区间的id是b,那么我们可以保证使用的两个id有着(\(a<b\))的关系。我们按照这个关系进行绘制,从a区域开始,绘制到b区域,特殊处理开始的区域和结束的区域。同时特殊处理开始和结束在同一个区域的情况。
/*分段处理的函数*/
/************************/
//ins和ine是开始和结束的id
if (ins == ine){
//绘制在同一个区域的圆弧
...
}
else{
//1.起始段画圆弧
...
//2.中间画弧段
...
//3.终止点画弧段
...
}
我们再写一个可以进行矩阵乘法和矩阵求逆的函数(我个人觉得这很浪费时间,但是这里还是这么写了。)
void GetTtoPt(GLint T[2][2], GLint sx, GLint sy, GLint& retx, GLint& rety, bool mode) {
if (mode) { //求逆
//主对角线互换除以行列式,副对角线乘-1,除以行列式
GLint T1[2][2], temp;
T1[0][0]=T[1][1];
T1[1][1] = T[0][0];
T1[1][0] = T[1][0]*-1 ;
T1[0][1] = T[0][1]*-1;
temp = T[0][0] * T[1][1] - T[1][0] * T[0][1];
retx = (sx * T1[0][0] + sy * T1[0][1])/temp;
rety = (sx * T1[1][0] + sy * T1[1][1])/temp;
}
else {
retx = (sx * T[0][0] + sy * T[0][1]) ;
rety = (sx * T[1][0] + sy * T[1][1]) ;
}
}
然后是进行弧段绘制的函数,在绘制任意区域内的一点圆弧时,我们先把目标点的坐标通过乘上变换矩阵的逆矩阵来得到第0区域中对应的点。把目标点绘制之后,在变换到对应的区域。
SetArc
//绘制弧段
/**************/
void SetArc(GLint Rx, GLint Ry, GLint R, GLint ArcType, GLint T[2][2], GLint startx, GLint starty, GLint endx, GLint endy){
GLint x, y, d;
GLint p0x, p0y, p1x, p1y;
glBegin(GL_POINTS);
//ArcType解释:
//0 画1/8圆 是连续段的中部
//1 画0到start圆 是奇数段的开始 偶数段的结束
//2 画start到1/8x圆 是偶数段的开始 奇数段的结束
//3 画start到end圆 是连续段的全部
if (ArcType == 0 || ArcType == 1) {
x = 0;
y = R;
d = 5 - 4 * R;
glColor3f(1.0, 0, 0);
}
else {
glColor3f(0, 1.0, 0);
x = startx;
y = starty;
d = 8 * x - 4 * y + 5;
}
p0x = x, p0y = y;
//投影到第0区域
GetTtoPt(T, p0x,p0y, p1x,p1y);
glVertex2i(Rx + p1x, Ry - p1y);
while (1) {
if (ArcType == 0 || ArcType == 2) { //生成到镜像线八分之一处。
if (x > y)break;
}
else if(ArcType==1){ //生成(0,R)到start的圆弧
glColor3f(0, 1.0, 0);
if (x > startx)break;
}
else if (ArcType == 3) { //生成到end的圆弧
glColor3f(0, 0, 1.0);
if (x > endx)break;
}
//中点画圆法
if (d >= 0) {
x += 1, y -= 1;
d += 8 * (x - y) + 20;
}
else {
x += 1;
d += 8 * x + 12;
}
p0x = x;
p0y = y;
//从第0区域回到绘制区域
GetTtoPt(T, p0x, p0y, p1x, p1y);
glVertex2i(Rx + p1x, Ry - p1y);
}
glColor3f(1.0, 0, 0);
glFlush();
glEnd();
}
最后补充分段处理的函数和它的辅助绘制函数,注意,因为glut中的坐标默认是y轴朝下,所以在绘制的时候可以进行反转y轴的方式来进行纠正。
DrawCircleArc
void DrawCircleArc(GLint Rx,GLint Ry,GLint R,GLint startx,GLint starty,GLint endx,GLint endy) {
//圆心移动到原点,并把y轴反转
GLint sx = startx-Rx, sy = Ry-starty, ex = endx-Rx, ey = Ry-endy;
//第一步查找两个端点所在区域编号
GLint T[2][2], T1[2][2];
GLint pt0x=0, pt0y=0, pt1x=0, pt1y=0;
//找到对应的区间编号(使用移动后的坐标)
GLint ins = FIndIndexOfArc(sx, sy);
GLint ine = FIndIndexOfArc(ex, ey);
//保证结束区域的值大于开始区域,最后使用index%8得到计算区域
if (ine < ins)ine += 8;
if (ins == ine) {//同一个区域
//首先镜像到0号区域,然后镜像到对应的区域
GetMatrixT(T, ins); //获得0区域的镜像矩阵
GetTtoPt(T, sx, sy, pt0x, pt0y, GL_TRUE);
GetTtoPt(T, ex, ey, pt1x, pt1y, GL_TRUE);
if (pt0x < pt1x) {//保证了startx <= endx
SetArc(Rx, Ry, R, 3, T, pt0x, pt0y, pt1x, pt1y); //画弧线上的一段,从pt0到pt1
}
else {
SetArc(Rx, Ry, R, 3, T, pt1x, pt1y, pt0x, pt0y);
}
}
else {
//1.起始段画圆弧
GetMatrixT(T, ins);
GetTtoPt(T, sx, sy, pt0x, pt0y, GL_TRUE);
if (!(ins % 2)) {
SetArc(Rx, Ry, R, 2, T, pt0x, pt0y); //画弧线上的一段,从(0 ,R )到pt1
}
else {
SetArc(Rx, Ry, R, 1, T, pt0x, pt0y);
}
//2.中间画弧段
for (int i = ins + 1; i < ine; i++) {
GetMatrixT(T1, i);
SetArc(Rx, Ry, R, 0, T1); //画整段弧线
}
//3.终止点画弧段
GetMatrixT(T, ine);
GetTtoPt(T, ex, ey, pt1x, pt1y, GL_TRUE);
if (!(ine % 2)) {
SetArc(Rx, Ry, R, 1, T, pt1x, pt1y); //画弧线上的一段,从(0 ,R )到pt1
}
else {
SetArc(Rx, Ry, R, 2, T, pt1x, pt1y);
}
}
}
//辅助绘制函数
void DrawCircleArc(GLint P1x, GLint P1y, GLint P2x, GLint P2y, GLint P3x, GLint P3y) {
GLint x, y, radius;
x = P1x, y = P1y;
radius = static_cast<GLint>(sqrt((P1x - P2x) * (P1x - P2x) + (P1y - P2y) * (P1y - P2y)));
double distance = sqrt((P3x - P1x) * (P3x - P1x) + (P3y - P1y) * (P3y - P1y));
P3x = static_cast<GLint>(((P3x - P1x) * radius)/ distance + P1x);
P3y = static_cast<GLint>(((P3y - P1y) * radius)/ distance + P1y);
DrawCircleArc(x, y, radius, P2x, P2y, P3x, P3y);
}
下/上一篇
本文来自博客园,作者:zhywyt,转载请注明原文链接:https://www.cnblogs.com/zhywyt/p/17341756.html