mirror of
https://github.com/NotAKidoS/NAK_CVR_Mods.git
synced 2025-09-01 22:09:23 +00:00
GrabbableSteeringWheel: commit
This commit is contained in:
parent
39da88d3d3
commit
3a00ff104a
9 changed files with 825 additions and 0 deletions
17
GrabbableSteeringWheel/GrabbableSteeringWheel.csproj
Normal file
17
GrabbableSteeringWheel/GrabbableSteeringWheel.csproj
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="BTKUILib">
|
||||
<HintPath>..\.ManagedLibs\BTKUILib.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="TheClapper">
|
||||
<HintPath>..\.ManagedLibs\TheClapper.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Utility\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1,401 @@
|
|||
using System.Collections;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Profiling;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Animations;
|
||||
|
||||
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 == null || skinnedPoints.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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,203 @@
|
|||
using ABI_RC.Core.InteractionSystem;
|
||||
using ABI_RC.Core.InteractionSystem.Base;
|
||||
using UnityEngine;
|
||||
|
||||
// TODO:
|
||||
// Fix multi-grab (limitation of Pickupable)
|
||||
// Think this can be fixed by forcing ungrab & monitoring ourselves for release
|
||||
// Add configurable override for steering range
|
||||
// Fix steering wheel resetting immediatly on release
|
||||
// Fix input patch not being multiplicative (so joysticks can still work)
|
||||
// Prevent pickup in Desktop
|
||||
|
||||
public class SteeringWheelPickup : Pickupable
|
||||
{
|
||||
private RCC_CarControllerV3 _carController;
|
||||
|
||||
private static readonly Dictionary<RCC_CarControllerV3, SteeringWheelPickup> ActiveWheels = new();
|
||||
|
||||
public static float GetSteerInput(RCC_CarControllerV3 carController)
|
||||
{
|
||||
if (ActiveWheels.TryGetValue(carController, out SteeringWheelPickup wheel) && wheel.IsPickedUp)
|
||||
return wheel.GetNormalizedValue();
|
||||
return 0f;
|
||||
}
|
||||
|
||||
public void SetupSteeringWheel(RCC_CarControllerV3 carController)
|
||||
{
|
||||
_carController = carController;
|
||||
if (!ActiveWheels.ContainsKey(carController))
|
||||
{
|
||||
ActiveWheels[carController] = this;
|
||||
carController.useCounterSteering = false;
|
||||
carController.useSteeringSmoother = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_carController != null && ActiveWheels.ContainsKey(_carController))
|
||||
ActiveWheels.Remove(_carController);
|
||||
}
|
||||
|
||||
#region Configuration Properties Override
|
||||
|
||||
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 && _carController?.SteeringWheel != null;
|
||||
|
||||
#endregion Configuration Properties Override
|
||||
|
||||
#region RCC Stuff
|
||||
|
||||
private float GetMaxSteeringRange()
|
||||
=> _carController.steerAngle * Mathf.Abs(_carController.steeringWheelAngleMultiplier);
|
||||
|
||||
private float GetSteeringWheelSign()
|
||||
=> Mathf.Sign(_carController.steeringWheelAngleMultiplier) * -1f; // Idk
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
#endregion RCC Stuff
|
||||
|
||||
#region Rotation Tracking
|
||||
|
||||
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 void StartTrackingTransform(Transform trans)
|
||||
{
|
||||
if (trans == null) return;
|
||||
|
||||
_trackedTransforms.Add(trans);
|
||||
_lastPositions.Add(GetLocalPositionWithoutRotation(transform.position));
|
||||
_totalAngles.Add(0f);
|
||||
_isTracking = true;
|
||||
}
|
||||
|
||||
private void StopTrackingTransform(Transform trans)
|
||||
{
|
||||
int index = _trackedTransforms.IndexOf(trans);
|
||||
if (index != -1)
|
||||
{
|
||||
_trackedTransforms.RemoveAt(index);
|
||||
_lastPositions.RemoveAt(index);
|
||||
_totalAngles.RemoveAt(index);
|
||||
}
|
||||
|
||||
_isTracking = _trackedTransforms.Count > 0;
|
||||
}
|
||||
|
||||
private void UpdateRotationTracking()
|
||||
{
|
||||
if (!_isTracking || _trackedTransforms.Count == 0) return;
|
||||
|
||||
Vector3 trackingAxis = GetSteeringWheelLocalAxis();
|
||||
|
||||
for (int i = 0; i < _trackedTransforms.Count; i++)
|
||||
{
|
||||
if (_trackedTransforms[i] == null) continue;
|
||||
|
||||
Vector3 currentPosition = GetLocalPositionWithoutRotation(_trackedTransforms[i].position);
|
||||
if (currentPosition == _lastPositions[i]) continue;
|
||||
|
||||
Vector3 previousVector = _lastPositions[i];
|
||||
Vector3 currentVector = currentPosition;
|
||||
|
||||
previousVector = Vector3.ProjectOnPlane(previousVector, trackingAxis).normalized;
|
||||
currentVector = Vector3.ProjectOnPlane(currentVector, trackingAxis).normalized;
|
||||
|
||||
if (previousVector.sqrMagnitude > 0.001f && currentVector.sqrMagnitude > 0.001f)
|
||||
{
|
||||
float deltaAngle = Vector3.SignedAngle(previousVector, currentVector, trackingAxis);
|
||||
if (Mathf.Abs(deltaAngle) < 90f) _totalAngles[i] += deltaAngle; // Prevent big tracking jumps
|
||||
}
|
||||
|
||||
_lastPositions[i] = currentPosition;
|
||||
}
|
||||
|
||||
// Calculate average every frame using only valid transforms
|
||||
float sumAngles = 0f;
|
||||
int validTransforms = 0;
|
||||
|
||||
for (int i = 0; i < _trackedTransforms.Count; i++)
|
||||
{
|
||||
if (_trackedTransforms[i] == null) continue;
|
||||
sumAngles += _totalAngles[i];
|
||||
validTransforms++;
|
||||
}
|
||||
|
||||
if (validTransforms > 0)
|
||||
_averageAngle = sumAngles / validTransforms;
|
||||
}
|
||||
|
||||
private float GetNormalizedValue()
|
||||
{
|
||||
float maxRange = GetMaxSteeringRange();
|
||||
// return Mathf.Clamp(_averageAngle / (maxRange * 0.5f), -1f, 1f);
|
||||
return Mathf.Clamp(_averageAngle / (maxRange), -1f, 1f) * GetSteeringWheelSign();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
#endregion Rotation Tracking
|
||||
|
||||
public override void OnGrab(InteractionContext context, Vector3 grabPoint)
|
||||
{
|
||||
if (ControllerRay?.pivotPoint == null)
|
||||
return;
|
||||
|
||||
StartTrackingTransform(ControllerRay.transform);
|
||||
}
|
||||
|
||||
public override void OnDrop(InteractionContext context)
|
||||
{
|
||||
if (ControllerRay?.transform != null)
|
||||
StopTrackingTransform(ControllerRay.transform);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsPickedUp || ControllerRay?.pivotPoint == null || _carController == null)
|
||||
return;
|
||||
|
||||
UpdateRotationTracking();
|
||||
}
|
||||
|
||||
#region Unused Abstract Method Implementations
|
||||
|
||||
public override void OnUseDown(InteractionContext context) { }
|
||||
public override void OnUseUp(InteractionContext context) { }
|
||||
public override void OnFlingTowardsTarget(Vector3 target) { }
|
||||
|
||||
#endregion Unused Abstract Method Implementations
|
||||
}
|
61
GrabbableSteeringWheel/Main.cs
Normal file
61
GrabbableSteeringWheel/Main.cs
Normal file
|
@ -0,0 +1,61 @@
|
|||
using MelonLoader;
|
||||
using NAK.GrabbableSteeringWheel.Patches;
|
||||
|
||||
namespace NAK.GrabbableSteeringWheel;
|
||||
|
||||
public class GrabbableSteeringWheelMod : MelonMod
|
||||
{
|
||||
internal static MelonLogger.Instance Logger;
|
||||
|
||||
#region Melon Preferences
|
||||
|
||||
// private static readonly MelonPreferences_Category Category =
|
||||
// MelonPreferences.CreateCategory(nameof(GrabbableSteeringWheelMod));
|
||||
//
|
||||
// private static readonly MelonPreferences_Entry<bool> EntryEnabled =
|
||||
// Category.CreateEntry(
|
||||
// "use_legacy_mitigation",
|
||||
// true,
|
||||
// "Enabled",
|
||||
// description: "Enable legacy content camera hack when in Legacy worlds.");
|
||||
|
||||
#endregion Melon Preferences
|
||||
|
||||
#region Melon Events
|
||||
|
||||
public override void OnInitializeMelon()
|
||||
{
|
||||
Logger = LoggerInstance;
|
||||
|
||||
ApplyPatches(typeof(RCCCarControllerV3_Patches));
|
||||
ApplyPatches(typeof(CVRInputManager_Patches));
|
||||
}
|
||||
|
||||
#endregion Melon Events
|
||||
|
||||
#region Melon Mod Utilities
|
||||
|
||||
private static void InitializeIntegration(string modName, Action integrationAction)
|
||||
{
|
||||
if (RegisteredMelons.All(it => it.Info.Name != modName))
|
||||
return;
|
||||
|
||||
Logger.Msg($"Initializing {modName} integration.");
|
||||
integrationAction.Invoke();
|
||||
}
|
||||
|
||||
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
|
||||
}
|
24
GrabbableSteeringWheel/ModSettings.cs
Normal file
24
GrabbableSteeringWheel/ModSettings.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
using MelonLoader;
|
||||
|
||||
namespace NAK.GrabbableSteeringWheel;
|
||||
|
||||
internal static class ModSettings
|
||||
{
|
||||
#region Constants
|
||||
|
||||
internal const string ModName = nameof(GrabbableSteeringWheel);
|
||||
// internal const string LCM_SettingsCategory = "Legacy Content Mitigation";
|
||||
|
||||
#endregion Constants
|
||||
|
||||
#region Melon Preferences
|
||||
|
||||
private static readonly MelonPreferences_Category Category =
|
||||
MelonPreferences.CreateCategory(ModName);
|
||||
|
||||
// internal static readonly MelonPreferences_Entry<bool> EntryAutoForLegacyWorlds =
|
||||
// Category.CreateEntry("auto_for_legacy_worlds", true,
|
||||
// "Auto For Legacy Worlds", description: "Should Legacy View be auto enabled for detected Legacy worlds?");
|
||||
//
|
||||
#endregion Melon Preferences
|
||||
}
|
49
GrabbableSteeringWheel/Patches.cs
Normal file
49
GrabbableSteeringWheel/Patches.cs
Normal file
|
@ -0,0 +1,49 @@
|
|||
using ABI_RC.Systems.InputManagement;
|
||||
using ABI_RC.Systems.Movement;
|
||||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.GrabbableSteeringWheel.Patches;
|
||||
|
||||
internal static class RCCCarControllerV3_Patches
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(RCC_CarControllerV3), nameof(RCC_CarControllerV3.Awake))]
|
||||
private static void Postfix_RCC_CarControllerV3_Awake(ref RCC_CarControllerV3 __instance)
|
||||
{
|
||||
Transform steeringWheelTransform = __instance.SteeringWheel;
|
||||
if (steeringWheelTransform == null)
|
||||
return;
|
||||
|
||||
RCC_CarControllerV3 v3 = __instance;
|
||||
BoneVertexBoundsUtility.CalculateBoneWeightedBounds(
|
||||
steeringWheelTransform,
|
||||
0.8f,
|
||||
BoneVertexBoundsUtility.BoundsCalculationFlags.All,
|
||||
result =>
|
||||
{
|
||||
if (!result.IsValid)
|
||||
return;
|
||||
|
||||
BoxCollider boxCollider = steeringWheelTransform.gameObject.AddComponent<BoxCollider>();
|
||||
boxCollider.center = result.LocalBounds.center;
|
||||
boxCollider.size = result.LocalBounds.size;
|
||||
|
||||
SteeringWheelPickup steeringWheel = steeringWheelTransform.gameObject.AddComponent<SteeringWheelPickup>();
|
||||
steeringWheel.SetupSteeringWheel(v3);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
__instance.steering += SteeringWheelPickup.GetSteerInput(
|
||||
BetterBetterCharacterController.Instance._lastCvrSeat._carController);
|
||||
}
|
||||
}
|
33
GrabbableSteeringWheel/Properties/AssemblyInfo.cs
Normal file
33
GrabbableSteeringWheel/Properties/AssemblyInfo.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
using System.Reflection;
|
||||
using MelonLoader;
|
||||
using NAK.GrabbableSteeringWheel;
|
||||
using NAK.GrabbableSteeringWheel.Properties;
|
||||
|
||||
[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
|
||||
[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
|
||||
[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
|
||||
[assembly: AssemblyTitle(nameof(NAK.GrabbableSteeringWheel))]
|
||||
[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
|
||||
[assembly: AssemblyProduct(nameof(NAK.GrabbableSteeringWheel))]
|
||||
|
||||
[assembly: MelonInfo(
|
||||
typeof(GrabbableSteeringWheelMod),
|
||||
nameof(NAK.GrabbableSteeringWheel),
|
||||
AssemblyInfoParams.Version,
|
||||
AssemblyInfoParams.Author,
|
||||
downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/GrabbableSteeringWheel"
|
||||
)]
|
||||
|
||||
[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.GrabbableSteeringWheel.Properties;
|
||||
internal static class AssemblyInfoParams
|
||||
{
|
||||
public const string Version = "1.0.0";
|
||||
public const string Author = "NotAKidoS";
|
||||
}
|
14
GrabbableSteeringWheel/README.md
Normal file
14
GrabbableSteeringWheel/README.md
Normal file
|
@ -0,0 +1,14 @@
|
|||
# GrabbableSteeringWheel
|
||||
|
||||
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
GrabbableSteeringWheel/format.json
Normal file
23
GrabbableSteeringWheel/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