【UE5】Reactive Snow 互动雪地
## ⚡ Reactive Snow 互动雪地
可互动雪地材质 https://www.bilibili.com/video/BV1wt411m7L7
Creating Snow Trails in UE 4 https://www.raywenderlich.com/5760-creating-snow-trails-in-unreal-engine-4
Creating Interactive Grass in UE4 https://www.raywenderlich.com/6314-creating-interactive-grass-in-unreal-engine-4
实现雪地足迹的基本思路:
为足迹创建渲染目标,这是一个灰度遮罩,其中白色代表着足迹,黑色代表非足迹。
将渲染目标投影到地面,然后用它来混合纹理以及置换顶点,使得雪地有下陷效果。
但是,需要一种方法找到那些可影响雪的物体,可以先把对象渲染到自定义深度(Custom Depth)缓冲区,再使用一个带有后期处理材质的 SceneCapture 对所有渲染到自定义深度的对象进行遮罩,然后再把遮罩输出给渲染目标。
SceneCapture 的关键是它放置的位置。假如,是顶向下视角记录场景并输出给渲染目标,那么对于球体,它只是底部着地,但是顶视角会认为整个球体投身到地址的部分都是被遮罩的区域。
所以,SceneCaptue 摄像机应该从底部捕捉场景,也就是接触地面的角度拍摄。要判断物体是否接触地面,使用后期处理材质进行深度检测。这个检测会告诉我们物体是否高于地面深度且低于指定的偏移量。如果这两个条件均为 True,我们就可以为这一像素遮罩。
要进行深度检测,我们需要两个深度缓冲区。一个针对地面;另一个针对影响雪的物体。因为 Scene capture仅能看到地面,场景深度(Scene Depth)缓冲区 将会输出地面的深度。要获取物体的深度,我们只需要将它们渲染到 自定义深度(Custom Depth)缓冲区
示范项目基于 UE4 w ThirdPerson Shooter 模板,互动雪地功能基本逻辑:
创建一个 Actor 蓝图,命名为 *RenderTarget_BP*,添加并使用 SceneCaptureComponent2D 组件来捕捉三维场景中的信息。
创建一个 Material Parameters Collection 命名为 *SnowMPC*,用于向雪地材质*Snow_Mat*传递玩家坐标,使用`SetVectorParameterValue`方法。
在 BeginPlay 事件中,通过 `SetScalarParameterValue` 将 SceneCaptureComponent2D 组件的 OrthoWidth 投影宽度写入 *SnowMPC*。
与玩家坐标绑定,在 Tick 事件中,使用 `GetPlayerCharacter` 和 `GetActorLocation` 获取玩家的坐标位置,并使用 `AddActorWorldOffset` 方法更新到 RenderTarget_BP 本身的坐标。
创建两个 RenderTarget 用来存储玩家的足迹,一个命名为 *RenderTarget*,另一个命名为 *RenderTargetPersistant*。
创建一个材质命名为 *DrawToPersistant* 用来装入 *RenderTarget* 材质采样。
再利用 UPrimitiveComponent 提供的 `CreateDynamicMaterialInstance` 方法创建动态材质,通过 `DrawMaterialToRenderTarget` 将其绘制到 *RenderTargetPersistant*。
创建一个 WidgetBlueprint 用来在屏幕上显示玩家足迹,只需要添加一个 Image 组件,设置 Brush -> Texture 为 *RenderTarget*。
雪地材质*Snow_Mat*只是简单地使用白色模拟雪地,使用深棕色模拟土地,它们通过 LinearInterpolate 进行线性插值,Alpha 输入的插值控制来自 *RenderTarget* 记录的足迹数据,并经过自定义材质函数*MaskUV*处理。
使用 WorldPosition 获取雪地中像素坐标,并与玩家当前的坐标进行差值处理。
在 ThirdPersonCharacter 角色的 BeginPlay 事件,使用 `Create Widget` 创建其实例,再 `AddToViewport` 添加到场景中。
工程中使用了三个材质,材质混合模式及着色模式如下:
*Snow_Mat* 雪地材质,Surface 材质域,Opaque + Default Lit 模式,Two Sided;
*DrawToPersistant* 绘图用的材质,Surface 材质域,Opaque + Default Lit 模式,用于绘制 RenderTarget 的材质;
*Mat_Depth* Post Process 材质域,使用*SceneTexture*材质表达式的 CustomDepth 和 ScreenDepth 纹理,用来测量渲染深度;


