1. 概述

之所以写这个绘制简单三角形的实例其实是想知道如何在Unreal中通过代码绘制自定义Mesh,如果你会绘制一个三角形,那么自然就会绘制复杂的Mesh了。所以这是很多图形工作者的第一课。

2. 详论

2.1. 代码实现

Actor是Unreal的基本显示对象,有点类似于Unity中的GameObject或者OSG中的Node。因此,我们首先要实现一个继承自AActor的类

头文件CustomMeshActor.h:

#pragma once

// clang-format off
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CustomMeshActor.generated.h"
// clang-format on UCLASS()
class UESTUDY_API ACustomMeshActor : public AActor {
GENERATED_BODY() public:
// Sets default values for this actor's properties
ACustomMeshActor(); protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override; UStaticMesh* CreateMesh();
void CreateGeometry(FStaticMeshRenderData* RenderData);
void CreateMaterial(UStaticMesh* mesh); public:
// Called every frame
virtual void Tick(float DeltaTime) override; UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
UStaticMeshComponent* staticMeshComponent;
};

实现CustomMeshActor.cpp:

#include "CustomMeshActor.h"

#include "Output.h"

// Sets default values
ACustomMeshActor::ACustomMeshActor() {
// Set this actor to call Tick() every frame. You can turn this off to
// improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
} // Called when the game starts or when spawned
void ACustomMeshActor::BeginPlay() {
Super::BeginPlay(); staticMeshComponent = NewObject<UStaticMeshComponent>(this); staticMeshComponent->SetMobility(EComponentMobility::Stationary);
SetRootComponent(staticMeshComponent);
staticMeshComponent->RegisterComponent(); UStaticMesh* mesh = CreateMesh();
if (mesh) {
staticMeshComponent->SetStaticMesh(mesh);
}
} UStaticMesh* ACustomMeshActor::CreateMesh() {
UStaticMesh* mesh = NewObject<UStaticMesh>(staticMeshComponent);
mesh->NeverStream = true;
mesh->SetIsBuiltAtRuntime(true); TUniquePtr<FStaticMeshRenderData> RenderData =
MakeUnique<FStaticMeshRenderData>(); CreateGeometry(RenderData.Get()); CreateMaterial(mesh); mesh->SetRenderData(MoveTemp(RenderData));
mesh->InitResources();
mesh->CalculateExtendedBounds(); //设置包围盒之后调用这个函数起效,否则会被视锥体剔除
return mesh;
} void ACustomMeshActor::CreateMaterial(UStaticMesh* mesh) {
UMaterial* material1 = (UMaterial*)StaticLoadObject(
UMaterial::StaticClass(), nullptr,
TEXT("Material'/Game/Materials/RedColor.RedColor'")); mesh->AddMaterial(material1); UMaterial* material2 = (UMaterial*)StaticLoadObject(
UMaterial::StaticClass(), nullptr,
TEXT("Material'/Game/Materials/GreenColor.GreenColor'")); mesh->AddMaterial(material2);
} void ACustomMeshActor::CreateGeometry(FStaticMeshRenderData* RenderData) {
RenderData->AllocateLODResources(1);
FStaticMeshLODResources& LODResources = RenderData->LODResources[0]; int vertexNum = 4; TArray<FVector> xyzList;
xyzList.Add(FVector(0, 0, 50));
xyzList.Add(FVector(100, 0, 50));
xyzList.Add(FVector(100, 100, 50));
xyzList.Add(FVector(0, 100, 50)); TArray<FVector2D> uvList;
uvList.Add(FVector2D(0, 1));
uvList.Add(FVector2D(0, 0));
uvList.Add(FVector2D(1, 0));
uvList.Add(FVector2D(1, 1)); // 设置顶点数据
TArray<FStaticMeshBuildVertex> StaticMeshBuildVertices;
StaticMeshBuildVertices.SetNum(vertexNum);
for (int m = 0; m < vertexNum; m++) {
StaticMeshBuildVertices[m].Position = xyzList[m];
StaticMeshBuildVertices[m].Color = FColor(255, 0, 0);
StaticMeshBuildVertices[m].UVs[0] = uvList[m];
StaticMeshBuildVertices[m].TangentX = FVector(0, 1, 0); //切线
StaticMeshBuildVertices[m].TangentY = FVector(1, 0, 0); //副切线
StaticMeshBuildVertices[m].TangentZ = FVector(0, 0, 1); //法向量
} LODResources.bHasColorVertexData = false; //顶点buffer
LODResources.VertexBuffers.PositionVertexBuffer.Init(StaticMeshBuildVertices); //法线,切线,贴图坐标buffer
LODResources.VertexBuffers.StaticMeshVertexBuffer.Init(
StaticMeshBuildVertices, 1); //设置索引数组
TArray<uint32> indices;
int numTriangles = 2;
int indiceNum = numTriangles * 3;
indices.SetNum(indiceNum);
indices[0] = 2;
indices[1] = 1;
indices[2] = 0;
indices[3] = 3;
indices[4] = 2;
indices[5] = 0; LODResources.IndexBuffer.SetIndices(indices,
EIndexBufferStride::Type::AutoDetect); LODResources.bHasDepthOnlyIndices = false;
LODResources.bHasReversedIndices = false;
LODResources.bHasReversedDepthOnlyIndices = false;
// LODResources.bHasAdjacencyInfo = false; FStaticMeshLODResources::FStaticMeshSectionArray& Sections =
LODResources.Sections;
{
FStaticMeshSection& section = Sections.AddDefaulted_GetRef(); section.bEnableCollision = false;
section.MaterialIndex = 0;
section.NumTriangles = 1;
section.FirstIndex = 0;
section.MinVertexIndex = 0;
section.MaxVertexIndex = 2;
}
{
FStaticMeshSection& section = Sections.AddDefaulted_GetRef(); section.bEnableCollision = false;
section.MaterialIndex = 0;
section.NumTriangles = 1;
section.FirstIndex = 3;
section.MinVertexIndex = 3;
section.MaxVertexIndex = 5;
} double boundArray[7] = {0, 0, 0, 200, 200, 200, 200}; //设置包围盒
FBoxSphereBounds BoundingBoxAndSphere;
BoundingBoxAndSphere.Origin =
FVector(boundArray[0], boundArray[1], boundArray[2]);
BoundingBoxAndSphere.BoxExtent =
FVector(boundArray[3], boundArray[4], boundArray[5]);
BoundingBoxAndSphere.SphereRadius = boundArray[6];
RenderData->Bounds = BoundingBoxAndSphere;
} // Called every frame
void ACustomMeshActor::Tick(float DeltaTime) { Super::Tick(DeltaTime); }

