现代OpenGL教程 02——贴图

导读:现代OpenGL教程 01——入门指南
在本文中,我们将给三角形加一个贴图,这需要在顶点和片段着色器中加入一些新变量,创建和使用贴图对象,并且学习一点贴图单元和贴图坐标的知识。

本文会使用两个新的类到tdogl命名空间中:tdogl:Bitmap和tdogl:Texture。这些类允许我们将jpg,png或bmp图片上传到显存并用于着色器。tdogl:Program类也增加一些相关接口。

获取代码

所有例子代码的zip打包可以从这里获取:https://github.com/tomdalling/opengl-series/archive/master.zip

这一系列文章中所使用的代码都存放在:https://github.com/tomdalling/opengl-series。你可以在页面中下载zip,加入你会git的话,也可以复制该仓库。

本文代码你可以在source/02_textures目录里找到。使用OS X系统的,可以打开根目录里的opengl-series.xcodeproj,选择本文工程。使用Windows系统的,可以在Visual Studio 2013里打开opengl-series.sln,选择相应工程。

工程里已包含所有依赖,所以你不需要再安装或者配置额外的东西。如果有任何编译或运行上的问题,请联系我。

着色器变量Uniform与Attribute

教程(一)中的着色器变量都是attribute,本文介绍另外一种类型的变量:uniform变量。

着色器变量有两种类型:uniform和attribute。attribute变量可以在每个顶点上有不同值。而uniform变量在多个顶点上保持相同值。比如,你想要给一个三角形设置一种颜色,那你应该使用uniform变量,如果你希望每个三角形顶点有不同颜色,你应该使用attribute变量。从这开始,我称呼他们为“uniforms”和“attributes”。

Uniforms能被任意着色器访问,但是Attributes必须先进入顶点着色器,而非片段着色器。顶点着色器在需要时会将该值传给片段着色器。这因为Uniforms像常量-它们不会被任何着色器更改。然而,Attributes不是常量。顶点着色器会改变Attribute变量的值,在片段着色器获取之前。就是说,顶点着色器的输出就是片段着色器的输入。

为了设置Uniform的值,我们可以调用glUniform*系列函数。而设置Attribute的值,我们需要在VBO中保存,并且和VAO一起发送给着色器,就像前一篇教程里的glVertexAttribPointer。加入你不想把值存在VBO里,你也可以使用glVertexAttrib*系列函数来设置Attribute值。

贴图

贴图,大体上来说就是你应用在3D物体上的2D图像。它有其它用途,但显示2D图像在3D几何上是最常用的。有1D,2D,3D贴图,但本文只讲2D贴图。更深入阅读,请参见《Learning Modern 3D Graphics Programming》书中的Textures are not Pictures章节。

贴图是存放在显存里的。那就是说,你需要在使用之前上传你的贴图数据给显卡。这类似VBO在前文的作用-VBO也是在使用之前需要存放到显存上。

贴图的高和宽需要是2的幂次方。比如16,32,64,128,256,512。本文中使用的是256*256的图像作为贴图,如下图所示。

我们使用tdogl:Bitmap来加载“hazard.png”的原始像素数据到内存中,参见stb_image帮助文档。然后我们使用tdogl:Texture上传原始像素数据给OpenGL贴图对象。幸运的是OpenGL中的贴图创建方法从面世到现在都没有实质性的变化,所以网上有大量的创建贴图的好文章。虽然贴图坐标的传输方式有变化,但创建贴图还是跟以前一样。

以下是tdogl:Texture的构造函数,用于OpenGL贴图创建。


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

Texture::Texture(const Bitmap& bitmap, GLint minMagFiler, GLint wrapMode) :

    _originalWidth((GLfloat)bitmap.width()),

    _originalHeight((GLfloat)bitmap.height())

{

    glGenTextures(1, &_object);

    glBindTexture(GL_TEXTURE_2D, _object);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minMagFiler);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, minMagFiler);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapMode);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapMode);

    glTexImage2D(GL_TEXTURE_2D,

                 0, 

                 TextureFormatForBitmapFormat(bitmap.format()),

                 (GLsizei)bitmap.width(), 

                 (GLsizei)bitmap.height(),

                 0, 

                 TextureFormatForBitmapFormat(bitmap.format()), 

                 GL_UNSIGNED_BYTE, 

                 bitmap.pixelBuffer());

    glBindTexture(GL_TEXTURE_2D, 0);

}

