# [Chaos Vehicle] Experiences with the new vehicle implementation [+ SkidMark implementation]

Howto: Simple skid marks
I’ve been asked what my general approach for the skid marks is. In this post I want to share a few details of my quite simple (according to the correctness of the graphical result) implementation.

Disclaimer: There are many still-pending features and bugs within this implementation. The following snippets should give you just a brief overview of the general approach! If you got any questions or uncertainties feel free to ask

1. First you somehow need to access the wheel data which then can be used to compute all different things e.g. the skid marks. The principle to access this data hasn’t been changed from the old PhysX based vehicle system. You need to create a custom implementation of AWheeledVehiclePawn. By doing so you can override the tick function to collect the relevant wheel data. Here is my current implementation…
``````Header:
[SPOILER]
``````
``````
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Delegates/Delegate.h"
#include "Delegates/DelegateCombinations.h"
#include "PhysicalMaterials/PhysicalMaterial.h"
#include "PhysXIncludes.h"
#include "ExtendedChaosWheeledVehicleMovementComponent.h"
#include "WheeledVehiclePawn.h"
#include "ProceduralMeshComponent.h"
#include "ExtendedChaosVehicle.generated.h"

/**
*
*/
USTRUCT(Blueprintable)
struct FExtendedWheelInfo
{
GENERATED_BODY()

/** Get the wheel RPM [revolutions per minute] */
float Rpm;

/** Get the magnitude of the force pressing the wheel into the terrain [N.m] */

/** Get the slip angle for this wheel - angle between wheel forward axis and velocity vector [degrees] */
float SlipAngle;

/** Difference between the ground speed of the wheel and the road speed [km/h] */
float SlipVelocity;

/** Get the linear ground speed of the wheel based on its current rotational speed [km/h] */
float GroundSpeed;

/** Get the road speed at the wheel [km/h] */
float Speed;

/** Get the angular velocity of the wheel [degrees/sec] */
float AngularVelocity;

float NormalizedLateralSlip;

/** Get the current longitudinal slip value [0 no slip - using static friction, 1 full slip - using dynamic friction] */
float NormalizedLongitudinalSlip;

bool InAir;

FString ContactSurfaceName;

//TWeakObjectPtr<UPhysicalMaterial> ContactSurface;

int ContactSurfaceType;

FVector ContactPoint;

FVector ContactNormal;

bool Slipping;

FExtendedWheelInfo() :
Rpm(0.0f),
SlipAngle(0.0f),
SlipVelocity(0.0f),
GroundSpeed(0.0f),
Speed(0.0f),
NormalizedLateralSlip(0.0f),
NormalizedLongitudinalSlip(0.0f),
InAir(false),
//ContactSurface(nullptr),
ContactSurfaceType(-1),
Slipping(false)
{}
};

/**
*
*/
USTRUCT(Blueprintable)
struct FExtendedVehicleInfo
{
GENERATED_BODY()

int32 Speed;

float RPM;

int32 Gear;

bool Slipping;

bool Skidding;

TArray<FExtendedWheelInfo> WheelInfo;

FExtendedVehicleInfo() :
Speed(0),
RPM(0.0f),
Gear(0),
Slipping(false),
Skidding(false)
{}
};

/**
*
*/
USTRUCT(Blueprintable)
struct FTyreSqueal
{
GENERATED_BODY()

/** Threshold for the slip velocity [km/h] */
float SlipVelocityThreshold;

/** */
float SlipAngleThreshold;

FTyreSqueal() :
SlipVelocityThreshold(5.0f),
SlipAngleThreshold(40.0f)
{}
};

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FStartSlipping);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FStopSlipping);

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FWheelEventWithIndex, uint8, WheelIndex);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FWheelEventWithIndexAndWheelInfo, uint8, WheelIndex, FExtendedWheelInfo, WheelInfo);

