转自:http://xiaxveliang.blog.163.com/blog/static/29708034201272101349650/

不得不说,百度搜技术文章,搜出来的垃圾文章非常多。用搜索引擎,还是被墙了的google好,搜出来的技术文章很对味口。这不又找到了一个好东西,转来收藏和学习。

与OpenGL ES1.x渲染管线相比,OpenGL ES 2.0渲染管线中“顶点着色器”取代了OpenGL ES 1.x渲染管线中的“变换和光照”;“片元着色器”取代了OpenGL ES 1.x渲染管线中的“纹理环境和颜色求和”、“雾”以及“Alpha测试”。

这使得开发人员在使用OpenGL ES 2.0API进行开发时,不得不编写着色器代码,来完成一些顶点变换和纹理颜色计算的工作。

这里介绍的与shader相关的部分主要分为三个阶段:

1)         加载shader,其中包括加载shader、编译shader 、连接shader并且产生着色器程序id(文中的1、2、3部分);

2)         获取指向顶点着色器相应数据的Index(文中的第4部分);

3)        根据Index向顶点着色器中传递数据(文中的第5 部分);

1.   着色器与着色语言

着色语言是一种类C语言,但不像C语言一样支持双精度浮点型(double)、字节型(byte)、短整型(short)、长整型(long),并且取消了C中的联合体(union)、枚举类型(enum)、无符号数(unsigned)以及位运算等特性。

着色语言中有许多内建的原生数据类型以及构建数据类型,如:浮点型(float)、布尔型(bool)、整型(int)、矩阵型(matrix)以及向量型(vec2、vec3等)等。总体来说,这些数据类型可以分为标量、向量、矩阵、采样器、结构体以及数组等。

shader支持下面数据类型:

Float  bool  int

vec2                                                包含了2个浮点数的向量

vec3                                                包含了3个浮点数的向量

vec4                                                包含了4个浮点数的向量

ivec2                                              包含了2个整数的向量

ivec3                                              包含了3个整数的向量

ivec4                                              包含了4个整数的向量

bvec2                                             包含了2个布尔数的向量

bvec3                                             包含了3个布尔数的向量

bvec4                                             包含了4个布尔数的向量

mat2                                       2*2维矩阵

mat3                                             3*3维矩阵

mat4                                             4*4维矩阵

sampler1D                                  1D纹理着色器

sampler2D                                   2D纹理着色器

sampler3D                                  3D纹理着色器

1.1、  顶点着色器

(1)   顶点着色器代码

uniform mat4 uMVPMatrix;                     //应用程序传入顶点着色器的总变换矩阵

attribute vec4 aPosition;                          //应用程序传入顶点着色器的顶点位置

attribute vec2 aTextureCoord;            //应用程序传入顶点着色器的顶点纹理坐标

attribute vec4 aColor                                   //应用程序传入顶点着色器的顶点颜色变量

varying vec4 vColor                                      //用于传递给片元着色器的顶点颜色数据

varying vec2 vTextureCoord;                             //用于传递给片元着色器的顶点纹理数据

void main()

{

gl_Position = uMVPMatrix * aPosition;             //根据总变换矩阵计算此次绘制此顶点位置

vColor = aColor;                                                       //将顶点颜色数据传入片元着色器

vTextureCoord = aTextureCoord;                         //将接收的纹理坐标传递给片元着色器

}

(2)     顶点着色器介绍

顶点着色器是一个可编程的处理单元,并且执行顶点的变换、纹理坐标的变换、光照、材质的应用等顶点的相关操作,每顶点执行一次。

顶点着色器替代了顶点变换、光照以及纹理坐标的处理,开发人员可以根据自己的需求自行开发顶点变换、光照以及纹理坐标的处理,大大增加了程序的灵活性。

顶点着色器主要是传入相应的Attribute变量、Uniforms变量、采样器以及临时变量,经过顶点着色器后生成Varying变量。如图1-1所示。

图1-1顶点着色器工作原理

?     Attribute变量(属性变量)只能用于顶点着色器中,而不能在片元着色器中使用。一般用于每个顶点都各自不同的量,如顶点位置、颜色等。

