Unreal学习笔记2-绘制简单三角形
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
- Actor只是一个空壳,具体的功能是通过各种类型的Component实现的(这一点与Unity不谋而合),这里使用的是UStaticMeshComponent,这也是Unreal场景中用的最多的Mesh组件。 
- 这里组件初始化是在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);
 }
 }
 
- 承接2,在BeginPlay()中创建和在构造函数中创建的区别就在于前者是运行时创建,而后者在程序运行之前就创建了,可以在未运行的编辑器状态下看到静态网格体和材质。 
- 承接2,在构造函数中创建的UStaticMeshComponent移动性被设置成Static了,这时运行会提示“光照需要重建”,也就是静态对象需要烘焙光照,在工具栏"构建"->"仅构建光照"烘培一下即可。这种方式运行时渲染效率最高。 
- 对比4,运行时创建的UStaticMeshComponent移动性可以设置成Stationary,表示这个静态物体不移动,启用缓存光照法,并且缓存动态阴影。 
2.3. 解析:材质
- 在UE编辑器分别创建了红色和绿色简单材质,注意材质是单面还是双面的,C++代码设置的要和材质蓝图中设置的要保持一致。最开始我参考的就是参考文献1中的代码,代码中设置成双面,但是我自己的材质蓝图中用的单面,程序启动直接崩溃了。 
- 如果场景中材质显示不正确,比如每次浏览场景时的效果都不一样,说明可能法向量没有设置,我最开始就没有注意这个问题以为是光照的问题。 
- 单面材质的话,正面是逆时针序还是顺时针序?从这个案例来看应该是逆时针。UE是个左手坐标系,X轴向前,法向量是(0, 0, 1),从法向量的一边看过去,顶点顺序是(100, 100, 50)->(100, 0, 50)->(0, 0, 50),明显是逆时针。 
2.4. 解析:包围盒
- 包围盒参数最好要设置,UE似乎默认实现了视景体裁剪,不在范围内的物体会不显示。如果在某些视角场景对象突然不显示了,可能包围盒参数没有设置正确,导致视景体裁剪错误地筛选掉了当前场景对象。 - FBoxSphereBounds BoundingBoxAndSphere;
 //...
 RenderData->Bounds = BoundingBoxAndSphere;
 //...
 mesh->CalculateExtendedBounds(); //设置包围盒之后调用这个函数起效,否则会被视锥体剔除
 
- 即使是一个平面,包围盒的三个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. 参考
- UE4绘制简单三角形(二)
- UE4之坐标系
- [UE4 C++]三种方式绘制三角形
- Building a StaticMesh in C++ during runtime
- Build static mesh from description
- 虚幻 – StaticMesh 分析
- Creating a Custom Mesh Component in UE4
Unreal学习笔记2-绘制简单三角形的更多相关文章
- Unity3D学习笔记1——绘制一个三角形
		目录 1. 绪论 2. 概述 3. 详论 3.1. 准备 3.2. 实现 3.3. 解析 3.3.1. 场景树对象 3.3.2. 绘制方法 4. 结果 1. 绪论 最近想学习一下Unity3d,无奈发 ... 
- Unity3D学习笔记2——绘制一个带纹理的面
		目录 1. 概述 2. 详论 2.1. 网格(Mesh) 2.1.1. 顶点 2.1.2. 顶点索引 2.2. 材质(Material) 2.2.1. 创建材质 2.2.2. 使用材质 2.3. 光照 ... 
- Spring MVC 学习笔记10 —— 实现简单的用户管理(4.3)用户登录显示全局异常信息
		</pre>Spring MVC 学习笔记10 -- 实现简单的用户管理(4.3)用户登录--显示全局异常信息<p></p><p></p>& ... 
- Spring MVC 学习笔记9 —— 实现简单的用户管理(4)用户登录显示局部异常信息
		Spring MVC 学习笔记9 -- 实现简单的用户管理(4.2)用户登录--显示局部异常信息 第二部分:显示局部异常信息,而不是500错误页 1. 写一个方法,把UserException传进来. ... 
- Spring MVC 学习笔记8 —— 实现简单的用户管理(4)用户登录
		Spring MVC 学习笔记8 -- 实现简单的用户管理(4)用户登录 增删改查,login 1. login.jsp,写在外面,及跟WEB-INF同一级目录,如:ls Webcontent; &g ... 
