现代opengl绘制nurbs曲面是怎么绘制曲面的

第三十课 曲面细分
注意:此节中用到的颜色和位移纹理是用 Ben Cloward 创建的。
曲面细分( Tessellation )是 OpenGL4.x 中的一个令人兴奋的新特性,Tessellation 主要用于解决 3D 模型的静态属性,包括他们的精细度和多边形数量。具体来说就是当我们近距离观察一个复杂的模型(如人脸)时,我们希望能够看到这个模型的所有细节(例如皮肤的褶皱),所以我们需要使用一个高精细度的模型。一个高精细度的模型自然是需要更多的三角面以及更多的处理器资源。而当我们在稍远的距离观察这个模型的时候,我们更愿意使用一个精细度比较低的模型使得更多的计算机资源能够用于对离相机更近对象的渲染。这个技术简单来说就是用于平衡 GPU 资源,使得更多的资源能够用于距离相机很近的对象的渲染,因为这些地方的小细节是很容易被用户注意到的。
要解决这个问题,我们也可以使用 OpenGL 的一个已经存在的特性即为同一个模型生成多个不同层级精细度的模型(LOD)。例如,高精细度、一般精细度、低精细度。之后我们就可以根据与相机距离的不同来选择使用不同精细度版本的模型。但是这个方法会消耗更多的美术资源而且很多情况下这个方法也不够灵活。我们理想的方法是导入一个多边形数量很少的模型,之后将组成此模型的所有三角面细分成更小的三角面,这就是曲面细分( Tessellation )。在 OpenGL 4.x 中所提供的曲面细分管线中,这些都能够在 GPU 中进行动态的处理并且选择每个三角形的精细度。
在经过学术界和工业界多年的研究之后,曲面细分技术已经被整合到了 OpenGL 的渲染管线中。它的设计很大程度上是受到了数学中的几何表面和曲线、贝塞尔曲线以及细分的影响。我们将分两步来学习曲面细分技术,在这一课中,我们将主要聚焦于如何在渲染管线中使用新的机制来实现曲面细分技术而不会涉及太多的数学知识。这个技术本身是比较简单的,但是它需要使用很多与之相关的组件。在下一课中我们将学习贝塞尔曲线,并且探讨如何将其用于曲面细分技术之中。
让我们先看看在渲染管线中曲面细分技术是如何实现的。负责实现曲面细分技术的关键部分是两个新的着色器阶段,而且在他们之间有一个固定功能阶段,在这个阶段中可以从某种程度上来配置一些参数但并不需要运行着色器程序。第一个着色器阶段被称作细分控制着色器(TCS),固定功能阶段为图元生成(PG),而第二个着色器阶段为细分曲面计算着色器(TES)。下面这幅图片展示了管线中新增的几个阶段的顺序:
TCS 着色器需要一组被称作是 Control Points (控制点)的顶点。这些顶点并没有被真正定义成如三角形、矩形、五边形或者其他的多边形。相反的是,他们定义了一个几何曲面。这个曲面一般由一些多项式公式定义,其思想就是当我们移动其中一个控制点时,它会对整个曲面的都产生影响。如果你熟悉一些图形软件,那么你可能会知道在一些图形软件中会允许你使用一系列的控制点来定义曲面或者曲线,并且通过移动这些控制点来改变他们。这一组控制点通常被称作一个 Patch。在下面图中的黄色表面就是通过由 16 个控制点组成的 patch 来实现的:
TCS 着色器使用一个 patch 作为输入并且最终输出一个 patch 。开发者在着色器中可以对控制点进行一些变换甚至是添加或者删除控制点。除了输出一个 patch 之外,着色器程序还会计算出一组被称作 Tessellation Levels (TL) 的数据。TLs 决定了曲面细分的精细程度——对这一个 patch 需要生成多少个三角形。因为这些都是在着色器中实现的,开发者可以任意选择合适的算法来计算 TLs 。例如,我们可以定义如果光栅化之后的三角形将会覆盖的像素点的个数小于 100 则 TLs 值为 3,当覆盖的像素点的个数在 101 到 500 之间的时候 TLs 值为 7,超出 500 之后则为 12.5(我们后面会详细介绍如何通过 TL 的值来影响曲面细分)。还有的算法也可以根据对象到相机的距离来计算。更重要的一点是每个 patch 都可以根据其本身的特征来得到不同的 TLs 值。
在 TCS 着色器完成之后数据会进入固定功能阶段 PG 中,它的任务就是进行细分操作。这对于新手来说可能会比较迷惑。因为 PG 并不是真正对 TCS 着色器输出的 patch 进行细分。实际上它并没有权限去接触它。相反的是,它借助于 TLs 在一个特定的空间中进行细分操作。这个特定的空间可以是一个规范化的(在[0.0 ~ 1.0]之间)二维矩形或者一个由三维质心坐标(Barycentric coordinates)定义的一个等边三角形:
三角形的质心坐标系是一个通过组成三角形的三个顶点的加权值来定义三角形内部位置的方法。三角形的顶点有 U、V 以及 W 三个分量确定。三角形中的某一个点其位置越靠近一个顶点,则这个顶点的权值就越大相应的其他两个顶点的权值就会减小。如果这个点正好位于一个顶点上,那么对应这个顶点的权值为 1 ,另外两个顶点的权值都为 0 。例如质心坐标系的U为(1,0,0),V 为(0,1,0)、W 为(0,0,1)。三角形的中心用质心坐标系表示就是(1/3,1/3,1/3)。质心坐标系的一个很有趣的特点就是如果我们将三角形内部一点的三个分量相加,得到的结果始终都是 1 。为了简单起见,现在我们都先专注于三角形空间。
PG 借助于 TLs 中的信息并在此基础上在三角形内部生成一系列的点。每个点都是由这个三角形的质心坐标系确定的。开发者可以选择输出的拓扑结构为点或者是三角形。如果选择的是拓扑关系为点,那么 PG 会直接将其传入渲染管线的下一阶段并按照点来进行光栅化。如果选择的是三角形,PG 会将所有顶点连起来这样整个三角面就被细分成了多个小的三角面。
在一般情况下 TLs 告诉 PG 三角形边上的线段的数量,并且一环一环的绕着三角形的中心来构造三角形。
那么上面图片中的这些小三角形与我们之前提到过的 patch 有什么关系呢?其实,这就主要取决于你想使用曲面细分技术做什么了。一个非常简单的应用(本课中我们使用的)就是跳过曲面的多项式表示,简单来说就是你模型中的三角形面仅仅是简单的映射到 patch 上。在这种情况下组成三角形的 3 个顶点表示我们的 3 个控制点,而原始的三角形同时作为 TCS 的输入和输出 patch。我们使用 PG 来对三角形进行曲面细分并且生成由质心坐标表示的 domain 三角形并对这些坐标进行线性组合(例如:将他们与原始三角形的属性相乘)来对原始模型的三角形面进行细分。在下一节中我们我们将会介绍 patch 在几何曲面上的实际应用。无论如何要记住,PG 并不在意 TCS 的输入和输出 patch,它需要在意的是每个 patch 的 TLS 值。
在 PG 对三角形域进行曲面细分之后,我们还需要对其细分的结果进行一些处理。毕竟,PG 无法访问 patch。它唯一的输出就是质心坐标和他们的连通性。将他们传递到 TES 着色器中之后,TES 有权限去访问 TCS 中输出的 patch 和 PG 生成的质心坐标。PG 对每一个质心坐标都会执行 TES,而 TES 的功能就是为在 PG 中生成的每一个位于质心坐标系下的顶点都生成一个真正的可用的顶点。因为可以访问 patch,他可以从中获取诸如位置、法线等信息,并且通过这些来生成顶点。在 PG 对一个“小”三角形的三个质心坐标系下的顶点执行 TES 之后,由 TES 生成的这三个顶点会被传递到渲染管线的下一阶段传递并把他们当做一个完整的三角形进行光栅化。
TES 与顶点着色器在某种程度上十分相似,它只有一个输入(质心坐标)和一个输出(顶点)。TES 在每次调用过程中只能生成一个顶点,而且它不能丢弃顶点。OpenGL 中曲面细分阶段的 TES 着色器的主要目的就是借助于 PG 阶段生成的坐标来生成曲面。简单来说就是将质心坐标变换到表示曲面的多项式中并计算出最终结果。结果就是新的顶点的位置,之后这些顶点就能与普通顶点一样进行变换和投影了。正如你所看到的那样,在处理几何曲面的时候,如果我们选择的 TLs 值越高,我们获得的区域位置就越多,而且通过在 TES 中对他们进行计算我们得到的顶点就会更多,这样我们就能更好的表示精细的表面。在这一节中表面的计算公式我们简单的使用一个线性组合公式来代替。
在 TES 着色器执行之后,产生的新的顶点会被作为三角形传递到渲染管线的下一阶段。在 TES 之后接下来不管是 GS 还是光栅化阶段,这些都与我们之前的一样了。
现在让我们总结一下整个阶段:
对于 patch 中的每一个顶点都会执行顶点着色器,每个 patch 中都包含顶点缓存中的多个控制点(CPs)(控制点的最大值由驱动和 GPU 定义);
TCS 着色器获取由顶点处理器处理之后的数据并输出 patch,除此之外它也会产生 TLs;
基于我们定义的细分空间,PG 阶段通过获取从 TCS 着色器中传入的 TLS(细分层级)以及其输出的拓扑结构,PG 会生成这个空间下的顶点信息和其生成的顶点的连通性;
所有生成的位于这个特定空间下的位置都会经过 TES 着色器进行处理;
在第 3 步中生成的图元会沿着渲染管线继续传递,TES 着色器会生成这些图元的具体数据;
和往常一样执行 GS 阶段和光栅化阶段。
(tutorial30.cpp:80)
GLint MaxPatchVertices = 0;
glGetIntegerv(GL_MAX_PATCH_VERTICES, &MaxPatchVertices);
printf("Max supported patch vertices %d\n", MaxPatchVertices);
glPatchParameteri(GL_PATCH_VERTICES, 3);
当我们启用曲面细分时(例如我们使用了 TCS 和 TES 着色器)渲染管线需要知道一个 patch 输入由多少个顶点组成。需要记住一个 patch 不一定有一个确定的几何形式。它仅仅是一系列的控制点。上面程序中我们调用 glPatchParameteri() 是为了告诉渲染管线每个 patch 输入由三个顶点组成。这个参数的最大值由显卡驱动定义的值 GL_MAX_PATCH_VERTICES 确定,这个值对于不同的显卡驱动是不一样的,我们我们通过调用 glGetIntegerv() 函数来获取这个值并打印出来。
(lighting.vs)
#version 410 core
layout (location = 0) in vec3 Position_VS_
layout (location = 1) in vec2 TexCoord_VS_
layout (location = 2) in vec3 Normal_VS_
uniform mat4 gW
out vec3 WorldPos_CS_
out vec2 TexCoord_CS_
out vec3 Normal_CS_
void main()
WorldPos_CS_in = (gWorld * vec4(Position_VS_in, 1.0)).
TexCoord_CS_in = TexCoord_VS_
Normal_CS_in = (gWorld * vec4(Normal_VS_in, 0.0)).
这是我们的顶点着色器,这个顶点着色器与我们之前使用的顶点着色器不同的地方在于我们不再将顶点从局部坐标系变换到裁剪坐标系中(通过乘上 world-view-projection 矩阵)。原因很简单,因为这里面本来就没有任何顶点。我们希望能生成一大堆的新的顶点这样才会需要这个矩阵。因此这个变换会在我们 TES 着色器执行之后才会进行。
(lighting.cs)
#version 410 core
// define the number of CPs in the output patch
layout (vertices = 3)
uniform vec3 gEyeWorldP
// attributes of the input CPs
in vec3 WorldPos_CS_in[];
in vec2 TexCoord_CS_in[];
in vec3 Normal_CS_in[];
// attributes of the output CPs
out vec3 WorldPos_ES_in[];
out vec2 TexCoord_ES_in[];
out vec3 Normal_ES_in[];
这是 TCS 着色器程序的开始部分,对于输出 patch 中的每一个控制点都会执行一次这个着色器,首先我们定义了输出的每个 patch 中的控制点的数量。之后我们定义一个一致变量用于计算 TLs 。在那之后我们还定义了一些输入和输出的控制点属性。在这一课中我们输出和输出的 patch 结构都是一样的,但是一般情况下并不是这样的。每一个输入和输出的控制点都有位置、纹理坐标以及法线属性。因为在一个输入和输出 patch 中我们可以有多个控制点,所以每个属性都是用一个数组来定义的,这使得我们可以很方便的索引到任何一个控制点。
(lighting.cs:33)
void main()
// Set the control points of the output patch
TexCoord_ES_in[gl_InvocationID] = TexCoord_CS_in[gl_InvocationID];
Normal_ES_in[gl_InvocationID] = Normal_CS_in[gl_InvocationID];
WorldPos_ES_in[gl_InvocationID] = WorldPos_CS_in[gl_InvocationID];
在 TCS 着色器的主函数中,我们首先将输入的控制点属性复制到输出的控制点中。这个函数在每个控制点输出时都会调用一次,而内置变量 gl_InvocationID 则包含了当前控制点的索引值。这里不能确定顶点的执行顺序的原因是因为 GPU 的并行运算,所以这里我们使用 gl_InvocationID 作为输出和输入 patch 的索引(OpenGL 会为我们处理)。
(lighting.cs:40)
// Calculate the distance from the camera to the three control points
float EyeToVertexDistance0 = distance(gEyeWorldPos, WorldPos_ES_in[0]);
float EyeToVertexDistance1 = distance(gEyeWorldPos, WorldPos_ES_in[1]);
float EyeToVertexDistance2 = distance(gEyeWorldPos, WorldPos_ES_in[2]);
// Calculate the tessellation levels
gl_TessLevelOuter[0] = GetTessLevel(EyeToVertexDistance1, EyeToVertexDistance2);
gl_TessLevelOuter[1] = GetTessLevel(EyeToVertexDistance2, EyeToVertexDistance0);
gl_TessLevelOuter[2] = GetTessLevel(EyeToVertexDistance0, EyeToVertexDistance1);
gl_TessLevelInner[0] = gl_TessLevelOuter[2];
在产生输出 patch 之后,我们计算 TLs 的值,对于每个输出 patch,我们可以有不同的 TLs 值。OpenGL 为 TLs 提供了两个内置浮点数组: gl_TessLevelOuter (size 4) 、 gl_TessLevelInner (size 2)。在三角域里面我们只需要使用 gl_TessLevelOuter 数组的前三个成员以及 gl_TessLevelInner 数组的第一个成员(除了三角域之外还有矩形域和 isoline 域,不同的域对数组有不同的访问)。gl_TessLevelOuter 用于确定三角形每个边线上的线段的数量,gl_TessLevelInner[0] 确定三角形中将会包含多少个圈。如果我们用 U、V、W 来指定三角形的顶点,那么每个顶点对应的边线就是此顶点正对着的边:
我们用来计算 TLs 的算法非常简单,它是基于世界标系中顶点到相机的距离来计算的。它在 GetTessLevel 函数中实现(下面会介绍)。我们先计算相机与每个顶点的距离并且三次调用 GetTessLevel()函数来更新 gl_TessLevelOuter[] 数组中的每一个元素。每一次调用都与上图中的边线所对应( 0 号边线的 TL 值被存放在 gl_TessLevelOuter[0] 中),并且这个边线的 TL 值是基于组成这个边线的两个顶点到相机的距离计算出来的。内部的 TL 值我们选择使用与 W 边线相同的 TL 值。
你可以使用任何你想使用的算法来计算 TL 值,例如我们可以通过估计三角形最终显示到屏幕上的像素大小,并且设置 TL 值使得所有细分之后的三角形都不小于一个给定的像素值。
(lighting.cs:18)
float GetTessLevel(float Distance0, float Distance1)
float AvgDistance = (Distance0 + Distance1) / 2.0;
if (AvgDistance &= 2.0) {
return 10.0;
else if (AvgDistance &= 5.0) {
return 7.0;
return 3.0;
这个函数用于计算一条边的 TL 值,它是基于组成这条边的两个顶点到相机的距离来计算的。我们使用两个点到相机的平均距离来将 TL 值确定为 10、7 或者是 3。随着距离的增加我们希望得到一个更小的 TL 值这样不至于浪费 GPU 的资源。
(lighting.es)
#version 410 core
layout(triangles, equal_spacing, ccw)
在 TES 着色器开始,我们使用 ‘layout’ 关键字配置了三个属性:
Triangles 这是 PG 的工作域,其他两个选项是 quads 和 isolines;
equal_spacing 意味着三角形的边缘会被细分成等分的线段(根据 TLs)。你也可以使用 fractional_even_spacing 或者 fractional_odd_spacing 使得在线段长度上获得更加平滑的过渡,不论 TL 值是一个奇数整数还是一个偶数整数。例如,如果你使用 fractional_odd_spacing 并且 TL 值为 5.1,这意味着会有两个非常短的线段和 5 个比较长的线段。随着 TL 值增加到 7,所有线段的长度会变得十分接近。当 TL 值达到7,两个新的十分短的线段会被创建出来。fractional_even_spacing在处理偶数TL值时也是一样;
ccw 意味着 PG 中输出的三角形会按照逆时针顺序(你同样可以使用 cw 将其设定为顺时针顺序)。你也许会想,在顺时针为正面的情况下为什么我们还要将其设置为逆时针方向。这是因为在这一节中我使用的模型( quad2.obj )是用 Blender 按照逆时针顺序生成的,我本应该在导入模型的时候将 Assimp 标志设置为 'aiProcess_FlipWindingOrder' 这样在这里我们就能使用 cw 。我现在只是不想去修改 mesh.cpp 中的内容,底线就是无论你做什么,都要保证他们始终是一致的。
注意你也可以为上面每一个配置项目都单独使用一个 layout 关键字来声明,上面的写法仅仅是为了节省空间。
(lighting.es:5)
uniform mat4 gVP;
uniform sampler2D gDisplacementM
uniform float gDispF
in vec3 WorldPos_ES_in[];
in vec2 TexCoord_ES_in[];
in vec3 Normal_ES_in[];
out vec3 WorldPos_FS_
out vec2 TexCoord_FS_
out vec3 Normal_FS_
TES 着色器与其他着色器程序一样都有一致变量。偏移纹理实际上就是高度纹理,它记录了颜色纹理中每个像素在所在位置的高度信息。我们将会使用高度纹理在我们的网格模型的表面上产生起伏的状态。除此之外,TES 也可以访问 TCS 着色器的输出 patch。最后我们声明输出的顶点的属性。需要注意的是默认数组并没有在这里出现,这是因为 TES 的输出通常都是一个单一的顶点。
(lighting.es:27)
void main()
// Interpolate the attributes of the output vertex using the barycentric coordinates
TexCoord_FS_in = interpolate2D(TexCoord_ES_in[0], TexCoord_ES_in[1], TexCoord_ES_in[2]);
Normal_FS_in = interpolate3D(Normal_ES_in[0], Normal_ES_in[1], Normal_ES_in[2]);
Normal_FS_in = normalize(Normal_FS_in);
WorldPos_FS_in = interpolate3D(WorldPos_ES_in[0], WorldPos_ES_in[1], WorldPos_ES_in[2]);
这是 TES 着色器的主函数,现在概要整理下到目前为止我们已经完成了哪些工作了。网格顶点经过顶点着色器处理之后得到了世界坐标系下的位置、法线。TCS 将每个三角形作为一个带三个控制点的 patch 并直接将其传递到 TES 中。PG 将一个等边三角形细分成多个小三角形并且为每个新产生的顶点都执行 TES 着色器。在每次的 TES 调用中,我们都可以访问顶点的质心坐标(或者叫做 Tessellation Coordinates)。因为三角形内部一点的质心坐标代表了三个顶点的权值的线性组合。我们可以用它来插值得到新产生的顶点的所有属性。函数 interpolate2D() 和 interpolate3D() 就是用于完成这个工作的。他们从 patch 的控制点中获取属性并且使用 gl_TessCoord 进行插值。
(lighting.es:35)
// Displace the vertex along the normal
float Displacement = texture(gDisplacementMap, TexCoord_FS_in.xy).x;
WorldPos_FS_in += Normal_FS_in * Displacement * gDispF
gl_Position = gVP * vec4(WorldPos_FS_in, 1.0);
在将原始网格中的三角形细分成多个小三角形之后,这并不会对整个模型的外观产生很大的影响,因为这些小三角形都是位于原来的大三角形的平面里。我们希望能够通过某种方法对顶点进行一些偏移(或者替换)使得其能够与颜色纹理相对应。例如,如果纹理包含的内容是砖块或者岩石,我们希望我们的顶点沿着砖块或者岩石的边缘凸起。为了实现这个效果我们需要使用一个高度纹理( displacement map )作为颜色纹理的补充。现在有很多工具和编辑器都可以生成高度纹理,这里我们就不具体介绍其细节了。你可以在网上找到更多有关的信息。为了使用高度纹理,我们只需要使用当前纹理坐标从其中进行采样,这样就能获得顶点的高度信息。之后我们使用顶点法线与高度以及一个由应用程序控制的一致变量(位移因子)的乘积来替换原来的顶点。这样一来所有顶点都会根据其高度沿着其法线方向进行移动,并且移动的距离由从高度纹理中读取的高度信息决定。最后我们将新产生的位于世界坐标系中的顶点乘上 view-projection 矩阵并将其传入 ‘gl_Position’ 变量中。
(lighting.es:17)
vec2 interpolate2D(vec2 v0, vec2 v1, vec2 v2)
return vec2(gl_TessCoord.x) * v0 + vec2(gl_TessCoord.y) * v1 + vec2(gl_TessCoord.z) * v2;
vec3 interpolate3D(vec3 v0, vec3 v1, vec3 v2)
return vec3(gl_TessCoord.x) * v0 + vec3(gl_TessCoord.y) * v1 + vec3(gl_TessCoord.z) * v2;
这两个函数通过使用 'gl_TessCoord' 作为权值在二维向量和三维向量之间插值
(lighting_technique.cpp:277)
bool LightingTechnique::Init()
if (!AddShader(GL_TESS_CONTROL_SHADER, pTessCS)) {
if (!AddShader(GL_TESS_EVALUATION_SHADER, pTessES)) {
最后不要忘了编译这两个新的着色器程序。
关于这个 Demo
本节中的演示示例展示了如何对矩形地形进行曲面细分并且使顶点沿着颜色纹理中的岩石边缘进行偏移。你可以使用键盘中的 ‘+’ 和 ‘-’ 符号来更新位移因素进而控制位移的大小。你也可以通过 ‘Z’ 按键将场景切换到线框模式进而观察由曲面细分技术生成的三角形。你可以尝试在线框模式下远离或者靠近地形进而观察曲面细分的层级是如何基于顶点到相机的距离改变的。这就是我们需要 TCS 的原因。匿名用户不能发表回复!|
每天回帖即可获得10分可用分!小技巧:
你还可以输入10000个字符
(Ctrl+Enter)
请遵守CSDN,不得违反国家法律法规。
转载文章请注明出自“CSDN(www.csdn.net)”。如是商业用途请联系原作者。OpenGL(18)
编程语言(57)
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:180263次
积分:3865
积分:3865
排名:第7306名
原创:195篇
转载:74篇
评论:151条
(4)(15)(13)(22)(9)(21)(31)(19)(29)(31)(5)(14)(24)(21)(13)(1)(2)现代OpenGL教程 02——贴图
招聘信息:
导读:在本文中,我们将给三角形加一个贴图,这需要在顶点和片段着色器中加入一些新变量,创建和使用贴图对象,并且学习一点贴图单元和贴图坐标的知识。本文会使用两个新的类到tdogl命名空间中:tdogl:Bitmap和tdogl:Texture。这些类允许我们将jpg,png或bmp图片上传到显存并用于着色器。tdogl:Program类也增加一些相关接口。获取代码所有例子代码的zip打包可以从这里获取:。这一系列文章中所使用的代码都存放在:。你可以在页面中下载zip,加入你会git的话,也可以复制该仓库。本文代码你可以在source/02_textures目录里找到。使用OS X系统的,可以打开根目录里的opengl-series.xcodeproj,选择本文工程。使用Windows系统的,可以在Visual Studio 2013里打开opengl-series.sln,选择相应工程。工程里已包含所有依赖,所以你不需要再安装或者配置额外的东西。如果有任何编译或运行上的问题,请联系我。着色器变量Uniform与Attribute中的着色器变量都是attribute,本文介绍另外一种类型的变量:uniform变量。着色器变量有两种类型:uniform和attribute。attribute变量可以在每个顶点上有不同值。而uniform变量在多个顶点上保持相同值。比如,你想要给一个三角形设置一种颜色,那你应该使用uniform变量,如果你希望每个三角形顶点有不同颜色,你应该使用attribute变量。从这开始,我称呼他们为“uniforms”和“attributes”。Uniforms能被任意着色器访问,但是Attributes必须先进入顶点着色器,而非片段着色器。顶点着色器在需要时会将该值传给片段着色器。这因为Uniforms像常量-它们不会被任何着色器更改。然而,Attributes不是常量。顶点着色器会改变Attribute变量的值,在片段着色器获取之前。就是说,顶点着色器的输出就是片段着色器的输入。为了设置Uniform的值,我们可以调用系列函数。而设置Attribute的值,我们需要在VBO中保存,并且和VAO一起发送给着色器,就像前一篇教程里的。加入你不想把值存在VBO里,你也可以使用系列函数来设置Attribute值。贴图贴图,大体上来说就是你应用在3D物体上的2D图像。它有其它用途,但显示2D图像在3D几何上是最常用的。有1D,2D,3D贴图,但本文只讲2D贴图。更深入阅读,请参见《》书中的章节。贴图是存放在显存里的。那就是说,你需要在使用之前上传你的贴图数据给显卡。这类似VBO在前文的作用-VBO也是在使用之前需要存放到显存上。贴图的高和宽需要是2的幂次方。比如16,32,64,128,256,512。本文中使用的是256*256的图像作为贴图,如下图所示。我们使用tdogl:Bitmap来加载“hazard.png”的原始像素数据到内存中,参见帮助文档。然后我们使用tdogl:Texture上传原始像素数据给OpenGL贴图对象。幸运的是OpenGL中的贴图创建方法从面世到现在都没有实质性的变化,所以网上有大量的创建贴图的好文章。虽然贴图坐标的传输方式有变化,但创建贴图还是跟以前一样。以下是tdogl:Texture的构造函数,用于OpenGL贴图创建。Texture::Texture(const&Bitmap&&bitmap,&GLint&minMagFiler,&GLint&wrapMode)&:
&&&&_originalWidth((GLfloat)bitmap.width()),
&&&&_originalHeight((GLfloat)bitmap.height())
&&&&glGenTextures(1,&&_object);
&&&&glBindTexture(GL_TEXTURE_2D,&_object);
&&&&glTexParameteri(GL_TEXTURE_2D,&GL_TEXTURE_MIN_FILTER,&minMagFiler);
&&&&glTexParameteri(GL_TEXTURE_2D,&GL_TEXTURE_MAG_FILTER,&minMagFiler);
&&&&glTexParameteri(GL_TEXTURE_2D,&GL_TEXTURE_WRAP_S,&wrapMode);
&&&&glTexParameteri(GL_TEXTURE_2D,&GL_TEXTURE_WRAP_T,&wrapMode);
&&&&glTexImage2D(GL_TEXTURE_2D,
&&&&&&&&&&&&&&&&&0,&
&&&&&&&&&&&&&&&&&TextureFormatForBitmapFormat(bitmap.format()),
&&&&&&&&&&&&&&&&&(GLsizei)bitmap.width(),&
&&&&&&&&&&&&&&&&&(GLsizei)bitmap.height(),
&&&&&&&&&&&&&&&&&0,&
&&&&&&&&&&&&&&&&&TextureFormatForBitmapFormat(bitmap.format()),&
&&&&&&&&&&&&&&&&&GL_UNSIGNED_BYTE,&
&&&&&&&&&&&&&&&&&bitmap.pixelBuffer());
&&&&glBindTexture(GL_TEXTURE_2D,&0);
}贴图坐标毫无疑问,贴图坐标就是贴图上的坐标。关于贴图坐标比较奇特的是它们不是以像素为单位。它们范围是从0到1,(0, 0)是左下角,(1, 1)是右上角。假如你上传到OpenGL的图像是颠倒的,那(0, 0)就是左上角,而非左下角。将像素坐标转换为贴图坐标,你必须除上贴图的宽和高。比如,在256*256的图像中,像素坐标(128, 256)的贴图坐标是(0.5, 1)。贴图坐标通常被称为UV坐标。你也可以叫它们是XY坐标,但是XYZ通常被用来表示顶点,我们不希望将这两者混淆。贴图图像单元贴图图像单元,亦或简称“贴图单元”,是在OpenGL中略怪异的一部分。你无法直接发送贴图给着色器。首先,你要绑定贴图到贴图单元,然后呢要发送贴图单元的索引给着色器对于贴图单元是有数量限制的。在低端硬件上,如手机,它们只有两个贴图单元。既然如此,即使我们有许多的贴图,我们也只能同时使用两个贴图单元在着色器中。我们在本文中只用到了一个贴图,所以也只需要一个贴图单元,但它可以在多个不同的着色器中混合。实现贴图首先,让我们创建一个新的全局贴图。tdogl::Texture*&gTexture&=&NULL;我们为加载“hazard.png”图片新增一个函数。该函数能被AppMain所调用。static&void&LoadTexture()&{
&&&&tdogl::Bitmap&bmp&=&tdogl::Bitmap::bitmapFromFile(ResourcePath("hazard.png"));
&&&&bmp.flipVertically();
&&&&gTexture&=&new&tdogl::Texture(bmp);
}下一步,我们给每个三角形的顶点一个贴图坐标。假如你跟上图比较过UV坐标,就可以看出按顺序这个坐标表示(中,上),(左,下)和(右,下)。GLfloat&vertexData[]&=&{
&&&&//&X,Y,Z,U,V
&&&&0.0f,&0.8f,&0.0f,0.5f,1.0f,
&&&&-0.8f,-0.8f,0.0f,0.0f,0.0f,
&&&&0.8f,-0.8f,&0.0f,1.0f,0.0f,
};现在我们需要修改片段着色器,使得它能使用贴图和贴图坐标作为输入。下面是新的片段着色器代码:#version&150
uniform&sampler2D&&//this&is&the&texture
in&vec2&fragTexC&//this&is&the&texture&coord
out&vec4&finalC&//this&is&the&output&color&of&the&pixel
void&main()&{
&&&&finalColor&=&texture(tex,&fragTexCoord);
}uniform关键字说明tex是uniform变量。贴图是一致的,因为所有三角形顶点有相同的贴图。sampler2D是变量类型,说明它包含一个2D贴图。fragTexCoord是attribute变量,因为每个三角形顶点是不同的贴图坐标。texture函数是用来查找给定贴图坐标的像素颜色。在GLSL旧版本中,你应该使用texture2D函数来实现该功能。我们无法直接传送attribute给判断着色器,因为attribute必须首先通过顶点着色器。这儿是修改过的顶点着色器:#version&150
in&vec2&vertTexC
out&vec2&fragTexC
void&main()&{
&&&&//&Pass&the&tex&coord&straight&through&to&the&fragment&shader
&&&&fragTexCoord&=&vertTexC
&&&&gl_Position&=&vec4(vert,&1);
}顶点着色器使用vertTexCoord作为输入,并且将它不经修改,直接传给名为fragTexCoord的attribute片段着色器变量。着色器有两个变量需要我们设置:vertTexCoordattribute变量和texuniform变量。让我们从设置tex变量开始。打开main.cpp,找到Render()函数。我们在绘制三角形之前设置texuniform变量:glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D,&gTexture->object());
gProgram->setUniform("tex",&0);&//set&to&0&because&the&texture&is&bound&to&GL_TEXTURE0贴图在没有绑定到贴图单元时,是无法使用的。glActiveTexture告诉OpenGL我们希望使用哪个贴图单元。GL_TEXTURE0是第一个贴图单元,我们就使用它。下一本,我们使用glBindTexture来绑定我们的贴图到激活的贴图单元。然后我们设置贴图单元索引给texuniform着色器变量。我们使用0号贴图单元,所以我们设置tex变量为整数0。setUniform方法只是调用了glUnifrom1i函数。最后一步,获取贴图坐标给vertTexCoordattribute变量。为了实现它,我们需要修改LoadTriangle()函数中的VAO。之前的代码是这样的://&Put&the&three&triangle&vertices&into&the&VBO
GLfloat&vertexData[]&=&{
&&&&//&&X&&&&&Y&&&&&Z
&&&&&0.0f,&0.8f,&0.0f,
&&&&-0.8f,-0.8f,&0.0f,
&&&&&0.8f,-0.8f,&0.0f
//&connect&the&xyz&to&the&"vert"&attribute&of&the&vertex&shader
glEnableVertexAttribArray(gProgram->attrib("vert"));
glVertexAttribPointer(gProgram->attrib("vert"),&3,&GL_FLOAT,&GL_FALSE,&0,&NULL);现在我们需要改成这样://&Put&the&three&triangle&vertices&(XYZ)&and&texture&coordinates&(UV)&into&the&VBO
GLfloat&vertexData[]&=&{
&&&&//&&X&&&&&Y&&&&&Z&&&&&&&U&&&&&V
&&&&&0.0f,&0.8f,&0.0f,&&&0.5f,&1.0f,
&&&&-0.8f,-0.8f,&0.0f,&&&0.0f,&0.0f,
&&&&&0.8f,-0.8f,&0.0f,&&&1.0f,&0.0f,
//&connect&the&xyz&to&the&"vert"&attribute&of&the&vertex&shader
glEnableVertexAttribArray(gProgram->attrib("vert"));
glVertexAttribPointer(gProgram->attrib("vert"),&3,&GL_FLOAT,&GL_FALSE,&5*sizeof(GLfloat),&NULL);
//&connect&the&uv&coords&to&the&"vertTexCoord"&attribute&of&the&vertex&shader
glEnableVertexAttribArray(gProgram->attrib("vertTexCoord"));
glVertexAttribPointer(gProgram->attrib("vertTexCoord"),&2,&GL_FLOAT,&GL_TRUE,&&5*sizeof(GLfloat),&(const&GLvoid*)(3&*&sizeof(GLfloat)));我们第二次调用了glVertexAttribPointer,但我们也修改了第一个调用。最重要的是最后两个参数。两个glVertexAttribPointer调用的倒数第二个参数都是5*sizeof(GLfloat)。这是“步长”参数。该参数是表明每个值开始位置的间隔是多少字节,或者说是到下个值开始的字节数。在两个调用中,每个值是5个GLFloat长度。举个例子,加入我们从“X”开始,往前数5个值,我们会落在下个“X”值上。从“U”开始也一样,也是往前数5个。该参数是字节单位,不是浮点作为单位,所以我们必须乘上浮点类型所占字节数。最后一个参数glVertexAttribPointer是一个“偏移”参数。该参数需要知道从开始到第一个值有多少字节。开始是XYZ,所以偏移设置为NULL表示“到开始的距离为0字节”。第一个UV不在最前面-中间有3个浮点的距离。再说一遍,参数是以字节为单位,而非浮点,所以我们必须乘上浮点类型所占字节数。并且我们必须将数值转为const GLvoid*类型,因为在旧版本的OpenGL中该参数有别于现在的“偏移”。现在,当你运行程序,你就能看到如本文最上方的那个三角形。下篇预告下一篇教程中我们会学一些矩阵相关的东西,使用矩阵来旋转立方体,移动相机,和添加透视投影。我们还会学习深度缓冲和基于时间更新的逻辑,比如动画。更多OpenGL贴图相关资源 by Etay Meiri by Jakob Progsch
微信扫一扫
订阅每日移动开发及APP推广热点资讯公众号:CocoaChina
您还没有登录!请或
点击量5102点击量4688点击量4531点击量3771点击量3695点击量3215点击量3212点击量2972点击量2962
&2016 Chukong Technologies,Inc.
京公网安备89

我要回帖

更多关于 catia怎么绘制曲面 的文章

 

随机推荐