UE C++学习笔记

Posted Just_DevG

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UE C++学习笔记相关的知识,希望对你有一定的参考价值。

这是siki学院的公开课内容,详细视频教程可见B站链接:

Unreal入门第一季 - 虚幻C++基础训练_哔哩哔哩_bilibili

了解玩这一套基本理论语法之后完全可以上手UE的官方教程进行练习,并对其代码有更深一层的理解。

反射与垃圾回收系统

  1. 反射:程序运行时检查自身的能力,将变量等暴露给蓝图,编辑器,允许运行时调用
  2. 垃圾回收:检测某个对象是否不再被使用,不再适用会被标记成垃圾,在适当时候进行回收
  3. 如何使用:使用宏,之后紧跟的一段代码会参与反射与垃圾回收

 创建UObject子类

  1. 右键 New C++ Classes 选择所需父类,点击Next,进行创建

UCLASS(Blueprintable)//指示可转化为蓝图使用如果父类写了这里不写也可以

class MYBASICTRAINING_API UMyObject : public UObject

GENERATED_BODY()

;

  1. 使用UE自己的编译器,在window->Developer Tools->Message Log->Compiler Log可查看编译信息
  2. 右键这个C++类,可生成对应的蓝图

基础宏参数介绍

  1. 变量(希望可在蓝图中被读写)——BlueprintReadWrite——之后在蓝图编辑器中有Get/Set对用的节点
  2. 函数(希望可在蓝图中调用)——BlueprintCallable——之后在蓝图编辑器中右键搜索函数名可调用
  3. 只读——BlueprintReadonly
  4. 分类——Category:UPROPERTY(BlueprintReadWrite,Category="My Variable")

实现打印及实例化一个继承自Object的类

  1. UE_LOG(LogTemp,Log/Warning/Error,TEXT(“Hello World”)),在window->Developer Tools->Output Log中查看
  2. 实例化Construct object from class

 调用类的方法

 如何删除自定义的C++类

  1. 删除该类派生出的蓝图等资源
  2. 关闭对应VS代码
  3. 找到对应项目工程目录,找到Source(存储的就是自定义的类),进入Source找到与工程同名文件夹,找到需要删除的C++的.cpp和.h文件,进行删除
  4. 退回到Source同级目录,找到Binaries文件夹,整个一起删除
  5. 右键uproject文件,重新生成VS文件(Generate Visual Studio project files)

创建Actor子类和学习命名规范

  1. 创建C++类的目录下是不能直接加文件夹的,但是可以在创建时在路径中手动输入
  2. Actor的beginplay在游戏开始或者这个Actor生成(spawn)时回调
  3. 使用Actor.h中定义的变量控制是否每帧都调用:AActor::PrimaryActorTick.bCanEverTick = true;
  4. 继承自Actor使用A开头,Object使用U开头

在C++中创建静态网格组件

  1. 在头文件中声明:

//组件继承自UObject,所以是用U开头

UPROPERTY(VisibleAnywhere,Categroy="My Actor Componets")//对于组件的标记

UStaticMeshComponent* StaticMesh;

  1. 对变量赋初值.cpp构造函数中:

//使用创建子物体函数(前默认为this),参数填写该组件的标识(非名字),使用指针名作为组件名

StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MyStaticMesh"));

  1. 为C++类创建组件之后就可以将其直接拖到场景中了(拥有了组件提供的transform)
  2. 场景内的静态网格,选中后使用菜单栏中的蓝图下拉菜单,有转化为蓝图类的选项(Convert Select Actor to Blueprint Class)

UPROPERTY中的参数使用简介

使用SetActorLocation控制位置与宏参数EditInstanceOnly介绍

  1. 设置使用者的位置:SetActorLocation(InitLocation);参数是一个FVector
  2. 对参数FVector可以限定EditInstanceOnly:仅实例化的对象可编辑,对于创建他的蓝图类是不能编辑的

UPROPERTY(EditInstanceOnly, Category = "My Actor Properties | Vector")//|表示到下一级目录

FVector InitLocation;

  1. 默认单位是cm

VisibleInstanceOnly与EditDefaultsOnly宏参数

  1. 只在实例化对象上显示,不可编辑VisibleInstanceOnly
  2. bool型变量须以小写b开头
  3. EditDefaultsOnly只可默认编辑——只可在类模板中编辑

VisibleDefaultsOnly与EditAnywhere宏参数

  1. 带defaults就只能显示在蓝图类中
  2. EditAnywhere任何地方(蓝图类与实例化对象中都可以编辑)

在编辑器中限制输入值的范围及不要将组件指针设置为EditAnywhere

  1. 不要将组件指针设置为EditAnywhere,这将导致该指针指向的对象可改变,那么因为继承关系的存在,子类也可以为这个指针赋值,将导致对象类型不明
  2. 在编辑器中限制输入值的范围:使用meta