Uniforms变量(一致变量)用来将数据值从应用程其序传递到顶点着色器或者片元着色器。一般用于对同一组顶点组成的单个3D物体中所有顶点都相同的量,如当前的光源位置、总变换矩阵等。

?     Varying变量(易变变量)是从顶点着色器传递到片元着色器的数据变量。顶点着色器可以使用易变变量来传递需要插值的颜色、法向量、纹理坐标等任意值。例如:上面代码中应用程序中由顶点着色器传入片元着色器中的vColor变量。

?     gl_Position顶点着色器从应用程序中获得原始的顶点位置数据,这些原始的顶点数据在顶点着色器中经过平移、旋转、缩放等数学变换后,生成新的顶点位置。新的顶点位置通过在顶点着色器中写入gl_Position传递到渲染管线的后继阶段继续处理。

?     gl_PointSize顶点着色器中可以计算一个点的大小(单位为像素),并将其赋值给gl_PointSize(标量float类型)以传递给渲染管线。如果没有明确赋值的话,就是采用默认值1了。gl_PointSize的值一般只有在采用了点绘制方式之后才有意义。

1.2、  片元着色器

(1)   片元着色器代码

precision mediump float;                                     //设置工作精度

varying vec4 vColor;                                              //接收从顶点着色器过来的顶点颜色数据

varying vec2 vTextureCoord;                               //接收从顶点着色器过来的纹理坐标

uniform sampler2D sTexture;                             //纹理采样器,代表一幅纹理

void main() {

gl_FragColor = texture2D(sTexture, vTextureCoord) * vColor;                 //进行纹理采样

}

此片元着色器的主要功能为根据接收的记录片元纹理坐标的易变变量中的纹理坐标,调用texture2D内建函数从采样器中进行纹理采样,得到此片元的颜色值。最后,将采样到的颜色值传给gl_FragColor内建变量,完成片元的着色。

(2)   片元着色器介绍

片元着色器是一个处理片元值及其相关联数据的可编程单元,片元着色器可执行纹理的访问、颜色的汇总、雾化等操作,每片元执行一次。

片元着色器替代了纹理、颜色求和、雾以及Alpha测试,这一部分是需要开发者自己开发的。

图2-1 片元着色器工作原理

?     Varying指的是从顶点着色器传递到片元着色器的数据变量。

gl_FragColor(vec4类型)内置变量用来由片元着色器计算完成的片元颜色值,此颜色值将送入渲染管线的后继阶段进行处理,例如片元着色器中gl_FragColor = vColor。

2.   加载着色器脚本代码loadShader方法的介绍

/*

*shader的类型  GLES20.GL_VERTEX_SHADER   GLES20.GL_FRAGMENT_SHADER

*shader的脚本字符串

*/

private int loadShader( int shaderType,String source ) {

//创建一个新shader

int shader = GLES20.glCreateShader(shaderType);

//若创建成功则加载shader

if (shader != 0) {

//加载shader的源代码

GLES20.glShaderSource(shader, source);

//编译shader

GLES20.glCompileShader(shader);

//存放编译成功shader数量的数组

int[] compiled = new int[1];

//获取Shader的编译情况

GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);

if (compiled[0] == 0) {

//若编译失败则显示错误日志并删除此shader

Log.e(TAG, "Could not compile shader " + shaderType + ":");

Log.e(TAG, GLES20.glGetShaderInfoLog(shader));

GLES20.glDeleteShader(shader);

shader = 0;

}

}

return shader;

}

该方法中主要为三个方法

GLES20.glCreateShader(shaderType);

GLES20.glShaderSource(shader, source);

GLES20.glCompileShader(shader);

3.1.      GLES20.glCreateShader(shaderType);

用glCreateShader创建一个容纳shader的容器,称为shader容器。

方法参数:

GLES20.GL_VERTEX_SHADER               (顶点shader)

GLES20.GL_FRAGMENT_SHADER            (片元shader)

函数原型为:

int glCreateShader (int type)

如果调用成功的话,函数将返回一个整形的正整数作为shader容器的id。

3.2.      GLES20.glShaderSource(shader, source);

