1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 20:07:25 +08:00

Full legacy smoke implementation and temp default smoke

This commit is contained in:
Alden Wu 2022-09-18 12:10:01 -07:00
parent 493efd84a3
commit 613564b5b9
4 changed files with 521 additions and 67 deletions

View File

@ -1,14 +1,61 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Rendering;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public class DefaultSmoke : Smoke
{
private const double fade_out_delay = 8000;
private const double fade_out_speed = 3;
private const double fade_out_duration = 50;
private const float alpha = 0.5f;
protected override double LifetimeAfterSmokeEnd => fade_out_delay + fade_out_duration + (SmokeEndTime - SmokeStartTime) / fade_out_speed;
public DefaultSmoke()
{
Texture = null;
PathRadius = 2;
Radius = 2;
}
protected override DrawNode CreateDrawNode() => new DefaultSmokeDrawNode(this);
private class DefaultSmokeDrawNode : SmokeDrawNode
{
private double fadeOutTime;
public DefaultSmokeDrawNode(ITexturedShaderDrawable source)
: base(source)
{
}
protected override void UpdateDrawVariables(IRenderer renderer)
{
base.UpdateDrawVariables(renderer);
fadeOutTime = SmokeStartTime + fade_out_speed * (CurrentTime - (SmokeEndTime + fade_out_delay));
}
protected override Color4 ColorAtTime(double pointTime)
{
var color = Color4.White;
color.A = alpha;
double timeDoingFadeOut = fadeOutTime - pointTime;
if (timeDoingFadeOut > 0)
{
float fraction = Math.Clamp((float)(1 - (timeDoingFadeOut / fade_out_duration)), 0, 1);
fraction = MathF.Pow(fraction, 5);
color.A *= fraction;
}
return color;
}
}
}
}

View File

@ -1,19 +1,42 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Rendering;
using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
public class LegacySmoke : Smoke
{
private const double initial_fade_out_duration = 2500;
private const double re_fade_in_speed = 3;
private const double re_fade_in_duration = 50;
private const double final_fade_out_duration = 7500;
private const float initial_alpha = 0.8f;
private const float re_fade_in_alpha = 1.4f;
protected override double LifetimeAfterSmokeEnd
{
get
{
double initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, SmokeEndTime - SmokeStartTime);
return final_fade_out_duration + initialFadeOutDurationTrunc * (1 + re_fade_in_speed);
}
}
private ISkin skin;
public LegacySmoke(ISkin skin)
{
this.skin = skin;
PathRadius = 8;
Radius = 3;
}
protected override void LoadComplete()
@ -22,5 +45,71 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Texture = skin.GetTexture("cursor-smoke");
}
protected override DrawNode CreateDrawNode() => new LegacySmokeDrawNode(this);
protected class LegacySmokeDrawNode : SmokeDrawNode
{
private double initialFadeOutDurationTrunc;
private double initialFadeOutTime;
private double reFadeInTime;
private double finalFadeOutTime;
public LegacySmokeDrawNode(ITexturedShaderDrawable source)
: base(source)
{
}
public override void ApplyState()
{
base.ApplyState();
initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, SmokeEndTime - SmokeStartTime);
}
protected override void UpdateDrawVariables(IRenderer renderer)
{
base.UpdateDrawVariables(renderer);
initialFadeOutTime = Math.Min(CurrentTime, SmokeEndTime);
reFadeInTime = re_fade_in_speed * (CurrentTime - SmokeEndTime) + SmokeEndTime - initialFadeOutDurationTrunc;
finalFadeOutTime = CurrentTime - initialFadeOutDurationTrunc * (1 + 1 / re_fade_in_speed);
}
protected override Color4 ColorAtTime(double pointTime)
{
var color = Color4.White;
double timeDoingInitialFadeOut = initialFadeOutTime - pointTime;
if (timeDoingInitialFadeOut > 0)
{
float fraction = Math.Clamp((float)(timeDoingInitialFadeOut / initial_fade_out_duration), 0, 1);
fraction = MathF.Pow(fraction, 5);
color.A = (1 - fraction) * initial_alpha;
}
if (color.A > 0)
{
double timeDoingReFadeIn = reFadeInTime - pointTime;
double timeDoingFinalFadeOut = finalFadeOutTime - pointTime;
if (timeDoingFinalFadeOut > 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;
}
else 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;
}
}
return color;
}
}
}
}

View File

