有了之前的基础我们现在一步一步制作一个手部模型的数据集。
工程在这里代码。
先看看整个工程的结构
主程序在(1)中,调用一个工具类(2)编译管线(3),绘制模型生成彩图和深度图,保存在(4)中,用matlab代码(5)可以显示结果(6).
主程序tutorial17.cpp的结构
代码以main函数为起点,完成的工作包含:
- 用GLUT创建窗口,因为opengl不负责窗口创建,所以用了GLUT的库
- 创建在窗口中绘制模型的对象pAPP
- 在pAPP的init()函数中执行初始化
- 在pAPP的run()函数中执行渲染
接下来我们分别讲解构造函数、init函数和run函数的实现细节
int main(int argc, char** argv)
{
// Magick::InitializeMagick(*argv);
GLUTBackendInit(argc, argv, false, false);
if (!GLUTBackendCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, false, "Tutorial 17")) {
return 1;
}
Tutorial17* pApp = new Tutorial17();
if (!pApp->Init()) {
return 1;
}
pApp->Run();
delete pApp;
return 0;
}
构造函数
比较繁琐,直接看注释
Tutorial17()
{
m_pGameCamera = NULL; //控制相机的对象
m_pTexture = NULL; //纹理对象(暂时没有用到)
m_pEffect[0] = NULL; //管线1
m_pEffect[1] = NULL; //管线2
m_scale = 0; //模型旋转变换尺度(测试时用到)
m_directionalLight.Color = Vector3f(1.0f, 1.0f, 1.0f);//光照(暂时没有用到)
m_directionalLight.AmbientIntensity = 0.5f;
m_persProjInfo.FOV = 60.0f; //透视投影的相机视角
m_persProjInfo.Height = WINDOW_HEIGHT; //透视投影的屏幕高度、宽度
m_persProjInfo.Width = WINDOW_WIDTH;
m_persProjInfo.zNear = 1.0f; //透视投影的近点
m_persProjInfo.zFar = 100.0f; //透视投影的远点
colorArr = new GLbyte[ WINDOW_WIDTH * WINDOW_HEIGHT * 3 ];//彩色图像和深度图像的存储
depthArr = new GLbyte[ WINDOW_WIDTH * WINDOW_HEIGHT * 1 ];
change_scale = 0.1f; //相机变换尺度
/*read file*/
read_ball_get_data(ball_Heart,ball_radius,ball_link);
//读取模型参数,包括球心位置、半径、球之间的联系等
}
Init()
比较繁琐,直接看注释
bool Init()
{
//设置相机三个参数,包括位置,指向,垂线
Vector3f Pos(0.0f, 0.0f, -3.0f);
Vector3f Target(0.0f, 0.0f, 1.0f);
Vector3f Up(0.0, 1.0f, 0.0f);
m_pGameCamera = new Camera(WINDOW_WIDTH, WINDOW_HEIGHT, Pos, Target, Up);
//创建顶点缓冲区,通过它,把顶点数据从内存拷贝到GPU
CreateVertexBuffer();
//CreateIndexBuffer();//暂时不需要索引绘制
//创建两个渲染管线
m_pEffect[0] = new LightingTechnique();
m_pEffect[1] = new LightingTechnique("green.fs");
if (!m_pEffect[0]->Init()){return false;}
if (!m_pEffect[1]->Init()){return false;}
glClearColor(0.0f, 0.0f, 0.0f, 0.0f); //清空颜色缓冲区,设置窗口背景颜色为黑色
glClearDepth(1.0f); // 清空深度缓冲区
glEnable(GL_DEPTH_TEST); //允许深度测试
glDepthFunc(GL_LEQUAL); // GL_LEQUAL,如果输入的深度值小于或等于参考值,则通过
return true;
}
Run()
这里会循环调用渲染回调函数——RenderSceneCB(),这个函数会根据之前做好的设置执行绘制工作,接下来解读RenderSceneCB()这个函数
void Run()
{
GLUTBackendRun(this);
}
RenderSceneCB()
根据红宝书中的知识,opengl中顶点变换的过程如下:
这一个个的变换最终体现为一个个相乘的矩阵,我们把它封装在 Pipeline p中,最终会和顶点坐标一起传递给渲染管线,绘制出变换后的模型
首先我们把相机参数和透视投影参数传递给p,计算投影矩阵
m_pGameCamera->OnRender();
glClear(GL_COLOR_BUFFER_BIT);
glClear(GL_DEPTH_BUFFER_BIT);
m_scale += 2;
m_scale = m_scale % 360;
p.SetCamera(m_pGameCamera->GetPos()+m_pos_a, m_pGameCamera->GetTarget()+m_target_a, m_pGameCamera->GetUp()+m_up_a);
p.SetPerspectiveProj(m_persProjInfo);
接着我们把球画出来,这里用到了构造函数中最后一行读取:球心坐标ball_Heart、球的半径ball_radius、球之间的联系ball_link。根据球心坐标设置球在世界坐标系中的位置,根据半径绘制球。
第一行中的m_pEffect[0]->Enable();指定了绘制球时用到的渲染管线,其中vs设置了坐标(vs中用到了两个数据:点的坐标和变换矩阵。)、fs设置了绘制的颜色(写死在了fs中)。
for(int i=0;i<BALL_NUM;i++){
m_pEffect[0]->Enable();
//p.Rotate(m_scale,m_scale, 0.0f);
p.WorldPos(ball_Heart[i][0], ball_Heart[i][1], ball_Heart[i][2]);
m_pEffect[0]->SetWVP(p.GetWVPTrans());
glutWireSphere(ball_radius[i], slices, stacks);
}
接下来是比较麻烦的,绘制联系两个球的圆台,由于没有找到可直接用的绘制圆台的函数,我们只能手工绘制一个个的点,然后连起来。
首先创建顶点缓冲区,这会写入GPU。根据构造函数中最后一行读取:球心坐标ball_Heart、球的半径ball_radius、球之间的联系ball_link,我们首先根据link的数量创建顶点数组VAO,接下来对于每个link,根据联系的两个球的半径和球心坐标,计算圆台上下两个圆的采样点坐标,将坐标数组与VAO[i]绑定。
这里有两个比较重要的函数,就是他们告诉渲染管线如何解读存放在GPU中的VAO
glEnableVertexAttribArray(0);
/*在shader着色器教程中,可看到顶点着色器中使用的属性(位置、法线等)有索引来对它们进行映射,
* 使你能够绑定C/C++程序中的数据和着色器中的属性名称,而且必须要为每一个顶点属性添加索引。
* 在这个教程暂时不会使用任何着色器,但是
* 我们加载到buffer中的顶点位置在固定功能管线中是被认为是索引为0的顶点属性(当没有着色器绑定时被启用)。
* 你必须开启每一个顶点的属性,否则渲染管线无法获取这些数据。*/
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
/*
这个回调告诉管线怎样解析bufer中的数据。
第1个参定义了属性的索引,再这个例子中我们知道这个索引默认是0,但是当我们开始使用shader着色器的时候,我们既要明确的设置着色器中的属性索引同时也要检索它;
第2个参数指的是属性中的元素个数(3个表示的是:X,Y,Z坐标);
第3个参数指的是每个元素的数据类型;
第4个参数指明我们是否想让我们的属性在被管线使用之前被单位化,我们这个例子中希望我们的数据保持不变的被传送;
第5个参数(称作’stride‘)指的是缓冲中那个属性的两个实例之间的比特数距离。当只有一个属性(例如:buffer只含有一个顶点的位置数据)并且数据被紧密排布的时候将该参数值设置为0。如果我们有一个包含位置和法向量(都是有三个浮点数的vector向量,一共6个浮点数)两个属性的数据结构的数组的时候,我们将设置参数值为这个数据结构的比特大小(6*4=24);
最后一个参数在前一个例子中非常有用。我们需要在管线发现我们的属性的地方定义数据结构中的内存偏移值。在有位置数据和法向量数据的结构中,位置的偏移量为0,而法向量的偏移量则为12。
*/
void CreateVertexBuffer()
{
glGenVertexArrays(LINK_NUM, VAO);
glGenBuffers(LINK_NUM, m_VBO);
int i = 0;
Vector3f Vertices2[391*2];//GL_QUAD_STRIP
for(i=0;i<LINK_NUM;i++){
int ball1 = ball_link[i*2];
int ball2 = ball_link[i*2+1];
float r1 = ball_radius[ball1], r2 = ball_radius[ball2],
h = sqrt((ball_Heart[ball1][0]-ball_Heart[ball2][0])*(ball_Heart[ball1][0]-ball_Heart[ball2][0])
+(ball_Heart[ball1][1]-ball_Heart[ball2][1])*(ball_Heart[ball1][1]-ball_Heart[ball2][1])
+(ball_Heart[ball1][2]-ball_Heart[ball2][2])*(ball_Heart[ball1][2]-ball_Heart[ball2][2]));
//printf("%f %f %f \n", r1,r2,h);
for (int j = 0; j <= 390; j += 1)
{
float p = j * 3.14 / 180;
Vertices2[j*2] = Vector3f(r1*sin(p), 0.0f, r1*cos(p));
Vertices2[j*2+1] = Vector3f(r2*sin(p), h, r2*cos(p));
}
glBindVertexArray(VAO[i]);
glBindBuffer(GL_ARRAY_BUFFER, m_VBO[i]);
glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices2), Vertices2, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
}
}
创建完绘制圆台所需的VAO后,我们就可以回到RenderSceneCB()中继续看怎么绘制圆台了。
这里主要的工作是计算圆台平移量和旋转角度(这些值会记录在p中,传入渲染管线)
我们计算出从球1指向球2的单位向量(r2p_x,r2p_y,r2p_z),根据这个向量计算旋转角。圆台的轴线初始状态下是指向y轴的,我们的目标是让轴线绕x轴旋转,再绕z轴旋转,直到与r2重合。
首先排除在X轴上的情况。
然后如果不在X轴上,那么计算绕X轴的旋转角。这里有一个容易出错的地方,第二步中绕Z轴旋转,旋转过程可以看成一个圆锥面,这个圆锥面与yoz平面的交线才是绕x轴旋转时应该到达的地方。
接着,我们发现如果绕x轴旋转角度为90°或者270°是形成了万向锁,绕z轴旋转就没什么卵用了,这时需要绕y轴旋转
排除了万向锁的可能后,计算绕z轴旋转的角度
for(num =0;num<LINK_NUM;num++){
m_pEffect[1]->Enable();
glBindVertexArray(VAO[num]);
int ball1 = ball_link[num*2];
int ball2 = ball_link[num*2+1];
//float r1[3] =[ball_Heart[ball1][0],ball_Heart[ball1][1],ball_Heart[ball1][2]];
//float r2[3] =[ball_Heart[ball2][0],ball_Heart[ball2][1],ball_Heart[ball2][2]];
float r1x =ball_Heart[ball1][0];
float r1y =ball_Heart[ball1][1];
float r1z =ball_Heart[ball1][2];
float r2x =ball_Heart[ball2][0];
float r2y =ball_Heart[ball2][1];
float r2z =ball_Heart[ball2][2];
float r2p_x = r2x - r1x;
float r2p_y = r2y - r1y;
float r2p_z = r2z - r1z;
float lenth = sqrt(r2p_x*r2p_x + r2p_y*r2p_y+ r2p_z*r2p_z);
r2p_x = r2p_x/lenth;
r2p_y = r2p_y/lenth;
r2p_z = r2p_z/lenth;
printf("得到单位化向量:x:%f y:%f z:%f\n",r2p_x,r2p_y,r2p_z);//得到单位化向量
if(r2p_y==0&&r2p_z==0){//在x轴上
if(r2p_x>0){
p.Rotate(0,0,270);
p.WorldPos(r1x, r1y, r1z);
m_pEffect[1]->SetWVP(p.GetWVPTrans());
glDrawArrays(GL_QUAD_STRIP, 0,391*2);
}else if(r2p_x<0){
p.Rotate(0,0,90);
p.WorldPos(r1x, r1y, r1z);
m_pEffect[1]->SetWVP(p.GetWVPTrans());
glDrawArrays(GL_QUAD_STRIP, 0,391*2);
}else{continue;}
}else{//只要不在x轴上就计算绕x轴旋转角度
//绕Z轴旋转时的圆锥半径,r2p_x_O的目标是找到这个圆锥,z>0时,在0~90之间。z<0时,在180~270之间
float z_r = sqrt(r2p_y*r2p_y+r2p_x*r2p_x);
float r2p_x_O = acos(z_r)*180/PI;
if(r2p_z<=0){r2p_x_O = 180 + r2p_x_O;}
if(z_r==0){//在Z轴上,r2p_x_O=90or270,产生了万向锁,不过也不需要计算Z轴旋转量了
//绕x轴y轴旋转,计算角度
//float r2p_y_O = acos(r2p_z/(sqrt(r2p_x*r2p_x+r2p_z*r2p_z)))*180/PI;
//if(r2p_z >= 0&&r2p_x>=0){r2p_y_O = 360 - r2p_y_O;}
//if(r2p_z <= 0&&r2p_x>=0){r2p_y_O = 180 - r2p_y_O;}
//if(r2p_z >= 0&&r2p_x<=0){r2p_y_O = r2p_y_O;}
//if(r2p_z <= 0&&r2p_x<=0){r2p_y_O = 180 + r2p_y_O;}
//printf("x:%f y:%f\n",r2p_x_O,r2p_y_O);
p.Rotate(r2p_x_O,0,0);
p.WorldPos(r1x, r1y, r1z);
m_pEffect[1]->SetWVP(p.GetWVPTrans());
glDrawArrays(GL_QUAD_STRIP, 0,391*2);
}else{//没有万向锁,就可以放心绕x轴z轴旋转
float new_y =cos(r2p_x_O * PI / 180), new_z =sin(r2p_x_O * PI / 180);
printf("绕x轴旋转后的矢量n_y:%f n_z:%f\n",new_y,new_z);
float ans = r2p_y/(sqrt(r2p_y*r2p_y+r2p_x*r2p_x));
float r2p_z_O = acos(ans)*180/PI;
if(new_y >= 0&&r2p_x>=0){r2p_z_O = 360-r2p_z_O;}
if(new_y >= 0&&r2p_x<=0){r2p_z_O = r2p_z_O;}
if(new_y <= 0&&r2p_x>=0){r2p_z_O = 180- r2p_z_O;}
if(new_y <= 0&&r2p_x<=0){r2p_z_O = 180 + r2p_z_O;}
printf("x:%f z:%f\n",r2p_x_O,r2p_z_O);
p.Rotate(r2p_x_O,0,r2p_z_O);
//p.Rotate(120,20,0);
printf("%d \n",m_scale);
p.WorldPos(r1x, r1y, r1z);
m_pEffect[1]->SetWVP(p.GetWVPTrans());
glDrawArrays(GL_QUAD_STRIP, 0,391*2);
}
}
}
PS:我们在计算圆台定点的时候偷懒直接用了球心坐标,其实这样是有误差的,应该用红线标的地方