2022-11-13 19:59:17 +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.
using System ;
using System.Collections.Generic ;
2024-01-25 18:42:38 +08:00
using osu.Framework.Allocation ;
2022-11-13 19:59:17 +08:00
using osu.Framework.Bindables ;
using osu.Framework.Graphics ;
2024-01-25 18:42:38 +08:00
using osu.Framework.Graphics.Primitives ;
using osu.Framework.Graphics.Rendering ;
using osu.Framework.Graphics.Shaders ;
using osu.Framework.Graphics.Textures ;
using osu.Framework.Utils ;
using osuTK ;
2022-11-13 19:59:17 +08:00
namespace osu.Game.Graphics.Backgrounds
{
public partial class TrianglesV2 : Drawable
{
private const float triangle_size = 100 ;
private const float base_velocity = 50 ;
2022-11-16 20:02:09 +08:00
/// <summary>
/// sqrt(3) / 2
/// </summary>
private const float equilateral_triangle_ratio = 0.866f ;
2022-11-21 15:32:19 +08:00
public float Thickness { get ; set ; } = 0.02f ; // No need for invalidation since it's happening in Update()
2022-11-21 15:20:35 +08:00
2024-01-25 18:42:38 +08:00
public float ScaleAdjust { get ; set ; } = 1 ;
2022-11-13 19:59:17 +08:00
/// <summary>
/// Whether we should create new triangles as others expire.
/// </summary>
protected virtual bool CreateNewTriangles = > true ;
2023-01-26 14:46:41 +08:00
/// <summary>
2024-01-23 09:54:27 +08:00
/// Controls on which <see cref="Axes"/> the portion of triangles that falls within this <see cref="Drawable"/>'s
/// shape is drawn to the screen. Default is Axes.Both.
2023-01-26 14:46:41 +08:00
/// </summary>
2024-01-23 09:54:27 +08:00
public Axes ClampAxes { get ; set ; } = Axes . Both ;
2023-01-26 14:46:41 +08:00
2022-11-16 20:17:50 +08:00
private readonly BindableFloat spawnRatio = new BindableFloat ( 1f ) ;
2022-11-13 19:59:17 +08:00
/// <summary>
/// The amount of triangles we want compared to the default distribution.
/// </summary>
2022-11-16 20:17:50 +08:00
public float SpawnRatio
{
get = > spawnRatio . Value ;
set = > spawnRatio . Value = value ;
}
2022-11-13 19:59:17 +08:00
/// <summary>
/// The relative velocity of the triangles. Default is 1.
/// </summary>
public float Velocity = 1 ;
private readonly List < TriangleParticle > parts = new List < TriangleParticle > ( ) ;
2022-11-21 13:56:58 +08:00
private Random ? stableRandom ;
2022-11-13 19:59:17 +08:00
2022-11-21 13:56:58 +08:00
private IShader shader = null ! ;
private Texture texture = null ! ;
2022-11-13 19:59:17 +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 TrianglesV2 ( int? seed = null )
{
if ( seed ! = null )
stableRandom = new Random ( seed . Value ) ;
}
[BackgroundDependencyLoader]
2022-11-30 09:09:46 +08:00
private void load ( ShaderManager shaders , IRenderer renderer )
2022-11-13 19:59:17 +08:00
{
shader = shaders . Load ( VertexShaderDescriptor . TEXTURE_2 , "TriangleBorder" ) ;
2022-11-30 09:09:46 +08:00
texture = renderer . WhitePixel ;
2022-11-13 19:59:17 +08:00
}
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2022-11-16 20:17:50 +08:00
spawnRatio . BindValueChanged ( _ = > Reset ( ) , true ) ;
2022-11-13 19:59:17 +08:00
}
protected override void Update ( )
{
base . Update ( ) ;
Invalidate ( Invalidation . DrawNode ) ;
if ( CreateNewTriangles )
addTriangles ( false ) ;
float elapsedSeconds = ( float ) Time . Elapsed / 1000 ;
// Since position is relative, the velocity needs to scale inversely with DrawHeight.
2022-11-16 20:12:57 +08:00
float movedDistance = - elapsedSeconds * Velocity * base_velocity / DrawHeight ;
2022-11-13 19:59:17 +08:00
for ( int i = 0 ; i < parts . Count ; i + + )
{
TriangleParticle newParticle = parts [ i ] ;
2022-11-16 20:12:57 +08:00
newParticle . Position . Y + = Math . Max ( 0.5f , parts [ i ] . SpeedMultiplier ) * movedDistance ;
2022-11-13 19:59:17 +08:00
parts [ i ] = newParticle ;
2024-01-25 18:42:38 +08:00
float bottomPos = parts [ i ] . Position . Y + triangle_size * ScaleAdjust * equilateral_triangle_ratio / DrawHeight ;
2022-11-13 19:59:17 +08:00
if ( bottomPos < 0 )
parts . RemoveAt ( i ) ;
}
}
/// <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 ; }
private void addTriangles ( bool randomY )
{
// Limited by the maximum size of QuadVertexBuffer for safety.
const int max_triangles = ushort . MaxValue / ( IRenderer . VERTICES_PER_QUAD + 2 ) ;
2022-11-25 19:14:21 +08:00
AimCount = ( int ) Math . Clamp ( DrawWidth * 0.02f * SpawnRatio , 1 , max_triangles ) ;
2022-11-13 19:59:17 +08:00
2022-11-16 20:02:09 +08:00
int currentCount = parts . Count ;
for ( int i = 0 ; i < AimCount - currentCount ; i + + )
2022-11-13 19:59:17 +08:00
parts . Add ( createTriangle ( randomY ) ) ;
}
private TriangleParticle createTriangle ( bool randomY )
{
TriangleParticle particle = CreateTriangle ( ) ;
2022-11-16 20:02:09 +08:00
float y = 1 ;
if ( randomY )
{
// since triangles are drawn from the top - allow them to be positioned a bit above the screen
2024-01-25 18:42:38 +08:00
float maxOffset = triangle_size * ScaleAdjust * equilateral_triangle_ratio / DrawHeight ;
2022-11-16 20:02:09 +08:00
y = Interpolation . ValueAt ( nextRandom ( ) , - maxOffset , 1f , 0f , 1f ) ;
}
particle . Position = new Vector2 ( nextRandom ( ) , y ) ;
2022-11-13 19:59:17 +08:00
return particle ;
}
/// <summary>
2022-11-16 20:12:57 +08:00
/// Creates a triangle particle with a random speed multiplier.
2022-11-13 19:59:17 +08:00
/// </summary>
/// <returns>The triangle particle.</returns>
protected virtual TriangleParticle CreateTriangle ( )
{
const float std_dev = 0.16f ;
const float mean = 0.5f ;
float u1 = 1 - nextRandom ( ) ; //uniform(0,1] random floats
float u2 = 1 - nextRandom ( ) ;
float randStdNormal = ( float ) ( Math . Sqrt ( - 2.0 * Math . Log ( u1 ) ) * Math . Sin ( 2.0 * Math . PI * u2 ) ) ; // random normal(0,1)
2022-11-16 20:12:57 +08:00
float speedMultiplier = Math . Max ( mean + std_dev * randStdNormal , 0.1f ) ; // random normal(mean,stdDev^2)
2022-11-13 19:59:17 +08:00
2022-11-16 20:12:57 +08:00
return new TriangleParticle { SpeedMultiplier = speedMultiplier } ;
2022-11-13 19:59:17 +08:00
}
private float nextRandom ( ) = > ( float ) ( stableRandom ? . NextDouble ( ) ? ? RNG . NextSingle ( ) ) ;
protected override DrawNode CreateDrawNode ( ) = > new TrianglesDrawNode ( this ) ;
private class TrianglesDrawNode : DrawNode
{
protected new TrianglesV2 Source = > ( TrianglesV2 ) base . Source ;
2022-11-21 13:56:58 +08:00
private IShader shader = null ! ;
private Texture texture = null ! ;
2022-11-13 19:59:17 +08:00
private readonly List < TriangleParticle > parts = new List < TriangleParticle > ( ) ;
2022-11-29 08:21:59 +08:00
2024-01-25 18:42:38 +08:00
private Vector2 triangleSize ;
2022-11-29 08:21:59 +08:00
2022-11-13 19:59:17 +08:00
private Vector2 size ;
2022-11-21 15:20:35 +08:00
private float thickness ;
2022-11-27 01:05:34 +08:00
private float texelSize ;
2024-01-23 09:54:27 +08:00
private Axes clampAxes ;
2022-11-13 19:59:17 +08:00
public TrianglesDrawNode ( TrianglesV2 source )
: base ( source )
{
}
public override void ApplyState ( )
{
base . ApplyState ( ) ;
shader = Source . shader ;
texture = Source . texture ;
size = Source . DrawSize ;
2022-11-21 15:32:19 +08:00
thickness = Source . Thickness ;
2024-01-23 09:54:27 +08:00
clampAxes = Source . ClampAxes ;
2024-01-25 18:42:38 +08:00
triangleSize = new Vector2 ( 1f , equilateral_triangle_ratio ) * triangle_size * Source . ScaleAdjust ;
2022-11-29 08:21:59 +08:00
Quad triangleQuad = new Quad (
Vector2Extensions . Transform ( Vector2 . Zero , DrawInfo . Matrix ) ,
Vector2Extensions . Transform ( new Vector2 ( triangle_size , 0f ) , DrawInfo . Matrix ) ,
Vector2Extensions . Transform ( new Vector2 ( 0f , triangleSize . Y ) , DrawInfo . Matrix ) ,
Vector2Extensions . Transform ( triangleSize , DrawInfo . Matrix )
) ;
texelSize = 1.5f / triangleQuad . Height ;
2022-11-13 19:59:17 +08:00
parts . Clear ( ) ;
parts . AddRange ( Source . parts ) ;
}
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 )
2022-11-13 19:59:17 +08:00
{
base . Draw ( renderer ) ;
2022-11-29 07:36:27 +08:00
if ( Source . AimCount = = 0 | | thickness = = 0 )
2022-11-21 13:56:58 +08:00
return ;
2023-02-25 00:21:37 +08:00
borderDataBuffer ? ? = renderer . CreateUniformBuffer < TriangleBorderData > ( ) ;
borderDataBuffer . Data = borderDataBuffer . Data with
{
Thickness = thickness ,
TexelSize = texelSize
} ;
2022-11-13 19:59:17 +08:00
shader . Bind ( ) ;
2023-03-16 19:06:35 +08:00
shader . BindUniformBlock ( @"m_BorderData" , borderDataBuffer ) ;
2022-11-13 19:59:17 +08:00
2023-01-26 14:46:41 +08:00
Vector2 relativeSize = Vector2 . Divide ( triangleSize , size ) ;
2022-11-29 08:21:59 +08:00
2022-11-13 19:59:17 +08:00
foreach ( TriangleParticle particle in parts )
{
2023-01-26 14:46:41 +08:00
Vector2 topLeft = particle . Position - new Vector2 ( relativeSize . X * 0.5f , 0f ) ;
2024-01-23 09:54:27 +08:00
Quad triangleQuad = getClampedQuad ( clampAxes , topLeft , relativeSize ) ;
2022-11-13 19:59:17 +08:00
var drawQuad = new Quad (
2023-01-26 14:46:41 +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 )
2022-11-13 19:59:17 +08:00
) ;
2023-01-26 14:46:41 +08:00
RectangleF textureCoords = new RectangleF (
triangleQuad . TopLeft . X - topLeft . X ,
triangleQuad . TopLeft . Y - topLeft . Y ,
triangleQuad . Width ,
triangleQuad . Height
) / relativeSize ;
2022-11-13 19:59:17 +08:00
2024-01-20 13:41:34 +08:00
renderer . DrawQuad ( texture , drawQuad , DrawColourInfo . Colour . Interpolate ( triangleQuad ) , new RectangleF ( 0 , 0 , 1 , 1 ) , textureCoords : textureCoords ) ;
2022-11-13 19:59:17 +08:00
}
shader . Unbind ( ) ;
}
2024-01-23 09:54:27 +08:00
private static Quad getClampedQuad ( Axes clampAxes , Vector2 topLeft , Vector2 size )
2023-01-26 14:46:41 +08:00
{
2024-01-23 09:54:27 +08:00
Vector2 clampedTopLeft = topLeft ;
if ( clampAxes = = Axes . X | | clampAxes = = Axes . Both )
{
clampedTopLeft . X = Math . Clamp ( topLeft . X , 0f , 1f ) ;
size . X = Math . Clamp ( topLeft . X + size . X , 0f , 1f ) - clampedTopLeft . X ;
}
if ( clampAxes = = Axes . Y | | clampAxes = = Axes . Both )
{
clampedTopLeft . Y = Math . Clamp ( topLeft . Y , 0f , 1f ) ;
size . Y = Math . Clamp ( topLeft . Y + size . Y , 0f , 1f ) - clampedTopLeft . Y ;
}
return new Quad ( clampedTopLeft . X , clampedTopLeft . Y , size . X , size . Y ) ;
2023-01-26 14:46:41 +08:00
}
2022-11-13 19:59:17 +08:00
protected override void Dispose ( bool isDisposing )
{
base . Dispose ( isDisposing ) ;
2023-02-25 00:21:37 +08:00
borderDataBuffer ? . Dispose ( ) ;
2022-11-13 19:59:17 +08:00
}
}
protected struct TriangleParticle
{
/// <summary>
/// The position of the top vertex of the triangle.
/// </summary>
public Vector2 Position ;
/// <summary>
2022-11-16 20:12:57 +08:00
/// The speed multiplier of the triangle.
2022-11-13 19:59:17 +08:00
/// </summary>
2022-11-16 20:12:57 +08:00
public float SpeedMultiplier ;
2022-11-13 19:59:17 +08:00
}
}
}