2019-01-24 16:43:03 +08: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.
2018-04-13 17:19:50 +08:00
2022-06-17 15:37:17 +08:00
#nullable disable
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics ;
2020-01-09 12:43:44 +08:00
using osu.Framework.Utils ;
2018-11-20 15:51:59 +08:00
using osuTK ;
using osuTK.Graphics ;
2018-04-13 17:19:50 +08:00
using System ;
using osu.Framework.Graphics.Shaders ;
using osu.Framework.Graphics.Textures ;
using osu.Framework.Graphics.Colour ;
using osu.Framework.Graphics.Primitives ;
using osu.Framework.Allocation ;
using System.Collections.Generic ;
2022-07-29 21:33:34 +08:00
using osu.Framework.Graphics.Rendering ;
2022-08-05 19:36:28 +08:00
using osu.Framework.Graphics.Rendering.Vertices ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Lists ;
2022-11-15 16:06:28 +08:00
using osu.Framework.Bindables ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Graphics.Backgrounds
{
2022-11-24 13:32:20 +08:00
public partial class Triangles : Drawable
2018-04-13 17:19:50 +08:00
{
private const float triangle_size = 100 ;
private const float base_velocity = 50 ;
2022-11-15 21:20:08 +08:00
/// <summary>
/// sqrt(3) / 2
/// </summary>
private const float equilateral_triangle_ratio = 0.866f ;
2020-01-06 07:32:13 +08:00
private Color4 colourLight = Color4 . White ;
public Color4 ColourLight
{
get = > colourLight ;
set
{
2020-01-06 08:53:29 +08:00
if ( colourLight = = value ) return ;
2020-01-06 07:32:13 +08:00
colourLight = value ;
updateColours ( ) ;
}
}
private Color4 colourDark = Color4 . Black ;
public Color4 ColourDark
{
get = > colourDark ;
set
{
2020-01-06 08:53:29 +08:00
if ( colourDark = = value ) return ;
2020-01-06 07:32:13 +08:00
colourDark = value ;
updateColours ( ) ;
}
}
2018-04-13 17:19:50 +08:00
/// <summary>
/// Whether we should create new triangles as others expire.
/// </summary>
protected virtual bool CreateNewTriangles = > true ;
/// <summary>
/// The amount of triangles we want compared to the default distribution.
/// </summary>
protected virtual float SpawnRatio = > 1 ;
2022-11-15 16:06:28 +08:00
private readonly BindableFloat triangleScale = new BindableFloat ( 1f ) ;
public float TriangleScale
{
get = > triangleScale . Value ;
set = > triangleScale . Value = value ;
}
2018-04-13 17:19:50 +08:00
2023-01-25 17:20:51 +08:00
/// <summary>
/// If enabled, only the portion of triangles that falls within this <see cref="Drawable"/>'s
/// shape is drawn to the screen.
/// </summary>
2023-01-25 17:54:30 +08:00
public bool Masking { get ; set ; }
2023-01-25 17:20:51 +08:00
2018-04-13 17:19:50 +08:00
/// <summary>
/// Whether we should drop-off alpha values of triangles more quickly to improve
/// the visual appearance of fading. This defaults to on as it is generally more
/// aesthetically pleasing, but should be turned off in buffered containers.
/// </summary>
public bool HideAlphaDiscrepancies = true ;
/// <summary>
/// The relative velocity of the triangles. Default is 1.
/// </summary>
public float Velocity = 1 ;
private readonly SortedList < TriangleParticle > parts = new SortedList < TriangleParticle > ( Comparer < TriangleParticle > . Default ) ;
2020-11-17 12:06:30 +08:00
private Random stableRandom ;
2019-03-07 17:30:18 +08:00
private IShader shader ;
2022-08-02 18:50:57 +08:00
private Texture texture ;
2018-04-13 17:19:50 +08:00
2020-10-02 00:48:41 +08:00
/// <summary>
/// Construct a new triangle visualisation.
/// </summary>
/// <param name="seed">An optional seed to stabilise random positions / attributes. Note that this does not guarantee stable playback when seeking in time.</param>
public Triangles ( int? seed = null )
2018-04-13 17:19:50 +08:00
{
2020-10-02 00:48:41 +08:00
if ( seed ! = null )
stableRandom = new Random ( seed . Value ) ;
2018-04-13 17:19:50 +08:00
}
[BackgroundDependencyLoader]
2022-08-02 18:50:57 +08:00
private void load ( IRenderer renderer , ShaderManager shaders )
2018-04-13 17:19:50 +08:00
{
2022-08-02 18:50:57 +08:00
texture = renderer . WhitePixel ;
2023-01-24 12:38:42 +08:00
shader = shaders . Load ( VertexShaderDescriptor . TEXTURE_2 , "TriangleBorder" ) ;
2018-04-13 17:19:50 +08:00
}
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2022-11-15 16:06:28 +08:00
triangleScale . BindValueChanged ( _ = > Reset ( ) , true ) ;
2018-04-13 17:19:50 +08:00
}
protected override void Update ( )
{
base . Update ( ) ;
2020-02-25 10:30:33 +08:00
Invalidate ( Invalidation . DrawNode ) ;
2018-04-13 17:19:50 +08:00
if ( CreateNewTriangles )
addTriangles ( false ) ;
2019-02-28 12:31:40 +08:00
float adjustedAlpha = HideAlphaDiscrepancies
2018-04-13 17:19:50 +08:00
// Cubically scale alpha to make it drop off more sharply.
2019-11-25 07:45:42 +08:00
? MathF . Pow ( DrawColourInfo . Colour . AverageColour . Linear . A , 3 )
2019-02-28 12:31:40 +08:00
: 1 ;
2018-04-13 17:19:50 +08:00
float elapsedSeconds = ( float ) Time . Elapsed / 1000 ;
// Since position is relative, the velocity needs to scale inversely with DrawHeight.
// Since we will later multiply by the scale of individual triangles we normalize by
// dividing by triangleScale.
2022-11-15 16:06:28 +08:00
float movedDistance = - elapsedSeconds * Velocity * base_velocity / ( DrawHeight * TriangleScale ) ;
2018-04-13 17:19:50 +08:00
for ( int i = 0 ; i < parts . Count ; i + + )
{
TriangleParticle newParticle = parts [ i ] ;
// Scale moved distance by the size of the triangle. Smaller triangles should move more slowly.
2021-08-31 13:08:21 +08:00
newParticle . Position . Y + = Math . Max ( 0.5f , parts [ i ] . Scale ) * movedDistance ;
2018-04-13 17:19:50 +08:00
newParticle . Colour . A = adjustedAlpha ;
parts [ i ] = newParticle ;
2022-11-15 21:20:08 +08:00
float bottomPos = parts [ i ] . Position . Y + triangle_size * parts [ i ] . Scale * equilateral_triangle_ratio / DrawHeight ;
2018-04-13 17:19:50 +08:00
if ( bottomPos < 0 )
parts . RemoveAt ( i ) ;
}
}
2020-11-17 12:06:30 +08:00
/// <summary>
/// Clears and re-initialises triangles according to a given seed.
/// </summary>
/// <param name="seed">An optional seed to stabilise random positions / attributes. Note that this does not guarantee stable playback when seeking in time.</param>
public void Reset ( int? seed = null )
{
if ( seed ! = null )
stableRandom = new Random ( seed . Value ) ;
parts . Clear ( ) ;
addTriangles ( true ) ;
}
protected int AimCount { get ; private set ; }
2019-07-18 23:53:00 +08:00
2018-04-13 17:19:50 +08:00
private void addTriangles ( bool randomY )
{
2022-07-29 22:32:06 +08:00
// Limited by the maximum size of QuadVertexBuffer for safety.
const int max_triangles = ushort . MaxValue / ( IRenderer . VERTICES_PER_QUAD + 2 ) ;
2021-09-15 17:32:52 +08:00
2022-11-15 16:06:28 +08:00
AimCount = ( int ) Math . Min ( max_triangles , DrawWidth * DrawHeight * 0.002f / ( TriangleScale * TriangleScale ) * SpawnRatio ) ;
2018-04-13 17:19:50 +08:00
2022-11-15 16:32:05 +08:00
int currentCount = parts . Count ;
for ( int i = 0 ; i < AimCount - currentCount ; i + + )
2018-04-13 17:19:50 +08:00
parts . Add ( createTriangle ( randomY ) ) ;
}
private TriangleParticle createTriangle ( bool randomY )
{
TriangleParticle particle = CreateTriangle ( ) ;
2022-11-15 16:49:53 +08:00
particle . Position = getRandomPosition ( randomY , particle . Scale ) ;
2020-10-02 00:48:41 +08:00
particle . ColourShade = nextRandom ( ) ;
2020-01-06 19:51:38 +08:00
particle . Colour = CreateTriangleShade ( particle . ColourShade ) ;
2018-04-13 17:19:50 +08:00
return particle ;
}
2022-11-15 16:49:53 +08:00
private Vector2 getRandomPosition ( bool randomY , float scale )
{
float y = 1 ;
if ( randomY )
{
// since triangles are drawn from the top - allow them to be positioned a bit above the screen
2022-11-15 21:20:08 +08:00
float maxOffset = triangle_size * scale * equilateral_triangle_ratio / DrawHeight ;
2022-11-15 16:49:53 +08:00
y = Interpolation . ValueAt ( nextRandom ( ) , - maxOffset , 1f , 0f , 1f ) ;
}
return new Vector2 ( nextRandom ( ) , y ) ;
}
2018-04-13 17:19:50 +08:00
/// <summary>
/// Creates a triangle particle with a random scale.
/// </summary>
/// <returns>The triangle particle.</returns>
protected virtual TriangleParticle CreateTriangle ( )
{
const float std_dev = 0.16f ;
const float mean = 0.5f ;
2020-10-02 00:48:41 +08:00
float u1 = 1 - nextRandom ( ) ; //uniform(0,1] random floats
float u2 = 1 - nextRandom ( ) ;
2020-05-05 09:31:11 +08:00
float randStdNormal = ( float ) ( Math . Sqrt ( - 2.0 * Math . Log ( u1 ) ) * Math . Sin ( 2.0 * Math . PI * u2 ) ) ; // random normal(0,1)
2022-11-15 16:06:28 +08:00
float scale = Math . Max ( TriangleScale * ( mean + std_dev * randStdNormal ) , 0.1f ) ; // random normal(mean,stdDev^2)
2018-04-13 17:19:50 +08:00
return new TriangleParticle { Scale = scale } ;
}
2020-01-06 19:51:38 +08:00
/// <summary>
/// Creates a shade of colour for the triangles.
/// </summary>
/// <returns>The colour.</returns>
protected virtual Color4 CreateTriangleShade ( float shade ) = > Interpolation . ValueAt ( shade , colourDark , colourLight , 0 , 1 ) ;
2020-01-06 07:32:13 +08:00
private void updateColours ( )
{
for ( int i = 0 ; i < parts . Count ; i + + )
{
TriangleParticle newParticle = parts [ i ] ;
2020-01-06 19:51:38 +08:00
newParticle . Colour = CreateTriangleShade ( newParticle . ColourShade ) ;
2020-01-06 07:32:13 +08:00
parts [ i ] = newParticle ;
}
}
2018-04-13 17:19:50 +08:00
2020-11-17 12:06:30 +08:00
private float nextRandom ( ) = > ( float ) ( stableRandom ? . NextDouble ( ) ? ? RNG . NextSingle ( ) ) ;
2019-04-02 10:56:22 +08:00
protected override DrawNode CreateDrawNode ( ) = > new TrianglesDrawNode ( this ) ;
2018-04-13 17:19:50 +08:00
2019-04-02 10:56:22 +08:00
private class TrianglesDrawNode : DrawNode
2018-04-13 17:19:50 +08:00
{
2023-03-14 19:24:39 +08:00
private const float fill = 1f ;
2023-01-24 12:38:42 +08:00
2019-04-02 10:56:22 +08:00
protected new Triangles Source = > ( Triangles ) base . Source ;
2018-04-13 17:19:50 +08:00
2019-04-02 10:56:22 +08:00
private IShader shader ;
private Texture texture ;
2023-01-25 17:20:51 +08:00
private bool masking ;
2018-04-13 17:19:50 +08:00
2019-04-02 10:56:22 +08:00
private readonly List < TriangleParticle > parts = new List < TriangleParticle > ( ) ;
2023-01-24 12:38:42 +08:00
private readonly Vector2 triangleSize = new Vector2 ( 1f , equilateral_triangle_ratio ) * triangle_size ;
2018-04-13 17:19:50 +08:00
2023-01-24 12:38:42 +08:00
private Vector2 size ;
2022-07-29 22:32:06 +08:00
private IVertexBatch < TexturedVertex2D > vertexBatch ;
2018-04-13 17:19:50 +08:00
2019-04-02 10:56:22 +08:00
public TrianglesDrawNode ( Triangles source )
: base ( source )
{
}
2018-04-13 17:19:50 +08:00
2019-04-02 10:56:22 +08:00
public override void ApplyState ( )
{
base . ApplyState ( ) ;
2018-04-13 17:19:50 +08:00
2019-04-02 10:56:22 +08:00
shader = Source . shader ;
texture = Source . texture ;
size = Source . DrawSize ;
2023-01-25 17:20:51 +08:00
masking = Source . Masking ;
2019-04-02 10:56:22 +08:00
parts . Clear ( ) ;
parts . AddRange ( Source . parts ) ;
}
2019-03-01 12:01:52 +08:00
2023-02-25 00:21:37 +08:00
private IUniformBuffer < TriangleBorderData > borderDataBuffer ;
2023-12-04 07:51:21 +08:00
protected override void Draw ( IRenderer renderer )
2018-04-13 17:19:50 +08:00
{
2022-07-29 21:33:34 +08:00
base . Draw ( renderer ) ;
2018-04-13 17:19:50 +08:00
2019-07-22 17:29:04 +08:00
if ( Source . AimCount > 0 & & ( vertexBatch = = null | | vertexBatch . Size ! = Source . AimCount ) )
2019-07-19 13:24:11 +08:00
{
vertexBatch ? . Dispose ( ) ;
2022-07-29 22:32:06 +08:00
vertexBatch = renderer . CreateQuadBatch < TexturedVertex2D > ( Source . AimCount , 1 ) ;
2019-07-19 13:24:11 +08:00
}
2023-02-25 00:21:37 +08:00
borderDataBuffer ? ? = renderer . CreateUniformBuffer < TriangleBorderData > ( ) ;
borderDataBuffer . Data = borderDataBuffer . Data with
{
Thickness = fill ,
// Due to triangles having various sizes we would need to set a different "TexelSize" value for each of them, which is insanely expensive, thus we should use one single value.
// TexelSize computed for an average triangle (size 100) will result in big triangles becoming blurry, so we may just use 0 for all of them.
TexelSize = 0
} ;
2023-02-03 15:32:41 +08:00
2019-04-02 10:56:22 +08:00
shader . Bind ( ) ;
2023-03-16 19:06:35 +08:00
shader . BindUniformBlock ( @"m_BorderData" , borderDataBuffer ) ;
2018-04-13 17:19:50 +08:00
2019-04-02 10:56:22 +08:00
foreach ( TriangleParticle particle in parts )
2018-04-13 17:19:50 +08:00
{
2023-01-24 12:38:42 +08:00
Vector2 relativeSize = Vector2 . Divide ( triangleSize * particle . Scale , size ) ;
Vector2 topLeft = particle . Position - new Vector2 ( relativeSize . X * 0.5f , 0f ) ;
2023-01-25 17:20:51 +08:00
Quad triangleQuad = masking ? clampToDrawable ( topLeft , relativeSize ) : new Quad ( topLeft . X , topLeft . Y , relativeSize . X , relativeSize . Y ) ;
2023-01-24 12:38:42 +08:00
var drawQuad = new Quad (
2023-01-25 17:20:51 +08:00
Vector2Extensions . Transform ( triangleQuad . TopLeft * size , DrawInfo . Matrix ) ,
Vector2Extensions . Transform ( triangleQuad . TopRight * size , DrawInfo . Matrix ) ,
Vector2Extensions . Transform ( triangleQuad . BottomLeft * size , DrawInfo . Matrix ) ,
Vector2Extensions . Transform ( triangleQuad . BottomRight * size , DrawInfo . Matrix )
2018-04-13 17:19:50 +08:00
) ;
2018-09-06 17:02:04 +08:00
ColourInfo colourInfo = DrawColourInfo . Colour ;
2018-04-13 17:19:50 +08:00
colourInfo . ApplyChild ( particle . Colour ) ;
2023-01-25 17:20:51 +08:00
RectangleF textureCoords = new RectangleF (
triangleQuad . TopLeft . X - topLeft . X ,
triangleQuad . TopLeft . Y - topLeft . Y ,
triangleQuad . Width ,
triangleQuad . Height
) / relativeSize ;
renderer . DrawQuad ( texture , drawQuad , colourInfo , new RectangleF ( 0 , 0 , 1 , 1 ) , vertexBatch . AddAction , textureCoords : textureCoords ) ;
2018-04-13 17:19:50 +08:00
}
2019-04-02 10:56:22 +08:00
shader . Unbind ( ) ;
2018-04-13 17:19:50 +08:00
}
2019-03-01 12:01:52 +08:00
2023-01-25 17:20:51 +08:00
private static Quad clampToDrawable ( Vector2 topLeft , Vector2 size )
{
float leftClamped = Math . Clamp ( topLeft . X , 0f , 1f ) ;
float topClamped = Math . Clamp ( topLeft . Y , 0f , 1f ) ;
return new Quad (
leftClamped ,
topClamped ,
Math . Clamp ( topLeft . X + size . X , 0f , 1f ) - leftClamped ,
Math . Clamp ( topLeft . Y + size . Y , 0f , 1f ) - topClamped
) ;
}
2019-03-01 12:01:52 +08:00
protected override void Dispose ( bool isDisposing )
{
base . Dispose ( isDisposing ) ;
2019-07-19 00:14:55 +08:00
vertexBatch ? . Dispose ( ) ;
2023-02-25 00:21:37 +08:00
borderDataBuffer ? . Dispose ( ) ;
2019-03-01 12:01:52 +08:00
}
2018-04-13 17:19:50 +08:00
}
2020-01-06 19:51:38 +08:00
protected struct TriangleParticle : IComparable < TriangleParticle >
2018-04-13 17:19:50 +08:00
{
/// <summary>
/// The position of the top vertex of the triangle.
/// </summary>
public Vector2 Position ;
2020-01-06 07:32:13 +08:00
/// <summary>
/// The colour shade of the triangle.
/// This is needed for colour recalculation of visible triangles when <see cref="ColourDark"/> or <see cref="ColourLight"/> is changed.
/// </summary>
2020-01-06 19:51:38 +08:00
public float ColourShade ;
2020-01-06 07:32:13 +08:00
2018-04-13 17:19:50 +08:00
/// <summary>
/// The colour of the triangle.
/// </summary>
public Color4 Colour ;
/// <summary>
/// The scale of the triangle.
/// </summary>
public float Scale ;
/// <summary>
/// Compares two <see cref="TriangleParticle"/>s. This is a reverse comparer because when the
/// triangles are added to the particles list, they should be drawn from largest to smallest
/// such that the smaller triangles appear on top.
/// </summary>
/// <param name="other"></param>
public int CompareTo ( TriangleParticle other ) = > other . Scale . CompareTo ( Scale ) ;
}
}
}