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][1] = 0.0f;
    }
    vertexinited = 1;
  }
  glViewport(0, 0, width, height);
  glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glDisable(GL_DEPTH_TEST);
  /*打开混合模式*/
  glEnable(GL_BLEND);
  /*选择前景和背景像素叠加的融合模式,
     这样在同一位置绘制的次数越多,
     像素的颜色就越亮,这是从OpenGL red-book
     中学来的小技巧,之前自己还没这么想过呢
  */
  glBlendFunc(GL_ONE, GL_ONE);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  glUseProgram(program);

  int sizeLoc = glGetUniformLocation(program, "psize");
  glUniform1d(sizeLoc, psize);
  int fromLoc = glGetUniformLocation(program, "pfrom");
  glUniform2d(fromLoc, pfromx, pfromy);

  glEnableClientState(GL_VERTEX_ARRAY);
  glVertexPointer(2, GL_FLOAT, 2 * sizeof(float), vertex);
  glPatchParameteri(GL_PATCH_VERTICES, 1);
  glDrawArrays(GL_PATCHES, 0, sizeof(vertex) / (2 * sizeof(float)));
}

4.2顶点着色器(VS)

#version 110

void main()
{
  gl_Position = gl_Vertex;
}

4.3细分控制参数着色器(TCS)

#version 400
layout (vertices = 16)out;

void main(void)
{
  gl_TessLevelInner[0] = 16;
  gl_TessLevelInner[1] = 64;
  gl_TessLevelOuter[0] = 16;
  gl_TessLevelOuter[1] = 64;
  gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}

4.4细分顶点计算着色器(TES)

#version 400
/*等值线细分模式*/
layout (isolines) in;

void main()
{
  gl_Position = vec4(gl_in[0].gl_Position.x + gl_TessCoord.x / 8, gl_Tess Coord.y, 0.0, 1.0f);
}

4.5几何着色器(GS)

#version 400
uniform double psize;
uniform dvec2 pfrom;
layout(lines) in;
layout(points, max_vertices = 256) out;

out VS_OUT {
  vec4 vColor;
}vs_out;

void main()
{
  float x, u;
  int outcount;
  int i, j, last, limcount;
  u = float(gl_in[0].gl_Position.x);
  u *= float(psize);
  u += float(pfrom.x);
  if (u > 0.0 && u < 4.0) {
    x = 0.5;
    last = 0;
    limcount = 0;
    /*
       输出像素之前先迭代若干次,
       这个次数跟细分网格的y相关,
       这样可以在不同的y输入时输出的
       迭代位置所经过的迭代次数不一样
  */
    for (i = 0;i<512 * (1.0 + gl_in[0].gl_Position.y);i++) {
      x = u * x * (1.0 - x);
    }
    outcount = 0;
    for (i = 0;i<2000;i++) {
      float y; 
      x = u * x * (1.0 - x);
      y = ((x * 2 - 1.0f) - float(pfrom.y)) / float(psize);
      if (y > -1.0 && y < 1.0) {
         float xx, yy;
         xx = gl_in[0].gl_Position.x;
         yy = float(y);
         gl_Position = vec4(xx, yy, 0.0f, 1.0f);
         /*每绘制一次累加0.1到当前像素*/
         vs_out.vColor = vec4(gl_in[0].gl_Position.y/10.0, 0.01f, 0.0f, 1.0f);
         EmitVertex();
         outcount ++;
         if (outcount > 250)
           break;
        }
     }
  }

  gl_Position = vec4(0, 0, 0.0f, 1.0f);
  vs_out.vColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);
  EmitVertex();
  EndPrimitive();
}

4.6片段着色器(FS)

#version 400
in VS_OUT {
  vec4 vColor;
}vs_in;

void main()
{
  gl_FragColor = vs_in.vColor;
}

4.7后面来看几副图

这是四周期的上半部分:
在这里插入图片描述
这是上面部分再放大:
在这里插入图片描述
这是中间从混沌突然变到有序的局部放大图:
在这里插入图片描述
这个应该不是计算精度缺失导致的混沌到有序,应该是某种内在的机理导致的,感兴趣的可以到网络上搜Logistic映射的相关内容,我也不是太清楚其中的奥妙了。

