Fork me on GitHub

几何着色器&公告牌

几何着色器

QQ图片20221228095854

渲染管线:顶点着色器-细分着色器-几何着色器-片段着色器

作用:由于几何着色器可以在顶点发送到下一着色阶段之前对他们随意变换,所以作用就是将一个图元变换为另一个完全不同的图元,或者修改图元的位置

使用方法:

  • 在几何着色器的顶部,声明从顶点着色器输入的图元类型:layout (类型) in;
  • 可以输入的图元有5类:points,lines,lines_adjacency,triangles,triangles_adjacency
  • 指定几何着色器的输出图元类型:layout (primitive, max_vertices = n) out;
  • 输出图元有三种选项:points,line_strip,triangle_strip
  • 几何着色器还要设置一个他的最大输出顶点数量,如果绘制时超过了这个值,OpenGL将不会绘制多出的顶点

以一个简单的几何着色器为例(顶点和细分着色器都不用修改,但是要注意细分着色器的patch不能为四边形):

注意ES中没有gl_PositionIn内置变量,因此需要从TES中通过out和in传入顶点数据,且进入几何着色器的数据为一个图元所有顶点数据,因此传入的顶点数据为数组。并且对于不确定大小的数据,不能通过变量访问,因此不能使用i循环访问,需要使用0,1,2。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#version 330
#extension GL_ARB_geometry_shader4:enable
layout( triangles ) in;
//layout( line_strip, max_vertices = 3 ) out;的话一个三角形三个顶点用2条线绘制,四边形不能封口
layout( line_strip, max_vertices = 4 ) out;
out vec4 color;//将当前顶点color传给片段着色器的gl_FragColor
in vec4 fragmentColor[];//几何着色器的输入为图元的一组顶点相关数据,必须设置为数组类型
void main(void)
{
int i =0;
//输入图元的一个图元的顶点的个数
for(i=0;i<gl_PositionIn.length();i++)
{
gl_Position=gl_PositionIn[i];
color=fragmentColor[0];//颜色传入一组,为当前顶点选择一个颜色值
EmitVertex();//输出一个顶点
}
//四边形在[0]封口
gl_Position=gl_PositionIn[0];
color=fragmentColor[0];
EmitVertex();
EndPrimitive();//结束一个图元的输出
}

以上几何着色器代码实现的效果同理:

1
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

效果如下:

image-20221228133913472

暂时不太会用几何着色器和细分着色器的情况下添加光照计算

几何着色器绘制公告牌

​ 公告板技术(billboarding)。公告板是一个始终朝向相机的四边形,当相机在场景中转动的时候,公告板也会随着相机转动保证相机方向向量始终垂直于公告板的正面。可以将怪物角色、树木等任何场景中重复性高、数量多的物体以纹理贴图的形式直接贴到四边形公告板上(而不需要复杂的计算和渲染实际的3d模型),始终朝向相机。公告板常常用来创建需要大量树木的森林效果,每一个公告板只需要4个顶点。

让公告板始终朝向相机:

image-20230127114208268

​ 首先把公告板的四边形中心定义在世界中心center(0.0,0.0,0.0,0.0)。为了让四边形始终朝向相机,其四个顶点坐标与相机空间的u和s轴相同。(opengl使用右手坐标系)

几何着色器中先求出 -f 向量,即摄像机朝向。先获取公告板中心坐标,用该坐标-相机位置坐标(也是定义在世界空间),最后将其标准化。

1
2
vec3 Pos = gl_in[0].gl_Position.xyz;
vec3 toCamera = normalize(Pos-gCameraPos);

在选取一个up向量,一般选取(0.0,1.0,0.0),利用up向量和-f向量叉乘可以获得right向量,up向量和-f向量在一个平面,因此right一定垂直于-f。注意叉乘的顺序。

1
2
vec3 up = vec3(0.0, 1.0, 0.0);
vec3 right = normalize(cross(toCamera, up));

最后用rihgt向量和-f向量叉乘得到u向量

1
vec3 u = cross(right, toCamera);

通过中心点,u向量和right向量计算公告板的四个顶点坐标,每计算出一个顶点就输出一次顶点,还根据顶点位置计算UV坐标。顶点着色器只传入一个中心点,几何着色器输入point,输出triangle_strip形成三角形带。但是要注意的是up向量不一定就是u向量,向量仅仅是用于计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
layout( points ) in;
layout( triangle_strip, max_vertices = 4 ) out;

gl_Position = gVP * vec4((Pos - right * 0.5 - u * 0.5),1.0);
TexCoord = vec2(0.0, 0.0);
EmitVertex();
gl_Position = gVP * vec4((Pos - right * 0.5 + u * 0.5),1.0);
TexCoord = vec2(0.0, 1.0);
EmitVertex();
gl_Position = gVP * vec4((Pos + right * 0.5 - u * 0.5),1.0);
TexCoord = vec2(1.0, 0.0);
EmitVertex();
gl_Position = gVP * vec4((Pos + right * 0.5 + u * 0.5),1.0);
TexCoord = vec2(1.0, 1.0);
EmitVertex();
EndPrimitive();

下图是view矩阵的结构,

image-20230127120306288

glm库储存矩阵元素采用的是列优先的储存方式,因此除了在几何着色器中手动计算right,up向量,还可以通过如下代码直接获取right,up向量直接传入着色器,此时就不需要传入相机位置。

1
2
3
4
5
6
7
8
9
10
computeMatricesFromInputs();
glm::mat4 ProjectionMatrix = getProjectionMatrix();
glm::mat4 ViewMatrix = getViewMatrix();
//此处也可以使用glm::lookat函数计算ViewMatrix(参数:相机位置坐标,目标位置坐标,up向量)
glUniform3f(rightID, ViewMatrix[0][0], ViewMatrix[1][0], ViewMatrix[2][0]);
glUniform3f(upID, ViewMatrix[0][1], ViewMatrix[1][1], ViewMatrix[2][1]);
/*
glm::vec3 CameraPosition = glm::vec3(-ViewMatrix[0][2], -ViewMatrix[1][2], -ViewMatrix[2][2]);
glUniform3f(PosID, CameraPosition.x, CameraPosition.y, CameraPosition.z);
*/

细分着色器

细分着色器

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

​ 顶点着色器-曲面细分控制着色(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博客

封装纹理贴图

原理:将要绘制的图案利用帧缓冲作为纹理渲染在四边形上,在将四边形绘制在需要的位置。

重点代码:

1.初始化帧缓冲(同理tutorial示例14,phrtest示例07)

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
void initRTT(void) {
//生成一个纹理(如果四边形上要绘制多个纹理,就修改数字1)
glGenTextures(1, renderTextureID);

//纹理数组(空数组,仅分配了内存)
unsigned char* voxels;
voxels =
(unsigned char*)
malloc((size_t)(4 * 1200 * 800));

/*
for (GLint i = 0; i < 4; i++)
{
glBindTexture(GL_TEXTURE_2D, renderTextureID[i]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fboWidth, fboHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, voxels);
}
*/

//绑定纹理为2D渲染纹理
glBindTexture(GL_TEXTURE_2D, renderTextureID[0]);
//纹理过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//定义纹理,用空数组为纹理填充空图像
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1200, 800, 0, GL_RGBA, GL_UNSIGNED_BYTE, voxels);
free(voxels);

//生成并绑定一个帧缓冲(至少一个颜色附件)
glGenFramebuffers(1, &framebufferID);
glBindFramebuffer(GL_FRAMEBUFFER, framebufferID);

//渲染一个3D模型,要进行深度测试,生成并绑定一个深度渲染缓冲区
glGenRenderbuffers(1, &renderbufferID);
glBindRenderbuffer(GL_RENDERBUFFER, renderbufferID);
//指定深度渲染缓冲区的数据格式和大小
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT32, 1200, 800);
//深度渲染缓冲绑定为帧缓冲的深度附件
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, renderbufferID);
glBindFramebuffer(GL_FRAMEBUFFER, framebufferID);

//纹理绑定为帧缓冲的颜色附件(颜色附件可以有多个)
/*
for (int i = 0; i < 4; i++)
{
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, renderTextureID[i], 0);
}
*/
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, renderTextureID[0], 0);

//默认渲染到窗口
glBindFramebuffer(GL_FRAMEBUFFER, 0);

2.主循环重点代码

第二行设置的是四边形的背景颜色,不过一般需要剔除所以可以不用刻意设置,不设置的话四边形的背景颜色就是整个窗口的背景颜色。

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
//第一次清屏
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glClearColor(1.0f, 0.5f, 1.0f,1.0f);

//把渲染目标绑定到我们定义的帧缓冲
glBindFramebuffer(GL_FRAMEBUFFER, framebufferID);

//第二次清屏
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glViewport(0, 50, 1200, 800);

//指定接下来渲染时需要渲染到哪些颜色附件(此处仅一个颜色附件)
GLenum buf[1] = { GL_COLOR_ATTACHMENT0 };
glDrawBuffers(1, buf);

/*
绘制图案的代码(全部绘制在帧缓冲,不会显示在屏幕,用于接下来渲染四边形)
*/

//渲染目标绑定到默认帧缓冲,显示在屏幕
glBindFramebuffer(GL_FRAMEBUFFER, 0);

/*
绘制四边形的代码
(注意n个颜色附件就需要使用n个采样器,绑定n次纹理)
*/

3.着色器文件

(1)四边形顶点着色器文件主函数示例

1
2
3
4
5
// wall.vert: simple quad (-1.-1)--(1,1) map texture2D(0.0)--(1,1)
void main(void) {
gl_Position = MVP * vec4(position_wall,1);
UV = (position_wall.xy+ vec2(1.0,1.0))/2.0;
}

(2)四分屏的四边形片段着色器文件主函数

