【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
實現(xiàn)雪地足跡的基本思路:
為足跡創(chuàng)建渲染目標,這是一個灰度遮罩,其中白色代表著足跡,黑色代表非足跡。
將渲染目標投影到地面,然后用它來混合紋理以及置換頂點,使得雪地有下陷效果。
但是,需要一種方法找到那些可影響雪的物體,可以先把對象渲染到自定義深度(Custom Depth)緩沖區(qū),再使用一個帶有后期處理材質的 SceneCapture 對所有渲染到自定義深度的對象進行遮罩,然后再把遮罩輸出給渲染目標。
SceneCapture 的關鍵是它放置的位置。假如,是頂向下視角記錄場景并輸出給渲染目標,那么對于球體,它只是底部著地,但是頂視角會認為整個球體投身到地址的部分都是被遮罩的區(qū)域。
所以,SceneCaptue 攝像機應該從底部捕捉場景,也就是接觸地面的角度拍攝。要判斷物體是否接觸地面,使用后期處理材質進行深度檢測。這個檢測會告訴我們物體是否高于地面深度且低于指定的偏移量。如果這兩個條件均為 True,我們就可以為這一像素遮罩。
要進行深度檢測,我們需要兩個深度緩沖區(qū)。一個針對地面;另一個針對影響雪的物體。因為 Scene capture僅能看到地面,場景深度(Scene Depth)緩沖區(qū) 將會輸出地面的深度。要獲取物體的深度,我們只需要將它們渲染到 自定義深度(Custom Depth)緩沖區(qū)
示范項目基于 UE4 w?ThirdPerson Shooter 模板,互動雪地功能基本邏輯:
創(chuàng)建一個 Actor 藍圖,命名為 *RenderTarget_BP*,添加并使用 SceneCaptureComponent2D 組件來捕捉三維場景中的信息。
創(chuàng)建一個 Material Parameters Collection 命名為 *SnowMPC*,用于向雪地材質*Snow_Mat*傳遞玩家坐標,使用`SetVectorParameterValue`方法。
在 BeginPlay 事件中,通過 `SetScalarParameterValue` 將 SceneCaptureComponent2D 組件的 OrthoWidth 投影寬度寫入 *SnowMPC*。
與玩家坐標綁定,在 Tick 事件中,使用 `GetPlayerCharacter` 和 `GetActorLocation` 獲取玩家的坐標位置,并使用 `AddActorWorldOffset` 方法更新到 RenderTarget_BP 本身的坐標。
創(chuàng)建兩個 RenderTarget 用來存儲玩家的足跡,一個命名為 *RenderTarget*,另一個命名為 *RenderTargetPersistant*。
創(chuàng)建一個材質命名為 *DrawToPersistant* 用來裝入 *RenderTarget* 材質采樣。
再利用 UPrimitiveComponent 提供的 `CreateDynamicMaterialInstance` 方法創(chuàng)建動態(tài)材質,通過 `DrawMaterialToRenderTarget` 將其繪制到 *RenderTargetPersistant*。
創(chuàng)建一個 WidgetBlueprint 用來在屏幕上顯示玩家足跡,只需要添加一個 Image 組件,設置 Brush -> Texture 為 *RenderTarget*。
雪地材質*Snow_Mat*只是簡單地使用白色模擬雪地,使用深棕色模擬土地,它們通過 LinearInterpolate 進行線性插值,Alpha 輸入的插值控制來自 *RenderTarget* 記錄的足跡數(shù)據(jù),并經(jīng)過自定義材質函數(shù)*MaskUV*處理。
使用 WorldPosition 獲取雪地中像素坐標,并與玩家當前的坐標進行差值處理。
在 ThirdPersonCharacter 角色的 BeginPlay 事件,使用 `Create Widget` 創(chuàng)建其實例,再 `AddToViewport` 添加到場景中。
工程中使用了三個材質,材質混合模式及著色模式如下:
*Snow_Mat* 雪地材質,Surface 材質域,Opaque + Default Lit 模式,Two Sided;
*DrawToPersistant* 繪圖用的材質,Surface 材質域,Opaque + Default Lit 模式,用于繪制 RenderTarget 的材質;
*Mat_Depth* Post Process 材質域,使用*SceneTexture*材質表達式的 CustomDepth 和 ScreenDepth 紋理,用來測量渲染深度;


