IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> 学习制作UE的地形蓝图笔刷(Landmass) -> 正文阅读

[游戏开发]学习制作UE的地形蓝图笔刷(Landmass)

目标

在之前的博客《试用UE4新的地形编辑功能:地形编辑层与Landmass中的地形蓝图笔刷》中,我简单地尝试了地形蓝图笔刷,但当时对其内部机制并不了解。本篇将尝试制作两个最简单的蓝图笔刷,借此学习其内部机制。

此外,“Landmass” 本身是一个插件的名字,严格意义上并不等同于“地形蓝图笔刷”,然而很多资料似乎并没有明确区分二者,令我产生了些困扰,因此我希望先梳理清楚相关的概念。

Building Worlds with Landmass | Unreal Engine中作者展示了地形蓝图笔刷的效果,也简单介绍了如何制作地形蓝图笔刷,是本篇的主要参考资料。不过,为了真正搞清楚如何一步步自己创建蓝图笔刷,我还花了不少时间研究Landmass自带的两个地形蓝图笔刷的内部节点。

梳理相关概念

地形编辑层、地形蓝图笔刷、Landmass插件 都是 4.24 新添加的功能,可见Unreal Engine 4.24 Release Notes

地形编辑层(Landscape Edit Layers)

地形编辑层(Landscape Edit Layers)的概念类似于“图层”,使得编辑操作可以放在多层上而不是“破坏性”地在同一层上编辑。
功能启用后,可以看到堆叠的层:
在这里插入图片描述

一个地形编辑层对应的C++对象是FLandscapeLayer(完整定义见附录),定义它的代码是 \Engine\Source\Runtime\Landscape\Classes\Landscape.h。因此这个功能是在引擎本身中的,不需要启用Landmass插件。

(我在《简单尝试UE的地形编辑层(Landscape Edit Layers)功能》也做了些测试。)

地形蓝图笔刷(Landscape Blueprint Brushes)

Landscape Blueprint Brushes enable you to create and manipulate arbitrary terrain regions using shapes defined entirely in Blueprint. You can add multiple overlapping brushes and the system composites them together to display the final result.
Landscape Blueprint Brushes consist of a 2D spline shape and a collection of properties enabling you to specify Materials, meshes, falloff, and more. You can also apply effects such as blurring, noise, and curves. Optionally, you can inject heightmap and layer weight data into a brush by overriding its Render event.
借助地形蓝图笔刷,用户可以使用完全在蓝图中定义的形状来创建和操纵任意地形区域。用户可以添加多个层叠的笔刷,系统将把它们组合在一起来显示最终效果。
地形蓝图笔刷由一个2D样条形状和一系列属性组成,让用户能够对材质、网格、衰减等进行指定。用户还能应用模糊、噪点和曲线之类的效果。此外,还可以覆写笔刷的渲染事件,为其插入高度图层权重数据。

“地形蓝图笔刷” 是依赖于 “地形编辑层” 功能的。
在启用 “地形编辑层” 后,就可以看到在地形的 Sculpt 分页下有 “Blueprint” 的选项:
在这里插入图片描述

在 TOOL SETTINGS 中可以选择一个地形蓝图笔刷的种类,随后点击地形即可创建。
在这里插入图片描述

地形蓝图笔刷所对应的C++类是ALandscapeBlueprintBrushBase。定义它的代码是 Engine\Source\Runtime\Landscape\Public\LandscapeBlueprintBrushBase.h

每一个地形蓝图笔刷实例都会属于一个地形编辑层(FLandscapeLayer有 Brushes 成员,而每个Brush会引用一个ALandscapeBlueprintBrushBase对象,详见附录)。当前所选层的所有地形蓝图笔刷将会按顺序排列显示:(越下则计算越靠后)
在这里插入图片描述

“地形蓝图笔刷” 也是引擎本身的概念,不需要启用Landmass插件就可以看到。只不过,在不启用Landmass情况下,默认是没有可供选择的笔刷种类的:
在这里插入图片描述

此时你只能继承LandscapeBlueprintBrush来从零创建一个地形蓝图笔刷:
在这里插入图片描述

Landmass插件

Landmass 是一个 Built-In 插件:
在这里插入图片描述
其包含了 C++部分Content部分(即uasset部分)。

