Unreal学习笔记2-绘制简单三角形

news2025/1/13 9:47:22

文章目录

  • 1. 概述
  • 2. 详论
    • 2.1. 代码实现
    • 2.2. 解析:Component
    • 2.3. 解析:材质
    • 2.4. 解析:包围盒
    • 2.5. 解析:Section
  • 3. 其他
  • 4. 参考

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拖放到场景中,显示结果如下:

imglink1

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

上一篇
目录
下一篇

代码地址

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/191657.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网页开发:MySQL和Python案例

目录 一、MySQL的概念和引入 二、MySQL指令 1、数据库管理&#xff08;文件夹&#xff09; 2、数据表的管理&#xff08;文件&#xff09; 3、数据行操作 三、常用的数据类型 四、员工管理案例 1、使用MySQL内置工具&#xff08;命令&#xff09; 2、Python代码实现 ①…

Linux学习之常用基本命令【2】

文章目录一 文件内容查看1.1 catcatcat 由第一行开始显示1.2 tactactac 由最后一行开始显示1.3 nlnlnl 显示行号1.4 moremoremore 翻页1.5 lesslessless 翻页1.6 headheadhead 显示前几行1.7 tailtailtail 显示后几行1.8 拓展&#xff1a;LinuxLinuxLinux 链接概念二 VimVimVim…

Ubuntu18.04.6 配置固定ip、ssh登录、root账号

上文讲解了如何下载安装ubuntu&#xff0c;https://blog.csdn.net/weixin_47491957/article/details/128839639 ubuntu在安装完成后&#xff0c;是不能进行ssh登录、且没有root账号&#xff0c;本文带来如何配置ssh登录及root账号 在做这些设置之前&#xff0c;我们要确定ip固…

【手写 Promise 源码】第十三篇 - Promise.allsettled 和 Promise.any 的实现

一&#xff0c;前言 上一篇&#xff0c;主要实现了 Promise 静态 API&#xff08;类方法&#xff09;&#xff1a;Promise.race&#xff0c;主要涉及以下几个点&#xff1a; 测试原生 Promise.race 的使用&#xff1b;Promise.race 的功能与特性分析&#xff1b;Promise.race…

10、声明和创建字符串

目录 一、声明字符串 二、创建字符串 &#xff08;1&#xff09;String(char a[]) &#xff08;2&#xff09;String(char a[], int offset, int length) &#xff08;3&#xff09;String(char[] value) 一、声明字符串 在Java语言中字符串必须包含在一对双引号&#xf…

记录每日LeetCode 2325.解密消息 Java实现

题目描述&#xff1a; 给你字符串 key 和 message &#xff0c;分别表示一个加密密钥和一段加密消息。解密 message 的步骤如下&#xff1a; 使用 key 中 26 个英文小写字母第一次出现的顺序作为替换表中的字母 顺序 。 将替换表与普通英文字母表对齐&#xff0c;形成对照表。…

【Flink】详解StreamGraph

概述 没有看上一期的小伙伴请先看上一期【Flink】浅谈Flink架构和调度&#xff0c;上一期的一个核心内容就是 Flink 中的执行图可以分成四层&#xff1a;StreamGraph → JobGraph → ExecutionGraph → 物理执行图。 今天我们好好谈论一下StreamGraph&#xff0c;StreamGraph…

WPF MaterialDesignIn 收银系统(2)主界面

前言 接上一篇&#xff0c;既然登陆页面搞定了&#xff0c;接下来就是登陆成功后跳转的主界面了。 界面 思考 到了这一步&#xff0c;我们就要开始思考&#xff0c;怎么来丰富我们的应用&#xff0c;最少需要哪些东西、功能、模块、数据等等&#xff0c;才能支撑起一个收银系统…

java分支语句

流程控制1.1流程控制语句概述1.2流程控制语句分类顺序结构顺序结构是程序中最简单最基本的结构。分支结构(if,switch)循环结构(for,while,do...while)if语句2.1if语句格式1//格式 if(关系表达式){ 语句体&#xff1b; }执行流程&#xff1a;首先计算关系表达式的值如果关系表达…

每日一练10——井字棋密码强度等级

文章目录井字棋思路&#xff1a;代码&#xff1a;密码强度等级思路&#xff1a;代码&#xff1a;井字棋 题目链接&#xff1a; 思路&#xff1a; 井字棋&#xff0c;是一种在3*3格子上进行的连珠游戏&#xff0c;三个相同就代表获胜。 井字棋有四种情况表示当前玩家获胜&…

