GPUImage渲染流程解析

渲染

创建视图

1
2
3
4
5
6
7
//版本
mGlSurfaceView.setEGLContextClientVersion(2);
// 设置渲染器(这个渲染器的类非常重要)
mGlSurfaceView.setRenderer(mRenderer);
// 设置渲染模式为根据需要来渲染(RENDERMODE_CONTINUOUSLY)
mGlSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
mGlSurfaceView.requestRender();

坐标

  • 顶点坐标:在OpenGL中,顶点坐标使用的是笛卡尔右手坐标系(世界坐标系)。分X Y Z 3个轴,X轴朝右为正,Y轴朝上为正,Z轴垂直屏幕朝外为正。X Y Z 3个轴的最大与最小值为 1 和 -1。

  • 屏幕坐标:屏幕坐标系,就是应用在设备屏幕上的坐标系,也就是图形最终显示的地方。X轴朝右为正,Y轴朝下为正。

  • 纹理坐标:原点在左下角,X轴朝右为正,Y轴朝上为正,X Y 轴的最大与最小值为 0 和 1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//顶点坐标
static final float VERTICE[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f,
};
//纹理坐标 对应顶点坐标 与之映射
static final float TEXTURE[] = {
0.0f, 0.0f,
1.0f, 0.0f,
0.0f, 1.0f,
1.0f, 1.0f,
};

注意:纹理坐标与顶点坐标非必需一一对应,纹理的坐标会导致图片的缩放显示。

顶点着色器(Vertex Shader),片元着色器(Fragment Shader)

顶点着色器处理传入的顶点数据,包括顶点位置、顶点颜色、光照等,每个顶点都会执行一次。

片元着色器主要目的是计算一个像素的最终颜色。片元并不是真正意义上的像素。而是包含了很多状态的集合,这些状态用来计算最终颜色。这些状态包括但不限于它的屏幕坐标,深度信息,法线,纹理坐标等。

所以,顶点着色器用于绘制顶点,片元着色器用于给顶点连线后所包围的区域填充颜色,可以简单的理解成windows中画图的填充工具。所以下边是生成简单着色器的两个源码: GLSL基础语法介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public static final String NO_FILTER_VERTEX_SHADER = "" +
//顶点在画布中的位置
"attribute vec4 position;\n" +
// 纹理映射,纹理坐标是纹理映射的一部分。这意味着你想要对你的纹理进行某种滤镜操作的时候会用到它。
"attribute vec4 inputTextureCoordinate;\n" +
" \n" +
//变量,负责顶点着色器负责和片段着色器交流,以及信息的共享
"varying vec2 textureCoordinate;\n" +
" \n" +
"void main()\n" +
"{\n" +
" gl_Position = position;\n" +
//我们取出这个顶点中纹理坐标的 X 和 Y 的位置。我们只关心 inputTextureCoordinate 中的前两个参数,X 和 Y。这个坐标最开始是通过 4 个属性存在顶点着色器里的,但我们只需要其中的两个。
" textureCoordinate = inputTextureCoordinate.xy;\n" +
"}";

/**
* 这个着色器实际上不会改变图像中的任何东西。它是一个直通着色器,意味着我们输入每一个像素,然后输出完全相同的像素。
*/
public static final String NO_FILTER_FRAGMENT_SHADER = "" +

//因为片段着色器作用在每一个像素上,我们需要一个方法来确定我们当前在分析哪一个像素/片段。它需要存储像素的 X 和 Y 坐标。我们接收到的是当前在顶点着色器被设置好的纹理坐标。
"varying highp vec2 textureCoordinate;\n" +
" \n" +
//为了处理图像,我们从应用中接收一个图片的引用,我们把它当做一个 2D 的纹理。这个数据类型被叫做 sampler2D ,这是因为我们要从这个 2D 纹理中采样出一个点来进行处理。
"uniform sampler2D inputImageTexture;\n" +
" \n" +
"void main()\n" +
"{\n" +
//这是一个 GLSL 特有的方法:texture2D,顾名思义,创建一个 2D 的纹理。它采用我们之前声明过的属性作为参数来决定被处理的像素的颜色。这个颜色然后被设置给另外一个内建变量,gl_FragColor。因为片段着色器的唯一目的就是确定一个像素的颜色,gl_FragColor 本质上就是我们片段着色器的返回语句。
" gl_FragColor = texture2D(inputImageTexture, textureCoordinate);\n" +
"}";

有了源码,我们就可以创建着色器:
我们首先要做的是创建一个着色器对象,注意还是用ID来引用的,所以我们储存这个顶点着色器的ID为unsigned int,然后用glCreateShader创建这个着色器,我们把需要创建的着色器类型以参数形式提供给glCreateShader,由于我们正在创建一个顶点着色器,传递的参数是GLES20.GL_VERTEX_SHADER

1
int iShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);

下一步我们把这个着色器源码附加到着色器对象上,然后编译它:第一个参数是要编译的着色器对象,第二参数指定了传递的源码字符串.

1
2
GLES20.glShaderSource(iShader, strSource);
GLES20.glCompileShader(iShader);

同时,我们要检测在调用glCompileShader后编译是否成功了,如果没成功的话,也希望知道错误是什么,这以便修复它们.如果结果非0,即编译成功。

1
2
3
4
5
6
int[] compiled = new int[1];
GLES20.glGetShaderiv(iShader, GLES20.GL_COMPILE_STATUS, compiled, 0);
if (compiled[0] == 0) {
Log.d("Load Shader Failed", "Compilation\n" + GLES20.glGetShaderInfoLog(iShader));
return 0;
}

片元着色器的创建和顶点一样,知识参数不同和源码不同。参数类型是GLES20.GL_FRAGMENT_SHADER

着色器程序

通过glCreateProgram函数创建一个程序,并返回新创建程序对象的ID引用,然后我们需要把之前编译的着色器附加到程序对象上,最后用glLinkProgram链接它们。着色器对象链接到程序对象以后,删除着色器对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 创建程序,返回对象的ID
iProgId = GLES20.glCreateProgram();
// 向程序中加入顶点着色器
GLES20.glAttachShader(iProgId, iVShader);
// 向程序中加入片元着色器
GLES20.glAttachShader(iProgId, iFShader);
// 链接程序
GLES20.glLinkProgram(iProgId);
// 获取program的链接情况
GLES20.glGetProgramiv(iProgId, GLES20.GL_LINK_STATUS, link, 0);
if (link[0] <= 0) {
Log.d("Load Program", "Linking Failed");
return 0;
}
GLES20.glDeleteShader(iVShader);
GLES20.glDeleteShader(iFShader);
// 获取着色器中的属性引用id(传入的字符串就是我们着色器脚本中的属性名)
mGLAttribPosition = GLES20.glGetAttribLocation(mGLProgId, "position");
mGLUniformTexture = GLES20.glGetUniformLocation(mGLProgId, "inputImageTexture");
mGLAttribTextureCoordinate = GLES20.glGetAttribLocation(mGLProgId,
"inputTextureCoordinate");
//glClearColor为glClear清除颜色缓冲区时指定RGBA值(也就是所有的颜色都会被替换成指定的RGBA值)。每个值的取值范围都是0.0~1.0,超出范围的将被截断。
GLES20.glClearColor(0, 0, 0, 1);

纹理ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static int loadTexture(final IntBuffer data, final Size size, final int usedTexId) {
int textures[] = new int[1];
if (usedTexId == NO_TEXTURE) {
GLES20.glGenTextures(1, textures, 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, size.width, size.height,
0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, data);
} else {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, usedTexId);
GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, size.width,
size.height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, data);
textures[0] = usedTexId;
}
return textures[0];
}

绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 使用某套shader程序
GLES20.glUseProgram(mGLProgId);
// 设置缓冲区起始位置
cubeBuffer.position(0);
// 顶点位置数据传入着色器,为画笔指定顶点位置数据(mGLAttribPosition)
GLES20.glVertexAttribPointer(mGLAttribPosition, 2, GLES20.GL_FLOAT, false, 0, cubeBuffer);
// 允许使用顶点坐标数组
GLES20.glEnableVertexAttribArray(mGLAttribPosition);
textureBuffer.position(0);
// 纹理坐标数据传入着色器,为画笔指定纹理数据
GLES20.glVertexAttribPointer(mGLAttribTextureCoordinate, 2, GLES20.GL_FLOAT, false, 0,
textureBuffer);
GLES20.glEnableVertexAttribArray(mGLAttribTextureCoordinate);
if (textureId != OpenGlUtils.NO_TEXTURE) {
//设置纹理单元(sampler2D)
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
//设置uniform采样器的位置值, 也就是mGLUniformTexture纹理单元。
GLES20.glUniform1i(mGLUniformTexture, 0);
}
onDrawArraysPre();
//// 绘制
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
GLES20.glDisableVertexAttribArray(mGLAttribPosition);
GLES20.glDisableVertexAttribArray(mGLAttribTextureCoordinate);

滤镜纹理

1
2
3
4
5
6
7
 protected void onDrawArraysPre() {
if (mToneCurveTexture[0] != OpenGlUtils.NO_TEXTURE) {
GLES20.glActiveTexture(GLES20.GL_TEXTURE3);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mToneCurveTexture[0]);
GLES20.glUniform1i(mToneCurveTextureUniformLocation, 3);
}
}

通过上述代码就可以实现纹理滤镜的效果了。但是这个滤镜的纹理是如何获取到的呢?
简单介绍两种方式:

ps acv 文件

acv文件是ps用来设置曲线效果,不同曲线参数可以呈现出不同的滤镜效果。我们可以对acv文件进行读取,可以得到:Curves曲线信息数组,每个curvers可以分别获取到R,G,B 的PointF数组信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public void setFromCurveFileInputStream(InputStream input) {
try {
int version = readShort(input);
int totalCurves = readShort(input);
ArrayList<PointF[]> curves = new ArrayList<PointF[]>(totalCurves);
float pointRate = 1.0f / 255;

for (int i = 0; i < totalCurves; i++) {
// 2 bytes, Count of points in the curve (short integer from 2...19)
short pointCount = readShort(input);
PointF[] points = new PointF[pointCount];
// point count * 4
// Curve points. Each curve point is a pair of short integers where
// the first number is the output value (vertical coordinate on the
// Curves dialog graph) and the second is the input value. All coordinates have range 0 to 255.
for (int j = 0; j < pointCount; j++) {
short y = readShort(input);
short x = readShort(input);

points[j] = new PointF(x * pointRate, y * pointRate);
}

curves.add(points);
}
input.close();

mRgbCompositeControlPoints = curves.get(0);
mRedControlPoints = curves.get(1);
mGreenControlPoints = curves.get(2);
mBlueControlPoints = curves.get(3);
} catch (IOException e) {
e.printStackTrace();
}
}

然后根据RGB信息生成纹理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 // 设置纹理单元
GLES20.glActiveTexture(GLES20.GL_TEXTURE3);
// 绑定纹理
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mToneCurveTexture[0]);

