Эта статья, мой конспект, сигнальный флаг, или очередная тренировка изложения своих мыслей? В силу обстоятельства, прикоснулся к unreal, замечательный инструмент в "умелых руках", много од написано сему творению человеческой мысли, так что взаимодействие с ним большая честь для разработчика. Создание игр, визуализация, исследования, много всего интересного заложено в этот проект с многолетней историей развития. Открытость и большое сообщество, существенно понижает порог вхождения, конечно тривиальность писать такое, каждый второй инструмент с такими характеристиками, но это говорит о общей высокой планке нынешних инструментов для реализации любых техно извращённых фантазий. Невероятное стечение обстоятельств, получаю деньги за то что учусь взаимодействовать с этим инструментом.

Коротко о задаче:

  1. Загрузить файл фотограмметрии местности

  2. Начертить путь

  3. По заданному пути двигать камеру

  4. Сохранять изображения с камеры

Главное требование: реализовать большую часть функционала программированием C++, с минимальным использованием blueprint

Изначально я сделал простой прототип используя tkinter (python) для визуализации своих мыслей, вместо файла фотограмметрии, изображение.

простая визуализация
простая визуализация
Код визуализации
from tkinter import *
from tkinter import filedialog
from tkinter import messagebox
from tkinter import ttk
from PIL import Image, ImageTk
import os
import random
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import cv2
import math
import time


width_window = 300.0
height_window = 200.0


def crop_rect(img, rect, ix):
    print("rect!")
    # поворот фрагмента
    center, size, angle = rect[0], rect[1], rect[2]
    center, size = tuple(map(int, center)), tuple(map(int, size))
    height, width = img.shape[0], img.shape[1]
    M = cv2.getRotationMatrix2D(center, angle, 1)
    img_rot = cv2.warpAffine(img[:,:,:3], M, (width, height))
    cut_img = cv2.getRectSubPix(img_rot, size, center)
    
    # сохранить часть изображения/просмотр вырезанного фрагмента
    cv2.imwrite(f'data/cut_parts/{ix}_part.jpg', cut_img)
    cv2.imshow('cut_img', cut_img)
    time.sleep(0.08)
    if cv2.waitKey(1) == ord('q'):
        cv2.destroyAllWindows()
    return img


class CutTool():
    def __init__(self, master):
        # найстройки основного окна
        self.parent = master
        self.parent.title("CutTool")
        self.frame = Frame(self.parent)
        self.frame.pack(fill=BOTH, expand=1)
        self.parent.resizable(width = True, height = True)

        # инициализировать состояние курсора мыши
        self.STATE = {}
        self.STATE['click'] = 0
        self.STATE['x'], self.STATE['y'] = 0, 0

        # ссылка на координаты 
        self.coordList = []
        self.running = False
        
        self.image_container = False
        
        self.step_size = 20
        # ----------------- GUI stuff ---------------------

        # главная панель для маркировки
        self.mainPanel = Canvas(self.frame, cursor='tcross')
        self.mainPanel.bind('<ButtonPress-1>',self.start_motor)
        self.mainPanel.bind('<ButtonRelease-1>',self.stop_motor)
        
        
        self.mainPanel.bind("<Motion>", self.mouseMove)
        self.mainPanel.grid(row = 2, column = 1, rowspan = 4, sticky = W+N)

        # панель управления для навигации по изображениям
        self.ctrPanel = Frame(self.frame)
        self.ctrPanel.grid(row = 6, columмышиn = 1, columnspan = 2, sticky = W+E)

        # отображение положения курсора мыши
        self.disp = Label(self.ctrPanel, text='')
        self.disp.pack(side = RIGHT)
        
        self.loadImage()
        
    def mouseClick(self, event):
        if self.STATE['click'] == 0:
            self.STATE['x'], self.STATE['y'] = event.x, event.y
        print ("mouseClick event --->", self.STATE)

    def mouseMove(self, event):
        if self.running:
            self.draw(event)
        self.disp.config(text = 'x: %d, y: %d' %(event.x, event.y))

    def start_motor(self, event):
        self.running = True
        print("starting motor...")
    
    def stop_motor(self, event):
        print("stopping motor...")
        self.running = False
        self.curve_2d(self.coordList)
        self.coordList = []

    def curve_2d(self, x):
        R = len(x)
        pts = x
        x = np.array(x)
        ptdiff = lambda p1, p2: (p1[0]-p2[0], p1[1]-p2[1])

        diffs = (ptdiff(p1, p2) for p1, p2 in zip (pts, pts[1:]))
        path = sum(math.hypot(*d) for d in  diffs)

        img_array = np.array(self.img)
        img_array2 = np.array(self.img)
        print (img_array.shape)
        global img_
        for ix, i in enumerate(pts):
            if ix == 0: continue
            if ix % self.step_size == 0:
                    
                """
                1 узнать угол между настоящей точкой и прошлой по отношению к оси 'x'
                2 провернуть на этот угол точки для получения прямоугольника 
                """
                a = np.array(pts[ix])
                b = np.array(pts[ix-self.step_size]) 
                ab = a - b
                hypotenuse = np.hypot(ab[0], ab[1])       
                cos_A = ab[0]/hypotenuse
                angle = int(math.degrees(math.acos(cos_A)))   
                # правильное направление             
                if ab[1] < 0:
                    angle = -float(angle)
                else:
                    angle = float(angle)
                rect_ = ((float(a[0]), float(a[1])), (height_window, width_window), angle)
                img_array2_ = crop_rect(img_array, rect_, ix)
                # отображение линии
                box = cv2.boxPoints(rect_)
                box = np.int0(box)
                cv2.drawContours(img_array2, [box], 0, (0,0,255), 2)
                cv2.circle(img_array2, (a[0], a[1]), radius=10, color=(0, 0, 255), thickness=-1)               
                img_ = ImageTk.PhotoImage(image=Image.fromarray(img_array2))
                self.mainPanel.itemconfig(self.image_container, image=img_)  
                # обновить канвас
                self.mainPanel.update()                           

    def loadImage(self):
        # загрузить изображение
        self.img = Image.open("media-info/test.png")
        size = self.img.size
        self.factor = max(size[0]/10000., size[1]/10000., 1.)
        self.img = self.img.resize((int(size[0]/self.factor), int(size[1]/self.factor)))
        print (int(size[0]/self.factor), int(size[1]/self.factor))
        self.tkimg = ImageTk.PhotoImage(self.img)
        self.mainPanel.config(width = max(self.tkimg.width(), 400), height = max(self.tkimg.height(), 400))
        self.image_container = self.mainPanel.create_image(0, 0, image = self.tkimg, anchor=NW)
        
    def draw(self, event):
        self.coordList.append((event.x, event.y))
        self.mainPanel.create_oval(event.x - 3,
                                  event.y - 3,
                                  event.x + 3,
                                  event.y + 3,
                                  fill="red", outline="red")

        
