Android OpenGL ES详解——几何着色器

OpenGL ES for Android(几何着色器) 简介 几何着色器(Geometry Shader)是一个可选功能,他介于在顶点和片段着色器之间,接收一组顶点数据,可以对数据进行处理,而且可以根据数据生成不止一个图形,假如你想绘制四个顶点,按照以前的方式,需要for循环四次,每次都顶点数据进行处理,最后传入顶点着色器中。而集合着色器就做了这样一件事。在移动平台上,几何着色器需要OpenGL ES 3.2版本(android 7之上),同样的我们可以先参考文档或其他相关资料学习3.0新特性。 3.0变化 这里简单学习一下3.0和2.0比较重要的变化 阅读详情

目录

一、概念

1、图元

2、几何着色器

1、输入类型

2、输出类型

3、输出顶点数量最大值限制

二、使用几何着色器

三、应用举例——造几个房子

四、应用举例——爆破物体

1、获取法向量

2、显示法线

五、应用举例——细分三角形

六、应用举例——广告牌技术


一、概念

1、图元

再正式介绍几何着色器(后续简称为GS)之前,我们先来搞清楚什么是图元。

在三维空间中,一个图元指的是构成一个3D实体的顶点集合。例如空间中的一个点对应一个顶点,一条线段对应两个顶点,一个三角形对应三个顶点,这些都是图元。

通常情况下,图元为多边形对应的顶点,而最简单的多边形即为三角形。DirectX使用三角形来构造其它的多边形,例如两个三角形可以构成一个四边形。这是因为三角形的三个顶点一定是共面的,可以提高渲染性能(渲染非共面的顶点效率很低)。通过这种组合三角形的方式可以获得更大更复杂的多边形和网格,如下图为立方体图元与球体图元:

除了使用三角形以外,我们也可以使用一系列的点图元或者线段图元来组成一些复杂的图元。DirectX支持的基本图元类型(Primitive Type)有下面五种:

  • Point List
  • Line List
  • Line Strip
  • Triangle List
  • Triangle Strip

假如在渲染管线的Input Assembler阶段拿到如下6个顶点(数字代表对应顶点的顺序),我们来看看不同的图元类型对应的绘制结果。

如果图元类型为Point List,则每个顶点都被单独的作为一个点被绘制出来(效果图和上图一样),可以用它来模拟天空中的星星或者绘制由点构成的虚线等。

如果图元类型为Line List或者Line Strip,则每两个顶点构成一个线段图元。它们的区别在于,如果是Line List,这些线段都是独立的,而Line Strip的线段都是互相连接的(如下图)。Line List可以用来实现场景中雨水的效果,而Line Strip可以用来绘制非闭合的多边形。

如果图元类型为Triangle List或者Triangle Strip,则每三个顶点构成一个三角形图元。与Line类型的一样,Triangle List的三角形都是独立的,而Triangle Strip的三角形都是互相连接的(如下图)。使用Triangle List可以创建由不相交的片段构成的物体(含有缝隙),此外还常被用于创建带有锐利边缘并使用高洛德着色(Gouraud shading)的图元。3D场景中的大多数物体图元都是由Triangle Strip组成的,这是因为用Triangle Strip构成的复杂的对象,可以有效地节省内存和减少处理时间。

注:Triangle Strip中,第奇数个三角形和第偶数个三角形,如果按顶点顺序绘制的话,会导致三角形的绘制顺序相反。例如第一个三角形的顶点顺序是012,三角形的绘制顺序是顺时针,在DirectX中显示正面。第二个三角形的顶点顺序是123,那么三角形的绘制顺序是逆时针,显示背面。为了保证三角形的绘制顺序统一,GPU内部会将所有偶数位三角形的后两个顶点顺序对调,例如第二个三角形的顶点顺序变为132,从而变成顺时针。

使用Triangle Strip也可以模拟Triangle List效果,需要利用到面积为0的三角形(被称为:degenerate triangle)。例如上面的例子中,我们在第三个和第四个顶点之间新增两个顶点2', 3',其坐标分别和2, 3相同,构成顺序为0, 1, 2, 2', 3', 3, 4, 5八个顶点组成的Triangle Strip,其绘制结果如下:

其中△12'2,△22'3',△2'33',△3'34都是面积为0的三角形变成了一条线段(图中红色虚线),但是该线段不会被绘制出来。因此上述Triangle Strip只能绘制出△012和△354两个独立的三角形。

从Direct3D 10开始,除了Point List外的图元类型都新增了一个带有邻接关系(Adjacency)的版本:

  • Line List Adjacency
  • Line Strip Adjacency
  • Triangle List Adjacency
  • Triangle Strip Adjacency

带有邻接关系的图元额外包含了一些环绕在图元周边的顶点,其目的是为了提供更多的几何信息,并且只能通过几何着色器可见,常被用于silhouette detection,shadow volume extrusion等。

每个带有Adjacency的Line都需要额外2个顶点作邻接关系,因此每个带有Adjacency的Line都对应4个顶点。Line List Adjacency与Line Strip Adjacency示意图如下(新增两个顶点,否则List中无法有两个完整的Line Adjacency图元):

注:虚线部分为与邻接点所构成的图元,它们不会被实际渲染出来。在Strip中,中间的线段同样有邻接点,例如线段3,4的邻接点为2,5。

每个带有Adjacency的Triangle都需要额外3个顶点作邻接关系,因此每个带有Adjacency的Triangle都对应6个顶点。Triangle List Adjacency与Triangle Strip Adjacency示意图如下(为了好显示,顶点重新搞了下):

从顶点顺序中可以发现,不管是List还是Strip,下标为偶数的顶点都是非邻接三角形的顶点,而下标为奇数的顶点都是邻接点。

总结:对于point,line,triangle,line adjacency,triangle adjacency我们可以称之为基础图元,然后利用list或strip的组成方式可以将这些基础图元组成更复杂的图元。这些复杂图元根据其基础图元和组成方式对应不同的图元类型,例如Point List,Line Strip,Triangle List等等。

2、几何着色器

