Origin
zhuanlan.zhihu.com
Tags
开放世界
程序化
经验分享
技术详解
项目
地平线:零之曙光
收藏夹
创建时间
收藏类型
Cubox 深度链接
更新时间
原链接
描述
地平线有好几篇 GDC 分享,这个主要讲植物的程序化摆放。
动机:
- 快速迭代,实时看到生成结果
- 种类繁多
- 置信度
一开始在 CPU 做,后面移到 GPU 实现,完成植物的实时摆放。
摆放是基于密度图的,是整个生成的关键,后面会细说。
一些结果指标
多样性的参考指标
让美术完全控制摆放的数据,逻辑,资产。
数据都是 2D 图,流式加载,全部在 GPU 生成,全部在编辑器都是可直接绘制的。
这张图就是美术在编辑器中绘制的 trees 的 mask 图
绘制的 mask 图会被转换为 density(密度),并且通过 map value 映射到特定的植物种类。
用到的 mask 图,注意用的分辨率和压缩格式。
Topo Objects 和 Topo Roads 两张图是使用场景中的物件生成的,直接用一个 Top down camera 渲一次场景的物件和道路就行,用于种植避让 (不能在高速公路或者屋顶上种树) 或边缘处理
高度图信息,地形一张,物件 (比方说小石头) 一张,水一张,真实高度是叠加关系。分开存储可以处理水边或石头边的特殊种植逻辑。
侵蚀图,左图描述的是侵蚀时水的通量 (形成沟壑),右图描述的是侵蚀时的沉积物 (地平线沙漠地区狭缝中的冲积平原等)
植物摆放逻辑
美术绘制的植物种植 mask
植物 mask 和场景生成的 Topo Objects mask 图相乘,用于处理靠近物件的情况
和水相乘,用于处理靠近水的情况
和路相乘,用于处理靠近路的情况。
最后得到一个初步处理的植物 mask(注意这个 mask 不是非 0 即 1,是一个 0~1 连续的灰度图)
对植物做一个分类,基本按高到矮划分。
给每种植物指定 footprint,这个 footprint 就是单颗这种植物的半径 (用于后面计算同种植物的自重叠问题)
每种植物同样还需要指定它的密度,footprint 和 density 可以和 StaticMesh 绑定。
GPU 种植前
GPU 种植后
density map 混合逻辑是连连看
最终连成意大利面
连连看的优化,这是一个关键
将 graph 展平,遍历就行
编译中间格式,生成 shader
共用的 sub graph 可以复用逻辑,无用的 layer 可以跳过
LAYER: 前面连连看的逻辑
WORLDDATA:地形派生的基础图,比如 slope,aspect,各种 curvature,normal,height,ao,erosion-flow,erosion-deposition 等。这些图的计算可以看《Igor Florinsky - Digital Terrain Analysis in Soil Science and Geology (2016, Academic Press)》,里面有所有公式。
Evaluate:求解前面的连连看,生成每种植物的 density map
Descretize: 通过 density map,以及对应植物配置的 density,和一张分布位置图,求出植物的 instance transform buffer
Objects:拿 instance transform buffer 渲染植物。
然后地平线团队做到了可以在运行时查看 Evaluate 中连连看的中间步骤!
说实话这一小段 feature 演示我是来回看了好几次的。他只在口头说了怎么做,没有在 PPT 中说明,听写如下:
what we do with those page of graph we grab all the leaf node. And we collect them in a list of the layers and each of layers has all the information of each to placement the associate type in the world. Including a intermediate form for the graph. we compile the graph into like the intermediate forms And into going to what it deteck . You know some kind of intermediate instruction language. and we now base on to compute shader. And compute shader does nothing else but make the density logic inside the gpu and just bakes out the density map. Or you can actually flip indirectly to a gpu base interpretiate shader which you run in virtual machine. a virtual stack machine. you can push that intermediate form indirectly into the system and you can step through it on the gpu. you can hold and you can inspect time to debug these stuff. ok so, the one thing the intermeditate form does is it allow merging of different subgraph . so do you have to happen to centarn asset setup place in different way those million of different like layer stack use one of piece brush so you can use one tree is really common. And because everything is intermidiatly form like merge those together to single layer and artist can extractly find how you can some merge to occur. And it takes out multi of layers to come up what you need those of some set of thousand to set of hundred a lot of . you know , we can do that .
在没有听写之前,我脑子想的:
- 要么是从 graph 一步到位生成 shader,每个步骤都单独生成 shader 并编译。
- 要么是每个单独的节点都是一个 pass,然后走 subpass 降低带宽。
但是这里给出了另一个选项是,用 GPU 写一个虚拟机
我大概试了一下,设计了一套简单的指令集,保持 OpCode+Operand 对齐到一个 float4,开一个 1024 的 float4 数组当寄存器,通过 uniformbuffer 将代码传到 shader 中。
然后用蓝图写个简单的编辑器替代材质编辑器,但是 GPU 侧哪怕做分支和循环展开也还是不够快。
下次有机会的话还是要试一下一步到位的 shader 生成或者 subpass。主要还是 UE 的 shader 编译模型,是要拆成 VertexFactory,MaterialTemplate,Shader 三部分,我不太想碰这坨东西。
扯远了,继续说地平线这个
当通过连连看算出 density map 后,就要考虑如何通过这个 density map 生成实实在在的 instance transform buffer 了。
大概就是从上面的密度图,变成下面这个 dither 的样子,每个 dither 点,就是一个 instance。
怎么从密度图转成 dither 的呢?就是右侧这个 dither 矩阵生成的 dither mask 图。
dither 矩阵是一个随机分布的 1 到 16 的 4x4 矩阵,用这个 4x4 矩阵配合 uv 去 tiling,当 density * 16 小于矩阵对应位置的值时,就设置为 0,否则就这是为 1,然后生成 dither mask 图。
但是呢,用 dither 矩阵生成的 dither mask 图位置过于规则了,所以用一个工具生成一些半径为 1,不规则,不重叠但是填充满一个方形贴图的圆。用这个图中小圆圈的点的位置替代上面那个 dither 矩阵中数字的规则的 uv 位置。(也就是说真实用的时候不是靠这张图,更方便的用法是把这些小圆圈的中心点存到一个 constant buffer 中,并且按照 dither 矩阵中的数字从小到大进行排序)
关键来了,这些小圆圈的半径是归一化为 1 的,但是真实种植的时候,是要把这些小圆圈的点的位置乘上之前设置的植物的 footprint 的,这样就自动地解决了植被的自覆盖问题,它能够保证在你 footprint 范围内,不会种超过一颗的这种类型的树,只要你的 footprint 比你的植物模型大,植物的模型就不会相互重叠,当然更高级的用法还是 TA 控制植物的合理种植距离了,我这里从程序的角度来说明它是如何解决植物重叠的问题的。
用规则 dither 时,玩家很容易发现 pattern
用了上面的新 pattern 后,随机感就增加了。
在 Compute Shader 中,每个 thread group(wavefront,对齐到硬件 groupsize)是用的一张 dither mask 图 (constant buffer) 来处理位置。每个位置是一个 thread(invocation),也就是每个 thread 至多会产生一个植物的 instance。
首先判断 compute shader 边界,
然后每个 thread 用 thread local id 读取 dither constant buffer,获取预期 dither 值,当植物在该位置的 density 值小于预期 dither 值,则丢弃,否则生成一个 instance position。并生成 intance normal, 最终输出到 instance transform buffer 中。
大概是这个样子,植物的 footprint 不一样大,groupsize 覆盖的场景范围也不一样大,成正比的关系。
与 density map 做比较后,留下的可以生成的 instance 位置,也就是粉色那些三角形 / 圆圈
然后在 placement pass 中会做一些修饰工作,比如让树拐弯,采样地表 color 做一些 tint 等。
然后回读,为什么要回读?
因为这一趟流程只处理了一种植物的分布情况,在处理下一种植物的时候,是不能种在之前植物所在位置的,所以需要回读这棵树的 instance 位置所生成的 instance position mask 参与下一种树的 density map 的计算。
不同的树的 footprint 如果不一样大,确实需要做回读。
如果两种树是一样大的 footprint,是可以做一些优化的。
直接一个 pass 算完两种树的 instance position mask, 只要简单的做一个 density 叠加后,做 range 判断就行,类似的技巧在 UE 的 Dither LOD 计算中判断距离时有用到,可以一个运算直接得出当前是哪个级的 LOD 交界处,可以参考。
剩下的就是做 pipeline 的优化了,尽量减少同步点。 > 本文由简悦 SimpRead 转码