Realization of UE4_WindField Ares Wind Field Field Wind Injection

In the first few parts of the engine, from the object definition of rendering threads and logical threads to the fluid simulation of wind force in the algorithm, the basic wind simulation framework is built, and the preliminary debugging method is added. Everything in the engine has a basic look, but the most important thing is the external input, before the external input. Entry is simply replaced by a very simple Apply External WindSource. In this part, we will improve the external wind injection.

Look at the effect first. The previous part already has a directional wind source. This part looks at other types.

Omni wind source

Eddy current source

Catalog

1. Conversion from World Space to Texture Space

2. ADynamic Wind Motor Actor Wind Launcher Actor

3. UDynamic Wind Motor Component Wind Launch Component

4. Visual Motor Launcher

Before really injecting wind, we have another problem. Previous calculations were done directly in the texture space. For example, Apply External WindSource is injected directly into a coordinate in the texture space, but what we really need is a wind field moving with the player in the world space, so injecting wind. Force must be calculated in world space, and as players move, the results of previous simulation also need to do a Merge conversion from world space to texture space as players move. Before we really inject wind, we first solve this problem.

1. Conversion from World Space to Texture Space

There are two parts to apply conversion, one is the place of input, the other is the place of output application, the material of vegetation needs to be able to transform its own coordinates into texture space coordinates for sampling.

First of all, each frame will check the player's position, check whether the player moves beyond a certain range, and calculate an Offset. Then, after scaling the Offset according to the size of the texture, the former Velocity Texture will be offset and copied directly in the texture range, and the wind force will be transferred out of the range. Set to 0.

In the output area, several coefficients should be given to the material, one is the world coordinate position of the texture center, so that the material of vegetation can be calculated to the world space offset of the texture center according to its own coordinates, the other is the zoom coefficient of the world space offset mapped to the texture space, so that the material can be calculated inside. Sampling coordinates on texture.

Then it's implemented. UDynamic WindField Component adds the corresponding variables

virtual void UpdateCenterLocationInfo(); //Update Center Current Location per Frame
FVector WindFieldLocationCalcUnit; //Scaling coefficients of axes in space conversion
FVector CenterCurrentLocation; //Current Real Player Center Location
FVector CenterHistoryLocation; //Texture Center Location for Computing

bool bIsCenterLocCalced; //Flag bit, used to determine whether or not the Component's CurrentLocation is updated in each frame

UPROPERTY(EditAnywhere, Category = "DynamicWindFieldComponent")
float CenterMoveRadius; //Configurable radius of update center location

UPROPERTY(EditAnywhere, Category = "DynamicWindFieldComponent")
FVector WindFieldRange; //The scale factor is the size of the range of configurable actual wind power divided by the size of the texture.

The scaling factor is calculated according to the configuration at the beginning and passed to the material container for use.

WindFieldLocationCalcUnit = FVector(WindFieldRange.X / WindFieldResourceSizeX, WindFieldRange.Y / WindFieldResourceSizeX, WindFieldRange.Z / WindFieldResourceSizeZ);

Update Center Location Info, as the commentary says, is to get the player's current position per frame according to the controller.

In Tick, we check whether we need to move the texture center according to the configuration of enterMoveRadius. If we need to move, we will set up History Location, pass new coordinates to the material container to allow the material to use, and notify Simulator to update the central coordinates of the texture space.

	if (!bIsCenterLocCalced)
		UpdateCenterLocationInfo();

	bool bCenterLocUpdated = false;
	if (CenterMoveRadius == 0.f || FVector::Distance(CenterCurrentLocation, CenterHistoryLocation) > (CenterMoveRadius + KINDA_SMALL_NUMBER))
	{
		if (NatureMPCInst)
			NatureMPCInst->SetVectorParameterValue(FiledTextureCenterLocName, CenterCurrentLocation);

		CenterHistoryLocation = CenterCurrentLocation;
		bCenterLocUpdated = true;
	}

Then Simulator updates, send RHICommand updates

	FDynamicWindFieldSimulator* Simulator = &WindFieldSimulator;

	ENQUEUE_RENDER_COMMAND(FDynamicWindFieldTickCommand)(
		[Simulator, DeltaTime, this, bCenterLocUpdated](FRHICommandList& RHICmdList)
	{
		Simulator->DeltaTime = DeltaTime;
		Simulator->CurrentFrameNumber = GFrameNumber;
		if(bCenterLocUpdated)
			Simulator->FieldPivotCurrentLocation = this->CenterCurrentLocation - WindFieldRange / 2.0f;
	});

	bIsCenterLocCalced = false;

