06. 시야 연출 2 - Elysia-ff/UE4-Custom_Stencil_Tutorial GitHub Wiki

커스텀 메시 생성

이제 C++ 에서 메시를 만들 차례다.

먼저 LineTrace의 결과값을 저장하는 struct를 하나 만들자.

struct FViewCastInfo
{
	FVector Point;

	FViewCastInfo(const FVector& _Point)
		: Point(_Point)
	{
	}
};

StealthTutorial.Build.cs를 다음과 같이 수정하고

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "AIModule", "ProceduralMeshComponent" });

UProceduralMeshComponent를 확장해서 ULosComponent를 생성한다.

LosComponent.hLosComponent.cpp에 일정 각도마다 장애물이 있는지 검사하는 코드를 추가한다.

private:
	void AddLineOfSightPoints(TArray<FVector>* ViewPoints);

	FVector GetDirection(float AngleInDegrees) const;

	FViewCastInfo ViewCast(float AngleInDegrees, float Distance) const;

public:
	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;
#include "StealthTypes.hpp"

void ULosComponent::AddLineOfSightPoints(TArray<FVector>* ViewPoints)
{
	check(ViewPoints);
	check(FarViewCastCount >= 2);

	float StartAngle = -FarSightAngle * 0.5f;
	float DeltaAngle = (float)FarSightAngle / (FarViewCastCount - 1);

	for (int32 i = 0; i < FarViewCastCount; i++)
	{
		float Angle = StartAngle + (DeltaAngle * i);
		FViewCastInfo ViewCastInfo = ViewCast(Angle, FarDistance);

		ViewPoints->Add(ViewCastInfo.Point);
	}
}

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.ImpactPoint);
	}
	
	return FViewCastInfo(LineEnd);
}

그리고 매프레임마다 메시를 생성한다.
주의할 점은 각 정점의 위치는 Component의 로컬좌표를 기준으로 한다.

public:
	ULosComponent(const FObjectInitializer& ObjectInitializer);

	virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

private:
	void DrawMesh();
ULosComponent::ULosComponent(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	PrimaryComponentTick.bCanEverTick = true;
	SetCastShadow(false);
	SetCollisionProfileName(TEXT("NoCollision"));
}

void ULosComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

	DrawMesh();
}

void ULosComponent::DrawMesh()
{
	TArray<FVector> ViewPoints;
	AddLineOfSightPoints(&ViewPoints);

	TArray<FVector> Vertices;
	Vertices.Reserve(ViewPoints.Num() + 1);
	Vertices.Add(FVector::ZeroVector);

	TArray<int32> Triangles;
	Triangles.Reserve((ViewPoints.Num() - 1) * 3);

	int32 ViewPointsNum = ViewPoints.Num();
	for (int32 i = 0; i < ViewPointsNum; i++)
	{
		Vertices.Add(GetComponentTransform().InverseTransformPosition(ViewPoints[i]));

		if (i < ViewPointsNum - 1)
		{
			Triangles.Add(0);
			Triangles.Add(i + 2);
			Triangles.Add(i + 1);
		}
	}

	TArray<FVector> Normals;
	TArray<FVector2D> UV0;
	TArray<FLinearColor> VertexColors;
	TArray<FProcMeshTangent> Tangents;
	CreateMeshSection_LinearColor(0, Vertices, Triangles, Normals, UV0, VertexColors, Tangents, false);
}

이제 Hitman.h에 아래 코드를 추가하고

class ULosComponent;

public:
	UPROPERTY(VisibleAnywhere, Category = "Line Of Sight")
	ULosComponent* LosComponent;

Hitman.cpp의 생성자하단에 다음 코드를 추가한 다음

LosComponent = CreateDefaultSubobject<ULosComponent>(TEXT("LosComponent"));
LosComponent->SetupAttachment(RootComponent);

BP_Hitman에서 FarSightAngleFarDistance로 시야각, 거리를 조절하고 FarViewCastCount의 수치를 조절하면 자연스러운 부채꼴모양의 메시가 표시된다.
image los

앞만 볼 수 있는건 좀 이상하니까 주변 짧은 거리에도 시야를 줄 것이다.
시야각만 360 - FarSightAngle로 지정하고 나머지는 이전과 같다.

private:
	void AddNearSightPoints(TArray<FVector>* ViewPoints);

public:
	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;
void ULosComponent::DrawMesh()
{
	TArray<FVector> ViewPoints;
	AddLineOfSightPoints(&ViewPoints);
	AddNearSightPoints(&ViewPoints);

	// 생략
}

void ULosComponent::AddNearSightPoints(TArray<FVector>* ViewPoints)
{
	check(ViewPoints);
	check(NearViewCastCount >= 2);

	float NearSightAngle = 360.0f - FarSightAngle;
	float StartAngle = FarSightAngle * 0.5f;
	float DeltaAngle = NearSightAngle / (NearViewCastCount - 1);

	for (int32 i = 0; i < NearViewCastCount; i++)
	{
		float Angle = StartAngle + (DeltaAngle * i);
		FViewCastInfo ViewCastInfo = ViewCast(Angle, NearDistance);

		ViewPoints->Add(ViewCastInfo.Point);
	}
}