接下来,要在创建好的shader容器中添加shader的源代码。源代码应该以字符串数组的形式表示。当然,也可以只用一个字符串来包含所有的源代码。

glShaderSource函数的原型如下:

void glShaderSource (int shader, String string)

参数含义:

shader是代表shader容器的id(由glCreateShader返回的整形数);

strings是包含源程序的字符串数组;

3.3.      GLES20.glCompileShader(shader);

最后,使用glCompileShader函数来对shader容器中的源代码进行编译。

函数的原型如下:

void glCompileShader (int shader)

参数含义:

shader是代表shader容器的id

3.4.      调试

调试一个shader是非常困难的。shader的世界里没有printf,无法在控制台中打印调试信息。但是可以通过一些OpenGL提供的函数来获取编译和连接过程中的信息。

在编译阶段使用glGetShaderiv获取编译情况,在连接阶段使用glGetProgramiv获取连接情况。

当错误产生的时候,还可以从InfoLog中获得更多的信息。InfoLog中存储了关于上一个操作执行时的相关信息,比如编译阶段的警告和错误,以及连接阶段产生的问题。不幸的是对于错误信息没有统一的标准,所以不同的硬件或驱动程序将提供不同的错误信息。

编译阶段使用glGetShaderInfoLog获取编译错误,

(1)       编译阶段使用glGetShaderiv获取编译情况

void glGetShaderiv (int shader, int pname, int[] params, int offset)

参数含义:

shader是一个shader的id;

pname使用GL_COMPILE_STATUS;

params是返回值,如果一切正常返回GL_TRUE代,否则返回GL_FALSE。

(2)       编译阶段使用glGetShaderInfoLog获取编译错误

函数原型:

String glGetShaderInfoLog (int shader)

参数含义:

shader是一个顶点shader或者片元shader的id

这里值得一说的是,笔者随便打印了一个shader的编译错误,得出的string字符串却为空,不知原因?????。

(3)       在连接阶段使用glGetProgramiv获取连接情况

函数原型:

void glGetProgramiv (int program, int pname, int[] params, int offset)

参数含义:

program是一个着色器程序的id;

pname是GL_LINK_STATUS;

param是返回值,如果一切正常返回GL_TRUE代,否则返回GL_FALSE。

(4)       在连接阶段使用glGetProgramInfoLog获取连接错误

函数原型:

String glGetProgramInfoLog (int program)

参数含义:

program是一个着色器程序的id;

对于连接错误的打印,因为尚没有遇到过,所以不知道打印字符串的信息。

(5)       清理shader的glDeleteShader方法

当不再需要某个shader或某个程序的时候,需要对其进行清理,以释放资源。前面,提到过如何向一个程序中添加一个shader。这里可调用下面的函数来将一个shader从一个程序中清除掉:

函数原型:

void glDeleteShader (int shader);

参数含义:

shader是要被排除的顶点shader或者片元shader的id

如果,一个shader被删除之前没有从相应的程序中排除,那么这个shader不会被实际删除,而只是被标记为被删除;当shader被从程序中排除的时候,才会被真正地删除。

3.   生成着色器程序id的createProgram方法介绍

//创建shader程序的方法

private int createProgram(String vertexSource, String fragmentSource) {

//加载顶点着色器

int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);

if (vertexShader == 0) {

return 0;

}

//加载片元着色器

int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);

if (pixelShader == 0) {

return 0;

}

//创建着色器程序

int program = GLES20.glCreateProgram();

//若程序创建成功则向程序中加入顶点着色器与片元着色器

if (program != 0) {

//向程序中加入顶点着色器

GLES20.glAttachShader(program, vertexShader);

//向程序中加入片元着色器

GLES20.glAttachShader(program, pixelShader);

//链接程序

GLES20.glLinkProgram(program);

//存放链接成功program数量的数组

int[] linkStatus = new int[1];

//获取program的链接情况

GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);

//若链接失败则报错并删除程序

if (linkStatus[0] != GLES20.GL_TRUE) {

Log.e(TAG, "Could not link program: ");

Log.e(TAG, GLES20.glGetProgramInfoLog(program));

GLES20.glDeleteProgram(program);

program = 0;

}

}

