OpenGL超级宝典学习笔记——纹理映射(一)


纹理映射,是将纹理空间中的纹理像素映射到屏幕空间中的像素的过程。

纹理映射是真实感图像制作的一个重要部分,运用它可以方便的制作出极具真实感的图形而不必花过多时间来考虑物体的表面细节。然而纹理加载的过程可能会影响程序运行速度,当纹理图像非常大时,这种情况尤为明显。如何妥善的管理纹理,减少不必要的开销,是系统优化时必须考虑的一个问题。其中OpenGL提供了纹理对象对象管理技术来解决上述问题。与显示列表一样,纹理对象通过一个单独的数字来标识。这允许OpenGL硬件能够在内存中保存多个纹理,而不是每次使用的时候再加载它们,从而减少了运算量,提高了速度。

加载纹理

要把纹理映射到几何图形中,首先我们需要加载纹理到内存中,加载之后这个纹理就成为OpenGL当前纹理状态的一部分。OpenGL提供了下面三个方法从内存缓冲区中加载纹理:

void glTexImage1D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLint border, GLenum format, GLenum type, void *data);

void glTexIamge2D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, void *data);

void glTexIamge3D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLenum format, GLenum type, void *data);

这三个方法告诉了OpenGL你加载的纹理数据的信息。OpenGL支持1维、2维、3维的纹理数据的映射,使用一致的根函数调用(glTexImage)加载纹理,使其成为当前纹理。使用上面的函数时,OpenGL会拷贝data参数所指向的位置的纹理信息。这种数据复制可能代价很高。OpenGL中可以使用纹理对象来缓解性能的问题。

第一个参数target是说明纹理对象是几维的,其中1D、2D、3D接受的参数分别为GL_TEXTURE_1D、GL_TEXTURE_2D和GL_TEXTURE_3D。你也可以用用相同的方式来指定代理纹理。参数为GL_PROXY_TEXTURE_1D、GL_PROXY_TEXTURE_2D、GL_PROXY_TEXTURE_3D,然后通过glGetTexParameter来提取代理查询的结果。

第二个参数level指定要Mipmap的等级。

第三个参数internalformat告诉OpenGL内部用什么格式存储和使用这个纹理数据(一个像素包含多少个颜色成分,是否压缩)。常用的常量如下:

常量 描述
GL_APHPA 按照ALPHA值存储纹理单元
GL_LUMINANCE 按照亮度值存储纹理单元
GL_LUMINANCE_ALPHA 按照亮度和alpha值存储纹理单元
GL_RGB 按照RGB成分存储纹理单元
GL_RGBA 按照RGBA成分存储纹理单元

widht, height, depth分别指定了纹理的宽度、高度和深度。在OPENGL2.0之前,这三个值要求是2的n次幂(即1,2,4,8,16,32等),否则纹理将无法显示。虽然opengl2.0之后不要求纹理是2的n次幂了,但这不保证性能。考虑性能的话,一般还是把纹理做成2的n次幂。

border参数是指定纹理的边界宽度。纹理边界允许我们对边界外的纹理单元进行额外的设置,对它的宽度、高度、深度进行扩展。这在纹理过滤中很有用。

后面三个参数format、type、data和glDrawPixles函数的参数相同。

加载完纹理数据之后,我们还需要通过glEnable()接受GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D其中一个为参数开启相应维数的纹理映射。通过glDisable()来关闭。

PS:通过glTexImage加载的纹理数据一样会通过像素和图像管道进行变换。这意味着像素压缩,放大缩小,颜色表,和卷积都会被应用到被加载的纹理数据上。

颜色缓冲区中读取

跟从颜色缓冲区中读取像素一样,纹理数据一样可以从颜色缓冲区中读取。使用如下两个方法:

void glCopyTexImage1D(GLenum target, GLint level, GLenum internalformat,GLint x, GLint y, GLsizei width, GLint border);

void glCopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLsizei height, GLint border);

前面三个参数所代表的意义和glTexImage函数是一样的。x,y指定从颜色缓冲区的哪个位置开始读取数据。width height指定宽度、高度,border指定边界宽度。注意:我们无法从2维的颜色缓冲区中读取三维的纹理数据,所以没有glCopyTexImage3D这个方法。

更新纹理

如果我们只需要修改纹理中的一部分数据,而不想重新加载纹理,我们可以使用glTexSubImage方法。这个方法比每次都去重新加载纹理数据要快得多。它的三个变型如下:

void glTexSubImage1D(GLenum target, GLint level, GLint xOffset, GLsizei width, GLenum format, GLenum type, const GLvoid *data);

void glTexSubImage2D(GLenum target, GLint level, GLint xOffset, GLint yOffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void *data);

void glTexSubImage3D(Glenum target, GLint level, GLint xOffset, GLint yOffset, GLint zOffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void *data);

xOffset, yOffset, zOffset指定已存在的纹理数据中的偏移值,从这个位置开始替换更新纹理数据。width, height, depth指定要更新到现在的纹理中的纹理数据的规格宽、高、深度。

下面的函数允许我们从颜色缓冲区中读取数据并插入或替换现在纹理的部分数据:

void glCopyTexSubImage1D(GLenum target, GLint level, GLint xOffset, GLint x, GLint y, GLsizei width);

void glCopyTexSubImage2D(GLenum target, GLint level, GLint xOffset, GLint yOffset, Glint x, GLint y, GLsizei width, GLsizei height);

void glCopyTexSubImage3D(GLenum target, GLint level, GLint xOffset, GLint yOffset, GLint zOffset, GLint x, GLint y, GLsizei width, GLsizei height);

这些参数与上面解释过的是一样的意义。注意没有glCopyTexImage3D函数,但是我们仍可以用glTexSubImage3D函数把2维的颜色缓冲区中的数据,应用到3维纹理的一个平面中。

映射纹理到几何图元

要把纹理映射到几何图元中,我们需要告诉OpenGL如何映射这些纹理。纹理元素不是根据内存中的位置来放置的(与像素不同)。纹理用更抽象的纹理坐标来确定纹理元素放置的位置。纹理坐标包含s、t、r和q坐标轴类似于顶点坐标的x、y、z和w,以此来支持1维到3维的纹理。q用来缩放纹理坐标,即纹理坐标归一化后的坐标值为s/q、t/q、r/q,默认为1.0。纹理坐标值为浮点数,范围为[0.0,1.0]。下图解释纹理坐标如何放置纹理元素的:

image

我们可以通过glTexCoord函数来设置纹理坐标,这类似于设置顶点坐标。下面是3个常用的glTexCoord变型:

void glTexCoord1f(GLfloat s);

void glTexCoord2f(GLfloat s, GLfloat t);

void glTexCoord3f(GLfloat s, GLfloat t, GLfloat r);

注意纹理坐标像表面法线,和颜色值一样要在设置顶点之前进行设置。用上面的这些函数为每个顶点设置纹理坐标。然后OpenGL会根据需要对纹理进行缩放后映射到几何图元中(其中应用到纹理过滤,后面再解释)。下图是把2维的纹理映射到一个四方形GL_QUADE图元中,注意纹理的四个角与四方形的四个角是一一对应的。

image

当然我们还可以把一个四方形的纹理图映射到一个三角形的几何图元中:

image

纹理矩阵

纹理坐标也可以通过纹理矩阵来进行变换。纹理矩阵的工作方式与投影矩阵,模型视图矩阵类似。通过glMatrixMode(GL_TEXTURE):来开启纹理矩阵模式,在此函数调用后面的矩阵变换将被应用于纹理坐标。纹理坐标可以进行移动、缩放、旋转。纹理矩阵的栈最多只能容纳两个纹理矩阵。通过glPushMatrix 和glPopMatrix来进行栈操作。

一个简单的例子

下面的例子是一个金字塔,我把每个面设置成不同的颜色,然后再进行纹理坐标映射,并使用定时器让金字塔旋转。

纹理图如下:

image

pyramid代码如下:

#include "gltools.h"
#include "math3d.h"

static GLint iWidth, iHeight, iComponents;
static GLenum eFormat;
static GLfloat xRot, yRot;

static GLfloat noLight[4] = {0.0f, 0.0f, 0.0f, 1.0f};
static GLfloat ambientLight[4] = {0.3f, 0.3f, 0.3f, 1.0f};
static GLfloat diffuseLight[4] = {0.7f, 0.7f, 0.7f, 1.0f};
static GLfloat brightLight[4] = {1.0f, 1.0f, 1.0f, 1.0f};

//光的位置在右上角
static GLfloat lightPos[] = { 5.0f, 5.0f, 5.0f, 1.0f};

void SetupRC()
{
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

glCullFace(GL_BACK);
glFrontFace(GL_CCW);
glEnable(GL_CULL_FACE);
glEnable(GL_DEPTH_TEST);
//设置光照环境
glEnable(GL_LIGHTING);
glLightModelfv(GL_LIGHT_MODEL_AMBIENT, noLight);
glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight);
glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight);
glLightfv(GL_LIGHT0, GL_SPECULAR, brightLight);
glLightfv(GL_LIGHT0, GL_POSITION, lightPos);
glEnable(GL_LIGHT0);

//开启颜色追踪
glEnable(GL_COLOR_MATERIAL);
glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE);
glMaterialfv(GL_FRONT, GL_SPECULAR, brightLight);

//镜面光加亮的范围设置大一点
glMateriali(GL_FRONT, GL_SHININESS, 30);

//读取图像文件
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
void *pImage = NULL;
pImage = gltLoadTGA("..\\stone.tga", &iWidth, &iHeight, &iComponents, &eFormat);

if (pImage)
{
//加载纹理,然后释放临时的内存
glTexImage2D(GL_TEXTURE_2D, 0, iComponents, iWidth, iHeight, 0, eFormat, GL_UNSIGNED_BYTE, pImage);
free(pImage);
pImage = NULL;
}

//设置纹理过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

//设置纹理环境
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glEnable(GL_TEXTURE_2D);
}

void RenderScene()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