UPROPERTY(EditAnywhere, Category = "My Actor Properties | Vector", meta = (ClampMin = -5.0f, ClampMax = 5.0f, UIMin = -5.0f, UIMax = 5.0f))//Clamp表示限制值的范围,UIMxx表示使用UI拖动时的范围

FVector LocationOffset;

物理系统

简单碰撞与复杂碰撞

  1. 双击打开模型资源文件,勾选Collision中的选项可显示碰撞体;
  2. 创建碰撞则在上方菜单栏下拉框中,选择创建何种类型的碰撞,下方提供了Auto Convert自动生成凸包的一个算法;

模拟物理与重力

  1. 选择场景对象,找到其physical选项卡,勾选Simulate Physic即可启动物理模拟(先要生成其碰撞体才能勾选)
  2. 注意physical中是否启用重力选项,仅仅也只是针对当前模型;
  3. 运行时,使用~键调出控制台,输入命令:show Collision查看场景物体的碰撞体

通过代码添加力与力矩

  1. 提示不完整的类类型,一般是头文件没有包含,查找头文件通过unreal.doc文档进行搜索(搜索不到注意切换为英文),翻到Header栏目,一般是Classes后边就是头文件路径。注意在.h中加入#include包含头文件要加载generate之前,在cpp中就是加最后一个#include之后就可以;
  2. 对模型网格添加力AddForce:

StaticMesh->AddForce(InitForce);//注意AddForce的默认参数,第三个可设置是否忽略质量进行加速

  1. 使用AddTorque添加力矩,用法同上;

使用Sweep在不开启物理模拟的情况下进行碰撞

  1. 是函数AddActorLocalOffset的第二个参数,表示是否扫描,是的时候会对即将移动到的路径上进行一个扫描,当发现有物体时,就不去移动了,产生一个遮挡的效果:AddActorLocalOffset(LocationOffset,true);

碰撞通道与击中信息

  1. 碰撞通道:在Collision Presets(碰撞预设)中修改为custom,由用户自定义检测哪些通道进行碰撞的检测,可用于检测过滤等
  2. AddActorLocalOffset的第三个参数传出碰撞结果信息:

FHitResult hitResult;

AddActorLocalOffset(LocationOffset,true,&hitResult);

UE_LOG(LogTemp, Warning, TEXT("X:%f,Y:%f,Z:%f\\n"), hitResult.Location.X, hitResult.Location.Y, hitResult.Location.Z);

常用函数与可探索部分

Get/Set——Location/Rotation/Scale

FMath数学类

创建自己的Pawn类,自己的根组件并将静态网格组件附加到其上

  1. 选择继承于Pawn创建自己的Pawn类;
  2. Pawn相比于Actor多出来一个函数,用于接收玩家输入:

virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

  1. 创建根组件

//创建根组件,可能是从父类继承来的,不需要声明而是直接赋值

RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootComponent"));

  1. 创建静态网格组件:

MyStaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MyStaticMesh"));

  1. 将静态网格附加到根组件上,包括获取根组件方式在内有多种方法:

MyStaticMesh->SetupAttachment(GetRootComponent());

MyStaticMesh->SetupAttachment(RootComponent);

MyStaticMesh->AttachTo(RootComponent);

为Pwan设置摄像机

  1. 像添加静态网格体一样作为组件添加到根组件上:

MyCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("MyCamera"));

MyCamera->SetupAttachment(RootComponent);

  1. 控制调整相机相对于根组件的位置:

MyCamera->SetRelativeLocation(FVector(-300.0f, 0.0f, 300.0f));

MyCamera->SetRelativeRotation(FRotator(-45.0f, 0.0f, 0.0f));

设置GameMode使之自动持有我们建立的Pawn

  1. GameMode是一种游戏规则的定义,可以在C++ Classes文件夹下找到,是系统自动创建出来的,我们可以对其右键创建其对应蓝图,对里边的量进行设置;
  2. 双击打开建立的蓝图类,在Class Default中找到Default Pawn Class进行设置(选择我们自己想要的)
  3. 找到场景的WorldSetting中的GameMode,选择设置我们自己创建的GameMode蓝图类
  4. 设置操控接收信息:AutoPossessPlayer = EAutoReceiveInput::Player0;

按键映射与轴事件绑定

  1. 在Edit->projectsetting->Input
  2. 间断性的动作使用Action map,连续性动作使用轴映射Axis map,如WSAD操作,前后左右移动

 1.在头文件中声明对应的前后左右移动函数,并创建定义:

void MoveForward(float Value);

void MoveRight(float Value);

