OpenGL中的曲面细分和几何着色器

[摘要]本文我们先介绍OpenGL中的曲面细分的一些基本概念,然后给两个例子说明不得不用这项技术的理由。
曲面细分是OpenGL 4.0之后才定义的功能,使用之前请确认你的显卡驱动支持OpenGL4.0以上。

1. 曲面细分的目的

OpenGL用透视变换将三维几何模型映射到二维平面上,这样随着透视参数(比如模拟相机位置,方向,角度,焦距等等)的不同,以及几何模型在场景中的位置和大小不同,几何模型在二维平面显示的大小会不同。在传统的OpenGL应用中,一个几何模型建立后,GPU是不能增减模型中的顶点的。这样一个几何模型放大显示时,会出现棱角,细节部分就会变得不光滑。如果建立模型时使用很多顶点,在缩小显示时又会造成极大的浪费(一个三角形可能一个像素都不到)。
因此,让OpenGL能够根据显示场景的不同,调整模型中的细节,这是曲面细分的初衷。事实上,曲面细分还可以进行非线性几何形状的建模,比如由GPU完成三次样条曲面的建模,由用户给出形状的控制点,通过建模参数和建模程序,可以将特定的形状构建出来,这样可以大大减少CPU的计算量和CPU到GPU数据传输的带宽要求,提高整体性能。

2.曲面细分的应用流程

曲面细分由绘制GL_PATCHES类型的图元开始,绘制命令跟其他的OpenGL绘制命令是一样的,一个GL_PATCHES类型的图元包含的顶点数目由OpenGL API:

 void glPatchParameteri (GLenum pname, GLint value);

比如:

glPatchParameteri(GL_PATCH_VERTICES,4);

指定一个图元由4个顶点组成。 这样每绘制一个GL_PATCHES必须提供4个顶点。
顶点送到OpenGL流水线之后,照例先过VS,进行顶点着色器计算,此时可以进行常规的顶点几何变换以及其他处理。然后送到一个称为曲面细分控制参数着色器(TCS)的程序中运行,生成所谓的曲面细分控制参数。细分控制参数由两组,分别是外侧参数和内侧参数,可以粗略理解为细分网格的内外侧分割数,数字越大,分得越细,具体的解释与细分方式相关。
下面是一个简单的TCS例子:

#version 400
layout (vertices=4)out;
void main() {
   
  gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
  gl_TessLevelInner[0] = 5.0f;
  gl_TessLevelInner[1] = 6.0f;
  gl_TessLevelOuter[0] = 1.0f;
  gl_TessLevelOuter[1] = 2.0f;
  gl_TessLevelOuter[2] = 3.0f;
  gl_TessLevelOuter[3] = 4.0f;
}

其中gl_TessLevelInner是内侧细分参数,gl_TessLevelOuter是外侧细分参数。
TCS可以根据图元的顶点,来生成细分控制参数,基本的思路是如果发现图元在显示表面上占的像素多,就分得更细一些,反之就分的粗一些。如果没有指定TCS,则可以通过glPatchParameterfv来设置细分控制参数(通过设置GL_PATCH_DEFAULT_OUTER_LEVEL或者GL_PATCH_DEFAULT_INNER_LEVEL参数即可),此时就不能根据PATCHES中顶点的情况来调整细分控制参数了。
再往后,顶点数据送到一个名为曲面细分顶点计算着色器(TES)的程序中,同时送到这个着色器的还有细分后的网格相对位置,这个相对位置跟细分方式相关。细分方式则由TES指定。TES生成网格点对应的顶点的位置以及其他顶点参数(比如颜色,纹理坐标等等),当然这些生成应该有某种非线性的算法,如果都是线性的算法,干脆让OpenGL内部做线性插值算了,何必又炫技,玩高深呢。
TES生成的顶点接着往后送,如果有GS,则送到GS,否则就组装成图元并进行光栅化,FS等一系列操作,最终形成像素。
在这里插入图片描述
要在VS,TCS, TES, GS, FS各个Shader之间传送除了Position之外的数据时,应该在上游定义输出块结构并赋值,下游定义同样的输入块结构。比如TES要送数据color到GS中然后同样的数据送到FS中,则定义如下:
TES:

out TES_OUT {
   
  vec3 color;
}te_out;
void main() {
   

        。。。

        te_out.color = vec3(。。。);

}

GS:

#version 400
layout (triangles)in;
layout(line_strip,max_vertices = 16)out;
uniform mat4 MV;
uniform mat4 P;

in TES_OUT {
   
  vec3 color;
}gs_in[];

out GS_OUT {
   
  vec3 color;
}gs_out;

void main() {
   
   int i;
   for (i = 0;i<gl_in.length();i++) {
   
 	  gl_Position= gl_in[i].gl_Position;
	  gs_out.color= gs_in[i].color;
	  EmitVertex();
    }
	EndPrimitive();
}

FS:

#version 330 core
in GS_OUT {
   
  vec3 color;
}fs_in;

void main() {
   
    gl_FragColor = vec4(fs_in.color, 1.0);
}

注意上下游的结构名字必须一致,内部的成员顺序,名称和类型必须完全一样,否则着色器程序连接时会失败。
值得注意的是,有些OpenGL实现要求各个着色器的版本必须一致,不能混着用。

3.细分网格生成

细分网格是由OpenGL中的一个内部模块根据TCS输出的细分控制参数和TES指定的细分模式生成的,它生成一个单位图元内部的分割网格,然后细分模式指定的图元模式往后面的流水线送顶点,其中顶点的属性是由TES根据网格的相对坐标以及外部输入的顶点数据来生成的。
细分模式有三种,矩形细分模式,三角形细分模式和等值线细分模式,矩形细分模式和三角形细分模式输出的细分图元是GL_TRIANGLES,等值线细分模式输出的图元是GL_LINES。

3.1矩形细分模式

矩形细分模式由TES的layout指定,比如:

#version 400
layout (quads, equal_spacing, ccw)in;

void main() {
   
  vec4 p = vec4(0.0);
  float u = gl_TessCoord.x;
  float v = gl_TessCoord.y;
  p =     v * (u * gl_in[0].gl_Position + (1-u) * gl_in[1].gl_Position) +
       (1-v) * (u * gl_in[2].gl_Position + (1-u) * gl_in[3].gl_Position);
  gl_Position = p;
}

就是指定矩形方式细分,并完全按照相对位置按线性插值方式生成网格顶点。其中gl_TessCoord.xy输入网格点的相对位置,gl_in输入GL_PATCHES对应的顶点位置,TES要做得就是为相对位置的地方生成对应的顶点位置和其他参数。这样输出的网格是这样的:
在这里插入图片描述

具体的办法是,把一个单位正方形(0,1;0,1)内部按Inner参数分为矩形网格(分的方法由TES的layout in参数指定),外侧四边按照outter参数分段,然后将网格分成三角形,按照TES要求的顺序给出每个网格点的相对位置(u,v), 该相对位置通过gl_TessCoord.xy传入到TES中,由TES根据原始输入的顶点生成对应的顶点,比如这里使用的是线性插值。如果输入4x4个控制点,可以实现贝塞尔曲面的插值,可见输入的顶点不见得代表形状占有的位置,可以将它们看成是另外一组控制参数。后面的示例中有更加复杂的例子。

3.2三角形细分模式

类似于矩形细分,但是细分后的每个顶点相对位置由三个坐标分量组成,这三个分量代表三角形三个顶点的权重,通过gl_TessCoord.xyz输入到TES,细分模块保证这三个分量的和为1,并且都在[0,1]之中。

TCS的控制参数中,Outer的三个分量用于分割三角形三条边,Inner的第一个分量用于控制三角形内部的分割层数。三角形从三条边开始到重心,被分为Inner指定的层,每一层的内部边比外部边的分割数减少2,直到最里层只有一个三角形或者是一个顶点为止。

比如下面的TCS:

#version 400 core
layout(vertices=3)out
void main(void)
{
   
  gl_TessLevelInner[0] = 5.0f;
  gl_TessLevelOuter[0] = 6.0f;
  gl_TessLevelOuter[1] = 7.0f;
  gl_TessLevelOuter[2] = 8.0f;
}

