2018年11月9日 星期五

SSAO使用於打光AO map的做法

一般的SSAO最讓人詬病的部份,就是會把模型弄髒,感覺就像是鋼彈模型舊化失敗的作品,就算是把Density調低或探測球調小,仍然是不改直接覆改打光結果的根本問題。

其實在Unity的PBR渲染,是把AO這一層當間接光照處理,也就是他並不會影響直接光照,在LightWeight SRP的做法如下:
half3 GlobalIllumination(in BRDFData theBRDFData, in LightInputData lightInputData, in SurfaceData surfaceData, in half occlusion)
{
    half3 reflectVector = reflect(-lightInputData.worldViewDirection, lightInputData.worldNormal);
    //有機會優化,也許直接參考一張貼圖
    half fresnelTerm = Pow4(1.0 - saturate(dot(lightInputData.worldNormal, lightInputData.worldViewDirection)));
    //half3 indirectDiffuse = half3(0.0, 0.0, 0.0);
    half3 indirectDiffuse = half3(0.0, 0.0, 0.0);
#ifdef _PROCESS_VERTEX_SH
    //indirectDiffuse = lightInputData.bakedGI * occlusion;
    indirectDiffuse = lightInputData.bakedGI;
#endif
    indirectDiffuse *= Gamma22ToLinear(occlusion);

    half3 indirectSpecular = GlossyEnvironmentReflection(reflectVector, theBRDFData.perceptualRoughness, occlusion);
    return EnvironmentBRDF(theBRDFData, indirectDiffuse, indirectSpecular, fresnelTerm);
}

half3 GlossyEnvironmentReflection(half3 reflectVector, half perceptualRoughness, half occlusion)
{
#if !defined(_GLOSSYREFLECTIONS_OFF)
    half mip = PerceptualRoughnessToMipmapLevel(perceptualRoughness);
    half4 encodedIrradiance = SAMPLE_TEXTURECUBE_LOD(unity_SpecCube0, samplerunity_SpecCube0, reflectVector, mip);

#if !defined(UNITY_USE_NATIVE_HDR)
    half3 irradiance = DecodeHDREnvironment(encodedIrradiance, unity_SpecCube0_HDR);
#else
    half3 irradiance = encodedIrradiance.rbg;
#endif

    return irradiance * occlusion;
#endif // GLOSSY_REFLECTIONS

    return _GlossyEnvironmentColor.rgb * occlusion;
}
所以這邊的做法是將SSAO計算出來,並把其放在打光的計算上,而且這樣一來,也不會影響到Emission的部份。

未上SSAO的結果:


SSAO後置特效的狀況,很明顯示會弄髒Model:


SSAO使用AO map打光計算後的結果(為了讓差別更清楚,暫時讓GlossyEnvironmentReflection不受AO的影響,而且可以加入打光計算後,可以更有彈性調整)


關於優化的部份,為了讓打光時不用再讀一張SSAO map,將其資料壓在ScreenSpaceShadowMap的G通道裡,圖中紅色那張貼圖,有點偏黃色的資料就是SSAO。

相關比較視頻如下:

2018年10月8日 星期一

我的MobileSRP

本來一開始想說直接使用LightWeightSRP就好,但越研究之後,發現有許多架構覺得不太適
合,於是開啟了設計自己的MobileSRP之路。

首先我們先來看看基本架構:


原則上,跟LWSRP幾乎是一樣的,比較大的差別是捨棄了IRendererSetup的架構,主要的考量
是目前的需求是不太需要每個Camera有不同的PassContainer,為了這個還要在每個
Frame做GetComponent來增加GC覺得不值得,所以採用固定式PassContainer的方式,儲存在
MobileForwardRendererProcessor裡面。

