背景
图片基础知识
图片现在主要分为位图和矢量图,矢量图虽然能保证缩小和放大下图片不失真,但是表达像素范围过低,一般只用来表示标识、图标、Logo等简单直接的图像。因此我们现在的算法都是操作位图。位图图像也称为点阵图像,位图使用我们称为像素的一格一格的小点来描述图像,一格的颜色由RGBA(Red,Green,Blue,Alpha)来决定。
效果渲染
目前很多相机应用都主打效果渲染,那渲染原理是什么呢?简单来说就是将原图的RGBA值经过一定算法转换成另一种值,比如滤镜、增强效果就是典型的转换。
但是有一个问题:基于像素级别的渲染属于IO密集型操作,所以GPU比较适合,但是GPU有个缺点就是不适合执行一些复杂计算,因此渲染时候就会面临一个问题,在shader脚本里执行算法计算新的RGBA值会造成性能压力,严重就会出现ANR等卡顿问题。
那有解决办法吗,答案是肯定的,其实从另一方面想,算法相当于一个函数,我们传进去RGBA值,它返回新的RGBA值,那就涉及到性能优化的一个通用原则了:预加载。我们为啥不先计算出每种RGBA值的返回结果,然后GPU只要负责查找对应映射值就行了?接下来就来介绍一下这种方法
颜色查找表
正如我们上面提到的解决办法,如何提前计算每个RGBA值的返回结果呢?其实也不难,我们用遍历法取RGB值范围内所有组合,然后用一个数组存放所有取值的映射结果就行了,不过有两个点需要考虑:
RGB三个维度,那我们要建立一个三维数组?
数组大小多少比较合适,即RGB划分粒度多少不会影响效果?
针对上面问题,我们有以下几个考虑和设计
既然要预加载,那么就会考虑大小问题,我们肯定是希望占用空间越小越好,所以还是用一个二维数组
数组划分粒度涉及到效果精细度(插值),16等分一般就能满足用户P图的需要
既然有了设计,就开始实现吧,先来看一下几个概念:
映射表
对于ps里面的曲线调整,简单来讲就是统一对固定的像素(适用于单通道映射)进行调整,如下图,从RGB的角度来讲就是将比如都是为128的颜色增加到150,35的颜色增加到50。既然固定颜色增加的频率是一样的,那就可以用查找表的方法来映射

基准图
映射表可以解决单通道映射的像素变换,但是对于RGB同时改变的图像变换来说就无法支持,因此引入了基准图的概念。基准图简而言之就是通过将每一个原始的颜色进行转换成为一个新的颜色。
先来看一下基准图长啥样:

那基准图是如何发挥作用呢?
基准图本质是一个颜色查找表,即将所有的颜色进行一次(矩阵)转换,很多的滤镜功能就是提供了这么一个转换的矩阵,在原始色彩的基础上进行颜色的转换。
基准图的作用就是预计算所有RGB映射后的值,在具体渲染时候就可以拿原图的RGB查找映射后的RGB,这样渲染的处理一下子就变得简单多了。打一个比方,比如原始颜色是红色(r:255,g:0,b:0),进行转换后变为绿色(r:0,g:255,b:0),以后所有是红色的地方都会被自动转换为绿色。
那渲染时如何映射呢?这就涉及到基准图的解析,如下图所示,我们将RGB全部均等分为16等分,然后包含在一张图中,将该图作为PS里的一个图层和原始图片一起参与效果调试,调完效果之后的基准图作为我们效果映射的图表。最后根据我们实际的RGB去查询位置,由于是16等分因此我们实际查找的值是取两个临近的值做差值计算出结果像素的位置