void AMyPawn::MoveForward(float Value)

void AMyPawn::MoveRight(float Value)

  1. 在Pawn类的SetupPlayerInputComponent函数中绑定操作事件

PlayerInputComponent->BindAxis(TEXT("MoveForward")/*操作明,在Input中的设置*/, this/**绑定到哪个类*/, &AMyPawn::MoveForward/*响应函数的引用*/);

PlayerInputComponent->BindAxis(TEXT("MoveRight")/*操作明,在Input中的设置*/, this/**绑定到哪个类*/, &AMyPawn::MoveRight/*响应函数的引用*/);

  1. 为响应函数添加实现:进入响应函数后,修改变量Velocity的值,并在Tick里边实现调用

void AMyPawn::MoveForward(float Value)

Velocity.X = FMath::Clamp(Value, -1.0f, 1.0f)*maxSpeed;

void AMyPawn::MoveRight(float Value)

Velocity.Y = FMath::Clamp(Value, -1.0f, 1.0f)*maxSpeed;

void AMyPawn::Tick(float DeltaTime)

Super::Tick(DeltaTime);

AddActorLocalOffset(Velocity*DeltaTime, true);

  1. 控制台t.maxFPS 10(调节最大帧率)

添加SpringArm组件

  1. 类似于拍电影的摄像机悬臂:

MySpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("MySpringArm"));

MySpringArm->SetupAttachment(MyStaticMesh);

MySpringArm->SetRelativeRotation(FRotator(-45.0f, 0.0f, 0.0f));

MySpringArm->TargetArmLength = 400.0f;//作用相当于摄像机的相对位置设定

MySpringArm->bEnableCameraLag = true;//开启镜头平滑移动

MySpringArm->CameraLagSpeed = 3.0f;//平滑移动速度

MyCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("MyCamera"));

MyCamera->SetupAttachment(MySpringArm);//将摄像机附着在SpringArm上对摄像机的位置设定不需要了

  1. 注意一次编译之后可能看不出效果,要将原来创建的蓝图删除了重新创建生成等;

使用C++代码设置默认的模型Mesh和材质

  1. 加载系统的模型Mesh:

首先包含路径:#include “UObject/ConstructorHelpers.h”

  1. 对以上路径下的静态类进行调用:

static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticMeshAsset(/*资源路径*/);

static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticMeshAsset(TEXT("StaticMesh'/Engine/BasicShapes/Sphere.Sphere'"));

//材质

static ConstructorHelpers::FObjectFinder<UMaterialInterface> MaterialAsset(TEXT("Material'/Game/StarterContent/Materials/M_Metal_Gold.M_Metal_Gold'"));

  1. 进行设置:

if (StaticMeshAsset.Succeeded()&& MaterialAsset.Succeeded())

MyStaticMesh->SetStaticMesh(StaticMeshAsset.Object);

MyStaticMesh->SetMaterial(0/*材质插槽*/,MaterialAsset.Object);

  1. SprintArm的优势在于当场景出现摄像机与待观察模型之间有阻挡时,悬臂会自动调节长度保证我们可以观察到对象,而不是一直保持相对距离角度不改变;等到正常的时候自动回到设定。

将StaticMesh设置为根组件

  1. Sweep只对根组件有效
  2. 将StaticMesh的值直接给到rootComponent

MyStaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MyStaticMesh"));

RootComponent = MyStaticMesh;

控制视野上下看

  1. 使用Input设置轴映射(同前),绑定好对应的函数之后进行函数的响应计算,再到Tick中变换

void AMyPawn::LookUp(float Value)

MouseInput.Y = FMath::Clamp(Value, -1.0f, 1.0f);

void AMyPawn::LookRight(float Value)

MouseInput.X = FMath::Clamp(Value, -1.0f, 1.0f);

PlayerInputComponent->BindAxis(TEXT("LookUp"), this, &AMyPawn::LookUp);

PlayerInputComponent->BindAxis(TEXT("LookRight"), this, &AMyPawn::LookRight);

  1. Tick中对SpringArm进行旋转带动摄像机(相当于眼睛)进行旋转

//XYZ在旋转中对应Row,Pitch,Yaw

FRotator NewSpringArmRotation = MySpringArm->GetComponentRotation();

//控制抬头和低头角度范围

NewSpringArmRotation.Pitch = FMath::Clamp(NewSpringArmRotation.Pitch += MouseInput.Y, -75.0f, 75.0f);

MySpringArm->SetWorldRotation(NewSpringArmRotation);

使用Controller实现角度旋转控制

  1. 直接在Tick中调用:

//左右旋转

AddControllerYawInput(MouseInput.X);

