本文正在参加「金石计划」
前篇回顾
前面一篇文章我们学习了OpenGLES的一些基础知识,包括:图像渲染管线,顶点着色器,片段着色器,着色器程序对象,顶点数组对象(VAO),顶点缓冲对象(VBO),顶点数据结构等,并用这些基础知识画了一个三角形。
今天我们来学习画一个正方体,并给正方体附上纹理。在绘图之前,我们需要先来学习一些前置知识。
什么是纹理?
前面我们绘制三角形的时候,是对每个顶点做了颜色的分配,这种情况适用于纯色绘制,如果要绘制一些复杂图像(如图片)到三角形上,那么就需要足够多的顶点数据以及颜色数据,这对开发者来说是很大的挑战,且很笨重,那么这个时候纹理就派上用场了。
在OpenGL中,纹理更像是一张贴纸,当我们画好形状之后,再将纹理贴纸贴到对应的形状上,这样就可以实现将图片映射到物体上的酷炫且复杂的场景。
纹理使用场景很好理解,但是内部是如何实现的呢?
图元:
你可能还记得在图像渲染管线中,在顶点着色器后的阶段就是图元装配:
那么什么是图元装配呢?
概念很简单:就是将顶点连接成不同的形状或者单独作为一个原点使用,这些连接后的形状或者原点就是一个个图元。
要注意了很多同学不清楚的会经常将片段和图元搞混淆,片段只是某个图元上的一个像素,这个像素是需要在片段着色器中进行上色的。
OpenGL中一共有7种图元,根据图元的不同,可以将OpenGL中构成图形的三种基本图形扩展成其他的图形。
图元名称 | 图元描述 |
---|---|
GL_POINTS | 每个顶点在屏幕上都是单独的点,没有连接 |
GL_LINES | 每一对顶点(每两个顶点)构成一条线段 |
GL_LINE_STRIP | 从第一个顶点开始,依次(依顶点顺序)连接后一个顶点形成n-1条线段,n为顶点数量 |
GL_LINE_LOOP | 与GL_LINE_STRIP相同的连接顺序,不同点是最后一个顶点和第一个顶点最后需要连接起来形成闭环 |
GL_TRIANGLES | 依照顶点顺序,每三个顶点构成一个三角形 |
GL_TRIANGLE_STRIP | 共用一个条带上的顶点组成新的三角形,从而形成三角形带 |
GL_TARIANGLE_FAN | 以一个顶点为中心,呈扇形排列,共用相邻的两个顶点的三角形扇 |
7种图元对应的图形如下:
比较常见使用的就是GL_POINTS,GL_LINES,GL_TRIANGLES这三种。
从上面的图形也可以看出,其他图元都可以使用三角形进行变换组成,区别就是三角形的顶点数组不一样而已,其实都是三角形的变种。所以在我们开发中,使用GL_TRIANGLES就可以了。
知道图元我们再来讲解下纹理映射。
什么是纹理映射?
纹理映射也叫做纹理贴图,通过给图元的顶点坐标设置纹理坐标,通过纹理坐标在纹理图中选定特定的纹理区域,最后通过纹理坐标与顶点的映射关系,将选定的纹理区域映射到指定图元上。
简单理解就是将纹理坐标系中指定的区域和顶点坐标系指定的区域进行区域映射。
纹理坐标系
:
顶点坐标系
:
4个纹理坐标:
T0(0,0),T1(1,0),T2(1,1),T3(0,1)
4个顶点坐标:
V0(-1,-1),V1(0.5,-0.5),V2(0.5,0.5),V3(-0.5,0.5)
假设我们需要渲染上面的图片,则需要先绘制一个正方形,然后对正方形贴上纹理即可。
我们知道正方形可以分割为两个三角形,那就只需要绘制两个三角形图元就可以了。
这里使用:V0V1V2和V2V3V0
绘制正方形
有了上面的分析,我们的顶点数据就可以设计为:
//0.初始化顶点数据
GLfloat vertices[] = {
//顶点 //纹理坐标
-0.5f, -0.5f, 0.0f,0.0f, 0.0f,//V0
0.5f,-0.5f, 0.0f,1.0f,0.0f,//V1
0.5f, 0.5f, 0.0f, 1.0f, 1.0f,//V2
-0.5f, 0.5f, 0.0f,0.0f, 1.0f//V3
};
每一行的前三位为顶点数据,后两位为纹理坐标数据。
这里为了切割为两个三角形,需要引入EBO索引缓冲对象:
EBO主要用来告诉OpenGL,我们需要指定哪几个顶点为一组三角形。
//确定三角形索引
GLint indices[] = {
0,1,2,
0,2,3
};
创建EBO的方式和VAO是一样的,只是type不同:
glGenBuffers(1,&EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,sizeof(indices),indices,GL_STATIC_DRAW);
指定为EBO后,我们还需要设置顶点纹理坐标属性:
//顶点坐标属性
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,5*sizeof(GLfloat),(GLvoid*)0);
glEnableVertexAttribArray(0);
//顶点颜色属性
glVertexAttribPointer(1,2,GL_FLOAT,GL_FALSE,5*sizeof(GLfloat),(GLvoid*)(3*sizeof(GLfloat)));
glEnableVertexAttribArray(1);
绘制纹理贴图
最后,我们需要把我们的纹理附加到着色器中:
//生成纹理
glGenTextures(1, &textureID);
int width, height, nrComponents;
//解析sdcard中的纹理图片数据
unsigned char *data = ImageUtil::_stb_image_load("/sdcard/mmpic.png", &width, &height, &nrComponents, 0);
if (data)
{
GLenum format;
if (nrComponents == 1)
format = GL_RED;
else if (nrComponents == 3)
format = GL_RGB;
else if (nrComponents == 4)
format = GL_RGBA;
//绑定纹理图片
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
ImageUtil::_stb_image_free(data);
}
else
{
LOGCATE("Texture failed to load at path: %s",data);
ImageUtil::_stb_image_free(data);
}
注意:
这里我使用了github上的一个开源库来解析图片:其实就是一个头文件,已经放在了demo源码中了。
做了这些,当然在绘制之前还需要去激活纹理Textture:
glUniform1i(glGetUniformLocation(programObj, "textureColor"), 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureID);
最后调用下面代码进行绘制:
glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);
看下效果图
:
额。。咋是倒过来的呢?
好吧,这里用OpenGL官网的解释:
You probably noticed that the texture is flipped upside-down! This happens because OpenGL expects the 0.0 coordinate on the y-axis to be on the bottom side of the image, but images usually have 0.0 at the top of the y-axis.
大概意思就是OpenGL希望纹理坐标原点是以图片左下方为起始位置,但是图片读取的话一般都是以右上方为原点开始读取,所以我们拿到的纹理数据,起始是转了180度的,想象下。。
那怎么解决这个问题:官网给出了建议
Luckily for us, stb_image.h can flip the y-axis during image loading by adding the following statement before loading any image:
我们可以使用stb_image.h中的:
stbi_set_flip_vertically_on_load(true);
让我们读取的数据翻转回来。
设置后效果
:
完整代码如下:
/**
* 绘制前操作
* 0.初始化顶点数据
* 1.创建着色器程序对象
* 2.生成VAO,VBO对象
* */
void MyGLRenderContext::beforeDraw() {
if(programObj!= 0){
return;
}
//0.初始化顶点数据
GLfloat vertices[] = {
//顶点 //纹理坐标
-0.5f, -0.5f, 0.0f,0.0f, 0.0f,//V0
0.5f,-0.5f, 0.0f,1.0f,0.0f,//V1
0.5f, 0.5f, 0.0f, 1.0f, 1.0f,//V2
-0.5f, 0.5f, 0.0f,0.0f, 1.0f//V3
};
//确定三角形索引
GLint indices[] = {
0,1,2,
0,2,3
};
//1.创建着色器程序,此处将着色器程序创建封装到一个工具类中
char vShaderStr[] =
"#version 300 es \n"
"layout(location = 0) in vec4 vPosition; \n"
"layout(location = 1) in vec2 texCords; \n"
"out vec2 color; \n"
"void main() \n"
"{ \n"
" gl_Position = vPosition; \n"
" color = texCords; \n"
"} \n";
char fShaderStr[] =
"#version 300 es \n"
"precision mediump float; \n"
"in vec2 color; \n"
"out vec4 fragColor; \n"
"uniform sampler2D textureColor; \n"
"void main() \n"
"{ \n"
" vec3 picColor = vec3 (texture(textureColor,color)); \n"
" fragColor = vec4 (picColor, 1.0 ); \n"
"} \n";
programObj = GLUtils::CreateProgram(vShaderStr,fShaderStr);
//2.生成VAO,VBO对象,并绑定顶点属性
GLuint VBO,EBO;
glGenVertexArrays(1,&VAO);
glGenBuffers(1,&VBO);
glGenBuffers(1,&EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,sizeof(indices),indices,GL_STATIC_DRAW);
//顶点坐标属性
glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,5*sizeof(GLfloat),(GLvoid*)0);
glEnableVertexAttribArray(0);
//顶点颜色属性
glVertexAttribPointer(1,2,GL_FLOAT,GL_FALSE,5*sizeof(GLfloat),(GLvoid*)(3*sizeof(GLfloat)));
glEnableVertexAttribArray(1);
glBindVertexArray(GL_NONE);
//生成纹理
glGenTextures(1, &textureID);
ImageUtil::_stbi_set_flip_vertically_on_load(true);
int width, height, nrComponents;
unsigned char *data = ImageUtil::_stb_image_load("/sdcard/mmpic.png", &width, &height, &nrComponents, 0);
if (data)
{
GLenum format;
if (nrComponents == 1)
format = GL_RED;
else if (nrComponents == 3)
format = GL_RGB;
else if (nrComponents == 4)
format = GL_RGBA;
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
ImageUtil::_stb_image_free(data);
}
else
{
LOGCATE("Texture failed to load at path: %s",data);
ImageUtil::_stb_image_free(data);
}
}
/**
* 1.清除buffer
* 2.使用程序着色器对象
* 3.开始绘制
* 4.解绑
* */
void MyGLRenderContext::OnDrawFrame() {
beforeDraw();
if(programObj == 0){
return;
}
//清除buffer
glClear(GL_COLOR_BUFFER_BIT);
glClearColor(0.3f,0.5f,0.4f,1.0f);
glUniform1i(glGetUniformLocation(programObj, "textureColor"), 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureID);
//使用程序着色器对象
glUseProgram(programObj);
//绑定VAO
glBindVertexArray(VAO);
//开始绘制
//glDrawArrays(GL_TRIANGLES,0,6);
glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);
//解绑VAO
glBindVertexArray(GL_NONE);
//解绑程序着色器对象
glUseProgram(GL_NONE);
}
总结
本文主要通过一个简单的demo来演示了如何给一个图像装上贴图纹理。 涉及的内容有:图元,纹理,纹理图片加载,EBO,纹理坐标系,顶点坐标系等内容,内容不多,但是理解起来还是有点难,多动手吧,别仅限于看懂。
好了,本篇文章讲到这里了,我是小余,关注我,免费获取最新面试资料,我们下期见。