后期處理材質需要指定材質域為 Post Process,并且,后期處理材質只能使用自發(fā)光顏色(Emissive Color)輸出新顏色。此外,可以定義在后期處理過程中應在何處應用此通道(Blendable Location),如果有多個通道,則應按什么順序處理(Blendable Priority)。
*RenderTarget_BP* 內部的 *SceneCaptureComponent2D* 組件需要使用深度測試材質 *Mat_Depth*,其邏輯說明:
使用 *SceneTexture* 材質表達式的 CustomDepth 和 ScreenDepth 紋理的 R 分量,求其差值,并除以 25。
即如果某像素距地面 25 個單位以內,那么它將被遮罩。遮罩的強度(masking intensity)依賴于像素和地面的距離。
?再通過 *Saturate* 節(jié)點范圍到 0 ~ 1 的范圍,并使用 *OneMinus* 求反值。
將上面的標題值和 RenderTargetPersistent 的 RGB 矢量相加,這一步混合了舊的足跡數(shù)據(jù)。
將結果輸出到 Emissive Color,這作為后期處理材質的專用通道。
添加對象到場景時,默認會開啟 Rendering -> Render CustomDepth Pass。這個單獨的特性通過將某些對象渲染到另一個深度緩沖區(qū)(CustomDepth)來屏蔽它們。 這增加了額外的繪制調用,但不使用更多材質。渲染相當便宜,因為我們只輸出深度。
材質參數(shù)集合 SnowMPC 傳遞了兩個參數(shù):
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 的數(shù)組設置中,添加 *Mat_Depth*;
將 *RenderTarget_BP* 添加到場景內,并設置:
Location? -> (0, 0, -2000)
將 *RenderTarget_BP* 添加到場景內,并設置 Location 為 (0, 0, -2000),目的是將 SceneCapture 攝像機放到地面以下,使用底視角來捕捉做深度測試的數(shù)據(jù)。這些數(shù)據(jù)為保存到 Scene Capture 設置的 Texture Target 對象上。
兩個 RenderTarget 對象的數(shù)據(jù)流向如下,主要是通過 RenderTarget_BP 各個方法處理:
*RenderTarget* -> DrawToPersistant -> *RenderTargetPersistent* -> Mat_Depth -> RenderTarget_BP -> *RenderTarget*。
如果不通過 *RenderTargetPersistent* 使足跡持久化,那么每次心跳事件中,新的足跡都會覆蓋掉 *RenderTarget* 原有的足跡。所以,需要一個渲染目標作為持久化緩存(persistent buffer),在足跡被覆蓋之前把它存儲到持久化緩存中。然后再通過 *Mat_Depth* 把持久化緩存返回給 SceneCapture,所以再次捕捉到的深度信息就會包含持久化的足跡。這樣我們就得到了兩個渲染目標互相寫來寫去的循環(huán)結構,它就是實現(xiàn)持久化的方法。
因為渲染目標也是消耗內存,分辨率要盡可能低,以充分利用內存空間。使用 *PixelWorldSize* 表示一個像素對應多大的實際面積。
對于雪地上的足跡,不需要那么多的細節(jié),使用不需要 1:1 的比例。筆者建議使用更大的比例,這樣就能把低分辨率的渲染目標用到更大的捕獲區(qū)域上。注意,也不要用太大的比例,那樣會損失細節(jié)。本例中,使用 8:1 的比例,即一個像素對應 8×8 的世界單位。注意,渲染目標默認的分辨率是256×256。通過 Scene Capture -> Ortho Width 指定捕獲區(qū)域大小,在 Perspective 投影方式下無效。比如,想捕獲 1024×1024 的區(qū)域,因為使用的比例是 8:1,所以將它設為 2048(256×8)。
地面使用的材質,即 *Snow_Mat* 也需要訪問捕獲區(qū)域的大小從而準確投影渲染目標。一種簡單的方式是將捕獲區(qū)域的大小存儲在材質變量集(Material Parameter Collection),這是因為任何材質都可以訪問變量集。