使用系统继承自Controller的方式(前提是开启此继承)

  1. 如何开启起继承:使用系统变量bUseControllerRotationYaw = true;//或是在蓝图中进行一个勾选

注意编写代码实现的时候要注意蓝图中对应勾选栏目是否同步了

UE4渲染管线学习笔记

菜鸡入门学习笔记,各种不足还请大佬指点

CSDN传上来的图会变糊,将就看了只有,理解就行。。

个人比较推荐的学习顺序:

1.UE官方渲染介绍:

Epic Games

2.UE是如何渲染一帧的:

原文

Unreal – Interplay of Light

译:UE4是如何渲染一帧的(1) - 知乎

译:UE4是如何渲染一帧的(2) - 知乎

译:UE4是如何渲染一帧的(3) - 知乎

这里是逆向的分析,但为了防止以偏概全还是建议配合源码正向的一起看

3.官方文档:

https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/Rendering/ShaderDevelopment/

usf文件存的是HLSL,为了让引擎识别和编译着色器,需要声明VS和PS的C++类

UE4中的渲染API封装是个独立的模块,称为RHI(Render Hardware Interface)。

5.知乎大佬:

Jiff: Jiff - 知乎

巴洛克:从巴洛克到浪漫的你 - 知乎

YiVan: 虚幻4渲染编程专题概述及目录 - 知乎

Bluerose:

bluerose - 知乎

尤其推荐巴洛克大佬的文章,以下截图都源自大佬文章:

从巴洛克到浪漫的你 - 知乎

多线程渲染关注GameThred RenderThread和RHIThread。游戏线程发送命令,渲染线程和RHI线程执行。渲染线程向RHI线程发送CommondList,再由RHI线程向不同的硬件平台发送渲染Task以便对应的GPU完成运算。

 

切入点是FSceneRenderer派生出的FDeferredShadingSceneRenderer和FMobileSceneRenderer俩子类中复写的Render函数 主渲染函数

 

 

其实刚开始看这些文章的时候可能会略感迷茫,不太清楚这些内容的含义。但是没有关系,带着这些基础认知,随着之后的学习,时不时再回来翻一下看看,逐渐就能理解了。UE就是这样,越往山上走,山脚下风景的轮廓才会越清晰。

6.跟着yivan的专栏走一遍流程

虚幻4渲染编程专题概述及目录 - 知乎

yivan的文章使用的4.19,从4.19到4.27 UE的改动很大,所以用4.27的时候很多地方会有变化。像Drawing policy这种4.27中已经没有的我就跳过了,学新不学旧。理解了原理后再加上引擎源码本身有很多参考,一边学习一边修改在4.27跑通还是没问题的。

我个人的学习使用4.27,主要走的是deferred的流程,推荐学习特性更丰富的PC端流程,其和mobile的管线是有区别,但原理是互通的。学习的过程中,其实理解了原理后我们就可以自己尝试获取一些数据,实现一些功能了,这也是入门的意义所在。

以下笔记都基于4.27版本。

7.have fun

OK 了解完理论,开始实践

下面我划分了4部分内容

Part1:Global Shader和一些基础认知

官方有一个global shader的例子

https://docs.unrealengine.com/4.27/zh-CN/ProgrammingAndScripting/Rendering/ShaderDevelopment/AddingGlobalShaders/

但我感觉还是知乎大佬讲得好,bluerose大佬git上有完整的4.27可用的GlobalShader案例

https://github.com/blueroseslol/BRPlugins

我建议还是自己敲一遍,加深理解。

具体的步骤还是详见文章,我这里就记录下一些关键步骤和过程中遇到的问题

其实427UE已经全部在用RDG的流程了,但是文章里这种写法依然能生效,可以先学习,后面会遇到问题再引入RDG

在跟完yivan的前几章内容后,我们应该知道:

global shader类的基本声明方式。

C++参数和usf文件如何映射,其中的变量如何做绑定。

渲染线程函数的实现。

顶点格式的声明,顶点缓存,顶点索引。

Uniformbuffer设置。

Computeshader基础。

419的写法和现在有些不同,但yivan前几章的评论区中大家都给出了很多解决方案,灰常nice

如声明顶点输入布局类中与usf中的对应关系

 

 

 

一些需要注意的的点:

引擎声明变量的方式可参考各类源码

顶点数据的声明很重要

 

 

 

使用uniformbuffer首先是参数的声明

最后在渲染函数中用 设置buffer的值

使用Compute shader要注意自定义的SetParameters函数与自定义的UnbindBuffers函数

 SetParameters需要用到UAV类型的形参

UnbindBuffers为变量解除绑定

UAV定义

 

  

 渲染函数中声明的UAV类型和usf中的RT格式类型和外部创建的Texture2D的格式类型需要兼容,不然无法写入数据。

