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() {