5.绘制圆环

前面的例子似乎还不是典型的曲面细分的应用,大部分功能实现在GS中实现,曲面细分只是减少了CPU的顶点输入,以及在缩放时产生更多的顶点,细分控制并没有按照本文开始说的按照绘制模型显示的大小来调整。我们再给出一个例子,展示细分图元的参数的解释,细分网格的解释,充分展现曲面细分的风采。
我们的目标是绘制一个圆环,其中心在(0,0)点,绘制代码只是通过顶点的x,y分别给出圆环内侧半径和外侧半径,我们通过曲面细分来绘制整个圆环,并根据投影后的圆环在屏幕上的大小调整细分参数,来实现不同分辨率下的细分控制。
为了表现不同分辨率下细分参数的改变,我们绘制多个圆环,并将圆环置于一个与视点距离不同的场景中,细分参数按照矩形细分来做,水平方向映射到环的圆周,因此细分参数按照环的中心圆圈的长度(在屏幕上实际像素长度)来计算,这样大的环,水平方向分得段数多,小的环则分段少,垂直方向对应径向,我们根据环的宽度(像素单位)来确定分段数,同样环越宽,分段越多。这样能体现曲面的细分的本意。
我们采用矩形细分方式,并在TES中将网格相对坐标解释为极坐标,x,y分别对应极角和极半径(相对圆环内侧半径到外侧半径之间的比例),这样就讲一个矩形映射到一个圆环上,最终绘制出圆环。
少废话,古人说得好:talk is cheap, show me the code!

5.1绘制代码

static ShaderItem shaders[] = { 
  {GL_VERTEX_SHADER, "d:/openglshader/ring.vert", 0},
  {GL_FRAGMENT_SHADER, "d:/openglshader/ring.frag", 0},
  {GL_TESS_CONTROL_SHADER, "d:/openglshader/ring.tcs", 0},
  {GL_TESS_EVALUATION_SHADER, "d:/openglshader/ring.tes", 0},
  {GL_GEOMETRY_SHADER, "d:/openglshader/ring.gs", 0},
  {0, 0},
};

static int ringapp_glapp_Init(HOBJECT object)
{
  sRingApp * pApp;
  pApp = (sRingApp *)objectThis(object);
  pApp->m_program = glLoadProgram(shaders);
  pApp->viewwidthLoc = glGetUniformLocation(pApp->m_program, "viewwidth");
  pApp->tLoc = glGetUniformLocation(pApp->m_program, "t");
  pApp->PLoc = glGetUniformLocation(pApp->m_program, "P");
  pApp->MVLoc = glGetUniformLocation(pApp->m_program, "MV");
  pApp->inited = 1;
  return 0;
}

#define PI 3.14159265f
/*
   圆环生成的顶点,
   x表示圆环内半径,
   y表示外侧半径,
   z表示圆环颜色变化的一个起点
   可以感受到,这里的顶点参数完全脱离了顶点本来的含义,
   跟传统OpenGL的顶点属性差别很大,这就是曲面细分的
   优势,可以把顶点作为细分的控制参数传到TCS和TES中控制
   最终的几何模型。
*/

static float cubevertex[] = {
  0.8f, 1.0f, 0.0f * PI * 2,
  0.7f, 0.75f, 0.3f * PI * 2,
  0.1f, 0.6f, 0.6f * PI * 2,
};

/*我们一次绘制三个圆环*/
static char cubeindex[] = {
  0,1,2
};

/*
   这里用所谓的LCOM来组织代码,
   留到后面的文章中介绍,凡是带
   object的代码都是LCOM的代码,
   先把它看成是一个普通的struct访问即可。
*/

int ringapp_glapp_Render(HOBJECT object, int x, int y, int width, int height)
{
  sRingApp * pApp;
  float mv[16];
  float p[16]; 
  GLfloat h;

  pApp = (sRingApp *)objectThis(object);
  if (pApp->inited == 0) {
    ringapp_glapp_Init(object);
  }

  pApp->x = x;
  pApp->y = y;
  pApp->w = width;
  pApp->h = height;

  h = (GLfloat) height / (GLfloat) width;
  glViewport(x, y, width, height);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glFrustum(-1.0, 1.0, -h, h, 5.0, 90.0);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  glEnable(GL_DEPTH_TEST);
  glClearColor(0, 0, 0, 1.0f);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glTranslatef(0.0f, 0.0f, -pApp->m_dist);
  pApp->m_dist += pApp->m_distinc;
  if (pApp->m_dist > 40 || pApp->m_dist < 6.0)
    pApp->m_distinc = -pApp->m_distinc;
    
  glRotatef(pApp->m_ViewRotX, 1.0f, 0.0f, 0.0f);
  glRotatef(pApp->m_ViewRotY, 0.0f, 1.0f, 0.0f);
  glRotatef(pApp->m_ViewRotZ, 0.0f, 0.0f, 1.0f);

  glGetFloatv(GL_PROJECTION_MATRIX, p);
  glGetFloatv(GL_MODELVIEW_MATRIX, mv);

  glUseProgram(pApp->m_program);
  glPatchParameteri(GL_PATCH_VERTICES, 1);
  glUniform1f(pApp->viewwidthLoc, (float)width);
  glUniform1f(pApp->tLoc, pApp->m_t);
  glUniformMatrix4fv(pApp->PLoc, 1, GL_FALSE, p);
  glUniformMatrix4fv(pApp->MVLoc, 1, GL_FALSE, mv);
  glEnableClientState(GL_VERTEX_ARRAY);
  glVertexPointer(3, GL_FLOAT, 3 * sizeof(float), cubevertex);
  glDrawArraysInstanced(GL_PATCHES, 0, sizeof( cubevertex )/ (sizeof (float) * 3), 1);
  glDisableClientState(GL_VERTEX_ARRAY);

  pApp->m_ViewRotX += 1.0f;
  pApp->m_ViewRotY -= 0.2f;
  pApp->m_ViewRotZ += 0.22f;
  pApp->m_t += 0.02f;
  return 0;
}

5.2顶点着色器(VS)

#version 110

void main() {
   /*直接后传*/
    gl_Position = gl_Vertex;
}

5.3细分控制着色器(TCS)

#version 400
layout (vertices=1)out;
uniform mat4 MV; /* 模型视角矩阵 */
uniform mat4 P;  /* 投影矩阵 */
uniform float viewwidth; /* view port width*/

void main() {
  vec4 u, v; 
  float len, lenu, lenv;
  /* 水平细分参数,映射到圆周上,因此按照环中心圆的周长计算 */
  /*if (gl_InvocationID == 0) */{
    lenu = 0.0f;
    len = (gl_in[gl_InvocationID].gl_Position.x + gl_in[gl_InvocationID] .gl_Position.y) / 2.0f;
   /*为了保证圆环旋转到任何角度都能看到,我们在x,y,z三个方向分别计算实际显示的像素*/
    u = vec4(len, 0.0f, 0.0f, 1.0f);
    u = P * (MV * u); /* 计算中心圆周半径在显示画面中的长度 */
    u /= u.w; /* 归一化 */
    lenu += sqrt(u.x * u.x + u.y * u.y);
    u = vec4(0.0f, len, 0.0f, 1.0f);
    u = P * (MV * u); /* 计算中心圆周半径在显示画面中的长度 */
    u /= u.w; /* 归一化 */
    lenu += sqrt(u.x * u.x + u.y * u.y);

    u = vec4(0.0f, 0.0f, len, 1.0f);
    u = P * (MV * u); /* 计算中心圆周半径在显示画面中的长度 */
    u /= u.w; /* 归一化 */
    lenu += sqrt(u.x * u.x + u.y * u.y);

    lenu *= 3.1415926 * 2;
    lenu = (lenu * viewwidth) / 32.0f; 

    /* 限制在8到64之间 */
    if (lenu > 64.0f) 
      lenu = 64.0f;
    else if (lenu < 8.0f)
      lenu = 8.0f;

    /* 垂直细分参数,映射到径向,因此根据环的宽度计算 */
    lenv = 0.0f;
   len = gl_in[gl_InvocationID].gl_Position.y - gl_in[gl_InvocationID]. gl_Position.x;
    v = vec4(len, 0.0f, 0.0f, 1.0f);
    v = P * (MV * v);
    v /= v.w;
    lenv += sqrt(v.x * v.x + v.y * v.y);

    v = vec4(0.0f, len, 0.0f, 1.0f);
    v = P * (MV * v);
    v /= v.w;
    lenv += sqrt(v.x * v.x + v.y * v.y);

    v = vec4(0.0f, 0.0f, len, 1.0f);
    v = P * (MV * v);
    v /= v.w;
    lenv += sqrt(v.x * v.x + v.y * v.y);

    lenv = (lenv * viewwidth) / 32.0f;
    if (lenv > 64.0f) 
      lenv = 64.0f;
    else if (lenv < 4.0f)
       lenv = 4.0f;
    
    gl_TessLevelInner[0] = lenv;
    gl_TessLevelInner[1] = lenu;
    gl_TessLevelOuter[0] = lenu;
    gl_TessLevelOuter[1] = lenv; 
    gl_TessLevelOuter[2] = lenu;
    gl_TessLevelOuter[3] = lenv;
  }
  gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}

