OpenGL 编程/中级/纹理
一位读者要求扩展此页面,以包含更多内容。 你可以通过 添加新内容 (了解如何操作) 或在 阅览室 中寻求帮助。 |
有几种不同类型的纹理可用于 OpenGL。
- GL_TEXTURE_1D: 这是一个一维纹理。(需要 OpenGL 1.0)
- GL_TEXTURE_2D: 这是一个二维纹理(它既有宽度又有高度)。(需要 OpenGL 1.0)
- GL_TEXTURE_3D: 这是一个三维纹理(具有宽度、高度和深度)。(需要 OpenGL 1.2)
- GL_TEXTURE_CUBE_MAP: 立方体贴图类似于 2D 贴图,但通常在贴图内存储六张图像。特殊的贴图映射用于将这些图像映射到虚拟球体。(需要 OpenGL 1.3)
- GL_TEXTURE_RECTANGLE_ARB: 这种纹理格式非常类似于二维纹理,但支持非 2 的幂大小的纹理。(需要
GLuint theTexture; glGenTextures(1, &theTexture); glBindTexture(GL_TEXTURE_2D, theTexture); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL); // ... glTexImage2D(...); // draw stuff here glDeleteTextures(1, &theTexture);
当您设置新的纹理时,通常会在内存中有一个像素数组,以及图像的尺寸。(图像库或 OpenGL 包装器可能会为您提供读取各种图形格式并直接从它们创建纹理的例程,但最终都是一样的)。
为此,您首先告诉 OpenGL 为您提供一个新的纹理“模板”,然后您选择它以在之后使用它。您设置各种参数,例如纹理的绘制方式。可以使其即使对于具有 alpha 通道的纹理,也会绘制其后面的对象,但这与深度缓冲区不兼容(它不知道纹理是半透明的,将前面的对象标记为实体,并且不会绘制后面的对象)。您需要自己按距离相机远近对对象进行排序并按此顺序绘制它们,以获得正确的结果。顺便说一下,这在这里没有处理。
最后,您将像素数组提供给 OpenGL,它会将纹理加载到内存中。
让我们编写一个允许我们从文件读取像素数组的函数,并将维度指定为参数。
#include <stdio.h>
#include <stdlib.h>
#include <GL/gl.h>
#include <GL/glut.h>
GLuint raw_texture_load(const char *filename, int width, int height)
{
GLuint texture;
unsigned char *data;
FILE *file;
// open texture data
file = fopen(filename, "rb");
if (file == NULL) return 0;
// allocate buffer
data = (unsigned char*) malloc(width * height * 4);
// read texture data
fread(data, width * height * 4, 1, file);
fclose(file);
// allocate a texture name
glGenTextures(1, &texture);
// select our current texture
glBindTexture(GL_TEXTURE_2D, texture);
// select modulate to mix texture with color for shading
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_DECAL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_DECAL);
// when texture area is small, bilinear filter the closest mipmap
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
// when texture area is large, bilinear filter the first mipmap
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// texture should tile
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// build our texture mipmaps
gluBuild2DMipmaps(GL_TEXTURE_2D, 4, width, height, GL_RGBA, GL_UNSIGNED_BYTE, data);
// free buffer
free(data);
return texture;
}
该函数接受包含所有像素值(以 R、G、B、A 顺序排列)的图像文件。例如,可以使用 GIMP 创建这样的文件:制作图像,合并所有图层(必要,因为 RAW 导出模块已损坏),赋予它一个 alpha 通道,保存它并选择 RAW 作为文件类型列表(在底部)。(可能需要最新版本,2.3 或更高版本,我在使用 2.2 时遇到了问题)。
它返回一个 OpenGL 纹理 ID,您可以使用 glBindTexture(GL_TEXTURE_2D, texture)(传递 texture 的 ID) 来选择它。
现在我们已经加载了纹理,看看我们如何使用它。
/* compile with: gcc -lGL -lglut -Wall -o texture texture.c */
#include <GL/gl.h>
#include <GL/glut.h>
/* This program does not feature some physical simulation screaming
for continuous updates, disable that waste of resources */
#define STUFF_IS_MOVING 0
#if STUFF_IS_MOVING
#include <unistd.h>
#endif
#include <stdlib.h>
#include <math.h>
#include <time.h>
/* using the routine above - replace this declaration with the snippet above */
GLuint raw_texture_load(const char *filename, int width, int height);
static GLuint texture;
void render()
{
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
/* fov, aspect, near, far */
gluPerspective(60, 1, 1, 10);
gluLookAt(0, 0, -2, /* eye */
0, 0, 2, /* center */
0, 1, 0); /* up */
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glPushAttrib(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_TEXTURE_2D);
/* create a square on the XY
note that OpenGL origin is at the lower left
but texture origin is at upper left
=> it has to be mirrored
(gasman knows why i have to mirror X as well) */
glBegin(GL_QUADS);
glNormal3f(0.0, 0.0, 1.0);
glTexCoord2d(1, 1); glVertex3f(0.0, 0.0, 0.0);
glTexCoord2d(1, 0); glVertex3f(0.0, 1.0, 0.0);
glTexCoord2d(0, 0); glVertex3f(1.0, 1.0, 0.0);
glTexCoord2d(0, 1); glVertex3f(1.0, 0.0, 0.0);
glEnd();
glDisable(GL_TEXTURE_2D);
glPopAttrib();
glFlush();
glutSwapBuffers();
}
void init()
{
glClearColor(0.0, 0.0, 0.0, 0.0);
glShadeModel(GL_SMOOTH);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glEnable(GL_DEPTH_TEST);
glLightfv(GL_LIGHT0, GL_POSITION, (GLfloat[]){2.0, 2.0, 2.0, 0.0});
glLightfv(GL_LIGHT0, GL_AMBIENT, (GLfloat[]){1.0, 1.0, 1.0, 0.0});
texture = raw_texture_load("texture.raw", 200, 256);
}
#if STUFF_IS_MOVING
void idle()
{
render();
usleep((1 / 50.0) * 1000000);
}
#endif
void resize(int w, int h)
{
glViewport(0, 0, (GLsizei) w, (GLsizei) h);
}
void key(unsigned char key, int x, int y)
{
if (key == 'q') exit(0);
}
int main(int argc, char *argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH);
glutInitWindowSize(640, 480);
glutCreateWindow("Texture demo - [q]uit");
init();
glutDisplayFunc(render);
glutReshapeFunc(resize);
#if STUFF_IS_MOVING
glutIdleFunc(idle);
#endif
glutKeyboardFunc(key);
glutMainLoop();
return 0;
}
哇,这真是很多代码。让我们尝试理解它,从 main() 开始。
第一行设置了 GLUT,不值得进一步解释。
init() 设置画布和光源,并将纹理加载到全局变量中。由于我们这里只有一个纹理,所以它没什么用,因为我们不需要切换纹理,但它仍然在这里。(您可以使用 glBindTexture 切换纹理,如上所述)。
然后设置了一些回调,也不值得谈论,除了显示函数。首先设置相机(如果您要在模拟过程中更改透视,您将使用它 - 在这种情况下是多余的)。然后清除画布(您也应该知道这一点)。
现在我们开启纹理模式,这意味着我们现在所做的所有操作都将使用纹理。将使用当前选定的纹理,在本例中是我们之前创建的纹理(因为它仍然是选定的纹理,因为它是在设置过程中首次选定的)。然后在 XY 轴上创建一个正方形。请注意 glTexCoord2d 调用:它们定义纹理的哪一部分将被分配给下一个顶点。我们将在另一个示例中更多地使用它。
嗯,然后它被绘制了。它并没有真正造成伤害,对吧?
您想在工作目录中放置一个名为 texture.raw 的 RAW 图像,RGBA 256x256。可以使用一些图形编辑器(包括 GIMP) 创建此类文件。
这段 c++ 代码片段展示了将 png 图像文件加载到 OpenGL 纹理对象中的示例。它需要 libpng 和 OpenGL 才能运行。要使用 gcc 编译,请链接 png glu32 和 opengl32。这大部分内容直接来自 libpng 手册。没有检查或转换 png 格式到 OpenGL 纹理格式。这只是提供了基本思路。
#include <GL/gl.h>
#include <GL/glu.h>
#include <png.h>
#include <cstdio>
#include <string>
#define TEXTURE_LOAD_ERROR 0
using namespace std;
/** loadTexture
* loads a png file into an opengl texture object, using cstdio , libpng, and opengl.
*
* \param filename : the png file to be loaded
* \param width : width of png, to be updated as a side effect of this function
* \param height : height of png, to be updated as a side effect of this function
*
* \return GLuint : an opengl texture id. Will be 0 if there is a major error,
* should be validated by the client of this function.
*
*/
GLuint loadTexture(const string filename, int &width, int &height)
{
//header for testing if it is a png
png_byte header[8];
//open file as binary
FILE *fp = fopen(filename.c_str(), "rb");
if (!fp) {
return TEXTURE_LOAD_ERROR;
}
//read the header
fread(header, 1, 8, fp);
//test if png
int is_png = !png_sig_cmp(header, 0, 8);
if (!is_png) {
fclose(fp);
return TEXTURE_LOAD_ERROR;
}
//create png struct
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL,
NULL, NULL);
if (!png_ptr) {
fclose(fp);
return (TEXTURE_LOAD_ERROR);
}
//create png info struct
png_infop info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr) {
png_destroy_read_struct(&png_ptr, (png_infopp) NULL, (png_infopp) NULL);
fclose(fp);
return (TEXTURE_LOAD_ERROR);
}
//create png info struct
png_infop end_info = png_create_info_struct(png_ptr);
if (!end_info) {
png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp) NULL);
fclose(fp);
return (TEXTURE_LOAD_ERROR);
}
//png error stuff, not sure libpng man suggests this.
if (setjmp(png_jmpbuf(png_ptr))) {
png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
fclose(fp);
return (TEXTURE_LOAD_ERROR);
}
//init png reading
png_init_io(png_ptr, fp);
//let libpng know you already read the first 8 bytes
png_set_sig_bytes(png_ptr, 8);
// read all the info up to the image data
png_read_info(png_ptr, info_ptr);
//variables to pass to get info
int bit_depth, color_type;
png_uint_32 twidth, theight;
// get info about png
png_get_IHDR(png_ptr, info_ptr, &twidth, &theight, &bit_depth, &color_type,
NULL, NULL, NULL);
//update width and height based on png info
width = twidth;
height = theight;
// Update the png info struct.
png_read_update_info(png_ptr, info_ptr);
// Row size in bytes.
int rowbytes = png_get_rowbytes(png_ptr, info_ptr);
// Allocate the image_data as a big block, to be given to opengl
png_byte *image_data = new png_byte[rowbytes * height];
if (!image_data) {
//clean up memory and close stuff
png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
fclose(fp);
return TEXTURE_LOAD_ERROR;
}
//row_pointers is for pointing to image_data for reading the png with libpng
png_bytep *row_pointers = new png_bytep[height];
if (!row_pointers) {
//clean up memory and close stuff
png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
delete[] image_data;
fclose(fp);
return TEXTURE_LOAD_ERROR;
}
// set the individual row_pointers to point at the correct offsets of image_data
for (int i = 0; i < height; ++i)
row_pointers[height - 1 - i] = image_data + i * rowbytes;
//read the png into image_data through row_pointers
png_read_image(png_ptr, row_pointers);
//Now generate the OpenGL texture object
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D,0, GL_RGBA, width, height, 0,
GL_RGB, GL_UNSIGNED_BYTE, (GLvoid*) image_data);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
//clean up memory and close stuff
png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
delete[] image_data;
delete[] row_pointers;
fclose(fp);
return texture;
}
纹理坐标将纹理中的某些点分配给顶点。它允许您节省纹理内存,并可以使工作更轻松。例如,骰子有六个不同的面。您可以将每个面并排放在一个纹理中,然后说“这面应该使用纹理的最左侧三分之一”。
在对像体(如人)建模时,这种方法更加有用,因为您拥有身体、腿、胳膊、头等等。如果您对每个部位都使用额外的纹理文件,很快就会积累大量纹理文件,这些文件非常难以管理。最好将所有部位放在一个文件中,并在绘制时选择相应的部位,这样效率更高。
您已经在前面的示例中看到了纹理坐标,尽管选择的部位是整个纹理。提供给 glTexCoord2d 的两个参数是 0 到 1 之间的数字。(这样做的好处是与大小无关 - 您可以在以后决定使用更高分辨率的纹理,而无需更改渲染代码!)
- 切换纹理效率低下。尝试首先绘制所有使用纹理 A 的对象,然后切换纹理并绘制所有使用纹理 B 的对象,依此类推。(如果您有 alpha 纹理,则不能始终实现这一点,因为您必须自己对所有对象进行排序。关于这方面的文章可能很快就会发布)。
- 尝试将小的纹理组合成一个大的纹理,并使用纹理坐标选择您想要的部位。这将减少内存开销,并减少纹理切换次数。