Here are a few details. One is that Simulator's FieldPivot Current Location anchor coordinates need to be subtracted by half the wind field range size, because the texture coordinate origin is in the upper left corner, and the other is that the tick end of each frame clears the mark indicating whether the frame has updated Current Location, mainly because of Component. The order of ticks is not guaranteed. In each frame, if other wind transmitters want to inject wind before FieldComponent updates its position, they also want to use the latest CurrentLocation at that time. Therefore, if other wind transmitters tick first, check that FiledComponent's own tick has not gone yet, it is up to them to The other Components call and update first.

In Simulator, there are also two vector s to represent History and Current, and there will be an Apply History Offset method to call CS to update Velocity Texture.

FVector FieldPivotHistoryLocation;
FVector FieldPivotCurrentLocation;

template<bool bUse2DTemplate>
void ApplyHistoryOffset(FRHICommandListImmediate& RHICmdList);

If the Field Pivot Current Location change is checked, Apply History Offset is called.

//Move history by location.
if (FieldPivotHistoryLocation != FieldPivotCurrentLocation)
{
	if(bUse2D)
		ApplyHistoryOffset<true>(RHICmdList);
	else
		ApplyHistoryOffset<false>(RHICmdList);
}

The calculation in Shader is,

The reference History Offset = Field Pivot History Location - Field Pivot Current Location / Owner - > WindField Range, converts a texture space offset outside, and then the internal logic is to check the offset and copy within the range, or discard it, where the copy is a linear sampling copy.

float3 HistoryOffset;

[numthreads(THREADS_X, THREADS_Y, THREADS_Z)]
void ApplyHistoryOffset(
	uint3 DispatchThreadId : SV_DispatchThreadID,
	uint3 GroupId : SV_GroupID,
	uint3 GroupThreadId : SV_GroupThreadID,
	uint ThreadId : SV_GroupIndex)
{
	float3 OldCoords = GetLocationTexCoords(DispatchThreadId);
	float3 NewCoords = OldCoords - HistoryOffset;
	if (NewCoords.x <= 0 || NewCoords.x >= 1 || NewCoords.y <= 0 || NewCoords.y >= 1)
	{
		OutVelocityFieldTexture[DispatchThreadId] = 0;
	}
	else
	{
		OutVelocityFieldTexture[DispatchThreadId] = CurrentVelocityFieldTexture.SampleLevel(LinearTextureSampler, NewCoords, 0);
	}
}

In this way, the Velocity Texture can be moved as the player moves.

2. ADynamic Wind Motor Actor Wind Launcher Actor

After space conversion, we can create the object of wind power launch in the world space. Because the main logic of wind power launch is written on Component, so Attach can be on any object, so this Actor can start easily, as long as there is a wind power launch component.

class ENGINE_API ADynamicWindMotorActor : public AInfo
{
	GENERATED_UCLASS_BODY()

private:
	UPROPERTY(Category = WindField, VisibleAnywhere, BlueprintReadOnly)
	class UDynamicWindMotorComponent* Component;

public:
	/** Returns Component subobject **/
	class UDynamicWindMotorComponent* GetComponent() const { return Component; }
}

Later in this section, we will add some visual things to Actor to facilitate debugging, but this will be enough for us to improve the function first.

3. UDynamic Wind Motor Component Wind Launch Component

This is where the main functions are carried. First of all, let's define how many types of mid-wind launches there are. Following Warlord's example, let's first define three types of mid-wind launches.

UENUM()
enum class EWindFieldSourceType:uint8
{
	Directional, //Directional wind source
	Omni, //Pan wind source
	Vortex, //Vortex wind source
};

Define a wind event, which is sent to Simulator by each frame of the Wind Launch Component and consumed by Simulator.

USTRUCT(/*BlueprintType*/)
struct FWindFieldSourceEvent
{
	EWindFieldSourceType SourceType; //Wind source type

	float DeltaTime; //Stepping interval

	float Strength; //Strength

	float Radius; //radius

	FVector Position; //World Space Position

