「实验题目」
实现基于鼠标交互的卡通人物设计与绘制,使用颜色填充与反走样技术对卡通人物外貌以及衣着进行绘制。实现对卡通人物轮廓对交互控制,点击鼠标左键可以对人物五官位置进行拖拽移动调整。并能对卡通人物进行放缩等操作。
「题目分析」
可以将这个题目分割看看。
1. 卡通人物的绘制怎么实现?
2. 怎么实现对卡通人物部位的交互?怎么知道选中的是哪个部件?
3. 剩下一些细碎的东西
圆形绘制
基于极坐标实现,通过单位圆在X、Y轴上投影的缩放得到圆形(或者椭圆)上位置的坐标。
可以选择绘制模式,是得到一个面还是一段圆弧。
void drawCircle(GLenum Mode,int N, double radius, int angleStart = 0, int angleEnd = 360, double x = 0, double y = 0, double a = 1, double b = 1) {
glBegin(Mode);
double start = (double)angleStart / 360.0*N;
double end = (double)angleEnd / 360 * N;
for (int i = start; i < end + 1; i++) {
glVertex2f(x + a * cos(i * 2 * PI / N)*radius, y + b * sin(i * 2 * PI / N) *radius);
}
glEnd();
}
三次贝塞尔曲线绘制
很简单的实现了四个控制点的三次贝塞尔曲线。
n次的贝塞尔曲线实现需要写个递归(De Casteljau算法),其实就是在控制点组成的线段上不断选取一个比例 t ∈ [ 0 , 1 ] t\in[0,1] t∈[0,1]进行插值,直到递归至只剩一个点(这个点就是我们需要绘制的贝塞尔曲线上的点)
贝塞尔曲线的最终坐标实际上是控制点坐标和伯恩斯坦多项式系数的组合(Bernstein polynomial)
P.S.想想二项式展开,其实很简单
Point setBezier(Point p1, Point p2, Point p3, Point p4,double t) {
Point p;
double a1 = pow((1 - t), 3);
double a2 = pow((1 - t), 2) * 3 * t;
double a3 = 3 * pow(t,2) * (1 - t);
double a4 = pow(t,3);
p.x = a1*p1.x + a2*p2.x + a3*p3.x + a4*p4.x;
p.y = a1*p1.y + a2*p2.y + a3*p3.y + a4*p4.y;
return p;
}
OpenGL中的实现pick操作,我们着重需要注意的是:
绘制物体代码
(在其中对名字栈进行操作)
void drawObject(GLenum mode) {
if (mode == GL_SELECT)
glPushName(0);
drawEye(leftEye[0], leftEye[1]);
if (mode == GL_SELECT)
glPushName(1);
drawEye(rightEye[0], rightEye[1]);
if (mode == GL_SELECT)
glPushName(2);
drawMouth(mouth[0], mouth[1]);
if (mode == GL_SELECT)
glPushName(3);
drawFace(face[0], face[1]);
if (mode == GL_SELECT)
glPushName(4);
drawSecondObject();
}
Pick物体的代码
#define SIZE 512
bool processHits(int x, int y) {
GLint hits;
GLint viewport[4];
GLuint selectBuf[SIZE];
glGetIntegerv(GL_VIEWPORT, viewport); //获得viewport
glSelectBuffer(SIZE, selectBuf); //初始化selectBuf数组用来保存点击结果
glRenderMode(GL_SELECT);//进入选择模式
glInitNames();//初始化名字栈
glMatrixMode(GL_PROJECTION);//进入投影阶段
glPushMatrix();//保存原来的投影矩阵
glLoadIdentity();//载入单位矩阵
//设置拾取框
gluPickMatrix(x, viewport[3] - y, 5, 5, viewport);
gluOrtho2D(-(windowWidth / 2), windowWidth / 2, -(windowHeight / 2), windowHeight /2);
drawObject(GL_SELECT);
glPopMatrix(); // 返回正常的投影变换
hits = glRenderMode(GL_RENDER);//返回选择的对象的个数
if (hits > 0) {
GLuint name, *ptr;
name = *selectBuf + 2;//选中图元在堆栈中位置,跳过两个深度信息
ptr = selectBuf + name;
if(*ptr == 0) leftEye_selected = true;
if(*ptr == 1) rightEye_selected = true;
if(*ptr == 2) mouth_selected = true;
}
glutPostRedisplay();
}
反走样/抗锯齿是图形学渲染中的一个重要的技术。因为本文不做专门讲解,这里只是粗略的提一下。OpenGL开启反走样还是比较简单的,不牵扯具体实现只是通过调用一系列函数就OK了。
Alisa/Jaggies ,这里说的走样或者说锯齿,是在空间中采样出现的问题;形象的说我们在屏幕上用离散的像素块去表示连续的图形,肯定不能表示的很圆滑,那肯定会在边缘有锯齿问题。
其他走样问题还有空间上、时间上之类的走样,根本是采样的频率跟不上函数(信号)变化的速度(牵扯到一点点信号处理的知识)这样理解就可以啦
OpenGL支持两种抗锯齿方式,但要注意的是两者不能同时使用。
混合Blend
OpenGL使用混合把像素的目标颜色与周边像素的颜色进行混合。在图元的边缘上,像素的颜色会稍微延伸到相邻的像素上,比如透过红色的玻璃去看一块儿蓝色的玻璃,我们看到的是两种颜色的混合,就会有一种半透明的效果。(有点迷之联想到MSAA的实现,超采样每个像素,维护更大的depthBuffer、frameBuffer,然后用pixel内的子像素求color均值,也是有混合)
//开启反走样,首先要开启alpha混合
//可以通过glBlendEquation来改变混合方程。默认情况下是混合方程被设置为GL_ADD.
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
//开启点反走样,线反走样,多边形反走样
glEnable(GL_POINT_SMOOTH);
glEnable(GL_LINE_SMOOTH);
glEnable(GL_POLYGON_SMOOTH);
glHint(GL_POINT_SMOOTH_HINT, GL_NICEST);
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
多重采样(multisampling)
多重采样(multisampling)实现时先要开启支持多重采样的渲染环境;
GLUT中可以在glutInitDisplayMode中,增加一个GLUT_MULTISAMPLE字段来获得一个多重采样的渲染环境。
glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGB | GLUT_DEPTH | GLUT_MULTISMAPLE);
然后开启/关闭多重采样
glEnable(GL_MULTSAMPLE); //glDisable(GL_MULTSAMPLE);
OpenGL在实现多重采样时会在已经包含了颜色、深度之类信息的帧缓冲区中添加一个额外的缓冲区,所有的图元在每个像素上都进行多次采样,其结果就存储在这个新增的缓冲区中。
每次当像素进行更新时,就会针对这些采样值进行解析,以产生一个单独的值。这个处理属于是在“幕后发生的事情”,它会带来额外的内存和处理器开销,会对性能造成一定的影响。因此,有些OpenGL实现可能并不支持多渲染环境中的多重采样处理。
最后再注意一遍。多重采样和混合不能同时开启, 这两种方法只能互斥使用。即使用任何一个方法前需要禁用另一个方法。
放缩可以使用函数 glScalef( ) 在x、y、z三个方向上进行缩放;不过这里选择了另一种缩放方式。比如说在绘制圆时传入一个全局的缩放参数scale乘以半径,再在监听keyboard事件时对全局变量scale进行加加减减就可以简单的实现全局缩放了。
旋转的话,基于glRotatef( )实现绕三轴的旋转,就不多赘述了。
基于变量实现的缩放
void drawFace(int x,int y) {
rotate(theta);
//脸部
glColor3f(1.0f,0.85f,0.372f);
drawCircle(GL_POLYGON,200, scale*150, 0, 360, scale*x, scale*y, 1, 1);
//脸部轮廓
glColor3f(0.8f,0.572f,0.36f);
glLineWidth(scale*7);
drawCircle(GL_LINE_STRIP,360,scale*150,0,360,scale*x,scale*y,1,1);
关于拖动功能,可以使用glTranslatef()实现。
这里采用的是,根据鼠标点击选中的物体,决定对哪个部件进行拖动。记录点击位置和绘制物体部件时中心的一段偏移向量,在mouseDrag时,从返回位置向上找这么一段偏移向量,在这个最终位置重绘就可以了。这样操作可以保证物体紧跟鼠标点击位置拖动,不会有一种飘忽的bug。
mouseClick
根据点击位置判断选中物体,并保存相应的一段偏移向量
void mouseClick(int btn, int state, int x, int y) {
//按下pick物体,开启drag绘制
if (btn == GLUT_LEFT_BUTTON && state == GLUT_DOWN) {
processHits(x, y);//返回选中物体
if (leftEye_selected) {
fixVectorX = leftEye[0] - (x - windowWidth / 2);
fixVectorY = leftEye[1] - (windowHeight / 2 - y);
}
if (rightEye_selected) {
fixVectorX = rightEye[0] - (x - windowWidth / 2);
fixVectorY = rightEye[1] - (windowHeight / 2 - y);
}
if (mouth_selected) {
fixVectorX = mouth[0] - (x - windowWidth / 2);
fixVectorY = mouth[1] - (windowHeight / 2 - y);
}
}
//抬起取消drag绘制
if (btn == GLUT_LEFT_BUTTON && state == GLUT_UP) {
leftEye_selected = false;
rightEye_selected = false;
mouth_selected = false;
}
}
mouseDrag
在拖动位置往偏移向量方向上找回一段重绘
/鼠标拖拽事件
void mouseDrag(int x, int y) {
int dragX = x - windowWidth / 2;
int dragY = windowHeight / 2 - y;
if (leftEye_selected) {
leftEye[0] = fixVectorX + dragX;
leftEye[1] = fixVectorY + dragY;
}
if (rightEye_selected) {
rightEye[0] = fixVectorX + dragX;
rightEye[1] = fixVectorY + dragY;
}
if (mouth_selected) {
mouth[0] = fixVectorX + dragX;
mouth[1] = fixVectorY + dragY;
}
draw();
}
没有小结,,,,
完整代码资源戳这里️,欢迎进行详细学习或魔改