RCCVirtualSteeringWheel: renamed mod, fixed things

This commit is contained in:
NotAKidoS 2025-01-07 02:36:44 -06:00
parent 3a00ff104a
commit 5c8c724b58
13 changed files with 445 additions and 314 deletions

View file

@ -0,0 +1,40 @@
using MelonLoader;
using NAK.RCCVirtualSteeringWheel.Patches;
namespace NAK.RCCVirtualSteeringWheel;
public class RCCVirtualSteeringWheelMod : MelonMod
{
private static MelonLogger.Instance Logger;
#region Melon Events
public override void OnInitializeMelon()
{
Logger = LoggerInstance;
ApplyPatches(typeof(RCCCarControllerV3_Patches));
ApplyPatches(typeof(CVRInputManager_Patches));
Logger.Msg(ModSettings.EntryCustomSteeringRange);
}
#endregion Melon Events
#region Melon Mod Utilities
private void ApplyPatches(Type type)
{
try
{
HarmonyInstance.PatchAll(type);
}
catch (Exception e)
{
LoggerInstance.Msg($"Failed while patching {type.Name}!");
LoggerInstance.Error(e);
}
}
#endregion Melon Mod Utilities
}

View file

@ -0,0 +1,31 @@
using MelonLoader;
namespace NAK.RCCVirtualSteeringWheel;
internal static class ModSettings
{
#region Constants
private const string ModName = nameof(RCCVirtualSteeringWheel);
#endregion Constants
#region Melon Preferences
private static readonly MelonPreferences_Category Category =
MelonPreferences.CreateCategory(ModName);
internal static readonly MelonPreferences_Entry<bool> EntryOverrideSteeringRange =
Category.CreateEntry("override_steering_range", false,
"Override Steering Range", description: "Should the steering wheel use a custom steering range instead of the vehicle's default?");
internal static readonly MelonPreferences_Entry<float> EntryCustomSteeringRange =
Category.CreateEntry("custom_steering_range", 60f,
"Custom Steering Range", description: "The custom steering range in degrees when override is enabled (default: 60)");
internal static readonly MelonPreferences_Entry<bool> EntryInvertSteering =
Category.CreateEntry("invert_steering", false,
"Invert Steering", description: "Inverts the steering direction");
#endregion Melon Preferences
}

View file

@ -0,0 +1,47 @@
using ABI_RC.Systems.InputManagement;
using ABI_RC.Systems.Movement;
using HarmonyLib;
using NAK.RCCVirtualSteeringWheel.Util;
using UnityEngine;
namespace NAK.RCCVirtualSteeringWheel.Patches;
internal static class RCCCarControllerV3_Patches
{
[HarmonyPostfix]
[HarmonyPatch(typeof(RCC_CarControllerV3), nameof(RCC_CarControllerV3.Awake))]
private static void Postfix_RCC_CarControllerV3_Awake(RCC_CarControllerV3 __instance)
{
Transform steeringWheelTransform = __instance.SteeringWheel;
if (steeringWheelTransform == null)
return;
BoneVertexBoundsUtility.CalculateBoneWeightedBounds(
steeringWheelTransform,
0.8f,
BoneVertexBoundsUtility.BoundsCalculationFlags.All,
result =>
{
if (!result.IsValid)
return;
SteeringWheelRoot.SetupSteeringWheel(__instance, result.LocalBounds);
});
}
}
internal static class CVRInputManager_Patches
{
[HarmonyPostfix]
[HarmonyPatch(typeof(CVRInputManager), nameof(CVRInputManager.UpdateInput))]
private static void Postfix_CVRInputManager_UpdateInput(ref CVRInputManager __instance)
{
// Steering input is clamped in RCC component
if (BetterBetterCharacterController.Instance.IsSittingOnControlSeat()
&& SteeringWheelRoot.TryGetWheelInput(
BetterBetterCharacterController.Instance._lastCvrSeat._carController, out float steeringValue))
{
__instance.steering = steeringValue;
}
}
}

