使用點雲數據在Unity中渲染場景


  最近接觸了一個用點雲數據渲染的方案, 非常給力, 幾乎就是毫秒級的加載速度, 特別是在顯示一些城市大尺度場景的時候, 簡直快的沒法形容, 之前的城市場景用了很多重復模型, 並且大量優化之后加載一個城市不僅時間很久, 10分鍾級的, 而且內存消耗巨大, 10G級別的, 運行時CPU裁剪都能耗掉40ms, 幾乎沒有任何意義了...

  這個方案好的地方在於它的SDK提供的返回是 ColorBuffer + DepthBuffer, 在任何引擎上都能很快實現它的渲染, 這就是所謂的"雲場景"方案了吧.

  因為它提供的Demo跑起來效率不高, 也就在30幀左右, 所以做了一些優化, 發現優化之后能上1000幀, 記錄一下.

 

  未優化的幀數:

 

  優化后的幀數:

 

  其實方向很簡單, 第一個是SDK提供的方法並不需要在主線程中去調用, 可以通過多線程進行請求, 雖然會造成丟幀之類的, 可是本來點雲數據就是遠程數據, 不可能有本地數據返回的實效性, 所以可以直接放線程里, 然后跟主線程就像是線程排隊的例子一樣, 主線程提供相機信息, 然后工作線程獲取返回buffer, 通知主相機渲染, 大概跟下圖一樣:

  這樣1,2,3,4.....是本地渲染幀, 請求返回的是遠程渲染幀, 可以看到比如第一幀的請求, 在第四幀返回, 那么如果相機一直在動的話, 渲染疊加在一起是偏移的, 非常明顯的就是地平線相對天空的偏移, 很難看.

  解決的方法也簡單, 把本地和遠程同步起來就行了, 也就是延后本地的渲染, 先記錄發送遠程請求時的相機信息, 在請求未返回時, 本地的相機修改先做記錄, 不進行修改, 等到遠程返回后, 把記錄的相機信息同步給主相機, 然后同步渲染遠程數據, 這樣本地就和遠程渲染同步了, 然后再把記錄下來的操作作為下次遠程渲染的信息發送請求, 這樣本地渲染就被遠程同步了, 並且本地邏輯不受渲染的影響, 這樣就把邏輯跟渲染分離出來了.

  大概如下 : 

  這樣就可以同步渲染了, 原Demo直接在相機渲染中等待遠程返回, 所以幀數受到很大影響.

 

  順便記錄一下中間一些過程...

一. 通過深度圖獲取世界坐標.

  這次用的 Unity2019, 相機有個獲取視錐頂點的函數 Camera.CalculateFrustumCorners , 不知道是什么時候開始有的, 這樣獲取相機視錐射線就簡單了:

    var nearInv = 1.0f / cam.nearClipPlane;
    cam.CalculateFrustumCorners(new Rect(0, 0, 1, 1), cam.nearClipPlane, Camera.MonoOrStereoscopicEye.Mono, _frustumCorners);
    for(int i = 0; i < _frustumCorners.Length; i++)
    {
        var worldSpaceRay_interpolation = cam.transform.TransformVector(_frustumCorners[i]) * nearInv;
    }

  CalculateFrustumCorners  獲得的是相機的本地坐標系中的位置, 將它轉換為世界坐標系(TransformVector), 就如下圖所示:

  它是近裁面上的視錐點, 除以近裁面之后就是單位長度的向量了(基於深度的單位長度, 不是向量的單位長度), 或者把cam.nearClipPlane換成1.0的深度.

  然后是怎樣對視錐向量插值來獲得每個像素對應的視錐向量, Shader的頂點過程可以獲取頂點Index的信息, 然后測試看看屏幕后處理的頂點跟相機視錐頂點的關系:

    struct appdata
    {
        float4 vertex : POSITION;
        float2 uv : TEXCOORD0;
        uint index : SV_VertexID;
    };
    struct v2f
    {
        float2 uv : TEXCOORD0;
        float4 vertex : SV_POSITION;
        float4 ray : TEXCOORD1;
    };

