Chinaunix首页 | 论坛 | 博客
  • 博客访问: 5513734
  • 博文数量: 763
  • 博客积分: 12108
  • 博客等级: 上将
  • 技术积分: 15717
  • 用 户 组: 普通用户
  • 注册时间: 2007-09-28 21:21
个人简介

业精于勤,荒于嬉

文章分类

全部博文(763)

文章存档

2018年(6)

2017年(15)

2016年(2)

2015年(31)

2014年(14)

2013年(87)

2012年(75)

2011年(94)

2010年(190)

2009年(38)

2008年(183)

2007年(28)

分类: C/C++

2010-01-30 10:12:26

我已经决定提前介绍纹理映射,因为它可能更容易纹理映射一个对象,而不是面对一个多面(或三维物体) 。此外,似乎这是iPhone OpenGL ES的程序员最希望得到的知识,所以我会坚持到现在都说到纹理映射。

我知道我已经跳过了OpenGL支持的很多细节,使你直接在屏幕上实验绘制物体,而没有一遍又一遍的介绍OpenGL的历史,介绍OpenGL和OpenGL ES之间有什么不同等等。有时候呢,我也会跳过一些技术细节。

这次,我将介绍相当多的细节,也就是说,这是一个很长的教程。

话虽如此,大部分的代码只是加载纹理到我们的工程里,并且将它放入OpenGL的引擎中,这样一来OpenGL就可以使用它。这并不复杂,这仅需要调用iPhone SDK的一点点工作。

纹理的准备工作
在我们开始使用一个纹理前,我们需要加载它到我们的应用程序里,用OpenGL的格式格式化它,并且告诉OpenGL在哪里可以找到它。一旦我们做好之前的工作,其他的工作就如同我们之前教程里给矩形上色那么容易。
开启Xcode,并且打开EAGLView.h在编辑区。首先,我们需要提供给OpenGL需要的一个变量。增加下面这个声明:

    GLuint textures[1];  // 表示一个图片的句柄



显然,这个一个GLuint的数组。你以前见过我使用过GLfloat,再次说明,GLuint就是OpenGL为无符号整型的一个重命名(typedef)。你应该一直使用GLxxxx 这样的重命名,而不是使用Objective_C的类型参数。因为这些是为了OpenGL定义的OpenGL命名参数,我们是在使用OpenGL,而不是开发环境。

下面,我们将调用OpenGL函数glGenTextures()来填充这个变量。我们现在只声明了,之后我们需要覆盖glGenTextures()函数及这个变量。

在该方法的原型下,添加下列函数:

- (void)loadTexture;

这里我们将添加代码去加载纹理。

在你的工程里添加上 CoreGraphics Framework

为了加载纹理和对它进行处理,我们将使用CoreGraphics框架,因为它提供了所有我们需要的方法,而无需写您在Windows的OpenGL教程中看到的所有低级别的代码。

在 Xcode “Groups & Files” 侧栏, 右键点击 “Frameworks” 组并选择 Add -> Existing Frameworks...

在搜索框里, 输入 “CoreGraphics.framework” ,并查阅该文件夹中的结果,这些结果符合您的应用目标( iPhone的SDK 2.2.1在我的情况下) 。单击文件夹,并将其添加到您的项目(文件夹图标的框架)。

下一步,我们需要在我们的工程里增加一个纹理图片,所以它需要包含在我们的应用程序包里。下载纹理 checkerplate.png 并且保存它在工程目录里。增加这个图片到你的工程资源目录可以这样做,右键点击资源目录并且选择 Add -> Existing Files...  选择这个图片,这样就添加进入了。

把纹理加载到我们的应用程序和OpenGL中
切换到 EAGLView.m 并且我们开始执行 loadTexture 函数.

- (void)loadTexture {
    
}

下列的代码将顺序的添加到这个函数中,所以你只要在每行的后面添加就行了。第一件事,我们需要将添加这个图片到我们的应用程序里,使用下列的代码:

// 加载图片 (纹理)
CGImageRef textureImage = [UIImage imageNamed:@"checkerplate.png"].CGImage;
if (textureImage == nil) {
    NSLog(@"Failed to load texture image");
    return;
}