View file

@ -0,0 +1,33 @@
using System.Reflection;
using MelonLoader;
using NAK.RCCVirtualSteeringWheel;
using NAK.RCCVirtualSteeringWheel.Properties;
[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyTitle(nameof(NAK.RCCVirtualSteeringWheel))]
[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
[assembly: AssemblyProduct(nameof(NAK.RCCVirtualSteeringWheel))]
[assembly: MelonInfo(
typeof(RCCVirtualSteeringWheelMod),
nameof(NAK.RCCVirtualSteeringWheel),
AssemblyInfoParams.Version,
AssemblyInfoParams.Author,
downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/RCCVirtualSteeringWheel"
)]
[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")]
[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)]
[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)]
[assembly: MelonColor(255, 246, 25, 99)] // red-pink
[assembly: MelonAuthorColor(255, 158, 21, 32)] // red
[assembly: HarmonyDontPatchAll]
namespace NAK.RCCVirtualSteeringWheel.Properties;
internal static class AssemblyInfoParams
{
public const string Version = "1.0.0";
public const string Author = "NotAKidoS";
}

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<RootNamespace>GrabbableSteeringWheel</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Reference Include="BTKUILib">
<HintPath>..\.ManagedLibs\BTKUILib.dll</HintPath>
</Reference>
<Reference Include="TheClapper">
<HintPath>..\.ManagedLibs\TheClapper.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,36 @@
using ABI_RC.Core.InteractionSystem;
using ABI_RC.Core.InteractionSystem.Base;
using UnityEngine;
namespace NAK.RCCVirtualSteeringWheel;
public class SteeringWheelPickup : Pickupable
{
#region Public Properties
public override bool DisallowTheft => true;
public override float MaxGrabDistance => 0.8f;
public override float MaxPushDistance => 0f;
public override bool IsAutoHold => false;
public override bool IsObjectRotationAllowed => false;
public override bool IsObjectPushPullAllowed => false;
public override bool IsObjectUseAllowed => false;
public override bool CanPickup => IsPickupable && !IsPickedUp;
internal SteeringWheelRoot root;
#endregion
#region Public Methods
public override void OnUseDown(InteractionContext context) { }
public override void OnUseUp(InteractionContext context) { }
public override void OnFlingTowardsTarget(Vector3 target) { }
public override void OnGrab(InteractionContext context, Vector3 grabPoint) {
if (ControllerRay?.pivotPoint != null)
root.StartTrackingTransform(ControllerRay.transform);
}
public override void OnDrop(InteractionContext context) {
if (ControllerRay?.transform != null)
root.StopTrackingTransform(ControllerRay.transform);
}
#endregion
}

View file

