UE5.8 程序化植被与 Foliage:沙漠到森林的生长规则

📑 目录

UE5.8 程序化植被与 Foliage:沙漠到森林的生长规则

一个被植被埋掉的早晨

阿杰刚进公司就收到美术组的消息:测试关卡的草原在低端机上帧率只有 22 帧。他打开场景,发现一片半径五百米的草甸里塞了三十七万棵草实例。更离谱的是,草一直刷到玩家根本走不到的山坡背面,树下还长着跟开阔地一样高、一样密的草。

这片草原是他上周用传统 Foliage 模式手工刷的。刷的时候很爽,拖动笔刷哗啦一下就铺满。但优化时他就傻眼了。删哪棵?留哪棵?怎么保证近处密、远处稀?树下的草要不要去掉一半?这些问题没有规则,只有感觉。

老周在旁边看了一眼,说:"你要让场景自己知道草该长在哪。"

这句话把阿杰引向了 UE5.8 的 Procedural Foliage 编辑器。它不会去管每一棵草的具体坐标,只会问这块地能不能长、该长什么。规则对了,一片森林能自己长出来;规则错了,就是数字杂草灾难。这篇文章把我在几个项目里踩过的坑、试过的方案,整理成一套能用在工作流里的经验。

程序化植被与手工笔刷的分工

很多刚接触 UE5.8 的人会把 Procedural Foliage 当成更高级的 Foliage 笔刷。这种想法容易吃亏。传统 Foliage 笔刷的核心是控制:你告诉引擎每一棵草、每一棵树的具体位置。程序化植被的核心是约束。你告诉引擎哪些地方允许生长、哪些植物互相排斥、密度和尺寸怎么随环境变化,然后让系统去填空。

两种方式各有活法。 Foliage 模式适合雕琢关键构图区域,比如主路径两侧的迎客松、营地中央的标志枯树。程序化生成适合铺背景、中景和大面积的自然过渡,比如一整片针叶林的林缘线、河谷两侧的灌木带。

我在项目里一般是三七开:手工 Foliage 占三成,负责视觉焦点;程序化占七成,负责体量和氛围。这个比例是我根据项目经验定的。靠着它,我们把低端机帧率从 22 帧拉到了 55 帧。

Procedural Foliage 编辑器的核心结构

打开 Procedural Foliage 编辑器,你会看到两个关键概念:Foliage Type 和 Foliage Spawner。Foliage Type 是单一植物的档案,包含网格、材质、碰撞、LOD、阴影、风动参数等。Foliage Spawner 则是一张配方表,决定哪些 Foliage Type 参与生成、各自权重多少、遵循什么规则。

一个 Spawner 下面可以挂很多 Type。比如我们的温带森林 Spawner 就挂了五类:

  • 优势乔木:欧洲云杉,数量权重最高,决定天际线。
  • 伴生乔木:白桦树,权重低一些,增加颜色变化。
  • 林下灌木:蓝莓、蕨类,填补树冠间隙。
  • 地面草本:枯草和苔藓,覆盖率最大但单实例极轻。
  • 倒木与枯枝:权重很低,用来打破地面的重复感。

下面是这套结构在编辑器里的关系示意。

flowchart TD
    subgraph editor ["Procedural Foliage Editor"]
        spawner["Foliage Spawner"]
        type_a["Foliage Type: 云杉"]
        type_b["Foliage Type: 白桦"]
        type_c["Foliage Type: 蕨类"]
        type_d["Foliage Type: 枯草"]
    end
    subgraph world ["World"]
        landscape["Landscape"]
        grass["Landscape Grass Type"]
        pcg["PCG Graph"]
    end
    spawner --> type_a
    spawner --> type_b
    spawner --> type_c
    spawner --> type_d
    type_a --> landscape
    type_b --> landscape
    grass --> landscape
    pcg --> landscape

Spawner 本身不会自动往场景里种东西。它更像一个蓝图资产,需要被拖到关卡里,或者用体积(Foliage Spawner Volume)框定范围才会生效。这个设计有个好处:同一张 Spawner 可以复用到多个关卡或同一个关卡的不同生物群落区,你只要改局部参数就能做出差异。

生物群落规则:从真实照片里提取约束