return program;

}

3.1.      glCreateProgram

在连接shader之前,首先要创建一个容纳程序的容器,称为着色器程序容器。可以通过glCreateProgram函数来创建一个程序容器。

函数的原型如下:

int glCreateProgram ()

如果函数调用成功将返回一个整形正整数作为该着色器程序的id

3.2.      glAttachShader

接下来,我们要将shader容器添加到程序中。这时的shader容器不一定需要被编译,他们甚至不需要包含任何的代码。我们要做的只是将shader容器添加到程序中。使用glAttachShader函数来为程序添加shader容器。

函数的原型如下:

void glAttachShader (int program, int shader)

参数含义:

program是着色器程序容器的id;

shader是要添加的顶点或者片元shader容器的id。

如果你同时拥有了,顶点shader和片元shader,需要分别将他们各自的两个shader容器添加的程序容器中。

3.3.      glLinkProgram

最后,使用glLinkProgram来连接程序。

函数的原型如下:

void glLinkProgram (int program)

参数含义:

program是着色器程序容器的id

在连接操作执行以后,可以任意修改shader的源代码,对shader重新编译不会影响整个程序,除非重新连接程序。

3.4.      glUseProgram

在连接了程序以后,我们可以使用glUseProgram函数来加载并使用连接好的程序。glUseProgram函数原型如下:

void glUseProgram (int program)

参数含义:

program是要使用的着色器程序的id,

如果将program设置为0,表示使用固定功能管线。如果程序已经在使用的时候,对程序进行重新编译,编译后的应用程序会自动替代以前的那个被调用,这时你不需要再次调用这个函数。

4.   获取指向顶点着色器相应数据的Index(glGetAttribLocation方法, glGetUniformLocation方法)

public int mProgram;                                   //自定义渲染管线程序id

public int muMVPMatrixHandle;              //总变换矩阵引用id

public int maPositionHandle;                     //顶点位置属性引用id

public int maTextureHandle;                      //顶点纹理坐标属性引用id

public int maColorHandle;                          //顶点颜色坐标属性引用id

//基于顶点着色器与片元着色器创建着色器程序, 生成着色器程序id

mProgram = createProgram(mVertexShader, mFragmentShader);

//获取指向着色器中aPosition的index

maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition");

//获取指向着色器中aTextureCoord的index

maTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTextureCoord");

//获取指向着色器中aColor的index

maColorHandle = GLES20.glGetAttribLocation(mProgram, "aColor");

//获取指向着色器中uMVPMatrix的index

muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");

glGetAttribLocation

调用 glGetAttribLocation 来获取顶点着色器中,指定attribute名的index。以后就可以通过这个index向顶点着色器中传递数据。获取失败则返回-1。

5.   向顶点着色器中传递数据

在创建了定点着色器和片源着色器后,我们在OpenGL中如何使用这两个着色器呢,我们从下面的示例代码来学习。

使用第4节获取的指向顶点着色器相应数据的各个Index,就能将我们自己定义的顶点数据、顶点颜色数据以及纹理顶点映射数据传递到顶点着色器当中了。在传递数据前先要调用GLES20.glUseProgram(mProgram);方法使用我们在第3节创建的着色器程序mProgram。

//制定使用某套shader程序

GLES20.glUseProgram(mProgram);

//将最终变换矩阵传入shader程序

GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, MatrixState.getFinalMatrix(), 0);

//设置缓冲区起始位置

mRectBuffer.position(0);

//顶点位置数据传入着色器

GLES20.glVertexAttribPointer(maPositionHandle,

3,

GLES20.GL_FLOAT,

false,

20,

mRectBuffer);

//顶点颜色数据传入着色器中

GLES20.glVertexAttribPointer(maColorHandle, 4, GLES20.GL_FLOAT, false, 4*4, mColorBuffer);

//顶点坐标传递到顶点着色器

GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, 20, mRectBuffer);

//允许使用顶点坐标数组

GLES20.glEnableVertexAttribArray(maPositionHandle);