if __name__ == '__main__':
    root = Tk()
    tool = CutTool(root)
    root.resizable(width =  True, height = True)
    root.mainloop()

План есть, визуальный ориентир есть, время приключений! Установка unreal 4.27 заняла несколько дней из-за ограничений наложенных epic на мой регион обитания. Первый запуск unreal, и сразу навеяло приятные воспоминания о интерфейсе blender до 2.8 версии, и в голове: ну "гойда", снова в неизведанные дали страданий знаний. Ознакомившись с интерфейсом, перешёл к краткому ознакомлению документации C++. На вводную часть ушло несколько дней, возраст даёт о себе знать, мозг не так гибок и всячески противиться воспринимать новую информацию, так и норовит выйти из под контроля и переключиться на чтение чужих статей habr и новостей в 3dnews. Пройдя краткий курс молодого бойца, приступаю к выполнению первой задачи.

1. Загрузить файл фотограмметрии местности в unreal

отображение в Blender
отображение в Blender

Фотограмметрия мне встречалась только в статьях, поэтому никакого взаимодействия с реальными данными не было, тестовый файл полученный от новых товарищей-единомышленников был в формате непонятном unreal, .dae. Будучи автономной вычислительной системой, решил исправлять возникшую проблему самостоятельно перекодировав в нужный формат. Для этого воспользовался уже дорогим мне, программным обеспечением blender. Blender открыл файл без проблем, конвертирую данные в формат .fbx и загружаю в unreal, наблюдаю подобную проблему:

проблема - потерянные грани
проблема - потерянные грани
проблема - потерянные грани
проблема - потерянные грани

Не являюсь экспертом в 3d, любитель, этот дефект мне был не понятен, благо интернет всегда рядом. Вообще трудно представить обучение таким вещам без интернета, сколько бы потребовалось времени и сколько нужно было бы посетить библиотек, что бы решить подобную проблему. Стандартный рецепт решения из первых двух страниц поисковика, не принес желаемого результат, неправильное направление граней. Скудность моего культурного словарного запаса, не позволяет литературно описать недовольство. Я наполнился негодованием, но всё же терпение и сосредоточенность привели меня к "делаемому". За этими несколькими строками скрывается очень много перебора, хорошо что додумался автоматизировать, "большой" исходный файл это долгая процедура, производительность blender...

Код, решивший проблему
import bpy, bmesh
from mathutils import Vector, Matrix
import numpy as np 

# получить обьект из сцены
bpy.context.active_object.select_set(False)
foto_obj = bpy.context.scene.objects[0]
bpy.context.view_layer.objects.active = foto_obj
foto_obj.select_set(True)

# Выбрать все грани
me = foto_obj.data
bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.select_mode(type = 'EDGE')
bpy.ops.mesh.select_all(action = 'SELECT')
"""
Функция Beautify Faces работает только 
с выбранными существующими гранями. 
Переставляет выбранные треугольники, 
чтобы получить более «сбалансированные» 
(т.е. менее длинные и тонкие треугольники).
"""
bpy.ops.mesh.beautify_fill()
bpy.ops.mesh.normals_tools(mode="RESET")

Итоговый результат:

результат с исправленными гранями
результат с исправленными гранями

Но как всегда в нашем деле, одна проблема следует за другой, в моём случае эти проблемы связаны с отсутствием в тот момент знаний нужных для понимания происходящего. Файл фотограмметрии созданный в agisoft зашифровывает кусочки картинки в грани, это создаёт эффект объёма. В blender можно легко включить и отображать без проблем, но в unreal не смог найти способ отобразить так же как в blender, если сталкивались с подобным, поделитесь опытом. И исходного изображения у меня нет, для наложении в качестве текстуры. Не изменяя своим принципам, с этим, казусом, буду разбираться самостоятельно. Создал изображение, способом который называют запекание текстуры. Когда будите исправлять меня в комментариях, учитывайте что я не эксперт в этой области, любитель, будьте тактичней. Видео инструкция:

Получив изображение в подходящем для меня разрешении, возвращаюсь в unreal и подключаю полученную текстуру. Есть нюансы о которых я не показывал/писал, интересно узнать из комментариев о чем умолчал. Итоговый вариант:

с подключённой текстурой
с подключённой текстурой

Первый пункт плана выполнен, двигаюсь дальше. В начале статьи, поставил цель создать весь функционал используя C++ не прибегая к blueprint. Буду придерживаться этой цели, такой подход позволяет мне хорошо познакомиться с концепцией unreal. Получаемый опыт буду применять для следующего этапа работы. Естественно в процессе знакомства/поиска в интернете ответов на трудности, пришлось немного освоить bluerprint, вся идеология unreal по большей части построена на тесном взаимодействии между текстовым и визуальным программированием, такой себе симбиоз. В то же время нет ограничений, в праве выбрать один способ который подходит именно мне, для своих творческих изысканий, и всё же нужно проникнуться идеологией заложенной создателями unrel что бы правильно понимать какой подход лучше применять в определенных условиях, очень сильно влияет на производительность, о чём регулярно сообщают в своей документации разработчики из epic. Творчество в unreal силами C++ более гибкое и производительное в готовом варианте, но как и все связанное с C++, много, очень много кода, это не python. Приступаю к решению следующей задачи, после такого лирического отступления.

2. Начертить путь

Для текущей задачи буду использовать spline, логично для подобной задачи. Для большинства читающих "занесённых" в эту статью, нет ничего нового, встречается во всех популярных 3d редакторах, стандартный инструмент упрощающий техно извращения. Создаю C++ класс актер с названием FlightActor.

создание c++ класса FlightActor
создание c++ класса FlightActor

