Realization of UE4_WindField Ares Wind Field Preliminary Debugging

Posted by QSDragon on Wed, 18 Sep 2019 07:47:39 +0200

The previous part built the basic framework of wind simulation in UE4, but now you can only see the empty code, one is very inconvenient to debug, and can not be applied to the game, after all, in order to use various interactive objects in the game, it is better to output a 2D or 3D stripe. In principle, before further development, we need to improve the basic debugging tools and functions to facilitate future development.

In this part, our main goal is to output texture to RenderTarget 2D and visualize the wind simulation results. In addition, we use RenderDoc to realize single-step debugging, which can make the simulation process more controllable.

One by one~

Catalog

1. RenderTarget 2D Visual Texture Output

(1) Create UAV

(2) Shader Modification in 2D Space Computing

(3) Modifying GlobalShader's template declaration

(4) Modifying template declarations in method calls

(5) In PreRender, output switching is done according to Use2D configured on Component

(6) Finally, Output2D is exported to RenderTarget

2.RenderDoc single-step debugging

1. RenderTarget 2D Visual Texture Output

Look at the effect first. Here's a Directional Wind that diffuses every 1S, such as once into the wind field.

Since UE4 currently does not support RenderTarget 3D in 4.22.3, there are many changes needed to add this. This will be completed later. Now we begin to output the results to a 2D texture, and the simulation calculation is also simulated in 2D space. First, we can see a real-time dynamic result.

(1) Create UAV

First, the properties of bUse2D are added to UDynamic WindField Component and FDynamic WindField Simulator. When editing the properties in the editor, the rendering thread is notified to update the data by PostEditChangeProperty, so that the output of 2D and 3D can be switched in real time.

Then, in UDynamic WindField Component, add the reference of UTextureRenderTarget 2D and corresponding UAV, and the corresponding initialization method.

UPROPERTY(EditAnywhere, Category = "DynamicWindFieldComponent")
UTextureRenderTarget2D* WindFieldRT;

FUnorderedAccessViewRHIRef WindFieldRTUAV;

void InitWindFieldRTUAV();
void ReleaseWindFiledRTUAV();

Looking at the UTextureRenderTarget code, you can see that it has an attribute uint32 bCanCreateUAV: 1; but it is not open by UProperty, so you need to manually set up RenderResource to update it to create UAV.

Improve the initialization method.

void UDynamicWindFieldComponent::InitWindFieldRTUAV()
{
	if (!WindFieldRT)
		return;

	WindFieldRT->bCanCreateUAV = true;
	WindFieldRT->UpdateResource();

	ENQUEUE_RENDER_COMMAND(FDynamicWindFieldInitWindFieldRTUAV)(
		[this](FRHICommandList& RHICmdList)
	{
		this->WindFieldRTUAV = RHICreateUnorderedAccessView(this->WindFieldRT->GetRenderTargetResource()->TextureRHI);
	});
}

void UDynamicWindFieldComponent::ReleaseWindFiledRTUAV()
{
	if (!WindFieldRT || !this->WindFieldRTUAV)
		return;
	ENQUEUE_RENDER_COMMAND(FDynamicWindFieldReleaseWindFiledRTUAV)(
		[this](FRHICommandList& RHICmdList)
	{
		this->WindFieldRTUAV.SafeRelease();
	});
}

RenderTarget's preparations were completed and then used.

(2) Shader Modification in 2D Space Computing

That is to say, adding USE_2D_SPACE macro-partition to shader, adding the calculation of 2D space.

Take the calculation of advection sampling coordinates as an example, that is, only the velocity on xy can be calculated.

float3 GetAdvectedPosTexCoords(uint3 TexCoords)
{
	float3 Position = TexCoords;

#if !USE_2D_SPACE
	float3 Velocity = CurrentVelocityFieldTexture.Load(int4(TexCoords, 0)).xyz;
#else
	float3 Velocity = float3(CurrentVelocityFieldTexture.Load(int4(TexCoords, 0)).xy, 0.f);
#endif

	Position -= Timestep * Forward * Velocity;

	Position += float3(0.5, 0.5, 0.5);

	return float3( Position.x / TextureResolution.x, Position.y / TextureResolution.y, Position.z / TextureResolution.z);
}

(3) Modifying GlobalShader's template declaration

