【OpenGL ES】顶点着色器

时间:2021-10-03 03:33:00

1、输入输出

顶点着色器可用于传统的基于顶点操作,例如通过矩阵变换位置、计算照明方程式以生成逐顶点的颜色以及生成或者变换纹理坐标。顶点着色器的输入、输出如下图所示。

【OpenGL ES】顶点着色器

输入变量或属性:用顶点数组提供的逐顶点数据。
统一变量和统一变量缓冲区:顶点着色器使用的不变数据。
采样器:代表顶点着色器使用的纹理的特殊统一变量类型。
着色器程序:顶点着色器程序源代码或者描述在操作顶点的可执行文件。
输出或可变变量:在图元光栅化阶段,为每个生成的片段计算这些变量,并作为片段着色器的输入传入。
gl_Position和gl_PointSize是两个内建变量,内建变量包括特殊变量、统一状态以及规定最大值的常量。

内建特殊变量——
gl_VertexID:整数型输入变量,用于保存顶点的整数索引,精度限定符为highp。
gl_InstanceID:整数型输入变量,用于保存实例化绘图调用中图元的实例编号,对于常规的绘图调用,该值为0,精度限定符为highp。
gl_Position:浮点型输出变量,用于输出顶点位置的裁剪坐标,该值在裁剪和视口阶段用于执行相应的图元裁剪以及从裁剪坐标到屏幕坐标的顶点位置变换,顶点着色器没有写入gl_Position时其值是未定义的,精度限定符为highp。
glPointSize:浮点输出变量,用于写入以像素表示的点尺寸,在渲染点时使用,顶点着色器输出的这个变量值被限定在OpenGL ES 3.0实现支持的非平滑点大小范围之内,精度限定符为highp。
glFrontFacing:特殊的布尔型变量,不是由顶点着色器直接写入的,而是根据顶点着色器生成的位置值和渲染的图元类型生成的。

内建统一状态——
顶点着色器有一个内建统一状态,glDepthRange,是窗口坐标中的深度范围,类型为uniform变量,如下所示:

struct glDepthRangeParameters
{
highp float near; // near Z
highp float far; // far Z
highp float diff; // far - near
};
uniform glDepthRangeParameters glDepthRange;

内建常量——
顶点着色器有如下内建常量,为每个内建常量指定的值是所有OpenGL ES 3.0实现必须支持的最小值,各种实现可能超过下面所述的最小值的常量值,实际支持的值可以通过glGetIntegerv函数查询。

const medump int gl_MaxVertexAttribs = 16;
const medump int gl_MaxVertexUniformVectors = 256;
const medump int gl_MaxVertexOutputVectors = 16;
const medump int gl_MaxVertexTextureImageUnits = 16;
const medump int gl_MaxCombinedTextureImageUnits = 32;

统一变量限制——
上面提到的gl_MaxVertexUniformVectors规定了可以用于顶点着色器的统一变量的最大数量,统一变量存储用统一变量限定符声明的变量、常数变量(const)、字面值以及特定于实现的常量。统一变量存储有一定的打包规则,根据打包规则,可以确定存储统一变量、常数变量和字面值所需的统一变量存储总数,然后确定特定于实现的常量数量是不可能的,因为这个值不仅在不同实现中不同,而且取决于顶点着色器使用的内建着色语言函数。OpenGL ES 3.0着色语言规范规定不做任何字面值的常量传播,也就是说同一个字面值的多个实例将被计算多次,所以在顶点着色器中尽可能避免使用字面值如0.0、1.0,应该声明相应的常数变量代替字面值。

2、精度限定符

精度限定符用于指定任何基于浮点数或整数的变量的精度,关键字为lowp、mediump和highp,用法如下面的例子:

highp vec4 position;
out lowp vec4 color;
mediump float specularExp;
highp int oneConstant;

除了给每个变量指定精度限定符之外,还可以使用默认的精度限定符,而不必为每个变量单独指定精度限定符,在顶点着色器的开始处进行声明,如下:

presition highp float;
presition mediump int;

在顶点着色器中,如果不显式指定默认精度,float和int的默认精度都为highp。对于通常在顶点着色器中进行的操作,最可能需要的精度限定符是highp,例如用矩阵变换位置、变换法线和纹理坐标或者生成纹理坐标的操作都需要在highp精度下进行,颜色计算和照明方程式最可能在mediump精度下进行。

3、矩阵变换