5.4细分顶点计算着色器(TES)

#version 400
/*矩形细分模式*/
layout (quads, equal_spacing, ccw)in;
uniform float t;
uniform mat4 MV;
uniform mat4 P;

out TES_OUT {
  vec3 color;
  vec3 uvw;
}te_out;

void main() {
  vec4 p;
  float cr, cb;
  float u = gl_TessCoord.x;
  float v = gl_TessCoord.y;
/*
  矩形细分网格坐标解释为极坐标,
  u表示极半径(圆环内侧到外侧的相对值)
  v表示极角,映射到0..2PI
*/
  float r = gl_in[0].gl_Position.x + u * (gl_in[0].gl_Position.y - gl_in[0]. gl_Position.x);
  float angle = v * 2.0f * 3.14159265f;
  /*换算成直角坐标*/
  p.x = r * cos(angle);
  p.y = r * sin(angle);
  p.z = 0.0f;
  p.w = 1.0f;
  gl_Position = p;

  /* 颜色值按照相对位置计算 */
  cr = (cos(angle+t+gl_in[0].gl_Position.z) + 3.0f) / 4.0f;
  cb = (sin(angle+t+gl_in[0].gl_Position.z)+ 3.0f) / 4.0f;
  if (u > 0.5f)
    u = 1.0f - u;
  u += 0.5f;
  te_out.color = vec3(cr * u, cb * u / 2.0f, 0.0f);

  /* 记录细分坐标和圆环宽度给GS用 */
  te_out.uvw = vec3(gl_TessCoord.xy, (gl_in[0].gl_Position.y - gl_in[0]. gl_Position.x) / 2.0f);
}

5.5几何着色器(GS)

#version 400

/*控制是生成线条还是生成三角形*/
#define LINEOUT 0
/*控制圆环是否进行轮胎样的扩展*/
#define ZEXTEND 1
/*矩形细分模式输入三角形*/
layout (triangles)in;

layout (
#if LINEOUT
line_strip, /*输出线条是,我们输出的图元改为line_strip*/
#else
triangle_strip, /*输出三角形是,我们输出triangle_strip*/
#endif
max_vertices = 16)out;

uniform mat4 MV;
uniform mat4 P;
uniform float t;

in TES_OUT {
  vec3 color;
  vec3 uvw;
}gs_in[];

out GS_OUT { 
  vec3 color;
  vec3 uvw;
}gs_out;