	FVector Direction; //wind direction

	uint32 FrameNumberWhenAdd; //The injected frame stamp prevents the accumulation of too many events from being processed, and the event used to judge events that are too long is discarded.
};

The MotorComponent declares control items similar to those required and injection functions for the corresponding wind source types.

class ENGINE_API UDynamicWindMotorComponent : public USceneComponent
{
	virtual void AddDirectionalWindFiledSource(float DeltaTime, float InStrength, float InRadius, FVector const& InPosition, FVector const& InDirection);
	virtual void AddOmniWindFiledSource(float DeltaTime, float InStrength, float InRadius, FVector const& InPosition);
	virtual void AddVortexWindFiledSource(float DeltaTime, float InStrength, float InRadius, FVector const& InPosition, FVector const& InAxis);
	
	EWindFieldSourceType SourceType; //Wind source type

	float Strength; //Strength

	float Frequency; //Injection frequency for intensity sampling

	float Radius; //radius
...
}

In Tick of Component, after intensity sampling, the injection method is called according to the type of wind source.

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

	ElapsedTime += DeltaTime;

	switch (SourceType)
	{
	case EWindFieldSourceType::Directional:
		AddDirectionalWindFiledSource(DeltaTime, GetWindStrength(DeltaTime), Radius, GetComponentLocation(), GetComponentQuat().GetForwardVector());
		break;
	case EWindFieldSourceType::Omni:
		AddOmniWindFiledSource(DeltaTime, GetWindStrength(DeltaTime), Radius, GetComponentLocation());
		break;
	case EWindFieldSourceType::Vortex:
		AddVortexWindFiledSource(DeltaTime, GetWindStrength(DeltaTime), Radius, GetComponentLocation(), GetComponentQuat().GetUpVector());
		break;
	default:
		break;
	}
}

Here's the strength curve. I'm looking for this one.

Strength * FMath::Sin(2.f * M_PI / 23.f * Frequency * ElapsedTime + Phase) + FMath::Sin(2.f * M_PI / 28.f * Frequency * ElapsedTime + Phase) + FMath::Sin(2.f * M_PI / 33.f * Frequency * ElapsedTime + Phase) + 3.0f);

This is because 232833 seems to correspond to the renewal time of body, mind and spirit. Haha, writing code is also a metaphysical thing.~

It looks like this. https://www.desmos.com/calculator/j88bwwj3vh

Of course, you can try something else.

Then we call the injection event of FieldComponent, add the corresponding method in FieldComponent, and pass the event in.

class ENGINE_API UDynamicWindFieldComponent : public USceneComponent
{
...
	virtual void AddDirectionalWindFiledSource(float InDeltaTime, float InStrength, float InRadius, FVector const& InPosition, FVector const& InDirection);
	virtual void AddOmniWindFiledSource(float InDeltaTime, float InStrength, float InRadius, FVector const& InPosition);
	virtual void AddVortexWindFiledSource(float InDeltaTime, float InStrength, float InRadius, FVector const& InPosition, FVector const& InAxis);
}

FieldComponent is sent to Simulator. Take Directional Wind as an example. First, as mentioned above, check if the frame's central location is updated and correct, and then pass event to join Simulator's WindField Source Events array.

void UDynamicWindFieldComponent::AddDirectionalWindFiledSource(float InDeltaTime, float InStrength, float InRadius, FVector const& InPosition, FVector const& InDirection)
{
	if (CheckEventInvalid(InStrength, InRadius, InPosition))
		return;

	FDynamicWindFieldSimulator* Simulator = &WindFieldSimulator;

	ENQUEUE_RENDER_COMMAND(FDynamicWindFieldTickCommand)(
		[Simulator, InDeltaTime, InStrength, InRadius, InPosition, InDirection](FRHICommandList& RHICmdList)
	{
		FWindFieldSourceEvent SourceEvent(EWindFieldSourceType::Directional, InDeltaTime, InStrength, InRadius, InPosition, InDirection, GFrameNumber);
		Simulator->WindFieldSourceEvents.Add(SourceEvent);
	});
}

After that, Simulator consumes the events in the array at the PreRender of each frame. Here, Simulator adds the corresponding method.

void ApplySourceEvents(FRHICommandListImmediate& RHICmdList);

template<bool bUse2DTemplate>
void ApplyDirectionalWindFiledSource(FRHICommandListImmediate& RHICmdList, FWidFieldSourceEvent &DirectionalEvent);
template<bool bUse2DTemplate>
void ApplyOmniWindFiledSource(FRHICommandListImmediate& RHICmdList, FWindFieldSourceEvent &OmniEvent);
template<bool bUse2DTemplate>
void ApplyVortexWindFiledSource(FRHICommandListImmediate& RHICmdList, FWindFieldSourceEvent &VortexEvent);

ApplySource Events is responsible for the overall scheduling and checks whether the timestamp is correct. If it is not the frame, it will be discarded and the accumulated data will not be processed.

void FDynamicWindFieldSimulator::ApplySourceEvents(FRHICommandListImmediate& RHICmdList)
{
	if (WindFieldSourceEvents.Num() == 0)
		return;

	for (int32 Index = 0; Index < WindFieldSourceEvents.Num(); ++Index)
	{
		if(CurrentFrameNumber != WindFieldSourceEvents[Index].FrameNumberWhenAdd)
			continue;

		switch (WindFieldSourceEvents[Index].SourceType)
		{
		case EWindFieldSourceType::Directional:
			if(bUse2D)
				ApplyDirectionalWindFiledSource<true>(RHICmdList, WindFieldSourceEvents[Index]);
			else
				ApplyDirectionalWindFiledSource<false>(RHICmdList, WindFieldSourceEvents[Index]);
			break;
		case  EWindFieldSourceType::Omni:
			if(bUse2D)
				ApplyOmniWindFiledSource<true>(RHICmdList, WindFieldSourceEvents[Index]);
			else
				ApplyOmniWindFiledSource<false>(RHICmdList, WindFieldSourceEvents[Index]);
			break;
		case  EWindFieldSourceType::Vortex:
			if (bUse2D)
				ApplyVortexWindFiledSource<true>(RHICmdList, WindFieldSourceEvents[Index]);
			else
				ApplyVortexWindFiledSource<false>(RHICmdList, WindFieldSourceEvents[Index]);
			break;
		default:
			break;
		}
	}

	WindFieldSourceEvents.Empty();
}

Then there is the concrete CS processing logic. Take the direction wind source Apply Direct Wind Filed Source as an example.

ApplyDirectionalWindFiledSourceCS->SetParameters(
	RHICmdList,
	DirectionalEvent.Strength * DirectionalEvent.Direction * DirectionalEvent.DeltaTime, //Strength value is strength * direction * Timestep time step
	(DirectionalEvent.Position - FieldPivotCurrentLocation) / Owner->WindFieldLocationCalcUnit, //Scale the position to the size of the texture space
    //The radius is also scaled. In order to save CS calculation, square or cubic values are first calculated and then passed in.
	FMath::Pow(DirectionalEvent.Radius , bUse2D ? 2 : 3) / FMath::Pow(Owner->WindFieldLocationCalcUnit.X, bUse2D ? 2 : 3)); 

CS implementation, which basically imitates the implementation in Warlord's PPT, takes Apply Direct WindFiledSource as an example

[numthreads(THREADS_X, THREADS_Y, THREADS_Z)]
void ApplyDirectionalWindFiledSource(
	uint3 DispatchThreadId : SV_DispatchThreadID,
	uint3 GroupId : SV_GroupID,
	uint3 GroupThreadId : SV_GroupThreadID,
	uint ThreadId : SV_GroupIndex)
{
	//WindVelocity = direction * strength * deltaTime
	float distanceSq = LengthSquare(DispatchThreadId - WindPosition);
	if (distanceSq < WindRadiusSq)
		OutVelocityFieldTexture[DispatchThreadId] += float4(WindVelocity, 0.f);
}

Basically the same, but also very simple, is to judge whether the scope, if again within the scope, add the wind speed.

So far, the injection of wind power has basically been completed, but here is a notable point, but also the problem I encountered is that the speed here is added directly, so although the previous strength sampling curve is a smooth sin function, but the direct addition here leads to the speed is still not smooth, so after this. It will need to be dealt with again.

4. Visual Motor Launcher

Finally, add some visualization to these transmitters to facilitate instant viewing, such as adding Arrow to Directional Wind and adding Billboard icon to Omni.