矩阵涉及三个类型,模型矩阵将物体坐标转换为世界坐标,视图矩阵将世界矩阵转换为眼睛坐标,投影矩阵将眼睛坐标转换为裁剪坐标,这三个矩阵的乘积便是模型-视图-投影(MVP)矩阵,包括缩放、平移、旋转等,下面是矩阵相关函数的代码实现。

#define PI 3.1415926535897932384626433832795f

typedef struct
{
GLfloat m[4][4];
} ESMatrix;

//
/// \brief Multiply matrix specified by result with a scaling matrix and return new matrix in result
/// \param result Specifies the input matrix. Scaled matrix is returned in result.
/// \param sx, sy, sz Scale factors along the x, y and z axes respectively
//
void esScale ( ESMatrix *result, GLfloat sx, GLfloat sy, GLfloat sz )
{
result->m[0][0] *= sx;
result->m[0][1] *= sx;
result->m[0][2] *= sx;
result->m[0][3] *= sx;

result->m[1][0] *= sy;
result->m[1][1] *= sy;
result->m[1][2] *= sy;
result->m[1][3] *= sy;

result->m[2][0] *= sz;
result->m[2][1] *= sz;
result->m[2][2] *= sz;
result->m[2][3] *= sz;
}

//
/// \brief Multiply matrix specified by result with a translation matrix and return new matrix in result
/// \param result Specifies the input matrix. Translated matrix is returned in result.
/// \param tx, ty, tz Scale factors along the x, y and z axes respectively
//
void esTranslate ( ESMatrix *result, GLfloat tx, GLfloat ty, GLfloat tz )
{
result->m[3][0] += ( result->m[0][0] * tx + result->m[1][0] * ty + result->m[2][0] * tz );
result->m[3][1] += ( result->m[0][1] * tx + result->m[1][1] * ty + result->m[2][1] * tz );
result->m[3][2] += ( result->m[0][2] * tx + result->m[1][2] * ty + result->m[2][2] * tz );
result->m[3][3] += ( result->m[0][3] * tx + result->m[1][3] * ty + result->m[2][3] * tz );
}

//
/// \brief Multiply matrix specified by result with a rotation matrix and return new matrix in result
/// \param result Specifies the input matrix. Rotated matrix is returned in result.
/// \param angle Specifies the angle of rotation, in degrees.
/// \param x, y, z Specify the x, y and z coordinates of a vector, respectively
//
void esRotate ( ESMatrix *result, GLfloat angle, GLfloat x, GLfloat y, GLfloat z )
{
GLfloat sinAngle, cosAngle;
GLfloat mag = sqrtf ( x * x + y * y + z * z );

sinAngle = sinf ( angle * PI / 180.0f );
cosAngle = cosf ( angle * PI / 180.0f );

if ( mag > 0.0f )
{
GLfloat xx, yy, zz, xy, yz, zx, xs, ys, zs;
GLfloat oneMinusCos;
ESMatrix rotMat;

x /= mag;
y /= mag;
z /= mag;

xx = x * x;
yy = y * y;
zz = z * z;
xy = x * y;
yz = y * z;
zx = z * x;
xs = x * sinAngle;
ys = y * sinAngle;
zs = z * sinAngle;
oneMinusCos = 1.0f - cosAngle;

rotMat.m[0][0] = ( oneMinusCos * xx ) + cosAngle;
rotMat.m[0][1] = ( oneMinusCos * xy ) - zs;
rotMat.m[0][2] = ( oneMinusCos * zx ) + ys;
rotMat.m[0][3] = 0.0F;

rotMat.m[1][0] = ( oneMinusCos * xy ) + zs;
rotMat.m[1][1] = ( oneMinusCos * yy ) + cosAngle;
rotMat.m[1][2] = ( oneMinusCos * yz ) - xs;
rotMat.m[1][3] = 0.0F;

rotMat.m[2][0] = ( oneMinusCos * zx ) - ys;
rotMat.m[2][1] = ( oneMinusCos * yz ) + xs;
rotMat.m[2][2] = ( oneMinusCos * zz ) + cosAngle;
rotMat.m[2][3] = 0.0F;

rotMat.m[3][0] = 0.0F;
rotMat.m[3][1] = 0.0F;
rotMat.m[3][2] = 0.0F;
rotMat.m[3][3] = 1.0F;

esMatrixMultiply ( result, &rotMat, result );
}
}

//
/// \brief Multiply matrix specified by result with a perspective matrix and return new matrix in result
/// \param result Specifies the input matrix. New matrix is returned in result.
/// \param left, right Coordinates for the left and right vertical clipping planes
/// \param bottom, top Coordinates for the bottom and top horizontal clipping planes
/// \param nearZ, farZ Distances to the near and far depth clipping planes. Both distances must be positive.
//
void esFrustum ( ESMatrix *result, float left, float right, float bottom, float top, float nearZ, float farZ )
{
float deltaX = right - left;
float deltaY = top - bottom;
float deltaZ = farZ - nearZ;
ESMatrix frust;

if ( ( nearZ <= 0.0f ) || ( farZ <= 0.0f ) ||
( deltaX <= 0.0f ) || ( deltaY <= 0.0f ) || ( deltaZ <= 0.0f ) )
{
return;
}

frust.m[0][0] = 2.0f * nearZ / deltaX;
frust.m[0][1] = frust.m[0][2] = frust.m[0][3] = 0.0f;

frust.m[1][1] = 2.0f * nearZ / deltaY;
frust.m[1][0] = frust.m[1][2] = frust.m[1][3] = 0.0f;

frust.m[2][0] = ( right + left ) / deltaX;
frust.m[2][1] = ( top + bottom ) / deltaY;
frust.m[2][2] = - ( nearZ + farZ ) / deltaZ;
frust.m[2][3] = -1.0f;

frust.m[3][2] = -2.0f * nearZ * farZ / deltaZ;
frust.m[3][0] = frust.m[3][1] = frust.m[3][3] = 0.0f;

esMatrixMultiply ( result, &frust, result );
}

//
/// \brief Multiply matrix specified by result with a perspective matrix and return new matrix in result
/// \param result Specifies the input matrix. New matrix is returned in result.
/// \param fovy Field of view y angle in degrees
/// \param aspect Aspect ratio of screen
/// \param nearZ Near plane distance
/// \param farZ Far plane distance
//
void esPerspective ( ESMatrix *result, float fovy, float aspect, float nearZ, float farZ )
{
GLfloat frustumW, frustumH;

frustumH = tanf ( fovy / 360.0f * PI ) * nearZ;
frustumW = frustumH * aspect;

esFrustum ( result, -frustumW, frustumW, -frustumH, frustumH, nearZ, farZ );
}

//
/// \brief Multiply matrix specified by result with a perspective matrix and return new matrix in result
/// \param result Specifies the input matrix. New matrix is returned in result.
/// \param left, right Coordinates for the left and right vertical clipping planes
/// \param bottom, top Coordinates for the bottom and top horizontal clipping planes
/// \param nearZ, farZ Distances to the near and far depth clipping planes. These values are negative if plane is behind the viewer
//
void esOrtho ( ESMatrix *result, float left, float right, float bottom, float top, float nearZ, float farZ )
{
float deltaX = right - left;
float deltaY = top - bottom;
float deltaZ = farZ - nearZ;
ESMatrix ortho;

if ( ( deltaX == 0.0f ) || ( deltaY == 0.0f ) || ( deltaZ == 0.0f ) )
{
return;
}

esMatrixLoadIdentity ( &ortho );
ortho.m[0][0] = 2.0f / deltaX;
ortho.m[3][0] = - ( right + left ) / deltaX;
ortho.m[1][1] = 2.0f / deltaY;
ortho.m[3][1] = - ( top + bottom ) / deltaY;
ortho.m[2][2] = -2.0f / deltaZ;
ortho.m[3][2] = - ( nearZ + farZ ) / deltaZ;

esMatrixMultiply ( result, &ortho, result );
}

//
/// \brief Perform the following operation - result matrix = srcA matrix * srcB matrix
/// \param result Returns multiplied matrix
/// \param srcA, srcB Input matrices to be multiplied
//
void esMatrixMultiply ( ESMatrix *result, ESMatrix *srcA, ESMatrix *srcB )
{
ESMatrix tmp;
int i;

for ( i = 0; i < 4; i++ )
{
tmp.m[i][0] = ( srcA->
m[i][0] * srcB->m[0][0] ) +
( srcA->m[i][1] * srcB->m[1][0] ) +
( srcA->m[i][2] * srcB->m[2][0] ) +
( srcA->m[i][3] * srcB->m[3][0] ) ;

tmp.m[i][1] = ( srcA->m[i][0] * srcB->m[0][1] ) +
( srcA->m[i][1] * srcB->m[1][1] ) +
( srcA->m[i][2] * srcB->m[2][1] ) +
( srcA->m[i][3] * srcB->m[3][1] ) ;

tmp.m[i][2] = ( srcA->m[i][0] * srcB->m[0][2] ) +
( srcA->m[i][1] * srcB->m[1][2] ) +
( srcA->m[i][2] * srcB->m[2][2] ) +
( srcA->m[i][3] * srcB->m[3][2] ) ;

tmp.m[i][3] = ( srcA->m[i][0] * srcB->m[0][3] ) +
( srcA->m[i][1] * srcB->m[1][3] ) +
( srcA->m[i][2] * srcB->m[2][3] ) +
( srcA->m[i][3] * srcB->m[3][3] ) ;
}

memcpy ( result, &tmp, sizeof ( ESMatrix ) );
}