Заполняю полученные файлы кодом:

FlightActor.h
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "aboveActor.generated.h"

UCLASS()
class ABOVE2_API AaboveActor : public AActor
{
	GENERATED_BODY()

public:	
	// Sets default values for this actor's properties
    AaboveActor();
    class USplineComponent* MySpline;
    class APlayerController* MyPlayerController;

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
    virtual void Tick(float DeltaTime) override;
    virtual void PostActorCreated() override;
    void SetPosition();		
};

FlightActor.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "aboveActor.h"
#include "Logging/LogMacros.h"
#include "Components/SplineComponent.h"
#include "Kismet/GameplayStatics.h"

#include "Components/SplineMeshComponent.h"
#include "Components/SceneComponent.h"
#include "Engine/StaticMesh.h"
#include "Containers/Array.h"


// Sets default values
AaboveActor::AaboveActor()
{
 	// 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;
    // Add spline to actor
    this->MySpline = CreateDefaultSubobject<USplineComponent>("MySpline",true);
    SetRootComponent(this->MySpline);
    this->MySpline->bDrawDebug = true;
    this->MyPlayerController = UGameplayStatics::GetPlayerController(this, 0);
    int32 NumberPoints = MySpline->GetNumberOfSplinePoints();
    UE_LOG(LogTemp, Warning, TEXT("Test Log. Num Points %d"), NumberPoints); //Display
}

// Called when the game starts or when spawned
void AaboveActor::BeginPlay()
{
	Super::BeginPlay();
    float LenGth_s = this->MySpline->GetSplineLength();
    FVector actLocation = this->GetActorLocation();
    UE_LOG(LogTemp, Warning, TEXT("SplineLength-->%f, Location-->%s"), LenGth_s, *actLocation.ToString());
}

// Called every frame
void AaboveActor::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}

// This is called when actor is spawned (at runtime or when you drop it into the world in editor)
void AaboveActor::PostActorCreated()
{
    Super::PostActorCreated();
    SetPosition();
}

void AaboveActor::SetPosition()
{
    FVector startPos = FVector(0.0f,0.0f,20.0f);
    this->SetActorLocation(startPos);
    UE_LOG(LogTemp, Warning, TEXT("START THIS !!!"));
}

Компилирую, радуюсь первому рабочему результату в unreal. Переопределил PostActorCreated() для автоматического позиционирования объекта в нужные координаты и добавил spline к актёру. К счастью моё руководство не про теорию, а про практическое решение поставленной задачи, и я в праве не писать о структурных особенностях объектов, почему так а не иначе лучше получить ответ самостоятельно. Все работает, теперь нужно изменить код непосредственно для работы в моей задаче. Код покажется знакомым, и это не удивительно, учебные пособия для всех одинаковы...

FlightActor.h
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Components/SplineComponent.h"
#include "DrawDebugHelpers.h"
#include "FlightActor.generated.h"

UCLASS()
class ABOVE2_API AFlightActor : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AFlightActor();

    /** Returns the next flight curve */
    UCurveFloat* GetFlightCurve() { return FlightCurve; };

    /** Returns the next flight spline component */
    USplineComponent* GetFlightSplineComp() { return FlightComp; };

protected:
    /** The FloatCurve corresponding to the next flight spline component */
    UPROPERTY(EditAnywhere)
    UCurveFloat* FlightCurve;

    /** A static mesh for our flight stop */
    UPROPERTY(VisibleAnywhere)
    UStaticMeshComponent* SM;

    /** The spline component that describes the flight path of the next flight */
    UPROPERTY(VisibleAnywhere)
    USplineComponent* FlightComp;
};

FlightActor.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "FlightActor.h"
#include "Logging/LogMacros.h"

// Sets default values
AFlightActor::AFlightActor()
{
 	// 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;

    SM = CreateDefaultSubobject<UStaticMeshComponent>(FName("SM"));
    SetRootComponent(SM);

    //Init splines
    FlightComp = CreateDefaultSubobject<USplineComponent>(FName("SplineComp"));

    //Attach them to root component
    FlightComp->SetupAttachment(SM);
}

3. По заданному пути двигать камеру

В предыдущем этапе создал программно spline для построения траектории желаемого маршрута, к полученному нужно прикрепить объект для движения по траектории. В этот раз создаю класс character, полученные файлы привожу к такому виду:

FlightC.h
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Components/TimelineComponent.h"
#include "Components/BoxComponent.h"
#include "GameFramework/Character.h"
#include "FlightActor.h"
#include "CaptureManager.h" // еще не создано
#include "FlightC.generated.h"

UCLASS(config=Game)
class ABOVE2_API AFlightC : public ACharacter
{
	GENERATED_BODY()

   UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
   class USpringArmComponent* CameraBoom;

   UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
   class UCameraComponent* FollowCamera;

public:
	// Sets default values for this character's properties
	AFlightC();

    FTimeline FlightTimeline;

    UFUNCTION()
    void TickTimeline(float Value);

    /** Base turn rate, in deg/sec. Other scaling may affect final turn rate. */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera)
    float BaseTurnRate;

    /** Base look up/down rate, in deg/sec. Other scaling may affect final rate. */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Camera)
    float BaseLookUpRate;

    // еще не создано
    UPROPERTY(EditAnywhere)
    class ACaptureManager* CaptureActor;  //Create a pointer to your another class

    /** The active spline component, meaning the flight path that the character is currently following */
    USplineComponent* ActiveSplineComponent;

    /** The selected flight stop actor */
    AFlightActor* ActiveFlightActor;
    /** Box overlap function */
    UFUNCTION()
    void OnFlightBoxColliderOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);

    /** Executes when we're pressing the FlightPath key bind */
    void FlightPathSelected();

    /** Updates the flight timeline with a new curve and starts the flight */
    void UpdateFlightTimeline(UCurveFloat* CurveFloatToBind);

    UFUNCTION()
    void ResetActiveFlightActor();