@ -0,0 +1,313 @@
using UnityEngine;
namespace NAK.RCCVirtualSteeringWheel;
public class SteeringWheelRoot : MonoBehaviour
{
#region Static Variables
private static readonly Dictionary<RCC_CarControllerV3, SteeringWheelRoot> ActiveWheels = new();
#endregion Static Variables
#region Static Methods
public static bool TryGetWheelInput(RCC_CarControllerV3 carController, out float steeringInput)
{
if (ActiveWheels.TryGetValue(carController, out SteeringWheelRoot wheel) && wheel._averageAngle != 0f)
{
steeringInput = wheel.GetNormalizedValue();
return true;
}
steeringInput = 0f;
return false;
}
public static void SetupSteeringWheel(RCC_CarControllerV3 carController, Bounds steeringWheelBounds)
{
Transform steeringWheel = carController.SteeringWheel;
if (carController == null) return;
SteeringWheelRoot wheel = steeringWheel.gameObject.AddComponent<SteeringWheelRoot>();
wheel._carController = carController;
Array.Resize(ref wheel._pickups, 2);
CreatePickup(out wheel._pickups[0]);
CreatePickup(out wheel._pickups[1]);
return;
void CreatePickup(out SteeringWheelPickup wheelPickup)
{
GameObject pickup = new()
{
transform =
{
parent = steeringWheel.transform,
localPosition = Vector3.zero,
localRotation = Quaternion.identity,
localScale = Vector3.one
}
};
BoxCollider collider = pickup.AddComponent<BoxCollider>();
collider.size = steeringWheelBounds.size;
collider.center = steeringWheelBounds.center;
wheelPickup = pickup.AddComponent<SteeringWheelPickup>();
wheelPickup.root = wheel;
}
}
#endregion Static Methods
#region Public Properties
private bool IsWheelBeingHeld => _pickups[0].IsPickedUp || _pickups[1].IsPickedUp;
#endregion Public Properties
#region Private Variables
private RCC_CarControllerV3 _carController;
private SteeringWheelPickup[] _pickups;
private float _originalSteeringWheelAngleMultiplier;
private float _originalSteeringWheelSign;
private readonly List<Transform> _trackedTransforms = new();
private readonly List<Vector3> _lastPositions = new();
private readonly List<float> _totalAngles = new();
private bool _isTracking;
private float _averageAngle;
private float _timeWheelReleased = -1f;
private const float RETURN_TO_CENTER_DURATION = 2f;
#endregion Private Variables
#region Unity Events
private void Start()
{
ActiveWheels.TryAdd(_carController, this);
InitializeWheel();
}
private void Update()
{
if (_carController == null) return;
UpdateWheelState();
UpdateSteeringBehavior();
}
private void OnDestroy()
{
ActiveWheels.Remove(_carController);
}
#endregion Unity Events
#region Public Methods
internal void StartTrackingTransform(Transform trans)
{
if (trans == null) return;
var currentAngle = 0f;
if (_isTracking)
{
var sum = 0f;
var validTransforms = 0;
for (var i = 0; i < _trackedTransforms.Count; i++)
if (_trackedTransforms[i] != null)
{
sum += _totalAngles[i];
validTransforms++;
}
if (validTransforms > 0)
currentAngle = sum / validTransforms;
}
_trackedTransforms.Add(trans);
_lastPositions.Add(GetLocalPositionWithoutRotation(transform.position));
_totalAngles.Add(currentAngle);
_isTracking = true;
}
internal void StopTrackingTransform(Transform trans)
{
var index = _trackedTransforms.IndexOf(trans);
if (index == -1) return;
var currentAverage = CalculateCurrentAverage();
_trackedTransforms.RemoveAt(index);
_lastPositions.RemoveAt(index);
_totalAngles.RemoveAt(index);
if (_trackedTransforms.Count <= 0)
return;
for (var i = 0; i < _totalAngles.Count; i++)
_totalAngles[i] = currentAverage;
}
private float CalculateCurrentAverage()
{
var sum = 0f;
var validTransforms = 0;
for (var i = 0; i < _trackedTransforms.Count; i++)
if (_trackedTransforms[i] != null)
{
sum += _totalAngles[i];
validTransforms++;
}
return validTransforms > 0 ? sum / validTransforms : 0f;
}
#endregion Public Methods
#region Private Methods
private void InitializeWheel()
{
_originalSteeringWheelAngleMultiplier = _carController.steeringWheelAngleMultiplier;
_originalSteeringWheelSign = Mathf.Sign(_originalSteeringWheelAngleMultiplier);
_carController.useCounterSteering = false;
_carController.useSteeringSmoother = false;
}
private void UpdateWheelState()
{
var isHeld = IsWheelBeingHeld;
if (!isHeld && _timeWheelReleased < 0f)
_timeWheelReleased = Time.time;
else if (isHeld)
_timeWheelReleased = -1f;
}
private void UpdateSteeringBehavior()
{
UpdateSteeringMultiplier();
if (IsWheelBeingHeld)
UpdateRotationTracking();
else if (_timeWheelReleased >= 0f)
HandleWheelReturn();
}
private void UpdateSteeringMultiplier()
{
_carController.steeringWheelAngleMultiplier = ModSettings.EntryOverrideSteeringRange.Value
? ModSettings.EntryCustomSteeringRange.Value * _originalSteeringWheelSign / _carController.steerAngle
: _originalSteeringWheelAngleMultiplier;
}
private void HandleWheelReturn()
{
var timeSinceRelease = Time.time - _timeWheelReleased;
if (timeSinceRelease < RETURN_TO_CENTER_DURATION)
{
var t = timeSinceRelease / RETURN_TO_CENTER_DURATION;
_averageAngle = Mathf.Lerp(_averageAngle, 0f, t);
for (var i = 0; i < _totalAngles.Count; i++)
_totalAngles[i] = _averageAngle;
}
else
{
_averageAngle = 0f;
for (var i = 0; i < _totalAngles.Count; i++)
_totalAngles[i] = 0f;
}
}
private float GetMaxSteeringRange()
{
return _carController.steerAngle * Mathf.Abs(_carController.steeringWheelAngleMultiplier);
}
private float GetSteeringWheelSign()
{
return _originalSteeringWheelSign * (ModSettings.EntryInvertSteering.Value ? 1f : -1f);
}
private Vector3 GetSteeringWheelLocalAxis()
{
return _carController.steeringWheelRotateAround switch
{
RCC_CarControllerV3.SteeringWheelRotateAround.XAxis => Vector3.right,
RCC_CarControllerV3.SteeringWheelRotateAround.YAxis => Vector3.up,
RCC_CarControllerV3.SteeringWheelRotateAround.ZAxis => Vector3.forward,
_ => Vector3.forward
};
}
private Vector3 GetLocalPositionWithoutRotation(Vector3 worldPosition)
{
Transform steeringTransform = _carController.SteeringWheel;
Quaternion localRotation = steeringTransform.localRotation;
steeringTransform.localRotation = _carController.orgSteeringWheelRot;
Vector3 localPosition = steeringTransform.InverseTransformPoint(worldPosition);
steeringTransform.localRotation = localRotation;
return localPosition;
}
private float GetNormalizedValue()
{
return Mathf.Clamp(_averageAngle / GetMaxSteeringRange(), -1f, 1f) * GetSteeringWheelSign();
}
private void UpdateRotationTracking()
{
if (!_isTracking || _trackedTransforms.Count == 0) return;
Vector3 trackingAxis = GetSteeringWheelLocalAxis();
UpdateTransformAngles(trackingAxis);
UpdateAverageAngle();
}
private void UpdateTransformAngles(Vector3 trackingAxis)
{
for (var i = 0; i < _trackedTransforms.Count; i++)
{
if (_trackedTransforms[i] == null) continue;
Vector3 currentPosition = GetLocalPositionWithoutRotation(_trackedTransforms[i].position);
if (currentPosition == _lastPositions[i]) continue;
Vector3 previousVector = Vector3.ProjectOnPlane(_lastPositions[i], trackingAxis).normalized;
Vector3 currentVector = Vector3.ProjectOnPlane(currentPosition, trackingAxis).normalized;
if (previousVector.sqrMagnitude > 0.001f && currentVector.sqrMagnitude > 0.001f)
{
var deltaAngle = Vector3.SignedAngle(previousVector, currentVector, trackingAxis);
if (Mathf.Abs(deltaAngle) < 90f)
_totalAngles[i] += deltaAngle;
}
_lastPositions[i] = currentPosition;
}
}
private void UpdateAverageAngle()
{
var sumAngles = 0f;
var validTransforms = 0;
for (var i = 0; i < _trackedTransforms.Count; i++)
{
if (_trackedTransforms[i] == null) continue;
sumAngles += _totalAngles[i];
validTransforms++;
}
if (validTransforms > 0)
_averageAngle = sumAngles / validTransforms;
}
#endregion Private Methods
}