/**
*
*/
UCLASS(Blueprintable)
class AExtendedChaosVehicle : public AWheeledVehiclePawn
{
GENERATED_BODY()

UPROPERTY(BlueprintAssignable, BlueprintCallable, Category = "ExtendedChaosVehicle")
FStartSlipping OnStartSlipping;

UPROPERTY(BlueprintAssignable, BlueprintCallable, Category = "ExtendedChaosVehicle")
FStopSlipping OnStopSlipping;

UPROPERTY(BlueprintAssignable, BlueprintCallable, Category = "ExtendedChaosVehicle")
FWheelEventWithIndexAndWheelInfo OnWheelStartSlipping;

UPROPERTY(BlueprintAssignable, BlueprintCallable, Category = "ExtendedChaosVehicle")
FWheelEventWithIndexAndWheelInfo OnWheelSlipping;

UPROPERTY(BlueprintAssignable, BlueprintCallable, Category = "ExtendedChaosVehicle")
FWheelEventWithIndexAndWheelInfo OnWheelStopSlipping;

private:
FExtendedVehicleInfo LatestVehicleInfo;
bool IsSlipping;

public:

UMaterialInterface* Material;

FTyreSqueal TyreSqueal;

AExtendedChaosVehicle(const FObjectInitializer& ObjectInitializer) :
IsSlipping(false),
Super(ObjectInitializer.SetDefaultSubobjectClass<UExtendedChaosWheeledVehicleMovementComponent>(VehicleMovementComponentName))
{
}

void Tick(float deltaSeconds);

UFUNCTION(BlueprintCallable, BlueprintPure, Category = "ExtendedChaosVehicle")
inline FExtendedVehicleInfo GetVehicleInfo() { return LatestVehicleInfo; }

private:
FExtendedVehicleInfo DetermineVehicleInfo();
};

``````
``````[/SPOILER]

Source:
[SPOILER]
``````
``````

// Fill out your copyright notice in the Description page of Project Settings.

#include "ExtendedChaosVehicle.h"

#define CHECK_BIT(var,pos) (((var)>>(pos)) & 1)

void AExtendedChaosVehicle::Tick(float deltaSeconds)
{
// Local variables
const auto movementComp = this->GetVehicleMovementComponent();

Super::Tick(deltaSeconds);

// Get next info
auto info = DetermineVehicleInfo();

// Check all wheels
bool skidding = false;
bool slipping = false;
for (auto idx = 0; idx < info.WheelInfo.Num(); ++idx)
{
// Get last wheel info
if (idx >= LatestVehicleInfo.WheelInfo.Num()) continue;
auto& lastWheel = LatestVehicleInfo.WheelInfo[idx];

// Get next wheel info
auto& wheel = info.WheelInfo[idx];

// Fire wheel event: start/stop slipping
if (!lastWheel.Slipping && wheel.Slipping)
{
// Reset slipping duration
//wheel.SlippingDuration = 0.0f;

// Fire callback
}
else if (lastWheel.Slipping && !wheel.Slipping) OnWheelStopSlipping.Broadcast(idx, wheel);
else if (wheel.Slipping)
{
// Update slipping duration
//wheel.SlippingDuration = lastWheel.SlippingDuration + deltaSeconds;

// Fire callback
}

// Accumulate skidding & slipping
slipping = slipping || wheel.Slipping;
}
info.Slipping = slipping;

// Fire events
if (!IsSlipping && slipping)
{
IsSlipping = true;
}
else if (IsSlipping && !slipping)
{
IsSlipping = false;
}

// Update latest info
LatestVehicleInfo = info;
}