protected:
	// Called when the game starts or when spawned
    virtual void BeginPlay() override;

    /*The Box component that detects any nearby flight stops*/
    UPROPERTY(VisibleAnywhere)
    UBoxComponent* FlightBoxCollider;

    /** Called for forwards/backward input */
    void MoveForward(float Value);

    /** Called for side to side input */
    void MoveRight(float Value);

    /**
     * Called via input to turn at a given rate.
     * @param Rate	This is a normalized rate, i.e. 1.0 means 100% of desired turn rate
     */
    void TurnAtRate(float Rate);

    /**
     * Called via input to turn look up/down at a given rate.
     * @param Rate	This is a normalized rate, i.e. 1.0 means 100% of desired turn rate
     */
    void LookUpAtRate(float Rate);

    /** Handler for when a touch input begins. */
    void TouchStarted(ETouchIndex::Type FingerIndex, FVector Location);

    /** Handler for when a touch input stops. */
    void TouchStopped(ETouchIndex::Type FingerIndex, FVector Location);


public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

	// Called to bind functionality to input
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

    /** Returns CameraBoom subobject **/
    FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
    /** Returns FollowCamera subobject **/
    FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }
};

FlightC.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "FlightC.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/SceneCaptureComponent2D.h"
#include "HeadMountedDisplayFunctionLibrary.h"
#include "Kismet/GameplayStatics.h"
#include "CaptureManager.h"
#include "Logging/LogMacros.h"


// Sets default values
AFlightC::AFlightC()
{
 	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true;

    // Set size for collision capsule
    GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);

    // set our turn rates for input
    BaseTurnRate = 45.f;
    BaseLookUpRate = 45.f;

    // Don't rotate when the controller rotates. Let that just affect the camera.
    bUseControllerRotationPitch = false;
    bUseControllerRotationYaw = false;
    bUseControllerRotationRoll = false;

    // Create a camera boom (pulls in towards the player if there is a collision)
    CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
    CameraBoom->SetupAttachment(RootComponent);
    CameraBoom->TargetArmLength = 300.0f; // The camera follows at this distance behind the character
    CameraBoom->bUsePawnControlRotation = true; // Rotate the arm based on the controller

    // Create a follow camera
    FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
    FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); // Attach the camera to the end of the boom and let the boom adjust to match the controller orientation
    FollowCamera->bUsePawnControlRotation = false; // Camera does not rotate relative to arm

    // Note: The skeletal mesh and anim blueprint references on the Mesh component (inherited from Character)
    // are set in the derived blueprint asset named MyCharacter (to avoid direct content references in C++)
    FlightBoxCollider = CreateDefaultSubobject<UBoxComponent>(FName("FlightBoxCollider"));
    FlightBoxCollider->SetBoxExtent(FVector(150.f));
    FlightBoxCollider->SetupAttachment(GetRootComponent());

}


// Called when the game starts or when spawned
void AFlightC::BeginPlay()
{
    Super::BeginPlay();
    //Register a function that gets called when the box overlaps with a component
    FlightBoxCollider->OnComponentBeginOverlap.AddDynamic(this, &AFlightC::OnFlightBoxColliderOverlap);

    // еще не создано, см. 4 пункт  
    // https://code911.top/howto/unreal-how-to-delete-link-code-example
    AActor* FoundActor = UGameplayStatics::GetActorOfClass(GetWorld(), ACaptureManager::StaticClass());
    CaptureActor = Cast<ACaptureManager>(FoundActor);
    if (CaptureActor)
    {
        UE_LOG(LogTemp, Warning, TEXT("CaptureActor.... %s"), *CaptureActor->GetFName().ToString());
    }

}

// Called every frame
void AFlightC::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

    //If the timeline has started, advance it by DeltaSeconds
    if (FlightTimeline.IsPlaying()) FlightTimeline.TickTimeline(DeltaTime);

}

// Called to bind functionality to input
void AFlightC::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    // Set up gameplay key bindings
    check(PlayerInputComponent);
    PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump);
    PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);

    PlayerInputComponent->BindAxis("MoveForward", this, &AFlightC::MoveForward);
    PlayerInputComponent->BindAxis("MoveRight", this, &AFlightC::MoveRight);

    // Bind the functions that execute on key press
    PlayerInputComponent->BindAction("FlightPath", IE_Pressed, this, &AFlightC::FlightPathSelected);

    // We have 2 versions of the rotation bindings to handle different kinds of devices differently
    // "turn" handles devices that provide an absolute delta, such as a mouse.
    // "turnrate" is for devices that we choose to treat as a rate of change, such as an analog joystick
    PlayerInputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput);
    PlayerInputComponent->BindAxis("TurnRate", this, &AFlightC::TurnAtRate);
    PlayerInputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput);
    PlayerInputComponent->BindAxis("LookUpRate", this, &AFlightC::LookUpAtRate);

    // handle touch devices
    PlayerInputComponent->BindTouch(IE_Pressed, this, &AFlightC::TouchStarted);
    PlayerInputComponent->BindTouch(IE_Released, this, &AFlightC::TouchStopped);

}

void AFlightC::TickTimeline(float Value)
{
    float SplineLength = ActiveSplineComponent->GetSplineLength();
    // Get the new location based on the provided values from the timeline.
    // The reason we're multiplying Value with SplineLength is because all our designed curves in the UE4 editor have a time range of 0 - X.
    // Where X is the total flight time
    FVector NewLocation = ActiveSplineComponent->GetLocationAtDistanceAlongSpline(Value * SplineLength, ESplineCoordinateSpace::World);

    SetActorLocation(NewLocation);

    FRotator NewRotation = ActiveSplineComponent->GetRotationAtDistanceAlongSpline(Value * SplineLength, ESplineCoordinateSpace::World);

    //We're not interested in the pitch value of the above rotation so we make sure to set it to zero
    NewRotation.Pitch = 0;
    SetActorRotation(NewRotation);

    // еще не создано, см. 4 пункт 
    CaptureActor->TestFunc(NewLocation);

}

FVector GetSplinePointLocationInConstructionScript(USplineComponent* SplineComponent, int32 PointIndex)
{
// Check if the SplineComponent is valid
if (!SplineComponent)
{
    UE_LOG(LogTemp, Error, TEXT("Invalid SplineComponent"));
    return FVector::ZeroVector;
}

// Check if the PointIndex is valid
if (PointIndex < 0 || PointIndex >= SplineComponent->GetNumberOfSplinePoints())
{
    UE_LOG(LogTemp, Error, TEXT("Invalid PointIndex"));
    return FVector::ZeroVector;
}

// Get the location of the spline point
FVector SplinePointLocation = SplineComponent->GetLocationAtSplinePoint(PointIndex, ESplineCoordinateSpace::World);
return SplinePointLocation;
}