贴图坐标

毫无疑问,贴图坐标就是贴图上的坐标。关于贴图坐标比较奇特的是它们不是以像素为单位。它们范围是从0到1,(0, 0)是左下角,(1, 1)是右上角。假如你上传到OpenGL的图像是颠倒的,那(0, 0)就是左上角,而非左下角。将像素坐标转换为贴图坐标,你必须除上贴图的宽和高。比如,在256*256的图像中,像素坐标(128, 256)的贴图坐标是(0.5, 1)。

贴图坐标通常被称为UV坐标。你也可以叫它们是XY坐标,但是XYZ通常被用来表示顶点,我们不希望将这两者混淆。

贴图图像单元

贴图图像单元,亦或简称“贴图单元”,是在OpenGL中略怪异的一部分。你无法直接发送贴图给着色器。首先,你要绑定贴图到贴图单元,然后呢要发送贴图单元的索引给着色器

对于贴图单元是有数量限制的。在低端硬件上,如手机,它们只有两个贴图单元。既然如此,即使我们有许多的贴图,我们也只能同时使用两个贴图单元在着色器中。我们在本文中只用到了一个贴图,所以也只需要一个贴图单元,但它可以在多个不同的着色器中混合。

实现贴图

首先,让我们创建一个新的全局贴图。


1

tdogl::Texture* gTexture = NULL;

我们为加载“hazard.png”图片新增一个函数。该函数能被AppMain所调用。


1

2

3

4

5

static void LoadTexture() {

    tdogl::Bitmap bmp = tdogl::Bitmap::bitmapFromFile(ResourcePath("hazard.png"));

    bmp.flipVertically();

    gTexture = new tdogl::Texture(bmp);

}

下一步,我们给每个三角形的顶点一个贴图坐标。假如你跟上图比较过UV坐标,就可以看出按顺序这个坐标表示(中,上),(左,下)和(右,下)。


1

2

3

4

5

6

GLfloat vertexData[] = {

    // X,Y,Z,U,V

    0.0f, 0.8f, 0.0f,0.5f,1.0f,

    -0.8f,-0.8f,0.0f,0.0f,0.0f,

    0.8f,-0.8f, 0.0f,1.0f,0.0f,

};

现在我们需要修改片段着色器,使得它能使用贴图和贴图坐标作为输入。下面是新的片段着色器代码:


1

2

3

4

5

6

7

#version 150

uniform sampler2D tex; //this is the texture

in vec2 fragTexCoord; //this is the texture coord

out vec4 finalColor; //this is the output color of the pixel

void main() {

    finalColor = texture(tex, fragTexCoord);

}

uniform关键字说明tex是uniform变量。贴图是一致的,因为所有三角形顶点有相同的贴图。sampler2D是变量类型,说明它包含一个2D贴图。

fragTexCoord是attribute变量,因为每个三角形顶点是不同的贴图坐标。

texture函数是用来查找给定贴图坐标的像素颜色。在GLSL旧版本中,你应该使用texture2D函数来实现该功能。

我们无法直接传送attribute给判断着色器,因为attribute必须首先通过顶点着色器。这儿是修改过的顶点着色器:


1

2

3

4

5

6

7

8

9

#version 150

in vec3 vert;

in vec2 vertTexCoord;

out vec2 fragTexCoord;

void main() {

    // Pass the tex coord straight through to the fragment shader

    fragTexCoord = vertTexCoord;

    gl_Position = vec4(vert, 1);

}

顶点着色器使用vertTexCoord作为输入,并且将它不经修改,直接传给名为fragTexCoord的attribute片段着色器变量。

着色器有两个变量需要我们设置:vertTexCoordattribute变量和texuniform变量。让我们从设置tex变量开始。打开main.cpp,找到Render()函数。我们在绘制三角形之前设置texuniform变量:


1

2

3

glActiveTexture(GL_TEXTURE0);

glBindTexture(GL_TEXTURE_2D, gTexture->object());

gProgram->setUniform("tex", 0); //set to 0 because the texture is bound to GL_TEXTURE0

