前往
大廳
主題

【教學】Unity HDRP 使用 Volume 自定義 Post Process 以及多 Pass 實現方法

FunS | 2021-10-27 23:09:51 | 巴幣 2100 | 人氣 2428

內文使用版本 2021.2 HDRP 12.0.0
內文所使用的Shader效果 小呈 【學習日誌】自己寫一個 One last kiss 的風格渲染 10/24

目前設定後期處理有兩種方法:
CustomPassVolume 使用 CustomPass 套用 FullScreenShader,作法會類似 Bilt。
Volume (我實在是不知道為什麼腳本不繼續像以前一樣叫 PostProcessing ) 可以做出區域、全域的後期處理效果。( 本文使用方法、有空也會把 CustomPass 的方法整理上來 )

必要的東西

1. ✨Shader - HDRP Post Process

2. Script - HDRP C# Post Processing
( 筆記 Script 名子必須與 Class 同名,與 MonoBehaviour 一樣 )

環境設定

設定一個 Global 的 Post Processing 物件

可以找到新效果 Add Override > Post-Processing > Custom > OneLastKiss

到 Project Settings > HDRP Settings > Custom Post Process Orders > After Post Process > +OneLastKiss

目前官方有四個注入點位,目前的是插入到內建效果後 ( AfterPostProcess )  [ Pipeline Graph ]

這邊的設定是做後期處理效果排序,自訂效果是無法從內建效果中插入的( Uber Shader )

Shader 裡面有什麼 ?

官方有內建的函式庫可以使用 Common.hlsl Color.hlsl

Vertex Shader 中處理了兩個變數,函式可以參考 Common.hlsl 不贅述

重要的四個變數
Input Description
positionCS 回傳剪裁空間的位置。( 值 [0 , 螢幕大小] )
The clip space position of the pixel. This value is between 0 and the current screen size.
texcoord 螢幕空間的 UV 。 ( 值 [0 ,1] )
The full screen UV coordinate. This value is between 0 and 1.
_InputTexture
畫面的材質
The source Texture.
_Intensity 用操控效果強度的
The intensity of the effect.

在 Fragment Shader 內有個有趣的地方,Texture 原來有 Load 與 Sample 的使用方法 (參考1 參考2)

Sample 是紋理採樣,應用 紋理巡址( addressing modes (clamp, wrap, border) ) 紋理過濾( filtering (bilinear, trilinear, aniso) ),採樣為 UV [0,1]

Load 是載入特定位置的 Texel ,採樣為 像素Index [0, textureWidth - 1] x [0, textureHeight - 1]

在範例中是使用 Load 去載入像素,所以必須將 texcoord 展開到符合螢幕像素 ( positionSS )
uint2 positionSS = uint2(input.positionCS.xy);

or

uint2 positionSS  = input.texcoord * _ScreenSize.xy - float2(0, 0.5);

float3 col = LOAD_TEXTURE2D(_InputTexture, positionSS).rgb;

Load 適合用在不會縮放像素與符合像素大小的情況,對 Post Processing 來說是非常適合作為優化的選擇。

準備完畢!開始寫 Shader !

效果請參考 樂小呈 & 五十六 這邊只取 Post Processing 的重點
這邊把「提取線搞上色分為兩個 Pass 去處理,目的僅為示範所以當然是可以寫在一起的!

Pass#0 LineCapturePass 提取線搞
這邊我們先宣告一個變數,做為提取採樣範圍
int _Radius;

注意!採樣 Texture 的方法已經從 Sample 改為 Load => [ 0 , 1 ] 改為 [ 0 , 貼圖大小 ]
所以我們輸入的 UV 必須是像素的 Index,也就是 positionCS
float4 LineCapturePass(Varyings input) : SV_Target
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
    uint2 positionSS = uint2(input.positionCS.xy);
    float col = Luminance(LOAD_TEXTURE2D_X(_InputTexture, positionSS ));
    float maxValue = GetNearbyMax(positionSS ,_Radius);
    col /= maxValue;

    return float4(col.xxx, 1);
}