//
//// \brief Return an identity matrix
//// \param result Returns identity matrix
//
void esMatrixLoadIdentity ( ESMatrix *result )
{
memset ( result, 0x0, sizeof ( ESMatrix ) );
result->m[0][0] = 1.0f;
result->m[1][1] = 1.0f;
result->m[2][2] = 1.0f;
result->m[3][3] = 1.0f;
}

//
/// \brief Generate a transformation matrix from eye position, look at and up vectors
/// \param result Returns transformation matrix
/// \param posX, posY, posZ eye position
/// \param lookAtX, lookAtY, lookAtZ look at vector
/// \param upX, upY, upZ up vector
//
void esMatrixLookAt ( ESMatrix *result,
float posX, float posY, float posZ,
float lookAtX, float lookAtY, float lookAtZ,
float upX, float upY, float upZ )
{
float axisX[3], axisY[3], axisZ[3];
float length;

// axisZ = lookAt - pos
axisZ[0] = lookAtX - posX;
axisZ[1] = lookAtY - posY;
axisZ[2] = lookAtZ - posZ;

// normalize axisZ
length = sqrtf ( axisZ[0] * axisZ[0] + axisZ[1] * axisZ[1] + axisZ[2] * axisZ[2] );

if ( length != 0.0f )
{
axisZ[0] /= length;
axisZ[1] /= length;
axisZ[2] /= length;
}

// axisX = up X axisZ
axisX[0] = upY * axisZ[2] - upZ * axisZ[1];
axisX[1] = upZ * axisZ[0] - upX * axisZ[2];
axisX[2] = upX * axisZ[1] - upY * axisZ[0];

// normalize axisX
length = sqrtf ( axisX[0] * axisX[0] + axisX[1] * axisX[1] + axisX[2] * axisX[2] );

if ( length != 0.0f )
{
axisX[0] /= length;
axisX[1] /= length;
axisX[2] /= length;
}

// axisY = axisZ x axisX
axisY[0] = axisZ[1] * axisX[2] - axisZ[2] * axisX[1];
axisY[1] = axisZ[2] * axisX[0] - axisZ[0] * axisX[2];
axisY[2] = axisZ[0] * axisX[1] - axisZ[1] * axisX[0];

// normalize axisY
length = sqrtf ( axisY[0] * axisY[0] + axisY[1] * axisY[1] + axisY[2] * axisY[2] );

if ( length != 0.0f )
{
axisY[0] /= length;
axisY[1] /= length;
axisY[2] /= length;
}

memset ( result, 0x0, sizeof ( ESMatrix ) );

result->m[0][0] = -axisX[0];
result->m[0][1] = axisY[0];
result->m[0][2] = -axisZ[0];

result->m[1][0] = -axisX[1];
result->m[1][1] = axisY[1];
result->m[1][2] = -axisZ[1];

result->m[2][0] = -axisX[2];
result->m[2][1] = axisY[2];
result->m[2][2] = -axisZ[2];

// translate (-posX, -posY, -posZ)
result->m[3][0] = axisX[0] * posX + axisX[1] * posY + axisX[2] * posZ;
result->m[3][1] = -axisY[0] * posX - axisY[1] * posY - axisY[2] * posZ;
result->m[3][2] = axisZ[0] * posX + axisZ[1] * posY + axisZ[2] * posZ;
result->m[3][3] = 1.0f;
}

4、照明

直射光——
直射光照明方程式计算中的几何因素如下图所示。

【OpenGL ES】顶点着色器

直射光是距离场景中被照明物体无限远处的光源,因为距离无限远,所以光线是平行的,如太阳光。照明方向向量是一个常量,不需要逐顶点计算。在上图中,Peye是观看者的位置,Plight是光源的位置(w分量为0),N是法线,H是半平面向量,可以用||VPlight+VPeye||计算,即||Plight.xyz+(0,0,1)||。下面是计算直射光照明方程式的顶点着色器代码。

