diff --git a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs index aba4d0ff63..46c8e7c02a 100644 --- a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs +++ b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -22,8 +23,6 @@ namespace osu.Game.Rulesets.Osu.Skinning { public abstract class SmokeSegment : Drawable, ITexturedShaderDrawable { - private const int max_point_count = 18_000; - // fade anim values private const double initial_fade_out_duration = 4000; @@ -85,12 +84,6 @@ protected override void LoadComplete() totalDistance = pointInterval; } - private Vector2 nextPointDirection() - { - float angle = RNG.NextSingle(0, 2 * MathF.PI); - return new Vector2(MathF.Sin(angle), -MathF.Cos(angle)); - } - public void AddPosition(Vector2 position, double time) { lastPosition ??= position; @@ -107,33 +100,27 @@ public void AddPosition(Vector2 position, double time) Vector2 pointPos = (pointInterval - (totalDistance - delta)) * increment + (Vector2)lastPosition; increment *= pointInterval; - if (SmokePoints.Count > 0 && SmokePoints[^1].Time > time) - { - int index = ~SmokePoints.BinarySearch(new SmokePoint { Time = time }, new SmokePoint.UpperBoundComparer()); - SmokePoints.RemoveRange(index, SmokePoints.Count - index); - } - totalDistance %= pointInterval; - for (int i = 0; i < count; i++) + if (SmokePoints.Count == 0 || SmokePoints[^1].Time <= time) { - SmokePoints.Add(new SmokePoint + for (int i = 0; i < count; i++) { - Position = pointPos, - Time = time, - Direction = nextPointDirection(), - }); + SmokePoints.Add(new SmokePoint + { + Position = pointPos, + Time = time, + Angle = RNG.NextSingle(0, 2 * MathF.PI), + }); - pointPos += increment; + pointPos += increment; + } } Invalidate(Invalidation.DrawNode); } lastPosition = position; - - if (SmokePoints.Count >= max_point_count) - FinishDrawing(time); } public void FinishDrawing(double time) @@ -157,7 +144,7 @@ protected struct SmokePoint { public Vector2 Position; public double Time; - public Vector2 Direction; + public float Angle; public struct UpperBoundComparer : IComparer { @@ -171,6 +158,17 @@ public int Compare(SmokePoint x, SmokePoint target) return x.Time > target.Time ? 1 : -1; } } + + public struct LowerBoundComparer : IComparer + { + public int Compare(SmokePoint x, SmokePoint target) + { + // Similar logic as UpperBoundComparer, except returned index will always be + // the first element larger or equal + + return x.Time < target.Time ? -1 : 1; + } + } } protected class SmokeDrawNode : TexturedShaderDrawNode @@ -187,11 +185,11 @@ protected class SmokeDrawNode : TexturedShaderDrawNode private Vector2 drawSize; private Texture? texture; private int rotationSeed; - private int rotationIndex; + private int firstVisiblePointIndex; // anim calculation vars (color, scale, direction) private double initialFadeOutDurationTrunc; - private double firstVisiblePointTime; + private double firstVisiblePointTimeAfterSmokeEnded; private double initialFadeOutTime; private double reFadeInTime; @@ -206,9 +204,6 @@ public override void ApplyState() { base.ApplyState(); - points.Clear(); - points.AddRange(Source.SmokePoints); - radius = Source.radius; drawSize = Source.DrawSize; texture = Source.Texture; @@ -220,11 +215,18 @@ public override void ApplyState() rotationSeed = Source.rotationSeed; initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, SmokeEndTime - SmokeStartTime); - firstVisiblePointTime = SmokeEndTime - initialFadeOutDurationTrunc; + firstVisiblePointTimeAfterSmokeEnded = SmokeEndTime - initialFadeOutDurationTrunc; - initialFadeOutTime = CurrentTime; - reFadeInTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / re_fade_in_speed); - finalFadeOutTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / final_fade_out_speed); + initialFadeOutTime = Math.Min(CurrentTime, SmokeEndTime); + reFadeInTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTimeAfterSmokeEnded * (1 - 1 / re_fade_in_speed); + finalFadeOutTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTimeAfterSmokeEnded * (1 - 1 / final_fade_out_speed); + + double firstVisiblePointTime = Math.Min(SmokeEndTime, CurrentTime) - initialFadeOutDurationTrunc; + firstVisiblePointIndex = ~Source.SmokePoints.BinarySearch(new SmokePoint { Time = firstVisiblePointTime }, new SmokePoint.LowerBoundComparer()); + int futurePointIndex = ~Source.SmokePoints.BinarySearch(new SmokePoint { Time = CurrentTime }, new SmokePoint.UpperBoundComparer()); + + points.Clear(); + points.AddRange(Source.SmokePoints.Skip(firstVisiblePointIndex).Take(futurePointIndex - firstVisiblePointIndex)); } public sealed override void Draw(IRenderer renderer) @@ -234,9 +236,14 @@ public sealed override void Draw(IRenderer renderer) if (points.Count == 0) return; - rotationIndex = 0; + quadBatch ??= renderer.CreateQuadBatch(200, 4); + + if (points.Count > quadBatch.Size && quadBatch.Size != IRenderer.MAX_QUADS) + { + int batchSize = Math.Min(quadBatch.Size * 2, IRenderer.MAX_QUADS); + quadBatch = renderer.CreateQuadBatch(batchSize, 4); + } - quadBatch ??= renderer.CreateQuadBatch(max_point_count / 10, 10); texture ??= renderer.WhitePixel; RectangleF textureRect = texture.GetTextureRect(); @@ -248,8 +255,8 @@ public sealed override void Draw(IRenderer renderer) shader.Bind(); texture.Bind(); - foreach (var point in points) - drawPointQuad(point, textureRect); + for (int i = 0; i < points.Count; i++) + drawPointQuad(points[i], textureRect, i + firstVisiblePointIndex); shader.Unbind(); renderer.PopLocalMatrix(); @@ -263,30 +270,34 @@ protected virtual Color4 PointColour(SmokePoint point) { var color = Color4.White; - double timeDoingInitialFadeOut = Math.Min(initialFadeOutTime, SmokeEndTime) - point.Time; + double timeDoingFinalFadeOut = finalFadeOutTime - point.Time / final_fade_out_speed; - if (timeDoingInitialFadeOut > 0) + if (timeDoingFinalFadeOut > 0 && point.Time >= firstVisiblePointTimeAfterSmokeEnded) { - float fraction = Math.Clamp((float)(timeDoingInitialFadeOut / initial_fade_out_duration), 0, 1); - color.A = (1 - fraction) * initial_alpha; + float fraction = Math.Clamp((float)(timeDoingFinalFadeOut / final_fade_out_duration), 0, 1); + fraction = MathF.Pow(fraction, 5); + color.A = (1 - fraction) * re_fade_in_alpha; } - - if (color.A > 0) + else { - double timeDoingReFadeIn = reFadeInTime - point.Time / re_fade_in_speed; - double timeDoingFinalFadeOut = finalFadeOutTime - point.Time / final_fade_out_speed; + double timeDoingInitialFadeOut = initialFadeOutTime - point.Time; - if (timeDoingFinalFadeOut > 0) + if (timeDoingInitialFadeOut > 0) { - float fraction = Math.Clamp((float)(timeDoingFinalFadeOut / final_fade_out_duration), 0, 1); - fraction = MathF.Pow(fraction, 5); - color.A = (1 - fraction) * re_fade_in_alpha; + float fraction = Math.Clamp((float)(timeDoingInitialFadeOut / initial_fade_out_duration), 0, 1); + color.A = (1 - fraction) * initial_alpha; } - else if (timeDoingReFadeIn > 0) + + if (point.Time > firstVisiblePointTimeAfterSmokeEnded) { - float fraction = Math.Clamp((float)(timeDoingReFadeIn / re_fade_in_duration), 0, 1); - fraction = 1 - MathF.Pow(1 - fraction, 5); - color.A = fraction * (re_fade_in_alpha - color.A) + color.A; + double timeDoingReFadeIn = reFadeInTime - point.Time / re_fade_in_speed; + + if (timeDoingReFadeIn > 0) + { + float fraction = Math.Clamp((float)(timeDoingReFadeIn / re_fade_in_duration), 0, 1); + fraction = 1 - MathF.Pow(1 - fraction, 5); + color.A = fraction * (re_fade_in_alpha - color.A) + color.A; + } } } @@ -301,33 +312,33 @@ protected virtual float PointScale(SmokePoint point) return fraction * (final_scale - initial_scale) + initial_scale; } - protected virtual Vector2 PointDirection(SmokePoint point) + protected virtual Vector2 PointDirection(SmokePoint point, int index) { - float initialAngle = MathF.Atan2(point.Direction.Y, point.Direction.X); - float finalAngle = initialAngle + nextRotation(); - double timeDoingRotation = CurrentTime - point.Time; float fraction = Math.Clamp((float)(timeDoingRotation / rotation_duration), 0, 1); fraction = 1 - MathF.Pow(1 - fraction, 5); - float angle = fraction * (finalAngle - initialAngle) + initialAngle; + float angle = fraction * getRotation(index) + point.Angle; return new Vector2(MathF.Sin(angle), -MathF.Cos(angle)); } - private float nextRotation() => max_rotation * (StatelessRNG.NextSingle(rotationSeed, rotationIndex++) * 2 - 1); + private float getRotation(int index) => max_rotation * (StatelessRNG.NextSingle(rotationSeed, index) * 2 - 1); - private void drawPointQuad(SmokePoint point, RectangleF textureRect) + private void drawPointQuad(SmokePoint point, RectangleF textureRect, int index) { Debug.Assert(quadBatch != null); var colour = PointColour(point); - float scale = PointScale(point); - var dir = PointDirection(point); - var ortho = dir.PerpendicularLeft; - - if (colour.A == 0 || scale == 0) + if (colour.A == 0) return; + float scale = PointScale(point); + if (scale == 0) + return; + + var dir = PointDirection(point, index); + var ortho = dir.PerpendicularLeft; + var localTopLeft = point.Position + (radius * scale * (-ortho - dir)); var localTopRight = point.Position + (radius * scale * (-ortho + dir)); var localBotLeft = point.Position + (radius * scale * (ortho - dir));