void AFlightC::OnFlightBoxColliderOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    if (OtherActor->IsA<AFlightActor>())
    {
        //Store a reference of the nearby flight stop actor
        ActiveFlightActor = Cast<AFlightActor>(OtherActor);
    }
}
void AFlightC::FlightPathSelected()
{
    GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Orange, TEXT("KLICK BUTTON"));

    // get actor in scene by class
    AActor* FoundActor = UGameplayStatics::GetActorOfClass(GetWorld(), AFlightActor::StaticClass());
    AFlightActor* GameManager = Cast<AFlightActor>(FoundActor);

    ActiveFlightActor = Cast<AFlightActor>(GameManager);

    if (ActiveFlightActor)
    {
        //Get the next flight path's spline component and update the flight timeline with the corresponding curve
        ActiveSplineComponent = ActiveFlightActor->GetFlightSplineComp();
        UpdateFlightTimeline(ActiveFlightActor->GetFlightCurve());
    }
}

void AFlightC::UpdateFlightTimeline(UCurveFloat* CurveFloatToBind)
{
    UE_LOG(LogTemp, Warning, TEXT("...UpdateFlightTimeline"));
    //Initialize a timeline
    FlightTimeline = FTimeline();

    FOnTimelineFloat ProgressFunction;

    //Bind the function that ticks the timeline
    ProgressFunction.BindUFunction(this, FName("TickTimeline"));

    //Assign the provided curve and progress function for our timeline
    FlightTimeline.AddInterpFloat(CurveFloatToBind, ProgressFunction);
    FlightTimeline.SetLooping(false);
    FlightTimeline.PlayFromStart();

    //Set the timeline's length to match the last key frame based on the given curve
    FlightTimeline.SetTimelineLengthMode(TL_LastKeyFrame);

    //The ResetActiveFlightActor executes when the timeline finishes.
    //By calling ResetActiveFlightActor at the end of the timeline we make sure to reset any invalid references on ActiveFlightActor
    FOnTimelineEvent TimelineEvent;
    TimelineEvent.BindUFunction(this, FName("ResetActiveFlightActor"));
    FlightTimeline.SetTimelineFinishedFunc(TimelineEvent);
}

void AFlightC::ResetActiveFlightActor()
{
    UE_LOG(LogTemp, Warning, TEXT("...ResetActiveFlightActor")); //Display
    ActiveFlightActor = nullptr;
}

void AFlightC::TouchStarted(ETouchIndex::Type FingerIndex, FVector Location)
{
    // jump, but only on the first touch
    if (FingerIndex == ETouchIndex::Touch1)
    {
        Jump();
    }
}

void AFlightC::TouchStopped(ETouchIndex::Type FingerIndex, FVector Location)
{
    if (FingerIndex == ETouchIndex::Touch1)
    {
        StopJumping();
    }
}

void AFlightC::TurnAtRate(float Rate)
{
    // calculate delta for this frame from the rate information
    AddControllerYawInput(Rate * BaseTurnRate * GetWorld()->GetDeltaSeconds());
}

void AFlightC::LookUpAtRate(float Rate)
{
    // calculate delta for this frame from the rate information
    AddControllerPitchInput(Rate * BaseLookUpRate * GetWorld()->GetDeltaSeconds());
}

void AFlightC::MoveForward(float Value)
{
    if ((Controller != NULL) && (Value != 0.0f))
    {
        // find out which way is forward
        const FRotator Rotation = Controller->GetControlRotation();
        const FRotator YawRotation(0, Rotation.Yaw, 0);

        // get forward vector
        const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
        AddMovementInput(Direction, Value);
    }
}

void AFlightC::MoveRight(float Value)
{
    if ( (Controller != NULL) && (Value != 0.0f) )
    {
        // find out which way is right
        const FRotator Rotation = Controller->GetControlRotation();
        const FRotator YawRotation(0, Rotation.Yaw, 0);

        // get right vector
        const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
        // add movement in that direction
        AddMovementInput(Direction, Value);
    }
}

Подключаю компонент сapsule к классу character, это зона в которой при нажатии на кнопку начнётся движение, по spline созданном в задача 2. К объекту подключаю компонент записывающей камеры из следующей задача 4. В функции TickTimeline() изменяю расположение character получая данные из spline в n-шаге, и выполняю CaptureActor->TestFunc(NewLocation), передав текущие координаты FlightC в CaptureManager (задача 4).

4. Сохранять изображения с камеры

С этой частью мне повезло, рабочий код, представлен несколькими вариантами в разных репозиториях, выбрал этот https://github.com/TimmHess/UnrealImageCapture. Автор решения, позаботился о асинхронном выполнении функции, сохранения изображения, что очень хорошо. К этому моменту накопилось достаточно опыта работы с unreal и подключение нужных компонентов не создало трудностей. Один взгляд на код и уже есть понимание что происходит...

CaptureManager.h
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

class ASceneCapture2D;
class UMaterial;

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Containers/Queue.h"
#include "CaptureManager.generated.h"


USTRUCT()
struct FRenderRequestStruct{
    GENERATED_BODY()

    TArray<FColor> Image;
    FRenderCommandFence RenderFence;

    FRenderRequestStruct(){

    }
};


USTRUCT()
struct FFloatRenderRequestStruct{
    GENERATED_BODY()

    TArray<FFloat16Color> Image;
    FRenderCommandFence RenderFence;

    FFloatRenderRequestStruct(){

    }
};


UCLASS(Blueprintable)
class ABOVE2_API  ACaptureManager : public AActor
{
    GENERATED_BODY()

public:
    // Sets default values for this actor's properties
     ACaptureManager();

    // Captured Data Sub-Directory Name
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture")
    FString SubDirectoryName = "";

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture")
    int NumDigits = 6;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture")
    int FrameWidth = 640;
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture")
    int FrameHeight = 480;

    // If not UsePNG, JPEG format is used (For Non-Color purposes PNG is necessary, elsewise compression will mess with labels!)
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture")
    bool UsePNG = false;

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture")
    bool UseFloat = false;

    // Color Capture Components
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture")
    ASceneCapture2D* CaptureComponent;