FExtendedVehicleInfo AExtendedChaosVehicle::DetermineVehicleInfo()
{
// Local variables
FExtendedVehicleInfo info;
const auto movementComp = (UExtendedChaosWheeledVehicleMovementComponent*)this->GetVehicleMovementComponent();

// Process wheel data
for (int32_t wIdx = 0; wIdx < movementComp->PhysicsVehicle()->Wheels.Num(); ++wIdx)
{
// Get reference
auto& wheel = movementComp->PhysicsVehicle()->Wheels[wIdx];
const auto& chaosWheel = movementComp->GetWheel(wIdx);

// Extract material's name
TWeakObjectPtr<UPhysicalMaterial> contactSurfaceMaterial = nullptr;
FString contactSurfaceString = "";
int contactSurfaceType = -1;

if (chaosWheel->GetContactSurfaceMaterial() && chaosWheel->GetContactSurfaceMaterial()->IsValidLowLevel())
{
contactSurfaceMaterial = chaosWheel->GetContactSurfaceMaterial();
contactSurfaceString = contactSurfaceMaterial != nullptr ? contactSurfaceMaterial->GetName() : FString(TEXT("NONE"));
contactSurfaceType = (int)contactSurfaceMaterial->SurfaceType;
}

FExtendedWheelInfo wheelInfo;
wheelInfo.InAir = !wheel.bInContact;
wheelInfo.Rpm = wheel.GetWheelRPM();
wheelInfo.SlipVelocity = (wheel.GetWheelGroundSpeed() - wheel.GetRoadSpeed()) * 0.036f;
wheelInfo.AngularVelocity = wheel.GetAngularVelocity() * 57.3;
wheelInfo.GroundSpeed = wheel.GetWheelGroundSpeed() * 0.036f;
wheelInfo.NormalizedLongitudinalSlip = wheel.GetNormalizedLongitudinalSlip();
wheelInfo.NormalizedLateralSlip = FMath::Clamp(RadToDeg(wheel.GetSlipAngle()) / 30.0f, -1.f, 1.f);//wheel.GetNormalizedLateralSlip();
wheelInfo.ContactSurfaceName = contactSurfaceString;
wheelInfo.ContactSurfaceType = contactSurfaceType;
wheelInfo.ContactPoint = chaosWheel->HitResult.ImpactPoint;
wheelInfo.ContactNormal = chaosWheel->HitResult.ImpactNormal;

// Determine slipping & skidding
float slipAngle = abs(wheelInfo.SlipAngle);
wheelInfo.Slipping =
slipAngle > TyreSqueal.SlipAngleThreshold && slipAngle < (180.0f - TyreSqueal.SlipAngleThreshold) ||
abs(wheelInfo.SlipVelocity) > TyreSqueal.SlipVelocityThreshold;
wheelInfo.Slipping &= !wheelInfo.InAir;

}

return info;
}

``````
``````[/SPOILER]
``````
1. The next step is to ensure that you actually use this implementation as base class of your vehicle.
You can then set-up the events for slip-state-notifications: Event connection posted by anonymous | blueprintUE | PasteBin For Unreal Engine
2. The start slip event is currently ignored (empty method). The events stop-slip & slipping are controlling the skid mark rendering. See the blueprints:
Slipping: https://blueprintue.com/blueprint/p370pm9f/
Stop-Slip: Event: wheel stop slipping posted by LeBoozer | blueprintUE | PasteBin For Unreal Engine
3. My approach to render the skid mark meshes uses a *UProceduralMeshComponent *per tyre to emulate a ribbon of skid marks on the ground. These components are generated in a custom implementation of UChaosWheeledVehicleMovementComponent (the custom implementation is already linked the first step). My component creates a skid mark mesh per tyre during the start up. It also offers to method to control the skid mark mesh. The first function *AddSkidMarkSupportPoint *adds a new support point with a few attributes to the ribbon / mesh. The second method *IncreaseSkidMarkSection *increases the section counter (a section represents a continuous sequence of triangles) to add interruptions to the ribbon / mesh.
See the implementation of custom movement component as well as of the skid mark mesh…
``````Movement component (header):
[SPOILER]
``````
``````

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Delegates/Delegate.h"
#include "Delegates/DelegateCombinations.h"
#include "PhysicalMaterials/PhysicalMaterial.h"
#include "PhysXIncludes.h"
#include "ChaosWheeledVehicleMovementComponent.h"
#include "SkidMarkMesh.h"
#include "ExtendedChaosWheeledVehicleMovementComponent.generated.h"

