使用OpenGL绘制一个地球
一、程序简介
本项目使用 OpenGL 实现了球型3D模型的生成和渲染;进行了纹理贴图,同时使用了多个纹理,渲染了地球围绕太阳旋转的场景,并添加了简单的光照效果。
二、程序实现
1. 生成3D球模型并绘制
球的三维坐标表示为:
绘制带有纹理的球的难度问题在于:我们无法将这样一个连续的方程用计算机绘制出来,因此我们要进行离散化。若使用球的坐标方程进行离散化,其离散后两点之前的弧长并不一致,因此,引入球的参数坐标方程进行离散化。以表示某一点的坐标且,定义到的转换如下:
我们声明一下步长uStep和vStep,即每一次在u轴上和v轴上怎加的值,这个步长是由两个整数uStepNum和vStepNumber决定的,即在每个方向上从0增加几次到1.0,这样保证了球恰好被渲染出来。通过这个参数坐标我们可以进行均匀的离散化,得到一个在单位正方形内均匀分布的(u,v)点阵,然后我们通过上述公式进行转换。
//转换代码
Point getPoint(double u, double v){
double x = sin(PI * v)*cos(2 * PI * u);
double y = sin(PI * v)*sin(2 * PI * u);
double z = cos(PI * v);
return Point(x, y, z);
}
//生成球的模型信息
void getSphere(){
double ustep = 1 / (double)ustepNum, vstep = 1 / (double)vstepNum;
points[0] = getPoint(0, 0);
coords[0] = 0.5;
coords[1] = 1;
int c = 1;
for (int i = 1; i < vstepNum; i++)
{
for (int j = 0; j <= ustepNum; j++)
{
points[c] = getPoint(ustep*j, vstep*i);
//记录纹理坐标
coords[2 * c] = ustep*j;
coords[2 * c + 1 ] = vstep*i;
c++;
}
}
points[c] = getPoint(1, 1);
//...
//生成三角面片定点索引信息
}
有了球面上所有顶点的坐标信息,我们怎么画出一个球面呢,可以通过将这些点组成一个个三角面片,通过openGL内置画三角形的函数进行绘制,因此我们来进行三角形对于顶点的生成。
一个由三角形组成的“球”
通过转化公式我们可以发现,离散化后的点有一个特征:当v一定时,其z值是一定的,u值的变化会导致x和y值的变化,这样可以理解为球面与多个x-y平面相交的被切成了很多“圆”,每一个圆上又被均匀的细分成了多个点。这对我们考虑球模型三角面片的索引和纹理贴图坐标有很大的帮助。
首先我们考虑两个特殊的点,其参数坐标中v值为0和1,这对应着z=0和z=1的上下两个顶点,在这里,三角面片的序号顺寻应该是顶点和离顶点最近的两个圆上相邻的两个顶点组合而成。
实现:
for (int i = 0; i <= ustepNum ; i++)//球体上第一层
{
indexes.push_back(0);//上顶点总是第一个点
indexes.push_back(1 + i);
indexes.push_back(1 + i + 1);
};
int last = 1 + (ustepNum+1) * (vstepNum - 1);
int start = 1 + (ustepNum+1) * (vstepNum - 2);
for (int i = 1 + (ustepNum+1) * (vstepNum - 2); i < 1 + ustepNum * (vstepNum - 1); i++)//球体上最后一层
{
indexes.push_back(i);
indexes.push_back(last); //逆时针排列
indexes.push_back(last + 1);
}
而在中间的部分则是相邻两个圆之间由若干个矩形组成的,一个矩形又由俩三角组成。
实现:
for (int i = 1; i < vstepNum - 1; i++){
int start = 1 + (i - 1)*(ustepNum+1);
for (int j = start; j < start + (1+ustepNum); j++){
/*
* j
* |\
* | \
* |__\
*
*/
indexes.push_back(j);
indexes.push_back(j + ustepNum + 1);
indexes.push_back(j + ustepNum + 2);
/*
*
* j __
* \ |
* \ |
* \|
*
*/
indexes.push_back(j);
indexes.push_back(j+1);
indexes.push_back(j + ustepNum + 2);
}
}
至此,我们生成了一个球的模型。其点信息存放在points向量中。
通过这些信息,我们可以通过OpenGl中绘制三角形的函数,并配合EBO、VAO、VBO的使用,绘制这个球。
2. 纹理贴图
之前我们获得了求模型各个点的坐标信息和球面上各个三角面片的顶点索引信息,要给球体贴上纹理,最重要的是找到球体局部坐标系中各个点的三维坐标和纹理各个二维点上的映射关系。
之前我们也提到了球体参数坐标(u,v)到球体空间坐标(x,y,z)的映射,这里我们不妨使用其逆映射关系(x,y,z)->(u,v)进行三维到二维的映射。
earth.jpg
其中需要特殊考虑的还是两个顶点,由于球中的顶点铺平到二维平面后是(v=1或v=0)一条线。由于无法用一个点包含到一条线所有的信息,因此一开始考虑在线上进行随机采样并取其平均值。后观察 后发现,题目中所提供的图片在v=0或v=1时,其颜色基本一致,故最终顶点映射到了两条线上的中点。
实现: 我们在第一步进行离散化时,已知(u,v)到(x,y,z)的映射,因此只需记录在离散化时进行记录即可。
double ustep = 1 / (double)ustepNum, vstep = 1 / (double)vstepNum;
points[0] = getPoint(0, 0);
coords[0] = 0.5;
coords[1] = 1;
int c = 1;
for (int i = 1; i < vstepNum; i++)
{
for (int j = 0; j <= ustepNum; j++)
{
points[c] = getPoint(ustep*j, vstep*i);
//记录纹理坐标
coords[2 * c] = 1 - ustep*j;
coords[2 * c + 1 ] = vstep*i;
c++;
}
}
points[c] = getPoint(1, 1);
通过传入纹理坐标,再传入将纹理通过采样器传进fragmentShader,我们就可以得到一个“地球”。
项目地址:
https://github.com/tjuwhy/OpenGLDemo
所有三角形绘制和纹理贴图相关实现均参考于 OpenGL 官方学习文档