后期处理材质需要指定材质域为 Post Process,并且,后期处理材质只能使用自发光颜色(Emissive Color)输出新颜色。此外,可以定义在后期处理过程中应在何处应用此通道(Blendable Location),如果有多个通道,则应按什么顺序处理(Blendable Priority)。
*RenderTarget_BP* 内部的 *SceneCaptureComponent2D* 组件需要使用深度测试材质 *Mat_Depth*,其逻辑说明:
使用 *SceneTexture* 材质表达式的 CustomDepth 和 ScreenDepth 纹理的 R 分量,求其差值,并除以 25。
即如果某像素距地面 25 个单位以内,那么它将被遮罩。遮罩的强度(masking intensity)依赖于像素和地面的距离。
再通过 *Saturate* 节点范围到 0 ~ 1 的范围,并使用 *OneMinus* 求反值。
将上面的标题值和 RenderTargetPersistent 的 RGB 矢量相加,这一步混合了旧的足迹数据。
将结果输出到 Emissive Color,这作为后期处理材质的专用通道。
添加对象到场景时,默认会开启 Rendering -> Render CustomDepth Pass。这个单独的特性通过将某些对象渲染到另一个深度缓冲区(CustomDepth)来屏蔽它们。 这增加了额外的绘制调用,但不使用更多材质。渲染相当便宜,因为我们只输出深度。
材质参数集合 SnowMPC 传递了两个参数:
SceneCaptureComponent2D 组件的 OrthoWidth 投影宽度,标量。
RenderTarget_BP 对象的 Actor Location,四三维矢量。
设置 *RenderTarget_BP* 内部的 SceneCaptureComponent2D 组件:
Projection 部分:
Projection Type -> Orthographic 正交投影
OrthoWidth -> 2048 像素的矩形(大概覆盖20m范围)
Scene Capture 部分:
Texture Target -> *RenderTarget*
Capture Source -> Final Color (LDR) in RGB
Post Process Volume -> Rendering Features 部分:
Post Process Materials 的数组设置中,添加 *Mat_Depth*;
将 *RenderTarget_BP* 添加到场景内,并设置:
Location -> (0, 0, -2000)
将 *RenderTarget_BP* 添加到场景内,并设置 Location 为 (0, 0, -2000),目的是将 SceneCapture 摄像机放到地面以下,使用底视角来捕捉做深度测试的数据。这些数据为保存到 Scene Capture 设置的 Texture Target 对象上。
两个 RenderTarget 对象的数据流向如下,主要是通过 RenderTarget_BP 各个方法处理:
*RenderTarget* -> DrawToPersistant -> *RenderTargetPersistent* -> Mat_Depth -> RenderTarget_BP -> *RenderTarget*。
如果不通过 *RenderTargetPersistent* 使足迹持久化,那么每次心跳事件中,新的足迹都会覆盖掉 *RenderTarget* 原有的足迹。所以,需要一个渲染目标作为持久化缓存(persistent buffer),在足迹被覆盖之前把它存储到持久化缓存中。然后再通过 *Mat_Depth* 把持久化缓存返回给 SceneCapture,所以再次捕捉到的深度信息就会包含持久化的足迹。这样我们就得到了两个渲染目标互相写来写去的循环结构,它就是实现持久化的方法。
因为渲染目标也是消耗内存,分辨率要尽可能低,以充分利用内存空间。使用 *PixelWorldSize* 表示一个像素对应多大的实际面积。
对于雪地上的足迹,不需要那么多的细节,使用不需要 1:1 的比例。笔者建议使用更大的比例,这样就能把低分辨率的渲染目标用到更大的捕获区域上。注意,也不要用太大的比例,那样会损失细节。本例中,使用 8:1 的比例,即一个像素对应 8×8 的世界单位。注意,渲染目标默认的分辨率是256×256。通过 Scene Capture -> Ortho Width 指定捕获区域大小,在 Perspective 投影方式下无效。比如,想捕获 1024×1024 的区域,因为使用的比例是 8:1,所以将它设为 2048(256×8)。
地面使用的材质,即 *Snow_Mat* 也需要访问捕获区域的大小从而准确投影渲染目标。一种简单的方式是将捕获区域的大小存储在材质变量集(Material Parameter Collection),这是因为任何材质都可以访问变量集。