在顶点和片段着色器之间有一个可选的着色器,叫做几何着色器(Geometry Shader)。几何着色器以一个或多个表示为一个单独基本图形(primitive)的顶点作为输入,比如可以是一个点或者三角形。几何着色器在将这些顶点发送到下一个着色阶段之前,可以将这些顶点转变为它认为合适的内容。几何着色器有意思的地方在于它可以把(一个或多个)顶点转变为完全不同的基本图形(primitive),从而生成比原来多得多的顶点。

从OpenGL 3.2开始支持几何着色器阶段,并在OpenGL 4.0引入了一些新的特性。

与顶点着色器输入的都是单个顶点不同,几何着色器的输入为单个基础图元。对于strip类型,同样会被当作是一个个单独的基础图元完整的输入到GS中,因此GS不需要去区分图元类型是line还是strip。例如四个顶点的triangle strip构成两个三角形,会调用两次GS,第一次按顺序传入下标为0,1,2的三个顶点,第二次按顺序传入下标为1,3,2的三个顶点(strip里第偶数位三角形的后两个顶点会自动对调)。这种情况会产生额外的性能消耗,因为一些图元共享的顶点会在GS中被多次处理。

在顶点着色器中,不能新增或者删除顶点,但是在几何着色器中,我们可以新增和删除顶点来改变图元(输入与输出的图元可以不一样),从而实现一些特殊效果。GS的输出只可以是point list,line strip和triangle strip,但是我们可以通过一些方法来模拟line list以及triangle list,后续介绍。

比如GS的输入为line,我们可以通过新增顶点将其扩展成triangle strip进行输出。还可以把line中的某一个顶点变成point进行输出(另个顶点被舍弃),甚至根据一些条件都不输出(两个顶点都被舍弃),从而达到删除顶点的效果。

在只有顶点着色器和像素着色器的渲染管线中,VS输出的顶点信息要在齐次裁剪空间下,后续执行光栅化阶段(包括视椎体剔除,透视除法,视口变换到屏幕空间,图元映射到像素等操作),最后进入到PS阶段。而如果我们启用了几何着色器,那么VS输出的顶点信息会先进入到GS中。由于我们往往在模型空间下编写GS逻辑,因此此时VS的输出不需要在齐次裁剪空间下,变成了GS输出的顶点信息需要在齐次裁剪空间下,然后GS输出的顶点信息执行光栅化操作,最后进入到PS阶段。

几何着色器在实现一些特殊效果的时候,例如广告牌技术,动态粒子系统,毛发系统,Shadow Volume等,可以提供很大的性能提升,此外还常被用于实现可视化法线,物体爆破等效果。下图为在Unity中利用几何着色器实现的广告牌效果:

由于GS可以新增顶点以及改变图元的特性,使得我们可以将一些操作放到GS中从而提升性能。拿广告牌的功能举例,要使得广告牌始终对着Camera,一种做法就是在CPU端每帧更新广告牌四个顶点的位置,然后将这些顶点信息传递给GPU渲染。由于每帧要更新顶点信息,所以这些顶点只能存放在upload heap中,并且使用memcpy来将更新后的顶点信息拷贝到GPU Buffer中。当广告牌多时(例如用其渲染大量的植被)会造成不小的性能开销,以及占用大量内存。而如果我们利用GS,我们只需要在一开始将这些广告牌的中心点传递给GPU,因为这些中心点位置是不会随着Camera变的,因此可以放在default heap中从而提升性能,并且由于此时一个广告牌只对应一个顶点信息,也减少了内存的占用。然后每帧在GS中去新增广告牌的四个顶点并且根据Camera计算对应位置。在广告牌很多的情况下,在GPU中做顶点信息的计算同样可以提升性能(具体实现后面介绍)。

当然了几何着色器也有对应的缺点,当几何着色器输出的顶点信息过多时,其性能会变得很差,后续会介绍。

我们直接用一个例子深入了解一下:

#version 330 core
layout (points) in;
layout (line_strip, max_vertices = 2) out;

void main() {
    gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0);
    EmitVertex();

    gl_Position = gl_in[0].gl_Position + vec4(0.1, 0.0, 0.0, 0.0);
    EmitVertex();

    EndPrimitive();
}

1、输入类型

每个几何着色器开始位置我们需要声明输入的基本图形(primitive)类型,这个输入是我们从顶点着色器中接收到的。我们在in关键字前面声明一个layout标识符。这个输入layout修饰符可以从一个顶点着色器接收以下基本图形值:

基本图形描述
points绘制GL_POINTS基本图形的时候(1)
lines当绘制GL_LINES或GL_LINE_STRIP(2)时
lines_adjacencyGL_LINES_ADJACENCY或GL_LINE_STRIP_ADJACENCY(4)
trianglesGL_TRIANGLES, GL_TRIANGLE_STRIP或GL_TRIANGLE_FAN(3)
triangles_adjacencyGL_TRIANGLES_ADJACENCY或GL_TRIANGLE_STRIP_ADJACENCY(6)

这是我们能够给渲染函数的几乎所有的基本图形。如果我们选择以GL_TRIANGLES绘制顶点,我们要把输入修饰符设置为triangles。括号里的数字代表一个基本图形所能包含的最少的顶点数。

不同基本图形下,数组中顶点的下标对应关系如下图:

如下图的Triangle Strip Adjacency,第一次会依次给GS输入0, 1, 2, 6, 4, 3这6个顶点,第二次会依次输入2, 5, 6, 8, 4, 0这6个顶点,以此类推。

2、输出类型

当我们需要指定一个几何着色器所输出的基本图形类型时,我们就在out关键字前面加一个layout修饰符。和输入layout标识符一样,输出的layout标识符也可以接受以下基本图形值:

  • points
  • line_strip
  • triangle_strip

使用这3个输出修饰符我们可以从输入的基本图形创建任何我们想要的形状。为了生成一个三角形,我们定义一个triangle_strip作为输出,然后输出3个顶点。

3、输出顶点数量最大值限制

在GS开头,我们需要使用max_vertices 关键字来声明该GS单次调用可以输出的最大顶点数量(VertexNum)。每次调用时,输出的顶点数量是可以变化的,但是不能超出设定的最大数目。例如:[max_vertices = 4]代表每次调用可以输出0到4个顶点,但是不能多于4个顶点。