再來看一下MobileForwardRendererProcessor的架構:
我覺得LightWeightSRP最好的部份,就是在Shadow處理採用了ScreenSpaceShadow的架構,
雖然這樣需要先做一次RenderDepthOnly,但這樣一來可以大幅減少
Receive Shadow Over drawing的部份。
另外在Multi Light的部份,也改成用One pass for loop的方式來處理,不再像之前的Forward是
採Multi pass(其實我之前就覺得這部份很奇怪),所這部份目前就保留了下來,其他
像SetupCameraProperties、CreateColorOrDepthTexture及SetupLightConstants不用來畫圖的Pass
,主要是為了順序性及Pass處理的架構,所以不是所有的Pass都是用來畫圖的。
還有最重要的,就是FinalBlt及CreateColorOrDepthTexture的部份,這是為了處理
Scale Render,一般來說都是直接把3D物件畫入FrameBuffer,但這樣一來處理的像素就過多
,利用這個方式可以將降低3D繪圖處理的像素量,另外在GUI的部份又可以採取原生解析
度的方式,其實在沒有SRP的時候,我早就這樣做了,所以這一部份當然保留下來。

最後來看一下PostEffect的部份:
LWSRP的處理方式,是在每個Frame去call GetComponent抓取PostProcessLayer,以達到每個
Camera都可以有不同的Post特效,但為了這個目的卻要每個Frame抓取Component,所以這裡
捨棄了這樣的做法,採取Interface的方式,由PostEffect主動跟Pass註冊(PostEffexct打錯了
是PostEffect)Interface,來避免perframe call GetComponent,雖然這樣一來,每個Camera的
後置特效都一樣,但對目前的狀況來說夠用了。

2018年9月13日 星期四

客製化自己的Unity LWRP

說實在的,完全客製化一個自己的SRP,又能兼顧效能及功能並不容易…
我發現在ScriptableRenderPipeline\TestProjects\LWGraphicsTest裡面的
045_CustomLWPipe.unity可以做到某種程度的客製化。
在其中的CustomLWPipe.cs中展示了可以自行設計加入想要的Pass流程,
這個範例目前只用到了以下幾個pass來完成繪圖:

  1. SetupForwardRenderingPass
  2. CreateLightweightRenderTexturesPass
  3. SetupLightweightConstanstPass
  4. RenderOpaqueForwardPass

然而LWRP如何做到使用你的客製流程,答案是利用了IRenderSetup,
他會在畫每一個Camera的時候,利用GetComponent來取得IRenderSetup,
如果有的話,就會取代掉原本的DefaultRendererSetup,這樣他可以做到
每個Camera使用不同的IRenderSetup,非常有彈性,雖然我個人是覺得
這樣有效能的疑慮。

目前這個範例並沒辦法處理陰影,但透過一些些流程,就可以支持,細結可以參考
DefaultRendererSetup,流程如下:

  1. 在Init函式加入陰影相關pass及相關的renderTexture
    ...
    private DepthOnlyPass _depthOnlyPass;
    private RenderTargetHandle _depthTextureHandle;
    private DirectionalShadowsPass _directionalShadowPass;
    private RenderTargetHandle _directionalShadowmapHandle;
    private ScreenSpaceShadowResolvePass _screenSpaceShadowResovePass;
    private RenderTargetHandle _screenSpaceShadowmap;
    ...
  2. 在Setup函式設定及加入Pass:

    if (_directionalShadowPass != null)
    {
      _directionalShadowPass.Setup(_directionalShadowmapHandle);
      renderer.EnqueuePass(_directionalShadowPass);
    }
    renderer.SetupPerObjectLightIndices(ref cullResults, ref renderingData.lightData);
    RenderTextureDescriptor baseDescriptor = ScriptableRenderer.CreateRTDesc(ref
      renderingData.cameraData);
    renderer.EnqueuePass(m_SetupForwardRenderingPass);
    //一定要在 m_SetupForwardRenderingPass之後…
    if (_depthOnlyPass != null)
    {
      _depthOnlyPass.Setup(baseDescriptor, _depthTextureHandle, SampleCount.One);
      renderer.EnqueuePass(_depthOnlyPass);
    }
    if (_screenSpaceShadowResovePass != null)
    {
      _screenSpaceShadowResovePass.Setup(baseDescriptor, _screenSpaceShadowmap);
      renderer.EnqueuePass(_screenSpaceShadowResovePass);
    }
我覺這是一個讓你可以快速體驗SRP又可以享受LWRP的複雜功能的範例
(尤其是影子),大家有空可以自行下載ScriptableRenderPipeline來玩玩看。
這是相關整行後的結果:

2018年9月1日 星期六

試做自己的Unity SRP

