GPU绑定。 第二部分 无尽的森林



在几乎每个游戏中,都需要用创造虚拟世界的视觉丰富性,美观性和可变性的对象来填充游戏关卡。 参加任何开放世界的游戏。 那里的树木,草,土和水是图片的主要“占位符”。 今天,GPGPU数量很少,但是我将尝试告诉您如何在无法但确实想要的时候在框架中绘制很多树木和石头。

应当立即注意到,我们有一个小型独立工作室,而且我们通常没有资源来绘制和建模每件小东西。 因此,要求各种子系统成为发动机现成功能的“上层建筑”。 因此,这是关于动画的周期的第一篇文章(我们使用并加速了完整的Unity动画系统),因此将在此处。 这极大地简化了游戏中新功能的引入(减少了学习,减少了错误等)。

因此,任务是:您需要绘制很多森林。 在游戏中,我们有一个大型(30x30 km)实时策略(RTS),这为渲染系统设置了基本要求:

  • 在小地图的帮助下,我们可以立即转移到关卡上的任意点。 并且有关用于新位置的对象的数据应已准备就绪。 在FPS或TPS游戏中过一段时间后,我们不能依靠资源加载。
  • 如此大的对象需要大量的对象。 数十万,甚至数百万。
  • 同样,较大的级别使得手动设置“森林”非常漫长且困难。 森林,石头和灌木丛的程序生成是必要的,但是在游戏级别的关键位置可能需要手动调整和安排。

这个问题怎么解决? 这样数量的单元通常布置的物体仍将不被拉动。 我们将死于淘汰和批处理。 可以使用实例化进行渲染。 有必要编写一个控制系统。 树木需要建模。 树动画系统需要完成。 哦 我立即要精美。 有SpeedTree,但没有用于动画的api,广告牌的顶视图很糟糕,因为没有“水平广告牌”,并且文档很差。 但是什么时候阻止了我们? 我们将优化SpeedTree渲染。

渲染图


让我们开始看看普通的speedtree对象是否一切都很糟糕:



舞台上大约有2,000棵树。 一切按渲染顺序排列,实例化将树分成批处理,但使用CPU则一切都很糟糕。 相机渲染的时间有一半在冷却。 我们需要成千上万。 我们绝对拒绝GameObjects,但是现在我们需要揭示SpeedTree模型的结构,切换LOD的机制以及使用手柄进行所有操作。

SpeedTree树由几个LOD(通常为4个)组成,最后一个是广告牌,其余所有都是细节程度不同的几何图形。 它们中的每一个都包含多个sabmesh,具有自己的材质:


这不是SpeedTree的特异性。 任何结构都可以具有这种结构。 LOD切换以两种可用模式实现:

  1. 淡入淡出:

  2. 速度树:


CrossFade (就Unity着色器而言,由LOD_FADE_CROSSFADE预处理器定义定义)是具有多个细节级别的任何场景对象的主要LOD切换方法。 它包含以下事实:更改LOD时,应消失的网格不仅消失(模型的质量跳跃将清晰可见),还通过抖动在屏幕上“溶解”。 一种简单的效果,并且避免使用真正的透明度(alpha混合)。 应该以完全相同的方式出现的模型“出现”在屏幕上。

SpeedTree (LOD_FADE_PERCENTAGE)专为树木制成。 除主坐标外,相对于当前LOD级别的次顶点位置的附加坐标也记录在树叶,树枝和树干的几何形状中。 从一个级别到另一个级别的过渡程度是这两个位置的线性插值的权重值。 使用CrossFade方法可以将广告牌移入/移出广告牌。

原则上,这是实现自己的LOD交换系统所需的全部知识。 渲染本身很简单。 我们遍历所有LOD以及每个LOD的所有下垂的所有类型的树。 我们安装适当的材料,并使用实例化绘制该对象的所有实例。 因此,DrawCalls的数量等于场景中唯一对象的数量。 我们怎么知道画什么? 这对我们有帮助

森林发电机


登陆本身简单而朴实。 对于每种树,我们将世界划分为四边形,以便每棵树适合一棵树。 我们遍历所有四边形并检查表单的掩码:



在该级别的给定点上,可以在此处植树吗? 带有“树木繁茂”位置的面具是由关卡设计师绘制的。 最初,整个过程都在CPU和C#上。 生成器工作缓慢,并且级别的大小增大,因此等待数十分钟的更新变得充满压力。 决定将生成器转移到GPU和计算着色器。 在这里,一切都很简单。 我们需要土地的高度图,植树蒙版和AppendStructuredBuffer,在其中添加生成的树(位置和ID,即所有数据)。

手动脚本在关键点处安排一个特殊的脚本,将其放入通用数组中,并从场景中删除原始对象。

剔除和LOD切换


仅知道树的位置和类型不足以进行有效的渲染。 有必要确定每个帧哪些对象可见,以及哪个LOD(考虑过渡逻辑)发送给渲染。

一个特殊的计算着色器也可以做到这一点。 对于每个对象,首先执行平截头剔除:


如果对象可见,则执行LOD切换逻辑。 根据屏幕上的大小,我们确定所需的LOD级别。 如果为该组的LOD设置了CrossFade模式,则我们增加抖动的过渡时间。 如果使用SpeedTree百分比,则考虑LOD之间的归一化过渡值。

现代的图形API具有出色的功能,可以将图形提交信息传递到计算缓冲区中的图形调用(例如,D3D11的ID3D11DeviceContext :: DrawIndexedInstancedIndirect)。 这意味着您也可以在GPU上填充此计算缓冲区。 因此,事实证明,这是一个完全独立于CPU的系统(好吧,只需调用Graphics.DrawMeshInstancedIndirect)。 在我们的情况下,只需要记录每个sabmesh的实例数。 其余信息(网格中的索引数和偏移量)是静态的。