image image


디버그 코드 추가

그런데 메시의 색도 어둡고 LineTrace가 어디서 일어나는지도 잘 모르겠으니 디버그 코드를 추가하자.
아래 코드를 LosComponent.h에 추가한다.

public:
	virtual void BeginPlay() override;

public:
	UPROPERTY(EditAnywhere, Category = "Line Of Sight|Debug")
	UMaterialInterface* DebugMaterial;

	UPROPERTY(EditAnywhere, Category = "Line Of Sight|Debug")
	bool bDrawDebugLine;

LosComponent.cpp에 아래 함수를 추가하고

void ULosComponent::BeginPlay()
{
	Super::BeginPlay();

	if (DebugMaterial != nullptr)
	{
		SetMaterial(0, DebugMaterial);
	}
}

DrawMesh함수를 다음과 같이 수정한다.

#include <Engine/Public/DrawDebugHelpers.h>

void ULosComponent::DrawMesh()
{
	// 생략

	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);
		}

	// 생략

그 다음 에디터에서 마테리얼을 생성하고 BP_Hitman에 연결한 다음 게임을 시작하면 보기 편해진 메시를 볼 수 있다.
image image image


완성된 코드

/LineOfSight/ViewCastInfo.hpp
// Copyright 2021. Elysia-ff

#pragma once

/**
 * 
 */
struct FViewCastInfo
{
	FVector Point;

	FViewCastInfo(const FVector& _Point)
		: Point(_Point)
	{
	}
};
/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(TArray<FVector>* ViewPoints);

	void AddNearSightPoints(TArray<FVector>* ViewPoints);

	FVector GetDirection(float AngleInDegrees) const;

	FViewCastInfo ViewCast(float AngleInDegrees, float Distance) const;

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;
};
/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()
{
	TArray<FVector> ViewPoints;
	AddLineOfSightPoints(&ViewPoints);
	AddNearSightPoints(&ViewPoints);

	TArray<FVector> Vertices;
	Vertices.Reserve(ViewPoints.Num() + 1);
	Vertices.Add(FVector::ZeroVector);

	TArray<int32> Triangles;
	Triangles.Reserve((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);
		}
	}

	TArray<FVector> Normals;
	TArray<FVector2D> UV0;
	TArray<FLinearColor> VertexColors;
	TArray<FProcMeshTangent> Tangents;
	CreateMeshSection_LinearColor(0, Vertices, Triangles, Normals, UV0, VertexColors, Tangents, false);
}

void ULosComponent::AddLineOfSightPoints(TArray<FVector>* ViewPoints)
{
	check(ViewPoints);
	check(FarViewCastCount >= 2);

	float StartAngle = -FarSightAngle * 0.5f;
	float DeltaAngle = (float)FarSightAngle / (FarViewCastCount - 1);

	for (int32 i = 0; i < FarViewCastCount; i++)
	{
		float Angle = StartAngle + (DeltaAngle * i);
		FViewCastInfo ViewCastInfo = ViewCast(Angle, FarDistance);

		ViewPoints->Add(ViewCastInfo.Point);
	}
}

void ULosComponent::AddNearSightPoints(TArray<FVector>* ViewPoints)
{
	check(ViewPoints);
	check(NearViewCastCount >= 2);

	float NearSightAngle = 360.0f - FarSightAngle;
	float StartAngle = FarSightAngle * 0.5f;
	float DeltaAngle = NearSightAngle / (NearViewCastCount - 1);

	for (int32 i = 0; i < NearViewCastCount; i++)
	{
		float Angle = StartAngle + (DeltaAngle * i);
		FViewCastInfo ViewCastInfo = ViewCast(Angle, NearDistance);

		ViewPoints->Add(ViewCastInfo.Point);
	}
}

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.ImpactPoint);
	}
	
	return FViewCastInfo(LineEnd);
}
/Character/Hitman.h
// Copyright 2021. Elysia-ff

#pragma once

#include "CoreMinimal.h"
#include "Character/StealthCharacter.h"
#include "Hitman.generated.h"

class ULosComponent;

/**
 * 
 */
UCLASS()
class STEALTHTUTORIAL_API AHitman : public AStealthCharacter
{
	GENERATED_BODY()
	
public:
	AHitman(const FObjectInitializer& ObjectInitializer);

public:
	UPROPERTY(VisibleAnywhere, Category = "Line Of Sight")
	ULosComponent* LosComponent;
};
/Character/Hitman.cpp
// Copyright 2021. Elysia-ff


#include "Hitman.h"

#include "Character/Controller/HitmanAIController.h"
#include "LineOfSight/LosComponent.h"

AHitman::AHitman(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	AIControllerClass = AHitmanAIController::StaticClass();

	LosComponent = CreateDefaultSubobject<ULosComponent>(TEXT("LosComponent"));
	LosComponent->SetupAttachment(RootComponent);
}
⚠️ **GitHub.com Fallback** ⚠️