/**
*
*/
UCLASS(Blueprintable)
class UExtendedChaosWheeledVehicleMovementComponent : public UChaosWheeledVehicleMovementComponent
{
GENERATED_BODY()

protected:
TArray<TSharedPtr<SkidMarkMesh>> SkidMarkMeshes;

public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category= "Extended - SkidMarks")
float SkidMarkMeshWidth;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category= "Extended - SkidMarks")
float SkidMarkMeshGroundOffset;

UExtendedChaosWheeledVehicleMovementComponent() :
SkidMarkMeshWidth(20.0f),
SkidMarkMeshGroundOffset(0.02f)
{}

virtual void BeginPlay() override;
//void CreateVehicle(TArray<FWheelSetup> previousTires);

UFUNCTION(BlueprintCallable, Category = "ExtendedWheeledVehicleMovementComponent4W")
void AddSkidMarkSupportPoint(int wheelIdx, FVector position, FVector normal, float intensity, float uvDivider, float minSqrDistanceToPrevious, UMaterialInterface* material);

UFUNCTION(BlueprintCallable, Category = "ExtendedWheeledVehicleMovementComponent4W")
void IncreaseSkidMarkSection(int wheelIdx);

UChaosVehicleWheel* GetWheel(int wheelIdx);

protected:
void DropSkidMarkMeshes();
void CreateSkidMarkMeshes();
};

``````
``````[/SPOILER]

Movement component (source):
[SPOILER]
``````
``````

#include "ExtendedChaosWheeledVehicleMovementComponent.h"
#include "SkidMarkMeshManager.h"

void UExtendedChaosWheeledVehicleMovementComponent::BeginPlay()
{
// Call parent
UChaosWheeledVehicleMovementComponent::BeginPlay();

// Create skid mark meshes
CreateSkidMarkMeshes();
}

/*void UExtendedChaosWheeledVehicleMovementComponent::CreateVehicle(TArray<ChaosWheelS> previousTires)
{
// Call parent
UChaosWheeledVehicleMovementComponent::CreateVehicle();

// Re-create skid mark meshes
if (previousTires.Num() != WheelSetups.Num())
{
DropSkidMarkMeshes();
CreateSkidMarkMeshes();
}
}*/

void UExtendedChaosWheeledVehicleMovementComponent::AddSkidMarkSupportPoint(int wheelIdx, FVector position, FVector normal, float intensity,
float uvDivider, float minSqrDistanceToPrevious, UMaterialInterface* material)
{
// Check wheel index
if (wheelIdx >= SkidMarkMeshes.Num())
{
UE_LOG(LogTemp, Warning, TEXT("Cannot add support point to skid mark mesh. Invalid wheel index '%d' or skid mark mesh (Wheel count: %d)"), wheelIdx, SkidMarkMeshes.Num());
return;
}

if(uvDivider == 0.0f) uvDivider = 1.0f;
SkidMarkMeshes[wheelIdx]->AddSupportPoint(position, normal, intensity, SkidMarkMeshWidth, SkidMarkMeshGroundOffset, uvDivider, minSqrDistanceToPrevious, material);
}

void UExtendedChaosWheeledVehicleMovementComponent::IncreaseSkidMarkSection(int wheelIdx)
{
// Check wheel index
if (wheelIdx >= SkidMarkMeshes.Num())
{
UE_LOG(LogTemp, Warning, TEXT("Cannot add support point to skid mark mesh. Invalid wheel index: '%d' or skid mark mesh (Wheel count: %d)"), wheelIdx, SkidMarkMeshes.Num());
return;
}

// Increase skid mark section
SkidMarkMeshes[wheelIdx]->IncreaseSection();
}

UChaosVehicleWheel* UExtendedChaosWheeledVehicleMovementComponent::GetWheel(int wheelIdx)
{
// Check parameter
check(wheelIdx <= this->Wheels.Num());
return this->Wheels[wheelIdx];
}

