大家好,我是阿赵。
继续介绍屏幕后处理,这一期介绍一下Tonemapping色调映射
一、Tone Mapping的介绍
Tone Mapping色调映射,是一种颜色的映射关系处理,简单一点说,一般是从原始色调(通常是高动态范围,HDR)映射到目标色调(通常是低动态范围,LDR)。
由于HDR的颜色值是能超过1的,但实际上在LDR范围,颜色值最大只能是1。如果我们要在LDR的环境下,尽量模拟HDR的效果,超过1的颜色部分怎么办呢?
最直接想到的是两种可能:
1、截断大于1的部分
大于1的部分,直接等于1,小于1的部分保留。这种做法,会导致超过1的部分全部变成白色,在原始图片亮度比较高的情况下,转换完之后可能就是一片白茫茫的效果。
2、对颜色进行线性的缩放
把原始颜色的最大值看做1,然后把原始的所有颜色进行整体的等比缩放。这样做,能保留一定的效果。但由于原始的HDR颜色的跨度可能比0到1大很多,所以整体缩小之后,整个画面就会变暗很多了,没有了HDR的通透光亮的感觉。
为了能让HDR颜色映射到LDR之后,还能保持比较接近的效果,上面两种方式的处理显然都是不好的。
Tonemapping也是把HDR颜色范围映射到0-1的LDR颜色范围,但它并不是线性缩放,而是曲线的缩放。
从上面这个例子可以看出来,Tonemapping映射之后的颜色,有些地方是变暗了,比如深颜色的裤子,但有些地方却是变亮了的,比如头发和肩膀衣服上的阴影。整体的颜色有一种电影校色之后的感觉。
很多游戏美工在没有技术人员配合的情况下,都很喜欢自己挂后处理,其中Tonemapping应该是除了Bloom以外,美工们最喜欢的一种后处理了,虽然不知道为什么,但就是觉得颜色好看了。
虽然屏幕后处理看着好像很简单实现,挂个组件调几个参数,就能化腐朽为神奇,把原本平淡无奇的画面变得好看。但其实后处理都是有各种额外消耗的,所以我一直不是很建议美工们只会依靠后处理来扭转画面缺陷的,特别是做手游。
二、Tonemapping的代码实现
1、C#代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TonemappingCtrl : MonoBehaviour
{
private Material toneMat;
public bool isTonemapping = false;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
}
private bool TonemappingFun(RenderTexture source, RenderTexture destination)
{
if (toneMat == null)
{
toneMat = new Material(Shader.Find("Hidden/ToneMapping"));
}
if (toneMat == null || toneMat.shader == null || toneMat.shader.isSupported == false)
{
return false;
}
Graphics.Blit(source, destination, toneMat);
return true;
}
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if(isTonemapping == false)
{
Graphics.Blit(source, destination);
return;
}
RenderTexture finalRt = source;
if (TonemappingFun(finalRt,finalRt)==false)
{
Graphics.Blit(source, destination);
}
else
{
Graphics.Blit(finalRt, destination);
}
}
}
C#部分的代码和其他后处理没区别,都是通过OnRenderImage里面调用Graphics.Blit
2、Shader
Shader "Hidden/ToneMapping"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
// No culling or depth
Cull Off ZWrite Off ZTest Always
Pass
{
CGPROGRAM
#pragma vertex vert_img
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float3 ACES_Tonemapping(float3 x)
{
float a = 2.51f;
float b = 0.03f;
float c = 2.43f;
float d = 0.59f;
float e = 0.14f;
float3 encode_color = saturate((x*(a*x + b)) / (x*(c*x + d) + e));
return encode_color;
}
fixed4 frag (v2f_img i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
half3 linear_color = pow(col.rgb, 2.2);
half3 encode_color = ACES_Tonemapping(linear_color);
col.rgb = pow(encode_color, 1 / 2.2);
return col;
}
ENDCG
}
}
}
需要说明一下:
1.色彩空间的转换
由于默认显示空间是Gamma空间,所以先通过pow(col.rgb, 2.2)把颜色转换成线性空间,然后再进行Tonemapping映射,最后再pow(encode_color, 1 / 2.2),把颜色转回Gamma空间
2.Tonemapping映射算法
float3 ACES_Tonemapping(float3 x)
{
float a = 2.51f;
float b = 0.03f;
float c = 2.43f;
float d = 0.59f;
float e = 0.14f;
float3 encode_color = saturate((x*(a*x + b)) / (x*(c*x + d) + e));
return encode_color;
}
把颜色进行Tonemapping映射。这个算法是网上都可以百度得到的。
三、Tonemapping和其他后处理的配合
一般来说,Tonemapping只是一个固定颜色映射效果,所以应该是需要配合着其他的效果一起使用,才会得到比较好的效果。比如我之前介绍过的校色、暗角、Bloom等。
可以做出各种不同的效果,不同于原始颜色的平淡,调整完之后的颜色看起来会比较有电影的感觉。
这也是我为什么要在Unity有PostProcessing后处理插件的情况下,还要介绍使用自己写Shader实现屏幕后处理的原因。PostProcessing作为一个插件,它可能会存在很多功能,会有很多额外的计算,你可能只需要用到其中的某一个小部分的功能和效果。
而我们自己写Shader实现屏幕后处理,自由度非常的高,喜欢在哪里添加或者修改一些效果,都可以。
比如,我可以写一个脚本,把之前介绍过的所有后处理效果都加进去:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//[ExecuteInEditMode]
public class ImageEffectCtrl : MonoBehaviour
{
//--------调色-----------
private Material colorMat;
public bool isColorAjust = false;
[Range(-5,5)]
public float saturation = 1;
[Range(-5,5)]
public float contrast = 1;
[Range(0,1)]
public float hueShift = 0;
[Range(0,5)]
public float lightVal = 1;
[Range(0,3)]
public float vignetteIntensity = 1.8f;
[Range(0,5)]
public float vignetteSmoothness = 5;
//-------模糊-----------
private Material blurMat;
public bool isBlur = false;
[Range(0, 4)]
public float blurSize = 0;
[Range(-3,3)]
public float blurOffset = 1;
[Range(1,3)]
public int blurType = 3;
//-----光晕----------
private Material brightMat;
private Material bloomMat;
public bool isBloom = false;
[Range(0,1)]
public float brightCut = 0.5f;
[Range(0, 4)]
public float bloomSize = 0;
[Range(-3, 3)]
public float bloomOffset = 1;
public int bloomType = 3;
[Range(1, 3)]
//---toneMapping-----
private Material toneMat;
public bool isTonemapping = false;
// Start is called before the first frame update
void Start()
{
//if(colorMat == null||colorMat.shader == null||colorMat.shader.isSupported == false)
//{
// this.enabled = false;
//}
}
// Update is called once per frame
void Update()
{
}
private bool AjustColor(RenderTexture source, RenderTexture destination)
{
if(colorMat == null)
{
colorMat = new Material(Shader.Find("Hidden/AzhaoAjustColor"));
}
if(colorMat == null||colorMat.shader == null||colorMat.shader.isSupported == false)
{
return false;
}
colorMat.SetFloat("_Saturation", saturation);
colorMat.SetFloat("_Contrast", contrast);
colorMat.SetFloat("_HueShift", hueShift);
colorMat.SetFloat("_Light", lightVal);
colorMat.SetFloat("_VignetteIntensity", vignetteIntensity);
colorMat.SetFloat("_VignetteSmoothness", vignetteSmoothness);
Graphics.Blit(source, destination, colorMat, 0);
return true;
}
private Material GetBlurMat(int bType)
{
if(bType == 1)
{
return new Material(Shader.Find("Hidden/AzhaoBoxBlur"));
}
else if(bType == 2)
{
return new Material(Shader.Find("Hidden/AzhaoGaussianBlur"));
}
else if(bType == 3)
{
return new Material(Shader.Find("Hidden/AzhaoKawaseBlur"));
}
else
{
return null;
}
}
private bool CheckNeedCreateBlurMat(Material mat,int bType)
{
if(mat == null)
{
return true;
}
if(mat.shader == null)
{
return true;
}
if(bType == 1)
{
if(mat.shader.name != "Hidden/AzhaoBoxBlur")
{
return true;
}
else
{
return false;
}
}
else if(bType == 2)
{
if (mat.shader.name != "Hidden/AzhaoGaussianBlur")
{
return true;
}
else
{
return false;
}
}
else if (bType == 3)
{
if (mat.shader.name != "Hidden/AzhaoKawaseBlur")
{
return true;
}
else
{
return false;
}
}
else
{
return false;
}
}
private bool BlurFun(RenderTexture source, RenderTexture destination,float blurTime,int bType,float offset )
{
if(CheckNeedCreateBlurMat(blurMat,bType)==true)
{
blurMat = GetBlurMat(bType);
}
if (blurMat == null || blurMat.shader == null || blurMat.shader.isSupported == false)
{
return false;
}
blurMat.SetFloat("_BlurOffset", offset);
float width = source.width;
float height = source.height;
int w = Mathf.FloorToInt(width);
int h = Mathf.FloorToInt(height);
RenderTexture rt1 = RenderTexture.GetTemporary(w, h);
RenderTexture rt2 = RenderTexture.GetTemporary(w, h);
Graphics.Blit(source, rt1);
for (int i = 0; i < blurTime; i++)
{
ReleaseRT(rt2);
width = width / 2;
height = height / 2;
w = Mathf.FloorToInt(width);
h = Mathf.FloorToInt(height);
rt2 = RenderTexture.GetTemporary(w, h);
Graphics.Blit(rt1, rt2, blurMat, 0);
width = width / 2;
height = height / 2;
w = Mathf.FloorToInt(width);
h = Mathf.FloorToInt(height);
ReleaseRT(rt1);
rt1 = RenderTexture.GetTemporary(w, h);
Graphics.Blit(rt2, rt1, blurMat, 1);
}
for (int i = 0; i < blurTime; i++)
{
ReleaseRT(rt2);
width = width * 2;
height = height * 2;
w = Mathf.FloorToInt(width);
h = Mathf.FloorToInt(height);
rt2 = RenderTexture.GetTemporary(w, h);
Graphics.Blit(rt1, rt2, blurMat, 0);
width = width * 2;
height = height * 2;
w = Mathf.FloorToInt(width);
h = Mathf.FloorToInt(height);
ReleaseRT(rt1);
rt1 = RenderTexture.GetTemporary(w, h);
Graphics.Blit(rt2, rt1, blurMat, 1);
}
Graphics.Blit(rt1, destination);
ReleaseRT(rt1);
rt1 = null;
ReleaseRT(rt2);
rt2 = null;
return true;
}
private bool BrightRangeFun(RenderTexture source, RenderTexture destination)
{
if(brightMat == null)
{
brightMat = new Material(Shader.Find("Hidden/BrightRange"));
}
if (brightMat == null || brightMat.shader == null || brightMat.shader.isSupported == false)
{
return false;
}
brightMat.SetFloat("_BrightCut", brightCut);
Graphics.Blit(source, destination, brightMat);
return true;
}
private bool BloomAddFun(RenderTexture source,RenderTexture destination, RenderTexture brightTex)
{
if(bloomMat == null)
{
bloomMat = new Material(Shader.Find("Hidden/AzhaoBloom"));
}
if (bloomMat == null || bloomMat.shader == null || bloomMat.shader.isSupported == false)
{
return false;
}
bloomMat.SetTexture("_brightTex", brightTex);
Graphics.Blit(source, destination, bloomMat);
return true;
}
private bool TonemappingFun(RenderTexture source, RenderTexture destination)
{
if(toneMat == null)
{
toneMat = new Material(Shader.Find("Hidden/ToneMapping"));
}
if (toneMat == null || toneMat.shader == null || toneMat.shader.isSupported == false)
{
return false;
}
Graphics.Blit(source, destination, toneMat);
return true;
}
private void CopyRender(RenderTexture source,RenderTexture destination)
{
Graphics.Blit(source, destination);
}
private void ReleaseRT(RenderTexture rt)
{
if(rt!=null)
{
RenderTexture.ReleaseTemporary(rt);
}
}
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
RenderTexture finalRt = source;
RenderTexture rt2 = RenderTexture.GetTemporary(source.width, source.height);
RenderTexture rt3 = RenderTexture.GetTemporary(source.width, source.height);
if (isBloom == true)
{
if(BrightRangeFun(finalRt, rt2)==true)
{
if(BlurFun(rt2, rt3, bloomSize,bloomType,bloomOffset)==true)
{
if(BloomAddFun(source, finalRt, rt3)==true)
{
}
}
}
}
if(isBlur == true)
{
if (blurSize > 0)
{
if (BlurFun(finalRt, finalRt, blurSize,blurType,blurOffset) == true)
{
}
}
}
if (isTonemapping == true)
{
if (TonemappingFun(finalRt, finalRt) == true)
{
}
}
if (isColorAjust == true)
{
if (AjustColor(finalRt, finalRt) == true)
{
}
}
CopyRender(finalRt, destination);
ReleaseRT(finalRt);
ReleaseRT(rt2);
ReleaseRT(rt3);
}
}
一个脚本控制所有后处理。当然这样的做法只是方便,也不见得很好,我还是比较喜欢根据实际用到多少个效果,单独去写对应的脚本,那样我觉得性能才是最好的。