Android 平台美颜实现
1 使用 OpenGL ES 渲染一帧图片
OpenGL 的大名就不用再赘述了,如果已经是 OpenGL 的高手了,可以直接跳过,如果你还是一个小白我推荐看这个教程 Learn OpenGL ,只需要学习完入门,就能够轻松渲染图片了。如果不想看更加繁琐的教程或者想看 Android 版的实现的,那就看我写的这教程吧。
1.1 构建 OpenGL ES 环境
GLSurfaceView 已经为我们搭建好了环境,在 View 初始化的时候我们要指定我们需要的 OpenGL ES 的版本和设置我们的渲染实现 Renderer:
init {
setEGLContextClientVersion(3)
setRenderer(MyRenderer(this))
renderMode = RENDERMODE_WHEN_DIRTY
}
Renderer 接口:
public interface Renderer {
/**
* 环境已经创建好
*/
void onSurfaceCreated(GL10 gl, EGLConfig config);
/**
* View 的大小发生改变
*/
void onSurfaceChanged(GL10 gl, int width, int height);
/**
* 绘制每一帧
*/
void onDrawFrame(GL10 gl);
}
我们通常在 onSurfaceCreated
回调时初始化需要绘制需要的东西,在 onDrawFrame
回调中绘制每一帧。
通常在 onSurfaceChanged()
方法中需要设置 OpenGL 的 ViewPort 和 clear 的 Color:
// ...
GLES31.glViewport(0, 0, width, height)
GLES31.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)
GLES31.glClear(GLES31.GL_COLOR_BUFFER_BIT)
// ...
GLSurfaceView 会创建一个 RenderThread,OpenGL 的各种操作都是在这个线程中完成的,不会阻塞主线程。
1.2 编译 OpenGL 渲染程序
先不要惊讶,使用 OpenGL 前需要编译一个渲染程序,需要我们自己编写顶点着色器和片段着色器两个脚本代码,然后把他们链接成一个完整的渲染程序。
OpenGL 的渲染代码是类 C 代码,对于我们来说还是相对友好,上手比较快。
顶点着色器代码:
#version 310 es
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
void main() {
gl_Position = vec4(aPos, 1.0);
TexCoord = aTexCoord;
}
片段着色器代码:
#version 310 es
precision highp float; // Define float precision
in vec2 TexCoord;
out vec4 FragColor;
uniform sampler2D Texture;
void main() {
FragColor = texture(Texture, TexCoord);
}
上述就是两个非常简单的代码,其中有一些系统变量和一些我们自定义的变量我们后面再说,其中 main 函数为入口函数。
可以简单地认为顶点着色器是告诉 GPU 我们要绘制的形状,形状的是用一些顶点和绘制的的参数来描述;片段着色器就是告诉 GPU 我们绘制的形状中用什么颜色填充。例如我们像要用 OpenGL 显示一张图片,首先图片是一个矩形,顶点着色器就是确定这个矩形,而这个矩形中要填充什么颜色呢?就需要片段着色器来确定。
了解了一些基础知识我们在 Android 环境中来编译渲染器程序,我们通常在 Renderer 的 onSurfaceCreated 中完成编译.
1.2.1 编译顶点着色器
/**
* 编译顶点着色器
*/
val vertexShader = GLES31.glCreateShader(GLES31.GL_VERTEX_SHADER)
GLES31.glShaderSource(vertexShader, vertexShaderSource)
GLES31.glCompileShader(vertexShader)
val vertexCompileState = ByteBuffer.allocateDirect(4).let {
it.order(ByteOrder.nativeOrder())
it.asIntBuffer()
}
GLES31.glGetShaderiv(vertexShader, GLES31.GL_COMPILE_STATUS, vertexCompileState)
vertexCompileState.position(0)
if (vertexCompileState.get() <= 0) {
val log = GLES31.glGetShaderInfoLog(vertexShader)
GLES31.glDeleteShader(vertexShader)
Log.e(TAG, "Compile vertex shader fail: $log")
return null
}
上面代码很简单,编译完成后检查一下状态,如果状态不正确,获取错误的日志。
1.2.2 编译片段着色器
/**
* 编译片段着色器
*/
val fragmentShader = GLES31.glCreateShader(GLES31.GL_FRAGMENT_SHADER)
GLES31.glShaderSource(fragmentShader, fragmentShaderSource)
GLES31.glCompileShader(fragmentShader)
val fragmentCompileState = ByteBuffer.allocateDirect(4).let {
it.order(ByteOrder.nativeOrder())
it.asIntBuffer()
}
GLES31.glGetShaderiv(fragmentShader, GLES31.GL_COMPILE_STATUS, fragmentCompileState
fragmentCompileState.position(0)
if (fragmentCompileState.get() <= 0) {
val log = GLES31.glGetShaderInfoLog(fragmentShader)
GLES31.glDeleteShader(vertexShader)
GLES31.glDeleteShader(fragmentShader)
Log.e(TAG, "Compile fragment shader fail: $log")
return null
}
和顶点着色器的编译我想说几乎一摸一样。
1.2.3 链接顶点着色器和片段着色器
/**
* 链接着色器程序
*/
val shaderProgram = GLES31.glCreateProgram()
GLES31.glAttachShader(shaderProgram, vertexShader)
GLES31.glAttachShader(shaderProgram, fragmentShader)
GLES31.glLinkProgram(shaderProgram)
GLES31.glDeleteShader(vertexShader)
GLES31.glDeleteShader(fragmentShader)
val linkProgramState = ByteBuffer.allocateDirect(4).let {
it.order(ByteOrder.nativeOrder())
it.asIntBuffer()
}
GLES31.glGetProgramiv(shaderProgram, GLES31.GL_LINK_STATUS, linkProgramState)
linkProgramState.position(0)
if (linkProgramState.get() <= 0) {
val log = GLES31.glGetProgramInfoLog(shaderProgram)
GLES31.glDeleteProgram(shaderProgram)
Log.e(TAG, "Link program fail: $log")
return null
}
Log.d(TAG, "Compile program success!!")
链接就是把前面编译好的顶点着色器和片段着色器连接成一个完整的 OpenGL 渲染程序,供后面我们渲染时使用。
1.3 传递矩形和纹理的坐标到顶点着色器
我们的图片就需要用纹理来描述,纹理通常是给片段着色器使用,片段着色器可以通过纹理和纹理坐标就可以获取到图片某个位置的 RGBA 值了,在这之前我们需要将图片的数据传递给对应的纹理。
在 OpenGL 中顶点的坐标系的原点在屏幕的中心,x 轴和 y 轴的值都是从 -1 到 1,坐标系大概这样的:
纹理的坐标原点在屏幕的左上角,x 轴和 y 轴的值是从 0 到 1,坐标系大概这样:
如果我们想要我们的图片拉升填充至屏幕,我们就可以确定我们的坐标和纹理坐标的对应了。
val vertices = floatArrayOf(
// 坐标(position 0) // 纹理坐标(position 1)
-1.0f, 1.0f, 0.0f, 0.0f, 0.0f, // 左上角
1.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右上角
1.0f, -1.0f, 0.0f, 1.0f, 1.0f, // 右下角
-1.0f, -1.0f, 0.0f, 0.0f, 1.0f, // 左下角
)
就相当于一个顶点有 5 个 float 值,前三个表示 opengl 中的坐标,后两个表示 纹理坐标。
对应在前面描述到的顶点着色器中的变量:
// ...
layout (location = 0) in vec3 aPos; // GL 坐标
layout (location = 1) in vec2 aTexCoord; // 纹理坐标
// ...
后面展示如何将这些顶点数据冲 Kotlin 代码中传入到顶点着色器, 如果这些顶点数据在渲染时不会改变,在 Create 阶段就可以完成传递:
fun glGenBuffers(): Int {
val buffer = newGlIntBuffer()
GLES31.glGenBuffers(1, buffer)
buffer.position(0)
return buffer.get()
}
fun glGenVertexArrays(): Int {
val buffer = newGlIntBuffer()
GLES31.glGenVertexArrays(1, buffer)
buffer.position(0)
return buffer.get()
}
val imageVAO: Int = glGenVertexArrays()
val imageVBO: Int = glGenBuffers()
GLES31.glBindVertexArray(imageVAO)
GLES31.glBindBuffer(GLES31.GL_ARRAY_BUFFER, imageVBO)
GLES31.glVertexAttribPointer(0, 3, GLES31.GL_FLOAT, false, 20, 0)
GLES31.glEnableVertexAttribArray(0)
GLES31.glVertexAttribPointer(1, 2, GLES31.GL_FLOAT, false, 20, 12)
GLES31.glEnableVertexAttribArray(1)
GLES31.glBufferData(GLES31.GL_ARRAY_BUFFER, vertices.size * 4, vertices.toGlBuffer(), GLES31.GL_STATIC_DRAW)
首先创建和绑定 VertexArray (VAO),然后创建和绑定 ArrayBuffer(VBO),然后要告诉 OpenGL 这些数据点的大小和对应的步长和Offset和对应的 location,最后将数据传递给 顶点着色器,也就是对应的下面两个值:
// ...
layout (location = 0) in vec3 aPos; // GL 坐标
layout (location = 1) in vec2 aTexCoord; // 纹理坐标
// ...
其中的 glVertexAttribPointer 方法第一个参数就是指定 上面的 location.
1.4 纹理创建和图片数据绑定
1.4.1 生成纹理和设置纹理属性
fun glGenTexture(): Int {
val buffer = newGlIntBuffer()
GLES31.glGenTextures(1, buffer)
buffer.position(0)
return buffer.get()
}
val texture = glGenTexture()
GLES31.glBindTexture(GLES31.GL_TEXTURE_2D, texture)
GLES31.glTexParameteri(GLES31.GL_TEXTURE_2D, GLES31.GL_TEXTURE_WRAP_S, GLES31.GL_REPEAT)
GLES31.glTexParameteri(GLES31.GL_TEXTURE_2D, GLES31.GL_TEXTURE_WRAP_T, GLES31.GL_REPEAT)
GLES31.glTexParameteri(GLES31.GL_TEXTURE_2D, GLES31.GL_TEXTURE_MIN_FILTER, GLES31.GL_LINEAR)
GLES31.glTexParameteri(GLES31.GL_TEXTURE_2D, GLES31.GL_TEXTURE_MAG_FILTER, GLES31.GL_LINEAR)
GLES31.glGenerateMipmap(GLES31.GL_TEXTURE_2D)
Open GL 默认会激活 Texture0,同时会自动绑定到片段着色器的纹理变量:
// ...
uniform sampler2D Texture;
// ...
1.4.2 向纹理中绑定图片数据
Android 中可以通过以下方法把 Bitmap 中的数据绑定到 OpenGL 的纹理中:
//...
GLUtils.texImage2D(GLES31.GL_TEXTURE_2D, 0, bitmap, 0)
bitmap.recycle()
1.5.0 最终绘制
1.5.1 再看着色器代码
顶点着色器代码:
#version 310 es
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
void main() {
gl_Position = vec4(aPos, 1.0);
TexCoord = aTexCoord;
}
其中 aPos 就是 gl 点的坐标,aTexCoord 就是 纹理的点的坐标,我们在前面已经完成了数据的输入,其中 TexCoord 为顶点着色器要输入到片段着色器的变量。
在 main 函数中,我们直接将传入的坐标赋值给系统变量 gl_Position,这就告诉系统最终的顶点位置,把纹理的坐标直接赋值给 TexCoord 变量,传递给后面的片段着色器。
片段着色器代码:
#version 310 es
precision highp float; // Define float precision
in vec2 TexCoord;
out vec4 FragColor;
uniform sampler2D Texture;
void main() {
FragColor = texture(Texture, TexCoord);
}
TexCoord 就是由顶点着色器传递过来的纹理坐标,FragColor 是最终这个坐标输入到屏幕上的颜色,Texture 就是我们在上面创建的纹理对象。 在 main 函数中,我们直接取了纹理中对应坐标中的颜色值,简单理解就是直接绘制纹理到屏幕上,不做任何处理,如果要做美颜的话我们就需要在这里做额外的处理。
1.5.2 最终绘制
前面的工作中我们完成了顶点坐标的输入和纹理的数据输入,所以我们就可以绘制了:
// ...
GLES31.glClear(GLES31.GL_COLOR_BUFFER_BIT)
GLES31.glUseProgram(program)
// ...
绘制前需要先清除上一帧的画面,同时使用前面编译好的程序。
GLES31.glBindVertexArray(imageVAO)
GLES31.glBindBuffer(GLES31.GL_ARRAY_BUFFER, imageVBO)
GLES31.glBindTexture(GLES31.GL_TEXTURE_2D, textureId)
GLES31.glDrawArrays(GLES31.GL_TRIANGLE_FAN, 0, 4)
首先我们要绑定之前生成好的 VAO,VBO 和纹理,最后调用绘制方法,我们要绘制 4 个点的矩形,需要使用 GL_TRIANGLE_FAN,到这里我们就完成了图片的绘制。
1.5.3 最后总结
我们需要在 onSurfaceCreated
中编译渲染程序,生成 VAO,VBO和纹理,如果你的纹理数据或者顶点数据是不变的,也可以在这里绑定你的纹理数据和顶点数据。
需要在 onSurfaceChanged
中设置 GL 的 ViewPort。
在 onDrawFrame
中绘制每一帧图片,这首先要使用 create 中编译好的渲染程序,如果顶点数据或者纹理数据发生了改变,还要重新绑定改变的数据,最后绑定 VAO,VBO和纹理完成绘制。
2 美颜
假如你已经能够通过 OpenGL 绘制摄像头采集到的图片到屏幕上了,那么就可以尝试让你采集到的图片变得更美了。
在瘦脸和大眼就需要使用人脸关键点检测,我使用的是开源的库 TengineKit.
2.1 美白
美白是美颜中最简单的,在 OpenGL 的 Rgba 各个分量的颜色值都是从 0 到 1.0,我们先把颜色值从 RGB 转换成 YUV,对 YUV 颜色空间不熟悉的同学可以去查查资料,然后我们提升表示明亮的值 Y 就完成了提升画面的亮度,由于 OpenGL 不能直接显示 YUV 的颜色,所以还需要把修改后的 YUV 转换成 RGB.
vec3 rgbToYuv(vec3 rgb) {
float r = rgb.x;
float g = rgb.y;
float b = rgb.z;
float y = 0.183 * r + 0.614 * g + 0.062 * b + 16.0;
float u = -0.101 * r - 0.339 * g + 0.439 * b + 128.0;
float v = 0.439 * r - 0.399 * g - 0.040 * b + 128.0;
return vec3(y, u, v);
}
vec3 yuvToRgb(vec3 yuv) {
float y = yuv.x;
float u = yuv.y;
float v = yuv.z;
float r = 1.164 * (y - 16.0) + 1.793 * (v - 128.0);
float g = 1.164 * (y - 16.0) - 0.213 * (u - 128.0) - 0.533 * (v - 128.0);
float b = 1.164 * (y - 16.0) + 2.112 * (u - 128.0);
return vec3(r, g, b);
}
/**
* 美白
*/
vec3 whitening(vec3 rgb, float strength) {
vec3 yuv255 = rgbToYuv(rgb * 255.0);
yuv255.x = log(yuv255.x / 255.0 * (strength - 1.0) + 1.0) / log(strength) * 255.0;
vec3 rgb255 = yuvToRgb(yuv255);
return rgb255 / 255.0;
}
我们把直接从纹理中获取到的 rgb 值加上美颜的强度参数后,就能够返回美颜后的 rgb 值. 其中美颜亮度的修改公式为:f(x) = log(x * (strength - 1.0) + 1.0) / log(strength).
2.2 磨皮
磨皮的第一步是皮肤检测,我们只处理皮肤区域,皮肤的检测代码如下:
/**
* 是否是皮肤颜色
*/
bool isSkinColor(vec4 color) {
float r = color.x * 255.0;
float g = color.y * 255.0;
float b = color.z * 255.0;
return r > 95.0 && g > 40.0 && b > 20.0 && r > g && r > b && (max(r, max(g, b)) - min(r, min(g, b))) > 15.0 && (abs(r - b) > 15.0);
}
磨皮的工作原理是对于人的皮肤区域在一定的范围 RGB 值取均值,就能够淡化人皮肤颜色变化过于大的部分,能够使人的皮肤看上去更加的平滑,淡化痘痘,脸上的坑。
举个例子:
假如这个时候取的皮肤点的坐标是 (5, 5),假如我设置的是平均周围一个像素的值(通常情况下不止一个像素,我自己就取的四个):
(4, 4), (4, 5), (4, 6)
(5, 4), (5, 5), (5, 6)
(6, 4), (6, 5), (6, 6)
我就要取以上的点来求平均值,如果要做得更好,也可以不做平均值,而是加权平均,越靠近当前坐标的值,加权越大。
参考代码:
/**
* 正态分布函数
*/
float gauthFunc(float maxValue, float centerLine, float changeRate, float x) {
return maxValue * exp(- pow(x - centerLine, 2.0) / (2.0 * changeRate));
}
/**
* 磨皮
*/
vec4 skinSmooth(sampler2D inputTexture, vec2 texCoord, float widthPixelStep, float heightPixelStep, float radius, float strength) {
vec4 centerColor = texture(inputTexture, texCoord);
if (isSkinColor(centerColor)) {
float gauthMaxValue = 1.0;
float gauthCenterLine = 0.0;
vec4 colorSum = centerColor;
float colorRateSum = 1.0;
vec2 upVec = vec2(0.0, - heightPixelStep);
vec2 upRightVec = vec2(widthPixelStep, - heightPixelStep);
vec2 rightVec = vec2(widthPixelStep, 0.0);
vec2 rightDownVec = vec2(widthPixelStep, heightPixelStep);
vec2 downVec = vec2(0.0, heightPixelStep);
vec2 downLeftVec = vec2(-widthPixelStep, heightPixelStep);
vec2 leftVec = vec2(-widthPixelStep, 0.0);
vec2 leftUpVec = vec2(-widthPixelStep, -heightPixelStep);
for (float i = 1.0; i <= radius; i = i + 1.0) {
float colorRate = gauthFunc(gauthMaxValue, gauthCenterLine, strength, i);
vec2 u = texCoord + upVec * i;
if (checkTextureCoord(u)) {
vec4 c = texture(inputTexture, u);
if (isSkinColor(c)) {
colorRateSum += colorRate;
colorSum += colorRate * c;
}
}
vec2 ur = texCoord + upRightVec * i;
if (checkTextureCoord(ur)) {
vec4 c = texture(inputTexture, ur);
if (isSkinColor(c)) {
colorRateSum += colorRate;
colorSum += colorRate * c;
}
}
vec2 r = texCoord + rightVec * i;
if (checkTextureCoord(r)) {
vec4 c = texture(inputTexture, r);
if (isSkinColor(c)) {
colorRateSum += colorRate;
colorSum += colorRate * c;
}
}
vec2 rd = texCoord + rightDownVec * i;
if (checkTextureCoord(rd)) {
vec4 c = texture(inputTexture, rd);
if (isSkinColor(c)) {
colorRateSum += colorRate;
colorSum += colorRate * c;
}
}
vec2 d = texCoord + downVec * i;
if (checkTextureCoord(d)) {
vec4 c = texture(inputTexture, d);
if (isSkinColor(c)) {
colorRateSum += colorRate;
colorSum += colorRate * c;
}
}
vec2 dl = texCoord + downLeftVec * i;
if (checkTextureCoord(dl)) {
vec4 c = texture(inputTexture, dl);
if (isSkinColor(c)) {
colorRateSum += colorRate;
colorSum += colorRate * c;
}
}
vec2 l = texCoord + leftVec * i;
if (checkTextureCoord(l)) {
vec4 c = texture(inputTexture, l);
if (isSkinColor(c)) {
colorRateSum += colorRate;
colorSum += colorRate * c;
}
}
vec2 lu = texCoord + leftUpVec * i;
if (checkTextureCoord(lu)) {
vec4 c = texture(inputTexture, lu);
if (isSkinColor(c)) {
colorRateSum += colorRate;
colorSum += colorRate * c;
}
}
}
return colorSum / colorRateSum;
} else {
return centerColor;
}
}
磨皮后的这个图像会像高斯模糊的图片一样,看上去非常不清晰,这时我们还需要原来颜色值的一些细节,我们可以通过 OpenGL 的内置函数 mix 来融合原来的颜色和处理后的颜色,其中原来的颜色占 0.6,处理后的颜色占 0.4.
// ...
// 磨皮
if (skinSmoothSwitch == 1) {
vec4 smoothColor = skinSmooth(Texture, fixedCoord, textureWidthPixelStep, textureHeightPixelStep, 6.0, skinSmoothStrength);
outputColor = mix(outputColor, smoothColor, 0.6);
}
// ...
2.3 大眼
大眼的工作原理和放大镜类似,我们把眼睛看成一个圆,圆中心的像素向圆的周边扩散,越靠近圆心扩散越多,越靠近圆的边扩散越少。
算法如下:
vec2 enlarge(vec2 currentCoordinate, vec2 circleCenter, float radius, float strength)
{
float dis = distance(currentCoordinate, circleCenter);
if (dis > radius) {
return currentCoordinate;
}
float k0 = strength / 100.0;
float k = 1.0 - k0 * (1.0 - pow(dis / radius, 2.0));
float nx = (currentCoordinate.x - circleCenter.x) * k + circleCenter.x;
float ny = (currentCoordinate.y - circleCenter.y) * k + circleCenter.y;
return vec2(nx, ny);
}
当然我们的眼睛不是圆形,可以近似看成一个椭圆,通过人脸的关键点,可以求出双眼椭圆的圆心,半长轴和半短轴,椭圆内的点与圆心构成的直线与椭圆的交点到圆心的距离就可以当作放大眼睛的半径,参考的代码如下:
vec2 enlargeOval(vec2 currentCoordinate, vec2 center, float a, float b, float strength) {
float dx = currentCoordinate.x - center.x;
float dy = currentCoordinate.y - center.y;
float checkDistence = (dx * dx) / (a * a) + (dy * dy) / (b * b);
if (checkDistence > 1.0) {
return currentCoordinate;
}
float x = 0.0;
float y = 0.0;
float x1 = 0.0;
float y1 = 0.0;
float x2 = 0.0;
float y2 = 0.0;
float minStep = 0.0003;
if (abs(center.x - currentCoordinate.x) < minStep) {
x1 = center.x;
y1 = center.y + b;
x2 = center.x;
y2 = center.y - b;
} else if (abs(center.y - currentCoordinate.y) < minStep) {
x1 = center.x + a;
y1 = center.y;
x2 = center.x - a;
y2 = center.y;
} else {
float lineA = (currentCoordinate.y - center.y) / (currentCoordinate.x - center.x);
float lineB = (currentCoordinate.y * center.x - currentCoordinate.x * center.y) / (center.x - currentCoordinate.x);
float fucA = ((1.0 / pow(a, 2.0)) + (pow(lineA, 2.0) / pow(b, 2.0)));
float fucB = ((2.0 * lineA * (lineB - center.y)) / pow(b, 2.0)) - (2.0 * center.x) / pow(a, 2.0);
float fucC = pow(center.x / a, 2.0) + pow((lineB - center.y) / b, 2.0) - 1.0;
x1 = (- fucB + sqrt(pow(fucB, 2.0) - 4.0 * fucA * fucC)) / (2.0 * fucA);
y1 = lineA * x1 + lineB;
x2 = (- fucB - sqrt(pow(fucB, 2.0) - 4.0 * fucA * fucC)) / (2.0 * fucA);
y2 = lineA * x2 + lineB;
}
float d1 = distance(vec2(x1, y1), currentCoordinate);
float d2 = distance(vec2(x2, y2), currentCoordinate);
if (d1 < d2) {
x = x1;
y = y1;
} else {
x = x2;
y = y2;
}
float radius = distance(center, vec2(x, y));
return enlarge(currentCoordinate, center, radius, strength);
}
2.4 瘦脸
瘦脸的算法和大眼的算法原理差不多,根据人脸关键点的数据,取人左右脸颊两个瘦脸的点向鼻尖拉伸,拉伸的半径我取的是双眼瞳孔的间距的一半。
参考代码:
// 瘦脸
vec2 stretch(vec2 textureCoord, vec2 circleCenter, vec2 targetPosition, float radius, float strength)
{
float k1 = distance(textureCoord, circleCenter);
if (k1 >= radius) {
return textureCoord;
}
float k0 = 100.0 / strength;
float tx = pow((pow(radius, 2.0) - pow(textureCoord.x - circleCenter.x, 2.0)) / (pow(radius, 2.0) - pow(textureCoord.x - circleCenter.x, 2.0) + k0 * pow(targetPosition.x - circleCenter.x, 2.0)), 2.0) * (targetPosition.x - circleCenter.x);
float ty = pow((pow(radius, 2.0) - pow(textureCoord.y - circleCenter.y, 2.0)) / (pow(radius, 2.0) - pow(textureCoord.y - circleCenter.y, 2.0) + k0 * pow(targetPosition.y - circleCenter.y, 2.0)), 2.0) * (targetPosition.y - circleCenter.y);
float nx = textureCoord.x - tx * (1.0 - k1 / radius);
float ny = textureCoord.y - ty * (1.0 - k1 / radius);
return vec2(nx, ny);
}
3 最后
如果需要查看源码的具体实现,点这里AndroidOpenGLPractice, 如果觉得这个对你有帮助 Star it.