osu/osu.Game.Rulesets.Osu/Skinning/Smoke.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

451 lines
14 KiB
C#
Raw Normal View History

2022-09-18 19:08:34 +00:00
// 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;
2022-09-18 19:08:34 +00:00
using System.Collections.Generic;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
2022-09-18 19:08:34 +00:00
using osu.Framework.Graphics;
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;
2022-09-18 19:08:34 +00:00
using osu.Game.Rulesets.Osu.UI;
using osuTK;
using osuTK.Graphics;
2022-09-18 19:08:34 +00:00
namespace osu.Game.Rulesets.Osu.Skinning
{
public abstract class Smoke : Drawable, ITexturedShaderDrawable
2022-09-18 19:08:34 +00:00
{
public IShader? TextureShader { get; private set; }
public IShader? RoundedTextureShader { get; private set; }
private float? radius;
2022-09-19 01:32:33 +00:00
protected float Radius
{
get => radius ?? Texture?.DisplayWidth * 0.165f ?? 3;
set
{
if (radius == value)
return;
radius = value;
Invalidate(Invalidation.DrawNode);
}
}
private Texture? texture;
2022-09-19 01:32:33 +00:00
protected Texture? Texture
{
get => texture;
set
{
texture = value;
Invalidate(Invalidation.DrawNode);
}
}
private double smokeTimeStart = double.MinValue;
2022-09-19 01:32:33 +00:00
protected double SmokeStartTime
{
get => smokeTimeStart;
private set
{
if (smokeTimeStart == value)
return;
smokeTimeStart = value;
Invalidate(Invalidation.DrawNode);
}
}
private double smokeTimeEnd = double.MaxValue;
2022-09-19 01:32:33 +00:00
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;
2022-09-19 01:32:33 +00:00
protected Vector2 TopLeft
{
get => topLeft;
set
{
if (topLeft == value)
return;
topLeft = value;
2022-09-19 01:32:33 +00:00
Invalidate();
}
}
private Vector2 bottomRight;
2022-09-19 01:32:33 +00:00
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;
2022-09-18 19:08:34 +00:00
protected bool IsActive { get; private set; }
protected readonly List<SmokePoint> SmokePoints = new List<SmokePoint>();
private float totalDistance;
2022-09-19 01:32:33 +00:00
private Vector2? lastPosition;
2022-09-20 11:36:44 +00:00
private const int max_point_count = 18_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)}.");
}
2022-09-18 19:08:34 +00:00
[Resolved(CanBeNull = true)]
private SmokeContainer? smokeContainer { get; set; }
[BackgroundDependencyLoader]
private void load(ShaderManager shaders)
2022-09-18 19:08:34 +00:00
{
RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED);
TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
2022-09-18 19:08:34 +00:00
}
protected override void LoadComplete()
{
base.LoadComplete();
2022-09-19 17:16:05 +00:00
Anchor = Anchor.TopLeft;
Origin = Anchor.TopLeft;
SmokeStartTime = Time.Current;
totalDistance = PointInterval;
2022-09-18 19:08:34 +00:00
if (smokeContainer != null)
{
smokeContainer.SmokeMoved += onSmokeMoved;
smokeContainer.SmokeEnded += onSmokeEnded;
2022-09-18 19:08:34 +00:00
IsActive = true;
2022-09-19 17:16:05 +00:00
onSmokeMoved(smokeContainer.LastMousePosition, Time.Current);
}
2022-09-18 19:08:34 +00:00
}
private Vector2 nextPointDirection()
{
float angle = RNG.NextSingle(0, 2 * MathF.PI);
return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
}
private void onSmokeMoved(Vector2 position, double time)
2022-09-18 19:08:34 +00:00
{
if (!IsActive)
return;
2022-09-18 19:08:34 +00:00
lastPosition ??= position;
2022-09-18 19:08:34 +00:00
float delta = (position - (Vector2)lastPosition).LengthFast;
totalDistance += delta;
int count = (int)(totalDistance / PointInterval);
if (count > 0)
{
Vector2 increment = position - (Vector2)lastPosition;
increment.NormalizeFast();
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;
2022-09-19 01:32:33 +00:00
for (int i = 0; i < count; i++)
{
SmokePoints.Add(new SmokePoint
{
Position = pointPos,
Time = time,
Direction = nextPointDirection(),
});
pointPos += increment;
}
Invalidate(Invalidation.DrawNode);
adaptBounds(position);
}
lastPosition = position;
if (SmokePoints.Count >= max_point_count)
onSmokeEnded(time);
2022-09-18 19:08:34 +00:00
}
private void recalculateBounds()
2022-09-18 19:08:34 +00:00
{
TopLeft = BottomRight = Vector2.Zero;
2022-09-18 19:08:34 +00:00
foreach (var point in SmokePoints)
adaptBounds(point.Position);
}
2022-09-18 19:08:34 +00:00
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);
2022-09-18 19:08:34 +00:00
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);
2022-09-18 19:08:34 +00:00
}
private void onSmokeEnded(double time)
2022-09-18 19:08:34 +00:00
{
if (!IsActive)
return;
2022-09-18 19:08:34 +00:00
IsActive = false;
SmokeEndTime = time;
LifetimeEnd = time + LifetimeAfterSmokeEnd + 100;
2022-09-18 19:08:34 +00:00
}
protected abstract override DrawNode CreateDrawNode();
2022-09-18 19:08:34 +00:00
protected override void Update()
{
base.Update();
Position = TopLeft;
2022-09-18 19:08:34 +00:00
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (smokeContainer != null)
{
smokeContainer.SmokeMoved -= onSmokeMoved;
smokeContainer.SmokeEnded -= onSmokeEnded;
2022-09-18 19:08:34 +00:00
}
}
protected struct SmokePoint
2022-09-18 19:08:34 +00:00
{
public Vector2 Position;
public double Time;
public Vector2 Direction;
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;
2022-09-19 01:32:33 +00:00
protected 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;
}
public sealed override void Draw(IRenderer renderer)
2022-09-18 19:08:34 +00:00
{
base.Draw(renderer);
if (Points.Count == 0)
return;
QuadBatch ??= renderer.CreateQuadBatch<TexturedVertex2D>(max_point_count / 10, 10);
Texture ??= renderer.WhitePixel;
var shader = GetAppropriateShader(renderer);
renderer.SetBlend(BlendingParameters.Additive);
2022-09-20 11:20:32 +00:00
renderer.PushLocalMatrix(DrawInfo.Matrix);
shader.Bind();
Texture.Bind();
UpdateDrawVariables(renderer);
2022-09-20 11:20:32 +00:00
UpdateVertexBuffer(renderer);
shader.Unbind();
2022-09-20 11:20:32 +00:00
renderer.PopLocalMatrix();
}
protected Color4 ColorAtPosition(Vector2 localPos) => DrawColourInfo.Colour.HasSingleColour
? ((SRGBColour)DrawColourInfo.Colour).Linear
: DrawColourInfo.Colour.Interpolate(Vector2.Divide(localPos, DrawSize)).Linear;
protected abstract Color4 PointColor(SmokePoint point);
protected abstract float PointScale(SmokePoint point);
protected abstract Vector2 PointDirection(SmokePoint point);
protected virtual void UpdateDrawVariables(IRenderer renderer)
{
Debug.Assert(clock != null);
Debug.Assert(Texture != null);
CurrentTime = clock.CurrentTime;
TextureRect = Texture.GetTextureRect();
}
2022-09-20 11:20:32 +00:00
protected virtual void UpdateVertexBuffer(IRenderer renderer)
{
foreach (var point in Points)
drawPointQuad(point);
}
private void drawPointQuad(SmokePoint point)
{
Debug.Assert(QuadBatch != null);
var color = PointColor(point);
float scale = PointScale(point);
var dir = PointDirection(point);
var ortho = dir.PerpendicularLeft;
if (color.A == 0 || scale == 0)
return;
var localTopLeft = point.Position + (Radius * scale * (-ortho - dir)) - PositionOffset;
var localTopRight = point.Position + (Radius * scale * (-ortho + dir)) - PositionOffset;
var localBotLeft = point.Position + (Radius * scale * (ortho - dir)) - PositionOffset;
var localBotRight = point.Position + (Radius * scale * (ortho + dir)) - PositionOffset;
2022-09-18 19:08:34 +00:00
QuadBatch.Add(new TexturedVertex2D
{
2022-09-20 11:20:32 +00:00
Position = localTopLeft,
TexturePosition = TextureRect.TopLeft,
Colour = Color4Extensions.Multiply(ColorAtPosition(localTopLeft), color),
});
QuadBatch.Add(new TexturedVertex2D
{
2022-09-20 11:20:32 +00:00
Position = localTopRight,
TexturePosition = TextureRect.TopRight,
Colour = Color4Extensions.Multiply(ColorAtPosition(localTopRight), color),
});
QuadBatch.Add(new TexturedVertex2D
{
2022-09-20 11:20:32 +00:00
Position = localBotRight,
TexturePosition = TextureRect.BottomRight,
Colour = Color4Extensions.Multiply(ColorAtPosition(localBotRight), color),
});
QuadBatch.Add(new TexturedVertex2D
{
2022-09-20 11:20:32 +00:00
Position = localBotLeft,
TexturePosition = TextureRect.BottomLeft,
Colour = Color4Extensions.Multiply(ColorAtPosition(localBotLeft), color),
});
2022-09-18 19:08:34 +00:00
}
}
}
}