分屏代码计算:https://www.jianshu.com/p/1b1d9fd8ba17

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main (void)
{
if(UV.x < 0.5){
if(UV.y<0.5)
gl_FragColor = texture2D(samplerRTT1, UV*2.0);
else
gl_FragColor = texture2D(samplerRTT1, 2.0*vec2(UV.x,UV.y-0.5));
}
else{
if(UV.y<0.5)
gl_FragColor = texture2D(samplerRTT1, 2.0*vec2(UV.x-0.5,UV.y));
else
gl_FragColor = texture2D(samplerRTT1, 2.0*vec2(UV.x-0.5,UV.y-0.5));
}
}

(3)不分屏的四边形片段着色器文件主函数

1
2
3
4
5
void main (void)
{
vec4 color=texture2D(samplerRTT1, UV);
gl_FragColor = color;
}

该方法生成的纹理数据作为一张静态图由采样器根据UV采样访问,渲染在其他地方。

image-20221227143718485

git版本管理命令使用

权限问题

https://blog.csdn.net/qq_50790981/article/details/124991911

创建版本库及版本管理命令

1.创建工程目录

1
$ mkdir project(任取)

2.进入工程目录下,新建版本仓库(或者合并成git init project)

1
$ git init

3.利用ls命令查看隐藏目录.git(版本库,最终要的是暂存区、分支及分支头指针)

1
$ ls -ah

4.工作目录或其子目录下编写或修改文件,如在project文件夹中新建一个readme.txt的文本文件

(1)将readme.txt 单个文件添加到版本库的暂存区(一般不直接从工作区到分支)

1
$ git add readme.txt
命令 含义 使用场景
$ git add . 将当前目录的所有文件添加到暂存区 版本库管理工程
$ git add all 相当于$ git add . 版本库管理工程
$ git add folder 将指定目录folder及其子目录添加到暂存区 版本库管理一个目录

5.将暂存区的文件提交到仓库分支,并对本次提交进行说明

1
$ git commit readme.txt -m "文件说明"
命令 含义
$ git commit –v 提交时显示diff信息
$ git commit –amend –m “我写了一个很好的文档” 用本次提交代替上一次提交;若文件没有变化,则改写提交信息
$ git commit –amend 重做一次commit
$ git commit –a 将自上次提交后的工作区的变化直接提交到仓库

6.提交前可以先进行确认和检查

查看工作区的文件又没有改动等信息,modified后为改动过的文件

1
$ git status
image-20221208114505983

查看文件进行了哪些修改,即暂存区和工作区的差异

1
$ git diff
image-20221208114623103

如果运行git add readme.txt后,则暂存区和工作区无差异

image-20221208115714252
命令 含义
$ git diff –cached 显示暂存区和上一个commit的差异
$ git diff HEAD 显示工作区与当前分支下的最新commit之间的差异
$ git diff [branch1] [branch2] 显示两次提交之间的差异

7.提交后查看当前仓库中的所有提交的历史记录

image-20221208114756299
命令
$ git log –pretty=oneline 显示提交历史的简要信息
$ git reflog 显示当前分支最近几次操作

8.版本回退

命令 含义
$ git reset –hard HEAD^ 将版本库、暂存区、工作区都回退到上一个版本
$ git reset –soft HEAD^ 只回退版本库
$ git reset –mixed HEAD^ 回退版本库和暂存区
$ git reset –hard commit_id 回退到commit_id表示的版本,可以是当前版本之前或之后的版本

QQ图片20221208160912

9.恢复

用暂存区(版本库)的内容覆盖工作区的内容(如果工作区的内容删除了也可以用这种方式恢复)

1
$ git checkout --文件名

10.删除

将文件删除并提交到暂存区(需要先从工作区删除,再使用本条命令,之后还要commit)

1
$ git rm 文件名

只从stage中删除,保留物理文件

1
$ git rm --cached 文件名

11.分支

建立一个副本,在副本上修改,主分支master的内容不变

(1)创建分支

命令 含义
$ git branch 列出所有本地分支
$ git branch -r 列出所有远程分支
$ git branch -a 列出所有本地和远程分支
$ git branch dev 新建一个分支dev,但不切换到此分支
$ git branch dev commit_id 新建一个分支,并指向某个commit
$ git branch –d dev 删除本地分支dev
$ git branch –dr 删除远程分支

在dev2上不存在第一个修改的记录

QQ图片20221209092742 QQ图片20221209092738

(2)切换分支

命令 含义
$ git checkout –b dev 新建一个分支dev,并切换到此分支
$ git checkout dev 切换到分支dev,并更新工作区

(3)分支合并

将分支dev合并到当前分支

1
$ git merge dev

选择一个commit,合并到当前分支

1
$ git cherry --pick commit_id

(4)解决冲突

如果同一个文件在合并分支时都被修改了则会引起冲突

QQ图片20221209110831 QQ图片20221209110835