贴图在没有绑定到贴图单元时,是无法使用的。glActiveTexture告诉OpenGL我们希望使用哪个贴图单元。GL_TEXTURE0是第一个贴图单元,我们就使用它。

下一本,我们使用glBindTexture来绑定我们的贴图到激活的贴图单元。

然后我们设置贴图单元索引给texuniform着色器变量。我们使用0号贴图单元,所以我们设置tex变量为整数0。setUniform方法只是调用了glUnifrom1i函数。

最后一步,获取贴图坐标给vertTexCoordattribute变量。为了实现它,我们需要修改LoadTriangle()函数中的VAO。之前的代码是这样的:


1

2

3

4

5

6

7

8

9

10

// Put the three triangle vertices into the VBO

GLfloat vertexData[] = {

    //  X     Y     Z

     0.0f, 0.8f, 0.0f,

    -0.8f,-0.8f, 0.0f,

     0.8f,-0.8f, 0.0f

};

// connect the xyz to the "vert" attribute of the vertex shader

glEnableVertexAttribArray(gProgram->attrib("vert"));

glVertexAttribPointer(gProgram->attrib("vert"), 3, GL_FLOAT, GL_FALSE, 0, NULL);

现在我们需要改成这样:


1

2

3

4

5

6

7

8

9

10

11

12

13

// Put the three triangle vertices (XYZ) and texture coordinates (UV) into the VBO

GLfloat vertexData[] = {

    //  X     Y     Z       U     V

     0.0f, 0.8f, 0.0f,   0.5f, 1.0f,

    -0.8f,-0.8f, 0.0f,   0.0f, 0.0f,

     0.8f,-0.8f, 0.0f,   1.0f, 0.0f,

};

// connect the xyz to the "vert" attribute of the vertex shader

glEnableVertexAttribArray(gProgram->attrib("vert"));

glVertexAttribPointer(gProgram->attrib("vert"), 3, GL_FLOAT, GL_FALSE, 5*sizeof(GLfloat), NULL);

// connect the uv coords to the "vertTexCoord" attribute of the vertex shader

glEnableVertexAttribArray(gProgram->attrib("vertTexCoord"));

glVertexAttribPointer(gProgram->attrib("vertTexCoord"), 2, GL_FLOAT, GL_TRUE,  5*sizeof(GLfloat), (const GLvoid*)(3 * sizeof(GLfloat)));

我们第二次调用了glVertexAttribPointer,但我们也修改了第一个调用。最重要的是最后两个参数。

两个glVertexAttribPointer调用的倒数第二个参数都是5*sizeof(GLfloat)。这是“步长”参数。该参数是表明每个值开始位置的间隔是多少字节,或者说是到下个值开始的字节数。在两个调用中,每个值是5个GLFloat长度。举个例子,加入我们从“X”开始,往前数5个值,我们会落在下个“X”值上。从“U”开始也一样,也是往前数5个。该参数是字节单位,不是浮点作为单位,所以我们必须乘上浮点类型所占字节数。

最后一个参数glVertexAttribPointer是一个“偏移”参数。该参数需要知道从开始到第一个值有多少字节。开始是XYZ,所以偏移设置为NULL表示“到开始的距离为0字节”。第一个UV不在最前面-中间有3个浮点的距离。再说一遍,参数是以字节为单位,而非浮点,所以我们必须乘上浮点类型所占字节数。并且我们必须将数值转为const GLvoid*类型,因为在旧版本的OpenGL中该参数有别于现在的“偏移”。

现在,当你运行程序,你就能看到如本文最上方的那个三角形。

下篇预告

下一篇教程中我们会学一些矩阵相关的东西,使用矩阵来旋转立方体,移动相机,和添加透视投影。我们还会学习深度缓冲和基于时间更新的逻辑,比如动画。

更多OpenGL贴图相关资源

The texture page on the OpenGL wiki

The texturing chapters of the Learning Modern 3D Graphics Programming book

Tutorial 16 - Basic Texture Mapping by Etay Meiri

The texturing example code by Jakob Progsch

时间: 2024-11-05 16:07:32

现代OpenGL教程 02——贴图的相关文章

OpenGL教程翻译 第十六课 基本的纹理贴图

