找回密码
 立即注册
查看: 203|回复: 0

使用Unity的SRP实现GPU Driven实践记录(一)

[复制链接]
发表于 2022-11-17 15:10 | 显示全部楼层 |阅读模式
GPU Driven的简单介绍

在模型的渲染的过程中,渲染的瓶颈有时是因为GPU需要的计算过多,GPU不堪重负,叫苦连连。也有时是因为在应用阶段CPU需要做的功能过多,所以说CPU运行的时间过多。对于现代的GPU来说,GPU的性能还是相当给力的,CPU分配的任务大多数能够很快的完成。而对于CPU来说,不能像GPU一样专攻一处,导致很多时候GPU都在等待CPU的指令。为了解决这种问题,人们发明了GPUDriven的方式来减少应用准备阶段要做的工作和等待的时间。
那么GPUDriven是怎么做到的呢?我们知道我们每一帧都会有Draw Call。DrawCall定义了GPU该怎么画,画什么,并告诉GPU可以开始绘制了。但是对于很多渲染的物体来说,它们并不需要每一帧都更改绘画的状态,因为它每一帧都是一样的。就好比达芬奇画的那数不清的鸡蛋,每次画鸡蛋之前老师都要重新教一遍达芬奇该怎么画。但是有一天达芬奇烦了:我可是个绘画大师,老师你不用一遍遍的教我,你只要告诉我“画!”,我就能画一个完美的鸡蛋,并且我还能做的更多,即使鸡蛋有细微的变化,只要他是个鸡蛋我就能画出来。
比较抽象啊,但是基本的思路大概就是这样。当然CPU就是老师,GPU呢也不是达芬奇,GPU可能是会画画的小学生。所以说问题被转化成三步:
一:老师教会小学生画鸡蛋。
二:老师把所有鸡蛋给小学生,让小学生画。
三:小学生一遍遍的画鸡蛋。
这时候老师每天去下达画鸡蛋的命令之后,老师就不用再管了,然后老师这一天就能腾出更多的时间去做别的事了。以上就是GPUDriven比较通俗的解释。如果想要详细的了解的话可以参照刺客信条大革命的Siggragh分享:
或者是下面这篇文章:
接下来我们把抽象的问题落实到实际:
分析问题:

我们所需要的最核心的API在Unity中是DrawProceduralIndirect的指令。这就是每天老师下达的画的指令,这个指令需要一些数据,也就是要拿给小学生做参照的鸡蛋。我们从这个数据入手来看我们需要在应用开始时把什么样的“鸡蛋”给GPU。注意这里老师没有每一天挑个鸡蛋给小学生画,而是一股脑的把鸡蛋都给了小学生。也就是把所有的数据最开始都给了GPU。所以说这种方式是一种用空间换时间的做法,当然这个时候GPU也做了额外的工作,但是对于GPU来说是很容易完成的。在使用的使用应该注意显存的影响。
我们需要准备的数据包括:
1.原本顶点缓冲的数据我们要储存在ComputeBuffer中。
2.顶点索引缓冲的数据。
3.告诉GPU要画几个实例,每个实例多少顶点,和一些的配置信息的Buffer。
其余的像材质,包围盒这种都是不太重要的信息,配置好了就可以了。而对于上面我们所说的信息则需要我们小心的管理。注意这个时候更改信息时我们都需要在GPU中处理相关的信息。因为这个时候老师已经走了。准备好这些数据我们现在画每天一模一样的鸡蛋是没有问题了。
但是老师肯定需要学生不能机械的做这些工作,之前有老师指导这些工作,现在老师需要把一些变通之法提前告诉学生。现在我们回想不使用GPU Driven CPU都做了什么?CPU进行了SetPassCall,和视锥体剔除等一些剔除,对于动的物体还可能计算了骨骼动画。在后面进阶的问题再一步步的教给小学生。
解决问题:

到这我们差不多把我们要解决的问题理清了,接下来我们就是一步一步的解决问题。
画一个最简单的“鸡蛋”

我们把所有GPU Driven形式绘制的顶点数据在开始运行时放到GPU里面,对于不同的两个绘制物体也要存到一个Buffer里面因为我们只会有一个DrawCall。我们在GPU中使用这样的代码就能正确的去取用顶点数据。这个时候我们实现了最简单的GPU Driven,并能够画简单的模型。
StructuredBuffer<Vertex> _VertexBuffer;//顶点的数据
Varyings vert(uint vertexID : SV_VertexID, uint instanceID : SV_InstanceID){
    float4 pos = float4(VertexDataArray[vertexID],1);
}
但是我们我们为了学会老师学的东西,需要更多的东西,比如老师的指导笔记这种,我们的数据会进一步的变的更加复杂。
Setpass Call:

我们先解决Setpass Call的问题,并提前考虑视锥剔除来设计一下我们要准备数据的层次结构(以下的就包括我瞎写瞎想的内容了)。在刺客信条大革命GPU Driven的视锥体的剔除工作中,他们将模型按照64个顶点为一组来在GPU中进行剔除工作。这样一个Cluster(一组)就是模型组织的最小的聚合单位。考虑到Unity中模型的操作逻辑我在Cluster上面加入RenderObject这一层(也就是直觉上的一个模型)。一个renderObject 包含一个或数个Cluster。
public struct ObjectInfo{
        public MaterialProperty materialProperty;
        public Matrix4x4 local2WordMatrix;
        public static int GetStride() {
            
            return MaterialProperty.GetStride() + sizeof(float) * (4 * 4) ;
        }
    }
    [System.Serializable]
    public struct MaterialProperty
    {
        public Vector4 mainColor;
        public static int GetStride()
        {
            return sizeof(float) * (4);
        }
    }

    public ObjectInfo[] renderObjects;
    public int renderObjectCount = 0;上面是RenderObject层级储存的信息。包含了材质的数据和模型空间转换到世界空间的矩阵。这和正常情况下一个GameObject的逻辑是一致的。
public struct ClusterInfo
    {
        public Bounds bounds;
        public int objIndex;

        public static int GetStride()
        {
            return Bounds.GetStride() + sizeof(float) * (0) + sizeof(int) * 1;
        }
    }
    [Serializable]
    public struct Bounds
    {
        public Vector3 extent;
        public Matrix4x4 localToWorldMatrix;
        public static int GetStride() {
            return sizeof(float) * (3 + 4 * 4);
        }
    }



    public ClusterInfo[] clusterInfos;
    public int clusterCount = 0;上面是Cluster层级存储的信息。包括了剔除所需的包围盒信息,和标记Cluster属于哪一个RenderObject。
然后我们在Shader中按照一定的规则来处理数据
Shader "GpuDrivenShader"
{
    Properties
    {
        
    }

    SubShader
    {
        Cull Off
        // URP的shader要在Tags中注明渲染管线是UniversalPipeline
        Tags
        {
            "RanderType" = "Opaque"
            "LightMode" = "GpuDriven"
        }

        HLSLINCLUDE

            // 引入Core.hlsl头文件,替换UnityCG中的cginc
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            // 将不占空间的材质相关变量放在CBUFFER中,为了兼容SRP Batcher
        
            //------------------------------------------------------------------------------------//
            struct Bounds
            {
                float3 extent;
                float4x4 localToWorldMatrix;
            };
            struct Cluster
            {
                Bounds bound;
                int objIndex;
            };
            struct MaterialProperty
            {
                 float4 mainColor;     
            };
            struct ObjectInfo
            {
                MaterialProperty material_property;
                float4x4 local2WorldMatrix;
            };
            
            struct Vertex
            {
                float3 pos;
                float2 uv;
                float3 normal;
                float3 tangent;
            };
             //------------------------------------------------------------------------------------//
        
            StructuredBuffer<float3> VertexDataArray;
        
            StructuredBuffer<Vertex> _VertexBuffer;//顶点的数据
            StructuredBuffer<Cluster> _ClusterBuffer;
            StructuredBuffer<ObjectInfo> _RenderObjectBuffer;//渲染物体
            StructuredBuffer<int> _ResultBuffer;//剔除结果
            
            Texture2DArray<float4> _Textures;
            
        
            
            

            struct Attributes
            {
                uint vertexID : SV_VertexID;
                uint instanceID : SV_InstanceID;
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                uint objectIndex : TEXCOORD0;
            };

        ENDHLSL

        Pass
        {
            // 声明Pass名称,方便调用与识别
            Name "ForwardUnlit"
            Tags {"LightMode" = "GpuDriven"}
            

            HLSLPROGRAM

                // 声明顶点/片段着色器对应的函数
                #pragma vertex vert
                #pragma fragment frag
                #pragma target 5.0
                #pragma enable_d3d11_debug_symbols //允许调试

                // 顶点着色器
                Varyings vert(uint vertexID : SV_VertexID, uint instanceID : SV_InstanceID)
                {
                    //处理Cluster数据
                    uint cluster_id = _ResultBuffer[instanceID];
                    cluster_id = instanceID;
                    uint renderObjectId = _ClusterBuffer[cluster_id].objIndex;
                    
                    //获取渲染物的信息  
                    const ObjectInfo object_info= _RenderObjectBuffer[renderObjectId];
                    float4 pos = float4(VertexDataArray[vertexID],1);
                    float4 posWS = mul(object_info.local2WorldMatrix, pos);
                    float4 posCS = mul(unity_MatrixVP, posWS);
            
                    //Output
                    Varyings output;
                    //output.uv = TRANSFORM_TEX(input.uv, _BaseMap);
               
                    output.positionCS = posCS;
                    output.objectIndex = renderObjectId;
                    
                    return output;
                }

                // 片段着色器
                half4 frag(Varyings input) : SV_Target
                {
                    MaterialProperty material_property = _RenderObjectBuffer[input.objectIndex].material_property;//材质属性
                    float4 mainColor = material_property.mainColor;

                    return mainColor;
                }
            
            ENDHLSL
        }
    }
}其中的剔除的结果ResultBuffer保存的数据是剔除之后所有有效的Cluster的编号,这个我们以后会用到。
此时我们事实上实现了最简单的GPU Instancing的功能。图片可以看到我们定义了两个RenderObject并定义了材质的属性,我们绘制了两个不同的物体,理论上我们每增加一个RenderObject就多绘制一个物体。并且Dall Call一直只有一个并且没有Setpass Call的负担。


我们解决的办法就是在RenderObject这个层级之下储存了模型的材质信息,在运行开始时也就会将材质信息交给GPU,然后GPU将数据取出来进行运用。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-21 17:41 , Processed in 0.126741 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表