diff --git a/SomeChartsAvaloniaExamples/Program.cs b/SomeChartsAvaloniaExamples/Program.cs index 13f5de1..db1d7e4 100644 --- a/SomeChartsAvaloniaExamples/Program.cs +++ b/SomeChartsAvaloniaExamples/Program.cs @@ -1,14 +1,11 @@ using System; -using System.Linq; -using FreeTypeSharp.Native; using SomeChartsAvaloniaExamples.elements; -using SomeChartsUi.ui.text; namespace SomeChartsAvaloniaExamples; internal class Program { [STAThread] public static void Main(string[] args) { - ElementsExamples.RunPieChart(); + ScatterChartExample.Run(); } } \ No newline at end of file diff --git a/SomeChartsAvaloniaExamples/SomeChartsAvaloniaExamples.csproj b/SomeChartsAvaloniaExamples/SomeChartsAvaloniaExamples.csproj index c658408..d6de551 100644 --- a/SomeChartsAvaloniaExamples/SomeChartsAvaloniaExamples.csproj +++ b/SomeChartsAvaloniaExamples/SomeChartsAvaloniaExamples.csproj @@ -44,6 +44,9 @@ Always + + Always + diff --git a/SomeChartsAvaloniaExamples/data/particle2.png b/SomeChartsAvaloniaExamples/data/particle2.png new file mode 100644 index 0000000..6a5344a Binary files /dev/null and b/SomeChartsAvaloniaExamples/data/particle2.png differ diff --git a/SomeChartsAvaloniaExamples/src/elements/ScatterChartExample.cs b/SomeChartsAvaloniaExamples/src/elements/ScatterChartExample.cs new file mode 100644 index 0000000..ac33115 --- /dev/null +++ b/SomeChartsAvaloniaExamples/src/elements/ScatterChartExample.cs @@ -0,0 +1,85 @@ +using System; +using MathStuff.vectors; +using SomeChartsUi.data; +using SomeChartsUi.elements.charts.scatter; +using SomeChartsUi.themes.colors; +using SomeChartsUi.themes.themes; +using SomeChartsUi.utils.shaders; +using SomeChartsUiAvalonia.controls.gl; +using SomeChartsUiAvalonia.utils; + +namespace SomeChartsAvaloniaExamples.elements; + +public static class ScatterChartExample { + // point count + // ScatterChart handle at most 15000 points + private const int _count = 10_000; + private const float _bounds = 10_000; + private const float _pointSizeMul = 80; + private const float _pointSizeAdd = 10; + private static Random _rnd = new(); + + public static void Run() { + AvaloniaRunUtils.RunAfterStart(AddElements); + AvaloniaRunUtils.RunAvalonia(); + } + + private static void AddElements() { + AvaloniaGlChartsCanvas canvas = AvaloniaRunUtils.AddGlCanvas(); + + // generate random data + (float3[] points, indexedColor[] colors, ScatterShape[] shapes) = GenerateRandomPoints(); + + IChartData pointsSrc = new ArrayChartData(points); + IChartData colorsSrc = new ArrayChartData(colors); + IChartData shapesSrc = new ArrayChartData(shapes); + + // load shapes shader, so it will render triangles and circles + // by default it uses basic shader, which render only quads + // disable depth test for transparency (required by shape shader) + Material mat = new(GlShaders.shapes); + mat.depthTest = false; + + ScatterChart pie = new(canvas.canvas) { + // values using float3 + // x,y - point position + // z - point size + values = pointsSrc, + colors = colorsSrc, + + // you can skip shapes, because it's using default value (circles) + shapes = shapesSrc, + isDynamic = true, + + // set scale, so bound will render properly + // you can disable setting drawBounds to false + scale = _bounds, + material = mat, + }; + canvas.AddElement(pie); + } + + private static (float3[] points, indexedColor[] colors, ScatterShape[] shapes) GenerateRandomPoints() { + float3[] points = new float3[_count]; + indexedColor[] colors = new indexedColor[_count]; + ScatterShape[] shapes = new ScatterShape[_count]; + + for (int i = 0; i < _count; i++) { + float x = RndPosX(); + float y = RndPosY(); + float s = RndSize(); + indexedColor col = x > _bounds * .5f ? theme.good_ind : theme.bad_ind; + ScatterShape shape = y > _bounds * .5f ? ScatterShape.circle : ScatterShape.triangle; + + points[i] = new(x, y, s); + colors[i] = col; + shapes[i] = shape; + } + + return (points, colors, shapes); + } + + private static float RndPosX() => _rnd.NextSingle() * _bounds; + private static float RndPosY() => _rnd.NextSingle() * _bounds; + private static float RndSize() => _rnd.NextSingle() * _pointSizeMul + _pointSizeAdd; +} \ No newline at end of file diff --git a/SomeChartsUi/src/elements/charts/scatter/ScatterChart.cs b/SomeChartsUi/src/elements/charts/scatter/ScatterChart.cs index fbee109..5a490ca 100644 --- a/SomeChartsUi/src/elements/charts/scatter/ScatterChart.cs +++ b/SomeChartsUi/src/elements/charts/scatter/ScatterChart.cs @@ -5,6 +5,7 @@ using SomeChartsUi.themes.colors; using SomeChartsUi.themes.themes; using SomeChartsUi.ui.canvas; using SomeChartsUi.ui.elements; +using SomeChartsUi.utils.mesh; namespace SomeChartsUi.elements.charts.scatter; @@ -13,14 +14,18 @@ public class ScatterChart : RenderableBase { public IChartData values; public IChartData colors; - public IChartData? shapes; + public IChartData shapes = new ConstChartData(ScatterShape.circle); public indexedColor boundsColor = theme.default2_ind; public float2 scale = 1000; public bool drawBounds = true; - public ScatterChart(ChartsCanvas owner) : base(owner) { } + private Mesh noTextureMesh; + + public ScatterChart(ChartsCanvas owner) : base(owner) { + noTextureMesh = owner.factory.CreateMesh(); + } protected override unsafe void GenerateMesh() { mesh!.Clear(); @@ -31,12 +36,14 @@ public class ScatterChart : RenderableBase { int bufferLen = math.min(_bufferSize, len); float3* bufferValues = stackalloc float3[bufferLen]; indexedColor* bufferColors = stackalloc indexedColor[bufferLen]; + ScatterShape* bufferShapes = stackalloc ScatterShape[bufferLen]; - int vCount = (bufferLen + 4) * 4; - int iCount = (bufferLen + 4) * 6; + int vCount = bufferLen * 4; + int iCount = bufferLen * 6; values.GetValues(0, bufferLen, 0, bufferValues); colors.GetValues(0, bufferLen, 0, bufferColors); + shapes.GetValues(0, bufferLen, 0, bufferShapes); mesh.vertices.EnsureCapacity(vCount); mesh.indexes.EnsureCapacity(iCount); @@ -44,6 +51,15 @@ public class ScatterChart : RenderableBase { for (int i = 0; i < bufferLen; i++) { float3 element = bufferValues[i]; color col = bufferColors[i].GetColor(); + ScatterShape shape = bufferShapes[i]; + + rect uvs = new(0, 0, 1, 1); + uvs.left = shape switch { + ScatterShape.circle => 0, + ScatterShape.triangle => 1, + ScatterShape.quad => 2, + _ => throw new ArgumentOutOfRangeException() + }; float s = element.z * .5f; mesh.AddRect( @@ -51,17 +67,31 @@ public class ScatterChart : RenderableBase { new(element.x - s, element.y + s), new(element.x + s, element.y + s), new(element.x + s, element.y - s), - col ); - } - - if (drawBounds) { - float thickness = 10; - AddLine(mesh, new(0,0), new(0,scale.y), thickness, boundsColor.GetColor()); - AddLine(mesh, new(0,scale.y), new(scale.x,scale.y), thickness, boundsColor.GetColor()); - AddLine(mesh, new(scale.x,scale.y), new(scale.x,0), thickness, boundsColor.GetColor()); - AddLine(mesh, new(scale.x,0), new(0,0), thickness, boundsColor.GetColor()); + col, + uvs); } mesh.OnModified(); } + + protected override void OnFrequentUpdate() { + noTextureMesh.Clear(); + noTextureMesh.vertices.EnsureCapacity(4 * 4); + noTextureMesh.indexes.EnsureCapacity(4 * 6); + + if (drawBounds) { + float thickness = 2 / canvas.transform.scale.animatedValue.avg; + AddLine(noTextureMesh, new(0,0), new(0,scale.y), thickness, boundsColor.GetColor()); + AddLine(noTextureMesh, new(0,scale.y), new(scale.x,scale.y), thickness, boundsColor.GetColor()); + AddLine(noTextureMesh, new(scale.x,scale.y), new(scale.x,0), thickness, boundsColor.GetColor()); + AddLine(noTextureMesh, new(scale.x,0), new(0,0), thickness, boundsColor.GetColor()); + } + + noTextureMesh.OnModified(); + } + + protected override void AfterDraw() { + base.AfterDraw(); + DrawMesh(null, noTextureMesh); + } } \ No newline at end of file diff --git a/SomeChartsUi/src/ui/elements/RenderableBase.cs b/SomeChartsUi/src/ui/elements/RenderableBase.cs index e044195..49f10fa 100644 --- a/SomeChartsUi/src/ui/elements/RenderableBase.cs +++ b/SomeChartsUi/src/ui/elements/RenderableBase.cs @@ -22,6 +22,9 @@ public abstract partial class RenderableBase { /// frame skip on dynamic update public int updateFrameSkip = 8; + + /// frame skip on dynamic update + public int updateRareFrameSkip = 64; protected int framesCount; /// material of mesh

if null, renderer will use basic material
@@ -47,6 +50,8 @@ public abstract partial class RenderableBase { public void Render() { framesCount++; beforeRender(); + OnFrequentUpdate(); + if (framesCount % (updateRareFrameSkip + 1) == 0) OnRareUpdate(); if (CheckMeshForUpdate()) { GenerateMesh(); isDirty = false; @@ -63,6 +68,12 @@ public abstract partial class RenderableBase { /// called every frame after render

usable for rendering multiple meshes, like
protected virtual void AfterDraw() { } + /// called every frame after beforeRender() + protected virtual void OnFrequentUpdate() {} + + /// called after OnFrequentUpdate and before GenerateMesh dynamically, using updateRareFrameSkip + protected virtual void OnRareUpdate() {} + protected virtual void Destroy() { mesh?.Dispose(); mesh = null; diff --git a/SomeChartsUi/src/ui/elements/RenderableBaseUtils.cs b/SomeChartsUi/src/ui/elements/RenderableBaseUtils.cs index c0f7b92..0dd89bb 100644 --- a/SomeChartsUi/src/ui/elements/RenderableBaseUtils.cs +++ b/SomeChartsUi/src/ui/elements/RenderableBaseUtils.cs @@ -5,6 +5,7 @@ using MathStuff.vectors; using SomeChartsUi.data; using SomeChartsUi.elements; using SomeChartsUi.ui.canvas; +using SomeChartsUi.utils.mesh; using SomeChartsUi.utils.shaders; namespace SomeChartsUi.ui.elements; @@ -22,9 +23,8 @@ public abstract partial class RenderableBase { // protected void DrawVertices(float2[] points, float2[]? uvs, color[]? colors, ushort[] indexes) => // renderer.backend.DrawMesh(points, uvs, colors, indexes, transform.Get(this)); - protected void DrawMesh(Material? material) { - canvas.renderer.backend.DrawMesh(mesh!, material, transform); - } + protected void DrawMesh(Material? mat) => DrawMesh(mat, mesh!); + protected void DrawMesh(Material? mat, Mesh m) => canvas.renderer.backend.DrawMesh(m, mat, transform); // protected void DrawText(string txt, float2 pos, color col, FontData font, float scale = 12) => // renderer.backend.DrawText(txt, col, font, transform + new RenderableTransform(pos, scale, float3.zero)); diff --git a/SomeChartsUi/src/utils/mesh/Mesh.cs b/SomeChartsUi/src/utils/mesh/Mesh.cs index 01c124d..bd1c227 100644 --- a/SomeChartsUi/src/utils/mesh/Mesh.cs +++ b/SomeChartsUi/src/utils/mesh/Mesh.cs @@ -63,13 +63,21 @@ public class Mesh : IDisposable { public void AddVertex(Vertex v) => vertices.Add(v); public void AddIndex(int v) => indexes.Add((ushort) v); - /// add quadrilateral to mesh (vertices and indices)

use front normal and 0-1 uv coordinates
+ /// add quadrilateral to mesh (vertices and indices)

using front normal and 0-1 uv coordinates
public void AddRect(float3 p0, float3 p1, float3 p2, float3 p3, color c0) => AddRect( new(p0, float3.front, new(0,0), c0), new(p1, float3.front, new(0,1), c0), new(p2, float3.front, new(1,1), c0), new(p3, float3.front, new(1,0), c0) ); + + /// add quadrilateral to mesh (vertices and indices)

using front normal
+ public void AddRect(float3 p0, float3 p1, float3 p2, float3 p3, color c0, rect uvs) => AddRect( + new(p0, float3.front, uvs.leftBottom, c0), + new(p1, float3.front, uvs.leftTop, c0), + new(p2, float3.front, uvs.rightTop, c0), + new(p3, float3.front, uvs.rightBottom, c0) + ); /// add quadrilateral to mesh (vertices and indices) public void AddRect(Vertex p0, Vertex p1, Vertex p2, Vertex p3) { diff --git a/SomeChartsUiAvalonia/SomeChartsUiAvalonia.csproj b/SomeChartsUiAvalonia/SomeChartsUiAvalonia.csproj index 745c89c..c570125 100644 --- a/SomeChartsUiAvalonia/SomeChartsUiAvalonia.csproj +++ b/SomeChartsUiAvalonia/SomeChartsUiAvalonia.csproj @@ -54,6 +54,12 @@ Always + + Always + + + Always +
diff --git a/SomeChartsUiAvalonia/data/shaders/shapes.frag b/SomeChartsUiAvalonia/data/shaders/shapes.frag new file mode 100644 index 0000000..70c6abe --- /dev/null +++ b/SomeChartsUiAvalonia/data/shaders/shapes.frag @@ -0,0 +1,50 @@ +// PROCESS FRAGMENT + +// inputs +in vec3 fragPos; +in vec3 fragNormal; +in vec2 fragUv; +in vec4 fragCol; + +// outputs +out vec4 outFragColor; + +// some code grabbed from https://thebookofshaders.com/07/ +#define PI 3.14159265359 +#define TWO_PI 6.28318530718 + +float circle(vec2 coords, float d) { + const float radius = .9; + float sm = radius * 4 * d; + vec2 dist = coords - vec2(.5); + return 1.0 - smoothstep(radius - sm, radius + sm, dot(dist, dist) * 4); +} + +float triangle(vec2 coords, float d) { + float sm = 2.5 * d; + coords = coords * 2.0 - 1.0; + + const float n = 3; + float angle = atan(coords.x, coords.y) + PI; + float radius = TWO_PI / n; + float dist = cos(floor(.5 + angle / radius) * radius - angle) * length(coords); + return 1.0 - smoothstep(.5 - sm, .5 + sm, dist); +} + +float quad(vec2 coords, float d) { + float sm = 2.5 * d; + coords = coords * 2.0 - 1.0; + float dist = max(abs(coords.x), abs(coords.y)); + return 1.0 - smoothstep(.8 - sm, .8 + sm, dist); +} + +float sample(vec2 coords) { + float d = dFdx(coords.x); + if (coords.x <= 1.0) return circle(coords, d); + if (coords.x <= 2.0) return triangle(coords - vec2(1,0), d); + return quad(coords - vec2(2,0), d); +} + +void main() { + gl_FragColor = sample(fragUv) * fragCol; +} \ No newline at end of file diff --git a/SomeChartsUiAvalonia/data/shaders/shapes.vert b/SomeChartsUiAvalonia/data/shaders/shapes.vert new file mode 100644 index 0000000..edafe60 --- /dev/null +++ b/SomeChartsUiAvalonia/data/shaders/shapes.vert @@ -0,0 +1,27 @@ +// PROCESS VERTEX + +// attributes +in vec3 pos; +in vec3 normal; +in vec2 uv; +in vec4 col; + +// uniforms +uniform mat4 mvp; + +// output +out vec3 fragPos; +out vec3 fragNormal; +out vec2 fragUv; +out vec4 fragCol; + +void main() { + float scale = 1.0; + vec3 scaledPos = pos * scale; + + gl_Position = mvp * vec4(scaledPos, 1.0); + fragPos = gl_Position.xyz; + fragUv = uv; + fragCol = col; + fragNormal = normal; +} \ No newline at end of file diff --git a/SomeChartsUiAvalonia/src/controls/gl/AvaloniaGlChartsCanvas.cs b/SomeChartsUiAvalonia/src/controls/gl/AvaloniaGlChartsCanvas.cs index a0dd847..3aa00ed 100644 --- a/SomeChartsUiAvalonia/src/controls/gl/AvaloniaGlChartsCanvas.cs +++ b/SomeChartsUiAvalonia/src/controls/gl/AvaloniaGlChartsCanvas.cs @@ -75,7 +75,6 @@ public class AvaloniaGlChartsCanvas : CustomGlControlBase { } protected override void OnOpenGlRender(GlInterface gl, int framebuffer) { - gl.Enable(GL_DEPTH_TEST); gl.Enable(GL_MULTISAMPLE); gl.Enable(GL_BLEND); diff --git a/SomeChartsUiAvalonia/src/utils/GlShaders.cs b/SomeChartsUiAvalonia/src/utils/GlShaders.cs index 7fa5f07..9373602 100644 --- a/SomeChartsUiAvalonia/src/utils/GlShaders.cs +++ b/SomeChartsUiAvalonia/src/utils/GlShaders.cs @@ -3,6 +3,7 @@ namespace SomeChartsUiAvalonia.utils; public static class GlShaders { public static readonly GlShader basic = GlShader.LoadFrom("basic", "data/shaders/basic.vert", "data/shaders/basic.frag"); public static readonly GlShader basicTextured = GlShader.LoadFrom("basicTextured", "data/shaders/basicTextured.vert", "data/shaders/basicTextured.frag"); + public static readonly GlShader shapes = GlShader.LoadFrom("shapes", "data/shaders/shapes.vert", "data/shaders/shapes.frag"); public static readonly GlShader fxaa = GlShader.LoadFrom("fxaa", "data/shaders/fxaa.vert", "data/shaders/fxaa.frag"); public static readonly GlShader bloom = GlShader.LoadFrom("bloom", "data/shaders/bloom.vert", "data/shaders/bloom.frag"); public static readonly GlShader basicText = GlShader.LoadFrom("text", "data/shaders/text.vert", "data/shaders/text.frag"); diff --git a/SomeChartsUiAvalonia/src/utils/GlTexture.cs b/SomeChartsUiAvalonia/src/utils/GlTexture.cs index 77f3000..65c1e82 100644 --- a/SomeChartsUiAvalonia/src/utils/GlTexture.cs +++ b/SomeChartsUiAvalonia/src/utils/GlTexture.cs @@ -52,14 +52,14 @@ public class GlTexture : Texture, IDisposable { using ILockedFramebuffer lockedFramebuffer = bitmap!.Lock(); IntPtr ptr = lockedFramebuffer.Address; - (int type, int format) = lockedFramebuffer.Format switch { - PixelFormat.Rgb565 => (GlConsts.GL_UNSIGNED_SHORT_5_6_5, GlConsts.GL_RGB), - PixelFormat.Rgba8888 => (GlConsts.GL_UNSIGNED_INT_8_8_8_8, GlConsts.GL_RGBA), - PixelFormat.Bgra8888 => (GlConsts.GL_UNSIGNED_INT_8_8_8_8, GlConsts.GL_BGRA), + (int type, int format, int internalFormat) = lockedFramebuffer.Format switch { + PixelFormat.Rgb565 => (GlConsts.GL_UNSIGNED_SHORT_5_6_5, GlConsts.GL_RGB, GlConsts.GL_RGB), + PixelFormat.Rgba8888 => (GlConsts.GL_UNSIGNED_INT_8_8_8_8, GlConsts.GL_RGBA, GlConsts.GL_RGBA), + PixelFormat.Bgra8888 => (GlConsts.GL_UNSIGNED_INT_8_8_8_8, GlConsts.GL_BGRA, GlConsts.GL_RGBA), _ => throw new ArgumentOutOfRangeException() }; - GlInfo.gl.TexImage2D(GlConsts.GL_TEXTURE_2D, 0, GlConsts.GL_RGB, (int)bitmap.Size.Width, (int)bitmap.Size.Height, 0, format, type, ptr); + GlInfo.gl.TexImage2D(GlConsts.GL_TEXTURE_2D, 0, internalFormat, (int) size.x, (int) size.y, 0, format, type, ptr); } public void Bind() {