mirror of
https://github.com/NotAKidoS/NAK_CVR_Mods.git
synced 2025-09-02 06:19:22 +00:00
RCCVirtualSteeringWheel: renamed mod, fixed things
This commit is contained in:
parent
3a00ff104a
commit
5c8c724b58
13 changed files with 445 additions and 314 deletions
40
RCCVirtualSteeringWheel/Main.cs
Normal file
40
RCCVirtualSteeringWheel/Main.cs
Normal 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
|
||||
}
|
31
RCCVirtualSteeringWheel/ModSettings.cs
Normal file
31
RCCVirtualSteeringWheel/ModSettings.cs
Normal 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
|
||||
}
|
47
RCCVirtualSteeringWheel/Patches.cs
Normal file
47
RCCVirtualSteeringWheel/Patches.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
33
RCCVirtualSteeringWheel/Properties/AssemblyInfo.cs
Normal file
33
RCCVirtualSteeringWheel/Properties/AssemblyInfo.cs
Normal 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";
|
||||
}
|
15
RCCVirtualSteeringWheel/RCCVirtualSteeringWheel.csproj
Normal file
15
RCCVirtualSteeringWheel/RCCVirtualSteeringWheel.csproj
Normal 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>
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
RCCVirtualSteeringWheel/README.md
Normal file
15
RCCVirtualSteeringWheel/README.md
Normal 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.
|
||||
~~~~
|
23
RCCVirtualSteeringWheel/format.json
Normal file
23
RCCVirtualSteeringWheel/format.json
Normal 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"
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue