掛在攝像機上的腳本,把source抄到中間臨時紋理,然後再到destination
void OnRenderImage (RenderTexture source, RenderTexture destination) {
RenderTexture r = RenderTexture.GetTemporary(
source.width, source.height, 0, source.format
);
Graphics.Blit(source, r);
Graphics.Blit(r, destination);
RenderTexture.ReleaseTemporary(r);
}
Blur:利用GPU內置的雙線性過濾,最爲簡單並且運行效率最高。通過改變臨時紋理分辨率爲一半(除以2)downsampling,在回到destination,upsampling即可。
這樣可以除以2、4、8、16、32……用更小分辨率的中間紋理得到更模糊的圖像,但是效果不佳。
爲提高效果,要漸進處理不要直接使用source直接blit到小分辨率中間紋理上。
[Range(1, 16)]
public int iterations = 4;
RenderTexture[] textures = new RenderTexture[16];
void OnRenderImage (RenderTexture source, RenderTexture destination) {
int width = source.width / 2;
int height = source.height / 2;
RenderTextureFormat format = source.format;
RenderTexture currentDestination = textures[0] =
RenderTexture.GetTemporary(width, height, 0, format);
Graphics.Blit(source, currentDestination);
RenderTexture currentSource = currentDestination;
int i = 1;
for (; i < iterations; i++) {
width /= 2;
height /= 2;
if (height < 2) {
break;
}
currentDestination = textures[i] =
RenderTexture.GetTemporary(width, height, 0, format);
Graphics.Blit(currentSource, currentDestination);
currentSource = currentDestination;
}
for (i -= 2; i >= 0; i--) {
currentDestination = textures[i];
textures[i] = null;
Graphics.Blit(currentSource, currentDestination);
RenderTexture.ReleaseTemporary(currentSource);
currentSource = currentDestination;
}
Graphics.Blit(currentSource, destination);
RenderTexture.ReleaseTemporary(currentSource);
}
改善GPU雙線性的效果
要用自己box filter kernel代替GPU的採樣,要使用自己的shader
Shader "Custom/Bloom" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
}
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_TexelSize;
struct VertexData {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct Interpolators {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
// vs沒有特別用處,ps定義在不同的pass裏
Interpolators VertexProgram (VertexData v) {
Interpolators i;
i.pos = UnityObjectToClipPos(v.vertex);
i.uv = v.uv;
return i;
}
// 採樣,只是rgb
half3 Sample (float2 uv) {
return tex2D(_MainTex, uv).rgb;
}
// box 採樣,通過delta來決定是up還是down
half3 SampleBox (float2 uv, float delta) {
float4 o = _MainTex_TexelSize.xyxy * float2(-delta, delta).xxyy;
half3 s =
Sample(uv + o.xy) + Sample(uv + o.zy) +
Sample(uv + o.xw) + Sample(uv + o.zw);
return s * 0.25f;
}
ENDCG
SubShader {
Cull Off
ZTest Always
ZWrite Off
Pass { // 0
CGPROGRAM
#pragma vertex VertexProgram
#pragma fragment FragmentProgram
// 傳1,down
half4 FragmentProgram (Interpolators i) : SV_Target {
return half4(SampleBox(i.uv, 1), 1);
}
ENDCG
}
Pass { // 1
CGPROGRAM
#pragma vertex VertexProgram
#pragma fragment FragmentProgram
// 傳0.5,up
half4 FragmentProgram (Interpolators i) : SV_Target {
return half4(SampleBox(i.uv, 0.5), 1);
}
ENDCG
}
}
}
shader通過一個材質給Blit使用:
[NonSerialized]
Material bloom;
// Blit使用那個pass
const int BoxDownPass = 0;
const int BoxUpPass = 1;
void OnRenderImage (RenderTexture source, RenderTexture destination) {
if (bloom == null) {
bloom = new Material(bloomShader);
bloom.hideFlags = HideFlags.HideAndDontSave;
}
…
Graphics.Blit(source, currentDestination, bloom, BoxDownPass);
…
Graphics.Blit(currentSource, currentDestination, bloom, BoxDownPass);
…
Graphics.Blit(currentSource, currentDestination, bloom, BoxUpPass);
…
Graphics.Blit(currentSource, destination, bloom, BoxUpPass);
…
}
Bloom
bloom的第一步就是blur,第二步就是把blur的圖像和原始圖做additive
增加第3個pass,和對原始紋理的採樣
sampler2D _MainTex, _SourceTex;
Pass { // 2
CGPROGRAM
#pragma vertex VertexProgram
#pragma fragment FragmentProgram
half4 FragmentProgram (Interpolators i) : SV_Target {
half4 c = tex2D(_SourceTex, i.uv);
c.rgb += SampleBox(i.uv, 0.5);
return c;
}
ENDCG
}
腳本使用pass:
const int BoxDownPass = 0;
const int BoxUpPass = 1;
const int ApplyBloomPass = 2;
bloom.SetTexture("_SourceTex", source);
Graphics.Blit(currentSource, destination, bloom, ApplyBloomPass);
RenderTexture.ReleaseTemporary(currentSource);
threshold, soft threshold
只有很亮的地方纔應該收到bloom影響,要排除掉不受影響的像素就用threshold來調節。
在shader中加入prefilter來在blur前處理,這裏使用了threshold和soft threshold來控制最後的結果,公式爲:
(b-t+k)(b-t+k)/4k
b:亮度
t:threshold
k:thresholdsoftThreshold
half3 Prefilter (half3 c) {
// 亮度,由rgb最大值決定
half brightness = max(c.r, max(c.g, c.b));
// 以下公式開始,變量叫knee由於公式的圖像爲膝蓋形……
half knee = _Threshold * _SoftThreshold;
half soft = brightness - _Threshold + knee;
soft = clamp(soft, 0, 2 * knee); // clamp了一下
soft = soft * soft / (4 * knee + 0.00001); // 防止除0
half contribution = max(soft, brightness - _Threshold);
// 除以亮度
contribution /= max(brightness, 0.00001); // 防止除0
return c * contribution;
}
intensity
intensity爲強度,在各處乘一下。
...
c.rgb += _Intensity * SampleBox(i.uv, 0.5);
...
return half4(_Intensity * SampleBox(i.uv, 0.5), 1);
完整代碼:
爲了效率一部分公式運算從shader移出到了cs代碼,用一個vector。
bloom.SetVector("_Filter", filter);
BloomEffect.cs
using UnityEngine;
using System;
[ExecuteInEditMode, ImageEffectAllowedInSceneView]
public class BloomEffect : MonoBehaviour {
const int BoxDownPrefilterPass = 0;
const int BoxDownPass = 1;
const int BoxUpPass = 2;
const int ApplyBloomPass = 3;
const int DebugBloomPass = 4;
public Shader bloomShader;
[Range(0, 10)]
public float intensity = 1;
[Range(1, 16)]
public int iterations = 4;
[Range(0, 10)]
public float threshold = 1;
[Range(0, 1)]
public float softThreshold = 0.5f;
public bool debug;
RenderTexture[] textures = new RenderTexture[16];
[NonSerialized]
Material bloom;
void OnRenderImage (RenderTexture source, RenderTexture destination) {
if (bloom == null) {
bloom = new Material(bloomShader);
bloom.hideFlags = HideFlags.HideAndDontSave;
}
float knee = threshold * softThreshold;
Vector4 filter;
filter.x = threshold;
filter.y = filter.x - knee;
filter.z = 2f * knee;
filter.w = 0.25f / (knee + 0.00001f);
bloom.SetVector("_Filter", filter);
bloom.SetFloat("_Intensity", Mathf.GammaToLinearSpace(intensity));
int width = source.width / 2;
int height = source.height / 2;
RenderTextureFormat format = source.format;
RenderTexture currentDestination = textures[0] =
RenderTexture.GetTemporary(width, height, 0, format);
Graphics.Blit(source, currentDestination, bloom, BoxDownPrefilterPass);
RenderTexture currentSource = currentDestination;
int i = 1;
for (; i < iterations; i++) {
width /= 2;
height /= 2;
if (height < 2) {
break;
}
currentDestination = textures[i] =
RenderTexture.GetTemporary(width, height, 0, format);
Graphics.Blit(currentSource, currentDestination, bloom, BoxDownPass);
currentSource = currentDestination;
}
for (i -= 2; i >= 0; i--) {
currentDestination = textures[i];
textures[i] = null;
Graphics.Blit(currentSource, currentDestination, bloom, BoxUpPass);
RenderTexture.ReleaseTemporary(currentSource);
currentSource = currentDestination;
}
if (debug) {
Graphics.Blit(currentSource, destination, bloom, DebugBloomPass);
}
else {
bloom.SetTexture("_SourceTex", source);
Graphics.Blit(currentSource, destination, bloom, ApplyBloomPass);
}
RenderTexture.ReleaseTemporary(currentSource);
}
}
Bloom.shader
Shader "Custom/Bloom" {
Properties {
_MainTex ("Texture", 2D) = "white" {}
}
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex, _SourceTex;
float4 _MainTex_TexelSize;
half4 _Filter;
half _Intensity;
struct VertexData {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct Interpolators {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
Interpolators VertexProgram (VertexData v) {
Interpolators i;
i.pos = UnityObjectToClipPos(v.vertex);
i.uv = v.uv;
return i;
}
half3 Sample (float2 uv) {
return tex2D(_MainTex, uv).rgb;
}
half3 SampleBox (float2 uv, float delta) {
float4 o = _MainTex_TexelSize.xyxy * float2(-delta, delta).xxyy;
half3 s =
Sample(uv + o.xy) + Sample(uv + o.zy) +
Sample(uv + o.xw) + Sample(uv + o.zw);
return s * 0.25f;
}
half3 Prefilter (half3 c) {
half brightness = max(c.r, max(c.g, c.b));
half soft = brightness - _Filter.y;
soft = clamp(soft, 0, _Filter.z);
soft = soft * soft * _Filter.w;
half contribution = max(soft, brightness - _Filter.x);
contribution /= max(brightness, 0.00001);
return c * contribution;
}
ENDCG
SubShader {
Cull Off
ZTest Always
ZWrite Off
Pass { // 0
CGPROGRAM
#pragma vertex VertexProgram
#pragma fragment FragmentProgram
half4 FragmentProgram (Interpolators i) : SV_Target {
return half4(Prefilter(SampleBox(i.uv, 1)), 1);
}
ENDCG
}
Pass { // 1
CGPROGRAM
#pragma vertex VertexProgram
#pragma fragment FragmentProgram
half4 FragmentProgram (Interpolators i) : SV_Target {
return half4(SampleBox(i.uv, 1), 1);
}
ENDCG
}
Pass { // 2
Blend One One
CGPROGRAM
#pragma vertex VertexProgram
#pragma fragment FragmentProgram
half4 FragmentProgram (Interpolators i) : SV_Target {
return half4(SampleBox(i.uv, 0.5), 1);
}
ENDCG
}
Pass { // 3
CGPROGRAM
#pragma vertex VertexProgram
#pragma fragment FragmentProgram
half4 FragmentProgram (Interpolators i) : SV_Target {
half4 c = tex2D(_SourceTex, i.uv);
c.rgb += _Intensity * SampleBox(i.uv, 0.5);
return c;
}
ENDCG
}
Pass { // 4
CGPROGRAM
#pragma vertex VertexProgram
#pragma fragment FragmentProgram
half4 FragmentProgram (Interpolators i) : SV_Target {
return half4(_Intensity * SampleBox(i.uv, 0.5), 1);
}
ENDCG
}
}
}