07. 시야 연출 3 - Elysia-ff/UE4-Custom_Stencil_Tutorial GitHub Wiki
잘된 것 처럼 보이지만 거리가 멀어지면 문제가 생긴다.
(이렇게 벽을 침범해서 메시가 생성된다)
무식하게 FarViewCastCount
를 왕창 늘리면 해결되지만 먼 거리로 갈 수록 더 많은 연산이 필요하므로 비효율적이다.
그래서 두개의 LineTrace사이에 다른 정점이 추가로 필요한지 검출하는 코드를 추가할 것이다.
검은선이 벽, 파란선이 원래의 LineTrace고 두 파란선의 끝점을 이은 것이 기존에 생성되던 메시다.
여기서 노란선을 검사해서 빨간점을 새 정점으로 추가해 메시를 수정하는 것이 목표다.
AddLineOfSightPoints
에서 직전에 검사한 결과를 ViewCastA
, 새로 검사한 결과를 ViewCastB
라고 할때
ViewCastA
와 ViewCastB
가 만난 오브젝트가 다르거나 노말값이 다르면 두 정점 사이에 Edge가 존재한다고 판단한다.
만약 그렇다면 ViewCastA
와 ViewCastB
의 중앙각도를 다시 검사해서 이를 NewViewCast
라고 정의한다.
이제 ViewCastA
와 NewViewCast
가 같다면 이 둘 사이에는 Edge가 존재하지 않는 것이므로 NewViewCast
와 ViewCastB
사이를 검사한다.
이를 반복하면 두 정점 사이에 있는 Edge를 찾아낼 수 있다. 그리고 무한루프에 빠지지 않도록 사이각이 일정각도 이하라면 검사를 종료한다.
먼저 ViewCastInfo.hpp
에 Edge검사를 위한 변수를 추가한다.
#include <Engine/Classes/Engine/NetSerialization.h>
struct FViewCastInfo
{
TWeakObjectPtr<AActor> Actor;
FVector Point;
float AngleInDegrees;
FVector_NetQuantizeNormal Normal;
FViewCastInfo()
: Actor(nullptr)
, Point(FVector::ZeroVector)
, AngleInDegrees(0.0f)
, Normal(FVector_NetQuantizeNormal::ZeroVector)
{
}
FViewCastInfo(TWeakObjectPtr<AActor> _Actor, const FVector& _Point, float _AngleInDegrees, const FVector_NetQuantizeNormal& _Normal)
: Actor(_Actor)
, Point(_Point)
, AngleInDegrees(_AngleInDegrees)
, Normal(_Normal)
{
}
bool operator==(const FViewCastInfo& Other) const
{
return Actor == Other.Actor && Normal == Other.Normal;
}
bool operator!=(const FViewCastInfo& Other) const
{
return !(*this == Other);
}
};
LosComponent.h
에 다음 코드를 추가한다.
private:
void FindEdge(FViewCastInfo ViewCastA, FViewCastInfo ViewCastB, float Distance, TArray<FVector>* ViewPoints);
public:
UPROPERTY(EditAnywhere, Category = "Line Of Sight|Edge")
float EdgeFindingThreshold = 0.01f;
LosComponent.cpp
의 AddLineOfSightPoints
와 AddNearSightPoints
를 아래와 같이 수정하고 헤더에 추가한 함수를 정의한다.
void ULosComponent::AddLineOfSightPoints(TArray<FVector>* ViewPoints)
{
// 생략
FViewCastInfo OldViewCast;
for (int32 i = 0; i < FarViewCastCount; i++)
{
float Angle = StartAngle + (DeltaAngle * i);
FViewCastInfo ViewCastInfo = ViewCast(Angle, FarDistance);
if (i > 0 && OldViewCast != ViewCastInfo)
{
FindEdge(OldViewCast, ViewCastInfo, FarDistance, ViewPoints);
}
ViewPoints->Add(ViewCastInfo.Point);
OldViewCast = ViewCastInfo;
}
}
FViewCastInfo ULosComponent::ViewCast(float AngleInDegrees, float Distance) const
{
// 생략
FHitResult HitResult;
if (GetWorld()->LineTraceSingleByChannel(HitResult, LineStart, LineEnd, ECC_ObstacleTrace))
{
return FViewCastInfo(HitResult.Actor, HitResult.ImpactPoint, AngleInDegrees, HitResult.ImpactNormal);
}
return FViewCastInfo(nullptr, LineEnd, AngleInDegrees, FVector_NetQuantizeNormal::ZeroVector);
}
void ULosComponent::FindEdge(FViewCastInfo ViewCastA, FViewCastInfo ViewCastB, float Distance, TArray<FVector>* ViewPoints)
{
check(ViewPoints);
check(EdgeFindingThreshold > 0.0f);
if (FMath::Abs(ViewCastA.AngleInDegrees - ViewCastB.AngleInDegrees) <= EdgeFindingThreshold)
{
ViewPoints->Add(ViewCastA.Point);
ViewPoints->Add(ViewCastB.Point);
return;
}
float Angle = (ViewCastA.AngleInDegrees + ViewCastB.AngleInDegrees) * 0.5f;
FViewCastInfo NewViewCast = ViewCast(Angle, Distance);
if (ViewCastA == NewViewCast)
{
FindEdge(NewViewCast, ViewCastB, Distance, ViewPoints);
}
else if (ViewCastB == NewViewCast)
{
FindEdge(ViewCastA, NewViewCast, Distance, ViewPoints);
}
else
{
FindEdge(ViewCastA, NewViewCast, Distance, ViewPoints);
FindEdge(NewViewCast, ViewCastB, Distance, ViewPoints);
}
}
이제 FarViewCastCount
는 LineTrace사이에 장애물이 존재하지 않을 정도로만 설정하면 된다.
(두 ViewCast
가 장애물을 찾지 못해서 Edge검사 조건이 false가 된다)
실행하면 깔끔한 메시가 그려진다.
극단적인 예로 FarViewCastCount
가 2인 경우나 벽이 구형인 경우에도 잘 나온다 :)
마지막으로 DrawMesh
에서 사용되는 로컬 변수들을 클래스의 멤버 변수로 변경했다.
/LineOfSight/ViewCastInfo.hpp
// Copyright 2021. Elysia-ff
#pragma once
#include <Engine/Classes/Engine/NetSerialization.h>
/**
*
*/
struct FViewCastInfo
{
TWeakObjectPtr<AActor> Actor;
FVector Point;
float AngleInDegrees;
FVector_NetQuantizeNormal Normal;
FViewCastInfo()
: Actor(nullptr)
, Point(FVector::ZeroVector)
, AngleInDegrees(0.0f)
, Normal(FVector_NetQuantizeNormal::ZeroVector)
{
}
FViewCastInfo(TWeakObjectPtr<AActor> _Actor, const FVector& _Point, float _AngleInDegrees, const FVector_NetQuantizeNormal& _Normal)
: Actor(_Actor)
, Point(_Point)
, AngleInDegrees(_AngleInDegrees)
, Normal(_Normal)
{
}
bool operator==(const FViewCastInfo& Other) const
{
return Actor == Other.Actor && Normal == Other.Normal;
}
bool operator!=(const FViewCastInfo& Other) const
{
return !(*this == Other);
}
};
/LineOfSight/LosComponent.h
// Copyright 2021. Elysia-ff
#pragma once
#include "LineOfSight/ViewCastInfo.hpp"
#include "CoreMinimal.h"
#include "ProceduralMeshComponent.h"
#include "LosComponent.generated.h"
/**
*
*/
UCLASS()
class STEALTHTUTORIAL_API ULosComponent : public UProceduralMeshComponent
{
GENERATED_BODY()
public:
ULosComponent(const FObjectInitializer& ObjectInitializer);
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
private:
void DrawMesh();
void AddLineOfSightPoints();
void AddNearSightPoints();
FVector GetDirection(float AngleInDegrees) const;
FViewCastInfo ViewCast(float AngleInDegrees, float Distance) const;
void FindEdge(FViewCastInfo ViewCastA, FViewCastInfo ViewCastB, float Distance);
public:
UPROPERTY(EditAnywhere, Category = "Line Of Sight|Debug")
UMaterialInterface* DebugMaterial;
UPROPERTY(EditAnywhere, Category = "Line Of Sight|Debug")
bool bDrawDebugLine;
UPROPERTY(EditAnywhere, Category = "Line Of Sight|Far", meta = (UIMin = 0, UIMax = 360))
int32 FarSightAngle = 0;
UPROPERTY(EditAnywhere, Category = "Line Of Sight|Far", meta = (UIMin = 2))
int32 FarViewCastCount = 2;
UPROPERTY(EditAnywhere, Category = "Line Of Sight|Far", meta = (UIMin = 0))
float FarDistance = 0.0f;
UPROPERTY(EditAnywhere, Category = "Line Of Sight|Near", meta = (UIMin = 2))
int32 NearViewCastCount = 2;
UPROPERTY(EditAnywhere, Category = "Line Of Sight|Near", meta = (UIMin = 0))
float NearDistance = 0.0f;
UPROPERTY(EditAnywhere, Category = "Line Of Sight|Edge")
float EdgeFindingThreshold = 0.01f;
private:
TArray<FVector> ViewPoints;
TArray<FVector> Vertices;
TArray<int32> Triangles;
TArray<FVector> Normals;
TArray<FVector2D> UV0;
TArray<FLinearColor> VertexColors;
TArray<FProcMeshTangent> Tangents;
};
/LineOfSight/LosComponent.cpp
// Copyright 2021. Elysia-ff
#include "LosComponent.h"
#include <Engine/Public/DrawDebugHelpers.h>
#include "StealthTypes.hpp"
ULosComponent::ULosComponent(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
PrimaryComponentTick.bCanEverTick = true;
SetCastShadow(false);
SetCollisionProfileName(TEXT("NoCollision"));
}
void ULosComponent::BeginPlay()
{
Super::BeginPlay();
if (DebugMaterial != nullptr)
{
SetMaterial(0, DebugMaterial);
}
}
void ULosComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
DrawMesh();
}
void ULosComponent::DrawMesh()
{
ViewPoints.Reset();
AddLineOfSightPoints();
AddNearSightPoints();
Vertices.Reset(ViewPoints.Num() + 1);
Vertices.Add(FVector::ZeroVector);
Triangles.Reset((ViewPoints.Num() - 1) * 3);
int32 ViewPointsNum = ViewPoints.Num();
for (int32 i = 0; i < ViewPointsNum; i++)
{
if (bDrawDebugLine)
{
DrawDebugLine(GetWorld(), GetComponentLocation(), ViewPoints[i], FColor::Black, false, (-1.0f), (uint8)'\000', 1.0f);
}
Vertices.Add(GetComponentTransform().InverseTransformPosition(ViewPoints[i]));
if (i < ViewPointsNum - 1)
{
Triangles.Add(0);
Triangles.Add(i + 2);
Triangles.Add(i + 1);
}
}
CreateMeshSection_LinearColor(0, Vertices, Triangles, Normals, UV0, VertexColors, Tangents, false);
}
void ULosComponent::AddLineOfSightPoints()
{
check(FarViewCastCount >= 2);
float StartAngle = -FarSightAngle * 0.5f;
float DeltaAngle = (float)FarSightAngle / (FarViewCastCount - 1);
FViewCastInfo OldViewCast;
for (int32 i = 0; i < FarViewCastCount; i++)
{
float Angle = StartAngle + (DeltaAngle * i);
FViewCastInfo ViewCastInfo = ViewCast(Angle, FarDistance);
if (i > 0 && OldViewCast != ViewCastInfo)
{
FindEdge(OldViewCast, ViewCastInfo, FarDistance);
}
ViewPoints.Add(ViewCastInfo.Point);
OldViewCast = ViewCastInfo;
}
}
void ULosComponent::AddNearSightPoints()
{
check(NearViewCastCount >= 2);
float NearSightAngle = 360.0f - FarSightAngle;
float StartAngle = FarSightAngle * 0.5f;
float DeltaAngle = NearSightAngle / (NearViewCastCount - 1);
FViewCastInfo OldViewCast;
for (int32 i = 0; i < NearViewCastCount; i++)
{
float Angle = StartAngle + (DeltaAngle * i);
FViewCastInfo ViewCastInfo = ViewCast(Angle, NearDistance);
if (i > 0 && OldViewCast != ViewCastInfo)
{
FindEdge(OldViewCast, ViewCastInfo, NearDistance);
}
ViewPoints.Add(ViewCastInfo.Point);
OldViewCast = ViewCastInfo;
}
}
FVector ULosComponent::GetDirection(float AngleInDegrees) const
{
FVector ForwardVector = GetForwardVector();
return ForwardVector.RotateAngleAxis(AngleInDegrees, GetUpVector());
}
FViewCastInfo ULosComponent::ViewCast(float AngleInDegrees, float Distance) const
{
FVector Dir = GetDirection(AngleInDegrees);
FVector LineStart = GetComponentLocation();
FVector LineEnd = LineStart + (Dir * Distance);
FHitResult HitResult;
if (GetWorld()->LineTraceSingleByChannel(HitResult, LineStart, LineEnd, ECC_ObstacleTrace))
{
return FViewCastInfo(HitResult.Actor, HitResult.ImpactPoint, AngleInDegrees, HitResult.ImpactNormal);
}
return FViewCastInfo(nullptr, LineEnd, AngleInDegrees, FVector_NetQuantizeNormal::ZeroVector);
}
void ULosComponent::FindEdge(FViewCastInfo ViewCastA, FViewCastInfo ViewCastB, float Distance)
{
check(EdgeFindingThreshold > 0.0f);
if (FMath::Abs(ViewCastA.AngleInDegrees - ViewCastB.AngleInDegrees) <= EdgeFindingThreshold)
{
ViewPoints.Add(ViewCastA.Point);
ViewPoints.Add(ViewCastB.Point);
return;
}
float Angle = (ViewCastA.AngleInDegrees + ViewCastB.AngleInDegrees) * 0.5f;
FViewCastInfo NewViewCast = ViewCast(Angle, Distance);
if (ViewCastA == NewViewCast)
{
FindEdge(NewViewCast, ViewCastB, Distance);
}
else if (ViewCastB == NewViewCast)
{
FindEdge(ViewCastA, NewViewCast, Distance);
}
else
{
FindEdge(ViewCastA, NewViewCast, Distance);
FindEdge(NewViewCast, ViewCastB, Distance);
}
}