这个 CGImageRef 是 CoreGraphics 的一个数据类型,为了收集图片的所有信息。要获得此信息,我们要做的就是使用UIImage类方法imageNamed :创造一个autorelease'd UIImage来找到我们应用程序主包的文件名。


To get this information all we do is use the UIImage class method imageNamed: which creates an autorelease’d UIImage finding the file by it’s name in our Application’s main bundle. UIImage自动创建的CGImageRef和访问的UIImage类中的CGImage 。

现在,我们为了下面的参考,需要获得图片的尺寸.

    NSInteger texWidth = CGImageGetWidth(textureImage);
    NSInteger texHeight = CGImageGetHeight(textureImage);

这个 CGImageRef 数据包含了图片的宽和高,但是我们不能直接访问它,我们需要使用上述两种提取函数。

这个CGImageRef,就象它数据类型名称所暗示的,不包含图片的数据,只说明了图片的数据。所以我们需要开辟一些内存来包含这个图片的数据

      GLubyte *textureData = (GLubyte *)malloc(texWidth * texHeight * 4);

分配出的正确数据大小,应该是宽*高*4。记得之前的教程里说过,OpenGL只支持RGBA值?每个像素占四个字节,也就是一个RGBA种一种颜色占一个字节。

现在,我们需要调用一些绝对重要的函数(张开嘴??老外真的搞笑)

    CGContextRef textureContext = CGBitmapContextCreate(
                    textureData,
                    texWidth,
                    texHeight,
                    8, texWidth * 4,
                    CGImageGetColorSpace(textureImage),
                    kCGImageAlphaPremultipliedLast);

        CGContextDrawImage(textureContext,
                   CGRectMake(0.0, 0.0, (float)texWidth, (float)texHeight),
                   textureImage);

        CGContextRelease(textureContext);

首先,顾名思义,这个 CoreGraphics 函数返回一个 Quartz2D 图形绘制句柄. 基本上来说,我们定义了一个 CoreGraphics的指针,指向我们的纹理数据,并且告诉它我们纹理的数据和格式。

下面,我们实际上是绘制图片到我们开辟的数据中(纹理数据指针)从我们之前创建的图形绘制句柄的数据指针。这个句柄包含了OpenGL需要的所有的信息及数据都复制到malloc()中。

当我们完成了 CoreGraphics的时候,我们需要释放我们创建的 textureContext句柄。

我知道我加快了上述代码的讲解部分,但是我们对OpenGL其他方面的事情更感兴趣。你可以使用这些代码去加载任何被加入你的工程里的png格式的图片纹理。

现在,到了OpenGL编程了。

现在,还记得我们在头文件中定义的数组了吗?我们现在要使用它。看看下一行代码:

        glGenTextures(1, &textures[0]);

我们需要从我们的应用程序复制这个纹理数据到OpenGL引擎,所以我们要告诉OpenGL为它开辟内存空间。(我们不能直接使用它). 记得textures[]定义为GLuint?一旦我们调用glGenTextures,OpenGL就会创建一个句柄或者一个指针,我们加载到 OpenGL里的每个纹理都是唯一的。OpenGL返回的值对我们来说并不重要,每次当我们要使用checkerplate.png的纹理的时候,我们只需要使用textures[0]就可以了。OpenGL就象我们说的那样去做的。

我们也可以一次为多个纹理分配空间。比如,如果我们需要为我们的应用程序准备10个纹理。我们可以如下做:

    GLuint textures[10];
    glGenTextures(10, &textures[0]);

在我们的例子里,我们只需要一张纹理,所以我们开辟一张。

下面,我们需要激活我们刚才生成的纹理:

        glBindTexture(GL_TEXTURE_2D, textures[0]);

第二个参数是显而易见的,它的纹理,我们刚刚建立。第一个参数通常时GL_TEXTURE_2D 因为所有的 OpenGL ES 在这点上都接受它。 “Full” 的OpenGL允许1D和3D纹理,但我相信这仍然是需要在今后的OpenGL ES的兼容性。

千万别忘记了使用它来激活纹理。

下面,我们发送我们的纹理数据(textureData 的指针)到OpenGL。OpenGL在它的那方面(服务面)管理纹理数据,所以数据必须被转换为硬件支持的格式并保存到OpenGl的空间里。这个听上去有点拗口,不过大多数的OpenGL ES的限制都是相同的。

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texWidth, texHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, textureData);

遍历这些参数,它们是:

0.    ?target – 基本上,通常是 GL_TEXTURE_2D?
0.    ?level – 规定纹理的详细程度。0表示允许图片的全部细节。高数字表示n级别mipmap图片细节。(这边不懂,请知道的朋友告诉我下。)
0.    ?internal_format – 这个 internal format 和 format 必须是相同的。这两个都是 GL_RGBA .?
0.    ?width - width of the image?
0.    ?height - height of the image?
0.    ?border – 必须始终设置为0 , OpenGL ES 不支持纹理边界.?
0.    ?format – 必须和 internal_format?相同。
0.    ?type – 每个像素的类型。想起来没,每个像素为四个字节。因此每个像素占用1个无符号整型(4字节)
0.    ?pixels – 实际上的图片数据指针。

因此,尽管有很多参数,但大部分都是常识,要需要你输入定义变量 (textureData, texWidth, & texHeight).要记得对你的纹理数据做句柄控制,在OpenGL内。

现在,我们已经将数据传输到OpenGL里了,我们可以释放我们之前创建的texturedata.

       free(textureData);

这里有三个函数调用并使用:

    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glEnable(GL_TEXTURE_2D);

这三个函数的调用就是对OpenGL做最后的设置,并且设置OpenGL的纹理映射的状态。这前两个函数告诉OpenGL,在放大的(近距离 - GL_TEXTURE_MAG_FILTER) 及缩小的(远距离 - GL_TEXTURE_MIN_FILTER). 时候,如何处理。为了纹理映射的工作及GL_LINEAR的选择,你必须至少指定一个。


最后,我们调用glEnable()告诉OpenGL使用纹理,当我们告诉OpenGL执行绘制代码的时候。

最后,我们需要在initWithCoder初始化里增加这个函数。

        [self setupView];
        [self loadTexture];// Add this line

就是添加第二行在setupView函数的后面

drawView的调整
这是一个艰苦的工作。改变drawView函数并不比我们之前教程里给矩形上色更困难。首先,注销掉 squareColours[] 数组,我们不需要使用它了。

现在,回忆我们在为矩形上色的时候,我们为每个矩形的顶点,提供了一个颜色的值。当涉及到纹理映射的时候,我们要做同样的事,就象告诉每个顶点是什么颜色一样,我们告诉每个顶点对应的纹理坐标。

在我们这样做之前,我们需要知道什么是纹理坐标OpenGL定义纹理坐标的原点(0,0)在左下角每个轴就是从0—1。看下我们纹理的图的说明:

西蒙iphone-OpenGL ES 教程 -06 - 勇者之尊 - 等待

参照我们的 squareVertices[].

const GLfloat squareVertices[] = {
        -1.0, 1.0, 0.0,               // Top left
        -1.0, -1.0, 0.0,              // Bottom left
        1.0, -1.0, 0.0,               // Bottom right
        1.0, 1.0, 0.0                 // Top right
    };

你可以看到第一个纹理坐标,我们是不是要指定到左上角的纹理?这里的纹理坐标是(0,1). 我们的第二点是右上方的广场,因此,纹理坐标( 1 , 1 ) 。然后,我们去的右下角,这是纹理坐标( 1 , 0 ) ,最后结束的左下角,我们结束纹理坐标( 0 , 0 )。因此,我们指定squareTextureCoords [ ]如下:

    const GLshort squareTextureCoords[] = {
        0, 1,       // top left
        0, 0,       // bottom left
        1, 0,       // bottom right
        1, 1        // top right
    };

注意:我们使用GLshort不是GLfloat的。添加上述代码到您的项目。

你看看,这个是不是类似于我们的纹理数组?

好了,现在我们需要修改绘制代码。不用管三角形的绘制代码,直接从矩形的代码开始。新的矩形绘制代码如下:

    glLoadIdentity();
    glColor4f(1.0, 1.0, 1.0, 1.0);      //NEW
    glTranslatef(1.5, 0.0, -6.0);
    glRotatef(rota, 0.0, 0.0, 1.0);
    glVertexPointer(3, GL_FLOAT, 0, squareVertices);
    glEnableClientState(GL_VERTEX_ARRAY);
    
    glTexCoordPointer(2, GL_SHORT, 0, squareTextureCoords);     // NEW
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);                // NEW
    
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);               // NEW

ok,这段代码有四行新的代码。我删除了之前教程里的为矩形上色的代码。第一行代码是调用glColor4f(),我会在下面的内容中说明。

下面的三行代码你现在应该非常的熟悉了。这里所指的不是物体的顶点或者颜色,我们现在只针对纹理。

    glTexCoordPointer(2, GL_SHORT, 0, squareTextureCoords);     // NEW
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);                // NEW

第一个调用就是告诉OpenGL我们的纹理坐标数组是在哪里存储并且格式是什么。所不同的,我们告诉它每个坐标只有两个值(这是当然的,因为是2d纹理),数据类型我们使用的是GLushort所以这里用GL_SHORT,这里没有 stride (0),并且指向我们的坐标指针。

现在我们告诉OpenGL客户端状态,为了纹理映射我们指定的坐标数组。

调用glDrawArrays()没有改变:

    glDisableClientState(GL_TEXTURE_COORD_ARRAY);               // NEW

记不记得,当我们矩形和三角形采取了不同的颜色的时候,我们关闭了颜色数组?再次,我们需要对纹理映射这样做(关闭它),不然OpenGL将使用这个纹理去映射三角形。

保存代码,点击 “Build and Go”, 你可以看到如下的界面

西蒙iphone-OpenGL ES 教程 -06 - 勇者之尊 - 等待

我们的 checkerplate 纹理现在映射到矩形上,而我们的三角形和以前一样。

进一步的实验
首先,让我说下我们增加在绘制矩形之前这行代码:

   glColor4f(1.0, 1.0, 1.0, 1.0);      // NEW

当然,这是改变绘制颜色为白色,不透明。你能猜出来,我为什么要增加这行吗?OpenGL就是一个状态机,所以一旦我们设置了某个状态,这个状态就一直保留,直到我们改变它。所以绘制颜色被设置为蓝色,直到我们把它改为白色。


Ok,到纹理映射的时候,OpenGL将当前颜色设置(蓝色)乘以当前纹理像素做为最后的颜色。这就是

// 很经典呀。。怎么得到一个图片中的某个像素点的 RGB 值?????
                           R    G     B    A
    Colour Set:           0.0, 0.0, 0.8, 1.0
    Texture Pixel Colour: 1.0, 1.0, 1.0, 1.0

所以,当OpenGL执行绘制的,乘积为:

        Colour_Red * Pixel_Colour_Red = Rendered_colour
       0.0      *     1.0              = 0.0
    Colour_Green * Pixel_Colour_Green
       0.0      *     0.0              = 0.0
     Colour_Blue * Pixel_Colour_Blue
       0.8      *     1.0              = 0.8

注销掉这行glColour4f函数,结果就变为了:

  
西蒙iphone-OpenGL ES 教程 -06 - 勇者之尊 - 等待


当它是白色的时候,乘积就是:

Set Colour :     1.0, 1.0, 1.0, 1.0
                  is mulitplied by
Pixel Colour :   0.8, 0.8, 0.8, 1.0

         Result: 0.8, 0.8, 0.8, 1.0

这就是我们为什么将颜色设置为白色的原因.

好了,就是这样!
在本教程里,我真的说了很多,但我希望你可以看到,实际用于纹理映射的代码不是很多,更多的工作,是在建立纹理的时候。

阅读(8509) | 评论(0) | 转发(3) |
给主人留下些什么吧!~~