- WebGL学习笔记二——绘制基本图元
		webGL的基本图元点.线.三角形 gl.drawArrays(mode, first,count) first,代表从第几个点开始绘制即顶点的起始位置 count,代表绘制的点的数量. mode,代 ... 
- blfs(systemv版本)学习笔记-制作一个简单的桌面系统
		我的邮箱地址:zytrenren@163.com欢迎大家交流学习纠错! 大概思路: lfs(系统)+xorg(驱动)+i3-wm(窗口+桌面)+lightdm(显示管理器+登录管理器) 链接: lfs ... 
- [原创]java WEB学习笔记41:简单标签之带属性的自定义标签(输出指定文件,计算并输出两个数的最大值 demo)
		本博客为原创:综合 尚硅谷(http://www.atguigu.com)的系统教程(深表感谢)和 网络上的现有资源(博客,文档,图书等),资源的出处我会标明 本博客的目的:①总结自己的学习过程,相当 ... 
- [原创]java WEB学习笔记40:简单标签概述(背景,使用一个标签,标签库的API,SimpleTag接口,创建一个自定义的标签的步骤 和简单实践)
		本博客为原创:综合 尚硅谷(http://www.atguigu.com)的系统教程(深表感谢)和 网络上的现有资源(博客,文档,图书等),资源的出处我会标明 本博客的目的:①总结自己的学习过程,相当 ... 
- python学习笔记(3)--turtle简单绘制
		参考:大学生mooc 北京理工大学的python程序与设计课程 蟒蛇绘制代码如下: #pythonDraw.py import turtle turtle.setup(650,350,200,200) ... 
随机推荐
- or、and表达式
			or 逻辑表达式 result = a or b 如果a为空则执行or后面的b,如果a不为空,则执行or前面的a 即:赋值运算中,如果or前面为真,就不会去执行or后面的,如果or前面为假才会执行or ... 
- ES的java端API操作
			首先简单介绍下写这篇博文的背景,最近负责的一个聚合型的新项目要大量使用ES的检索功能,之前对es的了解还只是纯理论最多加个基于postman的索引创建操作,所以这次我得了解在java端如何编码实现:网 ... 
- 2022-11-16  Acwing每日一题
			本系列所有题目均为Acwing课的内容,发表博客既是为了学习总结,加深自己的印象,同时也是为了以后回过头来看时,不会感叹虚度光阴罢了,因此如果出现错误,欢迎大家能够指出错误,我会认真改正的.同时也希望 ... 
- IDEA提交任务到spark standalone集群
			参考文章: 在idea里面怎么远程提交spark任务到yarn集群 代码 注意setJars,提交的代码,要提前打好包.否则会报找不到类的错误 个人理解就相当于运行的main方法是起了一个spark- ... 
- laravel框架 forelse和foreach
			1. @forelse($data as $v) 循环数据内容..... @empty 数据为空提示... @endforelse 2. @foreach($data as $v) 循环数据内容... ... 
- day09 常用工具类&包装类&集合——List、Set
			day09 常用工具类 java.lang.Math数值运算 基本数值运算,如初等函数.对数.平方根和三角函数 //最大最小值 Math.max(12, 21); Math.min(2, 3); // ... 
- Springboot 整合 SpringCache 使用 Redis 作为缓存
			一直以来对缓存都是一知半解,从没有正经的接触并使用一次,今天腾出时间研究一下缓存技术,开发环境为OpenJDK17与SpringBoot2.7.5 SpringCache基础概念 接口介绍 首先看看S ... 
- WeakHashMap 和 HashMap 的区别是什么,何时使用?
			本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问. 前言 大家好,我是小彭. 在之前的文章里,我们聊到了 Java 标准库中 HashMap 与 LinkedH ... 
- OpenLooKeng
			一.登录注册 网址为:https://tryme.openlookeng.io/,进入该网址后可以看到以下界面: 可以选择左边的GitHub或者右边的Gitee进行登录,如果没有账号需要注册一个账号进 ... 
- Java开发如何通过IoT边缘ModuleSDK进行协议转换?
			摘要:使用ModuleSDK开发插件应用,接入其他协议设备(如HTTP请求数据),将其他协议的数据转化为MQTT协议JSON数据上报到IoTDA. 本文分享自华为云社区<[华为云IoTEdge开 ... 