void UExtendedChaosWheeledVehicleMovementComponent::DropSkidMarkMeshes()
{
// Local variables

// Find skid mark manager
if (!manager)
{
UE_LOG(LogTemp, Error, TEXT("Cannot destroy skid mark meshes. Manager object not found in level!"));
return;
}

// Release all components
for (auto& comp : SkidMarkMeshes)
manager->DestroyMesh(comp);
SkidMarkMeshes.Empty();
}

void UExtendedChaosWheeledVehicleMovementComponent::CreateSkidMarkMeshes()
{
// Local variables

// Find skid mark manager
if (!manager)
{
UE_LOG(LogTemp, Error, TEXT("Cannot create skid mark meshes. Manager object not found in level!"));
return;
}

// Create list for components
SkidMarkMeshes.Reserve(WheelSetups.Num());
for (int i = 0; i < WheelSetups.Num(); ++i)
{
}
}

``````

[/SPOILER]

``````Skid mark mesh (header):
[SPOILER]
``````
``````

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "PhysicalMaterials/PhysicalMaterial.h"
#include "PhysXIncludes.h"
#include "ProceduralMeshComponent.h"

/**
*
*/
class SkidMarkMesh
{
private:
UProceduralMeshComponent* ProceduralMeshComponent;

uint32 SkidMarkMeshId;
int32 CurrentSection;
TArray<FVector> SupportPoints;
TArray<FVector> Vertices;
TArray<int32> Triangles;
TArray<FVector> Normals;
TArray<FVector2D> UV0;
TArray<FLinearColor> VertexColors;
TArray<FProcMeshTangent> Tangents;

public:
SkidMarkMesh(uint32 id, AActor* parent, const FString& name);

inline uint32 GetSkidMarkMeshId() const { return SkidMarkMeshId; }

void AddSupportPoint(FVector position, FVector normal, float intensity, float width, float groundOffset, float textureLength, float minSqrDistanceToPrev, UMaterialInterface* material);
void IncreaseSection();
void Drop();

protected:
void ProcessFirstSupportPoint(FVector point, FVector normal, FVector direction, float intensity, float width, float groundOffset);
};

``````

[/SPOILER]

``````Skid mark mesh (source):
[SPOILER]
``````
``````

#include "SkidMarkMesh.h"

SkidMarkMesh::SkidMarkMesh(uint32 id, AActor* parent, const FString& name) :
SkidMarkMeshId(id),
CurrentSection(0)
{
// Create procedural mesh component
ProceduralMeshComponent = NewObject<UProceduralMeshComponent>(parent);
ProceduralMeshComponent->RegisterComponent();
ProceduralMeshComponent->AttachTo(parent->GetRootComponent());
}

void SkidMarkMesh::AddSupportPoint(FVector position, FVector normal, float intensity, float width, float groundOffset,
float textureLength, float minSqrDistanceToPrev, UMaterialInterface* material)
{
// Move point along normal to gain a little gap between the ground and the support point
position = position + normal * groundOffset;

// Check min distance, if possible
if (SupportPoints.Num() > 2 && FVector::DistSquared(position, SupportPoints[SupportPoints.Num() -1]) < minSqrDistanceToPrev)
return;

// We need at least two point to create a visible mesh
if (SupportPoints.Num() < 2) return;

// Extract the previous and the current/next support points
FVector prev = SupportPoints[SupportPoints.Num() - 2];
FVector next = SupportPoints[SupportPoints.Num() - 1];

// Calculate direction between the previous & the next support point
FVector dir = (next - prev);
FVector xDir = FVector::CrossProduct(dir, normal);
xDir.Normalize();

// Generate the two quad vertices on a perpendicular line to the direction
FVector posL = next + xDir * width * 0.5f;
FVector posR = next - xDir * width * 0.5f;

// Calculate the tangent vector for this two vertices
FProcMeshTangent tangent = FProcMeshTangent(xDir.X, xDir.Y, xDir.Z);

// The second support was added?
// -> Calculate the initial values for the first support point
if (SupportPoints.Num() == 2)
ProcessFirstSupportPoint(prev, normal, dir, intensity, width, groundOffset);

// Fill buffers

int32 vCount = Vertices.Num();

// Create / update section
ProceduralMeshComponent->CreateMeshSection_LinearColor(CurrentSection, Vertices, Triangles, Normals, UV0, VertexColors, Tangents, false);

// Set material
if (material)
ProceduralMeshComponent->SetMaterial(CurrentSection, material);
}

void SkidMarkMesh::IncreaseSection()
{
// Increase section counter
++CurrentSection;

// Clear buffer lists
SupportPoints.Empty();
Vertices.Empty();
Triangles.Empty();
Normals.Empty();
UV0.Empty();
VertexColors.Empty();
Tangents.Empty();
}

void SkidMarkMesh::Drop()
{
// Check parameter
if (!ProceduralMeshComponent) return;

// Destroy compontent
ProceduralMeshComponent->Deactivate();
//ProceduralMeshComponent->BeginDestroy();
//ProceduralMeshComponent = nullptr;
}

void SkidMarkMesh::ProcessFirstSupportPoint(FVector point, FVector normal, FVector direction, float intensity, float width, float groundOffset)
{
// Calculate the artificial direction for the first support point
FVector xDir = FVector::CrossProduct(direction, FVector::UpVector);
xDir.Normalize();

// Generate the two quad vertices on a perpendicular line to the direction
FVector posL = point + xDir * width * 0.5f;
FVector posR = point - xDir * width * 0.5f;

// Calculate the tangent vector for this two vertices
FProcMeshTangent tangent = FProcMeshTangent(xDir.X, xDir.Y, xDir.Z);

// Fill buffers
}

``````

[/SPOILER]

1. The movement component actually does not create the skid mark meshes by itself, but uses a skid mark manager. The skid mark manager is a normal actor, which can be and has to be placed in a map. The skid mark meshes will be rendered by the manager. This approach ensures that the skid mark meshes of a hidden vehicle will still be rendered.
``````Skid mark manager (header):
[SPOILER]
``````
``````

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Vehicle/SkidMarkMesh.h"
#include "SkidMarkMeshManager.generated.h"

UCLASS()
{
GENERATED_BODY()

uint32 IdCounter;
TMap<uint32, TSharedPtr<SkidMarkMesh>> SkidMarkMeshes;

public:
IdCounter(0)
{}

TSharedPtr<SkidMarkMesh> CreateNewMesh(FString name);
void DestroyMesh(TSharedPtr<SkidMarkMesh> mesh);

virtual void BeginDestroy() override;

};

``````

[/SPOILER]

``````Skid mark manager (source):
[SPOILER]
``````
``````

#include "SkidMarkMeshManager.h"
#include "Kismet/GameplayStatics.h"

{
// Generate mesh
TSharedPtr<SkidMarkMesh> result = MakeShared<SkidMarkMesh>(IdCounter, this, name);

// Update id counter
++IdCounter;

return result;
}

{
// Check parameter
if (!mesh) return;

// Remove instance from list
TSharedPtr<SkidMarkMesh> outPtr;
if (!SkidMarkMeshes.RemoveAndCopyValue(mesh->GetSkidMarkMeshId(), outPtr)) return;
outPtr->Drop();
}

{
// Release all meshes
for (auto& comp : SkidMarkMeshes)
comp.Value->Drop();
SkidMarkMeshes.Empty();

// Call parent's method
AActor::BeginDestroy();
}

{
// Local variables

// Check parameter
if (!enquirer) return nullptr;

// Find skid mark manager
TArray<AActor*> foundActors;
if (foundActors.Num() == 0 || (manager = Cast<ASkidMarkMeshManager>(foundActors[0])) == nullptr)
return nullptr;

return manager;
}

``````

[/SPOILER]

Improved skid mark demo video:

2 Likes