Эта статья, мой конспект, сигнальный флаг, или очередная тренировка изложения своих мыслей? В силу обстоятельства, прикоснулся к unreal, замечательный инструмент в "умелых руках", много од написано сему творению человеческой мысли, так что взаимодействие с ним большая честь для разработчика. Создание игр, визуализация, исследования, много всего интересного заложено в этот проект с многолетней историей развития. Открытость и большое сообщество, существенно понижает порог вхождения, конечно тривиальность писать такое, каждый второй инструмент с такими характеристиками, но это говорит о общей высокой планке нынешних инструментов для реализации любых техно извращённых фантазий. Невероятное стечение обстоятельств, получаю деньги за то что учусь взаимодействовать с этим инструментом.
Коротко о задаче:
Загрузить файл фотограмметрии местности
Начертить путь
По заданному пути двигать камеру
Сохранять изображения с камеры
Главное требование: реализовать большую часть функционала программированием 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
Фотограмметрия мне встречалась только в статьях, поэтому никакого взаимодействия с реальными данными не было, тестовый файл полученный от новых товарищей-единомышленников был в формате непонятном 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.
Заполняю полученные файлы кодом:
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);
Итоговый вариант соответствует поставленной задаче. Приобрёл новые знания и готов к новым вызовам. Статья в первую очередь маяк для разработчиков с схожими интересами, те кто понял что "тут происходит", и где это будет применяться, пишите в лс, для вас есть работа, невероятных сумм у нас нет, скромно, но своевременная оплата гарантированна!!!