细分着色器

细分着色器

​ 导入低精度的模型,自动将图元细分。同一个模型,在远处时希望用一个低精度的模型来代替展示,以省下更多的计算资源用于处理靠近镜头的物体,使得细节更加清晰。要增加细节就需要在原基础上细分更多的顶点,组成更多的三角形。细分也可用于将平面变成曲面。

​ 顶点着色器-曲面细分控制着色(TCS)-图源生成(PG,固定功能阶段,硬件完成)-曲面细分评估着色(TES)-几何着色器-片段着色器

​ 顶点着色器定义输入patch几个顶点组成–(输入patch一组顶点[])–TCS定义输出patch几个顶点组成、细分区域、细分等级–硬件生成新顶点信息(根据TES设置的layout类型决定传输重心坐标还是uv)–输出新patch一组顶点[]和顶点信息–TES计算顶点坐标及属性、生成图元类型

1.平面区域细分顶点

​ 细分过程并不对OpenGL典型的几何图元(点、线和三角形)进行操作,而是使用一个新的图元,称为patch。patch是传入到OpenGL的一系列顶点列表。当对patch进行渲染时,使用glDrawArrays()渲染命令,函数参数为GL_PATCHES,并从绑定的顶点缓冲对象(VBO)读出的顶点数据,然后为该绘制调用进行处理。当使用一个patch时,需要告诉OpenGL要使用多少个顶点来组成一个patch。同一个绘制调用所处理的patch,每个patch的顶点个数是相同的。输入控制点的最大数量由驱动程序定义的GL_MAX_PATCH_VERTICES决定,但是至少需要大于等于3。

(1)应用程序文件

1
2
3
4
5
6
7
//设置线框绘制,更容易看见使用细分着色器后的变化
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
//告诉OpenGL顶点数组中要使用多少个控制点作为顶点组来组成一个patch
glPatchParameteri(GL_PATCH_VERTICES, 3);
/*顶点/颜色缓冲区相关设置*/
//绘制patch,比如正方体需要12个3个顶点的patch,参数设置为36
glDrawArrays(GL_PATCHES, 0, 3*12);

​ glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)的设置可能会影响后续其他图形的绘制,所以之后需要用glPolygonMode(GL_FRONT, GL_FILL)设置填充绘制。

(2)顶点着色器

​ 顶点缓冲中的所有顶点都会执行顶点着色器。使用细分着色器时,因为需要生成更多的顶点,而这些顶点都需要矩阵变换,因此没必要在顶点着色器进行MVP矩阵变换,推迟到TES将新顶点生成后再进行变换。此处简单的将顶点数据原样传给TCS。

1
2
3
4
5
6
7
#version 400 core
in vec3 position_vert;
out vec3 position_tcs;
void main()
{
position_tcs = position_vert;
}

(3)曲面细分控制着色文件TCS

​ TCS着色器以顶点着色器处理之后的数据作为输入并生成和输出新的patch,新的patch与输入的patch可以不同,控制点数可以不同。除此之外它还负责控制细分发生的区域,并通过任意算法计算细分等级TLs;OpenGL有三种细分区域:四边形、三角形、等值线集合。

​ 内侧细分层级:设置的是细分区域的内部划分方式,保存在gl_TessLevelInner中,内侧细分层级的值设置了区域内水平和垂直方向上各自划分多少区域。

​ 外侧细分层级:负责控制细分区域的周长划分成几段,保存在gl_TessLevelOuter数组中,外侧细分层级的值与周长上每条边细分的段数是对应的。

​ 在三角形域的情况下,只能使用gl_TessLevelOuter的前三个成员和gl_TessLevelInner的第一个成员,四边形域可以使用gl_TessLevelOuter的四个成员和gl_TessLevelInner的两个成员。

