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時,畫面如下(其中藍色的部份因為不透明物件的關係,顯示比較正確的結果):

2018年7月7日 星期六

使用Unity Command buffer調整SSAO後置效果的繪圖流程

SSAO如果擺在最後的後置繪圖流程,會造成因為Alpha Blending的物件沒有深度的問題,
而造成不正確的視覺效果。
為了解決這個問題,最好是將其放在整個Render完所有的非透明物件時再將其畫出,雖然
應該可以用一個full screen face quad物件,調整其render queue來畫應該也可以解決,但是
Unity有更好用的東西(Command Buffer)可以讓我們在特定的繪圖流程時機來畫後置特效。

這個是在畫完所有物件時機畫的結果:

2018年6月28日 星期四

在Unity下模擬水接收陰影從淺灘到深海的過渡

基本上設計思路很簡單,就是先讓水可以接收陰影,之後再依照水平面深度逐漸將陰影顯現出來,在深度最深的時候完全接收百分百的光源衰減值。

在折射處理方面,因為是使用GrabPass,所以並沒有針對折射另外進行拍射陰影,不會增加額外的DrawCall,但有花了拷貝背景圖的成本。
重點如下:
  1. UNITY_LIGHT_ATTENUATION(LightAttenuation, Input, Input.WorldPosition)
  2. LightAttenuation = lerp(1.0, LightAttenuation, pow(DepthRatio, 5));
之後在打光的地方使用這個光源衰減值即可,相關影片如下(記得要住意岸邊與深海接收影子的過渡)。