(从这里开始可能会记录的更简略一些,时间紧张想迅速读完这本书的主要内容,可能有的部分不会自己上手做)
屏幕后处理通常指渲染完整个场景得到屏幕图像后,再对图像进行操作,抓取屏幕可以使用OnRenderImage(RenderTexture src, RenderTexture dest)将当前渲染的图像存储在 第一个参数对应源渲染纹理中,执行函数中的操作后再进行目标纹理渲染,即第二个参数对应的渲染纹理显示到屏幕上。
通常过程为,在摄像中添加一个用于屏幕后处理的脚本,使用OnRenderImage来信获取当前屏幕的渲染纹理,再调用Graphics.Blit函数使用特定的Shader来对图像处理,再将返回的渲染纹理显示到屏幕上。
原图:
调整亮度、饱和度、对比度
亮度调整:原颜色乘亮度系数得到一个颜色值,再通过每个颜色分量乘一个系数得到亮度值,再使用该亮度创建一个饱和度为0的颜色值,并用该颜色值与上面亮度系数的颜色值以_Saturation进行插值,得到饱和度颜色,最终的颜色为对比度为0的颜色与饱和度颜色以_Contrast属性进行插值的结果。
选中材质并附着shader:
protected Material CheckShaderAndCreateMaterial(Shader shader, Material material) {
if (shader == null) {
return null;
}
if (shader.isSupported && material && material.shader == shader)
return material;
if (!shader.isSupported) {
return null;
}
else {
material = new Material(shader);
material.hideFlags = HideFlags.DontSave;
if (material)
return material;
else
return null;
}
}
frag:
fixed4 frag(v2f i) : SV_Target {
fixed4 renderTex = tex2D(_MainTex, i.uv);
// Apply brightness
fixed3 finalColor = renderTex.rgb * _Brightness;
// Apply saturation
fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b;
fixed3 luminanceColor = fixed3(luminance, luminance, luminance);
finalColor = lerp(luminanceColor, finalColor, _Saturation);
// Apply contrast
fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
finalColor = lerp(avgColor, finalColor, _Contrast);
return fixed4(finalColor, renderTex.a);
}
边缘检测
原理为应用一些边缘检测算子对图像进行卷积操作,图形学中简化的卷积知识见 games101 lecture6。 卷积操作的效用在于选择不同的卷积核,边缘体现了相邻像素之间存在明显的颜色、亮度、纹理等属性差别,这种差别可以用梯度来表示,边缘处梯度的绝对值比较大。常见的边缘检测算子包含两个方向卷积核分别用于检测水平与竖直,我们对每个像素卷积后的到两个方向上的梯度值,计算整体梯度近似并以此判断边缘。
先放结果:
xxx_TexelSize用于访问纹理对应每个纹素的大小,利用该值计算各个相邻区域的纹理坐标,再vert中计算边缘检测要用的纹理坐标,v2f中定义一个纹理数组(half2 表示二维的16位浮点数)维数为9,得到使用sobel采样时需要的9个纹理坐标。
v2f vert(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);
o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);
o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);
o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);
return o;
}
片元着色器中实现了调用sobel函数计算每个像素的梯度值edge,并用edge分别计算背景为原图与纯色下的颜色值,利用输入的值边缘度对二者进行插值。下面luminance得到的是按照特定系数乘三个分量得到的亮度颜色,即显示颜色,对其进行卷积操作。
fixed luminance(fixed4 color) {
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}
half Sobel(v2f i) {
const half Gx[9] = {-1, 0, 1,
-2, 0, 2,
-1, 0, 1};
const half Gy[9] = {-1, -2, -1,
0, 0, 0,
1, 2, 1};
half texColor;
half edgeX = 0;
half edgeY = 0;
for (int it = 0; it < 9; it++) {
texColor = luminance(tex2D(_MainTex, i.uv[it]));
edgeX += texColor * Gx[it];
edgeY += texColor * Gy[it];
}
half edge = 1 - abs(edgeX) - abs(edgeY);
return edge;
}
高斯模糊
使用高斯核进行卷积计算,其计算基于高斯方程,标准方差σ一般取1,xy表示当前位置到核中心的整数,计算核中各个位置的高斯核,并对其归一化即让每个权重除以所有权重的和可以保证图像不会变暗。
高斯核维度越高,模糊程度越大,NXN的高斯核对图像卷积滤波,就需要NXNXWXH次采样,n越大 采样次数会更大,把二维的高斯函数拆分为两个一维的高斯函数结果相同,但采样次数变为2xNxWxH,再shader中先后调用两个pass分别两个方向的高斯核滤波。通过图像缩放进一步提高性能,通过改变滤波次数来控制模糊程度。
脚本中调用两次blit,将第一次竖直方向卷积结果存入缓存纹理,再调用blit再缓存纹理基础上进行第二次卷积结果存储显示。
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
int rtW = src.width/downSample;
int rtH = src.height/downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear;
Graphics.Blit(src, buffer0);
for (int i = 0; i < iterations; i++) {
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// Render the vertical pass
Graphics.Blit(buffer0, buffer1, material, 0);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// Render the horizontal pass
Graphics.Blit(buffer0, buffer1, material, 1);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
Graphics.Blit(buffer0, dest);
RenderTexture.ReleaseTemporary(buffer0);
} else {
Graphics.Blit(src, dest);
}
}
两个pass各自有vert,共用一个frag;在vert中完成。GGINCLUDE与ENDCG语义放于Pass语义块中可以避免需要重复写相同的frag,blursize用于控制采样距离,顶点着色器中做的事情和之前类似,就是将从texcoord得到的中心坐标得到要采样的周围点 存入uv数组中。在顶点着色器中通过uv数组中坐标对应到纹理上的坐标,进行处理。
fixed4 fragBlur(v2f i) : SV_Target {
float weight[3] = {0.4026, 0.2442, 0.0545};
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
for (int it = 1; it < 3; it++) {
sum += tex2D(_MainTex, i.uv[it*2-1]).rgb * weight[it];
sum += tex2D(_MainTex, i.uv[it*2]).rgb * weight[it];
}
Bloom效果
模拟画面中较亮的区域扩散到周围的区域中,造成一种朦胧的效果。
原理:根据一个阈值提取出图像中较亮区域,存储在一张纹理中,对其进行高斯模糊,再与原图像混合。
仍然使用CGINCLUDE与ENDCG组织代码,在提取较亮区域的部分:顶点着色器中只需实现位置变换与纹理获取,在片元着色器中将采样得到的亮度值与阈值相减并控制范围。接着实现混合亮图像与原图像时使用的顶点着色器与片元着色器,在顶点着色器中完成对两个纹理的获得,片元着色器中将其混合。
共用到了四个pass,第一个用于获取较亮区域,第二第三pass用于高斯采样,最后一个用于混合。
获取较亮区域的片元着色器:
fixed4 fragExtractBright(v2f i) : SV_Target {
fixed4 c = tex2D(_MainTex, i.uv);
fixed val = clamp(luminance(c) - _LuminanceThreshold, 0.0, 1.0);
return c * val;
}
用于混合的顶点片元着色器:
v2fBloom vertBloom(appdata_img v) {
v2fBloom o;
o.pos = UnityObjectToClipPos (v.vertex);
o.uv.xy = v.texcoord;
o.uv.zw = v.texcoord;
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0.0)
o.uv.w = 1.0 - o.uv.w;
#endif
return o;
}
fixed4 fragBloom(v2fBloom i) : SV_Target {
return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.zw);
}