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.