P 图功能与 OpenGL
玩过 P 图软件的朋友一定对这个功能有所了解,P 图我们可以简单地看做把一个区域的像素按照某一方向进行移动,产生一定形变效果,基于这个原理,我们可以手动实现瘦脸、长腿、瘦腰、大眼、丰胸等等一系列效果,从而达到美颜、美型的目的。
我们将一个区域的像素移走以后,那么用什么来填充这个被"掏空"的区域呢?答案是, OpenGL 自带插值功能会使用周围的像素对被"掏空"的区域进行插值填充。
回想下 OpenGL 纹理贴图,将图像贴到相对大的区域,就会产生拉伸的效果,贴到相对更小的区域就会产生挤压的效果,这都是借助于 OpenGL 的双线性插值算法实现。
对纹理贴图不了解的同学可以移步:Android OpenGL ES 系统性学习教程
所以,当我们选中一块图像区域进行移动时,OpenGL 纹理贴图时会在移动的方向上产生挤压的效果,而反方向便会产生拉伸效果,从而可以实现对人体部位形变效果。
OpenGL 实现 P 图功能
根据上节讨论的原理,我们把选定位图像区域看成一个圆形,圆形之外的区域不进行偏移形变(不受影响),圆内的区域的像素则是越靠近圆心移动位移相对越大。
如上图所示,BC 表示偏移方向和偏移程度的向量,将圆内的所有像素按照向量 BC 的方向进行一定程度的偏移,像素偏移的强度,和像素与圆心的距离相关,越靠近圆心强度越大。
再回想下纹理贴图(纹理映射)那篇文章,我们只是将图像映射到一个网格(2个三角形组成),这是我们只能对整图做形变,无法做到对如脸部等一小块具体的区域做形变。
以此类比,这个时候我们就需要更多的网格,当网格足够密集,就可以覆盖整张图的全部区域,这个时候我们通过调整网格,可以实现对任意区域的形变,从而手动实现瘦脸、长腿、瘦腰、大眼、丰胸等等效果不在话下。
生成更多的网格实际上是为了能控制一小块网格区域图像的形变,也就是一定范围内网格区域图像的形变,不对这个范围外的图像产生影响。
所以,剩下来的问题就是生成很多网格,然后控制网格结点的偏移,通过简单纹理映射实现 P 图功能。
生成网格及对应的顶点坐标和纹理坐标:
代码语言:txt复制/**
*
* @param verticalNum 垂直方向网格数
* @param horizonNum 水平方向网格数
* @param ppVertices 顶点坐标集合
* @param ppTexCoor 纹理坐标集合
*/
void GLRender::GenVertexMesh(int verticalNum, int horizonNum, float **ppVertices, float **ppTexCoor)
{
*ppVertices = static_cast<float *>(malloc(verticalNum * horizonNum * 18 * sizeof(float)));
*ppTexCoor = static_cast<float *>(malloc(verticalNum * horizonNum * 12 * sizeof(float)));
m_pStatusTexCoords = static_cast<float *>(malloc(verticalNum * horizonNum * 12 * sizeof(float)));
float dS = 1.0f / horizonNum;
float dT = 1.0f / verticalNum;
for (int i = 0; i < horizonNum; i) //S
{
float s0 = i * dS;
float s1 = (1 i) * dS;
for (int j = 0; j < verticalNum; j) //T
{
float t0 = j * dT;
float t1 = (1 j) * dT;
int meshIndex = j * horizonNum i;
(*ppTexCoor)[meshIndex * 12 0] = s0;
(*ppTexCoor)[meshIndex * 12 1] = t0;
(*ppTexCoor)[meshIndex * 12 2] = s0;
(*ppTexCoor)[meshIndex * 12 3] = t1;
(*ppTexCoor)[meshIndex * 12 4] = s1;
(*ppTexCoor)[meshIndex * 12 5] = t0;
(*ppTexCoor)[meshIndex * 12 6] = s1;
(*ppTexCoor)[meshIndex * 12 7] = t0;
(*ppTexCoor)[meshIndex * 12 8] = s0;
(*ppTexCoor)[meshIndex * 12 9] = t1;
(*ppTexCoor)[meshIndex * 12 10] = s1;
(*ppTexCoor)[meshIndex * 12 11] = t1;
// vertex coordinate
(*ppVertices)[meshIndex * 18 0] = 2 * s0 - 1;
(*ppVertices)[meshIndex * 18 1] = 1 - 2 * t0;
(*ppVertices)[meshIndex * 18 2] = 0;
(*ppVertices)[meshIndex * 18 3] = 2 * s0 - 1;
(*ppVertices)[meshIndex * 18 4] = 1 - 2 * t1;
(*ppVertices)[meshIndex * 18 5] = 0;
(*ppVertices)[meshIndex * 18 6] = 2 * s1 - 1;
(*ppVertices)[meshIndex * 18 7] = 1 - 2 * t0;
(*ppVertices)[meshIndex * 18 8] = 0;
(*ppVertices)[meshIndex * 18 9] = 2 * s1 - 1;
(*ppVertices)[meshIndex * 18 10] = 1 - 2 * t0;
(*ppVertices)[meshIndex * 18 11] = 0;
(*ppVertices)[meshIndex * 18 12] = 2 * s0 - 1;
(*ppVertices)[meshIndex * 18 13] = 1 - 2 * t1;
(*ppVertices)[meshIndex * 18 14] = 0;
(*ppVertices)[meshIndex * 18 15] = 2 * s1 - 1;
(*ppVertices)[meshIndex * 18 16] = 1 - 2 * t1;
(*ppVertices)[meshIndex * 18 17] = 0;
}
}
memcpy(m_pStatusTexCoords, (*ppTexCoor), verticalNum * horizonNum * 12 * sizeof(float));
}
在圆的范围内控制网格结点的偏移,圆形之外的区域不进行偏移形变(不受影响),圆内的区域的像素则是越靠近圆心移动位移相对越大。
代码语言:txt复制//prePoint 圆心,radius 半径
void GLRender::UpdateVertexMeshWithLinear(PointF prePoint, PointF curPoint, float radius, float *pVertices)
{
int pointNum = m_MeshNum * 6;
float textureWidth = m_RenderImg.width;
float textureHeight = m_RenderImg.height;
// 转化为图片坐标
float imgRadius = radius * textureWidth;
PointF imgSize = {textureWidth, textureHeight};
PointF imgPrePoint = prePoint * imgSize;
PointF imgCurPoint = curPoint * imgSize;
PointF texPoint;
for (int i = 0; i < pointNum; i)
{
texPoint.x = m_TexCoords[i * 2 0];
texPoint.y = m_TexCoords[i * 2 1];
PointF imgTexPoint = texPoint * imgSize;
float r = distance(imgTexPoint, imgPrePoint);
//判断是否在圆的范围内
if (r < imgRadius)
{
//越靠近圆心偏移越大
float alpha = 1.0f - r / imgRadius;
//做个平滑
alpha = smoothstep(0.f, 1.f, alpha);
//移动方向
PointF dVec = (imgCurPoint - imgPrePoint) * pow(alpha, 2.0f);
//乘以一个系数,不然偏移太大了
dVec = dVec * 0.08f;
//移动后的网格结点
PointF newImgTexPoint = imgTexPoint dVec;
//归一化
newImgTexPoint = newImgTexPoint / imgSize;
//更新对应的纹理坐标和顶点坐标
m_TexCoords[i * 2 0] = newImgTexPoint.x;
m_TexCoords[i * 2 1] = newImgTexPoint.y;
pVertices[i * 3 0] = 2 * newImgTexPoint.x - 1;
pVertices[i * 3 1] = 1 - 2 * newImgTexPoint.y;
}
}
}
纹理贴图使用的 shader,一个常规的纹理采样。
代码语言:txt复制const 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"
"uniform mat4 u_MVPMatrix; n"
"void main() n"
"{ n"
" gl_Position = u_MVPMatrix * a_position; n"
" v_texCoord = a_texCoord; n"
"} n";
const 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"
"}";
推荐参考项目:https://github.com/githubhaohao/NDK_OpenGLES_3_0