void main() {
  int i;
  float u0, u1, u2;
  vec4 p0, p1, p2, p3, p4, p5;
  mat4 mvp;
  vec3 u3;

  /*先算出MVP,减少后面的重复计算*/
  mvp = P * MV;

#if ZEXTEND
  /*按照在圆环中径向相对位置0..1,计算z分量*/
  u3 = vec3(gs_in[0].uvw.x, gs_in[1].uvw.x, gs_in[2].uvw.x);
  /*细分位置映射到 -1..1 */
  u3 = u3 * 2.0f - 1.0f;
  /*根据x计算单位圆圆周上的y值*/
  u3 = sqrt(1.0f - u3*u3);
  /*映射到环宽度的一半*/
  u3 *= gs_in[0].uvw.z;
  u0 = u3.x;
  u1 = u3.y;
  u2 = u3.z;
  
  p0 = mvp * vec4(gl_in[0].gl_Position.xy, u0, 1.0f);
  p1 = mvp * vec4(gl_in[1].gl_Position.xy, u1, 1.0f);
  p2 = mvp * vec4(gl_in[2].gl_Position.xy, u2, 1.0f);
  p3 = mvp * vec4(gl_in[0].gl_Position.xy, -u0, 1.0f);
  p4 = mvp * vec4(gl_in[1].gl_Position.xy, -u1, 1.0f);
  p5 = mvp * vec4(gl_in[2].gl_Position.xy, -u2, 1.0f);

  gl_Position = p0;
  gs_out.color = gs_in[0].color;
  gs_out.uvw = gs_in[0].uvw;
  EmitVertex();

  gl_Position = p1;
  gs_out.color = gs_in[1].color;
  gs_out.uvw = gs_in[1].uvw;
  EmitVertex();

  gl_Position = p2;
  gs_out.color = gs_in[2].color;
  gs_out.uvw = gs_in[2].uvw;
  EmitVertex();

#if LINEOUT
  gl_Position = p0;
  gs_out.color = gs_in[0].color;
  gs_out.uvw = gs_in[0].uvw;
  EmitVertex();
#endif
   EndPrimitive();

  gl_Position = p3;
  gs_out.color = gs_in[0].color;
  gs_out.uvw = gs_in[0].uvw;
  EmitVertex();

  gl_Position = p4;
  gs_out.color = gs_in[1].color;
  gs_out.uvw = gs_in[1].uvw;
  EmitVertex();

  gl_Position = p5;
  gs_out.color = gs_in[2].color;
  gs_out.uvw = gs_in[2].uvw;
  EmitVertex();

#if LINEOUT
  gl_Position = p3;
  gs_out.color = gs_in[0].color;
  gs_out.uvw = gs_in[0].uvw;
  EmitVertex();
#endif

  EndPrimitive(); 

#else
  gl_Position = mvp * gl_in[0].gl_Position;
  gs_out.color = gs_in[0].color;
  gs_out.uvw = gs_in[0].uvw;
  EmitVertex();

  gl_Position = mvp * gl_in[1].gl_Position;
  gs_out.color = gs_in[1].color;
  gs_out.uvw = gs_in[1].uvw;
  EmitVertex();

  gl_Position = mvp * gl_in[2].gl_Position;
  gs_out.color = gs_in[2].color;
  gs_out.uvw = gs_in[2].uvw;
  EmitVertex();
  
#if LINEOUT
  gl_Position = mvp * gl_in[0].gl_Position;
  gs_out.color = gs_in[0].color;
  gs_out.uvw = gs_in[0].uvw;
  EmitVertex();
#endif
  EndPrimitive();
#endif
}

5.6片段着色器(FS)

#version 400 core

in GS_OUT {
  vec3 color;
  vec3 uvw;
}fs_in;

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

来看几副图:
这个是GS中选择LINEOUT为1,ZEXTEND为0,FS中选择输出绿色,我们用绿色线条输出,可以看到曲面细分生成的网格。其中对宽的圆环,径向细分就更细一些。
在这里插入图片描述
圆环离相机远了,在屏幕上小了,此时分得就粗一些。
在这里插入图片描述
来看个带颜色的效果
在这里插入图片描述
换个角度
在这里插入图片描述
原文发在微信公众号中,这是复制过来的版本,请参考微信公众号版本
1.OpenGL中的曲面细分和几何着色器
2.OpenGL绘制分形图形MandelBrot集合
3.续:OpenGL绘制分形图形MandelBrot集合

  • 14
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

饶先宏

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

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

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

打赏作者

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

抵扣说明:

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

余额充值