我们已经学习过2D纹理了,今天来学习立方体贴图,立方体纹理就是多个纹理组合起来映射到一张纹理。
简单来说,立方体贴图就是一个包含了6个2D纹理的纹理,每个2D纹理都组成了立方体的一个面:一个有纹理的立方体
想一想,显示2D纹理时需要指定纹理坐标,那这个立方体贴图怎么指定?立方体贴图有一个非常有用的特性,它可以通过一个方向向量来进行索引/采样。假设我们有一个1x1x1的单位立方体,方向向量的原点位于它的中心。使用一个橘黄色的方向向量来从立方体贴图上采样一个纹理值会像是这样:
那立方体贴图有什么用呢?游戏中天空盒经常会用到这个
1、创建立方体贴图
立方体贴图在glsl代码中声明方式和2D纹理不一样了,另外它的纹理坐标也不再是vec2
,而是vec3
了
uniform samplerCube skybox;
in vec3 TexCoords;
其它方式和2D纹理差不多,都是先生成,再绑定,再设置参数,只不过参数会略有不同:
std::vector<std::string> faces
{
"res/right.jpg",
"res/left.jpg",
"res/top.jpg",
"res/bottom.jpg",
"res/front.jpg",
"res/back.jpg"
};
glGenTextures(1, &mSkyId);
glBindTexture(GL_TEXTURE_CUBE_MAP, mSkyId);
int width, height;
for (int i = 0; i < faces.size(); i++) {
void *pixel;
MyGlRenderContext::getInstance()->getBitmap(faces[i].data(), &pixel, width, height);
LOGI("prepareTexture width = %d, height = %d", width, height);
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGBA, width, height, 0, GL_RGBA,
GL_UNSIGNED_BYTE, pixel);
}
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
请注意,立方体贴图的类型是:GL_TEXTURE_CUBE_MAP
立方体有6个面,需要给每个面指定图像buf
纹理目标 | 方位 |
---|---|
GL_TEXTURE_CUBE_MAP_POSITIVE_X |
右 |
GL_TEXTURE_CUBE_MAP_NEGATIVE_X |
左 |
GL_TEXTURE_CUBE_MAP_POSITIVE_Y |
上 |
GL_TEXTURE_CUBE_MAP_NEGATIVE_Y |
下 |
GL_TEXTURE_CUBE_MAP_POSITIVE_Z |
后 |
GL_TEXTURE_CUBE_MAP_NEGATIVE_Z |
前 |
因为这些枚举值都是递增的,所以上面代码就在for循环中使用递增来指定纹理目标了。
2、天空盒
想象下用户在一个大的立方体空间中,空间上下前后左右有不同的背景,用户可以随意看不同的方向,能看到不同的风景,这就是天空盒
现在我们要写一个天空盒,还要在这个天空盒中绘制一个立方体,滑动页面时,整个天空盒也跟着调整方向
首先,我们要绘制天空盒,也要绘制立方体,绘制这两个东东的glsl代码会有明显不同,所以我们需要准备不同的glsl代码:
绘制天空盒的glsl代码:
#version 300 es
out vec4 FragColor;
in vec3 TexCoords;
uniform samplerCube skybox;
void main()
{
FragColor = texture(skybox, TexCoords);
}
#version 300 es
layout (location = 0) in vec3 aPos;
out vec3 TexCoords;
uniform mat4 projection;
uniform mat4 view;
void main()
{
TexCoords = aPos;
vec4 pos = projection * view * vec4(aPos, 1.0);
gl_Position = pos.xyww;
}
绘制盒子的glsl代码:
#version 300 es
layout(location = 0) in vec3 a_position;
layout(location = 1) in vec2 a_texCoord;
out vec2 v_texCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main() {
gl_Position = projection * view * model * vec4(a_position, 1.0);
v_texCoord = vec2(a_texCoord.x, 1.0 - a_texCoord.y);
}
#version 300 es
precision mediump float;
in vec2 v_texCoord;
out vec4 outColor;
uniform sampler2D cubeId;
void main() {
outColor = texture(cubeId, v_texCoord);
}
真正绘制代码:
void SkyBoxSample::draw() {
glEnable(GL_DEPTH_TEST);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
auto rat = MyGlRenderContext::getInstance()->getWidth() * 1.0f /
MyGlRenderContext::getInstance()->getHeight();
glm::mat4 model = glm::mat4(1.0f);
glm::mat4 projection = glm::perspective(glm::radians(45.0f), rat, 0.1f, 100.0f);
glm::mat4 view = camera.getViewMatrix();
cubeShader.use();
cubeShader.setMat4("model", model);
cubeShader.setMat4("view", view);
cubeShader.setMat4("projection", projection);
glBindVertexArray(mCubeVao);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, mCubeId);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
glDepthFunc(GL_LEQUAL);
shader.use();
view = glm::mat4(glm::mat3(camera.getViewMatrix()));
shader.setMat4("view", view);
shader.setMat4("projection", projection);
glBindVertexArray(mSkyVao);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, mSkyId);
shader.setInt("skybox", 0);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
glDepthFunc(GL_LESS);
}
2.1、深度处理
注意到绘制代码中,我们是先绘制盒子再绘制天空盒。正常情况下,天空盒会把盒子盖住,但这显示不是我们想要的。天空盒只是一个大的背景,任何东西都应该显示在天空盒之上。
如何让天空盒显示在最下面呢?结合深度测试逻辑,只要天空盒的深度缓冲值永远是最大值1.0,天空盒就会永远显示在最下面。那怎么让天空盒的深度值是最大值呢?
透视除法是在顶点着色器运行之后执行的,将gl_Position
的xyz
坐标除以w分量。我们又从深度测试知识知道,相除结果的z分量等于顶点的深度值。所以,如果我们把天空图的z值一直置为w,那么它的深度值肯定就会一直为1.0,永远在最下面了。
gl_Position = pos.xyww;
2.2、天空盒不能移动
天空盒不能移动,这个很好理解吧。
但当我们手指移动时,view
矩阵就会变化,会产生旋转、缩放和位移。view
矩阵变化了就会影响天空盒。我们需要去掉天空盒的位移效果:
view = glm::mat4(glm::mat3(camera.getViewMatrix()));
为什么这么做就可以去掉位移效果呢?
旋转、缩放两个操作均可以使用矩阵做乘法操作实现,但位移不行,如果矩阵不增加齐次坐标就只能使用加法实现。为了统一矩阵的乘法操作,所以opengl里的坐标是四维的,xyzw,增加了一个w坐标,即齐次坐标,通过齐次坐标才实现使用乘法来实现位移,现在我们去掉齐次坐标,自然就能去掉位移效果。
齐次坐标原理可参见这篇文章:OPENGL–快速理解齐次坐标作用 – 知乎 (zhihu.com)
3、反射
盒子上的纹理是我们自己添加上去的,能不能反射周边的环境
下面这张图展示了我们如何计算反射向量,并如何使用这个向量来从立方体贴图中采样:
我们根据观察方向向量I
和物体的法向量N
,来计算反射向量R
。我们可以使用GLSL内建的reflect函数来计算这个反射向量。最终的R
向量将会作为索引/采样立方体贴图的方向向量,返回环境的颜色值。最终的结果是物体看起来反射了天空盒。
I
怎么得到呢?可以根据camera位置和当前元素位置计算出来。
#version 300 es
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
out vec3 normal;
out vec3 position;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main() {
normal = mat3(transpose(inverse(model))) * aNormal;
position = vec3(model * vec4(aPos, 1.0));
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
#version 300 es
precision mediump float;
out vec4 outColor;
in vec3 normal;
in vec3 position;
uniform samplerCube skybox;
uniform vec3 cameraPos;
void main() {
vec3 I = normalize(position - cameraPos);
vec3 R = reflect(I, normalize(normal));
outColor = vec4(texture(skybox, R).rgb, 1.0);
}
盒子使用如上glsl代码后,再更新下坐标,因为需要法向量了,就能看见本文开篇时的效果了。