OpenGL ES之实现“大头小头”和“头部晃动”的效果
【摘要】 一、纹理映射 ① 什么是纹理 ?纹理(Texture)最通常的作用是装饰 3D 物体,它就像贴纸一样贴在物体表面,丰富了物体的表面和细节。在 OpenGL ES 开发中,纹理除了用于装饰物体表面,还可以用来作为存储数据的容器。那么,在 OpenGL ES 中,纹理实际上是一个可以被采样的复杂数据集合,是 GPU 的图像数据结构,纹理分为 2D 纹理、 立方图纹理和 3D 纹理:2D 纹理是...
一、纹理映射
① 什么是纹理 ?
- 纹理(Texture)最通常的作用是装饰 3D 物体,它就像贴纸一样贴在物体表面,丰富了物体的表面和细节。在 OpenGL ES 开发中,纹理除了用于装饰物体表面,还可以用来作为存储数据的容器。
- 那么,在 OpenGL ES 中,纹理实际上是一个可以被采样的复杂数据集合,是 GPU 的图像数据结构,纹理分为 2D 纹理、 立方图纹理和 3D 纹理:
-
- 2D 纹理是 OpenGL ES 中最常用和最常见的纹理形式,是一个图像数据的二维数组,纹理中的一个单独数据元素称为纹素或纹理像素;
-
- 立方图纹理是一个由 6 个单独的 2D 纹理面组成的纹理,立方图纹理像素的读取通过使用一个三维坐标(s,t,r)作为纹理坐标;
-
- 3D 纹理可以看作 2D 纹理作为切面的一个数组,类似于立方图纹理,使用三维坐标对其进行访问。
② 什么是纹理映射 ?
- 在 OpenGL ES 中,纹理映射就是通过为图元的顶点坐标指定恰当的纹理坐标,通过纹理坐标在纹理图中选定特定的纹理区域,最后通过纹理坐标与顶点的映射关系,将选定的纹理区域映射到指定图元上。
- 纹理映射也称为纹理贴图,简单地说就是将纹理坐标(纹理坐标系)所指定的纹理区域,映射到顶点坐标(渲染坐标系或OpenGL ES 坐标系)对应的渲染区域。
- 4 个纹理坐标分别为 T0(0,0),T1(0,1),T2(1,1),T3(1,0):
- 4 个纹理坐标对于的顶点坐标分别为 V0(-1,0.5),V1(-1, -0.5),V2(1,-0.5),V3(1,0.5):
- 由于 OpenGL ES 绘制是以三角形为单位的,设置绘制的 2 个三角形为 V0V1V2 和 V0V2V3。当调整纹理坐标的顺序保持顶点坐标的顺序不变,如 T0T1T2T3 -> T1T2T3T0 ,绘制后将得到一个顺时针旋转 90 度的纹理贴图,因此调整纹理坐标和顶点坐标的对应关系可以实现纹理图简单的旋转。
③ 纹理映射的实现
- 纹理映射的一般步骤:
-
- 生成纹理,编译链接着色器程序;
-
- 确定纹理坐标及对应的顶点坐标;
-
- 加载图像数据到纹理,加载纹理坐标和顶点坐标到着色器程序;
-
- 绘制。
- 生成纹理并加载图像数据到纹理:
// 生成一个纹理,将纹理 id 赋值给 m_TextureId
glGenTextures(1, &m_TextureId);
// 将纹理 m_TextureId 绑定到类型 GL_TEXTURE_2D 纹理
glBindTexture(GL_TEXTURE_2D, m_TextureId);
// 设置纹理 S 轴(横轴)的拉伸方式为截取
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
// 设置纹理 T 轴(纵轴)的拉伸方式为截取
glTexParameterf(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);
// 加载 RGBA 格式的图像数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_RenderImage.width, m_RenderImage.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_RenderImage.ppPlane[0]);
- 对纹理采样的片元着色器脚本:
#version 300 es
precision mediump float;
in vec2 v_texCoord;
layout(location = 0) out vec4 outColor;
uniform sampler2D s_TextureMap; // 声明采用器
void main() {
// texture() 为内置的采样函数,v_texCoord 为顶点着色器传进来的纹理坐标
// 根据纹理坐标对纹理进行采样,输出采样的 rgba 值(4维向量)
outColor = texture(s_TextureMap, v_texCoord);
}
- 实现代码:
// 生成纹理,编译链接着色器程序
void TextureMapSample::Init() {
//create RGBA texture
glGenTextures(1, &m_TextureId);
glBindTexture(GL_TEXTURE_2D, m_TextureId);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(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);
glBindTexture(GL_TEXTURE_2D, GL_NONE);
char vShaderStr[] =
"#version 300 es \n"
"layout(location = 0) in vec4 a_position; \n"
"layout(location = 1) in vec2 a_texCoord; \n"
"out vec2 v_texCoord; \n"
"void main() \n"
"{ \n"
" gl_Position = a_position; \n"
" v_texCoord = a_texCoord; \n"
"} \n";
char fShaderStr[] =
"#version 300 es \n"
"precision mediump float; \n"
"in vec2 v_texCoord; \n"
"layout(location = 0) out vec4 outColor; \n"
"uniform sampler2D s_TextureMap; \n"
"void main() \n"
"{ \n"
" outColor = texture(s_TextureMap, v_texCoord); \n"
"} \n";
m_ProgramObj = GLUtils::CreateProgram(vShaderStr, fShaderStr, m_VertexShader, m_FragmentShader);
if (m_ProgramObj) {
m_SamplerLoc = glGetUniformLocation(m_ProgramObj, "s_TextureMap");
} else {
LOGCATE("TextureMapSample::Init create program fail");
}
}
// 加载图像数据、纹理坐标和顶点坐标数据,绘制实现纹理映射
void TextureMapSample::Draw(int screenW, int screenH) {
LOGCATE("TextureMapSample::Draw()");
if(m_ProgramObj == GL_NONE || m_TextureId == GL_NONE) return;
GLfloat verticesCoords[] = {
-1.0f, 0.5f, 0.0f, // Position 0
-1.0f, -0.5f, 0.0f, // Position 1
1.0f, -0.5f, 0.0f, // Position 2
1.0f, 0.5f, 0.0f, // Position 3
};
GLfloat textureCoords[] = {
0.0f, 0.0f, // TexCoord 0
0.0f, 1.0f, // TexCoord 1
1.0f, 1.0f, // TexCoord 2
1.0f, 0.0f // TexCoord 3
};
GLushort indices[] = { 0, 1, 2, 0, 2, 3 };
// upload RGBA image data
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureId);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_RenderImage.width, m_RenderImage.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, m_RenderImage.ppPlane[0]);
glBindTexture(GL_TEXTURE_2D, GL_NONE);
// Use the program object
glUseProgram (m_ProgramObj);
// Load the vertex position
glVertexAttribPointer (0, 3, GL_FLOAT,
GL_FALSE, 3 * sizeof (GLfloat), verticesCoords);
// Load the texture coordinate
glVertexAttribPointer (1, 2, GL_FLOAT,
GL_FALSE, 2 * sizeof (GLfloat), textureCoords);
glEnableVertexAttribArray (0);
glEnableVertexAttribArray (1);
// Bind the RGBA map
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureId);
// Set the RGBA map sampler to texture unit to 0
glUniform1i(m_SamplerLoc, 0);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices);
}
二、大头小头效果
- 利用“纹理映射”的基本原理,就可以很轻易的实现对图像指定的区域进行拉伸和缩放。
- 典型的纹理映射着色器:
// 顶点着色器
#version 300 es
layout(location = 0) in vec4 a_position;
layout(location = 1) in vec2 a_texCoord;
uniform mat4 u_MVPMatrix;
out vec2 v_texCoord;
void main() {
gl_Position = u_MVPMatrix * a_position;
v_texCoord = a_texCoord;
}
// 片段着色器
#version 300 es
precision highp float;
layout(location = 0) out vec4 outColor;
in vec2 v_texCoord;
uniform sampler2D s_TextureMap;
void main() {
outColor = texture(s_TextureMap, v_texCoord);
}
- 纹理映射一般都是发生在规则的矩形区域,而大头小头效果实际上是对不规则的脸部区域进行缩放。这时就不能按照规则的矩形来划分网格,原因有两个:
-
- 因为只想形变发生在头部区域,而规则的矩形网格会导致图像背景发生畸变;
-
- 通过规则的矩形网格难以控制对头部(不规则)区域的形变程度。
- 为了防止背景发生严重的畸变,可以设计如下所示辐射状的网格结构,对头部区域进行形变就需要知道头部区域的关键点,头部区域的关键点可以通过 AI 算法来获得:
- 为了展示方便,将头部区域的关键点简化为 9 个,其中 8 个点位于头部边缘,一个点位于头部中心位置。直线 x=1、y=1 和纹理坐标轴连成了一个矩形,每个头部边缘的关键点和头部中心点确定一条直线,该直线会与矩形的边存在交点,用这些交点和头部关键点来构建这个呈辐射状的网格。
- 如下所示,每个头部边缘关键点和头部中心点确定一条直线,这条直线可以用二元一次方程来表示,它与上述矩形边的交点,可以通过求解二元一次方程得出:
- 通过关键点计算出交点的函数如下(inputPoint 表示头部边缘关键点,centerPoint 表示头部中心点,DotProduct 函数表示计算两个向量的点积):
vec2 BigHeadSample::CalculateIntersection(vec2 inputPoint, vec2 centerPoint) {
vec2 outputPoint;
// 直线与 y 轴平行
if(inputPoint.x == centerPoint.x) {
vec2 pointA(inputPoint.x, 0);
vec2 pointB(inputPoint.x, 1);
float dA = distance(inputPoint, pointA);
float dB = distance(inputPoint, pointB);
outputPoint = dA > dB ? pointB : pointA;
return outputPoint;
}
// 直线与 x 轴平行
if(inputPoint.y == centerPoint.y) {
vec2 pointA(0, inputPoint.y);
vec2 pointB(1, inputPoint.y);
float dA = distance(inputPoint, pointA);
float dB = distance(inputPoint, pointB);
outputPoint = dA > dB ? pointB : pointA;
return outputPoint;
}
// y = a*x + c
float a=0, c=0;
a = (inputPoint.y - centerPoint.y) / (inputPoint.x - centerPoint.x);
c = inputPoint.y - a * inputPoint.x;
// x=0, x=1, y=0, y=1 四条线交点
// x=0
vec2 point_0(0, c);
float d0 = DotProduct((centerPoint - inputPoint),(centerPoint - point_0));
if(c >= 0 && c <= 1 && d0 > 0)
outputPoint = point_0;
// x=1
vec2 point_1(1, a + c);
float d1 = DotProduct((centerPoint - inputPoint),(centerPoint - point_1));
if((a + c) >= 0 && (a + c) <= 1 && d1 > 0)
outputPoint = point_1;
// y=0
vec2 point_2(-c / a, 0);
float d2 = DotProduct((centerPoint - inputPoint),(centerPoint - point_2));
if((-c / a) >= 0 && (-c / a) <= 1 && d2 > 0)
outputPoint = point_2;
// y=1
vec2 point_3((1-c) / a, 1);
float d3 = DotProduct((centerPoint - inputPoint),(centerPoint - point_3));
if(((1-c) / a) >= 0 && ((1-c) / a) <= 1 && d3 > 0)
outputPoint = point_3;
return outputPoint;
}
- 在纹理坐标系上构建好辐射状的网格之后,需要进行坐标系变换,即将纹理坐标系转换为渲染坐标系(屏幕坐标系),得到纹理坐标所对应的顶点坐标:
// 纹理将坐标系转换为渲染坐标系(屏幕坐标系)的对应关系
(x,y)->(2*x-1, 1-2*y)
- 另外,控制头部变大和变小实际上是,通过控制头部边缘关键点对应顶点坐标的相对位置来实现的,当头部边缘关键点对应的顶点坐标靠近头部中心点时,头部变小,远离头部中心点时,反之变大。如下所示,头部边缘关键点对应的顶点坐标靠近头部中心点,在计算上可以通过点与向量相加来实现:
- 点与向量相加的几何意义是点按照向量的方向移动一定的距离,该向量可以通过头部中心点坐标减去边缘关键点坐标得出。移动边缘关键点的函数:
// input 为边缘关键点,centerPoint 为头部中心点,level 控制移动的距离
vec2 BigHeadSample::WarpKeyPoint(vec2 input, vec2 centerPoint, float level) {
vec2 output;
vec2 direct_vec = centerPoint - input;
output = input + level * direct_vec * 0.3f;
return output;
}
- 更新移动后的关键点坐标,绘制图像:
// 设置视口
glViewport(0, 0, screenW, screenH);
m_FrameIndex ++;
// 变换矩阵
UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_AngleY, (float)screenW / screenH);
// 强度
float ratio = (m_FrameIndex % 100) * 1.0f / 100;
ratio = (m_FrameIndex / 100) % 2 == 1 ? (1 - ratio) : ratio;
// 计算新的网格
CalculateMesh(ratio - 0.5f);
// 更新顶点数组
glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(m_Vertices), m_Vertices);
// 绘制图像
glUseProgram (m_ProgramObj);
glBindVertexArray(m_VaoId);
glUniformMatrix4fv(m_MVPMatLoc, 1, GL_FALSE, &m_MVPMatrix[0][0]);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureId);
glUniform1i(m_SamplerLoc, 0);
glDrawArrays(GL_TRIANGLES, 0, TRIANGLE_COUNT * 3);
三、头部晃动效果
- 那么如何实现如下的头部晃动的效果呢?其实,就是控制头部关键点的位置。简而言之就是,控制头部所有关键点统一按照某一圆的轨迹进行移动,这里指的头部关键点是在屏幕坐标系中纹理坐标所对应的点。
- 实现关键点按照某一圆的轨迹进行移动的函数(input 为头部关键点,rotaryAngle 为转动角度):
vec2 RotaryHeadSample::RotaryKeyPoint(vec2 input, float rotaryAngle) {
return input + vec2(cos(rotaryAngle), sin(rotaryAngle)) * 0.02f; // 0.02f 表示圆的半径
}
- 更新移动后的关键点坐标,绘制图像:
// 设置视口
glViewport(0, 0, screenW, screenH);
m_FrameIndex ++;
// 变换矩阵
UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_AngleY, (float)screenW / screenH);
float ratio = (m_FrameIndex % 100) * 1.0f / 100;
// 计算新的网格
CalculateMesh(static_cast<float>(ratio * 2 * MATH_PI));
// 更新顶点数组
glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(m_Vertices), m_Vertices);
// 绘制图像
glUseProgram (m_ProgramObj);
glBindVertexArray(m_VaoId);
glUniformMatrix4fv(m_MVPMatLoc, 1, GL_FALSE, &m_MVPMatrix[0][0]);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureId);
glUniform1i(m_SamplerLoc, 0);
glDrawArrays(GL_TRIANGLES, 0, TRIANGLE_COUNT * 3);
- 去掉网格后的效果:
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)