Unity实现GPU Cull渲染

这篇具有很好参考价值的文章主要介绍了Unity实现GPU Cull渲染。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

开放世界游戏中植被和物件的数量往往是巨大,  而传统组织大量植被渲染的方式是利用QuadTree/Octree/Kd-Tree等数据结构对植被Intance数据进行预先生成一个个Cluster,然后在运行时进行FrustumCull,收集可视的所有Cluster,最后进行DrawInstance.

这种方式往往存在两个缺点: 

[1]Cluster粒度和DrawInstance渲染的粒度冲突,也就是单个Cluster粒度很大时,很多位于视椎体外的Instance是浪费的绘制(VS/PS上的浪费)。而当Cluster粒度比较小时, DrawInstance渲染批次可能会明显上升。

[2]第二个缺点是完全依赖CPU的剔除, 从效率上很难做到单个Instance的高效剔除。

unity调用gpu,computer graphics,Unity图形渲染,unity,游戏引擎

 对于上面两点,基于GPU剔除的方案可以很好解决这个问题.

CPU Cull + GPU Cull

CPU Cull(QuadTree)

以四叉树组织Instance数据

unity调用gpu,computer graphics,Unity图形渲染,unity,游戏引擎


static class QuadTreeUtil
{
    public const int QUAD_TREE_NODE_MAX_NUM = 128;
}

[Serializable]
public struct InstanceData
{
    public Vector4 instance;
    public InstanceData(Vector3 inPos, int clusterId)
    {
        instance = new Vector4(inPos.x, inPos.y, inPos.z, (float)clusterId);
    }
}


[Serializable]
public class QuadTree
{
    [NonSerialized]
    public Vector3[] alllTreePos;
    
    [HideInInspector]
    public InstanceData[] instanceDatas;
    private int[] treeCullFlags;
    public TreeNode rootNode;
    public int leafId;

    public QuadTree(Vector3[] inAllTreePos, Vector2 regionMin, Vector2 regionMax)
    {
        List<int> indices = inAllTreePos.Select((item, index) => index).ToList();
        alllTreePos = inAllTreePos;
        leafId = 0;
        rootNode = new TreeNode(this, indices, regionMin, regionMax);
        BuildTreeClusterData();
    }
}

 默认下每个叶子节点最大数量为128个Instance

unity调用gpu,computer graphics,Unity图形渲染,unity,游戏引擎

 Instance数据得记录所在的Cluster(leafNodeId)

所以CPU阶段的QuadTree主要是两个作用:

[1]收集InstanceDatas

unity调用gpu,computer graphics,Unity图形渲染,unity,游戏引擎

 unity调用gpu,computer graphics,Unity图形渲染,unity,游戏引擎

[2]在CPU每帧进行QuadTree的粗粒度Frustum剔除,求出可见的leafNode Id集合。这里的leafNode Id集合用固定大小数组int[] 来表示, 1代表被剔除,0代表可见,用于后续的GPU剔除。unity调用gpu,computer graphics,Unity图形渲染,unity,游戏引擎

 unity调用gpu,computer graphics,Unity图形渲染,unity,游戏引擎

GPU Cull

GPU Cull是利用ComputeShader针对单个Instance进行的剔除,主要分为三步:

[1]基于CPU的粗粒度QuadTree剔除的结果,在GPU可以直接进行第一轮剔除

unity调用gpu,computer graphics,Unity图形渲染,unity,游戏引擎

 [2]在GPU针对Instance进行 SphereFrustumCull 进行第二轮剔除

unity调用gpu,computer graphics,Unity图形渲染,unity,游戏引擎

unity调用gpu,computer graphics,Unity图形渲染,unity,游戏引擎

 [3]基于HZB(Hierarchical Z-Buffering 分层 Z 缓冲)的遮挡剔除, 就是根据当前物体的渲染深度和它的在上一帧渲染的HZB的深度作对比来判断物体是否被遮挡。

unity调用gpu,computer graphics,Unity图形渲染,unity,游戏引擎

 unity调用gpu,computer graphics,Unity图形渲染,unity,游戏引擎

 HZB的原理和生成推荐 这篇博客 Compute Shader 进阶应用:结合Hi-Z 剔除海量草渲染 - 知乎

文章介绍的HZB生成方法质量比较不错,HZB各级Mip 像素偏移和丢失的现象较少。

最终GPU 剔除的ComputeShader如下:

// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel CSMain
#pragma enable_d3d11_debug_symbols
//#pragma use_dxc
// Create a RenderTexture with enableRandomWrite flag and set it
// with cs.SetTexture

StructuredBuffer<float4> instanceDatas;
StructuredBuffer<int> treeNodeCullFlag; // 1 mean culled, 0 mean visible
RWStructuredBuffer<float3> posVisibleBuffer;
RWStructuredBuffer<int> bufferWithArgs;

int allCount;
float4 bounds; // center + radius
float4 frustumPlanes[6];
float3 cameraWorldDirection;
float4x4 worldToViewProject;
Texture2D<float4> hizTexture;
float hizMapSize;
int maxHizMapLevel;