目標:先以LightWeight SRP為參考,並在過程中更理解Unity繪圖流程的細節,最終設計出自己覺得適合的SRP。

架構流程如下:
Light List相關(Setup light constants pass):
從代碼得知,Culling list裡的Light list是目前camera view所看到的lights,經過最大燈光的
數量的限制及SetGlobalVetorArray
會造成物件燈光計算的錯誤,因為是view culling list,並不是Object List…
void SetupAdditionalLightConstants(CommandBuffer cmd, ref LightData lightData){
            List lights = lightData.visibleLights;
            if (lightData.totalAdditionalLightsCount > 0)
            {
                int localLightsCount = 0;
                for (int i = 0; i < lights.Count && localLightsCount < maxVisibleLocalLights; ++i)
                {
                    VisibleLight light = lights[i];
                    if (light.lightType != LightType.Directional)
                    {
                        InitializeLightConstants(lights, i, out m_LightPositions[localLightsCount],
                            out m_LightColors[localLightsCount],
                            out m_LightDistanceAttenuations[localLightsCount],
                            out m_LightSpotDirections[localLightsCount],
                            out m_LightSpotAttenuations[localLightsCount]);
                        localLightsCount++;
                    }
                }

                cmd.SetGlobalVector(LightConstantBuffer._AdditionalLightCount, new Vector4(lightData.pixelAdditionalLightsCount,
                    lightData.totalAdditionalLightsCount, 0.0f, 0.0f));

                // if not using a compute buffer, engine will set indices in 2 vec4 constants
                // unity_4LightIndices0 and unity_4LightIndices1
                if (perObjectLightIndices != null)
                    cmd.SetGlobalBuffer("_LightIndexBuffer", perObjectLightIndices);
            }
            else
            {
                cmd.SetGlobalVector(LightConstantBuffer._AdditionalLightCount, Vector4.zero);
            }
            cmd.SetGlobalVectorArray(LightConstantBuffer._AdditionalLightPosition, m_LightPositions);
            cmd.SetGlobalVectorArray(LightConstantBuffer._AdditionalLightColor, m_LightColors);
            cmd.SetGlobalVectorArray(LightConstantBuffer._AdditionalLightDistanceAttenuation, m_LightDistanceAttenuations);
            cmd.SetGlobalVectorArray(LightConstantBuffer._AdditionalLightSpotDir, m_LightSpotDirections);
            cmd.SetGlobalVectorArray(LightConstantBuffer._AdditionalLightSpotAttenuation, m_LightSpotAttenuations);
        }

Object light list的處理:原來Unity底層有支援RendererConfiguration.PerObjectLightIndices8,
可以用來解決這個問題,如果有支援StructuredBuffer的話,可以走另一個流程
RendererConfiguration.ProvideLightIndices,相關代碼如下:
//這裡主要是在處理方向光需要從project light indices中移掉,交由MainLight接手。
public void SetupPerObjectLightIndices(ref CullResults cullResults, ref LightData lightData)
{
...
}


public static RendererConfiguration GetRendererConfiguration(int localLightsCount){
            RendererConfiguration configuration = RendererConfiguration.PerObjectReflectionProbes | RendererConfiguration.PerObjectLightmaps | RendererConfiguration.PerObjectLightProbe;
            if (localLightsCount > 0)
            {
                if (useComputeBufferForPerObjectLightIndices)
                    configuration |= RendererConfiguration.ProvideLightIndices;
                else
                    configuration |= RendererConfiguration.PerObjectLightIndices8;
            }

            return configuration;
        }

