Okay, found an even better solution. Not sure why I couldn’t find this function before but inside SkinnedMeshComponent there is a function called GetSkinnedVertexPosition() which gets exactly what we need to compute the UVs.
{
USkeletalMeshComponent* Mesh = Cast<USkeletalMeshComponent>(Hit.Component);
FSkeletalMeshRenderData* RenderData = Mesh->GetSkeletalMeshRenderData();
FSkeletalMeshLODRenderData& LODData = RenderData->LODRenderData[0];
FRawStaticIndexBuffer16or32Interface* Indices = RenderData->LODRenderData[0].MultiSizeIndexContainer.GetIndexBuffer();
if(Hit.FaceIndex * 3 + 2 > Indices->Num() || Hit.FaceIndex * 3 < 0) return false;
const int32 Index0 = Indices->Get(Hit.FaceIndex * 3 + 0);
const int32 Index1 = Indices->Get(Hit.FaceIndex * 3 + 1);
const int32 Index2 = Indices->Get(Hit.FaceIndex * 3 + 2);
const FVector Pos0 = Mesh->GetSkinnedVertexPosition(Mesh, Index0, LODData, RenderData->LODRenderData[0].SkinWeightVertexBuffer);
const FVector Pos1 = Mesh->GetSkinnedVertexPosition(Mesh, Index1, LODData, RenderData->LODRenderData[0].SkinWeightVertexBuffer);
const FVector Pos2 = Mesh->GetSkinnedVertexPosition(Mesh, Index2, LODData, RenderData->LODRenderData[0].SkinWeightVertexBuffer);
const FVector2D UV0 = Mesh->GetVertexUV(Index0, 0);
const FVector2D UV1 = Mesh->GetVertexUV(Index1, 0);
const FVector2D UV2 = Mesh->GetVertexUV(Index2, 0);
const FVector LocalHitPos = Mesh->GetComponentToWorld().InverseTransformPosition(Hit.Location);
const FVector BaryCoords = FMath::ComputeBaryCentric2D(LocalHitPos, Pos0, Pos1, Pos2);
UV = (BaryCoords.X * UV0) + (BaryCoords.Y * UV1) + (BaryCoords.Z * UV2);
return true;
}
This is much more performant than using my previous CPU skinning example. Again, you will need PerPolyCollision enabled on the SK mesh, but you will get more accurate calculations like this compared to testing against collision capsules which is important if you are using these UVs for things like decals/mesh paint etc.