[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 :slight_smile:

  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] */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedWheelInfo")
	float Rpm;
	
	/** Get the magnitude of the force pressing the wheel into the terrain [N.m] */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedWheelInfo")
	float LoadForce;
	
	/** Get the slip angle for this wheel - angle between wheel forward axis and velocity vector [degrees] */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedWheelInfo")
	float SlipAngle;
	
	/** Difference between the ground speed of the wheel and the road speed [km/h] */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedWheelInfo")
	float SlipVelocity;
	
	/** Get the linear ground speed of the wheel based on its current rotational speed [km/h] */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedWheelInfo")
	float GroundSpeed;
	
	/** Get the road speed at the wheel [km/h] */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedWheelInfo")
	float Speed;
	
	/** Get the angular velocity of the wheel [degrees/sec] */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedWheelInfo")
	float AngularVelocity;
	
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedWheelInfo")
	float NormalizedLateralSlip;
	
	/** Get the current longitudinal slip value [0 no slip - using static friction, 1 full slip - using dynamic friction] */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedWheelInfo")
	float NormalizedLongitudinalSlip;
	
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedWheelInfo")
	bool InAir;
	
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedWheelInfo")
	FString ContactSurfaceName;
	
	//UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedWheelInfo")
	//TWeakObjectPtr<UPhysicalMaterial> ContactSurface;
	
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedWheelInfo")
	int ContactSurfaceType;
	
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedWheelInfo")
	FVector ContactPoint;
	
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedWheelInfo")
	FVector ContactNormal;
	
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedWheelInfo")
	bool Slipping;
	
	FExtendedWheelInfo() :
	Rpm(0.0f),
	LoadForce(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()
	
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedVehicleInfo")
	int32 Speed;
	
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedVehicleInfo")
	float RPM;
	
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedVehicleInfo")
	int32 Gear;
	
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedVehicleInfo")
	bool Slipping;
	
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedVehicleInfo")
	bool Skidding;
	
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "ExtendedVehicleInfo")
	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] */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "TyreSqueal")
	float SlipVelocityThreshold;
	
	/** */
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "TyreSqueal")
	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:
	
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ExtendedChaosVehicle")
	UMaterialInterface* Material;
	
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "ExtendedChaosVehicle")
	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
	OnWheelStartSlipping.Broadcast(idx, wheel);
	}
	else if (lastWheel.Slipping && !wheel.Slipping) OnWheelStopSlipping.Broadcast(idx, wheel);
	else if (wheel.Slipping)
	{
	// Update slipping duration
	//wheel.SlippingDuration = lastWheel.SlippingDuration + deltaSeconds;
	
	// Fire callback
	OnWheelSlipping.Broadcast(idx, wheel);
	}
	
	// Accumulate skidding & slipping
	slipping = slipping || wheel.Slipping;
	}
	info.Slipping = slipping;
	
	// Fire events
	if (!IsSlipping && slipping)
	{
	IsSlipping = true;
	OnStartSlipping.Broadcast();
	}
	else if (IsSlipping && !slipping)
	{
	IsSlipping = false;
	OnStopSlipping.Broadcast();
	}
	
	// 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;
	}
	
	// Add new wheel info
	FExtendedWheelInfo wheelInfo;
	wheelInfo.InAir = !wheel.bInContact;
	wheelInfo.Rpm = wheel.GetWheelRPM();
	wheelInfo.LoadForce = wheel.GetWheelLoadForce() * 0.01f;
	wheelInfo.SlipAngle = RadToDeg(wheel.GetSlipAngle());
	wheelInfo.SlipVelocity = (wheel.GetWheelGroundSpeed() - wheel.GetRoadSpeed()) * 0.036f;
	wheelInfo.AngularVelocity = wheel.GetAngularVelocity() * 57.3;
	wheelInfo.GroundSpeed = wheel.GetWheelGroundSpeed() * 0.036f;
	wheelInfo.Speed = wheel.GetRoadSpeed() * 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;
	
	// Add to list
	info.WheelInfo.Add(wheelInfo);
	}
	
	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;
	}
	
	// Add skid mark
	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
	ASkidMarkMeshManager* manager = ASkidMarkMeshManager::FindManager(this->GetOwner());
	
	// 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
	ASkidMarkMeshManager* manager = ASkidMarkMeshManager::FindManager(this->GetOwner());
	
	// 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)
	{
	SkidMarkMeshes.Add(manager->CreateNewMesh(FString(TEXT("SkidMarkMesh_") + 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->bCastDynamicShadow = false;
	ProceduralMeshComponent->bCastCinematicShadow = false;
	ProceduralMeshComponent->bCastStaticShadow = false;
	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;
	
	// Add point
	SupportPoints.Add(position);
	
	// 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
	Vertices.Add(posL);
	Vertices.Add(posR);
	Tangents.Add(tangent);
	Tangents.Add(tangent);
	
	int32 vCount = Vertices.Num();
	
	Triangles.Add(vCount - 4);
	Triangles.Add(vCount - 3);
	Triangles.Add(vCount - 2);
	Triangles.Add(vCount - 3);
	Triangles.Add(vCount - 1);
	Triangles.Add(vCount - 2);
	
	Normals.Add(normal);
	Normals.Add(normal);
	
	VertexColors.Add(FLinearColor(1, 1, 1, intensity));
	VertexColors.Add(FLinearColor(1, 1, 1, intensity));
	
	UV0.Add(FVector2D(SupportPoints.Num() / textureLength, 0));
	UV0.Add(FVector2D(SupportPoints.Num() / textureLength, 1));
	
	// 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
	UV0.Add(FVector2D(0, 0));
	UV0.Add(FVector2D(0, 1));
	Vertices.Add(posL);
	Vertices.Add(posR);
	Tangents.Add(tangent);
	Tangents.Add(tangent);
	Normals.Add(normal);
	Normals.Add(normal);
	VertexColors.Add(FLinearColor(1, 1, 1, intensity));
	VertexColors.Add(FLinearColor(1, 1, 1, intensity));
	}
	

[/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()
	class ASkidMarkMeshManager : public AActor
	{
	GENERATED_BODY()
	
	uint32 IdCounter;
	TMap<uint32, TSharedPtr<SkidMarkMesh>> SkidMarkMeshes;
	
	public:
	ASkidMarkMeshManager() :
	IdCounter(0)
	{}
	
	TSharedPtr<SkidMarkMesh> CreateNewMesh(FString name);
	void DestroyMesh(TSharedPtr<SkidMarkMesh> mesh);
	
	virtual void BeginDestroy() override;
	
	static ASkidMarkMeshManager* FindManager(AActor* enquirer);
	};
	

[/SPOILER]

Skid mark manager (source):
[SPOILER]


	
	#include "SkidMarkMeshManager.h"
	#include "Kismet/GameplayStatics.h"
	
	TSharedPtr<SkidMarkMesh> ASkidMarkMeshManager::CreateNewMesh(FString name)
	{
	// Generate mesh
	TSharedPtr<SkidMarkMesh> result = MakeShared<SkidMarkMesh>(IdCounter, this, name);
	SkidMarkMeshes.Add(IdCounter, result);
	
	// Update id counter
	++IdCounter;
	
	return result;
	}
	
	void ASkidMarkMeshManager::DestroyMesh(TSharedPtr<SkidMarkMesh> mesh)
	{
	// Check parameter
	if (!mesh) return;
	
	// Remove instance from list
	TSharedPtr<SkidMarkMesh> outPtr;
	if (!SkidMarkMeshes.RemoveAndCopyValue(mesh->GetSkidMarkMeshId(), outPtr)) return;
	outPtr->Drop();
	}
	
	void ASkidMarkMeshManager::BeginDestroy()
	{
	// Release all meshes
	for (auto& comp : SkidMarkMeshes)
	comp.Value->Drop();
	SkidMarkMeshes.Empty();
	
	// Call parent's method
	AActor::BeginDestroy();
	}
	
	ASkidMarkMeshManager* ASkidMarkMeshManager::FindManager(AActor* enquirer)
	{
	// Local variables
	ASkidMarkMeshManager* manager = nullptr;
	
	// Check parameter
	if (!enquirer) return nullptr;
	
	// Find skid mark manager
	TArray<AActor*> foundActors;
	UGameplayStatics::GetAllActorsOfClass(enquirer->GetWorld(), ASkidMarkMeshManager::StaticClass(), foundActors);
	if (foundActors.Num() == 0 || (manager = Cast<ASkidMarkMeshManager>(foundActors[0])) == nullptr)
	return nullptr;
	
	return manager;
	}
	

[/SPOILER]

Improved skid mark demo video:

2 Likes