07. 시야 연출 3 - Elysia-ff/UE4-Custom_Stencil_Tutorial GitHub Wiki

디테일 및 최적화

잘된 것 처럼 보이지만 거리가 멀어지면 문제가 생긴다.
image
(이렇게 벽을 침범해서 메시가 생성된다)

무식하게 FarViewCastCount를 왕창 늘리면 해결되지만 먼 거리로 갈 수록 더 많은 연산이 필요하므로 비효율적이다.
그래서 두개의 LineTrace사이에 다른 정점이 추가로 필요한지 검출하는 코드를 추가할 것이다.

image
image

검은선이 벽, 파란선이 원래의 LineTrace고 두 파란선의 끝점을 이은 것이 기존에 생성되던 메시다.
여기서 노란선을 검사해서 빨간점을 새 정점으로 추가해 메시를 수정하는 것이 목표다.

Edge검사

AddLineOfSightPoints에서 직전에 검사한 결과를 ViewCastA, 새로 검사한 결과를 ViewCastB라고 할때
ViewCastAViewCastB가 만난 오브젝트가 다르거나 노말값이 다르면 두 정점 사이에 Edge가 존재한다고 판단한다.

만약 그렇다면 ViewCastAViewCastB의 중앙각도를 다시 검사해서 이를 NewViewCast라고 정의한다.
이제 ViewCastANewViewCast가 같다면 이 둘 사이에는 Edge가 존재하지 않는 것이므로 NewViewCastViewCastB사이를 검사한다.
이를 반복하면 두 정점 사이에 있는 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.cppAddLineOfSightPointsAddNearSightPoints를 아래와 같이 수정하고 헤더에 추가한 함수를 정의한다.

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사이에 장애물이 존재하지 않을 정도로만 설정하면 된다.
image
(두 ViewCast가 장애물을 찾지 못해서 Edge검사 조건이 false가 된다)

실행하면 깔끔한 메시가 그려진다.

image image

극단적인 예로 FarViewCastCount가 2인 경우나 벽이 구형인 경우에도 잘 나온다 :) image
image

마지막으로 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);
	}
}
⚠️ **GitHub.com Fallback** ⚠️