相關Shader如下:會利用GetLight這個函式來取得正確的Light index。
Light GetLight(half i, float3 positionWS)
{
    LightInput lightInput;

#if USE_STRUCTURED_BUFFER_FOR_LIGHT_DATA
    int lightIndex = _LightIndexBuffer[unity_LightIndicesOffsetAndCount.x + i];
#else
    // The following code is more optimal than indexing unity_4LightIndices0.
    // Conditional moves are branch free even on mali-400
    half i_rem = (i < 2.0h) ? i : i - 2.0h;
    half2 lightIndex2 = (i < 2.0h) ? unity_4LightIndices0.xy : unity_4LightIndices0.zw;
    int lightIndex = (i_rem < 1.0h) ? lightIndex2.x : lightIndex2.y;
#endif

    // The following code will turn into a branching madhouse on platforms that don't support
    // dynamic indexing. Ideally we need to configure light data at a cluster of
    // objects granularity level. We will only be able to do that when scriptable culling kicks in.
    // TODO: Use StructuredBuffer on PC/Console and profile access speed on mobile that support it.
    lightInput.position = _AdditionalLightPosition[lightIndex];
    lightInput.color = _AdditionalLightColor[lightIndex].rgb;
    lightInput.distanceAttenuation = _AdditionalLightDistanceAttenuation[lightIndex];
    lightInput.spotDirection = _AdditionalLightSpotDir[lightIndex];
    lightInput.spotAttenuation = _AdditionalLightSpotAttenuation[lightIndex];

    half4 directionAndRealtimeAttenuation = GetLightDirectionAndAttenuation(lightInput, positionWS);

    Light light;
    light.index = lightIndex;
    light.direction = directionAndRealtimeAttenuation.xyz;
    light.attenuation = directionAndRealtimeAttenuation.w;
    light.subtractiveModeAttenuation = lightInput.distanceAttenuation.w;
    light.color = lightInput.color;

    return light;
}

切割Pass原因:
主要是為容易切割處理流程、共用程式碼、以及提供足夠的時機做相關後置特效處理。另外也利用了切pass的時機,copy了render過程的結果,以便於除錯及觀察繪製流程。

接下來要做的事:
  1. 影子pass處理,預計會採用跟Unity一樣的CSM shadow map全場景投影的做法
  2. PBR材質
  3. 支持forward plus rendering,目前Unity是將這部份放在HRDP,估計應該是因為
    compute shader light list的問題,如果最後不行的話,最差的狀況至少也可以先為
    Vulkan做準備。

除錯資料(左上開始的小圖),由左而右,是繪製流程的順序,依序為
after draw opaques, depth, after draw sky, after draw transparencies.
另外也跟LWSRP一樣支持render scale,簡單來說就是不直接Render到viewport上,而是先
畫到RenderTexture,最後再Final Blt到viewport上,好處是:
  1. 讓3D採用較低的解析度,可以大大減少GPU處理像素的數量,提升效能。
  2. 可以直接使用NativeDepthBuffer,不用增加DrawCall。
  3. 由GUI一般會使用point filter,而3D大都會採用linear filter,可以切開。


相關影片如下:

2018年8月6日 星期一

Unity Batching與GPU instance效能分析

相關測試畫面如下(這邊提供的是GPU Instance的畫面,所以可以給不同的顏色,但材質卻是一樣的,Mesh都是Cylinder):






















測試分析結果如下:

2018年7月23日 星期一

初探Unity的SRP

基本上,SRP的流程架構,還算蠻清楚的,步驟大概如下:
RenderPipeline Render()
==ProcessPerCamera(迴圈)
==CullResults.Cull()
====ProcessPerPass(迴圈)
在大都數的狀況下,cameras.Length大都多是1(本來以為EditorView的Camera也會進來…)
,而要做的事,基本上都是context通溝(下CommandBuffer指令,Submit畫完之類的)。

這裡並沒有把繪圖流程包成一個Pass物件,因為相關代碼只會有一個Pass,其中DrawRendererSettings需要的第二個參數ShaderPassName相當重要,它指的是在ShaderCode裡有指定相關的Tag才畫挑出來畫,比方"LightMode" = "ForwardBase",而你的ShaderPassName也是設成"ForwardBase"時,才會被挑出來。

相關代碼如下:

2018年7月11日 星期三

DOF with alpha blend depth map

一般DOF的做法大多是參考Depth Map,然後依照遠近再來決使用Blur Map或Color Buffer。
但是因為Alpha Blend通常沒有輸出深度,所以會發生因為錯誤的深度導致Alpha物件會顯示
錯誤的模糊效果。

目前自己的解決方法是為Alpha Blend物件單獨輸出一張Alpha Test的Depth Map,之後再跟深度貼圖取最近的深度即可。

在還沒使用Alpha Blend Map時,畫面如下(其中藍色的部份因為不透明物件的關係,顯示比較正確的結果):