最让新手头疼的是规则怎么定。我的办法是去现场,或者至少找一组高质量参考照片。我们之前做一个沙漠峡谷项目,美术参考了美国西南部荒漠的航拍和地面照片。照片里出现了几个明确规律:

  • 峡谷底部有季节性河道,河床边会长一溜梭梭树和灌木。
  • 向阳的南坡植被稀疏,背阴的北坡相对密集。
  • 巨石背风面会积累土壤,长出小型肉质植物。
  • 沙丘顶部几乎没有植物,坡脚和凹陷处才有草丛。

这些规律就是规则。在 Procedural Foliage 里,我们用 Slope Angle、Height、Landscape Layer 把它们翻译出来。

Slope Angle 控制坡度。沙丘顶部坡度大,排除大型植物;坡脚平缓,允许灌木和草丛。Height 控制海拔。峡谷底部河道附近海拔低、湿度高,允许更多绿色植被;高处山脊干燥,只剩耐旱仙人掌。Landscape Layer 直接读取地形材质层,比如只在 Sand_Grass 层上长草,在 Rock_Dry 层上长仙人掌,在 Soil_Wet 层上长灌木。

规则评定的流程如下图所示。

flowchart TD
    A["Landscape 采样"] --> B{坡度是否小于阈值}
    B -->|"否"| C["排除大型植被"]
    B -->|"是"| D{高度是否在允许区间}
    D -->|"否"| E["排除喜湿植物"]
    D -->|"是"| F["读取 Landscape Layer"]
    F --> G{Layer 匹配}
    G -->|Sand| H["生成草 / 仙人掌"]
    G -->|Soil| I["生成灌木"]
    G -->|Rock| J["生成苔藓 / 多肉"]
    G -->|Water| K["不生成"]

UE5.8 里每个 Foliage Type 都有一张详细的 Placement 参数面板。关键参数包括:

  • Ground Slope Angle:生长允许的最小和最大坡度,单位度。
  • Height Range:基于 Landscape 全局 Z 高度的最小和最大海拔。
  • Landscape Layer:可以指定一个或多个地形图层,并用 Weight Range 限制图层权重区间。
  • Collision Radius:该植物占位圆盘的半径,用来避免同类或异类植物重叠。
  • Shade Radius:树荫影响范围,可以抑制喜光植物在树冠下生长。
  • Scale X/Y/Z Min/Max:尺寸随机范围。

这里有个小坑:Collision Radius 和 Shade Radius 都以厘米为单位,而且默认值经常偏大。我第一次用的时候按默认值,结果一棵树周围两米内寸草不生,林下地皮秃得像被除草剂喷过。后来把 Shade Radius 降到树冠半径的 0.6 倍左右,林下植被才自然起来。

密度、缩放与旋转:用数学把混乱变规则

阿杰那个三十七万棵草的草原,问题不在草本身,而在密度没有距离衰减。玩家能看清的范围大概就半径三十米,再远的草只是色块。 UE5.8 的 Procedural Foliage 允许按 Foliage Type 设置 Initial Seed Density,控制每平方米尝试播种的种子数。但这个数字不是最终实例数,因为后面还要经过规则过滤和排斥计算。

真正决定视觉密度的,是几个参数的组合:

Deffective=DseedPslopePheightPlayer(1Ocollision)(1Oshade)D_{effective} = D_{seed} \cdot P_{slope} \cdot P_{height} \cdot P_{layer} \cdot (1 - O_{collision}) \cdot (1 - O_{shade})

其中 DseedD_{seed} 是初始种子密度,PslopeP_{slope}PheightP_{height}PlayerP_{layer} 是各项地形条件通过概率,OcollisionO_{collision}OshadeO_{shade} 是碰撞和遮罩重叠率。这个式子是我估算有效密度的经验模型,不是引擎内部公式。它提醒团队:调密度不能只拉一个滑条。

缩放控制更微妙。自然界的植物尺寸更接近对数正态分布,均匀分布会显得很假。也就是说,小苗很多,参天大树很少。在 Foliage Type 的 Scale 设置里,我把 Z 轴缩放最小值设到 0.6,最大值设到 1.4,并打开 Scale Variation 的曲线,让中小尺寸出现概率更高。这样同样的实例数量,看起来会比均匀缩放丰富得多。