//允许使用顶点颜色数组

GLES20.glDisableVertexAttribArray(maColorHandle);

//允许使用定点纹理数组

GLES20.glEnableVertexAttribArray(maTextureHandle);

//绑定纹理

GLES20.glActiveTexture(GLES20.GL_TEXTURE0);

GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture);

//三角形绘制方式

GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, 4);

}

5.1.      glUseProgram

在连接了程序以后,我们可以使用glUseProgram函数来加载并使用连接好的程序。glUseProgram函数原型如下:

void glUseProgram (int program)

参数含义:

program是要使用的着色器程序的id,

如果将program设置为0,表示使用固定功能管线。如果程序已经在使用的时候,对程序进行重新编译,编译后的应用程序会自动替代以前的那个被调用,这时你不需要再次调用这个函数。

5.2.      glVertexAttribPointer

glVertexAttribPointer — 定义顶点属性数组

函数原型:

void glVertexAttribPointer (     int indx,

int size,

int type,

boolean normalized,

int stride,

Buffer ptr )

参数含义:

index 指定要修改的顶点着色器中顶点属性的索引值

size 指定每个顶点属性的组件数量。必须为1、2、3或者4。如position是由3个(x,y,z)组成,而颜色是4个(r,g,b,a))

type 指定数组中每个组件的数据类型。可用的符号常量有GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT,GL_UNSIGNED_SHORT, GL_FIXED, 和 GL_FLOAT,初始值为GL_FLOAT。

normalized指定当被访问时,固定点数据值是否应该被归一化(GL_TRUE)或者直接转换为固定点值(GL_FALSE)。

stride 指定连续顶点属性之间的偏移量。如果为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0。如果normalized被设置为GL_TRUE,意味着整数型的值会被映射至区间[-1,1](有符号整数),或者区间[0,1](无符号整数),反之,这些值会被直接转换为浮点值而不进行归一化处理。

Ptr顶点的缓冲数据

5.3.      启用或者禁用顶点属性数组

要启用或者禁用顶点属性数组,调用glEnableVertexAttribArray和glDisableVertexAttribArray传入参数index。如果启用,那么当glDrawArrays或者glDrawElements被调用时,顶点属性数组会被使用。

5.4.      glActiveTexture

glActiveTexture — 选择活动纹理单元

函数原型:

void glActiveTexture (int texture)

参数含义:

texture指定哪一个纹理单元被置为活动状态。texture必须是GL_TEXTUREi之一,其中0 <= i < GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS,初始值为GL_TEXTURE0。

glActiveTexture 确定了后续的纹理状态改变影响哪个纹理,纹理单元的数量是依据该纹理单元所被支持的具体实现。

