Developing a CityEngine like Visibility Analysis View.

Goal

Guys, I want to develop a function to display the view of Camera/SceneCaptureComponent, just like the function that CityEngine has, please refer to this image:


I have some thoughts but can not finish it yet, really need some suggestions.

Edit:
The Project is quite lightweight, you can have a try

My Thoughts

My idea is Setting a SceneCaptureComponent2D, and set the Capture Source to SceneDepth, then set the Texture Target to a RenderTarget2D. Then calculate the ViewProjection matrix of the SceneCaptureComponent2D and pass it to a postprocess volume material. In the postprocess material’s custom node, it iterates over every pixel of the current view, convert the world position of the current pixel into the clipspace of the SceneCaptureComponent2D, then we can judge if the pixel is in the viewfrustum of the SceneCaptureComponent2D, and if the depth is bigger than the same position in the Captured Depth Texture, paint it red, otherwise paint it green.

Detailed Steps

  1. Create a SceneCapture2D in the scene, and set the Capture source to SceneDepth, and Texture Target to a RenderTarget2D, which is set to RTFR16F format;
  2. Create a PostProcess Material(Later added it to the prost process volume through c++);
  3. Create a MaterialInstanceDynamic from the PostProcess Material, and add it to the PostProcess Volume’s Material array
void AVisibilityView::BeginPlay()
{
	Super::BeginPlay();
	SceneCaptureComp = SceneCaptureActor.Get()->GetComponentByClass<USceneCaptureComponent2D>();
	MatInstance = UMaterialInstanceDynamic::Create(PostProcessMaterial.Get(), this);
	if (PostProcessVol.IsValid())
	{
		PostProcessVol->Settings.AddBlendable(MatInstance, 1.0f);
	}
}
  1. In Tick, get the ViewProjectionMatrix of the SceneCapture, and pass it to the Postprocess Material through 4 Vectors
void AVisibilityView::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	
	if (SceneCaptureComp->IsValidLowLevel() && MatInstance->IsValidLowLevel())
	{
		FMatrix TempMatrix = UMyBlueprintFunctionLibrary::FindViewProjectionMatrix(SceneCaptureComp);
		MatInstance->SetVectorParameterValue(FName("MatrixRow0"), FVector4(TempMatrix.M[0][0], TempMatrix.M[0][1], TempMatrix.M[0][2], TempMatrix.M[0][3]));
		MatInstance->SetVectorParameterValue(FName("MatrixRow1"), FVector4(TempMatrix.M[1][0], TempMatrix.M[1][1], TempMatrix.M[1][2], TempMatrix.M[1][3]));
		MatInstance->SetVectorParameterValue(FName("MatrixRow2"), FVector4(TempMatrix.M[2][0], TempMatrix.M[2][1], TempMatrix.M[2][2], TempMatrix.M[2][3]));
		MatInstance->SetVectorParameterValue(FName("MatrixRow3"), FVector4(TempMatrix.M[3][0], TempMatrix.M[3][1], TempMatrix.M[3][2], TempMatrix.M[3][3]));
	}
}

The code of function “FindViewProjectionMatrix”

FMatrix UMyBlueprintFunctionLibrary::FindViewProjectionMatrix(const USceneCaptureComponent2D* sceneCaptureComponent2D)
{
    // View Matrix
    const FTransform& transform = sceneCaptureComponent2D->GetComponentToWorld();
    FMatrix viewMatrix = transform.ToInverseMatrixWithScale();
    viewMatrix = viewMatrix * FMatrix(FPlane(0, 0, 1, 0), FPlane(1, 0, 0, 0), FPlane(0, 1, 0, 0), FPlane(0, 0, 0, 1));

    // Projection Matrix
    const float FOV = sceneCaptureComponent2D->FOVAngle * (float)PI / 360.0f;
    FIntPoint captureSize(sceneCaptureComponent2D->TextureTarget->GetSurfaceWidth(), sceneCaptureComponent2D->TextureTarget->GetSurfaceHeight());
    float XAxisMultiplier;
    float YAxisMultiplier;
    if (captureSize.X > captureSize.Y)
    {
        XAxisMultiplier = 1.0f;
        YAxisMultiplier = captureSize.X / (float)captureSize.Y;
    }
    else
    {
        XAxisMultiplier = captureSize.Y / (float)captureSize.X;
        YAxisMultiplier = 1.0f;
    }
    FMatrix projectionMatrix = FReversedZPerspectiveMatrix(FOV, FOV, XAxisMultiplier, YAxisMultiplier, GNearClippingPlane, GNearClippingPlane);

    
    return viewMatrix * projectionMatrix;

}
  1. About the PostProcess material