    //UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Capture")
    //ASceneCapture2D* SegmentationCapture = nullptr;

    // PostProcessMaterial used for segmentation
    UPROPERTY(EditAnywhere, Category="Capture")
    UMaterial* PostProcessMaterial = nullptr;

    UPROPERTY(EditAnywhere, Category="Logging")
    bool VerboseLogging = false;

protected:
    // RenderRequest Queue
    TQueue<FRenderRequestStruct*> RenderRequestQueue;

    // FloatRenderRequest Queue
    TQueue<FFloatRenderRequestStruct*> RenderFloatRequestQueue;
    int ImgCounter = 0;

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

    void SetupCaptureComponent();

    // Creates an async task that will save the captured image to disk
    void RunAsyncImageSaveTask(TArray64<uint8> Image, FString ImageName);
    FString ToStringWithLeadingZeros(int32 Integer, int32 MaxDigits);

public:
    // Called every frame
    virtual void Tick(float DeltaTime) override;

    UFUNCTION(BlueprintCallable, Category = "ImageCapture")
    void CaptureNonBlocking();

    UFUNCTION(BlueprintCallable, Category = "ImageCapture")
    void CaptureFloatNonBlocking();
public:
    UFUNCTION()
    void TestFunc(FVector NewLocation); //FVector NewLocation

};


class AsyncSaveImageToDiskTask : public FNonAbandonableTask{
    public:
        AsyncSaveImageToDiskTask(TArray64<uint8> Image, FString ImageName);
        ~AsyncSaveImageToDiskTask();

    // Required by UE4!
    FORCEINLINE TStatId GetStatId() const{
        RETURN_QUICK_DECLARE_CYCLE_STAT(AsyncSaveImageToDiskTask, STATGROUP_ThreadPoolAsyncTasks);
    }

protected:
    TArray64<uint8> ImageCopy;
    FString FileName = "";

public:
    void DoWork();
};

CaptureManager.cpp
// Fill out your copyright notice in the Description page of Project Settings.

#include "CaptureManager.h"
#include "Runtime/Engine/Classes/Engine/Engine.h"

#include "Engine/SceneCapture2D.h"
#include "Components/SceneCaptureComponent2D.h"
#include "Engine/TextureRenderTarget2D.h"
#include "Kismet/GameplayStatics.h"
#include "ShowFlags.h"

#include "Materials/Material.h"

#include "RHICommandList.h"

#include "ImageWrapper/Public/IImageWrapper.h"
#include "ImageWrapper/Public/IImageWrapperModule.h"

#include "ImageUtils.h"

#include "Modules/ModuleManager.h"
#include "FlightC.h"

#include "Misc/FileHelper.h"

// Sets default values
ACaptureManager::ACaptureManager()
{
    // 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 ACaptureManager::BeginPlay()
{
    Super::BeginPlay();

    if(CaptureComponent){ // nullptr check
        SetupCaptureComponent();
        UE_LOG(LogTemp, Warning, TEXT("CaptureComponent.... %s"), *CaptureComponent->GetFName().ToString());
    } else{
        UE_LOG(LogTemp, Error, TEXT("No CaptureComponent set!"));
    }

}

// Called every frame
void ACaptureManager::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    if(UseFloat){
        // READ FLOAT IMAGE
        if(!RenderFloatRequestQueue.IsEmpty()){
            // Peek the next RenderRequest from queue
            FFloatRenderRequestStruct* nextRenderRequest = nullptr;
            RenderFloatRequestQueue.Peek(nextRenderRequest);

            if(nextRenderRequest){
                if(nextRenderRequest->RenderFence.IsFenceComplete()){
                    // Load the image wrapper module
                    IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));

                    FString fileName = "";
                    fileName = FPaths::ProjectSavedDir() + SubDirectoryName + "/img" + "_" + ToStringWithLeadingZeros(ImgCounter, NumDigits);
                    fileName += ".exr"; // Add file ending

                    static TSharedPtr<IImageWrapper> imageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::EXR); //EImageFormat::PNG //EImageFormat::JPEG
                    imageWrapper->SetRaw(nextRenderRequest->Image.GetData(), nextRenderRequest->Image.GetAllocatedSize(), FrameWidth, FrameHeight, ERGBFormat::RGBA, 16);
                    const TArray64<uint8>& PngData = imageWrapper->GetCompressed(0);
                    FFileHelper::SaveArrayToFile(PngData, *fileName);

                     // Delete the first element from RenderQueue
                    RenderFloatRequestQueue.Pop();
                    delete nextRenderRequest;

                    ImgCounter += 1;
                }
            }
        }
    }
    // READ UINT8 IMAGE
    // Read pixels once RenderFence is completed
    else{
        if(!RenderRequestQueue.IsEmpty()){
            // Peek the next RenderRequest from queue
            FRenderRequestStruct* nextRenderRequest = nullptr;
            RenderRequestQueue.Peek(nextRenderRequest);
            if(nextRenderRequest){ //nullptr check
                if(nextRenderRequest->RenderFence.IsFenceComplete()){ // Check if rendering is done, indicated by RenderFence
                    // Load the image wrapper module
                    IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
                    // Decide storing of data, either jpeg or png
                    FString fileName = "";
                    if(UsePNG){
                        //Generate image name
                        fileName = FPaths::ProjectSavedDir() + SubDirectoryName + "/img" + "_" + ToStringWithLeadingZeros(ImgCounter, NumDigits);
                        fileName += ".png"; // Add file ending

                        // Prepare data to be written to disk
                        static TSharedPtr<IImageWrapper> imageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG); //EImageFormat::PNG //EImageFormat::JPEG
                        imageWrapper->SetRaw(nextRenderRequest->Image.GetData(), nextRenderRequest->Image.GetAllocatedSize(), FrameWidth, FrameHeight, ERGBFormat::BGRA, 8);
                        const TArray64<uint8>& ImgData = imageWrapper->GetCompressed(5);
                        //const TArray<uint8>& ImgData =  static_cast<TArray<uint8, FDefaultAllocator>> (imageWrapper->GetCompressed(5));
                        RunAsyncImageSaveTask(ImgData, fileName);
                    } else{
                        // Generate image name
                        fileName = FPaths::ProjectSavedDir() + SubDirectoryName + "/img" + "_" + ToStringWithLeadingZeros(ImgCounter, NumDigits);
                        fileName += ".jpeg"; // Add file ending

                        // Prepare data to be written to disk
                        static TSharedPtr<IImageWrapper> imageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::JPEG); //EImageFormat::PNG //EImageFormat::JPEG
                        imageWrapper->SetRaw(nextRenderRequest->Image.GetData(), nextRenderRequest->Image.GetAllocatedSize(), FrameWidth, FrameHeight, ERGBFormat::BGRA, 8);
                        const TArray64<uint8>& ImgData = imageWrapper->GetCompressed(0);
                        //const TArray<uint8>& ImgData = static_cast<TArray<uint8, FDefaultAllocator>> (imageWrapper->GetCompressed(0));
                        RunAsyncImageSaveTask(ImgData, fileName);
                    }
                    if(VerboseLogging && !fileName.IsEmpty()){
                        UE_LOG(LogTemp, Warning, TEXT("%f"), *fileName);
                    }

                    ImgCounter += 1;

                    // Delete the first element from RenderQueue
                    RenderRequestQueue.Pop();
                    delete nextRenderRequest;
                }
            }
        }
    }
}

