分类: C/C++
2014-11-15 15:41:10
我已经决定提前介绍纹理映射,因为它可能更容易纹理映射一个对象,而不是面对一个多面(或三维物体) 。此外,似乎这是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。看下我们纹理的图的说明:
参照我们的 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”, 你可以看到如下的界面
我们的 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函数,结果就变为了:
当它是白色的时候,乘积就是:
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
这就是我们为什么将颜色设置为白色的原因.
好了,就是这样!
在本教程里,我真的说了很多,但我希望你可以看到,实际用于纹理映射的代码不是很多,更多的工作,是在建立纹理的时候。