<<<<<<<,=======,>>>>>>>标记出不同分支的内容,其中<<<HEAD是指主分支修改的内容,>>>>>dev6 是指dev6上修改的内容

QQ图片20221209110838

解决的办法是可以修改冲突文件后重新提交。master主分支应该非常稳定,用来发布新版本,一般情况下不允许在上面工作,工作一般情况下在新建的dev分支上工作,工作完后,比如上要发布,或者说dev分支代码稳定后可以合并到主分支master上。用git log –graph命令可以看到分支合并图。

(5)删除本地分支

1
$ git branch -d [branch-name] -D(大写)强制删除

12.为不好记的commit_id打标签

命令 含义
$ git tag 查看所有标签
$git tag V1.0 为当前分支打上标签V1.0
$git tag -d v1.0 删除当前分支的标签V1.0
$git tag V1.0 commit_id 为指定的某次提交打上标签
$git tag -a V1.0 -m “version1.0 released” 为当前分支创建带说明性文字的标签号
$git show V1.0 查看标签为V1.0的信息

远程相关、忽略文件相关

git基本知识

Git

Linus为了管理Linux代码库编写的一个开源的分布式版本控制系统,可以有效、高速地实现从很小到非常大的项目的版本管理。

版本控制:是一种在开发的过程中用于管理我们对文件、目录或工程等内容的修改历史,方便查看更改历史记录,备份以便恢复以前的版本的软件工程技术。

优点:分支管理能力更强;运行更快;更安全,系统内任何一台机器出故障都不影响整体项目

Git的本地系统结构

图片1

Git将一个工作目录分为工作区和版本库。工作在工作区完成;所谓版本库是一个名为.git的隐藏文件夹,包括很多内容,其中最重要的是stage(暂存区)和第一条分支master,以及指向当前分支的指针HEAD。所有文件都可以从工作区流向分支,也可以从分支流向工作区,这些工作都有相应的命令完成。

Git的分支管理原理

一开始的时候,master分支是一条线,Git用master指向最新的一次提交(最近的一次commit),再用HEAD指向master,就能确定当前分支,以及当前分支的提交点:

image-20221208103538577

每次提交,master分支都会向前移动一步,这样,随着你不断提交,master分支的线也越来越长。

当我们创建新的分支,例如dev时,Git新建了一个指针叫dev,指向master相同的提交,再把HEAD指向dev,就表示当前分支在dev上:

image-20221208103935618

除了增加一个dev指针,改改HEAD的指向,工作区的文件都没有任何变化。从现在开始,对工作区的修改和提交就是针对dev分支了,比如新提交一次后,dev指针往前移动一步,而master指针不变

image-20221208104024986

假如我们在dev上的工作完成了,就可以把dev合并到master上。把master指向dev的当前提交,就完成了合并

image-20221208104130235

合并完分支后,可以删除dev分支。删除dev分支就是把dev指针给删掉,删掉后就剩下了一条master分支

image-20221208104241927

现在,master分支和feature1分支各自都分别有新的提交,变成了这样:

image-20221208104314358

这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突(比如两次提交的某个文件的某一句被都被修改了,且修改不一样),如果冲突了Git会告诉我们,哪个文件存在冲突,必须手动解决冲突后再提交。再次提交后,master分支和feature1分支变成了下图所示:

image-20221208104338669

在实际开发中,我们应该按照几个基本原则进行分支管理:

首先,master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;

干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到master上,在master分支发布1.0版本;每个人都在dev分支上干活,每个人都有自己的分支,时不时地往dev分支上合并就可以了。

所以,团队合作的分支看起来就像这样:

image-20221208104403529

软件开发中,bug就像家常便饭一样。有了bug就需要修复,在Git中,每个bug都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。

Git下多人开发的工作模式

多人协作的工作模式通常是这样:

  1. 首先,可以试图用git push origin 推送自己的修改;

  2. 如果推送失败,则因为远程分支比你的本地更新,需要先用git pull试图合并;

  3. 如果合并有冲突,则解决冲突,并在本地提交;

  4. 没有冲突或者解决掉冲突后,再用git push origin 推送就能成功!

如果git pull提示no tracking information,则说明本地分支和远程分支的链接关系没有创建,用命令git branch –set-upstream-to origin/

着色器文件

1.最基本的顶点着色器

(1)attribute变量:是只能在顶点着色器中使用的变量,表示一些顶点的数据,cpp传值获取。

(2)uniform变量:是外部程序传递给顶点/片段的变量,函数glUniform**()函数赋值。相当于常量,不可改。如果uniform变量在顶点和片段两者之间声明方式完全一样,则它可以在vertex和fragment共享使用。一般用来表示变换矩阵,材质,光照参数和颜色等信息。

(3)“vertexPosition_modelspace”变量名:可以任取,其中保存的是顶点位置,顶点着色器每次运行时都会用到。

(4)gl_Position:内置变量之一,必须赋值。