OpenGL教程翻译 第十六课 基本的纹理贴图 原文地址:http://ogldev.atspace.co.uk/(源码请从原文主页下载) Background 纹理贴图就是将任意一种类型的图片应用到3D模型的一个或多个面.图片(也可以称之为纹理)内容可以是任何东西,但是他们一般都是一些比如砖,叶子,地面等的图案,纹理贴图增加了场景的真实性.例如,对比下面的两幅图片. 为了进行纹理贴图,你需要进行三个步骤:将图片加载到OpenGl中,定义模型顶点的纹理坐标(以对其进行贴图),用纹理坐标对图片进行

NeHe OpenGL教程 第三十八课:资源文件

转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线教程的编写,以及yarn的翻译整理表示感谢. NeHe OpenGL第三十八课:资源文件 从资源文件中载入图像: 如何把图像数据保存到*.exe程序中,使用Windows的资源文件吧,它既简单又实用. 欢迎来到NeHe教程第38课.离上节课的写作已经有些时日了,加上写了一整天的code,也许笔头已经

NeHe OpenGL教程 第二十三课:球面映射

转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线教程的编写,以及yarn的翻译整理表示感谢. NeHe OpenGL第二十三课:球面映射 球面映射: 这一个将教会你如何把环境纹理包裹在你的3D模型上,让它看起来象反射了周围的场景一样. 球体环境映射是一个创建快速金属反射效果的方法,但它并不像真实世界里那么精确!我们从18课的代码开始来创建这个教程

NeHe OpenGL教程 第三十五课:播放AVI

转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线教程的编写,以及yarn的翻译整理表示感谢. NeHe OpenGL第三十五课:播放AVI 在OpenGL中播放AVI: 在OpenGL中如何播放AVI呢?利用Windows的API把每一帧作为纹理绑定到OpenGL中,虽然很慢,但它的效果不错.你可以试试. 首先我得说我非常喜欢这一章节.Jonat

NeHe OpenGL教程 第四十四课:3D光晕

转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线教程的编写,以及yarn的翻译整理表示感谢. NeHe OpenGL第四十四课:3D光晕 3D 光晕 当镜头对准太阳的时候就会出现这种效果,模拟它非常的简单,一点数学和纹理贴图就够了.好好看看吧. 大家好,欢迎来到新的一课,在这一课中我们将扩展glCamera类,来实现镜头光晕的效果.在日常生活中,

NeHe OpenGL教程 第四十三课:FreeType库

转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线教程的编写,以及yarn的翻译整理表示感谢. NeHe OpenGL第四十三课:FreeType库 在OpenGL中使用FreeType库 使用FreeType库可以创建非常好看的反走样的字体,记住暴雪公司就是使用这个库的,就是那个做魔兽世界的.尝试一下吧,我只告诉你了基本的使用方式,你可以走的更远

NeHe OpenGL教程 第四十二课:多重视口

转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线教程的编写,以及yarn的翻译整理表示感谢. NeHe OpenGL第四十二课:多重视口 多重视口 画中画效果,很酷吧.使用视口它变得很简单,但渲染四次可会大大降低你的显示速度哦:) 欢迎来到充满趣味的另一课.这次我将向你展示怎样在单个窗口内显示多个视口.这些视口在窗口模式下能正确的调整大小.其中有

NeHe OpenGL教程 第三十六课:从渲染到纹理

转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线教程的编写,以及yarn的翻译整理表示感谢. NeHe OpenGL第三十六课:从渲染到纹理 放射模糊和渲染到纹理: 如何实现放射状的滤镜效果呢,看上去很难,其实很简单.把渲染得图像作为纹理提取出来,在利用OpenGL本身自带的纹理过滤,就能实现这种效果,不信,你试试. 嗨,我是Dario Corn

NeHe OpenGL教程 第十七课:2D图像文字

转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线教程的编写,以及yarn的翻译整理表示感谢. NeHe OpenGL第十七课:2D图像文字 2D图像文字: 在这一课中,你将学会如何使用四边形纹理贴图把文字显示在屏幕上.你将学会如何把256个不同的文字从一个256x256的纹理图像中分别提取出来,并为每一个文字创建一个显示列表,接着创建一个输出函数