计算缓冲区(带有用于draw调用的参数)分为多个部分,每个部分负责调用其子网格的渲染。 在要在当前帧中绘制的网格的计算着色器中,增加相应的InstanceCount值。

这是它在渲染中的外观:


GPU遮挡剔除是显而易见的下一步,但是对于具有这样的摄像头且不是非常高的丘陵的RTS,获胜并不是那么明显(这针对那些感兴趣的人)。 我还没做

为了正确绘制所有内容,您需要对SpeedTree着色器进行一些调整,以获取来自相应计算缓冲区的LOD之间转换的位置和值。

现在我们绘制美丽但静止的树木。 SpeedTree树实际上受到风的影响,从而使它们生气蓬勃。 此类动画的整个逻辑都在文件SpeedTreeWind.cginc中,但是没有文档或无法从Unity访问内部参数。

CBUFFER_START(SpeedTreeWind) float4 _ST_WindVector; float4 _ST_WindGlobal; float4 _ST_WindBranch; float4 _ST_WindBranchTwitch; float4 _ST_WindBranchWhip; float4 _ST_WindBranchAnchor; float4 _ST_WindBranchAdherences; float4 _ST_WindTurbulences; float4 _ST_WindLeaf1Ripple; float4 _ST_WindLeaf1Tumble; float4 _ST_WindLeaf1Twitch; float4 _ST_WindLeaf2Ripple; float4 _ST_WindLeaf2Tumble; float4 _ST_WindLeaf2Twitch; float4 _ST_WindFrondRipple; float4 _ST_WindAnimation; CBUFFER_END 

我们将如何挑选它们? 为此,对于每种类型的树,我们都将原始SpeedTree对象呈现在不可见位置的某个位置(或者,在Unity中可见,但在相机中不可见,否则参数将不会更新)。 这可以通过大大增加边界框并将对象放置在相机后面来实现。 使用material.GetVector(...)删除每个框架所需的一组值。

因此,树木在风中摇曳,但广告牌的顶视图令人沮丧:


使用着色器选项BILLBOARD_FACE_CAMERA_POS更糟:


我们需要水平(自上而下)的广告牌。 自King Pea以来,这是SpeedTree的标准功能,但是从论坛的角度来看,它仍未在Unity中实现。 SpeedTree官方论坛上的帖子 :“ Unity集成从未使用过水平广告牌。” 我们会紧紧的双手。 几何本身很容易制作。 如何在地图集中为她找出子画面的UV坐标?


我们获得了旧的SpeedTreeRT SDK,并在文档中找到了结构:

 struct SBillboard { bool m_bIsActive; const float* m_pTexCoords; const float* m_pCoords; float m_fAlphaTestValue; }; 

“ M_pTexCoords指向一组4(s,t)个纹理坐标,这些坐标定义了广告牌上使用的图像。 m_pTexCoords包含8个条目。”,它用外语表示。 好吧,我们将在二进制spm文件中查找4个浮点值的序列,每个值都在[0..1]范围内。 通过科学戳的方法,我们发现所需的序列在一个12个浮点的块的前面,并带有与该模式相对应的符号:

 float signs[] = { -1, 1, -1, 1, 1, -1, 1, 1, 1, -1, 1, 1 }; 

我们在专家上编写了一个小型控制台实用程序,该实用程序可迭代所有spm文件,并在其中查找水平广告牌的uv坐标。 输出是这样的CSV标签:

 Azalea_Desktop.spm: 0, 1, 0.333333, 1, 0.333333, 0.666667, 0, 0.666667, Azalea_Desktop_Flowers_1.spm: 0, 1, 0.333333, 1, 0.333333, 0.666667, 0, 0.666667, Azalea_Desktop_Flowers_2.spm: 0, 1, 0.333333, 1, 0.333333, 0.666667, 0, 0.666667, Leaf_Map_Maker_Desktop_1_Modeler_Use_Only.spm: Pattern not found! Leaf_Map_Maker_Desktop_2_Modeler_Use_Only.spm: Pattern not found! BarrelCactus_Cluster_Desktop_1.spm: 0, 0.592376, 0.407624, 0.592376, 0.407624, 0.184752, 0, 0.184752, BarrelCactus_Cluster_Desktop_2.spm: 0, 1, 0.499988, 1, 0.499988, 0.500012, 0, 0.500012, BarrelCactus_Desktop_1.spm: 0, 0.2208, 0.220748, 0.2208, 0.220748, 5.29885e-05, 0, 5.29885e-05, BarrelCactus_Desktop_2.spm: 0, 1, 0.301392, 1, 0.301392, 0.698608, 0, 0.698608, 

要将纹理坐标分配给水平广告牌的几何形状,我们找到所需的记录并进行解析。

现在是这样的:


仍然不是很好。 使用alpha测试阈值,我们将从角度到摄像机的录制中淡化垂直广告牌:



总结

Profiler显示动态(正在渲染多少东西)和静态(场景中有多少对象及其参数)统计信息:


好吧,最后一个漂亮的视频(下半部分显示了质量级别的切换):


我们到底拥有什么:

  • 系统完全独立于CPU。
  • 它运作迅速。
  • 它使用现成的SpeedTree资产,您可以在Internet上购买这些资产。
  • 当然,我与任何LODGroup结识了她的朋友,而不仅仅是SpeedTree。 现在也可以有这么多的卵石。

在缺点中可以注意到缺乏遮挡剔除并且仍然不是非常具有表现力的广告牌。

Source: https://habr.com/ru/post/zh-CN483946/


All Articles