Class GameDev* SheepAdult

[Unreal Engine 4.27] Laser Reflection System - C++ 본문

Unreal Project

[Unreal Engine 4.27] Laser Reflection System - C++

SheepAdult 2022. 3. 11. 02:38

 Light가 아닌 파티클이나 나이아가라를 통해 빛을 반사시키는 기능을 구현해 봤다. 빛이 면에 부딪히면 그 면의 법선벡터를 기준으로 반대로 반사되어야 하므로 이에 맞추어 구현했다.

 

 

 트레이스를 쏴서 블락이 되면 반사각을 정해줘야한다. 작성할 때 각을 먼저 정해줘 먼저 보면 트레이스가 부딪힌 컴포넌트의 Material이 내가 원하는(거울과 같이 반사할 수 있는, 혹은 본인이 원하는 Material) Material이면 반사를 시켜주고, 그렇지 않으면 반사를 멈추는 작업을 해준다. 반사를 하려면 부딪힌 지점을 다음 트레이스의 출발지점으로 정해줘야 하고, 방향은 부딪힌 트레이스의 벡터를 노말 벡터를 기준으로 대칭되는 방향으로 정해줘야 한다. 이때 사용한 함수가 "Mirror Vector by Normal()"이다. 그 후 While문을 계속 돌리기 위한 bool값을 true로 설정하고, 반사 횟수를 1 늘려준다. 만약 반사를 멈추려면 bool 값을 false, 반사 횟수를 0으로 설정한다. 이후 거울을 옮겨 실시간으로 빛을 반사시키고 싶을 때 반사되도록 설정한 것이다. 

트레이스가 반사되는 모습

 이제 여기에 Emitter를 입혀보자. 

https://www.youtube.com/watch?v=STjDxH5L80E 

 

레이저 Material과 Particle System으로 Emitter 만드는 것은 위의 영상을 참고했다. 그리고 아래와 같이 추가 설정을 해줬다.

먼저 실시간 빛 효과를 위해 Lifetime을 추가했으며, Min과 Max값을 0.1로 줬다.

LifrTime

그리고 Required에서 루프 횟수를 한 번만 주기 위해 설정을 해줬다. 아래의 설정을 안하면 Particle이 영구히 남는 것처럼 보이게 된다.

Required

BeamData에서 빛과같은 속도를 주기 위해 속도도 조절해 줬다. 아래의 Speed를 0.0으로 안 주게 되면 서서히 나가는 것처럼 되어 Lifetime이 다되면 원하는 지점에 도착하기도 전에 이펙트가 꺼지게 된다.

이제 Emitter를 Spawn하고 "Set Beam Source Point"로 시작 지점을, Set Beam End Point로 도착지점을 정해주면 된다. 특정 Material을 부딪히지 않아 반사가 끝나면, Beam의 Endpoint를 OutHit의 ImpactPoint가 아닌 특정 길이로 지정해줘 문제가 없게 해야 한다.

결과

.

 

header

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Particles/ParticleSystemComponent.h"

#include "LaserEmitter.generated.h"

UCLASS()
class CAP2_API ALaserEmitter : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	ALaserEmitter();

	int32 MaxReflectionNum = 10;
	UPROPERTY(EditAnywhere)
		float LaserStartPoint = 0.0f;

	void CastLight(FVector CastOrigin, FVector CastDirection);

	UPROPERTY(EditAnywhere)
		UStaticMeshComponent* StaticMesh;
	UPROPERTY(EditAnywhere)
		UStaticMeshComponent* Cube;
	UMaterialInstance* MirrorMat;

	UPROPERTY(EditAnywhere)
		UParticleSystem* LaserEmitter;
protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

};

cpp

#include "LaserEmitter.h"
#include "Kismet/KismetSystemLibrary.h"
#include "Kismet/KismetMathLibrary.h"
#include "Kismet/GameplayStatics.h"
#include "Materials/MaterialInterface.h"

// Sets default values
ALaserEmitter::ALaserEmitter()
{
	// 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;
	StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Glass"));
	RootComponent = StaticMesh;

	Cube = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Cube"));
	Cube->SetRelativeScale3D(FVector(0.3f));
	Cube->SetupAttachment(RootComponent);

	static ConstructorHelpers::FObjectFinder<UMaterialInstance> MirrorAsset(TEXT(
		"MaterialInstanceConstant'/Game/StarterContent/Materials/M_Metal_Burnished_Steel_Inst.M_Metal_Burnished_Steel_Inst'"));
	if (MirrorAsset.Succeeded())
	{
		MirrorMat = MirrorAsset.Object;
	}
	SetActorTickInterval(0.1f);
}