/*
金字塔顶点下标为0,底部坐标如下
1______2
| |
|______|
3 4
*/
//金字塔顶点数组
M3DVector3f vertices[5] = {{0.0f, 0.8f, 0.0f},
{-.50f, 0.0f, -.50f},
{.50f, 0.0f, -.50f},
{-.50f, 0.0f, .50f},
{.50f, 0.0f, .50f}};

//表面法线向量
M3DVector3f normal;

glPushMatrix();

//先往里和往下平移一点
glTranslatef(0.0f, -0.3f, -4.0f);
if (xRot > 360.5f)
{
xRot = 0.0f;
}

if (yRot > 360.5f)
{
yRot = 0.0f;
}

//进行旋转
glRotatef(xRot, 1.0f, 0.0f, 0.0f);
glRotatef(yRot, 0.0f, 1.0f, 0.0f);
xRot += 0.5f;
yRot += 0.5f;

glBegin(GL_TRIANGLES);

//底面的四方形由两个三角形组成
glColor3f(1.0f, 0.0f, 0.0f);

//注意法线和纹理都要在顶点之前设置
glNormal3f(0.0f, -1.0f, 0.0f);
glTexCoord2f(0.0f, 1.0f);
glVertex3fv(vertices[1]);
glTexCoord2f(1.0f, 1.0f);
glVertex3fv(vertices[2]);
glTexCoord2f(1.0f, 0.0f);
glVertex3fv(vertices[4]);

glTexCoord2f(1.0f, 0.0f);
glVertex3fv(vertices[4]);
glTexCoord2f(0.0f, 0.0f);
glVertex3fv(vertices[3]);
glTexCoord2f(0.0f, 1.0f);
glVertex3fv(vertices[1]);

//前面
glColor3f(0.0f, 1.0f, 0.0f);
m3dFindNormal(normal, vertices[0], vertices[3], vertices[4]);
glNormal3fv(normal);
glTexCoord2f(0.5f, 0.5f);
glVertex3fv(vertices[0]);
glTexCoord2f(0.0f, 0.0f);
glVertex3fv(vertices[3]);
glTexCoord2f(1.0f, 0.0f);
glVertex3fv(vertices[4]);

//左侧面
glColor3f(0.0f, 0.0f, 1.0f);
m3dFindNormal(normal, vertices[1], vertices[3], vertices[0]);
glNormal3fv(normal);
glTexCoord2f(0.0f, 0.0f);
glVertex3fv(vertices[1]);
glTexCoord2f(1.0f, 0.0f);
glVertex3fv(vertices[3]);
glTexCoord2f(0.5f, 0.5f);
glVertex3fv(vertices[0]);

//右侧面
glColor3f(0.0f, 1.0f, 1.0f);
m3dFindNormal(normal, vertices[0], vertices[4], vertices[2]);
glNormal3fv(normal);
glTexCoord2f(0.5f, 0.5f);
glVertex3fv(vertices[0]);
glTexCoord2f(0.0f, 0.0f);
glVertex3fv(vertices[4]);
glTexCoord2f(1.0f, 0.0f);
glVertex3fv(vertices[2]);

//后面
glColor3f(1.0f, 0.0f, 1.0f);
m3dFindNormal(normal, vertices[0], vertices[2], vertices[1]);
glNormal3fv(normal);
glTexCoord2f(0.5f, 0.5f);
glVertex3fv(vertices[0]);
glTexCoord2f(0.0f, 0.0f);
glVertex3fv(vertices[2]);
glTexCoord2f(1.0f, 0.0f);
glVertex3fv(vertices[1]);

glEnd();

glPopMatrix();

glutSwapBuffers();
}

void ChangeSize(GLsizei w, GLsizei h)
{
if (h == 0)
h = 1;

glViewport(0, 0, w, h);

float fAspect = (GLfloat)w/(GLfloat)h;

glMatrixMode(GL_PROJECTION);
glLoadIdentity();

gluPerspective(35.0, fAspect, 1.0, 100.0);

glMatrixMode(GL_MODELVIEW);
glLoadIdentity();

glutPostRedisplay();
}

void TimerFunc(int value)
{
glutPostRedisplay();
glutTimerFunc(60, TimerFunc, 1);
}


int main(int args, char *arv[])
{
glutInit(&args, arv);
glutInitDisplayMode(GL_RGB | GL_DOUBLE | GL_DEPTH);
glutInitWindowSize(800, 600);
glutCreateWindow("pyramid");

glutDisplayFunc(RenderScene);
glutReshapeFunc(ChangeSize);
SetupRC();

glutTimerFunc(50, TimerFunc, 1);

glutMainLoop();

return 0;
}

效果图如下:

image

image

OpenGL超级宝典 第4版 中文版PDF+英文版+源代码 见 

OpenGL编程指南(原书第7版)中文扫描版PDF 下载

OpenGL 渲染篇

Ubuntu 13.04 安装 OpenGL

OpenGL三维球体数据生成与绘制【附源码】

Ubuntu下OpenGL编程基础解析

如何在Ubuntu使用eclipse for c++配置OpenGL  

更多《OpenGL超级宝典学习笔记》相关知识 见 

相关内容