使用点云数据在Unity中渲染场景

摘要:
最近,我接触到了一种使用点云数据进行渲染的方案,这非常棒,几乎可以达到毫秒级的加载速度。尤其是在展示一些大型城市场景时,速度太快,难以描述。之前的城市场景使用了许多重复的模型,经过大量优化后,加载一个城市不仅需要很长时间,10分钟,而且还消耗了大量内存。10G级别的CPU削波在运行时可以消耗40ms,几乎没有意义……这个方案的好处是SDK提供了ColorBuffer+DepthBuffer

  最近接触了一个用点云数据渲染的方案, 非常给力, 几乎就是毫秒级的加载速度, 特别是在显示一些城市大尺度场景的时候, 简直快的没法形容, 之前的城市场景用了很多重复模型, 并且大量优化之后加载一个城市不仅时间很久, 10分钟级的, 而且内存消耗巨大, 10G级别的, 运行时CPU裁剪都能耗掉40ms, 几乎没有任何意义了...

  这个方案好的地方在于它的SDK提供的返回是 ColorBuffer + DepthBuffer, 在任何引擎上都能很快实现它的渲染, 这就是所谓的"云场景"方案了吧.

  因为它提供的Demo跑起来效率不高, 也就在30帧左右, 所以做了一些优化, 发现优化之后能上1000帧, 记录一下.

  未优化的帧数:

使用点云数据在Unity中渲染场景第1张

  优化后的帧数:

使用点云数据在Unity中渲染场景第2张

  其实方向很简单, 第一个是SDK提供的方法并不需要在主线程中去调用, 可以通过多线程进行请求, 虽然会造成丢帧之类的, 可是本来点云数据就是远程数据, 不可能有本地数据返回的实效性, 所以可以直接放线程里, 然后跟主线程就像是线程排队的例子一样, 主线程提供相机信息, 然后工作线程获取返回buffer, 通知主相机渲染, 大概跟下图一样:

使用点云数据在Unity中渲染场景第3张

  这样1,2,3,4.....是本地渲染帧, 请求返回的是远程渲染帧, 可以看到比如第一帧的请求, 在第四帧返回, 那么如果相机一直在动的话, 渲染叠加在一起是偏移的, 非常明显的就是地平线相对天空的偏移, 很难看.

  解决的方法也简单, 把本地和远程同步起来就行了, 也就是延后本地的渲染, 先记录发送远程请求时的相机信息, 在请求未返回时, 本地的相机修改先做记录, 不进行修改, 等到远程返回后, 把记录的相机信息同步给主相机, 然后同步渲染远程数据, 这样本地就和远程渲染同步了, 然后再把记录下来的操作作为下次远程渲染的信息发送请求, 这样本地渲染就被远程同步了, 并且本地逻辑不受渲染的影响, 这样就把逻辑跟渲染分离出来了.

  大概如下 : 

使用点云数据在Unity中渲染场景第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), 就如下图所示:

使用点云数据在Unity中渲染场景第5张

  它是近裁面上的视锥点, 除以近裁面之后就是单位长度的向量了(基于深度的单位长度, 不是向量的单位长度), 或者把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);
        }
    }

  根据颜色来看顶点:

使用点云数据在Unity中渲染场景第6张

  可以看到顺序跟编辑器下相机的顶点顺序是一样的, 左下红, 左上绿色, 右上蓝, 右下白, 因为平台时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;
}

使用点云数据在Unity中渲染场景第7张

使用点云数据在Unity中渲染场景第8张

  向前渲染, 不使用DHR/MSAA时得到空白贴图...

 使用点云数据在Unity中渲染场景第9张

  当打开HDR 或 MSAA之后能获取到贴图:

使用点云数据在Unity中渲染场景第10张

使用点云数据在Unity中渲染场景第11张 

 使用点云数据在Unity中渲染场景第12张

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

   当我不使用材质进行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);
    }

使用点云数据在Unity中渲染场景第13张

使用点云数据在Unity中渲染场景第14张

  它为什么形成了套娃??? 这时候它的相机渲染包含了UI的渲染??? 为什么上下颠倒了???

  然后把相机的HDR或MSAA打开, 又正常了:

使用点云数据在Unity中渲染场景第15张

 使用点云数据在Unity中渲染场景第16张

  然后回到套娃的设置下, 把UI关了, 看看图片它不套娃了...

使用点云数据在Unity中渲染场景第17张

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

  

免责声明:文章转载自《使用点云数据在Unity中渲染场景》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇APP漏洞自动化扫描专业评测报告(下篇)WPF DataGrid多选功能开发下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

External Tools

Preferences偏好设置-External Tools External Tools:     External Script Editor:外部脚本编辑器,通过此项可以切换您所擅用的脚本的编辑器     Editor Script Editor Args:     MonoDevelop Solution Properties:unity...

一文解读裸金属云 (转)

裸金属云,也叫裸机云,顾名思义就是同时拥有裸机的性能和云的弹性。在公有云出现之前,裸机云的前生是物理服务器托管。随着云概念的兴起,2010年和2012年 ,Softlayer和Rackspace相继推出“裸金属云”服务,支持自定义硬件基础,让托管服务器有了“云”的雏形。 裸金属云 拥有物理机同样的性能和安全 拥有云主机同样的灵活和弹性 取得了性能和灵活性很...

Threejs 开发3D地图实践总结

  前段时间连续上了一个月班,加班加点完成了一个3D攻坚项目。也算是由传统web转型到webgl图形学开发中,坑不少,做了一下总结分享。 1、法向量问题   法线是垂直于我们想要照亮的物体表面的向量。法线代表表面的方向因此他们为光源和物体的交互建模中具有决定性作用。每一个顶点都有一个关联的法向量。   如果一个顶点被多个三角形共享,共享顶点的法向量等于...

Unity实现滑页效果(UGUI)

简介 项目需要...直接展示效果吧: 原理 使用UGUI提供的ScrollRect和ScrollBar组件实现基本滑动以及自己控制每次移动一页来达到滑页的效果。 实现过程 1.创建两个panel,上面的panel用于显示,下面的panel用于存放按钮 2.在TopPanel上添加ScrollRect脚本,用于滑动 3.在TopPanel下创建...

unity, 设置帧率上限

用unity做了个demo,把所有开销大的特效都去了,在真机上运行仍然卡。显示帧率来看,最高到30。原来unity在ios设备上帧率默认限制为不超过30。 可以通过Application.targetFrameRate = 60;改成最高60。注意这个设置对编辑器无效。 参考: http://answers.unity3d.com/questions/32...

【Unity Shader学习笔记】(一)在表面着色器中控制顶点变换

通常境况下,我们可以方便地使用表面着色器对材质进行简单的金属光泽、平滑度等设置。但是如果要想对顶点进行控制,就需要使用顶点片段着色器。然而,在顶点片段着色器中,连最基本的漫反射、高光等都需要手动去写,显然比较麻烦。因此,如果能在表面着色器中进行顶点的控制就好了。 当然,这是可以做到的! 首先,在Unity中生成一个基本的表面着色器,在Project选项卡...