diff --git a/Runtime/Actions/Node3D/CopyPosition.cs b/Runtime/Actions/Node3D/CopyPosition.cs index c33b49c..af6757c 100644 --- a/Runtime/Actions/Node3D/CopyPosition.cs +++ b/Runtime/Actions/Node3D/CopyPosition.cs @@ -19,7 +19,6 @@ namespace Rokojori _OnTrigger(); } - protected override void _OnTrigger() { if ( source == null || target == null ) diff --git a/Runtime/Animation/Follow.cs b/Runtime/Animation/Follow.cs new file mode 100644 index 0000000..cf03b6a --- /dev/null +++ b/Runtime/Animation/Follow.cs @@ -0,0 +1,67 @@ +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Text; +using Godot; + +namespace Rokojori +{ + [Tool] + [GlobalClass] + public partial class Follow:Node + { + [Export] + public Node3D source; + + [Export] + public Node3D target; + + [Export] + public Smoothing positionSmoothing; + + [Export] + public TimeLine timeline; + + float _lastCoefficient = 0; + int _lastFrames = -1; + + public override void _Process( double delta ) + { + if ( positionSmoothing != null && positionSmoothing is ExpSmoothing exp ) + { + if ( _lastCoefficient != exp.coefficient ) + { + _lastCoefficient = exp.coefficient; + + var duration = exp.ComputeDuration(); + + var framesOn60hz = duration / ( 1f / 60f ); + + this.LogInfo( "Duration:", duration._FFF(), "seconds or", framesOn60hz._FF(), "frames", "For coefficient:", _lastCoefficient ); + } + } + + if ( positionSmoothing != null && positionSmoothing is FrameSmoothing fra ) + { + if ( _lastFrames != fra.frames ) + { + _lastFrames = fra.frames; + + var coefficient = fra.GetCoefficientForFrames( fra.frames ); + + var framesOn60hz = fra.frames * ( 1f / 60f ); + + this.LogInfo( "Frames:", fra.frames, "Seconds", framesOn60hz._FF(), "Coefficient:", coefficient ); + } + } + + if ( source == null || target == null || Engine.IsEditorHint() ) + { + return; + } + + var tl = TimeLine.IfNull_ReplaceByGameTime( timeline ); + target.GlobalPosition = Smoothing.Apply( positionSmoothing, source.GlobalPosition, tl.delta ); + } + } +} \ No newline at end of file diff --git a/Runtime/Animation/Smoothing/ExpSmoothing.cs b/Runtime/Animation/Smoothing/ExpSmoothing.cs new file mode 100644 index 0000000..709fab5 --- /dev/null +++ b/Runtime/Animation/Smoothing/ExpSmoothing.cs @@ -0,0 +1,26 @@ +using System.Collections; +using System.Collections.Generic; +using Godot; + +namespace Rokojori +{ + [Tool] + [GlobalClass] + public partial class ExpSmoothing: Smoothing + { + [Export( PropertyHint.Range, "0,200")] + public float coefficient = 1; + + protected override float _ComputeInterpolationAmount( float delta ) + { + return 1f - Mathf.Exp( -coefficient * delta ); + } + + public static float GetDurationForCoefficient( float coefficient ) + { + var exp = new ExpSmoothing(); + exp.coefficient = coefficient; + return exp.ComputeDuration(); + } + } +} \ No newline at end of file diff --git a/Runtime/Animation/Smoothing/FrameSmoothing.cs b/Runtime/Animation/Smoothing/FrameSmoothing.cs new file mode 100644 index 0000000..5c1c636 --- /dev/null +++ b/Runtime/Animation/Smoothing/FrameSmoothing.cs @@ -0,0 +1,66 @@ +using System.Collections; +using System.Collections.Generic; +using Godot; + +namespace Rokojori +{ + [Tool] + [GlobalClass] + public partial class FrameSmoothing: Smoothing + { + [Export( PropertyHint.Range, "0,600")] + public int frames = 10; + + static Dictionary _framesToCoefficient = new Dictionary(); + + protected override float _ComputeInterpolationAmount( float delta ) + { + if ( frames <= 0 ) + { + return 1; + } + + var coefficient = GetCoefficientForFrames( frames ); + return 1f - Mathf.Exp( -coefficient * delta ); + } + + public float GetCoefficientForFrames( int frames ) + { + if ( ! _framesToCoefficient.ContainsKey( frames ) ) + { + _framesToCoefficient[ frames ] = ComputeCoefficientForFrames( frames ); + } + + return _framesToCoefficient[ frames ]; + } + + static float ComputeCoefficientForFrames( int numFrames, float treshold = 1f/60f * 0.1f ) + { + + var framesDuration = numFrames * 1/60f; + + + var minMaxSearch = new MinMaxSearch( + ( a, b, t ) => Mathf.Lerp( a, b, t ), + ( c ) => ExpSmoothing.GetDurationForCoefficient( c ) + ); + + var lowDurationCoefficient = 100000f; + var highDurationCoefficient = 0.1f; + + + RJLog.Log( "Finding coefficient for frames", numFrames, ">>", framesDuration, "s" ); + + var coefficient = minMaxSearch.Find( framesDuration, lowDurationCoefficient, highDurationCoefficient, treshold ); + + RJLog.Log( "Found coefficient:", coefficient, ">> duration", ExpSmoothing.GetDurationForCoefficient( coefficient )._FF(), "s" ); + + return coefficient; + + } + + + + + } +} \ No newline at end of file diff --git a/Runtime/Animation/Smoothing/Smoothing.cs b/Runtime/Animation/Smoothing/Smoothing.cs new file mode 100644 index 0000000..36f02b8 --- /dev/null +++ b/Runtime/Animation/Smoothing/Smoothing.cs @@ -0,0 +1,129 @@ +using System.Collections; +using System.Collections.Generic; +using Godot; + +namespace Rokojori +{ + [Tool] + [GlobalClass] + public partial class Smoothing: Resource + { + float _currentFloat = 0; + Vector2 _currentVector2 = Vector2.Zero; + Vector3 _currentVector3 = Vector3.Zero; + Vector4 _currentVector4 = Vector4.Zero; + Quaternion _currentQuaternion = Quaternion.Identity; + Color _currentColor = Colors.Black; + + + public float Smooth( float nextValue, float delta ) + { + _currentFloat = Mathf.Lerp( _currentFloat, nextValue, _ComputeInterpolationAmount( delta ) ); + + return _currentFloat; + } + + public static float Apply( Smoothing sm, float value, float delta ) + { + if ( sm == null ){ return value; } + return sm.Smooth( value, delta ); + } + + public Vector2 Smooth( Vector2 nextValue, float delta ) + { + _currentVector2 = _currentVector2.Lerp( nextValue, _ComputeInterpolationAmount( delta ) ); + + return _currentVector2; + } + + public static Vector2 Apply( Smoothing sm, Vector2 value, float delta ) + { + if ( sm == null ){ return value; } + return sm.Smooth( value, delta ); + } + + public Vector3 Smooth( Vector3 nextValue, float delta ) + { + _currentVector3 = _currentVector3.Lerp( nextValue, _ComputeInterpolationAmount( delta ) ); + + return _currentVector3; + } + + public static Vector3 Apply( Smoothing sm, Vector3 value, float delta ) + { + if ( sm == null ){ return value; } + return sm.Smooth( value, delta ); + } + + public Vector4 Smooth( Vector4 nextValue, float delta ) + { + _currentVector4 = _currentVector4.Lerp( nextValue, _ComputeInterpolationAmount( delta ) ); + + return _currentVector4; + } + + public static Vector4 Apply( Smoothing sm, Vector4 value, float delta ) + { + if ( sm == null ){ return value; } + return sm.Smooth( value, delta ); + } + + public Quaternion Smooth( Quaternion nextValue, float delta ) + { + _currentQuaternion = _currentQuaternion.Slerp( nextValue, _ComputeInterpolationAmount( delta ) ); + + return _currentQuaternion; + } + + public static Quaternion Apply( Smoothing sm, Quaternion value, float delta ) + { + if ( sm == null ){ return value; } + return sm.Smooth( value, delta ); + } + + public Color Smooth( Color nextValue, float delta ) + { + _currentColor = _currentColor.Lerp( nextValue, _ComputeInterpolationAmount( delta ) ); + + return _currentColor; + } + + public static Color Apply( Smoothing sm, Color value, float delta ) + { + if ( sm == null ){ return value; } + return sm.Smooth( value, delta ); + } + + protected virtual float _ComputeInterpolationAmount( float delta ) + { + return 0; + } + + public virtual float ComputeDuration( float delta = 1f/240f, float tresholdValue = 0.05f ) + { + var cached = _currentFloat; + + var value = 1f; + _currentFloat = 1f; + + var duration = 0f; + var lastValue = value; + + var maxDuration = 30; + + while ( value > tresholdValue && duration < maxDuration ) + { + lastValue = value; + value = Smooth( 0, delta ); + + duration += delta; + + } + + _currentFloat = cached; + + return MathX.RemapClamped( tresholdValue, lastValue, value, duration - delta, duration ); + + } + } +} \ No newline at end of file diff --git a/Runtime/Procedural/Parametric/Deformer/Deformer.cs b/Runtime/Procedural/Parametric/Deformer/Deformer.cs index c68dd7f..00c194f 100644 --- a/Runtime/Procedural/Parametric/Deformer/Deformer.cs +++ b/Runtime/Procedural/Parametric/Deformer/Deformer.cs @@ -17,6 +17,7 @@ namespace Rokojori public class MappingData { public Vector3 localPosition; + public Vector3 localNormal; public float normalizedSplineParameter; public float weight; } @@ -74,7 +75,7 @@ namespace Rokojori MappingData[] deformerMappings; MeshGeometry meshGeometry; - MappingData CreateSourceMapping( Spline s, Vector3 worldPosition ) + MappingData CreateSourceMapping( Spline s, Vector3 worldPosition, Vector3 worldNormal ) { var curve = s.GetCurve(); var closestParameter = curve.GetClosestParameterTo( worldPosition, splineMappingResolution, splineMappingDepth ); @@ -82,10 +83,12 @@ namespace Rokojori var pose = curve.GetPoseByPointIndex( pointIndex ); var localPosition = pose.ApplyInverse( worldPosition ); + var localNormal = pose.rotation.Inverse() * worldNormal; var mappingData = new MappingData(); mappingData.localPosition = localPosition; + mappingData.localNormal = localNormal; mappingData.normalizedSplineParameter = closestParameter; mappingData.weight = 0; @@ -112,7 +115,8 @@ namespace Rokojori for ( int j = 0; j < sourceSplines.Length; j++ ) { var vertex = meshGeometry.vertices[ i ]; - var mapping = CreateSourceMapping( sourceSplines[ j ], vertex ); + var normal = meshGeometry.normals[ i ]; + var mapping = CreateSourceMapping( sourceSplines[ j ], vertex, normal ); var curve = sourceSplines[ j ].GetCurve(); var distance = curve.PositionAt( mapping.normalizedSplineParameter ) - vertex; var inverseWeight = MathX.NormalizeClamped( distance.Length(), splineMinDistance, splineMaxDistance ); @@ -140,7 +144,8 @@ namespace Rokojori for ( int i = 0; i < cloned.vertices.Count; i++ ) { - var vertexPosition = Vector3.Zero; + var vertex = Vector3.Zero; + var normal = Vector3.Zero; for ( int j = 0; j < deformerSplines.Length; j++ ) { @@ -151,19 +156,22 @@ namespace Rokojori { var pose = curve.SmoothedPoseAt( mapping.normalizedSplineParameter, targetSmoothing * 0.5f, 2, targetSmoothing ); pose.ApplyTwist( curve.TwistAt( mapping.normalizedSplineParameter ) ); - vertexPosition += pose.Apply( mapping.localPosition ) * mapping.weight; + vertex += pose.Apply( mapping.localPosition ) * mapping.weight; + normal += pose.rotation * mapping.localNormal * mapping.weight; } else { var pose = curve.PoseAt( mapping.normalizedSplineParameter ); pose.ApplyTwist( curve.TwistAt( mapping.normalizedSplineParameter ) ); - vertexPosition += pose.Apply( mapping.localPosition ) * mapping.weight; + vertex += pose.Apply( mapping.localPosition ) * mapping.weight; + normal += pose.rotation * mapping.localNormal * mapping.weight; } } - cloned.vertices[ i ] = vertexPosition; + cloned.vertices[ i ] = vertex; + cloned.normals[ i ] = normal.Normalized(); } // RJLog.Log( cloned.vertices.Count ); diff --git a/Runtime/Shading/Meshes/SphericalParticleMesh.cs b/Runtime/Shading/Meshes/SphericalParticleMesh.cs new file mode 100644 index 0000000..c9ad265 --- /dev/null +++ b/Runtime/Shading/Meshes/SphericalParticleMesh.cs @@ -0,0 +1,13 @@ +using Godot; +using System.Reflection; +using System.Collections.Generic; + +namespace Rokojori +{ + [Tool] + [GlobalClass] + public partial class SphericalParticleMesh : Mesh + { + + } +} \ No newline at end of file diff --git a/Runtime/Sorting/MinMaxSearch.cs b/Runtime/Sorting/MinMaxSearch.cs new file mode 100644 index 0000000..308ce79 --- /dev/null +++ b/Runtime/Sorting/MinMaxSearch.cs @@ -0,0 +1,93 @@ +using System.Collections; +using System.Collections.Generic; +using System; +using System.Reflection; +using System.Text.RegularExpressions; +using Godot; + +namespace Rokojori +{ + public class MinMaxSearch + { + Func lerp; + Func getValue; + Func validateLimits; + + public bool cacheValues = true; + public float interpolationAmount = 1f; + + Dictionary _cachedValues = new Dictionary(); + + public MinMaxSearch( Func lerp, Func getValue, Func validateLimits = null) + { + this.lerp = lerp; + this.getValue = getValue; + this.validateLimits = validateLimits; + } + + public T Find( float searchValue, T low, T high, float treshold ) + { + if ( validateLimits != null && ! validateLimits( low, high ) ) + { + throw new Exception( "Limit validation failed" ); + } + + var tweened = GetTweened( searchValue, low, high ); + var tweenedValue = GetValue( tweened ); + var tweenedDifference = searchValue - tweenedValue; + + Safe.While ( () => Mathf.Abs( tweenedDifference ) > treshold , + ()=> + { + if ( searchValue > tweenedValue ) + { + low = tweened; + } + else + { + high = tweened; + } + + if ( validateLimits != null && ! validateLimits( low, high ) ) + { + throw new Exception( "Limit validation failed" ); + } + + tweened = GetTweened( searchValue, low, high ); + tweenedValue = GetValue( tweened ); + tweenedDifference = searchValue - tweenedValue; + } + ); + + return tweened; + } + + public T GetTweened( float value, T low, T high ) + { + var lowValue = getValue( low ); + var highValue = getValue( high ); + + var position = MathX.Normalize( value, lowValue, highValue ); + position = Mathf.Lerp( 0.5f, position, interpolationAmount ); + + return lerp( low, high, position ); + } + + float GetValue( T t ) + { + if ( ! cacheValues ) + { + return getValue( t ); + } + + if ( _cachedValues.ContainsKey( t ) ) + { + return _cachedValues[ t ]; + } + + _cachedValues[ t ] = getValue( t ); + + return _cachedValues[ t ]; + } + } +} \ No newline at end of file diff --git a/Runtime/Text/RegexUtility.cs b/Runtime/Text/RegexUtility.cs index d31ad45..a8ffa34 100644 --- a/Runtime/Text/RegexUtility.cs +++ b/Runtime/Text/RegexUtility.cs @@ -9,7 +9,7 @@ using Godot; namespace Rokojori { - public class RegexUtility + public static class RegexUtility { public static Regex MakeSticky( Regex regex ) { @@ -61,6 +61,23 @@ namespace Rokojori return "0"; } + public static string _F( this float value ) + { + return value.ToString( "0.0", CultureInfo.InvariantCulture ); + } + + public static string _FF( this float value ) + { + return value.ToString( "0.00", CultureInfo.InvariantCulture ); + } + + public static string _FFF( this float value ) + { + return value.ToString( "0.000", CultureInfo.InvariantCulture ); + } + + + public static double ParseDouble( string source, double alternative = 0 ) { try diff --git a/Runtime/Time/TimeLine.cs b/Runtime/Time/TimeLine.cs index 457acbe..c904194 100644 --- a/Runtime/Time/TimeLine.cs +++ b/Runtime/Time/TimeLine.cs @@ -37,5 +37,28 @@ namespace Rokojori } } + public static TimeLine IfNull_ReplaceByGameTime( TimeLine other ) + { + if ( other != null ) + { + return other; + } + + var tm = Unique.Get(); + return tm.gameTimeTimeLine; + } + + public static TimeLine IfNull_ReplaceByRealTime( TimeLine other ) + { + if ( other != null ) + { + return other; + } + + var tm = Unique.Get(); + return tm.realTimeTimeLine; + } + + } } \ No newline at end of file diff --git a/Runtime/Time/TimeLineManager.cs b/Runtime/Time/TimeLineManager.cs index a2354d5..9e324ac 100644 --- a/Runtime/Time/TimeLineManager.cs +++ b/Runtime/Time/TimeLineManager.cs @@ -16,7 +16,7 @@ namespace Rokojori public TimeLine[] timeLines; [Export] - public TimeLine engineTimeScale; + public TimeLine gameTimeTimeLine; [Export] public TimeLine realTimeTimeLine; @@ -52,9 +52,9 @@ namespace Rokojori { UpdateRealTime( delta ); - if ( engineTimeScale != null ) + if ( gameTimeTimeLine != null ) { - Engine.TimeScale = GetModulatedSpeed( engineTimeScale ); + Engine.TimeScale = GetModulatedSpeed( gameTimeTimeLine ); } _runners.ForEach( r => r.UpdateTimeLine( unscaledTimeDelta, this ) ); diff --git a/Runtime/Tools/Safe.cs b/Runtime/Tools/Safe.cs index 1d2080f..dbb85b6 100644 --- a/Runtime/Tools/Safe.cs +++ b/Runtime/Tools/Safe.cs @@ -12,11 +12,16 @@ namespace Rokojori public static void While( Func condition, System.Action action, System.Action onTooManyIterations = null ) { + if ( condition == null || action == null ) + { + return; + } + var it = 0; while ( it < Safe.maxWhileIterations && condition() ) { - action(); + action(); it++; }