对于C++部分,内容很少,只是:

  • 定义了ALandmassActor(继承自AActor)。
  • 扩展了一个ULandmassBlueprintFunctionLibrary::GetCursorWorldRay函数,内容很简单。
  • TerrainCarvingSettings.hFalloffSettings.hBrushEffectsList.h中定义了一些结构体,但它们只是充当 UPROPERTY 容器的作用,自身没有逻辑。

重点是Content部分有较多的内容。比如现在比较关注的几个继承自ALandscapeBlueprintBrushBase的地形蓝图笔刷:
在这里插入图片描述

LandmassActor

ALandmassActor是定义在Landmass插件中的C++类,继承自AActor
它本身内容不多,但它是重要的LandmassBrushManager的基类。

LandmassBrushManager

它继承自LandmassActor,是一个蓝图类,在Landmass插件中.
在这里插入图片描述
它提供了一些制作地形蓝图笔刷必备的内容,比如高度图和权重图的RT:
在这里插入图片描述

制作第一个地形蓝图笔刷(功能最简化)

这里主要参考了Building Worlds with Landmass | Unreal Engine 中的 14:52 ~ 19:22。我会从零实现一个类似CustomBrush_MaterialOnly的蓝图笔刷,但是内容已经简化到了最简程度。其基本思路也在教程中有示意图,核心是使用材质来渲染地形纹理:
在这里插入图片描述

0. 创建蓝图

继承自ALandscapeBlueprintBrushBase
在这里插入图片描述
我将其命名为 MyTestLandBP
Affect Heightmap勾选,代表此蓝图会影响高度。
在这里插入图片描述

然后,创建三个变量:
在这里插入图片描述

  • BrushManager指向当前所使用的 LandmassBrushManager。(需要将其设为Public,即右侧睁眼,因为我这里实验发现私有情况下会出现丢失它的问题)
  • BrushMaterial 指向准备使用的材质。
  • DynamicMaterial 代表用来实际执行渲染的动态材质。

1. 初始化BrushManager

初始化阶段,需要创建一个,或者更新已存在的。

这方面的逻辑,可以拷贝CustomBrush_MaterialOnly的宏Spawn or Update Manager。我将这个宏拷贝到我的 MyTestLandBP中,并添加了些注释:
在这里插入图片描述

然后,在 EventGraph中,添加Initialize这个事件,并将其连入Spawn or Update Manager。现在,触发Initialize后将会调用Spawn or Update Manager
在这里插入图片描述

2. 准备材质

所使用的材质如下:
在这里插入图片描述

其大体逻辑很简单,就是为高度图随便叠加了一个噪声纹理(这里我选择了/Engine/MaterialTemplates/Textures/T_Noise01),然后其高度值缩放了3000。
不过需要注意的是对高度图纹理执行的Unpack以及其反操作Pack。这是因为高度图的值是16位的,占用了RG两个通道。

然后,将这个材质作为BrushMaterial的默认值。
在这里插入图片描述

3. 添加宏:创建动态材质

由于材质中的 HeightRT,即高度图的RenderTarget是需要动态赋值的,因此需要创建一个动态材质。再次,可以拷贝CustomBrush_MaterialOnly的宏Create MID if Needed,它的逻辑就是如果没有的话则重新创建一个:
在这里插入图片描述

5. 覆写Render函数

基类ALandscapeBlueprintBrushBaseRender函数定义如下:

UFUNCTION(BlueprintNativeEvent)
UTextureRenderTarget2D* Render(bool InIsHeightmap, UTextureRenderTarget2D* InCombinedResult, const FName& InWeightmapLayerName);

它如何被调用并不是本篇学习的内容。这里只需要明确,使用材质渲染RT的逻辑需要放在这里就可以了。

因此,在蓝图中覆写Render函数:
在这里插入图片描述

它有一个RT作为参数,还返回一个RT。但是这两个RT并不是一个RT。参数的RT将作为材质的参数传入,而返回的RT是BrushManager上的RT。连接的蓝图如下:
在这里插入图片描述


编译蓝图后可以做测试了。依照一样的流程,选择MyTestLandBP并点击地面,可以看到地形被修改,其图案就是叠加的噪声图案:
在这里插入图片描述

现在,最简化的流程已经做完。材质方面之后可以添加一些参数进行控制。

制作第二个地形蓝图笔刷(使用距离场指定范围)

第二个笔刷将在之前的基础上,使用Spline指定一个范围,生成距离场来进行后续的计算。原视频在 19:22~26:1 做了介绍,不过要想知道详细的原理主要还是需要研究CustomBrush_Landmass这个Landmass插件自带的蓝图笔刷。

