笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家。特邀编辑,畅销书做者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术具体解释》电子工业出版社等。数组
CSDN视频网址:http://edu.csdn.net/lecturer/144架构
在这里介绍立方体贴图主要是告诉读者,利用立方体贴图原理。咱们可以作很是多事情:比方天空盒,环境映射中的反射和折射效果等等。固然环境映射也可以使用一张纹理贴图实现。这个会在博文的最后给读者介绍,如下開始介绍立方体贴图实现原理。函数
咱们在游戏开发中一般的作法是将2D纹理映射到物体的一个面上,本篇博文介绍的是将多个纹理组合起来映射到一个单一纹理,这就称为立方体贴图。在介绍立方体贴图前。先解释一下纹理採样,假设咱们有一个单位立方体。有个以原点为起点的方向向量在它的中心。post
从立方体贴图上使用橘黄色向量採样一个纹理值看起来下图:性能
注意,方向向量的大小可有可无。一旦提供了方向,OpenGL就会获取方向向量碰触到立方体表面上的对应的纹理像素。这样就返回了正确的纹理採样值。优化
方向向量触碰到立方体表面的一点也就是立方体贴图的纹理位置。这意味着仅仅要立方体的中心位于原点上。咱们就可以使用立方体的位置向量来对立方体贴图进行採样。而后咱们就可以获取所有顶点的纹理坐标。就和立方体上的顶点位置同样。所得到的结果是一个纹理坐标,经过这个纹理坐标就能获取到立方体贴图上正确的纹理。
ui
如下開始介绍建立立方体贴图,立方体贴图和其它纹理同样。因此要建立一个立方体贴图,在进行不论什么纹理操做以前,需要生成一个纹理。激活对应纹理单元而后绑定到合适的纹理目标上。此次要绑定到 GL_TEXTURE_CUBE_MAP
纹理类型:spa
GLuint textureID; glGenTextures(1, &textureID); glBindTexture(GL_TEXTURE_CUBE_MAP, textureID);
因为立方体贴图包括6个纹理,立方体的每个面一个纹理,咱们必须调用glTexImage2D
函数6次,函数的參数和前面教程讲的相似。然而此次咱们必须把纹理目标(target)參数设置为立方体贴图特定的面。这是告诉OpenGL咱们建立的纹理是对应立方体哪一个面的。.net
所以咱们便需要为立方体贴图的每个面调用一次 glTexImage2D
。3d
因为立方体贴图有6个面,OpenGL就提供了6个不一样的纹理目标,来应对立方体贴图的各个面。
纹理目标(Texture target) | 方位 |
---|---|
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 | 前 |
GL_TEXTURE_CUBE_MAP_POSITIVE_X
为起始来对它们进行遍历,每次迭代枚举值加
1
,这样循环所有的纹理目标效率较高:
int width,height; unsigned char* image; for(GLuint i = 0; i < textures_faces.size(); i++) { image = SOIL_load_image(textures_faces[i], &width, &height, 0, SOIL_LOAD_RGB); glTexImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image ); }
这儿咱们有个vector叫textures_faces
。它包括立方体贴图所各个纹理的文件路径,并且以上表所列的顺序排列。它将为每个当前绑定的cubemp的每个面生成一个纹理。
因为立方体贴图和其它纹理没什么不一样,咱们也要定义它的围绕方式和过滤方式:
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_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_WRAP_R。它的含义仅仅是简单的设置了纹理的R坐标。R坐标对应于纹理的第三个维度(就像位置的z同样)。
咱们把放置方式设置为 GL_CLAMP_TO_EDGE
,因为纹理坐标在两个面之间,因此可能并不能触及哪一个面(因为硬件限制),所以使用 GL_CLAMP_TO_EDGE
后OpenGL会返回它们的边界的值。虽然咱们可能在两个两个面中间进行的採样。
在绘制物体以前。将使用立方体贴图,而在渲染前咱们要激活对应的纹理单元并绑定到立方体贴图上。这和普通的2D纹理没什么差异。
在片断着色器中。咱们也必须使用一个不一样的採样器——samplerCube,用它来从texture
函数中採样。但是此次使用的是一个vec3
方向向量,代替vec2
。如下是一个片断着色器使用了立方体贴图的样例:
in vec3 textureDir; // 用一个三维方向向量来表示立方体贴图纹理的坐标 uniform samplerCube cubemap; // 立方体贴图纹理採样器 void main() { color = texture(cubemap, textureDir); }立方体贴图的技术实现了后,咱们利用该技术实现天空盒:
天空盒是一个立方体,它由六个面组成,每个面需要一个贴图,网上有很是多这种天空盒的资源。固然美术也可以制做,这些天空盒一般有如下的样式:
假设你把这6个面折叠到一个立方体中,你机会得到模拟了一个巨大的风景的立方体。原理清楚了。接下来使用程序建立天空盒:
因为天空盒实际上就是一个立方体贴图,载入天空盒和以前咱们载入立方体贴图的没什么大的不一样。为了载入天空盒咱们将使用如下的函数。它接收一个包括6个纹理文件路径的vector:
GLuint loadCubemap(vector<const GLchar*> faces) { GLuint textureID; glGenTextures(1, &textureID); glActiveTexture(GL_TEXTURE0); int width,height; unsigned char* image; glBindTexture(GL_TEXTURE_CUBE_MAP, textureID); for(GLuint i = 0; i < faces.size(); i++) { image = SOIL_load_image(faces[i], &width, &height, 0, SOIL_LOAD_RGB); glTexImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image ); } glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_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); glBindTexture(GL_TEXTURE_CUBE_MAP, 0); return textureID; }在咱们调用这个函数以前,咱们将把合适的纹理路径载入到一个vector之中,顺序仍是依照立方体贴图枚举的特定顺序:
vector<const GLchar*> faces; faces.push_back("right.jpg"); faces.push_back("left.jpg"); faces.push_back("top.jpg"); faces.push_back("bottom.jpg"); faces.push_back("back.jpg"); faces.push_back("front.jpg"); GLuint cubemapTexture = loadCubemap(faces);
立方体贴图用于给3D立方体帖上纹理,可以用立方体的位置做为纹理坐标进行採样。当一个立方体的中心位于原点(0,0。0)的时候。它的每个位置向量也就是以原点为起点的方向向量。
这个方向向量就是咱们要获得的立方体某个位置的对应纹理值。
出于这个理由,咱们仅仅需要提供位置向量。而无需纹理坐标。为了渲染天空盒,咱们需要一组新着色器,它们不会太复杂。因为咱们仅仅有一个顶点属性。顶点着色器很是easy:
#version 330 core layout (location = 0) in vec3 position; out vec3 TexCoords; uniform mat4 projection; uniform mat4 view; void main() { gl_Position = projection * view * vec4(position, 1.0); TexCoords = position; }注意。顶点着色器有意思的地方在于咱们把输入的位置向量做为输出给片断着色器的纹理坐标。
片断着色器就会把它们做为输入去採样samplerCube:
#version 330 core in vec3 TexCoords; out vec4 color; uniform samplerCube skybox; void main() { color = texture(skybox, TexCoords); }
这样天空盒才干成为所有其它物体的背景来绘制出来。
glDepthMask(GL_FALSE); skyboxShader.Use(); // ... Set view and projection matrix glBindVertexArray(skyboxVAO); glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); glDepthMask(GL_TRUE); // ... Draw rest of the scene咱们但愿天空盒以玩家为中心。这样无论玩家移动了多远。天空盒都不会变近,这样就产生一种四周的环境真的很是大的印象。当前的视图矩阵对所有天空盒的位置进行了转转缩放和平移变换。因此玩家移动,立方体贴图也会跟着移动。咱们打算移除视图矩阵的平移部分,这样移动就影响不到天空盒的位置向量了。在基础光照教程里咱们提到过咱们可以仅仅用4X4矩阵的3×3部分去除平移。咱们可以简单地将矩阵转为33矩阵再转回来。就能达到目标。
glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix()));这会移除所有平移。但保留所有旋转。所以用户仍然可以向四面八方看。因为有了天空盒。场景便可变得巨大了。
假设你加入些物体而后自由在当中游荡一下子你会发现场景的真实度有了极大提高。最后的效果看起来像这样:
实现上述效果的核心代码例如如下所看到的:
// Clear buffers glClearColor(0.1f, 0.1f, 0.1f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Draw skybox first glDepthMask(GL_FALSE);// Remember to turn depth writing off skyboxShader.Use(); glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix())); // Remove any translation component of the view matrix glm::mat4 projection = glm::perspective(camera.Zoom, (float)screenWidth/(float)screenHeight, 0.1f, 100.0f); glUniformMatrix4fv(glGetUniformLocation(skyboxShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view)); glUniformMatrix4fv(glGetUniformLocation(skyboxShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); // skybox cube glBindVertexArray(skyboxVAO); glActiveTexture(GL_TEXTURE0); glUniform1i(glGetUniformLocation(shader.Program, "skybox"), 0); glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); glDepthMask(GL_TRUE); // Then draw scene as normal shader.Use(); glm::mat4 model; view = camera.GetViewMatrix(); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model)); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view)); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); // Cubes glBindVertexArray(cubeVAO); glActiveTexture(GL_TEXTURE0); glUniform1i(glGetUniformLocation(shader.Program, "texture_diffuse1"), 0); glBindTexture(GL_TEXTURE_2D, cubeTexture); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); // Swap the buffers glfwSwapBuffers(window);
因此最后渲染天空盒就可以给咱们带来轻微的性能提高。
採用这种方式,深度缓冲被所有物体的深度值全然填充,因此咱们仅仅需要渲染经过前置深度測试的那部分天空的片断便可了,并且能显著下降片断着色器的调用。问题是天空盒是个立方体,极有可能会渲染失败。因为极有可能通只是深度測试。简单地不用深度測试渲染它也不是解决方式,这是因为天空盒会在以后覆盖所有的场景中其它物体。咱们需要耍个花招让深度缓冲相信天空盒的深度缓冲有着最大深度值1.0。如此仅仅要有个物体存在深度測试就会失败,看似物体就在它前面了。
透视除法(perspective division)是在顶点着色器执行以后执行的。把gl_Position
的xyz坐标除以w元素。咱们从深度測试教程了解到除法结果的z元素等于顶点的深度值。利用这个信息。咱们可以把输出位置的z元素设置为它的w元素,这样就会致使z元素等于1.0了,因为,当透视除法应用后。它的z元素转换为w/w = 1.0:
void main() { vec4 pos = projection * view * vec4(position, 1.0); gl_Position = pos.xyww; TexCoords = position; }
终于。标准化设备坐标就总会有个与1.0相等的z值了。1.0就是深度值的最大值。仅仅有在没有不论什么物体可见的状况下天空盒才会被渲染(仅仅有经过深度測试才渲染。不然假若有不论什么物体存在,就不会被渲染,仅仅去渲染物体)。
咱们必须改变一下深度方程,把它设置为GL_LEQUAL
,原来默认的是GL_LESS
。深度缓冲会为天空盒用1.0这个值填充深度缓冲。因此咱们需要保证天空盒是使用小于等于深度缓冲来经过深度測试的,而不是小于。
// Clear buffers glClearColor(0.1f, 0.1f, 0.1f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Draw scene as normal shader.Use(); glm::mat4 model; glm::mat4 view = camera.GetViewMatrix(); glm::mat4 projection = glm::perspective(camera.Zoom, (float)screenWidth/(float)screenHeight, 0.1f, 100.0f); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model)); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view)); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); // Cubes glBindVertexArray(cubeVAO); glActiveTexture(GL_TEXTURE0); glUniform1i(glGetUniformLocation(shader.Program, "texture_diffuse1"), 0); glBindTexture(GL_TEXTURE_2D, cubeTexture); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); // Draw skybox as last glDepthFunc(GL_LEQUAL); // Change depth function so depth test passes when values are equal to depth buffer's content skyboxShader.Use(); view = glm::mat4(glm::mat3(camera.GetViewMatrix())); // Remove any translation component of the view matrix glUniformMatrix4fv(glGetUniformLocation(skyboxShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view)); glUniformMatrix4fv(glGetUniformLocation(skyboxShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); // skybox cube glBindVertexArray(skyboxVAO); glActiveTexture(GL_TEXTURE0); glUniform1i(glGetUniformLocation(shader.Program, "skybox"), 0); glBindTexture(GL_TEXTURE_CUBE_MAP, cubemapTexture); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); glDepthFunc(GL_LESS); // Set depth function back to default // Swap the buffers glfwSwapBuffers(window);
使用带有场景环境的立方体贴图,咱们还可以让物体有一个反射或折射属性。像这样使用了环境立方体贴图的技术叫作环境贴图技术,当中最重要的两个是反射(reflection)和折射(refraction)。
凡是是一个物体(或物体的某部分)反射(Reflect)他周围的环境的属性,比方物体的颜色多少有些等于它周围的环境,这要基于观察者的角度。
好比一个镜子是一个反射物体:它会基于观察者的角度泛着它周围的环境。
反射的基本思路不难。下图展现了咱们怎样计算反射向量,而后使用这个向量去从一个立方体贴图中採样:
咱们基于观察方向向量I和物体的法线向量N计算出反射向量R。咱们可以使用GLSL的内建函数reflect来计算这个反射向量。
最后向量R做为一个方向向量对立方体贴图进行索引/採样。返回一个环境的颜色值。
最后的效果看起来就像物体反射了天空盒。
因为咱们在场景中已经设置了一个天空盒,建立反射就不难了。
咱们改变一下箱子使用的那个片断着色器。给箱子一个反射属性:
#version 330 core in vec3 Normal; in vec3 Position; out vec4 color; uniform vec3 cameraPos; uniform samplerCube skybox; void main() { vec3 I = normalize(Position - cameraPos); vec3 R = reflect(I, normalize(Normal)); color = texture(skybox, R); }咱们先来计算观察/摄像机方向向量I,而后使用它来计算反射向量R,接着咱们用R从天空盒立方体贴图採样。要注意的是,咱们有了片断的插值Normal和Position变量,因此咱们需要修正顶点着色器适应它。
#version 330 core layout (location = 0) in vec3 position; layout (location = 1) in vec3 normal; out vec3 Normal; out vec3 Position; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { gl_Position = projection * view * model * vec4(position, 1.0f); Normal = mat3(transpose(inverse(model))) * normal; Position = vec3(model * vec4(position, 1.0f)); }
咱们用了法线向量,因此咱们打算使用一个法线矩阵(normal matrix)变换它们。Position
输出的向量是一个世界空间位置向量。
顶点着色器输出的Position
用来在片断着色器计算观察方向向量。
因为咱们使使用方法线。你还得更新顶点数据,更新属性指针。还要确保设置cameraPos
的uniform。
而后在渲染箱子前咱们还得绑定立方体贴图纹理:
glBindVertexArray(cubeVAO); glBindTexture(GL_TEXTURE_CUBE_MAP, skyboxTexture); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0);
C++程序实现的核心代码例如如下所看到的:
// Clear buffers glClearColor(0.1f, 0.1f, 0.1f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Draw scene as normal shader.Use(); glm::mat4 model; glm::mat4 view = camera.GetViewMatrix(); glm::mat4 projection = glm::perspective(camera.Zoom, (float)screenWidth/(float)screenHeight, 0.1f, 100.0f); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model)); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view)); glUniformMatrix4fv(glGetUniformLocation(shader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); glUniform3f(glGetUniformLocation(shader.Program, "cameraPos"), camera.Position.x, camera.Position.y, camera.Position.z); // Cubes glBindVertexArray(cubeVAO); glBindTexture(GL_TEXTURE_CUBE_MAP, skyboxTexture); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); // Draw skybox as last glDepthFunc(GL_LEQUAL); // Change depth function so depth test passes when values are equal to depth buffer's content skyboxShader.Use(); glm::mat4 view = glm::mat4(glm::mat3(camera.GetViewMatrix())); // Remove any translation component of the view matrix glUniformMatrix4fv(glGetUniformLocation(skyboxShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view)); glUniformMatrix4fv(glGetUniformLocation(skyboxShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection)); // skybox cube glBindVertexArray(skyboxVAO); glBindTexture(GL_TEXTURE_CUBE_MAP, skyboxTexture); glDrawArrays(GL_TRIANGLES, 0, 36); glBindVertexArray(0); glDepthFunc(GL_LESS); // Set depth function back to default // Swap the buffers glfwSwapBuffers(window);另外环境映射做用于角色身上的效果例如如下所看到的:
如下介绍反射技术,
环境映射的还有一个形式叫作折射(Refraction),它和反射差点儿相同。折射是光线经过特定材质对光线方向的改变。咱们一般看到像水同样的表面。光线并不是直接经过的,而是让光线弯曲了一点。它看起来像你把半仅仅手伸进水里的效果。
折射遵照斯涅尔定律,使用环境贴图看起来就像这样:
咱们有个观察向量I,一个法线向量N,此次折射向量是R。就像你所看到的那样。观察向量的方向有轻微弯曲。弯曲的向量R随后用来从立方体贴图上採样。
折射可以经过GLSL的内建函数refract来实现。除此以外还需要一个法线向量,一个观察方向和一个两种材质之间的折射指数。
折射指数决定了一个材质上光线扭曲的数量,每个材质都有本身的折射指数。下表是常见的折射指数:
材质 | 折射指数 |
---|---|
空气 | 1.00 |
水 | 1.33 |
冰 | 1.309 |
玻璃 | 1.52 |
宝石 | 2.42 |
咱们使用这些折射指数来计算光线经过两个材质的比率。在咱们的样例中,光线/视线从空气进入玻璃(假设咱们假设箱子是玻璃作的)因此比率是1.001.52 = 0.658。
咱们已经绑定了立方体贴图,提供了定点数据,设置了摄像机位置的uniform。现在仅仅需要改变片断着色器:
void main() { float ratio = 1.00 / 1.52; vec3 I = normalize(Position - cameraPos); vec3 R = refract(I, normalize(Normal), ratio); color = texture(skybox, R); }经过改变折射指数你可以建立出全然不一样的视觉效果。编译执行应用,结果也不是太有趣,因为咱们仅仅是用了一个普通箱子,这不能显示出折射的效果。看起来像个放大镜。
使用同一个着色器,纳米服模型却可以展现出咱们期待的效果:玻璃制物体。
以上都是利用立方体贴图实现的技术,事实上实现环境映射并不必定必须使用立方体贴图,也可以使用一张贴图实现效果,详情查看笔者已经介绍过的案例:
Cocos2d-x 3.x 图形学渲染系列二十二 关于使用一张贴图实现的环境映射效果,效果例如如下所看到的: