<四>初探opengl ,使用纹理
能画出三角形没什么用,但能画出一张纹理,就可以做一个游戏引擎了!(哪有那么简单啦)
好,如何使用纹理,我先大概简述下过程,和三角形一样,我们先定义出顶点坐标,另外,我们需要定义纹理坐标。
纹理坐标是0-1,使用纹理坐标颜色叫作采样。如果我们使用他三个纹理点,那代码应该如下:
float texCoords[] = { 0.0f, 0.0f, // 左下角 1.0f, 0.0f, // 右下角 0.5f, 1.0f // 上中 };
纹理环绕方式
如果我们取纹理取到0-1外面的话,会发生什么事情呢?系统默认是重复整个纹理,opengl提供了更多的选择。
我们通过glTexParamteri方法设置各种参数值,这里我们可以这么设置
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT); //水平方向镜像重复
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT); //垂直方向
第一个是我们设置的参数,因为是2d图,就用gl_texture_2d,第二个是纹理轴,第三个是参数,如果我们使用BORDER,还需要指定一个颜色值供填充。
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f }; glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
纹理过滤
平时我们也可以看到一个小图放大后会出现锯齿,也看到一些放大后很模糊,但没锯齿,比较平滑, 这个现象跟纹理的过滤方式有关,我们讨论最重要的2个方式临近过滤和线性过滤。
临近就是坐标点最近的这个像素颜色作为返回,线性则是把坐标附近的纹素拿出来通过按比例计算出一个插值作为返回。 效果图如此。
这个操作在放大和缩小的时候可以设置,原图就不需要怎么取点问题了。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); //缩小操作
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //放大操作
多级渐远纹理
当我们图片太小的时候,可能拿到的片段就很小了,这样获取的正确的颜色值就很困难,还可能造成内存的浪费,此时一个多级渐远纹理的概念出现。
可以看到出现了多个级别的纹理,每一个是上一级大小的1/2,opengl提供了一个glGenerateMipmaps函数来实现,当到一定大小后使用对应级别的纹理。
当然这样也可能出现上面说的情况,所以opengl还提供了更细腻的过滤方式,就是基于2个级别的纹理的插值过滤。
使用方法和上面一样,这里注意的是放大一般不会用多级纹理的过滤方式,因为多级纹理一般是处理缩小时的问题。如果为放大设置多级的过滤,就会出现一个GL_INVALID_ENUM的错误代码。
加载和创建纹理
开始加载我们的纹理,但纹理有各种格式,pngjpg一堆,每一种的结构不一样,我们需用一个stb_image.h库来加载,先去下载这个文件 点我下载,下载后加入到工程目录下
#define STB_IMAGE_IMPLEMENTATION //需要定义这个,让它只包含函数定义源码,等于说这个文件变为一个cpp文件,否则要你再写cpp来实现。
#include "stb_image.h"
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0); //文件目录,长宽,颜色通道个数,最后默认0
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); //纹理对象,多级纹理级别,0是基本级别,纹理储存方式, 宽, 高, 0(历史遗留问题),原图格式,我们用rgb方式加载,存为(byte)数组,最后是数据
glGenerateMipmap(GL_TEXTURE_2D); //生成多级纹理
stbi_image_free(data); 加载完后可以把释放掉了
然后,我们需要参考三角形那节,定义好顶点以及该顶点的纹理坐标
float 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 // 左上 };
这里位置要和纹理坐标对应上,不然会出现奇怪的问题。
然后我们设置链接节点
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); //现在一个节点信息有8个float,所以步长为8,每一步偏移6个float才是纹理坐标 glEnableVertexAttribArray(2);
这样,我们的顶点已经定义好了,那么我们就得修改下顶点着色器
#version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec3 aColor; layout (location = 2) in vec2 aTexCoord; //纹理坐标定义在位置2里 out vec3 ourColor; out vec2 TexCoord; void main() { gl_Position = vec4(aPos, 1.0); ourColor = aColor; TexCoord = aTexCoord; //把坐标传给顶点着色器使用 }
片段着色器
#version 330 core out vec4 FragColor; in vec3 ourColor; in vec2 TexCoord; uniform sampler2D ourTexture; void main() { FragColor = texture(ourTexture, TexCoord); }
texture可以用来进行颜色采样,前面是纹理,后面是坐标。第一个参数的纹理如何传递进来呢?GLSL有一个内建数据类型,叫采样器(Sampler),以类型为后缀,例如sampler1D,sampler3D,我们是2d图,用2D,通过对纹理采样,就能得出一个纹理颜色。
最后调用绘图方法即可画出纹理图案:
如果我们把颜色也混合进去,就会得到一个混色图案
FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);
纹理单元
当我们需要同时使用多个纹理混合在一起的时候,我们需要用到纹理单元。其存在的目的主要就是让我们在着色器中使用多于一个的纹理。用法如下:
glActiveTexture(GL_TEXTURE0); // 在绑定纹理之前先激活纹理单元 glBindTexture(GL_TEXTURE_2D, texture);
就是绑定前激活下就好,opengl至少保证有16个纹理单元可以使用,分别后缀为0-15,绑定完后,我们通过代码来设定对应的单元供给着色器使用
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0); // 手动设置 ourShader.setInt("texture2", 1); // 或者使用着色器类设置,这个我使用就报错说不能存在这个方法, 不知道什么原因
而片段着色器代码改为:
#version 330 core ... uniform sampler2D texture1; uniform sampler2D texture2; void main() { FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2); }
mix是混合方法,就是用前面片段乘以(1-a)混合到后面片段乘以a,让第一个片段比较明显。
这里纹理上下颠倒了,是因为opengl要求y轴坐标是图片的底部,但图片的y轴一般是在顶部。所以可以调用stbi_set_flip_vertically_on_load(true)来翻转过来。
基本加载纹理方式就介绍到这里,遇到一个坑就是图片加载后都歪得一批,本以为是顶点设置的问题,后来经过查询发现是因为图片没有按照4个倍数尺寸来定义,而系统默认用4来对齐,导致图片加载出现了问题。我们可以调用glPixelStorei(GL_UNPACK_ALIGNMENT, 1)来定义对齐方式,定义为1后,纹理就正常显示了,这里可以填1,2,4,8.