......

    v2f vert (appdata v)
    {
        v2f o;
        o.vertex = UnityObjectToClipPos(v.vertex);
        o.uv = v.uv;
        o.ray.w = v.index;
        return o;
    }
    fixed4 frag (v2f i) : SV_Target
    {
        if (i.ray.w < 0.5) 
        {
            return float4(1, 0, 0, 1);
        }
        else if (i.ray.w < 1.5) 
        { 
            return float4(0, 1, 0, 1); 
        }
        else if (i.ray.w < 2.5)
        {
            return float4(0, 0, 1, 1);
        }
        else if (i.ray.w < 3.5)
        {
            return float4(1, 1, 1, 1);
        }
    }

  根據顏色來看頂點:

  可以看到順序跟編輯器下相機的頂點順序是一樣的, 左下紅, 左上綠色, 右上藍, 右下白, 因為平台時PC所以就沒問題了. 所以核心代碼簡化如下:

    Vector3[] _frustumCorners = new Vector3[4];
    Matrix4x4 _frustumCornerVecs = Matrix4x4.identity;
    cam.CalculateFrustumCorners(new Rect(0, 0, 1, 1), 1.0f, Camera.MonoOrStereoscopicEye.Mono, _frustumCorners);
    for(int i = 0; i < _frustumCorners.Length; i++)
    {
        _frustumCornerVecs.SetRow(i, cam.transform.TransformVector(_frustumCorners[i]));
    }
    _material.SetMatrix("_FrustumCornerVecs", _frustumCornerVecs);
    struct appdata
    {
        float4 vertex : POSITION;
        float2 uv : TEXCOORD0;
        uint index : SV_VertexID;
    };
    struct v2f
    {
        float2 uv : TEXCOORD0;
        float4 vertex : SV_POSITION;
        float4 ray : TEXCOORD1;
    };

    uniform float4x4 _FrustumCornerVecs;    
    sampler2D _CameraDepthTexture;
    
    v2f vert (appdata v)
    {
        v2f o;
        o.vertex = UnityObjectToClipPos(v.vertex);
        o.uv = v.uv;
        o.ray.xyz = _FrustumCornerVecs[v.index].xyz;
        o.ray.w = v.index;
        return o;
    }
    fixed4 frag (v2f i) : SV_Target
    {
        float depthCam = tex2D(_CameraDepthTexture, i.uv).r;
        float depthEye = LinearEyeDepth(depthCam);
        float3 worldPos = _WorldSpaceCameraPos.xyz + (i.ray.xyz * depthEye);
        ......
    }

  視錐計算應該在Vert階段也是能計算的才對, 只要有相機的Fov以及屏幕的寬高比就行了, 找了一下應該從內置變量中都能找到:

_ProjectionParams

  float4 x is 1.0 (or –1.0 if currently rendering with a flipped projection matrix), y is the camera’s near plane, z is the camera’s far plane and w is 1/FarPlane.
_ScreenParams

  float4 x is the width of the camera’s target texture in pixels, y is the height of the camera’s target texture in pixels, z is 1.0 + 1.0/width and w is 1.0 + 1.0/height.

還差一個FOV不知道在哪, 可能從變換矩陣中可以弄出來吧. 這樣就可以擺脫C#代碼了.
 

 

二. 在不同生命周期中的特殊處理

  現在主要的兩個渲染路徑 Forward 和 Deferred 生命周期有很大不同, 並且數據都不同, 比如我想在繪制遠程數據的時候(ColorBuffer+DepthBuffer), 如果在 Forward 路徑中的話, 可以選擇在 BeforeForwardOpaque 中繪制, 然后寫入顏色和深度, 因為它屬於地形這種大面積的東西, 所以很多在它之后繪制的Unity物體可以被遮擋(並且可以不渲染深度貼圖), 這在有 Early-Z 的情況下能提升性能呢, 可是在 Deferred 路徑就沒有什么用, 它直接渲染GBuffer了, 並且在渲染前會清空一次(Color+Z+Stencil), 在它之前沒有什么意義, 在它之后也沒有什么意義(性能上), 所以為了簡便, 直接使用后處理了, 這樣在兩種路徑中都有同樣的生命周期...

  

三. CommandBuffer.Blit的問題

  已經不知道怎么描述了, 沒有文檔, 只能看運行結果了:

  1. 使用一個簡單Shader材質進行Blit, 在天空盒之前獲取屏幕截圖

    public RawImage raw;
    void Start()
    {
        CreateCommandBuffer();
    }
    private void CreateCommandBuffer()
    {
        var rt = new RenderTexture(Screen.width, Screen.height, 24, RenderTextureFormat.Default, RenderTextureReadWrite.Linear);
        raw.texture = rt;
        var cmd = new CommandBuffer();
        cmd.name = "TestBlit";
        var drawMaterial = new Material(Shader.Find("Test/RuntimeTest"));    // 簡單Shader
        cmd.Blit(BuiltinRenderTextureType.CameraTarget, rt, drawMaterial);
        cmd.SetRenderTarget(BuiltinRenderTextureType.CurrentActive);
        Camera.main.AddCommandBuffer(CameraEvent.BeforeSkybox, cmd);        // 天空盒之前
    }

  Shader就是最簡單的自帶Image Effect

......
fixed4 frag (v2f i) : SV_Target
{
    fixed4 col = tex2D(_MainTex, i.uv);
    return col;
}

  向前渲染, 不使用DHR/MSAA時得到空白貼圖...

 

  當打開HDR 或 MSAA之后能獲取到貼圖:

 

 

   ??????????????????????

   當我不使用材質進行Blit時, 更奇怪的事情來了:

    private void CreateCommandBuffer()
    {
        var rt = new RenderTexture(Screen.width, Screen.height, 24, RenderTextureFormat.Default, RenderTextureReadWrite.Linear);
        raw.texture = rt;
        var cmd = new CommandBuffer();
        cmd.name = "TestBlit";
        cmd.Blit(BuiltinRenderTextureType.CameraTarget, rt);  // 不用材質了還不行嗎
        cmd.SetRenderTarget(BuiltinRenderTextureType.CurrentActive);
        Camera.main.AddCommandBuffer(CameraEvent.BeforeSkybox, cmd);
    }

  它為什么形成了套娃??? 這時候它的相機渲染包含了UI的渲染??? 為什么上下顛倒了???

  然后把相機的HDR或MSAA打開, 又正常了:

 

  然后回到套娃的設置下, 把UI關了, 看看圖片它不套娃了...

  好吧............

 

 

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM