系列文章:
特效的实现原理
接下来这篇文章我们讲下特效的具体实现原理。
由于预览画面的渲染是将Surface传给CameraDevice由它去绘制的,而且我没有找到什么可以接管或者添加渲染效果的接口,所以并不能直接去处理摄像头的画面。
于是这里我们只能用一种游戏中常用的手段去处理,这种手段的名字叫做RTT(render to texture),中文名叫做渲染到纹理。
玩法是先将我们想要处理的画面,不直接绘制到屏幕,而是绘制成一张图片,然后我们再拿这张图片去做一些特殊的处理,或者特殊的用途:
5.png
例如游戏中水面的倒影一种比较古老的实现方法就是先将岸上的画面绘制成一张图片,然后倒过来然后做一些扭曲、模糊、淡化等处理,然后贴到水面上。
又例如下面这种狙击镜的实现原理就是先将摄像头位置调到远处,将远处的画面绘制到一张贴图上,然后将摄像头位置再调回角色处,把刚刚得到的远处的画面的图片直接贴到狙击镜上:
4.jpeg
所以在这个特效相机的例子里面我们的实现原理如下:
6.png
OpenGL实现
我们使用OpenGL ES 2.0版本,这个版本要求我们用GLSL实现顶点着色器和片元着色器。这两个着色器其实是两个运行在GPU的程序。
GLSL全称是OpenGL Shading Language即OpenGL着色语言,它在语法上和C语言有点像。只是看的话相信大家都能看懂,我就不仔细介绍语法了。
OpenGL可编程渲染管线的整个流程比较复杂,作为初学者我们只要理解其中的顶点着色器和和片元着色器就可以了。简单来讲就是OpenGL会在顶点着色器确定顶点的位置,然后这些顶点连起来就是我们想要的图形。接着在片元着色器里面给这些图形上色:
1.png
我们直接看看两个着色器的代码。
顶点着色器
OpenGL会将每个顶点的坐标传递给顶点着色器,我们可以在这里改变顶点的位置。例如我们给每个顶点都加上一个偏移,就能实现整个图形的移动。
在这个demo里面我们不改变顶点的坐标,只是简单的将它从二维转换成四维。现实世界里面都是三维的,那为什么要装换成四维呢?原因是我们可以用4*4的矩阵对坐标进行旋转、缩放、平移等变换,但是4*4的矩阵只能和四维向量相乘,所以需要在xyz之外加多一个维度,我们一般情况下直接把这个维度的值设成1就好。然后将计算得到的四维坐标放到gl_Position作为最终结果值:
attribute vec2 vPosition;
attribute vec2 vCoord;
varying vec2 vPreviewCoord;
uniform mat4 matTransform;
void main() {
gl_Position = vec4(vPosition, 0, 1);
vPreviewCoord = (matTransform * vec4(vCoord.xy, 0, 1)).xy;
}
然后除了vPosition这个顶点的坐标,大家还会看到vCoord,它是纹理坐标。什么是纹理坐标呢?
纹理其实可以理解成图片,我们将图片的左下角定义成原点(0,0),左上角、右上角、右下角分别为(0,1)、(1,1)、(1,0):
2.png
我们的每个顶点,除了携带顶点坐标之外,还携带了纹理坐标的信息,顶点坐标确定了这个图形的形状,而纹理坐标则确定贴图要怎么样贴到这个图形上。然后在片元着色器里面就可以根据这个纹理坐标去给图形贴上贴图了:
3.png
不过看到代码可以看到,我们这里还用matTransform这个矩阵对纹理坐标进行了变换。这里是由于我们的图片不是普通的图片,而是将摄像头的画面画到另外一个surface之后拿过来的,需要进行变换。这块等下再仔细讲解。
片元顶点着色器
#extension GL_OES_EGL_image_external : require
precision highp float;
varying vec2 vPreviewCoord;
uniform samplerExternalOES texPreview;
uniform mat4 uColorMatrix;
void main() {
gl_FragColor = uColorMatrix * texture2D(texPreview, vPreviewCoord).rgba;
}
片元着色器就比较简单了,第一行是由于我们使用了samplerExternalOES需要开启特殊配置,这个是由于在安卓上我们只能用samplerExternalOES类型的纹理去接收摄像头的画面,而使用samplerExternalOES需要开启GL_OES_EGL_image_external功能。
然后这个texPreview就是我们摄像头画面绘制成的那张图片了,我们用texture2D这个方法去读取图片某个像素的颜色值,它的第一个参数就是我们的纹理,第二个参数就是我们的纹理坐标,也就是上一步顶点着色器计算的到的纹理坐标:
vPreviewCoord = (matTransform * vec4(vCoord.xy, 0, 1)).xy;
这里有同学可能会疑问我们在顶点着色器不是只计算了顶点