得到的细分结果如下:
在这里插入图片描述
其中Outer分别是三条边上的分段数,Inner[0]可以理解为从三边到重心所经过的三角形个数。
下面是Inner[0]取6的细分结果,此时重心处是一个点(inner[0]为5时是一个三角形):
在这里插入图片描述
假如一个PATCHES确实是一个三角形构成,那么这个三角形细分后的坐标可以简单地这么计算(TES):

#version 400
layout (triangles) in;

void main(void)
{
   
  gl_Position = 
    (glTessCoord.x* gl_in[0].gl_Position) +
    (glTessCoord.y *gl_in[1].gl_Position) +
    (glTessCoord.z *gl_in[2].gl_Position);
}

当然,你可以在TES中根据需要解释glTessCord,事实上它只是一个三角形内部的相对三个顶点的权重坐标。

3.3 等值线细分

TES的layout中指定isolines方式,此时输出一系列水平线上的点(相对的u,v值,有点类似于矩形细分),由Outer参数给出水平线上的点数和水平线的数目。应用程序同样可以根据需要解释这个网格,并通过TES生成几何顶点。

4.用OpenGL绘制二维Logistic映射的分叉与混沌

Logistic映射是一个简单可以通过一个简单的迭代实现:

y=xy*(1-y)

其中y的初值是0.5,x作为迭代参数,取在(0,4)中。Logistic通过倍周期分叉,达到混沌,据说在生态学等领域有重要的应用。所谓倍周期分叉,就是当x取(0,2.9954)之间时,该迭代很快收敛到一个值,周期为1,再往后到3.4502附近,迭代的结果在两个值之间来回跳,周期数加倍为2,后面是4个值周期,八个值周期,直到陷入一种无法分辨周期的混沌状态,也许是迭代次数不足,也许是y方向精度不够导致无法分辨出周期所以看着混沌,其实还是有周期的。其实计算机计算时,每次都是有限精度计算,每次迭代都有误差积累,因此这种迭代不可能实现数学意义上的迭代,所以这种混沌用模拟计算来观察,不那么靠谱,只能将就着看看了。真要去深究,得通过数学手段才行。下面是一个绘图结果:
在这里插入图片描述
可以看到随着x的增加,y的收敛周期越来越长,然后陷入一个混沌区间,有意思的是,中间有几个地方偶尔又恢复了短一点的周期,然后又陷入混沌。

现在我们关心的是,如何用OpenGL来绘制这个图形,当然还要实现局部放大的效果才行。如果没有曲面细分和几何着色器的技术,我们只好用CPU来实现迭代,生成点然后送到OpenGL中绘制点。有曲面细分技术,所有计算就可以用GPU来完成了。

基本的思路是,我们绘制一系列水平线,然后应用曲面细分中的等值线细分将水平线分成x方向的点,将点传入到GS实现Logistic迭代生成顶点,最后用BLEND方式将迭代生成的颜色累积到帧存中,迭代位置经过的点越多,颜色就越鲜明,经过位置少的,自然就暗,这样可以看出整个收敛过程。

要注意两点:其一,曲面细分的细分控制参数有限制,一般实现是64,也就是说最多分出64个x值,这样为了保证1024的像素精度,我们需要绘制16个图元。其二,几何着色器的输出也是由限制的,一般实现最多256个顶点,因此在y方向上的输出分辨率受到限制,如果想表现混沌,256个点是不够的,解决的办法是使用等值线细分模式,生成多条水平线,按照水平线的y方向值来设置迭代次数,这样就可以模拟很多次迭代的效果。

嗯,看了这么多还没看到代码,实在有点对不住了啊,看代码吧:

4.1绘制代码

#define SPLIT 16
double pfromx = 3.27;
double pfromy =  0.0;
double psize = 1.1;

float vertex[SPLIT][2];
int vertexinited = 0;

void Render(GLuint program, int width, int height)
{
   
  if (vertexinited == 0) {
   
    for (int i = 0;i<SPLIT;i++) {
   
      vertex[i][0] = (i * 2.0f) / SPLIT - 1.0f;
      vertex[i]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

饶先宏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值