零. 前言
票圈里总能看到一些很唯美的滤镜,让我惊叹之余又好奇,这个东西是怎么实现的呢,后面经过一番搜索之后,发现了有LUT这个好东西,自己鼓捣了一番,借鉴了别的APP的滤镜,然后得到了下面的效果:
虽然抄过来的滤镜和原滤镜相比还是有很大区别,原滤镜应该还有自己的算法,但这个效果也还算可以啦,来看看怎么弄的吧~
一. 实现原理
LUT的实现原理其实是基于RGB的映射,由原始的RGB映射到结果的RGB中去,而LUT的作用就是这次映射过程中的查找表。
举个例子,比如你参加一场考试,现在给你一张表和三个数字,第一个数字代表哪栋楼,第二个数字代表哪一层,第三个数字代表哪个房间
现在给个数字345,那你在这张表里面就能找到,你要去科研楼12层的15号房间参加考试,这个原理和LUT的原理非常接近,可以这样初步理解一下。
我们知道,RGB是相互独立的三种颜色通道,其取值范围均为[0, 255],如果我们需要建立一个映射表,就可以用一个三维的数组来存储,总共有256 * 256 * 256种情况。
但有个问题,256的三次方,实在太大了,相当于我们需要这么多个像素情况进行一一映射,显然,对于一个APP来说,弄这么大的映射表实在浪费性能。
幸好,LUT通过巧妙的设计,用一张图包含了所有的信息,他是怎么做到的呢,可以看下面这张图:
LUT分割成了8 * 8 = 64个方格,每个方格等分成了64 * 64个像素,如果够细心的话,我们可以看到这里可以用64 * 64 * 64来对应RGB值了,和256 * 256 * 256相比,我们按1:4的值进行了压缩,也就是说,LUT里面的每一个像素跨度了4个值的信息。
如上图所示,LUT里,对于不同的小方格,其R、G的分布规律是相同的,而在同一个小方格中,从左到右包含了64个R值,从上到下包含了64个G值,呈现递增关系。
而对于同一个小方格,其B值是相同的,对于不同方格来说,B值随着方格所在的位置变化而变化,从左到右、从上到下递增。
一句话总结就是:根据B值定位到小方格、根据RG值定位到小方格里面的像素点。
知道了上面的原理之后,我们就可以根据原有的RGB映射到新的RGB了
二. 开始实战
我们来举个例子,我们来在LUT表里面找到(R, G, B) = (255, 255, 255)对应的像素。
1. 归一化
首先,将RGB归一化除以255,得到(1,1,1)。
2. 根据B值定位小方格
由于一共有[0, 63]共64个小方格,我们可以根据B * 63得到对应的小方格的位置,所以我们的小方格是第64个小方格,下标n=63。
我们根据n,定位小方格相对于LUT图的quadX、quadY,可以得到
quadY = floor(n / 8) = 7
quadX = n - quadY * 8 = 7
也就是该小方格处于第7行第7列。(下标从0开始算)
3. 根据RG值定位像素位置
首先定位该像素在小方格里面的相对位置,R决定了x坐标,G决定了y坐标:
stepSize = 0.5 // 由于要取中点,所以得到像素的左上角之后再+0.5
squareX = R * 63 + stepSize = 63.5
squareY = G * 63 + stepSize = 63.5
每个小方格大小为64 * 64,再定位到该像素点的位置:
x = quadX * 64 + squareX = 511.5
y = quadY * 64 + squareY = 511.5
最后归一化,得到该像素点对应的坐标
x = 511.5 / 512
y = 511.5 / 512
三. 更复杂的情况
当然,一个像素点很大概率不是整数,而有可能是小数,那就说一下小数的情况,如(0.6, 0.8, 0.2):
1. 根据B值定位小方格
由于B = 0.2,我们根据n = 63 * 0.2 = 12.6,可以得到两个小方格,n=12、n=13,最终结果由这两个小方格的像素点混合而成
其中:
quad1.y = floor(12.6) / 8 = 1
quad1.x = floor(12.6) - quad1.y * 8 = 4
quad2.y = ceil(12.6) / 8 = 1
quad2.x = ceil(12.6) - quad2.y * 8 = 5
可以看到两个小方格分别分布在第二行(下标为1)的第5个(下标为4)和第6个(下标为5)。
2. 根据RG值定位像素位置
首先定位该像素在小方格里面的相对位置,R决定了x坐标,G决定了y坐标:
stepSize = 0.5 // 由于要取中点,所以得到像素的左上角之后再+0.5
squareX = R * 63 + stepSize = 38.3
squareY = G * 63 + stepSize = 50.9
找到上面两个小方格像素点相对于LUT图的位置:
texPos1.x = (quad1.x * 64 + squareX) / 512 = 294.3 / 512
texPos1.y = (quad1.y * 64 + squareY) / 512 = 306.9 / 512
texPos2.x = (quad2.x * 64 + squareX) / 512 = 294.3 / 512
texPos2.y = (quad2.y * 64 + squareY) / 512 = 370.9 / 512
最后采样、混合即可,值得注意的是,需要根据B值的小数部分决定mix的percent值,小数部分越大,越靠近正方形2
float4 newColor1 = lookupTableTexture.sample(textureSampler, texPos1); // 正方形1的颜色值
float4 newColor2 = lookupTableTexture.sample(textureSampler, texPos2); // 正方形2的颜色值
float4 newColor = mix(newColor1, newColor2, fract(blueColor)); // 根据小数点的部分进行mix
我们还可以根据slideBar值决定最后的颜色更接近哪一边:
return mix(textureColor, float4(newColor.rgb, textureColor.a), intensity);
Shader代码如下:
#include <metal_stdlib>
#include "CCAVPShaderTypes.h"
using namespace metal;
constant float stepSize = 0.5;
fragment float4 lutFragment(SingleInputVertexIO input [[ stage_in ]],
texture2d<float> normalTexture [[ texture(0) ]],
texture2d<float> lookupTableTexture [[ texture(1) ]],
constant float &intensity [[ buffer(0) ]])
{
constexpr sampler textureSampler (mag_filter::linear,
min_filter::linear);
float4 textureColor = normalTexture.sample(textureSampler, input.textureCoordinate); //正常的纹理颜色
float blueColor = textureColor.b * 63.0; // 蓝色部分[0, 63] 共64种
float2 quad1; // 第一个正方形的位置, 假如blueColor=22.5,则y=22/8=2,x=22-8*2=6,即是第2行,第6个正方形;(因为y是纵坐标)
quad1.y = floor(floor(blueColor) * 0.125);
quad1.x = floor(blueColor) - (quad1.y * 8.0);
float2 quad2; // 第二个正方形的位置,同上。注意x、y坐标的计算,还有这里用int值也可以,但是为了效率使用float
quad2.y = floor(ceil(blueColor) * 0.125);
quad2.x = ceil(blueColor) - (quad2.y * 8.0);
// 该像素点相对于小方格的位置(取中间点,所以乘以63再加0.5)
float squareX = textureColor.r * 63 + stepSize;
float squareY = textureColor.g * 63 + stepSize;
float2 texPos1; // 正方形1对应像素点相对于LUT图的位置
texPos1.x = quad1.x * 64 + squareX;
texPos1.y = quad1.y * 64 + squareY;
float2 texPos2; // 正方形2对应像素点相对于LUT图的位置
texPos2.x = quad2.x * 64 + squareX;
texPos2.y = quad2.y * 64 + squareY;
float4 newColor1 = lookupTableTexture.sample(textureSampler, texPos1 / 512); // 正方形1的颜色值
float4 newColor2 = lookupTableTexture.sample(textureSampler, texPos2 / 512); // 正方形2的颜色值
float4 newColor = mix(newColor1, newColor2, fract(blueColor)); // 根据小数点的部分进行mix
return mix(textureColor, float4(newColor.rgb, textureColor.a), intensity);
}
四. 怎么用其他软件的滤镜
有个很简单的方法,下图是原始的LUT图,我们把这个图放进其他APP后,可以得到另一个LUT图
比如我找了个油画滤镜,得到了下面的效果:
然后把这个LUT图导入,和原图一起输入,就能得到开头的效果啦~不过和原滤镜的效果还是有比较大的区别,可能他们加了点算法吧= =