GetNearbyMax實現
float GetNearbyMax(uint2 uv,int radius) {
    float maxValue = Luminance(LOAD_TEXTURE2D_X(
_InputTexture, uv));
    for (int x = -radius; x < radius; x++)
    {
        for (int y = -radius; y < radius; y++)
        {
            int2 pixelOffset = int2(x, y);
            maxValue = max(maxValue, Luminance(LOAD_TEXTURE2D_X(
_InputTexture, uv + pixelOffset)));
        }
    }
    return maxValue;
}

實作 Post Processing 介面

先介紹一下腳本內的實作
IsActive() 判斷是否要渲染這個效果,如同預設效果確保強度為零時不渲染
injectionPoint 渲染注入點,要注意的是如果有修改設定要記得回 Project Settings > HDRP Settings > Custom Post Process Orders 去設定效果
Setup() 為初始化區塊
Render(CommandBuffer cmd, HDCamera camera, RTHandle source, RTHandle destination) 用法有點像 Graphics.Blit() camera為渲染的攝影機、source作為輸入、destination作為輸出
Cleanup() 渲染結束後呼叫釋放材質

宣告 radius 為 ClampedIntPararmeter ( 初始值 , 最小 , 最大 ) ( 後處理提供的所有的變數操控參數 VolumeParameter )



多 Pass 使用

在開始第二個 Pass 之前,多 Pass 中實現 PostProcessing _Intensity 強度調整會有點小麻煩,因為他不像單 Pass 一樣方便可以直接對 輸入 與 輸出 做 Lerp ,需要另外先保存效果前的 Source render texture 並在最後一個 Pass 再去做強度的 Lerp。

所以我們需要在 Shader 最一開始宣告放 Source Texture 的變數,再將 _InputTexture 改為 TEXTURE2D
( 這個 Bug 抓了好久好久..,一開始 _SourceTexture 為材質組用於抓取單通道/立體渲染,需要 TEXTURE2D_X 採樣,但在後面提到的 RTHandles.Alloc 抓出第一個 Pass 的 RenderTexture 後並輸入給 _InputTexture 就必須改為 TEXTURE2D 去採樣 )
TEXTURE2D_X(_SourceTexture);
TEXTURE2D(_InputTexture);

再來將原始的畫面命名為 _SourceTexture 方便辨認,所以必須將前一個 Pass 所有的 _InputTexture 都改為 _SourceTexture

在腳本內需要先宣告 MaterialPropertyBlock _prop; ,並放入Setup初始化。
public override void Setup()
{
    if(Shader.Find(kShaderName) != null) {
        m_Material = new Material(Shader.Find(kShaderName));
        
_prop = new MaterialPropertyBlock();
    }

    ...
}

實作 Render() ,DrawFullScreen 最後一個參數就是目標 Pass
務必要將 RTHandle 做 Release() 不然會造成 Memory Leak ( 電腦崩潰了好幾次QQ 參考 )
RTHandle 不能在渲染最後 Release 掉喔!必須放在下一幀渲染開始前處釋放,不然會因為被回收掉而黑畫面
RTHandle LineCaptureRT;

public override void Render(CommandBuffer cmd, HDCamera camera, RTHandle source, RTHandle destination)
{
    if (m_Material == null)
        return;

    if(LineCaptureRT != null) LineCaptureRT.Release();

    m_Material.SetFloat("_Intensity", intensity.value);
    m_Material.SetInt("_Radius", radius.value);

    _prop.SetTexture("_SourceTexture", source);
    LineCaptureRT = RTHandles.Alloc(camera.actualWidth, camera.actualHeight, colorFormat: GraphicsFormat.R16G16B16A16_SFloat);
    HDUtils.DrawFullScreen(cmd, m_Material, LineCaptureRT, _prop, 0);

    _prop.SetTexture("_InputTexture", LineCaptureRT);
    HDUtils.DrawFullScreen(cmd, m_Material, destination, _prop, 1);
}