View file

@ -0,0 +1,403 @@
using System.Collections;
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Profiling;
using UnityEngine;
using UnityEngine.Animations;
namespace NAK.RCCVirtualSteeringWheel.Util;
public class BoneVertexBoundsUtility : MonoBehaviour
{
private static readonly ProfilerMarker s_calculateBoundsMarker = new("BoneVertexBounds.Calculate");
private static readonly ProfilerMarker s_processChildRenderersMarker = new("BoneVertexBounds.ProcessChildRenderers");
private static readonly ProfilerMarker s_processSkinnedMeshesMarker = new("BoneVertexBounds.ProcessSkinnedMeshes");
private static readonly ProfilerMarker s_processVerticesMarker = new("BoneVertexBounds.ProcessVertices");
private static readonly ProfilerMarker s_processConstraintsMarker = new("BoneVertexBounds.ProcessConstraints");
private static readonly ProfilerMarker s_jobExecutionMarker = new("BoneVertexBounds.JobExecution");
private static readonly ProfilerMarker s_meshCopyMarker = new("BoneVertexBounds.MeshCopy");
private static BoneVertexBoundsUtility instance;
private static BoneVertexBoundsUtility Instance
{
get
{
if (instance != null) return instance;
GameObject go = new("BoneVertexBoundsUtility");
instance = go.AddComponent<BoneVertexBoundsUtility>();
DontDestroyOnLoad(go);
return instance;
}
}
[Flags]
public enum BoundsCalculationFlags
{
None = 0,
IncludeChildren = 1 << 0,
IncludeSkinnedMesh = 1 << 1,
IncludeConstraints = 1 << 2,
All = IncludeChildren | IncludeSkinnedMesh | IncludeConstraints
}
public struct BoundsResult
{
public bool IsValid;
public Bounds LocalBounds;
}
/// <summary>
/// Calculates the bounds of a transform based on:
/// - Children Renderers
/// - Skinned Mesh Weights
/// - Constrained Child Renderers & Skinned Mesh Weights
/// </summary>
public static void CalculateBoneWeightedBounds(Transform bone, float weightThreshold, BoundsCalculationFlags flags, Action<BoundsResult> onComplete)
=> Instance.StartCoroutine(Instance.CalculateBoundsCoroutine(bone, weightThreshold, flags, onComplete));
private IEnumerator CalculateBoundsCoroutine(Transform bone, float weightThreshold, BoundsCalculationFlags flags, Action<BoundsResult> onComplete)
{
using (s_calculateBoundsMarker.Auto())
{
BoundsResult result = new();
var allWeightedPoints = new List<Vector3>();
bool hasValidPoints = false;
// Child renderers
IEnumerator ProcessChildRenderersLocal(Transform targetBone, Action<List<Vector3>> onChildPoints)
{
using (s_processChildRenderersMarker.Auto())
{
var points = new List<Vector3>();
var childRenderers = targetBone.GetComponentsInChildren<Renderer>()
.Where(r => r is not SkinnedMeshRenderer)
.ToArray();
foreach (Renderer childRend in childRenderers)
{
Bounds bounds = childRend.localBounds;
var corners = new Vector3[8];
Vector3 ext = bounds.extents;
Vector3 center = bounds.center;
corners[0] = new Vector3(center.x - ext.x, center.y - ext.y, center.z - ext.z);
corners[1] = new Vector3(center.x + ext.x, center.y - ext.y, center.z - ext.z);
corners[2] = new Vector3(center.x - ext.x, center.y + ext.y, center.z - ext.z);
corners[3] = new Vector3(center.x + ext.x, center.y + ext.y, center.z - ext.z);
corners[4] = new Vector3(center.x - ext.x, center.y - ext.y, center.z + ext.z);
corners[5] = new Vector3(center.x + ext.x, center.y - ext.y, center.z + ext.z);
corners[6] = new Vector3(center.x - ext.x, center.y + ext.y, center.z + ext.z);
corners[7] = new Vector3(center.x + ext.x, center.y + ext.y, center.z + ext.z);
for (int i = 0; i < 8; i++)
points.Add(targetBone.InverseTransformPoint(childRend.transform.TransformPoint(corners[i])));
}
onChildPoints?.Invoke(points);
}
yield break;
}
// Skinned mesh renderers
IEnumerator ProcessSkinnedMeshRenderersLocal(Transform targetBone, float threshold, Action<List<Vector3>> onSkinnedPoints)
{
using (s_processSkinnedMeshesMarker.Auto())
{
var points = new List<Vector3>();
var siblingAndParentSkinnedMesh = targetBone.root.GetComponentsInChildren<SkinnedMeshRenderer>();
var relevantMeshes = siblingAndParentSkinnedMesh.Where(smr => DoesMeshUseBone(smr, targetBone)).ToArray();
foreach (SkinnedMeshRenderer smr in relevantMeshes)
{
yield return StartCoroutine(ProcessSkinnedMesh(smr, targetBone, threshold, meshPoints =>
{
if (meshPoints is { Length: > 0 })
points.AddRange(meshPoints);
}));
}
onSkinnedPoints?.Invoke(points);
}
}
// Constraints
IEnumerator ProcessConstraintsLocal(Transform targetBone, float threshold, BoundsCalculationFlags constraintFlags, Action<List<Vector3>> onConstraintPoints)
{
using (s_processConstraintsMarker.Auto())
{
var points = new List<Vector3>();
var processedTransforms = new HashSet<Transform>();
var constrainedTransforms = new List<Transform>();
// Find all constrained objects that reference our bone
var constraints = targetBone.root.GetComponentsInChildren<IConstraint>();
foreach (IConstraint constraint in constraints)
{
for (int i = 0; i < constraint.sourceCount; i++)
{
if (constraint.GetSource(i).sourceTransform != targetBone) continue;
constrainedTransforms.Add(((Behaviour)constraint).transform);
break;
}
}
// Process each constrained transform
foreach (Transform constrainedTransform in constrainedTransforms)
{
if (!processedTransforms.Add(constrainedTransform))
continue;
var localPoints = new List<Vector3>();
bool hasLocalPoints = false;
// Process child renderers if enabled
if ((constraintFlags & BoundsCalculationFlags.IncludeChildren) != 0)
{
yield return StartCoroutine(ProcessChildRenderersLocal(constrainedTransform, childPoints =>
{
if (childPoints is not { Count: > 0 }) return;
localPoints.AddRange(childPoints);
hasLocalPoints = true;
}));
}
// Process skinned mesh if enabled
if ((constraintFlags & BoundsCalculationFlags.IncludeSkinnedMesh) != 0)
{
yield return StartCoroutine(ProcessSkinnedMeshRenderersLocal(constrainedTransform, threshold, skinnedPoints =>
{
if (skinnedPoints is not { Count: > 0 }) return;
localPoints.AddRange(skinnedPoints);
hasLocalPoints = true;
}));
}
if (!hasLocalPoints)
continue;
// Convert all points to bone space
foreach (Vector3 point in localPoints)
points.Add(targetBone.InverseTransformPoint(constrainedTransform.TransformPoint(point)));
}
onConstraintPoints?.Invoke(points);
}
}
// Process child renderers
if ((flags & BoundsCalculationFlags.IncludeChildren) != 0)
{
yield return StartCoroutine(ProcessChildRenderersLocal(bone, childPoints =>
{
if (childPoints is not { Count: > 0 }) return;
allWeightedPoints.AddRange(childPoints);
hasValidPoints = true;
}));
}
// Process skinned mesh renderers
if ((flags & BoundsCalculationFlags.IncludeSkinnedMesh) != 0)
{
yield return StartCoroutine(ProcessSkinnedMeshRenderersLocal(bone, weightThreshold, skinnedPoints =>
{
if (skinnedPoints is not { Count: > 0 }) return;
allWeightedPoints.AddRange(skinnedPoints);
hasValidPoints = true;
}));
}
// Process constraints
if ((flags & BoundsCalculationFlags.IncludeConstraints) != 0)
{
// Use only Children and SkinnedMesh flags for constraint processing to prevent recursion (maybe make optional)?
BoundsCalculationFlags constraintFlags = flags & ~BoundsCalculationFlags.IncludeConstraints;
yield return StartCoroutine(ProcessConstraintsLocal(bone, weightThreshold, constraintFlags, constraintPoints =>
{
if (constraintPoints is not { Count: > 0 }) return;
allWeightedPoints.AddRange(constraintPoints);
hasValidPoints = true;
}));
}
if (!hasValidPoints)
{
result.IsValid = false;
onComplete?.Invoke(result);
yield break;
}
// Calculate final bounds in bone space
Bounds bounds = new(allWeightedPoints[0], Vector3.zero);
foreach (Vector3 point in allWeightedPoints)
bounds.Encapsulate(point);
// Ensure minimum size
Vector3 size = bounds.size;
size = Vector3.Max(size, Vector3.one * 0.01f);
bounds.size = size;
result.IsValid = true;
result.LocalBounds = bounds;
onComplete?.Invoke(result);
}
}
private static bool DoesMeshUseBone(SkinnedMeshRenderer smr, Transform bone)
=> smr.bones != null && smr.bones.Contains(bone);
private IEnumerator ProcessSkinnedMesh(SkinnedMeshRenderer smr, Transform bone, float weightThreshold,
Action<Vector3[]> onComplete)
{
Mesh mesh = smr.sharedMesh;
if (mesh == null)
{
onComplete?.Invoke(null);
yield break;
}
// Find bone index
int boneIndex = Array.IndexOf(smr.bones, bone);
if (boneIndex == -1)
{
onComplete?.Invoke(null);
yield break;
}
Mesh meshToUse = mesh;
GameObject tempGO = null;
try
{
// Handle non-readable meshes (ReadItAnyway lmao)
if (!mesh.isReadable)
{
using (s_meshCopyMarker.Auto())
{
tempGO = new GameObject("TempMeshReader");
SkinnedMeshRenderer tempSMR = tempGO.AddComponent<SkinnedMeshRenderer>();
tempSMR.sharedMesh = mesh;
meshToUse = new Mesh();
tempSMR.BakeMesh(meshToUse);
}
}
Mesh.MeshDataArray meshDataArray = Mesh.AcquireReadOnlyMeshData(meshToUse);
Mesh.MeshData meshData = meshDataArray[0];
var vertexCount = meshData.vertexCount;
var vertices = new NativeArray<Vector3>(vertexCount, Allocator.TempJob);
var weights = new NativeArray<BoneWeight>(vertexCount, Allocator.TempJob);
var results = new NativeArray<VertexResult>(vertexCount, Allocator.TempJob);
meshData.GetVertices(vertices);
weights.CopyFrom(mesh.boneWeights);
// Debug.Log(vertices.Length);
// Debug.Log(weights.Length);
using (s_processVerticesMarker.Auto())
{
try
{
Transform rootBone = smr.rootBone ? smr.rootBone.transform : smr.transform;
Matrix4x4 meshToWorld = Matrix4x4.TRS(smr.transform.position, smr.transform.rotation, rootBone.lossyScale);
// Fixes setup where mesh was in diff hierarchy & 0.001 scale, bone & root bone outside & above
meshToWorld *= Matrix4x4.TRS(Vector3.zero, Quaternion.identity, smr.transform.localScale);
ProcessVerticesJob processJob = new()
{
Vertices = vertices,
BoneWeights = weights,
Results = results,
BoneIndex = boneIndex,
WeightThreshold = weightThreshold,
MeshToWorld = meshToWorld,
WorldToBone = bone.worldToLocalMatrix
};
using (s_jobExecutionMarker.Auto())
{
int batchCount = Mathf.Max(1, vertexCount / 64);
JobHandle jobHandle = processJob.Schedule(vertexCount, batchCount);
while (!jobHandle.IsCompleted)
yield return null;
jobHandle.Complete();
}
// Collect valid points
var validPoints = new List<Vector3>();
for (int i = 0; i < results.Length; i++)
if (results[i].IsValid) validPoints.Add(results[i].Position);
onComplete?.Invoke(validPoints.ToArray());
}
finally
{
vertices.Dispose();
weights.Dispose();
results.Dispose();
meshDataArray.Dispose();
}
}
}
finally
{
// Destroy duplicated baked mesh if we created one to read mesh data
if (!mesh.isReadable && meshToUse != mesh) Destroy(meshToUse);
if (tempGO != null) Destroy(tempGO);
}
}
private struct VertexResult
{
public Vector3 Position;
public bool IsValid;
}
[BurstCompile]
private struct ProcessVerticesJob : IJobParallelFor
{
[ReadOnly] public NativeArray<Vector3> Vertices;
[ReadOnly] public NativeArray<BoneWeight> BoneWeights;
[NativeDisableParallelForRestriction]
public NativeArray<VertexResult> Results;
[ReadOnly] public int BoneIndex;
[ReadOnly] public float WeightThreshold;
[ReadOnly] public Matrix4x4 MeshToWorld;
[ReadOnly] public Matrix4x4 WorldToBone;
public void Execute(int i)
{
BoneWeight weight = BoneWeights[i];
float totalWeight = 0f;
if (weight.boneIndex0 == BoneIndex) totalWeight += weight.weight0;
if (weight.boneIndex1 == BoneIndex) totalWeight += weight.weight1;
if (weight.boneIndex2 == BoneIndex) totalWeight += weight.weight2;
if (weight.boneIndex3 == BoneIndex) totalWeight += weight.weight3;
if (totalWeight >= WeightThreshold)
{
// Transform vertex to bone space
Vector3 worldPos = MeshToWorld.MultiplyPoint3x4(Vertices[i]);
Vector3 boneLocalPos = WorldToBone.MultiplyPoint3x4(worldPos);
Results[i] = new VertexResult
{
Position = boneLocalPos,
IsValid = true
};
}
else
{
Results[i] = new VertexResult { IsValid = false };
}
}
}
}

