使用Unity的SRP实现GPU Driven实践记录(一)
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,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) ;
}
}
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;
}
}
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 &#34;GpuDrivenShader&#34;
{
Properties
{
}
SubShader
{
Cull Off
// URP的shader要在Tags中注明渲染管线是UniversalPipeline
Tags
{
&#34;RanderType&#34; = &#34;Opaque&#34;
&#34;LightMode&#34; = &#34;GpuDriven&#34;
}
HLSLINCLUDE
// 引入Core.hlsl头文件,替换UnityCG中的cginc
#include &#34;Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl&#34;
// 将不占空间的材质相关变量放在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 &#34;ForwardUnlit&#34;
Tags {&#34;LightMode&#34; = &#34;GpuDriven&#34;}
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;
cluster_id = instanceID;
uint renderObjectId = _ClusterBuffer.objIndex;
//获取渲染物的信息
const ObjectInfo object_info= _RenderObjectBuffer;
float4 pos = float4(VertexDataArray,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.material_property;//材质属性
float4 mainColor = material_property.mainColor;
return mainColor;
}
ENDHLSL
}
}
}其中的剔除的结果ResultBuffer保存的数据是剔除之后所有有效的Cluster的编号,这个我们以后会用到。
此时我们事实上实现了最简单的GPU Instancing的功能。图片可以看到我们定义了两个RenderObject并定义了材质的属性,我们绘制了两个不同的物体,理论上我们每增加一个RenderObject就多绘制一个物体。并且Dall Call一直只有一个并且没有Setpass Call的负担。
我们解决的办法就是在RenderObject这个层级之下储存了模型的材质信息,在运行开始时也就会将材质信息交给GPU,然后GPU将数据取出来进行运用。
页:
[1]