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