max_vertices 值会直接决定几何着色器的运行速度,出于性能的考虑,maxvertexcount的值应该尽可能的小,GS的性能和其输出的buffer大小成反比。

几何着色器同时希望我们设置一个它能输出的顶点数量的最大值(如果你超出了这个数值,OpenGL就会忽略剩下的顶点),我们可以在out关键字的layout标识符上做这件事。在这个特殊的情况中,我们将使用最大值为2个顶点,来输出一个line_strip。

这种情况,你会奇怪什么是线条:一个线条是把多个点链接起来表示出一个连续的线,它最少有两个点来组成。每个后一个点在前一个新渲染的点后面渲染,你可以看看下面的图,其中包含5个顶点:

上面的着色器,我们只能输出一个线段,因为顶点的最大值设置为2。

为生成更有意义的结果,我们需要某种方式从前一个着色阶段获得输出。GLSL为我们提供了一个内建变量,它叫做gl_in,它的内部看起来可能像这样:

in gl_Vertex
{
    vec4 gl_Position;
    float gl_PointSize;
    float gl_ClipDistance[];
} gl_in[];

这里它被声明为一个接口块(interface block,前面的教程已经讨论过,详见:Android OpenGL ES详解——高级GLSL-优快云博客),它包含几个有意思的变量,其中最有意思的是gl_Position,它包含着和我们设置的顶点着色器的输出相似的向量。

要注意的是,它被声明为一个数组,因为大多数渲染基本图形由一个以上顶点组成,几何着色器接收一个基本图形的所有顶点作为它的输入。几何着色器的输入参数为一个顶点数组。

使用从前一个顶点着色阶段的顶点数据,我们就可以开始生成新的数据了,这是通过2个几何着色器函数EmitVertex和EndPrimitive来完成的。几何着色器需要你去生成/输出至少一个你定义为输出的基本图形。在我们的例子里我们打算至少生成一个线条(line strip)基本图形。

void main() {
    gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0);
    EmitVertex();

    gl_Position = gl_in[0].gl_Position + vec4(0.1, 0.0, 0.0, 0.0);
    EmitVertex();

    EndPrimitive();
}

每次我们调用EmitVertex,当前设置到gl_Position的向量就会被添加到基本图形上。无论何时调用EndPrimitive,所有为这个基本图形发射出去的顶点都将结合为一个特定的输出渲染基本图形。一个或多个EmitVertex函数调用后,重复调用EndPrimitive就能生成多个基本图形。这个特殊的例子里,发射了两个顶点,它们被从顶点原来的位置平移了一段距离,然后调用EndPrimitive将这两个顶点结合为一个单独的有两个顶点的线条。

现在你了解了几何着色器的工作方式,你就可能猜出这个几何着色器做了什么。这个几何着色器接收一个基本图形——点,作为它的输入,使用输入点作为它的中心,创建了一个水平线基本图形。如果我们渲染它,结果就会像这样:

并不是非常引人注目,但是考虑到它的输出是使用下面的渲染命令生成的就很有意思了:

glDrawArrays(GL_POINTS, 0, 4);

这是个相对简单的例子,它向你展示了我们如何使用几何着色器来动态地在运行时生成新的形状。本章的后面,我们会讨论一些可以使用几何着色器获得有趣的效果,但是现在我们将以创建一个简单的几何着色器开始。

二、使用几何着色器

为了展示几何着色器的使用,我们将渲染一个简单的场景,在场景中我们只绘制4个点,这4个点在标准化设备坐标的z平面上。这些点的坐标是:

GLfloat points[] = {
 -0.5f,  0.5f, // 左上方
 0.5f,  0.5f,  // 右上方
 0.5f, -0.5f,  // 右下方
 -0.5f, -0.5f  // 左下方
};

顶点着色器只在z平面绘制点,所以我们只需要一个基本顶点着色器:

#version 330 core
layout (location = 0) in vec2 position;

void main()
{
    gl_Position = vec4(position.x, position.y, 0.0f, 1.0f);
}

我们会简单地为所有点输出绿色,我们直接在片段着色器里进行硬编码:

#version 330 core
out vec4 color;

void main()
{
    color = vec4(0.0f, 1.0f, 0.0f, 1.0f);
}

为点的顶点生成一个VAO和VBO,然后使用glDrawArrays进行绘制:

shader.Use();
glBindVertexArray(VAO);
glDrawArrays(GL_POINTS, 0, 4);
glBindVertexArray(0);

效果是黑色场景中有四个绿点(虽然很难看到):

但我们不是已经学到了所有内容了吗?对,现在我们将通过为场景添加一个几何着色器来为这个小场景增加点活力。

出于学习的目的我们将创建一个叫pass-through的几何着色器,它用一个point基本图形作为它的输入,并把它无修改地传(pass)到下一个着色器。

#version 330 core
layout (points) in;
layout (points, max_vertices = 1) out;

void main() {
    gl_Position = gl_in[0].gl_Position;
    EmitVertex();
    EndPrimitive();
}

现在这个几何着色器应该很容易理解了。它简单地将它接收到的输入的无修改的顶点位置发射出去,然后生成一个point基本图形。

一个几何着色器需要像顶点和片段着色器一样被编译和链接,但是这次我们将使用GL_GEOMETRY_SHADER作为着色器的类型来创建这个着色器:

geometryShader = glCreateShader(GL_GEOMETRY_SHADER);
glShaderSource(geometryShader, 1, &gShaderCode, NULL);
glCompileShader(geometryShader);  
...
glAttachShader(program, geometryShader);
glLinkProgram(program);

编译着色器的代码和顶点、片段着色器的基本一样。要记得检查编译和链接错误!

如果你现在编译和运行,就会看到和下面相似的结果:

它和没用几何着色器一样!我承认有点无聊,但是事实上,我们仍能绘制证明几何着色器工作了的点,所以现在是时候来做点更有意思的事了!

三、应用举例——造几个房子

绘制点和线没什么意思,所以我们将在每个点上使用几何着色器绘制一个房子。我们可以通过把几何着色器的输出设置为triangle_strip来达到这个目的,总共要绘制3个三角形:两个用来组成方形和另表示一个屋顶。

