一、圆柱,圆盘,圆锥的绘制
绘制一个物体,我们首先得确定其的顶点数据,再将顶点数据送入着色器进行绘制
圆柱的绘制
圆柱是由两个圆形以及一个矩形绘制成的图形,那么我们所要确立的顶点也就显而易见了,那便是上下两个圆的分割点,再由足够多上下四个点构成的两个三角面片绘制成侧面即可。
大致的图片已经贴出如下
代码如下(示例):
generateCylinder(int num_division, float radius, float height)
我们先来看下获取圆柱顶点函数所需要的参数
1,顶点的个数(这个决定了上下两个圆面的圆滑程度)
2,上下两个圆面的半径
3,上下两个圆片的高度
int num_samples = num_division;
float step = 2 * M_PI / num_samples; // 每个切片的弧度
在画圆的时候,如果采用直角坐标的表示法将会特别麻烦,所以这里采用了参数方程(或者极坐标),在圆的方程里,极坐标和参数方程的形式是一样的,所以这里可以随意选一个
那么,我们首先得获取的每个点的度数(代码如上)
// 按cos和sin生成x,y坐标,z为负,即得到下表面顶点坐标
// 顶点, 纹理
float z = -height;
for (int i = 0; i < num_samples; i++)
{
float r_r_r = i * step;
float x = radius * cos(r_r_r);
float y = radius * sin(r_r_r);
// 添加顶点坐标
vertex_positions.push_back(vec3(x, y, z));
vertex_normals.push_back( normalize(vec3(x, y, 0)));
// 这里颜色和法向量一样
vertex_colors.push_back( normalize(vec3(x, y, 0)));
}
// 按cos和sin生成x,y坐标,z为正,即得到上表面顶点坐标
z = height;
for (int i = 0; i < num_samples; i++)
{
float r_r_r = i * step;
float x = radius * cos(r_r_r);
float y = radius * sin(r_r_r);
vertex_positions.push_back(vec3(x, y, z));
vertex_normals.push_back( normalize(vec3(x, y, 0)));
vertex_colors.push_back( normalize(vec3(x, y, 0)));
}
再由极坐标公式可得到每个顶点的顶点坐标,获取后将其压进vector_position即可(这个容器类是由vector定义的,详情使用方法可自行百度),其余两个分别是存颜色和法向量的容器(用法类似)
由于有上下两个表面,所以这里得获取到上下两个圆面的坐标,在opengl中的中点坐标为0,所以上下两个高度分别为正负height
面片的获取
for (int i = 0; i < num_samples; i++)
{
// 面片1
faces.push_back(vec3i(i, (i + 1) % num_samples, (i) + num_samples));
// 面片1对应的顶点的纹理坐标
vertex_textures.push_back(vec2(1.0 * i / num_samples, 0.0));
vertex_textures.push_back(vec2(1.0 * (i+1) / num_samples, 0.0));
vertex_textures.push_back(vec2(1.0 * i / num_samples, 1.0));
// 对应的三角面片的纹理坐标的下标
texture_index.push_back( vec3i( 6*i, 6*i+1, 6*i+2 ) );
// 面片2
faces.push_back(vec3i((i) + num_samples, (i + 1) % num_samples, (i + num_samples + 1) % (num_samples) + num_samples));
// 面片2对应的顶点的纹理坐标
vertex_textures.push_back(vec2(1.0 * i / num_samples, 1.0));
vertex_textures.push_back(vec2(1.0 * (i+1) / num_samples, 0.0));
vertex_textures.push_back(vec2(1.0 * (i+1) / num_samples, 1.0));
// 对应的三角面片的纹理坐标的下标
texture_index.push_back( vec3i( 6*i+3, 6*i+4, 6*i+5 ) );
}
每三个点组成一个三角面片,这里i,(i+1)%num_sample,(i)+num_samples代表的顶点的序号(即下标,因为顶点以及被存进了vector里)
为什么要取余
因为当我们取到第num_sample个(最大序号)点时,num_sample+1点越界了,而我们实际上想得到的下一个点应该是取回第一个点,所以这里应当使用取余操作
i+num_sample
因为我们所要的面片是以2+1上面2个点,下面一个点的形式获取,那么i+num_sample便显而易见了,那便是i点所对应的的下表面的一个点
这样,我们便获取到了所需要的一个面片
下面的vertex_texture则是纹理坐标的获取,这个将于下文中进行阐述
完整代码如下:
void TriMesh::generateCylinder(int num_division, float radius, float height)
{
cleanData();
int num_samples = num_division;
float step = 2 * M_PI / num_samples; // 每个切片的弧度
// 按cos和sin生成x,y坐标,z为负,即得到下表面顶点坐标
// 顶点, 纹理
float z = -height;
for (int i = 0; i < num_samples; i++)
{
float r_r_r = i * step;
float x = radius * cos(r_r_r);
float y = radius * sin(r_r_r);
// 添加顶点坐标
vertex_positions.push_back(vec3(x, y, z));
vertex_normals.push_back( normalize(vec3(x, y, 0)));
// 这里颜色和法向量一样
vertex_colors.push_back( normalize(vec3(x, y, 0)));
}
// 按cos和sin生成x,y坐标,z为正,即得到上表面顶点坐标
z = height;
for (int i = 0; i < num_samples; i++)
{
float r_r_r = i * step;
float x = radius * cos(r_r_r);
float y = radius * sin(r_r_r);
vertex_positions.push_back(vec3(x, y, z));
vertex_normals.push_back( normalize(vec3(x, y, 0)));
vertex_colors.push_back( normalize(vec3(x, y, 0)));
}
// 面片生成三角面片,每个矩形由两个三角形面片构成
for (int i = 0; i < num_samples; i++)
{
// 面片1
faces.push_back(vec3i(i, (i + 1) % num_samples, (i) + num_samples));
// 面片1对应的顶点的纹理坐标
vertex_textures.push_back(vec2(1.0 * i / num_samples, 0.0));
vertex_textures.push_back(vec2(1.0 * (i+1) / num_samples, 0.0));
vertex_textures.push_back(vec2(1.0 * i / num_samples, 1.0));
// 对应的三角面片的纹理坐标的下标
texture_index.push_back( vec3i( 6*i, 6*i+1, 6*i+2 ) );
// 面片2
faces.push_back(vec3i((i) + num_samples, (i + 1) % num_samples, (i + num_samples + 1) % (num_samples) + num_samples));
// 面片2对应的顶点的纹理坐标
vertex_textures.push_back(vec2(1.0 * i / num_samples, 1.0));
vertex_textures.push_back(vec2(1.0 * (i+1) / num_samples, 0.0));
vertex_textures.push_back(vec2(1.0 * (i+1) / num_samples, 1.0));
// 对应的三角面片的纹理坐标的下标
texture_index.push_back( vec3i( 6*i+3, 6*i+4, 6*i+5 ) );
}
// 三角面片的每个顶点的法向量的下标,这里和顶点坐标的下标 faces是一致的,所以我们用faces就行
normal_index = faces;
// 三角面片的每个顶点的颜色的下标
color_index = faces;
storeFacesPoints();
}
圆盘的绘制
通过圆柱的绘制方式,我们不难猜想到,圆盘的绘制是由一个中心点以及圆周的分割点,以一个圆心+两个边上的点形成的三角面片所组成
void TriMesh::generateDisk(int num_division, float radius)
函数的定义类似,但在这里我们少了一个参数,那便是高度,这里的绘制方法并不是一个三维物体的绘制方法,而是以一个二维的圆片来近似的,那么我们便不再需要高度
int num_samples = num_division;
float step = 2 * M_PI / num_samples; // 每个切片的弧度
//先压原点
for (int i = 0; i < num_samples; i++)
{
float r_r_r = i * step;
float x = radius * cos(r_r_r)+0.5;
float z = radius * sin(r_r_r);
vertex_positions.push_back(vec3(x,0, z));
vertex_normals.push_back(normalize(vec3(x, 0, z)));
vertex_colors.push_back(normalize(vec3(x, 0, z)));
}
vec3 pos(0.5, 0, 0);
vertex_positions.push_back(pos);
vertex_normals.push_back(normalize(pos));
vertex_colors.push_back(normalize(pos));
获取圆上顶点的方法以及在上面圆柱给出,这里只需要把y设置为0,z代替原来的y来实现即可(因为物体是置于y=0这个平面的),这里有一个细节(女生所要男生的细节无处不在),那便是圆盘的中心点坐标我们是最后存进vector里面的,因为我们如果是在第一个存,那么他的下标便是0,当我们在取余数返回第一个点时就会出现错误,所以我们应当把中心点在最后存储
for (int i = 0; i < num_samples; i++)
{
float r_r_rs = (i + 1) % num_samples * step;
float r_r_r = i * step;
// 面片1
faces.push_back(vec3i(num_samples, (i + 1) % num_samples, (i) % num_samples));
// 面片1对应的顶点的纹理坐标
vertex_textures.push_back(vec2(0.5, 0.5));
vertex_textures.push_back(vec2(0.5 * cos(r_r_rs) + 0.5, 0.5 * sin(r_r_rs)+ 0.5));
vertex_textures.push_back(vec2(0.5 * cos(r_r_r) + 0.5, 0.5 * sin(r_r_r) + 0.5));//利用到上面的弧度
// 对应的三角面片的纹理坐标的下标
texture_index.push_back(vec3i(3 * i, 3 * i + 1, 3 * i + 2));//循环一次压进去3个顶点坐标
}
获取方法也和圆柱类似,重点的是我们的第一个值不能变,保持为num_sample,,因为第一个点永远是我们的圆心坐标在数组里的下标,对应第四行代码
完整代码
void TriMesh::generateDisk(int num_division, float radius)
{
cleanData();
// @TODO: Task2 请在此添加代码生成圆盘
int num_samples = num_division;
float step = 2 * M_PI / num_samples; // 每个切片的弧度
//先压原点
for (int i = 0; i < num_samples; i++)
{
float r_r_r = i * step;
float x = radius * cos(r_r_r)+0.5;
float z = radius * sin(r_r_r);
vertex_positions.push_back(vec3(x,0, z));
vertex_normals.push_back(normalize(vec3(x, 0, z)));
vertex_colors.push_back(normalize(vec3(x, 0, z)));
}
vec3 pos(0.5, 0, 0);
vertex_positions.push_back(pos);
vertex_normals.push_back(normalize(pos));
vertex_colors.push_back(normalize(pos));
// 面片生成三角面片,每个矩形由两个三角形面片构成
for (int i = 0; i < num_samples; i++)
{
float r_r_rs = (i + 1) % num_samples * step;
float r_r_r = i * step;
// 面片1
faces.push_back(vec3i(num_samples, (i + 1) % num_samples, (i) % num_samples));
// 面片1对应的顶点的纹理坐标
vertex_textures.push_back(vec2(0.5, 0.5));
vertex_textures.push_back(vec2(0.5 * cos(r_r_rs) + 0.5, 0.5 * sin(r_r_rs)+ 0.5));
vertex_textures.push_back(vec2(0.5 * cos(r_r_r) + 0.5, 0.5 * sin(r_r_r) + 0.5));//利用到上面的弧度
// 对应的三角面片的纹理坐标的下标
texture_index.push_back(vec3i(3 * i, 3 * i + 1, 3 * i + 2));//循环一次压进去3个顶点坐标
}
// 三角面片的每个顶点的法向量的下标,这里和顶点坐标的下标 faces是不一致的,要修改
normal_index = faces;
// 三角面片的每个顶点的颜色的下标
color_index = faces;
storeFacesPoints();
}
圆锥的绘制
圆锥的绘制则是在圆柱的基础上,上表面改为一个点即可
由数学知识可得,圆锥的展开图便是一个扇形,那么我们只要用上顶点作为圆心,按照上面圆的绘制方法即可完成绘制
这里就直接贴完整代码了
void TriMesh::generateCone(int num_division, float radius, float height)
{
cleanData();
// @TODO: Task2 请在此添加代码生成圆锥体
int num_samples = num_division;
float step = 2 * M_PI / num_samples;
// 顶点, 纹理
float z = height;
for (int i = 0; i < num_samples; i++)
{
float r_r_r = i * step;
float x = radius * cos(r_r_r)+0.8;
float y = radius * sin(r_r_r)+0.8;
// 添加顶点坐标
vertex_positions.push_back(vec3(x, y, z));
vertex_normals.push_back(normalize(vec3(x, y, 0)));
// 这里颜色和法向量一样
vertex_colors.push_back(normalize(vec3(x, y, 0)));
}
// 按cos和sin生成x,y坐标,z为正,即得到上表面顶点坐标
z = -height;
vertex_positions.push_back(vec3(0.75, 0.75, z));
vertex_normals.push_back(normalize(vec3(0.75, 0.75, 0)));
vertex_colors.push_back(normalize(vec3(0.75, 0.75, 0)));
//上表面只有一个点
// 面片生成三角面片,每个矩形由两个三角形面片构成
for (int i = 0; i < num_samples; i++)
{
// 面片1
faces.push_back(vec3i(num_samples, (i + 1) % num_samples, (i)%num_samples));
// 面片1对应的顶点的纹理坐标
vertex_textures.push_back(vec2(0.5, 1.0));
vertex_textures.push_back(vec2(1.0 * (i + 1) / num_samples, 0.0));
vertex_textures.push_back(vec2(1.0 * i / num_samples, 0.0));
// 对应的三角面片的纹理坐标的下标
texture_index.push_back(vec3i(3 * i, 3 * i + 1, 3* i + 2));
}
// 三角面片的每个顶点的法向量的下标,这里和顶点坐标的下标 faces是一致的,所以我们用faces就行
normal_index = faces;
// 三角面片的每个顶点的颜色的下标
color_index = faces;
storeFacesPoints();
}
二、纹理贴图
1.什么是纹理
简单的说,上面我们画了三个几何体,那么纹理就是让我们把三张照片贴到上面几个几何体上
所以第一步便是获取我们的图片
代码如下(示例):
painter->addMesh(cylinder, "mesh_a", "./assets/cylinder10.jpg", vshader, fshader); // 指定纹理与着色器
这里我们封装了addMesh这个函数,因为我们不可能只用一个单一的纹理
void MeshPainter::addMesh( TriMesh* mesh, const std::string &name, const std::string &texture_image, const std::string &vshader, const std::string &fshader ){
mesh_names.push_back(name);
meshes.push_back(mesh);
openGLObject object;
// 绑定openGL对象,并传递顶点属性的数据
bindObjectAndData(mesh, object, texture_image, vshader, fshader);
opengl_objects.push_back(object);
};
这里我们丢进去了一个物体类,一张图片,顶点着色器,还有片元着色器
通过数据绑定函数将物体与纹理绑定起来
bing函数的实现
void MeshPainter::bindObjectAndData(TriMesh *mesh, openGLObject &object, const std::string &texture_image, const std::string &vshader, const std::string &fshader){
// 初始化各种对象
std::vector<vec3> points = mesh->getPoints();
std::vector<vec3> normals = mesh->getNormals();
std::vector<vec3> colors = mesh->getColors();
std::vector<vec2> textures = mesh->getTextures();
// 创建顶点数组对象
#ifdef __APPLE__ // for MacOS
glGenVertexArraysAPPLE(1, &object.vao); // 分配1个顶点数组对象
glBindVertexArrayAPPLE(object.vao); // 绑定顶点数组对象
#else // for Windows
glGenVertexArrays(1, &object.vao); // 分配1个顶点数组对象
glBindVertexArray(object.vao); // 绑定顶点数组对象
#endif
// 创建并初始化顶点缓存对象
glGenBuffers(1, &object.vbo);
glBindBuffer(GL_ARRAY_BUFFER, object.vbo);
glBufferData(GL_ARRAY_BUFFER,
points.size() * sizeof(vec3) +
normals.size() * sizeof(vec3) +
colors.size() * sizeof(vec3) +
textures.size() * sizeof(vec2),
NULL, GL_STATIC_DRAW);
// 绑定顶点数据
glBufferSubData(GL_ARRAY_BUFFER, 0, points.size() * sizeof(vec3), points.data());
// 绑定颜色数据
glBufferSubData(GL_ARRAY_BUFFER, points.size() * sizeof(vec3), colors.size() * sizeof(vec3), colors.data());
// 绑定法向量数据
glBufferSubData(GL_ARRAY_BUFFER, (points.size() + colors.size()) * sizeof(vec3), normals.size() * sizeof(vec3), normals.data());
// 绑定纹理数据
glBufferSubData(GL_ARRAY_BUFFER, (points.size() + normals.size() + colors.size()) * sizeof(vec3), textures.size() * sizeof(vec2), textures.data());
object.vshader = vshader;
object.fshader = fshader;
object.program = InitShader(object.vshader.c_str(), object.fshader.c_str());
// 将顶点传入着色器
object.pLocation = glGetAttribLocation(object.program, "vPosition");
glEnableVertexAttribArray(object.pLocation);
glVertexAttribPointer(object.pLocation, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
// 将颜色传入着色器
object.cLocation = glGetAttribLocation(object.program, "vColor");
glEnableVertexAttribArray(object.cLocation);
glVertexAttribPointer(object.cLocation, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(points.size() * sizeof(vec3)));
// 将法向量传入着色器
object.nLocation = glGetAttribLocation(object.program, "vNormal");
glEnableVertexAttribArray(object.nLocation);
glVertexAttribPointer(object.nLocation, 3,
GL_FLOAT, GL_FALSE, 0,
BUFFER_OFFSET( (points.size() + colors.size()) * sizeof(vec3)));
// @TODO: Task1 将纹理坐标传入着色器
object.tLocation = glGetAttribLocation(object.program, "vTexture");
glEnableVertexAttribArray(object.tLocation);
glVertexAttribPointer(object.tLocation, 2,
GL_FLOAT, GL_FALSE, 0,
BUFFER_OFFSET((points.size() + colors.size()+ normals.size()) * sizeof(vec3)));
// 获得矩阵位置
object.modelLocation = glGetUniformLocation(object.program, "model");
object.viewLocation = glGetUniformLocation(object.program, "view");
object.projectionLocation = glGetUniformLocation(object.program, "projection");
object.shadowLocation = glGetUniformLocation(object.program, "isShadow");
// 读取纹理图片数
object.texture_image = texture_image;
// 创建纹理的缓存对象
glGenTextures(1, &object.texture);
// 调用stb_image生成纹理
load_texture_STBImage(object.texture_image, object.texture);
// Clean up
glUseProgram(0);
#ifdef __APPLE__ // for MacOS
glBindVertexArrayAPPLE(0);
#else // for Windows
glBindVertexArray(0);
#endif
};
这分别是顶点数据的绑定以及投影矩阵的绑定等等,但这些不是这期文章的重要内容
painter->addMesh(cylinder, "mesh_a", "./assets/cylinder10.jpg", vshader, fshader); // 指定纹理与着色器
// @TODO: Task1 将纹理坐标传入着色器
object.tLocation = glGetAttribLocation(object.program, "vTexture");
glEnableVertexAttribArray(object.tLocation);
glVertexAttribPointer(object.tLocation, 2,
GL_FLOAT, GL_FALSE, 0,
BUFFER_OFFSET((points.size() + colors.size()+ normals.size()) * sizeof(vec3)));
重点在这行代码,我们先在openglobject里定义一个tlocation变量,这个变量用于和着色器变量“vTexture”通过glgetattriblocation函数进行绑定,然后再通过glVertexAttribPointer函数存进缓冲对象即可,因为纹理只是一张图片,是二维的,所以相应的in变量也应该是vec2形式的,所以这里的第二个参数要改为2,这样便完成了纹理变量的绑定
2.纹理坐标的映射
(0,0) (1,1)
一张纹理图片,有着它自己的坐标系,
而我们在将它贴到我们所绘制的几何体中时,几何体也有着它的坐标系,那么我们就得实现两个坐标点之间的转换
圆柱纹理坐标
// 面片生成三角面片,每个矩形由两个三角形面片构成
for (int i = 0; i < num_samples; i++)
{
// 面片1
faces.push_back(vec3i(i, (i + 1) % num_samples, (i) + num_samples));
// 面片1对应的顶点的纹理坐标
vertex_textures.push_back(vec2(1.0 * i / num_samples, 0.0));
vertex_textures.push_back(vec2(1.0 * (i+1) / num_samples, 0.0));
vertex_textures.push_back(vec2(1.0 * i / num_samples, 1.0));
// 对应的三角面片的纹理坐标的下标
texture_index.push_back( vec3i( 6*i, 6*i+1, 6*i+2 ) );
// 面片2
faces.push_back(vec3i((i) + num_samples, (i + 1) % num_samples, (i + num_samples + 1) % (num_samples) + num_samples));
// 面片2对应的顶点的纹理坐标
vertex_textures.push_back(vec2(1.0 * i / num_samples, 1.0));
vertex_textures.push_back(vec2(1.0 * (i+1) / num_samples, 0.0));
vertex_textures.push_back(vec2(1.0 * (i+1) / num_samples, 1.0));
// 对应的三角面片的纹理坐标的下标
texture_index.push_back( vec3i( 6*i+3, 6*i+4, 6*i+5 ) );
}
三个纹理坐标,分别代表xyz轴,x对应为(i,0)是指把图片上(i,0)点的图片信息送到x点,其余类似,为什么最后要(i+6)呢,由上下可以看出,我们一次是存了6个纹理坐标的,所以这里应当是6
圆盘纹理坐标
for (int i = 0; i < num_samples; i++)
{
float r_r_rs = (i + 1) % num_samples * step;
float r_r_r = i * step;
// 面片1
faces.push_back(vec3i(num_samples, (i + 1) % num_samples, (i) % num_samples));
// 面片1对应的顶点的纹理坐标
vertex_textures.push_back(vec2(0.5, 0.5));
vertex_textures.push_back(vec2(0.5 * cos(r_r_rs) + 0.5, 0.5 * sin(r_r_rs)+ 0.5));
vertex_textures.push_back(vec2(0.5 * cos(r_r_r) + 0.5, 0.5 * sin(r_r_r) + 0.5));//利用到上面的弧度
// 对应的三角面片的纹理坐标的下标
texture_index.push_back(vec3i(3 * i, 3 * i + 1, 3 * i + 2));//循环一次压进去3个顶点坐标
}
我们画的图是一个圆,我们要贴的图也是一个圆,这两个圆的区别在哪里?
没错,就是圆心的位置以及半径的不同,那么我们只需要在原cos,sin的基础上把半径换成图片的半径,位置换到中心点即可实现,注意
1,这里是只有三个纹理坐标,所以i只需要加3即可
2,第一个纹理的坐标即x,永远是圆心,这是不能改变的
圆锥纹理坐标
圆锥的纹理坐标和圆类似,但是我们可以同将下面的点近似为直线来进行简化操作
for (int i = 0; i < num_samples; i++)
{
// 面片1
faces.push_back(vec3i(num_samples, (i + 1) % num_samples, (i)%num_samples));
// 面片1对应的顶点的纹理坐标
vertex_textures.push_back(vec2(0.5, 1.0));
vertex_textures.push_back(vec2(1.0 * (i + 1) / num_samples, 0.0));
vertex_textures.push_back(vec2(1.0 * i / num_samples, 0.0));
// 对应的三角面片的纹理坐标的下标
texture_index.push_back(vec3i(3 * i, 3 * i + 1, 3* i + 2));
}
这里便是综合了圆以及矩形的顶点方法
第一个点存圆锥的上顶点
下面便按i+1,i一字排开即可实现