Take FApply External WindSource CS as an example, compile two versions of Shader, 2D and 3D, according to input.

template<bool bUse2D>
class FApplyExternalWindSourceCS : public FGlobalShader
{
	DECLARE_SHADER_TYPE(FApplyExternalWindSourceCS, Global);
public:
...
	static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
	{
		FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
...
		OutEnvironment.SetDefine(TEXT("USE_2D_SPACE"), bUse2D);
	}
...
};
IMPLEMENT_SHADER_TYPE(template<>, FApplyExternalWindSourceCS<false>, TEXT("/Engine/Private/DynamicWindField.usf"), TEXT("ApplyExternalWindSource"), SF_Compute);
IMPLEMENT_SHADER_TYPE(template<>, FApplyExternalWindSourceCS<true>, TEXT("/Engine/Private/DynamicWindField.usf"), TEXT("ApplyExternalWindSource"), SF_Compute);

(4) Modifying template declarations in method calls

Take FDynamic WindField Simulator:: Apply External WindSource as an example

template<bool bUse2DTemplate>
void FDynamicWindFieldSimulator::ApplyExternalWindSource(FRHICommandListImmediate& RHICmdList, FVector WindPositon, float VelocityScaling)
{
	SCOPED_DRAW_EVENT(RHICmdList, ApplyExternalWindSource);

	TShaderMapRef<FApplyExternalWindSourceCS<bUse2DTemplate>> ApplyExternalWindSourceCS(GetGlobalShaderMap(GetFeatureLevel()));

	...
}

(5) In PreRender, output switching is done according to Use2D configured on Component

In PreRender, simple if else calls different methods

void FDynamicWindFieldSimulator::PreRender(FRHICommandListImmediate& RHICmdList, const FSceneViewFamily& ViewFamily)
{
...
	while (SubstepDeltaTime > 0.f && SubstepIndex < MaxTimesteps)
	{

		if (bUse2D)
		{
			AdvectVelocity<true>(RHICmdList, TimeStep);	
            
            ComputeVelocityDivergence<true>(RHICmdList);

			ComputePressure<true>(RHICmdList);

			ProjectVelocity<true>(RHICmdList);

			Output2D(RHICmdList);
		}
		else
		{
			AdvectVelocity<false>(RHICmdList, TimeStep);	

            ComputeVelocityDivergence<false>(RHICmdList);

			ComputePressure<false>(RHICmdList);

			ProjectVelocity<false>(RHICmdList);

			Output3D(RHICmdList); //After output to 3D texture, add
		}
	}
}

(6) Finally, Output2D is exported to RenderTarget

ComputeShader is very simple. It just sampled the data on Slice 0 of the incoming 3D Velocity Texture and output it directly.

RWTexture2D<float4> Out2DVelocityFieldTexture;
[numthreads(THREADS_X, THREADS_Y, THREADS_Z)]
void Compress3DTo2D(
	uint3 DispatchThreadId : SV_DispatchThreadID,
	uint3 GroupId : SV_GroupID,
	uint3 GroupThreadId : SV_GroupThreadID,
	uint ThreadId : SV_GroupIndex)
{
	float4 value = InVelocityFieldTexture.Load(int4(DispatchThreadId.xy, 0, 0.f));
	Out2DVelocityFieldTexture[DispatchThreadId.xy] = value;
}

Here, by default, 0 layers are sampled, and then the wind is injected into 0 layers. Just remember to set the Z value to 0.

GlobalShder's declaration is very similar to the previous one. Instead of repeating it, write the calling method directly.

void FDynamicWindFieldSimulator::Output2D(FRHICommandListImmediate& RHICmdList)
{
	SCOPED_DRAW_EVENT(RHICmdList, Output2D);

	RHICmdList.ClearTinyUAV(Owner->WindFieldRTUAV, ClearValues);

	TShaderMapRef<FCompress3DTo2DCS> Compress3DTo2DCS(GetGlobalShaderMap(GetFeatureLevel()));

	RHICmdList.SetComputeShader(Compress3DTo2DCS->GetComputeShader());

	Compress3DTo2DCS->SetOutput(RHICmdList, Owner->WindFieldRTUAV);

	Compress3DTo2DCS->SetParameters(
		RHICmdList,
		&Owner->VelocityFieldResource[0]);

	DispatchComputeShader(
		RHICmdList,
		*Compress3DTo2DCS,
		WindFieldResourceSizeX / THREADS_PER_AXIS,
		WindFieldResourceSizeY / THREADS_PER_AXIS,
		1);

	Compress3DTo2DCS->UnbindBuffers(RHICmdList);
}