The core is the Custom node here: The input “Tex” is fed with a Texture Object, which is fed with the RenderTarget2D; The input “WorldPosition” is fed with a WorldPosition node

Custom Node Input and Output Settings:

Custom Node Code:

float4 col0 = Col0;
float4 col1 = Col1;
float4 col2 = Col2;
float4 col3 = Col3;
float4x4 vpmatrix = float4x4(col0.r, col1.r, col2.r, col3.r,
                             col0.g, col1.g, col2.g, col3.g,
                             col0.b, col1.b, col2.b, col3.b,
                             col0.a, col1.a, col2.a, col3.a);
float3 worldPos = WorldPosition;
float4 clipspace = mul(worldPos, vpmatrix);
return (clipspace.x > -1 
    && clipspace.x < 1 
    && clipspace.y > -1 
    && clipspace.y < 1 
    && clipspace.z > 0 
    && clipspace.z < 1) ? 1.0 : 0.2 ;
// Cannot produce the expected result, so stopped here

Question
You can see the Custom node code is not finished, because I can’t visualize the clipspace of the SceneCaptureComponent correctly, is there some problem when passing the ViewProjection Matrix?

After some digging I made a little bit of progress, one big issue is that the PostProcess Material should be set to “Before Tone Mapping”, otherwise the world location will be distorted.
And I realised that the lack of basic understanding of Computer Graphic knowledge is the main obstacle for me to do this right.

Here’s the new structure of the material:

Here’s the new hlsl code of the custom node:

float4x4 vpmatrix = float4x4(Row0.r, Row1.r, Row2.r, Row3.r,
                            Row0.g, Row1.g, Row2.g, Row3.g,
                            Row0.b, Row1.b, Row2.b, Row3.b,
                            Row0.a, Row1.a, Row2.a, Row3.a);

float4 clipspace = mul(WorldPosition, vpmatrix);
float3 ndc = (clipspace / clipspace.w).xyz;

// Expected to return a region that is in the SceneCaptureComponent2D's View, but failed
return (ndc.x>-1 && ndc.x < 1 &&
       ndc.y > -1 && ndc.y < 1 &&
       ndc.z > -1 && ndc.z < 1);

// float2 uv = (ndc.xy + 1)/2;
// float sample = Texture2DSample(DepthTex,DepthTexSampler,uv);
// return sample;

I’m expecting to see a bright region that is in the SceneCaptureComponent2D’s view, other part of the world should be black, however I only got two wired black lines:

I uploaded the project on github, here’s the link:

Why not use the built in functions to get projections?


FMatrix AVisibilityRenderer::FindViewProjectionMatrix(USceneCaptureComponent2D* sceneCaptureComponent2D)
{         
 TOptional<FMatrix> CustomProjectionMatrix;
 if (sceneCaptureComponent2D->bUseCustomProjectionMatrix)
 {
     CustomProjectionMatrix =
         sceneCaptureComponent2D->CustomProjectionMatrix;
 }

 FMatrix OutViewMatrix;
 FMatrix ViewProjectionMatrix;
 FMatrix ProjectionMatrix;

 FMinimalViewInfo ViewInfo;
 sceneCaptureComponent2D->GetCameraView(0.0f, ViewInfo);
 
 APlayerCameraManager* PCM = UGameplayStatics::GetPlayerCameraManager(GetWorld(), 0);
 FViewport* Viewport = GetWorld()->GetGameViewport()->Viewport;

 FMinimalViewInfo CamInfo;
 CamInfo.Location = PCM->GetCameraLocation();
 CamInfo.Rotation = PCM->GetCameraRotation();
 CamInfo.FOV = PCM->GetFOVAngle();    
 FMatrix camMatrix = CamInfo.CalculateProjectionMatrix();

 FSceneViewProjectionData PData;
 ViewInfo.CalculateProjectionMatrixGivenView(CamInfo, EAspectRatioAxisConstraint::AspectRatio_MajorAxisFOV, Viewport, PData);
 ProjectionMatrix = PData.ComputeViewProjectionMatrix();
  

 FMatrix OutProjectionMatrix;

 UGameplayStatics::CalculateViewProjectionMatricesFromMinimalView(ViewInfo, CustomProjectionMatrix, OutViewMatrix, OutProjectionMatrix, ViewProjectionMatrix);

 return OutProjectionMatrix;
}