在OpenGL中三角形带(triangle strip)绘制起来更高效,因为它所使用的顶点更少。第一个三角形绘制完以后,每个后续的顶点会生成一个毗连前一个三角形的新三角形:每3个毗连的顶点都能构成一个三角形。如果我们有6个顶点,它们以三角形带的方式组合起来,那么我们会得到这些三角形:(1, 2, 3)、(2, 3, 4)、(3, 4, 5)、(4,5,6)因此总共可以表示出4个三角形。一个三角形带至少要用3个顶点才行,它能生曾N-2个三角形;6个顶点我们就能创建6-2=4个三角形。下面的图片表达了这点:

使用一个三角形带作为一个几何着色器的输出,我们可以轻松创建房子的形状,只要以正确的顺序来生成3个毗连的三角形。下面的图像显示,我们需要以何种顺序来绘制点,才能获得我们需要的三角形,图上的蓝点代表输入点:

上图的内容转变为几何着色器:

#version 330 core
layout (points) in;
layout (triangle_strip, max_vertices = 5) out;

void build_house(vec4 position)
{
    gl_Position = position + vec4(-0.2f, -0.2f, 0.0f, 0.0f);// 1:左下角
    EmitVertex();
    gl_Position = position + vec4( 0.2f, -0.2f, 0.0f, 0.0f);// 2:右下角
    EmitVertex();
    gl_Position = position + vec4(-0.2f,  0.2f, 0.0f, 0.0f);// 3:左上
    EmitVertex();
    gl_Position = position + vec4( 0.2f,  0.2f, 0.0f, 0.0f);// 4:右上
    EmitVertex();
    gl_Position = position + vec4( 0.0f,  0.4f, 0.0f, 0.0f);// 5:屋顶
    EmitVertex();
    EndPrimitive();
}

void main()
{
    build_house(gl_in[0].gl_Position);
}

这个几何着色器生成5个顶点,每个顶点是点(point)的位置加上一个偏移量,来组成一个大三角形带。接着最后的基本图形被像素化,片段着色器处理整三角形带,结果是为我们绘制的每个点生成一个绿房子:

可以看到,每个房子实则是由3个三角形组成,都是仅仅使用空间中一点来绘制的。绿房子看起来还是不够漂亮,所以我们再给每个房子加一个不同的颜色。我们将在顶点着色器中为每个顶点增加一个额外的代表颜色信息的顶点属性。

下面是更新了的顶点数据:

GLfloat points[] = {
    -0.5f,  0.5f, 1.0f, 0.0f, 0.0f, // 左上
     0.5f,  0.5f, 0.0f, 1.0f, 0.0f, // 右上
     0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // 右下
    -0.5f, -0.5f, 1.0f, 1.0f, 0.0f  // 左下
};

然后我们更新顶点着色器,使用一个接口块来项几何着色器发送颜色属性:

#version 330 core
layout (location = 0) in vec2 position;
layout (location = 1) in vec3 color;

out VS_OUT {
    vec3 color;
} vs_out;

void main()
{
    gl_Position = vec4(position.x, position.y, 0.0f, 1.0f);
    vs_out.color = color;
}

接着我们还需要在几何着色器中声明同样的接口块(使用一个不同的接口名):

in VS_OUT {
    vec3 color;
} gs_in[];

因为几何着色器把多个顶点作为它的输入,从顶点着色器来的输入数据总是被以数组的形式表示出来,即使现在我们只有一个顶点。

我们不是必须使用接口块来把数据发送到几何着色器中。我们还可以这么写:

in vec3 vColor[];

如果顶点着色器发送的颜色向量是out vec3 vColor那么接口块就会在比如几何着色器这样的着色器中更轻松地完成工作。事实上,几何着色器的输入可以非常大,把它们组成一个大的接口块数组会更有意义。

然后我们还要为下一个像素着色阶段声明一个输出颜色向量:

out vec3 fColor;

因为片段着色器只需要一个(已进行了插值的)颜色,传送多个颜色没有意义。fColor向量这样就不是一个数组,而是一个单一的向量。当发射一个顶点时,为了它的片段着色器运行,每个顶点都会储存最后在fColor中储存的值。对于这些房子来说,我们可以在第一个顶点被发射,对整个房子上色前,只使用来自顶点着色器的颜色填充fColor一次:

fColor = gs_in[0].color; //只有一个输出颜色,所以直接设置为gs_in[0]
gl_Position = position + vec4(-0.2f, -0.2f, 0.0f, 0.0f);    // 1:左下
EmitVertex();
gl_Position = position + vec4( 0.2f, -0.2f, 0.0f, 0.0f);    // 2:右下
EmitVertex();
gl_Position = position + vec4(-0.2f,  0.2f, 0.0f, 0.0f);    // 3:左上
EmitVertex();
gl_Position = position + vec4( 0.2f,  0.2f, 0.0f, 0.0f);    // 4:右上
EmitVertex();
gl_Position = position + vec4( 0.0f,  0.4f, 0.0f, 0.0f);    // 5:屋顶
EmitVertex();
EndPrimitive();

所有发射出去的顶点都把最后储存在fColor中的值嵌入到他们的数据中,和我们在他们的属性中定义的顶点颜色相同。所有的分房子便都有了自己的颜色:

为了好玩儿,我们还可以假装这是在冬天,给最后一个顶点一个自己的白色,就像在屋顶上落了一些雪。

fColor = gs_in[0].color;
gl_Position = position + vec4(-0.2f, -0.2f, 0.0f, 0.0f); 
EmitVertex();
gl_Position = position + vec4( 0.2f, -0.2f, 0.0f, 0.0f);
EmitVertex();
gl_Position = position + vec4(-0.2f,  0.2f, 0.0f, 0.0f); 
EmitVertex();
gl_Position = position + vec4( 0.2f,  0.2f, 0.0f, 0.0f);  
EmitVertex();
gl_Position = position + vec4( 0.0f,  0.4f, 0.0f, 0.0f); 
fColor = vec3(1.0f, 1.0f, 1.0f);
EmitVertex();
EndPrimitive();

结果就像这样:

你可以看到,使用几何着色器,你可以使用最简单的基本图形就能获得漂亮的新玩意。因为这些形状是在你的GPU超快硬件上动态生成的,这要比使用顶点缓冲自己定义这些形状更为高效。几何缓冲在简单的经常被重复的形状比如体素(voxel)的世界和室外的草地上,是一种非常强大的优化工具。

四、应用举例——爆破物体

绘制房子的确很有趣,但我们不会经常这么用。这就是为什么现在我们将撬起物体缺口,形成爆炸式物体的原因!虽然这个我们也不会经常用到,但是它能向你展示一些几何着色器的强大之处。

当我们说对一个物体进行爆破(Explode)的时候并不是说我们将要把之前的那堆顶点炸掉,但是我们打算把每个三角形沿着它们的法线向量移动一小段距离。效果是整个物体上的三角形看起来就像沿着它们的法线向量爆炸了一样。纳米服上的三角形的爆炸式效果看起来是这样的:

这样一个几何着色器效果的一大好处是,它可以用到任何物体上,无论它们多复杂。

1、获取法向量

因为我们打算沿着三角形的法线向量移动三角形的每个顶点,我们需要先计算它的法线向量。我们要做的是计算出一个向量,它垂直于三角形的表面,使用这三个我们已经的到的顶点就能做到。你可能记得变换教程中,我们可以使用叉乘获取一个垂直于两个其他向量的向量。如果我们有两个向量a和b,它们平行于三角形的表面,我们就可以对这两个向量进行叉乘得到法线向量了。下面的几何着色器函数做的正是这件事,它使用3个输入顶点坐标获取法线向量:

vec3 GetNormal()
{
   vec3 a = vec3(gl_in[0].gl_Position) - vec3(gl_in[1].gl_Position);
   vec3 b = vec3(gl_in[2].gl_Position) - vec3(gl_in[1].gl_Position);
   return normalize(cross(a, b));
}

这里我们使用减法获取了两个向量a和b,它们平行于三角形的表面。两个向量相减得到一个两个向量的差值,由于所有3个点都在三角形平面上,任何向量相减都会得到一个平行于平面的向量。一定要注意,如果我们调换了a和b的叉乘顺序,我们得到的法线向量就会使反的,顺序很重要!

知道了如何计算法线向量,我们就能创建一个explode函数,函数返回的是一个新向量,它把位置向量沿着法线向量方向平移:

vec4 explode(vec4 position, vec3 normal)
{
    float magnitude = 2.0f;
    vec3 direction = normal * ((sin(time) + 1.0f) / 2.0f) * magnitude;
    return position + vec4(direction, 0.0f);
}

函数本身并不复杂,sin(正弦)函数把一个time变量作为它的参数,它根据时间来返回一个-1.0到1.0之间的值。因为我们不想让物体坍缩,所以我们把sin返回的值做成0到1的范围。最后的值去乘以法线向量,direction向量被添加到位置向量上。

爆炸效果的完整的几何着色器是这样的,它使用我们的模型加载器,绘制出一个模型:

#version 330 core
layout (triangles) in;
layout (triangle_strip, max_vertices = 3) out;

in VS_OUT {
    vec2 texCoords;
} gs_in[];

out vec2 TexCoords;

uniform float time;

vec4 explode(vec4 position, vec3 normal) { ... }

vec3 GetNormal() { ... }

void main() {
    vec3 normal = GetNormal();

    gl_Position = explode(gl_in[0].gl_Position, normal);
    TexCoords = gs_in[0].texCoords;
    EmitVertex();
    gl_Position = explode(gl_in[1].gl_Position, normal);
    TexCoords = gs_in[1].texCoords;
    EmitVertex();
    gl_Position = explode(gl_in[2].gl_Position, normal);
    TexCoords = gs_in[2].texCoords;
    EmitVertex();
    EndPrimitive();
}

注意我们同样在发射一个顶点前输出了合适的纹理坐标。

也不要忘记在OpenGL代码中设置time变量:

glUniform1f(glGetUniformLocation(shader.Program, "time"), glfwGetTime());
 ```

最后的结果是一个随着时间持续不断地爆炸的3D模型(不断爆炸不断回到正常状态)。尽管没什么大用处,它却向你展示出很多几何着色器的高级用法。你可以用[完整的源码](http://learnopengl.com/code_viewer.php?code=advanced/geometry_shader_explode)和[着色器](http://learnopengl.com/code_viewer.php?code=advanced/geometry_shader_explode_shaders)对比一下你自己的。

# 显示法向量

在这部分我们将使用几何着色器写一个例子,非常有用:显示一个法线向量。当编写光照着色器的时候,你最终会遇到奇怪的视频输出问题,你很难决定是什么导致了这个问题。通常导致光照错误的是,不正确的加载顶点数据,以及给它们指定了不合理的顶点属性,又或是在着色器中不合理的管理,导致产生了不正确的法线向量。我们所希望的是有某种方式可以检测出法线向量是否正确。把法线向量显示出来正是这样一种方法,恰好几何着色器能够完美地达成这个目的。

思路是这样的:我们先不用几何着色器,正常绘制场景,然后我们再次绘制一遍场景,但这次只显示我们通过几何着色器生成的法线向量。几何着色器把一个三角形基本图形作为输入类型,用它们生成3条和法线向量同向的线段,每个顶点一条。伪代码应该是这样的:

```c++
shader.Use();
DrawScene();
normalDisplayShader.Use();
DrawScene();

这次我们会创建一个使用模型提供的顶点法线,而不是自己去生成。为了适应缩放和旋转我们会在把它变换到裁切空间坐标前,使用法线矩阵来生成法线(几何着色器用他的位置向量做为裁切空间坐标,所以我们还要把法线向量变换到同一个空间)。这些都能在顶点着色器中完成:

#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 normal;

out VS_OUT {
    vec3 normal;
} vs_out;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

void main()
{
    gl_Position = projection * view * model * vec4(position, 1.0f);
    mat3 normalMatrix = mat3(transpose(inverse(view * model)));
    vs_out.normal = normalize(vec3(projection * vec4(normalMatrix * normal, 1.0)));
}

经过变换的裁切空间法线向量接着通过一个接口块被传递到下个着色阶段。几何着色器接收每个顶点(带有位置和法线向量),从每个位置向量绘制出一个法线向量:

#version 330 core
layout (triangles) in;
layout (line_strip, max_vertices = 6) out;

in VS_OUT {
    vec3 normal;
} gs_in[];

const float MAGNITUDE = 0.4f;

void GenerateLine(int index)
{
    gl_Position = gl_in[index].gl_Position;
    EmitVertex();
    gl_Position = gl_in[index].gl_Position + vec4(gs_in[index].normal, 0.0f) * MAGNITUDE;
    EmitVertex();
    EndPrimitive();
}

void main()
{
    GenerateLine(0); // First vertex normal
    GenerateLine(1); // Second vertex normal
    GenerateLine(2); // Third vertex normal
}

到现在为止,像这样的几何着色器的内容就不言自明了。需要注意的是我们我们把法线向量乘以一个MAGNITUDE向量来限制显示出的法线向量的大小(否则它们就太大了)。

2、显示法线

由于把法线显示出来通常用于调试的目的,我们可以在片段着色器的帮助下把它们显示为单色的线(如果你愿意也可以更炫一点)。

#version 330 core
out vec4 color;

void main()
{
    color = vec4(1.0f, 1.0f, 0.0f, 1.0f);
}

现在先使用普通着色器来渲染你的模型,然后使用特制的法线可视着色器,你会看到这样的效果:

除了我们的纳米服现在看起来有点像一个带着隔热手套的全身多毛的家伙外,它给了我们一种非常有效的检查一个模型的法线向量是否有错误的方式。你可以想象下这样的几何着色器也经常能被用在给物体添加毛发上。

五、应用举例——细分三角形

利用几何着色器,我们可以将triangle list或strip中的每个三角形通过如下图添加三个顶点的方式细分成四个小三角形。

需要注意的是细分后的图元没法利用一个含有6个顶点的triangle strip来构成,需要拆分成两个triangle strip:v0,m0,m2,m1,v2和m0,v1,m1,一共8个顶点(其中m0和m1重复一次)。

六、应用举例——广告牌技术

广告牌技术简单来说就是通过渲染一张一直朝向Camera的模型图片(渲染一张图片只需要4个顶点组成的方块)来模拟3D模型(模型具有较多的顶点)来达到以假乱真的目的,常用于植被的渲染以及一些特效上(例如火焰)。

要使得广告牌一直对着Camera,我们就要一直更新广告牌的四个顶点信息,简单的几何关系如下图:

广告牌四个顶点和几个向量的关系图:

设世界坐标下,Camera坐标为(x, y, z),广告牌中心点坐标为center = (i, j, k),图中三个向量的值为:

LookAt = normalize( (x, 0, z) - (i, 0, k) )
up = (0, 1, 0)
right = cross(up, LookAt)

再假设广告牌的宽高分别为w和h,那么我们就可以计算出四个顶点在世界坐标下的位置:

v[0] = center + w / 2 * right - h / 2 * up;
v[1] = center + w / 2 * right + h / 2 * up;
v[2] = center - w / 2 * right - h / 2 * up;
v[3] = center - w / 2 * right + h / 2 * up;

若不使用几何着色器,广告牌四个顶点的坐标更新的操作应该在CPU端每帧进行,这就导致每个广告牌要在CPU端存储4个顶点信息,然后每帧还要将它们传递给GPU(因此顶点信息只能存放在upload heap中),对性能和内存都不友好。聪明一点的做法是CPU端顶点位置保持不变,在顶点着色器中去计算顶点的位置信息,这样可以把顶点信息存放在default heap中提升GPU的读取速度,但是依旧要存放4n个顶点。

但是如果使用几何着色器,我们就可以在CPU端只存储广告牌的中心点(n个顶点,且可存放在default heap中),然后利用GS将其扩展为四个顶点方块并计算对应的顶点信息即可(如下图):

利用GS将point变为triangle strip

参考文章

几何着色器 - LearnOpenGL-CN

OpenGL学习笔记一之高级OpenGL篇九 几何着色器_opengl adjacency-优快云博客

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

Android OpenGl ES使用原理总结与代码示例 一、相关概念简介: OpenGl : OpenGl是一个定义好的跨平台图形处理接口库,通过它可操作GPU来完成图像处理。它跨平台是因为各个硬件厂家都按照这套接口规范具体实现了对应功能,供上层调用。 OpenGl ES: 手机由于性能相对较弱,难以支持OpenGl的全部功能,所以Android中使用的是OpenGl的子集OpenGl ES。 EGL: OpenGl是一套跨平台的接口,它与各个平台本地窗口系统之间的交互,是借助于一个中间控制层,这个中间控制层... 阅读详情

相关推荐

OpenGL学习笔记(十)-几何着色器-实例化

参考网址:LearnOpenGL 中文版 4.7 几何着色器 4.7.1 基本概念 1、顶点和片段着色器之间有一个可选的几何着色器,几何着色器的输入是一个图元(如点或三角形)的一组顶点,顶点发送到下一着色器之前可对它们随意变换,将顶点变换为完全不同的图元,并且还能生成比原来更多的顶点。 #version 330 core layout (points) in; layout (line_strip, max_vertices = 2) out; void main() { gl_Posit

Catog的博客 1754

Android OpenGL ES 学习(一) -- 基本概念

最近在研究 SurfaceView 和 TextureView,发现栅格化这东西,一直没怎么搞明白,一搜都是关于 OpenGL 的,没办法,当初也是要学习这个,现在重试拾起。之前接触 OpenGL ,是主要解决一个渲染模糊问题,使用 OpenGL 就不会,而使用 SurfaceView 则不会,但存在延时问题,一直没扣明白,这个系列,期待重新认识 OpenGL。Android 图形学这东西,真的好多东西啊 - -。

夏至的稻穗的博客 7055

OpenGL ES入门 基本概念篇