(5)“layout(location = 0)“:指向存储vertexPosition_modelspace属性的缓冲。每个顶点有多种属性,因此我们必须将glvertexAttribPointer函数的第一个参数值赋给layout,以此告知OpenGL每个缓冲对应的是哪种属性数据。第二个参数“0”并不重要,也可以换成12(但是不能超过glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &v)),关键是C++和GLSL两边数值必须保持一致。

或使用GLuint vertexPosition_modelspaceID = glGetAttribLocation(programID, “vertexPosition_modelspace”);将glvertexAttribPointer函数的第一个参数值设置为vertexPosition_modelspaceID

1
2
3
4
5
6
7
attribute vec3 vertexPosition_modelspace;
//layout(location=*0) in vec3 vertexPosition_modelspace;
uniform mat4 MVP;
void main(){
//MVP变换顶点
gl_Position = MVP * vec4(vertexPosition_modelspace,1);
}

2.最基本的片段着色器

直接在着色器用vec4向量给内置变量gl_FragColor赋值

1
2
3
4
5
6
7
8
#version 120
//out vec3 color;
void main()
{
//color = vec3(1,0,0);
gl_FragColor = vec4(1,0,0, 1);
}

3.加入颜色数组

varying变量是顶点和片段之间做数据传递用的。一般顶点修改varying变量的值,然后片段使用该varying变量的值。因此varying变量在二者之间的声明必须是一致的。cpp中不能使用该变量。

顶点着色器声明添加如下

1
2
3
4
//in vec3 vertexColor;
//out vec3 fragmentColor;
varying vec3 fragmentColor;
attribute vec3 vertexColor;

顶点着色器主函数添加如下

1
fragmentColor = vertexColor;

片段着色器声明添加如下

1
2
3
//in vec3 fragmentColor;
//out vec3 color;
varying vec3 fragmentColor;

片段着色器主函数添加如下

1
2
//color = fragmentColor;
gl_FragColor = vec4(fragmentColor, 1);

如果要使用透明效果,片段着色器单独设置内置变量gl_FragColor的a值(或传值)

1
gl_FragColor.a = 0.3;

4.UV坐标纹理渲染

顶点着色器声明添加如下

1
2
3
//UV坐标
varying vec2 UV;
attribute vec2 vertexUV;

顶点着色器主函数添加如下

1
UV = vertexUV;

片段着色器声明添加如下

1
2
3
//采样器
uniform sampler2D myTextureSampler;
varying vec2 UV;

片段着色器主函数添加如下

1
2
//采样器根据UV坐标插值采样渲染图像
gl_FragColor = texture2D( myTextureSampler, UV );

6.2D文本

为了让代码在640*480和1080p分辨率下都能正常工作,x和y的范围分别设为[0-800]和[0-600]。顶点着色器将根据实际屏幕大小做对它做调整到[-1,1]之间。

1
2
vec2 vertexPosition_homoneneousspace = vertexPosition_screenspace - vec2(400,300); // [0..800][0..600] -> [-400..400][-300..300]
vertexPosition_homoneneousspace /= vec2(400,300);

7.法线贴图

顶点着色器

对比光照的顶点着色器,先不计算观察空间下的法线数组,替换成如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// model to camera = ModelView
vec3 vertexTangent_cameraspace = MV3x3 * vertexTangent_modelspace;
vec3 vertexBitangent_cameraspace = MV3x3 * vertexBitangent_modelspace;
vec3 vertexNormal_cameraspace = MV3x3 * vertexNormal_modelspace;

mat3 TBN = transpose(mat3(
vertexTangent_cameraspace,
vertexBitangent_cameraspace,
vertexNormal_cameraspace
)); // You can use dot products instead of building this matrix and transposing it. See References for details.

LightDirection_tangentspace = TBN * LightDirection_cameraspace;
EyeDirection_tangentspace = TBN * EyeDirection_cameraspace;

片段着色器

不直接使用观察空间下计算的法线数组,改成如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#version 130
uniform sampler2D DiffuseTextureSampler;
uniform sampler2D NormalTextureSampler;
uniform sampler2D SpecularTextureSampler;
uniform mat3 MV3x3;
void main(){

vec3 MaterialDiffuseColor = texture2D( DiffuseTextureSampler, UV ).rgb;
vec3 MaterialAmbientColor = vec3(0.1,0.1,0.1) * MaterialDiffuseColor;
vec3 MaterialSpecularColor = texture2D( SpecularTextureSampler, UV ).rgb * 0.3;

// Local normal, in tangent space. V tex coordinate is inverted because normal map is in TGA (not in DDS) for better quality
vec3 TextureNormal_tangentspace = normalize(texture2D( NormalTextureSampler, vec2(UV.x,-UV.y) ).rgb*2.0 - 1.0);

}

渲染管线&项目创建

一、渲染管线知识

1、OpenGL渲染过程介绍

渲染过程、定点输入、顶点着色器、编译着色器、片元着色器、着色器程序、链接顶点属性、顶点数组对象、索引缓冲对象:

https://blog.csdn.net/weixin_45782925/article/details/124822990