#version 300 es

struct directional_light
{
vec3 direction; // 眼睛空间内的规范化照明方向
vec3 halfplane; // 规范化的半平面向量H,对于直射光可以预先计算,因为它不会变化
vec4 ambient_color; // 环境光颜色
vec4 diffuse_color; // 漫射光颜色
vec4 specular_color; // 反射光颜色
};

struct material_properties
{
vec4 ambient_color; // 材料的环境颜色
vec4 diffuse_color; // 材料的漫射颜色
vec4 specular_color; // 材料的反射颜色
float specular_exponent; // 材料的光亮度的反光指数,用于控制反射高光的亮度
};

const float c_zero = 0.0;
const float c_one = 1.0;

uniform directional_light light;
uniform material_properties material;

// normal是一个规范化向量而且转换到了眼睛空间
// 函数中将环境光\漫射光\反射光组合为单个颜色
// 返回计算后的颜色
// 多个光源时应该为每个光源计算一次
vec4 directional_light_color(vec3 normal)
{
vec4 computerd_color = vec4(c_zero, c_zero, c_zero, c_zero);
float ndotl; // dot product of normal & light direction
float ndoth; // dot product of normal & half-plane vector
ndotl = max(c_zero, dot(normal, light.direction);
ndoth = max(c_zero, dot(normal, light.halfplane);
computered_color += (light.ambient_color * material.ambient_color);
computered_color += (ndotl * light.diffuse_color * material.diffuse_color);
if (ndoth > c_zero) {
computered_color += (pow(ndoth, material.specular_exponent) * material.specular_color * light.specular_color);
}
return computered_color;
}

聚光灯——
聚光灯照明方程式计算中的几何因素如下图所示。

【OpenGL ES】顶点着色器

聚光灯的点光源是从空间中某个位置向所有方向发出光线的光源,由位置向量(x,y,z,w)给出,其中w不等于0。点光源在各个方向上的亮度均匀,但是根据光源到物体的位置,亮度逐渐衰减,这种衰减可以用如下公式计算。

【OpenGL ES】顶点着色器

聚光灯是兼具位置和方向的光源,模拟从一个位置(Plight)以一定方向(SPOT direction)发出的光锥。光线的强度由根据与光锥中心所成的角度得出的点截止因子衰减,与光锥中轴所成的角度用VPlight与SPOT direction的点乘算出,点截止因子在SPOT direction给出的方向上为1.0,并根据SPOT cutoff angle给出的角度(弧度)按指数关系衰减为0。下面是计算聚光灯(点光源)照明方程式的顶点着色器代码。

#version 300 es

struct spot_light
{
vec4 position; // 在眼睛空间内的照明方向
vec4 ambient_color; // 环境光颜色
vec4 diffuse_color; // 漫射光颜色
vec4 specular_color; // 反射光颜色
vec3 spot_direction; // 规范化的点方向向量
vec3 attenuation_factors; // 距离衰减因子K0 K1 K2
bool compute_distance_attenuation; // 确定距离衰减是否必须计算
float spot_exponent; // 用于计算点截止因子的聚光灯指数
float spot_cutoff_angle; // 聚光灯截止角度(度数)
};

struct material_properties
{
vec4 ambient_color; // 材料的环境颜色
vec4 diffuse_color; // 材料的漫射颜色
vec4 specular_color; // 材料的反射颜色
float specular_exponent; // 材料的光亮度的反光指数,用于控制反射高光的亮度
};

const float c_zero = 0.0;
const float c_one = 1.0;

uniform spot_light light;
uniform material_properties material;

// normal和position在眼睛空间
// normal是一个规范化的向量
// 返回计算后的颜色
vec4 spot_light_color(vec3 normal, vec4 position)
{
vec4 computerd_color = vec4(c_zero, c_zero, c_zero, c_zero);
vec3 lightdir;
vec3 halfplane;
float ndotl;
float ndoth;
float att_factor;
att_factor = c_one;
// 假设光源位置和顶点位置的w分量相同
lightdir = light.position.xyz - position.xyz;
// 计算距离衰减因子
if (light.compute_distance_attenuation) {
vec3 att_dist;
att_dist.x = c_one;
att_dist.z = dot(lightdir, lightdir);
att_dist.y = sqrt(att_dist.z);
att_factor = c_one / dot(att_dist, light.attenuation_factors);
}
// 规范化光源方向向量
lightdir = normalize(lightdir);
// 计算点截止因子
if (light.spot_cutoff_angle < 180.0) {
float spot_factor = dot(-lightdir, light.spot_direction);
if (spot_factor >= cos(radians(light.spot_cutoff_angle))) {
spot_factor = pow(spot_factor, light.spot_exponent);
}
else {
spot_factor = c_zero;
}
// 计算距离和点衰减因子的组合
att_factor *= spot_factor;
}
if (att_factor > c_zero) {
// 根据光照公式计算光照颜色
computerd_color += (light.ambient_color * material.ambient_color);
ndotl = max(c_zero, dot(normal, lightdir);
computered_color += (ndotl * light.diffuse_color * material.diffuse_color);
halfplane = normalized(lightdir + vec3(c_zero, z_zero, c_one));
ndoth = dot(normal, halfplane);
if (ndoth > c_zero) {
computered_color += (pow(ndoth, material.specular_exponent) * material.specular_color * light.specular_color);
}
// 颜色乘以衰减因子
computerd_color *= att_factor;
}

return computered_color;
}

5、生成纹理坐标

下面是两个在顶点着色器中生成纹理坐标的例子,用于渲染场景中的反光物体,其中的坐标将供对应的片段着色器使用,以计算反光物体的反射图像。例子中,通过生成一个反射向量,然后用这个向量计算一个纹理坐标,这个坐标在经纬图(球面图)或者立方图(代表捕捉反射环境的6个视图或者面,假定观察点在反光物体的中心)中索引。固定功能OpenGL规范分别将纹理坐标生成模型描述为GL_SPHERE_MAP和GL_REFLECTION_MAP,前者生成一个使用反射向量的纹理坐标,以计算可以在2D纹理贴图中查找的2D纹理坐标,后者生成的纹理坐标是一个反射向量,这个向量可以用于在立方体图中查找的3D纹理坐标。

// position is the normalized position coordinate in eye space.
// normal is the normalized normal coordinate in eye space.
// this function returns a vec2 texture coordinate.
vec2 sphere_map(vec3 position, vec3 normal)
{
reflection = reflect(position, normal);
m = 2.0 * sqrt(reflection.x * reflection.x + reflection.y * reflection.y);
return vec2(reflection.x / m + 0.5, reflection.y / m + 0.5);
}

// position is the normalized position coordinate in eye space.
// normal is the normalized normal coordinate in eye space.
// this function returns the reflection vector as a vec3 texture coordinate.
vec3 cube_map(vec3 position, vec3 normal)
{
return reflect(position, normal);
}

6、顶点蒙皮

顶点蒙皮用于平滑多边形之间的连接点,通过向每个顶点应用具有相应权重的附加变换矩阵来实现。用于顶点蒙皮的多个矩阵保存在一个矩阵调色板里,每个顶点的矩阵索引引用矩阵调色板中用于该顶点蒙皮的对应矩阵。顶点蒙皮常用于3D游戏中的角色模型,确保它们尽可能平滑和逼真地出现而无需使用附加的几何形状。用于一个顶点蒙皮的矩阵数通常为2到4个,顶点蒙皮通过如下公式计算。

【OpenGL ES】顶点着色器

例如,用一个包含32个矩阵、每个顶点最多4个矩阵的矩阵调色板生成蒙皮顶点,矩阵调色板中的矩阵通常是4x3的列优先矩阵(每个矩阵有4个vec3项目)。如果矩阵以列优先顺序存储时,则存储1行需要128个统一变量项目,每个项目有3个元素,上面提到的gl_MaxVertexUniformVectors最小值为256个vec4项目,所以只能存储4行,按照统一变量打包规则,浮点值行只能存储声明为float类型的统一变量,这样也就没有空间存储vec2、vec3或者vec4统一变量。在矩阵调色板中,如果以行优先顺序,为每个矩阵使用3个vec4项目,将能更好地存储矩阵,这样只会使用统一变量存储中的96个vec4项目,剩下的160个vec4可以用于存储其它统一变量。虽然没有足够的统一变量存储来保存计算蒙皮法线所需的逆转置矩阵,不过,在大部分情况下,所用的矩阵都是标准正交矩阵,因此可以用于变换顶点位置和法线。

7、变换反馈

变换反馈模式允许将顶点着色器的输出捕捉到缓冲区对象中,然后,输出缓冲区可以作为后续绘图调用中顶点数据的来源,这种方法对于在GPU上执行动画而无需任何CPU干预的许多技术很有用,例如粒子动画或者渲染到顶点缓冲区的物理学模拟。使用如下的glTransformFeedbackVaryings函数指定变换反馈模式期间捕捉的一组顶点属性。

void glTransformFeedbackVaryings(GLuint program,
GLsizei count,
const char ** varyings,
GLenum bufferMode);
void glGetTransformFeedbackVarying(GLuint program,
GLuint index,
GLsizei bufSize,
GLsizei * length,
GLsizei * size,
GLenum * type,
char * name);
void glBeginTransformFeedback(GLenum primitiveMode);
void glEndTransformFeedback(void);

program指定程序对象的句柄。count指定用于变换反馈的顶点输出变量的数量。varyings指定一个由count个以0为结束符的字符串组成的数组,这些字符串指定用于变换反馈的顶点输出变量的名称。bufferMode指定变换反馈启用时用于捕捉顶点输出变量的模式,有效值是GL_INTERLEAVED_ATTRIBS和GL_SEPARATE_ATTRIBS,前者将顶点输出变量捕捉到单一缓冲区,后者将每个顶点输出变量捕捉到自己的缓冲区中。调用glTransformFeedbackVaryings后,必须用glLinkProgram链接程序对象。然后,需要用带GL_TRANSFORM_FEEDBACK_BUFFER的参数的glBindBuffer绑定一个或者多个缓冲区对象作为变换反馈缓冲区,该缓冲区用带GL_TRANSFORM_FEEDBACK_BUFFER参数的glBufferData分配,用glBindBufferBase或glBindBufferRange绑定到索引绑定点。绑定变换反馈缓冲区之后,使用glBeginTransformFeedback和glEndTransformFeedback进入和退出变换反馈模式,primitiveMode指定将被捕捉到变换反馈的缓冲区对象中的图元输出类型,变换反馈限于非索引的GL_POINTS、GL_LINES和GL_TRANANGLES,glBeginTransformFeedback和glEndTransformFeedback之间发生的绘图调用的顶点输出将被捕捉到变换反馈缓冲区。

另外,可以在设置带GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN参数的glBeginQuery和glEndQuery之后,用glGetObjectuiv检索成功写入变换反馈缓冲区对象的图元数量。还可以在变换反馈模式中捕捉的同时用GL_RASTERIZER_QISCARD参数的glEnable和glDisable启用和禁用光栅化,启用GL_RASTERIZER_QISCARD时,将不会运行任何片段着色器。

8、顶点纹理

OpenGL ES 3.0支持顶点着色器中的纹理查找操作,这在实现位移贴图等技术时很有用,我们可以根据顶点着色器中的纹理查找值,沿顶点法线移动顶点位置,位移贴图技术的典型应用之一是渲染地形或者水面。在顶点着色器中执行纹理查找有一些值得注意的限制:细节层次不是隐含计算的,不接受texture查找函数中的偏差参数,基本纹理用于mip贴图纹理。OpenGL ES实现支持的纹理图像单元的最大数量可以用带GL_MAX_VERTEX_TEXTURE_UNITS参数的glGetIntegerv查询,OpenGL ES 3.0实现可以支持的最小值是16。下面是一个执行位移贴图的顶点着色器例子。

#version 300 es
// uniforms used by the vertex shader
uniform mat4 u_mvpMatrix; // matrix to convert P from model space to clip space
uniform sampler2D displacementMap;
// attribute inputs to the vertex shader
layout(location = 0) in vec4 a_position; // input position value
layout(location = 1) in vec3 a_normal; // input normal value
layout(location = 2) in vec2 a_texcoord; // input texcoord value
layout(location = 3) in vec4 a_color; // input color
// vertex shader output and input to the fragment shader
out vec4 v_color;
void main()
{
v_color = a_color;
float displacement = texture(displacementMap, a_texcoord).a;
vec4 displaced_position = a_position + vec4(a_normal * diplacement, 0.0);
gl_Position = u_mvpMatrix * displaced_position;
}

9、例子

下面的例子通过顶点着色器绘制一个旋转的立方体。
源码地址https://github.com/geminy/aidear/tree/master/graphics/mu/examples/opengles3/Simple_VertexShader

// Simple_VertexShader.c
typedef struct
{
// Handle to a program object
GLuint programObject;

// Uniform locations
GLint mvpLoc;

// Vertex daata
GLfloat *vertices;
GLuint *indices;
int numIndices;

// Rotation angle
GLfloat angle;

// MVP matrix
ESMatrix mvpMatrix;
} UserData;

///
// Initialize the shader and program object
//
int Init ( ESContext *esContext )
{
UserData *userData = esContext->userData;
const char vShaderStr[] =
"#version 300 es \n"
"uniform mat4 u_mvpMatrix; \n"
"layout(location = 0) in vec4 a_position; \n"
"layout(location = 1) in vec4 a_color; \n"
"out vec4 v_color; \n"
"void main() \n"
"{ \n"
" v_color = a_color; \n"
" gl_Position = u_mvpMatrix * a_position; \n"
"} \n";

const char fShaderStr[] =
"#version 300 es \n"
"precision mediump float; \n"
"in vec4 v_color; \n"
"layout(location = 0) out vec4 outColor; \n"
"void main() \n"
"{ \n"
" outColor = v_color; \n"
"} \n";

// Load the shaders and get a linked program object
userData->programObject = esLoadProgram ( vShaderStr, fShaderStr );

// Get the uniform locations
userData->mvpLoc = glGetUniformLocation ( userData->programObject, "u_mvpMatrix" );

// Generate the vertex data
userData->numIndices = esGenCube ( 1.0, &userData->vertices,
NULL, NULL, &userData->indices );

// Starting rotation angle for the cube
userData->angle = 45.0f;

glClearColor ( 1.0f, 1.0f, 1.0f, 0.0f );
return GL_TRUE;
}


///
// Update MVP matrix based on time
//
void Update ( ESContext *esContext, float deltaTime )
{
UserData *userData = esContext->userData;
ESMatrix perspective;
ESMatrix modelview;
float aspect;

// Compute a rotation angle based on time to rotate the cube
userData->angle += ( deltaTime * 40.0f );

if ( userData->angle >= 360.0f )
{
userData->angle -= 360.0f;
}

// Compute the window aspect ratio
aspect = ( GLfloat ) esContext->width / ( GLfloat ) esContext->height;

// Generate a perspective matrix with a 60 degree FOV
esMatrixLoadIdentity ( &perspective );
esPerspective ( &perspective, 60.0f, aspect, 1.0f, 20.0f );

// Generate a model view matrix to rotate/translate the cube
esMatrixLoadIdentity ( &modelview );

// Translate away from the viewer
esTranslate ( &modelview, 0.0, 0.0, -2.0 );

// Rotate the cube
esRotate ( &modelview, userData->angle, 1.0, 0.0, 1.0 );

// Compute the final MVP by multiplying the
// modevleiw and perspective matrices together
esMatrixMultiply ( &userData->mvpMatrix, &modelview, &perspective );
}

///
// Draw a triangle using the shader pair created in Init()
//
void Draw ( ESContext *esContext )
{
UserData *userData = esContext->userData;

// Set the viewport
glViewport ( 0, 0, esContext->width, esContext->height );

// Clear the color buffer
glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

// Use the program object
glUseProgram ( userData->programObject );

// Load the vertex position
glVertexAttribPointer ( 0, 3, GL_FLOAT,
GL_FALSE, 3 * sizeof ( GLfloat ), userData->vertices );

glEnableVertexAttribArray ( 0 );

// Set the vertex color to red
glVertexAttrib4f ( 1, 1.0f, 0.0f, 0.0f, 1.0f );

// Load the MVP matrix
glUniformMatrix4fv ( userData->mvpLoc, 1, GL_FALSE, ( GLfloat * ) &userData->mvpMatrix.m[0][0] );

// Draw the cube
glDrawElements ( GL_TRIANGLES, userData->numIndices, GL_UNSIGNED_INT, userData->indices );
}

///
// Cleanup
//
void Shutdown ( ESContext *esContext )
{
UserData *userData = esContext->userData;

if ( userData->vertices != NULL )
{
free ( userData->vertices );
}

if ( userData->indices != NULL )
{
free ( userData->indices );
}

// Delete program object
glDeleteProgram ( userData->programObject );
}


int esMain ( ESContext *esContext )
{
esContext->userData = malloc ( sizeof ( UserData ) );

esCreateWindow ( esContext, "Simple_VertexShader", 320, 240, ES_WINDOW_RGB | ES_WINDOW_DEPTH );

if ( !Init ( esContext ) )
{
return GL_FALSE;
}

esRegisterShutdownFunc ( esContext, Shutdown );
esRegisterUpdateFunc ( esContext, Update );
esRegisterDrawFunc ( esContext, Draw );

return GL_TRUE;
}