旋转方面,我一般让 Yaw 全随机 0 到 360 度,Pitch 和 Roll 只做微小偏移,范围在 -5 到 5 度之间。 Pitch 和 Roll 太大的话,树会像被风吹倒一样歪。另外,Billboard 草的 Yaw 随机很重要,否则一片草会齐刷刷朝同一个方向,像军训队列。

下面这段代码展示了我们项目中自定义 foliage 生成后处理的一个简化逻辑,用来根据到相机的距离再做一次密度衰减。它挂在编辑器脚本里,生成完成后执行一次。

import unreal

editor = unreal.EditorLevelLibrary()
foliage_actors = editor.get_all_level_actors()
camera = editor.get_editor_player_camera_manager()
cam_pos = camera.get_camera_location()

for actor in foliage_actors:
    if isinstance(actor, unreal.FoliageInstancedStaticMeshActor):
        for comp in actor.get_components_by_class(unreal.InstancedStaticMeshComponent):
            instances = comp.get_instance_count()
            to_remove = []
            for i in range(instances):
                pos = comp.get_instance_transform(i).translation
                dist = (pos - cam_pos).length()
                # 超过 150m 的实例按距离概率剔除
                if dist > 15000.0:
                    if (dist - 15000.0) / 30000.0 > unreal.random_unit_fraction():
                        to_remove.append(i)
            comp.remove_instances(to_remove)

这段脚本只有二十七行,但帮我们在一个开放世界原型里省掉了大约 18% 的远距离实例。实际项目里这段逻辑后来被挪到了 HLOD 和 Cull Distance Volume 里,运行时代价更小。

Landscape Grass Type:铺地被的最快路径

Procedural Foliage 擅长树、灌木、石头这类有体积、有遮挡、需要精确放置的对象。对于大面积低矮草本,比如草、苔藓、落叶层,用 Landscape Grass Type 更快。

Landscape Grass Type 是一种基于地形材质权重的自动铺草机制。你在地形材质里输出一张 Grass node,把不同草类型挂到不同 Landscape Layer。运行时引擎会根据相机距离和屏幕空间大小动态生成或销毁 Grass Instance。它的优势是极轻、极快、无需烘焙,而且只对视野内的区域生效。

我们项目里草的分层是这样的:

  • 近处(0 到 8 米):高密度手动 Foliage 或 Procedural Foliage 生成的细节草,有风动和碰撞。
  • 中处(8 到 40 米):Landscape Grass Type 自动铺的简模草,每平米实例数按相机距离递减。
  • 远处(40 米以上):地形材质本身的颜色变化和法线细节已经足够,不再单独生成草实例。

这样三层下来,玩家眼前草很密,但总体实例数只有全刷方案的三分之一。

UE5.8 里 Landscape Grass Type 新增了一个让我喜欢的功能:可以在 Grass Type 里指定 Exclude Slope 和 Exclude Height,而且支持读取 Landscape Layer 的 mask。这意味着你不需要再为草单独写 PCG Graph,也能做出沙丘顶部不长草、河床边缘长芦苇的效果。

下面这张图对比了 Procedural Foliage 和 Landscape Grass Type 在项目里的职责边界。

graph LR
    A["地形表面覆盖需求"] --> B{是否需要个体形态}
    B -->|"是,树/石/灌木"| C["Procedural Foliage"]
    B -->|"否,草/苔藓/落叶"| D["Landscape Grass Type"]
    C --> E["预生成实例,支持碰撞与 LOD"]
    D --> F["运行时动态生成,按距离剔除"]

与 PCG 的对比与互补

UE5 的 PCG(Procedural Content Generation)插件这几年进步很大。很多团队会问:既然有 PCG,为什么还要用 Procedural Foliage?我的回答是,两者不在同一层。

Procedural Foliage 是引擎内置的专门化系统,已经为植物放置做了很多预设:排斥半径、坡度过滤、高度过滤、图层过滤、 shade 抑制、随机缩放旋转。你几乎不用连线,调参数就能出效果。

PCG 是通用图节点系统,理论上什么都能做,但你要自己实现植物放置的常用逻辑。它的优势在复杂结构:沿道路种树、按样条线分布电线杆、在建筑物周围生成碎石、根据河流网络生成河岸植被带。