float GetDistance(float3 pos, float4 plane)
{
    return plane.x * pos.x + plane.y * pos.y +  plane.z * pos.z + plane.w;
}

int IsOutOfFrustum(float3 pos, float radius, float4 frustum[6])
{
    for(int i = 0; i < 6; i++)
    {
        float distance = GetDistance(pos, frustum[i]);
        if(distance >= radius)
        {
            return 1;
        }
    }

    return 0;
}


float3 TransformToNdc(float3 worldPos)
{
    float4 worldAlignPos = float4(worldPos, 1.0);
    float4 ndc = mul(worldToViewProject, worldAlignPos);
    ndc.xyz /= ndc.w;
    ndc.xy = ndc.xy * 0.5 + 0.5;
    return ndc.xyz;
}

int GetHizMapIndex(float2 boundMin, float2  boundMax)
{
    float2 uv = (boundMax - boundMin) * hizMapSize;
    uint2 coord = ceil(log2(uv));
    uint index =  max(coord.x, coord.y);
    return min((int)index, maxHizMapLevel);
}

bool IsCullByHizMap(float3 pos, float boxWidth)
{
    float3 offsetPos = pos;
    float3 ndc1 = TransformToNdc(pos + float3(boxWidth, boxWidth, boxWidth));
    float3 ndc2 = TransformToNdc(pos + float3(boxWidth, -boxWidth, boxWidth));
    float3 ndc3 = TransformToNdc(pos + float3(boxWidth, boxWidth, -boxWidth));
    float3 ndc4 = TransformToNdc(pos + float3(boxWidth, -boxWidth, -boxWidth));
    float3 ndc5 = TransformToNdc(pos + float3(-boxWidth, boxWidth, boxWidth));
    float3 ndc6 = TransformToNdc(pos + float3(-boxWidth, -boxWidth, boxWidth));
    float3 ndc7 = TransformToNdc(pos + float3(-boxWidth, boxWidth, -boxWidth));
    float3 ndc8 = TransformToNdc(pos + float3(-boxWidth, -boxWidth, -boxWidth));

    float3 min0 = min(min(ndc1, ndc2), min(ndc3, ndc4));
    float3 min1 = min(min(ndc5, ndc6), min(ndc7, ndc8));
    float3 boundsMin = min(min0, min1);

    float3 max0 = max(max(ndc1, ndc2), max(ndc3, ndc4));
    float3 max1 = max(max(ndc5, ndc6), max(ndc7, ndc8));
    float3 boundsMax = max(max0, max1);
    uint mip = GetHizMapIndex(boundsMin, boundsMax);

    float currentHizMapWidth = hizMapSize / pow(2, mip);
    float2 uv0 = min(currentHizMapWidth - 1, floor(boundsMin.xy * currentHizMapWidth));
    float2 uv1 = min(currentHizMapWidth - 1, floor(boundsMax.xy * currentHizMapWidth));

    float d0 = hizTexture.mips[mip][uv0].r;
    float d1 = hizTexture.mips[mip][uv1].r;
    return boundsMax.z < d0 && boundsMax.z < d1;
}