OpenGL ES 2.0 Shader相关介绍相关推荐

  1. Adreno OpenGL ES 2.0 介绍(3)

    Adreno OpenGL ES 2.0 介绍(3) 1. 应用程序示例 1.2 在Android上创建ES 2.0 上下文/环境 1.3 Adreno GPU检测 1.4 检查支持的ES拓展 1. ...

  2. OPENGL ES 2.0 知识串讲 (10) ——OPENGL ES 详解IV(纹理优化)

    上节回顾 上一节学习了如何从一张原始图片中,获取生成纹理所需要的信息,然后根据这些信息,通过OpenGL ES API在GPU内存中生成了一张纹理,并且还介绍了纹理属性,知道了如何通过纹理坐标将纹理映 ...

  3. OPENGL ES 2.0 知识串讲 (4)——GLSL 语法(II)

    上节回顾 上一节,我们讲解了 Shader 的功能,并从预处理和注释开始,讲解 GLSL 的语法知识.想要学习和使用一门语言,必须先学习这门语言的语法,语法中除了上一节说到的预处理.注释,还有更加重要 ...

  4. OpenGL ES 2.0 总体概述

    文章目录 OpenGL ES 2.0 总体概述 1. OpenGL ES 的两个小伙伴 1.1 EGL 1.2 GLSL 2. 屏幕图片的本质和产生过程 3. OpenGL ES pipeline 3 ...

  5. OPENGL ES 2.0 知识串讲(1)――OPENGL ES 2.0 概括

    更多图形知识请关注我的公众号: 前言 电脑是做什么用的? 电脑又被称为计算机,那么最重要的工作就是计算.看过三体的同学都知道, 电脑中有无数纳米级别的计算单元,通过 0 和 1 的转换,完成加减乘除的 ...

  6. 《Android 3D游戏开发技术宝典——OpenGL ES 2.0》——2.8节本章小结

    本节书摘来自异步社区<Android 3D游戏开发技术宝典--OpenGL ES 2.0>一书中的第2章,第2.8节本章小结,作者 吴亚峰,更多章节内容可以访问云栖社区"异步社区 ...

  7. 《Android 3D游戏开发技术宝典——OpenGL ES 2.0》——2.4节文件I/O

    本节书摘来自异步社区<Android 3D游戏开发技术宝典--OpenGL ES 2.0>一书中的第2章,第2.4节文件I/O,作者 吴亚峰,更多章节内容可以访问云栖社区"异步社 ...

  8. 使用Android OpenGL ES 2.0绘图之三:绘制形状

    传送门 ☞ 轮子的专栏 ☞ 转载请注明 ☞ http://blog.csdn.net/leverage_1229 在定义好待绘制的形状之后,就要开始绘制它们了.使用OpenGL ES 2.0绘制形状可 ...

  9. 使用Android OpenGL ES 2.0绘图之一:搭建一个OpenGL ES环境

    传送门☞Android兵器谱☞转载请注明☞http://blog.csdn.net/leverage_1229 传送门☞系统架构设计☞转载请注明☞http://blog.csdn.net/levera ...

最新文章

  1. 【做题】SRM701 Div1 Hard - FibonacciStringSum——数学和式&矩阵快速幂
  2. 李铁军教授专访:当数学家遇见人工智能
  3. 【基础】EM 还是 REM?这是一个问题!
  4. P3159 [CQOI2012]交换棋子(费用流)
  5. Docker最全教程——数据库容器化之持久保存数据(十一)
  6. Cactoos中的面向对象的声明式输入/输出
  7. openstack实例状态错误_Openstack虚机操作总结
  8. 基于matlab的频率响应分析,基于MATLAB/GUI的二阶低通电路频率响应分析与仿真
  9. 让NUnit轻松支持.NET 2.0
  10. iRobot 公司招聘,机器人、SLAM、视觉感知、路径规划方向
  11. java绘制矩形 鼠标_如何用java在面板上用鼠标绘制出一个矩形
  12. 怎么查看页面跳转过程_fastcapture注册码怎么获取?FastStone注册码分享
  13. FileZilla中文乱码解决方法
  14. 洛谷P4158 [SCOI2009]粉刷匠 题解
  15. 乌云曝光铁道部12306网站出现SQL漏洞
  16. c语言测试你的性格,著名人格测试:五张图看出你的性格
  17. 在Ubuntu 8.10 中安装使用新一代输入法ibus Deb包下载
  18. Brunei Shell Marketing (BSM)选择P97的PetroZone(R)为文莱36个加油站提供壳牌驾驶者应用
  19. 华硕电脑 计算机管理员,华硕笔记本BIOS怎么用管理员密码清除用户密码...-卓优商学问答...
  20. 短视频拍摄5大景别及11种运镜手法,新手必备拍摄干货丨国仁网络资讯

热门文章

  1. rsa前后端加密流程_不懂前后端分离?这篇就够了
  2. Xcode中c语言读键盘,使用Objective-C自定义键盘-Xcode8
  3. 火狐中怎么把xml转换为html,创建兼容IE、火狐、chrome、oprea浏览器的xmlDom对象方法...
  4. arduino温湿度计库文件_arduino学习笔记八 温湿度计
  5. lnmp 1.4 mysql_lnmp1.4配置https教程
  6. 8位并行左移串行转换电路_74ls194串行数据到并行数据的转换
  7. 数据库设计三大范式应用实例剖析(讲得比较清楚)
  8. 经典解释监视器和对象锁
  9. Calendar的api方法
  10. Scala学习笔记05:函数