void ACaptureManager::SetupCaptureComponent(){
    if(!IsValid(CaptureComponent)){
        UE_LOG(LogTemp, Error, TEXT("SetupCaptureComponent: CaptureComponent is not valid!"));
        return;
    }

    // Create RenderTargets
    UTextureRenderTarget2D* renderTarget2D = NewObject<UTextureRenderTarget2D>();

    // Float Capture
    if(UseFloat){
        renderTarget2D->RenderTargetFormat = ETextureRenderTargetFormat::RTF_RGBA32f;
        renderTarget2D->InitCustomFormat(FrameWidth, FrameHeight, PF_FloatRGBA, true); // PF_B8G8R8A8 disables HDR which will boost storing to disk due to less image information
        UE_LOG(LogTemp, Warning, TEXT("Set Render Format for DepthCapture.."));
    }
    // Color Capture
    else{
        renderTarget2D->RenderTargetFormat = ETextureRenderTargetFormat::RTF_RGBA8; //8-bit color format
        renderTarget2D->InitCustomFormat(FrameWidth, FrameHeight, PF_B8G8R8A8, true); // PF... disables HDR, which is most important since HDR gives gigantic overhead, and is not needed!
        UE_LOG(LogTemp, Warning, TEXT("Set Render Format for Color-Like-Captures"));
    }

    renderTarget2D->bGPUSharedFlag = true; // demand buffer on GPU

    // Assign RenderTarget
    CaptureComponent->GetCaptureComponent2D()->TextureTarget = renderTarget2D;
    // Set Camera Properties
    CaptureComponent->GetCaptureComponent2D()->CaptureSource = ESceneCaptureSource::SCS_FinalColorLDR;
    CaptureComponent->GetCaptureComponent2D()->TextureTarget->TargetGamma = GEngine->GetDisplayGamma();
    CaptureComponent->GetCaptureComponent2D()->ShowFlags.SetTemporalAA(true);
    // lookup more showflags in the UE4 documentation..

    // Assign PostProcess Material if assigned
    if(PostProcessMaterial){ // check nullptr
        CaptureComponent->GetCaptureComponent2D()->AddOrUpdateBlendable(PostProcessMaterial);
    } else {
        UE_LOG(LogTemp, Log, TEXT("No PostProcessMaterial is assigend"));
    }
    UE_LOG(LogTemp, Warning, TEXT("Initialized RenderTarget!"));


    FVector startPos = FVector(0.0f,0.0f,20.0f);
    CaptureComponent->SetActorLocation(startPos);
    UE_LOG(LogTemp, Error, TEXT("New ActorLocation ------>%s"), *CaptureComponent->GetActorLocation().ToString());
}

void ACaptureManager::CaptureNonBlocking(){
    if(!IsValid(CaptureComponent)){
        UE_LOG(LogTemp, Error, TEXT("CaptureColorNonBlocking: CaptureComponent was not valid!"));
        return;
    }
    UE_LOG(LogTemp, Warning, TEXT("Entering: CaptureNonBlocking"));
    CaptureComponent->GetCaptureComponent2D()->TextureTarget->TargetGamma = 1.2f;
    // CaptureComponent->GetCaptureComponent2D()->TextureTarget->TargetGamma = GEngine->GetDisplayGamma();

    // Get RenderConterxt
    FTextureRenderTargetResource* renderTargetResource = CaptureComponent->GetCaptureComponent2D()->TextureTarget->GameThread_GetRenderTargetResource();
    UE_LOG(LogTemp, Warning, TEXT("Got display gamma"));
    struct FReadSurfaceContext{
        FRenderTarget* SrcRenderTarget;
        TArray<FColor>* OutData;
        FIntRect Rect;
        FReadSurfaceDataFlags Flags;
    };
    UE_LOG(LogTemp, Warning, TEXT("Inited ReadSurfaceContext"));
    // Init new RenderRequest
    FRenderRequestStruct* renderRequest = new FRenderRequestStruct();
    UE_LOG(LogTemp, Warning, TEXT("inited renderrequest"));

    // Setup GPU command
    FReadSurfaceContext readSurfaceContext = {
        renderTargetResource,
        &(renderRequest->Image),
        FIntRect(0,0,renderTargetResource->GetSizeXY().X, renderTargetResource->GetSizeXY().Y),
        FReadSurfaceDataFlags(RCM_UNorm, CubeFace_MAX)
    };
    UE_LOG(LogTemp, Warning, TEXT("GPU Command complete"));

    // Send command to GPU
    /* Up to version 4.22 use this
    ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER(
        SceneDrawCompletion,//ReadSurfaceCommand,
        FReadSurfaceContext, Context, readSurfaceContext,
    {
        RHICmdList.ReadSurfaceData(
            Context.SrcRenderTarget->GetRenderTargetTexture(),
            Context.Rect,
            *Context.OutData,
            Context.Flags
        );
    });
    */
    // Above 4.22 use this
    ENQUEUE_RENDER_COMMAND(SceneDrawCompletion)(
    [readSurfaceContext](FRHICommandListImmediate& RHICmdList){
        RHICmdList.ReadSurfaceData(
            readSurfaceContext.SrcRenderTarget->GetRenderTargetTexture(),
            readSurfaceContext.Rect,
            *readSurfaceContext.OutData,
            readSurfaceContext.Flags
        );
    });

    // Notifiy new task in RenderQueue
    RenderRequestQueue.Enqueue(renderRequest);

    // Set RenderCommandFence
    renderRequest->RenderFence.BeginFence();
}

