Files
UnrealEngine/Engine/Source/Runtime/Foliage/Private/FoliageComponent.cpp
2025-05-18 13:04:45 +08:00

252 lines
12 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
/*=============================================================================
FoliageComponent.cpp: Foliage rendering implementation.
=============================================================================*/
#include "CoreMinimal.h"
#include "Engine/EngineTypes.h"
#include "RenderingThread.h"
#include "GameFramework/Controller.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/Pawn.h"
#include "StaticMeshResources.h"
#include "StaticMeshSceneProxy.h"
#include "InteractiveFoliageActor.h"
#include "InteractiveFoliageComponent.h"
#include "Engine/StaticMesh.h"
#include "Engine/DamageEvents.h"
/** Scene proxy class for UInteractiveFoliageComponent. */
class FInteractiveFoliageSceneProxy final : public FStaticMeshSceneProxy
{
public:
SIZE_T GetTypeHash() const override
{
static size_t UniquePointer;
return reinterpret_cast<size_t>(&UniquePointer);
}
FInteractiveFoliageSceneProxy(UInteractiveFoliageComponent* InComponent) :
FStaticMeshSceneProxy(InComponent, false),
FoliageImpluseDirection(0,0,0),
FoliageNormalizedRotationAxisAndAngle(0,0,1,0)
{}
/** Accessor used by the rendering thread when setting foliage parameters for rendering. */
void GetFoliageParameters(FVector& OutFoliageImpluseDirection, FVector4& OutFoliageNormalizedRotationAxisAndAngle) const
{
OutFoliageImpluseDirection = FoliageImpluseDirection;
OutFoliageNormalizedRotationAxisAndAngle = FoliageNormalizedRotationAxisAndAngle;
}
/** Updates the scene proxy with new foliage parameters from the game thread. */
void UpdateParameters_GameThread(const FVector& NewFoliageImpluseDirection, const FVector4& NewFoliageNormalizedRotationAxisAndAngle)
{
checkSlow(IsInGameThread());
FInteractiveFoliageSceneProxy* FoliageProxy = this;
ENQUEUE_RENDER_COMMAND(UpdateFoliageParameters)(
[FoliageProxy, NewFoliageImpluseDirection, NewFoliageNormalizedRotationAxisAndAngle](FRHICommandListImmediate& RHICmdList)
{
FoliageProxy->FoliageImpluseDirection = NewFoliageImpluseDirection;
FoliageProxy->FoliageNormalizedRotationAxisAndAngle = NewFoliageNormalizedRotationAxisAndAngle;
});
}
protected:
FVector FoliageImpluseDirection;
FVector4 FoliageNormalizedRotationAxisAndAngle;
};
UInteractiveFoliageComponent::UInteractiveFoliageComponent(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
}
FPrimitiveSceneProxy* UInteractiveFoliageComponent::CreateSceneProxy()
{
// If a static mesh does not exist then this component cannot be added to the scene.
if(GetStaticMesh() == NULL
|| GetStaticMesh()->GetRenderData() == NULL
|| GetStaticMesh()->GetRenderData()->LODResources.Num() == 0
|| GetStaticMesh()->GetRenderData()->LODResources[0].VertexBuffers.StaticMeshVertexBuffer.GetNumVertices() == 0)
{
return NULL;
}
// Store the foliage proxy so we can push updates to it during Tick
FoliageSceneProxy = new FInteractiveFoliageSceneProxy(this);
return FoliageSceneProxy;
}
void UInteractiveFoliageComponent::DestroyRenderState_Concurrent()
{
Super::DestroyRenderState_Concurrent();
FoliageSceneProxy = NULL;
}
float AInteractiveFoliageActor::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
FHitResult Hit;
FVector ImpulseDir;
DamageEvent.GetBestHitInfo(this, (EventInstigator ? EventInstigator->GetPawn() : NULL), Hit, ImpulseDir);
// Discard the magnitude of the momentum and use Damage as the length instead
FVector DamageImpulse = ImpulseDir.GetSafeNormal() * DamageAmount * FoliageDamageImpulseScale;
// Apply force magnitude clamps
DamageImpulse.X = FMath::Clamp<FVector::FReal>(DamageImpulse.X, -MaxDamageImpulse, MaxDamageImpulse);
DamageImpulse.Y = FMath::Clamp<FVector::FReal>(DamageImpulse.Y, -MaxDamageImpulse, MaxDamageImpulse);
DamageImpulse.Z = FMath::Clamp<FVector::FReal>(DamageImpulse.Z, -MaxDamageImpulse, MaxDamageImpulse);
FoliageForce += DamageImpulse;
// Bring this actor out of stasis so that it gets ticked now that a force has been applied
SetActorTickEnabled(true);
return 0.f;
}
void AInteractiveFoliageActor::CapsuleTouched(UPrimitiveComponent* OverlappedComp, AActor* Other, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
if (Other != NULL && OtherComp != NULL)
{
UCapsuleComponent* const TouchingActorCapsule = Cast<UCapsuleComponent>(OtherComp);
if (TouchingActorCapsule && CapsuleComponent)
{
const FVector CenterToTouching = FVector(TouchingActorCapsule->Bounds.Origin.X, TouchingActorCapsule->Bounds.Origin.Y, CapsuleComponent->Bounds.Origin.Z) - CapsuleComponent->Bounds.Origin;
// Keep track of the first position on the collision cylinder that the touching actor intersected
//@todo - need to handle multiple touching actors
TouchingActorEntryPosition = GetRootComponent()->Bounds.Origin + CenterToTouching.GetSafeNormal() * CapsuleComponent->GetScaledCapsuleRadius();
}
// Bring this actor out of stasis so that it gets ticked now that a force has been applied
SetActorTickEnabled(true);
}
}
void AInteractiveFoliageActor::SetupCollisionCylinder()
{
if (GetStaticMeshComponent()->GetStaticMesh())
{
const FBoxSphereBounds MeshBounds = GetStaticMeshComponent()->GetStaticMesh()->GetBounds();
const FVector Scale3D = GetStaticMeshComponent()->GetRelativeScale3D();
// Set the cylinder's radius based off of the static mesh's bounds radius
// CollisionRadius is in world space so apply the actor's scale
CapsuleComponent->SetCapsuleSize(static_cast<float>(MeshBounds.SphereRadius * .7f * FMath::Max(Scale3D.X, Scale3D.Y)), static_cast<float>(MeshBounds.BoxExtent.Z * Scale3D.Z));
// Ensure delegate is bound (just once)
CapsuleComponent->OnComponentBeginOverlap.RemoveDynamic(this, &AInteractiveFoliageActor::CapsuleTouched);
CapsuleComponent->OnComponentBeginOverlap.AddDynamic(this, &AInteractiveFoliageActor::CapsuleTouched);
}
}
void AInteractiveFoliageActor::Tick(float DeltaSeconds)
{
UInteractiveFoliageComponent* const FoliageComponent = CastChecked<UInteractiveFoliageComponent>(GetStaticMeshComponent());
// Can only push updates to the scene proxy if we are being ticked while registered
// The proxy will be NULL on dedicated server
if (FoliageComponent->IsRegistered() && FoliageComponent->FoliageSceneProxy)
{
TSet<AActor*> Touching;
GetOverlappingActors(Touching);
for (AActor* TouchingActor : Touching)
{
if (TouchingActor != NULL && TouchingActor->GetRootComponent() != NULL)
{
const FVector TouchingActorPosition(TouchingActor->GetRootComponent()->Bounds.Origin.X, TouchingActor->GetRootComponent()->Bounds.Origin.Y, GetRootComponent()->Bounds.Origin.Z);
//DrawDebugLine(GetWorld(), TouchingActorPosition, GetRootComponent()->Bounds.Origin, 255, 255, 255, false);
// Operate on the touching actor's collision cylinder
//@todo - handle touching actors without collision cylinders
UCapsuleComponent* TouchingActorCapsule = Cast<UCapsuleComponent>(TouchingActor->GetRootComponent());
if (TouchingActorCapsule && CapsuleComponent)
{
FVector TouchingToCenter = GetRootComponent()->Bounds.Origin - TouchingActorPosition;
// Force the simulated position to be in the XY plane for simplicity
TouchingToCenter.Z = 0;
// Position on the collision cylinder mirrored across the cylinder's center from the position that the touching actor entered
const FVector OppositeTouchingEntryPosition = GetRootComponent()->Bounds.Origin + GetRootComponent()->Bounds.Origin - TouchingActorEntryPosition;
// Project the touching actor's center onto the vector from where it first entered to OppositeTouchingEntryPosition
// This results in the same directional force being applied for the duration of the other actor touching this foliage actor,
// Which prevents strange movement that results from just comparing cylinder centers.
const FVector ProjectedTouchingActorPosition = (TouchingActorPosition - OppositeTouchingEntryPosition).ProjectOnTo(TouchingActorEntryPosition - OppositeTouchingEntryPosition) + OppositeTouchingEntryPosition;
// Find the furthest position on the cylinder of the touching actor from OppositeTouchingEntryPosition
const FVector TouchingActorFurthestPosition = ProjectedTouchingActorPosition + (TouchingActorEntryPosition - OppositeTouchingEntryPosition).GetSafeNormal() * TouchingActorCapsule->GetScaledCapsuleRadius();
// Construct the impulse as the distance between the furthest cylinder positions minus the two cylinder's diameters
const FVector ImpulseDirection =
- (OppositeTouchingEntryPosition - TouchingActorFurthestPosition
- (OppositeTouchingEntryPosition - TouchingActorFurthestPosition).GetSafeNormal() * 2.0f * (TouchingActorCapsule->GetScaledCapsuleRadius() + CapsuleComponent->GetScaledCapsuleRadius()));
//DrawDebugLine(GetWorld(), GetRootComponent()->Bounds.Origin + FVector(0,0,100), GetRootComponent()->Bounds.Origin + ImpulseDirection + FVector(0,0,100), 100, 255, 100, false);
// Scale and clamp the touch force
FVector Impulse = ImpulseDirection * FoliageTouchImpulseScale;
Impulse.X = FMath::Clamp<FVector::FReal>(Impulse.X, -MaxTouchImpulse, MaxTouchImpulse);
Impulse.Y = FMath::Clamp<FVector::FReal>(Impulse.Y, -MaxTouchImpulse, MaxTouchImpulse);
Impulse.Z = FMath::Clamp<FVector::FReal>(Impulse.Z, -MaxTouchImpulse, MaxTouchImpulse);
FoliageForce += Impulse;
}
}
}
// Apply spring stiffness, which is the force that pushes the simulated particle back to the origin
FoliageForce += -FoliageStiffness * FoliagePosition;
// Apply spring quadratic stiffness, which increases in magnitude with the square of the distance to the origin
// This prevents the spring from being displaced too much by touch and damage forces
FoliageForce += -FoliageStiffnessQuadratic * FoliagePosition.SizeSquared() * FoliagePosition.GetSafeNormal();
// Apply spring damping, which is like air resistance and causes the spring to lose energy over time
FoliageForce += -FoliageDamping * FoliageVelocity;
FoliageForce.X = FMath::Clamp<FVector::FReal>(FoliageForce.X, -MaxForce, MaxForce);
FoliageForce.Y = FMath::Clamp<FVector::FReal>(FoliageForce.Y, -MaxForce, MaxForce);
FoliageForce.Z = FMath::Clamp<FVector::FReal>(FoliageForce.Z, -MaxForce, MaxForce);
FoliageVelocity += FoliageForce * DeltaSeconds;
FoliageForce = FVector::ZeroVector;
const float MaxVelocity = 1000.0f;
FoliageVelocity.X = FMath::Clamp<FVector::FReal>(FoliageVelocity.X, -MaxVelocity, MaxVelocity);
FoliageVelocity.Y = FMath::Clamp<FVector::FReal>(FoliageVelocity.Y, -MaxVelocity, MaxVelocity);
FoliageVelocity.Z = FMath::Clamp<FVector::FReal>(FoliageVelocity.Z, -MaxVelocity, MaxVelocity);
FoliagePosition += FoliageVelocity * DeltaSeconds;
//DrawDebugLine(GetWorld(), GetRootComponent()->Bounds.Origin + FVector(0,0,100), GetRootComponent()->Bounds.Origin + FVector(0,0,100) + FoliagePosition, 255, 100, 100, false);
//@todo - derive this height from the static mesh
const float IntersectionHeight = 100.0f;
// Calculate the rotation angle using Sin(Angle) = Opposite / Hypotenuse
const FVector::FReal RotationAngle = -FMath::Asin(FoliagePosition.Size() / IntersectionHeight);
// Use a rotation angle perpendicular to the impulse direction and the z axis
const FVector NormalizedRotationAxis = FoliagePosition.SizeSquared() > KINDA_SMALL_NUMBER ?
(FoliagePosition ^ FVector(0,0,1)).GetSafeNormal() :
FVector(0,0,1);
// Propagate the new rotation axis and angle to the rendering thread
FoliageComponent->FoliageSceneProxy->UpdateParameters_GameThread(FoliagePosition, FVector4(NormalizedRotationAxis, RotationAngle));
if (FoliagePosition.SizeSquared() < FMath::Square(KINDA_SMALL_NUMBER * 10.0f)
&& FoliageVelocity.SizeSquared() < FMath::Square(KINDA_SMALL_NUMBER * 10.0f))
{
// Go into stasis (will no longer be ticked) if this actor's spring simulation has stabilized
SetActorTickEnabled(false);
}
}
Super::Tick(DeltaSeconds);
}
void AInteractiveFoliageActor::PostActorCreated()
{
Super::PostActorCreated();
SetupCollisionCylinder();
}
void AInteractiveFoliageActor::PostLoad()
{
Super::PostLoad();
SetupCollisionCylinder();
}