雪地材质细节面板中的设置说明:
启用 Two Sided 是因为 SceneCapture 是自底向上看地面,即需要看到它的背面。默认情况下,引擎并不渲染背面,这样也就无法把地面深度存储到深度缓冲区。因此我们要让引擎对地面进行双面渲染。
设置 Tessellation -> D3D11 Tessellation 为 Flat Tessellation(使用 PN Triangles 也可以)。 Tessellation 会把网格的三角面分解成更小的三角面。从而有效提高网格分辨率,是我们在顶点置换时获得更多的细节。否则,顶点密度过小很难生成生动的足迹。Tessellation 即内嵌,在三角面中分割嵌套小的三角面,提升模型细节。
开启 Tessellation 后,材质的 World Displacement 和 Tessellation Multiplier 通道也会被启用。后者控制内嵌三角面的数量,本例中不对该通道连接,这意味着使用其默认值 1。
World Displacement 通道获取输入的向量值,根据向量的方向和大小移动顶点。要计算这个引脚的值,我们先得把渲染目标投影到地面。
引擎升级到 UE5EA 引入 Nanite 虚拟微多边形几何体,可以处理上亿级别的多边形,可以使用 Photogrammetry 的扫描数据和 CAD 数据,并且无需制作 LOD 模型。原先的 Tessellation 已经被移除,可以使用 Virtual HeightField Mesh 这样的替代技术。使用 Modeling Tools Editor Mode 建模插件解决静态的置换问题,其工具面板 Deform -> Displace 提供了贴图置换功能。
*Snow_Mat* 材质中,使用 *WorldPosition* 获取雪地中像素坐标,并计算与玩家当前的坐标差值。因为 SceneCapture 组件放置在地下向上拍摄的视角,所以获取到的像素坐标需要进行反转以匹配 RenderTarget 的像素坐标。注意,摄像机的画面坐标是 X 竖直向上,Y 轴水平向左,朝向 Z 轴正方向,这各视图的坐标系统是一致的。但是玩家的角度是顶视图,所以顶底翻转方向后,X 轴翻转 180°。
翻转后的坐标下步处理就是将以世界中心坐标的 (0, 0) 像素坐标转换到 UV 坐标系,按渲染目标的尺寸居中,这就需要将坐标和 ShowMVC 接收到 Size 的一半相加。在使用 *MaskUV* 自定义材质函数过滤前,
这里面对齐 UV 坐标是比较麻烦的部分,因为 RenderTarget 尺寸有限,只能用来显示玩家当前的活动区域的足迹。当玩家活动范围超过其尺寸,就以最后可覆盖的区域为准,截断超出范围的部分。这样就需要对纹理的 UV 坐标进行动态更新,以和玩家的位置一致。工程中,还使用了自定义了材质函数*MaskUV*,用来,其内部使用了 *Custom* 编写自定义材质表达,使用 *FunctionInput* 接收一个向量输入:
if (UV.x < 0 || UV.x > 1 || UV.y < 0 || UV.y > 1)
{
return 0;
}
return 1;



*RenderTarget_BP* 心跳事件执行 设置动态材质输入参数并绘制到RenderTargetPersistant

在*RenderTarget_BP*同定义的 *MoveRT* 作用就是用来将玩家的足迹活动转换到 RenderTarget 尺寸范围。
其内部还定义了一个蓝图宏,*SnapPixelToGrid*,主要是使用数学表达式 Math Expression,将输入的玩家坐标 (PlayerX, PlayerY) 转换为规范值,以实现附着:
((vector((floor((PlayerX / PixelWorldSize))), (floor((PlayerY / PixelWorldSize))), 0)) + 0.500000) * PixelWorldSize
输入参数 PixelWorldSize 就是将 SceneCaptureComponent2D 组件的 OrthoWidth 投影宽度细分为 256 格子,坐标就附着在这些格子的中间。
处理后的坐标会传递到材质参数集体,以供*Snow_Mat*材质使用,并使用 `AddActorWorldOffset` 方法更新 RenderTarget_BP 本身的坐标。

创建一个 WidgetBlueprint 用来在屏幕上显示玩家足迹,只需要添加一个 Image 组件,设置 Brush -> Texture 为 *RenderTarget*。