前言 OpenGL是一种包含了一系列可以操作图形图像函数的图形编程接口的规范标准,它规范了每个函数该如何执行以及它们的输出值,OpenGL ES是OpenGL的一个精简子集,主要用于手持和嵌入式设备,OpenGL ES很大程度上兼容了OpenGL,主要是删除了一些OpenGL的一些冗余接口和引入一些新功能提高处理手持设备和嵌入式设备的特定性能和功耗限制,因此在写此博客时主要是与OpenGL的对比过...

weixin_42128759的博客 517

android openGL ES详解(一)

主线程与GLSurfaceView:主线程负责的实例化、布局和生命周期管理,并通过特定方法(如queueEvent)与渲染线程进行通信。渲染线程与GLSurfaceView:渲染线程是内部维护的,专门用于执行OpenGL ES渲染命令的线程。渲染线程根据渲染器的指示,在需要时重新绘制场景。主线程与渲染线程:两者通过提供的机制(如queueEvent)进行通信,确保UI更新和渲染工作能够协调进行,避免阻塞主线程或造成渲染延迟。通过这种方式,

闲暇部落 3098

OpenGL ES 3.0核心技术解析与移动图形开发优化

OpenGL ES作为移动图形开发的核心标准,其3.0版本通过统一着色器架构、多渲染目标(MRT)等关键技术革新,显著提升了移动设备的图形处理能力。在图形渲染领域,可编程管线设计允许开发者通过GLSL ES着色语言实现更复杂的视觉效果,而ETC2纹理压缩等技术则解决了移动平台的内存带宽瓶颈。从工程实践角度看,合理运用实例化渲染、Transform Feedback等特性,可在华为等旗舰设备上实现40%的性能提升。针对Android/iOS跨平台开发,正确的EGL上下文配置和着色器预编译方案能有效降低300m

weixin_34279184的博客 582

OpenGL几何着色器

具体来说,几何着色器会在每个图元(比如三角形、线段等)生成期间被调用一次,并且在生成完所有顶点后,它最终会将新的图元传递给下一个阶段的像素着色器进行处理。几何着色器可以执行的操作非常丰富,除了基本的顶点变换、投影以及裁剪等操作外,还可以进行一些高级的几何体操作,比如曲面细分、实例化、粒子系统等。以上是一个简单的OpenGL渲染示例程序,其中包括了基本的顶点着色器、几何着色器和片元着色器代码实现,以及主程序中的渲染流程和数据处理部分。几何着色器:对顶点着色器输出的图元进行处理和操作。OpenGL几何着色器。

CyberNova的博客 373

学习OpenGL ES for Android(二十四)— 几何着色器

文章目录简介 在本章之前还有两章:高级数据和高级GLSL,都是比较理论的知识,有兴趣的话可以自己学习。 简介 几何着色器(Geometry Shader)是一个可选功能,他介于在顶点和片段着色器之间,接收一组顶点数据,可以对数据进行处理,而且可以根据数据生成不止一个图形,假如你想绘制四个顶点,按照以前的方式,需要for循环四次,每次都顶点数据进行处理,最后传入顶点着色器中。而集合着色器就做了这样一...

chends 1577

OpenCV透视变换实战:5分钟搞定歪斜证件照矫正(附完整代码)

本文详细介绍了如何使用OpenCV的透视变换技术快速矫正歪斜证件照,包含完整的Python代码实现。通过特征检测、坐标计算和矩阵变换三个核心步骤,5分钟内即可将手机拍摄的歪斜图像自动矫正为专业级证件照,大幅提升图像处理效率。

weixin_29094979的博客 481

OpenGL ES 3.2 在 Android 12 上的配置:从 GLSurfaceView 到 EGL 的 5 步实战

本文详细介绍了在Android 12上配置OpenGL ES 3.2的5步实战指南,包括现代移动图形开发的技术选型、工程环境搭建、核心渲染架构实现、性能优化策略以及高级特性实战。通过GLSurfaceView和EGL的配置,开发者可以充分利用OpenGL ES 3.2的计算着色器、几何着色器等特性,提升图形应用的性能和渲染质量。

weixin_30458043的博客 285

电赛MSP430F5529之超声波测距+OLED 显示(含主要代码)

目录 前言 一、超声波介绍 二、部分代码 三、总结 题外话: 前言 哎,又双叒叕,电赛延期了,看起来像是遥遥无期,但是肯定要考虑大局,大家就继续抓紧时间学习知识,巩固自己的知识,更进一步。好了,今天主要是继承上一篇博文(OLED显示)写的,主要是写超声波测距的(US-015) 一、超声波介绍 利用TI公司的MSP430F5529单片机+超声波模块+OLED 实现 这里主要是利用单片机定时器的捕获功能来捕获超声波高电平持续时间,再计算得到距离,最后再在OLED 屏幕上显示出来,上图

不说二话的自家人的博客 1万+

华为数通---配置风暴控制案例

风暴控制可以通过阻塞报文或关闭端口来阻断广播、未知组播和未知单播报文的流量。

2301_77081516的博客 1624

Android OpenGL ES 3.0 开发完全指南:从零基础到高级特效实战

想要掌握 Android 平台上的高性能图形编程吗?NDK_OpenGLES_3_0 项目为你提供了一个系统性的 Android OpenGL ES 3.0 学习教程,从基础三角形绘制到复杂的 3D 模型渲染,再到抖音特效实现,这个开源项目涵盖了 OpenGL ES 3.0 开发的方方面面。无论你是 Android 开发者想提升图形处理能力,还是想学习移动端图形编程,这个项目都是绝佳的学习资源。

gitblog_01047的博客 626

UE5导入MetaHuman虚拟头像后,连接live link face,面部表情捕捉出错修正

UE5 利用live link face 驱动meta human面部表情捕捉

m0_53122690的博客 6594

[Java] [OpenGL ES 3.x] 程序模板