雪地材質細節(jié)面板中的設置說明:
啟用 Two Sided 是因為 SceneCapture 是自底向上看地面,即需要看到它的背面。默認情況下,引擎并不渲染背面,這樣也就無法把地面深度存儲到深度緩沖區(qū)。因此我們要讓引擎對地面進行雙面渲染。
設置 Tessellation -> D3D11 Tessellation 為 Flat Tessellation(使用 PN Triangles 也可以)。 Tessellation 會把網(wǎng)格的三角面分解成更小的三角面。從而有效提高網(wǎng)格分辨率,是我們在頂點置換時獲得更多的細節(jié)。否則,頂點密度過小很難生成生動的足跡。Tessellation 即內嵌,在三角面中分割嵌套小的三角面,提升模型細節(jié)。
開啟 Tessellation 后,材質的 World Displacement 和 Tessellation Multiplier 通道也會被啟用。后者控制內嵌三角面的數(shù)量,本例中不對該通道連接,這意味著使用其默認值 1。
World Displacement 通道獲取輸入的向量值,根據(jù)向量的方向和大小移動頂點。要計算這個引腳的值,我們先得把渲染目標投影到地面。
引擎升級到 UE5EA 引入 Nanite 虛擬微多邊形幾何體,可以處理上億級別的多邊形,可以使用 Photogrammetry 的掃描數(shù)據(jù)和 CAD 數(shù)據(jù),并且無需制作 LOD 模型。原先的 Tessellation 已經(jīng)被移除,可以使用 Virtual HeightField Mesh 這樣的替代技術。使用 Modeling Tools Editor Mode 建模插件解決靜態(tài)的置換問題,其工具面板 Deform -> Displace 提供了貼圖置換功能。
*Snow_Mat* 材質中,使用 *WorldPosition* 獲取雪地中像素坐標,并計算與玩家當前的坐標差值。因為 SceneCapture 組件放置在地下向上拍攝的視角,所以獲取到的像素坐標需要進行反轉以匹配 RenderTarget 的像素坐標。注意,攝像機的畫面坐標是 X 豎直向上,Y 軸水平向左,朝向 Z 軸正方向,這各視圖的坐標系統(tǒng)是一致的。但是玩家的角度是頂視圖,所以頂?shù)追D方向后,X 軸翻轉 180°。
翻轉后的坐標下步處理就是將以世界中心坐標的 (0, 0) 像素坐標轉換到 UV 坐標系,按渲染目標的尺寸居中,這就需要將坐標和 ShowMVC 接收到 Size 的一半相加。在使用 *MaskUV* 自定義材質函數(shù)過濾前,
這里面對齊 UV 坐標是比較麻煩的部分,因為 RenderTarget?尺寸有限,只能用來顯示玩家當前的活動區(qū)域的足跡。當玩家活動范圍超過其尺寸,就以最后可覆蓋的區(qū)域為準,截斷超出范圍的部分。這樣就需要對紋理的 UV 坐標進行動態(tài)更新,以和玩家的位置一致。工程中,還使用了自定義了材質函數(shù)*MaskUV*,用來,其內部使用了 *Custom* 編寫自定義材質表達,使用 *FunctionInput* 接收一個向量輸入:
if (UV.x < 0 || UV.x > 1 || UV.y < 0 || UV.y > 1)
{
? ? return 0;
}
return 1;



*RenderTarget_BP* 心跳事件執(zhí)行 設置動態(tài)材質輸入?yún)?shù)并繪制到RenderTargetPersistant

在*RenderTarget_BP*同定義的 *MoveRT* 作用就是用來將玩家的足跡活動轉換到 RenderTarget 尺寸范圍。
其內部還定義了一個藍圖宏,*SnapPixelToGrid*,主要是使用數(shù)學表達式 Math Expression,將輸入的玩家坐標 (PlayerX, PlayerY) 轉換為規(guī)范值,以實現(xiàn)附著:
? ? ((vector((floor((PlayerX / PixelWorldSize))), (floor((PlayerY / PixelWorldSize))), 0)) + 0.500000) * PixelWorldSize
輸入?yún)?shù) PixelWorldSize 就是將 SceneCaptureComponent2D 組件的 OrthoWidth 投影寬度細分為 256 格子,坐標就附著在這些格子的中間。
處理后的坐標會傳遞到材質參數(shù)集體,以供*Snow_Mat*材質使用,并使用 `AddActorWorldOffset` 方法更新 RenderTarget_BP 本身的坐標。

創(chuàng)建一個 WidgetBlueprint 用來在屏幕上顯示玩家足跡,只需要添加一個 Image 組件,設置 Brush -> Texture 為 *RenderTarget*。