image-20221225163139088
1
2
3
4
5
6
gl_TessLevelOuter[0] = 2.0;
gl_TessLevelOuter[1] = 3.0;
gl_TessLevelOuter[2] = 2.0;
gl_TessLevelOuter[3] = 4.0;
gl_TessLevelInner[0] = 3.0;
gl_TessLevelInner[1] = 4.0;
image-20221225163207047
1
2
3
//6条等值线,每条等值线划分承8段
gl_TessLevelOuter[0] = 6;
gl_TessLevelOuter[1] = 8
image-20221225163235088
1
2
3
4
gl_TessLevelOuter[0] = 6;
gl_TessLevelOuter[1] = 5;
gl_TessLevelOuter[2] = 8;
gl_TessLevelInner[0] = 5;

​ 以细分三角形为例:

​ layout设置输出的控制点个数,与glPatchParameteri的设置可以不同,也就是输入控制点数量不一定要等与输出控制点数量(即输入patch与输出patch不一定相同)。每有一个控制点输出,就会运行一次TCS,在如下代码中,TCS会在该patch上运行3次。

1
2
#version 400 core
layout (vertices = 3) out;

​ 细分着色器是处理patch,顶点着色器输入patch包含的一组顶点给TCS,TCS输入新patch包含的一组顶点给TES,因此使用数组修饰符[]定义每个属性。in的数组大小是glPatchParameteri设置的,out的数据大小是layout (vertices=?) out;设置的。此处没有显示指明数组的大小,opengl会自动为其分配大小。也可以显示指明in vec3 position_tcs[3];

1
2
3
uniform vec3 gEyeWorldPos;
in vec3 position_tcs[];
out vec3 position_tes[];

​ 此处基于摄像机和顶点之间的世界空间距离计算TL,距离越近划分越细。

​ gl_InvocationID是OpenGL自带的变量,是控制点的索引值,类似于gl_VertexID。当TCS运行在第一个控制点时,gl_InvocationID值为0,当运行在第二个控制点时,gl_InvocationID值为1…以此类推。输出的控制点数量决定了TCS要运行多少次,所以gl_InvocationID也会与之对应。

​ 此处输入的控制点数量(通过glPatchParameteri设置)等于输出的控制点数量(通过layout (vertices=?) out;设置的),用gl_InvocationID作为索引值,将TCS的数据原样传递给TES。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;}
else {return 3.0;}
}
void main()
{
//或者使用内置变量gl_out[gl_InvocationID].gl_Positon=gl_in[gl_InvocationID].gl_Positon
position_tes[gl_InvocationID] = position_tcs[gl_InvocationID];
float EyeToVertexDistance0 = distance(gEyeWorldPos, WorldPos_ES_in[0]);
float EyeToVertexDistance1 = distance(gEyeWorldPos, WorldPos_ES_in[1]);
float EyeToVertexDistance2 = distance(gEyeWorldPos, WorldPos_ES_in[2]);
//仅在第一次执行(第一个顶点)时控制细分程度)
if (gl_InvocationID == 0)
{
gl_TessLevelOuter[0] = GetTessLevel(EyeToVertexDistance1,EyeToVertexDistance2);
gl_TessLevelOuter[1] = GetTessLevel(EyeToVertexDistance2, EyeToVertexDistance0);
gl_TessLevelOuter[2] = GetTessLevel(EyeToVertexDistance0, EyeToVertexDistance1);
gl_TessLevelInner[0] = gl_TessLevelOuter[2];
}
}

​ TCS后由硬件生成图元,图元生成是一个固定功能阶段。此阶段仅当一个TES在当前程序或程序流水线中活动时才会执行。图元生成受TCS中设置的曲面细分程度和TES中设置的layout影响。图元生成不受TCS中定义的输出patch大小影响。图元生成对实际的顶点坐标与其它patch数据是不可见的。图元生成的目的是确定要生成多少个顶点,用哪个次序来生成它们,以及用哪种图元来构造它们。实际为这些顶点的每个顶点的数据,诸如位置、颜色等等,是通过细分曲面计算着色器来生成的,基于图元生成所提供的信息。

(4)曲面细分求值着色文件TES

​ 以细分三角形为例:

​ layout参数:

​ 1.图元生成域,其他两个选项是四边形和等值线(即与TCS细分等级对应的细分区域,也是传入几何着色器的图元类型)

