[摘要]本文我们先介绍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集合。