我们在项目里的分工是:

  • Procedural Foliage:自然区域的大面积植被基底。
  • Landscape Grass Type:低矮地被的动态填充。
  • PCG:规则结构、人工痕迹、关卡叙事相关的点缀,比如废弃公路两侧的野草、营地周围的轮胎和油桶、河岸边倒伏的枯木。

下面用序列图展示美术、程序和 PCG 节点一起工作时的典型流程。

sequenceDiagram
    participant TA as "技术美术小林"
    participant LD as "关卡阿杰"
    participant PFE as "Procedural Foliage"
    participant PCG as "PCG Graph"
    participant LAND as Landscape

    TA->>LAND: 确定地形 Layer 与生物群落分区
    LD->>PFE: 配置 Foliage Type 与 Spawner
    PFE->>LAND: 采样坡度、高度、Layer
    PFE-->>LD: 生成初步植被分布
    LD->>PCG: 传入道路、河流、建筑位置
    PCG->>LAND: 读取地形与几何信息
    PCG-->>LD: 输出结构型细节
    LD->>PFE: 手工修正视觉焦点区域
    TA->>PFE: 调整 LOD 与 Cull 参数

季节变化与 Data Layers

有次策划提需求:同一个关卡要同时做春、夏、秋、冬四个版本,给玩家赛季活动用。如果用传统做法,复制四个关卡文件,任何修改都要同步四次,维护成本直接爆炸。

UE5.8 的 Data Layers 救了这件事。 Data Layer 允许你在同一个关卡里维护多个数据层,每层可以独立加载、卸载、编辑、烘焙。我们把四季植被做成了四个 Data Layer:

  • DL_Spring:浅绿嫩叶、野花、湿润地表。
  • DL_Summer:浓绿冠层、茂盛杂草、干燥土路。
  • DL_Autumn:黄叶、落叶层、裸露树枝。
  • DL_Winter:积雪覆盖、常绿针叶、枯枝。

每个 Data Layer 里只放差异部分。比如秋季层里替换了一部分树冠材质、增加了地面落叶实例、把草地颜色调暗。共同的地形、岩石、建筑保持在主关卡里不动。

运行时通过 C++ 或蓝图切换 Data Layer 可见性。下面是一段简化蓝图逻辑,只在玩家打开赛季面板时切换一次,避免每帧判断。

void ASeasonSwitcher::SwitchToLayer(const FName& LayerName)
{
    UWorld* World = GetWorld();
    if (!World) return;

    AWorldDataLayers* DataLayers = World->GetWorldDataLayers();
    if (!DataLayers) return;

    const FActorDataLayer TargetLayer(LayerName);
    DataLayers->SetDataLayerRuntimeState(TargetLayer, EDataLayerRuntimeState::Activated);
}

Data Layer 切换时,植被实例是预生成好放在层里的,所以比运行时动态替换材质更稳。但它也有代价:四层意味着四份实例数据,包体体积会涨。我们的做法是只在重点活动区域做四季层,偏远背景共用夏季资源,通过材质变色简单模拟季节感。

性能优化:HLOD、LOD 与剔除

植被优化是个老话题,但在 UE5.8 里有些新东西值得注意。

LOD 是最基础的。每个 Foliage Type 可以设置四到五级 LOD,远距离用 billboard 或合并简模。关键不是 LOD 数量,而是切换距离。太近切换会看到明显 pop,太远切换浪费三角形。我们的经验是:第一级 LOD 覆盖 0 到 15 米,第二级 15 到 40 米,第三级 40 到 100 米,第四级 billboard 到 300 米。具体数值要根据摄像机高度和场景尺度微调。

HLOD(Hierarchical Level of Detail)对大面积森林很有效。它把远处的多棵实例合并成一张 impostor 或一个合并 mesh,大幅减少 draw call。 UE5.8 的 HLOD 支持 foliage 实例参与合并,但需要在 World Settings 里打开 HLOD 并生成集群。生成时间比较长,建议晚上跑。

剔除方面,Cull Distance Volume 和 foliage 自己的 Cull Distance 参数一起用。Cull Distance Volume 是关卡体积,可以按屏幕大小剔除实例。 foliage 的 Cull Distance 则是按距离硬切。两者结合:近处用屏幕大小判断,远处用距离硬切。