Clear up the data before, and then pass in the Velocity Texture output from the last stage of the simulation phase.

After that, you can see the effect.~

2.RenderDoc single-step debugging

Single-step debugging should be the most important function for program development. However, at present, all the code logic rendering threads are available, and many of the logic is written in Shader. The process of injecting wind and fluid simulation is uncontrollable. So if we want to debug, we still hope to debug single-step, so that we can do it. See the results of step-by-step simulation.

And it's better to combine it with RenderDoc, one frame at a time, and one step at a time.

Here you need to look at the code where RenderDoc intercepts the frame.

In RenderDocPluginModule.cpp, where the truncation begins and ends

class FRenderDocFrameCapturer
{
public:
	static void BeginCapture(HWND WindowHandle, FRenderDocPluginLoader::RENDERDOC_API_CONTEXT* RenderDocAPI, FRenderDocPluginModule* Plugin)
	{
		UE4_GEmitDrawEvents_BeforeCapture = GetEmitDrawEvents();
		SetEmitDrawEvents(true);

		RENDERDOC_DevicePointer Device = GDynamicRHI->RHIGetNativeDevice();
		RenderDocAPI->StartFrameCapture(Device, WindowHandle);
	}
	static void EndCapture(HWND WindowHandle, FRenderDocPluginLoader::
		RENDERDOC_API_CONTEXT* RenderDocAPI, FRenderDocPluginModule* Plugin)
	{
		RENDERDOC_DevicePointer Device = GDynamicRHI->RHIGetNativeDevice();
		RenderDocAPI->EndFrameCapture(Device, WindowHandle);

		SetEmitDrawEvents(UE4_GEmitDrawEvents_BeforeCapture);

		TGraphTask<FRenderDocAsyncGraphTask>::CreateTask().ConstructAndDispatchWhenReady(ENamedThreads::GameThread, [Plugin]()
		{
			Plugin->StartRenderDoc(FPaths::Combine(*FPaths::ProjectSavedDir(), *FString("RenderDocCaptures")));
		});
	}

private:
	static bool UE4_GEmitDrawEvents_BeforeCapture;
};
bool FRenderDocFrameCapturer::UE4_GEmitDrawEvents_BeforeCapture = false;

As you can see, when Begin Capture, SetEmitDrawEvents(true) will be invoked; if GEmitDrawEvents is set to true and the end place is set back again, it will not be easy for us. As long as we judge at PreRender, GEmitDrawEvents will be true, otherwise it will not be executed, then every frame can be intercepted. Not only once, the intercepted data can also be viewed in RenderDoc.

Okay, it's perfect. ~Wind farm, ask you if you're afraid, huh? You're about to be debugged step by step, huh? Are you afraid, huh?

After three consecutive years, I started to write code. After judging from GetEmitDrawEvents() in PreRender, I found that, no, I kept walking, and found that there was still a small pit here.

The reason for this is that in the plugin above, UE4_GEmitDrawEvents_BeforeCapture is used to cache the state values before starting RenderDoc. It turns out that the previous GEmitDrawEvents have been true, so setup here has not changed. When UE4 starts, it checks that RenderDoc starts, it will be directly tuned. With FDynamic RHI:: Enable Ideal GPUC apture Options (bool B Enabled), GEmitDraw Events is set to true directly from the beginning.

So there's another place to change: comment out // SetEmitDrawEvents(bShouldEnableDrawEvents) in the method of Dynamic RHI. cpp's FDynamic RHI:: Enable Ideal GPUCapture Options (bool bEnabled); this line, by default, is no longer true, so you can debug it step by step.~

Ah, aren't you afraid of the small arena? Huh?

Catalog: Realization of UE4_WindField Ares Wind Field (0)_Overview

Last article: Implementation of UE4_WindField Ares Wind Field (1)_Framework

Next article: Realization of UE4_WindField Ares Wind Field (3)_Wind Injection

Topics: simulator Attribute