Groom hair can pop/twitch when simulating physics, if its tick is flushed early

My current understanding of this issue is that the current position of the mesh and its physics asset shapes are filled out at the end of the frame on the game thread, these are then transfered to the render thread and set on shaders for the GPU hair simulation.

Almost everything is up to date (I’ll come back to that in a moment) and has the correct transforms and positions for the meshes current pose and position in th world. The exception seems to be the deform / skinning related buffers that are used as part of the interpolation (DeformedTrianglePositionBuffer / MeshSampleWeightsBuffer / DeformedSamplePositionsBuffer), which I think are filled out as part of a rendering pass which would normally happen before the hair is simulated.

When flushing the gpu ticks (FNiagaraGpuComputeDispatch::ProcessPendingTicksFlush) those buffers would not have been updated yet (as I understand it) so the position of the hair and all of the other data that has been passed into the simulation don’t match. This means that any time the niagara gpu ticks are flushed, if a character with hair is moving, you may see their hair jump.

There does seem to be one other genuine bug when setting up the transforms for the simulation, in NiagaraDataInterfaceHairStrands.cpp, 933 - ComputeBoneTransform it directly gets the transform from RigidCurrentLocalToWorld which is basically the scene proxy position, and this will not have been updated yet when flushing the gpu tick. The related ComputeWorldTransform function uses GetCurrentLocalToWorld() which in this instance uses SkinningCurrentLocalToWorld which has been updated by this point, and this mismatch also seems to cause issues.

We would also like to be able to warm up some hair simulations, but this seems incompatible with requiring this rendering pass to generate the required buffers. Since if we spawn a new character in a pose that is very different from their rest pose and want to simulate the hair into the correct starting position, it may not be possible as it will not have been rendered yet.

I could of course be completely mistaken about the cause of the offset collision issue, but the fact is that it still happens.

[Attachment Removed]

Steps to Reproduce
Create a groom asset using hair strands which is skinned to a mesh, with a hair interpolation type of “Smooth Transform”

Set the hair to use the Groom Springs solver, and enable it to Solve Collisions and Project Collisions

Create a physics asset to match the shape of your mesh, that the hair can rest against

Set up an actor with this mesh and hair that can be moved around a level

Set up another niagara system that requires GPU ticks / simulates on the GPU and configure it with a large number of warmup ticks.

When this other system is activated that large number of queued ticks causes the GPU ticks to be flushed early, and the hair will be simulated earlier in the rendering pipeline that it usually would, causing the hair simulation to use some data from the previous frame (it would seem). If the hair actor is moving when this flush happens, the hair will collide with seemingly out of place collision and cause it to jump.

[Attachment Removed]

Hi Anthony, Thanks for reporting the issue. I am going to try reproducing the issue and see and could be done to fix it. Thanks, Michael

[Attachment Removed]

Hi Anthony, would you be able to try setting the fx.Niagara.Batcher.TickFlush.Mode to 0 (no flush) to see if it is kind of fixing your issue. The correct fix will be on our side to have an option per niagara system to opt out of the flushing mechanism if your niagara system depends on other rendering results. Will see what we can do on that side. Thanks, Michael

[Attachment Removed]

ok thanks for the feedback. Will check with the niagara team what we can do on that front to disable per system the flush.

Thanks

Michael

[Attachment Removed]

The best I’ve got so far is to update the physics proxy data when we run outside of the scene renderer, to copy the previous transforms back into the current transform (and then the mesh position / collision seem to match). Then on the next frame set the previous collision transforms back to what they where before we simulated outside of the scene renderer (so what it was two frames ago), so that the current / previous transforms remain consistent while I bodge them.

I’ve got this somewhat suspect code running in from FNDIHairStrandsProxy::PreStage. Apart from assuming that this always runs before the physics data interface’s PreStage, I’m pretty sure I’m not supposed to be modifyig other proxy’s data, so it’s more of a hack than a fix.

if (Context.GetComputeDispatchInterface().IsOutsideSceneRenderer())
{
				// Cache the previous state from when everything was valid
				for (uint32 TransformIndex = 0; TransformIndex < PHYSICS_ASSET_MAX_TRANSFORMS; ++TransformIndex)
				{
					ProxyData->HairStrandsBuffer->EarlySimPreviousTransform[TransformIndex] = PhysicsProxyData->AssetArrays.WorldTransform[TransformIndex + PHYSICS_ASSET_MAX_TRANSFORMS];
					ProxyData->HairStrandsBuffer->EarlySimPreviousInverse[TransformIndex] = PhysicsProxyData->AssetArrays.InverseTransform[TransformIndex + PHYSICS_ASSET_MAX_TRANSFORMS];
				}
 
				ProxyData->HairStrandsBuffer->SimulatedOutsideSceneRenderer = (int32)ESimulatedOutsideSceneRenderer::ThisFrame;
}
 
switch(ProxyData->HairStrandsBuffer->SimulatedOutsideSceneRenderer)
{
case ESimulatedOutsideSceneRenderer::ThisFrame:
{
				// Copy previous back to current, since the simulation cannot see the new mesh position
				for (uint32 TransformIndex = 0; TransformIndex < PHYSICS_ASSET_MAX_TRANSFORMS; ++TransformIndex)
				{
					PhysicsProxyData->AssetArrays.WorldTransform[TransformIndex] = ProxyData->HairStrandsBuffer->EarlySimPreviousTransform[TransformIndex];
					PhysicsProxyData->AssetArrays.InverseTransform[TransformIndex] = ProxyData->HairStrandsBuffer->EarlySimPreviousInverse[TransformIndex];
				}
}
break;
 
case ESimulatedOutsideSceneRenderer::LastFrame:
{
				// Set the previous transforms to what the simulation actually saw last frame (ie. the original previous transforms from before the jump)
				for (uint32 TransformIndex = 0; TransformIndex < PHYSICS_ASSET_MAX_TRANSFORMS; ++TransformIndex)
				{
					PhysicsProxyData->AssetArrays.WorldTransform[TransformIndex + PHYSICS_ASSET_MAX_TRANSFORMS] = ProxyData->HairStrandsBuffer->EarlySimPreviousTransform[TransformIndex];
					PhysicsProxyData->AssetArrays.InverseTransform[TransformIndex + PHYSICS_ASSET_MAX_TRANSFORMS] = ProxyData->HairStrandsBuffer->EarlySimPreviousInverse[TransformIndex];
				}
}
break;
}

[Attachment Removed]

Yes, disabling the flush does avoid the problem.

[Attachment Removed]