MainActivity.java package com.Diamond.example; import android.app.Activity; import android.os.Bundle; import com.Diamond.example.MySurfaceView; public class MainActivity extends Activity { public MySurfaceView surfaceView; @Override prot

qq_53530146的博客 1051

蓝桥杯算法:打印图形——六芒星

【输入格式】 输入一行包含一个整数 n。 【输出格式】 输出一个六芒星,表示满足题意的图形。 【样例输入】 3 4 【样例输出】 【评测用例规模与约定】 对于所有评测用例,1 ≤ n ≤ 20。 总结: 第一次做图形题,做了蛮久,图形题有很多的方式去完成,它考验的是你能不能找到更简单的那一种方式. 我初看到这个图形,第一种想法是把这个六芒星当做六个小三角形来做,然后以某种规则排列一下,就能做出来,但是尝试了一下感觉太麻烦了。 然后我的第二种想法是,这个图形是对称图形,怎么能写出一半吗? 这样另一半想办

KeepCodeing 3835

[QMT量化交易小白入门]-四十四、年化收益48%的策略要兼容回测和实盘,将实盘改为定时任务的方式

本专栏主要是介绍QMT的基础用法,常见函数,写策略的方法,也会分享一些量化交易的思路,大概会写100篇左右。QMT的相关资料较少,在使用过程中不断的摸索,遇到了一些问题,记录下来和大家一起沟通,共同进步。

木头大左的博客 1368

OpenGL/ES

OpenGL/ES

xie__jin__cheng的博客 1149

筑牢上线前安全防线:安全运维服务中的检测实践与深化

在日益严峻的网络安全形势下,将漏洞扫描、渗透测试和基线核查有机结合的上线前安全检测,是保障信息系统安全、稳定、合规上线的基石。通过专业、严谨、协作的检测实践,乙方安全团队能够有效协助甲方在系统生命周期的起点就构筑起坚固的安全防线,为业务的顺利开展和用户的信任保驾护航,真正实现“安全赋能业务”的价值。这道“守门人”的职责,是数字时代企业稳健前行的不可或缺的保障。专业的安全服务提供商(乙方)需要与甲方(系统所有者/运营方)建立紧密、透明、协作的伙伴关系,实现风险的闭环管理,甚至推动甲方安全能力的长期提升。

huluang的专栏 183

OpenGL ES 3.1 Android扩展包介绍

OpenGL ES 3.1 Android扩展包介绍5 OpenGL ES 3.1 Android扩展包介绍5.1 概述5.2 几何着色器5.3 镶嵌着色器5.4 高级混合模式5.5 ASTC LDR 5 OpenGL ES 3.1 Android扩展包介绍 5.1 概述 AEP是Adreno SDK的一部分,其中包含一组OpenGL ES 3.1扩展,请阅读本章以快速介绍这些扩展背后的概念以及它们可能为应用带来的好处。更详细的信息。 5.2 几何着色器 几何着色器是一种新的着色器类型,可以选择在Fragm

weixin_38498942的博客 8445

PyTorch生成式人工智能(7)——生成对抗网络(Generative Adversarial Network, GAN)实践详解

生成对抗网络 (Generative Adversarial Network, GAN) 最早由 Ian Goodfellow 于 2014 年提出,其中生成器试图创建与真实样本无法区分的数据样本,而判别器则试图区分生成器生成的样本与真实样本。在本节中,我们将介绍 GAN 的理论基础。然后,介绍如何使用 PyTorch 从零开始构建 GAN,以便了解所有细节,深入理解 GAN 的内部工作原理,为后续讨论 GAN 的其他更高级内容奠定基础,比如生成高分辨率图像或逼真的音乐。

盼小辉丶的博客 1万+

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

android中多态的应用_OpenGL简介与OpenGL ES在Android中的应用

OpenGLOpenGL 、OpenGL ES简介OpenGLOpenGLES 3.0图形API汇总着色器与管线着色器程序Shader顶点着色器VertexShader片元着色器FragmentShader管线内置变量与内置函数内置变量内置函数总结渲染宿主渲染窗口宿主GLSurfaceView和TextureView的区别EGL什么是EGL自己写一个TextureEGLHelper来管理...

weixin_39566578的博客 243

lwip-2.0.3

lwip是瑞典计算机科学院(SICS)的Adam Dunkels 开发的一个小型开源的TCP/IP协议栈。实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用。

Android OpenGL ES 学习(二) -- 图形渲染管线和GLSL

上一章,我们学习了 OpenGL 的基本知识,这一章,一起学习OpenGL的渲染模式和渲染语言GLSL。

夏至的稻穗的博客 6441

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

OpenGL ES SDK for Android - 7

Using multiview rendering 此示例提供了GL_OVR_multiview和GL_OVR_multiview2扩展以及它们如何用于提高虚拟现实用例的性能。 Introduction 多视图渲染允许绘制调用同时渲染到数组纹理的多个层。 顶点着色器可以知道它正在写入哪个图层,因此每个图层的渲染结果可能不同。 这对于虚拟现实应用非常有用,其中需要从两个不同位置渲染相同的场景。 多...

weixin_34161064的博客 1525

linux opengl es,OpenGL ES 概述

什么是 OpenGLOpenGL(全名:Open Graphics Library) 所以一个图形API,由C++编写的图形处理框架。可以进行跨平台的操作,OpenGL规定严格规定了每个函数的执行,以及它的输出。至于内部具体每个函数是如何实现(Implement)的,将由OpenGL库的开发者自行决定(译注:这里开发者是指编写OpenGL库的人)。因为OpenGL规范并没有规定实现的细节,具体的O...

weixin_35816790的博客 437

OpenGL ES 和 Metal

ios开发app之间的通信方案。

逝水无情的博客 1141

求职GO:OpenGL与OpenGL-ES面试大全(持续更新)

求职GO:OpenGL与OpenGL-ES面试大全(持续更新)一、基本概念 1.1 名词解释1.2 简述OpenGL渲染管线过程1.3 简述着色器的使用流程1.4 如何使用日志输出着色器错误1.5 GL_LINES、GL_LINE_STRIP 和 GL_LINE_LOOP有什么区别?1.6 GL_TRIANGLES、GL_TRIANGLE_STRIP 和 GL_TRIANGLE_FAN有什么

fishmai的专栏 1187
上一篇: Android OpenGL ES详解——高级GLSL
下一篇: Android OpenGL ES详解——版本3.0和2.0 的异同点和新功能
闲暇部落
博客等级 码龄12年 2775粉丝 215原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

打赏作者

闲暇部落

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

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

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

打赏作者

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

抵扣说明:

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

余额充值