这里制作的笔刷同样做了最大程度的简化。其原理正如视频中所给出的,和之前类似,但是多了个 “New land data generated”
在这里插入图片描述
而所谓的 “New land data generated” 在这里就是使用Spline所定义的距离场(使用 Jump Flood 算法):
在这里插入图片描述

0. 复制第一个蓝图

由于第一个蓝图包含了最基础的功能,因此这里的第二个蓝图笔刷将拷贝它,以它为起点。
我将其命名为MyTestLandBP_2

然后,添加一个Spline组件:
在这里插入图片描述
点数需要三个或三个以上。另外,可以将初始点数距离调大点,比如几千,方便预览,毕竟地形网格默认单位是100。

1. 绘制表示范围的三角形

接下来,需要将Spline转换为一个范围,这是通过构建若干个绕着[0]号点的三角形完成的:
在这里插入图片描述
比如在四个点的情况下,就会构建 0,1,2 与 0,2,3 的三角形,再多一个点就会是 0,3,4。以此类推。

因此,要添加一些变量:
在这里插入图片描述

  • CanvasUVTris,它是一个数组,用来存储三角形。
  • ShapeMID,用来将三角形画到RT上的动态材质。

绘制的具体逻辑,可以拷贝CustomBrush_LandmassDrawCanvasShape函数。我拷贝它之后做了一些简化,整理了下节点,然后加上注释以后:
在这里插入图片描述
基本上它分为两部分:

  • 首先循环画出所有三角形到CanvasUVTris中。
  • 然后将CanvasUVTris中的三角形画到BrushMananger的一张RT中。使用的材质是ShapeMID这个动态材质(其母材质是Landmass中的一个材质,但其内容极其简单,基本上就是画出红色,自己也可以轻易制作一个)。

然后,将DrawCanvasShape连到Render函数中靠前的位置:
在这里插入图片描述

随后,就可以做测试了:
添加一个此蓝图笔刷就可以在场景中的BrushMananger中看到Depth And Shape RT A这张RT:
在这里插入图片描述

拖动Spline控制点,就可以看到动态的变化:
在这里插入图片描述

2. 生成 Jump Flood

这方面,Landmass已经有现成的机制了,所以可以直接调用。
Render函数中添加:
在这里插入图片描述
现在,拖动Spline的控制点时,可以在BrushMananger中看到Jump Flood RT A这张RT的变化:
在这里插入图片描述

3. 创建用于存储距离场的RT

添加一个RT变量用于存储距离场:
在这里插入图片描述
创建RT的宏将拷贝自CustomBrush_LandmassCreate RT if Null or Changed函数。不需要变动内容,我整理节点后添加了注释以后如下:
在这里插入图片描述
而这个宏将在初始化阶段调用:
在这里插入图片描述

4. 绘制距离场

添加一个动态材质变量用于绘制距离场:
在这里插入图片描述
它的母材质是/Landmass/Landscape/BlueprintBrushes/Materials/CacheDistanceField_RG8,之前渲染的RT将作为参数传入。

函数Cache Distance Field拷贝自CustomBrush_Landmass,但是添加了两部分内容:“创建动态材质(如果没有的话)”和 “设置材质参数”,原先这两部分被放在了别处,我为了方便一起放在这里:
在这里插入图片描述随后,将在Render函数中调用它:
在这里插入图片描述
现在,可以看到距离场了,它将随着Spline的控制点的变化而变化:
在这里插入图片描述

5. 材质

为了测试,这里的材质依旧很简单,只是为高度叠加了距离场:
在这里插入图片描述
需要注意的是,距离场的读取是通过/Landmass/Landscape/BlueprintBrushes/MF/ReadCachedDistanceField_RG8这个材质函数完成的。如果打开它可以看到他有一个CachedDistanceFieldHeight的参数,需要传入在上一步生成的距离场RT。

6. 完整Render函数

现在,补上传入距离场RT参数的操作之后,完整的Render函数如下:
在这里插入图片描述
效果:
在这里插入图片描述

可以看到,虽然有瑕疵,但是整个流程是跑通了。

总结

地形编辑层是引擎本身的功能,和Landmass无关。

地形蓝图笔刷依赖于地形编辑层功能。它的核心是定义了一个Render函数:

UFUNCTION(BlueprintNativeEvent)
UTextureRenderTarget2D* Render(bool InIsHeightmap, UTextureRenderTarget2D* InCombinedResult, const FName& InWeightmapLayerName);

它的子类蓝图可以覆写它来定义自己的逻辑。
虽然地形蓝图笔刷并不依赖于 Landmass,但是默认情况下,不启用Landmass是没有现成的地形蓝图笔刷可用的。

Landmass是一个插件,内容主要是在Content有各种蓝图、材质、材质函数等。
虽然准确来说创建一个地形蓝图笔刷不一定需要Landmass。但是Landmass中有一些帮助创建地形蓝图笔刷的内容,比如本文创建蓝图笔刷就用到了:

  • LandmassBrushManager,它有地形高度图和权重图的RT
  • 绘制距离场所用的 Jump Flood 函数
  • 一些材质

因此也难怪很多资料,比如本篇的参考视频中,没有将“Landmass蓝图笔刷”和“地形蓝图笔刷”区分开,因为他介绍的蓝图笔刷,包括本文做的蓝图笔刷,确实是 “依赖于Landmass的一些机制的地形蓝图笔刷”。

本文暂时是将Landmass的一些机制当作黑盒了。不过,后续如果想要将蓝图笔刷集成于更大的系统,或者添加一些类似距离场的其他数据,可能需要对Landmass进行更深入的理解。

附录:一些相关C++对象定义

USTRUCT()
struct FLandscapeLayer
{
	GENERATED_USTRUCT_BODY()

	FLandscapeLayer()
		: Guid(FGuid::NewGuid())
		, Name(NAME_None)
		, bVisible(true)
		, bLocked(false)
		, HeightmapAlpha(1.0f)
		, WeightmapAlpha(1.0f)
		, BlendMode(LSBM_AdditiveBlend)
	{}

	FLandscapeLayer(const FLandscapeLayer& OtherLayer) = default;

	UPROPERTY(meta = (IgnoreForMemberInitializationTest))
	FGuid Guid;

	UPROPERTY()
	FName Name;

	UPROPERTY(Transient)
	bool bVisible;

	UPROPERTY()
	bool bLocked;

	UPROPERTY()
	float HeightmapAlpha;

	UPROPERTY()
	float WeightmapAlpha;

	UPROPERTY()
	TEnumAsByte<enum ELandscapeBlendMode> BlendMode;

	UPROPERTY()
	TArray<FLandscapeLayerBrush> Brushes;

	UPROPERTY()
	TMap<TObjectPtr<ULandscapeLayerInfoObject>, bool> WeightmapLayerAllocationBlend; // True -> Substractive, False -> Additive
};
USTRUCT()
struct FLandscapeLayerBrush
{
	GENERATED_USTRUCT_BODY()

	FLandscapeLayerBrush()
#if WITH_EDITORONLY_DATA
		: FLandscapeLayerBrush(nullptr)
#endif
	{}

	FLandscapeLayerBrush(ALandscapeBlueprintBrushBase* InBlueprintBrush)
#if WITH_EDITORONLY_DATA
		: BlueprintBrush(InBlueprintBrush)
		, LandscapeSize(MAX_int32, MAX_int32)
		, LandscapeRenderTargetSize(MAX_int32, MAX_int32)
#endif
	{}

#if WITH_EDITOR
	UTextureRenderTarget2D* Render(bool InIsHeightmap, const FIntRect& InLandscapeSize, UTextureRenderTarget2D* InLandscapeRenderTarget, const FName& InWeightmapLayerName = NAME_None);
	ALandscapeBlueprintBrushBase* GetBrush() const;
	bool IsAffectingHeightmap() const;
	bool IsAffectingWeightmapLayer(const FName& InWeightmapLayerName) const;
	void SetOwner(ALandscape* InOwner);
#endif

private:

#if WITH_EDITOR
	bool Initialize(const FIntRect& InLandscapeExtent, UTextureRenderTarget2D* InLandscapeRenderTarget);
#endif

#if WITH_EDITORONLY_DATA
	UPROPERTY()
	TObjectPtr<ALandscapeBlueprintBrushBase> BlueprintBrush;

	FTransform LandscapeTransform;
	FIntPoint LandscapeSize;
	FIntPoint LandscapeRenderTargetSize;
#endif
};
UCLASS(Blueprintable, hidecategories = (Replication, Input, LOD, Actor, Cooking, Rendering))
class ALandmassActor : public AActor
{
	GENERATED_UCLASS_BODY()

public:

	UFUNCTION(BlueprintNativeEvent, CallInEditor, BlueprintCallable, Category = "Tick")
	void CustomTick(float DeltaSeconds);

	virtual bool IsEditorOnly() const override { return true; }

	virtual bool ShouldTickIfViewportsOnly() const override;
	virtual void Tick(float DeltaSeconds) override;

	UFUNCTION(BlueprintCallable, category = "Default")
	void SetEditorTickEnabled(bool bEnabled) { EditorTickIsEnabled = bEnabled; }

	UPROPERTY()
	bool EditorTickIsEnabled = false;

	UFUNCTION(BlueprintNativeEvent, CallInEditor, BlueprintCallable, Category = "Selection")
	void ActorSelectionChanged(bool bSelected);

private:
	bool bWasSelected = false;

	FDelegateHandle OnActorSelectionChangedHandle;

	/** Called when the editor selection has changed. */
	void HandleActorSelectionChanged(const TArray<UObject*>& NewSelection, bool bForceRefresh);

};
UCLASS(Abstract, NotBlueprintable)
class LANDSCAPE_API ALandscapeBlueprintBrushBase : public AActor
{
	GENERATED_UCLASS_BODY()

protected:
#if WITH_EDITORONLY_DATA
	UPROPERTY(Transient)
	TObjectPtr<class ALandscape> OwningLandscape;

	UPROPERTY(Category = "Settings", EditAnywhere, BlueprintReadWrite)
	bool AffectHeightmap;

	UPROPERTY(Category = "Settings", EditAnywhere, BlueprintReadWrite)
	bool AffectWeightmap;

	UPROPERTY(Category = "Settings", EditAnywhere, BlueprintReadWrite)
	TArray<FName> AffectedWeightmapLayers;

	UPROPERTY(Transient)
	bool bIsVisible;

	uint32 LastRequestLayersContentUpdateFrameNumber;
#endif

public:
	virtual UTextureRenderTarget2D* Render_Native(bool InIsHeightmap, UTextureRenderTarget2D* InCombinedResult, const FName& InWeightmapLayerName) {return nullptr;}
	virtual void Initialize_Native(const FTransform& InLandscapeTransform, const FIntPoint& InLandscapeSize, const FIntPoint& InLandscapeRenderTargetSize) {}

	UFUNCTION(BlueprintNativeEvent)
	UTextureRenderTarget2D* Render(bool InIsHeightmap, UTextureRenderTarget2D* InCombinedResult, const FName& InWeightmapLayerName);

	UFUNCTION(BlueprintNativeEvent)
	void Initialize(const FTransform& InLandscapeTransform, const FIntPoint& InLandscapeSize, const FIntPoint& InLandscapeRenderTargetSize);

	UFUNCTION(BlueprintCallable, Category = "Landscape")
	void RequestLandscapeUpdate();

	UFUNCTION(BlueprintImplementableEvent)
	void GetBlueprintRenderDependencies(TArray<UObject*>& OutStreamableAssets);

#if WITH_EDITOR
	virtual void CheckForErrors() override;

	virtual void GetRenderDependencies(TSet<UObject*>& OutDependencies);

	virtual void SetOwningLandscape(class ALandscape* InOwningLandscape);
	class ALandscape* GetOwningLandscape() const;

	bool IsAffectingHeightmap() const { return AffectHeightmap; }
	bool IsAffectingWeightmap() const { return AffectWeightmap; }
	bool IsAffectingWeightmapLayer(const FName& InLayerName) const;
	bool IsVisible() const { return bIsVisible; }
	bool IsLayerUpdatePending() const;

	void SetIsVisible(bool bInIsVisible);
	void SetAffectsHeightmap(bool bInAffectsHeightmap);
	void SetAffectsWeightmap(bool bInAffectsWeightmap);

	virtual bool ShouldTickIfViewportsOnly() const override;
	virtual void Tick(float DeltaSeconds) override;
	virtual void PostEditMove(bool bFinished) override;
	virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
	virtual void Destroyed() override;

	virtual void PushDeferredLayersContentUpdate();

	virtual EActorGridPlacement GetDefaultGridPlacement() const override { return EActorGridPlacement::AlwaysLoaded; }
#endif
};
  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2021-11-11 13:02:12  更:2021-11-11 13:02:51 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/28 0:44:36-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码