The logic is also relatively simple. When constructing, find the corresponding Texture, initialize Arrow and Billboard components.

#if WITH_EDITORONLY_DATA
	SetActorHiddenInGame(false);

	ArrowComponent = CreateEditorOnlyDefaultSubobject<UArrowComponent>(TEXT("ArrowComponent0"));

	static ConstructorHelpers::FObjectFinderOptional<UTexture2D> SpriteDirectionalTexture(TEXT("/Engine/EditorResources/S_WindDirectional"));
	DirectionalTexture = SpriteDirectionalTexture.Get();

	static ConstructorHelpers::FObjectFinderOptional<UTexture2D> SpriteOmniTexture(TEXT("/Engine/EditorResources/S_Emitter"));
	OmniTexture = SpriteOmniTexture.Get();

	static ConstructorHelpers::FObjectFinderOptional<UTexture2D> SpriteVoetexTexture(TEXT("/Engine/EditorResources/Ai_Spawnpoint"));
	VortexTexture = SpriteVoetexTexture.Get();

	if (!IsRunningCommandlet())
	{
		// Structure to hold one-time initialization
		struct FConstructorStatics
		{
			FName ID_WindMotor;
			FText NAME_WindMotor;
			FConstructorStatics()
				: ID_WindMotor(TEXT("WindMotor"))
				, NAME_WindMotor(NSLOCTEXT("SpriteCategory", "WindMotor", "WindMotor"))
			{
			}
		};
		static FConstructorStatics ConstructorStatics;

		if (ArrowComponent)
		{
			ArrowComponent->ArrowColor = FColor(150, 200, 255);
			ArrowComponent->bTreatAsASprite = true;
			ArrowComponent->SpriteInfo.Category = ConstructorStatics.ID_WindMotor;
			ArrowComponent->SpriteInfo.DisplayName = ConstructorStatics.NAME_WindMotor;
			ArrowComponent->SetupAttachment(Component);
			ArrowComponent->bIsScreenSizeScaled = true;
			ArrowComponent->bUseInEditorScaling = true;
			ArrowComponent->SetEditorScale(3.0f);
		}

		if (UBillboardComponent* SpriteComp = GetSpriteComponent())
		{
			SpriteComp->Sprite = DirectionalTexture;
			SpriteComp->RelativeScale3D = FVector(0.5f, 0.5f, 0.5f);
			SpriteComp->SpriteInfo.Category = ConstructorStatics.ID_WindMotor;
			SpriteComp->SpriteInfo.DisplayName = ConstructorStatics.NAME_WindMotor;
			SpriteComp->SetupAttachment(Component);
			SpriteComp->SetHiddenInGame(false);
			SpriteComp->SetEditorScale(3.0f);
		}
	}
#endif // WITH_EDITORONLY_DATA

Finally, when running the PostLoad phase, and when editing and modifying the PostEditChangeProperty, update the status

#if WITH_EDITORONLY_DATA

	if(ArrowComponent->bHiddenInGame == true)
		ArrowComponent->SetHiddenInGame(false);

	if (UDynamicWindMotorComponent* MotorComponent = GetComponent())
	{
		switch (MotorComponent->SourceType)
		{
		case EWindFieldSourceType::Directional:
			if (ArrowComponent)
			{
				ArrowComponent->SetVisibility(true);			
			}
			if (UBillboardComponent* SpriteComp = GetSpriteComponent())
			{
				SpriteComp->SetSprite(DirectionalTexture);
			}
			break;
		case EWindFieldSourceType::Omni:
			if (ArrowComponent)
			{
				ArrowComponent->SetVisibility(false);
			}
			if (UBillboardComponent* SpriteComp = GetSpriteComponent())
			{
				SpriteComp->SetSprite(OmniTexture);
			}
			break;
		case EWindFieldSourceType::Vortex:
			if (ArrowComponent)
			{
				ArrowComponent->SetVisibility(true);
			}
			if (UBillboardComponent* SpriteComp = GetSpriteComponent())
			{
				SpriteComp->SetSprite(VortexTexture);
			}
			break;
		default:
			break;
		}
	}
#endif // WITH_EDITORONLY_DATA

Okay, wind injection, that's about it.~

Tags: simulator calculator

Posted on Thu, 12 Sep 2019 07:15:52 -0700 by ViperSBT