[numthreads(16, 16,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    uint instanceId = id.x * 1500 + id.y;
    if(instanceId >= allCount)
        return;
    
    float4 instance = instanceDatas[instanceId];
    int clusterId = (int)instance.w;
    if(treeNodeCullFlag[clusterId] == 1)
        return;

    float3 worldPos = instance.xyz;
    float3 pos =  worldPos + bounds.xyz;
    if(IsOutOfFrustum(pos, bounds.w, frustumPlanes) == 0 && (!IsCullByHizMap(pos, bounds.w)))
    {
        int currentIndex;
        InterlockedAdd(bufferWithArgs[0], 1, currentIndex);
        posVisibleBuffer[currentIndex] = worldPos;
    }
}

Draw Instance Indirect 

利用GPU剔除得到的RWStructuredBuffer<float3> posVisibleBuffer 和 RWStructuredBuffer<int> bufferWithArgs 进行DrawInstanceIndirect。

Graphics.DrawMeshInstancedIndirect(treeMesh, 0, treeMaterial, drawIndirectBounds, bufferWithArgs, 0, null, ShadowCastingMode.Off, false);

Demo效果:

CPU:AMD 锐龙7950X

GPU:  RTX3060Ti

植被数量:200W

Cpu cull + GPU Cull: 2ms左右

Draw Instance Indirect :4.5ms左右

unity调用gpu,computer graphics,Unity图形渲染,unity,游戏引擎

Unity GPU Cull

项目Git链接

https://github.com/2047241149/GPUFoliageCull

参考资料

[1]Compute Shader 进阶应用:结合Hi-Z 剔除海量草渲染 - 知乎

[2]https://www.cnblogs.com/mmc1206x/p/15542745

[3]Hierarchical-Z map based occlusion culling – RasterGrid

[4]Mobile hierarchical z buffer occlusion culling - 知乎

[5]Mobile GpuCulling for Unreal - 知乎

[6]Unity中使用ComputeShader做视锥剔除(View Frustum Culling) - 知乎

[7]LearnOpenGL - Frustum Culling

[8]视锥体剔除AABB和OBB包围盒的优化方法 - 知乎

[9]有没有较完善高效的视锥剔除算法? - 知乎

[10]AABB-Plane · 3DCollisions文章来源地址https://www.toymoban.com/news/detail-725681.html

到了这里,关于Unity实现GPU Cull渲染的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Unity性能优化与分析--GPU

    每一帧,Unity 都需要确定必须渲染哪些对象,然后创建绘制调用。绘制调用是调用图形 API 来绘制对象(如三角形),而批处理是要一起执行的一组绘制调用。 随着项目变得更加复杂,您需要用管线来优化 GPU 的工作负载。 通用渲染管线 (URP) 目前使用单通道前向渲染器将高质

    2024年03月15日
    浏览(41)
  • unity 性能优化之GPU和资源优化

    众所周知,我们在unity里编写Shader使用的HLSL/CG都是高级语言,这是为了可以书写一套Shader兼容多个平台,在unity打包的时候,它会编译成对应平台可以运行的指令,而变体则是,根据宏生成的,而打包运行时,GPU会根据你设置的宏切换这些打包出来的代码,而不是我们书写那

    2024年02月02日
    浏览(47)
  • Unity中Batching优化的GPU实例化(1)

    在之前的文章中,我们解析了 Batching 优化中的 动态合批 和 静态合批。在这篇文章中我们来做一下 GPU实例化测试之前的准备 Unity中Batching优化的动态合批 Unity中Batching优化的静态合批 GPU实例化主要应用于大量网格生成的情况 我们先在Unity中,实现一下大量生成网格 public Gam

    2024年02月04日
    浏览(31)
  • Unity中Batching优化的GPU实例化(3)

    在上篇文章中,我们主要解析了 Unity 中 GPU实例化的定义 实例化ID 步骤干了什么。 Unity中Batching优化的GPU实例化(2) 我们在这篇文章中,把定义的 实例化ID 给使用起来,使合成一个批次的模型 包含的渲染的对象坐标显示正确。 UNITY_SETUP_INSTANCE_ID(v); 放在顶点着色器/片断着色

    2024年02月03日
    浏览(35)
  • Unity中Batching优化的GPU实例化(4)

    在之前的文章中,我们解决了GPU实例化需要的 appdata 、v2f 数据准备 和 使GPU实例化后的顶点位置正确。 Unity中Batching优化的GPU实例化(2) Unity中Batching优化的GPU实例化(3) 在这篇文章中,我们来实现一下GPU实例化后怎么使不同对象使用不同的材质颜色,这里只是用颜色作为例

    2024年02月05日
    浏览(50)
  • Unity3D GPU Selector/Picker

    1.动机 Unity3D中通常情况下使用物理系统进行物体点击选择的基础,对于含大量对象的场景,添加Collider组件会增加内容占用,因此使用基于GPU的点击选择方案 2.实现思路 对于场景的每个物体,构建一个meshrenderer,color,在点击后,替换物体材质的颜色为该字典中相应键值的颜色

    2024年02月14日
    浏览(30)
  • Unity Shader Cull(双面显示)

    先不概述了原理我也不是很理解,但找到办法就分享出来。 首先使用Unity并不是很熟的原因吧,双面显示很简单的东西都得设及Shader创建,那不说多了直接进入正题! 首先先创建个URP,资产栏创建 ShaderGraph—URP—LitshaderGraph 我们就得到了一个标准编辑器的一个东西 双击打开

    2024年02月12日
    浏览(30)
  • Unity3D学习笔记8——GPU实例化(3)

    在前两篇文章《Unity3D学习笔记6——GPU实例化(1)》《Unity3D学习笔记6——GPU实例化(2)》分别介绍了通过简单的顶点着色器+片元着色器,以及通过表面着色器实现GPU实例化的过程。而在Unity的官方文档Creating shaders that support GPU instancing里,也提供了一个GPU实例化的案例,这里就详

    2023年04月22日
    浏览(34)
  • Unity GPU Instancing合批_如何基于单个的实体修改材质参数

    最近在做DOTS的教程,由于DOTS(版本1.0.16)目前不支持角色的骨骼动画,我们是将角色的所有动画数据Baker到一个纹理里面,通过修改材质中的参数AnimBegin,AnimEnd来决定动画播放的起点和终点,材质参数AnimTime记录当前过去的动画时间。但是在做大规模战斗控制的时候,有10000+的小

    2024年01月21日
    浏览(34)
  • 如何选择合适的GPU进行渲染?最佳渲染GPU推荐

    以下是为 3D 艺术选择显卡的一些技巧: 考虑一下您的预算 : 显卡的价格从几百元到几万元不等。在开始购物之前确定您愿意花多少钱。 阅读评论 : 网上有很多关于显卡的评论,阅读它们以了解特定卡的性能如何。 决定您要创作哪种 3D 艺术 : 如果您刚刚开始,您可能不

    2024年02月13日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包