还有一个容易被忽略的参数:Cast Shadow。远景植被完全可以关掉动态阴影,靠级联阴影贴图的远距离部分或者烘焙的静态阴影来补。我们在低端机上把 80 米外的 foliage 阴影全关,帧率提了 6 到 8 帧。

风动也是性能热点。简单草可以用 World Position Offset 做正弦波动,复杂树需要 Pivot Painter 或 Niagara 风吹。风动 shader 要尽量早做 early-z,避免 overdraw 爆炸。我们对远景 billboard 直接关闭风动,反正玩家也看不出。

照片参考与工作流

前面提到的沙漠峡谷项目,我们收集参考照片的方式有点讲究。不会随便去 Pinterest 抓几张好看的图,会按生物群落区分类建立相册:

  • 远景天际线:看树冠轮廓和林缘线。
  • 中景群落:看植物搭配比例,几棵树配多少灌木。
  • 近景地面:看地被层次,草、落叶、石块、裸露土壤的混合方式。
  • 特殊地貌:河床、岩石背风面、沙丘坡脚等例外位置。
  • 季节变化:同一场景春夏秋冬四组照片,用于 Data Layer 设计。

每张照片旁边会写几条笔记,比如南坡云杉密度比北坡低 40%,白桦多长在林缘和倒木附近。这些笔记最后会变成 Foliage Type 里的具体数字。

工作流上,我们是这样反复调整的:

  1. 地形确定后,先画一张生物群落分布图,用不同颜色标出森林、草原、沙漠、湿地。
  2. 每个生物群落区建一个 Foliage Spawner,挂上对应的 Foliage Type。
  3. 按参考照片调 Placement 参数,先生成一版,截图跟照片对比。
  4. 对比差异,调整密度、尺寸、坡度范围,再生成。
  5. 关键构图区域用手工 Foliage 修正。
  6. 跑性能测试,根据帧率和 overdraw 优化 LOD、阴影、剔除。

这个过程通常会重复三到五轮。程序化植被的好处在于规则确定后,可以快速把全局效果再打磨一轮。

几个容易踩的坑

说几个我实际遇到过的问题。

第一个坑是 Layer 权重精度。地形材质某个 Layer 权重看起来是 0.3,但 Procedural Foliage 采样时可能因为压缩或混合得到 0.28 或 0.32。如果你的 Weight Range 设成 0.3 到 1.0,就会看到边界上实例忽有忽无。解决办法是设成 0.25 到 1.0,留一点容差。

第二个坑是 Seed Density 单位。它表示的是每平方米尝试播种的数量,不是最终实例数。很多人会把它设得很高,以为这样能出更密的效果,结果大量种子在碰撞检测里被剔除,反而拖慢生成速度。有效做法是先设一个中等值,再调 Collision Radius 和 Shade Radius。

第三个坑是 Z Offset。植物根部如果没有贴合地形,会悬空或插进地里。大多数 Foliage Type 默认有自动贴合,但如果你的网格原点不在根部,或者地形有剧烈凹凸,就会出现树根悬空的情况。检查方法是开线框模式拉远看看脚底。

第四个坑是生成后再改 Landscape。地形一调,之前生成的实例可能全飞到天上或埋进地下。 UE5.8 里可以右键 Spawner 选择 Resimulate,但重算范围大的话要等很久。建议地形大体稳定后再大面积生成植被。

写在最后

程序化植被这件事,说到底是在用规则替代手工。规则可以来自照片、来自群落学知识、来自团队对场景气氛的判断。 UE5.8 的 Procedural Foliage 编辑器把这些规则可视化、参数化,让美术能调,让程序能控。

阿杰那个草原关卡最后没有删掉三十七万棵草。他只是把草原拆成三层:近景手工细节、中景程序化草、远景用地形材质带过。规则写好后,整个草原只需要维护十几个参数,赛季活动换 Data Layer 就能切季节。帧率稳定在了 58 帧,美术组也不用再一棵棵数草了。

如果你刚开始接触这套系统,我建议先做一个最小生物群落区:比如一片五十米乘五十米的沙丘,只放一种草和一种仙人掌。把坡度、高度、Layer 三个参数摸清楚,再逐步加入树、灌木、落叶。植被的复杂度是一点一点加上去的。规则清楚之后,沙漠和森林都会自己生长。