MySQL 面试题(一):索引失效的几种情况

❤️ 博客首页&#xff1a;水滴技术 &#x1f680; 支持水滴&#xff1a;点赞&#x1f44d; 收藏⭐ 留言&#x1f4ac; &#x1f338; 订阅专栏&#xff1a;MySQL 教程&#xff1a;从入门到精通 文章目录一、like 以通配符 % 开头的索引会失效二、is not null 无法使用索引&a…

SGI STL二级空间配置器源码剖析(1)

之前大概写过SGI STL相关的东西有关SGI STL&#xff0c;讲了讲空间配置器的原理&#xff0c;这一系列就剖析一下源码。 目录 下面就看看重要成员信息&#xff1a; 两个辅助接口函数&#xff1a; 关于C的STL的空间配置器allocator的实现就是分了四部分&#xff1a; allocate&…

云原生学习——容器的基本概念和Kubernetes 核心概念

https://developer.aliyun.com/learning/course/572/detail/7786什么是容器(Container)&#xff1f;什么是镜像(docker)&#xff1f;分层和复用运行后名称为 demo&#xff0c;进程是top命令二、容器的生命周期数据卷是一个特殊的目录四、容器 VS VM虚拟机一、什么是 Kubernetes…

KD:Distilling the Knowledge in a Neural Network 原理与代码解析

paper&#xff1a;Distilling the Knowledge in a Neural Networkcode&#xff1a;https://github.com/megvii-research/mdistiller/blob/master/configs/cifar100/kd.yaml存在的问题训练阶段&#xff0c;我们可以不考虑计算成本和训练时间&#xff0c;为了更高的精度训练一个很…

小程序提升篇-npm、数据共享、分包、自定义tabBar

npm 包的使用1.1 npm限制小程序支持npm第三方包&#xff0c;提高开发效率&#xff0c;有以下三种限制&#xff1a;不支持依赖node.js内置库包不支持依赖浏览器内置对象的包不支持依赖C插件的包限制较多&#xff0c;因此小程序可以使用的包不多1.2 Vant Weapp是一套开源的小程序…

带你读懂——频率响应与采样频率之间的关系

频响范围 频率响应&#xff1a;不同频率下的输入信号经过系统后响应之后的输出信号增益。大白话就是&#xff0c;输入信号频率是xxx Hz&#xff0c;幅值为yyy mg&#xff0c;观察此时的输出信号幅值为AyAyAy mg&#xff0c;此时升高或降低了AAA倍。 电压增益计算公式&#xff…

浅读人月神话笔记(2)

读书笔记&#xff1a;今日翻书浅读&#xff0c;从《为什么巴比伦塔会失败》开始至《干将莫邪》结束&#xff0c;巴比伦塔的建造对当下项目推进有广泛借鉴意义&#xff0c;今天这几个章节在PMBOK中有一些可以互相对照学习的内容&#xff0c;《为什么巴比伦塔会失败&#xff1f;》…

RPA自动化办公04——软件自动化(excel,word,浏览器)

参考&#xff1a;软件自动化_UiBot开发者指南 虽然我们可以使用前面的鼠标点击等操作打开excel表然后写入什么的&#xff0c;但是直接用Uibot里面的命令会更方便。 Excel 在旁边的命令里面打开excel簿 随便选一个excel表实验一下&#xff0c;然后读取区域&#xff0c;可以选。…

使用字典快速获取唯一值与重复值【单个字典对象】

在以前的博客《使用字典快速获取唯一值与重复值&#xff08;交集与并集&#xff09;》使用多个字典对象获取交集与并集&#xff0c;最近有同学提问&#xff0c;是否可以只使用一个字典对象实现相同的功能&#xff0c;对于有“编程洁癖”的同学来说&#xff0c;可能不喜欢使用多…

记录:windows+opencv3.4.16+vs2013+cmake编译

环境&#xff1a;vs2013,x64&#xff0c;opencv3.4.16&#xff0c;cmakeopencv官网&#xff1a;https://opencv.org/releases/1、opencv source下载&#xff1a;因为想用vs2013&#xff0c;现在opencv官网windows版安装包只有vc14和vc15了&#xff0c;只能自己编译了。找一个自…