我做的过程中几个导致了报错需要注意的点:

1、LAYOUT_FIELD宏序列化

2、VS的输出顺序要和PS的输入顺序对上

 Signatures - Win32 apps | Microsoft Docs

 

 3、注意uniformbuffer的声明方式

4、computeshader的RHI设置 应使用如下方式调用 不然会导致声明的compute和dispatch的对不上

前几章的方式是使用插件添加的形式,而在yivan第九章的实现中。一是开始将修改放在了引擎中,二是427继续使用这种方式添加的shader会有问题。

需要注意的点:

1.Yivan第九卷自定义的文章中一是获取SceneColor的方式过时了,需要用

 2.没有使用RDG而是这种老方式声明的pass,可以看到不是RDG方式添加的pass会在最前面调用。因为DeferredShadingRenderer中的Render函数中都是RDG组织的,会在最后会调GraphBuilder.Execute()。所以用老的方式就算是写在fog后也是不行的。这里需要用RDG的方式添加。

 如之前所说。427已经是用RDG重构后的流程,所以我们想要修改引擎渲染管线就必须使用现在的方式。所以引入RDG的学习。

RDG(自动管理Pass之间的资源引用和复用,剔除没有连接到FinalColor的Pass,很好的解耦和管理系统资源占用):俩重要组件,FRDGBuilder负责构建,FRDGResource为RDG的资源类,所有资源由它派生。

官方PPT Box

【翻译】UE4 RDG系统速成课:RDG 101_ A Crash Course - 知乎

 

AddPass函数第一个参数为pass事件名,第二个为passParameters,第三个为pass的类型,第四个为Lambda函数。一个pass必须带有一个passParameters,passParameters里至少要包含此次Pass的RenderTarget

我们可以参考引擎的RenderFog学习

 

 

Pass需要用到的参数放在FFogPassParameters中,可看到uniformBuffer在里面包了一层。并且声明了RENDER_TARGET_BINDING_SLOTS的宏将MultiRenderTarget和DepthStencilTarget的输入暴露给Pass,然后在Gragh里才能Bind

 

进入RenderViewFog我们可以看到里面的操作和之前渲染函数中的实现是一样的。

 以上是global shader的情况,而类似BasePass这种mesh draw pipeline的流程427也已经都是用RDG组织的了,下面的内容会提到。

Part2:Mesh Draw Pipeline

了解了之前的GlobalShader相关的内容,接下来就可以继续到使用的更多的MaterialShader相关的学习了。我们需要学习整个Mesh Draw Pipeline的流程。

首先是需要了解的基本概念:

UPrimitiveComponent:可渲染或进行交互的资源基础类

FPrimitiveSceneProxy:UPrimitiveComponent的渲染线程版本

FPrimitiveSceneInfo:UPrimitiveComponent在渲染线程中的相关状态与FPrimitiveSceneProxy关联映射

https://www.youtube.com/watch?v=UJ6f1pm_sdU&t=1s

https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/Rendering/MeshDrawingPipeline/

这里极力推荐UE官方当时重构mesh draw pipeline的演讲和官方文档,对整个流程有一个比较详细的介绍

如之前所说,学新不学旧,所以drawing Policy就跳过了

 

所以我们要学习的是meshbatch的生成,meshPassProcessor的生成和MeshDrawCommand的生成和使用流程

Meshbatch记录了一组拥有相同MaterialRenderProxy(材质实例)和VertexFactory(顶点工厂)和FMeshBatchElement(单个网格模型的数据,包含网格渲染中所需的部分数据,如顶点、索引、Uniformbuffer及各种标识等)

Meshbatch用来解耦FPrimitiveSceneProxy与不同pass之间的联系。

如官方文档所示,FMeshBatch有Cache和Dynamic俩种生成方式

Cache方式生成meshBatch及之后的流程,以StaticMesh为例:

 

 

DrawStaticElements和之后会提到的GetDynamicMeshElements都是在FPrimitiveSceneProxy中的纯虚函数,我们可以搜索引擎中已有的继承自FPrimitiveSceneProxy的类,查看他们是如何重写的。

DrawStaticElements中的

 

 可以看到最终MeshBatch存在了PrimitiveSceneInfo->StaticMeshes中

 

生成完meshbatch后,如之前所说。接下来就是meshpassprocessor的生成。DrawStaticElements后的CacheMeshDrawCommands中会创建相应pass类型的Passprocessor并且调用AddmeshBatch,进而在各自的process中创建meshdrawCommands。

