Very Large Vehicles

Hello!

We are investigating viable ways to construct a very large vehicle, in an already very large level with high physics complexity. The default Mover with the minimal number of meshes for the vehicle is already hitting > 2ms on the game thread, mostly updating the child transforms and overlaps. We want to allow the player to build inside, drop physics objects, and dock smaller vehicles to it. So we need a stable physics sim inside and we can’t just turn off collisions and overlaps on everything. It also needs to work in multiplayer, and we have replication graph to do custom relevancy behavior if needed.

Its possible we can get that 2ms baseline down, but it seems like we will always be fighting for performance with the ‘naive’ approach.

The most promising solution so far that I’d love some feedback on, is to have two actors: an exterior actor and the interior:

  • The exterior is a minimal set of meshes and colliders attached to the movement root.
  • The interior is as complicated as it needs to be, but does not actually move.

This interior actor sits far away from the game world, and we manually call FScene::UpdatePrimitiveTransform_RenderThread in TG_LastDemotable on all its primitive components, to offset the primitive proxies on the render thread to look like they are attached to the exterior actor in the world.

  • Any player that enters the vehicle teleports to the interior, then has their primitives and view transform offset back to the real world, but not their interaction traces.
  • Any gameplay actors added to the interior actor will draw as if they were attached to the exterior. By using the primitive proxy transforms, we get correct culling, shadows, and mesh distance fields. It seems to ‘just work ™’…
  • It also gives us a nice physics sim because none of the interior actors are actually moving.
  • Replication relevancy can be handled by a special replication graph node that knows about the offset.

However, there will also be many knock on effects for gameplay systems relying on player and actor world transforms, as well as audio listener locations. But these are solvable, and we consider fixing these sorts of issues to be potentially easier than trying to optimize the naive approach.

The scarier side of this is the rendering; as I said it looks like it works, but ISM need more work and Niagara may not work so easily. Basically, we don’t know what other rendering issues might come up?

So I’m also curious about approaches that may be less drastic.

// Offset a list of primtive components to appear relative to an arbitraty transform
for (UPrimitiveComponent* Primitive : OffsetComponents)
{
    FPrimitiveData PrimitiveData;
    PrimitiveData.PrimitiveComponentId = Primitive->GetPrimitiveSceneId();
    // Calculate world transforms and update PrimtiveData

    PrimitiveDataList.Add(PrimitiveData);
    
    // Just flags the transform as not dirty, so that the normal end of frame logic doesn't override our manual transform
    Primitive->FlushRenderTransformDirty();
}

if (GetWorld()->Scene)
{
    FScene* Scene = static_cast<FScene*>(GetWorld()->Scene);

    ENQUEUE_RENDER_COMMAND(UpdateOffsetRenderingStatesRendering)(
       [Scene, bMovedThisFrame, PrimitiveDataListRT = MoveTemp(PrimitiveDataList)](FRHICommandListImmediate& RHICmdList)
       {
          for (const auto& PrimitiveData : PrimitiveDataListRT)
          {
             FPrimitiveSceneInfo* PrimitiveSceneInfo = Scene->GetPrimitiveSceneInfo(PrimitiveData.PrimitiveComponentId);
             if (!PrimitiveSceneInfo || !PrimitiveSceneInfo->Proxy)
             {
                continue;
             }

             Scene->UpdatePrimitiveTransform_RenderThread(
                PrimitiveSceneInfo->Proxy,
                PrimitiveData.WorldBounds,
                PrimitiveData.LocalBounds,
                PrimitiveData.RenderMatrix,
                PrimitiveData.ActorLocation,
                PrimitiveData.OldRenderTransform);
             
             Scene->UpdatePrimitiveVelocityState_RenderThread(PrimitiveSceneInfo, bMovedThisFrame);
          }
       });
}




[Attachment Removed]

Hi there! Sorry for the delayed response, I’ve reached out internally in a couple places for guidance here because it touches networking/physics/rendering. The initial thing that came to mind for me is our vehicles in Lego FN which use ChaosModularVehicle, which is also multiplayer and dynamic, but may be overkill for what you need and not solve your interior physics issues. Is your large vehicle made up of parts that might be disconnected/attached - like engines/wheels that can be added/removed (destructible parts)

The default Mover with the minimal number of meshes for the vehicle is already hitting > 2ms on the game thread, mostly updating the child transforms and overlaps.

Can we get an insights trace of this and a log or info about the hardware it’s run on to get an idea of the details where that 2ms is going? Alternately, if this large vehicle is already available in the build we have access to, I might be able to spawn it and take some Insight captures.

The scarier side of this is the rendering; as I said it looks like it works, but ISM need more work and Niagara may not work so easily.

You might run into velocity issues when teleporting objects from exterior to interior and back, but that should be solvable, and if you’re using GPUScene ISMs (default) then you need to make sure the instance buffers get updated. I’ve reached out to colleagues more familiar with Niagara about what can be done in this situation.

[Attachment Removed]

Apologies but It looks like EPS failed to attach the trace to your reply. This is a known issue that hasn’t been fixed yet and the only known workaround is to make a new reply, attach the trace again and include some image in the post. Doesn’t matter what the image is, it just does something in the background logic or timing.

I’ve heard back from my colleagues on Physics that duplicating the scenes is a reasonable approach if the interior is complex and you need good interactions - it’s been used before with success, but I’m waiting to hear back about the details. If it’s OK with you, it might be more efficient to schedule a call to talk about this - I’m going to get that request started, but the call will likely be after the Epic Games summer holiday break (July 13)

I haven’t heard back from some key Niagara engineers yet. If you already have run into issues with Niagara particles in your prototype we can try to address those. I will see if we can include a Niagara engineer on the call if that would be helpful.

[Attachment Removed]

Thanks for providing the image! I’ve passed that along and started the process for getting that meeting scheduled after the break.

[Attachment Removed]

Yes, apologies for the delay, we meant to have the meeting by now. We discussed getting it scheduled with Jack today so I anticipate it will happen soon.

[Attachment Removed]

Quick update - the meeting is scheduled. It would be helpful for us to get the trace file to look at before the meeting if that’s not too much trouble. The site support team recommended clearing your cache before retrying the upload, but if that doesn’t work, or if you’d rather not try this method again, we do have a secure Box folder set up for sharing files with us that Jack can upload the trace to.

Update #2: We’ve received the files from Jack, thanks!

[Attachment Removed]

Post-meeting update:

For the specific vehicle use-case discussed, your approach which keeps that vehicle at a fixed position outside the world is likely the most performant from a physics perspective and the rendering approach you have above should work except there are a couple things to check on regarding velocity.

1) Verify the UpdatePrimitiveVelocityState_RenderThread call is actually doing anything. The UpdatePrimitiveTransform_RenderThread call enqueues the new current and previous transform but that is processed in RenderScene.cpp FScene::Update

FScene::Update()
{
...
    {
        CSV_SCOPED_TIMING_STAT_EXCLUSIVE(UpdatePrimitiveTransform);
        SCOPE_CYCLE_COUNTER(STAT_UpdatePrimitiveTransformRenderThreadTime);
        for (const auto& Item : UpdatedTransforms)
        {
...
            if (ShouldPrimitiveOutputVelocity(PrimitiveSceneInfo->Proxy, GetShaderPlatform()))
            {
                PrimitiveSceneInfo->bRegisteredWithVelocityData = true;
                VelocityData.UpdateTransform(PrimitiveSceneInfo, 
                    Item.Payload.LocalToWorld,                         // transform you updated
                    PrimitiveSceneProxy->GetLocalToWorld()); // proxy's current location
            }
...
        }
        for (const auto& Transform : OverridenPreviousTransforms)
        {
            FPrimitiveSceneInfo* PrimitiveSceneInfo = Transform.SceneInfo;
            VelocityData.OverridePreviousTransform(PrimitiveSceneInfo->PrimitiveComponentId, Transform.Payload.Value); // prev transform you provided
        }

2) You can occasionally get single frame blurring when teleporting or spawning and attaching an object if the very first time, usually when another transform update gets added after yours ([related [Content removed] I recommend adding some debug code after the VelocityData updates in FScene::Update to check for really big transform differences and also turn on the Motion Blur visualization (ShowFlag.VisualizeMotionBlur 1) and verify the large vehicle is still yellow when moving . If you see streaks or ghosting that can also be because TSR isn’t getting accurate motion vectors based on velocity.

ISM need more work

Can you describe in more detail what you’re seeing with ISMs? Is it that the rotation/scale isn’t working or something else?

[Attachment Removed]

Apologies for the delay, I haven’t had a chance yet to look into what might be preventing the ISMs from rotating correctly - are the instances being added with local (default) or worldspace? It’s possible the FInstanceUpdateComponentDesc.PrimitiveLocalToWorld needs to be updated, like in UInstancedStaticMeshComponent::BuildComponentInstanceData. You could try r.GPUScene.UploadEveryFrame=1 and see if the issue is that the GPU scene isn’t getting updated with the right data, but I doubt that’s what is going on.

[Attachment Removed]

Our requirements I would say are closer to Sea of Thieves for instance, but it’s a submarine so its an enclosed space with discrete entrances. The player doesn’t modify the exterior but basically can add arbitrary actors attached to the interior.

The attached trace is the best I was able to get, but our Mover implementation has changed so now the movement sweeps are taking more time than the update. So in this trace, the base mover cost is very high but I am assuming we can fix the number of traces (for instance by using the physics backend), while the update child transforms and components is not as fixable. We also have overlap events disabled at the project level, so the meshes in this trace I am assuming need them for gameplay reasons.

I also want to explore other options than fully offsetting the rendering of an actor. For instance, I was able to create a new Chaos scene and manually add rigid bodies to it. I am wondering if there might be a way to get components to register their physics bodies in this scene rather than the world scene. Then we could do the same trick with a stationary reference frame in the secondary chaos scene.

[Attachment Removed]

Ill add a screenshot of the trace which highlights the general problem.

[Image Removed]A call would be great, I would love to hear more about how to implement the mirrored physics scene.

Niagara effects are indeed causing the most issues so far, but it’s not clear what the issue is yet. Without any special handling, they don’t render at the render proxies location or the original location. However since these are cheaper to move and there are fewer of them, it might be viable for use to actually set the component location.

[Attachment Removed]

Sorry I tried to attach the trace along with that image, but it didn’t upload again. I’ll try one more time here (same image). The trace is fairly large, ~170Mb after zip, maybe that’s the problem.

  • edit: looks like it did not work again, sorry!

[Image Removed]

[Attachment Removed]

Hi Alex, I just wanted to follow up since I think you are all back now?

[Attachment Removed]

Thank you for your and the others time, it was great to get some sanity checking on this approach.

ShowFlag.VisualizeMotionBlur 1 seems to look right based on my understanding. The vehicle geometry that is not actually moving turns yellow when proxy space transform changes. Also the background parallax changes caused by the space moving when the player is inside but not moving themselves, also show velocity lines in the opposite direction.

ISMs seem like the root position of the component itself is getting moved, but the rotation is not changed. So all the meshes have the correct relative positions in the world but they don’t rotate with the space. We don’t need to worry about scale. I haven’t looked into how the ISM render proxy works but it seems to make sense that there would be more data per instance for these.

[Attachment Removed]