View file

@ -0,0 +1,15 @@
# RCCVirtualSteeringWheel
Experiment with making rigged RCC vehicle steering wheels work for VR steering input.
---
Here is the block of text where I tell you this mod is not affiliated with or endorsed by ABI.
https://documentation.abinteractive.net/official/legal/tos/#7-modding-our-games
> This mod is an independent creation not affiliated with, supported by, or approved by Alpha Blend Interactive.
> Use of this mod is done so at the user's own risk and the creator cannot be held responsible for any issues arising from its use.
> To the best of my knowledge, I have adhered to the Modding Guidelines established by Alpha Blend Interactive.
~~~~

View file

@ -0,0 +1,23 @@
{
"_id": -1,
"name": "LegacyContentMitigation",
"modversion": "1.0.1",
"gameversion": "2024r177",
"loaderversion": "0.6.1",
"modtype": "Mod",
"author": "Exterrata, NotAKidoS",
"description": "Experimental mod that manually renders both VR eyes separately while in Legacy Worlds.\n\nThe mod will automatically kick-in when loading NonSpi Worlds & prevent Shader Replacement for all content while active. You can also manually toggle it within the BTKUI Misc tab for experimenting.\n\n-# There is an obvious performance penalty with rendering the world 2x & toggling the head hiding per eye, so enabling the mod outside of Legacy Worlds is not recommended.",
"searchtags": [
"legacy",
"content",
"spi",
"shaders"
],
"requirements": [
"BTKUILib"
],
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r44/LegacyContentMitigation.dll",
"sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/LegacyContentMitigation/",
"changelog": "- Initial release",
"embedcolor": "#f61963"
}