first commit
This commit is contained in:
87
Code/Utils/EngineStream/ControllerConverter.cs
Normal file
87
Code/Utils/EngineStream/ControllerConverter.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using Sandbox;
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace VeloX.Audio;
|
||||
|
||||
[JsonConverter( typeof( ControllerConverter ) )]
|
||||
public sealed class Controller
|
||||
{
|
||||
public enum InputTypes
|
||||
{
|
||||
Throttle,
|
||||
RpmFraction,
|
||||
}
|
||||
public enum OutputTypes
|
||||
{
|
||||
Volume,
|
||||
Pitch,
|
||||
}
|
||||
|
||||
public required InputTypes InputParameter { get; set; }
|
||||
[Property] public ValueRange InputRange { get; set; }
|
||||
public required OutputTypes OutputParameter { get; set; }
|
||||
public ValueRange OutputRange { get; set; }
|
||||
}
|
||||
|
||||
public record struct ValueRange
|
||||
{
|
||||
public ValueRange( float inputMin, float inputMax )
|
||||
{
|
||||
Min = inputMin;
|
||||
Max = inputMax;
|
||||
}
|
||||
|
||||
[Range( 0, 1 )] public float Min { get; set; }
|
||||
[Range( 0, 1 )] public float Max { get; set; }
|
||||
|
||||
public readonly float Remap( float value, float newMin, float newMax )
|
||||
{
|
||||
var normalized = (value - Min) / (Max - Min);
|
||||
return newMin + normalized * (newMax - newMin);
|
||||
}
|
||||
};
|
||||
|
||||
public sealed class ControllerConverter : JsonConverter<Controller>
|
||||
{
|
||||
public override Controller Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options )
|
||||
{
|
||||
if ( reader.TokenType != JsonTokenType.StartArray )
|
||||
throw new JsonException( "Expected array for Controller" );
|
||||
|
||||
reader.Read();
|
||||
string inputParam = reader.GetString() ?? string.Empty;
|
||||
reader.Read();
|
||||
float inputMin = reader.GetSingle();
|
||||
reader.Read();
|
||||
float inputMax = reader.GetSingle();
|
||||
reader.Read();
|
||||
string outputParam = reader.GetString() ?? string.Empty;
|
||||
reader.Read();
|
||||
float outputMin = reader.GetSingle();
|
||||
reader.Read();
|
||||
float outputMax = reader.GetSingle();
|
||||
reader.Read(); // End of array
|
||||
|
||||
return new Controller
|
||||
{
|
||||
InputParameter = Enum.Parse<Controller.InputTypes>( inputParam, true ),
|
||||
InputRange = new ValueRange( inputMin, inputMax ),
|
||||
OutputParameter = Enum.Parse<Controller.OutputTypes>( outputParam, true ),
|
||||
OutputRange = new ValueRange( outputMin, outputMax )
|
||||
};
|
||||
}
|
||||
|
||||
public override void Write( Utf8JsonWriter writer, Controller value, JsonSerializerOptions options )
|
||||
{
|
||||
writer.WriteStartArray();
|
||||
writer.WriteStringValue( value.InputParameter.ToString() );
|
||||
writer.WriteNumberValue( value.InputRange.Min );
|
||||
writer.WriteNumberValue( value.InputRange.Max );
|
||||
writer.WriteStringValue( value.OutputParameter.ToString() );
|
||||
writer.WriteNumberValue( value.OutputRange.Min );
|
||||
writer.WriteNumberValue( value.OutputRange.Max );
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
}
|
||||
35
Code/Utils/EngineStream/EngineStream.cs
Normal file
35
Code/Utils/EngineStream/EngineStream.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Sandbox;
|
||||
using System.Collections.Generic;
|
||||
using VeloX.Audio;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
[GameResource( "Engine Stream", "engstr", "Engine Sound", Category = "VeloX", Icon = "time_to_leave" )]
|
||||
public sealed class EngineStream : GameResource
|
||||
{
|
||||
public sealed class Layer
|
||||
{
|
||||
[Property,
|
||||
Title( "Sound File" ),
|
||||
Description( "Sound asset for this layer" )]
|
||||
public SoundFile AudioPath { get; set; }
|
||||
|
||||
[Property, Title( "Use Redline Effect" ),
|
||||
Description( "Enable RPM-based vibration effect" )]
|
||||
public bool UseRedline { get; set; } = true;
|
||||
|
||||
[Property, Title( "Controllers" ),
|
||||
Description( "Audio parameter controllers" )]
|
||||
public List<Controller> Controllers { get; set; }
|
||||
[Property, Title( "Is Muted" )]
|
||||
public bool IsMuted { get; set; }
|
||||
}
|
||||
|
||||
[Property, Title( "Stream Parameters" ),
|
||||
Description( "Global engine sound parameters" )]
|
||||
public StreamParameters Parameters { get; set; }
|
||||
|
||||
[Property, Title( "Sound Layers" ),
|
||||
Description( "Individual sound layers" )]
|
||||
public Dictionary<string, Layer> Layers { get; set; }
|
||||
}
|
||||
119
Code/Utils/EngineStream/EngineStreamPlayer.cs
Normal file
119
Code/Utils/EngineStream/EngineStreamPlayer.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using Sandbox;
|
||||
using Sandbox.Audio;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using VeloX.Audio;
|
||||
using static VeloX.EngineStream;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
public class EngineStreamPlayer( EngineStream stream ) : IDisposable
|
||||
{
|
||||
|
||||
private static readonly Mixer EngineMixer = Mixer.FindMixerByName( "Car Engine" );
|
||||
public EngineStream Stream { get; set; } = stream;
|
||||
|
||||
public EngineState EngineState { get; set; }
|
||||
public bool EngineSoundPaused => EngineState != EngineState.Running;
|
||||
|
||||
public float Throttle { get; set; }
|
||||
public bool IsRedlining { get; set; }
|
||||
public float RPMPercent { get; set; }
|
||||
|
||||
private float _wobbleTime;
|
||||
|
||||
public readonly Dictionary<Layer, SoundHandle> EngineSounds = [];
|
||||
|
||||
public void Update( float deltaTime, Vector3 position, bool isLocal = false )
|
||||
{
|
||||
var globalPitch = 1.0f;
|
||||
|
||||
// Gear wobble effect
|
||||
if ( _wobbleTime > 0 )
|
||||
{
|
||||
_wobbleTime -= deltaTime * (0.1f + Throttle);
|
||||
globalPitch += MathF.Cos( _wobbleTime * Stream.Parameters.WobbleFrequency ) * _wobbleTime * (1 - _wobbleTime) * Stream.Parameters.WobbleStrength;
|
||||
}
|
||||
|
||||
globalPitch *= Stream.Parameters.Pitch;
|
||||
|
||||
// Redline effect
|
||||
var redlineVolume = 1.0f;
|
||||
if ( IsRedlining )
|
||||
{
|
||||
redlineVolume = 1 - Stream.Parameters.RedlineStrength +
|
||||
MathF.Cos( RealTime.Now * Stream.Parameters.RedlineFrequency ) *
|
||||
Stream.Parameters.RedlineStrength;
|
||||
|
||||
}
|
||||
// Process layers
|
||||
foreach ( var (id, layer) in Stream.Layers )
|
||||
{
|
||||
EngineSounds.TryGetValue( layer, out var channel );
|
||||
|
||||
if ( !channel.IsValid() )
|
||||
{
|
||||
channel = Sound.PlayFile( layer.AudioPath );
|
||||
EngineSounds[layer] = channel;
|
||||
}
|
||||
|
||||
if ( channel.Paused && (EngineSoundPaused || layer.IsMuted) )
|
||||
continue;
|
||||
|
||||
// Reset controller outputs
|
||||
float layerVolume = 1.0f;
|
||||
float layerPitch = 1.0f;
|
||||
|
||||
// Apply all controllers
|
||||
foreach ( var controller in layer.Controllers )
|
||||
{
|
||||
var inputValue = controller.InputParameter switch
|
||||
{
|
||||
Controller.InputTypes.Throttle => Throttle,
|
||||
Controller.InputTypes.RpmFraction => RPMPercent,
|
||||
_ => 0.0f
|
||||
};
|
||||
|
||||
var normalized = Math.Clamp( inputValue, controller.InputRange.Min, controller.InputRange.Max );
|
||||
var outputValue = controller.InputRange.Remap(
|
||||
normalized,
|
||||
controller.OutputRange.Min,
|
||||
controller.OutputRange.Max
|
||||
);
|
||||
|
||||
// Apply to correct parameter
|
||||
switch ( controller.OutputParameter )
|
||||
{
|
||||
case Controller.OutputTypes.Volume:
|
||||
layerVolume *= outputValue;
|
||||
break;
|
||||
case Controller.OutputTypes.Pitch:
|
||||
layerPitch *= outputValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply redline effect if needed
|
||||
layerVolume *= layer.UseRedline ? redlineVolume : 1.0f;
|
||||
layerPitch *= globalPitch;
|
||||
// Update audio channel
|
||||
channel.Pitch = layerPitch;
|
||||
channel.Volume = layerVolume * Stream.Parameters.Volume;
|
||||
channel.Position = position;
|
||||
channel.ListenLocal = isLocal;
|
||||
channel.Paused = EngineSoundPaused || layer.IsMuted;
|
||||
channel.TargetMixer = EngineMixer;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach ( var item in EngineSounds )
|
||||
{
|
||||
item.Value?.Stop();
|
||||
item.Value?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Code/Utils/EngineStream/StreamParameters.cs
Normal file
24
Code/Utils/EngineStream/StreamParameters.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Sandbox;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
public sealed class StreamParameters
|
||||
{
|
||||
[Property, Range( 0.1f, 2.0f ), Description( "Base pitch multiplier" )]
|
||||
public float Pitch { get; set; } = 1.0f;
|
||||
|
||||
[Property, Range( 0.0f, 2.0f ), Description( "Base volume level" )]
|
||||
public float Volume { get; set; } = 1.0f;
|
||||
|
||||
[Property, Range( 1f, 100f ), Description( "Vibration frequency at max RPM" )]
|
||||
public float RedlineFrequency { get; set; } = 55.0f;
|
||||
|
||||
[Property, Range( 0.0f, 1.0f ), Description( "Vibration intensity at max RPM" )]
|
||||
public float RedlineStrength { get; set; } = 0.2f;
|
||||
|
||||
[Property, Range( 1f, 50f ), Description( "Idle vibration frequency" )]
|
||||
public float WobbleFrequency { get; set; } = 25.0f;
|
||||
|
||||
[Property, Range( 0.0f, 0.5f ), Description( "Idle vibration intensity" )]
|
||||
public float WobbleStrength { get; set; } = 0.13f;
|
||||
}
|
||||
78
Code/Utils/PhysicsExtensions.cs
Normal file
78
Code/Utils/PhysicsExtensions.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
|
||||
using Sandbox;
|
||||
|
||||
namespace VeloX;
|
||||
public static class PhysicsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates the linear and angular velocities on the center of mass for an offset impulse.
|
||||
/// </summary>
|
||||
/// <param name="physObj">The physics object</param>
|
||||
/// <param name="impulse">The impulse acting on the object in kg*units/s (World frame)</param>
|
||||
/// <param name="position">The location of the impulse in world coordinates</param>
|
||||
/// <returns>
|
||||
/// Vector1: Linear velocity from the impulse (World frame)
|
||||
/// Vector2: Angular velocity from the impulse (Local frame)
|
||||
/// </returns>
|
||||
public static (Vector3 LinearVelocity, Vector3 AngularVelocity) CalculateVelocityOffset( this PhysicsBody physObj, Vector3 impulse, Vector3 position )
|
||||
{
|
||||
if ( !physObj.IsValid() || !physObj.MotionEnabled )
|
||||
return (Vector3.Zero, Vector3.Zero);
|
||||
|
||||
|
||||
Vector3 linearVelocity = impulse / physObj.Mass;
|
||||
|
||||
Vector3 centerOfMass = physObj.MassCenter;
|
||||
Vector3 relativePosition = position - centerOfMass;
|
||||
Vector3 torque = relativePosition.Cross( impulse );
|
||||
|
||||
Rotation bodyRotation = physObj.Transform.Rotation;
|
||||
Vector3 localTorque = bodyRotation.Inverse * torque;
|
||||
|
||||
Vector3 localInverseInertia = physObj.Inertia.Inverse;
|
||||
|
||||
Vector3 localAngularVelocity = new(
|
||||
localTorque.x * localInverseInertia.x,
|
||||
localTorque.y * localInverseInertia.y,
|
||||
localTorque.z * localInverseInertia.z );
|
||||
|
||||
return (linearVelocity, localAngularVelocity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the linear and angular impulses on the object's center of mass for an offset impulse.
|
||||
/// </summary>
|
||||
/// <param name="physObj">The physics object</param>
|
||||
/// <param name="impulse">The impulse acting on the object in kg*units/s (World frame)</param>
|
||||
/// <param name="position">The location of the impulse in world coordinates</param>
|
||||
/// <returns>
|
||||
/// Vector1: Linear impulse on center of mass (World frame)
|
||||
/// Vector2: Angular impulse on center of mass (Local frame)
|
||||
/// </returns>
|
||||
public static (Vector3 LinearImpulse, Vector3 AngularImpulse) CalculateForceOffset(
|
||||
this PhysicsBody physObj,
|
||||
Vector3 impulse,
|
||||
Vector3 position )
|
||||
{
|
||||
if ( !physObj.IsValid() || !physObj.MotionEnabled )
|
||||
{
|
||||
return (Vector3.Zero, Vector3.Zero);
|
||||
}
|
||||
|
||||
// 1. Linear impulse is the same as the input impulse (conservation of momentum)
|
||||
Vector3 linearImpulse = impulse;
|
||||
|
||||
// 2. Calculate angular impulse (torque) from the offset force
|
||||
// τ = r × F (cross product of position relative to COM and force)
|
||||
Vector3 centerOfMass = physObj.MassCenter;
|
||||
Vector3 relativePosition = position - centerOfMass;
|
||||
Vector3 worldAngularImpulse = relativePosition.Cross( impulse );
|
||||
|
||||
// Convert angular impulse to local space (since we'll use it with LocalInertia)
|
||||
Rotation bodyRotation = physObj.Transform.Rotation;
|
||||
Vector3 localAngularImpulse = bodyRotation.Inverse * worldAngularImpulse;
|
||||
|
||||
return (linearImpulse, localAngularImpulse);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user