void ACaptureManager::CaptureFloatNonBlocking(){
    // Initial Check
    if(!UseFloat){
        UE_LOG(LogTemp, Error, TEXT("Called CaptureFloatNonBlocking but UseFloat is false! Will omit this call to prevent crashes!"));
        return;
    }

    // Get RenderContext
    FTextureRenderTargetResource* renderTargetResource = CaptureComponent->GetCaptureComponent2D()->TextureTarget->GameThread_GetRenderTargetResource();

    // Read the render target surface data back.
    struct FReadSurfaceFloatContext
    {
        FRenderTarget* SrcRenderTarget;
        TArray<FFloat16Color>* OutData;
        FIntRect Rect;
        ECubeFace CubeFace;
    };

    // Init new RenderRequest
    FFloatRenderRequestStruct* renderFloatRequest = new FFloatRenderRequestStruct();

    // Setup GPU command
    //TArray<FFloat16Color> SurfaceData;
    FReadSurfaceFloatContext Context = {
        renderTargetResource,
        &(renderFloatRequest->Image),
        //&SurfaceData,
        FIntRect(0, 0, FrameWidth, FrameHeight),
        ECubeFace::CubeFace_MAX //no cubeface
    };

    ENQUEUE_RENDER_COMMAND(ReadSurfaceFloatCommand)(
        [Context](FRHICommandListImmediate& RHICmdList) {
            RHICmdList.ReadSurfaceFloatData(
                Context.SrcRenderTarget->GetRenderTargetTexture(),
                Context.Rect,
                *Context.OutData,
                Context.CubeFace,
                0,
                0
                );
        });

    RenderFloatRequestQueue.Enqueue(renderFloatRequest);
    renderFloatRequest->RenderFence.BeginFence();
}

FString ACaptureManager::ToStringWithLeadingZeros(int32 Integer, int32 MaxDigits){
    FString result = FString::FromInt(Integer);
    int32 stringSize = result.Len();
    int32 stringDelta = MaxDigits - stringSize;
    if(stringDelta < 0){
        UE_LOG(LogTemp, Error, TEXT("MaxDigits of ImageCounter Overflow!"));
        return result;
    }
    //FIXME: Smarter function for this..
    FString leadingZeros = "";
    for(size_t i=0;i<stringDelta;i++){
        leadingZeros += "0";
    }
    result = leadingZeros + result;

    return result;
}

void ACaptureManager::RunAsyncImageSaveTask(TArray64<uint8> Image, FString ImageName){
    UE_LOG(LogTemp, Warning, TEXT("Running Async Task"));
    (new FAutoDeleteAsyncTask<AsyncSaveImageToDiskTask>(Image, ImageName))->StartBackgroundTask();
}

void ACaptureManager::TestFunc(FVector NewLocation){
    UE_LOG(LogTemp, Warning, TEXT("ACaptureManager's Name is %s"), *this->GetFName().ToString());
    UE_LOG(LogTemp, Warning, TEXT("Location CaptureManager %s"), *this->CaptureComponent->GetActorLocation().ToString());
    CaptureComponent->SetActorLocation(NewLocation);
    CaptureNonBlocking();
}

/*
*******************************************************************
*/

AsyncSaveImageToDiskTask::AsyncSaveImageToDiskTask(TArray64<uint8> Image, FString ImageName){
    ImageCopy = Image;
    FileName = ImageName;
}

AsyncSaveImageToDiskTask::~AsyncSaveImageToDiskTask(){
    UE_LOG(LogTemp, Warning, TEXT("AsyncTaskDone"));
}

void AsyncSaveImageToDiskTask::DoWork(){
    UE_LOG(LogTemp, Warning, TEXT("Starting Work"));
    FFileHelper::SaveArrayToFile(ImageCopy, *FileName);
    UE_LOG(LogTemp, Log, TEXT("Stored Image: %s"), *FileName);
}

Компилирую, наслаждаюсь результатом:

Внимательный разработчик с опытом в unreal, сможет упрекнуть меня: не показал создание и подключение curve. Ответ на упрёк, добавить в FlightActor.cpp:

    FlightCurve = NewObject<UCurveFloat>(this, TEXT("DynamicUCurveFloat"));
    FKeyHandle KeyHandle = FlightCurve->FloatCurve.AddKey(0.0f, 0.1f);
    FlightCurve->FloatCurve.SetKeyInterpMode(KeyHandle, ERichCurveInterpMode::RCIM_Cubic, true);
    FKeyHandle KeyHandle2 = FlightCurve->FloatCurve.AddKey(100.0f, 1300.0f);
    FlightCurve->FloatCurve.SetKeyInterpMode(KeyHandle2, ERichCurveInterpMode::RCIM_Cubic, true);

Итоговый вариант соответствует поставленной задаче. Приобрёл новые знания и готов к новым вызовам. Статья в первую очередь маяк для разработчиков с схожими интересами, те кто понял что "тут происходит", и где это будет применяться, пишите в лс, для вас есть работа, невероятных сумм у нас нет, скромно, но своевременная оплата гарантированна!!!

Комментарии (2)


  1. ignorabimus
    21.09.2023 10:11

    Na Jožina z bažin, koho by to napadlo,platí jen a pouze práškovací letadlo - это то самое летадло?


  1. chnav
    21.09.2023 10:11
    +1

    >> Эта статья, мой конспект, сигнальный флаг, или очередная тренировка изложения своих мыслей.

    >> Статья в первую очередь маяк для разработчиков с схожими интересами, те кто понял что "тут происходит", и где это будет применяться, пишите в лс, для вас есть работа, невероятных сумм у нас нет, скромно, но своевременная оплата гарантированна!!!

    Живём в 21 веке, неужели ещё нет какой-нибудь нейронки для вычитки текстов. Глаза можно сломать от стилистики, орфографии и пунктуации. Тренировка не пройдена.