然后将这个类对象ACustomMeshActor拖放到场景中,显示结果如下:

2.2. 解析:Component

  1. Actor只是一个空壳,具体的功能是通过各种类型的Component实现的(这一点与Unity不谋而合),这里使用的是UStaticMeshComponent,这也是Unreal场景中用的最多的Mesh组件。

  2. 这里组件初始化是在BeginPlay()中创建的,如果在构造函数中创建,那么就不能使用NewObject,而应该使用如下方法:

    // Sets default values
    ACustomMeshActor::ACustomMeshActor() {
    // Set this actor to call Tick() every frame. You can turn this off to
    // improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true; staticMeshComponent =
    CreateDefaultSubobject<UStaticMeshComponent>(TEXT("SceneRoot"));
    staticMeshComponent->SetMobility(EComponentMobility::Static);
    SetRootComponent(staticMeshComponent); UStaticMesh* mesh = CreateMesh();
    if (mesh) {
    staticMeshComponent->SetStaticMesh(mesh);
    }
    }
  3. 承接2,在BeginPlay()中创建和在构造函数中创建的区别就在于前者是运行时创建,而后者在程序运行之前就创建了,可以在未运行的编辑器状态下看到静态网格体和材质。

  4. 承接2,在构造函数中创建的UStaticMeshComponent移动性被设置成Static了,这时运行会提示“光照需要重建”,也就是静态对象需要烘焙光照,在工具栏"构建"->"仅构建光照"烘培一下即可。这种方式运行时渲染效率最高。

  5. 对比4,运行时创建的UStaticMeshComponent移动性可以设置成Stationary,表示这个静态物体不移动,启用缓存光照法,并且缓存动态阴影。

2.3. 解析:材质

  1. 在UE编辑器分别创建了红色和绿色简单材质,注意材质是单面还是双面的,C++代码设置的要和材质蓝图中设置的要保持一致。最开始我参考的就是参考文献1中的代码,代码中设置成双面,但是我自己的材质蓝图中用的单面,程序启动直接崩溃了。

  2. 如果场景中材质显示不正确,比如每次浏览场景时的效果都不一样,说明可能法向量没有设置,我最开始就没有注意这个问题以为是光照的问题。

  3. 单面材质的话,正面是逆时针序还是顺时针序?从这个案例来看应该是逆时针。UE是个左手坐标系,X轴向前,法向量是(0, 0, 1),从法向量的一边看过去,顶点顺序是(100, 100, 50)->(100, 0, 50)->(0, 0, 50),明显是逆时针。