​ (1)quads——单位正方形中的一个矩形域;域坐标:带有范围在[0, 1]内的u, v值的坐标对(u, v)。

​ (2) triangles——使用重心坐标的一个三角形域;域坐标:带有范围在[0, 1]内的a、b、c三个值的重心坐标(a, b, c),这里a+b+c=1。

​ (3)isolines——跨单位正方形的一组线;域坐标:u值范围在[0, 1],v值范围在[0, 1)范围的(u, v)坐标对。

​ 域坐标就是传递给TES的坐标信息,TES利用坐标信息计算顶点并插值顶点属性。

​ 2.equal_spaceing意味着三角形边缘将被细分为具有相等长度的段

​ 3.第三个参数:图元面朝向(cw顺,ccw逆)

​ 如果想输出点,而不是等值线或填充区域的话,可以添加第四个参数point_mode选项。该选项将为每个由TES所处理的顶点渲染一单个点。

1
layout (triangles, equal_spacing, ccw, point_mode) in;

​ TES可以像任何其他着色器一样具有统一变量uniform,由于TES生成了新顶点,因此在TES中进行MVP矩阵变换。TES访问TCS输出的patch,因此TES输入的顶点数据为数组,数组大小即TCS中通过layout (vertices=?) out;设置的。最后,声明输出顶点的属性,此处输出顶点数据不为数组,因为TES输出单个顶点。

1
2
3
4
5
#version 400 core
layout(triangles, equal_spacing, ccw) in;
uniform mat4 gVP;
in vec3 position_tes[];
out vec3 position;

​ 所有生成的位于细分空间下的新顶点都会经过TES着色器进行处理,TES根据硬件传来的信息计算并生成真正的顶点,除了顶点位置还会插值顶点属性。如下是通过硬件传来的三角形重心坐标生成新的顶点坐标,并且进行MVP变换。如果选择的TL值越高,获得的区域位置点就越多,而且通过在TES中对他们进行计算得到的顶点就会更多,这样就能更好的表示精细的表面。

1
2
3
4
5
6
7
8
9
10
11
vec3 interpolate3D(vec3 v0, vec3 v1, vec3 v2)
{
return vec3(gl_TessCoord.x) * v0 + vec3(gl_TessCoord.y) * v1 + vec3(gl_TessCoord.z) * v2;

}
void main()
{
//通过硬件获得的重心坐标和TCS输出patch的顶点计算新的顶点
position = interpolate3D(position_tes[0], position_tes[1], position_tes[2]);
gl_Position = gVP * vec4(position, 1.0);
}

​ 以细分四边形为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//生成新顶点坐标和颜色值
float u = gl_TessCoord.x;
float v = gl_TessCoord.y;
//从TCS输出的顶点(即,在gl_out数组中的gl_Position的值)在TES中的gl_in变量中可用,此处TCS的输出patch有4个顶点
vec4 p0=gl_in[0].gl_Position;
vec4 p1 = gl_in[1].gl_Position;
vec4 p2 = gl_in[2].gl_Position;
vec4 p3 = gl_in[3].gl_Position;
void main(void)
{
//根据硬件传来的uv坐标信息插值计算,该算法在平面上生成相同性质的点(可采用其他算法)
gl_Position = p0 * (1-u) * (1-v) + p1 * u * (1-v) + p2 * v * (1-u) + p3 * u * v;
fragmentColor=tcs_color[0] * (1-u) * (1-v) + tcs_color[1] * u * (1-v) + tcs_color[2] * v * (1-u) + tcs_color[3] * u * v;
}

​ 以上细分只是patch上细分了多个顶点,形成了更多的三角形,插值出来的顶点都坐落在原始平面或三角形平面上,还未进行实际应用。

参考文献:

https://blog.csdn.net/lele0503/article/details/105881101

https://blog.csdn.net/aoxuestudy/article/details/124116047

其他问题:

如果设置四个顶点:

1
2
3
4
-1.0f, -1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
-1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 0.0f

采用三角形细分区域,输出patch为4个顶点,最后得到的为三角形:

image-20230427174631026

用四个顶点调用glDrawArrays(GL_TRIANGLES, 0,4);也只能绘制出一个三角形。

如果设置六个顶点:

1
2
3
4
5
6
-1.0f, -1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
-1.0f, 1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f

采用四边形细分区域,输出patch为6个顶点,最后得到的为三角形:

image-20230427174631026
2.PN三角曲面细分

(1)顶点着色器

​ TCS依赖于具有单位长度的法线,否则将无法正确生成新的控制点,因此在顶点着色器需要将法线标准化。

1
2
3
in vec3 normal_vert;
out vec3 normal_tcs;
normal_tcs = normalize(normal_vert);

(2)曲面细分控制着色文件

​ layout的输出为1而不是为10:layout设置的输出控制点为多少,TCS就会执行多少次。但在这个算法中,对于10个控制点执行的是不同的操作,因此对所有控制点执行相同的操作不太合适。将输出patch的所有数据封装在OutputPatch结构中,并声明了一个名为oPatch的输出变量。oPatch的前缀是内置关键字patch。该关键字表示变量包含与整个patch相关的数据,而不是当前输出的某一个控制点。TCS主函数将为每个patch运行一次,而不是每个控制点运行一次,并且在这次执行过程中会将这10个控制点的数据存放在结构体中。

1
2
3
4
5
6
7
8
9
10
11
12
#version 400 core
layout (vertices = 1) out;
in vec3 position_tcs[];
in vec3 normal_tcs[];
struct OutputPatch
{
vec3 B030;vec3 B021;vec3 B012;vec3 B003;vec3 B102;
vec3 B201;vec3 B300;vec3 B210;vec3 B120;vec3 B111;
vec3 Normal[3];
vec2 uv[3];
};
out patch OutputPatch oPatch;

​ 三个法线原样从输入复制到输出,并且设置细分等级。10个控制点通过CalcPosition生成。

1
2
3
4
5
6
7
8
9
10
11
void main()
{
for (int i = 0 ; i < 3 ; i++) {
oPatch.Normal[i] = normal_tcs[i];
}
CalcPositions();
gl_TessLevelOuter[0] = 2;
gl_TessLevelOuter[1] = 2;
gl_TessLevelOuter[2] = 2;
gl_TessLevelInner[0] = 2;
}

image-20230126224517474

​ 如图所示,控制点的形式像是在三角形上形成一个臌胀的表面。先把三个顶点作为输入patch。然后通过计算产生10个控制点并设置TLs。硬件将根据TLs细分三角形区域,TES会在每个新的顶点上执行。TES将把从PG硬件得到的新顶点的重心坐标和从TCS中得到的10个控制点输入到贝塞尔三角对应的多项式中,得到坐落在那个膨胀三角面上的新的顶点坐标。

image-20230126225943786

