在介绍美颜定制之前,我们先来复习一下OpenGL中图像绘制原理。OpenGL的图像绘制,是由许许多多三角形构成的。OpenGL的绘制离不开三角形的绘制。通常对于不需要对图像细节进行处理的时候,我们一般会使用glDrawArrays方法将整张图片绘制处理。但如果要对图像的某一个部分进行形变等微调,这时候通常将图像划分为许许多多的三角形。比如MLS算法原理就是通过调整三角形的顶点位置实现图像形变的。将一张图像划分为许许多多的三角形之后,使用glDrawArrays就不够划算了,由于glDrawArrays在图像有多个连续的三角形构成的时候,会出现许多重复的边,这里面不仅仅产生比较大的内存开销,也对CPU到GPU传递数据的带宽造成一定的影响,对于移动端来说,内存和带宽都比较受限。这时候,使用glDrawElements是一个比较好的方式。
人脸三角形索引构建
本人将结合美颜类相机的美型处理用到的技术,详细介绍glDrawElements的用法。
我用的是Face++免费提供的人脸关键点检测SDK,虽然免费使用有设备和次数限制,但对于验证来说,足够了,在此感谢Face++的帮助。根据Face++的SDK的文档,106个关键点如下图所示:
我们在得到人脸关键点后,需要对关键点进行三角划分,三角剖分算法通常是Delaunay Triangulation,关于Delaunay Triangulation 算法可以参考官方资料。这里不做介绍,这里并不是重点。由于人脸关键点的位置是相对固定的,人脸关键点内部的关系是固定的,因此在实时预览的时候,并不需要每次都用Delaunay算法来对图像进行三角剖分,并且对于移动端来说,每次都用Delaunay进行三角划分是不实际的,不管是CPU负载还是手机发热等方面都是行不通的。我们只需要提前建立一个索引,配合人脸检测SDK得到的顶点坐标和纹理坐标就可以做三角划分以及图像重建工作了。
我们把上图中122个关键点连起来,可以得到以下的图像:
在得到索引数组之后,我们接下来对图像进行三角剖分和重建工作。
图像三角形绘制与图像重建
上一步,我们根据图像得到了三角剖分的索引,接下来我们需要使用glDrawElements方法将人脸识别得到的关键点的顶点和纹理坐标给计算出来。为了方便处理,在106个关键点基础上,添加眉心、脸颊、额头以及图像四周8个顶点坐标进来,然后将得到的顶点坐标和纹理坐标传递到Filter中进行处理。然后在绘制前更新顶点坐标和纹理左边缓冲Buffer,将坐标传给shader进行处理即可。
美颜定制分类
美型定制分类主要可以分成三大类:
美颜定制分类实现
一、美白、磨皮处理
磨皮可以参考本人关于实时美颜处理的优化文章:
Android OpenGLES 实时美颜(磨皮)的优化
Android OpenGLES 实时美颜(磨皮)的优化(二)
本项目主要基于第二篇文章的处理实现,具体的过程这里就不做重复介绍了。
肤色(美白)调节
肤色调节主要通过LUT将皮肤的色彩映射成白色的。我们需要限定皮肤的灰度范围,根据灰度范围进行映射调节即可。灰度纹理是一个256 x 1大小的问题,也就是通常说的一阶颜色查找表。另外,由于皮肤的颜色比较单一,我们可以将通用的512 x 512大小的Lookup Table缩放成64x64像素的,提高映射效率。由于实现起来比较简单,也不知该怎么说比较好,可以参考本人的开源相机CainCamera 中的GLImageBeautyComplexionFilter的实现,整个fragment shader 如下:
// 美肤滤镜
precision mediump float;
varying highp vec2 textureCoordinate;
uniform sampler2D inputTexture; // 图像texture
uniform sampler2D grayTexture; // 灰度查找表
uniform sampler2D lookupTexture; // LUT
uniform highp float levelRangeInv; // 范围
uniform lowp float levelBlack; // 灰度level
uniform lowp float alpha; // 肤色程度
void main() {
lowp vec3 textureColor = texture2D(inputTexture, textureCoordinate).rgb;
textureColor = clamp((textureColor - vec3(levelBlack, levelBlack, levelBlack)) * levelRangeInv, 0.0, 1.0);
textureColor.r = texture2D(grayTexture, vec2(textureColor.r, 0.5)).r;
textureColor.g = texture2D(grayTexture, vec2(textureColor.g, 0.5)).g;
textureColor.b = texture2D(grayTexture, vec2(textureColor.b, 0.5)).b;
mediump float blueColor = textureColor.b * 15.0;
mediump vec2 quad1;
quad1.y = floor(blueColor / 4.0);
quad1.x = floor(blueColor) - (quad1.y * 4.0);
mediump vec2 quad2;
quad2.y = floor(ceil(blueColor) / 4.0);
quad2.x = ceil(blueColor) - (quad2.y * 4.0);
highp vec2 texPos1;
texPos1.x = (quad1.x * 0.25) + 0.5 / 64.0 + ((0.25 - 1.0 / 64.0) * textureColor.r);
texPos1.y = (quad1.y * 0.25) + 0.5 / 64.0 + ((0.25 - 1.0 / 64.0) * textureColor.g);
highp vec2 texPos2;
texPos2.x = (quad2.x * 0.25) + 0.5 / 64.0 + ((0.25 - 1.0 / 64.0) * textureColor.r);
texPos2.y = (quad2.y * 0.25) + 0.5 / 64.0 + ((0.25 - 1.0 / 64.0) * textureColor.g);
lowp vec4 newColor1 = texture2D(lookupTexture, texPos1);
lowp vec4 newColor2 = texture2D(lookupTexture, texPos2);
lowp vec3 newColor = mix(newColor1.rgb, newColor2.rgb, fract(blueColor));
textureColor = mix(textureColor, newColor, alpha);
gl_FragColor = vec4(textureColor, 1.0);
二、通过遮罩纹理进行颜色变换/映射处理的
亮眼的实现并不算什么难度,一句话概括就是—— 把眼睛白色部分变得更白,黑色部分变得更黑。为了得到颜色差值,我们需要得到一张比较平滑的图像,再与原图比较才能得到明显的颜色差。因此,实现思路如下:
1、我们需要对图像进行高斯模糊,这里需要不同的kernel进行高斯模糊处理,将眼睛部分与输入原图的颜色差值更加明显
2、将得到的高斯模糊图像与输入图像进行比较,得到颜色差,再将颜色差值放大几倍。最后做线性混合处理即可。
实现代码如下:
// 高斯模糊的图像颜色值
vec4 blurColor = texture2D(blurTexture, textureCoordinate);
// 统计颜色
vec3 sumColor = vec3(0.0, 0.0, 0.0);
// 将RGB颜色差值放大。突出眼睛明亮部分
sumColor = clamp((sourceColor.rgb - blurColor.rgb) * 6.0, 0.0, 1.0);
sumColor = max(sourceColor.rgb, sumColor);
// 用原图和最终得到的明亮部分进行线性混合处理
color = mix(sourceColor, vec4(sumColor, 1.0), strength);
如果整张图片做这个处理,将会是这样的:
可以看到颜色差比较明显。你可以将6.0改小一点就没那么明显过爆的感觉了,这里可以根据实际需要进行调节。
这里有个问题,由于是整图处理的,所以图像其他地方也出现了明显的颜色差变化,我们不需要除了眼睛外的其他地方颜色差发生变化,因此我们需要添加眼睛部分的遮罩纹理进来做处理。
眼睛遮罩图如下:
private static final float[] mEyeMaskTextureVertices = new float[] {
0.102757f, 0.465517f,
0.175439f, 0.301724f,
0.370927f, 0.310345f,
0.446115f, 0.603448f,
0.353383f, 0.732759f,
0.197995f, 0.689655f,
0.566416f, 0.629310f,
0.659148f, 0.336207f,
0.802005f, 0.318966f,
0.884712f, 0.465517f,
0.812030f, 0.681034f,
0.681704f, 0.750023f,
0.273183f, 0.241379f,
0.275689f, 0.758620f,
0.721805f, 0.275862f,
0.739348f, 0.758621f,
眼睛部分的三角剖分索引如下:
* 眼睛部分索引
private static final short[] mEyeIndices = new short[] {
0, 5, 1,
1, 5, 12,
12, 5, 13,
12, 13, 4,
12, 4, 2,
2, 4, 3,
6, 7, 11,
7, 11, 14,
14, 11, 15,
14, 15, 10,
14, 10, 8,
8, 10, 9
同时,我们要取得某个人脸的眼睛的顶点坐标:
* 取得亮眼需要的顶点坐标
* @param vertexPoints
* @param faceIndex
public synchronized void getBrightEyeVertices(float[] vertexPoints, int faceIndex) {
if (vertexPoints == null || vertexPoints.length < 32
|| faceIndex >= mFaceArrays.size() || mFaceArrays.get(faceIndex) == null) {
return;
// 眼睛边沿部分 index = 0 ~ 11
for (int i = 52; i < 64; i++) {
vertexPoints[(i - 52) * 2] = mFaceArrays.get(faceIndex).vertexPoints[i * 2];
vertexPoints[(i - 52) * 2 + 1] = mFaceArrays.get(faceIndex).vertexPoints[i * 2 + 1];
vertexPoints[12 * 2] = mFaceArrays.get(faceIndex).vertexPoints[72 * 2];
vertexPoints[12 * 2 + 1] = mFaceArrays.get(faceIndex).vertexPoints[72 * 2 + 1];
vertexPoints[13 * 2] = mFaceArrays.get(faceIndex).vertexPoints[73 * 2];
vertexPoints[13 * 2 + 1] = mFaceArrays.get(faceIndex).vertexPoints[73 * 2 + 1];
vertexPoints[14 * 2] = mFaceArrays.get(faceIndex).vertexPoints[75 * 2];
vertexPoints[14 * 2 + 1] = mFaceArrays.get(faceIndex).vertexPoints[75 * 2 + 1];
vertexPoints[15 * 2] = mFaceArrays.get(faceIndex).vertexPoints[76 * 2];
vertexPoints[15 * 2 + 1] = mFaceArrays.get(faceIndex).vertexPoints[76 * 2 + 1];
至此,我们准备好了需要绘制的遮罩纹理、遮罩纹理坐标、图像顶点坐标、眼睛部分的索引。所有数据都准备好了。我们就可以用这些数据进行亮眼处理了。绘制阶段,我们只需要将对应的遮罩纹理坐标传给shader,使用glDrawElements绘制三角形即可。
单一亮眼处理的完整shader如下:
// 亮眼处理
precision highp float;
varying vec2 textureCoordinate;
varying vec2 maskCoordinate;
uniform sampler2D inputTexture; // 输入图像纹理
uniform sampler2D blurTexture; // 经过高斯模糊处理的图像纹理
uniform sampler2D maskTexture; // 眼睛遮罩图像纹理
uniform float strength; // 明亮程度
uniform int enableProcess; // 是否允许亮眼,没有人脸时不需要亮眼处理
void main()
vec4 sourceColor = texture2D(inputTexture, textureCoordinate);
vec4 maskColor = texture2D(maskTexture, maskCoordinate);
vec4 color = sourceColor;
if (enableProcess == 1) {
// 如果遮罩纹理存在
if (maskColor.r > 0.01) {
// 高斯模糊的图像颜色值
vec4 blurColor = texture2D(blurTexture, textureCoordinate);
// 统计颜色
vec3 sumColor = vec3(0.0, 0.0, 0.0);
// 将RGB颜色差值放大。突出眼睛明亮部分
sumColor = clamp((sourceColor.rgb - blurColor.rgb) * 3.0, 0.0, 1.0);
sumColor = max(sourceColor.rgb, sumColor);
// 用原图和最终得到的明亮部分进行线性混合处理
color = mix(sourceColor, vec4(sumColor, 1.0), strength * maskColor.r);
gl_FragColor = color;
跟前面的亮眼处理类似,都需要遮罩只对嘴巴部分进行映射处理。牙齿美白过程则跟肤色调节处理一样,通过颜色查找表进行映射处理。我们首先要得到嘴巴的遮罩纹理,并对其进行三角剖分,入下图所示:
private static final float[] mTeethMaskTextureVertices = new float[] {
0.154639f, 0.378788f,
0.295533f, 0.287879f,
0.398625f, 0.196970f,
0.512027f, 0.287879f,
0.611684f, 0.212121f,
0.728523f, 0.287879f,
0.872852f, 0.378788f,
0.742268f, 0.704546f,
0.639176f, 0.848485f,
0.522337f, 0.636364f,
0.398625f, 0.833333f,
0.240550f, 0.651515f,
美牙所需要的索引:
* 美牙索引
private static final short[] mTeethIndices = new short[] {
0, 11, 1,
1, 11, 10,
1, 10, 2,
2, 10, 3,
3, 10, 9,
3, 9, 8,
3, 8, 4,
4, 8, 5,
5, 8, 7,
5, 7, 6,
以及某个人脸上嘴巴顶点的坐标:
* 取得美牙需要的顶点坐标,嘴巴周围12个顶点
* @param vertexPoints
* @param faceIndex
public synchronized void getBeautyTeethVertices(float[] vertexPoints, int faceIndex) {
if (vertexPoints == null || vertexPoints.length < 24
|| faceIndex >= mFaceArrays.size() || mFaceArrays.get(faceIndex) == null) {
return;
for (int i = 84; i < 96; i++) {
vertexPoints[(i - 84) * 2] = mFaceArrays.get(faceIndex).vertexPoints[i * 2];
vertexPoints[(i - 84) * 2 + 1] = mFaceArrays.get(faceIndex).vertexPoints[i * 2 + 1];
在得到了遮罩纹理坐标、绘制需要的索引、人脸嘴巴顶点坐标后,我们就可以做美牙处理了。由于牙齿的颜色比较单一,整体都是白偏黄。因此颜色查找表可以用 64 x 64大小。shader代码如下:
vec4 maskColor = texture2D(maskTexture, maskCoordinate.xy);
if (maskColor.r > 0.001) {
mediump float blueColor = sourceColor.b * 15.0;
vec2 quad1;
vec2 quad2;
quad1.y = floor(floor(blueColor) * 0.25);
quad1.x = floor(blueColor) - (quad1.y * 4.0);
quad2.y = floor(ceil(blueColor) * 0.25);
quad2.x = ceil(blueColor) - (quad2.y * 4.0);
vec2 texPos1;
vec2 texPos2;
texPos1.x = (quad1.x * 0.25) + 0.0078125 + (0.234375 * sourceColor.r);
texPos1.y = (quad1.y * 0.25) + 0.0078125 + (0.234375 * sourceColor.g);
texPos2.x = (quad2.x * 0.25) + 0.0078125 + (0.234375 * sourceColor.r);
texPos2.y = (quad2.y * 0.25) + 0.0078125 + (0.234375 * sourceColor.g);
lowp vec3 newColor1 = texture2D(teethLookupTexture, texPos1).rgb;
lowp vec3 newColor2 = texture2D(teethLookupTexture, texPos2).rgb;
lowp vec3 newColor = mix(newColor1, newColor2, fract(blueColor));
color = vec4(mix(sourceColor.rgb, newColor, teethStrength * maskColor.r), 1.0);
法令纹理、卧蚕、眼袋处理这些由于没有遮罩纹理,这里就不再介绍了。处理思路跟亮眼差不多,都是找到颜色差之后进行颜色差消除、突出颜色差等处理实现。
这部分本人将其统称为人脸美化滤镜,可以参考本项目中的GLImageBeautyFaceFilter的实现。这里为了方便,将整个shader集中起来,利用不同的processType进行分类,shader如下:
vertex_beauty_face.glsl:
attribute vec4 aPosition; // 图像顶点坐标
attribute vec4 aTextureCoord; // 遮罩纹理坐标,这里是复用了原来的图像纹理坐标
varying vec2 textureCoordinate;
varying vec2 maskCoordinate;
void main() {
gl_Position = aPosition;
maskCoordinate = aTextureCoord.xy;
// 用顶点坐标来处理纹理坐标
textureCoordinate = aPosition.xy * 0.5 + 0.5;
fragment_beauty_face.glsl:
// 人脸美化处理
precision highp float;
varying vec2 textureCoordinate;
varying vec2 maskCoordinate;
uniform sampler2D inputTexture; // 输入图像纹理
uniform sampler2D blurTexture; // 经过高斯模糊处理的图像纹理
uniform sampler2D blurTexture2; // 经过高斯模糊处理的图像纹理2
uniform sampler2D maskTexture; // 遮罩图像纹理
uniform sampler2D teethLookupTexture; // 美牙的lookup table 纹理
uniform float brightEyeStrength; // 亮眼程度
uniform float teethStrength; // 美牙程度
uniform float nasolabialStrength; // 法令纹处理程度
uniform float furrowStrength; // 卧蚕处理程度
uniform float eyeBagStrength; // 眼袋处理程度
uniform int processType; // 处理类型, 1表示亮眼处理,2表示美牙处理,3表示消除法令纹,4表示消除卧蚕眼袋,其他类型则直接绘制原图
void main()
vec4 sourceColor = texture2D(inputTexture, textureCoordinate);
vec4 color = sourceColor;
if (processType == 1) { // 亮眼处理
// 如果遮罩纹理存在
vec4 maskColor = texture2D(maskTexture, maskCoordinate);
if (maskColor.r > 0.01) {
// 高斯模糊的图像颜色值
vec4 blurColor = texture2D(blurTexture2, textureCoordinate);
// 统计颜色
vec3 sumColor = vec3(0.0, 0.0, 0.0);
// 将RGB颜色差值放大。突出眼睛明亮部分
sumColor = clamp((sourceColor.rgb - blurColor.rgb) * 3.3, 0.0, 1.0);
sumColor = max(sourceColor.rgb, sumColor);
// 用原图和最终得到的明亮部分进行线性混合处理
color = mix(sourceColor, vec4(sumColor, 1.0), brightEyeStrength * maskColor.r);
} else if (processType == 2) { // 美牙处理
vec4 maskColor = texture2D(maskTexture, maskCoordinate.xy);
if (maskColor.r > 0.001) {
mediump float blueColor = sourceColor.b * 15.0;
vec2 quad1;
vec2 quad2;
quad1.y = floor(floor(blueColor) * 0.25);
quad1.x = floor(blueColor) - (quad1.y * 4.0);
quad2.y = floor(ceil(blueColor) * 0.25);
quad2.x = ceil(blueColor) - (quad2.y * 4.0);
vec2 texPos1;
vec2 texPos2;
texPos1.x = (quad1.x * 0.25) + 0.0078125 + (0.234375 * sourceColor.r);
texPos1.y = (quad1.y * 0.25) + 0.0078125 + (0.234375 * sourceColor.g);
texPos2.x = (quad2.x * 0.25) + 0.0078125 + (0.234375 * sourceColor.r);
texPos2.y = (quad2.y * 0.25) + 0.0078125 + (0.234375 * sourceColor.g);
lowp vec3 newColor1 = texture2D(teethLookupTexture, texPos1).rgb;
lowp vec3 newColor2 = texture2D(teethLookupTexture, texPos2).rgb;
lowp vec3 newColor = mix(newColor1, newColor2, fract(blueColor));
color = vec4(mix(sourceColor.rgb, newColor, teethStrength * maskColor.r), 1.0);
} else if (processType == 3) { // 消除法令纹
vec3 maskColor = texture2D(maskTexture, maskCoordinate.xy).rgb;
// 去除法令纹原理,用两张不同程度的高斯模糊图像差值比较,得到鼻唇沟附近的颜色差值比较,配合法令纹遮罩图像去除法令纹
if (maskColor.r > 0.01) {
vec3 blurColor1 = texture2D(blurTexture, textureCoordinate.xy).rgb;
vec3 blurColor2 = texture2D(blurTexture2, textureCoordinate.xy).rgb;
vec3 diffColor = clamp((blurColor2 - blurColor1) * 1.8 + 0.1 * blurColor2, 0.0, 0.5);
color = vec4(min(sourceColor.rgb + diffColor, 1.0), 1.0) * nasolabialStrength * maskColor.r;
} else if (processType == 4) {
vec4 maskColor = texture2D(maskTexture, maskCoordinate.xy);
if (maskColor.r > 0.005) { // 消除眼袋,用红色表示
vec3 blurColor1 = texture2D(blurTexture, textureCoordinate.xy).rgb;
vec3 blurColor2 = texture2D(blurTexture2, textureCoordinate.xy).rgb;
// 放大差值,用输入的图像加上差值,消除差值所带来的影响
vec3 diffColor = clamp((blurColor2 - blurColor1) * 2.0 + 0.05 * blurColor2, 0.0, 0.3);
color.rgb = mix(color.rgb, min(color.rgb + diffColor, 1.0), eyeBagStrength * maskColor.r);
} else if (maskColor.g > 0.005) { // 消除卧蚕,蓝色部分为卧蚕遮罩
color.rgb = mix(color.rgb, pow(color.rgb, vec3(0.5, 0.5, 0.5)), furrowStrength * maskColor.g);
gl_FragColor = color;
亮眼、美牙的效果如下:
三、调节像素偏移
调节像素偏移主要是用来处理瘦脸大眼、调节下巴等处理。前面我们做了图像三角剖分和重建工作,得到了整张人脸的索引。接下来我们只需要得到对应的顶点坐标、纹理坐标,更新顶点坐标缓冲、纹理坐标缓冲即可。这里需要根据是否有人脸来做顶点判断。其中updateCartesianVertices()方法是用来传递人脸关键点在图像中的实际笛卡尔坐标的,这里主要是为了方便做像素偏移计算:
if (LandmarkEngine.getInstance().hasFace()) {
LandmarkEngine.getInstance().updateFaceAdjustPoints(mVertices, mTextureVertices, 0);
mVertexBuffer.clear();
mVertexBuffer.put(mVertices);
mVertexBuffer.position(0);
mTextureBuffer.clear();
mTextureBuffer.put(mTextureVertices);
mTextureBuffer.position(0);
updateCartesianVertices();
mIndexBuffer.clear();
mIndexBuffer.put(FaceImageIndices);
mIndexBuffer.position(0);
mIndexLength = mIndexBuffer.capacity();
setInteger(mEnableReshapeHandle, 1);
} else { // 没有人脸时索引变回默认的6个
mIndexBuffer.clear();
mIndexBuffer.put(TextureRotationUtils.Indices);
mIndexBuffer.position(0);
mIndexLength = 6;
setInteger(mEnableReshapeHandle, 0);
接下来,我们就可以在fragment shader 中进行像素调节处理了。关于像素偏移调节,可以参考下面这篇文章,写得比较详细:
在OpenGL中利用shader进行实时瘦脸大眼等脸型微调
脸型调节的fragment shader 如下所示(截止本文章位置,部分脸型调节功能还没完成):
precision mediump float;
varying vec2 textureCoordinate;
uniform sampler2D inputTexture;
// 图像笛卡尔坐标系的关键点,也就是纹理坐标乘以宽高得到
uniform vec2 cartesianPoints[106];
#define INDEX_FACE_LIFT 0 // 瘦脸
#define INDEX_FACE_SHAVE 1 // 削脸
#define INDEX_FACE_NARROW 2 // 小脸
#define INDEX_CHIN 3 // 下巴
#define INDEX_FOREHEAD 4 // 额头
#define INDEX_EYE_ENLARGE 5 // 大眼
#define INDEX_EYE_DISTANCE 6 // 眼距
#define INDEX_EYE_CORNER 7 // 眼角
#define INDEX_NOSE_THIN 8 // 瘦鼻
#define INDEX_ALAE 9 // 鼻翼
#define INDEX_PROBOSCIS 10 // 长鼻
#define INDEX_MOUTH 11 // 嘴型
#define INDEX_SIZE 12 // 索引大小
// 美型程度参数列表
uniform float reshapeIntensity[INDEX_SIZE];
// 纹理宽度
uniform int textureWidth;
// 纹理高度
uniform int textureHeight;
// 是否允许美型处理,存在人脸时为1,没有人脸时为0
uniform int enableReshape;
// 曲线形变处理
vec2 curveWarp(vec2 textureCoord, vec2 originPosition, vec2 targetPosition, float radius)
vec2 offset = vec2(0.0);
vec2 result = vec2(0.0);
vec2 direction = targetPosition - originPosition;
float infect = distance(textureCoord, originPosition)/radius;
infect = 1.0 - infect;
infect = clamp(infect, 0.0, 1.0);
offset = direction * infect;
result = textureCoord - offset;
return result;
// 大眼处理
vec2 enlargeEye(vec2 currentCoordinate, vec2 circleCenter, float radius, float intensity)
float currentDistance = distance(currentCoordinate, circleCenter);
float weight = currentDistance / radius;
weight = 1.0 - intensity * (1.0 - weight * weight);
weight = clamp(weight, 0.0, 1.0);
currentCoordinate = circleCenter + (currentCoordinate - circleCenter) * weight;
return currentCoordinate;
// 瘦脸
vec2 faceLift(vec2 currentCoordinate, float faceLength)
vec2 coordinate = currentCoordinate;
vec2 currentPoint = vec2(0.0);
vec2 destPoint = vec2(0.0);
float faceLiftScale = reshapeIntensity[INDEX_FACE_LIFT] * 0.05;
float radius = faceLength;
currentPoint = cartesianPoints[3];
destPoint = currentPoint + (cartesianPoints[44] - currentPoint) * faceLiftScale;
coordinate = curveWarp(coordinate, currentPoint, destPoint, radius);
currentPoint = cartesianPoints[29];
destPoint = currentPoint + (cartesianPoints[44] - currentPoint) * faceLiftScale;
coordinate = curveWarp(coordinate, currentPoint, destPoint, radius);
radius = faceLength * 0.8;
currentPoint = cartesianPoints[10];
destPoint = currentPoint + (cartesianPoints[46] - currentPoint) * (faceLiftScale * 0.6);
coordinate = curveWarp(coordinate, currentPoint, destPoint, radius);
currentPoint = cartesianPoints[22];
destPoint = currentPoint + (cartesianPoints[46] - currentPoint) * (faceLiftScale * 0.6);
coordinate = curveWarp(coordinate, currentPoint, destPoint, radius);
return coordinate;
// 削脸
vec2 faceShave(vec2 currentCoordinate, float faceLength)
vec2 coordinate = currentCoordinate;
vec2 currentPoint = vec2(0.0);
vec2 destPoint = vec2(0.0);
float faceShaveScale = reshapeIntensity[INDEX_FACE_SHAVE] * 0.12;
float radius = faceLength * 1.0;
// 下巴中心
vec2 chinCenter = (cartesianPoints[16] + cartesianPoints[93]) * 0.5;
currentPoint = cartesianPoints[13];
destPoint = currentPoint + (chinCenter - currentPoint) * faceShaveScale;
coordinate = curveWarp(coordinate, currentPoint, destPoint, radius);
currentPoint = cartesianPoints[19];
destPoint = currentPoint + (chinCenter - currentPoint) * faceShaveScale;
coordinate = curveWarp(coordinate, currentPoint, destPoint, radius);
return coordinate;
// 处理下巴
vec2 chinChange(vec2 currentCoordinate, float faceLength)
vec2 coordinate = currentCoordinate;
vec2 currentPoint = vec2(0.0);
vec2 destPoint = vec2(0.0);
float chinScale = reshapeIntensity[INDEX_CHIN] * 0.08;
float radius = faceLength * 1.25;
currentPoint = cartesianPoints[16];
destPoint = currentPoint + (cartesianPoints[46] - currentPoint) * chinScale;
coordinate = curveWarp(coordinate, currentPoint, destPoint, radius);
return coordinate;
void main()
vec2 coordinate = textureCoordinate.xy;
// 禁用美型处理或者鼻子不在图像中,则直接绘制
if (enableReshape == 0 || (cartesianPoints[46].x / float(textureWidth) <= 0.03)
|| (cartesianPoints[46].y / float(textureHeight)) <= 0.03) {
gl_FragColor = texture2D(inputTexture, coordinate);
return;
// 将坐标转成图像大小,这里是为了方便计算
coordinate = textureCoordinate * vec2(float(textureWidth), float(textureHeight));
float eyeDistance = distance(cartesianPoints[74], cartesianPoints[77]); // 两个瞳孔的距离
// 瘦脸
coordinate = faceLift(coordinate, eyeDistance);
// 削脸
coordinate = faceShave(coordinate, eyeDistance);
// 小脸 TODO 眼睛到下巴图像线性缩小
// 下巴
coordinate = chinChange(coordinate, eyeDistance);
// 额头
// 大眼
float eyeEnlarge = reshapeIntensity[INDEX_EYE_ENLARGE] * 0.12; // 放大倍数
if (eyeEnlarge > 0.0) {
float radius = eyeDistance * 0.33; // 眼睛放大半径
coordinate = enlargeEye(coordinate, cartesianPoints[74] + (cartesianPoints[77] - cartesianPoints[74]) * 0.05, radius, eyeEnlarge);
coordinate = enlargeEye(coordinate, cartesianPoints[77] + (cartesianPoints[74] - cartesianPoints[77]) * 0.05, radius, eyeEnlarge);
// 眼距
// 眼角
// 瘦鼻
// 鼻翼
// 长鼻
// 嘴型
// 转变回原来的纹理坐标系
coordinate = coordinate / vec2(float(textureWidth), float(textureHeight));
// 输出图像
gl_FragColor = texture2D(inputTexture, coordinate);
具体的实现,可参考GLImageFaceReshapeFilter的实现。部分脸型调节效果如下: