继续 LearnOpenGL
纹理(Texture)。纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节;你可以想象纹理是一张绘有砖块的纸,无缝折叠贴合到你的3D的房子上,这样你的房子看起来就像有砖墙外表了。因为我们可以在一张图片上插入非常多的细节,这样就可以让物体非常精细而不用指定额外的顶点。
纹理坐标
为了能够把纹理映射(Map)到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标(Texture Coordinate),用来标明该从纹理图像的哪个部分采样(译注:采集片段颜色)。之后在图形的其它片段上进行片段插值(Fragment Interpolation)。
纹理坐标在x和y轴上,范围为0到1之间(注意我们使用的是2D纹理图像)。使用纹理坐标获取纹理颜色叫做采样(Sampling)。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。下面的图片展示了我们是如何把纹理坐标映射到三角形上的。
我们为三角形指定了3个纹理坐标点。如上图所示,我们希望三角形的左下角对应纹理的左下角,因此我们把三角形左下角顶点的纹理坐标设置为(0, 0);三角形的上顶点对应于图片的上中位置所以我们把它的纹理坐标设置为(0.5, 1.0);同理右下方的顶点设置为(1, 0)。我们只要给顶点着色器传递这三个纹理坐标就行了,接下来它们会被传片段着色器中,它会为每个片段进行纹理坐标的插值。
GLfloat texCoords[] = {
0.0f, 0.0f, // 左下角
1.0f, 0.0f, // 右下角
0.5f, 1.0f // 上中
};
加载与创建纹理
GLuint texture;
glGenTextures(1, &texture);
glGenTextures
函数首先需要输入生成纹理的数量,然后把它们储存在第二个参数的
GLuint
数组中(我们的例子中只是一个单独的
GLuint
),就像其他对象一样,我们需要绑定它,让之后任何的纹理指令都可以配置当前绑定的纹理:
glBindTexture(GL_TEXTURE_2D, texture);
纹理已经绑定了,我们纹理可以通过glTexImage2D
来生成:
Image* img = new Image();
img->initWithImageFile("wall.jpg");
int width = img->getWidth();
int height = img->getHeight();
unsigned char* imgdata = img->getData();
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, imgdata);
glGenerateMipmap(GL_TEXTURE_2D);
函数很长,参数一个一个地讲解:
- 第一个参数指定了纹理目标(Target)。设置为GL_TEXTURE_2D意味着会生成与当前绑定的纹理对象在同一个目标上的纹理(任何绑定到GL_TEXTURE_1D和GL_TEXTURE_3D的纹理不会受到影响)。
- 第二个参数为纹理指定多级渐远纹理的级别,如果你希望单独手动设置每个多级渐远纹理的级别的话。这里我们填0,也就是基本级别。
- 第三个参数告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有
RGB
值,因此我们也把纹理储存为RGB
值。 - 第四个和第五个参数设置最终的纹理的宽度和高度。我们之前加载图像的时候储存了它们,所以我们使用对应的变量。
- 下个参数应该总是被设为
0
(历史遗留问题)。 - 第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像,并把它们储存为
char
(byte)数组,我们将会传入对应值。 - 最后一个参数是真正的图像数据。
当调用glTexImage2D时,当前绑定的纹理对象就会被附加上纹理图像。然而,目前只有基本级别(Base-level)的纹理图像被加载了,如果要使用多级渐远纹理,我们必须手动设置所有不同的图像(不断递增第二个参数)。或者,直接在生成纹理之后调用glGenerateMipmap。这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。
生成了纹理和相应的多级渐远纹理后,释放图像的内存并解绑纹理对象是一个很好的习惯: //释放内存,解绑texture
CC_SAFE_DELETE(img);
glBindTexture(GL_TEXTURE_2D, 0);
生成一个纹理的过程应该看起来像这样:
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// 为当前绑定的纹理对象设置环绕、过滤方式
...
// 加载并生成纹理
int width, height;
unsigned char* image = SOIL_load_image("container.jpg", &width, &height, 0, SOIL_LOAD_RGB);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
glGenerateMipmap(GL_TEXTURE_2D);
SOIL_free_image_data(image);
glBindTexture(GL_TEXTURE_2D, 0);
应用纹理
这部分我们会使用glDrawElements绘制矩形。我们需要告知OpenGL如何采样纹理,所以我们必须使用纹理坐标更新顶点数据:GLfloat vertices[] = {
// ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 -
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上
};
由于我们添加了一个额外的顶点属性,我们必须告诉OpenGL我们新的顶点格式:
glVertexAttribPointer(2, 2, GL_FLOAT,GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
glEnableVertexAttribArray(2);
接着我们需要调整顶点着色器使其能够接受顶点坐标为一个顶点属性,并把坐标传给片段着色器:
// texture.vsh
attribute vec3 a_position;
attribute vec3 a_color;
attribute vec2 a_texcoord;
varying vec4 v_vertexColor;
varying vec2 v_textureCoord;
void main()
{
gl_Position = vec4(a_position, 1);
v_vertexColor = vec4(a_color, 1);
v_textureCoord = vec2(a_texcoord.x, 1.0 - a_texcoord.y);
}
片段着色器也应该能访问纹理对象,但是我们怎样能把纹理对象传给片段着色器呢?GLSL有一个供纹理对象使用的内建数据类型,叫做采样器 (Sampler),它以纹理类型作为后缀,比如
sampler1D
、
sampler3D
,或在我们的例子中的
sampler2D
。我们可以简单声明一个
uniform sampler2D
把一个纹理添加到片段着色器中,这个uniform不需要手动赋值,下面详解。
//texture.fsh
varying vec4 v_vertexColor;
varying vec2 v_textureCoord;
uniform sampler2D u_myTexture; //纹理采样器, 它不需要手动赋值,在onDraw()中绑定纹理时自动赋值。
void main()
{
gl_FragColor = texture2D(u_myTexture, v_textureCoord); //使用GLSL内建的texture2D函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是对应的纹理坐标。
}
我们使用GLSL内建的texture2D
函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是对应的纹理坐标。texture2D
函数会使用之前设置的纹理参数对相应的颜色值进行采样。这个片段着色器的输出就是纹理的(插值)纹理坐标上的(过滤后的)颜色。
在调用glDrawElements 之前绑定纹理了,它会自动把纹理赋值给片段着色器的采样器:
//绑定纹理, 它会自动把纹理赋值给片段着色器texture.fsh的采样器v_myTexture.因为一个纹理的默认纹理单元是0,它是默认的激活纹理单元。
GL::bindTexture2D(textureId);
glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
我们还可以把得到的纹理颜色与顶点颜色混合,来获得更有趣的效果。我们只需把纹理颜色与顶点颜色在片段着色器中相乘来混合二者的颜色:
gl_FragColor = v_vertexColor * texture2D(u_myTexture, v_textureCoord);
全部代码如下:
//
// Triangle.cpp
// shaderTest
//
// Created by MacSBL on 2016/12/15.
//
//
#include "OpenGLTexture.h"
bool OpenGLTexture::init()
{
if (!Layer::init()) {
return false;
}
GLfloat vertices[] = {
//顶点 //颜色 //纹理坐标
0.5, 0.5, 0, 1, 0, 0, 1.0, 1.0, //右上
0.5, -0.5, 0, 0, 1, 0, 1.0, 0, //右下
-0.5, -0.5, 0, 0, 0, 1, 0, 0, //左下
-0.5, 0.5, 0, 0, 0, 0, 0, 1,//左上
};
GLuint indices[] = {
0, 1, 3,
1, 2, 3
};
//创建并绑定纹理
//方法2
glGenTextures(1, &textureId);
glBindTexture(GL_TEXTURE_2D, textureId);
//纹理环绕方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
//纹理过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//加载纹理
Image* img = new Image();
img->initWithImageFile("wall.jpg");
int width = img->getWidth();
int height = img->getHeight();
unsigned char* imgdata = img->getData();
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, imgdata);
glGenerateMipmap(GL_TEXTURE_2D);
//释放内存,解绑texture
CC_SAFE_DELETE(img);
glBindTexture(GL_TEXTURE_2D, 0);
///
//1、绑定vao
//顶点数组对象(Vertex Array Object)被绑定后,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
// 2. 把顶点数组复制到缓冲(vbo)中供OpenGL使用
//使用glGenBuffers函数和一个缓冲ID生成一个VBO对象
GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
//把之前定义的顶点数据复制到缓冲的内存中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//3, ebo
GLuint ebo;
glGenBuffers(1, &ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//解析顶点数据, 应用到第1个顶点属性上
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8* sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// 颜色,应用到第2个属性上
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);
//纹理坐标,应用到第3个属性上
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
glEnableVertexAttribArray(2);
//解除绑定vao和vbo
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
//——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
auto program = new GLProgram;
program->initWithFilenames("texture.vsh", "texture.fsh");
// program->initWithFilenames("texture.vsh", "texturemix.fsh");
program->link();
this->setGLProgram(program);
return true;
}
void OpenGLTexture::visit(cocos2d::Renderer *renderer, const cocos2d::Mat4 &parentTransform, uint32_t parentFlags)
{
Layer::visit(renderer, parentTransform, parentFlags);
_command.init(_globalZOrder);
_command.func = CC_CALLBACK_0(OpenGLTexture::onDraw, this);
Director::getInstance()->getRenderer()->addCommand(&_command);
}
void OpenGLTexture::onDraw()
{
//获取当前node 的shader
auto glprogram = getGLProgram();
//需要在init中指定shader才能在这use
glprogram->use();
//绑定纹理, 它会自动把纹理赋值给片段着色器texture.fsh的采样器v_myTexture.因为一个纹理的默认纹理单元是0,它是默认的激活纹理单元。
GL::bindTexture2D(textureId);
glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}
纹理单元
你可能会奇怪为什么sampler2D
变量是个uniform,我们却不用glUniform给它赋值。使用glUniform1i,我们可以给纹理采样器分配一个位置值,这样的话我们能够在一个片段着色器中设置多个纹理。一个纹理的位置值通常称为一个纹理单元(Texture Unit)。一个纹理的默认纹理单元是0,它是默认的激活纹理单元,所以教程前面部分我们没有分配一个位置值。
纹理单元的主要目的是让我们在着色器中可以使用多于一个的纹理。通过把纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们首先激活对应的纹理单元。就像glBindTexture一样,我们可以使用glActiveTexture激活纹理单元,传入我们需要使用的纹理单元:
glActiveTexture(GL_TEXTURE0); //在绑定纹理之前先激活纹理单元
glBindTexture(GL_TEXTURE_2D, texture);
激活纹理单元之后,接下来的glBindTexture 函数调用会绑定这个纹理到当前激活的纹理单元,纹理单元 GL_TEXTURE0 默认总是被激活,所以我们在前面的例子里当我们使用
glBindTexture
的时候,无需激活任何纹理单元。
//在一个片段着色器中设置多个纹理
//texturemix.fsh
varying vec4 v_vertexColor;
varying vec2 v_textureCoord;
uniform sampler2D u_myTexture1; //纹理采样器,
uniform sampler2D u_myTexture2; //
void main()
{
//最终输出颜色现在是两个纹理的结合。GLSL内建的mix函数需要接受两个值作为参数,并对它们根据第三个参数进行线性插值。如果第三个值是0.0,它会返回第一个输入;如果是1.0,会返回第二个输入值。0.2会返回80%的第一个输入颜色和20%的第二个输入颜色,即返回两个纹理的混合色。
gl_FragColor = mix(texture2D(u_myTexture1, v_textureCoord), texture2D(u_myTexture2, v_textureCoord), 0.4);
}
我们现在需要载入并创建另一个纹理;你应该对这些步骤很熟悉了。记得创建另一个纹理对象,载入图片,使用glTexImage2D生成最终纹理。
为了使用第二个纹理(以及第一个),我们必须改变一点渲染流程,先绑定两个纹理到对应的纹理单元,然后定义哪个uniform采样器对应哪个纹理单元:
/
//纹理单元, uniform采样器对应纹理单元
GL::bindTexture2DN(0, textureId);
glUniform1i(glGetUniformLocation(glprogram->getProgram(), "u_myTexture1"), 0);
GL::bindTexture2DN(1, textureId2);
glUniform1i(glGetUniformLocation(glprogram->getProgram(), "u_myTexture2"), 1);
/
如果纹理上下颠倒了, 有两个选择:
- 我们可以改变顶点数据的纹理坐标,翻转
y
值(用1减去y坐标)。 - 我们可以编辑顶点着色器来自动翻转
y
坐标,替换TexCoord
的值为TexCoord = vec2(texCoord.x, 1.0f - texCoord.y);
。
//
// Triangle.cpp
// shaderTest
//
// Created by MacSBL on 2016/12/15.
//
//
#include "OpenGLTexture.h"
bool OpenGLTexture::init()
{
if (!Layer::init()) {
return false;
}
GLfloat vertices[] = {
//顶点 //颜色 //纹理坐标
0.5, 0.5, 0, 1, 0, 0, 1.0, 1.0, //右上
0.5, -0.5, 0, 0, 1, 0, 1.0, 0, //右下
-0.5, -0.5, 0, 0, 0, 1, 0, 0, //左下
-0.5, 0.5, 0, 0, 0, 0, 0, 1,//左上
};
GLuint indices[] = {
0, 1, 3,
1, 2, 3
};
//创建并绑定纹理
//
//方法1
auto sprite = Sprite::create("awesomeface.png");
textureId2 = sprite->getTexture()->getName();
//
//方法2
glGenTextures(1, &textureId);
glBindTexture(GL_TEXTURE_2D, textureId);
//纹理环绕方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
//纹理过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//加载纹理
Image* img = new Image();
img->initWithImageFile("wall.jpg");
int width = img->getWidth();
int height = img->getHeight();
unsigned char* imgdata = img->getData();
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, imgdata);
glGenerateMipmap(GL_TEXTURE_2D);
//释放内存,解绑texture
CC_SAFE_DELETE(img);
glBindTexture(GL_TEXTURE_2D, 0);
///
//1、绑定vao
//顶点数组对象(Vertex Array Object)被绑定后,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
// 2. 把顶点数组复制到缓冲(vbo)中供OpenGL使用
//使用glGenBuffers函数和一个缓冲ID生成一个VBO对象
GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
//把之前定义的顶点数据复制到缓冲的内存中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//3, ebo
GLuint ebo;
glGenBuffers(1, &ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//解析顶点数据, 应用到第1个顶点属性上
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8* sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// 颜色,应用到第2个属性上
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);
//纹理坐标,应用到第3个属性上
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
glEnableVertexAttribArray(2);
//解除绑定vao和vbo
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
//——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
auto program = new GLProgram;
// program->initWithFilenames("texture.vsh", "texture.fsh");
program->initWithFilenames("texture.vsh", "texturemix.fsh");
program->link();
this->setGLProgram(program);
return true;
}
void OpenGLTexture::visit(cocos2d::Renderer *renderer, const cocos2d::Mat4 &parentTransform, uint32_t parentFlags)
{
Layer::visit(renderer, parentTransform, parentFlags);
_command.init(_globalZOrder);
_command.func = CC_CALLBACK_0(OpenGLTexture::onDraw, this);
Director::getInstance()->getRenderer()->addCommand(&_command);
}
void OpenGLTexture::onDraw()
{
//获取当前node 的shader
auto glprogram = getGLProgram();
//需要在init中指定shader才能在这use
glprogram->use();
/
//纹理单元, uniform采样器对应纹理单元
GL::bindTexture2DN(0, textureId);
glUniform1i(glGetUniformLocation(glprogram->getProgram(), "u_myTexture1"), 0);
GL::bindTexture2DN(1, textureId2);
glUniform1i(glGetUniformLocation(glprogram->getProgram(), "u_myTexture2"), 1);
/
glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}