备注:
蓝色从左到右,从上到下均等分为16份,意思是第一格blue值就是1/16,第16格blue值就是1,所以可以看到右下角的那一格蓝色最深
每一格中红色和绿色代表x轴和y轴,即每一格中x从左到右,红色值就是从0到1,y轴从上到下就是绿色值从0到1
实际代码解析:
varying vec2 mt_CameraIndex;uniform sampler2D mt_mask_0;uniform float alpha;lowp vec4 lut3d(highp vec4 textureColor){//取得原始图对应位置的颜色,b通道的数据乘以16(0~15)因为查找表示4*4mediump float blueColor = textureColor.b * 15.0;//floor返回小于等于x的最大整数值mediump vec2 quad1;quad1.y = max(min(4.0,floor(floor(blueColor) 4.0)),0.0);quad1.x = max(min(4.0,floor(blueColor) - (quad1.y * 4.0)),0.0);//ceil返回大于等于x的最小整数值mediump vec2 quad2;quad2.y = max(min(floor(ceil(blueColor) 4.0),4.0),0.0);quad2.x = max(min(ceil(blueColor) - (quad2.y * 4.0),4.0),0.0);highp vec2 texPos1;//0.25是一个蓝色格的宽或者高,1/64是一个红绿坐标格宽高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(mt_mask_0, texPos1);lowp vec4 newColor2 = texture2D(mt_mask_0, texPos2);//返回线性混合的x和y,如:x⋅(1−a)+y⋅a//返回x-floor(x),即返回x的小数部分//x,y,z,w。w为第四位mediump vec4 newColor = mix(newColor1, newColor2, fract(blueColor));return newColor;}void main(){//取得原始图对应位置的颜色vec4 orgColor =texture2D(inputTexture, mt_CameraIndex);vec4 tempColor = lut3d(orgColor);gl_FragColor = mix(orgColor,tempColor,alpha);}
上面代码第一次看是会有点混乱,下面做个说明:
根据蓝色值先确定映射坐标位于16格中的哪一格,因为shader中blue值是[0,1],所以先乘以15把范围调整到[0,15],然后根据blue的值确定是哪一格。具体的位置用x,y坐标表示,其中x,y范围都是[0,3],而blue = y* 4 + x;所以y轴的值范围就是blue/4的下取整和上取整之间,这边最后取两种情况的加权值,y计算后x就按公式带一下就行了。
在确定是哪一格后再继续计算这一格内的具体位置,以x轴为例,
x = (quad1.x* 0.25) + 0.5/64.0 + ((0.25 - 1.0/64.0) * textureColor.r)
其中quad1.x是我们计算出来的蓝色格位置坐标的x轴,因为每行有4格,所以x * 0.25就是该格的最左边位置,又因为每个蓝色格划分成16x16的子方格,所以每行有64个子方格,以子方格左上角第一格的中点为原点,因此需要加上(1/64.0)/2,以red和green值为x轴和y轴计算具体位置,因为16格中每一格大小是0.25x0.25,所以red和green值需要乘以0.25,因为我们是以左上角第一格中心点为原点,前面已经加上半格大小了,所以整个蓝色格需要减去一个子方格的宽度后再计算位置。y轴具体位置计算和x轴一样。
经过上面的计算,我们得到了当前颜色对应基准图的具体坐标,然后我们根据基准图找到对应的颜色值就是我们想要的效果颜色了
因为颜色的搭配有很多种,所以我们不可能把每种搭配都放进基准图中,这边16等分就是一种采样,当shader从基准图中取颜色值时会自动根据插值算法来得到比较接近实际颜色的值,这种已经能够满足绝大多数需求了。
总结
基准图是目前普遍认可的一种查表法,PS也是基于这个做出效果渲染,举个例子,特效人员可以在Ps软件上基于原图调整效果,当调整到满意的效果后就可以导出一张基准图供P图软件使用,这就是一个简单滤镜包的生产过程,这样做的好处是特效人员可以不用写代码,分工合作(#^.^#)
好了,今天的分享就到这了,在当今P图盛行的环境下,了解一下特效包的生产和使用还是有必要的^_^