// Called when the game starts or when spawned
void ALaserEmitter::BeginPlay()
{
	Super::BeginPlay();


}

// Called every frame
void ALaserEmitter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	CastLight(Cube->GetComponentLocation(), Cube->GetForwardVector());
}

void ALaserEmitter::CastLight(FVector CastOrigin, FVector CastDirection)
{
	FVector _CastOrigin = CastOrigin + CastDirection * LaserStartPoint;
	FVector _CastDirection = CastDirection;
	int32 ReflectionNum = 0;
	bool bIsMirror = true;
	while (bIsMirror && MaxReflectionNum > ReflectionNum)
	{
		TArray<AActor*> ToIgnore;
		ToIgnore.Add(this);
		FHitResult OutHit;
		bool bIsOutHit = UKismetSystemLibrary::LineTraceSingle(GetWorld(), _CastOrigin, _CastOrigin + _CastDirection * 3000.0f, ETraceTypeQuery::TraceTypeQuery4, false,
			ToIgnore, EDrawDebugTrace::ForDuration, OutHit, true, FLinearColor::Red, FLinearColor::Green, 0.1f);

		// Laser Emitter
		if (bIsOutHit)
		{
			if (LaserEmitter)
			{
				UParticleSystemComponent* _LaserEmitter = UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), LaserEmitter, FVector(0.0f), FRotator(0.0f), FVector(1.0f), true, EPSCPoolMethod::None, true);
				_LaserEmitter->SetBeamSourcePoint(0, _CastOrigin, 0);
				_LaserEmitter->SetBeamEndPoint(0, OutHit.ImpactPoint);
			}	

			if (MirrorMat != nullptr && MirrorMat == OutHit.Component->GetMaterial(0))
			{
				_CastOrigin = OutHit.ImpactPoint + _CastDirection.MirrorByVector(OutHit.ImpactNormal) * 10.0f;
				_CastDirection = _CastDirection.MirrorByVector(OutHit.ImpactNormal);
				bIsMirror = true;
				ReflectionNum++;
			}
			else
			{
				ReflectionNum = 0;
				bIsMirror = false;
			}
		}
		else
		{
			if (LaserEmitter)
			{
				UParticleSystemComponent* _LaserEmitter = UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), LaserEmitter, FVector(0.0f), FRotator(0.0f), FVector(1.0f), true, EPSCPoolMethod::None, true);
				_LaserEmitter->SetBeamSourcePoint(0, _CastOrigin, 0);
				_LaserEmitter->SetBeamEndPoint(0, _CastOrigin + _CastDirection * 3000.0f);
			}
			ReflectionNum = 0;
			bIsMirror = false;
		}
	}
}

 위의 코드는 LightCast함수로 LineTrace로 먼저 빛의 움직임을 구현한 후 그 위에 파티클 시스템(Emitter)을 입히는 형식으로 구현했다. Loc과 Dir, b, num은 모두 로컬 변수이다. 아래에서도 설명하겠지만, while문을 돌면서 반사를 구현해, 그 이전 순서의 충돌지점과 방향을 이용해 다음 출발 지점과 방향을 구해야 하므로 로컬 변수를 사용한다. 먼저 처음에 매개변수 시작 위치와 방향을 로컬 변수에 저장해준다. 그리고 Loop를 돌건대, 특정 Material에 부딪히고, 정해준 반사 가능 횟수 내일 때 While문을 돈다. 반사 가능 횟수를 정해준 이유는 무한루프를 방지하기 위함이다. 논리상 왜 무한루프가 나오는진 모르겠는데 계속 떠서 횟수를 정해줬다. 이 부분은 더 고민을 해서 추후에 수정하는 것으로 해야겠다.  

 

반사 가능한 Material을 UMaterialInstance로 선언한다. 그리고 ConstructorHelpers로 찾아온 후 Trace시 충돌한 Component의 Material과 비교하면 된다. 그리고 추후에 있을 이벤트나 폰과의 충돌 여부 등의 변수를 대비해 Trace채널을 프로젝트 세팅에서 하나 추가한 뒤 사용해줬다. 나머지는 블루프린트와 동일하다.

 

 이 기능을 구현할 때 GetMaterialFromCollisionFaceIndex()을 사용해서 부딪힌 지점의 Material을 비교하는 방법을 사용하려 했으나 계속해서 에러가 나서 GetMaterial()로 처리를 했다. 이는 추후에 공부를 더 해봐야 할 것 같다.