@ -1,33 +1,175 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
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;
using osu.Framework.Graphics.Lines;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Textures;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning
{
public abstract class Smoke : TexturedPath
public abstract class Smoke : Drawable, ITexturedShaderDrawable
{
protected bool IsActive { get; private set; }
protected double SmokeTimeStart { get; private set; } = double.MinValue;
protected double SmokeTimeEnd { get; private set; } = double.MinValue;
public IShader? TextureShader { get; private set; }
public IShader? RoundedTextureShader { get; private set; }
protected readonly List<Vector2> SmokeVertexPositions = new List<Vector2>();
protected readonly List<double> SmokeVertexTimes = new List<double>();
private float radius = 1;
protected float Radius
{
get => radius;
set
{
if (radius == value)
return;
radius = value;
Invalidate(Invalidation.DrawNode);
}
}
private int rotationSeed = RNG.Next();
protected int RotationSeed
{
get => rotationSeed;
set
{
if (rotationSeed == value)
return;
rotationSeed = value;
Invalidate(Invalidation.DrawNode);
}
}
private Texture? texture;
protected Texture? Texture
{
get => texture;
set
{
texture = value;
Invalidate(Invalidation.DrawNode);
}
}
private double smokeTimeStart = double.MinValue;
protected double SmokeStartTime
{
get => smokeTimeStart;
private set
{
if (smokeTimeStart == value)
return;
smokeTimeStart = value;
Invalidate(Invalidation.DrawNode);
}
}
private double smokeTimeEnd = double.MaxValue;
protected double SmokeEndTime
{
get => smokeTimeEnd;
private set
{
if (smokeTimeEnd == value)
return;
smokeTimeEnd = value;
Invalidate(Invalidation.DrawNode);
}
}
public override IFrameBasedClock Clock
{
get => base.Clock;
set
{
base.Clock = value;
Invalidate(Invalidation.DrawNode);
}
}
private Vector2 topLeft;
protected Vector2 TopLeft
{
get => topLeft;
set
{
if (topLeft == value)
return;
topLeft = value;
Invalidate(Invalidation.All);
}
}
private Vector2 bottomRight;
protected Vector2 BottomRight
{
get => bottomRight;
set
{
if (bottomRight == value)
return;
bottomRight = value;
Invalidate(Invalidation.Layout);
}
}
protected abstract double LifetimeAfterSmokeEnd { get; }
protected virtual float PointInterval => Radius * 7f / 8;
protected bool IsActive { get; private set; }
protected readonly List<SmokePoint> SmokePoints = new List<SmokePoint>();
private float totalDistance;
private Vector2? lastPosition = null;
private const double max_duration = 60_000;
public override float Height
{
get => base.Height = BottomRight.Y - TopLeft.Y;
set => throw new InvalidOperationException($"Cannot manually set {nameof(Height)} of {nameof(Smoke)}.");
}
public override float Width
{
get => base.Width = BottomRight.X - TopLeft.X;
set => throw new InvalidOperationException($"Cannot manually set {nameof(Width)} of {nameof(Smoke)}.");
}
public override Vector2 Size
{
get => base.Size = BottomRight - TopLeft;
set => throw new InvalidOperationException($"Cannot manually set {nameof(Size)} of {nameof(Smoke)}.");
}
[Resolved(CanBeNull = true)]
private SmokeContainer? smokeContainer { get; set; }
protected struct SmokePoint
[BackgroundDependencyLoader]
private void load(ShaderManager shaders)
{
public Vector2 Position;
public double Time;
RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED);
TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
}
protected override void LoadComplete()
@ -36,72 +178,107 @@ namespace osu.Game.Rulesets.Osu.Skinning
if (smokeContainer != null)
{
smokeContainer.SmokeMoved += guardOnSmokeMoved;
smokeContainer.SmokeEnded += guardOnSmokeEnded;
smokeContainer.SmokeMoved += onSmokeMoved;
smokeContainer.SmokeEnded += onSmokeEnded;
IsActive = true;
}
Anchor = Anchor.TopLeft;
Origin = Anchor.TopLeft;
SmokeTimeStart = Time.Current;
SmokeStartTime = Time.Current;
}
private void guardOnSmokeMoved(Vector2 position, double time)
private void onSmokeMoved(Vector2 position, double time)
{
if (IsActive)
OnSmokeMoved(position, time);
}
if (!IsActive)
return;
private void guardOnSmokeEnded(double time)
{
if (IsActive)
OnSmokeEnded(time);
}
lastPosition ??= position;
protected virtual void OnSmokeMoved(Vector2 position, double time)
{
addSmokeVertex(position, time);
}
float delta = (position - (Vector2)lastPosition).LengthFast;
totalDistance += delta;
int count = (int)(totalDistance / PointInterval);
private void addSmokeVertex(Vector2 position, double time)
{
Debug.Assert(SmokeVertexTimes.Count == SmokeVertexPositions.Count);
if (SmokeVertexTimes.Count > 0 && SmokeVertexTimes.Last() > time)
if (count > 0)
{
int index = ~SmokeVertexTimes.BinarySearch(time, new UpperBoundComparer());
Vector2 increment = position - (Vector2)lastPosition;
increment.NormalizeFast();
SmokeVertexTimes.RemoveRange(index, SmokeVertexTimes.Count - index);
SmokeVertexPositions.RemoveRange(index, SmokeVertexPositions.Count - index);
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);
recalculateBounds();
}
totalDistance %= PointInterval;
for (int i = 0; i < count; i++)
{
SmokePoints.Add(new SmokePoint
{
Position = pointPos,
Time = time,
});
pointPos += increment;
}
Invalidate(Invalidation.DrawNode);
adaptBounds(position);
}
SmokeVertexTimes.Add(time);
SmokeVertexPositions.Add(position);
lastPosition = position;
if (time - SmokeStartTime > max_duration)
onSmokeEnded(time);
}
protected virtual void OnSmokeEnded(double time)
private void recalculateBounds()
{
IsActive = false;
SmokeTimeEnd = time;
TopLeft = BottomRight = Vector2.Zero;
foreach (var point in SmokePoints)
adaptBounds(point.Position);
}
private void adaptBounds(Vector2 position)
{
if (position.X < TopLeft.X)
TopLeft = new Vector2(position.X, TopLeft.Y);
else if (position.X > BottomRight.X)
BottomRight = new Vector2(position.X, BottomRight.Y);
if (position.Y < TopLeft.Y)
TopLeft = new Vector2(TopLeft.X, position.Y);
else if (position.Y > BottomRight.Y)
BottomRight = new Vector2(BottomRight.X, position.Y);
}
private void onSmokeEnded(double time)
{
if (!IsActive)
return;
IsActive = false;
SmokeEndTime = time;
LifetimeEnd = time + LifetimeAfterSmokeEnd + 100;
// TODO: HYPER MEGA JANK WTF??
if (Parent is SkinnableDrawable)
Parent.LifetimeEnd = LifetimeEnd;
}
protected abstract override DrawNode CreateDrawNode();
protected override void Update()
{
base.Update();
const double visible_duration = 8000;
const float disappear_speed = 3;
int index = 0;
if (!IsActive)
{
double cutoffTime = SmokeTimeStart + disappear_speed * (Time.Current - (SmokeTimeEnd + visible_duration));
index = ~SmokeVertexTimes.BinarySearch(cutoffTime, new UpperBoundComparer());
}
Vertices = new List<Vector2>(SmokeVertexPositions.Skip(index));
Position = -PositionInBoundingBox(Vector2.Zero);
Position = TopLeft;
}
protected override void Dispose(bool isDisposing)
@ -110,21 +287,163 @@ namespace osu.Game.Rulesets.Osu.Skinning
if (smokeContainer != null)
{
smokeContainer.SmokeMoved -= guardOnSmokeMoved;
smokeContainer.SmokeEnded -= guardOnSmokeEnded;
smokeContainer.SmokeMoved -= onSmokeMoved;
smokeContainer.SmokeEnded -= onSmokeEnded;
}
}
private struct UpperBoundComparer : IComparer<double>
protected struct SmokePoint
{
public int Compare(double x, double target)
{
// By returning -1 when the target value is equal to x, guarantees that the
// element at BinarySearch's returned index will always be the first element
// larger. Since 0 is never returned, the target is never "found", so the return
// value will be the index's complement.
public Vector2 Position;
public double Time;
return x > target ? 1 : -1;
public struct UpperBoundComparer : IComparer<SmokePoint>
{
public int Compare(SmokePoint x, SmokePoint target)
{
// By returning -1 when the target value is equal to x, guarantees that the
// element at BinarySearch's returned index will always be the first element
// larger. Since 0 is never returned, the target is never "found", so the return
// value will be the index's complement.
return x.Time > target.Time ? 1 : -1;
}
}
}
protected abstract class SmokeDrawNode : TexturedShaderDrawNode
{
protected new Smoke Source => (Smoke)base.Source;
protected IVertexBatch<TexturedVertex2D>? QuadBatch;
protected readonly List<SmokePoint> Points = new List<SmokePoint>();
protected float Radius;
protected Vector2 DrawSize;
protected Vector2 PositionOffset;
protected Texture? Texture;
protected double SmokeStartTime;
protected double SmokeEndTime;
protected double CurrentTime;
protected RectangleF TextureRect;
private IFrameBasedClock? clock;
private int rotationSeed;
private Random rotationRNG = new Random();
public SmokeDrawNode(ITexturedShaderDrawable source)
: base(source)
{
}
public override void ApplyState()
{
base.ApplyState();
Points.Clear();
Points.AddRange(Source.SmokePoints);
Radius = Source.Radius;
DrawSize = Source.DrawSize;
PositionOffset = Source.TopLeft;
Texture = Source.Texture;
clock = Source.Clock;
SmokeStartTime = Source.SmokeStartTime;
SmokeEndTime = Source.SmokeEndTime;
rotationSeed = Source.RotationSeed;
}
public sealed override void Draw(IRenderer renderer)
{
base.Draw(renderer);
if (Points.Count == 0)
return;
QuadBatch ??= renderer.CreateQuadBatch<TexturedVertex2D>(7200, 10);
Texture ??= renderer.WhitePixel;
var shader = GetAppropriateShader(renderer);
shader.Bind();
Texture.Bind();
UpdateDrawVariables(renderer);
UpdateVertexBuffer();
shader.Unbind();
}
protected Color4 ColorAtPosition(Vector2 localPos) => DrawColourInfo.Colour.HasSingleColour
? ((SRGBColour)DrawColourInfo.Colour).Linear
: DrawColourInfo.Colour.Interpolate(Vector2.Divide(localPos, DrawSize)).Linear;
protected abstract Color4 ColorAtTime(double pointTime);
protected virtual void UpdateDrawVariables(IRenderer renderer)
{
Debug.Assert(clock != null);
Debug.Assert(Texture != null);
CurrentTime = clock.CurrentTime;
TextureRect = Texture.GetTextureRect();
rotationRNG = new Random(rotationSeed);
}
protected virtual void UpdateVertexBuffer()
{
foreach (var point in Points)
drawPointQuad(point);
}
private Vector2 nextTextureDirection()
{
float angle = (float)rotationRNG.NextDouble() * 2 * MathF.PI;
return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
}
private void drawPointQuad(SmokePoint point)
{
Debug.Assert(QuadBatch != null);
var color = ColorAtTime(point.Time);
var dir = nextTextureDirection();
var ortho = dir.PerpendicularLeft;
var localTopLeft = point.Position + (Radius * (-ortho - dir)) - PositionOffset;
var localTopRight = point.Position + (Radius * (-ortho + dir)) - PositionOffset;
var localBotLeft = point.Position + (Radius * (ortho - dir)) - PositionOffset;
var localBotRight = point.Position + (Radius * (ortho + dir)) - PositionOffset;
QuadBatch.Add(new TexturedVertex2D
{
Position = Vector2Extensions.Transform(localTopLeft, DrawInfo.Matrix),
TexturePosition = TextureRect.TopLeft,
Colour = Color4Extensions.Multiply(ColorAtPosition(localTopLeft), color),
});
QuadBatch.Add(new TexturedVertex2D
{
Position = Vector2Extensions.Transform(localTopRight, DrawInfo.Matrix),
TexturePosition = TextureRect.TopRight,
Colour = Color4Extensions.Multiply(ColorAtPosition(localTopRight), color),
});
QuadBatch.Add(new TexturedVertex2D
{
Position = Vector2Extensions.Transform(localBotRight, DrawInfo.Matrix),
TexturePosition = TextureRect.BottomRight,
Colour = Color4Extensions.Multiply(ColorAtPosition(localBotRight), color),
});
QuadBatch.Add(new TexturedVertex2D
{
Position = Vector2Extensions.Transform(localBotLeft, DrawInfo.Matrix),
TexturePosition = TextureRect.BottomLeft,
Colour = Color4Extensions.Multiply(ColorAtPosition(localBotLeft), color),
});
}
}
}

View File

@ -3,7 +3,6 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;