public override void Cleanup()
{
    CoreUtils.Destroy(m_Material);
    RTHandles.Release(LineCaptureRT);
}

實作 Shader 上色 Pass !

Pass#1 ColoringLinePass 線稿上色
先宣告三個變數,做為漸變顏色與漸變旋轉角度
float4 _ColorA;
float4 _ColorB;
float _GrandientRotate;

實作
float4 ColoringLinePass(Varyings input) : SV_Target
{
    UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

    float2 uv = input.texcoord.xy;

    float Grandient = Unity_Rotate_Degrees_float(uv, float2(0.5,0.5), _GrandientRotate).x;
    Grandient = distance(Grandient, 0.5);

    float3 GrandientColor = lerp(_ColorA,_ColorB, Grandient);

    uint2 positionSS = uint2(input.positionCS.xy);
    float3 col = LOAD_TEXTURE2D(_InputTexture, positionSS).rgb;

    float3 output = GrandientColor * (1 - col);

    float3 source = LOAD_TEXTURE2D_X(_SourceTexture, positionSS).rgb;

    return float4(lerp(source, output, _Intensity), 1);
}

裡面使用的 Unity_Rotate_Degrees_float 可以到這裡參考 Code,用法跟 Shader Grpah 一樣
float2 Unity_Rotate_Degrees_float(float2 UV, float2 Center, float Rotation)
{
    Rotation = Rotation * (3.1415926f/180.0f);
    UV -= Center;
    float s = sin(Rotation);
    float c = cos(Rotation);
    float2x2 rMatrix = float2x2(c, -s, s, c);
    rMatrix *= 0.5;
    rMatrix += 0.5;
    rMatrix = rMatrix * 2 - 1;
    UV.xy = mul(UV.xy, rMatrix);
    UV += Center;
    return UV;
}

再次實作 Post Processing 介面


public override void Render(CommandBuffer cmd, HDCamera camera, RTHandle source, RTHandle destination)
{
    if (m_Material == null)
        return;

    if(LineCaptureRT != null) LineCaptureRT.Release();

    m_Material.SetFloat("_Intensity", intensity.value);
    m_Material.SetInt("_Radius", radius.value);
    m_Material.SetColor("_ColorA", colorA.value);
    m_Material.SetColor("_ColorB", colorB.value);
    m_Material.SetFloat("_GrandientRotate", grandientRotate.value);

    ...
}


原始碼

其它官方提到要特別注意的

- 如果你 Build 的所有場景都沒有引用到該 Shader 就不會被打包出去,記得到放到 Resources 資料夾或是設定到 Edit > Processing Settings > Graphics > Always Included Shader 。

( 不太清楚 也許是怕 clip 會影響到整個 buffer ?)
- ⚠ Note that, when HDRP executes your post-process effect, it uses a render target pooling system. It means that you don't know what the current color buffer contains, which is why you should never use any instructions that could show this color buffer. Do not use transparency, blend modes, or the clip() instruction in your Shader, otherwise your effect breaks.

參考

毛星云
> 高品质后处理:十种图像模糊算法的总结与实现 https://zhuanlan.zhihu.com/p/125744132
> XPL: Unity引擎的高品质后处理库 https://github.com/QianMo/X-PostProcessing-Library

Keijiro

如果覺得文章文字忽大忽小不舒服的人抱歉了,有些改過都會再跳回來,明明編輯介面看是對的
原始碼成一團已經改到不想改了XD,以後不要再去動到標題的大標小標的設定了

距離上次畢業時寫文章已經過了一年半,回憶這一段時間真的完全可以感受到進步的幅度,學習GOGO
,一起進步共勉之阿

相關創作

更多創作