2、渲染管线流程

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

3、图元、片元,渲染架构

[https://zhuanlan.zhihu.com/p/549136586]

二、创建项目

1.在vs工程项目目录下创建空项目

QQ图片20221201153051

2.在vs源文件文件夹下创建.cpp文件

QQ图片20221201153056

3.在新建项目目录下导入着色器文件

QQ图片20221201153059

4.修改CMakeLists.txt文件,在相关位置添加如下

QQ图片20221201153113

QQ图片20221201153115

5.依次点击

QQ图片20221201153103

6.vs新建项目目录下自动导入相关文件

QQ图片20221201153106

7.新建项目目录下会生成exe可执行文件

QQ图片20221201153109

三、CMakeLists.txt

cmake指令详解:http://t.zoukankan.com/lidabo-p-7359217.html

变量解释:http://www.360doc.com/content/19/1204/17/6959565_877409038.shtml

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#设置cmake的最小版本
cmake_minimum_required (VERSION 2.6)

#设置项目名称
project (Tutorials)

#设置构架类型,用debug模式编译
set(CMAKE_BUILD_TYPE "Debug")

#mesa是Linux下的OpenGL实现,如下代码包含mesa
find_package(OpenGL REQUIRED)

#关于目录的报错提醒
if( CMAKE_BINARY_DIR STREQUAL CMAKE_SOURCE_DIR )
message( FATAL_ERROR "Please select another Build Directory ! (and give it a clever name, like bin_Visual2012_64bits/)" )
endif()
if( CMAKE_SOURCE_DIR MATCHES " " )
message( "Your Source Directory contains spaces. If you experience problems when compiling, this can be the cause." )
endif()
if( CMAKE_BINARY_DIR MATCHES " " )
message( "Your Build Directory contains spaces. If you experience problems when compiling, this can be the cause." )
endif()

#将子目录添加到构建,外部依赖项
add_subdirectory (external)

# 在Visual 2005及更高版本上,此模块可以设置调试工作目录
#管理cmake策略设置
cmake_policy(SET CMP0026 OLD)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/external/rpavlik-cmake-modules-fe2273")
include(CreateLaunchers)
include(MSVCMultipleProcessCompile) # /MP

#添加子目录
if(INCLUDE_DISTRIB)
add_subdirectory(distrib)
endif(INCLUDE_DISTRIB)

#向工程添加多个特定的头文件搜索路径
include_directories(
external/AntTweakBar-1.16/include/
external/glfw-3.1.2/include/GLFW/
external/glm-0.9.7.1/
external/glew-1.13.0/include/
external/assimp-3.0.1270/include/
external/bullet-2.81-rev2613/src/
.
)

#将要链接的库名统一叫ALL_LIBS
set(ALL_LIBS
${OPENGL_LIBRARY}
glfw
GLEW_1130
)

#宏定义
add_definitions(
-DTW_STATIC
-DTW_NO_LIB_PRAGMA
-DTW_NO_DIRECT3D
-DGLEW_STATIC
-D_CRT_SECURE_NO_WARNINGS
)

# Tutorial 1

#添加一个可执行文件目标
#第一行:可执行文件
#第二行之后:由source列出的源文件(cpp/hpp/着色器文件)构建而来(源文件路径)
add_executable(tutorial01_first_window
tutorial01_first_window/tutorial01.cpp
)

#设置目标要连接库文件的名称
target_link_libraries(tutorial01_first_window
${ALL_LIBS}
)

#设置目标属性
# set_target_properties(目标 PROPERTIES 属性1 值1 属性2 值2)
#设置XCODE属性配置生成目录
set_target_properties(tutorial01_first_window PROPERTIES XCODE_ATTRIBUTE_CONFIGURATION_BUILD_DIR "${CMAKE_CURRENT_SOURCE_DIR}/tutorial01_first_window/")
#设置工作目录
#create_default_target_launcher设置默认工作目录
create_target_launcher(tutorial01_first_window WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tutorial01_first_window/")

#划分目录结构
#将common目录下面的文件全部分组为common
SOURCE_GROUP(common REGULAR_EXPRESSION ".*/common/.*" )
SOURCE_GROUP(shaders REGULAR_EXPRESSION ".*/.*(shader|vert|frag)$" )

#CMake针对不同平台支持本地构建工具列表。同时支持命令行工具和集成开发环境(IDE)工具。
#如果生成器不为Xcode工具
if (NOT ${CMAKE_GENERATOR} MATCHES "Xcode" )
#命令执行
#TARGET 目标 POST_BUILD命令将会在目标构建完后执行
#复制文件 当前正在被处理的二进制目录路径/与构建配置相对应的输出子路径/目标 本平台上可执行文件后缀 正在被处理的源码目录路径
add_custom_command(
TARGET tutorial01_first_window POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/tutorial01_first_window${CMAKE_EXECUTABLE_SUFFIX}" "${CMAKE_CURRENT_SOURCE_DIR}/tutorial01_first_window/"
)

elseif (${CMAKE_GENERATOR} MATCHES "Xcode" )
endif (NOT ${CMAKE_GENERATOR} MATCHES "Xcode" )

opengl-phrtest-cpp示例学习

示例一

内容:利用glMatrixMode函数操作矩阵,增加透视效果

函数解释:https://blog.csdn.net/caoshangpa/article/details/80266028

示例二

内容:利用顶点数组的方式绘制立方体(无VBO、句柄和定义属性)

顶点数组&顶点缓冲:http://blog.chinaunix.net/uid-24448954-id-3059555.html

1、顶点数组:

(1)OpenGL 1.1所提供的功能

(2)明显的减少函数调用次数

(3)通常所有的状态保存在服务端的,如是否启用了纹理。而指定顶点,实际上就是把顶点数据从客户端发送到服务端。是否启用顶点数组,只是控制发送顶点数据的方式而已。服务端只管接收顶点数据,而不必管顶点数据到底是用哪种方式指定的。所以,服务端不需要知道顶点数组是否开启。

(4)为了表示服务端状态和客户端状态的区别,服务端的状态用glEnable/glDisable,客户端的状态则用glEnableClientState/glDisableClientState。

(5)数据是存放在内存(客户端),可以随时修改。每次绘制的时候,需要把所有的顶点数据从客户端(内存)发送到服务端(显示设备)。对于显示列表,数据是放在显示列表(服务器端),无法取出修改,但不会重复的发送数据。

(6)顶点数据和索引数据可以混合

可以不需要着色器

2、顶点缓冲区对象:

(1)OpenGL 1.5所提供的功能

(2)数据存放在服务端,同时允许客户端修改

(3)顶点数据和索引数据在两个缓冲区

(4)数不再从指针所指的位置取数据。函数会先把指针转化为整数,假设转化后结果为k,则会从当前缓冲区的第k个字节开始取数据。

使用缓冲,从缓冲读数据到现存而不是从内存中读取数据,所以运行速度会快很多。

测速代码:

1
2
3
4
5
6
7
8
9
10
11
double lastTime = glfwGetTime();
do {
//计算1秒内的帧数从而计算一帧需要的毫秒数
double current = glfwGetTime();
nbFrames++;
if (current - lastTime >= 1.0) {
printf("%f ms/frame\n", 1000.0 / double(nbFrames));
nbFrames = 0;
lastTime += 1.0;
}
}

顶点缓冲:

image-20221226085052532

显示列表顶点数组:

image-20221226085030449

示例三

内容:自己绘制纹理,使用纹理数组选择要渲染的对象,绘制时利用封装的函数

示例四

内容:利用显示列表进行颜色渲染

glFrontFace函数:[https://blog.csdn.net/zhongjling/article/details/7528091]

示例四-1

内容:利用索引和显示列表绘制立方体(定义面的法线,省略法线数组)

示例五、六、七

内容:噪声绘制动态纹理

示例八

内容:着色器定义随时间变化的内容

示例九

内容:读取两张纹理图片绘制,通过纹理数组和纹理混和绘制重叠的背景图。

示例十

内容:创建着色器程序,利用帧缓冲分层渲染纹理

示例十一

内容:绘制点

1.glPointParameterfv(GL_POINT_DISTANCE_ATTENUATION,pointpara);

2.glEnable(GL_POINT_SMOOTH);

函数解释:https://www.codetd.com/article/1755475

启动抗锯齿:https://blog.csdn.net/tiankefeng19850520/article/details/18601709

示例十二

内容:设置深度使图形有层次

示例十三

内容:循环绘制堆叠图形

示例十四

内容:用同一组顶点,通过定义视口画出不同位置的两个图形

示例十五

内容:用纹理属性而非纹理数组绘制背景

示例十六

内容:用uniform给着色器发送颜色数据,能够覆盖着色器定义的颜色

uniforms(统一数据):可以传递给顶点着色器和片元着色器,传递的数据是不易更改的比较统一的数据,比如发生旋转的变换矩阵,进行颜色渲染的渲染矩阵。一般用于对同一组顶点组成的单个3D物体中所有顶点都相同的变量,如当前光源的位置。

示例十七

内容:不设置光照,索引绘制球体

示例十七-1

内容:绘制球体,设置环境光和漫反射光

球体绘制原理:https://www.jb51.net/article/254487.htm

补充

glScissor( 0, 0, 800, 800):剪裁测试用于限制绘制区域。我们可以指定一个矩形的剪裁窗口,当启用剪裁测试后,只有在这个窗口之内的像素才能被绘制,其它像素则会被丢弃。换句话说,无论怎么绘制,剪裁窗口以外的像素将不会被修改。

opengl-tutorial-cpp示例学习

示例一

内容:绘制第一个窗口

1.glfwWindowHint(GLFW_SAMPLES, 4);函数

抗锯齿化(多重采样):https://www.jianshu.com/p/c6d664c26f8e

2.gult/freeglut/glew/glfw/glad的联系与区别

https://blog.csdn.net/shliang310/article/details/123993668

实例二

内容:利用顶点缓冲对象绘制三角形

示例三

内容:矩阵变换

模型、观察、投影、MVP矩阵;模型、世界、观察、投影矩阵https://www.cnblogs.com/CodeReaper/p/15133771.html#%E4%B8%89%E5%90%84%E4%B8%AA%E5%9D%90%E6%A0%87%E7%B3%BB%E7%BB%9F

示例四

内容:利用顶点缓冲对象绘制,利用颜色缓冲着色

深度测试:http://www.opengl-tutorial.org/cn/beginners-tutorials/tutorial-4-a-colored-cube/

实例五

内容:加载纹理,UV坐标贴图

UV坐标、采样器:https://blog.csdn.net/weixin_44478077/article/details/124061067

实例六

内容:读取键盘和鼠标操作,计算MVP矩阵

背面剔除:https://blog.csdn.net/wangdingqiaoit/article/details/52267314

示例七

内容:模型加载,读取obj文件获取相关数组绘制

obj文件及导入:https://blog.csdn.net/silangquan/article/details/9707347

obj文件示例解释:https://blog.csdn.net/qq_22822335/article/details/50669653

示例八

内容:加入顶点的法线属性,渲染光照效果

mv矩阵、法线的作用,光照计算原理:

http://www.opengl-tutorial.org/cn/beginners-tutorials/tutorial-8-basic-shading/

示例九

内容:通过索引缓冲实现顶点复用

https://blog.csdn.net/jiexuan357/article/details/7727648

着色器文件不需要修改,cpp文件重点代码如下(以正方形为例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static const GLfloat position[] = {
-0.5f,-0.5f,
0.5f,0.5f,
-0.5f,0.5f,
0.5f,-0.5f
};
static const GLuint indices[] = {//索引数组为int类型,索引下标从0开始
0,1,2,
1,3,0
}
//除顶点缓冲外添加索引缓冲
GLuint elementbuffer;
glGenBuffers(1, &elementbuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementbuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
//do循环中启动完顶点数组后
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, elementbuffer);
glDrawElements(
GL_TRIANGLES,
sizeof(indices),
GL_UNSIGNED_INT, //此处数据类型要与GLuint相同!
(void*)0
);

示例九-2

内容:同一个着色器绘制两个图(注意不需要重复操作的部分)

示例九-3

内容:另一种读取纹理索引的封装函数(效果同示例九)

示例十

内容:透明(关闭背面剔除,启用混合函数)

两个混合函数:https://blog.csdn.net/weixin_42050609/article/details/125215948

在封装成纹理贴图的时,需要丢弃片段,混合函数用于渲染半透明的纹理。

如果不采用丢弃片段:

image-20221225152021510

此时可以将四边形背景颜色的透明度设置为0,图案的透明度设置为1(或者反过来),在片段着色器添加如下代码:

1
if(color.a < 1.0)discard;
image-20221225152327554

但是会丢弃一部分绘制的图案,可能图案与背景衔接的部分也是<1.0的,所以需要调节参数,如果把参数改成0.5:

示例十一

内容:2D文本(初始化,打印显示,删除)

示例十二

内容:opengl扩展函数debug

示例十三

内容:法线贴图显示凹凸感

http://www.opengl-tutorial.org/cn/intermediate-tutorials/tutorial-13-normal-mapping/

示例十四

内容:利用帧缓冲渲染纹理(帧缓冲/颜色附件/深度附件/四边形)

先正常绘制有纹理的图案,再绘制渲染了新纹理的四边形

https://www.cnblogs.com/dudujerry/p/13573649.html

示例十五

内容:利用软件创建静态光影,正常渲染

示例十六

内容:阴影贴图(渲染两次,第一次从光源视角,第二次主视角)

算法原理:https://copyfuture.com/blogs-details/202211130534068916

https://zhuanlan.zhihu.com/p/150570547

示例十六-1

内容:简化版,便于理解

示例十七

内容:利用欧拉和四元组进行物体旋转 旋转矩阵和平移放缩计算出模型矩阵,再计算MVP矩阵

欧拉原理:https://blog.csdn.net/D_XingGuang/article/details/97148669

旋转的时候一定要注意正向旋转还是反向旋转,在绘制背景图的时候,由于纹理只会渲染在四面形的一面,所以如果旋转反了会显示不出纹理。

正向:

image-20221225153133707

反向:

image-20221225153021951

示例十八

内容:绘制公告牌,正面永远正对相机方向

已知公告板(定义在世界空间)中心位置,先计算相机世界空间向量,根据此向量进行调整,调整后进入VP矩阵

公告板通过几何着色器利用四个顶点画出三角形条带(四边形)

示例十八-1

内容:绘制粒子,利用实例化数组和复用一次绘制大量物体

请我喝杯咖啡吧~

支付宝
微信