Dynamic方式生成meshBatch及之后的流程:

 

 这里的GetDynamicMeshElements如之前所说,我们可以搜索引擎中已有的继承自FPrimitiveSceneProxy的类,查看他们是如何重写。这里随便搜一个,可以看到生成的meshBatch存在了MeshElementCollector中。

 生成完meshbatch后,又到了meshpassprocessor的生成。GatherDynamicMeshElements后的SetupMeshPass中调用FMeshDrawCommandPassSetupTask::AnyThreadTask

里面的GenerateDynamicMeshDrawCommands中会调用AddmeshBatch

 

FMeshProcessor

MeshPassProcessor会选择绘制时所使用的shader,搜集这个pass绑定的顶点工厂,材质等。最后产生MeshDrawCommand。每个pass都有一个对应的MeshPassProcessor。其AddMEshBatch必须重载。

 

 

最后调用BuildMeshDrawCommand产生MeshDrawCommand

FMeshDrawCommand

 包含一个特定mesh pass draw call所需的绘制资源

ShaderBindings:各种绘制资源绑定信息

VertexStreams:当前绘制的顶点输入信息

IndexBuffer:当前绘制的IndexBuffer信息

(图自bluerose文章)

 

 

 通过打断点我们可以看到CacheMeshDrawCommands中生成PassMeshProcessor调用AddMeshBatch后,FCachedPassMeshDrawListContext::FinalizedCommand中会把commands向CachedMeshDrawCommandStateBuckets中添加。

 

 CachedMeshDrawCommand通过FDrawCommandRelevancePacket::AddCommandsForMesh来选择。上面所说的CachedMeshDrawCommandStateBuckets保存了场景里所有的CachedMeshDrawCommand,但实际绘制的时候还需要剔除各种没必要的MeshDrawCommand,最后调用AddCommandsForMesh获取Viewcommands。

ComputeAndMarkRelevanceForViewPrallel中的ViewCommands就已经是最终需要绘制的cache commands的结果了,可看到之后才到dynamic相关的流程。

而DynamicMesh的MeshDrawCommand需要每帧生成。对于dynamic方式生成的MeshDrawCommand的情况。PassMeshProcessor->AddmeshBatch中的BuildMeshDrawCommands的最后会调用MeshPassDrawListContext->FinalizeCommand

GenerateDynamicMeshDrawCommands中DrawListContext的类型是DynamicPassMeshDrawListContext。可看到其在重写的FinalizeCommand中将生成的MeshDrawCommand添加到DrawList中。

 可以看到DrawList其实就是Context.MeshDrawCommands

 

 最终对TaskContext.MeshDrawCommands进行绘制调用

了解了上述概念后,就可以跟着yivan的自定义深度的文章添加我们的自定义深度pass了

虚幻4渲染编程(Shader篇)【第十三卷:定制自己的MeshDrawPass】 - 知乎

具体的步骤还是推荐看yivan的文章,我这里就简要记录下步骤和一些改动和遇到的问题。就不放完整的截图了。

1.引擎本身DepthRendering文件就是很好的参考

2.创建我们自己的FMyDepthPassProcessor,设置uniformbuffer,重载AddMeshBatch函数,process实现

 3.创建MyDepthPass的usf和相应的c++ shader文件

4.向引擎全局manager注册我们添加的processor

5.Command cache的添加

6.SceneRenderTargets中添加自定义深度的RT,以及后续的设置。这个我们自定义的深度,后面的文章还会用到

7.DeferredShadingSceneRenderer中自定义深度绘制函数及调用

 遇到的一些问题和注意事项:

在DepthRendering中映射usf时可看到根据TDepthOnly会有多种情况

 yivan文中所使用的对应的就是这里positionOnly的情况。

 不同的情况会对应不同的VertexFactoryType,ShouldCompilePermutation中也要有对应的设置。

 我这里就和yivan一样直接走这种positionOnly的情况了,AddMeshBatch中select shader也直接走SupportsPositionOnlyStream的情况。

 

  

 同时yivan的文章中VS和PS在同一个usf中的写法427编译会有问题,因为PS中没有对应的VertexFactoryInput。解决方式就是分成俩usf就好了。

OK,截帧看没有问题,我们创建了自己的深度pass。

 

而在实际项目中,如卡渲描边需要再画一次mesh。除开自定义meshpass的方式外,还有一种通过GetDynamicElementsSection的方式实现。但这只是多绘制了一次模型,实现了类似多pass的效果,并不是真正的多pass,也获取不到渲染过程中的各类数据。但做为卡渲游戏的描边来说是没问题的 使用C++解决UE4的SkeletalMesh多Pass渲染问题 - 知乎

Part3:custom shadingmodel

【Unreal从0到1】【第三章:基于物理的渲染】3.1,Disney原则的PBR在UE4中的应用 - 知乎

尝试在UE4.22中实现罪恶装备Xrd的卡通渲染 - 知乎

UE4.26卡通材质 - 知乎

如知乎大佬所说,学习UE4的渲染主要分为C++侧的模块与组织逻辑和内建着色器框架俩部分。在着手添加自己的shadingmodel前,需要对内建着色器这部分有基础的认识。

 并且知道UE Deferred的GBuffer组织形式

Shadingmodel相关的文章很多,不同引擎版本稍微有一点区别,以下还是以427自定义原神角色效果的shadingmodel为例,简单记录下。

在EngineTypes中添加自定义shadingmodel

 

 ShadingCommon中添加ID索引

 

 HLSLMaterialTranslator中声明原神模板的宏

 

 Material.cpp中开放pin接口

MaterialShared中自定义pin接口名

 

BasePassCommon中添加原神模板宏允许CustomData写入GBuffer

 DeferredShadingCommon中添加原神索引让CustomData数据在GBuffer中生效

修改BasePassPixelShader允许原神模板中使用subsurface信息

 

 在ShadingModelsMaterial中将ID索引和HLSLMaterialTranslator中声明的宏相关联,同时将CustomData和SubsurfaceColor赋值到GBuffer.CustomData通道中

 GBuffer的数据都有了,最后就是拿数据算效果了,主要需要修改的地方是BxDF相关的计算,在ShadingModels中可看到引擎内置的各种BxDF算法,添加我们自定义的BxDF计算,最后在DeferredLightingCommon中计算光照。同时我这里也对Shadow.SurfaceShadow做了smoothstep并且做了些偏移,所以我这个效果是BxDF中算了二分光照,和SurfaceShadow是分开的,看想要啥样的效果都能在这里改就行了。

 而问题就在于,由前图可知,我们能自定义传值的通道最多也就M S R和CustomData共7个通道。PBR的流程就好在几个参数就能实现控制效果,但卡渲的参数往往非常之多。以原神为例,原神是有贴图通道划分人物的身体区域的,不同的区域像颜色,高光程度等都有单独的调整参数,以及不同的材质类型的遮罩区域,再加上顶点色。如果都要放在BxDF中计算7个通道是完全不够用的,再考虑不拓展MRT的情况下,卡渲游戏的这方面就不太占优。并且GBuffer中已经存好了各类信息,最后的计算是直接拿GBuffer信息做计算的,也就是说如原神中的rampTex采样需要用到NDL的结果,那这块计算就没法放在BxDF中去计算,所以我这里是在材质节点中计算了一次NDL采样后的RampTex颜色再输入。并且像分区的BaseColor和高光控制我也就没添加,直接用的一个统一的值了。

 

其结果就是可以实现,但不够优雅。不过能走引擎的光照流程还是比纯unlit连连看要更合理一些的。

最终效果

UE4 custom shadingmodel_哔哩哔哩_bilibili

Part4:自定义材质节点

UE4材质系统源码分析之UMaterial和材质节点介绍 - 知乎

UE4材质系统源码分析之材质编译成HLSL CODE - 知乎

虚幻4渲染编程(材质编辑器篇)【第十一卷: 自定义材质节点】 - 知乎

首先需要了解材质编译成HLSL的过程,以及如何自定义材质节点。

 可以看到引擎内的材质节点都有单独的类,基类都是UMaterialExpression。重点在于这个生成HLSL的Compile函数。

 既然都声明了自定义材质节点,那我们肯定希望是能拿到一些有意思的信息

查看默认worldPosition节点的写法,Compile中调用了MaterialCompiler中的函数,仿照其也在MaterialCompiler中添加我们自己的虚函数,以及在HLSLMaterialTranslator中重写。

已知MaterialTemplate做为一个框架,在翻译的过程中进行填充最后生成完整的shader。在MaterialTemplate中有定义相关的ush,HLSLMaterialTranslator中就可以获取到里面的字符串,然后通过AddCodeChunk方法通过字符串拼接创建新相应类型的变量返回给自定义节点输出。

 已知每一个Pass的Rendering大多有独立的VS/PS提供数据输入,以BasePassPixelShader为例。

可以看到MainPS中声明了MaterialTemplate中的FMaterialPixelParamerers结构体,在下面有一些Position相关的计算结果通过CalcMaterialParametersEx赋值。

如果是移动端流程的话,则是在MoblieBasePassPixelShader中。

而材质蓝图翻译的语句是插入在CalcMaterialParametersEx中的CalcPixelMaterialInputs中的,所以自定义材质节点中想拿到我们自己在FMaterialPixelParamerers中新增的参数,需要在调用CalcMaterialParametersEx前就赋值。

 这里说一下试错的过程,我一开始以为MainPS各种计算结果都可以返回。在调用CalcMaterialParametersEx前给自定义参数赋了个绿色,然后在GetPrecomputedIndirectLightingAndSkyLight后试图返回这个计算好的DiffuseIndirectLighting颜色,但发现输出的结果还是绿色。

 可以通过renderdoc DebugPixel查看流程(记得设置ini文件才能看到完整的HLSL)

 