if ((mRedCurve.size() >= 256) && (mGreenCurve.size() >= 256) && (mBlueCurve.size() >= 256) && (mRgbCompositeCurve.size() >= 256)) {
byte[] toneCurveByteArray = new byte[256 * 4];
for (int currentCurveIndex = 0; currentCurveIndex < 256; currentCurveIndex++) {
// BGRA for upload to texture
toneCurveByteArray[currentCurveIndex * 4 + 2] = (byte) ((int) Math.min(Math.max(currentCurveIndex + mBlueCurve.get(currentCurveIndex) + mRgbCompositeCurve.get(currentCurveIndex), 0), 255) & 0xff);
toneCurveByteArray[currentCurveIndex * 4 + 1] = (byte) ((int) Math.min(Math.max(currentCurveIndex + mGreenCurve.get(currentCurveIndex) + mRgbCompositeCurve.get(currentCurveIndex), 0), 255) & 0xff);
toneCurveByteArray[currentCurveIndex * 4] = (byte) ((int) Math.min(Math.max(currentCurveIndex + mRedCurve.get(currentCurveIndex) + mRgbCompositeCurve.get(currentCurveIndex), 0), 255) & 0xff);
toneCurveByteArray[currentCurveIndex * 4 + 3] = (byte) (255 & 0xff);
}

GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, 256 /*width*/, 1 /*height*/, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, ByteBuffer.wrap(toneCurveByteArray));
}

图片纹理

通过图片生成纹理ID,也就是滤镜图片:看一看资源文件目录信息:

下面是json和着色器源码:

  • json

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    {
    "filterList": [{
    "type": "filter",
    "name": "amaro",
    "vertexShader": "",
    "fragmentShader": "fragment.glsl",
    "uniformList":["blowoutTexture", "overlayTexture", "mapTexture"],
    "uniformData": {
    "blowoutTexture": "blowout.png",
    "overlayTexture": "overlay.png",
    "mapTexture": "map.png"
    },
    "strength": 1.0,
    "texelOffset": 0,
    "audioPath": "",
    "audioLooping": 1
    }]
    }
  • fragment.glsl内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
precision mediump float;

varying mediump vec2 textureCoordinate;

uniform sampler2D inputTexture;
uniform sampler2D blowoutTexture; //blowout;
uniform sampler2D overlayTexture; //overlay;
uniform sampler2D mapTexture; //map

uniform float strength;

void main()
{
vec4 originColor = texture2D(inputTexture, textureCoordinate.xy);
vec4 texel = texture2D(inputTexture, textureCoordinate.xy);
vec3 bbTexel = texture2D(blowoutTexture, textureCoordinate.xy).rgb;

texel.r = texture2D(overlayTexture, vec2(bbTexel.r, texel.r)).r;
texel.g = texture2D(overlayTexture, vec2(bbTexel.g, texel.g)).g;
texel.b = texture2D(overlayTexture, vec2(bbTexel.b, texel.b)).b;

vec4 mapped;
mapped.r = texture2D(mapTexture, vec2(texel.r, 0.16666)).r;
mapped.g = texture2D(mapTexture, vec2(texel.g, 0.5)).g;
mapped.b = texture2D(mapTexture, vec2(texel.b, 0.83333)).b;
mapped.a = 1.0;

mapped.rgb = mix(originColor.rgb, mapped.rgb, strength);

gl_FragColor = mapped;
}

效果图

1
2
3
4
5
6
static final float TEXTURE[] = {
0.0f, 1.0f,
1.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f,
};

渲染流程总结


染流程如下:顶点数据(Vertices) > 顶点着色器(Vertex Shader) > 图元装配(Assembly) > 几何着色器(Geometry Shader) > 光栅化(Rasterization) > 片元着色器(Fragment Shader) > 逐片元处理(Per-Fragment Operations) > 帧缓冲(FrameBuffer)。再经过双缓冲的交换(SwapBuffer),渲染内容就显示到了屏幕上。

总结

根据渲染流程,我们知道,美颜的操作即是对着色器编码的源码进行编写。采用不同的算法,实现不同的美颜功能。同时滤镜的纹理,可以是ps的acv文件,也可以是图片,着色器源码针对不同的 资源分别解析和使用。

参考资料

OpenGL ES

OpenGL入门1.2:渲染管线简介,画三角形

GLES2.0中文API

OpenGLES 绘制图片纹理