天空盒
或
天空穹顶
提供了有效且相对简单的方法,用来生成令人信服的地平线景观。
天空盒
- 如何为地平线制作纹理?
立方体有 6 个面,我们需要为这些面都添加纹理。
一种方法是使用 6 个图像文件和 6 个纹理单元。
另一种常见(且高效)的方式则是使用一个包含 6 个面的纹理的图像。
使用纹理立方体贴图为立方体添加纹理需要指定适当的纹理坐标。下图展示了纹理坐标的分布,这些坐标接着会分配给立方体的每个顶点。
- 如何让天空盒看起来“距离很远”?
通过使用以下两个技巧,可以使天空盒显得巨大(从而感觉距离很远):
(a)禁用深度测试并先渲染天空盒(在渲染场景中的其他对象时重新启用深度测试);
(b)天空盒随相机移动(如果相机需要移动)。
通过在禁用深度测试的情况下先绘制天空盒,深度缓冲器的值仍将全设为 1.0(即最远距离)。因此,场景中的所有其他对象将被完全渲染,即天空盒不会阻挡任何其他对象。这样,无论天空盒的实际大小如何,会使天空盒的各面的位置看起来比其他物体都更远。而实际的天空盒立方体本身可以非常小,只要它在相机移动时随相机一起移动即可。下图展示了从天空盒内部查看简单的场景。
场景中可见的天空盒部分是立方体贴图的最右侧部分。这是因为摄像机处于默认方向,面向 −Z 方向,因此正在观察天空盒立方体的背面(如“立方体贴图纹理坐标”图所示)。另请注意,立方体贴图的背面在场景中渲染时会呈水平反转状态;这是因为立方体贴图的“背面”部分已经折叠在相机周围,因此看起来是经过侧向翻转的(如顶部两图所示)。 - 如何构建纹理立方体贴图?
从图稿或照片构建纹理立方体贴图图像时,需要注意避免在立方体面交汇点处的“接缝”,并创建正确的透视图,才能让天空盒看起来逼真且无畸变。有许多工具可以辅助达成这一目标:Terragen、Autodesk 3Ds Max、Blender 和 Adobe Photoshop 都有用于构建或处理立方体贴图的工具。同时,还有许多网站提供各种现成的立方体地图。
天空穹顶
建立地平线效果的另一种方法是使用天空穹顶
。除了使用带纹理的球体(或半球体)代替带纹理的立方体,其基本思路与天空盒相同。与天空盒相同,我们首先渲染天空穹顶(禁用深度测试),并将摄像机保持在天空穹顶的中心位置(下图中的天空穹顶纹理是使用Terragen这个工具制作的)。
天空穹顶相比天空盒有自己的优势。例如,它们不易受到畸变和接缝的影响(尽管在纹理图像中必须考虑极点处的球形畸变)。
天空穹顶的缺点之一则是球体或穹顶模型比立方体模型更复杂,天空穹顶有更多的顶点,其数量取决于期望的精度。
当使用天空穹顶呈现室外场景时,通常与地平面或某种地形相结合。当使用天空穹顶呈现宇宙中的场景(例如星空)时,使用下图所示的球体通常更为实际(为了清晰地使球体可视化,球体表面添加了一道虚线)。
实现天空盒
尽管天空穹顶有许多优点,天空盒仍然更为常见。 OpenGL 对天空盒的支持也更好,在进行环境贴图时更方便(本章后面会介绍)。出于这些原因,我们将专注于天空盒的实现。天空盒有两种实现方法:从头开始构建一个简单的天空盒;或使用 OpenGL 中的立方体贴图工具。它们有各自的优点。
从头开始构建天空盒
这里,我们将看到如何简单地启用和禁用深度测试(只需要一行代码)。场景中仅包含一个带纹理的环面。
//所有变量声明,构造函数和 init()与之前相同
. . .
void display(GLFWwindow* window, double currentTime) {
// 清除颜色缓冲区和深度缓冲区,并像之前一样创建投影视图矩阵和摄像机视图矩阵
. . .
vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ));
glUseProgram(renderingProgram);
// 准备首先绘制天空盒。 M 矩阵将天空盒放置在摄像机位置
// 注:mMat当然不是负相机位置,因为相机在世界空间中位置就是(cameraX,cameraY,cameraZ)
mMat = glm::translate(glm::mat4(1.0f), glm::vec3(cameraX, cameraY, cameraZ));
// 构建 MODEL-VIEW 矩阵
mvMat = vMat * mMat;
// 如前,将 MV 和 PROJ 矩阵放入统一变量
. . .
// 设置包含顶点的缓冲区
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(0);
// 设置包含纹理坐标的缓冲区
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(1);
//激活天空盒纹理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, skyboxTexture);
glEnable(GL_CULL_FACE);
// 立方体缠绕顺序是顺时针的,但我们从内部查看,因此使用逆时针缠绕顺序:GL_CCW
glFrontFace(GL_CCW);
// 在[没有深度测试的情况下]绘制天空盒
glDisable(GL_DEPTH_TEST);
glDrawArrays(GL_TRIANGLES, 0, 36);
glEnable(GL_DEPTH_TEST);
//现在像之前一样绘制场景中的对象
. . .
glDrawElements( . . . ); //和之前的场景中的对象一样
}
void setupVertices(void) {
// cube_vertices 定义与之前相同
// 天空盒的立方体纹理坐标,如图“立方体贴图纹理坐标”所示
float cubeTextureCoord[72] = {
1.00f, 0.66f, 1.00f, 0.33f, 0.75f, 0.33f, // 背面右下角
0.75f, 0.33f, 0.75f, 0.66f, 1.00f, 0.66f, // 背面左上角
0.75f, 0.33f, 0.50f, 0.33f, 0.75f, 0.66f, // 右面右下角
0.50f, 0.33f, 0.50f, 0.66f, 0.75f, 0.66f, // 右面左上角
0.50f, 0.33f, 0.25f, 0.33f, 0.50f, 0.66f, // 正面右下角
0.25f, 0.33f, 0.25f, 0.66f, 0.50f, 0.66f, // 正面左上角
0.25f, 0.33f, 0.00f, 0.33f, 0.25f, 0.66f, // 左面右下角
0.00f, 0.33f, 0.00f, 0.66f, 0.25f, 0.66f, // 左面左上角
0.25f, 0.33f, 0.50f, 0.33f, 0.50f, 0.00f, // 下面右下角
0.50f, 0.00f, 0.25f, 0.00f, 0.25f, 0.33f, // 下面左上角
0.25f, 1.00f, 0.50f, 1.00f, 0.50f, 0.66f, // 上面右下角
0.50f, 0.66f, 0.25f, 0.66f, 0.25f, 1.00f // 上面左上角
};
//像往常一样为立方体和场景对象设置缓冲区
}
//用于加载着色器、纹理、 main()等的模块,如前
给的天空盒图片如下:
程序运行效果如下:
天空盒容易受到图像畸变和接缝的影响。 接缝指两个纹理图像接触的地方(比如沿着立方体的边缘)有时出现的可见线条。下图展示了一个图像上半部分出现接缝的示例,它是运行上面程序时出现的伪影。为了避免接缝,需要仔细构建立方体贴图图像,并分配精确的纹理坐标。有一些工具可以用来沿图像边缘减少接缝(例如GNU Image Manipulation Program)。
使用OpenGL立方体贴图
构建天空盒的另一种方法是使用 OpenGL纹理立方体贴图
。OpenGL 立方体贴图比我们在上面看到的简单方法稍微复杂一点。但是,使用 OpenGL 立方体贴图有自己的优点,例如减少接缝以及支持环境贴图。
OpenGL 纹理立方体贴图类似于稍后将要研究的 3D 纹理,它们都使用 3 个纹理坐标访问——通常标记为(s, t, r)。
OpenGL 纹理立方体贴图其中的图像以纹理图像的【左上角】为(0, 0, 0)。
上面程序通过读入单个图像来为立方体贴图添加纹理,而这里将用loadCubeMap()函数读入6个单独的立方体面图像文件。
在这里,SOIL2用于实例化和加载OpenGL立方体贴图也非常方便。
在使用OpenGL立方体贴图时,无须垂直翻转纹理,OpenGL会自动处理。
init()函数现在包含一个函数调用以启用 GL_TEXTURE_CUBE_MAP_SEAMLESS,它告诉 OpenGL 尝试混合立方体相邻的边以减少或消除接缝。
在display()中,立方体的顶点像以前一样沿管线向下发送,但这次不需要发送立方体的纹理坐标。我们将会看到,OpenGL 纹理立方体贴图通常使用立方体的顶点位置作为其纹理坐标。之后禁用深度测试并绘制立方体。然后为场景的其余部分重新启用深度测试。
完成后的 OpenGL 纹理立方体贴图使用了 int 类型的标识符进行引用。与阴影贴图时一样,通过将纹理包裹模式设置为“夹紧到边缘”,可以减少沿边框的伪影。在这种情况下,它还可以帮助进一步缩小接缝。请注意,这里需要为 3 个纹理坐标 s、 t 和 r 都设置纹理包裹模式。
在片段着色器中使用名为 samplerCube
的特殊类型的采样器访问纹理。在纹理立方体贴图中,【从采样器返回的值】是沿着方向向量(s, t, r)从原点“看到”的纹素(方向向量:模为零的向量)。因此,我们通常可以简单地使用传入的插值顶点位置作为纹理坐标。在顶点着色器中,我们将立方体顶点位置分配到输出纹理坐标属性中,以便在它们到达片段着色器时进行插值。另外需要注意,在顶点着色器中,我们将传入的视图矩阵转换为 3×3,然后再转换回 4×4。这个“技巧”有效地移除了平移分量,同时保留了旋转。这样,就将立方体贴图固定在了摄像机位置,同时仍允许合成相机“环顾四周”。
分析矩阵中值的存储 & mat4转mat3原理
(
平
移
变
换
)
(
X
+
T
x
Y
+
T
y
Z
+
T
z
1
)
=
[
1
0
0
T
x
0
1
0
T
y
0
0
1
T
z
0
0
0
1
]
×
(
X
Y
Z
1
)
(
缩
放
变
换
)
(
X
∗
S
x
Y
∗
S
y
Z
∗
S
z
1
)
=
[
S
x
0
0
0
0
S
y
0
0
0
0
S
z
0
0
0
0
1
]
×
(
X
Y
Z
1
)
(
绕
X
轴
旋
转
)
(
X
′
Y
′
Z
′
1
)
=
[
1
0
0
0
0
c
o
s
θ
−
s
i
n
θ
0
0
s
i
n
θ
c
o
s
θ
0
0
0
0
1
]
×
(
X
Y
Z
1
)
(
绕
Y
轴
旋
转
)
(
X
′
Y
′
Z
′
1
)
=
[
c
o
s
θ
0
s
i
n
θ
0
0
1
0
0
−
s
i
n
θ
0
c
o
s
θ
0
0
0
0
1
]
×
(
X
Y
Z
1
)
(
绕
Z
轴
旋
转
)
(
X
′
Y
′
Z
′
1
)
=
[
c
o
s
θ
−
s
i
n
θ
0
0
s
i
n
θ
c
o
s
θ
0
0
0
0
1
0
0
0
0
1
]
×
(
X
Y
Z
1
)
(平移变换) \left( \begin{array} { l } { X+T_x } \\ { Y+T_y } \\ { Z+T_z } \\ { 1 } \end{array} \right) = \left[ \begin{array} { l l l l } { 1 } & { 0 } & { 0 } & { T_x } \\ { 0 } & { 1 } & { 0 } & { T_y } \\ { 0 } & { 0 } & { 1 } & { T_z } \\ { 0 } & { 0 } & { 0 } & { 1 } \end{array} \right] \times \left( \begin{array} { l } { X } \\ { Y } \\ { Z } \\ { 1 } \end{array} \right)\\(缩放变换) \left( \begin{array} { l } { X*S_x } \\ { Y*S_y } \\ { Z*S_z } \\ { 1 } \end{array} \right) = \left[ \begin{array} { l l l l } { S_x } & { 0 } & { 0 } & { 0 } \\ { 0 } & { S_y } & { 0 } & { 0 } \\ { 0 } & { 0 } & { S_z } & { 0 } \\ { 0 } & { 0 } & { 0 } & { 1 } \end{array} \right] \times \left( \begin{array} { l } { X } \\ { Y } \\ { Z } \\ { 1 } \end{array} \right)\\(绕X轴旋转) \left( \begin{array} { l } { X' } \\ { Y' } \\ { Z' } \\ { 1 } \end{array} \right) = \left[ \begin{array} { l l l l } { 1 } & { 0 } & { 0 } & { 0 } \\ { 0 } & { cosθ } & { -sinθ } & { 0 } \\ { 0 } & { sinθ } & { cosθ } & { 0 } \\ { 0 } & { 0 } & { 0 } & { 1 } \end{array} \right] \times \left( \begin{array} { l } { X } \\ { Y } \\ { Z } \\ { 1 } \end{array} \right)\\(绕Y轴旋转) \left( \begin{array} { l } { X' } \\ { Y' } \\ { Z' } \\ { 1 } \end{array} \right) = \left[ \begin{array} { l l l l } { cosθ } & { 0 } & { sinθ } & { 0 } \\ { 0 } & { 1 } & { 0 } & { 0 } \\ { -sinθ } & { 0 } & { cosθ } & { 0 } \\ { 0 } & { 0 } & { 0 } & { 1 } \end{array} \right] \times \left( \begin{array} { l } { X } \\ { Y } \\ { Z } \\ { 1 } \end{array} \right)\\(绕Z轴旋转) \left( \begin{array} { l } { X' } \\ { Y' } \\ { Z' } \\ { 1 } \end{array} \right) = \left[ \begin{array} { l l l l } { cosθ } & { -sinθ } & { 0 } & { 0 } \\ { sinθ } & { cosθ } & { 0 } & { 0 } \\ { 0 } & { 0 } & { 1 } & { 0 } \\ { 0 } & { 0 } & { 0 } & { 1 } \end{array} \right] \times \left( \begin{array} { l } { X } \\ { Y } \\ { Z } \\ { 1 } \end{array} \right)
(平移变换)⎝⎜⎜⎛X+TxY+TyZ+Tz1⎠⎟⎟⎞=⎣⎢⎢⎡100001000010TxTyTz1⎦⎥⎥⎤×⎝⎜⎜⎛XYZ1⎠⎟⎟⎞(缩放变换)⎝⎜⎜⎛X∗SxY∗SyZ∗Sz1⎠⎟⎟⎞=⎣⎢⎢⎡Sx0000Sy0000Sz00001⎦⎥⎥⎤×⎝⎜⎜⎛XYZ1⎠⎟⎟⎞(绕X轴旋转)⎝⎜⎜⎛X′Y′Z′1⎠⎟⎟⎞=⎣⎢⎢⎡10000cosθsinθ00−sinθcosθ00001⎦⎥⎥⎤×⎝⎜⎜⎛XYZ1⎠⎟⎟⎞(绕Y轴旋转)⎝⎜⎜⎛X′Y′Z′1⎠⎟⎟⎞=⎣⎢⎢⎡cosθ0−sinθ00100sinθ0cosθ00001⎦⎥⎥⎤×⎝⎜⎜⎛XYZ1⎠⎟⎟⎞(绕Z轴旋转)⎝⎜⎜⎛X′Y′Z′1⎠⎟⎟⎞=⎣⎢⎢⎡cosθsinθ00−sinθcosθ0000100001⎦⎥⎥⎤×⎝⎜⎜⎛XYZ1⎠⎟⎟⎞
如果不用上面“技巧”的话,就用下述代码:
float cameraX = 0.0f; cameraY = 0.0f; cameraZ = 5.0f;
vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ));
mMat = glm::translate(glm::mat4(1.0f), glm::vec3(cameraX, cameraY, cameraZ));
mvMat = vMat * mMat;
即,将天空盒放置在摄像机位置,然后得到MV矩阵。
如果用上面的“技巧”:
[假设]
transMat = glm::translate(glm::mat4(1.0f), glm::vec3(5, 6, 7));
transRotMat = glm::rotate(transMat, toRadians(30), glm::vec3(1, 1, 1));
[调试]
打印transMat对象:(注意,OpenGL是按【列】组织数据的)
[0][0]=1 [0][1]=0 [0][2]=0 [0][3]=0 1 0 0 【5】
[1][0]=0 [1][1]=1 [1][2]=0 [1][3]=0 0 1 0 【6】
[2][0]=0 [2][1]=0 [2][2]=1 [2][3]=0 0 0 1 【7】
[3][0]=5 [3][1]=6 [3][2]=7 [3][3]=1 => 0 0 0 1
同理,打印transRotMat对象:
0.910684 -0.244017 0.333333 【5】
0.333333 0.910684 -0.244017 【6】
-0.244017 0.333333 0.910684 【7】
0 0 0 1
同理,打印mat4(mat3(transMat))对象:
0.910684 -0.244017 0.333333 【0】
0.333333 0.910684 -0.244017 【0】
-0.244017 0.333333 0.910684 【0】
【0】 【0】 【0】 【1】
通过上面数据的对象可知:
mat4转mat3——就是去掉最后一列和最后一行;
mat3转mat4——在最右列和最下行加上[0 0 0 1]完成的。
transRotMat通过先转mat3再转回mat4,移除了平移分量,同时保留了旋转。
VIEW矩阵(负相机位置矩阵)是为了将MODEL转换到VIEW空间。
而,天空盒是不需要根据相机空间来作变换的,是个绝对位置,所以天空盒这个MODEL可以保留旋转而得去除平移变换。
那么在VIEW矩阵表达了位置、平移、旋转、(相机是没有缩放说法的),而去掉矩阵第四列,又不影响位置、旋转,所以可以通过mat4->mat3->mat4完成去掉平移分量的操作。
. . .
int brickTexture, skyboxTexture;
int renderingProgram, renderingProgramCubeMap;
. . .
void setupVertices() {
float cubeVertexPositions[108] =
{ -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f,
1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, -1.0f,
1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f,
1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f,
1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f,
-1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f
};
...
}
void init(GLFWwindow* window) {
renderingProgram = Utils::createShaderProgram("vertShader.glsl", "fragShader.glsl");
renderingProgramCubeMap = Utils::createShaderProgram("vertCubeShader.glsl", "fragCubeShader.glsl");
glfwGetFramebufferSize(window, &width, &height);
aspect = (float)width / (float)height;
pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
setupVertices();
brickTexture = Utils::loadTexture("brick.jpg");// 场景中的环面
skyboxTexture = Utils::loadCubeMap("cubeMap");// 包含天空盒纹理的文件夹
glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);// 混合立方体相邻的边以减少或消除接缝
}
void display(GLFWwindow* window, double currentTime) {
// 清除颜色缓冲区和深度缓冲区,并像之前一样创建投影视图矩阵和摄像机视图矩阵
. . .
vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ));
// 准备首先绘制天空盒—注意,现在它的渲染程序不同了
glUseProgram(renderingProgramCubeMap);
// 将 P、 V 矩阵传入相应的统一变量
vLoc = glGetUniformLocation(renderingProgramCubeMap, "v_matrix");
glUniformMatrix4fv(vLoc, 1, GL_FALSE, glm::value_ptr(vMat));
projLoc = glGetUniformLocation(renderingProgramCubeMap, "p_matrix");
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));
. . .
// 立方体的顶点像以前一样沿管线向下发送
// 初始化立方体的顶点缓冲区(这里不再需要纹理坐标缓冲区)
// OpenGL纹理立方体贴图通常使用立方体的顶点位置作为其纹理坐标。
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(0);
// 激活立方体贴图纹理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, skyboxTexture);
// 禁用深度测试,之后绘制立方体贴图
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glDisable(GL_DEPTH_TEST);
glDrawArrays(GL_TRIANGLES, 0, 36);
glEnable(GL_DEPTH_TEST);
// 绘制场景其余内容
. . .
// 下面也有绑定纹理单元代码:
...
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, brickTexture);
...
}
GLuint Utils::loadCubeMap(const char *mapDir) {
GLuint textureRef;
// 假设 6 个纹理图像文件 xp、 xn、 yp、 yn、 zp、 zn 都是 JPG 格式图像
string xp = mapDir + "/xp.jpg";
string xn = mapDir + "/xn.jpg";
string yp = mapDir + "/yp.jpg";
string yn = mapDir + "/yn.jpg";
string zp = mapDir + "/zp.jpg";
string zn = mapDir + "/zn.jpg";
textureRef = SOIL_load_OGL_cubemap(
xp.c_str(), xn.c_str(), yp.c_str(),
yn.c_str(), zp.c_str(), zn.c_str(),
SOIL_LOAD_AUTO, SOIL_CREATE_NEW_ID, SOIL_FLAG_MIPMAPS
);
if (textureRef == 0)
cout << "didnt find cube map image file" << endl;
glBindTexture(GL_TEXTURE_CUBE_MAP, textureRef);// 绑定纹理对映射到Cube纹理单元
// 减少接缝。注:3个纹理坐标s、t、r都要设置
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);
return textureRef;
}
// 顶点着色器:vertCubeShader.glsl
#version 430
out vec3 tc;
uniform mat4 v_matrix;
uniform mat4 p_matrix;
layout(location = 0) in vec3 position;
void main(void) {
tc = position;
mat4 v3_matrix = mat4(mat3(v_matrix));
gl_Position = p_matrix * v3_matrix * vec4(position, 1.0);
}
// 片段着色器:fragCubeShader.glsl
#version 430
in vec3 tc;
out vec4 fragColor;
layout (binding = 0) uniform samplerCube samp;// 注意是samplerCube类型
void main(void) {
fragColor = texture(samp, tc);
}
6张贴图如下:
xp:positive x,朝向+x面上的贴图;xn:negative x,朝向-x面上的贴图;
yp:positive y,朝向+y面上的贴图;yn:negative y,朝向-y面上的贴图;
zp:positive z,朝向+z面上的贴图;zn:negative z,朝向-z面上的贴图。
SOIL_load_OGL_cubemap()函数传入图片数据的顺序是:xp,xn,yp,yn,zp,zn;这个顺序是影响立方体贴哪个面的顺序的。
下面是 SOIL2.h 源码中的注释:
/**
Loads 6 images from disk into an OpenGL cubemap texture.
param x_pos_file the name of the file to upload as the +x cube face
param x_neg_file the name of the file to upload as the -x cube face
param y_pos_file the name of the file to upload as the +y cube face
param y_neg_file the name of the file to upload as the -y cube face
param z_pos_file the name of the file to upload as the +z cube face
param z_neg_file the name of the file to upload as the -z cube face
param force_channels 0-image format, 1-luminous, 2-luminous/alpha, 3-RGB, 4-RGBA
param reuse_texture_ID 0-generate a new texture ID, otherwise reuse the texture ID (overwriting the old texture)
param flags can be any of SOIL_FLAG_POWER_OF_TWO | SOIL_FLAG_MIPMAPS | SOIL_FLAG_TEXTURE_REPEATS | SOIL_FLAG_MULTIPLY_ALPHA |
SOIL_FLAG_INVERT_Y | SOIL_FLAG_COMPRESS_TO_DXT | SOIL_FLAG_DDS_LOAD_DIRECT
return 0-failed, otherwise returns the OpenGL texture handle
**/
unsigned int SOIL_load_OGL_cubemap
(
const char *x_pos_file,
const char *x_neg_file,
const char *y_pos_file,
const char *y_neg_file,
const char *z_pos_file,
const char *z_neg_file,
int force_channels,
unsigned int reuse_texture_ID,
unsigned int flags
);
对比我们之前用的SOIL_load_OGL_texture,最后几个参数是一样的
unsigned int SOIL_load_OGL_texture
(
const char *filename,
int force_channels,
unsigned int reuse_texture_ID,
unsigned int flags
);
运行效果如下图:
从效果图中,我们看到的是zn.jpg图像的一部分,而zn.jpg是朝-z方向的面,所以从相机看向-z方向看到的就是zn.jpg这张贴图了。
环境贴图
我们从未对非常闪亮
的物体进行建模,例如镜子或铬制品。这些物体在有小范围镜面高光的同时,还能够反射出周围物体的镜像。当我们看向这些物品时,我们会看到房间里的其他东西,有时甚至会看到我们自己的倒影。ADS 照明模型并没有提供模拟这种效果的方法。
不过,纹理立方体贴图提供了一种相对简单的方法来模拟(至少部分模拟)反射表面。其诀窍是使用立方体贴图来构造反射对象本身。
同样的技巧也适用于通过对反光物体添加天空穹顶纹理图像来用天空穹顶替代天空盒的情况。
使用 OpenGL GL_TEXTURE_CUBE_MAP 实现立方体贴图,那么 OpenGL 可以使用与之前为立方体添加纹理相同的方法来进行环境贴图查找。我们使用视图向量和曲面法向量计算视图向量对应的离开对象表面的反射向量。然后可以使用反射向量直接对纹理立方体贴图图像进行采样。查找过程由 OpenGL samplerCube 辅助实现;回忆上述 samplerCube 使用视图方向向量索引。因此,反射向量非常适用于查找所需的纹素。
对于法向量和反射向量,在之前的代码中,它们用于实现 ADS 照明模型;而在这里,它们用于计算环境贴图的纹理坐标。所以它们用于完全不同的目的。
void display(GLFWwindow* window, double currentTime) {
// 用来绘制立方体贴图的代码未改变
. . .
// 所有修改都在绘制环面的部分
glUseProgram(renderingProgram);
// 矩阵变换的统一变量位置,包括法向量的变换
mvLloc = glGetUniformLocation(renderingProgram, "mv_matrix");
projLoc = glGetUniformLocation(renderingProgram, "proj_matrix");
nLoc = glGetUniformLocation(renderingProgram, "norm_matrix");
// 构建 MODEL 矩阵,如前
mMat = glm::translate(glm::mat4(1.0f), glm::vec3(torLocX, torLocY, torLocZ));
// 构建 MODEL-VIEW 矩阵,如前
mvMat = vMat * mMat;
invTrMat = glm::transpose(glm::inverse(mvMat));
// 法向量变换现在在统一变量中
glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));
glUniformMatrix4fv(nLoc, 1, GL_FALSE, glm::value_ptr(invTrMat));
// 激活环面顶点缓冲区,如前
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(0);
// 我们需要激活环面法向量缓冲区
glBindBuffer(GL_ARRAY_BUFFER, vbo[2]);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(1);
// 环面纹理现在是立方体贴图
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_CUBE_MAP, skyboxTexture);
// 绘制环面的过程未做更改
glClear(GL_DEPTH_BUFFER_BIT);
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glDepthFunc(GL_LEQUAL);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo[3]);
glDrawElements(GL_TRIANGLES, numTorusIndices, GL_UNSIGNED_INT, 0);
}
// 顶点着色器
#version 430
layout(location = 0) in vec3 position;
layout(location = 1) in vec3 normal;
out vec3 varyingNormal;
out vec3 varyingVertPos;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
uniform mat4 norm_matrix;
void main(void) {
varyingVertPos = (mv_matrix * vec4(position, 1.0)).xyz;
varyingNormal = (norm_matrix * vec4(normal, 1.0)).xyz;
gl_Position = proj_matrix * mv_matrix * vec4(position, 1.0);
}
// 片段着色器
#version 430
in vec3 varyingVertPos;
in vec3 varyingNormal;
out vec4 fragColor;
layout(binding = 0) uniform samplerCube tex_map;
void main(void) {
vec3 r = -reflect(normalize(-varyingVertPos), normalize(varyingNormal));
fragColor = texture(tex_map, r);
}
运行效果如下图:
从纹理(现在是立方体贴图)检索输出颜色, 使用反射向量而非纹理坐标进行查找。之前texture()的第二个参数是vec2类型,现在是vec3类型。
在这个例子中,我们甚至会感到在环面的左下方似乎有一个镜面高光,因为立方体贴图中包括太阳在水中反射的倒影。
关于reflect函数
第一个参数是入射光向量,第二个参数是(也必须是)归一化的表面法向量。
通过平面几何的知识,很容易推导出R = I - 2×N●I×N。
由于|N|=1,而我们示例中也将I归一化了,所以有:
R = I - 2×cosα×N(α是向量I与法向量N的夹角,并非上图中的β)
如果α=180°,即I与N反向,则R = -I,即:垂直入射光会全部按入射光反方向反射。
如果α=90°,R = I,即:反射光等于入射光。
如果α=0°,即I与N同向,则R = -I,即:与法向量平行的光全部按入射光反方向反射。
|R| = |I - 2×cosα×N| != |I|,所以反射光向量大小与入射光大小并不相等,并且这种不相等与归一化的入射光也没有关系,即使入射光向量归一化了,反射光向量大小也不等于1。只不过可以用公式算出来它的大小及方向。不过可以肯定的是如上图中的两个β角是相同的,但向量N不是向量I与向量R的角平分线,事实上表平面才是它们的角平分线。
我们在Blog“✠OpenGL-7-光照”有上图,并且GLSL代码中求反射光用的是如下代码:
// 计算光照向量L基于表面法向量N的反射光向量,并归一化
vec3 R = normalize(reflect(-L, N));
ADS模型对于顶点着色器,有如下代码:
// 计算相机空间中的光照向量(从相机空间中的顶点指向相机空间中的光源)
varyingLightDir = light.position - varyingVertPos;
vec3 L = normalize(varyingLightDir);
vec3 N = normalize(varyingNormal);
vec3 R = normalize(reflect(-L, N));
而我们这里没有光源的存在,所以自定义了一种反射方式:
vec3 r = -reflect(normalize(-varyingVertPos), normalize(varyingNormal));
如果加上这么一句:r = r × 1000000F 或 r = r × 0.000001F,运行程序,最终呈现效果是一模一样的。也就说明了,通过向量查找,应该就是向量射线所能穿过的表面的材质。
如果代码中vec3 r = -reflect(…); 去掉前面的负号,效果就不一样了,如下图:
明显没有一种反射倒影的效果。
环境贴图的主要限制之一是它只能构建反射立方体贴图内容的对象。
在场景中渲染的其他对象并不会出现在使用贴图模拟反射的对象中。这种限制是否可以接受取决于场景的性质。如果场景中存在必须出现在镜面或铬制对象中的对象,则必须使用其他方法。一种常见的方法是使用模板缓冲区
。模板缓冲区是通过 OpenGL 访问的第三个缓冲区——在颜色缓冲区和 Z 缓冲区之后。该主题不会涉及。
补充说明
用户也可以在没有 SOIL2 的情况下实例化并加载 OpenGL立方体贴图。虽然该主题不会涉及,但基本步骤如下:
(1)使用 C++工具读取 6 个图像文件(它们必须是正方形);
(2)使用 glGenTextures()为立方体贴图创建纹理及其整型引用;
(3)调用 glBindTexture(),指定纹理的 ID 和 GL_TEXTURE_CUBE_MAP;
(4)使用 glTexStorage2D()指定立方体贴图的存储需求;
(5)调用 glTexImage2D()或 glTexSubImage2D()将图像分配给立方体的各个面。
我们没有介绍天空穹顶的实现,虽然它们在某些方面可以说比天空盒更简单,并且不易受到失真的影响, 甚至用它实现环境贴图也更简单——至少在数学上——但 OpenGL 对立方体贴图的支持常常使得天空盒更加实用。
我们只简要介绍了可能出现的一些问题(例如接缝),但根据使用的纹理图像文件,可能会出现其他问题,需要额外修复。尤其是在动画场景中或相机可以通过交互进行移动时。
如何生成可用且令人信服的纹理立方体贴图图像,在这方面有许多优秀的工具,其中最受欢迎的是Terragen。本文中的所有立方体贴图都是使用Terragen制作的。