​ 上图所示是三角形的一边,每个端点都有它的原始法线(绿色那条线)。每个端点和它的法线构建了一个平面。如图,中间两个中间点按照虚线方向投影到最近顶点所在的平面上。10个顶点在三角形上形成一个臌胀的表面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void CalcPositions(
{
//三角形上的原始顶点保持不变 (名字为B003,B030和B300);
oPatch.WorldPos_B030 = WorldPos_CS_in[0];
oPatch.WorldPos_B003 = WorldPos_CS_in[1];
oPatch.WorldPos_B300 = WorldPos_CS_in[2];

vec3 EdgeB300 = oPatch.WorldPos_B003 - oPatch.WorldPos_B030;
vec3 EdgeB030 = oPatch.WorldPos_B300 - oPatch.WorldPos_B003;
vec3 EdgeB003 = oPatch.WorldPos_B030 - oPatch.WorldPos_B300;

//每条边上产生两个中间点:一个三1/3处,一个在2/3处;
oPatch.WorldPos_B021 = oPatch.WorldPos_B030 + EdgeB300 / 3.0;
oPatch.WorldPos_B012 = oPatch.WorldPos_B030 + EdgeB300 * 2.0 / 3.0;
oPatch.WorldPos_B102 = oPatch.WorldPos_B003 + EdgeB030 / 3.0;
oPatch.WorldPos_B201 = oPatch.WorldPos_B003 + EdgeB030 * 2.0 / 3.0;
oPatch.WorldPos_B210 = oPatch.WorldPos_B300 + EdgeB003 / 3.0;
oPatch.WorldPos_B120 = oPatch.WorldPos_B300 + EdgeB003 * 2.0 / 3.0;

//每个中间点投影到由最近的顶点和它的法线所在的平面上:
oPatch.WorldPos_B021 = ProjectToPlane(oPatch.WorldPos_B021, oPatch.WorldPos_B030,oPatch.Normal[0]);
oPatch.WorldPos_B012 = ProjectToPlane(oPatch.WorldPos_B012, oPatch.WorldPos_B003,oPatch.Normal[1]);
oPatch.WorldPos_B102 = ProjectToPlane(oPatch.WorldPos_B102, oPatch.WorldPos_B003,oPatch.Normal[1]);
oPatch.WorldPos_B201 = ProjectToPlane(oPatch.WorldPos_B201, oPatch.WorldPos_B300,oPatch.Normal[2]);
oPatch.WorldPos_B210 = ProjectToPlane(oPatch.WorldPos_B210, oPatch.WorldPos_B300,oPatch.Normal[2]);
oPatch.WorldPos_B120 = ProjectToPlane(oPatch.WorldPos_B120, oPatch.WorldPos_B030,oPatch.Normal[0]);

//为了计算B111,要计算得到一个向量,这个向量是从原三角形中心到6个投影后的中间点的平均点。延长该向量1.5的终点作为B111。
vec3 Center = (oPatch.WorldPos_B003 + oPatch.WorldPos_B030 + oPatch.WorldPos_B300) / 3.0;
oPatch.WorldPos_B111 = (oPatch.WorldPos_B021 + oPatch.WorldPos_B012 + oPatch.WorldPos_B102 + oPatch.WorldPos_B201 + oPatch.WorldPos_B210 + oPatch.WorldPos_B120) / 6.0;
oPatch.WorldPos_B111 += (oPatch.WorldPos_B111 - Center) / 2.0;
}
image-20230126225354504

​ P1和P2位于由平面创建的不同半空间上。当在绿色法线上投影v1时,得到d1的长度。将该长度乘以法线以接收d1本身。现在从P1减去它,得到它在平面上的投影。当在绿色法线上投影v2时,得到d2的长度,但它是负值。将其乘以法线以接收d2本身(负长度意味着它反转法线)。现在从P2减去它,得到它在平面上的投影。结论:无论点在平面的哪一侧,该方法都能正确工作。因此可以用如下算法计算投影后的新控制点坐标。

1
2
3
4
5
6
7
vec3 ProjectToPlane(vec3 Point, vec3 PlanePoint, vec3 PlaneNormal)
{
vec3 v = Point - PlanePoint;
float Len = dot(v, PlaneNormal);
vec3 d = Len * PlaneNormal;
return (Point - d);
}

(3)曲面细分求值着色文件

image-20230126231841370

​ 根据贝塞尔三角公式计算新生成的顶点,(u,v,w)是硬件传进来的重心坐标,通过gl_TessCoord访问,它们的和永远为1:u+v+w=1。Bxyz指的是控制点,式子右边有10个控制点,生成的b是通过插值后的真正的新顶点坐标,b坐标在patch平面之外,因而实现曲面效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#version 400 core

layout(triangles, equal_spacing, ccw) in;
uniform mat4 MVP;
struct OutputPatch
{
vec3 B030;vec3 B021;vec3 B012;vec3 B003;vec3 B102;
vec3 B201;vec3 B300; vec3 B210;vec3 B120;vec3 B111;
vec3 Normal[3];vec2 uv[3];
};
in patch OutputPatch oPatch;

//如果插值纹理就将vec3改成vec2,如下函数可以用于插值顶点其他属性
//vec3 interpolate3D(vec3 v0, vec3 v1, vec3 v2)
//{
// return vec3(gl_TessCoord.x) * v0 + vec3(gl_TessCoord.y) * v1 + vec3(gl_TessCoord.z) * v2;
//}

void main()
{

float u = gl_TessCoord.x;float v = gl_TessCoord.y;float w = gl_TessCoord.z;

float uPow3 = pow(u, 3);float vPow3 = pow(v, 3);float wPow3 = pow(w, 3);
float uPow2 = pow(u, 2);float vPow2 = pow(v, 2);float wPow2 = pow(w, 2);

vvec3 fi_position =
oPatch.B300 * wPow3 +
oPatch.B030 * uPow3 +
oPatch.B003 * vPow3 +
oPatch.B210 * 3.0 * wPow2 * u +
oPatch.B120 * 3.0 * w * uPow2 +
oPatch.B201 * 3.0 * wPow2 * v +
oPatch.B021 * 3.0 * uPow2 * v +
oPatch.B102 * 3.0 * w * vPow2 +
oPatch.B012 * 3.0 * u * vPow2 +
oPatch.B111 * 6.0 * w * u * v;

gl_Position = MVP * vec4(fi_position, 1.0);

}

​ 为了实现曲面效果,一个3个顶点的patch的三个法线不能设置成相同的,不然计算出的多余的10个控制点就不会在patch的平面之上形成曲面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static const GLfloat vertexdata[] = {
-1.0f, -1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
-1.0f, 1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f,
};
static const GLfloat normaldata[] = {
-0.5f, -0.5f, 1.0f,
0.5f, -0.5f, 1.0f,
-0.5f, 0.5f, 1.0f,
0.5f, -0.5f, 1.0f,
0.5f, 0.5f, 1.0f,
-0.5f, 0.5f, 1.0f
};

​ 效果如下,生成曲面:

image-20230127003907057

3.增加法线

​ 通过在曲面细分求值着色文件利用法线,利用算法重新将插值后的新顶点坐标进行修改,使其位于原平面之上。用该方法可以用于绘制曲面等。

​ 曲面细分控制着色文件:

1
2
3
4
//将法线数据传给TES
in vec3 normal_tcs[];
out vec3 normal_tes[];
normal_tes[gl_InvocationID] = normal_tcs[gl_InvocationID];

​ 曲面细分求值着色文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//根据顶点到中心的距离计算顶点的高度(可任意写)
float distance = max( abs(u-0.5), abs(v-0.5) );
distance = max( distance, abs(w-0.5) );
float offset = 0.0;
if (distance<0.2){
offset = (1.0 - distance*2)/9.0;
}
else if(distance<0.3){
offset = (1.0 - distance*2)/10.0;
}
else if(distance<0.4){
offset = (1.0 - distance*2)/11.0;
}
else if(distance<0.5){
offset = (1.0 - distance*2)/12.0;
}
else {
offset = (1.0 - distance*2)/13.0;
}
vec3 position = interpolate3D(position_tes[0], position_tes[1], position_tes[2]);
position += tes_normal[0]*offset +tes_normal[1]*offset +tes_normal[2]*offset;
gl_Position = MVP * vec4(position, 1.0);

​ 效果如下:

QQ图片20221226154044
4.修改成ES后出现的问题

(1)ES的必须3.2版本才能支持细分着色器

(2)ES的细分着色器TCS和TES间可以传递结构体,但传递的结构体中不能包含数组或者结构体

(3)ES的细分着色器需要设置精度,如在着色器开头添加precIntision mediump float;但是在添加该行后,就不能用Int类型给之后所有变量赋值

(4)TCS和TES中不可指定in/out数组大小

(5)TCS中访问当前的控制点只能使用内置变量gl_InvocationID

(6)ES不支持该函数glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);,因此如果需要用线框绘制,得使用几何着色器

(7)ES没有内置变量gl_FragColor

OpenGL 4.0的Tessellation Shader(细分曲面着色器)_opengl4.0_龙行天下01的博客-CSDN博客

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

扫一扫,分享到微信

微信分享二维码

请我喝杯咖啡吧~

支付宝
微信