I used the built in projection functions in unreal but the ViewProjectionMatrix seems skewed in a similar manor to your pic. ProjectionMatrix on the other hand returns something like this:

unfortunately the render target doesn’t seem to be updating. It’s always just full on red. Shouldn’t it be like a red version of the G-Buffer Depth pass?

Thank you for the code , I found your code might get the ViewProjectionMatrix of the current player’s camera, not the SceneCaptureComponent2D’s.
Actually I did not know there’re some built-in helper functions to get ViewProjection Matrix,Thanks a lot for the information!

Here’s the code I modified based on your version:

FMatrix AVisibilityRenderer::FindViewProjectionMatrix(USceneCaptureComponent2D* sceneCaptureComponent2D)
{
    TOptional<FMatrix> CustomProjectionMatrix;

    if (sceneCaptureComponent2D->bUseCustomProjectionMatrix)
    {
        CustomProjectionMatrix = sceneCaptureComponent2D->CustomProjectionMatrix;
    }

    FMatrix OutViewMatrix;
    FMatrix ViewProjectionMatrix;
    FMatrix ProjectionMatrix;

    FMinimalViewInfo ViewInfo;
    sceneCaptureComponent2D->GetCameraView(0.0f, ViewInfo);


    UGameplayStatics::GetViewProjectionMatrix(ViewInfo, OutViewMatrix, ProjectionMatrix, ViewProjectionMatrix);

    return ViewProjectionMatrix;
}

Actually it produces exactly the same result as my original function(I added some colors this time, the lines’ position is a little bit different because I moved the capture actor):

I think maybe it’s a hint that the ViewProjection Matrix is correct, probably the problem lies in the material…

As to the render target, it’s red because all the depth values are huge, you can divide it by a big number like 6000, then you’ll see some objects.

Found a big bug!


As shown in the image, a scalar of 1 should be appended to WorldPosition, Otherwise the w component of clipspace would always be 0, everything is broken after that.
By Appending this 1 component, I can finally see some bounaies and uvs!


The uv looks great!

Sampling some random texture with the uv works well!

HOWEVER, sampling the RenderTarget ( filled with depth texture from sceneCapture) with this uv always results in black… … WTF? It’s supposed to be quite bright!

The same RenderTarget works as expected in another material:

The updated hlsl code:

float4x4 vpmatrix = float4x4(Row0.r, Row1.r, Row2.r, Row3.r,
                             Row0.g, Row1.g, Row2.g, Row3.g,
                             Row0.b, Row1.b, Row2.b, Row3.b,
                             Row0.a, Row1.a, Row2.a, Row3.a);

float4 clipspace = mul( vpmatrix,WorldPosition);
float3 ndc = (clipspace / clipspace.w).xyz;

//convert uv from [-1,1] to [0,1]
float2 uv = (ndc.xy + 1)/2;

// Comment out the next line to see the uv
//return float3(uv * bounds,0);

// flip uv along y axis
float2 flippeduv = float2(uv.x,-uv.y);

// This is the bounds of SceneCapture's frustum
float bounds = (ndc.x>-1 && ndc.x < 1 &&
        ndc.y > -1 && ndc.y < 1 &&
        ndc.z > 0 && ndc.z < 1) ? 1 : 0;

float sample = saturate(Texture2DSample(Tex,TexSampler,flippeduv));
return sample * bounds;
// Always result in black

I’ve located the problem, it seems that when running, the RenderTarget turns black, even if I set the SceneCaptureComponent to “Capture Every Frame” , and even more , Calling CaptureScene() every frame, does not work. Any ideas?