可以看到在GetMaterialBaseColor后MaterialParameters中咱的参数就已经无了,下面的计算传参当然也就没用了。

 好的言归正传,那我们能获取到什么信息呢。比如在移动端,我们可以将CSM shadowmap的计算提前在CalcMaterialParametersEx之前,并将结果赋值给FMaterialPixelParamerers中我们自定义的参数,这样就能通过自定义材质节点拿到CSM的阴影信息,如可以在unlit材质里添加CSM阴影的影响。

但是Deferred的流程就不能这么做了。原因在于和Mobile阴影直接在basepass中计算不同,Deferred的shadow计算是在DeferredLightingCommon中的,和basepass都不是一个pass。并且其实UE本身也不希望你这样操作,可以看到MainPS开始后很早的时候就调用了CalcMaterialParametersEx,说明UE希望这里只做一些位置相关的计算。而如阴影相关的修改,请自己到相应的pass中在最后效果相关的计算中修改,比如加一个需要自定义阴影效果的shadingmodel之类的。

 好的,那么除开阴影。肯定还有一些别的信息是能获取到的,我这时想到了之前跟着yivan文章添加的自定义深度,下以我想要在basePass中获取到这个自定义深度的信息为例。

 之前添加自定义深度的时候我们的MySceneDepthZ就是仿照SceneDepthZ的写法添加的,那我们再看一下引擎画深度的流程。在DeferredShadingRenderer中找到Z-prepass部分,在RenderPrePass函数中可以看到调了SceneRenderTargets的Begin/EndRenderingPrepass和DispatchDraw,这样SceneDepthZ中就已经写入深度信息了。

 

 OK,有了深度RT,那之后的pass中肯定有各种计算需要用到深度,那shader要如何获取这个信息呢,没错这时候还得是咱的老朋友uniformbuffer了。可以看到这里的SceneTextureUniformParameter通过CreateSceneTextureUniformBuffer赋值后,后续的各种计算都会使用其中的buffer信息。

 

OK,知道了这些后我们来看看怎么拿我们的customdepth信息。首先创建一个uniformbuffer,然后和shader关联。再在SceneTexturesCommon中声明自定义深度的采样的函数,可以看到ScenDepthTexture也在这里采样。

 

这样HLSLMaterialTranslator中我们就能够通过字符串拼接获取到了。

 接下来,uniformbuffer还需要声明、创建、赋值、绑定和更新。

 在ScenePrivate中声明

 在PersistentUniformBuffers的Initialize函数中创建

Binding需要到mesh shader中,我们在BasePassRendering中找到TBasePassPixelShaderPolicyParamType,在其GetShaderBindings中添加绑定。并把uniform更新到指定的当前shader。

 

 

 赋值获取之前已写入好信息的MySceneDepthZ,然后UpdateUniformBufferImmediate更新。

 那么这个函数该何时调用呢,我们知道basePass主要计算是在RenderBasePass中,其核心调用函数是RenderBasePassInternal。在走basePass之前深度已经写好了,所以我们获取Depth的信息是没有问题的,所以这里我在调用BasePass的DispatchDraw前进行自定义uniformbuffer的赋值与更新。

  

 

 OK,以上操作完在引擎中材质里使用咱的自定义深度,比如这里就简单拿深度做个判断,让面片后深度范围内加自发光。截帧看拿到了自定义深度。

 

效果:

 到这我们可能会有一个疑问,为什么不透明和半透明材质都可以获取到customDepth。为什么在BasePassRendering中绑定的uniformbuffer信息,半透的mesh也可以获取到。其原因在于,Basepass半透的mesh shader和不透的mesh shader是一样的。

可以看到base和trans都是用的basePassMeshProcessor,所以里面的process绑定的shader是一样的,所以uniformbuffer也就是一致的了。

小结:学习完上面的内容,不知能不能配得上入门二字呢。长路漫漫

学习建议:耐心 思考 总结   好记性不如烂笔头

以上是关于UE C++学习笔记的主要内容,如果未能解决你的问题,请参考以下文章

UE4蓝图与C++交互——射击游戏中多武器系统的实现

问一下虚幻4引擎如果想自己做游戏是否很难,很耗时间?那个难学吗?我初中毕业后那一个暑假可以熟练掌

《Inside UE4》目录

UE4程序及资源加密保护方案

UE4 C++入门之路1-C++和蓝图的关系和介绍

[UE4虚幻引擎教程]-003-游戏框架的基本概念:第一个玩家控制器