2.4. 解析:包围盒

  1. 包围盒参数最好要设置,UE似乎默认实现了视景体裁剪,不在范围内的物体会不显示。如果在某些视角场景对象突然不显示了,可能包围盒参数没有设置正确,导致视景体裁剪错误地筛选掉了当前场景对象。

    FBoxSphereBounds BoundingBoxAndSphere;
    //...
    RenderData->Bounds = BoundingBoxAndSphere;
    //...
    mesh->CalculateExtendedBounds(); //设置包围盒之后调用这个函数起效,否则会被视锥体剔除
  2. 即使是一个平面,包围盒的三个Size参数之一也不能为0,否则还是可能会在某些视角场景对象不显示。

2.5. 解析:Section

Mesh内部是可以进行划分的,划分成多少个section就使用多少个材质,比如这里划分了两个section,最后就使用了两个材质。如下代码所示:

FStaticMeshLODResources::FStaticMeshSectionArray& Sections =
LODResources.Sections;
{
FStaticMeshSection& section = Sections.AddDefaulted_GetRef(); section.bEnableCollision = false;
section.MaterialIndex = 0;
section.NumTriangles = 1;
section.FirstIndex = 0;
section.MinVertexIndex = 0;
section.MaxVertexIndex = 2;
}
{
FStaticMeshSection& section = Sections.AddDefaulted_GetRef(); section.bEnableCollision = false;
section.MaterialIndex = 0;
section.NumTriangles = 1;
section.FirstIndex = 3;
section.MinVertexIndex = 3;
section.MaxVertexIndex = 5;
}

3. 其他

除了本文介绍的方法之外,也有其他的实现办法,具体可以参考文献3-5。实在是没有时间进行进一步的研究了,因此记录备份一下。另外,文献6-7可能对了解UE关于Mesh的内部实现有所帮助,笔者反正是看麻了。不得不说,这么一个微小的功能涉及到的内容还真不少,看来有的研究了。

4. 参考

  1. UE4绘制简单三角形(二)
  2. UE4之坐标系
  3. [UE4 C++]三种方式绘制三角形
  4. Building a StaticMesh in C++ during runtime
  5. Build static mesh from description
  6. 虚幻 – StaticMesh 分析
  7. Creating a Custom Mesh Component in UE4

上一篇

目录

下一篇

代码地址

最新文章

  1. MVC前台Post/Get异步获得数据时参数的取值问题
  2. linxu scp命令
  3. mac 安装 nginx 环境
  4. C# 3个线程A B C 依次打印123123123..
  5. bzoj 1196 二分+生成树判定
  6. VisualSvn+TortoiseSVN的安装说明
  7. 基于WebForm+EasyUI的业务管理系统形成之旅 -- ParamQueryGrid行、列合并(Ⅸ)
  8. poj 3026Borg Maze
  9. QStringLiteral(源代码里有一个通过构造函数产生的从const char*到QString的隐式转换,QStringLiteral字符串可以放在代码的任何地方,编译期直接生成utf16字符串,速度很快,体积变大)
  10. paip.tree 生成目录树到txt后的折叠查看
  11. Mysql5.7.20使用group by查询(select *)时出现错误--修改sql mode
  12. 【Java入门提高篇】Day13 Java中的反射机制
  13. bugku web web3
  14. Freeradius服务器的搭建流程
  15. [原创]浅谈IT人如何做理财规划
  16. 【Spark调优】数据本地化与参数调优
  17. [hive] hive 安装、配置
  18. 论文阅读:Prominent Object Detection and Recognition: A Saliency-based Pipeline
  19. H5页面开发笔记(react技术栈)
  20. ReactNative For Android 项目实战总结

热门文章

  1. Spring Core rce漏洞分析(CVE-2022-22965)
  2. Golang 加密方法
  3. 23种设计模式之自定义Spring框架(五)
  4. C语言嵌套for循环实现冒泡排序
  5. Idea在windows和mac中的一些快捷指令
  6. Go语言核心36讲30
  7. 面试官不按套路,竟然问我Java线程池是怎么统计线程空闲时间?
  8. layui table表格使用table.resize()方法 重置表格尺寸
  9. 记一次hook mac地址实现伪装硬件码
  10. python安装第三方库换源